esp32emu / js /soc.js
HelloSun's picture
ESP32 emulator update
8d577f5 verified
Raw History Blame Contribute Delete
39.7 kB
// ESP32 SoC model: memory map, interrupt matrix, timers, UART, SPI flash, GPIO, ...
import { XtensaCPU } from './xtensa.js';
import { XtensaCPUWasm, STATE_WORDS } from './cpu_wasm.js';
import { FlashChip, PsramChip, SpiController } from './spi.js';
import { GPIO } from './gpio.js';
import { I2S } from './i2s.js';
import { ShaEngine } from './sha.js';
import { Ulp, RtcIo, Sens } from './ulp.js';
import { Ps2Device } from './ps2.js';
const SLOW = 1;
// heap layout (bytes)
const HEAP_BASE = 0x100000; // first 1MB of linear memory belongs to the wasm module (data/stack)
const H_ROM_PRO = HEAP_BASE + 0x000000, H_ROM_APP = HEAP_BASE + 0x080000, H_RAM = HEAP_BASE + 0x100000, H_DRAM = HEAP_BASE + 0x150000;
const H_RTCF = HEAP_BASE + 0x1A2000, H_RTCS = HEAP_BASE + 0x1A4000, H_ZERO = HEAP_BASE + 0x1A6000, H_PSRAM = HEAP_BASE + 0x200000, H_FLASH = HEAP_BASE + 0x600000;
const PSRAM_SIZE = 4 * 1024 * 1024;
const FLASH_MAX = 16 * 1024 * 1024;
const HEAP_END = H_FLASH + FLASH_MAX;
const STATE_BASE = HEAP_END; // per-cpu state arrays (4KB each)
const TBL_BASE = (HEAP_END + 0x10000) & ~0xFFF; // page tables: rd0, wr0, rd1, wr1 (4MB each)
const MEM_SIZE = 64 * 1024 * 1024; // must match the wasm module's imported memory (1024 pages)
const EXT_INT_MASK = 0xDFFE373F;
const NSRC = 69;
class EventQueue {
constructor() { this.h = []; this.seq = 0; }
push(t, fn) { const e = { t, fn, id: ++this.seq, dead: false }; const h = this.h; h.push(e); let i = h.length - 1; while (i > 0) { const p = (i - 1) >> 1; if (h[p].t <= e.t) break; h[i] = h[p]; i = p; } h[i] = e; return e; }
peek() { const h = this.h; while (h.length && h[0].dead) this.pop(); return h.length ? h[0] : null; }
pop() {
const h = this.h; const top = h[0]; const last = h.pop();
if (h.length) { let i = 0; const n = h.length; for (;;) { let c = 2 * i + 1; if (c >= n) break; if (c + 1 < n && h[c + 1].t < h[c].t) c++; if (h[c].t >= last.t) break; h[i] = h[c]; i = c; } h[i] = last; }
return top;
}
}
// create an ESP32 with the WebAssembly CPU core (wasmBytes: Uint8Array of xtensa.wasm)
export async function createESP32(opts = {}) {
const memory = new WebAssembly.Memory({ initial: MEM_SIZE >> 16, maximum: MEM_SIZE >> 16 });
let soc = null;
const imports = { env: {
memory,
mmio_read: (cpu, addr, size) => soc.busRead(soc.cpus[cpu], addr >>> 0, size) | 0,
mmio_write: (cpu, addr, size, val) => soc.busWrite(soc.cpus[cpu], addr >>> 0, size, val),
rer: (cpu, addr) => soc.rer(soc.cpus[cpu], addr),
wer: (cpu, addr, val) => soc.wer(soc.cpus[cpu], addr, val),
hook: (cpu, pc, wb4) => (soc.runHook(soc.cpus[cpu], pc, wb4) ? 1 : 0),
timer_changed: () => {},
} };
const mod = opts.wasmModule || await WebAssembly.compile(opts.wasmBytes);
const inst = await WebAssembly.instantiate(mod, imports);
inst.exports.xt_init();
soc = new ESP32({ ...opts, memory, wasm: inst.exports });
return soc;
}
export class ESP32 {
constructor(opts = {}) {
this.opts = opts;
this.hooks = new Map();
this.memory = opts.memory || new WebAssembly.Memory({ initial: MEM_SIZE >> 16, maximum: MEM_SIZE >> 16 });
this.buffer = this.memory.buffer;
this.h8 = new Uint8Array(this.buffer); this.h16 = new Uint16Array(this.buffer);
this.h16s = new Int16Array(this.buffer); this.h32 = new Int32Array(this.buffer);
if (opts.wasm) {
this.engine = 'wasm';
this.cpus = [0, 1].map((i) => new XtensaCPUWasm(this, i, this.buffer, STATE_BASE + i * 0x1000, TBL_BASE + i * 0x800000, TBL_BASE + i * 0x800000 + 0x400000, opts.wasm));
} else {
this.engine = 'js';
this.cpus = [new XtensaCPU(this, 0), new XtensaCPU(this, 1)];
this.cpus.forEach((c, i) => {
c.h8 = this.h8; c.h16 = this.h16; c.h16s = this.h16s; c.h32 = this.h32;
c.rd = new Int32Array(this.buffer, TBL_BASE + i * 0x800000, 1 << 20); c.wr = new Int32Array(this.buffer, TBL_BASE + i * 0x800000 + 0x400000, 1 << 20);
c.rd.fill(SLOW); c.wr.fill(SLOW);
});
}
this.events = new EventQueue();
this.timeNs = 0;
this.cpuHz = 40e6; this.apbHz = 40e6; this.cpi = opts.cpi || 1;
this.srcLevel = new Uint8Array(NSRC);
this.srcLevel1 = new Uint8Array(NSRC); // per-cpu variants (cpu1) when srcPerCpu
this.srcPerCpu = new Uint8Array(NSRC);
this.intMap = [new Uint8Array(NSRC).fill(6), new Uint8Array(NSRC).fill(6)];
this.uartTx = [[], [], []];
this.onUart = null;
this.log = opts.log || ((s) => console.log(s));
this.dport = new Int32Array(0x1000);
this.mmu = [new Uint16Array(256), new Uint16Array(256)];
this.cacheCtrl = [0, 0]; this.cacheCtrl1 = [0, 0];
this.flashSize = 0;
this.misc = new Map();
this.sha = new ShaEngine(this);
this.rtcSlow32 = new Int32Array(this.buffer, H_RTCS, 2048);
this.gpio = new GPIO(this);
this.rtcio = new RtcIo(this); this.sens = new Sens(this); this.ulp = new Ulp(this);
this.i2s = [new I2S(this, 0), new I2S(this, 1)];
this.spi = [null, new SpiController(this, 1), new SpiController(this, 2), new SpiController(this, 3)];
this.rtc = new RtcCntl(this);
this.timg = [new TimerGroup(this, 0), new TimerGroup(this, 1)];
this.uart = [new Uart(this, 0), new Uart(this, 1), new Uart(this, 2)];
this.efuse = new Efuse(this);
this.frc = [new Frc(this, 0), new Frc(this, 1)];
this.devs = new Map();
const reg = (base, dev, pages = 1) => { for (let i = 0; i < pages; i++) this.devs.set((base >>> 12) + i, { dev, off: i << 12 }); };
reg(0x3ff40000, this.uart[0]); reg(0x3ff50000, this.uart[1]); reg(0x3ff6E000, this.uart[2]);
reg(0x3ff42000, this.spi[1]); reg(0x3ff64000, this.spi[2]); reg(0x3ff65000, this.spi[3]);
reg(0x3ff44000, this.gpio); reg(0x3ff49000, this.gpio.iomux);
reg(0x3ff47000, new FrcPage(this));
reg(0x3ff48000, new RtcPage(this)); reg(0x3ff5A000, this.efuse);
reg(0x3ff5F000, this.timg[0]); reg(0x3ff60000, this.timg[1]);
reg(0x3ff4F000, this.i2s[0]); reg(0x3ff6D000, this.i2s[1]);
reg(0x3ff66000, new Syscon(this)); reg(0x3ff75000, new Wdev(this));
reg(0x3ff43000, new Spi0(this));
this.ps2 = { kbd: new Ps2Device(this, 33, 32, 'keyboard'), mouse: new Ps2Device(this, 26, 27, 'mouse') };
this.appRunning = false;
this.appBoot = 0; this.appResetState = true; this.appClk = false; this.appStall = false;
this.rom = { pro: null, app: null };
this.bpHit = null;
}
// ---------------- ROM HLE hooks ----------------
addRomHook(addr, fn) {
this.hooks.set(addr, fn);
for (const base of [H_ROM_PRO, H_ROM_APP]) { const o = base + (addr - 0x40000000); this.h8[o] = 0; this.h8[o + 1] = 0; this.h8[o + 2] = 0; }
}
runHook(cpu, pc, wb4) {
const fn = this.hooks.get(pc >>> 0);
if (!fn) return false;
const ar = cpu.ar, ci = (cpu.ps >> 16) & 3, b = wb4 + 4 * ci;
const args = []; for (let k = 2; k < 8; k++) args.push(ar[(b + k) & 63]);
const ra = ar[b & 63];
const ret = fn.call(this, ...args);
ar[(b + 2) & 63] = ret | 0;
cpu.hookPc = ci === 0 ? ra : ((ra & 0x3FFFFFFF) | (pc & 0xC0000000)) | 0;
return true;
}
installRomHooks() {
const i2c = (this.i2c = new Map());
const upd = () => {
const g = (r) => i2c.get((0x6D << 8) | r) | 0;
const sdm2 = g(7) & 0x3F, sdm1 = g(8) & 0xFF, sdm0 = g(9) & 0xFF, od = g(4) & 0x1F;
this.apllHz = 40e6 * (4 + sdm2 + sdm1 / 256 + sdm0 / 65536) / (2 * (od + 2));
};
const wmask = (block, host, reg, msb, lsb, val) => {
const key = (block << 8) | reg; const old = i2c.get(key) | 0;
const mask = (((1 << (msb - lsb + 1)) - 1) << lsb);
i2c.set(key, (old & ~mask) | ((val << lsb) & mask)); if (block === 0x6D) upd(); return 0;
};
this.apllHz = 0;
this.addRomHook(0x400041fc, function (block, host, reg, msb, lsb, val) { return wmask(block & 0xFF, host, reg & 0xFF, msb, lsb, val); });
this.addRomHook(0x400041a4, function (block, host, reg, val) { i2c.set(((block & 0xFF) << 8) | (reg & 0xFF), val & 0xFF); if ((block & 0xFF) === 0x6D) upd(); return 0; });
const rd = (block, reg) => { const v = i2c.get(((block & 0xFF) << 8) | (reg & 0xFF)) | 0; return (block & 0xFF) === 0x6D && (reg & 0xFF) === 3 ? v | 0x80 : v; }; // APLL: calibration done
this.addRomHook(0x40004148, function (block, host, reg) { return rd(block, reg); });
this.addRomHook(0x400041c0, function (block, host, reg, msb, lsb) { return (rd(block, reg) >> lsb) & ((1 << (msb - lsb + 1)) - 1); });
}
// ---------------- time / events ----------------
schedule(atNs, fn) { return this.events.push(atNs, fn); }
scheduleIn(dtNs, fn) { return this.events.push(this.nowNs + dtNs, fn); }
cancel(e) { if (e) e.dead = true; }
get nowNs() { return this.ulpNow !== null && this.ulpNow !== undefined ? this.ulpNow : this.timeNs + this.curSliceNs(); }
curSliceNs() { return this.inSlice ? ((this.cpus[this.curCpu].sliceLen - this.cpus[this.curCpu].n) * this.cpi * 1e9 / this.cpuHz) : 0; }
setClocks(cpuHz, apbHz) {
this.cpuHz = cpuHz; this.apbHz = apbHz;
for (const t of this.timg) t.clockChanged();
for (const f of this.frc) f.clockChanged();
}
// ---------------- interrupts ----------------
setIrq(src, level) {
level = level ? 1 : 0;
if (this.srcLevel[src] === level) return;
this.srcLevel[src] = level;
this.updateIrq();
}
setIrqCpu(cpu, src, level) { // per-cpu sources (GPIO)
this.srcPerCpu[src] = 1;
if (cpu === 0) this.srcLevel[src] = level ? 1 : 0; else this.srcLevel1[src] = level ? 1 : 0;
this.updateIrq();
}
updateIrq() {
this.irqCores = 0;
for (let c = 0; c < 2; c++) {
let pins = 0;
const map = this.intMap[c], lv = c === 1 && this.srcPerCpu ? null : this.srcLevel;
for (let s = 0; s < NSRC; s++) {
const l = (c === 1 && this.srcPerCpu[s]) ? this.srcLevel1[s] : this.srcLevel[s];
if (l) pins |= (1 << map[s]);
}
pins &= EXT_INT_MASK;
const cpu = this.cpus[c];
if (cpu.intPins !== pins) {
cpu.intPins = pins; cpu.chkInt = true; if (cpu.halted) cpu.checkWake();
// model simultaneous interrupt entry on the cores that are not currently executing (they would sample the line in parallel on real silicon)
if (!(this.inSlice && c === this.curCpu)) { if (cpu.pendingLevel() > 0) this.irqCores++; cpu.deliverNow(); }
}
}
if (this.irqCores >= 2) this.lockstep = 1500; // both cores entered ISRs at the same instant: interleave finely so they race like real silicon
}
cpuTimerChanged(cpu) { /* picked up at next slice */ }
// ---------------- memory ----------------
mapRegion(c, vaddr, heapOff, size, writable = true) {
const rd = c.rd, wr = c.wr;
for (let a = 0; a < size; a += 4096) {
const p = ((vaddr + a) >>> 12);
const d = (heapOff + a - (vaddr + a)) | 0;
rd[p] = d; wr[p] = writable ? d : SLOW;
}
}
buildMaps() {
for (let i = 0; i < 2; i++) {
const c = this.cpus[i];
c.rd.fill(SLOW); c.wr.fill(SLOW);
this.mapRegion(c, 0x40000000, i === 0 ? H_ROM_PRO : H_ROM_APP, 0x70000, false);
this.mapRegion(c, 0x3FF90000, (i === 0 ? H_ROM_PRO : H_ROM_APP) + 0x60000, 0x10000, false);
this.mapRegion(c, 0x40070000, H_RAM, 0x50000);
this.mapRegion(c, 0x3FFAE000, H_DRAM, 0x52000);
this.mapRegion(c, 0x50000000, H_RTCS, 0x2000);
if (i === 0) { this.mapRegion(c, 0x400C0000, H_RTCF, 0x2000); this.mapRegion(c, 0x3FF80000, H_RTCF, 0x2000); }
if (this.psram) this.mapRegion(c, 0x3F800000, H_PSRAM, PSRAM_SIZE);
this.applyCache(i);
}
}
applyCache(i) {
const c = this.cpus[i], tbl = this.mmu[i];
const flashPages = this.flashSize >>> 16;
const map = (vbase, idxBase, startPage) => {
for (let k = startPage; k < 64; k++) {
const e = tbl[idxBase + k];
const v = vbase + k * 0x10000;
for (let q = 0; q < 16; q++) {
const p = (v + q * 4096) >>> 12;
if (c.rd[p] !== SLOW && c._cacheMapped && !c._cacheMapped[p]) continue; // static mapping wins
if (!c._cacheMapped) c._cacheMapped = new Uint8Array(1 << 20);
if ((e & 0x100) || (e & 0xFF) >= flashPages || flashPages === 0) { c.rd[p] = SLOW; c.wr[p] = SLOW; c._cacheMapped[p] = 0; }
else { const d = (H_FLASH + (e & 0xFF) * 0x10000 + q * 4096 - (v + q * 4096)) | 0; c.rd[p] = d; c.wr[p] = SLOW; c._cacheMapped[p] = 1; }
}
}
};
if (!c._cacheMapped) c._cacheMapped = new Uint8Array(1 << 20);
map(0x3F400000, 0, 0);
// IRAM0: pages below 0x400C2000 are static (rom/ram); page 12 partially
map(0x40000000, 64, 12);
map(0x40400000, 128, 0);
map(0x40800000, 192, 0);
}
// ---------------- bus slow path ----------------
busRead(cpu, addr, size) {
this.curCpu = cpu.id;
let a = addr;
if (a >= 0x60000000 && a < 0x60100000) a = (a - 0x60000000 + 0x3ff40000) >>> 0;
const page = a >>> 12;
if (page === 0x3ff03) return this.shaRead(a, size);
if (page >= 0x3ff00 && page < 0x3ff14) return this.dportRead(cpu, a, size);
const d = this.devs.get(page);
if (d) {
const aligned = a & ~3;
const v = d.dev.read(d.off + (aligned & 0xFFF), cpu) | 0;
if (size === 4) return v;
const sh = (a & 3) * 8;
return size === 2 ? (v >>> sh) & 0xFFFF : (v >>> sh) & 0xFF;
}
if (a >= 0x3F400000 && a < 0x3FC00000) return 0xbaadbaad >> ((a & 3) * 8) & (size === 4 ? -1 : size === 2 ? 0xFFFF : 0xFF);
return 0;
}
busWrite(cpu, addr, size, val) {
this.curCpu = cpu.id;
let a = addr;
if (a >= 0x60000000 && a < 0x60100000) a = (a - 0x60000000 + 0x3ff40000) >>> 0;
const page = a >>> 12;
if (page === 0x3ff03) { this.sha.write(a & 0xFFF, val | 0); return; }
if (page >= 0x3ff00 && page < 0x3ff14) { this.dportWrite(cpu, a, size, val); return; }
const d = this.devs.get(page);
if (d) {
const off = d.off + (a & 0xFFC);
if (size === 4) { d.dev.write(off, val | 0, cpu); return; }
const sh = (a & 3) * 8;
const mask = size === 2 ? 0xFFFF : 0xFF;
// sub-word write: for FIFO-like registers (UART FIFO) write byte directly
if (d.dev.writeSub) { d.dev.writeSub(off, a & 3, size, val, cpu); return; }
const old = d.dev.read(off, cpu) | 0;
d.dev.write(off, (old & ~(mask << sh)) | ((val & mask) << sh), cpu);
}
}
shaRead(a, size) { const v = this.sha.read(a & 0xFFF); if (size === 4) return v; const sh = (a & 3) * 8; return size === 2 ? (v >>> sh) & 0xFFFF : (v >>> sh) & 0xFF; }
rer() { return 0; }
wer() {}
// ---------------- DPORT ----------------
dportRead(cpu, a, size) {
const off = a & 0x3FFF;
let v = 0;
if (a >= 0x3ff10000 && a < 0x3ff10400) v = this.mmu[0][(off - 0x0) / 4 & 0xFF] ;
else if (a >= 0x3ff12000 && a < 0x3ff12400) v = this.mmu[1][((a - 0x3ff12000) / 4) & 0xFF];
else if (a >= 0x3ff10000 && a < 0x3ff11000) v = this.mmu[0][((a - 0x3ff10000) / 4) & 0xFF];
else if (a >= 0x3ff12000 && a < 0x3ff13000) v = this.mmu[1][((a - 0x3ff12000) / 4) & 0xFF];
else {
const o = a - 0x3ff00000;
switch (o) {
case 0x2c: v = this.appResetState ? 1 : 0; break;
case 0x30: v = this.appClk ? 1 : 0; break;
case 0x34: v = this.appStall ? 1 : 0; break;
case 0x38: v = this.appBoot; break;
case 0x3c: v = this.cpuPerSel | 0; break;
case 0x40: v = this.cacheCtrl[0]; break;
case 0x44: v = this.cacheCtrl1[0]; break;
case 0x58: v = this.cacheCtrl[1]; break;
case 0x5c: v = this.cacheCtrl1[1]; break;
case 0x3f0: case 0x418: v = 1 << 7; break;
default:
if (o >= 0x104 && o < 0x104 + NSRC * 4) v = this.intMap[0][(o - 0x104) >> 2];
else if (o >= 0x218 && o < 0x218 + NSRC * 4) v = this.intMap[1][(o - 0x218) >> 2];
else v = this.dport[(o >> 2) & 0x3FF] ;
if (o >= 0xdc && o < 0xec) v = this.dport[(o >> 2)];
else if (!(o >= 0x104 && o < 0x104 + NSRC * 4) && !(o >= 0x218 && o < 0x218 + NSRC * 4)) v = 0;
}
}
if (size === 4) return v | 0;
const sh = (a & 3) * 8; return size === 2 ? (v >>> sh) & 0xFFFF : (v >>> sh) & 0xFF;
}
dportWrite(cpu, a, size, val) {
if (size !== 4) { const old = this.dportRead(cpu, a & ~3, 4); const sh = (a & 3) * 8; const m = (size === 2 ? 0xFFFF : 0xFF) << sh; val = (old & ~m) | ((val << sh) & m); a &= ~3; }
if (a >= 0x3ff10000 && a < 0x3ff11000) { const i = ((a - 0x3ff10000) / 4) & 0xFF; if (this.mmu[0][i] !== (val & 0x1FF)) { this.mmu[0][i] = val & 0x1FF; this.applyCache(0); } return; }
if (a >= 0x3ff12000 && a < 0x3ff13000) { const i = ((a - 0x3ff12000) / 4) & 0xFF; if (this.mmu[1][i] !== (val & 0x1FF)) { this.mmu[1][i] = val & 0x1FF; this.applyCache(1); } return; }
const o = a - 0x3ff00000;
switch (o) {
case 0x2c: { const old = this.appResetState; this.appResetState = (val & 1) !== 0; if (old && !this.appResetState) this.appCpuRelease(); this.updateAppRun(); break; }
case 0x30: this.appClk = (val & 1) !== 0; this.updateAppRun(); break;
case 0x34: this.appStall = (val & 1) !== 0; this.updateAppRun(); break;
case 0x38: this.appBoot = val; break;
case 0x3c: this.cpuPerSel = val & 3; this.clockUpdate(); break;
case 0x40: if (val & 0x10) { val |= 0x20; val &= ~0x10; } this.cacheCtrl[0] = val; break;
case 0x44: this.cacheCtrl1[0] = val; break;
case 0x58: if (val & 0x10) { val |= 0x20; val &= ~0x10; } this.cacheCtrl[1] = val; break;
case 0x5c: this.cacheCtrl1[1] = val; break;
default:
if (o >= 0xdc && o < 0xec) { this.dport[o >> 2] = val & 1; this.setIrq(24 + ((o - 0xdc) >> 2), val & 1); }
else if (o >= 0x104 && o < 0x104 + NSRC * 4) { this.intMap[0][(o - 0x104) >> 2] = val & 31; this.updateIrq(); }
else if (o >= 0x218 && o < 0x218 + NSRC * 4) { this.intMap[1][(o - 0x218) >> 2] = val & 31; this.updateIrq(); }
}
}
appCpuRelease() {
const c = this.cpus[1]; c.reset(); c.cyc = this.cpus[0].cyc; this.appPending = true;
}
updateAppRun() {
const c = this.cpus[1];
c.stalled = this.appResetState || !this.appClk || this.appStall || this.rtc.appStall;
}
clockUpdate() {
const r = this.rtc;
let apb, cpu;
if (r.socClk === 1) { apb = 80e6; cpu = [80e6, 160e6, 240e6][this.cpuPerSel | 0] || 80e6; }
else { apb = 40e6; cpu = 40e6; }
this.setClocks(cpu, apb);
}
// ---------------- system control ----------------
setFlash(data) {
let size = 2 * 1024 * 1024;
for (const s of [2, 4, 8, 16]) if (data.length <= s * 1024 * 1024) { size = s * 1024 * 1024; break; }
if (data.length > FLASH_MAX) throw new Error('flash image too large');
this.flashSize = size;
this.h8.fill(0xFF, H_FLASH, H_FLASH + FLASH_MAX);
this.h8.set(data, H_FLASH);
this.flashChip = new FlashChip(this.h8.subarray(H_FLASH, H_FLASH + size), size);
this.flashDirty = false;
}
getFlash() { return this.h8.subarray(H_FLASH, H_FLASH + this.flashSize); }
loadRoms(pro, app) {
this.h8.set(pro.subarray(0, 0x70000), H_ROM_PRO);
this.h8.set(app.subarray(0, 0x70000), H_ROM_APP);
this.installRomHooks();
}
enablePsram(on = true) {
this.psram = on;
this.psramChip = on ? new PsramChip(this.h8.subarray(H_PSRAM, H_PSRAM + PSRAM_SIZE), 4) : null;
}
reset(kind = 'power') {
// power-on reset (RAM contents preserved, except at power on where we clear)
if (kind === 'power') { this.h8.fill(0, H_RAM, H_RTCS + 0x2000); this.h8.fill(0, H_PSRAM, H_PSRAM + PSRAM_SIZE); }
this.timeNs = this.timeNs || 0;
this.events = new EventQueue();
this.srcLevel.fill(0); this.srcLevel1.fill(0); this.intMap[0].fill(6); this.intMap[1].fill(6);
this.mmu[0].fill(0); this.mmu[1].fill(0); this.cacheCtrl = [0, 0]; this.cacheCtrl1 = [0, 0];
this.dport.fill(0); this.cpuPerSel = 0; this.appResetState = true; this.appClk = false; this.appStall = false; this.appBoot = 0;
this.rtc.reset(kind); this.gpio.reset(); this.efuse.reset(); this.rtcio.reset(); this.sens.reset(); this.ulp.reset();
for (const d of Object.values(this.ps2)) { d.reset(); d.gpio.setPullup(d.clk, true); d.gpio.setPullup(d.dat, true); }
for (const t of this.timg) t.reset(); for (const u of this.uart) u.reset();
for (const f of this.frc) f.reset();
for (const s of this.spi) if (s) s.reset();
for (const i of this.i2s) i.reset();
this.setClocks(40e6, 40e6);
this.buildMaps();
for (const c of this.cpus) c.reset();
this.cpus[1].stalled = true;
this.pendingReset = null;
}
requestReset(kind) { this.pendingReset = kind; }
// ---------------- run loop ----------------
// run for `maxInstr` instructions on cpu0 worth of time (both cores); returns when done
run(maxCycles) {
const MAXSLICE = this.opts.slice || 256;
let done = 0;
const cpus = this.cpus;
while (done < maxCycles) {
if (this.pendingReset) { const k = this.pendingReset; this.reset(k === 'power' ? 'power' : 'soft'); }
// compute slice length in instructions
let slice = this.lockstep > 0 ? 2 : MAXSLICE;
if (this.lockstep > 0) this.lockstep -= 2;
const ev = this.events.peek();
const nsPerInstr = this.cpi * 1e9 / this.cpuHz;
if (ev) { const dn = (ev.t - this.timeNs) / nsPerInstr; if (dn < slice) slice = Math.max(1, Math.ceil(dn)); }
let allIdle = true;
for (let i = 0; i < 2; i++) {
const c = cpus[i];
// cpu timers
let s = slice;
const cc = (c.cyc * this.cpi + c.ccountOff) >>> 0;
c.cyc = c.cyc; // keep
for (let k = 0; k < 3; k++) {
const bit = [6, 15, 16][k];
if (!((c.intSet >>> bit) & 1)) {
const dt = (c.ccompare[k] - cc) >>> 0;
const di = dt / this.cpi;
if (di < s) s = Math.max(1, Math.ceil(di));
}
}
if (s < slice) slice = s;
}
for (let i = 0; i < 2; i++) {
const c = cpus[i];
this.curCpu = i; this.inSlice = true;
const wasHalted = c.halted || c.stalled;
c.run(slice);
this.inSlice = false;
if (!(c.halted || c.stalled)) allIdle = false;
if (i === 1 && this.appPending) { this.appPending = false; }
// timer compare checks
const cc = (c.cyc * this.cpi + c.ccountOff) >>> 0;
for (let k = 0; k < 3; k++) {
const bit = [6, 15, 16][k];
// match if ccompare is within (cc - slice*cpi, cc]
const back = (cc - c.ccompare[k]) >>> 0;
if (back <= slice * this.cpi + 1 && !((c.intSet >>> bit) & 1)) {
if (this.tcheck(c, k, cc)) { c.raiseIntSet(1 << bit); }
}
}
}
this.timeNs += slice * this.cpi * 1e9 / this.cpuHz;
done += slice;
if (this.ulp.running) this.ulp.advance(this.timeNs);
// events
let ev2;
while ((ev2 = this.events.peek()) && ev2.t <= this.timeNs) { this.events.pop(); ev2.fn(); }
if (allIdle) {
// all cores asleep: skip to next event
const nx = this.events.peek();
let target = null;
// cpu timers
for (const c of cpus) {
if (c.stalled) continue;
const cc = (c.cyc * this.cpi + c.ccountOff) >>> 0;
for (let k = 0; k < 3; k++) {
const bit = [6, 15, 16][k];
if ((c.intenable >>> bit) & 1 && !((c.intSet >>> bit) & 1)) {
const dt = (c.ccompare[k] - cc) >>> 0;
const tn = this.timeNs + (dt / this.cpi) * (this.cpi * 1e9 / this.cpuHz);
if (target === null || tn < target) target = tn;
}
}
}
if (nx && (target === null || nx.t < target)) target = nx.t;
if (target === null) { this.idleForever = true; return done; }
if (this.idleLimit !== undefined && target > this.idleLimit) { this.idleBlocked = true; return done; }
const dt = target - this.timeNs;
if (dt > 0) {
const cycles = dt * this.cpuHz / 1e9 / this.cpi;
for (const c of cpus) c.cyc += cycles;
this.timeNs = target;
}
}
}
return done;
}
tcheck(c, k, cc) {
return ((cc - c.ccompare[k]) >>> 0) < 0x40000000;
}
}
// -------------- simple peripherals ---------------
class Uart {
constructor(soc, n) { this.soc = soc; this.n = n; this.reg = new Int32Array(0x100); this.reset(); }
reset() {
this.reg.fill(0); this.reg[0x14 >> 2] = 0x2B6; this.rx = []; this.baud = 115200;
this.txThr = 0; this.rxThr = 0; this.toutEna = false; this.toutThr = 0; this.rawExtra = 0;
this.soc.setIrq(34 + this.n, 0);
}
get src() { return 34 + this.n; }
updateIrq() {
const r = this.reg;
let raw = r[1] & ~((1 << 0) | (1 << 1) | (1 << 14) | (1 << 8));
raw |= (this.rx.length >= this.rxThr && this.rxThr >= 0 && this.rx.length > 0 ? 1 : 0);
raw |= 2; raw |= (1 << 14); // tx empty & tx done (fifo drained immediately)
if (this.rxTimeout) raw |= (1 << 8);
r[1] = raw;
r[2] = raw & r[3];
this.soc.setIrq(this.src, r[2] !== 0);
}
rxPush(bytes) {
for (const b of bytes) if (this.rx.length < 128) this.rx.push(b);
this.rxTimeout = true;
this.updateIrq();
}
read(off) {
const r = this.reg;
switch (off) {
case 0: { if (!this.rx.length) return 0xEE; const b = this.rx.shift(); if (!this.rx.length) this.rxTimeout = false; this.updateIrq(); return b; }
case 0x1C: return this.rx.length | 0;
case 0x28: case 0x2C: return 337;
case 0x58: return (1 << 3) | (1 << 7);
case 0x60: return (this.rx.length === 128 ? 0 : this.rx.length) << 13;
case 0x78: return 0x15122500;
default: return r[(off >> 2) & 0xFF];
}
}
write(off, v) {
const r = this.reg;
switch (off) {
case 0: if (this.soc.onUart) this.soc.onUart(this.n, v & 0xFF); break;
case 0x10: r[4] = v; r[2] &= ~v; if (v & (1 << 8)) this.rxTimeout = false; break;
case 0xC: r[3] = v; break;
case 0x14: { r[5] = v; const div = ((v & 0xFFFFF) << 4) + ((v >> 20) & 15); this.baud = div ? Math.floor((40000000 * 16) / div) : 115200; break; }
case 0x18: r[6] = v; r[0x30 >> 2] = (v & 1) ? 0x3FF : 0; break;
case 4: case 8: case 0x1C: break;
case 0x24: r[9] = v; this.txThr = (v >> 8) & 0x7F; this.rxThr = v & 0x7F; this.toutEna = !!(v >>> 31); this.toutThr = (v >> 24) & 0x7F; break;
default: r[(off >> 2) & 0xFF] = v;
}
this.updateIrq();
}
writeSub(off, byteOff, size, val) { if (off === 0) this.write(0, val & 0xFF); else this.write(off, val); }
}
class Syscon {
constructor(soc) { this.soc = soc; }
read(off) { if (off === 0x78) return 0x51454d55; if (off === 0x7c) return 0x16042000 | 0x80000000; return 0; }
write() {}
}
class Wdev {
constructor(soc) { this.soc = soc; this.s = 0x1234567 >>> 0; }
read(off) { if (off === 0x144) { let x = this.s; x ^= x << 13; x >>>= 0; x ^= x >>> 17; x ^= x << 5; x >>>= 0; this.s = x; return x | 0; } return 0; }
write() {}
}
class Spi0 {
constructor(soc) { this.soc = soc; this.r = new Int32Array(0x100); }
read(off) { return this.r[(off >> 2) & 0xFF]; }
write(off, v) { this.r[(off >> 2) & 0xFF] = v; }
}
class Efuse {
constructor(soc) { this.soc = soc; this.reset(); }
reset() {
this.rd = new Int32Array(0x80); this.cmd = 0; this.conf = 0;
this.rd[3] |= (1 << 15); this.rd[5] |= (1 << 20); // chip rev 3
this.rd[1] = 0x12345678; this.rd[2] = 0x00A4CC0D & 0xFFFF | 0x00A40000; // fake MAC
this.intRaw = 0; this.intEna = 0; this.wr = new Int32Array(0x80);
}
read(off) {
if (off < 0x98) return this.rd[off >> 2];
if (off === 0xf8) return 0; if (off === 0x100) return 0;
if (off === 0x104) return this.cmd; if (off === 0x108) return this.intRaw; if (off === 0x1fc) return 0x16042600;
return 0;
}
write(off, v) {
if (off === 0x104) { if (v & 3) { this.cmd = v & 3; this.soc.scheduleIn(2000, () => { this.intRaw |= this.cmd; this.cmd = 0; }); } }
else if (off === 0x114) this.intRaw &= ~v;
else if (off === 0x110) this.intEna = v;
}
}
class Frc {
constructor(soc, n) { this.soc = soc; this.n = n; this.mask = n === 0 ? 0x7FFFFF : 0xFFFFFFFF; this.reset(); }
reset() { this.load = 0; this.alarm = 0; this.countBase = 0; this.nsBase = 0; this.en = false; this.level = false; this.auto = false; this.presc = 1; this.status = false; this.ev = null; this.soc.setIrq(56 + this.n, 0); }
clockChanged() { if (this.en) { this.countBase = this.count(); this.nsBase = this.soc.nowNs; this.arm(); } }
count() {
if (!this.en) return this.countBase;
const ticks = Math.floor((this.soc.nowNs - this.nsBase) * this.soc.apbHz / 1e9 / this.presc);
return (this.countBase + ticks) % (this.mask + 1);
}
arm() {
this.soc.cancel(this.ev); this.ev = null;
if (!this.en) return;
let cur = this.count(), al = this.alarm & this.mask;
let to = al - cur; if (to <= 0) to += this.mask + 1;
const ns = to * this.presc * 1e9 / this.soc.apbHz;
this.ev = this.soc.scheduleIn(ns, () => this.fire());
}
fire() {
this.ev = null;
if (this.level) { this.status = true; this.soc.setIrq(56 + this.n, 1); }
if (this.auto) { this.countBase = this.count(); this.nsBase = this.soc.nowNs; this.arm(); }
}
read(off) {
switch (off) {
case 0x00: return this.load; case 0x04: return this.count() | 0;
case 0x08: return (this.level ? 1 : 0) | ((this.presc === 1 ? 0 : this.presc === 16 ? 2 : 4) << 1) | (this.auto ? 0x40 : 0) | (this.en ? 0x80 : 0) | (this.status ? 0x100 : 0);
case 0x10: return this.alarm;
}
return 0;
}
write(off, v) {
switch (off) {
case 0x0c: if (v & 1) { this.status = false; this.soc.setIrq(56 + this.n, 0); } break;
case 0x00: this.load = v; this.nsBase = this.soc.nowNs; this.countBase = v & this.mask; this.arm(); break;
case 0x08: {
this.countBase = this.count(); this.nsBase = this.soc.nowNs;
this.level = !!(v & 1); const p = (v >> 1) & 7; this.presc = p === 2 ? 16 : p === 4 ? 256 : 1;
this.auto = !!(v & 0x40); this.en = !!(v & 0x80); this.arm(); break;
}
case 0x10: this.alarm = v; this.arm(); break;
}
}
}
class FrcPage {
constructor(soc) { this.soc = soc; }
read(off) { return off < 0x20 ? this.soc.frc[0].read(off) : off < 0x40 ? this.soc.frc[1].read(off - 0x20) : 0; }
write(off, v) { if (off < 0x20) this.soc.frc[0].write(off, v); else if (off < 0x40) this.soc.frc[1].write(off - 0x20, v); }
}
class RtcCntl {
constructor(soc) { this.soc = soc; this.store = new Int32Array(8); this.reset('power'); }
reset(kind) {
this.socClk = 0; this.fastClk = 1; this.slowClk = 0; this.options0 = 0; this.timeBaseNs = this.soc.timeNs;
this.swStall = 0; this.appStall = false; this.resetCause = kind === 'power' ? 1 : 3; this.statVec = 3 << 12;
this.timeReg = 0; this.clkConf = 0; this.intEna = 0; this.intRaw = 0;
this.regs = new Int32Array(0x200);
if (kind === 'power') this.store.fill(0);
}
read(off) {
switch (off) {
case 0: return this.options0;
case 0x0c: return 1 << 30;
case 0x10: return this.timeReg % 4294967296 | 0;
case 0x14: return Math.floor(this.timeReg / 4294967296) | 0;
case 0x34: return (this.resetCause & 0x3f) | ((this.resetCause & 0x3f) << 6) | this.statVec;
case 0x4c: case 0x50: case 0x54: case 0x58: return this.store[(off - 0x4c) >> 2];
case 0x3c: return this.intEna; case 0x40: return this.intRaw; case 0x44: return this.intRaw & this.intEna;
case 0x70: return this.clkConf;
case 0xac: return this.swStall;
case 0xb0: case 0xb4: case 0xb8: case 0xbc: return this.store[4 + ((off - 0xb0) >> 2)];
case 0x13c: return 0x16042000;
default: return 0;
}
}
write(off, v) {
const soc = this.soc;
switch (off) {
case 0:
if (v & (1 << 31)) { this.resetCause = 3; soc.requestReset('soft'); v &= ~(1 << 31); }
if (v & (1 << 5)) { this.resetCause = 12; soc.requestReset('soft'); v &= ~(1 << 5); }
if (v & (1 << 4)) { v &= ~(1 << 4); soc.cpus[1].reset(); soc.appResetState = true; soc.updateAppRun(); }
this.options0 = v; this.updateStall(); break;
case 0x0c: if (v & (1 << 31)) this.timeReg = Math.floor(((soc.nowNs - this.timeBaseNs) / 1e9) * this.slowHz()); break;
case 0x34: this.statVec = v & (3 << 12); break;
case 0x3c: this.intEna = v; this.updateIrq(); break;
case 0x48: this.intRaw &= ~v; this.updateIrq(); break;
case 0x4c: case 0x50: case 0x54: case 0x58: this.store[(off - 0x4c) >> 2] = v; break;
case 0x70: this.clkConf = v; this.socClk = (v >> 27) & 3; this.slowClk = (v >> 30) & 3; soc.clockUpdate(); break;
case 0xac: this.swStall = v; this.updateStall(); break;
case 0xb0: case 0xb4: case 0xb8: case 0xbc: this.store[4 + ((off - 0xb0) >> 2)] = v; break;
}
}
ulpWake() { this.intRaw |= (1 << 5); this.updateIrq(); }
updateIrq() { this.soc.setIrq(46, (this.intRaw & this.intEna) !== 0); }
slowHz() { return [150000, 32768, 31250][this.slowClk] || 150000; }
updateStall() {
const m = 0x86;
const proc = (((this.swStall >>> 26) & 0x3f) << 2) | ((this.options0 >> 2) & 3);
const app = (((this.swStall >>> 20) & 0x3f) << 2) | (this.options0 & 3);
this.soc.cpus[0].stalled = proc === m;
this.appStall = app === m; this.soc.updateAppRun();
}
}
class RtcPage {
constructor(soc) { this.soc = soc; }
read(off, cpu) { if (off < 0x400) return this.soc.rtc.read(off); if (off < 0x800) return this.soc.rtcio.read(off - 0x400); if (off < 0xC00) return this.soc.sens.read(off - 0x800); return 0; }
write(off, v, cpu) { if (off < 0x400) this.soc.rtc.write(off, v); else if (off < 0x800) this.soc.rtcio.write(off - 0x400, v); else if (off < 0xC00) this.soc.sens.write(off - 0x800, v); }
}
class TimerGroup {
constructor(soc, n) { this.soc = soc; this.n = n; this.t = [new GTimer(this, 0), new GTimer(this, 1), new GTimer(this, 3)]; this.reset(); }
reset() {
this.intEna = 0; this.intRaw = 0; this.wdt = { cfg0: 0x0004c000, cfg1: 0x00010000, to: [0x018cba80, 0x07ffffff, 0xfffff, 0xfffff], prot: 0x50D83AA1 };
this.calMax = 1; this.calClkSel = 0; this.calStart = 1; this.calRdy = false; this.calValue = 0;
for (const t of this.t) t.reset();
this.doCal();
this.updateIrq();
}
clockChanged() { for (const t of this.t) t.rebase(); }
doCal() {
const f = this.calClkSel === 0 ? this.soc.rtc.slowHz() : this.calClkSel === 1 ? 32768 : 8000000 / 256;
this.calValue = Math.floor(40000000 * this.calMax / f); this.calRdy = true;
}
updateIrq() {
const st = this.intEna & this.intRaw;
const base = this.n === 0 ? 14 : 18;
for (let i = 0; i < 4; i++) this.soc.setIrq(base + i, (st >> i) & 1);
}
read(off) {
if (off <= 0x20) return this.t[0].read(off);
if (off <= 0x44) return this.t[1].read(off - 0x24);
if (off >= 0x70 && off <= 0x94) return this.t[2].read(off, true);
const w = this.wdt;
switch (off) {
case 0x48: return w.cfg0; case 0x4c: return w.cfg1;
case 0x50: case 0x54: case 0x58: case 0x5c: return w.to[(off - 0x50) >> 2];
case 0x64: return w.prot;
case 0x68: return (this.calStart << 31) | (this.calMax << 16) | ((this.calRdy ? 1 : 0) << 15) | (this.calClkSel << 13);
case 0x6c: return this.calValue << 7;
case 0x98: return this.intEna; case 0x9c: return this.intRaw; case 0xa0: return this.intEna & this.intRaw;
}
return 0;
}
write(off, v) {
if (off <= 0x20) { this.t[0].write(off, v); return; }
if (off <= 0x44) { this.t[1].write(off - 0x24, v); return; }
if (off >= 0x70 && off <= 0x94) { this.t[2].write(off, v, true); return; }
const w = this.wdt;
switch (off) {
case 0x48: if (w.prot === 0x50D83AA1) w.cfg0 = v; break;
case 0x4c: if (w.prot === 0x50D83AA1) w.cfg1 = v; break;
case 0x50: case 0x54: case 0x58: case 0x5c: if (w.prot === 0x50D83AA1) w.to[(off - 0x50) >> 2] = v; break;
case 0x64: w.prot = v; break;
case 0x68: this.calStart = (v >>> 31) & 1; this.calMax = (v >> 16) & 0x7FFF; this.calClkSel = (v >> 13) & 3; this.doCal(); break;
case 0x98: this.intEna = v; this.updateIrq(); break;
case 0xa4: this.intRaw &= ~v; this.updateIrq(); break;
}
}
}
class GTimer {
constructor(tg, idx) { this.tg = tg; this.soc = tg.soc; this.idx = idx; this.reset(); }
reset() {
this.cfg = (1 << 30) | (1 << 29) | (1 << 13); this.alarmVal = 0; this.loadVal = 0; this.countBase = 0; this.nsBase = 0; this.last = 0;
this.ev = null; this.parse();
}
parse() {
const c = this.cfg;
this.en = !!(c >>> 31); this.inc = !!((c >> 30) & 1); this.auto = !!((c >> 29) & 1);
let d = (c >> 13) & 0xFFFF; this.div = d === 0 ? 65536 : (d === 1 || d === 2) ? 2 : d;
this.edge = !!((c >> 12) & 1); this.level = !!((c >> 11) & 1); this.alarmEn = !!((c >> 10) & 1);
}
count(now) {
if (!this.en) return this.countBase;
const ticks = Math.floor((now - this.nsBase) * this.soc.apbHz / 1e9 / this.div);
return this.countBase + (this.inc ? ticks : -ticks);
}
rebase() { const now = this.soc.nowNs; this.countBase = this.count(now); this.nsBase = now; this.arm(); }
arm() {
this.soc.cancel(this.ev); this.ev = null;
if (!this.en || !this.alarmEn) return;
const now = this.soc.nowNs;
const cur = this.count(now);
const to = (this.alarmVal - cur) * (this.inc ? 1 : -1);
if (to <= 0) return;
const ns = to * this.div * 1e9 / this.soc.apbHz;
this.ev = this.soc.schedule(now + ns, () => this.fire());
}
fire() {
const tg = this.tg; const bit = this.idx === 3 ? 3 : this.idx;
this.ev = null;
if (this.level) { tg.intRaw |= (1 << bit); tg.updateIrq(); }
this.alarmEn = false; this.cfg &= ~(1 << 10); // alarm_en auto-clears
if (this.auto) { this.reload(); this.alarmEn = true; this.cfg |= (1 << 10); this.arm(); }
}
reload() { this.nsBase = this.soc.nowNs; this.countBase = this.loadVal; }
// offsets relative to timer base: 0 cfg, 4 lo, 8 hi, c update, 10 alarmlo, 14 alarmhi, 18 loadlo, 1c loadhi, 20 load
// for LACT (lact=true) offsets are absolute: 0x70 cfg,0x74 rtc,0x78 lo,0x7c hi,0x80 update,0x84 alo,0x88 ahi,0x8c llo,0x90 lhi,0x94 load
norm(off, lact) { if (!lact) return off; if (off === 0x70) return 0; if (off === 0x74) return -1; return off - 0x78 + 4; }
read(off, lact) {
off = this.norm(off, lact);
switch (off) {
case 0: return this.cfg;
case 4: return this.last % 4294967296 | 0;
case 8: return Math.floor(this.last / 4294967296) | 0;
case 0x10: return this.alarmVal % 4294967296 | 0; case 0x14: return Math.floor(this.alarmVal / 4294967296) | 0;
case 0x18: return this.loadVal % 4294967296 | 0; case 0x1c: return Math.floor(this.loadVal / 4294967296) | 0;
}
return 0;
}
write(off, v, lact) {
off = this.norm(off, lact);
switch (off) {
case 0: this.rebase(); this.cfg = v & ~(1 << 10) | (v & (1 << 10)); this.parse(); this.nsBase = this.soc.nowNs; this.arm(); break;
case 0xc: this.last = this.count(this.soc.nowNs); break;
case 0x10: this.alarmVal = (Math.floor(this.alarmVal / 4294967296) * 4294967296) + (v >>> 0); this.arm(); break;
case 0x14: this.alarmVal = (v >>> 0) * 4294967296 + (this.alarmVal % 4294967296); this.arm(); break;
case 0x18: this.loadVal = (Math.floor(this.loadVal / 4294967296) * 4294967296) + (v >>> 0); break;
case 0x1c: this.loadVal = (v >>> 0) * 4294967296 + (this.loadVal % 4294967296); break;
case 0x20: this.reload(); this.arm(); break;
}
}
}