[X]关闭
2

(LINUX)S06-CH07 PCIE的传图应用方案(基于VDMA)

摘要: 这一章主要讲述基于PCIE XDMA IP实现的一个图像传输应用,图像的数据流控制主要用到了VDMA IP,后面章节我们给出了基于FDMA自定的IP,进行传图应用,FDMA的方案要比VDMA要简单一些。本节课程的知识点点比较多,包括了 ...

软件版本:VIVADO2017.4

操作系统:Ubuntu16.4 64bit

硬件平台:适用米联客 ZYNQ系列开发板

米联客(MSXBO)论坛:www.osrc.cn答疑解惑专栏开通,欢迎大家给我提问!!

7.1 概述

      这一章主要讲述基于PCIE XDMA IP实现的一个图像传输应用,图像的数据流控制主要用到了VDMA IP,后面章节我们给出了基于FDMA自定的IP,进行传图应用,FDMA的方案要比VDMA要简单一些。本节课程的知识点点比较多,包括了中断部分知识,而中断部分内容前面的课程并没有涉及。

      本节课主要包括FPGA端设计和Ubuntu系统下应用程序设计。FPGA端主要实现如下功能

  • XDMA与DDR数据通信
  • VDMA实现视频数据搬运
  • 可编程VTC(Video Timing Control)模块,提供视频时序功能
  • AXIS to Video模块,实现axis到视频时序的转换
  • 图像处理模块,用户可自定义图像处理功能,例程中实现彩色图像的饱和度调整算法
  • Video to AXIS模块,实现视频时序到AXIS的转换
  • 用户逻辑寄存器模块,可桥接到XDMA,实现上位机通过XDMA对FPGA的寄存器控制

Ubuntu下应用程序实现如下功能:

  • 读取本地图像生成图像传输链表
  • 通过VDMA的MM2S中断来从Ubuntu传输图像到FPGA DDR
  • 通过VDMA的S2MM中断来从FPGA DDR获取图像数据
  • 将图像保存到本地

7.2 FPGA端设计

     首先来介绍一下整个图像数据通路

       由上图可以看出,图像数据是由PC端产生,然后用过XDMA传输到ZYNQ的DDR3(通过HP接口),数据再经过一个MM2S的VDMA从ZYNQ的DDR(通过HP接口)读出到AXIS接口,经过AXIS to VIDEO转换到视频时序,可以做视频处理,之后再经过VIDEO to AXIS转换到AXIS接口,经过MM2S的VDMA存入DDR,XDMA再从DDR将图像数据读出到底PC。这里面实际上包含了两条传输路径,一条是PCIE到视频,一条是视频到PCIE,一个工程,两个操作例子,很有参考意义。

下面开始在Block Design里面搭建基本互联系统。主要设计就是IP例化,配置以及连接。

7.2.1 VDMA配置

       为了系统结构比较清晰,我们使用两个VDMA,一个负责S2MM,一个负责MM2S。为了提供比较充足的缓存,这里每个VDMA都采用16帧缓存模式。

MM2S VDMA配置

将Memory Map Data Width设置为64,增加AXI4接口位宽

Read Burst Size 选择32

以上两个参数用户可以自行选择,这里面只是提供参考,但是设置数值过低可能会影响性能。

Frame Buffers 选择16

Stream Data Width 选择24 传输RGB888数据

在 Advanced 选项里面同步方式选择 None,也就是靠AXIS Slave 端来控制。

Allow Unaligned Transfers 不勾选

S2MM VDMA 配置

7.2.2 XDMA 配置

关于XDMA 配置在第一课中已经有提及,这里面是针对第二课的设计进行一些配置。 

首先对下图地址进行配置,这个地址是 pcie 控制接口 bar 地址到 axi 地址的映射关系,由于工程中 vdma 寄存器 基地址,用户寄存器基地址都以 0x44a00000 开始,这里面就将 pcie 控制接口到 axi 总线的地址映射到 0x44a00000,空间大小为1MB,1MB 足够了。

       接下来是配置用户中断,由于两个 VDMA 都要产生中断来通知 PCIE 进行数据传输,那么这里选择 2,XDMA 会将用户中断映射为 MSI 或者 Legacy 中断通过PCIE 传输到PC

7.2.3 AXI Interconnect 配置

      系统中存在3个AXI4 Master设备,分别为两个VDMA和一个XDMA,都需要访问DDR,因此必须使用AXI Interconnect进行互联,一般情况下很多用户就直接例化了IP,配置了接口数量就不管了,这种做法针对AXI4 Lite总线互联完全没有问题,但是针对AXI4总线互联系统,可能并不是最佳的。实际上这里面也是有东西可设置的,可以改善互联时序,可以提供系统性能。这一节介绍的是针对AXI4总线互联的设置。

     AXI Interconnect其实是由多个基本AXI IP组合而成,其中包括 AXI Crossbar,AXI Register Slice,AXI Data Width Converter,AXI Clock Converter,AXI FIFO等,具体参考PG059

       优化选项里面选择 Maximize Performance,即最大化性能。其实这个配置是针对AXI Interconnect里面的Crossbar,当选择Maximize Performance时,Crossbar配置为SAMD模式。


       当配置为Maximize Performance时候,可以看到Slave Interface里面自动使能512深度的FIFO,在AXI 路径上添加FIFO有助于提供性能,改善时序。在Enable register slice里面选择Outer and Auto,在AXI路径上插入寄存器,可以改善时序。具体含义请参考PG059

      以上AXI Interconnect配置,主要是针对高频率,高带宽的AXI4总线互联系统,经过配置,可以有效改善时序和性能,但是并不是万能的,如果遇到不管怎么配置时序上都存在不收敛情况,可以查看是否是在握手信号路径上存在时序不收敛,如果是,则可以根据AXI Interconnect结构自行设计Interconnect,每条路径上都插入寄存器,如果仍然不能改善,那么降低频率吧。

      还有一个AXI4 Lite互联,这个就没啥说的了,按照默认即可。

7.2.4 IP连接

       相信大家都会在Block Design里面进行信号连接,以及一些连接规则,下面就说下需要注意的地方。

       第一个地方,是中断信号的连接,两个VDMA的中断信号都需要连接到XDMA,那么使用concatip进行信号的组合,然后连接到XDMA,注意S2MM的中断连接到usr_irq_req的1号中断,MM2S的中断连接到usr_irq_req的0号中断,要记住这个顺序,在PC软件设计的时候要用到。

      有的读者可能会发现XDMA的usr_irq_ack引脚没有进行连接,而根据pg195所讲,usr_irq_req信号要等到usr_irq_ack给出响应以后再拉低,也就是说usr_irq_ack是中断响应信号,应该给VDMA中断提供反馈,这么想完全没错,但是通过pg020可知VDMA的中断是自动产生,但是需要手动清除,而这个清除操作是由PC软件通过PCIE来控制,也就是说PC肯定收到了中断才能给出清除操作,这完全符合时序要求,因此可以不用管usr_irq_ack信号。

       第二个地方,在整个设计里面,由于时钟域的关系,AXI4 Lite总线可能会存在跨时钟问题,这时候要么使用AXI Interconnect来实现跨时钟,要么使用 AXI Clock Converter来实现。

第三个地方就是将两个VDMA的AXIS接口进行引出,同时一些时钟信号,复位信号等进行引出。

搭建好后的VIVADO工程如下图。

7.2.5 VTC模块

       这个工程里面的VTC模块用于产生视频时序信号,并且是可配置的。模块文件名为video_timing_gen.v。

模块代码如下:

modulevideo_timing_gen

#(

parameter SENSOR_ACT_W = 1920,

parameter SENSOR_ACT_H = 1080,

parameter SENSOR_WIDTH = 2200,

parameter SENSOR_HEIGHT = 1125,

parameter SENSOR_H_START = 60,

parameter SENSOR_V_START = 10,

parameter SENSOR_HSYNC_START = 0,

parameter SENSOR_HSYNC_STOP = 40,

parameter SENSOR_VSYNC_START = 0,

parameter SENSOR_VSYNC_STOP = 4

)

(

inputvideo_rst,

inputvideo_clk,

input  [12:0]reg_h_size,

input  [12:0]reg_v_size,

input  [12:0]reg_h_total,

input  [12:0]reg_v_total,

input  [12:0]reg_h_start,

input  [12:0]reg_v_start,

input  [12:0]reg_h_sync_start,

input  [12:0]reg_h_sync_stop,

input  [12:0]reg_v_sync_start,

input  [12:0]reg_v_sync_stop,

inputreg_setting_valid,

input  [7:0] reg_timing_ctrl,

outputregvsync,

outputreghsync,

outputreg de,

outputregvblank,

outputreghblank

);

reg [7:0]  timing_ctrl_r;

reg [12:0] h_size_r ;

reg [12:0] v_size_r ;

reg [12:0] h_total_r;

reg [12:0] v_total_r;

reg [12:0] h_start_r;

reg [12:0] v_start_r;

reg [12:0] h_sync_start_r;

reg [12:0] h_sync_stop_r;

reg [12:0] v_sync_start_r;

reg [12:0] v_sync_stop_r;

regsetting_valid_r = 0;

reg [12:0] h_size;

reg [12:0] v_size;

reg [12:0] h_total;

reg [12:0] v_total;

reg [12:0] h_start;

reg [12:0] v_start;

reg [12:0] h_sync_start;

reg [12:0] h_sync_stop;

reg [12:0] v_sync_start;

reg [12:0] v_sync_stop;

regisrun;

regisstream;

registrig;

reg [12:0] hcnt;

reg [12:0] vcnt;  

wiresetting_valid_rising = ~setting_valid_r®_setting_valid;

always @(posedgevideo_clk) setting_valid_r<= reg_setting_valid;

always @(posedgevideo_clk)

begin

if(video_rst)

begin

h_size_r<= SENSOR_ACT_W;

v_size_r<= SENSOR_ACT_H;

h_total_r<= SENSOR_WIDTH;

v_total_r<= SENSOR_HEIGHT;

h_start_r<= SENSOR_H_START;

v_start_r<= SENSOR_V_START;

h_sync_start_r<= SENSOR_HSYNC_START;

h_sync_stop_r<= SENSOR_HSYNC_STOP;

v_sync_start_r<= SENSOR_VSYNC_START;

v_sync_stop_r<= SENSOR_VSYNC_STOP;

//timing_ctrl_r<= {1'b0,5'b0,1'b0,1'b0};

end

else if(setting_valid_rising)

begin

h_size_r<= reg_h_size      ;

v_size_r<= reg_v_size      ;

h_total_r<= reg_h_total     ;

v_total_r<= reg_v_total     ;

h_start_r<= reg_h_start     ;

v_start_r<= reg_v_start     ;

h_sync_start_r<= reg_h_sync_start;

h_sync_stop_r<= reg_h_sync_stop ;

v_sync_start_r<= reg_v_sync_start;

v_sync_stop_r<= reg_v_sync_stop ;

end

end

always @(posedgevideo_clk)

begin

if(video_rst)

begin

h_size<= SENSOR_ACT_W;

v_size<= SENSOR_ACT_H;

h_total<= SENSOR_WIDTH;

v_total<= SENSOR_HEIGHT;

h_start<= SENSOR_H_START;

v_start<= SENSOR_V_START;

h_sync_start<= SENSOR_HSYNC_START;

h_sync_stop<= SENSOR_HSYNC_STOP;

v_sync_start<= SENSOR_VSYNC_START;

v_sync_stop<= SENSOR_VSYNC_STOP;

end

else if((hcnt == h_total-1 &&vcnt == v_total-1)&&isrun || (hcnt == 0 &&vcnt == 0 && !isrun))

begin

h_size<= h_size_r      ;

v_size<= v_size_r      ;

h_total<= h_total_r     ;

v_total<= v_total_r     ;

h_start<= h_start_r     ;

v_start<= v_start_r     ;

h_sync_start<= h_sync_start_r;

h_sync_stop<= h_sync_stop_r ;

v_sync_start<= v_sync_start_r;

v_sync_stop<= v_sync_stop_r ;

end


end


always @(posedgevideo_clk)

begin

if(video_rst)

begin

timing_ctrl_r<= {1'b0,5'b0,1'b0,1'b0};

isrun<= 0;

isstream<= 0;

istrig<= 0;

end

else

begin

if(setting_valid_rising)

timing_ctrl_r<= reg_timing_ctrl ;

if((hcnt == h_total-1 &&vcnt == v_total-1) &&isrun || (hcnt ==0 &&vcnt == 0 && !isrun))

begin

isrun<= timing_ctrl_r[0];

isstream<= timing_ctrl_r[1];

istrig<= timing_ctrl_r[7];

timing_ctrl_r[7] <= 1'b0;

end

end

end

always @(posedgevideo_clk)

begin

if(video_rst)

begin

hcnt<= 0;

end

else if(hcnt == h_total-1 && (isrun|istrig))

hcnt<= 13'd0;

else if((isrun&isstream) | (~isstream&istrig))

hcnt<= hcnt + 1;

end

always @(posedgevideo_clk)

begin

if(video_rst)

begin

vcnt<= 0;

end

else if(hcnt == h_total-1 &&vcnt == v_total-1 && (isrun | istrig))

vcnt<= 13'd0;

else if(hcnt == h_total-1 && (isrun | istrig))

vcnt<= vcnt + 1;

end

always @(posedgevideo_clk)

begin

if(video_rst)

begin

hsync<= 1'b0;

end

else if(hcnt>h_sync_start&&hcnt<= h_sync_stop)

hsync<= 1'b1;

else

hsync<= 1'b0;

end

always @(posedgevideo_clk)

begin

if(video_rst)

begin

de<= 1'b0;

end

else if(hcnt>= h_start&&hcnt<h_start+h_size&&vcnt>= v_start&&vcnt<v_start+v_size)

de<= 1'b1;

else

de<= 1'b0;

end

always @(posedgevideo_clk)

begin

if(video_rst)

begin

hblank<= 1'b1;

end

else if(hcnt>= h_start&&hcnt<h_start+h_size)

hblank<= 1'b0;

else

hblank<= 1'b1;

end

always @(posedgevideo_clk)

begin

if(video_rst)

begin

vblank<= 1'b1;

end

else if(vcnt>= v_start-1 &&vcnt<= v_start+v_size )

vblank<= 1'b0;

else

vblank<= 1'b1;

end

always @(posedgevideo_clk)

begin

if(video_rst)

begin

vsync<= 1'b0;

end

else if(vcnt>v_sync_start&&vcnt<= v_sync_stop)

vsync<= 1'b1;

else

vsync<= 1'b0;

end

endmodule


       针对这个模块做一些说明。关于为什么要将数据转换到视频时序呢,将数据转换到视频时序上,可以方便的用于显示,以及将视频时序输入到需要Video接口的模块里面进行处理等操作。当然,如果需要将数据送入例如HLS生成的IP里面进行处理,完全没必要再转换到视频时序,直接使用VDMA的AXIS接口即可,这里面只是提供一个例子。关于视频时序就不做过多的陈述,相信大家都很熟悉了,下面只是对模块接口配置进行一下功能描述

接口

位宽

功能描述

reg_h_size

13

水平有效像素数

reg_v_size

13

垂直有效像素数

reg_h_total

13

水平总像素数

reg_v_total

13

垂直总像素数

reg_h_start

13

水平有效像素开始位置

reg_v_start

13

垂直有效像素开始位置

reg_h_sync_start

13

水平时序同步信号开始位置

reg_h_sync_stop

13

水平时序同步信号结束位置

reg_v_sync_start

13

垂直时序同步信号开始位置

reg_v_sync_stop

13

垂直时序同步信号结束位置

reg_timing_ctrl

8

时序控制寄存器,设计目标是实现两种功能,一种是连续视频模式,一种是触发模式,但触发模式未实现。实际使用中低2个bit有效,同时写1则开始工作,写0的话就等当前帧传输完毕停止工作

reg_setting_valid

1

在此信号上升沿,进行以上所有配置采样。并在当前帧传输完成以后生效


7.2.6 AXIS2VIDEO和VIDEO2AXIS

      这两个模块并非使用的Xilinx IP,而是使用Verilog编写的,主要就是利用FIFO来进行时序同步。VIDEO2AXIS与Xilinx IP功能基本一致,而AXIS2VIDEO属于Xilinx IP的简化版本,但是全都不带padding功能!

      这两个模块的代码参见工程文件,文档里面只介绍一下接口。首先是AXIS2VIDEO模块接口。

module axis2video

#(

parameter DW = 24

)

(

input axis_clk,

input axis_aresetn,

input [DW-1:0]      s_axis_tdata,

input               s_axis_tvalid,

input               s_axis_tlast,

input               s_axis_tuser,

output reg          s_axis_tready,

input               video_clk,

input               video_rst,

input               video_in_hsync,

input               video_in_vsync,

input               video_in_hblank,

input               video_in_vblank,

input               video_in_de,

output              video_out_hsync,

output              video_out_vsync,

output              video_out_hblank,

output              video_out_vblank,

output              video_out_de,

output     [DW-1:0] video_out_data,

output              video_out_locked

);

      从接口中可以看出,这个模块是将AXIS信号同步到VIDEO_IN信号,然后以VIDEO_OUT时序输出视频。只有当 video_out_locked 信号为高的时候数据才有效。

      VIDEO2AXIS模块接口

module video2axis

#(

parameter DW = 24

)

(

input axis_clk,

input axis_aresetn,

output  [DW-1:0]   m_axis_tdata,

output  [DW/8-1:0] m_axis_tkeep,

output             m_axis_tvalid,

output             m_axis_tlast,

output             m_axis_tuser,

input              m_axis_tready,

input              video_clk,

input              video_rst,

input              video_locked,

input              video_hsync,

input              video_vsync,

input              video_hblank,

input              video_vblank,

input              video_de,

input  [DW-1:0]    video_data,

input              video_ce

);

      可实现VIDEO信号到AXIS信号的转换,其中video_locked信号可由AXIS2VIDEO模块提供,video_ce信号是输入使能,为1的时候输入数据才会被转换为AXIS。

7.2.7 VIDEO PROCESS

      将VDMA输出的AXIS信号转换到VIDEO信号以后,可以进行VIDEO信号的图像处理,教程中采用了一个调节彩色图像饱和度的算法。也就是将输入图像按照寄存器配置进行饱和度调节,然后再输出,最终通过PCIE到达主机。

module video_process

#(

parameter DW = 24

)

(

input         video_clk       ,

input         video_rst       ,

input [7:0]   reg_saturation  ,

input         video_in_hsync  ,

input         video_in_vsync  ,

input         video_in_hblank ,

input         video_in_vblank ,

input         video_in_de     ,  

input [DW-1:0]video_in_data   ,

output        video_out_hsync ,

output        video_out_vsync ,

output        video_out_hblank,

output        video_out_vblank,

output        video_out_de    ,

output[DW-1:0]video_out_data  

);

reg_saturation[7:0] 就是图像饱和度设置,当设置为128的时候,饱和度不做任何改变,当小于128的时候,饱和度降低,大于128的时候,饱和度增加。可实时配置。

7.2.8 寄存器配置功能

       前面讲到,VTC模块可以通过配置接口进行视频时序配置,图像处理模块可以通过配置接口进行图像饱和度配置,那么如何通过PCIE来进行这些配置呢,在介绍XDMA的时候有讲过,XDMA有一个AXI Lite Master的接口,并且在IP配置的时候进行了BAR地址到AXI地址的映射,那么主机便可以通过访问PCIE的BAR地址然后经过XDMA的转换,将主机的访问操作转换到AXI Lite Master总线操作,进而可以实现对FPGA内部其他AXI Lite Slave的地址访问。那么根据这个信息,配置接口就需要做成一个AXI Slave设备,并将地址映射到XDMA的AXI Lite Master空间。我们可以使用一个AXI Lite Slave代码来实现,也可以用别的方法,就是本教程中的方法,使用AXI BRAM Ctrl模块,将AXI Lite转换到RAM读写时序上,可以极大方便操作。

在使用BRAM CTRL的时候,地址映射那里需要注意一下,由于BRAM CTRL是MEM设备,自动分配地址不与VDMA等控制接口地址挨着,XDMA的BAR到AXI 地址映射已经改为了0x44a00000,因此,我们将BRAM CTRL的地址排序下来,手动改为0x44a20000

接下来,我们再设计一个将BRAM CTRL的控制信号转换为寄存器信号的模块。

module bramctrl_to_config

#(

parameter ADDR_W = 16

)

(

input              bramctrl_clk,

input              bramctrl_rst,

input [ADDR_W-1:0] bramctrl_addr,

input [31:0]       bramctrl_data_out,

input [3:0]        bramctrl_we,

input              bramctrl_en,

output[31:0]       bramctrl_data_in,

output reg  [12:0] reg_h_size,

output reg  [12:0] reg_v_size,

output reg  [12:0] reg_h_total,

output reg  [12:0] reg_v_total,

output reg  [12:0] reg_h_start,

output reg  [12:0] reg_v_start,

output reg  [12:0] reg_h_sync_start,

output reg  [12:0] reg_h_sync_stop ,

output reg  [12:0] reg_v_sync_start,

output reg  [12:0] reg_v_sync_stop ,

output reg         reg_setting_valid,

output reg  [7:0]  reg_timing_ctrl,

output reg  [7:0]  reg_saturation

);

/*

register space

0x00  [7]:istrig [1]:isstream [0]:isrun

0x04  [28:16]:v_total [12:0]:h_total

0x08  [28:16]:v_size [12:0]:h_size

0x0c  [28:16]:v_start [12:0]:h_start

0x10  [28:16]:v_sync_start [12:0]:v_sync_stop

0x14  [28:16]:h_sync_start [12:0]:h_sync_stop

0x18  [0]:setting_valid high pulse valid

0x20  [7:0] reg_saturation

*/

reg [31:0] bramctrl_rddata;

wire we_ah00 = bramctrl_addr == 16'h0000;

wire we_ah04 = bramctrl_addr == 16'h0004;

wire we_ah08 = bramctrl_addr == 16'h0008;

wire we_ah0c = bramctrl_addr == 16'h000c;

wire we_ah10 = bramctrl_addr == 16'h0010;

wire we_ah14 = bramctrl_addr == 16'h0014;

wire we_ah18 = bramctrl_addr == 16'h0018;

wire we_ah20 = bramctrl_addr == 16'h0020;

wire rd_ah00 = bramctrl_addr == 16'h0000;

wire rd_ah04 = bramctrl_addr == 16'h0004;

wire rd_ah08 = bramctrl_addr == 16'h0008;

wire rd_ah0c = bramctrl_addr == 16'h000c;

wire rd_ah10 = bramctrl_addr == 16'h0010;

wire rd_ah14 = bramctrl_addr == 16'h0014;

wire rd_ah18 = bramctrl_addr == 16'h0018;

wire rd_ah20 = bramctrl_addr == 16'h0020;


always @(posedge bramctrl_clk)

begin

if(bramctrl_rst)

begin

reg_h_size        <= 1920;

reg_v_size        <= 1080;

reg_h_total       <= 2200;

reg_v_total       <= 1125;

reg_h_start       <= 60;

reg_v_start       <= 10;

reg_h_sync_start  <= 'b0;

reg_h_sync_stop   <= 40;

reg_v_sync_start  <= 'b0;

reg_v_sync_stop   <= 4;

reg_setting_valid <= 'b0;

reg_timing_ctrl   <= 'b0;

reg_saturation    <= 'd255;

end

else

begin

case({(&bramctrl_we)&bramctrl_en,we_ah00,we_ah04,we_ah08,we_ah0c,we_ah10,we_ah14,we_ah18,we_ah20})

9'b110000000:reg_timing_ctrl <= bramctrl_data_out[7:0];

9'b101000000:begin reg_v_total<= bramctrl_data_out[28:16];reg_h_total<= bramctrl_data_out[12:0];end

9'b100100000:begin reg_v_size<= bramctrl_data_out[28:16];reg_h_size<= bramctrl_data_out[12:0];end

9'b100010000:begin reg_v_start<= bramctrl_data_out[28:16];reg_h_start<= bramctrl_data_out[12:0];end

9'b100001000:begin reg_v_sync_start<= bramctrl_data_out[28:16];reg_v_sync_stop<= bramctrl_data_out[12:0];end

9'b100000100:begin reg_h_sync_start<= bramctrl_data_out[28:16];reg_h_sync_stop<= bramctrl_data_out[12:0];end

9'b100000010:begin reg_setting_valid<= bramctrl_data_out[0];end

9'b100000001:begin reg_saturation<= bramctrl_data_out[7:0];end

default:;

endcase

end

end

always @(posedge bramctrl_clk)

begin

if(bramctrl_rst)

begin

bramctrl_rddata <= 0;

end

else

begin

case({(bramctrl_we==4'b0)&bramctrl_en,rd_ah00,rd_ah04,rd_ah08,rd_ah0c,rd_ah10,rd_ah14,rd_ah18,rd_ah20})

9'b110000000: bramctrl_rddata[7:0] <= {24'b0,reg_timing_ctrl};

9'b101000000:begin bramctrl_rddata <= {3'b0,reg_v_total,3'b0,reg_h_total};end

9'b100100000:begin bramctrl_rddata <= {3'b0,reg_v_size,3'b0,reg_h_size};end

9'b100010000:begin bramctrl_rddata <= {3'b0,reg_v_start,3'b0,reg_h_start};end

9'b100001000:begin bramctrl_rddata <= {3'b0,reg_v_sync_start,3'b0,reg_v_sync_stop};end

9'b100000100:begin bramctrl_rddata <= {3'b0,reg_h_sync_start,3'b0,reg_h_sync_stop};end

9'b100000010:begin bramctrl_rddata <= {31'b0,reg_setting_valid};end

9'b100000001:begin bramctrl_rddata <= {24'b0,reg_saturation};end

default:bramctrl_rddata <= bramctrl_rddata;

endcase

end

end

assign bramctrl_data_in = bramctrl_rddata;

endmodule

我们的寄存器地址分配如下

偏移地址

有效位宽

功能描述

h00

[1:0]

VTC控制寄存器,目前版本只有低两个bit有效,同时写1,VTC开始工作

h04

[28:16] [12:0]

[28:16]:v_total [12:0]:h_total

h08

[28:16] [12:0]

[28:16]:v_size [12:0]:h_size

h0c

[28:16] [12:0]

[28:16]:v_start [12:0]:h_start

h10

[28:16] [12:0]

[28:16]:v_sync_start [12:0]:v_sync_stop

h14

[28:16] [12:0]

[28:16]:h_sync_start [12:0]:h_sync_stop

h18

[0]

设置有效信号,一个高脉冲生效

h20

[7:0]

图像处理模块饱和度寄存器

        关于BRAM CTRL操作时序这里不做描述。

7.3 Ubuntu 主机软件设计

       在第一课时,已经介绍了在Ubuntu下驱动编译,驱动加载,以及简单的数据传输测试等,这一节就根据参考程序来设计自己的应用程序。我们应用程序要实现的功能是,第一,控制VDMA和VTC以及图像处理模块的寄存器;第二,处理VDMA中断;第三,根据MM2S VDMA中断节拍将主机本地图像数据送入MM2S VDMA的缓存内存,根据S2MM VDMA中断节拍将S2MM VDMA缓存内存中的图像数据读出到主机。

7.3.1 软件设计预备

       根据软件设计目标,我们需要处理PCIE发回的VDMA中断,需要通过PCIE读取FPGA DDR数据,需要通过PCIE往FPGA DDR 写输入数据,还需要通过PCIE读写寄存器,几乎囊括了XDMA所有主要操作。

        当驱动加载完成以后,会在系统/dev/目录下创建设备节点文件,在终端使用命令“ls /dev”来查看一下,这里找出与XDMA相关的。

下面针对这些设备文件做一个简单介绍。

设备文件

描述

xdma0_c2h_0(1)

XDMA的Card to Host通道,也就是读数据通道,将FPGA数据传送到主机,根据XDMA IP设置,PCIE2.0最多拥有2个c2h通道

xdma0_h2c_0(1)

XDMA的Host  to Card 通道,也就是写数据通道,主机的数据传送到FPGA,根据XDMA IP设置,PCIE2.0最多拥有2个h2c通道

xdma0_events_0(0-15)

XDMA一共可提供16个用户中断线,每一个中断线就对应一个event,主机通过读取event来获取用户中断线

xdma0_user

应用层可通过此设备文件访问XDMA的AXI Lite接口,进而操作用户侧寄存器

xdma0_control

应用层可通过此接口访问XDMA内部寄存器

       由以上可知,主机向FPGA发送视频数据就使用“/dev/ xdma0_h2c_0(1)”,主机从FPGA读取数据就使用“/dev/xdma0_c2h_0(1)”,主机获取VDMA中断信息就使用“/dev/xdma0_events_0(1)”,主机访问VDMA寄存器和VTC以及图像处理寄存器就使用“/dev/xdma0_user”。

下面结合XDMA驱动源码来分别介绍一下如何使用这些设备文件。

7.3.1.1 通过PCIE获取中断操作

       Xilinx提供的例子里面没有关于如何使用中断的例子,不过我们可以在驱动里面找到关于中断操作的过程,进而可以得知如何在应用程序通过对event的操作来获取用户中断信息。

        Events设备是用户通过xdma获取发送过来的中断信息,被驱动封装称为一个字符设备,并且是只读的,下面先看一下驱动提供的设备操作方法。

基本上就是open和read操作。

open操作:

主要操作就是file->private_data = lro_char;

read操作:

这个读操作主要就是靠函数:

wait_event_interruptible(user_irq->events_wq,user_irq->events_irq != 0);

调用wait_event_interruptible将调用进程从调度队列删除等待唤醒。

当被唤醒之后,调用copy_to_user(buf, &events_user, 4);返回一个4字节的数值。其实这个数值没有特殊意义,只要不为0即可。

       上面是驱动提供的设备操作方法,也就是说应用层需要调用read函数来获取中断信息,下面继续看驱动代码,看用户中断是如何工作的。

中断申请

      申请中断,注册中断函数,当FPGA一侧通过usr_irq_req发送中断信息以后,xdma接收中断进入中断函数,中断函数再调用中断下半部work,work里面再调用中断服务程序,中断服务程序将唤醒条件置1,然后唤醒之前挂起的read操作。

      总结一下用户读取中断过程就是,应用层调用read方法,驱动在未获得中断之前,将调用read的进程从任务队列删除,当前进行挂起,当中断发生以后首先提供挂起进行的唤醒条件,然后调用wake_up_interruptible函数将进程加入到系统调度队列等待调度,一旦该进程被调度,则read函数返回,获得一次中断信息。由此可见这个中断并非实时的,这也是Linux系统的问题。

      通过分析驱动,我们确定中断获取函数为

static int open_event(char *filename)

{

int fd;

fd = open(filename, O_RDWR | O_SYNC);

if(fd == -1)

{

printf("open event error\n");

return -1;

}

return fd;

}

static int read_event(int fd)

{

int val;

read(fd,&val,4);

return val;

}

一个打开函数,一个读取函数。

2.3.1.2 通过PCIE读写寄存器操作

应用程序通过“/dev/xdma0_user”来读写用户寄存器空间。Xilinx提供的例子里面reg_rw程序就是实现这个功能的,我们来看一下他主要的操作。

首先是open 打开设备,然后使用了mmap方法,即内存映射,将PCIE的用户BAR空间映射到虚拟地址空间,通过虚拟地址空间即可读写PCIE的BAR地址。

以上分别为读写操作,注意大小端问题。

因此,我们设计用户空间寄存器读写功能函数为:

static int open_control(char *filename)

{

int fd;

fd = open(filename, O_RDWR | O_SYNC);

if(fd == -1)

{

printf("open control error\n");

return -1;

}

return fd;

}

static void *mmap_control(int fd,long mapsize)

{

void *vir_addr;

vir_addr = mmap(0, mapsize, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

return vir_addr;

}

static void write_control(void *base_addr,int offset,uint32_t val)

{

uint32_t writeval = htoll(val);

*((uint32_t *)(base_addr+offset)) = writeval;

}

static uint32_t read_control(void *base_addr,int offset)

{

uint32_t read_result = *((uint32_t *)(base_addr+offset));

read_result = ltohl(read_result);

return read_result;

}

2.3.1.3 通过PCIE读数据操作

       应用程序通过“/dev/xdma0_c2h_0(1)”来从PCIE读取数据,XIlinx提供的应用程序,dma_from_device 就是实现读操作的例子,看一下读操作是如何进行的。


      从例子代码中可以挑出关键函数

      posix_memalign((void **)&allocated, 4096/*alignment*/, size + 4096);申请地址对齐的内存,关于这里为什么要申请地址对齐的内存,作者查看了一下XDMA的驱动代码,发现他在构建描述符链表的时候使用的是用户空间内存,并非是内核空间内存,因此在申请内存的时候进行4k对齐也就解释的通了。

      int fpga_fd = open(devicename, O_RDWR | O_NONBLOCK);打开设备文件,获取描述符。

      off_t off = lseek(fpga_fd, addr, SEEK_SET);lseek函数,这个操作很重要,从解释里面就可以看出,他是用来设置AXI MM的地址的,也就是FPGA DDR的地址,在读取一个地址之前要先调用lseek来将读指针指向这个空间,注意,在读操作过程中,这个读指针是会自增的。

      rc = read(fpga_fd, buffer, size);读操作,从PCIE获取数据,存储到buffer空间,这个读操作与前面介绍的读取用户中断操作在驱动里面的表现基本一致,也就是应用层调用了read方法,驱动构建了传输描述符链表等之后就将该进行从任务队列删除,当传输完成中断发生之后,才加入到运行队列里面等待调度。

2.3.1.4 通过PCIE写数据操作

       应用程序通过“/dev/xdma0_h2c_0(1)”来向PCIE写入数据,XIlinx提供的应用程序,dma_to_device 就是实现写操作的例子,看一下写操作是如何进行的。

写过程与读操作类似,不再叙述。

7.3.2软件设计流程

         我们软件要实现的功能是读取本地图像,通过PCIE传送给FPGA,FPGA处理完成以后再通过PCIE传给主机,当所有图像处理完成以后退出程序。总结起来,分为4个操作,监控 MM2S VDMA中断,通过PCIE向MM2S VDMA传输图像,监控S2MM VDMA中断,通过PCIE从S2MM VDMA缓存中读取图像,因此应用程序设计为4线程,两个线程分别监控中断,两个线程发送和接收数据。

下面是4个线程的代码,以及主函数代码,中断监测线程与数据处理线程采用信号量的方式进行同步。关于信号量,可以网络搜索其操作及功能。

void *h2c_event_process()

{

int fd;

fd = open_event("/dev/xdma0_events_0");


printf("h2c event thread running\n");

while(1)

{

read_event(fd);

write_control(control_base,MM2S_VDMA_BASE+VDMA_MM2S_SR,0xffffffff);

sem_post(&h2c_sem);

}

}

void *h2c_data_process()

{

int fd;

int frame_cnt;

fd = open("/dev/xdma0_h2c_0",O_RDWR);


路过

雷人

握手

鲜花

鸡蛋
发表评论

最新评论

引用 nyamlkso 2023-9-15 14:44
是否可提供源码
引用 wldshy 2020-1-19 01:29
不全

查看全部评论(2)

本文作者
2019-9-20 14:15
  • 1
    粉丝
  • 8266
    阅读
  • 2
    回复

关注米联客

扫描关注,了解最新资讯

联系人:汤经理
电话:0519-80699907
EMAIL:270682667@qq.com
地址:常州溧阳市天目云谷3号楼北楼201B
热门评论
排行榜