免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

FPGA驱动OV7670:SCCB主机控制器设计与寄存器配置实战

FPGA驱动OV7670:SCCB主机控制器设计与寄存器配置实战 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位所以这个字节固定写0x00Phase 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实测可能也能用但不规范。还有一个细节叫“Dont 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的寄存器有几百个正常人不会全背也没有必要。真正决定输出形态的主要是下面这几类COM70x12控制复位、RGB/YUV输出选择、分辨率选择。例如RGB输出模式下需要把bit5置1QVGA分辨率下需要把对应分辨率位配好。COM150x40输出格式细节RGB565通常配置为0xD0YUV422下配置为0x00。CLKRC0x11内部时钟分频。外部给OV7670的XCLK一般是24MHz或12MHzCLKRC的bit[5:0]用于分频分频公式约为PCLK XCLK / (CLKRC[5:0] 1)。想要PCLK低一些这个寄存器就要配合分辨率来配。HSTART/HSTOP/VSTART/VSTOP等一组窗口寄存器决定输出图像有效区域和时序边界如果配错会出现图像偏移或只出半幅画面的情况。MVFP0x1E镜像控制。摄像头装反的时候不需要改机械结构改这个寄存器就能把画面翻转过来。在做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] 8h12; reg_data[0] 8h80; // 软件复位 reg_addr[1] 8h11; reg_data[1] 8h01; // 时钟分频 reg_addr[2] 8h40; reg_data[2] 8hD0; // 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 8d0; scl_tick 1b0; end else if (div_cnt 8d124) begin div_cnt 8d0; scl_tick 1b1; end else begin div_cnt div_cnt 8d1; scl_tick 1b0; end end这里scl_tick作为递进脉冲状态机每个周期只能跳一步这样就能保证所有数据变化都落在SCL低电平期间。4.2 写传输状态机实现SCCB主机的核心状态机可以按“位传输”来设计。一次完整写传输内部包含Start - 发送从机地址字节 - ACK - 发送寄存器高字节 - ACK - 发送寄存器低字节 - ACK - 发送数据字节 - ACK - Stop。如果把每次发送字节都拆成8个独立位状态机会非常长。更好的做法是复用“发送一个字节”的子状态机外层用一个任务状态机依次切换字节序号。基于位传输的主状态机大致如下localparam S_IDLE 3d0; localparam S_START 3d1; localparam S_SHIFT 3d2; localparam S_ACK 3d3; localparam S_STOP 3d4;在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 : 1bz;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模式选择有关。最后对照一份可靠的初始化表发现问题出在寄存器表顺序我的表把软件复位0x120x80放在最前面但复位后没有加延时紧接着就写其它寄存器软件复位过程把后面写入的寄存器全部重置或者部分重置了。结果就是看起来寄存器表里的每条配置都执行了但实际生效的是默认值。解决办法就是前面提到的先单独写软件复位延时5ms到10ms再写正式配置。这个案例给的经验是SCCB写操作有没有ACK只能证明“从机收到了这个字节”不能证明“这个寄存器配置最终生效”。软件复位、上电稳定这类时序延时必须通过FPGA里的计数器或者延时状态机来实现不能在寄存器表里找个寄存器假装延时。5.3 验证SCCB是否成功的三种手段当你怀疑SCCB配置没生效时按下面三个层次排查效率最高。第一层看波形。用逻辑分析仪抓取SIOC和SIOD确认Start、Stop、ACK位完整。重点看每个字节的第9个时钟ACK低脉冲必须存在。如果某个字节没有ACK立刻就能定位到是地址问题还是从机没上电。第二层回读校验。SCCB支持读操作把配置过的寄存器读回来比对。实际项目里不需要读全部寄存器挑几个关键寄存器即可比如0x12COM7、0x40COM15、0x11CLKRC。如果写入是0xD0读回来是0xD0说明这条链路没问题。第三层看图像输出。配置完寄存器后检查OV7670的VSYNC、HREF、PCLK信号。正常配置后VSYNC应该有规律的帧脉冲HREF在每个有效行内有脉冲PCLK持续翻转。如果一个脉冲都没有多半是配置的时钟分频不对或者芯片没有真正工作起来。6. 寄存器表组织与向图像处理链路扩展SCCB配置只是OV7670项目的起点摄像头真正输出图像后才是FPGA图像处理的开始。这里有几个基于实际项目的建议可以帮你把寄存器表这个“地基”打得更稳。6.1 配置参数对后续图像处理的影响我建议把摄像头初始化配置和图像处理算法分开考虑。图像处理链路中需要的原始图像格式在SCCB初始化阶段就应该确定下来而不是等图像数据处理到一半再回头改寄存器。比如做车牌识别场景OV7670输出RGB565给FPGA后FPGA要先把RGB565转成灰度图再做边缘检测、二值化、字符定位。如果在这里把分辨率配置成VGA640x480一行图像就有1280字节中间缓存就需要更多的BRAM或者SRAM处理延迟也会上升。配置成QVGA320x240后一行只有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图像领域的同学来说OV7670SCCB这套组合练好后续做更复杂的摄像头项目会轻松很多。我自己每次拿到一款新开发板第一步永远是先点亮摄像头把图像稳定输出到显示器上再做其它图像算法。配置链路稳定了后面的路才走得稳。
返回列表