公開日:2021/05/22
title.png

16th Season


#16-3 除算器の設計

 オリジナル16ビットCPUコアMoscoviumに、乗除算コプロセッサを追加して義務教育くらい卒業させてあげようという親心で進めているテーマです。前回、乗算コプロセッサの設計を済ませたので、今回は除算コプロセッサを追加して、テーマを完成させようと思います。

算数の時間:割り算

 時に皆様、「割り算」覚えていますか?

 10÷3=3…1は、良いですよね?被除数10から除数3は商3回引けて、あまりは剰余1、です。被除数Den、除数Dso、商Quo、剰余Remとおくと、以下の関係式となります。

  Den÷Dso=Quo…Rem
  Den=Dso×Quo+Rem

 じゃ、(−10)÷3はどうなりますか?=−3…−1?=−4…2?どっちだったっけ?本気で忘れてます(1^^;)。調べてみたところ、小学校算数で符号無、中学校算数で符号付の除算を習うみたいです。と、言う事は筆者は「中学算数を忘れてしまった」と言う事ですね?
 「−3…−1」でも「−4…2」でも、上の関係式はちゃんと満たしています。剰余Remも、abs(Rem)<abs(Dso)だし、どちらでも良さそう。
 さらに突っ込んで調べてみたところ、ユークリッド除法と呼ばれる除算法が有って、これによると剰余は、「除数より絶対値が小さい非負の数」だそうです。つまり、「−4…2」が正解、あくまでユークリッド除法に従うと、ですけどね。

 「算数」的にどう習ったかは忘れてしまったけど、コンピュータ除算では以下の通りです:

  10÷3=3…1
  10÷(−3)=−3…1
  (−10)÷3=−3…−1
  (−10)÷(−3)=3…−1

 被除数と除数の符号が変わっても、商と剰余の絶対値は変わらない、これがコンピュータ除算です。負の剰余が出てきますので、少なくともユークリッド除法ではないみたいですね。
 ここでは、コンピュータ除算の考え方に基づいて割り算回路を設計します。

※学校のテストでは、「教えられたルール」から逸脱したらX、間違いにされてしまうでしょう。この歳となっては、そんなの単なる「考え方(や定義方法)の違い」なので、別に間違いも正解も無いのですけどね……。

ステップ(シフト)除算器

 前回、乗算器で説明したのと同様な、ステップ(シフト)方式の除算器です。ブロック図は以下の様になります。

a163_divstp.png

 原理は、昔算数で習った縦積み割り算とほぼ同じです。ただし、演算は2進数です。2進数であることを最大限利用した「引き放し法」を使います。以下、上図を使いながら、引き放し法のアルゴリズムを解説してみます。

 この様にステップ除算では、商と剰余がほぼ同時に求まります。論理回路で設計する場合、1ステップ=1クロックなので、例えば16ビットの除算を実行するには、16サイクルかかります。剰余が必要な場合には更に+1サイクルです。
 特徴に関しては、ステップ乗算の時と全く同じです(↓)。


 TD IDEには、DIVIDERマクロが用意されています。「Toolsメニュー ⇒ IP Generator」から、DIVIDERマクロを選択して見てください。普通のNビット符号無ステップ除算器でした。シミュレーションしてみたところ、計算結果が求まるまでNサイクル掛かりました(ビット数Nは選択可能)。
 DIVIDERマクロを使うと、自分で除算器を設計しなくても済みますよ。

フラッシュ除算器

 乗算器がそうであった様に、除算器にもフラッシュ型の回路が存在します。

  module div4f (
    den,
    dso,
    quo,
    rem);

  input [3:0] den;
  input [3:0] dso;
  output [3:0] quo;
  output [3:0] rem;

  wire [7:0] dso3={ 1'h0, dso[3:0], 3'h0 };
  wire [7:0] dso2={ 2'h0, dso[3:0], 2'h0 };
  wire [7:0] dso1={ 3'h0, dso[3:0], 1'h0 };
  wire [7:0] dso0={ 4'h0, dso[3:0] };

  wire [7:0] den2={ 4'h0, den[3:0] };

  wire [7:0] rem3=den2[7:0] - dso3[7:0];
  wire [7:0] rem2=(rem3[7])? rem3[7:0] + dso2[7:0]: rem3[7:0] - dso2[7:0];
  wire [7:0] rem1=(rem2[7])? rem2[7:0] + dso1[7:0]: rem2[7:0] - dso1[7:0];
  wire [7:0] rem0=(rem1[7])? rem1[7:0] + dso0[7:0]: rem1[7:0] - dso0[7:0];

  assign quo[3:0]=(~{ rem3[7], rem2[7], rem1[7], rem0[7] });
  assign rem[3:0]=(rem0[7])? rem0[3:0] + dso0[3:0]: rem0[3:0];

  endmodule


 上のコードはイメージです。動作検証は一切していませんのでご注意ください。
 4ビットのフラッシュ除算器となっていて、とりあえず、「演算速度重視型」で作ったものです。このRTLだと、4÷4ビットの除算に8ビットの加算回路が7個と、4ビットの加算回路が1個生成されて、かなり規模の大きな回路になる代わりに、演算速度は速いです。
 実用的には、上のRTLを16ビットなり、32ビットなりに拡張すると、フラッシュ除算器が完成します。ただ、フラッシュ除算器はほとんど使われません。

 部分積が各々独立して計算できて、全ての部分積を加算するだけで良かったフラッシュ乗算器と異なり、除算は、各ステップ毎に除数が引けたかどうか?の判定が入り、判定結果で次のステップを加算するのか減算するのか?が、変わります。このため、演算速度が遅く、かつ、論理規模も大きくなってしまうのです。

※↑フラッシュ除算器は、「ステップ除算器よりはずっと速い」けど、「フラッシュ乗算器よりはずっと遅い」と言う事です。「速い」と「遅い」が入り乱れていてわかり難い文章になってしまいました……m(1__)m。

SRT除算器

 最近のカッコ良いCPUが搭載している除算回路はSRT除算と呼ばれる方式を使っています。SRTは、発案者(3名)の名前の頭文字だそうです。
 基本的にはステップ除算でありながら、テーブルを使って演算結果を予測することで2〜4ビット毎に商を計算できるらしいです。
 従って、演算に掛かる時間は、通常のステップ除算器に比べて×1/2〜1/4サイクルに短縮できるのが特長みたいです。

  SRT除算アルゴリズムについて検索する(別ウインドウが開きます)

 残念ながら、具体的な参考資料に辿り着けず、この方式を適用するには調査不足ですm(1x_x)m。

追加する除算命令仕様

 Moscoviumに追加する除算命令について考察を進めます。
 乗算器は、16×16=32ビット乗算器があると、そこから32×32=32ビット乗算結果を得ることは簡単でした。
 ところが、除算はそうはいきません。16÷16ビット除算器を使っても、32÷32ビット除算はできないのです。

※ちなみに、N÷Nビット除算で得られる商と剰余はそれぞれNビットです。

 Moscoviumは16ビットCPUですから、16÷16ビット除算が出来たら良いんじゃないの?とも、思いましたが、やはりそれでは足りずに、32ビット演算したい場合もあるのではなかろうか?と、思ってしまいました。そこで、追加する除算命令を以下の様に決めました。

 16ビットCPUのMoscoviumなのですが、32ビット除算ができるような仕様です(↑)。

 もし、普通のステップ除算器(TD IDEのDIVIDERマクロ等)を使うなら、32ビット除算には32サイクルを要します。そこで32ビット除算命令は、除算処理をキックする命令(divlu命令,divls命令)と、除算結果を回収する命令(divlq命令)に分離してあります。こうすることで、除算が終了するまでの32サイクルは除算以外の処理を進めておくことが出来ます。

割り込みへの対処

 この場合、気を付けなくてはならないことは、割り込み処理ルーチン内で他の除算命令を実行する場合、除算コプロセッサ内の演算結果レジスタ(QuoおよびRem)をスタックなどに退避してから使わなくてはならない事です。通常処理のdivlu命令/divls命令と、divlq命令の間で割り込みがかかって、割り込み処理ルーチン内で除算命令を使ってしまうと、通常処理に戻った時に誤った結果を読み出してしまうためです(下はISRの例)。

  isr_vect:  // 割り込みサービスルーチン(ISR)
  ldcl sr,3
  sesr sreg_b_dr
  divlq  // 除算コプロセッサの演算結果レジスタを読み出して
  pushwm r0,r1,r2,r3  // スタックへ退避
    :
  ldbiu r0,0xfe
  ldbiu r1,0x76
  divu r0,r1  // ISR内で割り算命令を実行する場合
    :
  popwm r3,r2,r1,r0
  divlr  // 演算結果レジスタを回復してから戻る
  rti


 あるいは、ソフトウェア(OS等)で、「割り込み処理ルーチン内では除算命令を使わない」等という取り決めをして、競合を回避してあれば、特に気にする必要はありません(内部レジスタの退避・回復不要)。

※32ビット除算命令のキックと結果回収命令を分離したことで、「割り込み応答性能」も向上しています。INTCから割り込み要求irqがアサートされた時、CPUは現在実行中の命令実行を終了後、割り込み処理を開始します。つまり、割り込みを受け付けて処理を開始するまでの応答時間は、最長の命令実行サイクル数で決まるのです。

ハイブリッド除算器

 ステップ除算器は遅すぎ、フラッシュ除算器は大きすぎ、SRT除算器は調査不足、と、どれもうまくありません。そこで、今回は折衷案として、ステップ除算とフラッシュ除算のハイブリッド方式で論理を組んでみようと思います。

 32ビットステップ除算では32ステップが必要なところ、4ビット分の部分除算を1回のフラッシュ除算に置き換えます。こうすることで、32/4=8ステップで算出することが可能となります。
 論理規模に関しても、32ビット分のフラッシュ除算回路に比べて、4ビット分の部分フラッシュ除算回路であれば、何とか許容範囲に収まってくれるでしょう。

 以下にハイブリッド除算コプロセッサのブロック図を示します。

a163_divhybl.png

 この構成で除算コプロセッサをコーディングして、コンパイル(論理合成・配置配線)してみたところ、791LUT、171FF、伝搬時間約21.7ns(約54.8MHz)という結果となりました。LUT数はCPUの半分弱……、かなり大きいですね。4ビット分とは言え、フラッシュ除算器の論理規模が大きいせいです。
 スピード(STA)の方は、今回のターゲットであるfcpu=24MHz(約40.6ns)程度では、全く問題ありませんでした。

論理検証

 前回の乗算コプロセッサは、基本的にTD IDEのDSPマクロを使っているので、まあ、演算結果は正しいでしょう。
 しかし、今回の除算コプロセッサは独自設計なので、ちゃんと検証しておかないと安心して使えません。

※昔、ペンティアムの割り算バグとか話題になりましたよねぇ?

 まず、適切な数値を使ってシミュレーションして、細かい動作が期待通りに動いているかどうかを確かめました。その後、データ依存で誤動作するケースが無いことを確かめるため、乱数を入力として使い、期待値をVerilog-HDLやExcelなどで計算しておいて、期待値との比較をする、ロードテストを実施しました。
 何分、被除数32ビット、除数32ビット、符号付/無と、232+32+2通りの組み合わせが有るので、全組み合わせのテストは現実的に不可能でしょう。この場は趣味&DIYのレベルなので、1日とか2日とかシミュレーションし続けてのロードテストも実施していません。

シミュレーション波形確認

 下図は、32ビット符号付除算実行中のシミュレーション波形です。

a163_divsim.png

 上の方にMoscoviumとのコプロセッサI/F信号が並んでいます。コプロセッサコマンドccmd[4:0]=4(SDEN)から32ビット符号付除算が始まって、ccmd[4:0]=4'hd(REML)でデータを受け取って、終了しています。上の例だと14サイクル要しています。
 32ビットステップ除算は8サイクル(crdy=0の期間)なので、残りの6サイクルはMoscoviumとのデータ転送に使っています。32ビットの商と剰余を16ビット幅のcbus[15:0]で転送するだけで4サイクルを費やしていますので、16ビットCPUであるMoscoviumでは、この辺りが限界みたいです。

※仮に、32ビットフラッシュ除算器を作って、1サイクルで除算を終わらせたとしても、データ転送などに費やす6サイクルはそのまま残るため、1+6=7サイクルが限界です。これ以上の性能向上にはCPUの32ビット化や、データ転送バスのバンド幅拡張などの手段が必要となります。

単体ロードテスト

 以下は、除算コプロセッサ単体シミュレーションで乱数の割り算をしながら、iverilogが計算した期待値と比較したロードテストのほんの一部抜粋です。

  実験サンプル:【 】内はCygwinシェルウインドウからの実行コマンド
  【iverilog -f test_ut_divc.lst ; if ( ! $status ) vvp a.out】
    :
  sl b46ac468 a8bca251 00000000 b46ac468 ; 00000000 b46ac468 OK
  sl 53aa9ba7 006e1382 000000c2 003fd323 ; 000000c2 003fd323 OK
  sl 0d06f11a 00008c71 000017bf 000001cb ; 000017bf 000001cb OK
  sl 5bcee1b7 000000cb 0073c71a 00000019 ; 0073c71a 00000019 OK
  sl d795dcaf 991d2b00 00000000 d795dcaf ; 00000000 d795dcaf OK
  sl c14a0c82 96720000 00000000 c14a0c82 ; 00000000 c14a0c82 OK
  sl ec09c0d8 c0000000 00000000 ec09c0d8 ; 00000000 ec09c0d8 OK
  ul 618fb9c3 234c8746 00000002 1af6ab37 ; 00000002 1af6ab37 OK
  ul abce9857 00e77053 000000be 00093abd ; 000000be 00093abd OK
  ul 3d13417a 00002541 0001a3b1 00001c89 ; 0001a3b1 00001c89 OK
  ul f5c624eb 00000005 3127a0fb 00000004 ; 3127a0fb 00000004 OK
  sw be65 e735 0002 effb ; 0002 effb OK
  sw df20 00c3 ffd5 ffe1 ; ffd5 ffe1 OK
  sw 102d f800 fffe 002d ; fffe 002d OK
  uw 0186 9e2b 0000 0186 ; 0000 0186 OK
  uw d382 0088 018e 0012 ; 018e 0012 OK
    :


 先頭の"sl"等は、Signed/UnsignedとLong/Wordの区別で、その後ろに除算コプロセッサdivcに計算させた被除数,除数,商,剰余が並び、セミコロン(;)の後ろにスティミュラスでiverilogに計算させた期待値の商,剰余が続き、その期待値比較結果(OK)が表示されています。
 ランダムな数10,000パタンを流してみた中ではエラー(ERR)は出ませんでした。
 パタン数を増やすと、上の様なテキストデータのログもバカにならないサイズになってきますので、波形データ(VCDファイル)の出力はせず、ログも期待値との比較がERRにならない限り出力しないようにしてテストしました。

 32ビット除算が232+32+2通りだと記載しましたが、16ビット除算機能もあるので、そちらがさらに216+16+2通りです。全パタンの検証は現実的に不可能なので、当方が満足したパタン数で検証は止めました。

※ペンティアムの割り算バグは、61/100万パタン程の低確率でしか起きなかったらしいので、数10,000パタンのロードテストでは全く不足です(1^^;)。本HPは趣味&DIYの世界なのであしからず。

結合テスト

 以下、コプロセッサ単体ではなく、Moscoviumマイコンとしての結合テストで検証した時のアセンブラソースをご参考にリンクしておきます。

  ロードテストプログラム:mcvm_ldt_divu.asm

 期待値データは64通りで止めてあります。Excelなどを駆使して期待値データを作っておくと、メモリ容量(Moscovium)が許す限りのデータを検証することが出来ます。

 演算の正しさに関しては単体テストで検証するのが順当ですから、結合テストで検証すべきはMoscoviumとのインターフェイスです。
 データの受け渡しが正しいか?無駄なサイクルが挟まっていたり、特定のデータを演算した時に読み出しタイミングが早すぎて、計算途中の結果を読み出していないか?等のインターフェイスに関係するエラーが無いことが確かめられれば良いと思います。

コンパイルオプション

 前回示した以下図の様に、CPU階層の下にコプロセッサとMoscoviumとの接続階層が作ってあります。

a162_copro.png

 以下の様なコンパイルオプションを用意してあるので、必要に応じて有効化/無効化してください。

  // compile option
  //`define MCVM_COPR_NOMUL  // 乗算コプロセッサを無効化する
  //`define MCVM_COPR_NODIV  // 除算コプロセッサを無効化する


 上の通りにコメントしておくと、Moscoviumに乗除算コプロセッサが接続されます。コメントを外すと、どちらか、または両方のコプロセッサが無効化され、FPGAボード(Sipeed Lichee Tang Primer)に書き込むデータから削除されます。コプロセッサを削除したところで、適切に端子処理されるため、MoscoviumおよびMoscoviumマイコンは正常に動作します。
 ただし、無効化したコプロセッサを必要とする演算命令(mulu命令やdivu命令等)は正しい答えを返さないため、使用禁止です。

※コプロセッサを接続していない時に、該当する命令を使ってしまったとしてもハングしたりしない様な端子処理をしてあります。演算結果が正しくない(常にゼロが返る)だけの事です。コプロセッサ接続有無を確認するために敢えて乗除算命令を実行して、非ゼロの結果が取得できるかどうか?確かめるのは有効です。

 以下ページに乗除算コプロセッサ対応版マイコンMCOC115CP0408のデザインデータをリンクしておきました。

btn_moscovium.png


  コンパイル(論理合成・配置配線)結果:


 ハイブリッド除算アルゴリズムによって、一番速い16ビット符号無除算が7サイクル、32ビット符号無除算が14サイクルですので、何とか、普通のステップ除算に比べて半分以下のサイクル数で割り算できる回路にできました。ただ、使用したフラッシュ除算回路は4ビット分の部分除算なので、除算そのものは1/4のステップ数(32/4=8ステップ)で計算できているのです。ちょっとオーバヘッドが痛い💉ですね……。


btn_arduino.png

btn_prev.png btn_next.png

home