3、Sensor寄存器编程基础:I2C/SPI通信协议、寄存器读写封装、常用寄存器类型
好,咱们进入正题。Sensor驱动开发,说白了就是跟寄存器打交道。你写驱动,本质上就是在做一件事:通过某种总线,往Sensor芯片的寄存器里写值,或者从里面读值。
那问题来了——怎么读写?用什么协议?怎么封装才能让代码好维护?增益、曝光、帧率这些关键寄存器到底长什么样?
这一章,我把这些基础给你捋清楚。
3.1 通信协议:I2C vs SPI
工业相机Sensor用得最多的两种总线,就是I2C和SPI。我个人的习惯是:配置寄存器用I2C,传图像数据用别的接口(比如MIPI)。但有些Sensor也支持SPI来配寄存器,速度更快。
先看个对比表格,心里有个底:
| 特性 | I2C | SPI |
|---|---|---|
| 信号线数量 | 2根(SCL, SDA) | 4根(SCLK, MOSI, MISO, CS) |
| 通信方式 | 半双工,主从架构 | 全双工,主从架构 |
| 速度 | 标准模式100kHz,快速400kHz,高速3.4MHz | 通常10MHz~50MHz,甚至更高 |
| 地址机制 | 7位或10位设备地址 | 片选信号(CS)选择设备 |
| 典型应用 | 寄存器配置、低速控制 | 高速配置、数据流(少用) |
| 抗干扰能力 | 较弱,长线需注意 | 较强,差分SPI更稳 |
嗯,这里要注意:I2C虽然线少,但协议本身比SPI复杂。有起始条件、停止条件、应答位、NACK处理……写驱动的时候,这些细节一个都不能漏。
3.2 寄存器读写封装
直接调用I2C或SPI的底层API去读写寄存器,代码会变得又臭又长。你想想看,每个寄存器操作都要写设备地址、寄存器地址、数据长度、超时时间……不出10行代码,你就晕了。
我建议做一层封装。把底层的总线操作抽象成几个简单的接口。
先看一个典型的I2C读写封装(伪代码):
// sensor_reg.h
typedef struct {
uint8_t slave_addr; // 7位I2C设备地址
uint16_t reg_addr; // 寄存器地址(16位或8位)
uint8_t *data; // 数据缓冲区
uint16_t length; // 数据长度
} sensor_reg_io_t;
// 读寄存器
int sensor_reg_read(sensor_reg_io_t *io);
// 写寄存器
int sensor_reg_write(sensor_reg_io_t *io);
// 批量读(连续地址)
int sensor_reg_burst_read(uint8_t slave, uint16_t start_addr, uint8_t *buf, uint16_t len);
// 批量写(连续地址)
int sensor_reg_burst_write(uint8_t slave, uint16_t start_addr, uint8_t *buf, uint16_t len);
底层实现里,我习惯把I2C的起始、停止、应答判断都封装好。这样上层驱动只需要关心:我要读哪个寄存器,数据放哪里。
对于SPI,封装思路类似,但多了片选管理:
// sensor_spi.h
int sensor_spi_reg_read(uint8_t cs_pin, uint16_t reg_addr, uint8_t *data, uint16_t len);
int sensor_spi_reg_write(uint8_t cs_pin, uint16_t reg_addr, uint8_t *data, uint16_t len);
SPI读写时,通常第一个字节是命令(读/写标志位+寄存器地址高位),后面跟数据。不同Sensor的SPI时序可能略有差异,封装时要留好适配接口。
3.3 常用寄存器类型
Sensor的寄存器少则几十个,多则上千。但核心的、驱动开发必须掌握的,其实就三大类:增益、曝光、帧率。其他的像镜像、测试图、黑电平校正等,都是锦上添花。
3.3.1 增益寄存器
增益控制的是图像的亮度。模拟增益和数字增益,通常分开配置。
- 模拟增益: 在像素读出阶段放大信号。增益值一般用线性或dB表示。比如0dB、6dB、12dB……
- 数字增益: 在ADC之后做数字放大。精度更高,但会放大噪声。
举个例子,某款Sensor的增益寄存器映射:
| 寄存器地址 | 名称 | 位宽 | 说明 |
|---|---|---|---|
| 0x3000 | ANALOG_GAIN | 8位 | 模拟增益,0x00=1x,0x10=2x,步进0.125x |
| 0x3001 | DIGITAL_GAIN | 8位 | 数字增益,0x80=1x,0xFF=2x |
写增益时,要注意增益值和寄存器值的换算关系。我见过不少新手直接写0x10进去,以为就是10倍增益,结果图像一片惨白。
3.3.2 曝光寄存器
曝光控制的是传感器收集光的时间。通常用行数(line)或微秒(us)来表示。
- 行曝光: 曝光时间 = 曝光行数 × 一行时间。一行时间由帧率和总行数决定。
- 时间曝光: 直接写微秒值,Sensor内部自动换算。
曝光寄存器通常是16位或20位的,分高字节和低字节:
// 写曝光时间(行数模式)
uint16_t exposure_lines = 1000; // 1000行
uint8_t high_byte = (exposure_lines >> 8) & 0xFF;
uint8_t low_byte = exposure_lines & 0xFF;
sensor_reg_write(0x3010, &high_byte, 1); // 曝光高字节
sensor_reg_write(0x3011, &low_byte, 1); // 曝光低字节
3.3.3 帧率寄存器
帧率控制的是每秒输出多少帧图像。帧率 = 像素时钟频率 / (总行数 × 总列数)。
常见的帧率控制方式有两种:
- 直接设置帧率值: 寄存器里写目标帧率(如30fps),Sensor内部自动调整。
- 设置行长和行长: 通过调整VTS(垂直总行数)和HTS(水平总列数)来间接控制帧率。
第二种方式更灵活,也更容易出问题。我个人的习惯是:先固定HTS(通常等于图像宽度+消隐区),然后通过调整VTS来改变帧率。
举个例子:
// 设置帧率为30fps
// 假设像素时钟为72MHz,HTS=2000
// 则VTS = 72,000,000 / (30 * 2000) = 1200
uint16_t vts = 1200;
sensor_reg_write(0x3020, (vts >> 8) & 0xFF, 1);
sensor_reg_write(0x3021, vts & 0xFF, 1);
3.4 知识结构总览
说了这么多,咱们用一张图把这一章的知识体系串起来:
3.5 避坑指南
最后,分享几个我这些年积累的经验:
- 读寄存器一定要校验: 写完寄存器后,马上读回来对比。很多Sensor的写入不是立即生效的,需要等几个帧同步周期。
- 注意寄存器地址的字节序: 有些Sensor用大端,有些用小端。封装层要统一处理,不然读出来的数据全是乱的。
- I2C速率别贪快: 工业环境干扰多,400kHz是安全线。我试过1MHz,结果在长线缆上频繁丢ACK。
- SPI的片选要干净: 片选信号抖动会导致通信错乱。加个10kΩ上拉电阻,能解决很多奇怪的问题。
一句话总结: 寄存器编程的核心就三件事——选对协议、封装好接口、搞清楚寄存器含义。这三件事做好了,Sensor驱动开发就成功了一半。