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.
@@ -1,791 +0,0 @@
1
- import { AttachmentBase } from './Attachment'
2
-
3
- /**
4
- * HD44780 LCD Controller Emulation — GPIO Attachment
5
- *
6
- * Emulates a 16×2 (or configurable) character LCD with HD44780 controller
7
- * connected via 8-bit parallel interface on a 6522 VIA.
8
- *
9
- * Pin mapping (accent on VIA ports):
10
- * Port B (D0–D7): 8-bit data bus
11
- * Port A bit 5: RS (Register Select — 0 = command, 1 = data)
12
- * Port A bit 6: RW (Read/Write — 0 = write, 1 = read)
13
- * Port A bit 7: E (Enable — active-high strobe, latched on falling edge)
14
- *
15
- * Display modes:
16
- * Standard character display with 5×8 pixel characters
17
- * Supports CGRAM for up to 8 user-defined characters
18
- *
19
- * Output: pixel buffer (cols*(5+1)-1) × (rows*(8+1)-1) with values:
20
- * -1 = no pixel (inter-character gap)
21
- * 0 = pixel off
22
- * 1 = pixel on
23
- *
24
- * Reference: vrEmuLcd by Troy Schrapel
25
- * https://github.com/visrealm/vrEmuLcd
26
- */
27
-
28
- // ── HD44780 Command bit masks ──────────────────────────────────────
29
-
30
- export const LCD_CMD_CLEAR = 0x01
31
- export const LCD_CMD_HOME = 0x02
32
- export const LCD_CMD_ENTRY_MODE = 0x04
33
- export const LCD_CMD_ENTRY_MODE_INCREMENT = 0x02
34
- export const LCD_CMD_ENTRY_MODE_SHIFT = 0x01
35
- export const LCD_CMD_DISPLAY = 0x08
36
- export const LCD_CMD_DISPLAY_ON = 0x04
37
- export const LCD_CMD_DISPLAY_CURSOR = 0x02
38
- export const LCD_CMD_DISPLAY_CURSOR_BLINK = 0x01
39
- export const LCD_CMD_SHIFT = 0x10
40
- export const LCD_CMD_SHIFT_DISPLAY = 0x08
41
- export const LCD_CMD_SHIFT_RIGHT = 0x04
42
- export const LCD_CMD_FUNCTION = 0x20
43
- export const LCD_CMD_FUNCTION_LCD_2LINE = 0x08
44
- export const LCD_CMD_SET_CGRAM_ADDR = 0x40
45
- export const LCD_CMD_SET_DRAM_ADDR = 0x80
46
-
47
- // ── Constants ──────────────────────────────────────────────────────
48
-
49
- const CHAR_WIDTH_PX = 5
50
- const CHAR_HEIGHT_PX = 8
51
-
52
- const DDRAM_SIZE = 128
53
- const CGRAM_STORAGE_CHARS = 16
54
- const ROM_FONT_CHARS = 256 - CGRAM_STORAGE_CHARS
55
- const DEFAULT_CGRAM_BYTE = 0xAA
56
-
57
- const DATA_WIDTH_CHARS_1ROW = 80
58
- const DATA_WIDTH_CHARS_2ROW = 40
59
-
60
- const CURSOR_MASK = LCD_CMD_DISPLAY_CURSOR_BLINK | LCD_CMD_DISPLAY_CURSOR
61
-
62
- // DDRAM row start addresses for multi-row displays
63
- const ROW_OFFSETS = [0x00, 0x40, 0x14, 0x54]
64
-
65
- // ── VIA Port A pin positions ───────────────────────────────────────
66
-
67
- const PIN_RS = 0x20 // bit 5
68
- const PIN_RW = 0x40 // bit 6
69
- const PIN_E = 0x80 // bit 7
70
-
71
- // ── HD44780 ROM A00 (Japanese) font ────────────────────────────────
72
- // 240 characters (indices 16–255), each stored as 5 bytes — one per
73
- // column, MSB at top. First 16 slots reserved for CGRAM.
74
-
75
- const FONT_A00: ReadonlyArray<readonly [number, number, number, number, number]> = [
76
- [0x00,0x00,0x00,0x00,0x00], // 16
77
- [0x00,0x00,0x00,0x00,0x00], // 17
78
- [0x00,0x00,0x00,0x00,0x00], // 18
79
- [0x00,0x00,0x00,0x00,0x00], // 19
80
- [0x00,0x00,0x00,0x00,0x00], // 20
81
- [0x00,0x00,0x00,0x00,0x00], // 21
82
- [0x00,0x00,0x00,0x00,0x00], // 22
83
- [0x00,0x00,0x00,0x00,0x00], // 23
84
- [0x00,0x00,0x00,0x00,0x00], // 24
85
- [0x00,0x00,0x00,0x00,0x00], // 25
86
- [0x00,0x00,0x00,0x00,0x00], // 26
87
- [0x00,0x00,0x00,0x00,0x00], // 27
88
- [0x00,0x00,0x00,0x00,0x00], // 28
89
- [0x00,0x00,0x00,0x00,0x00], // 29
90
- [0x00,0x00,0x00,0x00,0x00], // 30
91
- [0x00,0x00,0x00,0x00,0x00], // 31
92
- [0x00,0x00,0x00,0x00,0x00], // 32 - (space)
93
- [0x00,0x00,0xf2,0x00,0x00], // 33 - !
94
- [0x00,0xe0,0x00,0xe0,0x00], // 34 - "
95
- [0x28,0xfe,0x28,0xfe,0x28], // 35 - #
96
- [0x24,0x54,0xfe,0x54,0x48], // 36 - $
97
- [0xc4,0xc8,0x10,0x26,0x46], // 37 - %
98
- [0x6c,0x92,0xaa,0x44,0x0a], // 38 - &
99
- [0x00,0xa0,0xc0,0x00,0x00], // 39 - '
100
- [0x00,0x38,0x44,0x82,0x00], // 40 - (
101
- [0x00,0x82,0x44,0x38,0x00], // 41 - )
102
- [0x28,0x10,0x7c,0x10,0x28], // 42 - *
103
- [0x10,0x10,0x7c,0x10,0x10], // 43 - +
104
- [0x00,0x0a,0x0c,0x00,0x00], // 44 - ,
105
- [0x10,0x10,0x10,0x10,0x10], // 45 - -
106
- [0x00,0x06,0x06,0x00,0x00], // 46 - .
107
- [0x04,0x08,0x10,0x20,0x40], // 47 - /
108
- [0x7c,0x8a,0x92,0xa2,0x7c], // 48 - 0
109
- [0x00,0x42,0xfe,0x02,0x00], // 49 - 1
110
- [0x42,0x86,0x8a,0x92,0x62], // 50 - 2
111
- [0x84,0x82,0xa2,0xd2,0x8c], // 51 - 3
112
- [0x18,0x28,0x48,0xfe,0x08], // 52 - 4
113
- [0xe4,0xa2,0xa2,0xa2,0x9c], // 53 - 5
114
- [0x3c,0x52,0x92,0x92,0x0c], // 54 - 6
115
- [0x80,0x8e,0x90,0xa0,0xc0], // 55 - 7
116
- [0x6c,0x92,0x92,0x92,0x6c], // 56 - 8
117
- [0x60,0x92,0x92,0x94,0x78], // 57 - 9
118
- [0x00,0x6c,0x6c,0x00,0x00], // 58 - :
119
- [0x00,0x6a,0x6c,0x00,0x00], // 59 - ;
120
- [0x10,0x28,0x44,0x82,0x00], // 60 - <
121
- [0x28,0x28,0x28,0x28,0x28], // 61 - =
122
- [0x00,0x82,0x44,0x28,0x10], // 62 - >
123
- [0x40,0x80,0x8a,0x90,0x60], // 63 - ?
124
- [0x4c,0x92,0x9e,0x82,0x7c], // 64 - @
125
- [0x7e,0x90,0x90,0x90,0x7e], // 65 - A
126
- [0xfe,0x92,0x92,0x92,0x6c], // 66 - B
127
- [0x7c,0x82,0x82,0x82,0x44], // 67 - C
128
- [0xfe,0x82,0x82,0x44,0x38], // 68 - D
129
- [0xfe,0x92,0x92,0x92,0x82], // 69 - E
130
- [0xfe,0x90,0x90,0x90,0x80], // 70 - F
131
- [0x7c,0x82,0x92,0x92,0x5e], // 71 - G
132
- [0xfe,0x10,0x10,0x10,0xfe], // 72 - H
133
- [0x00,0x82,0xfe,0x82,0x00], // 73 - I
134
- [0x04,0x82,0x82,0xfc,0x00], // 74 - J
135
- [0xfe,0x10,0x28,0x44,0x82], // 75 - K
136
- [0xfe,0x02,0x02,0x02,0x02], // 76 - L
137
- [0xfe,0x40,0x30,0x40,0xfe], // 77 - M
138
- [0xfe,0x20,0x10,0x08,0xfe], // 78 - N
139
- [0x7c,0x82,0x82,0x82,0x7c], // 79 - O
140
- [0xfe,0x90,0x90,0x90,0x60], // 80 - P
141
- [0x7c,0x82,0x8a,0x84,0x7a], // 81 - Q
142
- [0xfe,0x90,0x98,0x94,0x62], // 82 - R
143
- [0x62,0x92,0x92,0x92,0x8c], // 83 - S
144
- [0x80,0x80,0xfe,0x80,0x80], // 84 - T
145
- [0xfc,0x02,0x02,0x02,0xfc], // 85 - U
146
- [0xf8,0x04,0x02,0x04,0xf8], // 86 - V
147
- [0xfc,0x02,0x1c,0x02,0xfc], // 87 - W
148
- [0xc6,0x28,0x10,0x28,0xc6], // 88 - X
149
- [0xe0,0x10,0x0e,0x10,0xe0], // 89 - Y
150
- [0x86,0x8a,0x92,0xa2,0xc2], // 90 - Z
151
- [0x00,0xfe,0x82,0x82,0x00], // 91 - [
152
- [0xa8,0x68,0x3e,0x68,0xa8], // 92 - yen
153
- [0x00,0x82,0x82,0xfe,0x00], // 93 - ]
154
- [0x20,0x40,0x80,0x40,0x20], // 94 - ^
155
- [0x02,0x02,0x02,0x02,0x02], // 95 - _
156
- [0x00,0x80,0x40,0x20,0x00], // 96 - `
157
- [0x04,0x2a,0x2a,0x2a,0x1e], // 97 - a
158
- [0xfe,0x12,0x22,0x22,0x1c], // 98 - b
159
- [0x1c,0x22,0x22,0x22,0x04], // 99 - c
160
- [0x1c,0x22,0x22,0x12,0xfe], // 100 - d
161
- [0x1c,0x2a,0x2a,0x2a,0x18], // 101 - e
162
- [0x10,0x7e,0x90,0x80,0x40], // 102 - f
163
- [0x30,0x4a,0x4a,0x4a,0x7c], // 103 - g
164
- [0xfe,0x10,0x20,0x20,0x1e], // 104 - h
165
- [0x00,0x22,0xbe,0x02,0x00], // 105 - i
166
- [0x04,0x02,0x22,0xbc,0x00], // 106 - j
167
- [0xfe,0x08,0x14,0x22,0x00], // 107 - k
168
- [0x02,0x82,0xfe,0x02,0x02], // 108 - l
169
- [0x3e,0x20,0x18,0x20,0x1e], // 109 - m
170
- [0x3e,0x10,0x20,0x20,0x1e], // 110 - n
171
- [0x1c,0x22,0x22,0x22,0x1c], // 111 - o
172
- [0x3e,0x28,0x28,0x28,0x10], // 112 - p
173
- [0x10,0x28,0x28,0x18,0x3e], // 113 - q
174
- [0x3e,0x10,0x20,0x20,0x10], // 114 - r
175
- [0x12,0x2a,0x2a,0x2a,0x04], // 115 - s
176
- [0x20,0xfc,0x22,0x02,0x04], // 116 - t
177
- [0x3c,0x02,0x02,0x04,0x3e], // 117 - u
178
- [0x38,0x04,0x02,0x04,0x38], // 118 - v
179
- [0x3c,0x02,0x0c,0x02,0x3c], // 119 - w
180
- [0x22,0x14,0x08,0x14,0x22], // 120 - x
181
- [0x30,0x0a,0x0a,0x0a,0x3c], // 121 - y
182
- [0x22,0x26,0x2a,0x32,0x22], // 122 - z
183
- [0x00,0x10,0x6c,0x82,0x00], // 123 - {
184
- [0x00,0x00,0xfe,0x00,0x00], // 124 - |
185
- [0x00,0x82,0x6c,0x10,0x00], // 125 - }
186
- [0x10,0x10,0x54,0x38,0x10], // 126 - ->
187
- [0x10,0x38,0x54,0x10,0x10], // 127 - <-
188
- [0x00,0x00,0x00,0x00,0x00], // 128
189
- [0x00,0x00,0x00,0x00,0x00], // 129
190
- [0x00,0x00,0x00,0x00,0x00], // 130
191
- [0x00,0x00,0x00,0x00,0x00], // 131
192
- [0x00,0x00,0x00,0x00,0x00], // 132
193
- [0x00,0x00,0x00,0x00,0x00], // 133
194
- [0x00,0x00,0x00,0x00,0x00], // 134
195
- [0x00,0x00,0x00,0x00,0x00], // 135
196
- [0x00,0x00,0x00,0x00,0x00], // 136
197
- [0x00,0x00,0x00,0x00,0x00], // 137
198
- [0x00,0x00,0x00,0x00,0x00], // 138
199
- [0x00,0x00,0x00,0x00,0x00], // 139
200
- [0x00,0x00,0x00,0x00,0x00], // 140
201
- [0x00,0x00,0x00,0x00,0x00], // 141
202
- [0x00,0x00,0x00,0x00,0x00], // 142
203
- [0x00,0x00,0x00,0x00,0x00], // 143
204
- [0x00,0x00,0x00,0x00,0x00], // 144
205
- [0x00,0x00,0x00,0x00,0x00], // 145
206
- [0x00,0x00,0x00,0x00,0x00], // 146
207
- [0x00,0x00,0x00,0x00,0x00], // 147
208
- [0x00,0x00,0x00,0x00,0x00], // 148
209
- [0x00,0x00,0x00,0x00,0x00], // 149
210
- [0x00,0x00,0x00,0x00,0x00], // 150
211
- [0x00,0x00,0x00,0x00,0x00], // 151
212
- [0x00,0x00,0x00,0x00,0x00], // 152
213
- [0x00,0x00,0x00,0x00,0x00], // 153
214
- [0x00,0x00,0x00,0x00,0x00], // 154
215
- [0x00,0x00,0x00,0x00,0x00], // 155
216
- [0x00,0x00,0x00,0x00,0x00], // 156
217
- [0x00,0x00,0x00,0x00,0x00], // 157
218
- [0x00,0x00,0x00,0x00,0x00], // 158
219
- [0x00,0x00,0x00,0x00,0x00], // 159
220
- [0x00,0x00,0x00,0x00,0x00], // 160
221
- [0x0e,0x0a,0x0e,0x00,0x00], // 161
222
- [0x00,0x00,0xf0,0x80,0x80], // 162
223
- [0x02,0x02,0x1e,0x00,0x00], // 163
224
- [0x08,0x04,0x02,0x00,0x00], // 164
225
- [0x00,0x18,0x18,0x00,0x00], // 165
226
- [0x50,0x50,0x52,0x54,0x78], // 166
227
- [0x20,0x22,0x2c,0x28,0x30], // 167
228
- [0x04,0x08,0x1e,0x20,0x00], // 168
229
- [0x18,0x12,0x32,0x12,0x1c], // 169
230
- [0x12,0x12,0x1e,0x12,0x12], // 170
231
- [0x12,0x14,0x18,0x3e,0x10], // 171
232
- [0x10,0x3e,0x10,0x14,0x18], // 172
233
- [0x02,0x12,0x12,0x1e,0x02], // 173
234
- [0x2a,0x2a,0x2a,0x3e,0x00], // 174
235
- [0x18,0x00,0x1a,0x02,0x1c], // 175
236
- [0x10,0x10,0x10,0x10,0x10], // 176
237
- [0x80,0x82,0xbc,0x90,0xe0], // 177
238
- [0x08,0x10,0x3e,0x40,0x80], // 178
239
- [0x70,0x40,0xc2,0x44,0x78], // 179
240
- [0x42,0x42,0x7e,0x42,0x42], // 180
241
- [0x44,0x48,0x50,0xfe,0x40], // 181
242
- [0x42,0xfc,0x40,0x42,0x7c], // 182
243
- [0x50,0x50,0xfe,0x50,0x50], // 183
244
- [0x10,0x62,0x42,0x44,0x78], // 184
245
- [0x20,0xc0,0x42,0x7c,0x40], // 185
246
- [0x42,0x42,0x42,0x42,0x7e], // 186
247
- [0x40,0xf2,0x44,0xf8,0x40], // 187
248
- [0x52,0x52,0x02,0x04,0x38], // 188
249
- [0x42,0x44,0x48,0x54,0x62], // 189
250
- [0x40,0xfc,0x42,0x52,0x62], // 190
251
- [0x60,0x12,0x02,0x04,0x78], // 191
252
- [0x10,0x62,0x52,0x4c,0x78], // 192
253
- [0x50,0x52,0x7c,0x90,0x10], // 193
254
- [0x70,0x00,0x72,0x04,0x78], // 194
255
- [0x20,0xa2,0xbc,0xa0,0x20], // 195
256
- [0x00,0xfe,0x10,0x08,0x00], // 196
257
- [0x22,0x24,0xf8,0x20,0x20], // 197
258
- [0x02,0x42,0x42,0x42,0x02], // 198
259
- [0x42,0x54,0x48,0x54,0x60], // 199
260
- [0x44,0x48,0xde,0x68,0x44], // 200
261
- [0x00,0x02,0x04,0xf8,0x00], // 201
262
- [0x1e,0x00,0x40,0x20,0x1e], // 202
263
- [0xfc,0x22,0x22,0x22,0x22], // 203
264
- [0x40,0x42,0x42,0x44,0x78], // 204
265
- [0x20,0x40,0x20,0x10,0x0c], // 205
266
- [0x4c,0x40,0xfe,0x40,0x4c], // 206
267
- [0x40,0x48,0x44,0x4a,0x70], // 207
268
- [0x00,0x54,0x54,0x54,0x02], // 208
269
- [0x1c,0x24,0x44,0x04,0x0e], // 209
270
- [0x02,0x14,0x08,0x14,0x60], // 210
271
- [0x50,0x7c,0x52,0x52,0x52], // 211
272
- [0x20,0xfe,0x20,0x28,0x30], // 212
273
- [0x02,0x42,0x42,0x7e,0x02], // 213
274
- [0x52,0x52,0x52,0x52,0x7e], // 214
275
- [0x20,0xa0,0xa2,0xa4,0x38], // 215
276
- [0xf0,0x02,0x04,0xf8,0x00], // 216
277
- [0x3e,0x00,0x7e,0x02,0x0c], // 217
278
- [0x7e,0x02,0x04,0x08,0x10], // 218
279
- [0x7e,0x42,0x42,0x42,0x7e], // 219
280
- [0x70,0x40,0x42,0x44,0x78], // 220
281
- [0x42,0x42,0x02,0x04,0x18], // 221
282
- [0x40,0x20,0x80,0x40,0x00], // 222
283
- [0xe0,0xa0,0xe0,0x00,0x00], // 223
284
- [0x1c,0x22,0x12,0x0c,0x32], // 224
285
- [0x04,0xaa,0x2a,0xaa,0x1e], // 225
286
- [0x1f,0x2a,0x2a,0x2a,0x14], // 226
287
- [0x14,0x2a,0x2a,0x22,0x04], // 227
288
- [0x3f,0x02,0x02,0x04,0x3e], // 228
289
- [0x1c,0x22,0x32,0x2a,0x24], // 229
290
- [0x0f,0x12,0x22,0x22,0x1c], // 230
291
- [0x1c,0x22,0x22,0x22,0x3f], // 231
292
- [0x04,0x02,0x3c,0x20,0x20], // 232
293
- [0x20,0x20,0x00,0x70,0x00], // 233
294
- [0x00,0x00,0x20,0xbf,0x00], // 234
295
- [0x50,0x20,0x50,0x00,0x00], // 235
296
- [0x18,0x24,0x7e,0x24,0x08], // 236
297
- [0x28,0xfe,0x2a,0x02,0x02], // 237
298
- [0x3e,0x90,0xa0,0xa0,0x1e], // 238
299
- [0x1c,0xa2,0x22,0xa2,0x1c], // 239
300
- [0x3f,0x12,0x22,0x22,0x1c], // 240
301
- [0x1c,0x22,0x22,0x12,0x3f], // 241
302
- [0x3c,0x52,0x52,0x52,0x3c], // 242
303
- [0x0c,0x14,0x08,0x14,0x18], // 243
304
- [0x1a,0x26,0x20,0x26,0x1a], // 244
305
- [0x3c,0x82,0x02,0x84,0x3e], // 245
306
- [0xc6,0xaa,0x92,0x82,0x82], // 246
307
- [0x22,0x3c,0x20,0x3e,0x22], // 247
308
- [0xa2,0x94,0x88,0x94,0xa2], // 248
309
- [0x3c,0x02,0x02,0x02,0x3f], // 249
310
- [0x28,0x28,0x3e,0x28,0x48], // 250
311
- [0x22,0x3c,0x28,0x28,0x2e], // 251
312
- [0x3e,0x28,0x38,0x28,0x3e], // 252
313
- [0x08,0x08,0x2a,0x08,0x08], // 253
314
- [0x00,0x00,0x00,0x00,0x00], // 254
315
- [0xff,0xff,0xff,0xff,0xff], // 255
316
- ]
317
-
318
- export class LCDAttachment extends AttachmentBase {
319
-
320
- // ── Display geometry ───────────────────────────────────────────
321
- readonly cols: number
322
- readonly rows: number
323
-
324
- // ── HD44780 internal state ─────────────────────────────────────
325
-
326
- private entryModeFlags: number = LCD_CMD_ENTRY_MODE_INCREMENT
327
- private displayFlags: number = 0x00
328
- private scrollOffset: number = 0
329
-
330
- /** 128-byte Display Data RAM */
331
- private ddRam: Uint8Array
332
- /** Current DDRAM pointer offset */
333
- private ddPtr: number = 0
334
- private dataWidthCols: number
335
-
336
- /** Character Generator RAM — 16 characters × 8 rows, stored column-major
337
- * as 16 × CHAR_WIDTH_PX bytes (matching vrEmuLcd cgRam layout) */
338
- private cgRam: Uint8Array
339
- /** Current CGRAM pointer (null when not in CGRAM mode) */
340
- private cgPtr: number | null = null
341
-
342
- /** Pixel buffer: each byte is -1 (gap), 0 (off) or 1 (on) */
343
- buffer: Int8Array
344
- readonly pixelsWidth: number
345
- readonly pixelsHeight: number
346
-
347
- // ── Cursor blink timing ────────────────────────────────────────
348
- private blinkAccumulator: number = 0
349
- private blinkState: boolean = false
350
- private static readonly BLINK_PERIOD_MS = 350
351
-
352
- // ── VIA bus latch state ────────────────────────────────────────
353
- private lastPortA: number = 0
354
- private lastE: boolean = false
355
-
356
- constructor(cols: number = 16, rows: number = 2, priority: number = 0) {
357
- super(priority, false, false, false, false)
358
-
359
- this.cols = cols
360
- this.rows = rows
361
-
362
- this.ddRam = new Uint8Array(DDRAM_SIZE)
363
- this.cgRam = new Uint8Array(CGRAM_STORAGE_CHARS * CHAR_WIDTH_PX)
364
-
365
- this.dataWidthCols = rows <= 1 ? DATA_WIDTH_CHARS_1ROW : DATA_WIDTH_CHARS_2ROW
366
-
367
- this.pixelsWidth = cols * (CHAR_WIDTH_PX + 1) - 1
368
- this.pixelsHeight = rows * (CHAR_HEIGHT_PX + 1) - 1
369
- this.buffer = new Int8Array(this.pixelsWidth * this.pixelsHeight)
370
-
371
- this.reset()
372
- }
373
-
374
- // ── Attachment interface ───────────────────────────────────
375
-
376
- reset(): void {
377
- super.reset()
378
-
379
- this.entryModeFlags = LCD_CMD_ENTRY_MODE_INCREMENT
380
- this.displayFlags = 0x00
381
- this.scrollOffset = 0
382
-
383
- this.ddRam.fill(0x20) // space
384
- this.ddPtr = 0
385
- this.cgRam.fill(DEFAULT_CGRAM_BYTE)
386
- this.cgPtr = null
387
-
388
- this.blinkAccumulator = 0
389
- this.blinkState = false
390
- this.lastPortA = 0
391
- this.lastE = false
392
-
393
- this.buffer.fill(-1)
394
- this.updatePixels()
395
- }
396
-
397
- tick(cpuFrequency: number): void {
398
- // Advance cursor blink timer
399
- const msPerTick = (128 / cpuFrequency) * 1000
400
- this.blinkAccumulator += msPerTick
401
- if (this.blinkAccumulator >= LCDAttachment.BLINK_PERIOD_MS) {
402
- this.blinkAccumulator -= LCDAttachment.BLINK_PERIOD_MS
403
- this.blinkState = !this.blinkState
404
- }
405
- }
406
-
407
- /**
408
- * Port A carries the control signals.
409
- * We detect E falling edge to latch the bus.
410
- */
411
- writePortA(value: number, ddr: number): void {
412
- const maskedValue = value & ddr
413
- const currentE = !!(maskedValue & PIN_E)
414
- const prevE = this.lastE
415
-
416
- // Latch on falling edge of E — use the PREVIOUS lastPortA so that
417
- // RS/RW reflect the state while E was still HIGH (HD44780 setup-time
418
- // requirement). If the CPU drops RS and E in the same VIA write,
419
- // this preserves the RS value that was active during the E=1 phase.
420
- if (prevE && !currentE) {
421
- this.latchBus()
422
- }
423
-
424
- this.lastPortA = maskedValue
425
- this.lastE = currentE
426
- }
427
-
428
- readPortA(ddr: number, or: number): number {
429
- return 0xFF
430
- }
431
-
432
- readPortB(ddr: number, or: number): number {
433
- // If R/W is high (read mode), provide data on Port B
434
- const portA = this.lastPortA
435
- if (portA & PIN_RW) {
436
- if (portA & PIN_RS) {
437
- // RS=1, RW=1 → Read data
438
- return this.readByte()
439
- } else {
440
- // RS=0, RW=1 → Read address / busy flag
441
- return this.readAddress()
442
- }
443
- }
444
- return 0xFF
445
- }
446
-
447
- // ── Bus latch (E falling edge) ────────────────────────────────
448
-
449
- private latchBus(): void {
450
- const portA = this.lastPortA
451
- const rw = !!(portA & PIN_RW)
452
- const rs = !!(portA & PIN_RS)
453
-
454
- if (rw) {
455
- // Read operations are handled via readPortB
456
- return
457
- }
458
-
459
- // Write operation — capture data from Port B output register.
460
- // Since we can't directly read the OR, the 6502 software must have
461
- // written data to Port B *before* toggling E. We store it in writePortB
462
- // — but the VIA card calls writePortB with the actual value. By the time
463
- // E falls the data on the bus is the Port B output register value.
464
- // We need the data value — it's available from the last writePortB call.
465
- // However, writePortB doesn't store anything here. The VIA resolves
466
- // the actual output from OR & DDR and passes it to us.
467
- //
468
- // For writes, the data on Port B is the value written by the CPU.
469
- // The VIA will have called writePortB with the data already.
470
- // We need to capture it — store it via writePortB override.
471
-
472
- // Actually — we need to capture the Port B value. Let's store it.
473
- const data = this.lastPortBValue
474
-
475
- if (rs) {
476
- this.writeByte(data)
477
- } else {
478
- this.sendCommand(data)
479
- }
480
-
481
- this.updatePixels()
482
- }
483
-
484
- private lastPortBValue: number = 0
485
-
486
- override writePortB(value: number, ddr: number): void {
487
- this.lastPortBValue = value & ddr
488
- }
489
-
490
- // ── HD44780 Command Processing ────────────────────────────────
491
-
492
- sendCommand(command: number): void {
493
- if (command & LCD_CMD_SET_DRAM_ADDR) {
494
- // Set DDRAM address — remaining 7 bits
495
- this.ddPtr = command & 0x7F
496
- this.cgPtr = null
497
- } else if (command & LCD_CMD_SET_CGRAM_ADDR) {
498
- // Set CGRAM address — remaining 6 bits
499
- this.cgPtr = command & 0x3F
500
- } else if (command & LCD_CMD_FUNCTION) {
501
- // Function set — we just acknowledge (8-bit mode, 2-line assumed)
502
- } else if (command & LCD_CMD_SHIFT) {
503
- if (command & LCD_CMD_SHIFT_DISPLAY) {
504
- // Shift entire display
505
- if (command & LCD_CMD_SHIFT_RIGHT) {
506
- --this.scrollOffset
507
- } else {
508
- ++this.scrollOffset
509
- }
510
- } else {
511
- // Shift cursor
512
- if (command & LCD_CMD_SHIFT_RIGHT) {
513
- this.incrementDdPtr()
514
- } else {
515
- this.decrementDdPtr()
516
- }
517
- }
518
- } else if (command & LCD_CMD_DISPLAY) {
519
- this.displayFlags = command
520
- } else if (command & LCD_CMD_ENTRY_MODE) {
521
- this.entryModeFlags = command
522
- } else if (command & LCD_CMD_HOME) {
523
- this.ddPtr = 0
524
- this.scrollOffset = 0
525
- } else if (command === LCD_CMD_CLEAR) {
526
- this.ddRam.fill(0x20)
527
- this.ddPtr = 0
528
- this.scrollOffset = 0
529
- this.entryModeFlags = LCD_CMD_ENTRY_MODE_INCREMENT
530
- }
531
- }
532
-
533
- // ── Data Write / Read ─────────────────────────────────────────
534
-
535
- writeByte(data: number): void {
536
- if (this.cgPtr !== null) {
537
- // Write to CGRAM
538
- const row = this.cgPtr % CHAR_HEIGHT_PX
539
- const charBase = this.cgPtr - row
540
-
541
- for (let i = 0; i < CHAR_WIDTH_PX; i++) {
542
- const bit = data & ((0x01 << (CHAR_WIDTH_PX - 1)) >> i)
543
- const addr = charBase * CHAR_WIDTH_PX / CHAR_HEIGHT_PX * CHAR_HEIGHT_PX
544
- // CGRAM is stored column-major like vrEmuLcd: cgRam[char][col]
545
- // Each column byte has rows packed as bits (MSB = row 0)
546
- const idx = Math.floor(this.cgPtr / CHAR_HEIGHT_PX) * CHAR_WIDTH_PX + i
547
- if (idx < this.cgRam.length) {
548
- if (bit) {
549
- this.cgRam[idx] |= (0x80 >> row)
550
- } else {
551
- this.cgRam[idx] &= ~(0x80 >> row)
552
- }
553
- }
554
- }
555
- } else {
556
- // Write to DDRAM
557
- if (this.ddPtr < DDRAM_SIZE) {
558
- this.ddRam[this.ddPtr] = data
559
- }
560
- }
561
- this.doShift()
562
- }
563
-
564
- private readByte(): number {
565
- if (this.cgPtr !== null) {
566
- const row = this.cgPtr % CHAR_HEIGHT_PX
567
- const charIdx = Math.floor(this.cgPtr / CHAR_HEIGHT_PX)
568
- let data = 0
569
- for (let i = 0; i < CHAR_WIDTH_PX; i++) {
570
- const idx = charIdx * CHAR_WIDTH_PX + i
571
- if (idx < this.cgRam.length && (this.cgRam[idx] & (0x80 >> row))) {
572
- data |= ((0x01 << (CHAR_WIDTH_PX - 1)) >> i)
573
- }
574
- }
575
- return data
576
- }
577
- return this.ddPtr < DDRAM_SIZE ? this.ddRam[this.ddPtr] : 0x20
578
- }
579
-
580
- readAddress(): number {
581
- if (this.cgPtr !== null) {
582
- return this.cgPtr & 0x3F
583
- }
584
- return this.ddPtr & 0x7F
585
- }
586
-
587
- // ── DDRAM pointer management ──────────────────────────────────
588
-
589
- private incrementDdPtr(): void {
590
- this.ddPtr++
591
- if (this.rows > 1) {
592
- if (this.ddPtr === 0x28) {
593
- this.ddPtr = 0x40
594
- } else if (this.ddPtr === 0x68 || this.ddPtr >= DDRAM_SIZE) {
595
- this.ddPtr = 0x00
596
- }
597
- } else if (this.ddPtr >= 80) {
598
- this.ddPtr = 0
599
- }
600
- }
601
-
602
- private decrementDdPtr(): void {
603
- this.ddPtr--
604
- if (this.rows > 1) {
605
- if (this.ddPtr < 0) {
606
- this.ddPtr = 0x67
607
- } else if (this.ddPtr === 0x3F) {
608
- this.ddPtr = 0x27
609
- }
610
- } else {
611
- if (this.ddPtr < 0) {
612
- this.ddPtr = 79
613
- }
614
- }
615
- }
616
-
617
- private doShift(): void {
618
- if (this.cgPtr !== null) {
619
- // Shift CGRAM pointer
620
- if (this.entryModeFlags & LCD_CMD_ENTRY_MODE_INCREMENT) {
621
- this.cgPtr++
622
- if (this.cgPtr >= CGRAM_STORAGE_CHARS * CHAR_HEIGHT_PX) {
623
- this.cgPtr = 0
624
- }
625
- } else {
626
- this.cgPtr--
627
- if (this.cgPtr < 0) {
628
- this.cgPtr = CGRAM_STORAGE_CHARS * CHAR_HEIGHT_PX - 1
629
- }
630
- }
631
- return
632
- }
633
-
634
- // Shift display or cursor
635
- if (this.entryModeFlags & LCD_CMD_ENTRY_MODE_SHIFT) {
636
- if (this.entryModeFlags & LCD_CMD_ENTRY_MODE_INCREMENT) {
637
- ++this.scrollOffset
638
- } else {
639
- --this.scrollOffset
640
- }
641
- }
642
-
643
- if (this.entryModeFlags & LCD_CMD_ENTRY_MODE_INCREMENT) {
644
- this.incrementDdPtr()
645
- } else {
646
- this.decrementDdPtr()
647
- }
648
- }
649
-
650
- // ── Character Data Lookup ─────────────────────────────────────
651
-
652
- /**
653
- * Get the 5-column font data for a character.
654
- * Characters 0–15 come from CGRAM; 16–255 from ROM.
655
- */
656
- private charBits(c: number): readonly number[] | Uint8Array {
657
- if (c < CGRAM_STORAGE_CHARS) {
658
- // Return a slice of cgRam for this character
659
- const start = c * CHAR_WIDTH_PX
660
- return this.cgRam.subarray(start, start + CHAR_WIDTH_PX)
661
- }
662
- return FONT_A00[c - CGRAM_STORAGE_CHARS]
663
- }
664
-
665
- // ── Data Offset Helper ────────────────────────────────────────
666
-
667
- private getDataOffset(row: number, col: number): number {
668
- if (row >= this.rows) row = this.rows - 1
669
-
670
- // Normalize negative scroll offset
671
- let scroll = this.scrollOffset
672
- while (scroll < 0) {
673
- scroll += this.dataWidthCols
674
- }
675
-
676
- const dataCol = (col + scroll) % this.dataWidthCols
677
-
678
- if (this.rows > 1) {
679
- return ROW_OFFSETS[row] + dataCol
680
- }
681
- return dataCol
682
- }
683
-
684
- // ── Pixel Buffer Update ───────────────────────────────────────
685
-
686
- updatePixels(): void {
687
- const displayOn = !!(this.displayFlags & LCD_CMD_DISPLAY_ON)
688
-
689
- // Determine cursor state
690
- let cursorOn = this.displayFlags & CURSOR_MASK
691
- if (this.displayFlags & LCD_CMD_DISPLAY_CURSOR_BLINK) {
692
- if (this.blinkState) {
693
- cursorOn &= ~LCD_CMD_DISPLAY_CURSOR_BLINK
694
- }
695
- }
696
-
697
- for (let row = 0; row < this.rows; row++) {
698
- for (let col = 0; col < this.cols; col++) {
699
- // Top-left pixel for this character cell
700
- const charTopLeftX = col * (CHAR_WIDTH_PX + 1)
701
- const charTopLeftY = row * (CHAR_HEIGHT_PX + 1)
702
-
703
- // DDRAM offset
704
- const ddOffset = this.getDataOffset(row, col)
705
- const charCode = this.ddRam[ddOffset] ?? 0x20
706
-
707
- // Should we draw cursor here?
708
- const drawCursor = cursorOn && (ddOffset === this.ddPtr) && this.cgPtr === null
709
-
710
- // Get font data
711
- const bits = this.charBits(charCode)
712
-
713
- // Render 5×8 character
714
- for (let y = 0; y < CHAR_HEIGHT_PX; y++) {
715
- for (let x = 0; x < CHAR_WIDTH_PX; x++) {
716
- const pixelIdx = (charTopLeftY + y) * this.pixelsWidth + (charTopLeftX + x)
717
-
718
- if (!displayOn) {
719
- this.buffer[pixelIdx] = 0
720
- continue
721
- }
722
-
723
- // Font data is column-major: bits[x] has row bits, MSB = row 0
724
- let pixel = (bits[x] & (0x80 >> y)) ? 1 : 0
725
-
726
- // Cursor override
727
- if (drawCursor) {
728
- if ((cursorOn & LCD_CMD_DISPLAY_CURSOR_BLINK) ||
729
- ((cursorOn & LCD_CMD_DISPLAY_CURSOR) && y === CHAR_HEIGHT_PX - 1)) {
730
- pixel = 1
731
- }
732
- }
733
-
734
- this.buffer[pixelIdx] = pixel as 0 | 1
735
- }
736
- }
737
- }
738
- }
739
- }
740
-
741
- // ── Public Accessors (for rendering / debugging) ──────────────
742
-
743
- /** Get the raw DDRAM contents */
744
- getDDRam(): Uint8Array {
745
- return this.ddRam
746
- }
747
-
748
- /** Get the current DDRAM address pointer */
749
- getDDPtr(): number {
750
- return this.ddPtr
751
- }
752
-
753
- /** Get the display flags */
754
- getDisplayFlags(): number {
755
- return this.displayFlags
756
- }
757
-
758
- /** Get the entry mode flags */
759
- getEntryModeFlags(): number {
760
- return this.entryModeFlags
761
- }
762
-
763
- /** Get scroll offset */
764
- getScrollOffset(): number {
765
- return this.scrollOffset
766
- }
767
-
768
- /** Get CGRAM pointer (null if not in CGRAM mode) */
769
- getCGPtr(): number | null {
770
- return this.cgPtr
771
- }
772
-
773
- /** Read the text content of a specific display row */
774
- getRowText(row: number): string {
775
- let text = ''
776
- for (let col = 0; col < this.cols; col++) {
777
- const offset = this.getDataOffset(row, col)
778
- text += String.fromCharCode(this.ddRam[offset])
779
- }
780
- return text
781
- }
782
-
783
- /** Pixel state at a given coordinate: -1 (gap), 0 (off), 1 (on) */
784
- pixelState(x: number, y: number): number {
785
- const offset = y * this.pixelsWidth + x
786
- if (offset >= 0 && offset < this.buffer.length) {
787
- return this.buffer[offset]
788
- }
789
- return -1
790
- }
791
- }