2、音频硬件抽象层(HAL):I2S接口详解、PDM接口详解、TDM接口详解

好,咱们直接进入正题。音频硬件抽象层,说白了就是帮我们把底层那些寄存器操作、时序细节给封装起来。你写应用层代码的时候,不用管I2S的时钟极性是上升沿还是下降沿采样,调用一个API就完事了。Zephyr的音频HAL做得挺清爽,今天我就把三个最常用的接口——I2S、PDM、TDM——给你掰开揉碎讲清楚。

2.1 I2S接口详解

I2S,全称Inter-IC Sound,飞利浦在80年代搞出来的标准。到现在还是音频传输的扛把子。我最早接触I2S是在做一款智能音箱的时候,当时被时钟同步问题折腾了好几天,后来才发现是BCLK和LRCLK的相位关系没搞对。

I2S的核心信号线只有三条:

  • BCLK(位时钟):每个脉冲对应一个数据位。采样率 × 位深 × 通道数,就是BCLK的频率。比如44.1kHz、16bit、立体声,BCLK就是44.1k × 16 × 2 = 1.4112MHz。
  • LRCLK(左右声道时钟):也叫帧时钟。高电平左声道,低电平右声道,或者反过来。频率等于采样率。
  • SD(串行数据):数据线,可以是输入或输出。

重要:I2S标准规定,数据在BCLK的下降沿变化,在上升沿被接收。但有些芯片厂商不按套路出牌,比如某些DAC要求上升沿变化。所以移植驱动时,一定要先看数据手册的时序图。

在Zephyr里,I2S的配置通过struct i2s_config结构体完成。我习惯先配好这个结构体,再调用i2s_configure()。给你看个例子:

struct i2s_config i2s_cfg = {
    .word_size = 16,          // 位深:16bit
    .channels = 2,            // 立体声
    .format = I2S_FMT_DATA_FORMAT_I2S,  // 标准I2S格式
    .options = I2S_OPT_PINGPONG,        // 乒乓缓冲,减少中断频率
    .frame_clk_freq = 48000,  // 采样率48kHz
    .mem_slab = &i2s_mem_slab,
    .block_size = 1024,       // 每次传输的字节数
    .timeout = 1000,          // 超时时间,毫秒
};

个人经验:block_size的选择很关键。太小了,中断频繁,CPU占用高;太大了,延迟增加。我一般根据音频帧大小来定,比如48kHz、16bit立体声,一帧是192字节,block_size设成1024刚好能装5帧多,延迟控制在20ms以内。

收发数据用i2s_write()i2s_read()。注意,这两个函数是异步的,数据通过缓冲区传递。你调用i2s_write()后,函数会立刻返回,数据在后台通过DMA传输。等传输完成,会触发回调或者信号量。

void audio_callback(const struct device *dev, struct i2s_event *event, void *user_data) {
    if (event->type == I2S_EVENT_TX_COMPLETE) {
        k_sem_give(&tx_sem);  // 通知发送完成
    }
}

// 注册回调
i2s_event_callback_register(i2s_dev, audio_callback, NULL);

// 发送数据
i2s_write(i2s_dev, buf, sizeof(buf));
k_sem_take(&tx_sem, K_FOREVER);  // 等待发送完成

2.2 PDM接口详解

PDM,脉冲密度调制。跟I2S不一样,PDM只有一条数据线和一条时钟线。数据线上是一串高速的1/0脉冲,密度代表模拟信号的幅度。说白了,就是用1的密度来表示电压高低。

PDM的特点:

  • 数据速率极高,通常是目标采样率的64倍或128倍。比如要得到48kHz的音频,PDM时钟可能是3.072MHz。
  • 需要后处理——抽取滤波(Decimation Filter),把高速1bit流转换成低速多bit PCM数据。
  • 抗干扰能力强,因为信号是差分传输的。

我在做一款TWS耳机时用过PDM麦克风。当时踩了个坑:PDM时钟频率不能随便设,必须跟麦克风的规格匹配。有些麦克风支持64倍过采样,有些只支持128倍。设错了,出来的声音全是噪声。

避坑指南:PDM接口的PCB走线要特别注意。时钟线和数据线要等长,避免信号偏移。我遇到过因为走线不等长导致高频衰减的问题,后来在Layout上加了蛇形线才解决。

Zephyr里PDM的配置相对简单。主要设置采样率和时钟分频:

struct pdm_config pdm_cfg = {
    .frequency = 3072000,     // PDM时钟3.072MHz
    .gain = 0,                // 增益,0dB
    .decimation_rate = 64,    // 64倍抽取,得到48kHz
    .mode = PDM_MODE_STEREO,  // 立体声模式
};

读取PDM数据时,得到的是经过抽取滤波后的PCM数据。Zephyr内部集成了CIC滤波器,你不需要自己实现。调用pdm_read()就能拿到干净的音频数据。

int16_t pcm_buffer[512];
pdm_read(pdm_dev, pcm_buffer, sizeof(pcm_buffer));
// 现在pcm_buffer里就是48kHz、16bit的PCM数据

2.3 TDM接口详解

TDM,时分复用。你可以把它理解成I2S的升级版。I2S只能传两个声道(左+右),TDM可以传8个、16个甚至更多。每个声道在时间上被分配了一个时隙(Slot)。

TDM的应用场景:

  • 多麦克风阵列,比如智能音箱的远场拾音,需要4个、6个甚至8个麦克风同时工作。
  • 多通道音频输出,比如5.1声道、7.1声道环绕声。
  • 音频总线共享,多个编解码器挂在同一条总线上。

TDM的配置比I2S多了一个参数——时隙数。每个时隙对应一个声道。时钟频率也要相应提高:BCLK = 采样率 × 位深 × 时隙数。

struct i2s_config tdm_cfg = {
    .word_size = 24,          // 24bit数据
    .channels = 8,            // 8个时隙
    .format = I2S_FMT_DATA_FORMAT_I2S,  // 数据格式
    .options = I2S_OPT_PINGPONG | I2S_OPT_FRAME_CLK_MASTER,
    .frame_clk_freq = 48000,  // 采样率48kHz
    .block_size = 2048,
};

注意,TDM的时隙对齐方式很重要。有些芯片要求数据在第一个BCLK上升沿开始,有些要求下降沿。我建议你在初始化后,先发一段已知数据,再用逻辑分析仪抓波形确认一下。

关键点:TDM模式下,每个时隙的数据是连续的。比如8个时隙、24bit数据,一帧就是8×24=192bit。接收端必须知道每个时隙的起始位置,否则数据会错位。Zephyr的驱动会自动处理时隙对齐,但前提是你在i2s_config里正确设置了channels字段。

实际项目中,TDM最头疼的问题是时钟抖动。多个麦克风共享一条时钟线,如果时钟质量不好,所有通道都会受影响。我建议在TDM时钟线上加一个RC滤波器,或者用专用的时钟缓冲器。

2.4 三个接口的对比与选型建议

特性 I2S PDM TDM
信号线数量 3(BCLK, LRCLK, SD) 2(CLK, DATA) 3(BCLK, SYNC, SD)
最大通道数 2 1(单声道) 16+
数据格式 多bit PCM 1bit脉冲密度 多bit PCM
典型应用 立体声播放/录音 数字麦克风 多通道阵列
抗干扰能力 中等 中等
CPU负载 低(DMA传输) 中(需抽取滤波) 低(DMA传输)

我的选型建议:

  • 做立体声音乐播放,用I2S,简单可靠。
  • 做麦克风阵列,用TDM,一根总线搞定多个麦克风。
  • 做低功耗、小尺寸产品,用PDM麦克风,省引脚、省空间。

嗯,这三个接口搞明白了,音频硬件抽象层你就掌握了八成。剩下的两成,是在实际项目中踩坑积累出来的。下一章我们讲音频管道和数据处理,到时候你会看到这些接口怎么跟音频流处理结合起来。