標準TTLだけ(!)でCPUをつくろう!(組立てキットです!)
(ホントは74HC、CMOSなんだけど…)
[第157回]

●タイマールーチンをつくりました

CPUのクロックは大抵は水晶発振です。正確な周波数のクロックが得られます。
これを利用しない手はありません。
といっても、BASICやCではそれほど正確なタイマールーチンを作ることはできません。
ここはマシン語(アセンブラも同じ)の独壇場です。

今回はNOPの利用例として、タイマールーチンを作ってみました。
実行時間が0.5秒のサブルーチンです。
テストプログラムの動作を確認するのに、0.5秒程度の周期でLED表示が点滅するようにできれば、便利かなあ、と思ったからです。
また、正確なパルス出力などにも応用できます。

CPUクロックが2MHzと非常に速いので、いきなり0.5秒を作るのは、ちょっと面倒です。
まずは2.5msのタイマールーチンを作って、それを200回CALLすることで目的の0.5秒のサブルーチンを作ります。
2段階サブルーチンです。

●2.5msのタイマールーチン

0040 F5       PUSH PSW (4)
0041 3EF6     MVI A,F6  (3)
0043 3D       DCR A    (4)
0044 C24300   JNZ 0043 (6) or (4)
0047 00       NOP      (2)
0048 00       NOP      (2)
0049 00       NOP      (2)
004A 00       NOP      (2)
004B F1       POP PSW (4)
004C C9       RET      (4)

命令の後ろの( )はその命令の実行時間です(単位μs)。
「つくるCPU」のCPUクロックは2MHzですから、1マシンクロックは0.5μsです。
各命令のクロック数に0.5μsを掛けたものが、その命令の実行時間になります。
命令のクロック数はタイミングチャートを見ればわかります。

INR r/DCR r命令のクロック数(および命令実行時間)は[第148回]で説明しました(INR/DCR命令の詳しい説明は[第59回]にあります)。
DCR AはAレジスタの値を−1します。
結果が00のときにZフラグが立ちます。また結果の正負によってS(サイン)フラグがON/OFFします。

JNZ命令についても、同じく[第148回]で説明しています(JNZ命令の詳しい説明は[第61回][第63回]にあります)。
Zフラグが立っていなければ、指定したアドレスにジャンプします。
Zフラグが立っているときは、何もしないで、その次の命令に行きます。

RET命令は[第154回]で説明しました(RET命令の詳しい説明は[第98回]にあります)。
このサブルーチンがCALLされた、もとのルーチンに戻ります。
動作としては、スタックから戻り先アドレスを取り出してPC(プログラムカウンタ)にセットします。

NOP命令は前回説明しました。
何もしないで、次の命令に行くという命令です。
クロック数は4クロックです。ですから実行時間は2μsになります。
ちなみに8080での、NOP命令のクロック数も4クロックです。

PUSH PSW命令の説明は[第69回]にあります。
AレジスタとF(フラグレジスタ)をスタックに格納します。
スタックについては[第67回]で説明をしています。
クロック数は8クロックです。実行時間は4μsになります。
8080のPUSH命令のクロック数は11クロックです。

POP PSW命令の説明は[第70回]にあります。
PUSH PSW命令と逆の動作をします。
スタックから取り出した2バイトの値をF(フラグ)レジスタとAレジスタに格納します。
クロック数は8クロックです。実行時間は4μsになります。
8080のPOP命令のクロック数は10クロックです。

MVI A命令の説明は[第49回]にあります。
この命令に続く1バイトの定数値をAレジスタに格納します。
クロック数は6クロックです。実行時間は3μsになります。
8080のMVI r命令のクロック数は7クロックです。

●2.5msタイマールーチンの説明

このルーチンはAレジスタを一定回数ダウンカウントすることで、そのトータル実行時間が2.5msになるようにしています。
PUSH PSWとPOP PSWは、このルーチンがCALLされることによって、Aレジスタ(およびフラグレジスタ)の値や状態を壊してしまわないために、一時的にスタックに退避させるために使っています。
PUSH命令とPOP命令は、このプログラムのように、そのレジスタの値を一時的に保存しておきたいときに使うのが、一般的な使い方です。

MVI A,F6でAレジスタにF6をセットしたあとで、DCR AとJNZ 0043を実行します。
F6は十進数では246です。
DCR A命令でAレジスタの値を−1します。結果が00になるまでは、not Z(zero)ですから、次のJNZ命令で、またDCR Aに戻って、−1されるという動作が繰り返し実行されます。

DCR AとJNZが246回、繰り返し実行されますから、ここの実行時間は、
(4+6)×246=2460μs
になる、と思ってしまいますが、最後の1回はDCR Aの結果、A=00になってZフラグが立ちますから、JNZ命令は条件が不成立になります。このときだけは、指定アドレスにジャンプしないで、次の命令に進みます。実行時間も条件成立時の6μsよりも2μs短い4μsです。
ですからその分を引いて、
2460−2=2458μsになります。

その他の命令は1回実行されるだけですから、それぞれの実行時間を加算したものが、このサブルーチンの実行時間になります。
4+3+2458+2+2+2+2+4+4=2481μs

2.5msルーチンなのですが、計算した結果は2500μsに足りません。
これは、最終的に0.5秒を作るためのパーツなので、0.5秒サブルーチンが2.5msルーチンをCALLするときにかかる時間を考慮しているためです。

●0.5secのタイマールーチン

0030 F5       PUSH PSW (4)
0031 3EC8     MVI A,C8  (3)
0033 CD4000   CALL 0040 (9+2481)
0036 3D       DCR A     (4)
0037 C23300   JNZ 0033  (6) or (4)
003A F1       POP PSW  (4)
003B C9       RET       (4)

プログラムの動作は2.5msルーチンとほとんど同じです。

CALL命令は[第154回]で説明しました(CALL命令の詳しい説明は[第97回]にあります)。
指定したアドレスから始まるサブルーチンをCALLします。
CALL命令の実行時間は9μsですが、CALLされるサブルーチンの実行時間も加算しなければなりません。
CALLされる2.5msサブルーチンの実行時間は2481μsですから、それとCALL命令自身の9μsを加算すると、2490μsになります。

CALL命令の次にはDCR A命令があって、さらにその次にはJNZ命令があります。
DCR Aの結果が00になるまでの間は、0033に戻ってまたCALL命令とDCR A命令とそしてJNZ命令がが繰り返し実行されます。
CALL命令とDCR A命令とJNZ命令が1回実行されるときにかかる時間は、
2490+4+6=2500μs
ちょうど2.5msです。

Aレジスタには、MVI A,C5命令で、C8(十進数の200)が入れられますから、200回のループでは、2.5×200=500msの実行時間となります。
厳密には、JNZ命令の最後の1回分の−2とPUSH PSW、POP PSW、そしてMVI A,C5とRETの分の実行時間を加減算すると、この0.5secサブルーチンは
500−0.002+0.004+0.004+0.003+0.004=500.013ms
になります。
さらにこの0.5secサブルーチンもメインルーチンからCALLされるのですから、そのCALL命令の分も加算すると、正確な実行時間は500.022secになります。

しかし、ここでは、とりあえずは、プログラムサンプルとして、正確な2.5msサブルーチンを作り、そしてそれをCALLして作る、ほぼ正確な0.5secサブルーチンの例をお見せすることが目的ですので、最終的な500msの正確さについては、問わないことにします。

●タイマールーチンでのNOP命令の働き

2.5msサブルーチンをCALLするときに、そのサブルーチン本体の実行時間以外に、CALL命令とDCR A命令とJNZ命令の実行時間が余分にかかることを説明しました。
1回について、4+9+6=19μsです。

2.5msサブルーチンはNOP命令4回を含めて2481μsなので、それに19μsを加算するとちょうど2500μsになります。
では、NOP命令を使わないことにしたら、どうなるでしょうか。

NOP命令は2μsです。それを4回実行していますから、NOP命令の分の実行時間は合計8μsです。
これを使わないことにしますから、2481−8+19=2492μsになって、8μs足りません。
ループ回数を1回増やして、MVI A,F7にしてみたら、どうでしょうか?

DCR AとJNZが1回増えることで、実行時間は4+6=10μs増加します。
2492+10=2502μ
になって、2μsオーバーしてしまいます。

このように、必要な命令の組み合わせだけでは、必要な実行時間を正確に作り出せないときに、NOP命令が時間調節のためのダミー命令として使われます。
なお、NOP命令の実行時間は2μsなので、1μs単位の調節には、そのままでは使うことはできません。
そのような場合には実行時間が奇数μsであるような命令を組み合わせて、ダミー時間を作り出すなどの工夫が必要になります。

アランブラと違って、マシン語のプログラムは、一度書いたプログラムの変更には困難を伴います。
間に命令を挿入するのは最も困難で、通常は別の空きアドレスへのJMP命令で対処します。
逆に途中の命令を削除するような場合に、それが数バイト程度の削除ならば、不要になった命令をNOPで置き換えることで、実質的に削除したと同じ効果を得ることができます。
マシン語プログラムの場合には、そういう目的でも、NOP命令はしばしは用いられることの多い命令です。

以上の説明のように、NOP命令はそれそのものが、いわばムダであることが明白な命令なので、あとからプログラムを見たときでも、そこで使用した目的がはっきりしていてわかりやすいという利点があります。
しかし、わかりやすさ、という利点さえ無視するならば、NOP命令以外にもダミー命令として使える命令はいくつか存在します。

その例は、MOV r,r’の中にもあります。
たとえば、MOV A,A、MOV B,Bなどのように、同じレジスタ間でのMOV命令は、結果としてはフラグを含めてなにも変化しませんから、NOP命令と同じようなダミー命令として使うことができます。
MOV r,r’の実行時間は3μsですから、奇数μsの補正に利用することができます。

MOV r,r’は1バイトの命令ですが、複数の命令を組み合わせてダミーの命令動作をさせることもできます。
たとえば、

PUSH B
POP B

のように同じレジスタに対するPUSH命令とPOP命令を続けて使ったときも、結果は実行時間がかかるだけで、何も変化しませんから、実行時間を増やすという特殊な目的では有効な使い方といえます。

さて、このあと、上で作った0.5secタイマールーチンをCALLする、XCHG命令他のテストプログラムの説明をしようと思ったのですが、サブルーチンの説明に時間がかかってしまって、本日はもう時間がなくなってしまいました。
XCHGのテストプログラムとその実行結果については、また次回に説明することにいたします。
2009.2.8upload

前へ
次へ
ホームページトップへ戻る