ESP32-S3 的双核 Xtensa LX7 处理器带有向量指令集扩展,适合执行嵌入式 ANC 所需的实时 DSP 运算。配合 ESP-DSP 库可以高效实现自适应滤波器。
I2S 麦克风采集 ANC 需要至少两路同步输入:参考麦克风(采集环境噪声)和误差麦克风(采集残余误差)。ESP32-S3 的 I2S 外设支持同时接收多路 ADC 数据,配置为 16 位、16 kHz 采样即可满足消费级 ANC 需求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include "driver/i2s.h"
#define I2S_SAMPLE_RATE 16000
#define I2S_BUFFER_SIZE 1024
void i2s_mic_init ( void ) {
i2s_config_t i2s_config = {
. mode = I2S_MODE_MASTER | I2S_MODE_RX ,
. sample_rate = I2S_SAMPLE_RATE ,
. bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT ,
. channel_format = I2S_CHANNEL_FMT_ONLY_LEFT ,
. communication_format = I2S_COMM_FORMAT_STAND_I2S ,
. intr_alloc_flags = ESP_INTR_FLAG_LEVEL1 ,
. dma_buf_count = 4 ,
. dma_buf_len = I2S_BUFFER_SIZE ,
. use_apll = true ,
. tx_desc_auto_clear = false ,
. fixed_mclk = 0
};
i2s_pin_config_t pin_config = {
. bck_io_num = GPIO_NUM_4 ,
. ws_io_num = GPIO_NUM_5 ,
. data_out_num = I2S_PIN_NO_CHANGE ,
. data_in_num = GPIO_NUM_6
};
i2s_driver_install ( I2S_NUM_0 , & i2s_config , 0 , NULL );
i2s_set_pin ( I2S_NUM_0 , & pin_config );
}
void read_mic_samples ( int16_t * buffer , size_t len ) {
size_t bytes_read ;
i2s_read ( I2S_NUM_0 , buffer , len * sizeof ( int16_t ), & bytes_read , portMAX_DELAY );
}
use_apll = true 启用 Audio PLL(音频锁相环)以获得更精确的音频时钟。APLL 由专用的 PLL 电路生成,比默认的 I2S 时钟抖动更小——时钟抖动会直接转化为采样时序误差,在 ANC 这种对相位精度敏感的系统中会严重降低降噪深度。dma_buf_count = 4 和 dma_buf_len = 1024 的含义是:4 个缓冲区轮转,每个缓冲区 1024 样本,总缓冲区大小为 4096 采样 @ 16 kHz ≈ 256 ms。较大的缓冲区可以容忍 CPU 调度的偶发延迟,但会增加 I2S 采集的延迟,需要在实时性和鲁棒性之间取舍。
下图为整个 ANC 数据流的路径,从参考麦克风和误差麦克风经 DMA 进入 NLMS 处理器,最终输出到扬声器:
flowchart TD
MIC1["参考麦克风<br/>I2S CH0"] --> DMA["DMA 双缓冲<br/>128 samples/frame"]
MIC2["误差麦克风<br/>I2S CH1"] --> DMA
DMA --> DSP["NLMS 处理<br/>归一化步长"]
DSP --> DAC["I2S 输出<br/>16bit PCM"]
DAC --> SPK["扬声器"]
classDef input fill:#2196F3,color:#fff
classDef proc fill:#9C27B0,color:#fff
classDef output fill:#4CAF50,color:#fff
class MIC1,MIC2,DMA input
class DSP proc
class DAC,SPK output自适应滤波器核心 NLMS 滤波器是整个 ANC 系统的核心。在 ESP32-S3 上,滤波器阶数取 64 已能覆盖 1 kHz 以下的主要低频噪声。环形缓冲区避免了每次采样后整体移位数据,将每个样本的复杂度从 O(N) 降为 O(1)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "esp_dsp.h"
#include "driver/i2s.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define SAMPLE_RATE 16000
#define FRAME_SIZE 128
#define FILTER_ORDER 64
typedef struct {
float w [ FILTER_ORDER ]; // 滤波器系数
float x_ring [ FILTER_ORDER ]; // 参考信号环形缓冲区
float s_hat [ 32 ]; // 次级路径估计
float s_ring [ 32 ]; // 滤波后信号环形缓冲区
int w_ptr ; // 环形缓冲区写指针
int s_ptr ;
float mu ; // 自适应步长
} ESP32_ANC ;
static ESP32_ANC g_anc ;
void esp_anc_init ( void ) {
memset ( & g_anc , 0 , sizeof ( g_anc ));
g_anc . mu = 0.0005f ;
dsps_fft2r_init_fc32 ( NULL , CONFIG_DSP_MAX_FFT_SIZE );
}
次级路径 s_hat 长度为 32 个抽头,通常通过离线辨识获得——在部署前播放一段白噪声并用麦克风录制,通过 LMS 估计出扬声器到误差麦克风的传递函数。
NLMS 帧处理 每个音频帧(128 样本)的实时处理流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static inline float fast_convolve ( const float * w , const float * x , int ptr , int len ) {
float output = 0.0f ;
for ( int i = 0 ; i < len ; i ++ ) {
int idx = ( ptr + len - i ) % len ;
output += w [ i ] * x [ idx ];
}
return output ;
}
void esp_anc_process_frame ( float * ref , float * err , float * out , int len ) {
for ( int n = 0 ; n < len ; n ++ ) {
g_anc . x_ring [ g_anc . w_ptr ] = ref [ n ];
g_anc . s_ring [ g_anc . s_ptr ] = ref [ n ];
float y = fast_convolve ( g_anc . w , g_anc . x_ring , g_anc . w_ptr , FILTER_ORDER );
float x_filt = fast_convolve ( g_anc . s_hat , g_anc . s_ring , g_anc . s_ptr , 32 );
float power = 0.0f ;
for ( int i = 0 ; i < FILTER_ORDER ; i ++ )
power += g_anc . x_ring [ i ] * g_anc . x_ring [ i ];
float norm_step = g_anc . mu / ( power + 1e-6 f );
for ( int i = 0 ; i < FILTER_ORDER ; i ++ ) {
int idx = ( g_anc . w_ptr + FILTER_ORDER - i ) % FILTER_ORDER ;
g_anc . w [ i ] += norm_step * err [ n ] * g_anc . x_ring [ idx ];
}
out [ n ] = y ;
g_anc . w_ptr = ( g_anc . w_ptr + 1 ) % FILTER_ORDER ;
g_anc . s_ptr = ( g_anc . s_ptr + 1 ) % 32 ;
}
}
环形缓冲区的卷积访问模式用动画看最直观——注意读取指针如何按反方向回绕:
环形缓冲区 · 卷积
5 阶 FIR · 卷积计算
w = [0.40, 0.30, 0.20, 0.07, 0.03]
fast_convolve 用环形索引计算卷积,避免每次重新排序数据。NLMS 系数更新时,正则项 1e-6f 防止参考信号功率过小导致除零。步长 mu = 0.0005 在收敛速度和稳态失调之间取折中——对于稳态噪声(如风扇、发动机)足够快,又不会在收敛后过度波动。
双核任务分配 将 ANC 循环锁定到 CPU1(即 PRO_CPU),CPU0 处理 Wi-Fi/蓝牙协议栈和其他系统任务。I2S 采样率为 16 kHz、帧长 128 样本时,每个帧的处理时间预算约为 8 ms。
下图展示了两个核心的具体分工——CPU0 负责系统管理,CPU1 专门处理 ANC 实时流水线:
flowchart TD
subgraph CPU0["CPU0 - 系统管理"]
A1["WiFi/BT 协议栈"]
A2["配置管理"]
A3["OTA 升级"]
end
subgraph CPU1["CPU1 - 实时 ANC"]
B1["I2S 读取<br/>双通道音频"]
B2["NLMS/FXLMS<br/>滤波运算"]
B3["I2S 写入<br/>抗噪声输出"]
B1 --> B2 --> B3 --> B1
end
classDef sys fill:#2196F3,color:#fff
classDef anc fill:#f44336,color:#fff
class A1,A2,A3 sys
class B1,B2,B3 anc 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void anc_task ( void * arg ) {
int16_t * raw_buffer = heap_caps_malloc ( FRAME_SIZE * 2 * sizeof ( int16_t ),
MALLOC_CAP_INTERNAL );
float * f_ref = heap_caps_malloc ( FRAME_SIZE * sizeof ( float ), MALLOC_CAP_INTERNAL );
float * f_err = heap_caps_malloc ( FRAME_SIZE * sizeof ( float ), MALLOC_CAP_INTERNAL );
float * f_out = heap_caps_malloc ( FRAME_SIZE * sizeof ( float ), MALLOC_CAP_INTERNAL );
int16_t * dac_buffer = heap_caps_malloc ( FRAME_SIZE * sizeof ( int16_t ), MALLOC_CAP_INTERNAL );
size_t bytes_read ;
for (;;) {
i2s_read ( I2S_NUM_0 , raw_buffer , FRAME_SIZE * 2 * sizeof ( int16_t ),
& bytes_read , portMAX_DELAY );
for ( int i = 0 ; i < FRAME_SIZE ; i ++ ) {
f_ref [ i ] = raw_buffer [ i * 2 ] / 32768.0f ;
f_err [ i ] = raw_buffer [ i * 2 + 1 ] / 32768.0f ;
}
esp_anc_process_frame ( f_ref , f_err , f_out , FRAME_SIZE );
for ( int i = 0 ; i < FRAME_SIZE ; i ++ ) {
float clamped = fmaxf ( - 1.0f , fminf ( 1.0f , f_out [ i ]));
dac_buffer [ i ] = ( int16_t )( clamped * 32767.0f );
}
i2s_write ( I2S_NUM_1 , dac_buffer , FRAME_SIZE * sizeof ( int16_t ),
& bytes_read , portMAX_DELAY );
}
}
void app_main ( void ) {
esp_anc_init ();
i2s_mic_init ();
xTaskCreatePinnedToCore ( anc_task , "anc_task" , 8192 , NULL ,
configMAX_PRIORITIES - 1 , NULL , 1 );
}
heap_caps_malloc(..., MALLOC_CAP_INTERNAL) 确保所有缓冲区都在内部 SRAM 中,避免因 PSRAM 访问延迟(通常比 SRAM 慢 3–5 倍)导致处理超时。任务栈大小 8192 字节足够容纳 DSP 调用链的局部变量。
I2S 输出(I2S_NUM_1)驱动外部 DAC 或 Class-D 功放,扬声器发出反相声波与原始噪声在声学上抵消。输出前通过 fmaxf/fminf 限幅,防止 DAC 削波失真。
绑到 CPU1 而非 CPU0 的原因:CPU0 在 ESP-IDF 中负责 Wi-Fi 和蓝牙协议栈,Wi-Fi 硬件中断优先级高且密集——每次信标帧接收或 TCP ACK 都会触发中断。这些中断会抢占 CPU0 上的用户任务,导致 ANC 帧处理出现不可预测的延迟抖动。CPU1(APP_CPU)不受 Wi-Fi 中断影响,因此是绑定实时 DSP 任务的更优选择。
ESP32-S3 vs ESP32 :S3 的 Xtensa LX7 处理器增加了向量指令扩展(AE_* 系列),可在单周期内完成多个乘加(MAC)操作,NLMS 的卷积更新速度比 ESP32(LX6)快约 3–5 倍。ESP32 的 CPU0 和 CPU1 均为 LX6 架构,不带向量扩展,DSP 效率明显偏低。如果使用 ESP32(非 S3),建议将滤波器阶数降至 32 或将帧长缩短至 64 样本以保持实时性。
构建配置 在 sdkconfig 中需要启用以下选项:
CONFIG_ESP_DSP_ENABLED=y — 启用 ESP-DSP 库CONFIG_DSP_MAX_FFT_SIZE=4096 — FFT 最大长度(ESP-DSP 初始化需要)CONFIG_FREERTOS_UNICORE=n — 启用双核调度CONFIG_I2S_SUPPRESS_DEPRECATE_WARN=n — 允许使用旧版 I2S API(ESP-IDF v5.x 移除了旧 API,需使用新的 I2S 驱动层)如果使用 ESP-IDF v5.0 以上版本,建议迁移到新的 i2s_chan_handle_t 驱动模式,但处理逻辑一致。
延迟预算 整个流水线的延迟由以下环节累积:
环节 延迟 I2S DMA 采集 1 帧(128 样本)≈ 8 ms ADC 转换 + 传输 ~0.5 ms 帧处理(NLMS) ~2–3 ms DAC 转换 + 传输 ~0.5 ms 声学路径 ~0.1–0.3 ms 总计 ~11–12 ms
下图为延迟预算的详细分解:
flowchart TD
A["I2S 采集<br/>~2ms"] --> B["DMA 传输<br/>~0.1ms"]
B --> C["NLMS 计算<br/>~3-5ms"]
C --> D["I2S 输出<br/>~1ms"]
D --> TOTAL["总延迟<br/>~6-8ms<br/>有效频率: ~60-80Hz"]
classDef stage fill:#2196F3,color:#fff
classDef result fill:#f44336,color:#fff
class A,B,C,D stage
class TOTAL result消费级 ANC 的有效频率上限约为 1 / (2 × 总延迟) ≈ 40–45 Hz,因此这套方案主要针对 50 Hz 以下的低频轰鸣声——空调压缩机、风扇、路面胎噪等。要提高有效频率,需要缩短帧长、降低滤波器阶数,或在更强的 MCU 上运行。