こちらは自作CPU Advent Calendar 2023の19日目の記事です.
adventar.org
はじめに
おはこんばんにちは,芭蕉梶木です.
今回は電子工作の「で」の字ぐらいの状態から高専祭までにCPU自作をやって間に合わせた話をします.
主に開発過程とかその中の思考プロセスとかを書いて,誰かの技術活動の刺激になればいいと思っています.
自己紹介
奈良高専情報工学科に所属してます.二年生(2023年現在)です.
Twitter:https://twitter.com/Basyo_Engineer
CPUとはなにか
クレイジーピエロ 様(https://twitter.com/Cra2yPierr0t)が無から始める自作CPUにてエキセントリックで素晴らしい説明をされているので引用します.
CPU、それは我々が暮らす情報社会の基盤となる魔法の石です。
世に存在する全てのソフトウェア、例えばゲーム、AI、Webサーバ、OS、これらは全てCPUが無ければ動きませんし、今や車や飛行機、家電にも全てCPUが入っている時代です。
...(中略)
CPU、我々が作る対象です。CPUとは一体なんでしょうか?概要すら知らないのに作ろうとするのは流石に無謀と言えます。ちょっとだけ先に知っておきましょう。
プログラミングという単語は皆さん人生のどこかで聞いたことがあるでしょう。最近の中高生はプログラミングの授業があるんですかね、気の毒ですね。プログラミング、プログラムを書いてゲームを作ったりモーターを動かしたりするアレですね。あなたがこの記事を読んでいるSafariやChromeもプログラムですし、YoutubeもTwitterもInstagramもプログラムです。ああ素晴らしきかなプログラム。プログラムが無ければお前は生きてはいけません。
ですが考えてみてください、そのプログラム、一体どこで動いているのでしょうか? その答えがCPUです。CPUとはCentral Processing Unit、中央演算装置の略で、石みたいなガラスみたいな物質で出来ています。
この世に存在するプログラムは全てCPUの上で動いています。このCPUが無ければプログラムは動かせませんし、いくらプログラムを書いても意味がありません。我々の生活基盤はこの石に支えられているという訳ですね、ありがとうCPU。愛してるCPU。
つまり,プログラムを動かすために必須ななんでもできる魔法の石です.
自作したCPUのスペック
とりあえず簡単に,自作したCPUの概要だけ.
- CPU名: PEC-1(Prototype Experimental Computer No.1)
- 命令長: オペコード 4bit + オペランド4bit
- データ長: 6bit
- レジスタ数: 4つ(00 ~ 11)
- フラグ: Zero Flag, Sign Flag, Overflow Flag
- ROM: 8+6bit(即値) * 32
- 命令デコーダ: Rasberry Pi Pico W
- 最大動作クロック: 1Hz
Logisimのファイルや基板ファイルなど回路の中身はGitHubに載せています.
GitHub
github.com
開発目的
そもそも電卓とかCPUの計算機系に興味はあったんですが,電子工作やるにも何から始めたらいいかわかんなかったし,自作とか原理を知るところまでは敷居が高そうで手を付けれませんでした.
そこで,CPU自作の名著「CPUの創りかた」に出会って,「CPU自作,余裕やん」と思い開発に取り掛かりました.
この本を読んで電子工作の「でn」ぐらいまでは学んで,素晴らしい説明によってCPUの構造を完全に理解しました(?)
高専祭で展示するために作ろうとなったとき,最初はTD4でいいか~と思っていたら,部活の先輩に「GitHubにある作品をCloneしても自分の創作物にはなりません泣」みたいなこと言われたので,8bitに拡張してやろうと思い至りました(正直これが開発の手間が増えた.)
設計
ISA設計
高専の先生になにからしたらいいかを聞いたら,「ISAから作るとユニット設計がしやすいよ」と言われたので,ISAの設計から考えました.
実はこれが思ったより時間かかりました.使える命令は決まったものの,機械語の割り当てや,Data Selector*1とかALU*2の制御信号はどう決めるとか,いろいろ考えるのはしんどかったと思います.
ISAが命令セット*3と同義ってことに後々気づきました.
シミュレータでの設計
とあるDiscord鯖のメンバーが自作CPUを論理回路シミュレータ「Logisim」で設計する~みたいなこと言ってたので,Logisimの使い方学びながらPEC-1の基本設計を模索して作ってました.
夏休み期間だったのに自分がのろのろしていて,9月は丸々シミュレータしかやっていなかったので,後々の回路基板設計の時間とかがギリギリすぎて,正直後悔してます.
回路基板の設計(KiCAD)
TD4みたいにちまちま配線しようとは一瞬考えましたが,DIP SWで実装したROMの配線に阿鼻叫喚したのでさすがにPCB*4で作るしかないなということで,KiCAD*5なんもわからんみたいなところから.(当時ROMのICがあること知らなかったから,DIP SWで実装するつもりだった.というか実装した)
回路図を作るのは割と簡単で,抵抗とか74HCシリーズのIC置いたりピンヘッダおいたりくらいはできたんですが,問題はフットプリントの割り当てとPCB編集です.「フットプリントって何!?」ってところだったし,フットプリントのパッケージの見方すらわからないうえに,データシート調べて寸法見るのとか下手くそだったので1週間ぐらい開発停滞しました.
何とか頑張って調べて,以下の神みたいな記事に出会たのでフットプリント割り当てとPCB編集を乗り切りました.
zenn.dev
この時多分,高専祭2週間前切ってたみたいな状況でめっちゃ焦ってました.
思ったのは,フットプリントはデータシートさえ調べればなんやかんや割り当てれるなぁっていう単純さと,PCB編集もクリックしたピンと接続してるピンが光ってくれるという機能とか,単純に表裏の銅線配線とシルク*6とかエッジカット*7のレイヤーを変えてお絵描きするだけっていう見た目と裏腹の簡単さに驚きました.明日から基板つくれるやん.
実装
はんだ付けと部品購入
自分は大阪に住んでいるので日本橋が割と近く,部品を買うためにシリコンハウスとかめっちゃ寄って大量購入しました.オンライン通販なら領収書発行とか容易だったのに.
はんだ付けは,基板と部品がそろった時点ではもう高専祭一週間前で,一人ではやり切れるわけがないため電研のCPU自作の協力部員に手伝ってもらいました.
一番やばかったのはみんなROMのはんだ付けって言ってます.ダイオード...
命令デコーダ
命令デコーダもロジックで実装しようかと思いましたが,やばいIC数と設計時間の問題で先送りにしました.
下のマシン語対応表をもとに,今回はRasberry Pi Pico WのGPIO*8を利用して実装しました.
そのソースコードが下です.
from machine import Pin import time oc3 = machine.Pin(1, machine.Pin.IN) oc2 = machine.Pin(2, machine.Pin.IN) oc1 = machine.Pin(3, machine.Pin.IN) oc0 = machine.Pin(4, machine.Pin.IN) ol3 = machine.Pin(5, machine.Pin.IN) ol2 = machine.Pin(6, machine.Pin.IN) ol1 = machine.Pin(7, machine.Pin.IN) ol0 = machine.Pin(8, machine.Pin.IN) zf = machine.Pin(9, machine.Pin.IN) of = machine.Pin(10, machine.Pin.IN) sf = machine.Pin(11, machine.Pin.IN) sela2 = machine.Pin(14, machine.Pin.OUT) sela1 = machine.Pin(13, machine.Pin.OUT) sela0 = machine.Pin(12, machine.Pin.OUT) selb2 = machine.Pin(17, machine.Pin.OUT) selb1 = machine.Pin(16, machine.Pin.OUT) selb0 = machine.Pin(15, machine.Pin.OUT) ld0 = machine.Pin(18, machine.Pin.OUT) ld1 = machine.Pin(19, machine.Pin.OUT) ld2 = machine.Pin(20, machine.Pin.OUT) ld3 = machine.Pin(21, machine.Pin.OUT) ldo = machine.Pin(22, machine.Pin.OUT) ldpc = machine.Pin(26, machine.Pin.OUT) pm = machine.Pin(27, machine.Pin.OUT) al = machine.Pin(28, machine.Pin.OUT) oc = [0, 0, 0, 0] ol = [0, 0, 0, 0] flag = [0, 0, 0] selector = [0, 0, 0, 0, 0, 0] Load = [1, 1, 1, 1, 1, 1] alithm = [0, 0] mov1 = [0, 0, 1, 0] mov2 = [0, 0, 1, 1] add1 = [0, 1, 0, 0] add2 = [0, 1, 0, 1] sub1 = [0, 1, 1, 0] sub2 = [0, 1, 1, 1] nan1 = [1, 0, 0, 0] nan2 = [1, 0, 0, 1] jmp = [0, 0, 0, 1] jno = [1, 0, 1, 1] jze = [1, 0, 1, 0] jmi = [1, 1, 0, 0] jpl = [1, 1, 0, 1] inp = [1, 1, 1, 0] outp = [1, 1, 1, 1] nop = [0, 0, 0, 0] def decoder (opeland): ret = [1, 1, 1, 1, 1, 1] if(opeland[0] == 0 and opeland[1] == 0): ret[0] = 0 if(opeland[0] == 0 and opeland[1] == 1): ret[1] = 0 if(opeland[0] == 1 and opeland[1] == 0): ret[2] = 0 if(opeland[0] == 1 and opeland[1] == 1): ret[3] = 0 return ret while True: oc[0] = oc3.value() oc[1] = oc2.value() oc[2] = oc1.value() oc[3] = oc0.value() ol[0] = ol3.value() ol[1] = ol2.value() ol[2] = ol1.value() ol[3] = ol0.value() flag[0] = zf.value() flag[1] = of.value() flag[2] = sf.value() if oc == mov1: selector = [0, ol[2], ol[3], 1, 1, 0] Load = decoder(ol) alithm = [0, 0] print(oc) print(ol) print(flag) print("mov1") if oc == mov2: selector = [1, 0, 1, 1, 1, 0] Load = decoder(ol) alithm = [0, 0] print(oc) print(ol) print(flag) print("mov2") if oc == add1: selector = [0, ol[0], ol[1], 0, ol[2], ol[3]] Load = decoder(ol) alithm = [0, 0] print(oc) print(ol) print(flag) print("add1") if oc == add2: selector = [0, ol[0], ol[1], 1, 0, 1] Load = decoder(ol) alithm = [0, 0] print(oc) print(ol) print(flag) print("add2") if oc == sub1: selector = [0, ol[0], ol[1], 0, ol[2], ol[3]] Load = decoder(ol) alithm = [1, 0] print(oc) print(ol) print(flag) print("sub1") if oc == sub2: selector = [0, ol[0], ol[1], 1, 0, 1] Load = decoder(ol) alithm = [1, 0] print(oc) print(ol) print(flag) print("sub2") if oc == nan1: selector = [0, ol[0], ol[1], 0, ol[2], ol[3]] Load = decoder(ol) alithm = [0, 1] print(oc) print(ol) print(flag) print("nan1") if oc == nan2: selector = [0, ol[0], ol[1], 1, 0, 1] Load = decoder(ol) alithm = [0, 1] print(oc) print(ol) print(flag) print("nan2") if oc == jmp: selector = [1, 0, 1, 1, 1, 0] Load = [1, 1, 1, 1, 1, 0] alithm = [0, 0] print(oc) print(ol) print(flag) print("jmp") if oc == jno and flag[1] == 0: selector = [1, 0, 1, 1, 1, 0] Load = [1, 1, 1, 1, 1, 0] alithm = [0, 0] print(oc) print(ol) print(flag) print("jno") if oc == jze and flag[0] == 1: selector = [1, 0, 1, 1, 1, 0] Load = [1, 1, 1, 1, 1, 0] alithm = [0, 0] print(oc) print(ol) print(flag) print("jze") elif oc == jze and flag[0] == 0: selector = [1, 1, 0, 1, 1, 0] Load = [1, 1, 1, 1, 1, 1] alithm = [0, 0] if oc == jmi and flag[2] == 1: selector = [1, 0, 1, 1, 1, 0] Load = [1, 1, 1, 1, 1, 0] alithm = [0, 0] print(oc) print(ol) print(flag) print("jmi") if oc == jpl and flag[0] == 0 and flag[2] == 0: selector = [1, 0, 1, 1, 1, 0] Load = [1, 1, 1, 1, 1, 0] alithm = [0, 0] print(oc) print(ol) print(flag) print("jpl") if oc == inp: selector = [1, 0, 0, 1, 1, 0] Load = decoder(ol) alithm = [0, 0] print(oc) print(ol) print(flag) print("inp") if oc == outp: selector = [0, ol[0], ol[1], 1, 1, 0] Load = [1, 1, 1, 1, 0, 1] alithm = [0, 0] print(oc) print(ol) print(flag) print("outp") if oc == nop: selector = [1, 1, 0, 1, 1, 0] Load = [1, 1, 1, 1, 1, 1] alithm = [0, 0] print(oc) print(ol) print(flag) print("nop") #print(Load) sela2.value(selector[0]) sela1.value(selector[1]) sela0.value(selector[2]) selb2.value(selector[3]) selb1.value(selector[4]) selb0.value(selector[5]) ld0.value(Load[0]) ld1.value(Load[1]) ld2.value(Load[2]) ld3.value(Load[3]) ldo.value(Load[4]) ldpc.value(Load[5]) pm.value(alithm[0]) al.value(alithm[1])
timeライブラリ入れてるのに使ってない...
GPIOに入る入力電圧レベルの組み合わせによって,出力を決めているという感じです.冗長すぎて頭悪いですね.
完成像
完成した様子
展示用に友達にポスターなんかも作ってもらいました.
CPU自作を完走した感想
開発一カ月くらいでよく頑張れたと思います.めっちゃ成長した.
自作8bit CPU「PEC-1」、完成致しました‼️
— 芭蕉梶木 (@Basyo_Engineer) 2023年11月3日
16種類の命令、4つの汎用レジスタ、その他諸々基本的な計算ができます。
明日開催される奈良高専祭の電気科展にて展示いたします。#第57回奈良高専祭 #奈良高専祭 pic.twitter.com/tEAvoLK8Rf
何も知らないところからとにかく調べて焦って実装みたいな計画性のNASAばかりでしたが,やりたかったことに熱中できて楽しかったです.
作るためのツールや知識は思ったより簡単だったり,手に届くところにある,ということに気づけたり,CPUに対する解像度が上がって低レイヤーに対する興味がより一層湧いた.
もっとCPU自作erとか低レイヤやってる人と絡んで,いろいろ刺激を受けたい.
高専祭での展示ではあんまりウケなっかたという印象.「CPUって何?」とか「これは何をしてるの?」というような質問をしてくる一般人に対するCPUの説明が難しかった.
I/O,特に画面描画やコントローラー入力ができるよう充実させれれば,もっとわかりやすく,知っているものが目に見えて在るのでもっと詳細が伝わりやすいかなと思いました.
第二世代の自作CPUではゲームくらい遊べるような汎用性とか,自作コンパイラでもっとプログラム書きやすくするとかを目指したい.
改良に向けて
PEC-1はASCIIも入らないデータ長で,高クロックに耐えられないし,ROMは少ないし上にRAMもない,というもはや何?というCPUなので,改良型の「GEC-2」の構想を考えています.来年ぐらいに,PEC-1改とGEC-2出せれたらいいな,ぐらいに思ってます.
しかし,それなりにガチなCPUを自作するとなると,TD4やPEC-1みたいなおもちゃレベル(?)ではいけないので,それなりにコンピュータアーキテクチャ分野に造詣がないと難しいわけです.
だから,Nand2Tetrisとか読んでインプットしたり,他の低レイヤー屋さんと絡んでインスピーレーションを受けてアイデアを浮かばせたり,ツールの使い方を学んで最高のCPUを作りたい,という気持ちでいます.
いま目に見えてる改良点は,さっき話したようなのところや,ちょっとレイヤー上げて開発しやすくするとかです.