公開日:2023/02/11
title.png

24th Season


#24-5 単精度浮動小数演算ユニット

 FPGAボード(Sipeed Lichee Tang Primer)を使って設計しているオリジナル16ビットCPUコアMoscoviumには、半精度浮動小数演算ユニット(Half precision FPU、以降H-FPUと呼びます)が搭載できます。

  「#16-5 半精度浮動小数演算ユニット

  「#16-6 半精度浮動小数演算ユニット(弐)

 半精度浮動小数型(16ビットhalf型)は、16ビット幅の汎用レジスタを持つMoscoviumには最適でした。一方、有効数字は10進数換算で約3桁強と、満足できる精度ではありませんでした。

 その後、汎用レジスタや演算器を32ビット幅に拡張したオリジナル32ビットCPUコアNihoniumを設計しました。現時点で、NihoniumのコプロセッサとしてH-FPUを接続することは可能なRTLにしてあります。

 折角、汎用レジスタが32ビットになったので、浮動小数型も32ビット長にすることができます。32ビット長の浮動小数型としては、half型と同様、IEEE754で規定されているbinary32(単精度浮動小数、以降float型と呼びます)が挙げられます。

 今回は、単精度浮動小数演算ユニット(Single precision FPU、以降S-FPUと呼びます)を設計します。

half型とfloat型の違い

 half型とfloat型は同じIEEE754で規定されているだけあって、下図の様にバイナリフォーマットは、ほぼ同じです。

a165_half.png
a245_float.png

 float型では、全体のビット幅が16ビット→32ビットに拡張されたのに伴って、指数部Ex’と仮数部Fx’のビット幅も拡張されています。

 手元には既に、H-FPUのRTLが有りますので、そこからチョチョイと、ビット幅を広げる設計変更をすれば、S-FPUが完成するはずです。従って、改めて作図し直すまでもなく、ブロック図レベルとしては、H-FPUのブロック図と同じ形となります(下図)。

a165_hfpu.png

※half→single、h***→s***等と読み替えてください。内部バスや演算器のビット幅が拡張となります。

 以下、代表的なブロックについて、float型への設計変更を進めて行きます。

加減算

 32ビットコプロセッサバスA[31:0]、B[31:0]から受け取った2数を加減算(A±B)します。演算アルゴリズムについては、half型と同じです。以下、half型との差分についてのみ、考察します。

 float型で表現可能な、非有限値(NaN、INF、ゼロ等)との加減算は、half型と同様です。

乗算

 32ビットコプロセッサバスA[31:0]、B[31:0]から受け取った2数を乗算(A×B)します。演算アルゴリズムについては、half型と同じです。以下、half型との差分についてのみ、考察します。

 float型で表現可能な、非有限値(NaN、INF、ゼロ等)との乗算は、half型と同様です。


 TD IDEのIP Generatorを使って、以下図の様に乗算器マクロを作成してください。

a245_sfpumul.png

 TD IDEの乗算器ハードマクロは18ビット長であるのに対して、必要なのは24ビット長の乗算器なので、4個の乗算器ハードマクロで部分積を作って、加算して求めるような論理を生成するはずです。
 H-FPUの乗算器は11ビット長だったので、18ビット乗算器ハードマクロ1個で充足していました。H-FPUに比べて、論理規模と伝搬遅延時間が増大しますが、H-FPUと同様、1サイクルで実行することにします。クリティカルパスは強化されますが、多分、問題ないでしょう(後ほどSTAで確認)。

除算

 32ビットコプロセッサバスA[31:0]、B[31:0]から受け取った2数を除算(A÷B)します。演算アルゴリズムについては、half型と同じです。以下、half型との差分についてのみ、考察します。

 float型で表現可能な、非有限値(NaN、INF、ゼロ等)との除算は、half型と同様です。

浮動小数の整数化

 float型からint型への型変換だけでなく、floor()関数やceil()関数の実装等に於いて、float型の数値を整数化する必要が生じます。

 floor()関数やceil()関数等を実装するためには、float型のまま、整数化する処理が必要となります。

 この処理を実行する回路だけ、新規設計して追加します。

NihoniumのFPU命令

 NihoniumにH-FPUを接続することは可能なので、H-FPU命令を備えています。H-FPU命令は、hadd命令hmul命令などの様に、先頭文字「h」で始まる命令です。
 では、S-FPU用として「f」で始まる、fadd命令fmul命令などを作りましょう。

 ここで一考。

 そこで、H-FPU命令と、S-FPU命令の命令コード(オペコード)を同一コードに割り当ててしまいます。すると、何という事でしょう!従来のH-FPU命令コードのまま、S-FPUを制御できてしまいます。
 命令数に変動が無いので、論理規模は変わらず、伝搬遅延時間も変わりありません。

※NHビットの設定だけで、16ビットモードと32ビットモードを切り替える、Nihoniumの基本方針と同じ考え方です。

 「欠点」とまで、言えるかどうかわかりませんが、考え得る欠点としては、Nihoniumに対して、H-FPUとS-FPUを同時に接続することは、できなくなります。でも、それだけのことです。普通、S-FPUを搭載したら、並行してH-FPUを使いたいケースは無いでしょうから、問題になることはないでしょう。
 従来通り、H-FPUだけを接続して使うことは可能ですし、新たにS-FPUだけを接続すれば、Nihoniumで単精度浮動小数float型の演算を実行することができます。


 アセンブラとしては、H-FPU命令(h***命令)とS-FPU命令(f***命令)を別名として、同じ命令コードにアセンブルします。アセンブラレベルでは、別命令扱いとなることに留意してください。アセンブルリストの一例を以下に示しておきます。

  // @adr  code  // adr*2 instruction
    @0000 7001  // 0000 fadd r0,r1
    @0001 7001  // 0002 hadd r0,r1


 アセンブラレベルでは、fadd命令hadd命令は別扱いですが、共に同じ命令コード0x7001にアセンブルされていることが判ります。

NHビット

 Nihoniumは、状態レジスタsrのNHビットによって、動作モードが変化します。

 コプロセッサとして接続するS-FPUには、32ビット幅の汎用レジスタや内部バスが必要なので、NH=1を設定した場合だけS-FPU命令を使用することができます。

H-FPU/S-FPUの検出

 プログラム実行時、NihoniumにH-FPUまたはS-FPUが接続されているかどうか?検出したい場合には、以下の命令列を実行して、判定することができます。

  ldbiu r0,1
  hneg r0,r0  // fneg r0,r0 でも良い


 fneg命令hneg命令は同じ命令コードに展開されるので、どちらを使っても結果は同じです。実行後の汎用レジスタr0の値で、以下の様に判定してください。

 検出したFPUに従って処理を変えたり、場合によってはエラー出力してプログラムを停止するなどの処置に使用できます。

整数化する命令の追加

 H-FPUからS-FPUへの設計変更で必要となった、float型のまま整数化する機能について、CPUにも新規に命令を追加します。
 動作は、「ゼロ方向への丸め(round down to zero)」なので、fdown命令とします。

 H-FPUでは、以下のようにすると同じ動作をエミュレートできるため、命令は追加しません。

  fdown r0,r1

    ↓ H-FPUで相当する動作

  huint r0,r1
  hhalf r0,r0
  hmvsg r0,r1


 Moscovium+H-FPUにも追加すると便利かもしれませんが、今回は追加しません。余計な修正がCPUに入ると、検証などの工数が馬鹿にならないので、必要に迫られない限り手を付けないのが賢明だという判断です。

※Blocklyを使った開発環境の整備に伴い、H-FPUにもhdown命令を追加しました。こちらの記事をご確認ください。upd

デザイン

 今回設計したS-FPUを搭載したマイコンNHOC113FS0408を作成しました。

 以下リンクより、最新のデザインをダウンロードすることができます。

btn_moscovium.png

 FPGAボード(Sipeed Lichee Tang Primer)への書き込み手順はこちらを参照してください。

コンパイル結果

 以下に、TD IDEでのコンパイル(論理合成・配置配線)結果をまとめておきます。

 S-FPU単体でコンパイルしてみると、3,300LUT程の論理規模でした。Moscoviumコア+乗除算器が約3,600LUTですので、CPU(Moscovium)ほどの大きさがあります。いくつか並べて比較してみましょう。

ユニット 論理規模[LUT] 乗算器マクロ数
Moscovium 2,600 0
Moscovium+乗除算器 3,600 1
Nihonium 5,600 4
H-FPUコプロセッサ 1,100 1
S-FPUコプロセッサ 3,300 4

 S-FPUは、H-FPUから比べると×3倍ほどの論理規模でした。浮動小数型のビット幅では×2倍なのですが……。まあまあ、大きい回路ですね……。演算精度が許すなら、H-FPUを使った方が、お得感があります。

シミュレーション検証

 iverilogを使って、論理検証を行います。検証方針は、H-FPUの時と同じです(下図)。

a166_verify.png

 half型(16ビット)→float型(32ビット)などに読み替えてください。
 ただし、1点だけ、H-FPUの時とは異なる点がありました。H-FPUの時はピッタリ期待値を一致させることができたのですが、S-FPUでは、どうしてもfloat型最下位ビット(LSB)の値だけ、うまく合わせることができませんでした。

 常に上例の様になるわけではなくて、完全に一致する場合もあり、合わせ込むことは断念しました。一々エラーを表示しても、目視で最下位ビットだけの不一致だと確認するのは手間なので、最下位ビットだけの相違であれば、パス(合致)として検証を実行しました。

 加減乗除算数10,000パタンと、その他のFPUコマンドによる処理結果を検証した結果、問題ありませんでした。勿論、確認した数10,000パタンの範囲内です。本HPの回路やプログラムは全て無保証ですので、ご理解の上、ご利用ください。

命令実行サイクル数比較

 S-FPU命令をシミュレーション実行して見て、各命令の実行サイクル数を調べました。以下表にまとめて、解説します。

H-FPU S-FPU メモ
命令 min max 命令 min max
hadd 3 8 fadd 3 8 仮数部ビット数増加で+1、
指数差≧0x20の対処で−1サイクル。
hsub 3 8 fsub 3 8
hcmp 3 8 fcmp 3 8
hmul 3 4 fmul 3 3 正規化を演算に押し込んで−1サイクル。
hdiv 6 7 fdiv 3 7 除算ステップ数+2、ロード−1、正規化−1サイクル。
huint 3 7 fuint 3 8 uint16→32に伴って、+1サイクル。
hfrac 3 7 ffrac 3 8
hhalf 3 5 fsngl 3 6
hmvsg 2 2 fmvsg 2 2 変わらず。
hneg 2 2 fneg 2 2
hsat 2 2 fsat 2 2
hdown 2 2 fdown 2 2

  解説:

 上に記載の通り、H-FPUのサイクル数を検証したところ、何ヶ所か無駄なサイクルが有りました。S-FPUへ設計変更する際に見直したので、H-FPUと比べてそん色ない速度で処理可能なコプロセッサとなりました。10進数換算で有効数字3桁強しか無かったH-FPUに比べると十分実用的な、有効数字7桁弱のS-FPUが、H-FPUとそん色無い速度で実行できるので、浮動小数演算が必要になった場合には、S-FPU一択で悩まずに済みます。


 なお、H-FPUでも適応可能な見直し箇所もあるのですが、修正すると論理検証までやり直しで無駄な工数🏭が発生するため、H-FPUへの適応は見送ります。S-FPUを使った応用は、次回です。


btn_arduino.png

btn_prev.png btn_next.png

home