AVRマイコン

2024年5月 3日 (金)

数値をBCD出力(表示)するルーチン #3

あれこれ紹介してきましたBCD出力ルーチン。
 ・2022年10月8日:数値をBCD出力(表示)するルーチン
 ・2024年4月28日:数値をBCD出力(表示)するルーチン #2

除算・剰余方式ではどうしても「遅れ」が目立ちます。
そこで、float値を文字変換する「dtostrf()」と処理時間を
比べてみました。

まずこれが「strbcd02.ino」:除算・剰余方式の速度。
 ch1が自作ルーチンstrbcd()の速度
 ch2がdtostrf()の速度
 ch3はstrbcdの中で使っている除算の時間です。
   (商と剰余の両方を得ている時間)
 ch4は液晶表示のタイミング。

「0」を表示したところ。
01_0

上位はゼロサプレスされるんで、
7文字のスペース+'0'という表示です。

「9999」の変換。
01_9999

上位4文字がゼロサプレス。
4回の除算が実行されていて、除算に時間が
かかっていることが見えます。

さらに「99.99999」の変換。
01_99r99999
  ※上の二つと時間軸が異なります。
ゼロサプレスはありません。
全桁で0~9に数字を得るために除算が7回実行
されています。
こうなるとdtostrf()が圧倒的に有利。

そこで、除算と剰余の計算でなく、上位桁から
10進数値を減算できるかどうかで、剰余を得る
方法を試してみました。

たとえば、「1234 = 0x04D2」を変換する手順。
・千桁 1234から1000=0x03E8 を何回引けるか → 1回 余り234
・百桁 234から100=0x0064 を何回引けるか → 2回 余り34
・十桁 34から10=0x000A を何回引けるか → 3回 余り4
・一桁 余り=4
これで「1234」とBCD値が得られます。
比較と加減算ですので除算より高速に処理できますが、
数値が「9」だと9回の比較・加減算が生じます。
ループ回数が増えると、これも時間がかかります。

strbcd03.ino

/********************************/
/* BCD文字出力処理 */
/********************************/
/***** BCD出力用文字バッファ *****/
char bcd_bff[16]; // 16文字 終端のnull含めて
// longは10桁
// BCD変換4bitデータ
byte bcd_4bit[10]; // 10桁の4bitバッファ
// 下位が10^0桁
// 先頭が10^9桁
// 減算データ
const int32_t bcd_chks[] PROGMEM ={ // 32bitは10桁
1000000000, // 0 10桁 元データからいくつ
100000000, // 1 9桁 引けるかを調べて
10000000, // 2 8桁 余りを次の桁へ
1000000, // 3 7桁
100000, // 4 6桁
10000, // 5 5桁
1000, // 6 4桁
100, // 7 3桁
10, // 8 2桁
}; // 1の桁は引かない 最後の余り

/***** 桁指定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, j;
byte sgn; // 符号フラグ
byte mins = 0; // マイナス符号処理済みフラグ
uint32_t e; // 減算値
char *s; // 0~9文字書込みバッファのアドレス
byte *t; // bcd_4bit変換データアドレス
if(d < 0){ // マイナス?
d = -d; // +の値にして
sgn = 1; // -フラグをオン
}
else{
sgn = 0;
}
// 引き算できるかどうかで余りを計算
for(i = 0; i < DIMSIZ(bcd_chks); i++){ // 桁数loop
e = pgm_read_dword(&bcd_chks[i]); // 減算値
j = 0; // 引ける数をカウント
while(1){
if((uint32_t)d < e) break; // 引けなければおわり
j++;
e += pgm_read_dword(&bcd_chks[i]); // 比較値を加算
}
bcd_4bit[i] = j; // 引けた数が余り
d -= (e - pgm_read_dword(&bcd_chks[i])); // 元値を減少
}
bcd_4bit[sizeof(bcd_4bit) - 1] = d; // 最下位 1の桁
// 変換した桁数をチェック ゼロサプレスのため
t = bcd_4bit; // 変換データの先頭
j = sizeof(bcd_4bit); // 変換データ数
while(j){
if(*t++ != 0) break; // 0じゃない
j--; // 0なら-1
} // jは0以外の数字の数
if(j == 0) j = 1; // 0なら1に
// 文字バッファ終端 nullをセット
i = n + p; // 全体の文字数
if(p) i++;
s = bcd_bff + i; // 文字バッファの最後尾
*s = '\0'; // nullをセット
// 4bitの変換データを文字にしながらコピー
t = bcd_4bit + sizeof(bcd_4bit); // 変換データの最後
if(p){ // 小数部あり
for(i = 0; i < p; i++){
*--s = *--t + '0'; // 0~9を書込み
if(j) j--;
}
*--s = '.'; // 小数点
}
for(i = 0; i < n; i++){ // 整数部
if(j){ // 0以外の数が残っている
*--s = *--t + '0'; // 0~9を書込み
j--;
}
else{ // 必要数字を全部書き込んだ
if((p != 0) && // 小数部あり
(i == 0)){ // .の左隣に
*--s = '0'; // ゼロを付加
}
else{
if((sgn != 0) && // マイナス
(mins == 0)){ // -符号処理まだ
*--s = '-'; // マイナスを付加
mins = 1;
}
else{ // 2桁目以降でゼロ
*--s = ' '; // ゼロサプレス
}
}
}
}
return s;
}

さっきと同じようにdtostrf()との速度比較。
「1」を表示したところ。
03_1

「9999」の変換。
03_9999

さらに「99.99999」の変換。
03_99r99999

なんとかdtostrf()に追いついている感じです。

この方式で有利なのが、プログラムサイズ。
それぞれのルーチンを使ったときのROMサイズを
見てみましょう。

メモリ比較 (strbcd03.ino)

 strbcd() dtostrf()  ROM RAM
-----------------
(1)  ○   ○   5900 359
(2)  ○   -   3738 335
(3)  -   ○   5348 333
   --------------
    ROM (3)-(2) = 1610

(2)dtostrfなどfloatを使わないようにした。

dtostrf()やfloatを除いてstrbcd()だけで処理すると
1600バイトほどプログラムの必要ROM容量が減りました。

strbcd()ルーチンそのもの実行ルーチンROMバイト数は、
  除算・剰余方式  :278バイト
  比較・加減算方式 :374バイト
でした。
比較・加減算方式でほ実行ルーチンの他に10桁~2桁の
加算データテーブルが36バイト必要です。

遅くても良いのでプログラムを短くしておきたいなら
「除算・剰余方式」。
ちょっとでも速くというのなら「比較・加減算方式」という
ところでしょう。
「ルーチンをまとめるのが面倒」「ROMに余裕はあるので
既設の関数で」というのなら「float」で「dtostrf()」を。
あれこれ、探ってみましたがこんなところかと。

| | コメント (0)

2024年4月28日 (日)

数値を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()の速度差をオシロで
見れるようにしました。

| | コメント (0)

2023年1月18日 (水)

ATmega4809のシリアルポート

Arduino UNOで使われているATmega328Pには
シリアルポート(UART)が一つだけしかありません。
  ※SoftwareSerialは別物

もっと欲しいゾという時は、
ATmega2560が搭載された「Arduino Mega 2560」を
使いました。
  ※RAMも8kに増えますし。
しかし・・・大きい。 基板サイズがデカいのです。

そこで新しいチップ「ATmega4809」のデータシートを
見ますと・・・UARTが4つになってます。 ←エエやん!

4809には40ピンDIPと48ピンのQFPの2種類が存在します。
40ピンのは48ピンのから
  ポートBの6本
  ポートCの2本
が除かれています。

ポートの割り当て表を眺めますと、UART3が配置されているのは
ポートBです。
代替ポートを選択しようとしてもポートB。

Pb

しかし、40ピンではポートBはが全部省かれてます
ということは、40ピンのATmega4809、UARTは3つしか使えない
ということになるのかと。  ・・・アカンやん。

Pb2
  ※英文のデータシートも同じだった
   UART、3コと4コの区分線がミスってる?

Arduino nano every (48ピンのが使われている)
でも買って、触ってみようかと。

| | コメント (0)

2023年1月 2日 (月)

PWMでD/A変換:アナログマルチプレクサの応用で

2020年8月13日:Arduino、analogWriteは捨てちゃえ。ちゃんとしたPWMを使おう
ということで、過去、PWMを使ったDAC(D/A変換回路)を
さまざまなツールで使っています。

例えば、
JIS C8708による充放電サイクル試験回路(トランジスタ技術2010年2月号)
2020年4月10日:JIS C8708:2019対応ニッケル水素電池充放電実験回路(回路図とプログラム)
2022年4月14日:Arduino IDEでRaspberry Pi Pico:PWMでD/A出力してA/D入力を試す
2022年4月15日:Arduino IDEでRaspberry Pi Pico:A/D入力が…あれっ?

これらでは、PWM波からDC電圧を作るのに、
基準電圧ICを使っています。
Vref値とGND間をPWM波でマルチプレックスし、
LPFを通すことで直流に。
デューティ比に比例したDC電圧を得ています。

この応用ということで、Arduino UNOのマイコン
ATmega328PのAREF電圧を取り出した使った実験も
しています。

例えば、この記事。
2022年10月5日:サーミスタ103JTで計った温度をシリアル出力

サーミスタの抵抗値を見るのに、AREF電圧を取り出して
A/D変換します。
  ※これはMPXとは関係なしの用例。

次のこの記事では、AREFをオペアンプでバッファして
PWMし、LPFを通してD/A変換しています。
2022年4月22日:Arduino UNOのA/D入力:PWMでD/A出力してA/D入力を試す

ここで問題に気が付きました。
「なんじゃこれは?」の信号が見えたのです。

こんな模式図を描いてみました。

M10

AREFをオペアンプでバッファして取り出します。
ATmega328Pに関してはこれでうまく基準電圧が
取り出せます。

そのオペアンプ出力を直にマルチプレクサの入力に
つないでいます。
外付け基準電圧ICを使った時のようにコンデンサを
入れたいところなのですが、発振の可能性があるので、
オペアンプの出力にコンデンサは入れられません。

たまたま、このPWM→D/A変換部の波形を見た所、
こんな信号になっていました。

M201m

AREF電圧を出力しているオペアンプ、そこにPWM波に
同期した負パルス(▼マーク位置)が出ているのです。
出力インピーダンスは低いはずなので、MPXの切換
ノイズが飛び込んでいるわけではありません。

そこで、こんな回路を使って実験してみました。
プラス側のIN1入力は1kΩで+5Vに。
マイナス側は1kΩでGNDに。

M12_20230102152301

すると・・・・
まず、1ゲートタイプのマルチプレクサ TC7W53
M302m

3回路入った74HC4053の場合。
M301m

MPXパルスが変化したエッジでIN0、IN1に瞬間的な
電流が流れている様子が見えています。

AREF電圧をバッファした通常速度のオペアンプでは、
この短いパルス(150nsくらい)を抑えきることができない
のでしょう。

なぜこんなことが起こるのか・・・
TC7W53の内部回路を見ると分かってきます。

M11_20230102152501
IN0とIN1の切換には、2直にしたインバータが
入っています。
IN1側がその後段につながっているので、IN0から
遅れて切換されます。
その一瞬、COM端子につながるOUT0とOUT1を通して
IN0とIN1が短絡するのです。
IN0はGNDにつながっていますので、オペアンプ出力を
つないでいるIN1が一瞬だけGNDに引っ張られることに
なってしまいます。
それがオペアンプ出力に現れた負パルスの原因です。

  ※観察のためIN0とIN1に1kΩの抵抗を入
   れましたが、IN0、IN1とGND間に0.01uF以上、
   0.1uF程度のコンデンサを入れると短絡パルス
   は見えなくなります。
   基準電圧ICを使った時のパスコンは有効という
   ことがわかります。

対策案
・ATmega328PのAREFをオペアンプでバッファして
 基準電圧を得るのはOKだが、それをPWM制御する
 マルチプレクサにつなぐのは×。
・PWMするマルチプレクサでD/A変換する時は、
 パスコンを付加できる基準電圧ICを使う。

こんなところでしょう。
  ※見えなくなるけど、それでも、短絡は発生してる。
   ノイズ発生の要因になるか。

※注1
基準電圧ICに付加するパスコン、こんなことも
ありますので。
2019年9月14日:良かれと思って付けたコンデンサが・・・
  ※AD680・・・発振!

※どうなんだろう
多チャンネル入力A/Dコンバータ、たいていの場合
マルチプレクサが入力。
チャンネル切換時に今回のようなことが起こる心配
は無いのか?
入力に入れたCRフィルタが役立つかも。

※ブレッドボードで実験
002_20230103102701
クロック源はATmega328Pを使ったパルスジェネレータ
拡大。
003_20230103102701


| | コメント (0)

2022年12月20日 (火)

ガレージのLED表示デジタル時計がダウン

昨晩、表示が不自然に点滅しているのに気が付きました。
   ・・・20年以上連続運転してた

資料、vectorにアップしてました。
  ・AVRCLK:AVRマイコンで作るデジタル時計
AVRマイコンAT90S1200を使ったLED表示デジタル時計の製作

おそらく、スイッチング電源が原因。
別のもの交換してみます。

11_20221220093201
回路図
Tokei1

AT90S1200
 ROMが1kバイト。
 RAM無し
  ということで、push、pop命令無し
 スタックは3段だけ。
むちゃ縛りのあるマイコンです。

※追記
ELCOの「ゴリラ」を外して、別の5V定電圧電源に
接続したら・・・OK。
バックアップも働いていて、現時刻をちゃんと表示しました。
   ・・・「たいしたもんや」と自画自賛。
これから、使えそうな電源を探しますわ。

※電源を交換して修理完了
12_20221220104201

「ゴリラ」の中は充填剤でどうなって
いるのか見えません。
13_20221220104201

以前にこんな報告してました。
  ・2019年8月21日:7seg LEDの輝度低下
「電源交換したか?」と記してましたが、昔の写真を
見ると、電源はこのゴリラから換えていませんので、
20年動いたということですわ。





| | コメント (0)

2022年11月15日 (火)

FLUKE 87IV デジタルテスターのデータをIrDAで吸い上げる

2022年7月6日:デジタルテスター「FLUKE 87IV」の赤外線通信ユニット完成
これでいちおう完成してるんですが、IrDAのインターフェース・チップを
テキサス・インスツルメンツの「TIR1000」 に換えて試してみました。

マイクロチップの「MCP2122」 と何が違うのか・・・

図示すると・・・ピンは同じ配列。
Tt11

(1) MCP2122は8pin DIPのがある。 そして安価。
 TIR1000はSOPとTSSOP。

(2) リセット入力の論理レベルが異なる。
 MCP2122はLでリセット。 TIR1000はHでリセット。

(3) 赤外線の自己反射光の処理が違う。
 こちらから出した赤外線、どこかに当たって返ってきて
 受信光としてRXIR信号に入ってきます。
 MCP2122では送信中のRXoutは禁止してくれるので、
 これを考えなくて良いのです。
 ところがTIR1000では筒抜けに。
 自分で処理しなくちゃなりません。

この(3)せいで、単純なターミナルでの実験だとローカルエコーが
返ってくる感じになります。

マイコンでの処理だと、IrDAへのシリアル送信処理が始める前に、
IrDAからの受信を禁止しちゃって無視するように。
そして、送信完了で受信を再開。
こんな手順を制御プログラムに入れなくてはなりません。

前に紹介したATtiny841のプログラムでは、
「送信データレジスタ空き割り込み」で次データを送り込んで
いたのですが、受信を再開させるには送信完了(全フレームの
送出完了)を知らなければなりません。
送信割り込みの方法を変えなければなりませんでした。

TIR1000を使うメリットって無いぞ、なんですが、
試しに回路を作っちゃったんで、仕上げてしまいました。
ジャンパーを追加してMCP2122かTIR1000かを切り替えられ
るようにしました。

| | コメント (0)

2022年10月 8日 (土)

数値をBCD出力(表示)するルーチン

ROM、RAM容量に余裕のあるマイコンなら
「printf」系の「sprintf」で文字列に
変換してシリアル出力、あるいは液晶表示
という手法が使えます。

しかし・・・
  ROM、RAMに余裕がないなぁ
  BCDにして出力(表示)するだけなのに
てな時に役立つルーチンを紹介しておきます。

・longデータ ということは整数だけ。
・ゼロサプレス付で数値に変換。
・マイナスは最も左側の数字の頭に「-」を付加。
・数値「-12345」の最小桁が0.001を意味する時の
 ため(例えば-12.345Vと表示したい)、整数部桁数
 と小数部桁数を指定できるようにした。
・値が指定桁数より大きくなっても、左にはみ
 出さない。
   液晶表示などでは表示がズレないけれど
   越えた部分の数字は見えない。
・1文字シリアル出力ルーチンの名をtx()として
 記している。
   液晶表示なら1文字書き込みに変える。
・「 -12.345」と出力したいのなら
   (先頭にスペース1コ付けている)
      txbcd(d, 4, 3);
 これで、-999.999 ~ 9999.999 の範囲を
 出力できる。
 -32768 ~ 32767のint値なら(longにキャストして)
   「 -32.678」~ 「  32.767」
  頭にスペース1つ  頭にスペース2つ
 wordなら
   「   0.000」 ~ 「  65.535」
  頭にスペース3つ  頭にスペース2つ

/*****  桁指定BCD出力       *****/
// n:整数部文字数 p:小数部文字数
// 数値は整数を入力 浮動小数点ではない
// 小数部は整数の基数が0.01などの時に用いる
// 12345が123.56を意味する時 (d,3,2)と指定
void txbcd(int32_t d, byte n, byte p)
{
char bff[20]; // 20文字(null入れず)
byte i, m;
byte sgn = 0; // 符号フラグ
memset(bff, ' ', sizeof(bff)); // スペースで埋める
if(d < 0){ // マイナス?
d = -d;
sgn = 1;
}
m = n + p; // 整数+小数 文字数
for(i = 0; i < (sizeof(bff) - 2); i++){ // longで12文字とマイナス
bff[i] = (d % 10) + '0'; // 下位桁から0~9に
d = d / 10; // 1/10して上位桁の処理
if((d == 0) && // 0になったら終わり
(i >= p)) break; // 小数桁ok?
}
if(sgn){ // 元値がマイナス?
bff[i + 1] = '-';
}
for(i = 0; i < m; i++){
tx(bff[m - i - 1]); // バッファを逆順で
if(p){
if(i == (m - p - 1)) tx('.'); // 小数点
}
}
}

例に記した「12345が123.56を意味する時 (d,3,2)と指定」
だと、整数部が3桁だけですのでマイナスを考えると正常に
出力できる(落ち無く)数値の範囲は「-99.99 ~ 999.99」
となります。

プログラムそのものは30行もありませんが、実プログラム
ではこれでも300バイトを越えてしまいます。
もっと小さくしたいときは、
 ・long:int32_t → int16_tに
 ・bffをローカル変数(スタックの処理が入る)で
  はなく関数の外に持ってくる。
てなところでしょうか。

sprintfだと、数値の単位や区切り文字を含めて1行
に表記できて便利ですが、これは数字だけ。

longの剰余と除算を使っています。
アセンブラで書くとなると、ここらから始めなければ
なりません。

Arduinoの「print」は数値の桁数を指定できない
ので不便。
数値とゼロサプレスの数を合わせて桁を揃えて
出力したい時は面倒でも「sprintf」。

 

| | コメント (6)

2022年10月 6日 (木)

5chサーミスタ温度計のA/D入力、map関数を使って補正

スカタンな応用例ばかり出てくるArduinoのmap関数
サーミスタ103JTで計った温度をシリアル出力 でのA/D変換値補正
で使ってみました。

まず、A/D入力電圧実測値と基準電圧値から真のA/D値を計算します。
  ツールはデジタルテスター。
  それと電圧発生器、あるいは抵抗を組み合わせて
  安定した入力電圧を得ます。
マイコンにはその時のA/D値をシリアル出力させます。
0に近いある値と1023に近いある値の2点をチェック。
どちらも同じなら補正の必要はありません。
ちょっとのズレなら「直線補間」の出番です。

今回の回路、サーミスタをつなぐch1~ch4はポートAでしたが
ch5だけがポートB。
この間でちょっとズレが出ていました。
まず0Vに近い方。
正値20に対してch1~4が17と-3。
ch5が16と-4と低めの値が出ていました。
フルスケールに近い方は、正値991に対して
ch1~4が992にと+1。ch5は誤差なし。

パラメータのテーブルにはこれらの値を10倍して設定します。
  ※浮動小数点にするとメモリーを食うので。
読み込んだA/D値も10倍してmap関数に食わせて補正を
行い、出てきた値の1の位を見て四捨五入。
10倍することで四捨五入の処理ができます。

これで、ずいぶんと温度測定の調子が良くなりました。
サーミスタの抵抗が低くなる高温域ではA/D値が小さくなります。
A/D入力電圧の低いところで誤差が生じていたのでこの補正が
うまく働いたのでしょう。
80℃付近ではA/D値の1bitが0.3~0.4℃の差になります。

map関数の応用例ということで、ご覧下さい。

//  A/D補正パラメータ
// 四捨五入のためA/D値を10倍にして
// x0とx1に実測値を設定する
// Rs = 10kΩで
// Lo側 200Ωでy0=20  Hi側 300kΩでy1=991
const struct{
const word x0; // in min (Lo)
const word x1; // in max (Hi)
const word y0; // out min
const word y1; // out max
}adj_tbl[] PROGMEM = {
{ 170, 9920, 200, 9910 }, // ch1 ADC1 PA1
{ 170, 9920, 200, 9910 }, // ch2 ADC2 PA2
{ 170, 9920, 200, 9910 }, // ch3 ADC3 PA3
{ 170, 9920, 200, 9910 }, // ch4 ADC7 PA7
{ 160, 9910, 200, 9910 }, // ch5 ADC8 PB0
};
/***** 線形補間 *****/
long map(long x, long in_min, long in_max, long out_min, long out_max)
{
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
/***** A/D値補正 *****/
// in ad : 0~1023 A/D値
// ch : 0~4 ch1~ch5
// 10bitの範囲でリターン
word adadj(word ad, byte ch)
{
long a, d;
a = map((long)ad * 10L, // 線形補間
(long)pgm_read_word(&adj_tbl[ch].x0), // in_min
(long)pgm_read_word(&adj_tbl[ch].x1), // in_max
(long)pgm_read_word(&adj_tbl[ch].y0), // out_min
(long)pgm_read_word(&adj_tbl[ch].y1)); // out_max
d = a / 10; // 1の位で四捨五入 10の位を+0か+1,-1
if(a >= 0){ // プラス
if((a % 10) >= 5) d++;
}
else{ // マイナス
if(((-a) % 10) >= 5) d--; // 正にして1の位で判定
}
if(d < 0) d = 0; // マイナスは0に
else if(d > 1023) d = 1023; // 最大は1023
return (word)d; // wordにしてリターン
}


5chの温度測定で重要なのが「Rs」=「神様抵抗」。
今回の製作では「0.1%」精度のを使ったので、
補正テーブルの「y0、y1」(out_min、out_max)は5chとも
同じになっています。
Rsの誤差は、この値の変更で吸収できます。

使っているAVRマイコンは14ピンのATtiny841。
ROMが8kバイトにRAMが512バイト。
現在のプログラムサイズが3826バイト。
このうち2048バイトが10bit ADC→0.1℃温度データ
の変換テーブル。
プログラムは実質1778バイト。BSSエリアが58バイト。

浮動小数点使わず。sprintf使わず。
この結果だと。

ROM、RAM容量半分のATtiny441にも入るかしら。

| | コメント (0)

2022年10月 5日 (水)

サーミスタ103JTで計った温度をシリアル出力

サーミスタ103JTで計った温度をアナログ出力
この製作で使ったマイコンは8ピンのATtiny85
内蔵タイマーが8ビットで、PWM出力でD/Aするには
分解能不足。
そこで、DACを使ってアナログ電圧を出力しました。

今回の「ダイソー ミニケース 5個組」を使った工作
やはりサーミスタ103JTを使った温度計なのですが、
シリアル出力でデータを得ようと目論見ました。

14ピンのATtiny841を使って、5つのサーミスタを
つなげられるようにしています。

ブロックダイヤグラム
C51_20221005170901
1秒間隔で測定値を出力します。
先頭が6桁数字が連番。
  データ抜けをチェックできます。
スペース区切りで5つの温度を出力。

セミテックの103JT温度・抵抗値表が元データ。
-30.0℃~125.0℃をテーブルにしています。

  ※10bit A/D値から温度データテーブルを
   直に呼び出すことで、浮動小数点関数計算
   を使わなくて済みます。
   ATtiny841のROMサイズは8kバイトしかあり
   ません。
   浮動小数点や「sprintf」による数値→文字列
   変換関数を使うと、ROM領域が足らなくなって
   しまいます。
   可能ならROM半分のATtiny441でも動くようにと。

ところが・・・ここでも失敗に遭遇。
  ATmega328Pを使ったピピちゃん温度計
では、外に取り出した内蔵基準電圧1.1Vを使っての
サーミスタ抵抗測定がうまく行ったので、ATtiny841でも
大丈夫だろうと考えたのです。

  ※ATtiny841と同じ14ピンのATtiny84は
   内蔵基準電圧を外に取り出せない。
   そして、外部からの基準電圧も2.0Vが
   最低というスペックで断念。
   ・回路設計はデータシートの熟読から。 スペックをちゃんと調べろ!

こんな接続をしたかった。 (ATmega328Pでは実績あり)
C52_20221005171201
ATtiny841のAREFピンからちゃんと1.1Vは出てきました。
これを低オフセット電圧のオペアンプでバッファします。
しかし、この出力を「神様抵抗」のプラス側としてA/D変換
すると・・・オープンにしても(10kΩでAREF電圧につながる)
フルスケールの「1023」が出てこないのです。
5つのチャンネルとも、計算値より低い値が出るのです。
フルスケールに近い電圧の高い所だと2~3。
電圧が低い所だと、3~4、出てくるA/D値が小さいのです。

全体に下へシフトしてる感じです。
  ※ATmega328Pではこんなことには
   ならなかった・・・

そこで、外部から基準電圧を供給してみることに
しました。
  ※基準電圧を出力するAREF端子。
   ノイズ低減のためにパスコンを入れる
   ためのもの。この電圧出力を利用
   できるとは、データシートには記され
   てない。
  ※ATtiny85や84では2.0Vが最低電圧
   という制限あり。
C53
そうすると大丈夫に。
  ※完璧に誤差がないというわけではく
   そこそこエエ感じ。

ということで、
 「同じシリーズのチップでうまいこと行った
  経験があっても、始めて使うチップは、何が
  あるかわからへんでぇ」
と、あたりまえの結果に。

ダイソーのプラケースに入れるとこんな感じ。
Cc1_20221005171601

  ※L型ダブルのピンヘッダーが無かったんで、
   オムロンのツメ付ヘッダーを切断改造。

| | コメント (0)

2022年10月 3日 (月)

サーミスタ103JTで計った温度をアナログ出力

失敗談、2022年9月28日:回路設計はデータシートの熟読から。 スペックをちゃんと調べろ!
この発端の製作物です。

「温度→電圧」変換はLM35やTMP35あたりのセンサーが
便利なんですが、ブツに接触させて温度を測るときにちょいと
問題があるんです。
 ・形状のせいか応答が遅い。(TO-92パッケージ)
 ・足から熱が逃げるのか、低い目の温度が出る

  ※型番、LM35は、昔はナショセミ。今はTIに。
   そしてTMP35はアナログ・デバイセズに。
   ややこしいぞ。

応答が早くて間違いなくブツに接触できる温度センサーという
ことで、「セミテックのサーミスタ103JT」を使って、
 ・センサーの抵抗値から温度を算出。
 ・温度をD/A出力。
という方法で、アナログ値を得るようにしました。
  ※単純なことをするのに、むちゃたいそうな回路。

Tn85_thm1

ATtiny85を選んだんがそもそも失敗。
でも、作りかけたんで、完成形に仕上げてみました。

測定温度範囲は0~80℃。
10bit分解能のA/Dで温度を得てますんで、分解能は
ざっと0.1℃。
これを0.0V~4.0Vの電圧に変換します。
1℃あたり0.05Vのスケールです。

AVR studioでプログラムを組んでます。
フォルダーごと圧縮。
   ・ダウンロード - thm_85_da1.zip

10bitのA/D値から2kバイトのテーブルで温度に変換
という手抜き手法。
複雑な算術関数は使っていません。

回路を組み込んだプラケースはダイソーの「ミニケース5個組」
M31
M32

この回路の電源は、
Arduino-UNO 12bit×4chアナログ SDカードデータロガー完成形
から供給します。

さて、温度センサーIC:LM35(TMP35)とサーミスタ103JTの
違い、どんなもんかと調べますと・・・

15Ωのホーロー抵抗の側面にセンサーをくっつけて、
抵抗を通電して発熱させます。
その時の温度変化を電圧として記録しました。

M11_20221003101301

M12_20221003101301

まずは、センサーを軽く貼り付けした状態から。
M13_20221003101301
すると・・・
Cap004_20221003101401
センサーICとサーミスタの出力に差が生じます。
時間が経過してもそれが埋まりません。

M14_20221003101301
テープでぐるぐる巻きにして、温度が逃げないように
したつもりでも・・・
Cap005_20221003101401
途中で段ボール箱をかぶせましたが、時間が経っても
2℃くらいの差が埋まりません。

そこで、2017年11月6日:液晶表示器「焼き鈍し」
で使った「保温箱」で試してみました。
この2種以外にも、センサーICとサーミスタを加えます。
M21
2と4が今回のセンサー。
1はセンサーICの外周を熱収縮チューブで覆っています。
4は同じ103JTですが、先端近くまで熱収縮チューブが来ています。

センサー部分はむき出しのまま、電線のところでマスキングテープを
巻いてまとめました。

65℃で保温された箱の中に投入しますと、数分で4つとも温度が
安定、収束します。
  ※電線部分、およそ20cmは保温箱の中に入っていて、
   先端のセンサー部と同じ温度になっているかと。
Cap006_20221003103401

また、4つのセンサーを箱に入れたまま、箱を室温
から加熱した場合も20分ほどで65℃に到達し、
その後の温度差はそれほど感じません。

Cap007_20221003103401
  赤:保温箱温度制御センサー
  緑:サーミスタ 4
  青:サーミスタ 3
  マゼンタ:センサIC 2
  シアン: センサIC 1

発熱体に直接接触させて温度を測る場合と、周囲の空気ごと
温度を測るということで差が生じるのでしょう。

このあたりをもうちょい勉強しようとすれば、何を
調べれば良いでしょうか?

| | コメント (6)