센서마다 GPIO 핀을 하나씩 배정하는 방식은 핀 개수 제한과 배선 복잡도 때문에 금방 한계에 부딪힌다. I2C는 SDA/SCL 두 핀만으로 여러 센서를 동시에 연결하는 버스 프로토콜이고, BME280 같은 저가형 온습도/기압 센서는 대부분 I2C를 표준 지원한다. 다만 레지스터 레벨 프로토콜이라 주소·레지스터·보정(calibration) 계수를 모르면 디버깅이 막막해진다. 이 글에서는 python-smbus2로 라즈베리파이 I2C 버스를 직접 다루며 BME280 레지스터를 읽고 보정 계수를 적용해 실제 값을 뽑아내는 과정을 정리한다.
I2C 버스 기본 개념
SDA(데이터)/SCL(클록) 두 선을 쓰고, 라즈베리파이 GPIO 헤더에는 풀업이 내장돼 있어 대부분 별도 저항이 필요 없다. 각 장치는 7비트 주소(0x03~0x77)를 가지며 마스터(라즈베리파이)가 주소를 보내면 일치하는 슬레이브만 응답하는 구조다. 대부분의 센서는 “이 주소의 이 레지스터를 읽어라/써라” 패턴으로 통신하고, smbus2의 read_byte_data(addr, register) 같은 API가 이를 그대로 반영한다. 40핀 헤더에 노출된 버스는 보통 1번(/dev/i2c-1)이며, smbus2는 이 디바이스 파일에 대한 ioctl 호출을 감싼 라이브러리다.
I2C 인터페이스 활성화와 장치 확인
sudo raspi-config
# Interface Options -> I2C -> Yes 선택 후 재부팅
# 헤드리스 환경이면 /boot/firmware/config.txt(구버전 /boot/config.txt)에 직접 추가
dtparam=i2c_arm=on
sudo apt update
sudo apt install -y i2c-tools python3-pip python3-venv
i2cdetect -y 1
BME280(SDA=GPIO2/핀3, SCL=GPIO3/핀5)을 연결 후 스캔하면 아래처럼 나온다. SDO를 GND에 연결하면 0x76, VDDIO에 연결하면 0x77.
$ i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- --
...
70: -- -- -- -- -- -- 76 -- -- -- -- -- -- -- -- --
python-smbus2로 레지스터 read/write
python3 -m venv venv
source venv/bin/activate
pip install smbus2
SMBus를 컨텍스트 매니저로 열면 read_byte_data/write_byte_data/read_i2c_block_data로 레지스터에 직접 접근한다.
from smbus2 import SMBus
BME280_ADDR = 0x76
with SMBus(1) as bus:
chip_id = bus.read_byte_data(BME280_ADDR, 0xD0) # BME280은 0x60
print(f"chip id: 0x{chip_id:02X}")
bus.write_byte_data(BME280_ADDR, 0xF2, 0x01)
block = bus.read_i2c_block_data(BME280_ADDR, 0x88, 24) # burst read
print(block)측정 전 세 레지스터 설정이 필요하다: 0xF2 습도 오버샘플링, 0xF4 온도/기압 오버샘플링+모드, 0xF5 standby/IIR 필터.
REG_CTRL_HUM = 0xF2
REG_CTRL_MEAS = 0xF4
REG_CONFIG = 0xF5
bus.write_byte_data(BME280_ADDR, REG_CTRL_HUM, 0x01) # 습도 오버샘플링 x1
bus.write_byte_data(BME280_ADDR, REG_CTRL_MEAS, 0x27) # 온도/기압 x1, normal mode
bus.write_byte_data(BME280_ADDR, REG_CONFIG, 0xA0) # standby 1000ms, filter off원시 데이터 읽기와 보정 계수 적용
raw ADC 값은 그대로 못 쓴다 — 센서마다 공장에서 측정한 고유 보정 계수를 레지스터에서 읽어 데이터시트 공식에 대입해야 실제 값이 나온다.
| 레지스터 | 이름 | 설명 |
|---|---|---|
| 0x88–0x9F | dig_T1~T3, dig_P1~P9 | 온도/기압 보정 계수 (24바이트) |
| 0xA1 | dig_H1 | 습도 보정 계수 1바이트 |
| 0xE1–0xE7 | dig_H2~H6 | 습도 보정 계수 (7바이트, 일부 12비트 packed) |
| 0xF7–0xF9 | press_msb/lsb/xlsb | 기압 raw 값 (20비트) |
| 0xFA–0xFC | temp_msb/lsb/xlsb | 온도 raw 값 (20비트) |
| 0xFD–0xFE | hum_msb/lsb | 습도 raw 값 (16비트) |
0xF7~0xFE 8바이트를 한 번에 burst read하면 세 값을 동시에 얻는다(개별 read보다 시점 차이 없이 정확). 보정 계수 파싱부터 최종 값 계산까지 전체 스크립트:
import time
import struct
from smbus2 import SMBus
I2C_BUS = 1
BME280_ADDR = 0x76 # SDO를 GND에 연결한 경우. VDDIO에 연결하면 0x77
REG_CTRL_HUM = 0xF2
REG_CTRL_MEAS = 0xF4
REG_CONFIG = 0xF5
REG_CALIB_00 = 0x88
REG_CALIB_26 = 0xE1
REG_DATA = 0xF7
class BME280:
def __init__(self, bus, addr=BME280_ADDR):
self.bus = bus
self.addr = addr
self._load_calibration()
self._configure()
def _load_calibration(self):
calib1 = self.bus.read_i2c_block_data(self.addr, REG_CALIB_00, 26)
calib2 = self.bus.read_i2c_block_data(self.addr, REG_CALIB_26, 7)
(self.dig_T1, self.dig_T2, self.dig_T3,
self.dig_P1, self.dig_P2, self.dig_P3, self.dig_P4,
self.dig_P5, self.dig_P6, self.dig_P7, self.dig_P8,
self.dig_P9) = struct.unpack('<HhhHhhhhhhhh', bytes(calib1[0:24]))
self.dig_H1 = calib1[25]
self.dig_H2, self.dig_H3 = struct.unpack('<hB', bytes(calib2[0:3]))
e4, e5, e6 = calib2[3], calib2[4], calib2[5]
self.dig_H4 = self._to_signed12((e4 << 4) | (e5 & 0x0F))
self.dig_H5 = self._to_signed12((e6 << 4) | (e5 >> 4))
self.dig_H6 = struct.unpack('<b', bytes([calib2[6]]))[0]
@staticmethod
def _to_signed12(value):
if value & 0x800:
value -= 1 << 12
return value
def _configure(self):
self.bus.write_byte_data(self.addr, REG_CTRL_HUM, 0x01) # 습도 x1
self.bus.write_byte_data(self.addr, REG_CTRL_MEAS, 0x27) # 온도/기압 x1, normal
self.bus.write_byte_data(self.addr, REG_CONFIG, 0xA0) # standby 1000ms
time.sleep(0.1)
def _read_raw(self):
data = self.bus.read_i2c_block_data(self.addr, REG_DATA, 8)
adc_p = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
adc_t = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
adc_h = (data[6] << 8) | data[7]
return adc_t, adc_p, adc_h
def read(self):
adc_t, adc_p, adc_h = self._read_raw()
temperature, t_fine = self._compensate_temperature(adc_t)
pressure = self._compensate_pressure(adc_p, t_fine)
humidity = self._compensate_humidity(adc_h, t_fine)
return temperature, pressure, humidity
def _compensate_temperature(self, adc_t):
var1 = (adc_t / 16384.0 - self.dig_T1 / 1024.0) * self.dig_T2
var2 = ((adc_t / 131072.0 - self.dig_T1 / 8192.0) ** 2) * self.dig_T3
t_fine = var1 + var2
return t_fine / 5120.0, t_fine
def _compensate_pressure(self, adc_p, t_fine):
var1 = t_fine / 2.0 - 64000.0
var2 = var1 * var1 * self.dig_P6 / 32768.0
var2 = var2 + var1 * self.dig_P5 * 2.0
var2 = var2 / 4.0 + self.dig_P4 * 65536.0
var1 = (self.dig_P3 * var1 * var1 / 524288.0 + self.dig_P2 * var1) / 524288.0
var1 = (1.0 + var1 / 32768.0) * self.dig_P1
if var1 == 0:
return 0.0
pressure = 1048576.0 - adc_p
pressure = (pressure - var2 / 4096.0) * 6250.0 / var1
var1 = self.dig_P9 * pressure * pressure / 2147483648.0
var2 = pressure * self.dig_P8 / 32768.0
pressure = pressure + (var1 + var2 + self.dig_P7) / 16.0
return pressure / 100.0 # hPa
def _compensate_humidity(self, adc_h, t_fine):
var_h = t_fine - 76800.0
var_h = (adc_h - (self.dig_H4 * 64.0 + self.dig_H5 / 16384.0 * var_h)) * (
self.dig_H2 / 65536.0 * (1.0 + self.dig_H6 / 67108864.0 * var_h *
(1.0 + self.dig_H3 / 67108864.0 * var_h))
)
var_h = var_h * (1.0 - self.dig_H1 * var_h / 524288.0)
return max(0.0, min(100.0, var_h))
if __name__ == "__main__":
with SMBus(I2C_BUS) as bus:
sensor = BME280(bus)
while True:
temp, pressure, humidity = sensor.read()
print(f"온도: {temp:.2f} C, 기압: {pressure:.2f} hPa, 습도: {humidity:.2f} %")
time.sleep(1)주의할 점: dig_H4/dig_H5는 12비트 단위로 니블이 겹쳐 저장돼 데이터시트 비트 배치를 정확히 따라야 습도 값이 안 튄다. dig_T1/dig_P1은 unsigned, 나머지는 signed라 struct.unpack 포맷(H/h)을 정확히 구분해야 한다.
$ python3 bme280_reader.py
온도: 26.43 C, 기압: 1012.87 hPa, 습도: 47.12 %
온도: 26.44 C, 기압: 1012.85 hPa, 습도: 47.09 %
주의사항
- PermissionError: [Errno 13] —
sudo usermod -aG i2c $USER후 로그아웃/재로그인(또는newgrp i2c).sudo로 스크립트 실행은 venv 꼬임 등 부작용이 있어 비권장. - OSError: [Errno 121] Remote I/O error — 배선(SDA/SCL 바뀜), 전원, 주소(0x76 vs 0x77) 확인.
- 동일 주소 센서 충돌 — BME280은 SDO로 0x76/0x77 두 주소만 가능. 3개 이상이면 TCA9548A 멀티플렉서나 별도 버스 필요.
- t_fine 계산 순서 — 온도 보정을 가장 먼저 호출해야 한다. 기압/습도 공식이 그
t_fine을 재사용하므로 순서를 바꾸면 값이 완전히 틀어진다. - 클록 스트레칭 불안정 — BCM283x 하드웨어 I2C가 일부 장치의 클록 스트레칭을 완벽히 지원 못 함.
dtparam=i2c_arm_baudrate=10000으로 속도를 낮추거나i2c-gpio오버레이로 우회.
마무리
I2C는 “주소로 장치를 고르고 레지스터 번호로 읽거나 쓰는” 규칙의 반복이고, smbus2는 이를 얇게 감싼 API일 뿐이다. BME280에서 익힌 “제어 레지스터 설정 → burst read → 보정 계수 적용” 흐름은 다른 I2C 센서에도 레지스터 맵만 바꿔 그대로 재사용할 수 있다.