第三章 音频通路与数据流管理: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模式
⚠️ 我曾经踩过的坑: 有一次我把BCLK算错了,导致音频数据对不齐。听起来就像有人在说话时突然卡住。查了两天才发现是BCLK频率设成了采样率×位深,忘了乘以声道数。嗯,这种低级错误,犯一次就记住了。

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);
}
💡 我的经验: Buffer大小怎么定?我一般按「10ms音频数据」来算。比如48kHz采样率、16bit立体声,10ms的数据量就是 48000 × 2 × 2 × 0.01 = 1920字节。这个大小既不会太频繁触发中断,也不会延迟太大。

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范围。怎么办?

我常用的方法有两种:

  1. 衰减再相加: 每路先衰减一半(-6dB),再加起来。这样肯定不会溢出,但信噪比会损失一点。
  2. 饱和处理: 直接相加,超出范围就截断到最大值。适合动态范围大的场景。

来看一个简单的混音实现:

// 两路音频混音(带饱和处理)
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未启动 逐级检查路由使能状态
💡 我的调试习惯: 遇到音频通路问题,我一般先做「信号注入测试」——在某个节点注入一个1kHz正弦波,然后用示波器或音频分析仪看后面能不能收到。这样一级一级排查,很快就能定位问题。

好了,这一章的内容就到这里。音频通路管理看起来琐碎,但它是整个音频系统的骨架。骨架搭好了,算法才能在上面跑得稳。下一章我们会聊更具体的算法集成,到时候见。


公众号:蓝海资料掘金营,微信deep3321