Implement native ESP-IDF radar firmware
Some checks failed
build / host-tests (push) Has been cancelled
build / esp-idf (push) Has been cancelled

This commit is contained in:
2026-07-21 07:47:02 +00:00
parent 8a43efb3f6
commit b473ed4369
28 changed files with 3770 additions and 1 deletions

436
main/audio_service.c Normal file
View File

@@ -0,0 +1,436 @@
#include "audio_service.h"
#include <math.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include "driver/i2s_std.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
/* CrowPanel 7.0-inch V3 onboard NS4168 connections. */
#define AUDIO_I2S_PORT I2S_NUM_0
#define AUDIO_PIN_BCLK 42
#define AUDIO_PIN_LRCLK 18
#define AUDIO_PIN_DOUT 17
#define AUDIO_SAMPLE_RATE_HZ 16000U
#define AUDIO_DMA_BUFFER_COUNT 4
#define AUDIO_DMA_FRAMES 128U
#define AUDIO_CHANNEL_COUNT 2U
#define PING_DURATION_MS 70U
#define PING_TOTAL_FRAMES ((AUDIO_SAMPLE_RATE_HZ * PING_DURATION_MS) / 1000U)
#define PING_ATTACK_FRAMES ((AUDIO_SAMPLE_RATE_HZ * 4U) / 1000U)
#define PING_RELEASE_FRAMES ((AUDIO_SAMPLE_RATE_HZ * 12U) / 1000U)
#define PING_PEAK_AMPLITUDE 7000
#define DISTANCE_MIN_M 0.3f
#define DISTANCE_MAX_M 8.0f
#define PING_FREQUENCY_NEAR_HZ 1800U
#define PING_FREQUENCY_FAR_HZ 700U
#define PING_INTERVAL_NEAR_MS 120U
#define PING_INTERVAL_FAR_MS 900U
#define SINE_TABLE_BITS 8U
#define SINE_TABLE_SIZE (1U << SINE_TABLE_BITS)
#define AUDIO_TASK_STACK_BYTES 3072U
#define AUDIO_WRITE_TIMEOUT_MS 25U
typedef struct {
bool enabled;
bool has_target;
float nearest_distance_m;
} audio_state_snapshot_t;
static const char *TAG = "audio_service";
static portMUX_TYPE s_state_lock = portMUX_INITIALIZER_UNLOCKED;
static bool s_initialized;
static bool s_initializing;
static bool s_enabled = true;
static bool s_has_target;
static float s_nearest_distance_m;
static TaskHandle_t s_audio_task;
static i2s_chan_handle_t s_tx_channel;
/* Kept out of the worker stack: 512 B each for the LUT and stereo DMA chunk. */
static int16_t s_sine_table[SINE_TABLE_SIZE];
static int16_t s_pcm_chunk[AUDIO_DMA_FRAMES * AUDIO_CHANNEL_COUNT];
static void audio_task(void *context);
static void init_sine_table(void)
{
const float phase_scale = (2.0f * 3.14159265358979323846f) / (float)SINE_TABLE_SIZE;
for (uint32_t i = 0; i < SINE_TABLE_SIZE; ++i) {
s_sine_table[i] = (int16_t)(sinf((float)i * phase_scale) * 32767.0f);
}
}
static audio_state_snapshot_t state_snapshot(void)
{
audio_state_snapshot_t snapshot;
taskENTER_CRITICAL(&s_state_lock);
snapshot.enabled = s_enabled;
snapshot.has_target = s_has_target;
snapshot.nearest_distance_m = s_nearest_distance_m;
taskEXIT_CRITICAL(&s_state_lock);
return snapshot;
}
static void notify_audio_task(TaskHandle_t task)
{
if (task != NULL) {
xTaskNotifyGive(task);
}
}
static uint32_t map_frequency_hz(float distance_m)
{
const float position = (distance_m - DISTANCE_MIN_M) /
(DISTANCE_MAX_M - DISTANCE_MIN_M);
const float span = (float)(PING_FREQUENCY_NEAR_HZ - PING_FREQUENCY_FAR_HZ);
return (uint32_t)((float)PING_FREQUENCY_NEAR_HZ - (position * span) + 0.5f);
}
static uint32_t map_interval_ms(float distance_m)
{
const float position = (distance_m - DISTANCE_MIN_M) /
(DISTANCE_MAX_M - DISTANCE_MIN_M);
const float span = (float)(PING_INTERVAL_FAR_MS - PING_INTERVAL_NEAR_MS);
return (uint32_t)((float)PING_INTERVAL_NEAR_MS + (position * span) + 0.5f);
}
static uint32_t envelope_q15(uint32_t frame)
{
if (frame < PING_ATTACK_FRAMES) {
return (frame * 32767U) / PING_ATTACK_FRAMES;
}
const uint32_t frames_after = PING_TOTAL_FRAMES - frame - 1U;
if (frames_after < PING_RELEASE_FRAMES) {
return (frames_after * 32767U) / PING_RELEASE_FRAMES;
}
return 32767U;
}
static bool write_pcm(const int16_t *samples, size_t frame_count)
{
const uint8_t *source = (const uint8_t *)samples;
const size_t total_bytes = frame_count * AUDIO_CHANNEL_COUNT * sizeof(int16_t);
size_t offset = 0;
while (offset < total_bytes) {
size_t bytes_written = 0;
const esp_err_t err = i2s_channel_write(s_tx_channel,
source + offset,
total_bytes - offset,
&bytes_written,
AUDIO_WRITE_TIMEOUT_MS);
if (err != ESP_OK || bytes_written == 0U) {
ESP_LOGW(TAG, "I2S write failed: %s (%u/%u bytes)",
esp_err_to_name(err),
(unsigned)offset,
(unsigned)total_bytes);
return false;
}
offset += bytes_written;
}
return true;
}
static bool target_still_audible(void)
{
const audio_state_snapshot_t snapshot = state_snapshot();
return snapshot.enabled && snapshot.has_target;
}
static bool play_ping(uint32_t frequency_hz)
{
uint32_t phase = 0;
const uint32_t phase_step = (uint32_t)(((uint64_t)frequency_hz << 32) /
AUDIO_SAMPLE_RATE_HZ);
for (uint32_t first_frame = 0; first_frame < PING_TOTAL_FRAMES;
first_frame += AUDIO_DMA_FRAMES) {
if (!target_still_audible()) {
return false;
}
uint32_t frame_count = PING_TOTAL_FRAMES - first_frame;
if (frame_count > AUDIO_DMA_FRAMES) {
frame_count = AUDIO_DMA_FRAMES;
}
for (uint32_t i = 0; i < frame_count; ++i) {
const uint32_t frame = first_frame + i;
const uint32_t envelope = envelope_q15(frame);
const int32_t wave = s_sine_table[phase >> (32U - SINE_TABLE_BITS)];
int32_t sample = (wave * PING_PEAK_AMPLITUDE) >> 15;
sample = (sample * (int32_t)envelope) >> 15;
phase += phase_step;
s_pcm_chunk[i * AUDIO_CHANNEL_COUNT] = (int16_t)sample;
s_pcm_chunk[(i * AUDIO_CHANNEL_COUNT) + 1U] = (int16_t)sample;
}
if (!write_pcm(s_pcm_chunk, frame_count)) {
return false;
}
}
return true;
}
/*
* Queue one DMA ring of zero samples. Once these writes complete, every tone
* buffer has reached the peripheral, so it is safe to clear and stop the DMA
* without clipping the 70 ms envelope tail.
*/
static void drain_with_silence(void)
{
memset(s_pcm_chunk, 0, sizeof(s_pcm_chunk));
for (uint32_t i = 0; i < AUDIO_DMA_BUFFER_COUNT; ++i) {
if (!write_pcm(s_pcm_chunk, AUDIO_DMA_FRAMES)) {
break;
}
}
}
static void stop_output(bool *i2s_running, bool drain)
{
if (!*i2s_running) {
return;
}
if (drain) {
drain_with_silence();
}
(void)i2s_channel_disable(s_tx_channel);
*i2s_running = false;
}
static void audio_task(void *context)
{
(void)context;
bool i2s_running = false;
bool active_session = false;
TickType_t last_ping_tick = 0;
taskENTER_CRITICAL(&s_state_lock);
s_audio_task = xTaskGetCurrentTaskHandle();
taskEXIT_CRITICAL(&s_state_lock);
for (;;) {
const audio_state_snapshot_t snapshot = state_snapshot();
if (!snapshot.enabled || !snapshot.has_target) {
stop_output(&i2s_running, false);
active_session = false;
(void)ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
continue;
}
const TickType_t now = xTaskGetTickCount();
if (active_session) {
const TickType_t interval_ticks =
pdMS_TO_TICKS(map_interval_ms(snapshot.nearest_distance_m));
const TickType_t elapsed_ticks = now - last_ping_tick;
if (elapsed_ticks < interval_ticks) {
(void)ulTaskNotifyTake(pdTRUE, interval_ticks - elapsed_ticks);
continue;
}
}
if (!i2s_running) {
if (i2s_channel_enable(s_tx_channel) != ESP_OK) {
ESP_LOGE(TAG, "Could not start I2S output");
(void)ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(250));
continue;
}
i2s_running = true;
}
last_ping_tick = xTaskGetTickCount();
active_session = true;
const bool completed = play_ping(map_frequency_hz(snapshot.nearest_distance_m));
stop_output(&i2s_running, completed);
}
}
esp_err_t audio_service_init(void)
{
taskENTER_CRITICAL(&s_state_lock);
if (s_initialized) {
taskEXIT_CRITICAL(&s_state_lock);
return ESP_OK;
}
if (s_initializing) {
taskEXIT_CRITICAL(&s_state_lock);
return ESP_ERR_INVALID_STATE;
}
s_initializing = true;
taskEXIT_CRITICAL(&s_state_lock);
init_sine_table();
i2s_chan_config_t channel_config =
I2S_CHANNEL_DEFAULT_CONFIG(AUDIO_I2S_PORT, I2S_ROLE_MASTER);
channel_config.dma_desc_num = AUDIO_DMA_BUFFER_COUNT;
channel_config.dma_frame_num = AUDIO_DMA_FRAMES;
channel_config.auto_clear = true;
const i2s_std_config_t standard_config = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(AUDIO_SAMPLE_RATE_HZ),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(
I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_STEREO),
.gpio_cfg = {
.mclk = I2S_GPIO_UNUSED,
.bclk = AUDIO_PIN_BCLK,
.ws = AUDIO_PIN_LRCLK,
.dout = AUDIO_PIN_DOUT,
.din = I2S_GPIO_UNUSED,
.invert_flags = {
.mclk_inv = false,
.bclk_inv = false,
.ws_inv = false,
},
},
};
esp_err_t err = i2s_new_channel(&channel_config, &s_tx_channel, NULL);
if (err == ESP_OK) {
err = i2s_channel_init_std_mode(s_tx_channel, &standard_config);
}
if (err != ESP_OK) {
if (s_tx_channel != NULL) {
(void)i2s_del_channel(s_tx_channel);
s_tx_channel = NULL;
}
taskENTER_CRITICAL(&s_state_lock);
s_initializing = false;
taskEXIT_CRITICAL(&s_state_lock);
ESP_LOGE(TAG, "NS4168 I2S initialisation failed: %s", esp_err_to_name(err));
return err;
}
const BaseType_t task_created = xTaskCreate(audio_task,
"proximity_audio",
AUDIO_TASK_STACK_BYTES,
NULL,
tskIDLE_PRIORITY + 4,
NULL);
if (task_created != pdPASS) {
(void)i2s_del_channel(s_tx_channel);
s_tx_channel = NULL;
taskENTER_CRITICAL(&s_state_lock);
s_initializing = false;
taskEXIT_CRITICAL(&s_state_lock);
return ESP_ERR_NO_MEM;
}
taskENTER_CRITICAL(&s_state_lock);
s_initialized = true;
s_initializing = false;
taskEXIT_CRITICAL(&s_state_lock);
ESP_LOGI(TAG, "NS4168 ready: 16 kHz, stereo, BCLK=%d LRCLK=%d DOUT=%d",
AUDIO_PIN_BCLK, AUDIO_PIN_LRCLK, AUDIO_PIN_DOUT);
return ESP_OK;
}
void audio_service_set_enabled(bool enabled)
{
TaskHandle_t task;
taskENTER_CRITICAL(&s_state_lock);
s_enabled = enabled;
task = s_audio_task;
taskEXIT_CRITICAL(&s_state_lock);
notify_audio_task(task);
}
bool audio_service_is_enabled(void)
{
bool enabled;
taskENTER_CRITICAL(&s_state_lock);
enabled = s_enabled;
taskEXIT_CRITICAL(&s_state_lock);
return enabled;
}
void audio_service_set_nearest_distance(float distance_m)
{
if (!isfinite(distance_m) || distance_m <= 0.0f) {
audio_service_clear_target();
return;
}
if (distance_m < DISTANCE_MIN_M) {
distance_m = DISTANCE_MIN_M;
} else if (distance_m > DISTANCE_MAX_M) {
distance_m = DISTANCE_MAX_M;
}
TaskHandle_t task;
taskENTER_CRITICAL(&s_state_lock);
s_nearest_distance_m = distance_m;
s_has_target = true;
task = s_audio_task;
taskEXIT_CRITICAL(&s_state_lock);
notify_audio_task(task);
}
void audio_service_clear_target(void)
{
TaskHandle_t task;
taskENTER_CRITICAL(&s_state_lock);
s_has_target = false;
s_nearest_distance_m = 0.0f;
task = s_audio_task;
taskEXIT_CRITICAL(&s_state_lock);
notify_audio_task(task);
}
bool audio_service_has_target(void)
{
bool has_target;
taskENTER_CRITICAL(&s_state_lock);
has_target = s_has_target;
taskEXIT_CRITICAL(&s_state_lock);
return has_target;
}
bool audio_service_get_nearest_distance(float *distance_m)
{
bool has_target;
float distance;
taskENTER_CRITICAL(&s_state_lock);
has_target = s_has_target;
distance = s_nearest_distance_m;
taskEXIT_CRITICAL(&s_state_lock);
if (has_target && distance_m != NULL) {
*distance_m = distance;
}
return has_target;
}