Skip to main content
Summary

멈추는 로봇에서 피하는 로봇으로

앞 장에서는 거리 센서로 앞에 있는 물체를 감지하고, 가까워지면 멈추는 로봇을 만들었다. 멈추는 로봇은 안전하지만, 아직 '스스로 움직인다'고 말하기에는 조금 아쉽다. 로봇이 정말 살아 있는 것처럼 보이려면 장애물을 만났을 때 멈추기만 하는 것이 아니라, 뒤로 물러나고 방향을 바꾼 뒤 다시 앞으로 나아가야 한다.

이번 장에서는 DC 기어드 모터, 모터 드라이버, 거리 센서를 하나로 묶어 장애물 회피 자동차를 만든다. 구조는 단순하다. 앞을 보고 달리다가 가까운 장애물이 감지되면 정지한다. 잠깐 후진한 뒤 좌회전이나 우회전으로 방향을 틀고 다시 전진한다. 이 간단한 규칙만으로도 로봇은 책상 위, 바닥, 복도에서 꽤 그럴듯하게 움직인다.

이 프로젝트의 핵심은 아주 특별한 부품이 아니다. 지금까지 배운 것을 한 몸에 넣는 것이다. 모터 제어 함수, 거리 측정 함수, 조건문, 전원 분리, 공통 GND, 배선 정리까지 모두 한 번에 등장한다. 그래서 이번 장은 작은 종합 실습에 가깝다.

장애물 회피 자동차

사용할 부품과 전체 구조

장애물 회피 자동차는 보통 2개의 구동 바퀴와 1개의 보조 바퀴로 만든다. 왼쪽과 오른쪽에 DC 기어드 모터를 하나씩 달고, 앞쪽이나 뒤쪽에 캐스터를 붙인다. 캐스터는 자유롭게 방향이 바뀌는 작은 보조 바퀴다. 마트 카트 앞바퀴처럼 로봇이 회전할 때 자연스럽게 따라 움직인다.

기본 구성은 다음과 같다.

  • 제어 보드: 아두이노 Uno, Nano, ESP32 등

  • 구동부: DC 기어드 모터 2개, 바퀴 2개, 캐스터 1개

  • 모터 드라이버: TB6612FNG, L298N, DRV8833 계열 등

  • 거리 센서: HC-SR04 초음파 센서 또는 적외선/ToF 거리 센서

  • 전원: 모터용 배터리, 보드용 전원 또는 전압 변환기

  • 프레임: 2WD 로봇 자동차 섀시, 아크릴판, 3D 프린트 프레임 등

처음 만든다면 2WD 자동차 키트를 사용하는 것이 가장 쉽다. 모터 고정 위치와 배터리 홀더 위치가 이미 준비되어 있기 때문이다. 직접 프레임을 만들 때는 두 모터의 축이 가능한 한 같은 선상에 오도록 맞추는 것이 중요하다. 왼쪽 바퀴가 조금 앞에 있고 오른쪽 바퀴가 조금 뒤에 있으면 로봇이 직진할 때도 미묘하게 흔들린다.

거리 센서는 전방을 바라보게 설치한다. 초음파 센서를 쓴다면 두 개의 둥근 송수신부가 로봇의 눈처럼 앞을 향하게 붙이면 된다. 센서가 너무 낮으면 바닥을 장애물로 오인할 수 있고, 너무 높으면 낮은 물체를 놓칠 수 있다. 작은 자동차에서는 바닥에서 5~10cm 정도 높이에 설치하면 시작하기 좋다.

프레임 위에 부품 배치하기

로봇 조립은 회로보다 먼저 '무게 중심'을 생각해야 한다. 코드가 아무리 좋아도 배터리가 한쪽으로 쏠려 있으면 로봇은 한쪽으로 기울어진 채 달린다. 장애물을 피하려고 회전할 때도 한쪽 바퀴가 헛돌 수 있다.

가장 무거운 부품은 대개 배터리다. 배터리는 프레임 중앙에 가깝게 놓는 것이 좋다. 가능하면 좌우 중심선 위에 배치한다. 모터는 좌우에 대칭으로 고정하고, 제어 보드와 모터 드라이버는 배선을 짧게 만들 수 있는 위치에 둔다. 거리 센서는 앞쪽 중앙에 배치한다.

실전에서 추천하는 배치는 이렇다. 왼쪽과 오른쪽 모터는 프레임 양옆에 단단히 고정한다. 캐스터는 앞쪽이나 뒤쪽 중앙에 붙인다. 만약 센서가 앞쪽에 있고 로봇이 장애물을 향해 다가가는 형태라면 캐스터를 뒤쪽에 두는 편이 조립이 편할 때가 많다. 배터리는 바닥판 중앙에 양면테이프나 벨크로 테이프로 고정한다. 보드는 그 위쪽 또는 뒤쪽에 올려 USB 케이블을 연결하기 쉽게 둔다.

여기서 중요한 것은 '흔들리지 않게 고정하기'다. 로봇이 움직이면 모터 진동이 프레임 전체로 전달된다. 거리 센서가 덜렁거리면 같은 벽을 보아도 측정값이 계속 출렁인다. 배터리 선이 바퀴에 닿으면 로봇이 갑자기 멈추거나 선이 끊어질 수 있다. 움직이는 로봇에서는 느슨한 부품 하나가 버그처럼 행동한다.

조립할 때는 다음 순서로 진행하면 실수가 줄어든다.

  • 모터와 바퀴를 먼저 고정하고 손으로 굴려 걸리는 부분이 없는지 확인한다.

  • 캐스터를 붙인 뒤 로봇을 바닥에 놓고 좌우로 기울어지지 않는지 확인한다.

  • 배터리, 모터 드라이버, 제어 보드, 거리 센서를 올리고 배선이 바퀴에 닿지 않게 정리한다.

  • 전원을 넣기 전에 모든 GND가 연결되어 있는지, 모터 전원과 보드 전원을 잘못 꽂지 않았는지 확인한다.

전원과 GND를 먼저 정리하자

장애물 회피 자동차에서 가장 흔한 문제는 코드가 아니라 전원이다. USB로 보드를 연결했을 때는 센서값이 잘 나오는데, 모터를 돌리는 순간 보드가 재부팅된다면 대부분 전원 공급이 불안정한 것이다. DC 모터는 출발할 때 순간적으로 큰 전류를 요구한다. 이때 전압이 툭 떨어지면 제어 보드는 잠깐 정신을 잃고 다시 시작한다.

기본 원칙은 보드 전원과 모터 전원을 분리하되 GND는 공유하는 것이다. 모터에는 배터리에서 충분한 전류를 공급하고, 보드에는 USB나 안정된 5V 전압 변환기를 사용한다. 단, 제어 보드가 모터 드라이버에 방향 신호를 보내려면 두 회로의 기준점이 같아야 한다. 그래서 보드 GND와 모터 드라이버 GND, 배터리 음극은 반드시 서로 연결해야 한다.

초보자가 자주 하는 실수는 모터 드라이버의 전원 단자에 보드의 5V 핀을 연결해 모터까지 돌리려는 것이다. 작은 모터 하나를 잠깐 돌릴 수는 있어도, 두 개의 DC 모터가 동시에 출발하면 보드가 감당하기 어렵다. 모터는 모터용 배터리에서, 보드는 보드에 맞는 전원에서 공급받는다고 기억하자.

ESP32를 사용할 때는 신호 전압도 주의해야 한다. ESP32의 GPIO는 3.3V 기준이다. HC-SR04 초음파 센서의 Echo 핀이 5V를 출력하는 모듈이라면, 저항 분압이나 레벨 시프터로 3.3V 이하로 낮춰 입력해야 한다. 아두이노 Uno는 5V 입력을 기본으로 다루므로 HC-SR04와 바로 연결하기 쉽지만, ESP32는 입력 핀 보호를 먼저 생각해야 한다.

모터 드라이버 배선하기

예제에서는 TB6612FNG 계열 모터 드라이버를 기준으로 설명한다. TB6612는 소형 2WD 로봇에 많이 쓰이며, L298N보다 전압 손실과 발열이 적은 편이다. 이미 L298N 모듈을 가지고 있다면 개념은 같다. 입력 핀 2개로 방향을 정하고, PWM 핀으로 속도를 조절한다.

TB6612FNG 모듈의 대표적인 핀은 다음과 같다.

  • AIN1, AIN2: 왼쪽 모터 방향 제어 입력

  • PWMA: 왼쪽 모터 속도 제어 PWM 입력

  • BIN1, BIN2: 오른쪽 모터 방향 제어 입력

  • PWMB: 오른쪽 모터 속도 제어 PWM 입력

  • STBY: 드라이버 대기 해제 핀

  • VM: 모터 전원 입력

  • VCC: 로직 전원 입력

  • GND: 공통 접지

  • A01, A02, B01, B02: 모터 출력 단자

VM에는 모터용 배터리 전압을 연결한다. 예를 들어 3~6V DC 기어드 모터라면 AA 배터리 4개, 2셀 리튬 배터리와 적절한 전압 변환기, 또는 모터 정격에 맞는 배터리를 사용할 수 있다. VCC에는 제어 보드의 로직 전압을 연결한다. 아두이노 Uno라면 5V, ESP32라면 모듈이 허용하는 조건에 맞춰 3.3V 또는 5V를 확인해야 한다.

아두이노 Uno 기준 예제 배선은 다음처럼 잡을 수 있다.

기능아두이노 핀TB6612FNG 핀
왼쪽 방향 1D4AIN1
왼쪽 방향 2D5AIN2
왼쪽 속도D6PWMA
오른쪽 방향 1D7BIN1
오른쪽 방향 2D8BIN2
오른쪽 속도D9PWMB
대기 해제D10STBY
로직 전원5VVCC
공통 접지GNDGND

모터를 연결한 뒤 처음 전진 명령을 보냈을 때 한쪽 바퀴만 반대로 돌 수 있다. 이것은 고장이 아니다. DC 모터는 두 선을 바꾸면 회전 방향이 반대로 바뀐다. 코드에서 방향을 뒤집어도 되고, 모터 출력 단자의 두 선을 서로 바꿔도 된다. 중요한 것은 '전진 함수'를 호출했을 때 두 바퀴가 모두 로봇을 앞으로 밀어야 한다는 점이다.

거리 센서 배선하기

HC-SR04 초음파 센서는 Trig 핀으로 짧은 신호를 보내고, Echo 핀으로 돌아오는 시간을 측정한다. 소리가 나가서 물체에 반사되어 돌아오는 시간을 이용해 거리를 계산하는 방식이다. 센서가 보내는 초음파는 사람 귀에는 들리지 않지만, 로봇에게는 앞을 더듬는 손전등 같은 역할을 한다.

아두이노 Uno 기준으로는 다음처럼 연결할 수 있다.

HC-SR04 핀연결
VCC5V
GNDGND
TrigD11
EchoD12

센서를 처음 사용할 때는 모터를 돌리지 말고 거리값만 시리얼 모니터에 출력해 보자. 손을 센서 앞에 가까이 가져갔을 때 값이 줄어들고, 멀리 가져갔을 때 값이 커지면 기본 동작은 정상이다. 만약 값이 계속 0이거나 아주 큰 값으로 고정된다면 Trig와 Echo 핀이 바뀌었는지, GND가 연결되어 있는지, 센서가 너무 기울어져 있지 않은지 확인한다.

초음파 센서는 부드러운 천, 비스듬한 벽, 얇은 물체를 잘 감지하지 못할 수 있다. 반사된 소리가 센서로 돌아오지 않기 때문이다. 그래서 장애물 회피 자동차를 테스트할 때는 처음에는 책, 상자, 벽처럼 넓고 단단한 물체를 사용하자. 로봇을 처음부터 의자 다리 사이에 풀어놓으면 센서보다 사람이 먼저 당황하게 된다.

먼저 모터 동작 함수 만들기

장애물 회피 알고리즘을 만들기 전에 모터부터 확실히 검증해야 한다. 로봇 프로젝트를 망치는 빠른 길은 센서와 모터와 알고리즘을 한 번에 올려놓고, 왜 안 되는지 한꺼번에 고민하는 것이다. 좋은 순서는 단순하다. 왼쪽 모터, 오른쪽 모터, 전진, 후진, 회전 순서로 하나씩 확인한다.

아래 코드는 TB6612FNG와 아두이노 Uno를 기준으로 한 기본 모터 제어 코드다. setLeftMotor()와 setRightMotor()는 -255부터 255 사이의 값을 받는다. 양수면 전진 방향, 음수면 후진 방향, 0이면 정지다.

const int AIN1 = 4;
const int AIN2 = 5;
const int PWMA = 6;

const int BIN1 = 7;
const int BIN2 = 8;
const int PWMB = 9;

const int STBY = 10;

void setup() {
  pinMode(AIN1, OUTPUT);
  pinMode(AIN2, OUTPUT);
  pinMode(PWMA, OUTPUT);

  pinMode(BIN1, OUTPUT);
  pinMode(BIN2, OUTPUT);
  pinMode(PWMB, OUTPUT);

  pinMode(STBY, OUTPUT);
  digitalWrite(STBY, HIGH);
}

void setLeftMotor(int speed) {
  speed = constrain(speed, -255, 255);

  if (speed > 0) {
    digitalWrite(AIN1, HIGH);
    digitalWrite(AIN2, LOW);
    analogWrite(PWMA, speed);
  } else if (speed < 0) {
    digitalWrite(AIN1, LOW);
    digitalWrite(AIN2, HIGH);
    analogWrite(PWMA, -speed);
  } else {
    digitalWrite(AIN1, LOW);
    digitalWrite(AIN2, LOW);
    analogWrite(PWMA, 0);
  }
}

void setRightMotor(int speed) {
  speed = constrain(speed, -255, 255);

  if (speed > 0) {
    digitalWrite(BIN1, HIGH);
    digitalWrite(BIN2, LOW);
    analogWrite(PWMB, speed);
  } else if (speed < 0) {
    digitalWrite(BIN1, LOW);
    digitalWrite(BIN2, HIGH);
    analogWrite(PWMB, -speed);
  } else {
    digitalWrite(BIN1, LOW);
    digitalWrite(BIN2, LOW);
    analogWrite(PWMB, 0);
  }
}

void stopMotors() {
  setLeftMotor(0);
  setRightMotor(0);
}

void moveForward(int speed) {
  setLeftMotor(speed);
  setRightMotor(speed);
}

void moveBackward(int speed) {
  setLeftMotor(-speed);
  setRightMotor(-speed);
}

void turnLeft(int speed) {
  setLeftMotor(-speed);
  setRightMotor(speed);
}

void turnRight(int speed) {
  setLeftMotor(speed);
  setRightMotor(-speed);
}

void loop() {
  moveForward(150);
  delay(1000);

  stopMotors();
  delay(500);

  moveBackward(150);
  delay(1000);

  stopMotors();
  delay(500);

  turnLeft(150);
  delay(600);

  stopMotors();
  delay(500);

  turnRight(150);
  delay(600);

  stopMotors();
  delay(2000);
}

이 코드를 올렸을 때 로봇이 전진, 정지, 후진, 좌회전, 우회전을 차례대로 수행하면 모터 제어의 기초는 준비된 것이다. 만약 전진에서 뒤로 간다면 왼쪽과 오른쪽 모터의 방향을 모두 반대로 바꾸면 된다. 한쪽만 반대로 간다면 그쪽 모터 선이나 해당 함수의 HIGH/LOW 조합만 바꾼다.

회전 함수는 좌우 바퀴를 반대 방향으로 돌리는 제자리 회전 방식이다. 좁은 공간에서 방향을 바꾸기 쉽지만, 바닥 마찰이 크면 바퀴가 끌리거나 배터리 소모가 커질 수 있다. 더 부드럽게 돌고 싶다면 한쪽 바퀴를 멈추고 다른 쪽 바퀴만 돌리는 방식도 사용할 수 있다.

거리 측정 함수 만들기

이제 로봇의 눈을 붙여 보자. 거리 측정 함수는 초음파가 돌아오는 시간을 마이크로초 단위로 측정한 뒤 센티미터로 바꾼다. 소리는 공기 중에서 왕복하므로, 측정 시간에 해당하는 거리를 계산한 뒤 2로 나누어야 한다.

const int TRIG_PIN = 11;
const int ECHO_PIN = 12;

long readDistanceCm() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);

  long duration = pulseIn(ECHO_PIN, HIGH, 30000);

  if (duration == 0) {
    return 999;
  }

  long distance = duration / 58;
  return distance;
}

pulseIn()의 세 번째 값 30000은 타임아웃이다. Echo 신호가 너무 오래 돌아오지 않으면 무한히 기다리지 말고 빠져나오라는 뜻이다. 여기서는 약 30ms 정도 기다린다. 측정에 실패했을 때는 999를 반환하도록 했다. 이렇게 하면 장애물이 없는 먼 거리처럼 처리할 수 있다.

거리값은 약간 흔들릴 수 있다. 같은 위치에 손을 가만히 두어도 19cm, 20cm, 21cm처럼 조금씩 바뀐다. 로봇 제어에서는 이것이 자연스러운 일이다. 센서값을 단 한 번 읽고 즉시 판단하면 로봇이 예민하게 튈 수 있다. 그래서 필요하면 여러 번 읽어 평균을 내거나, 너무 이상한 값은 무시하는 처리를 추가한다.

간단한 평균 함수는 다음처럼 만들 수 있다.

long readAverageDistanceCm() {
  long sum = 0;
  int count = 0;

  for (int i = 0; i < 3; i++) {
    long d = readDistanceCm();

    if (d > 0 && d < 400) {
      sum += d;
      count++;
    }

    delay(10);
  }

  if (count == 0) {
    return 999;
  }

  return sum / count;
}

처음에는 평균 없이 시작해도 괜찮다. 하지만 로봇이 벽 앞에서 멈췄다 달렸다를 반복하거나, 아무것도 없는데 갑자기 회피 동작을 한다면 평균이나 필터링을 넣어보자. 로봇은 센서값을 믿고 움직이지만, 우리는 센서값이 항상 완벽하지 않다는 사실을 알고 있어야 한다.

가장 단순한 장애물 회피 알고리즘

이제 로봇에게 아주 간단한 규칙을 준다.

앞이 충분히 비어 있으면 전진한다. 앞에 장애물이 가까우면 정지한다. 잠깐 뒤로 물러난다. 오른쪽이나 왼쪽으로 회전한다. 다시 전진한다.

이 규칙은 어린아이도 이해할 수 있을 만큼 단순하지만, 실제 로봇 제어의 기본 구조를 그대로 담고 있다. 센서로 현재 상태를 읽고, 기준값과 비교하고, 모터 출력으로 행동을 바꾼다. 바로 피드백 제어의 가장 작은 형태다.

전체 코드를 하나로 합치면 다음과 같다.

const int AIN1 = 4;
const int AIN2 = 5;
const int PWMA = 6;

const int BIN1 = 7;
const int BIN2 = 8;
const int PWMB = 9;

const int STBY = 10;

const int TRIG_PIN = 11;
const int ECHO_PIN = 12;

const int FORWARD_SPEED = 150;
const int TURN_SPEED = 150;
const int BACKWARD_SPEED = 140;

const int OBSTACLE_DISTANCE = 20;

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

  pinMode(AIN1, OUTPUT);
  pinMode(AIN2, OUTPUT);
  pinMode(PWMA, OUTPUT);

  pinMode(BIN1, OUTPUT);
  pinMode(BIN2, OUTPUT);
  pinMode(PWMB, OUTPUT);

  pinMode(STBY, OUTPUT);
  digitalWrite(STBY, HIGH);

  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);

  stopMotors();
}

void setLeftMotor(int speed) {
  speed = constrain(speed, -255, 255);

  if (speed > 0) {
    digitalWrite(AIN1, HIGH);
    digitalWrite(AIN2, LOW);
    analogWrite(PWMA, speed);
  } else if (speed < 0) {
    digitalWrite(AIN1, LOW);
    digitalWrite(AIN2, HIGH);
    analogWrite(PWMA, -speed);
  } else {
    digitalWrite(AIN1, LOW);
    digitalWrite(AIN2, LOW);
    analogWrite(PWMA, 0);
  }
}

void setRightMotor(int speed) {
  speed = constrain(speed, -255, 255);

  if (speed > 0) {
    digitalWrite(BIN1, HIGH);
    digitalWrite(BIN2, LOW);
    analogWrite(PWMB, speed);
  } else if (speed < 0) {
    digitalWrite(BIN1, LOW);
    digitalWrite(BIN2, HIGH);
    analogWrite(PWMB, -speed);
  } else {
    digitalWrite(BIN1, LOW);
    digitalWrite(BIN2, LOW);
    analogWrite(PWMB, 0);
  }
}

void stopMotors() {
  setLeftMotor(0);
  setRightMotor(0);
}

void moveForward(int speed) {
  setLeftMotor(speed);
  setRightMotor(speed);
}

void moveBackward(int speed) {
  setLeftMotor(-speed);
  setRightMotor(-speed);
}

void turnLeft(int speed) {
  setLeftMotor(-speed);
  setRightMotor(speed);
}

void turnRight(int speed) {
  setLeftMotor(speed);
  setRightMotor(-speed);
}

long readDistanceCm() {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);

  long duration = pulseIn(ECHO_PIN, HIGH, 30000);

  if (duration == 0) {
    return 999;
  }

  return duration / 58;
}

long readAverageDistanceCm() {
  long sum = 0;
  int count = 0;

  for (int i = 0; i < 3; i++) {
    long d = readDistanceCm();

    if (d > 0 && d < 400) {
      sum += d;
      count++;
    }

    delay(10);
  }

  if (count == 0) {
    return 999;
  }

  return sum / count;
}

void avoidObstacle() {
  stopMotors();
  delay(200);

  moveBackward(BACKWARD_SPEED);
  delay(400);

  stopMotors();
  delay(100);

  turnRight(TURN_SPEED);
  delay(550);

  stopMotors();
  delay(100);
}

void loop() {
  long distance = readAverageDistanceCm();

  Serial.print("Distance: ");
  Serial.print(distance);
  Serial.println(" cm");

  if (distance <= OBSTACLE_DISTANCE) {
    avoidObstacle();
  } else {
    moveForward(FORWARD_SPEED);
  }

  delay(30);
}

코드를 올린 뒤 로봇을 바닥에 놓기 전, 반드시 바퀴를 공중에 띄운 상태로 테스트하자. 로봇을 손에 들거나 작은 상자 위에 올려 바퀴가 바닥에 닿지 않게 한다. 센서 앞에 손을 가까이 가져가면 바퀴가 멈추고, 뒤로 돌고, 회전해야 한다. 이 확인을 하지 않고 바로 바닥에 내려놓으면 로봇이 책상 아래로 돌진하거나 USB 케이블을 끌고 달아날 수 있다.

임계값과 속도 조정하기

OBSTACLE_DISTANCE는 로봇이 장애물을 피하기 시작하는 거리다. 예제에서는 20cm로 설정했다. 로봇 속도가 느리고 바퀴가 잘 멈춘다면 15cm도 가능하다. 반대로 로봇이 빠르거나 바닥이 미끄럽다면 25~30cm로 늘리는 것이 안전하다.

속도 값도 중요하다. PWM 값 255는 최대 출력이지만, 처음부터 최대 속도로 달리면 센서가 판단하기 전에 로봇이 장애물에 부딪힐 수 있다. 작은 실내 로봇에서는 120~180 정도에서 시작하는 것이 좋다. 바닥 재질, 배터리 전압, 모터 감속비에 따라 같은 PWM 값도 전혀 다른 속도로 느껴진다.

회전 시간 delay(550)은 로봇마다 달라진다. 어떤 로봇은 550ms 동안 거의 90도 회전하고, 어떤 로봇은 45도밖에 돌지 못한다. 바퀴 크기, 모터 속도, 배터리 상태, 바닥 마찰이 모두 영향을 준다. 그래서 이 값은 반드시 실험으로 맞춰야 한다.

조정은 한 번에 하나씩 하는 것이 좋다. 먼저 전진 속도를 정한다. 그 다음 장애물 감지 거리를 정한다. 마지막으로 후진 시간과 회전 시간을 맞춘다. 여러 값을 동시에 바꾸면 어떤 변화가 로봇의 움직임을 개선했는지 알기 어렵다.

상황별 테스트하기

장애물 회피 자동차는 코드를 올렸다고 끝나는 프로젝트가 아니다. 진짜 재미는 테스트하면서 시작된다. 로봇을 바닥에 내려놓고 벽을 향해 달리게 해보자. 처음에는 아주 넓은 공간에서 테스트한다. 로봇 주변에 컵, 케이블, 노트북, 반려동물의 물그릇이 없어야 한다.

첫 번째 테스트는 정면 벽이다. 로봇이 벽을 향해 전진하다가 적당한 거리에서 멈추고 후진한 뒤 회전해야 한다. 너무 늦게 멈추면 임계값을 늘리거나 속도를 낮춘다. 너무 멀리서 겁먹고 회피한다면 임계값을 줄인다.

두 번째 테스트는 박스 하나다. 벽보다 폭이 좁은 장애물을 앞에 둔다. 로봇이 박스 앞에서 회전한 뒤 옆으로 빠져나가는지 확인한다. 이때 로봇이 항상 오른쪽으로만 돈다면 환경에 따라 같은 곳을 맴돌 수 있다. 나중에 좌우 거리 비교를 추가하면 이 문제가 줄어든다.

세 번째 테스트는 의자 다리처럼 좁은 물체다. 초음파 센서는 좁고 둥근 물체를 놓칠 수 있다. 로봇이 의자 다리를 감지하지 못한다면 센서 높이를 조절하거나 감지 거리를 늘려본다. 그래도 어렵다면 전방 센서 하나만으로는 한계가 있다는 뜻이다. 이때가 바로 센서를 추가하거나 스캔 구조를 고민할 시점이다.

테스트 중에는 로봇의 행동을 말로 설명해 보는 것도 도움이 된다. "앞이 멀다, 전진한다. 앞이 가깝다, 멈춘다. 뒤로 간다. 오른쪽으로 돈다." 이렇게 흐름을 따라가면 코드와 실제 동작을 연결해서 이해하기 쉽다. 로봇이 이상하게 움직일 때도 어느 단계에서 문제가 생겼는지 찾기 쉬워진다.

더 자연스럽게 움직이게 만들기

지금 코드는 장애물을 만나면 항상 오른쪽으로 회전한다. 단순하고 안정적이지만, 좁은 공간에서는 같은 행동을 반복할 수 있다. 예를 들어 오른쪽에도 벽이 있는데 계속 오른쪽으로만 돌면 로봇은 구석에서 빠져나오지 못하고 빙글빙글 돌 수 있다.

가장 쉬운 개선은 회전 방향을 번갈아 바꾸는 것이다. 한 번은 오른쪽, 다음은 왼쪽으로 도는 식이다.

bool turnRightNext = true;

void avoidObstacleAlternate() {
  stopMotors();
  delay(200);

  moveBackward(BACKWARD_SPEED);
  delay(400);

  stopMotors();
  delay(100);

  if (turnRightNext) {
    turnRight(TURN_SPEED);
  } else {
    turnLeft(TURN_SPEED);
  }

  delay(550);
  stopMotors();
  delay(100);

  turnRightNext = !turnRightNext;
}

이 방법은 센서를 추가하지 않아도 적용할 수 있다. 다만 실제로 어느 쪽이 더 넓은지는 모른다. 운이 좋으면 빠져나오고, 운이 나쁘면 더 막힌 방향으로 갈 수도 있다. 로봇이 '생각해서' 피하는 느낌을 내려면 왼쪽과 오른쪽 공간을 비교해야 한다.

서보로 좌우 거리 비교하기

앞 장에서 만든 서보 모터 제어기를 기억해 보자. 거리 센서를 서보 위에 올리면 로봇은 고개를 좌우로 돌려 주변을 살필 수 있다. 사람으로 치면 앞에 벽이 있을 때 고개를 왼쪽으로 돌려 보고, 오른쪽으로 돌려 본 뒤 더 넓은 쪽으로 가는 것이다.

구조는 간단하다. 초음파 센서를 작은 브래킷이나 글루건으로 서보 혼 위에 고정한다. 서보는 로봇 앞쪽 중앙에 붙인다. 평상시에는 센서를 정면 90도에 두고 전진한다. 장애물을 만나면 서보를 왼쪽으로 돌려 거리를 측정하고, 오른쪽으로 돌려 다시 측정한다. 둘 중 더 먼 쪽으로 회전한다.

아두이노의 Servo 라이브러리를 사용하면 다음처럼 방향 선택 함수를 만들 수 있다.

#include <Servo.h>

Servo scanServo;
const int SERVO_PIN = 3;

const int SERVO_CENTER = 90;
const int SERVO_LEFT = 150;
const int SERVO_RIGHT = 30;

void setup() {
  // 기존 setup 내용에 추가
  scanServo.attach(SERVO_PIN);
  scanServo.write(SERVO_CENTER);
  delay(500);
}

long lookAt(int angle) {
  scanServo.write(angle);
  delay(350);
  return readAverageDistanceCm();
}

void avoidObstacleWithScan() {
  stopMotors();
  delay(200);

  moveBackward(BACKWARD_SPEED);
  delay(350);

  stopMotors();
  delay(100);

  long leftDistance = lookAt(SERVO_LEFT);
  long rightDistance = lookAt(SERVO_RIGHT);

  scanServo.write(SERVO_CENTER);
  delay(200);

  if (leftDistance > rightDistance) {
    turnLeft(TURN_SPEED);
  } else {
    turnRight(TURN_SPEED);
  }

  delay(550);
  stopMotors();
  delay(100);
}

실제 코드에 넣을 때는 기존 avoidObstacle() 대신 avoidObstacleWithScan()을 호출하면 된다. 이 방식은 전방 센서 하나만 사용할 때보다 훨씬 똑똑해 보인다. 로봇이 잠깐 멈춰서 왼쪽을 보고, 오른쪽을 보고, 더 넓은 곳으로 방향을 잡는 동작은 작은 자동차에 생명력을 준다.

다만 서보도 전류를 사용한다. 특히 센서를 올린 서보가 자주 움직이면 보드 전원이 흔들릴 수 있다. 서보 전원은 가능하면 별도의 5V 전원에서 공급하고, GND는 제어 보드와 공유한다. 서보가 움직일 때 센서값이 이상해지거나 보드가 재부팅되면 전원 용량을 먼저 의심하자.

delay 방식의 한계

지금까지의 코드는 delay()를 많이 사용한다. delay()는 간단하고 이해하기 쉽다. 400ms 후진하고, 550ms 회전하는 식으로 동작을 만들기 좋다. 그러나 delay()가 실행되는 동안 제어 보드는 다른 일을 거의 하지 못한다. 회전하는 550ms 동안 앞에 새로운 장애물이 나타나도 바로 반응하지 못한다.

처음 만드는 장애물 회피 자동차에서는 이 정도로 충분하다. 하지만 로봇을 더 빠르게 움직이거나 복잡한 환경에 넣으면 문제가 드러난다. 로봇이 회전 중에 책상 다리에 부딪히거나, 후진 중에 뒤쪽 장애물을 보지 못할 수 있다. 이것은 코드가 틀렸다기보다 구조가 단순하기 때문에 생기는 자연스러운 한계다.

개선하려면 시간을 잘게 나누어 행동해야 한다. 예를 들어 550ms 동안 한 번에 회전하지 않고, 20ms씩 회전하면서 중간중간 센서를 읽는다. 더 나아가면 로봇의 상태를 전진, 후진, 회전, 정지 같은 상태로 나누고, 시간과 센서값에 따라 상태를 바꾸는 방식으로 작성할 수 있다. 이것을 상태 머신이라고 부른다. 상태 머신은 복잡한 로봇 동작을 안전하게 관리하는 데 매우 유용하다.

회전 시간 고정 방식의 한계

현재 로봇은 "550ms 동안 오른쪽으로 돌면 적당히 방향이 바뀔 것이다"라고 가정한다. 하지만 실제 세계는 그렇게 친절하지 않다. 배터리가 새것일 때는 빠르게 돌고, 배터리가 약해지면 천천히 돈다. 카펫 위에서는 바퀴가 잘 안 돌고, 매끈한 바닥에서는 미끄러진다. 오른쪽 모터와 왼쪽 모터의 힘이 조금 달라도 회전 각도는 달라진다.

그래서 시간 기반 회전은 간단하지만 정확하지 않다. 장애물 회피 자동차가 가끔은 90도 가까이 돌고, 가끔은 40도만 도는 이유가 여기에 있다. 처음에는 괜찮지만, 로봇을 원하는 경로로 안정적으로 움직이게 하려면 피드백이 필요하다.

가장 직접적인 개선은 엔코더를 붙이는 것이다. 엔코더가 있으면 바퀴가 실제로 얼마나 돌았는지 알 수 있다. 그러면 "550ms 동안 회전"이 아니라 "왼쪽 바퀴는 이만큼 뒤로, 오른쪽 바퀴는 이만큼 앞으로 돌 때까지 회전"으로 바꿀 수 있다. 배터리 전압이 조금 떨어져도 목표 회전량에 도달할 때까지 제어할 수 있으므로 동작이 훨씬 일정해진다.

IMU를 추가하는 방법도 있다. 자이로스코프로 로봇의 회전 각도를 추정하면, 실제로 90도 회전했는지 판단할 수 있다. 다만 IMU는 드리프트와 진동 문제를 고려해야 하므로 처음에는 엔코더 기반 개선이 더 직관적이다.

로봇이 직진하지 않을 때

2개의 DC 모터를 같은 PWM 값으로 돌려도 로봇이 완벽히 직진하지 않는 경우가 많다. 왼쪽 모터와 오른쪽 모터의 성능이 조금 다르고, 바퀴 마찰도 다르며, 프레임 조립 상태도 완전히 대칭이 아니기 때문이다. 이것은 거의 모든 바퀴 로봇에서 만나는 문제다.

가장 간단한 보정은 좌우 속도 값을 다르게 주는 것이다. 예를 들어 로봇이 계속 왼쪽으로 휜다면 왼쪽 바퀴가 느리거나 오른쪽 바퀴가 빠른 것이다. 왼쪽 속도를 조금 올리거나 오른쪽 속도를 조금 낮춘다.

void moveForwardCalibrated(int baseSpeed) {
  int leftSpeed = baseSpeed + 5;
  int rightSpeed = baseSpeed - 5;

  setLeftMotor(leftSpeed);
  setRightMotor(rightSpeed);
}

이 방법은 간단하지만 모든 상황에서 완벽하지는 않다. 배터리 전압이 바뀌거나 바닥이 달라지면 다시 휠 수 있다. 더 좋은 방법은 엔코더로 좌우 바퀴 속도를 측정하고, 느린 쪽은 PWM을 올리고 빠른 쪽은 PWM을 낮추는 것이다. 앞에서 배운 모터 속도 제어와 PID 제어가 바로 여기서 힘을 발휘한다.

자주 만나는 문제와 해결 방법

장애물 회피 자동차는 부품이 여러 개라서 문제가 생겼을 때 원인을 한 번에 찾기 어렵다. 이럴 때는 전체를 의심하지 말고, 기능을 나누어 확인한다. 전원, 모터, 센서, 알고리즘 순서로 좁혀가면 대부분 해결된다.

증상가능한 원인확인할 것
보드가 계속 재부팅된다모터 전류 때문에 전압이 떨어짐모터 전원 분리, 배터리 용량, 공통 GND
한쪽 바퀴만 돈다모터 배선 또는 드라이버 입력 문제모터 단자, 핀 번호, 납땜 상태
전진 명령에서 회전한다한쪽 모터 방향이 반대모터 선 교체 또는 방향 코드 수정
센서값이 0 또는 999만 나온다Trig/Echo 배선 오류, GND 누락핀 연결, 시리얼 출력, 센서 전원
장애물이 없는데 회피한다센서 노이즈, 임계값이 너무 큼평균 필터, 센서 각도, 임계값 조정
벽에 너무 가까이 가서 멈춘다속도가 빠르거나 임계값이 작음전진 속도 낮추기, 감지 거리 늘리기

문제가 생기면 코드를 복잡하게 고치기 전에 시리얼 모니터를 보자. 거리값이 정상인지, 장애물 감지 조건에 들어가는지, 회피 함수가 호출되는지 출력하면 로봇의 머릿속을 들여다보는 것과 같다. 움직이는 로봇을 디버깅할 때 시리얼 출력은 작은 블랙박스 역할을 한다.

더 발전시키는 아이디어

장애물 회피 자동차는 확장하기 좋은 플랫폼이다. 같은 프레임 위에 센서를 추가하고 알고리즘을 바꾸면 전혀 다른 로봇이 된다. 전방 초음파 센서 하나로 시작했지만, 좌우 센서를 추가하면 더 빠르게 판단할 수 있다. 엔코더를 붙이면 이동 거리와 회전량을 알 수 있다. IMU를 붙이면 방향 변화를 추정할 수 있다.

다음과 같은 방향으로 확장해 볼 수 있다.

  • 서보 스캔으로 왼쪽, 가운데, 오른쪽 거리를 비교해 더 넓은 방향으로 이동한다.

  • 엔코더를 추가해 후진 거리와 회전 각도를 시간 대신 바퀴 회전량으로 제어한다.

  • 전방뿐 아니라 좌우 또는 후방 센서를 추가해 좁은 공간에서 충돌을 줄인다.

  • Bluetooth나 Wi-Fi를 연결해 자동 주행 모드와 수동 조종 모드를 전환한다.

  • 라인 센서를 추가해 기본은 라인을 따라가되 장애물을 만나면 우회하도록 만든다.

여기서 중요한 것은 한 번에 모든 기능을 넣지 않는 것이다. 센서를 하나 추가하면 먼저 그 센서값만 확인한다. 알고리즘을 바꾸면 낮은 속도에서 테스트한다. 전원을 바꾸면 모터를 공중에 띄워 안전하게 확인한다. 로봇 제작은 멋진 기능을 한꺼번에 붙이는 일이 아니라, 작은 성공을 차곡차곡 쌓아 움직임을 안정시키는 일이다.

이번 장에서 완성한 것

이번 장에서는 DC 기어드 모터, 모터 드라이버, 거리 센서를 결합해 스스로 장애물을 피하는 바퀴 로봇을 만들었다. 프레임 위에 모터, 캐스터, 배터리, 제어 보드, 센서를 배치하고, 전원 분리와 공통 GND를 확인했다. 그 다음 모터 제어 함수를 만들고 전진, 후진, 좌회전을 검증했다. 마지막으로 거리 센서값을 읽어 장애물이 가까우면 정지, 후진, 회전하는 알고리즘을 구현했다.

이 로봇은 아직 완벽하지 않다. 회전 시간을 고정값으로 사용하기 때문에 바닥 상태와 배터리 전압에 따라 동작이 달라진다. 전방 센서 하나만으로는 옆이나 뒤의 장애물을 알 수 없다. 하지만 바로 이 한계가 다음 단계로 가는 문이다. 좌우 거리 비교를 넣으면 더 영리해지고, 엔코더 피드백을 넣으면 더 정확해지며, 라인 센서나 카메라를 붙이면 목적을 가진 이동 로봇으로 발전한다.

지금 손에 있는 작은 자동차는 단순한 장난감이 아니다. 센서로 세상을 보고, 코드로 판단하고, 모터로 행동하는 로봇의 기본 구조를 모두 갖춘 실험 플랫폼이다. 이제 이 플랫폼 위에 더 정교한 주행 규칙을 올릴 차례다. 다음 장에서는 바닥의 선을 읽고 따라가는 라인트레이서 로봇으로 넘어가, 센서값을 모터 속도 차이로 바꾸는 또 다른 주행 제어를 만들어 본다.

책 목록

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

전체 목차 보기

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

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

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

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

참여하기