@gba-kit/arm-emulator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/arm-cpu.d.ts +141 -0
- package/dist/arm-cpu.d.ts.map +1 -0
- package/dist/arm-cpu.js +1875 -0
- package/dist/arm-cpu.js.map +1 -0
- package/dist/cpu-snapshot.d.ts +18 -0
- package/dist/cpu-snapshot.d.ts.map +1 -0
- package/dist/cpu-snapshot.js +2 -0
- package/dist/cpu-snapshot.js.map +1 -0
- package/dist/disassembler.d.ts +17 -0
- package/dist/disassembler.d.ts.map +1 -0
- package/dist/disassembler.js +617 -0
- package/dist/disassembler.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/memory.d.ts +42 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +218 -0
- package/dist/memory.js.map +1 -0
- package/dist/types.d.ts +91 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +51 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +126 -0
- package/dist/utils.js.map +1 -0
- package/package.json +56 -0
package/dist/arm-cpu.js
ADDED
|
@@ -0,0 +1,1875 @@
|
|
|
1
|
+
import { LR, PC, SENTINEL_ADDR, SP } from './types.js';
|
|
2
|
+
import { addWithFlags, asr, bit, bits, isNegative, lsl, lsr, ror, signExtend, subWithFlags } from './utils.js';
|
|
3
|
+
// ─── CPSR Bit Positions ──────────────────────────────────────────────
|
|
4
|
+
const CPSR_N = 31;
|
|
5
|
+
const CPSR_Z = 30;
|
|
6
|
+
const CPSR_C = 29;
|
|
7
|
+
const CPSR_V = 28;
|
|
8
|
+
const CPSR_I = 7;
|
|
9
|
+
const CPSR_F = 6;
|
|
10
|
+
const CPSR_T = 5;
|
|
11
|
+
const CPSR_MODE_MASK = 0x1f;
|
|
12
|
+
// ─── CPU Mode Constants ──────────────────────────────────────────────
|
|
13
|
+
/** CPU mode codes (bits 4-0 of CPSR) */
|
|
14
|
+
export const MODE_USR = 0x10;
|
|
15
|
+
export const MODE_FIQ = 0x11;
|
|
16
|
+
export const MODE_IRQ = 0x12;
|
|
17
|
+
export const MODE_SVC = 0x13;
|
|
18
|
+
export const MODE_ABT = 0x17;
|
|
19
|
+
export const MODE_UND = 0x1b;
|
|
20
|
+
export const MODE_SYS = 0x1f;
|
|
21
|
+
/** Index into banked SP/LR arrays by mode */
|
|
22
|
+
const SP_LR_BANK_INDEX = {
|
|
23
|
+
[MODE_USR]: 0,
|
|
24
|
+
[MODE_SYS]: 0,
|
|
25
|
+
[MODE_FIQ]: 1,
|
|
26
|
+
[MODE_IRQ]: 2,
|
|
27
|
+
[MODE_SVC]: 3,
|
|
28
|
+
[MODE_ABT]: 4,
|
|
29
|
+
[MODE_UND]: 5,
|
|
30
|
+
};
|
|
31
|
+
/** Index into SPSR array by mode (only privileged modes have SPSR) */
|
|
32
|
+
const SPSR_BANK_INDEX = {
|
|
33
|
+
[MODE_FIQ]: 0,
|
|
34
|
+
[MODE_IRQ]: 1,
|
|
35
|
+
[MODE_SVC]: 2,
|
|
36
|
+
[MODE_ABT]: 3,
|
|
37
|
+
[MODE_UND]: 4,
|
|
38
|
+
};
|
|
39
|
+
/** Stub address range for external function calls */
|
|
40
|
+
const STUB_BASE = 0x08f00000;
|
|
41
|
+
/**
|
|
42
|
+
* Check if a CPU mode is valid.
|
|
43
|
+
*/
|
|
44
|
+
function isValidMode(mode) {
|
|
45
|
+
return (mode === MODE_USR ||
|
|
46
|
+
mode === MODE_FIQ ||
|
|
47
|
+
mode === MODE_IRQ ||
|
|
48
|
+
mode === MODE_SVC ||
|
|
49
|
+
mode === MODE_ABT ||
|
|
50
|
+
mode === MODE_UND ||
|
|
51
|
+
mode === MODE_SYS);
|
|
52
|
+
}
|
|
53
|
+
// ─── ARM Condition Codes ─────────────────────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* Evaluate an ARM condition code (bits 31-28 of an ARM instruction).
|
|
56
|
+
*/
|
|
57
|
+
function checkCondition(cond, n, z, c, v) {
|
|
58
|
+
switch (cond) {
|
|
59
|
+
case 0x0:
|
|
60
|
+
return z; // EQ
|
|
61
|
+
case 0x1:
|
|
62
|
+
return !z; // NE
|
|
63
|
+
case 0x2:
|
|
64
|
+
return c; // CS/HS
|
|
65
|
+
case 0x3:
|
|
66
|
+
return !c; // CC/LO
|
|
67
|
+
case 0x4:
|
|
68
|
+
return n; // MI
|
|
69
|
+
case 0x5:
|
|
70
|
+
return !n; // PL
|
|
71
|
+
case 0x6:
|
|
72
|
+
return v; // VS
|
|
73
|
+
case 0x7:
|
|
74
|
+
return !v; // VC
|
|
75
|
+
case 0x8:
|
|
76
|
+
return c && !z; // HI
|
|
77
|
+
case 0x9:
|
|
78
|
+
return !c || z; // LS
|
|
79
|
+
case 0xa:
|
|
80
|
+
return n === v; // GE
|
|
81
|
+
case 0xb:
|
|
82
|
+
return n !== v; // LT
|
|
83
|
+
case 0xc:
|
|
84
|
+
return !z && n === v; // GT
|
|
85
|
+
case 0xd:
|
|
86
|
+
return z || n !== v; // LE
|
|
87
|
+
case 0xe:
|
|
88
|
+
return true; // AL
|
|
89
|
+
case 0xf:
|
|
90
|
+
return true; // Unconditional (ARMv5+, treat as AL)
|
|
91
|
+
default:
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ─── ARM7TDMI Full CPU ──────────────────────────────────────────────
|
|
96
|
+
/**
|
|
97
|
+
* Full ARM7TDMI CPU supporting ARM and Thumb instruction sets,
|
|
98
|
+
* with CPU modes and banked registers.
|
|
99
|
+
*/
|
|
100
|
+
export class ArmCpu {
|
|
101
|
+
/** General-purpose registers r0-r15 (current view, mode-dependent) */
|
|
102
|
+
registers = new Uint32Array(16);
|
|
103
|
+
/**
|
|
104
|
+
* Current Program Status Register (full 32-bit).
|
|
105
|
+
*
|
|
106
|
+
* Bit layout:
|
|
107
|
+
* - 31: N (Negative)
|
|
108
|
+
* - 30: Z (Zero)
|
|
109
|
+
* - 29: C (Carry)
|
|
110
|
+
* - 28: V (Overflow)
|
|
111
|
+
* - 7: I (IRQ disable)
|
|
112
|
+
* - 6: F (FIQ disable)
|
|
113
|
+
* - 5: T (Thumb state: 0=ARM, 1=Thumb)
|
|
114
|
+
* - 4-0: Mode (0x10=USR, 0x11=FIQ, 0x12=IRQ, 0x13=SVC, 0x17=ABT, 0x1B=UND, 0x1F=SYS)
|
|
115
|
+
*/
|
|
116
|
+
cpsr = MODE_SYS | (1 << CPSR_I) | (1 << CPSR_F);
|
|
117
|
+
/** Memory bus */
|
|
118
|
+
memory;
|
|
119
|
+
// ─── Banked Registers ────────────────────────────────────────────
|
|
120
|
+
/**
|
|
121
|
+
* Banked SP and LR for each mode.
|
|
122
|
+
* Index: 0=USR/SYS, 1=FIQ, 2=IRQ, 3=SVC, 4=ABT, 5=UND
|
|
123
|
+
*/
|
|
124
|
+
#bankedSP = new Uint32Array(6);
|
|
125
|
+
#bankedLR = new Uint32Array(6);
|
|
126
|
+
/**
|
|
127
|
+
* FIQ banked r8-r12 (5 registers). Only FIQ has these extra banked regs.
|
|
128
|
+
*/
|
|
129
|
+
#fiqBankedR8to12 = new Uint32Array(5);
|
|
130
|
+
/**
|
|
131
|
+
* USR/SYS r8-r12 saved when switching to FIQ mode.
|
|
132
|
+
*/
|
|
133
|
+
#usrBankedR8to12 = new Uint32Array(5);
|
|
134
|
+
/**
|
|
135
|
+
* SPSR for each privileged mode.
|
|
136
|
+
* Index: 0=FIQ, 1=IRQ, 2=SVC, 3=ABT, 4=UND
|
|
137
|
+
*/
|
|
138
|
+
#spsr = new Uint32Array(5);
|
|
139
|
+
// ─── Execution State ─────────────────────────────────────────────
|
|
140
|
+
/** Whether the CPU has halted (function returned to sentinel) */
|
|
141
|
+
#halted = false;
|
|
142
|
+
/** Halt flag for SWI 0x02 (Halt) */
|
|
143
|
+
#haltedBySWI = false;
|
|
144
|
+
/** Map of stub addresses to symbol names */
|
|
145
|
+
#stubs = new Map();
|
|
146
|
+
/** Next available stub address */
|
|
147
|
+
#nextStub = STUB_BASE;
|
|
148
|
+
/** Recorded external calls */
|
|
149
|
+
#externalCalls = [];
|
|
150
|
+
/** Optional debug hooks */
|
|
151
|
+
#hooks;
|
|
152
|
+
/** Platform-specific SWI handler */
|
|
153
|
+
#swiHandler;
|
|
154
|
+
constructor(memory, options) {
|
|
155
|
+
this.memory = memory;
|
|
156
|
+
this.#hooks = options?.hooks;
|
|
157
|
+
this.#swiHandler = options?.swiHandler;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Set the banked SP for a given mode without switching modes.
|
|
161
|
+
* Used to initialize stack pointers (e.g. GBA BIOS sets IRQ/SVC stacks).
|
|
162
|
+
*/
|
|
163
|
+
setBankedSP(mode, value) {
|
|
164
|
+
const bankIdx = SP_LR_BANK_INDEX[mode];
|
|
165
|
+
if (bankIdx !== undefined) {
|
|
166
|
+
this.#bankedSP[bankIdx] = value;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// ─── CPSR Accessors ──────────────────────────────────────────────
|
|
170
|
+
/** Get condition flags from CPSR as a CpsrFlags object */
|
|
171
|
+
get flags() {
|
|
172
|
+
return {
|
|
173
|
+
n: this.getN(),
|
|
174
|
+
z: this.getZ(),
|
|
175
|
+
c: this.getC(),
|
|
176
|
+
v: this.getV(),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** Get Negative flag */
|
|
180
|
+
getN() {
|
|
181
|
+
return (this.cpsr & (1 << CPSR_N)) !== 0;
|
|
182
|
+
}
|
|
183
|
+
/** Get Zero flag */
|
|
184
|
+
getZ() {
|
|
185
|
+
return (this.cpsr & (1 << CPSR_Z)) !== 0;
|
|
186
|
+
}
|
|
187
|
+
/** Get Carry flag */
|
|
188
|
+
getC() {
|
|
189
|
+
return (this.cpsr & (1 << CPSR_C)) !== 0;
|
|
190
|
+
}
|
|
191
|
+
/** Get Overflow flag */
|
|
192
|
+
getV() {
|
|
193
|
+
return (this.cpsr & (1 << CPSR_V)) !== 0;
|
|
194
|
+
}
|
|
195
|
+
/** Get Thumb state bit */
|
|
196
|
+
getT() {
|
|
197
|
+
return (this.cpsr & (1 << CPSR_T)) !== 0;
|
|
198
|
+
}
|
|
199
|
+
/** Get current CPU mode */
|
|
200
|
+
getMode() {
|
|
201
|
+
return this.cpsr & CPSR_MODE_MASK;
|
|
202
|
+
}
|
|
203
|
+
/** Set Negative flag */
|
|
204
|
+
setN(val) {
|
|
205
|
+
if (val) {
|
|
206
|
+
this.cpsr |= 1 << CPSR_N;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
this.cpsr &= ~(1 << CPSR_N);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** Set Zero flag */
|
|
213
|
+
setZ(val) {
|
|
214
|
+
if (val) {
|
|
215
|
+
this.cpsr |= 1 << CPSR_Z;
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
this.cpsr &= ~(1 << CPSR_Z);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** Set Carry flag */
|
|
222
|
+
setC(val) {
|
|
223
|
+
if (val) {
|
|
224
|
+
this.cpsr |= 1 << CPSR_C;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
this.cpsr &= ~(1 << CPSR_C);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/** Set Overflow flag */
|
|
231
|
+
setV(val) {
|
|
232
|
+
if (val) {
|
|
233
|
+
this.cpsr |= 1 << CPSR_V;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
this.cpsr &= ~(1 << CPSR_V);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** Set Thumb state bit */
|
|
240
|
+
setT(val) {
|
|
241
|
+
if (val) {
|
|
242
|
+
this.cpsr |= 1 << CPSR_T;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
this.cpsr &= ~(1 << CPSR_T);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** Set all four condition flags at once from an AluResult */
|
|
249
|
+
setFlags(n, z, c, v) {
|
|
250
|
+
this.setN(n);
|
|
251
|
+
this.setZ(z);
|
|
252
|
+
this.setC(c);
|
|
253
|
+
this.setV(v);
|
|
254
|
+
}
|
|
255
|
+
/** Set NZ flags from a result value, leave C and V unchanged */
|
|
256
|
+
setNZ(result) {
|
|
257
|
+
const u = result >>> 0;
|
|
258
|
+
this.setN((u & 0x80000000) !== 0);
|
|
259
|
+
this.setZ(u === 0);
|
|
260
|
+
}
|
|
261
|
+
// ─── SPSR Access ─────────────────────────────────────────────────
|
|
262
|
+
/** Get the SPSR for the current mode. Returns 0 for USR/SYS (no SPSR). */
|
|
263
|
+
getSPSR() {
|
|
264
|
+
const mode = this.getMode();
|
|
265
|
+
const idx = SPSR_BANK_INDEX[mode];
|
|
266
|
+
if (idx === undefined) {
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
return this.#spsr[idx];
|
|
270
|
+
}
|
|
271
|
+
/** Set the SPSR for the current mode. No-op for USR/SYS. */
|
|
272
|
+
setSPSR(value) {
|
|
273
|
+
const mode = this.getMode();
|
|
274
|
+
const idx = SPSR_BANK_INDEX[mode];
|
|
275
|
+
if (idx === undefined) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
this.#spsr[idx] = value;
|
|
279
|
+
}
|
|
280
|
+
// ─── Mode Switching ──────────────────────────────────────────────
|
|
281
|
+
/**
|
|
282
|
+
* Switch CPU mode. Saves banked registers from the old mode
|
|
283
|
+
* and restores them for the new mode.
|
|
284
|
+
*/
|
|
285
|
+
switchMode(newMode) {
|
|
286
|
+
const oldMode = this.getMode();
|
|
287
|
+
if (oldMode === newMode) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (!isValidMode(newMode)) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
// Save registers for old mode
|
|
294
|
+
this.#saveBankedRegisters(oldMode);
|
|
295
|
+
// Update CPSR mode bits
|
|
296
|
+
this.cpsr = (this.cpsr & ~CPSR_MODE_MASK) | newMode;
|
|
297
|
+
// Restore registers for new mode
|
|
298
|
+
this.#restoreBankedRegisters(newMode);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Save current SP/LR (and r8-r12 for FIQ) into the bank for the given mode.
|
|
302
|
+
*/
|
|
303
|
+
#saveBankedRegisters(mode) {
|
|
304
|
+
// Save SP/LR for the old mode
|
|
305
|
+
const bankIdx = SP_LR_BANK_INDEX[mode];
|
|
306
|
+
if (bankIdx !== undefined) {
|
|
307
|
+
this.#bankedSP[bankIdx] = this.registers[SP];
|
|
308
|
+
this.#bankedLR[bankIdx] = this.registers[LR];
|
|
309
|
+
}
|
|
310
|
+
// FIQ also saves r8-r12
|
|
311
|
+
if (mode === MODE_FIQ) {
|
|
312
|
+
for (let i = 0; i < 5; i++) {
|
|
313
|
+
this.#fiqBankedR8to12[i] = this.registers[8 + i];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
// Non-FIQ modes share USR r8-r12
|
|
318
|
+
for (let i = 0; i < 5; i++) {
|
|
319
|
+
this.#usrBankedR8to12[i] = this.registers[8 + i];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Restore SP/LR (and r8-r12 for FIQ) from the bank for the given mode.
|
|
325
|
+
*/
|
|
326
|
+
#restoreBankedRegisters(mode) {
|
|
327
|
+
// Restore SP/LR for the new mode
|
|
328
|
+
const bankIdx = SP_LR_BANK_INDEX[mode];
|
|
329
|
+
if (bankIdx !== undefined) {
|
|
330
|
+
this.registers[SP] = this.#bankedSP[bankIdx];
|
|
331
|
+
this.registers[LR] = this.#bankedLR[bankIdx];
|
|
332
|
+
}
|
|
333
|
+
// FIQ restores its own r8-r12
|
|
334
|
+
if (mode === MODE_FIQ) {
|
|
335
|
+
for (let i = 0; i < 5; i++) {
|
|
336
|
+
this.registers[8 + i] = this.#fiqBankedR8to12[i];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
// Non-FIQ modes restore USR r8-r12
|
|
341
|
+
for (let i = 0; i < 5; i++) {
|
|
342
|
+
this.registers[8 + i] = this.#usrBankedR8to12[i];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// ─── Public API ──────────────────────────────────────────────────
|
|
347
|
+
/** Attach or detach debug hooks */
|
|
348
|
+
setDebugHooks(hooks) {
|
|
349
|
+
this.#hooks = hooks;
|
|
350
|
+
}
|
|
351
|
+
/** Register a stub for an external function call */
|
|
352
|
+
registerStub(symbolName) {
|
|
353
|
+
const addr = this.#nextStub;
|
|
354
|
+
this.#stubs.set(addr, symbolName);
|
|
355
|
+
// Write a "bx lr" (Thumb) at the stub
|
|
356
|
+
this.memory.write16(addr, 0x4770);
|
|
357
|
+
this.#nextStub += 4;
|
|
358
|
+
return addr;
|
|
359
|
+
}
|
|
360
|
+
/** Serialize to a plain snapshot. */
|
|
361
|
+
serialize() {
|
|
362
|
+
return {
|
|
363
|
+
registers: new Uint32Array(this.registers),
|
|
364
|
+
cpsr: this.cpsr,
|
|
365
|
+
bankedSP: new Uint32Array(this.#bankedSP),
|
|
366
|
+
bankedLR: new Uint32Array(this.#bankedLR),
|
|
367
|
+
fiqBankedR8to12: new Uint32Array(this.#fiqBankedR8to12),
|
|
368
|
+
usrBankedR8to12: new Uint32Array(this.#usrBankedR8to12),
|
|
369
|
+
spsr: new Uint32Array(this.#spsr),
|
|
370
|
+
halted: this.#halted,
|
|
371
|
+
haltedBySWI: this.#haltedBySWI,
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/** Restore from a snapshot. */
|
|
375
|
+
deserialize(snap) {
|
|
376
|
+
this.registers.set(snap.registers);
|
|
377
|
+
this.cpsr = snap.cpsr;
|
|
378
|
+
this.#bankedSP.set(snap.bankedSP);
|
|
379
|
+
this.#bankedLR.set(snap.bankedLR);
|
|
380
|
+
this.#fiqBankedR8to12.set(snap.fiqBankedR8to12);
|
|
381
|
+
this.#usrBankedR8to12.set(snap.usrBankedR8to12);
|
|
382
|
+
this.#spsr.set(snap.spsr);
|
|
383
|
+
this.#halted = snap.halted;
|
|
384
|
+
this.#haltedBySWI = snap.haltedBySWI;
|
|
385
|
+
}
|
|
386
|
+
/** Reset CPU state for a new execution */
|
|
387
|
+
resetState() {
|
|
388
|
+
this.registers.fill(0);
|
|
389
|
+
this.cpsr = MODE_SYS | (1 << CPSR_I) | (1 << CPSR_F);
|
|
390
|
+
this.#spsr.fill(0);
|
|
391
|
+
this.#fiqBankedR8to12.fill(0);
|
|
392
|
+
this.#usrBankedR8to12.fill(0);
|
|
393
|
+
this.#bankedSP.fill(0);
|
|
394
|
+
this.#bankedLR.fill(0);
|
|
395
|
+
this.#externalCalls = [];
|
|
396
|
+
this.#halted = false;
|
|
397
|
+
this.#haltedBySWI = false;
|
|
398
|
+
}
|
|
399
|
+
/** Check if IRQs are disabled (CPSR I bit set) */
|
|
400
|
+
irqDisabled() {
|
|
401
|
+
return (this.cpsr & (1 << CPSR_I)) !== 0;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Enter IRQ exception.
|
|
405
|
+
*
|
|
406
|
+
* Standard ARM7TDMI exception entry:
|
|
407
|
+
* 1. Save CPSR to SPSR_irq
|
|
408
|
+
* 2. Switch to IRQ mode
|
|
409
|
+
* 3. Set LR_irq to return address (next instruction + 4)
|
|
410
|
+
* 4. Set I bit (disable further IRQs)
|
|
411
|
+
* 5. Clear T bit (enter ARM state)
|
|
412
|
+
* 6. Set PC to IRQ vector (0x00000018)
|
|
413
|
+
*
|
|
414
|
+
* The BIOS stub at 0x18 (installed by Gba.#installBiosStub) handles:
|
|
415
|
+
* - Saving registers to IRQ stack
|
|
416
|
+
* - Calling the user's handler from [0x03007FFC]
|
|
417
|
+
* - Restoring registers and returning from IRQ
|
|
418
|
+
*
|
|
419
|
+
* The BIOS stub at 0x80 handles IE/IF acknowledgment and BIOS IF mirror
|
|
420
|
+
* update before calling the user handler.
|
|
421
|
+
*/
|
|
422
|
+
enterIrq() {
|
|
423
|
+
// Save current CPSR as SPSR_irq
|
|
424
|
+
const savedCpsr = this.cpsr;
|
|
425
|
+
// LR_irq = address of next instruction + 4
|
|
426
|
+
// The CPU checks for IRQ between instructions, so PC points to the next instruction.
|
|
427
|
+
// ARM7TDMI: LR_irq = PC + 4 (return via SUBS PC, LR, #4)
|
|
428
|
+
const returnAddr = (this.registers[PC] + 4) >>> 0;
|
|
429
|
+
// Switch to IRQ mode (saves current SP/LR, restores IRQ SP/LR)
|
|
430
|
+
this.switchMode(MODE_IRQ);
|
|
431
|
+
// Set LR_irq to return address
|
|
432
|
+
this.registers[LR] = returnAddr;
|
|
433
|
+
// Set SPSR_irq
|
|
434
|
+
this.setSPSR(savedCpsr);
|
|
435
|
+
// Disable IRQs and enter ARM state
|
|
436
|
+
this.cpsr |= 1 << CPSR_I; // Disable IRQs
|
|
437
|
+
this.cpsr &= ~(1 << CPSR_T); // Enter ARM state
|
|
438
|
+
// Jump to BIOS IRQ vector — the stub handles IE/IF acknowledgment,
|
|
439
|
+
// BIOS IF mirror update, and calling the user handler
|
|
440
|
+
this.registers[PC] = 0x00000018;
|
|
441
|
+
}
|
|
442
|
+
/** Enter Undefined Instruction exception */
|
|
443
|
+
enterUnd(instrAddr) {
|
|
444
|
+
const savedCpsr = this.cpsr;
|
|
445
|
+
const isThumb = !!(this.cpsr & (1 << CPSR_T));
|
|
446
|
+
// LR_und = address of undefined instruction + 2 (Thumb) or + 4 (ARM)
|
|
447
|
+
const returnAddr = isThumb ? (instrAddr + 2) >>> 0 : (instrAddr + 4) >>> 0;
|
|
448
|
+
this.switchMode(MODE_UND);
|
|
449
|
+
this.registers[LR] = returnAddr;
|
|
450
|
+
this.setSPSR(savedCpsr);
|
|
451
|
+
this.cpsr |= 1 << CPSR_I; // Disable IRQs
|
|
452
|
+
this.cpsr &= ~(1 << CPSR_T); // Enter ARM state
|
|
453
|
+
this.registers[PC] = 0x00000004; // UND vector
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Execute one instruction (ARM or Thumb based on T bit).
|
|
457
|
+
* Returns false if halted.
|
|
458
|
+
*/
|
|
459
|
+
step() {
|
|
460
|
+
if (this.#halted || this.#haltedBySWI) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
const pc = this.registers[PC];
|
|
464
|
+
if ((pc & ~1) === SENTINEL_ADDR || (pc & ~1) === (SENTINEL_ADDR & ~1)) {
|
|
465
|
+
this.#halted = true;
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
// Check stubs
|
|
469
|
+
const pcAligned = this.getT() ? pc & ~1 : pc & ~3;
|
|
470
|
+
const stubName = this.#stubs.get(pcAligned);
|
|
471
|
+
if (stubName !== undefined) {
|
|
472
|
+
this.#externalCalls.push({
|
|
473
|
+
callSite: pcAligned,
|
|
474
|
+
targetAddress: pcAligned,
|
|
475
|
+
symbolName: stubName,
|
|
476
|
+
r0: this.registers[0],
|
|
477
|
+
r1: this.registers[1],
|
|
478
|
+
r2: this.registers[2],
|
|
479
|
+
r3: this.registers[3],
|
|
480
|
+
});
|
|
481
|
+
this.registers[0] = 0;
|
|
482
|
+
const returnAddr = this.registers[LR];
|
|
483
|
+
this.registers[PC] = returnAddr & ~1;
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
if (this.getT()) {
|
|
487
|
+
return this.#stepThumb();
|
|
488
|
+
}
|
|
489
|
+
else {
|
|
490
|
+
return this.#stepArm();
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
/** Run until halt or instruction limit */
|
|
494
|
+
run(maxInstructions) {
|
|
495
|
+
const trackable = this.memory;
|
|
496
|
+
trackable.resetWriteLog?.();
|
|
497
|
+
this.#externalCalls = [];
|
|
498
|
+
let count = 0;
|
|
499
|
+
while (count < maxInstructions && this.step()) {
|
|
500
|
+
count++;
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
registers: new Uint32Array(this.registers),
|
|
504
|
+
cpsr: this.flags,
|
|
505
|
+
memoryWrites: trackable.getWriteLog?.() ?? [],
|
|
506
|
+
externalCalls: [...this.#externalCalls],
|
|
507
|
+
instructionsExecuted: count,
|
|
508
|
+
completed: this.#halted,
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
// ─── Thumb Execution ─────────────────────────────────────────────
|
|
512
|
+
/** Execute one Thumb instruction */
|
|
513
|
+
#stepThumb() {
|
|
514
|
+
const pc = this.registers[PC];
|
|
515
|
+
const instrAddr = pc & ~1;
|
|
516
|
+
const instr = this.memory.read16(instrAddr);
|
|
517
|
+
if (this.#hooks?.onInstructionPre) {
|
|
518
|
+
const action = this.#hooks.onInstructionPre(instrAddr, instr);
|
|
519
|
+
if (action === 'break') {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
this.registers[PC] = (pc + 2) >>> 0;
|
|
524
|
+
this.#executeThumb(instr, instrAddr);
|
|
525
|
+
this.#hooks?.onInstructionPost?.(instrAddr, instr);
|
|
526
|
+
return !this.#halted;
|
|
527
|
+
}
|
|
528
|
+
/** Decode and execute a single 16-bit Thumb instruction. */
|
|
529
|
+
#executeThumb(instr, _instrAddr) {
|
|
530
|
+
const op = instr >>> 8;
|
|
531
|
+
// Format 19: Long Branch with Link (BL) — two-part
|
|
532
|
+
if ((instr & 0xf800) === 0xf000) {
|
|
533
|
+
this.#thumbBlPrefix(instr);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
if ((instr & 0xf800) === 0xf800) {
|
|
537
|
+
this.#thumbBlSuffix(instr);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
// Format 18: Unconditional Branch
|
|
541
|
+
if ((instr & 0xf800) === 0xe000) {
|
|
542
|
+
const offset11 = signExtend(instr & 0x7ff, 11);
|
|
543
|
+
// ARM7TDMI pipeline: PC = instrAddr+4. registers[PC] = instrAddr+2, so add +2.
|
|
544
|
+
this.registers[PC] = (this.registers[PC] + 2 + offset11 * 2) >>> 0;
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
// Format 17: SWI
|
|
548
|
+
if ((instr & 0xff00) === 0xdf00) {
|
|
549
|
+
this.#swiHandler?.(this, instr & 0xff);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
// Format 16: Conditional Branch
|
|
553
|
+
if ((instr & 0xf000) === 0xd000) {
|
|
554
|
+
this.#thumbCondBranch(instr);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
// Format 14: Push/Pop
|
|
558
|
+
if ((instr & 0xf600) === 0xb400) {
|
|
559
|
+
this.#thumbPushPop(instr);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
// Format 13: Add offset to SP
|
|
563
|
+
if ((op & 0xff) === 0xb0) {
|
|
564
|
+
const s = bit(instr, 7);
|
|
565
|
+
const offset7 = (instr & 0x7f) << 2;
|
|
566
|
+
if (s === 0) {
|
|
567
|
+
this.registers[SP] = (this.registers[SP] + offset7) >>> 0;
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
this.registers[SP] = (this.registers[SP] - offset7) >>> 0;
|
|
571
|
+
}
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
// Format 11: SP-relative Load/Store
|
|
575
|
+
if ((instr & 0xf000) === 0x9000) {
|
|
576
|
+
const l = bit(instr, 11);
|
|
577
|
+
const rd = bits(instr, 10, 8);
|
|
578
|
+
const offset8 = (instr & 0xff) << 2;
|
|
579
|
+
const address = (this.registers[SP] + offset8) >>> 0;
|
|
580
|
+
if (l === 1) {
|
|
581
|
+
this.registers[rd] = this.memory.read32(address);
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
this.memory.write32(address, this.registers[rd]);
|
|
585
|
+
}
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
// Format 12: Load Address
|
|
589
|
+
if ((instr & 0xf000) === 0xa000) {
|
|
590
|
+
const sp = bit(instr, 11);
|
|
591
|
+
const rd = bits(instr, 10, 8);
|
|
592
|
+
const offset8 = (instr & 0xff) << 2;
|
|
593
|
+
if (sp === 0) {
|
|
594
|
+
const base = ((this.registers[PC] + 2) & ~3) >>> 0;
|
|
595
|
+
this.registers[rd] = (base + offset8) >>> 0;
|
|
596
|
+
}
|
|
597
|
+
else {
|
|
598
|
+
this.registers[rd] = (this.registers[SP] + offset8) >>> 0;
|
|
599
|
+
}
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
// Format 10: Load/Store Halfword Imm
|
|
603
|
+
if ((instr & 0xf000) === 0x8000) {
|
|
604
|
+
const l = bit(instr, 11);
|
|
605
|
+
const offset5 = bits(instr, 10, 6);
|
|
606
|
+
const rb = bits(instr, 5, 3);
|
|
607
|
+
const rd = bits(instr, 2, 0);
|
|
608
|
+
const address = (this.registers[rb] + (offset5 << 1)) >>> 0;
|
|
609
|
+
if (l === 1) {
|
|
610
|
+
this.registers[rd] = this.memory.read16(address);
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
this.memory.write16(address, this.registers[rd]);
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
// Format 9: Load/Store Imm Offset
|
|
618
|
+
if ((instr & 0xe000) === 0x6000) {
|
|
619
|
+
this.#thumbImmOffsetLoadStore(instr);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
// Format 8: Load/Store Sign-Extended
|
|
623
|
+
if ((instr & 0xf200) === 0x5200) {
|
|
624
|
+
this.#thumbSignExtLoadStore(instr);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
// Format 7: Load/Store Register Offset
|
|
628
|
+
if ((instr & 0xf200) === 0x5000) {
|
|
629
|
+
this.#thumbRegOffsetLoadStore(instr);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
// Format 6: PC-Relative Load
|
|
633
|
+
if ((instr & 0xf800) === 0x4800) {
|
|
634
|
+
const rd = bits(instr, 10, 8);
|
|
635
|
+
const offset8 = (instr & 0xff) << 2;
|
|
636
|
+
const base = ((this.registers[PC] + 2) & ~3) >>> 0;
|
|
637
|
+
const address = (base + offset8) >>> 0;
|
|
638
|
+
this.registers[rd] = this.memory.read32(address);
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
// Format 5: Hi Register Ops / BX
|
|
642
|
+
if ((instr & 0xfc00) === 0x4400) {
|
|
643
|
+
this.#thumbHiRegBx(instr);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
// Format 4: ALU Operations
|
|
647
|
+
if ((instr & 0xfc00) === 0x4000) {
|
|
648
|
+
this.#thumbAluOp(instr);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
// Format 3: Move/Compare/Add/Sub Immediate
|
|
652
|
+
if ((instr & 0xe000) === 0x2000) {
|
|
653
|
+
this.#thumbImmOp(instr);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
// Format 2: Add/Subtract
|
|
657
|
+
if ((instr & 0xf800) === 0x1800) {
|
|
658
|
+
this.#thumbAddSub(instr);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
// Format 1: Move Shifted Register
|
|
662
|
+
if ((instr & 0xe000) === 0x0000) {
|
|
663
|
+
this.#thumbShifted(instr);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
// Format 15: Multiple Load/Store
|
|
667
|
+
if ((instr & 0xf000) === 0xc000) {
|
|
668
|
+
this.#thumbBlockTransfer(instr);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// ── Thumb instruction implementations ─────────────────────────────
|
|
673
|
+
#thumbShifted(instr) {
|
|
674
|
+
const op = bits(instr, 12, 11);
|
|
675
|
+
const offset5 = bits(instr, 10, 6);
|
|
676
|
+
const rs = bits(instr, 5, 3);
|
|
677
|
+
const rd = bits(instr, 2, 0);
|
|
678
|
+
const rsVal = this.registers[rs] | 0;
|
|
679
|
+
let result;
|
|
680
|
+
let carry;
|
|
681
|
+
switch (op) {
|
|
682
|
+
case 0:
|
|
683
|
+
[result, carry] = lsl(rsVal, offset5, this.getC());
|
|
684
|
+
break;
|
|
685
|
+
case 1:
|
|
686
|
+
[result, carry] = lsr(rsVal, offset5, this.getC(), true);
|
|
687
|
+
break;
|
|
688
|
+
case 2:
|
|
689
|
+
[result, carry] = asr(rsVal, offset5, this.getC(), true);
|
|
690
|
+
break;
|
|
691
|
+
default:
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
this.registers[rd] = result >>> 0;
|
|
695
|
+
this.setN((result & 0x80000000) !== 0);
|
|
696
|
+
this.setZ(result >>> 0 === 0);
|
|
697
|
+
this.setC(carry);
|
|
698
|
+
}
|
|
699
|
+
#thumbAddSub(instr) {
|
|
700
|
+
const i = bit(instr, 10);
|
|
701
|
+
const op = bit(instr, 9);
|
|
702
|
+
const rnImm = bits(instr, 8, 6);
|
|
703
|
+
const rs = bits(instr, 5, 3);
|
|
704
|
+
const rd = bits(instr, 2, 0);
|
|
705
|
+
const rsVal = this.registers[rs];
|
|
706
|
+
const operand = i === 1 ? rnImm : this.registers[rnImm];
|
|
707
|
+
const alu = op === 0 ? addWithFlags(rsVal | 0, operand | 0) : subWithFlags(rsVal | 0, operand | 0);
|
|
708
|
+
this.registers[rd] = alu.value >>> 0;
|
|
709
|
+
this.setFlags(alu.n, alu.z, alu.c, alu.v);
|
|
710
|
+
}
|
|
711
|
+
#thumbImmOp(instr) {
|
|
712
|
+
const op = bits(instr, 12, 11);
|
|
713
|
+
const rd = bits(instr, 10, 8);
|
|
714
|
+
const imm8 = instr & 0xff;
|
|
715
|
+
const rdVal = this.registers[rd];
|
|
716
|
+
switch (op) {
|
|
717
|
+
case 0: // MOV
|
|
718
|
+
this.registers[rd] = imm8;
|
|
719
|
+
this.setN(false);
|
|
720
|
+
this.setZ(imm8 === 0);
|
|
721
|
+
return;
|
|
722
|
+
case 1: {
|
|
723
|
+
// CMP
|
|
724
|
+
const alu = subWithFlags(rdVal | 0, imm8);
|
|
725
|
+
this.setFlags(alu.n, alu.z, alu.c, alu.v);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
case 2: {
|
|
729
|
+
// ADD
|
|
730
|
+
const alu = addWithFlags(rdVal | 0, imm8);
|
|
731
|
+
this.registers[rd] = alu.value >>> 0;
|
|
732
|
+
this.setFlags(alu.n, alu.z, alu.c, alu.v);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
case 3: {
|
|
736
|
+
// SUB
|
|
737
|
+
const alu = subWithFlags(rdVal | 0, imm8);
|
|
738
|
+
this.registers[rd] = alu.value >>> 0;
|
|
739
|
+
this.setFlags(alu.n, alu.z, alu.c, alu.v);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
#thumbAluOp(instr) {
|
|
745
|
+
const op = bits(instr, 9, 6);
|
|
746
|
+
const rs = bits(instr, 5, 3);
|
|
747
|
+
const rd = bits(instr, 2, 0);
|
|
748
|
+
const rdVal = this.registers[rd] | 0;
|
|
749
|
+
const rsVal = this.registers[rs] | 0;
|
|
750
|
+
let result;
|
|
751
|
+
let carry = this.getC();
|
|
752
|
+
let overflow = this.getV();
|
|
753
|
+
let writeResult = true;
|
|
754
|
+
switch (op) {
|
|
755
|
+
case 0x0:
|
|
756
|
+
result = rdVal & rsVal;
|
|
757
|
+
break;
|
|
758
|
+
case 0x1:
|
|
759
|
+
result = rdVal ^ rsVal;
|
|
760
|
+
break;
|
|
761
|
+
case 0x2: {
|
|
762
|
+
const amount = rsVal & 0xff;
|
|
763
|
+
[result, carry] = lsl(rdVal, amount, this.getC());
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
case 0x3: {
|
|
767
|
+
const amount = rsVal & 0xff;
|
|
768
|
+
[result, carry] = lsr(rdVal, amount, this.getC());
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
case 0x4: {
|
|
772
|
+
const amount = rsVal & 0xff;
|
|
773
|
+
[result, carry] = asr(rdVal, amount, this.getC());
|
|
774
|
+
break;
|
|
775
|
+
}
|
|
776
|
+
case 0x5: {
|
|
777
|
+
const alu = addWithFlags(rdVal, rsVal, this.getC() ? 1 : 0);
|
|
778
|
+
result = alu.value;
|
|
779
|
+
carry = alu.c;
|
|
780
|
+
overflow = alu.v;
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
case 0x6: {
|
|
784
|
+
const alu = subWithFlags(rdVal, rsVal, this.getC() ? 1 : 0);
|
|
785
|
+
result = alu.value;
|
|
786
|
+
carry = alu.c;
|
|
787
|
+
overflow = alu.v;
|
|
788
|
+
break;
|
|
789
|
+
}
|
|
790
|
+
case 0x7: {
|
|
791
|
+
const amount = rsVal & 0xff;
|
|
792
|
+
[result, carry] = ror(rdVal, amount, this.getC());
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
case 0x8:
|
|
796
|
+
result = rdVal & rsVal;
|
|
797
|
+
writeResult = false;
|
|
798
|
+
break;
|
|
799
|
+
case 0x9: {
|
|
800
|
+
const alu = subWithFlags(0, rsVal);
|
|
801
|
+
result = alu.value;
|
|
802
|
+
carry = alu.c;
|
|
803
|
+
overflow = alu.v;
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
case 0xa: {
|
|
807
|
+
const alu = subWithFlags(rdVal, rsVal);
|
|
808
|
+
result = alu.value;
|
|
809
|
+
carry = alu.c;
|
|
810
|
+
overflow = alu.v;
|
|
811
|
+
writeResult = false;
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
case 0xb: {
|
|
815
|
+
const alu = addWithFlags(rdVal, rsVal);
|
|
816
|
+
result = alu.value;
|
|
817
|
+
carry = alu.c;
|
|
818
|
+
overflow = alu.v;
|
|
819
|
+
writeResult = false;
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
case 0xc:
|
|
823
|
+
result = rdVal | rsVal;
|
|
824
|
+
break;
|
|
825
|
+
case 0xd:
|
|
826
|
+
result = Math.imul(rdVal, rsVal);
|
|
827
|
+
carry = false;
|
|
828
|
+
break;
|
|
829
|
+
case 0xe:
|
|
830
|
+
result = rdVal & ~rsVal;
|
|
831
|
+
break;
|
|
832
|
+
case 0xf:
|
|
833
|
+
result = ~rsVal;
|
|
834
|
+
break;
|
|
835
|
+
default:
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (writeResult) {
|
|
839
|
+
this.registers[rd] = result >>> 0;
|
|
840
|
+
}
|
|
841
|
+
this.setN((result & 0x80000000) !== 0);
|
|
842
|
+
this.setZ(result >>> 0 === 0);
|
|
843
|
+
this.setC(carry);
|
|
844
|
+
this.setV(overflow);
|
|
845
|
+
}
|
|
846
|
+
#thumbHiRegBx(instr) {
|
|
847
|
+
const op = bits(instr, 9, 8);
|
|
848
|
+
const hd = bit(instr, 7);
|
|
849
|
+
const hs = bit(instr, 6);
|
|
850
|
+
const rs = bits(instr, 5, 3) | (hs << 3);
|
|
851
|
+
const rd = bits(instr, 2, 0) | (hd << 3);
|
|
852
|
+
let rsVal = this.registers[rs];
|
|
853
|
+
// Pipeline correction: PC reads as instrAddr + 4 in Thumb mode
|
|
854
|
+
if (rs === PC) {
|
|
855
|
+
rsVal = (rsVal + 2) >>> 0;
|
|
856
|
+
}
|
|
857
|
+
// Pipeline correction for Rd=PC (reads as instrAddr + 4)
|
|
858
|
+
let rdVal = this.registers[rd];
|
|
859
|
+
if (rd === PC) {
|
|
860
|
+
rdVal = (rdVal + 2) >>> 0;
|
|
861
|
+
}
|
|
862
|
+
switch (op) {
|
|
863
|
+
case 0: // ADD
|
|
864
|
+
this.registers[rd] = (rdVal + rsVal) >>> 0;
|
|
865
|
+
if (rd === PC) {
|
|
866
|
+
this.registers[PC] = this.registers[PC] & ~1;
|
|
867
|
+
}
|
|
868
|
+
break;
|
|
869
|
+
case 1: {
|
|
870
|
+
// CMP
|
|
871
|
+
const alu = subWithFlags(rdVal | 0, rsVal | 0);
|
|
872
|
+
this.setFlags(alu.n, alu.z, alu.c, alu.v);
|
|
873
|
+
break;
|
|
874
|
+
}
|
|
875
|
+
case 2: // MOV
|
|
876
|
+
this.registers[rd] = rsVal;
|
|
877
|
+
if (rd === PC) {
|
|
878
|
+
this.registers[PC] = this.registers[PC] & ~1;
|
|
879
|
+
}
|
|
880
|
+
break;
|
|
881
|
+
case 3: // BX
|
|
882
|
+
if ((rsVal & ~1) === SENTINEL_ADDR || (rsVal & ~1) === (SENTINEL_ADDR & ~1)) {
|
|
883
|
+
this.#halted = true;
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
// T bit determined by bit 0
|
|
887
|
+
this.setT((rsVal & 1) !== 0);
|
|
888
|
+
this.registers[PC] = rsVal & ~1;
|
|
889
|
+
break;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
#thumbRegOffsetLoadStore(instr) {
|
|
893
|
+
const l = bit(instr, 11);
|
|
894
|
+
const b = bit(instr, 10);
|
|
895
|
+
const ro = bits(instr, 8, 6);
|
|
896
|
+
const rb = bits(instr, 5, 3);
|
|
897
|
+
const rd = bits(instr, 2, 0);
|
|
898
|
+
const address = (this.registers[rb] + this.registers[ro]) >>> 0;
|
|
899
|
+
if (l === 1) {
|
|
900
|
+
if (b === 1) {
|
|
901
|
+
this.registers[rd] = this.memory.read8(address);
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
// ARM7TDMI: unaligned word reads rotate
|
|
905
|
+
const aligned = address & ~3;
|
|
906
|
+
const rotation = (address & 3) * 8;
|
|
907
|
+
let value = this.memory.read32(aligned);
|
|
908
|
+
if (rotation) {
|
|
909
|
+
value = ((value >>> rotation) | (value << (32 - rotation))) >>> 0;
|
|
910
|
+
}
|
|
911
|
+
this.registers[rd] = value;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
if (b === 1) {
|
|
916
|
+
this.memory.write8(address, this.registers[rd]);
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
this.memory.write32(address & ~3, this.registers[rd]);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
#thumbSignExtLoadStore(instr) {
|
|
924
|
+
const h = bit(instr, 11);
|
|
925
|
+
const s = bit(instr, 10);
|
|
926
|
+
const ro = bits(instr, 8, 6);
|
|
927
|
+
const rb = bits(instr, 5, 3);
|
|
928
|
+
const rd = bits(instr, 2, 0);
|
|
929
|
+
const address = (this.registers[rb] + this.registers[ro]) >>> 0;
|
|
930
|
+
if (s === 0 && h === 0) {
|
|
931
|
+
this.memory.write16(address, this.registers[rd]);
|
|
932
|
+
}
|
|
933
|
+
else if (s === 0 && h === 1) {
|
|
934
|
+
this.registers[rd] = this.memory.read16(address);
|
|
935
|
+
}
|
|
936
|
+
else if (s === 1 && h === 0) {
|
|
937
|
+
this.registers[rd] = signExtend(this.memory.read8(address), 8) >>> 0;
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
if (address & 1) {
|
|
941
|
+
this.registers[rd] = signExtend(this.memory.read8(address), 8) >>> 0;
|
|
942
|
+
}
|
|
943
|
+
else {
|
|
944
|
+
this.registers[rd] = signExtend(this.memory.read16(address), 16) >>> 0;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
#thumbImmOffsetLoadStore(instr) {
|
|
949
|
+
const b = bit(instr, 12);
|
|
950
|
+
const l = bit(instr, 11);
|
|
951
|
+
const offset5 = bits(instr, 10, 6);
|
|
952
|
+
const rb = bits(instr, 5, 3);
|
|
953
|
+
const rd = bits(instr, 2, 0);
|
|
954
|
+
const base = this.registers[rb];
|
|
955
|
+
const offset = b === 0 ? offset5 << 2 : offset5;
|
|
956
|
+
const address = (base + offset) >>> 0;
|
|
957
|
+
if (l === 1) {
|
|
958
|
+
if (b === 1) {
|
|
959
|
+
this.registers[rd] = this.memory.read8(address);
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
// ARM7TDMI: unaligned word reads rotate
|
|
963
|
+
const aligned = address & ~3;
|
|
964
|
+
const rotation = (address & 3) * 8;
|
|
965
|
+
let value = this.memory.read32(aligned);
|
|
966
|
+
if (rotation) {
|
|
967
|
+
value = ((value >>> rotation) | (value << (32 - rotation))) >>> 0;
|
|
968
|
+
}
|
|
969
|
+
this.registers[rd] = value;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
else {
|
|
973
|
+
if (b === 1) {
|
|
974
|
+
this.memory.write8(address, this.registers[rd]);
|
|
975
|
+
}
|
|
976
|
+
else {
|
|
977
|
+
this.memory.write32(address & ~3, this.registers[rd]);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
#thumbPushPop(instr) {
|
|
982
|
+
const l = bit(instr, 11);
|
|
983
|
+
const r = bit(instr, 8);
|
|
984
|
+
const rlist = instr & 0xff;
|
|
985
|
+
if (l === 0) {
|
|
986
|
+
// PUSH
|
|
987
|
+
let sp = this.registers[SP];
|
|
988
|
+
let count = 0;
|
|
989
|
+
for (let i = 0; i < 8; i++) {
|
|
990
|
+
if (rlist & (1 << i)) {
|
|
991
|
+
count++;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (r) {
|
|
995
|
+
count++;
|
|
996
|
+
}
|
|
997
|
+
sp = (sp - count * 4) >>> 0;
|
|
998
|
+
this.registers[SP] = sp;
|
|
999
|
+
let addr = sp;
|
|
1000
|
+
for (let i = 0; i < 8; i++) {
|
|
1001
|
+
if (rlist & (1 << i)) {
|
|
1002
|
+
this.memory.write32(addr, this.registers[i]);
|
|
1003
|
+
addr = (addr + 4) >>> 0;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
if (r) {
|
|
1007
|
+
this.memory.write32(addr, this.registers[LR]);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
else {
|
|
1011
|
+
// POP
|
|
1012
|
+
let addr = this.registers[SP];
|
|
1013
|
+
for (let i = 0; i < 8; i++) {
|
|
1014
|
+
if (rlist & (1 << i)) {
|
|
1015
|
+
this.registers[i] = this.memory.read32(addr);
|
|
1016
|
+
addr = (addr + 4) >>> 0;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
if (r) {
|
|
1020
|
+
const val = this.memory.read32(addr);
|
|
1021
|
+
addr = (addr + 4) >>> 0;
|
|
1022
|
+
if ((val & ~1) === SENTINEL_ADDR || (val & ~1) === (SENTINEL_ADDR & ~1)) {
|
|
1023
|
+
this.#halted = true;
|
|
1024
|
+
this.registers[SP] = addr;
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
this.registers[PC] = val & ~1;
|
|
1028
|
+
}
|
|
1029
|
+
this.registers[SP] = addr;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
#thumbBlockTransfer(instr) {
|
|
1033
|
+
const l = bit(instr, 11);
|
|
1034
|
+
const rb = bits(instr, 10, 8);
|
|
1035
|
+
const rlist = instr & 0xff;
|
|
1036
|
+
let addr = this.registers[rb];
|
|
1037
|
+
if (l === 1) {
|
|
1038
|
+
// LDMIA
|
|
1039
|
+
for (let i = 0; i < 8; i++) {
|
|
1040
|
+
if (rlist & (1 << i)) {
|
|
1041
|
+
this.registers[i] = this.memory.read32(addr);
|
|
1042
|
+
addr = (addr + 4) >>> 0;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
// No writeback if Rb is in the register list (loaded value takes precedence)
|
|
1046
|
+
if (!(rlist & (1 << rb))) {
|
|
1047
|
+
this.registers[rb] = addr;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
// STMIA
|
|
1052
|
+
for (let i = 0; i < 8; i++) {
|
|
1053
|
+
if (rlist & (1 << i)) {
|
|
1054
|
+
this.memory.write32(addr, this.registers[i]);
|
|
1055
|
+
addr = (addr + 4) >>> 0;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
this.registers[rb] = addr;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
#thumbCondBranch(instr) {
|
|
1062
|
+
const cond = bits(instr, 11, 8);
|
|
1063
|
+
const offset8 = signExtend(instr & 0xff, 8);
|
|
1064
|
+
if (!checkCondition(cond, this.getN(), this.getZ(), this.getC(), this.getV())) {
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
// ARM7TDMI pipeline: PC reads as instrAddr+4 in Thumb mode.
|
|
1068
|
+
// registers[PC] is instrAddr+2 (pre-incremented), so add +2 for pipeline.
|
|
1069
|
+
this.registers[PC] = (this.registers[PC] + 2 + offset8 * 2) >>> 0;
|
|
1070
|
+
}
|
|
1071
|
+
#thumbBlPrefix(instr) {
|
|
1072
|
+
const offset11 = signExtend(instr & 0x7ff, 11);
|
|
1073
|
+
// ARM7TDMI pipeline: PC = instrAddr+4. registers[PC] = instrAddr+2, so add +2.
|
|
1074
|
+
this.registers[LR] = (this.registers[PC] + 2 + (offset11 << 12)) >>> 0;
|
|
1075
|
+
}
|
|
1076
|
+
#thumbBlSuffix(instr) {
|
|
1077
|
+
const offset11 = (instr & 0x7ff) << 1;
|
|
1078
|
+
const target = (this.registers[LR] + offset11) >>> 0;
|
|
1079
|
+
this.registers[LR] = (this.registers[PC] | 1) >>> 0;
|
|
1080
|
+
this.registers[PC] = target & ~1;
|
|
1081
|
+
}
|
|
1082
|
+
// ─── ARM Execution ───────────────────────────────────────────────
|
|
1083
|
+
/** Execute one ARM (32-bit) instruction */
|
|
1084
|
+
#stepArm() {
|
|
1085
|
+
const pc = this.registers[PC];
|
|
1086
|
+
const instrAddr = pc & ~3;
|
|
1087
|
+
const instr = this.memory.read32(instrAddr);
|
|
1088
|
+
if (this.#hooks?.onInstructionPre) {
|
|
1089
|
+
const action = this.#hooks.onInstructionPre(instrAddr, instr);
|
|
1090
|
+
if (action === 'break') {
|
|
1091
|
+
return false;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
// Advance PC by 4 (ARM instructions are 4 bytes)
|
|
1095
|
+
this.registers[PC] = (pc + 4) >>> 0;
|
|
1096
|
+
// Check condition code (bits 31-28)
|
|
1097
|
+
const cond = (instr >>> 28) & 0xf;
|
|
1098
|
+
if (checkCondition(cond, this.getN(), this.getZ(), this.getC(), this.getV())) {
|
|
1099
|
+
this.#executeArm(instr, instrAddr);
|
|
1100
|
+
}
|
|
1101
|
+
this.#hooks?.onInstructionPost?.(instrAddr, instr);
|
|
1102
|
+
return !this.#halted;
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Decode and execute a single 32-bit ARM instruction.
|
|
1106
|
+
*
|
|
1107
|
+
* ARM instruction categories by bits 27-25:
|
|
1108
|
+
* - 00x: Data Processing / Multiply / Misc
|
|
1109
|
+
* - 010: Load/Store Word/Byte (immediate offset)
|
|
1110
|
+
* - 011: Load/Store Word/Byte (register offset)
|
|
1111
|
+
* - 100: Block Data Transfer (LDM/STM)
|
|
1112
|
+
* - 101: Branch (B/BL)
|
|
1113
|
+
* - 110: Coprocessor (undefined for GBA)
|
|
1114
|
+
* - 111: SWI / Coprocessor
|
|
1115
|
+
*/
|
|
1116
|
+
#executeArm(instr, instrAddr) {
|
|
1117
|
+
const bits27_25 = bits(instr, 27, 25);
|
|
1118
|
+
switch (bits27_25) {
|
|
1119
|
+
case 0b000:
|
|
1120
|
+
case 0b001:
|
|
1121
|
+
this.#armDataProcessingOrMisc(instr, instrAddr);
|
|
1122
|
+
break;
|
|
1123
|
+
case 0b010:
|
|
1124
|
+
this.#armSingleDataTransferImm(instr);
|
|
1125
|
+
break;
|
|
1126
|
+
case 0b011:
|
|
1127
|
+
if (bit(instr, 4) === 0) {
|
|
1128
|
+
this.#armSingleDataTransferReg(instr);
|
|
1129
|
+
}
|
|
1130
|
+
else {
|
|
1131
|
+
// Undefined instruction on ARM7TDMI
|
|
1132
|
+
this.enterUnd(instrAddr);
|
|
1133
|
+
}
|
|
1134
|
+
break;
|
|
1135
|
+
case 0b100:
|
|
1136
|
+
this.#armBlockDataTransfer(instr);
|
|
1137
|
+
break;
|
|
1138
|
+
case 0b101:
|
|
1139
|
+
this.#armBranch(instr, instrAddr);
|
|
1140
|
+
break;
|
|
1141
|
+
case 0b110:
|
|
1142
|
+
// Coprocessor — undefined on GBA
|
|
1143
|
+
break;
|
|
1144
|
+
case 0b111:
|
|
1145
|
+
if (bit(instr, 24) === 1) {
|
|
1146
|
+
// SWI
|
|
1147
|
+
this.#armSwi(instr);
|
|
1148
|
+
}
|
|
1149
|
+
// else coprocessor operations — undefined on GBA
|
|
1150
|
+
break;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
// ─── ARM Data Processing / Misc ──────────────────────────────────
|
|
1154
|
+
/**
|
|
1155
|
+
* Handle ARM data processing instructions and miscellaneous instructions
|
|
1156
|
+
* that share the same top bits (27-25 = 00x).
|
|
1157
|
+
*/
|
|
1158
|
+
#armDataProcessingOrMisc(instr, _instrAddr) {
|
|
1159
|
+
// Check for multiplies: bits 27-22=000000, bits 7-4=1001
|
|
1160
|
+
if ((instr & 0x0fc000f0) === 0x00000090) {
|
|
1161
|
+
this.#armMultiply(instr);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
// Check for long multiplies: bits 27-23=00001, bits 7-4=1001
|
|
1165
|
+
if ((instr & 0x0f8000f0) === 0x00800090) {
|
|
1166
|
+
this.#armMultiplyLong(instr);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
// Check for single data swap: bits 27-23=00010, bits 11-4=00001001
|
|
1170
|
+
if ((instr & 0x0fb00ff0) === 0x01000090) {
|
|
1171
|
+
this.#armSwap(instr);
|
|
1172
|
+
return;
|
|
1173
|
+
}
|
|
1174
|
+
// Check for BX: 0001_0010_1111_1111_1111_0001
|
|
1175
|
+
if ((instr & 0x0ffffff0) === 0x012fff10) {
|
|
1176
|
+
this.#armBx(instr);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
// Check for halfword/signed transfers: bits 27-25=000, bit7=1, bit4=1
|
|
1180
|
+
// But NOT multiply (already checked above)
|
|
1181
|
+
if ((instr & 0x0e000090) === 0x00000090 && (instr & 0x00000060) !== 0) {
|
|
1182
|
+
this.#armHalfwordTransfer(instr);
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
// Check for MRS: bits 27-23=00010, bits 21-20=00, bits 11-0=0000_0000_0000
|
|
1186
|
+
if ((instr & 0x0fbf0fff) === 0x010f0000) {
|
|
1187
|
+
this.#armMrs(instr);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
// Check for MSR (register): bits 27-25=000, bits 24-23=10, bit 21=1, bit 20=0,
|
|
1191
|
+
// bits 15-12=1111, bits 11-4=00000000. Field mask (bits 19-16) varies.
|
|
1192
|
+
if ((instr & 0x0fb0fff0) === 0x0120f000) {
|
|
1193
|
+
this.#armMsrReg(instr);
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
// Check for MSR (immediate): bits 27-25=001, bits 24-23=10, bit 21=1, bit 20=0,
|
|
1197
|
+
// bits 15-12=1111. Field mask (bits 19-16) varies.
|
|
1198
|
+
if ((instr & 0x0fb0f000) === 0x0320f000) {
|
|
1199
|
+
this.#armMsrImm(instr);
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
// Data Processing instruction
|
|
1203
|
+
this.#armDataProcessing(instr);
|
|
1204
|
+
}
|
|
1205
|
+
/** ARM barrel shifter: compute the shifter operand and carry out */
|
|
1206
|
+
#armBarrelShifter(instr, isImmediate) {
|
|
1207
|
+
if (isImmediate) {
|
|
1208
|
+
// Immediate: 8-bit value rotated right by 2*rotate4
|
|
1209
|
+
const imm8 = instr & 0xff;
|
|
1210
|
+
const rotate = ((instr >>> 8) & 0xf) * 2;
|
|
1211
|
+
if (rotate === 0) {
|
|
1212
|
+
return [imm8, this.getC()];
|
|
1213
|
+
}
|
|
1214
|
+
const result = (imm8 >>> rotate) | (imm8 << (32 - rotate)) | 0;
|
|
1215
|
+
return [result, ((result >>> 31) & 1) !== 0];
|
|
1216
|
+
}
|
|
1217
|
+
// Register operand
|
|
1218
|
+
const rm = instr & 0xf;
|
|
1219
|
+
let rmVal = this.registers[rm];
|
|
1220
|
+
const shiftType = (instr >>> 5) & 0x3;
|
|
1221
|
+
const regShift = bit(instr, 4);
|
|
1222
|
+
// ARM7TDMI pipeline: PC reads as instrAddr+8 normally, instrAddr+12 with register shift.
|
|
1223
|
+
// registers[PC] = instrAddr+4, so add +4 or +8 respectively.
|
|
1224
|
+
if (rm === PC) {
|
|
1225
|
+
rmVal = (rmVal + (regShift ? 8 : 4)) >>> 0;
|
|
1226
|
+
}
|
|
1227
|
+
let amount;
|
|
1228
|
+
if (regShift) {
|
|
1229
|
+
// Register-specified shift amount (Rs)
|
|
1230
|
+
const rsReg = (instr >>> 8) & 0xf;
|
|
1231
|
+
amount = this.registers[rsReg] & 0xff;
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
// Immediate-specified shift amount
|
|
1235
|
+
amount = (instr >>> 7) & 0x1f;
|
|
1236
|
+
}
|
|
1237
|
+
switch (shiftType) {
|
|
1238
|
+
case 0: // LSL
|
|
1239
|
+
return lsl(rmVal | 0, amount, this.getC());
|
|
1240
|
+
case 1: // LSR
|
|
1241
|
+
return lsr(rmVal | 0, amount, this.getC(), !regShift);
|
|
1242
|
+
case 2: // ASR
|
|
1243
|
+
return asr(rmVal | 0, amount, this.getC(), !regShift);
|
|
1244
|
+
case 3: // ROR
|
|
1245
|
+
if (!regShift && amount === 0) {
|
|
1246
|
+
// RRX (rotate right extended): shift right by 1, carry in from CPSR.C
|
|
1247
|
+
const carry = (rmVal & 1) !== 0;
|
|
1248
|
+
const result = (this.getC() ? 0x80000000 : 0) | (rmVal >>> 1) | 0;
|
|
1249
|
+
return [result, carry];
|
|
1250
|
+
}
|
|
1251
|
+
return ror(rmVal | 0, amount, this.getC());
|
|
1252
|
+
default:
|
|
1253
|
+
return [rmVal, this.getC()];
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
/** ARM data processing instructions */
|
|
1257
|
+
#armDataProcessing(instr) {
|
|
1258
|
+
const isImm = bit(instr, 25);
|
|
1259
|
+
const opcode = bits(instr, 24, 21);
|
|
1260
|
+
const setFlags = bit(instr, 20) === 1;
|
|
1261
|
+
const rn = bits(instr, 19, 16);
|
|
1262
|
+
const rd = bits(instr, 15, 12);
|
|
1263
|
+
const [op2, shifterCarry] = this.#armBarrelShifter(instr, isImm === 1);
|
|
1264
|
+
// ARM7TDMI pipeline: PC reads as instrAddr+8 for data processing.
|
|
1265
|
+
// registers[PC] = instrAddr+4, so add +4 for Rn=PC.
|
|
1266
|
+
// For register-shifted: PC reads as instrAddr+12, so add +8.
|
|
1267
|
+
const regShift = isImm === 0 && bit(instr, 4) === 1;
|
|
1268
|
+
let rnVal = this.registers[rn];
|
|
1269
|
+
if (rn === PC) {
|
|
1270
|
+
rnVal = (rnVal + (regShift ? 8 : 4)) >>> 0;
|
|
1271
|
+
}
|
|
1272
|
+
let result;
|
|
1273
|
+
let carry = shifterCarry;
|
|
1274
|
+
let overflow = this.getV();
|
|
1275
|
+
let writeResult = true;
|
|
1276
|
+
switch (opcode) {
|
|
1277
|
+
case 0x0: // AND
|
|
1278
|
+
result = rnVal & op2;
|
|
1279
|
+
break;
|
|
1280
|
+
case 0x1: // EOR
|
|
1281
|
+
result = rnVal ^ op2;
|
|
1282
|
+
break;
|
|
1283
|
+
case 0x2: {
|
|
1284
|
+
// SUB
|
|
1285
|
+
const alu = subWithFlags(rnVal | 0, op2 | 0);
|
|
1286
|
+
result = alu.value;
|
|
1287
|
+
carry = alu.c;
|
|
1288
|
+
overflow = alu.v;
|
|
1289
|
+
break;
|
|
1290
|
+
}
|
|
1291
|
+
case 0x3: {
|
|
1292
|
+
// RSB
|
|
1293
|
+
const alu = subWithFlags(op2 | 0, rnVal | 0);
|
|
1294
|
+
result = alu.value;
|
|
1295
|
+
carry = alu.c;
|
|
1296
|
+
overflow = alu.v;
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
case 0x4: {
|
|
1300
|
+
// ADD
|
|
1301
|
+
const alu = addWithFlags(rnVal | 0, op2 | 0);
|
|
1302
|
+
result = alu.value;
|
|
1303
|
+
carry = alu.c;
|
|
1304
|
+
overflow = alu.v;
|
|
1305
|
+
break;
|
|
1306
|
+
}
|
|
1307
|
+
case 0x5: {
|
|
1308
|
+
// ADC
|
|
1309
|
+
const alu = addWithFlags(rnVal | 0, op2 | 0, this.getC() ? 1 : 0);
|
|
1310
|
+
result = alu.value;
|
|
1311
|
+
carry = alu.c;
|
|
1312
|
+
overflow = alu.v;
|
|
1313
|
+
break;
|
|
1314
|
+
}
|
|
1315
|
+
case 0x6: {
|
|
1316
|
+
// SBC
|
|
1317
|
+
const alu = subWithFlags(rnVal | 0, op2 | 0, this.getC() ? 1 : 0);
|
|
1318
|
+
result = alu.value;
|
|
1319
|
+
carry = alu.c;
|
|
1320
|
+
overflow = alu.v;
|
|
1321
|
+
break;
|
|
1322
|
+
}
|
|
1323
|
+
case 0x7: {
|
|
1324
|
+
// RSC
|
|
1325
|
+
const alu = subWithFlags(op2 | 0, rnVal | 0, this.getC() ? 1 : 0);
|
|
1326
|
+
result = alu.value;
|
|
1327
|
+
carry = alu.c;
|
|
1328
|
+
overflow = alu.v;
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
case 0x8: // TST
|
|
1332
|
+
result = rnVal & op2;
|
|
1333
|
+
writeResult = false;
|
|
1334
|
+
break;
|
|
1335
|
+
case 0x9: // TEQ
|
|
1336
|
+
result = rnVal ^ op2;
|
|
1337
|
+
writeResult = false;
|
|
1338
|
+
break;
|
|
1339
|
+
case 0xa: {
|
|
1340
|
+
// CMP
|
|
1341
|
+
const alu = subWithFlags(rnVal | 0, op2 | 0);
|
|
1342
|
+
result = alu.value;
|
|
1343
|
+
carry = alu.c;
|
|
1344
|
+
overflow = alu.v;
|
|
1345
|
+
writeResult = false;
|
|
1346
|
+
break;
|
|
1347
|
+
}
|
|
1348
|
+
case 0xb: {
|
|
1349
|
+
// CMN
|
|
1350
|
+
const alu = addWithFlags(rnVal | 0, op2 | 0);
|
|
1351
|
+
result = alu.value;
|
|
1352
|
+
carry = alu.c;
|
|
1353
|
+
overflow = alu.v;
|
|
1354
|
+
writeResult = false;
|
|
1355
|
+
break;
|
|
1356
|
+
}
|
|
1357
|
+
case 0xc: // ORR
|
|
1358
|
+
result = rnVal | op2;
|
|
1359
|
+
break;
|
|
1360
|
+
case 0xd: // MOV
|
|
1361
|
+
result = op2;
|
|
1362
|
+
break;
|
|
1363
|
+
case 0xe: // BIC
|
|
1364
|
+
result = rnVal & ~op2;
|
|
1365
|
+
break;
|
|
1366
|
+
case 0xf: // MVN
|
|
1367
|
+
result = ~op2;
|
|
1368
|
+
break;
|
|
1369
|
+
default:
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
if (writeResult) {
|
|
1373
|
+
this.registers[rd] = result >>> 0;
|
|
1374
|
+
}
|
|
1375
|
+
if (setFlags) {
|
|
1376
|
+
if (rd === PC) {
|
|
1377
|
+
// Data processing with S=1 and Rd=PC: copy SPSR to CPSR (return from exception)
|
|
1378
|
+
const spsr = this.getSPSR();
|
|
1379
|
+
const newMode = spsr & CPSR_MODE_MASK;
|
|
1380
|
+
const oldMode = this.getMode();
|
|
1381
|
+
// Must switch mode BEFORE overwriting CPSR, so banked registers are
|
|
1382
|
+
// properly saved (old mode) and restored (new mode).
|
|
1383
|
+
if (newMode !== oldMode) {
|
|
1384
|
+
this.switchMode(newMode);
|
|
1385
|
+
}
|
|
1386
|
+
this.cpsr = spsr;
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
this.setN((result & 0x80000000) !== 0);
|
|
1390
|
+
this.setZ(result >>> 0 === 0);
|
|
1391
|
+
this.setC(carry);
|
|
1392
|
+
this.setV(overflow);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
// If Rd is PC and we wrote to it (non-flag instructions), flush pipeline
|
|
1396
|
+
if (writeResult && rd === PC) {
|
|
1397
|
+
// If the T bit changed, the new mode takes effect on next fetch
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
// ─── ARM Multiply ────────────────────────────────────────────────
|
|
1401
|
+
/** ARM multiply: MUL, MLA */
|
|
1402
|
+
#armMultiply(instr) {
|
|
1403
|
+
const accumulate = bit(instr, 21) === 1;
|
|
1404
|
+
const setFlags = bit(instr, 20) === 1;
|
|
1405
|
+
const rd = bits(instr, 19, 16);
|
|
1406
|
+
const rn = bits(instr, 15, 12);
|
|
1407
|
+
const rs = bits(instr, 11, 8);
|
|
1408
|
+
const rm = instr & 0xf;
|
|
1409
|
+
let result = Math.imul(this.registers[rm] | 0, this.registers[rs] | 0);
|
|
1410
|
+
if (accumulate) {
|
|
1411
|
+
result = (result + (this.registers[rn] | 0)) | 0;
|
|
1412
|
+
}
|
|
1413
|
+
this.registers[rd] = result >>> 0;
|
|
1414
|
+
if (setFlags) {
|
|
1415
|
+
this.setN((result & 0x80000000) !== 0);
|
|
1416
|
+
this.setZ(result >>> 0 === 0);
|
|
1417
|
+
// C is unpredictable on ARMv4T, V is unchanged
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
/** ARM long multiply: UMULL, UMLAL, SMULL, SMLAL */
|
|
1421
|
+
#armMultiplyLong(instr) {
|
|
1422
|
+
const isSigned = bit(instr, 22) === 1;
|
|
1423
|
+
const accumulate = bit(instr, 21) === 1;
|
|
1424
|
+
const setFlags = bit(instr, 20) === 1;
|
|
1425
|
+
const rdHi = bits(instr, 19, 16);
|
|
1426
|
+
const rdLo = bits(instr, 15, 12);
|
|
1427
|
+
const rs = bits(instr, 11, 8);
|
|
1428
|
+
const rm = instr & 0xf;
|
|
1429
|
+
let resultHi;
|
|
1430
|
+
let resultLo;
|
|
1431
|
+
if (isSigned) {
|
|
1432
|
+
// SMULL/SMLAL: signed 32x32 -> 64
|
|
1433
|
+
const a = this.registers[rm] | 0;
|
|
1434
|
+
const b = this.registers[rs] | 0;
|
|
1435
|
+
// Use BigInt for 64-bit precision
|
|
1436
|
+
const product = BigInt(a) * BigInt(b);
|
|
1437
|
+
resultLo = Number(product & 0xffffffffn) >>> 0;
|
|
1438
|
+
resultHi = Number((product >> 32n) & 0xffffffffn) >>> 0;
|
|
1439
|
+
}
|
|
1440
|
+
else {
|
|
1441
|
+
// UMULL/UMLAL: unsigned 32x32 -> 64
|
|
1442
|
+
const a = this.registers[rm] >>> 0;
|
|
1443
|
+
const b = this.registers[rs] >>> 0;
|
|
1444
|
+
const product = BigInt(a) * BigInt(b);
|
|
1445
|
+
resultLo = Number(product & 0xffffffffn) >>> 0;
|
|
1446
|
+
resultHi = Number((product >> 32n) & 0xffffffffn) >>> 0;
|
|
1447
|
+
}
|
|
1448
|
+
if (accumulate) {
|
|
1449
|
+
// Add to existing RdHi:RdLo
|
|
1450
|
+
const accLo = this.registers[rdLo] >>> 0;
|
|
1451
|
+
const accHi = this.registers[rdHi] >>> 0;
|
|
1452
|
+
const sum = BigInt(resultHi) * 0x100000000n + BigInt(resultLo) + BigInt(accHi) * 0x100000000n + BigInt(accLo);
|
|
1453
|
+
resultLo = Number(sum & 0xffffffffn) >>> 0;
|
|
1454
|
+
resultHi = Number((sum >> 32n) & 0xffffffffn) >>> 0;
|
|
1455
|
+
}
|
|
1456
|
+
this.registers[rdLo] = resultLo;
|
|
1457
|
+
this.registers[rdHi] = resultHi;
|
|
1458
|
+
if (setFlags) {
|
|
1459
|
+
this.setN((resultHi & 0x80000000) !== 0);
|
|
1460
|
+
this.setZ(resultHi === 0 && resultLo === 0);
|
|
1461
|
+
// C, V are unpredictable
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
// ─── ARM Swap ────────────────────────────────────────────────────
|
|
1465
|
+
/** ARM SWP/SWPB: atomic swap */
|
|
1466
|
+
#armSwap(instr) {
|
|
1467
|
+
const byteMode = bit(instr, 22) === 1;
|
|
1468
|
+
const rn = bits(instr, 19, 16);
|
|
1469
|
+
const rd = bits(instr, 15, 12);
|
|
1470
|
+
const rm = instr & 0xf;
|
|
1471
|
+
const address = this.registers[rn];
|
|
1472
|
+
if (byteMode) {
|
|
1473
|
+
const temp = this.memory.read8(address);
|
|
1474
|
+
this.memory.write8(address, this.registers[rm] & 0xff);
|
|
1475
|
+
this.registers[rd] = temp;
|
|
1476
|
+
}
|
|
1477
|
+
else {
|
|
1478
|
+
// SWP: unaligned word reads rotate (same as LDR)
|
|
1479
|
+
const aligned = address & ~3;
|
|
1480
|
+
const rotation = (address & 3) * 8;
|
|
1481
|
+
let temp = this.memory.read32(aligned);
|
|
1482
|
+
if (rotation) {
|
|
1483
|
+
temp = ((temp >>> rotation) | (temp << (32 - rotation))) >>> 0;
|
|
1484
|
+
}
|
|
1485
|
+
this.memory.write32(aligned, this.registers[rm]);
|
|
1486
|
+
this.registers[rd] = temp;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
// ─── ARM Branch Exchange ─────────────────────────────────────────
|
|
1490
|
+
/** ARM BX: branch and exchange instruction set */
|
|
1491
|
+
#armBx(instr) {
|
|
1492
|
+
const rm = instr & 0xf;
|
|
1493
|
+
const target = this.registers[rm];
|
|
1494
|
+
if ((target & ~1) === SENTINEL_ADDR || (target & ~1) === (SENTINEL_ADDR & ~1)) {
|
|
1495
|
+
this.#halted = true;
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
this.setT((target & 1) !== 0);
|
|
1499
|
+
this.registers[PC] = target & ~1;
|
|
1500
|
+
}
|
|
1501
|
+
// ─── ARM Branch ──────────────────────────────────────────────────
|
|
1502
|
+
/** ARM B/BL: branch (with optional link) */
|
|
1503
|
+
#armBranch(instr, instrAddr) {
|
|
1504
|
+
const link = bit(instr, 24) === 1;
|
|
1505
|
+
const offset = signExtend(instr & 0x00ffffff, 24) << 2;
|
|
1506
|
+
if (link) {
|
|
1507
|
+
// LR = address of instruction after this one
|
|
1508
|
+
this.registers[LR] = (instrAddr + 4) >>> 0;
|
|
1509
|
+
}
|
|
1510
|
+
// ARM7TDMI pipeline: PC = instrAddr+8. registers[PC] = instrAddr+4, so add +4.
|
|
1511
|
+
this.registers[PC] = (this.registers[PC] + 4 + offset) >>> 0;
|
|
1512
|
+
}
|
|
1513
|
+
// ─── ARM Single Data Transfer (Immediate offset) ─────────────────
|
|
1514
|
+
/** ARM LDR/STR with immediate offset */
|
|
1515
|
+
#armSingleDataTransferImm(instr) {
|
|
1516
|
+
const pre = bit(instr, 24) === 1;
|
|
1517
|
+
const up = bit(instr, 23) === 1;
|
|
1518
|
+
const byteMode = bit(instr, 22) === 1;
|
|
1519
|
+
const writeback = bit(instr, 21) === 1;
|
|
1520
|
+
const load = bit(instr, 20) === 1;
|
|
1521
|
+
const rn = bits(instr, 19, 16);
|
|
1522
|
+
const rd = bits(instr, 15, 12);
|
|
1523
|
+
const offset = instr & 0xfff;
|
|
1524
|
+
let base = this.registers[rn];
|
|
1525
|
+
if (rn === PC) {
|
|
1526
|
+
base = (base + 4) >>> 0; // PC+8 relative to instruction address
|
|
1527
|
+
}
|
|
1528
|
+
const effectiveOffset = up ? offset : -offset;
|
|
1529
|
+
let address;
|
|
1530
|
+
if (pre) {
|
|
1531
|
+
address = (base + effectiveOffset) >>> 0;
|
|
1532
|
+
}
|
|
1533
|
+
else {
|
|
1534
|
+
address = base;
|
|
1535
|
+
}
|
|
1536
|
+
if (load) {
|
|
1537
|
+
if (byteMode) {
|
|
1538
|
+
this.registers[rd] = this.memory.read8(address);
|
|
1539
|
+
}
|
|
1540
|
+
else {
|
|
1541
|
+
// ARM7TDMI: unaligned word reads rotate the value
|
|
1542
|
+
const aligned = address & ~3;
|
|
1543
|
+
const rotation = (address & 3) * 8;
|
|
1544
|
+
let value = this.memory.read32(aligned);
|
|
1545
|
+
if (rotation) {
|
|
1546
|
+
value = ((value >>> rotation) | (value << (32 - rotation))) >>> 0;
|
|
1547
|
+
}
|
|
1548
|
+
this.registers[rd] = value;
|
|
1549
|
+
}
|
|
1550
|
+
if (rd === PC) {
|
|
1551
|
+
this.registers[PC] = this.registers[PC] & ~3;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
else {
|
|
1555
|
+
let value = this.registers[rd];
|
|
1556
|
+
if (rd === PC) {
|
|
1557
|
+
value = (value + 4) >>> 0; // PC+12
|
|
1558
|
+
}
|
|
1559
|
+
if (byteMode) {
|
|
1560
|
+
this.memory.write8(address, value & 0xff);
|
|
1561
|
+
}
|
|
1562
|
+
else {
|
|
1563
|
+
this.memory.write32(address & ~3, value);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
// Writeback
|
|
1567
|
+
if (pre && writeback) {
|
|
1568
|
+
this.registers[rn] = (base + effectiveOffset) >>> 0;
|
|
1569
|
+
}
|
|
1570
|
+
else if (!pre) {
|
|
1571
|
+
// Post-indexed always writes back
|
|
1572
|
+
this.registers[rn] = (base + effectiveOffset) >>> 0;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
/** ARM LDR/STR with register offset */
|
|
1576
|
+
#armSingleDataTransferReg(instr) {
|
|
1577
|
+
const pre = bit(instr, 24) === 1;
|
|
1578
|
+
const up = bit(instr, 23) === 1;
|
|
1579
|
+
const byteMode = bit(instr, 22) === 1;
|
|
1580
|
+
const writeback = bit(instr, 21) === 1;
|
|
1581
|
+
const load = bit(instr, 20) === 1;
|
|
1582
|
+
const rn = bits(instr, 19, 16);
|
|
1583
|
+
const rd = bits(instr, 15, 12);
|
|
1584
|
+
// Compute shifted register offset
|
|
1585
|
+
const rm = instr & 0xf;
|
|
1586
|
+
const shiftType = bits(instr, 6, 5);
|
|
1587
|
+
const shiftAmount = bits(instr, 11, 7);
|
|
1588
|
+
let offset;
|
|
1589
|
+
const rmVal = this.registers[rm];
|
|
1590
|
+
switch (shiftType) {
|
|
1591
|
+
case 0: // LSL
|
|
1592
|
+
offset = shiftAmount === 0 ? rmVal : (rmVal << shiftAmount) >>> 0;
|
|
1593
|
+
break;
|
|
1594
|
+
case 1: // LSR
|
|
1595
|
+
offset = shiftAmount === 0 ? 0 : rmVal >>> shiftAmount;
|
|
1596
|
+
break;
|
|
1597
|
+
case 2: // ASR
|
|
1598
|
+
offset = shiftAmount === 0 ? (isNegative(rmVal) ? 0xffffffff : 0) : (rmVal | 0) >> shiftAmount;
|
|
1599
|
+
break;
|
|
1600
|
+
case 3: // ROR/RRX
|
|
1601
|
+
if (shiftAmount === 0) {
|
|
1602
|
+
// RRX
|
|
1603
|
+
offset = ((this.getC() ? 0x80000000 : 0) | (rmVal >>> 1)) >>> 0;
|
|
1604
|
+
}
|
|
1605
|
+
else {
|
|
1606
|
+
offset = ((rmVal >>> shiftAmount) | (rmVal << (32 - shiftAmount))) >>> 0;
|
|
1607
|
+
}
|
|
1608
|
+
break;
|
|
1609
|
+
default:
|
|
1610
|
+
offset = rmVal;
|
|
1611
|
+
}
|
|
1612
|
+
let base = this.registers[rn];
|
|
1613
|
+
if (rn === PC) {
|
|
1614
|
+
base = (base + 4) >>> 0;
|
|
1615
|
+
}
|
|
1616
|
+
const effectiveOffset = up ? offset : -offset | 0;
|
|
1617
|
+
let address;
|
|
1618
|
+
if (pre) {
|
|
1619
|
+
address = (base + effectiveOffset) >>> 0;
|
|
1620
|
+
}
|
|
1621
|
+
else {
|
|
1622
|
+
address = base;
|
|
1623
|
+
}
|
|
1624
|
+
if (load) {
|
|
1625
|
+
if (byteMode) {
|
|
1626
|
+
this.registers[rd] = this.memory.read8(address);
|
|
1627
|
+
}
|
|
1628
|
+
else {
|
|
1629
|
+
// ARM7TDMI: unaligned word reads rotate the value
|
|
1630
|
+
const aligned = address & ~3;
|
|
1631
|
+
const rotation = (address & 3) * 8;
|
|
1632
|
+
let value = this.memory.read32(aligned);
|
|
1633
|
+
if (rotation) {
|
|
1634
|
+
value = ((value >>> rotation) | (value << (32 - rotation))) >>> 0;
|
|
1635
|
+
}
|
|
1636
|
+
this.registers[rd] = value;
|
|
1637
|
+
}
|
|
1638
|
+
if (rd === PC) {
|
|
1639
|
+
this.registers[PC] = this.registers[PC] & ~3;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
else {
|
|
1643
|
+
let value = this.registers[rd];
|
|
1644
|
+
if (rd === PC) {
|
|
1645
|
+
value = (value + 4) >>> 0;
|
|
1646
|
+
}
|
|
1647
|
+
if (byteMode) {
|
|
1648
|
+
this.memory.write8(address, value & 0xff);
|
|
1649
|
+
}
|
|
1650
|
+
else {
|
|
1651
|
+
this.memory.write32(address & ~3, value);
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
if (pre && writeback) {
|
|
1655
|
+
this.registers[rn] = (base + effectiveOffset) >>> 0;
|
|
1656
|
+
}
|
|
1657
|
+
else if (!pre) {
|
|
1658
|
+
this.registers[rn] = (base + effectiveOffset) >>> 0;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
// ─── ARM Halfword / Signed Transfer ──────────────────────────────
|
|
1662
|
+
/** ARM LDRH/STRH/LDRSB/LDRSH */
|
|
1663
|
+
#armHalfwordTransfer(instr) {
|
|
1664
|
+
const pre = bit(instr, 24) === 1;
|
|
1665
|
+
const up = bit(instr, 23) === 1;
|
|
1666
|
+
const immOffset = bit(instr, 22) === 1;
|
|
1667
|
+
const writeback = bit(instr, 21) === 1;
|
|
1668
|
+
const load = bit(instr, 20) === 1;
|
|
1669
|
+
const rn = bits(instr, 19, 16);
|
|
1670
|
+
const rd = bits(instr, 15, 12);
|
|
1671
|
+
const sh = bits(instr, 6, 5); // S and H bits: 01=H, 10=SB, 11=SH
|
|
1672
|
+
let offset;
|
|
1673
|
+
if (immOffset) {
|
|
1674
|
+
// Immediate: high nibble | low nibble
|
|
1675
|
+
offset = ((instr >>> 4) & 0xf0) | (instr & 0xf);
|
|
1676
|
+
}
|
|
1677
|
+
else {
|
|
1678
|
+
// Register
|
|
1679
|
+
const rm = instr & 0xf;
|
|
1680
|
+
offset = this.registers[rm];
|
|
1681
|
+
}
|
|
1682
|
+
let base = this.registers[rn];
|
|
1683
|
+
if (rn === PC) {
|
|
1684
|
+
base = (base + 4) >>> 0;
|
|
1685
|
+
}
|
|
1686
|
+
const effectiveOffset = up ? offset : -offset;
|
|
1687
|
+
let address;
|
|
1688
|
+
if (pre) {
|
|
1689
|
+
address = (base + effectiveOffset) >>> 0;
|
|
1690
|
+
}
|
|
1691
|
+
else {
|
|
1692
|
+
address = base;
|
|
1693
|
+
}
|
|
1694
|
+
if (load) {
|
|
1695
|
+
switch (sh) {
|
|
1696
|
+
case 0b01: // LDRH (unsigned halfword)
|
|
1697
|
+
// ARM7TDMI: unaligned LDRH reads aligned halfword then rotates by 8
|
|
1698
|
+
if (address & 1) {
|
|
1699
|
+
const hw = this.memory.read16(address & ~1);
|
|
1700
|
+
this.registers[rd] = ((hw >>> 8) | (hw << 24)) >>> 0;
|
|
1701
|
+
}
|
|
1702
|
+
else {
|
|
1703
|
+
this.registers[rd] = this.memory.read16(address);
|
|
1704
|
+
}
|
|
1705
|
+
break;
|
|
1706
|
+
case 0b10: // LDRSB (signed byte)
|
|
1707
|
+
this.registers[rd] = signExtend(this.memory.read8(address), 8) >>> 0;
|
|
1708
|
+
break;
|
|
1709
|
+
case 0b11: // LDRSH (signed halfword)
|
|
1710
|
+
// ARM7TDMI: unaligned LDRSH loads byte and sign-extends (like LDRSB)
|
|
1711
|
+
if (address & 1) {
|
|
1712
|
+
this.registers[rd] = signExtend(this.memory.read8(address), 8) >>> 0;
|
|
1713
|
+
}
|
|
1714
|
+
else {
|
|
1715
|
+
this.registers[rd] = signExtend(this.memory.read16(address), 16) >>> 0;
|
|
1716
|
+
}
|
|
1717
|
+
break;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
else {
|
|
1721
|
+
// STRH (only sh=01 for store) — ignores bit 0 (force halfword alignment)
|
|
1722
|
+
if (sh === 0b01) {
|
|
1723
|
+
this.memory.write16(address & ~1, this.registers[rd] & 0xffff);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
if (pre && writeback) {
|
|
1727
|
+
this.registers[rn] = (base + effectiveOffset) >>> 0;
|
|
1728
|
+
}
|
|
1729
|
+
else if (!pre) {
|
|
1730
|
+
this.registers[rn] = (base + effectiveOffset) >>> 0;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
// ─── ARM Block Data Transfer (LDM/STM) ───────────────────────────
|
|
1734
|
+
/** ARM LDM/STM */
|
|
1735
|
+
#armBlockDataTransfer(instr) {
|
|
1736
|
+
const pre = bit(instr, 24) === 1;
|
|
1737
|
+
const up = bit(instr, 23) === 1;
|
|
1738
|
+
const sBit = bit(instr, 22) === 1; // PSR & force user mode
|
|
1739
|
+
const writeback = bit(instr, 21) === 1;
|
|
1740
|
+
const load = bit(instr, 20) === 1;
|
|
1741
|
+
const rn = bits(instr, 19, 16);
|
|
1742
|
+
const rlist = instr & 0xffff;
|
|
1743
|
+
let base = this.registers[rn];
|
|
1744
|
+
// Count registers in the list
|
|
1745
|
+
let regCount = 0;
|
|
1746
|
+
for (let i = 0; i < 16; i++) {
|
|
1747
|
+
if (rlist & (1 << i)) {
|
|
1748
|
+
regCount++;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
if (regCount === 0) {
|
|
1752
|
+
// Edge case: empty register list
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
// Calculate start address based on direction
|
|
1756
|
+
let address;
|
|
1757
|
+
if (up) {
|
|
1758
|
+
address = pre ? (base + 4) >>> 0 : base;
|
|
1759
|
+
}
|
|
1760
|
+
else {
|
|
1761
|
+
// Down: start from base - regCount*4
|
|
1762
|
+
address = pre ? (base - regCount * 4) >>> 0 : (base - regCount * 4 + 4) >>> 0;
|
|
1763
|
+
}
|
|
1764
|
+
if (load) {
|
|
1765
|
+
for (let i = 0; i < 16; i++) {
|
|
1766
|
+
if (rlist & (1 << i)) {
|
|
1767
|
+
this.registers[i] = this.memory.read32(address);
|
|
1768
|
+
address = (address + 4) >>> 0;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
// If PC is in the list and S bit is set, restore CPSR from SPSR
|
|
1772
|
+
if (rlist & (1 << PC) && sBit) {
|
|
1773
|
+
const spsr = this.getSPSR();
|
|
1774
|
+
const newMode = spsr & 0x1f;
|
|
1775
|
+
const oldMode = this.getMode();
|
|
1776
|
+
if (newMode !== oldMode) {
|
|
1777
|
+
this.switchMode(newMode);
|
|
1778
|
+
}
|
|
1779
|
+
this.cpsr = spsr;
|
|
1780
|
+
}
|
|
1781
|
+
// If PC was loaded, align it
|
|
1782
|
+
if (rlist & (1 << PC)) {
|
|
1783
|
+
this.registers[PC] = this.registers[PC] & ~3;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
else {
|
|
1787
|
+
for (let i = 0; i < 16; i++) {
|
|
1788
|
+
if (rlist & (1 << i)) {
|
|
1789
|
+
let value = this.registers[i];
|
|
1790
|
+
if (i === PC) {
|
|
1791
|
+
value = (value + 4) >>> 0; // PC+12
|
|
1792
|
+
}
|
|
1793
|
+
this.memory.write32(address, value);
|
|
1794
|
+
address = (address + 4) >>> 0;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
// Writeback (for LDM: no writeback if Rn is in the register list)
|
|
1799
|
+
if (writeback && !(load && rlist & (1 << rn))) {
|
|
1800
|
+
if (up) {
|
|
1801
|
+
this.registers[rn] = (base + regCount * 4) >>> 0;
|
|
1802
|
+
}
|
|
1803
|
+
else {
|
|
1804
|
+
this.registers[rn] = (base - regCount * 4) >>> 0;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
// ─── ARM MRS/MSR ─────────────────────────────────────────────────
|
|
1809
|
+
/** MRS: Move PSR to register */
|
|
1810
|
+
#armMrs(instr) {
|
|
1811
|
+
const useSPSR = bit(instr, 22) === 1;
|
|
1812
|
+
const rd = bits(instr, 15, 12);
|
|
1813
|
+
this.registers[rd] = useSPSR ? this.getSPSR() : this.cpsr;
|
|
1814
|
+
}
|
|
1815
|
+
/** MSR (register): Move register to PSR */
|
|
1816
|
+
#armMsrReg(instr) {
|
|
1817
|
+
const useSPSR = bit(instr, 22) === 1;
|
|
1818
|
+
const rm = instr & 0xf;
|
|
1819
|
+
const value = this.registers[rm];
|
|
1820
|
+
this.#writePsr(value, useSPSR, bits(instr, 19, 16));
|
|
1821
|
+
}
|
|
1822
|
+
/** MSR (immediate): Move immediate to PSR flags */
|
|
1823
|
+
#armMsrImm(instr) {
|
|
1824
|
+
const useSPSR = bit(instr, 22) === 1;
|
|
1825
|
+
const imm8 = instr & 0xff;
|
|
1826
|
+
const rotate = ((instr >>> 8) & 0xf) * 2;
|
|
1827
|
+
let value;
|
|
1828
|
+
if (rotate === 0) {
|
|
1829
|
+
value = imm8;
|
|
1830
|
+
}
|
|
1831
|
+
else {
|
|
1832
|
+
value = ((imm8 >>> rotate) | (imm8 << (32 - rotate))) >>> 0;
|
|
1833
|
+
}
|
|
1834
|
+
this.#writePsr(value, useSPSR, bits(instr, 19, 16));
|
|
1835
|
+
}
|
|
1836
|
+
/** Write to PSR based on field mask */
|
|
1837
|
+
#writePsr(value, useSPSR, fieldMask) {
|
|
1838
|
+
let mask = 0;
|
|
1839
|
+
if (fieldMask & 0x1) {
|
|
1840
|
+
mask |= 0x000000ff;
|
|
1841
|
+
} // control
|
|
1842
|
+
if (fieldMask & 0x2) {
|
|
1843
|
+
mask |= 0x0000ff00;
|
|
1844
|
+
} // extension
|
|
1845
|
+
if (fieldMask & 0x4) {
|
|
1846
|
+
mask |= 0x00ff0000;
|
|
1847
|
+
} // status
|
|
1848
|
+
if (fieldMask & 0x8) {
|
|
1849
|
+
mask |= 0xff000000;
|
|
1850
|
+
} // flags
|
|
1851
|
+
if (useSPSR) {
|
|
1852
|
+
const current = this.getSPSR();
|
|
1853
|
+
this.setSPSR((current & ~mask) | (value & mask));
|
|
1854
|
+
}
|
|
1855
|
+
else {
|
|
1856
|
+
const oldMode = this.getMode();
|
|
1857
|
+
this.cpsr = (this.cpsr & ~mask) | (value & mask);
|
|
1858
|
+
const newMode = this.getMode();
|
|
1859
|
+
if (newMode !== oldMode) {
|
|
1860
|
+
// Need to re-bank registers
|
|
1861
|
+
this.#saveBankedRegisters(oldMode);
|
|
1862
|
+
this.#restoreBankedRegisters(newMode);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
// ─── ARM SWI ─────────────────────────────────────────────────────
|
|
1867
|
+
/** ARM Software Interrupt */
|
|
1868
|
+
#armSwi(instr) {
|
|
1869
|
+
// The SWI number encoding is platform-specific. On GBA it's bits 23-16.
|
|
1870
|
+
// We pass the full 24-bit comment field; the handler extracts what it needs.
|
|
1871
|
+
const swiNumber = (instr >>> 16) & 0xff;
|
|
1872
|
+
this.#swiHandler?.(this, swiNumber);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
//# sourceMappingURL=arm-cpu.js.map
|