3、摄像头驱动接口设计:SCCB/I2C总线协议、摄像头寄存器配置、HLS实现I2C控制器
摄像头驱动,说白了就是让摄像头芯片听话。你给它供电、给时钟,它不会自己就开始工作。你得通过一根两线的总线,告诉它:输出多大分辨率、帧率多少、曝光时间多长。这根总线,就是SCCB或I2C。
我个人习惯把SCCB看成I2C的“近亲”。OV系列摄像头用SCCB,但绝大多数FPGA的I2C控制器可以直接兼容。嗯,这里要注意:SCCB的“无应答”机制和I2C略有不同,后面我会细说。
3.1 SCCB与I2C总线协议
先看一张图,这是我画的总线时序对比。左边是I2C,右边是SCCB。你一眼就能看出差别。
看到没?I2C每个字节后面都有个ACK位,从机拉低表示收到了。SCCB呢?写操作时,从机根本不回应。你发完数据就完事了。读操作时,主机需要发送一个“NA”位(高电平)来结束传输。
我在项目中遇到过一个问题:用标准I2C控制器去读写OV5640,结果读回来的数据全是0xFF。查了半天,发现是SCCB的读时序里,主机在最后一个字节后需要发送NACK,而我的I2C控制器自动发了ACK。摄像头一看:你还在确认?那我继续发数据。结果总线就乱套了。
核心区别总结:
- 应答机制:I2C每字节后ACK;SCCB写无应答,读有NACK
- 起始/停止:I2C用SDA/SCL边沿组合;SCCB用SIO_D的跳变
- 速率:I2C标准100kHz/400kHz;SCCB通常100kHz~400kHz
- 地址位宽:I2C 7位或10位;SCCB固定7位+1位读写
3.2 摄像头寄存器配置
摄像头芯片内部有几百个寄存器。你要配置的,其实就那么几个关键组:
| 寄存器组 | 典型地址 | 作用 | 我踩过的坑 |
|---|---|---|---|
| 系统控制 | 0x3000~0x300F | 软复位、时钟分频、PLL配置 | 复位后要等至少10ms才能写其他寄存器 |
| 分辨率设置 | 0x3800~0x380F | 水平/垂直起始、结束、输出尺寸 | 起始坐标和结束坐标要算对,否则图像偏移 |
| ISP控制 | 0x5000~0x500F | 自动曝光、白平衡、增益 | 关闭自动功能时,手动值要合理 |
| 帧率控制 | 0x4200~0x420F | VTS/HTS设置 | VTS太小会导致帧率异常 |
举个例子,配置OV5640输出640x480分辨率,你需要写这么一串:
// OV5640 640x480 配置序列(部分关键寄存器)
0x3103, 0x11 // 系统时钟使能
0x3008, 0x82 // 软复位(需要等待10ms)
0x3800, 0x00 // 水平起始高字节
0x3801, 0x00 // 水平起始低字节
0x3802, 0x00 // 垂直起始高字节
0x3803, 0x00 // 垂直起始低字节
0x3804, 0x0A // 水平结束高字节
0x3805, 0x3F // 水平结束低字节 (0x0A3F = 2623)
0x3806, 0x07 // 垂直结束高字节
0x3807, 0x9F // 垂直结束低字节 (0x079F = 1951)
0x3808, 0x02 // 输出宽度高字节
0x3809, 0x80 // 输出宽度低字节 (0x0280 = 640)
0x380A, 0x01 // 输出高度高字节
0x380B, 0xE0 // 输出高度低字节 (0x01E0 = 480)
你想想看,如果我不告诉你这些寄存器地址的含义,你对着数据手册一个个查,得花多少时间?我建议你把常用摄像头的寄存器配置做成头文件,像这样:
// ov5640_regs.h
#define OV5640_ADDR 0x3C
#define REG_SYSTEM_CTRL 0x3008
#define REG_HSTART_H 0x3800
#define REG_HSTART_L 0x3801
#define REG_VSTART_H 0x3802
#define REG_VSTART_L 0x3803
#define REG_HEND_H 0x3804
#define REG_HEND_L 0x3805
#define REG_VEND_H 0x3806
#define REG_VEND_L 0x3807
#define REG_OUTW_H 0x3808
#define REG_OUTW_L 0x3809
#define REG_OUTH_H 0x380A
#define REG_OUTH_L 0x380B
我的小技巧: 配置摄像头时,先写一个“初始化序列”,包含软复位和时钟配置。然后单独写“分辨率切换”函数。这样你调试时,只需要改分辨率参数,不用每次都从头初始化。
3.3 HLS实现I2C控制器
用HLS写I2C控制器,说白了就是把时序翻译成C代码。我习惯用状态机的方式,清晰又好调试。
先看顶层接口:
// i2c_master.h
#include "ap_int.h"
#include "hls_stream.h"
typedef enum {
IDLE,
START,
SEND_ADDR,
SEND_REG,
SEND_DATA,
STOP,
WAIT_ACK
} i2c_state_t;
void i2c_master(
ap_uint<7> dev_addr, // 设备地址
ap_uint<16> reg_addr, // 寄存器地址
ap_uint<8> data_in, // 要写入的数据
ap_uint<1> rw, // 0:写 1:读
ap_uint<8> *data_out, // 读出的数据
ap_uint<1> *busy, // 忙标志
ap_uint<1> *error, // 错误标志
// 物理接口
ap_uint<1> &scl,
ap_uint<1> &sda_in,
ap_uint<1> &sda_out,
ap_uint<1> &sda_oe
);
核心逻辑是这样的:
// i2c_master.cpp - 核心状态机
void i2c_master(...) {
#pragma HLS INTERFACE ap_ctrl_none port=return
#pragma HLS INTERFACE ap_none port=scl
#pragma HLS INTERFACE ap_none port=sda_in
#pragma HLS INTERFACE ap_none port=sda_out
#pragma HLS INTERFACE ap_none port=sda_oe
static i2c_state_t state = IDLE;
static ap_uint<4> bit_cnt = 0;
static ap_uint<16> shift_reg;
switch(state) {
case IDLE:
scl = 1;
sda_oe = 1;
sda_out = 1;
*busy = 0;
if(start) {
state = START;
*busy = 1;
}
break;
case START:
// 产生起始条件:SCL高时SDA从高变低
scl = 1;
sda_out = 0;
state = SEND_ADDR;
bit_cnt = 0;
// 将地址+读写位装入移位寄存器
shift_reg = (dev_addr << 1) | (rw & 0x01);
shift_reg = (shift_reg << 8) | reg_addr;
break;
case SEND_ADDR:
// 发送设备地址+读写位
scl = 0;
sda_out = shift_reg[15];
shift_reg = shift_reg << 1;
bit_cnt++;
if(bit_cnt == 8) {
state = WAIT_ACK;
sda_oe = 0; // 释放SDA,等待应答
}
break;
// ... 后续状态类似
}
}
曾经踩过的坑: 我曾经在HLS里直接用 sda = 0 这样的赋值,结果综合出来是三态门冲突。记住:I2C的SDA是开漏输出,必须用 sda_oe 控制输出使能,sda_out 控制输出值。读的时候把 sda_oe 置0,让外部上拉电阻拉高,然后读 sda_in。
完整的I2C控制器,我建议你包含以下功能:
- 单字节写:发送设备地址+寄存器地址+数据
- 单字节读:发送设备地址+寄存器地址,然后重新发送起始条件+读命令
- 多字节读写:连续读写多个寄存器(摄像头配置常用)
- 超时检测:防止总线卡死
最后,给你一个测试用的顶层代码片段:
// test_i2c.cpp - 测试用顶层
void test_i2c(
ap_uint<7> cam_addr,
ap_uint<16> reg,
ap_uint<8> val,
ap_uint<1> &scl,
ap_uint<1> &sda
) {
#pragma HLS INTERFACE ap_ctrl_none port=return
#pragma HLS INTERFACE ap_none port=scl
#pragma HLS INTERFACE ap_none port=sda
ap_uint<8> read_back;
ap_uint<1> busy, error;
// 写寄存器
i2c_master(cam_addr, reg, val, 0, &read_back, &busy, &error, scl, sda, sda, sda);
// 等待写完成
while(busy);
// 读回来验证
i2c_master(cam_addr, reg, 0, 1, &read_back, &busy, &error, scl, sda, sda, sda);
while(busy);
// 如果read_back != val,说明配置失败
}
嗯,到这里,摄像头驱动接口的核心内容就讲完了。你只要把I2C控制器调通,剩下的就是对着数据手册填寄存器值了。记住:先读ID寄存器确认通信正常,再配置其他参数。这是最稳妥的调试步骤。