適当のごった煮

Pythonと境界標とQGISを中心にいろいろと

2bitCPUをPythonでシミュレーション

スポンサードリンク

今更ですが、『CPUの創りかた』を読みました。最初は読み通せるか不安でしたが、割とすんなり読み進めることができました。しかし、この本はひと通り読み終わってからが本当の勝負、という気がします。

つまり、実際に4ビットCPU(TD4)を作るかどうかという問題です。

一応、原理は理解できたはずだから良しとするか、作りたい気持ちはあっても手間を考えるとためらってしまう、もしくは、とくかく部品を買いに行くなど、読後の行動は人それぞれだと思います。

個人的には、制作例として載っていた写真を見ただけでそのまま作ることは諦め、ROMをArduinoに置き換える方法などを検討しましたが、それでも多い配線作業をやり通す気力が湧かず迷っているとき、2ビットにすればかなり簡略化できるのではないかと思いついて検討を始めました。

考えた2bitCPUは、以下の通りです。基本的にTD4の4bitを2bitに置き換えただけです。

機能 2bitCPU
汎用レジスタ 2bit×1
アドレス空間 2bit
プログラムカウンタ 2bit
フラグレジスタ 1bit
算術演算 2bitの加算のみ

命令は、以下の通り4個しかありません。

命令コード ニーモニック 記号化
00 MOV A←Im
01 ADD A←A + Im
10 OUT OUT A
11 JMP JMP Im

回路図は専門に学んだこともなく不安でしたが、私の電子工作作品集 CPUの創りかたの回路図画像をA3で印刷して、実際にクロック信号によってどのようにデータが流れるかを追いかけながらメモを書き込んで理解を深めました。

全体的な流れを単純化すると下図のようになります。今回考えた2ビットCPUではレジスタが一つのなので、配線の数を合わせて接続対象(番号?)を書き加えれば回路図になりそうです。

f:id:tekito-gottani:20190102172127j:plain

2bitCPU概要

続いてデコーダは、命令コードの最上位をOP1、最下位をOP2として整理すると下表のようになりました。

命令 OP1 OP2 レジスタ OUT PC セレクタA セレクタB
MOV L L H L L L H
ADD L H H L L L L
OUT H L L H L L L
JMP H H L L H L H

OP1、OP2が入力なので、その2個と出力対象について個別で考えて、とにかく力技で回路を導き出しました。

出力対象 回路
レジスタ NOT OP1
OUT (NOT OP2) & OP1
PC OP1 & OP2
セレクタA 常にL
セレクタB NOT (OP1 ^ OP2)

あとは本の通りのICを買い揃えれば作れそうだと思い、ネットで在庫を調べてみたところ、必要なICが生産終了だったり存在しても表面実装用だったり、いくつかの店を探し回っても必要な部品が手に入るとは限らない印象を受けました。

仕方なく、Arduinoで作成しようと思っていたROMなどをテスト的にPythonで作ってみたら、案外簡単にできたので、勢いにのって2bitCPUシミュレータをPythonで作成しました。

デコーダで「and」をビット演算子のつもりで使用したため、思い通りの結果にならず30分ほど悩みました(「0b10 and 0b01」は「1」になる)。ビット演算では「&」を使わなければならないことを学習しました。同様に、否定の意味で「~」を使っていたら、やはり結果が想定とは異なり、否定ではなくてビット反転であることを学びました。

桁上がり(キャリーフラグ)は発生の表示をするだけで、演算結果を「0b00」にセットしていたり、ROMの実質容量は無限だったり、手抜きをしていますが、とにかくなんとか動くところまで作り、著者の言う「コンピュータ(CPU)の動作の本質はデータの転送」ということを体感することはできたので、よしとしています。

スクリプトの後に実行結果の画像を貼っています。

# 2進数を2桁の文字列に変換
def pb(data):
    strb = ''
    if not 0b10 & data:
        strb = '0' + str(data)
    else:
        strb = bin(data).replace('0b', '')
    return strb
        
# オペコード印字用変換
def pop(op):
    jisyo = {0:'MOV', 1:'ADD', 2:'OUT', 3:'JMP'}
    return jisyo[op]

# Reset処理
f_reg = False
f_out = False
f_pc = False
f_sa = False
f_sb = False
f_c = False

# アドレスとレジスタの初期化
adr = 0
reg = 0

# ROM
rom = [0b0001, 0b0101, 0b1000, 0b1101]
#rom = [0b0111, 0b0111, 0b1100]
#rom = [0b0011, 0b1000, 0b1100]
#rom = [0b0001]
#rom = [0b1100]

print('エンターキーでクロック発生(qで終了)', end='')
while True:
    result = '' # 実行結果保存用
    
    # クロック
    clk = input()
    if clk == 'q':
        break

    # アドレスを記録して、ROM読み出し、アドレスを次にセット
    result += '{} '.format(pb(adr))
    mem = rom[adr]
    adr += 1

    # オペコード、イミディエイトデータ取り出し
    op = mem >> 2 & 0b0011
    im = mem & 0b0011

    # デコーダー
    f_reg = not (op >> 1)
    f_out = (not (op & 0b01)) & (op >> 1)
    f_pc = (op >> 1) & (op & 0b01)
    f_sb = not ((op >> 1) ^ (op & 0b0001))

    # セレクタ:データ選択フラグを参照してレジスタ値か0b00をALUに送る
    if f_sb == True:
        set_alu = 0b00
    else:
        set_alu = reg

    # ALU:セレクタからのデータとROMからのimを足し算して桁上がりフラグセット
    fadder = set_alu + im
    if fadder > 0b11:
        alu = 0b00
        f_c = True
    else:
        alu = fadder
        f_c = False

    # 命令内容とALU計算結果保存
    result += '{} {} alu:{} '.format(pop(op), pb(im), pb(alu))

    # ALU演算結果をデコーダーでセットされたフラグに応じて取得
    if f_reg: # MOVまたはADD命令:ALUの出力をレジスタにセット
        reg = alu
    if f_out: # OUT命令
        result += 'OUT : {} '.format(pb(alu))
    if f_pc: # JMP命令:次のアドレスはALU演算の結果(0b00とimの和)
        adr = alu
    if f_c:
        result += '桁上がり発生 '

    print(result, end='')

    if adr + 1 > len(rom):
        print('\n指定できるアドレス空間を超えました')
        break

実行結果は以下のような感じです。

f:id:tekito-gottani:20190102174457j:plain

f:id:tekito-gottani:20190102175257j:plain

参考