I2C 오류 처리: 센서 먹통을 키우는 코딩 실수
I2C 센서가 가끔 멈출 때 전원이나 배선부터 바꾸고 있지는 않나요? 같은 회로가 재부팅 후 다시 작동한다면 원인은 하드웨어보다 오류를 무시하는 임베디드 코드에 있을 가능성이 큽니다. 특히 아두이노 프로젝트에서는 짧은 예제 코드를 그대로 제품이나 장시간 측정 장치에 옮긴 뒤, 센서 한 번의 응답 실패가 시스템 전체 먹통으로 번지는 일이 흔합니다.
I2C 반환값을 읽지 않으면 실패가 정상처럼 보입니다
endTransmission 결과를 버리는 실수
Wire.endTransmission()을 호출하기만 하고 반환값을 확인하지 않으면 주소 응답 실패, 데이터 전송 실패, 기타 버스 오류를 구분할 수 없습니다. 센서가 값을 주지 않았는데도 직전 측정값이나 초기화되지 않은 변수가 출력되어 사용자는 장치가 정상이라고 오해합니다. 숫자가 표시된다는 사실과 새로운 측정에 성공했다는 사실은 전혀 다릅니다.
실패 사례에서 자주 보이는 코드는 오류가 나도 다음 레지스터 읽기를 계속 시도합니다. 그러면 시리얼 모니터에는 0, 255 또는 비현실적인 온도처럼 그럴듯한 값이 나타납니다. 아두이노의 개념과 활용 범위를 이해하는 입문 단계에서는 예제의 간결함이 유용하지만, 실제 프로젝트에서는 반드시 오류 분기를 추가해야 합니다.
- 0: 전송 성공으로 처리하되 읽은 바이트 수도 다시 확인합니다.
- 0 이외의 값: 오류 횟수와 발생 시각을 기록하고 측정값을 폐기합니다.
- 실패한 값을 0으로 저장하지 말고 별도의 유효성 플래그를 둡니다.
- 연속 실패와 일시 실패를 구분할 카운터를 준비합니다.
센서 값보다 먼저 저장해야 할 정보는 ‘이번 통신이 성공했는가’입니다. 오류 상태가 없는 측정 로그는 고장 분석에 거의 도움이 되지 않습니다.
읽을 바이트 수를 확인하지 않는 코드는 조용히 망가집니다
requestFrom 다음의 성급한 read 호출
Wire.requestFrom()을 실행했다고 해서 요청한 데이터가 모두 도착한 것은 아닙니다. 두 바이트를 요청했지만 한 바이트만 수신됐는데 곧바로 Wire.read()를 두 번 호출하면, 두 번째 값은 유효한 센서 데이터가 아닐 수 있습니다. 통신이 빠르게 반복될수록 이런 문제는 간헐적으로 보여 재현하기도 어렵습니다.
예를 들어 16비트 온도 레지스터를 읽을 때 상위 바이트만 들어오면 계산 결과가 갑자기 큰 폭으로 튑니다. 초보자는 센서 보정 문제라고 판단해 보정 상수를 수정하지만, 실제 원인은 수신 길이 검증 누락입니다. 보정값을 건드리면 정상 통신 때의 값까지 틀어져 문제가 더 커집니다. 센서 연결 코드에는 요청 수량, 실제 수신 수량, 데이터 조립 순서가 함께 보여야 합니다.
requestFrom()의 반환값을 요청 바이트 수와 비교합니다.Wire.available()이 필요한 수량 이상인지 검사합니다.- 부족하면 해당 측정 주기를 실패 처리하고 버퍼를 비웁니다.
- 상위·하위 바이트 순서와 부호 확장 규칙을 데이터시트에서 확인합니다.
- 성공한 경우에만 공유 변수나 화면 표시 값을 갱신합니다.
센서가 100번 중 한 번만 짧게 응답해도 괜찮다고 생각하기 쉽습니다. 그러나 하루에 수만 번 측정하는 사물인터넷 장치라면 작은 실패율도 반복적인 이상값으로 이어집니다. 값 범위 검사까지 넣어 섭씨 온도가 갑자기 수백 도가 되는 데이터는 전송 전에 차단하는 편이 안전합니다.
무한 재시도는 복구가 아니라 시스템 정지입니다
응답이 올 때까지 기다리는 while문의 함정
센서 초기화에 실패했을 때 while(true)로 멈추거나, 성공할 때까지 쉬지 않고 재시도하는 코드는 시연에서는 단순해 보입니다. 하지만 현장 장치에서는 네트워크 전송, 버튼 입력, 워치독 갱신 같은 다른 작업까지 모두 중단시킵니다. 센서 하나가 빠졌다는 이유로 전체 사물인터넷 장치가 응답하지 않는 구조가 되는 셈입니다.
실패 직후 수천 번 연속 접근하면 버스 상태가 회복될 시간도 주지 못합니다. 전원 변동이나 센서 내부 처리 지연이 원인이었다면 과도한 재시도가 오히려 복구 가능성을 낮춥니다. 횟수가 제한된 재시도와 점진적인 대기 시간을 사용하고, 한도를 넘으면 기능을 축소한 상태로 메인 루프를 계속 실행해야 합니다.
- 첫 실패 후에는 짧게 기다린 뒤 같은 명령을 한 번 더 시도합니다.
- 2~3회 연속 실패하면 센서 재초기화 단계로 이동합니다.
- 재초기화도 실패하면 1초, 5초, 30초처럼 접근 간격을 늘립니다.
- 통신 불능 상태에서도 사용자 입력과 원격 상태 보고는 유지합니다.
- 재시도 횟수, 마지막 성공 시각, 복구 횟수를 로그에 남깁니다.
가령 온습도 센서가 빠진 환경 모니터라면 조도 측정과 네트워크 연결까지 멈출 이유는 없습니다. 오류 코드를 반환하는 센서 모듈, 이를 판단하는 애플리케이션, 상태를 표시하는 화면을 분리하면 고장 범위를 제한할 수 있습니다. 이런 계층 분리는 디바이스 드라이버 통합개발환경의 개요처럼 드라이버 중심 개발 흐름을 이해할 때도 도움이 됩니다.
초기화 명령을 한 번에 몰아넣지 마세요
센서 준비 시간을 무시한 연속 쓰기
전원을 켜자마자 ID 레지스터를 읽고 설정값을 연속으로 쓰는 코드는 보드에 따라 성공하기도 하고 실패하기도 합니다. 마이크로컨트롤러는 즉시 실행을 시작하지만 센서는 내부 발진기, 비휘발성 설정 복사, 자체 진단을 마친 뒤에야 I2C 명령을 받을 수 있기 때문입니다. 데이터시트에 전원 인가 후 대기 시간이나 리셋 완료 비트가 제시되어 있다면 임의의 짧은 지연으로 대체해서는 안 됩니다.
또 다른 실수는 소프트 리셋 명령을 전송한 직후 다음 설정 레지스터를 쓰는 것입니다. 리셋 과정에서 센서가 잠시 응답하지 않는데 코드가 이를 고장으로 판단해 다시 리셋하면 영원히 초기화 단계를 벗어나지 못할 수 있습니다. 초기화를 상태 기계로 나누고 각 단계의 성공 조건과 시간 제한을 명시하면 이 반복을 피할 수 있습니다.
- 전원 안정화 시간을 확보하고 버스 핀 상태를 확인합니다.
- 장치 ID를 읽어 예상 모델과 일치하는지 검사합니다.
- 리셋 후에는 고정 지연보다 준비 상태 비트를 우선 확인합니다.
- 설정 레지스터를 쓴 뒤 가능하면 다시 읽어 적용 여부를 검증합니다.
- 첫 측정값이 유효해질 때까지 필요한 변환 시간을 기다립니다.
센서 모듈이 바뀌었는데 주소만 같다고 기존 초기화 코드를 재사용하는 것도 피해야 합니다. 같은 계열 제품이라도 기본 측정 모드, 레지스터 폭, 명령 뒤 대기 시간이 다를 수 있습니다. 아두이노 관련 용어 설명을 참고하되, 구체적인 레지스터 동작은 반드시 사용 중인 센서의 최신 데이터시트와 라이브러리 릴리스 정보를 기준으로 확인해야 합니다.
인터럽트와 여러 작업에서 Wire를 동시에 부르지 마세요
재진입과 공유 데이터가 만드는 간헐적 고장
타이머 인터럽트 안에서 센서를 읽으면 일정한 주기를 얻을 수 있을 것 같지만, 많은 아두이노용 I2C 구현은 인터럽트 문맥에서 안전한 재진입을 보장하지 않습니다. 통신 완료를 기다리는 코드가 인터럽트 동작에 의존하는 환경이라면 인터럽트 안의 Wire 호출이 끝나지 않을 수도 있습니다. 메인 루프에서도 동시에 버스를 사용하면 내부 버퍼와 전송 상태가 꼬이는 원인이 됩니다.
RTOS 기반 임베디드 개발에서도 센서 태스크, 화면 태스크, 저장 태스크가 각자 I2C 장치를 호출하면 충돌이 발생합니다. 주소가 서로 달라도 물리적인 버스와 드라이버 자원은 공유됩니다. 테스트할 때는 멀쩡하다가 로깅이나 화면 갱신 주기가 겹치는 순간 멈춘다면 동시 접근 제어 누락을 의심해야 합니다.
- 인터럽트에서는 측정 시점을 알리는 플래그만 설정합니다.
- 실제 I2C 통신은 메인 루프나 전용 버스 태스크에서 수행합니다.
- RTOS에서는 뮤텍스로 전체 트랜잭션을 보호합니다.
- 레지스터 주소 쓰기와 데이터 읽기를 하나의 원자적 작업처럼 묶습니다.
- 공유 측정값은 복사 도중 바뀌지 않도록 임계 구역을 최소한으로 적용합니다.
버스 잠금은 바이트 하나가 아니라 ‘레지스터를 선택하고 결과를 읽는 전 과정’에 걸어야 합니다. 중간에 다른 태스크가 끼어들면 정상 응답도 엉뚱한 레지스터 값이 됩니다.
잠금을 오래 유지하는 것도 좋지 않습니다. 센서 변환을 시작한 뒤 수십 밀리초를 기다려야 한다면 잠금 상태로 대기하지 말고, 명령 전송과 결과 수집을 별도 상태로 나누세요. 그래야 기다리는 동안 OLED나 다른 센서가 같은 I2C 통신 버스를 효율적으로 사용할 수 있습니다.
오류 로그를 덮어쓰는 세 가지 습관이 원인을 숨깁니다
성공 메시지만 남기는 테스트 방식
마지막으로 현장에서 가장 많이 놓치는 실수는 오류 자체보다 기록 방식에 있습니다. 첫째, 재시도가 성공하면 앞선 실패를 없던 일처럼 처리합니다. 둘째, 시리얼 출력이 느리다는 이유로 오류 코드를 모두 제거합니다. 셋째, 센서 값을 정수 하나로만 저장해 통신 성공 여부와 측정 시각을 잃어버립니다. 이 세 가지 습관은 간헐적 장애를 ‘원인 불명’으로 만듭니다.
모든 바이트를 출력할 필요는 없습니다. 실패 시각, 장치 주소, 수행한 작업, 반환 코드, 요청·수신 바이트 수, 연속 실패 횟수만 고정 형식으로 남겨도 분석 속도가 크게 달라집니다. 메모리가 작은 아두이노라면 최근 오류 몇 건을 순환 버퍼에 보관하고, 라즈베리파이나 서버로 전송할 때 묶어서 보내는 방식이 실용적입니다.
- 실수 1: 재시도 성공 후 오류 카운터를 즉시 0으로 만들어 장애 빈도를 숨깁니다.
- 실수 2: 센서 값 0과 통신 실패를 같은 데이터로 저장해 실제 영점 측정을 구분하지 못합니다.
- 실수 3: 오류가 날 때마다 긴 문자열을 출력해 통신 주기와 타이밍을 바꿉니다.
- 개선: 숫자 코드와 타임스탬프를 짧게 기록하고 상세 해석은 수집 도구에서 수행합니다.
장시간 운용 전에는 센서 케이블을 잠깐 분리하거나 장치를 의도적으로 응답하지 않게 만들어 보세요. 시스템이 멈추지 않고 오류를 표시하며, 연결이 돌아왔을 때 제한된 절차로 복구되는지 확인해야 합니다. 정상 조건에서 값이 잘 나온다는 테스트만 통과한 코드는 아직 완성된 I2C 센서 코드가 아닙니다.

- 다음글아두이노 I2C 센서와 아날로그 센서, 한 달 써본 차이 26.09.04
등록된 댓글이 없습니다.
