두 선으로 여러 장치를 연결한다는 것
앞 장에서 시리얼 통신을 배웠습니다. 시리얼 통신은 두 장치가 서로 명령과 데이터를 주고받기에 아주 좋습니다. 라즈베리파이가 ESP32에게 FORWARD라고 보내고, ESP32가 모터를 움직이는 구조가 대표적입니다.
그런데 로봇 안에는 보드와 1:1로만 연결되는 장치보다, 작은 센서 여러 개가 한꺼번에 붙는 경우가 더 많습니다. IMU는 기울기를 알려주고, OLED는 상태를 표시하고, ToF 거리 센서는 앞쪽 물체와의 거리를 알려줍니다. 여기에 온도 센서, 전류 센서, 버튼 확장 모듈까지 붙이면 GPIO 핀이 금방 부족해집니다.
이때 등장하는 통신 방식이 I2C입니다. I2C는 보통 "아이 투 씨"라고 읽습니다. 핵심은 간단합니다. 데이터 선 하나와 클록 선 하나, 즉 두 개의 신호선만으로 여러 장치를 같은 통신 버스에 연결할 수 있습니다.
I2C에서 사용하는 두 신호선은 다음과 같습니다.
| 이름 | 의미 | 역할 |
|---|---|---|
| SDA | Serial Data | 실제 데이터가 오가는 선 |
| SCL | Serial Clock | 데이터 타이밍을 맞추는 클록 선 |
여기서 클록은 박자라고 생각하면 됩니다. 여러 사람이 동시에 대화하면 말이 뒤섞입니다. 그래서 "하나, 둘, 셋" 하고 박자를 맞추면 누가 언제 말하는지 정리됩니다. I2C의 SCL이 바로 그 박자 역할을 합니다. SDA는 그 박자에 맞춰 실제 데이터를 전달합니다.
로봇 제작에서 I2C가 특히 사랑받는 이유는 분명합니다. 핀을 적게 쓰면서도 여러 센서를 연결할 수 있기 때문입니다. 아두이노 UNO처럼 핀이 넉넉하지 않은 보드에서도 IMU와 OLED를 동시에 연결할 수 있고, ESP32처럼 기능이 많은 보드에서는 센서 묶음을 깔끔하게 정리할 수 있습니다.

I2C 버스 구조 이해하기
I2C에서는 여러 장치가 같은 SDA, SCL 선에 매달립니다. 이 공통 통신로를 버스라고 부릅니다. 버스라고 하니 어렵게 들릴 수 있지만, 실제로는 같은 전선 두 가닥을 여러 모듈이 함께 나눠 쓰는 구조입니다.
예를 들어 ESP32에 IMU, OLED, ToF 센서를 연결한다고 해 봅시다. 각각의 SDA를 ESP32의 SDA 핀에 연결하고, 각각의 SCL을 ESP32의 SCL 핀에 연결합니다. 전원과 GND도 함께 연결합니다.
| 장치 | VCC | GND | SDA | SCL |
|---|---|---|---|---|
| ESP32 | 3.3V | GND | GPIO 21 | GPIO 22 |
| IMU 모듈 | 3.3V | GND | GPIO 21에 함께 연결 | GPIO 22에 함께 연결 |
| OLED 모듈 | 3.3V | GND | GPIO 21에 함께 연결 | GPIO 22에 함께 연결 |
| ToF 센서 | 3.3V | GND | GPIO 21에 함께 연결 | GPIO 22에 함께 연결 |
아두이노 UNO에서는 보통 SDA가 A4, SCL이 A5입니다. 보드에 따라 SDA, SCL 전용 핀이 따로 표시되어 있기도 합니다. 라즈베리파이에서는 GPIO 2가 SDA, GPIO 3이 SCL로 자주 사용됩니다. 단, 보드마다 핀 위치와 기본 설정이 다르므로 실제 배선 전에는 사용하는 보드의 핀맵을 확인해야 합니다.
I2C 배선을 처음 보면 이런 생각이 들 수 있습니다.
"같은 선에 여러 장치를 다 붙이면 데이터가 섞이지 않을까?"
좋은 의심입니다. 실제로 아무 규칙 없이 같은 선을 공유하면 데이터가 뒤죽박죽이 됩니다. I2C가 동작할 수 있는 이유는 각 장치가 주소를 가지고 있고, 컨트롤러가 특정 주소를 불러서 통신하기 때문입니다.
컨트롤러와 장치, 그리고 주소
I2C 통신에서는 보통 명령을 시작하는 쪽이 있습니다. 아두이노, ESP32, 라즈베리파이 같은 제어 보드가 그 역할을 맡습니다. 센서나 디스플레이 모듈은 제어 보드의 요청에 응답합니다.
쉽게 말하면 제어 보드는 선생님이고, 센서들은 학생입니다. 선생님이 "0x68번, 지금 가속도 값 알려줘"라고 부르면, 0x68번 주소를 가진 IMU만 대답합니다. OLED의 주소가 0x3C라면 이때 OLED는 조용히 기다립니다. 반대로 선생님이 "0x3C번, 화면에 글자 표시해"라고 하면 OLED가 응답합니다.
I2C 주소는 보통 7비트 주소를 사용하며, 실습에서는 0x3C, 0x68처럼 16진수로 자주 표시합니다. 16진수는 컴퓨터에서 숫자를 간단히 표현하는 방식입니다. 앞에 붙은 0x는 "이 숫자는 16진수입니다"라는 표시입니다.
로봇에서 자주 만나는 I2C 장치 주소 예시는 다음과 같습니다.
| 모듈 예시 | 자주 쓰이는 주소 예시 | 용도 |
|---|---|---|
| SSD1306 OLED | 0x3C 또는 0x3D | 작은 상태 화면 표시 |
| MPU-6050 IMU | 0x68 또는 0x69 | 가속도, 자이로 측정 |
| VL53L0X ToF 센서 | 0x29 | 거리 측정 |
| BME280 환경 센서 | 0x76 또는 0x77 | 온도, 습도, 기압 측정 |
| PCF8574 I/O 확장 | 0x20 근처 | 입출력 핀 확장 |
주소는 모듈 제조 방식이나 납땜 점퍼 설정에 따라 달라질 수 있습니다. 같은 칩을 사용해도 판매되는 모듈마다 기본 주소가 조금 다를 수 있으므로, 코드에 적힌 주소를 무조건 믿기보다 실제 연결 후 확인하는 습관이 중요합니다.
주소 충돌이 생기면 어떤 일이 벌어질까
I2C에서 가장 흔한 문제 중 하나가 주소 충돌입니다. 주소 충돌은 같은 I2C 버스에 같은 주소를 가진 장치가 둘 이상 연결된 상황입니다.
예를 들어 OLED 두 개가 모두 0x3C 주소를 가지고 있다고 해 봅시다. 제어 보드가 0x3C에 데이터를 보내면 두 OLED가 동시에 반응합니다. 둘 다 같은 내용을 표시하는 정도라면 그나마 다행입니다. 하지만 센서처럼 데이터를 읽어야 하는 장치가 같은 주소를 공유하면 문제가 더 복잡해집니다. 두 장치가 동시에 응답하려고 하면서 데이터가 깨지거나, 프로그램이 멈춘 것처럼 보이거나, 아예 장치가 없는 것처럼 동작할 수도 있습니다.
주소 충돌을 해결하는 방법은 몇 가지가 있습니다.
모듈의 주소 변경 점퍼나 납땜 패드를 사용해 주소를 바꿉니다.
같은 주소를 가진 모듈을 동시에 쓰지 않도록 부품을 선택합니다.
I2C 멀티플렉서 모듈을 사용해 버스를 여러 갈래로 나눕니다.
ESP32처럼 핀 설정이 유연한 보드에서는 별도의 I2C 버스를 추가로 구성합니다.
초보 단계에서는 우선 주소가 서로 다른 모듈을 조합하는 편이 가장 쉽습니다. 예를 들어 0x68 주소의 IMU와 0x3C 주소의 OLED는 같은 버스에 놓아도 충돌하지 않습니다.
I2C 배선에서 꼭 지켜야 할 것
I2C는 선이 적어서 간단해 보이지만, 실제 로봇에 넣으면 의외로 예민해질 때가 있습니다. 특히 모터가 함께 돌아가는 로봇에서는 전원 흔들림과 전기적 노이즈 때문에 센서값이 튀거나 통신이 끊길 수 있습니다.
가장 먼저 확인할 것은 공통 GND입니다. 보드와 센서가 같은 기준 전압을 공유하지 않으면 SDA와 SCL의 HIGH, LOW 신호를 제대로 해석할 수 없습니다. 앞 장들에서 반복해서 이야기했듯이, 로봇 회로에서 GND는 단순한 마이너스 선이 아니라 모든 신호의 기준점입니다.
두 번째는 전압 레벨입니다. ESP32와 라즈베리파이는 3.3V 로직을 사용합니다. 아두이노 UNO는 보통 5V 로직을 사용합니다. 많은 I2C 센서 모듈은 3.3V 기반 칩을 사용합니다. 모듈에 레벨 변환 회로가 들어 있다면 5V 보드와도 연결할 수 있지만, 그렇지 않은 3.3V 전용 센서를 5V I2C 신호에 직접 연결하면 고장 날 수 있습니다.
세 번째는 풀업 저항입니다. I2C의 SDA와 SCL은 장치들이 선을 직접 HIGH로 강하게 밀어 올리는 방식이 아니라, 필요할 때 LOW로 끌어내리는 방식으로 동작합니다. 그래서 평소에는 선이 HIGH 상태가 되도록 위로 당겨 주는 저항이 필요합니다. 이것을 풀업 저항이라고 합니다. 많은 I2C 모듈에는 이미 풀업 저항이 들어 있어서 별도 저항 없이도 동작합니다. 하지만 모듈을 너무 많이 연결하면 풀업 저항이 병렬로 겹쳐 버스 특성이 나빠질 수 있습니다.
실습 단계에서는 다음 원칙을 기억하면 좋습니다.
SDA와 SCL 선은 가능한 짧게 유지합니다.
모터 전원선과 I2C 신호선을 너무 가깝게 붙이지 않습니다.
보드와 센서의 GND를 반드시 연결합니다.
3.3V 전용 센서에 5V 신호가 들어가지 않게 확인합니다.
문제가 생기면 센서를 하나씩만 연결해 테스트한 뒤 다시 늘립니다.
I2C는 책상 위에서는 잘 되다가 로봇 프레임에 올리면 갑자기 불안정해지는 경우가 있습니다. 이때는 코드보다 배선을 먼저 의심해야 합니다. 길게 늘어진 점퍼선, 헐거운 커넥터, 모터 옆을 지나가는 센서선이 문제의 주인공일 때가 많습니다.
I2C 스캐너로 장치 주소 찾기
I2C 실습에서 가장 먼저 익혀야 할 도구는 I2C 스캐너입니다. I2C 스캐너는 가능한 주소를 하나씩 불러 보면서 응답하는 장치가 있는지 확인하는 간단한 프로그램입니다.
이 프로그램은 세 가지 상황에서 특히 유용합니다.
센서가 실제로 연결되었는지 확인할 때
모듈의 I2C 주소를 모를 때
배선 오류인지 코드 오류인지 구분하고 싶을 때
아래 코드는 아두이노 IDE에서 사용할 수 있는 기본적인 I2C 스캐너입니다. 아두이노 UNO에서는 Wire.begin()만 사용하면 됩니다. ESP32에서는 보통 Wire.begin(21, 22)처럼 SDA, SCL 핀을 지정할 수 있습니다.
#include <Wire.h>
void setup() {
Serial.begin(115200);
while (!Serial) {
;
}
// Arduino UNO: Wire.begin();
// ESP32 예시: Wire.begin(21, 22); // SDA=21, SCL=22
Wire.begin();
Serial.println("I2C scanner start");
}
void loop() {
byte error;
byte address;
int count = 0;
Serial.println("Scanning...");
for (address = 1; address < 127; address++) {
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0) {
Serial.print("I2C device found at 0x");
if (address < 16) {
Serial.print("0");
}
Serial.println(address, HEX);
count++;
}
}
if (count == 0) {
Serial.println("No I2C devices found");
} else {
Serial.print("Found ");
Serial.print(count);
Serial.println(" device(s)");
}
Serial.println();
delay(3000);
}OLED 하나만 연결했는데 시리얼 모니터에 0x3C가 나온다면 배선이 제대로 된 것입니다. IMU를 추가했을 때 0x3C와 0x68이 함께 보이면 두 장치가 같은 버스에서 잘 인식되고 있다는 뜻입니다.
반대로 아무 장치도 나오지 않는다면 다음 순서로 확인합니다.
VCC와 GND가 제대로 연결되어 있는지 확인합니다.
SDA와 SCL을 서로 바꿔 꽂지 않았는지 확인합니다.
보드에 맞는 SDA, SCL 핀을 사용했는지 확인합니다.
센서 전압이 3.3V인지 5V인지 확인합니다.
같은 주소 장치가 충돌하고 있지 않은지 하나씩 분리해 봅니다.
I2C 스캐너는 마치 로봇 회로의 "출석 체크"와 같습니다. 수업을 시작하기 전에 누가 자리에 앉아 있는지 확인하는 것입니다. 출석부에 이름이 없는데 수업을 진행하면 당연히 문제가 생깁니다. 센서 코드도 마찬가지입니다. 먼저 스캐너로 장치가 보이는지 확인하고, 그다음 라이브러리 예제로 넘어가는 편이 훨씬 빠릅니다.
IMU와 OLED를 같은 버스에 연결하기
이제 실제 로봇에서 자주 쓰는 조합을 만들어 보겠습니다. IMU로 기울기와 움직임을 읽고, OLED에 값을 표시하는 예제입니다. 이동 로봇을 만들 때 이 구조를 응용하면 현재 기울기, 회전 속도, 배터리 상태, 모드 등을 작은 화면에 표시할 수 있습니다.
이번 예제에서는 다음 구성을 가정합니다.
| 부품 | 예시 주소 | 역할 |
|---|---|---|
| ESP32 또는 아두이노 | - | I2C 컨트롤러 |
| MPU-6050 IMU | 0x68 | 가속도와 자이로 측정 |
| SSD1306 OLED | 0x3C | 센서값 표시 |
배선은 단순합니다. IMU의 SDA와 OLED의 SDA를 같은 보드 SDA 핀에 연결합니다. IMU의 SCL과 OLED의 SCL도 같은 보드 SCL 핀에 연결합니다. 전원과 GND도 각각 연결합니다.
ESP32를 기준으로 하면 다음과 같이 연결할 수 있습니다.
| ESP32 | IMU | OLED |
|---|---|---|
| 3.3V | VCC | VCC |
| GND | GND | GND |
| GPIO 21 | SDA | SDA |
| GPIO 22 | SCL | SCL |
아두이노 UNO를 사용한다면 SDA는 A4, SCL은 A5에 연결하는 경우가 많습니다. 단, 사용하는 OLED와 IMU 모듈이 5V 입력을 지원하는지 반드시 확인해야 합니다.
센서값을 읽고 OLED에 표시하는 예제
아래 예제는 MPU-6050 IMU에서 가속도 값을 읽고, SSD1306 OLED에 표시하는 코드입니다. 아두이노 IDE에서 관련 라이브러리를 설치해야 합니다. 라이브러리 이름은 사용하는 모듈과 예제에 따라 조금씩 다를 수 있지만, 여기서는 많이 쓰이는 Adafruit_MPU6050, Adafruit_Sensor, Adafruit_SSD1306 계열 라이브러리 사용을 가정합니다.
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_MPU6050 mpu;
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
unsigned long lastDisplayTime = 0;
const unsigned long displayInterval = 100;
void setup() {
Serial.begin(115200);
// Arduino UNO: Wire.begin();
// ESP32 예시: Wire.begin(21, 22);
Wire.begin();
if (!mpu.begin(0x68)) {
Serial.println("MPU6050 not found");
while (1) {
delay(10);
}
}
if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
Serial.println("OLED not found");
while (1) {
delay(10);
}
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println("I2C Robot Monitor");
display.println("MPU6050 + OLED");
display.display();
delay(1000);
}
void loop() {
sensors_event_t accel;
sensors_event_t gyro;
sensors_event_t temp;
mpu.getEvent(&accel, &gyro, &temp);
Serial.print("AX: ");
Serial.print(accel.acceleration.x);
Serial.print(" AY: ");
Serial.print(accel.acceleration.y);
Serial.print(" AZ: ");
Serial.println(accel.acceleration.z);
unsigned long now = millis();
if (now - lastDisplayTime >= displayInterval) {
lastDisplayTime = now;
display.clearDisplay();
display.setCursor(0, 0);
display.println("IMU Accel");
display.println();
display.print("X: ");
display.println(accel.acceleration.x, 2);
display.print("Y: ");
display.println(accel.acceleration.y, 2);
display.print("Z: ");
display.println(accel.acceleration.z, 2);
display.println();
display.print("Temp: ");
display.print(temp.temperature, 1);
display.println(" C");
display.display();
}
delay(10);
}이 코드에서 중요한 부분은 두 장치가 모두 같은 Wire 객체를 사용한다는 점입니다. Wire는 아두이노 환경에서 I2C 통신을 담당하는 기본 객체입니다. IMU 라이브러리도 Wire를 사용하고, OLED 라이브러리도 Wire를 사용합니다. 주소가 다르기 때문에 같은 선을 공유해도 서로 헷갈리지 않습니다.
다만 OLED는 화면 전체를 갱신할 때 생각보다 시간이 걸립니다. 센서값을 아주 빠르게 읽어야 하는 로봇이라면 매 반복마다 화면을 갱신하지 않는 편이 좋습니다. 위 코드에서는 displayInterval을 사용해 100ms마다 한 번씩만 화면을 갱신합니다. 센서 읽기는 자주 하고, 화면 표시는 조금 느리게 하는 구조입니다.
이 방식은 실제 로봇 디버깅에도 유용합니다. 시리얼 모니터를 보려면 USB 케이블과 PC가 필요하지만, OLED를 달아 두면 로봇이 바닥 위를 달리는 중에도 현재 센서 상태를 바로 볼 수 있습니다.
I2C가 느려질 때 생각해야 할 것
I2C는 여러 장치를 한 버스에 연결할 수 있지만, 모든 장치가 동시에 빠르게 통신하는 구조는 아닙니다. 한 순간에는 하나의 장치와 통신합니다. 센서가 많아지고, OLED처럼 전송할 데이터가 많은 장치가 섞이면 전체 갱신 속도가 느려질 수 있습니다.
예를 들어 라인트레이서 로봇이 빠르게 달리는 중이라고 해 봅시다. 라인 센서 값은 아주 자주 읽어야 합니다. 그런데 같은 루프에서 OLED 화면을 계속 지우고 다시 그리면, 모터 보정 타이밍이 흔들릴 수 있습니다. 이런 경우 화면 표시는 0.1초 또는 0.5초마다 한 번으로 줄이고, 제어에 필요한 센서는 더 자주 읽는 식으로 역할을 나눠야 합니다.
I2C 속도는 보통 100kHz 또는 400kHz를 많이 사용합니다. 숫자만 보면 빠르게 느껴지지만, 로봇 제어에서는 통신 속도보다 일정한 주기가 더 중요할 때가 많습니다. 센서값을 읽는 시간이 매번 크게 달라지면 제어 루프가 흔들립니다. 그러면 같은 알고리즘을 사용해도 로봇이 부드럽게 움직이지 않을 수 있습니다.
그래서 실전 로봇에서는 다음처럼 설계하는 것이 좋습니다.
자세 제어나 라인트레이싱에 필요한 센서는 정해진 주기로 읽습니다.
OLED나 로그 출력처럼 느려도 되는 작업은 낮은 주기로 처리합니다.
통신 실패가 발생하면 이전 값을 무작정 믿지 말고 안전 동작으로 전환합니다.
센서 통신은 단순히 값을 읽는 일이 아닙니다. 로봇의 판단 속도를 결정하는 중요한 리듬입니다.
로봇에서 자주 만나는 I2C 문제 해결
I2C 문제는 처음에는 꽤 당황스럽습니다. 코드가 맞는 것 같은데 장치가 보이지 않거나, 가끔은 되다가 가끔은 안 됩니다. 하지만 증상별로 보면 원인을 좁힐 수 있습니다.
| 증상 | 가능한 원인 | 확인 방법 |
|---|---|---|
| 스캐너에서 아무 주소도 안 보임 | 전원, GND, SDA/SCL 배선 오류 | VCC/GND 확인, SDA/SCL 교차 여부 확인 |
| 주소가 하나만 보임 | 한 장치 배선 오류 또는 전원 문제 | 장치를 하나씩 연결해 스캔 |
| 예상과 다른 주소가 보임 | 모듈 기본 주소가 다름 | 코드의 주소값 수정 |
| 같은 주소 장치 두 개가 있음 | 주소 충돌 | 점퍼 설정 변경 또는 버스 분리 |
| 움직일 때만 통신 오류 발생 | 모터 노이즈, 전압 강하, 접촉 불량 | 배선 정리, 전원 분리, 커넥터 고정 |
| OLED는 되는데 IMU가 안 됨 | 라이브러리 주소 또는 초기화 문제 | 스캐너 주소와 코드 주소 비교 |
특히 로봇에서는 "책상 위에서는 됐는데 주행하면 안 된다"는 문제가 자주 나옵니다. 이때는 모터가 만들어 내는 전기적 잡음과 전압 강하를 의심해야 합니다. 센서 전원에 작은 커패시터를 추가하거나, 모터 전원과 보드 전원을 분리하고, GND를 한 점에서 안정적으로 연결하는 방식이 도움이 됩니다.
또 하나의 흔한 원인은 헐거운 점퍼선입니다. 브레드보드에 꽂힌 선은 고정된 것처럼 보여도 로봇이 흔들리면 순간적으로 접촉이 끊길 수 있습니다. 실험 단계에서는 브레드보드가 편하지만, 움직이는 로봇에 올릴 때는 커넥터, 납땜, 케이블타이 등으로 배선을 고정하는 것이 좋습니다.
I2C를 잘 쓰면 로봇 내부가 정리된다
I2C의 장점은 단순히 핀을 아끼는 데서 끝나지 않습니다. 로봇 내부 구조를 깔끔하게 만드는 데 큰 도움이 됩니다. 센서 모듈을 한쪽에 모으고 SDA, SCL, VCC, GND 네 선만 정리하면 배선이 훨씬 단순해집니다. OLED 같은 디버깅 화면도 같은 버스에 쉽게 추가할 수 있습니다.
다만 I2C는 만능이 아닙니다. 긴 거리 통신에는 적합하지 않고, 모터 드라이버처럼 큰 전류를 다루는 장치를 직접 제어하는 용도도 아닙니다. 빠른 데이터 전송이 필요한 디스플레이나 카메라에는 다른 통신 방식이 더 적합할 수 있습니다. 다음 장에서 다룰 SPI는 I2C보다 선은 더 많이 쓰지만 더 빠른 통신에 유리합니다.
이번 장에서 기억해야 할 핵심은 분명합니다. I2C는 SDA와 SCL 두 선을 여러 장치가 공유하는 통신 방식입니다. 각 장치는 주소를 가지고 있으며, 주소가 겹치면 충돌이 생깁니다. 문제가 생겼을 때는 I2C 스캐너로 먼저 장치가 보이는지 확인합니다. 그리고 IMU와 OLED처럼 주소가 다른 모듈은 같은 버스에서 함께 사용할 수 있습니다.
센서를 하나만 연결할 때는 배선이 단순해 보입니다. 하지만 로봇이 조금만 복잡해져도 센서는 늘어나고, 핀은 부족해지고, 디버깅은 어려워집니다. I2C를 익혀 두면 그 복잡함을 훨씬 차분하게 다룰 수 있습니다. 이제 로봇은 여러 감각 기관을 한 줄기 통신 버스에 묶어, 더 많은 정보를 더 적은 선으로 받아들일 준비가 되었습니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.