最近は見ることが無くなったフロッピーディスクですが,AVR (ATMega1284P)を使用して,フロッピーディスクドライブ(FDD)と同じように振舞いながら,AVRのフラッシュメモリに書き込んだディスクイメージにアクセスできるFDDエミュレータを作りました.
ネットワークブートしたい古いパソコンがあり,ブート用ROMを搭載していなかったのでgPXEを使おうとしましたが,USBブートに対応しておらず,ブート目的のためにHDDやCD-ROMをわざわざ使いたくなかったのでこのようなものを作りました.
gPXEを起動できればよいという目的だったので,ハードウェアはAVRだけを使ったシンプルな構成ですが,127KB以下のディスクイメージしか保持できずディスクへの書き込みには対応していないという機能制限があります.
本ページでは特に断らない限り,フロッピーディスクと言った場合には3.5インチのフロッピーディスクを指すことにします.
フロッピーディスクは磁性体が塗られたプラスチックの円盤で,次のように記憶領域が区分けがされています.
例えば,ヘッド数が2,片面あたりのトラック数が80,1トラックあたりのセクタ数が18,1セクタが512バイトのディスクの容量は,2*80*18*512=1440KBとなります.
ヘッド,トラック,セクタの3つの値を指定することにより,ディスク上の特定の場所を表現することができます.
一般的によく使われている3.5インチディスクの種類には,表1のようなものがあります.
| フォーマット時容量 | 640KB | 720KB | 1232KB | 1440KB |
|---|---|---|---|---|
| アンフォーマット時容量 | 1.0MB | 1.0MB | 1.6MB | 2.0MB |
| 記録ディスク | 2DD | 2DD | 2HD | 2HD |
| 磁気記録方式 | MFM | MFM | MFM | MFM |
| データ転送速度(kbits/sec) | 250 | 250 | 500 | 500 |
| bytes/sector | 512 | 512 | 1024 | 512 |
| sectors/track | 8 | 9 | 8 | 18 |
| tracks/sides | 80 | 80 | 77 | 80 |
| sides | 2 | 2 | 2 | 2 |
| 回転数(rpm) | 300 | 300 | 360 | 300 |
表1には,フォーマット時容量というものとアンフォーマット時容量というものが記載されています.例えば,アンフォーマット時には2.0MBの容量を持つディスクでもフォーマット後は1440KBしか利用できません.その差分の領域には,データが壊れていないか確認するためのCRCやディスク上の位置(シリンダ,ヘッド,レコード)を示すための情報,パルス間隔を同期させるための領域(SYNC)や緩衝用領域(GAPn)などが含まれています,
実際の720KBディスクの物理フォーマットは表2のようになっています.
| 領域 | バイト数 | データ (16進) | 内容 | 備考 | |
|---|---|---|---|---|---|
| プリアンブル (146 bytes) |
80 | 4e | GAP0 | ||
| 12 | 00 | SYNC | |||
| 4 | C2 C2 C2 FC | Index Mark | C2はミッシングクロックを含む. | ||
| 50 | 4E | GAP1 | |||
| セクタ1 (658 bytes) |
IDフィールド (44 bytes) |
12 | 00 | SYNC | |
| 4 | A1 A1 A1 FE | ID Address Mark | A1はミッシングクロックを含む. | ||
| 4 | シリンダ番号(0~79) ヘッド番号(0, 1) レコード番号(1~9) セクタサイズ(2: 512bytes) |
CHRN | 128*2^Nがセクタサイズ. | ||
| 2 | xx xx | CRC | ID Address MarkとCHRNのCRC. | ||
| 22 | 4E | GAP2 | |||
| データフィールド (614 bytes) |
12 | 00 | SYNC | ||
| 4 | A1 A1 A1 FB | Data Address Mark | A1はミッシングクロックを含む. | ||
| 512 | (データ) | DATA | |||
| 2 | xx xx | CRC | Data Address MarkとDATAのCRC. | ||
| 84 | 4E | GAP3 | |||
| セクタ2~セクタ9 (658 * 8 bytes) |
... | ... | ... | ... | ... |
| ポストアンブル (182 bytes) |
182 | 4E | GAP4 | ||
720KBのディスクの場合,転送速度は250kbpsでディスクの回転数は300rpmなので,1回転には60/300=0.2秒を要してその間に転送されるデータ量は0.2*250e3/8=6250バイトで上の各領域の容量の和(1トラックのバイト数)と一致します.また,そのうちメタデータ以外の実際のデータは512*9=4608バイトです.
ミッシングクロックについては次の節で説明します.
CRCは初期値を0xFFとしたCCITT-CRCで計算されます.
一般的な2DDや2HDのフロッピーディスクでは,ビット列はMFMエンコーディングというもので符号化された上で磁性体に記録されます.
フロッピーディスクのインターフェースではデータの転送はシリアルで行われ,MFMエンコーディングされたビット列のデータがやりとりされるのでこの符号化を理解しておく必要があります.
例えば,0xB1という1バイトの値は10110001というビット列で表現されますが,ディスク上に記録する際には各データビットの間に0か1のクロックビットというものが挿入されて倍のビット長のデータが記録されます.
MFMエンコーディングの場合は次のように符号化されます(他にFMエンコーディングというものもあります):
| データ | 0xB1 | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| データビット | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | ||||||||
| クロックビット | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | ||||||||
| 記録されるビット列 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 1 |
MFMエンコーディングでは,「データビットで0と0が連続する場合は1を挿入し,それ以外は0を挿入する」という単純な規則によりクロックビットが生成されます.データが連続する場合,先頭のクロックビットは直前のデータにも影響されることになります.
ディスク上にはMSBから順番にデータが記録されていきます.
ところで,表2の中でIndex MarkやAddress Markというものはディスク上の位置を表す印として使われますが,もしユーザーが書き込んだデータ中に同じビット列が含まれているとどれが本物の印か区別がつかなくなります.そのため,Index MarkやAddress Markをディスク上に書き込む際には,ミッシングクロックといって通常のクロックビットから一部のビットを取り除いたビット列を使用します.これにより,ユーザーが書き込んだデータには絶対に出現しないビット列が書き込まれるので,これらの印が区別できることになります.表3にミッシングクロックを示します.
| データ | 0xC2 | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| データビット | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | ||||||||
| クロックビット | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | ||||||||
| 記録されるビット列 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 |
| データ | 0xA1 | |||||||||||||||
| データビット | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | ||||||||
| クロックビット | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | ||||||||
| 記録されるビット列 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
ここでは,IBM PC互換機で使われている3.5インチフロッピーディスクドライブのインターフェースについて記述します.
インターフェースには34ピンのコネクタが使われます.
各ピンの信号は表4のとおりです.
| ピン番号 | 信号 | 方向 | 名称 | 備考 |
|---|---|---|---|---|
| 2 | - | - | - | |
| 4 | - | - | - | |
| 6 | - | - | - | |
| 8 | /INDEX | Out | INDEX | negative pulseによりトラックの開始位置を示す. |
| 10 | - | - | (MOTOR ON) | |
| 12 | /DS1 | In | DRIVE SELECT 1 | ドライブの選択. |
| 14 | - | - | (DRIVE SELECT 0) | |
| 16 | /MOTOR | In | MOTOR ON | LOWになるとモーターを回転させる. |
| 18 | /DIR | In | DIRECTION SELECT | STEPが入力された場合のトラックの移動方向を決める.LOWの場合は外側から内側に,HIGHの場合は内側から外側. |
| 20 | /STEP | In | STEP | 入力されたパルスの後縁でトラックを1つ移動させる. |
| 22 | /WDATA | In | WRITE DATA | |
| 24 | /WGATE | In | WRITE GATE | |
| 26 | /TRK00 | Out | TRACK 00 | ヘッドがトラック0にあればLOW,それ以外ではHIGHを出力する. |
| 28 | /WPT | Out | WRITE PROTECT | 書込み禁止のディスクが挿入されていればLOW,それ以外ではHIGH. |
| 30 | /RDATA | Out | READ DATA | negative pulseでクロックとデータが混在したビット列を出力する. |
| 32 | /SIDE1 | In | SIDE ONE SELECT | HIGHならばヘッド0を選択し,LOWならばヘッド1を選択する. |
| 34 | /DSKCHG | Out | DISK CHANGE | 電源投入後およびディスクイジェクト状態でLOWになる.ディスクが入った状態でSTEPが入力されるとその前縁でHIGHになる. |
| 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 33 | GND | GROUND |
フロッピーディスクのインターフェースは,複数のドライブがバス接続されるためオープンコレクタ出力で負論理となっています.
DS1がデバイスを選択する信号で,これがアクティブ(LOW)になるとFDDのアクセスランプが点灯して各入力信号に応じた動作をするようになり,非アクティブ(HIGH)になると全ての出力をHi-Zにして動作を止めます.
Read Enableのような信号は無くて,基本的にMOTORとDS1がアクティブの間は常にヘッドから読み取られたビット列がRDATAに出力されます.
ところでFDDをマザーボードにつなぐケーブルは,通常FDD側の10番から16番がねじれて配線されています(つまり,MBの10番とFDDの16番,MBの11番とFDDの15番,・・・が接続される).
通常のFDDはドライブ1のデバイス選択信号(DS1)だけをチェックするようになっていて本体にはデバイス番号を切り替えるスイッチがついていないので,ケーブルをツイストさせることにより1つめのデバイスのDS1にドライブ0の選択信号が,2つめのデバイスのDS1にドライブ1の選択信号が接続されるようになっています(ケーブルセレクト).
冒頭で触れたとおり,今回はgPXEのブートに使用するという目的でできるだけシンプルなハードウェアにしました.
128KBのフラッシュROMと16KBのRAMというAVRの中では大容量のリソースを利用できるATMega1284Pを使い,ディスクイメージはAVRのフラッシュROMに格納することにして,他のメモリ等は外付けしないことにしました.おかげで回路が簡単になりましたが,1FD Linux等を入れることはできません.最近は色々な選択肢があるのでAVR以外のマイコンの利用を考えてもよいのかもしれませんが,趣味でたまに電子工作をする程度だと開発環境を用意して新しいデバイスを使い始めるのにはそれなりに手間がかかるので少し躊躇してしまいます.
今回作成するFDDエミュレータの主な仕様です:
回路図と配線図は下記の通りです.
WDATAとWGATEは未接続です.
AVRは内蔵オシレータを使いましたが,OSCCALを設定して18MHzのクロックにしています.しかしながら設定できるクロック周波数は個々のチップに依存すると思われるので,20MHzのセラミック発振子を外付けした方がよいと思います.
完成した基板は次のようになりました:
このエミュレータの基本的な動作としては,タイマー割り込みを使ってバッファに蓄えられたディスクデータを出力して(RDATAのパルス),割り込み処理を行っている以外の時間は出力すべきデータをひたすらバッファに書き込みます.DS1やMOTORやSTEPといったFDDを制御する入力信号は外部割込みでキャッチして処理します.
720KBのFDDだとデータの転送速度が250kbpsで,クロックビットを考慮すると500kbpsになるので500kHzでタイマー割り込みを発生させます.AVRを18MHzで動作させた場合は各タイマー割り込みの間に18e6/500e3=36クロック分しか命令を実行できません(さらにそのうち10クロックは割り込みハンドラへのジャンプと復帰に使われます).FDDというのはレガシーな低速デバイスだと思っていましたが,AVRで扱う場合はあまり処理能力に余裕がありません.今回はアセンブラで書きました.
IDフィールドとデータフィールドに存在するCRCの値は事前に計算してEEPROMに入れることにしました.また,MFMエンコーディングでデータビット列からクロックビット列を生成するテーブルもEEPROM上に用意しました.
メモリの利用状況は次のようになっています:
| メモリ種別 | 開始アドレス | サイズ | 内容 |
|---|---|---|---|
| Flash ROM | 0x00000 | 1KB以下 | ファームウェア本体 |
| 0x00400 | 127KB以下 | ディスクイメージ | |
| EEPROM | 0x0000 | 256 * 2 | MFMビットパターンテーブル |
| 0x0200 | 2 * 80 * 9 * 2 | IDフィールドのCRC | |
| 0x0e00 | 254 * 2以下 | データフィールドのCRC |
擬似コードで書いたファームウェアの概要は次のようになります:
interruption_handler<DS1: L->H>() {
goto main;
}
interruption_handler<MOTOR: L->H or H->L>() {
if (MOTOR == H) {
interruption_disable(timer);
} else {
interruption_enable(timer);
}
}
interruption_handler<STEP: L->H>() {
dskchg = H;
if (DIR == L) {
if (track + 1 < 80) track++;
} else {
if (track - 1 >= 0) track--;
}
update();
}
interruption_handler<timer: 500kHz>() {
RDATA = (buf_data & 0x80) ? L : H;
wait(0.2us);
RDATA = H;
buf_data <<= 1;
buf_num--;
if (buf_num == 0) {
buf_data = pop(buffer);
buf_num = 8;
}
}
# 状態出力信号の更新
update() {
DSKCHG = dskchg;
TRK00 = (track == 0) ? L : H;
}
# 初期化
init() {
dskchg = L;
track = 0;
interrupt_enable(DS1, MOTOR, STEP);
}
# メインルーチン
main() {
interruption_all_disable();
LED = L;
WPT = H;
DSKCHG = H;
TRK00 = H;
while (DS1 == H) ; // DS1がアクティブになるまで出力をオフにして待機
LED = H;
WPT = L;
update();
interruption_handler<MOTOR>();
interruption_all_enable();
buffering: // 出力すべきデータをバッファに書き込み続ける
# Output Preamble
push(buffer, xx);
...
# Output ID Field
...
# Output Data Field
...
# Output Postamble
...
goto buffering;
}
デバッグは,主にロジックアナライザでINDEXとRDATAを測定して行いました.RDATAから出力されるパルス間隔は4/6/8usのいずれかであり,その間隔からデータ・クロック混合のビット列(01/001/0001)が再現できます.
最終的にファームウェアのサイズは782 bytesになりました.
次のディスクイメージで動作確認を行いました.