第3章 I2C/SPI控制总线:I2C协议在Codec配置中的应用、SPI接口与寄存器读写、多设备总线仲裁
好,咱们进入控制总线这一章。说实话,这部分是音频驱动开发里最容易被忽视,但又最容易出坑的地方。你想想看,Codec芯片本身再牛,如果控制总线没调通,它就是个哑巴。我这些年调试过的Codec不下二十款,从低端到高端,几乎每次翻车都跟总线时序或地址冲突有关。
3.1 I2C协议在Codec配置中的应用
I2C是Codec配置的绝对主力。为什么?因为它只需要两根线——SCL和SDA,而且支持多设备挂载。对于Codec这种寄存器数量不多、配置频率不高的外设来说,I2C简直是天选之子。
I2C的通信流程其实很简单:
- 起始条件:SCL高电平时,SDA从高变低
- 设备地址+读写位:7位地址+1位读写标志
- 从机应答:从机拉低SDA表示收到
- 数据传输:每字节后跟一个应答位
- 停止条件:SCL高电平时,SDA从低变高
我个人习惯把Codec的I2C地址记成两个:写地址和读地址。比如WM8960,7位地址是0x1A,那写地址就是0x34,读地址是0x35。很多新手搞混这个,我刚开始也犯过这错。
核心要点:Codec的I2C寄存器读写,本质就是「设备地址 + 寄存器地址 + 数据」的三段式操作。
写操作示例:
// 向WM8960的寄存器0x10写入0x03
// 步骤:起始 -> 0x34(写) -> ACK -> 0x10(寄存器) -> ACK -> 0x03(数据) -> ACK -> 停止
uint8_t buf[2] = {0x10, 0x03};
i2c_master_write(0x34, buf, 2); // 封装好的I2C写函数
读操作稍微绕一点:
// 从WM8960的寄存器0x10读取当前值
// 步骤:起始 -> 0x34(写) -> ACK -> 0x10(寄存器) -> ACK -> 重启 -> 0x35(读) -> ACK -> 读取数据 -> NACK -> 停止
uint8_t reg = 0x10;
uint8_t val;
i2c_master_write(0x34, ®, 1); // 先写寄存器地址
i2c_master_read(0x35, &val, 1); // 再读数据
嗯,这里要注意:有些Codec支持自动地址递增。你连续读写多个寄存器时,它会自动把地址加1。这在批量配置时特别有用。我在项目中遇到过,用单次读写方式配置整个Codec,结果初始化花了50ms,换成批量模式后直接降到5ms。
经验之谈:调试I2C时,逻辑分析仪是你的好朋友。别信代码里的打印,直接抓波形看。我曾经花了两小时查一个「写不进去」的问题,最后发现是上拉电阻焊错了。
3.2 SPI接口与寄存器读写
SPI在Codec里用得少一些,但一旦用上,基本都是高速或高精度场景。比如一些专业音频Codec,采样率192kHz、24bit,寄存器配置量巨大,I2C那100kHz或400kHz的速度就有点捉襟见肘了。
SPI的四根线:
- SCLK:时钟,由主设备产生
- MOSI:主出从入
- MISO:主入从出
- CS:片选,低电平有效
SPI读写Codec寄存器的套路和I2C不太一样。大多数Codec用的是「16位地址+8位数据」的格式,或者「8位地址+8位数据」。我见过最奇葩的是TI的TLV320AIC系列,它用7位地址+9位数据,因为寄存器是9位的。
// SPI写操作示例:向地址0x1234写入0x56
// 通常格式:CS拉低 -> 发送16位地址(高位在前) -> 发送8位数据 -> CS拉高
uint16_t addr = 0x1234;
uint8_t data = 0x56;
cs_low();
spi_transfer((addr >> 8) & 0xFF); // 地址高8位
spi_transfer(addr & 0xFF); // 地址低8位
spi_transfer(data); // 数据
cs_high();
读操作更讲究。有些Codec用「写地址+读数据」的两阶段模式,有些则用「地址包带读写位」的方式。我建议你仔细看数据手册的时序图,别想当然。
避坑指南:我曾经在SPI读操作上栽过跟头。某款Codec要求读操作时,地址的最高位要置1表示读。我没注意,结果读回来的全是0。查了两天才发现是地址格式搞错了。
SPI的速度优势很明显。I2C标准模式100kHz,快速模式400kHz。而SPI轻松跑到10MHz甚至更高。你想想看,如果Codec有100个寄存器要配置,I2C需要100 * (地址+数据) 约300个时钟周期,而SPI只需要100 * (地址+数据) 约24个时钟周期(假设16位地址+8位数据)。差距是10倍以上。
3.3 多设备总线仲裁
这是个大坑。一个I2C总线上挂多个设备,或者一个SPI总线上挂多个从机,怎么保证不打架?
I2C的多设备仲裁:
I2C本身支持多主多从。仲裁发生在数据线SDA上。当两个主设备同时拉低SDA时,谁先拉低谁赢。但说实话,在Codec场景下,我们很少用多主模式。一般都是单主(SoC)多从(Codec、传感器、EEPROM等)。
关键点在于地址不能冲突。每个I2C设备都有唯一的7位地址。Codec通常提供1-2个地址引脚(如AD0、AD1),通过拉高拉低来改变地址。比如:
| AD0电平 | 7位地址 | 写地址 | 读地址 |
|---|---|---|---|
| GND | 0x1A | 0x34 | 0x35 |
| VDD | 0x1B | 0x36 | 0x37 |
如果板子上有两个相同的Codec,你必须给它们分配不同的地址。我见过一个项目,硬件工程师把两个Codec的AD0都接地了,结果I2C总线上两个设备地址一样,读写全乱套。
SPI的多设备仲裁:
SPI的仲裁简单粗暴——靠片选CS。每个从机独占一根CS线。主设备想跟谁说话,就把谁的CS拉低,其他从机的CS保持高电平。从机看到CS为高,就乖乖闭嘴,MISO输出高阻。
但这里有个隐藏问题:MISO线的电平冲突。如果两个从机同时被选中(比如软件bug导致两个CS同时拉低),它们会同时在MISO上输出数据,造成短路。轻则数据错误,重则烧毁IO口。
设计原则:SPI总线上,同一时刻只能有一个从机的CS为低。这是铁律,别挑战它。
实际项目中,我建议你这样做:
- 画清楚总线拓扑图:哪个设备挂在哪条总线上,地址是什么,CS用哪个GPIO
- 初始化时做设备探测:上电后先尝试读每个设备的ID寄存器,确认设备存在且地址正确
- 加锁保护:多线程环境下,I2C/SPI操作要加互斥锁,防止两个线程同时操作总线
- 留足时序余量:I2C的上拉电阻、SPI的时钟频率,别卡着极限值设计
我的习惯:每个Codec驱动里,我都会写一个check_id()函数。上电后先读芯片的ID寄存器,跟预期值比对。对不上就直接报错,不往下走。这招帮我抓到了至少三次硬件焊接问题。
最后说一句,总线仲裁这事儿,软件能做的有限。硬件设计上把地址和CS分配好,比你在代码里加一万个判断都管用。嗯,今天就到这儿,下一章咱们聊聊Codec的电源管理和时钟树,那才是真正让人头大的部分。