2025年6月22日日曜日

PIC32MXでUSB MicroSDカードリーダーを作る (USB Device MSD class)

今回はPIC32MXでUSB接続のMicroSDカードリーダーを作ります。

もっぱら、PIC32MXではUSB Hostも対応しておりますので、USBメモリーの読み書きなどにも使えますが、今回はDevice側となります。 

環境

  • PIC32MX250F128B
  • MPLAB X IDE v6.20
  • XC32 v4.60
  • MPLAB Code Configurator v5.5.1

MPLAB Code Configurator (MCC)はMPLAB Harmonyを包含したコード生成ツールで、最近はHarmonyはMCC経由で入れることになるようです。

回路

まずは回路図です。 

シンプルにUSBとMicroSDカードのソケットを接続した回路になります。インジケーターとしてD1のLEDも用意しました。

PIC32MXシリーズでは内蔵オシレーターを持っていますが、USBを使うためには外付け8MHzのクロックが必要となります。

ファームウェア

続いて、MPLAB Xにてファームウェアを作成していきます。

空のプロジェクトの作成 

プロジェクトを作った後にMCCを使ってコード生成を行うので、空のプロジェクト(Application Project)を作成すればOKです。

ちなみにMCCはプロジェクトフォルダと同じ階層にsrcフォルダを作る仕様になっていて、これは設定等で変更できないようです。ですので、自分でプロジェクト用のフォルダを作って、その中にプロジェクトを作ると良いでしょう(あまり良い感じの解決法ではないですが)。

MPLAB Code Configurator (MCC)の設定

プロジェクトを作成すると自動的にMCCが起動します。起動しない場合はMPLABの上のほうの「MCC」アイコンをクリックします。アイコンが無い場合はプラグインが入っていない可能性があるのでググりましょう。

クロック設定

まずはクロック設定をします。Project GraphタブのPluginsからClock Configurationを選択します。

そうすると、Clock Diagramが出てくるので設定を行います。

まずはPrimary OscillatorをXTに設定し、周波数を8,000,000Hz (=8MHz)に設定します。ただしこのままでは右上のUSB Clockが8MHzのままなので、UPLLEN (USB PLL Enable)をONして48MHzにしてあげましょう。

これでクロックの設定は完了です。コンフィグレーションビットなどを直接触ることなく設定できるので超簡単ですね。

ピン設定

続いてピン設定を行います。PIC32MXシリーズもPPS (Peripheral Pin Select)という周辺回路を好きなピン(実はそんなに自由度は高くないですが)に割り当てる機能がありますが、MCCではその設定を行うこともできます。

クロック設定と同様にProject Graphタブから「Pin Configuration」を開きます。3つタブが出てきますが、Pin Tableで設定をしていきましょう。 

横にポート番号が並んでいて、縦に周辺回路の機能が並んでいます。青色が割り当て可能、緑が割り当て済み、灰色が割り当て不可です。

こんな感じに設定してみました。

USBはUSBID及びVBUSONは使用しないので未選択です。SPIはSPI2を使用することとします。SDカードのCSとLEDはGPIOになるのでその設定をします。クロックも忘れずにポートを設定する必要があります。

続いてPin Settingsタブを開いてGPIOの設定をします。

SDカードのCSピン(RB3)は出力ピンで、非選択のときはHighなのでそのように設定します。LEDのRB4も出力で、不点灯のときはLowなのでそのように設定します。 

Harmonyの設定

続いて使用するHarmony(ライブラリ)の設定をします。左側のDevice Resourcesから、以下のライブラリを取り込み(『+』ボタンを押し)ます (もしも出てこない場合は、ライブラリがインストールされていないので、MCC Content Managerから対応するライブラリを事前にインストールしておいてください)。

  • MSD Function Driver (Libraries - Harmony - USB - Device Stack)
  • SPI (Libraries - Harmony - Drivers - SDCARD)
  • SD Card (Libraries - Harmony - Drivers - SDCARD)
  • SPI2 (Libraries - Harmony - Peripherals - SPI)
  • CORE TIMER (Libraries - Harmony - Peripherals - CORE TIMER)

途中で依存関係にあたるライブラリを一緒に有効化するかという問い合わせが出てきます。基本OKで良いですが、FreeRTOSだけはNoにしておきます(容量がでかすぎて入らなくなるので)。

一通り入れ終わると、Project Graphタブにいろいろなモジュールが並びます。

赤色の部分は何かしら接続しないといけませんので接続していきます。黄色は必須じゃないらしいのでそのままで。

ライブラリの設定

ライブラリにも一部設定が必要なものがあります。

SD Card (SPI)_Instance 0を選択し、Chip Select PinをRB3に設定してやりましょう。  

USB Device Layerを選択するとUSBの設定が出てきます。デフォルトのままでも問題ないですが、何かしら変更したければここに入力すれば良いでしょう。 

コード生成 

最後に上の「Generate」を押すとコードが自動生成されます。

無事コードが生成されたら、最後にビルドしてビルドが通ることを確認しておきましょう。

アプリケーションコードの作成

さて、ビルドが通ったからと喜んで書き込んでみると、うんともすんとも言わないプログラムが出来上がります。デバイスマネージャーからもUSBが認識されない状態です。なぜかと言うと、アプリケーションコードが一切無いからです。USBデバイスを開いて~みたいな作業を全くしていないため何も起こらないわけです。 

なお、このセクションはこのGithubリポジトリのコードを参考にしています。

app.h

まずはapp.hを開いて以下の3つのファイルをincludeに追加します。

#include "definitions.h"
#include "usb/usb_chapter_9.h"
#include "usb/usb_device.h"

つづいて、APP_STATESとAPP_DATAを以下のように修正します。

typedef enum
{
    /* Application's state machine's initial state. */
    APP_STATE_INIT=0,

    /* Application's state machine running state */
    APP_STATE_RUNNING,
} APP_STATES;


typedef struct
{
    /* The application's current state */
    APP_STATES state;

    /* USB Device Handle */
    USB_DEVICE_HANDLE usbDeviceHandle;

    /* Device configured state */
    bool isConfigured;
    
} APP_DATA;

これでapp.hは完了です。

app.c

続いてapp.cの修正をしていきます。まずは下準備としてLEDの制御関数を作っておきます。

void LED_Off(void)
{
    GPIO_RB4_Clear();
}

void LED_On(void)
{
    GPIO_RB4_Set();
}

次にUSBのイベントが発生したときのイベントハンドラーを作っておきます。

void APP_USBDeviceEventHandler( USB_DEVICE_EVENT event, void * pEventData, uintptr_t context )
{
    /* This is an example of how the context parameter
       in the event handler can be used.*/

    APP_DATA * appDataObject = (APP_DATA*)context;

    switch( event )
    {
        case USB_DEVICE_EVENT_RESET:
        case USB_DEVICE_EVENT_DECONFIGURED:
            appData.isConfigured = false;
            /* Device was reset or de-configured. Update LED status */
            LED_Off();
            break;

        case USB_DEVICE_EVENT_CONFIGURED:
            appData.isConfigured = true;
            /* Device is configured. Update LED status */
            LED_On();
            break;

        case USB_DEVICE_EVENT_SUSPENDED:
            
            LED_Off();
            break;

        case USB_DEVICE_EVENT_POWER_DETECTED:

            /* VBUS is detected. Attach the device. */
            USB_DEVICE_Attach(appDataObject->usbDeviceHandle);
            break;

        case USB_DEVICE_EVENT_POWER_REMOVED:
            appData.isConfigured = false;
            /* VBUS is not detected. Detach the device */
            USB_DEVICE_Detach(appDataObject->usbDeviceHandle);
            LED_Off();
            break;

        /* These events are not used in this demo */
        case USB_DEVICE_EVENT_RESUMED:
            if(appData.isConfigured == true)
            {
                LED_On();
            }
            break;
        case USB_DEVICE_EVENT_ERROR:
        case USB_DEVICE_EVENT_SOF:
        default:
            break;
    }
}

続いて、APP_Initialize関数で追加したAPP_DATA構造体のメンバーの初期化を追加しておきましょう。

void APP_Initialize ( void )
{
    /* Place the App state machine in its initial state. */
    appData.state = APP_STATE_INIT;
    
    /* Set device layer handle as invalid */
    appData.usbDeviceHandle = USB_DEVICE_HANDLE_INVALID;
    
    appData.isConfigured = false;
}

最後に、APP_Tasks関数にてUSBデバイスの初期化を行っておきましょう。

void APP_Tasks ( void )
{
    /* Check the application's current state. */
    switch ( appData.state )
    {
        /* Application's initial state. */
        case APP_STATE_INIT:
        {
             appData.usbDeviceHandle = USB_DEVICE_Open(USB_DEVICE_INDEX_0, DRV_IO_INTENT_READWRITE);

            if(appData.usbDeviceHandle != USB_DEVICE_HANDLE_INVALID)
            {
                /* Set the Event Handler. We will start receiving events after
                 * the handler is set */
                USB_DEVICE_EventHandlerSet(appData.usbDeviceHandle, APP_USBDeviceEventHandler, (uintptr_t)&appData);

                /* Move the application to the next state */
                appData.state = APP_STATE_RUNNING;
            }
             
            break;
        }

        case APP_STATE_RUNNING:
            /* The MSD Device is maintained completely by the MSD function
             * driver and does not require application intervention. So there
             * is nothing related to MSD Device to do here. */
            break;

        /* The default state should never be executed. */
        default:
            break;
        
    }
}

USB_DEVICE_Open関数でデバイスを開いたうえで、USB_DEVICE_EventHandlerSet関数で先ほど実装したAPP_USBDeviceEventHandler関数を登録しているだけです。ソースコードに記載されている通り、初期化さえしてしまえばAPP_STATE_RUNNINGで特段ルーチン処理をする必要はありません。 

---

これにてひとまずパソコンに差すとSDカードのフォルダーが表示されるような状態になったはずです。目標であるMicroSDカードリーダーの完成です。

ちなみに、ファイルの読み書きをしてみるとわかりますがめちゃめちゃ読み書きが遅いです。まあ、まあ、USB Full Speed (12MHz) + SPIアクセス(5MHz)で動作させているので仕方ないですね。今や数百円でカードリーダーが買える時代にわざわざこのためだけに電子工作をする必要はありませんね。

では、このデバイスをベースに市販のカードリーダーには無いものを作るにはどうするか。ここがスタート地点です。 

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ワードでした。やはりかなりの量のメモリを使いますが、一方で半分いかないくらいなので、別のプログラムを盛り込んで何か素晴らしいアプリケーションを実装する余裕もあります。ここからまたどんどん夢が広がっていきますね。 

2025年6月1日日曜日

MPLAB X + Githubでソースコードを管理する

さて、最近久々にPICの比較的大きなプロジェクトをいじっているのですが、バージョン管理をしたいなと思ってMPLAB XとGit (Github)を使ってバージョン管理する方法を確立しました。

ですが、MPLAB XはGitをサポートしているようなので簡単にできるかと思いきや結構苦労しましたので、その方法をこのブログでまとめておきます。

前提条件として環境を書いておきます。

  • OS:Windows 11
  • IDE:MPLAB X 6.20 
  • Git:version 2.49.0.windows.1 

なお、公式ドキュメンテーションは以下のページにあります。
Git Integration in MPLAB® X IDE 

準備:SSHでGithubにアクセスできるようにする。

手始めにSSHでGithubにアクセスできるようにします。すでにできている人は必要ありません。

SSHキーの生成

ユーザーフォルダ直下の「.ssh」フォルダ内でWindowsターミナルを開き、「ssh-keygen -t rsa」を入力します。

そうすると、ファイル名をどうするか聞かれるので、適当に「id_rsa_github」と入力します。次にパスワードをどうするのか聞かれるので、適当に入力します。

PS C:\Users\(userdir)\.ssh> ssh-keygen -t rsa
Generating public/private rsa key pair.
Enter file in which to save the key (C:\Users\(userdir)/.ssh/id_rsa): id_rsa_github
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in id_rsa_github
Your public key has been saved in id_rsa_github.pub

そうすると、.sshフォルダ内に指定したファイル名のファイルが2種類出来上がります。拡張子が「.pub」のものがいわゆる公開キーで、そうではないものが秘密鍵です。公開鍵は誰に見せても大丈夫ですが、秘密鍵は自分のパソコンからは出さないようにしましょう。

SSHキーの登録(ローカル)

ここで出来上がった秘密鍵をgithub.comへアクセスする際に使うように登録します。

.sshフォルダ内にconfigファイル(拡張子なし)を作成し、以下の内容を記述し保存します。

Host github github.com
  HostName github.com
  IdentityFile ~/.ssh/id_rsa_github
  User git

IdentityFileの後のファイル名は自分の環境に合わせて適宜修正してください。 

SSHキーの登録(Github)

Githubを開き、右上のユーザーアイコンから「Settings」を選択し、左ペインの「SSH and GPG keys」を選択します。

SSH keysのところの「New SSH key」をクリックし、タイトルと公開鍵を入力します。公開鍵は先ほど生成された「id_rsa_github.pub」ファイル内の中身です。

最後に「Add SSH key」を押せば完了です。

接続確認

最後に念のためちゃんとGithubに接続できるか確認しておきましょう。ターミナルで「ssh -T git@github.com」と入力します。

PS C:\Users\(userdir)\.ssh> ssh -T git@github.com
Enter passphrase for key 'C:\Users\(userdir)/.ssh/id_rsa_github':
Hi EH500-Kintarou! You've successfully authenticated, but GitHub does not provide shell access.

このように"You've successfully authenticated"と表示されればOKです。Shellアクセスは提供されていないとか言ってきますが気にしなくてOKです。

1. リモートリポジトリの作成

まずはGithubでリモートリポジトリを作成します。

そんなに難しいことは無いです。名前と説明を入力して、Readmeやライセンスを指定すれば良いです。

ちなみに、リポジトリの「Code」を押して「SSH」タブを見るとこのリポジトリのURLが表示されます。後程使うので覚えておきましょう。

 

2. ローカルリポジトリの作成

続いて、MPLAB Xを起動してプロジェクトを作成します。…とは言っても、「File」→「New Project...」からウィザードに従って作っていくだけで、ここは何の変哲もありません。説明は省略します。

プロジェクトが出来上がったら、プロジェクトを右クリックして、 「Versioning」→「Initialize Git Repository」を選択します。

そうすると次のようなダイアログが出てきます。どのフォルダをGit管理下に置くかという話なので、何も考えずにOK押せば良いです(プロジェクトのフォルダになります)。

 

これでローカルリポジトリができました。めでたしめでたし。

次に、.gitignoreを作成します。諸説ありますが、以下のような内容でプロジェクトフォルダ内に作っておけば良いでしょう。

*.d
*.pre
*.p1
*.lst
*.sym
*.obj
*.o
*.sdb
*.obj.dmp
html/
nbproject/private/
nbproject/Package-*.bash
build/
nbbuild/
dist/
nbdist/
nbactions.xml
nb-configuration.xml
funclist
nbproject/Makefile-*
disassembly/
*.map

最後にデフォルトで生成されているファイルをコミットしておきましょう。 プロジェクトを右クリックして「Git」→「Commit」です。

3. リモートリポジトリとローカルリポジトリの紐づけ

さて、公式ドキュメントを見ると、ここから「Team」→「Remote」→「Clone」でリモートリポジトリをクローンせよと出てくるのですが、実際にやろうとすると「Incorrect credentials for repository at ~~~」というエラーが出てきてできません。

 

資格情報(credentials)が正しくないというエラーなので、パスワードとかそういう問題なのかと思っていろいろ試してみてもうまくいきませんでした。

というわけで、いったんMPLAB Xは終了してここからは手動で対応していきます。 

リモートリポジトリの登録

まずは、プロジェクトのフォルダをGit Bashで開いて、以下のコマンドを入力します。当然ながらリモートリポジトリのURLは自分のものに置き換えて考えてください。

$ git remote add origin git@github.com:EH500-Kintarou/PIC16F_MMC.git

これでリモートリポジトリが登録できました。念のため以下のコマンドで正しく登録されているか見ておきましょう。

$ git remote -v
origin  git@github.com:EH500-Kintarou/PIC16F_MMC.git (fetch)
origin  git@github.com:EH500-Kintarou/PIC16F_MMC.git (push)

大丈夫ですね。

上流ブランチの設定

さて、このままpushしようとしても上流ブランチが無いためエラーが出ます。

$ git push
fatal: The current branch master has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin master

To have this happen automatically for branches without a tracking
upstream, see 'push.autoSetupRemote' in 'git help config'.

アドバイスに従って、上流ブランチを設定しましょう。

$ git push --set-upstream origin master
Enter passphrase for key '/c/Users/(userdir)/.ssh/id_rsa_github':
Enumerating objects: 14, done.
Counting objects: 100% (14/14), done.
Delta compression using up to 12 threads
Compressing objects: 100% (14/14), done.
Writing objects: 100% (14/14), 8.26 KiB | 845.00 KiB/s, done.
Total 14 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
remote:
remote: Create a pull request for 'master' on GitHub by visiting:
remote:      https://github.com/EH500-Kintarou/PIC16F_MMC/pull/new/master
remote:
To github.com:EH500-Kintarou/PIC16F_MMC.git
 * [new branch]      master -> master
branch 'master' set up to track 'origin/master'.

この時点で、Githubのほうにmasterブランチが生成されます。 

ここで注意ですが、上流ブランチを設定する際に、すでにリモートリポジトリ内に同じ名前のブランチがあったらエラーが出ます。Githubはデフォルトブランチが「main」、MPLAB Xは「master」としてくるようなので問題は起きなさそうですが、MPLAB Xにもポリコレの流れが来たら同じになってしまうかもしれないので、その際はgit branch -mコマンドなどで一旦別の名前に変えておけば良いでしょう。

ブランチのマージ

さて、Githubで生成されたmainブランチとMPLAB Xで作られたmasterブランチが両方あるのは不便ですのでマージさせます。

まずはgit pullでリモートの情報を落としてきます。

$ git pull
Enter passphrase for key '/c/Users/(userdir)/.ssh/id_rsa_github':
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 4 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (4/4), 1.56 KiB | 177.00 KiB/s, done.
From github.com:EH500-Kintarou/PIC16F_MMC
 * [new branch]      main       -> origin/main
Already up to date.

次に、mainブランチに切り替えます。

$ git checkout main
branch 'main' set up to track 'origin/main'.
Switched to a new branch 'main'

そして、masterブランチをmainブランチにマージします。

$ git merge master
fatal: refusing to merge unrelated histories

おっと、エラーが出ました。それもそのはず、mainとmasterはルーツを同じにしないブランチです。ここは強制的にマージしてしまいましょう。

$ git merge --allow-unrelated-histories master
Merge made by the 'ort' strategy.
 .gitignore                   |  22 ++++++
 Makefile                     | 113 ++++++++++++++++++++++++++++
 nbproject/configurations.xml | 170 +++++++++++++++++++++++++++++++++++++++++++
 nbproject/project.xml        |  23 ++++++
 4 files changed, 328 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 Makefile
 create mode 100644 nbproject/configurations.xml
 create mode 100644 nbproject/project.xml

途中でコミットメッセージを入力するように促されますが、適当に入力して進めればOKでしょう。

最後にpushすれば、リモートリポジトリにアップロードが完了します。

$ git push
Enter passphrase for key '/c/Users/(userdir)/.ssh/id_rsa_github':
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 12 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (2/2), 370 bytes | 370.00 KiB/s, done.
Total 2 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To github.com:EH500-Kintarou/PIC16F_MMC.git
   9c6d364..5cd5929  main -> main

これでmasterブランチはいらない子なので削除してしまいましょう。

$ git branch -d master
Deleted branch master (was 4e5adfe).

リモートのmasterブランチもいらない子なので、Githubから削除しておきましょう。

4. PICプログラミング

これでやっと準備は整いました。普通にプログラミングして、commitして、pushしましょう。

ところがどっこい、MPLAB Xからのpushは未だにできません。「Incorrect credentials for repository at ~~~」というメッセージが引き続き出ます。普通にBashからpushできるのにどういうことやねん…。

ということで、この先もMPLAB Xは積極的に使わずに、SourceTreeなりTortoiseGitなりのクライアントを使って履歴管理していくのが良さそうですね。そもそもプロジェクトを右クリックしないとコミットやプッシュできないというのも使いにくいですし、.gitignoreが自動生成されないというのもかなりイケてないです。

今後のMPLAB Xの進化に期待ですね。