豊原備忘録

意味わからん学生が書き物します

何も知らない高専生が初めての電子工作でCPUを作った話

こちらは自作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とは一体なんでしょうか?概要すら知らないのに作ろうとするのは流石に無謀と言えます。ちょっとだけ先に知っておきましょう。
プログラミングという単語は皆さん人生のどこかで聞いたことがあるでしょう。最近の中高生はプログラミングの授業があるんですかね、気の毒ですね。プログラミング、プログラムを書いてゲームを作ったりモーターを動かしたりするアレですね。あなたがこの記事を読んでいるSafariChromeもプログラムですし、YoutubeTwitterInstagramもプログラムです。ああ素晴らしきかなプログラム。プログラムが無ければお前は生きてはいけません。
ですが考えてみてください、そのプログラム、一体どこで動いているのでしょうか? その答えが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と同義ってことに後々気づきました.

自作したISA.無駄な機能が多い
シミュレータでの設計

とあるDiscord鯖のメンバーが自作CPUを論理回路シミュレータ「Logisim」で設計する~みたいなこと言ってたので,Logisimの使い方学びながらPEC-1の基本設計を模索して作ってました.
夏休み期間だったのに自分がのろのろしていて,9月は丸々シミュレータしかやっていなかったので,後々の回路基板設計の時間とかがギリギリすぎて,正直後悔してます.

Logisimでのシミュレート
回路基板の設計(KiCAD)

TD4みたいにちまちま配線しようとは一瞬考えましたが,DIP SWで実装したROMの配線に阿鼻叫喚したのでさすがにPCB*4で作るしかないなということで,KiCAD*5なんもわからんみたいなところから.(当時ROMのICがあること知らなかったから,DIP SWで実装するつもりだった.というか実装した)

回路図を作るのは割と簡単で,抵抗とか74HCシリーズのIC置いたりピンヘッダおいたりくらいはできたんですが,問題はフットプリントの割り当てとPCB編集です.「フットプリントって何!?」ってところだったし,フットプリントのパッケージの見方すらわからないうえに,データシート調べて寸法見るのとか下手くそだったので1週間ぐらい開発停滞しました.


何とか頑張って調べて,以下の神みたいな記事に出会たのでフットプリント割り当てとPCB編集を乗り切りました.
zenn.dev
この時多分,高専祭2週間前切ってたみたいな状況でめっちゃ焦ってました.

思ったのは,フットプリントはデータシートさえ調べればなんやかんや割り当てれるなぁっていう単純さと,PCB編集もクリックしたピンと接続してるピンが光ってくれるという機能とか,単純に表裏の銅線配線とシルク*6とかエッジカット*7のレイヤーを変えてお絵描きするだけっていう見た目と裏腹の簡単さに驚きました.明日から基板つくれるやん.

ALUのPCB編集画面.裏にベタGNDやってない

実装

はんだ付けと部品購入

自分は大阪に住んでいるので日本橋が割と近く,部品を買うためにシリコンハウスとかめっちゃ寄って大量購入しました.オンライン通販なら領収書発行とか容易だったのに.

はんだ付けは,基板と部品がそろった時点ではもう高専祭一週間前で,一人ではやり切れるわけがないため電研のCPU自作の協力部員に手伝ってもらいました.
一番やばかったのはみんなROMのはんだ付けって言ってます.ダイオード...

Data Selectorはんだ付け
ALU完成の儀
命令デコーダ

命令デコーダもロジックで実装しようかと思いましたが,やばい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に入る入力電圧レベルの組み合わせによって,出力を決めているという感じです.冗長すぎて頭悪いですね.

デバッグ

クロック生成の基板がなぜか動かなくて,回路図はあっているのにクロック生成ができてなかったんですね.
テスターとかオシロスコープの使い方をなんも知らんので,完全にデバッグは電研の部長に頼みっきりでした.(原因は配線の内部断線)

この経験で大体デバッグはどうしたらいいとかを学べました.ありがとう部長

現場猫案件

高専祭前日,作っておいたラズパイの命令デコーダのプログラムも正常動作して展示だけだーってところで,プログラムを保存し忘れてしまいました泣
当日になって頑張って前日の内容を思い出しながら復元していましたが,コードエラーとかに悩まされて初動の一日潰しました.その代わり原因とかもわかって,二日目はしっかり動きました.

それに加えて,クロックを10Hzにするとプログラムの認識がバグって違う出力になっていました.
後からわかりましたが,ICに着けるパスコン*9が0.1μFのものだけで,基板一層ずつに着ける大きめの容量のキャパシタをつけていなかったのが一つの要因かなと思います.

完成像

完成した様子

完成

展示用に友達にポスターなんかも作ってもらいました.

ポスター

CPU自作を完走した感想

開発一カ月くらいでよく頑張れたと思います.めっちゃ成長した.


何も知らないところからとにかく調べて焦って実装みたいな計画性の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を作りたい,という気持ちでいます.
いま目に見えてる改良点は,さっき話したようなのところや,ちょっとレイヤー上げて開発しやすくするとかです.

追記

高専カンファレンス150 in 大阪にて,自作CPUについて登壇しました.
その時の資料を上げました.

www.docswell.com

*1:レジスタ番号やメモリ番地など参照してALUに送るデータを決めるユニット

*2:算術演算と論理演算などができる演算ユニット.Arithmetic Logic Unit

*3:CPUが実行できる命令の一覧

*4:プリント基板

*5:プリント基板が設計できるソフトウェア

*6:基板に文字や絵を載せるための層?

*7:基板外形

*8:汎用入出力ピン

*9:バイパスコンデンサ.電源とICを中継・仲介に使う