ac6502 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +261 -0
  3. package/dist/components/CPU.js +1170 -0
  4. package/dist/components/CPU.js.map +1 -0
  5. package/dist/components/Cart.js +23 -0
  6. package/dist/components/Cart.js.map +1 -0
  7. package/dist/components/IO/Empty.js +19 -0
  8. package/dist/components/IO/Empty.js.map +1 -0
  9. package/dist/components/IO/GPIOAttachments/GPIOAttachment.js +71 -0
  10. package/dist/components/IO/GPIOAttachments/GPIOAttachment.js.map +1 -0
  11. package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js +90 -0
  12. package/dist/components/IO/GPIOAttachments/GPIOJoystickAttachment.js.map +1 -0
  13. package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js +489 -0
  14. package/dist/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.js.map +1 -0
  15. package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js +274 -0
  16. package/dist/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.js.map +1 -0
  17. package/dist/components/IO/GPIOCard.js +597 -0
  18. package/dist/components/IO/GPIOCard.js.map +1 -0
  19. package/dist/components/IO/InputBoard.js +19 -0
  20. package/dist/components/IO/InputBoard.js.map +1 -0
  21. package/dist/components/IO/LCDCard.js +19 -0
  22. package/dist/components/IO/LCDCard.js.map +1 -0
  23. package/dist/components/IO/RAMCard.js +63 -0
  24. package/dist/components/IO/RAMCard.js.map +1 -0
  25. package/dist/components/IO/RTCCard.js +483 -0
  26. package/dist/components/IO/RTCCard.js.map +1 -0
  27. package/dist/components/IO/SerialCard.js +282 -0
  28. package/dist/components/IO/SerialCard.js.map +1 -0
  29. package/dist/components/IO/SoundCard.js +620 -0
  30. package/dist/components/IO/SoundCard.js.map +1 -0
  31. package/dist/components/IO/StorageCard.js +428 -0
  32. package/dist/components/IO/StorageCard.js.map +1 -0
  33. package/dist/components/IO/VGACard.js +9 -0
  34. package/dist/components/IO/VGACard.js.map +1 -0
  35. package/dist/components/IO/VideoCard.js +623 -0
  36. package/dist/components/IO/VideoCard.js.map +1 -0
  37. package/dist/components/IO.js +3 -0
  38. package/dist/components/IO.js.map +1 -0
  39. package/dist/components/Machine.js +310 -0
  40. package/dist/components/Machine.js.map +1 -0
  41. package/dist/components/RAM.js +24 -0
  42. package/dist/components/RAM.js.map +1 -0
  43. package/dist/components/ROM.js +23 -0
  44. package/dist/components/ROM.js.map +1 -0
  45. package/dist/index.js +441 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/tests/CPU.test.js +1626 -0
  48. package/dist/tests/CPU.test.js.map +1 -0
  49. package/dist/tests/Cart.test.js +119 -0
  50. package/dist/tests/Cart.test.js.map +1 -0
  51. package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js +339 -0
  52. package/dist/tests/IO/GPIOAttachments/GPIOAttachment.test.js.map +1 -0
  53. package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js +126 -0
  54. package/dist/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.js.map +1 -0
  55. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js +779 -0
  56. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.js.map +1 -0
  57. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js +355 -0
  58. package/dist/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.js.map +1 -0
  59. package/dist/tests/IO/GPIOCard.test.js +503 -0
  60. package/dist/tests/IO/GPIOCard.test.js.map +1 -0
  61. package/dist/tests/IO/RAMCard.test.js +229 -0
  62. package/dist/tests/IO/RAMCard.test.js.map +1 -0
  63. package/dist/tests/IO/RTCCard.test.js +177 -0
  64. package/dist/tests/IO/RTCCard.test.js.map +1 -0
  65. package/dist/tests/IO/SerialCard.test.js +423 -0
  66. package/dist/tests/IO/SerialCard.test.js.map +1 -0
  67. package/dist/tests/IO/SoundCard.test.js +528 -0
  68. package/dist/tests/IO/SoundCard.test.js.map +1 -0
  69. package/dist/tests/IO/StorageCard.test.js +647 -0
  70. package/dist/tests/IO/StorageCard.test.js.map +1 -0
  71. package/dist/tests/IO/VideoCard.test.js +549 -0
  72. package/dist/tests/IO/VideoCard.test.js.map +1 -0
  73. package/dist/tests/Machine.test.js +383 -0
  74. package/dist/tests/Machine.test.js.map +1 -0
  75. package/dist/tests/RAM.test.js +160 -0
  76. package/dist/tests/RAM.test.js.map +1 -0
  77. package/dist/tests/ROM.test.js +123 -0
  78. package/dist/tests/ROM.test.js.map +1 -0
  79. package/jest.config.cjs +9 -0
  80. package/package.json +43 -0
  81. package/src/components/CPU.ts +1371 -0
  82. package/src/components/Cart.ts +20 -0
  83. package/src/components/IO/GPIOAttachments/GPIOAttachment.ts +189 -0
  84. package/src/components/IO/GPIOAttachments/GPIOJoystickAttachment.ts +99 -0
  85. package/src/components/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.ts +465 -0
  86. package/src/components/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.ts +287 -0
  87. package/src/components/IO/GPIOCard.ts +677 -0
  88. package/src/components/IO/RAMCard.ts +68 -0
  89. package/src/components/IO/RTCCard.ts +518 -0
  90. package/src/components/IO/SerialCard.ts +335 -0
  91. package/src/components/IO/SoundCard.ts +711 -0
  92. package/src/components/IO/StorageCard.ts +473 -0
  93. package/src/components/IO/VideoCard.ts +730 -0
  94. package/src/components/IO.ts +11 -0
  95. package/src/components/Machine.ts +364 -0
  96. package/src/components/RAM.ts +23 -0
  97. package/src/components/ROM.ts +19 -0
  98. package/src/index.ts +474 -0
  99. package/src/tests/CPU.test.ts +2045 -0
  100. package/src/tests/Cart.test.ts +149 -0
  101. package/src/tests/IO/GPIOAttachments/GPIOAttachment.test.ts +413 -0
  102. package/src/tests/IO/GPIOAttachments/GPIOJoystickAttachment.test.ts +147 -0
  103. package/src/tests/IO/GPIOAttachments/GPIOKeyboardEncoderAttachment.test.ts +961 -0
  104. package/src/tests/IO/GPIOAttachments/GPIOKeyboardMatrixAttachment.test.ts +449 -0
  105. package/src/tests/IO/GPIOCard.test.ts +644 -0
  106. package/src/tests/IO/RAMCard.test.ts +284 -0
  107. package/src/tests/IO/RTCCard.test.ts +222 -0
  108. package/src/tests/IO/SerialCard.test.ts +530 -0
  109. package/src/tests/IO/SoundCard.test.ts +659 -0
  110. package/src/tests/IO/StorageCard.test.ts +787 -0
  111. package/src/tests/IO/VideoCard.test.ts +668 -0
  112. package/src/tests/Machine.test.ts +437 -0
  113. package/src/tests/RAM.test.ts +196 -0
  114. package/src/tests/ROM.test.ts +154 -0
  115. package/tsconfig.json +12 -0
@@ -0,0 +1,68 @@
1
+ import { IO } from '../IO'
2
+
3
+ /**
4
+ * RAMCard - Emulates banked RAM with 256KB total capacity
5
+ *
6
+ * Provides 256KB of banked RAM divided into 256 banks of 1KB each.
7
+ * A bank control register at address 0x3FF selects which bank is currently visible.
8
+ *
9
+ * Address Map:
10
+ * $000-$3FE: Bank data (1K window into selected bank)
11
+ * $3FF: Bank control register (read/write)
12
+ */
13
+ export class RAMCard implements IO {
14
+
15
+ static TOTAL_SIZE: number = 256 * 1024 // 256k bytes
16
+ static BANK_SIZE: number = 1024 // 1k per bank
17
+ static NUM_BANKS: number = RAMCard.TOTAL_SIZE / RAMCard.BANK_SIZE // 256 banks
18
+ static BANK_CONTROL_REGISTER: number = 0x3FF // Last byte in 1k window
19
+
20
+ data: number[] = [...Array(RAMCard.TOTAL_SIZE)].fill(0x00)
21
+ currentBank: number = 0
22
+
23
+ raiseIRQ = () => {}
24
+ raiseNMI = () => {}
25
+
26
+ /**
27
+ * Read from RAM or bank control register
28
+ */
29
+ read(address: number): number {
30
+ // Reading from bank control register returns current bank number
31
+ if (address === RAMCard.BANK_CONTROL_REGISTER) {
32
+ return this.currentBank
33
+ }
34
+
35
+ // Calculate actual address in RAM: bank * bank_size + offset and return data
36
+ return this.data[this.currentBank * RAMCard.BANK_SIZE + address]
37
+ }
38
+
39
+ /**
40
+ * Write to RAM or bank control register
41
+ */
42
+ write(address: number, data: number): void {
43
+ // Writing to bank control register switches banks
44
+ if (address === RAMCard.BANK_CONTROL_REGISTER) {
45
+ this.currentBank = data & 0xFF // Ensure 0-255 range
46
+ return
47
+ }
48
+
49
+ // Calculate actual address in RAM: bank * bank_size + offset and store data
50
+ this.data[this.currentBank * RAMCard.BANK_SIZE + address] = data & 0xFF
51
+ }
52
+
53
+ /**
54
+ * Tick - no timing behavior for RAM
55
+ */
56
+ tick(frequency: number): void {}
57
+
58
+ /**
59
+ * Reset the RAM card
60
+ */
61
+ reset(coldStart: boolean): void {
62
+ if (coldStart) {
63
+ this.currentBank = 0
64
+ this.data.fill(0x00)
65
+ }
66
+ }
67
+
68
+ }
@@ -0,0 +1,518 @@
1
+ import { IO } from '../IO'
2
+
3
+ /**
4
+ * DS1511Y Real-Time Clock IC Emulation
5
+ *
6
+ * Register Map (0x00-0x1F):
7
+ * 0x00: Seconds (BCD, 00-59)
8
+ * 0x01: Minutes (BCD, 00-59)
9
+ * 0x02: Hours (BCD, 00-23)
10
+ * 0x03: Day of Week (1-7, 1=Sunday)
11
+ * 0x04: Date (BCD, 01-31)
12
+ * 0x05: Month (BCD, 01-12) + Control bits (EOSC, E32K)
13
+ * 0x06: Year (BCD, 00-99)
14
+ * 0x07: Century (BCD, 00-39)
15
+ * 0x08: Alarm Seconds (BCD, 00-59) + AM1 bit
16
+ * 0x09: Alarm Minutes (BCD, 00-59) + AM2 bit
17
+ * 0x0A: Alarm Hours (BCD, 00-23) + AM3 bit
18
+ * 0x0B: Alarm Day/Date + AM4, DY/DT bits
19
+ * 0x0C: Watchdog (0.1 Second and 0.01 Second)
20
+ * 0x0D: Watchdog (0.1 Second and Second)
21
+ * 0x0E: Control A (BLF1, BLF2, PBS, PAB, TDF, KSF, WDF, IRQF)
22
+ * 0x0F: Control B (TE, CS, BME, TPE, TIE, KIE, WDE, WDS)
23
+ * 0x10: RAM Address (Extended RAM Address pointer)
24
+ * 0x11: Reserved
25
+ * 0x12: Reserved
26
+ * 0x13: RAM Data (Extended RAM Data at address pointed to by 0x10)
27
+ */
28
+ export class RTCCard implements IO {
29
+ raiseIRQ = () => {}
30
+ raiseNMI = () => {}
31
+
32
+ // RTC Registers (user-visible)
33
+ private userSeconds: number = 0 // 0x00
34
+ private userMinutes: number = 0 // 0x01
35
+ private userHours: number = 0 // 0x02
36
+ private userDayOfWeek: number = 1 // 0x03 (1=Sunday)
37
+ private userDate: number = 1 // 0x04
38
+ private userMonth: number = 1 // 0x05 (bits 0-4)
39
+ private monthControl: number = 0 // 0x05 (bits 5-7: EOSC, E32K)
40
+ private userYear: number = 0 // 0x06 (00-99)
41
+ private userCentury: number = 20 // 0x07 (00-39)
42
+
43
+ // Internal timekeeping registers
44
+ private internalSeconds: number = 0
45
+ private internalMinutes: number = 0
46
+ private internalHours: number = 0
47
+ private internalDayOfWeek: number = 1
48
+ private internalDate: number = 1
49
+ private internalMonth: number = 1
50
+ private internalYear: number = 0
51
+ private internalCentury: number = 20
52
+
53
+ // Alarm registers
54
+ private alarmSeconds: number = 0 // 0x08 (bits 0-6, bit 7 = AM1)
55
+ private alarmMinutes: number = 0 // 0x09 (bits 0-6, bit 7 = AM2)
56
+ private alarmHours: number = 0 // 0x0A (bits 0-6, bit 7 = AM3)
57
+ private alarmDayDate: number = 0 // 0x0B (bits 0-5, bit 6 = DY/DT, bit 7 = AM4)
58
+
59
+ // Watchdog
60
+ private watchdog1: number = 0 // 0x0C (0.1 Second and 0.01 Second)
61
+ private watchdog2: number = 0 // 0x0D (0.1 Second and Second)
62
+ private watchdogCounterCentis: number = 0
63
+ private watchdogCycleCounter: number = 0
64
+
65
+ // Control registers
66
+ private controlA: number = 0 // 0x0E
67
+ private controlB: number = 0 // 0x0F
68
+
69
+ // Extended RAM
70
+ private ramAddress: number = 0 // 0x10
71
+ private ramData: Uint8Array = new Uint8Array(256) // 256 bytes of extended RAM
72
+
73
+ // Time tracking for incrementing
74
+ private cycleCounter: number = 0 // Accumulator for CPU cycles
75
+ private cpuFrequency: number = 2000000 // Default 2MHz
76
+ private transferCycleCounter: number = 0
77
+ private pendingUserToInternal: boolean = false
78
+ private userSyncNeeded: boolean = false
79
+ private lastTEEnabled: boolean = false
80
+
81
+ constructor() {
82
+ this.initializeWithCurrentTime()
83
+ }
84
+
85
+ /**
86
+ * Initialize RTC with current system time
87
+ */
88
+ private initializeWithCurrentTime(): void {
89
+ const now = new Date()
90
+ this.internalSeconds = this.decimalToBCD(now.getSeconds())
91
+ this.internalMinutes = this.decimalToBCD(now.getMinutes())
92
+ this.internalHours = this.decimalToBCD(now.getHours())
93
+ this.internalDayOfWeek = now.getDay() === 0 ? 1 : now.getDay() // 1=Sunday
94
+ this.internalDate = this.decimalToBCD(now.getDate())
95
+ this.internalMonth = this.decimalToBCD(now.getMonth() + 1)
96
+ this.internalYear = this.decimalToBCD(now.getFullYear() % 100)
97
+ this.internalCentury = this.decimalToBCD(Math.floor(now.getFullYear() / 100))
98
+ this.monthControl = 0x80 // EOSC enabled by default (bit 7)
99
+ this.copyInternalToUser()
100
+ this.pendingUserToInternal = false
101
+ this.userSyncNeeded = false
102
+ this.transferCycleCounter = 0
103
+ this.lastTEEnabled = (this.controlB & 0x80) !== 0
104
+ }
105
+
106
+ /**
107
+ * Convert decimal to BCD (Binary Coded Decimal)
108
+ */
109
+ private decimalToBCD(decimal: number): number {
110
+ return ((Math.floor(decimal / 10) << 4) | (decimal % 10)) & 0xFF
111
+ }
112
+
113
+ /**
114
+ * Convert BCD to decimal
115
+ */
116
+ private bcdToDecimal(bcd: number): number {
117
+ return (((bcd >> 4) & 0x0F) * 10) + (bcd & 0x0F)
118
+ }
119
+
120
+ /**
121
+ * Get the number of days in a month
122
+ */
123
+ private getDaysInMonth(month: number, year: number, century: number): number {
124
+ const fullYear = (century * 100) + year
125
+
126
+ if ([1, 3, 5, 7, 8, 10, 12].includes(month)) return 31
127
+ if ([4, 6, 9, 11].includes(month)) return 30
128
+
129
+ // February
130
+ if ((fullYear % 4 === 0 && fullYear % 100 !== 0) || fullYear % 400 === 0) {
131
+ return 29
132
+ }
133
+ return 28
134
+ }
135
+
136
+ /**
137
+ * Get next day of week
138
+ */
139
+ private getNextDayOfWeek(currentDay: number): number {
140
+ return currentDay === 7 ? 1 : currentDay + 1
141
+ }
142
+
143
+ private copyInternalToUser(): void {
144
+ this.userSeconds = this.internalSeconds
145
+ this.userMinutes = this.internalMinutes
146
+ this.userHours = this.internalHours
147
+ this.userDayOfWeek = this.internalDayOfWeek
148
+ this.userDate = this.internalDate
149
+ this.userMonth = this.internalMonth
150
+ this.userYear = this.internalYear
151
+ this.userCentury = this.internalCentury
152
+ }
153
+
154
+ private copyUserToInternal(): void {
155
+ this.internalSeconds = this.userSeconds
156
+ this.internalMinutes = this.userMinutes
157
+ this.internalHours = this.userHours
158
+ this.internalDayOfWeek = this.userDayOfWeek
159
+ this.internalDate = this.userDate
160
+ this.internalMonth = this.userMonth
161
+ this.internalYear = this.userYear
162
+ this.internalCentury = this.userCentury
163
+ }
164
+
165
+ private getTransferCyclesRequired(): number {
166
+ return Math.max(1, Math.ceil(this.cpuFrequency * 366 / 1000000))
167
+ }
168
+
169
+ private markUserTimeWrite(): void {
170
+ this.pendingUserToInternal = true
171
+ if ((this.controlB & 0x80) !== 0 &&
172
+ this.transferCycleCounter >= this.getTransferCyclesRequired()) {
173
+ this.copyUserToInternal()
174
+ this.pendingUserToInternal = false
175
+ this.userSyncNeeded = false
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Increment time by one second
181
+ */
182
+ private incrementTime(): void {
183
+ let sec = this.bcdToDecimal(this.internalSeconds)
184
+ let min = this.bcdToDecimal(this.internalMinutes)
185
+ let hour = this.bcdToDecimal(this.internalHours)
186
+ let date = this.bcdToDecimal(this.internalDate)
187
+ let month = this.bcdToDecimal(this.internalMonth)
188
+ let year = this.bcdToDecimal(this.internalYear)
189
+ const century = this.bcdToDecimal(this.internalCentury)
190
+
191
+ sec++
192
+ if (sec >= 60) {
193
+ sec = 0
194
+ min++
195
+ if (min >= 60) {
196
+ min = 0
197
+ hour++
198
+ if (hour >= 24) {
199
+ hour = 0
200
+ this.internalDayOfWeek = this.getNextDayOfWeek(this.internalDayOfWeek)
201
+ date++
202
+
203
+ const daysInMonth = this.getDaysInMonth(month, year, century)
204
+ if (date > daysInMonth) {
205
+ date = 1
206
+ month++
207
+ if (month > 12) {
208
+ month = 1
209
+ year++
210
+ if (year > 99) {
211
+ year = 0
212
+ let cent = this.bcdToDecimal(this.internalCentury)
213
+ cent++
214
+ if (cent > 39) cent = 0
215
+ this.internalCentury = this.decimalToBCD(cent)
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ this.internalSeconds = this.decimalToBCD(sec)
224
+ this.internalMinutes = this.decimalToBCD(min)
225
+ this.internalHours = this.decimalToBCD(hour)
226
+ this.internalDate = this.decimalToBCD(date)
227
+ this.internalMonth = this.decimalToBCD(month)
228
+ this.internalYear = this.decimalToBCD(year)
229
+ }
230
+
231
+ /**
232
+ * Check if alarm should trigger
233
+ */
234
+ private checkAlarm(): void {
235
+ const am1 = (this.alarmSeconds & 0x80) !== 0
236
+ const am2 = (this.alarmMinutes & 0x80) !== 0
237
+ const am3 = (this.alarmHours & 0x80) !== 0
238
+ const am4 = (this.alarmDayDate & 0x80) !== 0
239
+
240
+ // If all AM bits are set, alarm is disabled
241
+ if (am1 && am2 && am3 && am4) return
242
+
243
+ // Check matching based on AM bits
244
+ const secondsMatch = am1 || (this.internalSeconds === (this.alarmSeconds & 0x7F))
245
+ const minutesMatch = am2 || (this.internalMinutes === (this.alarmMinutes & 0x7F))
246
+ const hoursMatch = am3 || (this.internalHours === (this.alarmHours & 0x7F))
247
+
248
+ let dayDateMatch = true
249
+ if (!am4) {
250
+ const dyDt = (this.alarmDayDate & 0x40) !== 0
251
+ const alarmValue = this.alarmDayDate & 0x3F
252
+
253
+ if (dyDt) {
254
+ // Day of week match
255
+ dayDateMatch = this.internalDayOfWeek === alarmValue
256
+ } else {
257
+ // Date match
258
+ dayDateMatch = this.internalDate === alarmValue
259
+ }
260
+ }
261
+
262
+ if (secondsMatch && minutesMatch && hoursMatch && dayDateMatch) {
263
+ this.controlA |= 0x08 // Set TDF flag (bit 3)
264
+ this.raiseInterruptIfEnabled(0x08, 0x08)
265
+ }
266
+ }
267
+
268
+ private raiseInterruptIfEnabled(flagMask: number, enableMask: number): void {
269
+ if ((this.controlA & flagMask) === 0) return
270
+ if ((this.controlB & enableMask) === 0) return
271
+ this.controlA |= 0x01 // Set IRQF flag (bit 0)
272
+ this.raiseIRQ()
273
+ }
274
+
275
+ private setKickstartFlag(): void {
276
+ this.controlA |= 0x04 // Set KSF flag (bit 2)
277
+ this.raiseInterruptIfEnabled(0x04, 0x04)
278
+ }
279
+
280
+ private decodeWatchdogCentis(): number {
281
+ const hundredths = this.watchdog1 & 0x0F
282
+ const tenths = (this.watchdog1 >> 4) & 0x0F
283
+ const seconds = this.watchdog2 & 0x0F
284
+ const tensSeconds = (this.watchdog2 >> 4) & 0x0F
285
+
286
+ const totalSeconds = (tensSeconds * 10) + seconds
287
+ const totalCentis = (tenths * 10) + hundredths
288
+ return (totalSeconds * 100) + totalCentis
289
+ }
290
+
291
+ private reloadWatchdog(): void {
292
+ this.watchdogCounterCentis = this.decodeWatchdogCentis()
293
+ this.watchdogCycleCounter = 0
294
+ }
295
+
296
+ private stepWatchdog(): void {
297
+ if ((this.controlB & 0x02) === 0) return // WDE disabled
298
+ if (this.watchdogCounterCentis <= 0) return
299
+
300
+ const cyclesPerCentisecond = Math.max(1, Math.floor(this.cpuFrequency / 100))
301
+ this.watchdogCycleCounter++
302
+ if (this.watchdogCycleCounter < cyclesPerCentisecond) return
303
+
304
+ this.watchdogCycleCounter = 0
305
+ this.watchdogCounterCentis -= 1
306
+ if (this.watchdogCounterCentis > 0) return
307
+
308
+ this.controlA |= 0x02 // Set WDF flag (bit 1)
309
+
310
+ if ((this.controlB & 0x01) === 0) {
311
+ this.raiseInterruptIfEnabled(0x02, 0x02)
312
+ } else {
313
+ // WDS=1 steers watchdog to reset; emulate by clearing WDE
314
+ this.controlB &= ~0x02
315
+ this.raiseNMI()
316
+ }
317
+ }
318
+
319
+ read(address: number): number {
320
+ address &= 0x1F
321
+
322
+ switch (address) {
323
+ case 0x00: return this.userSeconds
324
+ case 0x01: return this.userMinutes
325
+ case 0x02: return this.userHours
326
+ case 0x03: return this.userDayOfWeek & 0x07
327
+ case 0x04: return this.userDate
328
+ case 0x05: return this.userMonth | this.monthControl
329
+ case 0x06: return this.userYear
330
+ case 0x07: return this.userCentury
331
+ case 0x08: return this.alarmSeconds
332
+ case 0x09: return this.alarmMinutes
333
+ case 0x0A: return this.alarmHours
334
+ case 0x0B: return this.alarmDayDate
335
+ case 0x0C: return this.watchdog1
336
+ case 0x0D: return this.watchdog2
337
+ case 0x0E:
338
+ // Reading Control A clears the interrupt flags: IRQF, WDF, KSF, TDF
339
+ const result = this.controlA
340
+ this.controlA &= 0xF0 // Clear bits 0-3 (IRQF, WDF, KSF, TDF)
341
+ return result
342
+ case 0x0F: return this.controlB
343
+ case 0x10: return this.ramAddress
344
+ case 0x11: return 0 // Reserved
345
+ case 0x12: return 0 // Reserved
346
+ case 0x13: {
347
+ const value = this.ramData[this.ramAddress]
348
+ if ((this.controlB & 0x20) !== 0) {
349
+ this.ramAddress = (this.ramAddress + 1) & 0xFF
350
+ }
351
+ return value
352
+ }
353
+ default: return 0
354
+ }
355
+ }
356
+
357
+ write(address: number, data: number): void {
358
+ data &= 0xFF
359
+ address &= 0x1F
360
+
361
+ switch (address) {
362
+ case 0x00:
363
+ this.userSeconds = data
364
+ this.markUserTimeWrite()
365
+ break
366
+ case 0x01:
367
+ this.userMinutes = data
368
+ this.markUserTimeWrite()
369
+ break
370
+ case 0x02:
371
+ this.userHours = data
372
+ this.markUserTimeWrite()
373
+ break
374
+ case 0x03:
375
+ this.userDayOfWeek = data & 0x07
376
+ this.markUserTimeWrite()
377
+ break
378
+ case 0x04:
379
+ this.userDate = data
380
+ this.markUserTimeWrite()
381
+ break
382
+ case 0x05:
383
+ this.userMonth = data & 0x1F
384
+ this.monthControl = data & 0xE0
385
+ this.markUserTimeWrite()
386
+ break
387
+ case 0x06:
388
+ this.userYear = data
389
+ this.markUserTimeWrite()
390
+ break
391
+ case 0x07:
392
+ this.userCentury = data
393
+ this.markUserTimeWrite()
394
+ break
395
+ case 0x08:
396
+ this.alarmSeconds = data
397
+ break
398
+ case 0x09:
399
+ this.alarmMinutes = data
400
+ break
401
+ case 0x0A:
402
+ this.alarmHours = data
403
+ break
404
+ case 0x0B:
405
+ this.alarmDayDate = data
406
+ break
407
+ case 0x0C:
408
+ this.watchdog1 = data
409
+ this.reloadWatchdog()
410
+ break
411
+ case 0x0D:
412
+ this.watchdog2 = data
413
+ this.reloadWatchdog()
414
+ break
415
+ case 0x0E:
416
+ // Writing 1 to flag bits (0-3) clears them; control bits (4-7) are written normally
417
+ this.controlA = (data & 0xF0) | ((this.controlA & 0x0F) & ~(data & 0x0F))
418
+ break
419
+ case 0x0F:
420
+ this.controlB = data
421
+ this.raiseInterruptIfEnabled(0x04, 0x04)
422
+ if ((this.controlB & 0x02) !== 0) {
423
+ this.reloadWatchdog()
424
+ }
425
+ break
426
+ case 0x10:
427
+ this.ramAddress = data // Set RAM address pointer
428
+ break
429
+ case 0x11:
430
+ case 0x12:
431
+ // Reserved, ignore writes
432
+ break
433
+ case 0x13:
434
+ this.ramData[this.ramAddress] = data // Write to RAM at current address
435
+ if ((this.controlB & 0x20) !== 0) {
436
+ this.ramAddress = (this.ramAddress + 1) & 0xFF
437
+ }
438
+ break
439
+ }
440
+ }
441
+
442
+ tick(frequency: number): void {
443
+ // Advance RTC based on CPU frequency
444
+ // Store the frequency for use in time calculations
445
+ this.cpuFrequency = frequency > 0 ? frequency : 2000000
446
+
447
+ const teEnabled = (this.controlB & 0x80) !== 0
448
+ if (teEnabled !== this.lastTEEnabled) {
449
+ this.lastTEEnabled = teEnabled
450
+ this.transferCycleCounter = 0
451
+ }
452
+
453
+ if (teEnabled) {
454
+ this.transferCycleCounter++
455
+ } else {
456
+ this.transferCycleCounter = 0
457
+ }
458
+
459
+ const transferReady = teEnabled &&
460
+ this.transferCycleCounter >= this.getTransferCyclesRequired()
461
+
462
+ if (transferReady && this.pendingUserToInternal) {
463
+ this.copyUserToInternal()
464
+ this.pendingUserToInternal = false
465
+ this.userSyncNeeded = false
466
+ }
467
+
468
+ if ((this.monthControl & 0x80) === 0) {
469
+ // Oscillator disabled
470
+ this.stepWatchdog()
471
+ return
472
+ }
473
+
474
+ this.cycleCounter++
475
+
476
+ // Advance time when we've accumulated enough cycles for 1 second
477
+ if (this.cycleCounter >= this.cpuFrequency) {
478
+ this.cycleCounter = 0
479
+ this.incrementTime()
480
+ this.checkAlarm()
481
+
482
+ if (transferReady) {
483
+ this.copyInternalToUser()
484
+ this.userSyncNeeded = false
485
+ } else {
486
+ this.userSyncNeeded = true
487
+ }
488
+ } else if (transferReady && this.userSyncNeeded) {
489
+ this.copyInternalToUser()
490
+ this.userSyncNeeded = false
491
+ }
492
+
493
+ this.stepWatchdog()
494
+ }
495
+
496
+ reset(coldStart: boolean): void {
497
+ if (coldStart) {
498
+ // Cold start: Initialize with current time
499
+ this.initializeWithCurrentTime()
500
+ this.cycleCounter = 0
501
+ this.watchdogCounterCentis = 0
502
+ this.watchdogCycleCounter = 0
503
+ this.transferCycleCounter = 0
504
+ this.pendingUserToInternal = false
505
+ this.userSyncNeeded = false
506
+ this.setKickstartFlag()
507
+ } else {
508
+ // Warm start: Keep time, reset some control flags but preserve settings
509
+ this.controlA &= 0xF0 // Clear interrupt flags
510
+ this.cycleCounter = 0
511
+ this.watchdogCounterCentis = 0
512
+ this.watchdogCycleCounter = 0
513
+ this.transferCycleCounter = 0
514
+ this.pendingUserToInternal = false
515
+ this.userSyncNeeded = false
516
+ }
517
+ }
518
+ }