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的寄存器,我总结了一个三步走策略:

  1. 发送设备地址+写命令:0x3C
  2. 发送寄存器地址:16位地址,高字节在前
  3. 发送/接收数据:写操作直接发数据,读操作需要重新发送起始条件

写操作的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 实战配置流程——我常用的初始化序列

经过多次调试,我总结了一套比较稳定的初始化流程:

  1. 上电复位:拉低RESET引脚至少1ms,然后释放
  2. 等待稳定:释放复位后等20ms,让内部PLL起振
  3. 配置系统时钟:设置PLL倍频系数(寄存器0x3034-0x3036)
  4. 配置分辨率:设置输出窗口大小(寄存器0x3808-0x380B)
  5. 配置帧率:设置总像素数和总行数
  6. 配置输出格式:设置0x4300
  7. 启动输出:设置寄存器0x3008 = 0x02(软件复位后启动)

这里有个小技巧:我习惯在配置完所有寄存器后,读一遍关键寄存器来验证。比如读0x300A看看芯片ID是不是0x5640,确认通信正常。

核心要点:OV5640的驱动设计,说白了就是三件事——把SCCB时序写对、把寄存器配准、把格式选好。只要这三步不出错,图像就能正常出来。

最后分享一个我踩过的坑:有一次项目急着赶进度,我直接复制了网上的初始化序列,结果图像一直有竖条纹。后来逐寄存器排查,发现是PLL配置里有一个分频系数设错了,导致像素时钟频率不对。从那以后,我每次配摄像头都会用示波器量一下PCLK的频率,确认跟预期一致再往下走。

OV5640驱动设计核心流程 SCCB总线初始化 寄存器读写操作 分辨率与帧率配置 输出格式设置 图像数据输出 关键寄存器 • 0x3008: 系统控制 • 0x3034-0x3036: PLL配置 • 0x3808-0x380B: 输出窗口 • 0x380C-0x380F: 帧率控制 • 0x4300: 输出格式 • 0x4740: 时序控制 常见输出格式 • RGB565: 16位, 5-6-5 • RGB555: 16位, 5-5-5 • YUV422: 16位, YUYV • YUV420: 8位, 压缩格式 避坑提醒 ⚠ 切换格式后需复位PLL ⚠ 帧率计算包含消隐区

可可精品资料库