# ESP32 Platform Configuration # Included by all ESP32-based boards (XIAO ESP32-C3, XIAO ESP32-C6, etc.) # Only contains ESP32-specific settings — common BLE/Storage/Input is in zephcore_common.conf # # Include order: prj.conf → zephcore_common.conf → esp32_common.conf → /board.conf # ========== BLE: ESP32-specific controller settings ========== # DLE managed internally by Espressif blob — no CONFIG_BT_CTLR_DATA_LENGTH_MAX # BLE TX power — +9 dBm, matching stock Arduino MeshCore. # # This was previously left unset, on the belief that the Espressif blob managed # TX power internally. It does not. Zephyr's HCI driver initialises the # controller with BT_CONTROLLER_INIT_CONFIG_DEFAULT() (hci_esp32.c), whose # .txpwr_dft comes from CONFIG_ESP32_BT_CTLR_DFT_TX_POWER_LEVEL_EFF — and that # macro is a #if chain over Zephyr's CONFIG_BT_CTLR_TX_PWR_* symbols with an # #else /* use 0dB TX power as default */ # fallback. With none of them set the chain fell through and every ESP32 board # advertised and connected at 0 dBm, while stock Arduino MeshCore inherits the # Arduino-ESP32 sdkconfig (CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P9=y) and runs at # +9 dBm. That 9 dB deficit is the reported "ZephCore has worse BLE range than # official firmware" on Heltec V4 — roughly 2.8x the free-space distance. # # The symbols ARE settable here: BT_ESP32 selects HAS_BT_CTLR, and upstream # gates the +9/+12/+15/+18/+21 options on `depends on SOC_FAMILY_ESPRESSIF_ESP32` # specifically so ESP32 boards can pick one. # # Why +9 and not the +20 dBm ceiling the S3/C3/C6 parts can reach: +9 is what # stock MeshCore runs, it is comfortably inside the EU 2.4 GHz 100 mW (20 dBm) # EIRP limit, and it is the ONLY level that appears in all three Espressif # header families — so one line covers every board with no per-board override: # # include/esp32c3 (also S3) PLUS_9 -> ESP_PWR_LVL_P9 ceiling +20 dBm # include/esp32c6 (c5/h2) PLUS_9 -> literal 9 ceiling +20 dBm # include/esp32 (classic) PLUS_9 -> ESP_PWR_LVL_P9 ceiling +9 dBm # # That last row is the trap if anyone raises this later: the classic ESP32 # (ttgo_tbeam, ttgo_lora32) tops out at ESP_PWR_LVL_P9 and its chain has no # PLUS_12/15/18/21 arm at all, so any higher setting matches nothing there and # silently drops those two boards back to the 0 dBm `#else` — no warning, no # build error. Raising this above +9 therefore requires a PLUS_9 override in # both classic board.conf files. # # nRF/nRF54L use CONFIG_BT_CTLR_TX_PWR_PLUS_8 (their own ceiling); +8 is not # selectable on ESP32 and the ESP32 power enum has no P8 — 3 dB steps only. CONFIG_BT_CTLR_TX_PWR_PLUS_9=y # Host privacy: ESP32-only override. Load-bearing for iOS on the Espressif # controller (Zephyr #84182-class). nRF/MG24 keep privacy OFF. # # Root cause pinned 2026-06-15 by firmware BLE/SMP debug capture on shipping # blob 19f979cfe6 (xiao_esp32s3, real iOS): with privacy OFF the SC pairing # completes the full passkey + DHKey-check phase, the host hands the controller # a valid LTK (HCI LTK Request Reply, status 0x00), then the controller fails # to start link encryption — HCI Disconnect reason 0x3d (MIC failure), Encrypt # Change status 0x1f. It is a bug in the Espressif controller's privacy-OFF # Secure-Connections path (closed blob); the host did everything right (nRF runs # the identical host SMP code and bonds with iOS privacy-OFF fine). Privacy ON # keeps the controller on its working privacy-enabled SC path. # # Android "connect from app" is NOT sacrificed: ZephyrBLE advertises with # BT_LE_ADV_OPT_USE_IDENTITY, so even with privacy ON we expose the stable # identity address (no RPA) — the thing the Android companion needs. HW- # confirmed: iOS pairs + syncs, normal disconnect 0x13. CONFIG_BT_PRIVACY=y # BLE thread stacks — ESP32 software BLE controller needs larger stacks. # Zephyr 2026-Q1 added a synchronous sc_store() on the RX WQ in the # identity-resolved path (commit 2c6c80ddf65) which pushes the LittleFS # settings write onto the SMP callback chain. Default 2048 overflowed. CONFIG_BT_TX_PROCESSOR_STACK_SIZE=4096 CONFIG_BT_RX_STACK_SIZE=4096 # Stack-overflow detection — xtensa has no MPU stack guard in Zephyr, so # use the software sentinel. ARM boards rely on MPU_STACK_GUARD (already # on via HW_STACK_PROTECTION) which is incompatible with this symbol. CONFIG_STACK_SENTINEL=y # BLE TX buffers — the Espressif BLE controller blob bursts more aggressively # than the nRF softdevice and will deadlock the system workqueue if ACL TX # buffers run dry (bt_hci_cmd_alloc(K_FOREVER) blocks the cooperative syswq). # EVT_RX must strictly exceed ACL_TX (enforced by BUILD_ASSERT in buf.h). CONFIG_BT_BUF_ACL_TX_COUNT=12 CONFIG_BT_L2CAP_TX_BUF_COUNT=12 CONFIG_BT_CONN_TX_MAX=12 CONFIG_BT_BUF_EVT_RX_COUNT=14 # BLE controller link-layer encryption — Zephyr 4.4.1 (commit 7f01467, "align # esp32 controller knobs") re-gated this vendor default from unconditional # `default y` to `default y if BT_CTLR_LE_ENC`. ESP32 BLE uses the EXTERNAL # Espressif controller over HCI (BT_ESP32=y, BT_CTLR unset), so that gate is # never satisfied -> the controller's LE encryption gets compiled out while host # BT_SMP stays on. The controller then asserts (ke_event.c) the moment a peer # begins pairing: advertising works, connect never completes (GH #34). Restore # the pre-4.4.1 default. (Classic ESP32 / S3 / C3 controller; the C2/C5/C6/H2 # controller uses ESP32_BT_LE_SECURITY_ENABLE instead — set in those board.conf.) CONFIG_ESP32_BT_CTLR_LE_SECURITY_ENABLE=y # ========== Heap ========== # ESP32 BLE stack requires larger heap (override zephcore_common 2KB default). # # 32768 was NOT enough and failed non-obviously. The Espressif controller # allocates its exchange memory through esp_bt_malloc_func(), which the Zephyr # port maps to k_malloc() (esp_heap_adapter.h, CONFIG_ESP_BT_HEAP_SYSTEM=y) — # i.e. straight out of this pool. esp_bt_controller_init() asks for 0x7800 = # 30720 bytes in ONE allocation: 93.75% of a 32 KB pool, leaving 2 KB for block # overhead and every other k_malloc user that ran before bt_enable(). Boards # that allocated little before BLE init happened to fit; ThinkNode M9 (display + # colour overlay, STC8H keypad, LR1110) did not, and the blob asserted inside # its own allocator instead of returning ESP_ERR_NO_MEM: # # BLE assert emi.c 164, param 00000000 00007800 <- (NULL, requested size) # # Zephyr's own driver anticipates this and prints "Consider increasing # CONFIG_HEAP_MEM_POOL_SIZE" (drivers/bluetooth/hci/hci_esp32.c) — but only on # the graceful path the blob does not take. Every ESP32 board was one # allocation away from the same failure, so this is raised platform-wide. # # Headroom verified by build on all 12 shipped ESP32 boards: the S3/C3/C6 boards # have 66-135 KB free internal DRAM and absorb this easily. ttgo_tbeam is the # sole exception (99.45% DRAM, 768 B free) and overrides it back in its # board.conf — see the note there. CONFIG_HEAP_MEM_POOL_SIZE=65536 # ========== Flash ========== # 4MB is standard for XIAO/LilyGo RISC-V boards. # Station G2 (16MB) overrides in board.conf. CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y # ========== BLE bonds: dedicated NVS partition (not /lfs) ========== # Bonds live in their own `storage_partition` NVS region (defined per board in # board.overlay), isolated from the /lfs LittleFS volume that holds identity/ # prefs/contacts. This overrides the file-backed default in zephcore_common.conf # (loaded earlier): a busy bond store can no longer corrupt /lfs. Every ESP32 # board.overlay defines a storage_partition, so this is safe platform-wide. CONFIG_SETTINGS_FILE=n CONFIG_NVS=y CONFIG_SETTINGS_NVS=y # ========== Debug ========== # CONFIG_ASSERT disabled for ESP32: Espressif's closed-source BLE controller # blob leaves IRQs in a state that triggers false "Context switching while # holding lock!" assertions in kswap.h. The BLE stack works correctly — the # blob manages its own IRQ state internally. nRF52/nRF54L boards keep asserts # via their platform configs; ESP32 relies on logging for debug. # CONFIG_ASSERT=n (Zephyr default)