MCS-48

マイクロプロセッサの最初の開発動機は、すでに有名になっているでしょうが、高機能電卓用の主要回路製作でした。電卓は確かにコンピュータに似てはいますが、ユーザのプログラム可能性などは排除されていて、中身にはコンピュータが入っているかもしれないけれども、外側から見れば単なる電卓でしかありません。内部にコンピュータが入っているかどうかなど意識させない、機能だけを提供する組込み用途がマイクロプロセッサの当初の開発動機でした。組込み用途はコンピュータが主役になってはいけません。別の主役が正しく動作するように、あるいは使いやすくなるように、裏方として動作するコンピュータです。マイクロコンピュータを組み込んだら有利になるのだったら組み込むことにしてやってもよいかな、ってなくらいの立場です。とはいえ、一口に組込み用途といっても、量産の程度によって性格が変わります。テレビのクイズ番組の得点表示板のように1台程度しか作らなくてサイズや電源の制約が小さければ、技術者が何日もよけいに苦労して機能の少ない1チップコンピュータにプログラムを詰め込むより、マルチチップ構成の普通のマイクロコンピュータやパーソナルコンピュータを使って手早く完成した方が有利になるでしょう。パーソナルコンピュータが入っていたとしても、主役をサポートする裏方に徹していれば組み込み用のコンピュータです。ただ、数百台以上作られる種類の組込み用途では、一般に次のような要求があると思われます。

これらは独立した項目ではなく、相互に関係があります。小型であれば、プリント基板の面積が小さくなり、その分のコストが浮くでしょう。外付け部品が少なければ少ないほど、小型になります。電源が簡単になれば、電源のサイズやコストを考えればトータルで小型で安価になるでしょう。
結局、部品数を減らして小型にしようとすれば、ROMやRWMもマイクロプロセッサと同じチップ上に集積し、そればかりか入出力ポートまで同じチップに組み込んだ、1チップマイクロコンピュータがペストの選択になります。電源を供給して、クロック信号とリセット信号を与えるだけで、それなりのプログラムを動作させて各種入力を受け付けて対応する出力を行なうようなLSIができあがります。
1チップマイクロコンピュータを最初に開発したのはテキサスインスツルメンツ社で、TMS1000シリーズの4 bitマイクロコンピュータが最初のシングルチップです。もちろんシングルチップマイクロコンピュータの基本的特許もTI社が保有しています。もっともマイクロプロセッサの応用を広げる重要な鍵となるアイディアなので、各社とも汎用のマイクロプロセッサと同じくらい力を注いでシングルチップマイクロコンピュータを開発しています。

それで、使いにくいところもあるが、セカンドソースが多く(特にUV-EPROM版が)安く供給されていたために一世を風靡したのがIntelのMCS-48シリーズです。1976年に発表と、Intel開発の最初のシングルチップで、それほど使いやすくもなかったのですが、コスト優先の世界ですので意外なほど長命で、いまだに使われているかもしれません。お手持ちのPCのキーボードをばらしてみると、MCS-48ファミリのマイクロコンピュータが使われていたりしますよ。きっと。

これが写真。ただしROMがUV-EPROMになっている8748, 8749です。
8748, 8749
上はIntel製の8748で、中央はNEC製のuPD8748、下は同じくNEC製のuPD8749Hです。8748は共に1982年の9, 10月頃の製造で、チップサイズはほぼ同一です。ただし、マスクは明らかに異なっています。Intelの型番がD8748と読み取れるかもしれませんが、この最初のDはパッケージがCERDIPであることを表す文字です。2枚のセラミック板の間にチップとリードフレームを挟み込み、低融点ガラスで封止したパッケージです。NECのもD8748Dと読み取れるでしょうが、こちらの最初のDはuPD8748のuPが省略されたものです。末尾のDはCERDIPを含む広義のセラミックパッケージであることを表します。8748のメモリを拡張したものが8749です。詳細は後で。写真のuPD8749Hは1988年製。上の8748と比べて技術が進んだため、メモリ容量が増えているのにチップサイズが減少しているのがわかると思います。

8048は1 KByteのROMと64 ByteのRWM、27本のI/Oポートと8 bitのタイマ・カウンタを内蔵していて、6 MHzの水晶発振子を接続することによりクロックを内部で生成して1命令を最短2.5 usで実行します。命令は1 Byte命令と2 Byte命令のみで、3 Byte命令が存在しないのがひとつの特徴になっています。よけいな命令を削ぎ落として短い命令を多用するようにしたために、プログラムが小さくなり、内蔵のROMに納めやすくなっています。必要に応じて、外部にROMやRWMを増設することもできるようになっています。もっとも外部にそのようなものを増設すると、8085に8155とROMを外付けするのと差がなくなって、8048を使用する利点が減ってしまいますけれども。

えー、ハードウェアの説明を、ピン配置に絡めて行ないます。これが端子の並びです。

T0     1       40 VCC
XTAL1  2       39 T1
XTAL2  3       38 P27
RESET* 4       37 P26
SS*    5       36 P25
INT*   6       35 P24
EA     7       34 P17
RD*    8       33 P16
PSEN*  9       32 P15
WR*   10       31 P14
ALE   11       30 P13
DB0   12       29 P12
DB1   13       28 P11
DB2   14       27 P10
DB3   15       26 VDD
DB4   16       25 PROG
DB5   17       24 P23
DB6   18       23 P22
DB7   19       22 P21
VSS   20       21 P20

電源はVCCとVDDで、共に5 Vを与えます。VSSが電圧の基準とリターン線ですね。VDDは内蔵のROMに接続されていて、UVEPROM内蔵の8748ではプログラミング用の高電圧電源を供給する端子として使われます。一部のCMOS品や低消費電力品には、VDDを低消費電力モードへの切り替えに使用するものもあります。オリジナルの8048では最大135 mAもの電流を消費しますが、CMOS版では数mAの消費電流に押さえられたものもあり、電源に電池を使用することもできるようになっています。特に電源電圧範囲の広いCMOS版を低速で動作させれば、乾電池2本で1 mA以下の消費電流を期待できるものもあります。
T0端子はプログラムからHかLかを調べることのできる端子で、テスト0と呼ばれます。一種の入力ポートです。設定しだいによっては、クロックジェネレータの発振した周波数を1/3に分周した内部クロックを出力することもできるようになっています。
XTAL1とXTAL2は水晶発振子などを接続してクロック信号の源信号を発振するための、クロックジェネレータの端子です。1命令の実行時間は、源信号の周期の15倍か30倍のどちらかになります。つまり、6 MHzの水晶発振子を使用した場合には2.5 usか5 usです。セラミック振動子やLC発振回路を構成することもできますし、外部発振回路の信号を入力することもできます。なお、11 MHzまでの水晶発振子を使用できる高速版もあります。
RESET*端子はリセット信号入力で、負論理ですね。プルアップ抵抗を内蔵しているので、コンデンサを接続するだけで、パワーオンリセットを実現できます。
SS*信号は命令のシングルステップ動作を行なうための端子で、少々の外付け回路でプログラムを1命令ずつ実行できるようになります。プログラムのデバッグや動作検査のための信号ですね。普通は使用しません。
INT*信号は割り込み入力です。割り込み許可状態でINT*の立ち下がりで割り込みが発生します。また、割り込みとは無関係に、この端子にLが入力されているときに分岐する条件分岐命令であるJNI命令があり、一種の入力ポートとして使用することもできます。
EA信号は内蔵のROMをプログラムメモリ空間から隠すための端子で、この信号がアクティブになると内蔵ROMは使用されずに必ず外部プログラムメモリから命令を読み込むようになります。8048をROM無し版の8035相当のチップにしてしまう端子です。シングルチップマイクロコンピュータではなく、普通のマイクロプロセッサ的な動作をさせるための端子と考えることもできるでしょう。プログラム開発時やテスト時に外付けメモリのプログラムを動作させるような場合に使えますが、本来シングルチップで使用するように設計されているシステムの場合、外部メモリとのインターフェースにI/Oポートの一部を使用してしまい、本来のI/O動作ができないため、そう簡単にデバッグとか開発時に利用できません。
RD*, PSEN*, WR*の各信号は外付けメモリのためのストローブ信号です。読み書きのタイミングを示すための信号と理解してください。RD*は外部データメモリの読み込みストローブ、PSEN*は外部プログラムメモリからの読み込みストローブ、WR*は外部データメモリへの書き込みストローブとなっています。外部プログラムメモリに何かを書き込む命令や機能が存在しないので、そのための端子もありません。RD*とWR*はバスポートへの入出力のタイミングを外部に知らせるためにも使用されます。
ALEはアドレスラッチ用のストローブ信号です。外部にメモリを接続する場合、バスポートであるDB0 - DB7にはアドレスの下位8 bit分がまず最初に出力されて、その後に先のRD*, PSEN*, WR*のいずれかのストローブ信号に合わせてデータ転送が行われます。有効なアドレスがバスポートに出力されていることを示すのがALE信号の役割になります。このALE信号にあわせて、アドレスをラッチして、メモリアクセスに使えるようにします。
DB0からDB7まではバスポートと呼ばれる端子で、外部メモリを接続する際のアドレスやデータの入出力に使用される他、I/Oポートとしても使用できます。ただしI/Oポートとして使用する際には独特の制約があるので、注意が必要です。まずビットごとに入出力方向を指定することはできません。必ず8 bitまとめて同時に入力か出力を行ないます。入力を行なう場合には、少なくともRD*信号がアクティブな間はバスポートがハイインピーダンスになっていますから、その時に与えられた電圧レベルが読み込まれます。出力を行なうと、WR*がアクティブになると同時に指定されたデータが出力されますが、その後もデータがラッチされて出力が続いています。バスポートはトーテムポール的な出力バッファで、1 TTL入力分の流れ出しや流れ込み電流を保証しています。バスポートに出力する命令を実行すると、自動的にバスポートが出力状態になって、他の用途にバスポートを使用するまで、出力状態が維持されます。そのため、出力後にバスポートの入力命令を実行すると、入出力の切り替えタイミングに問題があって、不正確なデータを読み込む場合があります。MOVX @Rn, Aといった、(存在しないかもしれない)外部データメモリにアクセスするような命令を便宜的に入力命令の前に実行して、バスポートの出力状態を解除しておけば、正常に入力を行なえます。と、まぁ、少々クセのある使いにくいポートです。
P20からP23まではポート2の下位4 bit分の信号です。I/Oポートとしての機能はP24 - P27, P10 - P17と同一で、そちらで説明します。ただし、他に2通りの機能が、この4本の端子に割り当てられています。それは、外部プログラムメモリ接続時にアドレス上位4 bitの出力を行なう機能と、I/O拡張用LSIの8243を接続する際の専用のバスラインとしての機能です。外部プログラムメモリを接続する場合、あるいは8243を接続する場合には、この端子をI/Oポートとして使用できません。正確には回路を外付けすればI/Oとして利用することもできますが、他のI/Oにマルチプレクサやラッチを入れてポートを増設するのと比べて特に有利とは思えません。ただ、開発時にだけ外付けROMを利用する開発形態だと、開発用システムに外付けROMとポート入出力を両用可能にする面倒な回路を組んでも割にあうかもしれません。外部プログラムメモリと8243を同時に接続することは特別な回路なしで可能です。
PROG信号は8243を接続するためのタイミングパルス信号です。P20 - P23とPROG信号を8243に接続すると、4 bit幅の入出力ポートを4組単位で増設できます。つまり8243の1個あたり16本のI/Oポートを増設できます。8243を使用する場合の専用命令が用意されていて、以前は8 bit幅のラッチなども種類が少なく高価でしたから、I/O端子が少し不足する場合には便利でした。
P10 - P17とP24 - P27は、それぞれポート1とポート2の上位4 bit分のI/Oポートです。ポート1とポート2は擬似双方向ポートと呼ばれる回路になっていて、ビット単位で入出力方向を決められます。擬似双方向ポートというとすごそうですが、TTL回路でいえばオープンコレクタ出力を抵抗でプルアップした形式の出力バッファと普通の入力バッファが同時に接続されているだけのものです。オープンコレクタ出力ですから、その端子をVSSにショートしても規定の電流が流れるだけで電気的な悪影響はありません。出力ポートとして使用する場合はHでもLでも命令で書き込めば出力できますし、入力端子として使用する場合はHレベルを出力するようにしておくだけです。それだけでプルアップ抵抗付きの入力ポートとして扱えます。
T1信号はカウンタのクロック入力にも、単なる入力ポートとしても使用できる端子です。また、タイマ・カウンタを使用しないが、割り込み入力がINT*以外にもう一本必要な場合には、カウンタに0FFHを前もって設定しておけば、T1信号でカウンタがインクリメントされた瞬間にカウンタのオーバーフローが生じ、タイマ・カウンタ割り込みが発生します。
なお、内蔵のタイマ・カウンタ回路は8 bitのカウンタで、T1信号の入力パルスか、水晶発振回路で発振した周波数の1/480の周波数の信号のどちらかでインクリメントされるようになっています。カウンタが0FFHから0になる際(オーバーフロー時)に割り込みを発生させることもできますし、オーバーフロー発生時にタイマフラグがセットされ、タイマフラグの値によって条件分岐することも可能です(タイマフラグの判定後に自動的にタイマフラグがリセットされる)。任意の値にカウンタを初期設定することも、またカウンタの値を読み出すこともできます。このように、周期的割り込みや計時や外部信号の計数が、8 bitカウンタとはいえ可能になっています。ただしひとつしかありません。
以上で、全端子の説明は終わりましたね。結局、ポート1、ポート2、バスポートがそれぞれ8本ずつ計24本の入出力端子で、T0, T1, INT*端子が入力ポートとして使用できますから、合計27本のI/Oを持っているということになります。さらに1回路の8 bitタイマ・カウンタとROM、RWMを内蔵しているだけです。組込みの機能がそれくらいしかないという、やたら付加機能が増えている最近の組み込み用プロセッサと比べると、実にシンプルなマイクロコンピュータです。
8048を動かすのに最低限必要な外付け部品は、水晶発振子1個にコンデンサ4個(水晶発振子の負荷容量に2個、パワーオンリセット用に1個、電源バイパスコンデンサに1個)だけで、あとは5 V電源を供給するだけで内蔵ROMに記録されたプログラムを実行することができます。さすがはシングルチップというところですね。Z80, 8085, MC6802なんかを使うと、ずっと多くの部品と実装面積が必要になりますから。ただし、ややこしいI/Oポートを増設したり外部にメモリを増設したりすると、8085やMC6802より性能が低くてプログラミングしにくいコンピュータになってしまいます。外部増設で複雑なこともできるようになっているとはいえ、簡単な用途を簡単に実装するためのプロセッサです。

ソフトウェア側から見たモデルとして、レジスタとメモリ構成を眺めていきましょう。
8048 registers
これが内部レジスタです。アキュムレータと、フラグ類が納められたプログラムステータスワード(PSW)と、プログラムカウンタ(PC)くらいしかなくて、なんか驚くほどシンプルですけど、実はデータメモリの方にスクラッチパッドレジスタが確保されています。uPD751と似ていますね。
アキュムレータは8 bit幅のレジスタで、データ操作の中心となります。PSWの上位4 bitは各種フラグで、下位4 bitがスタックポインタとして使用されます。Cフラグはキャリーで、加算やローテートの際に使用されます。ACは補助キャリーで、BCD補正命令用ですね。F0はプログラマが自由に意味を与えて使用できる汎用フラグです。同じくF1という汎用フラグもありますが、割り込み時の扱いなどが異なります。詳細は割り込みの解説で。BSはバンクセレクトで、8048はデータメモリ上にレジスタバンクを2セット定義してありますが、そのどちらを使用するか選択するためのビットです。MBはJMPやCALL命令の説明の際に触れますが、これらの命令の分岐先の最上位ビットを決めるためのものです。
プログラムカウンタは12 bitしかありません。8048の内蔵しているプログラムメモリは1 KByteでしたし、8048を使用するアプリケーションでは数KByte以上のプログラムを必要としないだろうという割り切りの結果ですね。外部にプログラムメモリを拡張することも可能ですが、それでも最大4 KByteまでです。

メモリ構成を図にしたものを下に示します。
8048 memory
MCS-48シリーズではプログラム用のメモリとデータ用のメモリは分離していて、しかもデータメモリは内蔵分と外部のものとで別の空間に分けられていて、使用できる命令が異なっています。
プログラムメモリは8048の場合にチップ内部に1 KByteのROMを内蔵しています。全プログラムメモリ空間の残り3 KByteは外部に増設することができるようになっています。ただし、外部に増設する場合は12本のI/Oポートが使用できなくなります。
内部データメモリは8048の場合に64 Byteが内蔵となっています。内部データメモリは単にデータメモリと呼ぶことにします。データメモリの先頭8 ByteはR0からR7までの汎用レジスタの集合体であるレジスタバンク0が配置されていて、18Hから1FHまでの8 Byteには同じくR0からR7で構成されるレジスタバンク1があります。これらのレジスタバンクは同一の役割をしますが、プログラムで使用されるのはPSWレジスタ内のBSフラグによって指定されるものです。BSフラグが0の場合にレジスタバンク0が、1の場合にレジスタバンク1が使用されます。この二つのレジスタバンクに挟まれた08Hから17Hまでの16 Byteがスタック領域に使用されます。プログラムカウンタは12 bitで、退避に2 Byteが使用されますから8レベルのサブルーチン呼び出しが可能になっています。アキュムレータなどをプッシュすることはできません。スタックポインタにはPSWの下位4 bitが使用されます。アドレス20Hから3FHまでのデータメモリはユーザが一般に使用できるRWMで、プログラムの作業領域として普通に利用できます。もちろんアドレス0から1FHまでのデータメモリもユーザメモリと同一の命令でアクセスできますから、たとえばレジスタバンク1をプログラムで使用しなければ、そこも普通にメモリとして使用できます。
外部データメモリはオプションで、まぁ普通の用途には不要でしょう。どうしても必要な場合、増設することになります。その結果、バスポートと呼ばれるI/Oポート8本が使用できなくなります。このバスポートは外部プログラムメモリの増設時に使用される12本のI/Oポートに含まれていますから、外部プログラムメモリと外部データメモリを同時に増設する場合には12本のI/Oポートが使えなくなるだけで済みます。外部データメモリを使用する命令はアキュムレータへのロードとストアを行なうMOVX命令しかありません。そのため、使いにくいメモリですし、8048の発表直後を別にすれば内蔵ROMと内蔵データメモリをそれぞれ8048の2倍に拡張した8049がありますから、そちらを採用した方がプログラミングも楽でトータルのハードウェアコストも安上がりでしょう。MCS-48ファミリは後で整理してまとめます。

さて、MCS-48の命令ですが、アセンブリ言語命令としては96種類と(CMOS版では97種類)、やたら種類が多くなっています。制御用なのだから命令が整理されて少なくなっているのではないかと思うでしょう。実際、1 Byte命令と2 Byte命令だけで3 Byte命令が存在しないし、加算命令はあっても減算命令が用意されていないなど、実際に機能が縮小されています。では、なぜこのように種類が増えているのかというと、アドレッシングモードが異なると別種類の命令として数えていたり、内部データメモリ、外部データメモリ、プログラムメモリをアクセスするための独立したデータ転送命令がいくつもあったり、I/Oポートアドレスという考え方がなく、ポート1およびポート2に入出力する命令とバスポートに入出力する命令とタイマ・カウンタ操作用の専用命令が個別に存在したりするためです。一言で印象を言い表すとすれば、統一感のないごちゃごちゃした雑多な命令群というものになってしまいます。

アキュムレータ操作命令から見ていきましょう。

ニーモニック          命令       サイクル    動作
ADD  A, Rr      0110 1rrr            1  (A), (C) <- (A) + (Rr)          r = 0..7
ADD  A, @Rr     0110 000r            1  (A), (C) <- (A) + ((Rr))        r = 0..1
ADD  A, #data   0000 0011 dddd dddd  2  (A), (C) <- (A) + data
ADDC A, Rr      0111 1rrr            1  (A), (C) <- (A) + (Rr) + (C)    r = 0..7
ADDC A, @Rr     0111 000r            1  (A), (C) <- (A) + ((Rr)) + (C)  r = 0..1
ADDC A, #data   0001 0011 dddd dddd  2  (A), (C) <- (A) + data + (C)
ANL  A, Rr      0101 1rrr            1  (A) <- (A) & (Rr)               r = 0..7
ANL  A, @Rr     0101 000r            1  (A) <- (A) & ((Rr))             r = 0..1
ANL  A, #data   0101 0011 dddd dddd  2  (A) <- (A) & data
ORL  A, Rr      0100 1rrr            1  (A) <- (A) | (Rr)               r = 0..7
ORL  A, @Rr     0100 000r            1  (A) <- (A) | ((Rr))             r = 0..1
ORL  A, #data   0100 0011 dddd dddd  2  (A) <- (A) | data
XRL  A, Rr      1101 1rrr            1  (A) <- (A) ^ (Rr)               r = 0..7
XRL  A, @Rr     1101 000r            1  (A) <- (A) ^ ((Rr))             r = 0..1
XRL  A, #data   1101 0011 dddd dddd  2  (A) <- (A) ^ data
INC  A          0001 0111            1  (A) <- (A) + 1
DEC  A          0000 0111            1  (A) <- (A) - 1
CLR  A          0010 0111            1  (A) <- 0
CPL  A          0011 0111            1  (A) <- ~(A)
DA   A          0101 0111            1  BCD補正
SWAP A          0100 0111            1  (A4..7) <-> (A0..3)
RL   A          1110 0111            1  (A7) <- (A6), (A6) <- (A5), ..., (A0) <- (A7)
RLC  A          1111 0111            1  (A7) <- (A6), (A6) <- (A5), ..., (A0) <- (C), (C) <- (A7)
RR   A          0111 0111            1  (A0) <- (A1), (A1) <- (A2), ..., (A7) <- (A0)
RRC  A          0110 0111            1  (A0) <- (A1), (A1) <- (A2), ..., (A7) <- (C), (C) <- (A0)

以上がデータ転送を除くアキュムレータ操作に関する命令すべてです。2項演算は加算、桁上がり付き加算、AND, OR, ExORの論理演算だけです。乗除算はもちろんのこと、減算命令も存在しません。さらに大小比較命令もありません。
もっとも比較命令については演算命令の視点からだけでなく、条件判定のしくみも含めて考える必要があるでしょう。MCS-48には演算に関係したフラグがCフラグとACフラグしかなく、Zフラグはありません。アキュムレータの内容が0ならジャンプするというような形の分岐命令は存在しますが、アキュムレータを変化させずにフラグ類に大小関係を記録してそのフラグに応じて分岐するということにはCフラグしか使えないので、困難です。単に一致判定だけなら、XRL命令の後にAが0かどうか判定することで実現できます。
2項演算命令には、3種類のアドレッシングモードが用意されているのがわかります。演算の中心となるのはアキュムレータですが、もうひとつの項については8種類のスクラッチパッドレジスタ、2種類のポインタ、定数データの3種類が選べます。スクラッチパッドレジスタについては2バンクあるうちのBSフラグで選択された側にあるすべてのレジスタを使用できます。ポインタとして使われるのはスクラッチパッドレジスタの内のR0とR1の2種類だけです。また、このポインタで指されるメモリは内部データメモリに限られます。プログラムメモリや外部データメモリの内容を直接演算することはできません。内部データメモリは64 Byteとか128 Byteのものもありますが、その場合にはレジスタ内の下位ビットだけが有効で、上位の2 bitないし1 bitは無視されます。
SWAP命令やローテート命令でA4..7とかA0とかあるのは、アキュムレータの特定のビットを表し、A4..7でアキュムレータの上位4 bitを、A0でアキュムレータのLSBを表しています。
BCD補正命令のDA A命令はBCDデータ同士の加算を行った直後に実行すると、加算結果がBCD表現となるように補正します。Cフラグも適切にセットしなおして、多桁のBCD加算を行えるようになっています。
SWAP命令はアキュムレータの上位4 bitと下位4 bitを入れ替えます。ローテート命令は途中にCフラグを挟むかどうかと左右の回転の向きによって4種類存在しますが、算術シフトの類はありません。
ADD, ADDC命令ではCフラグとACフラグが影響を受けます。DA, RLC, RRC命令ではCフラグだけが影響を受けます。他の命令ではフラグが変化することはありません。

次は入出力命令です。

ニーモニック          命令       サイクル    動作
IN   A, Pp      0000 10pp            2  (A) <- (Pp)                     p = 1..2
OUTL Pp, A      0011 10pp            2  (Pp) <- (A)                     p = 1..2
ANL  Pp, #data  1001 10pp dddd dddd  2  (Pp) <- (Pp) & data             p = 1..2
ORL  Pp, #data  1000 10pp dddd dddd  2  (Pp) <- (Pp) | data             p = 1..2
INS  A, BUS     0000 1000            2  (A) <- (BUS)
OUTL BUS, A     0000 0010            2  (BUS) <- (A)
ANL  BUS, #data 1001 1000 dddd dddd  2  (BUS) <- (BUS) & data
ORL  BUS, #data 1000 1000 dddd dddd  2  (BUS) <- (BUS) | data
MOVD A, Pp      0000 11pp            2  (A0..3) <- (Pp), (A4..7) <- 0   p = 4..7
MOVD Pp, A      0011 11pp            2  (Pp) <- (A0..3)                 p = 4..7
ANLD Pp, A      1001 11pp            2  (Pp) <- (Pp) & (A0..3)          p = 4..7
ORLD Pp, A      1000 11pp            2  (Pp) <- (Pp) | (A0..3)          P = 4..7

ニーモニックでINは普通だとしてOUTLのLは何かといえばラッチの略のようです。ANLやORLも同じ。
このグループの上から4つの命令はP1とP2に対する入出力操作で、アキュムレータとのデータ転送の他、定数と論理演算を行えるようになっているため、特定のビットをセットしたりリセットしたりすることが簡単に行えます。
次の4種類の命令はバスポートへの入出力操作です。こちらの入力命令にはINSとSの文字が付いていますが、これはストローブ付きということのようで、入力タイミングを示すためにRD*信号が有効になるのを表現しているようですが、そこまでニーモニックに反映させても面倒な感じがしますね。
最後の4種類はI/O拡張用の8243のための専用命令で、4 bit幅のP4, P5, P6, P7への入出力命令です。ポートが4 bit幅のため、アキュムレータの下位4 bitだけに関係します(入力に関してはアキュムレータの上位4 bitが自動的にクリアされるので注意)。
入出力命令でフラグは変化しません。

スクラッチパッドレジスタの操作命令はこの3種類があります。

ニーモニック          命令       サイクル    動作
INC  Rr         0001 1rrr            1  (Rr) <- (Rr) + 1                r = 0..7
INC  @Rr        0001 000r            1  ((Rr)) <- ((Rr)) + 1            r = 0..1
DEC  Rr         1100 1rrr            1  (Rr) <- (Rr) - 1                r = 0..7

この3種類だけです。インクリメントとデクリメントだけなのですが、ここにも奇妙な非対称性があります。インクリメント命令には存在する内蔵データメモリへのポインタ参照が、デクリメント命令では使用できません。これらの命令でフラグは変化しません。

次はブランチ命令のグループです。

ニーモニック          命令       サイクル    動作
JMP  addr       aaa0 0100 aaaa aaaa  2  (PC10..0) <- addr, (PC11) <- (MB)
JMPP @A         1011 0011            2  (PC7..0) <- ((A))
DJNZ Rr, addr   1110 1rrr aaaa aaaa  2  (Rr) <- (Rr) - 1                r = 0..7
                                        if (Rr) != 0, (PC7..0) <- addr
JC   addr       1111 0110 aaaa aaaa  2  if (C) = 1, (PC7..0) <- addr
JNC  addr       1110 0110 aaaa aaaa  2  if (C) = 0, (PC7..0) <- addr
JZ   addr       1100 0110 aaaa aaaa  2  if (A) = 0, (PC7..0) <- addr
JNZ  addr       1001 0110 aaaa aaaa  2  if (A) != 0, (PC7..0) <- addr
JT0  addr       0011 0110 aaaa aaaa  2  if (T0) = 1, (PC7..0) <- addr
JNT0 addr       0010 0110 aaaa aaaa  2  if (T0) = 0, (PC7..0) <- addr
JT1  addr       0101 0110 aaaa aaaa  2  if (T1) = 1, (PC7..0) <- addr
JNT1 addr       0100 0110 aaaa aaaa  2  if (T1) = 0, (PC7..0) <- addr
JF0  addr       1011 0110 aaaa aaaa  2  if (F0) = 1, (PC7..0) <- addr
JF1  addr       0111 0110 aaaa aaaa  2  if (F1) = 1, (PC7..0) <- addr
JTF  addr       0001 0110 aaaa aaaa  2  if (TF) = 1, (PC7..0) <- addr, reset TF
JNI  addr       1000 0110 aaaa aaaa  2  if (INT*) = 0, (PC7..0) <- addr
JBb  addr       bbb1 0010 aaaa aaaa  2  if (Ab) = 1, (PC7..0) <- addr

ブランチ命令の分岐先は一般的なマイクロプロセッサより少々面倒な規則で決まります。まず、8048のプログラムメモリ空間は4096 Byteで、プログラムカウンタは12 bitであったことを思い出してください。JMP命令以外はアドレスオペランドが8 bitとなっています。この8 bitのアドレス部は相対アドレスを表すのではなく、絶対アドレスで、プログラムカウンタの下位8 bitにそのままロードされます。では、プログラムカウンタの上位4 bitはどうなるのかといえば、変化なしというのが正解です。つまり、プログラムメモリを先頭から256 Byte単位でグループしたものをページと呼ぶことにすれば、大半のブランチ命令は同一ページ内への分岐しかできません。仮にプログラムカウンタが0F0Hだとして、そこから102Hへと条件分岐したくても、条件分岐の行き先を直接102Hにすることはできないのです。別のプロセッサで8 bitの相対分岐アドレッシングを使用した場合でも、行き先が範囲外で悲しい思いをしながらプログラムの修正を行うことがよくあるというのに、このページ内分岐命令の制約は厳しいですね。最初はうまくページ内分岐一発でコーディングできたと思っても、コードの追加やサブルーチンの移動で分岐がページをまたぐようになってしまうとコードの修正が必要になります。その分コードが長くなるために、別の場所で同じことが起こってしまうかもしれません。しかも、さらにコードの追加やサブルーチンの移動で、再びページ内に納まるようになってしまうと、こんどは自動的にアセンブラやリンカが警告してくれるわけではありませんから、自分の目で確かめて修正するはめになります。いや、普通ならほっておいてもかまわないわけなんですが、1 KByteのROMいっぱいのコードになって、あと数バイト不足するなんてことが開発末期には起こりがちで、そうしたら今は不要になっているページ境界対策用コードというのも取り除く対象となりますから。そうでなくとも、このくらいの規模の組み込み用途では、コードの実行時間で外部タイミングを調整するということもよく行われ、その時間待ちを行うコードがページ境界にあたると単純にコードを書き換えってなことができなかったりして、サブルーチン単位で場所を入れ替えて、なんてこともあるでしょうし。
JMP命令に関しては、命令の第1バイトの上位3 bitがPCの10 bitから8 bitに入り、第2バイトがPCの7 bitから0 bitに入ります。これでは11 bitだけなので、最後の1 bitの分、PCのMSBにはMBフラグの内容が転送されます。MBフラグは後述のSEL MB0/SEL MB1命令で変更できます。8048や8049の内蔵メモリを使う限りにおいては、2 KByte以下なのでMBフラグのことは気にせずにJMP命令のオペランドだけで内蔵メモリの任意のアドレスへジャンプできますから、お手軽でしょう。JMP命令のアドレス部が11 bitに切り詰められているのは、やはり全命令を1 Byteか2 Byte命令にしてしまうための工夫というか苦肉の策なんでしょうね。
JMPP @A命令は、アキュムレータの内容をアドレスと解釈してプログラムメモリから1 Byte読み出し、それをプログラムカウンタの下位1 Byteに転送する間接分岐命令と資料にありますが、ちょっと使ったことがないので、これ以上は。プログラムメモリの上位4 bit分のアドレスは何によって決まるのかとか、不明です。
DJNZ命令は、スクラッチパッドレジスタをループカウンタとして使用するループ命令です。コンパクトで高速なループを実現できますが……これもページ内分岐なのでページ境界をまたぐと高速性を活かすことができなくなります。
JC, JNC, JZ, JNZ命令はごく一般的な条件分岐命令です。ただしZフラグというものが存在しませんから、JZ命令とJNZ命令はアキュムレータの現在の内容がゼロかどうかが分岐条件となります。つまり、メモリやI/Oポートからデータをアキュムレータに読み出せば、ただちにJZ, JNZ命令でその内容が0かどうか判定できます。特別な演算命令は不要です。
JT0, JNT0, JT1, JNT1はT0 (Test 0)端子とT1 (Test 1)端子の状態に応じて分岐する条件分岐命令です。JF0命令とJF1命令は、それぞれF0フラグとF1フラグによる条件分岐命令ですが、逆の条件分岐は用意されていません。その分ページ境界をまたいだ場合の手間は増えてしまいますね。
JTF命令はタイマ・カウンタに関係した条件分岐命令です。タイマ・カウンタがインクリメントされていってオーバーフローが生じると、そのタイミングで割り込みを発生させることも可能ですが、同時にタイマオーバーフローフラグ(TF)がセットされます。割り込みを利用せずに、このTFをプログラムから調べて利用することも可能で、それがJTF命令です。JTF命令の実行後には自動的にTFがリセットされていますから、次のオーバーフローを正しく検出することができます。
JNIはINT*端子の状態による条件分岐命令で、この命令のおかげでINT*割り込みを禁止していればINT*端子を単なる入力ポートとして扱えることになります。
JBb命令は、実際にはJB0, JB1, ..., JB7という8種類の命令ニーモニックで表現され、アキュムレータの特定のビットが1になっていることを条件として分岐する命令です。I/Oポートの特定のビットの状態によって条件分岐する命令はありませんから、IN命令やINS命令、MOVD命令によってAレジスタにI/Oポートのデータを読み込んでからJBb命令で分岐することになります。
特にこのブランチ命令グループでは顕著ですが、ニーモニックをシンプルに統一して、オペランドで条件の種類を表すと、もう少し整理されたアセンブリ言語になりそうです。たとえばJP C, addrとかJP T0, addrみたいな表記ですね。ただ、発表時期の1976年という時代を考えると、メモリが数K Word程度のミニコンピュータによるクロス開発を想定せざるをえず、そうなるとアセンブラの処理が簡単なことが重要視されるでしょうから、このようなニーモニックだけで命令のビットパターンが確定しやすい言語体系が選択されたのでしょうね。

ブランチ命令のついでにサブルーチン関係の3種類の命令も紹介しましょう。

ニーモニック          命令       サイクル    動作
CALL addr       aaa1 0100 aaaa aaaa  2  ((SP)) <- (PC)(PSW7..4), (SP) <- (SP) + 1
                                        (PC10..0) <- addr, (PC11) <- (MB)
RET             1000 0011            2  (SP) <- (SP) - 1, (PC) <- ((SP))
RETR            1001 0011            2  (SP) <- (SP) - 1, (PC)(PSW7..4) <- ((SP))

CALL命令がサブルーチン呼び出しで、行き先アドレスの表現はJMP命令と共通です。ジャンプする前にPCとPSWの上位4 bitの合計16 bit分のデータをスタックに格納します。8048のスタックポインタは本物のアドレスを保持したポインタではなくて、スタックのレベル数を保持したレベルカウンタとでも呼んだ方が正確な代物です。2 Byte分のデータを格納しても1レベル分の数値しかインクリメントされません。おっと、スタックは08Hから18Hまでのアドレス領域に固定されていて、他の場所には移せません。また、一般的なプッシュダウンスタックと異なり、CALLのたびに大きなアドレスの方へと移動します。
RET命令はCALL命令で呼び出されたサブルーチンから戻るための命令です。スタックからPCへデータを転送しますが、PSW上位4 bitは無視されてロードされません。関係するのはPCに相当する12 bit分のデータだけです。
RETR命令は割り込みサービスルーチンからの復帰命令です。こちらはPSWの上位4 bitを無視せずに再ロードします。さらに、割り込みサービスルーチンの実行中は再度の割り込みが禁止されていますが、その割り込み禁止状態の解除も同時に行う命令です。

次はフラグ操作命令です。

ニーモニック          命令       サイクル    動作
CLR  C          1001 0111            1  (C) <- 0
CPL  C          1010 0111            1  (C) <- ~(C)
CLR  F0         1000 0101            1  (F0) <- 0
CPL  F0         1001 0101            1  (F0) <- ~(F0)
CLR  F1         1010 0101            1  (F1) <- 0
CPL  F1         1011 0101            1  (F1) <- ~(F1)

Cフラグ、F0フラグ、F1フラグのクリアと反転命令、計6命令です。直接セットする命令はありません。Cフラグに関してはRLC命令やRRC命令を使う際に必要となることがあるでしょう。F0とF1に関しては、プログラムの流れを制御するフラグとして、条件分岐命令と組み合わせて使うことになるでしょう。F0は割り込み時に保存されますが、F1は保存されませんから、その性質の違いを利用することも考えるとよいでしょう。

データ転送命令はブランチ命令と並んで種類が多いですね。

ニーモニック          命令       サイクル    動作
MOV  A, Rr      1111 1rrr            1  (A) <- (Rr)                     r = 0..7
MOV  A, @Rr     1111 000r            1  (A) <- ((Rr))                   r = 0..1
MOV  A, #data   0010 0011 dddd dddd  2  (A) <- data
MOV  Rr, A      1010 1rrr            1  (Rr) <- (A)                     r = 0..7
MOV  @Rr, A     1010 000r            1  ((Rr)) <- (A)                   r = 0..1
MOV  Rr, #data  1011 1rrr dddd dddd  2  (Rr) <- data                    r = 0..7
MOV  @Rr, #data 1011 000r dddd dddd  2  ((Rr)) <- data                  r = 0..1
MOV  A, PSW     1100 0111            1  (A) <- (PSW)
MOV  PSW, A     1101 0111            1  (PSW) <- (A)
XCH  A, Rr      0010 1rrr            1  (A) <-> (Rr)                    r = 0..7
XCH  A, @Rr     0010 000r            1  (A) <-> ((Rr))                  r = 0..1
XCHD A, @Rr     0011 000r            1  (A0..3) <-> ((Rr)0..3)          r = 0..1
MOVX A, @Rr     1000 000r            2  (A) <- ((Rr))                   r = 0..1
MOVX @Rr, A     1001 000r            2  ((Rr)) <- (A)                   r = 0..1
MOVP A, @A      1010 0011            2  (A) <- ((PC11..8, A))
MOVP3 A, @A     1110 0011            2  (A) <- ((0011, A))

MOVというニーモニックで表される命令は、ごく普通のデータ転送命令です。@Rrがオペランドのものは、内部データメモリとのデータ転送になります。MOV @Rr, #dataで定数データを直接内蔵メモリに書き込めるのは強力ですが、必ずアドレスをR0かR1にロードしておく必要があります。メモリアドレスを直接指定するデータ転送命令はありません。
XCH命令はアキュムレータとのデータ交換で、XCHD命令は下位4 bitだけのデータ交換です。XCHD命令はレジスタ間接で内蔵データメモリとだけ可能で、スクラッチパッドレジスタとはできません。
MOVX命令は外部データメモリとのデータ転送命令です。外部データメモリにアクセスする命令はMOVX命令だけで、データ転送において必ずアキュムレータを介さなければならないのは不便です。
MOVP命令は同一ページ内のプログラムメモリからデータを読み出す命令で、プログラムのコードの一部として定義された定数データテーブルを利用する際に使用します。MOVP3命令では第3ページ、つまりプログラムメモリアドレスの上位4 bitが3に固定されたアドレス範囲からデータを読み出します。プログラムメモリをデータとして参照する命令は、この2種類しかありません。
MOV PSW, A以外の命令ではフラグ類が変化しません。

タイマ・カウンタ操作命令は内蔵タイマ・カウンタ専用の命令です。

ニーモニック          命令       サイクル    動作
MOV  A, T       0100 0010            1  (A) <- (T)
MOV  T, A       0110 0010            1  (T) <- (A)
STRT T          0101 0101            1  start timer
STRT CNT        0100 0101            1  start counter
STOP TCNT       0110 0101            1  stop timer/counter
EN   TCNTI      0010 0101            1  enable timer/counter interrupt
DIS  TCNTI      0011 0101            1  disable timer/counter interrupt

MCS-48は内部に8 bitのカウンタを内蔵していて、カウンタが255から0に遷移するときにオーバーフロー割り込みを発生させられます。そのカウンタの初期値設定やカウント値の読み出しに使われるのがTレジスタです。MOV A, T命令でカウンタの内容をアキュムレータに読み出せます。またMOV T, A命令でカウンタに初期値を設定できます。
このカウンタは、プロセッサのクロック信号の1/480の周波数の信号で駆動することができて、その場合にはタイマと呼ばれます。また、外部信号をクロック信号として扱うこともできて、そのときには外部信号の計数を行うためカウンタと呼ばれます。外部信号はT1端子から与えられ、ネガティブエッジでカウントアップします。これらの動作モードの設定を行うのが、STRT T, STRT CNT, STOP TCNTの3命令です。STRT T命令でタイマ動作を指示します。すると、カウンタの入力が内部クロック信号となり、一定周期で割り込みやタイマオーバーフローフラグのセットが行われるようになります。STRT CNT命令でカウンタモードになり、T1がカウンタの入力端子として使用されます。STOP TCNTで両方の動作を停止します。
カウンタ割り込みの制御に使われるのがEN TCNTI命令とDIS TCNTI命令で、それぞれ割り込み許可と禁止の命令です。INT*端子からの割り込みとは独立に許可や禁止を行います。

制御命令および雑命令には次のものがあります。

ニーモニック          命令       サイクル    動作
EN   I          0000 0101            1  enable external interrupt
DIS  I          0001 0101            1  disable external interrupt
SEL  RB0        1100 0101            1  (BS) <- 0
SEL  RB1        1101 0101            1  (BS) <- 1
SEL  MB0        1110 0101            1  (MB) <- 0
SEL  MB1        1111 0101            1  (MB) <- 1
ENT0 CLK        0111 0101            1  enable clock output on T0
NOP             0000 0000            1  no operation
HALT            0000 0001            1  enter HALT mode (CMOS only)

EN I命令とDIS I命令はINT*端子による外部割り込みの有効無効の制御を行います。タイマ割り込みには影響しません。
SEL命令はバンク関係のフラグの操作を行います。
ENT0 CLK命令を実行すると、XTAL1, XTAL2端子で発振したクロック信号の1/3の周波数の内部クロック信号をT0端子から出力するようになります。一度T0端子をクロック出力に指定すると、逆のことを行う命令は存在せず、リセットする以外にもとに戻すことはできません。つまりテスト入力端子としては使えなくなります。
NOP命令は一般的なもので、すべて0のビットパターンになっています。
HALT命令はCMOS版のプロセッサにのみ有効な命令で、発振回路を動作させたままで内部クロックの供給だけを停止する状態に遷移する命令です。この状態ではプログラムの実行は行えませんし、タイマは動作しません。RESET*入力かINT*入力でこの状態から抜けだせます。

MCS-48ファミリの代表的なものを内蔵メモリの観点から分類して以下に表にしてみました。

型番 ROM種類 ROM容量 RWM容量
8048 マスクROM 1 KByte 64 Byte
8049 マスクROM 2 KByte 128 Byte
8050 マスクROM 4 KByte 256 Byte
8035 無し(外付け) 0 KByte 64 Byte
8039 無し(外付け) 0 KByte 128 Byte
8040 無し(外付け) 0 KByte 256 Byte
8748 UV-EPROM 1 KByte 64 Byte
8749 UV-EPROM 2 KByte 128 Byte

このほかに、高速版や低消費電力版、CMOS版などのバリエーションがあります。CMOSや低消費電力版では、スタンバイ状態にするためのハードウェア機能や命令が追加されていたりします。
今までROMを外付けしなくてはならない用途には8048は不向きで8085を使う方が良いと書いてきましたが、なぜROM無しの8035などがあるのでしょうか。これらのROM無しチップは開発用というより組み込み用に使われていました。それは、少数のI/Oポートしか必要でない少量生産品の場合、8085を使用するよりI/Oポートと作業用のRWMを外付けする必要が少なくなるから、ということなのでしょう。8035などのROM無し版はプラスチックパッケージを使えるため、石英窓付きセラミックパッケージの8748よりは安価で、うまくすれば汎用のUV-EPROMと8035のコストの方が8748より安い場合もあるでしょう。また、8748は専用のROM書き込み装置を用意しなくてはなりませんから、汎用のUV-EPROM用の書き込み装置を所有している場合には追加投資が必要な分、汎用ROMを外付けした方が製造コストが安上がりになるかもしれません。この辺に、8035や8039が使われた理由があるのだろうと思います。もっとも8051のROM無し版の8031が普通に安く供給されるようになると、8035系を新規開発に使おうなんてことはなくなってしまいましたけどね。UV-EPROM内蔵の8751が8748よりもずっと高価だったため、8748や8749が使われ続けたのとは大違いでした。

ちなみに、先に少々触れたMCS-48シリーズのI/O拡張用LSIが、この8243です。
82C43
写真のものはNECのセカンドソースでCMOSプロセスのuPD82C43でしたね。5 V単一電源で、入出力ポート16本と8048などへのインタフェースが5本とチップセレクト端子が1本。合計24ピンの、まぁ比較的コンパクトなI/Oエキスパンダです。単なる入出力の他、出力中の値に対するAND演算やOR演算も可能です。

Return to IC Collection.