Cmod A7ボード(FPGA)を使って、オリジナル16ビットCPUコアMoscoviumの開発を進めています。以下記事などで、Moscoviumマイコンのプログラム開発にBlocklyを使用できるように整備しました。
「#31-6 Moscoviumの開発環境(カスカス)」
前回までに、デュアルコアと割り込みをBlocklyでハンドリングできるように拡張が終わり、Blockly for Moscoviumを使って、Moscoviumをマイコンとして一通り活用できるくらいの整備が終わりました。
今回のテーマはBlocklyとアセンブラの連携です。
前回、Blocklyに割り込み制御を組み込んでサンプルプログラムを作り、シミュレーションで検証しました。その時のシミュレーション波形図を以下に示し(再掲)、続いて注目すべき点について述べます。
送信/受信が同時に行われており、一方が書き込むと、他方が読み出す、結果、intc_irq信号が0⇔1トグルします。……一見、理屈としては正しい様に思います。
しかし本来、送信側が先行して動作を開始して、どんどんデータを送って来るのに対して、受信側は割り込みを受け付けてから、汎用レジスタの退避とIRQタスクへのサブルーチン分岐などのオーバヘッドを経て、漸く受信動作に移ることができます。
受信側が受信体制を整えるまでに相応の時間が経過しており、この間に送信側は相応のデータを送信して、データが溜まっているはずなのです。受信側が1個や2個のデータを受信したところで、受信FIFOに溜まったデータを読み出しつくせるはずがありません。
つまり、普通に考えて、intc_irq=1になったら、そのまま全データを受信しきるまでintc_irq=1のはずです。なんで、0⇔1トグルするのかな?違和感があります。
そこで、シミュレーション波形を詳細に確認してみると、以下のことが判りました。
違和感の正体が判明しました。
単に、想像していたよりも、Blockly版プログラムの動作が遅かっただけみたいです。愚直に、Blocklyプログラムをアセンブラに変換しており、最適化されていないので仕方ありません。
通信ユニットを使ってデータを送受信するだけ、等という、簡単で基本的な関数や手続きに関しては、アセンブラで組んだ方が良さそうです。
ということで、ICFF16ユニットに対する送受信手続きをアセンブラでプログラムして見ました。
例)送信手続き:
packブロックのチェックを外すと、変換スクリプトが変換対象から外します。Blockly版とアセンブラ版との比較を実施するため、どちらか一方だけにチェックを入れて選択可能と、しておきました。受信手続き側も、同様にアセンブラでプログラミングしてあります。
全体像は以下リンクよりダウンロードして、ご確認ください。
改訂版プログラム:blky_task_irq.xml
※ブラウザによっては、XMLファイルをレンダリングして表示してしまうので、「右クリック ⇒ リンク先を保存」などしてください。
Blockly for MoscoviumのXMLタブに貼り付けて、ご覧ください。
Blockly版の送受信手続きを使ったシミュレーションは、前回実施済みですので、こちらの実験をご参照ください。今回のシミュレーション方法(Vivado使用)なども、前回とほぼ同じですので、併せてご確認ください。
では、アセンブラ版の送受信手続きを使ったシミュレーション結果をGTKWaveで確認してみましょう(下図)。
intc_irq信号がトグルしなくなりました。そうだよね。今までアセンブラでプログラムしていた時の印象だと、こっちの方がしっくりきます。
そして何より、以下のシミュレーションログを確認すると、良く解ります。
実験サンプル:シミュレーションログ
CPU2 (#1): hello, CPU2
CPU1 (#2): how are you? CPU1
CPU2 (#3): fine thanks, &you?
CPU1 (#4): fine, thanks, too
CPU2 (#5): see you
CPU1 (#6): bye
Test Pass
CPI: 7961/ 3276( 0)= 2430/1000
finish: 338280 ns
シミュレーションログで終了時間を比べてみると、以下の通りです。
アセンブラ版の方が30%以上高速(短時間)に終了しました。言うまでもないのですが、やっぱりアセンブラ版の方が圧倒的に速いですね。
Blockly.XML→Moscovium.ASM変換およびアセンブル後のコードサイズも比較してみました。
コードサイズもアセンブラ版の方が小さくなりました。
プログラム全体の中で、送受信手続きが占める割合は小さいため、実行速度程の劇的な変化(30%↑)は見られませんでしたが、間違いなくコード効率は向上しています。
こちらの結果も、当然と言えば当然でした。
そもそも、オリジナル16ビットCPUコアMoscoviumを抽象度の高い高級言語でプログラミングしたくて、Blockly開発環境を立ち上げました。即ち、プログラム開発環境の、アセンブラからの脱却を図ったのです。
でもやっぱり、基本的な、規模の小さいライブラリ関数に関しては、アセンブラでプログラムした方が実行速度もコード効率も向上すると判りました。ここは適材適所で、Blocklyとアセンブラが連携できるような環境を整えることが肝要だと思いました。
こちらの様に、アセンブラで作ったコードを、1行1行asmブロックに記述して行くのは大変手間がかかります。そこで、アセンブラで記述したソースリスト(テキストデータ)から、Blocklyのasmブロック列を生成するスクリプトを作ってみました(以下リンク)。
asmブロック生成スクリプト:blkyxml_asmblk.olv
※スクリプト(.olv)の実行には拙作Olive+をWindowsにインストールしてください。
実行すると、テキストデータ入力を要求するウインドウが開きます。テキストエディタなどから、変換したいテキストデータをコピー&ペーストして、OKボタンを押してください。「blkyxml_asmblk_out.xml」ファイルが作成されます。
完成したファイルをBlocklyエディタのXMLタブへコピー&ペーストして使用することができます。
Blocklyでのプログラミングは、とても楽しいのですが、1つ不満があります。
例えば、色々なプログラムで共通的に使用する様な関数や手続きを作ったと仮定してください。その関数や手続きを色々なプログラムで使いたいのですが、Blocklyエディタには特定のブロック群をコピー&ペーストする機能がありません。
ある関数を使いたくなって、「この関数、あのプログラムで作ったな」と、思っても、その関数を今のプログラムに持ってくる(コピー&ペーストする)手段が無いのです。
前節のasmブロック生成スクリプトで生成したブロック群も同じです。コピー&ペーストできないのに、どうやって、今のプログラムに取り込んで使うのよ??
そこで、複数のBlockly.XMLファイルを併合(merge)して、1個のXMLファイルに出力するスクリプトを用意しました(以下リンク)。
Blockly.XML併合スクリプト:blkyxml_merge.olv
※スクリプト(.olv)の実行には拙作Olive+をWindowsにインストールしてください。
併合手順:
単純に併合しただけなので、ブロックの表示が重なっていたり、不必要な部分も一緒に併合されていたりします。Blocklyエディタの背景部分で「右クリック ⇒ Clean up Blocks」を実行してブロックを整列したり、不要なブロックを削除したりして、使って見てください。
例えばこちらの1/fゆらぎ乱数列(mcoc_candle.incl)の様に、サイズの大きなデータ等をBlocklyプログラムへ取り込みたい場合、asmブロックでincl疑似命令を使って取り込む方法があります(下図2例)。
サイズの大きなデータを、こちらのスクリプトでasmブロック列に変換して取り込むよりは、スマートな方法だと思います。
この場合、上図の様に、incl疑似命令で取り込んだデータをプログラムとして実行してしまわない様に配慮しなくてはなりません。
具体的には上図2例いずれかの様に、bra命令またはjmp命令を使って(※)、取り込んだデータ部分を跳び越すようなコードにしておくか、あるいはincl疑似命令の手前に、ランタイムライブラリのCPU停止コード(ラベル:_rtlb_prog_halt)へ無条件分岐する命令を配置しておくのが賢明でしょう。
※bra命令では、前後に2Kバイト程の範囲しか分岐できないので、incl疑似命令で取り込むデータがこのサイズを上回る場合、jmp命令を使ってください。
取り込んだデータの使い方(アクセス方法)に関しては、上例の様にデータ先頭アドレスを示すラベル(incl_data_top1,incl_data_top2等)を張っておき、mem readブロックなどで読み出すことができます(下図参考)。
※アドレスラベル(上例ではincl_data_top1)を参照するには、頭に接頭子「lab_」を追加する必要があります。詳しくは、アセンブラマニュアルを、ご参照ください。
Blockly for Moscoviumとアセンブラを連携して開発するに当たって、知っておくべき、お作法を以下にまとめてみます。
こちらで議論した様に、変数領域の先頭アドレスは汎用レジスタr6に保持されています。ユーザのアセンブラコードの中で、R6を破壊する事は禁止してあります。
変数へのアクセスですが、R6に設定された値を使ってアセンブラからアクセスする事は許容しません。
上図の様に、Blocklyで変数の値、またはポインタを取得後、アセンブラに制御を移すことを推奨します。
なお、変数値の取得や、mem addrブロックの実行には、汎用レジスタr0が使用されます。その他、R0やR7と言った汎用レジスタは、Blocklyの処理に於いて高優先順位で破壊されるので、R0やR7に変数値やポインタを取得する事は避けましょう。Blocklyの処理によって、上書きされる可能性が高いです。
Blockly for Moscoviumでは、関数や手続きの引数は全てグローバル変数なので、引数へアクセスする方法については、変数アクセスに準じます。
値を返す関数は、復帰(rtnw命令等)する前に、R0にセットしておいた値が返ります。
基本的な関数や手続きなども、incl疑似命令で外部ファイルから取り込むようにしておくと、関数や手続きのコードを改定した場合に、外部ファイルを更新するだけで、その関数や手続きを取り込んでいる全てのBlocklyプログラムを更新することができる長所を享受できます(下図)。
上図の例だと、引数2個がR1,R2へ代入された状態で、「func_incl.asm」ファイルに記述されたアセンブラに制御が移り、最後にR0へセットされた値を関数値として返しています。
ユーザのアセンブラプログラム内で保存しなくてはならないレジスタが有ります。以下の考察をご覧ください。
基本的には、ユーザのアセンブラコードの中で汎用レジスタr6を保存しておけば、Blockly側のコードに支障は出ません。
後、逆のケースは全てダメなので要注意です。
逆のケースとは、アセンブラで特定の汎用レジスタに何らかのデータをセットした上で、Blocklyのプログラムを実行する場合です。この場合、アセンブラでセットした値が保存される保証は有りません。
Blocklyとアセンブラ間でデータの授受が必要な場合は、汎用レジスタRn⇔変数アクセスを経由する事を推奨します。その上で、Blocklyが頻繁に使う汎用レジスタR0とR7は、Blocklyによって上書きされてしまう可能性が高いため、データの授受に使用しないことが望ましいです。
……最終的には、変換後のアセンブラソースリストで確認しながら、使って見てくださいm(1__)m。Blocklyの処理内容次第では、R0,R7以外の汎用レジスタも上書きされる場合がありますので……。
変換したアセンブラソースリストから、「mcoc115.incl」ファイルを取り込んでいるため、INCLファイル内で宣言された定数ラベルをBlocklyプログラム中で使用することができます。
逆に捉えると、「mcoc115.incl」ファイル内に宣言されている名前を、Blocklyプログラムで、変数名や関数名、手続き名などに使わないで下さい。
アドレスラベルや定数ラベルなど、アンダスコア(_)で始まる名前は、ランタイムライブラリと変換スクリプトが予約しており、使用禁止です。
関数や手続きを宣言すると、関数名または手続き名がアドレスラベルとして宣言されています。変数名も同様です。Blocklyで使った名前は、ユーザのアセンブラコード内での再定義・再宣言不可です。
それ以外は、マクロアセンブラが許容する名前を自由に使用可能です。
ブロックを組み立てて、ビジュアルにプログラミングできることがコンセプトのBlocklyですから、基本、アセンブラとの連携なんてものはナンセンスなんですよね?でも、Moscovium開発環境としては、アセンブラとの連携が重要と考えています。インラインアセンブラが使えるBlocklyなんて、このBlockly for Moscoviumくらいな物ではないでしょうか?