AVRtiny13Aでつくるシリーズ2 LMT01を使った温度計
- 2016/03/30 10:50
- カテゴリー:make:
秋月の少し前の新製品に,TIのLMT01という温度センサICが登場しました。面白そうだったのでそんなに安くはなかったのですが,買ってみました。
デジタル式の温度センサなのに,わずか2端子で電源の供給から測定値の送信までやってしまうという,なんだかよく分からないセンサなのですが,使いやすさ向上のためにピン数を減らして行く過程で,いつのまにやらピン数を減らすことが目的にすり替わり,使い勝手はむしろ悪くなっている事に気が付かないという,賢い人がよく陥る罠を地で行っているICだと思います。
まあ,本来の目的そっちのけで,意地になって違う道を追い続けるという姿勢は,私は嫌いではありません。
このIC,測定値をパルスの数にして,消費電流の変化で出力します。考えましたね,消費電流の変化とは。
元々の消費電流が極めて小さいので,抵抗を挟んで電源を供給しても動いてくれます。その抵抗の両端に発生する電圧をつかまえれば,パルスの数を数えることができますね。でも,数を数えるといっても,どこからどこまでの数を数えればいいのやら。
もしここで,勢いでLMT01を買ってしまい,途方にくれている人がいらっしゃったら,データシートを横目で見つつ,読み進めて下さい。
LMT01は気温を測る温度センサICです。出力はデジタルで,2ピンの小さなICの中に温度センサとA-Dコンバータ,そしてシリアルインターフェースが入っています。どんな風にやってるかは知りませんが,リニアリティの補正も行われているので,出てくるデジタルデータをそのまま使う事ができます。
その上,測定温度範囲が広く-50度から150度(おいおいシリコンの破壊限界じゃないか),しかも精度が他に比べて良好で,-20度から90度までなら誤差はわずか±0.5度です。電圧出力のセンサICではないので,A-Dコンバータが必要ありませんし,A-D変換時の誤差を考えなくて済みます。
デジタル出力のセンサですので,データは電圧でも電流でも抵抗でもなく,数字で出てきます。電源とデータ出力を兼ねるという変態っぷりを実現したのは,その特徴的なシリアル通信によってですが,これ,クロックが別にないだけではなく,スタートもエンドもわからないなんて,なんちゅう無茶をやるんだか。
私は最初,温度に応じた頻度でパルスが出る程度の物だろうから,ローパスフィルタで平均化して電圧値に変換すればいいか,位に考えていました。けど,それだったら,デジタル値で出力されるというこのセンサのメリットを生かせません。別にサーミスタでもいいという話です。
考え方としては,測定1回分のデータは,決まった時間の中で出てくるという事が基本になります。かつ,パルスの周期は温度に無関係で一定です。
このICは25度くらいで1200くらいのパルスを送って来ますが,温度の変化でこれが500になっても2000になっても,1回分の時間はほぼ同じです。パルスの周期も同じですから,あいた時間はパルスが出てこない期間となります。
なら,こうしましょう。とにかくパルスの数を数えます。パルス何周期分かの時間を待って,前後のパルスの数を比べてみましょう。もし変わっていればパルスが出続けている,つまりデータの転送中ということになりますから,まだ数え続けます。
もし,前後で値が変わっていなければ,もうパルスは出ていないという事ですから,転送は終了ということになります。すなわちその値が,測定値です。
僅かな電流の変化を,ほんの僅かな電圧変化に変換してマイコンに突っ込むわけですが,ここにはトランジスタによるIV変換を使う事が,データシートでは紹介されています。しかし,コンパレータを内蔵するマイコンなら,直結が可能です。幸い,tiny13Aにはコンパレータがあります。
コンパレータで電圧変化を掴んで,変化があれば割り込みで変数をカウントし数を数えます。しばらく待ってから数を比較してやればいけそうです。
そして,その値を摂氏に変換しないといけません。データシートでは,(パルスの数/16-50)になるということです。すごいですね,ちゃんとリニアリティの補正もすんでいます。
ですから,パルス数が1234だったら,27.125度ということになるわけです。お,小数点が出てきましたね。面倒くさそうです。
LCDは前回と同じデジットの8桁14セグのLCDを使います。
//
// LMT01 test for tiny13
// LCD : Digit 14seg type
// Clock : 9.6MHz internal
//
#include <avr/io.h>
#include <stdlib.h>
#include <util/delay.h>
#include <avr/pgmspace.h>
#include <avr/interrupt.h>
#define F_CPU 9600000UL
#define LE _BV(PB0) // _LE
#define DIN _BV(PB2) // DIN
#define SCK _BV(PB3) // SCK
#define MIN_SW PINB4
#define HOUR_SW PINB5
unsigned char tx_buf[18];
unsigned char digit_buf[8];
const PROGMEM int seg_pat[] = {
// 0 1 2 3 4 5 6 7 8 9
0x003f, 0x1200, 0x00db, 0x00cf, 0x00e6, 0x00ed, 0x00fd, 0x1401, 0x00ff, 0x00ef,
// SPC , -
0x0000, 0x2000, 0x00c0};
const PROGMEM unsigned char bit_pos0[14] = {
86, 85, 84, 83, 82, 80, 81, 1, 0, 6, 5, 4, 3, 2};
void LCD_Disp(void);
void LCD_Clear(void);
void LCD_Convert(void);
volatile int count = 0;
int LASTcount = 0;
// Analog Comparator Interrupt
ISR(ANA_COMP_vect) {
count++;
}
int main(void)
{
unsigned char i;
DDRB = 0b11001101; // PB4 = Input
PORTB = 0b00110000; // PB4 = pull-up
ADCSRB = 0b00000000;
ACSR = 0b01001010;
DIDR0 = 0b00000010;
for (i=0 ; i<8 ; i++){
digit_buf[i] = 10;
}
digit_buf[4] = 11;
sei();
while(1){
if ( count != 0){
while (count != LASTcount){
LASTcount = count;
_delay_ms(2);
}
LASTcount = LASTcount * 10 / 16 - 500;
if (LASTcount < 0){
digit_buf[1] = 12;
LASTcount = abs(LASTcount);
}else{
digit_buf[1] = 10;
}
digit_buf[5] = LASTcount % 10;
LASTcount /= 10;
digit_buf[3] = LASTcount % 10;
LASTcount /= 10;
digit_buf[2] = LASTcount % 10;
LASTcount /= 10;
LCD_Clear();
LCD_Convert();
LCD_Disp();
count = 0;
LASTcount = 0;
}
}
}
void LCD_Disp(void)
{
int i, j;
unsigned char k;
for (i=142 ; i>=0 ; i--){
PORTB &= ~SCK; //SCK = Low
j = i/8;
k = i%8;
if ((tx_buf[j] & (1<<k)) != 0)
{
PORTB |= DIN;
}
else
{
PORTB &= ~DIN;
}
_delay_us(1);
PORTB |= SCK; //SCK =High
_delay_us(1);
}
_delay_us(1);
PORTB |= LE;
_delay_us(1);
PORTB &= ~LE;
}
void LCD_Clear(void)
{
unsigned char i;
for (i=0 ; i<=17 ; i++) {
tx_buf[i] = 0;
}
}
void LCD_Convert(void)
{
int p, r, s;
int pattern, tx_bit_pos;
unsigned char digit_num, seg_num;
for (digit_num=0 ; digit_num<=7 ; digit_num++){
pattern = pgm_read_word(&seg_pat[digit_buf[digit_num]]);
for (seg_num=0 ; seg_num<=13 ; seg_num++){
p = pattern & 1; // LSB of pattern
pattern /= 2; // shift right
tx_bit_pos = pgm_read_byte(&bit_pos0[seg_num]);
tx_bit_pos += digit_num * 7;
r = tx_bit_pos / 8;
s = (1<<(tx_bit_pos % 8)) * p;
tx_buf[r] = ((tx_buf[r])& ~s) | s;
}
}
}
えー,LCDにまたしてもプログラムの大半がLCDのドライブ用です。メインはちょっとです。
前述の通り,割り込みで数を数え,少し前の数と比較して変わっていなければ転送終了と判断し,その値を測定値として採用するというものです。
実はここからが結構tiny13Aには荷が重いのです。まず摂氏への変換ですが,データシート通りにやるとfloat型を使う事になり,リソースを無駄に使ってしまいます。
そこで,LMT01の精度である0.5度をサポートするに十分なものとして,小数点以下は一桁だけ使う事に決めて,パルスの数を10倍し,オフセットとして500を引くことにしました。これならint型になるとはいえ,float型を使うことはなくなります。
実はこれ,後でもっと裏技を使う事になるのですが,それは後日。
次にマイナス表示です。温度計ですから,当然マイナスになることもあり得ます。0以下になったら絶対値を取ってマイナスを付けるという処理でよいのですが,問題はこの絶対値です。試行錯誤をしたのですが,算術ライブラリをインクルードしてabs関数を使ってもそんなに大してメモリも増えないので,黙って使う事にしました。
これについても,後日盛大な割り切りが・・・
これで一応完成です。使ってみて思ったのですが,このLMT01というICは非常に巧みな方法で,2端子で高精度な温度センサを成立させていますが,パルスのカウントミス,つまり通信エラーの発生については,測定値のズレという形で見えるようになります。
そのズレが,実際の温度と大きくずれていればわかりますが,そんなにそんなに大きくずれないのが普通ですので,通信エラーが起きているかどうかが,はっきりいってわかりません。
特に,割り込みで数を数えていますが,多重割り込みがかかったケースなどでカウントのミスをしたことに気が付かないというのは,気持ちが悪いです。
ですが,そこを気にするようなものでもないと考え至れば,温度の測定などは大雑把で良いのだと気が付きます。そもそも小数点以下1桁ですし。
あと,動作確認の途中で,指でつまむと温度が下がるという現状に悩まされました。それまで25度だったものが,指でつまむとみるみる値が減った行き,24度前半になるのです。
きっと計算間違いだ,あるいはカウントミスだ,と慌てたのですが,何のことはない,原因は私の指先が,25度以下になってしまっていたのでした。
何が言いたいかと言えば,温度なんてのは,そのくらいの話だという事です。そのくらいの話の割には,3度上がったり下がったりしただけで,我々人間は不快になったり体調を崩したりするのですから,敏感なんだか鈍感なんだか,よく分からないものです。
さて,次はTCXOを使った時計です。以前,Si5351Aで32.678kHzを作り,これで時計を作りましたが,CPUのクロックとしてTCXOで10MHzを作って突っ込んでやり,これを数えて時計を作れば解決と自嘲気味に書いたことがありますが,まさにそれです。