2、传感器驱动开发:I2C/SPI寄存器配置、初始化序列、时钟与同步
驱动开发,说白了就是让主控和传感器能「说上话」。我做了这么多年调试,见过太多硬件连上了、软件却死活不通的案例。问题往往就出在底层的通信协议和初始化时序上。这一章,咱们就把这些基本功掰开揉碎讲清楚。
2.1 通信接口选型:I2C vs SPI
选哪个接口,不是拍脑袋决定的。我习惯先看三个指标:速度、引脚数、从机数量。
| 特性 | I2C | SPI |
|---|---|---|
| 总线拓扑 | 多主多从,地址寻址 | 单主多从,片选寻址 |
| 信号线数量 | 2 根(SCL, SDA) | 4 根(SCLK, MOSI, MISO, CS) |
| 典型速率 | 100 kHz ~ 1 MHz | 10 MHz ~ 50 MHz |
| 传输方式 | 半双工 | 全双工 |
| 硬件复杂度 | 低(开漏输出,需上拉电阻) | 中(推挽输出,无需上拉) |
| 适用场景 | 配置寄存器、低速数据读取 | 高速图像数据、批量寄存器操作 |
我的经验: 对于传感器初始化配置,I2C 完全够用。但如果要读取 RAW 图数据或进行 OTP 烧录,SPI 是唯一选择。我曾经在一个项目中为了省引脚强行用 I2C 读 10-bit RAW 数据,结果帧率直接腰斩——教训深刻。
2.2 I2C 寄存器配置实战
I2C 的驱动核心就三件事:起始条件、从机地址 + 读写位、数据字节 + ACK。嗯,这里要注意,很多新手会忽略 ACK 检测。
2.2.1 标准读写流程
// 伪代码:写寄存器
i2c_start();
i2c_send_byte(DEV_ADDR << 1 | 0); // 写地址
if (!i2c_wait_ack()) error();
i2c_send_byte(REG_ADDR); // 寄存器地址
if (!i2c_wait_ack()) error();
i2c_send_byte(DATA); // 写入数据
if (!i2c_wait_ack()) error();
i2c_stop();
// 伪代码:读寄存器
i2c_start();
i2c_send_byte(DEV_ADDR << 1 | 0); // 写地址
i2c_wait_ack();
i2c_send_byte(REG_ADDR); // 指定寄存器
i2c_wait_ack();
i2c_start(); // 重复起始条件
i2c_send_byte(DEV_ADDR << 1 | 1); // 读地址
i2c_wait_ack();
data = i2c_read_byte(NACK); // 读取数据,主机发 NACK
i2c_stop();
避坑指南: 我曾经在调试一颗 200 万像素的传感器时,发现读回来的寄存器值总是错位。排查了两天,最后发现是 I2C 时钟频率设成了 400 kHz,而传感器只支持 100 kHz。速率不匹配,数据就会「串位」。建议初始化阶段先用低速(100 kHz)通信,确认无误后再提频。
2.3 SPI 寄存器配置实战
SPI 比 I2C 快,但坑也不少。我个人的习惯是:先确认极性(CPOL)和相位(CPHA),再写代码。
2.3.1 SPI 模式选择
| 模式 | CPOL | CPHA | 数据采样沿 |
|---|---|---|---|
| 模式 0 | 0 | 0 | 上升沿采样 |
| 模式 1 | 0 | 1 | 下降沿采样 |
| 模式 2 | 1 | 0 | 下降沿采样 |
| 模式 3 | 1 | 1 | 上升沿采样 |
关键点: 传感器 datasheet 里一定会写明支持哪种 SPI 模式。如果选错了,读回来的数据全是 0xFF 或 0x00。别问我怎么知道的——我踩过这个坑。
2.3.2 16-bit 寄存器地址处理
很多传感器使用 16-bit 寄存器地址。SPI 传输时,地址和数据都是 MSB 先行。我习惯这样封装:
uint8_t spi_read_reg(uint16_t reg_addr) {
uint8_t tx_buf[3];
uint8_t rx_buf[3];
// 构造命令:地址高字节 + 地址低字节 + 任意数据(用于产生时钟)
tx_buf[0] = (reg_addr >> 8) & 0xFF;
tx_buf[1] = reg_addr & 0xFF;
tx_buf[2] = 0x00;
CS_LOW();
spi_transfer(tx_buf, rx_buf, 3);
CS_HIGH();
return rx_buf[2]; // 第三个字节才是读回的数据
}
小技巧: 有些传感器在读操作时需要先发一个读命令字节(如 0x03),再发地址。务必仔细看 datasheet 的时序图,不要想当然。
2.4 初始化序列设计
传感器上电后,不是直接就能出图的。你需要按照严格的顺序配置寄存器。我把它总结为「三步走」:
- 软复位与 ID 校验:先发软复位命令,然后读取芯片 ID 确认通信正常。
- 时钟与 PLL 配置:设置输入时钟频率、PLL 倍频因子、输出像素时钟。
- 工作模式与输出格式:配置分辨率、帧率、数据格式(RAW10/RAW12)、MIPI 通道数。
2.4.1 一个典型的初始化序列示例
// 以 OV5640 为例(简化)
void sensor_init() {
// Step 1: 软复位
i2c_write_reg(0x3008, 0x80); // 复位寄存器
delay_ms(10); // 等待复位完成
// Step 2: 校验 ID
uint16_t id = i2c_read_reg(0x300A) << 8 | i2c_read_reg(0x300B);
if (id != 0x5640) {
// 通信异常,进入错误处理
return;
}
// Step 3: 配置 PLL
i2c_write_reg(0x3034, 0x18); // 设置 PLL 分频
i2c_write_reg(0x3035, 0x11); // 设置 PLL 倍频
i2c_write_reg(0x3036, 0x46); // 设置 PLL 控制
// Step 4: 设置输出格式
i2c_write_reg(0x4300, 0x30); // 输出格式:RAW10
i2c_write_reg(0x501F, 0x01); // ISP 旁路
// Step 5: 启动输出
i2c_write_reg(0x3008, 0x02); // 退出软件 standby
delay_ms(50);
}
避坑指南: 我曾经在初始化序列中漏掉了「等待 PLL 锁定」这一步。结果传感器有时出图、有时不出图,完全看心情。后来加上 PLL 锁定状态轮询,问题解决。记住:每个步骤之间要有足够的延时或状态检查。
2.5 时钟与同步
时钟是传感器的心脏。主控给传感器提供 MCLK(主时钟),传感器内部通过 PLL 倍频产生像素时钟。如果时钟不准,图像会出现条纹、闪烁甚至无图。
2.5.1 时钟树结构
2.5.2 时钟频率计算
举个例子。假设 MCLK = 24 MHz,传感器内部 PLL 倍频到 672 MHz,再分频得到像素时钟 PCLK = 84 MHz。那么一帧图像的时间怎么算?
// 参数示例
#define PCLK 84000000 // 84 MHz
#define H_TOTAL 2200 // 一行总像素数(含 blanking)
#define V_TOTAL 1125 // 一帧总行数(含 blanking)
// 帧率计算
float frame_rate = (float)PCLK / (H_TOTAL * V_TOTAL);
// 结果:84,000,000 / (2200 * 1125) ≈ 33.9 fps
核心公式: 帧率 = PCLK / (H_TOTAL × V_TOTAL)。H_TOTAL 和 V_TOTAL 包含有效像素和 blanking 区域,这些值在 datasheet 里都能找到。
2.6 同步机制
传感器输出图像时,会伴随 VSYNC(帧同步)和 HSYNC(行同步)信号。主控需要根据这些信号来正确接收数据。
- VSYNC:高电平表示一帧开始,低电平表示一帧结束(或相反,取决于配置)。
- HSYNC:高电平表示一行有效数据开始。
- PCLK:每个时钟沿输出一个像素数据。
我的习惯: 在调试初期,我会用逻辑分析仪抓 VSYNC 和 HSYNC 的波形。看一眼就知道传感器是否正常输出。如果 VSYNC 周期不对,先查 PLL 配置;如果 HSYNC 宽度不对,查 H_TOTAL 设置。
2.7 本章小结
驱动开发没有捷径。I2C 和 SPI 的寄存器读写是基本功,初始化序列是骨架,时钟与同步是灵魂。我见过太多人一上来就调 3A,结果发现图像根本不出——回头一看,驱动层就没通。
记住:先通通信,再出图,最后调优。顺序别搞反了。