嵌入式 ANC:STM32 实战

硬件架构

实现实时 ANC 首先要选择合适的硬件平台。控制器需要在微秒级完成自适应滤波运算,同时管理多路音频数据流。

模块功能典型选型
主控制器执行自适应算法STM32F4/F7, ESP32-S3, nRF5340
参考麦克风采集环境噪声Knowles SPH0645, Infineon IM69D130
误差麦克风采集残余噪声Knowles SPH0645, TDK ICS-43434
音频 DAC输出抗噪声信号ES9218, PCM5102
功放驱动扬声器Class-D

参考麦克风位于耳罩外侧,采集外部环境噪声作为算法参考输入。误差麦克风位于耳罩内侧,采集扬声器附近的残余噪声,用于评估降噪效果并驱动自适应更新。音频 DAC 将数字抗噪声信号转为模拟量,经功放放大后驱动扬声器。

MEMS 麦克风接口

现代 ANC 产品普遍使用数字 MEMS 麦克风,其内部集成 ADC 和 PDM(脉冲密度调制)接口。PDM 用 1 位过采样调制,主时钟频率为 1.024–3.072 MHz,双沿采样实现 64× 或 128× 过采样。

STM32 通过 DFSDM(数字滤波器 sigma-delta 调制器)外设直接连接 PDM 麦克风。DFSDM 内置 Sinc 滤波器,可将 PDM 流转换为 16 位或 24 位 PCM 数据,无需外部编解码器。

DFSDM 配置

以下代码配置 STM32 的 DFSDM 通道和滤波器,从一个数字 MEMS 麦克风采集音频数据:

c
 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
void DFSDM_Init(void) {
    hdfsdm1_Channel0.Instance = DFSDM1_Channel0;
    hdfsdm1_Channel0.Init.OutputClock.Divider = 32;
    hdfsdm1_Channel0.Init.OutputClock.Selection = DFSDM_CHANNEL_OUTPUT_CLOCK_AUDIO;
    hdfsdm1_Channel0.Init.Input.Multiplexer = DFSDM_CHANNEL_EXTERNAL_INPUTS;
    hdfsdm1_Channel0.Init.Input.DataPacking = DFSDM_CHANNEL_STANDARD_MODE;
    hdfsdm1_Channel0.Init.Input.Pins = DFSDM_CHANNEL_SAME_CHANNEL_PINS;
    hdfsdm1_Channel0.Init.SerialInterface.Type = DFSDM_CHANNEL_SPI_RISING;
    hdfsdm1_Channel0.Init.SerialInterface.SpiClock = DFSDM_CHANNEL_SPI_CLOCK_INTERNAL;
    hdfsdm1_Channel0.Init.Awd.FilterOrder = DFSDM_CHANNEL_FASTSINC_ORDER;
    hdfsdm1_Channel0.Init.Awd.Oversampling = 64;
    HAL_DFSDM_ChannelInit(&hdfsdm1_Channel0);

    hdfsdm1_Filter0.Instance = DFSDM1_Filter0;
    hdfsdm1_Filter0.Init.RegularParam.Trigger = DFSDM_FILTER_SW_TRIGGER;
    hdfsdm1_Filter0.Init.RegularParam.FastMode = ENABLE;
    hdfsdm1_Filter0.Init.SincOrder = DFSDM_FILTER_SINC3_ORDER;
    hdfsdm1_Filter0.Init.Oversampling = 64;
    hdfsdm1_Filter0.Init.IntOversampling = 1;
    HAL_DFSDM_FilterInit(&hdfsdm1_Filter0);

    HAL_DFSDM_FilterConfigRegChannel(&hdfsdm1_Filter0,
                                     DFSDM_CHANNEL_0,
                                     DFSDM_CONTINUOUS_CONV_ON);
}

关键参数说明:

  • OutputClock.Divider = 32:将系统时钟分频至 PDM 主时钟,典型值为 2.4 MHz
  • SincOrder = DFSDM_FILTER_SINC3_ORDER:三阶 Sinc 滤波器,通带平坦度更好
  • Oversampling = 64:过采样率,配合 PDM 时钟决定最终采样率
mermaid
flowchart TD
    PDM["PDM 比特流<br/>1-bit @ 2.048MHz"] --> SINC["Sinc³ 数字滤波器<br/>降采样"]
    SINC --> DEC["抽取<br/>64x → 32kHz"]
    DEC --> PCM["16-bit PCM<br/>@ 16kHz"]

    classDef raw fill:#f44336,color:#fff
    classDef filter fill:#2196F3,color:#fff
    classDef out fill:#4CAF50,color:#fff
    class PDM raw
    class SINC,DEC filter
    class PCM out

DFSDM 内部的核心处理链如上图所示。PDM 比特流经过 Sinc³ 滤波器完成降采样和抽取,最终输出 16 位 PCM 数据。

选择 Sinc³(三阶 Sinc)而非 FastSinc 滤波器,原因在于延迟与阻带衰减的权衡。Sinc³ 的传输函数为 H(z) = ((1 - z⁻ᴿ) / (1 - z⁻¹))³,在相同过采样率下提供更陡峭的滚降和更好的阻带衰减(约 -60dB/dec),适合音频应用。FastSinc 虽然计算量更低、群延迟更小,但阻带衰减性能差,通常只用于看门狗或阈值检测等辅助功能,不适合 ANC 主音频路径。

双麦克风配置(参考 + 误差)需要两个 DFSDM 通道,分别绑定不同的麦克风数据线。

CMSIS-DSP FXLMS 实现

CMSIS-DSP 是 ARM 官方提供的数字信号处理库,针对 Cortex-M 内核做了 SIMD 优化。在 STM32 上实现 FXLMS 算法时,直接使用 arm_fir_f32 完成滤波计算,避免手写循环。

arm_fir_f32 内部利用 Cortex-M4F/M7 的 DSP 扩展指令集,通过 SIMD(单指令多数据)方式完成滤波运算。在支持 FPU 的内核上,一条 VFMA 指令可在单周期内完成 2 次浮点乘加,配合循环展开(loop unrolling)和双发射(dual-issue)流水线,实际吞吐量可达每周期 2–4 次乘加。CMSIS-DSP 库对此做了深度优化,手写汇编通常无法超越其性能。

数据结构定义

c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include "arm_math.h"
#include "cmsis_os.h"

#define BLOCK_SIZE      64
#define FILTER_TAPS     64
#define NUM_TAPS_STAGE  32

typedef struct {
    arm_fir_instance_f32 fir_inst;
    float32_t state[FILTER_TAPS + BLOCK_SIZE - 1];
    float32_t coeffs[FILTER_TAPS];
} CMSIS_FIR_Filter;

typedef struct {
    CMSIS_FIR_Filter w_filter;
    CMSIS_FIR_Filter s_filter;
    float32_t mu;
    float32_t x_buf[BLOCK_SIZE];
    float32_t e_buf[BLOCK_SIZE];
} FXLMS_CMSIS;

w_filter 是自适应控制器,其系数在运行时持续更新。s_filter 是次级路径模型 S(z) 的估计,系数通过离线辨识预先确定,运行时不调整。

初始化与处理

c
 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
void fxlms_cmsis_init(FXLMS_CMSIS *fx, float32_t *s_hat, uint32_t s_len) {
    arm_fir_init_f32(&fx->w_filter.fir_inst, FILTER_TAPS,
                     fx->w_filter.coeffs, fx->w_filter.state, BLOCK_SIZE);
    memset(fx->w_filter.coeffs, 0, sizeof(fx->w_filter.coeffs));
    arm_fir_init_f32(&fx->s_filter.fir_inst, s_len,
                     s_hat, fx->s_filter.state, BLOCK_SIZE);
    fx->mu = 0.0005f;
}

void fxlms_cmsis_process(FXLMS_CMSIS *fx, float32_t *x_ref,
                         float32_t *e_mic, float32_t *output,
                         uint32_t block_size) {
    float32_t x_filtered[BLOCK_SIZE];
    float32_t y_output[BLOCK_SIZE];

    arm_fir_f32(&fx->w_filter.fir_inst, x_ref, y_output, block_size);
    arm_fir_f32(&fx->s_filter.fir_inst, x_ref, x_filtered, block_size);

    for (uint32_t n = 0; n < block_size; n++) {
        float32_t norm_factor = x_filtered[n] * x_filtered[n] + 1e-6f;
        float32_t step = fx->mu / norm_factor;
        for (uint32_t k = 0; k < FILTER_TAPS; k++) {
            if (n >= k) {
                fx->w_filter.coeffs[k] += step * e_mic[n] * x_filtered[n - k];
            }
        }
    }
    memcpy(output, y_output, block_size * sizeof(float32_t));
}

上面的权重更新使用了归一化 LMS(NLMS)策略。标准的 FXLMS 权重递推公式为:

1
w_k(n+1) = w_k(n) + μ · e(n) · x'(n-k)

其中 μ 为固定步长。NLMS 引入归一化因子,将步长调整为:

1
μ(n) = μ₀ / (||x'(n)||² + ε)

当参考信号功率较大时自动减小步长,避免发散;信号功率小时步长增大,加快收敛。归一化因子中的小常数 ε(代码中取 1e-6)防止除以零。

fxlms_cmsis_process 的执行流程:

  1. 参考信号 x_ref 经过 W(z) 生成抗噪声输出 y_output
  2. 参考信号经过 S(z) 估计生成滤波后的参考信号 x_filtered
  3. 使用滤波后的参考信号更新 W(z) 系数,步长做归一化处理

权重更新使用 x_filtered(而非原始 x_ref),这是 FXLMS 与标准 LMS 的核心区别,补偿了次级路径带来的相位偏移。

FreeRTOS 实时任务框架

在实际产品中,ANC 处理作为一个实时任务运行,由定时器或 DMA 中断触发。

c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void ANC_Processing_Task(void *argument) {
    FXLMS_CMSIS fxlms;
    float32_t s_hat[NUM_TAPS_STAGE] = { /* 离线辨识结果 */ };
    fxlms_cmsis_init(&fxlms, s_hat, NUM_TAPS_STAGE);

    int16_t raw_ref[BLOCK_SIZE];
    int16_t raw_err[BLOCK_SIZE];
    float32_t f_ref[BLOCK_SIZE], f_err[BLOCK_SIZE];
    float32_t output[BLOCK_SIZE];
    int16_t dac_output[BLOCK_SIZE];

    for (;;) {
        ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
        arm_q15_to_float(raw_ref, f_ref, BLOCK_SIZE);
        arm_q15_to_float(raw_err, f_err, BLOCK_SIZE);
        fxlms_cmsis_process(&fxlms, f_ref, f_err, output, BLOCK_SIZE);
        arm_float_to_q15(output, dac_output, BLOCK_SIZE);
        HAL_DAC_Start_DMA(&hdac, DAC_CHANNEL_1, (uint32_t*)dac_output,
                          BLOCK_SIZE, DAC_ALIGN_12B_L);
    }
}
mermaid
flowchart TD
    IRQ["DMA 半传输完成中断"] --> NOTIFY["ulTaskNotifyTake<br/>唤醒 ANC 任务"]
    NOTIFY --> PROC["处理前半块<br/>64 samples"]
    PROC --> DAC["DMA 启动 DAC 输出"]
    IRQ2["DMA 全传输完成中断"] --> NOTIFY2["唤醒 ANC 任务"]
    NOTIFY2 --> PROC2["处理后半块<br/>64 samples"]
    PROC2 --> DAC2["DMA 启动 DAC 输出"]
    DAC2 --> IRQ

    classDef irq fill:#f44336,color:#fff
    classDef task fill:#2196F3,color:#fff
    classDef out fill:#4CAF50,color:#fff
    class IRQ,IRQ2 irq
    class NOTIFY,PROC,NOTIFY2,PROC2 task
    class DAC,DAC2 out

DMA 的半传输和全传输完成中断交替触发,驱动 ANC 任务以流水线方式处理数据块。

任务流程:

  1. 等待通知(ulTaskNotifyTake)—— DMA 完成一次数据采集后发送通知
  2. 类型转换——Q15 到 float(arm_q15_to_float),CMSIS-DSP 提供批量转换函数
  3. 执行 FXLMS——调用 fxlms_cmsis_process
  4. 结果转回 Q15(arm_float_to_q15),启动 DAC 的 DMA 传输

实时性优化

ANC 对延迟极其敏感。从误差麦克风采集到扬声器输出的总延迟需控制在 0.5 ms 以内,否则相移过大导致算法失效甚至正反馈啸叫。

优化手段说明典型效果
降低采样率16 kHz 而非 48 kHz计算量降至 1/3
减小块大小16–32 采样点延迟降至 1–2 ms
降低滤波器阶数32–64 阶每块节省数千次乘加
定点运算Q15/Q31 格式取消浮点转换开销
DMA 双缓冲乒乓缓冲消除数据传输等待
核心绑定ANC 任务绑到专用核心避免任务切换抖动

定点优化

延迟计算

理解延迟需要建立直觉:16 kHz 采样率下,一个采样周期 = 1/16000 = 62.5 μs。块大小 16 → 块延迟 = 16 × 62.5 μs = 1 ms。从噪声进入麦克风到抗噪声离开扬声器的总端到端延迟由三部分构成:

延迟分量典型值说明
块延迟1–4 msDMA 填满一个块的时间,取决于采样率和块大小
计算延迟0.1–0.5 msFXLMS 滤波 + 权重更新耗时
DAC 延迟0.1–0.3 msDAC 转换 + 功放响应时间
总延迟1.2–4.8 ms须 <0.5 ms 才能有效降噪 → 需要极端优化

表中的 0.5 ms 目标是主动降噪的经验阈值。超过此值时,1600 Hz 以上频率的相移超过 180°,反馈会从降噪变为增强(正反馈啸叫)。因此实际 ANC 系统通常采用 16 kHz 采样率 + 16 块大小 + 32 阶滤波器 + 定点运算的组合,将总延迟压到 0.3–0.4 ms。

浮点 FXLMS 在 Cortex-M4/M7 上虽然硬件支持,但定点版本仍可进一步降低延迟。CMSIS-DSP 提供 arm_fir_q15arm_fir_q31 系列函数。将系数和信号转为 Q15 格式后,每次滤波运算从多条指令降为单条 SIMD 指令。

浮点 vs 定点的选择:LMS 权重更新涉及大量乘法累加操作。Q15 格式中,两个 16 位定点数相乘结果为 31 位(符号位保留),需要饱和处理(saturation)后右移回 Q15 范围,否则溢出会导致系数剧烈跳变。浮点数(float32)由硬件 FPU 直接处理,IEEE 754 标准的指数位自动处理动态范围,不存在溢出问题,代码编写更直观。

对比维度float32Q15/Q31
动态范围±3.4×10³⁸±1 (Q15) / ±2³¹ (Q31)
溢出风险需饱和处理
单周期 MAC1 (VFMA)2 (SMUAD)
功耗较高较低
代码可读性

综合来看,浮点适合原型开发和性能充裕的 M7 系列;定点适合追求极致续航和低成本场景,但调试周期显著增加。许多产品在浮点 M7 原型验证后,再移植到定点 M4 做成本优化。

DMA 双缓冲

双缓冲(ping-pong)模式是 DMA 数据传输中的经典技巧。两个缓冲区交替角色:当 DMA 控制器向缓冲区 A 写入新数据时,CPU 处理缓冲区 B 中已就绪的数据;DMA 完成 A 的写入后立即切换到 B,同时 CPU 开始处理 A。这种交替机制消除了数据搬移的等待时间,让 CPU 和 DMA 完全并行工作。

在 16 kHz 采样率、块大小 64 的条件下,DMA 填充一个缓冲区需要约 4 ms。如果使用单缓冲,CPU 必须等待 DMA 完成后才能开始处理,浪费一半的时间。双缓冲将有效计算吞吐量提升近一倍。

c
1
2
3
4
5
// DMA 乒乓缓冲——active_buf 在 0 和 1 之间交替
HAL_DFSDM_FilterRegConvStart_DMA(&hdfsdm1_Filter0,
                                 ping_pong_buf[active_buf],
                                 BLOCK_SIZE);
// CPU 同时处理 ping_pong_buf[1 - active_buf] 中的数据

下面的动画演示了双缓冲的交替过程——DMA 写入和 CPU 处理并行进行,两个缓冲区不断交换角色:

Buffer A Buffer B
DMA 写入 CPU 处理
CPU 处理 DMA 写入
Ping-Pong Buffer · DMA 写入 ↔ CPU 读取

异构架构(ESP32 + STM32)

在复杂 ANC 系统中,单一 MCU 难以同时满足实时音频处理和非实时系统管理的需求。一种常见的思路是采用异构架构:ESP32 负责无线连接、用户交互和系统管理,STM32 专注实时音频处理。

双芯片通过 UART、SPI 或 I2C 通信。传递的数据包括音频流和控制命令。

自定义通信帧格式

c
1
2
3
4
5
6
7
typedef struct {
    uint8_t  header[2];     // 0xAA 0x55
    uint8_t  type;          // 0x01=音频数据, 0x02=控制命令
    uint16_t seq;
    uint16_t len;
    int16_t  samples[];
} __attribute__((packed)) AudioFrame;

header 字段用于帧同步,接收端检测到 0xAA 0x55 后开始解析。type 区分音频采样流和参数配置命令。seq 用于丢包检测和重排序。

ESP32 端处理 Wi-Fi/蓝牙协议栈、用户界面和参数更新。当用户通过手机 App 调整 ANC 模式时,ESP32 将新的滤波器系数或步长参数封装为控制帧发送给 STM32。STM32 收到后热更新算法参数,无需暂停音频处理。

Mermaid:STM32 ANC 数据流

mermaid
flowchart TD
    subgraph Input
        REF["参考麦克风<br/>PDM"]
        ERR["误差麦克风<br/>PDM"]
    end
    subgraph STM32
        DFSDM["DFSDM<br/>PDM→PCM"]
        DMA["DMA<br/>乒乓缓冲"]
        FXLMS["FXLMS<br/>CMSIS-DSP"]
        DAC["DAC<br/>PCM→模拟"]
        TASK["FreeRTOS<br/>ANC Task"]
    end
    subgraph Output
        SPK["扬声器<br/>抗噪声"]
    end
    subgraph ESP
        WIFI["Wi-Fi/蓝牙<br/>参数下发"]
    end

    REF --> DFSDM
    ERR --> DFSDM
    DFSDM --> DMA
    DMA --> TASK
    TASK --> FXLMS
    FXLMS --> DAC
    DAC --> SPK
    WIFI -->|UART/SPI| TASK