公開日:2023/10/14
title.png

27th Season


#27-7 TennessineとMoscoviumのアセンブラ

 Cmod A7ボード(FPGA)を使って開発しているオリジナル16ビットCPUコアMoscoviumには、8,16,32ビットのCPUコアがスケーラブルに展開されています。

 命令コードは全て互換の16ビット固定長です。つまり、汎用レジスタおよび演算器などのビット幅が異なっても、命令コードは同じなのです。
 最初に開発した16ビットMoscoviumをベースとして、パフォーマンス向上を目指した32ビットNihoniumでは、起動直後16ビットモード(NHビット=0)で動作開始するため、Moscovium用プログラムとのオブジェクトコードレベル互換性を有しています。

※オブジェクトコードレベル互換性:アセンブル後のオブジェクトコードレベルで、全く同じプログラムを実行可能。

 問題は、論理規模縮小を目指した8ビットTennessineで、汎用レジスタや演算器が8ビットに変わったことで、16ビットのMoscoviumや、16ビットモードのNihoniumと同じプログラムを実行できても、演算結果が異なるため動作が異なってしまいます。

動作の違い

 8ビットTennessineコアの特徴は以下の通りです。

 つまり、メモリアクセスは、Moscoviumと同じオブジェクトコードを実行可能です。更に、演算結果が8ビット以下に収まるのであれば、Moscoviumと同じ動作となります。

  例)演算データおよび結果が8ビット以下?

 普通にプログラムを組むと、大抵のデータは8ビット幅を超え、16ビット幅になってしまうため、一般的にTennessineとMoscoviumで同じオブジェクトコードを実行する事は不可となります。

Tennessineでのプログラミング

 Arduino UNOに搭載されているATmega328というCPUも8ビットなのですが、C言語では、普通に16ビット演算を実行できます。普通、8ビットでは、ほとんどの処理に未達なので、8ビットCPUであっても命令を組み合わせて16ビットデータを処理できるようになっています。

 Tennessineのアセンブラソースで、16ビット演算を容易にするのが、マクロ集(tnsn_mcr_word.incl)です。このマクロ集には、以下のユーザマクロが定義されています。

 いずれも、汎用レジスタRnと裏レジスタQnの16ビットデータに対して演算を実行するマクロです。マクロ名に含まれる「w」が16ビット(ワード、Word)演算を表しており、「w」を消去すると、対応するMoscoviumの命令名となります。
 Tennessineでマクロ集(tnsn_mcr_word.incl)を使ってプログラムを組んだ場合、Moscoviumで、そのマクロを無効化するためのマクロ集(mcvm_remv_mcr_word.incl)も用意してあります。

  例)TennessineとMoscoviumでマクロ集を使用して展開した場合

ユーザマクロ Tennessine
tnsn_mcr_word.incl
Moscovium
mcvm_remv_mcr_word.incl
addw r0,r2 add r0,r2
cendw r0
cendw r2
adc r0,r2
cendw r0
cendw r2
add r0,r2
movw r7,r6 mov r7,r6
cendw r7
cendw r6
mov r7,r6
cendw r7
cendw r6
mov r7,r6
  :
  : その他同様なので省略
  :

 ただし、互換動作なのは、汎用レジスタの演算結果だけ、です。状態レジスタsrの演算フラグは互換動作となっていないので、展開される命令列(上表など)を意識した上で演算フラグを使わなくてはなりません。

※movwマクロの例(上表)では、Moscoviumの場合r7=r6=0x0000だった時にゼロ(Z)フラグ=1となります。しかし、Tennessineの場合、上位バイトが0x00なら、下位バイトが非ゼロ値でも、Zフラグ=1となります(q7:r7=q6:r6=0x00??でZフラグ=1)。

インクルードするファイルを切り替える

 前節の様に、ここまでお膳立てしてあれば、後はターゲットCPU毎にインクルードするマクロ集を切り替えれば良いだけです。それを容易に、かつ、明示的に指示するため、アセンブラのasm疑似命令を以下のように拡張しました。

※Moscoviumシリーズマクロアセンブラ(asm_mcvm.olv)の実行には、WindowsにOlive+のインストールが必要です。

 文字列マクロに設定される文字列は、以下の通りです。

ターゲットCPU target_ts target_mc target_nh target_mn
指定なし 未定義 未定義 未定義 未定義
tennessine "" "#" "#" "#"
moscovium "#" "" "#" ""
nihonium "#" "#" "" ""

 MoscoviumとNihonium(の16ビットモード)は、同じ扱いにできるため、target_mcとtarget_nhの論理ORを取ったtarget_mnを使うことができます。また、asm疑似命令でターゲットCPUを指定しなかった場合、文字列マクロは「未定義」です。使おうとすると、未定義エラーが発生します。

 アセンブラソースファイルの先頭部分に、定型句として以下の様な記述を記載しておきましょう。

  def target,"moscovium"
  #def target,"tennessine"
  asm "mcoc_irom.mem",$(target)
  $(target_mn)incl "mcvm_remv_mcr_word.incl"
  $(target_ts)incl "tnsn_mcr_word.incl"

 2行目の「def target,"tennessine"」は、コメント(#)ですので、上コードでは、ターゲットCPUが"moscovium"となっています。この場合、target_mn=""で、target_ts="#"ですので、「incl "mcvm_remv_mcr_word.incl"」が実行され、「incl "tnsn_mcr_word.incl"」は、コメントとして読み飛ばされます。
 2行目の「#def」→「def」に変えると、ターゲットCPUが"tennessine"となります。今度は、target_mn="#"で、target_ts=""ですので、「incl "mcvm_remv_mcr_word.incl"」がコメントとして読み飛ばされ、「incl "tnsn_mcr_word.incl"」が実行されます。つまり、

 結果、8ビット幅を超える演算が必要なケースでは、TennessineとMoscoviumでオブジェクトコードレベルの互換性は無いけれど、マクロ集を活用する事でソースコードレベルの互換性は確保することが可能となります。
 ソースコードレベル互換なので、Tennessine用、Moscovium用、など、都度ターゲットCPUを切り替えてアセンブルし直す必要があることには注意です。

既に適応済みのプログラム

 これまでいくつかのプログラムに、前節までの議論を適用してありました。例えば以下のプログラムが適応済みなので、確認して見てください。

 それぞれ、TennessineとMoscoviumをターゲットとしてアセンブルし、コードサイズを比較してみました(下表)。

プログラム Tennessine
コードサイズ
Moscovium
コードサイズ
サイズ比
Ts÷Mc
mcoc_blink.asm 2,332 1,872 125%
mcoc_candle_ca7.asm 2,758 2,248 123%
mcoc_boot.asm 482 232 208%

 単位は、アセンブル後のオブジェクトコードサイズ(バイト数)です。サイズ比はMoscoviumターゲットの場合を100%として、100%より大きな値はTennessineターゲットの方が大きくなったという事を表しています。

 なんと、ブートローダは×2倍ほど(208%)に膨れてしまいました。これについては次節にて、もう少し詳しく考察してみます。

 それ以外のアプリケーションでは、ザっと20%〜25%程の増加が見られるという結果となりました。
 Tennessineコアは、より小さい規模のFPGAにも搭載できる様に、論理規模削減を第一目標として設計したCPUコアです。

  「#26-3 Moscoviumのスケールダウン

  「#26-4 Tennessineコアの動作検証

 こちらで検証した様に、Moscoviumに比べて論理規模(LUT数)で半分以下にすることができました。その分、プログラムのコードサイズが25%増加程度で収まっているのであれば、論理規模が半分以下となっているTennessineの存在意義は大いに有ると改めて確認できました。

※LSIの場合、論理素子(LUT)より、メモリ素子(RAMマクロなど)の方が集積度は高いので、メモリ素子(≒コードサイズ)が多少大きくなったとしても、論理素子(LUT)圧縮による効果の方が勝ります。

ブートプログラムから得た知見

 ブートプログラムが×2倍以上に膨れてしまった原因について、少し考察しておきます。
 8ビットのTennessineでは、16ビット演算を実行するのに、複数命令の組み合わせが必要であることは前の節で指摘済みです。これを踏まえて、コードサイズが大きくなった要因を考察してみました。

  ブートプログラムのコードサイズ

 第一の要因は、そもそも、コードサイズが232バイトと言う小さいプログラムであったことです。
 そこへもってきて、16ビット演算処理の割合が比較的高かったために、コード増加の影響が大きく見えてしまったのでしょう。

 そして、第二の要因として挙げられるのが、バレルシフト演算です。
 バレルシフト演算命令は、1命令で汎用レジスタの複数シフト演算を一気に1サイクル実行できる命令です。
 ところが、Tennessineの様に、複数命令を組み合わせて16ビットデータのシフト演算を実行する場合、まず、以下の様に1ビット分のシフト演算プログラムを作っておいて、それをシフトビット数分繰り返すしかありません。

  例)16ビットデータの1ビットシフトプログラム

 上コード例の様に、1ビットシフトに4命令が必要なので、もしも10ビットシフトしようとすると、なんと、40命令もかかってしまいます。
 ブートプログラム(mcoc_boot.asm)では、バレルシフト演算命令を4か所で使っており、それぞれ、1,6,1,4ビットシフト演算でした。合計12ビットシフトなので、これだけで4命令×12シフト=48命令=96バイトも使用しています!

 バレルシフト演算は便利なことも多いのですが、Tennessineでは、バレルシフト演算命令を使わないようにプログラムした方が良いみたいです。使う場合には、8ビットデータに対してのみ使用するか、16ビットデータに対しては1〜数ビットシフトの範囲で使わないと、コードサイズが爆発💣してしまいます。

CPUの判定

 さて最後に、現在実行中のCPUが、どのコアなのかを判定するプログラムをご紹介しておきます。
 CPUコアが判明したら、その後は、当該CPU用のルーチンに分岐させたら良いので、必ずしもプログラム全体を、全CPUコアで共通に実行可能なプログラムにしなければならない訳ではありません。
 注意事項としては、コアが判明して分岐するまでは、8ビットのTennessineでも共通して動作するプログラムにしておく必要があるという事です。

 CPUコアの判定には、以下の特徴を使います。

CPUコア DR=1 SD=1 NH=1
Tennessine × × ×
Moscovium × ×
Moscovium-SS ×
Nihonium ×
Nihonium-SS

 いずれも、状態レジスタsrの各ビットに1を書き込めるか否かで判定します。以下に判定プログラムのサンプルをリンクしておきます。

  サンプルプログラム:mcoc_cpu_recg.asm

 判定結果の文字列表示ルーチンは、シミュレーション用のテストベンチ(test.v)を期待したプログラムです。シミュレーションでしか機能しません。実際のFPGAボードへ書き込んで使う場合には、各自修正してください。

  シミュレーション実行結果:(Tennessineで実行した場合の例)
  Tennessine core
  Test Pass
  CPI: 168/ 128( 0)= 1312/1000
  finish: 26560 ns

 CPU判定の結果、正しく「Tennessine core」と表示されました。

 なお、IDREGユニットが搭載されていて、そのI/Oレジスタ(idrgcodeレジスタ)から読み出した値が信用できるなら、当面の間、その方法での判定も可能です。もし将来、AMP(Asymmetric Multi-Processing; CPUコアが異なるデュアルコア)などに手を出した場合、CPU種を判定するのに、IDREGユニットだけでは不足すると思われるため、CPU単体(Srレジスタだけ)で判定できる上のプログラム(mcoc_cpu_recg.asm)の方が有用です。


 アセンブラソースコードレベルで同じプログラムを、8ビットTennessineコアと16ビットMoscoviumコアでアセンブルして比較したことにより、改めて、論理規模がMoscoviumコアに対して半分以下となったTennessineコアの存在意義を確かめられる結果が確認できました。満足です。


btn_arduino.png

btn_prev.png btn_next.png

home