ac6502 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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +261 -0
  3. package/dist/components/CPU.js +1170 -0
  4. package/dist/components/CPU.js.map +1 -0
  5. package/dist/components/Cart.js +23 -0
  6. package/dist/components/Cart.js.map +1 -0
  7. package/dist/components/IO/Empty.js +19 -0
  8. package/dist/components/IO/Empty.js.map +1 -0
  9. package/dist/components/IO/GPIOAttachments/GPIOAttachment.js +71 -0
  10. package/dist/components/IO/GPIOAttachments/GPIOAttachment.js.map +1 -0
  11. package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js +90 -0
  12. package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js.map +1 -0
  13. package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js +489 -0
  14. package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js.map +1 -0
  15. package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js +274 -0
  16. package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js.map +1 -0
  17. package/dist/components/IO/GPIOCard.js +597 -0
  18. package/dist/components/IO/GPIOCard.js.map +1 -0
  19. package/dist/components/IO/InputBoard.js +19 -0
  20. package/dist/components/IO/InputBoard.js.map +1 -0
  21. package/dist/components/IO/LCDCard.js +19 -0
  22. package/dist/components/IO/LCDCard.js.map +1 -0
  23. package/dist/components/IO/RAMCard.js +63 -0
  24. package/dist/components/IO/RAMCard.js.map +1 -0
  25. package/dist/components/IO/RTCCard.js +483 -0
  26. package/dist/components/IO/RTCCard.js.map +1 -0
  27. package/dist/components/IO/SerialCard.js +282 -0
  28. package/dist/components/IO/SerialCard.js.map +1 -0
  29. package/dist/components/IO/SoundCard.js +620 -0
  30. package/dist/components/IO/SoundCard.js.map +1 -0
  31. package/dist/components/IO/StorageCard.js +428 -0
  32. package/dist/components/IO/StorageCard.js.map +1 -0
  33. package/dist/components/IO/VGACard.js +9 -0
  34. package/dist/components/IO/VGACard.js.map +1 -0
  35. package/dist/components/IO/VideoCard.js +623 -0
  36. package/dist/components/IO/VideoCard.js.map +1 -0
  37. package/dist/components/IO.js +3 -0
  38. package/dist/components/IO.js.map +1 -0
  39. package/dist/components/Machine.js +310 -0
  40. package/dist/components/Machine.js.map +1 -0
  41. package/dist/components/RAM.js +24 -0
  42. package/dist/components/RAM.js.map +1 -0
  43. package/dist/components/ROM.js +23 -0
  44. package/dist/components/ROM.js.map +1 -0
  45. package/dist/index.js +441 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/tests/CPU.test.js +1626 -0
  48. package/dist/tests/CPU.test.js.map +1 -0
  49. package/dist/tests/Cart.test.js +119 -0
  50. package/dist/tests/Cart.test.js.map +1 -0
  51. package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js +339 -0
  52. package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js.map +1 -0
  53. package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js +126 -0
  54. package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js.map +1 -0
  55. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js +779 -0
  56. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js.map +1 -0
  57. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js +355 -0
  58. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js.map +1 -0
  59. package/dist/tests/IO/GPIOCard.test.js +503 -0
  60. package/dist/tests/IO/GPIOCard.test.js.map +1 -0
  61. package/dist/tests/IO/RAMCard.test.js +229 -0
  62. package/dist/tests/IO/RAMCard.test.js.map +1 -0
  63. package/dist/tests/IO/RTCCard.test.js +177 -0
  64. package/dist/tests/IO/RTCCard.test.js.map +1 -0
  65. package/dist/tests/IO/SerialCard.test.js +423 -0
  66. package/dist/tests/IO/SerialCard.test.js.map +1 -0
  67. package/dist/tests/IO/SoundCard.test.js +528 -0
  68. package/dist/tests/IO/SoundCard.test.js.map +1 -0
  69. package/dist/tests/IO/StorageCard.test.js +647 -0
  70. package/dist/tests/IO/StorageCard.test.js.map +1 -0
  71. package/dist/tests/IO/VideoCard.test.js +549 -0
  72. package/dist/tests/IO/VideoCard.test.js.map +1 -0
  73. package/dist/tests/Machine.test.js +383 -0
  74. package/dist/tests/Machine.test.js.map +1 -0
  75. package/dist/tests/RAM.test.js +160 -0
  76. package/dist/tests/RAM.test.js.map +1 -0
  77. package/dist/tests/ROM.test.js +123 -0
  78. package/dist/tests/ROM.test.js.map +1 -0
  79. package/jest.config.cjs +9 -0
  80. package/package.json +43 -0
  81. package/src/components/CPU.ts +1371 -0
  82. package/src/components/Cart.ts +20 -0
  83. package/src/components/IO/GPIOAttachments/GPIOAttachment.ts +189 -0
  84. package/src/components/IO/GPIOAttachments/GPIOJoystickAttachment.ts +99 -0
  85. package/src/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.ts +465 -0
  86. package/src/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.ts +287 -0
  87. package/src/components/IO/GPIOCard.ts +677 -0
  88. package/src/components/IO/RAMCard.ts +68 -0
  89. package/src/components/IO/RTCCard.ts +518 -0
  90. package/src/components/IO/SerialCard.ts +335 -0
  91. package/src/components/IO/SoundCard.ts +711 -0
  92. package/src/components/IO/StorageCard.ts +473 -0
  93. package/src/components/IO/VideoCard.ts +730 -0
  94. package/src/components/IO.ts +11 -0
  95. package/src/components/Machine.ts +364 -0
  96. package/src/components/RAM.ts +23 -0
  97. package/src/components/ROM.ts +19 -0
  98. package/src/index.ts +474 -0
  99. package/src/tests/CPU.test.ts +2045 -0
  100. package/src/tests/Cart.test.ts +149 -0
  101. package/src/tests/IO/GPIOAttachments/GPIOAttachment.test.ts +413 -0
  102. package/src/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.ts +147 -0
  103. package/src/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.ts +961 -0
  104. package/src/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.ts +449 -0
  105. package/src/tests/IO/GPIOCard.test.ts +644 -0
  106. package/src/tests/IO/RAMCard.test.ts +284 -0
  107. package/src/tests/IO/RTCCard.test.ts +222 -0
  108. package/src/tests/IO/SerialCard.test.ts +530 -0
  109. package/src/tests/IO/SoundCard.test.ts +659 -0
  110. package/src/tests/IO/StorageCard.test.ts +787 -0
  111. package/src/tests/IO/VideoCard.test.ts +668 -0
  112. package/src/tests/Machine.test.ts +437 -0
  113. package/src/tests/RAM.test.ts +196 -0
  114. package/src/tests/ROM.test.ts +154 -0
  115. package/tsconfig.json +12 -0
@@ -0,0 +1,11 @@
1
+ export interface IO {
2
+
3
+ raiseIRQ: () => void
4
+ raiseNMI: () => void
5
+
6
+ read(address: number): number
7
+ write(address: number, data: number): void
8
+ tick(frequency: number): void
9
+ reset(coldStart: boolean): void
10
+
11
+ }
@@ -0,0 +1,364 @@
1
+ import { CPU } from './CPU'
2
+ import { RAM } from './RAM'
3
+ import { ROM } from './ROM'
4
+ import { Cart } from './Cart'
5
+ import { GPIOCard } from './IO/GPIOCard'
6
+ import { RAMCard } from './IO/RAMCard'
7
+ import { RTCCard } from './IO/RTCCard'
8
+ import { SerialCard } from './IO/SerialCard'
9
+ import { SoundCard } from './IO/SoundCard'
10
+ import { StorageCard } from './IO/StorageCard'
11
+ import { VideoCard } from './IO/VideoCard'
12
+ import { GPIOKeyboardMatrixAttachment } from './IO/GPIOAttachments/GPIOKeyboardMatrixAttachment'
13
+ import { GPIOKeyboardEncoderAttachment } from './IO/GPIOAttachments/GPIOKeyboardEncoderAttachment'
14
+ import { GPIOJoystickAttachment } from './IO/GPIOAttachments/GPIOJoystickAttachment'
15
+ import { readFile } from 'fs/promises'
16
+
17
+ export class Machine {
18
+
19
+ static MAX_FPS: number = 60
20
+ static FRAME_INTERVAL_MS: number = 1000 / Machine.MAX_FPS
21
+
22
+ private ioCycleAccumulator: number = 0
23
+ private ioTickInterval: number = 128 // adjust (64/128/256)
24
+
25
+ cpu: CPU
26
+ ram: RAM
27
+ rom: ROM
28
+ io1: RAMCard
29
+ io2: RAMCard
30
+ io3: RTCCard
31
+ io4: StorageCard
32
+ io5: SerialCard
33
+ io6: GPIOCard
34
+ io7: SoundCard
35
+ io8: VideoCard
36
+
37
+ cart?: Cart
38
+
39
+ // GPIO Attachments
40
+ keyboardMatrixAttachment: GPIOKeyboardMatrixAttachment
41
+ keyboardEncoderAttachment: GPIOKeyboardEncoderAttachment
42
+ joystickAttachment: GPIOJoystickAttachment
43
+
44
+ isAlive: boolean = false
45
+ isRunning: boolean = false
46
+ frequency: number = 2000000 // 2 MHz
47
+ scale: number = 2
48
+ frames: number = 0
49
+ frameDelay: number = 0
50
+ frameDelayCount: number = 0
51
+ startTime: number = Date.now()
52
+ previousTime: number = performance.now()
53
+
54
+ transmit?: (data: number) => void
55
+ render?: (buffer: Buffer<ArrayBufferLike>) => void
56
+ pushAudioSamples?: (samples: Float32Array) => void
57
+
58
+ //
59
+ // Initialization
60
+ //
61
+
62
+ constructor() {
63
+ this.cpu = new CPU(this.read.bind(this), this.write.bind(this))
64
+ this.ram = new RAM()
65
+ this.rom = new ROM()
66
+ this.io1 = new RAMCard()
67
+ this.io2 = new RAMCard()
68
+ this.io3 = new RTCCard()
69
+ this.io4 = new StorageCard()
70
+ this.io5 = new SerialCard()
71
+ this.io6 = new GPIOCard()
72
+ this.io7 = new SoundCard()
73
+ this.io8 = new VideoCard()
74
+
75
+ // Connect RTCCard IRQ/NMI to CPU
76
+ this.io3.raiseIRQ = () => this.cpu.irq()
77
+ this.io3.raiseNMI = () => this.cpu.nmi()
78
+
79
+ // Connect SerialCard IRQ/NMI to CPU
80
+ this.io5.raiseIRQ = () => this.cpu.irq()
81
+ this.io5.raiseNMI = () => this.cpu.nmi()
82
+
83
+ // Connect SerialCard transmit callback (use arrow function to look up this.transmit at call time)
84
+ this.io5.transmit = (data: number) => {
85
+ if (this.transmit) {
86
+ this.transmit(data)
87
+ }
88
+ }
89
+
90
+ // Connect VideoCard IRQ/NMI to CPU
91
+ this.io8.raiseIRQ = () => this.cpu.irq()
92
+ this.io8.raiseNMI = () => this.cpu.nmi()
93
+
94
+ // Connect SoundCard pushSamples callback (use arrow function to look up this.pushAudioSamples at call time)
95
+ this.io7.pushSamples = (samples: Float32Array) => {
96
+ if (this.pushAudioSamples) {
97
+ this.pushAudioSamples(samples)
98
+ }
99
+ }
100
+
101
+ // Create GPIO Attachments
102
+ // Keyboard matrix (manual scanning) - highest priority for Port A rows (priority 10)
103
+ this.keyboardMatrixAttachment = new GPIOKeyboardMatrixAttachment(10)
104
+
105
+ // Keyboard encoder (ASCII on both Port A and Port B) - medium priority (priority 20)
106
+ this.keyboardEncoderAttachment = new GPIOKeyboardEncoderAttachment(20)
107
+
108
+ // Joystick (Port B) - lowest priority, fallback (priority 100)
109
+ this.joystickAttachment = new GPIOJoystickAttachment(false, 100)
110
+
111
+ // Attach peripherals to GPIO Card
112
+ // Keyboard matrix supports both ports
113
+ this.io6.attachToPortA(this.keyboardMatrixAttachment)
114
+ this.io6.attachToPortB(this.keyboardMatrixAttachment)
115
+
116
+ // Keyboard encoder supports both ports
117
+ this.io6.attachToPortA(this.keyboardEncoderAttachment)
118
+ this.io6.attachToPortB(this.keyboardEncoderAttachment)
119
+
120
+ // Joystick attached to Port B only
121
+ this.io6.attachToPortB(this.joystickAttachment)
122
+
123
+ this.cpu.reset()
124
+ }
125
+
126
+ //
127
+ // Methods
128
+ //
129
+
130
+ loadROM = async (path: string) => {
131
+ try {
132
+ this.rom.load(Array.from(new Uint8Array(await readFile(path))))
133
+ } catch (error) {
134
+ console.error('Error reading file:', error)
135
+ }
136
+ }
137
+
138
+ loadCart = async (path: string) => {
139
+ try {
140
+ const data = Array.from(new Uint8Array(await readFile(path)))
141
+ const cart = new Cart()
142
+ cart.load(data)
143
+ this.cart = cart
144
+ } catch (error) {
145
+ console.error('Error reading file:', error)
146
+ }
147
+ }
148
+
149
+ start(): void {
150
+ this.startTime = Date.now()
151
+ this.isRunning = true
152
+ this.isAlive = true
153
+ this.loop()
154
+ }
155
+
156
+ end(): void {
157
+ this.isRunning = false
158
+ this.isAlive = false
159
+ }
160
+
161
+ run(): void {
162
+ this.isRunning = true
163
+ }
164
+
165
+ stop(): void {
166
+ this.isRunning = false
167
+ }
168
+
169
+ step(): void {
170
+ // Step through one complete instruction
171
+ const cyclesExecuted = this.cpu.step()
172
+
173
+ // Tick IO cards for each cycle of the instruction
174
+ for (let i = 0; i < cyclesExecuted; i++) {
175
+ // SerialCard must be cycle-accurate
176
+ this.io5.tick(this.frequency)
177
+
178
+ this.ioCycleAccumulator++
179
+ if (this.ioCycleAccumulator >= this.ioTickInterval) {
180
+ // Skip ticking RAMCard IO1 and IO2 since they have no timing behavior
181
+ this.io3.tick(this.frequency)
182
+ this.io4.tick(this.frequency)
183
+ this.io6.tick(this.frequency)
184
+ this.io7.tick(this.frequency)
185
+ this.io8.tick(this.frequency)
186
+ this.ioCycleAccumulator = 0
187
+ }
188
+ }
189
+ }
190
+
191
+ tick(): void {
192
+ // Execute one CPU clock cycle
193
+ this.cpu.tick()
194
+
195
+ // SerialCard must be cycle-accurate
196
+ this.io5.tick(this.frequency)
197
+
198
+ // Tick other IO cards at intervals
199
+ this.ioCycleAccumulator++
200
+ if (this.ioCycleAccumulator >= this.ioTickInterval) {
201
+ // Skip ticking RAMCard IO1 and IO2 since they have no timing behavior
202
+ this.io3.tick(this.frequency)
203
+ this.io4.tick(this.frequency)
204
+ this.io6.tick(this.frequency)
205
+ this.io7.tick(this.frequency)
206
+ this.io8.tick(this.frequency)
207
+ this.ioCycleAccumulator = 0
208
+ }
209
+ }
210
+
211
+ onReceive(data: number): void {
212
+ this.io5.onData(data) // Pass data to Serial card
213
+ }
214
+
215
+ onKeyDown(scancode: number): void {
216
+ // Route keyboard input to attachments
217
+ this.keyboardMatrixAttachment.updateKey(scancode, true) // Update keyboard matrix
218
+ this.keyboardEncoderAttachment.updateKey(scancode, true) // Update keyboard encoder
219
+ }
220
+
221
+ onKeyUp(scancode: number): void {
222
+ // Release key from keyboard matrix and encoder
223
+ this.keyboardMatrixAttachment.updateKey(scancode, false) // Update keyboard matrix
224
+ this.keyboardEncoderAttachment.updateKey(scancode, false) // Update keyboard encoder
225
+ }
226
+
227
+ onJoystick(buttons: number): void {
228
+ // Update joystick attachment with button states
229
+ this.joystickAttachment.updateJoystick(buttons)
230
+ }
231
+
232
+ //
233
+ // Loop Operations
234
+ //
235
+
236
+ private loop(): void {
237
+ if (!this.isAlive) { return }
238
+
239
+ const now = performance.now()
240
+ const elapsedMs = now - this.previousTime
241
+ this.previousTime = now
242
+
243
+ if (this.isRunning) {
244
+ const ticksPerMs = this.frequency / 1000
245
+ let accumulator = (this as any)._accumulatorMs ?? 0
246
+ accumulator += elapsedMs
247
+
248
+ const maxCatchUpMs = 250
249
+ if (accumulator > maxCatchUpMs) accumulator = maxCatchUpMs
250
+
251
+ const ticksToRun = Math.floor(accumulator * ticksPerMs)
252
+ if (ticksToRun > 0) {
253
+ for (let i = 0; i < ticksToRun; i++) {
254
+ this.cpu.tick()
255
+
256
+ // SerialCard must be cycle-accurate
257
+ this.io5.tick(this.frequency)
258
+
259
+ this.ioCycleAccumulator++
260
+ if (this.ioCycleAccumulator >= this.ioTickInterval) {
261
+ // Skip ticking RAMCard IO1 and IO2 since they have no timing behavior
262
+ this.io3.tick(this.frequency)
263
+ this.io4.tick(this.frequency)
264
+ this.io6.tick(this.frequency)
265
+ this.io7.tick(this.frequency)
266
+ this.io8.tick(this.frequency)
267
+ this.ioCycleAccumulator = 0
268
+ }
269
+ }
270
+ accumulator -= ticksToRun / ticksPerMs
271
+ }
272
+
273
+ (this as any)._accumulatorMs = accumulator
274
+ }
275
+
276
+ if (this.render) {
277
+ this.render(this.io8.buffer)
278
+ this.frames += 1
279
+ }
280
+
281
+ setImmediate(() => this.loop())
282
+ }
283
+
284
+ //
285
+ // Bus Operations
286
+ //
287
+
288
+ reset(coldStart: boolean): void {
289
+ this.cpu.reset()
290
+ this.ram.reset(coldStart)
291
+ this.io1.reset(coldStart)
292
+ this.io2.reset(coldStart)
293
+ this.io3.reset(coldStart)
294
+ this.io4.reset(coldStart)
295
+ this.io5.reset(coldStart)
296
+ this.io6.reset(coldStart)
297
+ this.io7.reset(coldStart)
298
+ this.io8.reset(coldStart)
299
+ }
300
+
301
+ read(address: number): number {
302
+ switch(true) {
303
+ case (this.cart && address >= Cart.CODE && address <= Cart.END):
304
+ return this.cart.read(address - Cart.START)
305
+ case (address >= ROM.CODE && address <= ROM.END):
306
+ return this.rom.read(address - ROM.START)
307
+ case (address >= RAM.START && address <= RAM.END):
308
+ return this.ram.read(address)
309
+ case (address >= 0x8000 && address <= 0x83FF):
310
+ return this.io1.read(address - 0x8000) || 0
311
+ case (address >= 0x8400 && address <= 0x87FF):
312
+ return this.io2.read(address - 0x8400) || 0
313
+ case (address >= 0x8800 && address <= 0x8BFF):
314
+ return this.io3.read(address - 0x8800) || 0
315
+ case (address >= 0x8C00 && address <= 0x8FFF):
316
+ return this.io4.read(address - 0x8C00) || 0
317
+ case (address >= 0x9000 && address <= 0x93FF):
318
+ return this.io5.read(address - 0x9000) || 0
319
+ case (address >= 0x9400 && address <= 0x97FF):
320
+ return this.io6.read(address - 0x9400) || 0
321
+ case (address >= 0x9800 && address <= 0x9BFF):
322
+ return this.io7.read(address - 0x9800) || 0
323
+ case (address >= 0x9C00 && address <= 0x9FFF):
324
+ return this.io8.read(address - 0x9C00) || 0
325
+ default:
326
+ return 0
327
+ }
328
+ }
329
+
330
+ write(address: number, data: number): void {
331
+ switch(true) {
332
+ case (address >= RAM.START && address <= RAM.END):
333
+ this.ram.write(address, data)
334
+ return
335
+ case (address >= 0x8000 && address <= 0x83FF):
336
+ this.io1.write(address - 0x8000, data)
337
+ return
338
+ case (address >= 0x8400 && address <= 0x87FF):
339
+ this.io2.write(address - 0x8400, data)
340
+ return
341
+ case (address >= 0x8800 && address <= 0x8BFF):
342
+ this.io3.write(address - 0x8800, data)
343
+ return
344
+ case (address >= 0x8C00 && address <= 0x8FFF):
345
+ this.io4.write(address - 0x8C00, data)
346
+ return
347
+ case (address >= 0x9000 && address <= 0x93FF):
348
+ this.io5.write(address - 0x9000, data)
349
+ return
350
+ case (address >= 0x9400 && address <= 0x97FF):
351
+ this.io6.write(address - 0x9400, data)
352
+ return
353
+ case (address >= 0x9800 && address <= 0x9BFF):
354
+ this.io7.write(address - 0x9800, data)
355
+ return
356
+ case (address >= 0x9C00 && address <= 0x9FFF):
357
+ this.io8.write(address - 0x9C00, data)
358
+ return
359
+ default:
360
+ return
361
+ }
362
+ }
363
+
364
+ }
@@ -0,0 +1,23 @@
1
+ export class RAM {
2
+
3
+ static START: number = 0x0000
4
+ static END: number = 0x7FFF
5
+ static SIZE: number = RAM.END - RAM.START + 1
6
+
7
+ data: number[] = [...Array(RAM.SIZE)].fill(0x00)
8
+
9
+ read(address: number): number {
10
+ return this.data[address]
11
+ }
12
+
13
+ write(address: number, data: number): void {
14
+ this.data[address] = data
15
+ }
16
+
17
+ reset(coldStart: boolean): void {
18
+ if (coldStart) {
19
+ this.data.fill(0x00)
20
+ }
21
+ }
22
+
23
+ }
@@ -0,0 +1,19 @@
1
+ export class ROM {
2
+
3
+ static START: number = 0x8000
4
+ static END: number = 0xFFFF
5
+ static CODE: number = 0xA000
6
+ static SIZE: number = ROM.END - ROM.START + 1
7
+
8
+ data: number[] = [...Array(ROM.SIZE)].fill(0x00)
9
+
10
+ read(address: number): number {
11
+ return this.data[address]
12
+ }
13
+
14
+ load(data: number[]): void {
15
+ if (data.length != ROM.SIZE) { return }
16
+
17
+ this.data = data
18
+ }
19
+ }