3. I2C/SPI控制总线:Sensor寄存器读写机制、I2C时序与速率、SPI四线模式、多设备地址冲突解决
Sensor 再厉害,也得有人给它下指令。曝光时间、增益、帧率、输出格式……这些参数全得通过控制总线写进 Sensor 内部的寄存器。说白了,控制总线就是主机和 Sensor 之间的“对话通道”。
嵌入式里最常用的两条总线就是 I2C 和 SPI。我做了这么多年 ISP 调优,跟这两条总线打交道的时间比跟我老婆说话的时间都长。今天咱们就把它们彻底聊透。
3.1 I2C:两根线打天下
I2C 只有两根线:SCL(时钟)和 SDA(数据)。结构简单,成本低,是 Sensor 控制总线的主力。我见过的手机摄像头 Sensor,90% 以上都用 I2C。
3.1.1 读写机制:地址 + 寄存器 + 数据
I2C 的读写流程其实就三步:
- 主机发送设备地址(7 位或 10 位),加上读写位。
- 匹配的 Sensor 回 ACK。
- 主机发送寄存器地址,然后读或写数据。
写操作的标准时序长这样:
START | 设备地址(7bit) + W(0) | ACK | 寄存器地址(8bit) | ACK | 数据(8bit) | ACK | STOP
读操作稍微绕一点,得先“伪写”寄存器地址,再重新发 START 做读:
START | 设备地址 + W | ACK | 寄存器地址 | ACK |
START | 设备地址 + R | ACK | 数据 | NACK | STOP
我个人的习惯:读 Sensor 寄存器时,一定要在两次 START 之间留够时间。有些 Sensor 的“反应速度”很慢,连续操作太快会丢 ACK。我曾经在调试 OV5640 时被这个问题坑了整整两天。
3.1.2 时序与速率:别超速
I2C 有几种标准速率模式:
| 模式 | 最高速率 | 适用场景 |
|---|---|---|
| 标准模式 | 100 kbit/s | 老 Sensor、低速配置 |
| 快速模式 | 400 kbit/s | 绝大多数 Sensor 默认 |
| 快速+模式 | 1 Mbit/s | 高帧率配置、多寄存器写入 |
| 高速模式 | 3.4 Mbit/s | 极少用,对 PCB 走线要求极高 |
速率不是越快越好。我遇到过一块板子,I2C 上拉电阻没算好,跑 400kHz 时波形都变形了。你想想看,波形都方波变正弦波了,Sensor 还能正确识别数据吗?
避坑指南:我曾经在量产项目里把 I2C 速率设成 1MHz,结果 10% 的板子 Sensor 初始化失败。后来发现是 PCB 走线过长,信号反射严重。降回 400kHz 后问题消失。量产项目,速率够用就行,别追求极限。
3.1.3 多设备地址冲突:同一个 I2C 总线上挂了两个 Sensor
I2C 总线上每个设备必须有唯一的地址。但很多 Sensor 的 I2C 地址是固定的,比如 0x30。如果你把两个地址相同的 Sensor 挂到同一条总线上,它们会同时响应,数据就乱套了。
解决办法有几种:
- 地址引脚:很多 Sensor 有 ADDR 引脚,拉高或拉低可以改变地址。比如 OV 系列 Sensor,ADDR 拉高时地址是 0x6C,拉低时是 0x30。
- 使用 I2C 多路复用器:比如 PCA9548,可以分时选通不同的 I2C 分支。
- 分时上电:先给 Sensor A 上电完成配置,再给 Sensor B 上电。但这种方法不推荐,因为一旦需要同时操作两个 Sensor 就麻烦了。
我的经验:设计阶段就规划好所有 Sensor 的 I2C 地址。如果两个 Sensor 地址冲突且无法通过引脚修改,果断加 I2C MUX。别想着用软件技巧绕过去,硬件上的问题最终还得硬件解决。
3.2 SPI:四根线,更快更稳
SPI 比 I2C 快得多,而且没有地址概念,靠片选信号 CS 来选择设备。我一般在需要大量读写 Sensor 寄存器时用 SPI,比如高帧率 Sensor 的初始化。
3.2.1 四线模式:SCLK、MOSI、MISO、CS
标准 SPI 四线:
- SCLK:时钟,由主机产生。
- MOSI:主机输出,从机输入。
- MISO:主机输入,从机输出。
- CS:片选,低电平有效。
SPI 的读写是同步的。主机发一个字节的同时,从机也回一个字节。所以 SPI 读操作其实是个“假读真写”——主机得先发一个 dummy 字节,才能把从机的数据“顶”出来。
// SPI 读 Sensor 寄存器示例
uint8_t spi_read_register(uint8_t reg_addr) {
uint8_t tx_buf[2] = {reg_addr | 0x80, 0x00}; // 读命令 + dummy
uint8_t rx_buf[2] = {0};
CS_LOW();
spi_transfer(tx_buf, rx_buf, 2); // 同时收发
CS_HIGH();
return rx_buf[1]; // 第二个字节才是真正的数据
}
注意:SPI 的时钟极性(CPOL)和时钟相位(CPHA)必须和 Sensor 匹配。我见过有人把 CPOL 设反了,读回来的数据全是 0xFF。嗯,这种问题示波器一抓就现原形。
3.2.2 SPI vs I2C:怎么选?
| 对比项 | I2C | SPI |
|---|---|---|
| 线数 | 2 根 | 4 根(最少 3 根) |
| 速率 | 最高 3.4 MHz | 可达 50 MHz 以上 |
| 多设备 | 地址寻址 | 片选寻址 |
| 硬件复杂度 | 低 | 中 |
| 抗干扰 | 一般 | 较好 |
| 典型应用 | Sensor 参数配置 | 高帧率配置、固件升级 |
我个人倾向:普通 Sensor 配置用 I2C,够用且省引脚。如果 Sensor 支持 SPI 且你需要快速写入大量寄存器(比如初始化序列有几百个寄存器),那就用 SPI。
3.3 多设备地址冲突:实战解法
这个问题在实际项目中太常见了。一个摄像头模组里可能有主 Sensor、副 Sensor、甚至还有 EEPROM,它们可能共用一条 I2C 总线。
我整理了一个决策流程,帮你快速定位和解决地址冲突:
我曾经踩过的坑:在一个双目摄像头项目里,两个 Sensor 地址都是 0x30,且都没有地址引脚。我一开始想用软件分时访问——先给 Sensor A 上电配置完,再给 Sensor B 上电。结果发现两个 Sensor 需要同时开始曝光,分时方案根本行不通。最后老老实实加了 I2C MUX,多花了 0.5 美金,但问题彻底解决。
3.4 总结一下
I2C 和 SPI 是 Sensor 控制的两条腿。I2C 简单、省引脚,适合配置参数;SPI 快、抗干扰强,适合大量数据传输。地址冲突是 I2C 的“老毛病”,设计阶段就要想好对策。