こんにちは、ENGかぴです。
PIC16F1827はI2C(MSSP)通信機能を持っており湿度&温度センサーであるDHT20モジュールから測定データを取得することができます。MCCによるI2C通信の実装例としてDHT20モジュールからデータを取得しLCDに表示しました。
DHT20モジュールはGrove Temperature&Humidity Sensor(DHT20)(Seeed Studio製:秋月電子で購入)を使用しています。LCDはAQM1602XA-RN-GBW(秋月電子)を使用しています。
PIC16F1827で動作確認したことについてリンクをまとめています。
PICマイコン(PIC16F1827)で実現できる機能と解説リンクまとめ
PIC16F1827の設定
PIC16F1827の設定はMCCを使っています。MCCの使い方については下記記事にまとめています。
MPLAB Code Configurator(MCC)の追加と使い方
今回はMCCを使用してMSSP1及びTMR0の設定を行っています。
広告
PR:わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
System Moduleの設定
最初に共通事項であるSystem Moduleの設定を行います。MPLAB X IDEを起動しMCCのアイコンをクリックしてMCCを有効にします。
内部クロックを使用するためINTOSCを選択しています。クロック周波数は任意でもよいですが4MHz_HFを選択しています。クロックを高速にするほど消費電流が増えてしまいます。他にも設定項目はありますがクロックの設定のみとしています。
I2C通信を実装する
PICマイコンのI2C通信はMSSP機能に含まれています。Device Resources内のPeripherals欄のMSSP1を選択して追加します。
MSSP1を選択しSerial ProtocolをI2Cを選択するとModeやClockの設定画面が表示されます。PICマイコンがDHT20モジュールに対してコマンドを送信するためMasterとなります。通信速度は初期値の100kHzとします。
TMR0の設定
TMR0はDevice Resources内のPeripherals欄のTimerを選択しTMR0をクリックして追加します。
Timer Clock内でクロックに関する設定を行います。Clock Sourceは内部クロックを使用しているためFOSC(FOSC/4)を指定します。PrescalerはClock Sourceに対してTMR0のカウントアップする分周比を指定します。
Timer PeriodはTMR0がオーバーフローするタイミングを設定します。1ms毎にオーバーフロー割り込みが発生しますがCallback Function Rateを0x0A(10)を指定しているため10ms毎にコールバック関数が呼び出されます。
void mainTimer(void); //タイマー0割り込みのコールバック関数
//コールバック関数の使用例
TMR0_SetInterruptHandler(mainTimer); //コールバック関数を指定
例ではmainTimer()が10ms毎にコールされます。
DHT20モジュールからデータを取得する
DHT20モジュールは電圧範囲がDC2.2V~5.5Vと幅広いことや湿度と温度のデータが20ビットデータで高精度で測定できることが特徴です。PICマイコンからコマンドを送信しDHT20モジュールから湿度と温度のデータを取得する手順について説明します。
データを取得する手順
DHT20モジュールのデータシートの手順に従って測定とデータの取得を行います。
- 手順1センサーの状態確認(初期化時)
電源ONから100ms経過後、0x71を送信してセンサーからの応答が0x18であるかを確認し、手順2に進む。
※本記事では0x18の結果を問わず手順2に進むようにしています。 - 手順2測定開始コマンドの送信
10ms経過後0xACコマンドを送信する。0xACコマンドに続けて0x33、0x00を送信し、手順3に進む。
- 手順3測定待ちと測定状態の確認
測定に80ms必要なので待機しますが、測定完了はスレーブアドレスから1バイト目のstateの8ビット(bit7)で確認する。bit7が0であればデータを取得して手順4に進む。
- 手順4CRCの計算
stateを含めた湿度と温度データまでの6ビットに対してCRC計算を行い最後尾に付与されたCRCと一致するかをチェックする。一致したら手順5に進む。
- 手順5湿度と温度のデータを換算する
取得したデータを20ビットのデータで整理して換算式を使って湿度と温度のデータに換算する。再度測定する場合は手順2に戻る
手順1で0x18以外になった時データシートに初期化ルーチンに従って0x1B、0x1C、0x1Eレジスタを初期化するとありますが、Seeed Studioに公開されているソースコードで確認するしか手段がありませんでした。
手順2以降のデータの測定は計算に伴う消費電流の増加など内部で発生した熱の影響を受けるため最低でも2秒おきに測定データを取得することが推奨されています。
以下ではコマンドの送信例と手順に沿ったデータの取得方法の例を説明します。また取得したデータの健全性を確認するCRC8の計算方法について説明します。
測定をモードで管理する
switch(mode){
case DHT20_INIT: //手順1
//センサー状態の確認
mode = DHT20_MEASURE;
break;
case DHT20_MEASURE: //手順2
//測定開始トリガの送信
mode = DHT20_WAIT;
break;
case DHT20_WAIT: //手順3
//stateのbit[7]の状態を確認する
//bit[7]が0であればデータを読み込む
mode = DHT20_CHK;
break;
case DHT20_CHK: //手順4,5
//CRCをチェックして一致するとデータを換算する
Datachk();
mode = DHT20_MEASURE;
break;
}
DHT20モジュールのデータ取得の手順の手順1から手順5をモードで管理します。
- DHT20_INIT(手順1)
- DHT20_MEASURE(手順2)
- DHT20_WAIT(手順3)
- DHT20_CHK(手順4、手順5)
1.DHT20_INITは初期時のセンサーの情報を確認して次のモードに進みます。
2.DHT20_MEASUREは測定開始トリガを送信し、次のモードに進みます。
3.DHT_WAITはstateのbit[7]ビットが0であれば測定が完了しているためデータを取得して次のモードに進みます。
4.DHT20_CHKは一時保存したデータのCRCを計算して受信したCRCと一致するか確認します。CRCが健全であればデータを受け入れて湿度と温度に換算します。
測定開始トリガを送信する
uint8_t trig[] = { 0xAC, 0x33, 0x00};
I2C1_WriteNBytes(SLAVE_ADRS, &trig[0], sizeof(trig));
測定開始コマンドは0xACを送信します。0xACに続けてコマンドパラメータとして0x33と0x00を送信する必要があるため測定開始コマンド用の配列を準備します。
MCCが実装した関数であるI2C2_WriteNBytes()を使ってデータを送信します。第1引数にスレーブのアドレス、第2引数に送信する値を格納しているアドレス、第3引数に送信するサイズを指定します。測定が完了するまで80ms待つ必要があります
測定データを取得する
uint8_t rxdata[7];
//測定データをリードする
I2C2_ReadNBytes(SLAVE_ADRS, &rxdata[0], sizeof(rxdata) );
MCCが実装した関数I2C2_ReadNBytes()を使用して測定データを取得します。第1引数にスレーブのアドレス、第2引数に受信した測定データを格納するアドレス、第3引数に受信するデータサイズを指定します。
CRCチェックを行う
データシートに記載されているCRC8のチェックの多項式は以下の通りです。
CRC[7:0] = 1+X4+X5+X8
この多項式から標準値を求めます。X=2として計算するとCRCは0x131になります。CRC8はLSBから8ビットが対象となるためPolynomial(多項式)標準値は0x31になります。またデータシートにCRCの初期値に0xFFが指定されています。
#define POLYNOMIAL 0x31
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;
}
CRC8の計算方法は初期値と1バイト目のバイトデータのXORを計算します。この値を8ビット分左にシフトしていきますが、最上位ビットが立った時に1ビット分シフトしてからPolynomialとXORをとります。
2バイト目以降は前のバイトの結果を初期値として8ビット分シフトとXORを繰り返していき対象のデータのバイト数だけ繰り返します。
測定データの最後尾にstateデータから温度データまでの6バイトデータに対するCRCが付加されています。付加されたCRCと受信したデータからCRC計算した結果が一致した場合に有効なデータとして採用します。
広告
測定データの換算
DHT20のデータシートを確認するとstateデータに続けて湿度データが20bit、温度データが20bit配置されています。湿度と温度データを換算するために20bitのデータを4byteデータに構成し直します。
湿度データについて説明します。Humidity-1は20bitデータのMSBを含むデータなのでbit0から左に12回シフトして加算します。Humidty-2はbit4からbit11を構成するデータなのでbit0から左に4回シフトして加算します。Humidty-3はreadData[3]の上位4bitなので右に4回シフトして加算します。
温度データについて説明します。温度を構成するTemperature-1は20bitデータのMSBを含むデータですがreadData[3]の下位4bitを使用するため0x0Fで論理積をとったデータを使用します。Temperature-1はbit16からbit19までを構成するデータなのでbit0から左に16回シフトして加算します。Temperature-2はbit8からbit15を構成するデータなのでbit0から左に8回シフトして加算します。Temperature-3はそのまま加算します。
int64_t humid;
int64_t temp;
humid = ( (int64_t)readData[1] << 12 )
+ ( (int64_t)readData[2] << 4 )
+ ( (int64_t)readData[3] >> 4 );
temp = ( ((int64_t)readData[3] & 0x0F) << 16 )
+ ( (int64_t)readData[4] << 8 )
+ ( (int64_t)readData[5] );
データへの置き換えの例を示しています。4byteデータに置き換えてfloat演算でデータの換算を行うのが理想ですがPIC16F1827は演算器を持っていないためfloatを使用するとプログラム領域を圧迫し容量が不足してしまいます。
floatを使用せずに小数点以下2桁の表示ができるように100倍値で管理します。100倍値に変換したときにオーバーフローする可能性があるため4byteデータではなく8byteデータに置き換えます。humidとtempは次の換算式におけるSRHとSTになります。
データシートによると上記の式で換算すると湿度と温度の値が得られます。
int64_t humiddata;
int64_t tempdata;
humiddata = (humid*10000 >> 20); //湿度
tempdata = (temp*20000 >> 20) - 5000; //温度
100倍値で管理するため換算式も100倍になります。また100倍値で積算したときにオーバーフローする可能性があるためint64_t(8byte)データで換算しています。
換算式の220は1を左に20回シフトで計算できますが、換算式のように220で除算にする式を逆数で置き換えると1を右に20回シフトするのと同じになります。
湿度と温度の100倍値のデータを使ってLCD表示用のデータを生成します。
LCDの表示データを生成する
uint8_t disp1000;
uint8_t disp100;
uint8_t disp10;
uint8_t disp1;
humiddata = (humid*10000 >> 20);
disp1000 = ( humiddata % 10000) / 1000; //整数部2桁目
disp100 = (( humiddata % 10000) % 1000 ) / 100; //整数部1桁目
disp10 = ( humiddata % 100 ) / 10; //小数点以下1桁目
disp1 = ( humiddata % 100 ) % 10; //小数点以下2桁目
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;
湿度のデータをLCDのデータに変換します。湿度のデータを桁数に応じて文字変換します。除算の/と余り%を使って整数部と小数部の値を計算しています。
例)湿度のデータ(100倍値)が6754だった場合
disp1000を計算します。湿度のデータを10000で割った余りは6754になります。これを1000で割ると6になります。
disp100を計算します。湿度のデータを10000で割った余りの6754を1000で割った余りは754になります。これを100で割ると7になります。
disp10を計算します。湿度のデータを100で割った余りは54になります。これを10で割ると5になります。
disp1を計算します。湿度のデータを100で割った余りの54を10で割った余りは4になります。
例のようにして求めた値に0x30(アスキーコードで0)を加算して文字コードに置き換えることでLCDに表示するデータになります。
動作確認
PIC16F1827とLCD、DHT20モジュールをI2C通信接続して動作確認を行います。DHT20モジュールからデータを取得したデータをLCDに表示します。
動作確認用の回路
I2C通信にはプルアップ抵抗が必要ですがLCDやDHT20モジュールにプルアップ抵抗が実装されているためプルアップ抵抗は不要です。LCDとDHT20モジュールを同じI2Cに接続していますが、スレーブアドレスが異なるため混在して応答することはありません。
電源を投入し計測がスタートすると湿度と温度を表示します。5秒毎にDHT20モジュールからデータを取得してLCDに表示します。ピン設定は以下の通りです。
MSSP1に使用するSCL1とSDA1ピン以外はDOピンに設定しています。DHT20モジュールの待機時の信号レベルを初期化時にHighにするためStart Highにチェックしています。
動作結果
電源を投入から測定開始すると湿度(humi)と温度(temp)がLCDに表示されました。tempの単位は文字コードの関係でC(Celsius)にしています。5秒毎に温度と湿度を更新してLCDに表示できていることを確認しました。
湿度と温度は室内に設置している温湿度計とほとんど同じ結果だったのでうまく取得できていることが分かりました。
PR:スキマ時間を有効に!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
ソースコード全体
以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。
#include "mcc_generated_files/mcc.h"
#include "mcc_generated_files/examples/i2c1_master_example.h"
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define TIME_DHT20_MAX 500 //計測ウェイト
#define TIME_DHT20_WAIT 10 //計測ウェイト
#define TIME_OUT_MAX 1100 //通信タイムアウト
#define POLYNOMIAL 0x31
#define SLAVE_ADRS 0x38
#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
typedef enum{
DHT20_INIT = 0,
DHT20_MEASURE,
DHT20_WAIT,
DHT20_CHK,
DHT20_MAX
}DHT20_MODE;
uint8_t disptbl[2][16] ={"humi: 50.00 % ","temp: 20.00 C "};
uint8_t trig[] = { 0xAC, 0x33, 0x00};
int16_t timDht20start;
int16_t timDht20Out = TIME_OFF;
DHT20_MODE mode = DHT20_INIT;
uint8_t readData[7];
uint8_t dht20status;
void mainApp(void);
void mainTimer(void);
uint8_t Crc8Calc(uint8_t *data, uint8_t sz );
void LcdInit(void);
void DispSet(void);
void DspLine2Top(void);
void DspClear(void);
void Datachk(void);
/* Main application */
void main(void)
{
SYSTEM_Initialize();
TMR0_SetInterruptHandler(mainTimer);
INTERRUPT_GlobalInterruptEnable();
INTERRUPT_PeripheralInterruptEnable();
LcdInit();
while (1)
{
mainApp();
}
}
/* タイマ管理関数 */
void mainTimer(void){
if( timDht20start > TIME_UP ){
--timDht20start;
}
if( timDht20Out > TIME_UP ){
--timDht20Out;
}
}
/* メイン処理関数 */
void mainApp(void){
switch(mode){
case DHT20_INIT:
if( timDht20start == TIME_UP ){
timDht20start = TIME_DHT20_WAIT;
I2C1_Read1ByteRegister(SLAVE_ADRS, dht20status );
if( dht20status != 0x18 ){
//電源OFFしても以前の計測状態が残っていれば0x18以外になるため不要?
mode = DHT20_MEASURE;
}
else{
mode = DHT20_MEASURE;
}
}
break;
case DHT20_MEASURE:
if( timDht20start == TIME_UP ){
timDht20start = TIME_DHT20_WAIT;
timDht20Out = TIME_OUT_MAX;
I2C1_WriteNBytes(SLAVE_ADRS, &trig[0], sizeof(trig));
mode = DHT20_WAIT;
}
break;
case DHT20_WAIT:
if( timDht20start == TIME_UP ){
timDht20start = TIME_DHT20_WAIT;
I2C1_ReadNBytes(SLAVE_ADRS, &readData[0], sizeof(readData) );
if( (readData[0] & 0x80 ) == 0){
mode = DHT20_CHK;
}
else{
mode = DHT20_MEASURE;
}
}
break;
case DHT20_CHK:
if( timDht20start == TIME_UP ){
timDht20start = TIME_DHT20_MAX;
timDht20Out = TIME_OFF;
Datachk();
mode = DHT20_MEASURE;
}
break;
case DHT20_MAX:
//初期化に失敗した場合などの処理を追加するならここへ
break;
}
if( timDht20Out == TIME_UP ){
timDht20Out = TIME_OFF;
mode = DHT20_MEASURE;
timDht20start = DHT20_MEASURE;
}
}
/* CRC8計算関数 */
uint8_t Crc8Calc(uint8_t *data, uint8_t sz ){
int 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 (uint8_t)crc;
}
/* CRCチェックと湿度と温度データに換算 */
void Datachk(void){
int64_t temp;
int64_t humid;
//float tempdata;
//float humiddata;
int64_t humiddata;
int64_t tempdata;
uint8_t crc;
uint8_t i;
bool nega = false;
uint8_t disp10000;
uint8_t disp1000;
uint8_t disp100;
uint8_t disp10;
uint8_t disp1;
uint8_t cnt;
crc = Crc8Calc( &readData[0],6);
if( crc == readData[6]){
humid = ( (int64_t)readData[1] << 12 )
+ ( (int64_t)readData[2] << 4 )
+ ( (int64_t)readData[3] >> 4 ) ;
temp = ( ((int64_t)readData[3] & 0x0F) << 16 )
+ ( (int64_t)readData[4] << 8 )
+ ( (int64_t)readData[5] );
//humiddata = ((float)humid / (1 << 20)) * 100;
humiddata = (humid*10000 >> 20);
//tempdata = ((float)temp / (1 << 20))*200 - 50;
tempdata = (temp*20000 >> 20) - 5000;
//sprintf(disp1, "Humid: %d.%d %%", humi_i, humi_p);
disp10000 = humiddata / 10000;
disp1000 = ( humiddata % 10000) / 1000;
disp100 = (( humiddata % 10000) % 1000 ) / 100;
disp10 = ( humiddata % 100 ) / 10;
disp1 = ( humiddata % 100 ) % 10;
if( disp10000 == 0){
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;
DspClear();
for( i = 0; i < sizeof(disptbl[0]); i++ ){
I2C1_Write1ByteRegister(SLAVE_ADRS_LCD,LINE1_ADRS,disptbl[0][i]);
}
DspLine2Top();
//sprintf(disp2, "Temp: %d.%d%d C", temp_i, temp_p1,temp_p2);
if( tempdata < 0 ){
tempdata = abs(tempdata);
nega = true;
}
disp10000 = tempdata / 10000;
disp1000 = ( tempdata % 10000) / 1000;
disp100 = (( tempdata % 10000) % 1000 ) / 100;
disp10 = ( tempdata % 100 ) / 10;
disp1 = ( tempdata % 100 ) % 10;
if( disp10000 == 0){
if( disp1000 == 0 ){
cnt = 8;
}
else{
cnt = 7;
}
if( nega == true){
disptbl[1][cnt++] = '-';
}
else{
disptbl[1][cnt++] = ' ';
}
}
else{
cnt = 7;
disptbl[1][cnt++] = (uint8_t)disp10000 + 0x30;
}
if( disp1000 != 0 ){
disptbl[1][cnt++] = (uint8_t)disp1000 + 0x30;
}
disptbl[1][cnt++] = (uint8_t)disp100 + 0x30;
disptbl[1][cnt++] = '.';
disptbl[1][cnt++] = (uint8_t)disp10 + 0x30;
disptbl[1][cnt++] = (uint8_t)disp1 + 0x30;
for( i = 0; i < sizeof(disptbl[1]); i++ ){
I2C1_Write1ByteRegister(SLAVE_ADRS_LCD,LINE1_ADRS,disptbl[1][i]);
}
}
}
/* 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);
}
/* LCDの表示を2段目にセット */
void DspLine2Top(void){
I2C1_Write1ByteRegister(SLAVE_ADRS_LCD, 0x00, LINE2_TOP );
__delay_us(40);
}
/* LCDの表示をクリア */
void DspClear(void){
I2C1_Write1ByteRegister(SLAVE_ADRS_LCD, 0x00, CLR_DISP );
__delay_us(40);
}
/* End of File */
本ソースコードはMPLAB X IDEにMCCのプラグインをインストールしていることが前提となります。MCCをインストールする方法は下記記事を参考にしてください。
MPLAB Code Configurator(MCC)の追加と使い方
コンパイラーのバージョンが低い場合はうまく動作しないことがあります。XC8コンパイラー2.40で動作確認済みです。
関連リンク
PICマイコンを使ってマイコンのレジスタの設定やMPLAB X IDEのプラグインであるMCCを使用して動作確認したことについてまとめています。
PICマイコン(PIC12F675)で実現できる機能と解説リンクまとめ
PICマイコン(PIC16F1827)で実現できる機能と解説リンクまとめ
PR:無料トライアル実施中【PC専用】AIスライド資料作成ツールの利用:イルシル
最後まで、読んでいただきありがとうございました。
電源ONして測定を行った後で電源をOFFせずにリスタートすると0x71レジスタの値が0x18以外になります。電源をOFFして数秒経過してから0x71レジスタを確認すると0x18になるため、0x71レジスタの結果を問わず測定を開始しても問題ないと判断しています。