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,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'memory'
|
|
4
|
+
|
|
5
|
+
module MOS6502
|
|
6
|
+
# Address-decoding bus for the 6502's 16-bit address space.
|
|
7
|
+
#
|
|
8
|
+
# Devices are mapped into fixed address ranges. Reads and writes are routed to
|
|
9
|
+
# the mapped device with the appropriate device-local offset. Unmapped reads
|
|
10
|
+
# return the configured open-bus value and unmapped writes are ignored.
|
|
11
|
+
class Bus
|
|
12
|
+
# Internal mapping descriptor tying an address range to a concrete device.
|
|
13
|
+
Mapping = Struct.new(:start_address, :end_address, :device, :device_offset) do
|
|
14
|
+
# Returns the size of the mapped range in bytes.
|
|
15
|
+
#
|
|
16
|
+
# @return [Integer] the number of mapped bytes
|
|
17
|
+
def length
|
|
18
|
+
end_address - start_address + 1
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Tests whether the supplied bus address falls inside the mapped range.
|
|
22
|
+
#
|
|
23
|
+
# @param address [Integer] the bus address to test
|
|
24
|
+
# @return [Boolean] true when the mapping covers the address
|
|
25
|
+
def cover?(address)
|
|
26
|
+
address.between?(start_address, end_address)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Reads a byte from the mapped device.
|
|
30
|
+
#
|
|
31
|
+
# @param address [Integer] the bus address being accessed
|
|
32
|
+
# @return [Integer] the device byte value
|
|
33
|
+
def read_byte(address)
|
|
34
|
+
device.read_byte(offset_for(address))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Writes a byte into the mapped device.
|
|
38
|
+
#
|
|
39
|
+
# @param address [Integer] the bus address being accessed
|
|
40
|
+
# @param value [Integer] the value to store
|
|
41
|
+
# @return [Integer] the stored device byte value
|
|
42
|
+
def write_byte(address, value)
|
|
43
|
+
device.write_byte(offset_for(address), value)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Tests whether this mapping overlaps another mapping.
|
|
47
|
+
#
|
|
48
|
+
# @param other [Mapping] another mapped range
|
|
49
|
+
# @return [Boolean] true when the ranges overlap
|
|
50
|
+
def overlaps?(other)
|
|
51
|
+
start_address <= other.end_address && other.start_address <= end_address
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Converts a bus address into a device-local offset.
|
|
57
|
+
#
|
|
58
|
+
# @param address [Integer] the bus address being accessed
|
|
59
|
+
# @return [Integer] the corresponding device-local byte offset
|
|
60
|
+
def offset_for(address)
|
|
61
|
+
device_offset + address - start_address
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# @return [Array<Mapping>] the installed address mappings
|
|
66
|
+
attr_reader :mappings
|
|
67
|
+
|
|
68
|
+
# Builds the default standalone 6502 memory map: 64 KiB of RAM.
|
|
69
|
+
#
|
|
70
|
+
# @return [Bus] a bus with a single full-address-space RAM mapping
|
|
71
|
+
def self.default
|
|
72
|
+
new.tap do |bus|
|
|
73
|
+
bus.map(RAM.new, start_address: 0x0000, end_address: 0xffff)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Creates a new empty bus.
|
|
78
|
+
#
|
|
79
|
+
# @param open_bus_value [Integer] the value returned for unmapped reads
|
|
80
|
+
def initialize(open_bus_value: 0x00)
|
|
81
|
+
@open_bus_value = open_bus_value & 0xff
|
|
82
|
+
@mappings = []
|
|
83
|
+
@access_subscribers = []
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Maps a device into an address range.
|
|
87
|
+
#
|
|
88
|
+
# @param device [#read_byte, #write_byte] the device to map
|
|
89
|
+
# @param start_address [Integer] the first bus address handled by the mapping
|
|
90
|
+
# @param end_address [Integer] the last bus address handled by the mapping
|
|
91
|
+
# @param device_offset [Integer] the first device-local byte exposed by the mapping
|
|
92
|
+
# @return [Object] the mapped device
|
|
93
|
+
# @raise [ArgumentError] if the range is invalid, overlaps, or exceeds device size
|
|
94
|
+
def map(device, start_address:, end_address:, device_offset: 0)
|
|
95
|
+
validate_range!(start_address, end_address, device_offset)
|
|
96
|
+
|
|
97
|
+
mapping = Mapping.new(start_address, end_address, device, device_offset)
|
|
98
|
+
|
|
99
|
+
raise ArgumentError, 'Mapped address ranges must not overlap' if mappings.any? { |existing| existing.overlaps?(mapping) }
|
|
100
|
+
|
|
101
|
+
ensure_device_capacity!(mapping)
|
|
102
|
+
|
|
103
|
+
@mappings << mapping
|
|
104
|
+
@mappings.sort_by!(&:start_address)
|
|
105
|
+
device
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Registers a callback for every bus read and write.
|
|
109
|
+
#
|
|
110
|
+
# Subscribers receive a hash with:
|
|
111
|
+
#
|
|
112
|
+
# - `:operation` - `:read` or `:write`
|
|
113
|
+
# - `:address` - the normalized bus address
|
|
114
|
+
# - `:value` - the transferred byte
|
|
115
|
+
# - `:device` - the mapped device, or `nil` for open bus
|
|
116
|
+
# - `:mapped` - whether the access hit a mapped device
|
|
117
|
+
#
|
|
118
|
+
# @yieldparam event [Hash] the bus access event
|
|
119
|
+
# @return [Proc] the registered callback
|
|
120
|
+
def subscribe_accesses(&block)
|
|
121
|
+
raise ArgumentError, 'A block is required' unless block
|
|
122
|
+
|
|
123
|
+
@access_subscribers << block
|
|
124
|
+
block
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Removes a previously registered access callback.
|
|
128
|
+
#
|
|
129
|
+
# @param subscriber [Proc] the callback returned by {#subscribe_accesses}
|
|
130
|
+
# @return [Proc, nil] the removed callback
|
|
131
|
+
def unsubscribe_accesses(subscriber)
|
|
132
|
+
@access_subscribers.delete(subscriber)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Resets every uniquely mapped device that responds to `reset`.
|
|
136
|
+
#
|
|
137
|
+
# @return [Bus] the bus
|
|
138
|
+
def reset
|
|
139
|
+
mappings.map(&:device).uniq.each do |device|
|
|
140
|
+
device.reset if device.respond_to?(:reset)
|
|
141
|
+
end
|
|
142
|
+
self
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Advances every uniquely mapped device by a number of CPU cycles.
|
|
146
|
+
#
|
|
147
|
+
# @param cycles [Integer] the number of elapsed CPU cycles
|
|
148
|
+
# @return [Bus] the bus
|
|
149
|
+
def tick(cycles)
|
|
150
|
+
mappings.map(&:device).uniq.each do |device|
|
|
151
|
+
device.tick(cycles) if device.respond_to?(:tick)
|
|
152
|
+
end
|
|
153
|
+
self
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Reads a byte from the mapped address space.
|
|
157
|
+
#
|
|
158
|
+
# @param address [Integer] the bus address to read
|
|
159
|
+
# @return [Integer] the routed byte value or the open-bus value
|
|
160
|
+
def read_byte(address)
|
|
161
|
+
normalized = normalize_address(address)
|
|
162
|
+
mapping = mapping_for(normalized)
|
|
163
|
+
unless mapping
|
|
164
|
+
emit_access(operation: :read, address: normalized, value: @open_bus_value, device: nil, mapped: false)
|
|
165
|
+
return @open_bus_value
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
value = mapping.read_byte(normalized)
|
|
169
|
+
emit_access(operation: :read, address: normalized, value:, device: mapping.device, mapped: true)
|
|
170
|
+
value
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Writes a byte into the mapped address space.
|
|
174
|
+
#
|
|
175
|
+
# @param address [Integer] the bus address to write
|
|
176
|
+
# @param value [Integer] the value to store
|
|
177
|
+
# @return [Integer] the stored byte value
|
|
178
|
+
def write_byte(address, value)
|
|
179
|
+
normalized = normalize_address(address)
|
|
180
|
+
mapping = mapping_for(normalized)
|
|
181
|
+
unless mapping
|
|
182
|
+
masked_value = value & 0xff
|
|
183
|
+
emit_access(operation: :write, address: normalized, value: masked_value, device: nil, mapped: false)
|
|
184
|
+
return masked_value
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
written = mapping.write_byte(normalized, value)
|
|
188
|
+
emit_access(operation: :write, address: normalized, value: written, device: mapping.device, mapped: true)
|
|
189
|
+
written
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Reads a little-endian 16-bit word from the address space.
|
|
193
|
+
#
|
|
194
|
+
# @param address [Integer] the starting bus address
|
|
195
|
+
# @return [Integer] the reconstructed 16-bit value
|
|
196
|
+
def read_word(address)
|
|
197
|
+
read_byte(address) | (read_byte(address + 1) << 8)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Writes a little-endian 16-bit word into the address space.
|
|
201
|
+
#
|
|
202
|
+
# @param address [Integer] the starting bus address
|
|
203
|
+
# @param value [Integer] the 16-bit value to store
|
|
204
|
+
# @return [Integer] the written 16-bit value
|
|
205
|
+
def write_word(address, value)
|
|
206
|
+
write_byte(address, value)
|
|
207
|
+
write_byte(address + 1, value >> 8)
|
|
208
|
+
value & 0xffff
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private
|
|
212
|
+
|
|
213
|
+
# Locates the mapping responsible for a normalized bus address.
|
|
214
|
+
#
|
|
215
|
+
# @param address [Integer] the wrapped 16-bit bus address
|
|
216
|
+
# @return [Mapping, nil] the matching mapping, if any
|
|
217
|
+
def mapping_for(address)
|
|
218
|
+
mappings.find { |mapping| mapping.cover?(address) }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Wraps an address to the 6502's 16-bit bus width.
|
|
222
|
+
#
|
|
223
|
+
# @param address [Integer] the input address
|
|
224
|
+
# @return [Integer] the wrapped 16-bit address
|
|
225
|
+
def normalize_address(address)
|
|
226
|
+
address & 0xffff
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Validates a mapping range before installation.
|
|
230
|
+
#
|
|
231
|
+
# @param start_address [Integer] the first mapped address
|
|
232
|
+
# @param end_address [Integer] the last mapped address
|
|
233
|
+
# @param device_offset [Integer] the first exposed device-local offset
|
|
234
|
+
# @return [void]
|
|
235
|
+
# @raise [ArgumentError] if the arguments are invalid
|
|
236
|
+
def validate_range!(start_address, end_address, device_offset)
|
|
237
|
+
raise ArgumentError, 'Mapped addresses must be within 0x0000..0xFFFF' unless start_address.between?(0x0000, 0xffff) && end_address.between?(0x0000, 0xffff)
|
|
238
|
+
raise ArgumentError, 'Mapped address ranges must be ascending' if end_address < start_address
|
|
239
|
+
raise ArgumentError, 'Device offsets must be zero or positive' if device_offset.negative?
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Ensures that a mapping does not expose more bytes than the device provides.
|
|
243
|
+
#
|
|
244
|
+
# @param mapping [Mapping] the mapping being installed
|
|
245
|
+
# @return [void]
|
|
246
|
+
# @raise [ArgumentError] if the device is too small for the requested mapping
|
|
247
|
+
def ensure_device_capacity!(mapping)
|
|
248
|
+
return unless mapping.device.respond_to?(:size)
|
|
249
|
+
|
|
250
|
+
available = mapping.device.size - mapping.device_offset
|
|
251
|
+
return if available >= mapping.length
|
|
252
|
+
|
|
253
|
+
raise ArgumentError, 'Mapped range exceeds device capacity'
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Broadcasts a bus access event to registered subscribers.
|
|
257
|
+
#
|
|
258
|
+
# @param event [Hash] the event payload
|
|
259
|
+
# @return [void]
|
|
260
|
+
def emit_access(event)
|
|
261
|
+
@access_subscribers.each { |subscriber| subscriber.call(event) }
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|