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

2022年3月14日月曜日

e-Paper(電子ペーパー)を制御する

先日日本橋の電気街をぶらぶらしていたところ、組み込み用の電子ペーパーを見かけました。

電子ペーパーと言えばKindleなどに採用されているディスプレイで、電源を切っても表示内容を維持することができるタイプの画面です。そのため、原理的に画面更新時にしか電力を消費せず、電子書籍のように1分に1回とかの更新で良いような用途では有用なディスプレイですね。

しかし、そのお店で売っていた電子ペーパーは1万円台前半と非常に高く、「ちょっと買ってみるか」くらいでは手が出るものではありませんでした。ですが、ちょっと調べてみると、秋月電子で2,500円の電子ペーパーが売っているじゃありませんか。普通の液晶に比べればちょっと高めですが、まあ、買ってやれないものでもないでしょう。

ということで、実際に買って制御してみました。まずは完成形から見ていきましょう。

見ての通り、画面の更新に約30秒かかります。これは仕様のようです。

SPIでコントローラーのSRAMのバッファーを更新するのはほとんど一瞬で終わりますが、その後の画面リフレッシュに30秒くらいかかるようです。 時計などの用途を考えるとあんまりイケてなさそうですね。

回路設計

回路自体はそんなに難しくないのですが、ドキュメントがイケてないのでめちゃめちゃわかりにくいです。

File:2.13inch e-Paper HAT Schematic.pdf

こちらが回路図になります。ただとても読みにくい。何がどこにつながっているのかかなり難解ですし、挙句の果てには「P1」「J2」「U1」などの部品番号も基板上にプリントされていないという有様です。

解説すると、液晶コントローラー自体は3.3Vで動作するようで、 5V系のマイコンでも操作できるようにレギュレーターとレベルコンバーターが付いているようです。そのレベルコンバーターがU1、レギュレーターがU2です。P3(基板に横向きに出ている白色コネクタ)からコントロールする場合は5Vで制御できます。

一方で、P1はピンソケットで、Raspberry Pi Zeroに直接接続することを意図しているようです(というか液晶サイズもRaspberry Pi Zeroピッタリです)。ですので、ピン配列はRaspberry Pi Zeroのものを参照するとわかりやすいでしょう。ここのコネクタから3.3V系の回路に直結できますので、3.3Vで動作するマイコンに接続する場合はP3を使用する必要はなく、P1から直接接続することができます。

今回はPIC16F18326を使うことにしました。理由は、170円と安価な割にプログラムメモリが16kワードと大きいからです。今回の液晶は104×212ドットの2色表示のため、画像1枚で5.5kBほど使います。

回路図はこんな感じになります。一応e-paper側でDeep Sleepに入ると消費電力が数μAまで下がるようですが、完全にシャットアウトできるようにPICでe-paperの電源を掌握できるようにしています。

ソフト作成

ドライバソフトはメーカーのGithubに載っています。

e-Paper

ただし、PIC用は無いため、Arduino用のを移植する必要があります。今回使うのは3色タイプのLCDなので、e-Paper/Arduino/epd2in13bc/ 以下のファイルを移植していきます。

#include "epd2in13bc.h"


int EpdIf_Init(void) {
    /*
    SPI.begin();
    SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0));
    */
    
    return 0;
}

void EpdIf_SpiTransfer(unsigned char data) {
    DO_SCL = 0;
    DO_CS = 0;

    for(int i = 0; i < 8; i ++) {
        DO_SDA = (data & 0x80) ? 1 : 0;
        data <<= 1;
        DO_SCL = 1;
        NOP();
        NOP();
        DO_SCL = 0;
    }

    DO_CS = 1;
}



int Epd_Init(void) {
    /* this calls the peripheral hardware interface, see epdif */
    if(EpdIf_Init() != 0) {
        return -1;
    }
    /* EPD hardware init start */
    Epd_Reset();
    Epd_SendCommand(BOOSTER_SOFT_START);
    Epd_SendData(0x17);
    Epd_SendData(0x17);
    Epd_SendData(0x17);
    Epd_SendCommand(POWER_ON);
    Epd_WaitUntilIdle();
    Epd_SendCommand(PANEL_SETTING);
    Epd_SendData(0x8F);
    Epd_SendCommand(VCOM_AND_DATA_INTERVAL_SETTING);
    Epd_SendData(0x37);
    Epd_SendCommand(RESOLUTION_SETTING);
    Epd_SendData(EPD_WIDTH);     // width: 104
    Epd_SendData(0x00);
    Epd_SendData(EPD_HEIGHT);     // height: 212
    /* EPD hardware init end */
    return 0;

}

/**
 *  @brief: basic function for sending commands
 */
void Epd_SendCommand(unsigned char command) {
    DO_DC = 0;
    EpdIf_SpiTransfer(command);
}

/**
 *  @brief: basic function for sending data
 */
void Epd_SendData(unsigned char data) {
    DO_DC = 1;
    EpdIf_SpiTransfer(data);
}

/**
 *  @brief: Wait until the busy_pin goes HIGH
 */
void Epd_WaitUntilIdle(void) {
    while(DI_BUSY == 0) {      //0: busy, 1: idle
        __delay_ms(100);
    }      
}

/**
 *  @brief: module reset. 
 *          often used to awaken the module in deep sleep, 
 *          see Epd::Sleep();
 */
void Epd_Reset(void) {
    DO_RST = 0;
    __delay_ms(200);
    DO_RST = 1;
    __delay_ms(200);   
}

/**
 *  @brief: transmit partial data to the SRAM
 */
void Epd_SetPartialWindow(const unsigned char* buffer_black, const unsigned char* buffer_red, int x, int y, int w, int l) {
    Epd_SendCommand(PARTIAL_IN);
    Epd_SendCommand(PARTIAL_WINDOW);
    Epd_SendData(x & 0xf8);     // x should be the multiple of 8, the last 3 bit will always be ignored
    Epd_SendData((unsigned char)(((x & 0xf8) + w  - 1) | 0x07));
    Epd_SendData((unsigned char)(y >> 8));        
    Epd_SendData(y & 0xff);
    Epd_SendData((unsigned char)((y + l - 1) >> 8));        
    Epd_SendData((y + l - 1) & 0xff);
    Epd_SendData(0x01);         // Gates scan both inside and outside of the partial window. (default) 
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_1);
    if (buffer_black != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_black[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_2);
    if (buffer_red != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_red[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(PARTIAL_OUT);  
}

/**
 *  @brief: transmit partial data to the black part of SRAM
 */
void Epd_SetPartialWindowBlack(const unsigned char* buffer_black, int x, int y, int w, int l) {
    Epd_SendCommand(PARTIAL_IN);
    Epd_SendCommand(PARTIAL_WINDOW);
    Epd_SendData(x & 0xf8);     // x should be the multiple of 8, the last 3 bit will always be ignored
    Epd_SendData((unsigned char)(((x & 0xf8) + w  - 1) | 0x07));
    Epd_SendData((unsigned char)(y >> 8));        
    Epd_SendData(y & 0xff);
    Epd_SendData((unsigned char)((y + l - 1) >> 8));        
    Epd_SendData((y + l - 1) & 0xff);
    Epd_SendData(0x01);         // Gates scan both inside and outside of the partial window. (default) 
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_1);
    if (buffer_black != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_black[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(PARTIAL_OUT);  
}

/**
 *  @brief: transmit partial data to the red part of SRAM
 */
void Epd_SetPartialWindowRed(const unsigned char* buffer_red, int x, int y, int w, int l) {
    Epd_SendCommand(PARTIAL_IN);
    Epd_SendCommand(PARTIAL_WINDOW);
    Epd_SendData(x & 0xf8);     // x should be the multiple of 8, the last 3 bit will always be ignored
    Epd_SendData((unsigned char)(((x & 0xf8) + w  - 1) | 0x07));
    Epd_SendData((unsigned char)(y >> 8));
    Epd_SendData(y & 0xff);
    Epd_SendData((unsigned char)((y + l - 1) >> 8));
    Epd_SendData((y + l - 1) & 0xff);
    Epd_SendData(0x01);         // Gates scan both inside and outside of the partial window. (default) 
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_2);
    if (buffer_red != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_red[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(PARTIAL_OUT);  
}

/**
 * @brief: refresh and displays the frame
 */
void Epd_DisplayFrame(const unsigned char* frame_buffer_black, const unsigned char* frame_buffer_red) {
    if (frame_buffer_black != NULL) {
        Epd_SendCommand(DATA_START_TRANSMISSION_1);
        __delay_ms(2);
        for (int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
            Epd_SendData(frame_buffer_black[i]);
            //Epd_SendData(~(i & 0xFF));
        }
        __delay_ms(2);
    }
    if (frame_buffer_red != NULL) {
        Epd_SendCommand(DATA_START_TRANSMISSION_2);
        __delay_ms(2);
        for (int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
            Epd_SendData(frame_buffer_red[i]);
            //Epd_SendData(0xFF);
        }
        __delay_ms(2);
    }
    Epd_SendCommand(DISPLAY_REFRESH);
    Epd_WaitUntilIdle();
}

/**
 * @brief: clear the frame data
 */
void Epd_ClearFrame(void) {
    Epd_SendCommand(DATA_START_TRANSMISSION_1);           
    __delay_ms(2);
    for(int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
        Epd_SendData(0xFF);  
    }  
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_2);           
    __delay_ms(2);
    for(int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
        Epd_SendData(0xFF);  
    }  
    __delay_ms(2);

    Epd_SendCommand(DISPLAY_REFRESH);
    Epd_WaitUntilIdle();
}

/**
 * @brief: This displays the frame data from SRAM
 */
void Epd_Refresh(void) {
    Epd_SendCommand(DISPLAY_REFRESH); 
    Epd_WaitUntilIdle();
}


/**
 * @brief: After this command is transmitted, the chip would enter the deep-sleep mode to save power. 
 *         The deep sleep mode would return to standby by hardware reset. The only one parameter is a 
 *         check code, the command would be executed if check code = 0xA5. 
 *         You can use Epd::Reset() to awaken and use Epd::Init() to initialize.
 */
void Epd_Sleep() {
    Epd_SendCommand(POWER_OFF);
    Epd_WaitUntilIdle();
    Epd_SendCommand(DEEP_SLEEP);
    Epd_SendData(0xA5);     // check code
}
#ifndef EPD2IN13B_H
#define EPD2IN13B_H

#include <xc.h>
#define _XTAL_FREQ 32000000    // Fosc = 8MHz


#define DO_SDA          LATCbits.LATC2
#define DO_SCL          LATCbits.LATC0
#define DO_CS           LATCbits.LATC4
#define DO_DC           LATCbits.LATC5
#define DO_RST          LATCbits.LATC3
#define DI_BUSY         PORTAbits.RA4


// Display resolution
#define EPD_WIDTH       104
#define EPD_HEIGHT      212

// EPD2IN13B commands
#define PANEL_SETTING                               0x00
#define POWER_SETTING                               0x01
#define POWER_OFF                                   0x02
#define POWER_OFF_SEQUENCE_SETTING                  0x03
#define POWER_ON                                    0x04
#define POWER_ON_MEASURE                            0x05
#define BOOSTER_SOFT_START                          0x06
#define DEEP_SLEEP                                  0x07
#define DATA_START_TRANSMISSION_1                   0x10
#define DATA_STOP                                   0x11
#define DISPLAY_REFRESH                             0x12
#define DATA_START_TRANSMISSION_2                   0x13
#define VCOM_LUT                                    0x20
#define W2W_LUT                                     0x21
#define B2W_LUT                                     0x22
#define W2B_LUT                                     0x23
#define B2B_LUT                                     0x24
#define PLL_CONTROL                                 0x30
#define TEMPERATURE_SENSOR_CALIBRATION              0x40
#define TEMPERATURE_SENSOR_SELECTION                0x41
#define TEMPERATURE_SENSOR_WRITE                    0x42
#define TEMPERATURE_SENSOR_READ                     0x43
#define VCOM_AND_DATA_INTERVAL_SETTING              0x50
#define LOW_POWER_DETECTION                         0x51
#define TCON_SETTING                                0x60
#define RESOLUTION_SETTING                          0x61
#define GET_STATUS                                  0x71
#define AUTO_MEASURE_VCOM                           0x80
#define READ_VCOM_VALUE                             0x81
#define VCM_DC_SETTING                              0x82
#define PARTIAL_WINDOW                              0x90
#define PARTIAL_IN                                  0x91
#define PARTIAL_OUT                                 0x92
#define PROGRAM_MODE                                0xA0
#define ACTIVE_PROGRAM                              0xA1
#define READ_OTP_DATA                               0xA2
#define POWER_SAVING                                0xE3

int  Epd_Init(void);
void Epd_SendCommand(unsigned char command);
void Epd_SendData(unsigned char data);
void Epd_WaitUntilIdle(void);
void Epd_Reset(void);
void Epd_SetPartialWindow(const unsigned char* buffer_black, const unsigned char* buffer_red, int x, int y, int w, int l);
void Epd_SetPartialWindowBlack(const unsigned char* buffer_black, int x, int y, int w, int l);
void Epd_SetPartialWindowRed(const unsigned char* buffer_red, int x, int y, int w, int l);
void Epd_DisplayFrame(const unsigned char* frame_buffer_black, const unsigned char* frame_buffer_red);
void Epd_Refresh(void);
void Epd_ClearFrame(void);
void Epd_Sleep(void);

#endif

オリジナルのライブラリでは、ハードウェアのインターフェース部と論理部できれいに分けて実装されていたのですが、移植するにあたってごちゃ混ぜにしてしまいました。XC8がC++をサポートしていないうえ、PICのピン操作は直接レジスタ触ることになるというのが主な理由です。もう少しやりようはあったかもしれませんが、まあ処理速度優先ということで。

あと、一応回路上ではPICのMSSP(ハードウェアSPI)に対応したピンにアサインしているのですが、SPI送信中に何か別のことをしたいわけでもないので、ソフトウェアで簡単にSPIを実装しています。SPI程度ならMSSPの使い方をデータシート見ながら実装するよりGPIOで作っちゃったほうが簡単ですね。

void UpdateLcd()
{
    DO_LCD_PWR = 1;
    TRIS_LCD_PWR = 0;
    
    __delay_ms(100);
    
    Epd_Init();

    switch(content++) {
        case 0:
            Epd_DisplayFrame(img1_black, img1_yellow);
            break;
        case 1:
            Epd_DisplayFrame(img2_black, img2_yellow);
            break;
        default:
            Epd_ClearFrame();
            content = 0;
            break;
    }

    Epd_Sleep();    
    TRIS_LCD_PWR = 1;
    LATA = 0;
    LATC = 0;
}

void init()
{
    LATA = 0;
    WPUA = 0x20;
    ODCONA = 0;
    ANSELA = 0;
    TRISA = 0xFF;

    LATC = 0;
    WPUC = 0;
    ODCONC = 0;
    ANSELC = 0;
    TRISC = 0xC2;

    IOCAN = 0x20;
    PIE0 = 0x10;
    INTCON = 0x80;
}

void loop()
{
    SLEEP();
    NOP();
    
    if(DI_SW == 0)
        UpdateLcd();    
}

void __interrupt() ISR(void)
{
    if(PIR0bits.IOCIF) {
        if(IOCAFbits.IOCAF5) {
            IOCAFbits.IOCAF5 = 0;
        }
    }
}

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

main.cでは処理をしていない間はPICをスリープにするようにしています。スイッチを押されるとInterrupt On Change (IOC)にてスリープからウェイクアップすると続きの処理を実行するようになります。この際発生する割り込みの割り込みフラグはちゃんとリセットしないとスリープに入れない(常にスリープ復帰条件ができてしまう)ので、割り込みルーチンではフラグのリセットのみを行っています。

また、e-Paperをdeep sleepにした後はLCDの電源を切っていますが、その際に制御用ピンがHighになっていると、そちらから電源側へ電流が流れてしまう(おそらく液晶コントローラー内に保護用にダイオードが入っている)ので、同時にすべてのピンの出力をゼロにしています。

これで、待機中はナノアンペアオーダーの電流しか流れない、ボタン電池で駆動しても何の支障もないe-Paperができました。

 

それにしても、更新に30秒かかるのはちょっと残念ですね…。せめて数秒程度にしてほしかった…。

2020年5月1日金曜日

MPLAB X 5.35 + XC8 2.20でオートコンプリートが正常に動作しない問題を対処する

最近、久しぶりにPICマイコンでのプログラミングをしました。
久しぶりにするということで、ひとまず最新の環境に揃えた上でプログラミングを始めたところ、オートコンプリート機能が全く動かないというトラブルに見舞われました。
こんな感じです。ありとあらゆるレジスタの名前が未解決になってしまうため、IDEというよりかただのテキストエディタになってしまいます。この状態でプログラミングするだけでもストレスフルです。
なお、IDEのオートコンプリート機能がトラブっているだけで、コンパイル自体は正常に通ります。

環境

  • MPLAB X 5.35
  • XC8 2.20
  • PIC16F1579(確認はしていないがおそらく他の8bitファミリでも同じと思われる)

原因

結論から言うと、原因はMPLAB XがPIC16F1579.hを見失っていたからでした。そのため、各SFRの名前やアドレスが分からなくなっていたのです。

黄色い波線が引いてあるxc.hにCtrlキーを押しながらマウスオーバーするとこんなポップアップが出てきました。
xc.h内ではどうもプロジェクトの設定を元にデバイスのヘッダファイルを読み込む条件分岐があるようで、その中で「pic16f1579.h」見つけられずにレジスタ名をすべて解決できなくなっていたようです。
すでに上の画像にも書いていますが、試しに#include <pic16f1579.h>を追加してみたところ、以下のようなエラーが出てきました。

この中で赤文字の"C:\Program Files\Microchip\xc8\v2.20\pic\include\plib"を参照してみたところ、このようなフォルダはありませんでした。おそらく過去はこのようなパスにそれぞれのPICのヘッダファイルを入れていたのでしょうが、最近パスが変更された一方でMPLABのほうではその変更が漏れておりヘッダファイルを見失ってしまったのでしょう。
現在のフォルダは"C:\Program Files\Microchip\xc8\v2.20\pic\include\proc"でした。

対策

system include pathsが間違っているので、これを修正してやれば直るはずです。

…と思ったのですが、system include pathsの編集のしかたが分かりませんでした。設定にもそのような項目は見当たらず、かと言ってMPLAB Xの膨大な構成ファイルからそれが記録されている設定ファイルを探す気にもなれず、結局お手上げでした(もし知っている方がいたら教えてください)。

system include pathsの編集がダメだったので、付け焼き刃ではありますが、プロジェクト単位で"C:\Program Files\Microchip\xc8\v2.20\pic\include\proc"のパスを通してあげればpic16f1579.hが見えるようになりました。
プロジェクトのプロパティ画面のツリーの「XC8 Compiler」を選択し、「Include directories」に先ほどのパスを追加してあげます。
見事にSFRの下の波線もなくなり、オートコンプリートが正常に動作するようになりました。めでたしめでたし。


にしてもあれですね、MPLAB Xって必ず「こんなん絶対誰か気づくやろ」みたいなお粗末なバグが入ってますよね。訓練されたユーザーじゃないと使いこなせないソフトです…。

2018年1月24日水曜日

MPLAB Xでアセンブリリストを見る

私が初めてPICを触ったころ、それは10年以上前になりますが、その時はまだCコンパイラは数百ドルする有料版しかありませんでした。当時中学生だった私は、当然そのような高価なものは買えるわけもなく、一生懸命PICのアセンブリを覚えてプログラムを書いていました。

時代は変わり、PIC以外にもたくさんのマイコンが趣味レベルで使われるようになり、開発環境もC言語が主流になりました。PICも一足遅れて(実用レベルの)Cコンパイラを無償公開し、今ではMicrochipが買収したHi-Tech社のCコンパイラがXCコンパイラとしてPICの基幹コンパイラになっています。

今ではC言語向けに間接参照などの機能を強化したPICがたくさん出てきており、アセンブリで開発する理由は皆無ですが、それでもときどき「このコードはどのような形でアセンブリに展開されるのだろうか」と気になることがあります。

例えばEEPROMアクセス。
EEPROMは、不意な書き込みを防止するため、アンロックシーケンスと呼ばれる一定の手順で命令を実行しないと書き込みできないようになっています。
        BCF     INTCON, GIE
        MOVLW   55h
        MOVWF   EECON2
        MOVLW   AAh
        MOVWF   EECON2
        BSF     EECON1, WR
        BSF     INTCON, GIE
これが代表的なPIC16シリーズにおけるEEPROM書き込みシーケンスの一部です。EECON2には0x55を書き込んだ2サイクル後に0xAAを書き込み、さらに次のサイクルでEECON1のWRビットをセットしなければいけないため、C言語で書いていてもこの部分だけはインラインアセンブリで書かなければならなくなることがあります。
ですが、C言語で書いても次のように書けば上記のアセンブリに展開されることは容易に想像が付きます。
INTCONbits.GIE = 0;
EECON2 = 0x55;
EECON2 = 0xAA;
EECON1bits.WR = 1;
INTCONbits.GIE = 1;
でもまあ、C言語をどうアセンブリに展開するかはコンパイラの勝手なので、こういう書き方をすれば絶対に展開されるとは限りません。こういう時にどのような形でアセンブリ展開されるか見てみたくなりますよね。

アセンブリリストは…っとその前に、まずはお約束の環境を書いておきます。
  • MPLAB X IDE v4.05 
  • XC8 v1.45

アセンブリリストはメニューの
Window → Debugging → Output → Disassembly Listing File
で開くことができます。
ですがおそらく、最初は次のようなファイルが開かれて、アセンブリリストは出てこないはずです。

悩むほど難しい英語ではないですが、一応拙訳を付けておきます。
アセンブリリストの生成は無効になっています。
下記の情報に従ってプロジェクトをビルドしてください。
  1. プロジェクトのプロパティを開いてください。
  2. 「Conf: [現在の設定]」ノードの下にある「Loading」を選択してください。
  3. 「Load Symbols when Programming or building for production」にチェックを入れてください。
このエディタ上でダブルクリックをすると上記の設定ができます。
最後の1行にしれっと書いてありますが、エディタ上をダブルクリックすればワンタッチでダイアログの当該部分が表示される超便利機能があります。
上のほうにある「Load Symbols when Programming or building for production」にチェックを入れて、OKを押してプロジェクトをリビルドしましょう。
ご覧の通り、ばっちりアセンブリリストが表示されました。

実はXC8には組み込み関数でeeprom_writeとeeprom_readというのがあるので、直接レジスタを叩かなくてもEEPROMの読み書きはできるのですが、その部分のコードも展開されていることが分かります。
PICの違いによるレジスタ名の違いで、EECONxではなくNVMCONxになっていますが、5607行目からアンロックシーケンスが始まっていることが分かります。
書き込みを開始してから完了までの間は割り込みを禁止したままなんですね。CPUは動作しているのでそのようにする理由もあまり無い気もしますが…(割り込み中にEEPROMを読み出すのなら話は別ですが)。

2016年1月22日金曜日

ポケモン「サファイア」の時計を復活させる

さて、巷ではもっぱらリメイク版である「アルファサファイア」「オメガルビー」が話題ですが(とは言っても発売は1年以上前ですが)、ここではリメイクされる前の「サファイア」を対象にしています。
検証はしていませんが、「ルビー」「エメラルド」も同様に復活させられる可能性もありますし、また、サファイアでもロットが違う製品だと違うチップが載っていて同様にいかない可能性もあります。

流石にゲームボーイアドバンスはもうレトロゲーム認定してもいいのではないかと思います。とても懐かしいですね。
久しぶりにサファイアをプレイしたくなったので、起動してみました。


おお、時計が電池切れだと。
電池切れで時計が動かなくなったから時間絡みのイベントは起きなくなるけど、それ以外のイベントは正常に動くからプレイ可能だよとのメッセージが出てきました。
ゲームボーイカラーまではセーブデータがRAM上に保存されていて、バックアップ用電池が切れるとセーブデータも吹き飛んでいましたが、ゲームボーイアドバンスからはセーブデータ自体は不揮発性メモリに書き込まれるようになったようです。しかし、時計管理はあくまでもカートリッジの責務なんですね。 DSからは時計もゲーム機本体のほうの責務になったみたいですが。

まあとりあえず、ふむふむと思いながらプレイを開始してみましたが、案外時間関係のイベントが多いことに気が付きます。
例えば、
  • きのみが育たなくなる
  • 毎日きのみをくれる人がくれなくなる
  • レコードを混ぜると出現する友達の秘密基地で、1日1回のバトルができなくなる
  • ミナモデパートで1日1回のくじが引けなくなる
  • 1週間に1回わざマシンをくれる人がくれなくなる
  • あさせのほらあなであさせのかいがらやあさせのしおが採れなくなる
  • カイナシティのがんばりやさんが二度と頑張らなくなる
  • マボロシじまが出現しなくなる(時計が動いていても出現したの見たことありませんが)
などなど、ゲームの楽しみが一気に消え去ります。こんなポケモンで一体何をしろと言うのだ!ひたすら殿堂入りか?殿堂入りをすればいいのか!?


電池交換は、実はまだゲームボーイアドバンスのソフトならば任天堂がサポートしてくれています(※注)

バックアップ電池の交換について

ちゃんと正常にゲームをプレイしたい人はまずこっちに頼んだほうが手っ取り早く確実で安いです。ですが、こういうのを分解して自分で修理するのを楽しみたい私は、自分で修理に挑戦してみることにしました。
※注:記事投稿時点。現在はバックアップ電池の交換サービスは終了しているようです。

なお、当たり前ですが、自分で分解、修理をするとその製品について二度と任天堂のサポートを受けられなくなる可能性があります。また、ゲームバランスを大幅に崩すようなゲームの改造も民事訴訟の対象になる可能性が無いことはないようです。もちろん、この記事を見て改造をして失敗した人とかがいたとしても私は責任を負えませんので、自己責任でお願いいたします。
今回の私の記事は、純粋に修理をするためというのと、あとは、まあゲームボーイアドバンスはもう枯れ果てたゲーム市場ですから、メーカーに目くじらを立てられるようなことも無いでしょうということで書くことにしました。


分解、そして電池交換

まずはカートリッジを分解します。分解はY字の特殊ドライバーを使いますが、ゲームボーイカラーまでのカートリッジに比べたら格段と入手性の高い汎用特殊ドライバーで何とかなります。
ねじを外したら、筐体を少しスライドするとロックが外れてパカッと開きます。


こんな感じになります。
ボタン電池が付いていますね。これが切れた電池で、CR1616のタブ付き電池になっています。

まずはタブの根元をはんだごてで温めて古い電池を外します。


CR1616はリチウム電池なので公称3Vですが、もうすでに0.15Vまで下がっていました。完全放電状態です。

ネットをいろいろ探すと、ここまではたどり着いている方も多く、多くの紹介記事では、このタブと電池の接合部を剥がして、新しい電池をテープ等で固定している方が多いように見受けられました。

し かし、それではタブの金属疲労の問題は元より、電池との接触も悪くなることが多いのではないかと思い、私は電池に直接はんだ付けすることにしました。電池は精巧に作られた化学製品で、電池をはんだごてで熱すると中の電解液が気化するなどの化学的変化を起こし、最悪、電池が爆発することがあるので非常に危険です。なので、良い子は真似しないようにしましょう((((((

新しい電池を百円ショップで買ってきて取り付けた画像がこちらです。


マイナス電極側が上になるように取り付けていますが、プラス電極が横に回り込んできているので、接触してショートさせることがないように配線を一部ビニールで覆っています。

これで起動すると、冒頭に出てきていた電池切れのメッセージが出なくなりました。
よしこれで時間関係のイベントが復活だ!よっしゃああああああ!!!!!

まだ時間関係のイベントが発生しない!

起動時の電池切れメッセージこそ出なくなったものの、きのみを植えて1日程度待っても芽が生えてきません。きのみをくれる人もきのみをくれません。おかしいです。電池が復活しても時間関係のイベントが発生しないのです。なぜだ、なぜなのだ!

ヒントは、キナギタウンにいる、1週間に1回わざマシンをくれる人のところにありました。


あと2105日!?およそ5年と9か月です。
正常ならば、ここで「あと6にち」など、1週間以内の日数を案内してくれます。

ここで僕は気が付きました。
ゲームの内部では、次にわざマシンをあげられるようになる日時を記録していたとしたらどうなるでしょう。現在の日時との差分を取って、その日数を「あと○にちたてば~」と答えることになるはずです。
現在の日時はというと、先ほど交換した電池で駆動しているRTCが管理しています。電池を交換したのでRTCのカウントはリセットされ、0日目になったはずです。

そ れ だ

すなわち、ゲームプレイ開始から2098日目に、次にわざマシンがあげられるようになる日を2105日目と記録していたため、電池を交換後でもお構いなしにRTCの日数カウンタがその日になるのを待つ必要が生まれた、ということになります。電池切れなんてそうそう起こることでもなければ、ユーザー自身での修理を想定していたわけでもないので、このような適当な実装になっているのでしょうね。
おそらく、1日に1回起きる系のイベントも同様に記録されていて正常に発生しないということなのでしょう。

このことからも、GBAソフトをメーカーに送って電池交換をしてもらった場合は、電池交換後にRTCのカウンタをずらすか、専用のツールでセーブデータを書き換えてイベントが正常に発生するようにしているものと思われます。
今回は自分で交換したためそのような書き換えができませんので、2105日待つ必要があります。

えっ

ネットで調べたところ、同じような症状になっている人もちらほら見かけました。
そして、ルビー・サファイアではRTCが1年を刻むときのみが育たなくなるバグがあったのですが、その修正プログラムを使えば1度に限ってこの状態を修正できるとの情報がありました。しかし、すでにこのパッチは当てており、リーフグリーン・ファイアレッドからのファームウェア更新を利用して修正することはできませんでした。

つまり、いよいよもって本当に2105日待つ必要があるということになります。

待てるかい!

そして、RTCのハードウェアハックへ

さて、ここまで自分でいろいろやってしまって引くに引けなくなった僕が次に考えたことは、「RTCの値を上書きして2105日分進めればいいんじゃね?」ということでした。
ネットでは電池交換まではしたけど同様に時間関係のイベントが発生しない状態になっている人までは見かけましたが、さすがにRTCをいじろうとしている人までは見かけませんでした。
ですが、カートリッジの基板を見ると、ICチップの脇に丁寧に「1M/512K FLASH U2」とか「U3 RTC」などとICの説明が書いてあります。そうです、どれがRTCかも明確です。もっと言えば、脇に水晶発振子があることからも明白でした。そこで、このRTCの仕様さえわかればRTCの書き換えもできるのではないか、という発想で型番をググってみました。

S-3511A
2018年2月28日追記:リンク切れになっております。通販サイトのタイロテックのこちらからダウンロード可能です。

見事にヒットしました。当時はセイコーインスツルメンツでしたが、今は半導体部門は子会社化されてエスアイアイ・セミコンダクタになっているみたいですね。まあ何にせよSEIKOです。SEIKOが供給していたRTCモジュールが僕のサファイアには搭載されているみたいです。すでにディスコンになっているようですが、データシートは問題なく読めます。よっしゃああああああ!!!!!

まずはプロトコルの確認です。まあRTCだとI2Cかなと思って眺めましたが、ACKとかスレーブアドレスとか無さそうだな。うんうん、SPIかな…いやデータ線が1本しか無いぞこれは…

独自規格だー┗(^o^)┛WwwwWWww┏(^o^)┓ドコドコドコドコwwwwwwwww

なんてこった。独自規格で通信プロトコルを定義しています。
なので、書き換えをする場合はI2CやSPIなどマイコンに載っているハードウェアを使わずに、このプロトコルに則ったIOの制御を自前でしてや らなければなりません。


また、当たり前ですが、データを書き換えるためにはICに何かしら電気的に接続する必要があります。そして、バックアップ用電池で動作している状態(=通電した状態)で書き換えをしたうえで接続を外し、元のカートリッジの形に戻してあげなければなりません。はんだ付けとかでやるのはちょっとリスクが高すぎますよね(実は自分もやってみたりしたのですが、結構スリルがありました)

そこで登場するのがICクリップです。
ICテストクリップ(SOP-8) [TCLIP-SOP8]
これは、基板に実装されたICを挟むことで電気的に接続し、外部に配線を回してくれるツールです。ちょっと値段は高めですが、通電中に書き換えるのならば通電中はんだ付けよりもこういうツールを使ったほうが安心でしょう。


実際に買ってきてRTCを挟んだものがこれです。こんな感じで簡単に接続できます。これはいいです。

マイコンは何でもいいのですが、まあ、安さと小ささ、そしてプログラムメモリの容量からPIC16F1705を選びました。
PIC16F1705
PIC16F1705は特にアナログ機能が強化されたマイコンですが、残念ながら今回はその機能は使いません。

適当に表示用のI2C液晶と操作ボタンを付けた回路を構成し、完成です。


めんどくさいので電源はCR2032を使いました。液晶バックライトを使ったらそんなに持たないでしょうが、まあ長時間運用するものでもないですし大丈夫でしょう。

I2C液晶は適当に制御プログラムを移植してきて動作を確認します。今回aitendoで買ってきた液晶は、液晶の表示向きを反転させるピンが出ていて、それに気づかずにだいぶ苦労しました。適当にそのピンをGNDかVDDに接続することで向きを固定できます。

さて、本題のRTC通信プログラムに入っていきましょう。
タイミングチャートとにらめっこしながらプログラムを書きます。各パルス幅は最低0.1us程度なので、まあゆとりをもって1usくらい取ってあげることにします。

#define sequenceDelay() __delay_us(1);

void rtc_Init()
{
    RTC_CS = 0;
    RTC_SCK = 1;
    RTC_SIO = 0;
    TRIS_RTC_CS = 0;
    TRIS_RTC_SCK = 0;
    TRIS_RTC_SIO = 1;
}

void rtc_WriteBit(bool_t data)
{
    RTC_SCK = 0;
    sequenceDelay();
    RTC_SIO = data ? 1 : 0;  
    sequenceDelay();
    RTC_SCK = 1;
    sequenceDelay();
}

void rtc_WriteByte(uint8_t data)
{
    for(uint8_t i = 0; i < 8; i++) {
        rtc_WriteBit(data & 0x01);  //Send from LSB to MSB
        data >>= 1;
    }
}

void rtc_WriteSequence(uint8_t command, const uint8_t *pData, uint8_t length)
{
    RTC_SCK = 1;
    RTC_SIO = 0;
    
    sequenceDelay();
    RTC_CS = 1;
    sequenceDelay();
    TRIS_RTC_SIO = 0;
    rtc_WriteByte(command);
    for(uint8_t i = 0; i < length; i++)
        rtc_WriteByte(pData[i]);
    RTC_CS = 0;
    sequenceDelay();    
    TRIS_RTC_SIO = 1;
}

bool_t rtc_ReadBit()
{
    bool_t ret;
    
    RTC_SCK = 0;
    sequenceDelay();
    RTC_SCK = 1;
    sequenceDelay();
    ret = PORT_RTC_SIO;
    
    return ret;
}

uint8_t rtc_ReadByte()
{
    uint8_t data = 0;
    for(uint8_t i = 0; i < 8; i++) {
        data >>= 1;
        if(rtc_ReadBit())  //Read from LSB to MSB
            data |= 0x80;
    }
    return data;
}

void rtc_ReadSequence(uint8_t command, uint8_t *pData, uint8_t length)
{    
    RTC_SCK = 1;
    RTC_SIO = 0;
    
    sequenceDelay();
    RTC_CS = 1;
    sequenceDelay();    
    TRIS_RTC_SIO = 0;
    rtc_WriteByte(command);    
    TRIS_RTC_SIO = 1;
    sequenceDelay();
    for(uint8_t i = 0; i < length; i++)
        pData[i] = rtc_ReadByte();
    RTC_CS = 0;
}

ビットを読み書きする関数し、それをバイトデータを読み書きする関数から呼び出します。

書き込みシーケンスと読み込みシーケンスは、まずはそのデータ内容を示すコマンドを送ってからデータの読み書きをしますので、そういったことをやりやすくした関数を用意します。ここでとても重要なのが、データはLSBからMSBに向かって送りますが、コマンドはMSBからLSBに向かって送る点です。なんでこんなクソ仕様にしたのかわかりませんが、コマンドとデータで送る順序が違うので、ここでは、「コマンドの値をデータシートに書いてある順序と逆順にしておく」という方法で対応し、プログラム自体はすべてLSBからMSBに向かって送るようにしました。

実際のコマンドは全部で7種類あり、リセット以外の6種類は読み書き両方ができるため、実質13種類あります。
ですが、RTCを制御するうえでは、年データからの時刻データの読み書きと、ステータスレジスタの読み書きと、リセットコマンドの送信さえできれば問題ないので、その分だけ実装しました。

#define RTC_COMMAND_FULL_READ       0b10100110
#define RTC_COMMAND_FULL_WRITE      0b00100110
#define RTC_COMMAND_STATUS_READ     0b11000110
#define RTC_COMMAND_STATUS_WRITE    0b01000110
#define RTC_COMMAND_RESET           0b10000110

#define RTC_DATETIME_LENGTH 7


typedef union _tagRTC_STATUS {
    uint8_t value;
    struct _tagBits {
        unsigned bit0  : 1;
        unsigned INTFE : 1;
        unsigned bit2  : 1;
        unsigned INTME : 1;
        unsigned bit4  : 1;
        unsigned INTAE : 1;
        unsigned Is24  : 1;
        unsigned POWER : 1;
    } bits;
} RTC_STATUS;



void rtc_WriteDateTime(const DATE_TIME *pdt, bool_t Is24)
{
    uint8_t data[RTC_DATETIME_LENGTH];
    bool_t IsPM = pdt->hour >= 12;
    uint8_t hour = pdt->hour;
    
    if(IsPM && !Is24)
        hour -= 12;    

    data[0] = ByteToBCD(pdt->year);
    data[1] = ByteToBCD(pdt->month);
    data[2] = ByteToBCD(pdt->day);
    data[3] = pdt->weekday;
    data[4] = ByteToBCD(hour) | (IsPM ? 0x80 : 0x00);
    data[5] = ByteToBCD(pdt->min);
    data[6] = ByteToBCD(pdt->sec);
        
    rtc_WriteSequence(RTC_COMMAND_FULL_WRITE, data, RTC_DATETIME_LENGTH);
}

void rtc_ReadDateTime(DATE_TIME *pdt)
{
    uint8_t data[RTC_DATETIME_LENGTH];

    rtc_ReadSequence(RTC_COMMAND_FULL_READ, data, RTC_DATETIME_LENGTH);

    bool_t isPM = data[4] & 0x80;
    
    pdt->year    = BCDToByte(data[0]);
    pdt->month   = BCDToByte(data[1]);
    pdt->day     = BCDToByte(data[2]);
    pdt->weekday = data[3];
    pdt->hour    = BCDToByte(data[4] & 0x3F);
    pdt->min     = BCDToByte(data[5]);
    pdt->sec     = BCDToByte(data[6] & 0x7F);
    pdt->msec    = 0;

    if(isPM && pdt->hour < 12)
        pdt->hour += 12;
}

void rtc_WriteStatus(RTC_STATUS status)
{
    rtc_WriteSequence(RTC_COMMAND_STATUS_WRITE, &status.value, 1);
}

RTC_STATUS rtc_ReadStatus()
{
    RTC_STATUS status;
    rtc_ReadSequence(RTC_COMMAND_STATUS_READ, &status.value, 1);
    return status;
}

void rtc_Reset()
{
    rtc_WriteSequence(RTC_COMMAND_RESET, NULL, 0);
}

こんな感じになりました。

このRTCモジュールは12時間表示モードと24時間表示モードがあり、時刻データの読み書きをするときは現在どっちのモードなのかを意識する必要があります。時刻データには現在が午後か午前かを表すフラグがあるので、データ読み込み後にはそのフラグを見てデータを処理してからBCDをbyteに変換しなければいけませんし、書き込み前にはSTATUSレジスタを読み込んで現在のモードに合わせて12時間表示化してデータを送信する必要があります。

あとは、これをいい感じに制御するインターフェースを作れば完成です。
PICのタイマー2はコンパレーターとセットで動いていて、コンパレーターで指定した値に達したら割り込みを掛けるとともに自動でタイマーをゼロにリセットしてくれます。なので、時計用途でとても使いやすいです(割り込みルーチン内でタイマー値を再設定する必要が無いため)。
時計と時計合わせ機能、そして上記の5つのコマンドの送受信機能を実装すれば、あとはRTCの時刻データを読み込み、修正し、書き込むことができるようになります。これで、ゲームの時間を今回は2105日分+α早送りしてやれば見事ゲームが正常な状態になります。


ちなみにですが、このRTCは電池を交換した後にSTATUSレジスタの読み込みと、適宜リセットコマンドの送信をしてやらなければなりません。ですが、その機能はGBAカートリッジ側でゲーム起動時に行ってくれるようなので、時刻データを読み書きする分には特に気を使う必要はなさそうです。

あと、RTCに設定する時刻は、リアルの時刻ではありません。まず、年月日データが大きな意味を持っていないことは誰にでも想像がつくでしょう(製造時に時計合わせしているだなんて考えにくいですからね)。時刻データは、ゲーム初期化時に聞かれる時刻設定がそのままオフセットになっているのか、私の環境ではRTCが0時00分00秒を示しているときにゲーム内では夕方5時過ぎになっているようでした。なので、その分を意識して時計合わせをしなければなりません。

まあ、なんにせよ、何回か「ゲームを起動して時刻を確認してから終了し、値を更新する」という作業を繰り返さなければならないことは間違いありません。少し面倒ですが、これで完全に時計を合わせられるようになります。


書き換え風景です。
このICクリップですが、やはり足元での接触が悪いことがあり、その場合はRTCのデータが上手く読み書きできなかったり、最悪RTCの中身が吹き飛びます。ですがまあ、根気よくやっていれば大丈夫でしょう…。

時刻データを書き換えるということ

ここまで読んで気づいた方もいるかもしれませんが、今回の記事は「ゲームボーイアドバンス用ソフトの時計をずらす」ということに帰着します。今回やったのは修理のための行為ですが、チートに転用しうることも容易に想像がつくでしょう。
今回はRTCにアクセスし時刻を動かすツールですから、ゲームプログラムのROMの読み書きはできません。ROMの読み書きは特に海賊版のデータが出回る可能性を大いに含みますから、著作権の観点からも、利益の観点からも、メーカーも総力を挙げてそういったツールの撲滅にかかるようです。
ですが、今回はせいぜい時計をずらす程度のチートツールとしてしか使えません。著作権の侵害にあたる可能性はまず無く、せいぜい著作人格権で問題になることがあるかもしれないという程度でしょう(チートツールは作者の意図したゲーム性を破壊することで著作人格権の侵害とみなされることがあるらしいです)。しかし、ポケモンは時刻をずらしたところでそんなにゲーム性を著しく欠くほどのチートにはならないでしょうし、上にも書いた通り、ゲームボーイアドバンスはもう何世代も前のゲーム機で市場は枯れ果てていますから、メーカーも目くじらを立てて怒ってくることも無いと思っています。
というわけで、現在私はこの記事に重大な問題があるとは認識しておりません。関係者の方で、万が一何か問題があるというのならばぜひご一報ください。

改造、修理も男のロマンですからね。怒られない程度の範囲内ならばやって楽しいでしょう。というわけで、私は自分のやったことを公開することにしました。

この記事を参考に改造する方がいるのならば、くれぐれも分をわきまえて行うようにしてくださいね。


さて、いかがだったでしょうか。

ゲームソフトにハード的な寿命がある最後の世代だと言えるゲームボーイアドバンス、その過渡期だからこそ発生した「セーブデータと時計データの錯誤」が生み出した問題ですが、何とか自力で解決することができました。
組み込み制御の知識がある程度あって、かつこの時代のソフトを復活させたいのならば、ぜひやってみてはいかがでしょうか。

あ、あくまでも自己責任でお願いしますよ。

2016年1月14日木曜日

PICマイコンでBCD変換

さて、今回は車輪の再発明というか、別に世の中的に新しいことはありませんが、BCD変換アルゴリズムについて知ったことがあったので備忘録的にまとめておこうと思います。

二進化十進表現(BCD)は、何かと組み込みプログラミングに付いて回ります。値を液晶に表示するときなんかは、内部的にBCD変換をすることになるはずです(C言語ならばprintfとかを使えばプログラマーが意識することはなくなるかもしれませんが)。
他にも、RTCなんかはカレンダー機能を内蔵していて、データのやり取りにはBCD形式を用いているものも結構ありますし、PIC24Fシリーズが持っているRTCCもBCDです。

さて、このBCDですが、このように組み込みプログラミング上で使おうとすると、マイコン内部の値、すなわちただの2進数との変換が必要になってきます。私の中ではこれは案外コストがかかるものであるという認識がありました。
というのも、BCD変換は要するに10進数の桁の抽出ですから、変数を10で除したり、その剰余を計算する必要があります。マイコンのような貧弱なCPUでは除算命令を持っていないことが多く、内部的にはループ等で除算が実装されていると思われます。また、C言語では除算演算子と剰余演算子が別個なので、同じ除数、被除数の場合はループならば本来は同時に商と剰余が産出されますが、C言語で記述する場合は、コンパイラが最適化してくれない限り、商と剰余で2回除算が必要になります。
もちろん自分でループを実装すればその手間は省けますが、それでもあまりスマートな気はしませんよね。

というわけで、こういうのって絶対すごいアルゴリズムあるよね~ってググっていたら見つけました。
BCD変換ルーチンについて
2020/5/4追記: 上記サイトリンク切れにつき、当ブログにてアルゴリズムを詳しく紹介する記事を書きました。アルゴリズムについてはこちらを参照ください。)
1度読んだだけではどういう意味かよく分かりませんでしたが(ついでに<SUB>タグなどが上手く仕事していなくてとても読みにくいですが)、何度か読むと意味がわかってきました。シフト演算で値をコピーするときにBCD変換しようという発想で、基本的にはコピー先の各桁が10以上になったら6を足して繰り上げしてあげるってことみたいです。ただ、シフト演算は要するに2倍するということですので、2倍後の各桁は最大で18になりえます。ですが、それは4ビット値の最大値(15)を超えて不都合なので、シフト演算で2倍する前に5以上かを判別し、繰り上げ処理(2倍する前なので6の半分の3を足す)をやってあげるということのようです。これによって、BCD変換前の値のビット数だけのループ回数と、あとはシフト演算や簡単な比較、足し算だけでBCD変換ができてしまいます。なんと低コストなのでしょう。PICにはディジットキャリービットがありますから、下位4bitが桁あふれをしたかを判別することができるので、4bitごとの演算はとてもやりやすく、さらに計算の低コスト化ができるでしょう。コンパイラの頭の良さを試しどころです。

というわけで、PICを意識したC言語でこのアルゴリズムを実装してみました。
typedef union {
    uint24_t value;
    struct _tagField {
        unsigned binary  : 8;
        unsigned BCD_1   : 4;
        unsigned BCD_10  : 4;
        unsigned BCD_100 : 4;
    } field;
} BYTE_BCD_CONVERTER_UNION;

uint16_t ByteToBCD(uint8_t val)
{
    BYTE_BCD_CONVERTER_UNION conv;
    conv.value = 0;
    
    conv.field.binary = val;
    
    for(uint8_t i = 0; i < 8; i++) {
        if(conv.field.BCD_1 >= 5)
            conv.field.BCD_1 += 3;
        if(conv.field.BCD_10 >= 5)
            conv.field.BCD_10 += 3;
        if(conv.field.BCD_100 >= 5)
            conv.field.BCD_100 += 3;
        conv.value <<= 1;
    }
    
    return (uint16_t)(conv.value >> 8);
}
8bitの値(byte)から3桁のBCD値を作り出す関数です。変にシフト演算とかを使いまくって「桁の値が5以上だったら」などという処理をするよりか、うまいことビットフィールド構造体を使うことで、コンパイラができる限り低コストなコードを生成してくれます。と同時にリトルエンディアンの処理系依存になってしまいますが、まあ、大体身の回りはリトルエンディアンなので良いでしょう(ヘラヘラ
ためしに、BCDの1の位と10の位が5以上かを見て処理する部分の逆アセンブル結果を見てみましょう。ちなみに、コンパイラはXC8 v1.35の無料版で、ターゲットはPIC16F1823です。
22:                    if(conv.field.BCD_1 >= 5)
0279  087A     MOVF i, W
027A  390F     ANDLW 0xF
027B  00F4     MOVWF multiplicand
027C  3005     MOVLW 0x5
027D  0274     SUBWF multiplicand, W
027E  1C03     BTFSS STATUS, 0x0
027F  2A8A     GOTO 0x28A
23:                        conv.field.BCD_1 += 3;
0280  087A     MOVF i, W
0281  390F     ANDLW 0xF
0282  00F4     MOVWF multiplicand
0283  3003     MOVLW 0x3
0284  07F4     ADDWF multiplicand, F
0285  087A     MOVF i, W
0286  0674     XORWF multiplicand, W
0287  39F0     ANDLW 0xF0
0288  0674     XORWF multiplicand, W
0289  00FA     MOVWF i
24:                    if(conv.field.BCD_10 >= 5)
028A  0E7A     SWAPF i, W
028B  390F     ANDLW 0xF
028C  00F4     MOVWF multiplicand
028D  3005     MOVLW 0x5
028E  0274     SUBWF multiplicand, W
028F  1C03     BTFSS STATUS, 0x0
0290  2A9C     GOTO 0x29C
25:                        conv.field.BCD_10 += 3;
0291  0E7A     SWAPF i, W
0292  390F     ANDLW 0xF
0293  00F4     MOVWF multiplicand
0294  3003     MOVLW 0x3
0295  07F4     ADDWF multiplicand, F
0296  0EF4     SWAPF multiplicand, F
0297  087A     MOVF i, W
0298  0674     XORWF multiplicand, W
0299  390F     ANDLW 0xF
029A  0674     XORWF multiplicand, W
029B  00FA     MOVWF i
こんな感じになっています。1の位側はそのままANDで0x0Fでマスクしてから比較処理をし、10の位側はSWAPFで上4bitを下と入れ替えて、から同様にマスクして処理をしています。シフト演算ではなくSWAPFを使って4bit一気に動かしたという処理は、まあ及第点でしょう。
実際、自分でアセンブリで書くのならば、1の位はそのままマスクせずに0x05を引いてからSTATUSレジスタのディジットボロービットを見ることになりそうですし、0x03を足してもその4bit値としては桁あふれは絶対起こさないということはわかっているので、XOR→AND→XORなんていう回りくどい処理は省くでしょう。また、10の位はSWAPFなんてせずに、0x50を引いて上位4bitが5以上かを見ればよいですし、ここで足し算をするときもSWAP→マスク→3を足す→SWAP→XOR→AND→XORなんていう回りくどいことをせず、0x30を足せば良いでしょう。
…と考えると、このバイナリもまだまだ相当ステップ省略できそうですね。「桁あふれしないことはわかっている」っていうのはかなりハードな最適化なのでコンパイラができなくても仕方ないかなと思いますが、SWAPをして3を足す代わりにそのまま0x30を足す、くらいの芸当はやってほしかったですね。まあ、無料版だから仕方ないか。C言語でそう書けばいいんでしょうけどね…。