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麦克风,省引脚、省空间。
嗯,这三个接口搞明白了,音频硬件抽象层你就掌握了八成。剩下的两成,是在实际项目中踩坑积累出来的。下一章我们讲音频管道和数据处理,到时候你会看到这些接口怎么跟音频流处理结合起来。