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言語でそう書けばいいんでしょうけどね…。