はじめに
こんにちは。
J-7SYSTEM WORKSの長船です。
普段は小規模~中規模あたりのFPGAがメインターゲットの業務をしております。
今回アドベントカレンダーということで、あんまり説明されないNiosIIプロセッサのカスタム命令について説明しようと思います。
少なくともNiosIIを触った経験があることが前提になりますから、本記事は一通りのツール操作ができる中級者向けの内容となります。
ある程度のワークフローは説明するものの、QaurtusPrimeやPlatform Designer、NiosII Software Build Tools(本文中ではSBTと表記、NiosII EDSと書かれているものもある)の入門的な操作手順については省略しています。
スキルレベルは「QuartusPrimeは普通に使うことができ、Platform DesignerやNiosIIも触ったことがある」らへんを想定しています。
また今回の記事内容は、以下の環境で作成しています。
文献やGiuHubリポジトリなどは記事の最後に紹介していますので、そちらも参照していただればと思います。
- QuartusPrime 20.1 LiteEdition
- NiosII Software Build Tools 20.1 (QuartusPrime 20.1LEに付属)
- OS : Windows 10 Pro 64bit 1909
- ターゲットボード : Terasic DE0-Nano
1. NiosIIのカスタム命令について
NiosIIそのものについてはIntelのサイトを参照していただくとして、ここでは合成可能なソフトマクロプロセッサであるNiosIIの最大の特徴でもあるカスタム命令について紹介しましょう。
カスタム命令とはその名の通り、ユーザー側が自由に追加することのできる命令です。
一般的にプロセッサはメーカーが設計して製造し、エンドユーザーにはシリコンチップとして提供されますから、通常は命令セットをユーザーがどうこうすることはできません。
命令セット全部をいじれなくとも、こんな演算ができる命令があればいいのに、と思ったところでエンドユーザーの立場では用意された命令セットを組み合わせて演算を行わせるしかないわけです。
命令セットに思いを馳せる時点でだいぶ深みにはまっているというのはとりあえず横に置いておきます。
そんなプロセッサの命令セットですが、対象がFPGA用のソフトマクロプロセッサ(論理合成で機能を構成するプロセッサコアのことをこのように呼びます)となると状況が少し変わります。
なにせ論理回路を手元で合成できるFPGA向けですから、命令セットをいじることも可能なわけです。
とはいえ何から何まで丸ごといじってしまうと開発ツール側が対応できませんから、実際にはベースとなる命令セットにカスタマイズできる部分をくっつけたスタイルが多くなります。
で、NiosIIではこの「カスタマイズできる部分」の中に、ユーザーが自由に追加できるカスタム命令があるわけです。
(※画像はIntelのサイトより引用)
NiosIIのカスタム命令の特徴をざっと挙げると
- NiosII /eコアおよび/fコアに対応(無償版でも使える)
- 一つのNiosIIコアに対して最大256個のカスタム命令を追加できる
- 普通の3オペランド命令の他、内部状態を持つ命令や独自のレジスタセットを持つ命令も可能
- カスタム命令はPlatform Designerでコンポーネントとして扱うことができ、再利用が簡単
- NiosII SBTでC/C++用のマクロを自動生成
このようなカスタム命令をユーザーがどのように作ればいいかというと、具体的には演算部分をHDLで記述することになります。
カスタムとはいえ命令フィールドは普通の3オペランド命令ですから、インターフェースはほぼ決まっています。
今回は実例として擬似乱数を得るカスタム命令を作ってみることにしましょう。
2. カスタム命令のHDLとQuartusプロジェクト
2.1 Xorshiftとは
XorshiftはGeorge Marsaglia氏が2003年に提案した、疑似乱数列生成法の一つです。
演算がXOR(排他的論理和)とビットシフトのみで構成されていて高速でいて、なおかつ線形合同法に比べて非常に品質の良い乱数を得られるという特徴があります。
とはいえ擬似乱数ですから、シード値が一定ならば同じ数列を返してしまう欠点はそのまま残ります。
そこでソフトウェアとは無関係に内部状態を持つ(フリーランで走る)ハードウェアの出番となります。
Xorshiftはプロセッサでの実行速度も高速ですが、XORとビットシフトのみで構成されているこのアルゴリズムはHDLと親和性が高くとてもカスタム命令向きといえるでしょう。
以下のソース(ci_rand_xorshift128.v
)は周期2^128-1
のXorshiftをカスタム命令にしたものです。数理的な解説やHDLそのものの説明などはここでは省きます。
module ci_rand_xorshift128 (
input wire clk,
input wire clk_en,
input wire reset,
output wire [31:0] result
);
reg [31:0] x_reg, y_reg, z_reg, w_reg;
wire [31:0] t_sig, r_sig;
assign t_sig = x_reg ^ {x_reg[20:0], {11{1'b0}}};
assign r_sig = (w_reg ^ {{19{1'b0}}, w_reg[31:19]}) ^ (t_sig ^ {{8{1'b0}}, t_sig[31:8]});
always @(posedge clk or posedge reset) begin
if (reset) begin
x_reg <= 32'd123456789;
y_reg <= 32'd362436069;
z_reg <= 32'd521288629;
w_reg <= 32'd88675123;
end
else begin
if (clk_en) begin
x_reg <= y_reg;
y_reg <= z_reg;
z_reg <= w_reg;
w_reg <= r_sig;
end
end
end
assign result = w_reg;
endmodule
ここでのポイントとしては
-
内部状態を持つためクロックを使う(この場合
clk
,clk_en
,reset
の入力ポートが必須になる) -
値を出力するだけなので入力オペランドが無い(
dataa
,datab
入力ポートがなく、result
の出力ポートのみ) - エンティティ名やファイル名はPlatform Designerで生成されるモジュール内でユニークでなければならない
の3つです。
3つめの条件は少しわかり難いと思いますので補足をすると、Platform DesignerではHDLモジュールの同一性の識別を単純にファイル名で行っています。
生成する際にコンポーネント構成ファイルをすべて一つのIPコアとしてまとめてしまうので、別々のコンポーネントに同じ名前のファイルがあったときにそれを解決できません。
HDLのエンティティ名(モジュール名)も同様で、そもそもHDLではローカルの名前空間を持てないのでこのような場合は人力でユニーク名を付けなければなりません。
これらの名前が衝突していた場合、Platform Designerの生成でエラーが出たり、Quartusでのコンパイル途中でエラーが出たりと、かなり後になって問題が発覚することになります。
ツールのエラーメッセージや挙動からは原因が推測しにくいために、仕組みを知らないと問題解決に時間がかかってしまうので注意しましょう。
2.2 Quartusプロジェクト作成
次ににQuartusプロジェクトを作成します。Quartusの基本的な操作については今回の記事の範囲ではないので割愛します。
さて今回NiosIIカスタム命令の紹介ということで、FPGAの外部I/Oなどは特に使用しません。
プログラムのダウンロードも実行結果もJTAG経由で行いますから、実行結果はSBT上のコンソールに出てきます。
QuartusPrime 20.1LEがサポートしているデバイスであればお手元にあるIntel FPGAボードでそのまま利用できるでしょう。
私はターゲットボードとしてTerasic DE0-Nanoを使いました。たまたま手元に積んであったからです。
(※画像はTerasicのサイトより引用)
今回はde0nano_top
というトップエンティティのプロジェクトを作成しました。
DE0-Nano付属のCD-ROM(またはTerasicサイトからダウンロード)の中には、DE0_Nano_GOLDEN_TOP
というスケルトンのプロジェクトがあるので、ピンアサインはここから流用しています。
続いてプロジェクトフォルダの直下にip
フォルダを作り、以下のようにコンポーネント用のフォルダを作成して先ほどのHDLファイルを格納します。
<プロジェクトフォルダ>
├─ de0nano_top.qpf
├─ de0nano_top.qsf
├─ de0nano_top.qws
├─ <ip> ← プロジェクトローカルのIPコアを格納するフォルダ
│ └─ <ci_rand_xorshift128> ← カスタム命令コンポーネントのトップフォルダ
│ └─ <hdl>
│ └─ ci_rand_xorshift128.v
:
:
ip
フォルダはプロジェクトローカルのIPコアやPlatform Designerコンポーネントを配置するフォルダで、プロジェクトフォルダ直下にこの名前のフォルダがある場合は自動的に参照します。
しかしこの段階では単にHDLが置かれているだけで、まだPlatform Designerコンポーネントしては識別されません。
カスタム命令やAvalonペリフェラルなどPlatform Designerで扱えるコンポーネントにするにはさらに設定用のtclファイルが必要です。これらの作り方は後で説明していくことにしましょう。
3. Platform DesignerでNiosIIプロセッサのSoCモジュールを作る
3.1 NiosIIモジュールの作成
プロジェクトができたらNiosIIのモジュールを作成していきます。
QuartusのツールバーアイコンからPlatform Designerを立ち上げます。
初期状態では下のような画面がでてきます。最初に1つだけClock Sourceコンポーネントが配置された状態です。
デフォルトで50MHzのクロック入力で設定されているので、今回はそのまま使います。
次に左上のIP CalalogからNios II Processor
を選択してAdd
をクリックします。
コンポーネントを探しづらい場合は検索入力欄にnios
と入力すると一番下にでてきます。
System Contentsリストに追加されるまえにプロセッサの設定ウィンドウが開きます。
今回は無償版のNiosII /eを使うので、Main
タブのNios II Core
でNios II/e
を選択してFinish
をクリックします。
いくつかエラーが出ていますが、これらはあとで修正するので今は無視します。
続いてNiosIIの実行に必要なペリフェラルを追加していきます。
今回使うペリフェラルは以下の4つです。
- On-Chip Memory(プログラムの格納と実行時メモリ。32kバイトのRAMを割り当て)
- System ID(NiosII SBTでターゲットボードの確認に使用)
- Timer(HALでシステムタイムティックに使用する。ただし今回は未使用)
- JTAG-UART(HALの標準入出力ストリームに使用する)
これらのコンポーネントとメモリマップ、IRQ割り当ては以下のようにしました。
コンポーネント | インスタンス名 | ポート | メモリアドレス(命令バス) | メモリアドレス(データバス) | IRQ |
---|---|---|---|---|---|
NiosII | nios2_gen2_0 | debug_mem_slave | 0x0fff_0800 | 0x0fff_0800 | |
On-Chip Memory | onchip_memory2_0 | s1 | 0x0000_0000 | 0x0000_0000 | |
System ID | sysid_qsys_0 | control_slave | 0x1000_0000 | ||
Timer | timer_0 | s1 | 0x1000_0020 | 0 | |
JTAG-UART | jtag_uart_0 | avalon_jtag_slave | 0x1000_0040 | 1 |
NiosIIシステムのメモリマップ割り当てはいくつか流儀があります。ここでは私のやり方で、命令バス(instruction_master)に接続されるメモリペリフェラルは0x0000_0000
から、データバス(data_master)に接続されるペリフェラルのうち周辺I/Oは0x1000_0000
から、データ専用メモリは0x4000_0000
から配置するようにしています。
なぜこのような配置にしているか?
これは主にNiosIIの命令セットの都合とPlatform Designerのアドレス割り当ての都合に起因しています。
NiosIIは32bitプロセッサですからアドレス空間は4Gバイト分あります。そのうちデータバスは通常2Gバイト(bit31をキャッシュスルーフラグに割り当てる場合)のアドレス空間を使用できるのに対して、命令バスは256Mバイトのアドレス空間しかありません。
それに加えてPlatform Designerではペリフェラルの開始アドレスは、そのペリフェラルが使用するアドレス幅の単位でしか配置できません。
例えば32Mバイトのメモリペリフェラルは32Mバイトごとのアドレス境界にしか配置できないわけです。
また当然ですがペリフェラル同士でアドレスがオーバーラップするような配置もできません。
これにより必然的にプログラムコードを格納するメモリは前方に配置することになります。
さらにデバッガ用にdebug_mem_slaveを接続する必要があり、ペリフェラルアドレスのオーバーラップができない制限から、コード格納用のメモリサイズは128Mバイト(1Gbit)が上限になってしまいます。
閑話休題。
メモリペリフェラルが接続されたので、NiosIIのエラー箇所を直していきます。
System ContentsウィンドウのNiosIIコンポーネント(nios2_gen2_0
)を右クリック→Edit
を選択して設定画面を開きます。
Vectors
タブに移動して、Reset vector memory
とException vector memory
の両方ともonchip_memory2_0.s1
を選択します。
これでエラーが消えるので、Finish
をクリックして閉じます。
ここまでできたらFile
→Save
でいったんデザインファイルを保存しておきます。
保存するファイル名がそのままモジュール名になるので、今回はnios2_ci_core
にしましょう。
3.2 カスタム命令コンポーネントの作成
ようやく本題のカスタム命令コンポーネント作成までたどり着きました。
この時点ではカスタム命令の動作をするHDLはあるものの、コンポーネントしてはまだ成立していません。
ここでは新しくコンポーネントを作成する手順を紹介していきます。
IP Catalogの一番上にあるNew Component...
をダブルクリック(または選択してAdd
ボタンをクリック)して、Component Editorを開きます。
Component EditorのComponent Type
タブのName
とDisplay name
にci_rand_component
を記入します。
Name
はインスタンス時に識別されるコンポーネント名。Display name
はIP Catalogリストに表示される名前です。
これらはの名前は有効なIPコア(IP Catalogリストに表示されるもの)でユニークな名前にしなければなりません。
ほどほどに長い名前をつけましょう。
次にFiles
タブに移動して、Add File...
で予め用意してあるHDLファイルを追加します。
その後Analyze Synthesis Files
をクリックすると追加したHDLファイルを解析して、コンポーネントのインターフェースや信号名を推測して仮設定を行います。
HDL側のポート名の命名規則である程度はインターフェースを指示することができますが、それなりに失敗も多いのであとで手作業で修正するのが普通です。
Signals & Interface
タブに移動して確認すると、カスタム命令信号が推測されていないので新しくインターフェースを追加します。
Name
ウィンドウの下にある_《add interface》
_を右クリック→Custom Instruction Slave
を選択します。
次にclk_en
,result
,clk
,reset
の信号を今し方作ったnios_custom_instruction_slave
の下にドラッグ&ドロップして移動させます。
空になったインターフェースは右クリック→Remove
を選択して削除します。
カスタム命令コンポーネントでは、HDLのポート名とインターフェースのSignal Type
は同じ名前になります。もし異なっているポートがあれば選択して修正を行います。
ここではclk_en
とresult
が違うインターフェースの信号タイプに設定されていた(信号タイプが赤く表示されている)ので、Name
リストで選択してSignal Type
を直しました。
Signal Type
の修正が終わったらName
ウィンドウのnios_custom_instruction_slave
を選択して、カスタム命令のパラメータを設定します。
今回のカスタム命令ではオペランドを取らない(出力のみ)なので、Operands
に0
を設定します。
最後にFinish
ボタンをクリックすればPlatform Designer用のコンポーネント設定ファイル(~_hw.tcl)が生成されます。
これでひとまずPlatform Designerからはコンポーネントとして認識されるようになったわけです。ただ、実はこのままでは本当にプロジェクトローカルなコンポーネントでしかなく使い回しがとてもやりにくくなっています。
どうしてかというと、Platform DesignerのComponent Editorで生成される定義ファイル(~_hw.tcl)はプロジェクトフォルダ直下に置かれるので、そのままではパッケージ化ができません。ip
フォルダにcloneすればOKというわけにはいかないのです。
<プロジェクトフォルダ>
├─ de0nano_top.qpf
├─ de0nano_top.qsf
├─ de0nano_top.qws
├─ nios2_ci_core.qsys ← Platform Desginerのモジュール
├─ ci_rand_component_hw.tcl ← いまはコンポーネント定義ファイルがここにある
├─ <ip>
│ └─ <ci_rand_xorshift128> ← このフォルダの下に完結させたい
│ └─ <hdl>
│ └─ ci_rand_xorshift128.v
:
:
はい。そこで他のコンポーネントのようにパッケージ化してみましょう。コンポーネント定義ファイルを操作するので、Platform Desginerはいったん終了させておきます。
パッケージ化とはいってもそれほど難しいことはありません。
コンポーネント定義ファイルはip
フォルダ以下のフォルダに格納しておけば自動で検索してくれますので配置場所にそれほど制約はありません。
場所の問題はコンポーネントが使用するファイル(HDLファイルやSDCファイル)の場所を指定する部分です。
ファイルパスについてはコンポーネント定義ファイルの場所からの相対パスで指定できるため、パスを少し修正すれば済みます。
具体的な箇所は、ci_rand_component_hw.tcl
のadd_fileset_file
コマンドの後ろ、PATH
オプションの部分です。
Component Editorで生成した状態ではプロジェクトフォルダからの相対パスになっています。これを所望のフォルダの相対に修正して、定義ファイルの場所を移してやれば完了です。
add_fileset_file ci_rand_xorshift128.v VERILOG PATH ip/ci_rand_xorshift128/hdl/ci_rand_xorshift128.v TOP_LEVEL_FILE
↓
add_fileset_file ci_rand_xorshift128.v VERILOG PATH hdl/ci_rand_xorshift128.v TOP_LEVEL_FILE
これで今後他のプロジェクトで再利用したい場合でも、ci_rand_xorshift128
フォルダをそのプロジェクトローカルのip
フォルダにコピーするだけでPlatform Desginerコンポーネントが使い回せるようになりました。
いちいちコピーするのが面倒くさいという場合は、ライブラリフォルダをどこかに用意してPlatform DesginerのIPフォルダ検索にパスを追加して参照させるような使い方もできます。
こういうものは使い回してナンボですから、どんどん作ってジャンジャン再利用して手間を省いていきましょう。
<プロジェクトフォルダ>
├─ de0nano_top.qpf
├─ de0nano_top.qsf
├─ de0nano_top.qws
├─ nios2_ci_core.qsys
├─ <ip>
│ └─ <ci_rand_xorshift128> ← このフォルダ以下で全て完結 °˖✧◝(⁰▿⁰)◜✧˖°
│ ├─ ci_rand_component_hw.tcl
│ └─ <hdl>
│ └─ ci_rand_xorshift128.v
:
:
3.3 カスタム命令を組み込む
ようやくカスタム命令コンポーネントを作ることができました。ここまでできればあとは簡単です。
Platform DesginerのIP Catalogにci_rand_component
が新しく追加されているので、これを選択してAdd
します。
コンポーネントをNiosIIの下まで移動させて、custom_instruction_master
と接続します。
最後にGenerate HDL
ボタンをクリックしてモジュールを生成しましょう。
Finish
をクリックすると最後にqipファイルをプロジェクトに追加するか尋ねられますのでOK
を選択しておきます。
qipファイルはPlatform Desginerが生成したモジュールのパッケージ定義ファイルで、これをプロジェクトに追加しておかないとQuartusでコンパイルできません。不用意に外さないように注意しましょう。
3.4 プロジェクトの合成
今回のデザインでは入出力ともにJTAGで行うので、モジュールのポートはクロックとリセットだけしかありません。トップモジュールはDE0-Nanoの50MHz入力とプッシュスイッチをそのまま配線するだけのシンプルなものです。
ピンアサインファイルやSDCファイルはDE0-Nanoのスケルトン(DE0_Nano_GOLDEN_TOP
)からそのまま流用しました。
module de0nano_top (
input wire CLOCK_50,
input wire [1:0] KEY
);
nios2_ci_core u_core (
.clk_clk (CLOCK_50),
.reset_reset_n (KEY[0])
);
endmodule
あとはQuartus側の作業ですが、こちらは詳細説明は省きます。
コンパイルレポートを確認してみると、カスタム命令は135LEで実装されているようですね。かなりコンパクトです。
4. NiosII SBTでアプリケーションとBSPを生成する
4.1 まずはHello World
ここからはNiosIIのソフトウェアの作業になります。
NiosII SBTを立ち上げて、まずはHello Worldからやっていきましょう。
ソフトウェア側でもプロジェクトから作成する必要があります。Quartusと同じとも言えますし、Eclipseの作法でもあります。
NiosII SBTではリンカやMakefileのツールチェーン、HALの各種設定も一緒になっているために、プロジェクト作成には専用のツールが用意されています。たとえ空のプロジェクトであっても生成ツールを使ってプロジェクトを作成する必要があります。
今回はまずHello Worldのテンプレートを生成させて動作を確認した後、それに手を加えていくことにしましょう。
Project Explorerの空欄を右クリックして、New
→NiosII Application and BSP from Template
を選択します。
続いて、SOPC Information File name:
にPlatform Desginerが生成したsopcinfoファイルを指定します。
sopcinfoファイルはモジュールを生成したときに一緒に作成されるソフトウェア用の定義ファイルで、qsysファイルと同じ場所(普通はプロジェクトフォルダ直下)に同名で生成されています。
今回の場合はプロジェクトフォルダ直下にnios2_ci_core.sopcinfo
というファイルがあるのでそれを指定します。
続いてProject name
にプロジェクト名を設定します。この名前でプロジェクトフォルダが作成されるため、既に同名のフォルダがあると入力時にエラーがでます。
Templates
からはHello World Small
を選択します。
Small版はHALのライブラリやPOSIXのドライバを削除してフットプリントを削った設定のもので、今回はコード領域もワーク領域も内蔵メモリにマッピングするためメモリ量が限られていてフルサイズのHALを実装できないのでこちらを選択します。
Finish
ボタンをクリックするとテンプレートのアプリケーションとBSPが生成されます。
4.2 NiosII SBT 20.1の不具合回避
18.1までのSBTではこれでいいのですが、19.1以降のSBTでは記事執筆時点でビルドツールチェーンに問題を抱えており、そのままではビルドエラーでコンパイルが完了しません。
どうも内部で呼び出しているwslpath
がMakefileの想定と異なった挙動をしているのが原因のようです。今のところアップデートの様子がなく、少々場当たり的ながらMakefileを直接修正して対処します。
この不具合は最新の QuartusPrime 20.1.1 で修正されました。既にそちら使われている場合はこの項の操作は不要です。4.3に進んでください。
まずアプリケーション側(今回ではci_testフォルダ)のMakefileを開き、以下の2点を修正します。
一つは132行付近のBUILD_PRE_PROCESS
の定義にtouch $(ELF).srec
を追加します。
BUILD_PRE_PROCESS :=
↓
BUILD_PRE_PROCESS := touch $(ELF).srec
二つ目は326行付近のAPP_LDFLAGS
の定義を-msys-lib=$(SYS_LIB)
に修正します。
APP_LDFLAGS += -msys-lib=$(call adjust-path-mixed,$(SYS_LIB))
↓
APP_LDFLAGS += -msys-lib=$(SYS_LIB)
この修正でひとまずビルドとダウンロードが通るようになります。
4.3 BSPの生成
NiosII SBTではRTOSやHAL、ペリフェラルドライバといったハードウェア部分についてはアプリケーション本体とは別にBSPプロジェクトとして管理されます。
BSPはPlatform Designerで設定したコンポーネントのインスタンス名やメモリマップ、設定値などのハードウェア情報をもとに、RTOSまたはベアメタルのHAL、ペリフェラルに紐付くドライバーがひとまとめにパッケージされています。
基本的には初回に1回BSPをビルドしておけばよく、アプリケーション側からはリンカで結合されます。
BSPの生成は、まずBSPの各種値を設定するところから始めます。今回のようにシンプルな構成の場合はデフォルトのままで問題ありません。とはいえ、デフォルト値に過度な期待をせずBSP生成前に設定値を確認しておくほうがよいでしょう。
Project Explorerの該当のBSPプロジェクト(今回の場合はci_test_bsp
)を右クリックし、NiosII
→BSP Editor...
を選択します。
BSP Editorが開き、各種タブで設定ができます。
今回はテンプレートで既に最小フットプリント構成になっているので、Linker Script
タブで全てのセクション配置が内蔵メモリ(onchip_memory2_0
)になっていることを確認しておけば十分です。GenerateボタンをクリックしてBSPを生成します。
BSPを生成したらEditorを閉じて、BSPプロジェクトのビルドを行います。
18.1までのSBTではBSPのビルドはアプリケーションビルド時に連動して行われるのですが、前述のとおり現時点ではツールチェーンに不具合があり、そのままビルドをするとリンカエラーでコンパイルが終わってしまいます。
リンカエラー回避のためにはBSPプロジェクト側だけ先にビルドする必要があります。しかしBSP側のMakefileの都合からか、どうやらBSPプロジェクトは一度ビルドしないとci_test_bsp
を右クリックしてもBuild Project
が選択できない状態になってしまうことがあります。
わりとデッドロック状態ですが、解決方法としては構わずアプリケーション側(ci_test
)からBuild Project
を実行してしまいます。
そのままでは当然エラーになります。ただし、この時点でBSP側のビルドは完了しているのでBSPビルドとしてはこれでOKです。
以上でBSPのビルドは完了です。
BSPのビルドはアプリケーション側のコーディングをしている範疇では最初に1回行うだけです。BSP側を変更する場合、例えばHALの設定変更やペリフェラルドライバの差し替えなどを行った際はBSPプロジェクトをCleanしてBSP Editor操作からやり直すことになります。
しかしPlatform Designerでハードウェアレベルでの変更をした場合はCleanだけでは不十分なことが多く、BSPプロジェクトそのものを再生成、あるいはテンプレート生成からやり直すのが無難でしょう。
5. NiosIIで実行する
5.1 Hello Worldのビルド
それではNiosIIでプログラムを実行させていきましょう。
まずはアプリケーションのビルドを行います。
と言っても特別なことをする必要はなく、アプリケーション側(ci_test
)のビルド操作をもう一回行うだけです。
BSP側のビルドが完了していればアプリケーション側のビルドも正しく完了します。
5.2 NiosIIへダウンロード
アプリケーションのビルドが終わったらNiosIIへプログラムのダウンロードを行います。
DE0-Nanoを接続してQuartusProgrammerでsofファイルを書き込んだら、ci_test
を右クリック→Run As
→3 NiosII Hardware
を選択します。
そのままターゲットのNiosIIに接続できる場合もあるものの、初回はだいたいTarget Connection
がエラーになります。
Run Configurationsが開くので、Target Connection
タブに移動してRefresh Connections
をクリックします。
Run Configurationsで接続がエラーになる原因はいくつかあり、代表的なものをあげてみます。
- FPGAがコンフィギュレーションされてない、または電源が入ってない
- 別のデザインがコンフィギュレーションされている
- クロックが間違っている、またはリセットがかかりっぱなし
- 指定するJTAGダウンロードケーブルが間違っている
- 他にFTDIのドライバを利用するものが居る(FTDI互換IC等を使っている場合)
- JTAGラインの信号品質の問題(互換書き込みケーブルや独自ボードなどでは起こりやすい)
気を付ければ済むものから回避の難しいものまであり、どれが原因かはケースバイケースではあります。ただ、自作ボードでJTAGラインの信号品質で悩まされるのは意外とよく出くわす問題かつ、後からはなかなか修正の難しい原因ですので基板アートワークの段階で十分なレビューをすることをおすすめしておきます。
DE0-NanoのようなJTAGダウンロードケーブル機能が内蔵された評価ボードのいいところは、そういう検証が済んでいるので動かない場合でも原因の特定が容易なことです。
ターゲットのFPGAがきちんとコンフィギュレーションされていて、JTAGダウンロードケーブル(USB-Blaster)が接続されていればRun
ボタンがアクティブになります。ボタンをクリックして実行させましょう。
ターゲットのNiosIIにプログラムがダウンロードされ、自動的にNiosII Console
タブ(JTAGコンソール)が開いてメッセージが表示されました。おめでとうございます!
一度分かれば大した手間でもないんですけど、最初はここまでたどり着くのはかなり大変なのではないでしょうか。あとは反復あるのみです。
さて、ほうっておくとずっとこのままなので、右の■ボタンをクリックしてJTAG接続を切断して終了させましょう。
5.3 カスタム命令を使ってみる
やっと本題です。
ここまででとりあえずNiosIIを実装して、その上で何らかのプログラムを実行させることができました。ですが記事の目的であるカスタム命令を動かすのはまだできていません。
ハードウェア的には既に実装されているカスタム命令を、ソフトウェアからはどのように使えばいいのでしょうか。
と言っても難しいことは何もありません。仕組みはすでにBSPプロジェクトの中に用意されています。具体的にはsystem.h
の中で、ALT_CI_<インスタンス名>
というマクロで定義されています。
今回の場合ではALT_CI_CI_RAND_COMPONENT_0
というマクロが宣言されています。オペランドを持たない命令なので、引数のないマクロになっています。
ソースコード側ではsystem.h
をインクルードしてALT_CI_CI_RAND_COMPONENT_0
を読み出す記述をすれば、カスタム命令を使ったバイナリになるわけです。
サンプルとして、ソフトウェアで実装したXorshiftとカスタム命令と比較するコードを作ってみました。
#include <stdint.h>
#include "sys/alt_stdio.h"
#include "system.h"
uint32_t xorshift128()
{
static uint32_t x = 123456789;
static uint32_t y = 362436069;
static uint32_t z = 521288629;
static uint32_t w = 88675123;
uint32_t t;
t = x ^ (x << 11);
x = y; y = z; z = w;
return w = (w ^ (w >> 19)) ^ (t ^ (t >> 8));
}
int main()
{
alt_putstr("Hello from Nios II!\n");
for (int i=0 ; i<10 ; i++) {
uint32_t r1 = ALT_CI_CI_RAND_COMPONENT_0; // カスタム命令によるxorshift
uint32_t r2 = xorshift128(); // 関数によるxorshift
alt_printf("ci_rand=0x%x, func_rand=0x%x\n", r1, r2);
}
while (1);
return 0;
}
実行した結果が下の図です。xorshift128()
とカスタム命令をそれぞれ10回呼び出して値を表示しています。単純なものですが擬似乱数の挙動がよく分かります。
一つの結果だけを見るとソフトウェアで実装したXorshiftはランダムな値を返しているように見えます。ところが何回か結果を比較すると、実行ごとに同じ数列になっているのがわかります。一方でカスタム命令で実装したXorshiftでは毎回違う数列を返しています。
同じアルゴリズムの擬似乱数ですから両者はおなじ数列を生成しているはずです。しかしカスタム命令の中のHDLはソフトウェアやプロセッサの動作とは無関係に動くため、結果的にソフトウェアからは予測できない値に見えるわけです。
事実、カスタム命令のHDLをシミュレーションで見てみると、生成される数列はソフトウェアのXorshift関数が返す数列と同じ値になっています。
もちろんリセットの解除から命令を実行するタイミングまで、クロック単位で厳密に一致していればカスタム命令であっても同じ数列を返すことになります。そういう意味ではいくらランダム性が高くても自然乱数ではなく擬似乱数の範疇ではあります。とはいえ、現実で狙って再現するのはなかなかの難易度でしょう。
またXorshiftがいかに軽量なアルゴリズムとはいえ、バレルシフタを持たない超軽量のNiosII /eコアでは演算に300クロック以上費やしています。高性能な/fコアにしても10クロックを下回らないでしょう。カスタム命令ではこれを1命令で行うわけですから、性能へのインパクトもなかなかです。
このように、ユーザーでハードウェアで実装する命令を自由に追加できるカスタム命令は、少しの手間でソフトウェアの性能を大きく広げることができるオプションです。
無論ピッタリ嵌まる場合とそうでない場合とありますが、そのさじ加減も含めてうまく付き合っていけば力強い手段となってくれることうけあいです。
それでは、皆さま良いカスタム命令ライフを!
6. 参考
NiosII プロセッサー・サポート
https://www.intel.co.jp/content/www/jp/ja/programmable/products/processors/support.html
NiosII Custom Instruction User Guide
https://www.intel.com/content/www/us/en/programmable/documentation/cru1439932898327.html
Intel Quartus Prime Standard Edition User Guide : Platform Designer
https://www.intel.com/content/www/us/en/programmable/documentation/jrw1529444674987.html
Terasic DE0-Nano
https://www.terasic.com.tw/cgi-bin/page/archive.pl?Language=English&CategoryNo=139&No=593&PartNo=1
Xorshift RNGs
https://www.jstatsoft.org/article/view/v008i14
この記事で作成したプロジェクト
https://github.com/osafune/nios2_ci_sample