SL9821

はじめに

ダウンロード

使い方

FAQ

実行画面

技術的なはなし

プログラム構成

CPU

テキスト

グラフィック

PEGC

サウンド

プログラム構成

まずは概念的な構造図を以下に示します。大雑把なものなのですべての関係を網羅しているわけではありません。図の見方としてはおおむねそれぞれの機能ブロックがそれに相当するファイルとして存在するという形になっています。太い線と細い線はデータやイベントのやりとりとその方向について示しています。線の太さはそのブロックの主要な機能についてのやりとりであるかどうかを示しています。なお、色のついた箱は独立したスレッドを持っていることを意味します。

プログラムは大きく2つで構成されていて、.NET Frameworkで作られたUIウインドウと、C言語で書かれたネイティブコードのエミュレータ本体に分かれます。2つのモジュール間は双方向のデータのやりとりがあり、UIからエミュレータ本体へはDllImportを用いたアンマネージドコードの呼び出し、エミュレータ本体からUIへはイベント通知のコールバック関数によって行います。
UIウインドウは表面上のものでエミュレータの動作に関わる大きな機能はありません。エミュレータの起動やリセットの通知、エミュレータを動作させるためのパラメータの変更、保存および、動作中の動的変化の表示(アクセスランプ等)を行います。

エミュレータ本体は大きくホストOSに依存した実装部分と、OSに依存しない共通部で構成されています。OS依存部は、ファイルアクセス、描画処理といったホストの機能に依存する処理を実装し、共通部は主に論理的な処理や状態遷移を管理し(実際にはそれほど明確な切り分けが出来ているわけではないですが...)、一体となって動作しています。以下各モジュールについて解説します。

クロック同期サイクル
エミュレータのメインの処理ループです。ここでは大きく以下の2つの処理を繰り返しで行います。
1. ハードウェアエミュレーション起因の処理発生タイミングのチェックと処理タイミングが来たときの処理の実行
2. CPUの割り込みチェックとインストラクション処理
最小1μs単位のクロックタイミングの同期はこのループ処理によって行っています。
タイミングチェック
タイミングチェックは、タイマのタイムアウトやV-SYNCの同期タイミングといったような一定時間後に処理が必要となる事象が発生したときに登録され、所定の時刻が経過したら登録されたイベントを実行する仕組みになっています。
CPU処理
CPUのエミュレーションはクロック同期サイクルから呼び出され大きく以下の処理を行います。
1. ソフトウェア割り込みのチェックと割り込みがあった場合の割り込みイベント処理の実行
2. 外部割り込み発生のチェックと発生した場合の割り込みイベント処理の実行
3. インストラクションフェッチとインストラクションの実行
周辺デバイスの処理はインストラクションの実行のうち、I/Oアクセス関係の命令が実行されたときまたは特定のアドレスへのメモリアクセスがあったときに呼び出されます。
PIC
割り込みコントローラのエミュレーションを行います。IO処理部の他のモジュールから割り込み発生要因を受け取り、CPUへ外部割り込みの通知と割り込みのステータスの管理を行います。
DMA
DMAのエミュレーションを行います。DMAはフロッピーディスクコントローラとSCSIコントローラが使用します。
メディア統括
メディアへのアクセスはIOアクセスに対し即時応答出来ないものもあるため、メイン処理とは別のスレッドを起動してメディア関連のイベント処理を行います。ハードディスクやフロッピーディスクは本来非同期で独立していますが、スレッドは一つだけで処理を行っています。メディアタイプごとにそれぞれCPUから要求されたコマンドリクエストの処理、ホストコンピュータへ要求したメディアアクセス処理完了時のイベント処理、メディアの入れ替えなどの非同期のイベント処理を行います。
IDE/HDD
IDE接続のHDDコントロールを行います。
FDD(FDC)
μPD765A相当のフロッピーディスクコントロールを行います。
SCSI/CD-ROM
WD33C93A相当のSCSIコントロールと、その上で動くCD-ROM制御を行います。CDDAの再生にはMCI(Media Control Interface)を使用しています。
オーディオ
ビープ音源の鳴動処理部分と86音源相当のサウンド処理を行います。サウンド処理は鳴動タイミングの安定化と音切れ抑制のため鳴動データをバッファリングして実際の鳴動要求から数十ms遅れて音が鳴るようになっています。これらの制御全般をメインのスレッドとは別のスレッドで処理しています。
画面制御
IOアクセスのほか、256色モードのメモリマップトIO、V-RAMアクセス時の処理を行います。一方GDCのコマンド処理とV-SYNCに同期して動作する処理のためのスレッドでCPUとは非同期の処理を来ないます。
この機能だけスレッドの作成をOS依存部側で行っています。これは画面描画をDirectXで行う場合とOpenGL(OSX)で行う場合でスレッドの作成過程がだいぶ異なってしまうことによるものです。
キーボード/マウス
キーボード、マウス、ゲームパッドの入力イベントに対応した処理を行います。
EEPROM
主にソフトウェアディップスイッチの設定状態などの不揮発情報を格納するEEPROMへのコントロールをエミュレーションします。
ペリフェラル
上記以外のIO制御のエミュレーションを行います。具体的にはインターバルタイマ、ウエイト、カレンダ、システムポート、リセット、シリアルです。

だいぶ動くようなった現在だと若干機能の割り振りに不満があったりもするのですが、すでに動作しているものを作り直す気にもなかなかなれず現状のような構成になっています。(補足:バージョン0.2.5.0時点で、この不満はだいぶ解消された形になっています。それにあわせ上記の内容も若干書き直しています)

9821アーキテクチャの実現

つぎに、PC-9821のアーキテクチャを実現するための根幹の部分について説明します。
PC-9821は電源投入すると、CPUのリセットベクタ(x86なのでアドレスF000:FFF0)からCPUの命令の実行を始め、その中でメモリアクセスやI/Oアクセスを通してメモリや周辺デバイスの機能にアクセスします。
エミュレータにおける主たる機能はこのCPUの命令実行処理サイクルを実施し、CPUから要求されたメモリアクセスやI/Oアクセスに応じて適切な周辺デバイスの機能を実現することにあります。
実現すべき周辺デバイスの機能の中にはタイマや、V-SYNCというような正確な時間管理の下に実行される必要があるものもあります。このようなものの中には例えばタイマ割り込みの発生をCPUのカウンタつきのループ処理で待つようにしていて、ループカウンタが満了する前に割り込みが発生したら正常、割り込み発生前にループカウンタが満了したら異常というように、CPUの処理の進捗度合いから周辺デバイスが正常に動作しているかを判断するケースがあります。こうした状況に対処するためにはCPUの処理サイクルと時間管理が必要な周辺デバイスの動作には高い同期性が求められます。
こうした条件を実現すること踏まえ、このメイン処理ループを便宜上クロック同期サイクルと称します。
クロック同期サイクルではほかに、ユーザインタフェースで指定された動作クロックにCPUエミュレーションのクロック速度を近づける処理も行っています。

●関連ソース
arch9821.c

クロック同期サイクル

クロック同期サイクルは、エミュレータの電源ON時に起動されるスレッドのメイン処理で、電源OFFが要求されるまでループ処理を継続します。ループ内では大きく以下のようなフローをとります。

1. ループの先頭で前回のサイクルでかかった時間を取得します。(初回は直前に行っている初期化処理からの差分になります)
2. 登録されているイベントの実行までの待ち時間との差分を取り、実行時間が来ていたらイベント関数を呼びます。
3. CPU処理にかかったクロック数の累積が、ユーザインターフェースで指定した動作クロックに相当する値に達したら、それまでにかかった実際の処理時間と指定した動作クロックの単位時間と差分を取り差分を積算します。(実際のコードではここで実際の動作クロックをUIへ表示するための移動平均を算出するための処理も行っています)
4. 積算した時間と、次に実行する必要がある同期イベントまでの時間のいずれか短い方が1msを超えていて、スリープが禁止されていない状態のとき、1ms単位でOSのスリープ(ウェイト)処理を行います。
5. ループの最後にCPU処理サイクル関数を呼び出します。
2.のイベントの登録についてですが、時間同期が必要な処理は必要となる周辺デバイスが必要となるタイミングでイベントを登録するという形になっています。イベントの登録にあたり、登録する周辺デバイスは登録時からイベント発生時までの時間とイベント発生時に実行する処理関数を渡します。クロック同期サイクルではこれらを配列で管理するとともに、配列の使用状況をビットで管理します。

CPUが命令を実行するとき、命令ごとに処理時間がかかります。動作クロックの算出のために私のエミュレータでは命令を実行する関数の戻り値に処理にかかる論理的なクロック数を返すようにしています。もっとも、正確にクロック数を算出するためには同じ命令でもオペレーションコードのサイズやオペレーションコードが奇数アドレスから始まるか偶数アドレスから始まるか、オペランドでメモリアクセスが必要かどうか等々でクロック数が変わってくるものなのですが、そこまで厳密な算出は行っていません(むしろ割と適当です)。ですので、UI画面上で実際の動作クロックとして移動平均を表示していますが、正確性は低いもので目安程度のものとなっています。

メモリアクセスとI/Oアクセス

クロック同期サイクルと同じファイルに処理がありCPUエミュレーションに先立って説明しておく必要があるため、メモリアクセスとI/Oアクセスについてここで説明します。
CPUの命令でメモリアクセスが発生したとき、エミュレータで処理を行うにあたりリード、ライトそれぞれについて以下の2つのケースが考えられます。

1. メインメモリやROM領域のように、エミュレータがアロケートしたメモリ領域をそのまま参照するケース
2. メモリマップトI/OやVRAMのようにメモリアクセスによって特定の機能を実現したり、特殊な結果を出す必要があるケース
本エミュレータでは1のケースはアロケートしたメモリの先頭から、エミュレータ上の物理アドレス引いたポインタをベースポインタとして保持し、2のケースはアクセスするアドレスを引数に渡す処理関数を保持するようにしています。
このポインタまたは関数の保持は、メモリ領域を最小で4キロバイトごとに分割し、それぞれの領域のリードライトそれぞれについて保持している情報がポインタなのか関数なのかとともに、テーブルとして管理しています。
このメモリのアクセス方法を管理しているテーブルは、例えばバンク切り替え可能なメモリ領域やグラフィックモードが256色モード、EGC、GRCGで切り替わるごとにアクセス時の処理関数が変わるような領域は、モードを切り替えるタイミングで動的に値を切り替えます。こうすることで、CPU側からはアクセスの方法を意識することなく適切なアドレスへのアクセスや機能の実現を行うことが出来ます。

I/Oアクセスはメモリアクセスと違って機能的なものしかないため、ポートアクセスがあった場合には対応する機能を実現する関数を呼ぶようにしています。
I/Oはメモリと違って領域をブロックで区切るようなことが出来ないためポート0000h~00FFhまでは関数テーブルで処理していますが、それより大きい値のポートはif~elseを使って値をマスクして比較を繰り返すというあまり賢くない方法で実装しています。将来あるか分かりませんが、プログラム外からdll等で機能拡張できるようにすることを考えた場合、現在の実装はあまりよい実装とはいえない感じです。(正確には設計当初スケーラビリティを持った形にしようと思って実装した残骸があるのですが、今の使い方だと完全に殺してしまっているという状況です...)