こんにちは!Yukiです。
今回はCH32V203を使って、SD+FAT32のファイルの読み書きをしてみたいと思います。
使ったもの
今回は信頼と安心のKIOXIAのMicroSDカードを使いました。(株価も上がってr(ry)
今回私は秋月のものを買いましたが、これであれば抵抗なども実装されているので便利かもしれません。(保証はしません)
FATとは
File Allocation Tableの略で、ディスクのファイルの位置情報や日付などに関する情報を保存しておくための領域です。
これらを使ったファイルシステムを FATファイルシステムと呼びます。
しかし、ほとんどの人はFAT = FATファイルシステムと呼びます。
FATには様々ながあり、
に分類されます。
FAT12は最大ファイルサイズ32MiB 最大ボリュームサイズ32MiBという今では小さすぎてなにもできない子です。フロッピーディスク(1.44MiB)に多く使われていました。
次にFAT16です。こちらは最大ファイルサイズ 2GiB 最大ボリュームサイズ 2GiBとなっておいて、こちらもまた今ではあまり見かけられないものになりました。
ちなみにWindows11とかのフォーマットで出てくるFATはこいつらしいです。
次にFAT32です。最大ファイルサイズ 4GiB 最大ボリュームサイズ2TiBとなっています。いきなりボリュームサイズが大きくなりました。ちなみにWindows標準のフォーマットツールだと32GiBまでしかフォーマットできません。
最後にexFATです。こちらはFAT32以前のシステムと大きくかわっており、exFATは少し異種です。最大ファイルサイズは16EiBで、最大ボリュームサイズは512TiBとなっています。ちなみに、ボリュームサイズはあくまでも推奨で、理論上は64ZiBまで扱えます。(まさかのPとYぬかしてのZですね…)
次に、ファイル名についてです。exFATには関係ない話です。
FATにはファイル名に8.3形式を採用しています。(ファイル名8文字以下 拡張子3文字以下) なおファイル名はすべて大文字という規則になっています。
HELLO.TXT とか NOTEPAD.EXE みたいな感じですね。
この形式をFATではSFN (Short File Name)といいます。
しかしこれだと不便だと思ったのか、MAX255文字までファイル名を拡張できるように仕様変更されました。これがLFN (Long File Name)です。
なのでFAT32でフォーマットされたUSBメモリなどでも長い名前で使えます。
ちなみにLFNを使わないと日本語も使えないので注意です。
FatFsについて
FATは規格が複雑で、自分で実装するにはハードルが高いです。
そこでFatFsというChaN様が製作されたライブラリを組み込んで使うことが多いです。
簡単に言うと、SDにアクセスする方法さえ自力で書ければ、あとはFatFsがファイル書き込みや読み込みなどを行ってくれるようになります。
もちろんSDを取り外しPCにさせば、マイコン側で書き込んだファイルを見たり編集することだってできます。
このシステムを作っていただいたChaN様にお礼申し上げます。本当に素晴らしいシステムです。
早速実装
前回の記事でSDの書き込み・読み込みは実装できているので、これを使います。
FatFsは本当は複数ブロックまとめて書き込み・読み込みをするマルチブロック書き込み・読み込みに対応しているので、非効率ではあるのですが、今回はシングルブロック書き込み・読み込みだけで実装していきます。
(また個人的に困ったことがあれば実装したいと思います)
実装するといっても、下位レイヤーを軽く書いてあげるだけです。
diskio.cを新規作成し、そこに書いていきます。
必要な関数は
- disk_initalize
- disk_status
- disk_read
- disk_write
- disk_ioctl
- disk_fattime
です。
まずインクルード・宣言・グローバル変数等です。
#include "diskio.h"
#include "../CH32V203_Mylib/SPI_Mylib.h"
#define DEV_SD_DEBUG 0
#define DEV_SD 1
static DSTATUS Stat_DEV[5] = {STA_NOINIT};
DEV_SD_DEBUGやDEV_SDはファイルアクセス時に使うことになるドライブ番号のようなものです。(WindowsでいうC:\みたいなもの)
DEBUGはUARTにログが出てきます。それだけの違いです。
Stat_DEV[5] はそのデバイスをマウントしたかどうか記録しておくための変数です。
次に、disk_initialize関数です。
DSTATUS disk_initialize (BYTE pdrv){
DSTATUS stat;
int result;
if(pdrv == DEV_SD_DEBUG){
result = SD_init_debug();
if(result == 0){
Stat_DEV[DEV_SD_DEBUG] = RES_OK;
stat = RES_OK;
}else{
stat = RES_ERROR;
}
return stat;
}else if(pdrv == DEV_SD){
result = SD_init();
if(result == 0){
Stat_DEV[DEV_SD] = RES_OK;
stat = RES_OK;
}else{
stat = RES_ERROR;
}
return stat;
}
return STA_NOINIT;
}
ここはSDの初期化について書くところです。
(なんかinitializeみてたら、急にマテリアライズを思い出しましたw)
次に、disk_statusです。
DSTATUS disk_status (BYTE pdrv){
if(pdrv == DEV_SD){
if(Stat_DEV[DEV_SD] == STA_NOINIT){
return STA_NOINIT;
}
return RES_OK;
}else if(pdrv == DEV_SD_DEBUG){
if(Stat_DEV[DEV_SD_DEBUG] == STA_NOINIT){
return STA_NOINIT;
}
return RES_OK;
}
return RES_ERROR;
}
ここはドライブが初期化されているかどうか・メディアがセットされているか・メディアにライトプロテクト(書き込み禁止)がされていないかを返すところです。
今回はメディアセットとライトプロテクトに関しては無視しています。
ドライブ初期化はdisk_initializeが正常に動作した際に代入される値で見ています。
(本当は適当なCMDから帰ってくるR1レスポンスを見るべきなのかもしれません)
次に、disk_read関数です。
DRESULT disk_read (BYTE pdrv, BYTE* buff, LBA_t sector, UINT count){
if(pdrv == DEV_SD){
int res = RES_OK;
for(int i = 0; i < count; i++){
res = SD_Read(sector + i, buff + (i * 512));
}
if(res != 0){
return RES_ERROR;
}
return res;
}else if(pdrv == DEV_SD_DEBUG){
int res = RES_OK;
for(int i = 0; i < count; i++){
res = SD_Read_debug(sector + i, buff + (i * 512));
}
if(res != 0){
return RES_ERROR;
}
return res;
}
return RES_ERROR;
}
ややこしいのですが、sectorは512byteずつで進んでいき、buffは1byteずつですすんでいくのでこのような記述になります。後はcount関数繰り返すだけです。
次に、disk_write関数です。
DRESULT disk_write (BYTE pdrv, const BYTE* buff, LBA_t sector, UINT count){
if(pdrv == DEV_SD){
int res = RES_OK;
for(int i = 0; i < count; i++){
res = SD_Write(sector + i, buff + (i * 512));
}
if(res != 0){
return RES_ERROR;
}
return RES_OK;
} else if (pdrv == DEV_SD_DEBUG){
int res = RES_OK;
for(int i = 0; i < count; i++){
res = SD_Write_debug(sector + i, buff + (i * 512));
}
if(res != 0){
return RES_ERROR;
}
return RES_OK;
}
return RES_ERROR;
}
こちらもRead関数と同様の書き方です。
次に、disk_ioctl関数です。
DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff){
switch(pdrv)
{
case DEV_SD:
if(cmd == CTRL_SYNC){
return RES_OK;
}else if(cmd == GET_SECTOR_SIZE){
*(WORD*)buff = 512;
return RES_OK;
}else if(cmd == GET_SECTOR_COUNT){
*(WORD*)buff = SD_AllBlockCnt_debug();
return RES_OK;
}else if(cmd == GET_BLOCK_SIZE){
*(WORD*)buff = 1;
return RES_OK;
}else{
return RES_ERROR;
}
break;
case DEV_SD_DEBUG:
if(cmd == CTRL_SYNC){
return RES_OK;
}else if(cmd == GET_SECTOR_SIZE){
*(WORD*)buff = 512;
return RES_OK;
}else if(cmd == GET_SECTOR_COUNT){
*(WORD*)buff = SD_AllBlockCnt();
return RES_OK;
}else if(cmd == GET_BLOCK_SIZE){
*(WORD*)buff = 1;
return RES_OK;
}else{
return RES_ERROR;
}
break;
default:
return RES_ERROR;
break;
}
return RES_ERROR;
}
こちらが少し面倒です。
cmd引数で来たコマンドに応じて返却しなくてはいけません。
まず、CTRL_SYNCです。
これはドライブの書き込み処理が終わっているか確かめるコマンドです。
今回実装しているSD_write関数は、書き込みが終わらないと終わらない仕様になっているので、RES_OKを返しています。
次に、GET_SECTOR_SIZEです。
これはドライブのセクタサイズを返すコマンドです。
SDカードはブロック単位が512byteですから、512を返しています。
次に、GET_SECTOR_COUNTです。
これはドライブ上の総セクタを返す必要があります。ということでドライブの総セクタ数を返す関数を作りました。ということで、*(WORD*)buff = SD_AllBlockCnt();だけです。
次に、GET_BLOCK_SIZEです。
これは削除ブロックのサイズを返します。不明であれば1でよいとリファレンスマニュアルにありましたので、1を返しています。
最後にCTRL_ERASE_SECTORです。
これはフラッシュの一部領域を削除する際に使うようです。
これはよくわからなかったので実装していません。(正直マイコン内で削除処理を行う行為はやめた方がいいと思います。別のデータも飛びそうです)
最後に get_fattime関数です。
// とりあえず 2026年1月1日 00:00:00 を返す
DWORD get_fattime (void) {
return ((DWORD)(2026 - 1980) << 25) /* Year 2026 */
| ((DWORD)1 << 21) /* Month 1 */
| ((DWORD)1 << 16) /* Mday 1 */
| ((DWORD)0 << 11) /* Hour 0 */
| ((DWORD)0 << 5) /* Min 0 */
| ((DWORD)0 >> 1); /* Sec 0 */
}
CH32V203マイコンにはRTCモジュールがついていますが、特に書き込み日時・読み込み日時についてどうでもいいということであれば、これでOKです。
ちゃんとやりたい場合は、RTCモジュールを使用し、UNIXタイムを返してあげましょう。
CH32V203での制限
ROMの容量が足りなかった関係で、LFN(Long File Name)に関しては使えません。(対応表がとんでもなく重いみたいです)
ということで、このマイコンでは8.3表記しか扱えないことに注意が必要です。
(HELLO.TXTとかMINE.EXEとか)
GitHub
今回のプログラムは長くなったので、GitHubに上げました。
少しですがサンプルのコードもあります。ぜひご活用ください。(つかう人がいればですが)
何かあったらここにコメントするか、issueに登録してください。(使う人いない気もしますが)
次回までにやってみたいこと
FAT32ができたので、MP3プレイヤーもどきとか作ってみたいですね。
(ただ、ID3対応前提になると思うので、そこだけ面倒ですが)
どうせならアルバムアートなんかも表示して…
とか思ってると結構難しいかも…
チャレンジしたいですね。