組み込み開発でI2C通信を使用していると、
最初は正常に通信していたのに、突然通信できなくなった
という現象に遭遇することがあります。
例えば、
- 電源投入途中で通信した
- 通信中にノイズが入った
- MCUだけリセットされた
- センサーだけ動作したままMCUが再起動した
- 通信途中で処理が中断された
- SDAがLOWのまま戻らない
- SCLがLOWのまま戻らない
- I2Cドライバがタイムアウトした
といったケースです。
このような状態を、実務では
「I2Cバスがハングした」
などと表現することがあります。
そこで重要になるのが、
I2Cのバス復帰(Bus Recovery / Bus Clear)
です。
この記事では、I2C初心者・新人組み込みエンジニア向けに、
- I2Cバスとは何か
- なぜ通信が止まるのか
- SDA LOWとSCL LOWの違い
- クロックを6回ON/OFFする方法
- 一般的な9クロックによるBus Clear
- STOP Conditionの生成
- General Call Reset
- ハードウェアRESET
- 電源再投入
- I2Cペリフェラルの再初期化
- タイムアウト処理
- C言語での抽象化した実装方法
- 実務で重要な復帰アルゴリズム
- やってはいけない実装
まで順番に解説します。
I2Cとは?
I2Cは、
Inter-Integrated Circuit
の略で、IC同士を接続するためによく使用されるシリアル通信方式です。
I2Cでは、基本的に次の2本の信号線を使用します。
SCL:クロック線
SDA:データ線
SCLは通信のタイミングを作る信号です。
SDAは実際の0/1のデータを送る信号です。
NXPのI²C仕様でも、I²CはSDAとSCLの2線で通信する方式として定義されています。
ControllerとTargetとは?
I2Cでは、通信を開始する側を
Controller
通信相手となる側を
Target
と呼びます。
従来はMaster / Slaveという表現も広く使われていましたが、現在のNXP仕様ではController / Targetという表現が使われています。
例えば、
RXマイコン
↓
I2C
↓
温度センサー
という構成の場合、
RXマイコン → Controller
温度センサー → Target
となるケースが一般的です。
I2C通信が正常な状態とは?
何も通信していない「Bus Free」と呼ばれる状態では、基本的に、
SCL = HIGH
SDA = HIGH
になっています。
イメージすると、
SCL ───────── HIGH
SDA ───────── HIGH
です。
I2CのSCLとSDAには通常プルアップ抵抗が接続されています。
つまり、誰も線をLOWに引っ張っていなければHIGHになります。
なぜI2Cバスがハングするのか?
典型的なのが、
Controllerだけ途中でリセットされた
というケースです。
例えばTargetがデータ送信中だったとします。
Controller ← Target
Targetは、
まだ通信の途中
だと思っています。
しかし、Controller側だけウォッチドッグなどによって再起動したとします。
するとControllerは、
通信は終わっている
と思っています。
一方、Target側は、
まだ次のクロックを待っている
という状態になる場合があります。
結果としてTargetが、
SDA = LOW
のまま保持してしまうことがあります。
これが典型的なI2Cバスハングです。
まずSDAとSCLの状態を確認する
バス復帰を考えるうえで非常に重要なのが、
SDAがLOWなのか?
SCLがLOWなのか?
を分けて考えることです。
これは復帰方法が異なるためです。
パターン1:SDA LOW / SCL HIGH
SCL = HIGH
SDA = LOW
この場合は、
Targetが通信途中の状態で止まっている
可能性があります。
この場合には、
SCLクロックを送ってTargetの内部状態を進める
というバスクリアが有効な場合があります。
パターン2:SCL LOW
SCL = LOW
この場合は注意が必要です。
SCLそのものがLOWのため、
クロックを送ろう
としてもSCLをHIGHにできない可能性があります。
NXPのI²C仕様では、SCLがLOWに張り付いている場合は、HW RESET端子があればハードウェアリセットを行い、それがなければデバイスの電源再投入によるPORを使う方法が優先されています。
つまり、
SDA LOW
↓
クロックによるBus Clearを検討
SCL LOW
↓
RESET / Power Cycleを優先
と考えると分かりやすいです。
一般的なI2C Bus Clearは9クロック
I2Cの一般的なバス復帰として覚えておきたいのは以下です。
SDAがLOW
↓
SCLを最大9回クロック
↓
SDAがHIGHになったか確認
↓
STOP Condition
NXPのI²C-bus specificationでは、SDAがLOWに張り付いている場合、Controllerは9クロックを送信し、その間にバスを保持しているデバイスがSDAを解放することを期待する、とされています。9クロック後にも解放されない場合はHW RESETまたは電源再投入へ進みます。
したがって、汎用的な実装では、
#define I2C_RECOVERY_CLOCKS (9U)
としておくのがおすすめです。
なぜ9回なの?
I2Cでは、通常1バイトのデータは8bitです。
さらにデータ転送後にはACK/NACKに関係するクロックがあります。
そのため、通信途中でTargetの内部状態が止まっていたとしても、複数回クロックを進めることでバイト転送状態から抜けられる可能性があります。
一般には「8データビット+ACK/NACKフェーズまで進めるための最大9クロック」と理解するとイメージしやすいですが、実際のTarget内部状態はデバイスごとに異なるため、仕様として重要なのは“9クロック送る”という点です。
クロックは9回全部送る必要がある?
必ずしも、
絶対9回全部送る
必要はありません。
実用上は、
1クロック送る
↓
SDA確認
まだLOW
↓
次のクロック
SDA HIGH
↓
終了
とする方法が便利です。
例えば、
for (uint8_t i = 0; i < 9; i++)
{
clock_pulse();
if (SDA_READ() == HIGH)
{
break;
}
}
です。
これなら、3クロック目でTargetがSDAを解放した場合には、そこで次の処理に進めます。
STOP Conditionも重要
クロックを送って、
SDA = HIGH
になっただけで終わらせるのではなく、
STOP Conditionを作る
設計も重要です。
I2CではSTOP Conditionは、
SCL = HIGHのとき
SDA
LOW → HIGH
という変化です。
イメージは、
SCL ───────────── HIGH
SDA ____/ ̄ ̄ ̄ ̄ ̄ ̄
↑
STOP
です。
これによってTarget側に、
通信終了
を明確に伝えます。
Open Drainを理解する
I2Cの復帰処理をGPIOで実装するとき、初心者が特に注意すべきポイントです。
I2Cでは基本的に、
HIGHを強制出力しない
という考え方が重要です。
SCLやSDAをHIGHにしたい場合、
GPIO HIGH出力
ではなく、
出力を解放
↓
プルアップ抵抗によってHIGHになる
という動作にします。
これが、
Open Drain
の考え方です。
簡単に言えば、
LOWにしたい
↓
MCUがGNDへ引っ張る
HIGHにしたい
↓
MCUは線から手を離す
↓
プルアップ抵抗がHIGHにする
という仕組みです。
なぜPush-Pull HIGHにしてはいけないの?
例えばTargetがSDAをLOWにしているとします。
そこでControllerが、
SDA = HIGHを強制出力
すると、
Controller → HIGH
Target → LOW
となります。
これは電気的に出力同士がぶつかる可能性があります。
そのためバス復帰用GPIOも、
LOW出力
または
Hi-Z / Open Drain release
になるよう設計します。
GPIOを使ったバス復帰
一般的な考え方は次の通りです。
1. I2Cペリフェラル停止
2. SCL/SDAをGPIOへ変更
3. SDA/SCLを解放
4. バス状態確認
5. SDA LOWなら最大9クロック
6. STOP生成
7. GPIOからI2C機能へ戻す
8. I2Cペリフェラル再初期化
9. Targetへ通信確認
この順番にすると管理しやすくなります。
コピペして流用できるC言語の抽象化実装
ここからは、特定のマイコンに依存しない形で実装してみます。
RX、STM32、PIC、nRF、AVRなどへ移植できるよう、
GPIO操作そのものをコールバック関数として外へ出します。
戻り値を定義する
typedef enum
{
I2C_RECOVERY_OK = 0,
I2C_RECOVERY_SDA_STUCK,
I2C_RECOVERY_SCL_STUCK,
I2C_RECOVERY_INVALID_ARG
} I2C_RecoveryResult;
これにより、
正常復帰
SDA異常
SCL異常
引数異常
を呼び出し側で判断できます。
GPIO操作を抽象化する
#include <stdint.h>
#include <stdbool.h>
typedef struct
{
void (*scl_drive_low)(void);
void (*scl_release)(void);
void (*sda_drive_low)(void);
void (*sda_release)(void);
bool (*scl_read)(void);
bool (*sda_read)(void);
void (*delay_us)(uint32_t us);
} I2C_BusIo;
重要なのは、
scl_drive_low()
scl_release()
としたところです。
あえて、
scl_high()
としていません。
HIGHに強制出力するのではなく、
release
つまり「バスを解放する」という意味にしています。
この方がI2CのOpen Drain動作を正しく表現できます。
SCLがHIGHになるまで待つ
Targetによっては、
Clock Stretching
を行う場合があります。
Clock Stretchingとは、Targetが、
ちょっと待って
という意味でSCLをLOWのまま保持する仕組みです。
NXPの仕様でも、Clock StretchingはTargetがSCLをLOWに保持することでトランザクションを一時停止する仕組みとして説明されています。
そのため、
scl_release();
delay_us(5);
だけではなく、
本当にSCLがHIGHになったか
確認する方が安全です。
static bool wait_scl_high(
const I2C_BusIo *io,
uint32_t timeout_us)
{
uint32_t elapsed = 0U;
io->scl_release();
while (!io->scl_read())
{
if (elapsed >= timeout_us)
{
return false;
}
io->delay_us(1U);
elapsed++;
}
return true;
}
1クロック送る関数
static bool i2c_recovery_clock(
const I2C_BusIo *io,
uint32_t half_period_us,
uint32_t scl_timeout_us)
{
/*
* SCL LOW
*/
io->scl_drive_low();
io->delay_us(half_period_us);
/*
* SCLを解放する。
* プルアップ抵抗によってHIGHになる。
*/
io->scl_release();
/*
* Clock StretchingやSCL張り付きも考慮して、
* 本当にHIGHになったか確認する。
*/
if (!wait_scl_high(io, scl_timeout_us))
{
return false;
}
io->delay_us(half_period_us);
return true;
}
STOP Conditionを生成する
static bool i2c_generate_stop(
const I2C_BusIo *io,
uint32_t half_period_us,
uint32_t scl_timeout_us)
{
/*
* まずSCL LOWにする。
*/
io->scl_drive_low();
/*
* SDA LOWにする。
*
* この時点ではSCLがLOWなので、
* SDA HIGH→LOWになってもSTARTにはならない。
*/
io->sda_drive_low();
io->delay_us(half_period_us);
/*
* SCLを解放してHIGHにする。
*/
io->scl_release();
if (!wait_scl_high(io, scl_timeout_us))
{
return false;
}
io->delay_us(half_period_us);
/*
* SCL HIGH状態で
* SDA LOW → HIGH
*
* これがSTOP Condition。
*/
io->sda_release();
io->delay_us(half_period_us);
return io->sda_read();
}
完成版:I2C Bus Recovery
実際に使用できる形へまとめます。
#include <stdint.h>
#include <stdbool.h>
#define I2C_RECOVERY_CLOCKS (9U)
#define I2C_RECOVERY_HALF_PERIOD_US (5U)
#define I2C_RECOVERY_SCL_TIMEOUT_US (1000U)
typedef enum
{
I2C_RECOVERY_OK = 0,
I2C_RECOVERY_SDA_STUCK,
I2C_RECOVERY_SCL_STUCK,
I2C_RECOVERY_INVALID_ARG
} I2C_RecoveryResult;
typedef struct
{
void (*scl_drive_low)(void);
void (*scl_release)(void);
void (*sda_drive_low)(void);
void (*sda_release)(void);
bool (*scl_read)(void);
bool (*sda_read)(void);
void (*delay_us)(uint32_t us);
} I2C_BusIo;
static bool wait_scl_high(
const I2C_BusIo *io,
uint32_t timeout_us)
{
uint32_t elapsed = 0U;
io->scl_release();
while (!io->scl_read())
{
if (elapsed >= timeout_us)
{
return false;
}
io->delay_us(1U);
elapsed++;
}
return true;
}
static bool i2c_recovery_clock(
const I2C_BusIo *io,
uint32_t half_period_us,
uint32_t scl_timeout_us)
{
io->scl_drive_low();
io->delay_us(half_period_us);
io->scl_release();
if (!wait_scl_high(io, scl_timeout_us))
{
return false;
}
io->delay_us(half_period_us);
return true;
}
static bool i2c_generate_stop(
const I2C_BusIo *io,
uint32_t half_period_us,
uint32_t scl_timeout_us)
{
/*
* SDAをLOWに変更するときにSCL HIGHだと
* START Conditionになるため、
* 先にSCLをLOWへ落とす。
*/
io->scl_drive_low();
io->sda_drive_low();
io->delay_us(half_period_us);
/*
* SCLをHIGHへ解放。
*/
io->scl_release();
if (!wait_scl_high(io, scl_timeout_us))
{
return false;
}
io->delay_us(half_period_us);
/*
* SCL HIGH中に
* SDA LOW → HIGH。
*
* STOP Condition。
*/
io->sda_release();
io->delay_us(half_period_us);
return io->sda_read();
}
I2C_RecoveryResult I2C_BusRecovery(
const I2C_BusIo *io)
{
uint8_t i;
/*
* NULLチェック
*/
if ((io == NULL) ||
(io->scl_drive_low == NULL) ||
(io->scl_release == NULL) ||
(io->sda_drive_low == NULL) ||
(io->sda_release == NULL) ||
(io->scl_read == NULL) ||
(io->sda_read == NULL) ||
(io->delay_us == NULL))
{
return I2C_RECOVERY_INVALID_ARG;
}
/*
* 最初は両方解放する。
*/
io->sda_release();
io->scl_release();
io->delay_us(I2C_RECOVERY_HALF_PERIOD_US);
/*
* SCLがLOWの場合。
*
* この場合はクロック自体を生成できないため、
* バスクリアよりHW RESETやPower Cycleを検討する。
*/
if (!io->scl_read())
{
return I2C_RECOVERY_SCL_STUCK;
}
/*
* SDAもHIGHなら既にBus Free。
*/
if (io->sda_read())
{
return I2C_RECOVERY_OK;
}
/*
* SDAがLOW。
*
* 最大9クロック送る。
*/
for (i = 0U; i < I2C_RECOVERY_CLOCKS; i++)
{
if (!i2c_recovery_clock(
io,
I2C_RECOVERY_HALF_PERIOD_US,
I2C_RECOVERY_SCL_TIMEOUT_US))
{
return I2C_RECOVERY_SCL_STUCK;
}
/*
* TargetがSDAを解放したか確認。
*/
if (io->sda_read())
{
break;
}
}
/*
* 9クロック送ってもSDA LOWなら復帰失敗。
*/
if (!io->sda_read())
{
return I2C_RECOVERY_SDA_STUCK;
}
/*
* STOP Conditionを作る。
*/
if (!i2c_generate_stop(
io,
I2C_RECOVERY_HALF_PERIOD_US,
I2C_RECOVERY_SCL_TIMEOUT_US))
{
return I2C_RECOVERY_SCL_STUCK;
}
/*
* 最終確認。
*/
if (!io->scl_read())
{
return I2C_RECOVERY_SCL_STUCK;
}
if (!io->sda_read())
{
return I2C_RECOVERY_SDA_STUCK;
}
return I2C_RECOVERY_OK;
}
実際のマイコン側ではどう使う?
例えば、マイコン固有のGPIO関数が、
GPIO_SCL_LOW();
GPIO_SCL_INPUT();
GPIO_SDA_LOW();
GPIO_SDA_INPUT();
GPIO_READ_SCL();
GPIO_READ_SDA();
delay_us();
だとします。
ラッパー関数を作ります。
static void Board_SclLow(void)
{
GPIO_SCL_LOW();
}
static void Board_SclRelease(void)
{
GPIO_SCL_INPUT();
}
static void Board_SdaLow(void)
{
GPIO_SDA_LOW();
}
static void Board_SdaRelease(void)
{
GPIO_SDA_INPUT();
}
static bool Board_SclRead(void)
{
return GPIO_READ_SCL();
}
static bool Board_SdaRead(void)
{
return GPIO_READ_SDA();
}
static void Board_DelayUs(uint32_t us)
{
delay_us(us);
}
そして、
static const I2C_BusIo g_i2cBusIo =
{
.scl_drive_low = Board_SclLow,
.scl_release = Board_SclRelease,
.sda_drive_low = Board_SdaLow,
.sda_release = Board_SdaRelease,
.scl_read = Board_SclRead,
.sda_read = Board_SdaRead,
.delay_us = Board_DelayUs
};
とします。
呼び出し側は、
I2C_RecoveryResult result;
result = I2C_BusRecovery(&g_i2cBusIo);
switch (result)
{
case I2C_RECOVERY_OK:
/*
* 復帰成功
*/
break;
case I2C_RECOVERY_SDA_STUCK:
/*
* SDAがまだLOW。
*
* Target HW RESETや
* Power Cycleを検討。
*/
break;
case I2C_RECOVERY_SCL_STUCK:
/*
* SCL LOW。
*
* Clock Stretching異常、
* Target異常、
* 配線短絡などを確認。
*/
break;
default:
/*
* プログラム異常
*/
break;
}
のようにできます。
6クロック方式を残したい場合
既存機器の仕様として、
6クロック送信
が必要なのであれば、クロック数を固定値にせず引数にしておくと便利です。
例えば、
I2C_RecoveryResult I2C_BusRecoveryClock(
const I2C_BusIo *io,
uint8_t clocks);
として、
I2C_BusRecoveryClock(&g_i2cBusIo, 6U);
とすれば6回、
I2C_BusRecoveryClock(&g_i2cBusIo, 9U);
なら一般的な9回にできます。
ただし、設計書には、
6クロック:
対象デバイス固有の復帰仕様
9クロック:
I2C一般的なBus Clear
と理由を残しておくことが重要です。
「昔のソフトが6回だったから」
だけで6回にすると、将来別のI2Cデバイスへ変更したときに問題になる可能性があります。
General Call Resetとは?
もう一つの復帰方法として、
General Call Reset
があります。
これはI2Cバス上に特殊なアドレスとコマンドを送って、対応しているTargetをソフトウェアリセットする方法です。
General Call Address
通常のI2Cでは、
0x38
0x40
0x50
など、Targetごとのアドレスへ通信します。
一方General Callでは、
0x00
という特殊なアドレスを使用します。
Software Resetでは、その後に、
0x06
を送ります。
つまり概念的には、
START
↓
General Call
0x00
↓
Software Reset command
0x06
↓
STOP
となります。
NXPの仕様ではGeneral Callの後に0x06を送る2バイトシーケンスによってSoftware Resetとなります。ただし、この機能はオプションであり、すべてのI²Cデバイスが対応するわけではありません。
General Call ResetのC言語例
一般的なHALを仮定すると、
#define I2C_GENERAL_CALL_ADDRESS (0x00U)
#define I2C_SOFTWARE_RESET_CMD (0x06U)
bool I2C_GeneralCallReset(void)
{
uint8_t command;
command = I2C_SOFTWARE_RESET_CMD;
if (!I2C_Write(
I2C_GENERAL_CALL_ADDRESS,
&command,
1U))
{
return false;
}
return true;
}
という形になります。
ただし、
使用前に必ずTargetのデータシートを確認してください。
General Call Resetの注意点
特に重要です。
同じI2Cバスに、
Device A
Device B
Device C
が接続されている場合、
General Callに対応している複数デバイスが、
同時にRESET
される可能性があります。
つまり、
このセンサーだけリセットしたい
という用途には向かない場合があります。
また、一部のI2CデバイスはGeneral Call自体に対応していません。General Call AddressやSoftware ResetはI²C仕様上Optionalです。
したがって、
使えるはず
ではなく、
対象ICのデータシートに
General Call / Software Reset対応と明記されているか
を必ず確認します。
HW RESET端子を使う
個人的に実務上非常に有効なのが、
Target専用RESET
です。
センサーやI/O Expanderなどに、
RESET
RST
RESET_N
RST_N
端子がある場合は、MCUから制御できるようにしておくと復帰性が大きく向上します。
例えば、
void Sensor_HardwareReset(void)
{
SENSOR_RESET_LOW();
delay_ms(10U);
SENSOR_RESET_HIGH();
delay_ms(20U);
}
です。
NXP仕様でも、SCLがLOWに張り付いた場合は、デバイスにHW RESET入力があればRESETを使う方法が優先されています。
電源再投入による復帰
RESET端子がない場合、
Targetの電源だけOFF
↓
少し待つ
↓
ON
という方法もあります。
これを、
Power Cycle
と呼びます。
例えば電源スイッチICをGPIOで制御できるなら、
void Sensor_PowerCycle(void)
{
SENSOR_POWER_OFF();
delay_ms(100U);
SENSOR_POWER_ON();
delay_ms(100U);
}
とできます。
I2C仕様でも、HW RESETがない場合には電源再投入によってPower-On Resetを動作させる方法が示されています。
I2Cペリフェラル自身の再初期化
意外と忘れやすいのが、
TargetではなくMCU側I2C回路が異常
というケースです。
例えば、
I2C BUSYフラグが戻らない
割り込み状態がおかしい
内部ステートマシンが止まった
などです。
この場合、
I2C停止
↓
内部フラグクリア
↓
GPIO Bus Recovery
↓
I2C再初期化
とすることがあります。
概念的には、
void I2C_Reinitialize(void)
{
I2C_Disable();
I2C_ClearErrors();
I2C_Init();
I2C_Enable();
}
です。
実際の関数名や必要な初期化手順は、
RX
STM32
PIC
nRF
AVR
などMCUによって異なります。
必ず使用しているMCUのハードウェアマニュアルを確認してください。
おすすめの段階的復帰アルゴリズム
実務では、
いきなり電源再投入
ではなく、軽い復帰処理から試す方がおすすめです。
例えば、
通信エラー
↓
① Retry
↓ NG
② I2C peripheral reset
↓ NG
③ Bus状態確認
↓
④ SDA LOWなら9 Clock Bus Clear
↓ NG
⑤ STOP生成
↓ NG
⑥ Target Software Reset
↓ NG
⑦ Target HW Reset
↓ NG
⑧ Target Power Cycle
↓ NG
⑨ システム異常通知
という段階構成です。
C言語での復帰管理例
typedef enum
{
I2C_RECOVER_OK = 0,
I2C_RECOVER_RETRY_FAILED,
I2C_RECOVER_BUS_CLEAR_FAILED,
I2C_RECOVER_DEVICE_RESET_FAILED,
I2C_RECOVER_FATAL
} I2C_RecoverStatus;
I2C_RecoverStatus I2C_Recover(void)
{
I2C_RecoveryResult result;
/*
* Step 1:
* MCU I2C Peripheral停止
*/
I2C_Disable();
/*
* Step 2:
* GPIOモードへ切り替え
*/
I2C_PinModeGpio();
/*
* Step 3:
* Bus Clear
*/
result = I2C_BusRecovery(&g_i2cBusIo);
/*
* Step 4:
* I2C機能へ戻す
*/
I2C_PinModePeripheral();
/*
* Step 5:
* I2C再初期化
*/
I2C_Init();
if (result == I2C_RECOVERY_OK)
{
return I2C_RECOVER_OK;
}
/*
* SCL/SDAが戻らない場合は、
* TargetのHW RESET等を実行。
*/
Sensor_HardwareReset();
/*
* 再確認
*/
if (I2C_CheckCommunication())
{
return I2C_RECOVER_OK;
}
return I2C_RECOVER_FATAL;
}
このように、
GPIOレベルのBus Recoveryと、システム全体のRecovery処理を分ける
ことがポイントです。
「復帰できた」の判定も重要
初心者がやりがちな実装が、
9クロック送りました
↓
復帰成功
としてしまうことです。
これは危険です。
本当に確認すべきなのは、
SCL HIGH?
SDA HIGH?
TargetがACKを返す?
Device IDが読める?
必要なレジスタが正常?
です。
つまり、復帰処理を実行したことと、復帰したことは別です。
復帰確認の例
bool I2C_CheckCommunication(void)
{
uint8_t value;
/*
* 例:
* WHO_AM_Iなど
* 読み取り専用レジスタを読む。
*/
if (!Sensor_ReadRegister(
SENSOR_DEVICE_ID_REG,
&value))
{
return false;
}
if (value != SENSOR_EXPECTED_ID)
{
return false;
}
return true;
}
このように、
既知の値が返るレジスタ
を使って復帰確認をすると信頼性が上がります。
タイムアウトは必ず入れる
組み込みで非常に重要です。
次のような処理は危険です。
while (I2C_BUSY())
{
}
I2Cが永久にBUSYなら、
CPUも永久にここから進まない
からです。
必ずタイムアウトを入れます。
bool I2C_WaitReady(uint32_t timeout_ms)
{
uint32_t start;
start = GetTickMs();
while (I2C_BUSY())
{
if ((GetTickMs() - start) >= timeout_ms)
{
return false;
}
}
return true;
}
無限Retryも避ける
これも重要です。
while (!I2C_Read())
{
/*
* 成功するまで永遠にRetry
*/
}
は避けましょう。
おすすめは、
#define I2C_MAX_RETRY (3U)
for (uint8_t retry = 0U;
retry < I2C_MAX_RETRY;
retry++)
{
if (I2C_Read())
{
break;
}
}
です。
Retry回数を超えたら、
Bus Recovery
へ移行します。
バス復帰で大切な優先順位
初心者エンジニアの方に最も覚えてもらいたいのは、
「復帰処理を入れること」より、「なぜ止まったのかを残すこと」
です。
例えば復帰処理だけ入れて、
通信異常
↓
自動復帰
↓
正常
となると、一見問題なく見えます。
しかし本当は、
電源ノイズ
EMC
プルアップ抵抗不適切
Clock Stretching
MCUのバグ
センサー電源異常
が隠れているかもしれません。
そこで、
LogError(
"I2C recovery: SDA=%d SCL=%d error=%d",
SDA_READ(),
SCL_READ(),
error_code);
などで記録しておくことが重要です。
実務ではログに何を残す?
最低限おすすめなのは、
発生日時
Target Address
処理内容
エラーコード
SDA状態
SCL状態
Retry回数
Recovery方法
Recovery結果
です。
例えば、
2026-10-14 10:15:22
I2C ERROR
ADDR=0x38
SDA=0
SCL=1
RETRY=3
RECOVERY=BUS_CLEAR
CLOCKS=4
RESULT=OK
のように残せると、不具合解析に非常に役立ちます。
Bus Recoveryを連続実行しない
復帰処理にも回数制限を入れるべきです。
例えば、
#define I2C_RECOVERY_MAX_COUNT (3U)
として、
Recovery 1回
↓
失敗
Recovery 2回
↓
失敗
Recovery 3回
↓
失敗
↓
異常確定
とします。
無限にBus Recoveryを繰り返すと、
本当はハード故障なのに
ソフトが永久に復帰処理している
状態になるためです。
プルアップ抵抗も確認する
I2Cトラブルが多い場合、
ソフトだけ
を疑うのは危険です。
I2Cは電気的な条件の影響を強く受けます。
特に、
- プルアップ抵抗
- 配線長
- バス容量
- 電源電圧
- ノイズ
- GND
- レベル変換
- コネクタ接触
- 電源立ち上がり順序
を確認します。
ロジックアナライザを使う
I2Cのトラブル解析では、
ロジックアナライザ
が非常に便利です。
ロジックアナライザとは、
SDA
SCL
などのデジタル信号を時間軸で記録する測定器です。
例えば、
START
ADDRESS
ACK
DATA
NACK
STOP
を確認できます。
バスハング直前の波形を取れると、
ACK待ちで止まった
途中のbitでMCUがRESETした
SCLが上がっていない
SDAをTargetが保持している
などが分かります。
初心者向け:まず何を確認すればいい?
I2Cが止まったら、次の順番がおすすめです。
① SDAを測る
② SCLを測る
③ ControllerのBUSY状態確認
④ Target電源確認
⑤ Retry
⑥ I2Cペリフェラル再初期化
⑦ SDA LOWなら最大9クロック
⑧ STOP生成
⑨ General Call Reset対応確認
⑩ HW RESET
⑪ Power Cycle
⑫ 原因解析
9クロック、General Call Resetの違い
最後に整理します。
| 方法 | 用途 | 注意点 |
|---|---|---|
| 9クロック | SDA LOWの一般的Bus Clear | I2C仕様に基づく |
| STOP生成 | 通信終了を明示 | SCL HIGH中にSDA LOW→HIGH |
| I2C再初期化 | MCU側異常 | MCU依存 |
| General Call Reset | 対応TargetをSoftware Reset | 全Target対応ではない |
| HW RESET | Target状態初期化 | RESET端子が必要 |
| Power Cycle | 最終的なTarget初期化 | システム影響を考慮 |
おすすめの実装方針
実務では、
通信API
↓
Retry管理
↓
Recovery Manager
├── Peripheral Reset
├── Bus Clear
├── Software Reset
├── Hardware Reset
└── Power Cycle
のように階層化すると保守しやすくなります。
特に、
I2C_BusRecovery()
を特定のセンサー専用処理にしないことが重要です。
GPIOアクセスを抽象化しておけば、
温度センサー
EEPROM
DAC
ADC
I/O Expander
距離センサー
など複数デバイスで再利用できます。
まとめ
I2Cのバス復帰で最も重要なのは、
「エラーが起きたから、とりあえずクロックを何回か出す」ではなく、SDAとSCLの状態を見て適切な復帰方法を選択すること
です。
特に覚えておきたいポイントは次の通りです。
- I2CはSDAとSCLの2線通信
- Bus Freeでは通常SDA/SCLともHIGH
- SDA LOWならTargetが通信途中で止まっている可能性がある
- 一般的なBus ClearはSCLを最大9クロック
- バスクリア後はSTOP Conditionを生成する
- GPIOでHIGHを強制出力せずOpen Drain動作を意識する
- SCL LOWではクロック方式だけでは復帰できない場合がある
- SCL LOWではHW RESETやPower Cycleを検討する
- General Call +
0x06でSoftware ResetできるTargetもある - General Call ResetはOptionalなのでデータシート確認が必須
- I2Cペリフェラル自身の再初期化も必要
- すべての待ち処理にTimeoutを入れる
- RetryとRecoveryの最大回数を決める
- 復帰処理だけでなく原因解析用ログを残す
- 復帰後は実際にTargetとの通信確認を行う
特に組み込み製品では、I2Cエラーそのものを完全にゼロにすることだけでなく、
「異常が発生しても安全に検出し、復帰し、原因を追跡できる設計」
が重要です。
Bus Recoveryは単なるテクニックではなく、製品の信頼性を高めるための重要な設計要素として考えるとよいでしょう。
