ac6502 1.11.0 → 1.13.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.
@@ -1,795 +0,0 @@
1
- import {
2
- LCDAttachment,
3
- LCD_CMD_CLEAR,
4
- LCD_CMD_HOME,
5
- LCD_CMD_ENTRY_MODE,
6
- LCD_CMD_ENTRY_MODE_INCREMENT,
7
- LCD_CMD_ENTRY_MODE_SHIFT,
8
- LCD_CMD_DISPLAY,
9
- LCD_CMD_DISPLAY_ON,
10
- LCD_CMD_DISPLAY_CURSOR,
11
- LCD_CMD_DISPLAY_CURSOR_BLINK,
12
- LCD_CMD_SHIFT,
13
- LCD_CMD_SHIFT_DISPLAY,
14
- LCD_CMD_SHIFT_RIGHT,
15
- LCD_CMD_FUNCTION,
16
- LCD_CMD_FUNCTION_LCD_2LINE,
17
- LCD_CMD_SET_CGRAM_ADDR,
18
- LCD_CMD_SET_DRAM_ADDR,
19
- } from '../../../components/IO/Attachments/LCDAttachment'
20
-
21
- // VIA Port A pin masks
22
- const PIN_RS = 0x20
23
- const PIN_RW = 0x40
24
- const PIN_E = 0x80
25
-
26
- describe('LCDAttachment', () => {
27
- let lcd: LCDAttachment
28
-
29
- beforeEach(() => {
30
- lcd = new LCDAttachment(16, 2)
31
- })
32
-
33
- // ── Helper: write a command via the GPIO bus ────────────────────
34
-
35
- /** Simulate a command write: RS=0, RW=0 */
36
- function writeCommand(lcd: LCDAttachment, cmd: number): void {
37
- lcd.sendCommand(cmd)
38
- lcd.updatePixels()
39
- }
40
-
41
- /** Simulate a data byte write: RS=1, RW=0 */
42
- function writeData(lcd: LCDAttachment, data: number): void {
43
- lcd.writeByte(data)
44
- lcd.updatePixels()
45
- }
46
-
47
- /** Write a string to the LCD */
48
- function writeString(lcd: LCDAttachment, str: string): void {
49
- for (let i = 0; i < str.length; i++) {
50
- writeData(lcd, str.charCodeAt(i))
51
- }
52
- }
53
-
54
- /** Simulate full GPIO bus cycle: set Port B data, set Port A control,
55
- * raise E, then lower E (falling-edge latch) */
56
- function gpioBusWrite(lcd: LCDAttachment, rs: boolean, data: number): void {
57
- const portAValue = (rs ? PIN_RS : 0) // RW = 0 (write)
58
- const ddr = 0xFF // all outputs
59
-
60
- // Set data on Port B
61
- lcd.writePortB(data, ddr)
62
-
63
- // Raise E
64
- lcd.writePortA(portAValue | PIN_E, ddr)
65
-
66
- // Lower E (falling edge triggers latch)
67
- lcd.writePortA(portAValue, ddr)
68
- }
69
-
70
- // ── Constructor & Reset ────────────────────────────────────────
71
-
72
- describe('constructor and reset', () => {
73
- it('should initialize with correct dimensions', () => {
74
- expect(lcd.cols).toBe(16)
75
- expect(lcd.rows).toBe(2)
76
- })
77
-
78
- it('should calculate pixel dimensions correctly', () => {
79
- // 16 chars × (5+1) - 1 = 95 pixels wide
80
- // 2 rows × (8+1) - 1 = 17 pixels high
81
- expect(lcd.pixelsWidth).toBe(95)
82
- expect(lcd.pixelsHeight).toBe(17)
83
- })
84
-
85
- it('should allocate pixel buffer', () => {
86
- expect(lcd.buffer).toBeDefined()
87
- expect(lcd.buffer.length).toBe(95 * 17)
88
- })
89
-
90
- it('should initialize DDRAM with spaces', () => {
91
- const ddRam = lcd.getDDRam()
92
- for (let i = 0; i < ddRam.length; i++) {
93
- expect(ddRam[i]).toBe(0x20)
94
- }
95
- })
96
-
97
- it('should start with DDRAM address at 0', () => {
98
- expect(lcd.getDDPtr()).toBe(0)
99
- })
100
-
101
- it('should start with display off', () => {
102
- expect(lcd.getDisplayFlags() & LCD_CMD_DISPLAY_ON).toBe(0)
103
- })
104
-
105
- it('should start with increment mode', () => {
106
- expect(lcd.getEntryModeFlags() & LCD_CMD_ENTRY_MODE_INCREMENT).toBeTruthy()
107
- })
108
-
109
- it('should support different display sizes', () => {
110
- const lcd20x4 = new LCDAttachment(20, 4)
111
- expect(lcd20x4.cols).toBe(20)
112
- expect(lcd20x4.rows).toBe(4)
113
- // 20 × (5+1) - 1 = 119
114
- // 4 × (8+1) - 1 = 35
115
- expect(lcd20x4.pixelsWidth).toBe(119)
116
- expect(lcd20x4.pixelsHeight).toBe(35)
117
- })
118
-
119
- it('should reset all state', () => {
120
- // Modify state
121
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON)
122
- writeData(lcd, 0x41) // 'A'
123
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x40)
124
-
125
- // Reset
126
- lcd.reset()
127
-
128
- expect(lcd.getDDPtr()).toBe(0)
129
- expect(lcd.getDisplayFlags()).toBe(0)
130
- expect(lcd.getScrollOffset()).toBe(0)
131
- expect(lcd.getDDRam()[0]).toBe(0x20) // space
132
- })
133
- })
134
-
135
- // ── Command Processing ─────────────────────────────────────────
136
-
137
- describe('clear display command', () => {
138
- it('should clear DDRAM to spaces', () => {
139
- writeData(lcd, 0x41) // 'A'
140
- writeData(lcd, 0x42) // 'B'
141
-
142
- writeCommand(lcd, LCD_CMD_CLEAR)
143
-
144
- const ddRam = lcd.getDDRam()
145
- expect(ddRam[0]).toBe(0x20)
146
- expect(ddRam[1]).toBe(0x20)
147
- })
148
-
149
- it('should reset DDRAM pointer to 0', () => {
150
- writeData(lcd, 0x41)
151
- writeCommand(lcd, LCD_CMD_CLEAR)
152
- expect(lcd.getDDPtr()).toBe(0)
153
- })
154
-
155
- it('should reset scroll offset', () => {
156
- writeCommand(lcd, LCD_CMD_SHIFT | LCD_CMD_SHIFT_DISPLAY)
157
- writeCommand(lcd, LCD_CMD_CLEAR)
158
- expect(lcd.getScrollOffset()).toBe(0)
159
- })
160
-
161
- it('should reset entry mode to increment', () => {
162
- writeCommand(lcd, LCD_CMD_ENTRY_MODE) // decrement mode
163
- writeCommand(lcd, LCD_CMD_CLEAR)
164
- expect(lcd.getEntryModeFlags() & LCD_CMD_ENTRY_MODE_INCREMENT).toBeTruthy()
165
- })
166
- })
167
-
168
- describe('home command', () => {
169
- it('should reset DDRAM pointer to 0', () => {
170
- writeData(lcd, 0x41)
171
- writeData(lcd, 0x42)
172
- writeCommand(lcd, LCD_CMD_HOME)
173
- expect(lcd.getDDPtr()).toBe(0)
174
- })
175
-
176
- it('should reset scroll offset', () => {
177
- writeCommand(lcd, LCD_CMD_SHIFT | LCD_CMD_SHIFT_DISPLAY)
178
- writeCommand(lcd, LCD_CMD_HOME)
179
- expect(lcd.getScrollOffset()).toBe(0)
180
- })
181
-
182
- it('should not clear DDRAM', () => {
183
- writeData(lcd, 0x41)
184
- writeCommand(lcd, LCD_CMD_HOME)
185
- expect(lcd.getDDRam()[0]).toBe(0x41)
186
- })
187
- })
188
-
189
- describe('entry mode command', () => {
190
- it('should set increment mode', () => {
191
- writeCommand(lcd, LCD_CMD_ENTRY_MODE | LCD_CMD_ENTRY_MODE_INCREMENT)
192
- expect(lcd.getEntryModeFlags() & LCD_CMD_ENTRY_MODE_INCREMENT).toBeTruthy()
193
- })
194
-
195
- it('should set decrement mode', () => {
196
- writeCommand(lcd, LCD_CMD_ENTRY_MODE) // no INCREMENT bit
197
- expect(lcd.getEntryModeFlags() & LCD_CMD_ENTRY_MODE_INCREMENT).toBeFalsy()
198
- })
199
-
200
- it('should enable display shift on write', () => {
201
- writeCommand(lcd, LCD_CMD_ENTRY_MODE | LCD_CMD_ENTRY_MODE_INCREMENT | LCD_CMD_ENTRY_MODE_SHIFT)
202
- expect(lcd.getEntryModeFlags() & LCD_CMD_ENTRY_MODE_SHIFT).toBeTruthy()
203
- })
204
- })
205
-
206
- describe('display control command', () => {
207
- it('should turn display on', () => {
208
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON)
209
- expect(lcd.getDisplayFlags() & LCD_CMD_DISPLAY_ON).toBeTruthy()
210
- })
211
-
212
- it('should turn display off', () => {
213
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON)
214
- writeCommand(lcd, LCD_CMD_DISPLAY) // display off
215
- expect(lcd.getDisplayFlags() & LCD_CMD_DISPLAY_ON).toBeFalsy()
216
- })
217
-
218
- it('should enable cursor', () => {
219
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON | LCD_CMD_DISPLAY_CURSOR)
220
- expect(lcd.getDisplayFlags() & LCD_CMD_DISPLAY_CURSOR).toBeTruthy()
221
- })
222
-
223
- it('should enable cursor blink', () => {
224
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON | LCD_CMD_DISPLAY_CURSOR_BLINK)
225
- expect(lcd.getDisplayFlags() & LCD_CMD_DISPLAY_CURSOR_BLINK).toBeTruthy()
226
- })
227
- })
228
-
229
- describe('set DDRAM address command', () => {
230
- it('should set DDRAM address', () => {
231
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x05)
232
- expect(lcd.getDDPtr()).toBe(0x05)
233
- })
234
-
235
- it('should set second row address', () => {
236
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x40)
237
- expect(lcd.getDDPtr()).toBe(0x40)
238
- })
239
-
240
- it('should clear CGRAM pointer', () => {
241
- writeCommand(lcd, LCD_CMD_SET_CGRAM_ADDR | 0x00)
242
- expect(lcd.getCGPtr()).not.toBeNull()
243
-
244
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x00)
245
- expect(lcd.getCGPtr()).toBeNull()
246
- })
247
- })
248
-
249
- describe('set CGRAM address command', () => {
250
- it('should set CGRAM address', () => {
251
- writeCommand(lcd, LCD_CMD_SET_CGRAM_ADDR | 0x10)
252
- expect(lcd.getCGPtr()).toBe(0x10)
253
- })
254
-
255
- it('should mask to 6 bits', () => {
256
- writeCommand(lcd, LCD_CMD_SET_CGRAM_ADDR | 0x3F)
257
- expect(lcd.getCGPtr()).toBe(0x3F)
258
- })
259
- })
260
-
261
- describe('shift command', () => {
262
- it('should shift cursor right', () => {
263
- writeCommand(lcd, LCD_CMD_SHIFT | LCD_CMD_SHIFT_RIGHT)
264
- expect(lcd.getDDPtr()).toBe(1)
265
- })
266
-
267
- it('should shift cursor left', () => {
268
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x05)
269
- writeCommand(lcd, LCD_CMD_SHIFT) // left
270
- expect(lcd.getDDPtr()).toBe(4)
271
- })
272
-
273
- it('should shift display right', () => {
274
- writeCommand(lcd, LCD_CMD_SHIFT | LCD_CMD_SHIFT_DISPLAY | LCD_CMD_SHIFT_RIGHT)
275
- expect(lcd.getScrollOffset()).toBe(-1)
276
- })
277
-
278
- it('should shift display left', () => {
279
- writeCommand(lcd, LCD_CMD_SHIFT | LCD_CMD_SHIFT_DISPLAY)
280
- expect(lcd.getScrollOffset()).toBe(1)
281
- })
282
- })
283
-
284
- // ── Data Write ─────────────────────────────────────────────────
285
-
286
- describe('data write to DDRAM', () => {
287
- it('should write a byte to DDRAM', () => {
288
- writeData(lcd, 0x41) // 'A'
289
- expect(lcd.getDDRam()[0]).toBe(0x41)
290
- })
291
-
292
- it('should auto-increment address in increment mode', () => {
293
- writeData(lcd, 0x41)
294
- expect(lcd.getDDPtr()).toBe(1)
295
- writeData(lcd, 0x42)
296
- expect(lcd.getDDPtr()).toBe(2)
297
- })
298
-
299
- it('should auto-decrement address in decrement mode', () => {
300
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x05)
301
- writeCommand(lcd, LCD_CMD_ENTRY_MODE) // decrement
302
- writeData(lcd, 0x41)
303
- expect(lcd.getDDPtr()).toBe(4)
304
- })
305
-
306
- it('should write a string to the first row', () => {
307
- writeString(lcd, 'Hello')
308
- expect(lcd.getRowText(0).substring(0, 5)).toBe('Hello')
309
- })
310
-
311
- it('should write to second row', () => {
312
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x40)
313
- writeString(lcd, 'World')
314
- expect(lcd.getRowText(1).substring(0, 5)).toBe('World')
315
- })
316
-
317
- it('should wrap from end of row 1 to row 2', () => {
318
- // Write 40 characters to fill first row of DDRAM
319
- for (let i = 0; i < 40; i++) {
320
- writeData(lcd, 0x41 + (i % 26))
321
- }
322
- // Pointer should now be at 0x40 (second row)
323
- expect(lcd.getDDPtr()).toBe(0x40)
324
- })
325
-
326
- it('should wrap from end of row 2 back to start', () => {
327
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x40)
328
- // Write 40 characters to fill second row
329
- for (let i = 0; i < 40; i++) {
330
- writeData(lcd, 0x41 + (i % 26))
331
- }
332
- // Should wrap to beginning
333
- expect(lcd.getDDPtr()).toBe(0x00)
334
- })
335
-
336
- it('should shift display when entry mode shift is enabled', () => {
337
- writeCommand(lcd, LCD_CMD_ENTRY_MODE | LCD_CMD_ENTRY_MODE_INCREMENT | LCD_CMD_ENTRY_MODE_SHIFT)
338
- writeData(lcd, 0x41)
339
- expect(lcd.getScrollOffset()).toBe(1)
340
- })
341
- })
342
-
343
- // ── Data Read ──────────────────────────────────────────────────
344
-
345
- describe('data read from DDRAM', () => {
346
- it('should read back written data via readAddress', () => {
347
- writeData(lcd, 0x41)
348
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x00)
349
- expect(lcd.readAddress()).toBe(0x00)
350
- })
351
-
352
- it('should read DDRAM address via readAddress', () => {
353
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x15)
354
- expect(lcd.readAddress()).toBe(0x15)
355
- })
356
-
357
- it('should read CGRAM address when in CGRAM mode', () => {
358
- writeCommand(lcd, LCD_CMD_SET_CGRAM_ADDR | 0x18)
359
- expect(lcd.readAddress()).toBe(0x18)
360
- })
361
- })
362
-
363
- // ── CGRAM ──────────────────────────────────────────────────────
364
-
365
- describe('CGRAM operations', () => {
366
- it('should write custom character data to CGRAM', () => {
367
- // Set CGRAM address for character 0, row 0
368
- writeCommand(lcd, LCD_CMD_SET_CGRAM_ADDR | 0x00)
369
-
370
- // Write 8 rows of pattern (smile face)
371
- const pattern = [0x00, 0x0A, 0x00, 0x00, 0x11, 0x0E, 0x00, 0x00]
372
- for (const row of pattern) {
373
- writeData(lcd, row)
374
- }
375
-
376
- // CGRAM pointer should have advanced
377
- expect(lcd.getCGPtr()).toBe(8)
378
- })
379
-
380
- it('should render CGRAM character on display', () => {
381
- // Define character 0 with a simple pattern (all pixels on in first row)
382
- writeCommand(lcd, LCD_CMD_SET_CGRAM_ADDR | 0x00)
383
- writeData(lcd, 0x1F) // row 0: all 5 bits on
384
- for (let i = 1; i < 8; i++) {
385
- writeData(lcd, 0x00) // rows 1-7: all off
386
- }
387
-
388
- // Now write character 0 to DDRAM and enable display
389
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x00)
390
- writeData(lcd, 0x00) // character 0 from CGRAM
391
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON)
392
- lcd.updatePixels()
393
-
394
- // Character 0 should be at position (0,0) - check first row pixels are on
395
- // The pixel at (0,0) should be on since we set bit 4 (MSB of 5-bit) in row 0
396
- const topLeftPixel = lcd.pixelState(0, 0)
397
- expect(topLeftPixel).toBe(1)
398
- })
399
-
400
- it('should wrap CGRAM pointer in increment mode', () => {
401
- writeCommand(lcd, LCD_CMD_SET_CGRAM_ADDR | 0x00)
402
- // Write 128 bytes (16 chars × 8 rows = full CGRAM)
403
- for (let i = 0; i < 128; i++) {
404
- writeData(lcd, 0x00)
405
- }
406
- // Should wrap back to 0
407
- expect(lcd.getCGPtr()).toBe(0)
408
- })
409
- })
410
-
411
- // ── Pixel Output ───────────────────────────────────────────────
412
-
413
- describe('pixel output', () => {
414
- it('should render all pixels off when display is off', () => {
415
- writeData(lcd, 0x41) // 'A'
416
- // Display is off by default
417
- lcd.updatePixels()
418
-
419
- for (let y = 0; y < lcd.pixelsHeight; y++) {
420
- for (let x = 0; x < lcd.pixelsWidth; x++) {
421
- const state = lcd.pixelState(x, y)
422
- // All character pixels should be 0 (off), gaps are -1
423
- if (state !== -1) {
424
- expect(state).toBe(0)
425
- }
426
- }
427
- }
428
- })
429
-
430
- it('should render character pixels when display is on', () => {
431
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON)
432
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x00)
433
- writeData(lcd, 0xFF) // char 255 — all pixels on in A00 font
434
- lcd.updatePixels()
435
-
436
- // Check that some pixels are on in the first character cell
437
- let hasOnPixel = false
438
- for (let y = 0; y < 8; y++) {
439
- for (let x = 0; x < 5; x++) {
440
- if (lcd.pixelState(x, y) === 1) {
441
- hasOnPixel = true
442
- }
443
- }
444
- }
445
- expect(hasOnPixel).toBe(true)
446
- })
447
-
448
- it('should have gap pixels between characters', () => {
449
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON)
450
- lcd.updatePixels()
451
-
452
- // Column 5 (between char 0 and char 1) should be gap (-1)
453
- const gapPixel = lcd.pixelState(5, 0)
454
- expect(gapPixel).toBe(-1)
455
- })
456
-
457
- it('should have gap pixels between rows', () => {
458
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON)
459
- lcd.updatePixels()
460
-
461
- // Row 8 (between row 0 and row 1) should be gap (-1)
462
- const gapPixel = lcd.pixelState(0, 8)
463
- expect(gapPixel).toBe(-1)
464
- })
465
-
466
- it('should return -1 for out-of-bounds coordinates', () => {
467
- expect(lcd.pixelState(-1, 0)).toBe(-1)
468
- expect(lcd.pixelState(0, -1)).toBe(-1)
469
- expect(lcd.pixelState(lcd.pixelsWidth, lcd.pixelsHeight)).toBe(-1)
470
- })
471
-
472
- it('should render spaces as all-off pixels', () => {
473
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON)
474
- // DDRAM is initialized to spaces
475
- lcd.updatePixels()
476
-
477
- // All pixels in space characters should be off
478
- for (let y = 0; y < 8; y++) {
479
- for (let x = 0; x < 5; x++) {
480
- expect(lcd.pixelState(x, y)).toBe(0)
481
- }
482
- }
483
- })
484
- })
485
-
486
- // ── Cursor Display ─────────────────────────────────────────────
487
-
488
- describe('cursor display', () => {
489
- it('should show underline cursor on bottom row of character', () => {
490
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON | LCD_CMD_DISPLAY_CURSOR)
491
- lcd.updatePixels()
492
-
493
- // Cursor at position 0: bottom row (y=7) should have pixels on
494
- for (let x = 0; x < 5; x++) {
495
- expect(lcd.pixelState(x, 7)).toBe(1)
496
- }
497
- })
498
-
499
- it('should not show cursor when cursor flag is off', () => {
500
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON)
501
- lcd.updatePixels()
502
-
503
- // Bottom row of first char should be off (space + no cursor)
504
- for (let x = 0; x < 5; x++) {
505
- expect(lcd.pixelState(x, 7)).toBe(0)
506
- }
507
- })
508
-
509
- it('should move cursor with DDRAM address', () => {
510
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON | LCD_CMD_DISPLAY_CURSOR)
511
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x01)
512
- lcd.updatePixels()
513
-
514
- // Row 0, column 0 character should NOT have cursor
515
- for (let x = 0; x < 5; x++) {
516
- expect(lcd.pixelState(x, 7)).toBe(0)
517
- }
518
-
519
- // Row 0, column 1 character should have cursor (x offset = 6)
520
- for (let x = 0; x < 5; x++) {
521
- expect(lcd.pixelState(6 + x, 7)).toBe(1)
522
- }
523
- })
524
- })
525
-
526
- // ── GPIO Bus Interface ─────────────────────────────────────────
527
-
528
- describe('GPIO bus interface', () => {
529
- it('should latch command on E falling edge', () => {
530
- const ddr = 0xFF
531
-
532
- // Write clear command via GPIO
533
- lcd.writePortB(LCD_CMD_CLEAR, ddr)
534
-
535
- // Set RS=0, RW=0, raise E
536
- lcd.writePortA(PIN_E, ddr)
537
-
538
- // Lower E
539
- lcd.writePortA(0x00, ddr)
540
-
541
- // DDRAM should be cleared (all spaces)
542
- expect(lcd.getDDPtr()).toBe(0)
543
- })
544
-
545
- it('should write data when RS is high', () => {
546
- // First turn on display via GPIO
547
- gpioBusWrite(lcd, false, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON)
548
-
549
- // Write 'A' with RS=1
550
- gpioBusWrite(lcd, true, 0x41)
551
-
552
- expect(lcd.getDDRam()[0]).toBe(0x41)
553
- })
554
-
555
- it('should not latch on E rising edge', () => {
556
- const ddr = 0xFF
557
-
558
- // Write data to Port B
559
- lcd.writePortB(0x41, ddr)
560
-
561
- // Raise E (should NOT latch)
562
- lcd.writePortA(PIN_RS | PIN_E, ddr)
563
-
564
- // Data should not have been written yet
565
- expect(lcd.getDDRam()[0]).toBe(0x20) // still space
566
- })
567
-
568
- it('should read DDRAM address via Port B when RW=1, RS=0', () => {
569
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x15)
570
-
571
- // Set RW=1, RS=0 on Port A
572
- const ddr = 0xFF
573
- lcd.writePortA(PIN_RW, ddr)
574
-
575
- const result = lcd.readPortB(ddr, 0)
576
- expect(result & 0x7F).toBe(0x15)
577
- })
578
-
579
- it('should read DDRAM data via Port B when RW=1, RS=1', () => {
580
- // Write 'A' at address 0
581
- writeData(lcd, 0x41)
582
-
583
- // Set address back to 0
584
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x00)
585
-
586
- // Set RW=1, RS=1 on Port A for data read
587
- const ddr = 0xFF
588
- lcd.writePortA(PIN_RW | PIN_RS, ddr)
589
-
590
- const result = lcd.readPortB(ddr, 0)
591
- expect(result).toBe(0x41)
592
- })
593
-
594
- it('should return 0xFF from Port B when RW is low', () => {
595
- const ddr = 0xFF
596
- lcd.writePortA(0x00, ddr) // RW=0
597
- expect(lcd.readPortB(ddr, 0)).toBe(0xFF)
598
- })
599
-
600
- it('should always return 0xFF from Port A', () => {
601
- expect(lcd.readPortA(0xFF, 0x00)).toBe(0xFF)
602
- })
603
-
604
- it('should perform a full write sequence via GPIO bus', () => {
605
- // Step 1: Function set (8-bit, 2-line)
606
- gpioBusWrite(lcd, false, LCD_CMD_FUNCTION | LCD_CMD_FUNCTION_LCD_2LINE | 0x10)
607
-
608
- // Step 2: Display ON, cursor ON
609
- gpioBusWrite(lcd, false, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON | LCD_CMD_DISPLAY_CURSOR)
610
-
611
- // Step 3: Clear display
612
- gpioBusWrite(lcd, false, LCD_CMD_CLEAR)
613
-
614
- // Step 4: Entry mode set — increment, no shift
615
- gpioBusWrite(lcd, false, LCD_CMD_ENTRY_MODE | LCD_CMD_ENTRY_MODE_INCREMENT)
616
-
617
- // Step 5: Write "Hi"
618
- gpioBusWrite(lcd, true, 0x48) // 'H'
619
- gpioBusWrite(lcd, true, 0x69) // 'i'
620
-
621
- expect(lcd.getRowText(0).substring(0, 2)).toBe('Hi')
622
- expect(lcd.getDDPtr()).toBe(2)
623
- expect(lcd.getDisplayFlags() & LCD_CMD_DISPLAY_ON).toBeTruthy()
624
- expect(lcd.getDisplayFlags() & LCD_CMD_DISPLAY_CURSOR).toBeTruthy()
625
- })
626
- })
627
-
628
- // ── DDRAM Pointer Wrapping ─────────────────────────────────────
629
-
630
- describe('DDRAM pointer wrapping', () => {
631
- it('should wrap from 0x27 to 0x40 in 2-row mode', () => {
632
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x27)
633
- writeData(lcd, 0x41) // triggers increment
634
- expect(lcd.getDDPtr()).toBe(0x40)
635
- })
636
-
637
- it('should wrap from 0x67 to 0x00 in 2-row mode', () => {
638
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x67)
639
- writeData(lcd, 0x41)
640
- expect(lcd.getDDPtr()).toBe(0x00)
641
- })
642
-
643
- it('should decrement from 0x00 to 0x67', () => {
644
- writeCommand(lcd, LCD_CMD_ENTRY_MODE) // decrement mode
645
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x00)
646
- writeData(lcd, 0x41) // triggers decrement
647
- expect(lcd.getDDPtr()).toBe(0x67)
648
- })
649
-
650
- it('should decrement from 0x40 to 0x27', () => {
651
- writeCommand(lcd, LCD_CMD_ENTRY_MODE) // decrement mode
652
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x40)
653
- writeData(lcd, 0x41) // triggers decrement
654
- expect(lcd.getDDPtr()).toBe(0x27)
655
- })
656
-
657
- describe('1-row mode', () => {
658
- let lcd1: LCDAttachment
659
-
660
- beforeEach(() => {
661
- lcd1 = new LCDAttachment(16, 1)
662
- })
663
-
664
- it('should wrap from position 79 to 0', () => {
665
- writeCommand(lcd1, LCD_CMD_SET_DRAM_ADDR | 79)
666
- writeData(lcd1, 0x41)
667
- expect(lcd1.getDDPtr()).toBe(0)
668
- })
669
-
670
- it('should wrap from position 0 to 79 when decrementing', () => {
671
- writeCommand(lcd1, LCD_CMD_ENTRY_MODE) // decrement mode
672
- writeCommand(lcd1, LCD_CMD_SET_DRAM_ADDR | 0x00)
673
- writeData(lcd1, 0x41)
674
- expect(lcd1.getDDPtr()).toBe(79)
675
- })
676
- })
677
- })
678
-
679
- // ── Display Shift ──────────────────────────────────────────────
680
-
681
- describe('display shift', () => {
682
- it('should shift displayed content left', () => {
683
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON)
684
-
685
- // Write "AB" starting at position 0
686
- writeData(lcd, 0x41) // A
687
- writeData(lcd, 0x42) // B
688
-
689
- // Shift display left
690
- writeCommand(lcd, LCD_CMD_SHIFT | LCD_CMD_SHIFT_DISPLAY)
691
-
692
- // After shifting, the second character should now appear as first visible
693
- expect(lcd.getScrollOffset()).toBe(1)
694
- })
695
-
696
- it('should shift displayed content right', () => {
697
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON)
698
-
699
- writeData(lcd, 0x41)
700
-
701
- // Shift display right
702
- writeCommand(lcd, LCD_CMD_SHIFT | LCD_CMD_SHIFT_DISPLAY | LCD_CMD_SHIFT_RIGHT)
703
-
704
- expect(lcd.getScrollOffset()).toBe(-1)
705
- })
706
- })
707
-
708
- // ── Tick (Cursor Blink) ────────────────────────────────────────
709
-
710
- describe('tick and blink', () => {
711
- it('should toggle blink state after enough ticks', () => {
712
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON | LCD_CMD_DISPLAY_CURSOR_BLINK)
713
-
714
- const cpuFrequency = 1000000 // 1 MHz
715
-
716
- // Each tick = 128 cycles at 1 MHz = 0.128 ms
717
- // Need 350ms / 0.128ms ≈ 2734 ticks
718
- for (let i = 0; i < 3000; i++) {
719
- lcd.tick(cpuFrequency)
720
- }
721
-
722
- // Blink state should have toggled
723
- lcd.updatePixels()
724
- // We can verify by checking that the test doesn't crash and pixels update
725
- })
726
-
727
- it('should not crash with high frequency', () => {
728
- lcd.tick(10000000) // 10 MHz
729
- lcd.tick(10000000)
730
- })
731
- })
732
-
733
- // ── Row Text Helper ────────────────────────────────────────────
734
-
735
- describe('getRowText', () => {
736
- it('should return text for row 0', () => {
737
- writeString(lcd, 'Hello, World!')
738
- expect(lcd.getRowText(0).substring(0, 13)).toBe('Hello, World!')
739
- })
740
-
741
- it('should return text for row 1', () => {
742
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x40)
743
- writeString(lcd, 'Line Two')
744
- expect(lcd.getRowText(1).substring(0, 8)).toBe('Line Two')
745
- })
746
-
747
- it('should return spaces for empty row', () => {
748
- const text = lcd.getRowText(0)
749
- expect(text.length).toBe(16)
750
- expect(text.trim()).toBe('')
751
- })
752
- })
753
-
754
- // ── Edge Cases ─────────────────────────────────────────────────
755
-
756
- describe('edge cases', () => {
757
- it('should handle multiple E transitions without crash', () => {
758
- const ddr = 0xFF
759
- // Rapid E toggling
760
- for (let i = 0; i < 100; i++) {
761
- lcd.writePortB(0x20, ddr)
762
- lcd.writePortA(PIN_E, ddr)
763
- lcd.writePortA(0x00, ddr)
764
- }
765
- })
766
-
767
- it('should handle function set command', () => {
768
- writeCommand(lcd, LCD_CMD_FUNCTION | LCD_CMD_FUNCTION_LCD_2LINE | 0x10)
769
- // Should not crash
770
- })
771
-
772
- it('should handle writing all character codes', () => {
773
- writeCommand(lcd, LCD_CMD_DISPLAY | LCD_CMD_DISPLAY_ON)
774
- for (let i = 0; i < 256; i++) {
775
- writeCommand(lcd, LCD_CMD_SET_DRAM_ADDR | 0x00)
776
- writeData(lcd, i)
777
- lcd.updatePixels()
778
- }
779
- })
780
-
781
- it('should handle isEnabled and getPriority', () => {
782
- expect(lcd.isEnabled()).toBe(true)
783
- expect(lcd.getPriority()).toBe(0)
784
- })
785
-
786
- it('should handle interrupt methods', () => {
787
- expect(lcd.hasCA1Interrupt()).toBe(false)
788
- expect(lcd.hasCA2Interrupt()).toBe(false)
789
- expect(lcd.hasCB1Interrupt()).toBe(false)
790
- expect(lcd.hasCB2Interrupt()).toBe(false)
791
- lcd.clearInterrupts(true, true, true, true)
792
- lcd.updateControlLines(false, false, false, false)
793
- })
794
- })
795
- })