数値をBCD出力(表示)するルーチン #2
2022年10月8日:数値をBCD出力(表示)するルーチン
の改良版ということで、文字出力・表示ではなく
BCDの文字列に変換するようにしました。
txbcd()では変換結果の文字を、関数の中でいきなり
シリアル出力していたんで、同じデータを液晶に
表示したいときは、別の処理をしなくちゃなりま
せんでした。
変換結果を文字列にすると、同じ値をシリアル出力と
液晶表示に送り込みたいとき、変換操作が一度ですみます。
BCDへの文字変換ということで「strbcd()」という関数名
にしました。
使い方はほぼ同じです。
変換結果が入った文字列の先頭アドレスを持ってリターンす
るのでそのままSerial.printやLCD.printに渡せます。
/********************************/
/* BCD文字出力処理 */
/********************************/
/***** BCD出力用文字バッファ *****/
static char bcd_bff[16]; // 16文字 終端のnull含めて
// longは10桁
/***** 桁指定BCD文字出力 *****/
// d:変換データ long値
// n:整数部文字数(-を含めて)
// p:小数部文字数(.は含まない)
// 数値は整数を入力 浮動小数点ではない
// 小数部は整数の基数が0.01などの時に用いる
// 12345が123.56を意味する時 (d,3,2)と指定
// -1234を-123.4と表示するときは(d,4,1)と-を含めた文字数に
// bcd_bffに入った文字の先頭アドレスを持ってリターン
char *strbcd(int32_t d, byte n, byte p)
{
byte i;
byte sgn = 0; // 符号フラグ
byte zr = 0; // ゼロデータ処理済みフラグ
char *s; // 0~9書込みバッファのアドレス
s = bcd_bff+sizeof(bcd_bff) - 1; // バッファの最後尾
*s-- = '\0'; // 最後にnullをセット
if(d < 0){ // マイナス?
d = -d; // +の値にして
sgn = 1; // -フラグをオン
}
// 小数部 '.'まで
if(p){ // 小数部あり
for(i = 0; i < p; i++){ // loop
*s-- = (char)((d % 10) + '0'); // 下位桁から0~9に
d = d / 10; // 1/10して上位桁の処理
}
*s-- = '.'; // 小数点
}
// 整数部 上位ゼロサプレス
for(i = 0; i < n; i++){ // loop
if((i == 0) || // 1桁目あるいは
(d != 0)){ // ゼロ以外
*s-- = (char)((d % 10) + '0'); // 下位桁から0~9に
d = d / 10; // 1/10して上位桁の処理
}
else if((sgn != 0) && // 2桁目以降でゼロ、そしてマイナス
(zr == 0)){ // -符号処理まだ
*s-- = '-'; // マイナスを付加
zr = 1; // ゼロでマイナスを処理した
}
else{ // 2桁目以降でゼロ
*s-- = ' '; // ゼロサプレス
}
}
return (s + 1); // 文字列の先頭アドレスを持ってリターン
}
char *s; // 文字列のアドレスを置いておくポインタ
s = strbcd(d, 4, 2); // -999.99~9999.99の範囲をBCDに
Serial.print(s); // シリアルとLCDに同じ値を
LCD.print(s);
こんな格好で使います。
※次にstrbcdを使うまでは変化しません
食わせる変換元データdの指定サイズで関数の容量が
変わります。
例のように4バイトデータ、int32_tなら264バイト
2バイトのint16_tなら178バイト
でした。
±32768の範囲で処理できるなら、shortにしておけば
だいぶと小さくできます。
以前のtxbcdは前から変換結果を置いていましたが、
今回はバッファの最後から前に向かって詰めて行っ
てます。
そして、分かりやすくするため小数部と指数部に分けて
変換処理したので商計算と剰余計算がそれぞれ2回出て
きます。
このあたりが原因でプログラム(機械語レベルでの)の
サイズが大きくなったのかと思います。
数字の桁数を固定しての表示や出力、たいていの場合
「printf系」の処理が必要です。
確かに便利なんですが、printfを使うとプログラムが
膨れます。
それと、マイナスの数値を出したいときにちょいと不便。
printf("%4d.%02d", d/100, abs(d%100))
とした時、-123は-1.23と出てくれますが、2桁までの
マイナス値、例えば-12だと 0.12 となり整数部の0、
この頭にマイナス記号が付いてくれません。
「-0.xx」を出すのに悩んでしまいます。
もう一つが、表示幅。
printfだと、桁数指定より大きな数が来た時でも
ちゃんと正しく変換してくれます。
でも液晶表示のように、想定しているより表示桁数が
増えると、表示がずれちゃいます。
本来は、ズレないように数値に規制をかけなくちゃい
けないのですが、「とりあえずのプログラム」だと
めんどうなものです。
今回のstrbcd()で、範囲外の値はマイナス記号も含めて
捨ててしまうので、表示幅は確定します。
※異常値出現が分からないというのは
デメリットでしょうけど。
※2024-05-01追記
リターン値が「nとp」(整数部、小数部)の値で変わるというのも
けったいなので、いつも文字バッファの先頭を持ってリターン
するようにしてみました。
バッファへの文字書込みも、プリデクリメント「*--s」の記号を
使うようにしました。
AVRマイコンのメモリーへの間接アクセス命令は
ST X,Rr のアドレスそそまま(X以外にY,Z)
そして
ST X+,Rr のポストインクリメント
あるいは
ST --X,Rr のプリデクリメント
の3種。
で、「*--s」を使っていみたわけです。
※コンパイル結果は残念ながら「ST --X,Rr」には
展開されてませんでした。
「ST X,Rr」も「ST --X,Rr」もクロック数は同じ
なんですが・・・
/***** 桁指定BCD文字出力 *****/
char *strbcd(int32_t d, byte n, byte p)
{
byte i;
byte sgn = 0; // 符号フラグ
byte zr = 0; // ゼロデータ処理済みフラグ
char *s; // 0~9書込みバッファのアドレス
if(d < 0){ // マイナス?
d = -d; // +の値にして
sgn = 1; // -フラグをオン
}
i = n + p; // 文字数
if(p) i++; // 小数点あれば+1
s = bcd_bff + i; // バッファの最後尾
*s = '\0'; // 最後にnullをセット
// 小数部 '.'まで
if(p){ // 小数部あり
for(i = 0; i < p; i++){ // loop
if(d){ // 0でないときは計算
// PB2_H; // (!!!)
*--s = (byte)(d % 10) + '0'; // 下位桁から0~9に
d = d / 10; // 1/10して上位桁の処理
// PB2_L; // (!!!)
}
else{ // 0なら'0'を
*--s = '0';
}
}
*--s = '.'; // 小数点
}
// 整数部 上位ゼロサプレス
for(i = 0; i < n; i++){ // loop
if((i == 0) || // 1桁目あるいは
(d != 0)){ // ゼロ以外
// PB2_H; // (!!!)
*--s = (byte)(d % 10) + '0'; // 下位桁から0~9に
d = d / 10; // 1/10して上位桁の処理
// PB2_L; // (!!!)
}
else if((sgn != 0) && // ゼロでマイナス
(zr == 0)){ // -符号処理まだ
*--s = '-'; // マイナスを付加
zr = 1; // 始めてのゼロを処理した
}
else{ // 2桁目以降でゼロ
*--s = ' '; // ゼロサプレス
}
}
return s; // 文字列の先頭アドレスを持ってリターン
}
もう一つ問題。
printf()やdtostrf()を使った場合より、うんと全体のROM
容量は減らせるんですが、速度がでません。
ループの中にあるlongの割り算に時間がかかります。
※ちょいとでも速くとゼロのときをスキップする
ようにしても、変換桁数が多いと同じ。
longの割り算、商と剰余を使っています。
コンパイル結果は一度の割り算で商と剰余が出る「ldiv()」関数
を使っているようなのですが、割り算の時間はおよそ「40μs」。
変換桁数ぶんだけ時間がかかります。
ところが、浮動小数点を文字列に変換する「dtostrf()」は桁数が
多くなってもそんなに遅くならず、70μs~120μs(8桁)程度。
※8桁だとstrbcd()の時間は2倍を超えてしまいます。
binデータの数字文字変換、10で割ってその余りを使う以外に
高速な手法あるのかな。
↓のZ80で使ってるMSBからのcarryを見て10進加算という手法。
32bitなら10文字なんで、あっという間か。
※機械語ならあれこれ思いつくんだけど。
=============================
※ちょっと昔話・・・
Z-80でのBCD変換、こんなルーチンを使っていました。
; 16 bit binary カラ BCD ヘ ヘンカン
; HL --->AHL
; FFFF--->65535
BCD:
LD B,16
LD DE,0
LD C,D
BCD01:
ADD HL,HL
LD A,E
ADC A,A
DAA
LD E,A
LD A,D
ADC A,A
DAA
LD D,A
LD A,C
ADC A,A
DAA
LD C,A
DJNZ BCD01
EX DE,HL
RET
16bitのペアレジスタ 、HLに入った0x0000~
0xFFFFの16進数をBCDに変換して「AHL」に
入れてリターン。
00・00・00 ~ 06・55・35
の数値が得られます。
「DAA」命令が役立っています。
※このアセンブラ・ニーモニックを見て動きを
理解していただける人がどれだけいるか・・・
このようにHEXをBCDに変換した後、次の
16進文字変換ルーチンでASCII文字に直して
「TX」でシリアル出力します。
TXHEXH: ;上位4ビットの変換 0~9,A~F
RLCA ;hex data (H nibble)
RLCA
RLCA
RLCA
TXHEXL: ;下位4ビットの変換
AND 0FH ;hex data (L nibble)
ADD A,90H
DAA
ADC A,40H
DAA
JP TX ;0~9,A~Fを1文字出力
TXHEX: ;00~FF 2文字16進数出力
PUSH AF ;hex data (byte) (2 chr)
CALL TXHEXH
POP AF
JR TXHEXL
TXHL: ;0000~FFFFF 4文字16進数出力
LD A,H ;H,L (4 chr)
CALL TXHEX
LD A,L
JR TXHEX
ここでも「DAA」が活躍。
0x90を加算してDAA。
その後、carryを含めて0x40を加算してDAA。
これで、「0~9」の10進数字と「A~F」の
16進文字が得られるのです。
この謎の手法を知ったときはほんと驚きました。
条件分岐しないで16進文字が生まれるのですから。
※テストプログラム
ロータリーエンコーダーとI2C液晶(8文字×2行)を
つないでます。
・ダウンロード - strbcd02.zip
strbcd()とdtostrf()の速度差をオシロで
見れるようにしました。
| 固定リンク
「Arduino」カテゴリの記事
- おっと。map関数の計算桁に注意(2024.10.06)
- DDS方式の2相パルス発生回路、周波数スキャン機能を付ける(2024.10.05)
- 1クロックでも速くしたい 割込を「ISR_NAKED」で(2024.09.30)
- 1クロックでも速くしたい DDS方式の2相パルス発生器(2024.09.27)
- 秋月のI2C接続液晶 AQM1602XAを基板に直付け(2024.09.24)
「AVRマイコン」カテゴリの記事
- AVRマイコンAT90S1200を使ったデジタル時計(2024.06.02)
- 数値をBCD出力(表示)するルーチン #3(2024.05.03)
- 数値をBCD出力(表示)するルーチン #2(2024.04.28)
- ATmega4809のシリアルポート(2023.01.18)
- PWMでD/A変換:アナログマルチプレクサの応用で(2023.01.02)
コメント