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 初始化序列设计

传感器上电后,不是直接就能出图的。你需要按照严格的顺序配置寄存器。我把它总结为「三步走」:

  1. 软复位与 ID 校验:先发软复位命令,然后读取芯片 ID 确认通信正常。
  2. 时钟与 PLL 配置:设置输入时钟频率、PLL 倍频因子、输出像素时钟。
  3. 工作模式与输出格式:配置分辨率、帧率、数据格式(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 时钟树结构

传感器时钟树结构 MCLK (主时钟) PLL (倍频/分频) 像素时钟 (PCLK) MIPI 时钟 (MCLK) 像素阵列扫描 MIPI 数据输出 说明:MCLK 由主控提供,PLL 内部倍频后分两路——一路给像素阵列,一路给 MIPI 接口

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,结果发现图像根本不出——回头一看,驱动层就没通。

记住:先通通信,再出图,最后调优。顺序别搞反了。