Cmod A7ボード(FPGA)を使って、オリジナル16ビットCPUコアMoscoviumの開発を進めています。以下記事などで、Moscoviumマイコンのプログラム開発にBlocklyを使用できるように整備しました。
「#31-6 Moscoviumの開発環境(カスカス)」
まだまだ、漸くBlocklyを使ってプログラム開発可能な環境が整った、というレベルで発展途上です。
前回から引き続き、Blocklyでもデュアルコア用プログラムを開発できる様に改定して行きたいと思います。
本テーマの冒頭(前回)で、FreeRTOS等のOSに実装されている、「スレッド」までの対応は断念しました。単に、2個のCPUコアで別々なプログラムを実行可能とする、マルチコアタスクを実現するため、Blocklyを拡張して行きます。
まず、taskブロックを追加しました(下図)。
task CPU2ブロックに記述したプログラムは、デュアルコアのCPU2(サブCPU)だけが実行します。ランタイムライブラリからCPU2を識別して、task CPU2ブロックの先頭アドレスへサブルーチン分岐して来ます。
task CPU2ブロック内のプログラムを実行し終わって、task CPU2ブロックの末尾に到達するか、もしくは、if returnブロックが実行されると、サブルーチンから復帰してランタイムライブラリに戻ります。ランタイムライブラリに戻ってしまうとCPU2の動作は停止して、マイコンをリセットしない限り二度とタスクを実行する事はありません。
よって、CPU2の動作を停止したくない場合は、ユーザプログラムで無限ループにするなどの対処が必要です。
Blocklyのプログラム中で、task CPU2ブロックは1回だけ宣言する事ができます。2回以上、task CPU2ブロックが宣言されると、Blockly.XML→Moscovium.ASM変換スクリプトがエラーを発生します。
Blocklyで1個のCPUしか使用しない場合は、task CPU2ブロックを宣言しなくて良いです。この場合は、ランタイムライブラリがCPU2を安全に停止するため、問題ありません。
デュアルコア版であっても、シングルコア版と同様なプログラム実行となる機構です。
逆はダメで、task CPU2ブロックでCPU2の実行プログラムを宣言した場合、これをシングルコア版のMoscoviumで実行すると、ランタイムライブラリがエラーを発生します。
前回考察の結果、CPUコア毎に、変数領域とスタック領域を別々なアドレス範囲に設定しなくてはならないことが判りました。
スタック領域はCPUのスタックポインタspの設定がCPU毎に異なるだけの事なので、特に問題なく実装できます。
問題は変数領域でした。実は今までの変換スクリプトでは、変数の格納領域を定数で管理して、直接アクセスするコードを出力していました。デュアルコア対応に伴い、変数の格納アドレスを定数で指定することができなくなってしまったのです。
格納アドレスを定数にしてしまうと、各CPUから、同じアドレスに対してアクセスしてしまって、分離できないためです。
そこで、汎用レジスタr6を変数領域の先頭アドレスとして予約する事にしました。
ランタイムライブラリが、考察で議論した様な領域を設定して、CPUコア毎に適切なアドレスをR6に設定してからユーザプログラムへ分岐して来ます。
下図に、CPU毎の変数領域先頭アドレスを保持したR6と、スタック領域を示すスタックポインタspの関係を示します。
R6によるオフセットが付いたことで、赤文字で示した1命令分、実行速度とコード効率が低下しました。変数へアクセスする度に、このオーバヘッドがかかるため、少し痛い🐝です……。
処理実行のための一時レジスタとしてR6が使用できなくなることも、場合によっては問題になるかもしれません。何しろ、Moscoviumには8個しか存在しない汎用レジスタ(汎用レジスタバンクは除く)の、貴重な1個なのですから。
しかし、デュアルコアでBlocklyのコードを正常に動作させるためには必須なので、止む無しと判断しました。
Blocklyでプログラムしている限り、R6が破壊されるようなコードを実行する事は有りません。
インラインアセンブラ(asmブロック)等で、ユーザコードを実行する場合、R6を破壊しないように保証する必要があります(ユーザ責任)。具体的には、ユーザプログラムでのR6使用禁止、または、使う場合はスタックなどに退避・回復して、R6を破壊しない事が求められます。
ランタイムライブラリに収録していた、delay/delayMicrosecondsブロックのサブルーチンが、デュアルコアセーフになっていませんでした。
delay/delayMicrosecondsブロックのサブルーチンは共に、システムタイマSYSTIMユニットを使って実装しています。サブルーチン内では、I/Oレジスタsytmctlの設定ビットを変更していました。シングルコアとしては、正常に動作するプログラムでした。
ところが、デュアルコアの各CPUがほぼ同時にdelay/delayMicrosecondsブロックを実行すると、2つのCPUコアが別々にsytmctlレジスタの設定値を変更してしまうため、正常動作しなくなってしまう事に気が付きました。
これが、ずっと警告し続けてきた、「共有リソースの競合」というやつです。
複数のCPUコアが、1つのSYSTIMユニットにアクセスするので、SYSTIMユニットが共有リソースになっています。本来であればセマフォを使い、排他制御(リソース管理)しながらプログラムする必要があります。
今回は共有リソースのまま、セマフォも使うことなく、競合しない様にサブルーチンを改定することで回避しました。
delayブロックは、指定した時間[ms]ウェイトする機能なので、SYSTIMユニットのミリ秒カウンタsytmmilレジスタを読み出しながら、以下コードの様に改定しました。
旧delayサブルーチン | ⇒ | デュアルコア対応版 |
_delay_ms_r0: ldwi r7,sytmctl ldbiu r1,sytm_mloe stw [r7],r1 ldwi r7,_rtlb_sytmmil$(z) // wait ld$(z) r1,[r7] cmp r1,r0 blo pcnt-6 ldwi r7,sytmctl ldbiu r1,0 stw [r7],r1 // end of subroutine rtn$(z) |
_delay_ms_r0: ldwi r7,_rtlb_sytmmil$(z) ld$(z) r2,[r7] // wait ld$(z) r1,[r7] sub r1,r2 cmp r1,r0 blo pcnt-8 // end of subroutine rtn$(z) |
デュアルコア対応版のフロウとしては、以下の通りです。
これまでの旧delayサブルーチンでは、sytmctlレジスタのMLOEビットを変更していたため、デュアルコアで排他制御が必要でした。デュアルコア対応版コードでは、sytmmilレジスタを読み出すだけで、その他の設定変更を伴わないため、共有リソースでありながら競合しません。
デュアルコアCPUが同時並列に実行しても、安全に正しく動作します。
delay/delayMicrosecondsブロックは何とかなりましたが、uart printブロック等UART関係のリソースも含め、ほとんどのI/Oユニットは共有リソースです。これらは、セマフォなどを使って排他制御しないと、実質、使い物にならないと思います。各自ご対応願います。
Blockly for Moscovium開発環境の構築については、こちらの記事をご参照ください。
既に環境を立ち上げ済みの場合でも、今回の拡張に伴い、以下のファイルを更新する必要があります。
今回拡張したBlockly.XML→Moscovium.ASM変換スクリプトは、こちらからダウンロードしてください。
LSIのデザインとしては、SMPデュアルコアとなる、以下の型名などから選択して使ってください。
現在のところ、デュアルコア+I-RAM搭載版の型名設定が無いので、I-RAMの動作確認はできませんが、今回は良しとします。
※VHファイルに自分で手を加えると、型名設定の無い構成のマイコンを使う事も出来ます。用意されたVHファイルを参考に、I-RAMを有効化(`define MCOC_IRAM_4K)して使っていただくことも可能です。
上に例示した型名の場合、I-RAMなし、RAM容量16Kバイトなので、下図右側のメモリマップを選択する事になります。各領域(変数+スタック)×(CPU1+CPU2)の容量は、各々16Kバイト÷4=4Kバイトです。
Blocklyの数値変数および、文字列変数の合計記憶容量が4Kバイトを超えない様に、プログラムする必要があります。
※念のため申し添えておきます。スタック上に局所データ領域を確保したり、そのサブルーチンを再帰呼び出ししたりする場合には、スタック領域に関しても4Kバイトを超えないように制御しなくてはなりません。
Cmod A7ボード用Moscoviumマイコンの設計データ(Vivadoプロジェクト)は、GitHubにて配布しております。以下バナーよりアクセスしてください。
今回、FPGAは使いませんが、Cmod A7ボード(FPGA)への書き込み方法に関しては、こちらをご参照ください。
FPGAに書き込んで実機で確認する程の事ではないのと、返って、シミュレーションで内部信号を確認した方が、より詳細に動作検証可能な案件です。今回は、作成したプログラムを使って論理シミュレーションを実行し、動作検証といたします。
デュアルコア対応版Blocklyの動作検証用プログラムを作成します。以下の様なフロウで検証しようと思います。
あとは、GTKWave等を使って、ちゃんとtask CPU2ブロックのプログラムをCPU2が実行しているよね?CPU1も機能しているよね?と言った辺りを確認しておくと、検証完了です。
完成したプログラムを以下にリンクしておきます。
完成プログラム:blky_task_cpu2.xml
※ブラウザによっては、XMLファイルをレンダリングして表示してしまうので、「右クリック ⇒ リンク先を保存」などしてください。
task CPU2ブロックの使い方のサンプルでもあるので、Blockly for MoscoviumのXMLタブに貼り付けて、ご覧ください。
変換・アセンブル手順については、こちらをご参照ください。
Vivadoを使ったシミュレーション手順についてはこちらでご確認ください。
実験サンプル:シミュレーションログ
CPU1: iram_need=0
CPU1: r6, sp=5000, 8ffe
CPU1: numa, b=123a, 456b
CPU2: r6, sp=6000, 7ffc
CPU2: numa, b=0000, 0000
CPU2: numa, b=456a, 123b
CPU1: numa, b=123a, 456b
Test Pass
CPI: 5887/ 4380( 0)= 1344/1000
finish: 255320 ns
mcoc115dc0416.vhを使ってシミュレーションしました。ROM 4Kバイト、I-RAM無し、RAM 16Kバイトなので、以下の領域割り当て図(再掲)に相当します。
CPU1/CPU2が表示した「r6,sp=」の値を確認してみると、スタックポインタspは、いくらか使用後の値なので切りの良い数値になっていませんが、上図の通りに割り当てられたことが確認できます。
ちゃんと、変数領域(R6が指し示すアドレス)が、CPU1/CPU2で異なるアドレス範囲に配置されているので、「numa,numb=」で表示された変数値も、こちらのフロウで説明した期待値と一致しています。
シミュレーション波形(GTKWave)でも確認しておきましょう(下図)。
上図のカーソル位置が丁度、CPU2がtask CPU2ブロックで宣言したプログラムへ分岐しているところです。CPU1も変数の初期化(=0)が終了して、ユーザプログラムの先頭アドレスへ分岐する所でした。ランタイムライブラリの処理量は、CPU1/CPU2でそれほど変わらないので、ほぼ同じタイミングでユーザプログラムに制御が移る様です。
Blocklyで、最低限、デュアルコア用のプログラムも作成できるように拡張しました。もう一押しです。