从 OV7670 到 VGA:一条 FPGA 图像采集与稳定显示链路
这份工程的目标很直接把 OV7670 摄像头输出的 DVP 图像流稳定显示到 DE0-CV 的 VGA 接口上。看起来只是“采集再显示”但真正让画面稳定下来的关键不在某一个孤立模块而在时钟、配置、像素拼接、跨时钟 FIFO、SDRAM 帧缓存和 VGA 扫描之间的配合。工程链接FPGA_project: FPGA小项目万年历打地鼠贪吃蛇等游戏https://gitee.com/programming-loving-duck/fpga_project/tree/master/23C7_OV7670最终数据链路可以概括为flowchart LR A[OV7670 DVPbr/PCLK/VSYNC/HREF/D[7:0]] -- B[RGB565 拼接br/ov7670_data_16rgb565] B -- C[写异步 FIFObr/PCLK - 100MHz] C -- D[SDRAM 帧缓存br/512 x 600 x 16bit] D -- E[读异步 FIFObr/100MHz - 25MHz] E -- F[VGA 640x48060Hzbr/25MHz 像素时钟]硬件依据DE0-CV 用户手册给出了几个对本工程非常关键的约束板载主时钟是 50MHzCLOCK_50对应 FPGAPIN_M9。FPGA 器件是 Cyclone V5CEBA4F23C7N。VGA 接口使用 15-pin D-SUBRGB 每色 4bit 电阻网络 DAC。标准 VGA 模式为640x48060Hz像素时钟约 25MHz。板载 SDRAM 是 64MBx16 数据总线3.3V LVCMOS。两组 2x20 GPIO 可以连接摄像头模块并提供 3.3V、5V 和 GND。这些信息对应到工程约束文件中例如clk约束到PIN_M9VGA 的 RGB/HS/VS 约束到手册表格中的引脚OV7670 信号则接到 GPIO 引脚。1. 摄像头 XCLK先让 OV7670 跑起来OV7670 需要外部时钟 XCLK 才能工作。工程中 PLL 从板载 50MHz 生成两个主要时钟clk_25M提供给 OV7670 XCLK同时用于 VGA 像素时钟。clk_100M用于 SDRAM 控制器和读写仲裁。顶层直接把 25MHz 输出给摄像头assign ov7670_xclk clk_25M; assign ov7670_pwdn 1b0; assign ov7670_reset 1b1;这一步很重要没有稳定 XCLK后面的 SCCB 配置、PCLK 输出和图像采集都无从谈起。pwdn0表示摄像头退出掉电reset1表示释放复位。2. SCCB/I2C 配置把摄像头调到 RGB565/VGA 输出OV7670 的寄存器通过 SCCB 配置工程中由ov7670_init.v、SCCB_sender.v和ov7670_config.v完成。初始化逻辑先等待约 3ms再开始发送寄存器表。设备写地址使用8h42localparam device_id 8b0100_0010;SCCB_sender.v把一次寄存器写拆成START - device_id - register address - register value - STOP其中SCL_CNT_NUM 500在 25MHz 初始化时钟下约为 50kHz在 50MHz 下约为 100kHz。当前顶层把ov7670_init接在clk_25M上所以实际 SCCB 时钟更接近 50kHz速度保守但可靠。寄存器表里最关键的几项是16h1204 // COM7: RGB 输出 16h40d0 // COM15: RGB 输出控制 16h3a04 // TSLB 16h3dc8 // COM13 16h1180 // CLKRC: 内部时钟分频这几项决定了 OV7670 输出 RGB 格式、字节顺序和像素时钟节奏。采集模块后面按“两字节拼一个 RGB565 像素”的方式工作所以寄存器格式必须和采集逻辑一致。3. PCLK、VSYNC、HREF 采集只在有效窗口收像素OV7670 的 DVP 输出包含四类关键信号PCLK像素数据时钟由摄像头输出。VSYNC帧同步。HREF行有效。D[7:0]8bit 数据总线。工程在ov7670_data_16rgb565.v中使用ov7670_pclk作为采样时钟。这样做是正确的DVP 数据由摄像头发出就应该在摄像头给出的 PCLK 域里先采稳。采集窗口设为完整 VGAlocalparam [9:0] CAPTURE_WIDTH 10d640; localparam [8:0] CAPTURE_HEIGHT 9d480;模块还做了一个小但有用的处理初始化完成后不马上使用第一帧而是等两帧后再置frame_valid。这能避开摄像头刚配置完成时的过渡帧。行列计数逻辑由VSYNC和HREF驱动VSYNC上升沿认为新帧开始清零x_cnt/y_cnt。HREF从高到低认为一行结束y_cnt 1。每拼出一个完整像素x_cnt 1。4. RGB565 数据拼接两个 8bit 合成一个 16bit 像素OV7670 数据总线只有 8bit而 RGB565 像素是 16bit所以每个像素分两个 PCLK 字节输出。工程用byte_cnt在高低字节之间翻转if (!byte_cnt) pixel_buffer[15:8] din_r; else pixel_buffer[7:0] din_r;当第二个字节到来时输出完整 RGB565data_rgb565 {pixel_buffer[15:8], din_r}; data_rgb565_vld 1b1;这意味着data_rgb565_vld是真正的像素有效信号。后面的写 FIFO 只应该在它为高时写入。5. 帧缓存为什么这里不用简单直通OV7670 的 PCLK 和 VGA 的 25MHz 像素时钟不是同一个时钟源。即使频率接近也不能把摄像头数据直接送到 VGA 扫描端否则很容易出现撕裂、错位、花屏或随机白线。本工程采用两级异步 FIFO 加 SDRAMPCLK 写入 RGB565 - 异步 FIFO - 100MHz SDRAM burst 写 - 100MHz SDRAM burst 读 - showahead 异步 FIFO - 25MHz VGA 读取SDRAM 地址组织为localparam [9:0] IMG_COLS 10d512; localparam [9:0] IMG_ROWS 10d600;这不是显示分辨率而是帧缓存的线性地址组织512 * 600 307200正好等于640 * 480。每个像素 16bit刚好覆盖一帧 VGA 图像。当前工程为了稳定SDRAM 固定使用单 bankassign active_bank_addr 2b00;这会牺牲严格双缓冲能力但降低了读写 bank 切换相位不同步带来的复杂度。对于调通稳定画面这是一个很务实的选择。6. SDRAM 控制稳定显示的真正难点SDRAM_control.v包含初始化、预充电、刷新、ACTIVE、READ、WRITE 等状态。工程使用 100MHz 驱动 SDRAM 控制器并把sdram_clk输出为反相时钟assign sdram_clk ~clk;写侧策略写 FIFO 中数据量 8时触发一次 SDRAM 写。每次 burst 写 8 个 16bit 数据。写完后列地址加 8。读侧策略VGA 读 FIFO 中数据量 8时触发一次 SDRAM 读。每次 burst 读 8 个 16bit 数据。读出的数据写入 showahead FIFO供 VGA 端消费。调试中最关键的修正是 ACTIVE 阶段的行地址使用。当前代码在发 ACTIVE 命令时直接使用当前row_addrelse if (idle_to_active_start) sdram_addr {1b0, row_addr};如果这里误用上一拍锁存的旧行地址就可能出现“当前列地址 上一次行地址”的错配画面上表现为白线、分层、局部花屏。这个问题比线材问题更隐蔽因为画面不是完全没有而是“能看见但不稳定”。7. VGA 时序640x48060Hz 的扫描骨架DE0-CV 手册给出标准 VGA 时序项目水平垂直Sync96 pixel clocks2 linesBack porch48 pixel clocks33 linesActive640 pixel clocks480 linesFront porch16 pixel clocks10 linesPixel clock25MHz25MHz工程中的VGA_crtl.v使用localparam H_TOTAL 11d800; localparam H_SYNC 11d96; localparam H_START 11d144; localparam H_END 11d784; localparam V_TOTAL 11d525; localparam V_SYNC 11d2; localparam V_START 11d31; localparam V_END 11d511;这里水平完全对应96 48 640 16 800。垂直有效区从 31 开始比手册的 33 行 back porch 少 2 行但总行数仍是 525很多显示器可以兼容。若遇到显示器边缘位置异常可以优先把V_START调成 35 或按手册严格使用2 33 35作为有效区起点。VGA 输出只有 12bit每色 4bit所以 RGB565 被截位v_r pixel_data[15:12]; v_g pixel_data[10:7]; v_b pixel_data[4:1];这不是采集错误而是 DE0-CV VGA DAC 的硬件限制。8. 图像稳定显示的关键经验这类工程最容易误判的地方是看到白线或花屏就先怀疑摄像头坏了、线太长、屏幕太大。硬件连接当然重要但当画面已经能看到轮廓时更应该优先查数据链路。推荐排查顺序data_rgb565_vld是否每行稳定输出 640 个像素。byte_cnt是否在每个HREF低电平期间清零避免高低字节错位。写 FIFO 是否溢出读 FIFO 是否空。SDRAM 行、列、bank 地址是否在 burst 边界正确递增。SDRAM ACTIVE 命令使用的是当前行地址还是旧行地址。VGA 端pixel_req与 FIFOrdreq是否一一对应。最后再排查杜邦线、共地、PCLK 边沿、摄像头供电和镜头焦距。如果要继续优化可以考虑恢复真正双缓冲写 bank 只在 OV7670 帧结束后切换读 bank 只在 VGA 帧开始时切换并且读端只能切到“已经完整写完”的 bank。否则双缓冲反而会引入半帧旧数据和半帧新数据混读的问题。9. 调试中遇到的困难与解决方案这次调试最折磨人的地方是故障现象并不是“完全不工作”而是“能看到一点画面但画面偏红、闪烁、有白线、有分层”。这种状态很容易让人来回怀疑摄像头、显示器、杜邦线和寄存器配置但真正的问题往往藏在数据流边界上。第一类困难是 OV7670 输出格式不确定。早期画面整体偏红、花屏说明摄像头已经有数据输出但 FPGA 对数据格式的理解和摄像头寄存器配置没有完全对齐。解决办法是先把配置收敛到 RGB/VGA 输出COM716h1204COM1516h40d0并让采集模块固定按 RGB565 两字节拼接。这里的判断依据是如果配置从 YUV 或错误 RGB 顺序输出而采集端仍按 RGB565 解释颜色会整体异常不只是局部噪声。第二类困难是图像闪烁和不连续。摄像头 PCLK、SDRAM 100MHz、VGA 25MHz 是三个节奏不同的时钟域任何直接跨域都会埋下不稳定因素。解决办法是保留两级异步 FIFO写 FIFO 负责从 PCLK 域跨到 SDRAM 域读 FIFO 负责从 SDRAM 域跨到 VGA 域。与此同时帧同步、bank ready 这类控制信号只通过同步触发器或 toggle 方式跨域避免把多 bit 状态直接甩到另一个时钟域。第三类困难是白线和分层。这个现象最像线材接触不良也最容易误判。调试时更换短线、检查共地后问题仍在说明主因不在外部连接。继续追踪 SDRAM 后发现关键在 ACTIVE 命令发出时使用的行地址如果 ACTIVE 阶段取到的是上一笔事务锁存的旧row_addr_latch而后面的读写列地址又来自当前事务就会形成“旧行 新列”的错配。解决办法是 ACTIVE 直接使用当前row_addr也就是当前代码里的else if (idle_to_active_start) sdram_addr {1b0, row_addr};同样ACTIVE 阶段 bank 也使用当前bank_addr。这个修正后白线和分层问题明显改善是整条链路稳定下来的关键一步。第四类困难是双缓冲 bank 切换过早。工程中曾尝试用 SDRAM 不同 bank 做读写双缓冲但 OV7670 帧率和 VGA 帧率不完全同相写 bank 在摄像头帧边界切换读 bank 在 VGA 帧边界切换如果“已写完帧”的握手不严谨VGA 可能读到半帧旧数据或未写完的新数据。解决办法是调试阶段先固定单 bankassign active_bank_addr 2b00;这不是最终性能最优的结构但它把变量减少到最低先保证能稳定显示。后续要恢复双缓冲必须增加“完整帧写完”标志并且只允许读端在 VGA 帧起点切到 ready bank。第五类困难是 VGA 显示参数和显示器兼容性。DE0-CV 手册给出的 640x48060Hz 标准是 25MHz 像素时钟水平总周期 800垂直总行数 525。工程水平时序严格对应标准垂直有效区起点略有差异但多数显示器可以容忍。如果遇到画面偏移、边缘不完整优先检查 porch 参数而不是先改摄像头分辨率。这次调试的经验是当图像“完全没有”时先查 XCLK、SCCB、PCLK当图像“能看到但不稳定”时优先查 FIFO、SDRAM 地址、burst 边界和跨时钟同步。前者是让系统跑起来后者才是让画面安静下来。小结这份工程的稳定显示不是靠某个“神奇寄存器”完成的而是靠完整链路的节奏对齐OV7670 用 25MHz XCLK 工作SCCB 配成 RGB 输出PCLK 域内拼接 RGB565经异步 FIFO 跨到 100MHz SDRAM再经读 FIFO 回到 25MHz VGA 扫描域。真正值得记住的一点是图像工程里能看到画面不代表链路已经稳定。白线、分层、局部花屏往往不是摄像头本身而是帧缓存地址、burst 对齐、跨时钟 FIFO 或读写仲裁在某个边界条件上出了错。现象不太方便放出来有兴趣的朋友可以自己跑一遍哈并且OV7670的像素很低后续会换成OV2640摄像头跑一遍代码会持续更新