软件版本:VIVADO2017.4 操作系统:WIN10 64bit 硬件平台:适用米联客 ZYNQ系列开发板 米联客(MSXBO)论坛:www.osrc.cn答疑解惑专栏开通,欢迎大家给我提问!! 7.1 课程介绍 这一章主要讲述基于PCIE XDMA IP实现的一个图像传输应用,图像的数据流控制主要用到了VDMA IP,后面章节我们给出了基于FDMA自定的IP,进行传图应用,FDMA的方案要比VDMA要简单一些。本节课程的知识点点比较多,包括了中断部分知识,而中断部分内容前面的课程并没有涉及。 本节课主要包括FPGA端设计和WIN64系统下应用程序设计。FPGA端主要实现如下功能 - XDMA与DDR数据通信
- VDMA实现视频数据搬运
- 可编程VTC(Video Timing Control)模块,提供视频时序功能
- AXIS to Video模块,实现axis到视频时序的转换
- 图像处理模块,用户可自定义图像处理功能,例程中实现彩色图像的饱和度调整算法
- Video to AXIS模块,实现视频时序到AXIS的转换
- 用户逻辑寄存器模块,可桥接到XDMA,实现上位机通过XDMA对FPGA的寄存器控制
WIN64下应用程序实现如下功能: - 读取本地图像生成图像传输链表
- 通过VDMA的MM2S中断来从WIN64传输图像到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= v_start&&vcnt 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 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 WIN7/WIN10 下 XDMA 驱动代码修改 我们的硬件工程是需要上位机通过 PCIE 来配置的,比如 VDMA 寄存器的配置,图像算法中饱和度的配置, VTC 时序配置,这是使用的 XDMA 的AXI Lite master 来实现的。在驱动里面,这个接口称为用户接口,名称为 user,因此我们可以使用这样的方法来访问这个接口,xdma_rw.exe user write 0x00 0 1 2 3 这个操作是向XDMA 的 user BAR 地址0 写入 0 1 2 3 这四个字节,经过BAR to AXI 的转换(本教程转换地址为 0x44a00000)就可以通过XDMA的 AXI Lite Mater 接口去访问相应的 AXI 地址了。在本教程制作过程中,发现如果直接使用原驱动程序,通过user接口写数据到FPGA,比如写4个字节,从XDMA 的AXI Lite Master接口出来的数据被进行了写选通,使用了WSTRB功能,但是 VDMA 的AXI Lite 接口是不支持WSTRB 功能的,这样便会导致VDMA 的寄存器无法配置。 经过查看XDMA 的驱动代码,定位到对BAR 的操作位置,文件名为 file_io.c static NTSTATUS WriteBarFromRequest(IN PXDMA_DEVICE xdma, IN WDFREQUEST request, IN UINT32 nBar) 这 个函数实现的是将BAR 写请求写入到 BAR 地址。 可以看到驱动是使用了 WdfMemoryCopyToBuffer 方法,将请求数据写入到BAR 映射地址,那么问题很可能就出在这里,也就是说 WdfMemoryCopyToBuffer 这个函数很有可能是一个字节一个字节的写入,这必将导致 AXI 总线使用 WSTRB 信号对数据进行选通。那么如何修改呢,其实很简单,writeAddr = (PUCHAR)xdma->bar[nBar] + offset; 这个地址是经过映射后的地址,可以直接操作,因此我们做如下修改。 首先先判断写请求的 BAR 是否是 user bar 因为我们只关心 user bar 其他的bar 空间不关心也不必修改,万一改坏了呢。 如果是 user BAR,那么使用WdfMemoryCopyToBuffer 方法将请求数据存储到一个整型空间,(因为我们预先 知道,user 只用来进行寄存器的读写操作)然后直接将该整型数据写入BAR 地址,这样便解决了问题。 注意:这种修改存在一个 BUG,那就是每次写操作必须是 4 字节,多也不行,少也不行,如果用户并非使用user BAR 来进行寄存器空间,而是要读写比如BRAM 等,那么就不能进行如此修改,请根据自己需要进行修改! 这是写请求操作,读请求同样要进行修改。 static NTSTATUS ReadBarToRequest(IN PXDMA_DEVICE xdma, IN WDFREQUEST request, IN UINT32 nBar)这个是 读请求函数。修改如下: 在进行了上面的修改以后,就可以正确的配置 VDMA 等寄存器了,但是在使用过程中又发现一个问题,那就 是 XDMA 的驱动,总是在不断的读写 user BAR 的 0 地址,而凑巧的是,这个地址,正是 VDMA MM2S 的控制寄存器地址,因此导致该 VDMA 始终不能工作。经过代码排查,发现在 interrupt.c 文件这个函数 VOID EvtInterruptDpc(IN WDFINTERRUPT interrupt, IN WDFOBJECT device)里面有对user BAR 的操作,而且注释说明, 要根据 BITSTREAM 来修改这里,也就是说是留给用户进行修改的,而在 Xilinx Answer 65444 里面并没有提到这 个事情,太坑了。下面截图 根据注释来看,意思就是这个程序对应的硬件设计里面,user 中断是靠 user BAR 的前两个字节来控制的,产生中断以后,要进行清除操作,因此要访问 user BAR 的0 地址,但是我们设计中的 user 中断是 VDMA 产生的,所以这里是多余的,而且是捣乱的,因此注释掉。 修改以后重新编译驱动程序,再更新 XDMA 的驱动程序,这时候一切工作正常。 7.4 WIN7/WIN10 下 XDMA 应用程序设计 这一节主要来讲解XDMA 的应用程序。读者可以参考 Linux 系统下应用程序设计章节,来对比的学习。 对于XDMA 来讲,应用主要就是这几个方面,第一,前面讲到的 user 寄存器空间操作;第二,PC 通过 XDMA 写数据;第三,PC 通过XDMA 读数据;第四,用户中断获取。 我们先来看下官方提供的参考应用程序 user_event 用户中断测试程序 xdma_test 数据传输测试程序,前面有讲过 xdma_info xdma 信息获取 xdma_rw 功能函数,可以实现所有功能读写 这里多说两句,官方的测试程序 user_event 和xdma_test 里面使用了C++11 特性,比如 thread 功能,但是VS2010对 C++11 的支持还不是很好,同时,在xdma_rw 里面也使用了 c99 特性,VS2010 同样不支持,因此如果直接拿这个应用程序给 VS2010 编译,会报错。所以本教程使用的是vs2015,建议读者也使用vs2015。 关于驱动的操作,这里简述一下,WIN 下对驱动的操作,主要就是这么几个步骤 CreateFile SetFilePointer ReadFile WriteFile,读者可自行网上搜索,这里不做过多介绍。下面就进行传图应用程序设计,程序框架与 Linux 下的基本一致,下面给出应用程序测试 传输100 帧图像用时3.25s 帧率为 30 帧/s 效果图对比,可以发现饱和度进行了增强。用户可以自己修改代码来改变饱和度设置。 注意:本教程仅仅提供一个参考设计,程序里面可能存在资源未回收的 BUG,正常来说随着程序退出,资源 会被操作系统回收,但是同样应该引起注意! |