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

0 件のコメント:

コメントを投稿