SDカードの最近のブログ記事

フラッシュメモリベンダはここのところずっと赤字ですが、生産調整も行っているので以前に比べ価格の低下速度は鈍化しています。 それでも、現在主流のSDHCメモリカードは最大容量でだんだん底値に近づいてきているようです。

現在の単価は16GBで180円強/GB、32GBで200円強/GBです。

SDHCが32GBどまりである理由、次世代であるSDXCメモリカードの規格が不甲斐ないことなどを以前お伝えしましたが、大量生産により安価になること自体はユーザにとって歓迎ですね。

ちなみに、私の持っている一番新しいパソコン(Toshiba Portege R600-108)は、なぜか手持ちのSDHCカードを認識してくれません。 しょぼ~ん。


AVR-SDカードインタフェースは調子よく動いているので、ストリーミングを試してみました。

まず転送速度ですが、SDカードからの読み込み速度は現状でおよそ30kB/s出ています。 1秒でPC-6001のフリーエリアがいっぱいになってしまいますね(苦笑)。

ムービー

これまで、256x192ドットでの動画はオンメモリで8fpsでも厳しかったのですが、アルゴリズムを見直したところ、8fpsなら特に問題なく動作するようになりました。 in命令のみでデータが読み込め、メモリアクセスが不要なのが効いています。 ちなみに、inirを連続させて生データを読み込む方法だと、理論値でもかなり厳しいのと、現在はinirのスピードにAVRが追いつきません(AVRのクロックを上げればいいだけですが)。 したがって、圧縮アルゴリズムは以前のものをそのまま使っています。

動きの激しい部分などは少々スキャンラインが見えてしまいますが、何とかがんばっている、という感じです。


音声

実はムービーより音声のほうが厄介です。 タイミングを厳密に取る必要があるためです。

  • サンプリング周波数は8000Hzおよび11025Hz
  • DMA OFF
  • 割り込み禁止
  • 1チャネルのみ使用(4ビット、実のところは2ビット程度)

上記の条件だと、そこそこクリアに聞こえますが、1セクタごとにリードコマンドを発行する関係上、ポツポツとノイズが乗ります。 リードする際の待ち時間がSDカード依存で、平準化できないためです。

ここで以前の3チャネル高品質再生ではなくて1チャネルにしているのは、ムービーとの同時再生を目論んでいるためです。

当然、同時再生をするためにはDMAをONにしなければなりません。 再生時は多少時間的余裕があるので、待ち時間を減らしてDMAをONにしてみました。

すると...かなりnoisyです。 スピードの問題というよりは、BUSACKの最中に処理ができず、再生速度にムラができてしまうためかと思います。


余談

ところで、PSGの音量は以前解説したように設定値に対して対数の関係にあるのですが、それを勘案して音量レジスタの値をエンコードしてやると、何も考えずにリニアに設定するときよりも音質が悪化します。 不思議ですが、もともと4ビットと、解像度が悪いためかもしれません。


32GBの壁(本当は壁じゃない)が問題となっていたSDHCの後継規格、SDXCがようやく発表されました。

速度(今年中に104MB/s、ロードマップ上は300MB/sまで)はともかくとして、容量上限が2TBということなのですが、大丈夫かしらん? この程度ではあまり将来を見据えているとは思えませんねぇ。 SDHCの二の舞(あっという間に最大容量に到達)にならないか心配です。

ちなみにフォーマットについては、exFATになるようですが、以前お伝えしたようにライセンス上不透明な部分があります。 われわれのような個人がデバイスドライバを勝手に作れるのかどうか、現時点ではなんともいえません。 また、現在exFATはWindows XPではサポートされておらず、Windows Vista SP1(およびWindows Embedded CE 6.0)からのサポートとなります。

ちなみにexFATの規格からすると、これは64bit FATとでもいえるもので、2TBではなく16EB(Exabyte)までサポートしています。SDHCのときは物理的に2TBまでサポート可能なのにもかかわらず32GBまでのサポートにとどめていましたが、今回はその2TBが上限ということで、なんか釈然としません。

なんか発表の瞬間からして不安な要素いっぱいな気がするのですが...。

以前出たような複数カードのストライピングで高速化&1.8インチ SSD化できないものか、ちょっと思考実験してみましょう。


サイズと容量について

現時点で、100円ショップでも売ることが可能な格安のmicro SDカードがあります。 こいつを10枚でも20枚でも使ってストライピングできないものでしょうか?

サイズ的には、1.8インチHDD/SSDの体積は

8.0 x 54.0 x 78.5 = 33,912mm3
  

となります(東芝製MK8007GAHデータシートより)。

一方、micro SDカードのサイズは、

11.0 x 15.0 x 1.0 = 165mm3
  

となります。

物理配置や基板、配線等を無視すれば、1.8インチHDD/SSDのスペース内にmicro SDは200個以上入ることになります。

実際にはソケットに入れる必要があるでしょう。 こちらの製品例では、ソケットの外形寸法は

13.7 x 15.3 x 1.4 = 293.454mm3
  

です。また、プッシュプル方式ではないですが、こちらのアダプタ

12.6 x 14.5 x 1.5 = 274.050mm3
  

と、さらに小さくなっています。

計算だけだとこれでもHDD/SSDに100個以上の体積を収められますが、現実的には無理でしょう。

実際には、最大限でもこんな感じじゃないかな?

20081213-MicroSD.png

縮尺はあわせてあります。 緑色のが基板部分で、斜め線のがmicroSDカードスロットです。 厚さを考えると裏側にも同じだけ装備できるので、計24枚入ります。

これで、2GBのmicroSDを使えば48GB。 メディア代がおよそ2,500円。 安い~!


マイコンは使えるか?

次はこれのコントローラです。

趣味の電子工作の範囲だと、SDカードの入出力は簡単に可能で、もっぱらストレージの主流ですね。 ただ、電子工作ではプログラムが簡単なSPIモードを使うため、SDカード側のクロック制限はおおよそ25MHz(約3MB/sec)となります。

また、AVRを使うとすると、最大周波数は20MHz、SPI入出力ではその半分のクロックが最大供給クロックなので、結局10MHzです。 すると、ビット単位の入出力なので1.25MB/secとなり、あまり高速ではありませんね。 PICの場合も最大動作周波数は20MHzのようなので、似たり寄ったりです。

でも、超小型のATtinyあたりを16個並列動作させて、1ビットごとに分散させればスピードも16倍(パラレルATAの信号は16ビット幅)。 これでやっと20MB/secだ~。

どうも、これらの簡単マイコンを使うソリューションはあまりうまくないですね。


もっと高性能なコントローラ?

組み込みCPUは非常に多種のものがあり、ハイパワーなものから小消費電力なものまで千差万別です。

でも、一般的に上記のマイコンよりはかなり高速であり、数100MHz程度のものも珍しくありません。 SHシリーズなんか結構いいかも? これなら、ストライピングしたSDカードを十分ドライブできそうです。


というわけで、技術的には十分可能そうではあります。 さて、誰か作ってくれないかなぁ??



AVRのプログラマを作り、ブレッドボード上でプログラムできることを確認したので、いよいよ(?)まともにSDカードリーダを作り始めました。

回路図

回路図は以下のようになっています。

20081130-AVR_SD.PNG

SDカードが3.3V動作なので、途中で電圧変換をしなくて済むようにAVRはATmega8Lを使います。きわめて簡単な回路ですね。

一応説明をすると、入力の5Vは48M033Fで3.3Vに降圧しています。 ^A7と^IORQのORを割り込み入力とします。 A6~A3はすべてORをとってPB0に入力しており、I/O空間はA2~A0の3ビット分を予約しています。 PC側からみれば、ポート00~ポート07までを使います。

SDカードとの入出力はmega8Lに搭載されているSPIをそのまま使います。 同時に、ISPにも接続することにより、PC-6001からの電源をもらってプログラムできるようにしてあります。 プログラムを行う際はリセット端子がPCからの入力では都合が悪いので、スイッチをつけています。

ハード実装

手抜き実装で、プルアップ抵抗をまだ入れていませんが、とりあえず実装したものはこんな感じです。 一応この程度でも動くことはブレッドボード上で確認してあります。

20081130-AVRSD-card.jpg

中央下部に空間を空けてあるのは、あとでBIOS ROMを入れることを考えているためです。

簡単なテストプログラムを作って試してみているのですが、今のところPC本体がちゃんと起動しません...。

SD(HC)カードでは、ファイルシステムのフォーマットが物理的に制限されているわけではありません。 そのため、FAT16/2GBという規格のSD(SDHCではない)カードに、FAT16の規格を越える4GBサイズのものがありました。また、既存のSD(HC)カードに、規格以外のフォーマットをかけることも可能です。

SDHCが採用しているFAT32の論理上限は2TBなので、ソフト的な観点からは32GBを越えるカードも可能です。 しかし、実際には売られていません。なぜでしょう。

と思っていたら、英語版Wikipediaに情報がありました。


SDカードの仕様では、カードに128ビットのIDが入っています。 HCではないSDカード(仕様1.x)では、このうち12ビットがメモリクラスタの数(1~4,096)、3ビットがクラスタあたりのブロック数(4, 8, 16, 32, 64, 128, 256, 512)を表しています。 SDカード1.xのうち、古い仕様では、ブロックサイズが512バイト固定のため、最大容量は次のように求められます。

4,096(メモリクラスタ数) x 512(ブロック数/クラスタ) x 512(ブロックサイズ) = 1GB
  

より新しい1.xの仕様では、上記のIDフィールドにブロックサイズを表す4ビットの値を持って1,024および2,048バイトをサポートするようになり、1GBを越えるカードをサポートするようになりました。 しかし、これによりデバイスによっては1GBを越えるカードを読めないことも出てきました。 例えば、私の使っていたVodafoneの705SHはmicroSDカードをサポートしていますが、1GBまでのカードしか認識しません。 これなどは、おそらくこの古い仕様が原因ではないかと思います。

さて、SDHC(仕様2.0)では、メモリサイズを22ビットのフィールドで表します。 値は512kBの倍数です。 しかし、現状では22ビットのうち16ビットしか使われていません。 すなわち、フィールドの最大値は65,536となり、

512(kB) x 65,536 = 32GB
  

となります。 これが「32GB上限」のトリックです。

22ビットをフルに使うと、最大値は

512(kB) x 65,536 x 64(追加の6ビット分) = 2,048GB
  

となり、FAT32の論理最大の2TBまでサポートできることがわかります。

(11 Nov. 2008追記) 2TBはデフォルトのクラスタサイズ512バイトの場合です。FAT16のときのようにクラスタサイズを大きくすればさらに大容量化できます。

(31 May 2009追記) コメントで指摘されましたが、現在はディスクのパーティションの開始位置がMasterBootRecordのfirstSectorに、パーティションのサイズがBIOSParameterBlockのbigTotalSectorsに、それぞれDWORDで格納されているので、それぞれ32ビットの値が上限となり、そのままではクラスタサイズを広げることにより大容量化してもセクタ位置を認識できなくなります。

もちろん今これを行えば、4GBのSDカードのように互換性の問題も抱えることにはなるのですが、規格上はサポート可能にしておきながら、なぜ制限してしまったのでしょうか。

ちなみに、Windows XPが32GBを越えるFAT32のフォーマットを行わないのは、単にマーケティング上の理由です(NTFSに誘導したいため)。SDHCカードが32GB止まりであることとは、おそらく何の関係もありません。SDHCカードにNTFSなんか採用したら最後、激しくMicrosoft縛りがかかるばかりか、アクセスするためのソフトウェア・ファームウェアが複雑になりすぎて実用にならないでしょう。いくらSDカードアソシエーションのメンバにMicrosoftが入っているからといって...ねぇ。

なお、SD(HC)カードおよびホストアダプタを製造販売するには、SDアソシエーションのメンバになる必要があり、メンバに課される年会費は$1,500、加えて製造販売のためのロイヤリティは年$1,000です。まぁ、この手の会費としては格安ともいえます。そのせいか、SDアソシエーションのメンバ企業は非常に多く、「何でこの会社が?」というような企業もメンバに入っています。でも、意地でもソニーは入っていません

SDHCは登場からあっという間に最大容量の32GBに達し、 その32GBのSDHCカードが値下がりし始めています

SSDの代わりとしての利用も期待できるわけですから、早いところ何とかして欲しいものです。


(以下 11 Nov. 2008追記および修正)

FAT32の仕様自体は2TBまでサポートしているのは上記のとおりですが、以下の制限があります。

1ファイルのサイズは4GBまで

これは良く知られていますね。 理由は、ファイルの情報が格納されるディレクトリエントリにある (詳しくはこのページのDirectory Entry参照)ファイルサイズのフィールドが32ビット(4バイト、DWORD)分しかないためです。 FATのクラスタチェインだけを考えれば4GBを越えるファイルを表現することもできますが、ファイルサイズフィールドとの不整合が発生するため、アプリケーション誤動作の原因になりますし、高レベルAPIではエラーになる可能性があります。

容量が大きくなるにつれ、いわゆる「デフラグ」や空きスペースの計算量が極めて大きくなる

FATは、容量が大きくなるとフラグメンテーションが発生しやすいという問題はよく知られていますね。

ではなぜ計算量が大きくなるかというと、FATでは領域がクラスタ単位で管理されているFile Allocation Table(だからFAT)を基にして動作するためです。FAT32では、クラスタごとに32ビット(4バイト)の情報を持っています。

FATの各エントリは、次のクラスタが何番なのか、ファイルの最後なのか、利用されていないのか、不良なのかなどの情報を表しています(リンクリスト)。これが、FAT32では28ビット分まで使われるので、最大容量まで使い切ると228エントリ存在することになり、FAT領域の大きさは

4(バイト/エントリ) x 228(エントリ) = 1GB

という容量になってしまいます。空き容量を計算するために1GBの読み込みを行うのは、FATが主に使われている組み込み分野はおろか、現状のPCでも結構つらい作業でしょう。

これらの解決策として、exFATが提案されていますが、いろいろ技術的な事情(HDDには使えない、Windows XPでも使えない)や大人の事情(ライセンスが不透明、仕様が公開されていない)から、そう簡単には現在のFATの代替にはならないでしょう。


昨日SSD関連の最新動向について少しまとめましたが、続報です。SDHCカードの16GB最安値が3,680円になったそうです。1GBあたり230円ですね。

以前話題になったSDカードをストライピングしてSSDにするデバイスを使えば、高速な96GBが22,000円ちょっと(デバイス除く)で手に入れられることになります。これはかなり現実的な価格ですね。




ファイルシステムのアクセス方法や、コマンドプロンプトからの引数の受け渡し方法などを書いておきました。

Sourceforgeで参照できるようにしてあります。

それと、普通のユーザーズガイドをこのサイトに出してませんでしたので、以下に置いておきます。

アダプタの販売はこちらで扱っていますのでよろしくお願いします。

さて、AVRを使ってSDカードリーダを作ろうと思っています。本体側からIn/Out 1命令で入出力ができれば、リアルタイムでの動画再生なども不可能ではありません。

とりあえず現状の回路図。

AVR_SD.PNGポイントは以下のようなところです。
  • SDカードは3.3V動作なので、回路全体を3.3V動作にしています。
  • ATmega8Lは内蔵クロックで8MHz動作させます。
  • ISPインタフェースを用意しておいて、プログラムの書き換えに備えます。
  • アドレス線は7432でプリデコードしておくことにより、AVRのI/Oを節約します。
AVRのI/Oポートが少しあまっているのですが、とりあえず未使用にしておきます。

先日秋葉原でEジスPenやらプリント基板キットやらを買い込んできたのですが、この回路図からレイアウトを起こすのが面倒でそのままになっています。きっと世の中には自動レイアウトツールもあるんでしょうね...。

続いて、ムービーエンコーダとプレーヤをリリースします。

それぞれ中にREADMEがありますので参考にしてください。

プレーヤですが、実はこのままでは再コンパイルできません。Hexameterをダウンロードし、適切にMakefileを設定しなおす必要があります。私の環境のままアーカイブにしてしまったためなのですが、直しているといつまでたってもアップロードできないので、とりあえずまずはこれで許してください。

Hexameter 2.1.2をリリースしました.

Sourcceforgeからダウンロードできます.

変わったのは、SDOS 1.1用のテンプレートを用意したことだけですのでたいして大きな変化ではないですが、もしSDカードアダプタをお持ちの方でSDカードをアクセスしたいという奇特な(?)方がいらっしゃればぜひご利用ください. また、SDOS開発時に作成したいろいろ有用なルーチンも用意してますので、良かったら使ってみてください. 戦士カートリッジ版、1M ROMアダプタ版双方で動作するはずです.

一昨日日本から戻ってきました. 時差ぼけで体の調子が悪いのですが、SDOS関連ファイルの公開に向けてファイルの整理を始めました.

Hexameter (SDCC用コンパイル環境)のテンプレートとしてSDOS 1.1用の環境を準備しているほか、ムービープレイヤーなどのソース/バイナリファイルを公開できるように整理しています. とりあえず、Hexameterの方はsourceforgeのCVSの更新は終わったのでダウンロード自身は可能ですが、簡単にダウンロードできるリリース用のアーカイブはまだ準備していません.

私はいったん「入って」しまうと夢中で作業して一気に終わらせてしまう方なのですが、気が乗らないときは途中でほっぽり出してなかなか進みません. よくないことは分かっているのですが...

PC-6001シリーズのJoystickポート用SDカードアダプタですが、いよいよ発売されることになりました。

価格は1,800円です。普通のPC用のカードアダプタと比べても高くはないでしょ?

ハードウェアは私が作ると品質が確保できないので、西田ラヂオさんにお任せしています。もちろんSDOS(ドライバ)のROM付きです。

西田ラヂオさんのところで扱っていますので、興味のある方はどうぞ。

SDカードに記録したPC-6001用のゲーム(P6ファイル)がそのまま読み込めますので、膨大なPC-6001の資産が活用可能です。 また、独自のマシン語フォーマット(MP6)にも対応しています。今後こちらの対応ソフトウェアや、開発環境も公開していく予定ですのでお楽しみに。
プリンタポートの場合

カードリーダをプリンタポートに接続する場合のコードを考えてみました。昔のパソコンの大部分は入力がBUSY信号のみですが、幸いなことにSPIで必要とする入力信号は1本なので、これで間に合います。

PC-6001のプリンタ関連のポートは以下のとおりです。
	0x91	data output
	0xc0	busy(bit1)
  
なお、回路では出力の際、インバータを通しているので、ビットは反転出力する必要があります。

では、早速コードを見てみましょう。例により、レジスタEを出力としています。
-- 
	ld	d, #0xff
_main:
	ld	a, d
	out	(#0x91), a
	dec	a
	out	(#0x91), a
	in	a, (#0xc0)
	add	a, d
	rl	e
	; _mainからを8回繰り返す
	ret
-- 
  
ここではハードウェアの設定として、以下を仮定しています。
  • CLKはビット0とします。
  • DIはビット1~7のどこでもかまいません。
ここでのトリックは、

	add	a, d
です。ポート0xc0からの入力は、ビット1以外は常に0なので、これで入力がある(a=0x02)場合は0xffを足すとキャリーが立ちます。しかも、レジスタDを使いまわせるので、利用するレジスタがA, F, D, Eの4つのみです。

結構これは重要で、このルーチンの外側でループを使ってバッファに書き込みを行うので、

	ld	(hl), e
  
のようにバッファへのポインタ(この場合HL)とループカウンタを利用します。ジョイスティックの場合はバイト入力でAFDEHLを使ってしまい、残りのレジスタは2つ。ループカウンタ用のレジスタをBにしてdjnzでまわしても、ぎりぎりレジスタが足りないので、実は裏レジスタを利用しています。惜しい...。

プリンタポートだとその必要がないのでexxと裏表のレジスタ転送の分が浮きます。バイトごとにこの処理が必要なので、1セクタあたり12T*512回分。合計6,144Tで、意外に馬鹿になりません。

もし、SDカードからの入力信号が1のときにポート0xc0の値が0になるようであれば、上記の代わりに

	ld	c, #0x02
を準備しておいて、

	sub	c
とするとよいでしょう。

T-stateは1bitあたり53、1バイトで441となります。裏レジスタを利用しなくて済む分も考慮して、1セクタあたりの時間は約260,000T、期待できる転送レートは3kB/s(VDG on)または6kB/s(VDG off)となります。前回よりさらに1.5倍のスピードになりますね。

なお、これは現在のSDカードアダプタを、ケーブル配線のみ変更すれば適用可能です。ただ、テストはしていませんのでご注意ください。

高速読み出しの工夫
第1回で説明したように、ジョイスティックポートからのデータ入力はかなり手間がかかります。

高速化は最もたくさん実行する部分から行うのが定石なので、心臓部であるバイト入力をする部分を最適化するのが最も効果的です。
(ポート0xa0)<-0x0f	レジスタ0x0fをラッチ
(ポート0xa1)<-0x10	DIを書き込み
(ポート0xa1)<-0x11	クロックを反転
(ポート0xa0)<-0x0e	レジスタ0x0eをラッチ
A<-(ポート0xa2)	データ読みこみ、ビット0にデータが入る
    
これで1ビット。1バイト入力するにも8倍かかります。これを限界まで高速にする挑戦をします。

最初のコード

まず、Z80の入出力は

in	a, (n)
out	(n), a
    
がそれぞれ11 T-state(以下11Tと表記します)で最速ですので、これを使います。
同じポートから連続してバイト列を読み取りたい場合などは
ini
inir
outi
otir
でブロック転送するのがいいのですが、ここでは残念ながら適しません。
入力結果はレジスタeに入れると仮定します。
-- 
	ld	e, #0x00	; 結果
	ld	b, #0x08	; ループカウンタ
_loop:
	sla	e
	ld	a, #0x0f
	out	(#0xa0), a
	ld	a, #0x10
	out	(#0xa1), a
	ld	a, #0x11
	out	(#0xa1), a
	ld	a, #0x0e
	out	(#0xa0), a
	in	a, (#0xa2)
	and	a, #0x01
	jr	nz, _zero
	set	0, e
_zero:
	djnz	_loop
	ret
-- 
  
最初は上記のような感じでした。実際には入力ポートの判別を動的にやっていたので、もっと複雑だったのですが。
上記のコードのT-stateを計算すると、1bitあたり123T~126Tで、1バイトあたり1,003T~1,027Tとなります。

PC-6001のM1サイクルにはウェイトがあるらしいです。
海外のMSX関連の資料(たとえばこことか)を見たところ、やはりM1に1サイクル追加されるようです。よって、すべてのオペレーションのT-stateはZ80の仕様のドキュメントにあるT-stateから1増加させた数値になります。おそらくPC-6001も同じだと思われます。
ただし、この記事の中の数値はM1ウェイトを入れていない数値です。
これにセクタリード用のコマンド送信やらループ管理やらを入れて、1セクタ(512バイト)のリードにかかる時間はおよそ580,000T、実効速度2MHzとすると290msecということになります。実際にはさらにFATの管理コードの実行時間があるので、もっとかかります。

最適化

ここで、時間が短縮できるものがあります。それが以下の3種類です。
  1. Aレジスタのセットアップ
    上記のソースでは、いちいちAレジスタに即値代入をしていましたが、これは7Tかかります。あらかじめ別のレジスタに値を準備しておけばレジスタ間転送で4Tで済みます。
  2. Eレジスタの値の代入
    最初はandを使ってゼロフラグ経由で入力を判断していました。しかしうまいことに、入力がアキュームレータの第0ビットに入ってくるので、4Tのrrca一発でキャリーフラグに結果を追い出せます。また、8回シフトするのでEレジスタへ最初の0代入は実は不要です。
  3. ループ
    ループを展開すればサイズは大きくなりますが、djnzの分の時間は浮きますね。
というわけで、次のようなコードになりました。
-- 
	ld	hl, #0x0e0f
	ld	d, #0x10
_main:
	ld	a, l
	out	(#0xa0), a
	ld	a, d
	out	(#0xa1), a
	inc	a
 	out	(#0xa1), a
	ld	a, h
	out	(#0xa0), a
	in	a, (#0xa2)
	rrca
	rl	e
	; _mainからを8回繰り返す
	ret
-- 
  
inでデータを読み取ったあと、rrcaでビット0をキャリーに追い出して、それをeのビット0から入れています。
これでT-stateは1bitあたり83、1バイトあたり691と、最初のコードの70%ほどになります。さらに外部でも工夫して、1セクタあたり約390,000T、2MHzで200msec以下となりました。

計算上ではなくて実測してみると、およそ2kB/s出ているので、そちらから考えれば1セクタ250msです。FAT管理などのオーバヘッドを考えるとおよそ妥当なところでしょう。

なお、よく知られているようにVDGからのBUSREQを止めると、画面は乱れますが速度は約2倍、約4kB/sとなります。

ちなみに、バイト出力はもっと高速です。inする必要がなく、レジスタの再ラッチが不要なためです。

もしプリンタポートで入出力したとすれば、やはり同じ理由で入出力が少し高速になるでしょう。ただし、プリンタポートには電源が来ていないので、ジョイスティックポートよりも物理的な接続が面倒になります。

これがバイト入力に関して私が考え付いた限界です。もし上記でもっと最適化、高速化できる方法があったら教えてください。


FATの構造(2)

前回に続き、FATの構造を理解します。

BIOS Parameter Block

前回説明したMaster Boot RecordでfirstSectorの位置にはBIOS Parameter Blockがあります。その構造体は、FAT12/FAT16の場合、以下のようになっています。なおFAT32だと、定義が異なります。

struct BIOSParameterBlock {
	BYTE jmpOpCode[3];
	BYTE OEMName[8];
	WORD bytesPerSector;	/* bytes/sector (512) */
	BYTE sectorsPerCluster;	/* sectors/cluster */
	WORD reservedSectors;	/* reserved sector for BPB */
	BYTE numberOfFATs;	/* the number of file allocation tables */
	WORD rootEntries;	/* the number of root entries (512) */
	WORD totalSectors;	/* the number of secters for this partition */
	BYTE mediaDescriptor;	/* 0xf8: Hard Disk */
	WORD sectorsPerFAT;	/* number of sectors for FAT */
	WORD sectorsPerTrack;	/* sector/track (not used) */
	WORD heads;		/* heads number (not used) */
	DWORD hiddenSectors;	/* hidden sector number */
	DWORD bigTotalSectors;	/* total sector number */
	BYTE driveNumber;
	BYTE unused;
	BYTE extBootSignature;
	DWORD serialNumber;
	BYTE volumeLabel[11];
	BYTE fileSystemType[8];	/* "FAT1?   " */
	BYTE loadProgramCode[448];
	WORD sig;		/* 0x55, 0xaa */
};


ここで重要なのは、以下の情報です。

bytesPerSector
セクタあたりのバイト数。SDカードなら通常512です。フロッピーや昔のハードディスクで256の場合があります。SD-OSではここが512でない場合は異常と判断しています。以降、この値は512として話を進めます。

reservedSectors
BPBに割り当てられているセクタ数。BPBのセクタ位置にこの数を足すと、FATのセクタ位置が求まります。

sectorsPerFAT
1つのFATに割り当てられているセクタ数。

numberOfFATs
FATの数。通常FATは2組あるので、この数値は2です。FATのセクタ位置にsectorsPerFAT * numberOfFATsを足すと、ルートディレクトリエントリのセクタ位置が求まります。

sectorsPerCluster
1クラスタに含まれるセクタ数。FATではデータの格納位置をクラスタ番号で管理するので、この数は重要です。

totalSectors, bigTotalSectors
総セクタ数。totalSectorsは16ビットしかないので、約32MBまでしか表せません。これが昔あった「32MBの壁」ですね。これより大きな場合はtotalSectorsは0で、bigTotalSectorsに総セクタ数が入っています。この数値は32ビットなので、2TBまで表せます。あくまでセクタ数の話で、他にクラスタ番号の制約があるので話はそんなに簡単ではないですが。

rootEntries
ルートディレクトリのエントリ数。すなわちルートディレクトリに入れることのできるファイル・ディレクトリの数です。FAT16まではこの数値は512固定となります。

FAT

BIOS Parameter Blockの次にはFATがあります。ここには、「あるクラスタの次に格納されているデータ位置を示すクラスタ番号」が入っています。そのクラスタ番号は12/16/32ビットで表され、これがFAT12/FAT16/FAT32と言われる所以です。

FAT12だとクラスタ番号は0x000~0xfff=4096個あり、各エントリは12ビットなので、FATの最大サイズは

4,096(エントリ) * 12(bit) / 8(bits/byte) = 6,144バイト = 6kB = 12セクタ

となります。FAT16なら、

65,536(エントリ) * 16(bit) / 8(bits/byte) = 131,072バイト = 128kB = 256セクタ

あります。実際のサイズはBIOS Parameter BlockのsectorsPerFAT * bytesPerSectorで求めます。FATが占めるディスク領域は、やはりBIOS Parameter BlockのsectorsPerFAT * numberOfFATsで求められます。

各エントリは以下のような意味を持っています(FAT12/FAT16)。

0x000 / 0x0000
未使用
0x001 / 0x0001
予約
0x002 - 0xff6 / 0x0002 - 0xfff6
現在のクラスタの次のデータが格納されているクラスタ番号
0xff7 / 0xfff7
不良クラスタ
0xff8 - 0xfff / 0xfff8 - 0xffff
現在のクラスタでデータが最終することを示す

与えられたクラスタ番号に相当するFATエントリを見つけるには、

FAT開始位置 + クラスタ番号 * クラスタエントリの大きさ

となります。16ビットFATなら、2バイトごとにリトルエンディアンで格納されているので話は簡単です。

00000000: F8 FF FF FF 12 34 56 78 ...
エントリ: 00 00 11 11 22 22 33 33 ...
上記の場合、クラスタ番号0のエントリは 0xfff8、クラスタ番号3のエントリは0x7856です。

12ビットFATでは、各エントリが1.5バイトと半端なので、ちょっと計算が必要となります。たとえば、FATが開始位置から以下のようになっているとします。

00000000: F8 FF FF 12 34 56 78 9A ...
エントリ: 00 10 11 22 32 33 44 54 ...
ここで、クラスタ番号0(偶数番号)のエントリは1バイト目と2バイト目の下位4ビットを合わせて F8 F -> 0xff8 となります。
クラスタ番号3(奇数番号)のエントリは5バイト目の上位4ビットと6バイト目を合わせて 3 56 -> 0x563 となります。
やはりリトルエンディアンなので注意してください。

Root Directory Entry

FATの次に格納されているのがルートディレクトリエントリです。
Root Directory Entryの開始セクタは、以下で求められます。

FATの開始セクタ + sectorsPerFAT * numberOfFATs
FAT12/FAT16ではエントリ数が512個固定(BIOS Parameter BlockのrootEntries)となっています。

ディレクトリの1エントリは32バイトなので、Root Directory Entryが占めるディスク領域は、512 * 32バイトで、32セクタです。エントリの詳細については以下で説明します。

Data Area

Root Directory Entryの次からはデータエリアとなります。
データエリアの開始セクタは、以下で求められます。
Root Directory Entryの開始セクタ + 32 * rootEntries / bytesPerSector
通常rootEntriesおよびbytesPerSectorはともに512なので、結局、

Root Directory Entryの開始セクタ + 32
ということになります。

FATファイルシステムでは、データ領域はディレクトリエントリまたはファイルデータそのものです。

データはクラスタ単位で記録されています。あるクラスタを使い切ったら、そのクラスタ番号に対応するFATのエントリを参照することで、次のクラスタ番号を求めることができます。次のクラスタ番号が0xff8(FAT12)/0xfff8(FAT16)以上なら、次のクラスタはなく、そこでファイルやディレクトリのデータが終わることを意味します。

クラスタ番号に対応するセクタ番号を求めるには、最初の2つのクラスタ番号が予約されているので、

 (クラスタ番号 - 2) / sectorsPerCluster + データエリア開始セクタ
となります。すなわち、データエリアの最初のクラスタ番号は2です。

Directory Entry

Root Directory Entryまたはデータエリア内でディレクトリを表す場合、各ディレクトリエントリは32バイト固定の定義でできています。その構造は以下のとおりです。

struct DirEntry {
	unsigned char name[8];		/* name */
	unsigned char extension[3];	/* extension */
	BYTE attribute;			/* attribute [arc dir vol hid sys ro] */
	BYTE reserved;
	BYTE createTimeMs;		/* for VFAT */
	WORD createTime;		/* for VFAT */
	WORD createDate;		/* for VFAT */
	WORD accessDate;		/* for VFAT */
	WORD clusterHighWord;		/* for FAT32 */
	WORD updateTime;
	WORD updateDate;
	WORD cluster;			/* start cluster number */
	DWORD fileSize;			/* file size */
};
重要なフィールドは以下のとおりです。

name, extension

ファイル名と拡張子です。
最初のバイト(name[0])が0であれば、それはディレクトリの終了を示し、0xe5であれば、削除されたエントリを示します。

nameには"."および".."のエントリが存在し、それぞれカレントディレクトリ、親ディレクトリを表します。これらのエントリにも、以下に出てくるクラスタ番号は正しく格納されているので、普通のディレクトリエントリとして扱うことが出来ます。

attribute

各ビットがこのファイルエントリの属性を格納します。MSB -> LSBの順で、

N/A N/A Archive Directory Volume Hidden System Readonly
となります。VFATのロングネームエントリの場合、ここが0x0fとなります。VFATのファイル名は32バイトでは表しきれない場合があるので、複数のディレクトリエントリを使って表現することが可能です。
ロングネームを持つファイルは、常にMS-DOS互換形式のエントリも持ちます。たとえば、

a long file name.long extension
というファイル名は、VFATのエントリ(attribute=0x0fのエントリ)を1個以上持ち、かつ

ALONGF~1.LON
というエントリも格納されているので、こちらのみでもファイルの識別は可能です。
実際、SD-OSではattributeが0x0fのものは無視します。

cluster

このファイルの最初のデータが格納されているクラスタ番号です。ファイルを読み込む場合は、このクラスタからデータを読み込みます。

fileSize

ファイルサイズがバイトで入っています。ここが32ビットなので、FATで格納できる最大ファイルサイズは2^32=4GBということになります。このエントリがディレクトリの場合は、常にサイズは0です。

大体このくらいで、FATは終わりです。FAT32やVFATはサポートしませんが、これでFAT対応のドライバを書くことができるでしょう。

なお、この記事はFAT FS フォーマットの実装についての覚え書きからかなりの情報をいただきました。ありがとうございます。

私が使っているパソコンはHDDが1.8インチなので、SSDには並々ならぬ興味があります。

ただ、安価なものはMLC(Multi Level Cell)であることが多く、転送速度が遅かったり書き換え回数が少なかったりという難点があります。また、SDカードのものは概してCFよりも転送速度が遅く、HDDの置き換えには適しません。ただ、このアプローチは悪くないですね。SDHCを6枚挿してストライピングにより容量・速度を稼ごうというわけです。

問題は、私のPCがいわゆる「東芝型1.8インチPATA」という微妙な世代の1.8インチHDDを採用していることですね。一時期かなり採用があったこのインタフェースも、今ではすっかりZIFやSATAに取って代わられてしまいました。80GBを最後に新製品も出ておらず、その80GBのMK8007GAHも既に生産完了のようで、市場では高値安定しています。

この製品も2.5インチ、SATAインタフェースです。1.8インチバージョンを作ることは搭載SDカードの枚数のみの話で難しくないでしょうが、PATAへの対応、ましてや東芝タイプへの対応は望めそうにもありません...。

SDカードのファイルシステム

SDカードはファイルシステムにFATを採用しています。ですが実は、FATであることは必須ではありません。SDカードの低レベルインタフェース仕様のみを見れば、単なるセクタ単位の入出力なので、他のファイルシステムを入れることも可能です。
セクタサイズは512バイト(2GBのカードなどでたまに例外があります)なので、効率的な読み込みのためには最低512バイトのバッファが必要です。書き込みをする場合は、フラッシュメモリの特性である消去単位(数kB~数100kB)を考慮しないと性能や寿命の問題が出るのでさらに面倒ですが、ここではとりあえず読み込みだけを考えます。

ただ、現在販売されているSDカードはすべてFATフォーマット済みですし、パソコンを始めとしてデジタルカメラや携帯電話に至るまで、FATをサポートしているので、他のフォーマットを採用するのは相互運用性の面からは現実的ではありません。

ちなみに、デフォルトでMMC/SDカードは12ビットまたは16ビットFAT、SDHCカードは32ビットFATとなっています。通常はVFATとなっており、ロングファイルネームに対応しています。

さてそのFATですが、情報が少なく、実装はなかなか大変です。さらにZ80上での実装となるとほとんどWeb上には例がありません。AVRなんかだとMP3プレイヤーの実装例などがあるので比較的ソースを見つけることができるのですけどね。

そしてそのソースなのですが、バイト配列に対してオフセットを直接指定してアクセスしているものがほとんどで、非常に読みにくいです。そこで、今回は構造をきちんと定義した上で実装することからはじめました。ただこれも良し悪しで、gccなどの進化したCコンパイラならいいのですが、Z80で使えるsdccなどだと構造体の使い方によっては効率の悪いコードを吐き出します。実装ではこの辺も勘案してありますが、ここでは綺麗な世界(?)で説明します。

FATの構造

セクタ0: Master Boot Record

最初のセクタはいわゆるMBRです。SDカードに限らず、フロッピーディスクやハードディスクなど、ほとんどすべてのブロックデバイスは、初期化後に初めてカードを読み込むときは必ずセクタ0を読み込んで、基本情報を得ます。FATの場合、その構造は以下のようになっています。

struct MasterBootRecord {
BYTE checkRoutineOnX86[446];
struct {
BYTE bootDescriptor; /* 0x80: bootable device */
BYTE firstPartitionSector[3]; /* 1st sector number */
BYTE fileSystemDescriptor; /* FAT type */
BYTE lastPartitionSector[3];
DWORD firstSector; /* BPB sector number */
DWORD numberOfSectors;
} partitionTable[4]; /* up to four partitions per drive */
WORD sig; /* 0x55, 0xaa */
};


checkRoutineOnX86は、主にx86 PCのBIOS向けと考えていいでしょう。PCはフロッピーであろうがハードディスクであろうが、ブート可能なデバイスを読み込んだときにここにあるコードを実行して、初期化を進めるようです。SDカードは普通ブートデバイスではないですし、x86に限った話でもないのでここの情報にはあまり意味はありません。

この中で実装に重要な情報は以下です。

partitionTable
パーティションごとの情報を格納する。ひとつのディスクには最大4つのパーティションが格納できるので、この構造体も4つある。
firstSector
パーティションの最初のセクタ、BIOS Parameters Blockの位置を表す。通常SDカードはパーティションは1つしかないので、partitionTable[0].firstSectorを見ればよい。
sig
シグネチャで、0x55, 0xaaの順番で格納されている。これはレコードの正当性を判断するのに使う。

それから、fileSystemDescriptorというフィールドがあります。これはパーティションのFATの種類を以下のように表します(一部)。

0x01:FAT12
0x04または0x06:FAT16
0x0b:FAT32
0x0c:FAT32 (int32拡張)
0x0e:FAT16 (int32拡張)

FAT32は無視するとしても、この0x0eが曲者です。実は、Windows XPなどで容量の小さなSDカードをフォーマットすると、実際にはFAT12でフォーマットされているにもかかわらず、fileSystemDescriptorは0x0eになります。そのため、実際にはこのフィールドだけではFATの厳密な種類を判別できません。

あとで説明しますが、FAT12とFAT16ではデータアクセス時のFATの読み方が違うため、厳密に判定できないと問題です。そのためこのフィールドは使えません。市販されているSDカードをそのまま使う分には問題ないのですが、Windowsでフォーマットした途端に読めなくなってしまいます。

なんだか長くなったので、続きは次回...。

拙作ののSDカードアダプタは、ジョイスティックポート経由で接続し、データのやり取りをすることができます。これから少しづつその説明をしようと思います。

AY-3-8910

PC-6001のジョイスティックポートはPSGであるAY-3-8910が持つ、2つの汎用8ビットパラレルI/Oポートに接続されていて、I/Oポート0xa0~0xa2でアクセスすることができます。この仕組みはI/Oポートの番号も含めてMSXと同じです。

0xa0:  レジスタラッチ
0xa1:  レジスタへの書き込み
0xa2:  レジスタからの読み出し

I/Oポートアクセスのために利用するAY-3-8910のレジスタは以下のとおりです。

0x07:    I/O選択
0x0e:    ポートA
0x0f:     ポートB

PC-6001の回路

ここまではAY-3-8910の仕様なのですが、PC-6001の回路では、0x0e、0x0fの2つのレジスタが直接2つのジョイスティックポートにマッピングされているわけではないので、話は少し面倒です。
基本的に、レジスタ0x0eは読み出し専用、レジスタ0x0fは書き込み専用です。そして、実際のジョイスティックポートへの出力は各ポートあたり3本のみです。SDカードのSPIモードでは、出力3本、入力1本を使うため、ぎりぎり間に合います。

レジスタ0x0fのビットは以下のようになっています。

*OE  SEL  J1-8  J2-8  J1-7  J1-6  J2-7  J2-6

OE:    出力イネーブル(0で出力可能)
SEL:  入力するジョイスティックの選択(1=ジョイスティック1, 0=ジョイスティック2)
Jx-y:   ジョイスティックポートxのyピンに出力するデータ

SDカードアダプタ

前述のように、SDカードをSPIモードで使う場合、出力3本、入力1本が必要なので、SDカードアダプタはジョイスティックポートのピンを以下のように割り当てています。

ピン1: 入力データ(MISOまたはDO)
ピン6: クロック(CLK)
ピン7: セレクト(CS)
ピン8: 出力データ(MOSIまたはDI)

データの読み書き

AY-3-8910を使う場合、レジスタのラッチを行ってからそのレジスタに対する読み書きをする必要があるので、通常のバイト指向のI/Oポートよりも入出力が面倒です。また、SPIモードの仕様として、1ビットのデータ入出力にクロックをPC側から入れてやる必要があるので、さらに手順が煩雑になります。まぁ、これのおかげでスピードをPC側の都合で速くも遅くもできます。
実際の入出力は以下のような感じになります。

準備
(ポート0xa0)<-0x07   レジスタ0x07をラッチ
A<-(ポート0xa2)        レジスタ0x07の値を読み込む
A = A | 0x80        
(ポート0xa0)<-A        レジスタ0x0eを入力、レジスタ0x0fを出力に設定

出力
(ポート0xa0)<-0x0f                     レジスタ0x0fをラッチ
(ポート0xa1)<-0x10または0x00    ビットを書き込み
(ポート0xa1)<-0x11または0x01    クロックを反転

入力
(ポート0xa0)<-0x0f      レジスタ0x0fをラッチ
(ポート0xa1)<-0x10     DIを書き込み
(ポート0xa1)<-0x11     クロックを反転
(ポート0xa0)<-0x0e     レジスタ0x0eをラッチ
A<-(ポート0xa2)         データ読みこみ、ビット0にデータが入る

ふぅ。これで1ビットの入出力です。8回繰り返してようやく1バイトの入出力となります。なお、データはMSB→LSBの順で転送します。

PC-6001のジョイスティックポートに接続可能なSDカードアダプタを作成しました。

SDカードドライバ

ハード設計

手持ちの部品で作っているので抵抗とかがテキトーです。SDカードの仕様上は、DINとDATは50kohm〜100kohmでプルアップする必要がある ようですが、それだとLED電流が少なくなりすぎて光らなくなります。ちゃんとバッファを入れたりすればいいんでしょうが、面倒なのでこれでやっていま す。
Joystick-SDカードアダプタ回路図
サンハヤトのユニバーサル基板(ICB-86)上に実装しています。部品代は500円くらいでしょうか。

ATARI仕様のジョイスティックポートには通常のDSUB9ピンのシリアルケーブルだと、角が邪魔をして入りません。そのため、ケーブルはコネクタ部分の耳を切り落として挿入しています。


ソフト設計

ドライバは、Tiny-FatFsを参考にさせていただきましたが、以下のような事情から、ほとんど作り直しました。

  • Z80で利用できるSDCCコンパイラでコンパイルできるようにする。
  • PC-6001の容量の制限から、リードオンリーにする。
  • やはり容量の制限から、SDカードのみの対応とする。
  • やはり容量と、速度の制限から、シングルセクタ単位のアクセスとする。
転送速度

速度計測を行いました。32kBのファイルを用意し、読込み時間を計測したところ、およそ1.5kB/s程度です。手動計測なのであ まり正確に測れません。ドライバ内で割り込みを禁止する必要があるため、PC-6001のタイマ割り込みでは計測できないのです。

それでも、テープが1200bps=150bytes/sであることを考えれば、7倍の速度ということになります。PC- 6001mk2でフル実装の64kBいっぱい読んでも約40秒。PC-6001用の普通のゲームなら23kB以内なので15秒程度で読めます。 まぁ、我慢できる範囲ではないでしょうか。

ちなみに、よく知られているようにPC-6001の処理時間の大半はVDG MC6847(Video Display Generator)によるDMA転送に取られてしまいます。画面表示が乱れても良いなら、倍速読み込みも可能です。実際ムービープレイヤーではその手法をとっています。