一、實驗室任務
????????本章的實驗任務是 PS 將數據寫入BRAM,然后從 BRAM 中讀出數據,并通過串口打印出來;與此同時,PL 從通過自定義ip核從BRAM中同樣讀出數據,并通過ILA 來觀察讀出的數據與串口打印的數據是否一致。這里是通過PS寫入進的只有數據信息,而無法讀數據,因此通過ILA觀察數據。
二、實驗整體架構
三、自定義ip寫進ram的控制ip核
? ? ? ? 這個ip核呢是將我們通過AXI輸入進BRAM的數據通過自定義的ip核轉為合適的ram接口信息接到ramip核上,這樣就可以通過ps端輸入的讀使能,讀地址讀數據長度信息轉化為ram的讀使能,讀地址,保證時序正確。
四、設計流程
一、自定義ip核。
1、創建IP管理器
2、選擇AXI接口的ip核,然后一直保持默認最后點擊完成,
3、編輯這個ip,右鍵ip核,然后選擇編輯選項,進入編輯頁面,然后默認ok就行
4、添加一個控制文件(.v)如下
????????為什么要寫這個模塊呢?為什么不能通過ps側輸入讀使能,讀數據直接連接到ram的ip核上呢?在我看來這是因為ps側的邏輯無法精準控制時鐘邏輯,也就是ps側無法做到像pl側的嚴格時序控制。pl側的邏輯是在每個時鐘內做自己的事情,每個時鐘周期都有嚴格的要求,因此我們在PS側無法做到如此嚴格的時序要求。所以通過一個自定義控制模塊對輸入的信號進行處理,使得時序要求滿足我們pl側的邏輯。
module ram_ip
(input pl_rst_n ,input pl_clk ,input ps_read_en ,//開始讀的使能input [31:0] ps_start_addar ,//開始讀的起始地址input [31:0] ps_data_num ,//需要讀取的數量//bram接口output ram_clk ,//RAM時鐘input [31:0] ram_rd_data ,//RAM中讀出的數據,這個接口是讀出來的數據需要我們通過該控制器輸出到ps端。output reg ram_en ,//RAM使能信號,這個接口是讀使能信號,需要根據我們ps側輸入的幾個信息給出合適的讀信號。output reg [31:0] ram_addr ,//RAM地址,這個也需要通過ps側輸入的起始地址和需要讀取的數量來生成的。output reg [3 :0] ram_we ,//RAM讀寫控制信號output reg [31:0] ram_wr_data ,//RAM寫數據output ram_rst //RAM復位信號,高電平有效);assign ram_clk=pl_clk;
assign ram_rst=1'b0;always @(posedge pl_clk)
if(!pl_rst_n)ram_we<=4'd0;
else ram_we<=ram_we;always @(posedge pl_clk)
if(!pl_rst_n)ram_wr_data<=32'd0;
else ram_wr_data<=ram_wr_data;reg ps_read_en_reg;
always @(posedge pl_clk)
if(!pl_rst_n)ps_read_en_reg<=1'b0;
else ps_read_en_reg<=ps_read_en;wire ps_read_en_pos=ps_read_en&~ps_read_en_reg;parameter A_EDA=3'd0,A_ADD=3'd1,A_DOW=3'd2;reg [2:0] state;
always @(posedge pl_clk)if(!pl_rst_n)state<=A_EDA;else case(state)A_EDA:state<=(ps_read_en_pos) ? A_ADD : A_EDA;A_ADD:state<=(ram_addr-ps_start_addar+2'd2==ps_data_num) ? A_DOW : A_ADD;A_DOW:state<=A_EDA;default:state<=A_EDA;endcasealways @(posedge pl_clk)if(!pl_rst_n)ram_en<=1'b0;else case(state)A_EDA:if(ps_read_en_pos) ram_en<=1'b1;A_ADD:ram_en<=1'b1;default:ram_en<=1'b0; endcasealways @(posedge pl_clk)if(!pl_rst_n)ram_addr<=32'd0;else case(state)A_EDA:if(ps_read_en_pos) ram_addr<=ps_start_addar;A_ADD:ram_addr<=ram_addr+1'b1;default:ram_addr<=32'd0;endcaseendmodule
????????上面是我們的需要添加進我們ip核的verilog代碼。下面是systemverilog仿真代碼和仿真圖。由圖可知在我們的pl側受到來自ps的使能和讀數據信息時候,會產生一個高脈沖,然后通過狀態機完成對讀數據信息的轉化,轉化成我們pl側ram接口可以識別的信息,比如讀使能,使能對應的讀地址。由仿真圖也可以看出:當我們的pl側檢測到來自ps側的上升沿時就會立馬進入ram接口時序生成狀態,依據起始地址,讀數量等生成我們需要的接口時序。
`timescale 1ns/1ns
module ram_ip_sim();reg pl_rst_n ;
reg pl_clk ;
reg ps_read_en ;
reg [31:0] ps_start_addar ;
reg [31:0] ps_data_num ;//bram接口
wire ram_clk ;
reg [31:0] ram_rd_data ;
wire ram_en ;
wire [31:0] ram_addr ;
wire [3 :0] ram_we ;
wire [31:0] ram_wr_data ;
wire ram_rst ;initial beginpl_clk<=1'b0;forever #10 pl_clk=~pl_clk;
endinitial beginpl_rst_n=1'b0;repeat(3) @(posedge pl_clk);pl_rst_n=1'b1;
endinitial beginps_read_en =1'b0 ;ps_start_addar =32'd0;ps_data_num =32'd0;ram_rd_data =32'd0;
endtask data_send;repeat(3) @(posedge pl_clk);ps_read_en =1'b1 ;ps_start_addar =32'd0 ;ps_data_num =32'd10;repeat(15) @(posedge pl_clk);ps_read_en =1'b0 ;ps_start_addar =32'd0;ps_data_num =32'd0;endtask : data_send;initial beginwait(pl_rst_n);fork beginrepeat(1) @(posedge pl_clk);data_send;endjoin
endram_ip ram_ip_un
(.pl_rst_n (pl_rst_n ),.pl_clk (pl_clk ),.ps_read_en (ps_read_en ),//開始讀的使能.ps_start_addar (ps_start_addar ),//開始讀的起始地址.ps_data_num (ps_data_num ),//需要讀取的數量.ram_clk (ram_clk ),//RAM時鐘.ram_rd_data (ram_rd_data ),//RAM中讀出的數據,這個接口是讀出來的數據需要我們通過該控制器輸出到ps端。.ram_en (ram_en ),//RAM使能信號,這個接口是讀使能信號,需要根據我們ps側輸入的幾個信息給出合適的讀信號。.ram_addr (ram_addr ),//RAM地址,這個也需要通過ps側輸入的起始地址和需要讀取的數量來生成的。.ram_we (ram_we ),//RAM讀寫控制信號.ram_wr_data (ram_wr_data ),//RAM寫數據.ram_rst (ram_rst ) //RAM復位信號,高電平有效);endmodule
?5、需要修改的地方
在頂層模塊添加輸出端口
在頂層例化的模塊添加例化模塊修改的端口。
在AXI接口模塊添加輸出接口,然后例化我們的bram控制器
在例化我們添加的模塊可知,我們例化模塊的使能信號,我存在寄存器0的第一位,因此我們需要在ps側C語言代碼中操作該寄存器就可以給該bram控制模塊輸出想要的信息,同理我們用到了寄存器1和2分別接收來自ps側的起始地址和讀數據量,然后由該信息就可以讓bram控制模塊輸出滿足時序要求的讀使能端口和讀地址。
6、點擊文件組,這里就是我們添加和修改文件后需要刷新
7、這里是常量刷新(如果你是替換的代碼就先進代碼隨便打個空格然后保存就會刷新)
8、按照如下操作分別將信號接口從時鐘和復位接口移出去ram_rst和ram_clk
9、這里有兩個警告,這是因為我們頂層的輸出端口沒有和其他的ip核端口和形成映射關系,因此這里會有警告不知道對應的接口是哪個,因此我們添加輸出端口映射。
10、添加輸出端口映射。右鍵隨便一個端口,然后點擊add bus interface。
11、先修改我們要對接的端口,這里我們選擇bram端口。然后添加該接口映射的總線名字,然后選擇主端口,因為這個模塊控制的是ram ip的讀數據信息。
12、添加端口映射,點一下需要映射的端口然后點擊映射即可,按照此流程將所有端口映射。這樣做的目的是為了我們軟件可以自動幫我們連線,不需要我們一個一個連。
13、將端口的屬性添加到自適應列。這樣我們連接端口時,端口的信號會自動和從機端口屬性保持一致,這樣不需要我們一個一個的設置。
14、點擊提示信息
15、點擊下面的生成ip核
二、IP組裝
1、先修改我們的最小系統IP,因為在hello_word實驗時將有些端口取消了,這里我們重新添加回來,如下:GP接口和時鐘復位添加進來
2、添加AXI bram控制器,并按照如圖修改
3、添加bram ip核,并按照如圖修改兩個地方
4、添加我們自己寫的ip控制器,不需要修改
5、點擊自動連接,然后全部選中,然后為這兩個ip核的端口設置連接的ip核接口,這樣這兩個ip核的接口就會自動連接到我們的雙端口bram上去,實現一個ip核寫,一個ip核讀。然后點擊確定后點擊重新布局。?
6、驗證設計,然后驗證無誤點擊ok即可。
7、修改ram容量大小,這里可以將兩個都改為4k也可以不改。
8、生成輸出文件,然后ok即可
9、生成頂層文件,這里需要記住的是在原來的hello的工程基礎上的頂層沒有改動,可以不生成,如果是重新建立的工程要生成頂層文件。以下是已經生成過的頂層文件重新生成,默認點擊ok即可
三、添加ILA
1、點擊綜合
2、點擊綜合設計->點擊step up debug
3、next后添加我們要探針的信號,如圖,導航到ram ip核的U0下添加我們要探查的信號
4、為探查信號添加時鐘,搜索選擇ALL_CLOCK,然后選擇下列時鐘后點擊🆗。重復剛才步驟為這三個信號都添加時鐘。然后一直默認點擊ok。
5、然后點擊實現,然后點擊生成bit流
6、導出設計文件,勾選bit流后點擊🆗即可,一定要勾選bit流,因為這里用到了pl端的設計。
7、打開SDK
四、SDK設計
1、新建空項目。
2、建立一個源文件,命名main.c
3、復制如下代碼
#include "stdio.h"
#include "xbram_hw.h"
#include "stdio.h"
#include "custom_pl_bram_ps.h"
#include "xparameters.h"
#include "sleep.h"#define PL_BRAM_START CUSTOM_PL_BRAM_PS_S00_AXI_SLV_REG0_OFFSET//寄存器1輸出使能信號
#define PL_BRAM_START_ADDR CUSTOM_PL_BRAM_PS_S00_AXI_SLV_REG1_OFFSET//寄存器2輸出起始地址
#define PL_BRAM_LEN CUSTOM_PL_BRAM_PS_S00_AXI_SLV_REG2_OFFSET//寄存器3輸出數據長度#define PL_BRAM_BASE XPAR_CUSTOM_PL_BRAM_PS_0_S00_AXI_BASEADDR //自定義ip的地址#define START_ADDAR 0//
#define PL_BRAM_DATA_LEN 4//每個bram的深度為4個字節char input_data[1024]="www.com.lzs" ;
int num_data ;int main()
{//通過調試發現如果沒初值,那會將數據寫入到系統分配的初始地址位置。int wr_cnt=START_ADDAR;printf("test start!");sleep(5);//while(1)//{//scanf("please input data: %s",input_data);num_data=strlen(input_data);for(int i=START_ADDAR*PL_BRAM_DATA_LEN;i<(START_ADDAR+num_data)*PL_BRAM_DATA_LEN;i+=PL_BRAM_DATA_LEN){//第一個參數為寫入AXIip核的地址XBram_WriteReg(XPAR_BRAM_0_BASEADDR,i,input_data[wr_cnt]);printf("w_data:%c,odata : %d\n",input_data[wr_cnt],input_data[wr_cnt]);wr_cnt++;}//}//設備ip,ip映射(寄存器2),寫入數據的bram起始地址。其實就是像我們自定義的ip核里面的AXI寄存器寫入數據。//這個寄存器就是連接我們自己編寫的bram讀控制文件的輸入端口。CUSTOM_PL_BRAM_PS_mWriteReg(PL_BRAM_BASE,PL_BRAM_START_ADDR,START_ADDAR*PL_BRAM_DATA_LEN);//配置長度CUSTOM_PL_BRAM_PS_mWriteReg(PL_BRAM_BASE,PL_BRAM_LEN,(START_ADDAR+num_data)*PL_BRAM_DATA_LEN);//配置使能,讓我們的自定義ip采集上升沿CUSTOM_PL_BRAM_PS_mWriteReg(PL_BRAM_BASE,PL_BRAM_START,0x00000001);CUSTOM_PL_BRAM_PS_mWriteReg(PL_BRAM_BASE,PL_BRAM_START,0x00000000);printf("\naddares,data\n");int read_data;for(int i=START_ADDAR*PL_BRAM_DATA_LEN;i<(START_ADDAR+num_data)*PL_BRAM_DATA_LEN;i+=PL_BRAM_DATA_LEN){read_data=XBram_ReadReg(XPAR_BRAM_0_BASEADDR,i);printf("addares: %d, data: %c, odata : %d\n",i/PL_BRAM_DATA_LEN,read_data,read_data);}return 0;}
4、修改報錯信息,這里是因為系統生成的庫是按照我們自定義ip核名字生成的,如果你是按照我的步驟來的就復制最后修改好的代碼。
5、找到對應的庫文件,然后找到這幾個參數復制改變定義
6、找到這幾個庫文件的函數修改為以下函數
7、修改完后保存,如果你的自定義ip核的命名也為pl_ps_bram就可以直接復制下面的
#include "stdio.h"
#include "xbram_hw.h"
#include "stdio.h"
#include "pl_ps_bram.h"
#include "xparameters.h"
#include "sleep.h"#define PL_BRAM_START PL_PS_BRAM_S00_AXI_SLV_REG0_OFFSET//寄存器1輸出使能信號
#define PL_BRAM_START_ADDR PL_PS_BRAM_S00_AXI_SLV_REG1_OFFSET//寄存器2輸出起始地址
#define PL_BRAM_LEN PL_PS_BRAM_S00_AXI_SLV_REG2_OFFSET//寄存器3輸出數據長度#define PL_BRAM_BASE XPAR_PL_PS_BRAM_0_S00_AXI_BASEADDR //自定義ip的地址#define START_ADDAR 0//
#define PL_BRAM_DATA_LEN 4//每個bram的深度為4個字節char input_data[1024]="www.com.lzs" ;
int num_data ;int main()
{//通過調試發現如果沒初值,那會將數據寫入到系統分配的初始地址位置。int wr_cnt=START_ADDAR;printf("test start!");sleep(5);//while(1)//{//scanf("please input data: %s",input_data);num_data=strlen(input_data);for(int i=START_ADDAR*PL_BRAM_DATA_LEN;i<(START_ADDAR+num_data)*PL_BRAM_DATA_LEN;i+=PL_BRAM_DATA_LEN){//第一個參數為寫入AXIip核的地址XBram_WriteReg(XPAR_BRAM_0_BASEADDR,i,input_data[wr_cnt]);printf("w_data:%c,odata : %d\n",input_data[wr_cnt],input_data[wr_cnt]);wr_cnt++;}//}//設備ip,ip映射(寄存器2),寫入數據的bram起始地址。其實就是像我們自定義的ip核里面的AXI寄存器寫入數據。//這個寄存器就是連接我們自己編寫的bram讀控制文件的輸入端口。PL_PS_BRAM_mWriteReg(PL_BRAM_BASE,PL_BRAM_START_ADDR,START_ADDAR*PL_BRAM_DATA_LEN);//配置長度PL_PS_BRAM_mWriteReg(PL_BRAM_BASE,PL_BRAM_LEN,(START_ADDAR+num_data)*PL_BRAM_DATA_LEN);//配置使能,讓我們的自定義ip采集上升沿PL_PS_BRAM_mWriteReg(PL_BRAM_BASE,PL_BRAM_START,0x00000001);PL_PS_BRAM_mWriteReg(PL_BRAM_BASE,PL_BRAM_START,0x00000000);printf("\naddares,data\n");int read_data;for(int i=START_ADDAR*PL_BRAM_DATA_LEN;i<(START_ADDAR+num_data)*PL_BRAM_DATA_LEN;i+=PL_BRAM_DATA_LEN){read_data=XBram_ReadReg(XPAR_BRAM_0_BASEADDR,i);printf("addares: %d, data: %c, odata : %d\n",i/PL_BRAM_DATA_LEN,read_data,read_data);}return 0;}
五、SDK驗證
1、打開板子供電、連接串口
2、右鍵項目,打開運行配置
3、雙擊GDB,打開燒錄bit流和復位
4、點擊運行
5、我們發現AXIbram控制器ip核讀寫一致
六、ILA驗證
????????這里我們需要注意,由于我的設計用不了scanf輸入函數,因此我加了一個延遲然后馬上回到ILA界面運行,等待enb拉高,觸發邏輯分析儀。如果你的scanf能用就不用加延遲,就可以通過串口輸入寫入信息后,SDK程序會立馬運行到拉高enb的信號函數。
還有一點就是如果你調不出ILA界面請點擊以下鏈接,查看原因和解決方法
vivado 下載程序后沒有ILA界面
1、點擊設備管理器后點擊自動連接,進入ila界面
2、設置觸發條件,選擇enb,選擇上升沿觸發。
3、回到SDK燒錄界面,燒錄后馬上回到ILA界面點擊運行。這里一定要馬上回到這個界面點運行,然后等待觸發。前面說過如果你的scanf能用就可以在SDK 終端輸入后這里就會觸發。而這里由于我的scanf用不了,我就加了個延遲,等幾秒鐘后會按順序執行到SDK設置上升沿的使能程序。然后ILA界面就會觸發,如下圖
4、對比寫入數據。將數據以ASCLL碼的形式展現
5、對比發現我們邏輯分析儀捕捉到的自定義ip核讀數據轉為ASCLL碼后和我們SDK PS側寫進PS側RAM的數據是一致的。因此我們的設計正確。
?
?
?
?