/ «2007-02-03 (Sat) ^ 2007-02-18 (Sun)» ?
   西田 亙の本:GNU 開発ツール -- hello.c から a.out が誕生するまで --

Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール


2007-02-10 (Sat)

[Thoughts][Publish] Magic number 1440 (ディスクフォーマットの可視化編)

前回、MS-DOS の FORMAT.COM, Linux の superformat, fdformat、3種類のコマンドで初期化(物理フォーマット+mkfs)したディスケットイメージ全体を dd コマンドで読み込むと、それぞれの経過時間は

FORMAT.COM 48秒
superformat 50秒
fdformat 40秒

となりました。ディスク容量はすべて 1440 KiB であるにもかかわらず、なぜ10秒に及ぶ差が生じたのでしょうか?Magic number 1440 シリーズの最終となる今回は、この謎を明らかにします。

superformat 謎のメッセージ

Linux の superformat は、fdformat にはない「ディスクトラック容量測定」機能を備えています。/etc/driveprm がシステムに登録されていない場合、superformat は物理フォーマット開始前、次に示すようなメッセージを表示します。

 $ superformat /dev/fd0 
 Measuring drive 0's raw capacity
 warmup cycle:   2 199914 199863

ディスクトラックの容量測定経過が示されていますが、計測結果はプログラム内部で利用されるだけで、表示されません。Fdutils パッケージには、フロッピィディスクの容量や回転数を計測する専門ユーティリティ floppymeter が用意されていますので、そちらを使ってみましょう。

floppymeter

floppymeter の使い方は簡単で、ブランクのディスケットをドライブに挿入するだけです。

 $ time floppymeter 
 Warning: all data on the floppy disk will be lost. Continue (y/N)? y
 
  _____________________________________________
 |rotations  |average    |sliding    |missed   |
 |since start|time per   |average of |rotations|
 |of test    |rotation   |the last   |         |
 |           |since start|10         |         |
 |           |of test    |rotations  |         |
 |===========|===========|===========|=========|
 |  1000     |  199879   |  199886   |  0      |
 |___________|___________|___________|_________|
                                 
 capacity=199877 half bits (should be 200000 half bits)
 time_per_rotation=199879 microseconds (should be 200000)
 data transfer rate=499994 bits per second (should be 500000)
 
 deviation on capacity: -615 ppm
 deviation on time_per_rotation: -605 ppm
 deviation on data transfer rate: -12 ppm
 
 Insert the following line to your /etc/driveprm file:
 drive0: deviation=-615
 
 
 real    3m58.344s
 user    0m0.006s
 sys     0m0.027s

約4分かけて、トラック容量・モーター回転時間・データ転送レートの連続計測を行い、その平均が表示されます。

私が普段使用している Debian Etch ホストに搭載されているドライブのトラック容量は 199877 ハーフビット、ディスク1回転に要する時間は 199.879 ミリ秒、データ転送レートは毎秒 499.994 ビットと表示されました。

容量の単位として "half bit" と、聞き慣れない言葉が使われている点に着目してください。昔のフロッピィディスクは、FM (Frequency Modulation)方式でデータ記録が行われていましたが、現在は MFM (Modified FM)方式となっています。MFM 方式は FM 方式に比較して、同じ書き込み周波数で倍のデータを記録できることから、前者を倍密度記録、後者を単密度記録と呼びます。

いずれの方式においても、データのビット値はディスケット上の "ビットセル" に格納されますが、FM 方式ではビットセルの中央に記録されるデータビットに加えて、データ同期のためビットセルの先頭にクロックビットが記録されます。すなわち、FM 方式ではビットセルの内部に「2種類のビット」が存在することになります。"half bit" という名前の由来はここにあります。

MFM 方式のクロックビットは、連続したビットセルにデータビットが存在しない場合のみ、設定されます。このあたりの知識は、後述する "Read diagnostic" コマンドで読み込んだデータ中の「ビットずれ」を解釈する場合に必要になりますが、残念ながら現在市販されている書籍で FM/MFM 記録のビットパターンを解説したものはないでしょう。ビットずれの詳細については、1980ー1990年代に日本で発刊されたプロテクト解説書の数々が、最も丁寧に解説しています。

ディスケットのトラック容量は理想的には 200,000 ハーフビット、純粋なデータビットとしては 100,000 ビット、バイトで表現すると 12,500 バイトとなります。標準フォーマットでは、両面で160トラック(2 x 80)ですから、12,500 x 160 = 2000,000。本シリーズの最初に登場した「ディスケットのアンフォーマット時容量 2M バイト」の根拠がこれで明らかになりました。

なお、トラック容量はモーターの回転数により変化します。通常、データの書き込み周波数は "毎秒 500,000 ビット" に固定されていますので、モーターの回転が遅ければ、トラック容量は増えることになります。モーターが1回転に要する時間は200ミリ秒、毎分300回転に設定されていますが、私のドライブは 199.9 ミリ秒とやや速いため、この影響でトラック容量もやや低めになっています。

トラック容量と回転時間から、データ転送レートが計算されますが、一体どのようにして floppymeter は前者ふたつを測定するのでしょうか?回転時間から見てみましょう。

readid コマンド

初期化済みのフロッピィディスクを用意してください。まず、fdrawcmd を使い、ヘッドをトラック番号79へシークさせます。

 $ fdrawcmd seek 0 79
 0: 20
 1: 4f
 disk change
 

今度は、次のコマンドを実行してください。

 $ fdrawcmd readid 0 
 0: 0
 1: 0
 2: 0
 3: 4f
 4: 0
 5: b
 6: 2
 disk change
 
 $ fdrawcmd readid 0 
 0: 0
 1: 0
 2: 0
 3: 4f
 4: 0
 5: 7
 6: 2
 disk change

初めて登場した readid は、文字通り「ID情報を読み込む」ためのコマンドです。後ほど明らかになりますが、すべてのセクターには "ID フィールド" が前置されており、ヘッドは現在位置しているトラック上で指定されたID情報を検索し、該当したIDを持つデータフィールド上のデータにアクセスします。

readid コマンドは、以下に示す7つのバイトデータを返します。

ST0Status register 0
ST1Status register 1
ST2Status register 2
CCylinder number
HHead number
RRecord (sector) number
NNumber of bytes (128 x 2^N)

最初の3つは FDC (Floppy Disk Controller)内部のステータスレジスターの内容であり、説明は割愛します。重要なデータは続く4バイトです。先頭はトラック番号(実際はシリンダー番号ですが、両者の違いを説明する際に図が必要のため、便宜上トラックとします)、2番目はヘッド番号、3番目はセクター番号、4番目はセクター長。この「4つ組み」は、それぞれの頭文字を取り "CHRN" と呼ばれ、FDC 制御の勘所となっています。

各データは1バイト長のため、最後のセクター長は説明にあるようなパラメータとなっています。具体的には、N=1の時256バイト、N=2の時512バイト、N=7の時16384バイトになります。上記例の N は2になっていますから、FORMAT.COM で初期化されたトラックのセクター長は512バイトであることが分かります。

さて、トラックは円ですから、readid コマンドを発行し、ディスクヘッドをディスケットにロードした際にどのIDが読み込まれるかは、タイミング次第です。トラックの先頭から読み込まれる訳ではないことに注意してください。

上記の最初の実行例では、トラック番号は79 (0x4F)、ヘッド番号は0、セクター番号は11 (0x0B)になっていますが、2番目の例ではトラック・ヘッド番号は同じですが、セクター番号が7に変化しています。ディスケットが回転している様子を実感できると思います。

こうなると、連続して readid を発行したくなるのが、人情です。そこで、fdrawcmd には連続してコマンドを発行するための repeat オプションが用意されています。リザルトコードをこれまでのように、縦に表示すると画面に収まりきりませんから、short オプションを指定し、コンパクトに表示してみましょう。

 $ fdrawcmd repeat=20 short readid 0
 0x00 0x00 0x00 0x4f 0x00 0x0b 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x0c 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x0d 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x0e 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x0f 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x10 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x11 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x12 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x01 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x02 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x03 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x04 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x05 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x06 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x07 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x08 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x09 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x0a 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x0b 0x02 disk_change 
 0x00 0x00 0x00 0x4f 0x00 0x0c 0x02 disk_change 

連続20回、readid コマンドを実行してみました。「ブラボ〜!」であります。この例において、ヘッドは最初にセクター番号11に遭遇し、以後順番に12・13・14・15・16・17・18と進行。ここで1番に戻り、ふたたびセクター番号が順に1ずつ上がっていることが観察できます。

確かに、トラック上には「18個のセクター」が存在することを確認できました。

モーターの回転数を見る

先ほどの実験を少し発展させ、次のコマンドを実行してみてください。

 $ fdrawcmd repeat=20 short do_buffer print_time readid 0
 0x00 0x00 0x00 0x4f 0x00 0x0f 0x02 disk_change 494118
 0x00 0x00 0x00 0x4f 0x00 0x10 0x02 disk_change 504165
 0x00 0x00 0x00 0x4f 0x00 0x11 0x02 disk_change 514254
 0x00 0x00 0x00 0x4f 0x00 0x12 0x02 disk_change 524364
 0x00 0x00 0x00 0x4f 0x00 0x01 0x02 disk_change 534478
 0x00 0x00 0x00 0x4f 0x00 0x02 0x02 disk_change 544545
 0x00 0x00 0x00 0x4f 0x00 0x03 0x02 disk_change 554594
 0x00 0x00 0x00 0x4f 0x00 0x04 0x02 disk_change 564642
 0x00 0x00 0x00 0x4f 0x00 0x05 0x02 disk_change 574713
 0x00 0x00 0x00 0x4f 0x00 0x06 0x02 disk_change 584797
 0x00 0x00 0x00 0x4f 0x00 0x07 0x02 disk_change 594896
 0x00 0x00 0x00 0x4f 0x00 0x08 0x02 disk_change 604986
 0x00 0x00 0x00 0x4f 0x00 0x09 0x02 disk_change 615059
 0x00 0x00 0x00 0x4f 0x00 0x0a 0x02 disk_change 625118
 0x00 0x00 0x00 0x4f 0x00 0x0b 0x02 disk_change 653907
 0x00 0x00 0x00 0x4f 0x00 0x0c 0x02 disk_change 663952
 0x00 0x00 0x00 0x4f 0x00 0x0d 0x02 disk_change 673988
 0x00 0x00 0x00 0x4f 0x00 0x0e 0x02 disk_change 684022
 0x00 0x00 0x00 0x4f 0x00 0x0f 0x02 disk_change 694063
 0x00 0x00 0x00 0x4f 0x00 0x10 0x02 disk_change 704112

do_buffer と print_time オプションを新たに指定しています。print_time は実行時の時間をμ秒単位で表示するためのオプションです。do_buffer は、すべての処理が終了するまで結果をバッファー上に保管し、完了後に一括表示するためのオプションです。do_buffer を忘れると、標準出力への表示に余分の時間がかかってしまい、正確な時間計測ができなくなりますので注意してください。

この結果の中で、同じセクター番号を持つ2行に着目します。上記例の先頭は第15セクターで始まっていますが、この時のカウントは 494,118 μ秒。2回目に15セクターが登場した時のカウントは 694,063 μ秒。694063-494118 = 199945 μ秒より、ディスクの1回転には 199.9 ミリ秒かかっていることが分かります。floppymeter とほぼ同じ値です。

さらに実験を進めて、次のコマンドを実行してみてください。

 $ fdrawcmd repeat=100 short do_buffer print_time readid 0 2> rotations
 $ grep 0x01 rotations 
 0x00 0x00 0x00 0x4f 0x00 0x01 0x02 disk_change 543844
 0x00 0x00 0x00 0x4f 0x00 0x01 0x02 disk_change 743794
 0x00 0x00 0x00 0x4f 0x00 0x01 0x02 disk_change 943713
 0x00 0x00 0x00 0x4f 0x00 0x01 0x02 disk_change 1143637
 0x00 0x00 0x00 0x4f 0x00 0x01 0x02 disk_change 1343552
 0x00 0x00 0x00 0x4f 0x00 0x01 0x02 disk_change 1543463

今回は合計100回の readid コマンドを発行し、その結果を rotations ファイルにリダイレクションで保存します。fdrawcmd の結果は「標準エラー出力」に出力されるため、"2>" を指定している点に注意してください。最後に、セクター番号 0x01 を持つ行だけを grep で抽出しています。

カウンター値の差を計算すると以下のようになります。

 199911
 199915
 199924
 199919
 199950

10μ秒オーダーで回転時間に変動が見られることが分かります。Linux 自体の限界により、カーネルの影響で変動が混入している可能性もありますが、仮にモーターの回転に10μ秒の誤差が生じたとすると、

 500,000 bits/sec x 10μsec = 5 bits

から、データビット「5ビット分のずれ」が発生することになります。現在の優秀なドライブですら、1回転で5ビット弱のずれが生じる可能性がある訳ですが、昔のディスクドライブは2%にも及ぶ回転変動を持っていました。

このため、フロッピィディスクのトラックフォーマットには、「ぶれ」に対応する様々な工夫が設けられているのです。

フロッピィディスクの内部構造

トラックの構造は、大きく3つのパートから構成されます。

  • Preamble
  • Sector 1 to 18
  • Postamble

中央に、データを格納するセクター領域が位置しており、その前後にプリ・アンブル、ポスト・アンブル領域が用意されています。両者はセクターの最終尾から先頭の間に挿入されている、緩衝領域です。その目的は、モーターの回転変動に対応することがひとつ、もうひとつは多彩なフォーマットに対応するためのマージンとして設けられています。

Preamble

それでは、プリ・アンブル領域から詳細に見てみましょう。プリ・アンブルは4つのサブフィールドから構成されます。

  • Gap0 (4E : 80 bytes)
  • Sync (00 : 12 bytes)
  • Index mark (C2 C2 C2 FC)
  • Gap1 (4E : 50 bytes)

各フィールドの説明を行う前に、INDEX 信号について説明しておきましょう。何度も述べてきたように、ディスケット上のトラックは円の構造を持っています。このため、トラックのどこが "開始点" であるのかを示すために、INDEX 信号 がディスクドライブの端子から出力され、FDC に接続されています。興味のある方は、Y-E DATA 社のホームページ上で公開されている、フロッピィディスクドライブのデータシートを参照してください。

この INDEX 信号はどのように生成されているかが気になりますが、昔の8インチ・5.25インチのディスケットには小さな穴が開けられており、その穴を通じて光学的に INDEX 信号が生成されていました。現在の3.5インチ・ディスケットは、穴に代わってスピンドル・モーターから直接 INDEX 信号が出力されています。

トラックフォーマットを行うと、FDC は INDEX 信号を検出直後、Gap0 と呼ばれる緩衝用の領域を作成します。フロッピィディスクのトラック上には Gap0 から Gap4 まで合計5つの緩衝帯が存在しますが、フォーマット時の値はすべて 0x4E が使われます。

次に、12バイトのゼロで構成されるデータ(クロックビット)同期用の Sync (SYNChronization) フィールドを作成し、インデックスマークと呼ばれる4バイトのマーカーを挿入します。最後に、続くセクター領域に備えて、50バイト長の Gap1 緩衝帯が用意されています。

ID field

各セクターはIDフィールドとデータフィールドから構成されます。

  • Sync (00 : 12 bytes )
  • AM1: address mark1 (A1 A1 A1 FE)
  • ID (C H R N)
  • CRC (xx xx)
  • Gap2 (4E : 22 bytes)

IDフィールドの先頭には、ふたたび同期用の Sync サブフィールドが用意されており、4バイトのアドレスマークが続きます。その後ろには4バイト長のCHRNが位置しており、エラー検出のために2バイトのCRCが後置されています。データフィールドとの境目には Gap2 領域が作成されます。

頑丈に守られていることからも、CHRN情報がいかに重要なものであるかが分かります。

Data field

いよいよ目的のデータを格納するデータフィールドです。

  • Sync (00 : 12 bytes)
  • AM2: address mark2 (A1 A1 A1 FB)
  • Data (xx ... xx)
  • CRC (xx xx)
  • Gap3 (4E : variable)

データフィールドも、IDフィールドと同じく同期用の Sync サブフィールドから始まります。OS上で作業していると、デバイス上のデータ入出力は "No error" が当たり前ですが、裏方ではディスク変動に対応するために、これほど念入りな同期が行われているのです。

同期後は、4バイトのアドレスマークを前置したデータ領域が位置し、その後にCRC, Gap3 が続きます。なお、Gap3 の領域長だけは FDC を通じて自在に設定することが可能です。

デフォルトでは Gap3 領域に十分過ぎるほどの長さが割り当てられていますが、このサイズを縮めることで新たにセクター領域を作り出すことが可能です。実際、Linux 上には Gap3 を「けちり」、3セクターを余分に生み出した主婦顔負けの節約フォーマットが存在します。このフォーマットは、1トラックあたり21セクターになりますので、ディスク容量は 1.68 MiB となります。

蛇足ながら、Linux には先ほどの節約フォーマットに、さらにオーバートラックの手法を組み合わせ、1面83トラックとした 1.74 MiB フォーマットも用意されています。このあたりの「フォーマット亜型」に関する楽しいお話の続きは、Fdutils の info ファイルでどうぞ。

Postamble

最後のポスト・アンブルは、最終セクターフィールドの直後から、INDEX 信号が現れるまでの領域を埋める Gap4 緩衝帯として作成されます。

  • Gap4 (4E : variable)

ディスクフォーマットを見る!

これだけでは、トラックフォーマットも「絵に描いた餅」に過ぎません。何とかそのイメージをこの目で確認したいものですが、方策はないのでしょうか?

幸い、μPD765 には "Read diagnostic" と呼ばれるトラックフォーマット解析用のコマンドが用意されています。Linux などの OS が利用する通常の "Read" コマンドは、指定された「CHRNに合致した最初のセクター」がアクセス対象となりますが、"Read diagnostic" コマンドは「INDEX 信号直後のセクター」に操作が限定される特徴があります。すなわち物理的な先頭セクターを同定することが可能になるのです。

フォーマット方法の詳細は、オンラインで解説できる範囲を超えているため割愛しますが、物理フォーマットを行う際のCHRN情報はテーブルの形で FDC に入力されます。このことは、ユーザーが自在にCHRN情報を設定できることを意味しています。

例えば、あるトラックの全セクターのCHRNを "0, 0, 1, 2" に設定することも可能です。この場合、全セクターのトラック番号は0、ヘッド番号は0、セクター番号は1、セクター長は512バイトに設定されます。

このように、CHRNは実際のトラック・ヘッド・セクター長とは完全に独立したパラメータであることに注意してください。前回紹介した fdrawcmd の read コマンドにおいて、先頭のヘッド番号とは別に、CHRN情報中にヘッド番号を指定した理由はここにあります。

ともかく "Read diagnostic" コマンドで、トラック上の生データを観察してみましょう。実験にあたり、最初に fdformat で物理フォーマットのみを再実行します。フォーマット後にファイルシステムを作成すると、ビットの上書きが起こり、「ビットずれ」が途中で発生してしまうためです。フォーマットが完了したら、seek コマンドでヘッドをトラック0に移動させておきます。

 $ fdformat /dev/fd0
 Double-sided, 80 tracks, 18 sec/track. Total capacity 1440 kB.
 Formatting ... done
 Verifying ... done
 $ fdrawcmd seek 0 0
 0: 20
 1: 0
 no disk change

"Read diagnostic" 機能は fdrawcmd の man page には記載されおらず、隠しコマンドとなっています。ソースリストを読むと、この機能には "read_track" という名前が割り当てられており、次のコマンドを実行してください。

 $ fdrawcmd length=16384 read_track 0 0 0 0 7 1 27 255 > ch-0-0.img
 remaining= 0
 0: 0
 1: 24
 2: 20
 3: 1
 4: 0
 5: 1
 6: 7
 no disk change
 
 $ wc -c ch-0-0.img 
 16384 ch-0-0.img

引数の詳細には触れませんが、最も重要なパラメータは5番目の7です。これは、CHRN中のNに相当するパラメータであり、7の場合はセクター長16384バイトを意味しています。このコマンドを実行することで、現在位置しているトラック0上のヘッド0から、「インデックス信号直後のデータ領域」から16384バイトを一気に読み込みます。データはリダイレクションで ch-0-0.img ファイルに保存しました。

それでは、先頭部分から見ていきましょう。

 00000000  f6 f6 f6 f6 f6 f6 f6 f6  f6 f6 f6 f6 f6 f6 f6 f6  |????????????????|
 *
 00000200  2b f6 4e 4e 4e 4e 4e 4e  4e 4e 4e 4e 4e 4e 4e 4e  |+?NNNNNNNNNNNNNN|
 00000210  4e 4e 4e 4e 4e 4e 4e 4e  4e 4e 4e 4e 4e 4e 4e 4e  |NNNNNNNNNNNNNNNN|
 *
 00000260  4e 4e 4e 4e 4e 4e 4e 4e  4e 4e 4e 4e 4e 4e 00 00  |NNNNNNNNNNNNNN..|
 00000270  00 00 00 00 00 00 00 00  00 00 a1 a1 a1 fe 00 00  |..........????..|
 00000280  02 02 9f 3c 4e 4e 4e 4e  4e 4e 4e 4e 4e 4e 4e 4e  |...<NNNNNNNNNNNN|
 00000290  4e 4e 4e 4e 4e 4e 4e 4e  4e 4e 00 00 00 00 00 00  |NNNNNNNNNN......|
 000002a0  00 00 00 00 00 00 a1 a1  a1 fb f6 f6 f6 f6 f6 f6  |......??????????|
 000002b0  f6 f6 f6 f6 f6 f6 f6 f6  f6 f6 f6 f6 f6 f6 f6 f6  |????????????????|
 *
 000004a0  f6 f6 f6 f6 f6 f6 f6 f6  f6 f6 2b f6 4e 4e 4e 4e  |??????????+?NNNN|
 000004b0  4e 4e 4e 4e 4e 4e 4e 4e  4e 4e 4e 4e 4e 4e 4e 4e  |NNNNNNNNNNNNNNNN|
 *
 00000510  4e 4e 4e 4e 4e 4e 4e 4e  00 00 00 00 00 00 00 00  |NNNNNNNN........|
 00000520  00 00 00 00 a1 a1 a1 fe  00 00 03 02 ac 0d 4e 4e  |....????....?.NN|
 00000530  4e 4e 4e 4e 4e 4e 4e 4e  4e 4e 4e 4e 4e 4e 4e 4e  |NNNNNNNNNNNNNNNN|
 00000540  4e 4e 4e 4e 00 00 00 00  00 00 00 00 00 00 00 00  |NNNN............|
 00000550  a1 a1 a1 fb f6 f6 f6 f6  f6 f6 f6 f6 f6 f6 f6 f6  |????????????????|
 00000560  f6 f6 f6 f6 f6 f6 f6 f6  f6 f6 f6 f6 f6 f6 f6 f6  |????????????????|
 *

先ほど解説したトラック構造と照らし合わせながら見てください。fdformat によるトラックフォーマットは 0xF6 で行われており、先頭には512バイト連続する 0xF6 が観察できます。

0x200 番地からの 2B F6 はCRCコードです。

0x202 から 0x26D 番地までは Gap3 領域であり、108バイトの 0x4E で埋め尽くされています。既に述べた通り、実際にはこれほど長い緩衝領域は必要なく、1.68 MiB フォーマットの Gap3 領域長は、なんと12バイトに切り詰められています。

以上で、先頭セクターは終了し、第二セクターに続きます。

0x26E から 0x279 番地まではIDフィールドの Sync サブフィールドです。確かに12バイトのゼロが並んでいますね。

0x27A から 0x27D 番地に位置する A1 A1 A1 FE は、ID アドレスマーク。

0x27E から 0x281 番地が重要なCHRNです。0, 0, 2, 2 ですから、「トラック番号0・ヘッド番号0・セクター番号2・セクター長2」を意味しています。確かに第二セクターで間違いありません。

後の解析は、皆さんにお任せしますが、最後に 0x3000 番地付近をチェックしてみてください。

 00002d00  4e 4e 4e 4e 4e 4e 4e 4e  4e 4e 4e 4e 4e 4e 00 00  |NNNNNNNNNNNNNN..|
 00002d10  00 00 00 00 00 00 00 00  00 00 a1 a1 a1 fe 00 00  |..........????..|
 00002d20  12 02 9c 4f 4e 4e 4e 4e  4e 4e 4e 4e 4e 4e 4e 4e  |...ONNNNNNNNNNNN|
 00002d30  4e 4e 4e 4e 4e 4e 4e 4e  4e 4e 00 00 00 00 00 00  |NNNNNNNNNN......|
 00002d40  00 00 00 00 00 00 a1 a1  a1 fb f6 f6 f6 f6 f6 f6  |......??????????|
 00002d50  f6 f6 f6 f6 f6 f6 f6 f6  f6 f6 f6 f6 f6 f6 f6 f6  |????????????????|
 *
 00002f40  f6 f6 f6 f6 f6 f6 f6 f6  f6 f6 2b f6 4e 4e 4e 4e  |??????????+?NNNN|
 00002f50  4e 4e 4e 4e 4e 4e 4e 4e  4e 4e 4e 4e 4e 4e 4e 4e  |NNNNNNNNNNNNNNNN|
 *
 00003000  4e 4e 4e d0 90 90 90 90  90 90 90 90 90 90 90 90  |NNN?............|
 00003010  90 90 90 90 90 90 90 90  90 90 90 90 90 90 90 90  |................|
 *
 00003050  90 90 ff ff ff ff ff ff  ff ff ff ff ff ff 14 14  |..????????????..|
 00003060  14 01 90 90 90 90 90 90  90 90 90 90 90 90 90 90  |................|
 00003070  90 90 90 90 90 90 90 90  90 90 90 90 90 90 90 90  |................|
 *
 00003090  90 90 90 90 ff ff ff ff  ff ff ff ff ff ff ff ff  |....????????????|
 000030a0  0a 0a 0a 00 ff ff fe 7c  10 80 10 90 90 90 90 90  |....???|........|
 000030b0  90 90 90 90 90 90 90 90  90 90 90 90 90 90 90 90  |................|
 000030c0  ff ff ff ff ff ff ff ff  ff ff ff ff 0a 0a 0a 00  |????????????....|
 000030d0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
 *
 000032d0  c0 00 90 90 90 90 90 90  90 90 90 90 90 90 90 90  |?...............|

0x3000 番地前は最終セクター領域になります。0x2D1A から 0x2D1D 番地にIDアドレスマーク A1 A1 A1 FE が見え、その後方に "0, 0, 18, 2" が続いています。「18セクター」で間違いありません。18セクターのデータ領域は 0x2F49 番地で終了し、その後ろにはCRCに続いて Gap3 が始まります。Gap3 長は108バイトでしたから、0x2FB7 番地で終了します。0x2FB8 番地から Gap4 が始まり、INDEX 信号後に Gap 0 に引き継がれます。

ここでバイトイメージを観察すると、0x3003 番地を境にして 0x4E から 0x90 にデータが変化しています。この現象は、INDEX 信号の前後における、トラックフォーマットの書き始めと書き終わりの間で「ビットずれ」が起きていることを示しています(運が良ければ、ビットずれが起きていない連続データを観察できます)。

ビットずれにより、0x4E が 0x90 に化け、よく見ると12バイトの Sync フィールドがゼロから 0xFF に化けています。両者共にビットパターンが変化前後で全くことなっていますが、これは半ビット単位のずれにより、データビットではなく「クロックビット」が見えているためです。このあたり、とても面白いところなのですが、興味がある方は過去のプロテクト解説書をご参照ください。

"Wrap around" を起こした先の探索を続けましょう。ビットずれを起こしているため、解析が難しいのですが、データ長やパターンから各サブフィールドの位置を同定することができます。今回は 0x00 が 0xFF に化けているため、Sync サブフィールドの同定は簡単です。INDEX 信号直後から先頭セクターのデータサブフィールド開始までには、全部で3つの Sync が存在します。0x30C0 から 0x30CB 番地までが先頭セクター・データフィールドの Sync 領域であり、4バイトのアドレスマーク後に続く、0x30D0 番地からが先頭セクターのデータ領域となります。

以上より、このトラックの総容量は 12,496 (0x30D0) バイトであることが分かります。理想値である 12,500 バイトにほぼ近い値ですね。

このように、floppymeter は "Read diagnostic" コマンドから得られたデータのビットずれも考慮しながら、正確なトラック容量を弾き出すのです。

CHRN の妙

トラックデータを観察できたところで、今度はヘッド1側のトラックを観察してみましょう。

 $ fdrawcmd length=16384 read_track 4 0 0 0 7 1 27 255 > ch-0-1.img
 remaining= 0
 0: 4
 1: 24
 2: 20
 3: 1
 4: 0
 5: 1
 6: 7
 no disk change

今度はヘッド1ですから、read_track コマンドの第一引数は4になります(トラックはそのままなので、seek の必要なし)。

 00000260  4e 4e 4e 4e 4e 4e 4e 4e  4e 4e 4e 4e 4e 4e 00 00  |NNNNNNNNNNNNNN..|
 00000270  00 00 00 00 00 00 00 00  00 00 a1 a1 a1 fe 00 01  |..........????..|
 00000280  11 02 fe 2c 4e 4e 4e 4e  4e 4e 4e 4e 4e 4e 4e 4e  |..?,NNNNNNNNNNNN|
 00000290  4e 4e 4e 4e 4e 4e 4e 4e  4e 4e 00 00 00 00 00 00  |NNNNNNNNNN......|
 000002a0  00 00 00 00 00 00 a1 a1  a1 fb f6 f6 f6 f6 f6 f6  |......??????????|
 000002b0  f6 f6 f6 f6 f6 f6 f6 f6  f6 f6 f6 f6 f6 f6 f6 f6  |????????????????|
 *
 000004a0  f6 f6 f6 f6 f6 f6 f6 f6  f6 f6 2b f6 4e 4e 4e 4e  |??????????+?NNNN|
 000004b0  4e 4e 4e 4e 4e 4e 4e 4e  4e 4e 4e 4e 4e 4e 4e 4e  |NNNNNNNNNNNNNNNN|
 *
 00000510  4e 4e 4e 4e 4e 4e 4e 4e  00 00 00 00 00 00 00 00  |NNNNNNNN........|
 00000520  00 00 00 00 a1 a1 a1 fe  00 01 12 02 ab 7f 4e 4e  |....????....?.NN|
 00000530  4e 4e 4e 4e 4e 4e 4e 4e  4e 4e 4e 4e 4e 4e 4e 4e  |NNNNNNNNNNNNNNNN|
 00000540  4e 4e 4e 4e 00 00 00 00  00 00 00 00 00 00 00 00  |NNNN............|
 00000550  a1 a1 a1 fb f6 f6 f6 f6  f6 f6 f6 f6 f6 f6 f6 f6  |????????????????|
 00000560  f6 f6 f6 f6 f6 f6 f6 f6  f6 f6 f6 f6 f6 f6 f6 f6  |????????????????|
 *
 00000750  f6 f6 f6 f6 2b f6 4e 4e  4e 4e 4e 4e 4e 4e 4e 4e  |????+?NNNNNNNNNN|
 00000760  4e 4e 4e 4e 4e 4e 4e 4e  4e 4e 4e 4e 4e 4e 4e 4e  |NNNNNNNNNNNNNNNN|
 *
 000007c0  4e 4e 00 00 00 00 00 00  00 00 00 00 00 00 a1 a1  |NN............??|
 000007d0  a1 fe 00 01 01 02 fd 5f  4e 4e 4e 4e 4e 4e 4e 4e  |??....?_NNNNNNNN|
 000007e0  4e 4e 4e 4e 4e 4e 4e 4e  4e 4e 4e 4e 4e 4e 00 00  |NNNNNNNNNNNNNN..|
 000007f0  00 00 00 00 00 00 00 00  00 00 a1 a1 a1 fb f6 f6  |..........??????|
 00000800  f6 f6 f6 f6 f6 f6 f6 f6  f6 f6 f6 f6 f6 f6 f6 f6  |????????????????|
 *

先ほどと同様にして、IDフィールドを探索してみてください。第二セクターの CHRN は "0, 1, 17, 2" になっています。トラック0・ヘッド1までは良いのですが、セクターが17とはこれ如何に?セクター番号は2のはずですが、何かの間違いでしょうか。

読み進めると、続くCHRN は "0, 1, 18, 2", "0, 1, 1, 2" となっており、単なる間違いではなく、意図的にトラック先頭のセクター開始番号がずらされているようです。

seek コマンドを使い、トラック1以降もチェックすると、すべてのセクター開始番号が微妙にずらされていることが確認できます。

手作業による解析も最初は面白いのですが、かなり大変な作業なので、物理フォーマットのセクター番号シーケンスを自動的に解析するプログラム diskscan を作ってみました。

  $ ./diskscan
  0:0   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
   :1  16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15
  1:0  10, 11, 12, 13, 14, 15, 16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9
   :1   7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18,  1,  2,  3,  4,  5,  6
  2:0   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
   :1  16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15
  3:0  10, 11, 12, 13, 14, 15, 16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9
   :1   7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18,  1,  2,  3,  4,  5,  6
  4:0   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
   :1  16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15
  5:0  10, 11, 12, 13, 14, 15, 16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9
   :1   7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18,  1,  2,  3,  4,  5,  6
  6:0   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
   :1  16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15
  7:0  10, 11, 12, 13, 14, 15, 16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9
   :1   7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18,  1,  2,  3,  4,  5,  6
  8:0   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
   :1  16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15
 ...略

シリンダー毎に2行から構成され、左端がシリンダー番号、上段がヘッド0・下段がヘッド1から読み込んだトラックのセクターシーケンスとなります。

シリンダー0の場合、ヘッド0側のトラックは通常通りセクター番号1から始まり18で終わっていますが、裏側のヘッド1に移ると、開始セクター番号は16になっています。つまり、セクター1が「3セクター分後方」にずらされているのです。

シリンダー1になると、ヘッド0側はセクター1が9つ後方に、ヘッド1側はさらに3つ後方にずらされています。

なぜこのような「ずらし」が意図的に導入されているのでしょうか?既にお気づきの方もいらっしゃるように、ディスクは毎分300回転の速度で回っています。データの連続読み込みを行う際、ヘッド0のトラックが終了し、ヘッド1に移行する際には、「ヘッドの切り替え」作業が短時間ですが発生します。この作業中もディスケットは回転していますので、ヘッド0の18セクターを読み込みヘッド1の1セクターを読み込もうとすると、もたもたしている間にセクター1は過ぎ去っていた・・ということが起こるのです。

回転寿司での光景を思い起こしてみてください。目の前のお皿に集中しすぎた余り、お目当てのネタを取り損ねてしまった経験は、誰しもお持ちのことでしょう。これと全く同じ光景がフロッピィディスクの中で起こっているのです。

そこで、ヘッドが切り替わる際の遅れを念頭において、同じシリンダー上ではヘッド0とヘッド1に「3つのセクター差」が導入されたのです。これを専門用語で "Head skew" と呼びます。

シリンダーを変更するために seek を行う際には、さらに時間がかかります。このため、シリンダーが変わる際には「9つのセクター差」が導入されており、これを "Track skew" と呼びます。

8ビットマイコン時代は、CPU の処理スピードが遅かったため、同一トラック上のセクター間にも "Sector skew" が導入され、これを "Interleave format" と呼んでいました。

fdformat は "Head skew 3", "Track skew 9" を採用することで、最速の40秒を叩きだしたことになります。

次に、MS-DOS の FORMAT.COM の場合を見てみましょう。

$ ./diskscan
0:0   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
 :1   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
1:0   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
 :1   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
2:0   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
 :1   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
3:0   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
 :1   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
4:0   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
 :1   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
5:0   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
 :1   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
6:0   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
 :1   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
7:0   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
 :1   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
8:0   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
 :1   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
...略

fdformat とは打って変わり、整然としたシーケンスが続いています。すべてのトラックにおいて物理的な開始セクター番号は1であり、最終セクター番号は18。すなわち固定フォーマットであることが分かります。工夫の跡は全くありません。

最後に、superformat のセクターシーケンスを観察します。

 $ ./diskscan
  0:0   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
   :1  18,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17
  1:0  17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16
   :1  16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15
  2:0  13, 14, 15, 16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12
   :1  11, 12, 13, 14, 15, 16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10
  3:0   8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18,  1,  2,  3,  4,  5,  6,  7
   :1   6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18,  1,  2,  3,  4,  5
  4:0   3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18,  1,  2
   :1   1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18
  5:0  18,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17
   :1  17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16
  6:0  16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15
   :1  14, 15, 16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13
  7:0  11, 12, 13, 14, 15, 16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10
   :1   9, 10, 11, 12, 13, 14, 15, 16, 17, 18,  1,  2,  3,  4,  5,  6,  7,  8
  8:0   6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18,  1,  2,  3,  4,  5
   :1   4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16, 17, 18,  1,  2,  3
  ...略

この結果を見ると、superformat の Head skew は1〜2、Track skew は2〜5と微妙に変化しています。

同じ Linux kernel でありながら、fdformat と superformat の間でなぜ skew factor がことなるのか?この原因を知るためには、ソースリストを読むしかありません。

結論から言えば、fdformat は FDFMTBEG, FDFMTTRK, FDFMTEND 計3つの ioctl コマンドを使っているのに対して、superformat は FDRAWCMD という単一の ioctl コマンドを使っている点がことなります。

FDFMTTRK コマンドはカーネルの内部で Head skew 3, Track skew 9 に固定された物理フォーマットを行いますが、FDRAWCMD は文字通りユーザースペースから直接 FDC を制御するための ioctl コマンドであり、フォーマットに使われる CHRN データは、superformat が自身で決定しています。このような「変態 skew factor」が採用された理由は不明ですが、結果として単純な FORMAT.COM にも負けることとなりました(superformat にオプションを指定することで、fdformat と同じ skew factor を与えることは可能だと思いますが)。

となると、これからは fdformat を使いたいところですが、残念なことに FDFMTBEG, FDFMTTRK, FDFMTEND はカーネル開発コミュニティにおいて、既に "Obsolete" 扱いとなっていますので、fdformat が突然使えなくなる日は近いでしょう。fdformat ユーザは注意してください。

終わりに

つい最近まで、フロッピィディスクは手軽なデータ記録手段として花形でしたが、既にショップでディスケットを見かけることも少なくなり、市場から消え去る日は遠くないようです。

静的なデバイスが大多数を占める現在、フロッピィディスクは音がする最後の媒体のひとつであり、ビットずれを通して、デバイスの不安定さを垣間見させてくれる唯一の装置と言って良いでしょう。データ同期という、コンピュータ上の極めて基本的な仕組みを知る上でも、この上ない教材ですが、その仕組みを現代の若い人達に伝える書籍や資料はありません。

Magic number 1440 シリーズでは、fdrawcmd を使い Linux カーネルを通して FDC と会話した訳ですが、Linus 氏は自分自身で FDC の制御ドライバーを楽しみながらコーディングしていたようです。

FDC の制御には Polled mode, Interrupt mode, DMA mode という3つの方式があり、システムプログラミングの基本を順を追って学ぶことができます。fdrawcmd をただ使うよりは、自分の手で FDRAWCMD ioctl コマンドを使ったディスク解析プログラムを書く方が楽しいものです。しかし、直接 FDC の I/O ポートを操作しながら、割り込みコントローラとDMAコントローラを同時に操るプログラミングは、例えようがなく楽しい。この楽しみを往年の Linus 氏に独り占めさせておく手はありません。

ディスケットが市場から消え去るその前に、FDC を巡る楽しく深い物語を1冊書き上げたいと考えています。