file system driver の作成

メモリカードからの読み込みを目的として、 FAT からファイルを読み込みできるソースコードを書き始めている。書き込みはたぶんやらないと思う。

設計方針は下記である。

  • OS なしで動くようにする
  • C99 で実装するが、必要があれば C89 とかでもビルドできるといいな
  • disk, memory card などのデバイス依存部分は分離する
  • RAM は 0x200 byte のバッファを持ち、0x80 byte 程度の作業用 RAM を持つ
  • FAT の cluaster chain、つまり fragment の処理を効率化する
  • 乗除算をなくす

最近よく使わせていただいたソースで気になる部分がいくつかあったので、それへのオマージュというか、学習ということもある。

stdint.h の使用

名前の設定方法はいくつかあるが、 C99 標準規格ということでこれに準ずる。stdint.h がない古い処理系はそれに準じた名前を勝手に typedef すればいい。

byte order の対応

FAT の場合は 8bit の data の配列を持ち、場合によっては 16bit, 32bit の値を持つ。汎用性を持たせるために、16bit, 32bit のデータは inline 関数で持つようにする。(Ruby からとって unpack としたが、言葉が的確かちょっと自信がない)

static inline uint32_t unpack_le32(const uint8_t *buf, int offset)
{
	uint32_t v;
	buf += offset;
	v = buf[0];
	v |= buf[1] << 8;
	v |= buf[2] << 16;
	v |= buf[3] << 24;
	return v;
}

inline 関数は static の属性を与え、汎用性が高い場合はヘッダファイルに分離して複数の c ファイルから共有する。

inline をサポートしない古い処理系ではマクロで対応する。私は1行に複雑な処理を書きたくないので v という中間変数を設けて簡潔な記述を心がけているが、マクロではそういうことがやりづらい。

unpack 系がボトルネックとなるのであれば、使用する CPU 依存の専用の処理を書いてもいいだろう。

重要なのはソース中で共通となるインターフェースを持つことだ。

disk から filesystem parameter を取得する

ここからは ChaN 氏の文書に準拠して記載していく。
http://elm-chan.org/docs/fat.html

disk の先頭から 0x200 byte を取得し、BS とか BPB の prefix がつくパラメータを取得していく。文書によっては struct の形で cast してしまうやり方を書いているが、これはバイトオーダーや memory alignment が違うと使えないし、公式のパラメータの名称がわかりにくい事もあるので行わない。

これらの設定値は大きく下記の3つにわけられるが、0x200 byte のデータではぐちゃぐちゃになっているので注意したい。

BPB_BytsPerSec → disk 個別

disk では sector size をデータの固まりの最小単位として管理する。これがフォーマットやメディアの種類によって可変なために設定したと思われる。

BPB_SecPerClus → FAT

filesystem では cluster をデータの管理単位と定義する。大きいファイルは複数の cluster を持ち、どのクラスタを使うかは FAT とよばれる chain table に記載される。

このパラメータは前述の sector を複数つなげて利用することが出来る数をここに記載する。

BPB_BytsPerSec, BPB_SecPerClus にシフト単位も持たせる

  • file の offset から disk の物理アドレスの算出 (fseek とか)
  • filesystem の user data 容量計算

こういった処理には対応する chain table を見つけ出す必要があり、sector size と cluster size の乗除算が発生する。チープな CPU を使用する場合、乗除算の使用は避けたい。要するに某CPU向けにコンパイルするときに umodsi3 とか mulsi3 が見つからないというリンクエラーは見たくないし、リンクしても時間がかかる気がする。

この2つのパラメータを定義を見てみると BPB_BytsPerSec は 0x200, 0x400, 0x800, 0x1000 の4通り、BPB_SecPerClue は 2 ** n (n は 1 から 8) となっていてそれ以外はエラーとして弾いてよいと解釈できる。よってシフト演算に置き換えやすい仕様となっている。

前置きが長くなったが datasize とは別にシフト回数も取り込むようにする。

static bool filesystem_init(struct fat_filesystem *const t, const uint8_t *const buf)
{
	int errorcount = 0;
	
	t->fattype = FATTYPE_ERROR;
	if(buf[0x1fe] != 0x55 || buf[0x1ff] != 0xaa){
		DEBUGPUTS("signature is not found");
		errorcount += 1;
	}

	t->disk.sector_size = unpack_le16(buf, 11); //BPB_BytesPerSec
	switch(t->disk.sector_size){
	case 0x200:
		t->disk.sector_sizebit = 9;
		break;
	case 0x400:
		t->disk.sector_sizebit = 10;
		break;
	case 0x800:
		t->disk.sector_sizebit = 11;
		break;
	case 0x1000:
		t->disk.sector_sizebit = 12;
		break;
	default:
		t->disk.sector_sizebit = -1;
		DEBUGPUTS("disk.sector_size is illigal");
		errorcount += 1;
		break;
	}
	
	t->fat.cluster_par_sector = unpack_byte(buf, 13); //BPB_SecPerClus

#define CPS(bit) case (1 << (bit)): t->fat.cluster_par_sectorbit = bit; break

	switch(t->fat.cluster_par_sector){
	CPS(0); CPS(1); CPS(2); CPS(3);
	CPS(4); CPS(5); CPS(6); CPS(7);
	default:
		t->fat.cluster_par_sectorbit = -1;
		DEBUGPUTS("fat.cluster_par_sector is illigal");
		errorcount += 1;
		break;
	}
#undef CPS
	---- 中略 ----
	return errorcount == 0 ? true : false;

disk.sector_size のほうは4度なのでコピペして switch を作ったが、 fat.cluster_par_sector のほうは8度もコピペするのは質が落ちるのでマクロを使った。こういうマクロの使い方は賛否両論あると思うので、マクロの有効範囲をできるだけ狭めることが大切だと思う。

FAT12 の chain を取得する関数

話がかなり飛ぶが、 FAT12 の cluster 番号の取得は 12bit 単位となり、 2.5 byte 単位で参照することとなる。ChanN 氏の説明では byte 単位のソースコードで説明しているが、変数と変数の除算が発生するし、わかりにくいと私は考えている。

ここで、bit 単位で考えることにする。

/*
bitcount is fixed 小数点
bit
31:19 must be 0
18:3 byteoffset
2:0  bitoffset (1:0 is 0)
*/
	assert(cluster_num_in != 0);
	bitcount = cluster_num_in * 12;
	
	sn = bitcount >> 3;
	sn >>= t->filesystem->disk.sector_sizebit;
	sn += t->filesystem->fat.startsector;
	
	so = bitcount >> 3;
	so &= t->filesystem->disk.sector_size - 1;
	
	if(buffer_sector_cacheup(t, sn, so) == false){
		return false;
	}

まずクラスタ番号から sector number (sn) と sector byte offset (so) を算出する。 bitcount の算出に乗算を使うが、変数*定数であればコンパイラがシフト演算と加算に置き換えてくれることが期待できる。12 の場合は (1 + 2) << 2 となり、シフト演算のほうが低コストであるはずだ。

bitcount を3回右シフトすれば byteoffset になる。 byteoffset から sectorsize を割れば、 sector number がでてくる。普通にやると byteoffset / sectorsize なので変数/変数の除算となるが、シフト回数にすれば大幅なコストダウンと思われる (ただし、CPU によってはシフト回数が変数だと効率が若干落ちる可能性がある, たしか ARM がそうだった)。

so のほうは sector_size が 2 の累乗値であることを利用して除算(剰余)を and 演算に置き換える。

	switch(bitcount & 0x7){
	case 0:
		ret = buf[0];
		ret |= (buf[1] & 0xf) << 8;
		break;
	case 4:
		ret = buf[0] >> 4;
		ret |= buf[1] << 4;
		break;
	default:
		DEBUGPUTS("fat12 cluster bit offset is illigal");
		return false;
	}

記載は中略したが、 buf には対応するセクタからセクタ境界の問題も処理した 2byte が入っている。これを当初の bitcount の byte 未満の bit 部分で分岐させれば 12bit の cluster が取得できる。

変な処理をするときはコメントをいれたり、わかりやすい変数名をつけて >> 3 や & 7 のようなマジックナンバーの補足を行うことが大切である。広範囲に影響する定数であればマクロなどで名前をつけることも大切と私は考えている。

ここまでの総括

CP-MMS-DOS の時代にはこれらのファイルシステム算出はアセンブラで書かれていたと思われるし、乗除算なんて最初から使っていなかったはずだ。だからこんなことを自慢げに書くのはなかなか不毛なことだと思っている。

補足をするなら12の乗算は常に bit1:0 が 2'b00 で固定されるので cluster number は 3 でかけ算して、1回右シフトで byte offset を取得する方が懸命かもしれない。