Vitis HLS experiment — AxiStream single DMA
前言
Vitis HLS experiment — AxiStream single DMA
前言
趁著手上案子階段性驗證報告都丟出去之餘,抽空寫些東西,不然之後有陣子應該會非常忙🥲
另外 Vitis HLS 應該會跟 Vivado 介紹一樣,變成是獨立系列,所以前面不會有編號。
想要開這個專欄是因為受到遠在美國某位強者朋友的推薦及鼓勵下,想說來試試看這玩意兒,也很高興有去試玩這東西,從中發現了很多機會和有趣東西,同時也對 AMD FPGA 更著迷。當然也是怕自己忘記做過甚麼事情,所以先記錄下來。
因為是全程盡可能詳細演示,所以篇幅會非常巨大,還請見諒。
板子:
zcu104
軟體:
vivado 2023.1、vscode、vitis 2023.1、vitis hls 2023.1(之後應該會將個人端更新到2024.1發現有蠻多酷酷的新東西)
建議參考:
ug1399 Vitis High-Level Synthesis User Guide 之前還有看了一下 ug998 vivado high level synthesis,但基本上可以不用看了,因為他比較偏向完全從 vivado 角度切入,而且 vivado high level synthesis 已經完全被 vitis hls 取代了,1399也包了998大部分內容,但如果對 FPGA 是甚麼998也有介紹。
另外範本都是照強者朋友某堂課的 lab 實作,一步一步去了解這麼使用。
lab 連結
Parallel Programming for FPGAs: Projects and Labs — pp4fpgas 0.0.1 documentation
Vitis HLS Intro
簡單說就是因為傳統 RTL 描述算法運作太過複雜,於是業界和學界長年以來一直有一群大佬致力於研發將 Cpp 刻好的 behavior model 透過 compiler 無痛轉成 RTL ,後續流程就是原先硬體 flow 一樣合成產出 netlist/bistream 等等。
但我也不是這方面專家,所以相關如何映射,他們怎麼映射,煩請各路自行去搜尋。而這裡介紹比較偏向如何使用,後續如果對這個 eda tool 有更深體悟會持續更新。
另外 xilinx 也提供不用跟 hls/vivado 互動,直接透過 vitis 的 v++ compile 出能直接和OpenCL內核或硬體平台上執行的加速器,運行在類似 alveo 25/ alveo 50 硬體平台這些子卡上面做硬體加速,這個應用目前有聽過就是高頻交易,但似乎最近 AMD 的 xilinx 團隊很努力在推這個,我看他們都快要把傳統 FPGA 設計給廢掉。
不過這條 flow 我完全沒碰過,有興趣讀者可以自己去嘗試看看玩看看,或者之後我有時間試著打通看看這條 flow (alveo 50 可不便宜,只能偷閒在公司試玩),再來分享。
小小提下,不是所有 hls compiler 都長得一樣,大家有興趣可以去試一下 verilator 這個開源工具做出來相對高階 testbench 的 source code,會發現大方向差不多,但是一些語法和操控細節蠻不一樣的。
這裡可以舉一個非常明顯差別就是對 clock 的控制,verilator 對 clock 可以做到很細的控制,透過逐行更新方式去更新目前 clock 正負緣,但在 vitis hls 頂多讓你去調 HLS 運算要運行在多少頻率底下。
不過以上都是我粗淺試驗得出來的小小體悟,除了上述兩種工具,也沒使用過其他的,如果有這方面大佬發現我說的不對,歡迎來信指正。
Start your first HLS project
因為我也只是一個硬體菜鳥工程師,最終還是關注硬體,所以介紹都是跟硬體設計 flow 相關。
在開始前,先簡述 flow
- 打開 Vitis HLS 寫 high level synthesis model → C simulation → C synthesis → export RTL 導出IP
- 打開 Vivado 找到 Vitis hls 合出來的 IP,並開始兜硬體,導出 .xsa
- 打開 Vitis 開始寫 application 應用並燒入 FPGA debug
vivado 和 vitis 如果有翻過我之前文章的讀者,應該知道怎麼開始,所以這篇會側重在 HLS 遇到的問題和使用。
Vitis HLS Design
第一步就是打開 vitis hls,一開始建立頁面會叫你給 solution 取名,你沒取就是 solution1 這樣,打開之後在左上角會看到下面 explorer,其中 Source 和 Testbench 都會沒有東西,Solution1 那個也不會打勾

老樣子,右鍵 New Source file 然後先照著課上 lab 簡單設計,將每個輸入資料都 * 2。
這邊提醒各位,你要做 testbench 就是要多寫 headerfile ,不然有時候 c simulation 就是不會過,而且這也是寫設計好習慣。至於要寫 C 還是 C++ 隨便各位。
這邊吐槽一下,HLS 的介面很陽春,所以想當然我一定先在 preference 去把 vscode 加進來變成 default 開啟,結果一直失效。
之後還發現 project 會用到所有 library 都是在 .include 隱藏檔裡面,要加進來還費了一番功夫,最後還出現大部分情況 vscode 沒辦法和 HLS 同步,所以這算是我第一次 T 到鐵板,乖乖使用 HLS 原生編輯器。
不過也可能是我太廢了 😢,對於 JSON 和入庫還有很多沒摸透的地方。
如果有人知道怎麼解決 Vitis HLS 第三方開啟問題,請大方來信,不然看到那個編輯環境,vscode 戒斷症都起來了。
#include "ap_axi_sdata.h"
#include "hls_stream.h"
#include "streamAdd.h"
// 底下的 trank_pkt data format 在 .h 裡面定義
void smul(hls::stream< trans_pkt > &INPUT, hls::stream< trans_pkt > &OUTPUT)
{
// 將控制線映射成 aix_lite,應該知道要做什麼了吧。
// return 一般是返回這個函數計算完的結果,這邊可以看作要回傳函數狀態給處理器
// 同時有這個 interface 也可以透過處理器給這個函數下指令
// 至於後面的 bundle 會將 return 可能出來 port 合在一起,然後我這邊取名叫做 CTRL
// 所以合完的 block 看到這個 port 變成叫做 axi_CTRL 或 axi_lite_CTRL
#pragma HLS INTERFACE s_axilite port = return bundle = CTRL
// 然後這個 pragma 也是拿來決定這邊要不要做硬體 loop,硬體 pipeline 的語法,
// 基本上我的體悟 HLS 最大核心應該都在這上面了,如何用 pragma 做出最佳化硬體合成
// 但小的慚愧,還沒融會貫通,這方面可能有機會再來請教美國那位強者朋友
// 所以本篇最主要還是會放在 flow 的打通
// 將 INPUT stream 映射成 axis
#pragma HLS INTERFACE axis port=INPUT
// 將 OUTPUT stream 映射成 axis
#pragma HLS INTERFACE axis port=OUTPUT
// 將 .h 設好 type 拿來使用
trans_pkt data_p;
// 用 INPUT stream 將 trasn_pkt 的東西讀出來
INPUT.read(data_p);
// 只將 trans_pkt 裡面的 data 做 * 2 其他不管
data_p.data *= 2;
// 寫到 OUTPUT stream
OUTPUT.write(data_p);
}
這時候各位心中應該都有疑問,要怎麼知道傳入函數的 type 是 reference 還是 pointer 還是 sclae varibale?這個解答請打開 ug1399,因為我們是 IPI package flow,直接翻到249頁,下面有個表格,有寫這些 type 會被映射成甚麼電路,甚至持續往下讀還要告訴你 interface adapter 要用什麼 type 映射。

因為這個範例想要直接往 HLS 傳 stream,所以直接用 hls::stream 的 type,要用這個 type 要先引入 hls_stream.h ,如果要調整 stream data 大小就要在引入 ap_axi_sdata.h。
儘量也將 stream 格式的東西寫在 header file ,雖然寫在 design.cpp 沒差,但就在 maintain 上非常差,建議就是都丟在header file。
另外 ap_axiu 和 ap_axis 差別就只是無號和有號差別。各位可以自己打開 ap_axi_sdata.h 來看結構定義,一條 steram 是 uint 另一條是 int。
下面是 header file 範例
#ifndef SMUL_H
#define SMUL_H
#include "ap_axi_sdata.h"
#include "hls_stream.h"
// Define AXI Stream Data format
typedef ap_axiu<32, 0, 0, 0> trans_pkt; // Data width, User width, ID width, Dest width
// Function declare
void smul(hls::stream<trans_pkt> &INPUT, hls::stream<trans_pkt> &OUTPUT);
#endif // SMUL_H
再來就是寫 testbench,基本上就是 main function ,就隨便寫一個 1 到 10 回圈測試。
#include "streamAdd.h"
#include <iostream>
using namespace std;
int main() {
hls::stream<trans_pkt> input_stream;
hls::stream<trans_pkt> output_stream;
trans_pkt input_data;
trans_pkt output_data;
for(int i=0; i<10; ++i){
// 底下這些東西可以從 ap_axi_sdata.h 找到
input_data.data = i + 1;
input_data.keep = -1;
input_data.strb = -1;
input_data.user = 0;
input_data.id = 0;
input_data.dest = 0;
input_stream.write(input_data);
smul(input_stream, output_stream);
if (!output_stream.empty()) {
output_data = output_stream.read();
cout << "Input Data: " << input_data.data
<< ", Output Data: " << output_data.data;
cout << "\n";
} else {
cout << "Output Stream is empty!\n";
}
}
return 0;
}
上面 data, user 的摸板都可以在相對應 header file 裡面找到,就知道 testbecnch 可以控制甚麼東西

Vitis HLS C simulation
這步最主要是為了驗證寫的 behavior 是不是正確的,但只會做行為層面的驗證,而且幾乎都是軟體思維驗證。
來到這邊按 C Simulation,會出現底下視窗,稍為解釋下各個功能


- Launch Debugger: 他其實就是 vitis debug 的 hls behavior 版,但我覺得沒有很好用,因為畢竟不是 debug 硬體,很多記憶體位置甚麼什麼都很抽象,可能也是我不熟悉,有時間挖到他的殺手級應用,再來詳細介紹XDD。單步執行一樣是 F5,F6,但是沒有F7 跳到 return 處功能,再來他debug是根據 testbench mainfunction debug。 如果要退出 debug mode 的話就是綠色欄位上排最右邊的按紐。

- Build only: 這個是只對設計 cpp 做 compile,不做 testbench 打資料的動作。白話一點,如果選了這個,你寫的 main function 就沒有用,他單純只會看你寫的東西會不會 compile 過。如下,你的 main fucntion 東西都不會 load 進來。

- Clean Build: 他會自動把先前(注意是先前) complile 和 simulation 的結果清掉,重建一個 compile 檔案和 simulation 檔案。
- Optimizing Compile: 會對你的 project 做優化處理,可能會改變程式的執行邏輯,之前有跟FAE聊過,他可能會導致你程式預留一些變數要做debug,這些變數會被優化掉不見,不過我自己是沒用過這個選項啦,如果 compile 大師或者設計大師,可以來信跟我說下他的運作原理。
- Enable Pre-syn control flow viewer: 沒選過,不知道實際用途
- Input Arguments: 一般就是一些你自己設定在 main function 的傳入參數,我自己也是沒用過,都直接在 main function 直接做。
通常最多就是按 Clean build 重跑或 launch debug,在不然就是什麼都不選,直接按OK,也會開始跑。
然後這邊.log 刷新會在 solution1 → csim → report 底下,他自動跳出來的.log 一般都是第一次做 csim 的.log,之後被刷新都要自己手動到 report 點開,有點反智。
這邊也要吐槽一下,每次重做 c-sim,第一次.log都會跳出來,有點奇怪,除非到資料夾把第一次 .log 刪掉,不然就是會跳出來。

不過也可以直接看 console 確認

這邊插嘴一下,做完 C simulation 只能確保你寫的軟體沒錯,不代表說他能合成,這上面的東西你甚至都可以隨便開個編輯器改一下就能顯示結果,另外 vitis HLS 目前不支援合 recursive 。
關於 recursive 結尾分享下前陣子聽訓練講座總結出的小心得。
接下來要進到 C synthesis 了…
我知道很辛苦,但要撐住阿@@
Vitis HLS C synthesis
接下來就是比較核心 C synthesis ,這步最主要是要產出 RTL 合出硬體。
但首先要確定的是 project setting 裡面的 top function 是不是你的函數名稱。從這邊進去 project settings,另外一個 solutions settings 可以做些 Interface/behavior 的處理。

通常這邊 Top function一般不會是你的函數名稱,一開始可能會是檔案名稱,但因為我還沒下載2024.1 試試看,不知道這個不智能的東西被修掉了沒有。
接著點紅色的位置,去找到你的 function 名字,例如這邊就叫做 smul

這時候可能大家又有疑問,設計不都寫一個為什麼需要選。我一開始也有這個疑問,但當我試著在 HLS 把函數東包西包,最後恍然大悟,原來是要讓我們做小 module 驗證用啊。當然最後導出一定是一大包 top function。
按完 OK,Top function 也確定沒問題就繼續按 project settings 的 OK

接著到剛剛按 C simulation 的地方,這次要按 C synthesis,按下去會出現下面視窗。

基本上 period 就是你的 HLS function 要跑多快,我這邊就給他跑100Mhz,uncetainty 就是時序抖動/skew等,隨便抓個 100ps(也可以不要)。再來確定你的 part 有沒有選對,有一次開 project開的太快沒注意選 part,結果合完,在 vivado 抓不到 IP,就是這個 FPGA device part 沒有對應到。
這邊題外話,當然也是跟前輩學到經驗談,一般 100MHz 以下都會抓個 100ps, 200ps 的 uncertainty。
如果是高速鏈路進來 IP core,例如 Aurora 什麼的會給系統抓 50ps 左右,比較嚴格一點,主要是確認 IP 這麼高速傳輸,理論上你就要用比較嚴格標準去看審視他資料傳輸的完整性,不然他就是一個數學算很快但都算錯的大學生。
然後這裡要養成一個好習慣,默認單位雖然都是 ns,但還是請各位高抬貴手,把 ns 打上去。
底下的 flow target 除了 IPI flow 之外,還有給純 vitis 用的 kernal flow 但這就不是我的專長了 🫠

這時候頁面會跑出這些亂七八糟的資訊,這個頁面是.rpt檔案,會放在 solution1 → syn → report 裡面。然後左邊 explorer 裡面 syn 的資料夾會有合出來 RTL 如果你是 RTL 大師也可以從裡面看他合的硬體寫法是甚麼。
基本上會有兩包,一包是合成總結,包含你用什麼pragma,interface,時序等等,基本就是 block 最上面看到的東西,至於另一包.rpt 是更詳細合成資訊,會有用了幾個乘法器,幾個加發器等資訊,甚至有底下這個令人興奮的東西,硬體映射表。

然後兩分.rpt都支援轉出 html ,你各位碩士生如果做相關主題可以不用愁報告怎麼生的漂亮了。
基本上做到這步,沒有什麼太嚴重的 slack 或是需要割 pipeline 地方,就沒問題了。
接下來是就可以進到 C/RTL co-simulation
Vitis HLS C/RTL Co-simulation
這部分其實見仁見智,如果設計夠複雜,建議做一次 co-sim,以防軟體層面驗證可以過,但硬體其實不能這樣做。
核心思想將軟體層面驗證,在 RTL 部分再驗證一次,所以你的 C testbench 給甚麼 pattern,那這邊也是給甚麼 RTL pattern
一樣到 C simulation 那個地方按往下照 co-simulation,一定要做完 c syn,有 RTL 才會有這個選項,一樣也簡述以下功能。

第一個下拉式選單就是選擇模擬器,有 vivado XSIM、Modelsim、Riviera這三個可以選,
在來是 dump trace 可以選 all 全部訊號含內部或者你只要看 port 就好了,然後可以就可以直接按 OK,一般都選 port 就好了,不然跑太久。
其他我就沒用過了,最多就點過底下 wave debug 他就只是會自動跳出 vivado 跑出 simulation 的波型結果。
如果沒勾的話也可以在紅圈處把 vivado 波型叫出來,當然要跑完 co-sim 才能點他,但是一定要選 dump trance 沒有 dump file 就沒有波型圖,相信這個是常識吧?

波型圖大概如下,但基本上我不會叫出來看。

一般更在意會是 report / .log 等等,尤其這邊 status: Pass 代表說硬體 RTL 沒有問題,出來的結果和 C 一樣。


還有這種每次計算的 latency 等等。

這些報告都會在 sim(注意不是 c sim 喔) → report
前面這些準備工作都做完,就可以開始導出設計了。
Vitis HLS Export RTL
這步就是產生 HLS IP 了,產完 vivado 導入 ip 庫就可以拿來用,一樣在 c simulation 那個地方倒數第二個 export RTL。
點下去會出下面視窗,output location 默認是沒有的,這邊就隨便選,只要確保在 vivado 你能找到就沒問題。

再來下面這些 XDC constraint file ,不知道各位讀者平常用 xilinx IP 多不多,用的多會發現 xilinx IP 本身都有包屬於這個 IP 的 constratin,OOC 比較會是 IP 裡面小模塊 constraint, XDC 會是整個 IP constraint。如果不知道這點,下次在 vivado 叫大量 IP 出來用可以留意一下。
選好位置按 OK 就好了。
C:\…\project\project\project\impl\ip 通常導出 IP 文件都在這個 HLS project 路徑,裡面包含 driver/make file/.xml 什麼你想得到都有。
然後這步其實和 impl 都能導出 IP ,差別在於這個 impl 生出來可以直接燒在 FPGA 用的 bistream,這個比較偏向 kernel flow 設計,仰賴本身 FPGA 內部硬體平台。
然後 impl 和 expotr RTL 這兩包東西在包完都會變成壓縮檔,都可以傳給其他人做引用和 co-work。
那如果你喜歡是 vivado 參與到你的設計裡頭,並且自己在 vitis 搭建硬體平台,就是做到 Export RTL 就好了。
順帶一下,在 HLS 做 impl 會非常花時間,所以除非你知道你要做什麼,不然我覺得 export RTL 然後到 vivado 建自己的 hw platfrom 就好了。
所以本篇就先把 impl 給跳過了。不過還是稍為秀一下 impl 會出甚麼東西。
基本告訴你會用這個 FPGA 的甚麼到多少趴

一些時序報告

以上沒問題,我們就準備轉場到 vivado。
Vivado
老朋友了,BD 打開,老流程直上,load IP 庫,拉 IP 拉 zynq 連一連,不會的話,就翻翻我之前寫的,裡面都有紀錄怎麼做,再不會的話也可以來信討論。
原諒我偷懶用 zynq,講述一下基本流程,
zynq 往開發板上的 DDR 寫入資料,然後 DMA 去那個位置搬運資料給 HLS 最後在 HLS 算完傳給 DMA,DMA 再存入 DDR。

擔心有人對流程很抽象,這邊我畫一下流程架構。DMA 只是拿來做般運的動作(目的就是不要給 CPU 搬資料,浪費 CPU 資源),然後將 AXI Memroy mapped 轉成 AXI stream 流給 HLS 跟 HLS 流進去 stream 轉回去給 CPU。

中間標有記憶體位置是DDR
沒問題就直接 Generate bisteram 導出 .xsa 檔案。
接下來就迅速轉場到 vitis
Vitis
也是老朋友,導入 .xsa 建立 hw platform,就開始寫 embedded code
然後這邊介紹一下,包的 driver 跟在 vivado 包出來 IP 會差很多,基本上 xilinx 在 HLS 包的 IP 會提供很高層次的操控實例和低層次 register 操控 hw.h,那就看你對哪個熟悉。

下面我兩個版本都有提供,一個是對底層 32 bit register 控制,一個高層次 C 語言控制。如果你不知道底層 32-bit register 控制 command 要給什麼,點開 _hw.h 就都寫在裡面。
題外話,下面寫 code 的方式非常不好,IP function 控制最好就是包 header file 出去,main fucntion 裡面就留一些重要的處理或計算就好。會這樣寫只是懶惰,好講解。
另外練習寫 driver 包成高層次 header file 是個很好訓練,現在我也儘量練習自己去寫好用 driver,因為我也非 firmware 出生,所以也還在摸索中。
- Register 控制
#include "xparameters.h"
#include "xil_io.h"
#include "xil_printf.h"
#include "xaxidma.h"
#include "sleep.h"
/***** Define ******/
// Device addr
#define DMA_DEV_ID XPAR_AXI_DMA_0_DEVICE_ID
#define DMA_BASE_ADDR XPAR_AXI_DMA_0_BASEADDR
#define SMUL_BASE_ADDR XPAR_SMUL_0_S_AXI_CTRL_BASEADDR
// PS DDR addr
#define MEM_BASE_ADDR XPAR_PSU_DDR_0_S_AXI_BASEADDR
#define INPUT_BUFFER (MEM_BASE_ADDR + 0x00100000)
#define OUTPUT_BUFFER (MEM_BASE_ADDR + 0x00300000)
// HLS IP Register Offsets
// 0x0 : Control signals
// bit 0 - ap_start (Read/Write/COH)
// bit 1 - ap_done (Read/COR)
// bit 2 - ap_idle (Read)
// bit 3 - ap_ready (Read/COR)
// bit 7 - auto_restart (Read/Write)
// bit 9 - interrupt (Read)
// others - reserved
#define CTRL_ADDR_AP_CTRL (SMUL_BASE_ADDR + 0x0)
#define CTRL_ADDR_GIE (SMUL_BASE_ADDR + 0x4)
#define CTRL_ADDR_IER (SMUL_BASE_ADDR + 0x8)
#define CTRL_ADDR_ISR (SMUL_BASE_ADDR + 0xc)
/***** HLS interactive data size ******/
#define DATA_SIZE 20
/***** Function prototype *****/
// Vitis hls ip function
void smul_ip_start(); // Start the hls ip and restart
void smul_ip_status();
void smul_ip_stop();
// Axi Dma control
XStatus DmaSetup(XAxiDma *DmaInsPtr);
int DmaTransfer(XAxiDma *DmaInsPtr, u32 *input_buffer, u32 *output_buffer, int data_size);
void smul_ip_start() {
xil_printf("HLS IP Start\r\n");
Xil_Out32(CTRL_ADDR_AP_CTRL, 0x81);
xil_printf("HLS IP has completed the computation and is ready for next operation.\r\n");
}
void smul_ip_status(){
u32 HLS_STATUS = Xil_In32(CTRL_ADDR_AP_CTRL);
uint8_t ap_done = (HLS_STATUS >> 1) & 0x1; // bit 1
uint8_t ap_idle = (HLS_STATUS >> 2) & 0x1; // bit 2
uint8_t ap_ready = (HLS_STATUS >> 3) & 0x1; // bit 3
uint8_t combined_status = (ap_done << 2) | (ap_idle << 1) | ap_ready;
switch (combined_status) {
case 0b000: // ap_done = 0, ap_idle = 0, ap_ready = 0
xil_printf("HLS IP status: RUNNING (not done, not idle, not ready)\r\n");
break;
case 0b001: // ap_done = 0, ap_idle = 0, ap_ready = 1
xil_printf("HLS IP status: READY but NOT IDLE, NOT DONE\r\n");
break;
case 0b010: // ap_done = 0, ap_idle = 1, ap_ready = 0
xil_printf("HLS IP status: IDLE but NOT DONE\r\n");
break;
case 0b011: // ap_done = 0, ap_idle = 1, ap_ready = 1
xil_printf("HLS IP status: READY and IDLE, NOT DONE\r\n");
break;
case 0b100: // ap_done = 1, ap_idle = 0, ap_ready = 0
xil_printf("HLS IP status: DONE but NOT IDLE, NOT READY\r\n");
break;
case 0b101: // ap_done = 1, ap_idle = 0, ap_ready = 1
xil_printf("HLS IP status: DONE and READY, NOT IDLE\r\n");
break;
case 0b110: // ap_done = 1, ap_idle = 1, ap_ready = 0
xil_printf("HLS IP status: DONE and IDLE, NOT READY\r\n");
break;
case 0b111: // ap_done = 1, ap_idle = 1, ap_ready = 1
xil_printf("HLS IP status: DONE, IDLE, and READY (Fully Complete)\r\n");
break;
default:
xil_printf("Unknown HLS IP status.\r\n");
break;
}
}
void smul_ip_stop(){
xil_printf("HLS IP Stop\r\n");
Xil_Out32(CTRL_ADDR_AP_CTRL, 0x0);
}
XStatus DmaSetup(XAxiDma *DmaInsPtr){
XAxiDma_Config *DmaCfg;
int Status;
DmaCfg = XAxiDma_LookupConfig(DMA_DEV_ID);
if(!DmaCfg){
xil_printf("Dma dev not found!\r\n");
return XST_FAILURE;
}
Status = XAxiDma_CfgInitialize(DmaInsPtr, DmaCfg);
if(Status != XST_SUCCESS){
xil_printf("Dma configuration failed!\r\n");
return XST_FAILURE;
}
// Make sure scatter-gather mode is disabled
if(XAxiDma_HasSg(DmaInsPtr)){
return XST_FAILURE;
}
return XST_SUCCESS;
}
// Dma transfer
int DmaTransfer(XAxiDma *DmaInsPtr, u32 *input_buffer, u32 *output_buffer, int data_size){
int Status;
int TimeOut = 1000000; // Time threshold
// 這個我要稍為講一下
// 不管今天要不要用 dma 搬 DDR 資料,就算你只用 CPU 對 DDR 進行讀寫,也都要做下 data cache flush
// 相關發生甚麼事情,我貼在兩段程式碼後
Xil_DCacheFlushRange((UINTPTR)input_buffer, data_size * sizeof(u32));
Xil_DCacheFlushRange((UINTPTR)output_buffer, data_size * sizeof(u32));
Status = XAxiDma_SimpleTransfer(DmaInsPtr, (UINTPTR)input_buffer, data_size * sizeof(u32), XAXIDMA_DMA_TO_DEVICE);
if (Status != XST_SUCCESS) {
xil_printf("DMA transfer to device failed!\r\n");
return XST_FAILURE;
}
Status = XAxiDma_SimpleTransfer(DmaInsPtr, (UINTPTR)output_buffer, data_size * sizeof(u32), XAXIDMA_DEVICE_TO_DMA);
if (Status != XST_SUCCESS) {
xil_printf("DMA transfer to device failed!\r\n");
return XST_FAILURE;
}
while (TimeOut) {
if (!(XAxiDma_Busy(DmaInsPtr, XAXIDMA_DEVICE_TO_DMA)) && !(XAxiDma_Busy(DmaInsPtr, XAXIDMA_DMA_TO_DEVICE))) break;
TimeOut--;
usleep(1U);
}
if (TimeOut == 0) {
xil_printf("DMA transfer timed out!\r\n");
return XST_FAILURE;
}
// 這邊一樣,放在兩段程式碼後講解
Xil_DCacheInvalidateRange((UINTPTR)output_buffer, data_size * sizeof(u32));
return XST_SUCCESS;
}
int main(){
XAxiDma DmaInst;
u32* input_buffer = (u32*)INPUT_BUFFER;
u32* output_buffer = (u32*)OUTPUT_BUFFER;
if(DmaSetup(&DmaInst) != XST_SUCCESS){
xil_printf("Failed to initialize Dma!\r\n");
return XST_FAILURE;
}
for(int i=0; i<DATA_SIZE; i++){
input_buffer[i] = i;
}
xil_printf("\r\n");
smul_ip_status();
xil_printf("\r\n");
smul_ip_start();
xil_printf("\r\n");
smul_ip_status();
xil_printf("\r\n");
if(DmaTransfer(&DmaInst, input_buffer, output_buffer, DATA_SIZE) != XST_SUCCESS){
xil_printf("Dma Transefer failed!\r\n");
return XST_FAILURE;
}
for(int i=0; i<DATA_SIZE; ++i){
xil_printf("Input: %d, Output: %d\r\n", input_buffer[i], output_buffer[i]);
}
smul_ip_stop();
return XST_SUCCESS;
}
- 高層次 C 語言控制
#include "xparameters.h"
#include "xil_io.h"
#include "xil_printf.h"
#include "xaxidma.h"
#include "sleep.h"
#include "xsmul.h"
/***** Define ******/
// Device addr
#define DMA_DEV_ID XPAR_AXI_DMA_0_DEVICE_ID
#define SMUL_DEV_ID XPAR_SMUL_0_DEVICE_ID
#define DMA_BASE_ADDR XPAR_AXI_DMA_0_BASEADDR
#define SMUL_BASE_ADDR XPAR_SMUL_0_S_AXI_CTRL_BASEADDR
// PS DDR addr
#define MEM_BASE_ADDR XPAR_PSU_DDR_0_S_AXI_BASEADDR
#define INPUT_BUFFER (MEM_BASE_ADDR + 0x00100000)
#define OUTPUT_BUFFER (MEM_BASE_ADDR + 0x00300000)
/***** HLS interactive data size ******/
#define DATA_SIZE 20
/***** Function prototype *****/
XStatus smul_start(XSmul *SmulInst);
// Axi Dma control
XStatus DmaSetup(XAxiDma *DmaInsPtr);
int DmaTransfer(XAxiDma *DmaInsPtr, u32 *input_buffer, u32 *output_buffer, int data_size);
XStatus smul_start(XSmul *SmulInst){
XSmul_Config *SmulCfg;
int Status;
SmulCfg = XSmul_LookupConfig(SMUL_DEV_ID);
if(!SmulCfg){
xil_printf("Smul dev not found!\r\n");
return XST_FAILURE;
}
Status = XSmul_CfgInitialize(SmulInst, SmulCfg);
if(Status != XST_SUCCESS){
xil_printf("Smul configuration failed!\r\n");
return XST_FAILURE;
}
XSmul_Start(SmulInst);
XSmul_EnableAutoRestart(SmulInst);
return XST_SUCCESS;
};
XStatus DmaSetup(XAxiDma *DmaInsPtr){
XAxiDma_Config *DmaCfg;
int Status;
DmaCfg = XAxiDma_LookupConfig(DMA_DEV_ID);
if(!DmaCfg){
xil_printf("Dma dev not found!\r\n");
return XST_FAILURE;
}
Status = XAxiDma_CfgInitialize(DmaInsPtr, DmaCfg);
if(Status != XST_SUCCESS){
xil_printf("Dma configuration failed!\r\n");
return XST_FAILURE;
}
// Make sure scatter-gather mode is disabled
if(XAxiDma_HasSg(DmaInsPtr)){
return XST_FAILURE;
}
return XST_SUCCESS;
}
// Dma transfer
int DmaTransfer(XAxiDma *DmaInsPtr, u32 *input_buffer, u32 *output_buffer, int data_size){
int Status;
int TimeOut = 1000000; // Time threshold
Xil_DCacheFlushRange((UINTPTR)input_buffer, data_size * sizeof(u32));
Xil_DCacheFlushRange((UINTPTR)output_buffer, data_size * sizeof(u32));
Status = XAxiDma_SimpleTransfer(DmaInsPtr, (UINTPTR)input_buffer, data_size * sizeof(u32), XAXIDMA_DMA_TO_DEVICE);
if (Status != XST_SUCCESS) {
xil_printf("DMA transfer to device failed!\r\n");
return XST_FAILURE;
}
Status = XAxiDma_SimpleTransfer(DmaInsPtr, (UINTPTR)output_buffer, data_size * sizeof(u32), XAXIDMA_DEVICE_TO_DMA);
if (Status != XST_SUCCESS) {
xil_printf("DMA transfer to device failed!\r\n");
return XST_FAILURE;
}
while (TimeOut) {
if (!(XAxiDma_Busy(DmaInsPtr, XAXIDMA_DEVICE_TO_DMA)) && !(XAxiDma_Busy(DmaInsPtr, XAXIDMA_DMA_TO_DEVICE))) break;
TimeOut--;
usleep(1U);
}
if (TimeOut == 0) {
xil_printf("DMA transfer timed out!\r\n");
return XST_FAILURE;
}
Xil_DCacheInvalidateRange((UINTPTR)output_buffer, data_size * sizeof(u32));
return XST_SUCCESS;
}
int main(){
XSmul SmulInst;
XAxiDma DmaInst;
u32* input_buffer = (u32*)INPUT_BUFFER;
u32* output_buffer = (u32*)OUTPUT_BUFFER;
if(DmaSetup(&DmaInst) != XST_SUCCESS){
xil_printf("Failed to initialize Dma!\r\n");
return XST_FAILURE;
}
for(int i=0; i<DATA_SIZE; i++){
input_buffer[i] = i;
}
if(smul_start(&SmulInst) != XST_SUCCESS){
xil_printf("Failed to start smul!\r\n");
return XST_FAILURE;
}
if(DmaTransfer(&DmaInst, input_buffer, output_buffer, DATA_SIZE) != XST_SUCCESS){
xil_printf("Dma Transefer failed!\r\n");
return XST_FAILURE;
}
for(int i=0; i<DATA_SIZE; ++i){
xil_printf("Input: %d, Output: %d\r\n", input_buffer[i], output_buffer[i]);
}
XSmul_DisableAutoRestart(&SmulInst);
return XST_SUCCESS;
}
這邊解釋一下 Xil_DCacheFlushRange 和 Xil_DCacheInvalidateRange,當然這是屬於 zynq 的函數,不同硬體丟到 vitis 可能有不同的 Dcacheflush 和 Icacheflush 函數,這部分我也還在持續學習中。
需要這兩個函數原因如下
- DCacheFlushRange: 假設場景 Processor Cache → DDR 流向,我們先假設不做 flush
Processor Cache (最新數據): [1, 2, 3, 4]
DDR Memory (舊數據): [0, 0, 0, 0]
而此時 DMA 從 DDR 讀資料可能就會讀到舊數據,原因是 CPU 再寫資料一定是先寫到 Cache 再寫到 DDR 或其他設備,不會一口氣往 DDR 寫,這中間擔心就是處理器做的修改沒有流到 DDR 裡面。
所以保險做法就是處理器寫完資料或者 DMA 要搬 DDR 資料前,對 DDR 這個位置進行刷新。當然也有可能壞事不會發生,所以說這是一層保險,防止資料沒有 load 到DDR 導致 DMA 沒有抓到資料。
Processor Cache: [1, 2, 3, 4]
DDR Memory (最新數據): [1, 2, 3, 4]
- DCacheInvalidateRange 至於 DDR 透過 DMA 回到 CPU 沒有做 invalid 可能會發生 Cache 值一直被刷新成不是 DDR 的資料,這很好理解,系統如果夠大又不是只有 DDR 給 CPU 資料,就會導致 DDR 的值沒有被 CPU 撈到。
Processor data(舊數據): [0, 0, 0, 0]
DDR Memory (新數據): [19, 22, 43, 50]
所以解法直接無效 Cache,強制 CPU 直接去抓 DDR 相對應位置上面的資料
Processor data(最新數據): [19, 22, 43, 50]
DDR Memory (最新數據): [19, 22, 43, 50]
可以注意一下針對 DMA 搬 CPU 寫入資料,描述是 Processor cache,CPU 讀 DDR 資料,描述是 Processor data,然後再搭配我上面的畫的實驗流程圖,體會一下。
以下就是最後結果驗證。

說實話,做到這裡看到結果,是不是電腦前各位,想到這東西擁有無限可能,可以寫酷酷資料分析甚麼的,而且基本上已經在這邊幫各位 flow 打通了,剩下就是靠各位想像來玩這傢伙了。
最後我必須推一下 vitis debug 工具,對於資料存取流想管控真的好用,基本上你有任何 DDR 存取問題,第一個想到應該是跑 debug as 然後去把 DDR 位置叫出來看有沒有正常工作。



最後提一下前面有說到最近聽訓練講座得到感想。
FPGA 和 ASIC 底層架構是完全不一樣的,FPGA 各位可以想成就是組樂高玩具,功能都是靠底層已經做好的 LUT 和 component 組出來;而 ASIC 各位可以想成是在做現成的玩具,所有功能都是在一開始規劃好,下去寫設計到 tape-out 一切就都固定了,不能說我想要加什麼功能,就突然加上去,都要再回頭從新規劃。
這就體現出 FPGA 和 ASIC 處理問題大方向的不同,因為 FPGA 硬體已經都固定,只能拿現有硬體來組出你要功能,所以比較擅長處理已知的問題;ASIC 則相反,規劃階段還沒有任何硬體,所以擅長處理甚麼問題,可以給設計者自定義,這邊就暫定未知的問題。
已知的問題例如 for 回圈,知道系統收斂時間點;未知的問題例如 recusive,這個收斂點在哪裡是給使用者定義的。
而那場訓練講座是有關 vitis AI ,其中提到很重要一個觀點,AI 訓練結果要好,大方向硬體要支援遞迴運算,AI 本質就是不停對於問題做遞迴訓練。
而過去 xilinx 推的 vitis AI 效果沒這麼好,原因有可能出在 FPGA 只擅長處理已知的問題,本身底層硬體體質問題沒辦法去兜出遞迴,不過 vitis AI 之後可能會比較偏向 maintain 了,大家差不多可以找其他 solution 做 AI 硬體摸組?
當然這 AI 硬體摸組也是 AMD 現階段正在努力的方向,想辦法去堆更多遞迴在硬體裡面,當然我也不是這方面專家,就算是聽個熱鬧,寫寫感想,而且我的想法也未必是對的🫠,就留給各位參考參考。
最後還是老話一句,這上面東西,都是我一個一個點下去試試看之後得出來心得,比較站在踩雷通 flow 的腳色,遇到 bug 也儘量去翻閱手冊,嘗試解決,最後總結出來自己結論,所以有可能會有理解錯,建議大家看完,發現我講得並不是這麼一回事,歡迎私訊或來信指證。
메타데이터
- post_id
- d9a1b308fbb0
- slug
- vitis-hls-experiment-axistream-single-dma-d9a1b308fbb0
- url
- https://medium.com/@r07945002/vitis-hls-experiment-axistream-single-dma-d9a1b308fbb0
- canonical_url
- https://medium.com/@r07945002/vitis-hls-experiment-axistream-single-dma-d9a1b308fbb0
- author_url
- https://medium.com/@r07945002
- status
- ok
- fetched_at
- 2026-07-21 22:54:48