ESP32は、Wi-FiやBluetoothだけでなく、さまざまな周辺機器と通信できる豊富なインターフェースを備えています。
その中でもI²C(Inter-Integrated Circuit)は、センサーやRTC(リアルタイムクロック)、EEPROMなど、多くのデバイスで採用されている代表的な通信方式です。
ESP32で開発を始めると、
- I²Cとはどのような通信方式なの?
- SPIやUARTとは何が違うの?
- プルアップ抵抗はなぜ必要なの?
- ESP32ではどのように利用するの?
といった疑問を持つ方も多いでしょう。
この記事では、I²C通信の基本からESP32で利用する際のポイント、設計時の注意点まで、実務目線で分かりやすく解説します。
I²Cとは?
I²C(Inter-Integrated Circuit)は、1980年代にNXP(当時Philips)が開発したシリアル通信方式です。
最大の特徴は、2本の信号線だけで複数のデバイスと通信できることです。
SPIのようにデバイスごとにCS信号を用意する必要がなく、UARTのように1対1接続に限定されることもありません。
そのため、基板上で多くの周辺機器を効率よく接続したい場合に適しています。
現在では、多くのマイコンやセンサーがI²Cに対応しており、組み込み開発では最もよく使われる通信方式の一つです。
I²Cが使われる場面
ESP32では、次のようなデバイスとの接続によく利用されます。
- 温度・湿度センサー
- 加速度センサー
- 気圧センサー
- RTC(リアルタイムクロック)
- EEPROM
- I/Oエキスパンダ
- OLEDディスプレイ
これらは大量のデータを高速転送する用途ではなく、比較的少ないデータを定期的に読み書きする用途が中心です。
そのため、I²Cのシンプルな配線と複数デバイス接続のしやすさが大きなメリットになります。
SDAとSCLの役割
I²Cでは、基本的に2本の信号線を使用します。
- SDA(Serial Data):データの送受信を行う信号線
- SCL(Serial Clock):通信タイミングを合わせるクロック信号
この2本を複数のデバイスで共有し、それぞれが持つI²Cアドレスによって通信相手を識別します。
そのため、SPIのようにデバイスごとにCS信号を用意する必要がありません。
I²C通信の仕組み
I²Cでは、ESP32などのマスターが通信を開始し、各デバイス(スレーブ)は指定されたアドレスに応じて応答します。
通信は次のような流れで行われます。
- マスターが通信開始(スタートコンディション)
- スレーブアドレスを送信
- 対象デバイスが応答(ACK)
- データの送受信
- 通信終了(ストップコンディション)
この仕組みにより、複数のデバイスを同じ2本の配線で効率よく制御できます。
SPI・UARTとの違い
ESP32では、UART・SPI・I²Cを用途に応じて使い分けます。
- UART:マイコン同士や通信モジュールとの1対1通信
- SPI:高速なデータ転送が必要なディスプレイや外部メモリとの接続
- I²C:複数のセンサーやRTCなどを少ない配線で接続
I²Cは高速通信には向きませんが、配線を最小限に抑えながら多くのデバイスを接続できるため、組み込みシステムでは非常に使いやすい通信方式です。
今回使用する開発環境
この記事では、次の環境でI²C通信を確認します。
- ESP-IDF:v5.5
- ターゲット:ESP32-C5
- IDE:Visual Studio Code
- 接続デバイス:DS3231 RTC
ESP32-C5ではGPIOマトリクスにより、多くのGPIOへI²C信号を割り当てられます。
今回は
- SDA:GPIO6
- SCL:GPIO7
を使用します
想定回路
+3.3V
│
│
4.7kΩ
│
GPIO6 (SDA)────────┼────────DS3231 SDA
│
│
4.7kΩ
│
│
GPIO7 (SCL)────────┼────────DS3231 SCL
GND────────────────DS3231 GND
3.3V────────────────DS3231 VCC
配線について
I²CではSDAとSCLの両方にプルアップ抵抗が必要です。
今回のサンプルでは4.7kΩを使用しています。
評価ボードによっては既に実装されているため、重複して実装しないよう確認してください。
ESP32でI²Cを使用する方法
ESP32では、ESP-IDFのI²Cドライバを利用することで、比較的簡単にI²C通信を実装できます。
基本的な流れは次のとおりです。
- I²Cポートを初期化する
- SDA・SCLピンを設定する
- 通信速度を設定する
- 接続するデバイスのアドレスを指定する
- データの送受信を行う
ESP32はGPIOマトリクスを備えているため、多くのGPIOへI²C信号を割り当てることができます。
ただし、ESP32シリーズ(ESP32、ESP32-S2、ESP32-S3、ESP32-C3、ESP32-C5など)では、利用できるGPIOや周辺機能が異なるため、使用するデバイスのデータシートや技術資料を確認してください。
今回は、ESP-IDF v5.5系・ESP32-C5・DS3231 RTCを前提にしています。新しいI²Cマスタードライバであるdriver/i2c_master.hを使用します。Espressif公式では、このヘッダを使う場合、esp_driver_i2cコンポーネントへの依存を指定します。
CMakeLists.txt
idf_component_register(
SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES driver
)
ESP-IDF v5系の新しいI²Cマスタードライバは、esp_driver_i2cコンポーネントとして提供されています。
DS3231から時刻を読み取るサンプルコード
以下は、ESP32-C5とDS3231をI²Cで接続し、現在の時刻を1秒ごとに読み取るサンプルです。
この例では、次のGPIOを使用します。
- SDA:GPIO6
- SCL:GPIO7
- I²C通信速度:100kHz
- DS3231の7bitアドレス:
#include <stdint.h>
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/i2c_master.h"
#include "esp_err.h"
#include "esp_log.h"
#define I2C_MASTER_SDA_GPIO 6
#define I2C_MASTER_SCL_GPIO 7
#define I2C_MASTER_PORT I2C_NUM_0
#define I2C_MASTER_FREQUENCY_HZ 100000
#define I2C_TRANSACTION_TIMEOUT_MS 1000
#define DS3231_I2C_ADDRESS 0x68
#define DS3231_TIME_REGISTER 0x00
static const char *TAG = "DS3231_SAMPLE";
/**
* BCD形式のデータを10進数へ変換します。
*/
static uint8_t bcd_to_decimal(uint8_t bcd)
{
return (uint8_t)(((bcd >> 4) * 10U) + (bcd & 0x0FU));
}
/**
* DS3231の時刻レジスタを読み取ります。
*
* registers[0]:秒
* registers[1]:分
* registers[2]:時
*/
static esp_err_t ds3231_read_time(
i2c_master_dev_handle_t ds3231_handle,
uint8_t *hour,
uint8_t *minute,
uint8_t *second)
{
if ((hour == NULL) || (minute == NULL) || (second == NULL)) {
return ESP_ERR_INVALID_ARG;
}
uint8_t register_address = DS3231_TIME_REGISTER;
uint8_t registers[3] = {0};
esp_err_t result = i2c_master_transmit_receive(
ds3231_handle,
®ister_address,
sizeof(register_address),
registers,
sizeof(registers),
I2C_TRANSACTION_TIMEOUT_MS);
if (result != ESP_OK) {
return result;
}
/*
* 秒レジスタと分レジスタはBCD形式です。
* 秒のbit7は未使用なのでマスクします。
*/
*second = bcd_to_decimal(registers[0] & 0x7FU);
*minute = bcd_to_decimal(registers[1] & 0x7FU);
/*
* このサンプルでは24時間モードを前提とします。
* 時レジスタのbit6が0なら24時間モードです。
*/
if ((registers[2] & 0x40U) != 0U) {
ESP_LOGE(TAG, "DS3231 is configured for 12-hour mode");
return ESP_ERR_INVALID_STATE;
}
*hour = bcd_to_decimal(registers[2] & 0x3FU);
return ESP_OK;
}
void app_main(void)
{
/*
* I2Cマスターバスを設定します。
*/
i2c_master_bus_config_t bus_config = {
.i2c_port = I2C_MASTER_PORT,
.sda_io_num = I2C_MASTER_SDA_GPIO,
.scl_io_num = I2C_MASTER_SCL_GPIO,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 0,
.trans_queue_depth = 0,
.flags.enable_internal_pullup = false,
};
i2c_master_bus_handle_t bus_handle = NULL;
ESP_ERROR_CHECK(
i2c_new_master_bus(
&bus_config,
&bus_handle));
/*
* DS3231をI2Cバスへ登録します。
*/
i2c_device_config_t device_config = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = DS3231_I2C_ADDRESS,
.scl_speed_hz = I2C_MASTER_FREQUENCY_HZ,
.scl_wait_us = 0,
.flags.disable_ack_check = false,
};
i2c_master_dev_handle_t ds3231_handle = NULL;
ESP_ERROR_CHECK(
i2c_master_bus_add_device(
bus_handle,
&device_config,
&ds3231_handle));
/*
* DS3231がI2Cバス上で応答するか確認します。
*/
ESP_ERROR_CHECK(
i2c_master_probe(
bus_handle,
DS3231_I2C_ADDRESS,
I2C_TRANSACTION_TIMEOUT_MS));
ESP_LOGI(
TAG,
"DS3231 detected at address 0x%02X",
DS3231_I2C_ADDRESS);
while (1) {
uint8_t hour = 0;
uint8_t minute = 0;
uint8_t second = 0;
esp_err_t result = ds3231_read_time(
ds3231_handle,
&hour,
&minute,
&second);
if (result == ESP_OK) {
ESP_LOGI(
TAG,
"Time: %02u:%02u:%02u",
hour,
minute,
second);
} else {
ESP_LOGE(
TAG,
"Failed to read time: %s",
esp_err_to_name(result));
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
サンプルコードのポイント
I²Cマスターバスを初期化する
i2c_master_bus_config_t bus_config = {
.i2c_port = I2C_NUM_0,
.sda_io_num = GPIO_NUM_6,
.scl_io_num = GPIO_NUM_7,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
};
ここでは、I²Cポート、SDA、SCL、クロック源などを設定します。
その後、次の関数でI²Cマスターバスを生成します。
i2c_new_master_bus(
&bus_config,
&bus_handle);
DS3231をI²Cバスへ登録する
i2c_device_config_t device_config = {
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
.device_address = 0x68,
.scl_speed_hz = 100000,
};
DS3231の7bit I²Cアドレスは0x68です。
設定後、次の関数でDS3231をI²Cバスへ登録します。
i2c_master_bus_add_device(
bus_handle,
&device_config,
&ds3231_handle);
DS3231のI²Cアドレスは、データシート上では7bitの1101000であり、16進数では0x68です。
DS3231の接続を確認する
i2c_master_probe(
bus_handle,
DS3231_I2C_ADDRESS,
I2C_TRANSACTION_TIMEOUT_MS);
i2c_master_probe()を使用すると、指定したI²Cアドレスのデバイスが応答するか確認できます。
ここでタイムアウトになる場合は、次の点を確認してください。
- SDAとSCLが逆になっていないか
- GNDが共通になっているか
- DS3231へ電源が供給されているか
- SDAとSCLにプルアップ抵抗があるか
- I²Cアドレスが正しいか
Espressif公式資料でも、i2c_master_probe()でタイムアウトする場合はプルアップ抵抗を確認するよう案内されています。
時刻レジスタを読み取る
uint8_t register_address = 0x00;
uint8_t registers[3];
i2c_master_transmit_receive(
ds3231_handle,
®ister_address,
1,
registers,
3,
1000);
最初に0x00を送信して、DS3231内部のレジスタポインタを秒レジスタへ設定します。
続けて3バイトを受信することで、次のデータをまとめて取得します。
0x00:秒0x01:分0x02:時
DS3231の時刻レジスタは、秒・分・時の順で0x00~0x02へ配置されています。
BCD形式を10進数へ変換する
DS3231の時刻データは、通常の2進数ではなくBCD形式で格納されています。
例えば、45秒は次のように格納されます。
0x45
この値をそのまま10進数として扱うと69になるため、BCDから10進数への変換が必要です。
static uint8_t bcd_to_decimal(uint8_t bcd)
{
return ((bcd >> 4) * 10) + (bcd & 0x0F);
}
DS3231の時刻・カレンダーレジスタはBCD形式です。
実行結果
プログラムを書き込み、シリアルモニターを開くと、次のようなログが表示されます。
I (315) DS3231_SAMPLE: DS3231 detected at address 0x68
I (1325) DS3231_SAMPLE: Time: 14:25:31
I (2325) DS3231_SAMPLE: Time: 14:25:32
I (3325) DS3231_SAMPLE: Time: 14:25:33
I (4325) DS3231_SAMPLE: Time: 14:25:34
時刻が1秒ごとに更新されれば、ESP32-C5とDS3231のI²C通信は正常です。
なお、このサンプルはDS3231に既に正しい時刻が設定されていることを前提としています。初めて使用するRTCやバックアップ電池が切れていたRTCでは、時刻設定処理を別途行う必要があります。
回路接続の注意点
このサンプルでは、SDAとSCLに外付けのプルアップ抵抗を使用するため、プログラム内の内部プルアップは無効にしています。DS3231モジュール側にプルアップ抵抗が実装されている場合は、外付け抵抗との合成抵抗値が小さくなりすぎないよう注意してください。
I²CのSDAとSCLは双方向のオープンドレイン信号であり、外部プルアップ抵抗が必要です。Espressifは一般的な範囲として1kΩ~10kΩ、特に2kΩ~5kΩ程度を示していますが、適切な値は通信速度やバス容量によって変わります。
プルアップ抵抗が必要な理由
I²Cを学び始めたとき、多くの方が最初に疑問に思うのが「なぜプルアップ抵抗が必要なのか」という点です。
I²CのSDAとSCLは、**オープンドレイン(またはオープンコレクタ)**方式で動作します。
これは、デバイス自身が信号線をHighに駆動するのではなく、Lowに引き下げることだけを行う仕組みです。
そのため、信号線をHighに戻すためには外部のプルアップ抵抗が必要になります。
多くの評価ボードやセンサーモジュールには、あらかじめプルアップ抵抗が実装されています。
一方、自作基板ではプルアップ抵抗を実装し忘れると、正常に通信できない原因になります。
I²C通信でよくあるトラブル
I²Cは配線が少なく扱いやすい反面、設計上の注意点もあります。
1.同じI²Cアドレスのデバイスを接続してしまう
I²Cではアドレスによって通信相手を識別します。
そのため、同じアドレスを持つデバイスを同一バスへ接続すると、正常に通信できません。
データシートでアドレス変更機能の有無を確認し、必要に応じてジャンパ設定やI²Cマルチプレクサの利用を検討しましょう。
2.プルアップ抵抗が強すぎる・弱すぎる
抵抗値が適切でないと、
- 波形がなまる
- 通信エラー
- クロックが乱れる
といった問題が発生することがあります。
一般的には4.7kΩ前後がよく使われますが、バス容量や通信速度によって最適値は変わります。
3.配線が長すぎる
I²CはSPIより低速ですが、長距離通信向けではありません。
配線が長くなるとバス容量が増え、
- 波形が乱れる
- ノイズの影響を受ける
- 通信速度を上げられない
といった問題が起こりやすくなります。
基板内では、SDA・SCLをできるだけ短く引き回すことをおすすめします。
実務での設計ポイント
実際の組み込み機器では、I²Cは非常に便利な反面、設計段階で少し配慮するだけで品質が大きく変わります。
私が設計する際には、次の点を意識しています。
- バスに接続するデバイス数を事前に整理する
- I²Cアドレスの重複がないことを確認する
- プルアップ抵抗は回路全体で最適化する
- 将来の機能追加を見据えてアドレスやコネクタに余裕を持たせる
- デバッグしやすいようにテストポイントを設ける
I²Cは「2本だから簡単」ではなく、システム全体を見据えて設計することが、安定した製品づくりにつながります。
まとめ
I²Cは、ESP32でセンサーやRTC、EEPROMなどを接続する際に欠かせない通信インターフェースです。
SPIより配線が少なく、UARTより多くのデバイスを接続できるため、多くの組み込み機器で採用されています。
一方で、プルアップ抵抗やアドレス管理、配線長など、安定した通信のために押さえておきたいポイントもあります。
通信方式の特徴を理解し、用途に応じてUART・SPI・I²Cを使い分けることが、信頼性の高いシステム設計への第一歩です。
関連記事
ESP32 UART完全ガイド|UART0・UART1・UART2の違いと使い方を現役エンジニアが解説
ESP32 SPI完全ガイド|SPI通信の基本と使い方を現役エンジニアが解説
ESP32 BLEとWi-Fiは同時に使える?現役エンジニアが仕組みと注意点を解説
ESP32 Deep Sleepとは?消費電力を大幅に削減する省電力機能を現役エンジニアが解説
技術は、経験から価値になる。
DLROW Design|現場エンジニア