3. DSP芯片与平台概览
音频数据流处理,说白了就是解决「怎么把麦克风采集到的数字音频,高效、不卡顿地送到DSP芯片里去算」。这里面有三个核心环节:I2S协议负责物理层传输,DMA负责搬运数据不占CPU,乒乓Buffer负责解决读写冲突。我做了这么多年音频嵌入式开发,这三个东西但凡有一个没处理好,出来的声音不是爆音就是断流。
3.1 I2S协议详解
I2S(Inter-IC Sound)是飞利浦在1986年制定的数字音频总线标准。你想想看,那时候CD刚普及,飞利浦自己就是CD标准的制定者之一,所以I2S天然就是为高质量音频传输设计的。
3.1.1 信号线定义
I2S总线只需要三根线:
| 信号 | 全称 | 作用 |
|---|---|---|
| SCK | Serial Clock(位时钟) | 每个脉冲对应一个数据位,频率 = 采样率 × 位宽 × 声道数 |
| WS | Word Select(帧时钟/左右声道选择) | 高电平=右声道,低电平=左声道(标准I2S定义) |
| SD | Serial Data(串行数据) | 二进制补码格式传输音频样本 |
举个例子:CD音质(44.1kHz采样率、16位、立体声),SCK频率 = 44100 × 16 × 2 = 1.4112MHz。WS频率刚好等于采样率44.1kHz。
3.1.2 数据格式与时序
标准I2S协议有个关键特点:数据在SCK的下降沿变化,在上升沿采样。而且MSB(最高有效位)先传输,WS切换后,数据延迟一个SCK周期才开始传输。
为什么会这样?因为WS切换时,接收端需要时间准备接收新声道的数据。这个延迟设计保证了数据稳定性。
// I2S时序伪代码示例
// 假设16位数据,左声道先传输
void i2s_transmit_sample(int16_t left, int16_t right) {
// WS = 0 (左声道)
for (int i = 15; i >= 0; i--) {
SD = (left >> i) & 0x01; // MSB first
toggle_SCK();
}
// WS = 1 (右声道)
for (int i = 15; i >= 0; i--) {
SD = (right >> i) & 0x01;
toggle_SCK();
}
}
3.2 DMA双缓冲机制
如果每次I2S收到一个样本就触发一次中断,CPU会疯掉的。44.1kHz立体声,每秒要处理88200次中断,CPU啥也别干了。所以我们需要DMA(直接存储器访问)。
3.2.1 为什么需要DMA
DMA的本质是:让硬件自己搬数据,搬完了通知你一声。CPU只需要在DMA传输完成时处理一整块数据,而不是每个样本都去响应中断。
我记得刚入行时,有个同事用轮询方式读I2S数据,CPU占用率直接飙到80%以上。换成DMA后,降到5%以下。差距就这么大。
3.2.2 双缓冲(Double Buffering)原理
单缓冲的问题很明显:DMA正在往Buffer A写数据时,CPU想读Buffer A,就会读到一半的数据——这叫「读写冲突」。双缓冲就是准备两个Buffer:
- Buffer A:DMA写入(采集侧)
- Buffer B:CPU读取(处理侧)
当DMA写满Buffer A后,自动切换到Buffer B继续写,同时触发中断通知CPU:「Buffer A满了,快来处理」。CPU处理Buffer A时,DMA在写Buffer B,互不干扰。
3.2.3 DMA配置要点
配置DMA时,有几个参数必须搞对:
| 参数 | 说明 | 常见坑 |
|---|---|---|
| 传输宽度 | 每次传输的位数(8/16/32位) | 必须与I2S数据位宽一致 |
| 突发传输 | 连续传输次数 | 设太大可能影响总线仲裁 |
| 循环模式 | 传输完成后自动重新开始 | 双缓冲必须开启循环模式 |
| 中断触发 | 半传输中断 / 完全传输中断 | 双缓冲用半传输中断切换Buffer |
// STM32 HAL库 DMA配置示例(双缓冲模式)
DMA_HandleTypeDef hdma_i2s_rx;
hdma_i2s_rx.Instance = DMA1_Stream3;
hdma_i2s_rx.Init.Channel = DMA_CHANNEL_0;
hdma_i2s_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_i2s_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_i2s_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_i2s_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_i2s_rx.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_i2s_rx.Init.Mode = DMA_CIRCULAR; // 循环模式,双缓冲必须
hdma_i2s_rx.Init.Priority = DMA_PRIORITY_HIGH;
hdma_i2s_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
// 设置双缓冲内存地址
HAL_DMAEx_MultiBufferStart(&hdma_i2s_rx,
(uint32_t)&spi2_instance->DR, // 外设地址(I2S数据寄存器)
(uint32_t)buffer_a, // 内存地址1
(uint32_t)buffer_b, // 内存地址2
BUFFER_SIZE); // 每个Buffer的样本数
3.3 乒乓Buffer设计模式
双缓冲是硬件层面的机制,乒乓Buffer是软件层面的设计模式。说白了,双缓冲是「怎么让DMA在两个Buffer之间切换」,乒乓Buffer是「怎么让CPU和DMA协同工作」。两者配合,才能实现流畅的音频数据流。
3.3.1 乒乓Buffer的状态机
一个典型的乒乓Buffer有三个状态:
- 空闲态:两个Buffer都可用,等待DMA启动
- 写态:DMA正在写入某个Buffer,CPU处理另一个
- 切换态:DMA写满一个Buffer,触发中断,交换角色
3.3.2 代码实现框架
// 乒乓Buffer管理结构体
typedef struct {
int16_t buffer_a[AUDIO_BUF_SIZE];
int16_t buffer_b[AUDIO_BUF_SIZE];
volatile uint8_t active_buf; // 0=Buffer A, 1=Buffer B
volatile uint8_t buf_ready; // 1=有数据待处理
} pingpong_buffer_t;
pingpong_buffer_t pp_buf;
// DMA半传输中断回调(Buffer切换点)
void HAL_I2S_RxHalfCpltCallback(I2S_HandleTypeDef *hi2s) {
// DMA写满了Buffer A,准备切换
pp_buf.active_buf = 0; // CPU处理Buffer A
pp_buf.buf_ready = 1;
}
// DMA完全传输中断回调
void HAL_I2S_RxCpltCallback(I2S_HandleTypeDef *hi2s) {
// DMA写满了Buffer B
pp_buf.active_buf = 1; // CPU处理Buffer B
pp_buf.buf_ready = 1;
}
// 主循环中处理音频数据
void audio_processing_loop() {
while(1) {
if (pp_buf.buf_ready) {
pp_buf.buf_ready = 0;
// 获取当前可处理的Buffer指针
int16_t *data_to_process;
if (pp_buf.active_buf == 0) {
data_to_process = pp_buf.buffer_a;
} else {
data_to_process = pp_buf.buffer_b;
}
// 在这里做音频处理(滤波、增益、降噪等)
process_audio_frame(data_to_process, AUDIO_BUF_SIZE);
}
// 其他任务...
}
}
3.3.3 Buffer大小的权衡
Buffer大小直接影响延迟和稳定性:
| Buffer大小 | 延迟 | 稳定性 | 适用场景 |
|---|---|---|---|
| 小(32-64样本) | 低(<1.5ms) | 低(容易溢出) | 实时对讲、乐器效果器 |
| 中(128-256样本) | 中(3-6ms) | 中 | 语音处理、降噪 |
| 大(512-1024样本) | 高(12-23ms) | 高 | 音乐播放、录音 |
3.4 整体数据流架构
把I2S、DMA、乒乓Buffer串起来,就是完整的音频数据流处理链路。下面这张图展示了核心流程:
从这张图可以清楚看到:音频数据从麦克风进来,经过I2S串行化传输,DMA自动把数据搬到乒乓Buffer里,CPU只在Buffer切换时被中断通知,然后处理已经准备好的数据块。整个过程中,CPU大部分时间都在做音频算法,而不是等数据。