2. I2S音频总线协议详解:标准模式、左对齐、右对齐与DSP模式
各位做音频算法的朋友,咱们今天聊聊I2S。说实话,这玩意儿是Codec驱动里最基础也最容易踩坑的地方。我见过不少算法工程师,算法写得漂亮,结果移植到Codec上声音不对——查到最后,往往是I2S格式配错了。
I2S全称是Inter-IC Sound,飞利浦在80年代搞出来的。它专门用来在芯片之间传数字音频。你想想看,ADC采集完声音,Codec处理完,再送到CPU或者DSP——这一路全靠I2S在跑。
核心要点:I2S只有三条线——BCLK(位时钟)、LRCLK(帧时钟/左右声道选择)、SD(串行数据)。有些场景会多一条MCLK(主时钟),但那是给Codec内部PLL用的,不是I2S协议本身必须的。
2.1 四种模式的本质区别
我在项目里被问得最多的问题就是:“这几种模式到底差在哪?”说白了,就是数据跟帧时钟的对齐关系不同。咱们一个一个看。
2.1.1 I2S标准模式(飞利浦模式)
这是最常用的模式。它的特点是:数据比LRCLK跳变晚一个BCLK。
- LRCLK从高变低(或低变高)时,表示新的一帧开始
- 但数据不是立刻出现,而是等一个BCLK周期后才开始传
- 数据是MSB(最高有效位)在前
为什么要晚一个BCLK?我记得最早看飞利浦的文档时也纳闷。后来做项目才明白——这是给从机留出时间识别帧起始。嗯,硬件设计上的一个小巧思。
2.1.2 左对齐模式
左对齐,顾名思义,数据跟LRCLK的跳变同时开始。没有那个延迟的BCLK。
- LRCLK跳变的第一个BCLK上升沿,数据就开始传
- MSB对齐到帧起始
- 如果数据位宽小于槽位宽度,低位补0
这种模式在一些老Codec上比较常见。我遇到过一块ADI的Codec,默认就是左对齐,当时没看手册直接配了标准模式,结果声音全是噪声——折腾了两天才发现。
2.1.3 右对齐模式
右对齐跟左对齐正好相反——数据对齐到帧的末尾。
- LRCLK跳变后,前面是一段“空”的BCLK周期
- 数据的LSB(最低有效位)对齐到帧结束
- MSB在帧结束前的一个BCLK开始传
这种模式现在用得少了。但我记得在一些日本厂商的Codec上见过,比如AKM的某些型号。如果你在移植老芯片的驱动,得留个心眼。
2.1.4 DSP模式
DSP模式其实不是严格意义上的I2S,它是为了跟DSP的McASP或SSI接口对接而生的。它的特点是:帧起始用脉冲信号,而不是电平。
- LRCLK在帧开始时产生一个BCLK宽度的脉冲
- 左右声道数据连续传输,中间没有间隔
- 数据可以是MSB在前,也可以是LSB在前(看具体配置)
我在移植一个语音算法到TI的DSP平台时,就遇到了DSP模式。当时Codec是标准I2S,DSP那边只支持DSP模式——最后不得不在FPGA里做了个格式转换。所以,选型阶段就得确认两边模式是否匹配。
2.2 时序图解读
光说理论不够,咱们直接看时序。下面这张图是我用SVG画的四种模式对比,你仔细看看就明白了。
你看,标准模式和左对齐就差一个BCLK的偏移。右对齐前面多了一段空闲位。DSP模式最特殊——LRCLK变成了脉冲,数据连续传。
2.3 如何在Linux ALSA驱动中配置
好,理论讲完了,咱们来点实际的。在Linux ALSA驱动里配I2S格式,核心就是设置snd_soc_dai_set_fmt()这个函数。
ALSA用SND_SOC_DAIFMT_*宏来定义格式。我列个表,你一看就明白。
| ALSA宏定义 | 对应模式 | 数据对齐 | 帧起始方式 |
|---|---|---|---|
SND_SOC_DAIFMT_I2S |
I2S标准模式 | 晚1个BCLK | 电平跳变 |
SND_SOC_DAIFMT_LEFT_J |
左对齐 | 立即开始 | 电平跳变 |
SND_SOC_DAIFMT_RIGHT_J |
右对齐 | 对齐到帧尾 | 电平跳变 |
SND_SOC_DAIFMT_DSP_A |
DSP模式(脉冲在数据前) | 脉冲后立即开始 | 脉冲 |
SND_SOC_DAIFMT_DSP_B |
DSP模式(脉冲与数据同时) | 与脉冲同时 | 脉冲 |
实际代码里怎么配?我给你看一段我项目里的真实代码。
/* 设置Codec和CPU之间的I2S格式 */
static int my_board_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params)
{
struct snd_soc_pcm_runtime *rtd = substream->private_data;
struct snd_soc_dai *codec_dai = rtd->codec_dai;
struct snd_soc_dai *cpu_dai = rtd->cpu_dai;
int ret;
/* 设置CPU DAI为I2S标准模式,从机 */
ret = snd_soc_dai_set_fmt(cpu_dai,
SND_SOC_DAIFMT_I2S |
SND_SOC_DAIFMT_NB_NF |
SND_SOC_DAIFMT_CBS_CFS);
if (ret < 0) {
dev_err(rtd->dev, "Failed to set CPU DAI format\n");
return ret;
}
/* 设置Codec DAI为I2S标准模式,主机 */
ret = snd_soc_dai_set_fmt(codec_dai,
SND_SOC_DAIFMT_I2S |
SND_SOC_DAIFMT_NB_NF |
SND_SOC_DAIFMT_CBM_CFM);
if (ret < 0) {
dev_err(rtd->dev, "Failed to set Codec DAI format\n");
return ret;
}
/* 设置数据位宽和采样率 */
ret = snd_soc_dai_set_tdm_slot(cpu_dai, 0x3, 0x3, 2, 32);
if (ret < 0) {
dev_err(rtd->dev, "Failed to set TDM slot\n");
return ret;
}
return 0;
}
小提示:SND_SOC_DAIFMT_NB_NF表示正常BCLK极性、正常帧时钟极性。如果你的Codec要求反转极性,可以换成SND_SOC_DAIFMT_IB_NF或SND_SOC_DAIFMT_NB_IF。我建议你配完格式后,用示波器抓一下BCLK和LRCLK的波形,确认极性对不对。
2.4 避坑指南
做I2S配置这么多年,我踩过的坑不少。挑几个典型的说说。
坑一:主从模式不匹配
我曾经在一个项目里,CPU设成了主机(产生BCLK和LRCLK),Codec也设成了主机——两边都在产生时钟,结果总线冲突,数据全乱套。记住:总线上只能有一个主机。通常CPU做主机,Codec做从机。
坑二:位宽不一致
Codec支持24位数据,但CPU只配了16位。数据传过去,低位被截断,声音听起来像“破音”。我建议你统一用32位槽位,哪怕数据只有16位——这样兼容性最好。
坑三:DSP模式选错变体
DSP_A和DSP_B就差一个BCLK的偏移。选错了,数据全对不上。我一般先看DSP的参考手册,确认它用的是哪种,再配Codec那边。
2.5 小结
I2S协议本身不复杂,但细节多。标准模式、左对齐、右对齐、DSP模式——它们的区别说白了就是数据跟帧时钟的对齐关系。在ALSA驱动里配格式,记住三步:设格式、设极性、设主从。每一步都确认清楚,基本不会出问题。