Chiselとは?
Chiselとは、VHDLやVerilog等と同じく、ディジタル回路設計用のハードウェア記述言語(HDL)の一種です(ただし、ハードウェアの設計をアジャイルに行う事を主目的としているので、シミュレーション等の機能は弱い)。Chiselで書かれたコードはVerilogのコードに変換され、そのVerilogコードを使って、FPGAをプログラムしたり、ASICの製造に用います。
有名な用途としては、オープンソースのCPU ISAのRISC-Vの実装でもっとも有名なものの一つのRocket Chipや、GoogleのEdge TPUがあります(講演の動画はこちら)
Chiselの文法について知りたい方は、Chisel入門書「Digital Design with Chisel」1章の勉強記録から始まる記事を参照してください。
TD4とは?
TD4とは、CPUの創りかたにて、設計されている4bit CPUです。実用的なCPUではありません(ラーメンタイマーを作るのが精一杯…)ですが、CPUの基本的な構成要素は実装されているので、CPUの設計の学習には十分なものとなっています。
書籍では、手を動かして、ユニバーサル基板にICをハンダ付けしていきながら、電子工作自体も学習する事も目的としています。FPGAを使って実装するのは書籍の趣旨に反するのですが、HDLで簡単なCPUを実装するのはHDLの学習にも適しているので、Chiselで実装してみました。
本記事ではTD4自体の解説は行いません。詳細を知りたい場合はぜひ書籍を購入してみてください。
TD4の実装
書籍での解説順に実装していきます。
(最終の実装結果を見たい場合は、GitHubのリポジトリを参照してください)
リセットとクロック
Chiselでは、Moduleクラスを継承したクラスを定義していく事で、回路設計を行っていきます。Moduleクラスを継承したクラスはio(I/O)のメンバーを実装する必要があり、IOメソッドを呼び出して入出力を定義します。
package td4
import chisel3._
import chisel3.util._
import chisel3.core.withClockAndReset
/**
* TD4のトップモジュール
*/
class TD4Top extends Module {
val io = IO(new Bundle {
// ここに、モジュールへの入出力が定義される
})
}
また、Chiselのコードから、Verilogのコードを出力するために以下のオブジェクトを定義します。
/**
* TD4のVerilogファイルを生成するためのオブジェクト
*/
object TD4Top extends App {
chisel3.Driver.execute(args, () => new TD4Top)
}
リセットやクロックの定義は以下のようにします。
class TD4Top extends Module {
val io = IO(new Bundle {
val cpuReset = Input(Bool()) // CPU RESETボタンに接続
val isManualClock = Input(Bool()) // クロック信号をマニュアル操作するか
val manualClock = Input(Bool()) // マニュアル・クロック信号
val isHz10 = Input(Bool()) // 10Hzのクロックで動作するか? 偽の場合は1Hzで動作します。
})
// 1Hz, 10Hzのパルスを生成してクロック信号の代わりにする
val clockFrequency = 100000000 // 使用するFPGAボードの周波数(Hz)
val (clockCount, hz10Pulse) = Counter(true.B, clockFrequency / 10 / 2)
val hz10Clock = RegInit(true.B)
hz10Clock := Mux(hz10Pulse, ~hz10Clock, hz10Clock)
val (helz10Count, hz1Pulse) = Counter(hz10Pulse, 10)
val hz1Clock = RegInit(true.B)
hz1Clock := Mux(hz1Pulse, ~hz1Clock, hz1Clock)
// マニュアル・クロック用のボタンのチャタリングを除去
val manualClock = Debounce(io.manualClock, clockFrequency)
val td4Clock = Mux(io.isManualClock,
io.manualClock,
Mux(io.isHz10, hz10Clock, hz1Clock)).asClock
// CPU RESETボタンは負論理なので反転する。
withClockAndReset(td4Clock, ~io.cpuReset) {
// TODO: 実装する
}
}
/**
* プッシュボタン用デバンウンス
*/
class Debounce(hz: Int) extends Module {
val io = IO(new Bundle{
val in = Input(Bool())
val out = Output(Bool())
})
val (count, enable) = Counter(true.B, hz / 10) // 0.1秒間隔で値を取り込む
val reg0 = RegEnable(io.in, false.B, enable)
val reg1 = RegEnable(reg0, false.B, enable)
io.out := reg0 && !reg1 && enable // enableの時だけ変化を見るようにして、1クロックのパルスにする
}
/**
* プッシュボタン用デバンウンスのコンパニオン・オブジェクト
*/
object Debounce {
def apply(in: Bool, hz: Int): Bool = {
val debounce = Module(new Debounce(hz))
debounce.io.in := in
debounce.io.out
}
}
ROMを作る
書籍で解説されている内容で、実際に基板を使って作成するのが一番大変なのがROMですが、HDLで実装すると簡単です。
ここでは、プログラムの内容は決まっていないので、ROMの中身は全て0になっています。ROMは、しばらく使わないのでどこにも接続していません。
/**
* ROM
*/
class ROM extends Module {
val io = IO(new Bundle() {
val addr = Input(UInt(4.W)) // アドレス
val data = Output(UInt(8.W)) // データ
})
// ROMの中身
val rom = VecInit(List(
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
).map(_.asUInt(8.W)))
io.data := rom(io.addr)
}
演算できるようにする(NOT Aを実装)
書籍で最初に解説されている命令は、MOV A, Aであり、「迷ったらココに戻って来るコト」などと言われていますが、変化が全然なくて退屈なので、NOT Aから実装していきます。
CPUのコアの部分は独自のクラスに実装しています。
/**
* TD4 CPUコア
*/
class TD4 extends Module {
val io = IO(new Bundle() {
val out = Output(UInt(1.W))
})
// 1bitのAレジスタ(0で初期化)
val regA = RegInit(1.U(1.W))
// NOT A, A
regA := ~regA
// Aレジスタの内容を出力しておく
io.out := regA
}
class TD4Top extends Module {
val io = IO(new Bundle {
val cpuReset = Input(Bool()) // CPU RESETボタンに接続
val isManualClock = Input(Bool()) // クロック信号をマニュアル操作するか
val manualClock = Input(Bool()) // マニュアル・クロック信号
val isHz10 = Input(Bool()) // 10Hzのクロックで動作するか? 偽の場合は1Hzで動作します。
val out = Output(UInt(1.W)) // LEDへの出力
})
// 中略
withClockAndReset(td4Clock, ~io.cpuReset) {
val core = Module(new TD4())
io.out := core.io.out
}
}
FPGAボードの制約ファイルを作成すれば(ここでは、Digilent社のNexys4 DDRの制約ファイル)、Aレジスタの変化をLEDの点滅として見る事ができます。
## Clock signal
set_property -dict { PACKAGE_PIN E3 IOSTANDARD LVCMOS33 } [get_ports { clock }]; #IO_L12P_T1_MRCC_35 Sch=clk100mhz
create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports {clock}];
## Switches
set_property -dict { PACKAGE_PIN J15 IOSTANDARD LVCMOS33 } [get_ports { io_isManualClock }]; #IO_L24N_T3_RS0_15 Sch=sw[0]
set_property -dict { PACKAGE_PIN L16 IOSTANDARD LVCMOS33 } [get_ports { io_isHz10 }]; #IO_L3N_T0_DQS_EMCCLK_14 Sch=sw[1]
## LEDs
set_property -dict { PACKAGE_PIN H17 IOSTANDARD LVCMOS33 } [get_ports { io_out }]; #IO_L18P_T2_A24_15 Sch=led[0]
## Buttons
set_property -dict { PACKAGE_PIN C12 IOSTANDARD LVCMOS33 } [get_ports { io_cpuReset }]; #IO_L3P_T0_DQS_AD1P_15 Sch=cpu_resetn
set_property -dict { PACKAGE_PIN N17 IOSTANDARD LVCMOS33 } [get_ports { io_manualClock }]; #IO_L9P_T1_DQS_14 Sch=btnc
命令にしたがって処理を変更する(MOV A, AとNOT Aを切り替える)
CPUコアを以下のように変更します。
class TD4 extends Module {
val io = IO(new Bundle() {
val inst = Input(Bool()) // 命令(現在は、真ならMOV A, A、偽ならNOT A)
val out = Output(UInt(1.W)) // Aレジスタの内容
})
// 1bitのAレジスタ(0で初期化)
val regA = RegInit(1.U(1.W))
when (io.inst) {
regA := regA // MOV A, A
} .otherwise {
regA := ~regA // NOT A
}
// Aレジスタの内容を出力しておく
io.out := regA
}
TOPモジュールも以下のように変更します。(instをFPGAボードのスイッチに接続すれば、命令の切り替えを外部から入力出来ます)
class TD4Top extends Module {
val io = IO(new Bundle {
val cpuReset = Input(Bool()) // CPU RESETボタンに接続
val isManualClock = Input(Bool()) // クロック信号をマニュアル操作するか
val manualClock = Input(Bool()) // マニュアル・クロック信号
val isHz10 = Input(Bool()) // 10Hzのクロックで動作するか? 偽の場合は1Hzで動作します。
val inst = Input(Bool()) // 命令(現在は、真ならMOV A, A、偽ならNOT A)
val out = Output(UInt(1.W)) // LEDへの出力
})
// 中略
// CPU RESETボタンは負論理なので反転する。
withClockAndReset(td4Clock, ~io.cpuReset) {
val core = Module(new TD4())
core.io.inst := io.inst
io.out := core.io.out
}
}
レジスタの数を増やす(MOV A, B)
レジスタ間のデータ転送を実装します。
class TD4 extends Module {
val io = IO(new Bundle() {
val select = Input(UInt(2.W)) // レジスタ・セレクタ
val load = Input(Vec(4, Bool())) // 真の位置のレジスタに値をロードする
val out = Output(UInt(4.W)) // Aレジスタの内容
})
// 4bitのレジスタを4つ作成。レジスタの番号のビット位置のビットを立てる
val regs = RegInit(Vec(1.U(4.W), 2.U(4.W), 4.U(4.W), 8.U(4.W)))
// MOV X, X
val selectedVal = regs(io.select)
for (i <- 0 until regs.size) {
regs(i) := Mux(io.load(i), selectedVal, regs(i))
}
// Aレジスタの内容を出力しておく
io.out := regs(0)
}
class TD4Top extends Module {
val io = IO(new Bundle {
val cpuReset = Input(Bool()) // CPU RESETボタンに接続
val isManualClock = Input(Bool()) // クロック信号をマニュアル操作するか
val manualClock = Input(Bool()) // マニュアル・クロック信号
val isHz10 = Input(Bool()) // 10Hzのクロックで動作するか? 偽の場合は1Hzで動作します。
val select = Input(UInt(2.W)) // レジスタ・セレクタ
val load = Input(Vec(4, Bool())) // 真の位置のレジスタに値をロードする
val out = Output(UInt(4.W)) // LEDへの出力
})
// 中略
withClockAndReset(td4Clock, ~io.cpuReset) {
val core = Module(new TD4())
core.io.select := io.select
core.io.load := io.load
io.out := core.io.out
}
}
加算回路を追加(ADD X,Im)
class TD4 extends Module {
val io = IO(new Bundle() {
val select = Input(UInt(2.W)) // レジスタ・セレクタ
val load = Input(Vec(4, Bool())) // 真の位置のレジスタに値をロードする
val imData = Input(UInt(4.W)) // 即値データ
val out = Output(UInt(4.W)) // Aレジスタの内容
})
// 4bitのレジスタを4つ作成。レジスタの番号のビット位置のビットを立てる
val regs = RegInit(Vec(1.U(4.W), 2.U(4.W), 4.U(4.W), 8.U(4.W)))
val selectedVal = regs(io.select)
val addedVal = selectedVal + io.imData
for (i <- 0 until regs.size) {
regs(i) := Mux(io.load(i), addedVal, regs(i))
}
// Aレジスタの内容を出力しておく
io.out := regs(0)
}
class TD4Top extends Module {
val io = IO(new Bundle {
val cpuReset = Input(Bool()) // CPU RESETボタンに接続
val isManualClock = Input(Bool()) // クロック信号をマニュアル操作するか
val manualClock = Input(Bool()) // マニュアル・クロック信号
val isHz10 = Input(Bool()) // 10Hzのクロックで動作するか? 偽の場合は1Hzで動作します。
val select = Input(UInt(2.W)) // レジスタ・セレクタ
val load = Input(Vec(4, Bool())) // 真の位置のレジスタに値をロードする
val imData = Input(UInt(4.W)) // 即値データ
val out = Output(UInt(4.W)) // LEDへの出力
})
// 中略
withClockAndReset(td4Clock, ~io.cpuReset) {
val core = Module(new TD4())
core.io.select := io.select
core.io.load := io.load
core.io.imData := io.imData
io.out := core.io.out
}
}
MOV X,Im
以下のように変更します。
class TD4 extends Module {
val io = IO(new Bundle() {
val select = Input(UInt(2.W)) // レジスタ・セレクタ
val load = Input(Vec(4, Bool())) // 真の位置のレジスタに値をロードする
val imData = Input(UInt(4.W)) // 即値データ
val out = Output(UInt(4.W)) // Aレジスタの内容
})
// 4bitのレジスタを4つ作成。レジスタの番号のビット位置のビットを立てる
val regs = RegInit(Vec(1.U(4.W), 2.U(4.W), 4.U(4.W), 8.U(4.W)))
val selectedVal = MuxLookup(io.select, 0.U(4.W), Seq(
(0.U(4.W) -> regs(0)),
(1.U(4.W) -> regs(1)),
(2.U(4.W) -> regs(2)) // 3.Uの分はデフォルト値で代用
))
val addedVal = selectedVal + io.imData
for (i <- 0 until regs.size) {
regs(i) := Mux(io.load(i), addedVal, regs(i))
}
// Aレジスタの内容を出力しておく
io.out := regs(0)
}
キャリーフラグを実装
class TD4 extends Module {
val io = IO(new Bundle() {
val select = Input(UInt(2.W)) // レジスタ・セレクタ
val load = Input(Vec(4, Bool())) // 真の位置のレジスタに値をロードする
val imData = Input(UInt(4.W)) // 即値データ
val out = Output(UInt(4.W)) // Aレジスタの内容
})
// 4bitのレジスタを4つ作成。レジスタの番号のビット位置のビットを立てる
val regs = RegInit(Vec(1.U(4.W), 2.U(4.W), 4.U(4.W), 8.U(4.W)))
val carryFlag = RegInit(false.B)
val selectedVal = MuxLookup(io.select, 0.U(4.W), Seq(
(0.U(4.W) -> regs(0)),
(1.U(4.W) -> regs(1)),
(2.U(4.W) -> regs(2)) // 3.Uの分はデフォルト値で代用
))
val addedVal = selectedVal +& io.imData
carryFlag := addedVal(4)
for (i <- 0 until regs.size) {
regs(i) := Mux(io.load(i), addedVal(3, 0), regs(i))
}
// Aレジスタの内容を出力しておく
io.out := regs(0)
}
プログラム・カウンタを追加
プログラムカウンタを追加して、ROMから命令データを読み込んで行きます。ただし、命令を解釈する部分(デコーダ)がないので、意味のある処理はできません。
class TD4 extends Module {
val io = IO(new Bundle() {
val iAddr = Output(UInt(4.W)) // 命令アドレス
val iData = Input(UInt(8.W)) // 命令データ
val select = Input(UInt(2.W)) // レジスタ・セレクタ
val load = Input(Vec(4, Bool())) // 真の位置のレジスタに値をロードする
val out = Output(UInt(4.W)) // テスト用出力値
})
// 汎用レジスタ
val regA = RegInit(1.U(4.W)) // Aレジスタ
val regB = RegInit(2.U(4.W)) // Bレジスタ
val regC = RegInit(4.U(4.W)) // (Cレジスタ。最終的には別のものになる)
val programCounter = RegInit(0.U(4.W)) // プログラム・カウンター
val carryFlag = RegInit(false.B)
val selectedVal = MuxLookup(io.select, 0.U(4.W), Seq(
(0.U(4.W) -> regA),
(1.U(4.W) -> regB),
(2.U(4.W) -> regC) // 3.Uの分はデフォルト値で代用
))
val addedVal = selectedVal +& io.iData(3, 0)
carryFlag := addedVal(4)
when (io.load(0)) {
regA := addedVal(3, 0)
}
when (io.load(1)) {
regB := addedVal(3, 0)
}
when (io.load(2)) {
regC := addedVal(3, 0)
}
when (io.load(3)) {
programCounter := addedVal
} .otherwise {
programCounter := programCounter + 1.U
}
io.iAddr := programCounter
// 即値を出しておく
io.out := io.iData(3, 0)
}
class ROM extends Module {
val io = IO(new Bundle() {
val addr = Input(UInt(4.W)) // アドレス
val data = Output(UInt(8.W)) // データ
})
// ROMの中身
val rom = VecInit(List(
0x00, 0x01, 0x02, 0x03,
0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0A, 0x0B,
0x0C, 0x0D, 0x0E, 0x0F
).map(_.asUInt(8.W)))
io.data := rom(io.addr)
}
class TD4Top extends Module {
// 中略
withClockAndReset(td4Clock, ~io.cpuReset) {
val core = Module(new TD4())
core.io.select := io.select
core.io.load := io.load
io.out := core.io.out
val rom = Module(new ROM())
rom.io.addr := core.io.iAddr
core.io.iData := rom.io.data
}
}
I/Oポート
以下のように、入出力ポートを追加します。
class TD4 extends Module {
val io = IO(new Bundle() {
val iAddr = Output(UInt(4.W)) // 命令アドレス
val iData = Input(UInt(8.W)) // 命令データ
val select = Input(UInt(2.W)) // レジスタ・セレクタ
val load = Input(Vec(4, Bool())) // 真の位置のレジスタに値をロードする
val in = Input(UInt(4.W)) // 入力ポートへ
val out = Output(UInt(4.W)) // 出力ポートへ
})
// 汎用レジスタ
val regA = RegInit(1.U(4.W)) // Aレジスタ
val regB = RegInit(2.U(4.W)) // Bレジスタ
val regOut = RegInit(4.U(4.W)) // 出力ポート用レジスタ
val programCounter = RegInit(0.U(4.W)) // プログラム・カウンター
val carryFlag = RegInit(false.B)
val selectedVal = MuxLookup(io.select, 0.U(4.W), Seq(
(0.U(4.W) -> regA),
(1.U(4.W) -> regB),
(2.U(4.W) -> io.in) // 3.Uの分はデフォルト値で代用
))
val addedVal = selectedVal +& io.iData(3, 0)
carryFlag := addedVal(4)
when (io.load(0)) {
regA := addedVal(3, 0)
}
when (io.load(1)) {
regB := addedVal(3, 0)
}
when (io.load(2)) {
regOut := addedVal(3, 0)
}
when (io.load(3)) {
programCounter := addedVal
} .otherwise {
programCounter := programCounter + 1.U
}
// 出力
io.iAddr := programCounter
io.out := regOut
}
class TD4Top extends Module {
val io = IO(new Bundle {
val cpuReset = Input(Bool()) // CPU RESETボタンに接続
val isManualClock = Input(Bool()) // クロック信号をマニュアル操作するか
val manualClock = Input(Bool()) // マニュアル・クロック信号
val isHz10 = Input(Bool()) // 10Hzのクロックで動作するか? 偽の場合は1Hzで動作します。
val select = Input(UInt(2.W)) // レジスタ・セレクタ
val load = Input(Vec(4, Bool())) // 真の位置のレジスタに値をロードする
val in = Input(UInt(4.W)) // 入力ポート
val out = Output(UInt(4.W)) // 出力ポート
})
// 中略
withClockAndReset(td4Clock, ~io.cpuReset) {
val core = Module(new TD4())
core.io.select := io.select
core.io.load := io.load
core.io.in := io.in
io.out := core.io.out
val rom = Module(new ROM())
rom.io.addr := core.io.iAddr
core.io.iData := rom.io.data
}
}
命令デコーダを実装し、Lチカのプログラムを実行する
class TD4 extends Module {
val io = IO(new Bundle() {
val iAddr = Output(UInt(4.W)) // 命令アドレス
val iData = Input(UInt(8.W)) // 命令データ
val in = Input(UInt(4.W)) // 入力ポートへ
val out = Output(UInt(4.W)) // 出力ポートへ
})
/*
* レジスタ定義
*/
// 汎用レジスタ
val regA = RegInit(0.U(4.W)) // Aレジスタ
val regB = RegInit(0.U(4.W)) // Bレジスタ
// 出力ポート用レジスタ
val regOut = RegInit(0.U(4.W))
// プログラム・カウンタ
val programCounter = RegInit(0.U(4.W))
// キャリーフラグ
val carryFlag = RegInit(false.B)
/*
* 命令デコーダ
*/
val opData = Cat(io.iData(7, 4), carryFlag)
val ctrlSig = MuxCase(0.U(6.W), Seq(
// 本のデコーダの設計の最初の真理値表
((opData === BitPat("b0000?")) -> "b000111".U), // ADD A,Im
((opData === BitPat("b0001?")) -> "b010111".U), // MOV A,B
((opData === BitPat("b0010?")) -> "b100111".U), // IN A
((opData === BitPat("b0011?")) -> "b110111".U), // MOV A,Im
((opData === BitPat("b0100?")) -> "b001011".U), // MOV B,A
((opData === BitPat("b0101?")) -> "b011011".U), // ADD B,Im
((opData === BitPat("b0110?")) -> "b101011".U), // IN B
((opData === BitPat("b0111?")) -> "b111011".U), // MOV B,Im
((opData === BitPat("b1001?")) -> "b011101".U), // OUT B
((opData === BitPat("b1011?")) -> "b111101".U), // OUT Im
((opData === BitPat("b11100")) -> "b111110".U), // JNC(C=0)
((opData === BitPat("b11101")) -> "b111111".U), // JNC(C=1)"b??1111"
((opData === BitPat("b1111?")) -> "b111110".U) // JMP
))
val select = ctrlSig(5, 4)
// 本はloadの値は、負論理なので否定して、順序も逆なのでReverseする
val load = Reverse(~ctrlSig(3, 0))
/*
* レジスタ読み込み
*/
val selectedVal = MuxLookup(select, 0.U(4.W), Seq(
(0.U(4.W) -> regA),
(1.U(4.W) -> regB),
(2.U(4.W) -> io.in) // 3.Uの分はデフォルト値で代用
))
/*
* 演算処理(ALU)
*/
val addedVal = selectedVal +& io.iData(3, 0)
carryFlag := addedVal(4)
/*
* レジスタ書き戻し
*/
when (load(0)) {
regA := addedVal(3, 0)
}
when (load(1)) {
regB := addedVal(3, 0)
}
when (load(2)) {
regOut := addedVal(3, 0)
}
when (load(3)) {
programCounter := addedVal
} .otherwise {
programCounter := programCounter + 1.U
}
// 出力
io.iAddr := programCounter
io.out := regOut
}
class ROM extends Module {
val io = IO(new Bundle() {
val addr = Input(UInt(4.W)) // アドレス
val data = Output(UInt(8.W)) // データ
})
// ROMの中身
val rom = VecInit(List(
// Lチカプログラム
0xB3, 0xB6, 0xBC, 0xB8,
0xB8, 0xBC, 0xB6, 0xB3,
0xB1, 0xF0, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00
).map(_.asUInt(8.W)))
io.data := rom(io.addr)
}
class TD4Top extends Module {
val io = IO(new Bundle {
val cpuReset = Input(Bool()) // CPU RESETボタンに接続
val isManualClock = Input(Bool()) // クロック信号をマニュアル操作するか
val manualClock = Input(Bool()) // マニュアル・クロック信号
val isHz10 = Input(Bool()) // 10Hzのクロックで動作するか? 偽の場合は1Hzで動作します。
val in = Input(UInt(4.W)) // 入力ポート
val out = Output(UInt(4.W)) // 出力ポート
})
// 中略
withClockAndReset(td4Clock, ~io.cpuReset) {
val core = Module(new TD4())
core.io.in := io.in
io.out := core.io.out
val rom = Module(new ROM())
rom.io.addr := core.io.iAddr
core.io.iData := rom.io.data
}
}
全体としてのコードを見たい場合は、GitHubのリポジトリを参照してください