4. 图像采集接口与时序

做图像加速,第一步就是要把图像「拿进来」。

我最早接触摄像头驱动时,觉得不就是配几个寄存器嘛,结果折腾了两天才把图像调出来。后来发现,真正难的不是配置本身,而是理解背后的时序逻辑。

这一章,我们就来啃这块硬骨头。

4.1 OV5640摄像头:一颗经典的500万像素传感器

OV5640是OmniVision公司的一颗CMOS图像传感器。最大支持2592x1944分辨率,也就是500万像素。我项目中用得最多的其实是720p和1080p,因为带宽和存储都更友好。

它的核心特性:

  • 支持RAW RGB、RGB565、YUV422等多种输出格式
  • 内置ISP(图像信号处理),可以自动曝光、自动白平衡
  • 通过SCCB总线配置内部寄存器
  • 输出接口是DVP(并行)或MIPI(串行)

嗯,这里要注意:FPGA采集通常用DVP模式,因为并行接口时序简单,适合入门。

关键参数速查表
参数典型值
有效像素2592 x 1944
像素尺寸1.4 μm
输出格式RGB565 / YUV422 / RAW10
帧率(1080p)30 fps
供电电压1.8V / 2.8V
接口类型DVP(8/10位) / MIPI(2-lane)

4.2 SCCB/I2C协议:配置摄像头的「语言」

OV5640的寄存器配置走的是SCCB协议。说白了,SCCB就是简化版的I2C。区别不大,但有个坑——SCCB不支持多字节连续读写,必须一地址一数据地操作。

我曾经在项目中直接用I2C控制器去配OV5640,结果发现读回来的数据全是0xFF。查了半天,原来是SCCB的停止条件比I2C多了一个额外的时钟周期。嗯,细节决定成败。

4.2.1 时序结构

SCCB的写操作分三步:

  1. 发送设备地址(0x3C,写方向)
  2. 发送寄存器地址(16位,高字节在前)
  3. 发送寄存器数据(8位)

读操作稍微复杂一点:先写设备地址+寄存器地址,然后重新发送设备地址(0x3D,读方向),最后读取数据。

我的小技巧: 写一个SCCB控制器时,把时钟频率设在100kHz~400kHz之间。太快了容易出错,太慢了影响配置速度。我一般用200kHz,稳得很。

4.2.2 FPGA实现要点

写SCCB控制器,核心就是一个状态机:

// 伪代码示例
typedef enum {
    IDLE,
    START,
    SEND_ADDR,
    SEND_REG_H,
    SEND_REG_L,
    SEND_DATA,
    STOP
} sccb_state_t;

always @(posedge clk) begin
    case(state)
        IDLE: if(start) state <= START;
        START: begin
            scl <= 1'b0;
            sda <= 1'b0;
            state <= SEND_ADDR;
        end
        // ... 后续状态转移
    endcase
end

你想想看,这个状态机其实就8个状态左右。但要注意的是,每个状态都要等SCL的上升沿和下降沿,不能跳太快。

4.3 行场同步信号解析:读懂摄像头的「呼吸」

摄像头输出图像时,不是一股脑把像素全丢出来的。它用两个信号告诉你:「一行开始了」「一帧结束了」。

  • HREF(行同步):高电平时,每个PCLK输出一个有效像素
  • VSYNC(场同步):高电平表示一帧开始(或结束,取决于配置)
  • PCLK(像素时钟):每个上升沿锁存一个像素数据

我记得第一次用示波器抓这三个信号时,发现VSYNC的脉宽只有几行的时间。当时以为摄像头坏了,后来才知道OV5640的VSYNC默认是「高电平有效,帧开始前拉高」。嗯,多看几遍数据手册就明白了。

避坑指南: 我曾经在采集1080p图像时,发现图像左右错位。排查后发现是HREF的下降沿没有对齐到PCLK的上升沿。解决方案是在FPGA内部用两级寄存器同步HREF信号,消除亚稳态。

4.4 FPGA采集模块设计:把模拟信号变成数字帧

采集模块的核心任务就三个:

  1. 检测VSYNC的上升沿,开始新一帧
  2. 在HREF有效期间,每个PCLK锁存一个像素
  3. 把像素数据写入FIFO或DDR

4.4.1 模块架构

下面是我常用的采集模块结构图:

FPGA图像采集模块架构 OV5640 摄像头传感器 PCLK/HREF/VSYNC 同步模块 边沿检测+同步 采集控制FSM 行/帧计数 FIFO 缓存 像素数据[7:0] 数据对齐 格式转换 写入 图:FPGA采集模块数据流与同步控制 关键信号说明 VSYNC:帧同步信号,上升沿表示新帧开始 HREF:行同步信号,高电平期间输出有效像素 PCLK:像素时钟,每个上升沿锁存一个像素 D[7:0]:8位像素数据总线

4.4.2 核心代码片段

下面是一个简化的采集状态机:

// 采集状态机核心逻辑
always @(posedge pclk or negedge rst_n) begin
    if (!rst_n) begin
        frame_cnt <= 0;
        line_cnt  <= 0;
        pixel_cnt <= 0;
        fifo_wr_en <= 0;
    end else begin
        // VSYNC上升沿检测
        vsync_rise <= vsync_d1 & ~vsync_d2;
        
        if (vsync_rise) begin
            frame_cnt <= frame_cnt + 1;
            line_cnt  <= 0;
            pixel_cnt <= 0;
        end else if (href) begin
            // 行有效期间,每个PCLK写一个像素
            fifo_wr_en <= 1;
            pixel_cnt <= pixel_cnt + 1;
            if (pixel_cnt == LINE_WIDTH - 1) begin
                line_cnt  <= line_cnt + 1;
                pixel_cnt <= 0;
            end
        end else begin
            fifo_wr_en <= 0;
        end
    end
end
经验之谈: 我习惯在采集模块里加一个「帧完成」标志位。当检测到VSYNC下降沿时,拉高该标志,通知后续模块开始处理。这样能避免处理不完整的一帧数据。

4.5 配置实战:让OV5640输出720p@30fps

配置OV5640,说白了就是往寄存器里写值。下面是我常用的720p配置序列(部分):

// 初始化序列(部分关键寄存器)
0x3103, 0x11  // 软件复位
0x3008, 0x82  // 进入配置模式
0x3800, 0x01  // 水平起始高字节
0x3801, 0x50  // 水平起始低字节
0x3802, 0x01  // 垂直起始高字节
0x3803, 0xD0  // 垂直起始低字节
0x3804, 0x08  // 水平结束高字节
0x3805, 0xCF  // 水平结束低字节
0x3806, 0x06  // 垂直结束高字节
0x3807, 0x8F  // 垂直结束低字节
// ... 更多配置省略

配置完成后,记得把0x3008设为0x02,让摄像头进入流模式。这时候VSYNC和HREF就开始跳动了。

注意: 配置过程中不要着急读数据。我见过有人配完寄存器立刻去读图像,结果读到全黑。原因是摄像头内部PLL需要稳定时间。建议等100ms以上再开始采集。

好了,这一章的内容就到这里。图像采集是整个加速链路的起点,把这里搞扎实了,后面的处理才能站得住脚。