0


ZYNQ—BRAM全双工PS_PL数据交互(开源)

本资源实现了:采用ZYNQ7010vivado 2018.3xilinx 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查看数据。

标签: fpga开发

本文转载自: https://blog.csdn.net/qq_39376872/article/details/128524398
版权归原作者 陌夏微秋 所有, 如有侵权,请联系我们删除。

“ZYNQ—BRAM全双工PS_PL数据交互(开源)”的评论:

还没有评论