こんにちは、めのんです!
今年に入ってからATmega328の命令セットシミュレータを作っています。
ゆっくりしたペースですが、少しずつ形になってきていると思います。
今は地道に命令を追加していくだけですので、比較的簡単な命令から順に片付けています。
JMP命令の実装
今回は分岐命令を実装していきます。
分岐命令の中でもとくに簡単な無条件分岐だけを今回は扱うことにします。
無条件分岐命令といえばジャンプ命令です。
AVRのアセンブリ言語では「JMP」というニーモニックが付けられています。
それでは命令の説明をいつもの「AVR®命令一式手引書」から引用します。
今回は実行周期数も重要になりますので、そこまで含めて引用しました。
今回はじめて2ワードの命令が登場しました。
2ワード目には分岐先アドレスの下位16ビットが格納されています。
1ワード目にk16からk21が分散して格納されています。
あわせてk0からk21までの22ビットのオペランドを表現できるようになっています。
といっても、今対象にしているマイコンATmega328のプログラムメモリは0x0000から0x3fffまでしかアドレスがありませんので、2ワード目だけで全アドレスを表現できてしまいます。
ですので、実質的には2ワード目だけをオペランドとして考えれば問題ないことになります。
ただ、将来AVRの他のマイコンにも流用できるように、今回はマジメに実装することにします。
static void jmp(atmega328_t *cpu, uint16_t op)
{
int target = fetch(cpu);
target += (op & 0x0001) << 16 | (op & 0x01f0) << (17 - 4);
cpu->pc = target;
cpu->clock += 3;
}
今回特徴的なのは、命令を処理する関数の中でfetch関数を読んでいることと、クロックカウンタに3を足していることだと思います。
順に見ていきますね。
命令を処理する関数の中からfetch関数を呼び出すのは、本物のハードウェアを模倣する上ではどうかとも思いますが、この方が実装が簡単なのでこうしています。
2ワード目が必要かどうかは命令ごとに変わってきますので、ここで処理するのが一番楽なんです。
fetch関数で読み込んだ2ワード目に(仮引数opとして渡される)1ワード目の一部を合成してターゲットアドレスとしています。
そのあとpcにターゲットアドレスを代入していますね。
分岐というのは、プログラムカウンタに分岐先アドレスを設定するということを意味しています。
こうしてプログラムカウンタに任意のアドレスを設定しておけば、次はそこから命令フェッチが行われるようになります。
次にクロックカウンタです。
いつもは単にインクリメントしているだけだったのですが、今回は3を加算しています。
これはどういうことかというと、先ほど引用した命令の説明の最後に「実行周期表」が合ったと思います。
いつもはここに掲載されている周期数が1になっています(だから省略しています)。
今回は3または利用不可になっていますね。
ATmega328はAVRe+に相当しますので、この表ではAVReの欄を見る必要があります。
周期数が3になっていますので、クロックカウンタに3を足しています。
ざっくりいうと、命令を1ワード読み込むごとに1周期必要で、分岐でさらに1周期必要になるということです。
RJMP命令の実装
次はRJMP命令です。
JMP命令は分岐先のアドレスをダイレクトに指定していましたが、RJMP命令は相対値でアドレスを指定します。
つまり、現在のプログラムカウンタの位置からプラスいくら、またはマイナスいくらを指定してターゲットアドレスを作ります。
それでは説明を引用しますね。
JMP命令と違って今回は1ワードの命令です。
k0からk11の12ビットで-2048から+2047の相対アドレスを指定するようになっています。
今回はビットが分散していないので簡単ですね。
書式のところを見ると、プログラムカウンタが
PC ← PC + k + 1
となっていて、最後に1を足していますね。
これはどういうことかというと、おそらく命令フェッチした直後にプログラムカウンタをインクリメントしてしまうんだと思います(この命令セットシミュレータでもそのように実装しています)。
ですので、インクリメントしたあとのプログラムカウンタにオペランドで指定した-2048から+2047を加算したアドレスに分岐します。
結果として、RJMP命令が格納されているアドレス基準で考えると、-2047から+2048の範囲の分岐ができることになります。
RJMP命令の実行周期は2になっています。
1ワードの命令を読み込むのに1周期、分岐で1周期ということだと思います。
あと回しになってしまいましたが、実装を見てみましょう。
static void rjmp(atmega328_t *cpu, uint16_t op)
{
cpu->pc += (op & 0x0fff) - 0x800;
cpu->clock += 2;
}
今回は素直な実装なのでとくに説明は必要ないでしょう。
op_tableへの登録
では、それぞれの命令をop_tableに登録していきます。
基本的には前回までと同じなんですが、今回は2ワードの命令があるのでそこだけ注意する必要があります。
命令は2ワードでも、2ワード目は純粋なオペランドなので1ワード目だけで何の命令かを特定することができます。
ですので、op_tableに登録する際は1ワード目だけを考えればOKです。
for ($k = 0; $k < 64; ++$k)
{
$opcode_table[0b1001_0100_0000_1100 | ($k & 0b11_1110) << 3 | ($k & 0b0001)] = 'jmp';
}
for ($k = 0; $k < 0x1000; ++$k)
{
$opcode_table[0b1100_0000_0000_0000 | $k] = 'rjmp';
}
これでJMP命令とRJMP命令の実装は完了です。
直値の使用についての考え方
今回はオマケとして、私の直値の使用についての考え方を書いておきたいと思います。
今作っている命令セットシミュレータでは、それぞれの命令を処理する関数の中では普通に直値を記述しています。
見た目だけでこういうのを嫌がる方は結構いらっしゃるのではないでしょうか?
ただ、今の進め方として、それぞれの命令の説明を見ながらひとつひとつ実装していますので、値にマクロなどで名前を付けてコードを分散させるより、直値で記述した方がむしろ可読性が高くなると考えています。
仕様書と付き合わせて机上でデバッグするときにも直値で記述する方が圧倒的に有利です。
そういうわけで、命令の実装には今後も直値を積極的に使っていこうと思います。
これはあくまでも私の考え方ですし、あらゆるケースでそれが正しいわけでもありません。
もし、この命令セットシミュレータの実装に関して「もっとこうした方がいいよ」というご意見がございましたら、ぜひコメント欄で教えてください。
それではまた次回お会いしましょう!