기술 가이드

I²C 7비트 주소 혼동 해결: STM32 HAL·Arduino·Linux 변환 규칙

0x68·0xD0·0xD1이 같은 I²C 장치를 가리키는 이유와 API별 주소 인수를 설명합니다. C 예제로 이중 시프트와 잘못된 주소 쌍을 검사합니다.

목차

I²C 센서가 응답하지 않을 때 먼저 확인할 항목은 주소 숫자의 의미입니다. 같은 센서를 데이터시트는 0xD0/0xD1, Arduino 예제는 0x68, STM32 HAL 호출은 0xD0으로 표현할 수 있습니다. 값이 다르다는 이유만으로 서로 다른 장치라고 판단하면 시프트를 두 번 적용하는 버그가 생깁니다.

이 글은 7비트 I²C 장치 주소에 한정합니다. 10비트 주소, I3C 동적 주소, 센서 내부 레지스터 주소는 별도로 다뤄야 합니다. 예제의 0x68과 레지스터 0x0F는 계산을 설명하기 위한 가상 조합이며 특정 센서의 읽기 명령이 아닙니다.

장치 주소와 버스의 첫 바이트 구분하기

7비트 주소를 A라고 하면 버스에 보내는 주소 바이트는 쓰기에서 A × 2, 읽기에서 A × 2 + 1입니다. 마지막 비트가 R/W 방향을 나타내며 ACK/NACK는 그 뒤의 별도 클록에서 확인합니다. NXP I²C 규격 UM10204의 주소 형식을 기준으로 구분할 수 있습니다.

표현 가상 장치 0x68의 값 의미
7비트 장치 주소 0x68 애플리케이션에서 보관할 기준값
버스 쓰기 주소 바이트 0xD0 주소 비트와 R/W=0
버스 읽기 주소 바이트 0xD1 주소 비트와 R/W=1
내부 레지스터 주소 예 0x0F 장치가 정의하는 별도 데이터

로직 애널라이저도 디코더 설정에 따라 0x68 Read 또는 원시 바이트 0xD1을 보여줄 수 있습니다. 비교할 때는 화면의 주소 표시 방식부터 맞추세요. 내부 레지스터 주소에 장치 주소용 시프트를 적용해서는 안 됩니다.

API 경계에서 한 번만 변환하기

장치 설정과 공통 드라이버 계층에는 sensor_addr7처럼 의미를 드러내는 이름으로 7비트 주소를 보관합니다. 플랫폼별 어댑터에서만 필요한 변환을 수행하면 이식할 때 수정 범위를 줄일 수 있습니다.

호출 지점 0x68 장치에 전달하는 값 공식 근거
Arduino Wire.beginTransmission 0x68 인수는 장치의 7비트 주소
STM32F4 HAL_I2C_Mem_Read의 DevAddress 0xD0 문서의 7비트 주소를 왼쪽으로 시프트
Linux ioctl의 I2C_SLAVE, 7비트 모드 0x68 인수 하위 7비트에 주소 전달

Arduino의 공식 레퍼런스 원문은 7비트 인수를 명시합니다. STM32F4 HAL 드라이버HAL_I2C_Mem_Read 주석은 호출 전에 주소를 시프트하도록 안내합니다. STM32 HAL의 이 읽기 함수에도 DevAddress는 0xD0을 전달하고, 사용자가 읽기 비트를 붙여 0xD1로 바꿀 필요는 없습니다. 사용 중인 MCU 계열과 드라이버 버전의 인수 설명을 다시 확인하세요.

Linux에서는 커널의 사용자 공간 I²C 문서에 따라 I2C_SLAVE에 주소를 설정합니다. 읽기·쓰기 데이터 버퍼 앞에 주소 바이트를 임의로 추가하지 않습니다. 레지스터 지정 후 repeated START가 필요한 장치는 일반 writeread를 나눠 부르는 것으로 충분한지 확인해야 합니다. 해당 조건의 결합 전송은 어댑터 기능을 확인한 뒤 I2C_RDWR 등 적합한 인터페이스로 구성합니다.

PC에서 실행하는 주소 변환 검증

아래 전체 코드를 i2c_address.c로 저장합니다. Addr7은 값의 의미를 표시하는 작은 구조체이고, 입력은 먼저 16비트로 받아 범위를 확인한 다음 8비트로 저장합니다. 이렇게 해야 범위가 큰 값이 형 변환에서 잘린 뒤 유효한 주소로 통과하는 일을 피할 수 있습니다.

#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>

typedef struct { uint8_t value; } Addr7;

/* Bit-range check only; reserved addresses need a separate policy. */
static bool addr7_make(uint16_t input, Addr7 *out)
{
if (out == NULL || input > 0x7Fu) return false;
out->value = (uint8_t)input;
return true;
}

static uint16_t stm32_hal_address(Addr7 address)
{
return (uint16_t)((uint16_t)address.value << 1);
}

static uint8_t bus_address_byte(Addr7 address, bool read)
{
return (uint8_t)(stm32_hal_address(address) | (read ? 1u : 0u));
}

/* Use only when the datasheet explicitly supplies write/read bytes. */
static bool addr7_from_pair(uint16_t write_byte,
uint16_t read_byte, Addr7 *out)
{
if (write_byte > 0xFFu || read_byte > 0xFFu) return false;
if ((write_byte & 1u) != 0u) return false;
if (read_byte != (write_byte | 1u)) return false;
return addr7_make((uint16_t)(write_byte >> 1), out);
}

int main(void)
{
Addr7 address = {0};
assert(addr7_make(0x68u, &address));
assert(stm32_hal_address(address) == 0xD0u);
assert(bus_address_byte(address, false) == 0xD0u);
assert(bus_address_byte(address, true) == 0xD1u);
assert(addr7_from_pair(0xA0u, 0xA1u, &address));
assert(address.value == 0x50u);
assert(!addr7_from_pair(0xA1u, 0xA0u, &address));
assert(!addr7_from_pair(0xA0u, 0xA3u, &address));
assert(!addr7_from_pair(0x100u, 0x101u, &address));
assert(!addr7_from_pair(0xA0u, 0xA1u, NULL));
assert(!addr7_make(0x68u, NULL));

for (uint16_t a = 0; a < 128u; ++a) {
Addr7 restored = {0};
assert(addr7_make(a, &address));
uint8_t w = bus_address_byte(address, false);
uint8_t r = bus_address_byte(address, true);
assert(w / 2u == a && r == w + 1u);
assert(addr7_from_pair(w, r, &restored));
assert(restored.value == a);
}
for (uint32_t n = 0; n < 65536u; ++n) {
address.value = 0x55u;
bool valid = addr7_make((uint16_t)n, &address);
assert(valid == (n < 128u));
assert(address.value == (valid ? n : 0x55u));
}
puts("PASS: 128 round trips, 65536 inputs, known vectors");
return 0;
}

GCC가 설치된 환경에서 gcc -std=c11 -Wall -Wextra -Werror i2c_address.c -o i2c_address로 빌드한 뒤 Linux/macOS에서는 ./i2c_address, Windows PowerShell에서는 ./i2c_address.exe를 실행합니다. 이 검증은 assert를 사용하므로 NDEBUG를 정의하지 마세요.

예상 출력은 PASS: 128 round trips, 65536 inputs, known vectors입니다. 이번 게시 준비에서는 Windows의 GCC 13.2.0으로 빌드·실행했습니다. 128개 7비트 패턴의 왕복 변환, 65,536개 입력의 범위 검사, 알려진 주소 쌍과 잘못된 쌍의 거부를 확인했습니다. 실제 보드의 ACK, 전압, 타이밍은 이 호스트 테스트의 검증 범위가 아닙니다.

코드의 범위 검사는 비트 표현 검사입니다. 128개 값 전부를 일반 센서 주소로 사용해도 된다는 뜻은 아닙니다. 예약 주소와 특별 용도는 규격·장치 문서에 따라 별도의 정책으로 제한해야 합니다. Addr7 필드를 외부에서 임의로 바꾸면 검사도 우회되므로 실제 라이브러리에서는 생성 함수와 어댑터를 모듈 안에 두세요.

숫자만 보고 자동 판별하면 실패하는 이유

0xA0/0xA1이 데이터시트에서 쓰기·읽기 주소 바이트로 명시되었다면 기준 주소는 0x50입니다. 반면 0x50 자체는 정상적인 7비트 값이므로, 숫자가 작다는 이유만으로 이미 시프트된 값인지 구별할 수 없습니다. 0x50을 다시 오른쪽으로 시프트하면 다른 주소 0x28이 됩니다.

따라서 입력을 자동으로 추정하는 범용 함수보다 addr7_makeaddr7_from_pair처럼 입력 형식을 호출자가 선택하는 경로가 낫습니다. 코드의 쌍 검사 역시 두 값의 수학적 관계만 확인합니다. 데이터시트에서 해당 값이 실제 주소 바이트라고 설명하는지까지 증명하지는 않습니다.

센서 레지스터 읽기에서 확인할 순서

전형적인 레지스터 읽기는 다음 흐름으로 생각할 수 있습니다. 센서가 다른 규약을 사용하면 해당 데이터시트를 우선합니다.

  1. START 뒤에 쓰기 주소 바이트 0xD0을 보냅니다.
  2. 장치의 ACK 후 내부 레지스터 번호 0x0F를 보냅니다.
  3. 장치가 요구하는 repeated START 뒤에 읽기 주소 바이트 0xD1을 보냅니다.
  4. 정해진 길이를 읽고 마지막 바이트의 NACK와 STOP을 컨트롤러/드라이버 규칙대로 처리합니다.

주소 단계에서 NACK가 나면 기준 주소, 주소 선택 핀, 장치 전원, SDA/SCL 연결과 풀업부터 확인합니다. 주소에 ACK가 있었는데 데이터만 다르면 레지스터 번호·폭, 읽기 길이, 장치 준비 시간과 바이트 순서를 이어서 조사합니다. ACK가 있다는 사실만으로 원하는 센서가 연결되었다고 단정하지 말고, 가능하면 장치 식별 레지스터를 확인하세요.

프로젝트에 적용할 점검표

  • 주소 상수 이름에 addr7 또는 hal_addr처럼 표현을 남겼는가?
  • STM32 어댑터에서만 한 번 시프트하고 공통 설정은 그대로 유지하는가?
  • R/W 비트와 내부 레지스터 주소를 별개로 취급하는가?
  • 같은 버스의 주소 충돌과 주소 선택 핀 상태를 확인했는가?
  • 호스트 계산 테스트와 실제 보드의 로직 애널라이저 측정 결과를 따로 기록했는가?

이 구분은 STM32 펌웨어 개발임베디드 Linux BSP 개발 사이에서 센서 드라이버를 이식할 때도 유용합니다. 공식 문서 확인일은 2026년 9월 21일이며, API 링크가 가리키는 최신 소스와 프로젝트에 고정된 버전은 다를 수 있습니다.

관련 글

처리 중입니다...

잠시만 기다려주세요.