OpenCL について投稿しようと思ったのですが、環境構築が間に合いませんでした。そこで、自分で作った IP コアのポーティングについて書きます。
SGBM の IP コア
もともと SGBM(ステレオ視差画像)の IP コア(というか Verilong-HDL の Module) を Xilinx の ZCU102 で動かしていました。それを Intel の Cyclon-V や Arria 10 で動かしてみましょうという話です。
I/F は AXIS
I/F は AXIS そのままにしました。Stream の I/F なので他の I/F への変換はそれほど難しくないと思います。
まずは iverilog で動かす
将来的な移植性を考えて、特定の IP コアに依存していないことを確かめるために、まずは Linux で iverilog で動かしました。問題になった個所はもともと ZCU102 用にしていた箇所で次に関連した IP コアでした
- blk_mem に関連するモジュール
- 浮動小数点数に関連する演算
ブロックメモリに関するモジュールの移植
具体的には blk_mem_D2048wx12b とか blk_mem_D2048wx16b とか fifo_shift_16wx10b などで、要はデュアルポートの BRAM と FIFO です。必要なビット数はアプリケーションに依存する 10 や 12 や 16 です。ビット数を可変にしておけば移植も楽になるでしょう。
最終版はもうちょい変えてますが、わかりやすいソースを次に掲げておきます。
BRAM はこんな感じ
module bram
#(
parameter ADRESS_WIDTH = 11,
parameter DATA_WIDTH = 10
)
(
input clk,
input wr_en,
input [ADRESS_WIDTH-1:0] wr_addr,
input [DATA_WIDTH-1:0] wr_data,
input [ADRESS_WIDTH-1:0] rd_addr,
input [DATA_WIDTH-1:0] rd_data
);
// ----------------------------------------------------------------
(* ram_style = "block" *)
reg [DATA_WIDTH-1:0] mem [0:2**DATA_WIDTH-1];
reg [DATA_WIDTH-1:0] rd_data_r;
// ----------------------------------------------------------------
assign rd_data = rd_data_r;
// ----------------------------------------------------------------
always@( posedge clk )
begin
if ( wr_en ) begin
mem[wr_addr] <= wr_data;
end
end
always@( posedge clk )
begin
rd_data_r <= mem[rd_addr];
end
endmodule
fifo はこんな感じ
module fifo
#(
parameter integer ADDRESS_WIDTH = 8,
parameter integer DATA_WIDTH = 8,
parameter integer DEPTH = ( 1 << ADDRESS_WIDTH ),
parameter THRESHOLD_W= DEPTH/4*3,
parameter THRESHOLD_R= DEPTH/4*1
)
(
input clk,
input reset,
output write_ready,
input write_en,
input [DATA_WIDTH:0] wr_data,
output read_ready,
input read_en,
input [DATA_WIDTH:0] rd_data
);
reg [ADDRESS_WIDTH:0] wr_addr_1 = 0;
reg [ADDRESS_WIDTH:0] rd_addr_1 = 0;
wire [ADDRESS_WIDTH:0] diff_addr;
wire [ADDRESS_WIDTH-1:0] wr_addr;
wire [ADDRESS_WIDTH-1:0] rd_addr;
reg [DATA_WIDTH-1:0] mem [0:DEPTH-1];
reg [DATA_WIDTH-1:0] rd_data_r;
// ----------------------------------------------------------------
assign diff_addr = wr_addr - rd_addr;
assign write_rdy = ~diff_addr[ADDRESS_WIDTH] & ( diff_addr[ADDRESS_WIDTH-1:0] <= THRESHOLD_W );
assign read_rdy = ~diff_addr[ADDRESS_WIDTH] & ( diff_addr[ADDRESS_WIDTH-1:0] >= THRESHOLD_R );
assign wr_addr = wr_addr_1[ADDRESS_WIDTH-1:0];
assign rd_addr = rd_addr_1[ADDRESS_WIDTH-1:0];
// ----------------------------------------------------------------
always@( posedge clk )
begin
if ( reset ) begin
wr_addr_1 = #1 0;
end else if ( write_en ) begin
wr_addr_1 <= #1 wr_addr_1 + 1;
end
end
// ----------------------------------------------------------------
always@( posedge clk )
begin
if ( write_en ) begin
mem[wr_addr] <= #1 wr_data;
end
end
// ----------------------------------------------------------------
always@( posedge clk )
begin
if ( reset ) begin
rd_addr_1 = #1 0;
end else if ( read_en ) begin
rd_addr_1 <= #1 rd_addr_1 + 1;
end
end
// ----------------------------------------------------------------
always@( posedge clk )
begin
rd_data_r <= #1 mem[rd_addr];
end
endmodule
BRAM は遅延が重要だった
BRAM の遅延(レイテンシー)はよくよく考えたらベンダー依存になっていました。ということで、次のように読み込み時に1clock 分遅らせることにしました。
always@( posedge clk )
begin
rd_data_rr <= #1 rd_data_r;
rd_data_r <= #1 mem[rd_addr];
end
浮動小数点数演算
必要なのは fmul, fadd, fdiv, uitof 等でした。そして、適切にパイプライン化しないといけませんでした。これが結構大変だった。ソースが汚いままで恐縮ですが fmul の例を掲げておきます。
`timescale 1ns / 1ps
module fmul(
input clk,
input areset,
input [31:0] a,
input [31:0] b,
output [31:0] q);
localparam integer INF_LATENCY = 2;
localparam integer LATENCY = INF_LATENCY + 2;
localparam signed [9:0] exp_bias=10'd127;
reg [1:0] exc_inf;
reg [1:0] exc_zero;
reg [INF_LATENCY-1:0] exc_inf_fifo; initial exc_inf_fifo=1'b0;
reg [INF_LATENCY-1:0] exc_zero_fifo; initial exc_zero_fifo=1'b0;
reg sign;
reg [INF_LATENCY-1:0] sign_fifo;
reg signed [9:0] exp;
reg [7:0] exp_exc;
reg [7:0] exp_shift;
reg [47:0] man;
reg [24:0] man_exc;
reg [22:0] man_shift;
reg [31:0] sf;
localparam sf_zero = 32'h0;
localparam sf_inf = 32'h7f800000;
assign q = sf;
always@(posedge clk)
begin
exc_inf[0] <= #1 (a[30:23]==8'hff)? 1'b1:1'b0;
exc_inf[1] <= #1 (b[30:23]==8'hff)? 1'b1:1'b0;
exc_inf_fifo <= #1 {exc_inf_fifo[INF_LATENCY-2:0],exc_inf[0]|exc_inf[1]};
exc_zero[0] <= #1 (a[30:23]==8'b0)? 1'b1:1'b0;
exc_zero[1] <= #1 (b[30:23]==8'b0)? 1'b1:1'b0;
exc_zero_fifo <= #1 {exc_zero_fifo[INF_LATENCY-2:0],exc_zero[0]|exc_zero[1]};
sign <= #1 a[31]^b[31];
sign_fifo <= #1 {sign_fifo[INF_LATENCY-2:0],sign};
exp <= #1 $signed({2'b0,a[30:23]})+$signed({2'b0,b[30:23]})-$signed(exp_bias);
exp_exc <= #1 (exp[9]!=1'b1|exp!=10'h0)? ((exp[8]!=1'b1)? exp:8'hff):8'h00;
exp_shift <= #1 (man_exc[24]==1'b1&exp_exc!=8'hff&exp_exc!=8'h0)? exp_exc+1'b1:exp_exc;
man <= #1 (a!=32'b0&b!=32'b0)? {1'b1,a[22:0]}*{1'b1,b[22:0]}:48'b0;
man_exc <= #1 (exp[9]!=1'b1&exp!=10'h0&exp[8]!=1'b1)? man[47:23]:25'b0;
man_shift <= #1 (man_exc[24]==1'b1)? man_exc[23:1]:man_exc[22:0];
sf <= #1 (exc_zero_fifo[INF_LATENCY-1]==1'b0&exp_shift!=8'b0)? ((exc_inf_fifo[INF_LATENCY-1]==1'b0)? {sign_fifo[INF_LATENCY-1],exp_shift,man_shift}:sf_inf):sf_zero;
end
endmodule
レイテンシー調整
最終的には、浮動小数点数の計算はハードマクロを使いたいわけで、iverilog で通すためだけに書いたようなものでした。そして、ハードマクロではチップによってレイテンシが違います。自分の書いたなんちゃって浮動小数点数の計算もいろいろレイテンシを変えたくなるかもしれません。
ということで、呼び出し側でその値を調整します。本来なら、言語側に、いまつかうモジュールのレイテンシはどれくらいかな?という情報を得るような(Rust の trait みたいな)機能があればよいのでしょうが、Verilog-HDL にはそんな機能はないので、がんばって呼び出し側で調整します。
冒頭の箇所だけ掲げます。レイテンシーがちょっと違っていても吸収するようにしました。
module sgbm_3d_axis
#(
parameter integer TOTAL_LATENCY = 42,
parameter integer XY_LATENCY = 0,
parameter integer Z_LATENCY = 0
)
Cyclon-V や Arria 10 対応
bram や fifo もハードマクロを使おうかと思ったのですが、うまく動いてしまい、特段性能にも差が出ないようなので(なかでハードマクロに置き換わっているのでしょう)、そこはほっておくことにしました。
浮動小数点数の計算にはやはりハードマクロを使いたいので Wizard でつくることにしました。むかしは megafunction とか言っていた気がします。紆余曲折あって(基本的にはツールの使い方の問題。最新の情報でつかってみてください)なんとかうまく生成できました。
// megafunction wizard: %FP_FUNCTIONS Intel FPGA IP v20.1%
// GENERATION: XML
// fmul.v
// Generated using ACDS version 20.1 720
`timescale 1 ps / 1 ps
module fmul (
input wire clk, // clk.clk
input wire areset, // areset.reset
input wire [31:0] a, // a.a
input wire [31:0] b, // b.b
output wire [31:0] q // q.q
);
fmul_0002 fmul_inst (
.clk (clk), // clk.clk
.areset (areset), // areset.reset
.a (a), // a.a
.b (b), // b.b
.q (q) // q.q
);
endmodule
あとは生成時にレイテンシの情報がでるのでその情報をもとに呼び出し側の調整をします。Cyclon V と Arria 10 ではハードマクロのレイテンシが違うのでその辺を調整するわけです。
移植してみて
ひっかかったのは主にレイテンシ。元のソースが機種依存しない書き方になっていたので、基本的な部分はまったく修正しませんでした。ベンダーを跨ぐのはちょっと怖かったのですが、特に問題もなく、仮にトラブル起こっても普通にいっこいっこ確認していけば(そういうテストベンチを前もってそろえておけば)いいので、普通のデバッグと同じです。
唯一、時間を費やしたのが bram.v の記述。最初、安直に read と write を書いたらそのタイミングを律儀にコンパイラが調整しようとしていたらしく、「まったくコンパイルが終わりません」でした。もちろん、書き方が悪かったわけで、こういうのは先人の作った動くソースをベースに開発する方が楽になります。bram くらい簡単だろうと思ってなめてかかるとかえって時間がかかってしまいました。iverilog で動くから大丈夫もこの辺は通用しません(iverilog がたぶん厳密にやっていないから、自分のバグを見落とす)。