I²C 센서가 응답하지 않을 때 무조건 9클록을 내보내면 다른 컨트롤러와 충돌하거나, 아직 살아 있는 대상 장치에 원치 않는 비트를 보낼 수 있습니다. 먼저 SCL과 SDA 중 어느 선이 LOW인지 확인하고, 컨트롤러가 버스를 독점하는 구성인지 판단해야 합니다. 이 글은 NXP의 I²C 명세 3.1.16절과 Linux 커널의 결함 주입 문서를 바탕으로 복구 경계를 정리하고, GPIO 동작을 가짜 버스로 시험하는 C 예제를 제공합니다.
1. 두 선의 상태부터 분리하기
| 관측 |
의미와 조치 |
| SCL HIGH, SDA HIGH |
버스가 풀린 상태입니다. 복구 클록을 보내지 않습니다. |
| SCL LOW |
다른 장치의 클록 스트레칭 또는 물리적 고착 가능성이 있습니다. GPIO에서 HIGH를 강제로 출력하지 말고 제한 시간을 둔 뒤 하드웨어 리셋 또는 전원 복구를 검토합니다. |
| SCL HIGH, SDA LOW |
대상 장치가 바이트 중간에 멈춘 경우가 있습니다. 단일 컨트롤러가 버스를 소유하고 있음을 확인한 뒤 최대 9클록을 시도합니다. |
NXP UM10204 3.1.16 Bus clear는 SCL이 LOW에 고착되면 장치 리셋 또는 전원 재인가를 우선 제시합니다. SDA가 LOW라면 컨트롤러가 9개 클록을 보내고, 그래도 풀리지 않으면 리셋 또는 전원 재인가를 제시합니다. 9개는 보장된 복구 횟수가 아니라 시도 상한입니다.
멀티 컨트롤러 버스, 다른 작업이 진행 중인 버스, 전압이나 핀 설정을 모르는 보드에서는 이 예제를 바로 적용하지 마세요. 복구 전에 I²C 주변장치를 중지하고, GPIO를 오픈드레인으로 전환하며, 풀업 전압·핀 소유권·클록 스트레칭 제한 시간을 보드 설계와 대조해야 합니다. HIGH는 출력으로 밀어 올리지 않고 핀을 놓아 풀업에 맡깁니다.
2. 최대 9클록과 STOP 생성
클록마다 SCL을 LOW로 내린 다음 놓고, 실제로 HIGH가 되었는지 읽습니다. SCL이 HIGH가 되지 않으면 더 진행하지 않습니다. 각 상승 구간에서 SDA를 읽고, 놓인 순간 나머지 클록을 생략합니다. SDA가 풀렸다면 SCL LOW일 때 SDA를 LOW로 만든 뒤 SCL을 놓고, SCL HIGH에서 SDA를 놓아 STOP을 만듭니다. 9개를 모두 보냈는데 SDA가 여전히 LOW면 성공으로 기록하지 않습니다.
Linux 커널 I²C 결함 주입 문서는 복구 클록이 장치에 임의 데이터를 쓸 위험을 설명합니다. 따라서 SDA 해제 여부를 매 클록 확인하고, 필요한 STOP을 만들어야 합니다. 클록이 나왔다고 트랜잭션이 성공한 것은 아닙니다. 복구 후 I²C 주변장치를 다시 초기화하고 대상 장치의 상태 레지스터와 명령 재시도 정책을 따로 확인하세요.
3. 호스트에서 시험하는 C 예제
다음 코드는 핀 입출력을 콜백으로 분리해 버스가 이미 정상인 경우, 3번째·9번째 클록에 SDA가 풀리는 경우, SDA가 계속 LOW인 경우, SCL이 LOW인 경우를 시험합니다. delay()의 100회 반복은 샘플 모형의 유한 대기 횟수일 뿐 실제 시간 단위가 아닙니다. 실기에서는 MCU 타이머를 이용해 보드의 I²C 속도와 클록 스트레칭 한계에 맞춰 제한 시간을 정하세요.
i2c_bus_clear.c로 저장하고 gcc -std=c11 -Wall -Wextra -Werror i2c_bus_clear.c -o i2c_bus_clear로 빌드한 뒤 실행하세요. Windows PowerShell에서는 .\i2c_bus_clear.exe, Linux에서는 ./i2c_bus_clear입니다.
#include <stdbool.h>
#include <stdio.h>
typedef struct {
void *ctx;
void (*scl_low)(void *);
void (*scl_release)(void *);
void (*sda_low)(void *);
void (*sda_release)(void *);
bool (*read_scl)(void *);
bool (*read_sda)(void *);
void (*delay)(void *);
} Bus;
typedef enum { BUS_IDLE, BUS_CLEARED, SCL_STUCK, SDA_STUCK } Result;
/* The caller must disable the I2C controller and own an idle, single-controller bus. */
static bool wait_scl_high(Bus *bus) {
for (unsigned n = 0; n < 100; ++n) {
if (bus->read_scl(bus->ctx)) return true;
bus->delay(bus->ctx);
}
return false;
}
static Result clear_bus(Bus *bus) {
bus->sda_release(bus->ctx); /* Open-drain: never drive a logic high. */
bus->scl_release(bus->ctx);
if (!wait_scl_high(bus)) return SCL_STUCK;
if (bus->read_sda(bus->ctx)) return BUS_IDLE;
for (unsigned pulse = 0; pulse < 9; ++pulse) {
bus->scl_low(bus->ctx);
bus->delay(bus->ctx);
bus->scl_release(bus->ctx);
if (!wait_scl_high(bus)) return SCL_STUCK;
bus->delay(bus->ctx);
if (bus->read_sda(bus->ctx)) {
/* Form a STOP: SDA low while SCL low, then SDA rises while SCL high. */
bus->scl_low(bus->ctx);
bus->sda_low(bus->ctx);
bus->delay(bus->ctx);
bus->scl_release(bus->ctx);
if (!wait_scl_high(bus)) {
bus->sda_release(bus->ctx);
return SCL_STUCK;
}
bus->delay(bus->ctx);
bus->sda_release(bus->ctx);
bus->delay(bus->ctx);
return bus->read_sda(bus->ctx) ? BUS_CLEARED : SDA_STUCK;
}
}
return SDA_STUCK;
}
typedef struct {
bool scl_high, sda_high, force_scl_low, sda_driven;
unsigned rises, release_after, stops;
} Mock;
static void scl_low(void *p) { ((Mock *)p)->scl_high = false; }
static void scl_release(void *p) {
Mock *m = p;
if (!m->scl_high && !m->force_scl_low) ++m->rises;
m->scl_high = !m->force_scl_low;
if (!m->sda_driven && m->release_after && m->rises >= m->release_after)
m->sda_high = true;
}
static void sda_low(void *p) {
((Mock *)p)->sda_high = false;
((Mock *)p)->sda_driven = true;
}
static void sda_release(void *p) {
Mock *m = p;
if (m->scl_high && m->sda_driven) ++m->stops;
m->sda_driven = false;
if (m->release_after && m->rises >= m->release_after) m->sda_high = true;
}
static bool read_scl(void *p) { return ((Mock *)p)->scl_high; }
static bool read_sda(void *p) { return ((Mock *)p)->sda_high; }
static void delay_tick(void *p) { (void)p; }
static Result trial(Mock *m) {
Bus b = {m, scl_low, scl_release, sda_low, sda_release,
read_scl, read_sda, delay_tick};
return clear_bus(&b);
}
int main(void) {
Mock idle = { .scl_high = true, .sda_high = true };
Mock third = { .scl_high = true, .release_after = 3 };
Mock ninth = { .scl_high = true, .release_after = 9 };
Mock never = { .scl_high = true };
Mock clock_low = { .force_scl_low = true };
if (trial(&idle) != BUS_IDLE || idle.rises != 0) return 1;
if (trial(&third) != BUS_CLEARED || third.rises < 3 || third.stops != 1) return 2;
if (trial(&ninth) != BUS_CLEARED || ninth.rises < 9 || ninth.stops != 1) return 3;
if (trial(&never) != SDA_STUCK || never.rises != 9) return 4;
if (trial(&clock_low) != SCL_STUCK || clock_low.rises != 0) return 5;
puts("PASS: idle, release at 3/9, SDA stuck, SCL stuck");
return 0;
}
예상 출력:
PASS: idle, release at 3/9, SDA stuck, SCL stuck
이 모형은 실제 GPIO 전압, 상승 시간, 다른 컨트롤러와의 중재, 센서 재기동을 검증하지 않습니다. 펌웨어에 옮길 때는 실패 결과를 로그에 남기고, 반복 복구 횟수에 상한을 두며, 물리 버스에서 로직 애널라이저로 SCL·SDA와 STOP을 확인하세요.
공식 참고: NXP I²C 버스 명세 UM10204, Linux 커널 I²C 복구 결함 주입 안내.
댓글
첫 댓글을 남겨 보세요.