简介:本资源是一套基于Xilinx Nexys4 DDR开发板与OV7670 CMOS摄像头的FPGA实时视频监控系统完整实现方案,面向数字电路、嵌入式图像处理方向的本科高年级学生及FPGA初学者,解决图像采集→缓存→VGA实时显示这一典型视频流水线开发难题。压缩包共207个文件,14.7MB,涵盖Verilog/VHDL双语言工程(19个.v + 3个.vhd)、Vivado综合/实现脚本(.tcl/.do/.sh)、约束文件(.xdc)、IP核配置(.xci)、比特流与调试日志(.bit/.log/.rpt)等核心开发资产,结构完整,可直接编译下载运行。已有332人学习下载,提供两种语言可验证的顶层模块、RAM缓存控制逻辑、OV7670驱动时序与VGA时序生成模块,并包含已通过综合与布局布线的.dcp中间文件及可烧录的.bit文件,显著降低入门门槛与调试成本。 最近把一块吃灰的 Nexys4 DDR 开发板翻了出来,配上一个网购的 OV7670 摄像头模块,花了一个周末时间搭出了一套实时的视频采集显示系统。核心是 FPGA 从 OV7670 读取 DVP 接口的并行图像数据,经过 RGB565 拼接之后写入板载 DDR2 内存做帧缓存,再由 VGA 接口输出到显示器。整套链路跑通之后,画面流畅、色彩正常、帧率稳定,算是一个非常典型的 FPGA 数字图像处理入门到进阶的项目。
如果你手里正好有 Nexys4 DDR,或者任何一块带 DDR 内存的 Artix-7 / Zynq 开发板,又对摄像头采集、DVP 接口、帧缓存设计这些内容感兴趣,这篇博文就是为你准备的。我尽量把从硬件接线到 Verilog 代码设计的过程都讲清楚,包括那些你在教程里常常看不到的坑。
1. 项目总览:这套监控系统到底解决什么问题
先说说这个项目最核心的矛盾:摄像头图像数据是持续不断产生的流式数据,而 FPGA 内部可用的存储资源非常有限。OV7670 在 VGA 分辨率、RGB565 格式下,一帧裸数据是 640 乘 480 乘 2 字节,大约 600KB。如果你用的是不带 DDR 的 FPGA 开发板,内部 BRAM 通常只有几百 KB,连一帧都放不下,更别说做多帧缓冲。
Nexys4 DDR 这块板子之所以叫 DDR,就是因为它板载了一颗 128MB 的 DDR2 SDRAM 芯片。虽然我们用不了那么多,但哪怕只拿其中几 MB 做帧缓存,也足够把图像数据先存下来,再按显示器的节奏读出去。这就是整个系统的核心思路:摄像头进、DDR 存、VGA 出,三个时钟域互相独立,通过异步 FIFO 和读写指针解耦。
这个设计思路解决了好几个实际问题。比如说摄像头的 PCLK 像素时钟和 VGA 的像素时钟不是同一个频率源,两者之间必须有一个缓冲区。又比如说摄像头输出的图像质量受寄存器配置影响很大,不能简单上电就读,必须通过 SCCB 接口做初始化。把这些问题一个个拆开消化掉,你对 FPGA 数据通路设计、跨时钟域处理和常用总线协议的理解会上一个台阶。
适合谁来参考呢?我觉得三类人最合适:正在学 FPGA 想做综合项目的学生、想从 LED 流水灯进阶到图像处理的开发者、以及想快速验证 OV7670 摄像头模组功能但又不想碰单片机的硬件爱好者。这个项目虽然不复杂,但它把所有关键环节都串起来了,是个性价比很高的练手题材。
1.1 为什么选 FPGA 而不是单片机或者树莓派
很多人会问,摄像头监控这种活,用树莓派加 Python 几行代码就搞定了,为什么非要上 FPGA?这个问题我确实被问过很多次,答案也很实际。树莓派那条路线本质是“采集到内存,CPU 参与处理,再输出”,系统能跑,但图像数据在 CPU、GPU、内存之间绕了好几圈,延迟做不到非常低。
FPGA 的优势在于数据通路是专用的,摄像头数据流经过几个简单的状态机就被写进 DDR,读出来经过显示时序发生器直接推给 VGA 接口,整个过程不需要软件参与调度。而且如果你想在图像上叠加矩形框、做边缘检测、二值化这类预处理,直接在数据流路径上插入一个组合逻辑模块就能完成,延迟是纳秒级的。
另外,OV7670 这种 DVP 接口摄像头在嵌入式领域非常典型,时序固定、信号少、协议简单,特别适合用来理解图像传感器的工作原理。相比之下,MIPI CSI 接口的摄像头虽然现在更主流,但协议复杂、调试难度大,第一课就上 MIPI 有点劝退。先玩明白 DVP,后面再接触 MIPI 会轻松很多。
1.2 系统整体架构与数据流
整个系统的数据通路可以分成五段。第一段是 OV7670 摄像头本身,它负责把光学信号转换成数字 RGB 数据,通过 8 根数据线 D[7:0]、像素时钟 PCLK、行同步 HREF、帧同步 VSYNC 输出。第二段是 FPGA 内部的采集模块,负责在 HREF 为高的时候把两个连续的字节拼成一个 16 位的 RGB565 像素。
第三段是写 DDR 的链路。拼接好的像素数据经过一个异步 FIFO 跨到 DDR 控制器时钟域,然后组合成 AXI4 突发写请求,写入 DDR2 的帧缓冲区域。第四段是读 DDR 的链路,VGA 时序生成器按照行场扫描的节奏,从 DDR 中把对应位置的像素读出来,同样经过一个异步 FIFO 跨回 VGA 像素时钟域。
第五段就是 VGA 接口输出,行同步、场同步、使能信号和数据信号按标准时序送到显示器。
这里要特别注意的是,摄像头写入和 VGA 读取的区域必须分开,也就是说至少要有两个缓冲区。摄像头写第 0 帧的时候,VGA 读第 1 帧,下一帧再交换过来,这叫双缓冲。如果只有一个缓冲区,就会出现画面撕裂,也就是显示器上半部分是上一帧、下半部分是当前帧的奇怪现象。后面的章节我会详细讲这个设计。
2. 硬件选型与接线:别小看这步,坑都在线序里
OV7670 摄像头模块在淘宝上非常常见,价格从十几块到几十块不等,外形大同小异。需要注意的是,OV7670 这个传感器本身已经停产很多年了,现在能买到的绝大多数是库存芯片或者二手翻新芯片,不同批次的寄存器默认值可能会有点差异,这点先有心理准备。
Nexys4 DDR 板载的 FPGA 是 Xilinx Artix-7 XC7A100T-1CSG324C,有 4860Kb 的 Block RAM,引脚资源丰富,跑这个小项目绰绰有余。板子上的 PMOD 接口给摄像头模块供电和传信号都非常方便,但必须注意电压匹配。
2.1 OV7670 模块及其接口信号说明
OV7670 的接口信号不算多,但每个都有讲究。首先是最关键的 SCCB 配置接口,SIO_C 是时钟线,SIO_D 是数据线,它的时序和 I2C 几乎一样,只是协议细节略有区别。FPGA 通过这个接口向传感器内部的寄存器写入配置值,比如输出格式、分辨率、帧率、增益、曝光等。
然后是图像数据输出接口。XCLK 是外部输入时钟,由 FPGA 主动提供,一般给 24MHz 或 25MHz。PCLK 是像素时钟,由 OV7670 内部根据 XCLK 和相关寄存器分频后输出,在 VGA 模式 RGB565 下通常就是 24MHz 左右。VSYNC 是帧同步信号,新的一帧开始之前会有一个脉冲。HREF 是行有效信号,高电平期间,PCLK 的上升沿会驱动 D[7:0] 输出一个字节的数据。
模块上通常还会板载一个 3.3V 转 1.8V 的 LDO 稳压芯片,前端还有一个 25MHz 或 24MHz 的无源晶振。如果你不想用 FPGA 提供 XCLK,也可以直接用模块上的晶振,但那样的话 PCLK 和 FPGA 内部时钟之间就完全没有相位关系,跨时钟域风险更高。我建议还是由 FPGA 提供一个干净的时钟给 XCLK,这样时序从头到尾都在掌控之中。
2.2 Nexys4 DDR 端引脚分配与约束文件要点
我把摄像头模块接到了 Nexys4 DDR 的 JD PMOD 口,因为 JA 口被我用来接其他调试模块了,这纯属个人习惯。具体的引脚分配如下表:
| 信号名 | FPGA 引脚 | PMOD 引脚 |
|---|---|---|
| XCLK | A17 | JD1 |
| PCLK | B17 | JD2 |
| VSYNC | A18 | JD3 |
| HREF | B18 | JD4 |
| D[0] | A19 | JD7 |
| D[1] | B19 | JD8 |
| D[2] | A20 | JD9 |
| D[3] | B20 | JD10 |
| D[4] | C14 | JD5 |
| D[5] | D14 | JD6 |
| D[6] | E14 | JD11 |
| D[7] | F14 | JD12 |
| SIO_C | C17 | JD13 |
| SIO_D | D17 | JD14 |
由于 OV7670 的 D[7:0] 引脚在模块上排列并不连续,所以接线时一定要对照模块原理图,不能想当然地按顺序连。我第一版就吃过这个亏,以为模块上的 D0 到 D7 按顺序排开就行,结果图像颜色完全不对。
在 Vivado 的 XDC 约束文件里,除了引脚位置,还应该加上 IO 电平标准。PMOD 接口供电是 3.3V,所以全部设置为 LVCMOS33。这里有一个细节:OV7670 的输出引脚是 3.3V 电平的,但内部核心电压是 1.8V,模块上的电平转换电路一般已经处理好了,所以直接连 FPGA 没有问题。
2.3 时钟规划:24MHz 的来龙去脉
整个系统的时钟其实不需要太复杂,Nexys4 DDR 板载 100MHz 晶振,进 FPGA 后分发到三个地方。第一路经 MMCM/PLL 生成一个 24MHz 时钟给摄像头的 XCLK,同时也是采集模块的参考时钟。第二路是 VGA 用的 25.2MHz 像素时钟,实际上 25MHz 也完全够用,绝大多数显示器都能容忍这个偏差。
第三路是 DDR 控制器的 MIG 参考时钟。MIG IP 核需要输入一个 200MHz 或 100MHz 的参考时钟,用来内部产生 DDR2 所需的命令时序。这部分可以直接用板载 100MHz 时钟,然后把用户接口时钟配置成 200MHz,方便后续 AXI 突发传输。
三路时钟之间有天然的频率差,所以每一处跨时钟域的 FIFO 都必须设计成异步 FIFO,用各自的读时钟和写时钟驱动两侧端口。这个后面会专门展开。
3. SCCB 寄存器配置:给摄像头“写程序”
OV7670 传感器内部有上百个寄存器,控制着输出格式、分辨率、时序、自动曝光、白平衡、色彩饱和度等内容。出厂默认值虽然能出图,但颜色和质量都很糟糕,所以上电后的第一件事就是通过 SCCB 接口把寄存器配置成我们需要的状态。
SCCB 是 OmniVision 定义的一种类似 I2C 的串行接口,OV7670 从设备的 7 位地址是 0x21,加上读写位后,写地址是 0x42,读地址是 0x43。和 I2C 相比,SCCB 不支持多字节连续读写,每个寄存器都必须单独发送起始条件、设备地址、寄存器地址、数据和停止条件。
3.1 SCCB 时序与 I2C 的异同
SCCB 的时序可以完全套用 I2C 的 Master 逻辑,只要把地址位数和读写流程对应上就行。在 FPGA 里实现一个 SCCB Master,通常就是一个状态机:IDLE 空闲,START 发送起始条件,SEND_DEV_ADDR 发送 0x42,SEND_REG_ADDR 发送寄存器地址,SEND_DATA 发送配置值,STOP 发送停止条件。
时钟频率不用太高,OV7670 的规格书说最大 400kHz,我一般配置 100kHz 或者 200kHz,通过一个计数器从 24MHz 分频得到。写寄存器时要注意,在上电后至少等待 1ms 再开始配置,最好在复位后先向 0x12 寄存器写 0x80,这会触发传感器内部的一次软复位,然后延时几十毫秒再继续配置其他寄存器。
读寄存器稍微麻烦一点,需要先发一个“写寄存器地址”的虚拟写操作,把要读的寄存器地址告诉传感器,再重新发起起始条件,发送 0x43 读地址,然后读取数据。由于我在这个项目里只需要写寄存器、不需要读回校验,所以 FPGA 代码里只实现了写功能,省了不少状态。
3.2 关键寄存器逐个拆解
经过多个项目的验证,我整理了一份在 640x480、RGB565、30fps 下比较稳定的一组寄存器配置。注意这些值不一定在每一颗 OV7670 上都完全一致,如果你用的是某宝模块自带的方案,建议照着模块卖家给的参考代码来微调。
| 寄存器地址 | 寄存器名 | 配置值 | 作用说明 |
|---|---|---|---|
| 0x12 | COM7 | 0x80 | 软复位,写完后延时 |
| 0x12 | COM7 | 0x04 | 选择 RGB 输出格式 |
| 0x40 | COM15 | 0xD0 | 设置 RGB565 输出 |
| 0x3A | TSLB | 0x04 | 像素字节顺序 RGB |
| 0x3D | COM11 | 0x08 | 自动增益上限调整 |
| 0x11 | CLKRC | 0x00 | XCLK 直通,不额外分频 |
| 0x0C | COM3 | 0x00 | 关闭缩放,使用原始分辨率 |
| 0x17 | HSTART | 0x13 | 水平起始位置 |
| 0x18 | HSTOP | 0x01 | 水平结束位置 |
| 0x32 | HREF | 0xB6 | 行有效信号配置 |
| 0x19 | VSTART | 0x02 | 垂直起始位置 |
| 0x1A | VSTOP | 0x7A | 垂直结束位置 |
| 0x03 | VREF | 0x0A | 帧同步配置 |
| 0x70 | 白平衡 | 0x3A | 自动白平衡相关 |
| 0x71 | 白平衡 | 0x35 | 自动白平衡相关 |
| 0x72 | 白平衡 | 0x11 | 自动白平衡相关 |
COM7 是最重要的控制寄存器,bit[2] 是 RGB 模式选择位,bit[0] 是 Bayer RAW 选择位。只有把 COM7 设置为 0x04 之后,传感器才会输出 RGB 格式的数据。COM15 的 bit[7:6] 是输出格式选择,0xD0 对应 00 是 RGB565,bit[5:4] 是 11 表示 RGB 编码使用 565 布局。
TSLB 寄存器控制像素在两个字节中的排列顺序,0x04 表示先输出高位字节再输出低位字节,这样 D[7:0] 的第一个字节就是 R[4:0] 加 G[5:3],第二个字节是 G[2:0] 加 B[4:0],拼在一起就是标准 RGB565。
3.3 初始化状态机设计要点
初始化状态机在 FPGA 里就是一组寄存器地址和数据对应的 ROM,再加上一个计数器依次读取。每个寄存器的写入间隔建议留出至少 1ms 的延时,尤其写 0x12 复位之后要等更久。实际调试的时候我发现,如果延时太短,后面的寄存器可能会写入失败,表现就是图像虽然有了但是颜色错乱。
状态机可以用一个 case 语句实现,状态包括 RST、WAIT、WRITE、DONE 四个主要阶段。WRITE 状态下再细分发地址、发数据、等待应答这些子步骤。写完后可以通过一个 done 信号通知采集模块开始接收图像数据。
值得注意的是,OV7670 的寄存器默认值在断电后会丢失,每次上电都必须重新配置。所以这个初始化模块要跟着系统复位一起工作,不能只在启动时跑一次,否则热复位后摄像头会停留在未配置状态。
4. DVP 数据采集与 RGB565 拼接:从字节流到像素流
寄存器配置完成之后,OV7670 就会按照我们设定的格式持续不断地输出图像数据。这时候 FPGA 的第一个核心模块开始工作,它要做的事情是:把连续的字节流按正确的边界拆成像素,生成像素有效信号和帧同步信号。
这个模块放在整个数据链路的最前端,它的正确性直接决定后面所有环节的信号质量。如果这里拼接错位了,后面的 DDR 缓存和 VGA 显示再完美也白搭。
4.1 行场同步信号到底怎么理解
VSYNC 和 HREF 是理解 DVP 接口的两把钥匙。VSYNC 低电平脉冲标志一帧的结束,紧接着就是新一帧的开始。HREF 是高电平有效,拉高期间每一个 PCLK 上升沿对应一个字节的数据输出,拉低期间没有数据输出。
可以把 VSYNC 想象成书的目录翻页,每翻一页就是新的一章,也就是新的一帧。HREF 就是每一行的书写过程,从左到右写满一行后停一下,换到下一行继续。传感器内部的行列计数逻辑决定了这些同步信号什么时候拉高拉低,我们不需要去干预,只需要正确响应它们。
在 Vivado 的波形图里,你会看到 HREF 是一系列等宽的脉冲,VSYNC 则是一个很窄的脉冲。每两个 VSYNC 脉冲之间,有大约 480 个 HREF 脉冲,每个 HREF 脉冲期间有大约 640 个像素的字节数据输出。这些数量不是正好对应的,因为传感器还会输出一些边缘的无效像素,需要靠寄存器设置来裁剪掉。
4.2 像素数据拼接逻辑与字节对齐
OV7670 在 RGB565 模式下,每个像素由两个字节组成,先输出高字节再输出低字节。问题是 FPGA 端怎么知道当前 PCLK 上升沿来的字节到底是高字节还是低字节?答案就是看 HREF 的起始位置。
我采用的方案是:在 HREF 的上升沿,也就是一行开始的时候,用一个标志位 pixel_low 初始化为 0。每来一个 PCLK 上升沿,如果 pixel_low 是 0,说明当前字节是某个像素的高字节,把它存到高 8 位,然后把 pixel_low 置 1。下一个 PCLK 上升沿来的时候,把当前字节放到低 8 位,拼成一个 16 位的 RGB565 像素,同时产生一个 pixel_valid 脉冲,再把 pixel_low 置回 0。
这个逻辑用 Verilog 写出来非常简短,核心就一个 case 或者 if 语句。但有一个细节容易忽略:HREF 拉低之后再次拉高时,pixel_low 必须重新回到 0。也就是说,需要在 HREF == 0 的时候把 pixel_low 强制清零,保证每一行都从高字节开始。
4.3 帧有效检测与 FIFO 写入
拼接出来的 16 位像素怎么送到 DDR 里?直接写是写不进去的,因为 DDR 控制器工作在完全不同的时钟域和总线协议下。我的做法是先用一个异步 FIFO 把像素数据从 PCLK 域缓存到 DDR 控制器时钟域。
FIFO 的写侧时钟是 PCLK,写入数据是拼接好的 16 位 RGB565,写入使能是 pixel_valid。FIFO 的读侧时钟是 DDR 控制器的 user_clock,读取由写 DDR 的状态机控制。在 FIFO 的前面,再加一个帧同步模块,检测 VSYNC 脉冲,每检测到一帧的结束就产生一个 frame_start 信号,提醒写 DDR 状态机当前帧已经完整采集完毕,可以切换缓冲区了。
有一点要注意,FIFO 的深度最好能吸收一行的数据突发。VGA 模式下一行有 640 个有效像素,也就是 640 个 16 位数据,考虑到 DDR 写操作的突发长度,我配置了 1024 深度的 FIFO,这样即使 DDR 总线上有别的请求占用,也能保证这一行的数据不会溢出。
5. DDR 帧缓存:为什么必须用它,怎么接
说实话,如果只是想在显示器上看个动态画面,不用 DDR 其实也能做到,只要把 OV7670 的输出时序直接整理成 VGA 时序,显示器同样能显示。但这种方式有一个致命的问题:摄像头的帧速率和显示器的刷新率不同步,画面容易出现撕裂翻滚。
DDR 帧缓存的价值就在于,它把采集和显示两个过程彻底解耦。摄像头采集完一帧写到缓存区 A,显示端去读缓存区 B;下一帧采集写缓存区 B,显示端读缓存区 A。两边各干各的,互不干扰,显示出来的画面自然就稳定了。
5.1 带宽估算与帧缓存策略
先算一下 DDR 的带宽需求。640 乘 480 分辨率、30fps、RGB565 格式,每帧数据量是 614400 字节,每秒数据量接近 18.4MB。DDR 一侧需要同时处理写入和读出,也就是双向 36.8MB/s 的吞吐量。Nexys4 DDR 板载的 DDR2 SDRAM 理论带宽在几百 MB/s 级别,完全够用。
真正要小心的是系统总线上还有其他设备在访问 DDR。比如 MIG 生成的 AXI Interconnect 会仲裁不同主设备的请求,如果写视频和读视频的优先级设置不当,可能会出现某一帧写到一半就被读操作长期阻塞的情况。我在这里的做法是给写通道更高的优先级,因为采集是实时数据流,一旦丢失就补不回来,而读通道哪怕稍微等一拍也不至于造成严重问题。
帧缓存策略上,最理想的是三缓冲。摄像头写缓冲 0,VGA 读缓冲 1,同时 VGA 下一帧要切换到缓冲 2。三缓冲能最大程度减少显示端画面撕裂的概率,代价是多占用 1.2MB 的 DDR 空间。对于 128MB 的容量来说完全可以接受。我实际用了三缓冲,效果比双缓冲好不少。
5.2 MIG 与 AXI4 接口接入
Nexys4 DDR 在 Vivado 中使用 MIG(Memory Interface Generator)生成 DDR2 控制器。MIG 会生成一个完整的 AXI4 接口,主设备只需把读写请求发过去就能完成对 DDR 的访问。MIG 的配置过程有几个容易出错的地方。
首先是 DDR2 的型号选择,必须选择 MT47H128M16RT-25E,这是 Nexys4 DDR 板载的颗粒型号,选错了会导致初始化失败。其次是参考时钟,MIG 的参考时钟输入是板载 100MHz 的时钟引脚,这个在官方 XDC 文件里已经有约束,不要随便改。第三是用户端口宽度和突发长度,视频数据是 16 位宽的,我选择 32 位用户数据位宽、突发长度 8,这样一次突发可以传 4 个像素。
MIG 生成之后,在顶层模块里例化它,并把用户时钟、复位和 AXI 接口拉出来。视频读写模块作为 AXI Master 连接进去。需要说明的是,MIG 的初始化时间比较长,上电后需要等待 init_calib_done 信号拉高才能访问 DDR,否则读写请求会被直接丢弃。
5.3 写侧状态机:像素帧如何写入 DDR
写侧状态机的主要任务是把异步 FIFO 中的数据连续搬到 DDR 的指定地址空间。帧缓存地址空间规划如下:缓冲 0 从 0x01000000 开始,缓冲 1 从 0x01100000 开始,缓冲 2 从 0x01200000 开始,每个缓冲长度 0x100000,也就是 1MB,实际有效数据只占 0x96000 字节,多出来的空间留有余量。
状态机的工作流程是:收到 frame_start 信号后,把当前目标写地址切换到新的缓冲起始地址,然后进入写数据状态。在写数据状态下,只要 FIFO 非空,就发起 AXI 写突发请求,每次突发 8 个 32 位数据(对应 16 个像素),地址递增。一帧写完(即 480 行、640 列像素全部写入)后,回到等待状态。
在代码里,AXI 写通道的握手信号包括 awvalid、awaddr、wvalid、wdata、wlast 和 bready。状态机必须正确配对这些信号,尤其是 wlast,它表示最后一个突发数据的到来,MIG 靠它来判断当前写事务的结束。我在调试时遇到过 wlast 提前拉高导致后续数据写错地址的情况,排查了很久才发现是计数器边界条件写错了。
5.4 读侧状态机与显示联动
读侧状态机相对简单,因为 VGA 显示是周期性的,它在每个有效显示行的开始时刻,发起一个 AXI 读请求,把 DDR 中对应行的数据一次性读取出来,经过一个读 FIFO 送到 VGA 接口。
读侧与写侧最大的不同是它不需要关心帧的概念,只需要按照 VGA 行计数器来决定读取哪一行。VGA 当前正在显示第 line 行时,读侧就从当前活动缓冲区的基地址加上 line 乘以一行字节数,开始读取该行数据。为了保证 VGA 接口的数据不出现空档,读 FIFO 的深度至少能缓存两行数据,我用 2048 深度,再配合 AXI 读请求提前几拍发起,实际效果很稳定。
读缓冲区和写缓冲区之间通过一个简单的乒乓指针切换。frame_start 到来时,写侧切换缓冲区;显示侧则在 VGA 帧同步信号到来时检查写侧最新的缓冲区编号,选择当前可读的缓冲区。为了避免读写同时切到同一个缓冲区,三缓冲时还需要加一个互斥逻辑,确保写侧永远不和读侧使用同一个缓冲区编号。
6. VGA 显示驱动与系统联调
VGA 显示是所有 FPGA 图像项目的必修课。虽然现在很多新显示器已经不再标配 VGA 接口,但在开发板上它依然是成本最低、时序最直观的显示方案。Nexys4 DDR 板载 VGA 接口,直接用 3 位模拟色彩信号加行场同步就能驱动。
6.1 VGA 时序参数计算
640x480 分辨率、60Hz 刷新率的 VGA 时序是固定标准。像素时钟是 25.175MHz,但在 FPGA 里生成这个非整数频率比较麻烦,我用 MMCM 生成 25MHz,绝大多数显示器都能正常识别。完整的一行是 800 个像素时钟,其中有效显示区 640 个,行同步脉冲 96 个,行后沿 48 个,行前沿 16 个。完整的一帧是 525 行,其中有效显示区 480 行,场同步脉冲 2 行,场后沿 33 行,场前沿 10 行。
| 参数 | 数值 | 说明 |
|---|---|---|
| 行有效 | 640 | 显示区域宽度 |
| 行同步 | 96 | 行同步脉冲宽度 |
| 行后沿 | 48 | 同步后等待时间 |
| 行前沿 | 16 | 同步前等待时间 |
| 行总数 | 800 | 一行的总周期 |
| 场有效 | 480 | 显示区域高度 |
| 场同步 | 2 | 场同步脉冲宽度 |
| 场后沿 | 33 | 同步后等待时间 |
| 场前沿 | 10 | 同步前等待时间 |
| 场总数 | 525 | 一帧总行数 |
VGA 的 R、G、B 信号在 Nexys4 DDR 上各用 4 位,但板载电阻网络把低 1 位接地了,实际有效的是高 3 位,所以显示 24 位真彩色的能力有限。RGB565 的 5 位 R、6 位 G、5 位 B 需要截断到高 3 位再输出。
6.2 读 FIFO 填充与数据拼接
VGA 显示的核心是一个行场计数器,它产生当前扫描位置和行场同步信号,同时输出一个数据有效信号 de。de 为高时,表示当前处于有效显示区域,此时 FPGA 需要从读 FIFO 中取出一个像素数据送到 VGA 数据端口。
读 FIFO 的数据是 DDR 读回来的 32 位数据,包含两个 16 位像素。VGA 接口一个像素时钟只消费一个像素,所以需要一个解包逻辑,把 32 位数据拆成两个 16 位像素,先输出低 16 位,再输出高 16 位。这个过程要在 de 上升沿时同步完成。
如果读 FIFO 里的数据不够,VGA 就会输出空白或重复数据,表现为画面有横条纹。我实测下来的经验是,读 FIFO 的深度不要小于 512,否则 DDR 读请求一旦被仲裁延迟,FIFO 很容易被读空。
6.3 系统联调步骤与验证
上电联调时不要一上来就接摄像头,先把 VGA 显示一个纯色测试画面,确认显示链路没问题。具体做法是在 VGA 数据端口输出一个固定颜色,比如纯蓝色,看显示器是否正常显示、画面是否居中。这一步通过之后再加 DDR 读写测试,往固定地址写一串已知数据再读回来比对。
最后再接摄像头。先把 OV7670 的配置模块使能,用 ILA 抓 VSYNC、HREF 和拼接后的像素值,确认采集模块工作正常。这时再把读写 DDR 的通路打开,观察 VGA 输出。如果没有图像,按顺序检查配置模块是否有 done 信号、FIFO 是否有数据写入、写状态机是否产生了 AXI 写请求。
7. 踩坑记录与排查手册:实战中遇到的典型问题
这个项目从零到跑通,我前后花了差不多一个周末,中间踩过不少坑。有些坑查了很久才发现是低级错误,但这些问题在网上的教程里很少被系统整理,我把它们写下来,希望你能少走弯路。
7.1 症状速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 完全没有图像 | 寄存器初始化未完成 | 检查 done 信号、SCCB 波形 |
| 画面全黑但有同步信号 | HREF 或 PCLK 引脚约束错误 | 检查约束文件、ILA 抓引脚 |
| 图像偏色严重 | RGB 字节顺序或 COM15 配置错误 | 检查 TSLB 寄存器、拼接逻辑 |
| 画面有横条纹 | 读 FIFO 被读空 | 增大读 FIFO 深度、提前预取 |
| 画面有竖条纹 | 写侧突发长度或地址对齐错误 | 检查 AXI 写地址、wlast 时序 |
| 画面撕裂 | 缓冲切换逻辑错误 | 检查帧完成信号、乒乓逻辑 |
| 图像整体偏移 | HSTART/VSTART 寄存器配置不当 | 调整寄存器值、裁剪边缘像素 |
| 帧率明显偏低 | DDR 仲裁效率低 | 调整 AXI 优先级、增加突发长度 |
7.2 无图像输出的排查思路
无图像是最常见的故障,也是排查链路最长的。我一般按信号流向从前往后查:先确认 OV7670 有没有正常上电,PCLK 有没有输出,VSYNC 有没有脉冲。如果 VSYNC 有,说明摄像头基本工作,寄存器配置可能已经成功。接着看 ILA 抓到的 HREF,正常应该是 480 个脉冲左右,每个脉冲宽度在 640 个 PCLK 左右。
如果 VSYNC 和 HREF 都正常但显示器还是黑屏,那就把问题聚焦在写 DDR 链路。用 ILA 抓写侧状态机的状态,看它有没有进入写数据状态,AXI 写接口有没有握手成功。这里有一个隐藏很深的坑:MIG 生成时如果选择的内存型号或者参考时钟配置错误,init_calib_done 信号可能永远不会拉高,而你不会第一时间发现。
7.3 画面花屏与数据错位的处理方法
花屏一般是数据错位或者地址错位。最常见的是 RGB565 拼接错误,也就是高低字节搞反了。如果出现这种情况,图像会表现为蓝红互换、颜色完全混乱。把拼接逻辑里的高低字节交换一下,症状就会消失。
另一个常见原因是 AXI 写地址没有按字节地址对齐。MIG 的 AXI 接口地址是按字节编址的,一个 32 位数据占据 4 个字节地址。如果你的地址计数器每写一个像素加 2(因为 RGB565 是 2 字节),但在 AXI 接口上却应该每次加 4(因为一次传 32 位数据),就会造成数据覆盖和花屏。我建议把地址递增和突发长度分开规划,每次突发固定地址加 32,不要用手写像素数去推导。
7.4 使用 ILA 调试的技巧
Vivado 的 ILA 是这个项目的调试利器。我调试时在三个位置挂了探针:摄像头输入端的 PCLK、HREF、D[7:0] 和拼接后的像素值;写侧状态机的状态和 AXI 写通道信号;读侧 VGA 的 de 和像素输出。三个探针配合触发条件,能快速定位故障发生在哪一段。
ILA 的采样深度我一般设置 4096,足够抓到一帧的数据。触发条件设置为 VSYNC 下降沿,这样从帧头开始采样,就能看到完整的一行数据从 PCLK 到像素输出的全过程。采集完的数据可以在波形窗口里用十六进制查看,对照 OV7670 的输出规律,很容易发现数据是在哪个环节丢失或错位的。
提示:如果 ILA 显示采集到的 HREF 行数不对,比如偏多或者偏少,先检查 OV7670 的 HSTART、HSTOP、VSTART、VSTOP 寄存器。这些寄存器控制传感器内部的窗口裁剪,裁剪区域不合适时,有效行数就会和理论值有偏差。
8. 实操心得与后续扩展方向
整套系统跑通之后,我最大的感受是:这个项目真正难的地方不在某一块单独的电路设计,而在于把多个不同来源、不同时钟域、不同协议的模块拼在一起协同工作。每一段单独拿出来都不复杂,但串联起来之后,任何一个环节出问题都会导致最终画面异常。
从代码量来看,整个工程也就一千多行 Verilog,核心模块不外乎 SCCB 控制器、像素采集拼接、异步 FIFO、AXI 读写状态机、VGA 时序发生器。如果每个模块都能独立测试通过,联调阶段其实非常快。怕就怕在模块内部留了很多“差不多能用就行”的隐患,联调时就会集中爆发。
后续如果想继续做深,有几个方向值得尝试。一个是在数据通路上插入图像预处理模块,比如灰度化、二值化、Sobel 边缘检测,这些算法在 FPGA 里实现也不复杂,但会让项目更像一个边缘计算设备。另一个是换成 HDMI 输出, Nexys4 DDR 板子没有 HDMI 接口,但可以外接一个 HDMI 扩展板,把 VGA 时序转换成 TMDS 信号。还可以试试把 DDR 帧缓存扩展到真正的视频缓冲池,做多路视频叠加或者画中画效果。
最后分享一个小技巧:如果你用的是和我相同的 OV7670 模块,发现画面颜色始终偏黄或者偏绿,大概率不是代码问题,而是摄像头的白平衡寄存器没调好。OV7670 的自动白平衡在很多模块上默认就是关掉的,你需要手动把 0x70、0x71、0x72 这组寄存器写进去,色彩才会正常。这个坑我调试时反复折腾了很久,最后对着寄存器手册一个一个试才解决。希望这篇博文能帮你在类似的路上少踩几个坑。
本文还有配套的精品资源,点击获取