# Zephyr project configuration for nRF54L15 Meshtastic port # # NOTE: this prj.conf is shared by ALL Zephyr PlatformIO environments # in this project. Keep it compatible with any future Zephyr targets. # ── C++ support (required by Meshtastic) ────────────────────────────────────── CONFIG_CPP=y CONFIG_STD_CPP17=y # Full libstdc++ — provides , , , , etc. # Works with either newlib or picolibc (Zephyr auto-selects based on board). CONFIG_REQUIRES_FULL_LIBCPP=y # Disable C++ exceptions — not needed by Meshtastic and saves RAM/ROM CONFIG_CPP_EXCEPTIONS=n # ── Peripheral subsystems ───────────────────────────────────────────────────── CONFIG_SPI=y CONFIG_I2C=y CONFIG_GPIO=y # sys_reboot() used by BLE zombie-connection watchdog (BleDeferredThread). # nRF54L15 SW-LL occasionally drops the BLE link without forwarding # LE Disconnection Complete to the host; cold reboot is the only reliable # recovery path. CONFIG_REBOOT=y # ── ATT Prepare/Execute Write (LONG WRITE) ─────────────────────────────────── # iOS CoreBluetooth automatically fragments writes >MTU-3 via ATT Prepare Write # (opcode 0x16). Default CONFIG_BT_ATT_PREPARE_COUNT=0 makes Zephyr reject with # "Request Not Supported" (0x06), which iOS surfaces as a write error → # disconnect. With MTU=65 any ToRadio write >62 bytes triggers this path. # Enabling this allocates N prep_pool buffers (each BT_ATT_BUF_SIZE = 65 bytes) # and reassembles fragments into a single write_toradio() call on execute. # 4 × 65 = 260 B max assembled value — enough for typical iOS NodeInfo/admin # writes after config stream completes. CONFIG_BT_ATT_PREPARE_COUNT=4 # ── Filesystem — LittleFS on storage_partition (RRAM) ─────────────────────── # Size is set by the board overlay (the nRF54L15-DK overlay reclaims slot1 to # expand storage_partition to ~700 KB). Capacity is reported at runtime via # FIXED_PARTITION_SIZE(storage_partition) in InternalFileSystem::totalBytes(). CONFIG_FLASH=y CONFIG_FLASH_MAP=y CONFIG_FLASH_PAGE_LAYOUT=y CONFIG_FILE_SYSTEM=y CONFIG_FILE_SYSTEM_LITTLEFS=y CONFIG_FILE_SYSTEM_MKFS=y # Disable SPI NOR flash driver — MX25R64 node deleted from DTS, SPIM00 used # exclusively by RadioLib (SX1262). Without this, the spi_nor driver claims # SPIM00 at boot and tries to read MX25R64 ID (gets garbage since the chip is # not wired), producing "Device id a8 a8 a8 does not match config c2 28 17". CONFIG_SPI_NOR=n # Disable runtime PM — keeps SPI initialization path simple; avoids any # interaction between PM auto-suspend/resume cycles and the SPIM00 clock # request mechanism (CONFIG_CLOCK_CONTROL_NRF_HSFLL_GLOBAL). CONFIG_PM_DEVICE_RUNTIME=n # Suppress Zephyr FS subsystem's internal error/warning logs (ENOENT on # missing files and EEXIST on duplicate mkdir are expected and handled). CONFIG_FS_LOG_LEVEL_OFF=y # ── Console / logging ───────────────────────────────────────────────────────── # Use SEGGER RTT for console — does not require COM3 (CDC UART), reads via SWD CONFIG_UART_CONSOLE=n CONFIG_USE_SEGGER_RTT=y CONFIG_RTT_CONSOLE=y CONFIG_LOG=y CONFIG_LOG_BACKEND_RTT=y CONFIG_LOG_DEFAULT_LEVEL=2 # Immediate mode: log writes go directly to RTT backend without a separate thread. # Deferred mode requires the log thread to run (lowest priority — never gets CPU # in heavy setup()/loop() workloads), leaving the RTT buffer empty indefinitely. CONFIG_LOG_MODE_IMMEDIATE=y # Force RTT control block re-init on every boot — prevents stale/corrupted CB after crash CONFIG_SEGGER_RTT_INIT_MODE_ALWAYS=y # Use RTT channel 1 for the LOG backend, channel 0 (Terminal) for direct printk. # Sharing channel 0 forces LOG_PRINTK=y (deferred) to avoid corruption. CONFIG_LOG_BACKEND_RTT_BUFFER=1 # Buffer sizes shrunk from 24576 → 4096 to free ~40 KB of BSS for newlib heap. # At 24576 the BSS pushed _end up so far that newlib heap was only ~25 KB, # and BUF_ACL_RX_SIZE=152 + BLE/PhoneAPI lazy init ran out of malloc space. # 4 KB still gives several seconds of log retention before host attaches. CONFIG_LOG_BACKEND_RTT_BUFFER_SIZE=4096 # Overwrite oldest data if buffer fills — never stalls CONFIG_LOG_BACKEND_RTT_MODE_BLOCK=n CONFIG_LOG_BACKEND_RTT_MODE_OVERWRITE=y CONFIG_LOG_BACKEND_RTT_OUTPUT_BUFFER_SIZE=256 # ── LFXO clock source — use RC oscillator to avoid ~2s crystal stabilization # disrupting the GRTC timer and hanging k_sleep CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y # ── Stack sizes — Meshtastic setup() is heavy (RadioLib, NodeDB, printf) ────── # bt_enable() called from nrf54l15Setup() needs >8KB. # Phase 7: CONFIG_BT_SETTINGS=y causes bt_set_name() → settings_save_one() → # settings_file_save() → LittleFS I/O. The I/O chain needs ~3 KB of stack # headroom beyond what the BT init alone requires. Increase main stack to 24KB # and system workqueue to 8KB to cover both the cooperative-OSThread call path # (which runs on the main thread) and any async flash work items. CONFIG_MAIN_STACK_SIZE=24576 CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=8192 # Log processing thread stack — default 768 overflows when processing RTT fault dump CONFIG_LOG_PROCESS_THREAD_STACK_SIZE=2048 # ── Fault/exception diagnostics — identify ~2000ms crash ───────────────────── CONFIG_FAULT_DUMP=2 CONFIG_EXCEPTION_DEBUG=y CONFIG_STACK_SENTINEL=y # Thread names + extra exception info — fault dumps then identify the failing # thread (otherwise "Current thread: 0x... (unknown)") and include r4-r11 + psp # so the custom k_sys_fatal_error_handler can walk the stack to show the caller # chain. Cheap (~32 B/thread for names, no perf hit) and very useful when a # crash recurs in the field. CONFIG_THREAD_NAME=y CONFIG_EXTRA_EXCEPTION_INFO=y # SEGGER RTT buffer — keep modest (4 KB) to leave RAM for newlib heap. CONFIG_SEGGER_RTT_BUFFER_SIZE_UP=4096 # Report reset reason from previous crash CONFIG_HWINFO=y # ── Bluetooth ───────────────────────────────────────────────────────────────── # Zephyr BT host + LL SW controller (MPSL) — peripheral role only CONFIG_BT=y CONFIG_BT_PERIPHERAL=y # SMP / LE Encryption enabled so the Meshtastic app pairs with a PIN before # accessing the GATT service. CONFIG_BT_SMP=y # Enforce MITM so clients must complete passkey exchange — without this Just # Works pairings complete silently without prompting the user for a PIN. CONFIG_BT_SMP_ENFORCE_MITM=y # Fixed-passkey path so the device (no display) can advertise a known PIN via # bt_passkey_set() when config.bluetooth.mode == FIXED_PIN. CONFIG_BT_FIXED_PASSKEY=y # Allow legacy pairing as fallback. SC_PAIR_ONLY=y has been observed to cause # some clients to abort pairing with reason 0x01 within 150 ms of the pairing # request, before any PIN dialog appears. Accepting legacy lets the same # clients fall through to Passkey Entry successfully. CONFIG_BT_SMP_SC_PAIR_ONLY=n # BT_LL_SW_SPLIT is auto-selected from DT (zephyr,bt-hci-ll-sw-split node in nRF54L15 DTS) # Do NOT set CONFIG_BT_CTLR=y (deprecated — radio silently non-functional) # Dynamic device name so bt_set_name() can embed the node short ID at runtime CONFIG_BT_DEVICE_NAME_DYNAMIC=y CONFIG_BT_DEVICE_NAME_MAX=32 # Only need one simultaneous central connection CONFIG_BT_MAX_CONN=1 # BT subsystem logging — INF for connection/service diagnostics CONFIG_BT_LOG_LEVEL_INF=y # BT thread stacks — defaults are too small for nRF54L15 SW-LL init. # prio_recv_thread overflows at 2048; bump all BT stacks to safe sizes. # BT RX thread runs ALL our GATT write callbacks (rx_work_handler → hci_acl → # bt_conn_recv → bt_l2cap_recv → bt_att_recv → write_toradio_cb → # PhoneAPI::handleStartConfig → getFiles("/", 10) recursion + nanopb encode). # 4096 overflows on "Client wants config" → abort() / kernel panic (reason 4). CONFIG_BT_RX_STACK_SIZE=4096 CONFIG_BT_HCI_TX_STACK_SIZE=1024 # bt_long_wq runs bt_pub_key_gen (ECC P256 keygen) on this thread. # Defaults (prio=10, stack=1400) leave it starved by Meshtastic app threads # at boot: pub_key gen never completes, so smp_public_key() defers # indefinitely waiting for sc_public_key, and every SC pairing attempt # stalls right after exchanging public keys (no PIN prompt on iOS, every # AUTHEN-gated char rejects with ATT error 0x05). # Prio 0 = highest preemptible, ties with main; stack 4096 clears P256M # driver frames with margin. CONFIG_BT_LONG_WQ_PRIO=0 CONFIG_BT_LONG_WQ_STACK_SIZE=4096 # Use legacy advertising (bt_le_adv_start / HCI 0x2006 path). # With CONFIG_BT_EXT_ADV=y, bt_le_adv_start() is internally translated to the # extended HCI path with LEGACY-bit (0x2036), which produces non-connectable PDUs # on the nRF54L15 SW-LL. With CONFIG_BT_EXT_ADV=n the host uses pure legacy HCI # commands (0x2006/0x2008/0x200a) — the same path Nordic uses in all nRF54L15 # NCS examples (peripheral_uart, peripheral_lbs), which is iOS-compatible and # avoids the LE Remove Advertising Set (0x203c) controller timeout crash. CONFIG_BT_EXT_ADV=n # ── Phase 7: BT bond persistence ────────────────────────────────────────────── # CONFIG_BT_SETTINGS enables the BT host settings integration: the stack # automatically calls settings_save_subtree("bt/keys") after pairing, and # settings_load() on boot restores previously bonded peers. # # Backend: SETTINGS_FILE stores all key-value pairs in a single flat file in # LittleFS. No new partition needed — the existing storage_partition (mounted # at /lfs, size set by the board overlay) is used. File path: /lfs/bt_settings. # # Ordering guarantee: LittleFS is mounted by fsInit() BEFORE nrf54l15Setup() # calls nrf54l15_bt_preinit(), so the file backend is always available when # settings_load() is called after bt_enable(). CONFIG_BT_SETTINGS=y CONFIG_SETTINGS=y CONFIG_SETTINGS_FILE=y CONFIG_SETTINGS_FILE_PATH="/lfs/bt_settings" # BT_MAX_PAIRED default is 1 — first bond (e.g. iOS) blocks every subsequent # peer's SMP pairing request with "Unable to get keys" because there is no free # bt_keys slot to allocate. Raise to 4 so the device can simultaneously hold # iOS, Windows, Linux, and one spare bond. Add OVERWRITE_OLDEST so that when # the table fills, the LRU peer is evicted instead of rejecting the new pair. CONFIG_BT_MAX_PAIRED=4 CONFIG_BT_KEYS_OVERWRITE_OLDEST=y # Disable GATT database caching and Service Changed characteristic. # CONFIG_BT_GATT_CACHING (default y with BT_SETTINGS) marks every new client as # "not change-aware" and returns ATT_ERR_DB_OUT_OF_SYNC (0x12) on every GATT # request until the client reads the DB-hash characteristic. The Meshtastic app # does not implement GATT caching and silently aborts service discovery on 0x12, # causing the connection to stall with zero GATT activity. # CONFIG_BT_GATT_SERVICE_CHANGED (default y) adds the Generic Attribute Profile # service; disabling it is required before BT_GATT_CACHING can be disabled. CONFIG_BT_GATT_SERVICE_CHANGED=n CONFIG_BT_GATT_CACHING=n # Disable automatic PHY update (1M→2M) after connection. # The nRF54L15 SW-LL fails the LL_PHY_REQ/RSP exchange and disconnects # exactly 1.786s after connection — before any ATT/GATT operations. CONFIG_BT_AUTO_PHY_UPDATE=n # ATT/GATT/L2CAP debug logging — see exactly what happens after connection CONFIG_BT_ATT_LOG_LEVEL_DBG=y CONFIG_BT_GATT_LOG_LEVEL_DBG=y CONFIG_BT_SMP_LOG_LEVEL_DBG=y # L2CAP DBG: shows recv on fixed ATT channel — confirms whether iOS sends any data CONFIG_BT_L2CAP_LOG_LEVEL_DBG=y # Keep bt_conn at INF — DBG floods RTT buffer every ~150µs (tx_processor loop), # overwriting all ATT/GATT messages before they can be read. # Connection events (connected/disconnected) are logged at INF level. CONFIG_BT_CONN_LOG_LEVEL_INF=y # Keep HCI logs at INF to save RAM (log thread processing buffers, etc.). # (Earlier DBG was used to diagnose the hci_acl → L2CAP stall — fix applied.) CONFIG_BT_HCI_CORE_LOG_LEVEL_INF=y CONFIG_BT_HCI_DRIVER_LOG_LEVEL_INF=y # Fix: ACL packets reach hci_acl() but never reach bt_l2cap_recv(). # Root cause: bt_conn_recv() calls bt_conn_tx_notify(conn, true) which submits # tx_complete_work to k_sys_work_q and blocks on k_work_flush(). The BT rx # workqueue (bt_workq) is stuck in k_work_flush waiting for the system # workqueue, which is busy with LittleFS I/O / other work → dead stall until # iOS supervision timeout fires (5s) and disconnects with reason 0x13. # Solution: dedicate a separate workqueue for TX notify processing so it is # independent from the system workqueue. CONFIG_BT_CONN_TX_NOTIFY_WQ=y # Dedicated workqueue only runs tx_notify_process() (iterates tx_complete list, # calls short callbacks). Default 8192 is overkill and eats malloc heap needed # by PowerFSM init → realloc() returns NULL → bus fault during FSM::add_transition. CONFIG_BT_CONN_TX_NOTIFY_WQ_STACK_SIZE=2048 # ── ATT/L2CAP MTU — larger payloads for Meshtastic packets ─────────────────── # TX side: controller sends up to L2CAP_TX_MTU bytes per ATT operation. # RX side: server ATT MTU is min(BT_L2CAP_TX_MTU, BT_BUF_ACL_RX_SIZE - 4). # Both set to 247 / 251 → ATT MTU = 247 in each direction, matching Zephyr's # samples/bluetooth/mtu_update reference. This means typical iOS ToRadio # writes (NodeInfo, channel settings, common admin packets) fit in a single # ATT_WRITE_REQ and avoid the ATT Prepare/Execute Write path entirely. # CONFIG_BT_ATT_PREPARE_COUNT=4 (above) still backstops oversized writes. # # Heap dependency: bumping BUF_ACL_RX_SIZE > default (~69) grows the BT host # net_buf pools in BSS, which proportionally shrinks the newlib heap arena # (MAX_HEAP_SIZE = SRAM_SIZE - (_end - SRAM_BASE), so any BSS growth steals # from the heap directly). Empirically the lazy BLE init path # (setBluetoothEnable → startDisabled → bt_set_name → settings_save → # LittleFS) needs ~12 KB of newlib heap to run without bad_alloc. At # BUF_ACL_RX_SIZE=251 with the previous 24 KB RTT buffers (LOG_BACKEND_RTT + # SEGGER_RTT_BUFFER_SIZE_UP), the heap collapsed to ~4 KB free at # transition time → `new char[]` in RedirectablePrint::log returned NULL → # libstdc++ called abort() from main thread. Shrinking both RTT buffers to # 4 KB (above) frees ~40 KB of BSS for the heap and resolves it. # # DLE stays off (BT_DATA_LEN_UPDATE=n below): the LLCP remote table at # ull_llcp_remote.c:878 is guarded by #ifdef CONFIG_BT_CTLR_DATA_LENGTH, so # the controller answers iOS's LL_LENGTH_REQ with LL_UNKNOWN_RSP and falls # back to 27-byte LL PDUs. The host reassembles LL PDUs into L2CAP frames # up to BT_BUF_ACL_RX_SIZE before dispatching to ATT. CONFIG_BT_L2CAP_TX_MTU=247 # Server ATT MTU = BUF_ACL_RX_SIZE - 4 = 247 (matches L2CAP_TX_MTU) CONFIG_BT_BUF_ACL_RX_SIZE=251 # ── Fix: LL Feature Exchange collision (ROOT CAUSE of iOS GATT hang) ───────── # On connection, Zephyr host calls bt_hci_le_read_remote_features() because # BT_CTLR_PER_INIT_FEAT_XCHG=y makes can_initiate_feature_exchange() return # true for peripheral role. This makes the controller send LL_PER_INIT_FEAT_XCHG # to iOS right after connecting. # iOS (as central) simultaneously sends LL_FEATURE_REQ to the peripheral. # The nRF54L15 SW-LL mishandles this COLLISION: iOS waits for LL_FEATURE_RSP # to its LL_FEATURE_REQ, never gets it, and stalls — sending zero L2CAP bytes. # BT_CTLR_PER_INIT_FEAT_XCHG=n: host does NOT send HCI_LE_Read_Remote_Features # as peripheral → no LL_PER_INIT_FEAT_XCHG sent → no collision → iOS feature # exchange completes → iOS proceeds to L2CAP/ATT. CONFIG_BT_CTLR_PER_INIT_FEAT_XCHG=n # Fix: LL Connection Parameter Request handling. # BT_CTLR_CONN_PARAM_REQ=n was ineffective: the LLCP remote decode table in # ull_llcp_remote.c hardcodes PDU_DATA_LLCTRL_TYPE_CONN_PARAM_REQ → PROC_CONN_PARAM_REQ # regardless of Kconfig. With =n the handler is compiled out → controller asserts / # enters broken state when iOS sends LL_CONN_PARAM_REQ (which is optional from Central). # Fix: =y so the procedure is actually handled. To avoid host/peripheral vs Central # collision at 5 s (deferred_work → send_conn_le_param_update), disable auto-update # below so the host never initiates HCI_LE_Connection_Update. CONFIG_BT_CTLR_CONN_PARAM_REQ=y # Prevent Zephyr host from initiating connection parameter update 5 s after connect. # With CONN_PARAM_REQ=y, if iOS (Central) already issued LL_CONN_PARAM_REQ and the # SW-LL is mid-procedure, a simultaneous host-initiated HCI_LE_Connection_Update # creates an LL collision. Disabling the auto-update avoids the collision entirely. CONFIG_BT_GAP_AUTO_UPDATE_CONN_PARAMS=n # Disable optional LL procedures (belt-and-suspenders while debugging): # BT_PHY_UPDATE=n + BT_CTLR_PHY_2M=n: no PHY update procedure → iOS doesn't attempt LL_PHY_REQ # BT_DATA_LEN_UPDATE=n: no DLE → controller sends LL_UNKNOWN_RSP to LL_LENGTH_REQ CONFIG_BT_CTLR_PHY_2M=n CONFIG_BT_PHY_UPDATE=n CONFIG_BT_DATA_LEN_UPDATE=n