레이저 거리 센서 VL53L0X, XSHUT 핀으로 다중연결 (2/2)

똑같은 VL53L0X 센서를 2개, 3개, 혹은 그 이상 연결하고 싶을 때 가장 저렴하고 스마트한 방법을 소개합니다. 핵심은 **”잠든 센서를 하나씩 깨워서 새 이름을 지어주는 것”**입니다.

1. 하드웨어 연결의 핵심: XSHUT 핀

VL53L0X에는 XSHUT이라는 핀이 있습니다. 이 핀은 센서를 Standby(대기) 상태로 만드는 핀입니다.

  • XSHUT에 0(GND)을 주면: 센서가 꺼집니다 (I2C 통신 안 함).
  • XSHUT에 1(VCC)을 주면: 센서가 켜집니다 (기본 주소 0x29로 응답 시작).

연결 방법:

  1. VCC, GND, SCL, SDA는 모든 센서가 공유하도록 병렬로 연결합니다.
  2. XSHUT 핀만은 공유하지 않고, MCU(ESP32, Arduino 등)의 개별 디지털 핀에 각각 하나씩 연결합니다.

2. 소프트웨어 전략: “순차적 깨우기와 이름표 달기”

전원이 들어오는 순간 모든 센서는 주소가 0x29로 같습니다. 이 상태에서 통신하면 충돌이 나겠죠? 그래서 다음과 같은 순차적 초기화 로직을 사용합니다.

  1. 전원 OFF: 모든 센서의 XSHUT 핀을 LOW로 내려서 전부 잠재웁니다.
  2. 첫 번째 센서 깨우기: 1번 센서의 XSHUT만 HIGH로 올립니다. 이제 1번 센서만 0x29 주소로 응답합니다.
  3. 주소 변경: MCU가 0x29 주소로 명령을 보내 “너의 주소를 0x30으로 바꿔!”라고 지시합니다.
  4. 두 번째 센서 깨우기: 2번 센서의 XSHUT을 HIGH로 올립니다. 2번 센서는 방금 깬 것이므로 기본 주소인 0x29로 응답합니다. (1번은 이미 0x30이 되었으므로 충돌하지 않습니다.)
  5. 반복: 위 과정을 센서 개수만큼 반복하여 모든 센서에 고유 주소(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 스위치”라고 보시면 됩니다.
  • 특징:
    1. 모든 센서의 주소가 0x29로 같아도 상관없습니다.
    2. MCU가 “지금은 1번 통로만 열어줘”라고 명령하면 1번 센서와만 통신합니다.
    3. 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)

코멘트

“레이저 거리 센서 VL53L0X, XSHUT 핀으로 다중연결 (2/2)”에 대한 2개 응답

  1. micro2iot 아바타

    [구매정보] 블로그에서 사용한 부품 구매정보입니다. 부품 상세 사양 및 기술 정보 참조용으로 활용하세요.
    아래 링크를 통해 구매 시 소정의 수수료를 제공받으며, 채널 운영에 큰 도움이 됩니다.

  2. micro2iot 아바타

    위 레이저센서 4개 구입중 1개가 불량이라 1개만 반품 신청했는데 4개 다 반품 없이 전액 환불해 주었습니다.
    ESP32-C3 잘못 사면 불량이 종종 있었는데 TENSTAR 에서 구입한 것은 다 양호했고 TENSTAR 판매자는 강추합니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다