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处理……写驱动的时候,这些细节一个都不能漏。

我曾经踩过的坑: 有一次调试一款全局快门的Sensor,I2C总线上挂了两个设备。结果发现读寄存器时老是返回0xFF。查了半天,原来是地址冲突了——两个设备的7位地址一模一样。从那以后,我每次画板子前都会先确认Sensor的I2C地址引脚电平。

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的起始、停止、应答判断都封装好。这样上层驱动只需要关心:我要读哪个寄存器,数据放哪里。

一个小技巧: 很多Sensor的寄存器地址是16位的,但I2C协议一次只能传8位。这时候需要把高8位和低8位分开传。我一般会在封装层自动处理这个字节序,上层驱动不用操心。

对于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);   // 曝光低字节
注意: 曝光时间不能超过一帧的总时间。否则会出现帧率不稳,甚至图像撕裂。我曾经在一个项目中,把曝光设成了5000行,但帧率对应的总行数只有4000行,结果Sensor直接罢工了。

3.3.3 帧率寄存器

帧率控制的是每秒输出多少帧图像。帧率 = 像素时钟频率 / (总行数 × 总列数)。

常见的帧率控制方式有两种:

  1. 直接设置帧率值: 寄存器里写目标帧率(如30fps),Sensor内部自动调整。
  2. 设置行长和行长: 通过调整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 知识结构总览

说了这么多,咱们用一张图把这一章的知识体系串起来:

Sensor寄存器编程知识结构 Sensor寄存器编程知识结构 寄存器编程基础 通信协议 寄存器读写封装 常用寄存器类型 I2C协议 SPI协议 单寄存器读写 批量读写 增益寄存器 曝光寄存器 帧率寄存器 核心要点 掌握I2C/SPI协议差异 → 封装统一读写接口 → 理解增益/曝光/帧率寄存器映射

3.5 避坑指南

最后,分享几个我这些年积累的经验:

  • 读寄存器一定要校验: 写完寄存器后,马上读回来对比。很多Sensor的写入不是立即生效的,需要等几个帧同步周期。
  • 注意寄存器地址的字节序: 有些Sensor用大端,有些用小端。封装层要统一处理,不然读出来的数据全是乱的。
  • I2C速率别贪快: 工业环境干扰多,400kHz是安全线。我试过1MHz,结果在长线缆上频繁丢ACK。
  • SPI的片选要干净: 片选信号抖动会导致通信错乱。加个10kΩ上拉电阻,能解决很多奇怪的问题。

一句话总结: 寄存器编程的核心就三件事——选对协议、封装好接口、搞清楚寄存器含义。这三件事做好了,Sensor驱动开发就成功了一半。