/* * XIAO nRF54LM20A BLE recorder * * State machine: * IDLE Green LED off * RECORDING Green LED on; exits on BOOT press or after 10 seconds * TRANSFERRING Green LED blinks every 500 ms during BLE NUS transfer * * Audio: 16 kHz, 16-bit mono DMIC data stored in external SPI flash (PY25Q64) * Transfer: WAV header and flash data sent through NUS notifications every 30 ms * * Logging uses RTT over SWD; see app.overlay and prj.conf. */ #include #include #include #include #include #include #include #include #include #include #include #include #include LOG_MODULE_REGISTER(mic_ble, LOG_LEVEL_INF); /* ===== Audio parameters ===== */ #define RECORD_TIME_S 10 #define SAMPLE_RATE_HZ 16000 #define SAMPLE_BIT_WIDTH 16 #define BYTES_PER_SAMPLE (SAMPLE_BIT_WIDTH / 8) #define CHUNK_DURATION_MS 100 #define CHUNK_SIZE_BYTES (BYTES_PER_SAMPLE * (SAMPLE_RATE_HZ * CHUNK_DURATION_MS) / 1000) #define CHUNK_COUNT 8 #define TOTAL_CHUNKS (RECORD_TIME_S * 1000 / CHUNK_DURATION_MS) #define READ_TIMEOUT_MS 1000 /* ===== WAV header (44 bytes) ===== */ #define WAV_HEADER_SIZE 44 /* ===== External flash storage ===== */ #define FLASH_AUDIO_OFFSET 0x000000 #define FLASH_ERASE_SIZE 0x080000 /* 512 KB, sufficient for 16 seconds of audio */ /* ===== BLE transfer pacing ===== * BLE throughput is limited by the connection interval (CI). * Send one chunk every 30 ms to avoid saturating the ACL pipeline. */ #define BLE_CHUNK_SIZE 244 /* MTU 247 - 3 */ #define BLE_SEND_INTERVAL_MS 30 #define BLE_RETRY_ERROR_MS 50 /* ===== Application state ===== */ enum app_state { STATE_IDLE, STATE_RECORDING, STATE_TRANSFERRING, }; /* ===== Device handles ===== */ #define GREEN_LED_NODE DT_NODELABEL(green_led) #if !DT_NODE_EXISTS(GREEN_LED_NODE) #error "XIAO nRF54LM20A board definition must provide the green_led node" #endif static const struct device *const dmic_dev = DEVICE_DT_GET(DT_ALIAS(dmic20)); static const struct gpio_dt_spec green_led = GPIO_DT_SPEC_GET(GREEN_LED_NODE, gpios); static const struct gpio_dt_spec button = GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios); static const struct device *const power_en_dev = DEVICE_DT_GET(DT_NODELABEL(power_en)); static const struct device *const dmic_vdd_dev = DEVICE_DT_GET(DT_NODELABEL(dmic_vdd)); static const struct device *flash_dev = DEVICE_DT_GET(DT_NODELABEL(py25q64)); /* ===== Synchronization and state ===== */ static K_SEM_DEFINE(button_sem, 0, 1); static enum app_state current_state = STATE_IDLE; static struct bt_conn *default_conn; static bool nus_notif_enabled; /* ===== Flash write progress ===== */ static size_t flash_write_offset; static size_t flash_data_len; /* ===== WAV header template ===== */ static const uint8_t wav_header_template[WAV_HEADER_SIZE] = { 'R', 'I', 'F', 'F', 0, 0, 0, 0, 'W', 'A', 'V', 'E', 'f', 'm', 't', ' ', 16, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 'd', 'a', 't', 'a', 0, 0, 0, 0, }; /* ===== DMIC memory pool and configuration ===== */ K_MEM_SLAB_DEFINE_STATIC(mem_slab, CHUNK_SIZE_BYTES, CHUNK_COUNT, 4); static struct pcm_stream_cfg stream_cfg = { .pcm_rate = SAMPLE_RATE_HZ, .pcm_width = SAMPLE_BIT_WIDTH, .block_size = CHUNK_SIZE_BYTES, .mem_slab = &mem_slab, }; static struct dmic_cfg dmic_config = { .io = { .min_pdm_clk_freq = 1000000, .max_pdm_clk_freq = 3500000, .min_pdm_clk_dc = 40, .max_pdm_clk_dc = 60, }, .streams = &stream_cfg, .channel = { .req_num_streams = 1, .req_num_chan = 1, }, }; /* ===== Timers and delayable work ===== */ static struct k_timer record_timer; static struct k_timer led_blink_timer; static struct k_work_delayable ble_send_work; static struct k_work_delayable adv_work; static atomic_t record_timed_out; static size_t ble_send_offset; static size_t ble_send_total; /* BLE transmit buffers */ static uint8_t ble_send_buf[BLE_CHUNK_SIZE]; static uint8_t wav_hdr_buf[WAV_HEADER_SIZE]; /* ===== Forward declarations ===== */ static int start_recording(void); static int stop_recording(void); static void start_ble_transfer(void); static int start_advertising(void); /* ===== Button ISR ===== */ static struct gpio_callback button_cb_data; static void button_pressed(const struct device *dev, struct gpio_callback *cb, uint32_t pins) { k_sem_give(&button_sem); } /* ===== Recording timeout ===== */ static void record_timer_handler(struct k_timer *timer) { ARG_UNUSED(timer); /* Timer expiry runs outside the application thread. Do not call the * DMIC driver, logger, or BLE transfer code here; only signal the * capture loop, which performs the shutdown in normal thread context. */ atomic_set(&record_timed_out, 1); } /* ===== LED blink timer ===== */ static void led_blink_timer_handler(struct k_timer *timer) { if (current_state == STATE_TRANSFERRING) { gpio_pin_toggle_dt(&green_led); } } /* ===== DMIC power ===== */ static int enable_dmic_power(void) { int ret; if (!device_is_ready(power_en_dev)) { LOG_ERR("power_en regulator not ready"); return -ENODEV; } if (!device_is_ready(dmic_vdd_dev)) { LOG_ERR("dmic_vdd regulator not ready"); return -ENODEV; } /* power_en is enabled by the board devicetree. Let it settle before * accessing the nPM1300 and enabling the shared IMU/DMIC LDO1 rail. */ k_sleep(K_MSEC(100)); /* LDO1 deliberately has no regulator-boot-on property. Enable it only * after the PMIC bus and board power rail are stable. */ for (int attempt = 1; attempt <= 3; attempt++) { ret = regulator_enable(dmic_vdd_dev); printk("DMIC LDO1 enable attempt %d: %d\n", attempt, ret); if (ret == 0 || ret == -EALREADY) { break; } k_sleep(K_MSEC(50)); } if (ret < 0 && ret != -EALREADY) { return ret; } k_sleep(K_MSEC(100)); return 0; } /* ===== WAV header generation ===== */ static void wav_header_write(uint8_t *buf, uint32_t data_size) { uint32_t file_size = data_size + 36; uint16_t block_align = BYTES_PER_SAMPLE * 1; uint32_t byte_rate = SAMPLE_RATE_HZ * block_align; memcpy(buf, wav_header_template, WAV_HEADER_SIZE); buf[4] = (uint8_t)(file_size); buf[5] = (uint8_t)(file_size >> 8); buf[6] = (uint8_t)(file_size >> 16); buf[7] = (uint8_t)(file_size >> 24); buf[24] = (uint8_t)(SAMPLE_RATE_HZ); buf[25] = (uint8_t)(SAMPLE_RATE_HZ >> 8); buf[26] = (uint8_t)(SAMPLE_RATE_HZ >> 16); buf[27] = (uint8_t)(SAMPLE_RATE_HZ >> 24); buf[28] = (uint8_t)(byte_rate); buf[29] = (uint8_t)(byte_rate >> 8); buf[30] = (uint8_t)(byte_rate >> 16); buf[31] = (uint8_t)(byte_rate >> 24); buf[32] = (uint8_t)(block_align); buf[33] = (uint8_t)(block_align >> 8); buf[34] = (uint8_t)(SAMPLE_BIT_WIDTH); buf[35] = (uint8_t)(SAMPLE_BIT_WIDTH >> 8); buf[40] = (uint8_t)(data_size); buf[41] = (uint8_t)(data_size >> 8); buf[42] = (uint8_t)(data_size >> 16); buf[43] = (uint8_t)(data_size >> 24); } /* ===== LED helpers ===== */ static void led_on(void) { gpio_pin_set_dt(&green_led, 1); } static void led_off(void) { gpio_pin_set_dt(&green_led, 0); } /* ===== Flash preparation ===== */ static int flash_prepare(void) { int ret; if (!device_is_ready(flash_dev)) { LOG_ERR("External flash not ready"); return -ENODEV; } LOG_INF("Erasing flash (%u bytes)...", (unsigned int)FLASH_ERASE_SIZE); ret = flash_erase(flash_dev, FLASH_AUDIO_OFFSET, FLASH_ERASE_SIZE); if (ret < 0) { LOG_ERR("Flash erase failed: %d", ret); return ret; } LOG_INF("Flash erase done"); flash_write_offset = FLASH_AUDIO_OFFSET; flash_data_len = 0; return 0; } /* ===== Recording ===== */ static int start_recording(void) { int ret; LOG_INF("Recording started"); atomic_clear(&record_timed_out); ret = flash_prepare(); if (ret < 0) { led_off(); return ret; } current_state = STATE_RECORDING; led_on(); ret = dmic_configure(dmic_dev, &dmic_config); LOG_INF("dmic_configure: %d", ret); if (ret < 0) { current_state = STATE_IDLE; led_off(); return ret; } ret = dmic_trigger(dmic_dev, DMIC_TRIGGER_START); LOG_INF("dmic_trigger START: %d", ret); if (ret < 0) { current_state = STATE_IDLE; led_off(); return ret; } /* Discard the first chunk to remove startup noise. */ void *discard_buf; uint32_t discard_size; ret = dmic_read(dmic_dev, 0, &discard_buf, &discard_size, READ_TIMEOUT_MS); if (ret < 0) { LOG_WRN("Discard read failed: %d", ret); } else { LOG_INF("Discarded first chunk (%u bytes)", discard_size); k_mem_slab_free(&mem_slab, discard_buf); } k_timer_start(&record_timer, K_SECONDS(RECORD_TIME_S), K_NO_WAIT); return 0; } static int stop_recording(void) { int ret; LOG_INF("Recording stopped"); k_timer_stop(&record_timer); ret = dmic_trigger(dmic_dev, DMIC_TRIGGER_STOP); LOG_INF("dmic_trigger STOP: %d", ret); current_state = STATE_TRANSFERRING; LOG_INF("Captured %u bytes PCM", (uint32_t)flash_data_len); return 0; } /* Capture DMIC data directly to external flash until stopped or timed out. */ static int capture_audio_data(void) { int ret; void *buffer; uint32_t size; for (int i = 0; i < TOTAL_CHUNKS; i++) { /* Check for an early-stop button press without blocking. */ if (k_sem_take(&button_sem, K_NO_WAIT) == 0) { LOG_INF("Button: stop early at chunk %d/%d", i, TOTAL_CHUNKS); return 0; } ret = dmic_read(dmic_dev, 0, &buffer, &size, READ_TIMEOUT_MS); if (ret < 0) { LOG_ERR("DMIC read failed: %d", ret); return ret; } ret = flash_write(flash_dev, flash_write_offset, buffer, CHUNK_SIZE_BYTES); if (ret < 0) { LOG_ERR("Flash write @0x%x: %d", (unsigned int)flash_write_offset, ret); k_mem_slab_free(&mem_slab, buffer); return ret; } flash_write_offset += CHUNK_SIZE_BYTES; flash_data_len += CHUNK_SIZE_BYTES; k_mem_slab_free(&mem_slab, buffer); if ((i % 10) == 0) { LOG_INF("Captured %u bytes (%d/%d chunks)", (uint32_t)flash_data_len, i + 1, TOTAL_CHUNKS); } if (atomic_get(&record_timed_out)) { LOG_INF("Stopped by timeout"); return 0; } } return 0; } /* ===== BLE transfer ===== */ static void ble_send_work_handler(struct k_work *work) { int ret; size_t remaining, chunk, flash_off, hdr_off, hdr_part, pcm_part; if (!default_conn || !nus_notif_enabled) { LOG_WRN("BLE not ready, retry 500ms"); k_work_schedule(&ble_send_work, K_MSEC(500)); return; } /* Finish the transfer after all data has been sent. */ if (ble_send_offset >= ble_send_total) { LOG_INF("BLE transfer complete"); k_timer_stop(&led_blink_timer); led_off(); current_state = STATE_IDLE; ble_send_offset = 0; ble_send_total = 0; return; } /* Assemble a chunk from the WAV header and flash data. */ remaining = ble_send_total - ble_send_offset; chunk = remaining < BLE_CHUNK_SIZE ? remaining : BLE_CHUNK_SIZE; if (ble_send_offset < WAV_HEADER_SIZE) { hdr_off = ble_send_offset; hdr_part = WAV_HEADER_SIZE - hdr_off; if (hdr_part > chunk) { hdr_part = chunk; } pcm_part = chunk - hdr_part; memcpy(ble_send_buf, wav_hdr_buf + hdr_off, hdr_part); if (pcm_part > 0) { ret = flash_read(flash_dev, FLASH_AUDIO_OFFSET, ble_send_buf + hdr_part, pcm_part); if (ret < 0) { LOG_ERR("Flash read: %d", ret); k_work_schedule(&ble_send_work, K_MSEC(BLE_RETRY_ERROR_MS)); return; } } } else { flash_off = FLASH_AUDIO_OFFSET + ble_send_offset - WAV_HEADER_SIZE; ret = flash_read(flash_dev, flash_off, ble_send_buf, chunk); if (ret < 0) { LOG_ERR("Flash read: %d", ret); k_work_schedule(&ble_send_work, K_MSEC(BLE_RETRY_ERROR_MS)); return; } } ret = bt_nus_send(default_conn, ble_send_buf, chunk); if (ret == 0) { ble_send_offset += chunk; if ((ble_send_offset / (10 * BLE_CHUNK_SIZE)) != ((ble_send_offset - chunk) / (10 * BLE_CHUNK_SIZE))) { LOG_INF("Sent %u/%u bytes", (uint32_t)ble_send_offset, (uint32_t)ble_send_total); } k_work_schedule(&ble_send_work, K_MSEC(BLE_SEND_INTERVAL_MS)); return; } /* Retry after one connection interval if the buffers are full. */ if (ret == -EAGAIN || ret == -ENOMEM) { k_work_schedule(&ble_send_work, K_MSEC(BLE_SEND_INTERVAL_MS)); return; } LOG_ERR("BLE send error: %d", ret); k_work_schedule(&ble_send_work, K_MSEC(BLE_RETRY_ERROR_MS)); } static void start_ble_transfer(void) { wav_header_write(wav_hdr_buf, (uint32_t)flash_data_len); ble_send_offset = 0; ble_send_total = WAV_HEADER_SIZE + flash_data_len; LOG_INF("BLE transfer started, %u bytes total", (uint32_t)ble_send_total); /* Blink the green LED every 500 ms during transfer. */ k_timer_start(&led_blink_timer, K_MSEC(500), K_MSEC(500)); k_work_schedule(&ble_send_work, K_NO_WAIT); } /* ===== NUS callbacks ===== */ static void nus_notif_enabled_cb(bool enabled, void *ctx) { nus_notif_enabled = enabled; if (enabled) { LOG_INF("NUS notifications enabled"); } } static void nus_received_cb(struct bt_conn *conn, const void *data, uint16_t len, void *ctx) { } static struct bt_nus_cb nus_cb = { .notif_enabled = nus_notif_enabled_cb, .received = nus_received_cb, }; /* ===== Advertising restart on the system workqueue ===== */ static void adv_work_handler(struct k_work *work) { int ret = start_advertising(); if (ret) { LOG_ERR("Advertising restart failed: %d", ret); } } /* ===== Connection callbacks ===== */ static void connected(struct bt_conn *conn, uint8_t err) { if (err) { LOG_ERR("Connection failed (err %u)", err); return; } default_conn = bt_conn_ref(conn); LOG_INF("BLE connected"); } static void disconnected(struct bt_conn *conn, uint8_t reason) { LOG_INF("BLE disconnected (reason %u)", reason); if (default_conn) { bt_conn_unref(default_conn); default_conn = NULL; } nus_notif_enabled = false; if (current_state == STATE_TRANSFERRING) { LOG_WRN("Disconnected during transfer, aborting"); k_timer_stop(&led_blink_timer); led_off(); (void)k_work_cancel_delayable(&ble_send_work); ble_send_offset = 0; ble_send_total = 0; current_state = STATE_IDLE; } /* Restart advertising so the device remains discoverable. */ (void)k_work_schedule(&adv_work, K_NO_WAIT); } static struct bt_conn_cb conn_callbacks = { .connected = connected, .disconnected = disconnected, }; /* ===== BLE initialization ===== */ static int ble_init(void) { int ret; ret = bt_enable(NULL); if (ret) { LOG_ERR("bt_enable failed: %d", ret); return ret; } LOG_INF("BLE initialized"); bt_conn_cb_register(&conn_callbacks); ret = bt_nus_cb_register(&nus_cb, NULL); if (ret) { LOG_ERR("NUS cb register failed: %d", ret); return ret; } return 0; } /* ===== Advertising ===== */ static int start_advertising(void) { const struct bt_data ad[] = { BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)), BT_DATA(BT_DATA_NAME_COMPLETE, CONFIG_BT_DEVICE_NAME, sizeof(CONFIG_BT_DEVICE_NAME) - 1), }; return bt_le_adv_start(BT_LE_ADV_CONN_FAST_1, ad, ARRAY_SIZE(ad), NULL, 0); } /* ===== Main ===== */ int main(void) { int ret; printk("\nXIAO nRF54LM20A MIC boot\n"); if (!device_is_ready(dmic_dev) || !device_is_ready(green_led.port) || !device_is_ready(button.port)) { printk("Boot failed: DMIC, LED, or button is not ready\n"); LOG_ERR("Required device not ready"); return -ENODEV; } printk("Boot stage 1: core devices ready\n"); LOG_INF("DMIC / LED / button ready"); printk("Boot stage 2: enabling DMIC power\n"); ret = enable_dmic_power(); if (ret < 0) { printk("Boot failed: DMIC power error %d\n", ret); LOG_ERR("DMIC power failed: %d", ret); return ret; } printk("Boot stage 3: DMIC power stable\n"); LOG_INF("DMIC power enabled"); dmic_config.channel.req_chan_map_lo = dmic_build_channel_map(0, 0, PDM_CHAN_LEFT); ret = gpio_pin_configure_dt(&green_led, GPIO_OUTPUT_INACTIVE); if (ret < 0) { LOG_ERR("LED config: %d", ret); return ret; } ret = gpio_pin_configure_dt(&button, GPIO_INPUT); if (ret < 0) { LOG_ERR("Button config: %d", ret); return ret; } ret = gpio_pin_interrupt_configure_dt(&button, GPIO_INT_EDGE_TO_ACTIVE); if (ret < 0) { LOG_ERR("Button int config: %d", ret); return ret; } gpio_init_callback(&button_cb_data, button_pressed, BIT(button.pin)); gpio_add_callback(button.port, &button_cb_data); if (!device_is_ready(flash_dev)) { LOG_ERR("External flash not ready"); return -ENODEV; } LOG_INF("External flash ready"); k_timer_init(&record_timer, record_timer_handler, NULL); k_timer_init(&led_blink_timer, led_blink_timer_handler, NULL); k_work_init_delayable(&ble_send_work, ble_send_work_handler); k_work_init_delayable(&adv_work, adv_work_handler); ret = ble_init(); if (ret < 0) { return ret; } ret = start_advertising(); if (ret) { LOG_ERR("Advertising failed: %d", ret); return ret; } LOG_INF("Advertising started"); LOG_INF("=== XIAO nRF54LM20A BLE Recorder ready ==="); LOG_INF("BOOT: 1st press = record (green solid), 2nd press = stop & transfer (green blink)"); LOG_INF("Max recording: %d seconds", RECORD_TIME_S); while (1) { LOG_INF("Waiting for BOOT..."); k_sem_take(&button_sem, K_FOREVER); k_sleep(K_MSEC(50)); /* Debounce the button. */ switch (current_state) { case STATE_IDLE: ret = start_recording(); if (ret < 0) { LOG_ERR("start_recording failed: %d", ret); continue; } ret = capture_audio_data(); if (ret < 0) { LOG_ERR("capture failed: %d", ret); /* Always stop the active DMIC stream before returning to * idle, including flash-write and read error paths. */ if (current_state == STATE_RECORDING) { (void)stop_recording(); } current_state = STATE_IDLE; led_off(); continue; } if (current_state == STATE_RECORDING) { (void)stop_recording(); } start_ble_transfer(); break; case STATE_RECORDING: /* Handle a press between recording setup and capture. */ LOG_INF("Button during recording, stopping"); (void)stop_recording(); start_ble_transfer(); break; case STATE_TRANSFERRING: LOG_WRN("Transfer in progress, button ignored"); break; } } return 0; }