本页面属于一个小项目系列,这些项目用于探索 Smart Zynq 开发板的各种功能。
这个项目也发布在 HelloFPGA 网站,推荐中文读者阅读。
简介
本教程介绍如何将 OV7670 摄像头模块连接到 Smart Zynq,并在开发板自带的 HDMI 输出上实时显示视频信号。我还会演示如何借助一条简单的 Linux 命令,把原始视频流轻松保存到文件中。
本教程中的信息同样适用于其他类型的数据采集(data acquisition):下面展示的技术既可以配合其他图像数据源使用,也可以配合其他数字数据源使用。
摄像头模块连接到 Zynq 芯片的 PL(FPGA)部分。因此,在把图像数据发送给 ARM 处理器之前,可以很方便地加入实现图像处理的逻辑。由于本教程基于 Xillinux,系统处理部分由完整的 Linux 发行版组成。
选择 OV7670 模块来做这个演示,是因为该硬件便宜、常见且容易买到。另外,该器件产生的数字信号也比较容易理解。
不过,OV7670 有一个令人遗憾的缺点:默认情况下,视频流的颜色是错误的。这是该摄像头传感器一个已知的问题。只需修改这个摄像头的少数几个寄存器,就可以纠正这个问题。因此本教程分为两个部分:
- 如何把摄像头传感器连接到 Smart Zynq 开发板,以及如何用简单的 Linux 工具读取视频流。
- 如何获得颜色正确的视频流。这部分介绍如何利用 I2C,通过 Xillybus 来修改摄像头传感器的寄存器。
请注意,本教程有很大一部分是在解释实现原理。不过对于实际使用摄像头来说,并不需要理解这些解释。
OV7670 模块
本教程使用的是下图所示的摄像头模块:
该模块上,排针的大多数引脚都直接连接到 OV7670 器件。只有 3.3V 和 GND 与电压调节器相连。因此,FPGA 与模块之间的所有连接,都是 FPGA 与 OV7670 器件的直接连接。
市面上还有其他功能相同的模块,使用这些模块可能也没问题。例如,有一种不同的模块,其 PCB 上印着“2017/3/15”,这种模块也能正常工作。另一方面,有一种模块不能正常工作;不能正常工作的模块 PCB 上印着“QYF-OV7670 V3.0”。
另外还要注意,OV7670 器件有不同的版本。可以验证模块上使用的是否是正确的版本。如何验证将在本教程的第二部分中说明。
关于 OV7670,有两个主要信息来源。这两份文档都可以在互联网上找到:
- 数据手册:OV7670/OV7171 CMOS VGA (640x480) CAMERA CHIP Sensor with OmniPixel Technology, Version 1.4, August 21, 2006。获得这份文档的 1.4 版本很重要。
- 实现指南:OV7670/OV7171 CMOS VGA (640x480) CameraChip Implementation Guide。互联网上似乎只有 2005 年 9 月 2 日的 1.0 版。很遗憾,这个版本已经过时,对应的是更早版本的摄像头芯片。
准备 Vivado 工程
从演示包(demo bundle)的 zip 文件(boot partition kit)创建新的 Vivado 工程。用文本编辑器打开 verilog/src/xillydemo.v。删除代码中标有“PART 2”的部分,用下面的代码片段替换它:
/*
* PART 2
* ======
*
* This code demonstrates a frame grabber (data acquisition) from
* an OV7670 camera module.
*
*/
reg [1:0] clkdiv;
always @(posedge bus_clk)
clkdiv <= clkdiv + 1;
assign J6[10] = clkdiv[1]; // MCLK / XCLK
assign J6[0] = 0; // PWDN, the camera is always on
assign J6[1] = !user_w_write_32_open; // RESET#, active low
wire [7:0] D_in;
wire pclk_in, hsync_in, vsync_in;
assign D_in = J6[9:2];
assign pclk_in = J6[11];
assign hsync_in = J6[12];
assign vsync_in = J6[13];
(* IOB = "TRUE" *) reg [7:0] D_guard;
(* IOB = "TRUE" *) reg pclk_guard, hsync_guard, vsync_guard;
reg [7:0] D;
reg pclk, hsync, vsync;
always @(posedge bus_clk)
begin
// Metastability guards on asynchronous inputs
D_guard <= D_in;
pclk_guard <= pclk_in;
hsync_guard <= hsync_in;
vsync_guard <= vsync_in;
D <= D_guard;
pclk <= pclk_guard;
hsync <= hsync_guard;
vsync <= vsync_guard;
end
wire sample_valid;
reg previous_pclk;
always @(posedge bus_clk)
previous_pclk <= pclk;
assign sample_valid = pclk && !previous_pclk;
// wait_for_frame's purpose is to start getting data from the camera
// at the beginning of a frame.
reg wait_for_frame;
always @(posedge bus_clk)
if (!user_r_read_32_open)
wait_for_frame <= 1;
else if (sample_valid && vsync)
wait_for_frame <= 0;
// fifo_has_been_full changes to '1' when the FIFO becomes full, so
// that the data acquisition stops and an EOF is sent to the host.
// This ensures that the data that arrives to the host is contiguous.
reg fifo_has_been_nonfull, fifo_has_been_full;
wire fifo_full;
always @(posedge bus_clk)
begin
if (!fifo_full)
fifo_has_been_nonfull <= 1;
else if (!user_r_read_32_open)
fifo_has_been_nonfull <= 0;
if (fifo_full && fifo_has_been_nonfull)
fifo_has_been_full <= 1;
else if (!user_r_read_32_open)
fifo_has_been_full <= 0;
end
assign user_r_read_32_eof = fifo_has_been_full && user_r_read_32_empty;
// This part writes pixels from the camera to the FIFO
reg fifo_wr_en;
reg [1:0] byte_position;
reg [31:0] dataword;
always @(posedge bus_clk)
if (wait_for_frame)
begin
byte_position <= 0;
fifo_wr_en <= 0;
end
else if (sample_valid && hsync)
begin
case (byte_position)
0: dataword[7:0] <= D;
1: dataword[15:8] <= D;
2: dataword[23:16] <= D;
3: dataword[31:24] <= D;
endcase
if (byte_position == 3)
fifo_wr_en <= !fifo_has_been_full;
else
fifo_wr_en <= 0;
byte_position <= byte_position + 1;
end
else
fifo_wr_en <= 0;
fifo_32x512 fifo_32
(
.clk(bus_clk),
.srst(!user_r_read_32_open),
.din(dataword),
.wr_en(fifo_wr_en),
.full(fifo_full),
.rd_en(user_r_read_32_rden),
.dout(user_r_read_32_data),
.empty(user_r_read_32_empty)
);
或者,你也可以从这里下载修改后的 xillydemo.v。
完成上述修改后,照常生成比特流(bitstream)文件。下文会详细解释这段 Verilog 代码的工作原理。
连接摄像头模块
可以使用短杜邦线将摄像头模块连接到 Smart Zynq 开发板。导线长度应在 10 cm 或以下,最佳长度为 5 cm。如果导线过长,数字信号质量可能会因串扰而下降。水平同步信号上的过量噪声会导致视频图像跳动,并出现绿色和紫色条纹。
如果导线长度为 10 cm,则可能需要修改一个寄存器来降低 OV7670 的 I/O 驱动电流。本教程的第二部分会演示如何进行此项修改。
下面是将 OV7670 模块连接到 Smart Zynq SP 开发板的照片:
下面是从相反方向拍摄的照片。左上角较小的图片强调了排针的最后一个引脚没有连接任何东西。
上图显示了如何接线:首先在 Smart Zynq 开发板背面找到印有“Bank 33 VCCIO Vadj”的位置。靠近这个标记的那一排引脚就是我们要用的排针,也就是靠近 HDMI 连接器的那排排针。
在摄像头传感器和 Smart Zynq 开发板之间,共有 16 根线连接。只有 3.3V 和 GND 连接到排针上另外的位置。这两根线不需要很短。
注意,排针最后一个引脚是 5V,所以不要把线接到这个引脚上。
下面是摄像头模块与 Smart Zynq 排针之间的接线关系。你也可以从上图中推断出这些信息:
| 排针 | 1 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 35 |
| 模块引脚 | PWDN | D0 | D2 | D4 | D6 | MCLK | HS | SDA | GND |
| 模块引脚 | RST | D1 | D3 | D5 | D7 | PCLK | VS | SCL | 3.3V |
| 排针 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 37 |
再次特别提醒:请留意 3.3V 和 GND 的连接。把这两根线接错可能会损坏摄像头模块。
视频帧采集
使用基于更新后的 xillydemo.v(如上所示)的比特流(bitstream)文件来启动 Xillinux。
下面这条命令(在 shell 提示符下)可以从摄像头输出创建一段短视频:
# cat /dev/xillybus_read_32 > clip.raw
这条命令会运行几秒钟,然后停止。这是因为视频流的数据速率高于 SD 卡的写入速度,导致发生溢出(overflow),最终使数据流停止。下面会解释这种行为背后的机制。
可以用以下命令播放这段视频:
# mplayer -demuxer rawvideo -rawvideo w=640:h=480:format=uyvy:size=614400:fps=31.25 clip.raw
如果在 Xillinux 图形桌面中的终端窗口里运行这条命令,视频就会显示在 Xillinux 的图形界面上。
也可以使用另一个页面中介绍的技术,把视频显示到另一台电脑的屏幕上。例如,如果另一台电脑的 IP 地址是 192.168.1.11,就把命令改成以如下内容开头:
# DISPLAY=192.168.1.11:0 mplayer -demuxer rawvideo ...
如前所述,这段视频的颜色是错误的。下一页会解释如何纠正这个问题。
下面的命令会读取一帧视频,并保存为名为 frame.raw 的文件:
# dd if=/dev/xillybus_read_32 of=frame.raw bs=614400 iflag=fullblock count=1
原始帧的格式是 UYVY 4:2:2。也就是说,每个像素由 16 位组成。第一个字节是第一个像素的 U 分量(即 Cb),下一个字节是同一个像素的 Y 分量;第三、第四个字节分别是第二个像素的 V 和 Y 分量(即 Cr 和 Y)。
可以用下面的命令把这个文件转换成 PNG 文件:
# convert -size 640x480 pal:frame.raw frame.png
有一个简单的图像查看工具:
# display frame.png &
实时显示
为了获得摄像头的实时显示画面,创建一个名为 liveview.sh 的文件,内容如下:
#!/bin/bash
while [ 1 ] ; do
dd if=/dev/xillybus_read_32 bs=614400 iflag=fullblock count=1 2>/dev/null
done | mplayer -demuxer rawvideo -rawvideo w=640:h=480:format=uyvy:size=614400 -
用下面的命令运行该脚本(script):
# bash liveview.sh
为什么需要这个脚本?因为直接从设备文件读取数据再由 mplayer 播放并不可行——mplayer 的速度太慢。换句话说,Zynq 的 ARM 处理器能力不足以按正确的帧率(31.25 fps)播放视频。如果直接这样做,视频只会短暂播放,然后当 FPGA 内部发生溢出(overflow)时,数据流就会停止。
这个脚本建立在一个无限循环之上:每次循环都从 /dev/xillybus_read_32 读取一帧原始图像。这与前面把一帧读入 frame.raw 的命令相同。但这一次没有为 dd 指定输出文件,所以 dd 会把数据写到标准输出。
这个无限循环的输出通过管道重定向到 mplayer 的标准输入(注意循环末尾的“|”)。mplayer 会播放从标准输入到达的视频数据。
这个脚本解决了溢出问题,因为 dd 总是读取完整的视频帧。当 mplayer 暂时无法接收更多数据时,管道中的数据流会暂停。因此,dd 会跳过摄像头传感器输出的一些视频帧。于是,屏幕上显示的画面帧率会低于摄像头本身的帧率。更准确地说,显示出来的帧率是 mplayer 能够显示的最大帧率。
由于 mplayer 自身的缓冲,屏幕上出现的视频会有轻微延迟。要获得低延迟,就需要编写一个简单的程序,在屏幕上显示图像而不增加缓冲。
mplayer 是一个功能强大的媒体播放器。例如,如果不正确的颜色令人困扰,可以把视频片段以黑白方式播放。把下面这部分加到命令中,将饱和度降到零:
# mplayer -saturation -100 -demuxer rawvideo ...
本页实践部分到此结束
本页其余部分解释执行数据采集的逻辑实现。如果你只关心实践内容,可以直接阅读本教程的下一部分。
FPGA 与主机之间的通信
这个例子中的逻辑基于 Xillybus IP 核(IP core)。该 IP 核负责与主机通信。
回顾前文,Verilog 代码中替换掉的部分以如下内容结尾:
fifo_32x512 fifo_32
(
.clk(bus_clk),
.srst(!user_r_read_32_open),
.din(dataword),
.wr_en(fifo_wr_en),
.full(fifo_full),
.rd_en(user_r_read_32_rden),
.dout(user_r_read_32_data),
.empty(user_r_read_32_empty)
);
这是一个标准 FIFO 的实例化(instantiation)。该 FIFO 有三个用于从 FIFO 读取数据的端口:rd_en、dout 和 empty(空)。这些端口连接到 Xillybus IP 核,使 IP 核能够从 FIFO 读取数据并发送到主机。结果是,所有写入 FIFO 的数据都会到达名为 /dev/xillybus_read_32 的设备文件。换句话说,主机上的普通计算机程序可以像普通文件一样打开 /dev/xillybus_read_32。当程序从这个文件读取时,收到的是 FPGA 内应用逻辑写入 FIFO 的数据。
FIFO 还有三个与写入数据相关的端口:wr_en、din 和 full(满)。这些端口连接到从摄像头传感器采集像素数据的逻辑。本页后续部分将解释这部分逻辑的工作原理。目前只需知道:该逻辑借助 @dataword 和 @fifo_wr_en 把像素数据写入 FIFO。此后,由 Xillybus IP 核负责把这些数据送到主机上运行的计算机程序。这就是为什么下面这条命令(前面已经提到)能把数据写入文件:
# cat /dev/xillybus_read_32 > clip.raw
关于 FIFO 工作原理的一般性解释,请参考这个页面。
数据流可以用下面的示意图概括:
本网站有一个关于 Xillybus 的专题页面,其中有一篇讨论数据采集的文章。阅读那一页可能会很有帮助。
注意,user_r_read_32_open 连接到 FIFO 的 srst 端口。当主机打开 /dev/xillybus_read_32 时,这个信号变为高电平。该信号经过一个非门连接到 FIFO,因此当设备文件关闭时,FIFO 处于复位状态。这样可以确保每次设备文件关闭后,FIFO 中的所有数据都会被清除。
与摄像头传感器的接口
下面我们来看上面 Verilog 代码的开头部分:
reg [1:0] clkdiv;
always @(posedge bus_clk)
clkdiv <= clkdiv + 1;
assign J6[10] = clkdiv[1]; // MCLK / XCLK
@bus_clk 的频率是 100 MHz。借助 @clkdiv 对该时钟进行四分频。因此,摄像头模块得到 25 MHz 参考时钟。根据摄像头的数据手册,这是一个允许的频率。不过,该摄像头在参考时钟为 24 MHz 时被设计为产生 30 fps 的视频流,所以实际视频帧率会略高一些:31.25 fps。
应该说,这通常不是产生时钟的正确做法。正确做法是使用 PLL 或类似资源。在这种特定情况下,这样做并没有问题,因为 @clkdiv 只用来产生一个输出信号:FPGA 自身逻辑并不使用这个信号。
Verilog 代码的下一个部分是:
assign J6[0] = 0; // PWDN, the camera is always on
assign J6[1] = !user_w_write_32_open; // RESET#, active low
J6[0] 连接到摄像头模块的 PWDN 引脚。摄像头永远不会被断电。
J6[1] 连接到摄像头的 RESET#。当这个引脚为低电平时,摄像头处于复位状态。@user_w_write_32_open 在主机上的程序打开 /dev/xillybus_write_32 时为高电平。因此在正常情况下,摄像头不会被复位,因为 @user_w_write_32_open 为低电平,于是 J6[1] 为高电平。这样的安排允许我们用下面的命令复位摄像头:
# echo 1 > /dev/xillybus_write_32
这条命令会短暂地打开设备文件,从而实现所需的效果。
到目前为止,我展示了如何产生从 FPGA 到摄像头传感器的信号。下面来看从摄像头传感器到 FPGA 的信号。
OV7670 产生的像素时钟频率与 FPGA 提供的参考时钟相同。也就是说,PCLK 的频率是 25 MHz。在 Verilog 代码中,这个信号连接到 @pclk_in。
摄像头传感器还会产生三个包含视频数据的信号。在 Verilog 代码中,它们的名字是 @D_in、@hsync_in 和 @vsync_in。摄像头传感器会在 @pclk_in 从高到低变化(下降沿)的同时改变这些信号的值。更准确地说,@D_in、@hsync_in 和 @vsync_in 的变化与 @pclk_in 的下降沿对齐。从 FPGA 的角度看,这叫作源同步输入(source synchronous input)。
现在看 Verilog 代码中相关的部分:
wire [7:0] D_in;
wire pclk_in, hsync_in, vsync_in;
assign D_in = J6[9:2];
assign pclk_in = J6[11];
assign hsync_in = J6[12];
assign vsync_in = J6[13];
(* IOB = "TRUE" *) reg [7:0] D_guard;
(* IOB = "TRUE" *) reg pclk_guard, hsync_guard, vsync_guard;
reg [7:0] D;
reg pclk, hsync, vsync;
always @(posedge bus_clk)
begin
// Metastability guards on asynchronous inputs
D_guard <= D_in;
pclk_guard <= pclk_in;
hsync_guard <= hsync_in;
vsync_guard <= vsync_in;
D <= D_guard;
pclk <= pclk_guard;
hsync <= hsync_guard;
vsync <= vsync_guard;
end
注意,摄像头传感器的所有信号都借助 @bus_clk 来采样(sampling)。甚至摄像头的 PCLK 也和其他信号一样被采样。也就是说,PCLK 没有被当作时钟,而是被当作数据信号。我将在下面简单介绍这种技术。
还要注意,@bus_clk 与摄像头传感器信号之间的时序关系是未知的。因此,接收这些信号的触发器输出并不可靠:无法保证这些触发器的时序要求,它们可能会在短时间内变得不稳定。这是与跨时钟域(clock domain crossing)相关的一个已知问题。
这个问题的解决方法在一个单独的页面上有讨论,也就是亚稳态(metastability)保护:把两个触发器串联起来。第一个触发器(例如 @pclk_guard)连接外部信号,第二个触发器连接到第一个。因此,即使第一个触发器在短时间内不稳定,第二个触发器的时序要求也能得到保证,其输出是可靠的。
综上所述:@D、@pclk、@hsync 和 @vsync 是可靠的寄存器(它们与 @bus_clk 同步)。
前面说过,@bus_clk 的频率是 100 MHz,而 PCLK 的频率是 25 MHz。因此,每四个时钟周期才应该使用一次 @D、@hsync 和 @vsync 的值。但在这四个时钟周期中,该用哪一个呢?
答案在 Verilog 代码的这几行中:
wire sample_valid;
reg previous_pclk;
always @(posedge bus_clk)
previous_pclk <= pclk;
assign sample_valid = pclk && !previous_pclk;
这段代码的含义很简单:如果 @pclk 当前为高,而上一个时钟周期为低,那么就可以使用 @D、@hsync 和 @vsync 的值。前面提到,摄像头传感器的信号会在摄像头 PCLK 从高变低时改变。因此,当 PCLK 从低变高时,其他信号是稳定的。
但 @pclk、@D、@hsync 和 @vsync 都是寄存器。这些寄存器与 @bus_clk 同步,它们表示某个时刻摄像头传感器信号的快照。该逻辑并没有检测 PCLK 本身的上升沿,而是对 @pclk 做类似的事情:当 @pclk 的值从低变高时,就是使用其他寄存器值的正确时机。
这种技术称为 01 信号采样(01-signal sampling)。其思路在一个关于 01 信号采样的独立页面上有详细解释。那一页还解释了这种方法如何保证 FPGA 的时序要求。这样一来,逻辑就能确保 @D、@hsync 和 @vsync 中的值是正确的。
启动与停止数据流
下面我们来看两个用于防止向主机传输数据的寄存器:
- @wait_for_frame:这个寄存器的用途是确保设备文件中的数据从一帧的开头开始。
- @fifo_has_been_full:这个寄存器属于一种机制,用于在 FIFO 变满(full)时停止数据流。
下面详细解释这两个寄存器。先看 @wait_for_frame:
reg wait_for_frame;
always @(posedge bus_clk)
if (!user_r_read_32_open)
wait_for_frame <= 1;
else if (sample_valid && vsync)
wait_for_frame <= 0;
@wait_for_frame 在设备文件未打开时为高电平。该寄存器的值会在摄像头传感器的 vsync 信号有效时变为低。vsync 信号在两帧之间的时间间隔内为高电平。换句话说,当 @vsync 为高电平时,摄像头不会发送任何像素数据。
总之,当应该忽略摄像头的像素数据时,@wait_for_frame 为高:设备文件关闭时,或者设备文件刚被打开但摄像头仍处于一帧中间时。
下面接着解释 @fifo_has_been_full:确保主机收到的数据与摄像头传感器产生的数据一致是很重要的。然而,如果计算机程序读取设备文件的速度不够快,FIFO 就可能发生溢出(overflow):DMA 缓冲最终会满,导致没有地方存放 FIFO 的内容,于是 Xillybus IP 核无法从 FIFO 读取数据。发生这种情况时,FIFO 会变满(full),新的数据也无法写入。
对此,逻辑本身无能为力,但它可以确保到达主机的数据是连续的:如果 FIFO 变满,逻辑就停止向 FIFO 写入数据。另外,如果在 FIFO 曾经变满(full)之后又变为 empty(空),逻辑会请求向主机发送 EOF。于是,计算机程序会收到 FIFO 变满之前写入 FIFO 的所有数据。这些数据之后是 EOF(文件结束符),与读取普通文件到达文件末尾时的情况相同。
这种机制可以确保计算机程序相信收到的数据正确且连续。如果连续性无法维持,EOF 会迫使计算机程序关闭设备文件。如果程序再次打开设备文件,数据会借助 @wait_for_frame 从新的一帧开始。
Verilog 代码中相关的部分如下:
reg fifo_has_been_nonfull, fifo_has_been_full;
wire fifo_full;
always @(posedge bus_clk)
begin
if (!fifo_full)
fifo_has_been_nonfull <= 1;
else if (!user_r_read_32_open)
fifo_has_been_nonfull <= 0;
if (fifo_full && fifo_has_been_nonfull)
fifo_has_been_full <= 1;
else if (!user_r_read_32_open)
fifo_has_been_full <= 0;
end
assign user_r_read_32_eof = fifo_has_been_full && user_r_read_32_empty;
@fifo_has_been_full 在 FIFO 曾变满时为高电平。当设备文件未打开时,该寄存器变为低电平。当 @fifo_full 和 @fifo_has_been_nonfull 都为高电平时,@fifo_has_been_full 变为高电平。
@fifo_full 连接到 FIFO 的 full(满)端口。但是为什么需要 @fifo_has_been_nonfull 呢?原因在于,FIFO 在复位期间通常会让 full(满)保持高电平。这个特性的作用是告诉应用逻辑,FIFO 尚未准备好接收数据。@fifo_has_been_nonfull 的作用是防止 @fifo_has_been_full 在这种情形下误变为高电平。
@user_r_read_32_eof 在 @fifo_has_been_full 和 @user_r_read_32_empty 都为高电平时变为高电平。换句话说,当 FIFO 曾经变满(full),而现在已经 empty(空)时,就向主机发送 EOF。注意,在这种情况下,本来也不会再有新数据写入 FIFO。
有一个单独的页面讨论了类似的、确保数据连续性的解决方案。那一页所给出的解决方案适用于 FIFO 两侧属于不同时钟域(clock domain)的情况。在本页展示的代码中,FIFO 只与一个时钟同步,因此 @fifo_has_been_full 的实现也更简单。
把数据写入 FIFO
Verilog 代码的下一部分把像素数据写入 FIFO:
reg fifo_wr_en;
reg [1:0] byte_position;
reg [31:0] dataword;
always @(posedge bus_clk)
if (wait_for_frame)
begin
byte_position <= 0;
fifo_wr_en <= 0;
end
else if (sample_valid && hsync)
begin
case (byte_position)
0: dataword[7:0] <= D;
1: dataword[15:8] <= D;
2: dataword[23:16] <= D;
3: dataword[31:24] <= D;
endcase
if (byte_position == 3)
fifo_wr_en <= !fifo_has_been_full;
else
fifo_wr_en <= 0;
byte_position <= byte_position + 1;
end
else
fifo_wr_en <= 0;
摄像头传感器的像素数据以 8 位为单位到达。这部分逻辑把这些 8 位数据重组成 32 位,以便写入 FIFO。这里没有使用 8 位的 Xillybus 流(/dev/xillybus_write_8),原因有两个:
- 对于数据采集应用,32 位数据流更合适。当数据宽度仅为 8/16 位时,Xillybus IP 核传输数据的效率会比较低。
- /dev/xillybus_write_8 用于与摄像头传感器进行 I2C 通信,这一点将在本教程的下一部分中说明。
当 @wait_for_frame 为高电平时,不会有数据写入 FIFO,原因有两种可能:设备文件没有打开;或者设备文件已打开,但还没有到达新一帧的开头。
当摄像头传感器的 HSYNC 为高电平时,表示数据信号中携带的是有效像素。表达式 sample_valid && hsync 结合了两个条件:当 @sample_valid 为高时,@hsync 和 @D 包含有效值。因此,如果 @hsync 为高,就把 @D 的值复制到 @dataword 的一部分。另外,如果 @D 被复制到 @dataword 的最后一部分(即 @byte_position 等于 3),@fifo_wr_en 会变为高电平,从而把 @dataword 写入 FIFO。更准确地说,@fifo_wr_en 的逻辑表达式是:
fifo_wr_en <= !fifo_has_been_full;
因此,正如前面提到的,如果 @fifo_has_been_full 为高,就不会有数据写入 FIFO。
Verilog 代码与实际引脚的对应关系
上面的 Verilog 代码使用名为 J6 的 inout 端口,但这些连接是如何到达排针的呢?答案可以在 xillydemo.xdc 中找到。这个文件是生成比特流(bitstream)的 Vivado 工程的一部分(位于 vivado-essentials 目录)。
xillydemo.xdc 包含多种使 FPGA 作为电子元器件正常工作所需的信息。除此之外,该文件还包含下面这些行:
[ ... ]
## J6 on board (BANK33 VADJ)
set_property PACKAGE_PIN U22 [get_ports {J6[0]}]; #J6/1 = IO_B33_LN2
set_property PACKAGE_PIN T22 [get_ports {J6[1]}]; #J6/2 = IO_B33_LP2
set_property PACKAGE_PIN W22 [get_ports {J6[2]}]; #J6/3 = IO_B33_LN3
set_property PACKAGE_PIN V22 [get_ports {J6[3]}]; #J6/4 = IO_B33_LP3
set_property PACKAGE_PIN Y21 [get_ports {J6[4]}]; #J6/5 = IO_B33_LN9
set_property PACKAGE_PIN Y20 [get_ports {J6[5]}]; #J6/6 = IO_B33_LP9
set_property PACKAGE_PIN AB22 [get_ports {J6[6]}]; #J6/7 = IO_B33_LN7
set_property PACKAGE_PIN AA22 [get_ports {J6[7]}]; #J6/8 = IO_B33_LP7
[ ... ]
第一行表示,信号 J6[0] 应连接到 U22,这是 FPGA 物理封装上的一个位置。根据 Smart Zynq 的原理图,这个 FPGA 引脚连接排针的第一个引脚。其他端口的位置也以同样方式定义。
结论
本页展示了如何获取 OV7670 的像素数据,并利用 Xillybus IP 核将数据发送到主机。
本教程的下一部分介绍了如何使用 Xillybus IP 核,借助 SCCB(即 I2C)来更改摄像头传感器的寄存器。这对于修改摄像头的参数很有用,尤其是获得颜色正确的图像时所必需的。



