はじめに
Build an 8-bit computer from scratch
https://eater.net/8bit
の動画をみながら、専用のキット を組み立てて CPU の勉強をしました。
動機
ソフトウェアのプログラミングのお仕事を25年くらいしています。直近15年くらいはいわゆる 組み込み系 が中心だったのですが、組み込み系の中ではかなり高レイヤーに位置する ユーザーインターフェース が専門で、OS とかコンパイラとかハードウェアは僕に取っては 動いていて当然のもの でした。
少し前に 基礎から学ぶ-組込みRust を読んで、Rust やマイコン自体やマイコンでのプログラミングに触れたことで、(僕からみた)低レイヤーに興味を持ち、色々勉強をはじめ、簡単な回路を設計し、カスタム基板を作成し、 朝の待ち合わせ用の簡易的な時計 を自分で作れるようになったりしていました。
その流れで、会社の同僚が紹介してくれたのが前述の Ben Eater の「8-bit computer」で、最初は YouTube の動画をひたすら見ていたのですが、見れば見るほど作りたくなってしまい、せっかくなので公式のストアからキットを買い(たった数日で届きました!)、自分で組み立てることにしました。
得たもの
- CPU を構成する基礎的な電子部品や電子回路の知識
- ロジックICや様々なIDの知識
- 基本的な CPU の設計や構成
- 簡単なアセンブラの知識
クロックモジュールの作成 - Clock module
555 タイマーでクロック信号を作る
555タイマー というタイマーICの 無安定モード を利用して、CPU のクロック信号を作っていきます。
555タイマーの内部構造の解説を聞きながら、無安定モードの回路をブレッドボード上に作成し、LED を点滅させることができました。
シングルクロックボタンを作る
ボタンの チャタリング 対策として、なんとまた555タイマーが登場します。
今回はボタンが押されたときに555タイマーの 単安定モード が機能するように回路を組み立てました。
555でデバウンス対策のやつ pic.twitter.com/d0CgcKCcNF
— Tasuku Suzuki (@task_jp) April 30, 2024
トグルボタンを作る
プッシュ式もしくはスライド式のトグルスイッチを利用して、上記2つの信号を選択できるようにします。
今回も、このスイッチのチャタリング対策として555タイマーを利用します。
3回目は 両安定モード で、シンプルに555タイマーの中の SRラッチ を利用する形になります。
トグルも555(のRSラッチ)で pic.twitter.com/V6fFymz42e
— Tasuku Suzuki (@task_jp) April 30, 2024
切り替え機能を作る
トグルボタンの出力と(反転したものを)自動、手動の出力と AND し、両者を OR して1つの出力にまとめます。将来のために、コンピューターを停止する HLT という入力も追加しておきます
元の動画はクロック信号のところにLEDをつないでいますが、それによってクロックの電圧が下がって困る場合には、以下のように余ってるInverterを2回経由するといいかもしれません。
Kit1 のクロック回路完成!
— Tasuku Suzuki (@task_jp) April 30, 2024
とても綺麗に配線できたと思う。 pic.twitter.com/7iCkS1euYI
電子部品の基礎知識 - Background
本格的にブレッドボードで CPU を作りはじめる前に、基本的な部品のおさらいをしました。
半導体とは
共有結合のシリコン結晶にリン(P)を混ぜたりホウ素(B)を混ぜたりして結晶中を価電子が動くようになって、両者を組み合わせるとダイオードが完成します。
トランジスタとは
NPN の形で半導体を作ると、トランジスタになります。
ロジックゲート
トランジスタとスイッチを組み合わせて、以下の回路を作ってみました。
トランジスタを使ったバッファ回路 pic.twitter.com/3pXmDImzsm
— Tasuku Suzuki (@task_jp) April 30, 2024
トランジスタを使ったインバーター回路 pic.twitter.com/skkd2nzZ1u
— Tasuku Suzuki (@task_jp) May 1, 2024
トランジスタを使った AND 回路 pic.twitter.com/L1zMhnoeE5
— Tasuku Suzuki (@task_jp) May 1, 2024
トランジスタを使った OR 回路 pic.twitter.com/Yn3zJmoVZ6
— Tasuku Suzuki (@task_jp) May 1, 2024
トランジスタを使った XOR 回路 pic.twitter.com/9kGFfZj0dE
— Tasuku Suzuki (@task_jp) May 1, 2024
SR ラッチ
トランジスタでロジックゲートを作ったので、モジュール化されたロジックICで次のステップに進みます。
NORのロジックICを使った SRラッチ 回路 pic.twitter.com/3Vi24kbhqy
— Tasuku Suzuki (@task_jp) May 4, 2024
D ラッチ
Set/Reset を、Data/Enable にしてみました。
AND と NOR のロジック IC を使った D ラッチ回路できた pic.twitter.com/7Xd60ryxnQ
— Tasuku Suzuki (@task_jp) May 7, 2024
D フリップフロップ
Enable のところを RC回路 にして、クロックの立ち上がりの一瞬だけデータを取り込むようにしました。
ブレッドボードで D flip-flop を作った。 pic.twitter.com/xz0QFpdPvI
— Tasuku Suzuki (@task_jp) May 7, 2024
レジスター - Registers
バスアーキテクチャ
バスと呼ばれる8本の線を複数のモジュールに接続し、モジュール間でデータの共有を行うアーキテクチャで CPU を作ることを学びました。
1つのモジュールがバスにデータを共有し、他のモジュールがそこからデータを読むのですが、ちょっとタイミング関係が難しそうです。
トライステートロジック
High/Low の他に、接続していないという状態を持てることで、必要のないときはバスには関わらないようにできることを学びました。
1bit レジスター
D フリップフロップ(74LS74)を利用して1ビットを任意のタイミングでクロックに合わせて保持するレジスター回路の作り方を学びました。
74LS74 他のICがキットに含まれていなかったため、ここの実践はスキップしました。
8bit レジスター
74LS173 という4bitのDレジスターICを2個利用して、8bitのレジスターを作成しました。74LS173には出力にトライステートの機能が用意されていますが、レジスターの中身をLEDで可視化するためにそれは利用せず、別途 74LS245 を使うことにしました。
レジスターの動作確認
レジスターモジュールを複数個組み立て、バスを介して8bitデータのやりとりができるようになりました。
(左側の)8ビットのバスにハードコードしたデータを、クロックに合わせて(赤の)Aレジスタに保存して、Aレジスタの値をバスに共有して、それを(黄色の)Bレジスタに保存するコード(マジで「コード」)ができたああああ。たくさんのことにハマって、丸2日かかったw 電気難しい。 pic.twitter.com/Y3KWaaHyKA
— Tasuku Suzuki (@task_jp) May 8, 2024
さらに(インストラクション)レジスタを追加。バス経由でデータの読み書きが自由にできてる。嬉しい。 pic.twitter.com/PFxwIbNbO0
— Tasuku Suzuki (@task_jp) May 8, 2024
演算装置 - ALU
2進数の足し算をロジックゲートで行う方法
XOR で 1bit の足し算ができて、繰り上がりは AND で計算できる(半加算器)。
そこに XOR, AND, OR を追加すると、下からの繰り上がりを考慮した計算もできる(全加算器)。
負の数の表現
最上位bitを±として使ってみて、1の補数を試して、2の補数にたどりつきます。
演算装置の設計
ALU にAレジスターとBレジスターを直接つないで、加算を行う設計にします。
直前に足し算の回路を自作したので、ここでは4bitの足し算を行う 74LS283 を採用し、2個で8bitの計算ができるようにしました。
減算が少々難しいのですが、
- 2の補数を生成する
- 加算する
という仕組みで解決します。
2の補数は、ビット反転をして1を足すので、まず XOR でビット反転に対応します。
演算フラグ | 入力 | XOR 出力 |
---|---|---|
足し算(0) | 0 | 0(そのまま) |
足し算(0) | 1 | 1(そのまま) |
引き算(1) | 0 | 1(反転) |
引き算(1) | 1 | 0(反転) |
1を足すのは、全加算器のキャリー入力を有効にすることで対応します。
とてもスマートに解決することができました。
演算装置の実装
上記を頑張って組み立てます。
ALU を作って、足し算ができるようになった! pic.twitter.com/U3tpuFaslF
— Tasuku Suzuki (@task_jp) May 8, 2024
演算装置の修正
1つ前の実装にバグがあったようなので、原因を調査して修正をしました。
低レイヤーのデバッグは難易度が高いですね。
次のステップで、実際に自分の回路にもバグが見つかり同じように修正をすることになりました。
演算装置のテスト
A = A + B
の計算を演算装置で繰り返すことでテストをしました。
引き算もできてる気がする pic.twitter.com/baeIP8Ip66
— Tasuku Suzuki (@task_jp) May 8, 2024
バグを発見した pic.twitter.com/dPsKodHsdy
— Tasuku Suzuki (@task_jp) May 9, 2024
直した pic.twitter.com/B77esCm3AQ
— Tasuku Suzuki (@task_jp) May 9, 2024
RAM - Random access memory (RAM) module
RAM の概要
8bit の読み書きができる回路の配列で、インデックスをアドレスと呼び、2進数のアドレスから特定のアドレスを表す信号に変換して特定の場所の記憶装置を有効化しているようです。
レジスタをたくさん並べてもいいのですが、様々な理由で、トランジスタとコンデンサを組み合わせたもので記憶をし、情報が消えないように定期的にコンデンサの充電をする DRAM という方式が一般的なようです。
メモリを自作するのは今回は避けて、 74189 という、4bit × 16 = 64bit のメモリICを採用します。
8bit 必要なので、2個使います。
全体で16バイトのRAMになります。
RAM の実装
74189 の出力が反転されているので、74LS04 を2個使って信号を元に戻します。
また、バスとの入出力にまたトライステートバッファ(74LS245)を使います。
RAMを作った。データ書き込む時の挙動(全部光る)が微妙にお手本と違うんだけどいいのかな。 pic.twitter.com/uxfkC2WerJ
— Tasuku Suzuki (@task_jp) May 9, 2024
RAM のアドレスレジスタ
バスからメモリのアドレス(4bit)を取得するための仕組みをAレジスタと同じようにDレジスタIC(74LS173)を使って実装します。
メモリアドレスはバスへの出力はしないため、トライステートバッファは必要ありません。
それから、RAM は手動でも操作するので(プログラミングモード)、DIPスイッチでも操作できるようにします。
切り替えは、74LS157というICで行うことにしました。
RAMのアドレス側の対応をした。 pic.twitter.com/DPhAl3vw0i
— Tasuku Suzuki (@task_jp) May 9, 2024
RAM のデータ操作
メモリのアドレスと同じスイッチで手動での書き込みに対応します。
回路もほぼ同じですが、コンピューターの動作時の書き込みはクロック信号と同期して動作する必要があるので NAND を利用して一工夫しています。
RAM のデータ側の対応をした。 pic.twitter.com/5ebdutaBw1
— Tasuku Suzuki (@task_jp) May 9, 2024
RAM の動作確認
RAM の出力を動画と同じように下位3bitでしか行ってなかったのですが、5bit目と6bit目が入れ替わってしまっていて、私も苦労して原因を特定して修正しました。
また、動画の最後で、メモリの書き込み用の信号を RC 回路に変更してクロックの立ち上がりのエッジのタイミングで書き込むように変更しています。
プログラムカウンタ - Program counter
JK flip-flop
SRラッチを拡張して、セット/リセットが両方1のときに対応した回路を作成しました。
レースコンディションの調査
しかし、意図したようには動作しませんでした。
Master-slave JK flip-flop
SRラッチを2段にして、実用的なフリップフロップを作りました。
富豪的な解決方法ですね。
そして、一旦自分で作ったら、モジュール化されたIC(74LS76)を使うことにするようです。
バイナリカウンター
74LS76 を利用すると、クロック信号の倍の周期で切り替わる信号が作れます。
それを並べると、2進数のカウンターになります。
プログラムカウンターの設計
ジャンプやカウントアップの切り替え機能も実装されているモジュール化されたバイナリカウンターIC(74LS161)を採用します。
バスとのやりとりはここでも 74LS245 を利用します。
プログラムカウンターの実装
プログラムカウンターを作った。
— Tasuku Suzuki (@task_jp) May 10, 2024
LEDに抵抗入れてないせいか最後のとこがちと怪しい。 pic.twitter.com/D1GtJxl9sH
出力レジスタ - Output register
7セグデコーダー
ロジック回路で0〜Fを7セグの信号に変更してみました。無理。。。
EEPROM での実装
AT28C16 という 2K x 8bits の EEPROM を利用して、00~0F に、対応するセグメントのデータを入れます。
EEPROM の動作を学ぶために、データを読み書きする回路を作ってみます。
EEPROM の手動読み書き回路を作った pic.twitter.com/IcaC2BuVv8
— Tasuku Suzuki (@task_jp) May 11, 2024
↑の配線をスッキリさせた pic.twitter.com/Hds9wmr9Ih
— Tasuku Suzuki (@task_jp) May 11, 2024
自分で工夫をして、書き込みデータも DIP スイッチでできて、7セグ表示にも対応してみました。
Arduino Nano で EEPROM に書き込みをする
Arduino Nano 33 BLE ヘッダー付き もしくは互換があるものを利用して、PCでプログラミングしたデータを簡単に EEPROM にかけるようにします。
IOピンの数の関係で、11bit のアドレスをそのままは扱わず、8bitのシフトレジスター(74HC595)を2個利用しています。
Arduino で EEPROM を読み書きする回路を作った pic.twitter.com/MDMdeIhC37
— Tasuku Suzuki (@task_jp) May 11, 2024
3桁の7セグ表示に対応する
555タイマーと、JK flip-flop のバイナリカウンタと 74LS139 デコーダーの組み合わせで4桁をそれぞれ高速に表示し、人間の目には残像で4桁とも常に表示されているように見えるという魔法が登場しました。
8bitのデータを数値で表示できるようになった pic.twitter.com/ozLOn4SNBA
— Tasuku Suzuki (@task_jp) May 11, 2024
バスで各モジュールを結合する - Bringing it all together
出力レジスタがバスからデータを読むために 74LS273 を採用しました。
また、取り込みのコントロールラインとクロックを AND したものを取り込み信号として利用しています。
お手本はバスの8本を中央に並べる形になっていますが、少し工夫をして、CPU 全体で利用する以下のものも並べるようにしました。
- クロック
- ↑を反転したもの
- リセット
- ↑を反転したもの
全体を合体してる途中。ワクワクしてきた pic.twitter.com/16LBRhDx2x
— Tasuku Suzuki (@task_jp) May 12, 2024
x -= -1 をし続けるー pic.twitter.com/KuqXnpsCkP
— Tasuku Suzuki (@task_jp) May 12, 2024
コントロールシグナルの概要
各モジュールに存在するコントロール線(I/O など)を一ヶ所に集めて操作しやすくします。
全体をバスで繋いだ pic.twitter.com/axhIlDFjDc
— Tasuku Suzuki (@task_jp) May 12, 2024
CPU の制御回路 - CPU control logic
命令セット
8bit のうち、上位4bitを命令コード、下位4bitを引数として命令体系を構築します。
アドレス | 命令/データ | バイナリデータ |
---|---|---|
0000 | LDA 14 | 0001 1110 |
0001 | ADD 15 | 0010 1111 |
0002 | OUT | 1110 0000 |
... | ||
1110 | 28 | 00011100 |
1111 | 14 | 00001110 |
インストラクションカウンター
各CPU命令のマイクロインストラクションを逐次実行するためのカウンターを作成します。
バイナリカウンタ(74LS161)を利用し、クロックをカウントアップするのですが、処理のタイミングを考慮し、クロックを反転してソースとして利用します。
ついでにデコーダー(74LS138)で、各ステップを信号として独立して扱えるようにします。
カウンタ自体は16まで数えられますが、マイクロインストラクションの最大は5なので、
0から4まで数えたらカウンタICのリセットに信号を送るようにします。
インストラクションのカウンターを作った pic.twitter.com/3Q86G1ILkF
— Tasuku Suzuki (@task_jp) May 13, 2024
インストラクションの実装
EEPROM を利用して、インストラクションカウンターと命令コードをアドレスにとり、コントロール信号をデータとして出力するようにします。
コントロールの信号が15個あるため、EEPROM を2つ利用します。
データピンを個々のコントロール信号線とつないで、自動でマイクロインストラクションが実行されるようにします。
EEPROM の中身は手動で書き込みました。
コントロールのロジックをEEPROMで実装して、RAM上のプログラムを実行できるようになった! pic.twitter.com/BhtgkJtXbr
— Tasuku Suzuki (@task_jp) May 14, 2024
リセット回路
リセットボタンを作成し、反転した信号も作成し、各モジュールのリセット機能と接続しました。
インストラクションカウンターのリセット信号とも連携して動作するようになっています。
リセットボタンを作って、回路全体をリセットできるようにした pic.twitter.com/4C7Zf11lbU
— Tasuku Suzuki (@task_jp) May 13, 2024
後半は全体の電源の話をしていました。
Ben Eater おすすめのブレッドボード BB830 は、mouser から購入可能 です。でも透明のが届くかも。
Arduino で EEPROM を書き込む
プログラミングでマイクロコードが管理できるようになり、インストラクションを簡単に増やせるようになりました。
足し算を一回してクロックを止めて、リセットボタンで初めからやり直せるようになった! pic.twitter.com/uWG1OVUDe3
— Tasuku Suzuki (@task_jp) May 14, 2024
CPU 命令の追加
以下の命令を追加しました
- SUB
- STA
- LDI
- JMP
ジャンプ命令に対応して、永遠に x+=3 し続けられるようになったよ! pic.twitter.com/p2vx2ODEDZ
— Tasuku Suzuki (@task_jp) May 15, 2024
チューリング完全にしよう
コンピューターとは、についてのお勉強をしました。
CPU のフラグを管理するレジスタ
Carry フラグと Zero フラグを Dレジスタに保持するようにし、保持用のコントロール信号も追加しました。またマイクロインストラクションで利用できるよう、EEPROM にフラグを接続しました。
条件ジャンプ
フラグを考慮した形でマイクロインストラクションを書き換え、JC, JZ が利用できるようになりました!
JC JZ を実装して、255までカウントアップして、それから0までカウントアップするプログラムが動くようになった! pic.twitter.com/WKfIyVWDyJ
— Tasuku Suzuki (@task_jp) May 15, 2024
掛け算のプログラムも動くようになった!
— Tasuku Suzuki (@task_jp) May 15, 2024
8x9=72 あってる!! pic.twitter.com/zXl8UMQeMi
フィボナッチの計算できた!
— Tasuku Suzuki (@task_jp) May 16, 2024
0: LDI 1 // 1 -> A
1: STA 14 // A -> @14
2: STA 15 // A -> @15
3: OUT // A -> LED
4: ADD 14 // A += @14
5: JC 13 // HLT
6: STA 14 // A -> @14
7: OUT // A -> LED
8: ADD 15 // A += @15
9: JC 13 // HLT
10: JMP 2 // loop
...
13: HLT // halt
... pic.twitter.com/Q7vOWY3Lxy
完成
以上で Ben Eater の 8bit CPU のキットが完成しました。
試行錯誤
フィボナッチ数列の計算を効率よく行おう
11バイトで計算ができるのも十分すごいのですが、上記のコードを書いていたら、
「あれ?SUMレジスタの値を、AレジスタとBレジスタに交互に入れていけば計算できるのでは?」と閃きました。
ということで、フィボナッチ数列の計算のための専用命令を2つ追加し、わずか3バイトで計算ができるようになりました。
1命令でフィボナッチ数列の計算を1つ進めるインストラクションをCPUに実装したら、3バイトのプログラムで超高速に計算ができるようになった。
— Tasuku Suzuki (@task_jp) May 16, 2024
0x0: LDI 1
0x1: FBB 0x2
0x2: FBA 0x1
ALUの結果をAかBレジスタにいれつつ7セグ出力とジャンプをして、オーバーフロー時には回路が止まる命令すごい。 pic.twitter.com/oYUnaZNhDd
最大公約数の計算
16バイトでなんとかなるのすごい。
ユークリッドの互除法で最大公約数求めるプログラムが16ビットに収まったw
— Tasuku Suzuki (@task_jp) May 17, 2024
0: LDA E
1: SUB F
2: JZ B
3: JC 9
// swap:
4: LDA F
5: SWP1 E
6: SWP2 E
7: STA F
8: JMP 0
// continue:
9: STA E
A: JMP 0
// done:
B: LDA F
C: OUT
D: HLT
// data:
E: 15
F: 9 pic.twitter.com/fHjysIrdGx
キット組み立てに利用した道具の紹介
(アマゾンのリンクはアフィリエイトになっています)
ケーブルの長さを測るやつ
ブレッドボードにちょうどいい長さのケーブルを作成する器具を作った! pic.twitter.com/xAX8TRfEBO
— Tasuku Suzuki (@task_jp) March 13, 2024
ブレッドボードの電源ラインをつなぐやつ
ブレッドボードの±同士を繋ぐのめんどくさくなくするやつつくった pic.twitter.com/BYsYJhwNzF
— Tasuku Suzuki (@task_jp) May 4, 2024
抵抗とかコンデンサをブレッドボードにちょうど良く加工しやすくるすやつ
LEDとか抵抗とかをブレッドボードに差しやすい形にする道具を作った。足の広さを決めて、押し込んで(引っ張って)、カットするとちょうどいい感じになるー pic.twitter.com/WdOXVaCTxL
— Tasuku Suzuki (@task_jp) March 10, 2024
ロジックテスタ
ロジックテスタを作ってみよう!【MR-LOGIC-TESTER】
もっと早く買っておけばよかった。。。
オシロスコープ
とうとう買ってしまったよ。 pic.twitter.com/oKAKfVQ6Pm
— Tasuku Suzuki (@task_jp) May 13, 2024
ラジオペンチ
ツノダ(Tusnoda) King TTC 先曲りラジオペンチ 125mm RB-125
ニッパー
ホーザン(HOZAN) 精密ニッパー フルフラッシュカットタイプ 細銅線用 N-55
ケーブルストリッパー
エンジニア オートワイヤーストリッパー PAW-41
これは超便利です。
抵抗内蔵LED
抵抗つなぐのが面倒くさくて、大人買いして遠慮なく使いました。
ケース
最近ブレッドボードで組み立ててたCPU が、こちらの棚板にわりとピッタリ収まったので、とてもいい気分です。https://t.co/D6cyMtlimr pic.twitter.com/3evpv319wp
— Tasuku Suzuki (@task_jp) May 22, 2024