こんにちは、めのんです!
前回までは主にデータ構造の話でした。
今回からは実際の動作に関わる部分を作っていきます。
といっても一足飛びに進めることはできませんので、少しずつ積み上げていくことにします。
その第一歩として、今日は命令セットシミュレータの骨格を作ることにしましょう。
骨格なので中身はほとんど何もありませんが、全体の流れがある程度見えてくるのではないかと思います。
クロックカウンタ
本来なら前回やっておいた方がよかったのかもしれませんけど、CPUがリセットしてからの経過時間を記録するクロックカウンタをatmega328_t型に追加することにします。
あくまでもクロックカウンタですので、実時間を測るためのものではありません。
実際のCPUでは、クロック周波数によって1クロックあたりの時間が決まりますので、それをクロック数にかけてあげればシミュレーション時間が求まるはずです。
コードはすごく単純で、atmega328_t構造体の最後に次のようにメンバを追加することにします。
...
// プログラムカウンタ
uint16_t pc;
// クロックカウンタ
uint64_t clock;
} atmega328_t;
クロックは数十ナノ秒のオーダーですので、オーバーフローしないように64ビット符合無し整数を使うことにしました。
atmega328.cファイルの骨格
先ほどまではatmega328.hというヘッダファイルを作っていきました。
今回からはatmega328.cという名前で実装部のソースファイルを扱うことにします。
#include <stdio.h>
#include <stdbool.h>
#include "atmega328.h"
上のように、ソースファイルの先頭で必要なヘッダやヘッダファイルをインクルードしておきましょう。
「stdio.h」はファイルの読み込みやデバッグログの出力に使う予定です。
「stdbool.h」は論理型を使うためですね。
_Bool, 1, 0を使ってもいいのですが、できればbool, true, falseを使いたいですから。
ヘッダは状況によってまた追加するかもしれませんけど、いったんこれぐらいにしておきます。
リセット関数の実装
骨格といっても最低限の関数は用意しておきましょう。
まずはCPUにリセットがかかったときの処理を行う関数を作ります。
これは初期化処理だと考えてかまいません。
void reset(atmega328_t *cpu)
{
cpu->spl = 0xff;
cpu->sph = 0x08;
cpu->pc = 0x0000;
cpu->clock = 0;
}
引数には、どのCPUかを指定するためのatmega328_t型へのポインタを指定しています。
今後登場する関数のほとんどがatmega328_t型へのポインタを引数として受け取ることになります。
肝心の中身ですが、スタックポインタを0x8ffに、プログラムカウンタを0x0000に初期化しています。
この連載ではCPUしか扱いませんが、もしマイコンの制御レジスタもシミュレーションするのであれば、この関数でいっしょに初期値をセットしておくべきです。
レジスタ以外にクロックカウンタも0に初期化しておきましょう。
こうすることで、クロックカウンタはリセットがかかってからの経過クロック数を表すようになります。
reset関数はatmega328.hでも宣言しておいて、外部から呼び出せるようにしておいた方がいいですね。
命令フェッチ関数の実装
CPUがプログラムメモリから命令を読み込むことを「命令フェッチ」といいます。
言葉にすると難しそうですが、実際にやっていることはすごく単純です。
いろいろ言葉で説明するよりコードを見ていただいた方が早いでしょうね。
static inline uint16_t fetch(atmega328_t *cpu)
{
return cpu->program[cpu->pc++];
}
その時点でのプログラムカウンタを添数にしてプログラムメモリから1ワードを読み込んでいるだけです。
読み込んだあとは、プログラムカウンタを1進めています。
命令によってはオペランドとしてもう1ワード読み込む必要がありますので、その場合は各命令をシミュレートする関数の中でもう一度fetch関数を呼び出すことにします。
すごく厳密なことをいうと、プログラムカウンタがプログラムメモリの末尾まで行ってしまった場合の動作も定義して上げる必要があるんですけど、データシートを見ても(私の見落としがなければ)書いていないようですので放置することにします。
そんなことになればどっちにしてもCPUは暴走ですからね。
fetch関数はatmega328.c以外から呼び出す必要はないので、static記憶クラス指定子を付けて内部結合にしています。
先ほどのreset関数は外から呼び出す必要があるので外部結合にしています。
命令デコーダの骨格
次に命令でコーダの骨格を作っていきます。
命令デコーダを完全に実装しきるのはかなり大変な作業になるので、今回はあくまでも骨格だけにとどめます。
static operation_t *decode(atmega328_t *cpu, uint16_t op)
関数頭部は上のようにしようと思います。
関数の中では、実際の命令を処理する関数を表引きしようと考えています。
65,536要素もある配列を定義することになるので大変な作業になりますが、この部分は避けて通れません。
最初は命令の上位4ビットだけのテーブルにしようかとも思ったのですが、かえってコードが複雑になるのでやめました。
というわけで、decode関数の骨格は現時点では次のようになりました(追記:一部変更しました)。
static operation_t *decode(atmega328_t *cpu, uint16_t op)
{
static operation_t* const op_table[0x10000] =
{
// TODO 各命令を処理する関数へのポインタを並べる
};
return op_table[op];
}
operation_t型というのが出てきていますね。
これは各命令を処理するための関数型で、次のように定義しています。
typedef void operation_t(atmega328_t*, uint16_t);
operation_t型の関数は、第1引数にいつものatemega328_t型へのポインタ、そして第2引数に命令を受け取ります。
命令を受け取るのは、その中にオペランドが含まれている場合があるからで、命令自体は同じでオペランドだけが異なる場合は同じ関数にしようと考えています。
ステップ実行関数の実装
(骨格だけですが)命令デコーダの関数までできましたので、次はこれらを組み合わせてステップ実行する関数を作ることにします。
1ステップごとにレジスタの状態をログに出力するといったことが予想されますので、連続実行ではなくあえてステップ実行を基本にしたいと思います。
その方がデバッガを作るのも簡単ですしね!
void step(atmega328_t *cpu)
{
uint8_t opcode= fetch(cpu);
operation_t* pfn = decode(cpu, opcode);
(*pfn)(cpu, opcode);
}
これも流れとしてはすごく簡単で、命令フェッチによって命令(オペコード)を読み込み、命令デコーダで実際に命令を処理する関数を引き当てています。
そして、引き当てた関数を呼び出して実際の命令を処理しています。
クロックカウンタの更新は各命令を処理する関数の中で行う予定です。
命令によってどれだけのクロックがかかるかが変わってきますからね。
こんな感じで命令セットシミュレータの骨格が出来上がりました。
次回は少しずつ命令を追加していくことにします。
どうぞご期待ください!