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 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-29 19:41:39 +01:00
parent eadd9b801d
commit dfb003d2f7
19 changed files with 282 additions and 38 deletions

View file

@ -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<uint8_t>(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<eth_clock_mode_t>(rec.ethClkMode);
settings.Ethernet.CLKMode = rec.ethClkMode;
settings.Ethernet.phyAddress = rec.ethPhyAddress;
settings.Ethernet.PWRPin = rec.ethPWRPin;
settings.Ethernet.MDCPin = rec.ethMDCPin;

View file

@ -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<int>(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<eth_clock_mode_t>(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<uint8_t>(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<eth_clock_mode_t>(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);

View file

@ -88,14 +88,26 @@ class WifiSettings: BaseSettings {
class EthernetSettings: BaseSettings {
public:
EthernetSettings();
uint8_t CLKMode = static_cast<uint8_t>(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<uint8_t>(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<uint8_t>(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);

View file

@ -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<uint8_t>(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);

View file

@ -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;

View file

@ -5,8 +5,8 @@
#include <sdkconfig.h>
#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<eth_clock_id_t>(id) == eth_clock_id_t::GPIO0_IN ? 50 : -1;
#else
switch(static_cast<eth_clock_id_t>(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 <ETH.h>
// 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<eth_clock_id_t>(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

View file

@ -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},

View file

@ -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;

View file

@ -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.<kind>.esp32<chipModel>.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.