1、概述
本文主要是为了验证之前设计的以太网发送模块,确保之前的设计没有问题,或者找到并修改存在的问题。
工程的系统框图如下所示,主要包含OV7725初始化模块、像素数据封装处理模块、FIFO、以太网模块、锁相环模块。
OV7725最多只能输出640*480的60帧图像数据,即每秒传输640*480*60*16bit=294912000bit=281Mbit数据,千兆以太网即使存在帧头、帧间隙、校验码等数据,传输速率也远大于281Mbit,所以不需要添加DDR3等外部存储器存储数据,只需要一个FIFO暂存小部分数据即可。
本次使用原子的上位机对以太网接收的数据进行显示,实测当上位机点击打开按钮后,上位机会通过以太网向FPGA发送一个长度为1的数据报,报文数据为8’h31,当FPGA接收到数据后,即可向上位机传输数据。
上位机对传输的数据有格式要求,一帧数据的开始需要传输固定长度为4字节的帧头数据32’h,然后需要传输一帧图像的水平像素和垂直像素个数。上位机才能够在接收数据后正确显示图像。
所以每帧图像的开始需要多传输8字节数据,一般规定每次传输一行数据,由于OV7725一行有640个像素,每个像素16位,而以太网每个时钟传输8位数据,因此需要1280个时钟才能传输完一次数据。第一行需要1288个时钟才能全部传输。
按理说FIFO的深度设置为2048即可,因为有时候上位机可能会通过ARP获取FPGA的MAC地址,导致FIFO中的数据不能及时被读取发送,所以把FIFO的深度设置得稍微大一点,毕竟2048深度能够充分利用构成FIFO的RAM地址线吧。
整体设计思路是当以太网接收到上位机发送的8’h31数据后,当检测到场同步信号的上升沿之后,当FIFO中数据个数大于等于一次传输的数据个数时,且以太网发送模块处于空闲,将UDP发送使能信号拉高,之后读取FIFO中的数据进行发送。
OV7725摄像头初始化和以太网发送模块在前文均已经做过详细讲解,本文需要着重设计的其实只有摄像头的数据封装模块。
2、图像封装处理模块
上位机是原子开发的,据说点击关闭后会向开发板发送8’h30,但是实测点击关闭后并没有向开发板发出指令,本处就默认会发结束传输的指令吧。
上位机通过以太网向开发板发送8’h31后,FPGA开始通过以太网向上位机传输以太网数据,开发板接收到上位机发送的8’h30后,FPGA停止向上位机传输图片数据。
上位机在检测到4字节的帧头数据后,才会接收数据并显示图像。上位机还要知道需要显示图像尺寸,因此在传输四字节的帧头后,需要传输2字节的水平像素个数和2字节的垂直像素点个数。
下面通过代码讲解具体设计思路,首先当FPGA接收到UDP数据报文长度为1,如果数据为8’h31,则将发送数据标志信号拉高,如果数据为8’h30,则将发送数据标志信号拉低,其余时间保持不变。由于以太网发送时钟和以太网接收时钟在FPGA内部是同一个时钟,因此后文以太网发送时钟可以直接使用该信号,不属于异步信号。
//解析接收以太网传输的指令数据,其实以太网发送时钟和接收端的时钟时同一个时钟;
always@(posedge gmii_rx_clk)begin
if(rst_n==1'b0)begin//初始值为0;
transfer_flag <= 1'b0;
end
else if(udp_rx_data_vld && (udp_rx_data_num == 16'd1))begin
if(udp_rx_data == 8'h31)//开始传输;
transfer_flag <= 1'b1;
else if(udp_rx_data == 8'h30)//停止传输;
transfer_flag <= 1'b0;
end
end
首先考虑FIFO的复位,因为xilinx的FIFO复位需要多个时钟周期,因此使用了一个计数器,将复位脉冲拉高多个时钟周期,调节计数器的位宽就可更改复位脉冲长度。
当上位机不接收数据时,FIFO一直处于复位状态。每次检测到场同步信号的上升沿,也会对FIFO进行一次复位,清除FIFO中残留数据,保证上次传输过程可能出现的错误不会影响下次传输,对应代码如下:
//后文主要思路:首先为了确保每帧数据的正确显示,当检测到场同步信号的上升沿时,表示后面就是下一帧图像数据了,此时把FIFO复位。
//xilinx的FIFO复位一般需要多个时钟周期,当FIFO复位完成之后,就需要把帧头和水平垂直的像素个数数据写入FIFO中;
//过段时间就会出现图像数据,就把图像数据写入FIFO中。
//读FIFO数据时需要注意,第一行数据包含帧头等信息,会多8字节数据,从第二行开始就是正常的数据个数。
//把场同步信号打两拍,用于检测场同步信号上升沿;
always@(posedge cam_pclk)begin
cam_vsync_r <= {cam_vsync_r[0],cam_vsync};
fifo_wrrst_busy_r <= fifo_wrrst_busy;
end
assign cam_vsync_pos = cam_vsync_r[0] & (~cam_vsync_r[1]);//检测场同步信号上升沿;
assign fifo_wrrst_busy_neg = fifo_wrrst_busy_r & (~fifo_wrrst_busy);//检测FIFO复位完成信号的下降沿;
//因为xilinx FIFO复位需要持续多个时钟周期才能有效,所以需要一个计数器来辅助复位;
always@(posedge cam_pclk)begin
if(rst_n==1'b0)begin//初始值为0;
vsync_rst <= 1'b0;
end
else if(&rst_cnt)begin//复位计数器所有位均为高电平时拉低复位信号;
vsync_rst <= 1'b0;
end//当检测到场同步信号上升沿时把FIFO复位信号拉高;
else if(cam_vsync_pos)begin
vsync_rst <= 1'b1;
end
end
//复位计数器,对复位脉冲进行计数,改变该计数器位宽即可更改复位脉冲持续时间;
always@(posedge cam_pclk)begin
if(rst_n==1'b0)begin//初始值为0;
rst_cnt <= 3'd0;
end//对复位脉冲宽度进行计数。
else if(vsync_rst)begin
rst_cnt <= rst_cnt + 1;
end
end
assign fifo_rst = (~transfer_flag) || vsync_rst;//FIFO复位信号;
当FIFO复位完成之后,就将4字节帧头数据、2字节水平和垂直像素数据写入FIFO中。因此需要一个标志信号和一个计数器,对应代码如下。为了保证确保数据正确,标志信号和计数器增加了清零的逻辑。
//写入帧头数据标志信号,初始值为0,当FIFO复位或者写入全部帧头后清零,当FIFO复位完成后拉高;
always@(posedge cam_pclk)begin
if(rst_n==1'b0)begin//初始值为0;
head_flag <= 1'b0;
end//当FIFO复位或者写入全部帧头后清零;
else if(vsync_rst || (&head_cnt))begin
head_flag <= 1'b0;
end
else if(fifo_wrrst_busy_neg)begin//FIFO复位完成,开始向FIFO中写入帧头数据;
head_flag <= 1'b1;
end
end
//帧头计数器,对帧头标志信号进行计数,因为需要8个数据,所以计数到7之后可以通过溢出清零;
always@(posedge cam_pclk)begin
if(rst_n==1'b0)begin//初始值为0;
head_cnt <= 3'd0;
end
else if(fifo_rst)begin//FIFO复位的时候将帧头计数器清零;
head_cnt <= 3'd0;
end
else if(head_flag)begin
head_cnt <= head_cnt + 1;
end
end
//FIFO写使能信号。
always@(posedge cam_pclk)begin
if(rst_n==1'b0)begin//初始值为0;
fifo_wr_en <= 1'b0;
end//当帧头写入标志有效或者输入有效数据时拉高,其余时间均为低电平;
else begin
fifo_wr_en <= (head_flag || cam_href) && (~fifo_wrrst_busy);
end
end
//FIFO写数据信号,向FIFO中写入有效数据;
always@(posedge cam_pclk)begin
if(rst_n==1'b0)begin//初始值为0;
fifo_wdata <= 8'd0;
end
else if(head_flag)begin
case (head_cnt)//在输出帧头数据时,根据计数器的值输出对应的数据;
3'd0 : fifo_wdata <= IMG_FRAME_HEAD[31:24];//帧头;
3'd1 : fifo_wdata <= IMG_FRAME_HEAD[23:16];//帧头;
3'd2 : fifo_wdata <= IMG_FRAME_HEAD[15: 8];//帧头;
3'd3 : fifo_wdata <= IMG_FRAME_HEAD[ 7: 0];//帧头;
3'd4 : fifo_wdata <= {6'd0,CMOS_H_PIXEL[9: 8]};//水平方向分辨率;
3'd5 : fifo_wdata <= CMOS_H_PIXEL[7: 0];//水平方向分辨率;
3'd6 : fifo_wdata <= {7'd0,CMOS_V_PIXEL[8]};//垂直方向分辨率;
3'd7 : fifo_wdata <= CMOS_V_PIXEL[7: 0];//垂直方向分辨率;
default : ;
endcase
end//像素数据有效时,将像素数据输出;
else if(cam_href)begin
fifo_wdata <= cam_data;
end
end
之后就是将接收的摄像头数据写入FIFO中,当写入帧头标志信号或者输入图像数据有效且FIFO不处于空闲状态时,写使能拉高。写数据根据帧头计数器写入对应帧头数据,否则如果输入像素数据有效,则将对应数据写入FIFO。
之后再来查看FIFO读侧逻辑,由于每帧数据开头需要多传输8字节数据,所以也需要检测一帧的开始。因此把场同步信号同步到千兆网发送时钟域下,并且检测其上升沿。
//把场同步信号同步到以太网发送时钟域下,然后检测其上升沿,所以需要将场同步信号延迟三个时钟周期。
//延迟的前两个时钟周期用于同步,后一个时钟周期用于检测上升沿;
always@(posedge gmii_tx_clk)begin
cam_vsync_txc_r <= {cam_vsync_txc_r[1:0],cam_vsync};
end
//在以太网发送时钟域下检测cam_vsync信号上升沿;
assign cam_vsync_txc_pos = cam_vsync_txc_r[1] & (~cam_vsync_txc_r[2]);
如果检测到场同步信号上升沿,表示一帧图像传输的开始,那么需要发送的数据个数为水平像素点*2+8,乘2是因为一个水平像素点包含16位数据,而千兆网一个时钟只能发送8位。当检测到以太网发送使能信号有效时,表示已经发送过一次数据了,即帧头被发送,那么后续发送的数据就不包含帧头,会少8字节数据。
//以太网每次发送数据的报文长度,单位字节。
always@(posedge gmii_tx_clk)begin
if(rst_n==1'b0)begin//初始值为0;
udp_tx_data_num <= {CMOS_H_PIXEL,1'b0};
end
else if(cam_vsync_txc_pos)begin//第一行数据需要多传输8个字节的帧头数据;
udp_tx_data_num <= {CMOS_H_PIXEL,1'b0} + 16'd8;
end
else if(udp_tx_en)//其余行正常传输数据,由于像素点为16位数据,以太网每次传输8位数据,所以实际发送数据是水平像素点的2倍;
udp_tx_data_num <= {CMOS_H_PIXEL,1'b0};
end
最后是以太网发送使能信号,当以太网发送模块处于空闲且FIFO不处于复位状态且FIFO中的数据个数大于一次传输的数据个数且发送标志信号有效时才能进行发送。
//生成UDP发送使能信号;
always@(posedge gmii_tx_clk)begin
if(rst_n==1'b0)begin//初始值为0;
udp_tx_en <= 1'b0;
end
else begin//当UDP发送模块处于空闲且FIFO中的数据大于一次发送所需数据且FIFO不处于复位状态,则将使能信号拉高,其余时间使能信号均为低电平;
udp_tx_en <= (udp_tx_rdy && (fifo_rdusedw >= udp_tx_data_num) && (~fifo_rdrst_busy) && transfer_flag);
end
end
3、顶层模块
顶层模块跟以前一样,主要就是对各个模块的引脚进行连接,对应的RTL视图如下所示。
注意power_en信号只是控制模块开关电源工作的信号,只与固定模块有关,不使用该模块可以不考虑。
参考代码如下:
assign power_en = 1'b1;//使能模块电源,仅对此模块有用;
//例化锁相环,输出200MHZ时钟,作为以太网的参考时钟;
//还需要生成一路频率为12MHz的时钟信号,驱动摄像头工作;
clk_wiz_0 u_clk_wiz_0(
.clk_out1 ( clk_200m ),//output clk_out1
.clk_out2 ( cam_xclk ),//output clk_out2
.resetn ( rst_n ),//input resetn
.locked ( sys_rst_n ),//output locked
.clk_in1 ( clk ) //input clk_in1
);
//例化OV7725摄像头采集模块;
ov7725_top #(
.CMOS_H_PIXEL ( CMOS_H_PIXEL ),//图像水平方向分辨率;
.CMOS_V_PIXEL ( CMOS_V_PIXEL ) //图像垂直方向分辨率;
)
u_ov7725_top (
.clk ( clk ),//系统时钟信号,100MHz;
.rst_n ( sys_rst_n ),//系统复位信号,低电平有效;
.init_done ( init_done ),//初始化完成信号;
.scl ( scl ),//SCCB串行时钟信号;
.sda ( sda ) //SCCB双向串行数据信号;
);
//例化摄像头数据处理模块;
cam_data_treat #(
.CMOS_H_PIXEL ( CMOS_H_PIXEL ),//图像水平方向分辨率;
.CMOS_V_PIXEL ( CMOS_V_PIXEL ) //图像垂直方向分辨率;
)
u_cam_data_treat (
.rst_n ( rst_n ),//复位信号,低电平有效;
//摄像头相关信号;
.cam_pclk ( cam_pclk ),//摄像头数据像素时钟;
.cam_vsync ( cam_vsync ),//摄像头场同步信号;
.cam_href ( cam_href ),//摄像头行同步信号;
.cam_data ( cam_data ),//摄像头输入数据信号;
//以太网接收相关信号;
.gmii_rx_clk ( gmii_rx_clk ),//以太网接收时钟信号;
.udp_rx_done ( udp_rx_done ),//udp数据报接收完成信号;
.udp_rx_data ( udp_rx_data ),//udp数据接收的数据;
.udp_rx_data_num ( udp_rx_data_num ),//udp接收一帧数据的长度;
.udp_rx_data_vld ( udp_rx_data_vld ),//udp接收数据有效指示信号;
//以太网发送相关信号;
.gmii_tx_clk ( gmii_tx_clk ),//以太网发送时钟;
.udp_tx_rdy ( udp_tx_rdy ),//以太网发送模块空闲指示信号,高电平有效;
.udp_tx_en ( udp_tx_en ),//UDP发送使能信号。
.udp_tx_data_num ( udp_tx_data_num ),//udp一帧数据需要发送的个数;
//FIFO写数据端口信号;
.fifo_wrrst_busy ( fifo_wrrst_busy ),//FIFO复位完成指示信号;
.fifo_rdrst_busy ( fifo_rdrst_busy ),//FIFO复位完成指示信号;
.fifo_rdusedw ( fifo_rdusedw ),//FIFO读侧看到的数据个数;
.fifo_rst ( fifo_rst ),//FIFO复位信号,高电平有效;
.fifo_wr_en ( fifo_wr_en ),//FIFO写使能信号;
.fifo_wdata ( fifo_wdata ) //FIFO写数据信号;
);
//例化存储摄像头数据的FIFO模块,深度2048个字节;
tx_fifo u_tx_fifo (
.rst ( fifo_rst ),//input wire rst;
.wr_clk ( cam_pclk ),//input wire wr_clk;
.din ( fifo_wdata ),//input wire [7 : 0] din;
.wr_en ( fifo_wr_en ),//input wire wr_en;
.rd_clk ( gmii_tx_clk ),//input wire rd_clk;
.rd_en ( udp_tx_req ),//input wire rd_en;
.dout ( udp_tx_data ),//output wire [7 : 0] dout;
.full ( ),//output wire full;
.empty ( ),//output wire empty;
.rd_data_count ( fifo_rdusedw ),//output wire [10 : 0] rd_data_count;
.wr_rst_busy ( fifo_wrrst_busy ),//output wire wr_rst_busy;
.rd_rst_busy ( fifo_rdrst_busy ) //output wire rd_rst_busy;
);
//例化以太网发送和接收模块;
eth #(
.BOARD_MAC ( BOARD_MAC ),//开发板MAC地址 00-11-22-33-44-55
.BOARD_IP ( BOARD_IP ),//开发板IP地址 192.168.1.10;
.DES_MAC ( DES_MAC ),//目的MAC地址 ff_ff_ff_ff_ff_ff;
.DES_IP ( DES_IP ),//目的IP地址 192.168.1.102;
.BOARD_PORT ( BOARD_PORT ),//开发板的UDP端口号;
.DES_PORT ( DES_PORT ) //目的端口号;
)
u_eth (
//GMII接口;
.rst_n ( sys_rst_n ),//复位信号,低电平有效。
.gmii_rx_clk ( gmii_rx_clk ),//GMII接收数据时钟。
.gmii_rx_dv ( gmii_rx_dv ),//GMII输入数据有效信号。
.gmii_rxd ( gmii_rxd ),//GMII输入数据。
.gmii_tx_clk ( gmii_tx_clk ),//GMII发送数据时钟。
.gmii_tx_en ( gmii_tx_en ),//GMII输出数据有效信号。
.gmii_txd ( gmii_txd ),//GMII输出数据。
//用户接口;
.arp_req ( 1'b0 ),//arp请求数据报发送信号。
.udp_tx_en ( udp_tx_en ),//UDP发送使能信号。
.udp_tx_data ( udp_tx_data ),//udp需要发送的数据信号,滞后tx_req信号一个时钟;
.udp_tx_data_num ( udp_tx_data_num ),//udp一帧数据需要发送的个数;
.udp_tx_req ( udp_tx_req ),//请求输入udp发送数据;
.udp_rx_done ( udp_rx_done ),//udp数据报接收完成信号;
.udp_rx_data ( udp_rx_data ),//udp数据接收的数据;
.udp_rx_data_num ( udp_rx_data_num ),//udp接收一帧数据的长度;
.udp_rx_data_vld ( udp_rx_data_vld ),//udp接收数据有效指示信号;
.udp_tx_rdy ( udp_tx_rdy ) //以太网发送模块忙闲指示信号;
);
//例化gmii转RGMII模块。
rgmii_to_gmii u_rgmii_to_gmii (
.idelay_clk ( clk_200m ),//IDELAY时钟;
.rst_n ( sys_rst_n ),
//GMII接口信号
.gmii_tx_en ( gmii_tx_en ),//GMII发送数据使能信号;
.gmii_txd ( gmii_txd ),//GMII发送数据;
.gmii_rx_clk ( gmii_rx_clk ),//GMII接收时钟;
.gmii_rx_dv ( gmii_rx_dv ),//GMII接收数据有效信号;
.gmii_rxd ( gmii_rxd ),//GMII接收数据;
.gmii_tx_clk ( gmii_tx_clk ),//GMII发送时钟;
//RGMII接口信号;
.rgmii_rxc ( rgmii_rxc ),//RGMII接收时钟;
.rgmii_rx_ctl ( rgmii_rx_ctl ),//RGMII接收数据控制信号;
.rgmii_rxd ( rgmii_rxd ),//RGMII接收数据;
.rgmii_txc ( rgmii_txc ),//RGMII发送时钟;
.rgmii_tx_ctl ( rgmii_tx_ctl ),//RGMII发送数据控制信号;
.rgmii_txd ( rgmii_txd ) //RGMII发送数据;
);
4、上板实测
将上述工程的ILA注释取消,然后进行综合,下载到开发板上,开发板环境如下所示,连接摄像头和网线。
之后打开Wireshark工具,打开原子的上位机软件,进行如下设置。接收图像格式为RGB565的图像数据进行显示,目的IP、UDP端口地址等设置需要与工程顶层模块的对应参数保持一致。
然后点击上位机的打开按钮,通过Wireshark软件抓取数据如下图所示,首先上位机会先向FPGA开发板发送2个长度为1的UDP报文。如果上位机没有绑定开发板的MAC地址和IP地址,还会发送ARP请求,由于筛选的关系,此处看不见ARP请求数据报文。
之后FPGA开始向上位机发送图像数据,第一帧数据长度为1288,之后数据长度均为1280。如下图所示,第一帧虽然包含帧头,但是数据其实是错误的。错误的原因在于上位机可能在任何时候发起开始信号,有可能在一帧图像的中间开始传输数据,这样导致FIFO中的数据其实是溢出了很多的,最终导致错误。
但是第二帧开始时FIFO会复位清空,然后就能正常传输数据了,因此没有去做修改。
在wireshark中可以通过frame.len>=1330去筛选报文长度,进而可以查看全部长度为1288的报文,点击第二帧数据的第一个报文。发现帧头和分辨率都显示正确,没有问题。
之后将ILA设置抓取第一行数据报文的时序,当FIFO中数据等于1288时,产生UDP使能信号,下个时钟周期需要发送报文长度变为1280。
读出数据如下图所示,帧头数据和像素尺寸均与设置保持一致,传输的数据没有问题。
还可以抓一下FIFO复位之后的时序,如下图所示,当检测到场同步信号上升沿后,将FIFO复位信号拉高几个时钟周期,等FIFO复位结束之后,将第一行需要发送的帧头数据写入FIFO中,之后就等待需要写入FIFO的像素数据到来即可。
最后来查看一下摄像头传输的效果吧,如下面视频所示,由上位机显示结果可知,帧率维持在30左右,与前文的计算能够对应。
以太网摄像头
如果想要提高帧率,只需要修改PCLK时钟频率就行,目前为24MHz,如果将PCLK频率更改为48MHz,那么帧率可以达到60帧。
修改方式如下图所示,在初始化OV7725摄像头寄存器时,只需要将地址为8’h0d的高两位改为2’b11即可。
60帧测试结果如下所示,没有什么问题,只是存在很小的波动。
图13 60帧网络视频测试
只是需要注意一个问题,就是原子的这个上位机软件点击“关闭”之后,并不会向开发板发送以太网报文,这个可以通过wireshrak软件去抓,实际上抓不到任何报文。只是上位机软件不会接收图像数据了而已,可能是开发者忘记了这个功能吧。
5、总结
本文将OV7725图像数据通过以太网传输到PC端上位机进行显示,由于前文实现了摄像头数据采集和以太网收发模块的设计,本文只需要将摄像头采集的图像封装成上位机显示图像的格式即可,总体比较简单。
为了确保上一帧图像的错误传输不会影响下一帧数据,每次检测到场同步信号之后,需要复位FIFO,将FIFO清空,然后写入帧头数据,之后将图像数据写入FIFO。
每当FIFO中的数据超过一行图像数据时,向以太网发送模块的使能信号拉高,然后读取FIFO中的数据通过以太网传输给上位机。
需要本工程的在公众号后台回复“基于FPGA的网络摄像头”(不包括引号)即可。
如果对文章内容理解有疑惑或者对代码不理解,可以在评论区或者后台留言,看到后均会回复!
如果本文对您有帮助,还请多多点赞👍、评论💬和收藏⭐!您的支持是我更新的最大动力!将持续更新工程!