9.1 KiB
9.1 KiB
English | 简体中文
M0 feasibility report (esp-emu-peris)
Date: 2026-10-29 · Environment: esp-emulator browser wasm v0.51.0
(_tools/esp-emu-browser/www/pkg)
Firmware: ESP-IDF master (C:\esp\master\esp-idf, env via
C:\Users\A\Desktop\quick\idfm.bat)
Reproduce: node tests/m0_probe.mjs (the probe) + tests/firmware/peris_probe
(test firmware; idf.py build && idf.py merge-bin, then load
build/merged-binary.bin)
Summary of conclusions
| # | question | result | evidence |
|---|---|---|---|
| Q1a | can firmware GPIO output be observed by polling gpio_state() |
PASS | all 8 toggles visible (sampling granularity 50k instructions/batch) |
| Q1b | can gpio_set_input() on a pin raise the firmware's GPIO interrupt |
PASS | probe drove 6 falling edges; firmware ISR counted 6/6 |
| Q2a | do LEDC PWM waveforms show up in the GPIO output registers (pad-level duty-cycle polling) | FAIL | pin oe=1 but out constantly 0; estimates stuck at 0 during duty=50%/100% |
| Q2b | can LEDC duty registers be read via read_memory() and do they track the setpoint |
PASS | 53/53 samples tracked; half(4096)→DUTY_R=0x10000=4096×16, max(8191)→0x1fff0 |
| Q3a | can RMT/WS2812 waveforms be sampled at pin level with small run_batch(40) calls |
FAIL | out4 constant 0 across 2472 sampled batches, no level change at all |
| Q3b | can RMT channel RAM be read via read_memory() and decoded into WS2812 frames |
PASS | 46 frames decoded from 93 snapshots, colours matching the firmware log 0x111111/0x222222/0x333333 |
Decisive impact on the peripheral plans
- LED / push button / touch button (GPIO semantics): directly viable — Q1a + Q1b fully pass.
- PWM dimmed LED: take the MMIO register path (Q2b), not pin sampling
(Q2a proven unviable). Reading
LEDC_CHn_DUTY_Rgives the exact duty cycle — no aliasing, no overhead. - WS2812: take the RMT channel RAM path (Q3b), not pin sampling (Q3a proven unviable). Symbols remain in RAM after transmission and can be decoded afterwards.
- Bit-banged I2C/SPI/IR: still viable — Q1a shows CPU-driven GPIO_OUT flips are visible; the CPU does not sleep while flipping, so the "time fast-forward" issue below does not apply. High-rate sampling only works inside CPU-active windows.
Key technical details
ESP32-C3 register addresses (from IDF soc headers)
| register | address | notes |
|---|---|---|
| GPIO_OUT / GPIO_ENABLE | 0x60004004 / 0x60004020 | gpio_state() mirrors exactly these words |
| GPIO_FUNCn_OUT_SEL_CFG | 0x60004000+0x554+4n | low 9 bits = GPIO-matrix output signal select |
| IO_MUX_GPIO0..11_REG | 0x60009000+0x04+4n | bits 10..12 MCU_SEL and other pad config |
| LEDC_CHn_(CONF0/HPOINT/DUTY/CONF1/DUTY_R) | 0x60019000+0x14n(+0/4/8/C/10) | C3 uses the LSCHn naming |
| RMT_CHnCONF0 / channel RAM (RMTMEM) | 0x60016010+8n / 0x60016400+0xC0n | 48 words (192 bytes) per channel |
| RMT_CHnSTATUS | 0x60016028+4n | bits[11:9] = channel state machine (1 = sending) — M3 |
Measured behaviour highlights
- LEDC duty layout: the
DUTY_CH0field sits in bits[18:4]; register value = duty × 16 (same as real hardware). - GPIO-matrix routing is readable: after the firmware maps GPIO9 to
LEDC0,
FUNC9_OUT_SEL=0x2d(=45, LEDC_LS_CH0_SIG); after mapping GPIO4 to RMT,FUNC4_OUT_SEL=0x33(=51, RMT_SIG_0). Auto-discovery of peripheral wiring can be built on this: poll every pin's OUT_SEL + IO_MUX to learn which peripheral signal currently drives each pad. (M2 follow-up: the full C3 LEDC signal range ch0–5 = 45–50 was verified live — rgb-led auto-binding in tests/m1_smoke.mjs S8 — and the duty fraction decodes via conf0.TIMER_SEL → timer DUTY_RES, see peris/devices/ledc-lib.js. M3 follow-up: RMT_SIG_OUT0/1 = 51/52 are the C3's only RMT pad signals — ch2/3 are RX-only, so ws2812 offers just two TX channels.) - RMT RAM retention: symbols are not cleared after transmission;
48 words per channel. (M3 follow-up refined the model: the copy encoder
writes one RMT word per WS2812 bit — d0/l0 = high run, d1/l1 = low
run of the same bit — words land sequentially at
k mod 48and wrap, so after a transmission the RAM holds exactly the reset word, one all-zero transmit-terminator word, and the last 46 bits of the frame. The reset symbol delimits a frame (low ≥30µs, i.e. d0≥300 with l0=0); a reset lands at word24·L mod 48(0 or 24), so any new frame's first 26 writes overwrite every possible stale reset. Consequence, identical to real C3 hardware: strips longer than ~2 LEDs are only recoverable in their tail — ⌊46/24⌋ LED slots, the oldest of them minus its two head bits.) - Bit-grid resynchronisation: the RAM window is the frame tail, so the
bitstream is missing its head bits relative to the LED byte grid (the
RAM wrap destroyed them); prepending
(24 − visible mod 24) mod 24zero bits re-aligns it (M0 measured 2 lost bits for 8-LED frames; the per-LED "grad" burst +tests/m1_smoke.mjsS9 pin the exact behaviour on real firmware — ws2812-lib implements it indecodeWs2812).CHnSTATUSbits[11:9] ≠ 0 while the TX state machine is sending, so the M3 device skips half-overwritten windows. - Time fast-forward: during
vTaskDelaythe emulator does not spend full CPU cycles (cycles()can jump by thousands inside onerun_batch(40)), therefore peripheral waveforms during CPU sleep are not capturable at pin level — the root cause of the Q3a failure, independent of sample rate. - Performance (Node 24, direct wasm calls): coarse (50k instr/batch) ≈
5000+ MIPS; fine (40 instr/batch +
gpio_state()each batch) ≈ 99 MIPS (~53× overhead). A browser worker is in the same ballpark — fine-grained sampling should only be a temporary measure inside CPU-active windows; standing sampling uses coarse batches + MMIO. - Firmware must be a merged image (bootloader@0x0 + partition
table@0x8000 + app@0x10000), produced by
idf.py merge-bin; a bare app bin does not boot.
Ruled out / still limited by upstream
- Hardware I2C/SPI external device models, a real touch peripheral, ADC, I2S, SPI/RGB565 displays, SD cards, UART1: no wasm API — waiting on esp-emulator upstream (see docs/ROADMAP.md).
- Pin-level PWM/RMT waveform observation: structurally unviable (Q2a/Q3a), independent of sample rate.
M4 measurements (bit-bang protocol devices + IR)
Firmware tests/firmware/peris_probe P4 phase, wasm v0.51.0; scripts were
scratch probes (conclusions verified by the shipped tests):
- Guest clock distortion through ROM delays:
esp_rom_delay_usadvances emulator time at ~50% of nominal — a "9 ms" NEC leader measures 4.505 ms, a "13 µs" carrier half-period ~6.5 µs (exact 0.50 ratio everywhere;run_batchsizes were ruled out as the cause by tracing cycles between edges). Consequences: the NEC decoder is ratio-based (nec-lib.decodeNec— leader dominance, leader-gap ½ vs ¼, bit gap/burst ≈1 vs ≈3); the I2C engine is level-based (no timing at all);vTaskDelaysleeps keep full emulated duration (the distortion is in the ROM delay loop, not the sleep path). - Armed-batch sleep fast-forward: with an arm lease standing
(~1000-instruction batches), each batch during
vTaskDelayjumps ~680k cycles — a whole firmware round (P1–P4, ~11 s emulated) passes in ~2.5k armed batches, while P4's CPU-active fine windows (300-instr batches) need ~166k batches. Batch-count test windows therefore overshoot entire rounds; every m1_smoke window is marker-bounded. - Input pulse cadence for GPIO ISRs (button semantics at armed sizes): pulses every 8 batches held 3 (~10–40 ms emulated during sleep fast-forward) count 4/4; press+release inside one batch gap loses the edge (known), and M0's instruction-count shapes (150k instr held / 2M instr apart) no longer fit inside one P1 (~470 armed batches) — only ~1 of 3 counts. Emulated timing, not instruction counts, is what the ISR needs.
CHnSTATUS.APB_MEM_WADDR(bits[20:12]) reads 0 always — the emulator does not implement the RMT write pointer. Mid-transmission channel-RAM stitching is therefore impossible; post-TX RAM windows are the only RMT decode path (NEC fits: 34 words ≤ 46).- Two I2C slaves on one bus need wired-AND input arbitration:
gpio_set_inputis a level write with a single winner, so a second slave engine's "release" silently overwrote the addressed slave's ACK pull (every transaction NACKed). The runtime now resolves shared-bus pad drives as a wired-AND (sharedBusdevice metadata). - Soft-I2C round-trip is exact at ~20 kHz firmware clocking (25 µs
half-period → ~12.5 µs emulated): write+sequential-read, pointer wrap,
33-byte factory calibration, forced measurements — all
ack=0. The C↔JS compensation parity check caughtdig_P1needing uint16 (36477 wraps negative in int16 — T/H matched, only P diverged, which is what localised it).