01signal.com

在 Smart Zynq 上从 OV7670 摄像头传感器实现实时显示与视频采集

本页面属于一个小项目系列,这些项目用于探索 Smart Zynq 开发板的各种功能。

这个项目也发布在 HelloFPGA 网站,推荐中文读者阅读。

简介

本教程介绍如何将 OV7670 摄像头模块连接到 Smart Zynq,并在开发板自带的 HDMI 输出上实时显示视频信号。我还会演示如何借助一条简单的 Linux 命令,把原始视频流轻松保存到文件中。

本教程中的信息同样适用于其他类型的数据采集(data acquisition):下面展示的技术既可以配合其他图像数据源使用,也可以配合其他数字数据源使用。

摄像头模块连接到 Zynq 芯片的 PL(FPGA)部分。因此,在把图像数据发送给 ARM 处理器之前,可以很方便地加入实现图像处理的逻辑。由于本教程基于 Xillinux,系统处理部分由完整的 Linux 发行版组成。

选择 OV7670 模块来做这个演示,是因为该硬件便宜、常见且容易买到。另外,该器件产生的数字信号也比较容易理解。

不过,OV7670 有一个令人遗憾的缺点:默认情况下,视频流的颜色是错误的。这是该摄像头传感器一个已知的问题。只需修改这个摄像头的少数几个寄存器,就可以纠正这个问题。因此本教程分为两个部分:

请注意,本教程有很大一部分是在解释实现原理。不过对于实际使用摄像头来说,并不需要理解这些解释。

OV7670 模块

本教程使用的是下图所示的摄像头模块:

The OV7670 camera sensor module used in this project

该模块上,排针的大多数引脚都直接连接到 OV7670 器件。只有 3.3V 和 GND 与电压调节器相连。因此,FPGA 与模块之间的所有连接,都是 FPGA 与 OV7670 器件的直接连接。

市面上还有其他功能相同的模块,使用这些模块可能也没问题。例如,有一种不同的模块,其 PCB 上印着“2017/3/15”,这种模块也能正常工作。另一方面,有一种模块不能正常工作;不能正常工作的模块 PCB 上印着“QYF-OV7670 V3.0”。

另外还要注意,OV7670 器件有不同的版本。可以验证模块上使用的是否是正确的版本。如何验证将在本教程的第二部分中说明。

关于 OV7670,有两个主要信息来源。这两份文档都可以在互联网上找到:

准备 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 开发板的照片:

OV7670 module connected to the Smart Zynq board

下面是从相反方向拍摄的照片。左上角较小的图片强调了排针的最后一个引脚没有连接任何东西。

OV7670 module connected to the Smart Zynq board

上图显示了如何接线:首先在 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 工作原理的一般性解释,请参考这个页面

数据流可以用下面的示意图概括:

Simplified diagram of data acquisition with Xillybus

本网站有一个关于 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:

   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),原因有两个:

当 @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)来更改摄像头传感器的寄存器。这对于修改摄像头的参数很有用,尤其是获得颜色正确的图像时所必需的。

本页由机器从英文翻译而来。如有疑问,请参阅原文
Copyright © 2021-2026. All rights reserved. (dcc38493)