🐇

ハードウェア記述言語で遊ぼう!

2025/01/01に公開

はじめに

こんにちはみみです。

この記事は UEC Advent Calendar 2024の 19 日目の記事です。

書いているのは 2025 年です。(遅れてすいませんでした)

昨日は、Kanaru さんの Minecraft で始める論理設計学 でした。今回は、これの内容を ハードウェア 記述言語の Veryl を使って実装してみる、という話です。

Veryl もといハードウェア記述言語について

ハードウェア記述言語とは

ハードウェア記述言語(HDL)とは、デジタル回路などのハードウェアの構成を記述するための言語です(そのまま)。

例えば、みなさんの馴染みのある AND ゲートや OR ゲートを以下のように記述することができるんです。

ANDゲート
module And (
  x: logic input,
  y: logic input,
  z: logic output
){
  always_comb{
    z = x & y;
  }
}
ORゲート
module Or (
  x: logic input,
  y: logic input,
  z: logic output
){
  always_comb{
    z = x | y;
  }
}

このように記述して作成したモジュールに入力を渡すと結果を得ることができます。
AND ゲートにxyを入力すれば論理積を取った結果が返ってくるし、OR ゲートにxyを入力すれば論理和を取った結果が返ってきます。

HDL を使って AND や OR、NOT を組み合わせて(実は NAND だけで論理完全系を成しているので十分だったりします) CPU を作ることもできます。素晴らしいですね。

神は「CPU よあれ」と言われた。すると NAND があった。

Veryl とは

さて、今回は Veryl という HDL を使っていきます。Veryl は SystemVerilog という HDL にトランスパイルされる言語で、いろいろめんどくさい SystemVerilog を書きやすくしているものです。

開発が 2022 年に始まっており、比較的新しい言語でまだまだユーザー数が少ないですが、今後流行ってほしいなと勝手に思っています。

先ほどの AND・OR ゲートのコードも Veryl です。

Veryl の導入方法はドキュメントを見てやってくださいね。(めんどくさいわけではない)

https://veryl-lang.org/install/

Verilator というシミュレーション用のパッケージも一緒に入れておきましょう。

3 の倍数判定を行う回路

早速実装していきましょう。
まず最初に、昨日の Kanaru さんの記事の「3 の倍数判定を行う回路」を実装していきます。

回路図などはありがたく使わせていただきます。

Veryl のプロジェクトを作成する

適当なディレクトリを作成し、その中に入って

veryl init

でプロジェクトが作成されます。srcディレクトリを作ってその中に各種ファイルを置いていきましょう。

今回は、3の倍数判定を行うので triple.veryl という感じで Veryl のソースファイルを置きました。

実装

では、早速実装していきます。

...実装したものがこちらです。

triple.veryl
module Triple (
    clk: input  clock,
    rst: input  reset,
    x  : input  logic,
    y  : output logic,
) {

    var q0: logic;
    var q1: logic;

    always_ff {
        if_reset {
            q0 = 0;
            q1 = 0;
        } else {
            q0 = (!q0 & !q1 & x) | (!q1 & !x);
            q1 = (q0 & !x) | (q1 & x);
        }
    }

    always_comb {
        y = (!q0 & !q1 & !x) | (q0 & x);
    }
}

簡単に説明すると、

module Triple (
  clk: input  clock,
  rst: input  reset,
  x  : input  logic,
  y  : output logic,
){
  ...
}

の部分で入力・出力を定義しています。
今回の場合は、クロック信号用のclkとリセット信号用のrst、入力であるxと結果を出力するyがこのTripleというモジュール(回路)についています。

var q0: logic;
var q1: logic;

の部分では、ここで使用するレジスタ(フリップフロップ・FF)を定義しています。

always_ff {
  ...
}

ではクロック信号に同期する形で、ノンブロッキング代入というものが行われます。ノンブロッキング代入では、同じタイミングで動く全ての代入文が評価された後、同時に変更されます。
この中でクロック信号に合わせてレジスタの値を変更しています。

一方、

always_comb {
  ...
}

の中では、ブロッキング代入が行われます。これは、右辺に含まれるレジスタや入力が変化したら逐次実行されるもので、組み合わせ回路と同じ振る舞いをします。

テストベンチの実装

次に、この回路をシュミレートするためにテストベンチを作成していきます。

src ディレクトリの中に、tb_verilator.cpp を作成し、以下のように実装します。

#include "Vtriple.h"  // Verilatorが生成するヘッダーファイル
#include "verilated.h" // Verilatorの基本ヘッダー
#include <iostream>
#include <string>

int main(int argc, char **argv) {
    Verilated::commandArgs(argc, argv); // Verilatorの初期化

    // DUT (Device Under Test) のインスタンス
    Vtriple* dut = new Vtriple;

    // 初期化
    dut->clk = 0;
    dut->rst = 1;
    dut->eval();

    // リセット解除
    dut->rst = 0;
    dut->eval();

    dut->rst = 1;

    // テスト入力
    std::string input = "111";
    std::string current_input = "";

    for (int i = 0; i < 3; i++) {
        dut->x = (int)input[i];
        dut->clk = 0;
        current_input = current_input + input[i];
        dut->eval();
        std::cout << "[INPUT = " << current_input << "] -> " << (int)dut->y << std::endl;
        dut->clk = 1;
        dut->eval();
    }

    // 終了処理
    delete dut;
    return 0;
}

今回は、111 を入力して 3 の倍数か判定してみたいと思います。
左から順に読んでいくので、クロックを 3 回まわします。

それぞれのクロックで、x に入力を与え、結果が y に格納されているのでそれを表示しています。

ビルド・シミュレータのビルド

さて、回路のビルドとシミュレータのビルドを行っていきます。

veryl fmt
veryl build

で Veryl で記述した回路のフォーマットとビルドを行います。

次に、

verilator -f triple.f --exe src/tb_verilator.cpp --Mdir obj_dir
make -C obj_dir -f Vtriple.mk
mv obj_dir/Vtriple obj_dir/sim

これで、実行可能なシミュレーションのファイルが生成されます。

いちいちこんなコマンドを打つのもめんどくさいと思うので、Makefile なんかを使ってまとめておくと良いでしょう。

私が今回使ったものを置いておきます。参考にどうぞ...

PROJECT = triple
FILELIST = $(PROJECT).f

TB_PROGRAM = src/tb_verilator.cpp
OBJ_DIR = obj_dir/
SIM_NAME = sim
VARILATOR_FLAGS = ""

v:
	veryl fmt
	veryl build

tb:
	verilator --cc $(VARILATOR_FLAGS) -f $(FILELIST) --exe $(TB_PROGRAM) --Mdir $(OBJ_DIR)
	make -C $(OBJ_DIR) -f V$(PROJECT).mk
	mv $(OBJ_DIR)/V$(PROJECT) $(OBJ_DIR)/$(SIM_NAME)

clean:
	veryl clean
	rm -rf $(OBJ_DIR)


.PHONY: v tb clean

実行

さて実行してみましょう。

obj_dir/sim

[INPUT = 1] -> 0
[INPUT = 11] -> 1
[INPUT = 111] -> 0

0b1 = 1、0b111=7 は 3 の倍数ではない一方、0b11 = 3 は紛れもなく 3 の倍数ですから、ちゃんと判定できていそうですね!

正規言語を受理する回路

正規表現E=1(01+10)^*を受理する回路を作っていきます。

これも、回路図をお借りして...

同じように実装して実行

module Regex (
    clk: input  clock,
    rst: input  reset,
    x  : input  logic,
    y  : output logic,
) {

    var q0: logic;
    var q1: logic;

    always_ff {
        if_reset {
            q0 = 0;
            q1 = 0;
        } else {
            q0 = ((q0 & x) | (q1 & x)) | ((q0 & q1) | (!q0 & !q1 & !x));
            q1 = q0 | !q1;
        }
    }

    always_comb {
        y = (q0 & !q1 & !x) | (!q0 & !q1 & x);
    }
}

論理式の部分を変えるだけですね、簡単です。

テストベンチを書いて実行してみます。
入力例として10110を入れてみると、

obj_dir/sim

[INPUT = 1] -> 1
[INPUT = 10] -> 0
[INPUT = 101] -> 1
[INPUT = 1011] -> 0
[INPUT = 10110] -> 1

正しく動いていますね!同様にすることで任意の正規言語を受け取る回路を作れそうです!

Cellular automaton Rule110 を実装したかった...

実装したかったんです...

3 変数 x, y, z(それぞれ、左上、上、右上の状態)を与えられたら次状態を返すモジュールはもちろん一瞬で作成できるんですが、
初期状態を与えてクロックしていれば勝手に次の状態に遷移していくようなものを作ろうとしたところで力つきました。

Veryl を書いたのが初めてで...という言い訳をさせてください。(これで悪戦苦闘している間に年を越していました。)

そのうち Rule110 を実装した記事を出します。(希望)

まとめ

さて、最後の方力つきてしまいましたが、Veryl で論理回路を実装してみる話、いかがでしたか?

技術書展で「Veryl で作る CPU」という本を買って Veryl を初めてみたところ、けっこう面白かったので記事にしてみました。

この本は Web でも読むことができるので、気になる人はぜひ覗いてみてください。

https://cpu.kanataso.net/

明日は、SHINN さんの記事です。何を書こうか迷っているらしいですが大丈夫でしょうか?
現在時刻は

date

Wed Jan  1 23:10:40 JST 2025

となっております。気長に待ちましょう(大遅刻した人が何を言う...)

ではまた。

Discussion