4bit CPU TD4のVerilog実装simpleTD4の解説

# simpleTD4 
TD4は『[CPUの創りかた](https://www.amazon.co.jp/dp/4839909865/
)』で設計されたシンプルな4bitのCPUです。『CPUの創りかた』ではICで実装していますが、[simpleTD4](https://github.com/asfdrwe/simpleTD4)ではTD4をVerilogで60行程度の1ファイルで実装しました。

『[作ろう! CPU](https://www.amazon.co.jp/dp/4839968519)』という書籍ではSystemVerilogでTD4を実装していて、SystemVerilogの方が色々な点でVerilogより優れていると思いますが、2021/4/24時点ではsimpleTD4で使用しているIcarus VerlogのSystemVerilog対応が不十分なのでVerilogで実装しています。

前半でsimpleTD4の動かし方を説明し、後半でsimpleTD4の実装を行単位で解説していきます。

# simpleTD4の動かし方
## PCでシミュレーションする場合
[Icarus verlog](http://iverilog.icarus.com/)でシミュレーションします。Linuxなら大抵のディストリビューションでバイナリが用意されていると思います。[Windows版](http://bleyer.org/icarus/)もあります。

### ダウンロード
gitでファイルをダウンロードしてください。

```
$ git clone https://github.com/asfdrwe/simpleTD4.git
```

### 論理合成
Icarus verilogで論理合成します。

   ```
$ iverlog -o TD4 TD4.v TD4_test.v
   ```

### シミュレーション実行
simleTD4はROM.binを命令メモリとして読み込み実行します。

   ```
$ ./TD4
   ```

[gtkwave](http://gtkwave.sourceforge.net/)でsimpleTD4の信号線の状態を調べることができます。./TD4実行後にgtkwaveでTD4.vcdを読み込んでください。

   ```
$ gtkwave TD4.vcd
   ```

### ROM.bin
simpleTD4ではROM.binを差し替えることで動作が変わります。

まずはあらかじめ用意してある3つの例、ROMLED.bin、ROMRAMEM.bin、ROMINOUT.binのファイル名をROM.binに変えてTD4を実行してみてください。

- [ROMLED.bin](https://github.com/asfdrwe/simpleTD4/blob/master/ROMLED.bin)
TD4のLED点滅プログラム(外部出力)です。  
0011, 0110, 1100, 1000, 1000, 1100, 0110, 0011, 0001の順に繰り返します。
- [ROMRAMEM.bin](https://github.com/asfdrwe/simpleTD4/blob/master/ROMRAMEM.bin)  
TD4のラーメンタイマープログラムです。  
0111のあと少し経つと0110になりそのあと0000と0100を繰り返し最後1000になります。
1サイクル1秒にすれば3分後1000になるはずです。
- [ROMINOUT.bin](https://github.com/asfdrwe/simpleTD4/blob/master/ROMINOUT.bin)  
TD4の入力と出力のテストプログラムです。
入力内容をBレジスタに入れBレジスタの内容を出力します。

自分でハンドアセンブルしてROM.binを作成することもできます。
ROM.binは1命令を1行8bitの2進数で記述します。\_は区切りで無視されます。 // 以下はコメントです。 以下の仕様と命令一覧を参考にしてください。

### TD4の仕様
TD4は1クロック1秒(1Hz)で動作し1クロックですべての処理を行うシングルサイクルのCPUです。

- 命令メモリ(16バイト)
- レジスタ(4bit)
    - Aレジスタ
    - Bレジスタ
    - OUTレジスタ
    - プログラムカウンタ(1サイクルごとに通常+1)
- フラグレジスタ
    - キャリーフラグ(1bit)
- 算術論理演算装置(ALU)
    - 4bit+4bitの加算演算のみ
- 入出力
    - 4bit入力ポート
    - 4bit出力ポート(OUTレジスタ接続)
- 命令フォーマット
    - 8bit固定長 上位4bitが命令・下位4bitが即値

### TD4の命令一覧
(A => Aレジスタ、B => Bレジスタ、OUT => OUTレジスタ(出力ポート)、PC => プログラムカウンタ、IN => 入力ポート、 IM => 即値)  

| 命令 | ニモニック | 意味 |
----|---------|----
| 0000 | ADD A, Im | A + 即値(Im)をAに代入 (A + Im => A) 		|
| 0001 | MOV A, B  | BをAに代入(B + 0000 => A) 			|
| 0010 | IN A      | 入力ポートの値をAに代入(IN + 0000 => A)	|
| 0011 | MOV A, Im | 即値(Im)の値をAに代入(Im + 0000 => A)		|
| 0100 | MOV B, A  | AをBに代入 (A + 0000 => B)			|
| 0101 | ADD B, Im | B + 即値(Im)をBに代入 (B + Im => B)		|
| 0110 | IN B      | 入力ポートの値をBに代入 (IN + 0000 => B)	|
| 0111 | MOV B, Im | 即値(Im)の値をBに代入 (Im + 0000 => B)	|
| 1001 | OUT B     | BをOUTレジスタに代入 (B + 0000 => OUT)	|
| 1011 | OUT Im    | 即値(im)をOUTレジスタに代入 (Im + 0000 => OUT)|
| 1110 | JNC Im    | キャリーフラグが立っていないならImにジャンプ (Im + 0000 => PC if carry)|
| 1111 | JMP Im    | Imにジャンプ (Im + 0000 => PC)		|

## FPGAの場合
[Sipeed Tang Nano] (https://tangnano.sipeed.com/en/)で動作確認しています。2021/4/24時点で[shigezone](https://www.shigezone.com/?product=tang-nano)で800円で買えます。

### ダウンロード
PCでシミュレーションする場合と同様にgitでファイルをダウンロードしてください。

```
$ git clone https://github.com/asfdrwe/simpleTD4.git
```

### Tang Nanoでの実行
[Sipeed Tang Nanoで遊んでみる (Linux版)] (https://qiita.com/ciniml/items/bb9723673c91d8374b63) や [SiPeed Tang Nanoの環境構築(Windows編)] (https://qiita.com/tomorrow56/items/7e3508ef43d3d11fefab) を参考に環境設定を行ってTang Nanoの動作確認をしてください。

#### nano\_simple

Tang Nanoのみで動作させる場合はnano\_simpleを使ってください。USB端子を右とした場合、上のボタンがリセット、TD4の出力ポートのうち下位3bitをTang Nano内蔵LEDの青、緑、赤に割り振り、TD4の入力ポートの最下位ビットが下のボタンです。それ以外は、出力ポートの最上位は38番(左下)、入力ポートの残りは下位から40番41番42番(左下の3番目から5番目)です。

Open Projectからnano\_simple/TD4_nano1.gprjを開き、Processから Synthesize => Place & Route => Program Deviceで書き込みしてください。Synthesizeできない場合はTD4.vを開いて適当に改行を入れて保存してからSynthesizeしてください。

Tang Nanoのピンアサインは [Tang Nano](https://tangnano.sipeed.com/en/) の[Pinout Diagram](https://tangnano.sipeed.com/assets/tang_nano_pinout_v1.0.0_w5676_h4000_large.png) を参照してください。

#### ROMLED.bin
<iframe width="560" height="315" src="https://www.youtube.com/embed/hQinmth7yE8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

#### nano\_breadboardやnano\_uart
ブレッドボードを組み合わせる場合はnano\_breadboard、UARTでPCに出力する場合はnano\_uartを使用してください。詳しくは[README_jp.md](
https://raw.githubusercontent.com/asfdrwe/simpleTD4/master/README_jp.md)や下のリンクからsimpleTD4の動作動画を参照してください。

https://www.youtube.com/channel/UCq8oCldgn23dA9urRdlULbw/featured


## simpleTD4の設計と実装
必要な知識は論理回路、組み合わせ回路、順序回路、Verilog HDL等です。以下のページなどを参考にしてください。

- 論理回路、組み合わせ回路、順序回路
	- [井澤氏の論理回路の解説1](http://www7b.biglobe.ne.jp/~yizawa/logic/index.html)
	- [井澤氏の論理回路の解説2](http://www7b.biglobe.ne.jp/~yizawa/logic2/index.html)
- Verilog
	- [リコンフィギュラブルコンピューティングシステム研究室のVerilog解説](http://www.darwin.esys.tsukuba.ac.jp/home/ohyou/verilog)


### ブロックダイアグラム
[やまもとみのる氏の電子工作作品集](http://xyama.sakura.ne.jp/hp/4bitCPU_TD4.html)を参考に次のブロックダイアグラムを作りました。このダイアグラムに従ってsimpleTD4を実装しています。

![TD4_block_diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/882900/1a3492f3-40f1-e101-ce6e-a0b6337b6424.png)

### 構成ユニット
- 命令メモリ
    - rom
- レジスタとフラグ
    - reg_a (Aレジスタ)
    - reg_b (Bレジスタ)
    - reg_out (OUTレジスタ)
    - pc (プログラムカウンタ)
    - cflag (キャリーフラグ)
- ALU (加算演算)
- セレクタ
    - alu_sel(ALU入力用selector)
    - next_pc(次のPC用)
- バス
    - レジスタ用バス(load_selで出力先選択)
- 入出力
    - in_port(入力用)
    - out_port(出力用)

### 各ステージの入力と出力
1サイクルの間に、命令読み出し(Fetch)・解読(Decode)・演算(Execute)・レジスタへの書き出し(Write Back)・次のPC書き出し(Next PC)の順に各ステージを実行しています。

- Fetch  
    - 入力: pc  
    - 出力: opcode  
- Decode  
    - 入力: opcode  
    - 出力: alu\_sel, load\_sel, jmp, im  
- Execute  
    - 入力: reg\_a, reg\_b, in\_port, 4'b0000, alu\_sel  
    - 出力: alu\_out, nextcflag  
- Write Back  
    - 入力: alu\_out, load\_sel  
    - 出力：reg\_a, reg\_b, reg\_out  
- Next PC  
    - 入力: load\_sel, jmp, cflag, pc, alu\_out
    - 出力: next\_pc

### コードの解説
[TD4のソース](https://github.com/asfdrwe/simpleTD4/blob/master/TD4.v)を行ごとに解説していきます。

1行目はmodule TD4の定義、2~3行目は著作権表示です。

```
module TD4 (input wire clock, input wire reset_n, input wire [3:0] in_port, output wire [3:0] pc_out, output wire [7:0] op, output wire [3:0] out_port, output wire [3:0] alu_data);
// Copyright (c) 2020 asfdrwe (asfdrwe@gmail.com)
// SPDX-License-Identifier: MIT
```

レジスタは4〜8行目で定義されています。

```
  reg [3:0] reg_a, reg_b, reg_out; 
  reg [3:0] pc = 4'b0;
  reg cflag = 1'b1;
  assign out_port = reg_out;
  assign pc_out = pc;
```

 命令メモリは10~11行目で定義されています。ROM.binを読み込んでromの内容とします。

```
  reg [7:0] rom[0:15];
  initial $readmemb("ROM.bin", rom);
```

 Fetchステージは13~15行目で、現在のpcのアドレスにある命令をromから取り出し、opcodeに出力します。

```
  wire [7:0] opcode;
  assign opcode = rom[pc];
  assign op = opcode;
```

 Decodeステージは17~23行目です。

即値(im)はopcode[3:0]を出力します。
ALUへの入力を選択するalu\_selはJNC命令やJMP命令のときは2'b11でそれ以外はopcode[5:4]に対応付けし、出力レジスタを選択するload\_selはopcode[7:6]に対応付けし、JMP命令かどうかを示すjmpはopcode[4]を出力します。

```
  wire [1:0] alu_sel, load_sel;
  wire jmp;
  wire [3:0] im; // IMMEDIATE 
  assign alu_sel = (opcode[7:6] == 2'b11) ? 2'b11 : opcode[5:4];
  assign load_sel = opcode[7:6];
  assign jmp = opcode[4];
  assign im = opcode[3:0];
```

Executeステージは25\~34行目です。
ALUへの入力は25\~29行目でalu\_selに基づきreg\_a(2'b00)かreg\_b(2'b01)かin\_port(2'b10)か0入力(2'b11)を決定し、33行目でalu\_selの
選択した値とimを加算し、alu\_outとnextcflagに出力します。

```
  wire [3:0] alu_in;
  assign alu_in = (alu_sel == 2'b00) ? reg_a : // from A
                  (alu_sel == 2'b01) ? reg_b : // from B
                  (alu_sel == 2'b10) ? in_port : // from input port
                                       4'b0000; // zero
  
  wire [3:0] alu_out;
  wire nextcflag;
  assign {nextcflag, alu_out} = alu_in + im;
  assign alu_data = alu_out;
```

 Write Backステージは36\~39行目でload\_selからデータバスの出力先を決定します。
53\~56行目でクロックごとにreg\_a、reg\_b、reg\_out、cflagを設定しています。

```
  wire load_a, load_b, load_out, load_pc;
  assign load_a = (load_sel == 2'b00) ? 1'b0 : 1'b1; // negative logic
  assign load_b = (load_sel == 2'b01) ? 1'b0 : 1'b1; // negative logic
  assign load_out = (load_sel == 2'b10) ? 1'b0 : 1'b1; // negative logic
```

 Next PCステージは40~43行目です。
load\_selからジャンプ命令であるか調べ、無条件ジャンプまたはキャリーフラグが立っていない(負論理)なら次のpcを選択するload\_pcを決め、43行目でジャンプ命令ならalu\_out、そうでないならpc+1を次のpc(next\_pc)にします。

```
  assign load_pc = (load_sel == 2'b11 && (jmp == 1'b1 || cflag)) ? 1'b0 : 1'b1; // negative logic

  wire [3:0] next_pc;
  assign next_pc = (load_pc == 1'b0) ? alu_out : pc + 1;

```

 45\~51行目でリセットが押されていればreg\_a、reg\_b、reg\_out、cflag、pcを
初期化しています。

```
  always @(posedge clock or negedge reset_n) begin
    if (!reset_n) begin
      reg_a <= 4'b0;
      reg_b <= 4'b0;
      reg_out <= 4'b0;
      cflag <= 1'b1;
      pc <= 4'b0;
```

最後に52\~58行目で次のクロックごとにreg\_a、reg\_b、reg\_out、cflag、pcの
値を設定しています。

```
    end else begin
      reg_a <= #1 (load_a == 1'b0) ? alu_out : reg_a;
      reg_b <= #1 (load_b == 1'b0) ? alu_out : reg_b;
      reg_out <= #1 (load_out == 1'b0) ? alu_out : reg_out;
      cflag  <= #1 ~nextcflag; // negative logic carry 
      pc <= #1 next_pc;
    end
```