Processing math: 100%

2019/01/14

HDLでフィボナッチ数

ソフトウェア言語ではHello, world!の次ぐらいにやると思う. それをあえてハードウェアで実装してみようと思う.

日本語版WikipediaのVHDLのページにもフィボナッチ数の実装が載っているけど, あまりにも的はずれな内容なのでおとなしく英語版Wikipediaを見よう.

実際に実装したgithubリポジトリは以下.

https://github.com/toms74209200/FIB

インターフェース

まずは外部と接続するインターフェースを決める. 規格化されたインターフェースを使うことで, 再利用性を高めることが大事. 特にFPGAではタイミング制御が重要なので, インターフェースと内部の処理を完全に切り分けることはできないことが多い.

ここではIntel(altera) FPGAを想定するため, 考えられるインターフェースは以下の通り.

  • Avalon-MM[1]
  • Avalon-ST[2]
  • Nios II Custom Instruction[3]

CPUとの連携を考えるとAvalon-MMかCustom Instructionだが, ここではHDLで実装しやすいAvalon-STにする.

Avalon-ST

Avalon-STではvaliddataの2つの信号でsourceからsinkへとデータを送る(参考[2]のFigure 21がわかりやすい). このインターフェースには使う信号によってバリエーションがあるが, シンプルにvaliddata, errorのみにする. これに従ってフィボナッチ演算を行うモジュールの入出力を表にまとめると以下の通り.

Name I/O P/N Description
RESET_n I N Reset
CLK I P Clock
ASI_READY O P Avalon-ST sink data ready
ASI_VALID I P Avalon-ST sink data valid
ASI_DATA[31:0] I P Avalon-ST sink data: index n
ASO_VALID O P Avalon-ST source data valid
ASO_DATA[31:0] O P Avalon-ST source data: Fibonacci number
ASO_ERROR O P Avalon-ST source error

sourceからのデータ (ASI_DATA) はフィボナッチ演算をいくつまで行うかという引数を与える. フィボナッチ数の漸化式

Fn=Fn1+Fn2

nにあたる. そしてモジュールが出力するデータ (ASO_DATA) はnに対応するフィボナッチ数である.

フィボナッチ演算の構成

フィボナッチ数の漸化式は下式の通り

Fn=Fn1+Fn2.

この数列の第1項と第2項は特別に

F1=1,F=2

とする. これは参考[4]からの答え合わせをしやすくするためだ.

よって必要なものは

  • レジスタ
  • 加算器

となる.

回路全体はCLKnRSTでコントロールする.

レジスタ

遅延器はレジスタで表現する. CLKに同期してデータを引き渡せばよい. レジスタを構成する上で重要なことはそのビット幅である. といってもフィボナッチ数列の場合は加算のみなので, ビット幅が減少することはない. 加算による桁上げを考慮してFn1,Fn2に対してFnはビット幅が1増える. ここでFn1,Fn2のビット幅はDW-1とする(上記では32ビット: DW=32).

また第1項と第2項は初期値としてレジスタに代入する. ASI_VALIDによって演算が開始するタイミングでレジスタを初期化する.

-- Internal signal --
signal first_term : std_logic_vector(DW-1 downto 0); -- Fibonacci first term
signal fn : std_logic_vector(DW downto 0); -- F_n
signal fnm1 : std_logic_vector(DW-1 downto 0); -- F_n-1
signal fnm2 : std_logic_vector(DW-1 downto 0); -- F_n-2
~~
process (CLK, RESET_n) begin
if (RESET_n = '0') then
fnm1 <= (others => '0');
fnm2 <= (others => '0');
elsif (CLK'event and CLK = '1') then
if (busy_i = '0' and ASI_VALID = '1') then
fnm1 <= first_term;
fnm2 <= first_term;
else
fnm1 <= fn(fnm1'range);
fnm2 <= fnm1;
end if;
end if;
end process;

加算器

加算器の構成は様々考えられるが, 単純に+演算子で表現することにしよう. VHDLでは代入の左辺と右辺は同じビット幅である必要があるので以下のように書く.

fn <= ('0' & fnm1) + ('0' & fnm2);

ビジー信号

フィボナッチ演算をコントロールするためにビジー信号を実装する. ビジー信号が有効のときは演算を行い, 外部の入力を受け付けないようにする.ビジー信号によって演算が行われるように, レジスタにビジー信号を接続する. 上記のレジスタ部にビジー信号(busy_i)を追加すると以下のようになる.

process (CLK, RESET_n) begin
if (RESET_n = '0') then
fnm1 <= (others => '0');
fnm2 <= (others => '0');
elsif (CLK'event and CLK = '1') then
if (busy_i = '0' and ASI_VALID = '1') then
fnm1 <= first_term;
fnm2 <= first_term;
elsif (busy_i = '1') then
fnm1 <= fn(fnm1'range);
fnm2 <= fnm1;
end if;
end if;
end process;
view raw fib_reg_en.vhd hosted with ❤ by GitHub

ビジー信号自体のコントロールには, 入力された演算回数をに到達したかチェックするカウンタが必要である.

signal busy_i : std_logic; -- Calculation enable
~~
process (CLK, RESET_n) begin
if (RESET_n = '0') then
busy_i <= '0';
elsif (CLK'event and CLK = '1') then
if (done_i = '1') then
busy_i <= '0';
elsif (ASI_VALID = '1') then
busy_i <= '1';
end if;
end if;
end process;
view raw fib_en.vhd hosted with ❤ by GitHub

カウンタcntは演算回数をカウントするが, 演算は1CLK毎に行われるため, 通常のクロックカウンタである.

process (CLK, RESET_n) begin
if (RESET_n = '0') then
cnt <= (others => '0');
elsif (CLK'event and CLK = '1') then
if (busy_i = '1') then
cnt <= cnt + 1;
elsif (ASI_VALID = '1') then
cnt <= B"00_0011";
end if;
end if;
end process;
view raw fib_cnt.vhd hosted with ❤ by GitHub

初期化処理によってレジスタに第1項と第2項が代入されるため, 演算は第3項から行われる. そのためカウンタは初期化によって3項目から計算するようにする.

完了信号

フィボナッチ演算の完了を通知し, 演算結果を有効にするための信号を実装する. 完了信号(done_i)は組み合わせ回路で実装する.

done_i <= '1' when (busy_i = '1' and cnt > calc_n - 1) else
          '1' when (busy_i = '1' and calc_n < 3) else
          '0';

演算は3項目から始めるから, 入力された演算回数(calc_n)が3より小さい場合はレジスタの初期値を出力すればよい.

完了信号が有効のときに演算結果を出力するが, 複数クロック動作することによって出力結果が変化しないように完了時の結果を保持するようにしよう. 結果的にレジスタは以下のようになる.

process (CLK, RESET_n) begin
if (RESET_n = '0') then
fnm1 <= (others => '0');
fnm2 <= (others => '0');
elsif (CLK'event and CLK = '1') then
if (busy_i = '0' and ASI_VALID = '1') then
fnm1 <= first_term;
fnm2 <= first_term;
elsif (busy_i = '1') then
if (done_i = '1') then
fnm1 <= fnm1;
fnm2 <= fnm2;
else
fnm1 <= fn(fnm1'range);
fnm2 <= fnm1;
end if;
end if;
end if;
end process;

入力された演算回数は演算開始時に保持する. 演算中に更新されないようにすること.

signal calc_n : std_logic_vector(5 downto 0); -- Calculation end count N
~~
process (CLK, RESET_n) begin
if (RESET_n = '0') then
calc_n <= (others => '0');
elsif (CLK'event and CLK = '1') then
if (busy_i = '0' and ASI_VALID = '1') then
calc_n <= ASI_DATA(calc_n'range);
end if;
end if;
end process;
view raw fib_calc_n.vhd hosted with ❤ by GitHub

インターフェースとの接続

このモジュールが出力する信号は以下の通り.

  • ASI_READY
  • ASO_VALID
  • ASO_DATA
  • ASO_ERROR

ASI_READYはモジュールが入力可能かを示す. ビジー信号を反転すると入力可能を表すことができる.

ASO_VALIDは演算結果の出力有効である. 完了信号(done_i)に接続する.

ASO_DATAは演算結果を出力する. 加算した結果を出力するが , ビット幅を合わせる必要がある. また入力された演算回数(calc_n)が3より小さい場合はレジスタの初期値を出力する.

ASO_DATA <= first_term when (calc_n < 3) else 
            fn(ASO_DATA'range);

エラー処理

ASO_ERRORは演算結果のエラーを通知する. ここでは以下の2つの場合にエラーを通知するようにする.

  • 加算結果のビットオーバーフロー
  • 入力された演算回数が演算可能の回数より多い場合

ビットオーバーフロー

加算結果のビットオーバーフローは加算によって生じた桁上げをそのまま通知する.

signal over_flow : std_logic; -- Bit overflow assert
signal over_flow_reg : std_logic; -- Bit overflow assert
~~
process (CLK, RESET_n) begin
if (RESET_n = '0') then
over_flow_reg <= '0';
elsif (CLK'event and CLK = '1') then
if (busy_i = '1') then
if (cnt < calc_n) then
if (fn(fn'left) = '1') then
over_flow_reg <= '1';
end if;
end if;
else
over_flow_reg <= '0';
end if;
end if;
end process;
over_flow <= '1' when (fn(fn'left) = '1') else '0';

なぜかまどろっこしいやり方をしているが, 今後のための冗長性だ.

入力エラー

入力された演算回数(calc_n)は入力(ASI_DATA)よりもビット幅が少ないため, 取りこぼしてそのまま違う演算結果を出力してしまう可能性がある. 対してビット幅を等しくしてしまうと, オーバーフローを起こすとわかっている演算に関してもカウンタがその値に達するまで演算を行ってしまう. そこで明らかにオーバーフローを起こすとわかっている演算回数が指定された場合に, 入力(ASI_DATA)を見てエラーとする.

signal n_over : std_logic; -- Count N overflow
~~
process (CLK, RESET_n) begin
if (RESET_n = '0') then
n_over <= '0';
elsif (CLK'event and CLK = '1') then
if (busy_i = '0' and ASI_VALID = '1') then
if (ASI_DATA > X"3F") then
n_over <= '1';
else
n_over <= '0';
end if;
end if;
end if;
end process;
view raw fib_in_err.vhd hosted with ❤ by GitHub

これら2つの論理和をとってエラーとして出力する.

まとめ

これでフィボナッチ数を演算する回路ができた. 例としてIntel Cyclone Vコンパイルした結果を以下に示す.

+-------------------------------------------------------------------------------+
; Flow Summary                                                                  ;
+---------------------------------+---------------------------------------------+
; Flow Status                     ; Successful - Wed Jan 29 21:57:42 2020       ;
; Quartus Prime Version           ; 17.1.0 Build 590 10/25/2017 SJ Lite Edition ;
; Revision Name                   ; FIB                                         ;
; Top-level Entity Name           ; FIB                                         ;
; Family                          ; Cyclone V                                   ;
; Device                          ; 5CGXFC7C7F23C8                              ;
; Timing Models                   ; Final                                       ;
; Logic utilization (in ALMs)     ; 42 / 56,480 ( < 1 % )                       ;
; Total registers                 ; 80                                          ;
; Total block memory bits         ; 0 / 7,024,640 ( 0 % )                       ;
; Total DSP Blocks                ; 0 / 156 ( 0 % )                             ;
+---------------------------------+---------------------------------------------+
+--------------------------------------------------+
; Slow 1100mV 85C Model Fmax Summary               ;
+------------+-----------------+------------+------+
; Fmax       ; Restricted Fmax ; Clock Name ; Note ;
+------------+-----------------+------------+------+
; 221.09 MHz ; 221.09 MHz      ; CLK        ;      ;
+------------+-----------------+------------+------+

FPGAとしては非常に小さいモジュールだが, C言語のようなプログラムよりも遥かに高速に演算する事ができる. 実はフィボナッチ数の演算は過去の結果を利用するため, 並列化しづらくそのまま回路にしても高速化しない. それでも1クロックサイクルごとに演算が完了するため, 高速に演算することができる.

参考

[1] 3. Avalon Memory-Mapped Interfaces - Avalon Interface Specifications

[2] 5. Avalon Streaming Interfaces - Avalon Interface Specifications

[3] Nios II Custom Instruction User Guide

[4] 100番目までのフィボナッチ数列 - すぐる学習会 -

0 件のコメント:

コメントを投稿