公開日:2021/02/27
15th Season
#15-4 CPUの動作設計
マイコンを作るシリーズの最初のターゲットはCPU(前回命名、Moscoviumコア)でした。以下の様なブロック図で設計を進めています。
引き続き、CPUコアの設計を続けます。
パイプライン
設計方針では、「パイプライン方式ではなく、昔ながらのステートマシン方式」を選択しました。以下図に一般的な5段パイプラインの実行状態を示します。
- IF: フェッチステージ
- メモリから命令コードを読み込んでくる実行状態(ステージ)です。
- ID: 命令復号ステージ
- 読み込んだ命令コードを復号して、それがどんな命令なのか?どのような動作をすれば良いのか?解析して準備するステージです。
- EX: 実行ステージ
- 復号結果に基づいて、実際に必要な動作を行うステージです。
- 算術演算や、レジスタ間のデータ転送などが実行されます。
- MA: メモリアクセスステージ
- 命令の中には、外部メモリにアクセスする命令も存在します。
- このステージでは、外部メモリとの間でデータの書き出し、読み込みを実行します。
- WB: ライトバックステージ
- メモリアクセスによって読み込んだデータを内部のレジスタに書き戻すステージです。
上図では、横軸に時間の経過が取られていて、縦軸には実行する命令が上から順に並んでいます。1ステージはクロック立ち上がりエッジ間の1サイクルで実行します。従って、1個の命令には5サイクルの実行時間がかかります。
しかし、IF,ID,EX,MA,WBの各ステージは1段づつずれて同時並列に実行されるため、実質的には各命令共、1サイクルで処理を終了しています。
※上図の場合、縦に7個の命令が、横に11サイクルで実行終了しているので、1命令当たりだと11サイクル/7命令=1.57サイクル/命令です。上図では、命令7個で止めているので、最後の命令7の5サイクル分が加算される分、1.0サイクル/命令を上回っています。命令数N→∞の極限では、1.0サイクル/命令となります。
ただこの図は、よく教科書で見かける「理想的」なケースなのです。
実際の命令に置き換えて考えてみると、例えば命令1のWBステージでレジスタr0にデータをセットする場合、命令2または3のEXで同じr0を操作しようとすると、WBの実行終了を待つ(ストール)必要が生じます。
他にも分岐命令などを実行すると、分岐先アドレスから新たに命令をフェッチ(IFステージ)する必要が生じるので、パイプラインが乱れます。
Moscovium 2段パイプライン
一方、下図が今設計を進めているCPUコア、Moscoviumの実行状態図です。
何だかんだ、Moscoviumの実行形態も、2段パイプラインとなっています。……たったの2段を「パイプライン」と言えるかどうかは疑問ですが……。
Moscoviumの実行ステージは、IDとEX(またはMA)が縮退して1つのステージを構成しています。一般的な5段パイプラインだと2つのステージに分割されている論理を、1つのステージ(1サイクル)に押し込んでいるので、信号の伝搬速度としては遅くなっていて、5段パイプラインに比べて実行クロック周波数を高速化できない欠点があります。
上図は、以下のプログラムを実行した場合の様子です。命令セットのマニュアルはこちらです。
pushw r0
ldbiu r0,2
loop:
subi r0,1
bne loop
popw r0
cmpi r0,0
動作としては、r0=2として、r0を減算(−1)しながら、ループ(loop:)しています。r0=2でスタートしているので、都合2回ループしたら抜けます。従って、上図4行目の分岐(bne)命令は「分岐」していますが、6行目のbne命令は分岐していません。
※Moscoviumの分岐命令は、条件が成立して分岐すると2サイクル、分岐しなかった場合は1サイクルで実行します。
bne命令の条件が成立して分岐した時だけ、次命令のIFステージがストールしてbne命令の実行サイクル数が2サイクルになっていることが解ります。パイプラインが浅い分、乱れた時のオーバヘッドは最小限で済んでいます。
また、がちがちのパイプラインに縛られないので、通常ID&EXステージであるべきところを飛ばして、ID&MAステージとして実行したり、柔軟にリソースを最適活用できるのも利点です。
命令の動作サンプル
続いて、Moscoviumのブロック図を使って、具体的な処理の中身を見て行きます。こちらの命令セットマニュアルで説明してあるように、add命令やand命令などの単純な演算命令は1サイクルで実行が済んでしまって、何も説明することが無いので、少し複雑な動きをするjal命令を例に処理の流れを説明します。
まず、jal命令ですが、サブルーチンへ分岐する命令です。サブルーチンの処理終了後、rtn命令でjal命令の次命令に戻ってくる必要があるので、分岐する前に、現在のプログラムカウンタpc値をスタックに積んでおく必要があります。
この様な複雑な処理のため、jal命令には3サイクルの実行時間がかかります。
- 1サイクル目: IFステージ
-
jal命令の命令コードをフェッチする。1個前の命令と同時に実行される。
- レジスタファイルRGFブロック内のプログラムカウンタ値が専用線を通ってフェッチFCHブロックに転送される。
- FCHブロックからPc値をアドレスとして出力すると、対応するデータが読み出される。
- 読み出したデータをFCHブロック内の命令レジスタirに取り込み、この値を制御CTLブロックに送る。
- 2サイクル目: ID&MAステージ
-
スタックにPcを積む。
- レジスタファイルRGFブロックからスタックポインタsp−2をAバスに出力、PcをBバスに出力する。
- ※Sp−2は、RGFブロック内のSpレジスタが持っているデクリメンタ(−2)でデクリメントした後の値。Moscoviumのスタックはフル下降型なので、新しくデータをスタックに積む場合には、予めSpをデクリメントしておく必要がある。
- メモリアクセスMEMブロックが受け取って、Aバスをアドレス、Bバスを書き込みデータとして出力し、ライトサイクルを発行する。
- 3サイクル目: WBステージ
-
pcに分岐先アドレス(汎用レジスタの内容)を転送する。
- RGFブロック内の汎用レジスタをAバスに出力する。
- ここでは、算術論理演算ユニットALUは何も演算せず、Aバス値をそのままCバスに出力する。
- RGFブロック内のPcがCバス値を取り込む。
- 4サイクル目: IFステージ
- 次命令のフェッチ。動作としては1サイクル目と同じ。
- ただし、3サイクル目、WBステージで更新された新しいPcが指し示すアドレスから命令をフェッチする。
2サイクル目、ID&MAステージではAバスとBバスを使っているのですが、3サイクル目WBステージでもAバスを使い、かつ、ALUを通って汎用レジスタ→Pcへのデータ転送しています。このように、2サイクル目の処理と3サイクル目の処理はリソース(この場合はAバス)が競合しているため、1サイクル内で同時に実行することができず、jal命令の実行には3サイクルを要しています。
例えば、MEMブロックへのレジスタデータ転送パスと、ALUへのレジスタデータ転送パスを独立して2系統持たせることが出来れば、メモリアクセスと汎用レジスタ→Pc転送を1サイクル内で同時に処理できるようになるので、高速に(1サイクル短く)処理することが出来るようになります。
一方、このようにどんどん専用リソースを追加して行くと、論理規模が大きく肥大化して行きます。
今回のMoscoviumコアは、「教科書に載っている様なシンプルなCPU」を目指しているので、高速化のためのリソース多重化はスコープ外としています。最小限に絞ったリソースをうまく使って、各命令を処理していきます。
では以降、CPU内の各ブロックにフォーカスを当てて、基本設計を進めて行きましょう。
メモリアクセスMEMブロック
メモリアクセスMEMブロックのブロック図は以下の様になります。
MEMブロックは、構造としては簡単です。
MEMブロックはCPUの外に接続されたROMやRAM、I/Oレジスタなどのメモリにアクセスするための、「メモリバス🚌」アクセスを司っています。
- babf: アドレスバッファ
- CPU内部のAバス値をメモリバスのアドレスとして、アドレスバスbadr[15:0]に出力します。
- CPUからメモリバスに対してアクセス要求が無い場合、アドレスbadr[15:0]=16'h0000を出力します。
- bwbf: ライトデータバッファ
- Bバス値をメモリバスの書き込みデータとして、ライトデータバスbdatw[15:0]に出力します。
- ワードアクセス(bcmdb=0)では、bdatw[15:0]=Bバス[15:0]
- バイトアクセス(bcmdb=1)では、bdatw[15:0]={Bバス[7:0],Bバス[7:0]}
- ※バイトアクセスでは、汎用レジスタの下位8ビットがアクセス対象データなので、Bバスの下位8ビットを上位・下位バイトにコピーして出力しています。
- CTLブロックからライトアクセス要求(bcmdw=1)が無い場合、bdatw[15:0]=16'h0000を出力します。
- brbf: リードデータバッファ
- メモリバスから読み出したデータはリードデータバスbdatr[15:0]に入力されるので、このデータをCバスに出力します。
- ワードアクセス(bcmdb=0)では、Cバス[15:0]=bdatr[15:0]
- バイトアクセス(bcmdb=1)の偶数アドレスアクセスでは、Cバス[15:0]={8'h00,bdatr[15:8]}
- バイトアクセス(bcmdb=1)の奇数アドレスアクセスでは、Cバス[15:0]={8'h00,bdatr[7:0]}
- bctl:
- CTLブロックからの信号と、メモリバスの制御信号に従って、メモリバスアクセスを制御します。
メモリバス仕様
ここで、メモリバスのアクセス仕様を確定しておきます。まず、メモリバスに使われる信号名とその仕様を示します。
- bclk: バスクロック
- メモリバスアクセスはこのクロックに同期して実行されます。
- Moscoviumでは、CPUクロックと同じクロックを使います。
- brdy: バスレディ
- メモリバスが次のコマンドを受け付ける準備ができた時にbrdy=1となります。
- アクセスに複数サイクルを要する様な、遅いメモリにアクセスする間、brdy=0としてCPUにウェイトを要求します。
- bcmd[3:0]: バスコマンド
- メモリバスに対する、アクセスコマンドを指定します。
- bcmd[0]=bcmdrと記す場合があります。bcmdr=1とすることで、メモリバスに対するリードアクセスを指定します。
- bcmd[1]=bcmdwと記す場合があります。bcmdw=1とすることで、メモリバスに対するライトアクセスを指定します。
- bcmd[2]=bcmdbと記す場合があります。bcmdb=1とすることで、メモリバスに対するバイトデータアクセスを指定します。
- bcmd[3]=bcmdl(エル)と記す場合があります。bcmdl=1とすることで、メモリバスに対するロングワードデータアクセスを指定します。
- なお、バスステートコントローラが、16ビットアクセス空間に対するロングワードアクセスを、ワードアクセス×2回に分割して実行します。
- 従って、16ビットアクセス空間に接続するI/Oユニットなどは、bcmdlを参照する必要はありません。
- 32ビットアクセス空間に接続する、ROM/RAMなど、32ビットアクセス可能なメモリだけがbcmdlを参照します。
- bcmd[1:0]の組み合わせは以下の通りです。
- bcmd[1:0]=2'b00: no operation、メモリバスはなにも動作しません。
- bcmd[1:0]=2'b01: メモリバスに対するリードアクセスを指定します。
- bcmd[1:0]=2'b10: メモリバスに対するライトアクセスを指定します。
- bcmd[1:0]=2'b11: inhibited、このコマンドを発行することは禁止です。
- bcmd[3:2]の組み合わせは以下の通りです。
- bcmd[3:2]=2'b00: ワードサイズアクセスを指定します。
- bcmd[3:2]=2'b01: バイトサイズアクセスを指定します。
- bcmd[3:2]=2'b10: ロングワードサイズアクセスを指定します。
- bcmd[3:2]=2'b11: inhibited、このコマンドを発行することは禁止です。
- バスコマンドの一部機能しか使用しないユニットは、不要なコマンドを知る必要がありません。
- 例)すべてのレジスタがリードオンリのI/Oユニットは、bcmdrを知っていれば良く、bcmdwを知る必要がありません。
- 例)すべてのレジスタがワードアクセスだけであるI/Oユニットは、bcmdbを知る必要がありません。
- こちらも参照してください(ご参考)。
- bmst: バスマスタID
- 現在アクセス中のアクセス主体バスマスタID(CPU ID)を示します。
- 信号の意味は以下の通りです。
- シングルコア版の場合、常にbmst=0
- デュアルコア版でバスマスタがCPU1の時、bmst=0
- デュアルコア版でバスマスタがCPU2の時、bmst=1
- アサートタイミングはbcmd,badrと同じ、アドレスコマンド相です(こちらを参照)。
- bmst信号はBUSCが、バス権を与えたCPUのIDに従って出力します。
- デュアルコア版でどちらのCPUからのアクセスなのかを知らなくて良いI/Oユニットは、本信号を接続する必要はありません。
- badr[15:0]: アドレスバス
- メモリバスのアクセス対象となるアドレスを指定します。
- アドレスはバイトアドレスです。
- 16ビット幅なので、アクセス可能なメモリ空間は64Kバイトです。
- bdatw[15:0]: ライトデータバス
- メモリバスに対するライトアクセスで、ライトデータを指定します。
- ワードアクセス(bcmdb=0)の場合、16ビットデータをbdatw[15:0]に出力します。
- 奇数アドレス(badr[0]=1)に対するワードアクセスは禁止です。
- バイトアクセス(bcmdb=1)の場合、以下の様にアライメントして使います。
- 偶数アドレス(badr[0]=0)に対するライトアクセスでは、有効データは上位側bdatw[15:8]。下位データは無視(Do not care)です。
- 奇数アドレス(badr[0]=1)に対するライトアクセスでは、有効データは下位側bdatw[7:0]。上位データは無視(Do not care)です。
- ※Moscoviumはバイトアクセス(bcmdb=1)の場合、bdatw[15:0]の上位側と下位側に同じデータをコピーして出力します。
- bdatr[15:0]: リードデータバス
- メモリバスから読み出したデータはこのバスに乗ります。
- ワードアクセス(bcmdb=0)の場合、16ビットデータはbdatr[15:0]に乗ります。
- 奇数アドレス(badr[0]=1)に対するワードアクセスは禁止です。
- バイトアクセス(bcmdb=1)の場合、以下の様にアライメントされます。
- 偶数アドレス(badr[0]=0)に対するリードアクセスではデータは上位側bdatr[15:8]。下位データは無視(Do not care)です。
- 奇数アドレス(badr[0]=1)に対するリードアクセスではデータは下位側bdatr[7:0]。上位データは無視(Do not care)です。
はい、そうです。勿論、ビッグエンディアンです。
メモリバスアクセスタイミング
以下、リードおよびライトバスサイクルのアクセスタイミング例です。
- 上半分がライトサイクル、下半分がリードサイクルのタイミング図です。
- メモリバスは、アドレスコマンド相(水色)とリードデータ相(緑色)の2相構成となります。
- ライトサイクルにはリードデータ相(緑色)は存在しません。
- アドレスコマンド相は、brdy=1のclk↑で受け付けられるので、次のリードデータ相と、別なアドレスコマンド相をパイプライン状に発行することができます。
- ライトサイクル: (上図上側を参照してください)
- brdy=1のbclk立ち上がりエッジ(以降bclk↑と記します)で、bcmdw=1がアサートされると、ライトサイクルが発行されます(水色部分)。
- この時、ライトデータも同じタイミングで確定する必要があるので、ライトアクセスの場合、bcmd[2:0]、badr[15:0]、bdatw[15:0]がセットでアサートされます。
- メモリバスに対する書き込み指示はこの1サイクル(水色)で終了するので、CPUは即座に次の処理に移ることが出来ます。
- 一方、ライトアクセスに複数サイクル掛かる、遅いメモリへのアクセスだった場合、brdy=0としてバスサイクルを延長する場合があります(黄色部分)。
- リードサイクル: (上図下側を参照してください)
- brdy=1のbclk↑で、bcmdr=1がアサートされると、リードサイクルが発行されます(水色部分)。
- リードアクセスの場合、bcmd[2:0]、badr[15:0]がセットでアサートされます。
- リードアクセスに複数サイクル掛かる、遅いメモリへのアクセスだった場合、brdy=0としてバスサイクルを延長する場合があります(黄色部分)。
- リードサイクルが受け付けられた(水色)後、再びbrdy=1となったbclk↑で、リードデータbdatr[15:0]が有効データとなります(緑色部分)。
- CPU(バスマスタ)は、brdy=1のbclk↑でコマンドが受け付けられたら、次のサイクルで直ちに次のアクセスに対するアドレスとコマンドを出力して良いことになっています。
- ただし、そのコマンドが受け付けられるか否かは、次のサイクルのbrdy値で決まります。
- 複数サイクルアクセスの遅いメモリと通信している間はbrdy=0となってバスサイクルが延長されているので、CPUが次のコマンドを出力していても無視されて、CPUが待たされます。
リード/ライトアクセスとも、1サイクルで読み書きが終了する高速メモリの場合、brdy=0となる黄色部分のサイクルは一切挿入されません。
レジスタファイルRGFブロック
レジスタファイルRGFブロック内には、プログラマモデルで決定したCPUのレジスタを実装します(下図)。
※後日拡張したレジスタなどが更新された最新のブロック図となっています。
- RGFブロックの最上位には、汎用レジスタのバンク×4組と制御レジスタのステータスレジスタsr、プログラムカウンタpc、スタックポインタspが実装されています。
- すべてのレジスタはリセット解除後、0x0000に初期化されています。
- 各々、2つの読み出し口がAバス、Bバスに接続されています。
- Moscoviumのバスは、全て単方向のスタティックバスです。
- すべてのレジスタ(トランシーバ)は、アクセス要求が無い間は16'h0000を出力しています。
- CPUのCTLブロックからアクセス要求があると、指定されたバス(AまたはB)の出力にだけ、その時のレジスタ値を駆動します。
- CTLブロックはバス(AまたはB)につながるレジスタの内、1個だけにアクセス要求を出すので、全てのレジスタ出力をビット毎論理OR演算して出力した物がバスの値となります。
- 各レジスタへの書き込み口はCバスです。
- ステータスレジスタsr:
- 演算ステータス(N,C,V,Z)フラグをCTLブロックに出力したり、ALUの演算結果(N,C,V,Z)を取り込んだりするための信号線が各ブロックと接続されています。
- 汎用レジスタのバンクを選択するビットはsr_bank[1:0]と言う信号名で制御ブロックrctlに出力されており、rctlブロックで復号して各バンクへ分配(bank_sel[3:0])されます。
- プログラムカウンタpc:
- 実行する命令コードが格納されたアドレスを保持するレジスタです。
- フェッチする度にインクリメント(+2)されます。
- このインクリメンタは、フェッチFCHブロックが持っていて、PcからFCHブロックへPcを転送すると、インクリメントした値がRGFブロックのPcに戻ってきて、自動更新されます。
- Pcはリセット解除後、0x0000に初期化されているので、Moscoviumは0x0000番地から命令を実行開始します。
- スタックポインタsp:
- フル下降型スタックを操作するためのポインタを格納するレジスタです。
- 内部にインクリメンタ(+2)とデクリメンタ(−2)を備えていて、CTLブロックからの指示によって、値の増減(+2または−2)や、読み出し口(A,Bバス)への(Sp−2)値の出力機能を持っている点で、他のレジスタとは異なる機能となります。
バンク内のブロック図は以下図の通りです。
バンクbankブロック内に汎用レジスタ×8個を格納しています。A,Bバス出力は、bank内で1系統に縮退してから出力します。設計としては、bankブロックを追加/削除することで、容易に実装バンク数を変更可能な構成にしておきます。
算術論理演算ユニットALU
算術論理演算ユニットALUには、加減算器art、論理演算器log、シフト演算器sft(バレルシフタ)が実装されています(下図)。
CTLブロックからのコマンドに基づいてAバス値とBバス値の演算を施し、Cバスに出力します。以下表に演算コマンドをまとめておきます。
ブロック |
コマンド名 |
コード |
演算 |
ALU |
NOP |
4'h0 |
no operation: C=0x0000 |
art |
ADD |
4'h1 |
add: C=A + B |
SUB |
4'h2 |
subtract: C=A - B |
ADC |
4'h3 |
add with carry: C=A + B + cin |
SBB |
4'h4 |
subtract with borrow: C=A - B - cin |
log |
AND |
4'h5 |
bitwise and: C=A&B |
OR |
4'h6 |
bitwise or: C=A | B |
EOR |
4'h7 |
bitwise exclusive or: C=A ^ B |
NOT |
4'h8 |
bitwise not: C=~A |
OUTA |
4'h9 |
through: C=A |
CEND |
4'ha |
change endian: C=(A<<8) | (A>>8) |
LDIS |
4'hb |
load signed byte: C=int8(B&0xff) |
LDIL |
4'hc |
load low byte: C=(A&0xff00) | B&0xff |
LDIH |
4'hd |
load high byte: C=(B<<8) | (A&0xff) |
sft |
ASFT |
4'he |
arithmetical shift: C=int16(A)<<int5(B) |
LSFT |
4'hf |
logical shift: C=uint16(A)<<int5(B) |
- artブロック:
- 加減算(算術演算)を担当します。
- 減算は、2進数の演算ルールに則り、Aに(−B)を加算します。
- 2の補数表現では−B=(~B)+1なので、この処理がartブロック内のnegブロックで実行されています。
- また、Moscoviumは16ビットCPUですので、32ビットの加算を実行するためには下位16ビットを加算した時のキャリフラグ(SrレジスタのCフラグ)を上位16ビットの加算時に足し込む必要があります。
-
例)レジスタペアr0:r1とr2:r3の32ビット加算コード
add r1,r3
adc r0,r2
各々、偶数レジスタが上位ワード、奇数レジスタが下位ワードの場合。
- この処理を可能とするため、RGFブロックのSrレジスタからCフラグ値を接続して、ADCコマンド実行時にcinとして加算する機能(ADCコマンド)を持っています。
- 同じく、減算時のボロウに対処するためのSBBコマンドを有します。
- logブロック:
- 所謂論理演算(NOT,AND,OR,EOR等)だけでなく、バイト操作などのコマンドもこのブロックで担当します。
- Aバスのデータを何もせずにそのままCバスにスルーする演算(OUTAコマンド)もこのブロックを通ります。
- sftブロック:
- Moscoviumには、16ビットのバレルシフタを搭載します。
- ソフトウェア的には、比較的コストの小さい演算ですが、ハードウェア的にはそうはいきません。
- 確かに、ハードウェアにとっても定数シフト演算のコストは低いです。
- と、言うより、定数シフトは単なる配線の繋ぎ変えだけなので、コストはほぼ0です。
- しかし、バレルシフトとなると話は別です。
- 16ビットのバレルシフトは、Aバス値の-16〜15までのシフト結果32通りをあらかじめ用意しておいて、シフト量(Bバス値)に従って32通りから1つのデータを選択する、16ビット32 to 1という巨大なセレクタとなり、相当数の論理規模となります。
- ここにさらに、算術シフトと論理シフトの選択肢が追加されるので、Moscoviumのバレルシフタは、16ビット64 to 1セレクタというお化けです。
- 非常にコスト高な回路なので、普通のCPUコアは1ビットシフト演算命令だけだったり、精々1,2,4ビット程度のシフト演算命令しか搭載していないのですが、まあ、ARMにも搭載されているので、16ビットフルのバレルシフタを搭載することにしました。……あちら(ARM)は32ビットのバレルシフタなので、もはや怪獣……。
- CPU完成後、バレルシフタの論理規模については検証しておこうと思います。
- ※バレルシフタの論理規模削減については、こちらのアップデートをご覧ください。
フェッチFCHブロック
フェッチFCHブロックは、プログラムメモリから命令コードをフェッチするブロックです(下図)。
フェッチバスから命令コードを読み出します。
メモリバスとは異なりフェッチバスは、指定したアドレス(16ビット境界)からワードデータを読み出すだけですので、このバスはレイテンシ0のメモリとなっています。つまり、CPUクロックclk↑でアドレスfadr[15:0]が更新されると、次のclk↑で有効データがfdat[15:0]から読み出すことが出来るメモリです。
プログラムカウンタpcはフェッチする度にインクリメント(+2)して次々命令を読み出す必要があるので、Pc+2した結果をRGFブロックのPcに戻すパスを持っています。Pcはリセット解除後、0x0000に初期化されているので、Moscoviumは0x0000番地から命令を実行開始します。
読み出したデータは命令レジスタir(Instruction Register)に取り込み、CTLブロックで復号するため、送信します。
CPUブロック図の中で、残るはCTLブロックのみとなりました。CTLブロックについては次回解説して、CPUの設計を終了したいと思います。