ラベル FatFs の投稿を表示しています。 すべての投稿を表示
ラベル FatFs の投稿を表示しています。 すべての投稿を表示

2025年6月4日水曜日

Enhanced Midrange PIC16FシリーズでSDカードの読み書きをする

PIC16F18857など一部のEnhanced midrange PIC16Fシリーズは、FATファイルシステムを持つMMC互換カード(要するにSDカード)の読み書きをできるだけの非常に大きなプログラムメモリとデータメモリを持っています。 

MMCカードは、通常はセクターサイズが512バイトになっているため、512バイトごとの読み書きが求められます。そのため、少なくとも512バイトのバッファーは必要となります。一方で、PIC16F18857は4kB(4096バイト)ものデータメモリを持っているため、充分にその能力があるのです。PIC16Fシリーズもここまで来ました(参考までに、PIC16F84Aは68バイト、PIC16F1827は384バイトです)。

***** 

余談ですが、PIC16Fシリーズは基本的に以下のようなデータメモリ構造になっています。 

このアーキテクチャは昔から変わりませんね。ひとつのバンクに7bit分(128バイト分)のメモリがあって、12バイトのコアレジスタ(どのバンクからでも共通で参照される特別なレジスタ)、SFR(周辺機器を制御するためのレジスタ)、80バイトの汎用レジスタ、16バイトの共通汎用レジスタ(どのバンクからでも共通で参照される汎用レジスタ)となっています。

PIC16F18857では、なんとバンクが全部で64個もあります。 そして、この80バイトの汎用レジスタはバンク0~50で実装されており、80×51=4080バイト、そして共通汎用レジスタの16バイトを足して4096バイトというわけです。

また、PIC16Fシリーズはハーバードアーキテクチャ(プログラムメモリとデータメモリが別のメモリ空間となっているアーキテクチャ)ですが、Enhanced MidrangeではFSR (File Select Register)が16bit長に拡張されており、また、データメモリもプログラムメモリもすべて同じメモリ空間上で間接参照できるようになっています。この汎用データメモリも物理的には80バイト×51バンクとバラバラに配置されていますが、FSR経由でリニアにアクセスできるようにされています。

もはや執念すら感じますね。

***** 

さて、話を戻して、伝統的なアーキテクチャに縛られながらも大容量メモリを実現したこのマイコンを使用して、MMCの読み書きを行ってみたいと思います。

MMCの読み書きをする以上は、FATファイルシステムに対応できなければ意味がありません。今回は(も?)ChaN氏のFatFsを使用してMMCの読み書きをしてみます。

回路

まずは以下のような回路を実装します。

私はユニバーサル基板とDIP化されたMicroSDスロットを使用して実装しました。ブレッドボードでももしかしたらいけるかもしれませんが、SPIが数MHzでの送信となるため、周囲との結合容量が大きいともしかしたら上手く動かないかもしれません。

電源のことは回路図に書きませんでしたが、簡単にUSBから電源をもらって3.3Vの3端子レギュレーターで落とす回路を作って供給しています。

  • RA1:挿入検知
  • RA2:CS
  • RB0:MISO(MMC出力→PIC入力)
  • RC6:MOSI(PIC出力→MMC入力)
  • RC7:SCLK 

もっぱらPIC16F18857にはPPS (Peripheral Pin Select)機能があって、SPI含むペリフェラルを任意のポート(※)に割り当てることができます。そのため、これと全く同じ回路が必須というわけではなく、PPSで割り当て可能な範囲で別のポートを使っても問題ありません。

※:割り当て可能なポートの範囲はペリフェラルごとに決まっています(本当に何でも好きなポートに割り当てられるわけではありません)。データシートをよく読んで回路を設計してください。 

FatFsの適用

さて、FatFsをプロジェクトに追加していきます。結論だけ知りたい人は、こちらのGithubレポジトリにコードを上げておいたので、こちらも参考にしてください。

やることは主に以下の2つです。

  • FatFsのオプション設定
  • FatFsの改造 
  • diskio.cの実装

順に説明していきます。

FatFsのオプションの設定

ffconf.hをいじります。デフォルトで読み書き両方できるオプションになっているので大きくいじる必要は無いかと思います。RTCが搭載されている場合は、FF_FS_NORTCを0にしたうえでget_fattime関数を実装しましょう。今回はそうではないので、RTC代わりに固定の日付を設定しておきます。下の例は日付を2025/6/1としているパターンです。

#define FF_FS_NORTC      1
#define FF_NORTC_MON     6
#define FF_NORTC_MDAY    1
#define FF_NORTC_YEAR    2025
/* The option FF_FS_NORTC switches timestamp feature. If the system does not have
/  an RTC or valid timestamp is not needed, set FF_FS_NORTC = 1 to disable the
/  timestamp feature. Every object modified by FatFs will have a fixed timestamp
/  defined by FF_NORTC_MON, FF_NORTC_MDAY and FF_NORTC_YEAR in local time.
/  To enable timestamp function (FF_FS_NORTC = 0), get_fattime() need to be added
/  to the project to read current time form real-time clock. FF_NORTC_MON,
/  FF_NORTC_MDAY and FF_NORTC_YEAR have no effect.
/  These options have no effect in read-only configuration (FF_FS_READONLY = 1). */

FatFsの改造

さて、PIC16FシリーズでFatFsを使う場合、FatFs本体の改造を一点だけする必要があります。

というのも、XC8では関数の再帰呼び出しができないのですが、FatFsでは再帰呼び出しがされている場所があります。putc_bfd(putbuff* pb, TCHAR c)という内部関数なのですが、LFをCRLFに置き換える場合、\nがやって来た時に再帰呼び出しで\rを追加する処理を行っています。

再帰呼び出しを使わずとも簡単なif文で対処できる程度のものなので、再帰以外の部分をputc_bfd_internal(putbuff* pb, TCHAR c)という別の関数に分離して対処しましょう。

static void putc_bfd_internal(putbuff* pb, TCHAR c)
{
    UINT n;
    int i, nc;
#if FF_USE_LFN && FF_LFN_UNICODE
    WCHAR hs, wc;
#if FF_LFN_UNICODE == 2
    DWORD dc;
    const TCHAR* tp;
#endif
#endif

    i = pb->idx;            /* Write index of pb->buf[] */
    if (i < 0) return;        /* In write error? */
    nc = pb->nchr;            /* Write unit counter */

#if FF_USE_LFN && FF_LFN_UNICODE
#if FF_LFN_UNICODE == 1        /* UTF-16 input */
    if (IsSurrogateH(c)) {    /* Is this a high-surrogate? */
        pb->hs = c; return;    /* Save it for next */
    }
    hs = pb->hs; pb->hs = 0;
    if (hs != 0) {            /* Is there a leading high-surrogate? */
        if (!IsSurrogateL(c)) hs = 0;    /* Discard high-surrogate if a stray high-surrogate */
    } else {
        if (IsSurrogateL(c)) return;    /* Discard stray low-surrogate */
    }
    wc = c;
#elif FF_LFN_UNICODE == 2    /* UTF-8 input */
    for (;;) {
        if (pb->ct == 0) {    /* Not in the multi-byte sequence? */
            pb->bs[pb->wi = 0] = (BYTE)c;    /* Save 1st byte */
            if ((BYTE)c < 0x80) break;                    /* Single byte code? */
            if (((BYTE)c & 0xE0) == 0xC0) pb->ct = 1;    /* 2-byte sequence? */
            if (((BYTE)c & 0xF0) == 0xE0) pb->ct = 2;    /* 3-byte sequence? */
            if (((BYTE)c & 0xF8) == 0xF0) pb->ct = 3;    /* 4-byte sequence? */
            return;                                        /* Invalid leading byte (discard it) */
        } else {                /* In the multi-byte sequence */
            if (((BYTE)c & 0xC0) != 0x80) {    /* Broken sequence? */
                pb->ct = 0; continue;        /* Discard the sequence */
            }
            pb->bs[++pb->wi] = (BYTE)c;    /* Save the trailing byte */
            if (--pb->ct == 0) break;    /* End of the sequence? */
            return;
        }
    }
    tp = (const TCHAR*)pb->bs;
    dc = tchar2uni(&tp);            /* UTF-8 ==> UTF-16 */
    if (dc == 0xFFFFFFFF) return;    /* Wrong code? */
    hs = (WCHAR)(dc >> 16);
    wc = (WCHAR)dc;
#elif FF_LFN_UNICODE == 3    /* UTF-32 input */
    if (IsSurrogate(c) || c >= 0x110000) return;    /* Discard invalid code */
    if (c >= 0x10000) {        /* Out of BMP? */
        hs = (WCHAR)(0xD800 | ((c >> 10) - 0x40));     /* Make high surrogate */
        wc = 0xDC00 | (c & 0x3FF);                    /* Make low surrogate */
    } else {
        hs = 0;
        wc = (WCHAR)c;
    }
#endif
    /* A code point in UTF-16 is available in hs and wc */

#if FF_STRF_ENCODE == 1        /* Write a code point in UTF-16LE */
    if (hs != 0) {    /* Surrogate pair? */
        st_word(&pb->buf[i], hs);
        i += 2;
        nc++;
    }
    st_word(&pb->buf[i], wc);
    i += 2;
#elif FF_STRF_ENCODE == 2    /* Write a code point in UTF-16BE */
    if (hs != 0) {    /* Surrogate pair? */
        pb->buf[i++] = (BYTE)(hs >> 8);
        pb->buf[i++] = (BYTE)hs;
        nc++;
    }
    pb->buf[i++] = (BYTE)(wc >> 8);
    pb->buf[i++] = (BYTE)wc;
#elif FF_STRF_ENCODE == 3    /* Write a code point in UTF-8 */
    if (hs != 0) {    /* 4-byte sequence? */
        nc += 3;
        hs = (hs & 0x3FF) + 0x40;
        pb->buf[i++] = (BYTE)(0xF0 | hs >> 8);
        pb->buf[i++] = (BYTE)(0x80 | (hs >> 2 & 0x3F));
        pb->buf[i++] = (BYTE)(0x80 | (hs & 3) << 4 | (wc >> 6 & 0x0F));
        pb->buf[i++] = (BYTE)(0x80 | (wc & 0x3F));
    } else {
        if (wc < 0x80) {    /* Single byte? */
            pb->buf[i++] = (BYTE)wc;
        } else {
            if (wc < 0x800) {    /* 2-byte sequence? */
                nc += 1;
                pb->buf[i++] = (BYTE)(0xC0 | wc >> 6);
            } else {            /* 3-byte sequence */
                nc += 2;
                pb->buf[i++] = (BYTE)(0xE0 | wc >> 12);
                pb->buf[i++] = (BYTE)(0x80 | (wc >> 6 & 0x3F));
            }
            pb->buf[i++] = (BYTE)(0x80 | (wc & 0x3F));
        }
    }
#else                        /* Write a code point in ANSI/OEM */
    if (hs != 0) return;
    wc = ff_uni2oem(wc, CODEPAGE);    /* UTF-16 ==> ANSI/OEM */
    if (wc == 0) return;
    if (wc >= 0x100) {
        pb->buf[i++] = (BYTE)(wc >> 8); nc++;
    }
    pb->buf[i++] = (BYTE)wc;
#endif

#else                            /* ANSI/OEM input (without re-encoding) */
    pb->buf[i++] = (BYTE)c;
#endif

    if (i >= (int)(sizeof pb->buf) - 4) {    /* Write buffered characters to the file */
        f_write(pb->fp, pb->buf, (UINT)i, &n);
        i = (n == (UINT)i) ? 0 : -1;
    }
    pb->idx = i;
    pb->nchr = nc + 1;
}

static void putc_bfd (putbuff* pb, TCHAR c)
{
    if(FF_USE_STRFUNC == 2 && c == '\n') {     /* LF -> CRLF conversion */
        putc_bfd_internal(pb, '\r');
    }
    putc_bfd_internal(pb, c);
}

diskio.cの実装

さて、こいつが山場です。MMCへの入出力をすべて実装しなければなりません。

色々悩みましたが、まずはdiskio_hardware.hというファイルを作り、PICの入出力ピンの設定などを盛り込むこととしました。

#ifndef DISKIO_HARDWARE_H
#define    DISKIO_HARDWARE_H

#include <xc.h> // include processor files - each processor file is guarded.  

#define _XTAL_FREQ 32e6

/* Definitions of physical drive number for each drive */
#define DEV_MMC        0    /* Example: Map MMC/SD card to physical drive 1 */

// #PIC   #MMC
//  RA1 -> INS
//  RA2 -> CS
//  RB0 -> DO
//  RC6 -> DI
//  RC7 -> SCLK

#define MMC_TRISA_MASK  0xFB
#define MMC_TRISB_MASK  0xFF
#define MMC_TRISC_MASK  0x3F

#define MMC_PPSOUT_SCK    RC7PPS
#define MMC_PPSOUT_SDO    RC6PPS
#define MMC_PPSIN_SDI    0x08    // RB0
#define MMC_PPSIN_SCK    0x17    // 0x09 for RC7

#define MMC_CS                (LATAbits.LATA2)
//#define MMC_CE                (LATDbits.LATD7)
#define MMC_INS_PORT        (PORTAbits.RA1)
#define MMC_INS_WPU            (WPUAbits.WPUA1)
#define MMC_INS_IOCP        (IOCAPbits.IOCAP1)
#define MMC_INS_IOCN        (IOCANbits.IOCAN1)
#define MMC_INS_IOCF        (IOCAFbits.IOCAF1)
#define MMC_IsInserted()    (!MMC_INS_PORT)

// If Chip enable is implemented, these macro should be implemented
#define MMC_ChipEnable(on)
#define MMC_IsChipEnable()  (true)

void MMC_Init(void);
void MMC_Interrupt(void);

void MMC_Eject(void);
bool MMC_IsEjected(void);

// need to be mplemented in main.c
void MMC_AccessLamp(bool on);

#endif    /* DISKIO_HARDWARE_H */

ここに登録している関数(MMC_Initなど)はFatFsを通り越して呼ぶことを前提とした関数としました。主な役目は以下の通りです。

  • MMC_Init:MMCモジュールの初期化を行う。主にTRISの設定、Interrupt On Changeの設定など。
  • MMC_Interrupt:割り込み処理ルーチン。MMCの抜き差しに反応して発生する割り込みを処理する。
  • MMC_Eject:MMCを安全に取り出す。これを操作した以後はファイルの読み書きはできなくなる。
  • MMC_IsEjected:MMCが安全に取り出されているかどうか。
  • MMC_AccessLamp:MMCのアクセスランプを制御するコールバック関数。main.cなどで実装が必要。

これらの関数と、diskio.hで指定された関数を実装したdiskio.cを作りました。長いので全体はGithubを参照してください。

いくらか抜粋です。まずはdisk_initialize関数です。

DSTATUS disk_initialize (
    BYTE pdrv                /* Physical drive nmuber to identify the drive */
)
{
    BYTE n, cmd, ty, ocr[4];
    UINT tmr;

    if(pdrv != DEV_MMC)
        return STA_NOINIT;    
    if(DiskStat & STA_NODISK)
        return DiskStat;
    
    MMC_ChipEnable(true);
    MMC_SPIInit();
    __delay_ms(5);

    for (n = 10; n; n--)
        MMC_SendSPI(0xFF);  /* 80 dummy clocks */

    ty = 0;
    if(MMC_send_cmd(CMD0, 0) == 1) {            /* Enter Idle state */
        if(MMC_send_cmd(CMD8, 0x1AA) == 1) {    /* SDv2? */
            for(n = 0; n < 4; n++)
                ocr[n] = MMC_SendSPI(0xFF);        /* Get trailing return value of R7 resp */
            if(ocr[2] == 0x01 && ocr[3] == 0xAA) {        /* The card can work at vdd range of 2.7-3.6V */
                for(tmr = 1000; tmr; tmr--) {            /* Wait for leaving idle state (ACMD41 with HCS bit) */
                    if(MMC_send_cmd(ACMD41, 1UL << 30) == 0)
                        break;
                    __delay_ms(1);
                }
                if(tmr && MMC_send_cmd(CMD58, 0) == 0) {        /* Check CCS bit in the OCR */
                    for(n = 0; n < 4; n++)
                        ocr[n] = MMC_SendSPI(0xFF);
                    ty = (ocr[0] & 0x40) ? CT_SD2 | CT_BLOCK : CT_SD2;    /* SDv2 */
                }
            }
        } else {                            /* SDv1 or MMCv3 */
            if(MMC_send_cmd(ACMD41, 0) <= 1)     {
                ty = CT_SD1;
                cmd = ACMD41;    /* SDv1 */
            } else {
                ty = CT_MMC;
                cmd = CMD1;    /* MMCv3 */
            }
            for(tmr = 1000; tmr; tmr--) {            /* Wait for leaving idle state */
                if(MMC_send_cmd(cmd, 0) == 0)
                    break;
                __delay_ms(1);
            }
            if(!tmr || MMC_send_cmd(CMD16, 512) != 0)    /* Set R/W block length to 512 */
                ty = 0;
        }
    }
    CardType = ty;
    MMC_deselect();

    if (ty) {            /* Initialization succeded */
        DiskStat &= ~STA_NOINIT;        /* Clear STA_NOINIT */
    }

    return DiskStat;
}

コマンドをいくつか送ってカードの判別と初期化をしています。

つづいてディスクの読み込み機能です。 

DRESULT disk_read (
    BYTE pdrv,        /* Physical drive nmuber to identify the drive */
    BYTE *buff,        /* Data buffer to store read data */
    LBA_t sector,    /* Start sector in LBA */
    UINT count        /* Number of sectors to read */
)
{
    if((pdrv != DEV_MMC) || (count == 0))
        return RES_PARERR;
    if(DiskStat & STA_NOINIT)
        return RES_NOTRDY;
    
    if(!(CardType & CT_BLOCK))
        sector *= 512;    /* Convert to byte address if needed */

    if(count == 1) {    /* Single block read */
        if((MMC_send_cmd(CMD17, sector) == 0) && MMC_ReceiveDataBlock(buff, 512))
            count = 0;
    } else {                /* Multiple block read */
        if(MMC_send_cmd(CMD18, sector) == 0) {    /* READ_MULTIPLE_BLOCK */
            do {
                if(!MMC_ReceiveDataBlock(buff, 512))
                    break;
                buff += 512;
            } while (--count);
            MMC_send_cmd(CMD12, 0);                /* STOP_TRANSMISSION */
        }
    }
    MMC_deselect();

    return (count > 0) ? RES_ERROR : RES_OK;
}

シングルブロックリードとマルチブロックリードでコマンドを送り分けています。

つづいてディスクの書き込み機能です。

DRESULT disk_write (
    BYTE pdrv,            /* Physical drive nmuber to identify the drive */
    const BYTE *buff,    /* Data to be written */
    LBA_t sector,        /* Start sector in LBA */
    UINT count            /* Number of sectors to write */
)
{
    if((pdrv != DEV_MMC) || (count == 0))
        return RES_PARERR;
    if(DiskStat & STA_NOINIT)
        return RES_NOTRDY;
    if(DiskStat & STA_PROTECT)
        return RES_WRPRT;

    if(!(CardType & CT_BLOCK))
        sector *= 512;    /* Convert to byte address if needed */

    if(count == 1) {    /* Single block write */
        if((MMC_send_cmd(CMD24, sector) == 0) && MMC_SendDataBlock(buff, 0xFE))
            count = 0;
    } else {                /* Multiple block write */
        if(CardType & CT_SDC) MMC_send_cmd(ACMD23, count);
        if(MMC_send_cmd(CMD25, sector) == 0) {    /* WRITE_MULTIPLE_BLOCK */
            do {
                if(!MMC_SendDataBlock(buff, 0xFC)) break;
                buff += 512;
            } while (--count);
            if (!MMC_SendDataBlock(0, 0xFD))    /* STOP_TRAN token */
                count = 1;
        }
    }
    MMC_deselect();

    return count ? RES_ERROR : RES_OK;
}

こちらもシングルブロックライトとマルチブロックライトを別々に実装しています。ここでポイントは、データを送ったからと言って即MMCのフラッシュメモリにデータが書き込まれているわけではないことです。あくまでもデータをMMCに送っただけで、それとは非同期でフラッシュメモリへのデータの書き込みが実行されていきます。

では、書き込みが完了したことを確認するにはどうしたら良いかと言うと、次のdisk_ioctlのコマンドCTRL_SYNCを送れば良いです。

DRESULT disk_ioctl (
    BYTE pdrv,        // Physical drive nmuber (0..)
    BYTE cmd,        // Control code
    void *buff        // Buffer to send/receive control data
)
{
    DRESULT res;
    BYTE n, csd[16];
    DWORD csize;
#if CMD_FATFS_NOT_USED
    BYTE *ptr = buff;
#endif
#if _USE_ERASE
    DWORD *dp, st, ed;
#endif
    
    if(pdrv != DEV_MMC)
        return RES_PARERR;

    res = RES_ERROR;

    if(DiskStat & STA_NOINIT)
        return RES_NOTRDY;

    switch (cmd) {
    case CTRL_SYNC :        // Make sure that no pending write process. Do not remove this or written sector might not left updated. 
        if(MMC_select())
            return RES_OK;
        break;

    case GET_SECTOR_COUNT :    // Get number of sectors on the disk (DWORD) 
        if ((MMC_send_cmd(CMD9, 0) == 0) && MMC_ReceiveDataBlock(csd, 16)) {
            if ((csd[0] >> 6) == 1) {    // SDC ver 2.00 
                csize = csd[9] + ((WORD)csd[8] << 8) + ((DWORD)(csd[7] & 63) << 16) + 1;
                *(DWORD*)buff = csize << 10;
            } else {                    // SDC ver 1.XX or MMC
                n = (BYTE)((csd[5] & 15) + ((csd[10] & 128) >> 7) + ((csd[9] & 3) << 1) + 2);
                csize = (csd[8] >> 6) + ((WORD)csd[7] << 2) + ((WORD)(csd[6] & 3) << 10) + 1;
                *(DWORD*)buff = csize << (n - 9);
            }
            res = RES_OK;
        }
        break;

    case GET_SECTOR_SIZE :    // Get sector size (WORD) 
        *(WORD*)buff = 512;
        res = RES_OK;
        break;

    case GET_BLOCK_SIZE :    // Get erase block size in unit of sector (DWORD) 
        if (CardType & CT_SD2) {    // SDv2? 
            if (MMC_send_cmd(ACMD13, 0) == 0) {    // Read SD status 
                MMC_SendSPI(0xFF);
                if (MMC_ReceiveDataBlock(csd, 16)) {                // Read partial block 
                    for (n = 64 - 16; n; n--) MMC_SendSPI(0xFF);    // Purge trailing data 
                    *(DWORD*)buff = 16UL << (csd[10] >> 4);
                    res = RES_OK;
                }
            }
        } else {                    // SDv1 or MMCv3 
            if ((MMC_send_cmd(CMD9, 0) == 0) && MMC_ReceiveDataBlock(csd, 16)) {    // Read CSD 
                if (CardType & CT_SD1) {    // SDv1 
                    *(DWORD*)buff = (((WORD)(csd[10] & 63) << 1) + ((WORD)(csd[11] & 128) >> 7) + 1) << ((csd[13] >> 6) - 1);
                } else {                    // MMCv3 
                    *(DWORD*)buff = ((DWORD)((csd[10] & 124) >> 2) + 1) * ((BYTE)(((csd[11] & 3) << 3) + ((csd[11] & 224) >> 5) + 1));
                }
                res = RES_OK;
            }
        }
        break;
#if _USE_ERASE
    case CTRL_ERASE_SECTOR :    // Erase a block of sectors (used when _USE_ERASE == 1) 
        if (!(CardType & CT_SDC)) break;                // Check if the card is SDC 
        if (disk_ioctl(drv, MMC_GET_CSD, csd)) break;    // Get CSD 
        if (!(csd[0] >> 6) && !(csd[10] & 0x40)) break;    // Check if sector erase can be applied to the card 
        dp = buff; st = dp[0]; ed = dp[1];                // Load sector block 
        if (!(CardType & CT_BLOCK)) {
            st *= 512; ed *= 512;
        }
        if (send_cmd(CMD32, st) == 0 && send_cmd(CMD33, ed) == 0 && send_cmd(CMD38, 0) == 0 && wait_ready(30000))    // Erase sector block 
            res = RES_OK;    // FatFs does not check result of this command 
        break;
#endif
#if CMD_FATFS_NOT_USED
    // Following commands are never used by FatFs module 
    case MMC_GET_TYPE :        // Get card type flags (1 byte)
        *ptr = CardType;
        res = RES_OK;
        break;

    case MMC_GET_CSD :        // Receive CSD as a data block (16 bytes)
        if (MMC_send_cmd(CMD9, 0) == 0        // READ_CSD
            && MMC_ReceiveDataBlock(ptr, 16))
            res = RES_OK;
        break;

    case MMC_GET_CID :        // Receive CID as a data block (16 bytes)
        if (MMC_send_cmd(CMD10, 0) == 0        // READ_CID
            && MMC_ReceiveDataBlock(ptr, 16))
            res = RES_OK;
        break;

    case MMC_GET_OCR :        // Receive OCR as an R3 resp (4 bytes)
        if (MMC_send_cmd(CMD58, 0) == 0) {    // READ_OCR
            for (n = 4; n; n--) *ptr++ = MMC_SendSPI(0xFF);
            res = RES_OK;
        }
        break;

    case MMC_GET_SDSTAT :    // Receive SD statsu as a data block (64 bytes)
        if (MMC_send_cmd(ACMD13, 0) == 0) {    // SD_STATUS
            MMC_SendSPI(0xFF);
            if (MMC_ReceiveDataBlock(ptr, 64))
                res = RES_OK;
        }
        break;
    case CTRL_POWER:
        switch (ptr[0]) {
        case 0:        // Sub control code (POWER_OFF) 
            MMC_ChipEnable(false);
            res = RES_OK;
            break;
        case 1:        // Sub control code (POWER_GET) 
            ptr[1] = MMC_IsChipEnable();
            res = RES_OK;
            break;
        default :
            res = RES_PARERR;
        }
        return res;
#endif
    default:
        res = RES_PARERR;
    }

    MMC_deselect();

    return res;
}

FatFsが使わないメソッドも実装していますが、PICのプログラムメモリを節約できるようにプリプロセッサで除外できるようにしています。

mainルーチンの作成

ここまで作ったらほとんどできたも同然です。mainルーチンは比較的シンプルに実装できます。

#define _XTAL_FREQ 32e6
#include <xc.h>
#include <stdbool.h>
#include <string.h>
#include "FatFs/diskio_hardware.h"
#include "FatFs/ff.h"

#define    LED_TRIS    (TRISBbits.TRISB5)
#define    LED_LAT        (LATBbits.LATB5)
#define    LED(on)        (LED_LAT = (on))

static FATFS fs;
static FIL fp;

void MMC_AccessLamp(bool on)
{
    LED(on);
}

void setup()
{
    ANSELA = 0x00;
    ANSELB = 0x00;
    ANSELC = 0x00;

    TRISA = 0xFF;
    TRISB = 0xFF;
    TRISC = 0xFF;
    
    LED(false);
    LED_TRIS = 0;
    
    MMC_Init();
    
    INTCONbits.PEIE = 1;
    INTCONbits.GIE = 1;
}

void loop()
{
    f_mount(&fs, "0:", 0);
    if(f_open(&fp, "TEST.TXT", FA_OPEN_APPEND | FA_WRITE | FA_READ) == FR_OK) {
        f_puts("Hello, world!!\n", &fp);

        f_close(&fp);
    }

    f_unmount("0:");
    
    __delay_ms(1000);
}

void main(void)
{
    setup();
    while(1)
        loop();
}

void __interrupt() isr(void)
{
    MMC_Interrupt();
}

1秒に1回「Hello, world!!」というテキストをファイルに書き込んでいくだけのプログラムです。

前述した通り、MMC_Interrupt()関数でカードの抜き差しを検知していますので、カードを差していなかったらちゃんとf_open関数がエラーになって書き込みは行われません。また、MMC_AccessLamp関数でアクセス時にLEDを点灯させることもちゃんとできています。

まとめ 

無事、FatFsがPIC16Fシリーズでも動きました。私が初めて触ったマイコンもPIC16Fシリーズでしたが、同じシリーズのマイコンでここまでできるようになるとは、当時の私には想像もできなかったでしょうね。 

さて、ここまで作って使ったメモリはどれくらいでしょう。

データメモリは1,382バイト、プログラムメモリは13,372ワードでした。やはりかなりの量のメモリを使いますが、一方で半分いかないくらいなので、別のプログラムを盛り込んで何か素晴らしいアプリケーションを実装する余裕もあります。ここからまたどんどん夢が広がっていきますね。 

2014年7月15日火曜日

PIC24FJ64GB002でUSBメモリーにアクセスする

最近、秋月電子でPIC24FJ64GB002の取り扱いが始まりました。
USB-OTGに対応したPICとしてはかなり有名どころで、多くの書籍やウェブページでも取り上げられており、資料は豊富にあります。にもかかわらず入手性は最悪で、国内の通販では共立電子くらいでしか取り扱っていなくて、さもなくばDigiKeyとかで海外から取り寄せるくらいしか方法はありませんでした。
しかし、ついに秋月電子で取り扱いが始まりました。秋葉原に行くだけで買える!しかも、共立電子の半額以下という破格の値段です。

というわけで、早速買ってきました。

 
手前がPIC24FJ64GB002で、奥がPIC32MX250F128Bです。
この2つ、前者は16bitマイコン、 後者は32bitマイコンでアーキテクチャも何もかも全く違うのかなと思ったら、なんとまあピンコンパチ(多分)でした。少なくとも電源やUSB周りは同じで、PIC32MXのほうをいじってみたときに使った回路をそのまま転用することができました。
というわけで、そのPIC32MXをいじったときに最終的にいきついたFatFsを動かすお話に用意したプログラムをPIC24FJ64GB002に移植してみました。


と言ってもほとんど苦労はしません。

そもそもこのデモプログラムはPIC24FJ64GB004でも動くように作られてて、多分これと002の違いはピン数とかパッケージとかその程度です。また、FatFsは完全にデバイス非依存で、型の大きさとかに気をつけておく程度でちゃんと動作はしてくれるはずです。というわけで、プログラムの修正はほとんど必要ありません。


まずは前回のプロジェクトのプロパティからDeviceをPIC24FJ64GB002に変更します。
…と言いたいところですが、PIC32MXのほうの設定を消すのはもったいないです。かと言ってバックアップとってゴニョゴニョやるのもめんどくさい。というわけで、プロジェクトのコンフィグレーションを追加してしまいましょう。MPLAB Xのプロジェクトはコンフィグレーションと呼ばれるものを複数作っておくことで、同じ構成のプログラムを別のデバイスに簡単に移植できるようになっているようです。もちろん、コードがそれに対応していたらの話ですが。

プロパティのカテゴリーの欄の下にある「Manage Configurations...」というボタンを押すとこんな画面が出てきます。


Duplicateで既存のコンフィグレーションをコピーできますが、まあコピーしてもコンパイラが違うとコンパイラの設定が全部吹っ飛んじゃうんであまり意味無いでしょう。ということで、Newを押して、適当な名前のコンフィグレーションを作ります。そして、Set Activeボタンを押せば、そのコンフィグレーションがアクティブになります。


そして、作ったPIC24FJ64GB002のほうのコンフィグレーションに対して、設定をしていきます。デバイスが未設定なので、右上のデバイスの欄からPIC24FJ64GB002を選択します。そして、コンパイラをXC16に設定します。そして前回同様、インクルードファイルのパスとヒープ領域の容量を設定してあげます。


次に、ソースコードの修正を若干します。修正箇所は、コンフィグレーションビットの設定とマイコンの初期化のプログラムのみです。

とは言っても、デモプログラムのほうにPIC24FJ64GB004のコードが入っているので、プリプロセッサの__PIC24FJ64GB004__を__PIC24FJ64GB002__に変更してやるだけです。というより、#if definedで追加してやる形にしました(具体的なコードはMicrochipの著作物なので掲載は遠慮しておきます。各自MLAのサンプルコードからコピーしてください)。

#if defined(__PIC24FJ64GB002__) || defined(__PIC24FJ64GB004__)
    //中略
#elif defined( __PIC32MX__ )
    //中略
#else
    #error Unsupposed Processor
#endif

コンパイルは、クリーンビルドをしてください。PIC32MXのほうの何かが残っているとリンクで謎のエラーが起きて焦ります。

そして、書き込みが終わればUSBメモリーを挿して、めでたくサンプルファイルの書き込みがされて完成です。

めでたしめでたし。



ここまで書いてなんですが、値段で見ると、PIC24FJ64GB002が340円、 PIC32MX250F128Bが360円です。20円差でより高性能なPICが買えるなら、別にPIC32MXのほうでいいんじゃないかなーって気はします。
まあでも最初に言った通り、PIC24のほうは資料の豊富さは格別です。気が向いたらBluetooth Stackとかに手を出してみようかなー。

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です。