From a1cbd923288d0fb1a80f284d40e7f4164c61e07e Mon Sep 17 00:00:00 2001 From: Zhou Xiao Date: Wed, 2 Sep 2026 11:40:18 +0800 Subject: [PATCH 01/13] refactor(ble_log): preserve full HCI type byte in ble_log_write_hci ble_log_write_hci encoded the direction in bit 7 of the caller's type byte and then cleared that bit unconditionally after the write, leaving the caller's buffer mutated. Save the complete type byte, set bit 7 only around the synchronous copy, and restore it afterwards; the direction argument is now treated as a boolean. HCI H4 type bytes (0x01..0x04) always have bit 7 clear, so no legal input observes a behavior change. --- .../bt/common/ble_log/include/ble_log.h | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/components/bt/common/ble_log/include/ble_log.h b/components/bt/common/ble_log/include/ble_log.h index 0a358a34f34..7668faf4d48 100644 --- a/components/bt/common/ble_log/include/ble_log.h +++ b/components/bt/common/ble_log/include/ble_log.h @@ -45,14 +45,17 @@ typedef enum { #define BLE_LOG_HCI_DOWNSTREAM 0 #define BLE_LOG_HCI_UPSTREAM 1 -/* HCI Log Write Macro - * Encodes direction in MSB of data[0] (HCI type byte) before writing. - * Safe because ble_log_write_hex -> ble_log_lbm_write_trans does synchronous memcpy. - * Parser reads MSB to determine direction; old firmware with MSB=0 defaults to "sent". */ -#define ble_log_write_hci(direction, data, len) do { \ - (data)[0] |= ((direction) << 7); \ - ble_log_write_hex(BLE_LOG_SRC_HCI, (data), (len)); \ - (data)[0] &= 0x7F; \ +/* Encodes HCI direction in payload byte 0 bit 7 for the synchronous copy, + * then restores the complete original HCI type byte. The caller guarantees a + * non-NULL buffer with len > 0. */ +#define ble_log_write_hci(direction, data, len) do { \ + uint8_t *const ble_log_hci_data__ = (data); \ + const uint8_t ble_log_hci_type__ = ble_log_hci_data__[0]; \ + ble_log_hci_data__[0] = (ble_log_hci_type__ & 0x7fU) | \ + ((direction) ? 0x80U : 0U); \ + (void)ble_log_write_hex(BLE_LOG_SRC_HCI, ble_log_hci_data__, \ + (len)); \ + ble_log_hci_data__[0] = ble_log_hci_type__; \ } while (0) /* INTERFACE */ From e801a4d509204c6b2afb170e8ef7d880b2538a93 Mon Sep 17 00:00:00 2001 From: Zhou Xiao Date: Wed, 2 Sep 2026 11:40:22 +0800 Subject: [PATCH 02/13] feat(ble_log): switch to the unified transport pool Replace the legacy multi-LBM transport layer with one shared pool: - one bitmap-indexed pool of CONFIG_BLE_LOG_POOL_TRANS_CNT transports with a non-yield reserve for ISR and critical-section writers; - per-transport state machine (FREE/OPEN/CLAIMED/SENDING) guarded by a per-transport CAS lock; bitmaps are only candidate hints; - claim/commit interface for the compressed-log path; claim bookkeeping lives in the pool (ble_log_pool_claim_t), not in the peripheral transport; - dedicated Internal Snapshot transport and fixed-layout snapshot record (reason flags, version block, lc/esp/os clock samples, pool state, compact core statistics); - source-local 24-bit wire sequences and compact per-source counters; - UART console redirection stream writer (ble_log_redir_t); - CMakeLists compiles ble_log_lbm_v2.c instead of ble_log_lbm.c; the legacy ble_log_lbm.c/.h stay in-tree, out of the build, until their removal; - the public ble_log_src_t ABI is restored and frozen; protocol v7 wire source IDs are a separate internal enum (ble_log_wire_src_t) and the older BLE Stack sources map onto the v7 core sources (LL_TASK/LL_ISR -> LL, LL_HCI/HCI -> HCI, HOST -> CUSTOM); BLE_LOG_VERSION 7; - Kconfig: pool sizing options replace the LBM options, whose names remain as hidden deprecated symbols for backward sdkconfig compatibility; BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED becomes the single BLE_LOG_HCI_LOG_ENABLED switch for host and controller HCI traffic (Bluedroid and NimBLE host call sites follow the rename), enabled by default on all targets instead of only where BLE_LOG_IS_ESP_LEGACY_CONTROLLER; the old name keeps a deprecated promptless shim that selects the new symbol, so sdkconfig files still pinning it keep host HCI logging enabled; - util: ble_log_cas_acquire/release become macros; multi-call-site pool helpers are no longer inline (IRAM); - test app record structs gain a source_meta field for the v7 wire source byte; - the Internal Snapshot no longer carries a schema_version: the record is self-identifying via int_src_code and the version block. --- components/bt/common/CMakeLists.txt | 2 +- components/bt/common/ble_log/Kconfig.in | 156 +-- .../bt/common/ble_log/include/ble_log.h | 4 + components/bt/common/ble_log/src/ble_log.c | 30 +- .../bt/common/ble_log/src/ble_log_lbm_v2.c | 1209 +++++++++++++++++ components/bt/common/ble_log/src/ble_log_rt.c | 103 +- components/bt/common/ble_log/src/ble_log_ts.c | 2 + .../bt/common/ble_log/src/ble_log_util.c | 8 +- .../src/internal_include/ble_log_lbm_v2.h | 240 ++++ .../src/internal_include/ble_log_prph.h | 41 +- .../src/internal_include/ble_log_util.h | 29 +- .../ble_log/src/prph/ble_log_prph_dummy.c | 6 +- .../src/prph/ble_log_prph_spi_master_dma.c | 13 +- .../src/prph/ble_log_prph_spi_master_hd.c | 5 +- .../ble_log/src/prph/ble_log_prph_test.c | 2 +- .../ble_log/src/prph/ble_log_prph_uart_dma.c | 161 ++- .../main/test_ble_log_main.h | 1 + .../ble_log_rt_test/main/test_ble_log_main.h | 1 + .../ble_log_test/main/test_ble_log_main.h | 1 + .../bt/host/bluedroid/api/esp_bluedroid_hci.c | 4 +- components/bt/host/bluedroid/hci/hci_hal_h4.c | 4 +- .../host/nimble/esp-hci/src/esp_nimble_hci.c | 8 +- 22 files changed, 1786 insertions(+), 244 deletions(-) create mode 100644 components/bt/common/ble_log/src/ble_log_lbm_v2.c create mode 100644 components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h diff --git a/components/bt/common/CMakeLists.txt b/components/bt/common/CMakeLists.txt index dc8a30653b8..1c39e904b4d 100644 --- a/components/bt/common/CMakeLists.txt +++ b/components/bt/common/CMakeLists.txt @@ -170,7 +170,7 @@ list(APPEND bt_common_priv_include_dirs if(CONFIG_BLE_LOG_ENABLED) list(APPEND bt_common_srcs "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log.c" - "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_lbm.c" + "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_lbm_v2.c" "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_rt.c" "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_util.c" ) diff --git a/components/bt/common/ble_log/Kconfig.in b/components/bt/common/ble_log/Kconfig.in index 8888ab70559..f2e6b850bcd 100644 --- a/components/bt/common/ble_log/Kconfig.in +++ b/components/bt/common/ble_log/Kconfig.in @@ -1,55 +1,39 @@ config BLE_LOG_ENABLED bool "Enable BT Log Async Output (Dev Only)" select BLE_COMPRESSED_LOG_ENABLE - select BLE_HOST_COMPRESSED_LOG_ENABLE if BT_BLUEDROID_ENABLED + select BLE_HOST_COMPRESSED_LOG_ENABLE if BT_BLUEDROID_ENABLED || BT_NIMBLE_ENABLED select ESP_TIMER_IN_IRAM default n help Enable BT Log Async Output if BLE_LOG_ENABLED - config BLE_LOG_LBM_AUTO_FLUSH - bool "Enable automatic BLE Log LBM buffer flush" - default n + config BLE_LOG_POOL_TRANS_CNT + int "Number of transport buffers in the unified pool" + range 2 32 + default 8 help - Periodically flush partially-filled BLE Log LBM transport buffers - that remain pending, reducing latency for low-volume or - intermittent logging. + Total number of transports shared by ordinary, Link Layer, ISR, + and critical-section log writers. Availability is represented by + 32-bit bitmaps, so the maximum is 32. - config BLE_LOG_LBM_TRANS_BUF_SIZE - int "Total buffer memory per common LBM (bytes)" - default 2048 - help - Total buffer memory allocated for each common pool log buffer - manager (LBM). This memory is divided equally among internal - transport buffers. Must be a multiple of BLE_LOG_TRANS_BUF_CNT - (currently 4). - - The common pool contains: - - BLE_LOG_LBM_ATOMIC_LOCK_TASK_CNT atomic LBMs (task context) - - BLE_LOG_LBM_ATOMIC_LOCK_ISR_CNT atomic LBMs (ISR context) - - 2 spinlock-protected LBMs (one for task, one for ISR fallback) - - Total common pool memory: - (ATOMIC_TASK_CNT + ATOMIC_ISR_CNT + 2) * BLE_LOG_LBM_TRANS_BUF_SIZE - - config BLE_LOG_LBM_ATOMIC_LOCK_TASK_CNT - int "Count of log buffer managers with atomic lock protection for task context" - default 2 - help - BLE Log module will search for an LBM with atomic lock protection first; if - all LBMs with atomic lock protection are unavailable, BLE Log module will - try to use the LBM with spin lock protection. So the more LBMs with atomic - lock protection are created, the better the logging performance will be. - - config BLE_LOG_LBM_ATOMIC_LOCK_ISR_CNT - int "Count of log buffer managers with atomic lock protection for ISR context" + config BLE_LOG_POOL_NON_YIELD_RESERVE_CNT + int "Transport buffers reserved for non-yieldable contexts" + range 1 31 default 1 help - BLE Log module will search for an LBM with atomic lock protection first; if - all LBMs with atomic lock protection are unavailable, BLE Log module will - try to use the LBM with spin lock protection. So the more LBMs with atomic - lock protection are created, the more ISRs can nest. + Buffers that ordinary task writers cannot consume. ISR and + critical-section writers try the shared region first, then this + reserve. The value must remain below BLE_LOG_POOL_TRANS_CNT. + + config BLE_LOG_POOL_TRANS_SIZE + int "Per-transport buffer size (bytes)" + range 640 10240 + default 640 + help + Size of every unified-pool transport. One complete frame must fit + in one transport. SPI builds require a multiple of four so they + can append peripheral-only zero padding to each logical frame. config BLE_LOG_IS_ESP_CONTROLLER bool "Current BLE Controller is ESP BLE Controller" @@ -77,44 +61,9 @@ if BLE_LOG_ENABLED help Enable BLE Log for Link Layer - if BLE_LOG_LL_ENABLED - config BLE_LOG_LBM_LL_TRANS_BUF_SIZE - int "Total buffer memory per Link Layer LBM (bytes)" - default 2048 - help - Total buffer memory allocated for each Link Layer dedicated - log buffer manager (LBM). This memory is divided equally among - internal transport buffers. Must be a multiple of - BLE_LOG_TRANS_BUF_CNT (currently 4). - - There are 2 Link Layer LBMs without lock protection (each is - accessed from a single context only): - - LL task LBM (Link Layer task context logs) - - LL HCI LBM (Link Layer HCI context logs) - - Total LL pool memory: 2 * BLE_LOG_LBM_LL_TRANS_BUF_SIZE - - config BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT_ENABLED - bool "Enable LL HCI Log Payload Length Limit" - default n - help - Enable length limit for LL HCI Log payload (addr_append). - When enabled, if len_append exceeds the configured limit, - it will be truncated to the maximum length. - - config BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT - int "LL HCI Log Payload Length Limit (bytes)" - depends on BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT_ENABLED - default 32 - help - Maximum length for LL HCI Log payload (len_append). - When the feature is enabled and len_append exceeds this value, - it will be truncated. - endif - config BLE_LOG_HOST_LOG bool "Enable BLE Log for Host" - depends on BT_BLUEDROID_ENABLED + depends on BT_BLUEDROID_ENABLED || BT_NIMBLE_ENABLED default y help Enable BLE Log for the Host stack (Bluedroid or NimBLE). @@ -124,13 +73,24 @@ if BLE_LOG_ENABLED the standard UART logging path. Disable this option to keep BLE Log enabled for the controller/link layer only. - config BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED - bool "Enable BLE Host side HCI Logging" - default y if BLE_LOG_IS_ESP_LEGACY_CONTROLLER + config BLE_LOG_HCI_LOG_ENABLED + bool "Enable BLE HCI Logging" + default y help Enable HCI packet logging captured on the Host side - (Bluedroid / NimBLE HCI HAL). Useful for correlating - host events with controller-side HCI traces. + (Bluedroid / NimBLE HCI HAL). This is the single HCI + logging switch for both Host and Controller traffic: + the controller no longer maintains its own internal + LL HCI log, and controller-side HCI records are not + emitted separately. + + config BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED + bool "Enable BLE Host side HCI Logging" + default n + select BLE_LOG_HCI_LOG_ENABLED + help + Deprecated. Renamed to BLE_LOG_HCI_LOG_ENABLED; kept so sdkconfig + files pinning the old name keep Host side HCI logging enabled. config BLE_LOG_TS_ENABLED bool "Enable BLE Log Timestamp Synchronization (TS)" @@ -165,8 +125,11 @@ if BLE_LOG_ENABLED bool "Test only transport" help Test transport that models DMA ownership and optional - link-rate backpressure. Selected by BLE Log test apps - only; not shown in menuconfig. + link-rate backpressure. Intended for the BLE Log test apps + only: outside them nothing drains the transmitted buffers, + so selecting this transport in a production build stalls + log output. The BLE Log test apps select it through their + sdkconfig.defaults. config BLE_LOG_PRPH_SPI_MASTER_DMA bool "Utilize SPI master DMA driver as transport" @@ -240,6 +203,35 @@ if BLE_LOG_ENABLED int default 512 + config BLE_LOG_LBM_TRANS_BUF_SIZE + int + default 2048 + + config BLE_LOG_LBM_ATOMIC_LOCK_TASK_CNT + int + default 2 + + config BLE_LOG_LBM_ATOMIC_LOCK_ISR_CNT + int + default 1 + + config BLE_LOG_LBM_LL_TRANS_BUF_SIZE + int + default 2048 + + config BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT_ENABLED + bool + default n + + config BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT + int + depends on BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT_ENABLED + default 32 + + config BLE_LOG_LBM_AUTO_FLUSH + bool + default n + config BLE_LOG_ENH_STAT_ENABLED bool default y diff --git a/components/bt/common/ble_log/include/ble_log.h b/components/bt/common/ble_log/include/ble_log.h index 7668faf4d48..25669aa7dea 100644 --- a/components/bt/common/ble_log/include/ble_log.h +++ b/components/bt/common/ble_log/include/ble_log.h @@ -20,6 +20,8 @@ * The number of BLE Log source code will directly determine the number of statistic manager * memory requirements, keep it as less as possible; it's recommended to use subcode for more * log data structure decoding */ +/* CRITICAL: this enum is a public ABI and must not be reordered or renamed. + * Its values are also the on-wire source IDs of protocol v7 frames. */ typedef enum { /* Internal */ BLE_LOG_SRC_INTERNAL = 0, @@ -65,6 +67,8 @@ bool ble_log_enable(bool enable); /* Blocking; call only from a caller-owned task, not an ISR or system callback. */ void ble_log_flush(void); bool ble_log_write_hex(ble_log_src_t src_code, const uint8_t *addr, size_t len); +uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, uint32_t *handle); +void ble_log_commit(uint32_t handle, size_t actual_len); void ble_log_dump_to_console(void); #if CONFIG_BLE_LOG_LL_ENABLED void ble_log_write_hex_ll(uint32_t len, const uint8_t *addr, diff --git a/components/bt/common/ble_log/src/ble_log.c b/components/bt/common/ble_log/src/ble_log.c index 6a4e1936515..d434effc624 100644 --- a/components/bt/common/ble_log/src/ble_log.c +++ b/components/bt/common/ble_log/src/ble_log.c @@ -10,7 +10,7 @@ /* INCLUDE */ #include "ble_log.h" #include "ble_log_rt.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "ble_log_prph.h" #include "ble_log_util.h" #include "esp_log.h" @@ -45,24 +45,24 @@ bool ble_log_init(void) } #endif /* CONFIG_BLE_LOG_TS_ENABLED */ - /* Initialize BLE Log Runtime */ - if (!ble_log_rt_init()) { - goto exit; - } - - /* Initialize BLE Log LBM */ + /* Allocate pool and dedicated Internal transport before runtime starts. */ if (!ble_log_lbm_init()) { goto exit; } - /* Initialize BLE Log peripheral interface */ if (!ble_log_prph_init(BLE_LOG_TRANS_TOTAL_CNT)) { goto exit; } - /* Initialization done */ + if (!ble_log_rt_init()) { + goto exit; + } + ble_log_inited = true; - ble_log_enable(true); + if (!ble_log_enable(true) || + !ble_log_internal_snapshot(BLE_LOG_SNAPSHOT_REASON_INIT, NULL, true)) { + goto exit; + } esp_err_t ret = esp_register_shutdown_handler(ble_log_shutdown_handler); if (ret == ESP_OK) { shutdown_handler_registered = true; @@ -70,12 +70,6 @@ bool ble_log_init(void) ESP_LOGW(TAG, "Register shutdown handler failed, ret = 0x%x", ret); } - /* Write initialization done log */ - ble_log_info_t ble_log_info = { - .int_src_code = BLE_LOG_INT_SRC_INIT_DONE, - .version = BLE_LOG_VERSION, - }; - ble_log_write_hex(BLE_LOG_SRC_INTERNAL, (const uint8_t *)&ble_log_info, sizeof(ble_log_info_t)); return true; exit: @@ -94,9 +88,9 @@ void ble_log_deinit(void) } } ble_log_inited = false; - ble_log_lbm_close(); + ble_log_lbm_begin_deinit(); - /* CRITICAL — Deinit ordering rationale: + /* CRITICAL - Deinit ordering rationale: * * 1. The LBM writer gate is closed before submodule teardown. Writers * already inside the gate keep a reference until they finish; later diff --git a/components/bt/common/ble_log/src/ble_log_lbm_v2.c b/components/bt/common/ble_log/src/ble_log_lbm_v2.c new file mode 100644 index 00000000000..9306da8ea8a --- /dev/null +++ b/components/bt/common/ble_log/src/ble_log_lbm_v2.c @@ -0,0 +1,1209 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +/* ------------------------------- */ +/* BLE Log - Log Buffer Management */ +/* ------------------------------- */ + +/* INCLUDE */ +#include "ble_log.h" +#include "ble_log_lbm_v2.h" +#include "ble_log_rt.h" + +#include "esp_timer.h" + +#if CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER +#if CONFIG_BT_DUAL_MODE_ARCH +#include "ble_mbuf.h" +#define BLE_MBUF_COPY(buf, off, len, dst) ble_mbuf_copydata((struct ble_mbuf *)(buf), off, len, dst) +#else +#include "os/os_mbuf.h" +#define BLE_MBUF_COPY(buf, off, len, dst) os_mbuf_copydata((struct os_mbuf *)(buf), off, len, dst) +#endif // CONFIG_BT_DUAL_MODE_ARCH +#endif /* CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER */ + +/* MACRO */ +#define BLE_LOG_POOL_MASK(count) (0xFFFFFFFFu >> (32 - (count))) +#define BLE_LOG_POOL_ALL_MASK BLE_LOG_POOL_MASK(BLE_LOG_POOL_TRANS_CNT) +#define BLE_LOG_POOL_SHARED_MASK BLE_LOG_POOL_MASK(BLE_LOG_POOL_SHARED_CNT) +#define BLE_LOG_POOL_RESERVE_MASK (BLE_LOG_POOL_ALL_MASK & ~BLE_LOG_POOL_SHARED_MASK) +#define BLE_LOG_POOL_SEM_MAX_COUNT (~(UBaseType_t)0) +#define BLE_LOG_WAIT_TIMEOUT_TICKS pdMS_TO_TICKS(1000) + +/* Single-instruction clock read; a function would add an IRAM call site. */ +#define ble_log_timestamp_now() ((uint32_t)esp_timer_get_time()) + +/* ------------------------------- */ +/* Global Pool Context */ +/* ------------------------------- */ +typedef struct { + ble_log_prph_trans_t *trans[BLE_LOG_POOL_TRANS_CNT]; + + /* Hint bitmaps: a set bit means "this buffer is likely FREE/OPEN". The + * real ownership is the per-buffer atomic_lock; bitmaps only accelerate + * candidate lookup and may be transiently stale. */ + volatile uint32_t free_bitmap; + volatile uint32_t open_bitmap; + + /* Round-robin scan cursors. */ + volatile uint32_t open_cursor; + volatile uint32_t free_cursor; + + /* Peak number of non-free pool buffers. */ + volatile uint32_t inflight_peak; + + /* Number of task writers currently blocked waiting for a buffer. */ + volatile uint32_t waiting_task_count; + + /* Event-only counting semaphore used to wake blocked task writers. + * Not a lock: it carries no ownership and never protects shared data. */ + SemaphoreHandle_t sem; +} ble_log_pool_t; + +/* ------------------------------- */ +/* Claim/Commit Bookkeeping */ +/* ------------------------------- */ +/* State that must survive between ble_log_claim() and ble_log_commit(). It + * lives with the pool, not in ble_log_prph_trans_t: the peripheral transport + * only needs buf/size/pos and the lifecycle state for DMA handoff. */ +typedef struct { + uint8_t src_code; /* source of the pending claim */ + uint8_t non_yield; /* claimed from a non-yieldable context */ + uint16_t max_len; /* maximum committed payload length */ + uint32_t frame_sn; /* Global SN consumed at claim entry */ + uint32_t generation; /* stale-handle detection, wraps 24-bit */ +} ble_log_pool_claim_t; + +/* VARIABLE */ +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR volatile uint32_t lbm_ref_count = 0; +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t lbm_inited = 0; +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t lbm_enabled = 0; +BLE_LOG_STATIC volatile bool flush_in_progress = false; +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_pool_t g_pool; +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_stat_mgr_t stat_mgr_ctx[BLE_LOG_SRC_MAX]; +/* Global SN (all non-INTERNAL sources) and the separate Internal Snapshot + * sequence; see ble_log_lbm_v2.h. */ +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t g_frame_sn; +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t g_snapshot_sn; +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_prph_trans_t *internal_trans; +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_pool_claim_t pool_claim_ctx[BLE_LOG_POOL_TRANS_CNT]; +BLE_LOG_STATIC ble_log_internal_snapshot_t internal_snapshot; + +#if CONFIG_BLE_LOG_PRPH_TEST +extern void ble_log_test_claim_pre_publish_hook(void) __attribute__((weak)); +extern void ble_log_test_enable_before_lifecycle_lock_hook(void) __attribute__((weak)); +extern void ble_log_test_disable_before_wake_hook(void) __attribute__((weak)); +#endif + +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +void ble_log_lbm_disable(void) +{ + BLE_LOG_ATOMIC_STORE_SEQ_CST(lbm_enabled, false); + /* Pairs with the waiter-side fence so either wake_all observes the waiter + * or the waiter observes the disabled flag before blocking. */ + __atomic_thread_fence(__ATOMIC_SEQ_CST); +} + +/* PRIVATE FUNCTION DECLARATION */ +BLE_LOG_STATIC ble_log_prph_trans_t *ble_log_pool_acquire(size_t log_len, + bool use_reserve, + bool wait); +BLE_LOG_STATIC void ble_log_pool_seal_and_send(ble_log_prph_trans_t *trans); +BLE_LOG_STATIC void ble_log_pool_write_frame(ble_log_prph_trans_t *trans, + uint32_t frame_sn, + uint8_t source_meta, + const uint8_t *prefix, + uint16_t prefix_len, + const uint8_t *addr, uint16_t len, + const uint8_t *addr_append, + uint16_t len_append, bool omdata); +BLE_LOG_STATIC void ble_log_stat_mgr_mark_lost(ble_log_src_t src_code); +BLE_LOG_STATIC void ble_log_snapshot_stats(ble_log_source_stat_t *snapshots); +BLE_LOG_STATIC bool ble_log_pool_flush_all_trans(void); +BLE_LOG_STATIC void ble_log_pool_wake_all(void); +#if BLE_LOG_UART_REDIR_ENABLED +BLE_LOG_STATIC void ble_log_redir_seal(ble_log_prph_trans_t *trans, ble_log_src_t src_code); +#endif /* BLE_LOG_UART_REDIR_ENABLED */ + +/* ------------------------- */ +/* BITMAP HELPERS */ +/* ------------------------- */ +/* Non-inline: each helper is called from several IRAM sites, and inlining + * would duplicate its body into every call site (IRAM is the scarce + * resource, not the call cost). */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void +ble_log_pool_bitmap_set(volatile uint32_t *bitmap, uint8_t id) +{ + __atomic_fetch_or(bitmap, BIT(id), __ATOMIC_RELEASE); +} + +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +uint32_t ble_log_pool_bitmap_clear(volatile uint32_t *bitmap, uint8_t id) +{ + /* Clearing a hint publishes no data; ownership is already held by lock. */ + return __atomic_fetch_and(bitmap, ~BIT(id), __ATOMIC_RELAXED); +} + +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void +ble_log_pool_update_peak(uint32_t free_bitmap) +{ + uint32_t used = BLE_LOG_POOL_TRANS_CNT - + __builtin_popcount(free_bitmap & BLE_LOG_POOL_ALL_MASK); + uint32_t peak = BLE_LOG_ATOMIC_LOAD_RELAXED(g_pool.inflight_peak); + while (used > peak && + !__atomic_compare_exchange_n(&g_pool.inflight_peak, &peak, used, true, + __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { + } +} + +/* A newly available shared transport satisfies one waiter. The SEQ_CST fence + * pairs with the waiter-side store/fence/load handshake: either the waiter + * observes the bitmap publication or this side observes the waiter count. */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void ble_log_pool_notify_waiter(uint8_t id) +{ + /* Reserve transports never satisfy task waiters. */ + if (!(BLE_LOG_POOL_SHARED_MASK & BIT(id))) { + return; + } + + __atomic_thread_fence(__ATOMIC_SEQ_CST); + + SemaphoreHandle_t sem = g_pool.sem; + /* The fence above provides ordering; the count is only a wake predicate. */ + if (!sem || + BLE_LOG_ATOMIC_LOAD_RELAXED(g_pool.waiting_task_count) == 0) { + return; + } + + if (BLE_LOG_IN_ISR()) { + BaseType_t woken = pdFALSE; + xSemaphoreGiveFromISR(sem, &woken); + if (woken) { + portYIELD_FROM_ISR(woken); + } + } else { + xSemaphoreGive(sem); + } +} + +/* Publish OPEN and release ownership as one operation so notification can + * never be moved before the lock release. */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void +ble_log_pool_publish_open_and_unlock(ble_log_prph_trans_t *trans) +{ + BLE_LOG_ATOMIC_STORE_RELAXED(trans->state, BLE_LOG_TRANS_STATE_OPEN); + /* The following lock release publishes both state and frame data. */ + __atomic_fetch_or(&g_pool.open_bitmap, BIT(trans->id), __ATOMIC_RELAXED); + ble_log_cas_release(&trans->atomic_lock); + ble_log_pool_notify_waiter(trans->id); +} + +/* Task context only: release all potentially-blocked task writers. */ +BLE_LOG_STATIC void ble_log_pool_wake_all(void) +{ + if (!g_pool.sem) { + return; + } + uint32_t waiters = BLE_LOG_ATOMIC_LOAD_RELAXED(g_pool.waiting_task_count); + for (uint32_t i = 0; i < waiters; i++) { + (void)xSemaphoreGive(g_pool.sem); + } +} + +/* -------------------------------------- */ +/* UNIFIED TRANSPORT RECYCLE */ +/* -------------------------------------- */ +BLE_LOG_IRAM_ATTR void ble_log_lbm_recycle_trans(ble_log_prph_trans_t *trans) +{ + trans->pos = 0; + + if (trans->owner_kind == BLE_LOG_TRANS_OWNER_INTERNAL) { + BLE_LOG_ATOMIC_STORE_RELEASE(trans->state, BLE_LOG_TRANS_STATE_FREE); + return; + } + + if (trans->owner_kind == BLE_LOG_TRANS_OWNER_REDIR) { +#if BLE_LOG_UART_REDIR_ENABLED + ble_log_redir_t *redir = ble_log_prph_get_redir_lbm(); + if (redir) { + __atomic_sub_fetch(&redir->inflight, 1, __ATOMIC_RELAXED); + } +#endif + BLE_LOG_ATOMIC_STORE_RELEASE(trans->state, BLE_LOG_TRANS_STATE_FREE); + return; + } + + /* Publish FREE state before advertising the pool bitmap hint. */ + BLE_LOG_ATOMIC_STORE_RELEASE(trans->state, BLE_LOG_TRANS_STATE_FREE); + ble_log_pool_bitmap_set(&g_pool.free_bitmap, trans->id); + ble_log_pool_notify_waiter(trans->id); +} + +/* -------------------------------------- */ +/* SEAL AND SUBMIT (holds lock) */ +/* -------------------------------------- */ +BLE_LOG_IRAM_ATTR void ble_log_pool_seal_and_send(ble_log_prph_trans_t *trans) +{ + BLE_LOG_ATOMIC_STORE_RELAXED(trans->state, BLE_LOG_TRANS_STATE_SENDING); + /* A claimed buffer is already absent from free_bitmap. Remove any OPEN + * hint before releasing the SENDING buffer to the runtime task. */ + ble_log_pool_bitmap_clear(&g_pool.open_bitmap, trans->id); + + /* Release the lock BEFORE submitting. The buffer is now in no bitmap and + * is SENDING, so no other writer can find it. */ + ble_log_cas_release(&trans->atomic_lock); + ble_log_rt_submit_trans(trans); +} + +/* -------------------------------------- */ +/* AVAILABLE BUFFER CLAIM */ +/* -------------------------------------- */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +ble_log_prph_trans_t *ble_log_pool_try_claim_from(volatile uint32_t *bitmap, + volatile uint32_t *cursor, + uint32_t domain_mask, + ble_log_trans_state_t expected_state, + uint32_t frame_len) +{ + uint32_t candidates = BLE_LOG_ATOMIC_LOAD_ACQUIRE(*bitmap) & domain_mask; + if (!candidates) { + return NULL; + } + + uint32_t start = BLE_LOG_ATOMIC_LOAD_RELAXED(*cursor); + + for (uint32_t offset = 0; offset < BLE_LOG_POOL_TRANS_CNT; offset++) { + uint8_t id = (uint8_t)((start + offset) % BLE_LOG_POOL_TRANS_CNT); + if (!(candidates & BIT(id))) { + continue; + } + + ble_log_prph_trans_t *trans = g_pool.trans[id]; + if (!ble_log_cas_acquire(&trans->atomic_lock)) { + continue; + } + if (BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state) != expected_state) { + /* The bitmap is only a hint; another owner may have changed state. */ + ble_log_cas_release(&trans->atomic_lock); + continue; + } + + if (expected_state == BLE_LOG_TRANS_STATE_OPEN && + BLE_LOG_TRANS_FREE_SPACE(trans) < frame_len) { + ble_log_pool_seal_and_send(trans); /* releases the lock */ + continue; + } + + uint32_t previous = ble_log_pool_bitmap_clear(bitmap, id); + if (bitmap == &g_pool.free_bitmap) { + ble_log_pool_update_peak(previous & ~BIT(id)); + } + /* Keep packing the same OPEN transport instead of scanning the rest + * of the pool before wrapping back to it on the next write. */ + uint32_t next = expected_state == BLE_LOG_TRANS_STATE_OPEN + ? id + : (id + 1u) % BLE_LOG_POOL_TRANS_CNT; + BLE_LOG_ATOMIC_STORE_RELAXED(*cursor, next); + return trans; /* returned locked */ + } + + return NULL; +} + +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +ble_log_prph_trans_t *ble_log_pool_try_claim_available(uint32_t frame_len, bool use_reserve) +{ + uint32_t open_domain = use_reserve ? BLE_LOG_POOL_ALL_MASK : BLE_LOG_POOL_SHARED_MASK; + + /* The cursor normally points at the OPEN transport used by the previous + * write. Claim it directly; fall back to bitmap scanning on contention, + * stale state, or insufficient space. */ + uint8_t open_id = (uint8_t)BLE_LOG_ATOMIC_LOAD_RELAXED(g_pool.open_cursor); + if (open_domain & BIT(open_id)) { + ble_log_prph_trans_t *open_trans = g_pool.trans[open_id]; + if (ble_log_cas_acquire(&open_trans->atomic_lock)) { + if (BLE_LOG_ATOMIC_LOAD_RELAXED(open_trans->state) == BLE_LOG_TRANS_STATE_OPEN) { + if (BLE_LOG_TRANS_FREE_SPACE(open_trans) >= frame_len) { + ble_log_pool_bitmap_clear(&g_pool.open_bitmap, open_id); + return open_trans; + } + ble_log_pool_seal_and_send(open_trans); /* releases the lock */ + } else { + ble_log_cas_release(&open_trans->atomic_lock); + } + } + } + + /* Prefer partially-filled buffers to preserve packing efficiency. */ + ble_log_prph_trans_t *trans = + ble_log_pool_try_claim_from(&g_pool.open_bitmap, &g_pool.open_cursor, + open_domain, BLE_LOG_TRANS_STATE_OPEN, + frame_len); + if (trans) { + return trans; + } + + trans = ble_log_pool_try_claim_from(&g_pool.free_bitmap, &g_pool.free_cursor, + BLE_LOG_POOL_SHARED_MASK, BLE_LOG_TRANS_STATE_FREE, + 0); + if (!trans && use_reserve) { + trans = ble_log_pool_try_claim_from(&g_pool.free_bitmap, &g_pool.free_cursor, + BLE_LOG_POOL_RESERVE_MASK, BLE_LOG_TRANS_STATE_FREE, + 0); + } + return trans; +} + +/* -------------------------------------- */ +/* ACQUIRE (with task backpressure) */ +/* -------------------------------------- */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +ble_log_prph_trans_t *ble_log_pool_acquire(size_t log_len, + bool use_reserve, + bool wait) +{ + uint32_t frame_len = (uint32_t)log_len + BLE_LOG_FRAME_OVERHEAD; + if (frame_len > BLE_LOG_POOL_TRANS_SIZE) { + return NULL; + } + + for (;;) { + ble_log_prph_trans_t *trans = + ble_log_pool_try_claim_available(frame_len, use_reserve); + if (trans || !wait || !BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled)) { + return trans; + } + + __atomic_add_fetch(&g_pool.waiting_task_count, 1, __ATOMIC_ACQ_REL); + __atomic_thread_fence(__ATOMIC_SEQ_CST); + + trans = ble_log_pool_try_claim_available(frame_len, use_reserve); + if (trans) { + __atomic_sub_fetch(&g_pool.waiting_task_count, 1, __ATOMIC_ACQ_REL); + return trans; + } + if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled)) { + __atomic_sub_fetch(&g_pool.waiting_task_count, 1, __ATOMIC_ACQ_REL); + return NULL; + } + + /* A parked LL task keeps its waiter registration but releases the + * normal writer reference so flush/deinit can close the gate. */ + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + xSemaphoreTake(g_pool.sem, portMAX_DELAY); + BLE_LOG_REF_COUNT_ACQUIRE_SEQ_CST(&lbm_ref_count); + __atomic_sub_fetch(&g_pool.waiting_task_count, 1, __ATOMIC_ACQ_REL); + + if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled)) { + return NULL; + } + } +} + +/* -------------------------------------- */ +/* WRITE ONE FRAME (holds lock) */ +/* -------------------------------------- */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +void ble_log_pool_write_frame(ble_log_prph_trans_t *trans, uint32_t frame_sn, + uint8_t source_meta, + const uint8_t *prefix, uint16_t prefix_len, + const uint8_t *addr, uint16_t len, + const uint8_t *addr_append, uint16_t len_append, bool omdata) +{ + uint8_t *buf = trans->buf + trans->pos; + uint16_t payload_len = prefix_len + len + len_append; + ble_log_src_t src_code = (ble_log_src_t)BLE_LOG_SRC_ID(source_meta); + ble_log_stat_mgr_t *stat_mgr = &stat_mgr_ctx[src_code]; + ble_log_frame_head_t frame_head = { + .length = payload_len, + .frame_meta = BLE_LOG_MAKE_FRAME_META(source_meta, frame_sn), + }; + + /* Memory operation */ + BLE_LOG_MEMCPY(buf, &frame_head, BLE_LOG_FRAME_HEAD_LEN); + uint8_t *payload = buf + BLE_LOG_FRAME_HEAD_LEN; + if (prefix_len) { + BLE_LOG_MEMCPY(payload, prefix, prefix_len); + payload += prefix_len; + } + if (len) { + BLE_LOG_MEMCPY(payload, addr, len); + payload += len; + } + if (len_append) { +#if CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER + if (omdata) { + BLE_MBUF_COPY(addr_append, 0, len_append, payload); + } + else +#endif /* CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER */ + { + BLE_LOG_MEMCPY(payload, addr_append, len_append); + } + } + /* Data integrity check */ + uint32_t checksum = ble_log_fast_checksum((const uint8_t *)buf, BLE_LOG_FRAME_HEAD_LEN + payload_len); + BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN + payload_len, &checksum, BLE_LOG_FRAME_TAIL_LEN); + + /* Update transport and publish the completed core-stat record. */ + trans->pos += payload_len + BLE_LOG_FRAME_OVERHEAD; + BLE_LOG_ATOMIC_ADD_RELAXED(stat_mgr->counters.written_frame_cnt, 1); + + /* Completion: seal if nearly full, otherwise publish as OPEN. */ + if (BLE_LOG_TRANS_FREE_SPACE(trans) <= BLE_LOG_FRAME_OVERHEAD) { + ble_log_pool_seal_and_send(trans); /* releases the lock */ + } else { + ble_log_pool_publish_open_and_unlock(trans); + } +} + +/* ---------------------------------------------- */ +/* CLAIM / COMMIT INTERFACE (holds lock) */ +/* ---------------------------------------------- */ +uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, uint32_t *handle) +{ + if (!handle || src_code != BLE_LOG_SRC_ENCODE || max_len == 0 || + max_len > UINT16_MAX || BLE_LOG_IN_ISR()) { + return NULL; + } + *handle = UINT32_MAX; + + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_enabled)) { + return NULL; + } + + /* Every claim attempt consumes one Global SN at entry; an abandoned or + * failed claim therefore leaves a gap. */ + uint32_t frame_sn = BLE_LOG_GET_GLOBAL_SN(); + + /* The timestamp is the log occurrence time, before pool contention. */ + uint32_t timestamp = ble_log_timestamp_now(); + bool non_yield = !xPortCanYield() || + xTaskGetSchedulerState() != taskSCHEDULER_RUNNING; + size_t payload_capacity = sizeof(timestamp) + max_len; + ble_log_prph_trans_t *trans = + ble_log_pool_acquire(payload_capacity, non_yield, false); + if (!trans) { + ble_log_stat_mgr_mark_lost(src_code); + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return NULL; + } + +#if CONFIG_BLE_LOG_PRPH_TEST + if (ble_log_test_claim_pre_publish_hook) { + ble_log_test_claim_pre_publish_hook(); + } +#endif + ble_log_pool_claim_t *claim = &pool_claim_ctx[trans->id]; + claim->src_code = (uint8_t)src_code; + claim->non_yield = non_yield; + claim->max_len = (uint16_t)max_len; + claim->frame_sn = frame_sn; + claim->generation = (claim->generation + 1) & 0x00ffffffU; + + uint8_t *payload = trans->buf + trans->pos + BLE_LOG_FRAME_HEAD_LEN; + BLE_LOG_MEMCPY(payload, ×tamp, sizeof(timestamp)); + + /* Publish only after all claim metadata and payload prefix are complete. + * An acquire load in commit then rejects stale generations without ever + * observing a partially initialized claim. */ + BLE_LOG_ATOMIC_STORE_RELEASE(trans->state, BLE_LOG_TRANS_STATE_CLAIMED); + *handle = (claim->generation << 8) | trans->id; + return payload + sizeof(timestamp); +} + +void ble_log_commit(uint32_t handle, size_t actual_len) +{ + uint8_t id = handle & 0xffU; + uint32_t generation = handle >> 8; + if (id >= BLE_LOG_POOL_TRANS_CNT) { + return; + } + ble_log_prph_trans_t *trans = g_pool.trans[id]; + ble_log_pool_claim_t *claim = &pool_claim_ctx[id]; + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(trans->state) != + BLE_LOG_TRANS_STATE_CLAIMED || + claim->generation != generation) { + return; + } + + ble_log_src_t src_code = (ble_log_src_t)claim->src_code; + if (actual_len == 0 || actual_len > claim->max_len) { + ble_log_stat_mgr_mark_lost(src_code); + if (trans->pos == 0) { + BLE_LOG_ATOMIC_STORE_RELEASE(trans->state, BLE_LOG_TRANS_STATE_FREE); + ble_log_pool_bitmap_set(&g_pool.free_bitmap, trans->id); + ble_log_cas_release(&trans->atomic_lock); + ble_log_pool_notify_waiter(trans->id); + } else { + ble_log_pool_publish_open_and_unlock(trans); + } + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return; + } + + uint16_t payload_len = (uint16_t)(sizeof(uint32_t) + actual_len); + uint8_t *buf = trans->buf + trans->pos; + ble_log_stat_mgr_t *stat_mgr = &stat_mgr_ctx[src_code]; + uint8_t source_meta = BLE_LOG_MAKE_SOURCE_META(src_code, + claim->non_yield); + ble_log_frame_head_t frame_head = { + .length = payload_len, + .frame_meta = BLE_LOG_MAKE_FRAME_META(source_meta, claim->frame_sn), + }; + BLE_LOG_MEMCPY(buf, &frame_head, BLE_LOG_FRAME_HEAD_LEN); + + uint32_t checksum = ble_log_fast_checksum(buf, + BLE_LOG_FRAME_HEAD_LEN + payload_len); + BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN + payload_len, + &checksum, BLE_LOG_FRAME_TAIL_LEN); + + trans->pos += payload_len + BLE_LOG_FRAME_OVERHEAD; + BLE_LOG_ATOMIC_ADD_RELAXED(stat_mgr->counters.written_frame_cnt, 1); + + if (BLE_LOG_TRANS_FREE_SPACE(trans) <= BLE_LOG_FRAME_OVERHEAD) { + ble_log_pool_seal_and_send(trans); + } else { + ble_log_pool_publish_open_and_unlock(trans); + } + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); +} + +#if BLE_LOG_UART_REDIR_ENABLED +/* ------------------------------------------------- */ +/* STREAM WRITE INTERFACE (UART redirection) */ +/* */ +/* Stream mode appends raw data into a transport */ +/* buffer with deferred frame encapsulation. */ +/* Redirection transports are single-writer under */ +/* redir->mutex; their only concurrent mutation is */ +/* the UART tx-done recycle (state SENDING -> FREE), */ +/* so state access uses atomics. */ +/* ------------------------------------------------- */ +BLE_LOG_STATIC +ble_log_prph_trans_t *ble_log_redir_get_trans(ble_log_redir_t *redir, + ble_log_src_t src_code) +{ + for (int i = 0; i < BLE_LOG_TRANS_BUF_CNT; i++) { + ble_log_prph_trans_t *trans = redir->trans[redir->trans_idx]; + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(trans->state) != BLE_LOG_TRANS_STATE_SENDING) { + if (BLE_LOG_TRANS_FREE_SPACE(trans) >= BLE_LOG_FRAME_OVERHEAD) { + return trans; + } + if (trans->pos > BLE_LOG_FRAME_HEAD_LEN) { + ble_log_redir_seal(trans, src_code); + } + } + redir->trans_idx = (redir->trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); + } + return NULL; +} + +BLE_LOG_STATIC +void ble_log_redir_seal(ble_log_prph_trans_t *trans, ble_log_src_t src_code) +{ + if (trans->pos <= BLE_LOG_FRAME_HEAD_LEN) { + return; + } + + uint16_t payload_len = trans->pos - BLE_LOG_FRAME_HEAD_LEN; + /* REDIR frames take the Global SN at seal time; the stream has no + * core-stat slot. */ + uint32_t frame_sn = BLE_LOG_GET_GLOBAL_SN(); + ble_log_frame_head_t frame_head = { + .length = payload_len, + .frame_meta = BLE_LOG_MAKE_FRAME_META(src_code, frame_sn), + }; + BLE_LOG_MEMCPY(trans->buf, &frame_head, BLE_LOG_FRAME_HEAD_LEN); + + uint32_t checksum = ble_log_fast_checksum(trans->buf, trans->pos); + BLE_LOG_MEMCPY(trans->buf + trans->pos, &checksum, BLE_LOG_FRAME_TAIL_LEN); + trans->pos += BLE_LOG_FRAME_TAIL_LEN; + + ble_log_redir_t *redir = ble_log_prph_get_redir_lbm(); + BLE_LOG_ASSERT(redir); + uint32_t infl = __atomic_add_fetch(&redir->inflight, 1, __ATOMIC_RELAXED); + uint32_t peak = BLE_LOG_ATOMIC_LOAD_RELAXED(redir->inflight_peak); + while (infl > peak && + !__atomic_compare_exchange_n(&redir->inflight_peak, &peak, infl, true, + __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { + } + + BLE_LOG_ATOMIC_STORE_RELAXED(trans->state, BLE_LOG_TRANS_STATE_SENDING); + ble_log_rt_submit_trans(trans); +} + +void ble_log_lbm_stream_write(ble_log_redir_t *redir, ble_log_src_t src_code, + uint32_t timestamp, const uint8_t *data, size_t len) +{ + while (len > 0) { + ble_log_prph_trans_t *trans = ble_log_redir_get_trans(redir, src_code); + if (!trans) { + /* Burn one Global SN so the dropped console batch leaves a + * sequence gap. */ + (void)BLE_LOG_GET_GLOBAL_SN(); + return; + } + + if (trans->pos == 0) { + trans->pos = BLE_LOG_FRAME_HEAD_LEN; + BLE_LOG_MEMCPY(trans->buf + trans->pos, ×tamp, + sizeof(timestamp)); + trans->pos += sizeof(timestamp); + } + + uint16_t available = BLE_LOG_TRANS_FREE_SPACE(trans); + if (available <= BLE_LOG_FRAME_TAIL_LEN) { + ble_log_redir_seal(trans, src_code); + continue; + } + available -= BLE_LOG_FRAME_TAIL_LEN; + + size_t to_write = (len < available) ? len : available; + BLE_LOG_MEMCPY(trans->buf + trans->pos, data, to_write); + trans->pos += to_write; + data += to_write; + len -= to_write; + + if (BLE_LOG_TRANS_FREE_SPACE(trans) <= BLE_LOG_FRAME_OVERHEAD) { + ble_log_redir_seal(trans, src_code); + } + } +} + +void ble_log_lbm_stream_flush(ble_log_redir_t *redir, ble_log_src_t src_code) +{ + int trans_idx = redir->trans_idx; + for (int i = 0; i < BLE_LOG_TRANS_BUF_CNT; i++) { + ble_log_prph_trans_t *trans = redir->trans[trans_idx]; + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(trans->state) != BLE_LOG_TRANS_STATE_SENDING && + trans->pos > BLE_LOG_FRAME_HEAD_LEN) { + ble_log_redir_seal(trans, src_code); + } + trans_idx = (trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); + } +} +#endif /* BLE_LOG_UART_REDIR_ENABLED */ + +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +void ble_log_stat_mgr_mark_lost(ble_log_src_t src_code) +{ + /* The Global SN was already consumed at API entry; only the per-source + * loss counter is attributed here. */ + ble_log_stat_mgr_t *stat_mgr = &stat_mgr_ctx[src_code]; + BLE_LOG_ATOMIC_ADD_RELAXED(stat_mgr->counters.lost_frame_cnt, 1); +} + +/* -------------------------- */ +/* INTERNAL INTERFACE */ +/* -------------------------- */ +bool ble_log_lbm_init(void) +{ + /* Avoid double init */ + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_inited)) { + return true; + } + + BLE_LOG_MEMSET(&g_pool, 0, sizeof(g_pool)); + + /* The semaphore carries wake events rather than buffer ownership, so its + * capacity must accommodate every possible blocked writer. Counting + * semaphores have a zero-sized queue item, so this does not allocate + * storage proportional to the maximum count. */ + g_pool.sem = xSemaphoreCreateCounting(BLE_LOG_POOL_SEM_MAX_COUNT, 0); + if (!g_pool.sem) { + goto exit; + } + + /* Allocate the transport buffers of the global pool. */ + for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { + if (!ble_log_prph_trans_init(&(g_pool.trans[id]), BLE_LOG_POOL_TRANS_SIZE)) { + goto exit; + } + g_pool.trans[id]->id = (uint8_t)id; + g_pool.trans[id]->owner_kind = BLE_LOG_TRANS_OWNER_POOL; + g_pool.trans[id]->state = BLE_LOG_TRANS_STATE_FREE; + g_pool.trans[id]->atomic_lock = 0; + } + + if (!ble_log_prph_trans_init(&internal_trans, BLE_LOG_INTERNAL_TRANS_SIZE)) { + goto exit; + } + internal_trans->id = BLE_LOG_TRANS_ID_NONE; + internal_trans->owner_kind = BLE_LOG_TRANS_OWNER_INTERNAL; + internal_trans->state = BLE_LOG_TRANS_STATE_FREE; + internal_trans->atomic_lock = 0; + + g_pool.free_bitmap = BLE_LOG_POOL_ALL_MASK; + g_pool.open_bitmap = 0; + BLE_LOG_MEMSET(stat_mgr_ctx, 0, sizeof(stat_mgr_ctx)); + g_frame_sn = 0; + g_snapshot_sn = 0; + BLE_LOG_MEMSET(&internal_snapshot, 0, sizeof(internal_snapshot)); + internal_snapshot.int_src_code = BLE_LOG_INT_SRC_SNAPSHOT; + internal_snapshot.pool.trans_cnt = BLE_LOG_POOL_TRANS_CNT; + internal_snapshot.pool.non_yield_reserve_cnt = BLE_LOG_POOL_NON_YIELD_RESERVE_CNT; + + lbm_ref_count = 0; + BLE_LOG_ATOMIC_STORE_RELEASE(lbm_enabled, false); + BLE_LOG_ATOMIC_STORE_RELEASE(lbm_inited, true); + return true; + +exit: + ble_log_lbm_deinit(); + return false; +} + +void ble_log_lbm_begin_deinit(void) +{ + /* Serialize both producer-gate closes with enable and flush restore. */ + BLE_LOG_ENTER_CRITICAL(); + ble_log_lbm_disable(); + BLE_LOG_ATOMIC_STORE_SEQ_CST(lbm_inited, false); + BLE_LOG_EXIT_CRITICAL(); + + /* Wake any blocked task writers and wait until BOTH the reference count + * and the waiting-task count drain to zero. Blocked writers hold no + * reference while parked, so waiting on ref_count alone could free the + * pool while a woken task still touches it (use-after-free). */ + TickType_t ticks_waited = 0; + while ((BLE_LOG_ATOMIC_LOAD_SEQ_CST(lbm_ref_count) > 0) || + (BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.waiting_task_count) > 0)) { + ble_log_pool_wake_all(); + vTaskDelay(1); + BLE_LOG_ASSERT(ticks_waited++ < BLE_LOG_WAIT_TIMEOUT_TICKS); + } +} + +__attribute__((noinline)) BLE_LOG_STATIC +void ble_log_snapshot_stats(ble_log_source_stat_t *snapshots) +{ + BLE_LOG_ENTER_CRITICAL(); + for (int i = 0; i < BLE_LOG_SRC_CORE_COUNT; i++) { + ble_log_stat_mgr_t *stat_mgr = + &stat_mgr_ctx[BLE_LOG_SRC_CORE_FIRST + i]; + snapshots[i].written_frame_cnt = + BLE_LOG_ATOMIC_LOAD_RELAXED(stat_mgr->counters.written_frame_cnt); + snapshots[i].lost_frame_cnt = + BLE_LOG_ATOMIC_LOAD_RELAXED(stat_mgr->counters.lost_frame_cnt); + } + BLE_LOG_EXIT_CRITICAL(); +} + +void ble_log_lbm_deinit(void) +{ + ble_log_lbm_begin_deinit(); + + ble_log_prph_trans_deinit(&internal_trans); + for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { + ble_log_prph_trans_deinit(&(g_pool.trans[id])); + } + + if (g_pool.sem) { + vSemaphoreDelete(g_pool.sem); + g_pool.sem = NULL; + } +} + +bool ble_log_lbm_is_enabled(void) +{ + return BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled); +} + +void ble_log_internal_set_version_info(const ble_log_version_info_t *version_info) +{ + if (version_info) { + BLE_LOG_MEMCPY(&internal_snapshot.version_info, version_info, + sizeof(*version_info)); + } +} + +bool ble_log_internal_snapshot(uint16_t reason_flags, + const ble_log_ts_info_t *ts_info, + bool required) +{ + const uint32_t *gate = required ? &lbm_inited : &lbm_enabled; + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, gate)) { + return false; + } + if (!internal_trans) { + goto failed; + } + + /* Capture the complete occurrence sample before any dedicated-buffer + * drain or wait. Without a TS sync sample, esp_ts comes from the frame + * timestamp and os_ts from the current tick. */ + uint32_t timestamp = ts_info ? ts_info->esp_ts : ble_log_timestamp_now(); + TickType_t start_tick = xTaskGetTickCount(); + for (;;) { + if (ble_log_cas_acquire(&internal_trans->atomic_lock)) { + if (BLE_LOG_ATOMIC_LOAD_RELAXED(internal_trans->state) == + BLE_LOG_TRANS_STATE_FREE) { + break; + } + ble_log_cas_release(&internal_trans->atomic_lock); + } + if (!required || + (xTaskGetTickCount() - start_tick) >= BLE_LOG_WAIT_TIMEOUT_TICKS) { + goto lost; + } + (void)ble_log_rt_drain(); + vTaskDelay(1); + } + + internal_snapshot.reason_flags = reason_flags; + internal_snapshot.ts.io_level = ts_info ? ts_info->io_level : 0; + internal_snapshot.ts.lc_ts = ts_info ? ts_info->lc_ts : 0; + internal_snapshot.ts.esp_ts = timestamp; + internal_snapshot.ts.os_ts = ts_info ? ts_info->os_ts + : pdTICKS_TO_MS(xTaskGetTickCount()); + uint32_t free_bitmap = BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.free_bitmap) & + BLE_LOG_POOL_ALL_MASK; + internal_snapshot.pool.inflight = + (uint8_t)(BLE_LOG_POOL_TRANS_CNT - __builtin_popcount(free_bitmap)); + internal_snapshot.pool.inflight_peak = + (uint8_t)BLE_LOG_ATOMIC_LOAD_RELAXED(g_pool.inflight_peak); + ble_log_snapshot_stats(internal_snapshot.stats); + + uint32_t frame_sn = BLE_LOG_GET_SNAPSHOT_SN(); + ble_log_frame_head_t frame_head = { + .length = sizeof(timestamp) + sizeof(internal_snapshot), + .frame_meta = BLE_LOG_MAKE_FRAME_META(BLE_LOG_SRC_INTERNAL, frame_sn), + }; + uint8_t *buf = internal_trans->buf; + BLE_LOG_MEMCPY(buf, &frame_head, sizeof(frame_head)); + BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN, ×tamp, sizeof(timestamp)); + BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN + sizeof(timestamp), + &internal_snapshot, sizeof(internal_snapshot)); + uint32_t checksum = ble_log_fast_checksum( + buf, BLE_LOG_FRAME_HEAD_LEN + frame_head.length); + BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN + frame_head.length, + &checksum, sizeof(checksum)); + + internal_trans->pos = BLE_LOG_INTERNAL_FRAME_LEN; + BLE_LOG_ATOMIC_STORE_RELAXED(internal_trans->state, + BLE_LOG_TRANS_STATE_SENDING); + ble_log_cas_release(&internal_trans->atomic_lock); + ble_log_rt_submit_trans(internal_trans); + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return true; + +lost: + /* A skipped snapshot burns one snapshot SN: the gap in the snapshot + * sequence is the loss signal. */ + (void)BLE_LOG_GET_SNAPSHOT_SN(); +failed: + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return false; +} + +BLE_LOG_STATIC bool ble_log_pool_flush_all_trans(void) +{ + /* New writes are disabled, so every held lock will be released after its + * current frame copy. Seal every remaining OPEN transport. */ + for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { + ble_log_prph_trans_t *trans = g_pool.trans[id]; + while (!ble_log_cas_acquire(&trans->atomic_lock)) { + } + if (BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state) == BLE_LOG_TRANS_STATE_OPEN && + trans->pos > 0) { + ble_log_pool_bitmap_clear(&g_pool.open_bitmap, id); + ble_log_pool_seal_and_send(trans); /* releases the lock */ + } else { + ble_log_cas_release(&trans->atomic_lock); + } + } + + TickType_t start_tick = xTaskGetTickCount(); + while ((BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.free_bitmap) & + BLE_LOG_POOL_ALL_MASK) != BLE_LOG_POOL_ALL_MASK) { + (void)ble_log_rt_drain(); + if ((xTaskGetTickCount() - start_tick) >= BLE_LOG_WAIT_TIMEOUT_TICKS) { + BLE_LOG_CONSOLE("@EW: Timed out waiting for BLE Log transports\n"); + return false; + } + vTaskDelay(1); + } + + return true; +} + +void ble_log_lbm_flush_open_transports(void) +{ + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_enabled)) { + return; + } + + for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { + if (!(BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.open_bitmap) & BIT(id))) { + continue; + } + ble_log_prph_trans_t *trans = g_pool.trans[id]; + if (!ble_log_cas_acquire(&trans->atomic_lock)) { + continue; + } + if (BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state) == BLE_LOG_TRANS_STATE_OPEN && + trans->pos > 0) { + ble_log_pool_seal_and_send(trans); + } else { + ble_log_cas_release(&trans->atomic_lock); + } + } + + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); +} + +/* ------------------------ */ +/* PUBLIC INTERFACE */ +/* ------------------------ */ +bool ble_log_enable(bool enable) +{ +#if CONFIG_BLE_LOG_PRPH_TEST + if (ble_log_test_enable_before_lifecycle_lock_hook) { + ble_log_test_enable_before_lifecycle_lock_hook(); + } +#endif + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_inited)) { + return false; + } + + BLE_LOG_ENTER_CRITICAL(); + /* Recheck under the same lifecycle lock used by deinit and flush restore, + * so a racing enable cannot reopen a closed producer gate. */ + if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_inited) || flush_in_progress) { + BLE_LOG_EXIT_CRITICAL(); + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return false; + } + if (enable) { + BLE_LOG_ATOMIC_STORE_RELEASE(lbm_enabled, true); + } else { + ble_log_lbm_disable(); + } + BLE_LOG_EXIT_CRITICAL(); + + if (!enable) { +#if CONFIG_BLE_LOG_PRPH_TEST + if (ble_log_test_disable_before_wake_hook) { + ble_log_test_disable_before_wake_hook(); + } +#endif + /* Keep the lifetime reference until every waiter wake is complete. */ + ble_log_pool_wake_all(); + } + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return true; +} + +void ble_log_flush(void) +{ + BLE_LOG_ENTER_CRITICAL(); + if (flush_in_progress) { + BLE_LOG_EXIT_CRITICAL(); + return; + } + flush_in_progress = true; + BLE_LOG_EXIT_CRITICAL(); + + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_inited)) { + goto done; + } + + bool lbm_enabled_copy = BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled); + ble_log_lbm_disable(); + TickType_t start_tick = xTaskGetTickCount(); + while ((BLE_LOG_ATOMIC_LOAD_SEQ_CST(lbm_ref_count) > 1) || + (BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.waiting_task_count) > 0)) { + ble_log_pool_wake_all(); + if ((xTaskGetTickCount() - start_tick) >= BLE_LOG_WAIT_TIMEOUT_TICKS) { + BLE_LOG_CONSOLE("@EW: Timed out waiting for BLE Log writers\n"); + goto restore; + } + vTaskDelay(1); + } + +#if BLE_LOG_UART_REDIR_ENABLED + if (!ble_log_prph_flush()) { + goto restore; + } +#endif + + if (!ble_log_pool_flush_all_trans() || + !ble_log_internal_snapshot(BLE_LOG_SNAPSHOT_REASON_FLUSH, NULL, true)) { + goto restore; + } + + start_tick = xTaskGetTickCount(); + while (BLE_LOG_ATOMIC_LOAD_ACQUIRE(internal_trans->state) != + BLE_LOG_TRANS_STATE_FREE) { + (void)ble_log_rt_drain(); + if ((xTaskGetTickCount() - start_tick) >= BLE_LOG_WAIT_TIMEOUT_TICKS) { + BLE_LOG_CONSOLE("@EW: Timed out waiting for BLE Log Internal Snapshot\n"); + goto restore; + } + vTaskDelay(1); + } + + /* FLUSH is not a segment boundary: reset interval counters while the + * Global SN and the snapshot sequence stay continuous. Snapshot loss is + * lifecycle-cumulative in the snapshot sequence and is preserved. */ + for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { + BLE_LOG_ATOMIC_STORE_RELAXED(stat_mgr_ctx[i].counters.written_frame_cnt, 0); + BLE_LOG_ATOMIC_STORE_RELAXED(stat_mgr_ctx[i].counters.lost_frame_cnt, 0); + } + BLE_LOG_ATOMIC_STORE_RELAXED(g_pool.inflight_peak, 0); +#if BLE_LOG_UART_REDIR_ENABLED + ble_log_prph_reset_util_counters(); +#endif + +restore: + BLE_LOG_ENTER_CRITICAL(); + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_inited)) { + BLE_LOG_ATOMIC_STORE_RELEASE(lbm_enabled, lbm_enabled_copy); + } + BLE_LOG_EXIT_CRITICAL(); + + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); +done: + BLE_LOG_ENTER_CRITICAL(); + flush_in_progress = false; + BLE_LOG_EXIT_CRITICAL(); +} + +BLE_LOG_IRAM_ATTR +bool ble_log_write_hex(ble_log_src_t src_code, const uint8_t *addr, size_t len) +{ + /* Source IDs are the frozen public enum values; garbage values are + * rejected. A non-NULL buffer with len > 0 is the caller's contract. */ + if (src_code >= BLE_LOG_SRC_MAX || (!addr && len)) { + return false; + } + + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_enabled)) { + return false; + } + /* One Global SN per attempt, consumed before the length and pool checks + * so rejected or lost attempts leave a gap. */ + uint32_t frame_sn = BLE_LOG_GET_GLOBAL_SN(); + + size_t max_log_len = BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t); + if (len > max_log_len) { + goto failed; + } + + bool is_isr = BLE_LOG_IN_ISR(); + bool can_yield = !is_isr && xPortCanYield() && + xTaskGetSchedulerState() == taskSCHEDULER_RUNNING; + uint32_t timestamp = ble_log_timestamp_now(); + size_t payload_len = sizeof(timestamp) + len; + ble_log_prph_trans_t *trans = + ble_log_pool_acquire(payload_len, !can_yield, false); + if (!trans) { + goto failed; + } + + uint8_t source_meta = BLE_LOG_MAKE_SOURCE_META(src_code, !can_yield); + ble_log_pool_write_frame(trans, frame_sn, source_meta, + (const uint8_t *)×tamp, sizeof(timestamp), + addr, (uint16_t)len, NULL, 0, false); + + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return true; + +failed: + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_inited)) { + ble_log_stat_mgr_mark_lost(src_code); + } + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return false; +} + +#if CONFIG_BLE_LOG_LL_ENABLED +BLE_LOG_IRAM_ATTR +void ble_log_write_hex_ll(uint32_t len, const uint8_t *addr, + uint32_t len_append, const uint8_t *addr_append, uint32_t flag) +{ + /* Controller-side HCI logging duplicates the retained Host-side HCI path. */ + if ((flag & BIT(BLE_LOG_LL_FLAG_HCI)) || + (flag & BIT(BLE_LOG_LL_FLAG_HCI_UPSTREAM)) || + (!addr && len) || (!addr_append && len_append)) { + return; + } + + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_enabled)) { + return; + } + + /* One Global SN per attempt; the entry order it assigns also breaks + * ties between equal-timestamp records from different sources. */ + uint32_t frame_sn = BLE_LOG_GET_GLOBAL_SN(); + + /* Controller-side HCI records are dropped above, so only the LL task + * and ISR identities remain: ISR-flagged records keep their own + * LL_ISR source ID, everything else is LL_TASK. */ + ble_log_src_t src_code = (flag & BIT(BLE_LOG_LL_FLAG_ISR)) ? + BLE_LOG_SRC_LL_ISR : BLE_LOG_SRC_LL_TASK; + + bool is_isr = BLE_LOG_IN_ISR(); + bool can_yield = !is_isr && xPortCanYield() && + xTaskGetSchedulerState() == taskSCHEDULER_RUNNING; + bool non_yield = !can_yield || (flag & BIT(BLE_LOG_LL_FLAG_ISR)); + bool omdata = flag & BIT(BLE_LOG_LL_FLAG_OMDATA); + if (is_isr) { + /* os_mbuf_copydata is in flash and cannot run from an ISR. */ + omdata = false; + } + + if (len > BLE_LOG_MAX_PAYLOAD_LEN || + len_append > BLE_LOG_MAX_PAYLOAD_LEN - len) { + goto failed; + } + + size_t payload_len = len + len_append; + ble_log_prph_trans_t *trans = + ble_log_pool_acquire(payload_len, non_yield, can_yield && !non_yield); + if (!trans) { + goto failed; + } + + uint8_t source_meta = BLE_LOG_MAKE_SOURCE_META(src_code, non_yield); + ble_log_pool_write_frame(trans, frame_sn, source_meta, + NULL, 0, addr, (uint16_t)len, + addr_append, (uint16_t)len_append, omdata); + + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); + return; + +failed: + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_inited)) { + ble_log_stat_mgr_mark_lost(src_code); + } + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); +} +#endif /* CONFIG_BLE_LOG_LL_ENABLED */ + +void ble_log_dump_to_console(void) +{ + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_inited)) { + return; + } + + BLE_LOG_ENTER_CRITICAL(); + BLE_LOG_CONSOLE("[BLE_LOG_DUMP_START:\n"); + for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { + ble_log_prph_trans_t *trans = g_pool.trans[id]; + BLE_LOG_FEED_WDT(); + for (int k = 0; k < trans->size; k++) { + BLE_LOG_CONSOLE("%02x ", trans->buf[k]); + if (!(k & 0xFF)) { + BLE_LOG_FEED_WDT(); + } + } + } + BLE_LOG_CONSOLE("\n:BLE_LOG_DUMP_END]\n\n"); + BLE_LOG_EXIT_CRITICAL(); + + BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); +} diff --git a/components/bt/common/ble_log/src/ble_log_rt.c b/components/bt/common/ble_log/src/ble_log_rt.c index b085468be0a..d6ed9ea8571 100644 --- a/components/bt/common/ble_log/src/ble_log_rt.c +++ b/components/bt/common/ble_log/src/ble_log_rt.c @@ -11,7 +11,7 @@ /* INCLUDE */ #include "ble_log.h" #include "ble_log_rt.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "esp_log.h" #include "esp_timer.h" @@ -55,6 +55,7 @@ BLE_LOG_STATIC BLE_LOG_DRAM_ATTR volatile uint32_t rt_ref_count = 0; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR QueueHandle_t rt_queue_handle = NULL; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR esp_timer_handle_t rt_defer_timer = NULL; BLE_LOG_STATIC uint32_t rt_last_hook_os_ts = 0; +BLE_LOG_STATIC ble_log_version_info_t rt_version_info; #if CONFIG_BLE_LOG_TS_ENABLED BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t rt_ts_enabled = 0; BLE_LOG_STATIC esp_timer_handle_t rt_ts_timer = NULL; @@ -63,6 +64,7 @@ BLE_LOG_STATIC esp_timer_handle_t rt_ts_timer = NULL; /* PRIVATE FUNCTION DECLARATION */ BLE_LOG_STATIC void ble_log_rt_defer_cb(void *arg); BLE_LOG_STATIC bool ble_log_rt_dispatch(QueueHandle_t queue, UBaseType_t pending); +BLE_LOG_STATIC void ble_log_rt_version_info_init(void); BLE_LOG_STATIC void ble_log_rt_run_hook(void); #if CONFIG_BLE_LOG_TS_ENABLED BLE_LOG_STATIC void ble_log_rt_ts_trigger(void *arg); @@ -75,53 +77,58 @@ BLE_LOG_STATIC void ble_log_commit_copy(uint8_t *dst, const char *src, size_t le BLE_LOG_MEMCPY(dst, src, strnlen(src, len)); } +BLE_LOG_STATIC void ble_log_rt_version_info_init(void) +{ + BLE_LOG_MEMSET(&rt_version_info, 0, sizeof(rt_version_info)); + rt_version_info.int_src_code = BLE_LOG_INT_SRC_VERSION_INFO; + rt_version_info.version = BLE_LOG_VERSION; +#ifdef BLE_LOG_IDF_COMMIT + BLE_LOG_MEMCPY(rt_version_info.idf_commit, BLE_LOG_IDF_COMMIT, + BLE_LOG_IDF_COMMIT_LEN); +#endif +#if CONFIG_BT_CONTROLLER_ENABLED && defined(BLE_LOG_CONTROLLER_GET_COMMIT) + ble_log_commit_copy(rt_version_info.controller_commit, + BLE_LOG_CONTROLLER_GET_COMMIT(), BLE_LOG_LIB_COMMIT_LEN); +#endif +#if CONFIG_BT_CONTROLLER_ENABLED && defined(BLE_LOG_BTDM_COMMON_GET_COMMIT) + ble_log_commit_copy(rt_version_info.btdm_common_commit, + BLE_LOG_BTDM_COMMON_GET_COMMIT(), BLE_LOG_LIB_COMMIT_LEN); +#endif +#if CONFIG_BLE_MESH && CONFIG_BLE_MESH_V11_SUPPORT + const char *mesh_commit = strrchr(bt_mesh_v11_commit_str, ' '); + if (mesh_commit) { + ble_log_commit_copy(rt_version_info.mesh_commit, mesh_commit + 1, + BLE_LOG_LIB_COMMIT_LEN); + } +#endif +#if CONFIG_BT_AUDIO && CONFIG_SOC_BLE_AUDIO_SUPPORTED + ble_log_commit_copy(rt_version_info.audio_commit, lib_audio_commit_get(), + BLE_LOG_LIB_COMMIT_LEN); +#endif + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + rt_version_info.chip_model = (uint16_t)chip_info.model; + rt_version_info.chip_revision = chip_info.revision; + ble_log_internal_set_version_info(&rt_version_info); +} + BLE_LOG_STATIC void ble_log_rt_run_hook(void) { + if (!ble_log_lbm_is_enabled()) { + return; + } +#if CONFIG_BLE_LOG_TS_ENABLED + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(rt_ts_enabled)) { + return; + } +#endif uint32_t now = pdTICKS_TO_MS(xTaskGetTickCount()); if ((uint32_t)(now - rt_last_hook_os_ts) < BLE_LOG_TS_TRIGGER_TIMEOUT_MS) { return; } rt_last_hook_os_ts = now; - - /* Write version info: BLE Log version, idf commit (build-time), - * linked-in BLE lib commits, chip model/revision (efuse, runtime-only). - * Libs absent from the build leave their fields zero. */ - ble_log_version_info_t version_info = { - .int_src_code = BLE_LOG_INT_SRC_VERSION_INFO, - .version = BLE_LOG_VERSION, - }; -#ifdef BLE_LOG_IDF_COMMIT - BLE_LOG_MEMCPY(version_info.idf_commit, BLE_LOG_IDF_COMMIT, BLE_LOG_IDF_COMMIT_LEN); -#endif -#if CONFIG_BT_CONTROLLER_ENABLED && defined(BLE_LOG_CONTROLLER_GET_COMMIT) - ble_log_commit_copy(version_info.controller_commit, BLE_LOG_CONTROLLER_GET_COMMIT(), - BLE_LOG_LIB_COMMIT_LEN); -#endif -#if CONFIG_BT_CONTROLLER_ENABLED && defined(BLE_LOG_BTDM_COMMON_GET_COMMIT) - ble_log_commit_copy(version_info.btdm_common_commit, BLE_LOG_BTDM_COMMON_GET_COMMIT(), - BLE_LOG_LIB_COMMIT_LEN); -#endif -#if CONFIG_BLE_MESH && CONFIG_BLE_MESH_V11_SUPPORT - /* The hash is the substring after the last space of the lib string */ - const char *mesh_commit = strrchr(bt_mesh_v11_commit_str, ' '); - if (mesh_commit) { - ble_log_commit_copy(version_info.mesh_commit, mesh_commit + 1, - BLE_LOG_LIB_COMMIT_LEN); - } -#endif -#if CONFIG_BT_AUDIO && CONFIG_SOC_BLE_AUDIO_SUPPORTED - ble_log_commit_copy(version_info.audio_commit, lib_audio_commit_get(), - BLE_LOG_LIB_COMMIT_LEN); -#endif - esp_chip_info_t chip_info; - esp_chip_info(&chip_info); - version_info.chip_model = (uint16_t)chip_info.model; - version_info.chip_revision = chip_info.revision; - ble_log_write_hex(BLE_LOG_SRC_INTERNAL, (const uint8_t *)&version_info, - sizeof(version_info)); - - ble_log_write_enh_stat(); - ble_log_write_buf_util(); + (void)ble_log_internal_snapshot(BLE_LOG_SNAPSHOT_REASON_PERIODIC, + NULL, false); } BLE_LOG_STATIC bool ble_log_rt_dispatch(QueueHandle_t queue, UBaseType_t pending) @@ -170,10 +177,12 @@ BLE_LOG_STATIC void ble_log_rt_ts_trigger(void *arg) return; } - ble_log_ts_info_t *ts_info = NULL; - ble_log_ts_info_update(&ts_info); - if (ts_info) { - ble_log_write_hex(BLE_LOG_SRC_INTERNAL, (const uint8_t *)ts_info, sizeof(ble_log_ts_info_t)); + ble_log_ts_info_t ts_info; + if (ble_log_ts_info_update(&ts_info)) { + (void)ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC | + BLE_LOG_SNAPSHOT_REASON_TS_VALID, + &ts_info, false); } } #endif /* CONFIG_BLE_LOG_TS_ENABLED */ @@ -185,6 +194,7 @@ bool ble_log_rt_init(void) return true; } + ble_log_rt_version_info_init(); rt_queue_handle = xQueueCreate(BLE_LOG_TRANS_TOTAL_CNT, sizeof(ble_log_prph_trans_t *)); if (!rt_queue_handle) { goto exit; @@ -314,11 +324,12 @@ BLE_LOG_IRAM_ATTR void ble_log_rt_submit_trans(ble_log_prph_trans_t *trans) #if CONFIG_BLE_LOG_TS_ENABLED bool ble_log_sync_enable(bool enable) { - if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(rt_inited)) { + if (!ble_log_ref_count_try_acquire(&rt_ref_count, &rt_inited)) { return false; } BLE_LOG_ATOMIC_STORE_RELEASE(rt_ts_enabled, enable); ble_log_ts_reset(enable); + BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); return true; } #endif /* CONFIG_BLE_LOG_TS_ENABLED */ diff --git a/components/bt/common/ble_log/src/ble_log_ts.c b/components/bt/common/ble_log/src/ble_log_ts.c index 03f931fe6ce..9bc822d7679 100644 --- a/components/bt/common/ble_log/src/ble_log_ts.c +++ b/components/bt/common/ble_log/src/ble_log_ts.c @@ -84,9 +84,11 @@ void ble_log_ts_reset(bool status) return; } + BLE_LOG_ENTER_CRITICAL(); if (!status && !ts_info->io_level) { gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, 1); } ts_info->io_level = 0; gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, 0); + BLE_LOG_EXIT_CRITICAL(); } diff --git a/components/bt/common/ble_log/src/ble_log_util.c b/components/bt/common/ble_log/src/ble_log_util.c index b602f3ced41..e8ea3bd39bc 100644 --- a/components/bt/common/ble_log/src/ble_log_util.c +++ b/components/bt/common/ble_log/src/ble_log_util.c @@ -74,12 +74,12 @@ uint32_t ble_log_fast_checksum(const uint8_t *data, size_t len) BLE_LOG_IRAM_ATTR bool ble_log_ref_count_try_acquire(volatile uint32_t *ref_count, - const uint32_t *inited) + const uint32_t *gate) { - /* The seq_cst increment/check pairs with deinit's seq_cst gate close - * before it waits for the reference count. */ + /* The seq_cst increment/check pairs with a seq_cst gate close before the + * owner waits for the reference count. */ BLE_LOG_REF_COUNT_ACQUIRE_SEQ_CST(ref_count); - if (BLE_LOG_ATOMIC_LOAD_SEQ_CST(*inited)) { + if (BLE_LOG_ATOMIC_LOAD_SEQ_CST(*gate)) { return true; } BLE_LOG_REF_COUNT_RELEASE(ref_count); diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h new file mode 100644 index 00000000000..aa0260ae377 --- /dev/null +++ b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h @@ -0,0 +1,240 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef __BLE_LOG_LBM_V2_H__ +#define __BLE_LOG_LBM_V2_H__ + +/* -------------------------------------------------- */ +/* BLE Log - Unified Transport Pool (v7) */ +/* -------------------------------------------------- */ +/* Replaces the legacy multi-LBM design (ble_log_lbm.h) with one shared + * transport pool. Written as a new module so the legacy implementation + * stays untouched and reviewable as a pure addition. */ + +/* INCLUDE */ +#include "ble_log.h" +#include "ble_log_prph.h" +#include "ble_log_ts.h" + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +/* ------------------------- */ +/* Log Frame Defines */ +/* ------------------------- */ +typedef struct { + uint16_t length; + uint32_t frame_meta; +} __attribute__((packed)) ble_log_frame_head_t; + +#define BLE_LOG_FRAME_HEAD_LEN (sizeof(ble_log_frame_head_t)) +#define BLE_LOG_FRAME_TAIL_LEN (sizeof(uint32_t)) +#define BLE_LOG_FRAME_OVERHEAD (BLE_LOG_FRAME_HEAD_LEN + BLE_LOG_FRAME_TAIL_LEN) +#define BLE_LOG_MAKE_SOURCE_META(src, non_yield) \ + (((src) & BLE_LOG_SRC_ID_MASK) | ((non_yield) ? BLE_LOG_SRC_FLAG_NON_YIELD : 0)) +#define BLE_LOG_MAKE_FRAME_META(source_meta, sn) \ + (((source_meta) & 0xffU) | (((sn) & 0x00ffffffU) << 8)) + +/* ------------------------------------- */ +/* Unified Buffer Pool Defines */ +/* ------------------------------------- */ +#define BLE_LOG_POOL_TRANS_CNT CONFIG_BLE_LOG_POOL_TRANS_CNT +#define BLE_LOG_POOL_NON_YIELD_RESERVE_CNT CONFIG_BLE_LOG_POOL_NON_YIELD_RESERVE_CNT +#define BLE_LOG_POOL_SHARED_CNT (BLE_LOG_POOL_TRANS_CNT - BLE_LOG_POOL_NON_YIELD_RESERVE_CNT) +#define BLE_LOG_POOL_TRANS_SIZE CONFIG_BLE_LOG_POOL_TRANS_SIZE +#define BLE_LOG_TRANS_SIZE BLE_LOG_POOL_TRANS_SIZE +#define BLE_LOG_MAX_PAYLOAD_LEN (BLE_LOG_POOL_TRANS_SIZE - BLE_LOG_FRAME_OVERHEAD) +#define BLE_LOG_TRANS_INTERNAL_CNT (1) + +#if BLE_LOG_UART_REDIR_ENABLED +#define BLE_LOG_TRANS_REDIR_CNT BLE_LOG_TRANS_BUF_CNT +#else +#define BLE_LOG_TRANS_REDIR_CNT (0) +#endif + +#define BLE_LOG_TRANS_TOTAL_CNT \ + (BLE_LOG_POOL_TRANS_CNT + BLE_LOG_TRANS_INTERNAL_CNT + BLE_LOG_TRANS_REDIR_CNT) + +/* --------------------------------------- */ +/* Protocol v7 Source ID Space */ +/* --------------------------------------- */ +/* The frozen public ble_log_src_t values are the on-wire and statistic + * source IDs: the frame source byte carries the enum value directly and + * every source keeps its own wire sequence, matching the legacy wire + * identity so existing decoders keep working. */ +#define BLE_LOG_SRC_ID_MASK 0x7f +#define BLE_LOG_SRC_FLAG_NON_YIELD 0x80 +#define BLE_LOG_SRC_ID(source_meta) ((source_meta) & BLE_LOG_SRC_ID_MASK) +#define BLE_LOG_SRC_IS_NON_YIELD(source_meta) (((source_meta) & BLE_LOG_SRC_FLAG_NON_YIELD) != 0) + +/* Statistic slots in the Internal Snapshot: every public source that can + * produce frames, i.e. CUSTOM through ENCODE. INTERNAL frames carry their + * own snapshot sequence in the frame header; REDIR is a raw console + * stream. */ +#define BLE_LOG_SRC_CORE_FIRST BLE_LOG_SRC_CUSTOM +#define BLE_LOG_SRC_CORE_COUNT (BLE_LOG_SRC_ENCODE - BLE_LOG_SRC_CORE_FIRST + 1) + +/* ------------------------------------- */ +/* UART Redirection Manager */ +/* ------------------------------------- */ +#if BLE_LOG_UART_REDIR_ENABLED +typedef struct { + ble_log_prph_trans_t *trans[BLE_LOG_TRANS_BUF_CNT]; + SemaphoreHandle_t mutex; + int trans_idx; + volatile uint32_t inflight; + volatile uint32_t inflight_peak; +} ble_log_redir_t; +#endif + +/* -------------------------------- */ +/* Compact Core Statistics */ +/* -------------------------------- */ +typedef struct { + uint32_t written_frame_cnt; + uint32_t lost_frame_cnt; +} __attribute__((packed)) ble_log_source_stat_t; + +/* Runtime per-source counters. The aligned wrapper keeps the embedded + * packed counters word-aligned, so atomic updates stay single-instruction + * (amo/s32c1i) on both toolchains. */ +typedef struct { + ble_log_source_stat_t counters; +} __attribute__((aligned(4))) ble_log_stat_mgr_t; + +#define BLE_LOG_GET_FRAME_SN(VAR) BLE_LOG_ATOMIC_ADD_RELAXED(VAR, 1) + +/* One 24-bit Global SN is shared by every non-INTERNAL source: consumed at + * API entry (before pool contention), it orders all log attempts — + * including equal-timestamp records from different sources — and every + * lost or rejected attempt leaves a gap. Internal Snapshot frames keep + * their own separate sequence: a skipped periodic snapshot burns one + * snapshot SN, so a gap there counts lost snapshots without a dedicated + * field. Neither sequence is ever reset. */ +#define BLE_LOG_SN_MODULO 0x00ffffffU +#define BLE_LOG_GET_GLOBAL_SN() \ + ((uint32_t)BLE_LOG_GET_FRAME_SN(g_frame_sn) & BLE_LOG_SN_MODULO) +#define BLE_LOG_GET_SNAPSHOT_SN() \ + ((uint32_t)BLE_LOG_GET_FRAME_SN(g_snapshot_sn) & BLE_LOG_SN_MODULO) + +/* -------------------------------- */ +/* Internal Snapshot Frame */ +/* -------------------------------- */ +#define BLE_LOG_SNAPSHOT_REASON_INIT BIT(0) +#define BLE_LOG_SNAPSHOT_REASON_PERIODIC BIT(1) +#define BLE_LOG_SNAPSHOT_REASON_FLUSH BIT(2) +#define BLE_LOG_SNAPSHOT_REASON_TS_VALID BIT(3) + +typedef struct { + uint8_t int_src_code; + uint16_t reason_flags; + ble_log_version_info_t version_info; + + /* Clock samples captured at the same instant, mirroring + * ble_log_ts_info_t (minus its int_src_code). */ + struct { + uint8_t io_level; + uint32_t lc_ts; /* Link-layer clock; 0 when not available */ + uint32_t esp_ts; /* esp_timer_get_time() */ + uint32_t os_ts; /* FreeRTOS tick count in ms */ + } __attribute__((packed)) ts; + + /* Unified pool state at capture time. */ + struct { + uint8_t trans_cnt; + uint8_t non_yield_reserve_cnt; + uint8_t inflight; + uint8_t inflight_peak; + } __attribute__((packed)) pool; + + ble_log_source_stat_t stats[BLE_LOG_SRC_CORE_COUNT]; +} __attribute__((packed)) ble_log_internal_snapshot_t; + +#define BLE_LOG_INTERNAL_FRAME_LEN \ + (BLE_LOG_FRAME_OVERHEAD + sizeof(uint32_t) + sizeof(ble_log_internal_snapshot_t)) +#define BLE_LOG_INTERNAL_TRANS_SIZE \ + ((BLE_LOG_INTERNAL_FRAME_LEN + 3U) & ~3U) + +/* -------------------------- */ +/* Link Layer Defines */ +/* -------------------------- */ +#if CONFIG_BLE_LOG_LL_ENABLED +/* Numeric positions are an ABI with prebuilt controller libraries. */ +enum { + BLE_LOG_LL_FLAG_CONTINUE = 0, + BLE_LOG_LL_FLAG_END = 1, + BLE_LOG_LL_FLAG_TASK = 2, + BLE_LOG_LL_FLAG_ISR = 3, + BLE_LOG_LL_FLAG_HCI = 4, + BLE_LOG_LL_FLAG_RAW = 5, + BLE_LOG_LL_FLAG_OMDATA = 6, + BLE_LOG_LL_FLAG_HCI_UPSTREAM = 7, +}; +#endif + +/* ------------------------------- */ +/* Compile-Time Guards */ +/* ------------------------------- */ +_Static_assert(sizeof(ble_log_frame_head_t) == 6, + "Unexpected BLE Log frame header size"); +_Static_assert(BLE_LOG_POOL_TRANS_CNT >= 2 && BLE_LOG_POOL_TRANS_CNT <= 32, + "BLE_LOG_POOL_TRANS_CNT must be within [2, 32]"); +_Static_assert(BLE_LOG_POOL_NON_YIELD_RESERVE_CNT > 0 && + BLE_LOG_POOL_NON_YIELD_RESERVE_CNT < BLE_LOG_POOL_TRANS_CNT, + "Non-yield reserve must leave at least one shared transport"); +_Static_assert(BLE_LOG_POOL_TRANS_SIZE >= BLE_LOG_FRAME_OVERHEAD + sizeof(uint32_t), + "BLE_LOG_POOL_TRANS_SIZE is too small for a timestamped frame"); +_Static_assert(BLE_LOG_POOL_TRANS_SIZE <= UINT16_MAX, + "BLE_LOG_POOL_TRANS_SIZE exceeds the transport size field"); +_Static_assert(BLE_LOG_POOL_TRANS_SIZE <= 10240, + "BLE_LOG_POOL_TRANS_SIZE exceeds the peripheral transfer limit"); +#if CONFIG_BLE_LOG_PRPH_SPI_MASTER_DMA || CONFIG_BLE_LOG_PRPH_SPI_MASTER_HD +_Static_assert((BLE_LOG_POOL_TRANS_SIZE & 3U) == 0, + "SPI BLE Log transport size must be four-byte aligned"); +#endif +_Static_assert(sizeof(ble_log_version_info_t) == 58, + "Unexpected BLE Log version information size"); +_Static_assert(sizeof(ble_log_source_stat_t) == 8, + "Unexpected BLE Log source statistic size"); +_Static_assert(offsetof(ble_log_stat_mgr_t, counters) == 0 && + sizeof(ble_log_stat_mgr_t) == 8, + "stat manager layout must keep the counters word-aligned"); +_Static_assert(sizeof(ble_log_internal_snapshot_t) == 134, + "Unexpected BLE Log Internal Snapshot size"); +_Static_assert(BLE_LOG_INTERNAL_FRAME_LEN == 148, + "Unexpected BLE Log Internal frame size"); +_Static_assert(BLE_LOG_INTERNAL_TRANS_SIZE >= BLE_LOG_INTERNAL_FRAME_LEN, + "Internal transport is too small"); +_Static_assert((BLE_LOG_TRANS_BUF_CNT & (BLE_LOG_TRANS_BUF_CNT - 1)) == 0, + "BLE_LOG_TRANS_BUF_CNT must be a power of two"); + +/* --------------------------- */ +/* Internal Interfaces */ +/* --------------------------- */ +bool ble_log_lbm_init(void); +void ble_log_lbm_begin_deinit(void); +void ble_log_lbm_deinit(void); +bool ble_log_lbm_is_enabled(void); +void ble_log_lbm_flush_open_transports(void); +void ble_log_lbm_recycle_trans(ble_log_prph_trans_t *trans); +void ble_log_internal_set_version_info(const ble_log_version_info_t *version_info); +bool ble_log_internal_snapshot(uint16_t reason_flags, + const ble_log_ts_info_t *ts_info, + bool required); + +/* Claim/commit: the public ble_log_src_t is accepted for API stability, but + * only BLE_LOG_SRC_ENCODE is supported; its frames are stamped with the + * ENCODE source ID on the wire. */ +uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, uint32_t *handle); +void ble_log_commit(uint32_t handle, size_t actual_len); + +#if BLE_LOG_UART_REDIR_ENABLED +void ble_log_lbm_stream_write(ble_log_redir_t *redir, ble_log_src_t src_code, + uint32_t timestamp, const uint8_t *data, size_t len); +void ble_log_lbm_stream_flush(ble_log_redir_t *redir, ble_log_src_t src_code); +ble_log_redir_t *ble_log_prph_get_redir_lbm(void); +#endif + +#endif /* __BLE_LOG_LBM_V2_H__ */ diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_prph.h b/components/bt/common/ble_log/src/internal_include/ble_log_prph.h index e9986c6efb7..801445bae69 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_prph.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_prph.h @@ -10,32 +10,57 @@ /* BLE Log - Peripheral Interface */ /* ------------------------------ */ -/* INCLUDE */ #include "ble_log_util.h" -/* TYPEDEF */ +#if defined(CONFIG_BLE_LOG_PRPH_UART_DMA) && (CONFIG_BLE_LOG_PRPH_UART_DMA_PORT == 0) +#define BLE_LOG_UART_REDIR_ENABLED (1) +#else +#define BLE_LOG_UART_REDIR_ENABLED (0) +#endif + +typedef enum { + BLE_LOG_TRANS_STATE_FREE = 0, + BLE_LOG_TRANS_STATE_OPEN, + BLE_LOG_TRANS_STATE_CLAIMED, + BLE_LOG_TRANS_STATE_SENDING, +} ble_log_trans_state_t; + +typedef enum { + BLE_LOG_TRANS_OWNER_POOL = 0, + BLE_LOG_TRANS_OWNER_INTERNAL, + BLE_LOG_TRANS_OWNER_REDIR, +} ble_log_trans_owner_t; + +#define BLE_LOG_TRANS_ID_NONE (0xff) + typedef struct { - volatile uint32_t prph_owned; + /* Transport lifecycle ownership, shared with the runtime dispatch and + * the peripheral tx-done recycle. Claim/commit bookkeeping deliberately + * lives in the pool (ble_log_lbm_v2.c), not here. */ + volatile ble_log_atomic_lock_t atomic_lock; + volatile uint8_t state; + uint8_t id; + uint8_t owner_kind; + uint8_t *buf; uint16_t size; uint16_t pos; - /* Peripheral implementation specific context */ + /* Peripheral implementation specific context. */ void *ctx; - - /* Opaque back-reference to owning LBM, set once at init */ - void *owner; } ble_log_prph_trans_t; #define BLE_LOG_TRANS_FREE_SPACE(trans) ((trans)->size - (trans)->pos) #define BLE_LOG_TRANS_BUF_CNT (4) -/* INTERFACE */ bool ble_log_prph_init(size_t trans_cnt); void ble_log_prph_deinit(void); bool ble_log_prph_trans_init(ble_log_prph_trans_t **trans, size_t trans_size); void ble_log_prph_trans_deinit(ble_log_prph_trans_t **trans); void ble_log_prph_send_trans(ble_log_prph_trans_t *trans); +#if BLE_LOG_UART_REDIR_ENABLED +bool ble_log_prph_flush(void); void ble_log_prph_reset_util_counters(void); +#endif #endif /* __BLE_LOG_PRPH_H__ */ diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_util.h b/components/bt/common/ble_log/src/internal_include/ble_log_util.h index 1ce2dbb719a..260cd6851ac 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_util.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_util.h @@ -27,9 +27,11 @@ #define BLE_LOG_ATOMIC_LOAD_RELAXED(VAR) __atomic_load_n(&(VAR), __ATOMIC_RELAXED) #define BLE_LOG_ATOMIC_STORE_RELEASE(VAR, VALUE) __atomic_store_n(&(VAR), (VALUE), __ATOMIC_RELEASE) #define BLE_LOG_ATOMIC_STORE_RELAXED(VAR, VALUE) __atomic_store_n(&(VAR), (VALUE), __ATOMIC_RELAXED) +#define BLE_LOG_ATOMIC_ADD_RELAXED(VAR, VALUE) __atomic_fetch_add(&(VAR), (VALUE), __ATOMIC_RELAXED) + +typedef uint32_t ble_log_atomic_lock_t; /* Reference counting macros */ -#define BLE_LOG_REF_COUNT_ACQUIRE(VAR) __atomic_fetch_add(VAR, 1, __ATOMIC_ACQUIRE) #define BLE_LOG_REF_COUNT_RELEASE(VAR) __atomic_fetch_sub(VAR, 1, __ATOMIC_RELEASE) /* Closing gate: pairs an inited-flag store with a reference-count load (and * vice versa) at seq_cst so deinit and a submitter cannot both observe the @@ -76,22 +78,14 @@ extern void esp_panic_handler_feed_wdts(void); #define BLE_LOG_FEED_WDT() esp_panic_handler_feed_wdts() /* INLINE */ -BLE_LOG_IRAM_ATTR static inline -bool ble_log_cas_acquire(volatile bool *cas_lock) -{ - bool expected = false; - return __atomic_compare_exchange_n( - cas_lock, &expected, true, false, __ATOMIC_ACQUIRE, __ATOMIC_RELAXED - ); -} +/* Compare-and-swap lock as macros: single-instruction acquire/release used + * from several IRAM sites; a function would add an IRAM call site each. */ +#define ble_log_cas_acquire(cas_lock) \ + (__atomic_exchange_n((cas_lock), 1, __ATOMIC_ACQUIRE) == 0) +#define ble_log_cas_release(cas_lock) \ + __atomic_store_n((cas_lock), 0, __ATOMIC_RELEASE) -BLE_LOG_IRAM_ATTR static inline -void ble_log_cas_release(volatile bool *cas_lock) -{ - __atomic_store_n(cas_lock, false, __ATOMIC_RELEASE); -} - -#define BLE_LOG_VERSION (6) +#define BLE_LOG_VERSION (7) #define BLE_LOG_IDF_COMMIT_LEN (12) /* Lib commit hashes are at most 10 hex chars; zero-padded when shorter */ #define BLE_LOG_LIB_COMMIT_LEN (10) @@ -106,6 +100,7 @@ typedef enum { BLE_LOG_INT_SRC_BUF_UTIL, BLE_LOG_INT_SRC_FINAL_STAT, BLE_LOG_INT_SRC_VERSION_INFO, + BLE_LOG_INT_SRC_SNAPSHOT, BLE_LOG_INT_SRC_MAX, } ble_log_int_src_t; @@ -131,7 +126,7 @@ uint32_t ble_log_fast_checksum(const uint8_t *data, size_t len); /* Acquire a lifetime reference only while the closing gate remains open. */ bool ble_log_ref_count_try_acquire(volatile uint32_t *ref_count, - const uint32_t *inited); + const uint32_t *gate); /* Task-context wait; returns false if the count stays above max for one second. */ bool ble_log_ref_count_wait(volatile uint32_t *ref_count, uint32_t max_ref_count); diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_dummy.c b/components/bt/common/ble_log/src/prph/ble_log_prph_dummy.c index e00c3b0f1fe..38d366d064c 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_dummy.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_dummy.c @@ -9,7 +9,7 @@ /* INCLUDE */ #include "ble_log_prph_dummy.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" /* INTERFACE */ bool ble_log_prph_init(size_t trans_cnt) @@ -82,8 +82,8 @@ void ble_log_prph_trans_deinit(ble_log_prph_trans_t **trans) } /* Dummy transport has no DMA/hardware -- recycle the buffer immediately - * so that ble_log_lbm_get_trans() can reuse it and ble_log_flush() does - * not hang waiting for prph_owned to clear. Real peripherals (UART DMA, + * so that the pool can reuse it and ble_log_flush() does not hang waiting + * for it to drain. Real peripherals (UART DMA, * SPI DMA) do the same work inside their asynchronous tx_done callbacks. */ void ble_log_prph_send_trans(ble_log_prph_trans_t *trans) { diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_dma.c b/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_dma.c index 7014a58892b..0b2c99043f0 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_dma.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_dma.c @@ -10,7 +10,7 @@ /* INCLUDE */ #include "ble_log_prph_spi_master_dma.h" #include "ble_log_prph_spi_common.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "esp_timer.h" @@ -26,6 +26,7 @@ /* VARIABLE */ BLE_LOG_STATIC bool prph_inited = false; +BLE_LOG_STATIC bool bus_inited = false; BLE_LOG_STATIC spi_device_handle_t dev_handle = NULL; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t last_tx_done_ts = 0; @@ -37,7 +38,7 @@ BLE_LOG_STATIC void spi_master_dma_pre_tx_cb(spi_transaction_t *spi_trans); BLE_LOG_SPI_MASTER_DMA_CB_ATTR BLE_LOG_STATIC void spi_master_dma_tx_done_cb(spi_transaction_t *spi_trans) { /* SPI slave performance issue workaround */ - last_tx_done_ts = esp_timer_get_time(); + last_tx_done_ts = (uint32_t)esp_timer_get_time(); /* Recycle transport */ ble_log_prph_trans_t *trans = (ble_log_prph_trans_t *)(spi_trans->user); @@ -47,6 +48,7 @@ BLE_LOG_SPI_MASTER_DMA_CB_ATTR BLE_LOG_STATIC void spi_master_dma_tx_done_cb(spi BLE_LOG_SPI_MASTER_DMA_CB_ATTR BLE_LOG_STATIC void spi_master_dma_pre_tx_cb(spi_transaction_t *spi_trans) { + (void)spi_trans; /* SPI slave performance issue workaround */ while ((esp_timer_get_time() - last_tx_done_ts) < BLE_LOG_SPI_TRANS_ITVL_MIN_US) {} } @@ -74,6 +76,7 @@ bool ble_log_prph_init(size_t trans_cnt) if (spi_bus_initialize(BLE_LOG_SPI_BUS, &bus_config, SPI_DMA_CH_AUTO) != ESP_OK) { goto exit; } + bus_inited = true; spi_device_interface_config_t dev_config = { .clock_speed_hz = SPI_MASTER_FREQ_20M, @@ -108,8 +111,10 @@ void ble_log_prph_deinit(void) dev_handle = NULL; } - /* Note: We don't care if the bus has been inited or not */ - spi_bus_free(BLE_LOG_SPI_BUS); + if (bus_inited) { + spi_bus_free(BLE_LOG_SPI_BUS); + bus_inited = false; + } } bool ble_log_prph_trans_init(ble_log_prph_trans_t **trans, size_t trans_size) diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_hd.c b/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_hd.c index 04730fe0f22..9b202e56ed0 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_hd.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_hd.c @@ -10,7 +10,7 @@ /* INCLUDE */ #include "ble_log_prph_spi_master_hd.h" #include "ble_log_prph_spi_common.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "hal/spi_ll.h" #include "hal/spi_types.h" @@ -219,8 +219,7 @@ BLE_LOG_IRAM_ATTR void ble_log_prph_send_trans(ble_log_prph_trans_t *trans) if (spi_device_queue_trans(dev_handle, &ctx->end, 0) != ESP_OK) { uint8_t old_status = __atomic_fetch_or(&ctx->status, BLE_LOG_SPI_HD_END_QUEUE_FAILED, __ATOMIC_ACQ_REL); if (old_status & BLE_LOG_SPI_HD_DATA_DONE) { - /* Data already on the wire: drop it from the buffer so the next - * flush does not re-send these bytes (recycle keeps pos on purpose) */ + /* Data is already on the wire; recycle it without re-sending. */ trans->pos = 0; ble_log_lbm_recycle_trans(trans); } diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_test.c b/components/bt/common/ble_log/src/prph/ble_log_prph_test.c index 28f6982901c..04f649419c8 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_test.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_test.c @@ -9,7 +9,7 @@ /* INCLUDE */ #include "ble_log_prph_test.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "esp_timer.h" #include "freertos/queue.h" #include "freertos/semphr.h" diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c b/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c index 8af7851c3bd..2916c29637c 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c @@ -11,7 +11,8 @@ /* INCLUDE */ #include "ble_log_prph_uart_dma.h" #include "ble_log.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" +#include "ble_log_rt.h" #if BLE_LOG_PRPH_UART_DMA_REDIR @@ -24,10 +25,11 @@ /* MACRO */ #define BLE_LOG_UART_MAX_TRANSFER_SIZE (10240) #define BLE_LOG_UART_RX_BUF_SIZE (256) -/* ponytail: data burst disabled — UHCI enforces burst-size alignment (addr+len) on +/* ponytail: data burst disabled - UHCI enforces burst-size alignment (addr+len) on * uhci_transmit() once GDMA weighted arbitration is enabled, and UART log bandwidth * is baud-rate limited anyway, so burst buys nothing here */ #define BLE_LOG_UART_DMA_BURST_SIZE (1) +#define BLE_LOG_UART_FLUSH_TIMEOUT_TICKS pdMS_TO_TICKS(1000) #if BLE_LOG_PRPH_UART_DMA_REDIR #define BLE_LOG_UART_REDIR_BUF_SIZE (512) #define BLE_LOG_UART_REDIR_FLUSH_PERIOD_US (1000 * 1000) @@ -38,8 +40,9 @@ BLE_LOG_STATIC BLE_LOG_DRAM_ATTR bool prph_inited = false; BLE_LOG_STATIC uhci_controller_handle_t dev_handle = NULL; #if BLE_LOG_PRPH_UART_DMA_REDIR BLE_LOG_STATIC bool uart_driver_inited = false; -BLE_LOG_STATIC ble_log_lbm_t *redir_lbm = NULL; +BLE_LOG_STATIC ble_log_redir_t *redir_lbm = NULL; BLE_LOG_STATIC esp_timer_handle_t redir_flush_timer = NULL; +BLE_LOG_STATIC volatile uint32_t redir_writer_count = 0; #endif /* BLE_LOG_PRPH_UART_DMA_REDIR */ /* PRIVATE FUNCTION DECLARATION */ @@ -56,20 +59,20 @@ BLE_LOG_IRAM_ATTR BLE_LOG_STATIC bool uart_dma_tx_done_cb( /* Recycle transport */ ble_log_prph_trans_ctx_t *uart_trans_ctx = (ble_log_prph_trans_ctx_t *)( - (uint8_t *)edata->buffer - sizeof(ble_log_prph_trans_ctx_t) - ); + (uint8_t *)edata->buffer - sizeof(ble_log_prph_trans_ctx_t) + ); ble_log_prph_trans_t *trans = uart_trans_ctx->trans; - trans->pos = 0; ble_log_lbm_recycle_trans(trans); return true; } #if BLE_LOG_PRPH_UART_DMA_REDIR -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void esp_timer_cb_flush_log(void *arg) +BLE_LOG_STATIC void esp_timer_cb_flush_log(void *arg) { (void)arg; - if (!prph_inited) { + if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(prph_inited) || + !ble_log_lbm_is_enabled()) { return; } @@ -87,7 +90,7 @@ BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void esp_timer_cb_flush_log(void *arg) bool ble_log_prph_init(size_t trans_cnt) { /* Avoid double init */ - if (prph_inited) { + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(prph_inited)) { return true; } @@ -99,8 +102,8 @@ bool ble_log_prph_init(size_t trans_cnt) .stop_bits = UART_STOP_BITS_1, }; if ((uart_param_config(CONFIG_BLE_LOG_PRPH_UART_DMA_PORT, &uart_config) != ESP_OK) || - (uart_set_pin(CONFIG_BLE_LOG_PRPH_UART_DMA_PORT, - CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM, -1, -1, -1) != ESP_OK)) { + (uart_set_pin(CONFIG_BLE_LOG_PRPH_UART_DMA_PORT, + CONFIG_BLE_LOG_PRPH_UART_DMA_TX_IO_NUM, -1, -1, -1) != ESP_OK)) { goto exit; } @@ -117,19 +120,18 @@ bool ble_log_prph_init(size_t trans_cnt) .on_tx_trans_done = uart_dma_tx_done_cb, }; if ((uhci_new_controller(&uhci_config, &dev_handle) != ESP_OK) || - (uhci_register_event_callbacks(dev_handle, &uhci_cbs, NULL) != ESP_OK)) { + (uhci_register_event_callbacks(dev_handle, &uhci_cbs, NULL) != ESP_OK)) { goto exit; } -/* Redirection is required when utilizing UART port 0 */ + /* Redirection is required when utilizing UART port 0 */ #if BLE_LOG_PRPH_UART_DMA_REDIR - /* Initialize a dedicated LBM for redirection */ - redir_lbm = (ble_log_lbm_t *)BLE_LOG_MALLOC(sizeof(ble_log_lbm_t)); + /* Initialize a dedicated redirection manager (separate from the pool) */ + redir_lbm = (ble_log_redir_t *)BLE_LOG_MALLOC(sizeof(ble_log_redir_t)); if (!redir_lbm) { goto exit; } - BLE_LOG_MEMSET(redir_lbm, 0, sizeof(ble_log_lbm_t)); - redir_lbm->lock_type = BLE_LOG_LBM_LOCK_MUTEX; + BLE_LOG_MEMSET(redir_lbm, 0, sizeof(ble_log_redir_t)); /* Transport initialization */ for (int i = 0; i < BLE_LOG_TRANS_BUF_CNT; i++) { @@ -137,7 +139,10 @@ bool ble_log_prph_init(size_t trans_cnt) BLE_LOG_UART_REDIR_BUF_SIZE)) { goto exit; } - redir_lbm->trans[i]->owner = (void *)redir_lbm; + /* Redirection transports are not part of the global pool. */ + redir_lbm->trans[i]->id = BLE_LOG_TRANS_ID_NONE; + redir_lbm->trans[i]->owner_kind = BLE_LOG_TRANS_OWNER_REDIR; + redir_lbm->trans[i]->state = BLE_LOG_TRANS_STATE_FREE; } /* Mutex initialization */ @@ -146,11 +151,13 @@ bool ble_log_prph_init(size_t trans_cnt) goto exit; } - /* Initialize UART driver for redirection */ + /* Initialize UART driver for redirection. */ if (!uart_is_driver_installed(UART_NUM_0)) { - if (uart_driver_install(UART_NUM_0, BLE_LOG_UART_RX_BUF_SIZE, 0, 0, NULL, 0) == ESP_OK) { - uart_driver_inited = true; + if (uart_driver_install(UART_NUM_0, BLE_LOG_UART_RX_BUF_SIZE, + 0, 0, NULL, 0) != ESP_OK) { + goto exit; } + uart_driver_inited = true; } uart_vfs_dev_use_driver(UART_NUM_0); @@ -158,15 +165,19 @@ bool ble_log_prph_init(size_t trans_cnt) esp_timer_create_args_t timer_args = { .callback = esp_timer_cb_flush_log, .dispatch_method = ESP_TIMER_TASK, + .skip_unhandled_events = true, }; if (esp_timer_create(&timer_args, &redir_flush_timer) != ESP_OK) { goto exit; } #endif /* BLE_LOG_PRPH_UART_DMA_REDIR */ - prph_inited = true; + BLE_LOG_ATOMIC_STORE_RELEASE(prph_inited, true); #if BLE_LOG_PRPH_UART_DMA_REDIR - esp_timer_start_periodic(redir_flush_timer, BLE_LOG_UART_REDIR_FLUSH_PERIOD_US); + if (esp_timer_start_periodic(redir_flush_timer, + BLE_LOG_UART_REDIR_FLUSH_PERIOD_US) != ESP_OK) { + goto exit; + } #endif /* BLE_LOG_PRPH_UART_DMA_REDIR */ return true; @@ -178,27 +189,45 @@ exit: void ble_log_prph_deinit(void) { - prph_inited = false; + __atomic_store_n(&prph_inited, false, __ATOMIC_SEQ_CST); #if BLE_LOG_PRPH_UART_DMA_REDIR /* Release flush timer */ if (redir_flush_timer) { - esp_timer_stop(redir_flush_timer); + esp_timer_stop_blocking(redir_flush_timer, portMAX_DELAY); esp_timer_delete(redir_flush_timer); redir_flush_timer = NULL; } - /* Delete UART driver if it's installed by current module */ - if (uart_driver_inited) { - uart_driver_delete(UART_NUM_0); + while (__atomic_load_n(&redir_writer_count, __ATOMIC_SEQ_CST) > 0) { + vTaskDelay(1); } - /* Release redirection LBM */ + /* Flush redirection buffers before waiting for all submitted DMA. */ if (redir_lbm) { if (redir_lbm->mutex) { xSemaphoreTake(redir_lbm->mutex, portMAX_DELAY); ble_log_lbm_stream_flush(redir_lbm, BLE_LOG_SRC_REDIR); xSemaphoreGive(redir_lbm->mutex); + } + } +#endif /* BLE_LOG_PRPH_UART_DMA_REDIR */ + + if (dev_handle) { + uhci_wait_all_tx_transaction_done(dev_handle, portMAX_DELAY); + } + +#if BLE_LOG_PRPH_UART_DMA_REDIR + /* Restore the VFS before deleting a driver installed by this module. */ + if (uart_driver_inited) { + uart_vfs_dev_use_nonblocking(UART_NUM_0); + uart_driver_delete(UART_NUM_0); + uart_driver_inited = false; + } + + /* Release redirection LBM only after DMA callbacks have completed. */ + if (redir_lbm) { + if (redir_lbm->mutex) { vSemaphoreDelete(redir_lbm->mutex); } @@ -214,7 +243,6 @@ void ble_log_prph_deinit(void) #endif /* BLE_LOG_PRPH_UART_DMA_REDIR */ if (dev_handle) { - uhci_wait_all_tx_transaction_done(dev_handle, portMAX_DELAY); uhci_del_controller(dev_handle); dev_handle = NULL; } @@ -277,67 +305,102 @@ void ble_log_prph_trans_deinit(ble_log_prph_trans_t **trans) BLE_LOG_IRAM_ATTR void ble_log_prph_send_trans(ble_log_prph_trans_t *trans) { if (uhci_transmit(dev_handle, trans->buf, trans->pos) != ESP_OK) { + /* No tx_done will fire on failure: recycle here to avoid leaking. */ ble_log_lbm_recycle_trans(trans); } } /* Redirection is required when utilizing UART port 0 */ #if BLE_LOG_PRPH_UART_DMA_REDIR -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -void ble_log_redir_uart_tx_chars(const char *src, size_t len) +BLE_LOG_STATIC +bool ble_log_redir_uart_tx_chars(const char *src, size_t len) { - if (BLE_LOG_IN_ISR() || xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED) { - return; + __atomic_add_fetch(&redir_writer_count, 1, __ATOMIC_SEQ_CST); + if (!__atomic_load_n(&prph_inited, __ATOMIC_SEQ_CST) || + !ble_log_lbm_is_enabled()) { + __atomic_sub_fetch(&redir_writer_count, 1, __ATOMIC_SEQ_CST); + return false; } + + if (BLE_LOG_IN_ISR() || xTaskGetSchedulerState() == taskSCHEDULER_SUSPENDED) { + __atomic_sub_fetch(&redir_writer_count, 1, __ATOMIC_SEQ_CST); + return true; + } + uint32_t timestamp = (uint32_t)esp_timer_get_time(); xSemaphoreTake(redir_lbm->mutex, portMAX_DELAY); - ble_log_lbm_stream_write(redir_lbm, BLE_LOG_SRC_REDIR, - (const uint8_t *)src, len); + ble_log_lbm_stream_write(redir_lbm, BLE_LOG_SRC_REDIR, timestamp, + (const uint8_t *)src, len); xSemaphoreGive(redir_lbm->mutex); + __atomic_sub_fetch(&redir_writer_count, 1, __ATOMIC_SEQ_CST); + return true; } int __real_uart_tx_chars(uart_port_t uart_num, const char *buffer, uint32_t len); int __wrap_uart_tx_chars(uart_port_t uart_num, const char *buffer, uint32_t len) { - if (!prph_inited || (uart_num != UART_NUM_0)) { + if ((uart_num != UART_NUM_0) || + !ble_log_redir_uart_tx_chars(buffer, len)) { return __real_uart_tx_chars(uart_num, buffer, len); } - ble_log_redir_uart_tx_chars(buffer, len); return len; } int __real_uart_write_bytes(uart_port_t uart_num, const void *src, size_t size); int __wrap_uart_write_bytes(uart_port_t uart_num, const void *src, size_t size) { - if (!prph_inited || (uart_num != UART_NUM_0)) { + if ((uart_num != UART_NUM_0) || + !ble_log_redir_uart_tx_chars(src, size)) { return __real_uart_write_bytes(uart_num, src, size); } - ble_log_redir_uart_tx_chars(src, size); return size; } int __real_uart_write_bytes_with_break(uart_port_t uart_num, const void *src, size_t size, int brk_len); int __wrap_uart_write_bytes_with_break(uart_port_t uart_num, const void *src, size_t size, int brk_len) { - if (!prph_inited || (uart_num != UART_NUM_0)) { + if ((uart_num != UART_NUM_0) || + !ble_log_redir_uart_tx_chars(src, size)) { return __real_uart_write_bytes_with_break(uart_num, src, size, brk_len); - } else { - (void)brk_len; - return __wrap_uart_write_bytes(uart_num, src, size); } + return size; } -ble_log_lbm_t *ble_log_prph_get_redir_lbm(void) +BLE_LOG_IRAM_ATTR ble_log_redir_t *ble_log_prph_get_redir_lbm(void) { return redir_lbm; } #endif /* BLE_LOG_PRPH_UART_DMA_REDIR */ +#if BLE_LOG_PRPH_UART_DMA_REDIR +bool ble_log_prph_flush(void) +{ + while (__atomic_load_n(&redir_writer_count, __ATOMIC_SEQ_CST) > 0) { + vTaskDelay(1); + } + if (!redir_lbm) { + return true; + } + + xSemaphoreTake(redir_lbm->mutex, portMAX_DELAY); + ble_log_lbm_stream_flush(redir_lbm, BLE_LOG_SRC_REDIR); + xSemaphoreGive(redir_lbm->mutex); + (void)ble_log_rt_drain(); + + TickType_t start_tick = xTaskGetTickCount(); + while (BLE_LOG_ATOMIC_LOAD_ACQUIRE(redir_lbm->inflight) > 0) { + if ((xTaskGetTickCount() - start_tick) >= BLE_LOG_UART_FLUSH_TIMEOUT_TICKS) { + return false; + } + vTaskDelay(1); + } + return true; +} + void ble_log_prph_reset_util_counters(void) { -#if BLE_LOG_PRPH_UART_DMA_REDIR if (redir_lbm) { - __atomic_store_n(&redir_lbm->trans_inflight, 0, __ATOMIC_RELAXED); - __atomic_store_n(&redir_lbm->trans_inflight_peak, 0, __ATOMIC_RELAXED); + uint32_t inflight = BLE_LOG_ATOMIC_LOAD_RELAXED(redir_lbm->inflight); + BLE_LOG_ATOMIC_STORE_RELAXED(redir_lbm->inflight_peak, inflight); } -#endif } +#endif /* BLE_LOG_PRPH_UART_DMA_REDIR */ diff --git a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.h b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.h index 1f82cf73b33..4e6abd1a6ce 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.h +++ b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.h @@ -14,6 +14,7 @@ typedef struct { ble_log_src_t src; + uint8_t source_meta; uint32_t sn; const uint8_t *payload; size_t payload_len; diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.h b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.h index 1f82cf73b33..4e6abd1a6ce 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.h +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.h @@ -14,6 +14,7 @@ typedef struct { ble_log_src_t src; + uint8_t source_meta; uint32_t sn; const uint8_t *payload; size_t payload_len; diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.h b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.h index 1f82cf73b33..4e6abd1a6ce 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.h +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.h @@ -14,6 +14,7 @@ typedef struct { ble_log_src_t src; + uint8_t source_meta; uint32_t sn; const uint8_t *payload; size_t payload_len; diff --git a/components/bt/host/bluedroid/api/esp_bluedroid_hci.c b/components/bt/host/bluedroid/api/esp_bluedroid_hci.c index 486af894490..142328fefb1 100644 --- a/components/bt/host/bluedroid/api/esp_bluedroid_hci.c +++ b/components/bt/host/bluedroid/api/esp_bluedroid_hci.c @@ -99,9 +99,9 @@ void hci_host_send_packet(uint8_t *data, uint16_t len) #if CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED ble_log_spi_out_hci_write(BLE_LOG_SPI_OUT_SOURCE_HCI_DOWNSTREAM, data, len); #endif // CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED -#if CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED +#if CONFIG_BLE_LOG_HCI_LOG_ENABLED ble_log_write_hci(BLE_LOG_HCI_DOWNSTREAM, data, len); -#endif /* CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED */ +#endif /* CONFIG_BLE_LOG_HCI_LOG_ENABLED */ #if (BT_CONTROLLER_INCLUDED == TRUE) esp_vhci_host_send_packet(data, len); #else /* BT_CONTROLLER_INCLUDED == TRUE */ diff --git a/components/bt/host/bluedroid/hci/hci_hal_h4.c b/components/bt/host/bluedroid/hci/hci_hal_h4.c index d24f558b153..b1fe9473aed 100644 --- a/components/bt/host/bluedroid/hci/hci_hal_h4.c +++ b/components/bt/host/bluedroid/hci/hci_hal_h4.c @@ -619,9 +619,9 @@ static int host_recv_pkt_cb(uint8_t *data, uint16_t len) #if CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED ble_log_spi_out_hci_write(BLE_LOG_SPI_OUT_SOURCE_HCI_UPSTREAM, data, len); #endif // CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED -#if CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED +#if CONFIG_BLE_LOG_HCI_LOG_ENABLED ble_log_write_hci(BLE_LOG_HCI_UPSTREAM, data, len); -#endif /* CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED */ +#endif /* CONFIG_BLE_LOG_HCI_LOG_ENABLED */ //Target has packet to host, malloc new buffer for packet BT_HDR *pkt = NULL; #if (BLE_42_SCAN_EN == TRUE) diff --git a/components/bt/host/nimble/esp-hci/src/esp_nimble_hci.c b/components/bt/host/nimble/esp-hci/src/esp_nimble_hci.c index de0af66c5b8..62ac2dbc96b 100644 --- a/components/bt/host/nimble/esp-hci/src/esp_nimble_hci.c +++ b/components/bt/host/nimble/esp-hci/src/esp_nimble_hci.c @@ -87,9 +87,9 @@ void esp_vhci_host_send_packet_wrapper(uint8_t *data, uint16_t len) #if CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED ble_log_spi_out_hci_write(BLE_LOG_SPI_OUT_SOURCE_HCI_DOWNSTREAM, data, len); #endif // CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED -#if CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED +#if CONFIG_BLE_LOG_HCI_LOG_ENABLED ble_log_write_hci(BLE_LOG_HCI_DOWNSTREAM, data, len); -#endif /* CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED */ +#endif /* CONFIG_BLE_LOG_HCI_LOG_ENABLED */ esp_vhci_host_send_packet(data, len); } @@ -274,9 +274,9 @@ static int host_rcv_pkt(uint8_t *data, uint16_t len) #if CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED ble_log_spi_out_hci_write(BLE_LOG_SPI_OUT_SOURCE_HCI_UPSTREAM, data, len); #endif // CONFIG_BT_BLE_LOG_SPI_OUT_HCI_ENABLED -#if CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED +#if CONFIG_BLE_LOG_HCI_LOG_ENABLED ble_log_write_hci(BLE_LOG_HCI_UPSTREAM, data, len); -#endif /* CONFIG_BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED */ +#endif /* CONFIG_BLE_LOG_HCI_LOG_ENABLED */ bt_record_hci_data(data, len); From 66d4715a73853ee93f336bf0621b8d93c1a8a36d Mon Sep 17 00:00:00 2001 From: Zhou Xiao Date: Thu, 27 Aug 2026 12:01:36 +0800 Subject: [PATCH 03/13] feat(ble_log): write compressed logs directly into the pool Compression encoders claim pool storage through the new ble_log_claim/ble_log_commit interface and encode in place: the per-channel static payload buffers and their CAS busy management go away, and each record is one ENCODE frame. The timestamp and CAS helper macros (and their lbm call sites) are capitalized to the all-uppercase convention for function-like macros as part of the rewrite. Test coverage lands with the feature: - ble_log_test: golden v7 wire bytes, snapshot layout, pool exhaustion and reserve use, claim/commit stale-handle rejection, periodic snapshot busy/loss, FLUSH reset semantics; the stale-claim assertions are sabotage-verified to fail when the state or generation check is deleted; - ble_log_rt_test: runtime dispatch latency and batch drain, TS-valid snapshot cadence, deinit races across repeated init/deinit rounds (valid HCI macro inputs, corrected regression setup); - ble_log_perf_test: per-writer throughput and cycle measurement against the unified pool with the shared/reserve split and paced no-loss profiles. --- components/bt/common/ble_log/README.md | 681 ++++--------- .../log_compression/ble_log_compression.c | 194 ++-- .../include/log_compression/utils.h | 85 +- .../bt/common/ble_log/src/ble_log_lbm_v2.c | 36 +- .../src/internal_include/ble_log_util.h | 4 +- .../test_apps/ble_log_perf_test/README.md | 6 +- .../main/test_ble_log_main.c | 6 +- .../main/test_ble_log_perf.c | 113 ++- .../test_apps/ble_log_rt_test/README.md | 4 +- .../ble_log_rt_test/main/test_ble_log_main.c | 6 +- .../main/test_ble_log_runtime.c | 85 +- .../ble_log/test_apps/ble_log_test/README.md | 17 +- .../ble_log_test/main/test_ble_log_main.c | 6 +- .../ble_log_test/main/test_ble_log_rt.c | 914 +++++++++++++++++- 14 files changed, 1379 insertions(+), 778 deletions(-) diff --git a/components/bt/common/ble_log/README.md b/components/bt/common/ble_log/README.md index eb049112cde..bc9b25f2058 100644 --- a/components/bt/common/ble_log/README.md +++ b/components/bt/common/ble_log/README.md @@ -1,495 +1,216 @@ -# BLE Log Module +# BLE Log -A high-performance, modular Bluetooth logging system that provides real-time log capture and asynchronous transmission capabilities for the ESP-IDF Bluetooth stack. +BLE Log is an asynchronous binary logging transport for Bluetooth controller, +Host HCI, application, and compressed logs. -## Table of Contents +## Architecture -- [Overview](#overview) -- [Architecture Design](#architecture-design) -- [Features](#features) -- [Quick Start](#quick-start) -- [Configuration Options](#configuration-options) -- [API Reference](#api-reference) -- [Usage Examples](#usage-examples) -- [Performance & Memory Optimization](#performance--memory-optimization) -- [Troubleshooting](#troubleshooting) -- [Important Notes](#important-notes) - -## Overview - -The BLE Log module is an efficient logging system specifically designed for the ESP-IDF Bluetooth stack, supporting real-time log capture, multi-source log collection, and various transmission methods. This module has been refactored with a modular design, featuring high-concurrency processing capabilities and low-latency characteristics. - -### Main Components - -- **BLE Log Core** (`ble_log.c`): Module core responsible for initialization and coordination of sub-modules -- **Runtime Manager** (`ble_log_rt.c`): One-shot ESP Timer dispatch for log transmission scheduling -- **Log Buffer Manager** (`ble_log_lbm.c`): Log buffer management supporting multiple locking mechanisms -- **Peripheral Interface** (`ble_log_prph_*.c`): Peripheral interface abstraction layer supporting various transmission methods -- **Timestamp Sync** (`ble_log_ts.c`): Timestamp synchronization module -- **Utility** (`ble_log_util.c`): Common utility functions - -## Features - -### Core Functionality - -- **Multi-source Log Collection**: Supports multiple log sources including Link Layer, Host, HCI, UART redirection, etc. -- **High Concurrency Processing**: Uses atomic and spin lock mechanisms for multi-task concurrent writing -- **Real-time Transmission**: Asynchronous transmission through ESP Timer task dispatch -- **Data Integrity**: Checksum mechanism ensures data integrity (always enabled) -- **Multi-buffer Transport**: Each LBM manages multiple transport buffers (default 4) for improved throughput over the legacy ping-pong design -- **Cross-pool Buffer Fallback**: LBM acquire attempts all atomic LBMs before falling back to spinlock LBMs, improving buffer availability under contention - -### Advanced Features - -- **UART Redirection**: When using UART DMA on PORT 0, UART output (including `esp_rom_printf`) is transparently redirected through the async log pipeline -- **Timestamp Synchronization**: Supports timestamp synchronization with external devices (optional) -- **Enhanced Statistics**: Detailed logging statistics including written/lost frame and byte counts (always enabled) -- **Buffer Utilization Reporting**: Per-LBM buffer utilization and inflight peak tracking for diagnostics -- **Link Layer Integration**: Deep integration with ESP-IDF Bluetooth Link Layer -- **Multiple Transmission Methods**: Supports SPI Master DMA, UART DMA, and Dummy transmission - -### Performance Features - -- **IRAM Optimization**: Critical path code runs in IRAM ensuring low latency -- **Lock-free Design**: Most operations use atomic operations reducing lock contention -- **Buffer Reuse**: Intelligent buffer management reduces memory allocation overhead - -## Quick Start - -### 1. Enable Module - -Enable the BLE Log module in `menuconfig`: - -``` -Component config → Bluetooth → Enable BT Log Async Output (Dev Only) +```mermaid +flowchart TD + A[Public and LL writers] --> P[Unified bitmap pool] + C[Compression encoder] --> P + S[Internal snapshot] --> I[Fixed internal transport] + R[UART0 redirection] --> D[Redirection transports] + P --> Q[ESP Timer runtime queue] + I --> Q + D --> Q + Q --> X[Peripheral DMA] ``` -### 2. Basic Configuration +The shared pool contains `CONFIG_BLE_LOG_POOL_TRANS_CNT` transports. The last +`CONFIG_BLE_LOG_POOL_NON_YIELD_RESERVE_CNT` transports are reserved for ISR and +other contexts that cannot yield. Ordinary public writes are non-blocking. +Only the ordinary controller LL task path waits for a shared transport. + +The Internal Snapshot and UART0 redirection transports are not members of the +bitmap pool. + +## Runtime dispatch + +Sealed transports enter one bounded queue. The first submission anchors a +one-shot ESP Timer deadline at 1 ms; later submissions do not move it. Each +callback drains the queue depth captured at entry and schedules another fixed +defer only for arrivals left behind, so it does not continuously monopolize +the shared ESP Timer task. + +## Version 7 frame + +All multi-byte fields use the target's little-endian representation. + +```text +offset size field +0 2 payload length +2 4 frame_meta +6 N payload +6 + N 4 checksum +``` + +`frame_meta` is: + +```text +bits 0..6 base source +bit 7 NON_YIELD +bits 8..31 sequence number, low 24 bits +``` + +The checksum is `ble_log_fast_checksum()` over the six-byte header and exactly +`payload length` bytes. It excludes the checksum field and peripheral-only DMA +padding. + +Core frames begin their payload with: + +```text +[4-byte low32 esp_timer_get_time() microseconds][source payload] +``` + +The timestamp is captured at API entry before any pool or UART0 redirection +mutex wait. It wraps approximately every 71.6 minutes and must be unwrapped +modulo 2^32 by the receiver. UART0 redirection retains its extension-specific +stream aggregation while using the same ESP Timer timestamp semantics. + +### Sources + +The public `ble_log_src_t` ABI is frozen and its values are the on-wire +source IDs of protocol v7 frames — the frame source byte carries the enum +value directly, so existing decoders keep working: + +```text +0 INTERNAL +1 CUSTOM +2 LL_TASK +3 LL_HCI +4 LL_ISR +5 HOST +6 HCI +7 ENCODE +8 REDIR extension +``` + +All non-INTERNAL sources share one 24-bit Global SN: it is consumed at API +entry, so it totally orders log attempts — including equal-timestamp records +from different sources — and every lost or rejected attempt leaves a gap +in the sequence. Internal Snapshot frames carry their own separate +sequence; a gap there counts skipped snapshots. Neither sequence is reset. +Actual ISR and critical-section records carry `NON_YIELD` in source bit 7. + +Controller-side HCI records are not emitted by BLE Log, and the controller no +longer maintains its own internal LL HCI log. Host-side Bluedroid and NimBLE +HCI capture (`CONFIG_BLE_LOG_HCI_LOG_ENABLED`) is the single HCI logging +switch and the authoritative `HCI` stream for both Host and Controller +traffic. Its direction bit continues to use HCI payload byte 0 bit 7; this is +independent of the source metadata bit. + +## Internal Snapshot + +All BLE Log-owned Internal information is emitted as one fixed-layout frame +from one dedicated 148-byte transport. Its logical frame length is 148 bytes. + +The snapshot contains: + +- reason flags (`INIT`, `PERIODIC`, `FLUSH`, `TS_VALID`); +- the complete 58-byte build, library, chip, and protocol version block; +- the LC clock, ESP Timer, and FreeRTOS tick samples captured together with + the GPIO sync level; +- pool count, reserve count, current inflight count, and inflight peak; +- compact statistics for `CUSTOM` through `ENCODE` (one slot per public + source in that range, `HOST` included). + +A skipped periodic snapshot burns one snapshot SN instead: the gap in the +snapshot sequence is the loss signal, and it is visible directly in the +frame header without a dedicated payload field. + +Each source statistic contains only: ```c -#include "ble_log.h" +uint32_t written_frame_cnt; +uint32_t lost_frame_cnt; +``` -void app_main() { - // Initialize BLE Log module - if (!ble_log_init()) { - ESP_LOGE(TAG, "Failed to initialize BLE Log"); - return; - } - - // Write log data - uint8_t data[] = {0x01, 0x02, 0x03, 0x04}; - ble_log_write_hex(BLE_LOG_SRC_CUSTOM, data, sizeof(data)); - - // End session and flush buffers - ble_log_flush(); - - // Cleanup resources - ble_log_deinit(); +Successful counts are incremented only after a complete frame is committed to +a transport; they do not imply confirmed physical TX. Core counts and the pool +peak restart after a completed FLUSH; the Global SN and the snapshot +sequence remain continuous. A busy periodic Internal +transport is never overwritten; that snapshot is skipped and counted. Required +INIT and FLUSH snapshots use a bounded task-context wait. + +The snapshot is not an Anchor: it does not seal the shared pool and does not +define a Log Segment boundary. + +## Public API + +```c +bool ble_log_init(void); +void ble_log_deinit(void); +bool ble_log_enable(bool enable); +void ble_log_flush(void); +bool ble_log_write_hex(ble_log_src_t source, const uint8_t *data, size_t len); +uint8_t *ble_log_claim(ble_log_src_t source, size_t maximum, uint32_t *handle); +void ble_log_commit(uint32_t handle, size_t actual_len); +void ble_log_write_hex_ll(uint32_t len, const uint8_t *data, + uint32_t append_len, const uint8_t *append, + uint32_t flags); +``` + +`ble_log_flush()` is an ordinary FreeRTOS task API. Do not call it from an ISR, +critical section, or the shared ESP Timer task. + +A public or LL record that cannot fit completely in one pool transport is +rejected and counted as lost; BLE Log never emits a truncated record. + +## Compression direct write + +Compression encoders write directly into shared-pool storage: + +```c +uint32_t handle; +uint8_t *payload = ble_log_claim(BLE_LOG_SRC_ENCODE, maximum, &handle); +if (payload) { + size_t encoded = encode(payload, maximum); + ble_log_commit(handle, encoded); } ``` -### 3. Link Layer Integration +`ble_log_claim()` reserves the hidden frame header, ESP Timer timestamp, and +checksum. Every successful claim must be committed exactly once before +`ble_log_deinit()`; `ble_log_commit(handle, 0)` cancels it. Handles include a +transport generation so a stale handle cannot commit a later claim in the same +lifecycle. -When `CONFIG_BLE_LOG_LL_ENABLED` is enabled, Link Layer logs are automatically integrated: +Mesh, ISO, Bluedroid, and NimBLE compression no longer allocate three static +payload buffers per channel. Each logical compression source uses a non-blocking +trylock so its task-switch state follows commit order; a contending record is +canceled and counted as lost. -```c -// Link Layer logs will automatically call this function -void ble_log_write_hex_ll(uint32_t len, const uint8_t *addr, - uint32_t len_append, const uint8_t *addr_append, - uint32_t flag); +## Configuration + +| Option | Default | Meaning | +| --- | ---: | --- | +| `CONFIG_BLE_LOG_ENABLED` | n | Enable BLE Log | +| `CONFIG_BLE_LOG_POOL_TRANS_CNT` | 8 | Unified pool transport count, range 2..32 | +| `CONFIG_BLE_LOG_POOL_NON_YIELD_RESERVE_CNT` | 1 | ISR/critical reserve count | +| `CONFIG_BLE_LOG_POOL_TRANS_SIZE` | 640 | Bytes per shared transport; SPI builds require a multiple of four | +| `CONFIG_BLE_LOG_LL_ENABLED` | target dependent | Controller LL logging | +| `CONFIG_BLE_LOG_HCI_LOG_ENABLED` | target dependent | Host-side HCI capture | +| `CONFIG_BLE_LOG_TS_ENABLED` | n | GPIO/LC timestamp synchronization | + +Old multi-LBM sizing options remain hidden only so existing sdkconfig files can +be parsed; they no longer control allocation. + +## Validation apps + +```bash +. ./export.sh +cd components/bt/common/ble_log/test_apps/ble_log_test +idf.py build + +cd ../ble_log_rt_test +idf.py build + +cd ../ble_log_perf_test +idf.py build ``` -## Configuration Options - -### Basic Configuration - -| Configuration | Default | Description | -|---------------|---------|-------------| -| `CONFIG_BLE_LOG_ENABLED` | n | Enable BT Log Async Output | -| `CONFIG_BLE_LOG_LBM_TRANS_BUF_SIZE` | 2048 | Total buffer memory per common LBM (bytes). Divided equally among `BLE_LOG_TRANS_BUF_CNT` (4) internal transport buffers. | -| `CONFIG_BLE_LOG_LBM_ATOMIC_LOCK_TASK_CNT` | 2 | Number of atomic lock LBMs for task context | -| `CONFIG_BLE_LOG_LBM_ATOMIC_LOCK_ISR_CNT` | 1 | Number of atomic lock LBMs for ISR context | - -### Link Layer Configuration - -| Configuration | Default | Description | -|---------------|---------|-------------| -| `CONFIG_BLE_LOG_LL_ENABLED` | y | Enable Link Layer logging | -| `CONFIG_BLE_LOG_LBM_LL_TRANS_BUF_SIZE` | 2048 | Total buffer memory per Link Layer LBM (bytes). Divided equally among `BLE_LOG_TRANS_BUF_CNT` (4) internal transport buffers. | - -### Other Features - -| Configuration | Default | Description | -|---------------|---------|-------------| -| `CONFIG_BLE_LOG_TS_ENABLED` | n | Enable timestamp synchronization | -| `CONFIG_BLE_LOG_HOST_HCI_LOG_ENABLED` | n | Enable BLE Host side HCI logging | - -> **Note**: Payload checksum and enhanced statistics are now always enabled and no longer have separate Kconfig options. - -### Transport Method Configuration - -| Transport | Configuration | Description | -|-----------|---------------|-------------| -| Dummy | `CONFIG_BLE_LOG_PRPH_DUMMY` | Debug dummy transport (default unless `SOC_UHCI_SUPPORTED`) | -| SPI Master DMA | `CONFIG_BLE_LOG_PRPH_SPI_MASTER_DMA` | SPI DMA transport | -| UART DMA | `CONFIG_BLE_LOG_PRPH_UART_DMA` | UART DMA transport (default when `SOC_UHCI_SUPPORTED`). Default baud rate: 3000000. | - -### Deprecated / Removed - -| Module | Status | Notes | -|--------|--------|-------| -| Legacy SPI Log Output | Deprecated | Moved to `deprecated/` directory. Use BT Log Async Output instead. A separate Kconfig menu "Legacy SPI Log Output" is available for backward compatibility. | -| UHCI Out | Removed | The standalone UHCI Out module (`ble_log_uhci_out.c`) has been removed. UART DMA transport under the main BLE Log peripheral interface replaces it. | - -## API Reference - -### Core API - -#### `bool ble_log_init(void)` - -Initialize the BLE Log module. - -**Return Value**: -- `true`: Initialization successful -- `false`: Initialization failed - -**Note**: Must be called before using any other APIs. - -#### `void ble_log_deinit(void)` - -Cleanup the BLE Log module and release all resources. - -**Note**: -- All pending logs will be lost after calling this function -- Peripheral interface will be cleaned up first to avoid DMA transmission issues during memory release -- This function may run concurrently with BLE Log write APIs. New writes are - rejected after shutdown begins, and deinit waits for in-progress writers - before releasing their buffers. - -#### `bool ble_log_write_hex(ble_log_src_t src_code, const uint8_t *addr, size_t len)` - -Write hexadecimal log data. - -**Parameters**: -- `src_code`: Log source code -- `addr`: Data pointer -- `len`: Data length - -**Return Value**: -- `true`: Write successful -- `false`: Write failed (module not initialized or insufficient buffer) - -#### `void ble_log_flush(void)` - -End the current logging session and flush pending logs immediately. - -This API temporarily suspends ordinary log writes, waits for in-progress -writers to exit, emits a final statistics internal frame, flushes pending -transport buffers, resets statistics, and then restores the enable state that -was in effect before the call. - -**Note**: This operation is blocking and must run in an ordinary caller-owned -FreeRTOS task. Do not call it from an ISR or a system callback such as an ESP -Timer callback. If BLE Log was enabled before the call, it remains enabled -after the flush completes. - -#### `void ble_log_dump_to_console(void)` - -Output all buffer contents to console in hexadecimal format for debugging. - -### Log Source Types - -```c -typedef enum { - BLE_LOG_SRC_INTERNAL = 0, // Internal system logs - BLE_LOG_SRC_CUSTOM, // User-defined logs - BLE_LOG_SRC_LL_TASK, // Link Layer task logs - BLE_LOG_SRC_LL_HCI, // Link Layer HCI logs - BLE_LOG_SRC_LL_ISR, // Link Layer interrupt logs - BLE_LOG_SRC_HOST, // Host layer logs - BLE_LOG_SRC_HCI, // HCI layer logs - BLE_LOG_SRC_ENCODE, // Encoding layer logs - BLE_LOG_SRC_REDIR, // UART redirection (PORT 0 only) - BLE_LOG_SRC_MAX, -} ble_log_src_t; -``` - -### HCI Log Macro - -```c -#define ble_log_write_hci(direction, data, len) -``` - -Writes an HCI packet with direction encoding. `direction` is `BLE_LOG_HCI_DOWNSTREAM` (0) or `BLE_LOG_HCI_UPSTREAM` (1). Direction is encoded in the MSB of the first byte (HCI type). - -### Link Layer API (Conditional Compilation) - -#### `void ble_log_write_hex_ll(uint32_t len, const uint8_t *addr, uint32_t len_append, const uint8_t *addr_append, uint32_t flag)` - -Link Layer dedicated log writing interface. - -**Parameters**: -- `len`: Main data length -- `addr`: Main data pointer -- `len_append`: Append data length -- `addr_append`: Append data pointer -- `flag`: Log flag bits - -**Flag Definitions**: -```c -enum { - BLE_LOG_LL_FLAG_CONTINUE = 0, - BLE_LOG_LL_FLAG_END, - BLE_LOG_LL_FLAG_TASK, - BLE_LOG_LL_FLAG_ISR, - BLE_LOG_LL_FLAG_HCI, - BLE_LOG_LL_FLAG_RAW, - BLE_LOG_LL_FLAG_OMDATA, - BLE_LOG_LL_FLAG_HCI_UPSTREAM, -}; -``` - -### Timestamp Synchronization API (Conditional Compilation) - -#### `bool ble_log_sync_enable(bool enable)` - -Enable or disable timestamp synchronization functionality. - -**Parameters**: -- `enable`: true to enable, false to disable - -**Return Value**: -- `true`: Operation successful -- `false`: Operation failed (module not initialized) - -## Usage Examples - -### Example 1: Basic Logging - -```c -#include "ble_log.h" - -void example_basic_logging() { - // Initialize - if (!ble_log_init()) { - printf("BLE Log init failed\n"); - return; - } - - // Log some example data - uint8_t hci_cmd[] = {0x01, 0x03, 0x0C, 0x00}; // HCI Reset Command - ble_log_write_hex(BLE_LOG_SRC_HCI, hci_cmd, sizeof(hci_cmd)); - - uint8_t host_data[] = {0x02, 0x00, 0x20, 0x0B, 0x00, 0x07, 0x00, 0x04, 0x00, 0x10, 0x01, 0x00, 0xFF, 0xFF, 0x00, 0x28}; - ble_log_write_hex(BLE_LOG_SRC_HOST, host_data, sizeof(host_data)); - - // End session and flush buffers - ble_log_flush(); - - // Cleanup - ble_log_deinit(); -} -``` - -### Example 2: ISR Context Logging - -```c -void IRAM_ATTR some_isr_handler() { - uint8_t isr_data[] = {0xDE, 0xAD, 0xBE, 0xEF}; - - // Safe to write logs in ISR context - ble_log_write_hex(BLE_LOG_SRC_LL_ISR, isr_data, sizeof(isr_data)); -} -``` - -### Example 3: Logging with Timestamp Synchronization - -```c -void example_with_timestamp_sync() { - if (!ble_log_init()) { - return; - } - - #if CONFIG_BLE_LOG_TS_ENABLED - // Enable timestamp synchronization - ble_log_sync_enable(true); - #endif - - // Log data... - uint8_t data[] = {0x01, 0x02, 0x03}; - ble_log_write_hex(BLE_LOG_SRC_CUSTOM, data, sizeof(data)); - - // Timestamp information will be automatically included in logs - - ble_log_deinit(); -} -``` - -### Example 4: Performance Testing - -```c -void example_performance_test() { - if (!ble_log_init()) { - return; - } - - uint8_t test_data[100]; - for (int i = 0; i < 100; i++) { - test_data[i] = i; - } - - uint32_t start_time = esp_timer_get_time(); - - // Send 1000 logs - for (int i = 0; i < 1000; i++) { - ble_log_write_hex(BLE_LOG_SRC_CUSTOM, test_data, sizeof(test_data)); - } - - // End session and flush buffers - ble_log_flush(); - uint32_t end_time = esp_timer_get_time(); - - printf("Time to write 1000 logs: %lu us\n", end_time - start_time); - - ble_log_deinit(); -} -``` - -## Performance & Memory Optimization - -### Memory Usage Estimation - -Each LBM's total buffer memory is configured directly via Kconfig. The configured value is divided equally among `BLE_LOG_TRANS_BUF_CNT` (currently 4) internal transport buffers. - -Memory usage under default configuration: - -``` -Common Pool: - LBM count = Atomic Task (2) + Atomic ISR (1) + Spinlock (2) = 5 - Total = 5 × BLE_LOG_LBM_TRANS_BUF_SIZE = 5 × 2048 = 10240 bytes - -Link Layer Pool (when CONFIG_BLE_LOG_LL_ENABLED): - LBM count = 2 (LL task + LL HCI) - Total = 2 × BLE_LOG_LBM_LL_TRANS_BUF_SIZE = 2 × 2048 = 4096 bytes - -Statistics (always enabled): - Total = BLE_LOG_SRC_MAX × sizeof(ble_log_stat_mgr_t) = 9 × 20 = 180 bytes - -UART Redirect (when UART DMA on PORT 0): - Additional BLE_LOG_TRANS_BUF_CNT (4) transport buffers -``` - -### Performance Optimization Recommendations - -1. **Adjust LBM Count**: Adjust atomic lock LBM count based on concurrency requirements -2. **Buffer Size**: Adjust total buffer memory per LBM based on log volume; must be a multiple of `BLE_LOG_TRANS_BUF_CNT` (4) -3. **Transport Method**: Choose optimal transport method based on hardware (UART DMA is default on supported SoCs) - -### Real-time Considerations - -- Critical code paths are marked with `BLE_LOG_IRAM_ATTR` and run in IRAM -- Atomic operations avoid lock contention -- Multi-buffer transport ensures continuous writing even when some buffers are in-flight - -## Troubleshooting - -### Common Issues - -#### 1. Initialization Failure - -**Symptoms**: `ble_log_init()` returns `false` - -**Possible Causes**: -- Insufficient memory -- Peripheral configuration error -- Duplicate initialization - -**Solutions**: -```c -// Check available memory -printf("Free heap: %d bytes\n", esp_get_free_heap_size()); - -// Ensure initialization only happens once -static bool initialized = false; -if (!initialized) { - initialized = ble_log_init(); -} -``` - -#### 2. Log Loss - -**Symptoms**: Some logs don't appear in output - -**Possible Causes**: -- Buffer overflow -- Transmission speed can't keep up with write speed -- Module not properly initialized - -**Solutions**: -```c -// Enhanced statistics are always enabled — check written/lost frame -// and byte counts in the log stream output - -// Increase total buffer memory per LBM -// CONFIG_BLE_LOG_LBM_TRANS_BUF_SIZE=4096 - -// Increase atomic lock LBM count -// CONFIG_BLE_LOG_LBM_ATOMIC_LOCK_TASK_CNT=4 -``` - -#### 3. Performance Issues - -**Symptoms**: System response becomes slow - -**Possible Causes**: -- Transmission bottleneck -- Lock contention - -**Solutions**: -```c -// Use faster transmission method -// CONFIG_BLE_LOG_PRPH_UART_DMA=y (default on SOC_UHCI_SUPPORTED targets) - -// Increase baud rate (default is now 3000000) -// CONFIG_BLE_LOG_PRPH_UART_DMA_BAUD_RATE=3000000 -``` - -### Debugging Techniques - -#### 1. Use Dummy Transport for Debugging - -```c -// Select Dummy transport in menuconfig -// Then use dump function to view buffer contents -ble_log_dump_to_console(); -``` - -#### 2. Check Enhanced Statistics - -```c -// Enhanced statistics are always enabled -// Written/lost frame and byte counts are automatically output to logs -``` - -#### 3. Monitor Memory Usage - -```c -void monitor_memory() { - printf("Free heap before init: %d\n", esp_get_free_heap_size()); - ble_log_init(); - printf("Free heap after init: %d\n", esp_get_free_heap_size()); -} -``` - -## Important Notes - -### Buffer Size Constraints - -- `CONFIG_BLE_LOG_LBM_TRANS_BUF_SIZE` and `CONFIG_BLE_LOG_LBM_LL_TRANS_BUF_SIZE` must be multiples of `BLE_LOG_TRANS_BUF_CNT` (currently 4) -- The per-buffer size (total ÷ 4) must be at least large enough to hold one frame overhead (`BLE_LOG_FRAME_OVERHEAD`) -- `BLE_LOG_TRANS_BUF_CNT` must be a power of 2 - -### Migration from Legacy Modules - -- **UHCI Out**: The standalone `ble_log_uhci_out` module has been removed. Use the UART DMA peripheral transport (`CONFIG_BLE_LOG_PRPH_UART_DMA`) instead. -- **SPI Out**: The legacy SPI log output has been moved to `deprecated/`. A separate Kconfig menu "Legacy SPI Log Output (Deprecated)" is available for backward compatibility, but new projects should use BT Log Async Output with the SPI Master DMA peripheral transport. +`ble_log_test` validates golden v7 bytes, the consolidated Internal Snapshot, +source/HCI metadata, pool exhaustion and reserve use, snapshot busy loss, +stale claims, and enable/disable/deinit races. `ble_log_rt_test` covers batched +dispatch, timer behavior, inflight statistics, and repeated deinit races. diff --git a/components/bt/common/ble_log/extension/log_compression/ble_log_compression.c b/components/bt/common/ble_log/extension/log_compression/ble_log_compression.c index 908f80289c1..8eaef8661b0 100644 --- a/components/bt/common/ble_log/extension/log_compression/ble_log_compression.c +++ b/components/bt/common/ble_log/extension/log_compression/ble_log_compression.c @@ -11,12 +11,27 @@ #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "sdkconfig.h" +#include "ble_log_lbm_v2.h" #include "ble_log_util.h" #include "log_compression/utils.h" #if CONFIG_BLE_COMPRESSED_LOG_ENABLE -#define BLE_CP_DROP_LOG_PERIOD 256U +#if CONFIG_BLE_MESH_COMPRESSED_LOG_ENABLE +_Static_assert(CONFIG_BLE_MESH_COMPRESSED_LOG_BUFFER_LEN <= + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t), + "Mesh compressed log record exceeds one BLE Log transport"); +#endif +#if CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE +_Static_assert(CONFIG_BLE_ISO_COMPRESSED_LOG_BUFFER_LEN <= + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t), + "ISO compressed log record exceeds one BLE Log transport"); +#endif +#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE +_Static_assert(CONFIG_BLE_HOST_COMPRESSED_LOG_BUFFER_LEN <= + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t), + "Host compressed log record exceeds one BLE Log transport"); +#endif #define BLE_CP_TRY_PUSH(expr) do { \ if ((expr) != 0) { \ @@ -24,61 +39,39 @@ } \ } while (0) -#define BUF_NAME(name, idx) name##_buffer##idx -#define BUF_MGMT_NAME(name) name##_log_buffer_mgmt - -#define DECL_BUF_OP(name, len, idx) \ - static uint8_t BUF_NAME(name, idx)[len]; - -#define INIT_MAP_OP(name, _, buffer_idx) \ - {.busy = 0, \ - .idx = 0, \ - .buffer = BUF_NAME(name, buffer_idx), \ - .len = sizeof(BUF_NAME(name, buffer_idx))}, - -#define DECLARE_BUFFERS(NAME, BUF_LEN, BUF_CNT) \ - FOR_EACH_IDX(DECL_BUF_OP, NAME, BUF_LEN, GEN_INDEX(BUF_CNT)); - -#define INIT_BUFFER_MGMT(NAME, BUF_CNT) \ - ble_cp_log_buffer_mgmt_t BUF_MGMT_NAME(NAME)[BUF_CNT] = { \ - FOR_EACH_IDX(INIT_MAP_OP, NAME, 0, GEN_INDEX(BUF_CNT)) \ - }; - #if CONFIG_BLE_MESH_COMPRESSED_LOG_ENABLE -DECLARE_BUFFERS(mesh, CONFIG_BLE_MESH_COMPRESSED_LOG_BUFFER_LEN, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); -INIT_BUFFER_MGMT(mesh, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); char * mesh_last_task_handle = NULL; +static ble_log_atomic_lock_t mesh_source_lock; #endif #if CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE -/* The BLE_ISO buffer is shared by every source compiled into the unified - * ISO channel: esp_ble_iso, esp_ble_audio (and future ISO consumers, e.g. - * HID-over-ISO), as well as the AUDIO_LIB runtime callback (prebuilt - * libble_audio.a, source code 5) — they all funnel here. */ -DECLARE_BUFFERS(iso, CONFIG_BLE_ISO_COMPRESSED_LOG_BUFFER_LEN, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); -INIT_BUFFER_MGMT(iso, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); char * iso_last_task_handle = NULL; +static ble_log_atomic_lock_t iso_source_lock; #endif #if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE && CONFIG_BT_BLUEDROID_ENABLED -DECLARE_BUFFERS(host, CONFIG_BLE_HOST_COMPRESSED_LOG_BUFFER_LEN, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); -INIT_BUFFER_MGMT(host, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); char * host_last_task_handle = NULL; +static ble_log_atomic_lock_t host_source_lock; #endif #if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE && CONFIG_BT_NIMBLE_ENABLED -DECLARE_BUFFERS(nimble, CONFIG_BLE_HOST_COMPRESSED_LOG_BUFFER_LEN, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); -INIT_BUFFER_MGMT(nimble, LOG_CP_MAX_LOG_BUFFER_USED_SIMU); char * nimble_last_task_handle = NULL; +static ble_log_atomic_lock_t nimble_source_lock; +#endif + +#if CONFIG_BLE_LOG_PRPH_TEST +extern void ble_log_test_compression_after_lock_hook(uint8_t source) +__attribute__((weak)); #endif /* The maximum number of supported parameters is 64 */ #define LOG_HEADER(log_type, info) ((log_type << 6) | (info & 0x3f)) -int ble_compressed_log_cb_get(uint8_t source, ble_cp_log_buffer_mgmt_t **mgmt) +int ble_compressed_log_cb_get(uint8_t source, ble_cp_log_buffer_mgmt_t *mgmt) { - ble_cp_log_buffer_mgmt_t *buffer_mgmt = NULL; char ** last_handle = NULL; + ble_log_atomic_lock_t *source_lock = NULL; + uint16_t claim_len = 0; char * cur_handle = pcTaskGetName(NULL); switch (source) @@ -86,25 +79,28 @@ int ble_compressed_log_cb_get(uint8_t source, ble_cp_log_buffer_mgmt_t **mgmt) #if CONFIG_BLE_MESH_COMPRESSED_LOG_ENABLE case BLE_COMPRESSED_LOG_OUT_SOURCE_MESH: case BLE_COMPRESSED_LOG_OUT_SOURCE_MESH_LIB: - buffer_mgmt = BUF_MGMT_NAME(mesh); last_handle = &mesh_last_task_handle; + source_lock = &mesh_source_lock; + claim_len = CONFIG_BLE_MESH_COMPRESSED_LOG_BUFFER_LEN; break; #endif #if CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE case BLE_COMPRESSED_LOG_OUT_SOURCE_ISO: case BLE_COMPRESSED_LOG_OUT_SOURCE_AUDIO_LIB: - buffer_mgmt = BUF_MGMT_NAME(iso); last_handle = &iso_last_task_handle; + source_lock = &iso_source_lock; + claim_len = CONFIG_BLE_ISO_COMPRESSED_LOG_BUFFER_LEN; break; #endif #if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE && (CONFIG_BT_BLUEDROID_ENABLED || CONFIG_BT_NIMBLE_ENABLED) case BLE_COMPRESSED_LOG_OUT_SOURCE_HOST: + claim_len = CONFIG_BLE_HOST_COMPRESSED_LOG_BUFFER_LEN; #if CONFIG_BT_BLUEDROID_ENABLED - buffer_mgmt = BUF_MGMT_NAME(host); last_handle = &host_last_task_handle; + source_lock = &host_source_lock; #elif CONFIG_BT_NIMBLE_ENABLED - buffer_mgmt = BUF_MGMT_NAME(nimble); last_handle = &nimble_last_task_handle; + source_lock = &nimble_source_lock; #endif break; #endif @@ -113,37 +109,59 @@ int ble_compressed_log_cb_get(uint8_t source, ble_cp_log_buffer_mgmt_t **mgmt) return -1; } - for (int i = 0; i < LOG_CP_MAX_LOG_BUFFER_USED_SIMU; i++) { - if (ble_log_cas_acquire(&(buffer_mgmt[i].busy))) { - *mgmt = &buffer_mgmt[i]; - if (ble_log_cp_push_u8(*mgmt, source) != 0) { - (*mgmt)->idx = 0; - ble_log_cas_release(&((*mgmt)->busy)); - return -1; - } - if (*last_handle == NULL || - *last_handle != cur_handle) { - if (ble_log_cp_push_u8(*mgmt, LOG_HEADER(LOG_TYPE_INFO, LOG_TYPE_INFO_TASK_SWITCH)) != 0) { - (*mgmt)->idx = 0; - ble_log_cas_release(&((*mgmt)->busy)); - return -1; - } - *last_handle = cur_handle; - } - return 0; - } + mgmt->len = claim_len; + mgmt->idx = 0; + mgmt->source_lock = source_lock; + mgmt->last_task_handle = last_handle; + mgmt->current_task_handle = cur_handle; + mgmt->task_switched = false; + mgmt->buffer = ble_log_claim(BLE_LOG_SRC_ENCODE, claim_len, &mgmt->handle); + if (!mgmt->buffer) { + return -1; } + if (!BLE_LOG_CAS_ACQUIRE(source_lock)) { + ble_log_commit(mgmt->handle, 0); + return -1; + } +#if CONFIG_BLE_LOG_PRPH_TEST + if (ble_log_test_compression_after_lock_hook) { + ble_log_test_compression_after_lock_hook(source); + } +#endif - return -1; + if (ble_log_cp_push_u8(mgmt, source) != 0) { + ble_log_commit(mgmt->handle, 0); + BLE_LOG_CAS_RELEASE(source_lock); + return -1; + } + char *previous_handle = __atomic_load_n(last_handle, __ATOMIC_RELAXED); + if (previous_handle == NULL || previous_handle != cur_handle) { + if (ble_log_cp_push_u8( + mgmt, LOG_HEADER(LOG_TYPE_INFO, LOG_TYPE_INFO_TASK_SWITCH)) != 0) { + ble_log_commit(mgmt->handle, 0); + BLE_LOG_CAS_RELEASE(source_lock); + return -1; + } + mgmt->task_switched = true; + } + return 0; } -static inline int ble_compressed_log_buffer_free(ble_cp_log_buffer_mgmt_t *mgmt) +static inline int ble_compressed_log_commit(ble_cp_log_buffer_mgmt_t *mgmt) { -#if BLE_LOG_CP_CONTENT_CHECK_ENABLE - memset(mgmt->buffer, BLE_LOG_CP_CONTENT_CHECK_VAL, mgmt->idx); -#endif - mgmt->idx = 0; - ble_log_cas_release(&mgmt->busy); + ble_log_commit(mgmt->handle, mgmt->idx); + if (mgmt->task_switched) { + __atomic_store_n(mgmt->last_task_handle, mgmt->current_task_handle, + __ATOMIC_RELAXED); + } + BLE_LOG_CAS_RELEASE(mgmt->source_lock); + return 0; +} + +static inline int ble_compressed_log_abort(ble_cp_log_buffer_mgmt_t *mgmt) +{ + ble_log_commit(mgmt->handle, 0); + BLE_LOG_CAS_RELEASE(mgmt->source_lock); return 0; } @@ -287,76 +305,72 @@ int ble_log_compressed_hex_print_internal(ble_cp_log_buffer_mgmt_t *mgmt, uint32 int ble_log_compressed_hex_printv(uint8_t source, uint32_t log_index, size_t args_cnt, va_list args) { - ble_cp_log_buffer_mgmt_t *mgmt = NULL; + ble_cp_log_buffer_mgmt_t mgmt; if (ble_compressed_log_cb_get(source, &mgmt)) { return 0; } - if (ble_log_compressed_hex_print_internal(mgmt, log_index, args_cnt, args) != 0) { - ble_compressed_log_buffer_free(mgmt); + if (ble_log_compressed_hex_print_internal(&mgmt, log_index, args_cnt, args) != 0) { + ble_compressed_log_abort(&mgmt); return 0; } - ble_compressed_log_output(source, mgmt->buffer, mgmt->idx); - ble_compressed_log_buffer_free(mgmt); + ble_compressed_log_commit(&mgmt); return 0; } int ble_log_compressed_hex_print(uint8_t source, uint32_t log_index, size_t args_cnt, ...) { - ble_cp_log_buffer_mgmt_t *mgmt = NULL; + ble_cp_log_buffer_mgmt_t mgmt; if (ble_compressed_log_cb_get(source, &mgmt)) { return 0; } if (args_cnt == 0) { - if (ble_log_cp_push_u8(mgmt, LOG_HEADER(LOG_TYPE_HEX_ARGS, 0)) != 0 || - ble_log_cp_push_u16(mgmt, log_index) != 0) { - ble_compressed_log_buffer_free(mgmt); + if (ble_log_cp_push_u8(&mgmt, LOG_HEADER(LOG_TYPE_HEX_ARGS, 0)) != 0 || + ble_log_cp_push_u16(&mgmt, log_index) != 0) { + ble_compressed_log_abort(&mgmt); return 0; } } else { va_list args; va_start(args, args_cnt); - if (ble_log_compressed_hex_print_internal(mgmt, log_index, args_cnt, args) != 0) { + if (ble_log_compressed_hex_print_internal(&mgmt, log_index, args_cnt, args) != 0) { va_end(args); - ble_compressed_log_buffer_free(mgmt); + ble_compressed_log_abort(&mgmt); return 0; } va_end(args); } - ble_compressed_log_output(source, mgmt->buffer, mgmt->idx); - ble_compressed_log_buffer_free(mgmt); + ble_compressed_log_commit(&mgmt); return 0; } int ble_log_compressed_hex_print_buf(uint8_t source, uint32_t log_index, uint8_t buf_idx, const uint8_t *buf, size_t len) { - ble_cp_log_buffer_mgmt_t *mgmt = NULL; + ble_cp_log_buffer_mgmt_t mgmt; if (ble_compressed_log_cb_get(source, &mgmt)) { return 0; } if (buf == NULL) { - ble_log_cp_push_u8(mgmt, LOG_HEADER(LOG_TYPE_INFO, LOG_TYPE_INFO_NULL_BUF)); - ble_log_cp_push_u16(mgmt, log_index); - ble_compressed_log_output(source, mgmt->buffer, mgmt->idx); - ble_compressed_log_buffer_free(mgmt); + ble_log_cp_push_u8(&mgmt, LOG_HEADER(LOG_TYPE_INFO, LOG_TYPE_INFO_NULL_BUF)); + ble_log_cp_push_u16(&mgmt, log_index); + ble_compressed_log_commit(&mgmt); return 0; } - if (ble_log_cp_push_u8(mgmt, LOG_HEADER(LOG_TYPE_HEX_BUF, buf_idx)) != 0 || - ble_log_cp_push_u16(mgmt, log_index) != 0 || - ble_log_cp_push_u16(mgmt, (uint16_t)len) != 0 || - ble_log_cp_push_buf(mgmt, buf, (uint16_t)len) != 0) { - ble_compressed_log_buffer_free(mgmt); + if (ble_log_cp_push_u8(&mgmt, LOG_HEADER(LOG_TYPE_HEX_BUF, buf_idx)) != 0 || + ble_log_cp_push_u16(&mgmt, log_index) != 0 || + ble_log_cp_push_u16(&mgmt, (uint16_t)len) != 0 || + ble_log_cp_push_buf(&mgmt, buf, (uint16_t)len) != 0) { + ble_compressed_log_abort(&mgmt); return 0; } - ble_compressed_log_output(source, mgmt->buffer, mgmt->idx); - ble_compressed_log_buffer_free(mgmt); + ble_compressed_log_commit(&mgmt); return 0; } #endif /* CONFIG_BLE_COMPRESSED_LOG_ENABLE */ diff --git a/components/bt/common/ble_log/extension/log_compression/include/log_compression/utils.h b/components/bt/common/ble_log/extension/log_compression/include/log_compression/utils.h index 6197b1d6e0f..fe35e793ff3 100644 --- a/components/bt/common/ble_log/extension/log_compression/include/log_compression/utils.h +++ b/components/bt/common/ble_log/extension/log_compression/include/log_compression/utils.h @@ -10,49 +10,6 @@ #include #include -#ifndef CONCAT -#define CONCAT(a, b) a##b -#endif - -#ifndef _CONCAT -#define _CONCAT(a, b) CONCAT(a, b) -#endif - -#define _0 0 -#define _1 1 -#define _2 2 -#define _3 3 -#define _4 4 -#define _5 5 -#define _6 6 -#define _7 7 -#define _8 8 -#define _9 9 - -#define __COUNT_ARGS(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _n, X...) _n -#define COUNT_ARGS(X...) __COUNT_ARGS(, ##X, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) - -#ifndef FOR_EACH_IDX -#define FOR_EACH_IDX(macro, name, len, ...) \ - _CONCAT(_FOR_EACH_, COUNT_ARGS(__VA_ARGS__))(macro, name, len, __VA_ARGS__) -#endif - -#define _FOR_EACH_0(m, n, l, ...) -#define _FOR_EACH_1(m, n, l, i1, ...) m(n, l, i1) -#define _FOR_EACH_2(m, n, l, i1, ...) m(n, l, i1) _FOR_EACH_1(m, n, l, __VA_ARGS__) -#define _FOR_EACH_3(m, n, l, i1, ...) m(n, l, i1) _FOR_EACH_2(m, n, l, __VA_ARGS__) -#define _FOR_EACH_4(m, n, l, i1, ...) m(n, l, i1) _FOR_EACH_3(m, n, l, __VA_ARGS__) -#define _FOR_EACH_5(m, n, l, i1, ...) m(n, l, i1) _FOR_EACH_4(m, n, l, __VA_ARGS__) - -#define _GEN_INDEX_0() -#define _GEN_INDEX_1() _0 -#define _GEN_INDEX_2() _0, _1 -#define _GEN_INDEX_3() _0, _1, _2 -#define _GEN_INDEX_4() _0, _1, _2, _3 -#define _GEN_INDEX_5() _0, _1, _2, _3, _4 -#define GEN_INDEX(n) _CONCAT(_GEN_INDEX_, n)() - - enum { BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, BLE_COMPRESSED_LOG_OUT_SOURCE_MESH, @@ -72,9 +29,6 @@ enum { ARG_SIZE_TYPE_MAX, }; -/* The maximum number of buffers used simultaneously */ -#define LOG_CP_MAX_LOG_BUFFER_USED_SIMU 3 - #define LOG_TYPE_ZERO_ARGS 0 #define LOG_TYPE_HEX_ARGS 1 #define LOG_TYPE_HEX_BUF 2 @@ -87,32 +41,22 @@ enum { #define LOG_TYPE_INFO_TASK_SWITCH 2 typedef struct { - volatile bool busy; - uint8_t *buffer; - uint16_t idx; - uint16_t len; + uint8_t *buffer; /* claim() payload pointer */ + uint16_t idx; /* bytes written so far */ + uint16_t len; /* claimed capacity (max_len) */ + uint32_t handle; /* claim handle for commit/abort */ + volatile uint32_t *source_lock; + char **last_task_handle; + char *current_task_handle; + bool task_switched; } ble_cp_log_buffer_mgmt_t; -#define CONTENT_CHECK(idx, buf, except_val, len) -#define LENGTH_CHECK(idx, pbuffer_mgmt) do ( if(unlikely((idx) > (pbuffer_mgmt->len))) assert(0 && "Maximum log buffer length exceeded");) while(0) - -#define BLE_LOG_CP_CONTENT_CHECK_ENABLE 0 -#define BLE_LOG_CP_CONTENT_CHECK_VAL 0x00 - static inline int ble_log_cp_buffer_safe_check(ble_cp_log_buffer_mgmt_t *pbuf_mgmt, uint16_t write_len) { if ((pbuf_mgmt->idx + write_len) > pbuf_mgmt->len) { printf("Maximum length of buffer(%p) idx %d write_len %d exceed\n", pbuf_mgmt, pbuf_mgmt->idx, write_len); return -1; } -#if BLE_LOG_CP_CONTENT_CHECK_ENABLE - for (int i = pbuf_mgmt->idx; i < pbuf_mgmt->idx + write_len; i++) { - if (pbuf_mgmt->buffer[i] != BLE_LOG_CP_CONTENT_CHECK_VAL) { - printf("The value(%02x) in the buffer does not match the expected(%02x)\n", pbuf_mgmt->buffer[i], BLE_LOG_CP_CONTENT_CHECK_VAL); - return -1; - } - } -#endif return 0; } @@ -182,17 +126,4 @@ static inline int ble_log_cp_update_half_byte(ble_cp_log_buffer_mgmt_t *pbuf_mgm return 0; } -static inline int ble_log_cp_buffer_print(ble_cp_log_buffer_mgmt_t *pbuf_mgmt) -{ - for (size_t i = 0; i < pbuf_mgmt->idx; i++) { - printf("%02x ", pbuf_mgmt->buffer[i]); - } - printf("\n"); - return 0; -} - -static inline int ble_compressed_log_output(uint8_t source, uint8_t *data, uint16_t len) -{ - return ble_log_write_hex(BLE_LOG_SRC_ENCODE, data, len); -} #endif /* _BLE_LOG_COMPRESSION_UTILS_H */ diff --git a/components/bt/common/ble_log/src/ble_log_lbm_v2.c b/components/bt/common/ble_log/src/ble_log_lbm_v2.c index 9306da8ea8a..a52039fb04a 100644 --- a/components/bt/common/ble_log/src/ble_log_lbm_v2.c +++ b/components/bt/common/ble_log/src/ble_log_lbm_v2.c @@ -33,7 +33,7 @@ #define BLE_LOG_WAIT_TIMEOUT_TICKS pdMS_TO_TICKS(1000) /* Single-instruction clock read; a function would add an IRAM call site. */ -#define ble_log_timestamp_now() ((uint32_t)esp_timer_get_time()) +#define BLE_LOG_TIMESTAMP_NOW() ((uint32_t)esp_timer_get_time()) /* ------------------------------- */ /* Global Pool Context */ @@ -196,7 +196,7 @@ ble_log_pool_publish_open_and_unlock(ble_log_prph_trans_t *trans) BLE_LOG_ATOMIC_STORE_RELAXED(trans->state, BLE_LOG_TRANS_STATE_OPEN); /* The following lock release publishes both state and frame data. */ __atomic_fetch_or(&g_pool.open_bitmap, BIT(trans->id), __ATOMIC_RELAXED); - ble_log_cas_release(&trans->atomic_lock); + BLE_LOG_CAS_RELEASE(&trans->atomic_lock); ble_log_pool_notify_waiter(trans->id); } @@ -253,7 +253,7 @@ BLE_LOG_IRAM_ATTR void ble_log_pool_seal_and_send(ble_log_prph_trans_t *trans) /* Release the lock BEFORE submitting. The buffer is now in no bitmap and * is SENDING, so no other writer can find it. */ - ble_log_cas_release(&trans->atomic_lock); + BLE_LOG_CAS_RELEASE(&trans->atomic_lock); ble_log_rt_submit_trans(trans); } @@ -281,12 +281,12 @@ ble_log_prph_trans_t *ble_log_pool_try_claim_from(volatile uint32_t *bitmap, } ble_log_prph_trans_t *trans = g_pool.trans[id]; - if (!ble_log_cas_acquire(&trans->atomic_lock)) { + if (!BLE_LOG_CAS_ACQUIRE(&trans->atomic_lock)) { continue; } if (BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state) != expected_state) { /* The bitmap is only a hint; another owner may have changed state. */ - ble_log_cas_release(&trans->atomic_lock); + BLE_LOG_CAS_RELEASE(&trans->atomic_lock); continue; } @@ -323,7 +323,7 @@ ble_log_prph_trans_t *ble_log_pool_try_claim_available(uint32_t frame_len, bool uint8_t open_id = (uint8_t)BLE_LOG_ATOMIC_LOAD_RELAXED(g_pool.open_cursor); if (open_domain & BIT(open_id)) { ble_log_prph_trans_t *open_trans = g_pool.trans[open_id]; - if (ble_log_cas_acquire(&open_trans->atomic_lock)) { + if (BLE_LOG_CAS_ACQUIRE(&open_trans->atomic_lock)) { if (BLE_LOG_ATOMIC_LOAD_RELAXED(open_trans->state) == BLE_LOG_TRANS_STATE_OPEN) { if (BLE_LOG_TRANS_FREE_SPACE(open_trans) >= frame_len) { ble_log_pool_bitmap_clear(&g_pool.open_bitmap, open_id); @@ -331,7 +331,7 @@ ble_log_prph_trans_t *ble_log_pool_try_claim_available(uint32_t frame_len, bool } ble_log_pool_seal_and_send(open_trans); /* releases the lock */ } else { - ble_log_cas_release(&open_trans->atomic_lock); + BLE_LOG_CAS_RELEASE(&open_trans->atomic_lock); } } } @@ -479,7 +479,7 @@ uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, uint32_t *handle) uint32_t frame_sn = BLE_LOG_GET_GLOBAL_SN(); /* The timestamp is the log occurrence time, before pool contention. */ - uint32_t timestamp = ble_log_timestamp_now(); + uint32_t timestamp = BLE_LOG_TIMESTAMP_NOW(); bool non_yield = !xPortCanYield() || xTaskGetSchedulerState() != taskSCHEDULER_RUNNING; size_t payload_capacity = sizeof(timestamp) + max_len; @@ -535,7 +535,7 @@ void ble_log_commit(uint32_t handle, size_t actual_len) if (trans->pos == 0) { BLE_LOG_ATOMIC_STORE_RELEASE(trans->state, BLE_LOG_TRANS_STATE_FREE); ble_log_pool_bitmap_set(&g_pool.free_bitmap, trans->id); - ble_log_cas_release(&trans->atomic_lock); + BLE_LOG_CAS_RELEASE(&trans->atomic_lock); ble_log_pool_notify_waiter(trans->id); } else { ble_log_pool_publish_open_and_unlock(trans); @@ -835,15 +835,15 @@ bool ble_log_internal_snapshot(uint16_t reason_flags, /* Capture the complete occurrence sample before any dedicated-buffer * drain or wait. Without a TS sync sample, esp_ts comes from the frame * timestamp and os_ts from the current tick. */ - uint32_t timestamp = ts_info ? ts_info->esp_ts : ble_log_timestamp_now(); + uint32_t timestamp = ts_info ? ts_info->esp_ts : BLE_LOG_TIMESTAMP_NOW(); TickType_t start_tick = xTaskGetTickCount(); for (;;) { - if (ble_log_cas_acquire(&internal_trans->atomic_lock)) { + if (BLE_LOG_CAS_ACQUIRE(&internal_trans->atomic_lock)) { if (BLE_LOG_ATOMIC_LOAD_RELAXED(internal_trans->state) == BLE_LOG_TRANS_STATE_FREE) { break; } - ble_log_cas_release(&internal_trans->atomic_lock); + BLE_LOG_CAS_RELEASE(&internal_trans->atomic_lock); } if (!required || (xTaskGetTickCount() - start_tick) >= BLE_LOG_WAIT_TIMEOUT_TICKS) { @@ -885,7 +885,7 @@ bool ble_log_internal_snapshot(uint16_t reason_flags, internal_trans->pos = BLE_LOG_INTERNAL_FRAME_LEN; BLE_LOG_ATOMIC_STORE_RELAXED(internal_trans->state, BLE_LOG_TRANS_STATE_SENDING); - ble_log_cas_release(&internal_trans->atomic_lock); + BLE_LOG_CAS_RELEASE(&internal_trans->atomic_lock); ble_log_rt_submit_trans(internal_trans); BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); return true; @@ -905,14 +905,14 @@ BLE_LOG_STATIC bool ble_log_pool_flush_all_trans(void) * current frame copy. Seal every remaining OPEN transport. */ for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { ble_log_prph_trans_t *trans = g_pool.trans[id]; - while (!ble_log_cas_acquire(&trans->atomic_lock)) { + while (!BLE_LOG_CAS_ACQUIRE(&trans->atomic_lock)) { } if (BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state) == BLE_LOG_TRANS_STATE_OPEN && trans->pos > 0) { ble_log_pool_bitmap_clear(&g_pool.open_bitmap, id); ble_log_pool_seal_and_send(trans); /* releases the lock */ } else { - ble_log_cas_release(&trans->atomic_lock); + BLE_LOG_CAS_RELEASE(&trans->atomic_lock); } } @@ -941,14 +941,14 @@ void ble_log_lbm_flush_open_transports(void) continue; } ble_log_prph_trans_t *trans = g_pool.trans[id]; - if (!ble_log_cas_acquire(&trans->atomic_lock)) { + if (!BLE_LOG_CAS_ACQUIRE(&trans->atomic_lock)) { continue; } if (BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state) == BLE_LOG_TRANS_STATE_OPEN && trans->pos > 0) { ble_log_pool_seal_and_send(trans); } else { - ble_log_cas_release(&trans->atomic_lock); + BLE_LOG_CAS_RELEASE(&trans->atomic_lock); } } @@ -1096,7 +1096,7 @@ bool ble_log_write_hex(ble_log_src_t src_code, const uint8_t *addr, size_t len) bool is_isr = BLE_LOG_IN_ISR(); bool can_yield = !is_isr && xPortCanYield() && xTaskGetSchedulerState() == taskSCHEDULER_RUNNING; - uint32_t timestamp = ble_log_timestamp_now(); + uint32_t timestamp = BLE_LOG_TIMESTAMP_NOW(); size_t payload_len = sizeof(timestamp) + len; ble_log_prph_trans_t *trans = ble_log_pool_acquire(payload_len, !can_yield, false); diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_util.h b/components/bt/common/ble_log/src/internal_include/ble_log_util.h index 260cd6851ac..e12787523b5 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_util.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_util.h @@ -80,9 +80,9 @@ extern void esp_panic_handler_feed_wdts(void); /* INLINE */ /* Compare-and-swap lock as macros: single-instruction acquire/release used * from several IRAM sites; a function would add an IRAM call site each. */ -#define ble_log_cas_acquire(cas_lock) \ +#define BLE_LOG_CAS_ACQUIRE(cas_lock) \ (__atomic_exchange_n((cas_lock), 1, __ATOMIC_ACQUIRE) == 0) -#define ble_log_cas_release(cas_lock) \ +#define BLE_LOG_CAS_RELEASE(cas_lock) \ __atomic_store_n((cas_lock), 0, __ATOMIC_RELEASE) #define BLE_LOG_VERSION (7) diff --git a/components/bt/common/ble_log/test_apps/ble_log_perf_test/README.md b/components/bt/common/ble_log/test_apps/ble_log_perf_test/README.md index de7c10477f3..50fd303a5ec 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_perf_test/README.md +++ b/components/bt/common/ble_log/test_apps/ble_log_perf_test/README.md @@ -28,9 +28,11 @@ Runtime dispatch behavior and latency are covered by the sibling - `throughput`: fixed 32B / 64B / 128B / mixed 8-64B payload profiles, each at 2 Mbps, 20 Mbps, and unlimited link. Runs 3 write_hex writers + LL task + LL HCI + compressed writer + 1 kHz ISR writer concurrently. -- `write_hex cycles`: single writer, no link cap, payload 8/32/64/128 B. +- `write_hex cycles`: single writer, no link cap, payload 8/32/64/128 B. The + scheduler remains active during each measured call, followed by an unmeasured + one-tick pacing delay so the no-loss profile does not become a saturation test. - `write_hex drop path cycles`: saturated 2 Mbps link, measures the cost of a - failed (dropped) write. + failed (dropped) write without the no-loss pacing delay. - `write_hex_ll cycles`: payload 8/32/64/128 B; plus a 32+32 B append case. - `compressed write cycles`: workload matrix of the compressed entry points — U32 args (0/1/2/mixed), U64 values (full 8B / leading-zero LZ / zero), diff --git a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.c b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.c index 3437fed856c..2642e629c31 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.c +++ b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.c @@ -10,7 +10,7 @@ #include "unity_test_runner.h" #include "ble_log.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "test_ble_log_main.h" bool test_ble_log_walk_frames(const uint8_t *data, size_t len, @@ -35,8 +35,10 @@ bool test_ble_log_walk_frames(const uint8_t *data, size_t len, } if (observer) { + uint8_t source_meta = head.frame_meta & 0xff; test_ble_log_frame_t frame = { - .src = head.frame_meta & 0xff, + .src = BLE_LOG_SRC_ID(source_meta), + .source_meta = source_meta, .sn = head.frame_meta >> 8, .payload = data + offset + BLE_LOG_FRAME_HEAD_LEN, .payload_len = head.length, diff --git a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c index 202c3f8e9a4..5679d0e24df 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c +++ b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c @@ -12,7 +12,7 @@ #include #include "ble_log.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "ble_log_prph_test.h" #include "log_compression/utils.h" #include "esp_cpu.h" @@ -125,7 +125,7 @@ typedef struct { uint32_t ll_flag; /* UINT32_MAX: use ble_log_write_hex */ bool compressed; /* call ble_log_compressed_hex_print instead */ bool record_cycles; - bool isolate_write; /* vTaskSuspendAll around the timed write */ + bool pace_after_write; /* one tick outside a no-loss timed call */ const uint8_t *ll_append; /* write_hex_ll append part */ size_t ll_append_len; const perf_compress_cfg_t *compress_cfg; @@ -174,7 +174,6 @@ typedef struct { bool compress; bool isr; bool measure_cycles; - bool isolate_write; bool expect_no_loss; const uint8_t *ll_append; size_t ll_append_len; @@ -316,8 +315,7 @@ static void print_run_separator(const char *tag, const perf_run_cfg_t *cfg) tag, (unsigned)cfg->run_idx, (unsigned)cfg->run_total, cfg->mode_name, cfg->profile_name); print_link(cfg->bytes_per_second); - printf(" isolate=%s ============\n", - cfg->isolate_write ? "sched" : "none"); + printf(" isolate=none ============\n"); } /* ---------------- */ @@ -349,9 +347,6 @@ static void perf_writer_task(void *arg) size_t payload_len = w->profile[frame_index % w->profile_count]; memcpy(payload, &frame_index, sizeof(frame_index)); - if (w->isolate_write) { - vTaskSuspendAll(); - } uint32_t start_cycles = w->record_cycles ? esp_cpu_get_cycle_count() : 0; bool ok; @@ -427,9 +422,6 @@ static void perf_writer_task(void *arg) if (w->record_cycles) { write_cycles = esp_cpu_get_cycle_count() - start_cycles; } - if (w->isolate_write) { - xTaskResumeAll(); - } if (ok) { w->cycles->ok_frames++; @@ -443,7 +435,14 @@ static void perf_writer_task(void *arg) } } frame_index++; - taskYIELD(); + if (w->pace_after_write) { + /* Cycle profiles measure one production-semantics call, then + * yield enough time for deferred dispatch and sink recycling. + * Throughput and explicit drop profiles remain unpaced. */ + vTaskDelay(1); + } else { + taskYIELD(); + } } xSemaphoreGive(w->done); @@ -485,21 +484,21 @@ static void observe_perf_frame(const test_ble_log_frame_t *frame, void *ctx) sink->frames_by_src[frame->src]++; } - /* Capture enhanced statistics records (written/lost per source). - * Frames carry a 4-byte os_timestamp prefix before the record. */ if (frame->src == BLE_LOG_SRC_INTERNAL && - frame->payload_len == sizeof(uint32_t) + sizeof(ble_log_enh_stat_t) && - frame->payload[sizeof(uint32_t)] == BLE_LOG_INT_SRC_ENH_STAT) { - ble_log_enh_stat_t stat; - memcpy(&stat, frame->payload + sizeof(uint32_t), sizeof(stat)); - if (stat.src_code < BLE_LOG_SRC_MAX) { - sink->stat_written[stat.src_code] = stat.written_frame_cnt; - sink->stat_lost[stat.src_code] = stat.lost_frame_cnt; + frame->payload_len == sizeof(uint32_t) + + sizeof(ble_log_internal_snapshot_t)) { + ble_log_internal_snapshot_t snapshot; + memcpy(&snapshot, frame->payload + sizeof(uint32_t), sizeof(snapshot)); + if (snapshot.int_src_code != BLE_LOG_INT_SRC_SNAPSHOT) { + return; } - } else if (frame->src == BLE_LOG_SRC_INTERNAL && - frame->payload_len == sizeof(uint32_t) + sizeof(ble_log_final_stat_t) && - frame->payload[sizeof(uint32_t)] == BLE_LOG_INT_SRC_FINAL_STAT) { - sink->stat_final_seen = true; + for (int i = 0; i < BLE_LOG_SRC_CORE_COUNT; i++) { + int src = BLE_LOG_SRC_CORE_FIRST + i; + sink->stat_written[src] = snapshot.stats[i].written_frame_cnt; + sink->stat_lost[src] = snapshot.stats[i].lost_frame_cnt; + } + sink->stat_final_seen = + (snapshot.reason_flags & BLE_LOG_SNAPSHOT_REASON_FLUSH) != 0; } } @@ -541,7 +540,7 @@ static void fill_writer(perf_writer_t *w, const char *name, ble_log_src_t src, w->ll_flag = ll_flag; w->compressed = compressed; w->record_cycles = cfg->measure_cycles; - w->isolate_write = cfg->isolate_write; + w->pace_after_write = cfg->measure_cycles && cfg->expect_no_loss; w->ll_append = cfg->ll_append; w->ll_append_len = cfg->ll_append_len; w->compress_cfg = cfg->compress_cfg; @@ -651,8 +650,8 @@ static void run_perf_case(const perf_run_cfg_t *cfg) } if (ll_hci) { - fill_writer(&writers[writer_count], "write_hex_ll_hci", BLE_LOG_SRC_LL_HCI, - BIT(BLE_LOG_LL_FLAG_HCI), false, cfg, &ll_hci_cycles, start); + fill_writer(&writers[writer_count], "write_hex_hci", BLE_LOG_SRC_HCI, + UINT32_MAX, false, cfg, &ll_hci_cycles, start); TEST_ASSERT_EQUAL(pdPASS, xTaskCreatePinnedToCore(perf_writer_task, "perf_hci", PERF_WRITER_STACK_SIZE, @@ -744,6 +743,8 @@ static void run_perf_case(const perf_run_cfg_t *cfg) } sink.stop = true; TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(sink.done, pdMS_TO_TICKS(1000))); + TEST_ASSERT_TRUE_MESSAGE(sink.stat_final_seen, + "Final statistics snapshot was not received"); uint8_t discard[BLE_LOG_TRANS_SIZE]; while (ble_log_prph_test_read(discard, sizeof(discard), 0, 0, NULL)) { @@ -753,12 +754,12 @@ static void run_perf_case(const perf_run_cfg_t *cfg) printf("BLE_LOG_PERF mode=%s profile=%s payload=%uB ", cfg->mode_name, cfg->profile_name, cfg->profile[0]); print_link(cfg->bytes_per_second); - printf(" isolate=%s duration=%" PRIu64 " ms\n", - cfg->isolate_write ? "sched" : "none", elapsed_us / 1000); + printf(" isolate=none duration=%" PRIu64 " ms\n", elapsed_us / 1000); printf("BLE_LOG_PERF flush=%" PRIu32 " cycles\n", flush_cycles); uint64_t ok_total = 0; uint64_t failed_total = 0; + uint64_t custom_failed_total = 0; for (i = 0; i < cfg->custom_writer_count; i++) { char name[32]; snprintf(name, sizeof(name), "write_hex%" PRIu32, i); @@ -769,6 +770,7 @@ static void run_perf_case(const perf_run_cfg_t *cfg) } ok_total += custom_cycles[i].ok_frames; failed_total += custom_cycles[i].failed_frames; + custom_failed_total += custom_cycles[i].failed_frames; } #if CONFIG_BLE_LOG_LL_ENABLED if (ll_task) { @@ -782,9 +784,9 @@ static void run_perf_case(const perf_run_cfg_t *cfg) } if (ll_hci) { if (cfg->measure_cycles) { - print_cycles("write_hex_ll_hci", &ll_hci_cycles, true); + print_cycles("write_hex_hci", &ll_hci_cycles, false); } else { - print_writer_counts("write_hex_ll_hci", &ll_hci_cycles); + print_writer_counts("write_hex_hci", &ll_hci_cycles); } ok_total += ll_hci_cycles.ok_frames; failed_total += ll_hci_cycles.failed_frames; @@ -827,10 +829,12 @@ static void run_perf_case(const perf_run_cfg_t *cfg) } } #endif + uint64_t ll_frames = sink.frames_by_src[BLE_LOG_SRC_LL_TASK] + + sink.frames_by_src[BLE_LOG_SRC_LL_HCI] + + sink.frames_by_src[BLE_LOG_SRC_LL_ISR]; uint64_t sink_frames = sink.frames_by_src[BLE_LOG_SRC_CUSTOM] + - sink.frames_by_src[BLE_LOG_SRC_LL_TASK] + - sink.frames_by_src[BLE_LOG_SRC_LL_HCI] + - sink.frames_by_src[BLE_LOG_SRC_LL_ISR] + + ll_frames + + sink.frames_by_src[BLE_LOG_SRC_HCI] + sink.frames_by_src[BLE_LOG_SRC_ENCODE]; printf("BLE_LOG_PERF total ok=%" PRIu64 " failed=%" PRIu64 " sink_frames=%" PRIu64 " sink_bytes=%" PRIu64 @@ -840,19 +844,21 @@ static void run_perf_case(const perf_run_cfg_t *cfg) if (!cfg->measure_cycles) { print_throughput(sink.transport_bytes, elapsed_us, cfg->bytes_per_second); } - printf("BLE_LOG_PERF stat written=[custom=%" PRIu64 " ll_task=%" PRIu64 - " ll_hci=%" PRIu64 " ll_isr=%" PRIu64 " encode=%" PRIu64 - "] lost=[custom=%" PRIu64 " ll_task=%" PRIu64 " ll_hci=%" PRIu64 - " ll_isr=%" PRIu64 " encode=%" PRIu64 "]\n", + printf("BLE_LOG_PERF stat written=[custom=%" PRIu64 " ll=%" PRIu64 + " hci=%" PRIu64 " encode=%" PRIu64 + "] lost=[custom=%" PRIu64 " ll=%" PRIu64 + " hci=%" PRIu64 " encode=%" PRIu64 "]\n", sink.stat_written[BLE_LOG_SRC_CUSTOM], - sink.stat_written[BLE_LOG_SRC_LL_TASK], - sink.stat_written[BLE_LOG_SRC_LL_HCI], + sink.stat_written[BLE_LOG_SRC_LL_TASK] + + sink.stat_written[BLE_LOG_SRC_LL_HCI] + sink.stat_written[BLE_LOG_SRC_LL_ISR], + sink.stat_written[BLE_LOG_SRC_HCI], sink.stat_written[BLE_LOG_SRC_ENCODE], sink.stat_lost[BLE_LOG_SRC_CUSTOM], - sink.stat_lost[BLE_LOG_SRC_LL_TASK], - sink.stat_lost[BLE_LOG_SRC_LL_HCI], + sink.stat_lost[BLE_LOG_SRC_LL_TASK] + + sink.stat_lost[BLE_LOG_SRC_LL_HCI] + sink.stat_lost[BLE_LOG_SRC_LL_ISR], + sink.stat_lost[BLE_LOG_SRC_HCI], sink.stat_lost[BLE_LOG_SRC_ENCODE]); print_run_separator("END", cfg); @@ -862,13 +868,19 @@ static void run_perf_case(const perf_run_cfg_t *cfg) TEST_ASSERT_GREATER_THAN_UINT64(0, ok_total); if (cfg->expect_no_loss) { TEST_ASSERT_EQUAL_UINT64(0, failed_total); - TEST_ASSERT_EQUAL_UINT64(0, sink.stat_lost[BLE_LOG_SRC_CUSTOM]); - TEST_ASSERT_EQUAL_UINT64(0, sink.stat_lost[BLE_LOG_SRC_LL_TASK]); - TEST_ASSERT_EQUAL_UINT64(0, sink.stat_lost[BLE_LOG_SRC_LL_HCI]); - TEST_ASSERT_EQUAL_UINT64(0, sink.stat_lost[BLE_LOG_SRC_LL_ISR]); - TEST_ASSERT_EQUAL_UINT64(0, sink.stat_lost[BLE_LOG_SRC_ENCODE]); + for (int i = 0; i < BLE_LOG_SRC_CORE_COUNT; i++) { + TEST_ASSERT_EQUAL_UINT64(0, + sink.stat_lost[BLE_LOG_SRC_CORE_FIRST + i]); + } } else if (sink.stat_final_seen) { - TEST_ASSERT_EQUAL_UINT64(failed_total, sink.stat_lost[BLE_LOG_SRC_CUSTOM]); + TEST_ASSERT_EQUAL_UINT64(custom_failed_total, + sink.stat_lost[BLE_LOG_SRC_CUSTOM]); +#if CONFIG_BLE_LOG_LL_ENABLED + if (ll_hci) { + TEST_ASSERT_EQUAL_UINT64(ll_hci_cycles.failed_frames, + sink.stat_lost[BLE_LOG_SRC_HCI]); + } +#endif #if CONFIG_BLE_COMPRESSED_LOG_ENABLE if (compress) { /* Every hex_print call reaches write_hex(ENCODE) exactly once; @@ -934,7 +946,6 @@ static void run_cycle_case(const char *name, const uint16_t *profile, size_t cou .ll_hci = ll_hci, .compress = compress_cfg != NULL, .measure_cycles = true, - .isolate_write = true, .expect_no_loss = true, .compress_cfg = compress_cfg, }; @@ -982,7 +993,6 @@ TEST_CASE("BLE Log write_hex drop path cycles (link=2Mbps)", "[ble_log][perf][cy .bytes_per_second = PERF_LINK_2MBPS_BPS, .custom_writer_count = 1, .measure_cycles = true, - .isolate_write = true, }; run_perf_case(&cfg); } @@ -1011,7 +1021,6 @@ TEST_CASE("BLE Log write_hex_ll append cycles (32+32B)", "[ble_log][perf][cycle] .custom_writer_count = 0, .ll_task = true, .measure_cycles = true, - .isolate_write = true, .expect_no_loss = true, .ll_append = append_buf, .ll_append_len = 32, diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/README.md b/components/bt/common/ble_log/test_apps/ble_log_rt_test/README.md index 75af9d61753..7f729f03b65 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/README.md +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/README.md @@ -19,12 +19,12 @@ the hand-over time, so dispatch latency is measured without a real link. - `runtime` regressions: shared ESP Timer task fairness, periodic timestamp delivery without light-sleep wakeups, ISR-only submission delivery, consumer-independent receipt timing, extra-marker detection, exact 1 ms - first-submission defer scheduling, full task-pool snapshot dispatch, deinit + first-submission defer scheduling, full shared-pool snapshot dispatch, deinit racing submissions (SMP-pinned writer, failure-safe recovery), bounded inflight-peak statistics, and monotonic millisecond waits at both supported tick rates. -The first-deadline and full task-pool regressions require dispatch exclusion +The first-deadline and full shared-pool regressions require dispatch exclusion while enqueueing, so they run on single-core builds only (they are ignored on SMP targets). The deinit-race regression pins its writer to the other core on SMP. diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c index bdb96ad8ae9..c8281ff6662 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c @@ -10,7 +10,7 @@ #include "unity_test_runner.h" #include "ble_log.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "ble_log_prph_test.h" #include "test_ble_log_main.h" @@ -36,8 +36,10 @@ bool test_ble_log_walk_frames(const uint8_t *data, size_t len, } if (observer) { + uint8_t source_meta = head.frame_meta & 0xff; test_ble_log_frame_t frame = { - .src = head.frame_meta & 0xff, + .src = BLE_LOG_SRC_ID(source_meta), + .source_meta = source_meta, .sn = head.frame_meta >> 8, .payload = data + offset + BLE_LOG_FRAME_HEAD_LEN, .payload_len = head.length, diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c index 1f1791d1c53..1cc8286e3a5 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c @@ -12,7 +12,7 @@ #include #include "ble_log.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "ble_log_prph_test.h" #include "ble_log_rt.h" #include "esp_timer.h" @@ -24,7 +24,6 @@ #define RT_SAMPLE_COUNT (32) #define RT_BURST_SIZE (4) -#define RT_TASK_POOL_TRANS_COUNT ((BLE_LOG_LBM_ATOMIC_TASK_CNT + 1) * BLE_LOG_TRANS_BUF_CNT) #define RT_READ_TIMEOUT_MS (100) #define RT_QUIET_TIMEOUT_MS (10) #define RT_QUIET_DRAIN_DEADLINE_MS (2000) @@ -37,7 +36,7 @@ #define RT_CONSUMER_DELAY_MS (30) #define RT_RECEIVE_MAX_LATENCY_US (10000) #define RT_BURST_SPAN_MAX_US (500) -#define RT_PEAK_WRITES (8) +#define RT_PEAK_WRITES (BLE_LOG_POOL_SHARED_CNT) #define RT_DEINIT_ROUNDS (200) #define RT_JOIN_TIMEOUT_MS (5000) @@ -100,9 +99,14 @@ static void observe_runtime_marker(const test_ble_log_frame_t *frame, void *ctx) { rt_marker_observer_t *observer = ctx; if (frame->src == BLE_LOG_SRC_INTERNAL && - frame->payload_len > sizeof(uint32_t) && - frame->payload[sizeof(uint32_t)] == BLE_LOG_INT_SRC_TS) { - observer->ts_count++; + frame->payload_len == sizeof(uint32_t) + + sizeof(ble_log_internal_snapshot_t)) { + ble_log_internal_snapshot_t snapshot; + memcpy(&snapshot, frame->payload + sizeof(uint32_t), sizeof(snapshot)); + if (snapshot.int_src_code == BLE_LOG_INT_SRC_SNAPSHOT && + (snapshot.reason_flags & BLE_LOG_SNAPSHOT_REASON_TS_VALID)) { + observer->ts_count++; + } } if (frame->src != BLE_LOG_SRC_CUSTOM || @@ -275,18 +279,21 @@ static void observe_buf_util(const test_ble_log_frame_t *frame, void *ctx) { rt_peak_observer_t *observer = ctx; if (frame->src != BLE_LOG_SRC_INTERNAL || - frame->payload_len < sizeof(uint32_t) + sizeof(ble_log_buf_util_t) || - frame->payload[sizeof(uint32_t)] != BLE_LOG_INT_SRC_BUF_UTIL) { + frame->payload_len != sizeof(uint32_t) + + sizeof(ble_log_internal_snapshot_t)) { return; } - ble_log_buf_util_t util; - memcpy(&util, frame->payload + sizeof(uint32_t), sizeof(util)); - observer->buf_util_frames++; - if (util.inflight_peak > observer->max_inflight_peak) { - observer->max_inflight_peak = util.inflight_peak; + ble_log_internal_snapshot_t snapshot; + memcpy(&snapshot, frame->payload + sizeof(uint32_t), sizeof(snapshot)); + if (snapshot.int_src_code != BLE_LOG_INT_SRC_SNAPSHOT) { + return; } - if (util.inflight_peak > util.trans_cnt) { + observer->buf_util_frames++; + if (snapshot.pool.inflight_peak > observer->max_inflight_peak) { + observer->max_inflight_peak = snapshot.pool.inflight_peak; + } + if (snapshot.pool.inflight_peak > snapshot.pool.trans_cnt) { observer->over_limit = true; } } @@ -654,7 +661,7 @@ TEST_CASE("BLE Log runtime dispatch latency", "[ble_log][runtime][perf][ignore]" print_latency_stats("burst_last", RT_BURST_SIZE, s_burst_last_latency_us); } -TEST_CASE("BLE Log runtime drains the full task pool in one batch", +TEST_CASE("BLE Log runtime drains the full shared pool in one batch", "[ble_log][runtime][ignore]") { #if !CONFIG_FREERTOS_UNICORE @@ -666,8 +673,8 @@ TEST_CASE("BLE Log runtime drains the full task pool in one batch", TEST_ASSERT_TRUE(ble_log_enable(true)); - /* Flush from this ordinary test task so every task-pool transport is free - * before constructing the callback-entry snapshot. */ + /* Flush from this ordinary test task so every shared-pool transport is + * free before constructing the callback-entry snapshot. */ ble_log_prph_test_set_auto_recycle_hook(noop_callback, NULL); ble_log_flush(); ble_log_prph_test_set_auto_recycle_hook(NULL, NULL); @@ -678,15 +685,15 @@ TEST_CASE("BLE Log runtime drains the full task pool in one batch", * without an artificial item cap. */ bool wrote = true; vTaskSuspendAll(); - for (uint32_t i = 0; i < RT_TASK_POOL_TRANS_COUNT; i++) { + for (uint32_t i = 0; i < BLE_LOG_POOL_SHARED_CNT; i++) { prepare_payload(base_seq + i); wrote = wrote && ble_log_write_hex(BLE_LOG_SRC_CUSTOM, s_payload, sizeof(s_payload)); } (void)xTaskResumeAll(); - TEST_ASSERT_TRUE_MESSAGE(wrote, "Full task-pool enqueue failed"); + TEST_ASSERT_TRUE_MESSAGE(wrote, "Full shared-pool enqueue failed"); - for (uint32_t i = 0; i < RT_TASK_POOL_TRANS_COUNT; i++) { + for (uint32_t i = 0; i < BLE_LOG_POOL_SHARED_CNT; i++) { uint32_t received_seq; int64_t received_at_us; TEST_ASSERT_TRUE_MESSAGE(read_runtime_marker(&received_seq, NULL, @@ -722,10 +729,18 @@ TEST_CASE("BLE Log LBM inflight peak stays bounded under bursts", TEST_ASSERT_TRUE(write_runtime_marker(seq, NULL)); } - /* Snapshot the recorded peaks. A full marker forces the partial BUF_UTIL - * transport to roll over through the normal LBM submission path. */ - ble_log_write_buf_util(); - TEST_ASSERT_TRUE(write_runtime_marker(UINT32_C(0x81000), NULL)); + /* Recycle the burst after its peak has been recorded. A runtime hook may + * also have occupied the dedicated Internal transport, so drain twice. */ + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_capture, sizeof(s_capture), + 0, 0, NULL) > 0) { + } + } + + /* Snapshot the retained peak through the dedicated Internal transport. */ + TEST_ASSERT_TRUE(ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC, NULL, true)); TEST_ASSERT_TRUE(ble_log_rt_drain()); const int64_t deadline_us = esp_timer_get_time() + @@ -747,7 +762,7 @@ TEST_CASE("BLE Log LBM inflight peak stays bounded under bursts", TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE( 0, observer.buf_util_frames, - "No BUF_UTIL snapshots observed after flush"); + "No Internal utilization snapshot observed"); TEST_ASSERT_FALSE_MESSAGE(observer.over_limit, "inflight_peak exceeded the LBM transport count"); TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE( @@ -763,6 +778,7 @@ TEST_CASE("BLE Log runtime survives deinit racing submissions", "[ble_log][runtime][ignore]") { rt_deinit_writer_ctx_t *ctx = &s_deinit_race; + TaskHandle_t writer_task = NULL; bool reinit_ok = true; memset(ctx, 0, sizeof(*ctx)); prepare_payload(UINT32_C(0x70000)); @@ -770,13 +786,14 @@ TEST_CASE("BLE Log runtime survives deinit racing submissions", #if CONFIG_FREERTOS_UNICORE BaseType_t task_created = xTaskCreate( deinit_writer_task, "ble_log_deinit_wr", 4096, ctx, - uxTaskPriorityGet(NULL), NULL); + uxTaskPriorityGet(NULL), &writer_task); #else /* Pin the writer away from this core so submissions run concurrently * with deinit instead of alternating at yield points. */ BaseType_t task_created = xTaskCreatePinnedToCore( deinit_writer_task, "ble_log_deinit_wr", 4096, ctx, - uxTaskPriorityGet(NULL), NULL, (xPortGetCoreID() == 0) ? 1 : 0); + uxTaskPriorityGet(NULL), &writer_task, + (xPortGetCoreID() == 0) ? 1 : 0); #endif TEST_ASSERT_EQUAL_MESSAGE(pdPASS, task_created, "Writer task create failed"); @@ -786,8 +803,8 @@ TEST_CASE("BLE Log runtime survives deinit racing submissions", taskYIELD(); } - /* Stop the writer and join with a bound before touching ctx or asserting: - * a unity longjmp past a live writer would leave it on a dead stack. */ + /* Stop the writer and join with a bound before asserting so a Unity + * longjmp cannot leak the writer into later tests. */ __atomic_store_n(&ctx->stop, true, __ATOMIC_RELEASE); const int64_t join_deadline_us = esp_timer_get_time() + (int64_t)RT_JOIN_TIMEOUT_MS * 1000; @@ -799,6 +816,11 @@ TEST_CASE("BLE Log runtime survives deinit racing submissions", vTaskDelay(join_ticks); } + bool writer_exited = __atomic_load_n(&ctx->exited, __ATOMIC_ACQUIRE); + if (!writer_exited && writer_task) { + vTaskDelete(writer_task); + } + /* Recover module state before any assertion can abort the test: a * failed re-init leaves the module deinit-ed and tearDown does not * restore it, which would cascade into every later test. */ @@ -806,9 +828,8 @@ TEST_CASE("BLE Log runtime survives deinit racing submissions", bool recovered = ble_log_init(); reinit_ok = reinit_ok && recovered; - TEST_ASSERT_TRUE_MESSAGE( - __atomic_load_n(&ctx->exited, __ATOMIC_ACQUIRE), - "Writer task did not exit after stop"); + TEST_ASSERT_TRUE_MESSAGE(writer_exited, + "Writer task did not exit after stop"); TEST_ASSERT_TRUE_MESSAGE(reinit_ok, "BLE Log re-init failed during the race"); TEST_ASSERT_GREATER_THAN_UINT32_MESSAGE( RT_DEINIT_ROUNDS, ctx->attempts, diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/README.md b/components/bt/common/ble_log/test_apps/ble_log_test/README.md index 251e6e27533..47b48cf12a2 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/README.md +++ b/components/bt/common/ble_log/test_apps/ble_log_test/README.md @@ -7,12 +7,15 @@ | Supported Targets | | ----------------- | -This test app verifies the BLE Log runtime behaviour on target, using the -in-memory test peripheral (`CONFIG_BLE_LOG_PRPH_TEST=y`) to capture the -transport stream written by the runtime dispatch hook. +This app uses the in-memory test peripheral (`CONFIG_BLE_LOG_PRPH_TEST=y`) to +validate the BLE Log transport on target. -Currently covered: +It covers: -- `BLE_LOG_INT_SRC_VERSION_INFO` frame: BLE Log version, ESP-IDF build commit, - controller lib commit, btdm_common lib commit, BLE Mesh and BLE Audio lib - commits, chip model and chip revision +- literal protocol-v7 framing and fixed Internal Snapshot ABI; +- build, library, chip, and protocol versions inside the snapshot; +- task and `NON_YIELD` source metadata plus HCI direction encoding; +- direct compression claim/commit, stale handles, and per-source serialization; +- oversized-record rejection, flush sequence continuity, pool exhaustion, and non-yield reserve use; +- periodic snapshot busy/loss behavior; +- enable, disable, parked-writer, and deinit races. diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c index 3437fed856c..2642e629c31 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c @@ -10,7 +10,7 @@ #include "unity_test_runner.h" #include "ble_log.h" -#include "ble_log_lbm.h" +#include "ble_log_lbm_v2.h" #include "test_ble_log_main.h" bool test_ble_log_walk_frames(const uint8_t *data, size_t len, @@ -35,8 +35,10 @@ bool test_ble_log_walk_frames(const uint8_t *data, size_t len, } if (observer) { + uint8_t source_meta = head.frame_meta & 0xff; test_ble_log_frame_t frame = { - .src = head.frame_meta & 0xff, + .src = BLE_LOG_SRC_ID(source_meta), + .source_meta = source_meta, .sn = head.frame_meta >> 8, .payload = data + offset + BLE_LOG_FRAME_HEAD_LEN, .payload_len = head.length, diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c index 6f1ecb2e182..9fb3d2167d6 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c @@ -16,9 +16,13 @@ #include "unity.h" #include "ble_log.h" +#include "ble_log_lbm_v2.h" #include "ble_log_prph_test.h" #include "ble_log_rt.h" #include "test_ble_log_main.h" +#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE +#include "log_compression/utils.h" +#endif #if !CONFIG_BLE_LOG_PRPH_TEST #error "BLE Log test app requires CONFIG_BLE_LOG_PRPH_TEST" @@ -35,6 +39,8 @@ #define TEST_READ_BUF_SIZE (4096) #define TEST_READER_STACK_SIZE (3072) #define TEST_READER_PRIO (2) +#define TEST_LIFECYCLE_STACK_SIZE (2048) +#define TEST_LIFECYCLE_PRIO (3) typedef struct { size_t version_info_count; @@ -49,6 +55,65 @@ typedef struct { } reader_ctx_t; static uint8_t s_read_buf[TEST_READ_BUF_SIZE]; +static bool s_claim_hook_armed; +static uint32_t s_stale_claim_handle; +static volatile bool s_enable_hook_armed; +static SemaphoreHandle_t s_enable_hook_entered; +static SemaphoreHandle_t s_enable_hook_continue; +static volatile bool s_disable_hook_armed; +static SemaphoreHandle_t s_disable_hook_entered; +static SemaphoreHandle_t s_disable_hook_continue; +#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE +static volatile bool s_compression_hook_armed; +static SemaphoreHandle_t s_compression_hook_entered; +static SemaphoreHandle_t s_compression_hook_continue; +#endif + +void ble_log_test_claim_pre_publish_hook(void); +void ble_log_test_enable_before_lifecycle_lock_hook(void); +void ble_log_test_disable_before_wake_hook(void); +#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE +void ble_log_test_compression_after_lock_hook(uint8_t source); +extern int ble_log_compressed_hex_print(uint8_t source, uint32_t log_index, + size_t args_cnt, ...); +#endif + +void ble_log_test_claim_pre_publish_hook(void) +{ + if (s_claim_hook_armed) { + /* A valid length exercises the state check on the still-OPEN + * transport: a deleted check would frame stale claim metadata + * (duplicate SN) on the wire. */ + ble_log_commit(s_stale_claim_handle, 1); + } +} + +void ble_log_test_enable_before_lifecycle_lock_hook(void) +{ + if (s_enable_hook_armed) { + xSemaphoreGive(s_enable_hook_entered); + xSemaphoreTake(s_enable_hook_continue, portMAX_DELAY); + } +} + +void ble_log_test_disable_before_wake_hook(void) +{ + if (s_disable_hook_armed) { + xSemaphoreGive(s_disable_hook_entered); + xSemaphoreTake(s_disable_hook_continue, portMAX_DELAY); + } +} + +#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE +void ble_log_test_compression_after_lock_hook(uint8_t source) +{ + if (s_compression_hook_armed && + source == BLE_COMPRESSED_LOG_OUT_SOURCE_HOST) { + xSemaphoreGive(s_compression_hook_entered); + xSemaphoreTake(s_compression_hook_continue, portMAX_DELAY); + } +} +#endif /* A commit field is hex characters, zero-padded after a shorter value; * anything else (garbage, non-hex, zeros after data) is invalid. */ @@ -83,14 +148,19 @@ static bool commit_is_zero(const uint8_t *commit, size_t len) static void capture_version_info_frame(const test_ble_log_frame_t *frame, void *ctx) { version_capture_t *capture = ctx; - /* Every frame payload starts with a 4-byte timestamp prefix */ + if (frame->payload_len < sizeof(uint32_t)) { + return; + } const uint8_t *record = frame->payload + sizeof(uint32_t); size_t record_len = frame->payload_len - sizeof(uint32_t); if (frame->src == BLE_LOG_SRC_INTERNAL && - record_len == sizeof(ble_log_version_info_t) && - record[0] == BLE_LOG_INT_SRC_VERSION_INFO) { - memcpy(&capture->version_info, record, sizeof(capture->version_info)); + record_len == sizeof(ble_log_internal_snapshot_t) && + record[0] == BLE_LOG_INT_SRC_SNAPSHOT) { + ble_log_internal_snapshot_t snapshot; + memcpy(&snapshot, record, sizeof(snapshot)); + memcpy(&capture->version_info, &snapshot.version_info, + sizeof(capture->version_info)); capture->version_info_count++; } } @@ -114,6 +184,72 @@ static void test_reader_task(void *arg) vTaskDelete(NULL); } +typedef struct { + size_t count; + test_ble_log_frame_t frame; +} golden_capture_t; + +static void capture_golden_frame(const test_ble_log_frame_t *frame, void *ctx) +{ + golden_capture_t *capture = ctx; + capture->frame = *frame; + capture->count++; +} + +TEST_CASE("BLE Log v7 framing matches golden bytes", "[ble_log][wire]") +{ + static const uint8_t golden_frame[] = { + 0x05, 0x00, 0x87, 0xde, 0xc0, 0x00, + 0x78, 0x56, 0x34, 0x12, 0xab, + 0xf1, 0x12, 0x54, 0x88, + }; + static const uint8_t golden_payload[] = { + 0x78, 0x56, 0x34, 0x12, 0xab, + }; + + TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_VERSION); + TEST_ASSERT_EQUAL_HEX8(0x80, BLE_LOG_SRC_FLAG_NON_YIELD); + TEST_ASSERT_EQUAL_UINT8(1, BLE_LOG_SRC_CORE_FIRST); + TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_SRC_CORE_COUNT); + TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_SRC_ENCODE); + TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_INT_SRC_VERSION_INFO); + TEST_ASSERT_EQUAL_UINT8(8, BLE_LOG_INT_SRC_SNAPSHOT); + TEST_ASSERT_EQUAL_size_t(8, sizeof(ble_log_source_stat_t)); + TEST_ASSERT_EQUAL_size_t(134, sizeof(ble_log_internal_snapshot_t)); + TEST_ASSERT_EQUAL_size_t( + 4, offsetof(ble_log_internal_snapshot_t, version_info)); + TEST_ASSERT_EQUAL_size_t( + 62, offsetof(ble_log_internal_snapshot_t, ts.lc_ts)); + TEST_ASSERT_EQUAL_size_t( + 66, offsetof(ble_log_internal_snapshot_t, ts.esp_ts)); + TEST_ASSERT_EQUAL_size_t( + 70, offsetof(ble_log_internal_snapshot_t, ts.os_ts)); + TEST_ASSERT_EQUAL_size_t( + 78, offsetof(ble_log_internal_snapshot_t, stats)); + TEST_ASSERT_EQUAL_UINT32(148, BLE_LOG_INTERNAL_FRAME_LEN); +#if CONFIG_BLE_LOG_LL_ENABLED + TEST_ASSERT_EQUAL_UINT8(4, BLE_LOG_LL_FLAG_HCI); + TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_LL_FLAG_HCI_UPSTREAM); +#endif + TEST_ASSERT_EQUAL_HEX32( + 0x00c0de87, BLE_LOG_MAKE_FRAME_META(0x87, 0x00c0de)); + TEST_ASSERT_EQUAL_HEX32( + 0x00000007, BLE_LOG_MAKE_FRAME_META(0x07, 0x01000000)); + + golden_capture_t capture = {0}; + TEST_ASSERT_TRUE(test_ble_log_walk_frames(golden_frame, + sizeof(golden_frame), + capture_golden_frame, + &capture)); + TEST_ASSERT_EQUAL_size_t(1, capture.count); + TEST_ASSERT_EQUAL_UINT8(BLE_LOG_SRC_ENCODE, capture.frame.src); + TEST_ASSERT_EQUAL_HEX8(0x87, capture.frame.source_meta); + TEST_ASSERT_EQUAL_HEX32(0x00c0de, capture.frame.sn); + TEST_ASSERT_EQUAL_size_t(sizeof(golden_payload), capture.frame.payload_len); + TEST_ASSERT_EQUAL_MEMORY(golden_payload, capture.frame.payload, + sizeof(golden_payload)); +} + TEST_CASE("BLE Log runtime hook reports build and chip versions", "[ble_log]") { static const uint8_t payload[TEST_PAYLOAD_LEN] = {0}; @@ -128,12 +264,8 @@ TEST_CASE("BLE Log runtime hook reports build and chip versions", "[ble_log]") TEST_READER_PRIO, &reader)); TEST_ASSERT_TRUE(ble_log_enable(true)); - /* Transports are auto-submitted once full, which arms the defer alarm; - * after the throttle window elapses, a hook pass writes the version frame - * into the LBM and a later transport carries it out. ble_log_flush() - * cannot be used here: it disables the module while waiting for the - * transports to drain, so the hook frame written during the flush window - * would be dropped. */ + /* A hook pass submits one consolidated Internal Snapshot containing the + * version, statistics, utilization, and optional TS sample. */ for (int round = 0; round < TEST_MAX_ROUNDS && ctx.capture.version_info_count == 0; round++) { vTaskDelay(pdMS_TO_TICKS(TEST_HOOK_SETTLE_MS)); for (int i = 0; i < TEST_WRITES_PER_ROUND; i++) { @@ -187,3 +319,765 @@ TEST_CASE("BLE Log runtime hook reports build and chip versions", "[ble_log]") vSemaphoreDelete(ctx.done); } + +typedef struct { + bool task_frame; + bool non_yield_frame; + bool hci_downstream_frame; + bool hci_upstream_frame; + bool claimed_frame; + bool stale_protected_frame; + int encode_frame_cnt; +} frame_meta_capture_t; + +static void capture_frame_meta(const test_ble_log_frame_t *frame, void *ctx) +{ + frame_meta_capture_t *capture = ctx; + if (frame->src == BLE_LOG_SRC_ENCODE) { + capture->encode_frame_cnt++; + } + if (frame->payload_len < sizeof(uint32_t) + 1) { + return; + } + + uint8_t marker = frame->payload[sizeof(uint32_t)]; + if (frame->src == BLE_LOG_SRC_CUSTOM && marker == 0x11) { + capture->task_frame = !BLE_LOG_SRC_IS_NON_YIELD(frame->source_meta); + } else if (frame->src == BLE_LOG_SRC_CUSTOM && marker == 0x22) { + capture->non_yield_frame = + BLE_LOG_SRC_IS_NON_YIELD(frame->source_meta); + } else if (frame->src == BLE_LOG_SRC_HCI && marker == 0x01) { + capture->hci_downstream_frame = true; + } else if (frame->src == BLE_LOG_SRC_HCI && marker == 0x82) { + capture->hci_upstream_frame = true; + } else if (frame->src == BLE_LOG_SRC_ENCODE && marker == 0x33) { + capture->claimed_frame = true; + } else if (frame->src == BLE_LOG_SRC_ENCODE && marker == 0x44) { + capture->stale_protected_frame = true; + } +} + +TEST_CASE("BLE Log marks non-yield context and commits claimed payload", + "[ble_log][lbm]") +{ + const uint8_t task_marker = 0x11; + const uint8_t critical_marker = 0x22; + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &task_marker, sizeof(task_marker))); + + portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED; + portENTER_CRITICAL(&mux); + bool critical_written = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &critical_marker, + sizeof(critical_marker)); + portEXIT_CRITICAL(&mux); + TEST_ASSERT_TRUE(critical_written); + + uint8_t hci_type = 0x81; + ble_log_write_hci(BLE_LOG_HCI_DOWNSTREAM, &hci_type, sizeof(hci_type)); + TEST_ASSERT_EQUAL_HEX8(0x81, hci_type); + hci_type = 0x02; + ble_log_write_hci(BLE_LOG_HCI_UPSTREAM, &hci_type, sizeof(hci_type)); + TEST_ASSERT_EQUAL_HEX8(0x02, hci_type); + /* The HCI macro requires one writable type byte. Exercise invalid public + * input at the validating API instead. */ + TEST_ASSERT_FALSE(ble_log_write_hex(BLE_LOG_SRC_HCI, NULL, 1)); + + uint32_t handle; + uint8_t *claimed = ble_log_claim(BLE_LOG_SRC_ENCODE, 8, &handle); + TEST_ASSERT_NOT_NULL(claimed); + claimed[0] = 0x33; + ble_log_commit(handle, 1); + + /* Invoke the stale commit immediately before the next claim publishes + * CLAIMED. It must see the prior OPEN state and leave the new claim + * intact. */ + s_stale_claim_handle = handle; + s_claim_hook_armed = true; + uint32_t fresh_handle; + claimed = ble_log_claim(BLE_LOG_SRC_ENCODE, 8, &fresh_handle); + s_claim_hook_armed = false; + TEST_ASSERT_NOT_NULL(claimed); + TEST_ASSERT_NOT_EQUAL(handle, fresh_handle); + /* The fresh claim is published: the same stale handle must now be + * rejected by the generation check instead. A zero length mirrors a + * stale abort, which would otherwise consume the fresh claim. */ + ble_log_commit(s_stale_claim_handle, 0); + claimed[0] = 0x44; + ble_log_commit(fresh_handle, 1); + + ble_log_lbm_flush_open_transports(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + + frame_meta_capture_t capture = {0}; + for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT; i++) { + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + TEST_ASSERT_TRUE(test_ble_log_walk_frames(s_read_buf, len, + capture_frame_meta, + &capture)); + } + + TEST_ASSERT_TRUE(capture.task_frame); + TEST_ASSERT_TRUE(capture.non_yield_frame); + TEST_ASSERT_TRUE(capture.hci_downstream_frame); + TEST_ASSERT_TRUE(capture.hci_upstream_frame); + TEST_ASSERT_TRUE(capture.claimed_frame); + TEST_ASSERT_TRUE(capture.stale_protected_frame); + /* Exactly the two committed claims: a stale commit that slipped past + * the state or generation check would add or consume an ENCODE frame. */ + TEST_ASSERT_EQUAL(2, capture.encode_frame_cnt); +} + +#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE +#define TEST_CP_INDEX_FIRST UINT16_C(0x601) +#define TEST_CP_INDEX_DROPPED UINT16_C(0x602) +#define TEST_CP_INDEX_SECOND UINT16_C(0x603) +#define TEST_CP_TASK_SWITCH UINT8_C(0xc2) +#define TEST_CP_ZERO_ARGS UINT8_C(0x40) + +typedef struct { + bool first_found; + uint32_t first_sn; + bool dropped_found; + bool second_found; + bool second_switched; + uint32_t second_sn; + bool third_found; + bool third_switched; + uint32_t third_sn; +} compression_capture_t; + +typedef struct { + SemaphoreHandle_t committed; + SemaphoreHandle_t exit; + SemaphoreHandle_t exited; +} compression_writer_ctx_t; + +static void capture_compressed_frame(const test_ble_log_frame_t *frame, + void *ctx) +{ + compression_capture_t *capture = ctx; + if (frame->src != BLE_LOG_SRC_ENCODE || + frame->payload_len < sizeof(uint32_t) + 4) { + return; + } + + const uint8_t *record = frame->payload + sizeof(uint32_t); + size_t offset = 0; + if (record[offset++] != BLE_COMPRESSED_LOG_OUT_SOURCE_HOST) { + return; + } + size_t record_len = frame->payload_len - sizeof(uint32_t); + bool switched = record[offset] == TEST_CP_TASK_SWITCH; + offset += switched; + if (record_len - offset < 3 || record[offset++] != TEST_CP_ZERO_ARGS) { + return; + } + uint16_t log_index; + memcpy(&log_index, record + offset, sizeof(log_index)); + + if (log_index == TEST_CP_INDEX_FIRST) { + capture->first_found = true; + capture->first_sn = frame->sn; + } else if (log_index == TEST_CP_INDEX_DROPPED) { + capture->dropped_found = true; + } else if (log_index == TEST_CP_INDEX_SECOND) { + capture->second_found = true; + capture->second_switched = switched; + capture->second_sn = frame->sn; + } else if (log_index == TEST_CP_INDEX_SECOND + 1) { + capture->third_found = true; + capture->third_switched = switched; + capture->third_sn = frame->sn; + } +} + +static void compression_writer_task(void *arg) +{ + compression_writer_ctx_t *ctx = arg; + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, + TEST_CP_INDEX_SECOND, 0); + xSemaphoreGive(ctx->committed); + xSemaphoreTake(ctx->exit, portMAX_DELAY); + xSemaphoreGive(ctx->exited); + vTaskDelete(NULL); +} + +TEST_CASE("BLE Log serializes task context per compression source", + "[ble_log][compression]") +{ + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_transports(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, + TEST_CP_INDEX_FIRST, 0); + + s_compression_hook_entered = xSemaphoreCreateBinary(); + s_compression_hook_continue = xSemaphoreCreateBinary(); + compression_writer_ctx_t writer = { + .committed = xSemaphoreCreateBinary(), + .exit = xSemaphoreCreateBinary(), + .exited = xSemaphoreCreateBinary(), + }; + TEST_ASSERT_NOT_NULL(s_compression_hook_entered); + TEST_ASSERT_NOT_NULL(s_compression_hook_continue); + TEST_ASSERT_NOT_NULL(writer.committed); + TEST_ASSERT_NOT_NULL(writer.exit); + TEST_ASSERT_NOT_NULL(writer.exited); + + s_compression_hook_armed = true; + TEST_ASSERT_EQUAL( + pdTRUE, + xTaskCreate(compression_writer_task, "ble_log_cp", + TEST_LIFECYCLE_STACK_SIZE, &writer, + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_TRUE(xSemaphoreTake(s_compression_hook_entered, + pdMS_TO_TICKS(1000))); + + /* This call claims pool space but fails the per-source trylock. It must + * cancel immediately, count one lost ENCODE SN, and leave task state to + * the lock owner. */ + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, + TEST_CP_INDEX_DROPPED, 0); + xSemaphoreGive(s_compression_hook_continue); + TEST_ASSERT_TRUE(xSemaphoreTake(writer.committed, + pdMS_TO_TICKS(1000))); + s_compression_hook_armed = false; + + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, + TEST_CP_INDEX_SECOND + 1, 0); + xSemaphoreGive(writer.exit); + TEST_ASSERT_TRUE(xSemaphoreTake(writer.exited, pdMS_TO_TICKS(1000))); + + ble_log_lbm_flush_open_transports(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + compression_capture_t capture = {0}; + for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT; i++) { + size_t len = ble_log_prph_test_read( + s_read_buf, sizeof(s_read_buf), pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + TEST_ASSERT_TRUE(test_ble_log_walk_frames( + s_read_buf, len, capture_compressed_frame, &capture)); + } + + TEST_ASSERT_TRUE(capture.first_found); + TEST_ASSERT_FALSE(capture.dropped_found); + TEST_ASSERT_TRUE(capture.second_found); + TEST_ASSERT_TRUE(capture.second_switched); + TEST_ASSERT_TRUE(capture.third_found); + TEST_ASSERT_TRUE(capture.third_switched); + /* The blocked writer claims its SN before taking the compression lock; + * the rejected contender burns the following SN. */ + TEST_ASSERT_EQUAL_HEX32((capture.first_sn + 1) & 0x00ffffffU, + capture.second_sn); + TEST_ASSERT_EQUAL_HEX32((capture.second_sn + 2) & 0x00ffffffU, + capture.third_sn); + + vSemaphoreDelete(writer.committed); + vSemaphoreDelete(writer.exit); + vSemaphoreDelete(writer.exited); + vSemaphoreDelete(s_compression_hook_entered); + vSemaphoreDelete(s_compression_hook_continue); + s_compression_hook_entered = NULL; + s_compression_hook_continue = NULL; +} +#endif /* CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE */ + +typedef struct { + ble_log_src_t src; + uint8_t marker; + bool found; + uint32_t sn; +} sequence_capture_t; + +typedef struct { + sequence_capture_t *captures; + size_t count; +} sequence_capture_set_t; + +static void capture_sequence_frame(const test_ble_log_frame_t *frame, void *ctx) +{ + sequence_capture_set_t *set = ctx; + bool is_ll = frame->src == BLE_LOG_SRC_LL_TASK || + frame->src == BLE_LOG_SRC_LL_ISR; + size_t marker_offset = is_ll ? 0 : sizeof(uint32_t); + /* LL payloads already carry the controller's LC timestamp; unlike the + * other sources, they must not gain an ESP timestamp prefix. */ + if (frame->payload_len != marker_offset + 1) { + return; + } + + uint8_t marker = frame->payload[marker_offset]; + for (size_t i = 0; i < set->count; i++) { + sequence_capture_t *capture = &set->captures[i]; + if (frame->src == capture->src && marker == capture->marker) { + capture->found = true; + capture->sn = frame->sn; + } + } +} + +static void read_sequence_frames(sequence_capture_t *captures, size_t count) +{ + sequence_capture_set_t set = { + .captures = captures, + .count = count, + }; + for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT; i++) { + size_t len = ble_log_prph_test_read( + s_read_buf, sizeof(s_read_buf), pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + TEST_ASSERT_TRUE(test_ble_log_walk_frames( + s_read_buf, len, capture_sequence_frame, &set)); + } +} + +static void auto_recycle_noop(void *ctx) +{ + (void)ctx; +} + +TEST_CASE("BLE Log preserves LL payload and rejects oversized records", + "[ble_log][lbm][wire]") +{ + enum { + CUSTOM_BEFORE, + CUSTOM_OVERSIZED, + CUSTOM_AFTER, +#if CONFIG_BLE_LOG_LL_ENABLED + LL_BEFORE, + LL_PRIMARY_OVERSIZED, + LL_APPEND_OVERSIZED, + LL_AFTER, +#endif + SEQUENCE_CAPTURE_COUNT, + }; + static uint8_t custom_oversized_payload[ + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t) + 1]; +#if CONFIG_BLE_LOG_LL_ENABLED + static uint8_t ll_oversized_payload[BLE_LOG_MAX_PAYLOAD_LEN + 1]; + static uint8_t ll_oversized_append[BLE_LOG_MAX_PAYLOAD_LEN]; +#endif + const uint8_t custom_before = 0x61; + const uint8_t custom_oversized = 0x62; + const uint8_t custom_after = 0x63; +#if CONFIG_BLE_LOG_LL_ENABLED + const uint8_t ll_before = 0x64; + const uint8_t ll_primary_oversized = 0x65; + const uint8_t ll_append_oversized = 0x66; + const uint8_t ll_after = 0x67; +#endif + sequence_capture_t captures[SEQUENCE_CAPTURE_COUNT] = { + [CUSTOM_BEFORE] = {.src = BLE_LOG_SRC_CUSTOM, .marker = custom_before}, + [CUSTOM_OVERSIZED] = { + .src = BLE_LOG_SRC_CUSTOM, + .marker = custom_oversized, + }, + [CUSTOM_AFTER] = {.src = BLE_LOG_SRC_CUSTOM, .marker = custom_after}, +#if CONFIG_BLE_LOG_LL_ENABLED + [LL_BEFORE] = {.src = BLE_LOG_SRC_LL_TASK, .marker = ll_before}, + [LL_PRIMARY_OVERSIZED] = { + .src = BLE_LOG_SRC_LL_TASK, + .marker = ll_primary_oversized, + }, + [LL_APPEND_OVERSIZED] = { + .src = BLE_LOG_SRC_LL_TASK, + .marker = ll_append_oversized, + }, + [LL_AFTER] = {.src = BLE_LOG_SRC_LL_TASK, .marker = ll_after}, +#endif + }; + + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_transports(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &custom_before, sizeof(custom_before))); + memset(custom_oversized_payload, 0, sizeof(custom_oversized_payload)); + custom_oversized_payload[0] = custom_oversized; + TEST_ASSERT_FALSE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + custom_oversized_payload, + sizeof(custom_oversized_payload))); + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &custom_after, sizeof(custom_after))); + +#if CONFIG_BLE_LOG_LL_ENABLED + ble_log_write_hex_ll(sizeof(ll_before), &ll_before, 0, NULL, + BIT(BLE_LOG_LL_FLAG_TASK)); + memset(ll_oversized_payload, 0, sizeof(ll_oversized_payload)); + ll_oversized_payload[0] = ll_primary_oversized; + ble_log_write_hex_ll(sizeof(ll_oversized_payload), ll_oversized_payload, + 0, NULL, BIT(BLE_LOG_LL_FLAG_TASK)); + memset(ll_oversized_append, 0, sizeof(ll_oversized_append)); + ble_log_write_hex_ll(sizeof(ll_append_oversized), &ll_append_oversized, + sizeof(ll_oversized_append), ll_oversized_append, + BIT(BLE_LOG_LL_FLAG_TASK)); + ble_log_write_hex_ll(sizeof(ll_after), &ll_after, 0, NULL, + BIT(BLE_LOG_LL_FLAG_TASK)); +#endif + + ble_log_lbm_flush_open_transports(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + read_sequence_frames(captures, SEQUENCE_CAPTURE_COUNT); + + TEST_ASSERT_TRUE(captures[CUSTOM_BEFORE].found); + TEST_ASSERT_FALSE(captures[CUSTOM_OVERSIZED].found); + TEST_ASSERT_TRUE(captures[CUSTOM_AFTER].found); + TEST_ASSERT_EQUAL_HEX32( + (captures[CUSTOM_BEFORE].sn + 2) & 0x00ffffffU, + captures[CUSTOM_AFTER].sn); +#if CONFIG_BLE_LOG_LL_ENABLED + TEST_ASSERT_TRUE(captures[LL_BEFORE].found); + TEST_ASSERT_FALSE(captures[LL_PRIMARY_OVERSIZED].found); + TEST_ASSERT_FALSE(captures[LL_APPEND_OVERSIZED].found); + TEST_ASSERT_TRUE(captures[LL_AFTER].found); + TEST_ASSERT_EQUAL_HEX32( + (captures[LL_BEFORE].sn + 3) & 0x00ffffffU, + captures[LL_AFTER].sn); +#endif +} + +TEST_CASE("BLE Log flush preserves source-local sequence continuity", + "[ble_log][wire]") +{ + const uint8_t before_marker = 0x71; + const uint8_t after_marker = 0x72; + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_transports(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &before_marker, sizeof(before_marker))); + ble_log_lbm_flush_open_transports(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + sequence_capture_t before = { + .src = BLE_LOG_SRC_CUSTOM, + .marker = before_marker, + }; + read_sequence_frames(&before, 1); + TEST_ASSERT_TRUE(before.found); + + /* The test peripheral normally recycles only when read. Auto-recycle lets + * synchronous flush observe completion without a second reader task. */ + ble_log_prph_test_set_auto_recycle_hook(auto_recycle_noop, NULL); + ble_log_flush(); + ble_log_prph_test_set_auto_recycle_hook(NULL, NULL); + + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &after_marker, sizeof(after_marker))); + ble_log_lbm_flush_open_transports(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + sequence_capture_t after = { + .src = BLE_LOG_SRC_CUSTOM, + .marker = after_marker, + }; + read_sequence_frames(&after, 1); + TEST_ASSERT_TRUE(after.found); + TEST_ASSERT_EQUAL_HEX32((before.sn + 1) & 0x00ffffffU, after.sn); +} + +#define SNAPSHOT_CAPTURE_MAX 8 + +typedef struct { + uint32_t sn[SNAPSHOT_CAPTURE_MAX]; + int count; +} snapshot_capture_t; + +static void capture_snapshot_loss(const test_ble_log_frame_t *frame, void *ctx) +{ + snapshot_capture_t *capture = ctx; + if (frame->src != BLE_LOG_SRC_INTERNAL || + frame->payload_len != sizeof(uint32_t) + + sizeof(ble_log_internal_snapshot_t)) { + return; + } + + ble_log_internal_snapshot_t snapshot; + memcpy(&snapshot, frame->payload + sizeof(uint32_t), sizeof(snapshot)); + if (snapshot.int_src_code == BLE_LOG_INT_SRC_SNAPSHOT && + (snapshot.reason_flags & BLE_LOG_SNAPSHOT_REASON_PERIODIC) && + capture->count < SNAPSHOT_CAPTURE_MAX) { + capture->sn[capture->count++] = frame->sn; + } +} + +TEST_CASE("BLE Log periodic snapshot fails fast while its transport is busy", + "[ble_log][lbm]") +{ + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_transports(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + TEST_ASSERT_TRUE(ble_log_enable(false)); + TEST_ASSERT_FALSE(ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC, NULL, false)); + TEST_ASSERT_TRUE(ble_log_enable(true)); + + TEST_ASSERT_TRUE(ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC, NULL, false)); + TEST_ASSERT_FALSE(ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC, NULL, false)); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + TEST_ASSERT_GREATER_THAN_size_t( + 0, ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), 0, NULL)); + + TEST_ASSERT_TRUE(ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC, NULL, false)); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + + snapshot_capture_t capture = {0}; + for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT; i++) { + size_t len = ble_log_prph_test_read( + s_read_buf, sizeof(s_read_buf), pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + TEST_ASSERT_TRUE(test_ble_log_walk_frames( + s_read_buf, len, capture_snapshot_loss, &capture)); + } + /* The busy periodic attempt burned one snapshot SN: the gap in the + * snapshot sequence is the loss signal. */ + TEST_ASSERT_GREATER_OR_EQUAL_INT(2, capture.count); + bool gap_seen = false; + for (int i = 1; i < capture.count; i++) { + if (((capture.sn[i] - capture.sn[i - 1]) & 0x00ffffffU) >= 2) { + gap_seen = true; + } + } + TEST_ASSERT_TRUE(gap_seen); +} + +#if CONFIG_BLE_LOG_LL_ENABLED +typedef struct { + SemaphoreHandle_t started; + SemaphoreHandle_t done; +} waiter_ctx_t; + +typedef struct { + SemaphoreHandle_t done; + bool result; +} enable_ctx_t; + +typedef struct { + SemaphoreHandle_t started; + SemaphoreHandle_t done; +} deinit_ctx_t; + +static void blocked_ll_writer_task(void *arg) +{ + waiter_ctx_t *ctx = arg; + static const uint8_t marker = 0x55; + xSemaphoreGive(ctx->started); + ble_log_write_hex_ll(sizeof(marker), &marker, 0, NULL, + BIT(BLE_LOG_LL_FLAG_TASK)); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +static void racing_enable_task(void *arg) +{ + enable_ctx_t *ctx = arg; + ctx->result = ble_log_enable(true); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +static void racing_disable_task(void *arg) +{ + enable_ctx_t *ctx = arg; + ctx->result = ble_log_enable(false); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +static void lifecycle_deinit_task(void *arg) +{ + deinit_ctx_t *ctx = arg; + xSemaphoreGive(ctx->started); + ble_log_deinit(); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +TEST_CASE("BLE Log deinit closes a parked LL writer before racing enable", + "[ble_log][lbm]") +{ + static const uint8_t full_payload[ + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t)] = {0}; + + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_transports(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), 0, 0, NULL) > 0) { + } + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), 0, 0, NULL) > 0) { + } + + /* Keep every task-usable transport SENDING in the test peripheral. */ + for (int i = 0; i < BLE_LOG_POOL_SHARED_CNT; i++) { + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, full_payload, + sizeof(full_payload))); + } + static const uint8_t reserve_marker = 0x56; + TEST_ASSERT_FALSE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &reserve_marker, + sizeof(reserve_marker))); + uint32_t exhausted_handle; + TEST_ASSERT_NULL(ble_log_claim(BLE_LOG_SRC_ENCODE, 1, + &exhausted_handle)); + + portMUX_TYPE reserve_mux = portMUX_INITIALIZER_UNLOCKED; + portENTER_CRITICAL(&reserve_mux); + bool reserve_written = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &reserve_marker, + sizeof(reserve_marker)); + portEXIT_CRITICAL(&reserve_mux); + TEST_ASSERT_TRUE(reserve_written); + + waiter_ctx_t waiter = { + .started = xSemaphoreCreateBinary(), + .done = xSemaphoreCreateBinary(), + }; + TEST_ASSERT_NOT_NULL(waiter.started); + TEST_ASSERT_NOT_NULL(waiter.done); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(blocked_ll_writer_task, "ble_log_wait", + TEST_LIFECYCLE_STACK_SIZE, &waiter, + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_TRUE(xSemaphoreTake(waiter.started, pdMS_TO_TICKS(1000))); + vTaskDelay(1); + TEST_ASSERT_EQUAL(pdFALSE, xSemaphoreTake(waiter.done, 0)); + + s_enable_hook_entered = xSemaphoreCreateBinary(); + s_enable_hook_continue = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(s_enable_hook_entered); + TEST_ASSERT_NOT_NULL(s_enable_hook_continue); + s_enable_hook_armed = true; + + enable_ctx_t enabler = { + .done = xSemaphoreCreateBinary(), + }; + TEST_ASSERT_NOT_NULL(enabler.done); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(racing_enable_task, "ble_log_enable", + TEST_LIFECYCLE_STACK_SIZE, &enabler, + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_TRUE(xSemaphoreTake(s_enable_hook_entered, + pdMS_TO_TICKS(1000))); + + /* The enable task is paused immediately before the lifecycle lock. Close + * and destroy the module, then let enable perform its locked recheck. */ + ble_log_deinit(); + TEST_ASSERT_TRUE(xSemaphoreTake(waiter.done, pdMS_TO_TICKS(1000))); + xSemaphoreGive(s_enable_hook_continue); + TEST_ASSERT_TRUE(xSemaphoreTake(enabler.done, pdMS_TO_TICKS(1000))); + s_enable_hook_armed = false; + TEST_ASSERT_FALSE(enabler.result); + + vSemaphoreDelete(waiter.started); + vSemaphoreDelete(waiter.done); + vSemaphoreDelete(enabler.done); + vSemaphoreDelete(s_enable_hook_entered); + vSemaphoreDelete(s_enable_hook_continue); + s_enable_hook_entered = NULL; + s_enable_hook_continue = NULL; + + TEST_ASSERT_TRUE(ble_log_init()); +} + +TEST_CASE("BLE Log disable keeps waiter semaphore alive during deinit", + "[ble_log][lbm]") +{ + TEST_ASSERT_TRUE(ble_log_enable(true)); + s_disable_hook_entered = xSemaphoreCreateBinary(); + s_disable_hook_continue = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(s_disable_hook_entered); + TEST_ASSERT_NOT_NULL(s_disable_hook_continue); + s_disable_hook_armed = true; + + enable_ctx_t disabler = { + .done = xSemaphoreCreateBinary(), + }; + deinit_ctx_t deinit = { + .started = xSemaphoreCreateBinary(), + .done = xSemaphoreCreateBinary(), + }; + TEST_ASSERT_NOT_NULL(disabler.done); + TEST_ASSERT_NOT_NULL(deinit.started); + TEST_ASSERT_NOT_NULL(deinit.done); + + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(racing_disable_task, "ble_log_disable", + TEST_LIFECYCLE_STACK_SIZE, &disabler, + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_TRUE(xSemaphoreTake(s_disable_hook_entered, + pdMS_TO_TICKS(1000))); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(lifecycle_deinit_task, "ble_log_deinit", + TEST_LIFECYCLE_STACK_SIZE, &deinit, + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_TRUE(xSemaphoreTake(deinit.started, pdMS_TO_TICKS(1000))); + TEST_ASSERT_EQUAL(pdFALSE, + xSemaphoreTake(deinit.done, pdMS_TO_TICKS(20))); + + xSemaphoreGive(s_disable_hook_continue); + TEST_ASSERT_TRUE(xSemaphoreTake(disabler.done, pdMS_TO_TICKS(1000))); + TEST_ASSERT_TRUE(xSemaphoreTake(deinit.done, pdMS_TO_TICKS(1000))); + s_disable_hook_armed = false; + TEST_ASSERT_TRUE(disabler.result); + + vSemaphoreDelete(disabler.done); + vSemaphoreDelete(deinit.started); + vSemaphoreDelete(deinit.done); + vSemaphoreDelete(s_disable_hook_entered); + vSemaphoreDelete(s_disable_hook_continue); + s_disable_hook_entered = NULL; + s_disable_hook_continue = NULL; + + TEST_ASSERT_TRUE(ble_log_init()); +} +#endif /* CONFIG_BLE_LOG_LL_ENABLED */ From 7c2d698baa13905d39115e2eb9bf46adf314e727 Mon Sep 17 00:00:00 2001 From: Zhou Xiao Date: Mon, 31 Aug 2026 17:57:26 +0800 Subject: [PATCH 04/13] refactor(ble_log): extract the UART redirection stream writer Move the stream write interface (redir_get_trans / redir_seal / stream_write / stream_flush) out of ble_log_lbm_v2.c into a dedicated ble_log_redir.c with its own internal header, matching the rt/ts module layout. The pool keeps the recycle inflight hook and the flush hooks. The REDIR console stream keeps its own 24-bit frame sequence in ble_log_redir_t: a raw console stream is not a log attempt, so it never consumes the Global SN (which would fake loss gaps). g_frame_sn stays TU-local to ble_log_lbm_v2.c, and the redundant modulo masks are dropped: BLE_LOG_MAKE_FRAME_META enforces the 24-bit wire field at the packing site, so BLE_LOG_SN_MODULO is removed. REDIR payloads are a raw console stream with no timestamp prefix: a per-frame ESP timer timestamp anchored at most ~1s of aggregated text, cost a breaking wire change, and had no consumer on the receiver side. Alignment with the core timeline uses receiver arrival time (bounded by the periodic redirection flush). --- components/bt/common/CMakeLists.txt | 1 + components/bt/common/ble_log/README.md | 26 ++-- .../bt/common/ble_log/src/ble_log_lbm_v2.c | 124 +---------------- .../bt/common/ble_log/src/ble_log_redir.c | 126 ++++++++++++++++++ .../src/internal_include/ble_log_lbm_v2.h | 28 ++-- .../src/internal_include/ble_log_redir.h | 21 +++ .../ble_log/src/prph/ble_log_prph_uart_dma.c | 4 +- 7 files changed, 183 insertions(+), 147 deletions(-) create mode 100644 components/bt/common/ble_log/src/ble_log_redir.c create mode 100644 components/bt/common/ble_log/src/internal_include/ble_log_redir.h diff --git a/components/bt/common/CMakeLists.txt b/components/bt/common/CMakeLists.txt index 1c39e904b4d..7688210c025 100644 --- a/components/bt/common/CMakeLists.txt +++ b/components/bt/common/CMakeLists.txt @@ -171,6 +171,7 @@ if(CONFIG_BLE_LOG_ENABLED) list(APPEND bt_common_srcs "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log.c" "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_lbm_v2.c" + "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_redir.c" "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_rt.c" "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_util.c" ) diff --git a/components/bt/common/ble_log/README.md b/components/bt/common/ble_log/README.md index bc9b25f2058..ae23c710aba 100644 --- a/components/bt/common/ble_log/README.md +++ b/components/bt/common/ble_log/README.md @@ -57,16 +57,20 @@ The checksum is `ble_log_fast_checksum()` over the six-byte header and exactly `payload length` bytes. It excludes the checksum field and peripheral-only DMA padding. -Core frames begin their payload with: +Core frames (all sources except REDIR) begin their payload with: ```text [4-byte low32 esp_timer_get_time() microseconds][source payload] ``` -The timestamp is captured at API entry before any pool or UART0 redirection -mutex wait. It wraps approximately every 71.6 minutes and must be unwrapped -modulo 2^32 by the receiver. UART0 redirection retains its extension-specific -stream aggregation while using the same ESP Timer timestamp semantics. +The timestamp is captured at API entry before any pool mutex wait. It wraps +approximately every 71.6 minutes and must be unwrapped modulo 2^32 by the +receiver. + +UART0 redirection payload is the raw console stream with no timestamp +prefix: the redirection stream keeps its own frame sequence, and its +receiver-side arrival time (aggregation delay bounded by the periodic +redirection flush) is the alignment reference against the core timeline. ### Sources @@ -86,11 +90,13 @@ value directly, so existing decoders keep working: 8 REDIR extension ``` -All non-INTERNAL sources share one 24-bit Global SN: it is consumed at API -entry, so it totally orders log attempts — including equal-timestamp records -from different sources — and every lost or rejected attempt leaves a gap -in the sequence. Internal Snapshot frames carry their own separate -sequence; a gap there counts skipped snapshots. Neither sequence is reset. +Log sources except INTERNAL and REDIR share one 24-bit Global SN: it is +consumed at API entry, so it totally orders log attempts — including +equal-timestamp records from different sources — and every lost or rejected +attempt leaves a gap in the sequence. Internal Snapshot frames carry their +own separate sequence (a gap counts skipped snapshots), and the REDIR +console stream keeps its own sequence as well (a gap counts a dropped +console batch). None of these sequences is reset. Actual ISR and critical-section records carry `NON_YIELD` in source bit 7. Controller-side HCI records are not emitted by BLE Log, and the controller no diff --git a/components/bt/common/ble_log/src/ble_log_lbm_v2.c b/components/bt/common/ble_log/src/ble_log_lbm_v2.c index a52039fb04a..b7bc01735e3 100644 --- a/components/bt/common/ble_log/src/ble_log_lbm_v2.c +++ b/components/bt/common/ble_log/src/ble_log_lbm_v2.c @@ -83,10 +83,12 @@ BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t lbm_enabled = 0; BLE_LOG_STATIC volatile bool flush_in_progress = false; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_pool_t g_pool; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_stat_mgr_t stat_mgr_ctx[BLE_LOG_SRC_MAX]; -/* Global SN (all non-INTERNAL sources) and the separate Internal Snapshot - * sequence; see ble_log_lbm_v2.h. */ +/* Global SN (the log sources except INTERNAL and REDIR) and the separate + * Internal Snapshot sequence; see ble_log_lbm_v2.h. */ BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t g_frame_sn; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t g_snapshot_sn; +#define BLE_LOG_GET_GLOBAL_SN() BLE_LOG_GET_FRAME_SN(g_frame_sn) +#define BLE_LOG_GET_SNAPSHOT_SN() BLE_LOG_GET_FRAME_SN(g_snapshot_sn) BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_prph_trans_t *internal_trans; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_pool_claim_t pool_claim_ctx[BLE_LOG_POOL_TRANS_CNT]; BLE_LOG_STATIC ble_log_internal_snapshot_t internal_snapshot; @@ -123,9 +125,6 @@ BLE_LOG_STATIC void ble_log_stat_mgr_mark_lost(ble_log_src_t src_code); BLE_LOG_STATIC void ble_log_snapshot_stats(ble_log_source_stat_t *snapshots); BLE_LOG_STATIC bool ble_log_pool_flush_all_trans(void); BLE_LOG_STATIC void ble_log_pool_wake_all(void); -#if BLE_LOG_UART_REDIR_ENABLED -BLE_LOG_STATIC void ble_log_redir_seal(ble_log_prph_trans_t *trans, ble_log_src_t src_code); -#endif /* BLE_LOG_UART_REDIR_ENABLED */ /* ------------------------- */ /* BITMAP HELPERS */ @@ -571,121 +570,6 @@ void ble_log_commit(uint32_t handle, size_t actual_len) BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); } -#if BLE_LOG_UART_REDIR_ENABLED -/* ------------------------------------------------- */ -/* STREAM WRITE INTERFACE (UART redirection) */ -/* */ -/* Stream mode appends raw data into a transport */ -/* buffer with deferred frame encapsulation. */ -/* Redirection transports are single-writer under */ -/* redir->mutex; their only concurrent mutation is */ -/* the UART tx-done recycle (state SENDING -> FREE), */ -/* so state access uses atomics. */ -/* ------------------------------------------------- */ -BLE_LOG_STATIC -ble_log_prph_trans_t *ble_log_redir_get_trans(ble_log_redir_t *redir, - ble_log_src_t src_code) -{ - for (int i = 0; i < BLE_LOG_TRANS_BUF_CNT; i++) { - ble_log_prph_trans_t *trans = redir->trans[redir->trans_idx]; - if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(trans->state) != BLE_LOG_TRANS_STATE_SENDING) { - if (BLE_LOG_TRANS_FREE_SPACE(trans) >= BLE_LOG_FRAME_OVERHEAD) { - return trans; - } - if (trans->pos > BLE_LOG_FRAME_HEAD_LEN) { - ble_log_redir_seal(trans, src_code); - } - } - redir->trans_idx = (redir->trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); - } - return NULL; -} - -BLE_LOG_STATIC -void ble_log_redir_seal(ble_log_prph_trans_t *trans, ble_log_src_t src_code) -{ - if (trans->pos <= BLE_LOG_FRAME_HEAD_LEN) { - return; - } - - uint16_t payload_len = trans->pos - BLE_LOG_FRAME_HEAD_LEN; - /* REDIR frames take the Global SN at seal time; the stream has no - * core-stat slot. */ - uint32_t frame_sn = BLE_LOG_GET_GLOBAL_SN(); - ble_log_frame_head_t frame_head = { - .length = payload_len, - .frame_meta = BLE_LOG_MAKE_FRAME_META(src_code, frame_sn), - }; - BLE_LOG_MEMCPY(trans->buf, &frame_head, BLE_LOG_FRAME_HEAD_LEN); - - uint32_t checksum = ble_log_fast_checksum(trans->buf, trans->pos); - BLE_LOG_MEMCPY(trans->buf + trans->pos, &checksum, BLE_LOG_FRAME_TAIL_LEN); - trans->pos += BLE_LOG_FRAME_TAIL_LEN; - - ble_log_redir_t *redir = ble_log_prph_get_redir_lbm(); - BLE_LOG_ASSERT(redir); - uint32_t infl = __atomic_add_fetch(&redir->inflight, 1, __ATOMIC_RELAXED); - uint32_t peak = BLE_LOG_ATOMIC_LOAD_RELAXED(redir->inflight_peak); - while (infl > peak && - !__atomic_compare_exchange_n(&redir->inflight_peak, &peak, infl, true, - __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { - } - - BLE_LOG_ATOMIC_STORE_RELAXED(trans->state, BLE_LOG_TRANS_STATE_SENDING); - ble_log_rt_submit_trans(trans); -} - -void ble_log_lbm_stream_write(ble_log_redir_t *redir, ble_log_src_t src_code, - uint32_t timestamp, const uint8_t *data, size_t len) -{ - while (len > 0) { - ble_log_prph_trans_t *trans = ble_log_redir_get_trans(redir, src_code); - if (!trans) { - /* Burn one Global SN so the dropped console batch leaves a - * sequence gap. */ - (void)BLE_LOG_GET_GLOBAL_SN(); - return; - } - - if (trans->pos == 0) { - trans->pos = BLE_LOG_FRAME_HEAD_LEN; - BLE_LOG_MEMCPY(trans->buf + trans->pos, ×tamp, - sizeof(timestamp)); - trans->pos += sizeof(timestamp); - } - - uint16_t available = BLE_LOG_TRANS_FREE_SPACE(trans); - if (available <= BLE_LOG_FRAME_TAIL_LEN) { - ble_log_redir_seal(trans, src_code); - continue; - } - available -= BLE_LOG_FRAME_TAIL_LEN; - - size_t to_write = (len < available) ? len : available; - BLE_LOG_MEMCPY(trans->buf + trans->pos, data, to_write); - trans->pos += to_write; - data += to_write; - len -= to_write; - - if (BLE_LOG_TRANS_FREE_SPACE(trans) <= BLE_LOG_FRAME_OVERHEAD) { - ble_log_redir_seal(trans, src_code); - } - } -} - -void ble_log_lbm_stream_flush(ble_log_redir_t *redir, ble_log_src_t src_code) -{ - int trans_idx = redir->trans_idx; - for (int i = 0; i < BLE_LOG_TRANS_BUF_CNT; i++) { - ble_log_prph_trans_t *trans = redir->trans[trans_idx]; - if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(trans->state) != BLE_LOG_TRANS_STATE_SENDING && - trans->pos > BLE_LOG_FRAME_HEAD_LEN) { - ble_log_redir_seal(trans, src_code); - } - trans_idx = (trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); - } -} -#endif /* BLE_LOG_UART_REDIR_ENABLED */ BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void ble_log_stat_mgr_mark_lost(ble_log_src_t src_code) diff --git a/components/bt/common/ble_log/src/ble_log_redir.c b/components/bt/common/ble_log/src/ble_log_redir.c new file mode 100644 index 00000000000..baad2794791 --- /dev/null +++ b/components/bt/common/ble_log/src/ble_log_redir.c @@ -0,0 +1,126 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* ------------------------------------------------- */ +/* BLE Log - UART Redirection Stream Writer */ +/* */ +/* Stream mode appends raw data into a transport */ +/* buffer with deferred frame encapsulation. */ +/* Redirection transports are single-writer under */ +/* redir->mutex; their only concurrent mutation is */ +/* the UART tx-done recycle (state SENDING -> FREE), */ +/* so state access uses atomics. */ +/* ------------------------------------------------- */ + +/* INCLUDE */ +#include "ble_log_redir.h" +#include "ble_log_rt.h" + +#if BLE_LOG_UART_REDIR_ENABLED +BLE_LOG_STATIC void ble_log_redir_seal(ble_log_prph_trans_t *trans, ble_log_src_t src_code); + +BLE_LOG_STATIC +ble_log_prph_trans_t *ble_log_redir_get_trans(ble_log_redir_t *redir, + ble_log_src_t src_code) +{ + for (int i = 0; i < BLE_LOG_TRANS_BUF_CNT; i++) { + ble_log_prph_trans_t *trans = redir->trans[redir->trans_idx]; + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(trans->state) != BLE_LOG_TRANS_STATE_SENDING) { + if (BLE_LOG_TRANS_FREE_SPACE(trans) >= BLE_LOG_FRAME_OVERHEAD) { + return trans; + } + if (trans->pos > BLE_LOG_FRAME_HEAD_LEN) { + ble_log_redir_seal(trans, src_code); + } + } + redir->trans_idx = (redir->trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); + } + return NULL; +} + +BLE_LOG_STATIC +void ble_log_redir_seal(ble_log_prph_trans_t *trans, ble_log_src_t src_code) +{ + if (trans->pos <= BLE_LOG_FRAME_HEAD_LEN) { + return; + } + + uint16_t payload_len = trans->pos - BLE_LOG_FRAME_HEAD_LEN; + ble_log_redir_t *redir = ble_log_prph_get_redir_lbm(); + BLE_LOG_ASSERT(redir); + /* REDIR keeps its own stream sequence: a raw console stream is not a + * log attempt, so it never eats Global SNs or fakes loss gaps. The + * stream has no core-stat slot. */ + uint32_t frame_sn = BLE_LOG_GET_FRAME_SN(redir->frame_sn); + ble_log_frame_head_t frame_head = { + .length = payload_len, + .frame_meta = BLE_LOG_MAKE_FRAME_META(src_code, frame_sn), + }; + BLE_LOG_MEMCPY(trans->buf, &frame_head, BLE_LOG_FRAME_HEAD_LEN); + + uint32_t checksum = ble_log_fast_checksum(trans->buf, trans->pos); + BLE_LOG_MEMCPY(trans->buf + trans->pos, &checksum, BLE_LOG_FRAME_TAIL_LEN); + trans->pos += BLE_LOG_FRAME_TAIL_LEN; + + uint32_t infl = __atomic_add_fetch(&redir->inflight, 1, __ATOMIC_RELAXED); + uint32_t peak = BLE_LOG_ATOMIC_LOAD_RELAXED(redir->inflight_peak); + while (infl > peak && + !__atomic_compare_exchange_n(&redir->inflight_peak, &peak, infl, true, + __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { + } + + BLE_LOG_ATOMIC_STORE_RELAXED(trans->state, BLE_LOG_TRANS_STATE_SENDING); + ble_log_rt_submit_trans(trans); +} + +void ble_log_lbm_stream_write(ble_log_redir_t *redir, ble_log_src_t src_code, + const uint8_t *data, size_t len) +{ + while (len > 0) { + ble_log_prph_trans_t *trans = ble_log_redir_get_trans(redir, src_code); + if (!trans) { + /* Burn one stream SN so the dropped console batch leaves a + * gap in the REDIR sequence. */ + (void)BLE_LOG_GET_FRAME_SN(redir->frame_sn); + return; + } + + if (trans->pos == 0) { + trans->pos = BLE_LOG_FRAME_HEAD_LEN; + } + + uint16_t available = BLE_LOG_TRANS_FREE_SPACE(trans); + if (available <= BLE_LOG_FRAME_TAIL_LEN) { + ble_log_redir_seal(trans, src_code); + continue; + } + available -= BLE_LOG_FRAME_TAIL_LEN; + + size_t to_write = (len < available) ? len : available; + BLE_LOG_MEMCPY(trans->buf + trans->pos, data, to_write); + trans->pos += to_write; + data += to_write; + len -= to_write; + + if (BLE_LOG_TRANS_FREE_SPACE(trans) <= BLE_LOG_FRAME_OVERHEAD) { + ble_log_redir_seal(trans, src_code); + } + } +} + +void ble_log_lbm_stream_flush(ble_log_redir_t *redir, ble_log_src_t src_code) +{ + int trans_idx = redir->trans_idx; + for (int i = 0; i < BLE_LOG_TRANS_BUF_CNT; i++) { + ble_log_prph_trans_t *trans = redir->trans[trans_idx]; + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(trans->state) != BLE_LOG_TRANS_STATE_SENDING && + trans->pos > BLE_LOG_FRAME_HEAD_LEN) { + ble_log_redir_seal(trans, src_code); + } + trans_idx = (trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); + } +} +#endif /* BLE_LOG_UART_REDIR_ENABLED */ diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h index aa0260ae377..5754bd8259e 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h @@ -84,6 +84,10 @@ typedef struct { ble_log_prph_trans_t *trans[BLE_LOG_TRANS_BUF_CNT]; SemaphoreHandle_t mutex; int trans_idx; + /* The REDIR console stream keeps its own 24-bit frame sequence: it is + * not a log attempt, so it never consumes the Global SN; a gap counts + * a dropped console batch. Zeroed when the manager is created. */ + volatile uint32_t frame_sn; volatile uint32_t inflight; volatile uint32_t inflight_peak; } ble_log_redir_t; @@ -106,18 +110,15 @@ typedef struct { #define BLE_LOG_GET_FRAME_SN(VAR) BLE_LOG_ATOMIC_ADD_RELAXED(VAR, 1) -/* One 24-bit Global SN is shared by every non-INTERNAL source: consumed at - * API entry (before pool contention), it orders all log attempts — - * including equal-timestamp records from different sources — and every - * lost or rejected attempt leaves a gap. Internal Snapshot frames keep - * their own separate sequence: a skipped periodic snapshot burns one - * snapshot SN, so a gap there counts lost snapshots without a dedicated - * field. Neither sequence is ever reset. */ -#define BLE_LOG_SN_MODULO 0x00ffffffU -#define BLE_LOG_GET_GLOBAL_SN() \ - ((uint32_t)BLE_LOG_GET_FRAME_SN(g_frame_sn) & BLE_LOG_SN_MODULO) -#define BLE_LOG_GET_SNAPSHOT_SN() \ - ((uint32_t)BLE_LOG_GET_FRAME_SN(g_snapshot_sn) & BLE_LOG_SN_MODULO) +/* One 24-bit Global SN is shared by the log sources except INTERNAL and + * REDIR: consumed at API entry (before pool contention), it orders all + * log attempts — including equal-timestamp records from different + * sources — and every lost or rejected attempt leaves a gap. INTERNAL + * snapshot frames and the REDIR console stream keep their own separate + * sequences (a gap counts a skipped snapshot or a dropped console batch). + * None of the sequences is ever reset. The per-counter macros live next + * to their counters in the owning translation units; the 24-bit wire + * field is enforced where the frame meta is packed. */ /* -------------------------------- */ /* Internal Snapshot Frame */ @@ -231,9 +232,6 @@ uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, uint32_t *handle) void ble_log_commit(uint32_t handle, size_t actual_len); #if BLE_LOG_UART_REDIR_ENABLED -void ble_log_lbm_stream_write(ble_log_redir_t *redir, ble_log_src_t src_code, - uint32_t timestamp, const uint8_t *data, size_t len); -void ble_log_lbm_stream_flush(ble_log_redir_t *redir, ble_log_src_t src_code); ble_log_redir_t *ble_log_prph_get_redir_lbm(void); #endif diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_redir.h b/components/bt/common/ble_log/src/internal_include/ble_log_redir.h new file mode 100644 index 00000000000..94437db6e61 --- /dev/null +++ b/components/bt/common/ble_log/src/internal_include/ble_log_redir.h @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef __BLE_LOG_REDIR_H__ +#define __BLE_LOG_REDIR_H__ + +/* ---------------------------------------------- */ +/* BLE Log - UART Redirection Stream API */ +/* ---------------------------------------------- */ + +#include "ble_log_lbm_v2.h" + +#if BLE_LOG_UART_REDIR_ENABLED +void ble_log_lbm_stream_write(ble_log_redir_t *redir, ble_log_src_t src_code, + const uint8_t *data, size_t len); +void ble_log_lbm_stream_flush(ble_log_redir_t *redir, ble_log_src_t src_code); +#endif /* BLE_LOG_UART_REDIR_ENABLED */ + +#endif /* __BLE_LOG_REDIR_H__ */ diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c b/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c index 2916c29637c..ce0333e577b 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c @@ -12,6 +12,7 @@ #include "ble_log_prph_uart_dma.h" #include "ble_log.h" #include "ble_log_lbm_v2.h" +#include "ble_log_redir.h" #include "ble_log_rt.h" #if BLE_LOG_PRPH_UART_DMA_REDIR @@ -326,9 +327,8 @@ bool ble_log_redir_uart_tx_chars(const char *src, size_t len) __atomic_sub_fetch(&redir_writer_count, 1, __ATOMIC_SEQ_CST); return true; } - uint32_t timestamp = (uint32_t)esp_timer_get_time(); xSemaphoreTake(redir_lbm->mutex, portMAX_DELAY); - ble_log_lbm_stream_write(redir_lbm, BLE_LOG_SRC_REDIR, timestamp, + ble_log_lbm_stream_write(redir_lbm, BLE_LOG_SRC_REDIR, (const uint8_t *)src, len); xSemaphoreGive(redir_lbm->mutex); __atomic_sub_fetch(&redir_writer_count, 1, __ATOMIC_SEQ_CST); From fdf4942485f2334ad1d1e78c1a892eca3b371e23 Mon Sep 17 00:00:00 2001 From: Zhou Xiao Date: Wed, 2 Sep 2026 14:35:48 +0800 Subject: [PATCH 05/13] feat(ble_log): add periodic auto flush with pending-seal handoff Partially-filled OPEN transports previously waited for a capacity seal or an explicit ble_log_flush(); an app that never calls flush loses the parked frames at test end. Three triggers now cover the gap: - The periodic output tick (the TS trigger, or the runtime hook when TS is disabled) flushes OPEN transports ahead of the periodic snapshot. The flusher never waits for a lock: a transport whose lock is held is left a pending-seal marker instead. - The next claim that takes the transport lock sees the marker and seals the buffered frames before scanning on for another transport; if no writer ever returns, the next periodic pass seals the transport uncontended. - ble_log_deinit() drains the remaining OPEN transports after the writer gate closes and before the runtime queue is destroyed; the peripheral deinit wait completes the delivery. The marker is cleared by seal_and_send (the choke point of every seal path), by the direct FREE return in ble_log_commit's error path, and on recycle, so it never survives a transport lifecycle. The full-barrier ble_log_flush() semantics are unchanged. Tests cover the pending-seal handoff (claim-locked flush hook), the deinit drain of a parked sub-capacity burst, and the real ble_log_deinit() path through the auto-recycle hook. --- components/bt/common/ble_log/src/ble_log.c | 9 +- .../bt/common/ble_log/src/ble_log_lbm_v2.c | 63 +++++- components/bt/common/ble_log/src/ble_log_rt.c | 13 +- .../src/internal_include/ble_log_lbm_v2.h | 6 + .../src/internal_include/ble_log_prph.h | 6 + .../ble_log_test/main/test_ble_log_rt.c | 186 ++++++++++++++++++ 6 files changed, 279 insertions(+), 4 deletions(-) diff --git a/components/bt/common/ble_log/src/ble_log.c b/components/bt/common/ble_log/src/ble_log.c index d434effc624..81dc6690dbd 100644 --- a/components/bt/common/ble_log/src/ble_log.c +++ b/components/bt/common/ble_log/src/ble_log.c @@ -90,11 +90,18 @@ void ble_log_deinit(void) ble_log_inited = false; ble_log_lbm_begin_deinit(); + /* Residual frames parked in OPEN transports would be discarded with + * the pool. Seal and dispatch them while the runtime queue is still + * alive; the peripheral deinit wait below completes the delivery. */ + ble_log_lbm_drain_open_transports(); + /* CRITICAL - Deinit ordering rationale: * * 1. The LBM writer gate is closed before submodule teardown. Writers * already inside the gate keep a reference until they finish; later - * writers are rejected. + * writers are rejected. With writers gone, the deinit drain seals + * the remaining OPEN transports and hands them to the runtime + * queue, so residual frames are not discarded with the pool. * * 2. Runtime dispatch must be stopped FIRST to prevent it from sending * transports to an already-destroyed peripheral driver. Active diff --git a/components/bt/common/ble_log/src/ble_log_lbm_v2.c b/components/bt/common/ble_log/src/ble_log_lbm_v2.c index b7bc01735e3..c17264de38d 100644 --- a/components/bt/common/ble_log/src/ble_log_lbm_v2.c +++ b/components/bt/common/ble_log/src/ble_log_lbm_v2.c @@ -97,6 +97,7 @@ BLE_LOG_STATIC ble_log_internal_snapshot_t internal_snapshot; extern void ble_log_test_claim_pre_publish_hook(void) __attribute__((weak)); extern void ble_log_test_enable_before_lifecycle_lock_hook(void) __attribute__((weak)); extern void ble_log_test_disable_before_wake_hook(void) __attribute__((weak)); +extern void ble_log_test_claim_locked_hook(void) __attribute__((weak)); #endif BLE_LOG_IRAM_ATTR BLE_LOG_STATIC @@ -217,6 +218,9 @@ BLE_LOG_STATIC void ble_log_pool_wake_all(void) BLE_LOG_IRAM_ATTR void ble_log_lbm_recycle_trans(ble_log_prph_trans_t *trans) { trans->pos = 0; + /* A marker set while the transport was SENDING must not leak into its + * next lifecycle. */ + BLE_LOG_ATOMIC_STORE_RELAXED(trans->pending_seal, false); if (trans->owner_kind == BLE_LOG_TRANS_OWNER_INTERNAL) { BLE_LOG_ATOMIC_STORE_RELEASE(trans->state, BLE_LOG_TRANS_STATE_FREE); @@ -245,6 +249,10 @@ BLE_LOG_IRAM_ATTR void ble_log_lbm_recycle_trans(ble_log_prph_trans_t *trans) /* -------------------------------------- */ BLE_LOG_IRAM_ATTR void ble_log_pool_seal_and_send(ble_log_prph_trans_t *trans) { + /* Consumes any pending-seal marker: every seal path (capacity, claim, + * periodic flush, full drain) funnels through here while holding the + * lock. */ + BLE_LOG_ATOMIC_STORE_RELAXED(trans->pending_seal, false); BLE_LOG_ATOMIC_STORE_RELAXED(trans->state, BLE_LOG_TRANS_STATE_SENDING); /* A claimed buffer is already absent from free_bitmap. Remove any OPEN * hint before releasing the SENDING buffer to the runtime task. */ @@ -290,7 +298,11 @@ ble_log_prph_trans_t *ble_log_pool_try_claim_from(volatile uint32_t *bitmap, } if (expected_state == BLE_LOG_TRANS_STATE_OPEN && - BLE_LOG_TRANS_FREE_SPACE(trans) < frame_len) { + (BLE_LOG_TRANS_FREE_SPACE(trans) < frame_len || + BLE_LOG_ATOMIC_LOAD_ACQUIRE(trans->pending_seal))) { + /* No room for this frame, or a periodic flush left a + * pending-seal marker while the transport was busy: send the + * buffered frames and scan on for another transport. */ ble_log_pool_seal_and_send(trans); /* releases the lock */ continue; } @@ -324,7 +336,19 @@ ble_log_prph_trans_t *ble_log_pool_try_claim_available(uint32_t frame_len, bool ble_log_prph_trans_t *open_trans = g_pool.trans[open_id]; if (BLE_LOG_CAS_ACQUIRE(&open_trans->atomic_lock)) { if (BLE_LOG_ATOMIC_LOAD_RELAXED(open_trans->state) == BLE_LOG_TRANS_STATE_OPEN) { - if (BLE_LOG_TRANS_FREE_SPACE(open_trans) >= frame_len) { + /* A pending-seal marker left by a periodic flush that lost + * the lock race sends the buffered frames first; this claim + * continues with another transport below. */ + if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(open_trans->pending_seal) && + BLE_LOG_TRANS_FREE_SPACE(open_trans) >= frame_len) { +#if CONFIG_BLE_LOG_PRPH_TEST + /* Test point: the transport is locked and still listed + * in open_bitmap, mirroring an in-progress writer for a + * concurrent flush attempt. */ + if (ble_log_test_claim_locked_hook) { + ble_log_test_claim_locked_hook(); + } +#endif ble_log_pool_bitmap_clear(&g_pool.open_bitmap, open_id); return open_trans; } @@ -532,6 +556,9 @@ void ble_log_commit(uint32_t handle, size_t actual_len) if (actual_len == 0 || actual_len > claim->max_len) { ble_log_stat_mgr_mark_lost(src_code); if (trans->pos == 0) { + /* Returning straight to FREE bypasses seal_and_send: also drop + * any pending-seal marker here. */ + BLE_LOG_ATOMIC_STORE_RELAXED(trans->pending_seal, false); BLE_LOG_ATOMIC_STORE_RELEASE(trans->state, BLE_LOG_TRANS_STATE_FREE); ble_log_pool_bitmap_set(&g_pool.free_bitmap, trans->id); BLE_LOG_CAS_RELEASE(&trans->atomic_lock); @@ -610,6 +637,7 @@ bool ble_log_lbm_init(void) g_pool.trans[id]->owner_kind = BLE_LOG_TRANS_OWNER_POOL; g_pool.trans[id]->state = BLE_LOG_TRANS_STATE_FREE; g_pool.trans[id]->atomic_lock = 0; + g_pool.trans[id]->pending_seal = 0; } if (!ble_log_prph_trans_init(&internal_trans, BLE_LOG_INTERNAL_TRANS_SIZE)) { @@ -826,6 +854,12 @@ void ble_log_lbm_flush_open_transports(void) } ble_log_prph_trans_t *trans = g_pool.trans[id]; if (!BLE_LOG_CAS_ACQUIRE(&trans->atomic_lock)) { + /* A writer holds the buffer: leave a pending-seal marker for + * the next claim instead of waiting (the flusher never + * competes for a lock). The marker is serviced by the next + * writer on this transport, or by the next periodic pass once + * the lock is uncontended. */ + BLE_LOG_ATOMIC_STORE_RELEASE(trans->pending_seal, true); continue; } if (BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state) == BLE_LOG_TRANS_STATE_OPEN && @@ -839,6 +873,31 @@ void ble_log_lbm_flush_open_transports(void) BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); } +void ble_log_lbm_drain_open_transports(void) +{ + /* Contract (see header): called after ble_log_lbm_begin_deinit() and + * before ble_log_rt_deinit(). The producer gate is closed and writers + * have drained, so no lock is held for longer than one frame copy. */ + for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { + if (!(BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.open_bitmap) & BIT(id))) { + continue; + } + ble_log_prph_trans_t *trans = g_pool.trans[id]; + while (!BLE_LOG_CAS_ACQUIRE(&trans->atomic_lock)) { + } + if (BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state) == BLE_LOG_TRANS_STATE_OPEN && + trans->pos > 0) { + ble_log_pool_seal_and_send(trans); /* releases the lock */ + } else { + BLE_LOG_CAS_RELEASE(&trans->atomic_lock); + } + } + + /* Hand the sealed buffers to the peripheral before the runtime queue + * is destroyed; the peripheral deinit wait completes the delivery. */ + (void)ble_log_rt_drain(); +} + /* ------------------------ */ /* PUBLIC INTERFACE */ /* ------------------------ */ diff --git a/components/bt/common/ble_log/src/ble_log_rt.c b/components/bt/common/ble_log/src/ble_log_rt.c index d6ed9ea8571..72d00f77edd 100644 --- a/components/bt/common/ble_log/src/ble_log_rt.c +++ b/components/bt/common/ble_log/src/ble_log_rt.c @@ -127,6 +127,10 @@ BLE_LOG_STATIC void ble_log_rt_run_hook(void) return; } rt_last_hook_os_ts = now; + /* Unified periodic output: best-effort flush of partially-filled OPEN + * transports ahead of the periodic snapshot, so parked frames do not + * wait for the next capacity seal. */ + ble_log_lbm_flush_open_transports(); (void)ble_log_internal_snapshot(BLE_LOG_SNAPSHOT_REASON_PERIODIC, NULL, false); } @@ -178,7 +182,14 @@ BLE_LOG_STATIC void ble_log_rt_ts_trigger(void *arg) } ble_log_ts_info_t ts_info; - if (ble_log_ts_info_update(&ts_info)) { + bool ts_valid = ble_log_ts_info_update(&ts_info); + + /* Unified periodic output: best-effort flush of partially-filled OPEN + * transports ahead of the periodic snapshot, so parked frames do not + * wait for the next capacity seal. */ + ble_log_lbm_flush_open_transports(); + + if (ts_valid) { (void)ble_log_internal_snapshot( BLE_LOG_SNAPSHOT_REASON_PERIODIC | BLE_LOG_SNAPSHOT_REASON_TS_VALID, diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h index 5754bd8259e..90eabec255b 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h @@ -219,6 +219,12 @@ void ble_log_lbm_begin_deinit(void); void ble_log_lbm_deinit(void); bool ble_log_lbm_is_enabled(void); void ble_log_lbm_flush_open_transports(void); +/* Deinit drain: seal every OPEN transport and dispatch it to the runtime + * queue. Contract: called after ble_log_lbm_begin_deinit() (producer gate + * closed, writers drained) and before ble_log_rt_deinit(); every transport + * lock is then uncontended. The peripheral deinit wait completes the + * delivery of the dispatched buffers. */ +void ble_log_lbm_drain_open_transports(void); void ble_log_lbm_recycle_trans(ble_log_prph_trans_t *trans); void ble_log_internal_set_version_info(const ble_log_version_info_t *version_info); bool ble_log_internal_snapshot(uint16_t reason_flags, diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_prph.h b/components/bt/common/ble_log/src/internal_include/ble_log_prph.h index 801445bae69..8d881ccd9d9 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_prph.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_prph.h @@ -42,6 +42,12 @@ typedef struct { uint8_t id; uint8_t owner_kind; + /* Lazy flush marker, pool-owned: set by the periodic flusher (without + * holding atomic_lock) when the transport was busy at flush time; the + * next claim that takes the lock seals the buffered frames first. + * Cleared by seal_and_send and on recycle. */ + volatile uint8_t pending_seal; + uint8_t *buf; uint16_t size; uint16_t pos; diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c index 9fb3d2167d6..daee52f5da1 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c @@ -57,6 +57,7 @@ typedef struct { static uint8_t s_read_buf[TEST_READ_BUF_SIZE]; static bool s_claim_hook_armed; static uint32_t s_stale_claim_handle; +static volatile bool s_locked_hook_armed; static volatile bool s_enable_hook_armed; static SemaphoreHandle_t s_enable_hook_entered; static SemaphoreHandle_t s_enable_hook_continue; @@ -70,6 +71,7 @@ static SemaphoreHandle_t s_compression_hook_continue; #endif void ble_log_test_claim_pre_publish_hook(void); +void ble_log_test_claim_locked_hook(void); void ble_log_test_enable_before_lifecycle_lock_hook(void); void ble_log_test_disable_before_wake_hook(void); #if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE @@ -88,6 +90,17 @@ void ble_log_test_claim_pre_publish_hook(void) } } +void ble_log_test_claim_locked_hook(void) +{ + if (s_locked_hook_armed) { + s_locked_hook_armed = false; + /* Runs while the claiming writer itself holds the OPEN transport + * lock: the flush must skip the busy transport and leave the + * pending-seal marker for the next claim. */ + ble_log_lbm_flush_open_transports(); + } +} + void ble_log_test_enable_before_lifecycle_lock_hook(void) { if (s_enable_hook_armed) { @@ -812,6 +825,179 @@ TEST_CASE("BLE Log flush preserves source-local sequence continuity", TEST_ASSERT_EQUAL_HEX32((before.sn + 1) & 0x00ffffffU, after.sn); } +/* ------- pending-seal and deinit drain ------- */ + +#define TEST_MARKER_CHUNK_MAX (4) + +typedef struct { + uint8_t markers[TEST_MARKER_CHUNK_MAX]; + int count; +} marker_chunk_capture_t; + +static void capture_custom_marker(const test_ble_log_frame_t *frame, void *ctx) +{ + marker_chunk_capture_t *capture = ctx; + if (frame->src == BLE_LOG_SRC_CUSTOM && + frame->payload_len == sizeof(uint32_t) + 1 && + capture->count < TEST_MARKER_CHUNK_MAX) { + capture->markers[capture->count++] = frame->payload[sizeof(uint32_t)]; + } +} + +/* Reads every pending transport chunk, keeping only the chunks that carry + * CUSTOM marker frames; interleaved snapshot transports are consumed and + * ignored. */ +static int read_marker_chunks(marker_chunk_capture_t *chunks, int max_chunks) +{ + int marker_chunks = 0; + for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT; i++) { + size_t len = ble_log_prph_test_read( + s_read_buf, sizeof(s_read_buf), pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + marker_chunk_capture_t chunk = {0}; + TEST_ASSERT_TRUE(test_ble_log_walk_frames(s_read_buf, len, + capture_custom_marker, + &chunk)); + if (chunk.count && marker_chunks < max_chunks) { + chunks[marker_chunks++] = chunk; + } + } + return marker_chunks; +} + +static void auto_recycle_count(void *ctx) +{ + (*(int *)ctx)++; +} + +TEST_CASE("BLE Log pending-seal marker defers the busy transport flush", + "[ble_log][lbm]") +{ + const uint8_t first_marker = 0xd1; + const uint8_t cursor_marker = 0xd2; + const uint8_t hooked_marker = 0xd3; + const uint8_t post_marker = 0xd4; + + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_transports(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + /* Park one frame in an OPEN transport; the claim cursor stays on it. */ + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &first_marker, sizeof(first_marker))); + + /* A prior test may leave open_cursor at any recycled transport. Prime + * it through either the direct path or the OPEN bitmap scan before + * arming the direct-path hook. */ + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &cursor_marker, sizeof(cursor_marker))); + + /* The armed hook runs a flush while the claiming writer itself holds + * the transport lock: the flush must skip the busy transport and leave + * the pending-seal marker for the NEXT claim. */ + s_locked_hook_armed = true; + bool hooked_write = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &hooked_marker, + sizeof(hooked_marker)); + bool hook_ran = !s_locked_hook_armed; + s_locked_hook_armed = false; + TEST_ASSERT_TRUE(hooked_write); + TEST_ASSERT_TRUE(hook_ran); + + /* The next writer must seal the flagged transport first and place its + * own frame in another one. */ + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &post_marker, sizeof(post_marker))); + + ble_log_lbm_flush_open_transports(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + + marker_chunk_capture_t chunks[2] = {0}; + TEST_ASSERT_EQUAL(2, read_marker_chunks(chunks, 2)); + TEST_ASSERT_EQUAL(3, chunks[0].count); + TEST_ASSERT_EQUAL_HEX8(first_marker, chunks[0].markers[0]); + TEST_ASSERT_EQUAL_HEX8(cursor_marker, chunks[0].markers[1]); + TEST_ASSERT_EQUAL_HEX8(hooked_marker, chunks[0].markers[2]); + TEST_ASSERT_EQUAL(1, chunks[1].count); + TEST_ASSERT_EQUAL_HEX8(post_marker, chunks[1].markers[0]); +} + +TEST_CASE("BLE Log deinit drain delivers parked open transports", + "[ble_log][lbm]") +{ + const uint8_t markers[TEST_MARKER_CHUNK_MAX] = {0xe1, 0xe2, 0xe3, 0xe4}; + + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_transports(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + /* Sub-capacity burst: frames park in an OPEN transport that no + * capacity seal ever sends. */ + for (int i = 0; i < TEST_MARKER_CHUNK_MAX; i++) { + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &markers[i], 1)); + } + + /* Exercise the exact deinit-drain contract: close the producer gate and + * wait for writers before sealing every OPEN transport. */ + ble_log_lbm_begin_deinit(); + ble_log_lbm_drain_open_transports(); + + marker_chunk_capture_t chunks[1] = {0}; + int chunk_count = read_marker_chunks(chunks, 1); + + /* Finish the partially-entered teardown and restore the module before + * assertions so the following Unity case starts from a valid lifetime. */ + ble_log_deinit(); + TEST_ASSERT_TRUE(ble_log_init()); + + TEST_ASSERT_EQUAL(1, chunk_count); + TEST_ASSERT_EQUAL(TEST_MARKER_CHUNK_MAX, chunks[0].count); + for (int i = 0; i < TEST_MARKER_CHUNK_MAX; i++) { + TEST_ASSERT_EQUAL_HEX8(markers[i], chunks[0].markers[i]); + } +} + +TEST_CASE("BLE Log deinit hands residual transports to the peripheral", + "[ble_log][lbm]") +{ + const uint8_t marker = 0xf1; + int recycled = 0; + + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_transports(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, + &marker, sizeof(marker))); + + /* Auto-recycle turns every dispatched transport into a countable + * event, so the real deinit path is observable without a reader + * task racing the teardown. */ + ble_log_prph_test_set_auto_recycle_hook(auto_recycle_count, &recycled); + ble_log_deinit(); + TEST_ASSERT_GREATER_OR_EQUAL(1, recycled); + TEST_ASSERT_TRUE(ble_log_init()); +} + #define SNAPSHOT_CAPTURE_MAX 8 typedef struct { From 06478ec3131e1df8adfa3021bab900a254be89b9 Mon Sep 17 00:00:00 2001 From: Zhou Xiao Date: Wed, 2 Sep 2026 14:55:10 +0800 Subject: [PATCH 06/13] feat(ble_log): make TS sync unconditional with toggle-IO-only Kconfig TS sync now always runs: the periodic tick (TS sample, OPEN transport flush, internal snapshot) exists in every build, so the unified periodic output no longer depends on BLE_LOG_TS_ENABLED and idle systems without dispatch activity still flush every second. - New BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED gates only the analyzer toggle IO (GPIO config, level writes, reset); with it unset, TS snapshots still sample the clocks and carry io_level = 0. BLE_LOG_TS_ENABLED keeps its prompt as a deprecated compatibility entry that selects BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED, so existing projects keep the old toggle behavior, and the TS trigger entries drop their dependency on it. - ble_log_ts is no longer a separate module: its 95-line shell (clock sampling, toggle IO management) joins ble_log_rt.c, the only runtime that drives it. ble_log_ts_info_t moves to ble_log_lbm_v2.h (the snapshot interface that consumes it), the BLE_LOG_GET_LC_TS chip table moves next to its single caller, the init/deinit folds into ble_log_rt_init/deinit, the reset folds into ble_log_sync_enable, and the sampler becomes a void static now that its failure paths are gone. - The runtime hook is gone: with the tick always present, its throttled defer-callback fallback for runtime-disabled sync served no one. The TS tick is the only periodic output source; ble_log_sync_enable(false) now means full periodic silence, and ble_log_rt_dispatch loses its return value. The version-info regression now arms the tick instead of the hook. - BLE_LOG_GET_LC_TS branches on CONFIG_BT_DUAL_MODE_ARCH: the dual-mode-arch controllers (ESP32-H4, ESP32-S31) expose their link layer timer as r_sched_timer_getCurrentTimeU32, but the symbol is obfuscated in the current prebuilt libraries, so those targets report lc_ts = 0 for now; call the accessor once the libraries export it. The Gen 2 branch keeps r_ble_lll_timer_current_tick_get (defined by every C5/C6/C61/H2/H21 library, verified by nm and by linking ble_log_test for ESP32-C6); ESP32-H4 and ESP32-S31 fall out of it. - ble_log_ts_info_update's old shape is gone entirely: the heap-allocated global ts_info and its critical section memcpy were leftovers from the pointer-return API; the sampler writes the caller object in place and keeps only the toggle IO phase as cross-call state. int_src_code is filled outside the critical section; the phase toggle stays inside to exclude the write in ble_log_sync_enable. Idle systems no longer touch the controller clock: the legacy accessors dereference controller state and INIT precedes controller initialization, so the sampler returns lc_ts = 0 while the controller is idle instead of reading it. The deferred dispatch callback drains only the queue depth observed at entry and re-arms itself for arrivals left behind, so it cannot monopolize the shared ESP timer task. --- components/bt/common/CMakeLists.txt | 5 - components/bt/common/ble_log/Kconfig.in | 20 +- .../bt/common/ble_log/include/ble_log.h | 2 - components/bt/common/ble_log/src/ble_log.c | 15 -- components/bt/common/ble_log/src/ble_log_rt.c | 207 +++++++++++------- components/bt/common/ble_log/src/ble_log_ts.c | 94 -------- .../src/internal_include/ble_log_lbm_v2.h | 10 +- .../ble_log/src/internal_include/ble_log_rt.h | 1 - .../ble_log/src/internal_include/ble_log_ts.h | 58 ----- .../ble_log_rt_test/main/test_ble_log_main.c | 5 +- .../main/test_ble_log_runtime.c | 4 +- .../ble_log_rt_test/sdkconfig.defaults | 2 +- .../ble_log_test/main/test_ble_log_main.c | 3 + .../ble_log_test/main/test_ble_log_rt.c | 8 +- components/bt/sdkconfig.rename | 4 + 15 files changed, 156 insertions(+), 282 deletions(-) delete mode 100644 components/bt/common/ble_log/src/ble_log_ts.c delete mode 100644 components/bt/common/ble_log/src/internal_include/ble_log_ts.h diff --git a/components/bt/common/CMakeLists.txt b/components/bt/common/CMakeLists.txt index 7688210c025..2f2f99b10cb 100644 --- a/components/bt/common/CMakeLists.txt +++ b/components/bt/common/CMakeLists.txt @@ -185,11 +185,6 @@ if(CONFIG_BLE_LOG_ENABLED) "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/internal_include/prph" ) - # Timestamp synchronization extension - if(CONFIG_BLE_LOG_TS_ENABLED) - list(APPEND bt_common_srcs "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_ts.c") - endif() - # Peripheral interface implementation if(CONFIG_BLE_LOG_PRPH_DUMMY) list(APPEND bt_common_srcs diff --git a/components/bt/common/ble_log/Kconfig.in b/components/bt/common/ble_log/Kconfig.in index f2e6b850bcd..85d6a2ad303 100644 --- a/components/bt/common/ble_log/Kconfig.in +++ b/components/bt/common/ble_log/Kconfig.in @@ -92,18 +92,19 @@ if BLE_LOG_ENABLED Deprecated. Renamed to BLE_LOG_HCI_LOG_ENABLED; kept so sdkconfig files pinning the old name keep Host side HCI logging enabled. - config BLE_LOG_TS_ENABLED - bool "Enable BLE Log Timestamp Synchronization (TS)" + config BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED + bool "Toggle a GPIO on every BLE Log TS sync sample" default n help - Enable BLE Log TS with external logging module. Synchronization is - triggered periodically by an ESP Timer using task dispatch. The - timer does not wake the system from light sleep. + BLE Log TS sync always runs: every periodic output tick samples + the link-layer, ESP and OS clocks into the internal snapshot. + Enable this option to additionally toggle a GPIO on every + sample so an external logic analyzer can align the device + clocks with the capture. - if BLE_LOG_TS_ENABLED + if BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED config BLE_LOG_SYNC_IO_NUM int "GPIO number for Timestamp Synchronization (TS) toggle output" - depends on BLE_LOG_TS_ENABLED default 0 help GPIO number for TS toggle output @@ -252,27 +253,22 @@ if BLE_LOG_ENABLED config BLE_LOG_TS_TRIGGER_TIMEOUT_MS int - depends on BLE_LOG_TS_ENABLED default 1000 config BLE_LOG_TS_TRIGGER_CHOICE bool - depends on BLE_LOG_TS_ENABLED default y config BLE_LOG_TS_TRIGGER_ESP_TIMER bool - depends on BLE_LOG_TS_ENABLED default y config BLE_LOG_TS_TRIGGER_TASK_EVENT bool - depends on BLE_LOG_TS_ENABLED default n config BLE_LOG_TS_TRIGGER_ESP_TIMER_ISR_DISPATCH_METHOD bool - depends on BLE_LOG_TS_ENABLED default n endif diff --git a/components/bt/common/ble_log/include/ble_log.h b/components/bt/common/ble_log/include/ble_log.h index 25669aa7dea..efa20298585 100644 --- a/components/bt/common/ble_log/include/ble_log.h +++ b/components/bt/common/ble_log/include/ble_log.h @@ -74,8 +74,6 @@ void ble_log_dump_to_console(void); void ble_log_write_hex_ll(uint32_t len, const uint8_t *addr, uint32_t len_append, const uint8_t *addr_append, uint32_t flag); #endif /* CONFIG_BLE_LOG_LL_ENABLED */ -#if CONFIG_BLE_LOG_TS_ENABLED bool ble_log_sync_enable(bool enable); -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ #endif /* __BLE_LOG_H__ */ diff --git a/components/bt/common/ble_log/src/ble_log.c b/components/bt/common/ble_log/src/ble_log.c index 81dc6690dbd..9be67605685 100644 --- a/components/bt/common/ble_log/src/ble_log.c +++ b/components/bt/common/ble_log/src/ble_log.c @@ -15,9 +15,6 @@ #include "ble_log_util.h" #include "esp_log.h" #include "esp_system.h" -#if CONFIG_BLE_LOG_TS_ENABLED -#include "ble_log_ts.h" -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ /* VARIABLE */ #define TAG "ble_log" @@ -38,13 +35,6 @@ bool ble_log_init(void) return true; } -#if CONFIG_BLE_LOG_TS_ENABLED - /* Initialize BLE Log TS */ - if (!ble_log_ts_init()) { - goto exit; - } -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ - /* Allocate pool and dedicated Internal transport before runtime starts. */ if (!ble_log_lbm_init()) { goto exit; @@ -118,9 +108,4 @@ void ble_log_deinit(void) ble_log_rt_deinit(); ble_log_prph_deinit(); ble_log_lbm_deinit(); - -#if CONFIG_BLE_LOG_TS_ENABLED - /* Deinitialize BLE Log TS */ - ble_log_ts_deinit(); -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ } diff --git a/components/bt/common/ble_log/src/ble_log_rt.c b/components/bt/common/ble_log/src/ble_log_rt.c index 72d00f77edd..5482092d1e6 100644 --- a/components/bt/common/ble_log/src/ble_log_rt.c +++ b/components/bt/common/ble_log/src/ble_log_rt.c @@ -12,63 +12,81 @@ #include "ble_log.h" #include "ble_log_rt.h" #include "ble_log_lbm_v2.h" +#include "ble_log_util.h" #include "esp_log.h" #include "esp_timer.h" #include "esp_chip_info.h" +#if CONFIG_BLE_LOG_LL_ENABLED +#include "esp_bt.h" +#endif +#include "driver/gpio.h" /* MACRO */ #define TAG "ble_log_rt" #define BLE_LOG_RT_DEFER_TIMEOUT_US (1000) -#if CONFIG_BT_CONTROLLER_ENABLED -#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 -extern const char *btdm_controller_get_compile_version(void); -#define BLE_LOG_CONTROLLER_GET_COMMIT() btdm_controller_get_compile_version() -#elif !CONFIG_BT_DUAL_MODE_ARCH || CONFIG_BT_CTRL_BLE_ENABLE -/* BR/EDR-only dual-mode builds do not link the BLE controller lib */ -extern char *ble_controller_get_compile_version(void); -#define BLE_LOG_CONTROLLER_GET_COMMIT() ble_controller_get_compile_version() -#endif +/* Link-layer clock sample; 0 when the controller exports no accessor. */ +#if CONFIG_BLE_LOG_LL_ENABLED #if CONFIG_BT_DUAL_MODE_ARCH -/* BTDM common lib (dual-mode arch only) */ -extern const char *r_btdm_get_compile_version(void); -#define BLE_LOG_BTDM_COMMON_GET_COMMIT() r_btdm_get_compile_version() -#endif -#endif - -#if CONFIG_BLE_MESH && CONFIG_BLE_MESH_V11_SUPPORT -/* "Bluetooth Mesh v1.1 commit: " */ -extern const char bt_mesh_v11_commit_str[]; -#endif - -#if CONFIG_BT_AUDIO && CONFIG_SOC_BLE_AUDIO_SUPPORTED -extern const char *lib_audio_commit_get(void); -#endif +/* The dual-mode-arch controller (ESP32-H4, ESP32-S31) does not export its + * link-layer timer yet; its accessor is r_sched_timer_getCurrentTimeU32. + * Call it once the controller libraries export the symbol. */ +#define BLE_LOG_GET_LC_TS 0 +/* ESP BLE Controller Gen 2 */ +#elif defined(CONFIG_IDF_TARGET_ESP32H2) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32C5) ||\ + defined(CONFIG_IDF_TARGET_ESP32C61) || defined(CONFIG_IDF_TARGET_ESP32H21) +extern uint32_t r_ble_lll_timer_current_tick_get(void); +#define BLE_LOG_GET_LC_TS r_ble_lll_timer_current_tick_get() +/* ESP BLE Controller Gen 1 */ +#elif defined(CONFIG_IDF_TARGET_ESP32C2) +extern uint32_t r_os_cputime_get32(void); +#define BLE_LOG_GET_LC_TS r_os_cputime_get32() +/* Legacy BLE Controller */ +#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) +extern uint32_t lld_read_clock_us(void); +#define BLE_LOG_GET_LC_TS lld_read_clock_us() +#else /* Other targets */ +#define BLE_LOG_GET_LC_TS 0 +#endif /* BLE targets */ +#else /* !CONFIG_BLE_LOG_LL_ENABLED */ +#define BLE_LOG_GET_LC_TS 0 +#endif /* CONFIG_BLE_LOG_LL_ENABLED */ _Static_assert(sizeof(ble_log_version_info_t) == 58, "Unexpected BLE Log version info frame size"); +BLE_LOG_STATIC uint32_t ble_log_rt_lc_ts_get(void) +{ +#if CONFIG_BLE_LOG_LL_ENABLED + /* Legacy accessors dereference controller state. INIT is emitted before + * controller initialization completes, and standalone users may keep the + * controller idle for the entire BLE Log epoch. */ + if (esp_bt_controller_get_status() == ESP_BT_CONTROLLER_STATUS_IDLE) { + return 0; + } +#endif + return BLE_LOG_GET_LC_TS; +} + /* VARIABLE */ BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t rt_inited = 0; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR volatile uint32_t rt_ref_count = 0; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR QueueHandle_t rt_queue_handle = NULL; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR esp_timer_handle_t rt_defer_timer = NULL; -BLE_LOG_STATIC uint32_t rt_last_hook_os_ts = 0; BLE_LOG_STATIC ble_log_version_info_t rt_version_info; -#if CONFIG_BLE_LOG_TS_ENABLED BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t rt_ts_enabled = 0; -BLE_LOG_STATIC esp_timer_handle_t rt_ts_timer = NULL; -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR esp_timer_handle_t rt_ts_timer = NULL; +/* Toggle IO phase; stays false when the toggle IO is compiled out. */ +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR bool rt_ts_io_level = false; /* PRIVATE FUNCTION DECLARATION */ BLE_LOG_STATIC void ble_log_rt_defer_cb(void *arg); -BLE_LOG_STATIC bool ble_log_rt_dispatch(QueueHandle_t queue, UBaseType_t pending); +BLE_LOG_STATIC void ble_log_rt_dispatch(QueueHandle_t queue, + UBaseType_t pending); BLE_LOG_STATIC void ble_log_rt_version_info_init(void); -BLE_LOG_STATIC void ble_log_rt_run_hook(void); -#if CONFIG_BLE_LOG_TS_ENABLED +BLE_LOG_STATIC void ble_log_rt_ts_sample(ble_log_ts_info_t *info); BLE_LOG_STATIC void ble_log_rt_ts_trigger(void *arg); -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ /* PRIVATE FUNCTION */ /* Copies a NUL-terminated commit string into a fixed-width zero-padded field */ @@ -112,38 +130,35 @@ BLE_LOG_STATIC void ble_log_rt_version_info_init(void) ble_log_internal_set_version_info(&rt_version_info); } -BLE_LOG_STATIC void ble_log_rt_run_hook(void) +/* Captures the link-layer, ESP and OS clocks at one instant. */ +BLE_LOG_STATIC void ble_log_rt_ts_sample(ble_log_ts_info_t *info) { - if (!ble_log_lbm_is_enabled()) { - return; - } -#if CONFIG_BLE_LOG_TS_ENABLED - if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(rt_ts_enabled)) { - return; - } -#endif - uint32_t now = pdTICKS_TO_MS(xTaskGetTickCount()); - if ((uint32_t)(now - rt_last_hook_os_ts) < BLE_LOG_TS_TRIGGER_TIMEOUT_MS) { - return; - } - rt_last_hook_os_ts = now; - /* Unified periodic output: best-effort flush of partially-filled OPEN - * transports ahead of the periodic snapshot, so parked frames do not - * wait for the next capacity seal. */ - ble_log_lbm_flush_open_transports(); - (void)ble_log_internal_snapshot(BLE_LOG_SNAPSHOT_REASON_PERIODIC, - NULL, false); + info->int_src_code = BLE_LOG_INT_SRC_TS; + BLE_LOG_ENTER_CRITICAL(); +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED + /* The critical section keeps the toggle IO edge and the clock samples + * adjacent, and excludes the phase write in ble_log_sync_enable. */ + rt_ts_io_level = !rt_ts_io_level; + gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, rt_ts_io_level); +#endif /* CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED */ + info->io_level = rt_ts_io_level; + info->lc_ts = ble_log_rt_lc_ts_get(); + info->esp_ts = esp_timer_get_time(); + info->os_ts = pdTICKS_TO_MS(xTaskGetTickCountFromISR()); + BLE_LOG_EXIT_CRITICAL(); } -BLE_LOG_STATIC bool ble_log_rt_dispatch(QueueHandle_t queue, UBaseType_t pending) +/* Dispatch only the queue depth observed at callback entry. A backend may + * recycle synchronously on queue-full while another core immediately refills + * the runtime queue; an unbounded loop here could starve every other callback + * on the shared ESP timer task. */ +BLE_LOG_STATIC void ble_log_rt_dispatch(QueueHandle_t queue, + UBaseType_t pending) { ble_log_prph_trans_t *trans = NULL; - bool processed = false; while (pending-- && xQueueReceive(queue, &trans, 0) == pdTRUE) { ble_log_prph_send_trans(trans); - processed = true; } - return processed; } BLE_LOG_STATIC void ble_log_rt_defer_cb(void *arg) @@ -155,24 +170,19 @@ BLE_LOG_STATIC void ble_log_rt_defer_cb(void *arg) } QueueHandle_t queue = rt_queue_handle; - if (!queue) { - return; - } + ble_log_rt_dispatch(queue, uxQueueMessagesWaiting(queue)); - UBaseType_t pending = uxQueueMessagesWaiting(queue); - if (ble_log_rt_dispatch(queue, pending)) { - ble_log_rt_run_hook(); - } - - pending = uxQueueMessagesWaiting(queue); - if (pending && + /* A submit racing an active one-shot callback may fail to arm it. If the + * bounded batch left work behind, schedule another turn after yielding + * the shared timer task to callbacks that are already due. */ + if (uxQueueMessagesWaiting(queue) && ble_log_ref_count_try_acquire(&rt_ref_count, &rt_inited)) { - (void)esp_timer_start_once(rt_defer_timer, BLE_LOG_RT_DEFER_TIMEOUT_US); + (void)esp_timer_start_once(rt_defer_timer, + BLE_LOG_RT_DEFER_TIMEOUT_US); BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); } } -#if CONFIG_BLE_LOG_TS_ENABLED BLE_LOG_STATIC void ble_log_rt_ts_trigger(void *arg) { (void)arg; @@ -182,21 +192,18 @@ BLE_LOG_STATIC void ble_log_rt_ts_trigger(void *arg) } ble_log_ts_info_t ts_info; - bool ts_valid = ble_log_ts_info_update(&ts_info); + ble_log_rt_ts_sample(&ts_info); /* Unified periodic output: best-effort flush of partially-filled OPEN * transports ahead of the periodic snapshot, so parked frames do not * wait for the next capacity seal. */ ble_log_lbm_flush_open_transports(); - if (ts_valid) { - (void)ble_log_internal_snapshot( - BLE_LOG_SNAPSHOT_REASON_PERIODIC | - BLE_LOG_SNAPSHOT_REASON_TS_VALID, - &ts_info, false); - } + (void)ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC | + BLE_LOG_SNAPSHOT_REASON_TS_VALID, + &ts_info, false); } -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ /* INTERFACE */ bool ble_log_rt_init(void) @@ -206,6 +213,19 @@ bool ble_log_rt_init(void) } ble_log_rt_version_info_init(); + + /* Configure the analyzer toggle IO */ +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED + gpio_config_t sync_io_conf = { + .intr_type = GPIO_INTR_DISABLE, + .mode = GPIO_MODE_OUTPUT, + .pin_bit_mask = BIT64(CONFIG_BLE_LOG_SYNC_IO_NUM), + }; + if (gpio_config(&sync_io_conf) != ESP_OK) { + goto exit; + } +#endif /* CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED */ + rt_queue_handle = xQueueCreate(BLE_LOG_TRANS_TOTAL_CNT, sizeof(ble_log_prph_trans_t *)); if (!rt_queue_handle) { goto exit; @@ -222,8 +242,11 @@ bool ble_log_rt_init(void) goto exit; } -#if CONFIG_BLE_LOG_TS_ENABLED - BLE_LOG_ATOMIC_STORE_RELAXED(rt_ts_enabled, false); + /* TS sync is always on: the periodic tick drives the unified periodic + * output (TS sample, OPEN transport flush, snapshot). Test apps quiesce + * it with ble_log_sync_enable(false) for deterministic timing. */ + rt_ts_io_level = false; + BLE_LOG_ATOMIC_STORE_RELEASE(rt_ts_enabled, true); esp_timer_create_args_t ts_timer_args = { .callback = ble_log_rt_ts_trigger, .arg = NULL, @@ -236,9 +259,7 @@ bool ble_log_rt_init(void) esp_timer_start_periodic(rt_ts_timer, BLE_LOG_TS_TRIGGER_TIMEOUT_US) != ESP_OK) { goto exit; } -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ - rt_last_hook_os_ts = 0; BLE_LOG_ATOMIC_STORE_RELEASE(rt_inited, true); return true; @@ -257,14 +278,12 @@ void ble_log_rt_deinit(void) ESP_LOGE(TAG, "Timed out waiting for BLE Log runtime references"); BLE_LOG_ASSERT(false); } -#if CONFIG_BLE_LOG_TS_ENABLED BLE_LOG_ATOMIC_STORE_RELEASE(rt_ts_enabled, false); if (rt_ts_timer) { esp_timer_stop_blocking(rt_ts_timer, portMAX_DELAY); esp_timer_delete(rt_ts_timer); rt_ts_timer = NULL; } -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ if (rt_defer_timer) { esp_timer_stop_blocking(rt_defer_timer, portMAX_DELAY); @@ -281,6 +300,12 @@ void ble_log_rt_deinit(void) vQueueDelete(rt_queue_handle); rt_queue_handle = NULL; } + + /* Release the toggle IO */ + rt_ts_io_level = false; +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED + gpio_reset_pin(CONFIG_BLE_LOG_SYNC_IO_NUM); +#endif /* CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED */ } bool ble_log_rt_drain(void) @@ -297,7 +322,9 @@ bool ble_log_rt_drain(void) goto exit; } QueueHandle_t queue = rt_queue_handle; - (void)ble_log_rt_dispatch(queue, uxQueueMessagesWaiting(queue)); + while (uxQueueMessagesWaiting(queue)) { + ble_log_rt_dispatch(queue, uxQueueMessagesWaiting(queue)); + } drained = true; exit: @@ -332,15 +359,25 @@ BLE_LOG_IRAM_ATTR void ble_log_rt_submit_trans(ble_log_prph_trans_t *trans) BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); } -#if CONFIG_BLE_LOG_TS_ENABLED bool ble_log_sync_enable(bool enable) { if (!ble_log_ref_count_try_acquire(&rt_ref_count, &rt_inited)) { return false; } BLE_LOG_ATOMIC_STORE_RELEASE(rt_ts_enabled, enable); - ble_log_ts_reset(enable); +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED + /* Leave the toggle IO at a defined low level: when sync is disabled + * while the IO idles low, drive a short high pulse first so the + * analyzer sees a final falling edge. The critical section excludes + * the phase toggle in ble_log_rt_ts_sample. */ + BLE_LOG_ENTER_CRITICAL(); + if (!enable && !rt_ts_io_level) { + gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, 1); + } + rt_ts_io_level = false; + gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, 0); + BLE_LOG_EXIT_CRITICAL(); +#endif /* CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED */ BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); return true; } -#endif /* CONFIG_BLE_LOG_TS_ENABLED */ diff --git a/components/bt/common/ble_log/src/ble_log_ts.c b/components/bt/common/ble_log/src/ble_log_ts.c deleted file mode 100644 index 9bc822d7679..00000000000 --- a/components/bt/common/ble_log/src/ble_log_ts.c +++ /dev/null @@ -1,94 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -/* ----------------------------------- */ -/* BLE Log - Timestamp Synchronization */ -/* ----------------------------------- */ - -/* INCLUDE */ -#include "ble_log_ts.h" - -/* VARIABLE */ -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR bool ts_inited = false; -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_ts_info_t *ts_info = NULL; - -/* INTERFACE */ -bool ble_log_ts_init(void) -{ - if (ts_inited) { - return true; - } - - /* Initialize toggle IO */ - gpio_config_t sync_io_conf = { - .intr_type = GPIO_INTR_DISABLE, - .mode = GPIO_MODE_OUTPUT, - .pin_bit_mask = BIT64(CONFIG_BLE_LOG_SYNC_IO_NUM), - }; - if (gpio_config(&sync_io_conf) != ESP_OK) { - goto exit; - } - - /* Initialize sync data */ - ts_info = (ble_log_ts_info_t *)BLE_LOG_MALLOC(sizeof(ble_log_ts_info_t)); - if (!ts_info) { - goto exit; - } - BLE_LOG_MEMSET(ts_info, 0, sizeof(ble_log_ts_info_t)); - ts_info->int_src_code = BLE_LOG_INT_SRC_TS; - - ts_inited = true; - return true; - -exit: - ble_log_ts_deinit(); - return false; -} - -void ble_log_ts_deinit(void) -{ - ts_inited = false; - - /* Release sync data */ - if (ts_info) { - BLE_LOG_FREE(ts_info); - ts_info = NULL; - } - - /* Release toggle IO */ - gpio_reset_pin(CONFIG_BLE_LOG_SYNC_IO_NUM); -} - -void ble_log_ts_info_update(ble_log_ts_info_t **info) -{ - if (!ts_inited) { - return; - } - - BLE_LOG_ENTER_CRITICAL(); - ts_info->io_level = !ts_info->io_level; - gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, ts_info->io_level); - ts_info->lc_ts = BLE_LOG_GET_LC_TS; - ts_info->esp_ts = esp_timer_get_time(); - ts_info->os_ts = pdTICKS_TO_MS(xTaskGetTickCountFromISR()); - BLE_LOG_EXIT_CRITICAL(); - - *info = ts_info; -} - -void ble_log_ts_reset(bool status) -{ - if (!ts_inited) { - return; - } - - BLE_LOG_ENTER_CRITICAL(); - if (!status && !ts_info->io_level) { - gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, 1); - } - ts_info->io_level = 0; - gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, 0); - BLE_LOG_EXIT_CRITICAL(); -} diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h index 90eabec255b..f179d35ca37 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h @@ -16,7 +16,6 @@ /* INCLUDE */ #include "ble_log.h" #include "ble_log_prph.h" -#include "ble_log_ts.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" @@ -123,6 +122,15 @@ typedef struct { /* -------------------------------- */ /* Internal Snapshot Frame */ /* -------------------------------- */ +/* Clock sample captured at one instant by the runtime periodic tick. */ +typedef struct { + uint8_t int_src_code; + uint8_t io_level; + uint32_t lc_ts; + uint32_t esp_ts; + uint32_t os_ts; +} __attribute__((packed)) ble_log_ts_info_t; + #define BLE_LOG_SNAPSHOT_REASON_INIT BIT(0) #define BLE_LOG_SNAPSHOT_REASON_PERIODIC BIT(1) #define BLE_LOG_SNAPSHOT_REASON_FLUSH BIT(2) diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_rt.h b/components/bt/common/ble_log/src/internal_include/ble_log_rt.h index 28d1018ce55..89f51c7421b 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_rt.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_rt.h @@ -12,7 +12,6 @@ /* INCLUDE */ #include "ble_log_prph.h" -#include "ble_log_ts.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_ts.h b/components/bt/common/ble_log/src/internal_include/ble_log_ts.h deleted file mode 100644 index 6a09cd70f1b..00000000000 --- a/components/bt/common/ble_log/src/internal_include/ble_log_ts.h +++ /dev/null @@ -1,58 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#ifndef __BLE_LOG_TS_H__ -#define __BLE_LOG_TS_H__ - -/* ----------------------------------- */ -/* BLE Log - Timestamp Synchronization */ -/* ----------------------------------- */ - -/* INCLUDE */ -#include "ble_log_util.h" - -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include "esp_timer.h" -#include "driver/gpio.h" - -/* MACRO */ -#if CONFIG_BLE_LOG_LL_ENABLED -/* ESP BLE Controller Gen 2 */ -#if defined(CONFIG_IDF_TARGET_ESP32H2) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32C5) ||\ - defined(CONFIG_IDF_TARGET_ESP32C61) || defined(CONFIG_IDF_TARGET_ESP32H21) || defined(CONFIG_IDF_TARGET_ESP32H4) -extern uint32_t r_ble_lll_timer_current_tick_get(void); -#define BLE_LOG_GET_LC_TS r_ble_lll_timer_current_tick_get() -/* ESP BLE Controller Gen 1 */ -#elif defined(CONFIG_IDF_TARGET_ESP32C2) -extern uint32_t r_os_cputime_get32(void); -#define BLE_LOG_GET_LC_TS r_os_cputime_get32() -/* Legacy BLE Controller */ -#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) -extern uint32_t lld_read_clock_us(void); -#define BLE_LOG_GET_LC_TS lld_read_clock_us() -#else /* Other targets */ -#define BLE_LOG_GET_LC_TS 0 -#endif /* BLE targets */ -#else /* !CONFIG_BLE_LOG_LL_ENABLED */ -#define BLE_LOG_GET_LC_TS 0 -#endif /* CONFIG_BLE_LOG_LL_ENABLED */ - -/* TYPEDEF */ -typedef struct { - uint8_t int_src_code; - uint8_t io_level; - uint32_t lc_ts; - uint32_t esp_ts; - uint32_t os_ts; -} __attribute__((packed)) ble_log_ts_info_t; - -/* INTERFACE */ -bool ble_log_ts_init(void); -void ble_log_ts_deinit(void); -void ble_log_ts_info_update(ble_log_ts_info_t **ts_info); -void ble_log_ts_reset(bool status); - -#endif /* __BLE_LOG_TS_H__ */ diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c index c8281ff6662..903cede8507 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c @@ -53,14 +53,15 @@ bool test_ble_log_walk_frames(const uint8_t *data, size_t len, void setUp(void) { + /* The periodic TS tick is always on since TS sync became unconditional; + * quiesce it so timing-sensitive tests stay deterministic. */ + (void)ble_log_sync_enable(false); } void tearDown(void) { ble_log_prph_test_set_auto_recycle_hook(NULL, NULL); -#if CONFIG_BLE_LOG_TS_ENABLED (void)ble_log_sync_enable(false); -#endif } void app_main(void) diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c index 1cc8286e3a5..2bd0e916224 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c @@ -448,7 +448,6 @@ TEST_CASE("BLE Log ISR-only submission arms runtime dispatch", #endif } -#if CONFIG_BLE_LOG_TS_ENABLED TEST_CASE("BLE Log periodic timestamp skips light sleep wakeups", "[ble_log][runtime][timestamp][ignore]") { @@ -496,7 +495,6 @@ TEST_CASE("BLE Log periodic timestamp skips light sleep wakeups", 0, ts_count, "Periodic ESP timer did not emit a timestamp frame"); } -#endif TEST_CASE("BLE Log runtime dispatch yields to other timer callbacks", "[ble_log][runtime][ignore]") @@ -729,7 +727,7 @@ TEST_CASE("BLE Log LBM inflight peak stays bounded under bursts", TEST_ASSERT_TRUE(write_runtime_marker(seq, NULL)); } - /* Recycle the burst after its peak has been recorded. A runtime hook may + /* Recycle the burst after its peak has been recorded. A periodic tick may * also have occupied the dedicated Internal transport, so drain twice. */ for (int round = 0; round < 2; round++) { TEST_ASSERT_TRUE(ble_log_rt_drain()); diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/sdkconfig.defaults b/components/bt/common/ble_log/test_apps/ble_log_rt_test/sdkconfig.defaults index 75545a6db78..bc9a2936bc2 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/sdkconfig.defaults +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/sdkconfig.defaults @@ -6,7 +6,7 @@ CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD=y # Production tick rate; the 1000 Hz regression variant is built from # sdkconfig.defaults.tick_1000. CONFIG_FREERTOS_HZ=100 -CONFIG_BLE_LOG_TS_ENABLED=y +CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED=y # Keep the production TS/hook cadence (Kconfig default 1000 ms) so the # regressions see the production info/stat/buf-util cadence. CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c index 2642e629c31..20558a09e04 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c @@ -52,6 +52,9 @@ bool test_ble_log_walk_frames(const uint8_t *data, size_t len, void setUp(void) { + /* The periodic TS tick is always on since TS sync became unconditional; + * quiesce it so timing-sensitive tests stay deterministic. */ + (void)ble_log_sync_enable(false); } void tearDown(void) diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c index daee52f5da1..39b02135d1b 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c @@ -263,7 +263,7 @@ TEST_CASE("BLE Log v7 framing matches golden bytes", "[ble_log][wire]") sizeof(golden_payload)); } -TEST_CASE("BLE Log runtime hook reports build and chip versions", "[ble_log]") +TEST_CASE("BLE Log periodic tick reports build and chip versions", "[ble_log]") { static const uint8_t payload[TEST_PAYLOAD_LEN] = {0}; @@ -276,9 +276,10 @@ TEST_CASE("BLE Log runtime hook reports build and chip versions", "[ble_log]") TEST_READER_STACK_SIZE, &ctx, TEST_READER_PRIO, &reader)); TEST_ASSERT_TRUE(ble_log_enable(true)); + TEST_ASSERT_TRUE(ble_log_sync_enable(true)); - /* A hook pass submits one consolidated Internal Snapshot containing the - * version, statistics, utilization, and optional TS sample. */ + /* Each periodic tick submits one consolidated Internal Snapshot containing + * the version, statistics, utilization, and optional TS sample. */ for (int round = 0; round < TEST_MAX_ROUNDS && ctx.capture.version_info_count == 0; round++) { vTaskDelay(pdMS_TO_TICKS(TEST_HOOK_SETTLE_MS)); for (int i = 0; i < TEST_WRITES_PER_ROUND; i++) { @@ -330,6 +331,7 @@ TEST_CASE("BLE Log runtime hook reports build and chip versions", "[ble_log]") TEST_ASSERT_EQUAL_UINT16((uint16_t)chip_info.model, vi->chip_model); TEST_ASSERT_EQUAL_UINT16(chip_info.revision, vi->chip_revision); + (void)ble_log_sync_enable(false); vSemaphoreDelete(ctx.done); } diff --git a/components/bt/sdkconfig.rename b/components/bt/sdkconfig.rename index 12bd0e9d5e5..8fab0a89c2f 100644 --- a/components/bt/sdkconfig.rename +++ b/components/bt/sdkconfig.rename @@ -286,3 +286,7 @@ CONFIG_BT_NIMBLE_HCI_EVT_LO_BUF_COUNT CONFIG_BT_NIMBLE_TRA CONFIG_BT_NIMBLE_COEX_PHY_CODED_TX_RX_TLIM_EN CONFIG_BT_LE_COEX_PHY_CODED_TX_RX_TLIM_EN CONFIG_BT_NIMBLE_COEX_PHY_CODED_TX_RX_TLIM_DIS CONFIG_BT_LE_COEX_PHY_CODED_TX_RX_TLIM_DIS + +# BLE Log: periodic Internal Snapshots are always enabled now; the legacy +# TS switch only gates the analyzer toggle IO. +CONFIG_BLE_LOG_TS_ENABLED CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED From 281ce584f3adc5058c0d2c00417cd283d7103407 Mon Sep 17 00:00:00 2001 From: Zhou Xiao Date: Wed, 2 Sep 2026 19:25:33 +0800 Subject: [PATCH 07/13] refactor(ble_log): remove the dead legacy LBM module and tidy the v7 switch ble_log_lbm.c/h were superseded by the v2 LBM (protocol v7) and are not part of the build; the two files only referenced each other. The deprecated Kconfig entries stay untouched - downstream test apps may still pin the old symbols (e.g. BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT). Post-switch polish folded in: version info built in place inside lbm init, pool-internal tidying, simplified runtime submit/defer paths, per-snapshot clock sampling, shared waiter-count RMW in the acquire path, and dropped zero-value transport field inits. --- components/bt/common/ble_log/Kconfig.in | 10 +- components/bt/common/ble_log/README.md | 29 +- .../bt/common/ble_log/include/ble_log.h | 8 +- components/bt/common/ble_log/src/ble_log.c | 32 +- .../bt/common/ble_log/src/ble_log_lbm.c | 874 ------------------ .../bt/common/ble_log/src/ble_log_lbm_v2.c | 267 ++++-- .../bt/common/ble_log/src/ble_log_redir.c | 7 +- components/bt/common/ble_log/src/ble_log_rt.c | 159 ++-- .../bt/common/ble_log/src/ble_log_util.c | 15 +- .../src/internal_include/ble_log_lbm.h | 272 ------ .../src/internal_include/ble_log_lbm_v2.h | 25 +- .../src/internal_include/ble_log_prph.h | 3 + .../ble_log/src/internal_include/ble_log_rt.h | 9 + .../src/internal_include/ble_log_util.h | 5 + .../ble_log/src/prph/ble_log_prph_dummy.c | 1 - .../src/prph/ble_log_prph_spi_master_dma.c | 1 - .../src/prph/ble_log_prph_spi_master_hd.c | 2 - .../ble_log/src/prph/ble_log_prph_test.c | 4 - .../ble_log/src/prph/ble_log_prph_uart_dma.c | 7 +- .../ble_log_rt_test/main/test_ble_log_main.c | 4 +- .../main/test_ble_log_runtime.c | 138 ++- .../ble_log_test/main/test_ble_log_main.c | 4 +- .../ble_log_test/main/test_ble_log_rt.c | 146 ++- 23 files changed, 602 insertions(+), 1420 deletions(-) delete mode 100644 components/bt/common/ble_log/src/ble_log_lbm.c delete mode 100644 components/bt/common/ble_log/src/internal_include/ble_log_lbm.h diff --git a/components/bt/common/ble_log/Kconfig.in b/components/bt/common/ble_log/Kconfig.in index 85d6a2ad303..22d3ab6e7d0 100644 --- a/components/bt/common/ble_log/Kconfig.in +++ b/components/bt/common/ble_log/Kconfig.in @@ -96,11 +96,11 @@ if BLE_LOG_ENABLED bool "Toggle a GPIO on every BLE Log TS sync sample" default n help - BLE Log TS sync always runs: every periodic output tick samples - the link-layer, ESP and OS clocks into the internal snapshot. - Enable this option to additionally toggle a GPIO on every - sample so an external logic analyzer can align the device - clocks with the capture. + Every periodic Internal Snapshot samples the link-layer, ESP and + OS clocks regardless of this option. Enable this option to build + the GPIO toggle used by external logic analyzers. It starts + disabled and low; ble_log_ts_sync_io_toggle_enable controls its + edges without stopping periodic snapshots. if BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED config BLE_LOG_SYNC_IO_NUM diff --git a/components/bt/common/ble_log/README.md b/components/bt/common/ble_log/README.md index ae23c710aba..eb9c0802efe 100644 --- a/components/bt/common/ble_log/README.md +++ b/components/bt/common/ble_log/README.md @@ -74,9 +74,9 @@ redirection flush) is the alignment reference against the core timeline. ### Sources -The public `ble_log_src_t` ABI is frozen and its values are the on-wire -source IDs of protocol v7 frames — the frame source byte carries the enum -value directly, so existing decoders keep working: +The public `ble_log_src_t` ABI is frozen and its values are the base on-wire +source IDs of protocol v7 frames. Receivers must mask the `NON_YIELD` bit +before decoding the base source: ```text 0 INTERNAL @@ -96,7 +96,10 @@ equal-timestamp records from different sources — and every lost or rejected attempt leaves a gap in the sequence. Internal Snapshot frames carry their own separate sequence (a gap counts skipped snapshots), and the REDIR console stream keeps its own sequence as well (a gap counts a dropped -console batch). None of these sequences is reset. +console batch). `ble_log_init()` resets all three sequences, and its required +`INIT` snapshot starts a new receiver epoch. They remain continuous through +`FLUSH` within that epoch. Callers must not write until `ble_log_init()` +returns, so the `INIT` snapshot is submitted first. Actual ISR and critical-section records carry `NON_YIELD` in source bit 7. Controller-side HCI records are not emitted by BLE Log, and the controller no @@ -121,6 +124,8 @@ The snapshot contains: - compact statistics for `CUSTOM` through `ENCODE` (one slot per public source in that range, `HOST` included). +The periodic snapshot is an always-on system behavior from initialization +until deinitialization. It cannot be stopped by the TS sync IO control API. A skipped periodic snapshot burns one snapshot SN instead: the gap in the snapshot sequence is the loss signal, and it is visible directly in the frame header without a dedicated payload field. @@ -155,8 +160,21 @@ void ble_log_commit(uint32_t handle, size_t actual_len); void ble_log_write_hex_ll(uint32_t len, const uint8_t *data, uint32_t append_len, const uint8_t *append, uint32_t flags); +bool ble_log_ts_sync_io_toggle_enable(bool enable); +bool ble_log_sync_enable(bool enable); /* compatibility shim */ ``` +`ble_log_enable()` gates public producers only. Periodic system output remains +active while that gate is closed. + +`ble_log_ts_sync_io_toggle_enable()` controls only the optional external +analyzer GPIO, which starts disabled and low. Disabling it leaves the IO low +after a final falling edge; +periodic clock sampling, OPEN transport flushing, and Internal Snapshots +continue. When the GPIO feature is not built, the call remains a +lifecycle-checked no-op. `ble_log_sync_enable()` is the backward-compatible +name for the same behavior. + `ble_log_flush()` is an ordinary FreeRTOS task API. Do not call it from an ISR, critical section, or the shared ESP Timer task. @@ -197,7 +215,8 @@ canceled and counted as lost. | `CONFIG_BLE_LOG_POOL_TRANS_SIZE` | 640 | Bytes per shared transport; SPI builds require a multiple of four | | `CONFIG_BLE_LOG_LL_ENABLED` | target dependent | Controller LL logging | | `CONFIG_BLE_LOG_HCI_LOG_ENABLED` | target dependent | Host-side HCI capture | -| `CONFIG_BLE_LOG_TS_ENABLED` | n | GPIO/LC timestamp synchronization | +| `CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED` | n | Build the optional analyzer GPIO toggle | +| `CONFIG_BLE_LOG_TS_ENABLED` | n | Deprecated compatibility entry selecting the GPIO toggle | Old multi-LBM sizing options remain hidden only so existing sdkconfig files can be parsed; they no longer control allocation. diff --git a/components/bt/common/ble_log/include/ble_log.h b/components/bt/common/ble_log/include/ble_log.h index efa20298585..b62ace0ac2e 100644 --- a/components/bt/common/ble_log/include/ble_log.h +++ b/components/bt/common/ble_log/include/ble_log.h @@ -21,7 +21,7 @@ * memory requirements, keep it as less as possible; it's recommended to use subcode for more * log data structure decoding */ /* CRITICAL: this enum is a public ABI and must not be reordered or renamed. - * Its values are also the on-wire source IDs of protocol v7 frames. */ + * Its values are the base on-wire source IDs of protocol v7 frames. */ typedef enum { /* Internal */ BLE_LOG_SRC_INTERNAL = 0, @@ -63,6 +63,7 @@ typedef enum { /* INTERFACE */ bool ble_log_init(void); void ble_log_deinit(void); +/* Controls public producers only; periodic system output remains active. */ bool ble_log_enable(bool enable); /* Blocking; call only from a caller-owned task, not an ISR or system callback. */ void ble_log_flush(void); @@ -74,6 +75,11 @@ void ble_log_dump_to_console(void); void ble_log_write_hex_ll(uint32_t len, const uint8_t *addr, uint32_t len_append, const uint8_t *addr_append, uint32_t flag); #endif /* CONFIG_BLE_LOG_LL_ENABLED */ +/* Task-context only. Controls the optional TS sync IO toggle, which starts + * disabled and low; this is a lifecycle-checked no-op when the toggle is not + * built. Periodic Internal Snapshots remain active in either state. */ +bool ble_log_ts_sync_io_toggle_enable(bool enable); +/* Backward-compatible name for ble_log_ts_sync_io_toggle_enable(). */ bool ble_log_sync_enable(bool enable); #endif /* __BLE_LOG_H__ */ diff --git a/components/bt/common/ble_log/src/ble_log.c b/components/bt/common/ble_log/src/ble_log.c index 9be67605685..f74219ab8dc 100644 --- a/components/bt/common/ble_log/src/ble_log.c +++ b/components/bt/common/ble_log/src/ble_log.c @@ -49,8 +49,13 @@ bool ble_log_init(void) } ble_log_inited = true; - if (!ble_log_enable(true) || - !ble_log_internal_snapshot(BLE_LOG_SNAPSHOT_REASON_INIT, NULL, true)) { + /* Queue the required INIT snapshot before starting the periodic path or + * opening the public producer gate, so it starts the receiver epoch. + * INIT/FLUSH samples never toggle sync IO. */ + ble_log_ts_info_t ts_info; + ble_log_rt_ts_sample(&ts_info, false); + if (!ble_log_internal_snapshot(BLE_LOG_SNAPSHOT_REASON_INIT, &ts_info, true) || + !ble_log_rt_start_periodic() || !ble_log_enable(true)) { goto exit; } esp_err_t ret = esp_register_shutdown_handler(ble_log_shutdown_handler); @@ -80,23 +85,24 @@ void ble_log_deinit(void) ble_log_inited = false; ble_log_lbm_begin_deinit(); - /* Residual frames parked in OPEN transports would be discarded with - * the pool. Seal and dispatch them while the runtime queue is still - * alive; the peripheral deinit wait below completes the delivery. */ - ble_log_lbm_drain_open_transports(); + /* Seal and dispatch residual pool and UART0 REDIR data while the runtime + * queue is still alive. The peripheral waits below complete delivery. */ + ble_log_lbm_drain_open_trans(); +#if BLE_LOG_UART_REDIR_ENABLED + (void)ble_log_prph_flush(); +#endif /* CRITICAL - Deinit ordering rationale: * * 1. The LBM writer gate is closed before submodule teardown. Writers * already inside the gate keep a reference until they finish; later - * writers are rejected. With writers gone, the deinit drain seals - * the remaining OPEN transports and hands them to the runtime - * queue, so residual frames are not discarded with the pool. + * writers are rejected. With writers gone, the pool and REDIR drains + * seal and dispatch every residual frame while runtime is still live. * - * 2. Runtime dispatch must be stopped FIRST to prevent it from sending + * 2. Runtime dispatch is stopped FIRST to prevent it from sending * transports to an already-destroyed peripheral driver. Active - * submissions and callbacks finish before the timers are deleted; - * the queue is then drained and pending transports are discarded. + * submissions and callbacks finish before the timers and queue are + * deleted. * * 3. Peripheral interface is deinitialized SECOND. It waits for DMA * operations started before runtime dispatch stopped, then destroys @@ -104,7 +110,7 @@ void ble_log_deinit(void) * * 4. LBM is deinitialized LAST. At this point all DMA has completed * (ensured by step 3) and all queued transports have been drained - * (ensured by step 2), so freeing the buffers is safe. */ + * (ensured by steps 1 and 2), so freeing the buffers is safe. */ ble_log_rt_deinit(); ble_log_prph_deinit(); ble_log_lbm_deinit(); diff --git a/components/bt/common/ble_log/src/ble_log_lbm.c b/components/bt/common/ble_log/src/ble_log_lbm.c deleted file mode 100644 index 3e5d2566094..00000000000 --- a/components/bt/common/ble_log/src/ble_log_lbm.c +++ /dev/null @@ -1,874 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -/* ------------------------------- */ -/* BLE Log - Log Buffer Management */ -/* ------------------------------- */ - -/* INCLUDE */ -#include "ble_log.h" -#include "ble_log_lbm.h" -#include "ble_log_rt.h" -#include "esp_log.h" - -#if CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER -#if CONFIG_BT_DUAL_MODE_ARCH -#include "ble_mbuf.h" -#define BLE_MBUF_COPY(buf, off, len, dst) ble_mbuf_copydata((struct ble_mbuf *)(buf), off, len, dst) -#else -#include "os/os_mbuf.h" -#define BLE_MBUF_COPY(buf, off, len, dst) os_mbuf_copydata((struct os_mbuf *)(buf), off, len, dst) -#endif // CONFIG_BT_DUAL_MODE_ARCH -#endif /* CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER */ - -/* VARIABLE */ -#define TAG "ble_log" -#define BLE_LOG_LBM_WAIT_TIMEOUT_MS (1000) - -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR volatile uint32_t lbm_ref_count = 0; -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t lbm_inited = 0; -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t lbm_enabled = 0; -BLE_LOG_STATIC volatile bool flush_in_progress = false; -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_lbm_ctx_t *lbm_ctx = NULL; -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_stat_mgr_t *stat_mgr_ctx[BLE_LOG_SRC_MAX] = {0}; - -/* PRIVATE FUNCTION DECLARATION */ -BLE_LOG_STATIC -bool ble_log_lbm_acquire_trans(size_t log_len, ble_log_lbm_t **out_lbm, - ble_log_prph_trans_t ***out_trans); -BLE_LOG_STATIC void ble_log_lbm_release(ble_log_lbm_t *lbm); -BLE_LOG_STATIC void ble_log_lbm_submit_trans(ble_log_prph_trans_t **trans); -BLE_LOG_STATIC -ble_log_prph_trans_t **ble_log_lbm_get_trans(ble_log_lbm_t *lbm, size_t log_len); -BLE_LOG_STATIC bool ble_log_lbm_flush_all_trans(void); -BLE_LOG_STATIC void ble_log_lbm_reset_stats(void); -BLE_LOG_STATIC -void ble_log_lbm_write_trans(ble_log_prph_trans_t **trans, ble_log_src_t src_code, - const uint8_t *addr, uint16_t len, - const uint8_t *addr_append, uint16_t len_append, bool omdata); -BLE_LOG_STATIC -bool ble_log_write_hex_core(ble_log_src_t src_code, const uint8_t *addr, size_t len); -#if BLE_LOG_UART_REDIR_ENABLED -BLE_LOG_STATIC -void ble_log_lbm_stream_seal(ble_log_prph_trans_t **trans, ble_log_src_t src_code); -#endif /* BLE_LOG_UART_REDIR_ENABLED */ -BLE_LOG_STATIC void ble_log_stat_mgr_update(ble_log_src_t src_code, uint32_t len, bool lost); - -/* ------------------------- */ -/* PRIVATE INTERFACE */ -/* ------------------------- */ -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -bool ble_log_lbm_ref_acquire(bool require_enabled) -{ - if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_inited)) { - return false; - } - if (require_enabled && !BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled)) { - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); - return false; - } - return true; -} - -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -bool ble_log_lbm_acquire_trans(size_t log_len, ble_log_lbm_t **out_lbm, - ble_log_prph_trans_t ***out_trans) -{ - *out_lbm = NULL; - *out_trans = NULL; - - ble_log_lbm_t *lbm; - ble_log_prph_trans_t **trans; - ble_log_lbm_t *atomic_pool; - ble_log_lbm_t *spin_lbm; - int atomic_pool_size; - - if (BLE_LOG_IN_ISR()) { - atomic_pool = lbm_ctx->atomic_pool_isr; - spin_lbm = &(lbm_ctx->spin_isr); - atomic_pool_size = BLE_LOG_LBM_ATOMIC_ISR_CNT; - } else { - atomic_pool = lbm_ctx->atomic_pool_task; - spin_lbm = &(lbm_ctx->spin_task); - atomic_pool_size = BLE_LOG_LBM_ATOMIC_TASK_CNT; - } - - /* Try each atomic LBM: acquire lock, check buffer, fallback on failure */ - for (int i = 0; i < atomic_pool_size; i++) { - lbm = &atomic_pool[i]; - if (ble_log_cas_acquire(&(lbm->atomic_lock))) { - trans = ble_log_lbm_get_trans(lbm, log_len); - if (trans) { - *out_lbm = lbm; - *out_trans = trans; - return true; - } - ble_log_cas_release(&(lbm->atomic_lock)); - } - } - - /* Last resort: spinlock LBM */ - lbm = spin_lbm; - BLE_LOG_ACQUIRE_SPIN_LOCK(&(lbm->spin_lock)); - trans = ble_log_lbm_get_trans(lbm, log_len); - if (trans) { - *out_lbm = lbm; - *out_trans = trans; - return true; - } - BLE_LOG_RELEASE_SPIN_LOCK(&(lbm->spin_lock)); - - return false; -} - -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -void ble_log_lbm_release(ble_log_lbm_t *lbm) -{ - switch (lbm->lock_type) { - case BLE_LOG_LBM_LOCK_ATOMIC: - ble_log_cas_release(&(lbm->atomic_lock)); - break; - case BLE_LOG_LBM_LOCK_SPIN: - BLE_LOG_RELEASE_SPIN_LOCK(&lbm->spin_lock); - break; - case BLE_LOG_LBM_LOCK_NONE: - default: - break; - } -} - -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -void ble_log_lbm_submit_trans(ble_log_prph_trans_t **trans) -{ - ble_log_prph_trans_t *submitted = *trans; - BLE_LOG_ATOMIC_STORE_RELAXED(submitted->prph_owned, true); - - ble_log_lbm_t *lbm = (ble_log_lbm_t *)submitted->owner; - uint32_t inflight = __atomic_add_fetch(&lbm->trans_inflight, 1, __ATOMIC_RELAXED); - uint32_t peak = __atomic_load_n(&lbm->trans_inflight_peak, __ATOMIC_RELAXED); - while (inflight > peak && - !__atomic_compare_exchange_n(&lbm->trans_inflight_peak, &peak, inflight, - false, __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { - } - - ble_log_rt_submit_trans(submitted); -} - -BLE_LOG_STATIC bool ble_log_lbm_flush_all_trans(void) -{ - ble_log_lbm_t *lbm; - ble_log_prph_trans_t **trans; - bool in_progress; - TickType_t start_tick = xTaskGetTickCount(); - - /* Queue transports with logs */ - for (int i = 0; i < BLE_LOG_LBM_CNT; i++) { - lbm = &(lbm_ctx->lbm_pool[i]); - int trans_idx = lbm->trans_idx; - for (int j = 0; j < BLE_LOG_TRANS_BUF_CNT; j++) { - trans = &(lbm->trans[trans_idx]); - if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE((*trans)->prph_owned) && (*trans)->pos) { - ble_log_lbm_submit_trans(trans); - } - trans_idx = (trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); - } - } - - /* Dispatch anything still waiting on the defer alarm, then - * wait for transportation to finish */ - if (!ble_log_rt_drain()) { - return false; - } - - do { - in_progress = false; - for (int i = 0; i < BLE_LOG_LBM_CNT; i++) { - lbm = &(lbm_ctx->lbm_pool[i]); - for (int j = 0; j < BLE_LOG_TRANS_BUF_CNT; j++) { - trans = &(lbm->trans[j]); - in_progress |= BLE_LOG_ATOMIC_LOAD_ACQUIRE((*trans)->prph_owned); - } - } - if (in_progress) { - if ((xTaskGetTickCount() - start_tick) >= - pdMS_TO_TICKS(BLE_LOG_LBM_WAIT_TIMEOUT_MS)) { - ESP_LOGE(TAG, "Timed out waiting for BLE Log transports"); - return false; - } - vTaskDelay(1); - } - } while (in_progress); - - return true; -} - -BLE_LOG_STATIC void ble_log_lbm_reset_stats(void) -{ - ble_log_lbm_t *lbm; - - for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { - BLE_LOG_MEMSET(stat_mgr_ctx[i], 0, sizeof(ble_log_stat_mgr_t)); - } - - for (int i = 0; i < BLE_LOG_LBM_CNT; i++) { - lbm = &(lbm_ctx->lbm_pool[i]); - __atomic_store_n(&lbm->trans_inflight, 0, __ATOMIC_RELAXED); - __atomic_store_n(&lbm->trans_inflight_peak, 0, __ATOMIC_RELAXED); - } -#if CONFIG_BLE_LOG_PRPH_UART_DMA - ble_log_prph_reset_util_counters(); -#endif -} - -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -void ble_log_lbm_write_trans(ble_log_prph_trans_t **trans, ble_log_src_t src_code, - const uint8_t *addr, uint16_t len, - const uint8_t *addr_append, uint16_t len_append, bool omdata) -{ - /* Preparation before writing */ - uint8_t *buf = (*trans)->buf + (*trans)->pos; - uint16_t payload_len = len + len_append; - ble_log_stat_mgr_t *stat_mgr = stat_mgr_ctx[src_code]; - uint32_t frame_sn = BLE_LOG_GET_FRAME_SN(&(stat_mgr->frame_sn)); - ble_log_frame_head_t frame_head = { - .length = payload_len, - .frame_meta = BLE_LOG_MAKE_FRAME_META(src_code, frame_sn), - }; - - /* Memory operation */ - BLE_LOG_MEMCPY(buf, &frame_head, BLE_LOG_FRAME_HEAD_LEN); - if (len) { - BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN, addr, len); - } - if (len_append) { -#if CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER - if (omdata) { - BLE_MBUF_COPY(addr_append, 0, len_append, buf + BLE_LOG_FRAME_HEAD_LEN + len); - } - else -#endif /* CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER */ - { - BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN + len, addr_append, len_append); - } - } - - /* Data integrity check */ - uint32_t checksum = ble_log_fast_checksum((const uint8_t *)buf, BLE_LOG_FRAME_HEAD_LEN + payload_len); - BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN + payload_len, &checksum, BLE_LOG_FRAME_TAIL_LEN); - - /* Update peripheral transport */ - (*trans)->pos += payload_len + BLE_LOG_FRAME_OVERHEAD; - - ble_log_stat_mgr_update(src_code, payload_len, false); - - /* Queue trans if full */ - if (BLE_LOG_TRANS_FREE_SPACE((*trans)) <= BLE_LOG_FRAME_OVERHEAD) { - ble_log_lbm_submit_trans(trans); - } -} - -#if BLE_LOG_UART_REDIR_ENABLED -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -void ble_log_lbm_stream_seal(ble_log_prph_trans_t **trans, ble_log_src_t src_code) -{ - if ((*trans)->pos <= BLE_LOG_FRAME_HEAD_LEN) { - return; - } - - uint16_t payload_len = (*trans)->pos - BLE_LOG_FRAME_HEAD_LEN; - ble_log_stat_mgr_t *stat_mgr = stat_mgr_ctx[src_code]; - uint32_t frame_sn = BLE_LOG_GET_FRAME_SN(&(stat_mgr->frame_sn)); - ble_log_frame_head_t frame_head = { - .length = payload_len, - .frame_meta = BLE_LOG_MAKE_FRAME_META(src_code, frame_sn), - }; - BLE_LOG_MEMCPY((*trans)->buf, &frame_head, BLE_LOG_FRAME_HEAD_LEN); - - uint32_t checksum = ble_log_fast_checksum((*trans)->buf, (*trans)->pos); - BLE_LOG_MEMCPY((*trans)->buf + (*trans)->pos, &checksum, BLE_LOG_FRAME_TAIL_LEN); - (*trans)->pos += BLE_LOG_FRAME_TAIL_LEN; - - ble_log_stat_mgr_update(src_code, payload_len, false); - - ble_log_lbm_submit_trans(trans); -} -#endif /* BLE_LOG_UART_REDIR_ENABLED */ - -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -void ble_log_stat_mgr_update(ble_log_src_t src_code, uint32_t len, bool lost) -{ - /* Get statistic manager by source code */ - ble_log_stat_mgr_t *stat_mgr = stat_mgr_ctx[src_code]; - - /* Update aligned counters */ - uint32_t bytes_cnt = len + BLE_LOG_FRAME_OVERHEAD; - if (lost) { - BLE_LOG_GET_FRAME_SN(&(stat_mgr->frame_sn)); /* consume SN for loss detection */ - __atomic_fetch_add(&stat_mgr->lost_frame_cnt, 1, __ATOMIC_RELAXED); - __atomic_fetch_add(&stat_mgr->lost_bytes_cnt, bytes_cnt, __ATOMIC_RELAXED); - } else { - __atomic_fetch_add(&stat_mgr->written_frame_cnt, 1, __ATOMIC_RELAXED); - __atomic_fetch_add(&stat_mgr->written_bytes_cnt, bytes_cnt, __ATOMIC_RELAXED); - } -} - -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -bool ble_log_write_hex_core(ble_log_src_t src_code, const uint8_t *addr, size_t len) -{ - /* Get transport from the best available pool */ - size_t payload_len = len + sizeof(uint32_t); - ble_log_lbm_t *lbm; - ble_log_prph_trans_t **trans; - if (!ble_log_lbm_acquire_trans(payload_len, &lbm, &trans)) { - goto failed; - } - - /* Write transport */ - uint32_t os_ts = pdTICKS_TO_MS(BLE_LOG_IN_ISR()? - xTaskGetTickCountFromISR(): - xTaskGetTickCount()); - ble_log_lbm_write_trans(trans, src_code, (const uint8_t *)&os_ts, - sizeof(uint32_t), addr, len, false); - - /* Release */ - ble_log_lbm_release(lbm); - return true; - -failed: - if (BLE_LOG_ATOMIC_LOAD_RELAXED(lbm_inited)) { - ble_log_stat_mgr_update(src_code, payload_len, true); - } - return false; -} - -/* -------------------------- */ -/* INTERNAL INTERFACE */ -/* -------------------------- */ -/* CRITICAL: - * Recycle a transport back to its LBM pool after the send completes or fails. - * Leaves trans->pos untouched on purpose: a failed send keeps its buffered data - * so the next flush re-queues it (flush re-sends any trans with pos != 0) */ -BLE_LOG_IRAM_ATTR void ble_log_lbm_recycle_trans(ble_log_prph_trans_t *trans) -{ - ble_log_lbm_t *lbm = (ble_log_lbm_t *)trans->owner; - __atomic_fetch_sub(&lbm->trans_inflight, 1, __ATOMIC_RELAXED); - BLE_LOG_ATOMIC_STORE_RELEASE(trans->prph_owned, false); -} - -bool ble_log_lbm_init(void) -{ - /* Avoid double init */ - if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_inited)) { - return true; - } - - /* Initialize LBM context */ - lbm_ctx = (ble_log_lbm_ctx_t *)BLE_LOG_MALLOC(sizeof(ble_log_lbm_ctx_t)); - if (!lbm_ctx) { - goto exit; - } - BLE_LOG_MEMSET(lbm_ctx, 0, sizeof(ble_log_lbm_ctx_t)); - - /* Initialize peripheral transport for common LBMs */ - ble_log_lbm_t *lbm; - for (int i = 0; i < BLE_LOG_LBM_COMMON_CNT; i++) { - lbm = &(lbm_ctx->lbm_common_pool[i]); - for (int j = 0; j < BLE_LOG_TRANS_BUF_CNT; j++) { - if (!ble_log_prph_trans_init(&(lbm->trans[j]), - BLE_LOG_TRANS_SIZE)) { - goto exit; - } - lbm->trans[j]->owner = (void *)lbm; - } - } - - /* Initialize lock types for atomic pool */ - for (int i = 0; i < BLE_LOG_LBM_ATOMIC_CNT; i++) { - lbm_ctx->atomic_pool[i].lock_type = BLE_LOG_LBM_LOCK_ATOMIC; - } - - /* Initialize lock types for spin pool */ - for (int i = 0; i < BLE_LOG_LBM_SPIN_MAX; i++) { - lbm_ctx->spin_pool[i].lock_type = BLE_LOG_LBM_LOCK_SPIN; - portMUX_INITIALIZE(&(lbm_ctx->spin_pool[i].spin_lock)); - } - -#if CONFIG_BLE_LOG_LL_ENABLED - for (int i = 0; i < BLE_LOG_LBM_LL_MAX; i++) { - lbm = &(lbm_ctx->lbm_ll_pool[i]); - for (int j = 0; j < BLE_LOG_TRANS_BUF_CNT; j++) { - if (!ble_log_prph_trans_init(&(lbm->trans[j]), - BLE_LOG_TRANS_LL_SIZE)) { - goto exit; - } - lbm->trans[j]->owner = (void *)lbm; - } - } - - /* Initialize lock types for LL pool */ - for (int i = 0; i < BLE_LOG_LBM_LL_MAX; i++) { - lbm_ctx->lbm_ll_pool[i].lock_type = BLE_LOG_LBM_LOCK_NONE; - } -#endif /* CONFIG_BLE_LOG_LL_ENABLED */ - - /* Initialize statistic manager context */ - for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { - stat_mgr_ctx[i] = (ble_log_stat_mgr_t *)BLE_LOG_MALLOC(sizeof(ble_log_stat_mgr_t)); - if (!stat_mgr_ctx[i]) { - goto exit; - } - BLE_LOG_MEMSET(stat_mgr_ctx[i], 0, sizeof(ble_log_stat_mgr_t)); - } - - /* Initialization done */ - BLE_LOG_ATOMIC_STORE_RELAXED(lbm_enabled, false); - BLE_LOG_ATOMIC_STORE_RELEASE(lbm_inited, true); - return true; - -exit: - ble_log_lbm_deinit(); - return false; -} - -void ble_log_lbm_close(void) -{ - BLE_LOG_ATOMIC_STORE_SEQ_CST(lbm_inited, false); - BLE_LOG_ATOMIC_STORE_RELEASE(lbm_enabled, false); -} - -void ble_log_lbm_deinit(void) -{ - /* Close before waiting: every LBM entry increments the reference count - * before checking this seq_cst gate. */ - ble_log_lbm_close(); - - /* Disable module and wait for all references to be released */ - while (!ble_log_ref_count_wait(&lbm_ref_count, 0)) { - ESP_LOGE(TAG, "Timed out waiting for BLE Log references during deinit"); - BLE_LOG_ASSERT(false); - } - - /* Release statistic manager context */ - for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { - if (stat_mgr_ctx[i]) { - BLE_LOG_FREE(stat_mgr_ctx[i]); - stat_mgr_ctx[i] = NULL; - } - } - - /* Release LBM */ - if (lbm_ctx) { - /* Release peripheral transport for common pools */ - ble_log_lbm_t *lbm; - for (int i = 0; i < BLE_LOG_LBM_CNT; i++) { - lbm = &(lbm_ctx->lbm_pool[i]); - for (int j = 0; j < BLE_LOG_TRANS_BUF_CNT; j++) { - ble_log_prph_trans_deinit(&(lbm->trans[j])); - } - } - - /* Release LBM context */ - BLE_LOG_FREE(lbm_ctx); - lbm_ctx = NULL; - } -} - -BLE_LOG_IRAM_ATTR BLE_LOG_STATIC -ble_log_prph_trans_t **ble_log_lbm_get_trans(ble_log_lbm_t *lbm, size_t log_len) -{ - /* Check if available buffer can contain incoming log */ - ble_log_prph_trans_t **trans; - for (int i = 0; i < BLE_LOG_TRANS_BUF_CNT; i++) { - trans = &(lbm->trans[lbm->trans_idx]); - if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE((*trans)->prph_owned)) { - /* Return if there's enough free space in current transport */ - if (BLE_LOG_TRANS_FREE_SPACE((*trans)) >= (log_len + BLE_LOG_FRAME_OVERHEAD)) { - return trans; - } - - /* Queue transport if there's insufficient free space */ - if ((*trans)->pos) { - ble_log_lbm_submit_trans(trans); - } - } - - /* Current transport unavailable, switch to the other */ - lbm->trans_idx = (lbm->trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); - } - - /* All buffers are unavailable */ - return NULL; -} - -void ble_log_write_enh_stat(void) -{ - if (!ble_log_lbm_ref_acquire(true)) { - return; - } - - /* Snapshot all sources under one critical section so the set of - * counters is mutually consistent, then write outside the lock. */ - ble_log_enh_stat_t snapshots[BLE_LOG_SRC_MAX]; - for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { - snapshots[i].int_src_code = BLE_LOG_INT_SRC_ENH_STAT; - snapshots[i].src_code = i; - } - BLE_LOG_ENTER_CRITICAL(); - for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { - BLE_LOG_MEMCPY(&snapshots[i].written_frame_cnt, - &stat_mgr_ctx[i]->written_frame_cnt, - 4 * sizeof(uint32_t)); - } - BLE_LOG_EXIT_CRITICAL(); - - for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { - ble_log_write_hex(BLE_LOG_SRC_INTERNAL, (const uint8_t *)&snapshots[i], sizeof(ble_log_enh_stat_t)); - } - - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); -} - -#if BLE_LOG_UART_REDIR_ENABLED -/* ------------------------------------------------- */ -/* STREAM WRITE INTERFACE */ -/* */ -/* Stream mode appends raw data into a transport */ -/* buffer with deferred frame encapsulation: */ -/* - Header space is reserved on first write */ -/* - Data is memcpy'd after the reserved header */ -/* - Header and checksum are filled in at seal */ -/* */ -/* get_trans(lbm, 0) reuse safety: */ -/* */ -/* get_trans auto-queues a buffer raw (no seal) when */ -/* free_space < log_len + FRAME_OVERHEAD. With */ -/* log_len = 0, this triggers at free_space < 10. */ -/* */ -/* To prevent unsealed stream data from being sent */ -/* raw, stream_write auto-seals at free_space <= 10. */ -/* This guarantees that any unsealed stream buffer */ -/* seen by get_trans always has free_space > 10, */ -/* so get_trans returns it directly without queuing. */ -/* ------------------------------------------------- */ -BLE_LOG_IRAM_ATTR -void ble_log_lbm_stream_write(ble_log_lbm_t *lbm, ble_log_src_t src_code, - const uint8_t *data, size_t len) -{ - while (len > 0) { - ble_log_prph_trans_t **trans = ble_log_lbm_get_trans(lbm, 0); - if (!trans) { - ble_log_stat_mgr_update(src_code, len, true); - return; - } - - if ((*trans)->pos == 0) { - (*trans)->pos = BLE_LOG_FRAME_HEAD_LEN; - } - - uint16_t available = BLE_LOG_TRANS_FREE_SPACE((*trans)); - if (available <= BLE_LOG_FRAME_TAIL_LEN) { - ble_log_lbm_stream_seal(trans, src_code); - continue; - } - available -= BLE_LOG_FRAME_TAIL_LEN; - - size_t to_write = (len < available) ? len : available; - BLE_LOG_MEMCPY((*trans)->buf + (*trans)->pos, data, to_write); - (*trans)->pos += to_write; - data += to_write; - len -= to_write; - - if (BLE_LOG_TRANS_FREE_SPACE((*trans)) <= BLE_LOG_FRAME_OVERHEAD) { - ble_log_lbm_stream_seal(trans, src_code); - } - } -} - -BLE_LOG_IRAM_ATTR -void ble_log_lbm_stream_flush(ble_log_lbm_t *lbm, ble_log_src_t src_code) -{ - int trans_idx = lbm->trans_idx; - for (int i = 0; i < BLE_LOG_TRANS_BUF_CNT; i++) { - ble_log_prph_trans_t **trans = &(lbm->trans[trans_idx]); - if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE((*trans)->prph_owned) && - (*trans)->pos > BLE_LOG_FRAME_HEAD_LEN) { - ble_log_lbm_stream_seal(trans, src_code); - } - trans_idx = (trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); - } -} -#endif /* BLE_LOG_UART_REDIR_ENABLED */ - -BLE_LOG_STATIC void ble_log_emit_buf_util(ble_log_lbm_t *lbm, uint8_t lbm_id) -{ - ble_log_buf_util_t util = { - .int_src_code = BLE_LOG_INT_SRC_BUF_UTIL, - .lbm_id = lbm_id, - .trans_cnt = BLE_LOG_TRANS_BUF_CNT, - .inflight_peak = (uint8_t)__atomic_load_n(&lbm->trans_inflight_peak, - __ATOMIC_RELAXED), - }; - ble_log_write_hex(BLE_LOG_SRC_INTERNAL, - (const uint8_t *)&util, sizeof(ble_log_buf_util_t)); -} - -void ble_log_write_buf_util(void) -{ - if (!ble_log_lbm_ref_acquire(true)) { - return; - } - - ble_log_emit_buf_util(&lbm_ctx->spin_task, - BLE_LOG_BUF_UTIL_MAKE_ID(BLE_LOG_BUF_UTIL_POOL_COMMON_TASK, 0)); - for (int i = 0; i < BLE_LOG_LBM_ATOMIC_TASK_CNT; i++) { - ble_log_emit_buf_util(&lbm_ctx->atomic_pool_task[i], - BLE_LOG_BUF_UTIL_MAKE_ID(BLE_LOG_BUF_UTIL_POOL_COMMON_TASK, 1 + i)); - } - - ble_log_emit_buf_util(&lbm_ctx->spin_isr, - BLE_LOG_BUF_UTIL_MAKE_ID(BLE_LOG_BUF_UTIL_POOL_COMMON_ISR, 0)); - for (int i = 0; i < BLE_LOG_LBM_ATOMIC_ISR_CNT; i++) { - ble_log_emit_buf_util(&lbm_ctx->atomic_pool_isr[i], - BLE_LOG_BUF_UTIL_MAKE_ID(BLE_LOG_BUF_UTIL_POOL_COMMON_ISR, 1 + i)); - } - -#if CONFIG_BLE_LOG_LL_ENABLED - ble_log_emit_buf_util(&lbm_ctx->lbm_ll_task, - BLE_LOG_BUF_UTIL_MAKE_ID(BLE_LOG_BUF_UTIL_POOL_LL, 0)); - ble_log_emit_buf_util(&lbm_ctx->lbm_ll_hci, - BLE_LOG_BUF_UTIL_MAKE_ID(BLE_LOG_BUF_UTIL_POOL_LL, 1)); -#endif - -#if BLE_LOG_UART_REDIR_ENABLED - ble_log_lbm_t *redir_lbm = ble_log_prph_get_redir_lbm(); - if (redir_lbm) { - ble_log_emit_buf_util(redir_lbm, - BLE_LOG_BUF_UTIL_MAKE_ID(BLE_LOG_BUF_UTIL_POOL_REDIR, 0)); - } -#endif - - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); -} - -void ble_log_write_final_stat(void) -{ - if (!ble_log_lbm_ref_acquire(false)) { - return; - } - - ble_log_final_stat_t final_stat; - final_stat.int_src_code = BLE_LOG_INT_SRC_FINAL_STAT; - final_stat.src_cnt = BLE_LOG_SRC_MAX; - - BLE_LOG_ENTER_CRITICAL(); - for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { - ble_log_final_stat_entry_t *entry = &final_stat.entries[i]; - entry->src_code = i; - BLE_LOG_MEMCPY(&entry->written_frame_cnt, - &stat_mgr_ctx[i]->written_frame_cnt, - 4 * sizeof(uint32_t)); - } - BLE_LOG_EXIT_CRITICAL(); - - ble_log_write_internal((const uint8_t *)&final_stat, sizeof(final_stat)); - - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); -} - -/* ------------------------ */ -/* PUBLIC INTERFACE */ -/* ------------------------ */ -bool ble_log_enable(bool enable) -{ - if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_inited)) { - return false; - } - BLE_LOG_ATOMIC_STORE_RELEASE(lbm_enabled, enable); - return true; -} - -void ble_log_flush(void) -{ - /* Prevent concurrent flush — two concurrent callers would deadlock on - * the ref_count spin-wait (both hold a ref, both wait for ref_count <= 1). - * Second caller returns immediately instead of deadlocking. */ - if (__atomic_test_and_set(&flush_in_progress, __ATOMIC_ACQUIRE)) { - return; - } - - if (!ble_log_lbm_ref_acquire(false)) { - goto clear; - } - ble_log_write_enh_stat(); - ble_log_write_buf_util(); - - /* Write BLE Log flush log */ - ble_log_info_t ble_log_info = { - .int_src_code = BLE_LOG_INT_SRC_FLUSH, - .version = BLE_LOG_VERSION, - }; - ble_log_write_hex(BLE_LOG_SRC_INTERNAL, (const uint8_t *)&ble_log_info, sizeof(ble_log_info_t)); - - /* Disable module and wait for all other references to release */ - bool lbm_enabled_copy = BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled); - - BLE_LOG_ATOMIC_STORE_RELEASE(lbm_enabled, false); - if (!ble_log_ref_count_wait(&lbm_ref_count, 1)) { - ESP_LOGE(TAG, "Timed out waiting for BLE Log writers"); - goto fail; - } - - if (!ble_log_lbm_flush_all_trans()) { - goto fail; - } - - ble_log_write_final_stat(); - - if (!ble_log_lbm_flush_all_trans()) { - goto fail; - } - ble_log_lbm_reset_stats(); - -fail: - /* Resume enable status after a completed or failed flush. */ - BLE_LOG_ATOMIC_STORE_RELEASE(lbm_enabled, lbm_enabled_copy); - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); -clear: - __atomic_clear(&flush_in_progress, __ATOMIC_RELEASE); -} - -BLE_LOG_IRAM_ATTR -bool ble_log_write_hex(ble_log_src_t src_code, const uint8_t *addr, size_t len) -{ - bool ret = false; - - if (!ble_log_lbm_ref_acquire(true)) { - return false; - } - - ret = ble_log_write_hex_core(src_code, addr, len); - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); - return ret; -} - -BLE_LOG_IRAM_ATTR -bool ble_log_write_internal(const uint8_t *addr, size_t len) -{ - bool ret = false; - - if (!ble_log_lbm_ref_acquire(false)) { - return false; - } - - ret = ble_log_write_hex_core(BLE_LOG_SRC_INTERNAL, addr, len); - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); - return ret; -} - -#if CONFIG_BLE_LOG_LL_ENABLED -BLE_LOG_IRAM_ATTR -void ble_log_write_hex_ll(uint32_t len, const uint8_t *addr, - uint32_t len_append, const uint8_t *addr_append, uint32_t flag) -{ - if (!ble_log_lbm_ref_acquire(true)) { - return; - } - - /* Source code shall be determined before LBM determination */ - ble_log_src_t src_code = BLE_LOG_SRC_MAX; - bool use_ll_task = false; - if (flag & BIT(BLE_LOG_LL_FLAG_ISR)) { - src_code = BLE_LOG_SRC_LL_ISR; - } else if (flag & BIT(BLE_LOG_LL_FLAG_HCI)) { - src_code = BLE_LOG_SRC_LL_HCI; - } else if (flag & BIT(BLE_LOG_LL_FLAG_HCI_UPSTREAM)) { - src_code = BLE_LOG_SRC_HCI; - } else { - src_code = BLE_LOG_SRC_LL_TASK; - use_ll_task = true; - } - bool omdata = flag & BIT(BLE_LOG_LL_FLAG_OMDATA); - - /* Determine LBM and get transport */ - ble_log_lbm_t *lbm; - ble_log_prph_trans_t **trans; - size_t payload_len; - - if (BLE_LOG_IN_ISR()) { - /* os_mbuf_copydata is in flash and not safe to call from ISR */ - omdata = false; - - payload_len = len + len_append; - if (!ble_log_lbm_acquire_trans(payload_len, &lbm, &trans)) { - goto failed; - } - } else { - if (use_ll_task) { - lbm = &(lbm_ctx->lbm_ll_task); - } else { - lbm = &(lbm_ctx->lbm_ll_hci); -#if CONFIG_BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT_ENABLED - if (len_append > CONFIG_BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT) { - len_append = CONFIG_BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT; - } -#endif /* CONFIG_BLE_LOG_LL_HCI_LOG_PAYLOAD_LEN_LIMIT_ENABLED */ - } - payload_len = len + len_append; - trans = ble_log_lbm_get_trans(lbm, payload_len); - if (!trans) { - /* LL pools use LOCK_NONE, release is no-op */ - goto failed; - } - } - - /* Write transport */ - ble_log_lbm_write_trans(trans, src_code, addr, len, addr_append, len_append, omdata); - - ble_log_lbm_release(lbm); - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); - return; - -failed: - if (BLE_LOG_ATOMIC_LOAD_RELAXED(lbm_inited)) { - ble_log_stat_mgr_update(src_code, payload_len, true); - } - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); - return; -} -#endif /* CONFIG_BLE_LOG_LL_ENABLED */ - -void ble_log_dump_to_console(void) -{ - if (!ble_log_lbm_ref_acquire(false)) { - return; - } - - int trans_idx; - ble_log_lbm_t *lbm; - ble_log_prph_trans_t *trans; - BLE_LOG_ENTER_CRITICAL(); - BLE_LOG_CONSOLE("[BLE_LOG_DUMP_START:\n"); - for (int i = 0; i < BLE_LOG_LBM_CNT; i++) { - lbm = &(lbm_ctx->lbm_pool[i]); - trans_idx = lbm->trans_idx; - for (int j = 0; j < BLE_LOG_TRANS_BUF_CNT; j++) { - trans = lbm->trans[trans_idx]; - BLE_LOG_FEED_WDT(); - - for (int k = 0; k < trans->size; k++) { - BLE_LOG_CONSOLE("%02x ", trans->buf[k]); - if (!(k & 0xFF)) { - BLE_LOG_FEED_WDT(); - } - } - trans_idx = (trans_idx + 1) & (BLE_LOG_TRANS_BUF_CNT - 1); - } - } - BLE_LOG_CONSOLE("\n:BLE_LOG_DUMP_END]\n\n"); - BLE_LOG_EXIT_CRITICAL(); - - BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); - return; -} diff --git a/components/bt/common/ble_log/src/ble_log_lbm_v2.c b/components/bt/common/ble_log/src/ble_log_lbm_v2.c index c17264de38d..092596489f4 100644 --- a/components/bt/common/ble_log/src/ble_log_lbm_v2.c +++ b/components/bt/common/ble_log/src/ble_log_lbm_v2.c @@ -13,6 +13,7 @@ #include "ble_log_rt.h" #include "esp_timer.h" +#include "esp_chip_info.h" #if CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER #if CONFIG_BT_DUAL_MODE_ARCH @@ -35,6 +36,34 @@ /* Single-instruction clock read; a function would add an IRAM call site. */ #define BLE_LOG_TIMESTAMP_NOW() ((uint32_t)esp_timer_get_time()) +#if CONFIG_BT_CONTROLLER_ENABLED +#if CONFIG_IDF_TARGET_ESP32 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32S3 +extern const char *btdm_controller_get_compile_version(void); +#define BLE_LOG_CONTROLLER_GET_COMMIT() btdm_controller_get_compile_version() +#elif !CONFIG_BT_DUAL_MODE_ARCH || CONFIG_BT_CTRL_BLE_ENABLE +/* BR/EDR-only dual-mode builds do not link the BLE controller lib */ +extern char *ble_controller_get_compile_version(void); +#define BLE_LOG_CONTROLLER_GET_COMMIT() ble_controller_get_compile_version() +#endif +#if CONFIG_BT_DUAL_MODE_ARCH +/* BTDM common lib (dual-mode arch only) */ +extern const char *r_btdm_get_compile_version(void); +#define BLE_LOG_BTDM_COMMON_GET_COMMIT() r_btdm_get_compile_version() +#endif +#endif + +#if CONFIG_BLE_MESH && CONFIG_BLE_MESH_V11_SUPPORT +/* "Bluetooth Mesh v1.1 commit: " */ +extern const char bt_mesh_v11_commit_str[]; +#endif + +#if CONFIG_BT_AUDIO && CONFIG_SOC_BLE_AUDIO_SUPPORTED +extern const char *lib_audio_commit_get(void); +#endif + +_Static_assert(sizeof(ble_log_version_info_t) == 58, + "Unexpected BLE Log version info frame size"); + /* ------------------------------- */ /* Global Pool Context */ /* ------------------------------- */ @@ -77,6 +106,8 @@ typedef struct { } ble_log_pool_claim_t; /* VARIABLE */ +/* Process-lifetime count: a caller rejected by a closed gate may still owe + * its balancing release when the next LBM epoch starts. Never reset it. */ BLE_LOG_STATIC BLE_LOG_DRAM_ATTR volatile uint32_t lbm_ref_count = 0; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t lbm_inited = 0; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t lbm_enabled = 0; @@ -98,6 +129,7 @@ extern void ble_log_test_claim_pre_publish_hook(void) __attribute__((weak)); extern void ble_log_test_enable_before_lifecycle_lock_hook(void) __attribute__((weak)); extern void ble_log_test_disable_before_wake_hook(void) __attribute__((weak)); extern void ble_log_test_claim_locked_hook(void) __attribute__((weak)); +extern void ble_log_test_init_snapshot_before_acquire_hook(void) __attribute__((weak)); #endif BLE_LOG_IRAM_ATTR BLE_LOG_STATIC @@ -114,6 +146,7 @@ BLE_LOG_STATIC ble_log_prph_trans_t *ble_log_pool_acquire(size_t log_len, bool use_reserve, bool wait); BLE_LOG_STATIC void ble_log_pool_seal_and_send(ble_log_prph_trans_t *trans); +BLE_LOG_STATIC void ble_log_pool_seal_open_trans(void); BLE_LOG_STATIC void ble_log_pool_write_frame(ble_log_prph_trans_t *trans, uint32_t frame_sn, uint8_t source_meta, @@ -122,10 +155,14 @@ BLE_LOG_STATIC void ble_log_pool_write_frame(ble_log_prph_trans_t *trans, const uint8_t *addr, uint16_t len, const uint8_t *addr_append, uint16_t len_append, bool omdata); +BLE_LOG_STATIC void ble_log_pool_finish_frame(ble_log_prph_trans_t *trans, + uint16_t payload_len, + ble_log_stat_mgr_t *stat_mgr); BLE_LOG_STATIC void ble_log_stat_mgr_mark_lost(ble_log_src_t src_code); BLE_LOG_STATIC void ble_log_snapshot_stats(ble_log_source_stat_t *snapshots); BLE_LOG_STATIC bool ble_log_pool_flush_all_trans(void); BLE_LOG_STATIC void ble_log_pool_wake_all(void); +BLE_LOG_STATIC void ble_log_internal_version_info_init(void); /* ------------------------- */ /* BITMAP HELPERS */ @@ -151,11 +188,7 @@ ble_log_pool_update_peak(uint32_t free_bitmap) { uint32_t used = BLE_LOG_POOL_TRANS_CNT - __builtin_popcount(free_bitmap & BLE_LOG_POOL_ALL_MASK); - uint32_t peak = BLE_LOG_ATOMIC_LOAD_RELAXED(g_pool.inflight_peak); - while (used > peak && - !__atomic_compare_exchange_n(&g_pool.inflight_peak, &peak, used, true, - __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { - } + ble_log_atomic_update_peak(&g_pool.inflight_peak, used); } /* A newly available shared transport satisfies one waiter. The SEQ_CST fence @@ -212,6 +245,19 @@ BLE_LOG_STATIC void ble_log_pool_wake_all(void) } } +/* Waiter registration is one atomic RMW with ACQ_REL on both sides of the + * park: the register must be visible to a concurrent gate close before the + * task blocks, and the unregister must pair with the wake that released + * it. The RMW is a CAS loop plus barrier on Xtensa (several instructions), + * so it lives here once instead of at the four call sites of the IRAM + * acquire path. delta is +1 or -1. */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void +ble_log_pool_waiter_adjust(int delta) +{ + __atomic_add_fetch(&g_pool.waiting_task_count, (uint32_t)delta, + __ATOMIC_ACQ_REL); +} + /* -------------------------------------- */ /* UNIFIED TRANSPORT RECYCLE */ /* -------------------------------------- */ @@ -264,6 +310,27 @@ BLE_LOG_IRAM_ATTR void ble_log_pool_seal_and_send(ble_log_prph_trans_t *trans) ble_log_rt_submit_trans(trans); } +/* Seals every OPEN transport in the pool. Caller contract: writers are + * gone (producer gate closed and drained), so each lock is uncontended + * and the acquire waits are bounded. */ +BLE_LOG_STATIC void ble_log_pool_seal_open_trans(void) +{ + for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { + if (!(BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.open_bitmap) & BIT(id))) { + continue; + } + ble_log_prph_trans_t *trans = g_pool.trans[id]; + while (!BLE_LOG_CAS_ACQUIRE(&trans->atomic_lock)) { + } + if (BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state) == BLE_LOG_TRANS_STATE_OPEN && + trans->pos > 0) { + ble_log_pool_seal_and_send(trans); /* releases the lock */ + } else { + BLE_LOG_CAS_RELEASE(&trans->atomic_lock); + } + } +} + /* -------------------------------------- */ /* AVAILABLE BUFFER CLAIM */ /* -------------------------------------- */ @@ -399,16 +466,16 @@ ble_log_prph_trans_t *ble_log_pool_acquire(size_t log_len, return trans; } - __atomic_add_fetch(&g_pool.waiting_task_count, 1, __ATOMIC_ACQ_REL); + ble_log_pool_waiter_adjust(1); __atomic_thread_fence(__ATOMIC_SEQ_CST); trans = ble_log_pool_try_claim_available(frame_len, use_reserve); if (trans) { - __atomic_sub_fetch(&g_pool.waiting_task_count, 1, __ATOMIC_ACQ_REL); + ble_log_pool_waiter_adjust(-1); return trans; } if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled)) { - __atomic_sub_fetch(&g_pool.waiting_task_count, 1, __ATOMIC_ACQ_REL); + ble_log_pool_waiter_adjust(-1); return NULL; } @@ -417,7 +484,7 @@ ble_log_prph_trans_t *ble_log_pool_acquire(size_t log_len, BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); xSemaphoreTake(g_pool.sem, portMAX_DELAY); BLE_LOG_REF_COUNT_ACQUIRE_SEQ_CST(&lbm_ref_count); - __atomic_sub_fetch(&g_pool.waiting_task_count, 1, __ATOMIC_ACQ_REL); + ble_log_pool_waiter_adjust(-1); if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled)) { return NULL; @@ -466,11 +533,21 @@ void ble_log_pool_write_frame(ble_log_prph_trans_t *trans, uint32_t frame_sn, BLE_LOG_MEMCPY(payload, addr_append, len_append); } } - /* Data integrity check */ - uint32_t checksum = ble_log_fast_checksum((const uint8_t *)buf, BLE_LOG_FRAME_HEAD_LEN + payload_len); + /* Data integrity check, transport update and completion. */ + ble_log_pool_finish_frame(trans, payload_len, stat_mgr); +} + +/* Completes a frame whose head and payload are already in place at + * trans->pos: writes the checksum tail, advances pos, publishes the + * written-frame stat, then seals or publishes the transport. */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC +void ble_log_pool_finish_frame(ble_log_prph_trans_t *trans, uint16_t payload_len, + ble_log_stat_mgr_t *stat_mgr) +{ + uint8_t *buf = trans->buf + trans->pos; + uint32_t checksum = ble_log_fast_checksum(buf, BLE_LOG_FRAME_HEAD_LEN + payload_len); BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN + payload_len, &checksum, BLE_LOG_FRAME_TAIL_LEN); - /* Update transport and publish the completed core-stat record. */ trans->pos += payload_len + BLE_LOG_FRAME_OVERHEAD; BLE_LOG_ATOMIC_ADD_RELAXED(stat_mgr->counters.written_frame_cnt, 1); @@ -556,13 +633,12 @@ void ble_log_commit(uint32_t handle, size_t actual_len) if (actual_len == 0 || actual_len > claim->max_len) { ble_log_stat_mgr_mark_lost(src_code); if (trans->pos == 0) { - /* Returning straight to FREE bypasses seal_and_send: also drop - * any pending-seal marker here. */ - BLE_LOG_ATOMIC_STORE_RELAXED(trans->pending_seal, false); - BLE_LOG_ATOMIC_STORE_RELEASE(trans->state, BLE_LOG_TRANS_STATE_FREE); - ble_log_pool_bitmap_set(&g_pool.free_bitmap, trans->id); + /* Returning straight to FREE: recycle covers pos, pending_seal, + * state, the free-bitmap hint and the waiter wake. The transport + * is in no bitmap while CLAIMED, so releasing the lock first + * keeps it invisible to scanners until recycle publishes it. */ BLE_LOG_CAS_RELEASE(&trans->atomic_lock); - ble_log_pool_notify_waiter(trans->id); + ble_log_lbm_recycle_trans(trans); } else { ble_log_pool_publish_open_and_unlock(trans); } @@ -571,7 +647,6 @@ void ble_log_commit(uint32_t handle, size_t actual_len) } uint16_t payload_len = (uint16_t)(sizeof(uint32_t) + actual_len); - uint8_t *buf = trans->buf + trans->pos; ble_log_stat_mgr_t *stat_mgr = &stat_mgr_ctx[src_code]; uint8_t source_meta = BLE_LOG_MAKE_SOURCE_META(src_code, claim->non_yield); @@ -579,21 +654,9 @@ void ble_log_commit(uint32_t handle, size_t actual_len) .length = payload_len, .frame_meta = BLE_LOG_MAKE_FRAME_META(source_meta, claim->frame_sn), }; - BLE_LOG_MEMCPY(buf, &frame_head, BLE_LOG_FRAME_HEAD_LEN); + BLE_LOG_MEMCPY(trans->buf + trans->pos, &frame_head, BLE_LOG_FRAME_HEAD_LEN); + ble_log_pool_finish_frame(trans, payload_len, stat_mgr); - uint32_t checksum = ble_log_fast_checksum(buf, - BLE_LOG_FRAME_HEAD_LEN + payload_len); - BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN + payload_len, - &checksum, BLE_LOG_FRAME_TAIL_LEN); - - trans->pos += payload_len + BLE_LOG_FRAME_OVERHEAD; - BLE_LOG_ATOMIC_ADD_RELAXED(stat_mgr->counters.written_frame_cnt, 1); - - if (BLE_LOG_TRANS_FREE_SPACE(trans) <= BLE_LOG_FRAME_OVERHEAD) { - ble_log_pool_seal_and_send(trans); - } else { - ble_log_pool_publish_open_and_unlock(trans); - } BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); } @@ -628,16 +691,13 @@ bool ble_log_lbm_init(void) goto exit; } - /* Allocate the transport buffers of the global pool. */ + /* Allocate the transport buffers of the global pool. trans_init zeroes + * the storage, so only the non-zero identity fields need setting. */ for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { if (!ble_log_prph_trans_init(&(g_pool.trans[id]), BLE_LOG_POOL_TRANS_SIZE)) { goto exit; } g_pool.trans[id]->id = (uint8_t)id; - g_pool.trans[id]->owner_kind = BLE_LOG_TRANS_OWNER_POOL; - g_pool.trans[id]->state = BLE_LOG_TRANS_STATE_FREE; - g_pool.trans[id]->atomic_lock = 0; - g_pool.trans[id]->pending_seal = 0; } if (!ble_log_prph_trans_init(&internal_trans, BLE_LOG_INTERNAL_TRANS_SIZE)) { @@ -645,8 +705,6 @@ bool ble_log_lbm_init(void) } internal_trans->id = BLE_LOG_TRANS_ID_NONE; internal_trans->owner_kind = BLE_LOG_TRANS_OWNER_INTERNAL; - internal_trans->state = BLE_LOG_TRANS_STATE_FREE; - internal_trans->atomic_lock = 0; g_pool.free_bitmap = BLE_LOG_POOL_ALL_MASK; g_pool.open_bitmap = 0; @@ -657,8 +715,8 @@ bool ble_log_lbm_init(void) internal_snapshot.int_src_code = BLE_LOG_INT_SRC_SNAPSHOT; internal_snapshot.pool.trans_cnt = BLE_LOG_POOL_TRANS_CNT; internal_snapshot.pool.non_yield_reserve_cnt = BLE_LOG_POOL_NON_YIELD_RESERVE_CNT; + ble_log_internal_version_info_init(); - lbm_ref_count = 0; BLE_LOG_ATOMIC_STORE_RELEASE(lbm_enabled, false); BLE_LOG_ATOMIC_STORE_RELEASE(lbm_inited, true); return true; @@ -692,7 +750,10 @@ void ble_log_lbm_begin_deinit(void) __attribute__((noinline)) BLE_LOG_STATIC void ble_log_snapshot_stats(ble_log_source_stat_t *snapshots) { - BLE_LOG_ENTER_CRITICAL(); + /* The counter writers (claim/commit/write_hex_ll, some in IRAM/ISR + * context) use relaxed atomics and never take the spinlock, so an + * exclusive section here would exclude nobody. The relaxed loads + * are the whole protection. */ for (int i = 0; i < BLE_LOG_SRC_CORE_COUNT; i++) { ble_log_stat_mgr_t *stat_mgr = &stat_mgr_ctx[BLE_LOG_SRC_CORE_FIRST + i]; @@ -701,7 +762,6 @@ void ble_log_snapshot_stats(ble_log_source_stat_t *snapshots) snapshots[i].lost_frame_cnt = BLE_LOG_ATOMIC_LOAD_RELAXED(stat_mgr->counters.lost_frame_cnt); } - BLE_LOG_EXIT_CRITICAL(); } void ble_log_lbm_deinit(void) @@ -724,30 +784,71 @@ bool ble_log_lbm_is_enabled(void) return BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled); } -void ble_log_internal_set_version_info(const ble_log_version_info_t *version_info) +/* Copies a NUL-terminated commit string into a fixed-width zero-padded field */ +BLE_LOG_STATIC void ble_log_commit_copy(uint8_t *dst, const char *src, size_t len) { - if (version_info) { - BLE_LOG_MEMCPY(&internal_snapshot.version_info, version_info, - sizeof(*version_info)); + BLE_LOG_MEMCPY(dst, src, strnlen(src, len)); +} + +/* Fills the version-info record carried by internal snapshots, in place: + * no intermediate copy. ble_log_lbm_init zeroes internal_snapshot first. */ +BLE_LOG_STATIC void ble_log_internal_version_info_init(void) +{ + ble_log_version_info_t *vi = &internal_snapshot.version_info; + vi->int_src_code = BLE_LOG_INT_SRC_VERSION_INFO; + vi->version = BLE_LOG_VERSION; +#ifdef BLE_LOG_IDF_COMMIT + BLE_LOG_MEMCPY(vi->idf_commit, BLE_LOG_IDF_COMMIT, + BLE_LOG_IDF_COMMIT_LEN); +#endif +#if CONFIG_BT_CONTROLLER_ENABLED && defined(BLE_LOG_CONTROLLER_GET_COMMIT) + ble_log_commit_copy(vi->controller_commit, + BLE_LOG_CONTROLLER_GET_COMMIT(), BLE_LOG_LIB_COMMIT_LEN); +#endif +#if CONFIG_BT_CONTROLLER_ENABLED && defined(BLE_LOG_BTDM_COMMON_GET_COMMIT) + ble_log_commit_copy(vi->btdm_common_commit, + BLE_LOG_BTDM_COMMON_GET_COMMIT(), BLE_LOG_LIB_COMMIT_LEN); +#endif +#if CONFIG_BLE_MESH && CONFIG_BLE_MESH_V11_SUPPORT + const char *mesh_commit = strrchr(bt_mesh_v11_commit_str, ' '); + if (mesh_commit) { + ble_log_commit_copy(vi->mesh_commit, mesh_commit + 1, + BLE_LOG_LIB_COMMIT_LEN); } +#endif +#if CONFIG_BT_AUDIO && CONFIG_SOC_BLE_AUDIO_SUPPORTED + ble_log_commit_copy(vi->audio_commit, lib_audio_commit_get(), + BLE_LOG_LIB_COMMIT_LEN); +#endif + esp_chip_info_t chip_info; + esp_chip_info(&chip_info); + vi->chip_model = (uint16_t)chip_info.model; + vi->chip_revision = chip_info.revision; } bool ble_log_internal_snapshot(uint16_t reason_flags, const ble_log_ts_info_t *ts_info, - bool required) + bool wait_for_transport) { - const uint32_t *gate = required ? &lbm_inited : &lbm_enabled; - if (!ble_log_ref_count_try_acquire(&lbm_ref_count, gate)) { +#if CONFIG_BLE_LOG_PRPH_TEST + if ((reason_flags & BLE_LOG_SNAPSHOT_REASON_INIT) && + ble_log_test_init_snapshot_before_acquire_hook) { + ble_log_test_init_snapshot_before_acquire_hook(); + } +#endif + /* Internal Snapshots are system output and outlive the public producer + * gate. Only LBM teardown stops new snapshots. */ + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_inited)) { return false; } if (!internal_trans) { goto failed; } - /* Capture the complete occurrence sample before any dedicated-buffer - * drain or wait. Without a TS sync sample, esp_ts comes from the frame - * timestamp and os_ts from the current tick. */ - uint32_t timestamp = ts_info ? ts_info->esp_ts : BLE_LOG_TIMESTAMP_NOW(); + /* The caller sampled the clocks (with or without a sync IO toggle) + * before any dedicated-buffer drain or wait; the frame timestamp is + * that sample's esp_ts. */ + uint32_t timestamp = ts_info->esp_ts; TickType_t start_tick = xTaskGetTickCount(); for (;;) { if (BLE_LOG_CAS_ACQUIRE(&internal_trans->atomic_lock)) { @@ -757,7 +858,7 @@ bool ble_log_internal_snapshot(uint16_t reason_flags, } BLE_LOG_CAS_RELEASE(&internal_trans->atomic_lock); } - if (!required || + if (!wait_for_transport || (xTaskGetTickCount() - start_tick) >= BLE_LOG_WAIT_TIMEOUT_TICKS) { goto lost; } @@ -766,11 +867,10 @@ bool ble_log_internal_snapshot(uint16_t reason_flags, } internal_snapshot.reason_flags = reason_flags; - internal_snapshot.ts.io_level = ts_info ? ts_info->io_level : 0; - internal_snapshot.ts.lc_ts = ts_info ? ts_info->lc_ts : 0; + internal_snapshot.ts.io_level = ts_info->io_level; + internal_snapshot.ts.lc_ts = ts_info->lc_ts; internal_snapshot.ts.esp_ts = timestamp; - internal_snapshot.ts.os_ts = ts_info ? ts_info->os_ts - : pdTICKS_TO_MS(xTaskGetTickCount()); + internal_snapshot.ts.os_ts = ts_info->os_ts; uint32_t free_bitmap = BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.free_bitmap) & BLE_LOG_POOL_ALL_MASK; internal_snapshot.pool.inflight = @@ -815,19 +915,7 @@ BLE_LOG_STATIC bool ble_log_pool_flush_all_trans(void) { /* New writes are disabled, so every held lock will be released after its * current frame copy. Seal every remaining OPEN transport. */ - for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { - ble_log_prph_trans_t *trans = g_pool.trans[id]; - while (!BLE_LOG_CAS_ACQUIRE(&trans->atomic_lock)) { - } - if (BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state) == BLE_LOG_TRANS_STATE_OPEN && - trans->pos > 0) { - ble_log_pool_bitmap_clear(&g_pool.open_bitmap, id); - ble_log_pool_seal_and_send(trans); /* releases the lock */ - } else { - BLE_LOG_CAS_RELEASE(&trans->atomic_lock); - } - } - + ble_log_pool_seal_open_trans(); TickType_t start_tick = xTaskGetTickCount(); while ((BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.free_bitmap) & BLE_LOG_POOL_ALL_MASK) != BLE_LOG_POOL_ALL_MASK) { @@ -842,9 +930,11 @@ BLE_LOG_STATIC bool ble_log_pool_flush_all_trans(void) return true; } -void ble_log_lbm_flush_open_transports(void) +void ble_log_lbm_flush_open_trans(void) { - if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_enabled)) { + /* Periodic OPEN flush is system output, independent from the public + * producer gate. LBM teardown remains the lifetime boundary. */ + if (!ble_log_ref_count_try_acquire(&lbm_ref_count, &lbm_inited)) { return; } @@ -873,26 +963,12 @@ void ble_log_lbm_flush_open_transports(void) BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); } -void ble_log_lbm_drain_open_transports(void) +void ble_log_lbm_drain_open_trans(void) { /* Contract (see header): called after ble_log_lbm_begin_deinit() and * before ble_log_rt_deinit(). The producer gate is closed and writers * have drained, so no lock is held for longer than one frame copy. */ - for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { - if (!(BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.open_bitmap) & BIT(id))) { - continue; - } - ble_log_prph_trans_t *trans = g_pool.trans[id]; - while (!BLE_LOG_CAS_ACQUIRE(&trans->atomic_lock)) { - } - if (BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state) == BLE_LOG_TRANS_STATE_OPEN && - trans->pos > 0) { - ble_log_pool_seal_and_send(trans); /* releases the lock */ - } else { - BLE_LOG_CAS_RELEASE(&trans->atomic_lock); - } - } - + ble_log_pool_seal_open_trans(); /* Hand the sealed buffers to the peripheral before the runtime queue * is destroyed; the peripheral deinit wait completes the delivery. */ (void)ble_log_rt_drain(); @@ -973,8 +1049,15 @@ void ble_log_flush(void) } #endif - if (!ble_log_pool_flush_all_trans() || - !ble_log_internal_snapshot(BLE_LOG_SNAPSHOT_REASON_FLUSH, NULL, true)) { + if (!ble_log_pool_flush_all_trans()) { + goto restore; + } + + /* One-shot FLUSH snapshot: sample after the pool drained so the clocks + * reflect the flush completion; no sync IO toggle (periodic flow only). */ + ble_log_ts_info_t ts_info; + ble_log_rt_ts_sample(&ts_info, false); + if (!ble_log_internal_snapshot(BLE_LOG_SNAPSHOT_REASON_FLUSH, &ts_info, true)) { goto restore; } diff --git a/components/bt/common/ble_log/src/ble_log_redir.c b/components/bt/common/ble_log/src/ble_log_redir.c index baad2794791..009fac42a30 100644 --- a/components/bt/common/ble_log/src/ble_log_redir.c +++ b/components/bt/common/ble_log/src/ble_log_redir.c @@ -50,7 +50,6 @@ void ble_log_redir_seal(ble_log_prph_trans_t *trans, ble_log_src_t src_code) uint16_t payload_len = trans->pos - BLE_LOG_FRAME_HEAD_LEN; ble_log_redir_t *redir = ble_log_prph_get_redir_lbm(); - BLE_LOG_ASSERT(redir); /* REDIR keeps its own stream sequence: a raw console stream is not a * log attempt, so it never eats Global SNs or fakes loss gaps. The * stream has no core-stat slot. */ @@ -66,11 +65,7 @@ void ble_log_redir_seal(ble_log_prph_trans_t *trans, ble_log_src_t src_code) trans->pos += BLE_LOG_FRAME_TAIL_LEN; uint32_t infl = __atomic_add_fetch(&redir->inflight, 1, __ATOMIC_RELAXED); - uint32_t peak = BLE_LOG_ATOMIC_LOAD_RELAXED(redir->inflight_peak); - while (infl > peak && - !__atomic_compare_exchange_n(&redir->inflight_peak, &peak, infl, true, - __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { - } + ble_log_atomic_update_peak(&redir->inflight_peak, infl); BLE_LOG_ATOMIC_STORE_RELAXED(trans->state, BLE_LOG_TRANS_STATE_SENDING); ble_log_rt_submit_trans(trans); diff --git a/components/bt/common/ble_log/src/ble_log_rt.c b/components/bt/common/ble_log/src/ble_log_rt.c index 5482092d1e6..5c24513ccda 100644 --- a/components/bt/common/ble_log/src/ble_log_rt.c +++ b/components/bt/common/ble_log/src/ble_log_rt.c @@ -16,7 +16,6 @@ #include "esp_log.h" #include "esp_timer.h" -#include "esp_chip_info.h" #if CONFIG_BLE_LOG_LL_ENABLED #include "esp_bt.h" #endif @@ -29,10 +28,14 @@ /* Link-layer clock sample; 0 when the controller exports no accessor. */ #if CONFIG_BLE_LOG_LL_ENABLED #if CONFIG_BT_DUAL_MODE_ARCH -/* The dual-mode-arch controller (ESP32-H4, ESP32-S31) does not export its - * link-layer timer yet; its accessor is r_sched_timer_getCurrentTimeU32. - * Call it once the controller libraries export the symbol. */ -#define BLE_LOG_GET_LC_TS 0 +/* Temporary fallback until the dual-mode controller libraries export the + * link-layer timer accessor. A strong library definition overrides it. */ +uint32_t r_sched_timer_getCurrentTimeU32(void) __attribute__((weak)); +uint32_t r_sched_timer_getCurrentTimeU32(void) +{ + return 0; +} +#define BLE_LOG_GET_LC_TS r_sched_timer_getCurrentTimeU32() /* ESP BLE Controller Gen 2 */ #elif defined(CONFIG_IDF_TARGET_ESP32H2) || defined(CONFIG_IDF_TARGET_ESP32C6) || defined(CONFIG_IDF_TARGET_ESP32C5) ||\ defined(CONFIG_IDF_TARGET_ESP32C61) || defined(CONFIG_IDF_TARGET_ESP32H21) @@ -53,9 +56,6 @@ extern uint32_t lld_read_clock_us(void); #define BLE_LOG_GET_LC_TS 0 #endif /* CONFIG_BLE_LOG_LL_ENABLED */ -_Static_assert(sizeof(ble_log_version_info_t) == 58, - "Unexpected BLE Log version info frame size"); - BLE_LOG_STATIC uint32_t ble_log_rt_lc_ts_get(void) { #if CONFIG_BLE_LOG_LL_ENABLED @@ -74,72 +74,33 @@ BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t rt_inited = 0; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR volatile uint32_t rt_ref_count = 0; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR QueueHandle_t rt_queue_handle = NULL; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR esp_timer_handle_t rt_defer_timer = NULL; -BLE_LOG_STATIC ble_log_version_info_t rt_version_info; -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t rt_ts_enabled = 0; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR esp_timer_handle_t rt_ts_timer = NULL; /* Toggle IO phase; stays false when the toggle IO is compiled out. */ BLE_LOG_STATIC BLE_LOG_DRAM_ATTR bool rt_ts_io_level = false; +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED +/* Runtime IO control is independent from the always-on periodic snapshot. */ +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR bool rt_ts_io_toggle_enabled = false; +#endif /* PRIVATE FUNCTION DECLARATION */ BLE_LOG_STATIC void ble_log_rt_defer_cb(void *arg); BLE_LOG_STATIC void ble_log_rt_dispatch(QueueHandle_t queue, UBaseType_t pending); -BLE_LOG_STATIC void ble_log_rt_version_info_init(void); -BLE_LOG_STATIC void ble_log_rt_ts_sample(ble_log_ts_info_t *info); BLE_LOG_STATIC void ble_log_rt_ts_trigger(void *arg); /* PRIVATE FUNCTION */ -/* Copies a NUL-terminated commit string into a fixed-width zero-padded field */ -BLE_LOG_STATIC void ble_log_commit_copy(uint8_t *dst, const char *src, size_t len) -{ - BLE_LOG_MEMCPY(dst, src, strnlen(src, len)); -} - -BLE_LOG_STATIC void ble_log_rt_version_info_init(void) -{ - BLE_LOG_MEMSET(&rt_version_info, 0, sizeof(rt_version_info)); - rt_version_info.int_src_code = BLE_LOG_INT_SRC_VERSION_INFO; - rt_version_info.version = BLE_LOG_VERSION; -#ifdef BLE_LOG_IDF_COMMIT - BLE_LOG_MEMCPY(rt_version_info.idf_commit, BLE_LOG_IDF_COMMIT, - BLE_LOG_IDF_COMMIT_LEN); -#endif -#if CONFIG_BT_CONTROLLER_ENABLED && defined(BLE_LOG_CONTROLLER_GET_COMMIT) - ble_log_commit_copy(rt_version_info.controller_commit, - BLE_LOG_CONTROLLER_GET_COMMIT(), BLE_LOG_LIB_COMMIT_LEN); -#endif -#if CONFIG_BT_CONTROLLER_ENABLED && defined(BLE_LOG_BTDM_COMMON_GET_COMMIT) - ble_log_commit_copy(rt_version_info.btdm_common_commit, - BLE_LOG_BTDM_COMMON_GET_COMMIT(), BLE_LOG_LIB_COMMIT_LEN); -#endif -#if CONFIG_BLE_MESH && CONFIG_BLE_MESH_V11_SUPPORT - const char *mesh_commit = strrchr(bt_mesh_v11_commit_str, ' '); - if (mesh_commit) { - ble_log_commit_copy(rt_version_info.mesh_commit, mesh_commit + 1, - BLE_LOG_LIB_COMMIT_LEN); - } -#endif -#if CONFIG_BT_AUDIO && CONFIG_SOC_BLE_AUDIO_SUPPORTED - ble_log_commit_copy(rt_version_info.audio_commit, lib_audio_commit_get(), - BLE_LOG_LIB_COMMIT_LEN); -#endif - esp_chip_info_t chip_info; - esp_chip_info(&chip_info); - rt_version_info.chip_model = (uint16_t)chip_info.model; - rt_version_info.chip_revision = chip_info.revision; - ble_log_internal_set_version_info(&rt_version_info); -} - /* Captures the link-layer, ESP and OS clocks at one instant. */ -BLE_LOG_STATIC void ble_log_rt_ts_sample(ble_log_ts_info_t *info) +void ble_log_rt_ts_sample(ble_log_ts_info_t *info, bool toggle_io) { info->int_src_code = BLE_LOG_INT_SRC_TS; BLE_LOG_ENTER_CRITICAL(); #if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED - /* The critical section keeps the toggle IO edge and the clock samples - * adjacent, and excludes the phase write in ble_log_sync_enable. */ - rt_ts_io_level = !rt_ts_io_level; - gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, rt_ts_io_level); + /* The critical section keeps the optional IO edge and clock samples + * adjacent, and serializes runtime IO control with the phase update. */ + if (toggle_io && rt_ts_io_toggle_enabled) { + rt_ts_io_level = !rt_ts_io_level; + gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, rt_ts_io_level); + } #endif /* CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED */ info->io_level = rt_ts_io_level; info->lc_ts = ble_log_rt_lc_ts_get(); @@ -165,6 +126,8 @@ BLE_LOG_STATIC void ble_log_rt_defer_cb(void *arg) { (void)arg; + /* Deinit clears the queue only after stop_blocking has waited for this + * callback, so a live gate implies a live queue. */ if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(rt_inited)) { return; } @@ -186,18 +149,17 @@ BLE_LOG_STATIC void ble_log_rt_defer_cb(void *arg) BLE_LOG_STATIC void ble_log_rt_ts_trigger(void *arg) { (void)arg; - if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(rt_inited) || - !BLE_LOG_ATOMIC_LOAD_ACQUIRE(rt_ts_enabled)) { + if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(rt_inited)) { return; } ble_log_ts_info_t ts_info; - ble_log_rt_ts_sample(&ts_info); + ble_log_rt_ts_sample(&ts_info, true); /* Unified periodic output: best-effort flush of partially-filled OPEN * transports ahead of the periodic snapshot, so parked frames do not * wait for the next capacity seal. */ - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); (void)ble_log_internal_snapshot( BLE_LOG_SNAPSHOT_REASON_PERIODIC | @@ -212,8 +174,6 @@ bool ble_log_rt_init(void) return true; } - ble_log_rt_version_info_init(); - /* Configure the analyzer toggle IO */ #if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED gpio_config_t sync_io_conf = { @@ -221,7 +181,10 @@ bool ble_log_rt_init(void) .mode = GPIO_MODE_OUTPUT, .pin_bit_mask = BIT64(CONFIG_BLE_LOG_SYNC_IO_NUM), }; - if (gpio_config(&sync_io_conf) != ESP_OK) { + /* Program the output latch before enabling the driver to avoid a stale + * high level or a high glitch after deinit/reinit. */ + if (gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, 0) != ESP_OK || + gpio_config(&sync_io_conf) != ESP_OK) { goto exit; } #endif /* CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED */ @@ -242,11 +205,13 @@ bool ble_log_rt_init(void) goto exit; } - /* TS sync is always on: the periodic tick drives the unified periodic - * output (TS sample, OPEN transport flush, snapshot). Test apps quiesce - * it with ble_log_sync_enable(false) for deterministic timing. */ + /* Create the system-periodic path here; the top-level init starts it only + * after queuing INIT, then it runs through deinit. Runtime sync control + * affects only the optional analyzer IO toggle. */ rt_ts_io_level = false; - BLE_LOG_ATOMIC_STORE_RELEASE(rt_ts_enabled, true); +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED + rt_ts_io_toggle_enabled = false; +#endif esp_timer_create_args_t ts_timer_args = { .callback = ble_log_rt_ts_trigger, .arg = NULL, @@ -255,8 +220,7 @@ bool ble_log_rt_init(void) /* Do not wake light sleep or replay every missed periodic callback. */ .skip_unhandled_events = true, }; - if (esp_timer_create(&ts_timer_args, &rt_ts_timer) != ESP_OK || - esp_timer_start_periodic(rt_ts_timer, BLE_LOG_TS_TRIGGER_TIMEOUT_US) != ESP_OK) { + if (esp_timer_create(&ts_timer_args, &rt_ts_timer) != ESP_OK) { goto exit; } @@ -268,6 +232,21 @@ exit: return false; } +bool ble_log_rt_start_periodic(void) +{ + bool started = false; + if (!ble_log_ref_count_try_acquire(&rt_ref_count, &rt_inited)) { + return false; + } + if (rt_ts_timer && + esp_timer_start_periodic(rt_ts_timer, + BLE_LOG_TS_TRIGGER_TIMEOUT_US) == ESP_OK) { + started = true; + } + BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); + return started; +} + void ble_log_rt_deinit(void) { /* Closing gate: seq_cst on both sides (see also submit/drain) so a @@ -278,7 +257,6 @@ void ble_log_rt_deinit(void) ESP_LOGE(TAG, "Timed out waiting for BLE Log runtime references"); BLE_LOG_ASSERT(false); } - BLE_LOG_ATOMIC_STORE_RELEASE(rt_ts_enabled, false); if (rt_ts_timer) { esp_timer_stop_blocking(rt_ts_timer, portMAX_DELAY); esp_timer_delete(rt_ts_timer); @@ -294,7 +272,6 @@ void ble_log_rt_deinit(void) if (rt_queue_handle) { ble_log_prph_trans_t *trans = NULL; while (xQueueReceive(rt_queue_handle, &trans, 0) == pdTRUE) { - trans->pos = 0; ble_log_lbm_recycle_trans(trans); } vQueueDelete(rt_queue_handle); @@ -304,6 +281,7 @@ void ble_log_rt_deinit(void) /* Release the toggle IO */ rt_ts_io_level = false; #if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED + rt_ts_io_toggle_enabled = false; gpio_reset_pin(CONFIG_BLE_LOG_SYNC_IO_NUM); #endif /* CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED */ } @@ -335,13 +313,13 @@ exit: BLE_LOG_IRAM_ATTR void ble_log_rt_submit_trans(ble_log_prph_trans_t *trans) { if (!ble_log_ref_count_try_acquire(&rt_ref_count, &rt_inited)) { + /* No reference was taken, so this path must not release; it stays + * separate from the held-reference failures below. */ ble_log_lbm_recycle_trans(trans); return; } if (!rt_queue_handle) { - BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); - ble_log_lbm_recycle_trans(trans); - return; + goto fail; } bool in_isr = BLE_LOG_IN_ISR(); @@ -349,35 +327,46 @@ BLE_LOG_IRAM_ATTR void ble_log_rt_submit_trans(ble_log_prph_trans_t *trans) ? xQueueSendFromISR(rt_queue_handle, &trans, NULL) : xQueueSend(rt_queue_handle, &trans, 0); if (queued != pdTRUE) { - BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); - ble_log_lbm_recycle_trans(trans); - return; + goto fail; } /* An active timer keeps the deadline anchored to the first submission. */ (void)esp_timer_start_once(rt_defer_timer, BLE_LOG_RT_DEFER_TIMEOUT_US); BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); + return; + +fail: + BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); + ble_log_lbm_recycle_trans(trans); } -bool ble_log_sync_enable(bool enable) +bool ble_log_ts_sync_io_toggle_enable(bool enable) { if (!ble_log_ref_count_try_acquire(&rt_ref_count, &rt_inited)) { return false; } - BLE_LOG_ATOMIC_STORE_RELEASE(rt_ts_enabled, enable); + #if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED - /* Leave the toggle IO at a defined low level: when sync is disabled - * while the IO idles low, drive a short high pulse first so the - * analyzer sees a final falling edge. The critical section excludes - * the phase toggle in ble_log_rt_ts_sample. */ + /* Restart the analyzer phase from low. When disabling an already-low IO, + * drive it high first so the external analyzer still sees a final + * falling edge. The periodic snapshot remains active in either state. */ BLE_LOG_ENTER_CRITICAL(); + rt_ts_io_toggle_enabled = enable; if (!enable && !rt_ts_io_level) { gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, 1); } rt_ts_io_level = false; gpio_set_level(CONFIG_BLE_LOG_SYNC_IO_NUM, 0); BLE_LOG_EXIT_CRITICAL(); +#else + (void)enable; #endif /* CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED */ + BLE_LOG_REF_COUNT_RELEASE(&rt_ref_count); return true; } + +bool ble_log_sync_enable(bool enable) +{ + return ble_log_ts_sync_io_toggle_enable(enable); +} diff --git a/components/bt/common/ble_log/src/ble_log_util.c b/components/bt/common/ble_log/src/ble_log_util.c index e8ea3bd39bc..17a52df6ca6 100644 --- a/components/bt/common/ble_log/src/ble_log_util.c +++ b/components/bt/common/ble_log/src/ble_log_util.c @@ -25,9 +25,8 @@ BLE_LOG_DRAM_ATTR portMUX_TYPE ble_log_spin_lock = portMUX_INITIALIZER_UNLOCKED; BLE_LOG_IRAM_ATTR BLE_LOG_STATIC BLE_LOG_INLINE uint32_t ror32(uint32_t word, uint32_t shift) { - if (unlikely(shift == 0)) { - return word; - } + /* The mask folds shift == 0 into a zero left-shift amount, so the + * expression needs no shift == 0 guard. */ return (word >> shift) | (word << ((32 - shift) & 0x1F)); } @@ -98,3 +97,13 @@ bool ble_log_ref_count_wait(volatile uint32_t *ref_count, uint32_t max_ref_count } return true; } + +BLE_LOG_IRAM_ATTR +void ble_log_atomic_update_peak(volatile uint32_t *peak, uint32_t value) +{ + uint32_t current = BLE_LOG_ATOMIC_LOAD_RELAXED(*peak); + while (value > current && + !__atomic_compare_exchange_n(peak, ¤t, value, true, + __ATOMIC_RELAXED, __ATOMIC_RELAXED)) { + } +} diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_lbm.h b/components/bt/common/ble_log/src/internal_include/ble_log_lbm.h deleted file mode 100644 index b9f2b9e32d9..00000000000 --- a/components/bt/common/ble_log/src/internal_include/ble_log_lbm.h +++ /dev/null @@ -1,272 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#ifndef __BLE_LOG_LBM_H__ -#define __BLE_LOG_LBM_H__ - -/* --------------------------------------- */ -/* BLE Log - Log Buffer Management */ -/* --------------------------------------- */ - -/* ---------------- */ -/* Includes */ -/* ---------------- */ -#include "ble_log.h" -#include "ble_log_prph.h" - -#include "freertos/FreeRTOS.h" - -#if defined(CONFIG_BLE_LOG_PRPH_UART_DMA) && (CONFIG_BLE_LOG_PRPH_UART_DMA_PORT == 0) -#define BLE_LOG_UART_REDIR_ENABLED (1) -#else -#define BLE_LOG_UART_REDIR_ENABLED (0) -#endif -#include "freertos/semphr.h" - -/* ------------------------- */ -/* Log Frame Defines */ -/* ------------------------- */ -typedef struct { - uint16_t length; - uint32_t frame_meta; -} __attribute__((packed)) ble_log_frame_head_t; - -#define BLE_LOG_FRAME_HEAD_LEN (sizeof(ble_log_frame_head_t)) -#define BLE_LOG_FRAME_TAIL_LEN (sizeof(uint32_t)) -#define BLE_LOG_FRAME_OVERHEAD (BLE_LOG_FRAME_HEAD_LEN + BLE_LOG_FRAME_TAIL_LEN) -#define BLE_LOG_MAKE_FRAME_META(src_code, sn) (((src_code) & 0xFF) | ((sn) << 8)) - -/* ---------------------------------- */ -/* Log Buffer Manager Defines */ -/* ---------------------------------- */ -typedef enum { - BLE_LOG_LBM_LOCK_NONE, - BLE_LOG_LBM_LOCK_SPIN, - BLE_LOG_LBM_LOCK_ATOMIC, - BLE_LOG_LBM_LOCK_MUTEX, -} ble_log_lbm_lock_t; - -typedef struct { - int trans_idx; - ble_log_prph_trans_t *trans[BLE_LOG_TRANS_BUF_CNT]; - ble_log_lbm_lock_t lock_type; - union { - /* BLE_LOG_LBM_LOCK_NONE */ - void *none; - /* BLE_LOG_LBM_LOCK_SPIN */ - portMUX_TYPE spin_lock; - /* BLE_LOG_LBM_LOCK_ATOMIC */ - volatile bool atomic_lock; - /* BLE_LOG_LBM_LOCK_MUTEX */ - SemaphoreHandle_t mutex; - }; - uint32_t trans_inflight; - uint32_t trans_inflight_peak; -} ble_log_lbm_t; - -/* --------------------------------------- */ -/* Log Buffer Manager Pool Defines */ -/* --------------------------------------- */ -enum { -#if CONFIG_BLE_LOG_LL_ENABLED - BLE_LOG_LBM_LL_TASK, - BLE_LOG_LBM_LL_HCI, -#endif /* CONFIG_BLE_LOG_LL_ENABLED */ - BLE_LOG_LBM_LL_MAX, -}; - -enum { - BLE_LOG_LBM_SPIN_TASK = 0, - BLE_LOG_LBM_SPIN_ISR, - BLE_LOG_LBM_SPIN_MAX, -}; - -#define BLE_LOG_LBM_ATOMIC_TASK_CNT CONFIG_BLE_LOG_LBM_ATOMIC_LOCK_TASK_CNT -#define BLE_LOG_LBM_ATOMIC_ISR_CNT CONFIG_BLE_LOG_LBM_ATOMIC_LOCK_ISR_CNT -#define BLE_LOG_LBM_ATOMIC_CNT (BLE_LOG_LBM_ATOMIC_TASK_CNT +\ - BLE_LOG_LBM_ATOMIC_ISR_CNT) -#define BLE_LOG_LBM_COMMON_CNT (BLE_LOG_LBM_ATOMIC_CNT + BLE_LOG_LBM_SPIN_MAX) -#define BLE_LOG_LBM_CNT (BLE_LOG_LBM_COMMON_CNT + BLE_LOG_LBM_LL_MAX) - -/* Derived per-buffer size from user-configured total-per-LBM budget */ -#define BLE_LOG_TRANS_SIZE (CONFIG_BLE_LOG_LBM_TRANS_BUF_SIZE / BLE_LOG_TRANS_BUF_CNT) -#define BLE_LOG_TRANS_LL_SIZE (CONFIG_BLE_LOG_LBM_LL_TRANS_BUF_SIZE / BLE_LOG_TRANS_BUF_CNT) - -/* Unified queue depth derivation */ -#define BLE_LOG_TRANS_POOL_CNT (BLE_LOG_LBM_CNT * BLE_LOG_TRANS_BUF_CNT) -#if BLE_LOG_UART_REDIR_ENABLED -#define BLE_LOG_TRANS_REDIR_CNT BLE_LOG_TRANS_BUF_CNT -#else -#define BLE_LOG_TRANS_REDIR_CNT (0) -#endif -#define BLE_LOG_TRANS_TOTAL_CNT (BLE_LOG_TRANS_POOL_CNT + BLE_LOG_TRANS_REDIR_CNT) - -/* ------------------------------------------ */ -/* Log Buffer Manager Context Defines */ -/* ------------------------------------------ */ -typedef struct { - union { - ble_log_lbm_t lbm_pool[BLE_LOG_LBM_CNT]; - struct { - union { - ble_log_lbm_t lbm_common_pool[BLE_LOG_LBM_COMMON_CNT]; - struct { - union { - ble_log_lbm_t spin_pool[BLE_LOG_LBM_SPIN_MAX]; - struct { - ble_log_lbm_t spin_task; - ble_log_lbm_t spin_isr; - }; - }; - union { - ble_log_lbm_t atomic_pool[BLE_LOG_LBM_ATOMIC_CNT]; - struct { - ble_log_lbm_t atomic_pool_task[BLE_LOG_LBM_ATOMIC_TASK_CNT]; - ble_log_lbm_t atomic_pool_isr[BLE_LOG_LBM_ATOMIC_ISR_CNT]; - }; - }; - }; - }; - union { - ble_log_lbm_t lbm_ll_pool[BLE_LOG_LBM_LL_MAX]; -#if CONFIG_BLE_LOG_LL_ENABLED - struct { - ble_log_lbm_t lbm_ll_task; - ble_log_lbm_t lbm_ll_hci; - }; -#endif /* CONFIG_BLE_LOG_LL_ENABLED */ - }; - }; - }; -} ble_log_lbm_ctx_t; - -/* -------------------------------------------- */ -/* Buffer Utilization Reporting Defines */ -/* -------------------------------------------- */ -typedef enum { - BLE_LOG_BUF_UTIL_POOL_COMMON_TASK = 0, - BLE_LOG_BUF_UTIL_POOL_COMMON_ISR = 1, - BLE_LOG_BUF_UTIL_POOL_LL = 2, - BLE_LOG_BUF_UTIL_POOL_REDIR = 3, -} ble_log_buf_util_pool_t; - -typedef struct { - uint8_t int_src_code; - uint8_t lbm_id; - uint8_t trans_cnt; - uint8_t inflight_peak; -} __attribute__((packed)) ble_log_buf_util_t; - -#define BLE_LOG_BUF_UTIL_MAKE_ID(pool, idx) (((pool) << 4) | ((idx) & 0x0F)) -#define BLE_LOG_BUF_UTIL_GET_POOL(id) (((id) >> 4) & 0x0F) -#define BLE_LOG_BUF_UTIL_GET_INDEX(id) ((id) & 0x0F) - -/* ---------------------------------------- */ -/* Enhanced Statistics Data Defines */ -/* ---------------------------------------- */ -typedef struct { - uint8_t int_src_code; - uint8_t src_code; - uint32_t written_frame_cnt; - uint32_t lost_frame_cnt; - uint32_t written_bytes_cnt; - uint32_t lost_bytes_cnt; -} __attribute__((packed)) ble_log_enh_stat_t; - -/* -------------------------------- */ -/* Final Statistics Defines */ -/* -------------------------------- */ -typedef struct { - uint8_t src_code; - uint32_t written_frame_cnt; - uint32_t lost_frame_cnt; - uint32_t written_bytes_cnt; - uint32_t lost_bytes_cnt; -} __attribute__((packed)) ble_log_final_stat_entry_t; - -typedef struct { - uint8_t int_src_code; - uint8_t src_cnt; - ble_log_final_stat_entry_t entries[BLE_LOG_SRC_MAX]; -} __attribute__((packed)) ble_log_final_stat_t; - -#define BLE_LOG_FINAL_STAT_LEN sizeof(ble_log_final_stat_t) - -/* -------------------------------------- */ -/* Log Statistics Manager Context */ -/* -------------------------------------- */ -typedef struct { - uint32_t frame_sn; - /* Aligned live counters — updated by ble_log_stat_mgr_update(), - * snapshot by ble_log_write_enh_stat(). Natural 4-byte alignment - * ensures each load/store compiles to a single l32i/s32i on - * Xtensa/RISC-V, so individual field access is atomic without locks. - * The packed ble_log_enh_stat_t wire format is built on the stack - * only when serializing to UART. */ - uint32_t written_frame_cnt; - uint32_t lost_frame_cnt; - uint32_t written_bytes_cnt; - uint32_t lost_bytes_cnt; -} ble_log_stat_mgr_t; - -#define BLE_LOG_GET_FRAME_SN(VAR) __atomic_fetch_add(VAR, 1, __ATOMIC_RELAXED) - -/* -------------------------- */ -/* Link Layer Defines */ -/* -------------------------- */ -#if CONFIG_BLE_LOG_LL_ENABLED -enum { - BLE_LOG_LL_FLAG_CONTINUE = 0, - BLE_LOG_LL_FLAG_END, - BLE_LOG_LL_FLAG_TASK, - BLE_LOG_LL_FLAG_ISR, - BLE_LOG_LL_FLAG_HCI, - BLE_LOG_LL_FLAG_RAW, - BLE_LOG_LL_FLAG_OMDATA, - BLE_LOG_LL_FLAG_HCI_UPSTREAM, -}; -#endif /* CONFIG_BLE_LOG_LL_ENABLED */ - -/* ------------------------------- */ -/* Compile-Time Guards */ -/* ------------------------------- */ -_Static_assert(CONFIG_BLE_LOG_LBM_TRANS_BUF_SIZE % BLE_LOG_TRANS_BUF_CNT == 0, - "Common LBM total buffer size must be a multiple of BLE_LOG_TRANS_BUF_CNT (4)"); -#if CONFIG_BLE_LOG_LL_ENABLED -_Static_assert(CONFIG_BLE_LOG_LBM_LL_TRANS_BUF_SIZE % BLE_LOG_TRANS_BUF_CNT == 0, - "LL LBM total buffer size must be a multiple of BLE_LOG_TRANS_BUF_CNT (4)"); -#endif -_Static_assert(CONFIG_BLE_LOG_LBM_TRANS_BUF_SIZE / BLE_LOG_TRANS_BUF_CNT >= BLE_LOG_FRAME_OVERHEAD, - "Common LBM per-buffer size too small for a single frame"); -_Static_assert(BLE_LOG_TRANS_SIZE >= BLE_LOG_FINAL_STAT_LEN + sizeof(uint32_t) + BLE_LOG_FRAME_OVERHEAD, - "Common LBM per-buffer size too small for final statistics frame"); -_Static_assert((BLE_LOG_TRANS_BUF_CNT & (BLE_LOG_TRANS_BUF_CNT - 1)) == 0, - "BLE_LOG_TRANS_BUF_CNT must be a power of 2"); -_Static_assert(1 + BLE_LOG_LBM_ATOMIC_TASK_CNT <= 16, - "Common task pool exceeds lbm_id 4-bit index limit (max 15)"); -_Static_assert(1 + BLE_LOG_LBM_ATOMIC_ISR_CNT <= 16, - "Common ISR pool exceeds lbm_id 4-bit index limit (max 15)"); -_Static_assert(BLE_LOG_TRANS_BUF_CNT <= 255, - "BLE_LOG_TRANS_BUF_CNT must fit in uint8_t for ble_log_buf_util_t"); - -/* --------------------------- */ -/* Internal Interfaces */ -/* --------------------------- */ -bool ble_log_lbm_init(void); -void ble_log_lbm_close(void); -void ble_log_lbm_deinit(void); -void ble_log_lbm_recycle_trans(ble_log_prph_trans_t *trans); -bool ble_log_write_internal(const uint8_t *addr, size_t len); -void ble_log_write_final_stat(void); -void ble_log_write_enh_stat(void); -void ble_log_write_buf_util(void); -#if BLE_LOG_UART_REDIR_ENABLED -void ble_log_lbm_stream_write(ble_log_lbm_t *lbm, ble_log_src_t src_code, - const uint8_t *data, size_t len); -void ble_log_lbm_stream_flush(ble_log_lbm_t *lbm, ble_log_src_t src_code); -ble_log_lbm_t *ble_log_prph_get_redir_lbm(void); -#endif - -#endif /* __BLE_LOG_LBM_H__ */ diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h index f179d35ca37..f78f7e4c896 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h @@ -59,10 +59,9 @@ typedef struct { /* --------------------------------------- */ /* Protocol v7 Source ID Space */ /* --------------------------------------- */ -/* The frozen public ble_log_src_t values are the on-wire and statistic - * source IDs: the frame source byte carries the enum value directly and - * every source keeps its own wire sequence, matching the legacy wire - * identity so existing decoders keep working. */ +/* The frozen public ble_log_src_t values are the base on-wire and statistic + * source IDs. Bit 7 of the frame source byte carries NON_YIELD metadata, so + * receivers must mask it before decoding the base source. */ #define BLE_LOG_SRC_ID_MASK 0x7f #define BLE_LOG_SRC_FLAG_NON_YIELD 0x80 #define BLE_LOG_SRC_ID(source_meta) ((source_meta) & BLE_LOG_SRC_ID_MASK) @@ -115,9 +114,11 @@ typedef struct { * sources — and every lost or rejected attempt leaves a gap. INTERNAL * snapshot frames and the REDIR console stream keep their own separate * sequences (a gap counts a skipped snapshot or a dropped console batch). - * None of the sequences is ever reset. The per-counter macros live next - * to their counters in the owning translation units; the 24-bit wire - * field is enforced where the frame meta is packed. */ + * ble_log_init() resets all three sequences; its required INIT snapshot + * starts a new receiver epoch. They stay continuous through FLUSH within + * that epoch. The per-counter macros live next to their counters in the + * owning translation units; the 24-bit wire field is enforced where the + * frame meta is packed. */ /* -------------------------------- */ /* Internal Snapshot Frame */ @@ -226,18 +227,20 @@ bool ble_log_lbm_init(void); void ble_log_lbm_begin_deinit(void); void ble_log_lbm_deinit(void); bool ble_log_lbm_is_enabled(void); -void ble_log_lbm_flush_open_transports(void); +/* Best-effort system flush, gated by LBM lifetime rather than producers. */ +void ble_log_lbm_flush_open_trans(void); /* Deinit drain: seal every OPEN transport and dispatch it to the runtime * queue. Contract: called after ble_log_lbm_begin_deinit() (producer gate * closed, writers drained) and before ble_log_rt_deinit(); every transport * lock is then uncontended. The peripheral deinit wait completes the * delivery of the dispatched buffers. */ -void ble_log_lbm_drain_open_transports(void); +void ble_log_lbm_drain_open_trans(void); void ble_log_lbm_recycle_trans(ble_log_prph_trans_t *trans); -void ble_log_internal_set_version_info(const ble_log_version_info_t *version_info); +/* System output: gated by the LBM lifetime, not ble_log_enable(). A false + * wait_for_transport makes a busy dedicated transport a lossy fast path. */ bool ble_log_internal_snapshot(uint16_t reason_flags, const ble_log_ts_info_t *ts_info, - bool required); + bool wait_for_transport); /* Claim/commit: the public ble_log_src_t is accepted for API stability, but * only BLE_LOG_SRC_ENCODE is supported; its frames are stamped with the diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_prph.h b/components/bt/common/ble_log/src/internal_include/ble_log_prph.h index 8d881ccd9d9..bc2ab9b0fd5 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_prph.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_prph.h @@ -61,6 +61,9 @@ typedef struct { bool ble_log_prph_init(size_t trans_cnt); void ble_log_prph_deinit(void); +/* Allocates a transport whose storage is zeroed (every field except size + * starts at its zero value: state FREE, owner POOL, lock and pending_seal + * clear). Callers only need to set the non-zero identity fields. */ bool ble_log_prph_trans_init(ble_log_prph_trans_t **trans, size_t trans_size); void ble_log_prph_trans_deinit(ble_log_prph_trans_t **trans); void ble_log_prph_send_trans(ble_log_prph_trans_t *trans); diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_rt.h b/components/bt/common/ble_log/src/internal_include/ble_log_rt.h index 89f51c7421b..86347fd05b4 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_rt.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_rt.h @@ -12,6 +12,7 @@ /* INCLUDE */ #include "ble_log_prph.h" +#include "ble_log_lbm_v2.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -23,8 +24,16 @@ /* INTERFACE */ bool ble_log_rt_init(void); +/* Starts the always-on periodic path after the epoch INIT frame is queued. */ +bool ble_log_rt_start_periodic(void); void ble_log_rt_deinit(void); bool ble_log_rt_drain(void); void ble_log_rt_submit_trans(ble_log_prph_trans_t *trans); +/* Samples the link-layer, ESP and OS clocks at one instant. toggle_io allows + * the periodic path to toggle the sync IO when runtime IO toggling is enabled; + * one-shot snapshots (INIT/FLUSH) always pass false and leave the IO edge + * sequence untouched. io_level always reports the physical IO level. */ +void ble_log_rt_ts_sample(ble_log_ts_info_t *info, bool toggle_io); + #endif /* __BLE_LOG_RT_H__ */ diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_util.h b/components/bt/common/ble_log/src/internal_include/ble_log_util.h index e12787523b5..bc631d48a8a 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_util.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_util.h @@ -131,4 +131,9 @@ bool ble_log_ref_count_try_acquire(volatile uint32_t *ref_count, /* Task-context wait; returns false if the count stays above max for one second. */ bool ble_log_ref_count_wait(volatile uint32_t *ref_count, uint32_t max_ref_count); +/* Monotonic-peak publish: stores value into *peak only when it exceeds the + * current peak. Relaxed atomics are enough - a lost race can only leave the + * recorded peak below a transient maximum, and peaks are diagnostics. */ +void ble_log_atomic_update_peak(volatile uint32_t *peak, uint32_t value); + #endif /* __BLE_LOG_UTIL_H__ */ diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_dummy.c b/components/bt/common/ble_log/src/prph/ble_log_prph_dummy.c index 38d366d064c..0042581cc49 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_dummy.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_dummy.c @@ -87,6 +87,5 @@ void ble_log_prph_trans_deinit(ble_log_prph_trans_t **trans) * SPI DMA) do the same work inside their asynchronous tx_done callbacks. */ void ble_log_prph_send_trans(ble_log_prph_trans_t *trans) { - trans->pos = 0; ble_log_lbm_recycle_trans(trans); } diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_dma.c b/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_dma.c index 0b2c99043f0..eab253a8b2f 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_dma.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_dma.c @@ -42,7 +42,6 @@ BLE_LOG_SPI_MASTER_DMA_CB_ATTR BLE_LOG_STATIC void spi_master_dma_tx_done_cb(spi /* Recycle transport */ ble_log_prph_trans_t *trans = (ble_log_prph_trans_t *)(spi_trans->user); - trans->pos = 0; ble_log_lbm_recycle_trans(trans); } diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_hd.c b/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_hd.c index 9b202e56ed0..662be1ec579 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_hd.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_spi_master_hd.c @@ -44,7 +44,6 @@ BLE_LOG_SPI_MASTER_HD_CB_ATTR BLE_LOG_STATIC void spi_master_hd_tx_done_cb(spi_t } } - ctx->trans->pos = 0; ble_log_lbm_recycle_trans(ctx->trans); } @@ -220,7 +219,6 @@ BLE_LOG_IRAM_ATTR void ble_log_prph_send_trans(ble_log_prph_trans_t *trans) uint8_t old_status = __atomic_fetch_or(&ctx->status, BLE_LOG_SPI_HD_END_QUEUE_FAILED, __ATOMIC_ACQ_REL); if (old_status & BLE_LOG_SPI_HD_DATA_DONE) { /* Data is already on the wire; recycle it without re-sending. */ - trans->pos = 0; ble_log_lbm_recycle_trans(trans); } } diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_test.c b/components/bt/common/ble_log/src/prph/ble_log_prph_test.c index 04f649419c8..9dc034c8bbd 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_test.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_test.c @@ -155,7 +155,6 @@ void ble_log_prph_send_trans(ble_log_prph_trans_t *trans) uint32_t idx = __atomic_load_n(&s_auto_recycle_idx, __ATOMIC_ACQUIRE); ble_log_prph_test_hook_reg_t reg = s_auto_recycle_slots[idx & 1]; if (reg.hook) { - trans->pos = 0; ble_log_lbm_recycle_trans(trans); reg.hook(reg.ctx); __atomic_fetch_sub(&s_auto_recycle_busy, 1, __ATOMIC_RELEASE); @@ -164,7 +163,6 @@ void ble_log_prph_send_trans(ble_log_prph_trans_t *trans) __atomic_fetch_sub(&s_auto_recycle_busy, 1, __ATOMIC_RELEASE); if (xQueueSend(s_pending_trans, &trans, 0) != pdTRUE) { - trans->pos = 0; ble_log_lbm_recycle_trans(trans); BLE_LOG_ASSERT(false); } @@ -217,7 +215,6 @@ size_t ble_log_prph_test_read(uint8_t *data, size_t len, TickType_t timeout, if (delay_us > 0 && (esp_timer_start_once(s_tx_timer, (uint64_t)delay_us) != ESP_OK || xSemaphoreTake(s_tx_done, portMAX_DELAY) != pdTRUE)) { - trans->pos = 0; ble_log_lbm_recycle_trans(trans); BLE_LOG_ASSERT(false); return 0; @@ -233,7 +230,6 @@ size_t ble_log_prph_test_read(uint8_t *data, size_t len, TickType_t timeout, } size_t copied = len < trans->pos ? len : trans->pos; BLE_LOG_MEMCPY(data, trans->buf, copied); - trans->pos = 0; ble_log_lbm_recycle_trans(trans); if (uxQueueMessagesWaiting(s_pending_trans) == 0) { s_tx_deadline_us = 0; diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c b/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c index ce0333e577b..7736178d04e 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c @@ -143,7 +143,6 @@ bool ble_log_prph_init(size_t trans_cnt) /* Redirection transports are not part of the global pool. */ redir_lbm->trans[i]->id = BLE_LOG_TRANS_ID_NONE; redir_lbm->trans[i]->owner_kind = BLE_LOG_TRANS_OWNER_REDIR; - redir_lbm->trans[i]->state = BLE_LOG_TRANS_STATE_FREE; } /* Mutex initialization */ @@ -306,7 +305,11 @@ void ble_log_prph_trans_deinit(ble_log_prph_trans_t **trans) BLE_LOG_IRAM_ATTR void ble_log_prph_send_trans(ble_log_prph_trans_t *trans) { if (uhci_transmit(dev_handle, trans->buf, trans->pos) != ESP_OK) { - /* No tx_done will fire on failure: recycle here to avoid leaking. */ + /* The UHCI queue depth matches the transport count and each transport + * occupies at most one slot, so a full queue cannot occur here: this + * is a driver fault. The assert compiles out with NDEBUG; the recycle + * below still covers that case (no tx_done will fire on failure). */ + BLE_LOG_ASSERT(false); ble_log_lbm_recycle_trans(trans); } } diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c index 903cede8507..9db296dd963 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c @@ -53,8 +53,8 @@ bool test_ble_log_walk_frames(const uint8_t *data, size_t len, void setUp(void) { - /* The periodic TS tick is always on since TS sync became unconditional; - * quiesce it so timing-sensitive tests stay deterministic. */ + /* Preserve the external test-system contract: every test starts with the + * optional sync IO disabled and low. Periodic snapshots remain active. */ (void)ble_log_sync_enable(false); } diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c index 2bd0e916224..d1067cf1e9e 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c @@ -51,6 +51,13 @@ typedef struct { uint32_t ts_count; } rt_marker_observer_t; +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED +typedef struct { + bool found; + uint8_t io_level; +} rt_snapshot_observer_t; +#endif + typedef struct { SemaphoreHandle_t done; uint32_t remaining; @@ -122,6 +129,29 @@ static void observe_runtime_marker(const test_ble_log_frame_t *frame, void *ctx) } } +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED +static void observe_periodic_snapshot(const test_ble_log_frame_t *frame, + void *ctx) +{ + rt_snapshot_observer_t *observer = ctx; + if (observer->found || frame->src != BLE_LOG_SRC_INTERNAL || + frame->payload_len != sizeof(uint32_t) + + sizeof(ble_log_internal_snapshot_t)) { + return; + } + + ble_log_internal_snapshot_t snapshot; + memcpy(&snapshot, frame->payload + sizeof(uint32_t), sizeof(snapshot)); + uint16_t expected = BLE_LOG_SNAPSHOT_REASON_PERIODIC | + BLE_LOG_SNAPSHOT_REASON_TS_VALID; + if (snapshot.int_src_code == BLE_LOG_INT_SRC_SNAPSHOT && + (snapshot.reason_flags & expected) == expected) { + observer->found = true; + observer->io_level = snapshot.ts.io_level; + } +} +#endif + static TickType_t runtime_timeout_ticks(uint32_t timeout_ms) { uint64_t ticks = ((uint64_t)timeout_ms * configTICK_RATE_HZ + 999) / 1000; @@ -159,11 +189,12 @@ static bool write_runtime_marker(uint32_t seq, int64_t *enqueued_at_us) return true; } -static bool read_runtime_marker(uint32_t *seq, uint32_t *ts_count, - int64_t *received_at_us) +static bool read_runtime_marker_with_timeout(uint32_t *seq, uint32_t *ts_count, + int64_t *received_at_us, + uint32_t timeout_ms) { const int64_t deadline_us = esp_timer_get_time() + - (int64_t)RT_READ_TIMEOUT_MS * 1000; + (int64_t)timeout_ms * 1000; uint32_t observed_ts = 0; while (true) { @@ -197,6 +228,13 @@ static bool read_runtime_marker(uint32_t *seq, uint32_t *ts_count, } } +static bool read_runtime_marker(uint32_t *seq, uint32_t *ts_count, + int64_t *received_at_us) +{ + return read_runtime_marker_with_timeout(seq, ts_count, received_at_us, + RT_READ_TIMEOUT_MS); +} + static bool runtime_stream_is_quiet(void) { const int64_t drain_deadline_us = esp_timer_get_time() + @@ -229,6 +267,35 @@ static bool runtime_stream_is_quiet(void) return false; } +#if CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED +static bool read_periodic_snapshot(uint8_t *io_level) +{ + const int64_t deadline_us = esp_timer_get_time() + + (int64_t)BLE_LOG_TS_TRIGGER_TIMEOUT_US + 1000000; + while (true) { + TickType_t remaining; + if (!runtime_deadline_ticks(deadline_us, &remaining)) { + return false; + } + size_t len = ble_log_prph_test_read(s_capture, sizeof(s_capture), + remaining, 0, NULL); + if (!len) { + continue; + } + + rt_snapshot_observer_t observer = {0}; + if (!test_ble_log_walk_frames(s_capture, len, + observe_periodic_snapshot, &observer)) { + return false; + } + if (observer.found) { + *io_level = observer.io_level; + return true; + } + } +} +#endif + static void refill_runtime_queue(void *arg) { rt_starvation_ctx_t *ctx = arg; @@ -480,7 +547,7 @@ TEST_CASE("BLE Log periodic timestamp skips light sleep wakeups", TEST_ASSERT_TRUE(runtime_stream_is_quiet()); TEST_ASSERT_TRUE(ble_log_sync_enable(true)); for (int i = 0; i < 3; i++) { - vTaskDelay(runtime_timeout_ticks(CONFIG_BLE_LOG_TS_TRIGGER_TIMEOUT_MS)); + vTaskDelay(runtime_timeout_ticks(BLE_LOG_TS_TRIGGER_TIMEOUT_MS)); } TEST_ASSERT_TRUE(ble_log_sync_enable(false)); @@ -496,6 +563,65 @@ TEST_CASE("BLE Log periodic timestamp skips light sleep wakeups", "Periodic ESP timer did not emit a timestamp frame"); } +TEST_CASE("BLE Log sync IO control leaves periodic snapshots running", + "[ble_log][runtime][timestamp][ignore]") +{ +#if !CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED + TEST_IGNORE_MESSAGE("Requires BLE Log TS sync IO toggle support"); +#else + const uint32_t seq = UINT32_C(0x41000); + uint32_t received_seq; + uint8_t io_level; + TEST_ASSERT_TRUE(ble_log_enable(true)); + + TEST_ASSERT_TRUE(ble_log_ts_sync_io_toggle_enable(false)); + TEST_ASSERT_TRUE(runtime_stream_is_quiet()); + TEST_ASSERT_TRUE_MESSAGE(read_periodic_snapshot(&io_level), + "No periodic clock snapshot while sync IO was disabled"); + TEST_ASSERT_EQUAL_UINT8(0, io_level); + TEST_ASSERT_TRUE(runtime_stream_is_quiet()); + + /* Establish the timer phase above, then park a partial OPEN transport and + * close the public producer gate. The next periodic callback must still + * flush the transport and emit its Internal Snapshot. */ + prepare_payload(seq); + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, s_payload, + sizeof(rt_marker_t))); + TEST_ASSERT_TRUE(ble_log_enable(false)); + bool marker_received = read_runtime_marker_with_timeout( + &received_seq, NULL, NULL, + BLE_LOG_TS_TRIGGER_TIMEOUT_MS + 1000); + bool snapshot_received = read_periodic_snapshot(&io_level); + TEST_ASSERT_TRUE(ble_log_enable(true)); + + TEST_ASSERT_TRUE_MESSAGE( + marker_received, + "Producer disable stopped the periodic OPEN transport flush"); + TEST_ASSERT_EQUAL_UINT32(seq, received_seq); + TEST_ASSERT_TRUE_MESSAGE( + snapshot_received, + "Producer disable stopped periodic Internal Snapshots"); + TEST_ASSERT_EQUAL_UINT8(0, io_level); + + TEST_ASSERT_TRUE(runtime_stream_is_quiet()); + TEST_ASSERT_TRUE(ble_log_ts_sync_io_toggle_enable(true)); + bool high_seen = false; + for (int i = 0; i < 3 && !high_seen; i++) { + TEST_ASSERT_TRUE_MESSAGE(read_periodic_snapshot(&io_level), + "No periodic snapshot while sync IO was enabled"); + high_seen = io_level != 0; + } + TEST_ASSERT_TRUE_MESSAGE(high_seen, "Enabled sync IO never toggled high"); + + /* Exercise the compatibility shim on disable. */ + TEST_ASSERT_TRUE(ble_log_sync_enable(false)); + TEST_ASSERT_TRUE(runtime_stream_is_quiet()); + TEST_ASSERT_TRUE_MESSAGE(read_periodic_snapshot(&io_level), + "Sync IO disable stopped periodic snapshots"); + TEST_ASSERT_EQUAL_UINT8(0, io_level); +#endif +} + TEST_CASE("BLE Log runtime dispatch yields to other timer callbacks", "[ble_log][runtime][ignore]") { @@ -737,8 +863,10 @@ TEST_CASE("BLE Log LBM inflight peak stays bounded under bursts", } /* Snapshot the retained peak through the dedicated Internal transport. */ + ble_log_ts_info_t ts_info; + ble_log_rt_ts_sample(&ts_info, false); TEST_ASSERT_TRUE(ble_log_internal_snapshot( - BLE_LOG_SNAPSHOT_REASON_PERIODIC, NULL, true)); + BLE_LOG_SNAPSHOT_REASON_PERIODIC, &ts_info, true)); TEST_ASSERT_TRUE(ble_log_rt_drain()); const int64_t deadline_us = esp_timer_get_time() + diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c index 20558a09e04..4048079c8cf 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c @@ -52,8 +52,8 @@ bool test_ble_log_walk_frames(const uint8_t *data, size_t len, void setUp(void) { - /* The periodic TS tick is always on since TS sync became unconditional; - * quiesce it so timing-sensitive tests stay deterministic. */ + /* Preserve the external test-system contract: every test starts with the + * optional sync IO disabled and low. Periodic snapshots remain active. */ (void)ble_log_sync_enable(false); } diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c index 39b02135d1b..655b1cccdb5 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c @@ -28,9 +28,8 @@ #error "BLE Log test app requires CONFIG_BLE_LOG_PRPH_TEST" #endif -/* The runtime dispatch hook is throttled to one pass per - * BLE_LOG_TS_TRIGGER_TIMEOUT_MS; let the window elapse between write bursts - * so a hook pass is guaranteed to run after the settle delay. */ +/* Internal Snapshots use the production periodic cadence; let one full + * window elapse when a test needs to observe the next periodic sample. */ #define TEST_HOOK_SETTLE_MS (BLE_LOG_TS_TRIGGER_TIMEOUT_MS + 100) #define TEST_READ_TIMEOUT_MS (50) #define TEST_MAX_ROUNDS (3) @@ -56,6 +55,7 @@ typedef struct { static uint8_t s_read_buf[TEST_READ_BUF_SIZE]; static bool s_claim_hook_armed; +static bool s_init_snapshot_hook_armed; static uint32_t s_stale_claim_handle; static volatile bool s_locked_hook_armed; static volatile bool s_enable_hook_armed; @@ -72,6 +72,7 @@ static SemaphoreHandle_t s_compression_hook_continue; void ble_log_test_claim_pre_publish_hook(void); void ble_log_test_claim_locked_hook(void); +void ble_log_test_init_snapshot_before_acquire_hook(void); void ble_log_test_enable_before_lifecycle_lock_hook(void); void ble_log_test_disable_before_wake_hook(void); #if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE @@ -97,7 +98,14 @@ void ble_log_test_claim_locked_hook(void) /* Runs while the claiming writer itself holds the OPEN transport * lock: the flush must skip the busy transport and leave the * pending-seal marker for the next claim. */ - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); + } +} + +void ble_log_test_init_snapshot_before_acquire_hook(void) +{ + if (s_init_snapshot_hook_armed) { + vTaskDelay(pdMS_TO_TICKS(TEST_HOOK_SETTLE_MS)); } } @@ -178,6 +186,28 @@ static void capture_version_info_frame(const test_ble_log_frame_t *frame, void * } } +typedef struct { + bool found; + uint16_t reason_flags; +} first_snapshot_capture_t; + +static void capture_first_snapshot(const test_ble_log_frame_t *frame, void *ctx) +{ + first_snapshot_capture_t *capture = ctx; + if (capture->found || frame->src != BLE_LOG_SRC_INTERNAL || + frame->payload_len != sizeof(uint32_t) + + sizeof(ble_log_internal_snapshot_t)) { + return; + } + + ble_log_internal_snapshot_t snapshot; + memcpy(&snapshot, frame->payload + sizeof(uint32_t), sizeof(snapshot)); + if (snapshot.int_src_code == BLE_LOG_INT_SRC_SNAPSHOT) { + capture->found = true; + capture->reason_flags = snapshot.reason_flags; + } +} + /* Consumes pending test transports concurrently with the writer: transports * are recycled only once read, and the LBM has a small pool of them. */ static void test_reader_task(void *arg) @@ -230,7 +260,7 @@ TEST_CASE("BLE Log v7 framing matches golden bytes", "[ble_log][wire]") TEST_ASSERT_EQUAL_size_t(8, sizeof(ble_log_source_stat_t)); TEST_ASSERT_EQUAL_size_t(134, sizeof(ble_log_internal_snapshot_t)); TEST_ASSERT_EQUAL_size_t( - 4, offsetof(ble_log_internal_snapshot_t, version_info)); + 3, offsetof(ble_log_internal_snapshot_t, version_info)); TEST_ASSERT_EQUAL_size_t( 62, offsetof(ble_log_internal_snapshot_t, ts.lc_ts)); TEST_ASSERT_EQUAL_size_t( @@ -335,6 +365,41 @@ TEST_CASE("BLE Log periodic tick reports build and chip versions", "[ble_log]") vSemaphoreDelete(ctx.done); } +TEST_CASE("BLE Log INIT snapshot starts each receiver epoch", "[ble_log][wire]") +{ + ble_log_deinit(); + s_init_snapshot_hook_armed = true; + bool initialized = ble_log_init(); + s_init_snapshot_hook_armed = false; + TEST_ASSERT_TRUE(initialized); + + size_t len = ble_log_prph_test_read( + s_read_buf, sizeof(s_read_buf), pdMS_TO_TICKS(1000), 0, NULL); + TEST_ASSERT_GREATER_THAN_size_t(0, len); + + first_snapshot_capture_t capture = {0}; + TEST_ASSERT_TRUE(test_ble_log_walk_frames( + s_read_buf, len, capture_first_snapshot, &capture)); + TEST_ASSERT_TRUE(capture.found); + TEST_ASSERT_EQUAL_HEX16(BLE_LOG_SNAPSHOT_REASON_INIT, + capture.reason_flags); +} + +TEST_CASE("BLE Log sync IO APIs retain runtime lifecycle checks", "[ble_log]") +{ + ble_log_deinit(); + TEST_ASSERT_FALSE(ble_log_ts_sync_io_toggle_enable(true)); + TEST_ASSERT_FALSE(ble_log_sync_enable(true)); + + TEST_ASSERT_TRUE(ble_log_init()); + TEST_ASSERT_TRUE(ble_log_ts_sync_io_toggle_enable(false)); + TEST_ASSERT_TRUE(ble_log_sync_enable(false)); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } +} + typedef struct { bool task_frame; bool non_yield_frame; @@ -429,7 +494,7 @@ TEST_CASE("BLE Log marks non-yield context and commits claimed payload", claimed[0] = 0x44; ble_log_commit(fresh_handle, 1); - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); TEST_ASSERT_TRUE(ble_log_rt_drain()); frame_meta_capture_t capture = {0}; @@ -535,7 +600,7 @@ TEST_CASE("BLE Log serializes task context per compression source", "[ble_log][compression]") { TEST_ASSERT_TRUE(ble_log_enable(true)); - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); for (int round = 0; round < 2; round++) { TEST_ASSERT_TRUE(ble_log_rt_drain()); while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), @@ -583,7 +648,7 @@ TEST_CASE("BLE Log serializes task context per compression source", xSemaphoreGive(writer.exit); TEST_ASSERT_TRUE(xSemaphoreTake(writer.exited, pdMS_TO_TICKS(1000))); - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); TEST_ASSERT_TRUE(ble_log_rt_drain()); compression_capture_t capture = {0}; for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT; i++) { @@ -729,7 +794,7 @@ TEST_CASE("BLE Log preserves LL payload and rejects oversized records", }; TEST_ASSERT_TRUE(ble_log_enable(true)); - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); for (int round = 0; round < 2; round++) { TEST_ASSERT_TRUE(ble_log_rt_drain()); while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), @@ -762,7 +827,7 @@ TEST_CASE("BLE Log preserves LL payload and rejects oversized records", BIT(BLE_LOG_LL_FLAG_TASK)); #endif - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); TEST_ASSERT_TRUE(ble_log_rt_drain()); read_sequence_frames(captures, SEQUENCE_CAPTURE_COUNT); @@ -789,7 +854,7 @@ TEST_CASE("BLE Log flush preserves source-local sequence continuity", const uint8_t before_marker = 0x71; const uint8_t after_marker = 0x72; TEST_ASSERT_TRUE(ble_log_enable(true)); - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); for (int round = 0; round < 2; round++) { TEST_ASSERT_TRUE(ble_log_rt_drain()); while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), @@ -799,7 +864,7 @@ TEST_CASE("BLE Log flush preserves source-local sequence continuity", TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &before_marker, sizeof(before_marker))); - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); TEST_ASSERT_TRUE(ble_log_rt_drain()); sequence_capture_t before = { .src = BLE_LOG_SRC_CUSTOM, @@ -816,7 +881,7 @@ TEST_CASE("BLE Log flush preserves source-local sequence continuity", TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &after_marker, sizeof(after_marker))); - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); TEST_ASSERT_TRUE(ble_log_rt_drain()); sequence_capture_t after = { .src = BLE_LOG_SRC_CUSTOM, @@ -884,7 +949,7 @@ TEST_CASE("BLE Log pending-seal marker defers the busy transport flush", const uint8_t post_marker = 0xd4; TEST_ASSERT_TRUE(ble_log_enable(true)); - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); for (int round = 0; round < 2; round++) { TEST_ASSERT_TRUE(ble_log_rt_drain()); while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), @@ -919,7 +984,7 @@ TEST_CASE("BLE Log pending-seal marker defers the busy transport flush", TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &post_marker, sizeof(post_marker))); - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); TEST_ASSERT_TRUE(ble_log_rt_drain()); marker_chunk_capture_t chunks[2] = {0}; @@ -938,7 +1003,7 @@ TEST_CASE("BLE Log deinit drain delivers parked open transports", const uint8_t markers[TEST_MARKER_CHUNK_MAX] = {0xe1, 0xe2, 0xe3, 0xe4}; TEST_ASSERT_TRUE(ble_log_enable(true)); - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); for (int round = 0; round < 2; round++) { TEST_ASSERT_TRUE(ble_log_rt_drain()); while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), @@ -956,7 +1021,7 @@ TEST_CASE("BLE Log deinit drain delivers parked open transports", /* Exercise the exact deinit-drain contract: close the producer gate and * wait for writers before sealing every OPEN transport. */ ble_log_lbm_begin_deinit(); - ble_log_lbm_drain_open_transports(); + ble_log_lbm_drain_open_trans(); marker_chunk_capture_t chunks[1] = {0}; int chunk_count = read_marker_chunks(chunks, 1); @@ -980,7 +1045,7 @@ TEST_CASE("BLE Log deinit hands residual transports to the peripheral", int recycled = 0; TEST_ASSERT_TRUE(ble_log_enable(true)); - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); for (int round = 0; round < 2; round++) { TEST_ASSERT_TRUE(ble_log_rt_drain()); while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), @@ -1025,11 +1090,14 @@ static void capture_snapshot_loss(const test_ble_log_frame_t *frame, void *ctx) } } -TEST_CASE("BLE Log periodic snapshot fails fast while its transport is busy", +TEST_CASE("BLE Log periodic snapshot ignores producer gate and fails fast when busy", "[ble_log][lbm]") { + ble_log_ts_info_t ts_info; + ble_log_rt_ts_sample(&ts_info, false); + TEST_ASSERT_TRUE(ble_log_enable(true)); - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); for (int round = 0; round < 2; round++) { TEST_ASSERT_TRUE(ble_log_rt_drain()); while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), @@ -1038,24 +1106,34 @@ TEST_CASE("BLE Log periodic snapshot fails fast while its transport is busy", } TEST_ASSERT_TRUE(ble_log_enable(false)); + bool submitted = false; + for (int attempt = 0; attempt < TEST_MAX_ROUNDS && !submitted; attempt++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + submitted = ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC, &ts_info, false); + } + TEST_ASSERT_TRUE_MESSAGE(submitted, + "Periodic timer kept the Internal transport busy"); TEST_ASSERT_FALSE(ble_log_internal_snapshot( - BLE_LOG_SNAPSHOT_REASON_PERIODIC, NULL, false)); + BLE_LOG_SNAPSHOT_REASON_PERIODIC, &ts_info, false)); TEST_ASSERT_TRUE(ble_log_enable(true)); - - TEST_ASSERT_TRUE(ble_log_internal_snapshot( - BLE_LOG_SNAPSHOT_REASON_PERIODIC, NULL, false)); - TEST_ASSERT_FALSE(ble_log_internal_snapshot( - BLE_LOG_SNAPSHOT_REASON_PERIODIC, NULL, false)); - TEST_ASSERT_TRUE(ble_log_rt_drain()); - TEST_ASSERT_GREATER_THAN_size_t( - 0, ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), - pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), 0, NULL)); - - TEST_ASSERT_TRUE(ble_log_internal_snapshot( - BLE_LOG_SNAPSHOT_REASON_PERIODIC, NULL, false)); TEST_ASSERT_TRUE(ble_log_rt_drain()); snapshot_capture_t capture = {0}; + size_t first_len = ble_log_prph_test_read( + s_read_buf, sizeof(s_read_buf), pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + TEST_ASSERT_GREATER_THAN_size_t(0, first_len); + TEST_ASSERT_TRUE(test_ble_log_walk_frames( + s_read_buf, first_len, capture_snapshot_loss, &capture)); + + TEST_ASSERT_TRUE(ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC, &ts_info, false)); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT; i++) { size_t len = ble_log_prph_test_read( s_read_buf, sizeof(s_read_buf), pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), @@ -1137,7 +1215,7 @@ TEST_CASE("BLE Log deinit closes a parked LL writer before racing enable", BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t)] = {0}; TEST_ASSERT_TRUE(ble_log_enable(true)); - ble_log_lbm_flush_open_transports(); + ble_log_lbm_flush_open_trans(); TEST_ASSERT_TRUE(ble_log_rt_drain()); while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), 0, 0, NULL) > 0) { } From 278b0d96200c0522babffd95b86750db5f432815 Mon Sep 17 00:00:00 2001 From: Zhou Xiao Date: Thu, 3 Sep 2026 19:29:44 +0800 Subject: [PATCH 08/13] fix(ble_log): make yieldable public writes wait for a shared transport The NON_YIELD flag in bit 7 of the frame source byte forced every receiver to mask the byte before decoding the source, while carrying little information beyond what the source ID already implies. Protocol v7 is not released yet, so drop the flag instead of versioning it: the source byte now carries the bare frozen ble_log_src_t value. The internal pool reservation for non-yieldable contexts (BLE_LOG_POOL_NON_YIELD_RESERVE_CNT) is untouched: it is memory management, not wire format. Update the golden bytes (source byte 0x87 -> 0x07, recomputed checksum) and rename the critical-section frame capture test accordingly; the walker structs lose the redundant source_meta field. --- components/bt/common/ble_log/README.md | 14 +- .../bt/common/ble_log/include/ble_log.h | 2 + .../bt/common/ble_log/src/ble_log_lbm_v2.c | 23 +-- .../src/internal_include/ble_log_lbm_v2.h | 15 +- .../test_apps/ble_log_perf_test/README.md | 5 +- .../main/test_ble_log_main.c | 4 +- .../main/test_ble_log_main.h | 1 - .../main/test_ble_log_perf.c | 5 +- .../ble_log_rt_test/main/test_ble_log_main.c | 4 +- .../ble_log_rt_test/main/test_ble_log_main.h | 1 - .../ble_log/test_apps/ble_log_test/README.md | 2 +- .../ble_log_test/main/test_ble_log_main.c | 4 +- .../ble_log_test/main/test_ble_log_main.h | 1 - .../ble_log_test/main/test_ble_log_rt.c | 157 ++++++++++++++++-- 14 files changed, 170 insertions(+), 68 deletions(-) diff --git a/components/bt/common/ble_log/README.md b/components/bt/common/ble_log/README.md index eb9c0802efe..35a654ae8c1 100644 --- a/components/bt/common/ble_log/README.md +++ b/components/bt/common/ble_log/README.md @@ -19,8 +19,9 @@ flowchart TD The shared pool contains `CONFIG_BLE_LOG_POOL_TRANS_CNT` transports. The last `CONFIG_BLE_LOG_POOL_NON_YIELD_RESERVE_CNT` transports are reserved for ISR and -other contexts that cannot yield. Ordinary public writes are non-blocking. -Only the ordinary controller LL task path waits for a shared transport. +other contexts that cannot yield. Every yieldable-context writer, from the +public API and claims to the controller LL task, waits for a shared transport; +only ISR and critical-section writers fail fast. The Internal Snapshot and UART0 redirection transports are not members of the bitmap pool. @@ -48,8 +49,7 @@ offset size field `frame_meta` is: ```text -bits 0..6 base source -bit 7 NON_YIELD +bits 0..7 source bits 8..31 sequence number, low 24 bits ``` @@ -74,9 +74,8 @@ redirection flush) is the alignment reference against the core timeline. ### Sources -The public `ble_log_src_t` ABI is frozen and its values are the base on-wire -source IDs of protocol v7 frames. Receivers must mask the `NON_YIELD` bit -before decoding the base source: +The public `ble_log_src_t` ABI is frozen and its values are the on-wire +source IDs of protocol v7 frames: ```text 0 INTERNAL @@ -100,7 +99,6 @@ console batch). `ble_log_init()` resets all three sequences, and its required `INIT` snapshot starts a new receiver epoch. They remain continuous through `FLUSH` within that epoch. Callers must not write until `ble_log_init()` returns, so the `INIT` snapshot is submitted first. -Actual ISR and critical-section records carry `NON_YIELD` in source bit 7. Controller-side HCI records are not emitted by BLE Log, and the controller no longer maintains its own internal LL HCI log. Host-side Bluedroid and NimBLE diff --git a/components/bt/common/ble_log/include/ble_log.h b/components/bt/common/ble_log/include/ble_log.h index b62ace0ac2e..c5ce103819d 100644 --- a/components/bt/common/ble_log/include/ble_log.h +++ b/components/bt/common/ble_log/include/ble_log.h @@ -67,7 +67,9 @@ void ble_log_deinit(void); bool ble_log_enable(bool enable); /* Blocking; call only from a caller-owned task, not an ISR or system callback. */ void ble_log_flush(void); +/* Waits for a shared transport in yieldable contexts; ISR and critical-section callers fail fast. */ bool ble_log_write_hex(ble_log_src_t src_code, const uint8_t *addr, size_t len); +/* Same backpressure as ble_log_write_hex(): yieldable claims wait for a shared transport. */ uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, uint32_t *handle); void ble_log_commit(uint32_t handle, size_t actual_len); void ble_log_dump_to_console(void); diff --git a/components/bt/common/ble_log/src/ble_log_lbm_v2.c b/components/bt/common/ble_log/src/ble_log_lbm_v2.c index 092596489f4..c21ba964df9 100644 --- a/components/bt/common/ble_log/src/ble_log_lbm_v2.c +++ b/components/bt/common/ble_log/src/ble_log_lbm_v2.c @@ -99,7 +99,6 @@ typedef struct { * only needs buf/size/pos and the lifecycle state for DMA handoff. */ typedef struct { uint8_t src_code; /* source of the pending claim */ - uint8_t non_yield; /* claimed from a non-yieldable context */ uint16_t max_len; /* maximum committed payload length */ uint32_t frame_sn; /* Global SN consumed at claim entry */ uint32_t generation; /* stale-handle detection, wraps 24-bit */ @@ -149,7 +148,7 @@ BLE_LOG_STATIC void ble_log_pool_seal_and_send(ble_log_prph_trans_t *trans); BLE_LOG_STATIC void ble_log_pool_seal_open_trans(void); BLE_LOG_STATIC void ble_log_pool_write_frame(ble_log_prph_trans_t *trans, uint32_t frame_sn, - uint8_t source_meta, + ble_log_src_t src_code, const uint8_t *prefix, uint16_t prefix_len, const uint8_t *addr, uint16_t len, @@ -497,18 +496,17 @@ ble_log_prph_trans_t *ble_log_pool_acquire(size_t log_len, /* -------------------------------------- */ BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void ble_log_pool_write_frame(ble_log_prph_trans_t *trans, uint32_t frame_sn, - uint8_t source_meta, + ble_log_src_t src_code, const uint8_t *prefix, uint16_t prefix_len, const uint8_t *addr, uint16_t len, const uint8_t *addr_append, uint16_t len_append, bool omdata) { uint8_t *buf = trans->buf + trans->pos; uint16_t payload_len = prefix_len + len + len_append; - ble_log_src_t src_code = (ble_log_src_t)BLE_LOG_SRC_ID(source_meta); ble_log_stat_mgr_t *stat_mgr = &stat_mgr_ctx[src_code]; ble_log_frame_head_t frame_head = { .length = payload_len, - .frame_meta = BLE_LOG_MAKE_FRAME_META(source_meta, frame_sn), + .frame_meta = BLE_LOG_MAKE_FRAME_META(src_code, frame_sn), }; /* Memory operation */ @@ -584,7 +582,7 @@ uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, uint32_t *handle) xTaskGetSchedulerState() != taskSCHEDULER_RUNNING; size_t payload_capacity = sizeof(timestamp) + max_len; ble_log_prph_trans_t *trans = - ble_log_pool_acquire(payload_capacity, non_yield, false); + ble_log_pool_acquire(payload_capacity, non_yield, !non_yield); if (!trans) { ble_log_stat_mgr_mark_lost(src_code); BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); @@ -598,7 +596,6 @@ uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, uint32_t *handle) #endif ble_log_pool_claim_t *claim = &pool_claim_ctx[trans->id]; claim->src_code = (uint8_t)src_code; - claim->non_yield = non_yield; claim->max_len = (uint16_t)max_len; claim->frame_sn = frame_sn; claim->generation = (claim->generation + 1) & 0x00ffffffU; @@ -648,11 +645,9 @@ void ble_log_commit(uint32_t handle, size_t actual_len) uint16_t payload_len = (uint16_t)(sizeof(uint32_t) + actual_len); ble_log_stat_mgr_t *stat_mgr = &stat_mgr_ctx[src_code]; - uint8_t source_meta = BLE_LOG_MAKE_SOURCE_META(src_code, - claim->non_yield); ble_log_frame_head_t frame_head = { .length = payload_len, - .frame_meta = BLE_LOG_MAKE_FRAME_META(source_meta, claim->frame_sn), + .frame_meta = BLE_LOG_MAKE_FRAME_META(src_code, claim->frame_sn), }; BLE_LOG_MEMCPY(trans->buf + trans->pos, &frame_head, BLE_LOG_FRAME_HEAD_LEN); ble_log_pool_finish_frame(trans, payload_len, stat_mgr); @@ -1125,13 +1120,12 @@ bool ble_log_write_hex(ble_log_src_t src_code, const uint8_t *addr, size_t len) uint32_t timestamp = BLE_LOG_TIMESTAMP_NOW(); size_t payload_len = sizeof(timestamp) + len; ble_log_prph_trans_t *trans = - ble_log_pool_acquire(payload_len, !can_yield, false); + ble_log_pool_acquire(payload_len, !can_yield, can_yield); if (!trans) { goto failed; } - uint8_t source_meta = BLE_LOG_MAKE_SOURCE_META(src_code, !can_yield); - ble_log_pool_write_frame(trans, frame_sn, source_meta, + ble_log_pool_write_frame(trans, frame_sn, src_code, (const uint8_t *)×tamp, sizeof(timestamp), addr, (uint16_t)len, NULL, 0, false); @@ -1194,8 +1188,7 @@ void ble_log_write_hex_ll(uint32_t len, const uint8_t *addr, goto failed; } - uint8_t source_meta = BLE_LOG_MAKE_SOURCE_META(src_code, non_yield); - ble_log_pool_write_frame(trans, frame_sn, source_meta, + ble_log_pool_write_frame(trans, frame_sn, src_code, NULL, 0, addr, (uint16_t)len, addr_append, (uint16_t)len_append, omdata); diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h index f78f7e4c896..a61f337e566 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h @@ -31,10 +31,8 @@ typedef struct { #define BLE_LOG_FRAME_HEAD_LEN (sizeof(ble_log_frame_head_t)) #define BLE_LOG_FRAME_TAIL_LEN (sizeof(uint32_t)) #define BLE_LOG_FRAME_OVERHEAD (BLE_LOG_FRAME_HEAD_LEN + BLE_LOG_FRAME_TAIL_LEN) -#define BLE_LOG_MAKE_SOURCE_META(src, non_yield) \ - (((src) & BLE_LOG_SRC_ID_MASK) | ((non_yield) ? BLE_LOG_SRC_FLAG_NON_YIELD : 0)) -#define BLE_LOG_MAKE_FRAME_META(source_meta, sn) \ - (((source_meta) & 0xffU) | (((sn) & 0x00ffffffU) << 8)) +#define BLE_LOG_MAKE_FRAME_META(src, sn) \ + (((src) & 0xffU) | (((sn) & 0x00ffffffU) << 8)) /* ------------------------------------- */ /* Unified Buffer Pool Defines */ @@ -59,13 +57,8 @@ typedef struct { /* --------------------------------------- */ /* Protocol v7 Source ID Space */ /* --------------------------------------- */ -/* The frozen public ble_log_src_t values are the base on-wire and statistic - * source IDs. Bit 7 of the frame source byte carries NON_YIELD metadata, so - * receivers must mask it before decoding the base source. */ -#define BLE_LOG_SRC_ID_MASK 0x7f -#define BLE_LOG_SRC_FLAG_NON_YIELD 0x80 -#define BLE_LOG_SRC_ID(source_meta) ((source_meta) & BLE_LOG_SRC_ID_MASK) -#define BLE_LOG_SRC_IS_NON_YIELD(source_meta) (((source_meta) & BLE_LOG_SRC_FLAG_NON_YIELD) != 0) +/* The frozen public ble_log_src_t values are the on-wire and statistic + * source IDs: the frame source byte carries the bare enum value. */ /* Statistic slots in the Internal Snapshot: every public source that can * produce frames, i.e. CUSTOM through ENCODE. INTERNAL frames carry their diff --git a/components/bt/common/ble_log/test_apps/ble_log_perf_test/README.md b/components/bt/common/ble_log/test_apps/ble_log_perf_test/README.md index 50fd303a5ec..dc505520509 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_perf_test/README.md +++ b/components/bt/common/ble_log/test_apps/ble_log_perf_test/README.md @@ -31,8 +31,9 @@ Runtime dispatch behavior and latency are covered by the sibling - `write_hex cycles`: single writer, no link cap, payload 8/32/64/128 B. The scheduler remains active during each measured call, followed by an unmeasured one-tick pacing delay so the no-loss profile does not become a saturation test. -- `write_hex drop path cycles`: saturated 2 Mbps link, measures the cost of a - failed (dropped) write without the no-loss pacing delay. +- `write_hex drop path cycles`: saturated 2 Mbps link, measures the backpressure + cost of a parked write (wait and wake on transport recycle) without the + no-loss pacing delay. - `write_hex_ll cycles`: payload 8/32/64/128 B; plus a 32+32 B append case. - `compressed write cycles`: workload matrix of the compressed entry points — U32 args (0/1/2/mixed), U64 values (full 8B / leading-zero LZ / zero), diff --git a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.c b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.c index 2642e629c31..0e874caf815 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.c +++ b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.c @@ -35,10 +35,8 @@ bool test_ble_log_walk_frames(const uint8_t *data, size_t len, } if (observer) { - uint8_t source_meta = head.frame_meta & 0xff; test_ble_log_frame_t frame = { - .src = BLE_LOG_SRC_ID(source_meta), - .source_meta = source_meta, + .src = (ble_log_src_t)(head.frame_meta & 0xff), .sn = head.frame_meta >> 8, .payload = data + offset + BLE_LOG_FRAME_HEAD_LEN, .payload_len = head.length, diff --git a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.h b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.h index 4e6abd1a6ce..1f82cf73b33 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.h +++ b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_main.h @@ -14,7 +14,6 @@ typedef struct { ble_log_src_t src; - uint8_t source_meta; uint32_t sn; const uint8_t *payload; size_t payload_len; diff --git a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c index 5679d0e24df..fdf0ee0180e 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c +++ b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c @@ -979,8 +979,9 @@ TEST_CASE("BLE Log write_hex cycles (single writer, link=0)", "[ble_log][perf][c } } -/* Saturated link: most writes fail. Measures the drop-path cost and - * cross-checks client failed counts against the LBM's lost counters. */ +/* Saturated link: yieldable writers park on backpressure instead of dropping; + * measures the park/wake cost and cross-checks client failed counts against + * the LBM's lost counters. */ TEST_CASE("BLE Log write_hex drop path cycles (link=2Mbps)", "[ble_log][perf][cycle][ignore]") { perf_run_cfg_t cfg = { diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c index 9db296dd963..f8856c86936 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.c @@ -36,10 +36,8 @@ bool test_ble_log_walk_frames(const uint8_t *data, size_t len, } if (observer) { - uint8_t source_meta = head.frame_meta & 0xff; test_ble_log_frame_t frame = { - .src = BLE_LOG_SRC_ID(source_meta), - .source_meta = source_meta, + .src = (ble_log_src_t)(head.frame_meta & 0xff), .sn = head.frame_meta >> 8, .payload = data + offset + BLE_LOG_FRAME_HEAD_LEN, .payload_len = head.length, diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.h b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.h index 4e6abd1a6ce..1f82cf73b33 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.h +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_main.h @@ -14,7 +14,6 @@ typedef struct { ble_log_src_t src; - uint8_t source_meta; uint32_t sn; const uint8_t *payload; size_t payload_len; diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/README.md b/components/bt/common/ble_log/test_apps/ble_log_test/README.md index 47b48cf12a2..6649298b486 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/README.md +++ b/components/bt/common/ble_log/test_apps/ble_log_test/README.md @@ -14,7 +14,7 @@ It covers: - literal protocol-v7 framing and fixed Internal Snapshot ABI; - build, library, chip, and protocol versions inside the snapshot; -- task and `NON_YIELD` source metadata plus HCI direction encoding; +- task and critical-section writes plus HCI direction encoding; - direct compression claim/commit, stale handles, and per-source serialization; - oversized-record rejection, flush sequence continuity, pool exhaustion, and non-yield reserve use; - periodic snapshot busy/loss behavior; diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c index 4048079c8cf..0d0ec263587 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.c @@ -35,10 +35,8 @@ bool test_ble_log_walk_frames(const uint8_t *data, size_t len, } if (observer) { - uint8_t source_meta = head.frame_meta & 0xff; test_ble_log_frame_t frame = { - .src = BLE_LOG_SRC_ID(source_meta), - .source_meta = source_meta, + .src = (ble_log_src_t)(head.frame_meta & 0xff), .sn = head.frame_meta >> 8, .payload = data + offset + BLE_LOG_FRAME_HEAD_LEN, .payload_len = head.length, diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.h b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.h index 4e6abd1a6ce..1f82cf73b33 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.h +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_main.h @@ -14,7 +14,6 @@ typedef struct { ble_log_src_t src; - uint8_t source_meta; uint32_t sn; const uint8_t *payload; size_t payload_len; diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c index 655b1cccdb5..f79c10a1f51 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c @@ -242,16 +242,15 @@ static void capture_golden_frame(const test_ble_log_frame_t *frame, void *ctx) TEST_CASE("BLE Log v7 framing matches golden bytes", "[ble_log][wire]") { static const uint8_t golden_frame[] = { - 0x05, 0x00, 0x87, 0xde, 0xc0, 0x00, + 0x05, 0x00, 0x07, 0xde, 0xc0, 0x00, 0x78, 0x56, 0x34, 0x12, 0xab, - 0xf1, 0x12, 0x54, 0x88, + 0xf1, 0x12, 0xd4, 0x88, }; static const uint8_t golden_payload[] = { 0x78, 0x56, 0x34, 0x12, 0xab, }; TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_VERSION); - TEST_ASSERT_EQUAL_HEX8(0x80, BLE_LOG_SRC_FLAG_NON_YIELD); TEST_ASSERT_EQUAL_UINT8(1, BLE_LOG_SRC_CORE_FIRST); TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_SRC_CORE_COUNT); TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_SRC_ENCODE); @@ -275,7 +274,7 @@ TEST_CASE("BLE Log v7 framing matches golden bytes", "[ble_log][wire]") TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_LL_FLAG_HCI_UPSTREAM); #endif TEST_ASSERT_EQUAL_HEX32( - 0x00c0de87, BLE_LOG_MAKE_FRAME_META(0x87, 0x00c0de)); + 0x00c0de07, BLE_LOG_MAKE_FRAME_META(0x07, 0x00c0de)); TEST_ASSERT_EQUAL_HEX32( 0x00000007, BLE_LOG_MAKE_FRAME_META(0x07, 0x01000000)); @@ -286,7 +285,6 @@ TEST_CASE("BLE Log v7 framing matches golden bytes", "[ble_log][wire]") &capture)); TEST_ASSERT_EQUAL_size_t(1, capture.count); TEST_ASSERT_EQUAL_UINT8(BLE_LOG_SRC_ENCODE, capture.frame.src); - TEST_ASSERT_EQUAL_HEX8(0x87, capture.frame.source_meta); TEST_ASSERT_EQUAL_HEX32(0x00c0de, capture.frame.sn); TEST_ASSERT_EQUAL_size_t(sizeof(golden_payload), capture.frame.payload_len); TEST_ASSERT_EQUAL_MEMORY(golden_payload, capture.frame.payload, @@ -402,7 +400,7 @@ TEST_CASE("BLE Log sync IO APIs retain runtime lifecycle checks", "[ble_log]") typedef struct { bool task_frame; - bool non_yield_frame; + bool critical_frame; bool hci_downstream_frame; bool hci_upstream_frame; bool claimed_frame; @@ -422,10 +420,9 @@ static void capture_frame_meta(const test_ble_log_frame_t *frame, void *ctx) uint8_t marker = frame->payload[sizeof(uint32_t)]; if (frame->src == BLE_LOG_SRC_CUSTOM && marker == 0x11) { - capture->task_frame = !BLE_LOG_SRC_IS_NON_YIELD(frame->source_meta); + capture->task_frame = true; } else if (frame->src == BLE_LOG_SRC_CUSTOM && marker == 0x22) { - capture->non_yield_frame = - BLE_LOG_SRC_IS_NON_YIELD(frame->source_meta); + capture->critical_frame = true; } else if (frame->src == BLE_LOG_SRC_HCI && marker == 0x01) { capture->hci_downstream_frame = true; } else if (frame->src == BLE_LOG_SRC_HCI && marker == 0x82) { @@ -437,7 +434,7 @@ static void capture_frame_meta(const test_ble_log_frame_t *frame, void *ctx) } } -TEST_CASE("BLE Log marks non-yield context and commits claimed payload", +TEST_CASE("BLE Log writes from critical sections and commits claimed payload", "[ble_log][lbm]") { const uint8_t task_marker = 0x11; @@ -511,7 +508,7 @@ TEST_CASE("BLE Log marks non-yield context and commits claimed payload", } TEST_ASSERT_TRUE(capture.task_frame); - TEST_ASSERT_TRUE(capture.non_yield_frame); + TEST_ASSERT_TRUE(capture.critical_frame); TEST_ASSERT_TRUE(capture.hci_downstream_frame); TEST_ASSERT_TRUE(capture.hci_upstream_frame); TEST_ASSERT_TRUE(capture.claimed_frame); @@ -1065,6 +1062,138 @@ TEST_CASE("BLE Log deinit hands residual transports to the peripheral", TEST_ASSERT_TRUE(ble_log_init()); } +typedef struct { + SemaphoreHandle_t started; + SemaphoreHandle_t done; + bool result; +} blocked_writer_ctx_t; + +static void blocked_write_task(void *arg) +{ + blocked_writer_ctx_t *ctx = arg; + static const uint8_t marker = 0x57; + xSemaphoreGive(ctx->started); + ctx->result = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &marker, + sizeof(marker)); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +static void blocked_claim_task(void *arg) +{ + blocked_writer_ctx_t *ctx = arg; + uint32_t handle; + xSemaphoreGive(ctx->started); + uint8_t *payload = ble_log_claim(BLE_LOG_SRC_ENCODE, 1, &handle); + ctx->result = payload != NULL; + if (payload) { + payload[0] = 0x58; + } + ble_log_commit(handle, 1); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +typedef struct { + bool write_frame; + bool claim_frame; +} blocked_capture_t; + +static void capture_blocked_markers(const test_ble_log_frame_t *frame, void *ctx) +{ + blocked_capture_t *capture = ctx; + if (frame->payload_len != sizeof(uint32_t) + 1) { + return; + } + uint8_t marker = frame->payload[sizeof(uint32_t)]; + if (frame->src == BLE_LOG_SRC_CUSTOM && marker == 0x57) { + capture->write_frame = true; + } else if (frame->src == BLE_LOG_SRC_ENCODE && marker == 0x58) { + capture->claim_frame = true; + } +} + +TEST_CASE("BLE Log task writers wait for a shared transport", "[ble_log][lbm]") +{ + static const uint8_t full_payload[ + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t)] = {0}; + + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + /* Keep every task-usable transport SENDING in the test peripheral. */ + for (int i = 0; i < BLE_LOG_POOL_SHARED_CNT; i++) { + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, full_payload, + sizeof(full_payload))); + } + + blocked_writer_ctx_t writers[2] = {0}; + for (int i = 0; i < 2; i++) { + writers[i].started = xSemaphoreCreateBinary(); + writers[i].done = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(writers[i].started); + TEST_ASSERT_NOT_NULL(writers[i].done); + } + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(blocked_write_task, "ble_log_wait1", + TEST_LIFECYCLE_STACK_SIZE, &writers[0], + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(blocked_claim_task, "ble_log_wait2", + TEST_LIFECYCLE_STACK_SIZE, &writers[1], + TEST_LIFECYCLE_PRIO, NULL)); + for (int i = 0; i < 2; i++) { + TEST_ASSERT_TRUE(xSemaphoreTake(writers[i].started, + pdMS_TO_TICKS(1000))); + } + vTaskDelay(1); + /* No shared transport is free: both yieldable writers must be parked. */ + for (int i = 0; i < 2; i++) { + TEST_ASSERT_EQUAL(pdFALSE, xSemaphoreTake(writers[i].done, 0)); + } + + /* Recycling transports wakes the parked writers; neither write is lost. */ + TEST_ASSERT_TRUE(ble_log_rt_drain()); + for (int i = 0; i < 2; i++) { + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + TEST_ASSERT_GREATER_THAN_size_t(0, len); + } + for (int i = 0; i < 2; i++) { + TEST_ASSERT_TRUE(xSemaphoreTake(writers[i].done, pdMS_TO_TICKS(1000))); + TEST_ASSERT_TRUE(writers[i].result); + } + + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + blocked_capture_t capture = {0}; + for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT; i++) { + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + TEST_ASSERT_TRUE(test_ble_log_walk_frames(s_read_buf, len, + capture_blocked_markers, + &capture)); + } + TEST_ASSERT_TRUE(capture.write_frame); + TEST_ASSERT_TRUE(capture.claim_frame); + + for (int i = 0; i < 2; i++) { + vSemaphoreDelete(writers[i].started); + vSemaphoreDelete(writers[i].done); + } +} + #define SNAPSHOT_CAPTURE_MAX 8 typedef struct { @@ -1229,12 +1358,6 @@ TEST_CASE("BLE Log deinit closes a parked LL writer before racing enable", sizeof(full_payload))); } static const uint8_t reserve_marker = 0x56; - TEST_ASSERT_FALSE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, - &reserve_marker, - sizeof(reserve_marker))); - uint32_t exhausted_handle; - TEST_ASSERT_NULL(ble_log_claim(BLE_LOG_SRC_ENCODE, 1, - &exhausted_handle)); portMUX_TYPE reserve_mux = portMUX_INITIALIZER_UNLOCKED; portENTER_CRITICAL(&reserve_mux); From 0b8368ee86213a6a10ba0980273147058c451f86 Mon Sep 17 00:00:00 2001 From: Zhou Xiao Date: Sat, 5 Sep 2026 12:00:17 +0800 Subject: [PATCH 09/13] fix(ble_log): restore controller HCI capture without Host hooks Keep Host capture for Bluedroid and legacy VHCI NimBLE while retaining controller HCI records for transports without Host capture. Respect the HCI logging switch and preserve controller source mapping and payloads. Extend metadata regression coverage and document capture selection. --- components/bt/common/ble_log/Kconfig.in | 12 +++---- components/bt/common/ble_log/README.md | 20 ++++++----- .../bt/common/ble_log/src/ble_log_lbm_v2.c | 28 ++++++++++----- .../ble_log_test/main/test_ble_log_rt.c | 35 +++++++++++++++++++ 4 files changed, 71 insertions(+), 24 deletions(-) diff --git a/components/bt/common/ble_log/Kconfig.in b/components/bt/common/ble_log/Kconfig.in index 22d3ab6e7d0..f0ba28e06be 100644 --- a/components/bt/common/ble_log/Kconfig.in +++ b/components/bt/common/ble_log/Kconfig.in @@ -77,12 +77,12 @@ if BLE_LOG_ENABLED bool "Enable BLE HCI Logging" default y help - Enable HCI packet logging captured on the Host side - (Bluedroid / NimBLE HCI HAL). This is the single HCI - logging switch for both Host and Controller traffic: - the controller no longer maintains its own internal - LL HCI log, and controller-side HCI records are not - emitted separately. + Enable HCI packet logging. Bluedroid and legacy VHCI NimBLE + capture on the Host side and suppress duplicate Controller + HCI records. Other configurations, including non-legacy + NimBLE, retain Controller HCI records through the LL callback + and require BLE_LOG_LL_ENABLED. Disabling this option + suppresses HCI records from both capture paths. config BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED bool "Enable BLE Host side HCI Logging" diff --git a/components/bt/common/ble_log/README.md b/components/bt/common/ble_log/README.md index 35a654ae8c1..cb6fbda3288 100644 --- a/components/bt/common/ble_log/README.md +++ b/components/bt/common/ble_log/README.md @@ -100,12 +100,14 @@ console batch). `ble_log_init()` resets all three sequences, and its required `FLUSH` within that epoch. Callers must not write until `ble_log_init()` returns, so the `INIT` snapshot is submitted first. -Controller-side HCI records are not emitted by BLE Log, and the controller no -longer maintains its own internal LL HCI log. Host-side Bluedroid and NimBLE -HCI capture (`CONFIG_BLE_LOG_HCI_LOG_ENABLED`) is the single HCI logging -switch and the authoritative `HCI` stream for both Host and Controller -traffic. Its direction bit continues to use HCI payload byte 0 bit 7; this is -independent of the source metadata bit. +`CONFIG_BLE_LOG_HCI_LOG_ENABLED` controls HCI logging. Bluedroid and legacy +VHCI NimBLE retain Host-side capture and suppress duplicate Controller HCI +records. Other configurations, including non-legacy NimBLE, retain Controller +HCI records through the LL callback (requires `CONFIG_BLE_LOG_LL_ENABLED`). +Controller flags preserve their source mapping: `HCI` to `LL_HCI` and +`HCI_UPSTREAM` to `HCI`, with ISR precedence. Controller payloads are forwarded +unchanged. Host-side capture retains its direction bit in HCI payload byte 0 +bit 7. Disabling HCI logging suppresses records from both capture paths. ## Internal Snapshot @@ -212,7 +214,7 @@ canceled and counted as lost. | `CONFIG_BLE_LOG_POOL_NON_YIELD_RESERVE_CNT` | 1 | ISR/critical reserve count | | `CONFIG_BLE_LOG_POOL_TRANS_SIZE` | 640 | Bytes per shared transport; SPI builds require a multiple of four | | `CONFIG_BLE_LOG_LL_ENABLED` | target dependent | Controller LL logging | -| `CONFIG_BLE_LOG_HCI_LOG_ENABLED` | target dependent | Host-side HCI capture | +| `CONFIG_BLE_LOG_HCI_LOG_ENABLED` | y | HCI capture from Host or Controller, selected by transport | | `CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED` | n | Build the optional analyzer GPIO toggle | | `CONFIG_BLE_LOG_TS_ENABLED` | n | Deprecated compatibility entry selecting the GPIO toggle | @@ -234,6 +236,6 @@ idf.py build ``` `ble_log_test` validates golden v7 bytes, the consolidated Internal Snapshot, -source/HCI metadata, pool exhaustion and reserve use, snapshot busy loss, -stale claims, and enable/disable/deinit races. `ble_log_rt_test` covers batched +source/HCI metadata and capture selection, pool exhaustion and reserve use, +snapshot busy loss, stale claims, and enable/disable/deinit races. `ble_log_rt_test` covers batched dispatch, timer behavior, inflight statistics, and repeated deinit races. diff --git a/components/bt/common/ble_log/src/ble_log_lbm_v2.c b/components/bt/common/ble_log/src/ble_log_lbm_v2.c index c21ba964df9..ef33c795b64 100644 --- a/components/bt/common/ble_log/src/ble_log_lbm_v2.c +++ b/components/bt/common/ble_log/src/ble_log_lbm_v2.c @@ -1145,10 +1145,14 @@ BLE_LOG_IRAM_ATTR void ble_log_write_hex_ll(uint32_t len, const uint8_t *addr, uint32_t len_append, const uint8_t *addr_append, uint32_t flag) { - /* Controller-side HCI logging duplicates the retained Host-side HCI path. */ - if ((flag & BIT(BLE_LOG_LL_FLAG_HCI)) || - (flag & BIT(BLE_LOG_LL_FLAG_HCI_UPSTREAM)) || - (!addr && len) || (!addr_append && len_append)) { +#if !CONFIG_BLE_LOG_HCI_LOG_ENABLED || CONFIG_BT_BLUEDROID_ENABLED || \ + CONFIG_BT_NIMBLE_LEGACY_VHCI_ENABLE + /* Bluedroid and legacy NimBLE already capture HCI on the Host side. */ + if (flag & (BIT(BLE_LOG_LL_FLAG_HCI) | BIT(BLE_LOG_LL_FLAG_HCI_UPSTREAM))) { + return; + } +#endif + if ((!addr && len) || (!addr_append && len_append)) { return; } @@ -1160,11 +1164,17 @@ void ble_log_write_hex_ll(uint32_t len, const uint8_t *addr, * ties between equal-timestamp records from different sources. */ uint32_t frame_sn = BLE_LOG_GET_GLOBAL_SN(); - /* Controller-side HCI records are dropped above, so only the LL task - * and ISR identities remain: ISR-flagged records keep their own - * LL_ISR source ID, everything else is LL_TASK. */ - ble_log_src_t src_code = (flag & BIT(BLE_LOG_LL_FLAG_ISR)) ? - BLE_LOG_SRC_LL_ISR : BLE_LOG_SRC_LL_TASK; + /* Preserve the controller flag-to-source mapping and ISR precedence. */ + ble_log_src_t src_code; + if (flag & BIT(BLE_LOG_LL_FLAG_ISR)) { + src_code = BLE_LOG_SRC_LL_ISR; + } else if (flag & BIT(BLE_LOG_LL_FLAG_HCI)) { + src_code = BLE_LOG_SRC_LL_HCI; + } else if (flag & BIT(BLE_LOG_LL_FLAG_HCI_UPSTREAM)) { + src_code = BLE_LOG_SRC_HCI; + } else { + src_code = BLE_LOG_SRC_LL_TASK; + } bool is_isr = BLE_LOG_IN_ISR(); bool can_yield = !is_isr && xPortCanYield() && diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c index f79c10a1f51..dc6cc952e39 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c @@ -403,6 +403,8 @@ typedef struct { bool critical_frame; bool hci_downstream_frame; bool hci_upstream_frame; + int ll_hci_frame_cnt; + int ll_hci_upstream_frame_cnt; bool claimed_frame; bool stale_protected_frame; int encode_frame_cnt; @@ -431,6 +433,18 @@ static void capture_frame_meta(const test_ble_log_frame_t *frame, void *ctx) capture->claimed_frame = true; } else if (frame->src == BLE_LOG_SRC_ENCODE && marker == 0x44) { capture->stale_protected_frame = true; + } else if (marker == 0x51 || marker == 0x52) { + static const uint8_t controller_timestamp[] = {0x78, 0x56, 0x34, 0x12}; + TEST_ASSERT_EQUAL_size_t(sizeof(controller_timestamp) + 1, frame->payload_len); + TEST_ASSERT_EQUAL_MEMORY(controller_timestamp, frame->payload, + sizeof(controller_timestamp)); + if (marker == 0x51) { + TEST_ASSERT_EQUAL_UINT8(BLE_LOG_SRC_LL_HCI, frame->src); + capture->ll_hci_frame_cnt++; + } else { + TEST_ASSERT_EQUAL_UINT8(BLE_LOG_SRC_HCI, frame->src); + capture->ll_hci_upstream_frame_cnt++; + } } } @@ -468,6 +482,18 @@ TEST_CASE("BLE Log writes from critical sections and commits claimed payload", * input at the validating API instead. */ TEST_ASSERT_FALSE(ble_log_write_hex(BLE_LOG_SRC_HCI, NULL, 1)); +#if CONFIG_BLE_LOG_LL_ENABLED + /* Controller records keep their existing payload, including timestamp. + * Exercise both contiguous and appended payloads at the real LL entry. */ + static const uint8_t ll_hci_payload[] = {0x78, 0x56, 0x34, 0x12, 0x51}; + const uint8_t ll_hci_upstream = 0x52; + ble_log_write_hex_ll(sizeof(ll_hci_payload), ll_hci_payload, 0, NULL, + BIT(BLE_LOG_LL_FLAG_HCI)); + ble_log_write_hex_ll(sizeof(ll_hci_payload) - 1, ll_hci_payload, + sizeof(ll_hci_upstream), &ll_hci_upstream, + BIT(BLE_LOG_LL_FLAG_HCI_UPSTREAM)); +#endif + uint32_t handle; uint8_t *claimed = ble_log_claim(BLE_LOG_SRC_ENCODE, 8, &handle); TEST_ASSERT_NOT_NULL(claimed); @@ -511,6 +537,15 @@ TEST_CASE("BLE Log writes from critical sections and commits claimed payload", TEST_ASSERT_TRUE(capture.critical_frame); TEST_ASSERT_TRUE(capture.hci_downstream_frame); TEST_ASSERT_TRUE(capture.hci_upstream_frame); +#if CONFIG_BLE_LOG_LL_ENABLED && CONFIG_BLE_LOG_HCI_LOG_ENABLED && \ + !CONFIG_BT_BLUEDROID_ENABLED && !CONFIG_BT_NIMBLE_LEGACY_VHCI_ENABLE + TEST_ASSERT_EQUAL(1, capture.ll_hci_frame_cnt); + TEST_ASSERT_EQUAL(1, capture.ll_hci_upstream_frame_cnt); +#else + /* Disabled HCI or an existing Host capture must suppress duplicates. */ + TEST_ASSERT_EQUAL(0, capture.ll_hci_frame_cnt); + TEST_ASSERT_EQUAL(0, capture.ll_hci_upstream_frame_cnt); +#endif TEST_ASSERT_TRUE(capture.claimed_frame); TEST_ASSERT_TRUE(capture.stale_protected_frame); /* Exactly the two committed claims: a stale commit that slipped past From 2cf0638686c686f642b8a863a22ee2fba7bf1453 Mon Sep 17 00:00:00 2001 From: Zhou Xiao Date: Sat, 5 Sep 2026 01:35:05 +0800 Subject: [PATCH 10/13] feat(ble_log): attribute compressed records with a task-id registry Protocol v8: every ENCODE record carries the one-byte id of the task that formatted it, replacing the incremental TASK_SWITCH marker scheme. Attribution becomes a property of the record: the per-source CAS lock, the last_task_handle writeback, and the try-lock contention drops are deleted, so concurrent writers to one source can no longer lose records to attribution races. The registry is a self-contained module (ble_log_task_registry.c/h, structured like the UART redirection writer): an append-only name-keyed table shared by every ENCODE writer, 16 bytes of RAM per entry, sized by CONFIG_BLE_LOG_TASK_ID_MAX. Word compares resolve ids lock-free on the record path; the registration CAS serializes only the cold path (once per task lifetime), and a record resolves its writer id after its claim succeeds, so the claim's lifetime reference pins the registry epoch and the id cannot cross an init/deinit boundary. A full registry or a contended registration degrades that record to the unknown id (0xFF) and still emits it. ISR callers stop at the lookup-miss branch before registration mutates shared state. Bindings are module-owned system output: every periodic snapshot window broadcasts one INTERNAL frame packing one fixed-layout record per registered entry on the registry's own dedicated transport, with a sequence of its own (a gap counts a skipped broadcast window, never a lost snapshot). A busy transport skips the window and the next one rebroadcasts, so a receiver that joined late converges on the next window. BLE_LOG_VERSION is bumped to 8: old decoders must not parse the new record layout. The compression encoders reject truncated NULL-buffer records instead of committing partial payloads, and the test app enables host compression with a 4-entry registry so the table-full path is reachable on target. --- components/bt/common/CMakeLists.txt | 1 + components/bt/common/ble_log/Kconfig.in | 34 +- components/bt/common/ble_log/README.md | 70 ++- .../extension/log_compression/Kconfig.in | 1 + .../log_compression/ble_log_compression.c | 91 +-- .../include/log_compression/utils.h | 12 +- .../bt/common/ble_log/include/ble_log.h | 10 +- .../bt/common/ble_log/src/ble_log_lbm_v2.c | 31 +- components/bt/common/ble_log/src/ble_log_rt.c | 7 +- .../ble_log/src/ble_log_task_registry.c | 272 +++++++++ .../src/internal_include/ble_log_lbm_v2.h | 28 +- .../internal_include/ble_log_task_registry.h | 87 +++ .../src/internal_include/ble_log_util.h | 5 +- .../main/test_ble_log_perf.c | 82 +++ .../ble_log/test_apps/ble_log_test/README.md | 2 +- .../ble_log_test/main/test_ble_log_rt.c | 572 +++++++++++++----- .../test_apps/ble_log_test/sdkconfig.defaults | 5 + 17 files changed, 1033 insertions(+), 277 deletions(-) create mode 100644 components/bt/common/ble_log/src/ble_log_task_registry.c create mode 100644 components/bt/common/ble_log/src/internal_include/ble_log_task_registry.h diff --git a/components/bt/common/CMakeLists.txt b/components/bt/common/CMakeLists.txt index 2f2f99b10cb..91e8c14b379 100644 --- a/components/bt/common/CMakeLists.txt +++ b/components/bt/common/CMakeLists.txt @@ -173,6 +173,7 @@ if(CONFIG_BLE_LOG_ENABLED) "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_lbm_v2.c" "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_redir.c" "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_rt.c" + "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_task_registry.c" "${CMAKE_CURRENT_LIST_DIR}/ble_log/src/ble_log_util.c" ) diff --git a/components/bt/common/ble_log/Kconfig.in b/components/bt/common/ble_log/Kconfig.in index f0ba28e06be..ab45d42f07a 100644 --- a/components/bt/common/ble_log/Kconfig.in +++ b/components/bt/common/ble_log/Kconfig.in @@ -35,6 +35,24 @@ if BLE_LOG_ENABLED in one transport. SPI builds require a multiple of four so they can append peripheral-only zero padding to each logical frame. + config BLE_LOG_TASK_ID_MAX + int "Maximum number of distinct logging tasks" + range 2 32 + default 16 + help + Size of the task-id registry (protocol v8). Every ENCODE record + carries the one-byte id of the task that formatted it; the + registry maps task names to those stable ids. One binding record + per registered entry is broadcast as an INTERNAL frame on every + periodic snapshot window, so a receiver that missed or lost a + binding converges on the next window. A task that starts logging + after the registry is full falls back to the unknown id (0xFF) + and its records are still emitted. + + RAM cost is 16 bytes per entry (256 bytes at the default 16) + plus one dedicated binding transport (320 bytes at 16 entries, + 624 at 32; the broadcast frame must fit in one transport). + config BLE_LOG_IS_ESP_CONTROLLER bool "Current BLE Controller is ESP BLE Controller" depends on BT_CONTROLLER_ENABLED @@ -84,14 +102,6 @@ if BLE_LOG_ENABLED and require BLE_LOG_LL_ENABLED. Disabling this option suppresses HCI records from both capture paths. - config BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED - bool "Enable BLE Host side HCI Logging" - default n - select BLE_LOG_HCI_LOG_ENABLED - help - Deprecated. Renamed to BLE_LOG_HCI_LOG_ENABLED; kept so sdkconfig - files pinning the old name keep Host side HCI logging enabled. - config BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED bool "Toggle a GPIO on every BLE Log TS sync sample" default n @@ -270,6 +280,14 @@ if BLE_LOG_ENABLED config BLE_LOG_TS_TRIGGER_ESP_TIMER_ISR_DISPATCH_METHOD bool default n + + # Deprecated: renamed to BLE_LOG_HCI_LOG_ENABLED. No prompt, so it + # never appears in menuconfig; kept so sdkconfig files pinning the old + # name keep Host side HCI logging enabled. + config BLE_LOG_HOST_SIDE_HCI_LOG_ENABLED + bool + default n + select BLE_LOG_HCI_LOG_ENABLED endif menu "Legacy SPI Log Output (Deprecated - use BT Log Async Output instead)" diff --git a/components/bt/common/ble_log/README.md b/components/bt/common/ble_log/README.md index cb6fbda3288..67c75721077 100644 --- a/components/bt/common/ble_log/README.md +++ b/components/bt/common/ble_log/README.md @@ -34,7 +34,7 @@ callback drains the queue depth captured at entry and schedules another fixed defer only for arrivals left behind, so it does not continuously monopolize the shared ESP Timer task. -## Version 7 frame +## Version 8 frame All multi-byte fields use the target's little-endian representation. @@ -75,7 +75,7 @@ redirection flush) is the alignment reference against the core timeline. ### Sources The public `ble_log_src_t` ABI is frozen and its values are the on-wire -source IDs of protocol v7 frames: +source IDs of protocol v8 frames: ```text 0 INTERNAL @@ -93,12 +93,14 @@ Log sources except INTERNAL and REDIR share one 24-bit Global SN: it is consumed at API entry, so it totally orders log attempts — including equal-timestamp records from different sources — and every lost or rejected attempt leaves a gap in the sequence. Internal Snapshot frames carry their -own separate sequence (a gap counts skipped snapshots), and the REDIR -console stream keeps its own sequence as well (a gap counts a dropped -console batch). `ble_log_init()` resets all three sequences, and its required -`INIT` snapshot starts a new receiver epoch. They remain continuous through -`FLUSH` within that epoch. Callers must not write until `ble_log_init()` -returns, so the `INIT` snapshot is submitted first. +own separate sequence (a gap counts skipped snapshots), the periodic +task-binding broadcast keeps its own sequence as well (a gap counts a +skipped broadcast window), and the REDIR console stream keeps its own +sequence too (a gap counts a dropped console batch). `ble_log_init()` +resets all the sequences, and its required `INIT` snapshot starts a new +receiver epoch. They remain continuous through `FLUSH` within that epoch. +Callers must not write until `ble_log_init()` returns, so the `INIT` +snapshot is submitted first. `CONFIG_BLE_LOG_HCI_LOG_ENABLED` controls HCI logging. Bluedroid and legacy VHCI NimBLE retain Host-side capture and suppress duplicate Controller HCI @@ -111,8 +113,11 @@ bit 7. Disabling HCI logging suppresses records from both capture paths. ## Internal Snapshot -All BLE Log-owned Internal information is emitted as one fixed-layout frame -from one dedicated 148-byte transport. Its logical frame length is 148 bytes. +All BLE Log-owned internal information is emitted as fixed-layout frames +from dedicated transports: the snapshot frame is 148 logical bytes on its +own transport, and the periodic task-binding broadcast has its own +transport sized to the registry (one full binding frame per window; +`CONFIG_BLE_LOG_TASK_ID_MAX` entries of 19 bytes plus a timestamp). The snapshot contains: @@ -155,7 +160,8 @@ void ble_log_deinit(void); bool ble_log_enable(bool enable); void ble_log_flush(void); bool ble_log_write_hex(ble_log_src_t source, const uint8_t *data, size_t len); -uint8_t *ble_log_claim(ble_log_src_t source, size_t maximum, uint32_t *handle); +uint8_t *ble_log_claim(ble_log_src_t source, size_t maximum, + uint32_t *handle, bool wait_for_transport); void ble_log_commit(uint32_t handle, size_t actual_len); void ble_log_write_hex_ll(uint32_t len, const uint8_t *data, uint32_t append_len, const uint8_t *append, @@ -187,7 +193,8 @@ Compression encoders write directly into shared-pool storage: ```c uint32_t handle; -uint8_t *payload = ble_log_claim(BLE_LOG_SRC_ENCODE, maximum, &handle); +uint8_t *payload = ble_log_claim(BLE_LOG_SRC_ENCODE, maximum, + &handle, true); if (payload) { size_t encoded = encode(payload, maximum); ble_log_commit(handle, encoded); @@ -195,15 +202,38 @@ if (payload) { ``` `ble_log_claim()` reserves the hidden frame header, ESP Timer timestamp, and -checksum. Every successful claim must be committed exactly once before +checksum. It waits for a shared transport in yieldable contexts when +`wait_for_transport` is true (writer backpressure); pass false for a lossy +fast path from contexts that must not block, such as the shared ESP Timer +task. Non-yieldable contexts (ISR, critical section) fail fast either way. +Every successful claim must be committed exactly once before `ble_log_deinit()`; `ble_log_commit(handle, 0)` cancels it. Handles include a transport generation so a stale handle cannot commit a later claim in the same lifecycle. Mesh, ISO, Bluedroid, and NimBLE compression no longer allocate three static -payload buffers per channel. Each logical compression source uses a non-blocking -trylock so its task-switch state follows commit order; a contending record is -canceled and counted as lost. +payload buffers per channel. Every ENCODE record names its writer: the byte +after the source is a task id from a name-keyed registry. The registry is a +self-contained module (`ble_log_task_registry.c/h`, structured like the +UART redirection writer: a small append-only table shared by every ENCODE +writer; RAM cost 16 bytes per entry, sized by `CONFIG_BLE_LOG_TASK_ID_MAX`). + +The registry broadcast is module-owned system output: every periodic snapshot +window, one INTERNAL frame (`BLE_LOG_INT_SRC_TASK_BINDING`) packs one +fixed-layout record per registered entry, binding each id to its task name. +It rides the registry's own dedicated transport with a sequence of its own +(a window skipped despite a non-empty registry leaves a gap in the binding +sequence, never in the snapshot sequence), is never counted in the +per-source written/lost stats, and never contends with the snapshot +transport or with user records for pool transports. A receiver that joined +late or lost a frame converges on the next window; a record of a new task +keeps its id and is bound by name at the next window. Announcements are +idempotent on the wire and best effort (a busy binding transport — the +previous broadcast still in DMA — skips a window). +Attribution is a property of the record, so concurrent writers to one source +need no serialization and never drop a record on contention. When the registry +is full, a new task degrades to the unknown id (`0xFF`) and its records are +still emitted. ## Configuration @@ -215,6 +245,7 @@ canceled and counted as lost. | `CONFIG_BLE_LOG_POOL_TRANS_SIZE` | 640 | Bytes per shared transport; SPI builds require a multiple of four | | `CONFIG_BLE_LOG_LL_ENABLED` | target dependent | Controller LL logging | | `CONFIG_BLE_LOG_HCI_LOG_ENABLED` | y | HCI capture from Host or Controller, selected by transport | +| `CONFIG_BLE_LOG_TASK_ID_MAX` | 16 | Task-id registry size, range 2..32; 16 bytes of RAM per entry | | `CONFIG_BLE_LOG_TS_SYNC_TOGGLE_IO_ENABLED` | n | Build the optional analyzer GPIO toggle | | `CONFIG_BLE_LOG_TS_ENABLED` | n | Deprecated compatibility entry selecting the GPIO toggle | @@ -235,7 +266,8 @@ cd ../ble_log_perf_test idf.py build ``` -`ble_log_test` validates golden v7 bytes, the consolidated Internal Snapshot, +`ble_log_test` validates golden v8 bytes, the consolidated Internal Snapshot, source/HCI metadata and capture selection, pool exhaustion and reserve use, -snapshot busy loss, stale claims, and enable/disable/deinit races. `ble_log_rt_test` covers batched -dispatch, timer behavior, inflight statistics, and repeated deinit races. +snapshot busy loss, stale claims, and enable/disable/deinit races. +`ble_log_rt_test` covers batched dispatch, timer behavior, inflight statistics, +and repeated deinit races. diff --git a/components/bt/common/ble_log/extension/log_compression/Kconfig.in b/components/bt/common/ble_log/extension/log_compression/Kconfig.in index 9b2f442a547..4db87fcdc63 100644 --- a/components/bt/common/ble_log/extension/log_compression/Kconfig.in +++ b/components/bt/common/ble_log/extension/log_compression/Kconfig.in @@ -15,6 +15,7 @@ config BLE_COMPRESSED_LOG_ENABLE for installation instructions. if BLE_COMPRESSED_LOG_ENABLE + source "$IDF_PATH/components/bt/common/ble_log/extension/log_compression/profile/mesh/Kconfig.mesh.in" source "$IDF_PATH/components/bt/common/ble_log/extension/log_compression/iso/audio/Kconfig.iso.in" source "$IDF_PATH/components/bt/common/ble_log/extension/log_compression/host/Kconfig.host.in" diff --git a/components/bt/common/ble_log/extension/log_compression/ble_log_compression.c b/components/bt/common/ble_log/extension/log_compression/ble_log_compression.c index 8eaef8661b0..b86b6f3b275 100644 --- a/components/bt/common/ble_log/extension/log_compression/ble_log_compression.c +++ b/components/bt/common/ble_log/extension/log_compression/ble_log_compression.c @@ -9,9 +9,9 @@ // Private includes #include "freertos/FreeRTOS.h" -#include "freertos/task.h" #include "sdkconfig.h" #include "ble_log_lbm_v2.h" +#include "ble_log_task_registry.h" #include "ble_log_util.h" #include "log_compression/utils.h" @@ -39,69 +39,30 @@ _Static_assert(CONFIG_BLE_HOST_COMPRESSED_LOG_BUFFER_LEN <= } \ } while (0) -#if CONFIG_BLE_MESH_COMPRESSED_LOG_ENABLE -char * mesh_last_task_handle = NULL; -static ble_log_atomic_lock_t mesh_source_lock; -#endif - -#if CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE -char * iso_last_task_handle = NULL; -static ble_log_atomic_lock_t iso_source_lock; -#endif - -#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE && CONFIG_BT_BLUEDROID_ENABLED -char * host_last_task_handle = NULL; -static ble_log_atomic_lock_t host_source_lock; -#endif - -#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE && CONFIG_BT_NIMBLE_ENABLED -char * nimble_last_task_handle = NULL; -static ble_log_atomic_lock_t nimble_source_lock; -#endif - -#if CONFIG_BLE_LOG_PRPH_TEST -extern void ble_log_test_compression_after_lock_hook(uint8_t source) -__attribute__((weak)); -#endif - /* The maximum number of supported parameters is 64 */ #define LOG_HEADER(log_type, info) ((log_type << 6) | (info & 0x3f)) int ble_compressed_log_cb_get(uint8_t source, ble_cp_log_buffer_mgmt_t *mgmt) { - char ** last_handle = NULL; - ble_log_atomic_lock_t *source_lock = NULL; uint16_t claim_len = 0; - char * cur_handle = pcTaskGetName(NULL); switch (source) { #if CONFIG_BLE_MESH_COMPRESSED_LOG_ENABLE case BLE_COMPRESSED_LOG_OUT_SOURCE_MESH: case BLE_COMPRESSED_LOG_OUT_SOURCE_MESH_LIB: - last_handle = &mesh_last_task_handle; - source_lock = &mesh_source_lock; claim_len = CONFIG_BLE_MESH_COMPRESSED_LOG_BUFFER_LEN; break; #endif #if CONFIG_BLE_ISO_COMPRESSED_LOG_ENABLE case BLE_COMPRESSED_LOG_OUT_SOURCE_ISO: case BLE_COMPRESSED_LOG_OUT_SOURCE_AUDIO_LIB: - last_handle = &iso_last_task_handle; - source_lock = &iso_source_lock; claim_len = CONFIG_BLE_ISO_COMPRESSED_LOG_BUFFER_LEN; break; #endif #if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE && (CONFIG_BT_BLUEDROID_ENABLED || CONFIG_BT_NIMBLE_ENABLED) case BLE_COMPRESSED_LOG_OUT_SOURCE_HOST: claim_len = CONFIG_BLE_HOST_COMPRESSED_LOG_BUFFER_LEN; -#if CONFIG_BT_BLUEDROID_ENABLED - last_handle = &host_last_task_handle; - source_lock = &host_source_lock; -#elif CONFIG_BT_NIMBLE_ENABLED - last_handle = &nimble_last_task_handle; - source_lock = &nimble_source_lock; -#endif break; #endif default: @@ -111,57 +72,34 @@ int ble_compressed_log_cb_get(uint8_t source, ble_cp_log_buffer_mgmt_t *mgmt) mgmt->len = claim_len; mgmt->idx = 0; - mgmt->source_lock = source_lock; - mgmt->last_task_handle = last_handle; - mgmt->current_task_handle = cur_handle; - mgmt->task_switched = false; - mgmt->buffer = ble_log_claim(BLE_LOG_SRC_ENCODE, claim_len, &mgmt->handle); + mgmt->buffer = ble_log_claim(BLE_LOG_SRC_ENCODE, claim_len, + &mgmt->handle, true); if (!mgmt->buffer) { return -1; } - if (!BLE_LOG_CAS_ACQUIRE(source_lock)) { + /* Resolve the task id only after the claim succeeded: the claim's + * lifetime reference pins the current registry epoch, so the id + * cannot cross an init/deinit boundary (which reassigns ids from 0). + * Resolving before the claim instead left a window where a full + * deinit/reinit completed and the stale id named a different task. */ + uint8_t task_id = ble_log_task_id_current(); + if (ble_log_cp_push_u8(mgmt, source) != 0 || + ble_log_cp_push_u8(mgmt, task_id) != 0) { ble_log_commit(mgmt->handle, 0); return -1; } -#if CONFIG_BLE_LOG_PRPH_TEST - if (ble_log_test_compression_after_lock_hook) { - ble_log_test_compression_after_lock_hook(source); - } -#endif - - if (ble_log_cp_push_u8(mgmt, source) != 0) { - ble_log_commit(mgmt->handle, 0); - BLE_LOG_CAS_RELEASE(source_lock); - return -1; - } - char *previous_handle = __atomic_load_n(last_handle, __ATOMIC_RELAXED); - if (previous_handle == NULL || previous_handle != cur_handle) { - if (ble_log_cp_push_u8( - mgmt, LOG_HEADER(LOG_TYPE_INFO, LOG_TYPE_INFO_TASK_SWITCH)) != 0) { - ble_log_commit(mgmt->handle, 0); - BLE_LOG_CAS_RELEASE(source_lock); - return -1; - } - mgmt->task_switched = true; - } return 0; } static inline int ble_compressed_log_commit(ble_cp_log_buffer_mgmt_t *mgmt) { ble_log_commit(mgmt->handle, mgmt->idx); - if (mgmt->task_switched) { - __atomic_store_n(mgmt->last_task_handle, mgmt->current_task_handle, - __ATOMIC_RELAXED); - } - BLE_LOG_CAS_RELEASE(mgmt->source_lock); return 0; } static inline int ble_compressed_log_abort(ble_cp_log_buffer_mgmt_t *mgmt) { ble_log_commit(mgmt->handle, 0); - BLE_LOG_CAS_RELEASE(mgmt->source_lock); return 0; } @@ -357,8 +295,11 @@ int ble_log_compressed_hex_print_buf(uint8_t source, uint32_t log_index, uint8_t } if (buf == NULL) { - ble_log_cp_push_u8(&mgmt, LOG_HEADER(LOG_TYPE_INFO, LOG_TYPE_INFO_NULL_BUF)); - ble_log_cp_push_u16(&mgmt, log_index); + if (ble_log_cp_push_u8(&mgmt, LOG_HEADER(LOG_TYPE_INFO, LOG_TYPE_INFO_NULL_BUF)) != 0 || + ble_log_cp_push_u16(&mgmt, log_index) != 0) { + ble_compressed_log_abort(&mgmt); + return 0; + } ble_compressed_log_commit(&mgmt); return 0; } diff --git a/components/bt/common/ble_log/extension/log_compression/include/log_compression/utils.h b/components/bt/common/ble_log/extension/log_compression/include/log_compression/utils.h index fe35e793ff3..969e85e9d94 100644 --- a/components/bt/common/ble_log/extension/log_compression/include/log_compression/utils.h +++ b/components/bt/common/ble_log/extension/log_compression/include/log_compression/utils.h @@ -32,23 +32,19 @@ enum { #define LOG_TYPE_ZERO_ARGS 0 #define LOG_TYPE_HEX_ARGS 1 #define LOG_TYPE_HEX_BUF 2 -/* This type of message is used to update log information, - * such as there is currently a new task log */ +/* Informational in-band records (protocol v8). Task-id bindings used to + * be one of these; they are broadcast as INTERNAL frames on the periodic + * snapshot window now (see ble_log_task_registry.h), keeping the ENCODE + * stream user records only. */ #define LOG_TYPE_INFO 3 -#define LOG_TYPE_INFO_TASK_ID_UPDATE 0 #define LOG_TYPE_INFO_NULL_BUF 1 -#define LOG_TYPE_INFO_TASK_SWITCH 2 typedef struct { uint8_t *buffer; /* claim() payload pointer */ uint16_t idx; /* bytes written so far */ uint16_t len; /* claimed capacity (max_len) */ uint32_t handle; /* claim handle for commit/abort */ - volatile uint32_t *source_lock; - char **last_task_handle; - char *current_task_handle; - bool task_switched; } ble_cp_log_buffer_mgmt_t; static inline int ble_log_cp_buffer_safe_check(ble_cp_log_buffer_mgmt_t *pbuf_mgmt, uint16_t write_len) diff --git a/components/bt/common/ble_log/include/ble_log.h b/components/bt/common/ble_log/include/ble_log.h index c5ce103819d..98a7410e7c6 100644 --- a/components/bt/common/ble_log/include/ble_log.h +++ b/components/bt/common/ble_log/include/ble_log.h @@ -21,7 +21,7 @@ * memory requirements, keep it as less as possible; it's recommended to use subcode for more * log data structure decoding */ /* CRITICAL: this enum is a public ABI and must not be reordered or renamed. - * Its values are the base on-wire source IDs of protocol v7 frames. */ + * Its values are the base on-wire source IDs of protocol v8 frames. */ typedef enum { /* Internal */ BLE_LOG_SRC_INTERNAL = 0, @@ -69,8 +69,12 @@ bool ble_log_enable(bool enable); void ble_log_flush(void); /* Waits for a shared transport in yieldable contexts; ISR and critical-section callers fail fast. */ bool ble_log_write_hex(ble_log_src_t src_code, const uint8_t *addr, size_t len); -/* Same backpressure as ble_log_write_hex(): yieldable claims wait for a shared transport. */ -uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, uint32_t *handle); +/* Same backpressure as ble_log_write_hex(): yieldable claims wait for a + * shared transport when wait_for_transport is true; pass false for a lossy + * fast path from contexts that must not block (system periodic output). + * Non-yieldable contexts (ISR, critical section) fail fast either way. */ +uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, + uint32_t *handle, bool wait_for_transport); void ble_log_commit(uint32_t handle, size_t actual_len); void ble_log_dump_to_console(void); #if CONFIG_BLE_LOG_LL_ENABLED diff --git a/components/bt/common/ble_log/src/ble_log_lbm_v2.c b/components/bt/common/ble_log/src/ble_log_lbm_v2.c index ef33c795b64..9ac016d0bf3 100644 --- a/components/bt/common/ble_log/src/ble_log_lbm_v2.c +++ b/components/bt/common/ble_log/src/ble_log_lbm_v2.c @@ -10,6 +10,7 @@ /* INCLUDE */ #include "ble_log.h" #include "ble_log_lbm_v2.h" +#include "ble_log_task_registry.h" #include "ble_log_rt.h" #include "esp_timer.h" @@ -560,7 +561,16 @@ void ble_log_pool_finish_frame(ble_log_prph_trans_t *trans, uint16_t payload_len /* ---------------------------------------------- */ /* CLAIM / COMMIT INTERFACE (holds lock) */ /* ---------------------------------------------- */ -uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, uint32_t *handle) + +/* Claim with a caller-chosen wait policy. wait_for_transport=true applies + * backpressure in a yieldable context (the writer waits for a shared + * transport instead of dropping); false is a lossy fast path that returns + * NULL on a busy pool, for callers that must never block (e.g. system + * periodic output on the shared ESP timer task). Non-yieldable contexts + * (ISR, scheduler suspended) never wait whatever the policy: they keep + * their dedicated reserve and fail fast on contention. */ +uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, + uint32_t *handle, bool wait_for_transport) { if (!handle || src_code != BLE_LOG_SRC_ENCODE || max_len == 0 || max_len > UINT16_MAX || BLE_LOG_IN_ISR()) { @@ -581,8 +591,12 @@ uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, uint32_t *handle) bool non_yield = !xPortCanYield() || xTaskGetSchedulerState() != taskSCHEDULER_RUNNING; size_t payload_capacity = sizeof(timestamp) + max_len; - ble_log_prph_trans_t *trans = - ble_log_pool_acquire(payload_capacity, non_yield, !non_yield); + /* Non-yieldable contexts never wait, whatever the requested policy: + * blocking there would suspend the only context that can free the + * transport. wait_for_transport=false is the lossy fast path for + * yieldable contexts that must not block. */ + ble_log_prph_trans_t *trans = ble_log_pool_acquire( + payload_capacity, non_yield, wait_for_transport && !non_yield); if (!trans) { ble_log_stat_mgr_mark_lost(src_code); BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); @@ -706,6 +720,11 @@ bool ble_log_lbm_init(void) BLE_LOG_MEMSET(stat_mgr_ctx, 0, sizeof(stat_mgr_ctx)); g_frame_sn = 0; g_snapshot_sn = 0; + /* Task registry: fresh epoch (registry, sequence and its dedicated + * transport) for the new receiver epoch. */ + if (!ble_log_task_registry_init()) { + goto exit; + } BLE_LOG_MEMSET(&internal_snapshot, 0, sizeof(internal_snapshot)); internal_snapshot.int_src_code = BLE_LOG_INT_SRC_SNAPSHOT; internal_snapshot.pool.trans_cnt = BLE_LOG_POOL_TRANS_CNT; @@ -729,6 +748,11 @@ void ble_log_lbm_begin_deinit(void) BLE_LOG_ATOMIC_STORE_SEQ_CST(lbm_inited, false); BLE_LOG_EXIT_CRITICAL(); + /* Close the task-registry gate with the LBM gate: its broadcast is + * system output, so only teardown stops it. The publish path never + * blocks, so this drain is immediate. */ + ble_log_task_registry_begin_deinit(); + /* Wake any blocked task writers and wait until BOTH the reference count * and the waiting-task count drain to zero. Blocked writers hold no * reference while parked, so waiting on ref_count alone could free the @@ -763,6 +787,7 @@ void ble_log_lbm_deinit(void) { ble_log_lbm_begin_deinit(); + ble_log_task_registry_deinit(); ble_log_prph_trans_deinit(&internal_trans); for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { ble_log_prph_trans_deinit(&(g_pool.trans[id])); diff --git a/components/bt/common/ble_log/src/ble_log_rt.c b/components/bt/common/ble_log/src/ble_log_rt.c index 5c24513ccda..9d3d8ea452d 100644 --- a/components/bt/common/ble_log/src/ble_log_rt.c +++ b/components/bt/common/ble_log/src/ble_log_rt.c @@ -12,6 +12,7 @@ #include "ble_log.h" #include "ble_log_rt.h" #include "ble_log_lbm_v2.h" +#include "ble_log_task_registry.h" #include "ble_log_util.h" #include "esp_log.h" @@ -158,8 +159,12 @@ BLE_LOG_STATIC void ble_log_rt_ts_trigger(void *arg) /* Unified periodic output: best-effort flush of partially-filled OPEN * transports ahead of the periodic snapshot, so parked frames do not - * wait for the next capacity seal. */ + * wait for the next capacity seal. The task-id binding broadcast + * rides the same window, ahead of the snapshot, on its own dedicated + * transport: a receiver that missed a binding converges on the next + * one, and the snapshot never waits behind it. */ ble_log_lbm_flush_open_trans(); + ble_log_task_bindings_publish(); (void)ble_log_internal_snapshot( BLE_LOG_SNAPSHOT_REASON_PERIODIC | diff --git a/components/bt/common/ble_log/src/ble_log_task_registry.c b/components/bt/common/ble_log/src/ble_log_task_registry.c new file mode 100644 index 00000000000..f9e8c92a82d --- /dev/null +++ b/components/bt/common/ble_log/src/ble_log_task_registry.c @@ -0,0 +1,272 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* ------------------------------------ */ +/* BLE Log - Task-id Registry */ +/* */ +/* Owns the name-to-id registry, its */ +/* broadcast sequence and its dedicated */ +/* transport; a separate .c/.h pair */ +/* like the UART redirection writer. */ +/* ------------------------------------ */ + +/* INCLUDE */ +#include "ble_log_task_registry.h" +#include "ble_log_rt.h" + +#include "freertos/task.h" +#include "esp_timer.h" + +/* MACRO */ +/* Registry entries are word-compared on the record path. */ +#define BLE_LOG_TASK_NAME_WORDS (BLE_LOG_TASK_NAME_LEN / sizeof(uint32_t)) +/* ble_log_task_lookup() and ble_log_task_register() compare the four name + * words literally; a different name width must rework them. */ +_Static_assert(BLE_LOG_TASK_NAME_WORDS == 4, + "Unexpected BLE Log task name word count"); + +/* VARIABLE */ +/* Plain static variables (NOBITS .bss): the registry has no IRAM path, and zero + * page-image bytes beat DRAM_ATTR placement for a table this size. */ +BLE_LOG_STATIC uint32_t s_task_names[CONFIG_BLE_LOG_TASK_ID_MAX][BLE_LOG_TASK_NAME_WORDS]; +BLE_LOG_STATIC volatile uint8_t s_task_cnt; +BLE_LOG_STATIC ble_log_atomic_lock_t s_task_reg_lock; + +/* The binding broadcast keeps its own 24-bit frame sequence, separate + * from the snapshot sequence (see ble_log_lbm_v2.h): a gap counts a + * broadcast window skipped despite a non-empty registry, never a lost + * snapshot. ble_log_task_registry_init resets it. */ +BLE_LOG_STATIC uint32_t g_task_binding_sn; +#define BLE_LOG_GET_TASK_BINDING_SN() BLE_LOG_GET_FRAME_SN(g_task_binding_sn) + +/* Module lifetime gate, closed by begin_deinit (LBM teardown starts): + * publish is system output beyond the public producer gate, and only + * module teardown stops new broadcasts. The publish path never blocks, + * so the begin_deinit drain is immediate. */ +BLE_LOG_STATIC volatile uint32_t s_registry_ref_count = 0; +BLE_LOG_STATIC uint32_t s_registry_inited = 0; +/* The dedicated single-frame transport. It shares the INTERNAL owner + * kind: recycled by the peripheral tx-done straight back to FREE, with + * no pool bookkeeping. */ +BLE_LOG_STATIC ble_log_prph_trans_t *binding_trans; + +/* ------------------------------- */ +/* Registry Lookup/Insert */ +/* ------------------------------- */ +/* - Entries are append-only: an inserter (holding s_task_reg_lock) + * writes all name words first and publishes the entry by storing the + * new count with RELEASE; readers ACQUIRE-load the count and scan + * only the entries below it, so the read path is lock-free. + * - Names are matched by content, not by TCB pointer, so a recycled + * TCB cannot alias two different tasks onto one id. + * - Names longer than the storage are truncated; tasks sharing the + * truncated prefix share an id (identical names are indistinguishable + * in the decoded log anyway). + * - A full registry, or a contended registration attempt, degrades that + * record to the unknown id (BLE_LOG_TASK_ID_UNKNOWN) and still emits + * it; the task retries the registration with its next record. + * - Bindings are broadcast on every periodic snapshot window, so a + * receiver that joined late or lost a frame converges on the next + * window; broadcasts are idempotent on the wire and a lost one + * self-heals on the next window. A task that starts logging between + * windows emits records tagged with its id; the receiver binds the id + * at the next window. */ + +/* Normalizes a task name to NUL-padded words. Word-level compare + * replaces strcmp on the record path: the first word acts as the hash, + * a miss costs one load per entry, and there is no call overhead. Inline: + * every ENCODE record resolves its writer id, so this runs per record. */ +BLE_LOG_STATIC inline void ble_log_task_name_to_words( + const char *name, uint32_t w[BLE_LOG_TASK_NAME_WORDS]) +{ + BLE_LOG_MEMSET(w, 0, BLE_LOG_TASK_NAME_LEN); + BLE_LOG_MEMCPY(w, name, strnlen(name, BLE_LOG_TASK_NAME_LEN)); +} + +/* Per-record lookup: inline for the same reason as the normalizer above. */ +BLE_LOG_STATIC inline uint8_t ble_log_task_lookup( + const uint32_t w[BLE_LOG_TASK_NAME_WORDS]) +{ + uint8_t cnt = BLE_LOG_ATOMIC_LOAD_ACQUIRE(s_task_cnt); + for (uint8_t i = 0; i < cnt; i++) { + if (s_task_names[i][0] != w[0]) { + continue; + } + if (w[1] == s_task_names[i][1] && w[2] == s_task_names[i][2] && + w[3] == s_task_names[i][3]) { + return i; + } + } + return BLE_LOG_TASK_ID_UNKNOWN; +} + +/* Cold path, once per task lifetime. A single CAS attempt serializes + * inserters: on contention this record degrades to the unknown id + * instead of blocking, and the next record of the task retries. */ +BLE_LOG_STATIC uint8_t ble_log_task_register(const uint32_t w[BLE_LOG_TASK_NAME_WORDS]) +{ + uint8_t id = BLE_LOG_TASK_ID_UNKNOWN; + + if (!BLE_LOG_CAS_ACQUIRE(&s_task_reg_lock)) { + return BLE_LOG_TASK_ID_UNKNOWN; + } + uint8_t cnt = s_task_cnt; + for (uint8_t i = 0; i < cnt; i++) { + if (w[0] == s_task_names[i][0] && w[1] == s_task_names[i][1] && + w[2] == s_task_names[i][2] && w[3] == s_task_names[i][3]) { + BLE_LOG_CAS_RELEASE(&s_task_reg_lock); + return i; /* raced with another first lookup */ + } + } + if (cnt < CONFIG_BLE_LOG_TASK_ID_MAX) { + BLE_LOG_MEMCPY(s_task_names[cnt], w, BLE_LOG_TASK_NAME_LEN); + id = cnt; + BLE_LOG_ATOMIC_STORE_RELEASE(s_task_cnt, cnt + 1); + } + BLE_LOG_CAS_RELEASE(&s_task_reg_lock); + return id; +} + +uint8_t ble_log_task_id_current(void) +{ + const char *cur_name = pcTaskGetName(NULL); + if (!cur_name) { + return BLE_LOG_TASK_ID_UNKNOWN; + } + uint32_t w[BLE_LOG_TASK_NAME_WORDS]; + ble_log_task_name_to_words(cur_name, w); + uint8_t id = ble_log_task_lookup(w); + /* Registration mutates shared state; ISR callers stop at the + * lookup-hit path and read only. */ + if (id == BLE_LOG_TASK_ID_UNKNOWN && !BLE_LOG_IN_ISR()) { + id = ble_log_task_register(w); + } + return id; +} + +/* ----------------------------------- */ +/* Periodic Binding Broadcast */ +/* ----------------------------------- */ +/* One binding frame on the dedicated registry transport, packing every + * registered entry in id order. Best effort, single CAS attempt: a busy + * transport (the previous broadcast still in DMA) skips this window and + * the next one rebroadcasts. The registry is only read here (ACQUIRE + * count, append-only entries), so this races no inserter. */ +BLE_LOG_STATIC bool ble_log_task_binding_send(uint8_t cnt, uint32_t timestamp) +{ + /* Snapshot-sequence semantics: the window SN is consumed before + * contending for the transport, so a skipped window burns it and + * the gap in the binding sequence counts skipped windows. */ + uint32_t frame_sn = BLE_LOG_GET_TASK_BINDING_SN(); + if (!BLE_LOG_CAS_ACQUIRE(&binding_trans->atomic_lock)) { + return false; + } + if (BLE_LOG_ATOMIC_LOAD_RELAXED(binding_trans->state) != + BLE_LOG_TRANS_STATE_FREE) { + BLE_LOG_CAS_RELEASE(&binding_trans->atomic_lock); + return false; + } + + ble_log_frame_head_t frame_head = { + .length = (uint16_t)(sizeof(uint32_t) + + cnt * sizeof(ble_log_task_binding_t)), + .frame_meta = BLE_LOG_MAKE_FRAME_META(BLE_LOG_SRC_INTERNAL, frame_sn), + }; + uint8_t *buf = binding_trans->buf; + BLE_LOG_MEMCPY(buf, &frame_head, sizeof(frame_head)); + size_t pos = BLE_LOG_FRAME_HEAD_LEN; + BLE_LOG_MEMCPY(buf + pos, ×tamp, sizeof(timestamp)); + pos += sizeof(timestamp); + for (uint8_t i = 0; i < cnt; i++) { + ble_log_task_binding_t binding = { + .int_src_code = BLE_LOG_INT_SRC_TASK_BINDING, + .task_id = i, + }; + BLE_LOG_MEMCPY(binding.task_name, s_task_names[i], + BLE_LOG_TASK_NAME_LEN); + BLE_LOG_MEMCPY(buf + pos, &binding, sizeof(binding)); + pos += sizeof(binding); + } + uint32_t checksum = ble_log_fast_checksum( + buf, BLE_LOG_FRAME_HEAD_LEN + frame_head.length); + BLE_LOG_MEMCPY(buf + BLE_LOG_FRAME_HEAD_LEN + frame_head.length, + &checksum, sizeof(checksum)); + + binding_trans->pos = (uint16_t)(pos + BLE_LOG_FRAME_TAIL_LEN); + BLE_LOG_ATOMIC_STORE_RELAXED(binding_trans->state, + BLE_LOG_TRANS_STATE_SENDING); + BLE_LOG_CAS_RELEASE(&binding_trans->atomic_lock); + ble_log_rt_submit_trans(binding_trans); + return true; +} + +void ble_log_task_bindings_publish(void) +{ + if (!ble_log_ref_count_try_acquire(&s_registry_ref_count, &s_registry_inited)) { + return; + } + /* Empty registry: nothing to broadcast; no window attempt, no SN. */ + uint8_t cnt = BLE_LOG_ATOMIC_LOAD_ACQUIRE(s_task_cnt); + if (cnt != 0) { + uint32_t timestamp = (uint32_t)esp_timer_get_time(); + (void)ble_log_task_binding_send(cnt, timestamp); + } + BLE_LOG_REF_COUNT_RELEASE(&s_registry_ref_count); +} + +/* --------------------------- */ +/* Module Lifetime */ +/* --------------------------- */ +bool ble_log_task_registry_init(void) +{ + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(s_registry_inited)) { + return true; + } + + /* Fresh epoch: empty registry, no broadcast sequence. init runs + * single-threaded (from ble_log_lbm_init), so the registry state and + * its lock are cleared by value. */ + BLE_LOG_MEMSET(s_task_names, 0, sizeof(s_task_names)); + BLE_LOG_ATOMIC_STORE_RELEASE(s_task_cnt, 0); + s_task_reg_lock = 0; + g_task_binding_sn = 0; + + if (!ble_log_prph_trans_init(&binding_trans, BLE_LOG_TASK_BINDING_TRANS_SIZE)) { + return false; + } + binding_trans->id = BLE_LOG_TRANS_ID_NONE; + binding_trans->owner_kind = BLE_LOG_TRANS_OWNER_INTERNAL; + + BLE_LOG_ATOMIC_STORE_RELEASE(s_registry_inited, true); + return true; +} + +void ble_log_task_registry_begin_deinit(void) +{ + /* Closing gate: pairs with the seq_cst acquire in publish. */ + BLE_LOG_ATOMIC_STORE_SEQ_CST(s_registry_inited, false); + while (!ble_log_ref_count_wait(&s_registry_ref_count, 0)) { + BLE_LOG_ASSERT(false); + } +} + +void ble_log_task_registry_deinit(void) +{ + ble_log_task_registry_begin_deinit(); + ble_log_prph_trans_deinit(&binding_trans); +} + +#if CONFIG_BLE_LOG_PRPH_TEST +/* Test-only: wipes the task registry so test cases stay order-independent. + * Call between cases, with no writers in flight. */ +void ble_log_test_task_registry_reset(void) +{ + if (BLE_LOG_CAS_ACQUIRE(&s_task_reg_lock)) { + BLE_LOG_ATOMIC_STORE_RELEASE(s_task_cnt, 0); + BLE_LOG_CAS_RELEASE(&s_task_reg_lock); + } +} +#endif diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h index a61f337e566..0a057a0b93e 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h @@ -7,7 +7,7 @@ #define __BLE_LOG_LBM_V2_H__ /* -------------------------------------------------- */ -/* BLE Log - Unified Transport Pool (v7) */ +/* BLE Log - Unified Transport Pool (v8) */ /* -------------------------------------------------- */ /* Replaces the legacy multi-LBM design (ble_log_lbm.h) with one shared * transport pool. Written as a new module so the legacy implementation @@ -44,6 +44,8 @@ typedef struct { #define BLE_LOG_TRANS_SIZE BLE_LOG_POOL_TRANS_SIZE #define BLE_LOG_MAX_PAYLOAD_LEN (BLE_LOG_POOL_TRANS_SIZE - BLE_LOG_FRAME_OVERHEAD) #define BLE_LOG_TRANS_INTERNAL_CNT (1) +/* Dedicated transport of the task-id registry binding broadcast. */ +#define BLE_LOG_TRANS_TASK_BINDING_CNT (1) #if BLE_LOG_UART_REDIR_ENABLED #define BLE_LOG_TRANS_REDIR_CNT BLE_LOG_TRANS_BUF_CNT @@ -52,10 +54,11 @@ typedef struct { #endif #define BLE_LOG_TRANS_TOTAL_CNT \ - (BLE_LOG_POOL_TRANS_CNT + BLE_LOG_TRANS_INTERNAL_CNT + BLE_LOG_TRANS_REDIR_CNT) + (BLE_LOG_POOL_TRANS_CNT + BLE_LOG_TRANS_INTERNAL_CNT + \ + BLE_LOG_TRANS_TASK_BINDING_CNT + BLE_LOG_TRANS_REDIR_CNT) /* --------------------------------------- */ -/* Protocol v7 Source ID Space */ +/* Protocol v8 Source ID Space */ /* --------------------------------------- */ /* The frozen public ble_log_src_t values are the on-wire and statistic * source IDs: the frame source byte carries the bare enum value. */ @@ -105,9 +108,10 @@ typedef struct { * REDIR: consumed at API entry (before pool contention), it orders all * log attempts — including equal-timestamp records from different * sources — and every lost or rejected attempt leaves a gap. INTERNAL - * snapshot frames and the REDIR console stream keep their own separate - * sequences (a gap counts a skipped snapshot or a dropped console batch). - * ble_log_init() resets all three sequences; its required INIT snapshot + * snapshot frames, the periodic task-binding broadcast, and the REDIR + * console stream keep their own separate sequences (a gap counts a + * skipped snapshot, a skipped broadcast window, or a dropped console + * batch). ble_log_init() resets all of them; its required INIT snapshot * starts a new receiver epoch. They stay continuous through FLUSH within * that epoch. The per-counter macros live next to their counters in the * owning translation units; the 24-bit wire field is enforced where the @@ -235,10 +239,18 @@ bool ble_log_internal_snapshot(uint16_t reason_flags, const ble_log_ts_info_t *ts_info, bool wait_for_transport); +/* The task-id registry (protocol v8 attribution) lives in its own module: + * ble_log_task_registry.c/h, like the UART redirection writer. */ + /* Claim/commit: the public ble_log_src_t is accepted for API stability, but * only BLE_LOG_SRC_ENCODE is supported; its frames are stamped with the - * ENCODE source ID on the wire. */ -uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, uint32_t *handle); + * ENCODE source ID on the wire. wait_for_transport=false turns a busy pool + * into a lossy fast path (NULL, as in a non-yieldable context) instead of + * waiting, for output that must not block its caller (e.g. the shared ESP + * timer task); true waits in yieldable contexts, never in non-yieldable + * ones. */ +uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, + uint32_t *handle, bool wait_for_transport); void ble_log_commit(uint32_t handle, size_t actual_len); #if BLE_LOG_UART_REDIR_ENABLED diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_task_registry.h b/components/bt/common/ble_log/src/internal_include/ble_log_task_registry.h new file mode 100644 index 00000000000..8c08f1612bd --- /dev/null +++ b/components/bt/common/ble_log/src/internal_include/ble_log_task_registry.h @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef __BLE_LOG_TASK_REGISTRY_H__ +#define __BLE_LOG_TASK_REGISTRY_H__ + +/* ------------------------------------ */ +/* BLE Log - Task-id Registry */ +/* ------------------------------------ */ +/* Self-contained module (one .c/.h pair, like the UART redirection + * writer): owns the name-to-id registry, its broadcast sequence and its + * dedicated transport, so it never contends with the internal snapshot + * transport or the pool. */ + +/* INCLUDE */ +#include "ble_log_lbm_v2.h" + +/* ------------------------- */ +/* Wire Format (v8) */ +/* ------------------------- */ +/* Writer attribution: every ENCODE record names the task that formatted + * it (one byte after the record source); the registry maps task names to + * stable ids. The name width is a wire property of the binding record + * below. */ +#define BLE_LOG_TASK_ID_UNKNOWN UINT8_C(0xff) +#define BLE_LOG_TASK_NAME_LEN (16U) + +/* The binding record inside an INTERNAL frame: every periodic window + * broadcasts one frame that packs one record per registered entry, in id + * order. The name is NUL-padded to the registry width plus one byte, so + * a decoder can bind the id in place even when the task name fills the + * whole field. */ +typedef struct { + uint8_t int_src_code; + uint8_t task_id; + uint8_t task_name[BLE_LOG_TASK_NAME_LEN + 1]; +} __attribute__((packed)) ble_log_task_binding_t; + +/* One binding frame must fit the dedicated transport in a single frame: + * one submit per window, and the transport is recycled only when the + * peripheral has read it. */ +#define BLE_LOG_TASK_BINDING_FRAME_LEN \ + (BLE_LOG_FRAME_OVERHEAD + sizeof(uint32_t) + \ + CONFIG_BLE_LOG_TASK_ID_MAX * sizeof(ble_log_task_binding_t)) +/* Word-aligned by construction (SPI builds require aligned transfers). */ +#define BLE_LOG_TASK_BINDING_TRANS_SIZE \ + ((BLE_LOG_TASK_BINDING_FRAME_LEN + 3U) & ~3U) + +_Static_assert(sizeof(ble_log_task_binding_t) == 19, + "Unexpected task-binding record layout"); + +/* --------------------------- */ +/* Internal Interfaces */ +/* --------------------------- */ +/* Module lifetime, driven by the LBM layer (ble_log_lbm_init/deinit): + * init allocates the dedicated transport and wipes the registry and its + * sequence for a fresh epoch; begin_deinit closes the gate and waits for + * in-flight publishes; deinit frees the transport. */ +bool ble_log_task_registry_init(void); +void ble_log_task_registry_begin_deinit(void); +void ble_log_task_registry_deinit(void); + +/* Resolves the calling task's name to its stable registry id, registering + * the name on a miss. ISR-safe: an ISR caller reads only (a miss returns + * the unknown id). Every ENCODE record must resolve before its claim so + * the registry already holds the id the record carries. */ +uint8_t ble_log_task_id_current(void); + +/* Periodic window output: broadcasts one INTERNAL frame packing one + * fixed-layout binding record per registered entry, in id order, on the + * dedicated registry transport. Runs on the shared ESP timer task and + * must never block: a busy transport (previous broadcast still in DMA) + * skips this window and the next one rebroadcasts. Best-effort control + * traffic, never counted in the per-source written/lost stats, and + * carried on its own sequence (see ble_log_task_registry.c) so a skipped + * window cannot look like a lost snapshot. */ +void ble_log_task_bindings_publish(void); + +#if CONFIG_BLE_LOG_PRPH_TEST +/* Test-only: wipes the task registry so test cases stay order-independent. + * Call between cases, with no writers in flight. */ +void ble_log_test_task_registry_reset(void); +#endif + +#endif /* __BLE_LOG_TASK_REGISTRY_H__ */ diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_util.h b/components/bt/common/ble_log/src/internal_include/ble_log_util.h index bc631d48a8a..5e9b2eeca67 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_util.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_util.h @@ -85,7 +85,7 @@ extern void esp_panic_handler_feed_wdts(void); #define BLE_LOG_CAS_RELEASE(cas_lock) \ __atomic_store_n((cas_lock), 0, __ATOMIC_RELEASE) -#define BLE_LOG_VERSION (7) +#define BLE_LOG_VERSION (8) #define BLE_LOG_IDF_COMMIT_LEN (12) /* Lib commit hashes are at most 10 hex chars; zero-padded when shorter */ #define BLE_LOG_LIB_COMMIT_LEN (10) @@ -101,6 +101,9 @@ typedef enum { BLE_LOG_INT_SRC_FINAL_STAT, BLE_LOG_INT_SRC_VERSION_INFO, BLE_LOG_INT_SRC_SNAPSHOT, + /* protocol v8: periodic task-id binding broadcast; its frames carry a + * sequence of their own (a gap counts a skipped broadcast window). */ + BLE_LOG_INT_SRC_TASK_BINDING, BLE_LOG_INT_SRC_MAX, } ble_log_int_src_t; diff --git a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c index fdf0ee0180e..95d02496988 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c +++ b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c @@ -1031,6 +1031,88 @@ TEST_CASE("BLE Log write_hex_ll append cycles (32+32B)", "[ble_log][perf][cycle] #endif #if CONFIG_BLE_COMPRESSED_LOG_ENABLE +/* ---------------------- */ +/* Attribution cycles */ +/* ---------------------- */ + +/* Compressed records attribute every record to their writer task by + * normalizing the FreeRTOS task name to NUL-padded words and scanning + * the registry. A hand-rolled copy (no libc calls) was measured on + * ESP32-H2 at -Og against this libc version: the two trade places + * across name lengths (inline wins ~31 cycles on 3-char names, loses + * ~31 on full 16-char names, ties in between) against ~1.6k cycles per + * compressed record - no variant is meaningfully faster. The module + * keeps the libc version because it is three lines; this case records + * the numbers so the decision does not get re-litigated blind. */ +#define NORM_WORDS 4 /* BLE_CP_TASK_NAME_WORDS, private to the module */ +#define NORM_LEN (NORM_WORDS * sizeof(uint32_t)) +#define NORM_ROUNDS 5 +#define NORM_ITERS 20000 + +typedef void (*norm_fn_t)(const char *name, uint32_t w[NORM_WORDS]); + +static void norm_libc(const char *name, uint32_t w[NORM_WORDS]) +{ + memset(w, 0, NORM_LEN); + memcpy(w, name, strnlen(name, NORM_LEN)); +} + +static void norm_inline(const char *name, uint32_t w[NORM_WORDS]) +{ + for (unsigned i = 0; i < NORM_WORDS; i++) { + uint32_t v = 0; + for (unsigned b = 0; b < sizeof(uint32_t); b++) { + uint8_t c = (uint8_t)*name; + if (c == '\0') { + break; + } + v |= (uint32_t)c << (8 * b); + name++; + } + w[i] = v; + } +} + +static volatile uint32_t s_norm_sink; + +static uint32_t norm_bench_once(norm_fn_t fn, const char *name) +{ + uint32_t sink = 0; + uint32_t start = esp_cpu_get_cycle_count(); + for (uint32_t i = 0; i < NORM_ITERS; i++) { + uint32_t w[NORM_WORDS]; + fn(name, w); + sink ^= w[0] ^ w[1] ^ w[2] ^ w[3]; + } + s_norm_sink = sink; /* the copies must stay live */ + return (esp_cpu_get_cycle_count() - start) / NORM_ITERS; +} + +TEST_CASE("BLE Log task-name normalization cycles", "[ble_log][perf][cycle][ignore]") +{ + static const char *const names[] = {"BTU", "NIMBLE_HOST", "0123456789ABCDEF"}; + for (size_t i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + /* both variants must agree on every shape before timing them */ + uint32_t wa[NORM_WORDS], wb[NORM_WORDS]; + norm_libc(names[i], wa); + norm_inline(names[i], wb); + TEST_ASSERT_EQUAL_UINT32_ARRAY(wa, wb, NORM_WORDS); + + uint32_t best_libc = UINT32_MAX; + uint32_t best_inline = UINT32_MAX; + for (int r = 0; r < NORM_ROUNDS; r++) { /* interleaved rounds */ + uint32_t a = norm_bench_once(norm_libc, names[i]); + uint32_t b = norm_bench_once(norm_inline, names[i]); + best_libc = a < best_libc ? a : best_libc; + best_inline = b < best_inline ? b : best_inline; + } + printf("BLE_LOG_PERF norm name=%-16s libc=%4" PRIu32 " inline=%4" PRIu32 + " delta=%+" PRId32 " cycles/call\n", + names[i], best_libc, best_inline, + (int32_t)best_libc - (int32_t)best_inline); + } +} + /* Compressed records are log_index + 0..2 U32 args, not a raw payload * length. One case per arg count splits encode vs downstream write_hex * cost for each workload shape. */ diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/README.md b/components/bt/common/ble_log/test_apps/ble_log_test/README.md index 6649298b486..13923c713d7 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/README.md +++ b/components/bt/common/ble_log/test_apps/ble_log_test/README.md @@ -12,7 +12,7 @@ validate the BLE Log transport on target. It covers: -- literal protocol-v7 framing and fixed Internal Snapshot ABI; +- literal protocol-v8 framing and fixed Internal Snapshot ABI; - build, library, chip, and protocol versions inside the snapshot; - task and critical-section writes plus HCI direction encoding; - direct compression claim/commit, stale handles, and per-source serialization; diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c index dc6cc952e39..3ca6ae4ecd5 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "esp_chip_info.h" @@ -19,6 +20,7 @@ #include "ble_log_lbm_v2.h" #include "ble_log_prph_test.h" #include "ble_log_rt.h" +#include "ble_log_task_registry.h" #include "test_ble_log_main.h" #if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE #include "log_compression/utils.h" @@ -64,11 +66,6 @@ static SemaphoreHandle_t s_enable_hook_continue; static volatile bool s_disable_hook_armed; static SemaphoreHandle_t s_disable_hook_entered; static SemaphoreHandle_t s_disable_hook_continue; -#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE -static volatile bool s_compression_hook_armed; -static SemaphoreHandle_t s_compression_hook_entered; -static SemaphoreHandle_t s_compression_hook_continue; -#endif void ble_log_test_claim_pre_publish_hook(void); void ble_log_test_claim_locked_hook(void); @@ -76,7 +73,6 @@ void ble_log_test_init_snapshot_before_acquire_hook(void); void ble_log_test_enable_before_lifecycle_lock_hook(void); void ble_log_test_disable_before_wake_hook(void); #if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE -void ble_log_test_compression_after_lock_hook(uint8_t source); extern int ble_log_compressed_hex_print(uint8_t source, uint32_t log_index, size_t args_cnt, ...); #endif @@ -125,17 +121,6 @@ void ble_log_test_disable_before_wake_hook(void) } } -#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE -void ble_log_test_compression_after_lock_hook(uint8_t source) -{ - if (s_compression_hook_armed && - source == BLE_COMPRESSED_LOG_OUT_SOURCE_HOST) { - xSemaphoreGive(s_compression_hook_entered); - xSemaphoreTake(s_compression_hook_continue, portMAX_DELAY); - } -} -#endif - /* A commit field is hex characters, zero-padded after a shorter value; * anything else (garbage, non-hex, zeros after data) is invalid. */ static bool commit_is_valid(const uint8_t *commit, size_t len) @@ -239,7 +224,7 @@ static void capture_golden_frame(const test_ble_log_frame_t *frame, void *ctx) capture->count++; } -TEST_CASE("BLE Log v7 framing matches golden bytes", "[ble_log][wire]") +TEST_CASE("BLE Log v8 framing matches golden bytes", "[ble_log][wire]") { static const uint8_t golden_frame[] = { 0x05, 0x00, 0x07, 0xde, 0xc0, 0x00, @@ -250,7 +235,7 @@ TEST_CASE("BLE Log v7 framing matches golden bytes", "[ble_log][wire]") 0x78, 0x56, 0x34, 0x12, 0xab, }; - TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_VERSION); + TEST_ASSERT_EQUAL_UINT8(8, BLE_LOG_VERSION); TEST_ASSERT_EQUAL_UINT8(1, BLE_LOG_SRC_CORE_FIRST); TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_SRC_CORE_COUNT); TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_SRC_ENCODE); @@ -495,7 +480,7 @@ TEST_CASE("BLE Log writes from critical sections and commits claimed payload", #endif uint32_t handle; - uint8_t *claimed = ble_log_claim(BLE_LOG_SRC_ENCODE, 8, &handle); + uint8_t *claimed = ble_log_claim(BLE_LOG_SRC_ENCODE, 8, &handle, true); TEST_ASSERT_NOT_NULL(claimed); claimed[0] = 0x33; ble_log_commit(handle, 1); @@ -506,7 +491,7 @@ TEST_CASE("BLE Log writes from critical sections and commits claimed payload", s_stale_claim_handle = handle; s_claim_hook_armed = true; uint32_t fresh_handle; - claimed = ble_log_claim(BLE_LOG_SRC_ENCODE, 8, &fresh_handle); + claimed = ble_log_claim(BLE_LOG_SRC_ENCODE, 8, &fresh_handle, true); s_claim_hook_armed = false; TEST_ASSERT_NOT_NULL(claimed); TEST_ASSERT_NOT_EQUAL(handle, fresh_handle); @@ -554,135 +539,184 @@ TEST_CASE("BLE Log writes from critical sections and commits claimed payload", } #if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE -#define TEST_CP_INDEX_FIRST UINT16_C(0x601) -#define TEST_CP_INDEX_DROPPED UINT16_C(0x602) -#define TEST_CP_INDEX_SECOND UINT16_C(0x603) -#define TEST_CP_TASK_SWITCH UINT8_C(0xc2) -#define TEST_CP_ZERO_ARGS UINT8_C(0x40) +#define TEST_CP_TASK_UNKNOWN UINT8_C(0xff) +#define TEST_CP_ZERO_ARGS UINT8_C(0x40) +#define TEST_CP_WRITER_RECORDS 8 +#define TEST_CP_INDEX_MAIN_A UINT16_C(0x600) +#define TEST_CP_INDEX_MAIN_B UINT16_C(0x601) +#define TEST_CP_INDEX_BASE_A UINT16_C(0x700) +#define TEST_CP_INDEX_BASE_B UINT16_C(0x710) +#define TEST_CP_INDEX_FILL_A UINT16_C(0x720) +#define TEST_CP_INDEX_FILL_B UINT16_C(0x721) +#define TEST_CP_INDEX_FILLER UINT16_C(0x722) +#define TEST_CP_NAME_MAX 17 typedef struct { - bool first_found; - uint32_t first_sn; - bool dropped_found; - bool second_found; - bool second_switched; - uint32_t second_sn; - bool third_found; - bool third_switched; - uint32_t third_sn; + /* id-to-name bindings learned from INTERNAL task-binding records */ + char names[CONFIG_BLE_LOG_TASK_ID_MAX][TEST_CP_NAME_MAX]; + uint8_t announce_cnt[CONFIG_BLE_LOG_TASK_ID_MAX]; + /* per-writer record accounting */ + uint8_t main_records; + uint8_t a_records; + uint8_t b_records; + uint8_t filler_records; + uint8_t fill_a_records; + uint8_t fill_b_records; + bool a_id_set; + bool a_id_mismatch; + uint8_t a_id; + bool b_id_set; + bool b_id_mismatch; + uint8_t b_id; + bool main_id_set; + bool main_id_mismatch; + uint8_t main_id; + bool filler_unknown; + bool fill_a_id_set; + uint8_t fill_a_id; + bool fill_b_unknown; } compression_capture_t; typedef struct { - SemaphoreHandle_t committed; - SemaphoreHandle_t exit; - SemaphoreHandle_t exited; + SemaphoreHandle_t token; + SemaphoreHandle_t peer_token; + SemaphoreHandle_t done; + uint16_t index_base; } compression_writer_ctx_t; -static void capture_compressed_frame(const test_ble_log_frame_t *frame, - void *ctx) -{ - compression_capture_t *capture = ctx; - if (frame->src != BLE_LOG_SRC_ENCODE || - frame->payload_len < sizeof(uint32_t) + 4) { - return; - } - - const uint8_t *record = frame->payload + sizeof(uint32_t); - size_t offset = 0; - if (record[offset++] != BLE_COMPRESSED_LOG_OUT_SOURCE_HOST) { - return; - } - size_t record_len = frame->payload_len - sizeof(uint32_t); - bool switched = record[offset] == TEST_CP_TASK_SWITCH; - offset += switched; - if (record_len - offset < 3 || record[offset++] != TEST_CP_ZERO_ARGS) { - return; - } +typedef struct { + SemaphoreHandle_t done; uint16_t log_index; - memcpy(&log_index, record + offset, sizeof(log_index)); - - if (log_index == TEST_CP_INDEX_FIRST) { - capture->first_found = true; - capture->first_sn = frame->sn; - } else if (log_index == TEST_CP_INDEX_DROPPED) { - capture->dropped_found = true; - } else if (log_index == TEST_CP_INDEX_SECOND) { - capture->second_found = true; - capture->second_switched = switched; - capture->second_sn = frame->sn; - } else if (log_index == TEST_CP_INDEX_SECOND + 1) { - capture->third_found = true; - capture->third_switched = switched; - capture->third_sn = frame->sn; - } -} +} compression_once_ctx_t; static void compression_writer_task(void *arg) { compression_writer_ctx_t *ctx = arg; - ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, - TEST_CP_INDEX_SECOND, 0); - xSemaphoreGive(ctx->committed); - xSemaphoreTake(ctx->exit, portMAX_DELAY); - xSemaphoreGive(ctx->exited); + for (int i = 0; i < TEST_CP_WRITER_RECORDS; i++) { + xSemaphoreTake(ctx->token, portMAX_DELAY); + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, + ctx->index_base + i, 0); + xSemaphoreGive(ctx->peer_token); + } + xSemaphoreGive(ctx->done); vTaskDelete(NULL); } -TEST_CASE("BLE Log serializes task context per compression source", - "[ble_log][compression]") +static void compression_once_task(void *arg) { - TEST_ASSERT_TRUE(ble_log_enable(true)); - ble_log_lbm_flush_open_trans(); - for (int round = 0; round < 2; round++) { - TEST_ASSERT_TRUE(ble_log_rt_drain()); - while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), - 0, 0, NULL) > 0) { + compression_once_ctx_t *ctx = arg; + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, + ctx->log_index, 0); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +/* Walks one ENCODE frame: [ts 4B][source 1B][task_id 1B][header 1B]... + * and one INTERNAL task-binding frame: [ts 4B][int_src 1B][task_id 1B] + * [name 17B] (the periodic registry broadcast). */ +static void capture_compressed_frame(const test_ble_log_frame_t *frame, + void *ctx) +{ + compression_capture_t *capture = ctx; + /* Bindings ride the INTERNAL stream: one frame per periodic window, + * packing one fixed-layout record per registered entry. */ + if (frame->src == BLE_LOG_SRC_INTERNAL && + frame->payload_len >= sizeof(uint32_t) + + sizeof(ble_log_task_binding_t) && + (frame->payload_len - sizeof(uint32_t)) % + sizeof(ble_log_task_binding_t) == 0) { + const uint8_t *record = frame->payload + sizeof(uint32_t); + size_t records = (frame->payload_len - sizeof(uint32_t)) / + sizeof(ble_log_task_binding_t); + for (size_t r = 0; r < records; r++, record += sizeof(ble_log_task_binding_t)) { + if (record[0] != BLE_LOG_INT_SRC_TASK_BINDING) { + continue; + } + uint8_t task_id = record[1]; + const uint8_t *name = record + 2; + if (task_id < CONFIG_BLE_LOG_TASK_ID_MAX) { + capture->announce_cnt[task_id]++; + if (capture->announce_cnt[task_id] == 1) { + memcpy(capture->names[task_id], name, + sizeof(capture->names[task_id])); + } else if (strcmp(capture->names[task_id], + (const char *)name) != 0) { + /* same id re-announced with a different name */ + capture->names[task_id][0] = '\0'; + } + } + } + return; + } + if (frame->src != BLE_LOG_SRC_ENCODE) { + return; + } + const uint8_t *record = frame->payload + sizeof(uint32_t); + size_t record_len = frame->payload_len - sizeof(uint32_t); + if (record_len < 3 || + record[0] != BLE_COMPRESSED_LOG_OUT_SOURCE_HOST) { + return; + } + uint8_t task_id = record[1]; + uint8_t header = record[2]; + + if (header != TEST_CP_ZERO_ARGS || record_len < 5) { + return; + } + uint16_t log_index; + memcpy(&log_index, record + 3, sizeof(log_index)); + + if (log_index == TEST_CP_INDEX_MAIN_A || log_index == TEST_CP_INDEX_MAIN_B) { + capture->main_records++; + if (!capture->main_id_set) { + capture->main_id_set = true; + capture->main_id = task_id; + } else if (capture->main_id != task_id) { + capture->main_id_mismatch = true; + } + } else if (log_index >= TEST_CP_INDEX_BASE_A && + log_index < TEST_CP_INDEX_BASE_A + TEST_CP_WRITER_RECORDS) { + capture->a_records++; + if (!capture->a_id_set) { + capture->a_id_set = true; + capture->a_id = task_id; + } else if (capture->a_id != task_id) { + capture->a_id_mismatch = true; + } + } else if (log_index >= TEST_CP_INDEX_BASE_B && + log_index < TEST_CP_INDEX_BASE_B + TEST_CP_WRITER_RECORDS) { + capture->b_records++; + if (!capture->b_id_set) { + capture->b_id_set = true; + capture->b_id = task_id; + } else if (capture->b_id != task_id) { + capture->b_id_mismatch = true; + } + } else if (log_index >= TEST_CP_INDEX_FILLER && + log_index < TEST_CP_INDEX_FILLER + CONFIG_BLE_LOG_TASK_ID_MAX - 1) { + capture->filler_records++; + if (task_id == TEST_CP_TASK_UNKNOWN) { + capture->filler_unknown = true; + } + } else if (log_index == TEST_CP_INDEX_FILL_A) { + capture->fill_a_records++; + if (task_id != TEST_CP_TASK_UNKNOWN) { + capture->fill_a_id_set = true; + capture->fill_a_id = task_id; + } + } else if (log_index == TEST_CP_INDEX_FILL_B) { + capture->fill_b_records++; + if (task_id == TEST_CP_TASK_UNKNOWN) { + capture->fill_b_unknown = true; } } +} - ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, - TEST_CP_INDEX_FIRST, 0); - - s_compression_hook_entered = xSemaphoreCreateBinary(); - s_compression_hook_continue = xSemaphoreCreateBinary(); - compression_writer_ctx_t writer = { - .committed = xSemaphoreCreateBinary(), - .exit = xSemaphoreCreateBinary(), - .exited = xSemaphoreCreateBinary(), - }; - TEST_ASSERT_NOT_NULL(s_compression_hook_entered); - TEST_ASSERT_NOT_NULL(s_compression_hook_continue); - TEST_ASSERT_NOT_NULL(writer.committed); - TEST_ASSERT_NOT_NULL(writer.exit); - TEST_ASSERT_NOT_NULL(writer.exited); - - s_compression_hook_armed = true; - TEST_ASSERT_EQUAL( - pdTRUE, - xTaskCreate(compression_writer_task, "ble_log_cp", - TEST_LIFECYCLE_STACK_SIZE, &writer, - TEST_LIFECYCLE_PRIO, NULL)); - TEST_ASSERT_TRUE(xSemaphoreTake(s_compression_hook_entered, - pdMS_TO_TICKS(1000))); - - /* This call claims pool space but fails the per-source trylock. It must - * cancel immediately, count one lost ENCODE SN, and leave task state to - * the lock owner. */ - ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, - TEST_CP_INDEX_DROPPED, 0); - xSemaphoreGive(s_compression_hook_continue); - TEST_ASSERT_TRUE(xSemaphoreTake(writer.committed, - pdMS_TO_TICKS(1000))); - s_compression_hook_armed = false; - - ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, - TEST_CP_INDEX_SECOND + 1, 0); - xSemaphoreGive(writer.exit); - TEST_ASSERT_TRUE(xSemaphoreTake(writer.exited, pdMS_TO_TICKS(1000))); - +static void compression_drain_and_capture(compression_capture_t *capture) +{ ble_log_lbm_flush_open_trans(); TEST_ASSERT_TRUE(ble_log_rt_drain()); - compression_capture_t capture = {0}; + memset(capture, 0, sizeof(*capture)); for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT; i++) { size_t len = ble_log_prph_test_read( s_read_buf, sizeof(s_read_buf), pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), @@ -691,29 +725,267 @@ TEST_CASE("BLE Log serializes task context per compression source", break; } TEST_ASSERT_TRUE(test_ble_log_walk_frames( - s_read_buf, len, capture_compressed_frame, &capture)); + s_read_buf, len, capture_compressed_frame, capture)); + } +} + +static void compression_reset_streams(void) +{ + /* Wipe the task-id registry so neither compression case depends on the + * other's registrations or on test-case order. */ + ble_log_test_task_registry_reset(); + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } +} + +static uint8_t compression_announced_id(const compression_capture_t *capture, + const char *name) +{ + for (int i = 0; i < CONFIG_BLE_LOG_TASK_ID_MAX; i++) { + if (capture->announce_cnt[i] > 0 && + strcmp(capture->names[i], name) == 0) { + return (uint8_t)i; + } + } + return TEST_CP_TASK_UNKNOWN; +} + +/* Protocol v8: task attribution is a property of the record. Two writers + * interleave records on one compression source; every record carries its + * writer's id and no record is dropped for contention (there is no source + * lock to contend). Bindings are broadcast on the periodic window only + * (the LBM task-binding publish), so it is driven once before the capture + * below. */ +TEST_CASE("BLE Log attributes compressed records to their writer task", + "[ble_log][compression]") +{ + compression_reset_streams(); + + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, + TEST_CP_INDEX_MAIN_A, 0); + + compression_writer_ctx_t writer_a = { + .token = xSemaphoreCreateBinary(), + .peer_token = NULL, + .done = xSemaphoreCreateBinary(), + .index_base = TEST_CP_INDEX_BASE_A, + }; + compression_writer_ctx_t writer_b = { + .token = xSemaphoreCreateBinary(), + .peer_token = NULL, + .done = xSemaphoreCreateBinary(), + .index_base = TEST_CP_INDEX_BASE_B, + }; + writer_a.peer_token = writer_b.token; + writer_b.peer_token = writer_a.token; + TEST_ASSERT_NOT_NULL(writer_a.token); + TEST_ASSERT_NOT_NULL(writer_b.token); + TEST_ASSERT_NOT_NULL(writer_a.done); + TEST_ASSERT_NOT_NULL(writer_b.done); + + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(compression_writer_task, "cp_wr_a", + TEST_LIFECYCLE_STACK_SIZE, &writer_a, + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(compression_writer_task, "cp_wr_b", + TEST_LIFECYCLE_STACK_SIZE, &writer_b, + TEST_LIFECYCLE_PRIO, NULL)); + + xSemaphoreGive(writer_a.token); + TEST_ASSERT_TRUE(xSemaphoreTake(writer_a.done, pdMS_TO_TICKS(2000))); + TEST_ASSERT_TRUE(xSemaphoreTake(writer_b.done, pdMS_TO_TICKS(2000))); + + /* A second record from the main task must not register a new entry: + * its records keep the registered id. Periodic publish may repeat + * the binding in this window, always with the same name. */ + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, + TEST_CP_INDEX_MAIN_B, 0); + + /* Bindings ride the periodic window only: drive the LBM broadcast + * once so this capture carries every registered writer's binding. */ + ble_log_task_bindings_publish(); + + compression_capture_t capture; + compression_drain_and_capture(&capture); + + const char *main_name = pcTaskGetName(NULL); + TEST_ASSERT_NOT_NULL(main_name); + + /* All records present: contention between interleaved writers no + * longer drops records. */ + TEST_ASSERT_EQUAL_UINT8(TEST_CP_WRITER_RECORDS, capture.a_records); + TEST_ASSERT_EQUAL_UINT8(TEST_CP_WRITER_RECORDS, capture.b_records); + TEST_ASSERT_EQUAL_UINT8(2, capture.main_records); + + /* Every record of a writer carries that writer's announced id. */ + TEST_ASSERT_TRUE(capture.a_id_set); + TEST_ASSERT_FALSE(capture.a_id_mismatch); + TEST_ASSERT_TRUE(capture.b_id_set); + TEST_ASSERT_FALSE(capture.b_id_mismatch); + TEST_ASSERT_TRUE(capture.main_id_set); + TEST_ASSERT_FALSE(capture.main_id_mismatch); + TEST_ASSERT_NOT_EQUAL(TEST_CP_TASK_UNKNOWN, + compression_announced_id(&capture, "cp_wr_a")); + TEST_ASSERT_NOT_EQUAL(TEST_CP_TASK_UNKNOWN, + compression_announced_id(&capture, "cp_wr_b")); + TEST_ASSERT_NOT_EQUAL(TEST_CP_TASK_UNKNOWN, + compression_announced_id(&capture, main_name)); + TEST_ASSERT_EQUAL_UINT8(compression_announced_id(&capture, "cp_wr_a"), + capture.a_id); + TEST_ASSERT_EQUAL_UINT8(compression_announced_id(&capture, "cp_wr_b"), + capture.b_id); + TEST_ASSERT_EQUAL_UINT8(compression_announced_id(&capture, main_name), + capture.main_id); + + /* Every writer is announced at least once by the driven publish (a + * production tick may add another, with the same name), and the + * capture rejects a same-id rename. */ + TEST_ASSERT_TRUE( + capture.announce_cnt[compression_announced_id(&capture, "cp_wr_a")] >= 1); + TEST_ASSERT_TRUE( + capture.announce_cnt[compression_announced_id(&capture, "cp_wr_b")] >= 1); + TEST_ASSERT_TRUE( + capture.announce_cnt[compression_announced_id(&capture, main_name)] >= 1); + + vSemaphoreDelete(writer_a.token); + vSemaphoreDelete(writer_b.token); + vSemaphoreDelete(writer_a.done); + vSemaphoreDelete(writer_b.done); +} + +/* Periodic binding publish: bindings are broadcast on the snapshot window + * (the LBM periodic publish); a task's first record already carries its + * id and is bound by name at the next window. Drive the publish + * synchronously so the window is deterministic: capture the main task's + * record first to learn its id, then the driven publish binds that id to + * the name and announces no other id. */ +TEST_CASE("BLE Log republishes task bindings on the periodic window", + "[ble_log][compression]") +{ + compression_reset_streams(); + + const char *main_name = pcTaskGetName(NULL); + TEST_ASSERT_NOT_NULL(main_name); + + /* First window: the record carries the registered id, but no binding + * is guaranteed yet (a production tick may or may not have landed in + * this window, so no announce assertion is made here). */ + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, + TEST_CP_INDEX_MAIN_A, 0); + compression_capture_t first; + compression_drain_and_capture(&first); + TEST_ASSERT_TRUE(first.main_id_set); + uint8_t main_id = first.main_id; + TEST_ASSERT_NOT_EQUAL(TEST_CP_TASK_UNKNOWN, main_id); + + /* One synchronous publish; a production tick may add another with the + * same id, so only presence and id/name stability are asserted. */ + ble_log_task_bindings_publish(); + compression_capture_t second; + compression_drain_and_capture(&second); + + TEST_ASSERT_TRUE(second.announce_cnt[main_id] >= 1); + TEST_ASSERT_EQUAL_UINT8(main_id, + compression_announced_id(&second, main_name)); + /* Publishing never fabricates bindings: no other id is announced. */ + for (int i = 0; i < CONFIG_BLE_LOG_TASK_ID_MAX; i++) { + if (i != main_id) { + TEST_ASSERT_EQUAL_UINT8(0, second.announce_cnt[i]); + } + } +} + +/* The registry is sized by CONFIG_BLE_LOG_TASK_ID_MAX (4 in this + * app) and wiped between cases, so this case fills it itself: one-shot + * filler tasks consume every slot but the last, cp_fill_a takes the last + * free slot, and cp_fill_b beyond the registry degrades to the unknown id + * (0xFF) and its record is still emitted. Bindings are broadcast on the + * periodic window only, so the publish is driven once before the + * capture. */ +TEST_CASE("BLE Log degrades to the unknown task id when the registry is full", + "[ble_log][compression]") +{ + compression_reset_streams(); + + /* Consume every slot but the last with distinct one-shot tasks. The + * name is copied into the task's TCB, so one buffer is reused. */ + compression_once_ctx_t filler[CONFIG_BLE_LOG_TASK_ID_MAX - 1]; + char filler_name[8]; + for (int i = 0; i < CONFIG_BLE_LOG_TASK_ID_MAX - 1; i++) { + filler[i].done = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(filler[i].done); + filler[i].log_index = TEST_CP_INDEX_FILLER + i; + snprintf(filler_name, sizeof(filler_name), "cp_f%d", i); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(compression_once_task, filler_name, + TEST_LIFECYCLE_STACK_SIZE, &filler[i], + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_TRUE(xSemaphoreTake(filler[i].done, pdMS_TO_TICKS(2000))); } - TEST_ASSERT_TRUE(capture.first_found); - TEST_ASSERT_FALSE(capture.dropped_found); - TEST_ASSERT_TRUE(capture.second_found); - TEST_ASSERT_TRUE(capture.second_switched); - TEST_ASSERT_TRUE(capture.third_found); - TEST_ASSERT_TRUE(capture.third_switched); - /* The blocked writer claims its SN before taking the compression lock; - * the rejected contender burns the following SN. */ - TEST_ASSERT_EQUAL_HEX32((capture.first_sn + 1) & 0x00ffffffU, - capture.second_sn); - TEST_ASSERT_EQUAL_HEX32((capture.second_sn + 2) & 0x00ffffffU, - capture.third_sn); + compression_once_ctx_t fill_a = { + .done = xSemaphoreCreateBinary(), + .log_index = TEST_CP_INDEX_FILL_A, + }; + compression_once_ctx_t fill_b = { + .done = xSemaphoreCreateBinary(), + .log_index = TEST_CP_INDEX_FILL_B, + }; + TEST_ASSERT_NOT_NULL(fill_a.done); + TEST_ASSERT_NOT_NULL(fill_b.done); - vSemaphoreDelete(writer.committed); - vSemaphoreDelete(writer.exit); - vSemaphoreDelete(writer.exited); - vSemaphoreDelete(s_compression_hook_entered); - vSemaphoreDelete(s_compression_hook_continue); - s_compression_hook_entered = NULL; - s_compression_hook_continue = NULL; + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(compression_once_task, "cp_fill_a", + TEST_LIFECYCLE_STACK_SIZE, &fill_a, + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_TRUE(xSemaphoreTake(fill_a.done, pdMS_TO_TICKS(2000))); + + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(compression_once_task, "cp_fill_b", + TEST_LIFECYCLE_STACK_SIZE, &fill_b, + TEST_LIFECYCLE_PRIO, NULL)); + TEST_ASSERT_TRUE(xSemaphoreTake(fill_b.done, pdMS_TO_TICKS(2000))); + + /* Bindings ride the periodic window only: drive the LBM broadcast + * once so the capture carries every registered name. */ + ble_log_task_bindings_publish(); + + compression_capture_t capture; + compression_drain_and_capture(&capture); + + /* The fillers consumed the first slots: every record emitted with a + * valid id. */ + TEST_ASSERT_EQUAL_UINT8(CONFIG_BLE_LOG_TASK_ID_MAX - 1, + capture.filler_records); + TEST_ASSERT_FALSE(capture.filler_unknown); + + /* The last free slot still works: valid id, bound by the driven + * periodic publish. */ + TEST_ASSERT_EQUAL_UINT8(1, capture.fill_a_records); + TEST_ASSERT_TRUE(capture.fill_a_id_set); + TEST_ASSERT_NOT_EQUAL(TEST_CP_TASK_UNKNOWN, + compression_announced_id(&capture, "cp_fill_a")); + TEST_ASSERT_EQUAL_UINT8(compression_announced_id(&capture, "cp_fill_a"), + capture.fill_a_id); + + /* Beyond the registry: unknown id, no binding, log still emitted. */ + TEST_ASSERT_EQUAL_UINT8(1, capture.fill_b_records); + TEST_ASSERT_TRUE(capture.fill_b_unknown); + TEST_ASSERT_EQUAL_UINT8(TEST_CP_TASK_UNKNOWN, + compression_announced_id(&capture, "cp_fill_b")); + + for (int i = 0; i < CONFIG_BLE_LOG_TASK_ID_MAX - 1; i++) { + vSemaphoreDelete(filler[i].done); + } + vSemaphoreDelete(fill_a.done); + vSemaphoreDelete(fill_b.done); } #endif /* CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE */ @@ -1119,7 +1391,7 @@ static void blocked_claim_task(void *arg) blocked_writer_ctx_t *ctx = arg; uint32_t handle; xSemaphoreGive(ctx->started); - uint8_t *payload = ble_log_claim(BLE_LOG_SRC_ENCODE, 1, &handle); + uint8_t *payload = ble_log_claim(BLE_LOG_SRC_ENCODE, 1, &handle, true); ctx->result = payload != NULL; if (payload) { payload[0] = 0x58; diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/sdkconfig.defaults b/components/bt/common/ble_log/test_apps/ble_log_test/sdkconfig.defaults index 7bbad451380..6f03c3e16dd 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/sdkconfig.defaults +++ b/components/bt/common/ble_log/test_apps/ble_log_test/sdkconfig.defaults @@ -4,3 +4,8 @@ CONFIG_BLE_LOG_PRPH_TEST=y CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n CONFIG_UNITY_ENABLE_64BIT=y CONFIG_BLE_MESH=y +# Exercise the compressed-log encoders and the task-id registry (protocol v8). +CONFIG_BLE_COMPRESSED_LOG_ENABLE=y +CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE=y +# Small registry so the table-full degradation is reachable on target. +CONFIG_BLE_LOG_TASK_ID_MAX=4 From 22812d893ae279fbf1acae4e1d2c153462f4ff6b Mon Sep 17 00:00:00 2001 From: Zhou Xiao Date: Sun, 6 Sep 2026 20:13:24 +0800 Subject: [PATCH 11/13] fix(ble_log): harden the pool waiter handshake and claim ordering Consolidates the concurrency review fixes for the unified pool: - the shared ESP Timer task never waits for a transport: its identity is checked on the would-wait path only, so its callbacks always return and dispatch can progress (this also bounds claim()'s backpressure); - transports accepted from the FREE bitmap are ACQUIRE-loaded: the recycler publishes pos/pending_seal with a STORE_RELEASE(FREE) without holding the candidate lock, and the acceptance load pairs with that publication; - a waiter whose scan bounced off a candidate lock is re-advertised: the releasing side re-checks availability after the lock release, inside the same seq_cst window as the waiter-count read, so a transport that became claimable while locked cannot strand its wake; - waiters never self-wake on an unpublished FREE transport: notification for a FREE transport fires only when its free-bitmap hint is already published, so a registered waiter cannot mint and consume its own wake tokens in a self-sustaining spin; - flush and deinit drains re-check writer references after observing a zero waiter count: a writer waking between the two loads re-acquires its reference before unregistering, and the seq_cst-fenced re-read must observe it before the drain concludes. --- components/bt/common/ble_log/README.md | 18 +- .../bt/common/ble_log/include/ble_log.h | 10 +- .../bt/common/ble_log/src/ble_log_lbm_v2.c | 133 +++- .../ble_log/src/ble_log_task_registry.c | 4 +- .../src/internal_include/ble_log_lbm_v2.h | 6 +- .../ble_log_test/main/test_ble_log_rt.c | 740 ++++++++++++++++++ 6 files changed, 878 insertions(+), 33 deletions(-) diff --git a/components/bt/common/ble_log/README.md b/components/bt/common/ble_log/README.md index 67c75721077..c8506fef349 100644 --- a/components/bt/common/ble_log/README.md +++ b/components/bt/common/ble_log/README.md @@ -19,9 +19,12 @@ flowchart TD The shared pool contains `CONFIG_BLE_LOG_POOL_TRANS_CNT` transports. The last `CONFIG_BLE_LOG_POOL_NON_YIELD_RESERVE_CNT` transports are reserved for ISR and -other contexts that cannot yield. Every yieldable-context writer, from the -public API and claims to the controller LL task, waits for a shared transport; -only ISR and critical-section writers fail fast. +other contexts that cannot yield. Ordinary yieldable task writers, from the +public API and claims to the controller LL task, wait for a shared transport +unless a claim explicitly opts out. Writers on the shared ESP Timer task never +wait for a transport: its callbacks must return so dispatch can progress. +While yieldable, they use shared transports only; ISR and critical-section +writers retain reserve access and fail-fast behavior. The Internal Snapshot and UART0 redirection transports are not members of the bitmap pool. @@ -202,10 +205,11 @@ if (payload) { ``` `ble_log_claim()` reserves the hidden frame header, ESP Timer timestamp, and -checksum. It waits for a shared transport in yieldable contexts when -`wait_for_transport` is true (writer backpressure); pass false for a lossy -fast path from contexts that must not block, such as the shared ESP Timer -task. Non-yieldable contexts (ISR, critical section) fail fast either way. +checksum. It waits for a shared transport in ordinary yieldable tasks when +`wait_for_transport` is true (writer backpressure); pass false to opt out. +The shared ESP Timer task and non-yieldable contexts fail fast either way. +A busy-pool rejection leaves a Global SN gap and increments the source's loss +counter, just like other pool acquisition failures. Every successful claim must be committed exactly once before `ble_log_deinit()`; `ble_log_commit(handle, 0)` cancels it. Handles include a transport generation so a stale handle cannot commit a later claim in the same diff --git a/components/bt/common/ble_log/include/ble_log.h b/components/bt/common/ble_log/include/ble_log.h index 98a7410e7c6..06c4003bd25 100644 --- a/components/bt/common/ble_log/include/ble_log.h +++ b/components/bt/common/ble_log/include/ble_log.h @@ -67,12 +67,12 @@ void ble_log_deinit(void); bool ble_log_enable(bool enable); /* Blocking; call only from a caller-owned task, not an ISR or system callback. */ void ble_log_flush(void); -/* Waits for a shared transport in yieldable contexts; ISR and critical-section callers fail fast. */ +/* Waits for a shared transport in ordinary yieldable tasks. The shared ESP + * Timer task, ISR and critical-section callers fail fast when none is available. */ bool ble_log_write_hex(ble_log_src_t src_code, const uint8_t *addr, size_t len); -/* Same backpressure as ble_log_write_hex(): yieldable claims wait for a - * shared transport when wait_for_transport is true; pass false for a lossy - * fast path from contexts that must not block (system periodic output). - * Non-yieldable contexts (ISR, critical section) fail fast either way. */ +/* Same backpressure as ble_log_write_hex(): ordinary yieldable tasks wait + * when wait_for_transport is true; false opts out. The shared ESP Timer + * task and non-yieldable contexts fail fast regardless of this flag. */ uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, uint32_t *handle, bool wait_for_transport); void ble_log_commit(uint32_t handle, size_t actual_len); diff --git a/components/bt/common/ble_log/src/ble_log_lbm_v2.c b/components/bt/common/ble_log/src/ble_log_lbm_v2.c index 9ac016d0bf3..f25223e036a 100644 --- a/components/bt/common/ble_log/src/ble_log_lbm_v2.c +++ b/components/bt/common/ble_log/src/ble_log_lbm_v2.c @@ -26,6 +26,9 @@ #endif // CONFIG_BT_DUAL_MODE_ARCH #endif /* CONFIG_BLE_LOG_LL_ENABLED && CONFIG_SOC_ESP_NIMBLE_CONTROLLER */ +/* hint: private API; keep this declaration in sync with esp_timer_impl.h. */ +extern TaskHandle_t esp_timer_impl_get_timer_task_handle(void); + /* MACRO */ #define BLE_LOG_POOL_MASK(count) (0xFFFFFFFFu >> (32 - (count))) #define BLE_LOG_POOL_ALL_MASK BLE_LOG_POOL_MASK(BLE_LOG_POOL_TRANS_CNT) @@ -130,6 +133,11 @@ extern void ble_log_test_enable_before_lifecycle_lock_hook(void) __attribute__(( extern void ble_log_test_disable_before_wake_hook(void) __attribute__((weak)); extern void ble_log_test_claim_locked_hook(void) __attribute__((weak)); extern void ble_log_test_init_snapshot_before_acquire_hook(void) __attribute__((weak)); +extern void ble_log_test_flush_hint_seen_hook(uint8_t id) __attribute__((weak)); +extern void ble_log_test_flush_stale_locked_hook(void) __attribute__((weak)); +extern void ble_log_test_recycle_pre_bitmap_hook(ble_log_prph_trans_t *trans) __attribute__((weak)); +extern void ble_log_test_flush_drain_between_loads_hook(void) __attribute__((weak)); +extern void ble_log_test_acquire_after_unregister_hook(void) __attribute__((weak)); #endif BLE_LOG_IRAM_ATTR BLE_LOG_STATIC @@ -221,6 +229,31 @@ BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void ble_log_pool_notify_waiter(uint8_t id) } } +/* Releases a candidate lock that was taken on a stale bitmap hint without + * changing the transport state. The transport may have become claimable + * while the lock was held (recycle does not take this lock), and a waiter + * whose scan bounced off this lock depends on this release for its wake: + * availability is re-checked only AFTER the release, inside the same + * SC-fenced window as the waiter-count read. SENDING/CLAIMED must not + * mint: unconditional notification would let a registered scanning waiter + * wake itself on a full pool. A FREE transport notifies only when its + * free-bitmap hint is already published: state alone does not make it + * claimable (a preempted recycler sits between the FREE store and the + * bitmap set), and notifying an unadvertised transport lets a waiter + * mint and consume its own wake tokens in a self-sustaining spin. */ +BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void +ble_log_pool_release_candidate(ble_log_prph_trans_t *trans) +{ + BLE_LOG_CAS_RELEASE(&trans->atomic_lock); + __atomic_thread_fence(__ATOMIC_SEQ_CST); + ble_log_trans_state_t st = BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state); + if (st == BLE_LOG_TRANS_STATE_OPEN || + (st == BLE_LOG_TRANS_STATE_FREE && + (BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.free_bitmap) & BIT(trans->id)))) { + ble_log_pool_notify_waiter(trans->id); + } +} + /* Publish OPEN and release ownership as one operation so notification can * never be moved before the lock release. */ BLE_LOG_IRAM_ATTR BLE_LOG_STATIC void @@ -286,6 +319,11 @@ BLE_LOG_IRAM_ATTR void ble_log_lbm_recycle_trans(ble_log_prph_trans_t *trans) /* Publish FREE state before advertising the pool bitmap hint. */ BLE_LOG_ATOMIC_STORE_RELEASE(trans->state, BLE_LOG_TRANS_STATE_FREE); +#if CONFIG_BLE_LOG_PRPH_TEST + if (ble_log_test_recycle_pre_bitmap_hook) { + ble_log_test_recycle_pre_bitmap_hook(trans); + } +#endif ble_log_pool_bitmap_set(&g_pool.free_bitmap, trans->id); ble_log_pool_notify_waiter(trans->id); } @@ -358,9 +396,14 @@ ble_log_prph_trans_t *ble_log_pool_try_claim_from(volatile uint32_t *bitmap, if (!BLE_LOG_CAS_ACQUIRE(&trans->atomic_lock)) { continue; } - if (BLE_LOG_ATOMIC_LOAD_RELAXED(trans->state) != expected_state) { + /* Acceptance load: pairs with the recycler's STORE_RELEASE(FREE), + * which publishes pos/pending_seal without holding this lock (the + * last lock release predates the recycle). The OPEN domain is + * already covered by that lock pairing; the acquire is required + * for FREE acceptance. */ + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(trans->state) != expected_state) { /* The bitmap is only a hint; another owner may have changed state. */ - BLE_LOG_CAS_RELEASE(&trans->atomic_lock); + ble_log_pool_release_candidate(trans); continue; } @@ -421,7 +464,7 @@ ble_log_prph_trans_t *ble_log_pool_try_claim_available(uint32_t frame_len, bool } ble_log_pool_seal_and_send(open_trans); /* releases the lock */ } else { - BLE_LOG_CAS_RELEASE(&open_trans->atomic_lock); + ble_log_pool_release_candidate(open_trans); } } } @@ -462,7 +505,10 @@ ble_log_prph_trans_t *ble_log_pool_acquire(size_t log_len, for (;;) { ble_log_prph_trans_t *trans = ble_log_pool_try_claim_available(frame_len, use_reserve); - if (trans || !wait || !BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled)) { + /* The shared ESP Timer task cannot wait for its own dispatcher. + * Check its identity only on the yieldable, would-wait path. */ + if (trans || !wait || !BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled) || + xTaskGetCurrentTaskHandle() == esp_timer_impl_get_timer_task_handle()) { return trans; } @@ -485,6 +531,11 @@ ble_log_prph_trans_t *ble_log_pool_acquire(size_t log_len, xSemaphoreTake(g_pool.sem, portMAX_DELAY); BLE_LOG_REF_COUNT_ACQUIRE_SEQ_CST(&lbm_ref_count); ble_log_pool_waiter_adjust(-1); +#if CONFIG_BLE_LOG_PRPH_TEST + if (ble_log_test_acquire_after_unregister_hook) { + ble_log_test_acquire_after_unregister_hook(); + } +#endif if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled)) { return NULL; @@ -563,12 +614,9 @@ void ble_log_pool_finish_frame(ble_log_prph_trans_t *trans, uint16_t payload_len /* ---------------------------------------------- */ /* Claim with a caller-chosen wait policy. wait_for_transport=true applies - * backpressure in a yieldable context (the writer waits for a shared - * transport instead of dropping); false is a lossy fast path that returns - * NULL on a busy pool, for callers that must never block (e.g. system - * periodic output on the shared ESP timer task). Non-yieldable contexts - * (ISR, scheduler suspended) never wait whatever the policy: they keep - * their dedicated reserve and fail fast on contention. */ + * backpressure in ordinary yieldable tasks; false returns NULL on a busy + * pool. The shared ESP Timer task never waits, regardless of that policy. + * Non-yieldable contexts retain their reserve and fail-fast behavior. */ uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, uint32_t *handle, bool wait_for_transport) { @@ -756,10 +804,24 @@ void ble_log_lbm_begin_deinit(void) /* Wake any blocked task writers and wait until BOTH the reference count * and the waiting-task count drain to zero. Blocked writers hold no * reference while parked, so waiting on ref_count alone could free the - * pool while a woken task still touches it (use-after-free). */ + * pool while a woken task still touches it (use-after-free). The two + * counters are read separately: a writer waking between the loads + * re-acquires its reference before unregistering, so a zero waiter + * count alone is not a drain. The SC fence pairs with that acquire: + * once the waiter load has observed the unregister, the re-read below + * must observe the re-acquired reference. */ TickType_t ticks_waited = 0; - while ((BLE_LOG_ATOMIC_LOAD_SEQ_CST(lbm_ref_count) > 0) || - (BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.waiting_task_count) > 0)) { + while (true) { + bool drained = false; + if (BLE_LOG_ATOMIC_LOAD_SEQ_CST(lbm_ref_count) == 0) { + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.waiting_task_count) == 0) { + __atomic_thread_fence(__ATOMIC_SEQ_CST); + drained = BLE_LOG_ATOMIC_LOAD_SEQ_CST(lbm_ref_count) == 0; + } + } + if (drained) { + break; + } ble_log_pool_wake_all(); vTaskDelay(1); BLE_LOG_ASSERT(ticks_waited++ < BLE_LOG_WAIT_TIMEOUT_TICKS); @@ -872,7 +934,9 @@ bool ble_log_internal_snapshot(uint16_t reason_flags, TickType_t start_tick = xTaskGetTickCount(); for (;;) { if (BLE_LOG_CAS_ACQUIRE(&internal_trans->atomic_lock)) { - if (BLE_LOG_ATOMIC_LOAD_RELAXED(internal_trans->state) == + /* Acceptance load: pairs with the dedicated transport's + * lock-free recycle publication (pos=0, STORE_RELEASE(FREE)). */ + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(internal_trans->state) == BLE_LOG_TRANS_STATE_FREE) { break; } @@ -963,6 +1027,13 @@ void ble_log_lbm_flush_open_trans(void) continue; } ble_log_prph_trans_t *trans = g_pool.trans[id]; +#if CONFIG_BLE_LOG_PRPH_TEST + /* Test point: the OPEN hint was just observed; a test can play the + * other core before this scan takes the candidate lock. */ + if (ble_log_test_flush_hint_seen_hook) { + ble_log_test_flush_hint_seen_hook((uint8_t)id); + } +#endif if (!BLE_LOG_CAS_ACQUIRE(&trans->atomic_lock)) { /* A writer holds the buffer: leave a pending-seal marker for * the next claim instead of waiting (the flusher never @@ -976,7 +1047,14 @@ void ble_log_lbm_flush_open_trans(void) trans->pos > 0) { ble_log_pool_seal_and_send(trans); } else { - BLE_LOG_CAS_RELEASE(&trans->atomic_lock); +#if CONFIG_BLE_LOG_PRPH_TEST + /* Test point: this scan holds a candidate lock taken on a + * stale hint; the transport state is not OPEN. */ + if (ble_log_test_flush_stale_locked_hook) { + ble_log_test_flush_stale_locked_hook(); + } +#endif + ble_log_pool_release_candidate(trans); } } @@ -1053,8 +1131,29 @@ void ble_log_flush(void) bool lbm_enabled_copy = BLE_LOG_ATOMIC_LOAD_ACQUIRE(lbm_enabled); ble_log_lbm_disable(); TickType_t start_tick = xTaskGetTickCount(); - while ((BLE_LOG_ATOMIC_LOAD_SEQ_CST(lbm_ref_count) > 1) || - (BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.waiting_task_count) > 0)) { + bool writers_drained = false; + while (!writers_drained) { + if (BLE_LOG_ATOMIC_LOAD_SEQ_CST(lbm_ref_count) <= 1) { +#if CONFIG_BLE_LOG_PRPH_TEST + if (ble_log_test_flush_drain_between_loads_hook) { + ble_log_test_flush_drain_between_loads_hook(); + } +#endif + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.waiting_task_count) == 0) { + /* The refcount and the waiter count are two separate + * loads: a writer waking between them re-acquires its + * reference BEFORE unregistering, so a zero waiter count + * alone is not a drain. The SC fence pairs with that + * re-acquire: once this load has observed the unregister, + * the re-read must observe the re-acquired reference. */ + __atomic_thread_fence(__ATOMIC_SEQ_CST); + writers_drained = + BLE_LOG_ATOMIC_LOAD_SEQ_CST(lbm_ref_count) <= 1; + } + } + if (writers_drained) { + break; + } ble_log_pool_wake_all(); if ((xTaskGetTickCount() - start_tick) >= BLE_LOG_WAIT_TIMEOUT_TICKS) { BLE_LOG_CONSOLE("@EW: Timed out waiting for BLE Log writers\n"); diff --git a/components/bt/common/ble_log/src/ble_log_task_registry.c b/components/bt/common/ble_log/src/ble_log_task_registry.c index f9e8c92a82d..537833a594b 100644 --- a/components/bt/common/ble_log/src/ble_log_task_registry.c +++ b/components/bt/common/ble_log/src/ble_log_task_registry.c @@ -164,7 +164,9 @@ BLE_LOG_STATIC bool ble_log_task_binding_send(uint8_t cnt, uint32_t timestamp) if (!BLE_LOG_CAS_ACQUIRE(&binding_trans->atomic_lock)) { return false; } - if (BLE_LOG_ATOMIC_LOAD_RELAXED(binding_trans->state) != + /* Acceptance load: pairs with the dedicated transport's lock-free + * recycle publication (pos=0, STORE_RELEASE(FREE)). */ + if (BLE_LOG_ATOMIC_LOAD_ACQUIRE(binding_trans->state) != BLE_LOG_TRANS_STATE_FREE) { BLE_LOG_CAS_RELEASE(&binding_trans->atomic_lock); return false; diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h index 0a057a0b93e..da8dc7047f5 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h @@ -246,9 +246,9 @@ bool ble_log_internal_snapshot(uint16_t reason_flags, * only BLE_LOG_SRC_ENCODE is supported; its frames are stamped with the * ENCODE source ID on the wire. wait_for_transport=false turns a busy pool * into a lossy fast path (NULL, as in a non-yieldable context) instead of - * waiting, for output that must not block its caller (e.g. the shared ESP - * timer task); true waits in yieldable contexts, never in non-yieldable - * ones. */ + * waiting. true requests backpressure in ordinary yieldable tasks; the + * shared ESP Timer task and non-yieldable contexts never wait, regardless + * of the requested policy. */ uint8_t *ble_log_claim(ble_log_src_t src_code, size_t max_len, uint32_t *handle, bool wait_for_transport); void ble_log_commit(uint32_t handle, size_t actual_len); diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c index 3ca6ae4ecd5..c976ad3c219 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c @@ -11,6 +11,7 @@ #include #include "esp_chip_info.h" +#include "esp_timer.h" #include "freertos/FreeRTOS.h" #include "freertos/semphr.h" #include "freertos/task.h" @@ -67,11 +68,21 @@ static volatile bool s_disable_hook_armed; static SemaphoreHandle_t s_disable_hook_entered; static SemaphoreHandle_t s_disable_hook_continue; +/* Stale-hint candidate flush (pool lost-wakeup interleaving). */ +static volatile bool s_flush_hint_hook_armed; +static volatile bool s_flush_stale_hook_armed; +static volatile bool s_flush_hint_drained; +static volatile int s_flush_hint_id = -1; +static SemaphoreHandle_t s_stale_writer_start; +static SemaphoreHandle_t s_stale_writer_started; + void ble_log_test_claim_pre_publish_hook(void); void ble_log_test_claim_locked_hook(void); void ble_log_test_init_snapshot_before_acquire_hook(void); void ble_log_test_enable_before_lifecycle_lock_hook(void); void ble_log_test_disable_before_wake_hook(void); +void ble_log_test_flush_hint_seen_hook(uint8_t id); +void ble_log_test_flush_stale_locked_hook(void); #if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE extern int ble_log_compressed_hex_print(uint8_t source, uint32_t log_index, size_t args_cnt, ...); @@ -121,6 +132,42 @@ void ble_log_test_disable_before_wake_hook(void) } } +void ble_log_test_flush_hint_seen_hook(uint8_t id) +{ + if (!s_flush_hint_hook_armed) { + return; + } + s_flush_hint_hook_armed = false; + s_flush_hint_id = id; + /* Play the other core between the flusher's hint read and its candidate + * lock: seal, send and recycle the one OPEN transport. The recycle + * notification runs while no writer is registered yet, so only the + * flusher's own candidate release can re-advertise it. drain dispatches + * to the peripheral queue; the peripheral releases on read, and that + * queue holds only the hinted transport (the other shared ones are + * pinned by open claims), so one read recycles exactly it. */ + ble_log_lbm_flush_open_trans(); + s_flush_hint_drained = ble_log_rt_drain(); + if (s_flush_hint_drained) { + s_flush_hint_drained = + ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), 0, 0, NULL) > 0; + } +} + +void ble_log_test_flush_stale_locked_hook(void) +{ + if (!s_flush_stale_hook_armed) { + return; + } + s_flush_stale_hook_armed = false; + /* The flusher holds the recycled transport's candidate lock. Start the + * writer: with every other shared transport pinned by a claim, its two + * scans bounce off this lock and it parks before this hook returns. */ + xSemaphoreGive(s_stale_writer_start); + (void)xSemaphoreTake(s_stale_writer_started, portMAX_DELAY); + vTaskDelay(pdMS_TO_TICKS(20)); +} + /* A commit field is hex characters, zero-padded after a shorter value; * anything else (garbage, non-hex, zeros after data) is invalid. */ static bool commit_is_valid(const uint8_t *commit, size_t len) @@ -1501,6 +1548,291 @@ TEST_CASE("BLE Log task writers wait for a shared transport", "[ble_log][lbm]") } } +typedef struct { + SemaphoreHandle_t done; + bool write_result; +} stale_writer_ctx_t; + +static void stale_candidate_writer_task(void *arg) +{ + stale_writer_ctx_t *ctx = arg; + (void)xSemaphoreTake(s_stale_writer_start, portMAX_DELAY); + xSemaphoreGive(s_stale_writer_started); + const uint8_t marker = 0x61; + ctx->write_result = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &marker, 1); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +TEST_CASE("BLE Log flush re-advertises a stale candidate for parked writers", + "[ble_log][lbm]") +{ + /* Deterministic lost-wakeup interleaving: the periodic flusher reads an + * OPEN hint, the hinted transport is sealed, sent and recycled before + * the flusher takes its candidate lock, and a task writer registers + * and parks while that lock is held. Only the flusher's release can + * then re-advertise the FREE transport; a bare unlock loses the + * writer. */ + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), 0, 0, NULL) > 0) { + } + + /* Pin every shared transport but one with an open claim: claimed + * transports are in no bitmap and never enter the runtime queue. */ + uint32_t pinned[BLE_LOG_POOL_TRANS_CNT] = {0}; + int pinned_cnt = 0; + for (; pinned_cnt < BLE_LOG_POOL_SHARED_CNT - 1; pinned_cnt++) { + uint8_t *payload = ble_log_claim(BLE_LOG_SRC_ENCODE, 1, + &pinned[pinned_cnt], false); + TEST_ASSERT_NOT_NULL(payload); + } + + /* The last shared transport stays OPEN, so the armed flush targets it. */ + const uint8_t marker = 0x61; + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &marker, 1)); + + stale_writer_ctx_t ctx = {.done = xSemaphoreCreateBinary()}; + s_stale_writer_start = xSemaphoreCreateBinary(); + s_stale_writer_started = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(ctx.done); + TEST_ASSERT_NOT_NULL(s_stale_writer_start); + TEST_ASSERT_NOT_NULL(s_stale_writer_started); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(stale_candidate_writer_task, "ble_log_stale", + TEST_LIFECYCLE_STACK_SIZE, &ctx, + TEST_LIFECYCLE_PRIO, NULL)); + + s_flush_hint_id = -1; + s_flush_hint_drained = false; + s_flush_hint_hook_armed = true; + s_flush_stale_hook_armed = true; + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(s_flush_hint_drained); + TEST_ASSERT_TRUE(s_flush_hint_id >= 0); + + bool completed = xSemaphoreTake(ctx.done, pdMS_TO_TICKS(1000)) == pdTRUE; + if (!completed) { + /* Lost wake: un-park the writer, release the pinned claims and + * restore the gate before failing, so later cases stay clean. */ + TEST_ASSERT_TRUE(ble_log_enable(false)); + TEST_ASSERT_EQUAL(pdTRUE, xSemaphoreTake(ctx.done, pdMS_TO_TICKS(1000))); + TEST_ASSERT_FALSE(ctx.write_result); + for (int i = 0; i < pinned_cnt; i++) { + ble_log_commit(pinned[i], 0); + } + TEST_ASSERT_TRUE(ble_log_enable(true)); + TEST_FAIL_MESSAGE("stale-candidate release lost the parked writer's wake"); + } + TEST_ASSERT_TRUE(ctx.write_result); + + for (int i = 0; i < pinned_cnt; i++) { + ble_log_commit(pinned[i], 0); + } + + /* The recovered writer's frame reaches the peripheral. */ + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + TEST_ASSERT_GREATER_THAN_size_t( + 0, ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), 0, NULL)); + + vSemaphoreDelete(ctx.done); + vSemaphoreDelete(s_stale_writer_start); + vSemaphoreDelete(s_stale_writer_started); +} + +typedef struct { + SemaphoreHandle_t done; + uint8_t marker; + bool result; + bool finished; +} conc_writer_ctx_t; + +#define CONC_WRITER_CNT (4) +#define CONC_FRAMES_EACH (96) + +static void conc_writer_task(void *arg) +{ + conc_writer_ctx_t *ctx = arg; + for (uint16_t i = 0; i < CONC_FRAMES_EACH; i++) { + uint8_t payload[3] = {ctx->marker, (uint8_t)(i >> 8), (uint8_t)i}; + if (!ble_log_write_hex(BLE_LOG_SRC_CUSTOM, payload, sizeof(payload))) { + ctx->result = false; + break; + } + } + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +typedef struct { + size_t frames; + size_t per_marker[CONC_WRITER_CNT]; + uint32_t sn[CONC_WRITER_CNT * CONC_FRAMES_EACH]; + size_t sn_count; +} conc_capture_t; + +static void capture_conc_frame(const test_ble_log_frame_t *frame, void *ctx_) +{ + conc_capture_t *cap = ctx_; + if (frame->src != BLE_LOG_SRC_CUSTOM || frame->payload_len != 4 + 3) { + return; + } + cap->frames++; + uint8_t marker = frame->payload[4]; + for (int w = 0; w < CONC_WRITER_CNT; w++) { + if (marker == 0xa0 + w) { + cap->per_marker[w]++; + } + } + if (cap->sn_count < CONC_WRITER_CNT * CONC_FRAMES_EACH) { + cap->sn[cap->sn_count++] = frame->sn; + } +} + +typedef struct { + SemaphoreHandle_t done; + uint32_t hammer_cnt; + bool taken; +} hammer_ctx_t; + +static void hammer_claim_task(void *arg) +{ + hammer_ctx_t *ctx = arg; + uint32_t committed = 0; + const uint8_t marker = 0xb0; + for (int i = 0; i < 4000; i++) { + uint32_t handle; + uint8_t *payload = ble_log_claim(BLE_LOG_SRC_ENCODE, 1, + &handle, false); + if (payload) { + payload[0] = marker; + ble_log_commit(handle, 1); + committed++; + } + } + ctx->hammer_cnt = committed; + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +typedef struct { + uint32_t sn[CONC_WRITER_CNT * CONC_FRAMES_EACH + 16 * 1024]; + size_t sn_count; +} hammer_capture_t; + +static void capture_hammer_frame(const test_ble_log_frame_t *frame, void *ctx_) +{ + hammer_capture_t *cap = ctx_; + if (frame->src == BLE_LOG_SRC_ENCODE && + cap->sn_count < CONC_WRITER_CNT * CONC_FRAMES_EACH + 16 * 1024) { + cap->sn[cap->sn_count++] = frame->sn; + } +} + +static size_t count_unique_sn(const uint32_t *sn, size_t n) +{ + size_t unique = 0; + for (size_t i = 0; i < n; i++) { + bool first = true; + for (size_t j = 0; j < i; j++) { + if (sn[j] == sn[i]) { + first = false; + break; + } + } + unique += first; + } + return unique; +} + +typedef struct { + SemaphoreHandle_t done; + bool write_result; + bool claim_result; +} timer_writer_ctx_t; + +static void timer_writer_cb(void *arg) +{ + timer_writer_ctx_t *ctx = arg; + const uint8_t marker = 0x59; + ctx->write_result = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &marker, 1); + uint32_t handle; + uint8_t *payload = ble_log_claim(BLE_LOG_SRC_ENCODE, 1, &handle, true); + ctx->claim_result = payload != NULL; + if (payload) { + payload[0] = marker; + ble_log_commit(handle, 1); + } +#if CONFIG_BLE_LOG_LL_ENABLED + ble_log_write_hex_ll(1, &marker, 0, NULL, BIT(BLE_LOG_LL_FLAG_TASK)); +#endif +#if CONFIG_BLE_HOST_COMPRESSED_LOG_ENABLE + ble_log_compressed_hex_print(BLE_COMPRESSED_LOG_OUT_SOURCE_HOST, 0x730, 0); +#endif + xSemaphoreGive(ctx->done); +} + +TEST_CASE("BLE Log ESP Timer writers never wait for shared transports", + "[ble_log][lbm]") +{ + static const uint8_t full_payload[ + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t)] = {0}; + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), 0, 0, NULL) > 0) { + } + /* The test peripheral holds these transports until explicitly read. + * Reserve capacity remains available but must not be used by this callback. */ + for (int i = 0; i < BLE_LOG_POOL_SHARED_CNT; i++) { + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, full_payload, + sizeof(full_payload))); + } + + timer_writer_ctx_t ctx = {.done = xSemaphoreCreateBinary()}; + TEST_ASSERT_NOT_NULL(ctx.done); + esp_timer_handle_t timer; + const esp_timer_create_args_t args = { + .callback = timer_writer_cb, + .arg = &ctx, + .dispatch_method = ESP_TIMER_TASK, + .name = "ble_log_writer", + }; + TEST_ESP_OK(esp_timer_create(&args, &timer)); + + bool returned = true; + bool results_match = true; + /* First fire with shared capacity exhausted, then again after recycle: + * both callbacks must return, but only the second may write/claim. */ + for (int attempt = 0; attempt < 2; attempt++) { + TEST_ESP_OK(esp_timer_start_once(timer, 1)); + bool completed = xSemaphoreTake(ctx.done, pdMS_TO_TICKS(1000)) == pdTRUE; + returned &= completed; + if (!completed) { + /* A regressed writer may be parked. Wake it before waiting for + * callback completion; never delete a task inside a pool API. */ + TEST_ASSERT_TRUE(ble_log_enable(false)); + } + TEST_ESP_OK(esp_timer_stop_blocking(timer, portMAX_DELAY)); + (void)xSemaphoreTake(ctx.done, 0); + results_match &= ctx.write_result == (attempt != 0) && + ctx.claim_result == (attempt != 0); + + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), 0, 0, NULL) > 0) { + } + TEST_ASSERT_TRUE(ble_log_enable(true)); + } + TEST_ESP_OK(esp_timer_delete(timer)); + vSemaphoreDelete(ctx.done); + TEST_ASSERT_TRUE_MESSAGE(returned, "ESP Timer writer waited for a shared transport"); + TEST_ASSERT_TRUE(results_match); +} + #define SNAPSHOT_CAPTURE_MAX 8 typedef struct { @@ -1777,3 +2109,411 @@ TEST_CASE("BLE Log disable keeps waiter semaphore alive during deinit", TEST_ASSERT_TRUE(ble_log_init()); } #endif /* CONFIG_BLE_LOG_LL_ENABLED */ + +/* ------------------------------------------------------------------ */ +/* Review blocker repro: pool waiter self-wake spin. */ +/* A cancelled claim publishes FREE state before the free-bitmap hint; */ +/* a waiter scanning that transport from the stale open-cursor hint */ +/* must PARK. Unconditional notification on the unlocked FREE state */ +/* lets the waiter mint and consume its own wake tokens in a tight */ +/* loop: the writer never parks, and the publisher suspended inside */ +/* the recycle window can never run to publish the bitmap. */ +/* ------------------------------------------------------------------ */ +static volatile bool s_spin_hook_armed; +static SemaphoreHandle_t s_spin_writer_start; +static SemaphoreHandle_t s_spin_writer_entered; +static SemaphoreHandle_t s_spin_canceller_hold; + +typedef struct { + SemaphoreHandle_t done; + volatile bool result; +} spin_writer_ctx_t; + +static void spin_writer_task(void *arg) +{ + spin_writer_ctx_t *ctx = arg; + (void)xSemaphoreTake(s_spin_writer_start, portMAX_DELAY); + xSemaphoreGive(s_spin_writer_entered); + const uint8_t marker = 0xC1; + ctx->result = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &marker, 1); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +typedef struct { + SemaphoreHandle_t done; + uint32_t handle; +} spin_cancel_ctx_t; + +static void spin_canceller_task(void *arg) +{ + spin_cancel_ctx_t *ctx = arg; + s_spin_hook_armed = true; + ble_log_commit(ctx->handle, 0); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +void ble_log_test_recycle_pre_bitmap_hook(ble_log_prph_trans_t *trans); + +void ble_log_test_recycle_pre_bitmap_hook(ble_log_prph_trans_t *trans) +{ + (void)trans; + if (!s_spin_hook_armed) { + return; + } + s_spin_hook_armed = false; + /* Deterministic preemption between the FREE publication and the + * free-bitmap hint: release the waiting writer, then stay suspended + * exactly like a publisher preempted in this window. */ + xSemaphoreGive(s_spin_writer_start); + (void)xSemaphoreTake(s_spin_canceller_hold, portMAX_DELAY); +} + +typedef struct { + volatile bool stop; + volatile uint32_t loops; +} canary_ctx_t; + +static void spin_canary_task(void *arg) +{ + canary_ctx_t *ctx = arg; + while (!ctx->stop) { + ctx->loops++; + vTaskDelay(1); + } + vTaskDelete(NULL); +} + +TEST_CASE("BLE Log pool waiter parks instead of self-waking on an unpublished FREE", + "[ble_log][lbm][repro]") +{ + canary_ctx_t canary = {0}; + spin_writer_ctx_t wctx = {.done = xSemaphoreCreateBinary()}; + spin_cancel_ctx_t cctx = {.done = xSemaphoreCreateBinary()}; + s_spin_writer_start = xSemaphoreCreateBinary(); + s_spin_writer_entered = xSemaphoreCreateBinary(); + s_spin_canceller_hold = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(wctx.done); + TEST_ASSERT_NOT_NULL(cctx.done); + TEST_ASSERT_NOT_NULL(s_spin_writer_start); + TEST_ASSERT_NOT_NULL(s_spin_writer_entered); + TEST_ASSERT_NOT_NULL(s_spin_canceller_hold); + + /* Fresh pool: the first shared transport must land on id 0 so the + * stale open-cursor hint (still 0 from init) points at it. */ + ble_log_deinit(); + TEST_ASSERT_TRUE(ble_log_init()); + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + TEST_ASSERT_NOT_NULL(ble_log_claim(BLE_LOG_SRC_ENCODE, 1, + &cctx.handle, false)); + TEST_ASSERT_EQUAL(0, cctx.handle & 0xff); + uint32_t pinned[BLE_LOG_POOL_TRANS_CNT] = {0}; + int pinned_cnt = 0; + for (; pinned_cnt < BLE_LOG_POOL_SHARED_CNT - 1; pinned_cnt++) { + TEST_ASSERT_NOT_NULL(ble_log_claim(BLE_LOG_SRC_ENCODE, 1, + &pinned[pinned_cnt], false)); + } + + /* The canary shares the writer's core: it only progresses while the + * writer is parked; a spinning writer monopolizes the core. */ + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreatePinnedToCore(spin_canary_task, "ble_log_canary", + TEST_LIFECYCLE_STACK_SIZE, &canary, + 1, NULL, 1)); + vTaskDelay(pdMS_TO_TICKS(20)); + uint32_t canary_base = canary.loops; + TEST_ASSERT_GREATER_THAN(0, canary_base); + + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreatePinnedToCore(spin_writer_task, "ble_log_spin", + TEST_LIFECYCLE_STACK_SIZE, &wctx, + TEST_LIFECYCLE_PRIO + 1, NULL, 1)); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreatePinnedToCore(spin_canceller_task, "ble_log_cancel", + TEST_LIFECYCLE_STACK_SIZE, &cctx, + TEST_LIFECYCLE_PRIO - 1, NULL, 0)); + + TEST_ASSERT_TRUE(xSemaphoreTake(s_spin_writer_entered, pdMS_TO_TICKS(1000))); + /* Let the writer reach its steady state (parked or spinning), then + * sample the canary across a second window: a parked writer leaves + * the core to the canary; a spinning writer monopolizes it. */ + vTaskDelay(pdMS_TO_TICKS(30)); + uint32_t canary_settled = canary.loops; + vTaskDelay(pdMS_TO_TICKS(100)); + uint32_t canary_now = canary.loops; + bool completed_early = xSemaphoreTake(wctx.done, 0) == pdTRUE; + printf("B1 sample: writer completed_early=%d canary %u -> %u during hold (core1: %s)\n", + completed_early, (unsigned)canary_settled, (unsigned)canary_now, + canary_now == canary_settled ? "MONOPOLIZED by spinning writer" : "progressing, writer parked"); + + /* Let the suspended publisher finish the recycle and observe the + * recovery: the writer must complete once the bitmap is published. */ + xSemaphoreGive(s_spin_canceller_hold); + TEST_ASSERT_TRUE(xSemaphoreTake(cctx.done, pdMS_TO_TICKS(1000))); + TEST_ASSERT_TRUE(xSemaphoreTake(wctx.done, pdMS_TO_TICKS(1000))); + TEST_ASSERT_TRUE(wctx.result); + + for (int i = 0; i < pinned_cnt; i++) { + ble_log_commit(pinned[i], 0); + } + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + + canary.stop = true; + vTaskDelay(pdMS_TO_TICKS(5)); + vSemaphoreDelete(wctx.done); + vSemaphoreDelete(cctx.done); + vSemaphoreDelete(s_spin_writer_start); + vSemaphoreDelete(s_spin_writer_entered); + vSemaphoreDelete(s_spin_canceller_hold); + s_spin_writer_start = NULL; + s_spin_writer_entered = NULL; + s_spin_canceller_hold = NULL; + + /* Contract: while its only exit is suspended, the waiter must be + * PARKED (canary progresses across the hold window), never spinning + * on self-minted tokens (canary frozen = core monopolized). */ + TEST_ASSERT_GREATER_THAN(canary_settled, canary_now); +} + +/* ------------------------------------------------------------------ */ +/* Review blocker repro: flush drain straddle. */ +/* The drain loop reads the writer refcount and the waiter count as */ +/* two separate loads. A writer waking between them is in-flight (ref */ +/* re-acquired, waiter unregistered, gate not yet checked) when the */ +/* drain concludes; flush then snapshots and re-enables producers */ +/* while the pre-existing writer still owns its reference. */ +/* ------------------------------------------------------------------ */ +static volatile bool s_straddle_acquire_armed; +static volatile bool s_straddle_flush_armed; +static volatile int s_straddle_flush_evals; +static volatile bool s_straddle_hog_stop; +static volatile bool s_straddle_writer_frozen_at_return; +static volatile bool s_straddle_writer_result; +static volatile int64_t s_straddle_flush_us; +static SemaphoreHandle_t s_straddle_freeze; +static SemaphoreHandle_t s_straddle_inflight; +static SemaphoreHandle_t s_straddle_flush_start; +static SemaphoreHandle_t s_straddle_flush_done; +static SemaphoreHandle_t s_straddle_writer_started; +static SemaphoreHandle_t s_straddle_writer_done; +static SemaphoreHandle_t s_straddle_reader_start; + +void ble_log_test_acquire_after_unregister_hook(void); +void ble_log_test_flush_drain_between_loads_hook(void); +static void straddle_hog_task(void *arg); + +void ble_log_test_acquire_after_unregister_hook(void) +{ + if (!s_straddle_acquire_armed) { + return; + } + s_straddle_acquire_armed = false; + /* The writer is in-flight: reference re-acquired, waiter + * unregistered, producer gate not yet observed. Any preemption of + * this task leaves exactly this state behind. */ + xSemaphoreGive(s_straddle_inflight); + (void)xSemaphoreTake(s_straddle_freeze, portMAX_DELAY); +} + +void ble_log_test_flush_drain_between_loads_hook(void) +{ + if (!s_straddle_flush_armed) { + return; + } + switch (++s_straddle_flush_evals) { + case 1: + /* Evaluation #1, before this iteration's wake_all: start a hog on + * the writer's core (idle: the writer is parked) so the wake token + * minted moments later stays unconsumed. */ + (void)xTaskCreatePinnedToCore(straddle_hog_task, "ble_log_strh", + TEST_LIFECYCLE_STACK_SIZE, NULL, + TEST_LIFECYCLE_PRIO + 2, NULL, 0); + break; + case 2: + /* Evaluation #2, between the two condition loads: stop the hog so + * the parked writer consumes the wake and runs its real post-take + * handoff steps (re-acquire + unregister) inside this window. + * Only now may the link recycle transports: the writer is + * in-flight and unregistered, so recycles mint no wake tokens. */ + s_straddle_flush_armed = false; + s_straddle_hog_stop = true; + if (xSemaphoreTake(s_straddle_inflight, pdMS_TO_TICKS(2000)) == pdTRUE) { + xSemaphoreGive(s_straddle_reader_start); + } + break; + default: + break; + } +} + +static void straddle_writer_task(void *arg) +{ + (void)arg; + xSemaphoreGive(s_straddle_writer_started); + const uint8_t marker = 0xD2; + s_straddle_writer_result = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &marker, 1); + xSemaphoreGive(s_straddle_writer_done); + vTaskDelete(NULL); +} + +static void straddle_flusher_task(void *arg) +{ + (void)arg; + (void)xSemaphoreTake(s_straddle_flush_start, portMAX_DELAY); + int64_t t0 = esp_timer_get_time(); + ble_log_flush(); + s_straddle_flush_us = esp_timer_get_time() - t0; + s_straddle_writer_frozen_at_return = !s_straddle_acquire_armed; + xSemaphoreGive(s_straddle_flush_done); + vTaskDelete(NULL); +} + +static void straddle_hog_task(void *arg) +{ + (void)arg; + while (!s_straddle_hog_stop) { + } + vTaskDelete(NULL); +} + +static volatile bool s_straddle_reader_stop; + +static void straddle_reader_task(void *arg) +{ + (void)arg; + /* Recycle transports the way a real link does, so flush_all_trans + * completes and the measured flush duration reflects the writer + * drain rather than the transport wait. Blocked until the drain has + * handed the writer off: recycling earlier would wake the parked + * writer before the straddle point. */ + (void)xSemaphoreTake(s_straddle_reader_start, portMAX_DELAY); + while (!s_straddle_reader_stop) { + (void)ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(10), 0, NULL); + } + vTaskDelete(NULL); +} + +TEST_CASE("BLE Log flush drain does not straddle an in-flight parked writer", + "[ble_log][lbm][repro]") +{ + static const uint8_t full_payload[ + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t)] = {0}; + + s_straddle_freeze = xSemaphoreCreateBinary(); + s_straddle_inflight = xSemaphoreCreateBinary(); + s_straddle_flush_start = xSemaphoreCreateBinary(); + s_straddle_flush_done = xSemaphoreCreateBinary(); + s_straddle_writer_started = xSemaphoreCreateBinary(); + s_straddle_writer_done = xSemaphoreCreateBinary(); + s_straddle_reader_start = xSemaphoreCreateBinary(); + TEST_ASSERT_NOT_NULL(s_straddle_freeze); + TEST_ASSERT_NOT_NULL(s_straddle_inflight); + TEST_ASSERT_NOT_NULL(s_straddle_flush_start); + TEST_ASSERT_NOT_NULL(s_straddle_flush_done); + TEST_ASSERT_NOT_NULL(s_straddle_writer_started); + TEST_ASSERT_NOT_NULL(s_straddle_writer_done); + TEST_ASSERT_NOT_NULL(s_straddle_reader_start); + + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + /* Keep every task-usable transport SENDING in the test peripheral. */ + for (int i = 0; i < BLE_LOG_POOL_SHARED_CNT; i++) { + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, full_payload, + sizeof(full_payload))); + } + TEST_ASSERT_TRUE(ble_log_rt_drain()); + + /* The writer parks on the full shared pool. */ + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreatePinnedToCore(straddle_writer_task, "ble_log_strw", + TEST_LIFECYCLE_STACK_SIZE, NULL, + TEST_LIFECYCLE_PRIO, NULL, 0)); + TEST_ASSERT_TRUE(xSemaphoreTake(s_straddle_writer_started, pdMS_TO_TICKS(1000))); + vTaskDelay(pdMS_TO_TICKS(10)); + + /* The eval#1 hook starts the hog on the writer's core so the first + * wake stays unconsumed; the eval#2 hook stops it exactly between + * the two condition loads. */ + s_straddle_flush_evals = 0; + s_straddle_flush_armed = true; + s_straddle_acquire_armed = true; + s_straddle_hog_stop = false; + s_straddle_reader_stop = false; + s_straddle_writer_frozen_at_return = false; + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreate(straddle_reader_task, "ble_log_strr", + TEST_READER_STACK_SIZE, NULL, + TEST_READER_PRIO, NULL)); + TEST_ASSERT_EQUAL(pdTRUE, + xTaskCreatePinnedToCore(straddle_flusher_task, "ble_log_strf", + TEST_READER_STACK_SIZE, NULL, + TEST_READER_PRIO + 2, NULL, 1)); + xSemaphoreGive(s_straddle_flush_start); + + TEST_ASSERT_TRUE(xSemaphoreTake(s_straddle_flush_done, pdMS_TO_TICKS(10000))); + int64_t flush_ms = s_straddle_flush_us / 1000; + bool writer_done_early = xSemaphoreTake(s_straddle_writer_done, 0) == pdTRUE; + printf("B2 sample: flush returned in %lld ms, writer in-flight at return=%d, writer already done=%d\n", + (long long)flush_ms, s_straddle_writer_frozen_at_return, + writer_done_early); + + /* Transports are already claimable (the reader kept the link busy); + * release the frozen writer: on the straddle it observes the + * re-enabled gate and completes its pre-flush write after flush + * already returned. */ + xSemaphoreGive(s_straddle_freeze); + TEST_ASSERT_TRUE(xSemaphoreTake(s_straddle_writer_done, pdMS_TO_TICKS(1000))); + printf("B2 outcome: frozen writer passed the re-enabled gate and wrote=%d\n", + s_straddle_writer_result); + + s_straddle_reader_stop = true; + vTaskDelay(pdMS_TO_TICKS(30)); + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + + vSemaphoreDelete(s_straddle_freeze); + vSemaphoreDelete(s_straddle_inflight); + vSemaphoreDelete(s_straddle_flush_start); + vSemaphoreDelete(s_straddle_flush_done); + vSemaphoreDelete(s_straddle_writer_started); + vSemaphoreDelete(s_straddle_writer_done); + vSemaphoreDelete(s_straddle_reader_start); + s_straddle_freeze = NULL; + s_straddle_inflight = NULL; + s_straddle_flush_start = NULL; + s_straddle_flush_done = NULL; + s_straddle_writer_started = NULL; + s_straddle_writer_done = NULL; + s_straddle_reader_start = NULL; + + /* Contract: when a writer was in-flight at flush return, flush must + * have held the drain for the full documented timeout (1s) instead + * of concluding between the two counter loads. */ + if (s_straddle_writer_frozen_at_return) { + TEST_ASSERT_GREATER_OR_EQUAL(950, (int)flush_ms); + } +} From e3e3ac6541c411234d9704fab2beb2bb3e36efff Mon Sep 17 00:00:00 2001 From: Zhou Xiao Date: Sun, 6 Sep 2026 16:12:27 +0800 Subject: [PATCH 12/13] test(ble_log): soak the park/wake handshake and fix observer latching - concurrent-writer integrity and park/wake soak cases: many writers across shared and reserve transports keep frames intact and SNs unique through continuous park/wake turnover; - the deinit race case wakes parked writers before joining, so the join cannot deadlock on a task that must first observe the closed gate; - the perf observer latches its final counters only from FLUSH snapshots: periodic frames arriving after the FLUSH snapshot carry interval counters that the flush reset, and must not overwrite the final result; - README describes the timestamp prefix by writer path. --- components/bt/common/ble_log/README.md | 24 +- .../main/test_ble_log_perf.c | 17 +- .../main/test_ble_log_runtime.c | 11 +- .../ble_log_test/main/test_ble_log_rt.c | 211 ++++++++++++++++++ 4 files changed, 244 insertions(+), 19 deletions(-) diff --git a/components/bt/common/ble_log/README.md b/components/bt/common/ble_log/README.md index c8506fef349..fa86105ae0c 100644 --- a/components/bt/common/ble_log/README.md +++ b/components/bt/common/ble_log/README.md @@ -60,19 +60,21 @@ The checksum is `ble_log_fast_checksum()` over the six-byte header and exactly `payload length` bytes. It excludes the checksum field and peripheral-only DMA padding. -Core frames (all sources except REDIR) begin their payload with: +Core frame payloads follow the writer entry point, not a single rule: -```text -[4-byte low32 esp_timer_get_time() microseconds][source payload] -``` +- Public write / claim path (`write_hex`, compressed encoder): the API + prepends a `[4-byte low32 esp_timer_get_time() microseconds]` before + the source payload. The timestamp is captured at API entry before any + pool mutex wait. It wraps approximately every 71.6 minutes and must be + unwrapped modulo 2^32 by the receiver. +- LL callback path (`write_hex_ll`, covering the LL_TASK / LL_HCI / + LL_ISR sources): controller payloads are forwarded raw, with no ESP + timestamp prefix — the on-wire layout is exactly what the controller + handed over. -The timestamp is captured at API entry before any pool mutex wait. It wraps -approximately every 71.6 minutes and must be unwrapped modulo 2^32 by the -receiver. - -UART0 redirection payload is the raw console stream with no timestamp -prefix: the redirection stream keeps its own frame sequence, and its -receiver-side arrival time (aggregation delay bounded by the periodic +UART0 redirection payload is likewise the raw console stream with no +timestamp prefix: the redirection stream keeps its own frame sequence, and +its receiver-side arrival time (aggregation delay bounded by the periodic redirection flush) is the alignment reference against the core timeline. ### Sources diff --git a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c index 95d02496988..33f7fb8af86 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c +++ b/components/bt/common/ble_log/test_apps/ble_log_perf_test/main/test_ble_log_perf.c @@ -492,13 +492,18 @@ static void observe_perf_frame(const test_ble_log_frame_t *frame, void *ctx) if (snapshot.int_src_code != BLE_LOG_INT_SRC_SNAPSHOT) { return; } - for (int i = 0; i < BLE_LOG_SRC_CORE_COUNT; i++) { - int src = BLE_LOG_SRC_CORE_FIRST + i; - sink->stat_written[src] = snapshot.stats[i].written_frame_cnt; - sink->stat_lost[src] = snapshot.stats[i].lost_frame_cnt; + /* Latch the final counters only from the FLUSH snapshot: periodic + * snapshots keep arriving after it and carry interval counters + * that the flush reset; letting them through would overwrite the + * final result the run report and assertions rely on. */ + if (snapshot.reason_flags & BLE_LOG_SNAPSHOT_REASON_FLUSH) { + for (int i = 0; i < BLE_LOG_SRC_CORE_COUNT; i++) { + int src = BLE_LOG_SRC_CORE_FIRST + i; + sink->stat_written[src] = snapshot.stats[i].written_frame_cnt; + sink->stat_lost[src] = snapshot.stats[i].lost_frame_cnt; + } + sink->stat_final_seen = true; } - sink->stat_final_seen = - (snapshot.reason_flags & BLE_LOG_SNAPSHOT_REASON_FLUSH) != 0; } } diff --git a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c index d1067cf1e9e..87217c87276 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c +++ b/components/bt/common/ble_log/test_apps/ble_log_rt_test/main/test_ble_log_runtime.c @@ -932,6 +932,13 @@ TEST_CASE("BLE Log runtime survives deinit racing submissions", /* Stop the writer and join with a bound before asserting so a Unity * longjmp cannot leak the writer into later tests. */ __atomic_store_n(&ctx->stop, true, __ATOMIC_RELEASE); + /* A writer parked inside ble_log_claim() cannot observe stop: only the + * deinit waiter wake returns it. Deinit here, before the join, so its + * drain closes the producer gate, wakes any parked writer and waits + * for it to leave the pool API. The vTaskDelete fallback below can + * then never cut a task down inside pool bookkeeping (which would + * leak waiting_task_count and hang the recovery deinit). */ + ble_log_deinit(); const int64_t join_deadline_us = esp_timer_get_time() + (int64_t)RT_JOIN_TIMEOUT_MS * 1000; while (!__atomic_load_n(&ctx->exited, __ATOMIC_ACQUIRE)) { @@ -949,8 +956,8 @@ TEST_CASE("BLE Log runtime survives deinit racing submissions", /* Recover module state before any assertion can abort the test: a * failed re-init leaves the module deinit-ed and tearDown does not - * restore it, which would cascade into every later test. */ - ble_log_deinit(); + * restore it, which would cascade into every later test. Deinit ran + * before the join (see above); only the init half remains. */ bool recovered = ble_log_init(); reinit_ok = reinit_ok && recovered; diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c index c976ad3c219..02288e3a387 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c @@ -1748,6 +1748,217 @@ static size_t count_unique_sn(const uint32_t *sn, size_t n) return unique; } +TEST_CASE("BLE Log concurrent writers keep frames intact and SNs unique", + "[ble_log][lbm]") +{ + /* Scenario: 4 writers run in parallel across all cores, each emitting + * CONC_FRAMES_EACH marked frames while the pool recycles under them. + * Guards (mutation -> expected failure): + * - per-transport CAS lock weakened to plain test-and-set: two cores + * co-own one transport, frames interleave -> checksum walk fails. + * - Global SN fetch_add weakened to plain increment: concurrent RMWs + * lose updates -> duplicate SNs -> unique-count assertion fails. + * Every completed write must be present exactly once. */ + TEST_ASSERT_TRUE(ble_log_enable(true)); + ble_log_lbm_flush_open_trans(); + for (int round = 0; round < 2; round++) { + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + } + + conc_writer_ctx_t ctx[CONC_WRITER_CNT]; + static conc_capture_t cap; + memset(&cap, 0, sizeof(cap)); + for (int w = 0; w < CONC_WRITER_CNT; w++) { + ctx[w] = (conc_writer_ctx_t){ + .done = xSemaphoreCreateBinary(), + .marker = (uint8_t)(0xa0 + w), + .result = true, + }; + TEST_ASSERT_NOT_NULL(ctx[w].done); + } + for (int w = 0; w < CONC_WRITER_CNT; w++) { + TEST_ASSERT_EQUAL(pdTRUE, xTaskCreatePinnedToCore( + conc_writer_task, "ble_log_conc", + TEST_LIFECYCLE_STACK_SIZE, &ctx[w], + TEST_LIFECYCLE_PRIO, NULL, + w % portNUM_PROCESSORS)); + } + /* Feed the pool while the writers run; the main task is the + * consumer. */ + for (;;) { + bool all_done = true; + for (int w = 0; w < CONC_WRITER_CNT; w++) { + if (!ctx[w].finished) { + if (xSemaphoreTake(ctx[w].done, 0) == pdTRUE) { + ctx[w].finished = true; + TEST_ASSERT_TRUE(ctx[w].result); + vSemaphoreDelete(ctx[w].done); + } else { + all_done = false; + } + } + } + if (all_done) { + break; + } + TEST_ASSERT_TRUE(ble_log_rt_drain()); + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(20), 0, NULL); + if (len) { + TEST_ASSERT_TRUE(test_ble_log_walk_frames( + s_read_buf, len, capture_conc_frame, &cap)); + } + } + + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + for (;;) { + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + TEST_ASSERT_TRUE(test_ble_log_walk_frames(s_read_buf, len, + capture_conc_frame, &cap)); + } + TEST_ASSERT_EQUAL_size_t(CONC_WRITER_CNT * CONC_FRAMES_EACH, cap.frames); + for (int w = 0; w < CONC_WRITER_CNT; w++) { + TEST_ASSERT_EQUAL_size_t(CONC_FRAMES_EACH, cap.per_marker[w]); + } + /* Unique Global SNs: a lost fetch_add update surfaces as two frames + * sharing one SN. */ + TEST_ASSERT_EQUAL_size_t(CONC_WRITER_CNT * CONC_FRAMES_EACH, + count_unique_sn(cap.sn, cap.sn_count)); + + /* Phase 2 - SN hammer: wait=false claims in tight loops across all + * cores. The SN fetch_add window dominates the failing-claim path, so + * a plain (non-atomic) increment loses updates at a high rate. Losses + * are expected and allowed; only SN uniqueness is asserted. */ + enum { HAMMER_TASKS = 4 }; + hammer_ctx_t hctx[HAMMER_TASKS]; + for (int t = 0; t < HAMMER_TASKS; t++) { + hctx[t] = (hammer_ctx_t){.done = xSemaphoreCreateBinary()}; + TEST_ASSERT_NOT_NULL(hctx[t].done); + TEST_ASSERT_EQUAL(pdTRUE, xTaskCreatePinnedToCore( + hammer_claim_task, "ble_log_hammer", + TEST_LIFECYCLE_STACK_SIZE, &hctx[t], + TEST_LIFECYCLE_PRIO, NULL, + t % portNUM_PROCESSORS)); + } + /* Feed the pool while the hammers run. */ + static hammer_capture_t hcap; + memset(&hcap, 0, sizeof(hcap)); + static bool taken[HAMMER_TASKS]; + memset(taken, 0, sizeof(taken)); + for (int finished = 0; finished < HAMMER_TASKS;) { + for (int t = 0; t < HAMMER_TASKS; t++) { + if (!taken[t] && + xSemaphoreTake(hctx[t].done, 0) == pdTRUE) { + taken[t] = true; + finished++; + vSemaphoreDelete(hctx[t].done); + } + } + TEST_ASSERT_TRUE(ble_log_rt_drain()); + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(2), 0, NULL); + if (len) { + TEST_ASSERT_TRUE(test_ble_log_walk_frames( + s_read_buf, len, capture_hammer_frame, &hcap)); + } + } + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + for (;;) { + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + if (!len) { + break; + } + TEST_ASSERT_TRUE(test_ble_log_walk_frames(s_read_buf, len, + capture_hammer_frame, &hcap)); + } + TEST_ASSERT_GREATER_THAN_size_t(0, hcap.sn_count); + TEST_ASSERT_EQUAL_size_t(hcap.sn_count, + count_unique_sn(hcap.sn, hcap.sn_count)); +} + +typedef struct { + SemaphoreHandle_t done; + bool result; +} soak_writer_ctx_t; + +static void soak_writer_task(void *arg) +{ + soak_writer_ctx_t *ctx = arg; + const uint8_t marker = 0x5c; + ctx->result = ble_log_write_hex(BLE_LOG_SRC_CUSTOM, &marker, 1); + xSemaphoreGive(ctx->done); + vTaskDelete(NULL); +} + +TEST_CASE("BLE Log waiter handshake survives continuous park/wake turnover", + "[ble_log][lbm]") +{ + /* Scenario: each round fills every shared transport, parks two writers, + * then recycles transports one at a time. Each single recycle is the + * last-availability event for exactly one waiter: if the register/ + * publish SC-fence handshake is weakened, a waiter can miss the final + * notification and stay parked while capacity exists. + * Mutation (drop the SEQ_CST fences in the waiter/notify handshake) + * -> probabilistic: some round times out. This is a soak: it raises + * the hit probability, it cannot make the window deterministic. */ + TEST_ASSERT_TRUE(ble_log_enable(true)); + static const uint8_t full[ + BLE_LOG_MAX_PAYLOAD_LEN - sizeof(uint32_t)] = {0}; + + for (int round = 0; round < 64; round++) { + ble_log_lbm_flush_open_trans(); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + while (ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + 0, 0, NULL) > 0) { + } + for (int i = 0; i < BLE_LOG_POOL_SHARED_CNT; i++) { + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, full, + sizeof(full))); + } + soak_writer_ctx_t w[2]; + for (int i = 0; i < 2; i++) { + w[i] = (soak_writer_ctx_t){ + .done = xSemaphoreCreateBinary(), + .result = false, + }; + TEST_ASSERT_NOT_NULL(w[i].done); + TEST_ASSERT_EQUAL(pdTRUE, xTaskCreatePinnedToCore( + soak_writer_task, "ble_log_soak", + TEST_LIFECYCLE_STACK_SIZE, &w[i], + TEST_LIFECYCLE_PRIO, NULL, + i % portNUM_PROCESSORS)); + } + /* Let both writers scan twice and park. */ + vTaskDelay(pdMS_TO_TICKS(20)); + /* Recycle exactly one transport at a time: each read is one + * waiter's last chance to be notified. */ + TEST_ASSERT_TRUE(ble_log_rt_drain()); + for (int i = 0; i < 2; i++) { + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), + 0, NULL); + TEST_ASSERT_GREATER_THAN_size_t(0, len); + } + for (int i = 0; i < 2; i++) { + TEST_ASSERT_TRUE(xSemaphoreTake(w[i].done, pdMS_TO_TICKS(2000))); + TEST_ASSERT_TRUE(w[i].result); + vSemaphoreDelete(w[i].done); + } + } +} + typedef struct { SemaphoreHandle_t done; bool write_result; From 7a07b79fc6504014c2197cea1a6cd948a0b9ca99 Mon Sep 17 00:00:00 2001 From: guozifan Date: Wed, 9 Sep 2026 20:01:15 +0800 Subject: [PATCH 13/13] fix(ble_log): extend snapshot accounting and correct pool flushing Count successful frame bytes and share Global SN with snapshots. Track snapshot attempts with a separate 24-bit anchor_count. Keep protocol v8. Scan writer-held buffers during periodic flush and require a published FREE bit. Keep UART0 periodic draining active when producers are disabled. Document late pending-seal hints and update existing snapshot checks. --- components/bt/common/ble_log/README.md | 71 ++++++--- .../bt/common/ble_log/src/ble_log_lbm_v2.c | 66 +++++--- .../ble_log/src/ble_log_task_registry.c | 2 +- .../src/internal_include/ble_log_lbm_v2.h | 42 ++--- .../src/internal_include/ble_log_prph.h | 4 +- .../ble_log/src/prph/ble_log_prph_uart_dma.c | 4 +- .../ble_log/test_apps/ble_log_test/README.md | 3 +- .../ble_log_test/main/test_ble_log_rt.c | 143 +++++++++++++++--- 8 files changed, 247 insertions(+), 88 deletions(-) diff --git a/components/bt/common/ble_log/README.md b/components/bt/common/ble_log/README.md index fa86105ae0c..5fe0c793285 100644 --- a/components/bt/common/ble_log/README.md +++ b/components/bt/common/ble_log/README.md @@ -29,6 +29,19 @@ writers retain reserve access and fail-fast behavior. The Internal Snapshot and UART0 redirection transports are not members of the bitmap pool. +Periodic flushing scans the whole bounded pool, not just OPEN bitmap hints: +writers temporarily remove their hints while writing or holding a claim. A +failed try-lock leaves `pending_seal`; the next claim seals the old buffer +before appending, or a later periodic scan seals it when unlocked. This is a +deferred request, not a snapshot barrier. Seal and recycle clear the flag, +but a delayed flusher can set it afterwards; an extra early partial seal is +allowed. Full flush/deinit retain OPEN-only scans after writers drain. + +FREE acquisition requires the lock, FREE state, and atomically removing an +actually published FREE bit. A cached candidate alone cannot bypass the +recycler's state-to-bitmap publication window. UART0 periodic flushing is +independent of producer enable: disable stops new writes, not cached output. + ## Runtime dispatch Sealed transports enter one bounded queue. The first submission anchors a @@ -94,15 +107,18 @@ source IDs of protocol v8 frames: 8 REDIR extension ``` -Log sources except INTERNAL and REDIR share one 24-bit Global SN: it is -consumed at API entry, so it totally orders log attempts — including -equal-timestamp records from different sources — and every lost or rejected -attempt leaves a gap in the sequence. Internal Snapshot frames carry their -own separate sequence (a gap counts skipped snapshots), the periodic -task-binding broadcast keeps its own sequence as well (a gap counts a -skipped broadcast window), and the REDIR console stream keeps its own -sequence too (a gap counts a dropped console batch). `ble_log_init()` -resets all the sequences, and its required `INIT` snapshot starts a new +Core log sources (`CUSTOM` through `ENCODE`) and Internal Snapshots share one +24-bit Global SN. Core logs consume it after entry validation and gate acceptance, +before buffer contention; lost log attempts leave gaps accounted for by +`lost_frame_cnt`. Snapshots consume it only after acquiring their dedicated +transport, when assembling the frame. Busy/skipped snapshots do not consume a +Global SN and are not included in `lost_frame_cnt`. +Snapshots also carry a separate 24-bit `anchor_count` in their payload: +only snapshot attempts advance this counter, including skipped snapshots. +The periodic task-binding broadcast and REDIR console stream retain their +own separate header sequences (gaps count skipped binding windows or dropped +console batches). `ble_log_init()` resets all sequences and the anchor counter, +and its required `INIT` snapshot starts a new receiver epoch. They remain continuous through `FLUSH` within that epoch. Callers must not write until `ble_log_init()` returns, so the `INIT` snapshot is submitted first. @@ -119,14 +135,15 @@ bit 7. Disabling HCI logging suppresses records from both capture paths. ## Internal Snapshot All BLE Log-owned internal information is emitted as fixed-layout frames -from dedicated transports: the snapshot frame is 148 logical bytes on its -own transport, and the periodic task-binding broadcast has its own +from dedicated transports: the snapshot frame is 179 logical bytes on its +180-byte aligned transport, and the periodic task-binding broadcast has its own transport sized to the registry (one full binding frame per window; `CONFIG_BLE_LOG_TASK_ID_MAX` entries of 19 bytes plus a timestamp). The snapshot contains: - reason flags (`INIT`, `PERIODIC`, `FLUSH`, `TS_VALID`); +- a 24-bit little-endian `anchor_count` (three bytes), immediately after the reason flags; - the complete 58-byte build, library, chip, and protocol version block; - the LC clock, ESP Timer, and FreeRTOS tick samples captured together with the GPIO sync level; @@ -136,26 +153,40 @@ The snapshot contains: The periodic snapshot is an always-on system behavior from initialization until deinitialization. It cannot be stopped by the TS sync IO control API. -A skipped periodic snapshot burns one snapshot SN instead: the gap in the -snapshot sequence is the loss signal, and it is visible directly in the -frame header without a dedicated payload field. +A skipped periodic snapshot advances only the anchor count, not the Global SN. +Anchor counts start at zero for INIT, also advance for FLUSH, and wrap modulo +2^24 on the wire (the atomic runtime counter remains uint32_t). +A gap between anchor counts identifies missed snapshot attempts without +mistaking intervening ordinary logs for missed snapshots. Each source statistic contains only: ```c uint32_t written_frame_cnt; uint32_t lost_frame_cnt; +uint32_t written_bytes_cnt; ``` Successful counts are incremented only after a complete frame is committed to -a transport; they do not imply confirmed physical TX. Core counts and the pool -peak restart after a completed FLUSH; the Global SN and the snapshot -sequence remain continuous. A busy periodic Internal +a transport; they do not imply confirmed physical TX. `written_bytes_cnt` +counts the full logical frame (six-byte header, payload including any timestamp +prefix, and four-byte checksum), excluding peripheral DMA padding. Rejected or +aborted writes do not add bytes; there is no lost-byte counter. Internal and +REDIR frames are not included in these per-core-source byte counters. All three +counters are uint32 values and wrap modulo 2^32. +Core counts and the pool peak restart after a completed FLUSH; the Global SN +and anchor count remain continuous. A busy periodic Internal transport is never overwritten; that snapshot is skipped and counted. Required INIT and FLUSH snapshots use a bounded task-context wait. -The snapshot is not an Anchor: it does not seal the shared pool and does not -define a Log Segment boundary. +The protocol version remains v8 despite the changed snapshot payload layout and +header sequence semantics. Receivers must be updated together with the producer; +the version byte alone cannot distinguish the old and new layouts. +The snapshot remains a sampling point, +not a confirmed TX boundary: periodic OPEN flushing is best-effort, statistics +are sampled individually while writers can run, and Global SN allocation order +is not submission order. Neither deferred sealing nor the anchor count makes +snapshot differences an exact SN-interval accounting boundary. ## Public API @@ -229,7 +260,7 @@ window, one INTERNAL frame (`BLE_LOG_INT_SRC_TASK_BINDING`) packs one fixed-layout record per registered entry, binding each id to its task name. It rides the registry's own dedicated transport with a sequence of its own (a window skipped despite a non-empty registry leaves a gap in the binding -sequence, never in the snapshot sequence), is never counted in the +sequence, never in the Global SN or snapshot anchor count), is never counted in the per-source written/lost stats, and never contends with the snapshot transport or with user records for pool transports. A receiver that joined late or lost a frame converges on the next window; a record of a new task diff --git a/components/bt/common/ble_log/src/ble_log_lbm_v2.c b/components/bt/common/ble_log/src/ble_log_lbm_v2.c index f25223e036a..f173c1d0146 100644 --- a/components/bt/common/ble_log/src/ble_log_lbm_v2.c +++ b/components/bt/common/ble_log/src/ble_log_lbm_v2.c @@ -74,9 +74,10 @@ _Static_assert(sizeof(ble_log_version_info_t) == 58, typedef struct { ble_log_prph_trans_t *trans[BLE_LOG_POOL_TRANS_CNT]; - /* Hint bitmaps: a set bit means "this buffer is likely FREE/OPEN". The - * real ownership is the per-buffer atomic_lock; bitmaps only accelerate - * candidate lookup and may be transiently stale. */ + /* Bitmaps accelerate lookup; cached candidates may be stale. Ownership + * still requires the per-buffer lock and a state check. FREE acceptance + * also requires atomically taking a published FREE bit: state=FREE alone + * can precede the recycler's bitmap publication. OPEN is only a hint. */ volatile uint32_t free_bitmap; volatile uint32_t open_bitmap; @@ -117,12 +118,12 @@ BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t lbm_enabled = 0; BLE_LOG_STATIC volatile bool flush_in_progress = false; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_pool_t g_pool; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_stat_mgr_t stat_mgr_ctx[BLE_LOG_SRC_MAX]; -/* Global SN (the log sources except INTERNAL and REDIR) and the separate - * Internal Snapshot sequence; see ble_log_lbm_v2.h. */ +/* Global SN (core logs and snapshots) and the snapshot-only anchor count; + * task bindings and REDIR retain independent sequences. */ BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t g_frame_sn; -BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t g_snapshot_sn; +BLE_LOG_STATIC BLE_LOG_DRAM_ATTR uint32_t g_anchor_count; #define BLE_LOG_GET_GLOBAL_SN() BLE_LOG_GET_FRAME_SN(g_frame_sn) -#define BLE_LOG_GET_SNAPSHOT_SN() BLE_LOG_GET_FRAME_SN(g_snapshot_sn) +#define BLE_LOG_GET_ANCHOR_COUNT() BLE_LOG_GET_FRAME_SN(g_anchor_count) BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_prph_trans_t *internal_trans; BLE_LOG_STATIC BLE_LOG_DRAM_ATTR ble_log_pool_claim_t pool_claim_ctx[BLE_LOG_POOL_TRANS_CNT]; BLE_LOG_STATIC ble_log_internal_snapshot_t internal_snapshot; @@ -187,7 +188,8 @@ ble_log_pool_bitmap_set(volatile uint32_t *bitmap, uint8_t id) BLE_LOG_IRAM_ATTR BLE_LOG_STATIC uint32_t ble_log_pool_bitmap_clear(volatile uint32_t *bitmap, uint8_t id) { - /* Clearing a hint publishes no data; ownership is already held by lock. */ + /* Clearing publishes no data; ownership is held by lock. For FREE, the + * returned bit also confirms that recycle publication completed. */ return __atomic_fetch_and(bitmap, ~BIT(id), __ATOMIC_RELAXED); } @@ -297,8 +299,10 @@ ble_log_pool_waiter_adjust(int delta) BLE_LOG_IRAM_ATTR void ble_log_lbm_recycle_trans(ble_log_prph_trans_t *trans) { trans->pos = 0; - /* A marker set while the transport was SENDING must not leak into its - * next lifecycle. */ + /* Clear existing requests. A flusher delayed after a failed try-lock may + * store true after this clear, even in the next lifecycle. That hint may + * cause an extra partial seal; it never grants permission to send a + * buffer still owned by a writer. */ BLE_LOG_ATOMIC_STORE_RELAXED(trans->pending_seal, false); if (trans->owner_kind == BLE_LOG_TRANS_OWNER_INTERNAL) { @@ -419,6 +423,10 @@ ble_log_prph_trans_t *ble_log_pool_try_claim_from(volatile uint32_t *bitmap, uint32_t previous = ble_log_pool_bitmap_clear(bitmap, id); if (bitmap == &g_pool.free_bitmap) { + if (!(previous & BIT(id))) { + ble_log_pool_release_candidate(trans); + continue; + } ble_log_pool_update_peak(previous & ~BIT(id)); } /* Keep packing the same OPEN transport instead of scanning the rest @@ -600,6 +608,8 @@ void ble_log_pool_finish_frame(ble_log_prph_trans_t *trans, uint16_t payload_len trans->pos += payload_len + BLE_LOG_FRAME_OVERHEAD; BLE_LOG_ATOMIC_ADD_RELAXED(stat_mgr->counters.written_frame_cnt, 1); + BLE_LOG_ATOMIC_ADD_RELAXED(stat_mgr->counters.written_bytes_cnt, + payload_len + BLE_LOG_FRAME_OVERHEAD); /* Completion: seal if nearly full, otherwise publish as OPEN. */ if (BLE_LOG_TRANS_FREE_SPACE(trans) <= BLE_LOG_FRAME_OVERHEAD) { @@ -767,7 +777,7 @@ bool ble_log_lbm_init(void) g_pool.open_bitmap = 0; BLE_LOG_MEMSET(stat_mgr_ctx, 0, sizeof(stat_mgr_ctx)); g_frame_sn = 0; - g_snapshot_sn = 0; + g_anchor_count = 0; /* Task registry: fresh epoch (registry, sequence and its dedicated * transport) for the new receiver epoch. */ if (!ble_log_task_registry_init()) { @@ -842,6 +852,8 @@ void ble_log_snapshot_stats(ble_log_source_stat_t *snapshots) BLE_LOG_ATOMIC_LOAD_RELAXED(stat_mgr->counters.written_frame_cnt); snapshots[i].lost_frame_cnt = BLE_LOG_ATOMIC_LOAD_RELAXED(stat_mgr->counters.lost_frame_cnt); + snapshots[i].written_bytes_cnt = + BLE_LOG_ATOMIC_LOAD_RELAXED(stat_mgr->counters.written_bytes_cnt); } } @@ -963,7 +975,16 @@ bool ble_log_internal_snapshot(uint16_t reason_flags, (uint8_t)BLE_LOG_ATOMIC_LOAD_RELAXED(g_pool.inflight_peak); ble_log_snapshot_stats(internal_snapshot.stats); - uint32_t frame_sn = BLE_LOG_GET_SNAPSHOT_SN(); + /* Only a snapshot that acquired its transport consumes a Global SN. + * Busy snapshots are not in the core loss statistics; their gaps belong + * exclusively to anchor_count. The transport lock serializes successful + * snapshots, so no extra critical section is needed for these counters. */ + uint32_t frame_sn = BLE_LOG_GET_GLOBAL_SN(); + uint32_t anchor_count = BLE_LOG_GET_ANCHOR_COUNT(); + for (unsigned i = 0; i < sizeof(internal_snapshot.anchor_count); i++) { + internal_snapshot.anchor_count[i] = (uint8_t)(anchor_count >> (8U * i)); + } + ble_log_frame_head_t frame_head = { .length = sizeof(timestamp) + sizeof(internal_snapshot), .frame_meta = BLE_LOG_MAKE_FRAME_META(BLE_LOG_SRC_INTERNAL, frame_sn), @@ -987,9 +1008,7 @@ bool ble_log_internal_snapshot(uint16_t reason_flags, return true; lost: - /* A skipped snapshot burns one snapshot SN: the gap in the snapshot - * sequence is the loss signal. */ - (void)BLE_LOG_GET_SNAPSHOT_SN(); + (void)BLE_LOG_GET_ANCHOR_COUNT(); failed: BLE_LOG_REF_COUNT_RELEASE(&lbm_ref_count); return false; @@ -1023,14 +1042,13 @@ void ble_log_lbm_flush_open_trans(void) } for (int id = 0; id < BLE_LOG_POOL_TRANS_CNT; id++) { - if (!(BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.open_bitmap) & BIT(id))) { - continue; - } + /* Scan the bounded pool, including writers absent from OPEN hints. */ ble_log_prph_trans_t *trans = g_pool.trans[id]; #if CONFIG_BLE_LOG_PRPH_TEST - /* Test point: the OPEN hint was just observed; a test can play the - * other core before this scan takes the candidate lock. */ - if (ble_log_test_flush_hint_seen_hook) { + /* Preserve the stale-OPEN-hint test's target without filtering the + * production scan. Unrelated FREE candidates must not fire its hooks. */ + bool had_open_hint = BLE_LOG_ATOMIC_LOAD_ACQUIRE(g_pool.open_bitmap) & BIT(id); + if (had_open_hint && ble_log_test_flush_hint_seen_hook) { ble_log_test_flush_hint_seen_hook((uint8_t)id); } #endif @@ -1050,7 +1068,7 @@ void ble_log_lbm_flush_open_trans(void) #if CONFIG_BLE_LOG_PRPH_TEST /* Test point: this scan holds a candidate lock taken on a * stale hint; the transport state is not OPEN. */ - if (ble_log_test_flush_stale_locked_hook) { + if (had_open_hint && ble_log_test_flush_stale_locked_hook) { ble_log_test_flush_stale_locked_hook(); } #endif @@ -1192,11 +1210,11 @@ void ble_log_flush(void) } /* FLUSH is not a segment boundary: reset interval counters while the - * Global SN and the snapshot sequence stay continuous. Snapshot loss is - * lifecycle-cumulative in the snapshot sequence and is preserved. */ + * Global SN and snapshot anchor count stay continuous. */ for (int i = 0; i < BLE_LOG_SRC_MAX; i++) { BLE_LOG_ATOMIC_STORE_RELAXED(stat_mgr_ctx[i].counters.written_frame_cnt, 0); BLE_LOG_ATOMIC_STORE_RELAXED(stat_mgr_ctx[i].counters.lost_frame_cnt, 0); + BLE_LOG_ATOMIC_STORE_RELAXED(stat_mgr_ctx[i].counters.written_bytes_cnt, 0); } BLE_LOG_ATOMIC_STORE_RELAXED(g_pool.inflight_peak, 0); #if BLE_LOG_UART_REDIR_ENABLED diff --git a/components/bt/common/ble_log/src/ble_log_task_registry.c b/components/bt/common/ble_log/src/ble_log_task_registry.c index 537833a594b..5a1d8130d28 100644 --- a/components/bt/common/ble_log/src/ble_log_task_registry.c +++ b/components/bt/common/ble_log/src/ble_log_task_registry.c @@ -36,7 +36,7 @@ BLE_LOG_STATIC volatile uint8_t s_task_cnt; BLE_LOG_STATIC ble_log_atomic_lock_t s_task_reg_lock; /* The binding broadcast keeps its own 24-bit frame sequence, separate - * from the snapshot sequence (see ble_log_lbm_v2.h): a gap counts a + * from the log/snapshot Global SN (see ble_log_lbm_v2.h): a gap counts a * broadcast window skipped despite a non-empty registry, never a lost * snapshot. ble_log_task_registry_init resets it. */ BLE_LOG_STATIC uint32_t g_task_binding_sn; diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h index da8dc7047f5..fdace26460d 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_lbm_v2.h @@ -64,9 +64,8 @@ typedef struct { * source IDs: the frame source byte carries the bare enum value. */ /* Statistic slots in the Internal Snapshot: every public source that can - * produce frames, i.e. CUSTOM through ENCODE. INTERNAL frames carry their - * own snapshot sequence in the frame header; REDIR is a raw console - * stream. */ + * produce core frames, i.e. CUSTOM through ENCODE. Snapshots share the + * Global SN but have no statistic slot; REDIR is a raw console stream. */ #define BLE_LOG_SRC_CORE_FIRST BLE_LOG_SRC_CUSTOM #define BLE_LOG_SRC_CORE_COUNT (BLE_LOG_SRC_ENCODE - BLE_LOG_SRC_CORE_FIRST + 1) @@ -93,6 +92,9 @@ typedef struct { typedef struct { uint32_t written_frame_cnt; uint32_t lost_frame_cnt; + /* Successfully committed logical frame bytes, including head and tail, + * excluding DMA padding. Not a physical-TX acknowledgement. */ + uint32_t written_bytes_cnt; } __attribute__((packed)) ble_log_source_stat_t; /* Runtime per-source counters. The aligned wrapper keeps the embedded @@ -104,18 +106,17 @@ typedef struct { #define BLE_LOG_GET_FRAME_SN(VAR) BLE_LOG_ATOMIC_ADD_RELAXED(VAR, 1) -/* One 24-bit Global SN is shared by the log sources except INTERNAL and - * REDIR: consumed at API entry (before pool contention), it orders all - * log attempts — including equal-timestamp records from different - * sources — and every lost or rejected attempt leaves a gap. INTERNAL - * snapshot frames, the periodic task-binding broadcast, and the REDIR - * console stream keep their own separate sequences (a gap counts a - * skipped snapshot, a skipped broadcast window, or a dropped console - * batch). ble_log_init() resets all of them; its required INIT snapshot - * starts a new receiver epoch. They stay continuous through FLUSH within - * that epoch. The per-counter macros live next to their counters in the - * owning translation units; the 24-bit wire field is enforced where the - * frame meta is packed. */ +/* Core logs and INTERNAL snapshots share one 24-bit Global SN. Core logs + * consume it after entry validation and gate acceptance, before contention; + * failed log attempts leave gaps accounted for by lost_frame_cnt. Snapshots + * only consume it after acquiring their dedicated transport. Busy/skipped + * snapshots are not in core loss statistics and advance only the separate + * 24-bit anchor_count, never the Global SN. + * Task-binding broadcasts and REDIR retain independent header sequences. + * ble_log_init() resets every sequence; INIT starts a new receiver epoch. + * FLUSH resets interval statistics, but never the sequences or anchor count. + * Allocation order is not physical TX order; a snapshot is not a drain + * barrier. The wire SN is masked to 24 bits when frame_meta is packed. */ /* -------------------------------- */ /* Internal Snapshot Frame */ @@ -137,6 +138,9 @@ typedef struct { typedef struct { uint8_t int_src_code; uint16_t reason_flags; + /* Snapshot attempts (INIT/PERIODIC/FLUSH), including skipped points. + * Little-endian low 24 bits; the runtime counter remains uint32_t. */ + uint8_t anchor_count[3]; ble_log_version_info_t version_info; /* Clock samples captured at the same instant, mirroring @@ -203,14 +207,14 @@ _Static_assert((BLE_LOG_POOL_TRANS_SIZE & 3U) == 0, #endif _Static_assert(sizeof(ble_log_version_info_t) == 58, "Unexpected BLE Log version information size"); -_Static_assert(sizeof(ble_log_source_stat_t) == 8, +_Static_assert(sizeof(ble_log_source_stat_t) == 12, "Unexpected BLE Log source statistic size"); _Static_assert(offsetof(ble_log_stat_mgr_t, counters) == 0 && - sizeof(ble_log_stat_mgr_t) == 8, + sizeof(ble_log_stat_mgr_t) == 12, "stat manager layout must keep the counters word-aligned"); -_Static_assert(sizeof(ble_log_internal_snapshot_t) == 134, +_Static_assert(sizeof(ble_log_internal_snapshot_t) == 165, "Unexpected BLE Log Internal Snapshot size"); -_Static_assert(BLE_LOG_INTERNAL_FRAME_LEN == 148, +_Static_assert(BLE_LOG_INTERNAL_FRAME_LEN == 179, "Unexpected BLE Log Internal frame size"); _Static_assert(BLE_LOG_INTERNAL_TRANS_SIZE >= BLE_LOG_INTERNAL_FRAME_LEN, "Internal transport is too small"); diff --git a/components/bt/common/ble_log/src/internal_include/ble_log_prph.h b/components/bt/common/ble_log/src/internal_include/ble_log_prph.h index bc2ab9b0fd5..3a4eee27b02 100644 --- a/components/bt/common/ble_log/src/internal_include/ble_log_prph.h +++ b/components/bt/common/ble_log/src/internal_include/ble_log_prph.h @@ -45,7 +45,9 @@ typedef struct { /* Lazy flush marker, pool-owned: set by the periodic flusher (without * holding atomic_lock) when the transport was busy at flush time; the * next claim that takes the lock seals the buffered frames first. - * Cleared by seal_and_send and on recycle. */ + * Cleared by seal_and_send and on recycle, but a delayed flusher may + * store true afterwards. Such a stale hint permits an extra partial + * seal in the next lifecycle, never sending a writer-owned buffer. */ volatile uint8_t pending_seal; uint8_t *buf; diff --git a/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c b/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c index 7736178d04e..854e499aedb 100644 --- a/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c +++ b/components/bt/common/ble_log/src/prph/ble_log_prph_uart_dma.c @@ -72,8 +72,8 @@ BLE_LOG_STATIC void esp_timer_cb_flush_log(void *arg) { (void)arg; - if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(prph_inited) || - !ble_log_lbm_is_enabled()) { + /* Producer disable stops new writes, not periodic draining of old data. */ + if (!BLE_LOG_ATOMIC_LOAD_ACQUIRE(prph_inited)) { return; } diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/README.md b/components/bt/common/ble_log/test_apps/ble_log_test/README.md index 13923c713d7..affd8dae9fe 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/README.md +++ b/components/bt/common/ble_log/test_apps/ble_log_test/README.md @@ -17,5 +17,6 @@ It covers: - task and critical-section writes plus HCI direction encoding; - direct compression claim/commit, stale handles, and per-source serialization; - oversized-record rejection, flush sequence continuity, pool exhaustion, and non-yield reserve use; -- periodic snapshot busy/loss behavior; +- shared log/snapshot Global SN, independent 24-bit anchor counts, and snapshot busy/loss behavior; +- successful logical-byte counts for public, claim/commit, and LL writes, plus FLUSH reset; - enable, disable, parked-writer, and deinit races. diff --git a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c index 02288e3a387..fef271fa58f 100644 --- a/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c +++ b/components/bt/common/ble_log/test_apps/ble_log_test/main/test_ble_log_rt.c @@ -218,9 +218,18 @@ static void capture_version_info_frame(const test_ble_log_frame_t *frame, void * } } +static uint32_t snapshot_anchor_count(const ble_log_internal_snapshot_t *snapshot) +{ + return (uint32_t)snapshot->anchor_count[0] | + ((uint32_t)snapshot->anchor_count[1] << 8) | + ((uint32_t)snapshot->anchor_count[2] << 16); +} + typedef struct { bool found; uint16_t reason_flags; + uint32_t sn; + uint32_t anchor_count; } first_snapshot_capture_t; static void capture_first_snapshot(const test_ble_log_frame_t *frame, void *ctx) @@ -237,6 +246,8 @@ static void capture_first_snapshot(const test_ble_log_frame_t *frame, void *ctx) if (snapshot.int_src_code == BLE_LOG_INT_SRC_SNAPSHOT) { capture->found = true; capture->reason_flags = snapshot.reason_flags; + capture->sn = frame->sn; + capture->anchor_count = snapshot_anchor_count(&snapshot); } } @@ -288,19 +299,27 @@ TEST_CASE("BLE Log v8 framing matches golden bytes", "[ble_log][wire]") TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_SRC_ENCODE); TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_INT_SRC_VERSION_INFO); TEST_ASSERT_EQUAL_UINT8(8, BLE_LOG_INT_SRC_SNAPSHOT); - TEST_ASSERT_EQUAL_size_t(8, sizeof(ble_log_source_stat_t)); - TEST_ASSERT_EQUAL_size_t(134, sizeof(ble_log_internal_snapshot_t)); + TEST_ASSERT_EQUAL_size_t(12, sizeof(ble_log_source_stat_t)); + TEST_ASSERT_EQUAL_size_t(165, sizeof(ble_log_internal_snapshot_t)); TEST_ASSERT_EQUAL_size_t( - 3, offsetof(ble_log_internal_snapshot_t, version_info)); + 3, offsetof(ble_log_internal_snapshot_t, anchor_count)); TEST_ASSERT_EQUAL_size_t( - 62, offsetof(ble_log_internal_snapshot_t, ts.lc_ts)); + 6, offsetof(ble_log_internal_snapshot_t, version_info)); TEST_ASSERT_EQUAL_size_t( - 66, offsetof(ble_log_internal_snapshot_t, ts.esp_ts)); + 65, offsetof(ble_log_internal_snapshot_t, ts.lc_ts)); TEST_ASSERT_EQUAL_size_t( - 70, offsetof(ble_log_internal_snapshot_t, ts.os_ts)); + 69, offsetof(ble_log_internal_snapshot_t, ts.esp_ts)); TEST_ASSERT_EQUAL_size_t( - 78, offsetof(ble_log_internal_snapshot_t, stats)); - TEST_ASSERT_EQUAL_UINT32(148, BLE_LOG_INTERNAL_FRAME_LEN); + 73, offsetof(ble_log_internal_snapshot_t, ts.os_ts)); + TEST_ASSERT_EQUAL_size_t( + 81, offsetof(ble_log_internal_snapshot_t, stats)); + TEST_ASSERT_EQUAL_UINT32(179, BLE_LOG_INTERNAL_FRAME_LEN); + TEST_ASSERT_EQUAL_UINT32(180, BLE_LOG_INTERNAL_TRANS_SIZE); + const ble_log_internal_snapshot_t anchor_fixture = { + .anchor_count = {0x56, 0x34, 0x12}, + }; + TEST_ASSERT_EQUAL_size_t(3, sizeof(anchor_fixture.anchor_count)); + TEST_ASSERT_EQUAL_HEX32(0x123456, snapshot_anchor_count(&anchor_fixture)); #if CONFIG_BLE_LOG_LL_ENABLED TEST_ASSERT_EQUAL_UINT8(4, BLE_LOG_LL_FLAG_HCI); TEST_ASSERT_EQUAL_UINT8(7, BLE_LOG_LL_FLAG_HCI_UPSTREAM); @@ -413,6 +432,8 @@ TEST_CASE("BLE Log INIT snapshot starts each receiver epoch", "[ble_log][wire]") TEST_ASSERT_TRUE(capture.found); TEST_ASSERT_EQUAL_HEX16(BLE_LOG_SNAPSHOT_REASON_INIT, capture.reason_flags); + TEST_ASSERT_EQUAL_UINT32(0, capture.sn); + TEST_ASSERT_EQUAL_UINT32(0, capture.anchor_count); } TEST_CASE("BLE Log sync IO APIs retain runtime lifecycle checks", "[ble_log]") @@ -1185,21 +1206,23 @@ TEST_CASE("BLE Log preserves LL payload and rejects oversized records", TEST_ASSERT_TRUE(captures[CUSTOM_BEFORE].found); TEST_ASSERT_FALSE(captures[CUSTOM_OVERSIZED].found); TEST_ASSERT_TRUE(captures[CUSTOM_AFTER].found); - TEST_ASSERT_EQUAL_HEX32( - (captures[CUSTOM_BEFORE].sn + 2) & 0x00ffffffU, - captures[CUSTOM_AFTER].sn); + /* Periodic snapshots may allocate additional Global SNs between writes. */ + uint32_t custom_delta = (captures[CUSTOM_AFTER].sn - + captures[CUSTOM_BEFORE].sn) & 0x00ffffffU; + TEST_ASSERT_GREATER_OR_EQUAL_UINT32(2, custom_delta); + TEST_ASSERT_LESS_THAN_UINT32(0x00800000U, custom_delta); #if CONFIG_BLE_LOG_LL_ENABLED TEST_ASSERT_TRUE(captures[LL_BEFORE].found); TEST_ASSERT_FALSE(captures[LL_PRIMARY_OVERSIZED].found); TEST_ASSERT_FALSE(captures[LL_APPEND_OVERSIZED].found); TEST_ASSERT_TRUE(captures[LL_AFTER].found); - TEST_ASSERT_EQUAL_HEX32( - (captures[LL_BEFORE].sn + 3) & 0x00ffffffU, - captures[LL_AFTER].sn); + uint32_t ll_delta = (captures[LL_AFTER].sn - captures[LL_BEFORE].sn) & 0x00ffffffU; + TEST_ASSERT_GREATER_OR_EQUAL_UINT32(3, ll_delta); + TEST_ASSERT_LESS_THAN_UINT32(0x00800000U, ll_delta); #endif } -TEST_CASE("BLE Log flush preserves source-local sequence continuity", +TEST_CASE("BLE Log flush snapshot consumes the continuous Global SN", "[ble_log][wire]") { const uint8_t before_marker = 0x71; @@ -1240,7 +1263,11 @@ TEST_CASE("BLE Log flush preserves source-local sequence continuity", }; read_sequence_frames(&after, 1); TEST_ASSERT_TRUE(after.found); - TEST_ASSERT_EQUAL_HEX32((before.sn + 1) & 0x00ffffffU, after.sn); + /* At least the FLUSH snapshot consumes a Global SN between the logs; + * periodic snapshots may consume additional numbers during the reads. */ + TEST_ASSERT_GREATER_OR_EQUAL_UINT32( + 2, (after.sn - before.sn) & 0x00ffffffU); + TEST_ASSERT_LESS_THAN_UINT32(0x00800000U, (after.sn - before.sn) & 0x00ffffffU); } /* ------- pending-seal and deinit drain ------- */ @@ -2048,6 +2075,7 @@ TEST_CASE("BLE Log ESP Timer writers never wait for shared transports", typedef struct { uint32_t sn[SNAPSHOT_CAPTURE_MAX]; + ble_log_internal_snapshot_t snapshot[SNAPSHOT_CAPTURE_MAX]; int count; } snapshot_capture_t; @@ -2065,7 +2093,8 @@ static void capture_snapshot_loss(const test_ble_log_frame_t *frame, void *ctx) if (snapshot.int_src_code == BLE_LOG_INT_SRC_SNAPSHOT && (snapshot.reason_flags & BLE_LOG_SNAPSHOT_REASON_PERIODIC) && capture->count < SNAPSHOT_CAPTURE_MAX) { - capture->sn[capture->count++] = frame->sn; + capture->sn[capture->count] = frame->sn; + capture->snapshot[capture->count++] = snapshot; } } @@ -2109,6 +2138,27 @@ TEST_CASE("BLE Log periodic snapshot ignores producer gate and fails fast when b TEST_ASSERT_TRUE(test_ble_log_walk_frames( s_read_buf, first_len, capture_snapshot_loss, &capture)); + /* Interleave successful and rejected log attempts with snapshots. These + * consume Global SNs but must not consume snapshot anchor counts. */ + static const uint8_t marker[] = {0xa5, 0x5a, 0x33}; + static const uint8_t oversized[BLE_LOG_MAX_PAYLOAD_LEN] = {0}; + TEST_ASSERT_TRUE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, marker, sizeof(marker))); + TEST_ASSERT_FALSE(ble_log_write_hex(BLE_LOG_SRC_CUSTOM, oversized, sizeof(oversized))); + uint32_t log_attempts = 2; + uint32_t handle; + uint8_t *claimed = ble_log_claim(BLE_LOG_SRC_ENCODE, sizeof(marker), &handle, true); + TEST_ASSERT_NOT_NULL(claimed); + memcpy(claimed, marker, sizeof(marker)); + ble_log_commit(handle, sizeof(marker)); + claimed = ble_log_claim(BLE_LOG_SRC_ENCODE, sizeof(marker), &handle, true); + TEST_ASSERT_NOT_NULL(claimed); + ble_log_commit(handle, 0); /* An aborted claim contributes no written bytes. */ + log_attempts += 2; +#if CONFIG_BLE_LOG_LL_ENABLED + ble_log_write_hex_ll(sizeof(marker), marker, 0, NULL, BIT(BLE_LOG_LL_FLAG_TASK)); + log_attempts++; +#endif + TEST_ASSERT_TRUE(ble_log_internal_snapshot( BLE_LOG_SNAPSHOT_REASON_PERIODIC, &ts_info, false)); TEST_ASSERT_TRUE(ble_log_rt_drain()); @@ -2123,16 +2173,69 @@ TEST_CASE("BLE Log periodic snapshot ignores producer gate and fails fast when b TEST_ASSERT_TRUE(test_ble_log_walk_frames( s_read_buf, len, capture_snapshot_loss, &capture)); } - /* The busy periodic attempt burned one snapshot SN: the gap in the - * snapshot sequence is the loss signal. */ + /* Busy snapshots only advance anchor_count. Each captured snapshot and + * each ordinary log attempt consumes one Global SN; skipped snapshots + * must not create unaccounted gaps in that sequence. */ TEST_ASSERT_GREATER_OR_EQUAL_INT(2, capture.count); bool gap_seen = false; for (int i = 1; i < capture.count; i++) { - if (((capture.sn[i] - capture.sn[i - 1]) & 0x00ffffffU) >= 2) { + if (((snapshot_anchor_count(&capture.snapshot[i]) - + snapshot_anchor_count(&capture.snapshot[i - 1])) & 0x00ffffffU) >= 2) { gap_seen = true; } } TEST_ASSERT_TRUE(gap_seen); + int last = capture.count - 1; + TEST_ASSERT_EQUAL_UINT32( + ((uint32_t)last + log_attempts) & 0x00ffffffU, + (capture.sn[last] - capture.sn[0]) & 0x00ffffffU); + const ble_log_source_stat_t *before_stat = &capture.snapshot[0].stats[0]; + const ble_log_source_stat_t *after_stat = &capture.snapshot[last].stats[0]; + TEST_ASSERT_EQUAL_UINT32(1, after_stat->written_frame_cnt - before_stat->written_frame_cnt); + TEST_ASSERT_EQUAL_UINT32(1, after_stat->lost_frame_cnt - before_stat->lost_frame_cnt); + TEST_ASSERT_EQUAL_UINT32(BLE_LOG_FRAME_OVERHEAD + sizeof(uint32_t) + sizeof(marker), + after_stat->written_bytes_cnt - before_stat->written_bytes_cnt); + unsigned encode_slot = BLE_LOG_SRC_ENCODE - BLE_LOG_SRC_CORE_FIRST; + before_stat = &capture.snapshot[0].stats[encode_slot]; + after_stat = &capture.snapshot[last].stats[encode_slot]; + TEST_ASSERT_EQUAL_UINT32(1, after_stat->written_frame_cnt - before_stat->written_frame_cnt); + TEST_ASSERT_EQUAL_UINT32(1, after_stat->lost_frame_cnt - before_stat->lost_frame_cnt); + TEST_ASSERT_EQUAL_UINT32(BLE_LOG_FRAME_OVERHEAD + sizeof(uint32_t) + sizeof(marker), + after_stat->written_bytes_cnt - before_stat->written_bytes_cnt); +#if CONFIG_BLE_LOG_LL_ENABLED + unsigned ll_slot = BLE_LOG_SRC_LL_TASK - BLE_LOG_SRC_CORE_FIRST; + TEST_ASSERT_EQUAL_UINT32(BLE_LOG_FRAME_OVERHEAD + sizeof(marker), + capture.snapshot[last].stats[ll_slot].written_bytes_cnt - + capture.snapshot[0].stats[ll_slot].written_bytes_cnt); +#endif + + uint32_t previous_anchor = snapshot_anchor_count(&capture.snapshot[last]); + uint32_t previous_sn = capture.sn[last]; + ble_log_prph_test_set_auto_recycle_hook(auto_recycle_noop, NULL); + ble_log_flush(); + ble_log_prph_test_set_auto_recycle_hook(NULL, NULL); + capture.count = 0; + TEST_ASSERT_TRUE(ble_log_internal_snapshot( + BLE_LOG_SNAPSHOT_REASON_PERIODIC, &ts_info, true)); + TEST_ASSERT_TRUE(ble_log_rt_drain()); + for (int i = 0; i < BLE_LOG_TRANS_TOTAL_CNT && !capture.count; i++) { + size_t len = ble_log_prph_test_read(s_read_buf, sizeof(s_read_buf), + pdMS_TO_TICKS(TEST_READ_TIMEOUT_MS), 0, NULL); + TEST_ASSERT_GREATER_THAN_size_t(0, len); + TEST_ASSERT_TRUE(test_ble_log_walk_frames(s_read_buf, len, capture_snapshot_loss, &capture)); + } + TEST_ASSERT_GREATER_THAN_INT(0, capture.count); + TEST_ASSERT_EQUAL_UINT32(0, capture.snapshot[0].stats[0].written_frame_cnt); + TEST_ASSERT_EQUAL_UINT32(0, capture.snapshot[0].stats[0].lost_frame_cnt); + TEST_ASSERT_EQUAL_UINT32(0, capture.snapshot[0].stats[0].written_bytes_cnt); + TEST_ASSERT_EQUAL_UINT32(0, capture.snapshot[0].stats[encode_slot].written_bytes_cnt); + uint32_t anchor_delta = (snapshot_anchor_count(&capture.snapshot[0]) - + previous_anchor) & 0x00ffffffU; + uint32_t sn_delta = (capture.sn[0] - previous_sn) & 0x00ffffffU; + TEST_ASSERT_GREATER_OR_EQUAL_UINT32(2, anchor_delta); /* FLUSH + next sample */ + TEST_ASSERT_LESS_THAN_UINT32(0x00800000U, anchor_delta); + TEST_ASSERT_GREATER_OR_EQUAL_UINT32(2, sn_delta); + TEST_ASSERT_LESS_OR_EQUAL_UINT32(anchor_delta, sn_delta); } #if CONFIG_BLE_LOG_LL_ENABLED