第4章:Sensor寄存器读写实战——通过I2C/SPI配置BLC与DPC
好,咱们进入真正的实战环节了。
前面几章讲了黑电平校正和坏点校正的原理,说白了都是纸上谈兵。这一章,我们要真刀真枪地通过I2C或SPI总线,去读写Sensor内部的寄存器,把BLC和DPC功能真正配起来。
我刚开始带项目那会儿,总觉得配寄存器就是对着Datasheet填数字,没什么技术含量。直到有一次,一个Sensor的BLC怎么调都不对,画面偏色严重,折腾了两天才发现是I2C时序有问题,寄存器根本没写进去。嗯,从那以后,我再也不敢小看这一步了。
4.1 先搞清楚你的总线:I2C还是SPI?
拿到一款新Sensor,第一件事不是翻寄存器表,而是看接口类型。大部分手机和安防Sensor用I2C,高速或高分辨率Sensor更倾向SPI。我个人的习惯是:先看原理图,确认SDA/SCL或者MOSI/MISO/CS/SCLK接到了主控的哪组GPIO上。
| 特性 | I2C | SPI |
|---|---|---|
| 引脚数 | 2(SDA, SCL) | 4(MOSI, MISO, SCLK, CS) |
| 速度 | 标准100kHz~400kHz | 通常10MHz~50MHz |
| 寻址方式 | 7位或10位设备地址 | 片选CS选择设备 |
| 数据方向 | 半双工 | 全双工 |
| 典型场景 | 配置寄存器、读取状态 | 高速配置、固件下载 |
4.2 I2C读写Sensor寄存器的标准流程
不管你是用Linux的i2c-dev驱动,还是MCU上自己模拟I2C时序,核心流程都一样。说白了就三步:发设备地址、发寄存器地址、发数据或收数据。
以写一个16位地址的Sensor为例,流程是这样的:
- 主控发START信号
- 主控发设备地址(7位左移1位 + 写位0)
- 等待Sensor应答ACK
- 主控发寄存器高8位地址
- 等待ACK
- 主控发寄存器低8位地址
- 等待ACK
- 主控发要写入的数据
- 等待ACK
- 主控发STOP信号
读操作稍微复杂一点,需要先写寄存器地址,再重新发START信号,然后读数据。这叫「复合操作」。
// 伪代码示例:I2C写16位地址寄存器
uint8_t write_reg(uint16_t reg_addr, uint8_t data) {
uint8_t buf[3];
buf[0] = (reg_addr >> 8) & 0xFF; // 高8位
buf[1] = reg_addr & 0xFF; // 低8位
buf[2] = data; // 要写的数据
// 发送设备地址 + 写标志
i2c_start();
i2c_send_byte(DEVICE_ADDR << 1 | 0);
if (!i2c_wait_ack()) return ERROR;
// 发送寄存器地址 + 数据
for (int i = 0; i < 3; i++) {
i2c_send_byte(buf[i]);
if (!i2c_wait_ack()) return ERROR;
}
i2c_stop();
return OK;
}
4.3 SPI读写:片选和时钟极性是关键
SPI比I2C快,但坑也多。最大的坑是时钟极性(CPOL)和时钟相位(CPHA)。这两个参数配错了,读出来的数据全是乱的。
我个人的习惯是:先看Datasheet里的时序图,确认是模式0(CPOL=0, CPHA=0)还是模式3(CPOL=1, CPHA=1)。大部分Sensor用模式0,但别全信,以时序图为准。
// 伪代码示例:SPI读16位寄存器
uint16_t spi_read_reg(uint16_t reg_addr) {
uint16_t result = 0;
// 拉低片选
gpio_write(CS_PIN, 0);
// 发送读命令(假设读命令为0x03)+ 寄存器地址
spi_transfer_byte(0x03); // 读命令
spi_transfer_byte((reg_addr >> 8) & 0xFF); // 地址高8位
spi_transfer_byte(reg_addr & 0xFF); // 地址低8位
// 读两个字节数据
result = spi_transfer_byte(0x00) << 8; // 高字节
result |= spi_transfer_byte(0x00); // 低字节
// 拉高片选
gpio_write(CS_PIN, 1);
return result;
}
4.4 实战:配置BLC相关寄存器
好了,通信搞定了,咱们来配BLC。不同Sensor的BLC寄存器设计差异很大,但核心逻辑差不多:
- BLC使能寄存器:通常叫BLC_EN或OB_EN,置1开启。
- BLC目标值寄存器:设定黑电平的目标值,常见是64(10bit)或256(12bit)。
- BLC更新模式寄存器:选择是每帧更新还是手动触发更新。
- BLC校准窗口寄存器:设定从哪几行光学黑区(OB)取平均值。
我拿一个实际项目中的例子来说。某款Sensor的BLC配置如下:
| 寄存器名 | 地址 | 位域 | 说明 | 推荐值 |
|---|---|---|---|---|
| BLC_CTRL | 0x3010 | [0] | BLC使能位 | 0x01 |
| BLC_TARGET | 0x3012 | [11:0] | 黑电平目标值 | 0x0400(1024) |
| BLC_OB_LINE | 0x3014 | [3:0] | 选择OB行数 | 0x04(4行) |
| BLC_UPDATE | 0x3016 | [0] | 手动更新触发 | 写1触发 |
// 实际配置代码片段
void config_blc() {
// 1. 使能BLC
i2c_write_reg(0x3010, 0x01);
// 2. 设置目标值1024(12bit Sensor)
i2c_write_reg(0x3012, 0x04); // 高8位
i2c_write_reg(0x3013, 0x00); // 低8位
// 3. 选择4行OB区域
i2c_write_reg(0x3014, 0x04);
// 4. 触发一次手动更新
i2c_write_reg(0x3016, 0x01);
delay_ms(10);
i2c_write_reg(0x3016, 0x00);
printf("BLC configured, target=1024\n");
}
4.5 实战:配置DPC相关寄存器
DPC(坏点校正)的寄存器配置比BLC复杂一些,因为涉及坏点检测和校正两个阶段。常见的寄存器类型有:
- DPC_EN:坏点校正总使能。
- DPC_MODE:选择静态校正(查表)还是动态校正(实时检测)。
- DPC_THRESHOLD:坏点判断阈值,像素值与邻域像素的差值超过此值即判为坏点。
- DPC_TABLE_ADDR:静态坏点表的起始地址(如果Sensor内部有OTP存储)。
- DPC_UPDATE:触发坏点检测或表加载。
我遇到过最头疼的情况是:动态DPC阈值设得太低,结果把正常像素也当成坏点校正了,画面出现「网格状」纹理。后来我把阈值从16提高到48,问题就解决了。你想想看,阈值这东西,不是越小越好。
// 配置动态DPC示例
void config_dpc() {
// 1. 使能DPC
i2c_write_reg(0x3020, 0x01);
// 2. 选择动态模式
i2c_write_reg(0x3021, 0x01); // 0=静态, 1=动态
// 3. 设置阈值(假设阈值为48)
i2c_write_reg(0x3022, 48);
// 4. 触发一次检测
i2c_write_reg(0x3023, 0x01);
delay_ms(50);
i2c_write_reg(0x3023, 0x00);
// 5. 读取坏点计数,确认检测结果
uint16_t defect_count = i2c_read_reg(0x3024) << 8;
defect_count |= i2c_read_reg(0x3025);
printf("Detected defect pixels: %d\n", defect_count);
}
4.6 寄存器读写调试的「三板斧」
调试过程中,肯定会遇到寄存器写不进去、读出来不对的情况。别慌,我总结了三板斧:
- 读Device ID:这是最基础的通信验证。每个Sensor都有一个唯一的Device ID寄存器,读出来和Datasheet对一下,对上就说明I2C/SPI通信没问题。
- 写读对比:找一个可读可写的寄存器(比如测试寄存器),先写一个固定值,再读回来。如果一致,说明读写通路正常。
- 抓波形:如果前两步都过了,但功能还是不对,那就上示波器或逻辑分析仪。抓一下SDA/SCL或MOSI/MISO的波形,看看时序对不对,ACK有没有收到。
4.7 本章知识结构图
下面这张图总结了Sensor寄存器读写的核心流程和BLC/DPC配置的关键节点,方便你对照调试:
这张图从总线选择开始,到通信验证、寄存器配置、结果验证,再到调试手段,把整个流程串起来了。你可以把它贴在工位上,调试时对照着走,不容易漏步骤。