From 92e69d599f03493f612e4c20f5f31618064d9fdf Mon Sep 17 00:00:00 2001 From: cjkas Date: Thu, 29 Oct 2026 22:54:11 +0100 Subject: [PATCH] fix: warn of radio pins that block flashing over USB GPIO2 on the original ESP32, 46 on the S2/S3 and 8 on the C3/C6 don't stop a normal boot, but held at the wrong level at reset they keep esptool from starting the download mode. The WT32-ETH01 wiring puts TX on GPIO2. Each warning now says what the pin does and what to do: move it for a pin that stops the boot, disconnect it to flash for one that only blocks USB. Co-Authored-By: Claude Opus 5.5 --- data-src/js/pins.js | 39 ++++++++++++---------- src/BootPins.h | 39 +++++++++++++--------- src/Transceiver.cpp | 9 ++--- test/js/pins.test.js | 16 +++++++++ test/test_bootpins/test_bootpins.cpp | 17 ++++++++-- test/test_bootpins_s3/test_bootpins_s3.cpp | 7 ++++ 6 files changed, 87 insertions(+), 40 deletions(-) diff --git a/data-src/js/pins.js b/data-src/js/pins.js index 145ce0c..c81ee0a 100644 --- a/data-src/js/pins.js +++ b/data-src/js/pins.js @@ -1,13 +1,14 @@ // The strapping pins whose level at reset can keep each chip from starting -// the firmware, as src/BootPins.h has them for the log. A radio that drives -// one at power-up stops the board booting until EN is pressed (issue #34). -// Keep the pins, the radio pins checked and the shared RX/TX rule in step -// with that file. +// the firmware, or from being flashed over USB, as src/BootPins.h has them +// for the log. A radio that drives one at reset stops the board booting +// until EN is pressed (issue #34), or esptool from connecting. Keep the +// pins, the radio pins checked and the shared RX/TX rule in step with that +// file. // The minify step joins this file into index.js; Node loads it on its own for // the tests (test/js). class BootPins { // A warning for each of the radio config's pins that can stop the board - // booting. + // booting or being flashed. static radioWarnings(chipModel, config) { const chip = BootPins.CHIPS[chipModel || ''] || {}; const warnings = []; @@ -15,24 +16,28 @@ class BootPins { const pin = config[field]; if (field === 'RXPin' && pin === config.TXPin) continue; const effect = chip[pin]; - if (effect) warnings.push(`${name} is on GPIO-${pin}, a strapping pin: ${effect}. The radio can hold it there and stop the board booting after a power cut, so move it to another pin.`); + if (effect) warnings.push(`${name} is on GPIO-${pin}, a strapping pin the radio can drive at reset. ${effect}`); } return warnings; } } +BootPins.DOWNLOAD = 'Held low, it starts the download mode instead of the firmware: move it to another pin.'; +BootPins.FLASH_1V8 = 'Held high, it powers the flash at 1.8 V and the board bootloops: move it to another pin.'; +BootPins.NO_USB_HIGH = 'Held high, it stops flashing over USB, so disconnect it to flash.'; +BootPins.NO_USB_LOW = 'Held low, it stops flashing over USB, so disconnect it to flash.'; // By the chip model the header sends, '' for the original ESP32. BootPins.CHIPS = { - '': { 0: 'low at reset starts the download mode', 12: 'high at reset powers the flash at 1.8 V' }, - s2: { 0: 'low at reset starts the download mode', 45: 'high at reset powers the flash at 1.8 V' }, - s3: { 0: 'low at reset starts the download mode', 45: 'high at reset powers the flash at 1.8 V' }, - c3: { 9: 'low at reset starts the download mode' }, - c5: { 28: 'low at reset starts the download mode' }, - c6: { 9: 'low at reset starts the download mode' }, - p4: { 35: 'low at reset starts the download mode' }, - p4es: { 35: 'low at reset starts the download mode' } + '': { 0: BootPins.DOWNLOAD, 12: BootPins.FLASH_1V8, 2: BootPins.NO_USB_HIGH }, + s2: { 0: BootPins.DOWNLOAD, 45: BootPins.FLASH_1V8, 46: BootPins.NO_USB_HIGH }, + s3: { 0: BootPins.DOWNLOAD, 45: BootPins.FLASH_1V8, 46: BootPins.NO_USB_HIGH }, + c3: { 9: BootPins.DOWNLOAD, 8: BootPins.NO_USB_LOW }, + c5: { 28: BootPins.DOWNLOAD }, + c6: { 9: BootPins.DOWNLOAD, 8: BootPins.NO_USB_LOW }, + p4: { 35: BootPins.DOWNLOAD }, + p4es: { 35: BootPins.DOWNLOAD } }; -// The radio pins the CC1101 drives at power-up, before it is set up: GDO0 -// puts out a clock, GDO2 CHIP_RDYn, and SO follows CHIP_RDYn while CSN is -// low. +// The radio pins the CC1101 drives: at power-up GDO0 puts out a clock, GDO2 +// CHIP_RDYn, and SO follows CHIP_RDYn while CSN is low. Once it is set up, +// GDO2 puts out what it hears. BootPins.RADIO_PINS = [['TXPin', 'TX'], ['RXPin', 'RX'], ['MISOPin', 'MISO']]; if (typeof module !== 'undefined' && module.exports) module.exports = { BootPins }; diff --git a/src/BootPins.h b/src/BootPins.h index 0809623..b3097c1 100644 --- a/src/BootPins.h +++ b/src/BootPins.h @@ -6,30 +6,37 @@ #endif // The strapping pins whose level at reset can keep the chip from starting -// the firmware. The ROM reads them before any code runs, so a radio that -// drives one at power-up stops the board booting until EN is pressed (issue -// #34). The strapping pins that only pick a download mode, quiet the boot -// log or set the SDIO timing aren't listed. The web UI has the same pins in -// data-src/js/pins.js, and checks the same radio pins as -// transceiver_config_t::apply(): keep the three in step. -// -// Returns what the pin does at reset, or nullptr for a pin that can't stop -// the boot. +// the firmware, or from being flashed over USB. The ROM reads them before +// any code runs, so a radio that drives one at reset stops the board booting +// until EN is pressed (issue #34), or esptool from connecting. The strapping +// pins that only quiet the boot log or set the SDIO timing aren't listed. +// The web UI has the same pins in data-src/js/pins.js, and checks the same +// radio pins as transceiver_config_t::apply(): keep the three in step. +constexpr const char *BOOT_PIN_DOWNLOAD = "Held low, it starts the download mode instead of the firmware: move it to another pin."; +constexpr const char *BOOT_PIN_FLASH_1V8 = "Held high, it powers the flash at 1.8 V and the board bootloops: move it to another pin."; +constexpr const char *BOOT_PIN_NO_USB_HIGH = "Held high, it stops flashing over USB, so disconnect it to flash."; +constexpr const char *BOOT_PIN_NO_USB_LOW = "Held low, it stops flashing over USB, so disconnect it to flash."; + +// Returns what the pin does at reset and what to do, or nullptr for a pin +// that is safe. inline const char *bootPinEffect(uint8_t pin) { #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) - if(pin == 0) return "low at reset starts the download mode"; - if(pin == 45) return "high at reset powers the flash at 1.8 V"; + if(pin == 0) return BOOT_PIN_DOWNLOAD; + if(pin == 45) return BOOT_PIN_FLASH_1V8; + if(pin == 46) return BOOT_PIN_NO_USB_HIGH; #elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6) - if(pin == 9) return "low at reset starts the download mode"; + if(pin == 9) return BOOT_PIN_DOWNLOAD; + if(pin == 8) return BOOT_PIN_NO_USB_LOW; #elif defined(CONFIG_IDF_TARGET_ESP32C5) - if(pin == 28) return "low at reset starts the download mode"; + if(pin == 28) return BOOT_PIN_DOWNLOAD; #elif defined(CONFIG_IDF_TARGET_ESP32P4) - if(pin == 35) return "low at reset starts the download mode"; + if(pin == 35) return BOOT_PIN_DOWNLOAD; #else // The original ESP32. MTDI picks the flash voltage unless the // VDD_SDIO efuses are burned. - if(pin == 0) return "low at reset starts the download mode"; - if(pin == 12) return "high at reset powers the flash at 1.8 V"; + if(pin == 0) return BOOT_PIN_DOWNLOAD; + if(pin == 12) return BOOT_PIN_FLASH_1V8; + if(pin == 2) return BOOT_PIN_NO_USB_HIGH; #endif return nullptr; } diff --git a/src/Transceiver.cpp b/src/Transceiver.cpp index 74967ac..6b09f36 100644 --- a/src/Transceiver.cpp +++ b/src/Transceiver.cpp @@ -577,7 +577,7 @@ void transceiver_config_t::load() { } static void warnBootPin(const char *name, uint8_t pin) { const char *effect = bootPinEffect(pin); - if(effect) ESP_LOGW(TAG, "Radio %s is on GPIO%u, a strapping pin: %s. The radio can hold it there and stop the board booting after a power cut, so move it to another pin.", name, pin, effect); + if(effect) ESP_LOGW(TAG, "Radio %s is on GPIO%u, a strapping pin the radio can drive at reset. %s", name, pin, effect); } void transceiver_config_t::apply() { Preferences pref; @@ -603,9 +603,10 @@ void transceiver_config_t::apply() { pref.end(); if(!radioInit) return; ESP_LOGD(TAG, "Applying radio settings - Setting Data Pins RX:%u TX:%u", this->RXPin, this->TXPin); - // The pins the CC1101 drives at power-up, before it is set up: GDO0 - // puts out a clock, GDO2 CHIP_RDYn, and SO follows CHIP_RDYn while CSN - // is low. The web UI checks the same (data-src/js/pins.js). + // The pins the CC1101 drives: at power-up GDO0 puts out a clock, GDO2 + // CHIP_RDYn, and SO follows CHIP_RDYn while CSN is low. Once it is + // set up, GDO2 puts out what it hears. The web UI checks the same + // (data-src/js/pins.js). warnBootPin("TX", this->TXPin); if(this->RXPin != this->TXPin) warnBootPin("RX", this->RXPin); warnBootPin("MISO", this->MISOPin); diff --git a/test/js/pins.test.js b/test/js/pins.test.js index cd0a391..73d6b72 100644 --- a/test/js/pins.test.js +++ b/test/js/pins.test.js @@ -18,6 +18,22 @@ test('RX on GPIO12 warns of the flash voltage on the original ESP32', () => { assert.match(warnings[0], /1\.8 V/); }); +test('TX on GPIO2 warns of flashing over USB on the original ESP32', () => { + // The WT32-ETH01 wiring in the wiki. + const warnings = BootPins.radioWarnings('', Object.assign({}, esp32Defaults, { TXPin: 2 })); + assert.equal(warnings.length, 1); + assert.match(warnings[0], /^TX is on GPIO-2, .*Held high, it stops flashing over USB, so disconnect it to flash\.$/); +}); + +test('GPIO8 held low blocks flashing on the C3 and C6', () => { + const config = { SCKPin: 4, CSNPin: 7, MOSIPin: 6, MISOPin: 5, TXPin: 10, RXPin: 8 }; + for (const chip of ['c3', 'c6']) { + const warnings = BootPins.radioWarnings(chip, config); + assert.equal(warnings.length, 1); + assert.match(warnings[0], /^RX is on GPIO-8, .*Held low, it stops flashing over USB/); + } +}); + test('an unknown chip model warns of nothing', () => { assert.deepEqual(BootPins.radioWarnings('h2', Object.assign({}, esp32Defaults, { RXPin: 12 })), []); }); diff --git a/test/test_bootpins/test_bootpins.cpp b/test/test_bootpins/test_bootpins.cpp index 6444534..1f0e7ce 100644 --- a/test/test_bootpins/test_bootpins.cpp +++ b/test/test_bootpins/test_bootpins.cpp @@ -19,10 +19,18 @@ static void test_gpio0_picks_the_boot_mode() { TEST_ASSERT_NOT_NULL(strstr(effect, "download mode")); } +static void test_gpio2_blocks_flashing_over_usb() { + // It only counts with GPIO0 low, so the board boots, but esptool can't + // start the download mode while it is high. + const char *effect = bootPinEffect(2); + TEST_ASSERT_NOT_NULL(effect); + TEST_ASSERT_NOT_NULL(strstr(effect, "high")); + TEST_ASSERT_NOT_NULL(strstr(effect, "USB")); +} + static void test_harmless_strapping_pins_are_not_listed() { - // 2 only matters for the download mode, 5 and 15 set the SDIO timing and - // the boot log. 5 is the default CSN. - TEST_ASSERT_NULL(bootPinEffect(2)); + // 5 and 15 set the SDIO timing and quiet the boot log. 5 is the default + // CSN. TEST_ASSERT_NULL(bootPinEffect(5)); TEST_ASSERT_NULL(bootPinEffect(15)); } @@ -34,14 +42,17 @@ static void test_default_radio_pins_are_safe() { } static void test_other_chips_boot_pins_are_safe_here() { + TEST_ASSERT_NULL(bootPinEffect(8)); TEST_ASSERT_NULL(bootPinEffect(9)); TEST_ASSERT_NULL(bootPinEffect(45)); + TEST_ASSERT_NULL(bootPinEffect(46)); } int main(int, char **) { UNITY_BEGIN(); RUN_TEST(test_gpio12_picks_the_flash_voltage); RUN_TEST(test_gpio0_picks_the_boot_mode); + RUN_TEST(test_gpio2_blocks_flashing_over_usb); RUN_TEST(test_harmless_strapping_pins_are_not_listed); RUN_TEST(test_default_radio_pins_are_safe); RUN_TEST(test_other_chips_boot_pins_are_safe_here); diff --git a/test/test_bootpins_s3/test_bootpins_s3.cpp b/test/test_bootpins_s3/test_bootpins_s3.cpp index 011808e..876dca5 100644 --- a/test/test_bootpins_s3/test_bootpins_s3.cpp +++ b/test/test_bootpins_s3/test_bootpins_s3.cpp @@ -17,6 +17,12 @@ static void test_gpio0_picks_the_boot_mode() { TEST_ASSERT_NOT_NULL(bootPinEffect(0)); } +static void test_gpio46_blocks_flashing_over_usb() { + const char *effect = bootPinEffect(46); + TEST_ASSERT_NOT_NULL(effect); + TEST_ASSERT_NOT_NULL(strstr(effect, "USB")); +} + static void test_gpio12_is_safe() { // 12 is the S3's default SCK. TEST_ASSERT_NULL(bootPinEffect(12)); @@ -32,6 +38,7 @@ int main(int, char **) { UNITY_BEGIN(); RUN_TEST(test_gpio45_picks_the_flash_voltage); RUN_TEST(test_gpio0_picks_the_boot_mode); + RUN_TEST(test_gpio46_blocks_flashing_over_usb); RUN_TEST(test_gpio12_is_safe); RUN_TEST(test_default_radio_pins_are_safe); return UNITY_END();