Redesign BT1035 RESET#/SYS_CTRL handling, add CTS/RTS diagnostics

RESET# (pin 8) is no longer driven at all: reconfigured as a floating
input relying entirely on the module's own internal pull-up per
datasheet §4.8, as a diagnostic test to rule out host-side RESET# drive
as a contributor to intermittent boot failures.

SYS_CTRL (pin 34) now performs a genuine LOW(2.5s)->HIGH power-cycle on
every resetAndInitOnce() call rather than being asserted once ever:
previously every later retry from bt1035RetryTask silently reused an
already-HIGH SYS_CTRL line without ever actually power-cycling the
module.

Added read-only diagnostics on the CTS/RTS pins (physically wired,
named in board_pins.hpp since their original definition, never
configured by any driver code, host flow control disabled) to observe
their level around the boot-banner wait, after reviewing a sibling
project's PinScope report and re-reading the datasheet's UART flow
control and PA_MUTE default-function documentation.

Across ~45 minutes of live testing after these changes, zero successful
boots were observed - inconclusive on whether this improves anything,
but each change is independently correct per the datasheet. Escalated
to Feasycom support with the full findings. Documented in the RF
investigation report and TODO.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 13:58:27 +02:00
co-authored by Claude Sonnet 5
parent 3ffc9930cd
commit a91c63fc31
5 changed files with 239 additions and 36 deletions
@@ -36,14 +36,18 @@ constexpr int kBaudRate = 115200;
constexpr int kUartRxBuffer = 4096;
constexpr int kUartTxBuffer = 256;
constexpr int kResponseTimeoutMs = 2000;
constexpr int kPostResetMs = 500;
constexpr int kPostUartMs = 100;
/** Feasycom BT1035 programming user guide §2.2 (pin 34 SYS_CTRL): "Delay
* 100ms, pull high". */
constexpr int kSysCtlLeadInMs = 100;
/** Margin beyond the datasheet's own >20ms SYS_CTRL-assertion-to-power-up
* minimum (§4.7), for regulator/crystal settling before RESET releases. */
constexpr int kSysCtlSettleMs = 50;
/** Datasheet §4.7: "From the OFF state, SYS_CTRL must be asserted for
* >20 ms to start power up." */
constexpr int kSysCtlAssertMs = 20;
/** Datasheet §4.8: "Reset Protection timeout (typically greater than
* ~1.8 s) causes the device to power down if VCHG is not present and
* SYS_CTRL is low." Held comfortably longer than that before each
* power-up assert, to guarantee a genuine full power-down rather than a
* pulse too short for the module's own protection timer to act on — see
* resetAndInitOnce()'s comment for why this now runs on every attempt,
* not just the first. */
constexpr int kSysCtlDeassertMs = 2500;
/** Measured live (2026-08-20, power/wiring confirmed sound with a
* multimeter — VBAT_IN/SYS_CTRL/1.8V_OUT/VDD_IO all correct, TX/RX pins
* verified via continuity): the module's spontaneous boot banner
@@ -970,35 +974,47 @@ std::expected<void, Bt1035Error> Bt1035Driver::runInitSequence()
std::expected<void, Bt1035Error> Bt1035Driver::resetAndInitOnce()
{
// Feasycom BT1035 programming user guide §2.2, pin 34 SYS_CTRL:
// "Delay 100ms, pull high" — the datasheet's own OFF-state timing spec
// (§4.7) says SYS_CTRL must be asserted >20ms before the internal
// regulators start powering up at all, so pulling it high with no
// lead-in delay (the previous sequence here) races the chip's own
// power-on requirement. Held low with RESET already asserted, then a
// 100ms lead-in exactly matching the guide, then SYS_CTRL high, then
// extra settle time before releasing RESET into a chip that's had a
// chance to actually power up first.
// RESET# (pin 8) is deliberately never driven by this driver (see
// boot()'s GPIO config below): it's left a floating input, relying
// entirely on the BT1035's own "fixed strong pull-up to VDD_IO"
// (datasheet §4.8), which the datasheet explicitly says means the pin
// "can therefore be left unconnected". Diagnostic test (2026-08-22) to
// check whether the previous external RESET# drive was contributing to
// the intermittent boot failures.
//
// SYS_CTRL (pin 34) alternative usage (2026-08-22 experiment): drive a
// genuine LOW-then-HIGH cycle every time this function runs, not just
// once ever. With RESET# now Hi-Z, SYS_CTRL is the only pin this
// driver can still use to force a real power-cycle — previously it
// was asserted HIGH exactly once at first boot and never touched
// again, which meant every later retry (hardware::bt1035RetryTask
// calls boot() again on failure) just re-listened on an
// already-asserted line without ever actually power-cycling the
// module. Held LOW for kSysCtlDeassertMs first so the module's own
// Reset Protection timeout actually elapses (a shorter pulse risks
// the module staying "protected" on, per datasheet §4.8, so the
// retry wouldn't be a real fresh power-on at all), then HIGH for the
// datasheet's own >=20ms minimum.
const auto sysCtlPin = static_cast<gpio_num_t>(pins_.sysCtlGpio);
const auto resetPin = static_cast<gpio_num_t>(pins_.resetGpio);
const auto ctsPin = static_cast<gpio_num_t>(pins_.ctsGpio);
const auto rtsPin = static_cast<gpio_num_t>(pins_.rtsGpio);
gpio_set_level(sysCtlPin, 0);
gpio_set_level(resetPin, 0);
vTaskDelay(pdMS_TO_TICKS(kSysCtlLeadInMs));
ESP_LOGI(kTag, "pre-power: SYS_CTRL=%d RESET=%d (want 0,0)",
gpio_get_level(sysCtlPin), gpio_get_level(resetPin));
vTaskDelay(pdMS_TO_TICKS(kSysCtlDeassertMs));
gpio_set_level(sysCtlPin, 1);
vTaskDelay(pdMS_TO_TICKS(kSysCtlSettleMs));
ESP_LOGI(kTag, "post-syscl: SYS_CTRL=%d RESET=%d (want 1,0)",
gpio_get_level(sysCtlPin), gpio_get_level(resetPin));
gpio_set_level(resetPin, 1);
vTaskDelay(pdMS_TO_TICKS(kPostResetMs));
ESP_LOGI(kTag, "post-reset: SYS_CTRL=%d RESET=%d (want 1,1)",
vTaskDelay(pdMS_TO_TICKS(kSysCtlAssertMs));
ESP_LOGI(kTag, "power-up: SYS_CTRL=%d (want 1, driven) RESET#=%d "
"(want 1, Hi-Z + internal pull-up, not driven by us)",
gpio_get_level(sysCtlPin), gpio_get_level(resetPin));
ESP_LOGI(kTag, "before banner wait: CTS=%d (module's flow-control "
"input, host side floating) RTS/PIO2=%d (module's "
"PA_MUTE by factory default)",
gpio_get_level(ctsPin), gpio_get_level(rtsPin));
logRawUartBoot(uartPort_);
ESP_LOGI(kTag, "after banner wait: CTS=%d RTS/PIO2=%d",
gpio_get_level(ctsPin), gpio_get_level(rtsPin));
uart_flush_input(static_cast<uart_port_t>(uartPort_));
vTaskDelay(pdMS_TO_TICKS(kPostUartMs));
@@ -1011,18 +1027,25 @@ std::expected<void, Bt1035Error> Bt1035Driver::boot()
return {};
}
// GPIO_MODE_INPUT_OUTPUT (not plain GPIO_MODE_OUTPUT): gpio_config()
// only enables the pad's input buffer when the INPUT bit is set, so a
// pure-output config leaves gpio_get_level() reading a stale/always-0
// register instead of the real driven level — needed for the readback
// diagnostic below to be meaningful.
// RESET# (pin 8) is never driven by this driver (2026-08-22 diagnostic
// change): configured as a pure floating input, pull-up/pull-down both
// explicitly disabled, so nothing on the ESP32 side influences this
// net electrically — the BT1035's own internal RESET# pull-up (§4.8)
// is the only thing holding it high. GPIO_MODE_INPUT (not OUTPUT) still
// lets gpio_get_level() read it back for the diagnostic log below,
// without ever driving it.
gpio_config_t resetCfg = {};
resetCfg.pin_bit_mask = 1ULL << pins_.resetGpio;
resetCfg.mode = GPIO_MODE_INPUT_OUTPUT;
resetCfg.mode = GPIO_MODE_INPUT;
resetCfg.pull_up_en = GPIO_PULLUP_DISABLE;
resetCfg.pull_down_en = GPIO_PULLDOWN_DISABLE;
if (gpio_config(&resetCfg) != ESP_OK) {
return std::unexpected(Bt1035Error::ResetFailed);
}
// SYS_CTRL (pin 34) stays actively driven (GPIO_MODE_INPUT_OUTPUT:
// the INPUT bit is what makes gpio_get_level() read the real driven
// level instead of a stale register, for the diagnostic log).
gpio_config_t sysCfg = {};
sysCfg.pin_bit_mask = 1ULL << pins_.sysCtlGpio;
sysCfg.mode = GPIO_MODE_INPUT_OUTPUT;
@@ -1030,6 +1053,35 @@ std::expected<void, Bt1035Error> Bt1035Driver::boot()
return std::unexpected(Bt1035Error::ResetFailed);
}
// CTS (module pin 15, host->module) and RTS (module pin 16/PIO2,
// factory default function PA_MUTE per the Feasycom programming
// guide) — diagnostic only (2026-08-22): this driver does not
// implement UART hardware flow control (UART_HW_FLOWCTRL_DISABLE
// below), so these are wired but otherwise unused. Configured as
// floating inputs, pull-up/pull-down both disabled, purely to read
// back their level for the diagnostic log — never driven. Exploring
// whether the module's CTS input floating could be gating its own
// UART TX (a common hardware-flow-control behavior), and whether RTS
// toggles at all (would indicate the module's internal firmware is
// alive even when UART TX is silent).
gpio_config_t ctsCfg = {};
ctsCfg.pin_bit_mask = 1ULL << pins_.ctsGpio;
ctsCfg.mode = GPIO_MODE_INPUT;
ctsCfg.pull_up_en = GPIO_PULLUP_DISABLE;
ctsCfg.pull_down_en = GPIO_PULLDOWN_DISABLE;
if (gpio_config(&ctsCfg) != ESP_OK) {
return std::unexpected(Bt1035Error::ResetFailed);
}
gpio_config_t rtsCfg = {};
rtsCfg.pin_bit_mask = 1ULL << pins_.rtsGpio;
rtsCfg.mode = GPIO_MODE_INPUT;
rtsCfg.pull_up_en = GPIO_PULLUP_DISABLE;
rtsCfg.pull_down_en = GPIO_PULLDOWN_DISABLE;
if (gpio_config(&rtsCfg) != ESP_OK) {
return std::unexpected(Bt1035Error::ResetFailed);
}
if (!uartInstalled_) {
const uart_config_t uartCfg = {
.baud_rate = kBaudRate,