nao-milkの経験ブログ

25年間の半導体エンジニア経験で知り得た内容を記載したブログです。

CPUを含めた回路はFPGAが便利

f:id:nao-milk:20210325165741p:plain

ARMはハードマクロとして物理的に組み込まれているので別ですが、

NIOSやMicroblazeなどのソフトマクロCPUは便利です。

 

私が論理回路からのエンジニアだったので、ソフト処理は極力少なくしようと設計していました。

そんな中、便利だなぁ~と思ったのが、

  1. CPUが何個も配置できる
  2. メモリサイズが変更できる
  3. 内部メモリにそのままプログラムが保存できる

でした。

f:id:nao-milk:20210324202053p:plain

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のプログラムデータとしておく。

ということです。

下がイメージ図です。

f:id:nao-milk:20210325161757p:plain

仕組みイメージ図

① 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のプログラムのみ修正した場合は、

  1.  「Nios II Software Build Tools for Eclipse」でソースをコンパイル
  2.  「Nios II Software Build Tools for Eclipse」で「Make Targets」を実行
  3.  「Quartus」で[Processing] ⇒ [Update Memory Initialization File] を実行
  4.  「Quartus」でAssembler を実行

 の手順でコンフィグレーションコードを生成できます。

 すでに論理合成と配置配線が完了しているので、内蔵RAMの初期値データだけ変更するだけで済み、Quartus実行時間も少なくてすみます。

 

最後に

仕組みを理解すると、手順のイメージが付くと思います。

Niosコードを修正した場合の手順、RTLソースを修正した場合の手順、Platform Designerで構成を修正した場合の手順などの実行イメージが付くと思います。

 

NIOSでprintfを使用する場合

f:id:nao-milk:20210323082036p:plain

NIOSを使った時のデバッグ出力についてです。

JTAG_UARTを用いてprintfでデバッグ出力している場合は気を付ける場合があります。

 

USB Blasterと繋げている場合は出力表示されますが、

未接続の場合は、printfで止まる可能性があるので、気を付けてください。

 

特に、FPGA内蔵RAMにプログラムを保存し、USB Blasterと接続せず自走させて動かす時、このことを忘れていて動かない、ってこともあります。

 

 

記述例

実機評価(確認)中、SignalTapとprintfの表示とでデバッグしていくと思います。

その時、printfをバンバン入れていくと思いますが、製品として出す前にprintfを1つ1つ削除するのは、面倒だと思います。

 

そこで、下記のようにprintfにラッパー(display)を被せるようにすれば、一発でprintfを無効にできます。

 

f:id:nao-milk:20210322232745p:plain

 

使い方

上記述例を基準に説明します。

 

デバッグ表示させたいソースに「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()などでアクセス間隔を長くすると動く場合があります。

 

上位階層でタスク実行

f:id:nao-milk:20210320012140p:plain

対向モデル内のタスク実行についてです。

 

例えば、UARTが2ポート以上あるターゲット回路に便利です。

下図のような場合です。

f:id:nao-milk:20210320003559p:plain

 

対向モデルの構想

TestScenarioで対向モデル(MODEL_UART)を制御し、送受信を考えます

対向モデルにデータを渡すと対向モデルからUserLogicにシリアル送信し、対向モデルがシリアル受信を完了すると読めるようにします。

 

これを実現する為、タスクを利用します。

しかも、TestBenchに記述するとUARTポートが増えた時に面倒なので、対向モデル内に記述することで、ポートが増えれば、その分配置すればよくなります。

尚、「.」で下階層の信号にアクセスできるので、それを利用します。

 

対向モデルの記述例

前回のテストベンチ同様、ボーレートを自由に変更できるように、parameterを使用せず、real宣言を使用します。

 

下が、MODEL_UARTの記述例となります。

f:id:nao-milk:20210320005028p:plain

 

ここでは、タスクTXを実行するとtcTXとtsTXの時間に従って、シリアル送信します。

また、受信も同様に、RXがHigh→LowになるとtcRXに従って受信データを取り込み、tsRX後にタスクRXで受信データを読み取れるようになっています。

 

受信側では、信号RXはtcRX時間は状態を保持していることが前提(ひげ、ハザードは無い)の為、tcRX未満の変化があるとRxErrorが1になるようになっています。

 

テストシナリオ記述例

テストシナリオでは、対向モデルのインスタンスUART1又はUART2を指定してタスクを実行するだけとなります。

また、ボーレートを変更したい場合も、同様にして変更することができます。

 

下にテストシナリオの記述例を示します。

f:id:nao-milk:20210320010015p:plain

ボーレートを変更し、送信と受信をテストシナリオから制御しています。

 

タスクTXは、実行するとシリアル送信を開始し、送信を完了するとタスクが終了します。

また、RXは、8bit分のシリアル受信が終了するとタスクが終了します。

タスクRXが終了すると次の受信の為にタスクRXを実行しておく必要があります。

 

タイミング波形

 

f:id:nao-milk:20210320010436p:plain

送信波形

f:id:nao-milk:20210320010527p:plain

受信波形

 

最後に

対向モデル内でタスク記述し、上位階層で実行することが可能です。

オブジェクト指向に近い考え方です。

また、

案件の機能や検証内容に合わせて、どのような対抗モデルにするか検討する必要があります。

 

今回の例では、タスクTXを実行すると送信完了までタスクが終了しない、受信が完了する前にタスクRXを実行しないといけない構成になっています。

 

やり方次第(記述次第)で、

タスクTXを実行すると内部バッファにデータを格納し、内部バッファが空になるまでシリアル送信を継続する構成や、8bit分のシリアル受信する度に内部バッファに格納し、タスクRXを実行する度に内部バッファのデータを読み出せる構成にすることで、タスクがWaitすることは無くなります。

 

テストベンチでの双方向端子処理

f:id:nao-milk:20210321013529p:plain

ターゲット回路に双方向端子があり、テストベンチ内で双方向端子処理する場合です。

 

例えば、

外部にマイコンがあり、データバスが双方向端子でリード/ライトを行うLSI製品になります。

 

構成例

どのようなテストベンチ構成になるか、以下に示します。

f:id:nao-milk:20210321011235p:plain

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}}を代入すれば良いためです。

 

f:id:nao-milk:20210321011512p:plain

 

タイミング

テストベンチのタスクWrCPU(ライトタスク)とタスクRdCPU(リードタスク)を実行した時のタイミング図を示します。

f:id:nao-milk:20210321012356p:plain

 

最後に

製品になる場合は論理回路をイメージして記述する必要がありますが、テストベンチでは、reg宣言してしまえば好きな所で Hi-Zや不定値を代入できます。

それを利用してしまえば、双方向端子と繋がる信号だけwire宣言した信号と繋げれば良いだけです。

 

 

 

タスク内で制御

f:id:nao-milk:20210319220543p:plain

テストベンチでのタスク記述についてです。

 

例えば、

NIOSとUser LogicをAvalon-MMでつなげ、レジスタアクセスする場合、

シミュレーションのテストベンチでは、ターゲット回路をUser Logicとし、NIOS部分をテストシナリオで処理を記述し、TaskでAvalon-MMの生成を行ったりします。

 

f:id:nao-milk:20210319212753p:plain

 

ただ、Avalon-MMでは、WaitRequestがHighの時は待たせたり、ReadValidがHighの時にリードデータが確定するなど、Task内でも制御が必要になります。

 

こういう時、私は、Task内でwhile文を使って待たせています。

 

Avalon-MMライトタスク

タスクWrCPUがテストシナリオで実行すると、CLOCK↑で制御信号(AVA_WE,AVA_AD,AVA_WD)がアサートし、AVA_WT(WaitRequest)がLowになると終了します。

 

ライトタスクの記述例を以下に示します。

f:id:nao-milk:20210319213242p:plain

16行目でAVA_WTがLowの時、Flagが1になり、

Loopで14行目に行った時にFlagが1のため、while文を抜けます。

 

これをタイミング図を示したものを下に示します。

f:id:nao-milk:20210319214043p:plain

 

Avalon-MMリードタスク

タスクRdCPUがテストシナリオで実行すると、CLOCK↑で制御信号(AVA_RE,AVA_AD)がアサートし、AVA_WT(WaitRequest)がLowになるとAVA_RE,AVA_ADをネゲートします。

また、リードバリッド(AVA_VL)がHighになるとリードデータを取得し、終了します。

 

リードタスクの記述例を以下に示します。

f:id:nao-milk:20210319214421p:plain

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」としている所です。

 

これをタイミング図を示したものを下に示します。

f:id:nao-milk:20210319214909p:plain

 

テストシナリオの記述例

上記タスクを使って、テストシナリオを記述する場合を以下に示します。

これは、一例ですので、エンジニアそれぞれの記述があるかと思います。

f:id:nao-milk:20210319215546p:plain

 

最後に

タスクは同じ事を繰り返す時によく使います。

verilogの参考書では、このような説明は無いかと思います。

テストベンチに限り、こういう記述ができますので、実際の回路にする部分では論理合成が通らないので、注意してください。