Download js/i2s.js from HelloSun/esp32emu: direct link, hf CLI and curl.
- Browser
- Download file 9.65 kB
-
https://huggingface.co/spaces/HelloSun/esp32emu/resolve/main/js/i2s.js
- Command line
-
hf download hf://spaces/HelloSun/esp32emu/js/i2s.js
-
curl -L -o i2s.js https://huggingface.co/spaces/HelloSun/esp32emu/resolve/main/js/i2s.js
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); | |
| } | |
| } | |