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の進化に期待ですね。 

2025年5月6日火曜日

UNIX時間と日付時刻を相互変換する

UNIX時間という、1970年1月1日からの経過秒数を使って日付時刻を表示する手法があります。ただ、この大きな数字だけを見てもこれが一体何年何月何日なのかを知ることはできません。困りましたね。

世の中のモダンなフレームワークや処理系には、このUNIX時間とカレンダー形式を変換する機能を備えています。例えば.NET FrameworkではDateTimeOffset.FromUnixTimeSecondsメソッドで簡単に変換できます。C言語には仕様上そういうものはないですが、そもそもUNIX時間とはUNIX用のC言語処理系でtime.hで定義されているtime_t型がそのように時間を表現していたことに由来します。ですので、UNIX含め、他の処理系でもtime_t型の中身がUNIX時間として使える処理系も結構あったりします。

一方で、そのtime_tをUNIX時間として扱っている処理系の中には2038年問題を抱えているものもあります。 time_tを符号付32bit整数として定義してしまっているがために、2038年にオーバーフローしてしまうというものです。私が子供のころにはまだまだ先だなと思っていたものですが、気づいたらもう13年を切ってしまいましたね。時の流れは早いものです。

この2038年問題の回避をするために手っ取り早い方法はtime_tを符号付64bit整数型を使うことです。ただ、処理系として対応していない場合はオレオレ実装をしなければならず、結構厄介です。特に、1970年1月1日からの日数をカレンダーに変換しようとしたときにはうるう年という強敵が立ちはだかります。面倒ですね。

というわけで、C#でオレオレ実装を作ってみました。実際はマイコンで使いたかったので、型名や参照ポインタなどの簡単な置換でC言語でも動かせることを意識して実装しています。

カレンダー構造体の実装

まずは、カレンダー構造体ことstruct tmのオレオレ実装をしておきます。64bitを想定してtm64としました。

public struct tm64
{
    public byte tm_sec;      // 秒
    public byte tm_min;      // 分
    public byte tm_hour;     // 時
    public byte tm_mday;     // 日
    public byte tm_mon;      // 月
    public ushort tm_year;   // 年
    public byte tm_wday;     // 曜日 [0:日 1:月 ... 6:土]
}

tm_yday(年始からの経過日数)はめんどいので実装を省いています。また、tm_yearはC言語では1900年からの経過年数となっていますが、今回はわかりやすく西暦そのままで表現することとします。tm_monは、C言語では0~11で表現されます。なぜ現実世界の暦が0月0日からスタートではないのかという話は置いておいて、これもバグの温床であることは歴史が物語っているので、今回は1~12での表現としておきます。

ちなみにtm_yearをushort型とした場合は西暦約6万5千年までしか表現できないから64bitを生かしきれないのでは?と思うかもしれません。実際、time_tを64bitにすることで原理的には西暦3000億年くらいまで正常に時間をカウントできるようになります。それより小さい型を使うなんてナンセンスだ!と思うかもしれませんが、そもそも西暦6万5千年まで僕の作ったソフトを使う人なんていないので考えないことにしておきます(そうやって2038年問題が再生産されるのであった…)。

UNIX時間→カレンダーへの変換

さて、まずはUNIX時間からカレンダーへの変換を実装していきます。

public static void Clock_ToTm(ulong time, ref tm64 tm)
{
    Clock_ToTm_Date((uint)(time / 86400), ref tm);
    Clock_ToTm_Time((uint)(time % 86400), ref tm);
}

private static void Clock_ToTm_Date(uint dayfrom1970, ref tm64 tm)
{
    tm.tm_wday = (byte)((dayfrom1970 + 4) % 7);   // 1970/1/1は木曜日

    uint dayfrom16000301 = dayfrom1970 + 135080;    // 1600/3/1基準で考える
    ushort year_div400 = (ushort)(dayfrom16000301 / 146097);   // 400年単位が何回あったか
    uint day400y = dayfrom16000301 % 146097;   // 400年単位で3/1からの日数
    if(day400y == 146096) { // 最後の日のうるう日
        tm.tm_year = (ushort)(1600 + (year_div400 + 1) * 400);
        tm.tm_mon = 2;      //2月
        tm.tm_mday = 29;    //29日
    } else {
        byte year_div100 = (byte)(day400y / 36524); // 100年単位が何回あったか
        ushort day100y = (ushort)(day400y % 36524); // 100年単位で3/1からの日数

        byte year_div4 = (byte)(day100y / 1461);    // 4年単位が何回あったか
        ushort day4y = (ushort)(day100y % 1461);    // 4年単位で3/1からの日数

        if(day4y == 1460) { // 最後のうるう日
            tm.tm_year = (ushort)(1600 + year_div400 * 400 + year_div100 * 100 + (year_div4 + 1) * 4);
            tm.tm_mon = 2;      //2月
            tm.tm_mday = 29;    //29日
        } else {
            byte year_div1 = (byte)(day4y / 365);    // 1年単位が何回あったか
            ushort day1y = (ushort)(day4y % 365);    // 1年単位で3/1からの日数

            tm.tm_year = (ushort)(1600 + year_div400 * 400 + year_div100 * 100 + year_div4 * 4 + year_div1);

            if(day1y < 184) {
                if(day1y < 92) {
                    if(day1y < 31) {
                        tm.tm_mon = 3;  // 3月
                        tm.tm_mday = (byte)(day1y + 1);
                    } else if(day1y < 61) {
                        tm.tm_mon = 4;  // 4月
                        tm.tm_mday = (byte)(day1y - 30);
                    } else {
                        tm.tm_mon = 5;  // 5月
                        tm.tm_mday = (byte)(day1y - 60);
                    }
                } else {
                    if(day1y < 122) {
                        tm.tm_mon = 6;  // 6月
                        tm.tm_mday = (byte)(day1y - 91);
                    } else if(day1y < 153) {
                        tm.tm_mon = 7;  // 7月
                        tm.tm_mday = (byte)(day1y - 121);
                    } else {
                        tm.tm_mon = 8;  // 8月
                        tm.tm_mday = (byte)(day1y - 152);
                    }
                }
            } else {
                if(day1y < 275) {
                    if(day1y < 214) {
                        tm.tm_mon = 9;  // 9月
                        tm.tm_mday = (byte)(day1y - 183);
                    } else if(day1y < 245) {
                        tm.tm_mon = 10;  // 10月
                        tm.tm_mday = (byte)(day1y - 213);
                    } else {
                        tm.tm_mon = 11;  // 11月
                        tm.tm_mday = (byte)(day1y - 244);
                    }
                } else {
                    if(day1y < 306) {
                        tm.tm_mon = 12;  // 12月
                        tm.tm_mday = (byte)(day1y - 274);
                    } else if(day1y < 337) {
                        tm.tm_year++;
                        tm.tm_mon = 1;  // 1月
                        tm.tm_mday = (byte)(day1y - 305);
                    } else {
                        tm.tm_year++;
                        tm.tm_mon = 2;  // 2月
                        tm.tm_mday = (byte)(day1y - 336);
                    }
                }
            }
        }
    }
}

private static void Clock_ToTm_Time(uint daysec, ref tm64 tm)
{
    ushort daymin = (ushort)(daysec / 60);
    tm.tm_sec = (byte)(daysec % 60);
    tm.tm_min = (byte)(daymin % 60);
    tm.tm_hour = (byte)(daymin / 60);
}

UNIX時間を86400で割った余り、すなわち時分秒部分は超シンプルなので説明は割愛します。残りの1970年1月1日からの日数をカレンダーに変換する方法についてしっかり説明していきます。

まず、うるう年というのは4年に1回(西暦が4で割り切れる年)にやってきますが、西暦が100で割り切れる年はうるう年になりません。ただし、西暦が400で割り切れる年はうるう年となります。なぜこんなに面倒くさいルールなのかと言えば、地球が太陽の周りを公転するのにかかる時間が365.24219日だからです。うるう年を400年で97回にすることで、この端数に近似しようとしているんですね。

うるう日は2月29日に挿入されるので、400年ごとということも踏まえて、1600年3月1日からの日数を数えることにしましょう。400年は146,097日ですので、その日数を146,097で割って余りが146,096の場合は400年に一度の2月29日です。146,096未満の場合はそうではないので、今度はそれを100年の日数36,524で割ってみましょう。その余りが100年未満部分の日数なので、それを4年の日数1,461日で割ると、余りが1,460の場合は4年に一度の2月29日です。そうでなければうるう日ではありません。

このように、うるう日になりうる日を最終日に置くことで、剰余算でうるう日かどうかの判定を簡単にできるようになります。最後は365日で割った余りを得ることによって、3月1日起算で1年のうちの何日目かを求めることができます。これならば、あとは力業で何月かを振り分ければ終わりです。

振り分ける際は順に比較しても良いですが、こういうのは二分探索していったほうが早いので、まずは184日未満か(=3~8月か)を判断して、その中でさらに92日未満か(=3~5月か)を判断してという形で絞り込んでいっています。

こうすることで、複雑なうるう年判定をできるだけ構造化して実装することができました。

カレンダー→UNIX時間への変換

続いてカレンダーからUNIX時間への変換を実装していきます。

public static ulong Clock_FromTm(ref tm64 tm)
{
    if(tm.tm_year < 1970)
        return 0;

    uint days = Clock_FromTm_Date(ref tm);
    uint time =  Clock_FromTm_Time(ref tm);

    return (ulong)days * 86400 + time;
}

private static uint Clock_FromTm_Date(ref tm64 tm)
{
    ushort year = tm.tm_year;
    byte month = tm.tm_mon;
    while(month < 1) {
        month += 12;
        year--;
    }
    while(month > 12) {
        month -= 12;
        year++;
    }

    ushort yearfrom16000301 = (ushort)(year - ((month >= 3) ? 1600 : 1601));

    byte year_div400 = (byte)(yearfrom16000301 / 400);
    ushort year400y = (ushort)(yearfrom16000301 % 400);

    byte year_div100 = (byte)(year400y / 100);
    byte year100y = (byte)(year400y % 100);

    byte year_div4 = (byte)(year100y / 4);
    byte year4y = (byte)(year100y % 4);

    uint day = (uint)((uint)year_div400 * 146097 + year_div100 * 36524 + year_div4 * 1461 + year4y * 365);      // 1600年3月1日からの日数を計算

    if(month <= 6) {
        if(month <= 3) {
            if(month == 1)
                day += (uint)tm.tm_mday + 305;
            else if(month == 2)
                day += (uint)tm.tm_mday + 336;
            else
                day += (uint)tm.tm_mday - 1;
        } else {
            if(month == 4)
                day += (uint)tm.tm_mday + 30;
            else if(month == 5)
                day += (uint)tm.tm_mday + 60;
            else
                day += (uint)tm.tm_mday + 91;
        }
    } else {
        if(month <= 9) {
            if(month == 7)
                day += (uint)tm.tm_mday + 121;
            else if(month == 8)
                day += (uint)tm.tm_mday + 152;
            else
                day += (uint)tm.tm_mday + 183;
        } else {
            if(month == 10)
                day += (uint)tm.tm_mday + 213;
            else if(month == 11)
                day += (uint)tm.tm_mday + 244;
            else
                day += (uint)tm.tm_mday + 274;
        }
    }

    return day - 135080;    // 1970年1月1日からの日数になるように差し引く
}

private static uint Clock_FromTm_Time(ref tm64 tm)
{
    return ((uint)tm.tm_hour * 60 + tm.tm_min) * 60 + tm.tm_sec;
}

冒頭でwhileループを2つ回しているのは、月が1~12の範囲外だった時に適切に年にオーバーフローできるようにするためです。

以降の考え方は同じで、1600年3月1日からの経過年数をまず求め、うるう年の条件に合わせて振り分けてその年数に対応する日数を計算していきます。あとは、現在が何月何日かという情報から、3月1日から経過している日数を足すことで1600年3月1日からの経過日数を正確に求めていきます。そして最後に135,080日を引くことで1970年1月1日からの日数に換算しています。

まとめ

前述の通り、本当にこれで64bitをフルに使えるようになっているのかと言えば「いいえ」ですが、少なくとも当面は困らない実装ができました。

今回はわかりやすさ(=実装のしやすさ)を優先してシンプルな実装にしましたが、 例えばここ数十年の間では2000年~2099年の日付が流れてくるだろうと想定して、この範囲だったらこの範囲で最も早く計算できるような仕掛けを作るなどしても良かったかもしれません。4の割り算をするだけなら、組み込みみたいなパワーの小さいプロセッサでもシフト演算で簡単に実装できますしね。

2025年3月8日土曜日

Twitter(現X)にAPI経由でつぶやく2025年版

Twitter(現X)のAPIが大きく制限されてから久しいですが、久しぶりに最新のAPIを触りましたので記録しておきます。

まず、Twitter APIの認証にはOAuth 1.0aとOAuth 2.0の2種類がサポートされていますが、ユースケースによってそれらを使い分ける必要があります。2024年10月頃まではOAuth 2.0経由のツイートの投稿ができていたようですが、その後使えなくなったようです。

2025年現在の対応表は以下のようになっています。

今回はツイートを投稿しますので、OAuth 1.0a認証を行います。

APIキーの取得

何はともあれAPIキーを取得しなければ始まりません。Twitter Developer Accountにアクセスして、アプリを登録し、キーを確認します。

ここにある「API Key and Secret」と「Access Token and Secret」の計4種類を控えておきます。 API Key and Secretはアプリ特有のもの、Access Token and Secretは投稿するアカウント特有のものですが、そもそも無料アプリでは1アプリ&1アカウントまでしか使用できないので、これをそのまま使えば良いでしょう。

コード

投稿は以下のコードにて行います。

public class TwitterClient(HttpClient httpClient, string consumerKey, string consumerSecret, string accessToken, string tokenSecret)
{
    public async Task<HttpResponseMessage> PostTweetAsync(string text)
    {
        var timstamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
        var nonce = CreateNonce();
        var body = JsonSerializer.Serialize(new Dictionary<string, string>() { { "text", text } });
        var uri = new Uri("https://api.twitter.com/2/tweets");

        var request = new HttpRequestMessage {
            RequestUri = uri,
            Method = HttpMethod.Post,
            Content = new StringContent(body, Encoding.ASCII, "application/json")
        };

        var signatureBase64 = CreateSignature(uri.ToString(), "POST", nonce, timstamp);

        request.Headers.Authorization =
            new System.Net.Http.Headers.AuthenticationHeaderValue("OAuth",
                $@"oauth_consumer_key=""{Uri.EscapeDataString(consumerKey)}""," +
                $@"oauth_token=""{Uri.EscapeDataString(accessToken)}""," +
                $@"oauth_signature_method=""HMAC-SHA1""," +
                $@"oauth_timestamp=""{Uri.EscapeDataString(timstamp)}""," +
                $@"oauth_nonce=""{Uri.EscapeDataString(nonce)}""," +
                $@"oauth_version=""1.0""," +
                $@"oauth_signature=""{Uri.EscapeDataString(signatureBase64)}""");

        return await httpClient.SendAsync(request);
    }

    private string CreateSignature(string url, string method, string nonce, string timestamp)
    {
        var query = HttpUtility.ParseQueryString("");
        query.Add("oauth_consumer_key", consumerKey);
        query.Add("oauth_nonce", nonce);
        query.Add("oauth_signature_method", "HMAC-SHA1");
        query.Add("oauth_timestamp", timestamp);
        query.Add("oauth_token", accessToken);
        query.Add("oauth_version", "1.0");

        var signatureBaseString = $"{method}&{Uri.EscapeDataString(url)}&{Uri.EscapeDataString(query.ToString() ?? "")}";
        var compositeKey = $"{Uri.EscapeDataString(consumerSecret)}&{Uri.EscapeDataString(tokenSecret)}";

        using var hasher = new HMACSHA1(Encoding.ASCII.GetBytes(compositeKey));
        return Convert.ToBase64String(hasher.ComputeHash(Encoding.ASCII.GetBytes(signatureBaseString)));
    }

    static readonly Random random = new Random();

    private static string CreateNonce()
    {
        var nonce = new StringBuilder(32);

        for(int i = 0; i < 32; i++) {
            var rnd = random.Next(62);
            if(rnd < 26)
                nonce.Append((char)('A' + rnd));
            else if(rnd < 52)
                nonce.Append((char)('a' + rnd - 26));
            else
                nonce.Append((char)('0' + rnd - 52));
        }

        return nonce.ToString();
    }    
}

コンストラクタに引き渡すconsumerKey, consumerSecret, accessToken, tokenSecretは先ほど控えた4つのキーです。そのうえでPostTweetAsyncメソッドに投稿内容を投げてあげればつぶやけます。意外と簡単ですね。Twitter Developer Accountのサイトからキーを全て入手できるので、認証の面倒な手続きを実装する必要もありません。

ちなみに、 無料アカウントでは24時間あたり17件までしかツイートできません。実装したアプリをいろいろ試しているうちにあっという間に返ってくるHTTPステータスコードが429 Too Many Requestsになってしまいます。そうなったら諦めて24時間待ちましょう。HTTP Responseの詳細の中に"x-user-limit-24hour-reset"というものがあり、これをUnix timeとして読むと制限が解除される日時がわかります。

まあこんなんじゃ無料でAPIをいじってみようかとか、ライブラリ作って公開しようかとかいう人が全然現れなくなるのもやむを得ないですよね。まあ、APIを叩いてほしくないからこういうふうにしているのでしょうが。