ac6502 1.9.3 → 1.11.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 (77) hide show
  1. package/dist/components/CPU.d.ts +4 -0
  2. package/dist/components/CPU.js +87 -30
  3. package/dist/components/CPU.js.map +1 -1
  4. package/dist/components/IO/ACIA.d.ts +13 -21
  5. package/dist/components/IO/ACIA.js +53 -151
  6. package/dist/components/IO/ACIA.js.map +1 -1
  7. package/dist/components/IO/Attachments/KeyboardEncoderAttachment.d.ts +5 -10
  8. package/dist/components/IO/Attachments/KeyboardEncoderAttachment.js +43 -266
  9. package/dist/components/IO/Attachments/KeyboardEncoderAttachment.js.map +1 -1
  10. package/dist/components/IO/Empty.d.ts +1 -3
  11. package/dist/components/IO/Empty.js +1 -5
  12. package/dist/components/IO/Empty.js.map +1 -1
  13. package/dist/components/IO/RAMBank.d.ts +3 -4
  14. package/dist/components/IO/RAMBank.js +4 -13
  15. package/dist/components/IO/RAMBank.js.map +1 -1
  16. package/dist/components/IO/RTC.d.ts +2 -3
  17. package/dist/components/IO/RTC.js +17 -7
  18. package/dist/components/IO/RTC.js.map +1 -1
  19. package/dist/components/IO/Sound.d.ts +1 -3
  20. package/dist/components/IO/Sound.js +13 -23
  21. package/dist/components/IO/Sound.js.map +1 -1
  22. package/dist/components/IO/Storage.d.ts +1 -3
  23. package/dist/components/IO/Storage.js +1 -3
  24. package/dist/components/IO/Storage.js.map +1 -1
  25. package/dist/components/IO/VIA.d.ts +1 -3
  26. package/dist/components/IO/VIA.js +6 -7
  27. package/dist/components/IO/VIA.js.map +1 -1
  28. package/dist/components/IO/Video.d.ts +1 -3
  29. package/dist/components/IO/Video.js +3 -5
  30. package/dist/components/IO/Video.js.map +1 -1
  31. package/dist/components/IO.d.ts +1 -3
  32. package/dist/components/Machine.d.ts +1 -2
  33. package/dist/components/Machine.js +21 -74
  34. package/dist/components/Machine.js.map +1 -1
  35. package/dist/index.js +1 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/tests/IO/ACIA.test.js +57 -108
  38. package/dist/tests/IO/ACIA.test.js.map +1 -1
  39. package/dist/tests/IO/Attachments/KeyboardEncoderAttachment.test.js +334 -574
  40. package/dist/tests/IO/Attachments/KeyboardEncoderAttachment.test.js.map +1 -1
  41. package/dist/tests/IO/Empty.test.js +2 -14
  42. package/dist/tests/IO/Empty.test.js.map +1 -1
  43. package/dist/tests/IO/RAMBank.test.js +5 -12
  44. package/dist/tests/IO/RAMBank.test.js.map +1 -1
  45. package/dist/tests/IO/RTC.test.js +7 -16
  46. package/dist/tests/IO/RTC.test.js.map +1 -1
  47. package/dist/tests/IO/Sound.test.js +6 -8
  48. package/dist/tests/IO/Sound.test.js.map +1 -1
  49. package/dist/tests/IO/Storage.test.js +0 -6
  50. package/dist/tests/IO/Storage.test.js.map +1 -1
  51. package/dist/tests/IO/VIA.test.js +6 -10
  52. package/dist/tests/IO/VIA.test.js.map +1 -1
  53. package/dist/tests/IO/Video.test.js +7 -7
  54. package/dist/tests/IO/Video.test.js.map +1 -1
  55. package/package.json +1 -1
  56. package/src/components/CPU.ts +94 -31
  57. package/src/components/IO/ACIA.ts +57 -176
  58. package/src/components/IO/Attachments/KeyboardEncoderAttachment.ts +45 -217
  59. package/src/components/IO/Empty.ts +1 -4
  60. package/src/components/IO/RAMBank.ts +4 -15
  61. package/src/components/IO/RTC.ts +18 -7
  62. package/src/components/IO/Sound.ts +14 -27
  63. package/src/components/IO/Storage.ts +2 -5
  64. package/src/components/IO/VIA.ts +6 -8
  65. package/src/components/IO/Video.ts +5 -7
  66. package/src/components/IO.ts +1 -4
  67. package/src/components/Machine.ts +22 -90
  68. package/src/index.ts +1 -1
  69. package/src/tests/IO/ACIA.test.ts +60 -122
  70. package/src/tests/IO/Attachments/KeyboardEncoderAttachment.test.ts +342 -676
  71. package/src/tests/IO/Empty.test.ts +2 -17
  72. package/src/tests/IO/RAMBank.test.ts +5 -14
  73. package/src/tests/IO/RTC.test.ts +7 -20
  74. package/src/tests/IO/Sound.test.ts +6 -8
  75. package/src/tests/IO/Storage.test.ts +0 -7
  76. package/src/tests/IO/VIA.test.ts +6 -12
  77. package/src/tests/IO/Video.test.ts +7 -8
@@ -130,7 +130,6 @@ const TMS_NUM_REGISTERS = 8
130
130
  // Timing (NTSC)
131
131
  const TOTAL_SCANLINES = 262
132
132
  const FRAMES_PER_SECOND = 60
133
- const CYCLES_PER_TICK = 128 // Must match Machine.ts ioTickInterval
134
133
 
135
134
  // Border offsets (centering 256x192 in 320x240)
136
135
  const BORDER_X = (DISPLAY_WIDTH - TMS_PIXELS_X) / 2 // 32
@@ -138,9 +137,6 @@ const BORDER_Y = (DISPLAY_HEIGHT - TMS_PIXELS_Y) / 2 // 24
138
137
 
139
138
  export class Video implements IO {
140
139
 
141
- raiseIRQ = () => {}
142
- raiseNMI = () => {}
143
-
144
140
  // ---- VDP internal state ----
145
141
 
146
142
  /** Eight write-only registers */
@@ -207,16 +203,19 @@ export class Video implements IO {
207
203
  }
208
204
  }
209
205
 
210
- tick(frequency: number): void {
206
+ tick(frequency: number): number {
211
207
  const cyclesPerFrame = frequency / FRAMES_PER_SECOND
212
208
  const cyclesPerScanline = cyclesPerFrame / TOTAL_SCANLINES
213
209
 
214
- this.cycleAccumulator += CYCLES_PER_TICK
210
+ this.cycleAccumulator++
215
211
 
216
212
  while (this.cycleAccumulator >= cyclesPerScanline) {
217
213
  this.cycleAccumulator -= cyclesPerScanline
218
214
  this.processScanline()
219
215
  }
216
+
217
+ // Return IRQ status based on interrupt flag in status register
218
+ return (this.status & STATUS_INT) ? 0x80 : 0
220
219
  }
221
220
 
222
221
  reset(_coldStart: boolean): void {
@@ -432,7 +431,6 @@ export class Video implements IO {
432
431
  // Set interrupt flag at end of active display
433
432
  if (y === TMS_PIXELS_Y - 1 && (this.registers[TMS_REG_1] & TMS_R1_INT_ENABLE)) {
434
433
  this.status |= STATUS_INT
435
- this.raiseIRQ()
436
434
  }
437
435
 
438
436
  this.writeScanlineToBuffer(y, pixels)
@@ -1,11 +1,8 @@
1
1
  export interface IO {
2
2
 
3
- raiseIRQ: () => void
4
- raiseNMI: () => void
5
-
6
3
  read(address: number): number
7
4
  write(address: number, data: number): void
8
- tick(frequency: number, cycles?: number): void
5
+ tick(frequency: number): number // Returns interrupt status: bit 7 = IRQ, bit 6 = NMI
9
6
  reset(coldStart: boolean): void
10
7
 
11
8
  }
@@ -22,8 +22,6 @@ export class Machine {
22
22
  static MAX_FPS: number = 60
23
23
  static FRAME_INTERVAL_MS: number = 1000 / Machine.MAX_FPS
24
24
 
25
- private ioCycleAccumulator: number = 0
26
- private ioTickInterval: number = 128 // adjust (64/128/256)
27
25
  private loopHandle?: ReturnType<typeof setImmediate> | ReturnType<typeof setTimeout>
28
26
 
29
27
  cpu: CPU
@@ -92,10 +90,6 @@ export class Machine {
92
90
  const acia = new ACIA()
93
91
  this.io5 = acia
94
92
 
95
- // Connect ACIA IRQ/NMI to CPU
96
- acia.raiseIRQ = () => this.cpu.irq()
97
- acia.raiseNMI = () => this.cpu.nmi()
98
-
99
93
  // Connect ACIA transmit callback
100
94
  acia.transmit = (data: number) => {
101
95
  if (this.transmit) {
@@ -113,10 +107,6 @@ export class Machine {
113
107
  const via = new VIA()
114
108
  this.io8 = via
115
109
 
116
- // Connect VIA IRQ/NMI to CPU
117
- via.raiseIRQ = () => this.cpu.irq()
118
- via.raiseNMI = () => this.cpu.nmi()
119
-
120
110
  // Create KIM GPIO Attachments
121
111
  this.lcdAttachment = new LCDAttachment(16, 2, 10)
122
112
  this.keypadAttachment = new KeypadAttachment(true, 20)
@@ -131,10 +121,6 @@ export class Machine {
131
121
  const acia = new ACIA()
132
122
  this.io5 = acia
133
123
 
134
- // Connect ACIA IRQ/NMI to CPU
135
- acia.raiseIRQ = () => this.cpu.irq()
136
- acia.raiseNMI = () => this.cpu.nmi()
137
-
138
124
  // Connect ACIA transmit callback
139
125
  acia.transmit = (data: number) => {
140
126
  if (this.transmit) {
@@ -156,18 +142,6 @@ export class Machine {
156
142
  this.io7 = sound
157
143
  this.io8 = video
158
144
 
159
- // Connect RTC IRQ/NMI to CPU
160
- rtc.raiseIRQ = () => this.cpu.irq()
161
- rtc.raiseNMI = () => this.cpu.nmi()
162
-
163
- // Connect Video IRQ/NMI to CPU
164
- video.raiseIRQ = () => this.cpu.irq()
165
- video.raiseNMI = () => this.cpu.nmi()
166
-
167
- // Connect GPIO VIA IRQ/NMI to CPU
168
- via.raiseIRQ = () => this.cpu.irq()
169
- via.raiseNMI = () => this.cpu.nmi()
170
-
171
145
  // Connect Sound pushSamples callback
172
146
  sound.pushSamples = (samples: Float32Array) => {
173
147
  if (this.play) {
@@ -203,14 +177,6 @@ export class Machine {
203
177
  this.io7 = sound
204
178
  this.io8 = video
205
179
 
206
- // Connect Video IRQ/NMI to CPU
207
- video.raiseIRQ = () => this.cpu.irq()
208
- video.raiseNMI = () => this.cpu.nmi()
209
-
210
- // Connect VIA IRQ/NMI to CPU
211
- via.raiseIRQ = () => this.cpu.irq()
212
- via.raiseNMI = () => this.cpu.nmi()
213
-
214
180
  // Connect Sound pushSamples callback
215
181
  sound.pushSamples = (samples: Float32Array) => {
216
182
  if (this.play) {
@@ -235,10 +201,6 @@ export class Machine {
235
201
  const acia = new ACIA()
236
202
  this.io5 = acia
237
203
 
238
- // Connect ACIA IRQ/NMI to CPU
239
- acia.raiseIRQ = () => this.cpu.irq()
240
- acia.raiseNMI = () => this.cpu.nmi()
241
-
242
204
  // Connect ACIA transmit callback
243
205
  acia.transmit = (data: number) => {
244
206
  if (this.transmit) {
@@ -260,18 +222,6 @@ export class Machine {
260
222
  this.io7 = sound
261
223
  this.io8 = video
262
224
 
263
- // Connect RTC IRQ/NMI to CPU
264
- rtc.raiseIRQ = () => this.cpu.irq()
265
- rtc.raiseNMI = () => this.cpu.nmi()
266
-
267
- // Connect Video IRQ/NMI to CPU
268
- video.raiseIRQ = () => this.cpu.irq()
269
- video.raiseNMI = () => this.cpu.nmi()
270
-
271
- // Connect GPIO VIA IRQ/NMI to CPU
272
- via.raiseIRQ = () => this.cpu.irq()
273
- via.raiseNMI = () => this.cpu.nmi()
274
-
275
225
  // Connect Sound pushSamples callback
276
226
  sound.pushSamples = (samples: Float32Array) => {
277
227
  if (this.play) {
@@ -355,19 +305,7 @@ export class Machine {
355
305
 
356
306
  // Tick IO cards for each cycle of the instruction
357
307
  for (let i = 0; i < cyclesExecuted; i++) {
358
- // ACIA must be cycle-accurate
359
- this.io5.tick(this.frequency)
360
-
361
- this.ioCycleAccumulator++
362
- if (this.ioCycleAccumulator >= this.ioTickInterval) {
363
- // Skip ticking RAMBank IO1 and IO2 since they have no timing behavior
364
- this.io3.tick(this.frequency, this.ioTickInterval)
365
- this.io4.tick(this.frequency, this.ioTickInterval)
366
- this.io6.tick(this.frequency, this.ioTickInterval)
367
- this.io7.tick(this.frequency, this.ioTickInterval)
368
- this.io8.tick(this.frequency, this.ioTickInterval)
369
- this.ioCycleAccumulator = 0
370
- }
308
+ this.tickIO()
371
309
  }
372
310
  }
373
311
 
@@ -388,19 +326,26 @@ export class Machine {
388
326
  // Execute one CPU clock cycle
389
327
  this.cpu.tick()
390
328
 
391
- // ACIA must be cycle-accurate
392
- this.io5.tick(this.frequency)
393
-
394
- // Tick other IO cards at intervals
395
- this.ioCycleAccumulator++
396
- if (this.ioCycleAccumulator >= this.ioTickInterval) {
397
- // Skip ticking RAMBank IO1 and IO2 since they have no timing behavior
398
- this.io3.tick(this.frequency, this.ioTickInterval)
399
- this.io4.tick(this.frequency, this.ioTickInterval)
400
- this.io6.tick(this.frequency, this.ioTickInterval)
401
- this.io7.tick(this.frequency, this.ioTickInterval)
402
- this.io8.tick(this.frequency, this.ioTickInterval)
403
- this.ioCycleAccumulator = 0
329
+ // Tick all IO cards and handle level-triggered interrupts
330
+ this.tickIO()
331
+ }
332
+
333
+ private tickIO(): void {
334
+ let interrupt = 0
335
+ interrupt |= this.io3.tick(this.frequency)
336
+ interrupt |= this.io4.tick(this.frequency)
337
+ interrupt |= this.io5.tick(this.frequency)
338
+ interrupt |= this.io6.tick(this.frequency)
339
+ interrupt |= this.io7.tick(this.frequency)
340
+ interrupt |= this.io8.tick(this.frequency)
341
+
342
+ if (interrupt & 0x80) {
343
+ this.cpu.irqTrigger()
344
+ } else {
345
+ this.cpu.irqClear()
346
+ }
347
+ if (interrupt & 0x40) {
348
+ this.cpu.nmi()
404
349
  }
405
350
  }
406
351
 
@@ -455,20 +400,7 @@ export class Machine {
455
400
  if (ticksToRun > 0) {
456
401
  for (let i = 0; i < ticksToRun; i++) {
457
402
  this.cpu.tick()
458
-
459
- // ACIA must be cycle-accurate
460
- this.io5.tick(this.frequency)
461
-
462
- this.ioCycleAccumulator++
463
- if (this.ioCycleAccumulator >= this.ioTickInterval) {
464
- // Skip ticking RAMBank IO1 and IO2 since they have no timing behavior
465
- this.io3.tick(this.frequency, this.ioTickInterval)
466
- this.io4.tick(this.frequency, this.ioTickInterval)
467
- this.io6.tick(this.frequency, this.ioTickInterval)
468
- this.io7.tick(this.frequency, this.ioTickInterval)
469
- this.io8.tick(this.frequency, this.ioTickInterval)
470
- this.ioCycleAccumulator = 0
471
- }
403
+ this.tickIO()
472
404
  }
473
405
  accumulator -= ticksToRun / ticksPerMs
474
406
  }
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ import sdl from '@kmamal/sdl'
11
11
  import { readFile, writeFile } from 'fs/promises'
12
12
  import { existsSync } from 'fs'
13
13
 
14
- const VERSION = '1.9.3'
14
+ const VERSION = '1.11.0'
15
15
  const WIDTH = 320
16
16
  const HEIGHT = 240
17
17
 
@@ -127,34 +127,31 @@ describe('ACIA (6551 ACIA)', () => {
127
127
  // Should not throw and should process command
128
128
  })
129
129
 
130
- it('should enable receive IRQ when bit 2 is set', () => {
131
- const mockIRQ = jest.fn()
132
- serialCard.raiseIRQ = mockIRQ
133
-
134
- serialCard.write(0x02, 0x04) // Enable receive IRQ
130
+ it('should enable receive IRQ when RIIE bit (bit 1) is clear', () => {
131
+ serialCard.write(0x02, 0x04) // bit 1 = 0: receive IRQ enabled
135
132
  serialCard.onData(0x42)
136
133
 
137
- expect(mockIRQ).toHaveBeenCalled()
134
+ const status = serialCard.read(0x01)
135
+ expect(status & 0x80).toBe(0x80) // IRQ flag set in status
138
136
  })
139
137
 
140
138
  it('should disable receive IRQ when RIIE bit (bit 1) is set', () => {
141
- const mockIRQ = jest.fn()
142
- serialCard.raiseIRQ = mockIRQ
143
-
144
139
  serialCard.write(0x02, 0x02) // RIIE=1: receive IRQ disabled (R6551: bit1=1 disables)
145
140
  serialCard.onData(0x42)
146
141
 
147
- expect(mockIRQ).not.toHaveBeenCalled()
142
+ const status = serialCard.read(0x01)
143
+ expect(status & 0x80).toBe(0) // IRQ flag not set
148
144
  })
149
145
 
150
146
  it('should enable echo mode when REM bit (bit 4) is set', () => {
147
+ const mockTransmit = jest.fn()
148
+ serialCard.transmit = mockTransmit
149
+
151
150
  serialCard.write(0x02, 0x10) // REM=1: echo mode enabled (bit 4 per 6551 spec)
152
151
  serialCard.onData(0x42)
153
152
 
154
- // In echo mode, received data is added to transmit buffer
155
- // Verify the transmit buffer will send the echoed byte
156
- const statusBeforeTick = serialCard.read(0x01)
157
- expect(statusBeforeTick & 0x10).toBe(0) // TDRE should be clear (data in transmit buffer)
153
+ // In echo mode, received data is echoed immediately via transmit callback
154
+ expect(mockTransmit).toHaveBeenCalledWith(0x42)
158
155
  })
159
156
  })
160
157
 
@@ -185,17 +182,12 @@ describe('ACIA (6551 ACIA)', () => {
185
182
  serialCard.transmit = mockTransmit
186
183
 
187
184
  serialCard.write(0x00, 0x42)
188
-
189
- // Need to tick enough times for byte transmission
190
- // With default baud of 115200 and 1MHz clock: ~87 cycles per byte
191
- for (let i = 0; i < 100; i++) {
192
- serialCard.tick(1000000)
193
- }
185
+ serialCard.tick(1000000) // TX happens immediately on next tick
194
186
 
195
187
  expect(mockTransmit).toHaveBeenCalledWith(0x42)
196
188
  })
197
189
 
198
- it('should handle multiple bytes in transmission', () => {
190
+ it('should only transmit the last written byte if overwritten before tick', () => {
199
191
  const mockTransmit = jest.fn()
200
192
  serialCard.transmit = mockTransmit
201
193
 
@@ -203,13 +195,11 @@ describe('ACIA (6551 ACIA)', () => {
203
195
  serialCard.write(0x00, 0x43)
204
196
  serialCard.write(0x00, 0x44)
205
197
 
206
- for (let i = 0; i < 300; i++) {
207
- serialCard.tick(1000000)
208
- }
198
+ serialCard.tick(1000000)
209
199
 
210
- expect(mockTransmit).toHaveBeenNthCalledWith(1, 0x42)
211
- expect(mockTransmit).toHaveBeenNthCalledWith(2, 0x43)
212
- expect(mockTransmit).toHaveBeenNthCalledWith(3, 0x44)
200
+ // Single-byte TX register: only the last write is transmitted
201
+ expect(mockTransmit).toHaveBeenCalledTimes(1)
202
+ expect(mockTransmit).toHaveBeenCalledWith(0x44)
213
203
  })
214
204
 
215
205
  it('should set TDRE flag after transmission complete', () => {
@@ -219,9 +209,7 @@ describe('ACIA (6551 ACIA)', () => {
219
209
  serialCard.write(0x00, 0x42)
220
210
  expect(serialCard.read(0x01) & 0x10).toBe(0) // TDRE clear
221
211
 
222
- for (let i = 0; i < 100; i++) {
223
- serialCard.tick(1000000)
224
- }
212
+ serialCard.tick(1000000)
225
213
 
226
214
  expect(serialCard.read(0x01) & 0x10).toBe(0x10) // TDRE set
227
215
  })
@@ -240,13 +228,12 @@ describe('ACIA (6551 ACIA)', () => {
240
228
  expect(status & 0x08).toBe(0x08)
241
229
  })
242
230
 
243
- it('should handle multiple received bytes', () => {
231
+ it('should handle multiple received bytes (overrun)', () => {
244
232
  serialCard.onData(0x41) // 'A'
245
- serialCard.onData(0x42) // 'B'
246
- serialCard.onData(0x43) // 'C'
233
+ serialCard.onData(0x42) // 'B' - overwrites, causes overrun
234
+ serialCard.onData(0x43) // 'C' - overwrites again
247
235
 
248
- expect(serialCard.read(0x00)).toBe(0x41)
249
- expect(serialCard.read(0x00)).toBe(0x42)
236
+ // Single-byte RX: only last byte remains
250
237
  expect(serialCard.read(0x00)).toBe(0x43)
251
238
  })
252
239
 
@@ -258,50 +245,38 @@ describe('ACIA (6551 ACIA)', () => {
258
245
  })
259
246
 
260
247
  describe('Interrupt Handling', () => {
261
- it('should trigger IRQ on receive interrupt enabled', () => {
262
- const mockIRQ = jest.fn()
263
- serialCard.raiseIRQ = mockIRQ
264
-
265
- serialCard.write(0x02, 0x04) // Enable receive IRQ
248
+ it('should set IRQ flag on receive when interrupt enabled', () => {
249
+ serialCard.write(0x02, 0x00) // bit 1 = 0: receive IRQ enabled
266
250
  serialCard.onData(0x42)
267
251
 
268
- expect(mockIRQ).toHaveBeenCalled()
269
252
  expect(serialCard.read(0x01) & 0x80).toBe(0x80) // IRQ flag set
270
253
  })
271
254
 
272
- it('should trigger IRQ on transmit complete when enabled', () => {
273
- const mockIRQ = jest.fn()
255
+ it('should return IRQ status from tick on transmit complete when enabled', () => {
274
256
  const mockTransmit = jest.fn()
275
- serialCard.raiseIRQ = mockIRQ
276
257
  serialCard.transmit = mockTransmit
277
258
 
278
259
  serialCard.write(0x03, 0x00) // Set control register
279
260
  serialCard.write(0x02, 0x04) // TIC=01: transmit IRQ enabled with /RTS low (bits 3-2 = 01)
280
261
  serialCard.write(0x00, 0x42)
281
262
 
282
- for (let i = 0; i < 100; i++) {
283
- serialCard.tick(1000000)
284
- }
263
+ const result = serialCard.tick(1000000)
285
264
 
286
- // Should trigger IRQ when transmission complete
265
+ // tick() returns IRQ status when transmit complete IRQ fires
266
+ expect(result & 0x80).toBe(0x80)
287
267
  expect(serialCard.read(0x01) & 0x80).toBe(0x80)
288
268
  })
289
269
 
290
- it('should not trigger receive IRQ when disabled', () => {
291
- const mockIRQ = jest.fn()
292
- serialCard.raiseIRQ = mockIRQ
293
-
294
- serialCard.write(0x02, 0x02) // RIIE=1: receive IRQ disabled (W65C51N: bit1=1 disables)
270
+ it('should not set IRQ flag on receive when disabled', () => {
271
+ serialCard.write(0x02, 0x02) // RIIE=1: receive IRQ disabled
295
272
  serialCard.onData(0x42)
296
273
 
297
- expect(mockIRQ).not.toHaveBeenCalled()
274
+ const status = serialCard.read(0x01)
275
+ expect(status & 0x80).toBe(0) // IRQ flag not set
298
276
  })
299
277
 
300
278
  it('should clear IRQ flag when data is read', () => {
301
- const mockIRQ = jest.fn()
302
- serialCard.raiseIRQ = mockIRQ
303
-
304
- serialCard.write(0x02, 0x04) // Enable receive IRQ
279
+ serialCard.write(0x02, 0x00) // Enable receive IRQ
305
280
  serialCard.onData(0x42)
306
281
 
307
282
  expect(serialCard.read(0x01) & 0x80).toBe(0x80) // IRQ set
@@ -324,17 +299,18 @@ describe('ACIA (6551 ACIA)', () => {
324
299
  expect(statusAfter & 0x04).toBe(0x04) // Overrun flag set
325
300
  })
326
301
 
327
- it('should clear overrun after all buffered data is read', () => {
302
+ it('should clear overrun when data register is read', () => {
328
303
  serialCard.onData(0x42)
329
- serialCard.onData(0x43) // Cause overrun
304
+ serialCard.onData(0x43) // Cause overrun (rx still full)
330
305
 
331
- serialCard.read(0x00) // Read first byte (0x42)
332
- let status = serialCard.read(0x01)
333
- expect(status & 0x04).toBe(0x04) // Overrun still set (0x43 in buffer)
306
+ // Overrun flag is set
307
+ const statusBefore = serialCard.read(0x01)
308
+ expect(statusBefore & 0x04).toBe(0x04)
334
309
 
335
- serialCard.read(0x00) // Read second byte (0x43)
336
- status = serialCard.read(0x01)
337
- expect(status & 0x04).toBe(0) // Overrun cleared now (buffer empty)
310
+ // Reading data clears overrun
311
+ serialCard.read(0x00)
312
+ const statusAfter = serialCard.read(0x01)
313
+ expect(statusAfter & 0x04).toBe(0)
338
314
  })
339
315
  })
340
316
 
@@ -346,11 +322,7 @@ describe('ACIA (6551 ACIA)', () => {
346
322
  serialCard.write(0x02, 0x10) // REM=1: echo mode enabled (bit 4 per 6551 spec)
347
323
  serialCard.onData(0x42)
348
324
 
349
- // Tick to allow transmission
350
- for (let i = 0; i < 100; i++) {
351
- serialCard.tick(1000000)
352
- }
353
-
325
+ // Echo happens immediately in onData via transmit callback
354
326
  expect(mockTransmit).toHaveBeenCalledWith(0x42)
355
327
  })
356
328
 
@@ -361,47 +333,33 @@ describe('ACIA (6551 ACIA)', () => {
361
333
  serialCard.write(0x02, 0x00) // Echo mode disabled
362
334
  serialCard.onData(0x42)
363
335
 
364
- for (let i = 0; i < 100; i++) {
365
- serialCard.tick(1000000)
366
- }
367
-
368
336
  expect(mockTransmit).not.toHaveBeenCalled()
369
337
  })
370
338
 
371
- it('should clear TDRE flag in echo mode', () => {
372
- serialCard.write(0x02, 0x10) // REM=1: echo mode enabled (bit 4 per 6551 spec)
339
+ it('should not echo data through TX register (echoes directly)', () => {
340
+ const mockTransmit = jest.fn()
341
+ serialCard.transmit = mockTransmit
342
+
343
+ serialCard.write(0x02, 0x10) // Echo mode enabled
373
344
  serialCard.onData(0x42)
374
345
 
346
+ // Echo goes directly through transmit callback, TDRE stays set
375
347
  const status = serialCard.read(0x01)
376
- expect(status & 0x10).toBe(0) // TDRE should be clear
348
+ expect(status & 0x10).toBe(0x10) // TDRE set (TX register not used for echo)
377
349
  })
378
350
  })
379
351
 
380
352
  describe('Baud Rate Configuration', () => {
381
- it('should support multiple baud rates', () => {
382
- const baudRates = [
383
- [0x00, 115200],
384
- [0x01, 50],
385
- [0x02, 75],
386
- [0x03, 110],
387
- [0x04, 135],
388
- [0x05, 150],
389
- [0x06, 300],
390
- [0x07, 600],
391
- [0x08, 1200],
392
- [0x09, 1800],
393
- [0x0A, 2400],
394
- [0x0B, 3600],
395
- [0x0C, 4800],
396
- [0x0D, 7200],
397
- [0x0E, 9600],
398
- [0x0F, 19200]
399
- ]
400
-
401
- baudRates.forEach(([code, rate]) => {
353
+ it('should accept control register writes without error', () => {
354
+ // Baud rate is no longer emulated (USB serial operates at USB speeds)
355
+ // but the control register should still be writable/readable
356
+ const baudCodes = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
357
+ 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F]
358
+
359
+ baudCodes.forEach((code) => {
402
360
  const card = new ACIA()
403
- card.write(0x03, code as number) // Control register with baud rate code
404
- // Baud rate affects tick timing - verify no errors occur
361
+ card.write(0x03, code)
362
+ expect(card.read(0x03)).toBe(code)
405
363
  })
406
364
  })
407
365
  })
@@ -470,30 +428,10 @@ describe('ACIA (6551 ACIA)', () => {
470
428
  serialCard.transmit = mockTransmit
471
429
 
472
430
  serialCard.write(0x00, 0x42)
473
- for (let i = 0; i < 100; i++) {
474
- serialCard.tick(1000000)
475
- }
431
+ serialCard.tick(1000000)
476
432
 
477
433
  expect(mockTransmit).toHaveBeenCalled()
478
434
  })
479
-
480
- it('should support custom IRQ callback', () => {
481
- const mockIRQ = jest.fn()
482
- serialCard.raiseIRQ = mockIRQ
483
-
484
- serialCard.write(0x02, 0x04)
485
- serialCard.onData(0x42)
486
-
487
- expect(mockIRQ).toHaveBeenCalled()
488
- })
489
-
490
- it('should support custom NMI callback', () => {
491
- const mockNMI = jest.fn()
492
- serialCard.raiseNMI = mockNMI
493
-
494
- // NMI not currently triggered in implementation, but callback exists
495
- expect(typeof serialCard.raiseNMI).toBe('function')
496
- })
497
435
  })
498
436
 
499
437
  describe('Edge Cases', () => {