第三章 音频通路与数据流管理:I2S/PCM/TDM接口配置、音频DMA与Buffer管理、多路音频流的混音与路由
各位同学,欢迎来到第三章。这一章我们聊聊音频数据在TWS耳机里到底是怎么「跑」起来的。
说实话,很多刚入行的工程师把音频算法想得很玄乎,觉得算法调好了就万事大吉。但我在项目里踩过的坑告诉我——音频通路没搭好,算法再牛也白搭。你想想看,数据从麦克风进来,经过DMA搬运,再通过I2S送给DSP,中间任何一个环节出问题,出来的声音要么断断续续,要么噪声满天飞。
所以这一章,咱们就把音频通路这件事彻底讲透。
3.1 I2S/PCM/TDM接口配置——音频数据的「高速公路」
先说说接口。I2S、PCM、TDM,这三个东西说白了都是数字音频的传输协议。但它们的应用场景不太一样。
I2S 是最常见的,左右声道分开传。我习惯把它想象成一条双车道公路——左声道走左边,右声道走右边,互不干扰。
PCM 其实跟I2S很像,但时序上有点区别。有些老一点的codec芯片只支持PCM模式,这时候你就得注意了。
TDM 就厉害了。它是一条多车道高速公路,可以同时传4路、8路甚至16路音频。我在做多麦克风阵列的项目时,TDM几乎是标配。
配置这些接口时,有几个关键参数你必须搞清楚:
| 参数 | 说明 | 常见取值 |
|---|---|---|
| 采样率 | 每秒采集的样本数 | 8k, 16k, 48k, 96k |
| 位深 | 每个样本的比特数 | 16bit, 24bit, 32bit |
| 声道数 | 音频通道数量 | 1(单声道), 2(立体声), 4+ |
| BCLK频率 | 位时钟,决定传输速率 | 采样率 × 位深 × 声道数 |
| 帧同步格式 | 左右声道对齐方式 | I2S左对齐, 右对齐, DSP模式 |
3.2 音频DMA与Buffer管理——数据搬运的「物流系统」
接口配置好了,数据怎么从硬件搬到内存里?这就靠DMA了。
DMA(直接存储器访问)说白了就是一个专门干苦力活的搬运工。CPU不用亲自搬数据,DMA帮你搞定。但这里有个关键问题——Buffer怎么管理?
我个人习惯用双缓冲(ping-pong buffer)方案。原理很简单:
- 两个Buffer,一个叫Ping,一个叫Pong
- DMA往Ping里写数据时,CPU处理Pong里的数据
- 写满了,交换角色
这样做的好处是:CPU和DMA各干各的,互不阻塞。我见过一些新手工程师只用单Buffer,结果音频处理时经常出现「咔咔」的爆音——那就是Buffer被覆盖了。
来看看一个典型的DMA配置流程:
// 伪代码示例:配置音频DMA
void audio_dma_init() {
// 1. 配置DMA通道
dma_channel_config_t cfg;
cfg.src_addr = (uint32_t)&I2S_DR; // 源:I2S数据寄存器
cfg.dst_addr = (uint32_t)ping_buffer; // 目的:Ping Buffer
cfg.transfer_size = AUDIO_FRAME_SIZE; // 每次传输大小
cfg.circular_mode = true; // 循环模式
// 2. 启动DMA
dma_channel_start(DMA_CH_AUDIO_IN, &cfg);
// 3. 注册完成中断回调
dma_set_complete_callback(DMA_CH_AUDIO_IN, audio_buffer_ready);
}
3.3 多路音频流的混音与路由——音频数据的「交通指挥」
到了这一步,才是真正体现功力的地方。TWS耳机里通常有多个音频流:
- 通话麦克风输入(上行)
- 降噪参考麦克风输入
- 蓝牙解码后的音乐数据(下行)
- 提示音/语音助手音频
- 混音后的输出
这些流怎么管理?我把它分成两个层面:路由和混音。
3.3.1 音频路由
路由就是决定「谁的数据往哪走」。比如:
- 通话时:麦克风数据 → 上行处理 → 蓝牙发送
- 听音乐时:蓝牙解码数据 → 下行处理 → 喇叭输出
- 降噪时:参考麦克风数据 → 降噪算法 → 喇叭输出(叠加在音乐上)
我习惯用一张路由表来管理:
// 音频路由表结构
typedef struct {
audio_source_t src; // 源:MIC_IN, BT_DECODE, TONE_GEN
audio_sink_t dst; // 目的:SPK_OUT, BT_ENCODE, DSP_IN
bool enable; // 是否启用
uint8_t gain; // 增益调节
} audio_route_t;
// 典型路由配置
audio_route_t route_table[] = {
{MIC_IN, DSP_ANC, true, 0}, // 麦克风→降噪处理
{BT_DECODE, DSP_EQ, true, 0}, // 蓝牙→EQ处理
{DSP_ANC, MIXER, true, 0}, // 降噪结果→混音器
{DSP_EQ, MIXER, true, 0}, // EQ结果→混音器
{MIXER, SPK_OUT, true, 0}, // 混音结果→喇叭
};
3.3.2 音频混音
混音就是把多路音频叠加到一起。听起来简单,但做起来有讲究。
核心问题:溢出处理。 两路16bit音频相加,结果可能超过16bit范围。怎么办?
我常用的方法有两种:
- 衰减再相加: 每路先衰减一半(-6dB),再加起来。这样肯定不会溢出,但信噪比会损失一点。
- 饱和处理: 直接相加,超出范围就截断到最大值。适合动态范围大的场景。
来看一个简单的混音实现:
// 两路音频混音(带饱和处理)
void audio_mixer(int16_t *in1, int16_t *in2, int16_t *out, int len) {
for (int i = 0; i < len; i++) {
int32_t sum = (int32_t)in1[i] + (int32_t)in2[i];
// 饱和截断
if (sum > 32767) sum = 32767;
if (sum < -32768) sum = -32768;
out[i] = (int16_t)sum;
}
}
🔑 关键点: 混音时一定要用32位中间变量做累加,否则16位直接相加会溢出。我见过有人直接用 int16_t 做加法,结果声音一大了就失真——那是数据被截断了,不是算法问题。
3.4 实战中的常见问题与排查思路
最后,分享几个我在项目中实际遇到的问题,希望能帮你少走弯路。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 声音断断续续 | DMA Buffer太小或中断优先级低 | 增大Buffer,检查中断嵌套 |
| 左右声道串扰 | I2S帧同步配置错误 | 用示波器看BCLK和WS时序 |
| 混音后声音失真 | 未做饱和处理或增益过大 | 检查混音代码,降低输入增益 |
| 某路音频没声音 | 路由表未正确配置或DMA未启动 | 逐级检查路由使能状态 |
好了,这一章的内容就到这里。音频通路管理看起来琐碎,但它是整个音频系统的骨架。骨架搭好了,算法才能在上面跑得稳。下一章我们会聊更具体的算法集成,到时候见。
公众号:蓝海资料掘金营,微信deep3321