2014年6月18日水曜日

USBメモリーをFatFsで操作する

さて、前回の時点でUSBメモリーをPIC32MX250F128Bからアクセスすることに成功しました。
FATファイルシステムもMicrochipのライブラリで制御しておりますが、やはりFATファイルシステムのライブラリと言ったらFatFsですね。(少なくとも私の中では)

FatFs 汎用FATファイルシステム・モジュール

もうね、このライブラリには何も不満は抱けないです。それくらい完成度の高いライブラリです。
ディスクの初期化や指定セクタの読み書きなどのほんのいくつかのデバイスに依存した関数をユーザーが用意するだけで、FAT16/32のアクセスができるようになります。
そのほか、非常に細かくオプションを指定できたりし(メモリが足りればLFNやUnicodeファイル名も使える)、その上、パフォーマンスもかなり良いです。言うこと無しです。

それでは導入していきましょう。

まずは上記のサイトよりFatFsのソースコードを入手し、ヘッダファイル、Cファイルをともにプロジェクトに登録します。ついでに、MicrochipのライブラリのほうのファイルFSIO.cを無効化しておきましょう。


次に、整数型の定義をします。integer.h内で、各ビット数に対応した型を定義してやるのですが、すでにPICのプロジェクトはGenericTypeDefs.hで同名の型が定義されており、二重定義エラーが起きてしまいます。しかし、BYTEだのWORDだの、この辺の定義はもはやビット数はだいたい決まっていて、FatFs内の定義とGenericTypeDefs.h内の定義が同じなので、integer.hでGenericTypeDefs.hをIncludeしてあげるだけにしました。WCHARの定義を残して、それ以外はコメントアウトします。

そしたら、一番重要なdiskio.cの定義をしてあげます。こんなかんじになります。

#include "diskio.h"
#include "ffconf.h"        /* FatFs lower layer API */

/* Definitions of physical drive number for each media */
#define USB        0


typedef struct
{
    BYTE    errorCode;
    union
    {
        BYTE    value;
        struct
        {
            BYTE    sectorSize  : 1;
            BYTE    maxLUN      : 1;
        }   bits;
    } validityFlags;

    WORD    sectorSize;
    BYTE    maxLUN;
} MEDIA_INFORMATION;

typedef enum
{
    MEDIA_NO_ERROR,                     // No errors
    MEDIA_DEVICE_NOT_PRESENT,           // The requested device is not present
    MEDIA_CANNOT_INITIALIZE             // Cannot initialize media
} MEDIA_ERRORS;

MEDIA_INFORMATION * USBHostMSDSCSIMediaInitialize( void );
BYTE USBHostMSDSCSIMediaDetect( void );
BYTE USBHostMSDSCSIWriteProtectState( void );
BYTE USBHostMSDSCSISectorRead( DWORD sectorAddress, BYTE *dataBuffer );
BYTE USBHostMSDSCSISectorWrite( DWORD sectorAddress, BYTE *dataBuffer, BYTE allowWriteToZero );


static BOOL isInitialized = FALSE;

/*-----------------------------------------------------------------------*/
/* Inidialize a Drive
*/
/*-----------------------------------------------------------------------*/

DSTATUS disk_initialize (
    BYTE pdrv                /* Physical drive nmuber (0..) */
)
{
    DSTATUS stat;

    switch (pdrv) {
    case USB:
        if(USBHostMSDSCSIMediaDetect()) {    //メディアがつながっているか?
            if(!isInitialized) {
                MEDIA_INFORMATION *mediaInformation;
                
                mediaInformation = USBHostMSDSCSIMediaInitialize();

                if(mediaInformation->errorCode != MEDIA_NO_ERROR)
                    stat = STA_NOINIT;
                else {
                    stat = 0;
                    isInitialized = TRUE;
                }
            } else
                stat = 0;

            if(USBHostMSDSCSIWriteProtectState())
                stat |= STA_PROTECT;
        } else
            stat = STA_NODISK | STA_NOINIT;

        return stat;
    }
    return STA_NOINIT;
}


/*-----------------------------------------------------------------------*/
/* Get Disk Status
*/
/*-----------------------------------------------------------------------*/

DSTATUS disk_status (
    BYTE pdrv        /* Physical drive nmuber (0..) */
)
{
    DSTATUS stat;

    switch (pdrv) {
    case USB :
        if(USBHostMSDSCSIMediaDetect()) {
            if(isInitialized)
                return 0;
            else
                return STA_NOINIT;
        } else
            return STA_NODISK | STA_NOINIT;

        return stat;
    }
    return STA_NOINIT;
}


/*-----------------------------------------------------------------------*/
/* Read Sector(s)
*/
/*-----------------------------------------------------------------------*/

DRESULT disk_read (
    BYTE pdrv,        /* Physical drive nmuber (0..) */
    BYTE *buff,        /* Data buffer to store read data */
    DWORD sector,    /* Sector address (LBA) */
    UINT count        /* Number of sectors to read (1..128) */
)
{
    UINT i;
    WORD SectorSize;

    if(disk_ioctl(pdrv, GET_SECTOR_SIZE, SectorSize) != RES_OK)
        return RES_ERROR;

    switch (pdrv) {
    case USB:
        for(i = 0; i < count; i++) {
            if(USBHostMSDSCSISectorRead(sector + i, buff + i * SectorSize) == FALSE)
                return RES_ERROR;
        }

        return RES_OK;
    }
    return RES_PARERR;
}



/*-----------------------------------------------------------------------*/
/* Write Sector(s)
*/
/*-----------------------------------------------------------------------*/

#if _USE_WRITE
DRESULT disk_write (
    BYTE pdrv,            /* Physical drive nmuber (0..) */
    const BYTE *buff,    /* Data to be written */
    DWORD sector,        /* Sector address (LBA) */
    UINT count            /* Number of sectors to write (1..128) */
)
{
    UINT i;
    WORD SectorSize;

    if(disk_ioctl(pdrv, GET_SECTOR_SIZE, SectorSize) != RES_OK)
        return RES_ERROR;

    switch (pdrv) {
    case USB :
        for(i = 0; i < count; i++) {
            if(USBHostMSDSCSISectorWrite(sector, (BYTE *)buff + i * SectorSize, TRUE) == FALSE)
                return RES_ERROR;
        }

        return RES_OK;
    }
    return RES_PARERR;
}
#endif


/*-----------------------------------------------------------------------*/
/* Miscellaneous Functions
*/
/*-----------------------------------------------------------------------*/

#if _USE_IOCTL
DRESULT disk_ioctl (
    BYTE pdrv,        /* Physical drive nmuber (0..) */
    BYTE cmd,        /* Control code */
    void *buff        /* Buffer to send/receive control data */
)
{
    DRESULT res;
    int result;

    switch (pdrv) {
    case USB :
        switch(cmd) {
            case CTRL_SYNC:
                res = RES_OK;
                break;
            case GET_SECTOR_COUNT:    //フォーマットするときにしか使われない
                res = RES_ERROR;
                break;
            case GET_SECTOR_SIZE:
#if(_MAX_SS == _MIN_SS)
                *((WORD *)buff) = _MAX_SS;
                res = RES_OK;
#else
                res = RES_ERROR;
#endif
                break;
            case GET_BLOCK_SIZE:
                *((DWORD *)buff) = 1;
                res = RES_OK;
                break;
            case CTRL_ERASE_SECTOR:
                res = RES_OK;
                break;
            default:
                res = RES_PARERR;
                break;
        }
        return res;
    }
    return RES_PARERR;
}
#endif

typedef union _tagFATTIME {
    DWORD value;
    struct {
        unsigned SecDiv2 : 5;
        unsigned Min : 6;
        unsigned Hour : 5;
        unsigned Date : 5;
        unsigned Month : 4;
        unsigned YearFrom1980 : 7;
    };
} FATTIME;

DWORD get_fattime (void)
{
    FATTIME time;

    time.YearFrom1980 = 34;
    time.Month = 6;
    time.Date = 17;
    time.Hour = 23;
    time.Min = 16;
    time.SecDiv2 = 0;

    return time.value;
}

void disk_detatched(void)
{
    isInitialized = FALSE;
}

ディスクが初期化されたかどうかは結構厳密に聞いてくるようなので、isInitializedという変数を用意し初期化されたかを記憶しておきます。そして、disk_detached関数を作り、main.c側でUSBメモリーが取り外されたことがわかったらこの関数を呼び出し初期化されていないことを伝えます。

USBメモリーのアクセス関係の関数はusb_host_msd_scsi.cに入っているようです。
disk_initialize関数とdisk_status関数はUSBHostMSDSCSIMediaInitialize関数とUSBHostMSDSCSIMediaDetect関数をうまく使って処理しています。また、disk_writeとdisk_readはマルチセクタリード/ライトにも対応できるようなインターフェースをしていますが、USBHostMSDSCSISectorWrite関数とUSBHostMSDSCSISectorRead関数はマルチセクタリードに対応していないようなので、とりあえずfor文でそれをエミュレートしてあげています。デバイスドライバレベルでマルチセクタアクセスできたら相当速くなるんでしょうがね…。(少なくとも昔、SDカードのアクセスをやった時はそうでした。)

disk_ioctrl関数は一番めんどくさかったですね。
CTRL_SYNCはデバイスドライバレベルでキャッシュしてる場合にflushするための関数なようで、おそらくそういうことはしていないっぽいのでとりあえず何も処理をせずに成功を返すようにしました。CTRL_ERASE_SECTORは実装しなくても大丈夫なようなのでこちらも同様にそうしました。
GET_SECTOR_COUNTはセクタの個数だそうです。えっ、それってFATファイルシステム内読めばわかるでしょ?って思ったらどうもこれ、フォーマットするときに使うようですね。そりゃそうだ、フォーマットされていないディスクだったらディスクの容量はファイルシステムからじゃ読み出せませんからね。
…どうしろって言うんだ…。 usb_host_msd_scsi.cにはそれっぽいAPI無いですしね…。というわけで、フォーマットしなけりゃ問題ないっしょということで実装するのをやめました。はい。
GET_SECTOR_SIZEですが、普通は512バイトみたいですね。 最大セクタサイズと最小セクタサイズが同じだったらそれでいいっしょということでそれを返すようにしました。違ったらサポートしないってことで。結構適当な実装です。はい。ごめんなさい。

あとはget_fattime関数を定義してやる必要があります。これはファイルのタイムスタンプを書き込むための関数で、要するにRTCなどから読みだした現在時刻をここで返すようにしてやればいいわけです。
が、NTPだのGPSだの何だので時計合わせするようなハードはあいにく持ち合わせていないので、適当にコーディングしていた時の時刻を返すようにしました。FATファイルシステムはタイムスタンプを32bitに収めるために、 秒なんか2秒毎にしか記録できないんですね。シフト演算子を組合せて時刻を定義しても良いでしょうが、ここはFATTIMEという共用体を作ってあげることにしました。こうやって各時刻要素の構造体とDWORDを共用体にすることで、構造体に入れた値をそのままDWORDとして読みだすことができます。すごく素朴でコストの小さい書き方でとても良いと思います。

これで最低限は動きそうな感じになったと思うんで、最後にmain関数側からこれを呼び出します。

    while(1)
    {
        LATAbits.LATA0 = 0;
        //USB stack process function
        USBTasks();

        //if thumbdrive is plugged in
        if(USBHostMSDSCSIMediaDetect())
        {
            FRESULT result;
            FATFS fatfs;
            FIL file;

            deviceAttached = TRUE;
            LATAbits.LATA0 = 1;

            f_mount(&fatfs, "0:", 0);
            result = f_mkdir("0:testdir");
            if((result == FR_OK) || (result == FR_EXIST)) {
                result = f_open(&file, "0:testdir/file.txt", FA_CREATE_ALWAYS | FA_WRITE);
                if(result == FR_OK) {
                    char szText[32] = "Hello, FatFs!";
                    UINT bw;

                    f_write(&file, szText, strlen(szText) * sizeof(char), &bw);

                    f_close(&file);
                    f_mount(NULL, "0:", 0);

                    while(deviceAttached == TRUE)
                        USBTasks();
                }
            }
        }
    }

ディレクトリの生成の試験も兼ねて、ドライブの中にtestdirというディレクトリを生成し、その中にファイルを生成しています。


そして無事、ファイルを書き込むことができました。
Microchipのライブラリと違い、タイムスタンプもちゃんと入っています。

さらに、USBドライバとFatFsを入れた時点でのプログラムメモリの使用率はなんと54%なんですね。
プログラムメモリを78%も占有することになったMicrochip製のライブラリよりも相当コンパクトだと言えるでしょう。さっすがFatFsです。

12 件のコメント:

  1. すいません、分からないのでお教え頂けると有り難いです。
    ブログ一番上のディレクトリ構造ではヘッダーファイルが白抜けになっていますが、diskio.h, ff.h, usb_config.hは外してあってちゃんと動くんでしょうか?

    返信削除
    返信
    1. C言語では、CファイルをコンパイルするときにそのCファイル内の#includeの行がヘッダーファイルの中身に置き換えられて一緒にコンパイルされます。ですので、ヘッダーファイルを単体でコンパイルすることはありませんので、コンパイル対象から外しています。

      削除
  2. 言われてみればすべてのヘッダーファイルが外して有りますね。
    ご説明有難うございます。

    返信削除
  3. こんにちは。
    チップ PIC32MX250F128B とXC32コンパイラでやってみました。
    その結果、usb_config.c、usb_host_msd.c、usb_host.cでusb.hがありません、またusb_host_msd_scsi.cで fileio.hがありません、main.cでsystem.hがありませんと言われてしまいました。アドバイスいただけるとありがたいです。ちなみに、usb.hをくっつけると、それが呼び水になってCompiler.hが無いと言われてしまいます。

    返信削除
    返信
    1. 当時の環境は今やもう私の手元には無いので、具体的なアドバイスはできません。

      microchip_solutions_v2013-06-15\USB\Host - Mass Storage - Simple Demo
      のプロジェクトを起動し、コンパイルしてみてはどうでしょうか。デモプロジェクトならば、開発環境がしっかり整ってさえいればコンパイルは通るはずです。
      そしたら、プロジェクトの設定等からヘッダーファイルのディレクトリを辿り、ファイルの場所を当たってみてください。プロジェクト本体のディレクトリ以外にも、共通のヘッダーファイル用のディレクトリがあったと記憶しています。

      この記事はこの記事を書いた当時のMPLAB、XC32、MLAを使ってのプロジェクト構築のお話です。
      MLA自体はもうすでにObsoleteですので、もしかしたら、最新の環境では正常にコンパイルできないかもしれません。
      最新のMPLAB Harmonyを用いた環境でのビルドをお勧めします。Harmonyは内部的にFatFsを使っているので、あえて自分でFatFsを組み込む必要もありません。こちらの記事も参考にしてみてください。
      https://days-of-programming.blogspot.jp/2017/08/pic32mx250f128bharmonyusb.html

      削除
  4. お返事頂き有難う御座います。
    Host - Mass Storage - Simple Demoはprogra configの部分を直せば、PIC24F、XC16でもコンパイルは通りました。USBの読み書きもOKでした。上の記事ではusb.hが必要ファイルなのか不明なままです。

    PIC32MX250F128B でHarmonyの記事を実行してみました。Harmonyのインストールフォルダの中にあるapp.cとapp.hを今作ったプログラムに貼り付けして、さらに、app.cについてブログに記載のあったプログラムに修正したところ、system_init.cの132, 133行目で'BSP_USBVBUSSwitchOverCurrentDetect'と'BSP_USBVBUSPowerEnable'は宣言されていないよとエラーになりました。対処方法についてご存知でしたらアドバイス願います。
    main.cは何も修正しなくて良かったのでしょうか?

    返信削除
    返信
    1. コンフィギュレーションビットの設定ミスのように思います。たぶんですが。

      削除
  5. 追伸
    Harmonyのインストールフォルダの中にあるapp.cとapp.hを今作ったプログラムに貼り付けして
    ...これってファイルの中身の100%置き換えをしましたがあっておりましたか?追記とかでないですよね?

    返信削除
    返信
    1. 記事中にも書いてありますが、Harmonyに付属しているサンプルプログラムはおそらくPICのほかに様々な周辺機器も搭載された専用のマイコンボード用で、アクセスランプの点滅の機能も付いています。そのような環境でなければ、そのままではコンパイルが通りません。当該LEDの点滅に関するコードを削除する必要があります。

      あと、ここ何回かコメントのやり取りをしていて思っているのですが、あなたは少し私を頼りすぎです。残念ながら私はあなたのためにブログを書いてるわけでもなければ、あなたの先生でもありません。
      自分でいろいろと調べて、試行錯誤して、それでやっとうまく動いたときの感動は、私に頼りまくってコードを動かすよりも何十倍も楽しいですし、何百倍もあなたのためになりますよ。
      健闘を祈っております。

      削除
  6. Hello Kintarou, Great article of yours.

    I had already a working USB host with the same pic and MDD file system and MLA.
    Once I read your article I wanted to try out FatFs in order to replace MDD but I still want to use MLA (I don't want to use Harmony yet).
    So I followed your instructions and the code compiles successfully in MPLAB, but in the circuit, f_open does not create or open any file in the thumb drive.
    If I use a FRESULT variable to debug the return code via printf, the circuit resets every time I insert a thumb drive.

    Can you share your source code and the version of FatFs you used along with the project folder so I can study the code and find where exactly is my error?

    Thanks.

    返信削除
    返信
    1. Hello Leo, Thanks for your comment.

      I'm sorry but I don't have any source codes or environment which were written in 2014. So I can't confirm the version of FatFs or MLA and I can't try why your code doesn't work.

      If you succeeded to access your thumb drive via MDD, why don't try to read the MDD source code and copy its needed parts to diskio.c by yourself. I tried it when I write this blog.

      I'm sorry I can't help you.

      Best regards,

      削除
    2. Hi Kintarou,

      Thank you very much for your response, I really appreciate your tip about using the code of the MDD source. It's going to be hard but I think in the end it is also going to be a rewarding experience.

      No problem if you don't have the source at hand. After all, it's been almost 4 years now.

      Keep up the work, your blog is really very interesting and inspiring.

      Best regards,
      Leo

      削除