3. 传感器驱动开发:I2C/SPI配置寄存器、初始化序列编写、PLL与时钟树配置
摄像头传感器不是焊上去就能用的。你得跟它“对话”,告诉它怎么工作。这个对话,就是通过 I2C 或 SPI 总线来读写寄存器。说白了,传感器就是一堆寄存器,你往里面写特定的值,它就按你的要求干活。
我刚开始做摄像头驱动时,总觉得这步很简单。结果呢?图像出来全是花的,或者干脆没信号。后来才明白,寄存器配置的顺序、时序、甚至某个 bit 的延迟,都能决定成败。这一章,咱们就把这些坑填上。
3.1 通信接口选型:I2C vs SPI
大部分摄像头传感器都支持 I2C 和 SPI 两种配置接口。怎么选?我列个表,你一看就明白。
| 对比项 | I2C | SPI |
|---|---|---|
| 引脚数 | 2 根线(SDA, SCL) | 4 根线(MOSI, MISO, SCLK, CS) |
| 速度 | 标准 100kHz / 快速 400kHz | 通常 10MHz ~ 50MHz |
| 寻址方式 | 7位或10位设备地址 | 片选信号(CS)选择设备 |
| 多设备支持 | 总线挂载多个设备,地址不能冲突 | 每个设备独立CS,数量受GPIO限制 |
| 典型场景 | 配置寄存器、读取状态 | 配置寄存器 + 高速数据读取 |
我个人习惯:配置寄存器用 I2C。因为引脚少,布线方便。除非传感器要求 SPI 才能达到某个配置速度,或者你需要同时读写大量寄存器,否则 I2C 足够了。
3.2 I2C 寄存器读写:从时序到代码
I2C 的读写流程其实很固定。写一个寄存器,就是:起始信号 → 设备地址(写)→ 寄存器地址 → 数据 → 停止信号。读稍微复杂点,要先写寄存器地址,再重新发起始信号和设备地址(读),然后收数据。
嗯,这里要注意:很多传感器在连续读取时,地址会自动递增。你只需要发一次寄存器地址,后面连续读就行。但有些老芯片不会自动递增,你得每个字节都重新发地址。我踩过这个坑,读回来的数据全是同一个寄存器的值,图像颜色完全不对。
下面是一个典型的 I2C 写寄存器函数,我用的是 Linux 用户态 i2c-dev 接口:
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>
int sensor_write_reg(int fd, uint8_t dev_addr, uint16_t reg_addr, uint8_t data) {
struct i2c_msg msg;
struct i2c_rdwr_ioctl_data ioctl_data;
uint8_t buf[3];
// 假设寄存器地址是16位,高字节在前
buf[0] = (reg_addr >> 8) & 0xFF;
buf[1] = reg_addr & 0xFF;
buf[2] = data;
msg.addr = dev_addr;
msg.flags = 0; // 写操作
msg.len = 3;
msg.buf = buf;
ioctl_data.msgs = &msg;
ioctl_data.nmsgs = 1;
if (ioctl(fd, I2C_RDWR, &ioctl_data) < 0) {
perror("I2C write failed");
return -1;
}
return 0;
}
这段代码看起来简单,但有个细节:寄存器地址的字节序。有些传感器用 8 位地址,有些用 16 位。16 位地址里,还有高字节在前和低字节在前的区别。你得看数据手册,千万别想当然。
3.3 SPI 配置:速度与模式
SPI 比 I2C 快,但配置项也多一些。主要是 时钟极性(CPOL)和时钟相位(CPHA)。这两个参数决定了数据在时钟的哪个边沿采样。
- CPOL=0:空闲时时钟为低电平
- CPOL=1:空闲时时钟为高电平
- CPHA=0:在第一个时钟边沿采样数据
- CPHA=1:在第二个时钟边沿采样数据
大部分摄像头传感器用 Mode 0(CPOL=0, CPHA=0)或 Mode 3(CPOL=1, CPHA=1)。你想想看,为什么?因为这两种模式下,数据都是在时钟的上升沿采样,下降沿变化。很多 MCU 的 SPI 外设默认就支持这两种模式。
SPI 写寄存器的流程跟 I2C 类似,但多了一个片选信号:
int sensor_spi_write_reg(int spi_fd, uint8_t reg_addr, uint8_t data) {
uint8_t tx_buf[2];
// 很多传感器SPI写操作,寄存器地址的最高位要置1表示写
tx_buf[0] = reg_addr | 0x80;
tx_buf[1] = data;
struct spi_ioc_transfer tr = {
.tx_buf = (unsigned long)tx_buf,
.rx_buf = 0,
.len = 2,
.speed_hz = 10000000, // 10MHz
.delay_usecs = 0,
.bits_per_word = 8,
.cs_change = 0,
};
if (ioctl(spi_fd, SPI_IOC_MESSAGE(1), &tr) < 0) {
perror("SPI write failed");
return -1;
}
return 0;
}
注意那个 reg_addr | 0x80。很多 SPI 传感器的协议里,地址字节的最高位用来区分读写。1 表示写,0 表示读。这个细节很容易被忽略,但错了就完全不通。
3.4 初始化序列:顺序决定成败
传感器的初始化不是随便写一堆寄存器值就完事的。它有一个严格的上电时序和初始化序列。我一般把初始化分成几个阶段:
- 硬件复位:拉低复位引脚至少 1ms,然后释放。
- 等待稳定:释放复位后,等待 10-20ms,让内部 PLL 和时钟稳定。
- 软件复位:写软件复位寄存器(通常是 0x12 或 0xFF),让传感器进入已知状态。
- PLL 和时钟配置:先配时钟源和分频,再配 PLL。
- 输出格式配置:分辨率、帧率、数据格式(YUV/RGB/RAW)。
- 图像效果配置:增益、曝光、白平衡等。
- 启动输出:写启动寄存器,开始输出图像数据。
这个顺序不能乱。比如,你还没配 PLL 就去配输出格式,传感器可能根本不响应,因为内部时钟还没跑起来。
3.5 PLL 与时钟树:让传感器跑在正确频率上
摄像头传感器内部有一个复杂的时钟树。外部晶振通常是 24MHz 或 27MHz,但传感器内部需要多种频率:像素时钟、MIPI 时钟、内部逻辑时钟。这些频率都是通过 PLL 倍频和分频得到的。
我画了一张典型的传感器时钟树结构图,你看完就明白了:
配置 PLL 时,你通常需要设置三个参数:倍频系数(N)、分频系数(M)、后分频系数(P)。公式很简单:
PLL 输出 = 晶振频率 × N / M
像素时钟 = PLL 输出 / P
举个例子。假设晶振是 24MHz,你想让传感器输出 1080p@30fps。查数据手册发现,这个模式下像素时钟需要 74.25MHz。那 PLL 怎么配?
- 选 N=148, M=48,PLL 输出 = 24 × 148 / 48 = 74MHz
- 或者 N=99, M=32,PLL 输出 = 24 × 99 / 32 = 74.25MHz(更精确)
你看,不同的 N/M 组合能得到接近的频率。但要注意,PLL 的 VCO 频率有范围限制。比如某个传感器要求 VCO 在 400MHz~800MHz 之间。那你的 PLL 输出(VCO 频率)必须落在这个区间内。如果算出来是 74MHz,那说明还需要一个后分频器把 VCO 降下来。
3.6 实战:初始化序列代码示例
下面是一个完整的传感器初始化序列示例。我以 OV5640 为例,它是一款常用的 500 万像素摄像头传感器。
// OV5640 初始化序列(简化版)
typedef struct {
uint16_t reg;
uint8_t val;
} sensor_reg_t;
// 初始化寄存器表
sensor_reg_t ov5640_init_seq[] = {
// 1. 软件复位
{0x3008, 0x80}, // 软件复位寄存器
// 等待 20ms
{0xFFFF, 0x00}, // 延迟标记
// 2. 配置 PLL
{0x3034, 0x1A}, // PLL 配置:倍频系数
{0x3035, 0x21}, // PLL 配置:分频系数
{0x3036, 0x46}, // PLL 配置:VCO 分频
{0x3037, 0x08}, // PLL 配置:后分频
// 3. 配置输出格式(1080p@30fps, YUV)
{0x3800, 0x00}, // 水平起始高字节
{0x3801, 0x00}, // 水平起始低字节
{0x3802, 0x00}, // 垂直起始高字节
{0x3803, 0x00}, // 垂直起始低字节
{0x3804, 0x0A}, // 水平结束高字节
{0x3805, 0x3F}, // 水平结束低字节 (0x0A3F = 2623)
{0x3806, 0x07}, // 垂直结束高字节
{0x3807, 0x9F}, // 垂直结束低字节 (0x079F = 1951)
{0x3808, 0x07}, // 输出宽度高字节
{0x3809, 0x80}, // 输出宽度低字节 (0x0780 = 1920)
{0x380A, 0x04}, // 输出高度高字节
{0x380B, 0x38}, // 输出高度低字节 (0x0438 = 1080)
// 4. 配置数据格式
{0x4300, 0x30}, // YUV 格式,YUV422
// 5. 启动输出
{0x3008, 0x02}, // 启动传感器输出
};
void sensor_init(int i2c_fd, uint8_t dev_addr) {
int i = 0;
while (ov5640_init_seq[i].reg != 0xFFFF ||
ov5640_init_seq[i].val != 0x00) {
if (ov5640_init_seq[i].reg == 0xFFFF) {
// 延迟标记
usleep(20000); // 20ms
} else {
sensor_write_reg(i2c_fd, dev_addr,
ov5640_init_seq[i].reg,
ov5640_init_seq[i].val);
// 读回验证
uint8_t readback;
sensor_read_reg(i2c_fd, dev_addr,
ov5640_init_seq[i].reg, &readback);
if (readback != ov5640_init_seq[i].val) {
printf("Error: reg 0x%04X write failed!\n",
ov5640_init_seq[i].reg);
// 重试或报错
}
usleep(1000); // 1ms 延迟
}
i++;
}
printf("Sensor initialized successfully.\n");
}
这段代码里,我加了一个读回验证。每次写完寄存器,马上读回来检查。如果值不对,说明通信有问题或者传感器没准备好。这个习惯帮我省了很多调试时间。
3.7 调试技巧:用逻辑分析仪抓 I2C/SPI 波形
写驱动时,最怕的就是“写了但没写进去”。我强烈建议你准备一个逻辑分析仪,便宜的几十块钱的就行。调试时,把 SDA/SCL(或 SPI 的四根线)夹上去,看波形。
你主要看这几个点:
- 起始条件:SCL 高电平时,SDA 从高变低。
- 设备地址:7 位地址 + 读写位,后面跟着 ACK。
- 寄存器地址:8 位或 16 位,注意字节序。
- 数据:8 位数据,后面跟着 ACK。
- 停止条件:SCL 高电平时,SDA 从低变高。
如果波形不对,比如没有 ACK,那多半是设备地址错了,或者传感器没上电。如果数据不对,那就是字节序或 bit 顺序搞反了。
嗯,说到这个,我想起一个案例。有一次我调一个 SPI 接口的传感器,死活读不到正确的 ID 寄存器。用逻辑分析仪一看,发现 MISO 线上有数据,但 MCU 读到的全是 0xFF。后来才发现,SPI 的 CPOL 和 CPHA 配置反了,数据在错误的边沿采样,全采到了高电平。改过来就好了。
所以,逻辑分析仪是调试 I2C/SPI 的必备工具。别省这个钱,它能帮你省下几天的排查时间。