
똑같은 VL53L0X 센서를 2개, 3개, 혹은 그 이상 연결하고 싶을 때 가장 저렴하고 스마트한 방법을 소개합니다. 핵심은 **”잠든 센서를 하나씩 깨워서 새 이름을 지어주는 것”**입니다.
1. 하드웨어 연결의 핵심: XSHUT 핀
VL53L0X에는 XSHUT이라는 핀이 있습니다. 이 핀은 센서를 Standby(대기) 상태로 만드는 핀입니다.
- XSHUT에 0(GND)을 주면: 센서가 꺼집니다 (I2C 통신 안 함).
- XSHUT에 1(VCC)을 주면: 센서가 켜집니다 (기본 주소
0x29로 응답 시작).
연결 방법:
- VCC, GND, SCL, SDA는 모든 센서가 공유하도록 병렬로 연결합니다.
- XSHUT 핀만은 공유하지 않고, MCU(ESP32, Arduino 등)의 개별 디지털 핀에 각각 하나씩 연결합니다.
2. 소프트웨어 전략: “순차적 깨우기와 이름표 달기”
전원이 들어오는 순간 모든 센서는 주소가 0x29로 같습니다. 이 상태에서 통신하면 충돌이 나겠죠? 그래서 다음과 같은 순차적 초기화 로직을 사용합니다.
- 전원 OFF: 모든 센서의 XSHUT 핀을
LOW로 내려서 전부 잠재웁니다. - 첫 번째 센서 깨우기: 1번 센서의 XSHUT만
HIGH로 올립니다. 이제 1번 센서만0x29주소로 응답합니다. - 주소 변경: MCU가
0x29주소로 명령을 보내 “너의 주소를0x30으로 바꿔!”라고 지시합니다. - 두 번째 센서 깨우기: 2번 센서의 XSHUT을
HIGH로 올립니다. 2번 센서는 방금 깬 것이므로 기본 주소인0x29로 응답합니다. (1번은 이미0x30이 되었으므로 충돌하지 않습니다.) - 반복: 위 과정을 센서 개수만큼 반복하여 모든 센서에 고유 주소(
0x30,0x31,0x32…)를 할당합니다.
3. MicroPython 예제 코드 (ESP32-C3 기준)
이 로직을 코드로 구현하면 다음과 같습니다.
Python
from machine import Pin, I2C
import time
import vl53l0x
i2c = I2C(0, sda=Pin(0), scl=Pin(1))
# XSHUT 핀 설정
xshut = [Pin(2, Pin.OUT), Pin(3, Pin.OUT), Pin(4, Pin.OUT)]
# 1. 모든 센서 종료
for p in xshut: p.value(0)
time.sleep(0.1)
sensors = []
addresses = [0x30, 0x31, 0x32]
# 2. 순차적으로 깨워서 주소 변경
for i in range(3):
xshut[i].value(1) # 하나씩 깨우기
time.sleep(0.1)
# 깨어난 센서(기본 0x29)를 찾아서 새 주소 할당
s = vl53l0x.VL53L0X(i2c)
s.set_address(addresses[i])
s.init()
sensors.append(s)
print("모든 센서 주소 할당 완료!")
4. 이 방식의 장단점
장점:
- 비용 제로: 추가 부품 없이 MCU 핀만 더 사용하면 됩니다.
- 유연성: 소프트웨어적으로 주소를 마음대로 정할 수 있습니다.
단점:
- 핀 소모: 센서 개수만큼 MCU의 GPIO 핀이 필요합니다. (핀이 부족하면 TCA9548A 같은 멀티플렉서 칩을 써야 합니다.)
- 휘발성: 전원이 꺼지면 주소 설정이 날아갑니다. 부팅 시마다 초기화 코드를 실행해야 합니다.
마무리하며
VL53L0X를 여러 개 써서 자율주행 로봇이나 레이더를 만들고 싶다면, 이 XSHUT 제어 방식이 가장 빠르고 효율적인 정답입니다. 핵심은 **”모두 끄고, 하나씩 켜서, 이름을 바꿔준다”**는 것만 기억하세요!
Tip: I2C 라인에 풀업 저항이 적절히 달려있는지 확인하세요. 센서가 많아질수록 통신 속도를 조금 낮추는 것이 안정적입니다.
※ VL53L0X처럼 소프트웨어로 I2C 주소를 동적으로 변경하는 방식은 임베디드 설계에서 꽤나 영리하고 효율적인 트릭입니다. 보통은 납땜을 하거나 스위치를 조절해야 하는데, 이 방식은 핀 하나(XSHUT)만 더 써서 수십 개의 동일 센서를 제어할 수 있게 해주니까요.
이런 방식을 지원하거나, I2C 주소 충돌을 해결하기 위해 사용하는 다른 부품과 방법들을 소개해 드릴게요.
1. VL53L0X와 같은 방식을 쓰는 부품들
이 방식은 주로 “Daisy Chain” 초기화가 필요한 정밀 센서들에서 종종 발견됩니다.
- VL53L1X / VL53L3CX (STMicroelectronics): VL53L0X의 형제 모델들입니다. 작동 방식이 완전히 같아서 똑같이
XSHUT핀을 이용해 주소를 할당합니다. - PCA9685 (PWM 드라이버): 기본적으로는 점퍼 납땜형이지만, 소프트웨어 레지스터 설정을 통해 “All Call” 주소나 개별 주소 응답 여부를 제어할 수 있는 기능이 있습니다.
- 일부 스마트 배터리 관리 IC (BMS): 여러 개의 배터리 셀을 감시할 때, 초기 부팅 시 순차적으로 주소를 부여받는 방식을 사용하기도 합니다.
2. 하드웨어 주소 핀이 있는 부품 (가장 일반적)
대부분의 I2C 부품(OLED, 센서 등)은 주소 변경을 위해 ADR, ADDR, A0, A1 등의 핀을 가지고 있습니다.
- 동작 원리: 이 핀에
GND를 연결하느냐,VCC를 연결하느냐에 따라 주소가0x3C또는0x3D로 결정됩니다. - 한계: 보통 선택권이 2개(0 또는 1)뿐이라, 같은 센서를 3개 이상 쓰려면 이 방법만으로는 부족합니다.
3. 주소 충돌을 해결하는 ‘진짜’ 해결사: I2C Multiplexer
사용자님이 이번에 구현하신 XSHUT 제어 방식이 소프트웨어적인 트릭이라면, 하드웨어적으로 가장 깔끔한 해결책은 TCA9548A 같은 I2C 멀티플렉서를 쓰는 것입니다.
- 작동 방식: 입구는 하나인데 출구가 8개인 “I2C 스위치”라고 보시면 됩니다.
- 특징:
- 모든 센서의 주소가
0x29로 같아도 상관없습니다. - MCU가 “지금은 1번 통로만 열어줘”라고 명령하면 1번 센서와만 통신합니다.
- VL53L0X를 8개, 16개씩 써야 하는 대형 프로젝트에서는
XSHUT핀을 일일이 제어하는 것보다 이 칩 하나를 쓰는 게 핀 절약에 훨씬 유리합니다.
- 모든 센서의 주소가
4. 왜 이런 방식을 쓸까요?
I2C는 원래 **”주소가 고정되어 있다”**는 전제로 만들어진 규격입니다. 하지만 현실에서는 똑같은 센서를 여러 방항(왼쪽, 오른쪽, 중앙)에 달아야 할 일이 많죠.
- XSHUT 방식의 장점: 추가 비용(멀티플렉서 칩 값)이 들지 않고, MCU 핀만 넉넉하다면 무한정 확장 가능합니다.
- XSHUT 방식의 단점: 전원이 꺼지면 주소 설정이 날아가기 때문에,
boot.py나 초기화 루틴에서 항상 순차적으로 깨우는 코드가 반드시 들어가야 합니다.
Python vl53l0x.py
import time
from machine import I2C
class VL53L0X:
def __init__(self, i2c, address=0x29):
self.i2c = i2c
self.address = address
self.init()
def _read_reg(self, reg, length=1):
return self.i2c.readfrom_mem(self.address, reg, length)
def _write_reg(self, reg, data):
if isinstance(data, int):
data = bytes([data])
self.i2c.writeto_mem(self.address, reg, data)
def init(self):
# 기본 초기화 시퀀스 (단순화 버전)
self._write_reg(0x88, 0x00)
self._write_reg(0x80, 0x01)
self._write_reg(0xFF, 0x01)
self._write_reg(0x00, 0x00)
self._write_reg(0x91, self._read_reg(0x91))
self._write_reg(0x00, 0x01)
self._write_reg(0xFF, 0x00)
self._write_reg(0x80, 0x00)
def start_continuous(self):
self._write_reg(0x00, 0x02) # 계속 측정 모드 시작
def read(self):
# 거리 데이터 읽기 (High byte, Low byte 조합)
data = self._read_reg(0x14, 12)
dist = (data[10] << 8) | data[11]
if dist == 20 or dist == 8190: # 측정 오류 또는 범위 초과 값 처리
return 0
return dist
def set_address(self, new_address):
self.i2c.writeto_mem(self.address, 0x8A, bytes([new_address & 0x7F]))
self.address = new_address
time.sleep(0.01)
Python multi.py
import time
from machine import Pin, I2C
import ssd1306
import vl53l0x
# 1. I2C 설정
i2c = I2C(0, sda=Pin(0), scl=Pin(1), freq=400000)
def print_i2c_scan(step_name):
devices = i2c.scan()
hex_devices = [hex(d) for d in devices]
print(f"[{step_name}] I2C Scan: {hex_devices}")
# 2. XSHUT 핀 설정 (모두 OFF 상태로 시작)
xshut1 = Pin(2, Pin.OUT)
xshut2 = Pin(3, Pin.OUT)
xshut3 = Pin(4, Pin.OUT)
xshut1.value(0)
xshut2.value(0)
xshut3.value(0)
time.sleep(0.2)
print_i2c_scan("Initial (All OFF)")
# 3. 센서 초기화 및 주소 할당
sensors = []
def init_sensors():
# --- Sensor 1 ---
xshut1.value(1)
time.sleep(0.1)
s1 = vl53l0x.VL53L0X(i2c)
s1.set_address(0x30)
s1.init()
s1.start_continuous()
sensors.append(s1)
print("Sensor 1 (0x30) Started")
# --- Sensor 2 ---
xshut2.value(1)
time.sleep(0.1)
s2 = vl53l0x.VL53L0X(i2c)
s2.set_address(0x31)
s2.init()
s2.start_continuous()
sensors.append(s2)
print("Sensor 2 (0x31) Started")
# --- Sensor 3 ---
xshut3.value(1)
time.sleep(0.1)
s3 = vl53l0x.VL53L0X(i2c)
s3.set_address(0x32)
s3.init()
s3.start_continuous()
sensors.append(s3)
print("Sensor 3 (0x32) Started")
# 4. OLED 설정
try:
display = ssd1306.SSD1306_I2C(128, 64, i2c)
print("OLED Ready (0x3c)")
except:
display = None
print("OLED Not Found")
def update_display(dist1, dist2, dist3):
if display:
display.fill(0)
display.text("--- DISTANCE ---", 0, 0)
display.text(f"S1: {dist1:4d} mm", 0, 20)
display.text(f"S2: {dist2:4d} mm", 0, 35)
display.text(f"S3: {dist3:4d} mm", 0, 50)
display.show()
# 메인 루프
try:
init_sensors()
print_i2c_scan("Final Configuration")
# 플로터 범례 가독성을 위해 출력
print("\n--- Plotter Mode: S1,S2,S3 ---")
while True:
# 기존에 잘되던 순차적 읽기 방식
d1 = sensors[0].read()
time.sleep(0.01)
d2 = sensors[1].read()
time.sleep(0.01)
d3 = sensors[2].read()
time.sleep(0.01)
# 1. 시리얼 플로터 출력 (쉼표로 구분)
# 0이 나오는 것도 그대로 그래프에 찍히게 두었습니다.
print(f"{d1:5},{d2:5},{d3:5}")
# 2. OLED 업데이트
update_display(d1, d2, d3)
# 전체 루프 주기
time.sleep(0.02)
except Exception as e:
print("Runtime Error:", e)



답글 남기기