【PICマイコン】26円の温度計を作る(PIC16F1938,アキシャルサーミスタ204CT)

サーミスタの温度算出法

今回は,Theマイコンでできること的な温度計を作っていきます。



サーミスタとは?

サーミスタとは、温度によって抵抗が変わる素子のことを言います。例えば金属などは温度が上がると抵抗値が上がりますね。これを上手く検知してあげれば温度計になるわけです。

ただし、一般的に使われている材質は半導体です。温度の変化に対して抵抗値の変化が非常に大きいため、検出が簡単なのです。

アキシャルサーミスタ

ということで、今回は半導体で出来ているアキシャルサーミスタを使っていきます。

必要物品

今回使用するサーミスタの値段は何と、4本で100円。実際に一か所の温度計測には1本+抵抗1本でいいので、温度計のコストは26円となります。安い。

LM35DZという、よく使われている温度センサは一個100円ですので、もし「部屋中に温度計を配置する」という状況であればアキシャルサーミスタの方がいいでしょう(どんな状況だよ)

個人的にはサーミスタを使う方が電子工作している感があって好きです(お金がないからサーミスタを使っているなんて言えない)。

また、サーミスタの特徴として高い温度まで使用できることが挙げられます。上記のLM35DZでは100℃までしか使用できませんでしたが、アキシャルサーミスタは250℃まで使用できます。これで工場の排熱温度管理もばっちりですね。

温度と電圧の関係

冗談はさておき、これからサーミスタの抵抗値⇔温度変換をしていきます。

その前にまず、PICが何を検知できるのかを知っておきましょう。この後の項目で説明しますが、PICは電圧検知ができます。つまり、抵抗値⇔電圧の変換ができればOKということです。これは、以下の図のように配線すればできますね。

まず右側が電源(VDD)で、Vボルトの電圧です。これにサーミスタを繋ぎ、さらにその先に抵抗値Rを持つ抵抗を直列に繋ぎます。この抵抗はGNDに落とします。

ここで、中央の接合部の電圧Vxに注目してみましょう。サーミスタの抵抗値Rxは温度によって変わる変数なので、当然電圧降下ΔVは変わってきます。(電圧降下とは、ある素子に入る前と出た後の電圧ではどのくらい電圧が落ちたかを表すもので、ΔV=RIで表されます。)よって、

$$V_x = V – \Delta V$$

となりますね。ということで、この電圧降下分が分かればいいわけです。電圧降下はRxIですが、Rxが知りたい抵抗値(変数)なので現時点ではわかりません。しかし、IはRxを使ってこう表すことができます。

$$I = V/R_all = V/(R_x+R)$$

つまり、電圧降下分のRxI

$$\Delta V = R_xI = R_x * V/(R_x+R)$$

となるわけです。つまり、PICが感知できる電圧VxRxの関係は

$$V_x = V – \Delta V = V(1-\frac{-R_x}{R_x+R}) = V(\frac{R}{R_x+R}) \tag{1}$$

となります。簡単な計算でした。これで、サーミスタの抵抗値はPIC上でこれを計算してあげれば出ますね。

抵抗値と温度の関係

抵抗値が分かったので、これと温度の関係を見てみます。これが非常にややこしい。

$$T = \frac{1}{ln(R_x/R_0)/B+1/(T_0+273.15)}-273.15 \tag{2}$$

Tは温度(℃)で、Rxはサーミスタの抵抗値、R0はサーミスタの基準温度での抵抗値(今回使うサーミスタは25℃が基準)、BB定数T0は基準温度(25℃)です。

B定数というのはサーミスタの感度を表す数値らしいですが、そのままぶち込めばOKです。今回使用するサーミスタのB定数は3500でした。

また、今回使うサーミスタの基準温度の抵抗値R0は200kΩです(秋月電子の商品説明に書いてあります)。ということは、上式を今回使用するサーミスタに合わせると…

$$T = \frac{1}{ln(R_x/200k)/3500+1/(25.0+273.15)}-273.15 \tag{3}$$

となりますね。これをPIC内で計算してあげれば、そのサーミスタの温度が分かります!

電圧(Vx)の取得はどうすればいいの?

PICの電圧取得機能(AD変換)は1024段階で電圧を評価してくれます(後述あり)。この時に、電源電圧(VDD)を1024、GNDを0と評価するので、実際に私たちが得られるのはVxではなく、そのVDDとの比になります。…といっても意味不明でしょうから、式で表すと

$$ADvalue = \frac{V_x}{V}*1024 \tag{4}$$

ということです。ADvalueは私たちが読める変換された値です。ここで、後々便利なので

$$\frac{V}{V_x} = \frac{1024}{ADvalue} \tag{5}$$

と変換しておきましょう。ここで、(1)は次のように変換できます(唐突)

$$R_x = R*\frac{V}{V_x}-R = R(\frac{V-V_x}{V_x})=R(\frac{V}{V_x}-1) \tag{6}$$

この式の中に(5)式がありますね。これを代入すると、

$$R_x = R(\frac{V}{V_x}-1) = R(\frac{1024}{ADvalue}-1) \tag{7}$$

となります。私たちが得られるのはADvalueですから、これにぶち込むだけでRxが分かります。

これを(3)式に入れれば、温度Tが分かりますね!やっとADvalueと温度の対応が分かりました。では、ADvalueをどのように取得すればいいのでしょうか…

AD変換とは?

これには、AD変換という機能を使います。ADはAnalog/Digitalの略で、アナログな値(電圧)をデジタルな値(ADvalue)に変換する機能です。最近のPICには大体搭載されていて、データシートのピンにAN0などと書かれているピンはAD変換(Vxの検出)に使えます。

見てみると、AN13までありますね。数えてみると、全部で11個のピンがAD変換用に使うことができるようになってます。すごい。また、PIC16f1938のAD変換分解能は10bit、つまり1024段階で評価でき、

$$\Delta V_{min} = V_{DD}/1024 V = 4.88mV (V_{DD}=5Vの時)$$

より、判別できる最小の電位差は4.88mVとなります。結構細かいですね。世の中には12bitのAD変換器が搭載されているものもあり、これは1.22mVの電位差を検知できます。

AD変換の回路上の使い方は簡単で、電位を知りたいところにこのピンを繋げばいいだけです。

AD変換の簡単な流れ

AD変換の簡単な流れは

  1. AD変換に使うピンを入力に設定
  2. AD変換関連のレジスタを設定する
  3. AD変換を開始(レジスタに1を代入するだけ)
  4. AD変換値が格納されたレジスタの値を変数に入れる(終)

となります。レジスタの設定はほぼ共通なので、一度関数を作ってしまう方が楽です。

電圧取得関数を作る

AD変換はPICの機能なので、当然レジスタの設定をしなければなりませんが、ほぼ変わらないので、下記のコードをコピペでいいでしょう。(詳しい解説は他サイトなどを調べてみてください。)


    void PICinit(){
        OSCCON = 0b01110000;
        ADCS2 = 1;
        ADCS1 = 0;
        ADCS0 = 1;
        ADFM = 1;
        ADNREF = 0;
        ADCON1bits.ADPREF = 0;
        ANSELA = 0b00000001;
        ANSELB = 0b00000000;
        TRISA  = 0b00000001;
        TRISB  = 0b00000000;
        TRISC  = 0b00011000;
        PORTA  = 0b00000000;    //2進数で書いた場合
        PORTB  = 0x00;          //16進数で書いた場合
    }
    unsigned int adconvAX()     //サーミスタの電圧を10bitで取得する関数
    {
        ADCON0bits.CHS = 0;
        ADON = 1;	//AD変換ON
        __delay_us(20);
        GO = 1;	//AD変換待ち
        while(GO);	//AD変換終了まで待機
        return (ADRESH<<8) + ADRESL;
    }
  

ADCSレジスタはAD変換クロック(どのくらい時間をかけるか)を決めるものです。PICを8MHzで駆動する場合はこれでいいでしょう。

ADFM等はもはや魔法だと思えばいいです(データシートを調べれば意味が分かりますが、おそらくこの設定から変える機会はそうそう来ないでしょう)。

次に重要なのがANSELATRISAレジスタです。ANSELレジスタは「どのピンをAD変換に使うか」を設定するもので、

今回はAN0を使いたいので、ANSELA = 0b00000001;と、AN0だけオンにしました。ここで設定しなければ、そのピンはただのオンオフ用として使うことができます。

同様に、TRISAレジスタはピンの入力・出力をきめるもので、AD変換ピンは外から電流が流れ込むので、入力にしなければいけません。1を設定すると、そのピンが入力用になるので、TRISA = 0b00000001;としました。

他のAD変換ピン(AN13等)に対しても、全く同じように設定してあげればOKです。

次に、adconvAX関数について軽く説明(adconverter_Axial thermistorの略)。ADCON0レジスタのCHS~は以下のようになっています。

実は、AD変換は一度に一つのピンしか測れないのです。よって、AD変換を実施する直前に、どのピンの電圧を測るかをこちらで決めてあげましょう、というのがCHS~です。例えばAN3なら、CHSは00011ですから、ADCON0 = 00001100;とすればOKです。ここで、MPLABXIDEの便利機能を使ってみましょう。実はレジスタ名bits.と画面に打つと、そのレジスタの中身の名前候補を出してくれます。

この便利機能を使って、上のadconvAX関数の中身を書きました。今回はAN0しか使わないので、CHS = 0ですね。そして、この関数はAN0のピンにかかっている電圧を1024段階で返してくれます。これが、ADvalueです。

なので、


    ADvalue = adconvAX();
  

とするだけで電圧値Vxに対応するADvalueが得られます。(例えば、VDDが5V 、サーミスタが200kΩ(25℃)でRが200kΩならVxは2.5Vになります。この時のADvalueは1024*(2.5/5) = 512となります)

回路図

今回は、I2CLCDの使い方を参考に、温度をI2CLCDに表示してみます。

青く囲んだところが、前述画像のAD変換部分になっています。また、下にあるI2C接続のLCDはI2CLCDの使い方と同じつなぎ方です。

完成プログラム

上述のadconvAX()関数とPICinit()内のレジスタの設定に加え、ADvalueを温度に変換するこのような関数を作りました。


    double nowTemp(){           //現在の温度を返す関数
      const double BCONST = 3500;
      const double T0 = 298;  //K
      const double RESIST = 200000;  //Ω
      const double  RESIST0 = 200000; //Ω
      double AXresist = 0;      //サーミスタの抵抗値Rx
      double ADvalue;       //AD変換された値の格納用
      double ReciproTmp;    //温度の逆数(ケルビン単位)
      double tmp;           //温度(℃)

      for(int i=0;i<50;i++){       //ADvalueを50回積算
          ADvalue += adconvAX();
      }
      ADvalue = ADvalue/51;        //50で割って平均値を算出
      AXresist = RESIST * (1024/ADvalue - 1); //サーミスタの抵抗値算出
      ReciproTmp = log(AXresist / RESIST0) / BCONST + 1 / T0; //温度の逆数算出(K)
      tmp = 1 / ReciproTmp - 273.15;  //温度の算出
      return tmp;
    }
  

ごちゃごちゃしてますが、これは前述の計算式をそっくりそのまま書いただけです。途中のfor文で、ADvalueを50回積算して平均値を出しています。これは、AD変換に若干のばらつきがあるためです。ただし、積算回数を増やすと当然計測時間が延びるので、適当に積算回数を変えてみてください。

また、途中でReciproTmp(Reciproは逆数の意)を導入してますが、これはコードを見やすくするために分けただけです。その次の行のtmpの式に代入してみると、(3)式になります。

次に、温度を取得→LCDに表示するまでの流れを1つにした関数も作りました。


    static void showTemp()                       //LCDにtimerの値、温度を表示する関数
    {

        double tmp = nowTemp();                       //tmpに現在の温度をセット
        int tmpInt = tmp;           //tmpIntにtmpの整数部分をセット
        int tmpDec = (tmp-tmpInt)*10;       //tmpDecにtmpの小数部分をセット

        char tmpstr[16];            //表示文字用配列
        sprintf(tmpstr,"Temp:%d.%d",tmpInt,tmpDec);       //tmpstrに文字データをセット
        LCD_str(tmpstr);                                //LCDにtmpstrを表示

        writeCommand(0x02);              //LCDの1文字目へ移動
    }
  

コメントが各行についているので、それを見ていただければわかるかと思います。ここで、sprintf関数が出てきました。あまり見かけない関数ですね。これは、第一引数の配列(正確にはポインタ)に第二引数を文字列として代入するという関数です。printfと同じように、%d等を用いて変数を代入できます。ここでは、Temp:23.4のように表示されることになりますね。これをLCD_strで表示しておしまいです。

全体コード(コピペで温度計完成)

これらをすべて組み込んだ温度計のプログラムがこちらです。


      // PIC16F1938 Configuration Bit Settings

      // 'C' source line config statements

      // CONFIG1
      #pragma config FOSC = INTOSC    // Oscillator Selection (INTOSC oscillator: I/O function on CLKIN pin)
      #pragma config WDTE = OFF       // Watchdog Timer Enable (WDT disabled)
      #pragma config PWRTE = OFF      // Power-up Timer Enable (PWRT disabled)
      #pragma config MCLRE = OFF      // MCLR Pin Function Select (MCLR/VPP pin function is digital input)
      #pragma config CP = OFF         // Flash Program Memory Code Protection (Program memory code protection is disabled)
      #pragma config CPD = OFF        // Data Memory Code Protection (Data memory code protection is disabled)
      #pragma config BOREN = OFF      // Brown-out Reset Enable (Brown-out Reset disabled)
      #pragma config CLKOUTEN = OFF   // Clock Out Enable (CLKOUT function is disabled. I/O or oscillator function on the CLKOUT pin)
      #pragma config IESO = OFF       // Internal/External Switchover (Internal/External Switchover mode is disabled)
      #pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is disabled)

      // CONFIG2
      #pragma config WRT = OFF        // Flash Memory Self-Write Protection (Write protection off)
      #pragma config VCAPEN = OFF     // Voltage Regulator Capacitor Enable (All VCAP pin functionality is disabled)
      #pragma config PLLEN = OFF// PLL Enable (4x PLL disabled)
      #pragma config STVREN = ON      // Stack Overflow/Underflow Reset Enable (Stack Overflow or Underflow will cause a Reset)
      #pragma config BORV = LO        // Brown-out Reset Voltage Selection (Brown-out Reset Voltage (Vbor), low trip point selected.)
      #pragma config LVP = ON         // Low-Voltage Programming Enable (Low-voltage programming enabled)

      // #pragma config statements should precede project file includes.
      // Use project enums instead of #define for ON and OFF.

      #include <xc.h>
      #include <pic16f1938.h>
      #include <math.h>
      #include <stdio.h>
      #include <stdlib.h>

      #define _XTAL_FREQ 8000000
      #define LCD_ADD 0x7C
      #define Bconst 3500

      char moji[] = "Hello, PIC World!";
      char moji2[] = "Wak-tech";

      void I2C_Master_Init(const unsigned long c)
      {
        SSPCON1 = 0b00101000;
        SSPCON2 = 0;
        SSPADD = (_XTAL_FREQ/(4*c))-1;
        SSPSTAT = 0b00000000 ;    // 標準速度モードに設定する(100kHz)
      }

      void I2C_Master_Wait()
      {
        while ((SSPSTAT & 0x04) || (SSPCON2 & 0x1F));
      }

      void I2C_Master_Start()
      {
        I2C_Master_Wait();
        SEN = 1;
      }

      void I2C_Master_RepeatedStart()
      {
        I2C_Master_Wait();
        RSEN = 1;
      }

      void I2C_Master_Stop()
      {
        I2C_Master_Wait();
        PEN = 1;
      }

      void I2C_Master_Write(unsigned d)
      {
        I2C_Master_Wait();
        SSPBUF = d;
      }
      void writeData(char t_data){
          I2C_Master_Start();
          I2C_Master_Write(LCD_ADD);
          I2C_Master_Write(0x40);
          I2C_Master_Write(t_data);
          I2C_Master_Stop();
          __delay_ms(10);
      }
      void writeCommand(char t_command){
          I2C_Master_Start();
          I2C_Master_Write(LCD_ADD);
          I2C_Master_Write(0x00);
          I2C_Master_Write(t_command);
          I2C_Master_Stop();
          __delay_ms(10);
      }
      void PICinit(){
          OSCCON = 0b01110000;
          ADCS2 = 1;
          ADCS1 = 0;
          ADCS0 = 1;
          ADFM = 1;
          ADNREF = 0;
          ADCON1bits.ADPREF = 0;
          ANSELA = 0b00000001;
          ANSELB = 0b00000000;
          TRISA  = 0b00000001;
          TRISB  = 0b00000000;
          TRISC  = 0b00011000;
          PORTA  = 0b00000000;    //2進数で書いた場合
          PORTB  = 0x00;          //16進数で書いた場合
      }


      void LCD_Init(){            //LCDの初期化
        I2C_Master_Init(100000);
        __delay_ms(400);
        writeCommand(0x38);
        __delay_ms(20);
        writeCommand(0x39);
        __delay_ms(20);
        writeCommand(0x14);
        __delay_ms(20);
        writeCommand(0x73);
        __delay_ms(20);
        writeCommand(0x52);
        __delay_ms(20);
        writeCommand(0x6C);
        __delay_ms(250);
        writeCommand(0x38);
        __delay_ms(20);
        writeCommand(0x01);
        __delay_ms(20);
        writeCommand(0x0C);
        __delay_ms(20);
      }

      void LCD_str(char *c) {     //LCDに配列の文字を表示
        unsigned char i,wk;
        for (i=0 ; ; i++) {
          wk = c[i];
          if  (wk == 0x00) {break;}
          writeData(wk);
        }
      }

      unsigned int adconvAX()     //サーミスタの電圧を10bitで取得する関数
      {
          ADCON0bits.CHS = 0;
          ADON = 1;	//AD変換ON
          __delay_us(20);
          GO = 1;	//AD変換待ち
          while(GO);	//AD変換終了まで待機
          return (ADRESH<<8) + ADRESL;
      }

      double nowTemp(){           //現在の温度を返す関数
        const double BCONST = 3500;
        const double T0 = 298;  //K
        const double RESIST = 200000;  //Ω
        const double  RESIST0 = 200000; //Ω
        double AXresist = 0;      //サーミスタの抵抗値Rx
        double ADvalue;       //AD変換された値の格納用
        double ReciproTmp;    //温度の逆数(ケルビン単位)
        double tmp;           //温度(℃)

        for(int i=0;i<50;i++){       //ADvalueを50回積算
            ADvalue += adconvAX();
        }
        ADvalue = ADvalue/51;        //50で割って平均値を算出
        AXresist = RESIST * (1024/ADvalue - 1); //サーミスタの抵抗値算出
        ReciproTmp = log(AXresist / RESIST0) / BCONST + 1 / T0; //温度の逆数算出(K)
        tmp = 1 / ReciproTmp - 273.15;  //温度の算出
        return tmp;
      }
      static void showTemp()                       //LCDにtimerの値、温度を表示する関数
      {

          double tmp = nowTemp();                       //tmpに現在の温度をセット
          int tmpInt = tmp;           //tmpIntにtmpの整数部分をセット
          int tmpDec = (tmp-tmpInt)*10;       //tmpDecにtmpの小数部分をセット

          char tmpstr[16];            //表示文字用配列
          sprintf(tmpstr,"Temp:%d.%d",tmpInt,tmpDec);       //tmpstrに文字データをセット
          LCD_str(tmpstr);                                //LCDにtmpstrを表示

          writeCommand(0x02);              //LCDの1文字目へ移動
      }

      int main(void){
        PICinit();      //PICを初期化
        LCD_Init();       //LCDを初期化
        writeCommand(0x01); //画面をクリア
        __delay_ms(20);      //LCD処理待ち
        writeCommand(0x02); //ホームへカーソル移動
        __delay_ms(2); // LCD側の処理待ち

        while(1){
            showTemp();       //LCDに温度を表示
            __delay_ms(1000); //1秒おきに更新
        }
        return 0;
      }

    

完成した例(動画)

上記プログラムを書き込み、回路を作るとこのように温度が1秒おきに表示されます。

この動画では、始めは気温が28℃ですが、指をサーミスタに当てると温まることが分かりますね。温度計の出来上がりです。

温度計の精度・より詳しい原理

このサイトでは簡単に作れる(コピペ)ことを重視しているので、詳しい原理に関してはここでは書きません。B定数のことや精度のことが気になった方は、「セッピーナの趣味の天文計算」という私が温度計を初めて作った時に大変参考にさせていただいたサイトを見てみてください。詳しく書かれています。






「【PICマイコン】26円の温度計を作る(PIC16F1938,アキシャルサーミスタ204CT)」に7件のコメントがあります

  1. ピンバック: Wak-tech » RGBLEDの色を温度によって変える

  2. 吉田純造

    プログラム全部が乗っていればコピーできるのだが
    初心者には どこをどのように貼り付けるのかがわかりません

    make[2]: *** [build/default/production/newmain.p1] Error 1
    make[1]: *** [.build-conf] Error 2
    make: *** [.build-impl] Error 2

    BUILD FAILED (exit value 2, total time: 5s)

    1. このページは2ページに分かれておりまして、2ページ目に全プログラムが記載されています。分かりにくくてすみません。

    1. ご指摘誠にありがとうございます。
      お恥ずかしながら、サーミスタを5個入りだと勘違いして20円だと思い込んでいました。文中には4本と書いているのに…。
      26円に訂正いたしましたのでよろしくお願いします。

  3. タカネザワタクム

    a/d変換の部分とても参考になりました!
    これからも閲覧させていただこうと考えています。

    1. わくてく

      コメントありがとうございます!
      参考になったようで何よりです。これからもお願いします。

コメントする

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です