RAM【Turing Complete編】
はじめに
いつもブログをご覧いただきありがとうございます。
コーストFIRE中のIPUSIRONです😀
RAMステージ
RAMステージのゴールは、我々のコンピューターにRAMを組み込むことです。
RAMコンポーネント
RAMはコンポーネントとして用意されています。「IO」>「RAM」からRAMコンポーネントを選択できます。
RAMコンポーネントの入出力は次の通りです。
左のSettingsでRAMのサイズとデータ幅を設定できます。デフォルトでは256バイト、データ幅は8ビットになっています。本ステージはこのままで問題ありません。
入力 | ・Loadピン ・Saveピン ・Addressピン ・Save valueピン・・・4本用意されている。 |
出力 | ・Output 1ピン ・Output 2ピン ・Output 3ピン ・Output 4ピン |
RAMは256バイトのメモリーであり、どのバイトデータを扱うかアドレス指定します。RAMにロードあるいはセーブする場合は、RAMアドレスをレジスターにコピーします。
本ステージのテスト
[Next]ボタンや[Run]ボタンを押してみてください。Programコンポーネントから4バイトごと出力されていきます。
Programコンポーネントにはゼロ初期化されており、すべてが00hで埋まっています。つまり、毎回00 00 00 00hが処理されます。
・1バイト目が00h⇒オペコードはADD
・2バイト目が00h⇒計算対象(転送元)がREG 0の内容
・3バイト目が00h⇒計算対象(転送元)がREG 0の内容
・4バイト目が00h⇒計算結果(転送先)がREG 0
つまり、「REG 0←REG 0+REG 0」を実行しています。REG 0には最初0しか入っていないので、このADD演算を繰り返しても、結局0のままです。
ずっと変化しない処理を繰り返して、処理が終わりません。[Reset]ボタンを押して、テストを手動で停止させてください。
テストパターン
本ステージのテストでは、32回バイトデータが入力されるので、それを同じ順序で出力することがゴールとなります。
RAMに入力データを順番に記憶しておき、それを順番に出力するような回路とプログラムを作ることになります。
ただし、すべての入力を読み取る前に出力すると、テストは失敗するので注意してください。
RAMステージを解く
1:ナンバーフォーマットを符号なしに切り替える
流れているデータを見やすいように「Toggle number format」のアイコンを押して、ナンバーフォーマットを切り替えます。「+255」になると0~255で表現されます。
2:メモリーへのロード・セーブのオペコードを決める
メモリーへのロード・セーブのオペコードを自作することになります。
既存のオペコードについて復習しておきます。$x, y \in \{0,1\}$であり、1だと即値になります。
1バイト目 | オペコード | 処理内容 |
---|---|---|
xy00 0000b | ADD | 引数1と引数2を加算した結果を転送先にセットする。 |
xy00 0001b | SUB | 引数1から引数2を減算した結果を転送先にセットする。 |
xy00 0010b | AND | 引数1と引数2をAND演算した結果を転送先にセットする。 |
xy00 0011b | OR | 引数1と引数2をOR演算した結果を転送先にセットする。 |
xy00 0100b | NOT | 引数1をビット反転した結果を転送先にセットする。 |
xy00 0101b | XOR | 引数1と引数2をXOR演算した結果を転送先にセットする。 |
0010 0000b(=32d) | IF_EQUAL | 引数1と引数2が一致したら、指定のアドレスにジャンプする。 |
0010 0001b(=33d) | IF_NOT_EUQAL | 引数1と引数2が不一致なら、指定のアドレスにジャンプする。 |
0010 0010b(=34d) | IF_LESS | 引数1が引数2より小さいなら、指定のアドレスにジャンプする。 |
0010 0011b(=35d) | IF_LESS_OR_EQUAL | 引数1が引数2以下なら、指定のアドレスにジャンプする。 |
0010 0100b(=36d) | IF_GREATER | 引数1が引数2より大きいなら、指定のアドレスにジャンプする。 |
0010 0101b(=37d) | IF_GREATER_OR_EQUAL | 引数1が引数2以上なら、指定のアドレスにジャンプする。 |
指定の際に使う値 | 取り出す場所 or 格納する場所 |
---|---|
0 | REG 0 |
1 | REG 1 |
2 | REG 2 |
3 | REG 3 |
4 | REG 4 |
5 | REG 5 |
6 | カウンター |
7 | 転送元の場合は入力端子 転送先の場合は出力端子 |
モードでいうと、Calulationモード、Conditionモード、Immediateモードは実装済みです。残っているのは、Copyモードになります。Copyモードがあれば、入力端子から与えられたバイトデータをレジスターに直接記憶させられます。しかし、「CalulationモードのADD」と「即値の00h」を組み合わせれば、実質的に同様のことを実現できてしまいます。
例えば、入力端子からくるバイトデータをREG 5に保存したければ、次の命令をProgramコンポーネントに書き込めばよいのです。
1バイト目 | 2バイト目 | 3バイト目 | 4バイト目 |
---|---|---|---|
ADD | 入力端子 | 0(即値) | REG 5 |
0100 0000b | 0000 0111b(=7d) | 0000 0000b | 0000 0101b(=5d) |
同様にして、出力端子から送出することもできます。
1バイト目 | 2バイト目 | 3バイト目 | 4バイト目 |
---|---|---|---|
ADD | REG5 | 0(即値) | 出力端子 |
0100 0000b | 0000 0101b(=5d) | 0000 0000b | 0000 0111b(=7d) |
本ステージは、Programコンポーネントの「Edit memory」ボタンを押すと、アセンブリーエディターが立ち上がります。左の"Assembly codes"の下の[Add]ボタンを押すと、命令を定義できます。
本ステージのテストをパスするためには、入力端子から来たバイトデータをレジスターにいったん記憶して、それをメモリーにセーブすることになります。すべてのデータを読み込んだら、順番通りにロードして、出力端子から送出します。
「入力をレジスターに保存する処理」「レジスターから出力する処理」については既存のオペコードで実現できることは説明しました。
残されているのは、「レジスターのバイトデータをRAMにセーブする」「RAMのバイトデータをレジスターにロードする」という2つの処理になります。
ここではRAMにセーブ・ロードするためのレジスターは、REG 5固定とします。
※固定にすることで、プログラムの柔軟性は失われますが、回路が簡単になります。
既存のオペコードと被らないここと、回路を設計しやすくすることを考慮して、次の2つのオペコードを追加します。
1バイト目 | オペコード | 処理内容 |
---|---|---|
0001 0000b(=16d) | LOAD_RAM | REG 5内のデータを指定アドレスとして、RAMにロードする(読み込む)。 |
0001 0001b(=17d) | SAVE_RAM | REG 5内のデータを指定アドレスとして、RAMにセーブ(保存)する。 |
メモリーのアドレスを指定する必要があります。
1バイト目 | 2バイト目 | 3バイト目 | 4バイト目 |
---|---|---|---|
オペコード LOAD_RAM | なし | なし | 転送先(0~7) |
0001 0000b(=16d) | 1111 1111b(未割り当て用) | 1111 1111b(未割り当て用) | 【例】出力端子 0000 0111b |
1バイト目 | 2バイト目 | 3バイト目 | 4バイト目 |
---|---|---|---|
オペコード SAVE_RAM | 転送元(0~7) | なし | なし |
0001 0001b(=17d) | 【例】入力端子 0000 0111b | 1111 1111b(未割り当て用) | 1111 1111b(未割り当て用) ※例えば、0000 0000bを指定すると、REG 0に書き込み処理が走ってしまうので、もしこうした入力を想定するなら防止回路が必要です。 |
本ステージの一番のテーマであるRAM用のセーブ命令とロード命令については以上になります。
3:RAMを組み込む
RAMコンポーネント周辺を実装します。その結果が次の通りです。
RAMコンポーネントのLoadピンとSaveピンは3 Bit decoderの出力ピンから取ります。3 Bit decoderの出力ピンはどれか1つだけONになります。つまり、RAMコンポーネントにLaodピンとSaveピンの両方がONになることはありません(両方がOFFになることはある)。
RAMコンポーネントのAddressピンは、REG 5のOutputピンと直結します。途中にスイッチは不要です。
RAMコンポーネントのSave valueピンは4つありますが、今回は1バイトずつ扱うので、1つのピンしか使いません。命令語の2バイト目に対応するワイヤーとつながります。その際、8 Bit Muxコンポーネントの後で接続しました。
RAMコンポーネントのOutputピンは4つありますが、これについても1つのピンしか使いません。レジスターの入力側のワイヤーに接続します。ただし、直結してしまうとショートするので、途中にスイッチを入れます。LoadピンがONのときだけスイッチをONにするわけです。
4:バグを修正する
Conditionモードのとき、ジャンプ先アドレスとして7d以下が指定される可能性があります。Conditionモードではカウンターから入力してアドレスを上書きすべき状況であり、レジスターの内容は変わる必要はありません。
次に示す回路が完成版になります。「Conditionモードでない」かつ「4バイト目に7d以下」の場合に、レジスターのSaveが動作するようにしています。
※本当はこのステージではなく、Conditionalsステージで気づくべきでしたが、テストにパスしてしまいました。
5:RAMをリンクする【省略可】
Programコンポーネントの「Link components」アイコンを押して、8番にRAMをリンクします。
アセンブリーエディターのLinked componentsのところに、「8 RAM」が追加されました。
RAMがアクセスしているデータを確認できればよいのですが、残念ながらそうはなりませんでした。
6:プログラムを実装する
回路はできあがったので、RAMに連続でセーブしてから、連続でロードするプログラムを実装します。
No. | アドレス | 1バイト目 | 2バイト目 | 3バイト目 | 4バイト目 | 処理内容 |
---|---|---|---|---|---|---|
1 | 00h | 1100 0000b(=192d) ※ADDかつImmediateモード | 0000 0000b | 0010 0000b (=32d) | 0000 0100b ※REG 4 | ※読み込み回数(定数)をREG 4に保持しておく。Conditionモードでは即値を使えないため。 ※32回入力されることは問題文として与えられている。 |
2 | 04h | 0001 0001b(=17d) ※SAVE_RAM | 0000 0111b ※入力端子 | 1111 1111b(=255d) | 1111 1111b(=255d) | 入力端子⇒RAM ※この時点でREG 5は00hなので00h番地にセーブ。 |
3 | 08h | 0100 0000b(=64d) ※ADDかつImmediateモード | 0000 0101b ※REG 5 | 0000 0001b ※即値 | 0000 0101b ※REG 5 | REG 5のデータをインクリメントする。 ※REG 5はアドレス用のカウンター。 |
4 | 0Ch | 0010 0010b(=34d) ※IF_LESS(Conditionモード) | 0000 0101b ※REG 5 | 0000 0100b ※REG 4 | 0000 0100b ※ジャンプ先のアドレス | 32未満なら指定アドレス04hにジャンプ。 ※レジスタ群の番号とぶつかる可能性を考慮すること! |
5 | 10h | 0000 0101b(=5d) ※XOR | 0000 0101b ※REG 5 | 0000 0101b ※REG 5 | 0000 0101b ※REG 5 | REG 5をゼロクリアする。 |
6 | 14h | 0001 0001b(=16d) ※LOAD_RAM | 1111 1111b(=255d) | 1111 1111b(=255d) | 0000 0111b ※出力端子 | RAM⇒出力端子 |
7 | 18h | 0100 0000b(=64d) ※ADDかつImmediateモード | 0000 0101b ※REG 5 | 0000 0001b ※即値 | 0000 0101b ※REG 5 | REG 5のデータをインクリメントする。 |
8 | 1Ch | 0010 0010b(=34d) ※IF_LESS(Conditionモード) | 0000 0101b ※REG 5 | 0000 0100b ※REG 4 | 0001 0100b(=20d) ※ジャンプ先のアドレス | 32未満なら指定アドレス14hにジャンプ。 |
5:プログラムを入力する
作ったプログラムをアセンブリーエディターで入力します。2進数で入力するので少々手間がかかりますが、逆にこれが自作CPUの醍醐味でもあるので楽しんでください。
1行に1バイトごと10進数を入力しておきます。
# 命令1
192
0
32
4
# 命令2
17
7
255
255
# 命令3
64
5
1
5
# 命令4
34
5
4
4
# 命令5
5
5
5
5
# 命令6
16
255
255
7
# 命令7
64
5
1
5
# 命令8
34
5
4
20
6:テストする
アセンブリーエディター上で[Next]ボタンを使って、ステップ実行してください。特にループ処理がうまくいっているかが肝となります。
右下のLinked componentsには、接続したレジスターやカウンターが保持する現在の値が表示されます。ステップ実行するたびに値が変わっていく様子がわかります。期待通りに動作しないときは、デバッグすることになりますが、その際に活躍します。
テストにパスすると、ステージクリアになります。
“Byte 0 is 251, not 1″(「0番目のバイトは241であり、1ではない」)というエラーメッセージが出たときは、241を想定しているのに、1になっていてエラーということです。配線ミスでなければ、スイッチが怪しいです。8 Bit Switchコンポーネントを使うべきところに、Switchコンポーネント(1ビット用)を使ってしまったときにこのエラーを見ました。
見た目が似ており、デバッグで流れているデータを調べて、このミスにようやく気づきました。