ロジアナでフロッピーディスクを読んでみた

[令和にフロッピーディスクドライブ(FDD)を買ってみた](https://qiita.com/yunkya2/items/594b9f367b733e97bfbb) のですが残念ながら思ったのと違っていたので、もう少し遊んでみます。

## ロジアナを繋いでみる

FDDのデータ転送速度は2HDで500kbpsです。この程度であれば手持ちのロジアナ(ロジックアナライザ)で十分解析できる速度なので、ディスクからのデータを読んでみます。

このFDDで使われていたUSBインターフェース基板が以下の記事でも検証されていました。

https://jestineyong.com/examining-and-testing-a-small-universal-fdc-to-usb-adapter/

記事中に、基板上に出ているFDDの信号線が載っていたので[(ここです)](https://jestineyong.com/wp-content/uploads/2021/10/8-2.jpg)、2.INDEX と 24.RDATA それと GND にロジアナを繋いで、ディスクアクセス中の信号を覗いてみます。

![DSC_5904.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642667/a879221f-7af0-7795-8826-c0865dbea61b.jpeg)

INDEX(下図の0)はディスクが回転してインデックスホールを検出するたびにlowになる信号、RDATA(1)はドライブがヘッドから読みだしているデータそのものです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642667/f6be8595-9204-f535-bbf0-39df14170917.png)

RDATAの方を拡大してみると0→1→0のパルスが規則的に並んでいて、このパルスの間隔でデータを記録しているのが分かります。最も狭い幅の周波数もちゃんと500kHzになっていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642667/2c0446c1-a805-f120-571e-9441592bbdbe.png)

## データを解析してみる

フロッピーディスクのデータは、MFM(Modified Frequency Modulation)という変調方式で記録されています。"Modified"というからには、元のFM(Frequency Modulation[^1])があって、初期の1S, 2Sといったディスク(Single density:単密度)はFM、それ以降の2D(Double density:倍密度)、2DD(Double density Double track:倍密度倍トラック)、2HD(High density:高密度)はすべてMFMで記録されているそうです。

[^1]:FMラジオのFMと同じ意味です。例で言うと、1のビットではデータパルスとクロックパルスの両方、0のビットではクロックパルスのみが記録されるので、1のビットは0のビットの倍の周波数でパルスが発生し、データの値が周波数の違いに置き換えられる = 周波数変調になっていることが分かります。

FM、MFMの記録方式の違いは以下の通りです。データが1の時だけ発生させるデータパルスと、データのビットとビットの間に毎回発生させるクロックパルスがあって、FMではこの両方のパルスを記録する一方、MFMでは以下の規則でクロックパルスを記録します。
  * 通常はクロックパルスを記録しない
  * 0のデータが連続する場合のみ、0と0の間のクロックパルスを記録する

これにより、同じデータを記録する場合の周波数がFMの半分になるため、逆に周波数を倍にすることで同じ記録メディアでも2倍のデータ量を記録することができるという仕掛けです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642667/76055502-9ff4-38c6-8361-9abd610ae044.png)

MFMのデータのデコードは、発生しているパルスの間隔を調べることで行えます。

|前回のデータ|パルス間隔 x1|パルス間隔 x1.5|パルス間隔 x2|
|----------:|-------:|----------:|-------:|
|         0 |       0|         01|  エラー|
|         1 |       1|          0|      01|

前回デコードしたデータが1だった場合、クロック周波数の1倍後に次のパルスが来たら次のデータは1、1.5倍後だったら次のデータは0、という風にデコードしていきます。
(表で「エラー」となっている組み合わせは、ディスクのトラック内での同期を取るための、アドレスマークという特殊なデータを表すために使われます(後述))

先ほどの波形を、上記の規則に従ってデコードしてみると、0x4Eというデータが2つ現れました。
この値はフロッピーディスクで実際のデータやIDが入る前後のギャップ領域で使われる値と一致していて、この部分がギャップ領域の波形を表していることが確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642667/09458643-f327-631d-ea0f-19064476ecff.png)

## 更に解析してみる

### パルス間隔の分類

ロジアナ([これです](https://www.dreamsourcelab.com/product/dslogic-series/))のビューアで波形のデータをCSVファイルとして出力すると、こんなデータが取れます。

```
; CSV, generated by libsigrok4DSL 0.2.0 on Sun Oct 27 01:29:03 2024
; Channels (2/16)
; Sample rate: 50 MHz
; Sample count: 1.75261440 G Samples
Time(s), 0, 1
0,1,1
6.22344492,1,0
6.22344522,1,1
6.22344684,1,0
6.22344714,1,1
6.22344884,1,0
   :
```

入力に変化があった際の時刻と各入力ピンの値(ここではINDEX,RDATAの順)が並んでいるだけの単純なフォーマットなので、これをPythonスクリプトで解析してみます。

```python:fddecode1.py
#!/usr/bin/env python3
import csv
import sys

fname = sys.argv[1]

with open(fname,'r') as f:
  reader = csv.reader(f)

  wsum = 0
  ptm = 0

  for l in reader:
    if not (l[0][0] >= '0' and l[0][0] <= '9'):
      continue     # データのない行は読み飛ばす

    tm = float(l[0])  # timestamp
    f0 = int(l[1])    # INDEX
    f1 = int(l[2])    # RDATA

    w = int((tm - ptm) * 1e8)  # 経過時間(100ns単位)
    ptm = tm

    wsum += w     # 経過時間を積算

    if not f1:    # RDATAの立ち下がり
      # 前回の立ち下がりからの経過時間でパルス間隔を分類 (クロックは500kHz = 2us間隔)
      if wsum < 250:
        x = 0     #    ～2.5us : パルス間隔はクロックの1.0倍
      elif wsum < 350:
        x = 1     # 2.5～3.5us : パルス間隔はクロックの1.5倍
      elif wsum < 450:
        x = 2     # 3.5～4.5us : パルス間隔はクロックの2.0倍
      else:
        x = 3     # (エラー)

      print(f'{x}  {f0} {f1} {tm}')

      wsum = 0    # 経過時間を0に戻す
```

RDATAの立ち下がりから次の立ち下りまでの経過時間を取得して、クロック(500kHz = 2us)の何倍かで分類していきます。
(0=1.0倍 / 1=1.5倍 / 2=2.0倍)

取得したCSVデータを与えてみると、以下のようにパルス幅が0/1/2で分類されているのが分かります。

```
2  1 0 6.22782598
1  1 0 6.22782904
1  1 0 6.22783212
0  1 0 6.22783398
0  1 0 6.22783588
1  1 0 6.22783906
1  1 0 6.2278421
1  1 0 6.22784516
1  1 0 6.22784816
0  1 0 6.22785006
0  1 0 6.22785196
1  1 0 6.22785516
1  1 0 6.2278582
  :
```

### データビットの復元

得られたパルス間隔を元に、先ほどのMFM変調のデコード規則に従ってデータビットを復元します。
復元のためには「前回のデータ」が必要なのですが、解析開始時は不明なので仮で 1 を与えておきます。

```diff_python:fddecode2.pyへの差分
--- fddecode1.py
+++ fddecode2.py
@@ -8,6 +8,7 @@
   reader = csv.reader(f)

   wsum = 0
+  pbit = 1
   ptm = 0

   for l in reader:
@@ -34,6 +35,26 @@
       else:
         x = 3     # (エラー)

-      print(f'{x}  {f0} {f1} {tm}')
+      # パルス間隔からデータビットを復元する
+      if pbit:   # 前回のデータビットは1だった
+        if x == 0:
+          nbit = "1"
+        elif x == 1:
+          nbit = "0"
+        elif x == 2:
+          nbit = "01"
+        else:
+          nbit = "x"
+      else:      # 前回のデータビットは0だった
+        if x == 0:
+          nbit = "0"
+        elif x == 1:
+          nbit = "01"
+        elif x == 2:
+          nbit = "X"
+        else:
+          nbit = "x"
+      pbit = 0 if nbit[-1] == '0' else 1   # 「前回のデータビット」を更新
+
+      print(f'{nbit:2} {x}  {f0} {f1} {tm}')

       wsum = 0    # 経過時間を0に戻す
```

以下のように、パルス幅からデータビットが得られました。
データビットを並べて8ビットずつ区切っていくと、
`01001110:01001110:01..`
ギャップデータである0x4Eの並びが現れました。

```
01 2  1 0 6.22782598
0  1  1 0 6.22782904
01 1  1 0 6.22783212
1  0  1 0 6.22783398
1  0  1 0 6.22783588
0  1  1 0 6.22783906
01 1  1 0 6.2278421
0  1  1 0 6.22784516
01 1  1 0 6.22784816
1  0  1 0 6.22785006
1  0  1 0 6.22785196
0  1  1 0 6.22785516
01 1  1 0 6.2278582
  :
```

### アドレスマークの検出

ここまでで入力から0 1のビットの並びを得られましたが、これだけではどこからどこまでの8ビットが1バイトになるのかが分かりません。このために、ディスクのフォーマット時に埋め込まれる「アドレスマーク」という特殊なデータを利用します。

アドレスマークはデータとしては 0xA1 が書かれるのですが、下図のように4ビット目からのビット0の並びのうち、本来なら真ん中に入るべきクロックパルスが存在しません(ミッシングクロック)。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/642667/71c55912-21d8-6118-9c1f-31bdbd1bf134.png)

これは先ほど出てきたパルス間隔からのデコードの表で「エラー」にあたる部分で、これを手掛かりにバイトデータの境界を得ることができます。

アドレスマーク検出のために、パルス間隔の検出結果を蓄積する処理を追加します。
パルス間隔が x2 x1.5 x2 x1.5 の順に来ることになるので、パルス間隔分類後のデータとしては `2121` が来るのを待つことになります。
そして、このアドレスマークを手掛かりに、データビットを8ビットずつ区切ってバイトデータとして出力します[^2]。

[^2]:蓄積するデータビットが8ではなく9ビット分なのは、1回のパルスで2ビットが出力されて、これがバイト境界をまたぐケースがあるためです。

```diff_python:fddecode3.pyへの差分
--- fddecode2.py
+++ fddecode3.py
@@ -8,7 +8,9 @@
   reader = csv.reader(f)

   wsum = 0
+  xx = ''
   pbit = 1
+  bits = ''
   ptm = 0

   for l in reader:
@@ -35,6 +37,10 @@
       else:
         x = 3     # (エラー)

+      # アドレスマーク検出のためにパルス間隔の並びを蓄積する(過去8回分)
+      xx += str(x)
+      xx = xx[-8:]
+
       # パルス間隔からデータビットを復元する
       if pbit:   # 前回のデータビットは1だった
         if x == 0:
@@ -56,6 +62,21 @@
           nbit = "x"
       pbit = 0 if nbit[-1] == '0' else 1   # 「前回のデータビット」を更新

-      print(f'{nbit:2} {x}  {f0} {f1} {tm}')
+      # 得られたデータビットを蓄積する(過去9ビット分)
+      bits += nbit
+      bits = bits[-9:]
+
+      # アドレスマークを検出してデータビットからバイトデータを得る
+      out = "__"     # バイトデータが不明
+      if xx.endswith("2121"):      # アドレスマークを検出
+        out = "##"   # アドレスマーク
+        bits = ""    # バイト境界なのでデータビットをクリア
+        pbit = 1     # 「前回のデータビット」は1
+      else:          # バイトデータを得る(0/1と判断できたデータが8個揃った場合)
+        if len(bits) >= 8 and all(c in '01' for c in bits):
+          out = f'{int(bits[0:8], 2):02X}'
+          bits = bits[8:]
+
+      print(f'{out}  {nbit:2} {x}  {f0} {f1} {tm}')

       wsum = 0    # 経過時間を0に戻す
```

CSVデータを与えて、アドレスマークが検出できた箇所 `##` から先を見てみます。

```
##  0  1  0 0 7.15186446
__  1  0  0 0 7.1518664
__  01 2  0 0 7.15187042
__  0  1  0 0 7.15187342
__  X  2  0 0 7.15187742
##  0  1  0 0 7.15188044
__  1  0  0 0 7.15188236
__  01 2  0 0 7.15188638
__  0  1  0 0 7.1518894
__  X  2  0 0 7.15189334
##  0  1  0 0 7.15189638
__  1  0  0 0 7.15189836
__  1  0  0 0 7.15190032
__  1  0  0 0 7.15190232
__  1  0  0 0 7.1519043
__  1  0  0 0 7.1519063
__  1  0  0 0 7.15190828
__  1  0  0 0 7.15191024
FE  0  1  0 0 7.15191336
__  0  0  0 0 7.1519153
__  0  0  0 0 7.15191728
__  0  0  0 0 7.15191926
__  0  0  0 0 7.15192126
__  0  0  0 0 7.15192326
__  0  0  0 0 7.15192522
__  0  0  0 0 7.15192724
00  0  0  0 0 7.15192922
__  0  0  0 0 7.15193122
__  0  0  0 0 7.15193322
__  0  0  0 0 7.15193524
__  0  0  0 0 7.15193724
__  0  0  0 0 7.15193924
__  0  0  0 0 7.1519412
__  0  0  0 0 7.1519432
00  0  0  0 0 7.1519452
__  0  0  0 0 7.15194718
__  0  0  0 0 7.15194914
__  0  0  0 0 7.15195118
__  0  0  0 0 7.15195316
__  0  0  0 0 7.15195514
__  0  0  0 0 7.15195708
01  01 1  0 0 7.15196016
__  0  1  0 0 7.1519632
__  0  0  0 0 7.15196516
__  0  0  0 0 7.15196712
__  0  0  0 0 7.15196912
__  0  0  0 0 7.1519711
__  01 1  0 0 7.15197412
02  01 2  0 0 7.15197816
```
デコード後のバイトデータを見てみると、アドレスマーク3個から `## ## ## FE 00 00 01 02` とデータが続きます。
実はここは、これから続くデータがどのセクタのものかを表すIDフィールドという部分で、FE の後のデータはそれぞれ シリンダ番号/サイド番号/セクタ番号/セクタ長さ を表します。
このデータの場合「シリンダ0 サイド0 セクタ1 512バイトセクタ」という意味になります。

### データ出力

ここまでくれば、後は得られたバイトデータをまとめて出力するだけです。
最終的に出来上がったPythonスクリプトが以下の通りとなります。
(長いので畳んでます)

<details>

```python:fddecode.py
#!/usr/bin/env python3
import csv
import sys

fraw = False	# 生データを出力するか
f2dd = False	# ディスクが2DDか(パルス幅が倍になる)
fname = False	# ファイル名
for a in sys.argv:
  if a == '-r':
    fraw = True
  elif a == '-d':
    f2dd = True
  else:
    fname = a

with open(fname,'r') as f:
  reader = csv.reader(f)

  wsum = 0
  xx = ''
  pbit = 1
  bits = ''

  xascii = ''
  xcnt = 0

  pl = None        # 1行前のデータ
  for l in reader:
    if not (l[0][0] >= '0' and l[0][0] <= '9'):
      continue     # データのない行は読み飛ばす

    if not pl:
      pl = l
      continue

    tm = float(pl[0])  # timestamp
    f0 = int(pl[1])    # INDEX
    f1 = int(pl[2])    # RDATA

    if f1 == int(l[2]):
     continue         # RDATAに変化がない

    ptm = float(l[0]) # 前回のタイムスタンプ
    pl = l

    w = int((ptm - tm) * 1e8)  # 経過時間(100ns単位)

    wsum += w     # 経過時間を積算

    if not f1:    # RDATAの立ち下がり
      # 前回の立ち下がりからの経過時間でパルス間隔を分類
      if not f2dd:  # 2HDの場合 (クロックは500kHz = 2us間隔)
        if wsum < 250:
          x = 0     #    ～2.5us : パルス間隔はクロックの1.0倍
        elif wsum < 350:
          x = 1     # 2.5～3.5us : パルス間隔はクロックの1.5倍
        elif wsum < 450:
          x = 2     # 3.5～4.5us : パルス間隔はクロックの2.0倍
        else:
          x = 3     # (エラー)
      else:         # 2DDの場合 (クロックは250kHz = 4us間隔)
        if wsum < 500:
          x = 0     #    ～5.0us : パルス間隔はクロックの1.0倍
        elif wsum < 700:
          x = 1     # 5.0～7.0us : パルス間隔はクロックの1.5倍
        elif wsum < 900:
          x = 2     # 7.0～9.0us : パルス間隔はクロックの2.0倍
        else:
          x = 3     # (エラー)

      # アドレスマーク検出のためにパルス間隔の並びを蓄積する(過去8回分)
      xx += str(x)
      xx = xx[-8:]

      # パルス間隔からデータビットを復元する
      if pbit:   # 前回のデータビットは1だった
        if x == 0:
          nbit = "1"
        elif x == 1:
          nbit = "0"
        elif x == 2:
          nbit = "01"
        else:
          nbit = "x"
      else:      # 前回のデータビットは0だった
        if x == 0:
          nbit = "0"
        elif x == 1:
          nbit = "01"
        elif x == 2:
          nbit = "X"
        else:
          nbit = "x"
      pbit = 0 if nbit[-1] == '0' else 1   # 「前回のデータビット」を更新

      # 得られたデータビットを蓄積する(過去9ビット分)
      bits += nbit
      bits = bits[-9:]

      # アドレスマークを検出してデータビットからバイトデータを得る
      out = "__"     # バイトデータが不明
      if xx.endswith("2121"):      # アドレスマークを検出
        out = "##"   # アドレスマーク
        bits = ""    # バイト境界なのでデータビットをクリア
        pbit = 1     # 「前回のデータビット」は1
      else:          # バイトデータを得る(0/1と判断できたデータが8個揃った場合)
        if len(bits) >= 8 and all(c in '01' for c in bits):
          out = f'{int(bits[0:8], 2):02X}'
          bits = bits[8:]

      # 得られたデータを出力
      if not fraw:
        if out != '__':
          cc = '.'
          if out != '##':
            cc = chr(int(out, 16))
            if cc < ' ' or cc >= '\x7f':
              cc = '.'
          xascii += cc            # ASCIIダンプデータ

          print(f'{out} ',end='')
          xcnt += 1
          if xcnt == 16:          # 16バイトごとにASCIIダンプ
            print(f'  {xascii}')
            xcnt = 0
            xascii = ''

          if out == '##':         # アドレスマークが来たら時刻表示して改行
            xascii += ' ' * (16 - xcnt)
            print('   ' * (16 - xcnt) + f'  {xascii} {tm}')
            xcnt = 0
            xascii = ''
      else:
        print(f'{out}  {nbit:2} {x}  {f0} {f1} {tm}')

      wsum = 0    # 経過時間を0に戻す
```

</details>

出力は以下のようになります。
先ほど書いたように、アドレスマーク後 `FE` から始まるデータがIDフィールドで、その後 `FB` から始まるデータがセクタのデータそのものを格納したデータフィールドとなります。
ディスクの先頭セクタにあるブートセクタが確認できました[^3]。

[^3]:0xFBの後、セクタ長である512バイトのセクタデータが続き、その後は2バイトのCRCとなります。その後ろのデータは次のアドレスマークが来るまで無視されます。

```
##                                                .                7.15188044
##                                                .                7.15189638
FE 00 00 01 02 CA 6F 4E 4E 4E 4E 4E 4E 4E 4E 4E   ......oNNNNNNNNN
4E 4E 4E 4E 4E 4E 4E 4E 4E 4E 4E 4E 4E 00 00 00   NNNNNNNNNNNNN...
00 00 00 00 00 00 00 00 00 00 02 ##               ............     7.15259556
##                                                .                7.15261162
##                                                .                7.15262764
FB EB 3C 90 4D 53 44 4F 53 35 2E 30 00 02 01 01   ..<.MSDOS5.0....
00 02 E0 00 40 0B F0 09 00 12 00 02 00 00 00 00   ....@...........
00 00 00 00 00 00 00 29 B6 40 8A 72 4E 4F 20 4E   .......).@.rNO N
41 4D 45 20 20 20 20 46 41 54 31 32 20 20 20 33   AME    FAT12   3
C9 8E D1 BC F0 7B 8E D9 B8 00 20 8E C0 FC BD 00   .....{.... .....
7C 38 4E 24 7D 24 8B C1 99 E8 3C 01 72 1C 83 EB   |8N$}$....<.r...
3A 66 A1 1C 7C 26 66 3B 07 26 8A 57 FC 75 06 80   :f..|&f;.&.W.u..
CA 02 88 56 02 80 C3 10 73 EB 33 C9 8A 46 10 98   ...V....s.3..F..
F7 66 16 03 46 1C 13 56 1E 03 46 0E 13 D1 8B 76   .f..F..V..F....v
11 60 89 46 FC 89 56 FE B8 20 00 F7 E6 8B 5E 0B   .`.F..V.. ....^.
03 C3 48 F7 F3 01 46 FC 11 4E FE 61 BF 00 00 E8   ..H...F..N.a....
E6 00 72 39 26 38 2D 74 17 60 B1 0B BE A1 7D F3   ..r9&8-t.`....}.
A6 61 74 32 4E 74 09 83 C7 20 3B FB 72 E6 EB DC   .at2Nt... ;.r...
A0 FB 7D B4 7D 8B F0 AC 98 40 74 0C 48 74 13 B4   ..}.}....@t.Ht..
0E BB 07 00 CD 10 EB EF A0 FD 7D EB E6 A0 FC 7D   ..........}....}
EB E1 CD 16 CD 19 26 8B 55 1A 52 B0 01 BB 00 00   ......&.U.R.....
E8 3B 00 72 E8 5B 8A 56 24 BE 0B 7C 8B FC C7 46   .;.r.[.V$..|...F
F0 3D 7D C7 46 F4 29 7D 8C D9 89 4E F2 89 4E F6   .=}.F.)}...N..N.
C6 06 96 7D CB EA 03 00 00 20 0F B6 C8 66 8B 46   ...}..... ...f.F
F8 66 03 46 1C 66 8B D0 66 C1 EA 10 EB 5E 0F B6   .f.F.f..f....^..
C8 4A 4A 8A 46 0D 32 E4 F7 E2 03 46 FC 13 56 FE   .JJ.F.2....F..V.
EB 4A 52 50 06 53 6A 01 6A 10 91 8B 46 18 96 92   .JRP.Sj.j...F...
33 D2 F7 F6 91 F7 F6 42 87 CA F7 76 1A 8A F2 8A   3......B...v....
E8 C0 CC 02 0A CC B8 01 02 80 7E 02 0E 75 04 B4   ..........~..u..
42 8B F4 8A 56 24 CD 13 61 61 72 0B 40 75 01 42   B...V$..aar.@u.B
03 5E 0B 49 75 06 F8 C3 41 BB 00 00 60 66 6A 00   .^.Iu...A...`fj.
EB B0 42 4F 4F 54 4D 47 52 20 20 20 20 0D 0A 52   ..BOOTMGR    ..R
65 6D 6F 76 65 20 64 69 73 6B 73 20 6F 72 20 6F   emove disks or o
74 68 65 72 20 6D 65 64 69 61 2E FF 0D 0A 44 69   ther media....Di
73 6B 20 65 72 72 6F 72 FF 0D 0A 50 72 65 73 73   sk error...Press
20 61 6E 79 20 6B 65 79 20 74 6F 20 72 65 73 74    any key to rest
61 72 74 0D 0A 00 00 00 00 00 00 00 AC CB D8 55   art............U
AA 49 3A 4E 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C   .I:N............
9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C   ................
9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C   ................
9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C   ................
9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C   ................
9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C   ................
9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 9C 00 00   ................
00 00 00 00 00 00 00 00 00 01 ##                  ...........      7.1627685
##                                                .                7.1627845
##                                                .                7.16280048
```

## (追記)X68k/PC98のディスクも読んでみる

ここまではPCで一般的だった2HD 1.44MBのフロッピーディスクを扱ってきました。
そもそもの発端は買ったFDDが2モードだったため、X68000やPC-9801で使われていた1.23MBのディスクが扱えなかったからですが、この違いはメディア的にはディスクの回転数が毎分300回転か360回転かの違いだけなので、回転数が300回転のままだったら、単に出てくる信号のパルス間隔が 360/300 倍になっているだけのはず。

そこで、コマンドラインオプションを追加して、'-6' が指定されたら毎分360回転のディスクを読めるように、パルス間隔を分類するためのパラメータを調整してやります。
ついでに '-d' が指定されたら2DDのディスクも読めるようにしています(単に値を倍にするだけ)。

```diff_python:fddecodex.pyへの差分
--- fddecode.py
+++ fddecodex.py
@@ -4,15 +4,26 @@

 fraw = False   # 生データを出力するか
 f2dd = False   # ディスクが2DDか(パルス幅が倍になる)
+f360 = False   # ディスクが360rpmか(パルス幅が360/300倍になる)
 fname = False  # ファイル名
 for a in sys.argv:
   if a == '-r':
     fraw = True
   elif a == '-d':
     f2dd = True
+  elif a == '-6':
+    f360 = True
   else:
     fname = a

+# パルス間隔分類用パラメータ
+if f2dd:      # 2DDの場合 (クロックは250kHz = 4us間隔)
+  pw = [ 500, 700, 900 ]
+elif f360:    # 2HD 360rpmの場合 (クロックは417kHz = 2.4us間隔)
+  pw = [ 300, 420, 540 ]
+else:         # 2HDの場合 (クロックは500kHz = 2us間隔)
+  pw = [ 250, 350, 450 ]
+
 with open(fname,'r') as f:
   reader = csv.reader(f)

@@ -49,24 +60,14 @@

     if not f1:    # RDATAの立ち下がり
       # 前回の立ち下がりからの経過時間でパルス間隔を分類
-      if not f2dd:  # 2HDの場合 (クロックは500kHz = 2us間隔)
-        if wsum < 250:
-          x = 0     #    ～2.5us : パルス間隔はクロックの1.0倍
-        elif wsum < 350:
-          x = 1     # 2.5～3.5us : パルス間隔はクロックの1.5倍
-        elif wsum < 450:
-          x = 2     # 3.5～4.5us : パルス間隔はクロックの2.0倍
-        else:
-          x = 3     # (エラー)
-      else:         # 2DDの場合 (クロックは250kHz = 4us間隔)
-        if wsum < 500:
-          x = 0     #    ～5.0us : パルス間隔はクロックの1.0倍
-        elif wsum < 700:
-          x = 1     # 5.0～7.0us : パルス間隔はクロックの1.5倍
-        elif wsum < 900:
-          x = 2     # 7.0～9.0us : パルス間隔はクロックの2.0倍
-        else:
-          x = 3     # (エラー)
+      if wsum < pw[0]:
+        x = 0     # パルス間隔はクロックの1.0倍
+      elif wsum < pw[1]:
+        x = 1     # パルス間隔はクロックの1.5倍
+      elif wsum < pw[2]:
+        x = 2     # パルス間隔はクロックの2.0倍
+      else:
+        x = 3     # (エラー)

       # アドレスマーク検出のためにパルス間隔の並びを蓄積する(過去8回分)
       xx += str(x)
```

当初は、(2モードドライブでは認識できない)X68k/PC98フォーマットのディスクを入れると、ドライブ側がフォーマットチェックのため勝手にディスクの最終トラックにシークしてしまって上手く行かなったのですが、ロジアナでの読み込みの最中にドライブにREZERO UNITコマンドを送ることで[^4]、トラック0に移動した時の信号を取ることができました。

[^4]:USB FDDの制御に使われるUFIコマンド(の中のSCSIコマンド)の1つで、ドライブのヘッドをトラック0に移動します。コマンド制御の方法については長くなるのでここでは割愛します。

得られたCSVファイルをスクリプトで解析すると、X68000のブートセクタが読めました。ファームウェアが頑張れば、2モードドライブでもX68k/PC98のディスクも読めるということです。

```
##                                                .                5.53446378
##                                                .                5.53448302
FE 00 00 01 03 DA 4E 13 93 93 93 93 93 93 93 93   ......N.........
93 93 93 93 93 93 93 93 93 93 93 93 93 80 00 FF   ................
FF FF FF FF FF FF FF FF FF FF ##                  ...........      5.53532654
##                                                .                5.53534588
##                                                .                5.53536516
FB 60 3C 90 58 36 38 49 50 4C 33 30 00 04 01 01   .`<.X68IPL30....
00 02 C0 00 D0 04 FE 02 00 08 00 02 00 00 00 00   ................
00 00 00 00 00 00 00 00 00 00 00 00 20 20 20 20   ............
20 20 20 20 20 20 20 46 41 54 31 32 20 20 20 4F          FAT12   O
FA FF C0 4D FA 01 B8 4B FA 00 E0 49 FA 00 EA 43   ...M...K...I...C
FA 01 20 4E 94 70 8E 4E 4F 7E 70 E1 48 8E 40 26   .. N.p.NO~p.H.@&
3A 01 02 22 4E 24 3A 01 00 32 07 4E 95 66 28 22   :.."N$:..2.N.f("
4E 32 3A 00 FA 20 49 45 FA 01 78 70 0A 00 10 00   N2:.. IE..xp....
20 B1 0A 56 C8 FF F8 67 38 D2 FC 00 20 51 C9 FF    ..V...g8... Q..
E6 45 FA 00 E0 60 10 45 FA 00 FA 60 0A 45 FA 01   .E...`.E...`.E..
10 60 04 45 FA 01 28 61 00 00 94 22 4A 4C 99 00   .`.E..(a..."JL..
06 70 23 4E 4F 4E 94 32 07 70 4F 4E 4F 70 FE 4E   .p#NON.2.pONOp.N
4F 74 00 34 29 00 1A E1 5A D4 7A 00 A4 84 FA 00   Ot.4)...Z.z.....
9C 84 7A 00 94 E2 0A 64 04 08 C2 00 18 48 42 52   ..z....d.....HBR
02 22 4E 26 3A 00 7E 32 07 4E 95 34 7C 68 00 22   ."N&:.~2.N.4|h."
4E 0C 59 48 55 66 A6 54 89 B5 D9 66 A6 2F 19 20   N.YHUf.T...f./.
59 D1 D9 2F 08 2F 11 32 7C 67 C0 76 40 D6 88 4E   Y.././.2|g.v@..N
95 22 1F 24 1F 22 5F 4A 80 66 00 FF 7C D5 C2 53   .".$."_J.f..|..S
81 65 04 42 1A 60 F8 4E D1 70 46 4E 4F 08 00 00   .e.B.`.N.pFNO...
1E 66 02 70 00 4E 75 70 21 4E 4F 4E 75 72 0F 70   .f.p.Nup!NONur.p
22 4E 4F 72 19 74 0C 70 23 4E 4F 61 08 72 19 74   "NOr.t.p#NOa.r.t
0D 70 23 4E 4F 76 2C 72 20 70 20 4E 4F 51 CB FF   .p#NOv,r p NOQ..
F8 4E 75 00 00 04 00 03 00 00 06 00 08 00 1F 00   .Nu.............
09 1A 00 00 22 00 0D 48 75 6D 61 6E 2E 73 79 73   ...."..Human.sys
20 82 AA 20 8C A9 82 C2 82 A9 82 E8 82 DC 82 B9    .. ............
82 F1 00 00 25 00 0D 83 66 83 42 83 58 83 4E 82   ....%...f.B.X.N.
AA 81 40 93 C7 82 DF 82 DC 82 B9 82 F1 00 00 00   ..@.............
23 00 0D 48 75 6D 61 6E 2E 73 79 73 20 82 AA 20   #..Human.sys ..
89 F3 82 EA 82 C4 82 A2 82 DC 82 B7 00 00 20 00   .............. .
0D 48 75 6D 61 6E 2E 73 79 73 20 82 CC 20 83 41   .Human.sys .. .A
83 68 83 8C 83 58 82 AA 88 D9 8F ED 82 C5 82 B7   .h...X..........
00 68 75 6D 61 6E 20 20 20 73 79 73 00 00 00 00   .human   sys....
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00   ................
  :
```

## ラズパイPico(RP2040)でも読んでみる?(かも)

ここまでは一度ロジアナで読み込んでCSVとして出力したデータを対象にしてきましたが、この程度の速度であればマイコンチップ(MCU)でも処理できそうです。
特にRaspberry Pi Picoはこの手のデータ処理が得意なPIOというハードウェアを持っているので、フロッピーディスクコントローラのようにリアルタイムでのデコードが可能かも。

と、ちょっと調べてみたところ、既にフロッピーディスクのインターフェースコネクタをラズパイPico(RP2040)に繋ぐための商品が存在していました。

https://www.adafruit.com/product/5679

これはFDDコネクタからの信号をRP2040に繋ぐためにレベル変換しているだけで、実際のデコード処理は繋いだ先のRP2040で行います。
そのためのライブラリも既に公開されていました。

https://github.com/adafruit/Adafruit_Floppy

購入したドライブは26pin FFC/FPCという異なる端子なのですが、これを34pinコネクタに変換するための商品も存在します。

https://www.amazon.co.jp/dp/B0CX2G8PVY

というわけで、これらを揃えればラズパイPicoでフロッピーディスクのデータを読み出せそうです。

## 参考文献

* 試験に出るX1 (祝一平 著 / 日本ソフトバンク)
  * フロッピーディスクの物理フォーマットを行うためにはディスク上のデータフォーマットを知る必要があるため、この本のFDCに関する記述だけでもかなりのところが分かります
  * 第9章「ディスクを回すのである」を参照
* [最新フロッピ・ディスク装置とその応用ノウハウ](https://dl.ndl.go.jp/pid/12629242) (高橋昇司 著 / CQ出版)
  * 「試験に出るX1」の中でも紹介されていましたが、フロッピーディスクの物理的な構造やそのフォーマットについて詳細に説明されています
  * [国立国会図書館デジタルコレクション](https://dl.ndl.go.jp/)で公開されているため、IDを持っていれば全文を読めます