LoginSignup
24
6

More than 3 years have passed since last update.

NiosIIカスタム命令のつくりかた

Last updated at Posted at 2020-12-22

はじめに

こんにちは。
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ではこの「カスタマイズできる部分」の中に、ユーザーが自由に追加できるカスタム命令があるわけです。
nios2-hardware-acceleration.jpg
(※画像は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そのものの説明などはここでは省きます。

ci_rand_xorshift128.v
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を使いました。たまたま手元に積んであったからです。
de0nano_thumb.jpg
(※画像は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のクロック入力で設定されているので、今回はそのまま使います。
pd_001_a.png

次に左上のIP CalalogからNios II Processorを選択してAddをクリックします。
コンポーネントを探しづらい場合は検索入力欄にniosと入力すると一番下にでてきます。
pd_002_a.png

System Contentsリストに追加されるまえにプロセッサの設定ウィンドウが開きます。
今回は無償版のNiosII /eを使うので、MainタブのNios II CoreNios II/eを選択してFinishをクリックします。
いくつかエラーが出ていますが、これらはあとで修正するので今は無視します。
pd_003_a.png

続いてNiosIIの実行に必要なペリフェラルを追加していきます。
今回使うペリフェラルは以下の4つです。

  • On-Chip Memory(プログラムの格納と実行時メモリ。32kバイトのRAMを割り当て)
  • System ID(NiosII SBTでターゲットボードの確認に使用)
  • Timer(HALでシステムタイムティックに使用する。ただし今回は未使用)
  • JTAG-UART(HALの標準入出力ストリームに使用する)

pd_004_a.png
これらのコンポーネントとメモリマップ、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 memoryException vector memoryの両方ともonchip_memory2_0.s1を選択します。
これでエラーが消えるので、Finishをクリックして閉じます。
pd_005_a.png
ここまでできたらFileSaveでいったんデザインファイルを保存しておきます。
保存するファイル名がそのままモジュール名になるので、今回はnios2_ci_coreにしましょう。

3.2 カスタム命令コンポーネントの作成

ようやく本題のカスタム命令コンポーネント作成までたどり着きました。
この時点ではカスタム命令の動作をするHDLはあるものの、コンポーネントしてはまだ成立していません。
ここでは新しくコンポーネントを作成する手順を紹介していきます。

IP Catalogの一番上にあるNew Component...をダブルクリック(または選択してAddボタンをクリック)して、Component Editorを開きます。
pd_006_a.png

Component EditorのComponent TypeタブのNameDisplay nameci_rand_componentを記入します。
Nameはインスタンス時に識別されるコンポーネント名。Display nameはIP Catalogリストに表示される名前です。
これらはの名前は有効なIPコア(IP Catalogリストに表示されるもの)でユニークな名前にしなければなりません。
ほどほどに長い名前をつけましょう。
pd_007_a.png

次にFilesタブに移動して、Add File...で予め用意してあるHDLファイルを追加します。
その後Analyze Synthesis Filesをクリックすると追加したHDLファイルを解析して、コンポーネントのインターフェースや信号名を推測して仮設定を行います。
HDL側のポート名の命名規則である程度はインターフェースを指示することができますが、それなりに失敗も多いのであとで手作業で修正するのが普通です。
pd_008_a.png

Signals & Interfaceタブに移動して確認すると、カスタム命令信号が推測されていないので新しくインターフェースを追加します。
Nameウィンドウの下にある《add interface》を右クリック→Custom Instruction Slaveを選択します。
pd_009_a.png

次にclk_en,result,clk,resetの信号を今し方作ったnios_custom_instruction_slaveの下にドラッグ&ドロップして移動させます。
pd_010_a.png

空になったインターフェースは右クリック→Removeを選択して削除します。
pd_011_a.png

カスタム命令コンポーネントでは、HDLのポート名とインターフェースのSignal Typeは同じ名前になります。もし異なっているポートがあれば選択して修正を行います。
ここではclk_enresultが違うインターフェースの信号タイプに設定されていた(信号タイプが赤く表示されている)ので、Nameリストで選択してSignal Typeを直しました。
pd_012_a.png

Signal Typeの修正が終わったらNameウィンドウのnios_custom_instruction_slaveを選択して、カスタム命令のパラメータを設定します。
今回のカスタム命令ではオペランドを取らない(出力のみ)なので、Operands0を設定します。
最後にFinishボタンをクリックすればPlatform Designer用のコンポーネント設定ファイル(~_hw.tcl)が生成されます。
pd_013_a.png

これでひとまず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.tcladd_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

pd_013_b.png

これで今後他のプロジェクトで再利用したい場合でも、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します。
pd_014_a.png

コンポーネントをNiosIIの下まで移動させて、custom_instruction_masterと接続します。
最後にGenerate HDLボタンをクリックしてモジュールを生成しましょう。
pd_015_a.png

Finishをクリックすると最後にqipファイルをプロジェクトに追加するか尋ねられますのでOKを選択しておきます。
qipファイルはPlatform Desginerが生成したモジュールのパッケージ定義ファイルで、これをプロジェクトに追加しておかないとQuartusでコンパイルできません。不用意に外さないように注意しましょう。

3.4 プロジェクトの合成

今回のデザインでは入出力ともにJTAGで行うので、モジュールのポートはクロックとリセットだけしかありません。トップモジュールはDE0-Nanoの50MHz入力とプッシュスイッチをそのまま配線するだけのシンプルなものです。
ピンアサインファイルやSDCファイルはDE0-Nanoのスケルトン(DE0_Nano_GOLDEN_TOP)からそのまま流用しました。

de0nano_top.v
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で実装されているようですね。かなりコンパクトです。
pd_016_a.png

4. NiosII SBTでアプリケーションとBSPを生成する

4.1 まずはHello World

ここからはNiosIIのソフトウェアの作業になります。
NiosII SBTを立ち上げて、まずはHello Worldからやっていきましょう。

ソフトウェア側でもプロジェクトから作成する必要があります。Quartusと同じとも言えますし、Eclipseの作法でもあります。
NiosII SBTではリンカやMakefileのツールチェーン、HALの各種設定も一緒になっているために、プロジェクト作成には専用のツールが用意されています。たとえ空のプロジェクトであっても生成ツールを使ってプロジェクトを作成する必要があります。
今回はまずHello Worldのテンプレートを生成させて動作を確認した後、それに手を加えていくことにしましょう。

Project Explorerの空欄を右クリックして、NewNiosII Application and BSP from Templateを選択します。
sbt_001_a.png

続いて、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が生成されます。
sbt_002_a.png

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

sbt_003_a.png

二つ目は326行付近のAPP_LDFLAGSの定義を-msys-lib=$(SYS_LIB)に修正します。

APP_LDFLAGS += -msys-lib=$(call adjust-path-mixed,$(SYS_LIB))
 ↓
APP_LDFLAGS += -msys-lib=$(SYS_LIB)

sbt_004_a.png
この修正でひとまずビルドとダウンロードが通るようになります。

4.3 BSPの生成

NiosII SBTではRTOSやHAL、ペリフェラルドライバといったハードウェア部分についてはアプリケーション本体とは別にBSPプロジェクトとして管理されます。
BSPはPlatform Designerで設定したコンポーネントのインスタンス名やメモリマップ、設定値などのハードウェア情報をもとに、RTOSまたはベアメタルのHAL、ペリフェラルに紐付くドライバーがひとまとめにパッケージされています。
基本的には初回に1回BSPをビルドしておけばよく、アプリケーション側からはリンカで結合されます。

BSPの生成は、まずBSPの各種値を設定するところから始めます。今回のようにシンプルな構成の場合はデフォルトのままで問題ありません。とはいえ、デフォルト値に過度な期待をせずBSP生成前に設定値を確認しておくほうがよいでしょう。

Project Explorerの該当のBSPプロジェクト(今回の場合はci_test_bsp)を右クリックし、NiosIIBSP Editor...を選択します。
sbt_005_a.png

BSP Editorが開き、各種タブで設定ができます。
今回はテンプレートで既に最小フットプリント構成になっているので、Linker Scriptタブで全てのセクション配置が内蔵メモリ(onchip_memory2_0)になっていることを確認しておけば十分です。GenerateボタンをクリックしてBSPを生成します。
sbt_006_a.png

BSPを生成したらEditorを閉じて、BSPプロジェクトのビルドを行います。
18.1までのSBTではBSPのビルドはアプリケーションビルド時に連動して行われるのですが、前述のとおり現時点ではツールチェーンに不具合があり、そのままビルドをするとリンカエラーでコンパイルが終わってしまいます。

リンカエラー回避のためにはBSPプロジェクト側だけ先にビルドする必要があります。しかしBSP側のMakefileの都合からか、どうやらBSPプロジェクトは一度ビルドしないとci_test_bspを右クリックしてもBuild Projectが選択できない状態になってしまうことがあります。

わりとデッドロック状態ですが、解決方法としては構わずアプリケーション側(ci_test)からBuild Projectを実行してしまいます。
そのままでは当然エラーになります。ただし、この時点でBSP側のビルドは完了しているのでBSPビルドとしてはこれでOKです。
sbt_007_a.png
以上でBSPのビルドは完了です。

BSPのビルドはアプリケーション側のコーディングをしている範疇では最初に1回行うだけです。BSP側を変更する場合、例えばHALの設定変更やペリフェラルドライバの差し替えなどを行った際はBSPプロジェクトをCleanしてBSP Editor操作からやり直すことになります。
しかしPlatform Designerでハードウェアレベルでの変更をした場合はCleanだけでは不十分なことが多く、BSPプロジェクトそのものを再生成、あるいはテンプレート生成からやり直すのが無難でしょう。

5. NiosIIで実行する

5.1 Hello Worldのビルド

それではNiosIIでプログラムを実行させていきましょう。

まずはアプリケーションのビルドを行います。
と言っても特別なことをする必要はなく、アプリケーション側(ci_test)のビルド操作をもう一回行うだけです。
BSP側のビルドが完了していればアプリケーション側のビルドも正しく完了します。
sbt_008_a.png

5.2 NiosIIへダウンロード

アプリケーションのビルドが終わったらNiosIIへプログラムのダウンロードを行います。
DE0-Nanoを接続してQuartusProgrammerでsofファイルを書き込んだら、ci_testを右クリック→Run As3 NiosII Hardwareを選択します。
sbt_009_a.png

そのままターゲットのNiosIIに接続できる場合もあるものの、初回はだいたいTarget Connectionがエラーになります。
Run Configurationsが開くので、Target Connectionタブに移動してRefresh Connectionsをクリックします。
sbt_010_a.png

Run Configurationsで接続がエラーになる原因はいくつかあり、代表的なものをあげてみます。

  • FPGAがコンフィギュレーションされてない、または電源が入ってない
  • 別のデザインがコンフィギュレーションされている
  • クロックが間違っている、またはリセットがかかりっぱなし
  • 指定するJTAGダウンロードケーブルが間違っている
  • 他にFTDIのドライバを利用するものが居る(FTDI互換IC等を使っている場合)
  • JTAGラインの信号品質の問題(互換書き込みケーブルや独自ボードなどでは起こりやすい)

気を付ければ済むものから回避の難しいものまであり、どれが原因かはケースバイケースではあります。ただ、自作ボードでJTAGラインの信号品質で悩まされるのは意外とよく出くわす問題かつ、後からはなかなか修正の難しい原因ですので基板アートワークの段階で十分なレビューをすることをおすすめしておきます。

DE0-NanoのようなJTAGダウンロードケーブル機能が内蔵された評価ボードのいいところは、そういう検証が済んでいるので動かない場合でも原因の特定が容易なことです。
ターゲットのFPGAがきちんとコンフィギュレーションされていて、JTAGダウンロードケーブル(USB-Blaster)が接続されていればRunボタンがアクティブになります。ボタンをクリックして実行させましょう。
sbt_011_a.png

ターゲットのNiosIIにプログラムがダウンロードされ、自動的にNiosII Consoleタブ(JTAGコンソール)が開いてメッセージが表示されました。おめでとうございます!
一度分かれば大した手間でもないんですけど、最初はここまでたどり着くのはかなり大変なのではないでしょうか。あとは反復あるのみです。

さて、ほうっておくとずっとこのままなので、右の■ボタンをクリックしてJTAG接続を切断して終了させましょう。
sbt_012_a.png

5.3 カスタム命令を使ってみる

やっと本題です。

ここまででとりあえずNiosIIを実装して、その上で何らかのプログラムを実行させることができました。ですが記事の目的であるカスタム命令を動かすのはまだできていません。
ハードウェア的には既に実装されているカスタム命令を、ソフトウェアからはどのように使えばいいのでしょうか。

と言っても難しいことは何もありません。仕組みはすでにBSPプロジェクトの中に用意されています。具体的にはsystem.hの中で、ALT_CI_<インスタンス名>というマクロで定義されています。
sbt_013_a.png

今回の場合では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回呼び出して値を表示しています。単純なものですが擬似乱数の挙動がよく分かります。
sbt_014_a.png

一つの結果だけを見るとソフトウェアで実装したXorshiftはランダムな値を返しているように見えます。ところが何回か結果を比較すると、実行ごとに同じ数列になっているのがわかります。一方でカスタム命令で実装したXorshiftでは毎回違う数列を返しています。
同じアルゴリズムの擬似乱数ですから両者はおなじ数列を生成しているはずです。しかしカスタム命令の中のHDLはソフトウェアやプロセッサの動作とは無関係に動くため、結果的にソフトウェアからは予測できない値に見えるわけです。
事実、カスタム命令のHDLをシミュレーションで見てみると、生成される数列はソフトウェアのXorshift関数が返す数列と同じ値になっています。
sim_001_a.png

もちろんリセットの解除から命令を実行するタイミングまで、クロック単位で厳密に一致していればカスタム命令であっても同じ数列を返すことになります。そういう意味ではいくらランダム性が高くても自然乱数ではなく擬似乱数の範疇ではあります。とはいえ、現実で狙って再現するのはなかなかの難易度でしょう。

また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


24
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
6