MCS-51

MCS-48の成功をうけ、さらなる高性能をめざして開発されたのがMCS-51シリーズでした。タイマを強化し、シリアルインターフェースを付加し、命令も強力にして高速化しています。
命令の強化については、プログラムメモリとデータメモリ空間をそれぞれ16 bitアドレスに拡張し、16 bitポインタレジスタを追加して、乗除算やビット演算を含む高度な命令が追加されています。1 - 3 Byteの可変長命令が使用され、8 bitマイクロプロセッサとしては普通の設計といえるでしょうか。ビット演算はキャリーフラグを一種のアキュムレータとして、内部データメモリの一部とI/Oポートやレジスタの一部をビット単位で指定して演算や条件分岐の判定条件として使用できるようになっています。8048と比べて高速化も行われ、12 MHzクロックで1 usの命令実行時間と約2.5倍のスピードアップとなっています。
タイマは16 bitタイマが2本(数種類のカウンタ構成をプログラム可能)となって使いやすくなり、シリアルインターフェースもSPI風のクロック同期や9 bitデータ構成の調歩同期方式も可能となっています。ボーレートはタイマの一部を使用して内部で生成することができ、9 bitデータ構成の場合に受信データの9 bit目が1の場合にのみ割り込みを発生させることもできます。このため、一種の簡易LANのようなことも内蔵I/Oだけで効率良く実現できます。
I/Oポートは32本まで使用できます。シリアルやタイマ回路の機能ピンとパラレル入出力ポートの端子の一部が兼用になっています。また、外部にデータメモリやプログラムメモリを拡張することも可能ですが、その場合もパラレル入出力ポートの一部というか半分ほどがメモリインターフェースに使用されてしまいます。
割り込みも強化されており、5要因の割り込みを受け付ける他、割り込み要因に2レベルの優先順位を付けることも可能です。
まぁ、そんなこんなで、組み込みだからプログラミングが不便でもしかたがないなんて感じさせない程度に普通のマイクロプロセッサとしてプログラミングできる、比較的素直なシングルチップマイクロコンピュータです。

87C51
これはCMOS版で紫外線消去型のPROMを内蔵した87C51です。

8051は、8048と比べるとセカンドソースが少なく、特に製造数量の少ない応用や試作に便利なUV EPROM搭載の8751が高価(8748が1000円の時代に5000 - 10000円くらい)だったこともあって、普及のペースはかなり遅れました。ただ、最近になってUSBインターフェース回路を内蔵したものが8051シリーズに加わったりしていますから、もしかすると手元のUSBキーボードなんかにひっそりと使われているかもしれません。
かえって、内蔵ROMを削除された8031 (80C31)の方が、ROMやI/Oを増設することになっても小規模の組み込み用には外付け回路規模が8085と変わらないわりに割り込み応答などの能力が8085なんかより高かったりしたうえ、安価なプラスチックパッケージ品が提供されていたので、少量生産の応用などには多く使われていた傾向があります。

8051のピン配置はこうなっています。

      P1.0  1       40 VCC
      P1.1  2       39 P0.0 AD0
      P1.2  3       38 P0.1 AD1
      P1.3  4       37 P0.2 AD2
      P1.4  5       36 P0.3 AD3
      P1.5  6       35 P0.4 AD4
      P1.6  7       34 P0.5 AD5
      P1.7  8       33 P0.6 AD6
     RESET  9       32 P0.7 AD7
  RxD P3.0 10       31 EA*/VPP
  TxD P3.1 11       30 ALE/PROG*
INT0* P3.2 12       29 PSEN*
INT1* P3.3 13       28 P2.7 A15
   T0 P3.4 14       27 P2.6 A14
   T1 P3.5 15       26 P2.5 A13
  WR* P3.6 16       25 P2.4 A12
  RD* P3.7 17       24 P2.3 A11
     XTAL1 18       23 P2.2 A10
     XTAL2 19       22 P2.1 A9
       VSS 20       21 P2.0 A8

P1.0というのはポート1のビット0の意味で、ポート0からポート3までの8 bit幅のI/Oピンが存在します。これらのポートは擬似双方向ポートで、ビット単位で入力にも出力にも使用できます。さらにポート1以外は別の役割を持たされています。そのおかげで、40ピンしかない端子のうち32本をI/O端子に割り当てることが可能になっています。つまり残りの端子は8本。電源に2本、クロック発振に2本、リセットに1本と、この5本の端子はなかなか省略しにくいわけです。残りの3本の端子はI/Oやメモリの拡張に使用されていて、32本のI/Oというのは最大に近い本数になっています。
P0のAD0 - 7とP2のA8 - 15は外部にプログラムメモリやデータメモリを拡張する際のデータバスとアドレスバスの役割を果たします。アドレス下位8 bitとデータバスは兼用で時分割で使用されます。そのアドレス出力タイミングを表すのが30番ピンのALE信号です。プログラムメモリを読み出すタイミングは29番ピンのPSEN信号が示します。EA信号は内部プログラムメモリの読み出しを禁止して外部のプログラムメモリからコードを読み込ませるための信号です。8031のように内部プログラムメモリの存在しないモデルではGNDに接続します。また、ALE信号とEA*信号はEPROM内蔵の8751の内蔵ROMにプログラムを書き込む場合には書き込みタイミングを表すPROG*信号と書き込み用高圧電源のVPP端子としても使用されます。
外部データメモリを使用する場合にはタイミング信号としてP3.6とP3.7のWR*とRD*信号が使用されます。外部データメモリを使用しない場合に限り、汎用のI/OピンとしてP3.6とP3.7を使用することができます。
RESET信号は正論理のリセット信号。XTAL1とXTAL2に水晶発振子と2個の負荷容量を接続することによってクロック信号が生成されます。
さて、汎用のパラレルI/Oの他に、8051の内部にはシリアルインターフェース1回路と16 bitタイマ・カウンタが2回路、割り込みコントローラなどが内蔵されています。
そのシリアルインターフェースに対応する端子がP3.0とP3.1のRxDとTxDです。シリアルインタフェースはクロック同期のシフトレジスタ型の他、調歩同期シリアル転送も可能になっています。クロック同期の場合、TxDがクロック信号でRxDがシリアルデータの入出力端子になります。転送速度は内部で固定されていて、水晶発振子の周波数の1/12になります。調歩同期の場合はRxDとTxDがそれぞれ受信データと送信データとして使われ、転送速度は内部固定のモードも、タイマ1をビットレートジェネレータに使用することも可能です。また9 bitデータの転送も可能になっていて、9番目のビットをパリティビットとして使用することも、一種のアドレスマークとして使用することも可能になっています。アドレスマークとして使用する場合、受信時にアドレスマークがセットされていないデータでは受信割り込みを発生させないことも可能で、一種のLAN的なデータ転送を実装することが可能です。
タイマは2チャンネル内蔵されていて、それぞれ16 bitのカウンタやタイマとして使用できます。対応する入出力ピンはP3.2からP3.5までの4本です。T0とT1はそれぞれタイマ0とタイマ1のカウンタ入力です。この端子のパルス数を数えることが可能です。INT0*とINT1*は外部割り込み入力としても使用されますが、タイマ・カウンタの入力端子として使用する場合はそれぞれタイマ0とタイマ1のゲート信号になります。INT0*あるいはINT1*信号がHの間だけ、対応するタイマ・カウンタがカウントアップするように設定できます。うまく使えば1 us単位でINT0*信号などに入力されたパルス幅を計測することができます。
タイマ自体は4種類のモードがあり、16 bit幅のカウンタとしても、8 bit幅のオートロード方式のタイマとして使用したりすることもできます。タイマが0になったときの出力信号は用意されていませんが、その時点で割り込みを発生することができます。
INT0*とINT1*は外部割り込み入力として使用できます。ネガティブエッジで割り込みをかけるようにも、レベルで割り込みをかけるようにも、設定できます。このほか、タイマからの割り込みがそれぞれのチャンネルごとに使用でき、シリアルインターフェースの送信割り込みと受信割り込みも可能です。割り込みは0003Hから0023Hまでの、割り込み要因によって固定されたアドレスへのサブルーチン呼び出しのような形で実行されます。

内部レジスタをどのように説明しようか、少々悩みました。8048でI/Oポートにアドレスを割り振らずに専用命令を用意した結果、命令体系が繁雑になったのを反省したのか、8051ではI/Oポートは内部メモリ空間の一部にアドレスを与えられて配置されています。しかも、一般の内部レジスタについても、同じようにアドレスを割り当てて、メモリ参照命令でアクセスすることもできるようになっています。たとえばAレジスタには0E0Hというアドレスが割り当てられています。シングルチップコンピュータはマイクロプロセッサとメモリとI/Oポートをひとつのチップに集積したものです。8051ではI/Oポートとレジスタ類がすべて同じように見えるため、プロセッサ内部にあるとすべきか、I/O機能の一部であると考えるべきか、少々判別の難しいレジスタがあったりしますので。下の図は、命令体系に組み込まれていると思われるレジスタを集めたものです。
8051 registers
Acc.はアキュムレータで、単にAレジスタと呼ぶこともあります。Bレジスタを使用する命令はほとんどありませんが、乗除算のMUL命令とDIV命令でAレジスタと共に使用されます。DPTRはポインタレジスタとしてプログラムメモリやデータメモリのメモリアドレスを指示するのに使われます。8 bitアドレスで済む場合は、R0とR1をポインタとして使用することも可能です。SPはスタックポインタです。サブルーチン呼び出しや割り込み発生時にプログラムカウンタを退避するのに使用します。スタックとして使用できるのは内部データメモリだけなので、SPは8 bit幅のレジスタです。内部データメモリは8051で128 Byte、8052で256 Byteですし、その一部しかスタックとして使用しないはずですから、これで充分です。PCはプログラムカウンタで、唯一、これだけにはアドレスが割り当てられていません。その必要がないためでしょうね。プログラムメモリ空間は16 bitありますから、PCも16 bitレジスタです。
PSWはプログラムステータスワードで、フラグ類が集められています。CYがキャリーフラグ、ACがBCD演算用の補助キャリーで、OVがオーバーフローフラグ、Pがパリティフラグです。F0はフラグ0と呼ばれ、ユーザが自由に使用できます。*とマークされているビット1も名前はありませんが、ユーザが自由に定義して使用できます。RS1とRS0はレジスタバンク切り替えビットで、4セットのスクラッチパッドレジスタを切り替えて使用できます。
そのレジスタバンクが図の右半分にあります。バンク0からバンク3までの4セットに、それぞれR0からR7までの8個のレジスタが含まれています。図には内部データメモリとして参照する場合のアドレスも記載されています。このレジスタは、内部データメモリの一部を短い命令で参照するための工夫と理解すべきもので、たとえば8051に内蔵の128 Byteの内部データメモリのうちの32 Byte分をしっかりと使用しています。スクラッチパッドレジスタとは独立したメモリが別に128 Byte確保されているわけではないので注意してください。もちろん使用しないレジスタバンクはデータメモリとして自由に使用できます。サブルーチンなどのローカル変数のように特定のレジスタバンクのレジスタを使うほか、割り込み時などにうまくレジスタバンクを切り替えることにすれば、コンテキストの保存や復帰が容易に高速に行えます。
このほかに割り込みマスク用のレジスタも内部レジスタとすべきか悩みましたが、ここではI/O側の機能として扱うことにしました。
レジスタ類をメモリマップ方式にしたために、スタックポインタに値を設定したり、たまにしか使われないBレジスタのロードやストアなんかに専用の命令を割り当てる必要がなくなりました。すべてダイレクトアドレッシングモードのメモリ参照命令で設定やら保存やらが行なえます。メモリアドレスの指定のために1 Byte分の命令オペランドが必要になって、専用命令を使うより命令のバイト数が増える傾向がありそうですが、スタックポインタやBレジスタの操作なんてほとんど必要ありませんから、問題にはなりません。Aレジスタなんかには頻繁に使用されるので、専用の短い命令が用意されています。それでもメモリアドレスが割り振られていること、しかもAレジスタやPSWはビット演算が可能なアドレスに割り当てられていることによって、メモリアドレスとビット位置を指定して特定のビットを操作するビット演算命令を用いてAレジスタの特定のビットやPSWに納められているフラグのひとつをセットしたりリセットしたりすることもできますし、条件分岐の条件として使用することもできます。さすがにキャリーフラグを条件としての分岐命令は専用の短い命令コードが割り当てられていますけれど。

それでは個々の命令を見ていきましょう。スペースの都合で、命令の第1バイトは2進数で、第2、第3バイトは16進数的な表記になっています。命令の各バイトで小文字で表されているものの意味は表の後で説明します。

まずは算術演算命令について。

アセンブラ表記      第1バイト  第2 第3 clk   解説
ADD  A, Rn          0010 1rrr          12  Add register to Accumulator
ADD  A, direct      0010 0101  aa      12  Add direct byte to Accumulator
ADD  A, @Ri         0010 011i          12  Add Indirect RAM to Accumulator
ADD  A, #data       0010 0100  dd      12  Add immediate data to Accumulator
ADDC A, Rn          0011 1rrr          12  Add register to Accumulator with Carry
ADDC A, direct      0011 0101  aa      12  Add direct byte to Accumulator with Carry
ADDC A, @Ri         0011 011i          12  Add Indirect RAM to Accumulator with Carry
ADDC A, #data       0011 0100  dd      12  Add immediate data to Accumulator with Carry
SUBB A, Rn          1001 1rrr          12  Subtract register from Acc with borrow
SUBB A, direct      1001 0101  aa      12  Subtract direct byte from Acc with borrow
SUBB A, @Ri         1001 011i          12  Subtract Indirect RAM from Acc with borrow
SUBB A, #data       1001 0100  dd      12  Subtract immediate data from Acc with borrow
INC  A              0000 0100          12  Increment Accumulator
INC  Rn             0000 1rrr          12  Increment register
INC  direct         0000 0101  aa      12  Increment direct byte
INC  @Ri            0000 011i          12  Increment indirect RAM
DEC  A              0001 0100          12  Decrement Accumulator
DEC  Rn             0001 1rrr          12  Decrement register
DEC  direct         0001 0101  aa      12  Decrement direct byte
DEC  @Ri            0001 011i          12  Decrement indirect RAM
INC  DPTR           1010 0011          24  Increment Data Pointer
MUL  AB             1010 0100          48  Multiply A & B
DIV  AB             1000 0100          48  Divide A by B
DA   A              1101 0100          12  Decimal Adjust Accumulator

ここでアセンブラ表記の項に含まれるオペランドのRnはR0からR7までのレジスタを表します。具体的に内蔵データメモリのどこが参照されるかは、前述のようにPSW内のRS1とRS0で決まります。同じくオペランドのdirectは内蔵データメモリ空間の0000Hから00FFHまでの256 Byteの領域を8 bitの絶対アドレスで指定するものです。0000Hから007FHまでは一般的なメモリが(レジスタとして使用されている領域も含まれますが)割り当てられていて、0080Hから00FFHまではI/Oポートや特殊レジスタ類が割り当てられています。オペランドの@Riは実際には@R0と@R1のどちらかが使われます。それぞれR0かR1の内容が示す0000Hから00FFHまでの内蔵データメモリ空間に含まれる特定のバイトの内容を参照します。@RiではI/Oポートや特殊レジスタにはアクセスできず、同じアドレスに割り当てられている一般的なメモリを参照するという点に注意が必要です。MCS-51アーキテクチャでは、内蔵データメモリ空間の0080Hから00FFHには、通常のメモリと特殊レジスタが2重に割り当てられていて、ダイレクトアドレッシングモードでは特殊レジスタが、間接アドレッシングモードでは通常のメモリが、アクセスされます。なお、スタックポインタなどで参照する場合も間接アドレッシングの一種として扱われます。オペランドの#dataは8 bitのイミディエートデータです。
命令の第1バイトの中のrrrはレジスタ番号を意味し、000から111までの8通りの値を取ります。iは@Riのiと同じで、0か1のどちらかの値が入ります。第2バイトのaaは8 bitのダイレクトアドレス、ddは8 bitのイミディエートデータを意味します。
ADD, ADDC, SUBB, INC, DEC命令は、INCとDEC命令でイミディエートアドレッシングが無意味なために代わりにアキュムレータ指定となっている以外は、命令とアドレッシングモードが比較的きれいな直交性を持っています。ただ、ボローの関係しないSUB相当の命令が省略されて存在しないという点は変わっているかもしれません。まぁ、MCS-48では減算命令自体存在しませんでしたから。後述のCLR C命令でキャリーフラグを操作してSUBB命令を実行すればSUB命令相当になりますからね。8 bitポインタ操作で定数の減算が必要な場合、その定数の補数をADD命令で加算してしまえば良いわけですし、SUB命令の分、次に示す論理演算命令やビット操作命令を充実させた方が組み込み用のプロセッサとしては有利と考えられたのでしょう。
MCS-51で特徴的なのが命令実行クロック数で、12クロックの整数倍となっているのは内部回路の都合でしょうが、1 Byte命令も2 Byte命令も、あまり実行クロック数に影響を与えていないようです。なお、8051の最高動作クロック周波数は12 MHzですから、1 usが最短命令実行時間となります。
ADD, ADDC, SUBB命令ではCY, OV, ACフラグが変化します。INC命令とDEC命令ではフラグは変化しません。
INC DPTR命令は16 bitポインタのDPTRのインクリメントを行いますが、デクリメントを行う専用命令はありません。
MUL AB命令はAとBの内容をそれぞれ8 bit無符号整数として、A×Bを求めます。積の下位バイトがAに、上位バイトがBに入ります。Bが0以外なら(積が256以上なら)OVフラグがセットされます。CYフラグはクリアされます。
DIV AB命令もAとBの内容をそれぞれ8 bit無符号整数として、A/Bを求めます。商がAに、余りがBに入ります。命令実行前にBに0が入っていると、AとBの内容は不定となり、OVフラグがセットされます。ここでもCYフラグはクリアされます。
DA A命令はBCD加算補正命令です。BCD形式の数値の加算(ADD, ADDC)の直後に実行すれば、加算結果を正常なBCD形式に補正します。減算には適用できません。補正後の状態に応じてCYフラグが変化します。

次は論理演算命令について。

アセンブラ表記      第1バイト  第2 第3 clk   解説
ANL  A, Rn          0101 1rrr          12  AND Register to Accumulator
ANL  A, direct      0101 0101  aa      12  AND direct byte to Accumulator
ANL  A, @Ri         0101 011i          12  AND indirect RAM to Accumulator
ANL  A, #data       0101 0100  dd      12  AND immediate data to Accumulator
ANL  direct, A      0101 0010  aa      12  AND Accumulator to direct byte
ANL  direct, #data  0101 0011  aa  dd  24  AND immediate data to direct byte
ORL  A, Rn          0100 1rrr          12  OR Register to Accumulator
ORL  A, direct      0100 0101  aa      12  OR direct byte to Accumulator
ORL  A, @Ri         0100 011i          12  OR indirect RAM to Accumulator
ORL  A, #data       0100 0100  dd      12  OR immediate data to Accumulator
ORL  direct, A      0100 0010  aa      12  OR Accumulator to direct byte
ORL  direct, #data  0100 0011  aa  dd  24  OR immediate data to direct byte
XRL  A, Rn          0110 1rrr          12  Exclusive-OR Register to Accumulator
XRL  A, direct      0110 0101  aa      12  Exclusive-OR direct byte to Accumulator
XRL  A, @Ri         0110 011i          12  Exclusive-OR indirect RAM to Accumulator
XRL  A, #data       0110 0100  dd      12  Exclusive-OR immediate data to Accumulator
XRL  direct, A      0110 0010  aa      12  Exclusive-OR Accumulator to direct byte
XRL  direct, #data  0110 0011  aa  dd  24  Exclusive-OR immediate data to direct byte
CLR  A              1110 0100          12  Clear Accumulator
CPL  A              1111 0100          12  Complement Accumulator
RL   A              0010 0011          12  Rotate Accumulator Left
RLC  A              0011 0011          12  Rotate Accumulator Left through the Carry
RR   A              0000 0011          12  Rotate Accumulator Right
RRC  A              0001 0011          12  Rotate Accumulator Right through the Carry
SWAP A              1100 0100          12  Swap nibbles within the Accumulator

ANL, ORL, XRLは算術演算命令とほぼ同じようなアドレッシングモードを持ち、比較的直交性の良い命令体系になっています。ただし、算術演算命令に許されたアドレッシングモードの他、ディスティネーションがダイレクトアドレッシングで指定されるモードが2種類追加されています。ひとつはソースがアキュムレータとなるもので、もうひとつはイミディエートデータを指定するものです。特に後者のイミディエートデータでダイレクトアドレスで指定されたデータを書き換えられるモードは強力です。ダイレクトアドレスで指定される範囲にはI/Oポートも割り当てられていますから、アキュムレータの内容を変化させずに直接I/Oを操作することが可能です。I/Oポートの特定のビットを変化させるだけなら後述のビット操作命令で可能ですが、こちらの論理演算命令を利用すると複数ビットを同時に変化させることができます。そのため、論理演算命令には算術演算命令になかったこれらのアドレッシングモードを特に用意したのでしょう。
ローテート命令はありますが、算術シフトなどの命令はありません。
SWAP命令はアキュムレータの内容の上位4 bitと下位4 bitを入れ替える命令です。主にBCD表現のデータ操作に利用します。たとえば、Intel社の資料には、アキュムレータに100未満のバイナリデータが入っているとして、それをBCD表示に変換する次のようなコード例があげられています。

MOV   B, #10
DIV   AB
SWAP  A
ADD   A, B

アセンブリ言語ソースを示すのは初めてですから注意しておきますが、最初のMOV命令と最後のADD命令のオペランドのBレジスタは、アセンブラが自動的にダイレクトアドレッシングとして処理します。Bレジスタ専用の特別なアドレッシングモードがあるわけではなく、特殊レジスタのひとつとして0F0Hというアドレスが割り当てられていますから、そのアドレスを持つ予約ラベルの一種として処理されるわけです。
RLC命令とRRC命令でCYフラグが変化するほか、論理演算命令ではフラグが変化することはありません。もちろん、ダイレクトアドレッシングでPSWの含まれるアドレスを書き換えた場合はこの限りではありませんけど。

次はデータ転送命令について。

アセンブラ表記      第1バイト  第2 第3 clk   解説
MOV  A, Rn          1110 1rrr          12  Move register to Accumulator
MOV  A, direct      1110 0101  aa      12  Move direct byte to Accumulator
MOV  A, @Ri         1110 011i          12  Move indirect RAM to Accumulator
MOV  A, #data       0111 0100  dd      12  Move immediate data to Accumulator
MOV  Rn, A          1111 1rrr          12  Move Accumulator to register
MOV  Rn, direct     1010 1rrr  aa      24  Move direct byte to register
MOV  Rn, #data      0111 1rrr  dd      12  Move immediate data to register
MOV  direct, A      1111 0101  aa      12  Move Accumulator to direct byte
MOV  direct, Rn     1000 1rrr  aa      24  Move register to direct byte
MOV  direct, direct 1000 0101  ss  aa  24  Move direct byte to direct (ss) -> (aa)
MOV  direct, @Ri    1000 011i  aa      24  Move indirect RAM to direct byte
MOV  direct, #data  0111 0101  aa  dd  24  Move immediate data to direct byte
MOV  @Ri, A         1111 011i          12  Move Accumulator to indirect RAM
MOV  @Ri, direct    1010 011i  aa      24  Move direct byte to indirect RAM
MOV  @Ri, #data     0111 011i  dd      12  Move immediate data to indirect RAM
MOV  DPTR, #data16  1001 0000  dd  dd  24  Load Data Pointer with a 16 bit constant
MOVC A, @A + DPTR   1001 0011          24  Move Code byte relative to DPTR to Acc
MOVC A, @A + PC     1000 0011          24  Move Code byte relative to PC to Acc
MOVX A, @Ri         1110 001i          24  Move External RAM (8 bit addr.) to Acc
MOVX A, @DPTR       1110 0000          24  Move External RAM (16 bit addr.) to Acc
MOVX @Ri, A         1111 001i          24  Move Acc to External RAM (8 bit addr.)
MOVX @DPTR, A       1111 0000          24  Move Acc to External RAM (16 bit addr.)
PUSH direct         1100 0000  aa      24  Push direct byte onto stack
POP  direct         1101 0000  aa      24  Pop direct byte from stack
XCH  A, Rn          1100 1rrr          12  Exchange register with Accumulator
XCH  A, direct      1100 0101  aa      12  Exchange direct byte with Accumulator
XCH  A, @Ri         1100 011i          12  Exchange indirect RAM to Accumulator
XCHD A, @Ri         1101 011i          12  Exchange low order Digit indirect RAM with Acc

MOV命令はもっとも多様なオペランドの組み合わせを取ります。特にディスティネーションがダイレクトアドレッシングの時に5種類のソースを選べるのに注意してください。ダイレクトアドレッシングでは特殊レジスタも参照できますから、たとえばMOV direct, #dataでアキュムレータなどに影響を与えずに出力ポートなどの特殊レジスタに値を直接設定することができます。なおMOV direct, direct命令の第2バイトのssはソースアドレス、第3バイトのaaはディスティネーションアドレスを表します。
MOV DPTR, #data16命令はDPTRに定数を入れるための命令です。DPTRにも特殊レジスタとして上位バイト(DPH)と下位バイト(DPL)別々に名称とダイレクトアドレスが割り当てられていますから、MOV direct, #data形式の命令を2回続けて値を設定することもできます。もっともコードが冗長になるため、専用命令が用意されたのでしょう。ただし実際のコードには注意が必要で、命令の第2バイトに定数の上位バイトが、第3バイトに下位バイトが配置されます。なんと8080なんかとは逆になっていますから、注意が必要です。もっともアセンブリ言語でプログラムを書いていればアセンブラが勝手にやってくれることですから問題はありませんけどね。デバッグをする際にはコードを直接読むこともあるかもしれませんし、アセンブラを開発する人なんかも注意すべきでしょう。
MOVC命令はプログラムメモリに定義された表を参照する命令で、表の開始アドレスないし基準アドレスをDPTRやPCとすることができます。アキュムレータを表のインデックスとしてアドレス修飾に利用しますが、そのアキュムレータの内容が変化するので、連続した表引きには不便なところもありますか。プログラムメモリの内容をデータとして参照する命令はMOVC命令だけです。
MOVX命令は外部データメモリを参照する命令で、8 bitポインタを用いるものと16 bitポインタを用いるものの2種類があります。8 bitポインタ参照だけを利用すれば、上位8 bit分のアドレス出力は行われませんので、ポート2をI/Oポートとして利用できます。16 bitポインタ参照を行うとポート2には上位アドレスが出力されることになります。外部データメモリを利用する命令はMOVXだけです。
PUSH命令やPOP命令はダイレクトアドレッシングだけです。しかし、たとえばアキュムレータやPSWにもダイレクトアドレスが割り当てられていますから、PUSH AとかPOP PSWと表記することも可能で、アキュムレータの内容をプッシュしたり、ポップしたデータをPSWに格納することができます。MOV命令や演算命令ほど利用頻度が高くないだろうということで、アキュムレータやレジスタを直接指定するアドレッシングモードが省略されたのでしょう。PUSH命令では、まずスタックポインタをインクリメントしてからデータをスタックに入れます。POP命令はデータをスタックから転送した後に、スタックポインタをデクリメントします。8080などの他のマイクロプロセッサと異なり、スタックの伸びる向きがアドレスの増加する向きになっていることに注意が必要です。またデータ転送とスタックポインタの変更順序から、スタックポインタは常にスタックトップのデータを指していることもわかります。スタックポインタの初期値には、スタックとして用いる予定のメモリ領域の先頭アドレスマイナス1を与える必要があります。ところでスタックポインタにもダイレクトアドレスが割り当てられていますから、POP SPという命令も実行できます。スタックトップに40Hが入っていたとすると、先ほどの順序だとスタックポインタに40Hが転送されてからデクリメントるため3FHになりそうですが、実は40Hになります。スタックからのデータ転送はとりあえずプロセッサ内部のテンポラリレジスタに入れられて、スタックポインタがデクリメントされてから、テンポラリレジスタからスタックポインタへの転送が実際に行われるためです。
XCH命令は一方向へのデータ転送を行うMOV命令と異なり、アキュムレータともうひとつのオペランドの内容を入れ替える命令です。たとえばアキュムレータの内容を変更せずにあるレジスタの内容に定数を加えることを考えてみましょう。8080的な考え方ならアキュムレータをプッシュしてレジスタ操作をして、というような考え方で、このようなコードになりますか。

PUSH  A
MOV   A, R0
ADD   A, #10
MOV   R0, A
POP   A

もっとも8080と異なりPUSH/POPは2 Byte命令で、ちょっと長いコードになってしまいます。空いているレジスタがあれば、そこに退避すると多少短くはなります。こんなときXCH命令があれば、アキュムレータの退避場所として操作すべきレジスタ自身を利用できます。

XCH   R0
ADD   A, #10
XCH   R0

XCH命令はPUSH/POP命令より実行時間も短いし、コードサイズも短縮されることがわかります。
XCHD命令はアキュムレータの下位4 bitと8 bitポインタで指されたメモリの下位4 bitを入れ替える命令です。BCD表現のデータ操作などに利用します。

次はビット操作命令です。

アセンブラ表記      第1バイト  第2 第3 clk   解説
CLR  C              1100 0011          12  Clear Carry
CLR  bit            1100 0010  bb      12  Clear direct bit
SETB C              1101 0011          12  Set Carry
SETB bit            1101 0010  bb      12  Set direct bit
CPL  C              1011 0011          12  Complement Carry
CPL  bit            1011 0010  bb      12  Complement direct bit
ANL  C, bit         1000 0010  bb      24  AND direct bit to Carry
ANL  C, /bit        1011 0000  bb      24  AND complement of direct bit to Carry
ORL  C, bit         0111 0010  bb      24  OR direct bit to Carry
ORL  C, /bit        1010 0000  bb      24  OR complement of direct bit to Carry
MOV  C, bit         1010 0010  bb      12  Move direct bit to Carry
MOV  bit, C         1001 0010  bb      24  Move Carry to direct bit
JC   rel            0100 0000  rr      24  Jump if Carry is set
JNC  rel            0101 0000  rr      24  Jump if Carry is not set
JB   bit, rel       0010 0000  bb  rr  24  Jump if direct bit is set
JNB  bit, rel       0011 0000  bb  rr  24  Jump if direct bit is not set
JBC  bit, rel       0001 0000  bb  rr  24  Jump if direct bit is set & clear bit

ビット操作命令ではアキュムレータの役割をCYフラグが受け持っています。また、普通の演算命令で使われていたダイレクト(バイト)アドレッシングの代わりにダイレクトビットアドレッシングを利用できます。これ以外のビットアドレッシングモードはありません。さて、ダイレクトビットアドレッシングでは、8 bitで直接アドレスを指定します。1 Byteの中のビットを指定するのに3 bit必要ですから、アドレス指定には5 bit分、つまり32 Byteのアドレス範囲だけを指定できます。実際にはその32 Byteを、ダイレクトバイトアドレッシングと同じように、一般のメモリと特殊レジスタの領域のふたつに分けています。ビットアドレスのMSBが0だと、メモリアドレス0020Hから002FHまでの16 Byte領域に含まれる128 bitの任意の1 bitをアドレスします。ビットアドレスのMSBが1だと、特殊レジスタアドレスを2進数表示して1xxx x000Bという形になるもの、つまり16進数表示で080H, 088H, 090H, ..., 0F0H, 0F8Hの16 Byteに含まれる128 bitの任意の1 bitをアドレスします。このアドレスには、アキュムレータやBレジスタ、ポート0, 1, 2, 3の入出力レジスタ、割り込み制御用のレジスタなど、ちょうどビット操作が必要だろうと考えられるレジスタを選んで割り当てています。
命令の第2バイトに配置されるbbに相当するビットパターンは、メモリアドレス20Hから2FH、つまり2進数の0010 xxxxBというビットパターンで表現されるメモリ領域に関しては、ビット番号を2進数でbbbと表現すると、0xxx xbbbBという値になります。つまりMSBが0で、メモリアドレスの下位4 bitを3 bit分だけ左シフトしたものの下位3 bitにビット番号を合成した数値となります。また特殊レジスタの場合、ビットアドレス表現可能なレジスタのアドレスは1xxx x000Bで表現されますが、その下位3 bitにビット番号を合成したものがダイレクトビットアドレッシングを利用する命令の第2バイトに置かれます。
なお、アセンブリ言語でビットオペランドを記述する場合、LABEL.bitの形式で表現できます。たとえば、A.3と記述すればアキュムレータのビット3で、P1.6と記述すればポート1の入出力レジスタのビット6という意味になります。XXXというラベルが23Hという値を持っていれば、XXX.1で23Hのメモリのビット1になります。XXXに30Hなどビットアドレッシングの不可能なアドレスが入っていれば、普通はアセンブラが警告を出力するなりエラーを検出するなりするでしょう。また、ビットを表現するシンボルも定義できるはずです。そういう意味で、アセンブリ言語を用いる限りはどのレジスタやメモリがダイレクトビットアドレッシング可能か注意するだけで、結構自然にビット操作命令を利用できます。
CLR, SETB, CPL命令はビットに対する単項演算です。CLRは指定されたビットに0を、SETBは1を入れ、CPL命令は指定されたビットを反転します。
2項演算命令としては論理積と論理和を行うANL命令とORL命令が用意されていますが、オペランドの頭に/を記述するとオペランドの反転した値に対する演算になります。これは結構便利です。
MOV命令でアキュムレータに相当するCYフラグへのロードとストアが行えます。こちらは反転値をロードできません。
残りの5命令は特定のビットを条件にした分岐命令です。8 bitの相対アドレッシングモードで分岐先を指定します。これらの分岐命令の次の命令の先頭アドレスを基準にして-128から+127の範囲への分岐が可能です。
JC命令とJNC命令はCYフラグに関する条件分岐命令で、JB命令とJNB命令はダイレクトビットアドレッシングで条件判定に使用するビットを指定します。
JBC命令はJB命令と似ていますが、分岐とともにビット操作も行います。指定されたビットが1なら、そのビットを0にクリアしてから分岐します。指定したビットが0なら、分岐もクリア操作も行いません。

最後に(一般の)分岐命令を見ることにしましょう。

アセンブラ表記      第1バイト  第2 第3 clk   解説
ACALL addr11        aaa1 0001  aa      24  Absolute Subroutine Call
LCALL addr16        0001 0010  aa  aa  24  Long Subroutine Call
RET                 0010 0010          24  Return from Subroutine
RETI                0011 0010          24  Return from interrupt
AJMP addr11         aaa0 0001  aa      24  Absolute Jump
LJMP addr16         0000 0010  aa  aa  24  Long Jump
SJMP rel            1000 0000  rr      24  Short Jump (relative addr.)
JMP  @A + DPTR      0111 0011          24  Jump indirect relative to DPTR
JZ   rel            0110 0000  rr      24  Jump if Accumulator is Zero
JNZ  rel            0111 0000  rr      24  Jump if Accumulator is not Zero
CJNE A, direct, rel 1011 0101  aa  rr  24  Compare direct byte to Acc and Jump if not Equal
CJNE A, #data, rel  1011 0100  dd  rr  24  Compare immediate to Acc and Jump if not Equal
CJNE Rn, #data, rel 1011 1rrr  dd  rr  24  Compare immediate to register and Jump if not Equal
CJNE @Rn,#data, rel 1011 011i  dd  rr  24  Compare immediate to indirect and Jump if not Equal
DJNZ Rn, rel        1101 1rrr  rr      24  Decrement register and Jump if not Zero
DJNZ direct, rel    1101 0101  aa  rr  24  Decrement direct byte and Jump if not Zero
NOP                 0000 0000          12  No Operation

ACALL命令とLCALL命令はサブルーチン呼び出し命令です。ACALLのオペランドにはaddr11と書いてありますが、11 bit分のアドレス表現を意味します。スタックに戻り番地を退避してから、PCの上位5 bitは変えずに、下位11 bitだけが変更されます。命令コードは2 Byteですが、第1バイトの上位3 bitが目的PCのビット10からビット8に相当し、第2バイト全体が下位8 bitに相当します。プログラムメモリに2 KByte単位のページを想定することにすれば、ページ内サブルーチン呼び出しと考えられます。オリジナルの8051の内蔵ROMが4 KByteですから、まぁそれほど狭いページというわけではありませんが、プログラミングの際にはモジュール配置をある程度意識しないといけないことは確かです。なお、ページの基準点はACALL命令を実行しているときのPCの値、つまりACALL命令の次の命令の先頭アドレスをもとにして決められます。たとえばACALL 80Hという命令が07FFHというアドレスから配置されていたら、PCへの値の設定が行われるのはACALL命令の第2バイトを読み出した後ですから、PCが0801Hにまでインクリメントされたあとです。ですから、実際に呼び出されるのは0880Hからのサブルーチンで、0080Hではないことに注意しないといけません。LCALL命令だと16 bitアドレスをオペランドに持ちますから、プログラムメモリの任意の場所のサブルーチンを呼び出せます。時間的な違いはないので、面倒なら常にLCALL命令を利用するということもできるでしょうが、コードサイズが長くなると条件分岐命令の行き先ラベルが相対アドレッシング範囲外になる可能性も高まるので、痛し痒しというところですね。なお、LCALLの16 bitアドレスは命令の第2バイトにアドレス上位バイトが、命令の第3バイトにアドレス下位バイトが配置されます。
RET命令とRETI命令はそれぞれサブルーチンと割り込みサービスルーチンからの復帰を行う命令です。共にスタックから16 bitデータを取り出してPCに転送しますが、RETI命令の方は割り込み制御回路に影響を及ぼす点が異なります。
AJMP命令とLJMP命令は無条件分岐命令で、PCをスタックに退避しないこと以外はACALL命令とLCALL命令と同じです。さらにSJMPという相対アドレッシングを用いる分岐命令も用意されています。
JMP命令は一種のインデックス修飾されたジャンプ命令です。DPTR + Aで求められたアドレスに分岐します。アキュムレータの内容は無符号整数として扱われます。つまりDPTRとの加算時に符号拡張は行われません。
次の2命令は条件分岐命令です。ここには単純な条件分岐命令にはJZ命令とJNZ命令しかありませんが、ビット操作命令の項にCYフラグに関するJC命令とJNC命令が含まれています。また、PSWはダイレクトビットアドレッシング可能なレジスタですから、JB命令やJNB命令を用いてPSWに含まれるPフラグやOVフラグを条件とすることも可能です。ある意味、ビット操作命令の強力さがわかりますし、利用頻度の高い命令はバイト数を短くし、頻度の低い命令はサイズが大きくなってもかまわないけれど記述力は充分に備わっているという点で優れたアーキテクチャであることもわかります。アセンブラでもOVをPSW.2と定義してあれば、JB OV, labelのようなわかりやすい記述が可能なのですから。
CJNE命令とDJNZ命令は一種の複合命令で、CJNEは比較と条件分岐を、DJNZはループカウンタのデクリメントと条件分岐を行います。
CJNE命令はふたつのオペランドを比較して不一致なら分岐します。つまり一致するまで何かをくり返すループを実装する際に便利なようになっています。比較対象のオペランドの組み合わせに4種類あります。
DJNZ命令はZ80 CPUにあるのと同様に8 bitカウンタをデクリメントして、それが0でなければ分岐する命令で、ループ制御に便利なものです。しかもZ80 CPUではループカウンタがBレジスタに限定されていましたが、こちらのDJNZ命令は8種類のレジスタ、あるいはダイレクトアドレッシングで指定可能なメモリや特殊レジスタの、任意のものをループカウンタとして使用できます。R0からR7までのレジスタをループカウンタとした場合にはZ80 CPUのDJNZ命令と同じく2 Byte命令となります。
最後の命令としてNOP命令があります。命令コードは00Hで、プログラムカウンタを1進めるだけの機能ということで、プログラムカウンタを変更する分岐命令の仲間として扱っています。

命令の第1バイトに使用されていないビットパターンは0A5Hだけで、他の255通りはすべて有効なオペコードとなっています。散漫で整理されていない印象のあったMCS-48の命令体系と比較すると、ずっと強力で整理された命令体系に見えます。3 Byte命令も存在するからコードサイズがMCS-48より長くなりがちかというと、その3 Byte命令はMCS-48では複数の命令を組み合わせなければ実現できないようなものが多かったわけですから、かえってMCS-48のものよりコンパクトに記述できますし、プログラムの可読性も増していると思います。とはいえ、SUB命令が省略されていたり、INC DPTR命令はあるのにDEC DPTR命令がないとか、あるいはメモリ構成とか、Intel社の組み込みマイクロプロセッサ臭さも少々感じられますね。

Return to IC Collection.