FATFS と SDHI 取り扱いマニュアル(初心者向け)
#EX-Word
DATAPLUS3
対象: SH-3 コア + SDHI ホストコントローラ(@ 0xA4550000) を内蔵した組込み機器(ビッグエンディアン)
0. このマニュアルのゴール
SD カードから ファイルを一覧・読み出しできるようにする。そのために2つの層を理解します。
code:layers
アプリ ls / cd / ファイル読み ← やりたいこと
│
FAT層 ディレクトリ/ファイルの論理構造を解釈 ← FAT16フォーマット
│ (セクタ番号 ↔ ファイル名/クラスタ)
SDHI層 SDカードに CMD を送り 512Bセクタを読む ← ハードを叩く
│
SDカード
下の層(SDHI)から順に説明します。
1. SDHI 層 — SD カードの叩き方
1.1 SD カードの通信モデル(超基礎)
SD カードは コマンド(CMD) を送ると レスポンス(R1/R2/R3/R7) を返す。
データ読み出しは「CMD17 を送る → カードが 512 バイトを DATラインで送ってくる」。
すべて クロック駆動。クロックを止めるとカードも止まる(省電力)。
SDHI はこの CMD/レスポンス/データ転送を半自動でやってくれるホスト側ハード。
1.2 SDHI レジスタ全マップ(ベース 0xA4550000, 全16bit)
code:c
SDHI_CMD @ 0x00 // コマンド発行(下位6bit=CMD番号, 上位=応答種別等)
SDHI_ARGL @ 0x04 // 引数 下位16bit ★ (32bit直書き不可。下位0x04/上位0x06に分ける)
SDHI_ARGH @ 0x06 // 引数 上位16bit ★
SDHI_STOP @ 0x08 // マルチブロック停止制御
SDHI_BCNT @ 0x0A // 転送ブロック数
SDHI_RSP0 @ 0x0C // 応答15:0 (48bit応答は RSP1:RSP0)
SDHI_RSP1 @ 0x0E // 応答31:16
SDHI_RSP2.. @ 0x10.. // R2(CID/CSD 136bit)用
SDHI_STS @ 0x1C // ステータス (bit0=CMDEND, bit2=DATAEND)
SDHI_STS2 @ 0x1E // ステータス2 (エラー/データレディ。下記§1.3)
SDHI_IMASK @ 0x20 // 割込みマスク
SDHI_CLKCTL @ 0x24 // クロック制御 (初期化0x0320 / 転送0x0301)
SDHI_XLEN @ 0x26 // 転送バイト長 = 512
SDHI_OPT @ 0x28 // バス幅/タイムアウト (0x80E0 = 1bit幅/最長)
SDHI_DATA @ 0x30 // データポート (16bit×256 = 512B)
SDHI_DMAEN @ 0xD8 // DMA有効 (0 = PIO)
SDHI_RST @ 0xE0 // ソフトリセット (0→1)
// CLKCTL(0x24) ビット
// bit9 BASE_EN=0x0200 / bit8 SCLKEN=0x0100 / 分周 DIV64=0x20(≈262kHz) DIV2=0x01(≈8MHz)
// 初期化時=0x0320(BASE|SCLK|DIV64) / 転送時=0x0301(BASE|SCLK|DIV2)
// SDHI_CMD エンコード(下位6bit=CMD番号 と OR する)
// SCMD_RESP_R1=0x0100 / R2=0x0200(+ソフトNOCRC) / R3=0x0100(R1同+ソフトNOCRC扱い)
// SCMD_DATA=0x0800 / SCMD_READ=0x1000
// 例) CMD17読み = 17 | 0x0100 | 0x0800 | 0x1000
1.3 STS2 のビット意味(実機で確定)
ここを取り違えると一生読めません。最重要。
code:c
// SDHI_STS2 (0x1E) ビット定義
bit1 0x0002 CRCERR // R2/R3では常時立つ → 無視
bit3 0x0008 DATATO // データタイムアウト
bit6 0x0040 CMDTO // ★コマンドタイムアウト = エラー判定用。データ待ちに使うな
bit7 0x0080 RXRDY // カード選択中ほぼ常時1
bit8 0x0100 BRE // ★読み出しバッファ準備完了 = データ読みはこれを待つ
// (read後 bit8 を 0 で書いてクリア=ack)
正常応答時 STS2=0x0080。コマンド失敗(timeout)時は bit6(0x40) が立つ。
512B ブロックがバッファに届くと bit8(0x0100) が立つ。読み終えたら bit8 を 0 で書いてクリア(ack)。
📌 本体ファームウェア(ROM) の読み出しルーチンと実機比較でこの表を確定。
ROM も tst #0x40,STS2 をエラー判定に使っており、bit6=CMDTO で間違いない。
1.4 カード初期化シーケンス(sd_init)
電源投入後、決まった順番でコマンドを送ってカードを使える状態にします。
code:flow
電源 power-cycle … カードを物理リセット(後述)
↓
SDHI_RST=0→1 … ホスト側リセット
CLKCTL=低速(≈262kHz) … 初期化は遅いクロックで
↓
CMD0 GO_IDLE_STATE … カードをアイドルへ(応答なし)
CMD8 SEND_IF_COND(0x1AA)… 電圧確認。SD v2なら0x1AAをエコー
(v1カードは無応答=CMDTO。それで正常)
ACMD41 (CMD55→CMD41ループ) … 電源投入完了待ち。OCRのbit31=1でready
│ bit30(CCS)=1ならSDHC, 0ならSDSC
CMD2 ALL_SEND_CID … カードID取得
CMD3 SEND_RELATIVE_ADDR … RCA(相対アドレス)取得
CMD7 SELECT_CARD(RCA) … 転送状態へ
↓
CLKCTL=高速(≈8MHz) … 以後の読み書きは速いクロック
CMD16 SET_BLOCKLEN(512) … SDSCのみ。ブロック長512確定
→ 初期化完了
1.5 512B ブロック読み出し(sdhi_read_block)
code:flow
SDHI_DMAEN=0; XLEN=512; BCNT=1; // PIO, 1ブロック
STS クリア
SDHI_ARG = セクタアドレス // ★16bit×2で書く(罠1)
SDHI_CMD = CMD17 | 応答R1 | DATA | READ
↓
STS2 bit8(BRE) が立つまで待つ // ★bit6ではない(罠2)
bit8 をクリア
SDHI_DATA を 256回読む = 512バイト // 16bit×256
⚠️ 罠1: ARG は 16bit×2 で書く(SH3 ビッグエンディアン)
code:c
// ✗ *(volatile uint32_t*)(BASE+0x04) = arg; // 32bit直書きは上下逆になる!
// ○ 下位を 0x04, 上位を 0x06 に分けて書く:
*(volatile uint16_t*)(BASE+0x04) = (uint16_t)(arg & 0xFFFF); // 下位
*(volatile uint16_t*)(BASE+0x06) = (uint16_t)(arg >> 16); // 上位
ハードは「0x04=下位 / 0x06=上位」を期待。SH3 の32bitストアはBEなので 0x04←上位 となり逆。
症状: arg=0 のコマンド(CMD0/2/3)だけ通り、非0 argのCMD8/ACMD41/CMD17が全部失敗。
ROM のコマンド発行(0x80009430)も16bit×2で書いている。これが正。
⚠️ 罠2: 読み出しは bit8(BRE) を待つ。bit6(CMDTO) ではない
512B ブロックが届くと STS2 bit8 が立つ。これを待ってから DATA を読む。
bit8 はブロックあたり1回だけ立つ → 256ワード一括で読み切る。
旧コードは bit6(=CMDTO) を待っていて永久にデータ0になっていた。
SDSC のアドレッシング(HC=0)
SDSC(標準容量, HC=0): CMD17 の引数はバイトアドレス = セクタ番号 << 9(×512)。
SDHC(HC=1): 引数はブロック(セクタ)番号そのまま。
コード: arg = g_sd_hc ? lba : (lba << 9);
1.6 SD カード電源 GPIO(本機独自・power-cycle)★
⚠️ 本機は標準ポート外の カスタムレジスタでSDカード電源を制御。公式データシートに無い。
本体ファームウェア(ROM)解析が唯一の根拠(電源ON/OFF/ポート設定の各ルーチン)。
code:c
// SDカード電源/クロック制御レジスタ
0xA40001AC bit7 // SDPWR : カード電源enable(負論理) ON=0 / OFF=1
0xA4000184 (8bit)// SDAUX : 補助制御 ON=0x00 / OFF=0x7F
0xA40A0004 bit1 // SDCLK : SDHIモジュールクロック ON=1 / OFF=0 (CPG拡張0xA40A0000)
// 電源ON時のポート設定(出力Lowへ):
PEDR 0xA4000128 bit4,bit5 = 0
SCPDR 0xA4000136 bit1 = 0
本体OSは電源を常時ON維持し、決して落とさない。
メニュー操作→通信モード遷移で 未終端のCMD18が残りカードが固まることがある(§4 歴史参照)。
本実装では初期化の冒頭で 電源OFF→放電待ち→ON の power-cycle を行いカードを物理リセットしている。
power-cycle 完全シーケンス(sd_init 冒頭, ROM準拠)
code:c
// --- OFF (ROM 0x80009782 準拠順) ---
*(volatile uint8_t*)0xA4000184UL = 0x7Fu; // SDAUX OFF
*(volatile uint8_t*)0xA40A0004UL &= (uint8_t)~0x02u;// SDHIクロックOFF
*(volatile uint8_t*)0xA40001ACUL |= 0x80u; // SDPWR bit7=1 = 電源OFF
delay_loops(3000000u); // ~180ms 放電待ち(VDD完全放電)
// --- ポート設定 (ROM 0x8000924E相当): SD制御ピンをLowへ ---
*(volatile uint8_t*)0xA4000128UL &= (uint8_t)~0x30u;// PEDR bit5,bit4=0
*(volatile uint8_t*)0xA4000136UL &= (uint8_t)~0x02u;// SCPDR bit1=0
// --- ON (ROM 0x80009706 準拠順) ---
*(volatile uint8_t*)0xA40001ACUL &= (uint8_t)~0x80u;// SDPWR bit7=0 = 電源ON
delay_loops(1000000u); // ~60ms 電源安定
*(volatile uint8_t*)0xA40A0004UL |= 0x02u; // SDHIクロックON
*(volatile uint8_t*)0xA4000184UL = 0x00u; // SDAUX ON
delay_loops(500000u); // ~30ms 追加安定
// → この後 SDHI_RST → CMD0〜 の初期化(§1.4)へ
2. FAT 層 — ファイル構造の解釈
SDHI で生セクタが読めるようになったら、その中身を FAT16 として解釈します。
2.1 FAT16 ディスク全体像(標準フォーマット)
code:fat
MBR セクタ0: パーティションテーブル(末尾0x55AA)
予約領域 ブートセクタ(BPB=BIOS Parameter Block)
FAT1 ファイル割り当て表(クラスタの連結リスト)
FAT2 FAT1のコピー(冗長)
ルートディレクトリ 固定数のエントリ
データ領域 クラスタ単位でファイル本体
2.2 ⚠️ 独自フォーマット(MBR/BPB 無し)
本機のカードは MBR も BPB も無く、セクタ0からいきなり FAT が始まる特殊形式。
fat_init の判定:
1. セクタ0 を読む。末尾が 0x55AA でなく、先頭の bytes2-3==0xFFFF(FAT16の予約エントリ)なら独自形式。
2. セクタ1 も FAT なら FAT が2コピーと判断。
3. 指数探索(セクタ 1,2,4,8,…1024 を読む)でルートディレクトリの先頭を探す:
先頭バイトが 0x00/0xFF/0xF8 → まだ FAT 領域(継続)
先頭バイトが有効ファイル名文字(0x21-0x7E) or 0xE5 → ここがルート
2.3 ディレクトリエントリ(32バイト固定)
code:c
// オフセット サイズ 内容
// 0 11 8.3形式ファイル名(8+3, スペース埋め)
// 11 1 属性 (0x10=DIR, 0x0F=長名LFN, 0x08=ボリューム)
// 20 2 クラスタ番号 上位16bit (FAT32用, FAT16は0)
// 26 2 クラスタ番号 下位16bit
// 28 4 ファイルサイズ(バイト) ← 全てリトルエンディアン
8.3名: "NAME EXT"(基本名8+拡張子3)のように11バイト固定でスペース埋め。入力↔8.3を相互変換する。
先頭バイト 0x00=以降空き / 0xE5=削除済み。
属性 0x0F(LFN)は長いファイル名の断片 → 一覧ではスキップ。
2.4 クラスタとFATチェーン
ファイル本体はクラスタ(連続セクタの塊, g_fat_spcセクタ)単位で配置。
クラスタ番号 → 先頭セクタ:
code:c
lba = g_fat_lba_data + (clust - 2) * g_fat_spc; // クラスタ2が先頭
ファイルが複数クラスタに渡る場合、FAT が「次のクラスタ番号」を示す連結リスト:
code:c
next = FATclust; // 0xFFF8以上なら終端(EOC)
fat16_next_clust() が FAT セクタを読んで次クラスタを返す。
2.5 リトルエンディアン注意
FAT のディスク上の数値は リトルエンディアン。SH3 はビッグエンディアンなので必ず変換ヘルパで読む:
code:c
le16(p) = p0 | (p1<<8);
le32(p) = p0 | (p1<<8) | (p2<<16) | (p3<<24);
SDHI(ARG)のエンディアン罠とは別物。「ディスク上のFAT数値=リトル」「CPU=ビッグ」を常に意識。
3. 動作例: ls は内部で何をしているか
code:ls
ls
└ fat_ensure() // 初回のみ sd_init(power-cycle) + fat_init
└ dir_scan(現在のクラスタ) // g_cur_dir_clust のディレクトリを走査
└ クラスタ→LBA変換
└ sdhi_read_block() で各セクタ読み(512B=16エントリ)
└ 各32Bエントリを解釈 → ファイル名とサイズを表示
└ クラスタが続くなら fat16_next_clust() で次へ
cd NAME は同じ走査で NAME を探し、見つかったディレクトリのクラスタを g_cur_dir_clust に保存。
cd .. は ".." エントリのクラスタへ。状態は保持されるので次の ls はそのディレクトリを見る。
4. ⚠️ 歴史的教訓: 「カードが読めない」問題の顛末
長期間この問題に悩まされたが、真因は別の所にあった。記録として残す。
code:root_cause
当初の誤った仮説 実際
・マルチブロック読み(CMD18)残留 → 一部事実だが本質ではない
・電源 power-cycle が必要 → ロック解除には効くが、それでも読めなかった
★真因①: ARG 32bit直書きで上下逆 → 非0argのCMD8/ACMD41/CMD17が全滅していた
★真因②: 読み出しがbit6(CMDTO)待ち → 正しくはbit8(BRE)
開発初期は、カードがロック中の stale(古い)応答 を返していたため初期化が「偽成功」し、
本当は一度も実カードと通信できていなかった。それが真因を覆い隠していた。
教訓:
1. 「成功しているように見える」値が stale でないかを疑う(RCA=B368 が毎回同じ=怪しい)。
2. ステータスビットの意味は思い込まず ROM/データシートで確定する(bit6 vs bit8)。
3. エンディアンは レジスタ(ARG)とディスク(FAT)で別々に注意する。
4. 動く実装(本体ファームウェア)があるなら逆アセンブルして正解手順を写経するのが最速。
5. まとめ・チェックリスト
新しく SD/FAT を立ち上げるとき:
SDHI: ARG は 16bit×2(下位0x04/上位0x06) で書いたか
SDHI: 読み出しは STS2 bit8(BRE) を待ち、256ワード一括で読んだか
SDHI: STS2 bit6=CMDTO を「エラー」として正しく扱っているか
SDSC は CMD17 引数を ×512(バイトアドレス) にしたか
必要なら電源 power-cycle(0xA40001AC bit7 / 0xA4000184)でカードを物理リセットしたか
FAT: ディスク数値は le16/le32 で読んだか(リトルエンディアン)
FAT: このカードは MBR/BPB 無しの独自形式(セクタ0からFAT)と理解したか
FAT: 8.3名・32Bエントリ・クラスタチェーンを正しく解釈したか
困ったら: SD 仕様(SD Physical Layer Simplified Specification, 公式公開)、
そして 本体ファームウェア(ROM)の逆アセンブルを参照。
本マニュアルは実機検証と本体ファームウェアの解析に基づく。