ac6502 1.11.0 → 1.12.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.
@@ -12,9 +12,6 @@ import { Video } from './IO/Video'
12
12
  import { KeyboardMatrixAttachment } from './IO/Attachments/KeyboardMatrixAttachment'
13
13
  import { KeyboardEncoderAttachment } from './IO/Attachments/KeyboardEncoderAttachment'
14
14
  import { JoystickAttachment } from './IO/Attachments/JoystickAttachment'
15
- import { LCDAttachment } from './IO/Attachments/LCDAttachment'
16
- import { KeypadAttachment } from './IO/Attachments/KeypadAttachment'
17
- import { Empty } from './IO/Empty'
18
15
  import { IO } from './IO'
19
16
 
20
17
  export class Machine {
@@ -29,14 +26,14 @@ export class Machine {
29
26
  rom: ROM
30
27
  cart?: Cart
31
28
 
32
- io1: IO
33
- io2: IO
34
- io3: IO
35
- io4: IO
36
- io5: IO
37
- io6: IO
38
- io7: IO
39
- io8: IO
29
+ io1!: IO
30
+ io2!: IO
31
+ io3!: IO
32
+ io4!: IO
33
+ io5!: IO
34
+ io6!: IO
35
+ io7!: IO
36
+ io8!: IO
40
37
 
41
38
  // VIA Attachments
42
39
  keyboardMatrixAttachment?: KeyboardMatrixAttachment
@@ -44,11 +41,6 @@ export class Machine {
44
41
  joystickAttachmentA?: JoystickAttachment
45
42
  joystickAttachmentB?: JoystickAttachment
46
43
 
47
- // KIM mode attachments
48
- lcdAttachment?: LCDAttachment
49
- keypadAttachment?: KeypadAttachment
50
-
51
- target: string
52
44
  isRunning: boolean = false
53
45
  frequency: number = 1000000 // 1 MHz
54
46
  scale: number = 2
@@ -64,194 +56,62 @@ export class Machine {
64
56
  // Initialization
65
57
  //
66
58
 
67
- constructor(target: string) {
68
- this.target = target
59
+ constructor() {
69
60
  this.cpu = new CPU(this.read.bind(this), this.write.bind(this))
70
61
  this.ram = new RAM()
71
62
  this.rom = new ROM()
72
63
 
73
- this.io1 = new Empty()
74
- this.io2 = new Empty()
75
- this.io3 = new Empty()
76
- this.io4 = new Empty()
77
- this.io5 = new Empty()
78
- this.io6 = new Empty()
79
- this.io7 = new Empty()
80
- this.io8 = new Empty()
81
-
82
- this.configureTarget(target)
64
+ this.configure()
83
65
 
84
66
  this.startTime = Date.now()
85
67
  this.cpu.reset()
86
68
  }
87
69
 
88
- configureTarget(target: string): void {
89
- if (target === 'kim') {
90
- const acia = new ACIA()
91
- this.io5 = acia
92
-
93
- // Connect ACIA transmit callback
94
- acia.transmit = (data: number) => {
95
- if (this.transmit) {
96
- this.transmit(data)
97
- }
98
- }
99
-
100
- this.io1 = new Empty()
101
- this.io2 = new Empty()
102
- this.io3 = new Empty()
103
- this.io4 = new Empty()
104
- this.io6 = new Empty()
105
- this.io7 = new Empty()
106
-
107
- const via = new VIA()
108
- this.io8 = via
109
-
110
- // Create KIM GPIO Attachments
111
- this.lcdAttachment = new LCDAttachment(16, 2, 10)
112
- this.keypadAttachment = new KeypadAttachment(true, 20)
113
-
114
- // Attach LCD to Port A (control: RS/RW/E on bits 5-7) and Port B (data bus)
115
- via.attachToPortA(this.lcdAttachment)
116
- via.attachToPortB(this.lcdAttachment)
117
-
118
- // Attach keypad to Port A (bits 0-4)
119
- via.attachToPortA(this.keypadAttachment)
120
- } else if (target === 'dev') {
121
- const acia = new ACIA()
122
- this.io5 = acia
123
-
124
- // Connect ACIA transmit callback
125
- acia.transmit = (data: number) => {
126
- if (this.transmit) {
127
- this.transmit(data)
128
- }
129
- }
130
-
131
- const rtc = new RTC()
132
- const storage = new Storage()
133
- const via = new VIA()
134
- const sound = new Sound()
135
- const video = new Video()
136
-
137
- this.io1 = new RAMBank()
138
- this.io2 = new RAMBank()
139
- this.io3 = rtc
140
- this.io4 = storage
141
- this.io6 = via
142
- this.io7 = sound
143
- this.io8 = video
144
-
145
- // Connect Sound pushSamples callback
146
- sound.pushSamples = (samples: Float32Array) => {
147
- if (this.play) {
148
- this.play(samples)
149
- }
150
- }
151
-
152
- // Create standard GPIO attachments
153
- this.keyboardMatrixAttachment = new KeyboardMatrixAttachment(10)
154
- this.keyboardEncoderAttachment = new KeyboardEncoderAttachment(20)
155
- this.joystickAttachmentA = new JoystickAttachment(false, 100)
156
- this.joystickAttachmentB = new JoystickAttachment(false, 100)
157
-
158
- // Attach peripherals to GPIO Card
159
- via.attachToPortA(this.keyboardMatrixAttachment)
160
- via.attachToPortB(this.keyboardMatrixAttachment)
161
- via.attachToPortA(this.keyboardEncoderAttachment)
162
- via.attachToPortB(this.keyboardEncoderAttachment)
163
- via.attachToPortA(this.joystickAttachmentA)
164
- via.attachToPortB(this.joystickAttachmentB)
165
- } else if (target === 'vcs') {
166
- this.io5 = new Empty()
167
-
168
- const via = new VIA()
169
- const sound = new Sound()
170
- const video = new Video()
171
-
172
- this.io1 = new Empty()
173
- this.io2 = new Empty()
174
- this.io3 = new Empty()
175
- this.io4 = new Empty()
176
- this.io6 = via
177
- this.io7 = sound
178
- this.io8 = video
179
-
180
- // Connect Sound pushSamples callback
181
- sound.pushSamples = (samples: Float32Array) => {
182
- if (this.play) {
183
- this.play(samples)
184
- }
185
- }
70
+ private configure(): void {
71
+ const acia = new ACIA()
72
+ this.io5 = acia
186
73
 
187
- // Create standard GPIO attachments
188
- this.keyboardMatrixAttachment = new KeyboardMatrixAttachment(10)
189
- this.keyboardEncoderAttachment = new KeyboardEncoderAttachment(20)
190
- this.joystickAttachmentA = new JoystickAttachment(false, 100)
191
- this.joystickAttachmentB = new JoystickAttachment(false, 100)
192
-
193
- // Attach peripherals to GPIO Card
194
- via.attachToPortA(this.keyboardMatrixAttachment)
195
- via.attachToPortB(this.keyboardMatrixAttachment)
196
- via.attachToPortA(this.keyboardEncoderAttachment)
197
- via.attachToPortB(this.keyboardEncoderAttachment)
198
- via.attachToPortA(this.joystickAttachmentA)
199
- via.attachToPortB(this.joystickAttachmentB)
200
- } else if (target === 'cob') {
201
- const acia = new ACIA()
202
- this.io5 = acia
203
-
204
- // Connect ACIA transmit callback
205
- acia.transmit = (data: number) => {
206
- if (this.transmit) {
207
- this.transmit(data)
208
- }
74
+ // Connect ACIA transmit callback
75
+ acia.transmit = (data: number) => {
76
+ if (this.transmit) {
77
+ this.transmit(data)
209
78
  }
79
+ }
210
80
 
211
- const rtc = new RTC()
212
- const storage = new Storage()
213
- const via = new VIA()
214
- const sound = new Sound()
215
- const video = new Video()
216
-
217
- this.io1 = new RAMBank()
218
- this.io2 = new RAMBank()
219
- this.io3 = rtc
220
- this.io4 = storage
221
- this.io6 = via
222
- this.io7 = sound
223
- this.io8 = video
224
-
225
- // Connect Sound pushSamples callback
226
- sound.pushSamples = (samples: Float32Array) => {
227
- if (this.play) {
228
- this.play(samples)
229
- }
81
+ const rtc = new RTC()
82
+ const storage = new Storage()
83
+ const via = new VIA()
84
+ const sound = new Sound()
85
+ const video = new Video()
86
+
87
+ this.io1 = new RAMBank()
88
+ this.io2 = new RAMBank()
89
+ this.io3 = rtc
90
+ this.io4 = storage
91
+ this.io6 = via
92
+ this.io7 = sound
93
+ this.io8 = video
94
+
95
+ // Connect Sound pushSamples callback
96
+ sound.pushSamples = (samples: Float32Array) => {
97
+ if (this.play) {
98
+ this.play(samples)
230
99
  }
231
-
232
- // Create standard GPIO attachments
233
- this.keyboardMatrixAttachment = new KeyboardMatrixAttachment(10)
234
- this.keyboardEncoderAttachment = new KeyboardEncoderAttachment(20)
235
- this.joystickAttachmentA = new JoystickAttachment(false, 100)
236
- this.joystickAttachmentB = new JoystickAttachment(false, 100)
237
-
238
- // Attach peripherals to GPIO Card
239
- via.attachToPortA(this.keyboardMatrixAttachment)
240
- via.attachToPortB(this.keyboardMatrixAttachment)
241
- via.attachToPortA(this.keyboardEncoderAttachment)
242
- via.attachToPortB(this.keyboardEncoderAttachment)
243
- via.attachToPortA(this.joystickAttachmentA)
244
- via.attachToPortB(this.joystickAttachmentB)
245
- } else {
246
- this.io1 = new Empty()
247
- this.io2 = new Empty()
248
- this.io3 = new Empty()
249
- this.io4 = new Empty()
250
- this.io5 = new Empty()
251
- this.io6 = new Empty()
252
- this.io7 = new Empty()
253
- this.io8 = new Empty()
254
100
  }
101
+
102
+ // Create standard GPIO attachments
103
+ this.keyboardMatrixAttachment = new KeyboardMatrixAttachment(10)
104
+ this.keyboardEncoderAttachment = new KeyboardEncoderAttachment(20)
105
+ this.joystickAttachmentA = new JoystickAttachment(false, 100)
106
+ this.joystickAttachmentB = new JoystickAttachment(false, 100)
107
+
108
+ // Attach peripherals to GPIO Card
109
+ via.attachToPortA(this.keyboardMatrixAttachment)
110
+ via.attachToPortB(this.keyboardMatrixAttachment)
111
+ via.attachToPortA(this.keyboardEncoderAttachment)
112
+ via.attachToPortB(this.keyboardEncoderAttachment)
113
+ via.attachToPortA(this.joystickAttachmentA)
114
+ via.attachToPortB(this.joystickAttachmentB)
255
115
  }
256
116
 
257
117
  //
@@ -350,25 +210,17 @@ export class Machine {
350
210
  }
351
211
 
352
212
  onReceive(data: number): void {
353
- if (this.target !== 'vcs') {
354
- (this.io5 as ACIA).onData(data) // Pass data to Serial card
355
- }
213
+ (this.io5 as ACIA).onData(data) // Pass data to Serial card
356
214
  }
357
215
 
358
216
  onKeyDown(scancode: number): void {
359
- if (this.target === 'kim') {
360
- this.keypadAttachment?.updateKey(scancode, true)
361
- } else {
362
- this.keyboardMatrixAttachment?.updateKey(scancode, true)
363
- this.keyboardEncoderAttachment?.updateKey(scancode, true)
364
- }
217
+ this.keyboardMatrixAttachment?.updateKey(scancode, true)
218
+ this.keyboardEncoderAttachment?.updateKey(scancode, true)
365
219
  }
366
220
 
367
221
  onKeyUp(scancode: number): void {
368
- if (this.target !== 'kim') {
369
- this.keyboardMatrixAttachment?.updateKey(scancode, false)
370
- this.keyboardEncoderAttachment?.updateKey(scancode, false)
371
- }
222
+ this.keyboardMatrixAttachment?.updateKey(scancode, false)
223
+ this.keyboardEncoderAttachment?.updateKey(scancode, false)
372
224
  }
373
225
 
374
226
  onJoystickA(buttons: number): void {
@@ -408,13 +260,10 @@ export class Machine {
408
260
  (this as any)._accumulatorMs = accumulator
409
261
  }
410
262
 
411
- if (this.render && (this.target === 'kim' || this.target === 'dev')) {
412
- this.render()
413
- this.frames += 1
414
- } else if (this.render && (this.target === 'cob' || this.target === 'vcs')) {
415
- const Video = this.io8 as Video
416
- if (Video.frameReady) {
417
- Video.frameReady = false
263
+ if (this.render) {
264
+ const video = this.io8 as Video
265
+ if (video.frameReady) {
266
+ video.frameReady = false
418
267
  this.render()
419
268
  this.frames += 1
420
269
  }
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.11.0'
14
+ const VERSION = '1.12.0'
15
15
  const WIDTH = 320
16
16
  const HEIGHT = 240
17
17
 
@@ -45,7 +45,6 @@ interface EmulatorOptions {
45
45
  stopbits?: string
46
46
  port?: string
47
47
  storage?: string
48
- target?: string
49
48
  encoder?: string
50
49
  }
51
50
 
@@ -61,7 +60,7 @@ class Emulator {
61
60
 
62
61
  constructor(options: EmulatorOptions) {
63
62
  this.options = options
64
- this.machine = new Machine(options.target ?? 'cob')
63
+ this.machine = new Machine()
65
64
  this.controllers = new Map()
66
65
  this.joystickButtonStateA = 0x00
67
66
  this.joystickButtonStateB = 0x00
@@ -111,7 +110,7 @@ class Emulator {
111
110
  console.log('Loaded Cart: NONE')
112
111
  }
113
112
 
114
- if (this.options.storage && this.options.target !== 'kim') {
113
+ if (this.options.storage) {
115
114
  if (existsSync(this.options.storage)) {
116
115
  const storageData = await readFile(this.options.storage)
117
116
  ;(this.machine.io4 as Storage).loadData(new Uint8Array(storageData))
@@ -217,7 +216,6 @@ class Emulator {
217
216
  }
218
217
 
219
218
  private setupAudio(): void {
220
- if (this.options.target === 'kim') return
221
219
  try {
222
220
  this.audioDevice = sdl.audio.openDevice({ type: 'playback' }, {
223
221
  channels: AUDIO_CHANNELS as 1,
@@ -226,10 +224,7 @@ class Emulator {
226
224
  buffered: AUDIO_BUFFERED,
227
225
  })
228
226
 
229
- // Configure Sound sample rate to match audio device (not needed for dev target)
230
- if (this.options.target !== 'dev') {
231
- ;(this.machine.io7 as Sound).sampleRate = this.audioDevice.frequency
232
- }
227
+ ;(this.machine.io7 as Sound).sampleRate = this.audioDevice.frequency
233
228
 
234
229
  // Connect the Machine's audio callback to the SDL audio device
235
230
  this.machine.play = (samples: Float32Array) => {
@@ -254,29 +249,10 @@ class Emulator {
254
249
  }
255
250
 
256
251
  private setupWindow(): void {
257
- const isKIM = this.options.target === 'kim'
258
- const lcd = this.machine.lcdAttachment
259
-
260
- // LCD dot-matrix rendering constants
261
- const DOT_SIZE = 2 // Each LCD dot rendered as DOT_SIZE x DOT_SIZE pixels
262
- const DOT_GAP = 1 // Gap between dots
263
- const LCD_PADDING = 8 // Green border padding around the display
264
- const CELL = DOT_SIZE + DOT_GAP
265
-
266
- let windowWidth: number
267
- let windowHeight: number
268
- if (isKIM && lcd) {
269
- windowWidth = LCD_PADDING * 2 + lcd.pixelsWidth * CELL
270
- windowHeight = LCD_PADDING * 2 + lcd.pixelsHeight * CELL
271
- } else {
272
- windowWidth = WIDTH
273
- windowHeight = HEIGHT
274
- }
275
-
276
252
  this.window = sdl.video.createWindow({
277
- title: `6502 Emulator (${(this.options.target ?? 'cob').toUpperCase()})`,
278
- width: windowWidth * this.machine.scale,
279
- height: windowHeight * this.machine.scale,
253
+ title: '6502 Emulator (COB)',
254
+ width: WIDTH * this.machine.scale,
255
+ height: HEIGHT * this.machine.scale,
280
256
  accelerated: true,
281
257
  vsync: true
282
258
  })
@@ -291,83 +267,10 @@ class Emulator {
291
267
  this.machine.onKeyUp(event.scancode)
292
268
  })
293
269
 
294
- if (isKIM && lcd) {
295
- const lcdWidth = lcd.pixelsWidth
296
- const lcdHeight = lcd.pixelsHeight
297
- const renderWidth = windowWidth
298
- const renderHeight = windowHeight
299
- const rgbaBuffer = Buffer.alloc(renderWidth * renderHeight * 4)
300
-
301
- // Pre-fill with background color (LCD green for padding + gaps)
302
- for (let i = 0; i < renderWidth * renderHeight; i++) {
303
- const off = i * 4
304
- rgbaBuffer[off] = 0x50
305
- rgbaBuffer[off + 1] = 0x88
306
- rgbaBuffer[off + 2] = 0x38
307
- rgbaBuffer[off + 3] = 0xFF
308
- }
309
-
310
- this.machine.render = () => {
311
- if (!this.window) { return }
312
- const buf = lcd.buffer
313
-
314
- // Reset buffer to background color
315
- for (let i = 0; i < renderWidth * renderHeight; i++) {
316
- const off = i * 4
317
- rgbaBuffer[off] = 0x50
318
- rgbaBuffer[off + 1] = 0x88
319
- rgbaBuffer[off + 2] = 0x38
320
- rgbaBuffer[off + 3] = 0xFF
321
- }
322
-
323
- // Render each buffer pixel as a dot block
324
- for (let by = 0; by < lcdHeight; by++) {
325
- for (let bx = 0; bx < lcdWidth; bx++) {
326
- const val = buf[by * lcdWidth + bx]
327
-
328
- if (val < 0) {
329
- // Gap pixel - skip, shows background color
330
- continue
331
- }
332
-
333
- let r: number, g: number, b: number
334
- if (val === 0) {
335
- // Pixel off - slightly brighter than background for visible dot grid
336
- r = 0x60; g = 0xA0; b = 0x40
337
- } else {
338
- // Pixel on - dark
339
- r = 0x10; g = 0x20; b = 0x10
340
- }
341
-
342
- // Draw DOT_SIZE x DOT_SIZE block
343
- const screenX = LCD_PADDING + bx * CELL
344
- const screenY = LCD_PADDING + by * CELL
345
- for (let dy = 0; dy < DOT_SIZE; dy++) {
346
- for (let dx = 0; dx < DOT_SIZE; dx++) {
347
- const off = ((screenY + dy) * renderWidth + (screenX + dx)) * 4
348
- rgbaBuffer[off] = r
349
- rgbaBuffer[off + 1] = g
350
- rgbaBuffer[off + 2] = b
351
- rgbaBuffer[off + 3] = 0xFF
352
- }
353
- }
354
- }
355
- }
356
-
357
- this.window.render(renderWidth, renderHeight, renderWidth * 4, 'rgba32', rgbaBuffer)
358
- }
359
- } else if (this.options.target === 'cob' || this.options.target === 'vcs') {
360
- const Video = this.machine.io8 as Video
361
- this.machine.render = () => {
362
- if (!this.window) { return }
363
- this.window.render(WIDTH, HEIGHT, WIDTH * 4, 'rgba32', Video.buffer)
364
- }
365
- } else if (this.options.target === 'dev') {
366
- const video = this.machine.io8 as Video
367
- this.machine.render = () => {
368
- if (!this.window) { return }
369
- this.window.render(WIDTH, HEIGHT, WIDTH * 4, 'rgba32', video.buffer)
370
- }
270
+ const video = this.machine.io8 as Video
271
+ this.machine.render = () => {
272
+ if (!this.window) { return }
273
+ this.window.render(WIDTH, HEIGHT, WIDTH * 4, 'rgba32', video.buffer)
371
274
  }
372
275
 
373
276
  this.window.on('close', () => this.shutdown())
@@ -379,7 +282,6 @@ class Emulator {
379
282
  }
380
283
 
381
284
  private setupControllers(): void {
382
- if (this.options.target === 'kim') return
383
285
  // Controller device add/remove handlers
384
286
  (sdl.controller as any).on('deviceAdd', (device: any) => {
385
287
  console.log(`Controller added: ${device.name || device.id}`)
@@ -557,7 +459,7 @@ class Emulator {
557
459
  })
558
460
 
559
461
  // Save storage data if path was provided
560
- if (this.options.storage && this.options.target !== 'kim') {
462
+ if (this.options.storage) {
561
463
  const storageData = (this.machine.io4 as Storage).getData()
562
464
  writeFile(this.options.storage, storageData).then(() => {
563
465
  console.log(`Storage saved to: ${this.options.storage}`)
@@ -593,7 +495,6 @@ program
593
495
  .addOption(new Option('-s, --scale <scale>', 'Set the emulator scale').default('2'))
594
496
  .addOption(new Option('-S, --storage <path>', 'Path to storage data file for Compact Flash card persistence'))
595
497
  .addOption(new Option('-t, --stopbits <stopbits>', 'Stop Bits (1 | 1.5 | 2)').default('1'))
596
- .addOption(new Option('-T, --target <target>', 'System target').choices(['cob', 'vcs', 'kim', 'dev']).default('cob'))
597
498
  .addOption(new Option('-e, --encoder <mode>', 'Keyboard encoder active port (ps2 = Port A / CA1, matrix = Port B / CB1)').choices(['ps2', 'matrix', 'both']).default('matrix'))
598
499
  .addHelpText('beforeAll', figlet.textSync('6502 Emulator', { font: 'cricket' }) + '\n' + `Version: ${VERSION} | A.C. Wright Design\n`)
599
500
  .parse(process.argv)
package/src/lib.ts CHANGED
@@ -22,5 +22,3 @@ export { AttachmentBase } from './components/IO/Attachments/Attachment'
22
22
  export { JoystickAttachment } from './components/IO/Attachments/JoystickAttachment'
23
23
  export { KeyboardEncoderAttachment } from './components/IO/Attachments/KeyboardEncoderAttachment'
24
24
  export { KeyboardMatrixAttachment } from './components/IO/Attachments/KeyboardMatrixAttachment'
25
- export { KeypadAttachment } from './components/IO/Attachments/KeypadAttachment'
26
- export { LCDAttachment } from './components/IO/Attachments/LCDAttachment'
@@ -8,7 +8,7 @@ describe('Machine', () => {
8
8
  let machine: Machine
9
9
 
10
10
  beforeEach(() => {
11
- machine = new Machine('cob')
11
+ machine = new Machine()
12
12
  })
13
13
 
14
14
  afterEach(() => {
@@ -1,153 +0,0 @@
1
- import { AttachmentBase } from './Attachment'
2
-
3
- /**
4
- * USB HID keycode to keypad value mapping
5
- * Maps USB HID usage IDs to the 5-bit keypad codes ($00-$17) per the 6502 Keypad Mapping table
6
- *
7
- * Keypad layout (4 columns × 6 rows = 24 keys):
8
- * $00 = ◄ $01=1 $02=2 $03=3
9
- * $04 = 4 $05=5 $06=6 $07=7
10
- * $08 = 8 $09=9 $0A=0 $0B=►
11
- * $0C = F $0D=E $0E=D $0F=C
12
- * $10 = ESC $11=INS $12=PGUP $13=A
13
- * $14 = ▲/Enter $15=DEL $16=PGDN $17=B
14
- */
15
- const USB_HID_TO_KEYPAD: { [key: number]: number } = {
16
- 0x50: 0x00, // Left Arrow → ◄
17
- 0x1E: 0x01, // 1
18
- 0x1F: 0x02, // 2
19
- 0x20: 0x03, // 3
20
- 0x21: 0x04, // 4
21
- 0x22: 0x05, // 5
22
- 0x23: 0x06, // 6
23
- 0x24: 0x07, // 7
24
- 0x25: 0x08, // 8
25
- 0x26: 0x09, // 9
26
- 0x27: 0x0A, // 0
27
- 0x4F: 0x0B, // Right Arrow → ►
28
- 0x09: 0x0C, // f → F
29
- 0x08: 0x0D, // e → E
30
- 0x07: 0x0E, // d → D
31
- 0x06: 0x0F, // c → C
32
- 0x29: 0x10, // Escape → ESC
33
- 0x49: 0x11, // Insert → INS
34
- 0x4B: 0x12, // Page Up → PGUP
35
- 0x04: 0x13, // a → A
36
- 0x52: 0x14, // Up Arrow → ▲
37
- 0x28: 0x14, // Enter → ▲
38
- 0x4C: 0x15, // Delete → DEL
39
- 0x4E: 0x16, // Page Down → PGDN
40
- 0x05: 0x17, // b → B
41
- }
42
-
43
- /**
44
- * KeypadAttachment - Emulates a 4×6 matrix keypad with a built-in hardware encoder
45
- *
46
- * The encoder converts a key press into a 5-bit code (PA0–PA4) that appears on the GPIO
47
- * port. Bits 5–7 are never driven by the keypad and always read as 0 when data is present.
48
- *
49
- * Behaviour mirrors a typical 74C922-style encoder:
50
- * - On key press → the 5-bit keypad code is latched and a CA1/CB1 interrupt is asserted
51
- * - On port read → the latched code is returned on bits 0–4 (bits 5–7 = 0)
52
- * - clearInterrupts → clears the interrupt and the data-ready latch
53
- * - Key releases → ignored (encoder only reports on the falling edge of a keypress)
54
- *
55
- * The attachment may be wired to either Port A or Port B via the constructor parameter.
56
- * CA1/CB1 is the DA (Data Available) interrupt line from the 74C922.
57
- * CA2/CB2 is connected to the 74C922 OE (Output Enable) pin; data is only driven onto the
58
- * bus when OE is asserted LOW by the 6522.
59
- */
60
- export class KeypadAttachment extends AttachmentBase {
61
- private keypadValue: number = 0x00
62
- private dataReady: boolean = false
63
- private interruptPending: boolean = false
64
- private readonly attachedToPortA: boolean
65
-
66
- // OE state: CA2 for Port A, CB2 for Port B. HIGH = output disabled (default).
67
- private oeState: boolean = true
68
-
69
- constructor(attachToPortA: boolean = true, priority: number = 0) {
70
- super(priority, false, false, false, false)
71
- this.attachedToPortA = attachToPortA
72
- this.reset()
73
- }
74
-
75
- reset(): void {
76
- super.reset()
77
- this.keypadValue = 0x00
78
- this.dataReady = false
79
- this.interruptPending = false
80
- this.oeState = true // OE disabled until explicitly asserted by the 6522
81
- }
82
-
83
- updateControlLines(ca1: boolean, ca2: boolean, cb1: boolean, cb2: boolean): void {
84
- // CA2 controls OE for Port A; CB2 controls OE for Port B.
85
- // 74C922 OE is active-LOW, so a LOW signal enables the output.
86
- this.oeState = this.attachedToPortA ? ca2 : cb2
87
- }
88
-
89
- readPortA(ddr: number, or: number): number {
90
- // Only drive the bus when attached to Port A, OE is asserted (LOW), and data is latched
91
- if (this.attachedToPortA && !this.oeState && this.dataReady) {
92
- return this.keypadValue & 0x1F // bits 0–4 only; bits 5–7 = 0
93
- }
94
- return 0xFF // not driving the bus
95
- }
96
-
97
- readPortB(ddr: number, or: number): number {
98
- // Only drive the bus when attached to Port B, OE is asserted (LOW), and data is latched
99
- if (!this.attachedToPortA && !this.oeState && this.dataReady) {
100
- return this.keypadValue & 0x1F // bits 0–4 only; bits 5–7 = 0
101
- }
102
- return 0xFF // not driving the bus
103
- }
104
-
105
- hasCA1Interrupt(): boolean {
106
- return this.attachedToPortA && this.interruptPending
107
- }
108
-
109
- hasCB1Interrupt(): boolean {
110
- return !this.attachedToPortA && this.interruptPending
111
- }
112
-
113
- clearInterrupts(ca1: boolean, ca2: boolean, cb1: boolean, cb2: boolean): void {
114
- if ((this.attachedToPortA && ca1) || (!this.attachedToPortA && cb1)) {
115
- this.interruptPending = false
116
- this.dataReady = false
117
- }
118
- }
119
-
120
- /**
121
- * Notify the attachment of a USB HID key event.
122
- * Key releases are ignored; only presses generate output on the GPIO port.
123
- *
124
- * @param usbHidKeycode - USB HID usage ID for the key
125
- * @param pressed - true for key-down, false for key-up
126
- */
127
- updateKey(usbHidKeycode: number, pressed: boolean): void {
128
- if (!pressed) {
129
- return
130
- }
131
-
132
- const keypadCode = USB_HID_TO_KEYPAD[usbHidKeycode]
133
- if (keypadCode === undefined) {
134
- return // key is not present on this keypad
135
- }
136
-
137
- this.keypadValue = keypadCode
138
- this.dataReady = true
139
- this.interruptPending = true
140
- }
141
-
142
- /**
143
- * Returns the current latched keypad code (bits 0–4) or 0xFF if no data is ready.
144
- */
145
- getCurrentKey(): number {
146
- return this.dataReady ? (this.keypadValue & 0x1F) : 0xFF
147
- }
148
-
149
- /** Returns true when a key has been pressed and the latch has not yet been cleared. */
150
- hasDataReady(): boolean {
151
- return this.dataReady
152
- }
153
- }