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

View file

@ -919,20 +919,21 @@
<label for="selTransMOSIPin">MOSI</label>
</div>
<div class="field-group radioPins">
<select id="selTransMISOPin" name="transMISO" data-bind="transceiver.config.MISOPin" data-datatype="int"></select>
<select id="selTransMISOPin" name="transMISO" data-bind="transceiver.config.MISOPin" data-datatype="int" onchange="somfy.checkBootPins();"></select>
<label for="selTransMISOPin">MISO</label>
</div>
</div>
<div class="field-group1" style="margin-top:-20px;">
<div class="field-group radioPins">
<select id="selTransTXPin" name="transTX" data-bind="transceiver.config.TXPin" data-datatype="int"></select>
<select id="selTransTXPin" name="transTX" data-bind="transceiver.config.TXPin" data-datatype="int" onchange="somfy.checkBootPins();"></select>
<label for="selTransTXPin">TX</label>
</div>
<div class="field-group radioPins">
<select id="selTransRXPin" name="transRX" data-bind="transceiver.config.RXPin" data-datatype="int"></select>
<select id="selTransRXPin" name="transRX" data-bind="transceiver.config.RXPin" data-datatype="int" onchange="somfy.checkBootPins();"></select>
<label for="selTransRXPin">RX</label>
</div>
</div>
<div id="divRadioBootPins" style="display:none;color:#e65100;font-size:12px;margin:-14px 0 14px 0;white-space:normal;"></div>
<div class="field-group" style="display:inline-block;width:auto;min-width:247px;margin-top:-20px;vertical-align:top;">
<div class="field-group">
<input id="slidFrequency" name="frequency" type="range" min="433000" max="434399" step="1" style="width:100%;" data-bind="transceiver.config.frequency" data-datatype="float" data-mult="1000" oninput="somfy.frequencyChanged(this);" />

View file

@ -1944,8 +1944,9 @@ class Somfy {
this.loadPins('input', document.getElementById('selTransRXPin'));
//this.loadSomfy();
ui.toElement(document.getElementById('divTransceiverSettings'), {
transceiver: { config: { proto: 0, SCKPin: 18, CSNPin: 5, MOSIPin: 23, MISOPin: 19, TXPin: 12, RXPin: 13, frequency: 433.42, rxBandwidth: 97.96, type: 56, deviation: 11.43, txPower: 10, enabled: false, noiseDetection: false } }
transceiver: { config: { proto: 0, SCKPin: 18, CSNPin: 5, MOSIPin: 23, MISOPin: 19, TXPin: 13, RXPin: 35, frequency: 433.42, rxBandwidth: 97.96, type: 56, deviation: 11.43, txPower: 10, enabled: false, noiseDetection: false } }
});
this.checkBootPins();
this.loadPins('out', document.getElementById('selShadeGPIOUp'));
this.loadPins('out', document.getElementById('selShadeGPIODown'));
this.loadPins('out', document.getElementById('selShadeGPIOMy'));
@ -1969,8 +1970,18 @@ class Somfy {
let config = ProtoViews.radio(proto.schema, radio);
ui.toElement(document.getElementById('divTransceiverSettings'), { transceiver: { config: config } });
document.getElementById('divRadioError').style.display = config.radioInit ? 'none' : '';
this.checkBootPins();
});
}
// Warns of radio pins that can stop the board booting (issue #34).
checkBootPins() {
let trans = ui.fromElement(document.getElementById('divTransceiverSettings')).transceiver;
let chip = document.getElementById('divContainer').getAttribute('data-chipmodel') || '';
let warnings = BootPins.radioWarnings(chip, trans.config);
let div = document.getElementById('divRadioBootPins');
div.innerText = warnings.join('\n');
div.style.display = warnings.length > 0 ? '' : 'none';
}
saveRadio() {
let valid = true;
let getIntValue = (fld) => { return parseInt(document.getElementById(fld).value, 10); };

36
data-src/js/pins.js Normal file
View file

@ -0,0 +1,36 @@
// 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).
// 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.
static radioWarnings(chipModel, config) {
const chip = BootPins.CHIPS[chipModel || ''] || {};
const warnings = [];
for (const [field, name] of BootPins.RADIO) {
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.`);
}
return warnings;
}
}
// 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' }
};
// 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.
BootPins.RADIO = [['TXPin', 'TX'], ['RXPin', 'RX'], ['MISOPin', 'MISO']];
if (typeof module !== 'undefined' && module.exports) module.exports = { BootPins };

View file

@ -58,7 +58,7 @@ PNG_EXTENSIONS = {".png"}
# comes before index.js, which uses it. A source listed here isn't copied on
# its own.
BUNDLES = {
"index.js": ["js/schema.js", "js/model.js", "js/client.js", "js/views.js", "index.js"],
"index.js": ["js/schema.js", "js/model.js", "js/client.js", "js/views.js", "js/pins.js", "index.js"],
"app.css": ["main.css", "widgets.css", "icons.css"],
}

34
src/BootPins.h Normal file
View file

@ -0,0 +1,34 @@
#ifndef BOOT_PINS_H
#define BOOT_PINS_H
#include <stdint.h>
#if __has_include(<sdkconfig.h>)
#include <sdkconfig.h>
#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. web UI: data-src/js/pins.js.
//
// Returns what the pin does at reset, or nullptr for a pin that can't stop
// the boot.
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";
#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32C6)
if(pin == 9) return "low at reset starts the download mode";
#elif defined(CONFIG_IDF_TARGET_ESP32C5)
if(pin == 28) return "low at reset starts the download mode";
#elif defined(CONFIG_IDF_TARGET_ESP32P4)
if(pin == 35) return "low at reset starts the download mode";
#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";
#endif
return nullptr;
}
#endif

View file

@ -8,6 +8,7 @@
#include "driver/gpio.h"
#include "esp_log.h"
#include "Utils.h"
#include "BootPins.h"
#include "ConfigSettings.h"
#include "Somfy.h"
#include "outputs/ProtoSocket.h"
@ -523,8 +524,11 @@ void transceiver_config_t::load() {
break;
#endif
default:
// RX was on 12, a strapping pin (issue #34). A board that saved its
// radio settings keeps its pins. 35 is input only, which RX is; 36
// and 39 take false interrupts.
this->TXPin = 13;
this->RXPin = 12;
this->RXPin = 35;
this->MOSIPin = 23;
this->MISOPin = 19;
this->SCKPin = 18;
@ -571,6 +575,10 @@ void transceiver_config_t::load() {
pref.end();
//this->printBuffer = somfy.transceiver.printBuffer;
}
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);
}
void transceiver_config_t::apply() {
Preferences pref;
// A transmission in flight stops after its current frame so the radio can
@ -595,6 +603,12 @@ 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.
warnBootPin("TX", this->TXPin);
if(this->RXPin != this->TXPin) warnBootPin("RX", this->RXPin);
warnBootPin("MISO", this->MISOPin);
//if(this->TXPin != this->RXPin)
// pinMode(this->TXPin, OUTPUT);
//pinMode(this->RXPin, INPUT);

View file

@ -30,7 +30,7 @@ struct transceiver_config_t {
radio_proto proto = radio_proto::RTS;
uint8_t SCKPin = 18;
uint8_t TXPin = 13;
uint8_t RXPin = 12;
uint8_t RXPin = 35;
uint8_t MOSIPin = 23;
uint8_t MISOPin = 19;
uint8_t CSNPin = 5;

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();
}