PICマイコン(PIC16F1827)でSHT35のデータを取得

組み込みエンジニア

こんにちは、ENGかぴです。

PIC16F1827はI2C(MSSP)通信機能を持っており温湿度センサーであるSHT35-DISから測定データを取得することができます。MCCによるI2C通信の実装例としてSHT35-DISからデータを取得しLCDに温湿度を表示しました。

温湿度センサーはSHT35-DISモジュール(秋月電子)を使用し、LCDはAQM1602XA-RN-GBW(秋月電子)を使用しています。

SHT35-DISのデータを取得する

SHT35-DISはSENSIRION社の高精度温湿度センサーです。マイコンとの通信方式はI2Cであり測定レンジが-40℃から+125℃と広いのが特徴です。PICマイコンからコマンドを送信しSHT35-DISからデータを取得する方法をまとめます。

System Moduleの設定

最初に共通事項であるSystem Moduleの設定を行います。MPLAB X IDEを起動しMCCのアイコンをクリックしてMCCを有効にします。

System Moduleの設定(MCC)
System Moduleの設定(MCC)

内部クロックを使用するためINTOSCを選択しています。クロック周波数は任意でもよいですが4MHz_Hzを選択しています。クロックを高速にするほど消費電流が増えてしまいます。他にも設定項目はありますがクロックの設定のみとしています。

I2C通信を実装する

I2C通信をMCCで実装しLCDに表示する方法については下記記事を参考にしてください。今回はMSSP2にSHT35-DIS用のI2Cを追加します。

PICマイコン(PIC16F1827)のI2C通信を実装する

PICマイコンのI2C通信はMSSP機能に含まれています。Device Resources内のPeripherals欄のMSSP2を選択して追加します。

MSSP2の設定(MCC)
MSSP2の設定(MCC)

MSSP2を選択しSerial ProtocolをI2Cを選択するとModeやClockの設定画面が表示されます。PICマイコンがSHT35-DISに対してコマンドを送信するためMasterとなります。通信速度は初期値の100kHzとします。

コマンドを送信する

引用:SHT3x-DISデータシート(シングルショットモード)
引用:SHT3x-DISデータシート(シングルショットモード)

SHT35-DISはコマンドを16ビット長になるように送信(Write)する必要があります。MCCの関数I2C2WriteNByte()を使用してコマンドを送信します。

#define SLAVE_ADRS 0x45 //ADRオープン時のアドレス
uint8_t singleshot[2] = { 0x2C, 0x06}; //シングルショットモードでクロックストレッチ有効

I2C2_WriteNBytes(SLAVE_ADRS, &singleshot[0], sizeof(singleshot)); //コマンドを送信

シングルショットモード(繰り返し精度レベル:高)をクロックストレッチを有効にする場合は0x2c06がコマンドになります。

クロックストレッチを有効にすると測定中にSCLの信号レベルがLOWに維持されるためSCLがHighに復帰するタイミングで測定が完了したことが確認できます。

I2C2_WriteNBytes()の第1引数にスレーブのアドレス、第2引数に送信する値を格納しているアドレス、第3引数に送信するサイズを指定します。

測定データを取得する

uint8_t rxdata[6];

I2C2_ReadNBytes(SLAVE_ADRS, &rxdata[0], sizeof(rxdata) ); //測定データをリードする

MCCの関数I2C2_ReadNBytes()を使用して測定データを取得します。I2C2_ReadNBytes()の第1引数にスレーブのアドレス、第2引数に受信した測定データを格納するアドレス、第3引数に受信したいデータサイズを指定します。

温度データ16ビットと温度データのCRC、湿度データ16ビットと湿度データのCRCの合計6バイトを受信します。CRC計算の条件がデータシートに記載されています。

引用:SHT3x-DISデータシート(CRCの条件)
引用:SHT3x-DISデータシート(CRCの条件)

CRC-8の計算方法は初期値を0xFFとして1バイト目のバイトデータのXORを計算します。この値を8ビット分左にシフトしていき最上位ビットが立った時にPolynomialとXORをとり2バイト目の初期値とします。2バイト目も同様にしてシフトとXORを繰り返していき対象のデータのバイト数だけ繰り返します。

/* CRC8計算関数 */    
uint8_t Crc8Calc(uint8_t *data, uint8_t sz ){
    uint8_t crc = 0xFF;
    uint8_t i,j;
    
    for( i = 0; i < sz; i++){
        crc ^= *data;
        
        for( j = 0; j < 8; j++ ){
            if( crc & 0x80 ){
               crc = ( crc << 1 ) ^ POLYNOMIAL;
            }
            else{
               crc = crc << 1;
            }
        }
        ++data;
    }
    
    return crc;
}

SHT35-DISから受信した測定データのCRCの計算結果と取得したCRCの結果を比較して一致したとき有効なデータとして採用します。

測定データの換算

SHT35-DISから受信した測定データは16ビットの符号なしの整数型の数値ですが温湿度に換算する必要があります。測定データの換算方法はデータシートに記載されています。

引用:SHT3x-DISデータシート(温湿度データの換算)
引用:SHT3x-DISデータシート(温湿度データの換算)

換算式を見ると小数点以下が発生するためfloatを使用したり小数点以下を商で表すために100倍値を使用したりすることが必要です。例ではFLOATを使用する場合と100倍値を使用した場合の2通りを示しています。

uint16_t tempHex;
uint16_t humiHex;

    tempHex = ((uint16_t)rxdata[0] << 8) | rxdata[1];
    humiHex =((uint16_t)rxdata[3] << 8) | rxdata[4];

    #ifdef FLOAT_ENABLE
        temp = (tempHex / 65535.00) * 175 - 45;
        humi = (humiHex / 65535.0) * 100.0;
    #else
       //floatの場合処理が重たいので以下で代用
       //65535でなく65536で割っているため誤差がでるが
       //処理速度優先のためシフト演算とした。
       temp = ((uint32_t)tempHex*175*100 >> 16 ) - 45*100;
       humi = ((uint32_t)humiHex*100*100 >> 16 );
    #endif

PIC16F1827は演算器を持っていないためfloatの処理を行うとプログラム領域を多く使用してしまい処理が重たくなってしまいます。処理を軽くするためにシフト演算による計算を行っています。また小数点以下の2桁以下がシフトしたときに桁落ちしたいようにあらかじめ100倍値を取っています。

シフト演算をするため換算式の分母である216-1が厳密でなくなるため僅かに誤差となってしまいます。

正確な温湿度を取得したい場合はお勧めできませんが室温を測定する場合は微々たる誤差であるため問題ないと思います。

動作確認

PIC16F1827とLCD、SHT35-DISをI2C通信接続して動作確認を行います。SHT35-DISからデータを取得したデータをLCDに表示します。

動作確認用の回路

PIC16F1827のI2C通信の動作確認回路
PIC16F1827のI2C通信の動作確認回路

I2C通信にはプルアップ抵抗が必要ですがLCDやSHT35モジュールにはプルアップ抵抗が実装されています。電源を投入すると初期値として温度と湿度の値として00.00を表示します。5秒毎にSHT35-DISからデータを取得しLCDに温湿度を表示します。

電源投入時にLCDの動作が安定しないことがあるためLCDの初期化を行う前に安定時間確保のためウェイトを置いています。ピン設定は以下の通りです。

PIC16F1827のピン設定(MCC)
PIC16F1827のピン設定(MCC)

今回はIC2を2つ使用していますが、スレーブアドレスが異なればお互いに干渉しないため配線を共通化しても問題ありません。共通化する際にはプルアップ抵抗について一方を使用しないようにするかなど検討が必要です。

動作結果

SHT35-DISの動作確認
SHT35-DISの動作確認

電源を投入から測定開始するまでに00.00を表示していますが、5秒後に温度と湿度がLCDに表示できていることを確認しました。

温度は室温計とほぼ同じであり湿度は雨天だったので妥当かなと感じています。換算式はシフト演算で行っているため誤差がありますが室温計とほぼ同じ値であったため問題にならない誤差だと感じています。

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。チェックはしておりますがコメントなど細かな部分で間違っている可能性があります。参考としてお使いいただければと思います。

#include "mcc_generated_files/mcc.h"
#include "mcc_generated_files/examples/i2c1_master_example.h"
#include "mcc_generated_files/examples/i2c2_master_example.h"

#define TIME_OFF -1         //タイマーを使用しない場合
#define TIME_UP 0           //タイムアップ
#define TIME_SHT35_MAX 100  //ベースタイマカウント値
#define TIME_OUT_MAX    20  //SHT35の通信タイムアウト

#define SLAVE_ADRS 0x45
#define POLYNOMIAL 0x31

#define SLAVE_ADRS_LCD 0x3E//0x3E 0x7C
#define LINE1_ADRS 0x40
#define LINE2_TOP (0x40 +0x80)
#define FUNC1_SET 0x38
#define FUNC2_SET 0x39
#define INT_OSC 0x14
#define CONST_SET 0x77 //0x73
#define PWR_ICON_SET 0x54 //0x56
#define FOLLOWER_SET 0x6C
#define CLR_DISP 0x01
#define DISP_ONOFF_SET 0x0C
#define LCD_TIM_MAX 500

//#define FLOAT_ENABLE

typedef enum{
    SHT35_MEASURE = 0,
    SHT35_WAIT,
    SHT35_READ,
    SHT35_CHK,        
    SHT35_MAX  
}SHT35_MODE;

/* 変数宣言 */
uint8_t singleshot[2] = { 0x2C, 0x06};
int16_t timSht35start;
int8_t  timSht35Out = TIME_OFF;
int16_t timlcd;
SHT35_MODE mode = SHT35_MEASURE;
uint8_t rxdata[6];
uint16_t tempHex;
uint16_t humiHex;
uint8_t chksum[2];
uint8_t disptbl[2][16] ={"temp:   20.00deg","humi:   50.00%  "};

#ifdef FLOAT_ENABLE
float temp;
float humi;
#else
int32_t temp; //100倍値で表現する
uint32_t humi; //100倍値で表現する
#endif

/* 関数プロトタイプ宣言 */
void mainApp(void);
void mainTimer(void);
uint8_t Crc8Calc(uint8_t *data, uint8_t sz );
void LcdInit(void);
void DispSet(void);
void DspClear(void);

/* Main application */
void main(void)
{
    SYSTEM_Initialize();
    TMR0_SetInterruptHandler(mainTimer);
    INTERRUPT_GlobalInterruptEnable();
    INTERRUPT_PeripheralInterruptEnable();
    timSht35start = TIME_SHT35_MAX;
    LcdInit();

    while (1)
    {
        mainApp();
    }
}

/* タイマ管理関数 */
void mainTimer(void){
    
    if( timSht35start > TIME_UP ){
        --timSht35start;
    }
    
    if( timSht35Out > TIME_UP ){
        --timSht35Out;
    }
    
    if( timlcd > TIME_UP ){
        --timlcd;
    }
}
/* メイン処理関数 */
void mainApp(void){
    
    switch(mode){
        case SHT35_MEASURE:
            if( timSht35start == TIME_UP ){
                timSht35start = TIME_OFF;
                timSht35Out = TIME_OUT_MAX;
                I2C2_WriteNBytes(SLAVE_ADRS,&singleshot[0],sizeof(singleshot));
                mode = SHT35_WAIT;
            }
            break;
        case SHT35_WAIT:
            if( RB5_GetValue() == 1){
                mode = SHT35_READ;
            }
            break;
        case SHT35_READ:   
            I2C2_ReadNBytes(SLAVE_ADRS, &rxdata[0], sizeof(rxdata) );
            mode = SHT35_CHK;
            break;
        case SHT35_CHK:
            chksum[0] = Crc8Calc(&rxdata[0],2); //tempのCRCチェック
            chksum[1] = Crc8Calc(&rxdata[3],2); //humiのCRCチェック
            
            if( chksum[0] == rxdata[2] && chksum[1] == rxdata[5]){
                tempHex = ((uint16_t)rxdata[0] << 8) | rxdata[1];
                humiHex =((uint16_t)rxdata[3] << 8) | rxdata[4];

                #ifdef FLOAT_ENABLE
                    temp = (tempHex / 65535.00) * 175 - 45;
                    humi = (humiHex / 65535.0) * 100.0;
                #else
                    //floatの場合処理が重たいので以下で代用
                    //65535でなく65536で割っているため誤差がでるが
                    //処理速度優先のためシフト演算とした。
                    temp = ((uint32_t)tempHex*175*100 >> 16 ) - 45*100;
                    humi = ((uint32_t)humiHex*100*100 >> 16 );
                #endif              
            }
            
            timSht35start = TIME_SHT35_MAX;
            timSht35Out = TIME_OFF;
            mode = SHT35_MEASURE;
            break;
    }
    
    if( timSht35Out == TIME_UP ){
        timSht35Out = TIME_OFF;
        mode = SHT35_MEASURE;
    }
    
    if( timlcd == TIME_UP ){
        timlcd = LCD_TIM_MAX;
        DispSet(); //LCD表示部分をセット
    }     
}
/* CRC8計算関数 */    
uint8_t Crc8Calc(uint8_t *data, uint8_t sz ){
    uint8_t crc = 0xFF;
    uint8_t i,j;
    
    for( i = 0; i < sz; i++){
        crc ^= *data;
        
        for( j = 0; j < 8; j++ ){
            if( crc & 0x80 ){
               crc = ( crc << 1 ) ^ POLYNOMIAL;
            }
            else{
               crc = crc << 1;
            }
        }
        ++data;
    }
    
    return crc;
}
/* LCDの初期化 */            
void LcdInit(void){
    
    __delay_ms(100); //LCDクロックなど安定時間のウェイト
    I2C1_Write1ByteRegister(SLAVE_ADRS_LCD, 0x00, FUNC1_SET );
    __delay_us(40);
    I2C1_Write1ByteRegister(SLAVE_ADRS_LCD, 0x00, FUNC2_SET );
    __delay_us(40);
    I2C1_Write1ByteRegister(SLAVE_ADRS_LCD, 0x00, INT_OSC );
    __delay_us(40);    
    I2C1_Write1ByteRegister(SLAVE_ADRS_LCD, 0x00, CONST_SET );
    __delay_us(40);    
    I2C1_Write1ByteRegister(SLAVE_ADRS_LCD, 0x00, PWR_ICON_SET );
    __delay_us(40);
    I2C1_Write1ByteRegister(SLAVE_ADRS_LCD, 0x00, PWR_ICON_SET );
    __delay_us(40);    
    I2C1_Write1ByteRegister(SLAVE_ADRS_LCD, 0x00, FOLLOWER_SET );
    __delay_ms(250);
    I2C1_Write1ByteRegister(SLAVE_ADRS_LCD, 0x00, FUNC1_SET );
    __delay_us(40);
    
    DspClear();
    
    I2C1_Write1ByteRegister(SLAVE_ADRS_LCD, 0x00, DISP_ONOFF_SET );
    __delay_us(40);
}    
/* LINE2へのカーソル移動処理*/
void DspLine2Top(void){
    
    I2C1_Write1ByteRegister(SLAVE_ADRS_LCD, 0x00, LINE2_TOP );
    __delay_us(40);    
}
/* ディスプレイをクリア */
void DspClear(void){
  
    I2C1_Write1ByteRegister(SLAVE_ADRS_LCD, 0x00, CLR_DISP );
    __delay_us(40);
}

/* LCDに表示するデータの生成 */
void DispSet(void){
    uint8_t i;
    bool nega;
    uint8_t disp10000;
    uint8_t disp1000;
    uint8_t disp100;
    uint8_t disp10;
    uint8_t disp1;
    
    DspClear();
    
    if( temp < 0 ){
        temp = abs(temp);
        nega = true;
    }
    
    disp10000 = temp / 10000;
    disp1000 = ( temp % 10000) / 1000; 
    disp100 = (( temp % 10000) % 1000 ) / 100; 
    disp10 = ( temp % 100 ) / 10;
    disp1 = ( temp % 100 ) % 10;
    
    if( disp10000 == 0){
        if( nega == true){
            disptbl[0][7] = '-';
        }
        else{
            disptbl[0][7] = ' ';
        }
    }
    else{
        disptbl[0][7] = (uint8_t)disp10000 + 0x30;
    }
    
    disptbl[0][8] = (uint8_t)disp1000 + 0x30;    
    disptbl[0][9] = (uint8_t)disp100 + 0x30;
    disptbl[0][10] = '.';
    disptbl[0][11] = (uint8_t)disp10 + 0x30;
    disptbl[0][12] = (uint8_t)disp1 + 0x30;    
    
    for( i = 0; i < sizeof(disptbl[0]); i++ ){
        I2C1_Write1ByteRegister(SLAVE_ADRS_LCD,LINE1_ADRS,disptbl[0][i]);
    } 
    
    DspLine2Top();
    
    disp10000 = humi / 10000;
    disp1000 = ( humi % 10000) / 1000; 
    disp100 = (( humi % 10000) % 1000 ) / 100; 
    disp10 = ( humi % 100 ) / 10;
    disp1 = ( humi % 100 ) % 10;   
    
    if( disp10000 == 0){
        disptbl[1][7] = ' ';
    }
    else{
        disptbl[1][7] = (uint8_t)disp10000 + 0x30;
    }
    
    disptbl[1][8] = (uint8_t)disp1000 + 0x30;    
    disptbl[1][9] = (uint8_t)disp100 + 0x30;
    disptbl[1][10] = '.';
    disptbl[1][11] = (uint8_t)disp10 + 0x30;
    disptbl[1][12] = (uint8_t)disp1 + 0x30;      
    
    for( i = 0; i < sizeof(disptbl[1]); i++ ){
        I2C1_Write1ByteRegister(SLAVE_ADRS_LCD,LINE1_ADRS,disptbl[1][i]);
    } 
}
/**
 End of File
*/

本ソースコードはMPLAB X IDEにMCCのプラグインをインストールしていないと使用できません。

関連リンク

PICマイコンを使ってマイコンのレジスタの設定やMPLAB X IDEのプラグインであるMCCを使用して動作確認したことについてまとめています。

PICマイコン(PIC12F675)で実現できる機能と解説リンクまとめ

PICマイコン(PIC16F1827)で実現できる機能と解説リンクまとめ

あなたの市場価値を見いだす転職サイト【ミイダス】無料会員登録

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました