Class: Zemu::InteractiveInstance

Inherits:
Object
  • Object
show all
Defined in:
lib/zemu/interactive.rb

Overview

An interactive instance of a Zemu emulator. Wraps a Zemu::Instance to allow for user input and debugging.

Instance Method Summary collapse

Constructor Details

#initialize(instance, options = {}) ⇒ InteractiveInstance

Constructor.

Create a new interactive wrapper for the given instance. The options hash allows the user to configure the behaviour of the interactive instance:

:print_serial => true if serial input/output should be logged
                 to the emulator window.


12
13
14
15
16
17
18
19
20
21
22
# File 'lib/zemu/interactive.rb', line 12

def initialize(instance, options = {})
    @print_serial = options[:print_serial]
    @trace = []

    @instance = instance

    @symbol_table = {}

    @master, @slave = PTY.open
    log "Opened PTY at #{@slave.path}"
end

Instance Method Details

#add_breakpoint(addr_str) ⇒ Object

Add a breakpoint at the address given by the string.



217
218
219
# File 'lib/zemu/interactive.rb', line 217

def add_breakpoint(addr_str)
    @instance.break(addr_str.to_i(16), :program)
end

#closeObject

Close the interactive wrapper



30
31
32
33
34
# File 'lib/zemu/interactive.rb', line 30

def close
    @master.close
    @slave.close
    @instance.quit
end

#continue(cycles = -1)) ⇒ Object

Continue for *up to* the given number of cycles. Fewer cycles may be executed, depending on the behaviour of the processor.



172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/zemu/interactive.rb', line 172

def continue(cycles=-1)
    if cycles == 0
        log "Invalid value: #{cycles}"
        return
    end

    # Continue executing instruction-by-instruction.
    # Process IO in-between.
    cycles_left = cycles
    actual_cycles = 0

    serial_count = @instance.serial_delay.to_f

    while ((cycles == -1) || (cycles_left > 0))
        old_pc = r16("PC")

        if (serial_count >= @instance.serial_delay)
            process_serial
            serial_count = 0.0
        end

        cycles_done = @instance.continue(1)
        cycles_left -= cycles_done
        actual_cycles += cycles_done

        # Get elapsed time and calculate padding time to match clock speed.
        if @instance.clock_speed > 0
            execution_time = cycles_done * (1.0/@instance.clock_speed)
            serial_count += execution_time
        end

        # Have we hit a breakpoint or HALT instruction?
        if @instance.break?
            log "Hit breakpoint at #{r16("PC")}."
            break
        elsif @instance.halted?
            log "Executed HALT instruction at #{old_pc}."
            break
        end
    end

    log "Executed for #{actual_cycles} cycles."
end

#get_symbol(value) ⇒ Object

Gets the symbol associated with the given value. If no matching symbol, gives offset from previous symbol.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/zemu/interactive.rb', line 139

def get_symbol(value)
    syms = nil
    addr = value
    while addr > 0 do
        syms = @symbol_table[addr]
        break unless syms.nil?
        addr -= 1
    end

    sym = if syms.nil? then nil else syms[0] end

    sym_str = "<#{if sym.nil? then 'undefined' else sym.label end}#{if addr == value then '' else "+#{value-addr}" end}>"

    return sym_str
end

#hilo(hi, lo) ⇒ Object

Concatenates two 8-bit values, in big-endian format.



166
167
168
# File 'lib/zemu/interactive.rb', line 166

def hilo(hi, lo)
    return (hi << 8) | lo
end

#load_map(path) ⇒ Object

Loads a MAP file from the given path.



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/zemu/interactive.rb', line 244

def load_map(path)
    if path.nil?
        log "No path specified."
        return
    end

    unless File.exist?(path.to_s)
        log "Map file '#{path}' does not exist."
        return
    end

    if File.directory?(path.to_s)
        log "Cannot open '#{path}': it is a directory."
        return
    end

    syms = {}
    begin
        syms.merge! Debug.load_map(path.to_s).hash
    rescue ArgumentError => e
        log "Error loading map file: #{e.message}"
        syms.clear
    end

    @symbol_table.merge! syms
end

#log(message) ⇒ Object

Logs a message to the user output.



25
26
27
# File 'lib/zemu/interactive.rb', line 25

def log(message)
    STDOUT.puts "    " + message
end

#memory(address, size = "1") ⇒ Object

Dump an amount of memory.



222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/zemu/interactive.rb', line 222

def memory(address, size="1")
    if address.nil?
        log "Expected an address, got #{address}."
        return
    end

    if (address.to_i(16) < 1 || address.to_i(16) > 0xffff)
        log "Invalid address: 0x%04x" % address.to_i(16)
        return
    end
    
    (address.to_i(16)...address.to_i(16) + size.to_i(16)).each do |a|
        m = @instance.memory(a)
        if (m < 32 || m > 126)
            log "%04x: %02x    ." % [a, m]
        else
            log ("%04x: %02x    " % [a, m]) + m.chr("UTF-8")
        end
    end
end

#process_serialObject

Process serial input/output via the TTY.



272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/zemu/interactive.rb', line 272

def process_serial
    # Read/write serial.
    # Get the strings to be input/output.
    input = ""
    ready = IO.select([@master], [], [], 0)
    unless ready.nil? || ready.empty?
        input = @master.read(1)
    end

    output = @instance.serial_gets(1)

    unless input.empty?
        @instance.serial_puts input
        log "Serial in: #{input} ($#{input.ord.to_s(16)})" if @print_serial
    end

    unless output.empty?
        @master.write output
        log "Serial out: #{output} ($#{output.ord.to_s(16)})" if @print_serial
    end
end

#r(reg) ⇒ Object

Returns a particular 8-bit register value.



156
157
158
# File 'lib/zemu/interactive.rb', line 156

def r(reg)
    return "0x%02x" % @instance.registers[reg]
end

#r16(reg) ⇒ Object

Returns a particular 16-bit register value.



161
162
163
# File 'lib/zemu/interactive.rb', line 161

def r16(reg)
    return "0x%04x" % @instance.registers[reg]
end

#register_16(r) ⇒ Object

Displays the value of a 16-bit register.



124
125
126
127
128
# File 'lib/zemu/interactive.rb', line 124

def register_16(r)
    value = @instance.registers[r]

    log "#{r}: #{r16(r)} (#{get_symbol(value)})"
end

#registersObject

Outputs a table giving the current values of the instance’s registers. For the 16-bit registers (BC, DE, HL, IX, IY, SP, PC), attempts to identify the symbol to which they point.



108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/zemu/interactive.rb', line 108

def registers
    log "A:  #{r("A")} F: #{r("F")}"

    registers_gp('B', 'C')
    registers_gp('D', 'E')
    registers_gp('H', 'L')

    log ""

    register_16("IX")
    register_16("IY")
    register_16("SP")
    register_16("PC")
end

#registers_gp(hi, lo) ⇒ Object

Displays the value of a general-purpose 16-bit register pair.



131
132
133
134
135
# File 'lib/zemu/interactive.rb', line 131

def registers_gp(hi, lo)
    value = hilo(@instance.registers[hi], @instance.registers[lo])

    log "#{hi}:  #{r(hi)} #{lo}: #{r(lo)} (#{get_symbol(value)})"
end

#runObject

Run the interactive emulator until the user exits.



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/zemu/interactive.rb', line 37

def run
    quit = false

    until quit
        print "ZEMU> "
        # Get a command from the user.
        cmd = STDIN.gets.split

        if cmd[0] == "quit"
            quit = true

        elsif cmd[0] == "continue"
            if cmd[1].nil?
                continue
            else
                continue(cmd[1].to_i)
            end

        elsif cmd[0] == "step"
            continue(1)

        elsif cmd[0] == "registers"
            registers

        elsif cmd[0] == "break"
            add_breakpoint(cmd[1])

        elsif cmd[0] == "memory"
            if cmd[2].nil?
                memory(cmd[1])
            else
                memory(cmd[1], cmd[2])
            end

        elsif cmd[0] == "map"
            load_map(cmd[1])

        elsif cmd[0] == "trace"
            trace()

        elsif cmd[0] == "help"
            log "Available commands:"
            log "    continue [<n>]     - Continue execution for <n> cycles"
            log "    step               - Step over a single instruction"
            log "    registers          - View register contents"
            log "    memory <a> [<n>]   - View <n> bytes of memory, starting at address <a>."
            log "                         <n> defaults to 1 if omitted."
            log "    map <path>         - Load symbols from map file at <path>"
            log "    break  <a>         - Set a breakpoint at the given address <a>."
            log "    quit               - End this emulator instance."

        else
            log "Invalid command. Type 'help' for available commands."
            
        end
    end

    close
end

#traceObject

Print trace for the emulator instance (last 200 addresses visited).



99
100
101
102
103
# File 'lib/zemu/interactive.rb', line 99

def trace
    @trace.each do |t|
        puts "%04x" % t
    end
end