// 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); } }