こんにちは、ENGかぴです。
Raspberry Pi PicoでArduinoのWireライブラリを使用するとI2C通信を実装することができます。Wireを実装して温湿度センサーであるAHT21Bはから温湿度データを取得し換算する方法をまとめました。
Raspberry Pi Pico(以下Picoとする)と拡張基板のGrove Shield for Pi Picoを使用しています。温湿度センサーはAHT21Bスタンダードモデル(秋月電子で購入)を使用しています。
同様の確認を下記記事で行っています。
Seeeduino XIAOのWireでDHT20の温湿度を取得
Seeeduino XIAOをPicoに置き換えてWireライブラリの使用方法をまとめています。AHT21BはGrove Temperature&Humidity Sensor(DHT20モジュール)に使用されているセンサーなのでDHT20モジュールと同等な方法でデータが取得できます。
Wireライブラリの使用方法
AHT21BはASAIR製の湿度と温度のセンサーを備えたモジュールです。AHT21Bモジュールの通信方式はI2CなのでWireライブラリで温湿度データを取得することができます。Wireのメンバー関数を赤文字で表記します。
PR:わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
Wireライブラリの初期化
#include <Wire.h>
void setup() {
Wire.begin();
}
最初に「Wire.h」をインクルードします。begin()関数でI2Cの条件を初期化してスタートします。PicoのWireはデフォルトでSDAが4ピン(GP4)、SCLが5ピン(GP5)になります。
引数にスレーブアドレスを指定するとスレーブとして動作を開始しますが、指定しない場合はマスターとして動作を開始します。PicoからAHT21Bに対してデータを要求するのでPicoがマスターになります。
データ送信の例
/* AHT20Bへコマンド送出 */
uint8_t Aht21bSetData(uint8_t* cmd, uint8_t len){
uint8_t ret;
Wire.beginTransmission(0x38);
Wire.write(cmd,len);
ret = Wire.endTransmission(); //ストップ・コンディションの発行
}
Wireでデータ送信する例を示します。最初にbeginTransmisson()でスレーブアドレスを指定し初期化を行います。
write()関数で送信するデータをセット(一時保存のキューイング)を行います。第1引数に送信するデータのアドレスを指定します。第2引数は送信するサイズを指定します。
endTransmission()関数で生成したデータを送信します。送信に成功すると戻り値が0になるので送信の状況の確認に使用することができます。
データ受信の例
/* AHT20Bからデータを取得 */
bool Aht21bGetData(uint8_t reg_adr, uint8_t* data, uint8_t len){
bool ret = false;
if( reg_adr != NULL ){
Wire.beginTransmission(0x38); //スタート・コンディションの発行
Wire.write(reg_adr); //書き込む対象のアドレスをセット(ライトで指定)
Wire.endTransmission(); //ストップ・コンディションの発行
}
if( Wire.requestFrom(0x38, len) == len ){
Wire.readBytes(data,len);
ret = true;
}
return ret;
}
Wireでデータ受信する例を示します。スレーブアドレスを指定した後にレジスタを指定して読み込む場合は送信と同様の処理を行ってレジスタを指定します。(5~9行目)
AHT21Bから温湿度データを読み込む場合はレジスタを指定する必要がないため5~9行目を処理せず11行目以降の処理でデータを読み込みます。
requestFrom()関数でデータを取得します。第1引数にスレーブのアドレスを指定し、第2引数に読み込むデータのサイズを指定します。
requestFrom()関数の戻り値は読み込んだデータ数になるため指定したサイズと一致したことを確認し、一時保存したデータをreadBytes()関数で読み込んで変数に格納します。第1引数に読み込んだデータを格納するアドレス、第2引数に読み込むサイズを指定します。
AHT21Bからデータを取得する
AHT21Bは電圧範囲がDC2.2V~5.5Vと幅広いことや湿度と温度のデータが20ビットデータで高精度で測定できることが特徴です。データシートに記載されている手順に従って温湿度データを取得する手順について説明します。
- 手順1センサーの状態確認(初期化時)
電源ONから100ms経過後、0x71を送信してセンサーの状態を確認する。
0x18であれば手順3に進む0x18以外であれば0x1B、0x1C、0x1Eレジスタを初期化する。 - 手順2レジスタの初期化(初期化時)
Seeed Studioに公開されているソースコードで確認した0xbaコマンドでレジスタを初期化して手順3に進む。
- 手順3測定開始コマンドの送信
10ms経過後0xACコマンドを送信する。0xACコマンドに続けて0x33、0x00を送信する。
- 手順4測定待ちと測定状態の確認
測定に80ms必要なので待機しますが、測定完了はスレーブアドレスから1バイト目のstateの8ビット(bit7)で確認する。bit7が0であれば測定完了なので手順5に進む。
- 手順5CRCの計算
stateを含めた湿度と温度データまでの6ビットに対してCRC計算を行い最後尾に付与されたCRCと一致するかをチェックする。一致したら手順6に進む。
- 手順6湿度と温度のデータを換算する
取得したデータを20ビットのデータで整理して換算式を使って湿度と温度のデータに換算する。再度測定する場合は手順3に戻る
頻繁なデータ測定は計算に伴う消費電流の増加など内部で発生した熱の影響を受けるため最低でも2秒おきに測定データを取得することが推奨されています。
手順2ですが、データシートにはウェブサイトで初期化ルーチンを確認となっていますが見当たりませんでした。
以下ではコマンドの送信例と手順に沿ったデータの取得方法の例を説明します。また取得したデータの健全性を確認するCRC8の計算方法について説明します。
PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
測定をモードで管理する
uint8_t trig[] = { 0xAC, 0x33, 0x00 };
uint8_t readData[7];
switch(mode){
case AHT21B_MEASURE: //手順3
Aht21bSetData( &trig[0], sizeof(trig)); //測定開始トリガの送信
mode = AHT21B_WAIT;
break;
case AHT21B_WAIT: //手順4
if( Aht21bGetData(NULL, &readData[0], sizeof(readData)) ){
if( (readData[0] & 0x80 ) == 0){ //stateのbit[7]の状態を確認する
mode = AHT21B_CHK;
}
else{
mode = AHT21B_MEASURE;
}
}
break;
case AHT21B_CHK: //手順5,6
Datachk(); //CRCをチェックして一致するとデータを換算する
mode = AHT21B_MEASURE;
break;
}
データ取得は手順3から手順6までのモードを切り替えて管理します。
- AHT21B_MEASURE(手順3)
- AHT21B_WAIT(手順4)
- AHT21B_CHK(手順5、手順6)
1.AHT21B_MEASUREは測定開始トリガ(0xAC, 0x33, 0x00 )を送信し、次のモード(手順4)に進みます。
2.AHT21B_WAITは計測が完了しているかを確認します。トリガ開始から80ms以上経過すると計測完了状態になりますが、state情報のbit[7]ビットが0であるかを確認(11行目)して次のモード(手順5)に進みます。
3.AHT21B_CHKは取得したデータのCRCを計算して受信したCRCと一致するか確認(手順5)します。CRCが健全であればデータを受け入れて湿度と温度に換算(手順6)します。
PR:企業で求められる即戦力技術を身に付ける テックキャンプエンジニア転職
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;
}
CRC-8の計算方法は初期値と1バイト目のバイトデータのXORを計算します。この値を8ビット分左にシフトしていきますが、最上位ビットが立った時に1ビット分シフトしてからPolynomialとXORをとります。
2バイト目以降は前のバイトの結果を初期値として8ビット分シフトとXORを繰り返していき対象のデータのバイト数だけ繰り返します。
測定データの最後尾にstateデータから温度データまでの6バイトデータに対するCRCが付加されています。付加されたCRCと受信したデータからCRC計算した結果が一致した場合に有効なデータとして採用します。
PR:
わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジムPython入門講座の申込
測定データの換算

AHT21Bのデータシートを確認すると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はそのまま加算します。
uint32_t humid;
uint32_t temp;
humid = ( readData[1] << 12 ) + ( readData[2] << 4 )
+ ( readData[3] >> 4 ) ;
temp = ( (readData[3] & 0x0F) << 16 ) + ( readData[4] << 8 )
+ ( readData[5] );
4byteデータへの置き換えの例を示しています。humidとtempは次の換算式におけるSRHとSTになります。

データシートによると上記の式によって換算すると湿度と温度の値になります。
float tempdata;
float humiddata;
humiddata = ((float)humid / (1 << 20)) * 100; //湿度
tempdata = ((float)temp / (1 << 20))*200 - 50; //温度
4byteデータに構成した湿度と温度のデータを換算します。小数点以下を表示するためfloatの型にキャストしてから220(1を左に20回シフト)で割っています。
動作確認

Picoを拡張基板のGrove Shield for Pi Picoに挿入します。Wire(I2C)はデフォルトでGP4(I2C0 SDA)とGP5(I2C0 SCL)を使用します。Wire(I2C)を使用する場合、プルアップ抵抗が必要ですがAHT21Bスタンダードモデルに実装されているため不要です。
電源をONすると「Raspberry Pi Pico」「Arduino IDE」「AHT21B START!」をシリアルモニターに表示した後、5秒間隔での測定とデータの取得を行います。シリアルモニタの表示を確認するとデータが取得できていることが分かりました。

温度(Temperature)は部屋に置いている温湿度計によると26.5℃、湿度(Humidity)は49.5%でした。結果がほぼ一致しているため湿度と温度が取得できていることが確認できました。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
ソースコード全体
ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。
#include <Wire.h>
#define PIN_DO1 25
#define SLAVE_ADRS 0x38
#define STATUS_CHK 0x71
#define POLYNOMIAL 0x31
#define RESET_REG_ADDR 0xba
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define TIME_AHT21B_WAIT 10 //計測ウェイト
#define TIME_AHT21B_MAX 500 //計測ウェイト
#define TIME_OUT_MAX 1100 //通信タイムアウト
#define LED_ONOFF 50
typedef enum{
AHT21B_MEASURE = 0,
AHT21B_WAIT,
AHT21B_CHK,
AHT21B_MAX
}AHT21B_MODE;
typedef enum{
INIT_CHK = 0,
INIT_RESET,
INIT_END,
INIT_MAX
}INIT_MODE;
uint32_t beforetimCnt = millis();
float temp;
float humi;
int16_t timAht21bstart = TIME_AHT21B_WAIT;
int16_t timAht21bOut = TIME_OFF;
int16_t timAht21binit;
int16_t timled = LED_ONOFF;
AHT21B_MODE mode = AHT21B_MEASURE;
INIT_MODE initmode;
uint8_t trig[] = { 0xAC, 0x33, 0x00 };
uint8_t readData[7];
int8_t ledcnt;
/* プロトコル宣言 */
void InitAht21b(void);
bool Aht21bGetData(uint8_t reg_adr, uint8_t* data, uint8_t len);
uint8_t Aht21bSetData(uint8_t* cmd, uint8_t len);
uint8_t Crc8Calc(uint8_t *data, uint8_t sz );
void mainApp(void);
void mainTimer(void);
void setup() {
pinMode( PIN_DO1, OUTPUT);
Serial.begin(115200);
Wire.begin();
delay(3000);
Serial.println("Raspberry Pi Pico");
Serial.println("Arduino IDE");
Serial.println("AHT21B START!");
while( initmode != INIT_MAX ){
InitAht21b();
mainTimer();
}
}
void loop() {
mainTimer();
mainApp();
}
/* タイマ管理 */
void mainTimer(void){
if ( millis() - beforetimCnt > BASE_CNT ){
beforetimCnt = millis();
if( timAht21bstart > TIME_UP ){
--timAht21bstart;
}
if( timAht21bOut > TIME_UP ){
--timAht21bOut;
}
if( timAht21binit > TIME_UP ){
--timAht21binit;
}
if( timled > TIME_UP ){
--timled;
}
}
}
/* 初期化 */
void InitAht21b(void){
uint8_t buf;
uint8_t status;
switch (initmode){
case INIT_CHK:
if( timAht21binit == TIME_UP ){
timAht21binit = TIME_AHT21B_WAIT;
if( Aht21bGetData(STATUS_CHK, &status, 1) ){
Serial.print("status:");
Serial.println(status);
if( status != 0x18 ){
initmode = INIT_END;
//電源OFFしても以前の計測状態が残っていれば0x18以外になるため不要
timAht21binit = TIME_AHT21B_WAIT;
}
else{
initmode = INIT_MAX;
Serial.println("Init OK");
}
}
else{
Serial.println("slave-err");
while(1);
}
}
break;
case INIT_RESET:
if( timAht21binit == TIME_UP ){
timAht21binit = TIME_AHT21B_WAIT;
buf = RESET_REG_ADDR;
if( Aht21bSetData( &buf, 1) == 0 ){
initmode = INIT_END;
}
}
break;
case INIT_END:
if( timAht21binit == TIME_UP ){
timAht21binit = TIME_AHT21B_WAIT;
initmode = INIT_MAX;
}
break;
default:
break;
}
}
/* メイン処理関数 */
void mainApp(void){
if( timled == TIME_UP ){
timled = LED_ONOFF;
++ledcnt;
if( ledcnt % 2 ){
digitalWrite(PIN_DO1, HIGH);
}
else{
digitalWrite(PIN_DO1, LOW);
}
}
switch(mode){
case AHT21B_MEASURE:
if( timAht21bstart == TIME_UP ){
timAht21bstart = TIME_AHT21B_WAIT;
timAht21bOut = TIME_OUT_MAX;
Aht21bSetData( &trig[0], sizeof(trig)); //測定開始トリガの送信
mode = AHT21B_WAIT;
}
break;
case AHT21B_WAIT:
if( timAht21bstart == TIME_UP ){
timAht21bstart = TIME_AHT21B_WAIT;
if( Aht21bGetData(NULL, &readData[0], sizeof(readData)) ){
if( (readData[0] & 0x80 ) == 0){ //stateのbit[7]の状態を確認する
mode = AHT21B_CHK;
}
else{
mode = AHT21B_MEASURE;
}
}
}
break;
case AHT21B_CHK:
if( timAht21bstart == TIME_UP ){
timAht21bstart = TIME_AHT21B_MAX;
timAht21bOut = TIME_OFF;
Datachk();
mode = AHT21B_MEASURE;
}
break;
}
if( timAht21bOut == TIME_UP ){
timAht21bOut = TIME_OFF;
mode = AHT21B_MEASURE;
timAht21bstart = AHT21B_MEASURE;
}
}
/* AHT21Bへコマンド送出 */
uint8_t Aht21bSetData(uint8_t* cmd, uint8_t len){
uint8_t ret;
Wire.beginTransmission(SLAVE_ADRS);
Wire.write(cmd,len);
ret = Wire.endTransmission(); //ストップ・コンディションの発行
}
/* AHT21Bからデータを取得 */
bool Aht21bGetData(uint8_t reg_adr, uint8_t* data, uint8_t len){
bool ret = false;
if( reg_adr != NULL ){
Wire.beginTransmission(SLAVE_ADRS); //スタート・コンディションの発行
Wire.write(reg_adr); //書き込む対象のアドレスをセット(ライトで指定)
Wire.endTransmission(); //ストップ・コンディションの発行
}
if( Wire.requestFrom(SLAVE_ADRS, len) == len ){
Wire.readBytes(data,len);
ret = true;
}
return ret;
}
/* CRCチェックと湿度と温度データに換算 */
void Datachk(void){
uint32_t temp;
uint32_t humid;
float tempdata;
float humiddata;
uint8_t crc;
crc = Crc8Calc( &readData[0],6);
if( crc == readData[6]){
humid = ( (uint32_t)readData[1] << 12 )
+ ( (uint32_t)readData[2] << 4 )
+ ( (uint32_t)readData[3] >> 4 );
temp = ( ((uint32_t)readData[3] & 0x0F) << 16 )
+ ( (uint32_t)readData[4] << 8 )
+ ( (uint32_t)readData[5] );
humiddata = ((float)humid/(1 << 20)) * 100;
tempdata = ((float)temp/(1 << 20)) * 200 - 50;
Serial.print("Tempareture: ");
Serial.print(tempdata);
Serial.print("℃ ");
Serial.print("Humidity: ");
Serial.print(humiddata);
Serial.println("%");
}
}
/* 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;
}
本記事ではPicoでWireライブラリの使用方法を説明するため送信例や受信例をまとめました。AHT21BはDHT20モジュールに使用されているため、Arduino環境でDHT20ライブラリを追加して使用する方法もあります。下記記事にDHT20ライブラリを追加して実装した例をまとめています。
ESP32-WROOM-32Eで温湿度データをUDP通信する
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
PR:無料トライアル実施中【PC専用】AIスライド資料作成ツールの利用:イルシル
最後まで、読んでいただきありがとうございました。
電源ONして測定を行った後、電源をOFFせずにリスタートすると0x71レジスタの値が0x18以外になります。AHT21Bを購入して初めて電源をONした場合でも0x71レジスタが0x18になるため、確認せずにデータ取得の手順に進んでも問題ないと考えています。