4、OV5640驱动设计:SCCB(I2C)总线配置、寄存器读写操作、分辨率与帧率配置、输出格式设置(YUV/RGB)
OV5640这颗500万像素的CMOS传感器,我前前后后用了不下五个项目。说实话,刚开始调它的时候,我也被SCCB总线的时序折腾过几回。今天咱们就把它的驱动设计掰开揉碎了讲清楚。
4.1 SCCB总线协议——其实就是简化版I2C
OV5640的配置接口叫SCCB(Serial Camera Control Bus),说白了就是OmniVision公司自己定义的I2C变种。跟标准I2C比,它去掉了多主机仲裁,只保留单主机模式。你想想看,FPGA作为主机去配摄像头,根本不需要多主机,所以这个简化很合理。
我个人习惯把SCCB当作I2C来写代码,但要注意几个细节:
- 起始条件:SCL高电平时,SDA从高变低
- 停止条件:SCL高电平时,SDA从低变高
- 数据有效性:SCL低电平时改变SDA,SCL高电平时采样
- 应答机制:每字节后跟一个应答位,低电平表示ACK
嗯,这里要注意:OV5640的SCCB地址是0x3C(写)和0x3D(读),7位地址左移一位的结果。我刚开始写驱动时,直接把0x78当地址发出去,结果摄像头死活没反应——后来才发现OV的文档里地址是8位格式,跟标准I2C的7位地址写法不一样。
关键区别:标准I2C用7位地址+读写位,SCCB用8位地址(已包含读写位)。OV5640写地址=0x3C,读地址=0x3D。
4.2 寄存器读写操作——三步走策略
配置OV5640的寄存器,我总结了一个三步走策略:
- 发送设备地址+写命令:0x3C
- 发送寄存器地址:16位地址,高字节在前
- 发送/接收数据:写操作直接发数据,读操作需要重新发送起始条件
写操作的Verilog代码框架大概是这样:
// SCCB写操作状态机
localparam IDLE = 3'd0;
localparam START = 3'd1;
localparam DEV_ADDR = 3'd2;
localparam REG_ADDR = 3'd3;
localparam DATA = 3'd4;
localparam STOP = 3'd5;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
scl <= 1'b1;
sda <= 1'b1;
end else begin
case (state)
IDLE: begin
if (start_req) begin
state <= START;
scl <= 1'b1;
sda <= 1'b0; // 起始条件
end
end
// ... 后续状态转移
endcase
end
end
读操作稍微麻烦一点。你需要先发一个伪写操作(只发寄存器地址),然后重新发起始条件,再发读地址。我在项目中遇到过一个问题:读回来的数据总是0xFF,排查了半天才发现是伪写操作后没有等足够的时间就发读命令了。
避坑指南:我曾经在伪写操作和读操作之间只等了1个时钟周期,结果读回来的数据全是错的。建议至少等2-3个SCL周期,让摄像头准备好。
4.3 分辨率与帧率配置——找到平衡点
OV5640支持的分辨率很多,从QQVGA(160x120)到QSXGA(2592x1944)都有。但分辨率越高,帧率就越低。我一般根据项目需求来选:
| 分辨率 | 最大帧率(fps) | 典型应用场景 |
|---|---|---|
| 640x480 (VGA) | 90 | 高速运动检测 |
| 1280x720 (720p) | 60 | 高清视频监控 |
| 1920x1080 (1080p) | 30 | 标准高清采集 |
| 2592x1944 (5MP) | 15 | 静态图像拍摄 |
配置帧率的核心寄存器是0x380C和0x380D(总像素数高/低字节),以及0x380E和0x380F(总行数高/低字节)。帧率计算公式是:
帧率 = 输入时钟频率 / (总像素数 × 总行数)
举个例子,如果输入时钟是24MHz,想要VGA分辨率下60fps:
总像素数 = 24,000,000 / (60 × 总行数)
假设总行数 = 510(包含消隐区)
总像素数 ≈ 784
嗯,这里要注意:总像素数和总行数都包含了消隐区,不是有效像素。我刚开始算的时候直接拿640x480去算,结果帧率完全不对。
4.4 输出格式设置——YUV还是RGB?
OV5640支持多种输出格式,最常用的就是YUV和RGB。我个人偏好用YUV,因为后续做图像处理时,Y分量可以直接当灰度图用,省去一次格式转换。
输出格式由寄存器0x4300控制:
| 寄存器值 | 输出格式 | 数据位宽 |
|---|---|---|
| 0x00 | RGB565 | 16位 |
| 0x10 | RGB555 | 16位 |
| 0x30 | YUV422 | 16位 |
| 0x38 | YUV420 | 8位 |
配置代码示例:
// 设置为YUV422格式
// 寄存器0x4300 = 0x30
sccb_write(0x4300, 0x30);
// 同时需要配置输出时序
// 寄存器0x4740 = 0x21 (VSYNC高有效,HSYNC高有效)
sccb_write(0x4740, 0x21);
注意:切换输出格式后,一定要重新初始化PLL。我曾经直接改格式没复位PLL,结果输出图像颜色完全错乱。建议顺序是:先改格式寄存器 → 复位PLL → 等待稳定 → 开始采集。
4.5 实战配置流程——我常用的初始化序列
经过多次调试,我总结了一套比较稳定的初始化流程:
- 上电复位:拉低RESET引脚至少1ms,然后释放
- 等待稳定:释放复位后等20ms,让内部PLL起振
- 配置系统时钟:设置PLL倍频系数(寄存器0x3034-0x3036)
- 配置分辨率:设置输出窗口大小(寄存器0x3808-0x380B)
- 配置帧率:设置总像素数和总行数
- 配置输出格式:设置0x4300
- 启动输出:设置寄存器0x3008 = 0x02(软件复位后启动)
这里有个小技巧:我习惯在配置完所有寄存器后,读一遍关键寄存器来验证。比如读0x300A看看芯片ID是不是0x5640,确认通信正常。
核心要点:OV5640的驱动设计,说白了就是三件事——把SCCB时序写对、把寄存器配准、把格式选好。只要这三步不出错,图像就能正常出来。
最后分享一个我踩过的坑:有一次项目急着赶进度,我直接复制了网上的初始化序列,结果图像一直有竖条纹。后来逐寄存器排查,发现是PLL配置里有一个分频系数设错了,导致像素时钟频率不对。从那以后,我每次配摄像头都会用示波器量一下PCLK的频率,确认跟预期一致再往下走。
可可精品资料库