3
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-04-24

simpleTD4

TD4は『CPUの創りかた』で設計されたシンプルな4bitのCPUです。『CPUの創りかた』ではICで実装していますが、simpleTD4ではTD4をVerilogで60行程度の1ファイルで実装しました。

作ろう! CPU』という書籍ではSystemVerilogでTD4を実装していて、SystemVerilogの方が色々な点でVerilogより優れていると思いますが、2021/4/24時点ではsimpleTD4で使用しているIcarus VerlogのSystemVerilog対応が不十分なのでVerilogで実装しています。

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

simpleTD4の動かし方

PCでシミュレーションする場合

Icarus verlogでシミュレーションします。Linuxなら大抵のディストリビューションでバイナリが用意されていると思います。Windows版もあります。

ダウンロード

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

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

論理合成

Icarus verilogで論理合成します。

$ iverlog -o TD4 TD4.v TD4_test.v

シミュレーション実行

simleTD4はROM.binを命令メモリとして読み込み実行します。

$ ./TD4

gtkwaveで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 TD4のLED点滅プログラム(外部出力)です。
    0011, 0110, 1100, 1000, 1000, 1100, 0110, 0011, 0001の順に繰り返します。
  • ROMRAMEM.bin
    TD4のラーメンタイマープログラムです。
    0111のあと少し経つと0110になりそのあと0000と0100を繰り返し最後1000になります。 1サイクル1秒にすれば3分後1000になるはずです。
  • 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で動作確認しています。2021/4/24時点でshigezoneで800円で買えます。

ダウンロード

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

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

Tang Nanoでの実行

Sipeed Tang Nanoで遊んでみる (Linux版)SiPeed Tang Nanoの環境構築(Windows編) を参考に環境設定を行って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 NanoPinout Diagram を参照してください。

ROMLED.bin

nano_breadboardやnano_uart

ブレッドボードを組み合わせる場合はnano_breadboard、UARTでPCに出力する場合はnano_uartを使用してください。詳しくはREADME_jp.mdや下のリンクからsimpleTD4の動作動画を参照してください。

simpleTD4の設計と実装

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

ブロックダイアグラム

やまもとみのる氏の電子工作作品集を参考に次のブロックダイアグラムを作りました。このダイアグラムに従ってsimpleTD4を実装しています。

TD4_block_diagram.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のソースを行ごとに解説していきます。

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
3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1