From dfb003d2f7c6de90cb3d64d1142ad5a66ff034f9 Mon Sep 17 00:00:00 2001 From: cjkas Date: Thu, 29 Oct 2026 19:41:39 +0100 Subject: [PATCH 1/2] build: add the ESP32-P4 (#29) For boards such as Waveshare's ESP32-P4-WIFI6-POE-ETH: WiFi through the board's ESP32-C6 on SDIO (esp-hosted), Ethernet on the P4's RMII MAC with an IP101. Not tested on a device: no P4 board was at hand. - platformio.ini: env esp32p4 (esp32-p4_r3, chip v3.01+) and esp32p4es (esp32-p4, the esp32p4_es libraries, chips before v2.0). An image runs on one of the two only, so each has its own release files, chip models "p4" and "p4es". Both keep the OTA slot: at level 2 they leave about 128 KB of it. - Ethernet clock mode: stored as eth_clock_id_t in the ESP32's numbering and translated by name, like the PHY id. The P4's eth_clock_mode_t is the IDF's emac_rmii_clock_mode_t, whose 0 is a deprecated default; only clock in (GPIO 50) translates there. - P4 Ethernet defaults: the Waveshare wiring (IP101 as TLK110, address 1, MDC 31, MDIO 52, reset 51), board type 8. The ESP32's put MDIO on an SDIO pin. - Settings take pins up to GPIO 54, the P4's last, instead of 48. - Radio defaults: SPI 32/33/27, CSN 46 (the framework's Waveshare variant), TX 2, RX 3. - Web UI: a Waveshare ESP32-P4-ETH preset, the P4's clock mode, pin lists up to GPIO 54 without the pins the C6 link, Ethernet, USB and UART0 use. - CI builds and releases both, with their ELFs; bootloader at 0x2000. - ADR 0003: amended. Co-Authored-By: Claude Opus 5.5 --- .github/ISSUE_TEMPLATE/BUG-REPORT.yaml | 2 +- .github/workflows/build.yaml | 20 +++++++- data-src/index.js | 38 ++++++++++---- docs/adr/0003-pioarduino-idf55.md | 5 +- platformio.ini | 20 ++++++++ src/ConfigFile.cpp | 4 +- src/ConfigSettings.cpp | 21 +++++--- src/ConfigSettings.h | 14 +++++- src/DeviceOps.cpp | 2 +- src/ESPNetwork.cpp | 5 +- src/EthPhy.h | 45 ++++++++++++++++- src/ProtoSettings.cpp | 19 +++---- src/Transceiver.cpp | 15 ++++++ src/Utils.h | 2 +- test/stubs/ETH.h | 16 ++++++ test/test_ethphy/test_ethphy.cpp | 37 ++++++++++++++ test/test_ethphy_p4/test_ethphy_p4.cpp | 49 +++++++++++++++++++ .../test_protosettings/test_protosettings.cpp | 2 +- test/test_utils/test_utils.cpp | 4 ++ 19 files changed, 282 insertions(+), 38 deletions(-) create mode 100644 test/test_ethphy_p4/test_ethphy_p4.cpp diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yaml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yaml index 0889781..78b9eaa 100644 --- a/.github/ISSUE_TEMPLATE/BUG-REPORT.yaml +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yaml @@ -72,7 +72,7 @@ body: > After a crash, include the whole panic: the `Guru Meditation Error` line, the `Backtrace:` line and the `ELF file SHA256:` line. > Each release has a `SomfyController.ino..elf.zip` that decodes the backtrace: > `xtensa-esp32s3-elf-addr2line -pfiaC -e SomfyController.ino.esp32s3.elf ` - > (`riscv32-esp-elf-addr2line` on the C3, C5 and C6). + > (`riscv32-esp-elf-addr2line` on the C3, C5, C6 and P4). render: bash validations: required: false \ No newline at end of file diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 704dd7e..b80aa1f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -61,7 +61,7 @@ jobs: # elfname: the symbols of that binary, to decode a crash's backtrace # obname: onboard image (bootloader + partitions + firmware + littlefs merged) # for flashing a new chip via USB/serial - # addr_bootloader: chip-dependent (ESP32: 0x1000, S3/C3/C6: 0x0, C5: 0x2000) + # addr_bootloader: chip-dependent (ESP32: 0x1000, S3/C3/C6: 0x0, C5/P4: 0x2000) # addr_fs: must match the spiffs offset in the env's partition table # (0x310000 in both esp32_3MB.csv and esp32_huge_app.csv) include: @@ -110,6 +110,24 @@ jobs: elfname: SomfyController.ino.esp32c6.elf fsname: SomfyController.littlefs.esp32c6.bin obname: SomfyController.onboard.esp32c6.bin + - env: esp32p4 + name: ESP32-P4 + chip: ESP32-P4 + addr_bootloader: "0x2000" + addr_fs: "0x310000" + fwname: SomfyController.ino.esp32p4.bin + elfname: SomfyController.ino.esp32p4.elf + fsname: SomfyController.littlefs.esp32p4.bin + obname: SomfyController.onboard.esp32p4.bin + - env: esp32p4es + name: ESP32-P4 (before v3.0) + chip: ESP32-P4 + addr_bootloader: "0x2000" + addr_fs: "0x310000" + fwname: SomfyController.ino.esp32p4es.bin + elfname: SomfyController.ino.esp32p4es.elf + fsname: SomfyController.littlefs.esp32p4es.bin + obname: SomfyController.onboard.esp32p4es.bin steps: - name: Get Release diff --git a/data-src/index.js b/data-src/index.js index cecd83c..5efa3e6 100644 --- a/data-src/index.js +++ b/data-src/index.js @@ -1578,23 +1578,22 @@ class Wifi { { val: 4, label: 'T-Internet POE - LILYGO', clk: 3, ct: 0, addr: 0, pwr: 16, mdc: 23, mdio: 18 }, { val: 5, label: 'wESP32 v7+ - Silicognition', clk: 0, ct: 2, addr: 0, pwr: -1, mdc: 16, mdio: 17 }, { val: 6, label: 'wESP32 < v7 - Silicognition', clk: 0, ct: 0, addr: 0, pwr: -1, mdc: 16, mdio: 17 }, - { val: 1, label: 'WT32-ETH01 - Wireless Tag', clk: 0, ct: 0, addr: 1, pwr: 16, mdc: 23, mdio: 18 } + { val: 1, label: 'WT32-ETH01 - Wireless Tag', clk: 0, ct: 0, addr: 1, pwr: 16, mdc: 23, mdio: 18 }, + { val: 8, label: 'ESP32-P4-ETH - Waveshare', clk: 0, ct: 1, addr: 1, pwr: 51, mdc: 31, mdio: 52, p4: true } ]; ethClockModes = [{ val: 0, label: 'GPIO0 IN' }, { val: 1, label: 'GPIO0 OUT' }, { val: 2, label: 'GPIO16 OUT' }, { val: 3, label: 'GPIO17 OUT' }]; + // The P4 takes the clock in on GPIO 50 only (eth_clock_id_t in EthPhy.h). + ethClockModesP4 = [{ val: 0, label: 'GPIO50 IN' }]; // The ids the firmware stores (eth_phy_id_t in EthPhy.h). 4 is the DM9051, an SPI chip the ESP32's MAC can't drive. ethPhyTypes = [{ val: 0, label: 'LAN8720' }, { val: 1, label: 'TLK110' }, { val: 2, label: 'RTL8201' }, { val: 3, label: 'DP83848' }, { val: 5, label: 'KSZ8041' }, { val: 6, label: 'KSZ8081' }]; init() { document.getElementById("divNetworkStrength").innerHTML = this.displaySignal(-100); if (this.initialized) return; let addr = []; - this.loadETHDropdown(document.getElementById('selETHClkMode'), this.ethClockModes); this.loadETHDropdown(document.getElementById('selETHPhyType'), this.ethPhyTypes); - this.loadETHDropdown(document.getElementById('selETHBoardType'), this.ethBoardTypes); for (let i = 0; i < 32; i++) addr.push({ val: i, label: `PHY ${i}` }); this.loadETHDropdown(document.getElementById('selETHAddress'), addr); - this.loadETHPins(document.getElementById('selETHPWRPin'), 'power'); - this.loadETHPins(document.getElementById('selETHMDCPin'), 'mdc', 23); - this.loadETHPins(document.getElementById('selETHMDIOPin'), 'mdio', 18); + this.loadETHOptions(); ui.toElement(document.getElementById('divNetAdapter'), { wifi: {ssid:'', passphrase:''}, ethernet: { boardType: 1, wirelessFallback: false, dhcp: true, dns1: '', dns2: '', ip: '', gateway: '' } @@ -1602,14 +1601,27 @@ class Wifi { this.onETHBoardTypeChanged(document.getElementById('selETHBoardType')); this.initialized = true; } - loadETHPins(sel, type, selected) { + isP4() { return (document.getElementById('divContainer').getAttribute('data-chipmodel') || '').startsWith('p4'); } + clockModes() { return this.isP4() ? this.ethClockModesP4 : this.ethClockModes; } + // The board presets, clock modes and pins of the chip: the ESP32's or the + // P4's. The chip comes with the sync, after init(). + loadETHOptions() { + let p4 = this.isP4(); + this.loadETHDropdown(document.getElementById('selETHClkMode'), this.clockModes()); + this.loadETHDropdown(document.getElementById('selETHBoardType'), this.ethBoardTypes.filter(b => b.val === 0 || (b.p4 === true) === p4)); + let maxPin = p4 ? 54 : 35; + this.loadETHPins(document.getElementById('selETHPWRPin'), 'power', undefined, maxPin); + this.loadETHPins(document.getElementById('selETHMDCPin'), 'mdc', p4 ? 31 : 23, maxPin); + this.loadETHPins(document.getElementById('selETHMDIOPin'), 'mdio', p4 ? 52 : 18, maxPin); + } + loadETHPins(sel, type, selected, maxPin) { let arr = []; switch (type) { case 'power': arr.push({ val: -1, label: 'None' }); break; } - for (let i = 0; i < 36; i++) { + for (let i = 0; i <= maxPin; i++) { arr.push({ val: i, label: `GPIO ${i}` }); } this.loadETHDropdown(sel, arr, selected); @@ -1652,6 +1664,7 @@ class Wifi { else { let settings = ProtoViews.network(proto.schema, reply); console.log(settings); + this.loadETHOptions(); let hasEthernet = settings.hasEthernet; this.hasEthernet = hasEthernet; document.getElementById('divHardwired').style.display = hasEthernet ? '' : 'none'; @@ -1815,7 +1828,7 @@ class Wifi { if (obj.connType >= 2) { let boardType = this.ethBoardTypes.find(elem => obj.ethernet.boardType === elem.val); let phyType = this.ethPhyTypes.find(elem => obj.ethernet.phyType === elem.val); - let clkMode = this.ethClockModes.find(elem => obj.ethernet.CLKMode === elem.val); + let clkMode = this.clockModes().find(elem => obj.ethernet.CLKMode === elem.val); let div = document.createElement('div'); let html = `
`; html += '
BEWARE ... WARNING ... DANGER
'; @@ -2720,12 +2733,17 @@ class Somfy { { name: 's3', maxPins: 48, inputs: [19, 20, 22, 23, 24, 25, 27, 28, 29, 30, 31, 32], outputs: [19, 20, 22, 23, 24, 25, 27, 28, 29, 30, 31, 32] }, { name: 'c3', maxPins: 21, inputs: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20], outputs: [11, 12, 13, 14, 15, 16, 17, 21] }, { name: 'c5', maxPins: 28, inputs: [13, 14, 15, 16, 17, 18, 19, 20, 21, 22], outputs: [13, 14, 15, 16, 17, 18, 19, 20, 21, 22] }, - { name: 'c6', maxPins: 23, inputs: [], outputs: [] } + { name: 'c6', maxPins: 23, inputs: [], outputs: [] }, + // Without the C6's SDIO link (14-19, 54), USB Serial/JTAG (24, 25), + // Ethernet (28-31, 34, 35, 49-52) and UART0 (37, 38). + { name: 'p4', maxPins: 54, inputs: [14, 15, 16, 17, 18, 19, 24, 25, 28, 29, 30, 31, 34, 35, 37, 38, 49, 50, 51, 52, 54], outputs: [14, 15, 16, 17, 18, 19, 24, 25, 28, 29, 30, 31, 34, 35, 37, 38, 49, 50, 51, 52, 54] } ]; loadPins(type, sel, opt) { while (sel.firstChild) sel.removeChild(sel.firstChild); let cm = document.getElementById('divContainer').getAttribute('data-chipmodel'); + // The pre-v3 P4 build has the same pins. + if (cm === 'p4es') cm = 'p4'; let pm = this.pinMaps.find(x => x.name === cm) || { name: '', maxPins: 39, inputs: [], outputs: [] }; //console.log({ cm: cm, pm: pm }); for (let i = 0; i <= pm.maxPins; i++) { diff --git a/docs/adr/0003-pioarduino-idf55.md b/docs/adr/0003-pioarduino-idf55.md index fb87a35..00f4177 100644 --- a/docs/adr/0003-pioarduino-idf55.md +++ b/docs/adr/0003-pioarduino-idf55.md @@ -15,7 +15,8 @@ We moved every firmware env from `espressif32 @ 6.13.0` (Arduino 2.0.17 on ESP-I > **Amended by [ADR 0006](0006-two-security-modes-eleven-http-routes.md) (4.0.0).** The C3 builds again. Without the old API (ports 8080 and 8081), its image is 1,498,512 bytes at level 2 and leaves 72.6 KB of the OTA slot. It runs on chip revision v0.3 or later only: the framework's C3 libraries are built with `CONFIG_ESP32C3_REV_MIN_3`. Its default radio pins moved off GPIO 12-17, which hold the SPI flash. - **The C5 is updated over USB.** New in 4.0.0. At level 2 its image left 3.4 KB of the OTA slot, under CI's 8 KB, so like the C6 it is released as a debug build with one 3 MB app slot (`esp32_huge_app.csv`), which no OTA can write. The framework's C5 libraries run on chip revision v1.0 or later only (`CONFIG_ESP32C5_REV_MIN_FULL=100`). Its WiFi has 5 GHz too, so the passive scans for the configured network (`src/ESPNetwork.cpp`, 300 ms a channel) cover about 38 channels instead of 13: about 11 s instead of 4 s, depending on the country's channels. -- **Ethernet PHY ids are ours.** Arduino 3.x renumbered `eth_phy_type_t` (`ETH_PHY_GENERIC` is 0). NVS, backups and the web UI keep the Arduino 2.x numbers as `eth_phy_id_t` (`src/EthPhy.h`), translated by name when Ethernet starts. Never cast a stored id to the framework enum. -- **Ethernet only on the original ESP32.** Arduino 3.x has the RMII MAC API only where `CONFIG_ETH_USE_ESP32_EMAC` is set. S3, C3, C5 and C6 build without Ethernet. +- **The P4 has two builds.** New in 4.0.0, for boards such as Waveshare's ESP32-P4-WIFI6-POE-ETH. The framework's P4 libraries run on chip revision v3.01 or later only (`CONFIG_ESP32P4_REV_MIN_FULL=301`), its `esp32p4_es` libraries on revisions before v2.0, so `esp32p4` (chip model `p4`) and `esp32p4es` (`p4es`) each get their own release files. Both keep the OTA slot: at level 2 their images leave about 128 KB. WiFi goes through the board's ESP32-C6 on SDIO (esp-hosted, the framework's pins 14-19 and reset 54), so the C6 has to run a matching esp-hosted slave firmware, which Waveshare's boards ship with. +- **Ethernet PHY ids are ours.** Arduino 3.x renumbered `eth_phy_type_t` (`ETH_PHY_GENERIC` is 0). NVS, backups and the web UI keep the Arduino 2.x numbers as `eth_phy_id_t` (`src/EthPhy.h`), translated by name when Ethernet starts. Never cast a stored id to the framework enum. The RMII clock mode is kept the same way, as `eth_clock_id_t` in the ESP32's numbering: the P4's `eth_clock_mode_t` is the IDF's `emac_rmii_clock_mode_t`, whose 0 is a deprecated default. On the P4 only clock in (GPIO 50) translates, since `ETH.begin()` keeps the clock on that pin. +- **Ethernet only on the original ESP32 and the P4.** Arduino 3.x has the RMII MAC API only where `CONFIG_ETH_USE_ESP32_EMAC` is set. S3, C3, C5 and C6 build without Ethernet. - **Updates roll back until proven.** We override the core's `verifyRollbackLater()` hook to return true, and `loop()` marks the new firmware valid once it has run 60 s and the network is up. A reset before then boots the previous firmware. The LittleFS image an update wrote stays, so the old firmware runs with the new web UI. - **Tooling.** pioarduino needs its own Core fork (`pip install pioarduino`, the pioarduino IDE extension in VS Code), which it installs into `~/.platformio/penv` in place of PlatformIO Core. diff --git a/platformio.ini b/platformio.ini index 1803eb4..928c322 100644 --- a/platformio.ini +++ b/platformio.ini @@ -126,6 +126,26 @@ build_flags = ${esp32.build_flags} -DCORE_DEBUG_LEVEL=3 +; ESP32-P4 boards such as Waveshare's ESP32-P4-WIFI6-POE-ETH: WiFi through an +; ESP32-C6 on SDIO (esp-hosted), Ethernet on the P4's own RMII MAC. The +; framework's P4 libraries run on chip revision v3.01 or later only, and its +; esp32p4_es libraries on revisions before v2.0, so each gets its own build. +[env:esp32p4] +extends = esp32 +board = esp32-p4_r3 +board_build.partitions = esp32_3MB.csv +build_flags = + ${dualcore.build_flags} + -DCORE_DEBUG_LEVEL=2 + +[env:esp32p4es] +extends = esp32 +board = esp32-p4 +board_build.partitions = esp32_3MB.csv +build_flags = + ${dualcore.build_flags} + -DCORE_DEBUG_LEVEL=2 + ; Host-side unit tests: pio test -e native (needs gcc/g++ on PATH). ; Only the sources the tests cover are built; test/stubs stands in for Arduino.h. [env:native] diff --git a/src/ConfigFile.cpp b/src/ConfigFile.cpp index 27926cd..8b8445c 100644 --- a/src/ConfigFile.cpp +++ b/src/ConfigFile.cpp @@ -149,7 +149,7 @@ static net_record_t netRecord() { #if HAS_ETHERNET rec.ethBoardType = settings.Ethernet.boardType; rec.ethPhyType = settings.Ethernet.phyType; - rec.ethClkMode = static_cast(settings.Ethernet.CLKMode); + rec.ethClkMode = settings.Ethernet.CLKMode; rec.ethPhyAddress = settings.Ethernet.phyAddress; rec.ethPWRPin = settings.Ethernet.PWRPin; rec.ethMDCPin = settings.Ethernet.MDCPin; @@ -196,7 +196,7 @@ static void applyNetRecord(const net_record_t &rec, const restore_options_t &opt ESP_LOGI(TAG, "Restoring Ethernet adapter settings"); settings.Ethernet.boardType = rec.ethBoardType; settings.Ethernet.phyType = rec.ethPhyType; - settings.Ethernet.CLKMode = static_cast(rec.ethClkMode); + settings.Ethernet.CLKMode = rec.ethClkMode; settings.Ethernet.phyAddress = rec.ethPhyAddress; settings.Ethernet.PWRPin = rec.ethPWRPin; settings.Ethernet.MDCPin = rec.ethMDCPin; diff --git a/src/ConfigSettings.cpp b/src/ConfigSettings.cpp index 84fe3f8..a95cd01 100644 --- a/src/ConfigSettings.cpp +++ b/src/ConfigSettings.cpp @@ -145,6 +145,17 @@ bool ConfigSettings::begin() { strcpy(this->chipModel, "c6"); break; #endif + #ifdef CONFIG_IDF_TARGET_ESP32P4 + // Chips before v3.0 run their own build (esp32p4_es libraries), so they + // get their own release files. + case esp_chip_model_t::CHIP_ESP32P4: + #if CONFIG_ESP32P4_SELECTS_REV_LESS_V3 + strcpy(this->chipModel, "p4es"); + #else + strcpy(this->chipModel, "p4"); + #endif + break; + #endif default: sprintf(this->chipModel, "UNK%d", static_cast(ci.model)); break; @@ -499,7 +510,7 @@ bool EthernetSettings::begin() { bool EthernetSettings::fromJSON(JsonObject &obj) { if(!obj["boardType"].isNull()) this->boardType = obj["boardType"]; if(!obj["phyAddress"].isNull()) this->phyAddress = obj["phyAddress"]; - if(!obj["CLKMode"].isNull()) this->CLKMode = static_cast(obj["CLKMode"]); + if(!obj["CLKMode"].isNull()) this->CLKMode = obj["CLKMode"]; if(!obj["phyType"].isNull()) this->phyType = obj["phyType"]; if(!obj["PWRPin"].isNull()) this->PWRPin = obj["PWRPin"]; if(!obj["MDCPin"].isNull()) this->MDCPin = obj["MDCPin"]; @@ -507,9 +518,7 @@ bool EthernetSettings::fromJSON(JsonObject &obj) { return true; } bool EthernetSettings::usesPin(uint8_t pin) { - if((this->CLKMode == 0 || this->CLKMode == 1) && pin == 0) return true; - else if(this->CLKMode == 2 && pin == 16) return true; - else if(this->CLKMode == 3 && pin == 17) return true; + if(ethClockPin(this->CLKMode) == pin) return true; else if(this->PWRPin == pin) return true; else if(this->MDCPin == pin) return true; else if(this->MDIOPin == pin) return true; @@ -522,7 +531,7 @@ bool EthernetSettings::save() { pref.putChar("boardType", this->boardType); pref.putChar("phyAddress", this->phyAddress); pref.putChar("phyType", this->phyType); - pref.putChar("CLKMode", static_cast(this->CLKMode)); + pref.putChar("CLKMode", this->CLKMode); pref.putChar("PWRPin", this->PWRPin); pref.putChar("MDCPin", this->MDCPin); pref.putChar("MDIOPin", this->MDIOPin); @@ -534,7 +543,7 @@ bool EthernetSettings::load() { pref.begin("ETH"); this->boardType = pref.getChar("boardType", this->boardType); this->phyType = pref.getChar("phyType", this->phyType); - this->CLKMode = static_cast(pref.getChar("CLKMode", ETH_CLOCK_GPIO0_IN)); + this->CLKMode = pref.getChar("CLKMode", this->CLKMode); this->phyAddress = pref.getChar("phyAddress", this->phyAddress); this->PWRPin = pref.getChar("PWRPin", this->PWRPin); this->MDCPin = pref.getChar("MDCPin", this->MDCPin); diff --git a/src/ConfigSettings.h b/src/ConfigSettings.h index 9c6689b..0effc5a 100644 --- a/src/ConfigSettings.h +++ b/src/ConfigSettings.h @@ -88,14 +88,26 @@ class WifiSettings: BaseSettings { class EthernetSettings: BaseSettings { public: EthernetSettings(); + uint8_t CLKMode = static_cast(eth_clock_id_t::GPIO0_IN); // An eth_clock_id_t, see EthPhy.h. +#if defined(CONFIG_IDF_TARGET_ESP32P4) + // The IP101 wiring of Espressif's P4 boards, which Waveshare's ESP32-P4-ETH + // boards copy, and the web UI's board type for it. The ESP32's defaults + // would put MDIO on an SDIO pin. + uint8_t boardType = 8; + uint8_t phyType = static_cast(eth_phy_id_t::TLK110); + int8_t phyAddress = 1; + int8_t PWRPin = 51; + int8_t MDCPin = 31; + int8_t MDIOPin = 52; +#else uint8_t boardType = 0; // These board types are enumerated in the ui and used to set the chip settings. uint8_t phyType = static_cast(eth_phy_id_t::LAN8720); // An eth_phy_id_t, see EthPhy.h. - eth_clock_mode_t CLKMode = ETH_CLOCK_GPIO0_IN; // The LAN8720 wiring Arduino 2.x defaulted to. 3.x has no defaults. int8_t phyAddress = 0; int8_t PWRPin = -1; int8_t MDCPin = 23; int8_t MDIOPin = 18; +#endif bool begin(); bool fromJSON(JsonObject &obj); diff --git a/src/DeviceOps.cpp b/src/DeviceOps.cpp index 777b0aa..d0a60b0 100644 --- a/src/DeviceOps.cpp +++ b/src/DeviceOps.cpp @@ -62,7 +62,7 @@ static void writeSettings(ProtoWriter &w, settings_section_t section) { w.add(proto_field_t::hasEthernet, true); w.add(proto_field_t::boardType, settings.Ethernet.boardType); w.add(proto_field_t::phyType, settings.Ethernet.phyType); - w.add(proto_field_t::CLKMode, static_cast(settings.Ethernet.CLKMode)); + w.add(proto_field_t::CLKMode, settings.Ethernet.CLKMode); w.add(proto_field_t::phyAddress, settings.Ethernet.phyAddress); w.add(proto_field_t::PWRPin, settings.Ethernet.PWRPin); w.add(proto_field_t::MDCPin, settings.Ethernet.MDCPin); diff --git a/src/ESPNetwork.cpp b/src/ESPNetwork.cpp index 28fce1e..43eb53f 100644 --- a/src/ESPNetwork.cpp +++ b/src/ESPNetwork.cpp @@ -356,11 +356,14 @@ bool ESPNetwork::connectWired() { ETH.setHostname("ESPSomfy-RTS"); ESP_LOGD(TAG, "Set hostname to: %s", ETH.getHostname()); eth_phy_type_t phyType; + eth_clock_mode_t clkMode; bool begun = false; if(!ethPhyType(settings.Ethernet.phyType, phyType)) ESP_LOGE(TAG, "Ethernet PHY type %u is not supported", (unsigned int)settings.Ethernet.phyType); + else if(!ethClockMode(settings.Ethernet.CLKMode, clkMode)) + ESP_LOGE(TAG, "Ethernet clock mode %u is not supported", (unsigned int)settings.Ethernet.CLKMode); else - begun = ETH.begin(phyType, settings.Ethernet.phyAddress, settings.Ethernet.MDCPin, settings.Ethernet.MDIOPin, settings.Ethernet.PWRPin, settings.Ethernet.CLKMode); + begun = ETH.begin(phyType, settings.Ethernet.phyAddress, settings.Ethernet.MDCPin, settings.Ethernet.MDIOPin, settings.Ethernet.PWRPin, clkMode); if(!begun) { ESP_LOGE(TAG, "Ethernet Begin failed"); this->ethStarted = false; diff --git a/src/EthPhy.h b/src/EthPhy.h index 004b5b4..5f2f608 100644 --- a/src/EthPhy.h +++ b/src/EthPhy.h @@ -5,8 +5,8 @@ #include #endif -// Ethernet needs the RMII MAC, which only the original ESP32 has. The C3, C5, -// S3 and C6 build without it. +// Ethernet needs the RMII MAC, which only the original ESP32 and the P4 have. +// The C3, C5, S3 and C6 build without it. #if defined(CONFIG_ETH_USE_ESP32_EMAC) && CONFIG_ETH_USE_ESP32_EMAC #define HAS_ETHERNET 1 #else @@ -26,6 +26,32 @@ enum class eth_phy_id_t : uint8_t { KSZ8081 = 6 }; +// RMII clock ids as stored in NVS and backups and sent to the web UI: the +// original ESP32's eth_clock_mode_t values. The P4's eth_clock_mode_t is the +// IDF's emac_rmii_clock_mode_t, a different enum, so an id is translated. +enum class eth_clock_id_t : uint8_t { + GPIO0_IN = 0, // The clock comes in. On the P4, on GPIO 50. + GPIO0_OUT = 1, + GPIO16_OUT = 2, + GPIO17_OUT = 3 +}; + +// The GPIO a stored clock id puts the RMII clock on, -1 for an id the chip +// can't use. The P4's ETH.begin() takes the clock in on GPIO 50 only. +inline int8_t ethClockPin(uint8_t id) { +#if defined(CONFIG_IDF_TARGET_ESP32P4) + return static_cast(id) == eth_clock_id_t::GPIO0_IN ? 50 : -1; +#else + switch(static_cast(id)) { + case eth_clock_id_t::GPIO0_IN: + case eth_clock_id_t::GPIO0_OUT: return 0; + case eth_clock_id_t::GPIO16_OUT: return 16; + case eth_clock_id_t::GPIO17_OUT: return 17; + default: return -1; + } +#endif +} + #if HAS_ETHERNET #include // Sets type to the framework's PHY for a stored id. False for an id the RMII @@ -41,5 +67,20 @@ inline bool ethPhyType(uint8_t id, eth_phy_type_t &type) { default: return false; } } +// Sets mode to the framework's clock mode for a stored id. False for an id +// the chip can't use. +inline bool ethClockMode(uint8_t id, eth_clock_mode_t &mode) { + switch(static_cast(id)) { +#if defined(CONFIG_IDF_TARGET_ESP32P4) + case eth_clock_id_t::GPIO0_IN: mode = EMAC_CLK_EXT_IN; return true; +#else + case eth_clock_id_t::GPIO0_IN: mode = ETH_CLOCK_GPIO0_IN; return true; + case eth_clock_id_t::GPIO0_OUT: mode = ETH_CLOCK_GPIO0_OUT; return true; + case eth_clock_id_t::GPIO16_OUT: mode = ETH_CLOCK_GPIO16_OUT; return true; + case eth_clock_id_t::GPIO17_OUT: mode = ETH_CLOCK_GPIO17_OUT; return true; +#endif + default: return false; + } +} #endif #endif diff --git a/src/ProtoSettings.cpp b/src/ProtoSettings.cpp index e1b6c18..09fad81 100644 --- a/src/ProtoSettings.cpp +++ b/src/ProtoSettings.cpp @@ -47,9 +47,10 @@ static const settings_field_t FIELDS[] = { {settings_part_t::ethernet, proto_field_t::phyType, nullptr, 0, 255}, {settings_part_t::ethernet, proto_field_t::CLKMode, nullptr, 0, 3}, {settings_part_t::ethernet, proto_field_t::phyAddress, nullptr, -1, 31}, - {settings_part_t::ethernet, proto_field_t::PWRPin, nullptr, -1, 48}, - {settings_part_t::ethernet, proto_field_t::MDCPin, nullptr, 0, 48}, - {settings_part_t::ethernet, proto_field_t::MDIOPin, nullptr, 0, 48}, + // Pins run to GPIO 54, the P4's last. + {settings_part_t::ethernet, proto_field_t::PWRPin, nullptr, -1, 54}, + {settings_part_t::ethernet, proto_field_t::MDCPin, nullptr, 0, 54}, + {settings_part_t::ethernet, proto_field_t::MDIOPin, nullptr, 0, 54}, {settings_part_t::ip, proto_field_t::dhcp, nullptr, 0, 0}, {settings_part_t::ip, proto_field_t::ip, nullptr, 0, 15}, {settings_part_t::ip, proto_field_t::gateway, nullptr, 0, 15}, @@ -75,12 +76,12 @@ static const settings_field_t FIELDS[] = { // RTS, RTW, RTV, GP Relay or GP Remote (radio_proto), checked on its own. {settings_part_t::radio, proto_field_t::proto, nullptr, 0, 9}, {settings_part_t::radio, proto_field_t::enabled, nullptr, 0, 0}, - {settings_part_t::radio, proto_field_t::TXPin, nullptr, 0, 48}, - {settings_part_t::radio, proto_field_t::RXPin, nullptr, 0, 48}, - {settings_part_t::radio, proto_field_t::SCKPin, nullptr, 0, 48}, - {settings_part_t::radio, proto_field_t::MOSIPin, nullptr, 0, 48}, - {settings_part_t::radio, proto_field_t::MISOPin, nullptr, 0, 48}, - {settings_part_t::radio, proto_field_t::CSNPin, nullptr, 0, 48}, + {settings_part_t::radio, proto_field_t::TXPin, nullptr, 0, 54}, + {settings_part_t::radio, proto_field_t::RXPin, nullptr, 0, 54}, + {settings_part_t::radio, proto_field_t::SCKPin, nullptr, 0, 54}, + {settings_part_t::radio, proto_field_t::MOSIPin, nullptr, 0, 54}, + {settings_part_t::radio, proto_field_t::MISOPin, nullptr, 0, 54}, + {settings_part_t::radio, proto_field_t::CSNPin, nullptr, 0, 54}, // What the CC1101 can be set to, in MHz and kHz. {settings_part_t::radio, proto_field_t::frequency, nullptr, 30000, 92800}, {settings_part_t::radio, proto_field_t::rxBandwidth, nullptr, 5803, 81250}, diff --git a/src/Transceiver.cpp b/src/Transceiver.cpp index c66280c..9b07cd0 100644 --- a/src/Transceiver.cpp +++ b/src/Transceiver.cpp @@ -506,6 +506,21 @@ void transceiver_config_t::load() { this->SCKPin = 6; this->CSNPin = 0; break; +#endif +#ifdef CONFIG_IDF_TARGET_ESP32P4 + case esp_chip_model_t::CHIP_ESP32P4: + // The SPI pins of the framework's variant for Waveshare's ESP32-P4-ETH + // (variants/waveshare_p4_poe_eth); TX and RX take 2 and 3, which that + // board leaves free. The C6's SDIO link holds 14-19 and 54, Ethernet + // 28-31, 34, 35 and 49-52, and 34-38 are strapping pins. 46 is on LDO + // channel 4, which the core turns on when a pin of it is used. + this->TXPin = 2; + this->RXPin = 3; + this->MOSIPin = 32; + this->MISOPin = 33; + this->SCKPin = 27; + this->CSNPin = 46; + break; #endif default: this->TXPin = 13; diff --git a/src/Utils.h b/src/Utils.h index ffd3483..a134453 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -85,7 +85,7 @@ const char *wifiAuthModeName(uint8_t authMode); bool otaProven(bool networkUp, unsigned long uptimeMs, unsigned long minUptimeMs); // A release asset's name: "SomfyController..esp32.bin", kind // "ino" for the firmware or "littlefs" for the web UI image, chipModel as in -// ConfigSettings ("" for the ESP32, "s3", "c3", "c5", "c6"). +// ConfigSettings ("" for the ESP32, "s3", "c3", "c5", "c6", "p4", "p4es"). void releaseAssetName(char *buf, size_t size, const char *kind, const char *chipModel); // A coredump backtrace as " 0x%08x" per frame. bt holds maxFrames entries and // depth is how many the dump claims are valid. diff --git a/test/stubs/ETH.h b/test/stubs/ETH.h index c974957..a5e50b9 100644 --- a/test/stubs/ETH.h +++ b/test/stubs/ETH.h @@ -13,4 +13,20 @@ typedef enum { ETH_PHY_KSZ8081, ETH_PHY_MAX } eth_phy_type_t; +#if defined(CONFIG_IDF_TARGET_ESP32P4) +// The P4's ETH.h takes the IDF's enum, where 0 is a deprecated default. +typedef enum { + EMAC_CLK_DEFAULT, + EMAC_CLK_EXT_IN, + EMAC_CLK_OUT +} emac_rmii_clock_mode_t; +typedef emac_rmii_clock_mode_t eth_clock_mode_t; +#else +typedef enum { + ETH_CLOCK_GPIO0_IN, + ETH_CLOCK_GPIO0_OUT, + ETH_CLOCK_GPIO16_OUT, + ETH_CLOCK_GPIO17_OUT +} eth_clock_mode_t; +#endif #endif diff --git a/test/test_ethphy/test_ethphy.cpp b/test/test_ethphy/test_ethphy.cpp index 08228d2..b86b5eb 100644 --- a/test/test_ethphy/test_ethphy.cpp +++ b/test/test_ethphy/test_ethphy.cpp @@ -52,6 +52,39 @@ static void test_rejects_unknown_ids() { TEST_ASSERT_FALSE(ethPhyType(255, type)); } +/********************************************************************* + * Clock modes + ********************************************************************/ +static void test_clock_ids_match_stored_values() { + TEST_ASSERT_EQUAL_UINT8(0, static_cast(eth_clock_id_t::GPIO0_IN)); + TEST_ASSERT_EQUAL_UINT8(1, static_cast(eth_clock_id_t::GPIO0_OUT)); + TEST_ASSERT_EQUAL_UINT8(2, static_cast(eth_clock_id_t::GPIO16_OUT)); + TEST_ASSERT_EQUAL_UINT8(3, static_cast(eth_clock_id_t::GPIO17_OUT)); +} +static void test_translates_each_clock_mode() { + eth_clock_mode_t mode; + TEST_ASSERT_TRUE(ethClockMode(0, mode)); + TEST_ASSERT_EQUAL(ETH_CLOCK_GPIO0_IN, mode); + TEST_ASSERT_TRUE(ethClockMode(1, mode)); + TEST_ASSERT_EQUAL(ETH_CLOCK_GPIO0_OUT, mode); + TEST_ASSERT_TRUE(ethClockMode(2, mode)); + TEST_ASSERT_EQUAL(ETH_CLOCK_GPIO16_OUT, mode); + TEST_ASSERT_TRUE(ethClockMode(3, mode)); + TEST_ASSERT_EQUAL(ETH_CLOCK_GPIO17_OUT, mode); +} +static void test_rejects_unknown_clock_ids() { + eth_clock_mode_t mode; + TEST_ASSERT_FALSE(ethClockMode(4, mode)); + TEST_ASSERT_FALSE(ethClockMode(255, mode)); +} +static void test_clock_pins() { + TEST_ASSERT_EQUAL_INT8(0, ethClockPin(0)); + TEST_ASSERT_EQUAL_INT8(0, ethClockPin(1)); + TEST_ASSERT_EQUAL_INT8(16, ethClockPin(2)); + TEST_ASSERT_EQUAL_INT8(17, ethClockPin(3)); + TEST_ASSERT_EQUAL_INT8(-1, ethClockPin(4)); +} + int main(int, char **) { UNITY_BEGIN(); RUN_TEST(test_ids_match_stored_values); @@ -59,5 +92,9 @@ int main(int, char **) { RUN_TEST(test_lan8720_is_not_the_framework_zero); RUN_TEST(test_rejects_dm9051); RUN_TEST(test_rejects_unknown_ids); + RUN_TEST(test_clock_ids_match_stored_values); + RUN_TEST(test_translates_each_clock_mode); + RUN_TEST(test_rejects_unknown_clock_ids); + RUN_TEST(test_clock_pins); return UNITY_END(); } diff --git a/test/test_ethphy_p4/test_ethphy_p4.cpp b/test/test_ethphy_p4/test_ethphy_p4.cpp new file mode 100644 index 0000000..8d34b14 --- /dev/null +++ b/test/test_ethphy_p4/test_ethphy_p4.cpp @@ -0,0 +1,49 @@ +#include +// As on the ESP32-P4, which has the RMII MAC too but takes the IDF's clock +// enum. +#define CONFIG_ETH_USE_ESP32_EMAC 1 +#define CONFIG_IDF_TARGET_ESP32P4 1 +#include "EthPhy.h" + +void setUp() {} +void tearDown() {} + +static void test_builds_with_ethernet() { + TEST_ASSERT_EQUAL(1, HAS_ETHERNET); +} +static void test_ip101_is_the_tlk110_id() { + // Waveshare's ESP32-P4-ETH boards have an IP101, which Arduino drives as + // the TLK110; the web UI's preset stores 1. + eth_phy_type_t type = ETH_PHY_MAX; + TEST_ASSERT_TRUE(ethPhyType(1, type)); + TEST_ASSERT_EQUAL(ETH_PHY_TLK110, type); +} +static void test_clock_in_is_ext_in() { + // A cast would give the IDF's deprecated EMAC_CLK_DEFAULT. + eth_clock_mode_t mode = EMAC_CLK_DEFAULT; + TEST_ASSERT_TRUE(ethClockMode(static_cast(eth_clock_id_t::GPIO0_IN), mode)); + TEST_ASSERT_EQUAL(EMAC_CLK_EXT_IN, mode); +} +static void test_rejects_clock_out() { + // ETH.begin() keeps the clock on GPIO 50, which only takes a clock in. + eth_clock_mode_t mode; + TEST_ASSERT_FALSE(ethClockMode(1, mode)); + TEST_ASSERT_FALSE(ethClockMode(2, mode)); + TEST_ASSERT_FALSE(ethClockMode(3, mode)); + TEST_ASSERT_FALSE(ethClockMode(4, mode)); +} +static void test_clock_pin_is_50() { + TEST_ASSERT_EQUAL_INT8(50, ethClockPin(0)); + TEST_ASSERT_EQUAL_INT8(-1, ethClockPin(1)); + TEST_ASSERT_EQUAL_INT8(-1, ethClockPin(3)); +} + +int main(int, char **) { + UNITY_BEGIN(); + RUN_TEST(test_builds_with_ethernet); + RUN_TEST(test_ip101_is_the_tlk110_id); + RUN_TEST(test_clock_in_is_ext_in); + RUN_TEST(test_rejects_clock_out); + RUN_TEST(test_clock_pin_is_50); + return UNITY_END(); +} diff --git a/test/test_protosettings/test_protosettings.cpp b/test/test_protosettings/test_protosettings.cpp index f1e2ec7..5d53618 100644 --- a/test/test_protosettings/test_protosettings.cpp +++ b/test/test_protosettings/test_protosettings.cpp @@ -93,7 +93,7 @@ static void test_whole_numbers_are_checked_against_what_the_setting_holds() { TEST_ASSERT_EQUAL_STRING("connMode must be 1-3", expand(R"({"se":"network","cn":0})")); TEST_ASSERT_EQUAL_STRING("port must be 1-65535", expand(R"({"se":"mqtt","po":65536})")); TEST_ASSERT_EQUAL_STRING("txPower must be -30-12", expand(R"({"se":"radio","tp":13})")); - TEST_ASSERT_EQUAL_STRING("TXPin must be 0-48", expand(R"({"se":"radio","tx":-1})")); + TEST_ASSERT_EQUAL_STRING("TXPin must be 0-54", expand(R"({"se":"radio","tx":-1})")); TEST_ASSERT_EQUAL_STRING("securityType must be 0-2", expand(R"({"se":"security","sy":3})")); TEST_ASSERT_EQUAL_STRING("port must be 1-65535", expand(R"({"se":"mqtt","po":18446744073709551615})")); // A pin of -1 is none. diff --git a/test/test_utils/test_utils.cpp b/test/test_utils/test_utils.cpp index c95a91e..52e12a7 100644 --- a/test/test_utils/test_utils.cpp +++ b/test/test_utils/test_utils.cpp @@ -284,6 +284,10 @@ static void test_releaseAssetName_matches_the_release_workflow() { TEST_ASSERT_EQUAL_STRING("SomfyController.ino.esp32c5.bin", buf); releaseAssetName(buf, sizeof(buf), "littlefs", "c6"); TEST_ASSERT_EQUAL_STRING("SomfyController.littlefs.esp32c6.bin", buf); + releaseAssetName(buf, sizeof(buf), "ino", "p4"); + TEST_ASSERT_EQUAL_STRING("SomfyController.ino.esp32p4.bin", buf); + releaseAssetName(buf, sizeof(buf), "littlefs", "p4es"); + TEST_ASSERT_EQUAL_STRING("SomfyController.littlefs.esp32p4es.bin", buf); } /********************************************************************* From cbeb56d336d7479854c226803a102940a1761027 Mon Sep 17 00:00:00 2001 From: cjkas Date: Thu, 29 Oct 2026 20:10:36 +0100 Subject: [PATCH 2/2] fix: review fixes for the ESP32-P4 build - The pre-v3 build is for chips before v2.0, not v3.0: the esp32p4_es libraries stop at v1.99. The CI label and comments say so, and that no build runs on v2.x or v3.00. - A save refuses a clock mode the chip can't use: ProtoSettings takes CLKMode 0-ETH_CLOCK_ID_MAX, 0 only on the P4. Before, 1-3 saved and then stopped Ethernet at the next start. A range of one value reads "must be 0". - Tests for the pin limit (54 taken, 55 refused) and the clock limits. - The web UI's P4 pins leave out strapping pin 36 too. - The preset says ESP32-P4-WIFI6-POE-ETH, the board's name. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/build.yaml | 2 +- data-src/index.js | 7 ++++--- docs/adr/0003-pioarduino-idf55.md | 4 ++-- platformio.ini | 1 + src/ConfigSettings.cpp | 4 ++-- src/EthPhy.h | 9 ++++++++- src/ProtoSettings.cpp | 6 ++++-- test/test_ethphy/test_ethphy.cpp | 7 +++++++ test/test_ethphy_p4/test_ethphy_p4.cpp | 9 +++++++++ test/test_protosettings/test_protosettings.cpp | 7 +++++++ 10 files changed, 45 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index b80aa1f..2c4c97c 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -120,7 +120,7 @@ jobs: fsname: SomfyController.littlefs.esp32p4.bin obname: SomfyController.onboard.esp32p4.bin - env: esp32p4es - name: ESP32-P4 (before v3.0) + name: ESP32-P4 (before v2.0) chip: ESP32-P4 addr_bootloader: "0x2000" addr_fs: "0x310000" diff --git a/data-src/index.js b/data-src/index.js index 5efa3e6..598653d 100644 --- a/data-src/index.js +++ b/data-src/index.js @@ -1579,7 +1579,7 @@ class Wifi { { val: 5, label: 'wESP32 v7+ - Silicognition', clk: 0, ct: 2, addr: 0, pwr: -1, mdc: 16, mdio: 17 }, { val: 6, label: 'wESP32 < v7 - Silicognition', clk: 0, ct: 0, addr: 0, pwr: -1, mdc: 16, mdio: 17 }, { val: 1, label: 'WT32-ETH01 - Wireless Tag', clk: 0, ct: 0, addr: 1, pwr: 16, mdc: 23, mdio: 18 }, - { val: 8, label: 'ESP32-P4-ETH - Waveshare', clk: 0, ct: 1, addr: 1, pwr: 51, mdc: 31, mdio: 52, p4: true } + { val: 8, label: 'ESP32-P4-WIFI6-POE-ETH - Waveshare', clk: 0, ct: 1, addr: 1, pwr: 51, mdc: 31, mdio: 52, p4: true } ]; ethClockModes = [{ val: 0, label: 'GPIO0 IN' }, { val: 1, label: 'GPIO0 OUT' }, { val: 2, label: 'GPIO16 OUT' }, { val: 3, label: 'GPIO17 OUT' }]; // The P4 takes the clock in on GPIO 50 only (eth_clock_id_t in EthPhy.h). @@ -2735,8 +2735,9 @@ class Somfy { { name: 'c5', maxPins: 28, inputs: [13, 14, 15, 16, 17, 18, 19, 20, 21, 22], outputs: [13, 14, 15, 16, 17, 18, 19, 20, 21, 22] }, { name: 'c6', maxPins: 23, inputs: [], outputs: [] }, // Without the C6's SDIO link (14-19, 54), USB Serial/JTAG (24, 25), - // Ethernet (28-31, 34, 35, 49-52) and UART0 (37, 38). - { name: 'p4', maxPins: 54, inputs: [14, 15, 16, 17, 18, 19, 24, 25, 28, 29, 30, 31, 34, 35, 37, 38, 49, 50, 51, 52, 54], outputs: [14, 15, 16, 17, 18, 19, 24, 25, 28, 29, 30, 31, 34, 35, 37, 38, 49, 50, 51, 52, 54] } + // Ethernet (28-31, 34, 35, 49-52), UART0 (37, 38) and the strapping + // pins (34-38). + { name: 'p4', maxPins: 54, inputs: [14, 15, 16, 17, 18, 19, 24, 25, 28, 29, 30, 31, 34, 35, 36, 37, 38, 49, 50, 51, 52, 54], outputs: [14, 15, 16, 17, 18, 19, 24, 25, 28, 29, 30, 31, 34, 35, 36, 37, 38, 49, 50, 51, 52, 54] } ]; loadPins(type, sel, opt) { diff --git a/docs/adr/0003-pioarduino-idf55.md b/docs/adr/0003-pioarduino-idf55.md index 00f4177..8414c46 100644 --- a/docs/adr/0003-pioarduino-idf55.md +++ b/docs/adr/0003-pioarduino-idf55.md @@ -15,8 +15,8 @@ We moved every firmware env from `espressif32 @ 6.13.0` (Arduino 2.0.17 on ESP-I > **Amended by [ADR 0006](0006-two-security-modes-eleven-http-routes.md) (4.0.0).** The C3 builds again. Without the old API (ports 8080 and 8081), its image is 1,498,512 bytes at level 2 and leaves 72.6 KB of the OTA slot. It runs on chip revision v0.3 or later only: the framework's C3 libraries are built with `CONFIG_ESP32C3_REV_MIN_3`. Its default radio pins moved off GPIO 12-17, which hold the SPI flash. - **The C5 is updated over USB.** New in 4.0.0. At level 2 its image left 3.4 KB of the OTA slot, under CI's 8 KB, so like the C6 it is released as a debug build with one 3 MB app slot (`esp32_huge_app.csv`), which no OTA can write. The framework's C5 libraries run on chip revision v1.0 or later only (`CONFIG_ESP32C5_REV_MIN_FULL=100`). Its WiFi has 5 GHz too, so the passive scans for the configured network (`src/ESPNetwork.cpp`, 300 ms a channel) cover about 38 channels instead of 13: about 11 s instead of 4 s, depending on the country's channels. -- **The P4 has two builds.** New in 4.0.0, for boards such as Waveshare's ESP32-P4-WIFI6-POE-ETH. The framework's P4 libraries run on chip revision v3.01 or later only (`CONFIG_ESP32P4_REV_MIN_FULL=301`), its `esp32p4_es` libraries on revisions before v2.0, so `esp32p4` (chip model `p4`) and `esp32p4es` (`p4es`) each get their own release files. Both keep the OTA slot: at level 2 their images leave about 128 KB. WiFi goes through the board's ESP32-C6 on SDIO (esp-hosted, the framework's pins 14-19 and reset 54), so the C6 has to run a matching esp-hosted slave firmware, which Waveshare's boards ship with. -- **Ethernet PHY ids are ours.** Arduino 3.x renumbered `eth_phy_type_t` (`ETH_PHY_GENERIC` is 0). NVS, backups and the web UI keep the Arduino 2.x numbers as `eth_phy_id_t` (`src/EthPhy.h`), translated by name when Ethernet starts. Never cast a stored id to the framework enum. The RMII clock mode is kept the same way, as `eth_clock_id_t` in the ESP32's numbering: the P4's `eth_clock_mode_t` is the IDF's `emac_rmii_clock_mode_t`, whose 0 is a deprecated default. On the P4 only clock in (GPIO 50) translates, since `ETH.begin()` keeps the clock on that pin. +- **The P4 has two builds.** New in 4.0.0, for boards such as Waveshare's ESP32-P4-WIFI6-POE-ETH. The framework's P4 libraries run on chip revision v3.01 or later only (`CONFIG_ESP32P4_REV_MIN_FULL=301`), its `esp32p4_es` libraries on revisions before v2.0, so `esp32p4` (chip model `p4`) and `esp32p4es` (`p4es`) each get their own release files. No build runs on v2.x or v3.00. Both keep the OTA slot: at level 2 their images leave about 128 KB. WiFi goes through the board's ESP32-C6 on SDIO (esp-hosted, the framework's pins 14-19 and reset 54), so the C6 has to run a matching esp-hosted slave firmware, which Waveshare's boards ship with. +- **Ethernet PHY ids are ours.** Arduino 3.x renumbered `eth_phy_type_t` (`ETH_PHY_GENERIC` is 0). NVS, backups and the web UI keep the Arduino 2.x numbers as `eth_phy_id_t` (`src/EthPhy.h`), translated by name when Ethernet starts. Never cast a stored id to the framework enum. The RMII clock mode is kept the same way, as `eth_clock_id_t` in the ESP32's numbering: the P4's `eth_clock_mode_t` is the IDF's `emac_rmii_clock_mode_t`, whose 0 is a deprecated default. On the P4 only clock in (GPIO 50) translates, since `ETH.begin()` keeps the clock on that pin, and a save refuses the others (`ETH_CLOCK_ID_MAX`). - **Ethernet only on the original ESP32 and the P4.** Arduino 3.x has the RMII MAC API only where `CONFIG_ETH_USE_ESP32_EMAC` is set. S3, C3, C5 and C6 build without Ethernet. - **Updates roll back until proven.** We override the core's `verifyRollbackLater()` hook to return true, and `loop()` marks the new firmware valid once it has run 60 s and the network is up. A reset before then boots the previous firmware. The LittleFS image an update wrote stays, so the old firmware runs with the new web UI. - **Tooling.** pioarduino needs its own Core fork (`pip install pioarduino`, the pioarduino IDE extension in VS Code), which it installs into `~/.platformio/penv` in place of PlatformIO Core. diff --git a/platformio.ini b/platformio.ini index 928c322..601d0b9 100644 --- a/platformio.ini +++ b/platformio.ini @@ -130,6 +130,7 @@ build_flags = ; ESP32-C6 on SDIO (esp-hosted), Ethernet on the P4's own RMII MAC. The ; framework's P4 libraries run on chip revision v3.01 or later only, and its ; esp32p4_es libraries on revisions before v2.0, so each gets its own build. +; No build runs on v2.x or v3.00. [env:esp32p4] extends = esp32 board = esp32-p4_r3 diff --git a/src/ConfigSettings.cpp b/src/ConfigSettings.cpp index a95cd01..00d8c68 100644 --- a/src/ConfigSettings.cpp +++ b/src/ConfigSettings.cpp @@ -146,8 +146,8 @@ bool ConfigSettings::begin() { break; #endif #ifdef CONFIG_IDF_TARGET_ESP32P4 - // Chips before v3.0 run their own build (esp32p4_es libraries), so they - // get their own release files. + // Chips before v2.0 run their own build (esp32p4_es libraries), so they + // get their own release files. v2.x and v3.00 run neither. case esp_chip_model_t::CHIP_ESP32P4: #if CONFIG_ESP32P4_SELECTS_REV_LESS_V3 strcpy(this->chipModel, "p4es"); diff --git a/src/EthPhy.h b/src/EthPhy.h index 5f2f608..05acd20 100644 --- a/src/EthPhy.h +++ b/src/EthPhy.h @@ -35,9 +35,16 @@ enum class eth_clock_id_t : uint8_t { GPIO16_OUT = 2, GPIO17_OUT = 3 }; +// The highest clock id the chip takes: the P4's ETH.begin() takes the clock +// in on GPIO 50 only. +#if defined(CONFIG_IDF_TARGET_ESP32P4) +constexpr uint8_t ETH_CLOCK_ID_MAX = 0; +#else +constexpr uint8_t ETH_CLOCK_ID_MAX = 3; +#endif // The GPIO a stored clock id puts the RMII clock on, -1 for an id the chip -// can't use. The P4's ETH.begin() takes the clock in on GPIO 50 only. +// can't use. inline int8_t ethClockPin(uint8_t id) { #if defined(CONFIG_IDF_TARGET_ESP32P4) return static_cast(id) == eth_clock_id_t::GPIO0_IN ? 50 : -1; diff --git a/src/ProtoSettings.cpp b/src/ProtoSettings.cpp index 09fad81..0cec9da 100644 --- a/src/ProtoSettings.cpp +++ b/src/ProtoSettings.cpp @@ -3,6 +3,7 @@ #include #include "ProtoSettings.h" #include "ProtoParse.h" +#include "EthPhy.h" static const char *const SECTIONS[] = {"general", "network", "mqtt", "security", "radio"}; const char *settingsSectionName(settings_section_t section) { return SECTIONS[static_cast(section)]; } @@ -45,7 +46,7 @@ static const settings_field_t FIELDS[] = { {settings_part_t::wifi, proto_field_t::hidden, nullptr, 0, 0}, {settings_part_t::ethernet, proto_field_t::boardType, nullptr, 0, 255}, {settings_part_t::ethernet, proto_field_t::phyType, nullptr, 0, 255}, - {settings_part_t::ethernet, proto_field_t::CLKMode, nullptr, 0, 3}, + {settings_part_t::ethernet, proto_field_t::CLKMode, nullptr, 0, ETH_CLOCK_ID_MAX}, {settings_part_t::ethernet, proto_field_t::phyAddress, nullptr, -1, 31}, // Pins run to GPIO 54, the P4's last. {settings_part_t::ethernet, proto_field_t::PWRPin, nullptr, -1, 54}, @@ -138,7 +139,8 @@ static const char *checkValue(const settings_field_t &s, const proto_field_info_ return why; } if(inRange) return nullptr; - snprintf(why, sizeof(why), "%s must be %ld-%ld", f.name, (long)s.lo, (long)s.hi); + if(s.lo == s.hi) snprintf(why, sizeof(why), "%s must be %ld", f.name, (long)s.lo); + else snprintf(why, sizeof(why), "%s must be %ld-%ld", f.name, (long)s.lo, (long)s.hi); return why; } if(f.kind == 'n') { diff --git a/test/test_ethphy/test_ethphy.cpp b/test/test_ethphy/test_ethphy.cpp index b86b5eb..cfce06e 100644 --- a/test/test_ethphy/test_ethphy.cpp +++ b/test/test_ethphy/test_ethphy.cpp @@ -77,6 +77,12 @@ static void test_rejects_unknown_clock_ids() { TEST_ASSERT_FALSE(ethClockMode(4, mode)); TEST_ASSERT_FALSE(ethClockMode(255, mode)); } +static void test_a_save_takes_each_clock_id_that_translates() { + eth_clock_mode_t mode; + TEST_ASSERT_EQUAL_UINT8(3, ETH_CLOCK_ID_MAX); + TEST_ASSERT_TRUE(ethClockMode(ETH_CLOCK_ID_MAX, mode)); + TEST_ASSERT_FALSE(ethClockMode(ETH_CLOCK_ID_MAX + 1, mode)); +} static void test_clock_pins() { TEST_ASSERT_EQUAL_INT8(0, ethClockPin(0)); TEST_ASSERT_EQUAL_INT8(0, ethClockPin(1)); @@ -95,6 +101,7 @@ int main(int, char **) { RUN_TEST(test_clock_ids_match_stored_values); RUN_TEST(test_translates_each_clock_mode); RUN_TEST(test_rejects_unknown_clock_ids); + RUN_TEST(test_a_save_takes_each_clock_id_that_translates); RUN_TEST(test_clock_pins); return UNITY_END(); } diff --git a/test/test_ethphy_p4/test_ethphy_p4.cpp b/test/test_ethphy_p4/test_ethphy_p4.cpp index 8d34b14..af0c00e 100644 --- a/test/test_ethphy_p4/test_ethphy_p4.cpp +++ b/test/test_ethphy_p4/test_ethphy_p4.cpp @@ -32,6 +32,14 @@ static void test_rejects_clock_out() { TEST_ASSERT_FALSE(ethClockMode(3, mode)); TEST_ASSERT_FALSE(ethClockMode(4, mode)); } +static void test_a_save_takes_clock_in_only() { + // ProtoSettings refuses a CLKMode over this, so a save can't store a mode + // that stops Ethernet at the next start. + eth_clock_mode_t mode; + TEST_ASSERT_EQUAL_UINT8(0, ETH_CLOCK_ID_MAX); + TEST_ASSERT_TRUE(ethClockMode(ETH_CLOCK_ID_MAX, mode)); + TEST_ASSERT_FALSE(ethClockMode(ETH_CLOCK_ID_MAX + 1, mode)); +} static void test_clock_pin_is_50() { TEST_ASSERT_EQUAL_INT8(50, ethClockPin(0)); TEST_ASSERT_EQUAL_INT8(-1, ethClockPin(1)); @@ -44,6 +52,7 @@ int main(int, char **) { RUN_TEST(test_ip101_is_the_tlk110_id); RUN_TEST(test_clock_in_is_ext_in); RUN_TEST(test_rejects_clock_out); + RUN_TEST(test_a_save_takes_clock_in_only); RUN_TEST(test_clock_pin_is_50); return UNITY_END(); } diff --git a/test/test_protosettings/test_protosettings.cpp b/test/test_protosettings/test_protosettings.cpp index 5d53618..9e6acb3 100644 --- a/test/test_protosettings/test_protosettings.cpp +++ b/test/test_protosettings/test_protosettings.cpp @@ -98,6 +98,13 @@ static void test_whole_numbers_are_checked_against_what_the_setting_holds() { TEST_ASSERT_EQUAL_STRING("port must be 1-65535", expand(R"({"se":"mqtt","po":18446744073709551615})")); // A pin of -1 is none. TEST_ASSERT_GOOD(expand(R"({"se":"network","pw":-1})")); + // Pins run to GPIO 54, the P4's last. + TEST_ASSERT_GOOD(expand(R"({"se":"radio","tx":54})")); + TEST_ASSERT_EQUAL_STRING("TXPin must be 0-54", expand(R"({"se":"radio","tx":55})")); + TEST_ASSERT_GOOD(expand(R"({"se":"network","mi":54})")); + // The ESP32's four clock modes; a P4 build takes 0 only (ETH_CLOCK_ID_MAX). + TEST_ASSERT_GOOD(expand(R"({"se":"network","ck":3})")); + TEST_ASSERT_EQUAL_STRING("CLKMode must be 0-3", expand(R"({"se":"network","ck":4})")); TEST_ASSERT_GOOD(expand(R"({"se":"radio","tp":-30})")); } static void test_the_bit_length_is_56_or_80() {