第三章 图像传感器驱动设计:I2C配置接口实现、传感器寄存器读写、PLL时钟配置、帧同步信号解析

图像传感器驱动,说白了就是让摄像头芯片听你的话。

我刚开始做这个模块时,总觉得不就是写几个寄存器嘛,能有多难?结果第一次上板,画面全是雪花。嗯,后来才发现,I2C时序稍微偏一点,传感器就不理你了。这一章,我把这些年踩过的坑和总结的经验,一次性讲清楚。

3.1 I2C配置接口实现

图像传感器几乎清一色用I2C做配置接口。为什么?因为引脚少,两根线搞定所有寄存器读写。我个人习惯用FPGA的GPIO模拟I2C,而不是调用硬核IP。原因很简单——灵活。你可以随时调整时序,适配不同厂家的传感器。

3.1.1 I2C时序基础

I2C总线就两根线:SCL(时钟)和SDA(数据)。空闲时,两根线都被上拉电阻拉到高电平。启动信号:SCL高电平时,SDA从高变低。停止信号:SCL高电平时,SDA从低变高。

我曾经在一个项目中,因为上拉电阻选错了值,导致I2C通信时好时坏。后来查了三天,才发现是电阻太大,信号上升沿太慢。嗯,这里要注意:上拉电阻一般选4.7kΩ,总线电容大时可以降到2.2kΩ。

核心要点: I2C的速率通常选100kHz或400kHz。传感器配置不需要高速,100kHz足够稳定。

3.1.2 FPGA模拟I2C的Verilog实现

下面是我常用的I2C主控模块框架。注意,这里只展示了核心状态机,完整代码需要处理时钟分频和超时检测。

module i2c_master (
    input  wire       clk,        // 系统时钟,50MHz
    input  wire       rst_n,      // 异步复位
    input  wire       start,      // 启动传输
    input  wire [6:0] dev_addr,   // 设备地址
    input  wire       rw,         // 0:写, 1:读
    input  wire [7:0] reg_addr,   // 寄存器地址
    input  wire [7:0] wdata,      // 写数据
    output reg  [7:0] rdata,      // 读数据
    output reg        done,       // 传输完成
    output reg        ack_err,    // 应答错误
    inout  wire       scl,        // I2C时钟
    inout  wire       sda         // I2C数据
);

    // 状态定义
    localparam IDLE      = 4'd0;
    localparam START     = 4'd1;
    localparam SEND_ADDR = 4'd2;
    localparam WAIT_ACK1 = 4'd3;
    localparam SEND_REG  = 4'd4;
    localparam WAIT_ACK2 = 4'd5;
    localparam SEND_DATA = 4'd6;
    localparam WAIT_ACK3 = 4'd7;
    localparam STOP      = 4'd8;

    reg [3:0] state, next_state;
    reg [7:0] bit_cnt;      // 位计数器
    reg [7:0] shift_reg;    // 移位寄存器
    reg       scl_out, sda_out;
    reg       sda_oe;       // SDA输出使能

    // 时钟分频:50MHz -> 100kHz
    reg [8:0] clk_div;
    reg       scl_en;

    // ... 状态机逻辑和时序控制代码 ...
    // 实际项目中,这里大约200行代码

endmodule
我的经验: 写I2C状态机时,一定要加超时计数器。如果传感器没应答,状态机卡死,整个系统就挂了。我习惯设一个1ms的超时,超时后自动回到IDLE状态并报错。

3.2 传感器寄存器读写

每个图像传感器内部都有一堆寄存器,控制着曝光时间、增益、输出格式、窗口大小等等。说白了,你要通过I2C往这些寄存器里写值,传感器才会按你的要求工作。

3.2.1 典型的寄存器读写流程

以OV5640为例,写一个寄存器的流程是这样的:

  1. 发送启动信号
  2. 发送设备地址(0x3C,写操作)
  3. 等待传感器应答
  4. 发送寄存器高8位地址
  5. 等待传感器应答
  6. 发送寄存器低8位地址
  7. 等待传感器应答
  8. 发送要写入的8位数据
  9. 等待传感器应答
  10. 发送停止信号

读寄存器稍微复杂一点,需要先写寄存器地址,然后重新发送启动信号,再读数据。我记得第一次做读操作时,忘了重新发送启动信号,结果读回来的数据全是0xFF。你想想看,传感器根本没理你。

3.2.2 寄存器配置表的管理

实际项目中,传感器初始化需要配置几十甚至上百个寄存器。我习惯把这些配置做成一个表格,用ROM或者BRAM存储。这样修改配置时,只需要改ROM内容,不用动逻辑代码。

寄存器地址 寄存器名称 配置值 说明
0x3008 SYSTEM_CTRL0 0x82 软件复位
0x3103 CLOCK_ENABLE 0x11 使能MIPI时钟
0x3034 PLL_CONFIG 0x1A PLL倍频设置
0x3035 PLL_CONFIG2 0x21 PLL分频设置
0x3036 PLL_MULTIPLIER 0x46 PLL倍频系数
0x3037 PLL_DIVIDER 0x08 PLL分频系数
注意: 不同厂家的传感器,寄存器地址长度可能不同。OV系列是16位地址,索尼IMX系列是8位地址。写驱动前,一定要先看数据手册确认地址长度。

3.3 PLL时钟配置

图像传感器内部通常有一个PLL,用来产生像素时钟、MIPI时钟等。PLL配置错了,传感器要么不工作,要么输出时钟频率不对,导致图像采集失败。

3.3.1 PLL配置的核心公式

PLL输出频率 = 输入频率 × 倍频系数 / 分频系数。这个公式看起来简单,但实际配置时有很多坑。

举个例子,OV5640的输入时钟是24MHz,我需要输出84MHz的像素时钟。那么:

  • 倍频系数 = 84 / 24 × 分频系数
  • 如果分频系数选4,倍频系数 = 84 / 24 × 4 = 14
  • 但倍频系数必须是整数,且不能超过PLL的允许范围

我曾经在一个项目中,算出来的倍频系数是13.5,取整后时钟频率偏差太大,导致图像颜色不对。后来换了分频系数,才找到一组整数解。嗯,这里要注意:PLL配置一定要保证输出频率在传感器允许的范围内,偏差一般不超过±1%。

3.3.2 常见的PLL配置寄存器

不同传感器的PLL寄存器布局不同,但通常包含以下几类:

  1. 时钟源选择: 选择内部振荡器还是外部晶振
  2. 倍频系数: 决定PLL的倍频倍数
  3. 分频系数: 决定输出时钟的分频比例
  4. 输出使能: 控制各个时钟输出端口的开关
我的建议: 配置PLL时,先配置分频系数,再配置倍频系数。因为分频系数通常有更多选择,更容易找到整数解。另外,配置完PLL后,一定要等待PLL锁定信号,一般需要等待几毫秒。

3.4 帧同步信号解析

传感器输出图像时,会伴随帧同步信号。常见的同步信号有两种:

  • VSYNC(垂直同步): 一帧开始或结束的标志
  • HSYNC(水平同步): 一行开始或结束的标志

3.4.1 VSYNC和HSYNC的时序关系

典型的帧同步时序是这样的:

  1. VSYNC拉高,表示新一帧开始
  2. VSYNC拉低后,开始传输第一行数据
  3. 每行数据开始前,HSYNC拉高
  4. HSYNC拉低后,像素数据开始传输
  5. 一行传输完毕,HSYNC再次拉高,开始下一行
  6. 所有行传输完毕,VSYNC再次拉高,表示帧结束

你想想看,如果VSYNC和HSYNC的极性搞反了,采集到的图像就是颠倒的。我刚开始做时,就犯过这个错误,图像上下颠倒,找了半天才发现是VSYNC极性配置错了。

3.4.2 FPGA中解析帧同步信号

在FPGA中,我通常用状态机来解析帧同步信号:

// 帧同步解析状态机
localparam WAIT_VSYNC = 3'd0;
localparam WAIT_HSYNC = 3'd1;
localparam READ_LINE  = 3'd2;
localparam LINE_DONE  = 3'd3;

reg [2:0] frame_state;
reg [15:0] line_cnt;    // 行计数器
reg [15:0] pixel_cnt;   // 像素计数器

always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        frame_state <= WAIT_VSYNC;
        line_cnt <= 16'd0;
        pixel_cnt <= 16'd0;
    end else begin
        case (frame_state)
            WAIT_VSYNC: begin
                if (vsync) begin
                    frame_state <= WAIT_HSYNC;
                    line_cnt <= 16'd0;
                end
            end
            WAIT_HSYNC: begin
                if (hsync) begin
                    frame_state <= READ_LINE;
                    pixel_cnt <= 16'd0;
                end
            end
            READ_LINE: begin
                // 每个时钟周期采集一个像素
                pixel_cnt <= pixel_cnt + 1'b1;
                if (pixel_cnt == LINE_LENGTH - 1) begin
                    frame_state <= LINE_DONE;
                end
            end
            LINE_DONE: begin
                line_cnt <= line_cnt + 1'b1;
                if (line_cnt == FRAME_HEIGHT - 1) begin
                    frame_state <= WAIT_VSYNC;  // 一帧结束
                end else begin
                    frame_state <= WAIT_HSYNC;  // 继续下一行
                end
            end
        endcase
    end
end
注意: 不同传感器的帧同步信号极性可能不同。有的VSYNC高电平有效,有的低电平有效。一定要看数据手册确认,或者用示波器实测一下。我曾经因为没看手册,默认用高电平有效,结果传感器是低电平有效,图像一直采集不到。

3.5 本章知识体系

下面这张图,是我总结的传感器驱动设计核心流程。你看一眼,就能明白各个模块之间的关系。

图像传感器驱动设计核心流程 图像传感器 I2C配置接口(SCL/SDA) 传感器寄存器读写 PLL时钟配置 帧同步信号解析 像素数据采集 图像数据输出(MIPI/DVP) FPGA图像处理流水线

这张图展示了传感器驱动的完整链路。从I2C配置接口开始,到寄存器读写,再到PLL配置和帧同步解析,最后输出图像数据给FPGA处理。每一步都环环相扣,缺一不可。

本章总结: 传感器驱动设计,核心就是三件事:I2C通信、寄存器配置、同步信号解析。PLL配置是寄存器配置的一部分,但因为太重要,我单独拿出来讲。把这三点吃透了,市面上90%的图像传感器你都能搞定。

好了,这一章就到这里。记住,驱动调试时,先用示波器看I2C波形,再用逻辑分析仪抓帧同步信号。这两步走通了,图像数据基本就稳了。


公众号:蓝海资料掘金营,微信deep3321