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.
@@ -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