本文根据 Xillinux (xillybus)官方的子网站 www.01signal.com 中的内容转载整理而来,如对原始内容感兴趣的可访问下列链接进行查看 使用 Smart Zynq从 OV7670 camera sensor 进行实时取景和视频捕捉 或者英文原内容 Live view and video capture from an OV7670 camera sensor with Smart Zynq
也感谢xillybus 官方对原始内容的梳理, 本文仅在上述内容中对文中部分翻译内容做了简单微调,以便更好的理解。
备注:本章节内容需要在xilliinux 桌面环境下进行演示。 对于SP、SP2主板可以在HDMI 输出的桌面环境下进行操作,对于不带USB功能的SL主板,则可通过(Xillinux 章节十四 在 Windows 计算机上远程显示并操作Xillinux 桌面及应用) 章节的的方法进入桌面进行操作
介绍
本教程介绍如何将 OV7670摄像头模块连接到 Smart Zynq 并在板子本身的 HDMI输出上查看实时视频信号。我还将展示如何借助简单的 Linux 命令轻松地将 raw video stream (原始视频流)保存到文件中。
本教程中的信息也适用于其他类型的 data acquisition(数据采集) : 下面所演示的技术可用于其他图像数据源以及其他数字数据源。
摄像头模块连接到 Zynq 芯片的 PL (FPGA) 部分。因此,在将 image data(图像数据)发送到 ARM 处理器之前,可以轻松添加实现图像处理的逻辑。由于本教程基于 Xillinux,因此系统的 processor部分包括完整的Linux发行版。
此次演示选择 OV7670 摄像头模块是因为该硬件模块价格便宜、流行且易于购买。此外,该组件生成的数字信号很容易理解。
然而, OV7670 有一个不幸的缺陷: 默认情况下,视频流的颜色是不正确。这是该摄像头传感器的一个已知问题。通过更改该相机的少量寄存器可以纠正这个缺陷。因此,本教程分为两部分:
- 如何将摄像头传感器连接到 Smart Zynq 板,以及如何使用简单的 Linux 工具读取视频流。
- 如何获取具有正确颜色的视频流。本部分展示如何在Xillybus系统下使用I2C的来改变摄像头传感器的寄存器 。
请注意,本教程的大部分内容解释了实现原理。无需为了使用相机而理解这些说明。
OV7670 模块
本教程基于下图所示的摄像头模块:
在这个模块上,大部分引脚排针都直接连接到 OV7670 芯片引脚上。仅 3.3V 和 GND 连接到电源降压部分。因此, FPGA 和模块之间的所有连接都是 FPGA 和 OV7670 组件之间的直接连接。
市场上还有其他具有相同功能的模块。使用这些其他模块可能也可以。但是, OV7670 组件有不同的修订版。可以验证模块上使用的版本是否正确。本教程的第二部分解释了如何执行此操作。
备注,我这边实际测试发现部分网络店铺所售卖的OV7670摄像头模块在此教程下并不能很好的适配。 这部分不适配的OV7670都有一个特征,就是外部仅有一路LDO,如下图所示。 而本文中提到的CMOS 是有两路外部LDO供电的。 大家选型的时候注意区别。
另外大家也可以直接从丝印上进行判断,经过测试可以正常使用的摄像头 1的丝印标注有5110V0的信息, 另一块标注:2017/3/15 的信息 ,这两块摄像头在电商平台上也很容易购买的到,至于其他的OV7670大家可以从上文中介绍的LDO的数量来进行判断。
有关 OV7670的信息主要有两个来源。这两个文档可以在网上找到:
- 数据手册: OV7670/OV7171 CMOS VGA (640×480) CAMERA CHIP Sensor with OmniPixel Technology, Version 1.4, August 21, 2006 。获取该文档的 1.4 版本非常重要。
- 实施指南: OV7670/OV7171 CMOS VGA (640×480) CameraChip Implementation Guide 。显然,网上只能找到 Version 1.0, September 2, 2005 。不幸的是,这个版本是依据较早版本的摄像头芯片编写的。所以该本本在现在已经过时了。
准备Vivado项目
从演示包的zip文件(启动分区套件)中创建一个新的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) );
或者,您可以从此处 (此链接来自github,需要科学上网)下载此更改后的 xillydemo.v 。如你当前无科学上网条件,也可以从本站链接进行下载,记得下载后解压缩得到xillydemo.v文件
进行此更改后(按上文介绍的插入PART 2部分代码,或者替换完整xillydemo.v文件)之后按照以往一样创建 bitstream 文件。此 Verilog 代码的工作原理在本页下面详细解释。(这里我们可以将生成的bit文件通过TF读卡器替换TF卡上的文件,或者用Xillinux 章节十二 在Xillinux 系统上搭建 CIFS服务(samba),实现与Windows 文件共享 中介绍的方法通过网络替换TF卡中的文件(前提是TF card 已挂在到mnt/card,方法可以参考Xillinux 章节十三 在Xillinux 系统中自动挂载(mount) TF卡分区),替换后记得用reboot命令进行重启(SSH,串口,或者xillinux桌面环境的命令行都可以))
连接摄像头模块
有两种方式可以连接摄像头模块:
- 用杜邦线的方式(01signal中介绍的方式,本文中下文介绍的内容都会以该种方式进行连接)
- 用本网站的摄像头转接板来连接(引脚顺序和01signal中介绍的杜邦线序完全一致,可以在下列页面的附件中找到转接板图纸 ADB_ZD02-正点原子模块转接板(可连接摄像头及AD、DA等模块)附gerber文件可以直接打板)(用转接板连接摄像头 如下图所示)
短杜邦线可用于连接摄像头模块和 Smart Zynq 板。杜邦线长度应控制在10 cm 或更短。最佳长度是 5 cm。如果线较长, 数字信号的质量可能会受到串扰噪声的影响。 水平同步信号上噪声过大也会导致视频图像跳动,并出现绿色和紫色条纹。
如果电线长度为 10 cm,则可能需要更改一个寄存器以减少 OV7670的I/O驱动器电流。本教程的第二部分展示了如何进行此更改。
这是 OV7670 模块连接到 Smart Zynq SP 板的图片:
以下是从相反方向拍摄的照片。左上方的较小图像强调了引脚排针的最后一个引脚未连接到任何东西。(这里接的是排针的第二( 3.3V )第三个引脚 (GND),这里不要接错 )
上图显示了如何连接杜邦线: 首先,查找 Smart Zynq 板背面写有“Bank 33 VCCIO Vadj”的地方。引脚排针是我们要使用的引脚排针 。这是靠近 HDMI 连接器的引脚排针。
相机传感器和 Smart Zynq 板之间并联有 16 根电线。只有3.3V和GND连接到引脚排针的不同位置。这两根线不必很短。
请注意,引脚排针的最后一个引脚是5V,不要连接电线到这个引脚。
这是相机模块和 Smart Zynq排针引脚号之间的对应规则。这些信息也可以从上图中推断出来:
Pin header | 1 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 35 |
Module pin | PWDN | D0 | D2 | D4 | D6 | MCLK | HS | SDA | GND |
Module pin | RST | D1 | D3 | D5 | D7 | PCLK | VS | SCL | 3.3V |
Pin header | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 16 | 37 |
再次强调,请特别注意 3.3V 和 GND的连接。这两根电线的错误连接可能会损坏摄像头模块。
Frame grabbing (截取帧)
使用基于更新的 xillydemo.v 的 bitstream 文件启动 Xillinux (如上所示)。
在shell提示符下,执行以下命令可以从摄像头的输出创建一个短视频剪辑:
cat /dev/xillybus_read_32 > clip.raw
此命令运行几秒钟,然后停止。这是因为视频流的数据速率高于SD卡的写入数据速度。这会导致溢出,停止数据流动。本文后面将解释这种行为背后的机制。
可以使用以下命令播放刚才录制的视频内容:(备注,此处需要登录Xillinux的图形桌面)
mplayer -demuxer rawvideo -rawvideo w=640:h=480:format=uyvy:size=614400:fps=31.25 clip.raw
如果在 Xillinux图形桌面内的终端窗口中(terminal window) 中使用此命令,则视频将在 Xillinux图形界面上播放。
还可以使用之前描述的技术(Xillinux 章节十四 在 Windows 计算机上远程显示并操作Xillinux 桌面及应用)在不同计算机的屏幕上播放视频。例如,如果另一台计算机的 IP address 是 192.168.1.11,请更改命令,使其开头为:
DISPLAY=192.168.1.11:0 mplayer -demuxer rawvideo
如前文所述,视频的颜色并不正确,下一节将解释如何纠正此问题。(左图为OV7670采集到的画面,右边为实际应该显示的颜色)
此命令将一个视频帧读入名为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 &
Live view 实时视图显示
为了获取摄像头的实时图像,创建一个名为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 -
使用以下命令运行此脚本:
bash liveview.sh
备注: 这里如果在windows的记事本中编辑,然后再传输到xillinux板子上,会造成bash 运行无法工作的情况,和windows 编辑的换行符与 linux 是不同的有关。 所以liveview的编辑操作请务必在linux 系统下完成(也可以在xillinux 下完成,vi 或者xillinux 的图形文本编辑器也可以,直接从windows 复制再粘贴也不行,尽量手打)。为了方便大家操作,这里也将我编辑好的liveview.sh进行上传,大家可以直接下载。 liveview.sh (记得解压缩)
为什么需要这个脚本?直接从设备文件读取数据是不可能的,因为 mplayer 太慢了。换句话说, Zynq的 ARM处理器不够强大,无法以正确的帧率(31.25 fps)播放视频。如果您尝试这样做,视频只会短暂播放。当 FPGA内部有发生溢出时,数据流就会停止。
此脚本基于一个无限循环,每次从/dev/xillybus_read_32读取一个原始帧。这与之前用于将一帧读入 frame.raw的命令相同。但这一次,没有为 dd定义输出文件。因此, dd将数据写入标准输出而不是文件。
这个死循环的结果借助一个 pipe (通道)重定向到 mplayer的标准输入(注意循环末尾的“|”)。 mplayer 播放从标准输入接收的视频数据。
这个脚本解决了数据溢出的问题,因为 dd 总是读取完整的视频帧。当 mplayer 没有准备好接收更多数据时,通道(pipe)上的数据流会暂时停止。这样dd 就从相机传感器上跳过了视频帧的。因此,在屏幕上显示的帧率会低于摄像头的帧率。更准确地说,显示的帧速率是 mplayer 能够显示的最大帧速率。
由于 mplayer自带缓冲导致屏幕上出现的视频图像略有延迟。为了获得低延迟,需要编写一个简单的程序,在不添加缓冲的情况下将图像显示在屏幕上。
上述命令除了增加到脚本中,也可以直接在命令行中输入完整的命令运行,如下图所示
mplayer是一个功能强大的媒体播放器。例如,如果不正确的颜色很烦人,可以将视频剪辑播放为黑白视频。在命令中添加以下部分以将饱和度减少到零:
mplayer -saturation -100 -demuxer rawvideo ...
本页实践部分结束
本页的其余部分解释了数据采集部分的逻辑的实现。如果您只对实践部分的话题感兴趣,请继续阅读本教程的下一部分。
FPGA 与 host(主机)之间的通信
本示例中的逻辑基于 Xillybus IP核。该 IP核负责与 host(主机)的通信。
回想一下上面 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的实例化 。该 FIFO 有 3 个 端口 ,用于从 FIFO读取数据: rd_en、 dout 和 empty。这些端口连接到 Xillybus IP核。这使得 IP核可以从 FIFO 读取数据并将该数据发送到 host主机。结果是写入 FIFO 的所有内容都到达名为 /dev/xillybus_read_32的设备文件 。换句话说, host主机上的普通计算机程序可以将 /dev/xillybus_read_32 作为常规文件打开。当程序读取该文件时,它会接收到 FPGA内部的应用逻辑已写入 FIFO 的数据。
FIFO 有 3 个用于写入数据的端口 : wr_en、 din 和 full。这些端口连接到从相机传感器收集像素数据的逻辑 。本页的下一部分解释了逻辑的工作原理。现在我只是指出逻辑在 @dataword 和 @fifo_wr_en的帮助下将像素数据写入 FIFO 。从此时起, Xillybus IP 核的作用就是将这些数据传送到 host主机上运行的计算机程序种。这就是为什么这个命令(在上面已经提到过)能够将这些数据写入文件的原因:
cat /dev/xillybus_read_32 > clip.raw
有关 FIFO 工作原理的一般说明,请参阅此页面 (此页面来自01signal 官方网站)。
数据流可以用下图总结:
该网站上有一个关于 Xillybus的部分,该部分有一个讨论 data acquisition(数据采集)的页面。通读该页面可能会有所帮助(此页面来自01signal 官方网站)。
请注意, user_r_read_32_open 连接到 FIFO的 srst端口。当 /dev/xillybus_read_32 在 host主机上打开时,这个信号变为高电平。 该信号与一个非门连接,因此当设备文件关闭时, 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。这个 clock 在 @clkdiv的帮助实现四分频。因此,相机模块接收 25 MHz参考时钟。根据相机的数据手册,这是一个允许的频率。根据手册上可以看出,当参考时钟的频率为 24 MHz时,CMOS产生的视频流是30fps的。因此,我们在25M频率下实际生成的视频帧速率会略高于24M下的30fps,实际是 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引脚。当这个引脚为低电平时,相机被复位。当 host上的程序打开 /dev/xillybus_write_32 时, @user_w_write_32_open 为高电平。所以通常情况下,相机不会复位,因为 @user_w_write_32_open 为低电平,因此 J6[1] 为高电平。这种安排允许使用以下命令对相机进行复位:
echo 1 > /dev/xillybus_write_32
该命令会短暂打开 device file ,从而达到预期的结果。
到目前为止,本文已经展示了从 FPGA 到相机传感器的信号是如何生成的。现在介绍从相机传感器到 FPGA的信号。
OV7670 从 FPGA生成与 reference clock(参考时钟)频率相同的 pixel clock(像素时钟) 。也就是说, 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进行采样的。甚至相机的 PCLK 也以与其他信号相同的方式进行采样。换句话说, PCLK 不被视为时钟,而是被视为数据信号处理。下面我将简要解释该技术。
另请注意, @bus_clk 和相机传感器信号之间的时序关系是未知的。因此,接收这些信号的触发器的输出并不可靠: 无法确保这些触发器的时序要求 ,因此它们可能会在短时间内变得不稳定。这是与跨时钟域相关的已知问题。
该问题的解决方案在单独的页面(该页面来自01signal)上讨论: Metastability guards。这意味着有两个触发器相互串联。第一个触发器(例如@pclk_guard)连接到外部信号。第二个触发器连接到第一个触发器。因此,即使第一个触发器出现短暂不稳定,第二个触发器的时序要求也能得到保证。因此第二个触发器的输出是可靠的。
综上所述: @D、 @pclk、 @hsync 和 @vsync 是可靠的寄存器 (与 @bus_clk同步)。
回想一下, @bus_clk的频率是 100 MHz。另一方面, PCLK的频率是 25 MHz。因此,每四个 时钟循环中, @D、 @hsync 和 @vsync 的值应仅消耗一次。但这四个 clock cycle 中的哪一个呢?
答案在 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-signal sampling。该技术背后的想法在有关 01-signal sampling的单独页面(页面来自01signal)上进行了详细解释。该页面还解释了此方法如何保证 FPGA的时序要求。通过这样做,逻辑可确保 @D、 @hsync 和 @vsync 中的值正确。
启动和停止数据流
现在我们来看看两个寄存器 ,它们旨在阻止向 host传输数据:
- @wait_for_frame: 这个寄存器的目的是保证设备文件 (device file) 中的数据是从帧头的开头开始的。
- @fifo_has_been_full: 该寄存器是确保在 FIFO 变满时停止数据流的机制的一部分。
我现在将详细讨论这两个寄存器。一、 @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;
当设备文件(device file) 未打开时,@wait_for_frame的值为高电平。当相机传感器的 vsync 信号响应时,该寄存器的值会变为低电平。该信号在帧与帧之间的时间段内为高电平。换句话说,当 @vsync 为高电平时,相机不传输任何像素数据。
综上所述,当忽略相机的像素数据时, @wait_for_frame 为高代表: 当前设备文件处于关闭状态,或者设备文件是打开状态,但相机正处于帧与帧之间。
现在我将继续解释 @fifo_has_been_full: 确保到达 host 的数据与相机传感器产生的数据相同非常重要。但是,如果计算机程序没有足够快地从设备文件(device file)读取数据,则 FIFO 中可能会出现数据溢出 : DMA缓冲器最终会变满,因此将没有地方可以复制 FIFO 的内容。因此, Xillybus IP核将无法从 FIFO读取数据。当这种情况发生时, FIFO 就会变满,从而无法向其写入新数据。
还有逻辑也无法阻止这种情况。然而,逻辑可以保证到达 host 的数据是连续的: 如果 FIFO 已满,逻辑将停止向 FIFO写入数据。另外,当 FIFO 已满后 FIFO 变空时,逻辑请求向 host发送 EOF信号 。因此,计算机程序接收在 FIFO 变满之前写入 FIFO 的所有数据。在这些数据之后,计算机收到 EOF。这与到达常规文件末尾时发生的情况相同。
这种机制确保计算机程序可以相信到达的数据是正确且连续的。如果连续性丢失, EOF 会强制计算机程序关闭设备文件( device file)。如果程序再次打开设备文件 ,数据将从新的帧开始,这要归功于 @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 已满时,@fifo_has_been_full 为高电平。当 设备文件未打开时,该寄存器变为低电平。当 @fifo_full 和 @fifo_has_been_nonfull 都为高电平时, @fifo_has_been_full 变为高电平。
@fifo_full 连接到 FIFO的 “full” port。但为什么需要 @fifo_has_been_nonfull ?原因是,只要 FIFO 保持在复位状态时, FIFO 往往会保持“full”信号为高电平。该功能的目的是告诉应用逻辑 ,FIFO 尚未准备好接收数据。 @fifo_has_been_nonfull 的目的是防止 @fifo_has_been_full 在这种情况下错误地变高。
当 @fifo_has_been_full 和 @user_r_read_32_empty 都为高电平时,@user_r_read_32_eof 变为高电平。换句话说,当 FIFO 过去已满、现在为空时,会向主机发送EOF信号。请注意,在这种情况下,无论如何都不会向 FIFO 写入新数据。
有一个单独的页面(页面来自01signal)讨论了确保数据连续性的类似解决方案。当 FIFO 的两侧属于不同的时钟域时,需要用到该页面上提供的解决方案。在本页提供的代码中, FIFO 仅与一个时钟同步。因此,本页中 @fifo_has_been_full 的实现更加简单。。
写入数据到 FIFO
将图像像素数据写入FIFO的VERILOG代码如下:
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 位宽的数据元素到达。 这部分逻辑将这些数据元素重新组织成32位,以便可以将数据写入 FIFO。 这里不使用8位的 Xillybus数据流(/dev/xillybus_write_8)去传输内容是因为下面两个原因:
- 对于数据采集的应用, 32-bit数据流更合适。当数据字宽度仅为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 代码与真实 pins的对应关系
上面的 Verilog 代码使用了名为 J6的 inout port ,但是与这个端口的连接如何到达排针接插件的呢?答案可以在 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 pin 连接到排针的第一个引脚上 。另一个端口的位置也以同样的方式定义。
结论
本页介绍了如何从 OV7670 获取像素数据并使用 Xillybus IP核将此数据发送到 host 。
本教程的下一部分将介绍如何使用 Xillybus IP 核在 SCCB (即 I2C)的帮助下更改相机传感器的寄存器 。这对于更改相机的参数很有用。特别是,这对于获得具有正确颜色的图像是必要的。