fix: warn of radio pins that can stop the board booting (#34)

GPIO12 is a strapping pin on the original ESP32: high at reset, it powers
the flash at 1.8 V. A CC1101 can hold GDO2 high at power-up, so a board
with RX on 12 bootloops after a power cut until EN is pressed. It was the
default RX pin.

- RX defaults to GPIO35 on the original ESP32. Saved settings keep their
  pins.
- The log warns each time the radio starts, and the radio settings show a
  warning, while TX, RX or MISO is on a pin whose level at reset can stop
  the chip booting (src/BootPins.h, data-src/js/pins.js).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
cjkas 2026-10-29 22:29:39 +01:00
parent a92c7a26f6
commit 8a59f51111
10 changed files with 234 additions and 7 deletions

44
test/js/pins.test.js Normal file
View file

@ -0,0 +1,44 @@
// The radio pins that can stop the board booting (data-src/js/pins.js).
const test = require('node:test');
const assert = require('node:assert/strict');
const { BootPins } = require('../../data-src/js/pins.js');
// transceiver_config_t::load()'s defaults for the original ESP32.
const esp32Defaults = { SCKPin: 18, CSNPin: 5, MOSIPin: 23, MISOPin: 19, TXPin: 13, RXPin: 35 };
test('the original ESP32 defaults are safe', () => {
assert.deepEqual(BootPins.radioWarnings('', esp32Defaults), []);
});
test('RX on GPIO12 warns of the flash voltage on the original ESP32', () => {
// Issue #34.
const warnings = BootPins.radioWarnings('', Object.assign({}, esp32Defaults, { RXPin: 12 }));
assert.equal(warnings.length, 1);
assert.match(warnings[0], /^RX is on GPIO-12/);
assert.match(warnings[0], /1\.8 V/);
});
test('an unknown chip model warns of nothing', () => {
assert.deepEqual(BootPins.radioWarnings('h2', Object.assign({}, esp32Defaults, { RXPin: 12 })), []);
});
test('a missing chip model is the original ESP32', () => {
assert.equal(BootPins.radioWarnings(null, Object.assign({}, esp32Defaults, { TXPin: 0 })).length, 1);
});
test('GPIO12 is safe on the S3', () => {
assert.deepEqual(BootPins.radioWarnings('s3', { SCKPin: 12, CSNPin: 10, MOSIPin: 11, MISOPin: 13, TXPin: 15, RXPin: 14 }), []);
});
test('MISO and TX warn, the pins the radio only reads do not', () => {
const config = { SCKPin: 4, CSNPin: 0, MOSIPin: 1, MISOPin: 9, TXPin: 10, RXPin: 3 };
assert.deepEqual(BootPins.radioWarnings('c3', config).map((w) => w.split(' ')[0]), ['MISO']);
assert.deepEqual(BootPins.radioWarnings('c6', Object.assign({}, config, { MISOPin: 2, TXPin: 9, RXPin: 9 })).map((w) => w.split(' ')[0]),
['TX']);
});
test('the pre-v3 P4 has the P4 pins', () => {
assert.deepEqual(BootPins.radioWarnings('p4es', { SCKPin: 27, CSNPin: 46, MOSIPin: 32, MISOPin: 33, TXPin: 2, RXPin: 35 }),
BootPins.radioWarnings('p4', { SCKPin: 27, CSNPin: 46, MOSIPin: 32, MISOPin: 33, TXPin: 2, RXPin: 35 }));
assert.equal(BootPins.radioWarnings('p4', { SCKPin: 27, CSNPin: 46, MOSIPin: 32, MISOPin: 33, TXPin: 2, RXPin: 35 }).length, 1);
});

View file

@ -0,0 +1,49 @@
#include <unity.h>
#include <string.h>
// No target defined: the original ESP32.
#include "BootPins.h"
void setUp() {}
void tearDown() {}
static void test_gpio12_picks_the_flash_voltage() {
// Issue #34: a CC1101 holding GDO2 high on GPIO12 at power-up.
const char *effect = bootPinEffect(12);
TEST_ASSERT_NOT_NULL(effect);
TEST_ASSERT_NOT_NULL(strstr(effect, "1.8 V"));
}
static void test_gpio0_picks_the_boot_mode() {
const char *effect = bootPinEffect(0);
TEST_ASSERT_NOT_NULL(effect);
TEST_ASSERT_NOT_NULL(strstr(effect, "download mode"));
}
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));
TEST_ASSERT_NULL(bootPinEffect(5));
TEST_ASSERT_NULL(bootPinEffect(15));
}
static void test_default_radio_pins_are_safe() {
// transceiver_config_t::load()'s defaults for the original ESP32.
const uint8_t pins[] = {13, 35, 23, 19, 18, 5};
for(uint8_t pin : pins) TEST_ASSERT_NULL_MESSAGE(bootPinEffect(pin), "default radio pin");
}
static void test_other_chips_boot_pins_are_safe_here() {
TEST_ASSERT_NULL(bootPinEffect(9));
TEST_ASSERT_NULL(bootPinEffect(45));
}
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_harmless_strapping_pins_are_not_listed);
RUN_TEST(test_default_radio_pins_are_safe);
RUN_TEST(test_other_chips_boot_pins_are_safe_here);
return UNITY_END();
}

View file

@ -0,0 +1,38 @@
#include <unity.h>
#include <string.h>
// As on the ESP32-S3, where GPIO45 picks the flash voltage.
#define CONFIG_IDF_TARGET_ESP32S3 1
#include "BootPins.h"
void setUp() {}
void tearDown() {}
static void test_gpio45_picks_the_flash_voltage() {
const char *effect = bootPinEffect(45);
TEST_ASSERT_NOT_NULL(effect);
TEST_ASSERT_NOT_NULL(strstr(effect, "1.8 V"));
}
static void test_gpio0_picks_the_boot_mode() {
TEST_ASSERT_NOT_NULL(bootPinEffect(0));
}
static void test_gpio12_is_safe() {
// 12 is the S3's default SCK.
TEST_ASSERT_NULL(bootPinEffect(12));
}
static void test_default_radio_pins_are_safe() {
// transceiver_config_t::load()'s defaults for the S3.
const uint8_t pins[] = {15, 14, 11, 13, 12, 10};
for(uint8_t pin : pins) TEST_ASSERT_NULL_MESSAGE(bootPinEffect(pin), "default radio pin");
}
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_gpio12_is_safe);
RUN_TEST(test_default_radio_pins_are_safe);
return UNITY_END();
}