Skip to main content
Summary

로봇과 대화하는 가장 단순한 방법

로봇을 만들다 보면 어느 순간 이런 질문이 생깁니다.

"지금 로봇은 무슨 생각을 하고 있지?"

물론 작은 아두이노나 ESP32가 사람처럼 생각하는 것은 아닙니다. 하지만 로봇은 계속 판단합니다. 거리 센서값이 몇 센티미터인지, 라인 센서가 검은 선을 보고 있는지, 모터 속도를 얼마로 줄지, 배터리 전압이 낮아졌는지 같은 정보를 계속 읽고 계산합니다.

문제는 그 정보가 보드 안에만 있으면 사람이 알 수 없다는 점입니다. LED 하나를 깜빡여서 상태를 표현할 수도 있지만, 센서값이 23.7cm인지 145.2cm인지 LED만으로는 알기 어렵습니다. 이때 가장 먼저 사용하는 도구가 시리얼 통신입니다.

시리얼 통신은 보드와 PC, 또는 보드와 보드가 글자를 주고받는 기본 통신 방법입니다. 아두이노 예제에서 자주 보던 Serial.println()이 바로 이 통신을 사용합니다. 로봇 제작에서는 시리얼 통신을 다음과 같은 용도로 사용합니다.

  • PC 화면에 센서값과 디버깅 메시지를 출력한다.

  • PC에서 명령을 입력해 LED, 모터, 서보를 제어한다.

  • 라즈베리파이와 아두이노 또는 ESP32 사이에 간단한 명령을 주고받는다.

  • 문제가 생겼을 때 로봇 내부 상태를 사람이 읽을 수 있게 만든다.

앞 장에서 카메라가 달린 로봇을 이야기하며 LEFT, RIGHT, FORWARD, STOP 같은 명령을 언급했습니다. 카메라 처리는 라즈베리파이가 맡고, 모터 제어는 아두이노나 ESP32가 맡는 구조라면 두 보드 사이에 명령을 전달해야 합니다. 이때 가장 먼저 떠올릴 수 있는 연결 방식이 바로 UART 기반 시리얼 통신입니다.

시리얼 통신

UART란 무엇인가

시리얼 통신이라는 말은 넓게 쓰입니다. 그중 로봇 제작에서 가장 기본으로 다루는 방식은 UART입니다. UART는 Universal Asynchronous Receiver/Transmitter의 약자로, 직역하면 "범용 비동기 송수신 장치" 정도입니다. 이름은 길지만 핵심은 간단합니다.

UART는 데이터를 한 줄로 차례차례 보냅니다.

예를 들어 STOP이라는 글자를 보낸다고 생각해 봅시다. 실제로는 S, T, O, P라는 문자가 순서대로 전기 신호로 바뀌어 전송됩니다. 한 번에 여러 비트를 넓은 도로처럼 보내는 것이 아니라, 좁은 길 하나로 줄지어 보내는 방식입니다. 그래서 "시리얼", 즉 직렬 통신이라고 부릅니다.

UART 통신에는 보통 세 가지 연결이 필요합니다.

  • TX: 데이터를 보내는 선입니다. Transmit의 약자입니다.

  • RX: 데이터를 받는 선입니다. Receive의 약자입니다.

  • GND: 두 장치가 같은 전기적 기준을 공유하기 위한 선입니다.

여기서 가장 많이 하는 실수가 TX와 RX를 같은 이름끼리 연결하는 것입니다. 하지만 통신은 대화와 같습니다. 한쪽의 입은 다른 쪽의 귀에 연결되어야 합니다. 그래서 보통 다음처럼 연결합니다.

장치 A TX  →  장치 B RX
장치 A RX  ←  장치 B TX
장치 A GND ↔  장치 B GND

TX끼리 연결하면 두 사람이 동시에 말만 하고 아무도 듣지 않는 상태가 됩니다. RX끼리 연결하면 두 사람이 서로 듣기만 하고 아무도 말하지 않는 상태가 됩니다. 그리고 GND를 연결하지 않으면 두 장치가 같은 기준으로 전압을 해석하지 못해 신호가 불안정해질 수 있습니다.

이 책 앞부분에서 전원과 GND를 여러 번 강조했습니다. 통신에서도 마찬가지입니다. 모터 전원과 보드 전원을 분리하더라도 신호를 주고받는 장치끼리는 보통 GND를 공유해야 합니다. 그래야 "이 정도 전압이면 1이고, 이 정도 전압이면 0이다"라는 기준이 서로 맞습니다.

USB 시리얼과 핀 시리얼

아두이노를 USB 케이블로 PC에 연결하고 시리얼 모니터를 열면 보드가 보내는 문장을 볼 수 있습니다. 그런데 USB는 UART와 다른 통신 방식입니다. 그렇다면 왜 Serial.println()으로 문자가 보일까요?

많은 개발 보드에는 USB와 UART 사이를 바꿔 주는 회로가 들어 있습니다. PC는 USB로 통신하지만, 보드 안쪽에서는 UART처럼 다룰 수 있게 만들어 둔 것입니다. 그래서 사용자는 복잡한 USB 구조를 몰라도 Serial.begin()과 Serial.println()만으로 PC와 데이터를 주고받을 수 있습니다.

시리얼 통신은 크게 두 상황에서 자주 만납니다.

첫 번째는 보드와 PC 사이의 USB 시리얼입니다. 아두이노 IDE의 시리얼 모니터, PlatformIO의 시리얼 터미널, 파이썬의 pyserial 같은 도구가 여기에 해당합니다. 로봇을 테스트할 때 가장 많이 사용합니다.

두 번째는 보드와 보드 사이의 UART 핀 연결입니다. 예를 들어 라즈베리파이의 TX/RX 핀과 ESP32의 RX/TX 핀을 연결해 명령을 주고받을 수 있습니다. 카메라 처리, 웹 서버, AI 판단은 라즈베리파이가 맡고, 모터 PWM과 엔코더 읽기는 ESP32가 맡는 식입니다.

두 경우 모두 "문자를 주고받는다"는 감각은 비슷합니다. 다만 USB 시리얼은 케이블 하나로 전원과 통신이 함께 해결되는 경우가 많고, 핀 UART는 TX, RX, GND를 직접 연결해야 한다는 차이가 있습니다.

보레이트가 맞아야 글자가 제대로 보인다

시리얼 통신을 처음 할 때 자주 보는 이상한 장면이 있습니다. 분명 보드에서 Hello를 출력했는데 시리얼 모니터에는 @~x처럼 엉뚱한 기호가 섞인 문자가 보입니다. 보드가 고장 난 것처럼 보이지만, 대부분은 보레이트가 맞지 않아서 생긴 문제입니다.

보레이트는 1초에 몇 개의 신호 단위를 주고받을지 정하는 속도입니다. 흔히 9600, 57600, 115200 같은 값을 사용합니다. 보드에서는 다음처럼 설정합니다.

Serial.begin(115200);

그런데 PC의 시리얼 모니터가 9600으로 열려 있으면 어떻게 될까요? 보드는 빠른 속도로 말하고 있는데 PC는 느린 속도로 들으려 합니다. 사람이 빠르게 말하는 외국어를 느린 박자로 끊어 들으면 엉뚱하게 들리는 것처럼, 글자가 깨져 보입니다.

그래서 시리얼 통신을 시작할 때는 다음 세 가지를 먼저 확인해야 합니다.

  • 보드 코드의 보레이트와 PC 시리얼 모니터의 보레이트가 같은가?

  • TX는 상대 RX에, RX는 상대 TX에 연결했는가?

  • 두 장치의 GND가 연결되어 있는가?

초보 단계에서는 115200을 많이 사용해도 괜찮습니다. 센서값 출력이나 명령 전달에는 충분히 빠르고, 최신 보드와 PC 환경에서도 안정적으로 쓰기 좋습니다. 다만 예제나 라이브러리에서 9600을 사용한다면 시리얼 모니터도 같은 값으로 맞춰야 합니다.

첫 번째 실습: 보드가 PC에 말 걸기

가장 간단한 실습부터 해 보겠습니다. 보드가 일정 시간마다 메시지를 출력하게 만들고, PC의 시리얼 모니터에서 확인합니다.

아두이노 IDE를 사용한다면 보드를 USB 케이블로 연결하고, 포트를 선택한 뒤 아래 코드를 업로드합니다.

void setup() {
  Serial.begin(115200);
  delay(1000);
  Serial.println("Robot serial test start");
}

void loop() {
  Serial.println("Hello, robot maker!");
  delay(1000);
}

업로드 후 시리얼 모니터를 열고 보레이트를 115200으로 맞춥니다. 그러면 1초마다 다음과 비슷한 문장이 나타납니다.

Robot serial test start
Hello, robot maker!
Hello, robot maker!
Hello, robot maker!

별것 아닌 것처럼 보이지만, 이 순간 보드는 PC 화면을 통해 자기 상태를 말할 수 있게 되었습니다. 이제 센서값도 출력할 수 있고, 모터 출력값도 출력할 수 있고, 오류 메시지도 출력할 수 있습니다. 로봇 디버깅의 문이 열린 것입니다.

이번에는 가상의 거리 센서값을 출력해 보겠습니다. 실제 초음파 센서나 ToF 센서를 연결하지 않아도, 형식을 먼저 익히는 데 도움이 됩니다.

int fakeDistance = 30;

void setup() {
  Serial.begin(115200);
}

void loop() {
  Serial.print("distance_cm=");
  Serial.println(fakeDistance);

  fakeDistance = fakeDistance + 1;
  if (fakeDistance > 80) {
    fakeDistance = 30;
  }

  delay(500);
}

출력 결과는 다음처럼 보입니다.

distance_cm=30
distance_cm=31
distance_cm=32
distance_cm=33

여기서 Serial.print()와 Serial.println()의 차이를 봅시다. Serial.print()는 출력 후 줄을 바꾸지 않습니다. Serial.println()은 출력 후 줄을 바꿉니다. 그래서 이름과 값을 한 줄에 붙여 출력하고 싶을 때 print()와 println()을 함께 자주 사용합니다.

로봇을 만들 때는 숫자만 덩그러니 출력하는 것보다 distance_cm=30처럼 이름과 단위를 함께 출력하는 편이 좋습니다. 나중에 여러 센서값이 섞여 나올 때 사람이 훨씬 읽기 쉽습니다.

두 번째 실습: PC에서 명령 보내기

이번에는 방향을 바꿔 보겠습니다. PC가 보드에 명령을 보내고, 보드는 그 명령에 따라 LED를 켜고 끕니다. 외부에서 로봇을 조종하는 가장 작은 형태입니다.

아두이노 Uno라면 내장 LED가 보통 13번 핀에 연결되어 있습니다. ESP32 보드는 내장 LED 핀이 보드마다 다를 수 있으므로, 확실하게 하려면 LED와 저항을 원하는 GPIO에 직접 연결하는 것이 좋습니다. 여기서는 아두이노 기준으로 13번 핀을 사용합니다.

const int LED_PIN = 13;

void setup() {
  Serial.begin(115200);
  pinMode(LED_PIN, OUTPUT);
  Serial.println("Type ON or OFF");
}

void loop() {
  if (Serial.available() > 0) {
    String command = Serial.readStringUntil('n');
    command.trim();

    if (command == "ON") {
      digitalWrite(LED_PIN, HIGH);
      Serial.println("OK LED ON");
    } else if (command == "OFF") {
      digitalWrite(LED_PIN, LOW);
      Serial.println("OK LED OFF");
    } else {
      Serial.print("ERR unknown command: ");
      Serial.println(command);
    }
  }
}

시리얼 모니터 아래쪽 입력창에 ON을 입력해 전송하면 LED가 켜지고, OFF를 입력하면 LED가 꺼집니다. 시리얼 모니터의 줄 끝 설정은 "새 줄" 또는 "Newline"으로 맞추는 것이 좋습니다. 코드에서 Serial.readStringUntil('n')을 사용했기 때문에 줄바꿈 문자가 들어오면 명령 하나가 끝났다고 판단합니다.

이 예제에는 중요한 습관이 들어 있습니다. 보드는 명령을 실행한 뒤 OK LED ON처럼 응답을 보냅니다. 알 수 없는 명령이 들어오면 ERR unknown command라고 알려 줍니다. 실제 로봇에서도 명령을 보냈는데 실행됐는지 알 수 없으면 디버깅이 어려워집니다. 그래서 명령을 받았으면 가능하면 짧게라도 응답을 보내는 편이 좋습니다.

명령은 줄 단위로 끊어 생각하자

시리얼 통신은 글자가 한 글자씩 들어옵니다. PC에서 ON을 보냈다고 해도 보드 입장에서는 O가 먼저 오고, 그다음 N이 오고, 마지막에 줄바꿈 문자가 올 수 있습니다. 그래서 로봇 명령은 보통 "한 줄이 명령 하나"라는 규칙으로 설계하면 편합니다.

예를 들면 다음과 같습니다.

FORWARD
LEFT
RIGHT
STOP
SPEED 120
LED ON

이런 규칙을 정하면 사람이 시리얼 모니터에서 직접 입력하기도 쉽고, 나중에 라즈베리파이나 PC 프로그램이 자동으로 보내기도 쉽습니다. 줄바꿈 문자는 명령의 마침표 같은 역할을 합니다.

다만 readStringUntil()은 처음 배우기에는 편하지만, 정해진 시간 안에 줄바꿈이 들어오지 않으면 기다리는 특성이 있습니다. 간단한 실습에서는 문제가 없지만, 빠르게 반응해야 하는 로봇에서는 직접 버퍼를 만들어 한 글자씩 처리하는 방식이 더 안정적일 때가 있습니다. 지금은 원리를 익히는 단계이므로 줄 단위 명령 구조에 익숙해지는 것이 먼저입니다.

세 번째 실습: 시리얼 명령으로 모터 제어하기

LED를 켜고 끄는 구조를 모터에 적용하면 원격 조종의 기본이 됩니다. 여기서는 DC 모터 드라이버를 사용한다고 가정하겠습니다. 앞에서 배운 것처럼 보드 핀으로 모터를 직접 구동하면 안 됩니다. 모터는 전류를 많이 사용하므로 반드시 모터 드라이버를 거쳐야 합니다.

아래 예제는 방향 핀 두 개와 PWM 핀 하나로 모터 하나를 제어하는 단순 구조입니다. 실제 사용하는 드라이버가 TB6612, L298N, DRV8833 등 무엇인지에 따라 핀 이름과 동작 방식은 조금 달라질 수 있습니다. 하지만 "명령을 읽고, 방향을 정하고, PWM 값을 출력한다"는 흐름은 같습니다.

const int IN1 = 7;
const int IN2 = 8;
const int PWM_PIN = 9;

int motorSpeed = 120;

void motorStop() {
  digitalWrite(IN1, LOW);
  digitalWrite(IN2, LOW);
  analogWrite(PWM_PIN, 0);
}

void motorForward(int speedValue) {
  digitalWrite(IN1, HIGH);
  digitalWrite(IN2, LOW);
  analogWrite(PWM_PIN, speedValue);
}

void motorBackward(int speedValue) {
  digitalWrite(IN1, LOW);
  digitalWrite(IN2, HIGH);
  analogWrite(PWM_PIN, speedValue);
}

void setup() {
  Serial.begin(115200);

  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);
  pinMode(PWM_PIN, OUTPUT);

  motorStop();
  Serial.println("Motor command ready");
  Serial.println("Commands: F, B, S, SPEED 0~255");
}

void loop() {
  if (Serial.available() > 0) {
    String command = Serial.readStringUntil('n');
    command.trim();

    if (command == "F") {
      motorForward(motorSpeed);
      Serial.print("OK forward speed=");
      Serial.println(motorSpeed);
    } else if (command == "B") {
      motorBackward(motorSpeed);
      Serial.print("OK backward speed=");
      Serial.println(motorSpeed);
    } else if (command == "S") {
      motorStop();
      Serial.println("OK stop");
    } else if (command.startsWith("SPEED ")) {
      int value = command.substring(6).toInt();
      value = constrain(value, 0, 255);
      motorSpeed = value;
      Serial.print("OK speed=");
      Serial.println(motorSpeed);
    } else {
      Serial.print("ERR unknown command: ");
      Serial.println(command);
    }
  }
}

시리얼 모니터에서 SPEED 180을 보낸 뒤 F를 보내면 모터가 정방향으로 돕니다. B를 보내면 반대 방향으로 돌고, S를 보내면 멈춥니다.

실제 로봇 자동차라면 왼쪽 모터와 오른쪽 모터를 따로 제어해야 합니다. 그때는 명령을 조금 더 확장할 수 있습니다.

MOTOR 120 120
MOTOR 80 160
MOTOR -100 100
STOP

첫 번째 숫자는 왼쪽 모터, 두 번째 숫자는 오른쪽 모터의 속도라고 정하면 됩니다. 양수는 전진, 음수는 후진, 0은 정지로 해석할 수 있습니다. 이렇게 명령 형식을 정해 두면 PC, 라즈베리파이, 스마트폰 앱, 테스트 프로그램이 모두 같은 방식으로 로봇을 움직일 수 있습니다.

디버깅 메시지는 로봇의 블랙박스다

시리얼 통신의 가장 강력한 용도는 디버깅입니다. 디버깅은 "왜 내가 생각한 대로 움직이지 않는지 찾는 과정"입니다. 로봇은 소프트웨어만 있는 프로그램보다 디버깅이 더 어렵습니다. 코드가 맞아도 배터리가 약하면 이상하게 움직이고, 센서 위치가 조금만 틀어져도 판단이 달라지며, 모터 한쪽이 더 강하면 직진하지 않습니다.

이때 시리얼 출력은 로봇의 블랙박스가 됩니다. 로봇이 움직이는 동안 내부에서 어떤 값이 오갔는지 남겨 주기 때문입니다.

예를 들어 장애물 회피 로봇이라면 다음과 같은 메시지를 출력할 수 있습니다.

state=FORWARD distance=42 pwmL=150 pwmR=150
state=FORWARD distance=35 pwmL=150 pwmR=150
state=AVOID distance=18 pwmL=-120 pwmR=120
state=TURN distance=22 pwmL=100 pwmR=-100

이런 메시지를 보면 로봇이 왜 갑자기 회전했는지 추적할 수 있습니다. 거리값이 18cm로 떨어졌기 때문에 AVOID 상태에 들어갔다는 것을 알 수 있습니다. 만약 실제로 앞에 장애물이 없는데 거리값이 18cm로 나온다면 센서 문제를 의심할 수 있습니다. 반대로 거리값은 정상인데 모터가 움직이지 않았다면 모터 드라이버, 배선, 전원 문제를 확인해야 합니다.

좋은 디버깅 메시지는 사람이 읽기 쉬워야 합니다. 숫자만 나열하면 처음에는 괜찮아 보여도, 값이 많아지면 금방 헷갈립니다.

42 150 150 0

이렇게 출력하면 42가 거리인지 속도인지 시간이 지나면 잊어버리기 쉽습니다. 대신 다음처럼 이름을 붙이면 훨씬 오래 버틸 수 있습니다.

distance=42 pwmL=150 pwmR=150 error=0

처음에는 조금 귀찮아도, 나중에 로봇이 책상 밑으로 돌진하거나 벽 앞에서 떨고 있을 때 이 습관이 큰 차이를 만듭니다.

너무 많이 출력하면 로봇이 느려진다

디버깅 메시지는 유용하지만, 무작정 많이 출력하면 문제가 생깁니다. 시리얼 통신도 속도가 정해져 있기 때문입니다. 특히 짧은 주기로 센서값을 읽고 모터를 제어해야 하는 로봇에서 매번 긴 문자열을 출력하면 제어 루프가 느려질 수 있습니다.

예를 들어 1초에 100번 모터 제어를 해야 하는데, 매번 긴 메시지를 여러 줄 출력하면 실제 제어 주기가 흔들릴 수 있습니다. 로봇이 부드럽게 달리지 않고 덜컥거리거나, 센서 반응이 늦어질 수 있습니다.

따라서 디버깅 출력에는 요령이 필요합니다.

  • 매 반복마다 출력하지 말고 100ms 또는 500ms마다 한 번씩 출력한다.

  • 중요한 상태 변화가 있을 때만 출력한다.

  • 테스트가 끝난 뒤에는 디버깅 출력을 줄이거나 끌 수 있게 만든다.

다음 예제는 500ms마다 한 번씩만 상태를 출력하는 구조입니다.

unsigned long lastDebugTime = 0;
int distanceCm = 0;
int pwmLeft = 120;
int pwmRight = 120;

void setup() {
  Serial.begin(115200);
}

void loop() {
  // 여기에서 센서 읽기와 모터 제어를 한다고 가정한다.
  distanceCm = distanceCm + 1;
  if (distanceCm > 100) {
    distanceCm = 0;
  }

  unsigned long now = millis();
  if (now - lastDebugTime >= 500) {
    lastDebugTime = now;

    Serial.print("distance=");
    Serial.print(distanceCm);
    Serial.print(" pwmL=");
    Serial.print(pwmLeft);
    Serial.print(" pwmR=");
    Serial.println(pwmRight);
  }
}

delay()로 기다리지 않고 millis()를 사용하는 점도 중요합니다. delay(500)을 넣으면 그동안 다른 작업이 멈춥니다. 반면 millis()로 시간을 확인하면 로봇은 계속 움직이면서 필요한 순간에만 메시지를 출력할 수 있습니다.

문자열 출력 형식 정하기

시리얼 메시지는 사람이 읽기 쉬운 형식과 기계가 해석하기 쉬운 형식 사이에서 균형을 잡아야 합니다. 처음 배우는 단계에서는 사람이 읽기 쉬운 문장이 좋습니다. 하지만 라즈베리파이의 파이썬 프로그램이 아두이노의 센서값을 읽어 자동으로 처리한다면 규칙적인 형식이 필요합니다.

간단하고 실용적인 형식은 이름=값을 공백으로 구분하는 방식입니다.

distance=35 yaw=-2.4 battery=7.6 state=FORWARD

또 다른 방식은 쉼표로 값을 구분하는 CSV 형식입니다.

35,-2.4,7.6,FORWARD

CSV는 짧고 프로그램에서 나누기 쉽지만, 사람이 보기에는 각 숫자의 의미를 따로 기억해야 합니다. 반대로 이름=값 방식은 길지만 읽기 쉽습니다. 실습과 디버깅 단계에서는 이름=값 방식이 편하고, 데이터 로깅이나 고속 처리 단계에서는 CSV처럼 짧은 형식이 유리할 수 있습니다.

오류 메시지도 형식을 정해 두면 좋습니다.

OK command=STOP
ERR code=UNKNOWN_COMMAND text=JUMP
WARN battery_low voltage=6.4

OK, ERR, WARN처럼 메시지 앞에 종류를 붙이면 시리얼 모니터에서 빠르게 상태를 구분할 수 있습니다. 나중에 PC 프로그램이 로그를 분석할 때도 도움이 됩니다.

라즈베리파이와 마이크로컨트롤러를 연결할 때

라즈베리파이는 리눅스가 돌아가는 작은 컴퓨터입니다. 카메라 처리, 웹 서버, 파일 저장, AI 모델 실행처럼 비교적 무거운 일을 잘합니다. 반면 아두이노나 ESP32 같은 마이크로컨트롤러는 모터 PWM 출력, 엔코더 펄스 읽기, 짧은 주기의 센서 처리처럼 타이밍이 중요한 일을 잘합니다.

그래서 실전 로봇에서는 역할을 나누는 구조가 자주 등장합니다.

라즈베리파이: 카메라 처리, 경로 판단, 네트워크 통신
아두이노/ESP32: 모터 제어, 센서 읽기, 안전 정지

두 장치 사이에 UART를 연결하면 라즈베리파이가 "앞으로 가", "멈춰", "왼쪽으로 돌아" 같은 명령을 보내고, 마이크로컨트롤러는 "현재 속도", "엔코더 값", "오류 상태"를 되돌려 줄 수 있습니다.

연결은 기본적으로 다음과 같습니다.

라즈베리파이 TX  →  마이크로컨트롤러 RX
라즈베리파이 RX  ←  마이크로컨트롤러 TX
라즈베리파이 GND ↔  마이크로컨트롤러 GND

하지만 여기서 매우 중요한 점이 있습니다. 라즈베리파이의 GPIO UART 핀은 3.3V 신호를 사용하며, 5V 입력을 견디는 핀이 아닙니다. 5V 아두이노 Uno의 TX를 라즈베리파이 RX에 직접 연결하면 라즈베리파이 핀이 손상될 수 있습니다. 이 경우 전압 레벨 변환기를 사용하거나, 최소한 적절한 저항 분압 회로로 5V 신호를 3.3V 수준으로 낮춰야 합니다.

ESP32는 보통 3.3V 로직을 사용하므로 라즈베리파이와 전압 수준이 맞는 편입니다. 그래도 보드마다 핀 특성과 전원 구조가 다르므로 연결 전에 반드시 사용하는 보드의 핀 전압을 확인해야 합니다.

또 하나의 주의점은 라즈베리파이가 실시간 제어에 항상 안정적인 것은 아니라는 점입니다. 리눅스는 여러 작업을 동시에 처리하기 때문에 아주 정확한 주기로 모터 PWM을 직접 만들거나 엔코더 펄스를 빠짐없이 세는 일에는 불리할 수 있습니다. 그래서 라즈베리파이는 큰 판단을 하고, 마이크로컨트롤러가 빠른 제어를 맡는 구조가 안정적입니다.

간단한 명령 프로토콜 설계

프로토콜이라는 말이 거창하게 들릴 수 있습니다. 하지만 여기서는 "어떤 문장을 보내면 어떤 의미로 해석할지 정한 약속"이라고 생각하면 됩니다. 친구와 무전기로 대화할 때 "오버"라고 말하면 한 문장이 끝났다는 뜻이 되는 것처럼, 로봇과 보드 사이에도 약속이 필요합니다.

처음에는 아주 단순하게 시작하는 것이 좋습니다.

PING
STOP
MOVE 100 100
MOVE -80 80
LED ON
STATUS?

각 명령의 의미를 정해 봅시다. PING은 통신이 살아 있는지 확인하는 명령입니다. 보드는 OK PONG이라고 응답할 수 있습니다. STOP은 모든 모터를 즉시 정지하라는 명령입니다. MOVE 100 100은 왼쪽 모터와 오른쪽 모터를 각각 100의 속도로 움직이라는 뜻입니다. STATUS?는 현재 상태를 알려 달라는 요청입니다.

응답도 단순하게 정합니다.

OK PONG
OK STOP
OK MOVE left=100 right=100
STATUS distance=35 encL=1200 encR=1188 battery=7.4
ERR BAD_VALUE
ERR UNKNOWN_COMMAND

이런 구조에서 중요한 원칙은 몇 가지입니다.

  • 명령 하나는 한 줄로 끝낸다.

  • 성공하면 OK, 실패하면 ERR, 경고는 WARN처럼 앞에 표시한다.

  • 숫자의 범위를 정하고, 범위를 벗어나면 안전한 값으로 제한한다.

  • 알 수 없는 명령을 받으면 무시하거나 정지하고 오류를 출력한다.

  • 일정 시간 동안 새 명령이 오지 않으면 로봇을 멈추는 안전 조건을 둔다.

마지막 원칙은 특히 중요합니다. 무선 조종이나 라즈베리파이-마이크로컨트롤러 구조에서는 통신이 끊길 수 있습니다. 명령이 끊겼는데 마지막 명령이 FORWARD였다고 해서 로봇이 계속 전진하면 위험합니다. 그래서 "마지막 명령을 받은 지 500ms 또는 1초가 지나면 자동 정지" 같은 규칙을 넣는 것이 좋습니다.

다음 코드는 명령 타임아웃의 기본 형태입니다.

unsigned long lastCommandTime = 0;
const unsigned long COMMAND_TIMEOUT = 1000;

void stopRobot() {
  // 실제 프로젝트에서는 여기에서 좌우 모터를 정지시킨다.
}

void setup() {
  Serial.begin(115200);
  lastCommandTime = millis();
}

void loop() {
  if (Serial.available() > 0) {
    String command = Serial.readStringUntil('n');
    command.trim();

    lastCommandTime = millis();

    if (command == "STOP") {
      stopRobot();
      Serial.println("OK STOP");
    } else if (command == "PING") {
      Serial.println("OK PONG");
    } else {
      Serial.print("ERR UNKNOWN_COMMAND text=");
      Serial.println(command);
    }
  }

  if (millis() - lastCommandTime > COMMAND_TIMEOUT) {
    stopRobot();
  }
}

이 예제는 단순하지만 실제 로봇에서 매우 중요한 안전 습관을 담고 있습니다. 통신이 끊기면 정지한다. 명령이 이상하면 정지한다. 값을 믿기 전에 범위를 확인한다. 로봇은 움직이는 물체이기 때문에 통신 설계에도 안전이 들어가야 합니다.

PC 프로그램에서 시리얼을 사용할 때의 감각

시리얼 모니터는 사람이 직접 명령을 입력하기에 좋습니다. 하지만 조금 더 나아가면 PC나 라즈베리파이에서 프로그램으로 명령을 보내고 싶어집니다. 예를 들어 키보드 방향키로 로봇을 조종하거나, 카메라에서 빨간 공을 찾으면 MOVE 80 120을 보내는 식입니다.

파이썬을 사용하면 이런 흐름을 쉽게 만들 수 있습니다. 여기서는 코드 전체를 깊게 다루지는 않지만, 개념은 간단합니다. 시리얼 포트를 열고, 문자열 끝에 줄바꿈을 붙여 보내고, 보드가 보내는 응답을 읽습니다.

import serial
import time

ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=1)
time.sleep(2)

ser.write(b'PINGn')
line = ser.readline().decode(errors='ignore').strip()
print(line)

ser.write(b'MOVE 100 100n')
time.sleep(1)

ser.write(b'STOPn')
ser.close()

윈도우에서는 포트 이름이 COM3, COM4처럼 보일 수 있고, 라즈베리파이나 리눅스에서는 /dev/ttyUSB0, /dev/ttyACM0, /dev/serial0 같은 이름을 사용할 수 있습니다. 포트 이름은 환경에 따라 달라지므로, 연결된 장치를 확인한 뒤 맞게 지정해야 합니다.

중요한 것은 파이썬 코드도 사람이 시리얼 모니터에서 입력하던 것과 같은 문장을 보낸다는 점입니다. PINGn, MOVE 100 100n, STOPn처럼 줄바꿈으로 명령을 끝냅니다. 그래서 처음에는 시리얼 모니터로 직접 테스트하고, 잘 동작하면 나중에 프로그램으로 자동화하는 방식이 좋습니다.

흔한 문제와 빠른 점검

시리얼 통신이 안 될 때는 당황하지 말고 기본부터 확인하면 됩니다. 대부분의 문제는 복잡한 코드보다 단순한 설정에서 나옵니다.

글자가 전혀 보이지 않는다면 먼저 포트 선택을 확인합니다. 아두이노 IDE에서 다른 포트를 열고 있을 수 있습니다. USB 케이블이 충전 전용 케이블인 경우 데이터 통신이 되지 않을 수도 있습니다. 보드가 리셋되는지, 전원 LED가 들어오는지도 확인합니다.

글자가 깨져 보인다면 보레이트가 맞지 않을 가능성이 큽니다. 코드에서 Serial.begin(115200)을 사용했다면 시리얼 모니터도 115200으로 맞춰야 합니다.

보드와 보드를 핀으로 연결했는데 통신이 안 된다면 TX와 RX를 바꿔 연결했는지 확인합니다. 한쪽 TX는 다른 쪽 RX로 가야 합니다. 그리고 GND가 연결되어 있는지도 꼭 확인합니다.

라즈베리파이와 5V 아두이노를 연결했다면 전압 레벨을 점검해야 합니다. 라즈베리파이 RX에 5V 신호가 직접 들어가면 위험합니다. 통신이 되는지보다 보드가 안전한지가 먼저입니다.

명령을 보냈는데 보드가 반응하지 않는다면 줄 끝 설정을 확인합니다. 코드가 readStringUntil('n')으로 줄바꿈을 기다리는데, 시리얼 모니터가 줄바꿈 없이 전송하도록 되어 있으면 보드가 명령이 끝났다고 판단하지 못할 수 있습니다.

시리얼 통신은 다음 단계의 출발점이다

시리얼 통신은 화려한 기술처럼 보이지 않을 수 있습니다. Wi-Fi처럼 멀리 떨어져 조종하는 것도 아니고, 카메라처럼 화면이 보이는 것도 아닙니다. 하지만 실제 로봇 제작에서는 가장 자주, 가장 오래 쓰는 도구 중 하나입니다.

센서값이 이상할 때 시리얼로 확인합니다. 모터 명령이 제대로 들어갔는지 시리얼로 확인합니다. 라즈베리파이와 ESP32가 역할을 나눌 때도 시리얼로 첫 연결을 만듭니다. 로봇이 예상과 다르게 움직일 때, 시리얼 메시지는 작은 손전등처럼 내부를 비춰 줍니다.

이번 장에서 익힌 핵심은 단순합니다. TX는 상대 RX로, RX는 상대 TX로 연결합니다. GND는 함께 묶습니다. 보레이트는 양쪽이 같아야 합니다. 명령은 줄 단위로 보내고, 응답은 사람이 읽을 수 있게 출력합니다. 그리고 통신이 끊기면 로봇이 안전하게 멈추도록 설계합니다.

다음 장에서는 여러 센서를 적은 선으로 연결할 수 있는 I2C 통신을 다룹니다. UART가 두 장치가 서로 대화하는 기본 통로라면, I2C는 여러 센서가 같은 버스에 올라타는 방식입니다. IMU, OLED 디스플레이, 거리 센서 모듈을 연결할 때 자주 만나게 될 통신 방식입니다.

책 목록

센서와 모터로 시작하는 실전 로봇 만들기

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기