前々回・前回から引き続き検討してきたテーマであるMoscoviumのデュアルコア化が今回で完結します。
最終回の今回は、CPU間のデータ通信を支援するI/Oユニット設計と、全体の機能検証、FPGAボード(Sipeed Lichee Tang Primer)での実機確認を実施します。
2個のCPUが互いに関係のない仕事を個々にこなすだけでは、単に「マイコンが2個」あるのと同じです。折角1つのマイコンの中にCPUが2個あるのですから、互いに連携して1つの目標に向かって共同作業することがデュアルコアの活かし所となります。
PCとArduinoの連携も、マルチコア(AMP、非対称並列処理)の1つの形です。PCとArduinoがどうやって連絡を取り合うか?と言うと、お馴染みなのはシリアルモニタですね。UARTを使って、データやコマンドなどをやり取りします。同じマイコンの中にある2個のCPUであっても、同様にUARTの様な手段で通信することができると、コマンドやデータを受け渡して連携し易くなります。
と言っても、本当にシリアル通信する必要は無くて、要はFIFOバッファみたいなものを持っていて、互いにデータの送受信ができたらそれで良いはずです。折角同じFPGAボード内の通信なので、16ビット幅のFIFOバッファを使って、データを高速にやり取りできるI/Oユニットを作っておこうと思います。
名付けてCPU間相互通信FIFOユニットICFF16のブロック図を以下に示します。
※薄黄色の箱はCPUからアクセス可能なI/Oレジスタです。二重になっている箱はCPU毎に2組の回路が存在することを示しています。CPU毎のレジスタ選択はメモリバスのbmst信号で切り分けています。
FIFOバッファは、CPU1→CPU2へデータを転送するためのfifo12と、CPU2→CPU1転送用fifo21の2個です。データ幅は16ビットで、TD IDEのFIFOマクロを使用します。各自、以下スクリーンショットの様に作成してください。
段数はFIFOマクロ最小の512段(16ビット幅なので1Kバイト)としてあります。ユーザがより大きなFIFOマクロを作成して使用することは可能です。
上図のFIFOマクロを、CPU1→CPU2転送用(fifo12)とCPU2→CPU1転送用(fifo21)の2個インスタンスしています。
CPU1からの書き込みはfifo12、読み出しはfifo21が対象、
CPU2からの書き込みはfifo21、読み出しはfifo12が対象となります。
以下にICFF16ユニットのI/Oレジスタ一覧を示し、各レジスタの説明を加えます。
Addr | Name | R/W | Bit:15〜0 | Memo | |||||||||||||||
0xf090 | icffctl | RW | − | − | − | − | − | − | − | − |
FR AF |
FT AF |
− | − |
FR AE |
FT AE |
− |
FR ST |
intercommunication fifo control |
0xf092 | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | reserved |
0xf094 | icffsend | W | SDAT[15:0] | send data | |||||||||||||||
0xf096 | icffrecv | R | RDAT[15:0] | receive data | |||||||||||||||
0xf098 | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | reserved |
0xf09a | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | reserved |
0xf09c | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | reserved |
0xf09e | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | − | reserved |
BUSC出力のセレクト信号はbcs_icff_nです。上表の範囲でアサートされるので、ICFF16ユニット内では、アドレスbadr[3:0]を復号してレジスタを特定しています。このI/Oユニットはバイトアクセス禁止です。必ずワードアクセスしてください。
ICFF16ユニットのすべてのレジスタは、同じアドレスでCPU毎に二重化されています。BUSCからのbmst信号によって、各々のCPUに対応するレジスタにアクセスされます。
以後、こちらのCPU IDをA(a)、他方のCPU IDをB(b)として、icffsendAレジスタ、FRAFbフラグ等と表記します。A,B=1または2で、A≠Bです。
異なるマイコン間でUART等の通信をする感覚でパラレル(16ビット)データをやり取りできるため、ちょっとしたコマンドや処理データのやり取りなどに活用できると思います。
ようやく、変更が必要、または新規作成するユニットの解説が終わりました。以下リンクより、Moscoviumデュアルコア版MCOC115DC0416のデザインデータをダウンロードすることが可能です。
構成としては、(Moscovium+乗除算コプロセッサ)×2のデュアルコアに、ROM 4Kバイト、RAM 16Kバイトとしてあります。除算コプロセッサは論理規模が大きく、それがデュアルコア故に×2倍で効いてくるため、外しておきたかったのですが、この後、アプリケーションとして円周率の計算プログラム(calc_pi.asm)を予定しているため、除算器必須となってしまいました。
最上位モジュール(mcoc115dc0416.v)先頭付近の`defineマクロで、構成を変更できるので、場合によっては調整してお使いください。
以下にコンパイル(論理合成・配置配線)結果をまとめておきます。
構成にもよりますが、シングルコア版におけるLUT数のほぼ6〜7割はCPUの論理です。それが×2倍化されますので、さすがに乗除算コプロセッサ込みのデュアルコアだと、9k LUTに迫って来ましたね。FPGAボード(Anlogic EG4S20BG256)だと最大19.6k LUTですので、これでもまだ半分に届きません。まただいぶ余裕があるという事です。
シミュレーション(iverilog,GTKWave)によって、機能検証を実施しました。
最初は、従来のシングルコア版で作って使って来たセルフテストプログラム(mcvm_self.asm)を、2個のCPUで同時に実行させて、機能検証しようと考えました。
しかし、特定のメモリアドレスへアクセスして検証する命令が存在したため、2個のCPUで同時に同じテストを実行することはできませんでした。
そこで、以下の様なコードによってCPU IDをチェックし、予め選択したCPUだけがセルフテストを実行する様に変更しました。
// sr register, check cpu id to test
movfc r0,sr
lsfti r0,-sreg_b_id0
andi r0,sreg_id_3>>sreg_b_id0
beq reg_pass // cpu0: single core, ok go
cmpi r0,sreg_id_3>>sreg_b_id0
beq reg_fail // cpu3: reserved, error
cmpi r0,mcvm_dual_cpu
beq reg_pass // cpu for dut, ok go
ldbiu r0,mcvm_dual_cpu
cmpi r0,sreg_id_1>>sreg_b_id0
blo reg_fail // mcvm_dual_cpu must set 1 or 2
cmpi r0,sreg_id_3>>sreg_b_id0
bhs reg_fail // mcvm_dual_cpu must set 1 or 2
// stop cpu not to test
pause
bra pcnt-4
プログラム(mcvm_self.asm)先頭付近の「equ mcvm_dual_cpu,2」で選択したIDを持つCPUだけがテストを実行し、IDが一致しなかったCPUはpause命令で動作を停止します。
選択しなかったCPUがpause命令で停止するまでの間は、2個のCPUから同時にフェッチアクセスが発生するため、ROMからの同時フェッチが正常に動作していなければ、テストはパスしません。
CPUの選択(mcvm_dual_cpu)を変更しながら2回テストして、CPU1およびCPU2の両方でセルフテストがパスすることを確認しました。
CPUは、状態レジスタsrにIDフィールドを追加しただけなので動いて当然ではありますが、BUSC、ROM等の変更点が正常に動作しなければテストがフェイル(失敗)するはずですので、周辺回路の基本動作チェックとしての役割を果たします。
メモリ(RAM)に対するマーチングテストを2個のCPUから同時に実行させて、メモリテストがパスすることを確認しました。マーチングテストのアルゴリズムはこちらに解説があります。
当然、2個のCPUが並列して、同時に同じ領域のマーチングテストを実行すると、読み/書きのタイミングで必ずフェイル(失敗)します。個々のCPUがテストするRAM領域は、互いに重なり合わない様に分けて実施しました。
2個のCPUからのメモリ(RAM)アクセスが断続的に発生し、バスアクセスタイミングが競合しても、正しくメモリアクセスが完了することの検証となります。つまり、実行したのはメモリテスト(マーチング)ですが、実際にはバスステートコントローラBUSCのテストになっています。
以降、個々のテスト内容(プログラムやスティミュラス等)については省略します。検証の考え方と経緯についてのみご紹介しておきます。
CPUセルフテストによって、2個のCPUからROMへの同時フェッチアクセスについては検証できましたので、残るオペランドアクセスのテストを別途実施しました。
各CPUからROMに対してオペランドアクセスを実行しても、フェッチアクセスが失敗したり、オペランドアクセスで異なるデータが読み出せたりしないことを確認しました。
実はROMにはもう一つ、ブートモードでROM用RAMマクロにプログラムコードをダウンロードして書き込むという、もう一つのオペランドアクセスが存在します。こちらも、デュアルコア版MCOC115DC0416をブートモードで起動して、UARTからプログラムコードをダウンロードできることをシミュレーション確認しました。
なお、ブートモード(bootmd信号=1)で起動すると、CPU1はブートROMからフェッチしてブートローダを実行します。他方のCPU2は、ブートモードの時には動作しない様に作り込んであります。∵ブートモードでは、2個のCPUが同時に動く必要が無いため。
外部端子割り込みを使って、2個のCPU用に二重化されたintofs,intnum,intmsk,intmskhレジスタの動作検証を実施しました。
各CPUで個別に設定した割り込みマスクレジスタ(intmsk,intmskh)で許可した割り込みだけがCPUに受け付けられて、割り込みが発生することなど、基本動作を確認しました。
デュアルコア用に新規追加したCPU間割り込み(intcpuレジスタ)についても、互いに割り込み要求を発行し合って、割り込みが発生すること、割り込みフラグがクリアできることなどを確認しました。
新規に作成したI/Oユニット(SEMPH5R12U、ICFF16)に関しては、複合テストによって、普通に新規I/Oユニット設計時と同様なテストを実施しました。両I/Oユニットの基本動作検証と、割り込みなどの機能検証を兼ねています。
両I/Oユニット共、デュアルコア用に多重化されたレジスタを多く所有しているため、2個のCPUで個別に検証する必要があり、テストの内容はさておき、ボリュームは×2倍でした。
ユニット毎のシミュレーション検証が済んで、動作に自信が持てたところで、今度は具体的にアプリケーションを作って見ようと思います。ですが、OSが走っているわけでもなく、「プログラムを実行しさえすれば、勝手にOSがタスクをマルチCPUに振り分けて実行してくれる」わけでもありません。
さて、何に使った物か?いくつか使い道の例を挙げて検討してみましょう。
アカデミックにまとまっていないので、一応4通りに分類してみましたが一部用途が重なっていたりしますネ……。まあ、「思いつくまま挙げてみた」と言う事で。
デュアルコアにしては見たものの、具体的用途については思い及んでおりませんでした(1・_・)。
そこで、無理やり感は否めませんが、円周率を計算するプログラム(calc_pi.asm)をデュアルコア用に改造して、上の分類1)に相当する処理を実行してみたいと思います。
「#10-6 328duinoをクロックアップ(完結編)」: UNO,328duino+calc_pi.ino
「#11-9 一の次は二」: DUE+calc_pi.ino
「#14-10 Arduino IDEのESP32開発環境」: ESP32+calc_pi.ino
「#16-8 忘れてた実機動作確認」: Moscovium+calc_pi_asm
↑の様に、何度も登場しているテーマですので、重複は避けますが、必要な所だけ解説しておきます。
円周率の計算は上式の様に、2つのatan()関数を計算して、足し合わせることで実現しています。各々のatan()関数は独立して計算可能なので、以下の様に2個のCPUに分散して計算させることが可能です。
CPU1: 4×atan (1/239)
CPU2: 16×atan (1/5)
多倍長固定小数の記憶領域(メモリ)は×2倍必要となります。また、各atan()関数の計算後、2つの計算値を足し合わせる処理は分散できません。
atan()関数の計算を2個のCPUに分散させるため、計算時間は半分(1/2)……と、言いたいところですが、そうはなりません。
atan (1/x)のグレゴリー級数では、xが分母に来て、かつ、x2k+1のべき乗になっています。つまり、xの値が大きければ大きいほど、この級数は高速に0に収束して行き、従って演算時間が短くて済むのです。
上の分散例では、CPU1がx=239と大きく、CPU2がx=5と小さいため、CPU1の計算の方が圧倒的に早く終了してしまうのです。CPU1は短時間で計算を終了しますが、先の処理に進めないため、CPU2の計算終了をひたすら待つことになります。イメージ的には以下図の様な感じで、結局、早い方のatan (1/239)計算分だけが短縮できるイメージです。
※CPU1の「wait」期間は、せめてCPU2のバスアクセスを邪魔しない様に、pause命令で停止して待つことくらいしかできません。
元々のシングルコア版Moscovium用円周率計算プログラムでatan()関数の演算を実行するm_atan_1_xサブルーチンは、スタティック変数ではなく、スタック上に自動変数を確保して計算するプログラム(stwsp命令、ldwsp命令を使用)にしておきました。よって、CPU1,CPU2の判定と、CPU IDに応じたスタックポインタspの設定を適切に実施すれば、そのほかの部分はほぼそのまま使用可能でした。
以下に円周率計算プログラムをリンクしておきます。
完成版プログラム:calc_pi.asm
Moscoviumアセンブラでアセンブルして、MCOC115DC0416をコンパイル(論理合成・配置配線)実行、FPGAボードに書き込んでおいてください。手順はこちらです。
シリアルモニタを使うので、FTDIなどのUSB⇔UART変換モジュールをこちらの様に接続してください。以下、実験中の様子です(写真は流用)。
実験サンプル:【 】内はシリアルモニタからの送信データ
【pi,5116】
OK+ start calculation
OK+ dual core cpu edition
OK+ pi=3.
1415
9265
3589
:
0141
6314
9665
OK+ digits=5116 (size=1280)
OK: pi time[us]=0x02573792
5,000桁超まで、期待値との比較が合致しました。2個のCPUが同時にプログラムフェッチして、多倍長固定小数に対するRAMアクセスと、Moscovium+乗除算コプロセッサの演算結果が正しくなければ5,000桁以上の正しい計算結果は得られません。
実機動作検証は合格💮とします。
計算時間は、約39.3秒(=0x02573792μs)でした。シングルコア版(MCOC115CP0408)の計算時間(約45.7秒)と比べると、約×1.16倍速ということで、期待値×2倍速には遠く及ばず、です。勿論、予想通りの結果です。
分散処理のモチーフ(calc_pi.asm)があまり良くなかったのかもしれません。マンデルブロ集合とかの方が、分散処理のモチーフとしては適していたでしょう。ただ、マンデルブロとなると、半精度浮動小数演算ユニット(H-FPU)込みのデュアルコア化が必要となるので、更に論理規模が大きくなり、あまり実用的ではないと思って止めておきました。
単に「デュアルコアにしてみたい!」から始めたテーマで、自己満足ですから〜。最後、デュアルコア化の効果確認が中途半端になってしまい、残念極まりないですm(1__)m。第一に「応用ありき」でテーマを選定しないと、ダメですね。
※「#17-8 円周率とデュアルコア」にて、デュアルコアへの分散方法を改定してみましたので、ご確認ください。