ESP32 の I2C 通信におけるクロックストレッチ対応について注意点を紹介します.
はじめに
ESP32 は I2C のクロックストレッチ(clock stretch) に対応しています.ただ,実際に活用するに当たっては注意点があります.
ESP-IDF を使用する場合を例にとって具体的に紹介します.
注意点
結論としては下記になります.
- ハードウェア制約により,クロックストレッチできる期間は最大 13ms.
- ESP-IDF の場合,デフォルトでは 8 クロックでタイムアウト.これより待ちたい場合はタイムアウト時間の変更が必要.
- タイムアウト時間の変更は,
i2c_set_timeout
で行う i2c_master_cmd_begin
の第3引数のticks_to_wait
は関係ない
以降で,順に説明していきます.
クロックストレッチできる期間は最大 13ms
ESP32 のクロックストレッチのタイムアウト時間は,下記の I2C_TIME_OUT_REG レジスタに設定します.単位は APB clock cycles となっており,このクロックの周波数は 80MHz です.
レジスタのサイズは 20bit なので,最大値は 0xFFFFF となります.0xFFFFF / 80MHz = 13ms ですので,クロックストレッチできる最大時間は 13ms となります.
I2C 通信を 400kHz で行っている場合,13ms は 5,200 サイクルに相当しますので,通常問題になることはないと思いますが,特殊なデバイスを使っている場合注意が必要かもしれません.
デフォルトでは 8 クロックでタイムアウト
ESP-IDF では,上記レジスタへの設定を esp-idf/components/driver/i2c.c の i2c_param_config
関数のなかで行っています.
具体的には下記の箇所になります.
I2C_MASTER_TOUT_CNUM_DEFAULT
は 8 と define されており,8サイクルでタイムアウトさせるようになっています.
タイムアウト時間の変更は i2c_set_timeout
第2引数にタイムアウト時間を 80MHz でのクロック数で指定します.特にこだわりがなければ,最大値である 0xFFFFF を設定しておけば良いと思います.Arduino core for the ESP32 ではそうなっています.
i2c_master_cmd_begin の第3引数は関係ない
i2c_master_cmd_begin の第3引数は ticks_to_wait
となっており,ぱっと見クロックストレッチに関係しそうな気がしますが,実は全く関係ないです.
ESP32 の I2C ドライバは複数タスクから呼ばれることを想定して排他処理を行っています.ticks_to_wait はこの排他処理のロックをとれるまでの最大待ち時間を設定するものになります.
動作サンプル
上記を踏まえた I2C 通信のサンプルを紹介します.題材としては,温湿度センサである SHT3x を使用します.
SHT3x は測定データの読出し時に,下記のようにクロックストレッチを使うことができますので,これを試します.
コード全体は次のようになります.44行目の i2c_set_timeout(i2c_port, 0xFFFFF);
でクロックストレッチのタイムアウト時間を変更しています.これをコメントアウトすると計測データが正しく読めなくなります.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
#include "driver/i2c.h" #include "sdkconfig.h" #include <esp_log.h> #include <stdio.h> #define ARRAY_SIZE_OF(a) (sizeof(a) / sizeof(a[0])) static const uint8_t I2C_SHT3x_ADR = 0x44; static const uint8_t I2C_SCL_NUM = 22; static const uint8_t I2C_SDA_NUM = 21; static const uint32_t I2C_FREQ_HZ = 100000; static uint8_t crc8(const uint8_t *data, uint32_t len) { static const uint8_t POLY = 0x31; uint8_t crc = 0xFF; for (uint32_t i = 0; i < len; i++) { crc ^= data[i]; for (uint32_t j = 0; j < 8; j++) { if (crc & 0x80) { crc = (crc << 1) ^ POLY; } else { crc <<= 1; } } } return crc; } static void i2c_init(i2c_port_t i2c_port) { i2c_config_t conf; conf.mode = I2C_MODE_MASTER; conf.sda_io_num = I2C_SDA_NUM; conf.sda_pullup_en = GPIO_PULLUP_ENABLE; conf.scl_io_num = I2C_SCL_NUM; conf.scl_pullup_en = GPIO_PULLUP_ENABLE; conf.master.clk_speed = I2C_FREQ_HZ; ESP_ERROR_CHECK(i2c_param_config(I2C_NUM_0, &conf)); ESP_ERROR_CHECK(i2c_driver_install(I2C_NUM_0, conf.mode, 0, 0, 0)); i2c_set_timeout(i2c_port, 0xFFFFF); vTaskDelay(100 / portTICK_PERIOD_MS); // NOTE: pull up が有効になるのを待つ } static esp_err_t sht3x_check_val(uint8_t *buf) { if (crc8(buf + 0, 2) != buf[2]) { printf("CRC of temperature is INVALID. (exp: 0x%02X, calc: 0x%02X)\n", buf[2], crc8(buf + 0, 2)); return ESP_FAIL; } if (crc8(buf + 3, 2) != buf[5]) { printf("CRC of humidity is INVALID. (exp: 0x%02X, calc: 0x%02X)\n", buf[5], crc8(buf + 3, 2)); return ESP_FAIL; } return ESP_OK; } static void sht3x_dump(uint8_t *buf) { uint16_t temp_val; uint16_t humi_val; sht3x_check_val(buf); temp_val = ((uint16_t)buf[0]) << 8 | buf[1]; humi_val = ((uint16_t)buf[3]) << 8 | buf[4]; printf("TEMP: %.2f\n", -45.0 + (175.0 * temp_val) / ((1 << 16) - 1)); printf("HUMI: %.2f\n", 100.0 * humi_val / ((1 << 16) - 1)); } static void sht3x_read(i2c_port_t i2c_port) { uint8_t buf[6]; i2c_cmd_handle_t cmd = i2c_cmd_link_create(); ESP_ERROR_CHECK(i2c_master_start(cmd)); ESP_ERROR_CHECK(i2c_master_write_byte( cmd, (I2C_SHT3x_ADR << 1) | I2C_MASTER_WRITE, I2C_MASTER_ACK)); ESP_ERROR_CHECK(i2c_master_write_byte(cmd, 0x2C, I2C_MASTER_ACK)); ESP_ERROR_CHECK(i2c_master_write_byte(cmd, 0x0D, I2C_MASTER_ACK)); ESP_ERROR_CHECK(i2c_master_stop(cmd)); ESP_ERROR_CHECK(i2c_master_cmd_begin(i2c_port, cmd, 1 / portTICK_RATE_MS)); i2c_cmd_link_delete(cmd); cmd = i2c_cmd_link_create(); ESP_ERROR_CHECK(i2c_master_start(cmd)); ESP_ERROR_CHECK(i2c_master_write_byte( cmd, (I2C_SHT3x_ADR << 1) | I2C_MASTER_READ, I2C_MASTER_ACK)); for (uint32_t i = 0; i < ARRAY_SIZE_OF(buf); i++) { ESP_ERROR_CHECK(i2c_master_read_byte(cmd, buf + i, (i == (ARRAY_SIZE_OF(buf) - 1)) ? I2C_MASTER_NACK : I2C_MASTER_ACK)); } ESP_ERROR_CHECK(i2c_master_stop(cmd)); ESP_ERROR_CHECK( i2c_master_cmd_begin(i2c_port, cmd, 1 / portTICK_PERIOD_MS)); i2c_cmd_link_delete(cmd); sht3x_dump(buf); } void app_main() { i2c_port_t i2c_port = I2C_NUM_0; i2c_init(i2c_port); while (true) { sht3x_read(i2c_port); vTaskDelay(2000 / portTICK_PERIOD_MS); } } |
コメント
ESPrdeveloper32(soc:ESP32)を先日購入した初心者です。
これと、マスタ側がclockstretch対応が必要なセンサをI2Cで繋げようとしていて果たして問題がないのか調べていてもわからなかった矢先にkimata様の記事をみつけて大変勉強になりました。ありがとうございます。
ご質問させていただきたいのですが、上記、具体的には、SCD30(sensirion製)をつなぎたい状況です。
しかし、このセンサの通信仕様書の中のclock stretchingに関する記述を見ると
“Maximal I2C speed is 100 kHz and the master has to support clock stretching. Clock stretching period in write- and readframes is 12 ms, however, due to internal calibration processes a maximal clock stretching of 150 ms may occur once per day. ”
とあります。
この場合、最大150msが一日に一度程度の頻度でSCLをLOWにしようとセンサ側がしてしまう(ことが起きうる)ということだと思います。
こういった場合、ESPr developper 32にてこのセンサを接続するのはいかに工夫をしてもできないということになるのでしょうか。最大のおきうる時間に合わせて周波数が約3Hz(遅いですが、)のソフトウェアI2Cを実装するなどで、データの取得間隔に対する要求が緩い場合は対応できるのでしょうか。
aws IoTへ手軽にセンサ情報を送れそうだということで活用を検討しているのですが、どうしてもこの製品だと難しい場合代替案などありますでしょうか。
当方、周りに聞ける人がおらず困っております、
お手すきの際にご回答いただけますと幸いです。
クロック周波数にかかわらず,クロックストレッチ期間が 13ms を超えると i2c_master_cmd_begin が ESP_ERR_TIMEOUT を返してしまいます.
お使いのデバイスの場合,150ms を超えるのは一日に一回の頻度のようですので,リトライするのが良いように思います.
具体的には,ESP_ERR_TIMEOUT になった場合,一旦 i2c_driver_delete を呼んでから,再度 i2c_driver_install を呼ぶことで,また I2C 通信が行えるようになります.
(これをしないとドライバ内部の状態が中途半端なままなって,その後の i2c_master_cmd_begin が ESP_ERR_TIMEOUT を常に返すようになってしまいます)
こんなにお早く対応いただけるとは、、教えてくださりありがとうございます。
重ね重ね申し訳ないのですが、現状arduinoIDEを用いた実装(arduino.hとWire.h使用)で実現できないかこころみております。
ご教授いただいた内容と同様のことは実現できそうでしょうか。
もしご存知でしたら教えていただけますと幸いです。
ざっとコード見た感じですと,タイムアウト後に復旧させることは考えてなさそうなので,簡単ではないと思われます.素直に ESP-IDF 使うのがおすすめです.
ESP-IDF使いこなせるよう頑張ってみます。
今後とも参考にさせていただきます。
本当にありがとうございました。