本资源实现了:采用ZYNQ7010;vivado 2018.3;xilinx sdk;正点原子XCOM串口助手,PS与PL均读写操作。
1、PS写0-9地址 0-9数据,触发start给PL。
2、PL接收到触发后,做两次的打拍,再遍历一遍,然后PL将0-9地址的数据读取,分别+2,写在10-19地址上。
3、PS再读取10-19地址上的数据并显示。
博主认为,这才是真正完成了PS与PL之间的数据交互,而非仅仅使用了BRAM资源,而且有给定len长度与地址长判断,可以实现仅修改
len为20,实现PS0-19地址写入,PL读取后写在20-39。
先看效果图:
接下来让我们看如何操作。
ZYNQ如何实现PS与PL的数据交互
相信大家都看过采用AXI接口通过BRAM,实现PS、PL之间的数据交互,但是网上的教程不尽人意,要么只是将PL的BRAM资源拿出来使用,实际上还是PS自己进行回环,基本上没有什么意义,如下图1。
图1 BRAM内的数据回环
目前大部分厂商所构建的BRAM数据回环是下面这张图:但是基本上也都是假通信
图2 常见BRAM数据通信回环
本博客实现的不同点
我也是使用正点原子历程,但是这类代码基本上都是同样的结果:PS写入BRAM,PL的BRAM读取并非把数据读出来,而是搞个ILA可以实时看,这只有学习意义,也是假通信,而博主也是费尽心思,终于搞定了:
1、PS写0-9地址 0-9数据,触发start给PL。
2、PL接收到触发后,做两次的打拍,再遍历一遍,然后PL将0-9地址的数据实时读取,分别+2,写在10-19地址上。
3、PS再读取10-19地址上的数据并显示。
博主认为,这才是真正完成了PS与PL之间的数据交互,而非仅仅使用了BRAM资源。接下来让我们看如何操作。
实现方案
可以直接用正点原子等网上那些的BRAM历程,对三个.v文件修改即可。
pl_bram_rd_v1_0_S00_AXI.v
主要是修改端口和例化bram_rd模块
// Users to add ports here//bram portinput wire [31:0] din, //写入BRAMoutput wire [31:0] dout,//读出BRAMoutput wire en,//BRAM使能output wire [3:0] we,//写读选择output wire [31:0] addr,//地址output wire intr, //interrupt输出给PS做中断output wire bramclk,//bram时钟output wire bramrst_n,//bram复位// Add user logic herebram_rd u_bram_rd(.clk (S_AXI_ACLK),.rst_n (S_AXI_ARESETN),.start (slv_reg0[0]),//PS写完数据后输出的一个脉冲触发.init_data (slv_reg1),//未用到.len (slv_reg2),//PS写入数据的长度.start_addr (slv_reg3), //PS写BRAM的起始地址//RAM端口 .din (din),.en (en ),.addr (addr ),.we (we ),.dout (dout),.bramclk(bramclk),.bramrst_n(bramrst_n),//bram port //control signal.intr(intr) //start to read and write bram);// User logic ends
可以看到其实现了对外部的信号接口、例化了bram_rd模块。
pl_bram_rd_v1_0.v
// Users to add ports here//bram portinput wire [31:0] din, output wire [31:0] dout,output wire en,output wire [3:0] we,output wire [31:0] addr,output wire intr, //interruptoutput wire bramclk,output wire bramrst_n,pl_bram_rd_v1_0_S00_AXI_inst (//RAM端口 .din (din),.en (en ),.addr (addr ),.we (we ),.dout (dout),.bramclk(bramclk),.bramrst_n(bramrst_n),//bram port //control signal.intr(intr), //start to read and write bram.S_AXI_ACLK(s00_axi_aclk),.S_AXI_ARESETN(s00_axi_aresetn),.S_AXI_AWADDR(s00_axi_awaddr),.S_AXI_AWPROT(s00_axi_awprot),.S_AXI_AWVALID(s00_axi_awvalid),.S_AXI_AWREADY(s00_axi_awready),.S_AXI_WDATA(s00_axi_wdata),.S_AXI_WSTRB(s00_axi_wstrb),.S_AXI_WVALID(s00_axi_wvalid),.S_AXI_WREADY(s00_axi_wready),.S_AXI_BRESP(s00_axi_bresp),.S_AXI_BVALID(s00_axi_bvalid),.S_AXI_BREADY(s00_axi_bready),.S_AXI_ARADDR(s00_axi_araddr),.S_AXI_ARPROT(s00_axi_arprot),.S_AXI_ARVALID(s00_axi_arvalid),.S_AXI_ARREADY(s00_axi_arready),.S_AXI_RDATA(s00_axi_rdata),.S_AXI_RRESP(s00_axi_rresp),.S_AXI_RVALID(s00_axi_rvalid),.S_AXI_RREADY(s00_axi_rready));
这个文件的内容和上个文件相辅相成
bram_rd.v
module bram_rd(input clk,input rst_n,//bram portinput [31:0] din, output reg [31:0] dout,output reg en,output reg [3:0] we,output reg [31:0] addr,//control signalinput start, //start to read and write braminput [31:0] init_data, //没有用到output reg start_clr, //没有用到input [31:0] len, //data countinput [31:0] start_addr, //start bram address//Interruptinput intr_clr, //clear interruptoutput reg intr, //interruptoutput bramclk,output bramrst_n);assign bramclk = clk ;
assign bramrst_n = 1'b0 ;localparam IDLE = 4'd0 ; //上电初始化
localparam READ_INIT = 4'd1 ; //每次循环读的初始化
localparam INIT = 4'd2 ; //每次循环的初始化
localparam READ_START = 4'd3 ; //准备读前的初始化
localparam READ_RAM = 4'd4 ; //读
localparam READ_END = 4'd5 ;//读结束
localparam WRITE_START = 4'd6 ;//准备写的初始化
localparam WRITE_RAM = 4'd7 ; //写
localparam WRITE_END = 4'd8 ;//写结束
localparam END = 4'd9 ;//结束reg [3:0] state ;
reg [31:0] len_tmp ;
reg [31:0] start_addr_tmp ;
reg [31:0] start_addr_tmp2 ;
reg [31:0] read_data_temp;
reg [31:0] read_addr;
reg [31:0] write_addr;reg start_rd_d0;
reg start_rd_d1;
//wire define
wire pos_start_rd;
assign pos_start_rd = ~start_rd_d1 & start_rd_d0;
//延时两拍,采 start_rd 信号的上升沿 因为BRAM_B读取数据需要延迟两拍,即在PS写好数据,需要等一下才能读到RAM数据
always @(posedge clk or negedge rst_n) beginif(!rst_n) beginstart_rd_d0 <= 1'b0; start_rd_d1 <= 1'b0;endelse beginstart_rd_d0 <= start; start_rd_d1 <= start_rd_d0; endend//Main statement
always @(posedge clk or negedge rst_n)
beginif (!rst_n)beginstate <= IDLE ;dout <= 32'd0 ;en <= 1'b0 ;we <= 4'd0 ;addr <= 32'd0 ;intr <= 1'b0 ;start_clr <= 1'b0 ;len_tmp <= 32'd0 ; end elsebegincase(state)IDLE : beginif (pos_start_rd)beginaddr<=start_addr;read_addr <= start_addr; start_addr_tmp <= start_addr ;start_addr_tmp2<= start_addr+len ; //从已有数据的后一位开始写write_addr<=start_addr+len ; //从已有数据的后一位开始写len_tmp <= len ; intr <= 1'b0 ; //读取到后取消触发state <= INIT ; en <= 1'b1;we <= 4'd0; end else begin state <= IDLE;intr <= 1'b0; en <= 1'b0;addr <= addr;we <= 4'd0; endendREAD_INIT : beginif ((addr - start_addr_tmp) >= (len_tmp)) //当读取的遍历结束一遍beginstate <= INIT ; en <= 1'b0 ;we <= 4'd0 ;addr<=start_addr_tmp; //获取读地址 提前两个周期read_addr<=start_addr_tmp; endelse beginstate <= READ_INIT; //继续遍历addr<=read_addr; //获取读地址 遍历read_addr<=read_addr+32'd4;read_data_temp<=din; end endINIT : beginstate <= READ_START ;we <= 4'b0000 ;en <= 1'b1 ; //先en1addr<=read_addr; //获取读地址 提前两个周期//read_data_temp<=din; end READ_START : beginen <= en;we <= we; //保持一个周期//read_data_temp<=din;state <= READ_RAM ;end READ_RAM : begin read_data_temp<=din; state <= READ_END ; end READ_END : beginread_addr<=read_addr+32'd4;en <= 1'b0; state <= WRITE_START ; end WRITE_START : beginen <= 1'b1;we <= 4'b1111;state <= WRITE_RAM ; addr <= write_addr ; end WRITE_RAM : beginif ((addr - start_addr_tmp2) >= (len_tmp)) //write completedbeginstate <= END ; en <= 1'b0 ;we <= 4'd0 ;endelsebegindout<=read_data_temp+32'd2; //到最后一位就不再写了 state <= WRITE_END ;endendWRITE_END : beginwrite_addr <= write_addr+32'd4 ;dout<=32'd0;addr<=read_addr; //获取读地址 提前两个周期 en <= 1'b0 ;we <= 4'd0 ;state <= INIT ; endEND : beginaddr <= 32'd0 ;dout <= 32'd0; intr <= 1'b1 ;state <= IDLE ; end default : state <= IDLE ;endcaseend
end
endmodule
其实现了捕获PS端输出的一个start脉冲,通过AXI-Lite接口得到start_addr起始地址和len数据长度,依次遍历后(保证数据的刷新),再进入后续状态机,读数据到reg变量,对该变量+2,写入到len长度后的BRAM块,最终读写完成后,输出一个高电平脉冲intr触发PS中断。
VIVADO的BD文件开发
相信大家应该都有用过正点之类厂商的代码,也看过相应的流程,同样的本博客的也是采用图2的方式,通过构建BRAM,一端接在axi_bram_ctrl_0也就是由PS可控制,一端接在PL构建的IP核:pl_bram_rd_0,接下来首先看BD文件都需要什么。
1、PL->PS的中断:我们知道AXI接口可以实现PL与PS的基本数据交互,但是不能实现中断,对于PL端来说,因为是并行数据处理,我们可以仅仅通过判断AXI的一个位是否为高电平做触发, 但是对于PS这样的ARM,不可能让他自己在那里循环等待,(当然可以实现,但是比较呆)。因此,我们需要有一个PL到PS的中断,使得PL数据写好后,触发中断给PS,PS响应中断后读取数据即可,如下图。
这样我们就实现了一个PL->PS的中断接口。
2、串口数据交互,由于博主用的板子只有PS端的串口有芯片,但是不能接收电脑数据,因此用了FPGA的PL端做串口,但是是采用的EMIO,也就是说操作还是在PS端,
如图采用的PL串口,是需要进行RUN Synthesis后进行绑定管脚,然后用一个CH340的串口模块,通过连接GND、RX、TX后实现与电脑的通信,如果大家是正点的板子,就不用考虑这个问题了。
然后再生成bit流,并export,launch sdk即可。
VIVADO SDK开发PS端
#include "xil_printf.h"
#include "xbram.h"
#include <stdio.h>
#include "pl_bram_rd.h"
#include "xscugic.h"#define BRAM_CTRL_BASE XPAR_AXI_BRAM_CTRL_0_S_AXI_BASEADDR
#define BRAM_CTRL_HIGH XPAR_AXI_BRAM_CTRL_0_S_AXI_HIGHADDR
#define PL_RAM_BASE XPAR_PL_BRAM_RD_0_S00_AXI_BASEADDR
#define PL_RAM_CTRL PL_BRAM_RD_S00_AXI_SLV_REG0_OFFSET
#define PL_RAM_INIT_DATA PL_BRAM_RD_S00_AXI_SLV_REG1_OFFSET//没用到
#define PL_RAM_LEN PL_BRAM_RD_S00_AXI_SLV_REG2_OFFSET
#define PL_RAM_ST_ADDR PL_BRAM_RD_S00_AXI_SLV_REG3_OFFSET#define START_MASK 0x00000001 //b01
#define INTRCLR_MASK 0x00000002 //b10
#define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID
#define INTR_ID XPAR_FABRIC_PL_BRAM_RD_0_INTR_INTR#define TEST_START_VAL 0x0
/** BRAM bytes number*/
#define BRAM_BYTENUM 4//每个数据占的字节大小,一般默认用4字节即32bitXScuGic INTCInst;char ch_data[1024]; //写入BRAM的字符数组
int Len=10 ;//单次写入长度
int Start_Addr=0 ;//写地址起始位即偏移0
int Intr_flag ;
/** Function declaration*/
int bram_read_write() ;
int IntrInitFuntion(u16 DeviceId);
void IntrHandler(void *InstancePtr);int main()
{int Status;Intr_flag = 1 ;IntrInitFuntion(INTC_DEVICE_ID) ;while(1){if (Intr_flag){Intr_flag = 0 ;Status = bram_read_write() ;if (Status != XST_SUCCESS){xil_printf("Bram Test Failed!\r\n") ;xil_printf("******************************************\r\n");Intr_flag = 1 ;}sleep(2);}}
}// 对BRAM的读写操作
int bram_read_write()
{u32 Write_Data = TEST_START_VAL ; // 要写入的数据int i ;/** if exceed BRAM address range, assert error*/if ((Start_Addr + Len) > (BRAM_CTRL_HIGH - BRAM_CTRL_BASE + 1)/4){xil_printf("******************************************\r\n");xil_printf("Error! Exceed Bram Control Address Range!\r\n");return XST_FAILURE ;}/** Write data to BRAM*/ //写地址长度0-9for(i = BRAM_BYTENUM*Start_Addr ; i < BRAM_BYTENUM*(Start_Addr + Len) ; i += BRAM_BYTENUM){XBram_WriteReg(XPAR_BRAM_0_BASEADDR, i , Write_Data) ;Write_Data += 1 ; //写0-9}printf("完成PS写入BRAM\t\n等待捕获PL写BRAM结束中断\t\n");//Set ram read and write lengthPL_BRAM_RD_mWriteReg(PL_RAM_BASE, PL_RAM_LEN , BRAM_BYTENUM*Len) ;//写寄存器,告诉PL数据长度//Set ram start addressPL_BRAM_RD_mWriteReg(PL_RAM_BASE, PL_RAM_ST_ADDR , BRAM_BYTENUM*Start_Addr) ;//写寄存器,告诉PL数据起始地址//Set pl initial data 没用到//PL_BRAM_RD_mWriteReg(PL_RAM_BASE, PL_RAM_INIT_DATA , (Start_Addr+1)) ;//Set ram start signalPL_BRAM_RD_mWriteReg(PL_RAM_BASE, PL_RAM_CTRL , START_MASK) ; //输出高电平脉冲触发startreturn XST_SUCCESS ;
}int IntrInitFuntion(u16 DeviceId)//接收PL端的intr中断
{XScuGic_Config *IntcConfig;int Status ;//check device idIntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);//intializationStatus = XScuGic_CfgInitialize(&INTCInst, IntcConfig, IntcConfig->CpuBaseAddress) ;if (Status != XST_SUCCESS)return XST_FAILURE ;XScuGic_SetPriorityTriggerType(&INTCInst, INTR_ID,0xA0, 0x3);Status = XScuGic_Connect(&INTCInst, INTR_ID,(Xil_ExceptionHandler)IntrHandler,(void *)NULL) ;if (Status != XST_SUCCESS)return XST_FAILURE ;XScuGic_Enable(&INTCInst, INTR_ID) ;Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,(Xil_ExceptionHandler)XScuGic_InterruptHandler,&INTCInst);Xil_ExceptionEnable();return XST_SUCCESS ;}void IntrHandler(void *CallbackRef)//中断服务函数
{int Read_Data ;int i ;printf("捕获到PL写BRAM结束中断\t\n");//clear interrupt statusPL_BRAM_RD_mWriteReg(PL_RAM_BASE, PL_RAM_CTRL , INTRCLR_MASK) ;for(i = BRAM_BYTENUM*Start_Addr ; i < BRAM_BYTENUM*(Start_Addr + Len+15) ; i += BRAM_BYTENUM) //len+10即可,只是多打几位,验证PL写的正确性{Read_Data = XBram_ReadReg(XPAR_BRAM_0_BASEADDR , i) ;printf("Address is %d\t Read data is %d\t\n", i/BRAM_BYTENUM ,Read_Data) ;}Intr_flag = 1 ;
}
可以看到,定义数据传输的长度为10,BRAM_BYTENUM 大小为4,也就是说,一次占用4个字节即32bit,在BRAM端表现就是一个深度。
Start_Addr起始地址为0。
PL写完数据后,触发PS端在中断读取,速度绝对比轮询要快得多。
最终效果:
可以看到,当PS写入数据触发PL的start高电平,PL实时捕获到start后,开始读取BRAM的len长度数据分别+2,写在原数据的后面。
而且有给定len长度与地址长判断,可以实现仅修改
len为20,实现PS0-19地址写入,PL读取后写在20-39。
总结
本博客介绍了,PS写数据到BRAM,触发PL读取,PL读取并+2分别写入到后面地址上,触发PS中断读取。
网上找半天,都没有PL和PS联合BRAM的例子,大多都是搞个Xilinx官方历程,PS写好数据后,PL就遍历地址,通过ila查看数据。