1. 为什么OV7670配置绕不开SCCB
做FPGA图像采集项目,很多人的第一块传感器就是OV7670。便宜、资料多、DVP并口简单,一块带FIFO的模块几十块钱就能拿到,想从零接触图像传感器,几乎没有比它更合适的切入点。但凡是真正上手调过OV7670的人都知道一件事:这芯片上电后不会按你想要的格式直接出图,它内部有一大堆寄存器,必须通过SCCB接口逐项配置,否则输出的可能是默认的YUV、默认的VGA时序,甚至某些信号都不稳定。换句话说,SCCB虽然不是图像数据通路的一部分,却是整个摄像头能不能正常工作的前置条件。
SCCB的全称是Serial Camera Control Bus,直译过来就是“串行摄像头控制总线”,由OmniVision定义,用来读写传感器内部寄存器。第一次见SCCB波形的人很容易脱口而出:这不就是I2C吗?确实,两者在电气特性、时序结构、ACK机制上高度相似,SCCB甚至可以看作I2C的一个变种。但真正实现的时候,如果完全照搬I2C主机的思路去做,会在读操作上栽跟头。SCCB对“读传输”的定义和I2C的读时序有明显差别,这一点后面我会单独细讲。
这篇文章要解决的问题很具体:在FPGA上用Verilog实现一个SCCB主机控制器,用它完成OV7670的寄存器初始化配置。目标读者是刚接触FPGA图像处理、准备把自己第一个摄像头跑起来的同学,以及已经会I2C但想确认SCCB差异的朋友。我不打算贴一份几百行的完整工程,而是把状态机怎么设计、三态总线怎么处理、OV7670上电时序怎么安排、寄存器表怎么组织这些关键点拆开讲清楚。这些内容搞明白了,SCCB这个模块以后在其它传感器上也能平移复用。
2. SCCB帧格式详解:写传输、读传输与停止位处理
SCCB的帧格式是最容易被低估的部分。很多人觉得它就是I2C,直接拿现成的I2C控制器改一改就行,结果读操作死活读不回来。问题就在于SCCB的“读”和I2C的“读”并不是一回事。
2.1 三段式写传输拆解
SCCB的一次完整写传输,官方文档里叫“三段式传输”(3-Phase Write)。三段分别是:
- Phase 1:从机地址字节,共8位,其中高7位是从机地址,最低位是读写标志(0表示写)
- Phase 2:寄存器地址高字节,共8位,OV7670的寄存器地址只有8位,所以这个字节固定写0x00
- Phase 3:寄存器地址低字节+待写入数据,共16位
这里有一个容易混淆的地方:大家习惯把OV7670的寄存器地址当成一个字节,所以在代码里定义一个reg_addr[7:0]就够了。但SCCB协议本身支持16位寄存器地址,OV7670只用到了低8位。所以写传输的Phase 2必须发送0x00,即使它没有实际意义。如果有些精简代码省略了高字节,在OV7670上可能侥幸能跑,但换到别的传感器上就不一定了,我建议还是老老实实把高字节发出去。
每段传输中间,从机都会在第9个时钟周期拉低SIOD作为应答(ACK)。OV7670的8位从机地址是0x21,左移一位后写地址为0x42,读地址为0x43。这个值在代码里建议定义成参数,不要写死在状态机的case分支里。
2.2 两段式读传输与I2C的差异
SCCB的读传输是“两段式”(2-Phase Read),这一点和I2C有本质区别。
I2C读操作的标准流程是:主机发送从机地址+写位,发送寄存器地址,然后发送重复起始位(Repeated Start),再发送从机地址+读位,最后读取数据。整个过程是连续的一条总线事务,中间不需要Stop。但SCCB的定义不是这样,它的读操作由两次独立的传输组成:
第一次传输:完成一次“写寄存器地址”的传输。也就是从机地址+写位、寄存器高字节、寄存器低字节,然后总线停止。第二次传输:从机重新发起Start,发送从机地址+读位,然后读取数据字节,最后主机发送“NA”(Non-Acknowledge)并Stop。
换句话说,SCCB在读之前必须有一个显式的Stop,不能像I2C那样用Repeated Start直接衔接。如果你在FPGA里复用现成的I2C控制器,并且用了I2C的读方式去操作OV7670,你会发现某些寄存器能读出来,某些读出来是固定值或者全0,时序抓出来又看不出大问题,但实际上并没有严格遵守SCCB的要求。严格按SCCB来,用两次独立传输实现读,是最稳妥的。
2.3 应答位与无关位的坑
SCCB的应答机制需要分两个方向看。
写传输中,每一段第9个时钟由从机控制SIOD,从机应答时拉低总线。主机侧要做的就是释放SIOD(三态控制置为高阻),然后在SCL高电平期间采样。如果采样到高电平,说明从机没有应答,此时多半是从机地址不对、上电时序不对,或者摄像头处于PWDN掉电状态。
读传输中,主机读取完8个数据位之后,第9个时钟主机不需要像I2C写读那样区分ACK/NACK的语义去决定是否继续读下一字节,因为SCCB读永远只读一字节。在第9个时钟,主机应该主动释放总线,让SIOD被上拉电阻拉高,这个动作在SCCB文档里叫“NA”,含义是主机告诉从机“数据我已收到,结束本次读取”。有的实现里把这个NA省略了,直接发Stop,实测可能也能用,但不规范。
还有一个细节叫“Don't Care位”,部分SCCB文档在传输格式图里会把某些位标注为X,表示主机或从机都不关心该位电平。OV7670实际使用中,这些位通常直接拉高或固定值即可,不必特意处理,但不要因为看到X就把数据位当作可省略,数据位少了整个帧都错位了。
3. OV7670初始化配置:上电顺序与必配寄存器
协议层的东西讲完,接下来是真正和OV7670打交道时最影响成败的部分:上电时序和寄存器配置策略。SCCB只是传输通道,通道再好,寄存器表写得不对、上电顺序不对,摄像头一样哑火。
3.1 PWDN、RESET和软件复位的正确顺序
OV7670有三个跟初始化直接相关的控制引脚和寄存器,顺序搞错会非常折磨人。
PWDN是高电平有效的掉电引脚,正常工作必须拉低。很多模块板上已经默认拉低,但如果用的是自己画的板子,这个引脚悬空或者被FPGA误拉高,摄像头会处于掉电状态,表现出来就是SCCB写什么都收不到ACK。我曾经排查过一整天,最后发现是引脚约束文件里把这个信号接反了。
RESET引脚是低电平复位,正常工作必须拉高。初始化时推荐流程是:上电后先让RESET保持低电平至少1ms,给芯片内部完成稳定;然后拉高,再等待至少1ms;之后开始SCCB配置。有些板子没有单独的RESET引脚控制,而是直接接到RC上电复位电路,那FPGA侧就不用管硬复位,上电后等个10ms再开始配置即可。
软件复位是寄存器层面的操作,OV7670的COM7寄存器(地址0x12)最高位写1会触发软件复位,复位后芯片内部所有寄存器恢复默认值。这个复位是一个异步动作,执行后芯片需要一段时间稳定。很多初始化代码会把0x12 = 0x80放在寄存器表的第一条,写完后立刻继续写其它寄存器,这样很危险:软件复位还没完成,后续写入的寄存器可能被复位过程覆盖。稳妥的做法是把软件复位独立出来:先写0x12 = 0x80,然后延时5ms以上,再开始写正式配置。也可以反过来,把软件复位放在配置流程最后,但那样前面配置全白写了,所以一般还是放最前面。
3.2 输出格式、分辨率、时钟分频关键寄存器
OV7670的寄存器有几百个,正常人不会全背,也没有必要。真正决定输出形态的,主要是下面这几类:
- COM7(0x12):控制复位、RGB/YUV输出选择、分辨率选择。例如RGB输出模式下需要把bit5置1,QVGA分辨率下需要把对应分辨率位配好。
- COM15(0x40):输出格式细节,RGB565通常配置为0xD0,YUV422下配置为0x00。
- CLKRC(0x11):内部时钟分频。外部给OV7670的XCLK一般是24MHz或12MHz,CLKRC的bit[5:0]用于分频,分频公式约为PCLK = XCLK / (CLKRC[5:0] + 1)。想要PCLK低一些,这个寄存器就要配合分辨率来配。
- HSTART/HSTOP/VSTART/VSTOP等一组窗口寄存器:决定输出图像有效区域和时序边界,如果配错会出现图像偏移或只出半幅画面的情况。
- MVFP(0x1E):镜像控制。摄像头装反的时候不需要改机械结构,改这个寄存器就能把画面翻转过来。
在做FPGA图像处理项目时,我习惯把输出配成RGB565格式、QVGA分辨率(320x240),因为RGB565一个像素两个字节,数据量比VGA小一半,后续做灰度转换、边缘检测时处理压力小很多。如果做车牌识别这类具体场景,后续还要做灰度化、二值化、字符定位,分辨率太高反而拖慢处理速度。
3.3 寄存器表该怎么组织
我见过不少初学者把所有寄存器写入逻辑写在状态机里,几十个分支看得人头大。正确做法是把寄存器表抽象出来,用ROM或者case语句组织数据,状态机只是顺序读取执行。
reg [7:0] reg_addr [0:CFG_NUM-1]; reg [7:0] reg_data [0:CFG_NUM-1]; initial begin reg_addr[0] = 8'h12; reg_data[0] = 8'h80; // 软件复位 reg_addr[1] = 8'h11; reg_data[1] = 8'h01; // 时钟分频 reg_addr[2] = 8'h40; reg_data[2] = 8'hD0; // RGB565 // 后续继续填充 end这样写的最大好处是:状态机代码可以长期保持不变,后续调分辨率、改输出格式、调图像方向,只需要修改这个表的内容。很多摄像头驱动在Linux内核里也是这样组织的,一批寄存器配置放一个数组,驱动按条件选择不同的数组下发。这种思想在FPGA里同样适用。
4. FPGA端SCCB主机实现:三态总线与主状态机
协议理解了,寄存器表有了,接下来是在FPGA里把SCCB主机写出来。这里涉及FPGA开发里一个容易犯迷糊的点:inout三态总线怎么处理。我会重点讲这个。
4.1 模块划分与时钟分频设计
整个SCCB配置通路建议拆成三个部分:
- 寄存器表模块:存储寄存器地址和数据,输出当前要配置的条目
- SCCB主机控制器:负责时序,接收“写某寄存器”“读某寄存器”这种任务请求,完成后上报done
- 配置调度模块:上电延时结束后,按顺序把寄存器表条目发给SCCB主机,全部配完后再通知图像采集模块开始工作
时钟分频要单独设计。SIOC的时钟频率不应超过400kHz,实际使用中200kHz左右更稳妥,特别是板上走线较长、上拉电阻较大的时候,频率一旦拉高,信号沿变差,ACK会偶尔丢。假设FPGA主时钟是50MHz,想得到约200kHz的SCCB时钟,一个完整周期需要250个主时钟周期,半周期就是125个周期。用一个计数器在0到124之间循环,就能生成半个周期的方波。
reg [7:0] div_cnt; reg scl_tick; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt <= 8'd0; scl_tick <= 1'b0; end else if (div_cnt == 8'd124) begin div_cnt <= 8'd0; scl_tick <= 1'b1; end else begin div_cnt <= div_cnt + 8'd1; scl_tick <= 1'b0; end end这里scl_tick作为递进脉冲,状态机每个周期只能跳一步,这样就能保证所有数据变化都落在SCL低电平期间。
4.2 写传输状态机实现
SCCB主机的核心状态机可以按“位传输”来设计。一次完整写传输内部包含:Start -> 发送从机地址字节 -> ACK -> 发送寄存器高字节 -> ACK -> 发送寄存器低字节 -> ACK -> 发送数据字节 -> ACK -> Stop。如果把每次发送字节都拆成8个独立位,状态机会非常长。更好的做法是复用“发送一个字节”的子状态机,外层用一个任务状态机依次切换字节序号。
基于位传输的主状态机大致如下:
localparam S_IDLE = 3'd0; localparam S_START = 3'd1; localparam S_SHIFT = 3'd2; localparam S_ACK = 3'd3; localparam S_STOP = 3'd4;在S_SHIFT状态里,用一个bit_cnt从7递减到0。每次scl_tick到来时,如果当前SCL为低,就把SDA数据放到总线上;如果SCL为高,则保持数据稳定。数据变化必须发生在SCL低电平期间,这在I2C/SCCB里是硬性规定。完成8位数据发送后进入S_ACK状态,此时主机释放SIOD(三态输出置为高阻),在第9个时钟的高电平期间采样SIOD,低电平表示从机应答成功。
三态总线的处理是这个模块最关键的地方:
assign siod = siod_out_en ? siod_out_data : 1'bz;siod_out_en为高时,FPGA主动驱动SIOD;为低时释放总线,让外部上拉电阻把电平拉高。需要特别留意的是,siod_out_en不能在SCL高电平时切换,因为SIOD的电平转换必须发生在SCL低电平窗口内,否则会破坏建立时间和保持时间。
4.3 读传输状态机实现
读传输要严格按照2.2节讲的两段式来写。外层任务状态机可以这样设计:第一阶段先执行一次“只写地址”的传输,也就是Start -> 发送从机地址+写位 -> ACK -> 发送寄存器高字节 -> ACK -> 发送寄存器低字节 -> ACK -> Stop。第二阶段再执行一次读传输:Start -> 发送从机地址+读位 -> ACK -> 接收8位数据 -> 主机NA -> Stop。
读数据的过程与写数据方向相反。当发送完从机地址+读位并收到ACK后,主机释放SIOD,在后续8个时钟的高电平期间采样SIOD,依次拼成read_data字节。接收完8位后,第9个时钟主机应保持SIOD为高,实现不应答(NA),然后发送Stop。读回来的数据可以通过当前配置的寄存器地址一起存入一个结果寄存器,方便上层校验。
这里我强烈建议不要偷懒把读操作实现成I2C风格的一次性连续读,因为之前我实测过,OV7670对Repeated Start的兼容性并不理想,特别是某些批次的芯片,用I2C方式读回来的寄存器值和实际配置完全对不上。严格用SCCB两段式后,一切正常。
4.4 inout三态控制的关键细节
inout端口是FPGA开发里新手最容易翻车的地方,SCCB模块里尤其明显。
第一点:三态门的使能信号必须和输出数据同步。很多人的代码里输出数据和输出使能是分开写的,导致总线切换时出现毛刺。正确的写法是让siod_out_data和siod_out_en在同一个状态、同一个时钟沿变化。
第二点:SIOD总线需要上拉电阻。OV7670模块板上一般已经带了上拉,如果你的摄像头是裸片或者自制的板子,必须在SIOC和SIOD两根线上各接一个4.7k欧姆的上拉电阻到摄像头的IOVDD电源,不是到FPGA的VCCIO。OV7670的SCCB引脚电平最好和摄像头的IOVDD保持一致,很多模块IOVDD是2.8V。如果FPGA的IO bank电压是3.3V,直接把摄像头的SCCB引脚接到FPGA引脚上,长时间运行可能损坏摄像头。选FPGA引脚时尽量选在支持2.5V或者可调电平的bank上,或者使用带电平转换的模块板。
第三点:复位释放时三态总线必须先处于高阻状态。如果FPGA刚上电时siod_out_en是默认的1,会把SIOD拉低,此时摄像头可能误判为一个Start条件,导致状态错乱。所以复位信号释放前,一定要把siod_out_en清零。
5. 实测踩坑复盘:ACK异常、PWDN未拉低与假配置
这部分是整篇文章里我最想写的。协议规范看再多,都不如实际把板子调通一次学到的东西多。我这里复盘三个真实遇到过的坑,排查思路和解决过程都有参考价值。
5.1 案例一:写寄存器全部无ACK,问题绕了一圈
现象:逻辑分析仪抓取到的SIOC和SIOD波形看起来完全正常,Start条件对,地址字节是0x42,数据位也对上了,但每个字节的第9个时钟采样到的SIOD都是高电平,也就是说从机始终不应答。
排查过程:先检查OV7670的PWDN引脚,确认是低电平;再查RESET引脚,确认是高电平;然后用示波器看XCLK输入,确认有时钟。最后怀疑是不是SIOD和SIOC两根线接反了。查原理图发现FPGA的SCCB_SIOC信号确实接到了OV7670模块的SIO_C引脚,SCCB_SIOD接到了SIO_D引脚,没有接反。继续追,终于发现是模块的电源指示灯亮了,但用的供电电压是5V,模块板上的稳压芯片把VCC降到了3.3V,而OV7670的IOVDD是从VCC引脚取的,也就是说SCCB上拉到了3.3V,超出了芯片IOVDD的绝对最大额定值,芯片虽然没有烧坏,但工作状态已经不稳定。后来把模块供电改成3.3V直供,再测ACK就正常了。
这个案例给的经验是:OV7670的供电和SCCB上拉电压一定要对照模块原理图确认清楚,很多模块虽然标称5V供电,但芯片本身跑在3.3V甚至2.8V。SCCB出问题不要只盯着时序看,电气连接和电压域先排查一遍。
5.2 案例二:配置“成功”但图像花屏,软件复位位序问题
现象:SCCB波形显示所有寄存器都写进去了,每个ACK都正常,但接上DVP图像采集模块后,输出的图像花屏,颜色完全不对。
排查过程:先用SCCB回读关键寄存器,发现写入值和读出值完全一致,说明寄存器确实写进去了。再看PCLK频率,发现偏高一倍。想到CLKRC配置的是分频后的时钟,但输出异常也可能和COM7里的RGB模式选择有关。最后对照一份可靠的初始化表,发现问题出在寄存器表顺序:我的表把软件复位(0x12=0x80)放在最前面,但复位后没有加延时,紧接着就写其它寄存器,软件复位过程把后面写入的寄存器全部重置或者部分重置了。结果就是看起来寄存器表里的每条配置都执行了,但实际生效的是默认值。解决办法就是前面提到的,先单独写软件复位,延时5ms到10ms,再写正式配置。
这个案例给的经验是:SCCB写操作有没有ACK,只能证明“从机收到了这个字节”,不能证明“这个寄存器配置最终生效”。软件复位、上电稳定这类时序延时,必须通过FPGA里的计数器或者延时状态机来实现,不能在寄存器表里找个寄存器假装延时。
5.3 验证SCCB是否成功的三种手段
当你怀疑SCCB配置没生效时,按下面三个层次排查,效率最高。
第一层:看波形。用逻辑分析仪抓取SIOC和SIOD,确认Start、Stop、ACK位完整。重点看每个字节的第9个时钟,ACK低脉冲必须存在。如果某个字节没有ACK,立刻就能定位到是地址问题还是从机没上电。
第二层:回读校验。SCCB支持读操作,把配置过的寄存器读回来比对。实际项目里不需要读全部寄存器,挑几个关键寄存器即可,比如0x12(COM7)、0x40(COM15)、0x11(CLKRC)。如果写入是0xD0,读回来是0xD0,说明这条链路没问题。
第三层:看图像输出。配置完寄存器后,检查OV7670的VSYNC、HREF、PCLK信号。正常配置后,VSYNC应该有规律的帧脉冲,HREF在每个有效行内有脉冲,PCLK持续翻转。如果一个脉冲都没有,多半是配置的时钟分频不对或者芯片没有真正工作起来。
6. 寄存器表组织与向图像处理链路扩展
SCCB配置只是OV7670项目的起点,摄像头真正输出图像后才是FPGA图像处理的开始。这里有几个基于实际项目的建议,可以帮你把寄存器表这个“地基”打得更稳。
6.1 配置参数对后续图像处理的影响
我建议把摄像头初始化配置和图像处理算法分开考虑。图像处理链路中需要的原始图像格式,在SCCB初始化阶段就应该确定下来,而不是等图像数据处理到一半再回头改寄存器。
比如做车牌识别场景,OV7670输出RGB565给FPGA后,FPGA要先把RGB565转成灰度图,再做边缘检测、二值化、字符定位。如果在这里把分辨率配置成VGA(640x480),一行图像就有1280字节,中间缓存就需要更多的BRAM或者SRAM,处理延迟也会上升。配置成QVGA(320x240)后,一行只有640字节,各种中间结果可以压缩到更小的存储空间,处理时钟也能降下来。对于一些对实时性要求较高的项目,牺牲一点分辨率换取更高的处理帧率,是值得的。
时钟分频的配置也要提前想清楚。OV7670的PCLK是摄像头输出像素的同步时钟,当把分辨率调低后,PCLK也应该适当降低,否则DVP接口采集模块需要工作在更高的时钟频率下,时序约束更难满足。PCLK具体配置成多少,要看后续图像处理模块能接受的输入时钟范围,不要盲目追求高PCLK。
6.2 从OV7670到其它图像传感器的迁移思路
SCCB这个配置链路的思路,并不仅仅适用于OV7670。现在的摄像头传感器,比如OV5640、OV7725等,虽然接口变成了MIPI或者DVP,但寄存器配置通道仍然是SCCB或I2C。也就是说,你在FPGA里写好一个SCCB主机模块,将来换一颗新传感器,只需要改寄存器表、改从机地址,状态机和控制逻辑基本可以原封不动复用。
这也是我强调要把“SCCB主机控制器”和“寄存器表”分开设计的原因。一个纯粹的总线控制器,不应该关心上游配置的是什么寄存器,它只负责把地址和数据按协议时序发送出去。寄存器表才是跟具体传感器强绑定的部分。平时我做新项目时,总是先花半天把一个通用SCCB控制器写好、仿真通过,然后针对新传感器去翻阅手册整理寄存器表,后面就非常快。
从调试手段上看,SCCB的波形比MIPI好抓多了,逻辑分析仪直接就能看到,出了问题也很好定位。所以对刚入门FPGA图像领域的同学来说,OV7670+SCCB这套组合练好,后续做更复杂的摄像头项目会轻松很多。我自己每次拿到一款新开发板,第一步永远是先点亮摄像头,把图像稳定输出到显示器上,再做其它图像算法。配置链路稳定了,后面的路才走得稳。