OV7670 这颗摄像头传感器在入门级图像采集项目里出现频率很高,而 SCCB 协议就是配置它的关键通道。做 FPGA 图像处理时,很多人习惯把精力放在像素时序、灰度转换这些“看得见”的部分,结果卡在初始化配置上——图像出不来、花屏、偏色甚至摄像头完全无响应,多半就是 SCCB 这块没处理好。这篇博文我把 OV7670 的 SCCB 配置协议在 FPGA 上的完整实现思路、时序细节和调试验证经验一次讲透,适合正在调试摄像头初始化、想弄懂两线协议如何在 FPGA 上落地的朋友参考。
1. SCCB 与 OV7670:协议背景与整体设计思路
1.1 SCCB 协议到底是什么
SCCB(Serial Camera Control Bus)是 OmniVision 定义的一套两线串行控制总线,专门用来配置摄像头传感器内部的寄存器。OV7670 的寄存器有几百个,包括输出格式、分辨率、帧率、增益、曝光、白平衡等参数,全部通过 SCCB 接口写入或读出。可以把它理解成给摄像头递“控制指令”的小通道,而另一条 DVP 并行总线是摄像头往外吐图像的“大水管”。两条通道互相独立,但缺了 SCCB,大水管根本不工作。
SCCB 在电气层和 I2C 非常相似,也是两根线:SCL(时钟)和 SDA(数据),但协议细节有差异。SCCB 的读操作不支持连续读多个寄存器(I2C 支持),每次只能读一个地址,这是很多从 I2C 习惯转过来的人最容易踩的坑。另外 SCCB 的应答位(ACK)标准和 I2C 也有区别:SCCB 的 ACK 位称为“Don't Care”位,可以在从设备应答时输出高阻,而读操作的第九位是主设备不发应答的。用“兼容 I2C 的方式”去配置 OV7670,大部分时候能工作,但严格的 SCCB 时序实现还是要单独处理。
1.2 为什么用 FPGA 而不是单片机去配置
很多人会问:OV7670 初始化用 STM32 的 I2C 外设不就行了吗,为什么非要用 FPGA 写 SCCB?
这个问题的核心在于场景和架构。如果摄像头只是挂在单片机上的独立模块,用 MCU 的硬件 I2C 或 GPIO 模拟都合理。但一旦进入 FPGA 图像处理系统,比如做实时边缘检测、色彩空间转换、车牌识别预处理、双目测距同步采集,整个数据链路都在 FPGA 内部流转。此时若再用一颗 MCU 去配摄像头,就需要 MCU 与 FPGA 之间额外打通一条通道,或者让 MCU 先初始化完摄像头再启动 FPGA 采集,系统的启动流程就变成“串行依赖”,调试和维护都不方便。
把 SCCB 控制逻辑直接写在 FPGA 里,整条链路都收拢在一个器件内。上电后 FPGA 自己完成对 OV7670 的寄存器配置,配置完成后自动进入图像采集状态,链路完全可以自启动。这也是工业视觉、车载采集、智能摄像头这类产品中常见的做法——用一个器件把“配置”和“采集”全包了。
1.3 系统级接线与信号规划
以常见的 OV7670 摄像头模块(不带 FIFO 的那种)为例,FPGA 侧需要的信号有:
| 信号 | 方向 | 说明 |
|---|---|---|
| SCCB_SCL | FPGA → OV7670 | 配置时钟 |
| SCCB_SDA | FPGA ↔ OV7670 | 配置数据(双向) |
| CAM_PCLK | OV7670 → FPGA | 像素时钟 |
| CAM_VSYNC | OV7670 → FPGA | 帧同步 |
| CAM_HREF | OV7670 → FPGA | 行同步 |
| CAM_DATA[7:0] | OV7670 → FPGA | 像素数据 |
| CAM_XCLK | FPGA → OV7670 | 系统主时钟(24MHz) |
注意 OV7670 模块通常还带一个复位引脚和电源使能引脚,FPGA 上电后要按一定时序拉复位、稳定时钟,再启动 SCCB 配置。配置没有完成前,PCLK 和数据线上基本是安静的,这是判断摄像头是否被成功“唤醒”的一个直观标志。
2. SCCB 协议时序细节与关键参数
2.1 启动、停止和数据位的时序规则
SCCB 的两线协议核心是三段式时序:起始条件、数据传输、停止条件。
- 起始条件:SCL 为高电平时,SDA 产生一个高到低的跳变。
- 停止条件:SCL 为高电平时,SDA 产生一个低到高的跳变。
- 数据位:SDA 上的数据必须在 SCL 高电平期间保持稳定,只有在 SCL 低电平期间才允许改变。
这个“SCL 高电平锁存、低电平变化”的规则和 I2C 完全一致。之所以强调这一点,是因为很多人在写状态机时,容易把 SDA 的翻转时刻安排在 SCL 上升沿附近,一旦时序裕量不够,就会出现“SDA 变化时 SCL 恰好为高”的违规情况,从设备的采样就会错位。
OV7670 的 SCCB 总线最高速率通常建议控制在 400kHz 以内,实际工程里我一般跑在 100kHz 到 200kHz。这个速度对配置几百个寄存器来说完全够用,而且对线材、上拉电阻、电平转换电路的要求都宽松很多。低速换取稳定性,在镜头模组的排线环境里是值得的。
2.2 写寄存器操作的三阶段格式
SCCB 写单个寄存器,主设备要依次发出三个段:
- 从设备地址段(8 bit):OV7670 的写地址是 0x42(7 位地址 0x21,左移一位加上写标志)。这里要特别强调,OV7670 的 SCCB 地址是可以由引脚电平配置的,默认模块上一般是 0x42/0x43,但如果你用的是带地址选择引脚的模块,务必确认硬件上把地址引脚接对了。
- 寄存器地址段(8 bit):要写入的目标寄存器地址,比如 0x12 是输出格式控制寄存器。
- 数据段(8 bit):要写入的值。
每段之间都有一位“Don't Care”位(也就是 ACK 位)。SCCB 规范里规定这一段由从设备应答,但主设备在读这段时可以不关心其电平。实际工程中,把 SDA 释放掉(输出高阻),让从设备拉低或不拉都无所谓。
2.3 读寄存器操作与 I2C 的差异
SCCB 读单个寄存器,需要先写设备地址和寄存器地址,然后重新发起起始条件,再发送设备地址的读版本(0x43),之后从设备会在 SDA 上输出寄存器当前值。主设备在收到数据后,第九位不需要发 ACK(I2C 读需要一个 NACK 来终止),SCCB 规范里这一位干脆不关心,直接进入停止条件即可。
另外要记住,SCCB 本身不支持连续读。若想读取连续的寄存器地址,必须逐个读,每读一个地址都要重复“写地址段 + 写寄存器地址段 + 重启 + 读地址段”的完整流程。这一点在调试时非常关键,用逻辑分析仪抓波形时,看到一串重复的启动信号和地址段,不要觉得是异常,那是 SCCB 读多个寄存器的正常行为。
2.4 上拉电阻与电气特性
SCCB 的 SCL 和 SDA 都是开漏/开集输出结构,必须在外部加合适阻值的上拉电阻。常见模块上已经集成了 4.7kΩ 上拉电阻,如果你是自己画板子连接 OV7670 传感器,记得加上。上拉电阻太小会加重 FPGA 引脚驱动负担,太大则会导致上升沿过缓,高速通信时波形畸变。4.7kΩ 是稳妥起点,排线较长或干扰明显时可换成 2.2kΩ 试试。
还需要注意 OV7670 的 IO 电平。3.3V 供电的 OV7670 模块,其 SCCB 引脚也是 3.3V 电平。FPGA 如果使用 1.8V 或 2.5V Bank 供电,一定要做电平转换或者选择支持宽电压的引脚,否则配置会出现间歇性失败。
3. FPGA 实现架构与核心代码解析
3.1 模块划分与状态机设计
FPGA 上的 SCCB 控制器,我习惯把它拆成三个层次:
- 配置控制器(cfg_controller):负责从寄存器配置文件里读地址和值,组合成写操作请求,按顺序发送。
- SCCB 协议引擎(sccb_engine):核心状态机,完成起始、数据字节发送/接收、停止、ACK 处理等底层时序。
- 时钟分频器(clk_div):把系统时钟分频得到 SCCB 工作时钟。
这里最重要的是协议引擎的状态机。它接收上层传来的“写请求”或“读请求”,内部按照字节为单位进行移位发送。状态机建议按 bit 操作,不按字节操作,这样时序控制更细腻。核心状态跳转如下:
- IDLE:等待请求。
- START:产生起始条件。
- SEND_BYTE:逐位发送 8 位数据。
- WAIT_ACK:释放 SDA,采样应答。
- STOP:产生停止条件。
我把读写操作都抽象成“先写设备地址、再写寄存器地址、再写/读数据”的序列,用参数配置当前阶段,状态机可以复用同一套字节发送逻辑,代码更简洁。
3.2 一个可用的 SCCB 写时序 Verilog 实现
下面给出一段我实际工程用过的 SCCB 写单寄存器核心代码,经过上板验证,逻辑清晰、适合作为学习模板。为节省篇幅,只保留关键部分。
module sccb_write #( parameter CLK_FREQ = 50_000_000, parameter SCCB_FREQ = 200_000 )( input wire clk, input wire rst_n, input wire start, input wire [7:0] dev_addr, input wire [7:0] reg_addr, input wire [7:0] reg_data, output reg done, output reg scl, inout wire sda ); reg sda_out; reg sda_oe; assign sda = sda_oe ? sda_out : 1'bz; localparam CLK_DIV = CLK_FREQ / SCCB_FREQ / 2; // 状态定义 localparam S_IDLE = 4'd0; localparam S_START = 4'd1; localparam S_DEV = 4'd2; localparam S_ACK1 = 4'd3; localparam S_REG = 4'd4; localparam S_ACK2 = 4'd5; localparam S_DATA = 4'd6; localparam S_ACK3 = 4'd7; localparam S_STOP = 4'd8; localparam S_DONE = 4'd9; reg [3:0] state, next_state; reg [2:0] bit_cnt; reg [7:0] shift_reg; reg [15:0] div_cnt; reg scl_en; // scl 输出使能 // 时钟分频,生成 scl 周期 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt <= 0; scl_en <= 0; end else begin if (div_cnt >= CLK_DIV - 1) begin div_cnt <= 0; scl_en <= ~scl_en; // 产生 scl 时钟节拍 end else begin div_cnt <= div_cnt + 1; end end end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin scl <= 1; sda_out <= 1; sda_oe <= 0; done <= 0; state <= S_IDLE; end else begin case (state) S_IDLE: begin done <= 0; scl <= 1; sda_oe <= 0; if (start) begin state <= S_START; end end S_START: begin // SCL 高,SDA 高->低 if (scl_en) begin sda_oe <= 1; sda_out <= 0; state <= S_DEV; bit_cnt <= 3'd7; shift_reg <= dev_addr; end end S_DEV: begin scl <= ~scl_en ? 0 : 1; if (scl_en) begin sda_out <= shift_reg[bit_cnt]; if (bit_cnt == 0) begin state <= S_ACK1; end else begin bit_cnt <= bit_cnt - 1; end end end // 其余状态逻辑类似,这里省略中间细节 S_DONE: begin done <= 1; sda_oe <= 0; scl <= 1; if (!start) state <= S_IDLE; end endcase end end endmodule这段代码的思路是:scl_en 作为半周期脉冲,状态机在 scl_en 有效沿变化;每次只处理一位数据,先拉低 SCL,修改 SDA,再拉高 SCL,让从设备采样。实际工程里,更严谨的做法是将 SCL 高/低半周期分别作为两个状态,但上面这种“边沿分频、状态切换”的方式已经能稳定跑通。完整工程中还需要加上读操作状态,并把 ACK 采样、超时保护补全。
3.3 为什么状态机要按半周期细分
这里有个容易忽略的细节:SCCB 的 SCL 高电平时间是数据稳定的关键窗口。如果状态机只在某个节拍里“一口气”把 SCL 拉高、拉低,而没有在 SCL 高电平期间保持足够稳定时间,从设备可能采到错误的 SDA 电平。
我建议在 FPGA 实现里,把每一位的发送拆成“SCL 低 → 设置 SDA → SCL 高 → 保持 → SCL 低”的微状态。虽然状态数变多,但每个状态只做一件事,时序清晰,也方便加仿真约束。上面的简化代码用 scl_en 翻转产生 SCL,实际上已经隐含了高低电平各占一拍的处理,所以在 SCL 拉高后有一整个半周期让 SDA 稳定,满足 OV7670 的数据建立时间要求。
4. 寄存器配置表的上板策略与初始化流程
4.1 寄存器配置表的组织方式
OV7670 的寄存器配置,业内通常用“数组地址 + 值”的表格方式存放。工程上常见三种做法:
- Verilog 常量数组:直接用
reg [7:0] reg_file [0:N-1]存地址,另一个数组存数据。适合寄存器总量固定的场合。 - ROM 初始化文件:
.mem或.coe文件,把配置表烧进 FPGA 内部的 Block RAM 或分布式 RAM。修改配置只需重新生成 ROM 文件,不用改逻辑。 - 外部串行配置:从 SPI Flash 或 UART 加载,适合寄存器表需要频繁更新的场景。
在纯粹的学习项目里,用常量数组最直观。我把寄存器配置表按行组织,每一行包括“结束标志 + 寄存器地址 + 寄存器值”。配置控制器从上到下逐行执行,遇到结束标志就停止。
4.2 一个典型的初始化配置序列
这里给一份最基础的 OV7670 配置序列,适用于输出 RGB565、VGA(640x480)分辨率、内部时钟分频设置如下:
// reg_addr, reg_data {8'h12, 8'h80}, // COM7: 复位 {8'h12, 8'h00}, // COM7: 清除复位,选择 RGB 输出 {8'h40, 8'h10}, // COM15: RGB565 输出 {8'h11, 8'h00}, // CLKRC: 内部时钟不分频 {8'h6B, 8'h0A}, // DBLV: PLL 倍频设置 {8'h3A, 8'h04}, // TSLB: 行同步时序设置 {8'h17, 8'h13}, // HSTART: 水平起始 {8'h18, 8'h01}, // HSTOP: 水平结束 {8'h32, 8'hB6}, // HREF: 行参考 {8'h19, 8'h02}, // VSTART: 垂直起始 {8'h1A, 8'h7B}, // VSTOP: 垂直结束 {8'h03, 8'h0A}, // VREF: 垂直参考这份配置的要点是:COM7先写0x80让传感器复位,复位后立刻写0x00恢复;COM15选择 RGB565 格式;CLKRC和DBLV决定像素时钟 PCLK 和系统时钟的比例。时序相关寄存器 HSTART/HSTOP/VSTART/VSTOP 的默认值一般已经能输出有效图,但在某些模块上不设置会出现图像偏移或只出半屏的情况。
时序寄存器的设置要结合 OV7670 手册里的公式,不同分辨率下这些参数要配套调整。如果你只是刚开始调试,建议先用默认寄存器表,先把图“点亮”,再去调整窗口和色彩参数,这样排查问题面会小很多。
4.3 上电时序与配置完成后的衔接
FPGA 上电后,不能立刻开始 SCCB 配置,必须先等待电源稳定、时钟稳定、复位释放。建议按以下顺序:
- 拉低摄像头复位引脚,保持至少 1ms。
- 释放复位,同时给 XCLK 提供 24MHz 时钟。
- 等待 5ms 左右(手册要求上电延时,实际我习惯等 10ms 以上)。
- 启动 SCCB 配置流程。
- 全部寄存器写完后,再等待几十毫秒让传感器内部稳图。
OV7670 有一个特性:配置完成后,如果立刻采集,前几帧图像可能是花屏或全黑。这是因为内部的自动曝光、白平衡还在收敛中。所以在 FPGA 内部,我通常会再加一个“丢前几帧”的计数器——配置完成后先空采 3 到 5 帧,再开始真正的图像处理。这个小小的等待逻辑,能省掉很多“图像为什么黑/为什么花”的排查时间。
5. 调试记录与常见问题排查
5.1 摄像头无响应或 SDA 一直为高
这是最常见的情况。上电后 FPGA 开始发 SCCB 配置,但逻辑分析仪上看到 SDA 一直是高电平,没有数据翻转,或者只看到起始信号后就没有后续。
优先检查这几项:
- 上拉电阻是否缺失:SDA/SCL 对地电平如果是 0,那就是根本没接上拉或者引脚方向配置错误。
- 设备地址是否正确:确认模块是 0x42(写地址)而不是其他地址。有的模块带地址选择引脚,比如 ADDR 引脚接了高电平可能把地址变成 0x42,接低电平则是 0x40,务必看模块原理图。
- 时钟是否起来:如果摄像头模块上电后 PCLK 都没有,说明 XCLK、复位、供电某一个环节有问题。VCC 和 GND 别接反——这种事听起来低级,但模块针脚定义不同,真有人接过。
- FPGA 引脚约束错误:SDA 是 inout 引脚,必须在约束文件里设置
IOSTANDARD和合适的驱动强度,并且确认 FPGA 管脚资料里的 Bank 电压与模块电平匹配。
5.2 写配置时 SDA 被从设备拉死
有时候起始条件发完,发送设备地址后,SDA 一直被拉低,后面的数据根本发不出去。这种情况一般是从设备没有正确应答,或者在等 ACK 时主设备输出驱动电平与从设备冲突了。
SCCB 的应答位期间,主设备必须把 SDA 释放(output high impedance),让从设备能自由拉低。如果主设备在 ACK 位仍然输出强低电平,SDA 上的电平就“打架”了。用 FPGA 的三态缓冲实现 SDA 时,状态机必须在 ACK 阶段把 sda_oe 拉低,让引脚变输入态。这是新手在 FPGA 实现中最容易犯的错之一。
另外,SCCB 协议对 ACK 的要求不算严格,就算从设备没拉低 ACK,部分寄存器写入可能仍然成功。所以不要完全依赖“看到 ACK 才能继续”,可以试着一路写完,用最终图像输出判断配置是否生效。
5.3 图像能出但颜色明显偏色或花屏
这种情况说明 SCCB 配置已经成功了,问题多数出在寄存器参数或采集侧,而不是配置链路。
- 偏绿/偏紫:寄存器里颜色矩阵和饱和度参数不对,或者输出格式选择与采集端解析不一致。OV7670 的 RGB565 输出,如果采集端按 YUV 解析,图像颜色就会诡异。
- 花屏但帧同步正常:可能是 PCLK 极性问题。OV7670 的 PCLK 在数据有效时是高电平,如果 FPGA 采集时在 PCLK 下降沿采数,采到的数据就会错位。分别尝试上升沿/下降沿采集,会很快定位。
- 图像上半部分正常、下半部分偏移:通常是 VREF/VSTOP 等垂直时序寄存器配置和实际输出分辨率不匹配,导致行计数错误。
- 颜色暗淡或整体发黑:可能是曝光/增益寄存器被某些“优化配置”压低了。恢复默认再微调。
5.4 逻辑分析仪抓波形时要注意什么
SCCB 的调试,强烈建议用逻辑分析仪,而不是只靠“看图像判断”。24MHz 的 XCLK 不会出现在逻辑分析仪的事先保存文件里,但你可以抓 SCL/SDA,重点观察:
- 起始条件、停止条件是否出现,位置是否正常。
- 发送设备地址后,应答位处 SDA 是否被拉低过。
- 数据位是否和寄存器表一致。
- 配置完成后,是否多了一个多余的停止条件或重复的启动条件。
有些逻辑分析仪的协议解析器自带 I2C 解码,可以直接当 SCCB 用,因为两者帧格式基本兼容。遇到读操作解析异常时,记得看“重复启动”的解析是否正确,SCCB 读操作中途重启是最容易误报的。
6. 调试工具的实用配合与经验补充
最后再分享一点我在调试中积累的小技巧:
一是配置速率不要太激进。虽然 OV7670 手册标称 SCCB 可以到 400kHz,但实际模块排线较长、接触不良或者上拉电阻偏大时,高速下SDA 建立时间不足,会出现概率性配置失败。工程里我常用 100kHz 调试,稳定后再提到 200kHz,从没遇到过性能瓶颈。
二是给 SCCB 控制器加上超时保护。配置流程进行到一半,如果 SDA 被外部干扰钳住,状态机可能永远卡在等待应答状态。加一个计数器,超过 N 个 SCL 周期还等不到预期状态变化就强制返回 IDLE,并上报错误标识。这个机制调试时可能用不上,但它能防止系统在恶劣电磁环境下“假死”。这是从产品化角度做的保险,如果你只是学习,也可以不加。
三是在仿真阶段就把状态机波形看透。用 Verilator 或 ModelSim 仿真 SCCB 控制器时,把 SCL/SDA 的波形导出来,对照协议规范逐节检查。我见过不少项目在板子上调了很久,最后发现是仿真时时序就存在问题——迟早在波形里暴露的问题,就别让它在硬件上“背锅”了。
四是不要把 OV7670 的寄存器配置当一次性工作。不同批次、不同厂家的 OV7670 模块,默认参数可能有差异。如果你的项目要量产或长期使用,建议在初始化流程里保留参数调整接口,方便针对具体模组微调。FPGA 的优势就在这里——改配置表和逻辑都很快,不必换硬件。
SCCB 配置协议在 FPGA 上的实现,说到底就是一个“稳定的状态机 + 正确的三态控制 + 合理的时序”。把它吃透了,后面再接触其它基于 I2C 的传感器配置(比如各种温湿度、气压、陀螺仪模块),思路都是相通的。我的体会是,花一个晚上把 SCCB 时序亲手写好、仿真跑通、上板点亮图像,对理解 FPGA 的时序控制和协议状态机设计都有很大的帮助,远远比背一堆现成 IP 核的使用方法更有价值。