こんにちは、ENGかぴです。
VSCodeの拡張機能であるPico SDKを使用するとRaspberry Pi PicoにI2C通信を実装することができます。I2C通信を使ってSHT40Iから温湿度を取得してシリアルモニターに表示する方法をまとめました。
本記事はPico SDKを使ってC/C++のプロジェクトを作成してI2Cを実装します。
Raspberry Pi Pico(以下Picoとする)と拡張基板のGrove Shield for Pi Picoを使用しています。SHT40IはAE-SHT40I(秋月電子)を使用しています。
VSCodeのダウンロードとインストールの方法やVSCodeにPicoの開発環境を追加する方法は下記記事を参考にしてください。
Raspberry Pi Picoの開発をVSCodeで行う方法
Picoを使用してArduino IDEやVSCodeで動作確認したことをまとめています。
I2C通信を実装する
AE-SHT40IはSENSIRION社の高精度温湿度センサーSHT4xl-Digitalを実装したモジュールです。SHT4xI-Digital(以下SHT40とする)は相対湿度と温度を様々な精度で測定できる産業用のセンサーです。
温度の測定レンジが-40℃から+125℃であり幅広い測定が可能です。また内蔵ヒータにより結露するような環境においても対応できるように設計されています。
通信方式はI2C通信であり、複数のアドレスに対応して使用することができます。Picoから測定コマンドを送信し、データを読み込むことでSHT40から温湿度のデータを取得できます。
PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
プロジェクトの生成
プロジェクトの生成方法は下記記事にまとめています。
Raspberry Pi Picoの開発をVSCodeで行う方法
FeaturesのI2C interfaceにチェックを入れてサンプルコードを含めてプロジェクトを生成します。

プロジェクトを生成するとデフォルトでI2C関連の初期化と定義が自動で生成されます。

取得した温湿度をシリアルモニターに表示するため、Stdio supportのConsole over USB(disables other USB use)にチェックを入れます。
PR:(即戦力のスキルを身に着ける:DMM WEBCAMP 学習コース(はじめてのプログラミングコース))
初期化処理を実装する
#include "hardware/i2c.h"
#define I2C_PORT i2c0
#define I2C_SDA 8
#define I2C_SCL 9
i2c_init(I2C_PORT, 400*1000);
gpio_set_function(I2C_SDA, GPIO_FUNC_I2C);
gpio_set_function(I2C_SCL, GPIO_FUNC_I2C);
最初にhardware/i2c.hをインクルードします。FeaturesでI2CにチェックしているとCMakeLists.txtに追加されているので候補として表示されます。
I2CのチャンネルはI2C0とI2C1があります。Pico SDKではI2C0のデフォルトピンとしてGP8がSDA、GP9がSCLで割り当てられています。
メインの無限ループに入る前に初期化処理を行います。i2c_init()関数を使用するとI2Cに関する初期化処理が行われます。第1引数にI2Cポートの種別を指定し、第2引数にSCLの通信速度を指定します。デフォルトは400kHzですが、スレーブが応答できる通信速度に調整する必要があります。
gpio_set_function()関数はGPIOピンの周辺機器の設定を行います。第1引数に対象のGPIOピンを指定し、第2引数に周辺機器の機能を指定します。I2Cとして使用するためデフォルトから変更せずにGPIO_FUNC_I2Cを指定しています。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
I2C通信の送信
/* コマンド送信(書き込み) */
void Sht40Cmd(uint8_t cmd ){
i2c_write_blocking(I2C_PORT, SLAVE_ADRS, &cmd, 1, false);
}
//使用例
Sht40Cmd( CMD_HIGH_REPEAT );//0xFDを指定
SHT40の測定開始はスレーブアドレス(0x44)を選択してコマンドを書き込みます。使用例のコマンドはhigh repeatabilityのコマンド(0xFD)を指定しています。
I2C通信の送信は4行目のi2c_write_blocking()関数を使用します。第1引数にI2Cポートの種別を指定します。第2引数にスレーブアドレスを指定します。第3引数に送信するデータのアドレスを指定します。第4引数に送信するデータ数を指定します。
第5引数にI2C通信のストップ条件を送信するかを指定します。trueを指定するとストップ条件を送信せず、スレーブを選択した状態を維持するので連続して次のデータを送信することができます。
I2C通信の受信
/* 測定データ取得(読み込み) */
bool Sht40GetData(uint8_t* rcvdata, uint16_t len){
bool ret = false;
if( i2c_read_blocking(I2C_PORT,SLAVE_ADRS, rcvdata, len, false) == len ){
ret = true;
}
return ret;
}
//使用例
Sht40GetData( &rxdata[0], sizeof(rxdata));
測定開始のコマンドを送信した後は、測定データを読み出します。5行目のi2c_read_blocking()関数を使用するとデータを読み出すことができます。
第1引数にスレーブアドレスを指定します。第2引数に受信したデータを格納する配列などのアドレスを指定します。第3引数に受信するデータ数を指定します。
第4引数にI2C通信のストップ条件を送信するかを指定します。trueを指定するとストップ条件を送信せず、スレーブを選択した状態を維持するので連続して次のデータを送信することができます。
戻り値は受信したデータの数になるので指定したデータが格納されたのを確認することができます。(5行目)
PR:RUNTEQ(ランテック )- マイベスト4年連続1位を獲得した実績を持つWebエンジニア養成プログラミングスクール
SHT40の測定とデータ取得の例

SHT40のI2Cはコマンド送信後、スレーブアドレスを指定して読み出します。測定データは湿度の測定データが16ビット(2バイト)に続けて1バイトのCRC8が付加されます。続けて温度の測定データが16ビットに続けて1バイトのCRC8が付加されます。
SHT40の測定コマンドからデータ取得までの手順を説明します。取得したデータの健全性を確認するCRC8の計算方法について説明します。
- 手順1測定開始コマンドの送信
high repeatabilityのコマンド(0xFD)を指定して送信する。
- 手順2測定待ち
high repeatabilityの測定の場合、最大で8.3ms測定にかかるので10ms待機して手順3に進む。
- 手順3受信データの取得
湿度と温度の測定データを、CRC8チェックサムを含む6バイトの形式で読み込む。
- 手順4CRC8の計算
湿度の2バイトデータからCRC8を計算して、湿度に付加されたCRC8と比較する。温度も同様にCRC8を計算して、温度に付加されたCRC8と比較する。一致したら手順5に進む。
- 手順5湿度と温度のデータを換算する
換算式を使って湿度と温度のデータに換算する。再度測定する場合は手順1に戻る。
手順1はI2C通信の送信で説明した通りコマンドを送信します。
手順2はコマンドによって測定を開始するのでhigh repeatabilityで必要な最大8.3ms以上待機して手順3に進みます。
手順3はI2C通信の受信で説明した通り測定データを読み込みます。
手順4は湿度と温度の2バイトデータからCRC8を計算して、受信したCRC8値と比較して一致した場合にデータが健全であると判断して手順5に進みます。
手順5ではデータシートに記載されている換算式から湿度と温度のデータに換算します。手順1~5を繰り返して定期的に測定を行います。
測定をモードで管理する
上記の手順1~5をモードで管理する例を説明します。
switch(mode){
case SHT40_MEASURE: //手順1
//i2c_write_blocking()関数で測定開始トリガの送信
mode = SHT40_WAIT;
break;
case SHT40_WAIT: //手順2
if( timmeas == TIME_UP ){
timmeas = TIME_OFF;
mode = SHT40_READ;
}
break;
case SHT40_READ: //手順3,6
//i2c_read_blocking()関数でデータを読み込み 手順3
//CRC8をチェックして一致するとデータを換算する 手順4
//温湿度データに換算 手順5
mode = SHT40_MEASURE;
break;
}
SHT40の測定データ取得の手順の手順1~5をモードで管理します。
- SHT40_MEASURE(手順1)
- SHT40_WAIT(手順2)
- SHT40_READ(手順3~5)
1.SHT40_MEASUREはi2c_write_blocking()関数で測定開始し、次のモードに進みます。
2.SHT40_WAITはhigh repeatabilityで必要な最大8.3ms以上待機します。例では10ms待機するようにしています。
3.SHT40_READはi2c_read_blocking()関数でデータを読み込みます。読み込んだデータから湿度と温度のCRC8を計算して、3バイト目に付加されているCRC8と比較します。一致すると測定値を受け入れて湿度と温度に換算します。
PR:企業で求められる即戦力技術を身に付ける テックキャンプエンジニア転職
CRC8の計算方法

CRC8の計算方法はデータシートから初期値(Initialization)を0xFF、多項式(Polynomial)を使用して計算していきます。下記に計算の例を記載しています。
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を繰り返していき対象のデータのバイト数だけ繰り返します。
PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
測定データの換算
測定データは16ビット(2バイト)で構成されています。2バイトのデータを温湿度の数値に換算する必要があります。測定データの換算方法はデータシートに記載されています。

測定データは配列で1バイトずつ取得しているため2バイトデータに置き換えた後換算を行います。小数点以下を表示するためfloatを使用します。
uint16_t tempHex;
uint16_t humiHex;
float temp;
float humi;
tempHex = ((uint16_t)rxdata[0] << 8) | rxdata[1];
humiHex =((uint16_t)rxdata[3] << 8) | rxdata[4];
temp = ((float)tempHex / 65535) * 175 - 45;
humi = ((float)humiHex / 65535) * 125 - 6;
6行目と7行目は1バイトのデータから2バイトのデータを生成しています。MSBが含まれるデータを左に8回シフトし、LSBが含まれるデータを加算しています。
8行目は換算式(3)を使用して、温度のデータに換算しています。生成した2バイトのデータをfloat型にキャストして65535で除算しています。
9行目は換算式(1)を使用して、湿度のデータに換算しています。生成した2バイトのデータをfloat型にキャストして65535で除算しています。
PR:無料トライアル実施中【PC専用】AIスライド資料作成ツールの利用:イルシル
動作確認

PicoとSHT40の配線例を示しています。回路図の番号はPicoの左上を1ピンとした時反時計回りにピンを数えた場合の番号としています。ピン番号横の()内の番号はシルク印刷されているピンの名称です。
AE-SHT40IモジュールにはI2C用のプルアップ抵抗(10kΩ)が実装されているので追加のプルアップ抵抗は不要です。
電源ONしてから10秒間隔でSHT40の測定とデータの取得を行います。シリアルモニターの表示を確認するとデータが取得できていることが分かりました。

シリアルモニターで温度と湿度が表示できていることが確認できました。室内に設置した温度計とスマホのデータによる湿度とほぼ一致しておりI2C通信でSHT40からデータが取得できていることが確認できました。
ソースコード全体
ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。
#include <stdio.h>
#include "pico/stdlib.h"
#include "hardware/i2c.h"
#define I2C_PORT i2c0
#define I2C_SDA 8
#define I2C_SCL 9
#define SLAVE_ADRS 0x44
#define PIN_DO1 25
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define LED_ONOFF 50
#define TIME_SHT35_MAX 1000 //SHT35の計測タイマ
#define TIME_OUT_MAX 1200 //SHT35の通信タイムアウト
#define TIME_WAIT_MAX 10 //SHT40の計測待機
#define POLYNOMIAL 0x31
#define CMD_HIGH_REPEAT 0xFD
typedef enum{
SHT40_MEASURE = 0,
SHT40_WAIT,
SHT40_READ,
SHT40_MAX
}SHT40_MODE;
uint32_t beforetimCnt;
int16_t timled = LED_ONOFF;
int16_t timmeas;
int16_t timout = TIME_OFF;;
SHT40_MODE mode = SHT40_MEASURE;
uint8_t chksum[2];
uint8_t rxdata[6];
float temp;
float humi;
void mainApp(void);
void mainTimer(void);
uint8_t Crc8Calc(uint8_t *data, uint8_t sz );
void Sht40Cmd(uint8_t cmd );
bool Sht40GetData(uint8_t* reg_data, uint16_t len);
int main()
{
stdio_init_all();
gpio_init(PIN_DO1); // ピン初期化
gpio_set_dir(PIN_DO1, GPIO_OUT); //DOピン
i2c_init(I2C_PORT, 400*1000);
gpio_set_function(I2C_SDA, GPIO_FUNC_I2C);
gpio_set_function(I2C_SCL, GPIO_FUNC_I2C);
sleep_ms(4000);
printf("Hello I2C\r\n");
while (true) {
mainApp();
mainTimer();
}
}
/* メイン処理 */
void mainApp(void){
switch(mode){
case SHT40_MEASURE:
if( timmeas == TIME_UP ){
timmeas = TIME_WAIT_MAX;
timout = TIME_OUT_MAX;
Sht40Cmd( CMD_HIGH_REPEAT );
mode = SHT40_WAIT;
}
break;
case SHT40_WAIT:
if( timmeas == TIME_UP ){
timmeas = TIME_OFF;
mode = SHT40_READ;
}
break;
case SHT40_READ:
if( Sht40GetData( &rxdata[0], sizeof(rxdata))){
uint16_t tempHex;
uint16_t humiHex;
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];
temp = ((float)tempHex / 65535) * 175 - 45;
humi = ((float)humiHex / 65535) * 125 - 6;
if( humi > 100 ) humi = 100;
if( humi < 0 ) humi = 0;
printf("温度: %3.2f ℃", temp);
printf(" ");
printf("湿度: %2.2f %\r\n", humi);
}
timmeas = TIME_SHT35_MAX;
timout = TIME_OFF;
mode = SHT40_MEASURE;
}
break;
}
if( timout == TIME_UP ){
timout = TIME_OFF;
mode = SHT40_MEASURE;
timmeas = TIME_SHT35_MAX;
}
if( timled == TIME_UP ){
timled = LED_ONOFF;
gpio_put(PIN_DO1, !gpio_get(PIN_DO1));
}
}
/* タイマ管理 */
void mainTimer(void){
if( to_ms_since_boot(get_absolute_time()) - beforetimCnt > BASE_CNT ){
beforetimCnt = to_ms_since_boot(get_absolute_time());
//10msごとにここに遷移する
if( timled > TIME_UP ){
timled--;
}
if( timmeas > TIME_UP ){
timmeas--;
}
if( timout > TIME_UP ){
timout--;
}
}
}
/* コマンド送信(書き込み) */
void Sht40Cmd(uint8_t cmd ){
i2c_write_blocking(I2C_PORT, SLAVE_ADRS, &cmd, 1, false);
}
/* 測定データ取得(読み込み) */
bool Sht40GetData(uint8_t* rcvdata, uint16_t len){
bool ret = false;
if( i2c_read_blocking(I2C_PORT,SLAVE_ADRS, rcvdata, len, false) == len ){
ret = true;
}
return ret;
}
/* 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;
}
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
PR:無料トライアル実施中【PC専用】AIスライド資料作成ツールの利用:イルシル
最後まで、読んでいただきありがとうございました。