本资源实现了:采用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 port
input wire [31:0] din, //写入BRAM
output wire [31:0] dout,//读出BRAM
output 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 here
bram_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 port
input 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, //interrupt
output 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 port
input [31:0] din,
output reg [31:0] dout,
output reg en,
output reg [3:0] we,
output reg [31:0] addr,
//control signal
input start, //start to read and write bram
input [31:0] init_data, //没有用到
output reg start_clr, //没有用到
input [31:0] len, //data count
input [31:0] start_addr, //start bram address
//Interrupt
input intr_clr, //clear interrupt
output reg intr, //interrupt
output 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) begin
if(!rst_n) begin
start_rd_d0 <= 1'b0;
start_rd_d1 <= 1'b0;
end
else begin
start_rd_d0 <= start;
start_rd_d1 <= start_rd_d0;
end
end
//Main statement
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
begin
state <= 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
else
begin
case(state)
IDLE : begin
if (pos_start_rd)
begin
addr<=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;
end
end
READ_INIT : begin
if ((addr - start_addr_tmp) >= (len_tmp)) //当读取的遍历结束一遍
begin
state <= INIT ;
en <= 1'b0 ;
we <= 4'd0 ;
addr<=start_addr_tmp; //获取读地址 提前两个周期
read_addr<=start_addr_tmp;
end
else begin
state <= READ_INIT; //继续遍历
addr<=read_addr; //获取读地址 遍历
read_addr<=read_addr+32'd4;
read_data_temp<=din;
end
end
INIT : begin
state <= READ_START ;
we <= 4'b0000 ;
en <= 1'b1 ; //先en1
addr<=read_addr; //获取读地址 提前两个周期
//read_data_temp<=din;
end
READ_START : begin
en <= en;
we <= we; //保持一个周期
//read_data_temp<=din;
state <= READ_RAM ;
end
READ_RAM : begin
read_data_temp<=din;
state <= READ_END ;
end
READ_END : begin
read_addr<=read_addr+32'd4;
en <= 1'b0;
state <= WRITE_START ;
end
WRITE_START : begin
en <= 1'b1;
we <= 4'b1111;
state <= WRITE_RAM ;
addr <= write_addr ;
end
WRITE_RAM : begin
if ((addr - start_addr_tmp2) >= (len_tmp)) //write completed
begin
state <= END ;
en <= 1'b0 ;
we <= 4'd0 ;
end
else
begin
dout<=read_data_temp+32'd2; //到最后一位就不再写了
state <= WRITE_END ;
end
end
WRITE_END : begin
write_addr <= write_addr+32'd4 ;
dout<=32'd0;
addr<=read_addr; //获取读地址 提前两个周期
en <= 1'b0 ;
we <= 4'd0 ;
state <= INIT ;
end
END : begin
addr <= 32'd0 ;
dout <= 32'd0;
intr <= 1'b1 ;
state <= IDLE ;
end
default : state <= IDLE ;
endcase
end
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字节即32bit
XScuGic 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-9
for(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 length
PL_BRAM_RD_mWriteReg(PL_RAM_BASE, PL_RAM_LEN , BRAM_BYTENUM*Len) ;//写寄存器,告诉PL数据长度
//Set ram start address
PL_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 signal
PL_BRAM_RD_mWriteReg(PL_RAM_BASE, PL_RAM_CTRL , START_MASK) ; //输出高电平脉冲触发start
return XST_SUCCESS ;
}
int IntrInitFuntion(u16 DeviceId)//接收PL端的intr中断
{
XScuGic_Config *IntcConfig;
int Status ;
//check device id
IntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);
//intialization
Status = 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 status
PL_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查看数据。
版权归原作者 陌夏微秋 所有, 如有侵权,请联系我们删除。