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.
- package/dist/components/CPU.d.ts +4 -0
- package/dist/components/CPU.js +87 -30
- package/dist/components/CPU.js.map +1 -1
- package/dist/components/IO/ACIA.d.ts +13 -21
- package/dist/components/IO/ACIA.js +53 -151
- package/dist/components/IO/ACIA.js.map +1 -1
- package/dist/components/IO/Attachments/KeyboardEncoderAttachment.d.ts +5 -10
- package/dist/components/IO/Attachments/KeyboardEncoderAttachment.js +43 -266
- package/dist/components/IO/Attachments/KeyboardEncoderAttachment.js.map +1 -1
- package/dist/components/IO/Empty.d.ts +1 -3
- package/dist/components/IO/Empty.js +1 -5
- package/dist/components/IO/Empty.js.map +1 -1
- package/dist/components/IO/RAMBank.d.ts +3 -4
- package/dist/components/IO/RAMBank.js +4 -13
- package/dist/components/IO/RAMBank.js.map +1 -1
- package/dist/components/IO/RTC.d.ts +2 -3
- package/dist/components/IO/RTC.js +17 -7
- package/dist/components/IO/RTC.js.map +1 -1
- package/dist/components/IO/Sound.d.ts +1 -3
- package/dist/components/IO/Sound.js +13 -23
- package/dist/components/IO/Sound.js.map +1 -1
- package/dist/components/IO/Storage.d.ts +1 -3
- package/dist/components/IO/Storage.js +1 -3
- package/dist/components/IO/Storage.js.map +1 -1
- package/dist/components/IO/VIA.d.ts +1 -3
- package/dist/components/IO/VIA.js +6 -7
- package/dist/components/IO/VIA.js.map +1 -1
- package/dist/components/IO/Video.d.ts +1 -3
- package/dist/components/IO/Video.js +3 -5
- package/dist/components/IO/Video.js.map +1 -1
- package/dist/components/IO.d.ts +1 -3
- package/dist/components/Machine.d.ts +1 -2
- package/dist/components/Machine.js +21 -74
- package/dist/components/Machine.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tests/IO/ACIA.test.js +57 -108
- package/dist/tests/IO/ACIA.test.js.map +1 -1
- package/dist/tests/IO/Attachments/KeyboardEncoderAttachment.test.js +334 -574
- package/dist/tests/IO/Attachments/KeyboardEncoderAttachment.test.js.map +1 -1
- package/dist/tests/IO/Empty.test.js +2 -14
- package/dist/tests/IO/Empty.test.js.map +1 -1
- package/dist/tests/IO/RAMBank.test.js +5 -12
- package/dist/tests/IO/RAMBank.test.js.map +1 -1
- package/dist/tests/IO/RTC.test.js +7 -16
- package/dist/tests/IO/RTC.test.js.map +1 -1
- package/dist/tests/IO/Sound.test.js +6 -8
- package/dist/tests/IO/Sound.test.js.map +1 -1
- package/dist/tests/IO/Storage.test.js +0 -6
- package/dist/tests/IO/Storage.test.js.map +1 -1
- package/dist/tests/IO/VIA.test.js +6 -10
- package/dist/tests/IO/VIA.test.js.map +1 -1
- package/dist/tests/IO/Video.test.js +7 -7
- package/dist/tests/IO/Video.test.js.map +1 -1
- package/package.json +1 -1
- package/src/components/CPU.ts +94 -31
- package/src/components/IO/ACIA.ts +57 -176
- package/src/components/IO/Attachments/KeyboardEncoderAttachment.ts +45 -217
- package/src/components/IO/Empty.ts +1 -4
- package/src/components/IO/RAMBank.ts +4 -15
- package/src/components/IO/RTC.ts +18 -7
- package/src/components/IO/Sound.ts +14 -27
- package/src/components/IO/Storage.ts +2 -5
- package/src/components/IO/VIA.ts +6 -8
- package/src/components/IO/Video.ts +5 -7
- package/src/components/IO.ts +1 -4
- package/src/components/Machine.ts +22 -90
- package/src/index.ts +1 -1
- package/src/tests/IO/ACIA.test.ts +60 -122
- package/src/tests/IO/Attachments/KeyboardEncoderAttachment.test.ts +342 -676
- package/src/tests/IO/Empty.test.ts +2 -17
- package/src/tests/IO/RAMBank.test.ts +5 -14
- package/src/tests/IO/RTC.test.ts +7 -20
- package/src/tests/IO/Sound.test.ts +6 -8
- package/src/tests/IO/Storage.test.ts +0 -7
- package/src/tests/IO/VIA.test.ts +6 -12
- 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):
|
|
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
|
|
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)
|
package/src/components/IO.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
//
|
|
392
|
-
this.
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
@@ -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
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
155
|
-
|
|
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
|
|
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
|
-
|
|
207
|
-
serialCard.tick(1000000)
|
|
208
|
-
}
|
|
198
|
+
serialCard.tick(1000000)
|
|
209
199
|
|
|
210
|
-
|
|
211
|
-
expect(mockTransmit).
|
|
212
|
-
expect(mockTransmit).
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
262
|
-
|
|
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
|
|
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
|
-
|
|
283
|
-
serialCard.tick(1000000)
|
|
284
|
-
}
|
|
263
|
+
const result = serialCard.tick(1000000)
|
|
285
264
|
|
|
286
|
-
//
|
|
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
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
expect(
|
|
306
|
+
// Overrun flag is set
|
|
307
|
+
const statusBefore = serialCard.read(0x01)
|
|
308
|
+
expect(statusBefore & 0x04).toBe(0x04)
|
|
334
309
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
//
|
|
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
|
|
372
|
-
|
|
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(
|
|
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
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
|
404
|
-
|
|
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
|
-
|
|
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', () => {
|