文章目錄
本系列文檔通過實例說明 Vivado HLS 的基礎開發流程。
遺憾的是,HLS 沒有必要進行更爲深入的研究。因爲在本人看來,HLS 僅僅是一個在 FPGA 上快速實現算法的工具,稍有些複雜的算法就幾乎無法得到足夠的運行效率。原本對 HLS 的幻想是像 C/C++ 代碼一樣在 FPGA 中執行計算,但是在後續的進階實驗中越發感受到 HLS 的侷限,無論是代碼本身的優化或者使用 Directive 配置都沒有理想的結果。可能 HLS 面向的使用對象是不瞭解 FPGA 運行原理的 C/C++ 工程師,僅在對代碼運行效率沒有要求的情況下使用。
開發環境:
- zcu102 開發板
- Windows 10
- Vivado 2018.2.1
- Visual Studio Code
- Matlab 2017b
主要參考資料包括:
- ug998
- ug871
- ug902
- ug1197
- Vivado HLS 基本應用與圖像處理視頻教程
本文檔說明了使用 Vivado HLS 實現算法功能的基本流程,並且配置 VS Code 爲基礎的代碼編輯器(僅用於編輯代碼)。
建立 HLS 工程
Vivado Design Suite 完整安裝後,在 Win10 開始菜單中打開 HLS:
HLS 啓動後,在菜單中選擇 File > New Project
在彈出窗口中選擇工程名稱和路徑,點擊 Next 按鈕
後續的添加源文件(source files)和測試文件(testbench files)界面默認不做操作,直接按 Next 按鈕
在 solution 界面設置解決方案名稱、時鐘頻率,並選擇器件,最後點擊 Finish 按鈕
注意:同一個 HLS 工程對應一組源代碼和測試代碼,但是可以包含多個解決方案,每個解決方案可以對代碼的 FPGA 實現做不同的配置。
工程建立後的 Explorer 界面如下:
配置 VS Code
配置 include 路徑
首先確保 VS Code 已安裝 C/C++ 插件和 C++ Intellisense 插件
進入 HLS 工程所在目錄,在右鍵菜單選擇“通過 Code 打開”
在 VS Code 中通過菜單或者快捷鍵打開 Command Palatte
在彈出的 Command Palatte 中輸入 edit,並選擇:
C/C++: Edit Configurations (UI)
在配置界面的包含路徑中添加 HLS 工程 Includes 下全部的路徑
注意:${workspaceFolder}/** 路徑是點擊輸入框自動添加
其它配置項保持默認,關閉配置界面。
在 HLS 工程文件夾內添加了 .vscode 文件夾,打開其中的c_cpp_properties.json 即爲前述的配置內容
配置默認文件頭
參考:https://www.jianshu.com/p/07a7fd95954f
在 VS Code 菜單選擇 File > Preferences > User Snippets
在彈出命令框中選擇 C++
在打開的 cpp.json 文件中添加以下內容,通過關鍵詞 hls 添加與 Vivado 建立 HDL 文件相同的文件頭
{
"HLS CPP Title": {
"prefix": "hls",
"body": [
"//////////////////////////////////////////////////////////////////////////////////",
"// Company:",
"// Engineer:",
"//",
"// Create Date: $CURRENT_YEAR/$CURRENT_MONTH/$CURRENT_DATE $CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND",
"// Design Name:",
"// Module Name: $TM_FILENAME_BASE",
"// Project Name:",
"// Target Devices:",
"// Tool Versions:",
"// Description:",
"//",
"// Dependencies:",
"//",
"// Revision:",
"// Revision 0.01 - File Created",
"// Additional Comments:",
"//",
"//////////////////////////////////////////////////////////////////////////////////",
"\n",
"\n"
],
"description": "HLS CPP Title"
}
}
注意:prefix 後的內容是 snippet 的提示文字,在 cpp 文件中輸入 hls 後在彈出的 suggestion 中選擇 HLS CPP Title。當然必須保證在 VS Code 中打開 suggestion 功能。
編寫代碼
一般情況下 HLS 工程裏的源文件都使用 C++,而不是 C,原因是 C++ 可以通過模板功能自定義的整數位寬和函數功能。
源文件的功能是定義一個函數,將輸入的 8 位 led 數值循環移位,Verilog 代碼如下:
led_out[7:0] <= {led_in[6:0], led_in[7]};
在 VS Code 的 Explorer 的右鍵菜單中選擇 New FIle
建立源代碼文件: shift_led.cpp 、shift_led.h
測試代碼文件:tb_shift_led.cpp
代碼:shift_led.h
#ifndef _SHIFT_LED_H_
#define _SHIFT_LED_H_
// 時鐘頻率爲 125 MHz,for 循環 PERIOD 次即爲 1 秒
#define PERIOD 125000000
// 函數聲明
void shift_led(unsigned char* led_out, unsigned char led_in);
#endif
代碼:shift_led.cpp
#include "shift_led.h"
void shift_led(unsigned char* led_out, unsigned char led_in)
{
// 保存輸入數值
unsigned char temp;
temp = led_in;
// 用 for 循環計時 1 秒
unsigned int i;
for (i = 0; i < PERIOD; ++i)
{
;
}
// 循環移位 temp <= {temp[6:0], temp[7]};
temp = (temp << 1) | (temp >> 7);
//輸出數據
*led_out = temp;
}
代碼:tb_shift_led.cpp
#include <stdio.h>
#include "shift_led.h"
// 運行 HLS 函數的次數
#define TIME 17
// 人工觀察運行結果
int main()
{
unsigned char led_in = 0x7F;
unsigned char led_out = 0;
for (int i = 0; i < TIME; ++i)
{
// 調用 HLS 函數
shift_led(&led_out, led_in);
// 將 HLS 函數輸出值賦給輸入值用於下一輪計算
led_in = led_out;
// 打印輸出值
printf("current led value: %x\n", led_in);
}
// 返回 0 值表示測試正確,非 0 值表示測試錯誤
return 0;
}
向 HLS 工程添加代碼
HLS 工程的代碼包括 2 種:源代碼(Source)和測試代碼(Test Bench)。
源代碼用於綜合生成 RTL 代碼在 FPGA 中運行,測試代碼中實現 main 函數調用源代碼中定義的函數。
回到 HLS 工程,在 Explorer 的 Source 項的右鍵菜單上選擇 Add Files,並且在彈出窗口中選擇前述的 shift_led.cpp
注意:shift_led.h 文件不能添加進 HLS 工程
在 Explorer 的 Test Bench 項的右鍵菜單選擇 Add Files,添加 tb_shift_led.cpp
完成後的 Explorer 顯示如下:
在 HLS 工程中打開代碼文件之前應當先配置 HLS 環境(Eclipse)使用 UTF-8 的文本編碼格式與 VS Code 一致
VS Code 中的配置如下圖所示:
在 HLS 工程菜單中打開 Window > Preferences > Workspace 頁,配置以下 2 項:
- Text file encoding: Other UTF-8
- New text file line delimiter: Other Unix
在 HLS 工程中可以雙擊打開 cpp 代碼文件
C Simulation
在 HLS 菜單選擇 Project > Run C Simulation
彈出窗口保持默認,點擊 OK 按鈕
根據 Console 窗口提示,前述代碼的仿真測試正確完成。
如果將 tb_shift_led.cpp 中 main 函數返回值改爲 1,再次運行 C Simulation,則 Console 中提示仿真出錯:
注意:C Simulation 完成後在 Explorer 的 solution1 中出現了 csim 文件夾,在 report 中的 _csim.log 即爲仿真測試的記錄
C Synthesis
在 HLS 的菜單中選擇 Project > Project Settings
在 Synthesis 頁設置 Top Function 爲 shift_led,點擊 OK 按鈕
在 HLS 菜單選擇 Solution > Run C Synthesis > Active Solution
運行完成後,Explorer 的 solution1 中出現 impl 和 syn 文件夾,雙擊打開綜合報告:shift_led_csynth.rpt
在綜合報告的最下方 Interface 爲綜合生成 RTL 模塊的端口列表:
也可以在 Explorer 中打開 solution1 > syn > verilog > shift_led.v 查看
注意:此時生成的 RTL 文件根據 ug902 的說明,不建議直接用於 Vivado 工程使用,而是應當[生成 IP 供 Vivado 工程調用](#Vivado 工程)
Run C/RTL Cosimulation
只有在完成 C Synthesis 之後才能進行 C 與 RTL 的聯合仿真。
在 HLS 菜單選擇 Solution > Run C/RTL Cosimulation
在彈出窗口中選擇 Vivado Simulator 和 Dump Trace all (用於記錄仿真波形),點擊 OK 按鈕
仿真完成後在 Explorer 的 solution1 中出現 sim 文件夾,打開仿真報告文件:
solution1 > sim > report > shift_led_cosim.rpt
顯示通過仿真測試
打開波形文件==(調用 Vivado)==:
solution1 > sim > verilog > shift_led.wdb
注意:無法在 HLS 中雙擊打開,而必須在 Win10 文件夾中雙擊打開該文件
在 Vivado 中 Scope 頁右鍵點擊 inst_shift_led,選擇 Add to Wave Window
除此以外,時鐘和復位信號如下圖所示:
添加完成後的波形如下圖所示,用於觀察生成的 RTL 模塊的端口時序
修改
在前文的波形中可以發現 1 個時鐘週期就完成移位,而沒有按照代碼的設計等待 1 秒,很可能是編譯器將 shift_led.cpp 代碼中空執行的 for 循環優化清理掉了。
於是修改 shift_led.cpp 代碼如下:
#include "shift_led.h"
void shift_led(unsigned char* led_out, unsigned char led_in)
{
// 保存輸入數值
unsigned char temp;
temp = led_in;
// 用 for 循環計時 1 秒
unsigned int i;
for (i = 0; i < PERIOD; ++i)
{
if (i == 0)
{
// 循環移位 temp <= {temp[6:0], temp[7]};
temp = (temp << 1) | (temp >> 7);
}
}
//輸出數據
*led_out = temp;
}
並且爲了仿真方便,在 shift_led.h 文件中修改 PERIOD 數值如下:
#ifndef _SHIFT_LED_H_
#define _SHIFT_LED_H_
// 時鐘頻率爲 125 MHz,for 循環 125000000 次即爲 1 秒
// 爲減少仿真時間,將 PERIOD 值改爲 125
#define PERIOD 125
// 函數聲明
void shift_led(unsigned char* led_out, unsigned char led_in);
#endif
代碼修改完成後,重新執行以下步驟:
- [C Simulation](#C Simulation)
- [C Synthesis](#C Synthesis)
- [Run C/RTL Cosimulation](#Run C/RTL Cosimulation)
最後得到正確的波形:
導出 HLS IP
在導出 HLS IP 之前,修改 shift_led.h 中的 PERIOD 數值爲 125000000,以實現 1 秒延時。
修改完成後,直接進行 [C Synthesis](#C Synthesis)。
C Synthesis 正確完成後,在 HLS 菜單中選擇 Solution > Export RTL
在彈出窗口中如下圖配置後,點擊 OK 按鈕
完成操作後,在 HLS 的 Explorer 的 solution1 項的 impl 目錄下出現 ip 文件夾
Vivado 工程
Vivado 的使用細節本文檔不再詳述,具體方法可以參考之前的文檔:
Vivado 工程導入 IP
建立 Vivado 工程 hls_test 之後,在 Flow Navigator 的 PROJECT MANAGER 中點擊 Settings
在 IP > Repository 頁點擊 + 按鈕添加 HLS 生成的 IP 文件夾,完成後點擊 OK 按鈕
在 Vivado 的 Flow Navigator 中點擊 IP Catalog 打開 IP 列表界面
在搜索框內輸入 shift_led 找到 HLS 生成的 IP
雙擊 IP,並且在彈出窗口中保持默認,點擊 OK 按鈕
根據提示,保持默認配置完成 IP 生成
在 Sources 框內出現剛生成的 IP
HDL 代碼實現
在 Vivado 工程中建立頂層模塊 hls_top.v
添加以下代碼,shift_led 的端口時序參考[仿真波形](#Run C/RTL Cosimulation)
module hls_top(
input clk_p,
input clk_n,
output [7:0] leds
);
wire clk;// 125MHz工作時鐘
// 用 IBUFDS 原語實現差分時鐘轉單端
IBUFDS
#(
.IOSTANDARD("DEFAULT")// DEFAULT默認所有情況
)
IBUFDS_inst
(
.O(clk),// 輸出
.I(clk_p),// P端輸入
.IB(clk_n)// N端輸入
);
// 用計數器產生復位信號
reg [15:0] cnt_rst = 16'd0;
wire rst;
assign rst = ~(cnt_rst[15]);// 計數器最高位取反作爲高有效復位信號
always @(posedge clk) begin
case (cnt_rst)
16'hFFFF: begin
// 計數結束,數值保持
cnt_rst <= cnt_rst;
end
default: begin
// 計數器增 1
cnt_rst <= cnt_rst+16'd1;
end
endcase
end
// 例化 IP
reg start = 1'b0;
reg [7:0] led_in = 8'h7F;
wire [7:0] led_out;
wire led_out_vld;
shift_led_0 shift_led_u (
.ap_clk(clk), // input wire ap_clk
.ap_rst(rst), // input wire ap_rst
.ap_start(start), // input wire ap_start
.ap_idle(), // output wire ap_idle
.ap_done(), // output wire ap_done
.ap_ready(), // output wire ap_ready
.led_out(led_out), // output wire [7 : 0] led_out
.led_out_ap_vld(led_out_vld), // output wire led_out_ap_vld
.led_in(led_in) // input wire [7 : 0] led_in
);
// IP 功能控制
always @(posedge clk) begin
if (rst == 1'b1) begin
start <= 1'b0;
led_in <= 8'h7F;
end
else begin
// 啓動 IP 工作
start <= 1'b1;
// 產生有效輸出 led_out 時,向 led_in 賦值
if (led_out_vld == 1'b1) begin
led_in <= led_out;
end
else begin
led_in <= led_in;
end
end
end
endmodule
之後按照與zuc102_0_PL 端 LED 閃爍文檔相同的方法,添加引腳約束和時鐘約束,並且生成 bit 配置文件。
測試
按照zuc102_0_PL 端 LED 閃爍文檔的說明,在開發板的 FPGA 上燒寫 bit 配置文件,觀察板上 8 個 led 燈按照預期工作。
TODO:待上板測試。