3、I2S接口基础:I2S协议时序、Zephyr I2S驱动API、配置采样率与位宽

好,咱们今天来聊聊I2S。这东西在音频领域太常见了,说白了就是数字音频设备之间传数据的“普通话”。你想想看,不管是麦克风、Codec还是DSP,它们之间要交换音频数据,总得有个统一的规矩吧?I2S就是干这个的。

我个人习惯把I2S看作一条三车道的高速公路。一条车道跑时钟(BCLK),一条车道跑帧同步(WS/LRCLK),还有一条车道跑数据(SD)。嗯,有时候还会多一条车道给额外数据,比如TDM模式,但那是后话了。

I2S协议时序:别让数据跑错车道

先看时序。I2S协议最核心的一点是:数据在BCLK的下降沿变化,在上升沿被采样。这个细节我刚开始做驱动时经常搞反,结果出来的声音全是噪声,像收音机没调好台一样。

关键时序参数:

  • BCLK(位时钟):频率 = 采样率 × 位宽 × 通道数。比如44.1kHz、16位、立体声,BCLK就是44.1k × 16 × 2 = 1.4112MHz。
  • WS(字选择)/ LRCLK(左右声道时钟):频率等于采样率。高电平通常表示左声道,低电平表示右声道(也有反的,看具体芯片)。
  • SD(串行数据):MSB先行,紧跟在WS变化之后的一个BCLK周期开始传输。

这里有个坑,我踩过。WS信号的变化时机——标准I2S协议里,WS是在BCLK的下降沿变化,比数据提前一个BCLK周期。但有些厂商的“兼容I2S”实现,WS是在BCLK的上升沿变化。这两种混在一起,数据就全乱了。所以做驱动前,一定要先看Codec的数据手册,确认它用的是哪种“方言”。

我的小技巧:调试I2S时序时,用逻辑分析仪抓BCLK、WS和SD三根线。先看WS变化后,第一个数据位是不是MSB,以及它是在BCLK的哪个边沿被采样的。这个对了,后面基本就稳了。

Zephyr I2S驱动API:从配置到收发

Zephyr的I2S驱动API设计得挺清爽的。核心就几个函数:i2s_configurei2s_writei2s_readi2s_trigger。嗯,还有i2s_buf_freei2s_buf_ready用来查缓冲区状态。

先看配置。这是最关键的步骤,配置错了后面全白搭。

#include <drivers/i2s.h>

struct i2s_config config;
config.word_size = 16;          // 位宽:16位
config.channels = 2;            // 通道数:立体声
config.format = I2S_FMT_DATA_FORMAT_I2S; // 标准I2S格式
config.options = I2S_OPT_BIT_CLK_MASTER | I2S_OPT_FRAME_CLK_MASTER; // 主模式
config.frame_clk_freq = 48000;  // 采样率:48kHz
config.block_size = 1024;       // 每次传输的样本数(每个样本16位)

const struct device *dev_i2s = DEVICE_DT_GET(DT_NODELABEL(i2s0));
if (!device_is_ready(dev_i2s)) {
    printk("I2S设备未就绪\n");
    return -1;
}

int ret = i2s_configure(dev_i2s, I2S_DIR_TX, &config);
if (ret != 0) {
    printk("I2S配置失败: %d\n", ret);
    return ret;
}

这里要注意block_size。它决定了每次i2s_writei2s_read传输的样本数。我建议设成跟你的音频处理缓冲区大小一致,比如1024或512。设得太小,CPU频繁进中断,效率低;设得太大,延迟高,实时性差。

我曾经踩过的坑:配置frame_clk_freq时,以为设成48000就万事大吉了。结果发现实际输出的BCLK频率不对,声音变调。后来查了半天,发现是Zephyr的时钟树配置里,I2S模块的父时钟源频率不是整数倍关系,导致分频后采样率有偏差。解决办法是调整时钟树,或者用PLL给I2S一个精确的时钟源。

配置采样率与位宽:匹配的艺术

采样率和位宽的选择,直接决定了音频质量和系统开销。我一般遵循这个原则:

应用场景 推荐采样率 推荐位宽 说明
语音通话 8kHz / 16kHz 16位 人声频率范围窄,够用
音乐播放 44.1kHz / 48kHz 16位或24位 CD音质标准,24位动态范围更大
专业录音 96kHz / 192kHz 24位或32位 高频细节保留,但数据量巨大

配置时,word_sizeframe_clk_freq必须跟你的Codec或音频源匹配。比如你的Codec只支持16位数据,你设成24位,那它就会把高8位吃掉,或者产生噪声。

另外,Zephyr的i2s_configure函数里有个options字段,可以设置主从模式。我个人习惯把MCU设为主模式,Codec设为从模式。这样时钟由MCU产生,时序控制更灵活。但如果你用USB音频设备,它通常是主机,那MCU就得设成从模式。

调试建议:配置完成后,用i2s_trigger(dev, I2S_DIR_TX, I2S_TRIGGER_START)启动传输。然后抓BCLK和WS的波形,确认频率对不对。BCLK频率 = 采样率 × 位宽 × 通道数,这个公式一定要烂熟于心。

最后,别忘了处理错误。Zephyr的I2S驱动在发生溢出或欠载时,会返回错误码。我一般在i2s_writei2s_read的返回值里检查-EIO-EAGAIN,然后做重试或复位处理。嗯,这个在实时音频系统里特别重要,不然声音会断断续续的。

好了,I2S接口基础就聊到这儿。下一章咱们会深入Zephyr的I2S驱动实现,看看底层是怎么用DMA搬运数据的。到时候再聊。