esp32emu / js /i2s.js
HelloSun's picture
ESP32 emulator update
8d577f5 verified
Raw History Blame Contribute Delete
9.65 kB
// ESP32 I2S controller (TX DMA, LCD-mode parallel output) + VGA monitor model.
const ORDER = [2, 3, 0, 1]; // output order of the bytes of each 32-bit DMA word in 8-bit LCD mode
// VGA32 board wiring: 2 bits per colour channel (resistor DAC), index 1 = MSB
export const VGA32_BOARD = { r: [21, 22], g: [18, 19], b: [4, 5], hs: 23, vs: 15 };
export class VgaMonitor {
constructor(board) {
this.board = board || VGA32_BOARD;
this.lut = new Uint16Array(256);
this.idleHs = 1; this.idleVs = 1; this.calib = 0; this.cnt = [0, 0, 0, 0];
this.prevHs = 1; this.prevVs = 1;
this.cand = new Uint8Array(4096); this.candLen = 0; this.candBuf = new Uint8Array(4096);
this.rows = []; this.frameRows = 0; this.W = 0;
this.frame = new Uint8Array(4096 * 1536); this.frameW = 0; this.frameH = 0;
this.frames = 0; this.newFrame = false; this.hsMask = 0; this.vsMask = 0; this.fastOk = false;
this.lastFrameTimeNs = 0; this.frameRateHz = 0; this.padOfBit = [];
this.palette = new Uint32Array(64);
for (let i = 0; i < 64; i++) { const r = (i & 3) * 85, g = ((i >> 2) & 3) * 85, b = ((i >> 4) & 3) * 85; this.palette[i] = (255 << 24) | (b << 16) | (g << 8) | r; }
}
buildLut(padOfBit) {
this.padOfBit = padOfBit.slice();
const b = this.board;
for (let s = 0; s < 256; s++) {
const lvl = (pad) => { let v = 0, any = false; for (let k = 0; k < 8; k++) if (padOfBit[k] === pad) { any = true; v = (s >> k) & 1; } return any ? v : 0; };
const r = (lvl(b.r[1]) << 1) | lvl(b.r[0]);
const g = (lvl(b.g[1]) << 1) | lvl(b.g[0]);
const bl = (lvl(b.b[1]) << 1) | lvl(b.b[0]);
this.lut[s] = r | (g << 2) | (bl << 4) | (lvl(b.hs) << 8) | (lvl(b.vs) << 9);
}
const hsBit = padOfBit.indexOf(b.hs), vsBit = padOfBit.indexOf(b.vs);
this.hsBit = hsBit; this.vsBit = vsBit;
this.fastOk = hsBit >= 0 && vsBit >= 0 && padOfBit.lastIndexOf(b.hs) === hsBit && padOfBit.lastIndexOf(b.vs) === vsBit;
this.hsMask = this.fastOk ? (0x01010101 << hsBit) | 0 : 0;
this.vsMask = this.fastOk ? (0x01010101 << vsBit) | 0 : 0;
this.lutVersion = (this.lutVersion || 0) + 1;
}
// Process one DMA descriptor worth of samples. mem8/mem32: heap views, off: byte offset, len: bytes
feed(mem8, mem32, off, len) {
const lut = this.lut;
let hsP = this.prevHs, vsP = this.prevVs;
const idleHs = this.idleHs, idleVs = this.idleVs;
let sawHsEdge = false;
// fast path: whole aligned descriptor with all-idle sync bits
if (this.calib >= 3 && this.fastOk && (off & 3) === 0 && (len & 3) === 0 && hsP === idleHs && vsP === idleVs) {
const w0 = off >> 2, wn = w0 + (len >> 2);
const hsIdle = idleHs ? this.hsMask : 0, vsIdle = idleVs ? this.vsMask : 0;
const hm = this.hsMask, vm = this.vsMask;
let clean = true;
for (let w = w0; w < wn; w++) { const v = mem32[w]; if ((v & hm) !== hsIdle || (v & vm) !== vsIdle) { clean = false; break; } }
if (clean) { this.addCandidate(mem8, off, len); return; }
}
// slow path: per-sample sync tracking
const L = len;
for (let i = 0; i < L; i++) {
const v = lut[mem8[off + (i & ~3) + ORDER[i & 3]]];
const hs = (v >> 8) & 1, vs = (v >> 9) & 1;
if (this.calib < 3) this.cnt[hs] ++, this.cnt[2 + vs]++;
if (hs !== hsP) {
sawHsEdge = true;
if (hs === idleHs) this.endLine();
hsP = hs;
}
if (vs !== vsP) {
if (vs !== idleVs) this.endFrame();
vsP = vs;
}
}
this.prevHs = hsP; this.prevVs = vsP;
if (!sawHsEdge && L > 0) this.addCandidate(mem8, off, len);
}
addCandidate(mem8, off, len) {
if (len > this.candLen) {
const lut = this.lut, c = this.cand;
if (len > c.length) return;
for (let i = 0; i < len; i++) c[i] = lut[mem8[off + (i & ~3) + ORDER[i & 3]]] & 63;
this.candLen = len;
}
}
endLine() {
if (this.candLen >= 32) {
if (this.frameRows < 1536) {
if (this.candLen !== this.frameW) { if (this.frameRows === 0 || this.candLen > this.frameW) this.frameW = this.candLen; }
this.frame.set(this.cand.subarray(0, Math.min(this.candLen, 4096)), this.frameRows * 4096);
this.rowLen = this.rowLen || []; this.rowLen[this.frameRows] = this.candLen;
this.frameRows++;
}
}
this.candLen = 0;
}
endFrame() {
this.endLine();
if (this.calib < 3) {
this.calib++;
if (this.calib === 3) { this.idleHs = this.cnt[1] >= this.cnt[0] ? 1 : 0; this.idleVs = this.cnt[3] >= this.cnt[2] ? 1 : 0; this.prevHs = this.idleHs; this.prevVs = this.idleVs; }
}
if (this.frameRows > 8) { this.frameH = this.frameRows; this.newFrame = true; this.frames++; this.presentW = this.frameW; }
this.frameRows = 0;
}
// Convert the last complete frame to RGBA (Uint32Array) - returns null if no new frame
takeFrame() {
if (!this.newFrame) return null;
this.newFrame = false;
const w = this.presentW, h = this.frameH;
const out = new Uint32Array(w * h), pal = this.palette;
for (let y = 0; y < h; y++) { const base = y * 4096; for (let x = 0; x < w; x++) out[y * w + x] = pal[this.frame[base + x]]; }
return { w, h, data: out };
}
}
export class I2S {
constructor(soc, n) { this.soc = soc; this.n = n; this.monitor = new VgaMonitor(); this.reset(); }
get src() { return 32 + this.n; }
reset() {
this.r = new Int32Array(0x100);
this.intRaw = 0; this.intEna = 0; this.running = false; this.ev = null;
this.linkAddr = 0; this.cur = 0; this.eofAddr = 0; this.txStart = false; this.linkStarted = false;
this.fpix = 25175000; this.lutVer = -1; this.gen = 0;
this.soc.setIrq(this.src, 0);
}
updateIrq() { this.soc.setIrq(this.src, (this.intRaw & this.intEna) !== 0); }
clockHz() {
const clkm = this.r[0xAC >> 2];
const clka = (clkm >> 21) & 1;
const div = clkm & 0xFF, b = (clkm >> 8) & 0x3F, a = (clkm >> 14) & 0x3F;
const src = clka ? this.soc.apllHz : 160e6;
const divf = (div || 1) + (a ? b / a : 0);
const bck = (this.r[0xB0 >> 2] >> 6) & 0x3F;
return src / divf / Math.max(1, bck);
}
read(off) {
switch (off) {
case 0x08: return this.r[2] & ~0x30 | (this.txStart ? 0x10 : 0);
case 0x0C: return this.intRaw; case 0x10: return this.intRaw & this.intEna; case 0x14: return this.intEna;
case 0x38: return this.eofAddr | 0;
case 0xBC: return 0x0;
default: return this.r[(off >> 2) & 0xFF];
}
}
write(off, v) {
const r = this.r;
switch (off) {
case 0x08: {
r[2] = v;
const start = (v >> 4) & 1;
if (v & 1) this.stop(); // tx_reset
if (start && !this.txStart) { this.txStart = true; this.tryStart(); }
else if (!start && this.txStart) { this.txStart = false; this.stop(); }
break;
}
case 0x14: this.intEna = v; this.updateIrq(); break;
case 0x18: this.intRaw &= ~v; this.updateIrq(); break;
case 0x30: {
this.linkAddr = 0x3FF00000 | (v & 0xFFFFF);
if ((v >>> 29) & 1) { this.linkStarted = true; this.cur = this.linkAddr; this.tryStart(); }
if ((v >>> 28) & 1) { this.linkStarted = false; this.stop(); }
r[0x30 >> 2] = v & 0x8FFFFFFF;
break;
}
case 0x60: r[0x60 >> 2] = v; if (v & 2) { this.stop(); } break; // out_rst
default: r[(off >> 2) & 0xFF] = v;
}
}
stop() { this.running = false; this.gen++; this.soc.cancel(this.ev); this.ev = null; this.linkStarted = this.linkStarted && this.txStart; }
tryStart() {
if (!this.txStart || !this.linkStarted || this.running) return;
this.running = true; this.gen++;
this.fpix = this.clockHz() || 25175000;
this.cur = this.linkAddr;
this.scheduleNext(this.soc.nowNs);
}
// translate DMA address -> heap offset
heapOff(a) { const d = this.soc.cpus[0].rd[a >>> 12]; return d === 1 ? -1 : (a + d) | 0; }
scheduleNext(tNs) {
const soc = this.soc;
const doff = this.heapOff(this.cur);
if (doff < 0 || this.cur === 0) { this.running = false; this.intRaw |= (1 << 14); this.updateIrq(); return; } // descriptor error
const h32 = soc.h32;
const w0 = h32[doff >> 2];
const length = (w0 >>> 12) & 0xFFF;
const dur = length * 1e9 / this.fpix;
const gen = this.gen;
this.ev = soc.schedule(tNs + Math.max(dur, 200), () => this.complete(gen, tNs + Math.max(dur, 200)));
}
complete(gen, tNs) {
if (gen !== this.gen || !this.running) return;
const soc = this.soc, h32 = soc.h32;
const doff = this.heapOff(this.cur);
const w0 = h32[doff >> 2], buf = h32[(doff >> 2) + 1] >>> 0, next = h32[(doff >> 2) + 2] >>> 0;
const length = (w0 >>> 12) & 0xFFF, eof = (w0 >>> 30) & 1;
// refresh pixel pipeline LUT if GPIO matrix changed
if (this.lutVer !== soc.gpio.matrixVersion) { this.buildLut(); this.lutVer = soc.gpio.matrixVersion; }
const boff = this.heapOff(buf);
if (boff >= 0) this.monitor.feed(soc.h8, h32, boff, length);
this.intRaw |= (1 << 11);
if (eof) { this.intRaw |= (1 << 12); this.eofAddr = this.cur; }
if (next === 0) { this.intRaw |= (1 << 16); this.running = false; this.linkStarted = false; this.updateIrq(); return; }
this.cur = next;
this.updateIrq();
this.scheduleNext(tNs);
}
buildLut() {
const gpio = this.soc.gpio; const base = this.n === 0 ? 140 : 166;
const padOfBit = [];
for (let k = 0; k < 8; k++) {
let pad = -1;
for (let p = 0; p < 40; p++) if ((gpio.outSel[p] & 0x1FF) === base + k) { pad = p; break; }
padOfBit.push(pad);
}
this.monitor.buildLut(padOfBit);
}
}