パスワードを忘れた? アカウント作成
16550221 journal
日記

kitune-sanの日記: Verilog HDLでマイクロシーケンサ(CPU)を作る

日記 by kitune-san

ひさしぶりの日記。

MMC(SD)にアクセスしたり、カードにIDE(ATA)のプロトコルを使用してアクセスするためのラッパーなどを無理やりHDLコードを書いていたのだが、
ステートマシンを含めた組み合わせ回路が複雑化してきた。

そこで組み合わせ回路の部分をシーケンサに置き換えることで、全体の設計をシンプルに、そしてLEの使用量を削減できるのではないだろうか?という考えから、
勉強がてらマイクロシーケンサを作ってみることにした。多分初めて設計した自作CPU。
結果として結構使いにくそうなものができたので、実用になるかはわからん。

完成したものはその特徴からLD/STシーケンサ(仮)と名付けた。

ブロック図はこれ
https://github.com/kitune-san/LDST_Sequencer/blob/main/DOC/img/LDST_DIAGRAM.png

CPUのコードはこれ
https://github.com/kitune-san/LDST_Sequencer/blob/main/HDL/LDST_SEQUENCER.v

## 命令

命令は、12ビット固定長で、LD/LDI/ST/CALL/RET/JZ/JC/JO/JMPの9種類。
命令の形式はシンプルで、上位4ビットにOPコード、残りの下位8ビットをデータやレジスタの指定に割り当てた。
後述するが、ALUの操作命令はシーケンサの命令から取り除いた。
最初は、subleq命令のみを実装しようかと思ったが、プログラムの作成の難易度が上がるのと、コードの効率が悪そうだと考えたためこれはやめた。

0000 rrrr rrrr : LD: rに指定したレジスタNo.の値をワークレジスタにロードする。
0010 dddd dddd : LDI: データdをワークレジスタにロードする。
0001 rrrr rrrr : ST: ワークレジスタの値をrに指定したレジスタへストアする。
0100 dddd dddd : CALL: 現在のアドレスをスタックへプッシュし、(ワークレジスタ * 256 + データd)のアドレスへジャンプする。
0101 0000 0000 : RET: アドレスをスタックからポップし、そこへジャンプする。
1001 dddd dddd : JZ: ゼロフラグが1ならば、(ワークレジスタ * 256 + データd)のアドレスへジャンプする。
1010 dddd dddd : JC: キャリーフラグが1ならば、(ワークレジスタ * 256 + データd)のアドレスへジャンプする。
1100 dddd dddd : JO: オーバーフローフラグが1ならば、(ワークレジスタ * 256 + データd)のアドレスへジャンプする。
1000 dddd dddd : JMP: 無条件ジャンプ。(ワークレジスタ * 256 + データd)のアドレスへジャンプする。

## レジスタ
マイクロシーケンサ内部のレジスタは、5つ。この内ワークレジスタを除く4つのレジスタは、以下のアドレスに割り当てられている。

アドレス : 名前 : 機能
0 : A : Aレジスタ。ALU実行時に使用する。一時レジスタとしても使用できる。
1 : B : Bレジスタ。ALU実行時に使用する。一時レジスタとしても使用できる。
2 : FLAGS : ALUの計算結果に応じて対応するフラグが立つ。ジャンプ命令の条件判定として使用する。
3 : ALU OP/RESULT : ST命令で、ALU OPコードを設定する。 LD命令でALUOPコードが実行され、結果を取得する。
4-255 : ユーザー定義エリア : 外部バスを使用してユーザーが設定できる。

これらのレジスタに値を設定するときや、値を移動させるときは、LD/ST命令を使用してワークレジスタを介さなければならない。

例えば、
Aレジスタに5を入力する場合は:
0010 0000 0101: LD #5
0001 0000 0000: ST A

Aレジスタの値をBレジスタに入力する場合は:
0000 0000 0000: LD A
0001 0000 0001: ST B
となる。

レジスタのアドレス幅は、8ビットであるため、万が一255を超えるメモリなどが必要となった場合は、レジスタバンクなどを用意する必要がある。

## ALU
よくあるCPUの命令コードには、ALUの操作が含まれているが、
今回設計したシーケンサには、ALU操作命令は含まず、LD/STによるレジスタアクセスを介してALUを操作および結果の取得を行うようにした。
つまり、ALUを動作させる場合は、
1. ST ALU(0001 0000 0011)で実行したいALU OPコードをセットし、
2. LD ALU(0000 0000 0011)でALUを実行させ、結果をワークレジスタにセット。(同時にフラグが更新)
という手順を取る必要がある。

当初はこのようなことを考えておらず、subleq命令のみのシーケンサを考えていたが、このLD/STシーケンサよりもプログラムメモリ消費が多くなりそうに思えた。
プログラムメモリの消費がそれほど多くなく(ただし少なくもなく)、回路の規模もそれほど多くなく(ただし少なくもなく)といった、
都合のいい塩梅を考えてみた結果、このような構成にすることにした。ただし本当にうまい具合になっているかは検証してないのでわからない。

ALU OPコードは以下の通り、
OP Code : OP : 動作
0000 0000 : AND : A and B
0000 0100 : NAND : A nand B
0010 0000 : OR : A or B
0010 0100 : NOR : A nor B
0010 1100 : NOT : not A
0100 0000 : XOR : A xor B
0100 0100 : XNOR : A xnor B
1000 0000 : ADD : A add B
1000 0001 : ADC : A + B + carry
1000 0011 : SUB : A - B
1000 0011 : SBC : A - B - !carry
1010 0000 : SHL : A << 1
1010 0001 : SHCL : A << 1 最下位ビットにキャリーを格納
1100 0000 : SHR : A >> 1
1100 0001 : SHCR : A >> 1 最上位ビットにキャリーを格納
1110 0000 : SAR : A >> 1 最上位ビットはシフト前と同じ

ALU使用例:10+5の場合
---
# A ← 10
0010 0000 1010: LD #10
0001 0000 0000: ST A

# B ← 5
0010 0000 0101: LD #5
0001 0000 0001: ST B

# ALU ← ADD
0010 1000 0000: LD #ADD
0001 0000 0011: ST ALU

# work_reg ← A + B
0000 0000 0011: LD ALU
---

今でも、ALU操作命令をシーケンサのOPコードに含めるかどうか悩んでいる。
プログラムコードの効率という考え方で行くと、
今のやり方では一回の計算で12bit x 2命令 = 24bitかかるのに対し、
シーケンサのOPコードにALU操作命令を含んだ場合、OPコード長が4bit長くなるとしても、16bit x 1命令 = 16bitで済むため、24 - 16 = 8bit分コード効率がいいと考えることができる。
しかし、計算以外の単純な転送命令、ジャンプ命令などは16-12=4bitつねに無駄が生じることになる。

命令を可変長にすればいいのだろうか?まあでも今のやり方も可変長命令みたいなものである。

この議論は賞味期限が切れたので、アーカイブ化されています。 新たにコメントを付けることはできません。
typodupeerror

長期的な見通しやビジョンはあえて持たないようにしてる -- Linus Torvalds

読み込み中...