From 0204ad3c19372e3b0583b7f355fe4b92fb4bc35c Mon Sep 17 00:00:00 2001 From: MacNite Date: Tue, 8 Sep 2026 10:52:12 +0200 Subject: [PATCH 1/2] Serialize sensor measurements with BLE sessions --- firmware-nrf54lm20a/src/audio.c | 18 ++++++++---- firmware-nrf54lm20a/src/link.c | 52 ++++++++++++++++++++++++++++++++- firmware-nrf54lm20a/src/link.h | 5 +++- firmware-nrf54lm20a/src/main.c | 7 +++++ firmware-nrf54lm20a/src/mic.c | 5 ++-- firmware-nrf54lm20a/src/mic.h | 3 ++ firmware-nrf54lm20a/src/power.c | 24 ++++++++++++++- 7 files changed, 102 insertions(+), 12 deletions(-) diff --git a/firmware-nrf54lm20a/src/audio.c b/firmware-nrf54lm20a/src/audio.c index 71f4f76..fda058d 100644 --- a/firmware-nrf54lm20a/src/audio.c +++ b/firmware-nrf54lm20a/src/audio.c @@ -205,6 +205,16 @@ static void capture_thread_fn(void *a, void *b, void *c) ARG_UNUSED(a); ARG_UNUSED(b); ARG_UNUSED(c); for (;;) { k_sem_take(&audio_capture_start, K_FOREVER); + /* link_claim() transferred the session gate to this thread. Keep + * regulator settling and PDM setup out of the GATT write callback. */ + if (power_sensor_rail_enable() != 0 || mic_stream_start() != 0) { + (void)power_sensor_rail_disable(); + set_state(AUDIO_ARMED, AUDIO_ERR_MIC); + link_release(LINK_OWNER_AUDIO); + continue; + } + set_state(AUDIO_STREAMING, 0); + k_sem_give(&audio_tx_start); while (!atomic_get(&stopping) && link_conn() != NULL && k_uptime_get() < deadline_ms) { void *block; uint32_t size; @@ -336,18 +346,14 @@ static ssize_t ctrl_write(struct bt_conn *conn, const struct bt_gatt_attr *attr, ring_head = ring_tail = ring_count = ring_offset = 0; k_sem_reset(&audio_credits); for (int i = 0; i < AUDIO_CREDITS; i++) k_sem_give(&audio_credits); - if (power_sensor_rail_enable() != 0 || mic_stream_start() != 0) { - (void)power_sensor_rail_disable(); link_release(LINK_OWNER_AUDIO); - set_state(AUDIO_ARMED, AUDIO_ERR_MIC); return len; - } struct bt_le_conn_param cp = { .interval_min = HIVE_AUDIO_CONN_INTERVAL_UNITS, .interval_max = HIVE_AUDIO_CONN_INTERVAL_UNITS, .latency = 0, .timeout = 200 }; (void)bt_conn_le_param_update(conn, &cp); (void)bt_conn_le_data_len_update(conn, BT_LE_DATA_LEN_PARAM_MAX); (void)bt_conn_le_phy_update(conn, BT_CONN_LE_PHY_PARAM_2M); - set_state(AUDIO_STREAMING, 0); - k_sem_give(&audio_capture_start); k_sem_give(&audio_tx_start); + set_state(AUDIO_ARMED, 0); + k_sem_give(&audio_capture_start); return len; } diff --git a/firmware-nrf54lm20a/src/link.c b/firmware-nrf54lm20a/src/link.c index 0835b8f..89bfd39 100644 --- a/firmware-nrf54lm20a/src/link.c +++ b/firmware-nrf54lm20a/src/link.c @@ -6,6 +6,7 @@ #include "beacon.h" #include "hive_config.h" #include "ota.h" +#include "watchdog.h" #include #include @@ -15,6 +16,7 @@ static struct bt_conn *active_conn; static enum link_owner owner; static struct k_spinlock state_lock; +K_SEM_DEFINE(session_gate, 1, 1); static void arm_timeout_handler(struct k_work *work); K_WORK_DELAYABLE_DEFINE(arm_timeout_work, arm_timeout_handler); @@ -27,6 +29,12 @@ struct bt_conn *link_conn(void) bool link_claim(enum link_owner requested) { bool claimed = false; + + /* GATT callbacks must never wait for a sensor capture. Taking the gate + * without waiting makes START/BEGIN report busy while a cycle is in flight. */ + if (k_sem_take(&session_gate, K_NO_WAIT) != 0) { + return false; + } k_spinlock_key_t key = k_spin_lock(&state_lock); if (active_conn != NULL && owner == LINK_OWNER_NONE && @@ -35,6 +43,9 @@ bool link_claim(enum link_owner requested) claimed = true; } k_spin_unlock(&state_lock, key); + if (!claimed) { + k_sem_give(&session_gate); + } if (claimed) { (void)k_work_cancel_delayable(&arm_timeout_work); } @@ -44,12 +55,18 @@ bool link_claim(enum link_owner requested) void link_release(enum link_owner released) { bool arm = false; + bool released_gate = false; k_spinlock_key_t key = k_spin_lock(&state_lock); if (owner == released) { owner = LINK_OWNER_NONE; arm = active_conn != NULL; + released_gate = true; } k_spin_unlock(&state_lock, key); + /* Only the successful owner transition above owns this semaphore. */ + if (released_gate) { + k_sem_give(&session_gate); + } /* Releasing a completed service does not make an otherwise idle * connection safe to retain forever. Give the central a fresh arm window * in which to begin another bounded session. */ @@ -59,6 +76,28 @@ void link_release(enum link_owner released) } } +bool link_measurement_begin(void) +{ + /* A session can last minutes, so keep the watchdog alive rather than + * blocking the main thread indefinitely. */ + while (k_sem_take(&session_gate, + K_MSEC(HIVE_WDT_FEED_INTERVAL_MS)) != 0) { + hive_watchdog_feed(); + } + + /* A connection may have arrived while main was waiting for the gate. */ + if (link_is_busy()) { + k_sem_give(&session_gate); + return false; + } + return true; +} + +void link_measurement_end(void) +{ + k_sem_give(&session_gate); +} + bool link_is_owner(enum link_owner queried) { k_spinlock_key_t key = k_spin_lock(&state_lock); @@ -110,11 +149,22 @@ static void connected(struct bt_conn *conn, uint8_t err) static void disconnected(struct bt_conn *conn, uint8_t reason) { + bool release_gate = false; + k_spinlock_key_t key; + ARG_UNUSED(conn); (void)k_work_cancel_delayable(&arm_timeout_work); ota_link_disconnected(); audio_link_disconnected(); - owner = LINK_OWNER_NONE; + key = k_spin_lock(&state_lock); + if (owner != LINK_OWNER_NONE) { + owner = LINK_OWNER_NONE; + release_gate = true; + } + k_spin_unlock(&state_lock, key); + if (release_gate) { + k_sem_give(&session_gate); + } if (active_conn != NULL) { bt_conn_unref(active_conn); active_conn = NULL; diff --git a/firmware-nrf54lm20a/src/link.h b/firmware-nrf54lm20a/src/link.h index 7681a3b..90b2d28 100644 --- a/firmware-nrf54lm20a/src/link.h +++ b/firmware-nrf54lm20a/src/link.h @@ -16,4 +16,7 @@ bool link_claim(enum link_owner owner); void link_release(enum link_owner owner); bool link_is_owner(enum link_owner owner); bool link_is_busy(void); - +/* Hold around the complete powered sensor cycle. This waits in watchdog-sized + * slices, while link_claim() remains non-blocking for GATT callers. */ +bool link_measurement_begin(void); +void link_measurement_end(void); diff --git a/firmware-nrf54lm20a/src/main.c b/firmware-nrf54lm20a/src/main.c index 37dc858..6da8d60 100644 --- a/firmware-nrf54lm20a/src/main.c +++ b/firmware-nrf54lm20a/src/main.c @@ -186,6 +186,9 @@ int main(void) * than reset-looping, and MCUboot's own rollback deadline below is the * safety net for an image that cannot get this far. */ hive_watchdog_init(); + /* power_init() owns the boot-time rail reference; normal users take their + * own references below. */ + (void)power_sensor_rail_disable(); confirmation_pending = !boot_is_img_confirmed(); /* Do not confirm a test image merely because main() was reached. Wait until * one complete sensor cycle has run and Bluetooth has successfully published @@ -214,6 +217,9 @@ int main(void) k_msleep(100); continue; } + if (!link_measurement_begin()) { + continue; + } struct measurement m = { 0 }; /* LDO1 is normally off between cycles. Wake the IMU and microphone @@ -224,6 +230,7 @@ int main(void) accel_read(&m); mic_read(&m); (void)power_sensor_rail_disable(); + link_measurement_end(); battery_read(&m); print_readout(&m); diff --git a/firmware-nrf54lm20a/src/mic.c b/firmware-nrf54lm20a/src/mic.c index fd86566..6ce8292 100644 --- a/firmware-nrf54lm20a/src/mic.c +++ b/firmware-nrf54lm20a/src/mic.c @@ -132,9 +132,8 @@ void mic_read(struct measurement *m) { m->mic_ok = false; - /* link_is_busy() keeps the periodic sensor path out while audio owns the - * single PDM controller, so a second lock here would only obscure that - * system-level ownership rule. */ + /* link's session gate is held for the complete measurement cycle, keeping + * this one-shot capture exclusive with audio's use of the PDM controller. */ int err = mic_stream_start(); if (err != 0) { printk("[MIC] start failed (%d)\n", err); diff --git a/firmware-nrf54lm20a/src/mic.h b/firmware-nrf54lm20a/src/mic.h index 26df0bb..3b182b5 100644 --- a/firmware-nrf54lm20a/src/mic.h +++ b/firmware-nrf54lm20a/src/mic.h @@ -10,6 +10,9 @@ int mic_stream_read(void **buf, uint32_t *size, int32_t timeout_ms); void mic_stream_release(void *buf); void mic_stream_stop(void); +/* The streaming and one-shot APIs share one PDM controller and must not + * overlap. link's session gate serialises an entire measurement cycle with + * every BLE session before either path enters this module. */ /* Capture ~0.5 s of PCM from the PDM microphone and reduce it to RMS/peak dBFS * plus the five shared acoustic bands (sets m->mic_ok). A no-op that leaves * mic_ok false when the devicetree has no enabled nRF PDM node. */ diff --git a/firmware-nrf54lm20a/src/power.c b/firmware-nrf54lm20a/src/power.c index 00e118d..682db87 100644 --- a/firmware-nrf54lm20a/src/power.c +++ b/firmware-nrf54lm20a/src/power.c @@ -28,6 +28,8 @@ static const struct device *const ldo1 = DEVICE_DT_GET(LDO1_NODE); static bool sensor_rail_enabled; +static uint32_t sensor_rail_users; +K_MUTEX_DEFINE(sensor_rail_lock); /* P1.12 gates the complete Sense-board sensor supply. LDO1 being enabled is * not sufficient when this upstream fixed regulator is off. Seeed's DMIC @@ -132,6 +134,7 @@ void power_init(void) return; } sensor_rail_enabled = true; + sensor_rail_users = 1; /* The IMU and microphone share this rail. Give both parts time to * leave reset before their first I2C/PDM transaction. */ k_msleep(20); @@ -141,8 +144,11 @@ void power_init(void) int power_sensor_rail_enable(void) { int err; + k_mutex_lock(&sensor_rail_lock, K_FOREVER); if (sensor_rail_enabled) { + sensor_rail_users++; + k_mutex_unlock(&sensor_rail_lock); return 0; } @@ -151,30 +157,44 @@ int power_sensor_rail_enable(void) #if DT_NODE_EXISTS(DT_NODELABEL(power_en)) err = enable_regulator(power_en, "sensor power gate"); if (err != 0) { + k_mutex_unlock(&sensor_rail_lock); return err; } #endif err = enable_regulator(ldo1, "LDO1"); if (err != 0) { + k_mutex_unlock(&sensor_rail_lock); return err; } sensor_rail_enabled = true; + sensor_rail_users = 1; /* The IMU and microphone need time to leave reset before use. */ k_msleep(20); + k_mutex_unlock(&sensor_rail_lock); return 0; } int power_sensor_rail_disable(void) { int err; + k_mutex_lock(&sensor_rail_lock, K_FOREVER); - if (!sensor_rail_enabled) { + /* Every successful enable owns one reference. Only its matching disable + * may drop that reference, and hardware is switched off at zero. */ + if (!sensor_rail_enabled || sensor_rail_users == 0U) { + k_mutex_unlock(&sensor_rail_lock); + return 0; + } + if (--sensor_rail_users != 0U) { + k_mutex_unlock(&sensor_rail_lock); return 0; } err = disable_regulator(ldo1, "LDO1"); if (err != 0) { + sensor_rail_users = 1; + k_mutex_unlock(&sensor_rail_lock); return err; } sensor_rail_enabled = false; @@ -185,10 +205,12 @@ int power_sensor_rail_disable(void) * measurement. Reverse order of the enable above. */ err = disable_regulator(power_en, "sensor power gate"); if (err != 0) { + k_mutex_unlock(&sensor_rail_lock); return err; } #endif + k_mutex_unlock(&sensor_rail_lock); return 0; } From 1635c29923f6dd2831b8ff9a1935400aad13c229 Mon Sep 17 00:00:00 2001 From: MacNite Date: Tue, 8 Sep 2026 10:54:29 +0200 Subject: [PATCH 2/2] Bump firmware version to 0.6.2 --- firmware-nrf54lm20a/src/hive_config.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware-nrf54lm20a/src/hive_config.h b/firmware-nrf54lm20a/src/hive_config.h index 2133735..8ea621e 100644 --- a/firmware-nrf54lm20a/src/hive_config.h +++ b/firmware-nrf54lm20a/src/hive_config.h @@ -43,7 +43,7 @@ */ #define HIVEINSIDE_FW_VERSION_MAJOR 0 #define HIVEINSIDE_FW_VERSION_MINOR 6 -#define HIVEINSIDE_FW_VERSION_PATCH 1 +#define HIVEINSIDE_FW_VERSION_PATCH 2 #define HIVEINSIDE_STRINGIFY_(value) #value #define HIVEINSIDE_STRINGIFY(value) HIVEINSIDE_STRINGIFY_(value)