mos6502-workbench 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +189 -0
- data/bin/assemble +122 -0
- data/bin/disassemble +58 -0
- data/bin/run_assembled_program +80 -0
- data/bin/run_klaus_functional_test +19 -0
- data/examples/README.md +147 -0
- data/examples/branching.asm +16 -0
- data/examples/countdown.asm +17 -0
- data/examples/labels_and_data.asm +28 -0
- data/examples/primes.asm +61 -0
- data/examples/simple_machine.rb +25 -0
- data/examples/traced_machine.rb +55 -0
- data/lib/mos6502/workbench/assembler.rb +725 -0
- data/lib/mos6502/workbench/bus.rb +264 -0
- data/lib/mos6502/workbench/cpu.rb +1292 -0
- data/lib/mos6502/workbench/device.rb +64 -0
- data/lib/mos6502/workbench/disassembler.rb +234 -0
- data/lib/mos6502/workbench/flags.rb +74 -0
- data/lib/mos6502/workbench/intel_hex.rb +116 -0
- data/lib/mos6502/workbench/machine.rb +140 -0
- data/lib/mos6502/workbench/memory.rb +159 -0
- data/lib/mos6502/workbench/registers.rb +19 -0
- data/lib/mos6502/workbench/tui.rb +537 -0
- data/lib/mos6502/workbench/version.rb +9 -0
- data/lib/mos6502/workbench.rb +12 -0
- metadata +126 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOS6502
|
|
4
|
+
# Base class for memory-mapped peripherals and storage devices.
|
|
5
|
+
#
|
|
6
|
+
# Device implementations are expected to respond to:
|
|
7
|
+
#
|
|
8
|
+
# - {#read_byte} for bus reads
|
|
9
|
+
# - {#write_byte} for bus writes
|
|
10
|
+
# - {#reset} for power-on style reinitialisation
|
|
11
|
+
# - {#tick} for advancing time in cycle-driven machines
|
|
12
|
+
#
|
|
13
|
+
# Subclasses can expose an optional `size` reader when the bus should validate
|
|
14
|
+
# the mapped range against the device capacity.
|
|
15
|
+
class Device
|
|
16
|
+
# Creates a new device.
|
|
17
|
+
#
|
|
18
|
+
# The base implementation exists so subclasses can call `super()` cleanly in
|
|
19
|
+
# their own initializers.
|
|
20
|
+
#
|
|
21
|
+
# @return [void]
|
|
22
|
+
def initialize; end
|
|
23
|
+
|
|
24
|
+
# Resets the device to its power-on state.
|
|
25
|
+
#
|
|
26
|
+
# The base implementation is a no-op so simple devices only need to override
|
|
27
|
+
# it when they maintain internal state.
|
|
28
|
+
#
|
|
29
|
+
# @return [Device] the device
|
|
30
|
+
def reset
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Advances the device clock by a number of CPU cycles.
|
|
35
|
+
#
|
|
36
|
+
# The base implementation is a no-op. Timed peripherals can override this to
|
|
37
|
+
# model timers, serial shifting, video scan-out, or similar behaviour.
|
|
38
|
+
#
|
|
39
|
+
# @param _cycles [Integer] the number of CPU cycles elapsed
|
|
40
|
+
# @return [Device] the device
|
|
41
|
+
def tick(_cycles)
|
|
42
|
+
self
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Reads a byte from the device-local address space.
|
|
46
|
+
#
|
|
47
|
+
# @param _address [Integer] the device-local byte offset
|
|
48
|
+
# @return [Integer] the device byte value
|
|
49
|
+
# @raise [NotImplementedError] unless a subclass implements the method
|
|
50
|
+
def read_byte(_address)
|
|
51
|
+
raise NotImplementedError, "#{self.class} must implement #read_byte"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Writes a byte into the device-local address space.
|
|
55
|
+
#
|
|
56
|
+
# @param _address [Integer] the device-local byte offset
|
|
57
|
+
# @param _value [Integer] the value to store
|
|
58
|
+
# @return [Integer] the stored byte value
|
|
59
|
+
# @raise [NotImplementedError] unless a subclass implements the method
|
|
60
|
+
def write_byte(_address, _value)
|
|
61
|
+
raise NotImplementedError, "#{self.class} must implement #write_byte"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOS6502
|
|
4
|
+
# Raised when bytes cannot be disassembled because the decode metadata is invalid.
|
|
5
|
+
class DisassemblyError < StandardError; end
|
|
6
|
+
|
|
7
|
+
# A small disassembler for the implemented NMOS 6502 opcode table.
|
|
8
|
+
#
|
|
9
|
+
# The disassembler intentionally derives its decode information from
|
|
10
|
+
# {CPU::OPCODES} so the emulator and listing output stay in sync.
|
|
11
|
+
class Disassembler
|
|
12
|
+
# A single decoded instruction or raw data byte.
|
|
13
|
+
Line = Struct.new(
|
|
14
|
+
:address,
|
|
15
|
+
:bytes,
|
|
16
|
+
:opcode,
|
|
17
|
+
:mnemonic,
|
|
18
|
+
:mode,
|
|
19
|
+
:operand,
|
|
20
|
+
:byte_size,
|
|
21
|
+
:target_address,
|
|
22
|
+
:text
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# The number of bytes occupied by each supported addressing mode.
|
|
26
|
+
#
|
|
27
|
+
# @return [Hash{Symbol => Integer}]
|
|
28
|
+
INSTRUCTION_SIZES = {
|
|
29
|
+
implied: 1,
|
|
30
|
+
accumulator: 1,
|
|
31
|
+
immediate: 2,
|
|
32
|
+
zero_page: 2,
|
|
33
|
+
zero_page_x: 2,
|
|
34
|
+
zero_page_y: 2,
|
|
35
|
+
relative: 2,
|
|
36
|
+
indirect_x: 2,
|
|
37
|
+
indirect_y: 2,
|
|
38
|
+
absolute: 3,
|
|
39
|
+
absolute_x: 3,
|
|
40
|
+
absolute_y: 3,
|
|
41
|
+
indirect: 3
|
|
42
|
+
}.freeze
|
|
43
|
+
|
|
44
|
+
# @param opcodes [Hash{Integer => Array<Symbol>}] the opcode table to use
|
|
45
|
+
def initialize(opcodes: CPU::OPCODES)
|
|
46
|
+
@opcodes = opcodes
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Disassembles a byte stream into line objects.
|
|
50
|
+
#
|
|
51
|
+
# Unknown opcodes are rendered as `.byte` directives so arbitrary binaries can
|
|
52
|
+
# still be listed without raising.
|
|
53
|
+
#
|
|
54
|
+
# @param bytes [String, #to_a] the raw bytes to decode
|
|
55
|
+
# @param start_address [Integer] the load address of the first byte
|
|
56
|
+
# @param max_instructions [Integer, nil] an optional instruction limit
|
|
57
|
+
# @return [Array<Line>] the decoded listing
|
|
58
|
+
def disassemble(bytes, start_address: CPU::DEFAULT_LOAD_ADDRESS, max_instructions: nil)
|
|
59
|
+
stream = bytes.is_a?(String) ? bytes.bytes : bytes.to_a
|
|
60
|
+
lines = []
|
|
61
|
+
offset = 0
|
|
62
|
+
|
|
63
|
+
while offset < stream.length && within_instruction_budget?(lines.length, max_instructions)
|
|
64
|
+
address = (start_address + offset) & 0xffff
|
|
65
|
+
opcode = stream[offset]
|
|
66
|
+
mnemonic, mode = @opcodes[opcode]
|
|
67
|
+
|
|
68
|
+
if mnemonic.nil?
|
|
69
|
+
lines << data_line(address, opcode)
|
|
70
|
+
offset += 1
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
size = instruction_size(mode)
|
|
75
|
+
instruction_bytes = stream.slice(offset, size) || []
|
|
76
|
+
operand_bytes = instruction_bytes.drop(1)
|
|
77
|
+
operand, target_address = format_operand(mode, operand_bytes, address)
|
|
78
|
+
|
|
79
|
+
lines << Line.new(
|
|
80
|
+
address:,
|
|
81
|
+
bytes: instruction_bytes,
|
|
82
|
+
opcode:,
|
|
83
|
+
mnemonic:,
|
|
84
|
+
mode:,
|
|
85
|
+
operand:,
|
|
86
|
+
byte_size: size,
|
|
87
|
+
target_address:,
|
|
88
|
+
text: join_instruction_text(mnemonic, operand)
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
offset += size
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
lines
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Formats a decoded listing into a column-aligned string.
|
|
98
|
+
#
|
|
99
|
+
# @param lines [Array<Line>] the lines to render
|
|
100
|
+
# @return [String] the formatted listing text
|
|
101
|
+
def format_listing(lines)
|
|
102
|
+
lines.map do |line|
|
|
103
|
+
bytes = line.bytes.map { |byte| format('%02X', byte) }.join(' ').ljust(8)
|
|
104
|
+
"$#{format('%04X', line.address)}: #{bytes} #{line.text}"
|
|
105
|
+
end.join("\n")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
110
|
+
# @param count [Integer] how many instructions have already been emitted
|
|
111
|
+
# @param max_instructions [Integer, nil] the optional instruction limit
|
|
112
|
+
# @return [Boolean] whether another instruction may be decoded
|
|
113
|
+
def within_instruction_budget?(count, max_instructions)
|
|
114
|
+
max_instructions.nil? || count < max_instructions
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Builds a `.byte` line for an unknown opcode.
|
|
118
|
+
#
|
|
119
|
+
# @param address [Integer] where the byte lives
|
|
120
|
+
# @param opcode [Integer] the unknown opcode value
|
|
121
|
+
# @return [Line] the formatted data line
|
|
122
|
+
def data_line(address, opcode)
|
|
123
|
+
operand = format('$%02X', opcode)
|
|
124
|
+
Line.new(
|
|
125
|
+
address:,
|
|
126
|
+
bytes: [opcode],
|
|
127
|
+
opcode:,
|
|
128
|
+
mnemonic: nil,
|
|
129
|
+
mode: nil,
|
|
130
|
+
operand:,
|
|
131
|
+
byte_size: 1,
|
|
132
|
+
target_address: nil,
|
|
133
|
+
text: ".byte #{operand}"
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Joins mnemonic and operand text into a single listing field.
|
|
138
|
+
#
|
|
139
|
+
# @param mnemonic [Symbol] the decoded mnemonic
|
|
140
|
+
# @param operand [String, nil] the formatted operand
|
|
141
|
+
# @return [String] the combined text
|
|
142
|
+
def join_instruction_text(mnemonic, operand)
|
|
143
|
+
operand ? "#{mnemonic} #{operand}" : mnemonic.to_s
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Returns the width of the decoded instruction.
|
|
147
|
+
#
|
|
148
|
+
# @param mode [Symbol] the addressing mode
|
|
149
|
+
# @return [Integer] the instruction size in bytes
|
|
150
|
+
# @raise [DisassemblyError] if the mode is unknown
|
|
151
|
+
def instruction_size(mode)
|
|
152
|
+
INSTRUCTION_SIZES.fetch(mode)
|
|
153
|
+
rescue KeyError
|
|
154
|
+
raise DisassemblyError, "Unknown instruction size for #{mode}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Formats an operand for listing output.
|
|
158
|
+
#
|
|
159
|
+
# @param mode [Symbol] the addressing mode
|
|
160
|
+
# @param bytes [Array<Integer>] the operand bytes
|
|
161
|
+
# @param address [Integer] the instruction address
|
|
162
|
+
# @return [Array<(String, Integer, nil)>] the operand text and optional target
|
|
163
|
+
# @raise [DisassemblyError] if the mode cannot be rendered
|
|
164
|
+
def format_operand(mode, bytes, address)
|
|
165
|
+
case mode
|
|
166
|
+
when :implied
|
|
167
|
+
[nil, nil]
|
|
168
|
+
when :accumulator
|
|
169
|
+
['A', nil]
|
|
170
|
+
when :immediate
|
|
171
|
+
["#$#{hex_byte(bytes[0])}", nil]
|
|
172
|
+
when :zero_page
|
|
173
|
+
["$#{hex_byte(bytes[0])}", nil]
|
|
174
|
+
when :zero_page_x
|
|
175
|
+
["$#{hex_byte(bytes[0])}, X", nil]
|
|
176
|
+
when :zero_page_y
|
|
177
|
+
["$#{hex_byte(bytes[0])}, Y", nil]
|
|
178
|
+
when :absolute
|
|
179
|
+
["$#{hex_word(bytes[0], bytes[1])}", nil]
|
|
180
|
+
when :absolute_x
|
|
181
|
+
["$#{hex_word(bytes[0], bytes[1])}, X", nil]
|
|
182
|
+
when :absolute_y
|
|
183
|
+
["$#{hex_word(bytes[0], bytes[1])}, Y", nil]
|
|
184
|
+
when :indirect
|
|
185
|
+
["($#{hex_word(bytes[0], bytes[1])})", nil]
|
|
186
|
+
when :indirect_x
|
|
187
|
+
["($#{hex_byte(bytes[0])}, X)", nil]
|
|
188
|
+
when :indirect_y
|
|
189
|
+
["($#{hex_byte(bytes[0])}), Y", nil]
|
|
190
|
+
when :relative
|
|
191
|
+
target = branch_target(bytes[0], address)
|
|
192
|
+
[target ? format('$%04X', target) : '$????', target]
|
|
193
|
+
else
|
|
194
|
+
raise DisassemblyError, "Cannot format operand for #{mode}"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Formats a byte as uppercase hex, using `??` for missing data.
|
|
199
|
+
#
|
|
200
|
+
# @param value [Integer, nil] the raw byte
|
|
201
|
+
# @return [String] the formatted byte
|
|
202
|
+
def hex_byte(value)
|
|
203
|
+
value.nil? ? '??' : format('%02X', value & 0xff)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Formats a little-endian word as uppercase hex, preserving missing bytes.
|
|
207
|
+
#
|
|
208
|
+
# @param low [Integer, nil] the low byte
|
|
209
|
+
# @param high [Integer, nil] the high byte
|
|
210
|
+
# @return [String] the formatted word
|
|
211
|
+
def hex_word(low, high)
|
|
212
|
+
"#{hex_byte(high)}#{hex_byte(low)}"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Resolves a relative branch target from an 8-bit signed displacement.
|
|
216
|
+
#
|
|
217
|
+
# @param offset [Integer, nil] the raw branch displacement
|
|
218
|
+
# @param address [Integer] the instruction address
|
|
219
|
+
# @return [Integer, nil] the absolute branch target
|
|
220
|
+
def branch_target(offset, address)
|
|
221
|
+
return nil if offset.nil?
|
|
222
|
+
|
|
223
|
+
((address + 2 + signed_byte(offset)) & 0xffff)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Converts an unsigned byte to a signed relative displacement.
|
|
227
|
+
#
|
|
228
|
+
# @param value [Integer] the raw byte
|
|
229
|
+
# @return [Integer] the signed offset
|
|
230
|
+
def signed_byte(value)
|
|
231
|
+
value >= 0x80 ? value - 0x100 : value
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOS6502
|
|
4
|
+
# Status register helpers for the processor flags.
|
|
5
|
+
#
|
|
6
|
+
# The bit order matches the packed 6502 status register:
|
|
7
|
+
# `N V 1 B D I Z C`.
|
|
8
|
+
module Flags
|
|
9
|
+
FLAGS = %w[negative overflow high break decimal interrupt zero carry].freeze
|
|
10
|
+
FLAG_MASKS = {
|
|
11
|
+
'negative' => 0b1000_0000,
|
|
12
|
+
'overflow' => 0b0100_0000,
|
|
13
|
+
'break' => 0b0001_0000,
|
|
14
|
+
'decimal' => 0b0000_1000,
|
|
15
|
+
'interrupt' => 0b0000_0100,
|
|
16
|
+
'zero' => 0b0000_0010,
|
|
17
|
+
'carry' => 0b0000_0001
|
|
18
|
+
}.freeze
|
|
19
|
+
# Clears all mutable processor-status flags.
|
|
20
|
+
#
|
|
21
|
+
# Bit 5 (`high`) is implicit in the packed status register and is therefore
|
|
22
|
+
# not exposed as a mutable Ruby accessor.
|
|
23
|
+
#
|
|
24
|
+
# @return [void]
|
|
25
|
+
def flags_reset
|
|
26
|
+
FLAGS.each { |flag_name| instance_variable_set("@#{flag_name}", false) }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Defines predicate and writer methods for each mutable flag when included.
|
|
30
|
+
#
|
|
31
|
+
# @param base [Class] the including class
|
|
32
|
+
# @return [void]
|
|
33
|
+
def self.included(base)
|
|
34
|
+
FLAGS.each do |flag_name|
|
|
35
|
+
next if flag_name == 'high'
|
|
36
|
+
|
|
37
|
+
base.define_method(:"#{flag_name}?") { instance_variable_get("@#{flag_name}") }
|
|
38
|
+
base.define_method(:"#{flag_name}=") { |value| instance_variable_set("@#{flag_name}", value) }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Packs the current flags into an 8-bit processor-status value.
|
|
43
|
+
#
|
|
44
|
+
# @return [Integer] the encoded processor status register
|
|
45
|
+
def flags_encode
|
|
46
|
+
FLAGS.reduce(0) do |memo, flag_name|
|
|
47
|
+
bit = if flag_name == 'high'
|
|
48
|
+
1
|
|
49
|
+
else
|
|
50
|
+
bit_value(instance_variable_get("@#{flag_name}"))
|
|
51
|
+
end
|
|
52
|
+
(memo << 1) + bit
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Unpacks an 8-bit processor-status value into the individual flags.
|
|
57
|
+
#
|
|
58
|
+
# @param value [Integer] the packed processor status register
|
|
59
|
+
# @return [void]
|
|
60
|
+
def flags_decode(value)
|
|
61
|
+
FLAG_MASKS.each do |flag_name, mask|
|
|
62
|
+
instance_variable_set("@#{flag_name}", value.anybits?(mask))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Converts a Ruby boolean into the bit value used by the status register.
|
|
67
|
+
#
|
|
68
|
+
# @param bool [Boolean] the flag value
|
|
69
|
+
# @return [Integer] `1` when true, otherwise `0`
|
|
70
|
+
def bit_value(bool)
|
|
71
|
+
bool ? 1 : 0
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MOS6502
|
|
4
|
+
# Raised when Intel HEX text is malformed or unsupported.
|
|
5
|
+
class IntelHexError < StandardError; end
|
|
6
|
+
|
|
7
|
+
# A minimal Intel HEX parser and loader for 16-bit 6502 address space.
|
|
8
|
+
#
|
|
9
|
+
# The current implementation supports:
|
|
10
|
+
# - data records (`00`)
|
|
11
|
+
# - end-of-file records (`01`)
|
|
12
|
+
#
|
|
13
|
+
# Extended address records are intentionally rejected for now because the
|
|
14
|
+
# emulator only exposes a 16-bit address bus.
|
|
15
|
+
class IntelHex
|
|
16
|
+
# A single parsed Intel HEX record.
|
|
17
|
+
Record = Struct.new(:address, :record_type, :data)
|
|
18
|
+
|
|
19
|
+
# A parsed Intel HEX document ready to load into a CPU.
|
|
20
|
+
Document = Struct.new(:records) do
|
|
21
|
+
# Returns only the data-bearing records in the document.
|
|
22
|
+
#
|
|
23
|
+
# @return [Array<Record>] the parsed data records
|
|
24
|
+
def data_records
|
|
25
|
+
records.select { |record| record.record_type.zero? }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the lowest data address present in the document.
|
|
29
|
+
#
|
|
30
|
+
# @return [Integer, nil] the first occupied address, if any
|
|
31
|
+
def start_address
|
|
32
|
+
data_records.map(&:address).min
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Loads the parsed records into a CPU.
|
|
36
|
+
#
|
|
37
|
+
# @param cpu [CPU] the destination CPU
|
|
38
|
+
# @param set_reset_vector [Boolean] whether to update the reset vector
|
|
39
|
+
# @return [Integer, nil] the lowest loaded address
|
|
40
|
+
def load_into(cpu, set_reset_vector: true)
|
|
41
|
+
first_address = start_address
|
|
42
|
+
|
|
43
|
+
data_records.each do |record|
|
|
44
|
+
cpu.load(record.data, start_address: record.address, set_reset_vector: false)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
cpu.write_word(CPU::RESET_VECTOR, first_address) if set_reset_vector && first_address
|
|
48
|
+
first_address
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Parses Intel HEX text into a loadable document.
|
|
53
|
+
#
|
|
54
|
+
# @param source [String] the Intel HEX text
|
|
55
|
+
# @return [Document] the parsed document
|
|
56
|
+
def parse(source)
|
|
57
|
+
records = []
|
|
58
|
+
seen_eof = false
|
|
59
|
+
|
|
60
|
+
source.each_line.with_index(1) do |line, line_number|
|
|
61
|
+
stripped = line.strip
|
|
62
|
+
next if stripped.empty?
|
|
63
|
+
|
|
64
|
+
raise IntelHexError, "Unexpected data after EOF record on line #{line_number}" if seen_eof
|
|
65
|
+
|
|
66
|
+
record = parse_record(stripped, line_number)
|
|
67
|
+
records << record
|
|
68
|
+
seen_eof = true if record.record_type == 0x01
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
raise IntelHexError, 'Intel HEX document is missing an EOF record' unless seen_eof
|
|
72
|
+
|
|
73
|
+
Document.new(records:)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Parses a single Intel HEX line and validates its checksum.
|
|
79
|
+
#
|
|
80
|
+
# @param line [String] the raw line content
|
|
81
|
+
# @param line_number [Integer] the source line number
|
|
82
|
+
# @return [Record] the parsed record
|
|
83
|
+
def parse_record(line, line_number)
|
|
84
|
+
raise IntelHexError, "Intel HEX line #{line_number} must start with ':'" unless line.start_with?(':')
|
|
85
|
+
|
|
86
|
+
hex = line[1..]
|
|
87
|
+
raise IntelHexError, "Intel HEX line #{line_number} has an odd number of hex digits" if hex.length.odd?
|
|
88
|
+
|
|
89
|
+
bytes = hex.scan(/../).map do |pair|
|
|
90
|
+
Integer(pair, 16)
|
|
91
|
+
rescue ArgumentError
|
|
92
|
+
raise IntelHexError, "Intel HEX line #{line_number} contains invalid hex"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
raise IntelHexError, "Intel HEX line #{line_number} is too short" if bytes.length < 5
|
|
96
|
+
|
|
97
|
+
byte_count = bytes[0]
|
|
98
|
+
address = (bytes[1] << 8) | bytes[2]
|
|
99
|
+
record_type = bytes[3]
|
|
100
|
+
data = bytes[4...-1]
|
|
101
|
+
checksum = bytes[-1]
|
|
102
|
+
|
|
103
|
+
expected_length = byte_count + 5
|
|
104
|
+
raise IntelHexError, "Intel HEX line #{line_number} length does not match byte count" if bytes.length != expected_length
|
|
105
|
+
|
|
106
|
+
expected_checksum = ((-bytes[0...-1].sum) & 0xff)
|
|
107
|
+
raise IntelHexError, "Intel HEX line #{line_number} has an invalid checksum" if checksum != expected_checksum
|
|
108
|
+
|
|
109
|
+
raise IntelHexError, format('Intel HEX line %d uses unsupported record type 0x%02X', line_number, record_type) unless [0x00, 0x01].include?(record_type)
|
|
110
|
+
|
|
111
|
+
raise IntelHexError, "Intel HEX EOF record on line #{line_number} must not contain data" if record_type == 0x01 && !data.empty?
|
|
112
|
+
|
|
113
|
+
Record.new(address:, record_type:, data:)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'bus'
|
|
4
|
+
require_relative 'cpu'
|
|
5
|
+
|
|
6
|
+
module MOS6502
|
|
7
|
+
# Small composition root for building a 6502-based computer.
|
|
8
|
+
#
|
|
9
|
+
# A machine owns a {Bus}, a {CPU}, and any mapped devices. It provides a
|
|
10
|
+
# cleaner place than {CPU} to describe whole-system memory maps such as
|
|
11
|
+
# "RAM here, ROM there, I/O in the middle".
|
|
12
|
+
class Machine
|
|
13
|
+
# @return [Bus] the machine address bus
|
|
14
|
+
attr_reader :bus
|
|
15
|
+
# @return [CPU] the attached CPU core
|
|
16
|
+
attr_reader :cpu
|
|
17
|
+
|
|
18
|
+
# Creates a new machine around a bus and CPU.
|
|
19
|
+
#
|
|
20
|
+
# @param bus [Bus] the machine bus
|
|
21
|
+
# @param cpu [CPU, nil] an optional pre-built CPU
|
|
22
|
+
# @param install_default_reset_vector [Boolean] whether to install the CPU's standalone reset vector
|
|
23
|
+
def initialize(bus: Bus.new, cpu: nil, install_default_reset_vector: false)
|
|
24
|
+
@bus = bus
|
|
25
|
+
@cpu = cpu || CPU.new(bus:, install_default_reset_vector:)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Maps a RAM device into the machine address space.
|
|
29
|
+
#
|
|
30
|
+
# @param start_address [Integer] the first mapped address
|
|
31
|
+
# @param end_address [Integer] the last mapped address
|
|
32
|
+
# @param fill_byte [Integer] the byte used on RAM reset
|
|
33
|
+
# @param size [Integer, nil] an optional explicit RAM size
|
|
34
|
+
# @param device_offset [Integer] the first exposed device-local byte
|
|
35
|
+
# @return [RAM] the mapped RAM device
|
|
36
|
+
def map_ram(start_address:, end_address:, fill_byte: 0x00, size: nil, device_offset: 0)
|
|
37
|
+
length = end_address - start_address + 1
|
|
38
|
+
ram = RAM.new(size || length, fill_byte:)
|
|
39
|
+
bus.map(ram, start_address:, end_address:, device_offset:)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Maps an arbitrary device into the machine address space.
|
|
43
|
+
#
|
|
44
|
+
# @param device [Device] the device to map
|
|
45
|
+
# @param start_address [Integer] the first mapped address
|
|
46
|
+
# @param end_address [Integer] the last mapped address
|
|
47
|
+
# @param device_offset [Integer] the first exposed device-local byte
|
|
48
|
+
# @return [Device] the mapped device
|
|
49
|
+
def map_device(device, start_address:, end_address:, device_offset: 0)
|
|
50
|
+
bus.map(device, start_address:, end_address:, device_offset:)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Maps a ROM image into the machine address space.
|
|
54
|
+
#
|
|
55
|
+
# @param bytes [String, #to_a] the ROM image
|
|
56
|
+
# @param start_address [Integer] the first mapped address
|
|
57
|
+
# @param end_address [Integer, nil] the last mapped address
|
|
58
|
+
# @param device_offset [Integer] the first exposed device-local byte
|
|
59
|
+
# @param fill_byte [Integer] the byte returned for padded reads
|
|
60
|
+
# @return [ROM] the mapped ROM device
|
|
61
|
+
def map_rom(bytes, start_address:, end_address: nil, device_offset: 0, fill_byte: 0xff)
|
|
62
|
+
rom = ROM.new(bytes, fill_byte:)
|
|
63
|
+
end_address ||= start_address + rom.size - 1
|
|
64
|
+
bus.map(rom, start_address:, end_address:, device_offset:)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Resets the CPU without erasing machine memory.
|
|
68
|
+
#
|
|
69
|
+
# @param program_counter [Integer, nil] an optional explicit reset address
|
|
70
|
+
# @return [Machine] the machine
|
|
71
|
+
def reset(program_counter: nil)
|
|
72
|
+
cpu.reset(program_counter:)
|
|
73
|
+
self
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Performs a full power-on sequence for the machine.
|
|
77
|
+
#
|
|
78
|
+
# Unlike the standalone CPU default, this does not install a synthetic reset
|
|
79
|
+
# vector, which allows ROM-mapped systems to boot from their real vectors.
|
|
80
|
+
#
|
|
81
|
+
# @return [Machine] the machine
|
|
82
|
+
def power_on
|
|
83
|
+
cpu.power_on(install_default_reset_vector: false)
|
|
84
|
+
self
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Loads raw bytes into the machine through the CPU convenience API.
|
|
88
|
+
#
|
|
89
|
+
# @param program [String, #to_a] the bytes to load
|
|
90
|
+
# @param start_address [Integer] where to place the first byte
|
|
91
|
+
# @param set_reset_vector [Boolean] whether to update the reset vector
|
|
92
|
+
# @return [Integer] the start address used for the load
|
|
93
|
+
def load(program, start_address: CPU::DEFAULT_LOAD_ADDRESS, set_reset_vector: true)
|
|
94
|
+
cpu.load(program, start_address:, set_reset_vector:)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Loads Intel HEX text into the machine.
|
|
98
|
+
#
|
|
99
|
+
# @param source [String] the Intel HEX text to parse
|
|
100
|
+
# @param set_reset_vector [Boolean] whether to update the reset vector
|
|
101
|
+
# @return [Integer, nil] the lowest loaded address
|
|
102
|
+
def load_intel_hex(source, set_reset_vector: true)
|
|
103
|
+
cpu.load_intel_hex(source, set_reset_vector:)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Executes one CPU instruction.
|
|
107
|
+
#
|
|
108
|
+
# @return [Integer] the executed opcode byte
|
|
109
|
+
def step
|
|
110
|
+
opcode = cpu.step
|
|
111
|
+
bus.tick(cpu.last_cycles)
|
|
112
|
+
opcode
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Executes a fixed number of CPU instructions.
|
|
116
|
+
#
|
|
117
|
+
# @param max_instructions [Integer] the number of instructions to execute
|
|
118
|
+
# @return [Machine] the machine
|
|
119
|
+
def run(max_instructions:)
|
|
120
|
+
max_instructions.times { step }
|
|
121
|
+
self
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Services a maskable interrupt and ticks mapped devices when taken.
|
|
125
|
+
#
|
|
126
|
+
# @return [Boolean] true when the IRQ was taken
|
|
127
|
+
def irq
|
|
128
|
+
taken = cpu.irq
|
|
129
|
+
bus.tick(7) if taken
|
|
130
|
+
taken
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Services a non-maskable interrupt and ticks mapped devices.
|
|
134
|
+
#
|
|
135
|
+
# @return [Boolean] always true
|
|
136
|
+
def nmi
|
|
137
|
+
cpu.nmi.tap { bus.tick(7) }
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|