CPUを含めた回路はFPGAが便利
ARMはハードマクロとして物理的に組み込まれているので別ですが、
NIOSやMicroblazeなどのソフトマクロCPUは便利です。
私が論理回路からのエンジニアだったので、ソフト処理は極力少なくしようと設計していました。
そんな中、便利だなぁ~と思ったのが、
- CPUが何個も配置できる
- メモリサイズが変更できる
- 内部メモリにそのままプログラムが保存できる
でした。
CPUが何個も配置できる
FPGAのリソースもありますが、これは便利でした。
特に、レジスタのリード/ライトや低周期アクセスの通信系はCPU1に任せ、
Etherなど処理を速くするものは、CPU2で専門にさせるなどしていました。
メモリサイズが変更できる
CPU1とCPU2のプログラム量に応じてCPUが使用するメモリサイズを変更できるので、空いた分、User Logicで使うことができました。
内部メモリにそのままプログラムが保存できる
CPUが使うプログラムメモリを内蔵RAMに指定できることです。
これにより、外部(基板上)にプログラムを格納するFlashが不要になる。
ことでした。
Nios IIでは、「Nios II Software Build Tools for Eclipse」からCPU1またはCPU2を選択して書き込みができ、デバッグも容易でした。
どのような仕組み?
簡単に言うと、内蔵RAMの初期値ファイルをCPUのプログラムデータとしておく。
ということです。
下がイメージ図です。
① CPU1とCPU2のC言語ソースファイルを作成します。
「Nios II Software Build Tools for Eclipse」で、CPU1用とCPU2用を作ります。
② それぞれ「Nios II Software Build Tools for Eclipse」でコンパイルします。
コンパイル後、「Make Targets」で、内蔵RAMの初期値フォーマットに変換します。
③ Quartusを実行し、コンフィグレーションコードを生成します。
最初の場合、「Make Targets」で作成した「meminit.qip」をそれぞれQuartusに設定します。
以降は、関連付けされているので、設定する必要はないです。
④ コンフィグレーションコードを基板上のROMに書き込みます。
あとは、電源を投入するとROMの内容がFPGAに書き込まれ、自走します。
補足)
Quartusですでにコンパイルが済んでおり、NIOSのプログラムのみ修正した場合は、
- 「Nios II Software Build Tools for Eclipse」でソースをコンパイル
- 「Nios II Software Build Tools for Eclipse」で「Make Targets」を実行
- 「Quartus」で[Processing] ⇒ [Update Memory Initialization File] を実行
- 「Quartus」でAssembler を実行
の手順でコンフィグレーションコードを生成できます。
すでに論理合成と配置配線が完了しているので、内蔵RAMの初期値データだけ変更するだけで済み、Quartus実行時間も少なくてすみます。
最後に
仕組みを理解すると、手順のイメージが付くと思います。
Niosコードを修正した場合の手順、RTLソースを修正した場合の手順、Platform Designerで構成を修正した場合の手順などの実行イメージが付くと思います。
NIOSでprintfを使用する場合
NIOSを使った時のデバッグ出力についてです。
JTAG_UARTを用いてprintfでデバッグ出力している場合は気を付ける場合があります。
USB Blasterと繋げている場合は出力表示されますが、
未接続の場合は、printfで止まる可能性があるので、気を付けてください。
特に、FPGA内蔵RAMにプログラムを保存し、USB Blasterと接続せず自走させて動かす時、このことを忘れていて動かない、ってこともあります。
記述例
実機評価(確認)中、SignalTapとprintfの表示とでデバッグしていくと思います。
その時、printfをバンバン入れていくと思いますが、製品として出す前にprintfを1つ1つ削除するのは、面倒だと思います。
そこで、下記のようにprintfにラッパー(display)を被せるようにすれば、一発でprintfを無効にできます。
使い方
上記述例を基準に説明します。
デバッグ表示させたいソースに「DebugDisplay.h」をインクルードし、printfを使用せず、ラッパーのdisplay()を使用して、printfの引数のように記述します。
(13,25,28行目の記述例)
デバッグ時はこの記述例の「DebugDisplay.h」で良いです。
デバッグが完了し、USB Blasterを接続せずに自走させて動作確認する場合は、「DebugDisplay.h」の4行目を
#define DEBUG_MODE (0)
に変更して、再コンパイルして使用します。
このDEBUG_MODEが0になると、8行目のdisplayが有効になり、printf()が使用されなくなります。
最後に
実機動作確認で、気付きにくい所なので、注意してください。
また、printfが消えることにより、処理時間が変わります。
アクセス間隔が短くなり、今までうまくいっていた所がうまくいかなくなったりするので、その場合は、関数sleep()などでアクセス間隔を長くすると動く場合があります。
上位階層でタスク実行
対向モデル内のタスク実行についてです。
例えば、UARTが2ポート以上あるターゲット回路に便利です。
下図のような場合です。
対向モデルの構想
TestScenarioで対向モデル(MODEL_UART)を制御し、送受信を考えます
対向モデルにデータを渡すと対向モデルからUserLogicにシリアル送信し、対向モデルがシリアル受信を完了すると読めるようにします。
これを実現する為、タスクを利用します。
しかも、TestBenchに記述するとUARTポートが増えた時に面倒なので、対向モデル内に記述することで、ポートが増えれば、その分配置すればよくなります。
尚、「.」で下階層の信号にアクセスできるので、それを利用します。
対向モデルの記述例
前回のテストベンチ同様、ボーレートを自由に変更できるように、parameterを使用せず、real宣言を使用します。
下が、MODEL_UARTの記述例となります。
ここでは、タスクTXを実行するとtcTXとtsTXの時間に従って、シリアル送信します。
また、受信も同様に、RXがHigh→LowになるとtcRXに従って受信データを取り込み、tsRX後にタスクRXで受信データを読み取れるようになっています。
受信側では、信号RXはtcRX時間は状態を保持していることが前提(ひげ、ハザードは無い)の為、tcRX未満の変化があるとRxErrorが1になるようになっています。
テストシナリオ記述例
テストシナリオでは、対向モデルのインスタンスUART1又はUART2を指定してタスクを実行するだけとなります。
また、ボーレートを変更したい場合も、同様にして変更することができます。
下にテストシナリオの記述例を示します。
ボーレートを変更し、送信と受信をテストシナリオから制御しています。
タスクTXは、実行するとシリアル送信を開始し、送信を完了するとタスクが終了します。
また、RXは、8bit分のシリアル受信が終了するとタスクが終了します。
タスクRXが終了すると次の受信の為にタスクRXを実行しておく必要があります。
タイミング波形
最後に
対向モデル内でタスク記述し、上位階層で実行することが可能です。
オブジェクト指向に近い考え方です。
また、
案件の機能や検証内容に合わせて、どのような対抗モデルにするか検討する必要があります。
今回の例では、タスクTXを実行すると送信完了までタスクが終了しない、受信が完了する前にタスクRXを実行しないといけない構成になっています。
やり方次第(記述次第)で、
タスクTXを実行すると内部バッファにデータを格納し、内部バッファが空になるまでシリアル送信を継続する構成や、8bit分のシリアル受信する度に内部バッファに格納し、タスクRXを実行する度に内部バッファのデータを読み出せる構成にすることで、タスクがWaitすることは無くなります。
テストベンチでの双方向端子処理
ターゲット回路に双方向端子があり、テストベンチ内で双方向端子処理する場合です。
例えば、
外部にマイコンがあり、データバスが双方向端子でリード/ライトを行うLSI製品になります。
構成例
どのようなテストベンチ構成になるか、以下に示します。
D[31:0]が双方向端子(Inout)になります。
この端子は、LSI TopでDE=1の時、DOを出力し、DE=0の時、Hi-Zになります。
( CS_N=0 且つ RD_N=0の時、DE=1になります。)
テストベンチ記述例
テストベンチの記述例を以下に示します。
45~46行目はテストベンチでの双方向の記述となります。
60行目からライトとリードタスクになります。
テストベンチでは、Logicのように双方向切替信号(DE)のようなものは必要ありません。
これは、DI[31:0]がreg宣言した信号のため、Hi-Zにしたい時には{32{1'bZ}}を代入すれば良いためです。
タイミング
テストベンチのタスクWrCPU(ライトタスク)とタスクRdCPU(リードタスク)を実行した時のタイミング図を示します。
最後に
製品になる場合は論理回路をイメージして記述する必要がありますが、テストベンチでは、reg宣言してしまえば好きな所で Hi-Zや不定値を代入できます。
それを利用してしまえば、双方向端子と繋がる信号だけwire宣言した信号と繋げれば良いだけです。
タスク内で制御
テストベンチでのタスク記述についてです。
例えば、
NIOSとUser LogicをAvalon-MMでつなげ、レジスタアクセスする場合、
シミュレーションのテストベンチでは、ターゲット回路をUser Logicとし、NIOS部分をテストシナリオで処理を記述し、TaskでAvalon-MMの生成を行ったりします。
ただ、Avalon-MMでは、WaitRequestがHighの時は待たせたり、ReadValidがHighの時にリードデータが確定するなど、Task内でも制御が必要になります。
こういう時、私は、Task内でwhile文を使って待たせています。
Avalon-MMライトタスク
タスクWrCPUがテストシナリオで実行すると、CLOCK↑で制御信号(AVA_WE,AVA_AD,AVA_WD)がアサートし、AVA_WT(WaitRequest)がLowになると終了します。
ライトタスクの記述例を以下に示します。
16行目でAVA_WTがLowの時、Flagが1になり、
Loopで14行目に行った時にFlagが1のため、while文を抜けます。
これをタイミング図を示したものを下に示します。
Avalon-MMリードタスク
タスクRdCPUがテストシナリオで実行すると、CLOCK↑で制御信号(AVA_RE,AVA_AD)がアサートし、AVA_WT(WaitRequest)がLowになるとAVA_RE,AVA_ADをネゲートします。
また、リードバリッド(AVA_VL)がHighになるとリードデータを取得し、終了します。
リードタスクの記述例を以下に示します。
15行目でAVA_WTがLowの時、AVA_REとAVA_ADは0になり、
19行目でAVA_VLがHighの時、リードデータをDataに格納し、Flagを1にします。
Loopで13行目に行った時にFlagが1のため、while文を抜けます。
ポイントは、19行目で「else if」では無く、「if」としている所です。
これをタイミング図を示したものを下に示します。
テストシナリオの記述例
上記タスクを使って、テストシナリオを記述する場合を以下に示します。
これは、一例ですので、エンジニアそれぞれの記述があるかと思います。
最後に
タスクは同じ事を繰り返す時によく使います。
verilogの参考書では、このような説明は無いかと思います。
テストベンチに限り、こういう記述ができますので、実際の回路にする部分では論理合成が通らないので、注意してください。