diff --git a/docs/reference/hardware/esp32-p4-eth.md b/docs/reference/hardware/esp32-p4-eth.md new file mode 100644 index 000000000..0a4714273 --- /dev/null +++ b/docs/reference/hardware/esp32-p4-eth.md @@ -0,0 +1,63 @@ +# Waveshare ESP32-P4-ETH hardware reference + +Pin maps and onboard features for the **Waveshare ESP32-P4-ETH**, read from the official schematic so MoonLight work (Ethernet, audio) reads this instead of re-scraping the PDF. The board is `esp32p4rev1-eth-es8311` in firmware, `"Waveshare ESP32-P4-ETH"` in the device catalog. + +**Sources** +- Product page: +- Wiki: +- Schematic (local): `datasheet/ESP32-P4-ETH/ESP32-P4-ETH-datasheet.pdf` + +Same family as the Waveshare ESP32-P4-NANO (see [gpio-usage.md § ESP32-P4](gpio-usage.md#esp32-p4) for its pin map): ESP32-P4 (RISC-V dual-core), same onboard IP101 Ethernet PHY wiring, same CSI/DSI/I2C accessory-bus convention. This board adds an onboard **ES8311** audio codec feeding an SMD mic + a 2-pin JST speaker header, which the P4-NANO lacks. The [MHC-WLED ESP32-P4 shield](mhc-wled-esp32-p4-shield.md) is a different, unrelated P4 carrier board (RS-485/line-in, no onboard codec). + +## Audio (ES8311 codec) + +The onboard SMD mic and speaker header connect through an **ES8311 mono codec**. The ESP is the I2S master (it drives MCLK). + +| Signal | GPIO | Direction / role | +|---|---|---| +| I2S MCLK | 13 | master clock → codec (ESP is I2S master; MCLK = 256 × sample_rate) | +| I2S SCLK (BCLK) | 12 | bit clock | +| I2S LRCK (WS) | 10 | word select | +| I2S ASDOUT | 11 | **mic / ADC data: codec → ESP** (the record path) | +| I2S DSDIN | 9 | playback / DAC data: ESP → codec (speaker path, **not wired up** — out of scope, mic-only) | +| **ESP_I2C_SDA** | **7** | codec control bus (shared with the CSI/DSI accessory headers) | +| **ESP_I2C_SCL** | **8** | codec control bus | +| PA_Ctrl | 53 | speaker amp enable (not wired up — speaker is out of scope) | + +- **ES8311 I2C address: `0x18`** (the hardware default; no address-select strap). +- Driven by Espressif's **`esp_codec_dev`** managed component (same driver as the ESP32-S31's ES8311), gated behind the `CONFIG_MM_P4_ES8311` Kconfig option (set only by the `esp32p4rev1-eth-es8311` firmware fragment `sdkconfig.defaults.esp32p4-es8311`) — most other P4 boards share this chip target but have no codec on the I2C bus, so the codec bring-up can't be a chip-wide default: `audioCodecType`/`audioCodecPins` are compile-time constants, and once non-`None`, `AudioService::reinit()` unconditionally probes the codec over I2C and fails the whole mic path if nothing answers. +- The codec needs **MCLK running before it answers I2C**; `AudioService::reinit()` brings up the I2S channel (which drives MCLK) before the codec I2C config, same ordering as the S31. +- **Bench-verified end to end** 2026-10-03: Ethernet link + DHCP, `I2cScanModule` ACKs the codec at 0x18 on GPIO7/8, and `AudioService` reports live mic data (non-zero level RMS, onsets, peak Hz) with `mode="local audio"`, `micMode="I2S"`, `sckPin=12`, `wsPin=10`, `sdPin=11` (the catalog entry's defaults). +- **Two bugs found and fixed in `platform_esp32_es8311.cpp`** during that bench pass (both apply to the S31's codec path too, same file, not P4-ETH-specific): + 1. `esp_codec_dev`'s I2C control layer right-shifts the address it's given by one bit before handing it to the IDF `i2c_master` driver (`audio_codec_ctrl_i2c.c`: `.device_address = (i2c_cfg->addr >> 1)`) — it wants the 8-bit write-address form, not the bare 7-bit address a scan/probe uses. Passing `0x18` straight through silently targeted `0x0C`: every register write NACK'd (`ES8311: Open fail`) even though a raw `i2c_master_probe` at `0x18` ACKs cleanly (that path doesn't shift). Fixed by shifting at the one call site that needs it; `AudioCodecPins.i2cAddr` itself stays the natural 7-bit value everywhere else. + 2. `esp_codec_dev_new` requires a non-null `data_if`, which the code never set (by design — `audioMicInit` owns the real I2S RX channel, not `esp_codec_dev`). Fixed with a `data_if` whose rx/tx handles stay null on purpose; its `set_fmt`/`enable` calls log harmless `NULL` warnings each re-init (`i2s_channel_get_info: input parameter 'handle' is NULL`, etc.) since `esp_codec_dev_open` discards their return value — expected noise, not a fault. +- **Mic-only path** (audio-reactive input): all pins above confirmed independently from the schematic netlist *and* the Waveshare wiki (which separately publishes the same I2C/I2S pin set). The speaker path (DSDIN + PA_Ctrl) is a separate capability, not implemented here. + +## Ethernet (IP101 PHY, RMII) + +On-chip EMAC → **IP101GRI** PHY → RJ45, **RMII** with a 25 MHz crystal, externally-fed 50 MHz reference clock. Same reference design as the Waveshare P4-NANO — confirmed pin-for-pin identical: + +| Signal | GPIO | Source | +|---|---|---| +| MDC | 31 | schematic netlist (unambiguous two-pin net, matches the P4 pin table) | +| MDIO | 52 | schematic netlist (same) | +| PHY reset | 51 | direct schematic inspection (the flattened PDF text extraction was ambiguous here; confirmed by eye against the drawing) | +| RMII ref clock (external in) | 50 | carried over from the P4-NANO preset, consistent with the other three now-confirmed pins | + +Catalog entry: `NetworkModule.ethBoard = "ESP32-P4-ETH"`, its own `kEthPresets` row in `NetworkModule.h` (not a silent alias of `"P4-NANO"`) — the pins read identical today, but a future correction to one board's preset shouldn't silently move the other. + +## Other onboard features (not wired up by this branch) + +From the Waveshare wiki: MIPI-CSI (2-lane, OV5647-compatible), MIPI-DSI (2-lane, 5"/7"/8"/10.1" panels), SDIO 3.0 microSD slot, USB-C (native + UART bridge), BOOT/RESET buttons. None of these conflict with the audio (GPIO 7-13) or Ethernet (GPIO 28-31/49-52) pins above. + +## Free GPIO for user peripherals (LED strips) + +Following the P4-NANO's own reference set (`ParallelLedDriver` default `pins="20,21,22,23,24,25,26,27"`), with this board's audio pins added to the committed set: + +| Peripheral | GPIOs | +|---|---| +| Ethernet RMII (fixed silicon pads) | 28-31, 49-52 | +| ES8311 audio (I²S + I²C) | 7-13 | +| CSI/DSI accessory headers | share the I2C bus (7-8) plus their own dedicated MIPI diff-pairs | + +**Clear, following the P4-NANO's documented set**: 20-27, 32-33, 39-48 (the catalog's `ParallelLedDriver` default is `pins="20,21,22,23,24,25,26,27"`, same as the P4-NANO). GPIO 20/21 also appear as spare IO on this board's CSI header (same situation as the P4-NANO, which treats them as free LED pins regardless) — only relevant if a camera module is actually plugged in. See [gpio-usage.md § ESP32-P4](gpio-usage.md#esp32-p4) for the general P4 pin map this board extends. diff --git a/docs/reference/hardware/esp32-s31-coreboard.md b/docs/reference/hardware/esp32-s31-coreboard.md index 7189a94e2..f3467e6a4 100644 --- a/docs/reference/hardware/esp32-s31-coreboard.md +++ b/docs/reference/hardware/esp32-s31-coreboard.md @@ -38,6 +38,7 @@ The onboard electret mic (J6) and speaker connect through an **ES8311 mono codec needs **MCLK running before it answers I2C**, and `es8311_codec_cfg.mclk_div` must be set (256, the standard ratio) or `open` fails "unable to configure sample rate". So AudioService brings up the I2S channel (which drives MCLK on GPIO52) **before** the codec I2C config. - **Mic-only path** (audio-reactive input) needs MCLK/SCLK/LRCK + ASDOUT (record) + the I2C bus. The speaker path (DSDIN + PA_CTRL) is output, a separate capability. +- **Two codec-bring-up bugs fixed 2026-10-03** (found bench-testing the [ESP32-P4-ETH](esp32-p4-eth.md), which shares this exact code path): `esp_codec_dev`'s I2C control layer right-shifts the address it's given by one bit, so the raw 7-bit `0x18` must be passed pre-shifted (`<<1`) — the bare 7-bit value silently targeted `0x0C` and every register write NACK'd even though a scan at `0x18` ACKs cleanly (that path doesn't shift). And `esp_codec_dev_new` requires a non-null `data_if`, which the code never set. Both are now fixed in `platform_esp32_es8311.cpp`, so a full codec-open bench pass on the S31 (not just the I2C scan referenced above) is worth re-running before the `Audio` capability stays under `planned`. ## Ethernet (YT8531 PHY, RGMII, 1 Gbps) diff --git a/docs/reference/hardware/firmware-variants.md b/docs/reference/hardware/firmware-variants.md index 62f4c7b15..f7c32d144 100644 --- a/docs/reference/hardware/firmware-variants.md +++ b/docs/reference/hardware/firmware-variants.md @@ -14,6 +14,7 @@ Selected by `build_esp32.py --firmware `, reported by `SystemModule.firmwar | `esp32s3-n16r8`, `esp32s3-n8r8` | S3 | WiFi and W5500 SPI Ethernet | | `esp32p4rev1-eth` | P4 rev 1 (Waveshare ESP32-P4-NANO) | Ethernet only | | `esp32p4rev1-eth-wifi` | P4 rev 1 | Ethernet, plus WiFi via the on-board ESP32-C6 over esp_hosted | +| `esp32p4rev1-eth-es8311` | P4 rev 1 (Waveshare ESP32-P4-ETH) | Ethernet only, plus the board's onboard ES8311 audio codec (see [esp32-p4-eth.md](esp32-p4-eth.md)) | | `esp32p4rev3-eth`, `esp32p4rev3-eth-wifi` | P4 rev 3 silicon | As rev 1. Rev 3 is not binary-compatible with rev 1; untested, no v3 board on the bench | ## Scaling to available memory diff --git a/docs/reference/hardware/gpio-usage.md b/docs/reference/hardware/gpio-usage.md index 66a78575f..dbf8dee55 100644 --- a/docs/reference/hardware/gpio-usage.md +++ b/docs/reference/hardware/gpio-usage.md @@ -73,6 +73,8 @@ For **LED output** specifically — the pins a WS2812-class strand data line can **Clear on the P4-NANO:** 20-27, 32-33, 39-48 (the LED-driver default is `pins="20,21,22,23,24,25,26,27"`). Exact free set is board-specific; the NANO's is the reference. A carrier board changes what's exposed and how — the [MHC-WLED ESP32-P4 shield](mhc-wled-esp32-p4-shield.md) routes every terminal through level shifters / RS-485 transceivers / protected inputs (no bare GPIO), which is why a direct loopback jumper fails on it; its full terminal map is on that page. +**The Waveshare ESP32-P4-ETH** follows the NANO's Ethernet wiring exactly (RMII 28-31/49-52) but additionally commits **GPIO 7-13** to its onboard ES8311 audio codec (I2C 7-8 + I2S 9-13), beyond the I2C-only use the NANO makes of 7-8. Full pin map: [esp32-p4-eth.md](esp32-p4-eth.md). + ## ESP32-S31 [Datasheet](https://documentation.espressif.com/esp32-s31_datasheet_en.pdf). RISC-V, Wi-Fi 6 + on-chip 1 Gbps EMAC; shares the MoonLive RISC-V backend with the P4. The bench board's full schematic pin map (Ethernet RGMII, ES8311 audio codec, SD, USB-host) is on its own page: [ESP32-S31 coreboard](esp32-s31-coreboard.md). diff --git a/esp32/main/Kconfig.projbuild b/esp32/main/Kconfig.projbuild index ef0b24964..9b9ac9fb2 100644 --- a/esp32/main/Kconfig.projbuild +++ b/esp32/main/Kconfig.projbuild @@ -37,4 +37,20 @@ menu "MoonLight" the same HlsDriver the desktop uses. Only the P4 has the encoder silicon; other chips keep this off and build nothing. + config MM_P4_ES8311 + bool "ESP32-P4: onboard ES8311 audio codec (Waveshare ESP32-P4-ETH)" + default n + help + Pulls the esp_codec_dev managed component and enables the ES8311 + codec bring-up (I2C on GPIO7/8, I2S on GPIO9-13) for a P4 board + that actually carries the codec, e.g. the Waveshare ESP32-P4-ETH. + + OFF by default: most P4 boards (the P4-NANO, the MHC-WLED shield, + the MM testbench P4's direct bench mic) have no codec on that I2C + bus, and AudioService's codec bring-up is unconditional once + audioCodecType is non-None — it would probe for an ES8311 that + nothing answers and fail the whole mic path. Only the + esp32p4rev1-eth-es8311 build sets this, via + sdkconfig.defaults.esp32p4-es8311. + endmenu diff --git a/esp32/main/idf_component.yml b/esp32/main/idf_component.yml index 334c744d9..8454d1bbd 100644 --- a/esp32/main/idf_component.yml +++ b/esp32/main/idf_component.yml @@ -45,17 +45,34 @@ dependencies: espressif/esp-dsp: version: "^1.5.0" # esp_codec_dev — Espressif's audio-codec driver library; we use its ES8311 part - # (es8311_codec_new) to configure the ESP32-S31 CoreBoard's onboard mic, which is - # an analog mic behind an ES8311 I2S codec (I2C addr 0x18) rather than a direct - # I2S MEMS mic. Referenced only from platform_esp32_es8311.cpp, behind the codec - # gate. The `rules` gate scopes it to the S31 — the only board with the codec — - # so other targets don't resolve a component they never compile (same pattern as - # ip101/w5500 above). A managed component outside mainline v6.0, like the P4 - # esp_hosted exception (docs/how-to/building.md § ESP-IDF version). + # (es8311_codec_new) to configure a board's onboard mic where it is an analog mic + # behind an ES8311 I2S codec (I2C addr 0x18) rather than a direct I2S MEMS mic. + # Referenced only from platform_esp32_es8311.cpp, behind the codec gate. Two + # boards carry the codec today: the ESP32-S31 CoreBoard (unconditionally, it is + # the only S31 board) and the Waveshare ESP32-P4-ETH (MM_P4_ES8311, since most P4 + # boards have no codec on that bus — see Kconfig.projbuild). The `rules` gate + # scopes the dependency to those two cases so other targets/builds don't resolve + # a component they never compile (same pattern as ip101/w5500 above). ONE `if` + # with `||` — NOT two separate `rules` entries: the component manager ANDs + # multiple `rules` entries together (idf_component_tools/manifest/models.py, + # OptionalRequirement.version_spec_if_meet_conditions: "AND all the `rules`"), + # so `target == esp32s31` plus a separate `$CONFIG{...}` rule would require BOTH + # at once — impossible — and silently skip the dependency on every build + # ("NOTE: Skipping optional dependency: espressif/esp_codec_dev"), which is + # exactly what happened here first (bench-caught on the P4-ETH: the codec + # answered on the I2C scan but AudioService's own bring-up failed immediately, + # "codec init failed" — the real implementation never got linked in because + # `__has_include("esp_codec_dev.h")` was false, so platform_esp32_es8311.cpp + # built its inert stub, with no managed_components/espressif__esp_codec_dev/ in + # the build dir, and the "NOTE: Skipping" line confirmed it). `||` is the + # if-clause grammar's OR (idf_component_tools/manifest/if_parser.py uses `&&`/ + # `||`, not `and`/`or`), combining both conditions in the one rule that must + # match. A managed component outside mainline v6.0, like the P4 esp_hosted + # exception (docs/how-to/building.md § ESP-IDF version). espressif/esp_codec_dev: version: "^1.3.0" rules: - - if: "target == esp32s31" + - if: "target == esp32s31 || $CONFIG{MM_P4_ES8311} == True" # esp_wifi_remote + esp_hosted — WiFi for the ESP32-P4 via the on-board ESP32-C6 # co-processor over SDIO. The P4 has no native radio; these present the C6's # radio through the standard esp_wifi_* API (API-compatible, so the WiFi seam in diff --git a/esp32/sdkconfig.defaults.esp32p4-es8311 b/esp32/sdkconfig.defaults.esp32p4-es8311 new file mode 100644 index 000000000..2031556bc --- /dev/null +++ b/esp32/sdkconfig.defaults.esp32p4-es8311 @@ -0,0 +1,16 @@ +# Waveshare ESP32-P4-ETH: onboard ES8311 audio codec. +# Board: https://www.waveshare.com/esp32-p4-eth.htm +# Wiki: http://www.waveshare.com/wiki/ESP32-P4-ETH +# Schematic: datasheet/ESP32-P4-ETH/ESP32-P4-ETH-datasheet.pdf +# +# Layered onto sdkconfig.defaults + sdkconfig.defaults.esp32p4rev1-eth by +# moondeck/build/build_esp32.py. The eth fragment brings the EMAC / RMII / flash / +# PSRAM / partition layout; this fragment adds the onboard codec. +# +# Sets the Kconfig symbol declared in main/Kconfig.projbuild, which +# main/idf_component.yml reads to pull the esp_codec_dev managed component, and +# which src/platform/esp32/platform_config.h reads to set audioCodecType/ +# audioCodecPins to the ES8311 on I2C GPIO7/8 + I2S GPIO9-13. See MM_P4_ES8311's +# own help text for why this is a separate opt-in rather than a P4-wide default: +# most other P4 boards have no codec on that bus. +CONFIG_MM_P4_ES8311=y diff --git a/mkdocs.yml b/mkdocs.yml index 2827404bc..32754ca3a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -231,6 +231,7 @@ nav: - Firmware variants and memory: reference/hardware/firmware-variants.md - GPIO usage per MCU: reference/hardware/gpio-usage.md - ESP32-S31 coreboard: reference/hardware/esp32-s31-coreboard.md + - Waveshare ESP32-P4-ETH: reference/hardware/esp32-p4-eth.md - MHC-WLED ESP32-P4 shield: reference/hardware/mhc-wled-esp32-p4-shield.md - Light fixtures and DMX nodes: reference/hardware/light-fixtures.md - Control surfaces: reference/hardware/control-surfaces.md diff --git a/moondeck/build/build_esp32.py b/moondeck/build/build_esp32.py index 8f70ee724..11335501b 100644 --- a/moondeck/build/build_esp32.py +++ b/moondeck/build/build_esp32.py @@ -288,6 +288,21 @@ def check_idf_pin(idf_path: Path) -> None: "ships": True, "panel_cards": True, }, + "esp32p4rev1-eth-es8311": { + "chip": "esp32p4", + "fragments": ["sdkconfig.defaults", "sdkconfig.defaults.esp32p4rev1-eth", + "sdkconfig.defaults.esp32p4-es8311"], + "eth_only": True, + "description": "Waveshare ESP32-P4-ETH — Ethernet only (IP101 PHY, same " + "pins as the P4-NANO) plus the board's onboard ES8311 audio " + "codec (I2C GPIO7/8, I2S GPIO9-13), for P4 revisions 0.x/1.x " + "ONLY. No WiFi combo yet (no on-board C6 co-processor " + "question has been resolved for this board); follows the " + "same fragment-stacking pattern as esp32p4rev1-eth-wifi if " + "one is added later.", + "ships": True, + "panel_cards": True, + }, "esp32p4rev3-eth": { "chip": "esp32p4", "fragments": ["sdkconfig.defaults", "sdkconfig.defaults.esp32p4rev1-eth", diff --git a/mooninstaller/deviceModels.json b/mooninstaller/deviceModels.json index c1d195965..2f5c118ce 100644 --- a/mooninstaller/deviceModels.json +++ b/mooninstaller/deviceModels.json @@ -1004,6 +1004,69 @@ } ] }, + { + "name": "Waveshare ESP32-P4-ETH", + "chip": "ESP32-P4", + "firmwares": [ + "esp32p4rev1-eth-es8311" + ], + "image": "assets/deviceModels/esp32-p4-eth.png", + "url": "https://www.waveshare.com/esp32-p4-eth.htm", + "supported": [ + "LEDs", + "Ethernet", + "Audio" + ], + "planned": [ + "WiFi", + "Camera", + "Speaker" + ], + "modules": [ + { + "type": "System", + "id": "System", + "controls": { + "deviceModel": "Waveshare ESP32-P4-ETH" + } + }, + { + "type": "ParallelLedDriver", + "id": "ParallelLed", + "parent_id": "Drivers", + "controls": { + "peripheral": "Parlio", + "pins": "20,21,22,23,24,25,26,27" + } + }, + { + "type": "NetworkModule", + "id": "Network", + "controls": { + "ethBoard": "ESP32-P4-ETH" + } + }, + { + "type": "AudioService", + "id": "Audio", + "parent_id": "Services", + "controls": { + "mode": 2, + "sckPin": 12, + "wsPin": 10, + "sdPin": 11 + } + }, + { + "type": "I2cScanModule", + "id": "I2cScan", + "controls": { + "sda": 7, + "scl": 8 + } + } + ] + }, { "name": "MHC-WLED ESP32-P4 shield", "chip": "ESP32-P4", diff --git a/mooninstaller/firmwares.json b/mooninstaller/firmwares.json index 3a892c40e..c7caf0037 100644 --- a/mooninstaller/firmwares.json +++ b/mooninstaller/firmwares.json @@ -70,6 +70,13 @@ "ships": true, "description": "Waveshare ESP32-P4-NANO — Ethernet + WiFi via the on-board ESP32-C6 over SDIO (esp_hosted), for P4 revisions 0.x/1.x ONLY. Boots and associates as of IDF v6.1-rc1 with CONFIG_PM_SLEEP_CLK_ICG_ENABLE=n, which sidesteps esp-idf #18759 (sleep_clock_icg_startup_init failing ESP_ERR_NO_MEM and aborting cpu_start)." }, + { + "name": "esp32p4rev1-eth-es8311", + "chip": "esp32p4", + "eth_only": true, + "ships": true, + "description": "Waveshare ESP32-P4-ETH — Ethernet only (IP101 PHY, same pins as the P4-NANO) plus the board's onboard ES8311 audio codec (I2C GPIO7/8, I2S GPIO9-13), for P4 revisions 0.x/1.x ONLY. No WiFi combo yet (no on-board C6 co-processor question has been resolved for this board); follows the same fragment-stacking pattern as esp32p4rev1-eth-wifi if one is added later." + }, { "name": "esp32p4rev3-eth", "chip": "esp32p4", diff --git a/src/core/system/NetworkModule.h b/src/core/system/NetworkModule.h index 8e7b96d12..b585ffbe1 100644 --- a/src/core/system/NetworkModule.h +++ b/src/core/system/NetworkModule.h @@ -546,6 +546,11 @@ class NetworkModule : public MoonModule { {"Classic RMII (no reset)", 1, 0, 23, 18, -1, 17, false, -1, -1, -1, -1, -1, false}, // Waveshare P4-NANO and the boards following its shield pinout: IP101, the clock fed in. {"P4-NANO", 2, 1, 31, 52, 51, 50, true, -1, -1, -1, -1, -1, false}, + // Waveshare ESP32-P4-ETH: same IP101 reference wiring as the P4-NANO (MDC/MDIO confirmed + // from the schematic netlist, reset/clock confirmed by direct schematic inspection), kept + // as its own entry rather than a silent alias so a future pin correction to one board + // can't also move the other. + {"ESP32-P4-ETH", 2, 1, 31, 52, 51, 50, true, -1, -1, -1, -1, -1, false}, // The S31's 1 Gb PHY, addressed by scan rather than by a fixed address. {"S31 CoreBoard", 4, -1, 5, 6, 7, -1, false, -1, -1, -1, -1, -1, false}, {"Custom", 0, -1, -1, -1, -1, -1, false, -1, -1, -1, -1, -1, true}, diff --git a/src/platform/esp32/platform_config.h b/src/platform/esp32/platform_config.h index c57b43f45..bd7cc58ba 100644 --- a/src/platform/esp32/platform_config.h +++ b/src/platform/esp32/platform_config.h @@ -163,6 +163,14 @@ struct AudioCodecPins { #ifdef CONFIG_IDF_TARGET_ESP32S31 constexpr CodecType audioCodecType = CodecType::Es8311; constexpr AudioCodecPins audioCodecPins = { /*sda*/ 51, /*scl*/ 50, /*mclk*/ 52, /*addr*/ 0x18 }; +// The Waveshare ESP32-P4-ETH's onboard ES8311: its own compile-time gate (CONFIG_MM_P4_ES8311, +// set only by the esp32p4rev1-eth-es8311 build), since most other P4 boards share this chip +// target but have no codec on the bus — see Kconfig.projbuild's MM_P4_ES8311 help text. Pins +// confirmed against the schematic (datasheet/ESP32-P4-ETH/) and the Waveshare wiki; a swapped +// SDA/SCL (the S31's documented gotcha) was bench-tested and ruled out — same failure either way. +#elif defined(CONFIG_MM_P4_ES8311) +constexpr CodecType audioCodecType = CodecType::Es8311; +constexpr AudioCodecPins audioCodecPins = { /*sda*/ 7, /*scl*/ 8, /*mclk*/ 13, /*addr*/ 0x18 }; #else constexpr CodecType audioCodecType = CodecType::None; constexpr AudioCodecPins audioCodecPins = { 0, 0, 0, 0 }; diff --git a/src/platform/esp32/platform_esp32_es8311.cpp b/src/platform/esp32/platform_esp32_es8311.cpp index 88424d1ff..66c4907b7 100644 --- a/src/platform/esp32/platform_esp32_es8311.cpp +++ b/src/platform/esp32/platform_esp32_es8311.cpp @@ -8,7 +8,7 @@ /// ## The domain code is unchanged /// /// It calls the codec init, a no-op on a board with a direct microphone, before the microphone init, and reads samples as always. -/// The driver is the vendor's own managed component, gated to the one board that needs it. +/// The driver is the vendor's own managed component, gated to the boards that need it. /// This is the layer's first master bus on that interface, owned here behind the boundary. /// Everything else gets an inert stub that reports success, having nothing to bring up, so the one call works everywhere. @@ -17,7 +17,9 @@ #include "sdkconfig.h" #include "soc/soc_caps.h" -// esp_codec_dev is only pulled on the S31 (idf_component.yml rule). Gate the codec implementation on its presence so the file still compiles on every other target. +// esp_codec_dev is pulled on the S31 (unconditionally) or on a P4 build with CONFIG_MM_P4_ES8311 set +// (the Waveshare ESP32-P4-ETH; see idf_component.yml's esp_codec_dev rules). Gate the codec +// implementation on its presence so the file still compiles on every other target. #if SOC_I2S_SUPPORTED && __has_include("esp_codec_dev.h") #define MM_HAS_ES8311 1 #endif @@ -42,6 +44,7 @@ struct CodecState { i2c_master_bus_handle_t i2cBus = nullptr; const audio_codec_ctrl_if_t* ctrl = nullptr; const audio_codec_if_t* codec = nullptr; + const audio_codec_data_if_t* data = nullptr; esp_codec_dev_handle_t dev = nullptr; }; @@ -52,6 +55,7 @@ void deinitState(CodecState* st) { if (!st) return; if (st->dev) { esp_codec_dev_close(st->dev); esp_codec_dev_delete(st->dev); } if (st->codec) audio_codec_delete_codec_if(st->codec); + if (st->data) audio_codec_delete_data_if(st->data); if (st->ctrl) audio_codec_delete_ctrl_if(st->ctrl); if (st->i2cBus) i2c_del_master_bus(st->i2cBus); delete st; @@ -84,7 +88,15 @@ bool audioCodecInit(CodecType type, const AudioCodecPins& pins, uint32_t sampleR // ES8311 over I2C, the codec's control interface (the I2S *data* interface is ours: audioMicInit owns the RX channel, so we don't hand esp_codec_dev the I2S handles or use esp_codec_dev_read; the codec just needs its registers set so it streams the ADC onto the bus). The mic path is record-only. audio_codec_i2c_cfg_t i2cCtrlCfg = {}; i2cCtrlCfg.port = I2C_NUM_0; - i2cCtrlCfg.addr = pins.i2cAddr; // ES8311 default 0x18 + // esp_codec_dev's I2C_If right-shifts this by 1 before handing it to the IDF i2c_master driver + // (audio_codec_ctrl_i2c.c: `.device_address = (i2c_cfg->addr >> 1)`), i.e. it wants the 8-bit + // write-address form, not the bare 7-bit address a scan/probe uses. Passing the bare 7-bit 0x18 + // straight through silently targets 0x0C instead — every register write NACKs ("ES8311: Open + // fail") even though a raw i2c_master_probe at 0x18 ACKs cleanly, since that path doesn't shift. + // Bench-found on the P4-ETH 2026-10-03; pins.i2cAddr stays the natural 7-bit value everywhere + // else (AudioCodecPins, I2cScanModule's own probe), so the <<1 lives here at the one call site + // that needs it. + i2cCtrlCfg.addr = static_cast(pins.i2cAddr << 1); i2cCtrlCfg.bus_handle = st->i2cBus; st->ctrl = audio_codec_new_i2c_ctrl(&i2cCtrlCfg); if (!st->ctrl) { ESP_LOGE(ES_TAG, "codec i2c ctrl failed"); deinitState(st); return false; } @@ -99,8 +111,18 @@ bool audioCodecInit(CodecType type, const AudioCodecPins& pins, uint32_t sampleR st->codec = es8311_codec_new(&es8311Cfg); if (!st->codec) { ESP_LOGE(ES_TAG, "es8311_codec_new failed"); deinitState(st); return false; } + // esp_codec_dev_new requires a non-null data_if even though we never call esp_codec_dev_read: + // audioMicInit owns the real RX channel, so this one's rx/tx handles stay null (the I2S_If + // guards every call on the handle being set and the two calls esp_codec_dev_open makes through + // it — set_fmt/enable — both discard their return value), existing purely to satisfy that check. + audio_codec_i2s_cfg_t i2sDataCfg = {}; + i2sDataCfg.port = 0; + st->data = audio_codec_new_i2s_data(&i2sDataCfg); + if (!st->data) { ESP_LOGE(ES_TAG, "codec i2s data_if failed"); deinitState(st); return false; } + esp_codec_dev_cfg_t devCfg = {}; devCfg.codec_if = st->codec; + devCfg.data_if = st->data; devCfg.dev_type = ESP_CODEC_DEV_TYPE_IN; // input (mic) device st->dev = esp_codec_dev_new(&devCfg); if (!st->dev) { ESP_LOGE(ES_TAG, "esp_codec_dev_new failed"); deinitState(st); return false; }