CPU+コンパイラ自作キャンプの手引き
「CPU+コンパイラ自作キャンプ」は数ヶ月の事前準備+数日の泊まり込みを想定したイベント
本ページは自作キャンプの全行程を案内することを目的とする
2021年12月頃からCPUを作り始めた
それまではFPGAやVerilogは使ったことが無かった
そこから現在(2024年末)までにFPGAでCPU(およびマイコン)を作ってきた経験からこの資料を書いている
事前準備
Verilogの練習
6ビットのLEDを好きなビットパターンで光らせる
assign led = 6'b111001;
Tang Nano 9KのオンボードLEDは、0で点灯、1で消灯
1秒程度の間隔で点滅させる
カウンタを作る logic [23:0] counter;
クロックの立ち上がりでカウンタをインクリメントし、13.5M(0.5秒)に達したら0に戻す
カウンタが一周したら led のビットを反転させる
脱線ネタ:PWMを使って滑らかに明るさを変えて明滅させる ボタンを押すたびに1ビットずつ乱数を生成する
logic [15:0] lfsr;
6ビットのLEDを1ビットずらし、空いた1ビットに乱数を入れる
10ms程度の間隔でボタンを読み取る方法は簡単で効果が高い
基本的にクロック同期回路としてすべてを設計するのが簡単なので、まずは素直にその方針にすることをおすすめする
基本方針:assignは信号線に使い、alwaysはレジスタに使う
code:alwaysのひな形.sv
always @(posedge sys_clk, negedge rst_n) begin
if (~rst_n)
信号 <= 初期値;
else
信号 <= 意味のある値;
end
レジスタへの信号代入は、すべて上記のひな形を守る
当然だが、else ではなく else if を使うといった応用は必要に応じてやる
守ること:
alwaysのセンシティビティリストはクロックとリセットを指定
他の信号は入れない
alwaysの2行目は必ずリセット信号を判定するif文とする
Standard Mode
For Post-Synthesis Netlist
LFSR回路のボタン押下をトリガとして、lfsrレジスタの内容やLEDの値を取得してみる
受信より送信の方が若干簡単なため、先に送信をやってみる
Tang Nano 9KのオンボードUSB-UARTを使う
FPGA_TX:PIN17
FPGA_RX:PIN18
通信速度(ボーレート)を parameter BAUD = 9600; のようにパラメタ化
1ビット当たりの待ち時間(クロック数)は parameter BIT_PERIOD = 27000000 / BAUD; と計算可能
UART受信回路を書き、受信した8ビット(の下位6ビット)をLEDに表示させる 受信した文字をエコーバックする回路を書く(PCへ送り返す)
単にエコーバックする
脱線ネタ
何らかの変換をしてエコーバックする
a~zをA~Zに変換して送り返す
文字コードを1だけずらして送り返す(Aを送ったらBを返す)
UARTで送りつけた2つの8ビット値を加算する仕組みを作る
例:0A 05 を送ると 0F が返ってくる
出力先はUART
ヒント
今どちらのバイトを待っているかを管理するフラグ logic recv_phase; を作る
recv_phase が0なら1バイト目、1なら2バイト目を待っているということにする
2バイト目を受信し終えたら加算結果の送信を始める
加算はVerilogの + 演算子を使って良い
任意のバイナリを送る方法
普通のターミナルエミュレータは文字列の送受信に特化
「1」を送ると0x01ではなく0x31(ASCIIコード)が送られる
脱線ネタ
加算器をゲートロジックで組んでみる。例えば…
XORで半加算器を作り
2個の半加算器で全加算器を作り
7個の全加算器と1個の半加算器で8ビットの加算器を作る
加算と減算を選択できるようにする
「整数」「整数」「演算命令」という3バイトを続けて送る
演算命令の具体的なビット列は読者に任せる
「演算命令」を受信したら、すでに受信した2つの整数を用いて計算し、結果を出力する
どのバイトを受信したかを認識する方法のメリット・デメリットを考える
方法1:recv_phaseのビット幅を拡張し、0~2の状態を作り、それぞれのバイトに対応させる。
方法2:最上位ビットで整数と演算命令を区別できるようにする。例えば最上位ビットが1=整数、0=命令、とする。
減算の順序
例えば 1 2 - という3バイトはどういう計算をすべきか。1-2=-1か、2-1=1か。
どっちでも良いが、このテキストでは日本語の順序で考えることにする。
1 2 -は「1から2を引く」→1-2=-1
1 2 +は「1と2を足す」→1+2=3
コラム:減算と2の補数
Verilogで - 演算子を使えば減算ができるが…
ゲートロジックで作った加算器を使って減算するにはどうすれば良いだろうか?
減算 A-B は加算 A+(-B) と等価だから、整数を符号反転する方法があればゲートロジックで作った加算器で加算できるはず
2の補数表現 -B = ~B + 1
Bをビット反転したものに1を足す
例1:B = 1 = 0b00000001とすると~B = 0b11111110。-B = ~B+1 = 0b11111111 = 0xff
例2:B = 127 = 0b01111111とすると~B = 0b10000000。-B = ~B+1 = 0b10000001 = 0x81
例3:B = 126 = 0b01111110とすると~B = 0b10000001。-B = ~B+1 = 0b10000010 = 0x82
減算の計算例
1-127 = 1+(-127) = 0b00000001 + 0b10000001 = 0b10000010 = 0x82 = -126
加算器にとっては0x01と0x81を加算して0x82を出力しただけだが、減算になっている!
演算結果の出力先としてUARTかLEDを指定する仕組みを作る
「整数」「整数」「演算命令」「書き込み命令」という4バイトを続けて送る
書き込み命令の8ビットのうち、どこかのビットをUART/LEDの選択ビットとする
最下位ビットを選択ビットとした例:0x04はUARTへの書き込み、0x05はLEDへの書き込み
複雑な命令順に対応する
1 4 + 3 - LED という6バイトが来たらLEDに2を出力する
1, 4, 3 は整数
+ は加算命令
- は減算命令
LED はLEDへの書き込み命令
日本語として読むと「1と4を足し、3を引き、LEDへ出力する」
プログラムをBRAMに貯めて一気に実行するようにする
命令用のBRAMを導入
BRAMに命令列をため、命令列を受信し終えたら一気に実行する
BRAMの作り方は大きく2通り
レジスタ配列をBRAMとして推論させる
BRAMプリミティブを明示的にインスタンス化する
最終的にはBRAMプリミティブがインスタンス化されるのは同じ。推論でやるか、明示的にやるかの違い。
レジスタ配列を推論させるのをお勧めする
その方が信号のタイミングがとっても分かりやすくなる(筆者の経験)
logic [7:0] pmem[0:1023]; で1KiBのRAMを作れる
pmemはProgram Memoryの略のつもり
「命令列の受信完了」を知るための仕組みを考える
例えばこんなやり方がある
A. 命令列の後に送信完了マークを送る
B. 命令列を送る前に命令数を送る
C. 一定時間受信がなければそこで終わったと見なす
何度も加算と減算を繰り返せるようにする
1~10の総和を求めてみる
1 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + = という命令列を送る
=は出力命令(UART/LEDはどちらでも良い)
最初だけ整数が2つ連続し、その次からは「+ 整数」が繰り返される
加減算のコンパイラを作る
一般的にはコンパイラとアセンブラ(とリンカ)を別々に作ることが多い
コンパイラがアセンブリ言語を出力し、アセンブラが機械語を出力する
でも、ここで作るコンパイラは、機械語(16進数の文字列)を直接生成するので十分だ
乗算をサポートする
演算子の優先順位を意識する
1 + 2 * 3 は (1 + 2) * 3 ではなく 1 + (2 * 3) である
乗算の優先順位を保つ仕組みを考える
途中状態を保存しておく必要性
一般の電卓は前から順に計算してしまう(1+2*3と入力すると結果が9になる)
スタック型か、レジスタ型か
スタックの枯渇に対するケア(枯渇したらどうなるか)
デバッガ(GDBなど)を使ってコンパイラの処理を追いかける
ここまでが事前準備の目安
特別な変数 _SW2 を用意
LEDの表示を変数化
特別な変数 _LED を用意
複文に対応
send文に対応
C言語にはないが、UARTにデータを送る専用の構文として send <expr>; を導入する
変数の読み書きに対応
データ用BRAM
ノイマン型かハーバード型か
命令長の自由度
セキュリティ
動的プログラム生成
暗黙に定義される1文字変数
ベースボードが手に入ってからやる
自動テストを導入
外付けUSB-UART変換モジュールが欲しいのでベースボード入手後にやる
何をテストしているか・していないかを理解する
if文に対応
CPUにどの命令を追加するか、しないか
LTに加えてGTは必要か?
関連話題
NEG命令は必要か?
複合代入文
フラグレジスタとスタック
関係演算子に対応
論理演算子に対応
while文に対応
タイマーによる時間待ちを可能にする
プログラムからLEDを点滅させる
インクリメント演算子に対応
for文に対応
関数定義に対応
変数の定義に対応
LCDに対応する
キーパッド入力に対応する
プログラム上からのUART送受信に対応する
2台の自作CPUでチャットする
USB-UART変換モジュールを取り外せるようにしておく