이제 로봇이 스스로 길을 고른다
지금까지 우리는 로봇의 몸체를 만들고, 바퀴를 움직이고, 센서값으로 판단하는 방법을 배웠습니다. 이번 장에서는 이 조각들을 하나로 합칩니다. 목표는 간단합니다.
로봇이 앞을 보고, 장애물이 가까우면 멈추고, 좌우를 살핀 뒤 더 넓은 곳으로 방향을 바꾸게 만드는 것입니다.
사람이 복도에서 걷다가 앞에 상자가 있으면 잠깐 멈추고 왼쪽과 오른쪽을 봅니다. 더 비어 있는 쪽으로 몸을 돌린 뒤 다시 걷습니다. 장애물 회피 로봇도 같은 흐름으로 움직입니다.
입력: 초음파 센서가 앞, 왼쪽, 오른쪽 거리를 잽니다.
처리: 아두이노가 어느 쪽이 더 넓은지 비교합니다.
출력: 모터가 후진하거나 회전하고 다시 전진합니다.
이번 로봇은 완벽한 자율주행 자동차는 아닙니다. 하지만 로봇이 스스로 주변을 확인하고 행동을 바꾸는 기본 구조를 직접 경험하기에는 충분합니다.
AI 생성 이미지
초음파 센서를 전면에 달기
초음파 센서는 로봇의 눈 역할을 합니다. 실제 눈처럼 이미지를 보는 것은 아니지만, 앞에 물체가 얼마나 가까운지 거리로 알려 줍니다.
센서는 차체의 앞쪽 중앙에 달아야 합니다. 한쪽으로 치우치면 로봇이 실제보다 왼쪽이나 오른쪽을 더 많이 보는 것처럼 판단할 수 있습니다. 센서의 두 둥근 부분이 정면을 향하도록 고정합니다. 너무 위를 보면 낮은 장애물을 놓치고, 너무 아래를 보면 바닥을 장애물로 착각할 수 있습니다.
기본 연결은 다음과 같습니다.
| 부품 | 연결 핀 |
|---|---|
| 초음파 센서 VCC | 아두이노 5V |
| 초음파 센서 GND | 아두이노 GND |
| 초음파 센서 TRIG | D2 |
| 초음파 센서 ECHO | D11 |
| 서보 모터 신호선 | D3 |
| 서보 모터 VCC | 아두이노 5V 또는 별도 5V 전원 |
| 서보 모터 GND | 아두이노 GND와 공통 연결 |
서보 모터는 초음파 센서를 좌우로 돌리는 목 역할을 합니다. 작은 SG90 서보라면 아두이노 5V에서 동작하는 경우가 많지만, 서보가 떨리거나 아두이노가 재시작되면 전원이 부족한 것입니다. 이때는 서보에 별도 5V 전원을 사용하고, 그 전원의 GND와 아두이노 GND를 반드시 연결합니다.
여기서 핀 배치를 한 가지 바꿉니다. 아두이노 우노에서 Servo 라이브러리를 사용하면 9번과 10번 PWM 출력이 영향을 받을 수 있습니다. 그래서 이번 장에서는 모터 속도 PWM 핀을 5번과 6번으로 사용합니다.
모터 드라이버 연결 정리
아래 연결은 7장에서 사용한 TB6612FNG 모터 드라이버를 기준으로 합니다. L298N을 사용한다면 같은 역할의 핀으로 바꾸면 됩니다. 예를 들어 LEFT_PWM은 L298N의 ENA, RIGHT_PWM은 ENB에 해당합니다. L298N에는 TB6612FNG의 STBY 핀이 없으므로, 그 부분은 연결하지 않아도 됩니다.
| 아두이노 핀 | TB6612FNG 핀 | 역할 |
|---|---|---|
| D4 | AIN1 | 왼쪽 모터 방향 1 |
| D7 | AIN2 | 왼쪽 모터 방향 2 |
| D5 | PWMA | 왼쪽 모터 속도 PWM |
| D8 | BIN1 | 오른쪽 모터 방향 1 |
| D12 | BIN2 | 오른쪽 모터 방향 2 |
| D6 | PWMB | 오른쪽 모터 속도 PWM |
| D13 | STBY | 드라이버 동작 허용 |
| 5V | VCC | 드라이버 논리 전원 |
| 모터용 배터리 + | VM | 모터 전원 입력 |
| GND | GND | 아두이노, 드라이버, 배터리의 공통 GND |
| 왼쪽 모터 | AO1/AO2 | 왼쪽 모터 출력 |
| 오른쪽 모터 | BO1/BO2 | 오른쪽 모터 출력 |
모듈에 따라 모터 출력 단자가 AO1, AO2 대신 A01, A02처럼 표시되기도 합니다. 숫자 1과 2의 순서는 모터 회전 방향에 영향을 줍니다. 나중에 왼쪽 모터가 반대로 돌면 이 두 선을 서로 바꾸거나 코드에서 방향을 바꾸면 됩니다.
모터 전원은 배터리에서 모터 드라이버의 VM 또는 모터 전원 단자로 들어갑니다. 아두이노 5V 핀에서 모터 전원을 가져오면 안 됩니다. 모터는 순간적으로 큰 전류를 요구하기 때문입니다.
항상 기억할 원칙은 하나입니다.
모터 전원은 따로, GND는 함께.
장애물 기준 거리 정하기
로봇이 장애물을 피하려면 "얼마나 가까우면 위험한가?"를 정해야 합니다. 이것을 안전거리라고 합니다.
처음에는 안전거리를 25cm 정도로 잡아 봅니다.
const int SAFE_DISTANCE = 25; // cm로봇이 빠르면 더 일찍 멈춰야 하므로 30cm 이상이 필요할 수 있습니다. 로봇이 느리면 15~20cm도 가능할 수 있습니다. 정답은 부품이 아니라 실제 주행에서 정합니다.
초보자에게 좋은 시작값은 다음과 같습니다.
속도: 140~160
안전거리: 25cm
회전 시간: 400~600ms
먼저 느리게 움직이게 만든 뒤, 안정적으로 피하면 조금씩 속도를 올립니다.
회피 알고리즘의 흐름
이번 로봇의 판단 순서는 다음과 같습니다.
정면 거리를 잰다.
정면이 안전거리보다 멀면 앞으로 간다.
정면이 가까우면 멈춘다.
잠깐 뒤로 간다.
서보를 왼쪽으로 돌려 왼쪽 거리를 잰다.
서보를 오른쪽으로 돌려 오른쪽 거리를 잰다.
더 먼 쪽으로 회전한다.
다시 정면을 보고 앞으로 간다.
이 방식은 단순하지만 효과적입니다. 로봇이 지도를 갖고 있지 않아도, 그 순간 더 비어 있는 방향을 선택할 수 있습니다.
전체 코드 작성하기
아래 코드는 초음파 센서, 서보 모터, 바퀴 구동부를 모두 합친 예제입니다. 먼저 로봇을 바닥에 내려놓지 말고 책이나 상자 위에 올려 바퀴가 공중에서 도는 상태로 테스트하세요.
#include <Servo.h>
Servo headServo;
const int SERVO_PIN = 3;
const int TRIG_PIN = 2;
const int ECHO_PIN = 11;
const int LEFT_IN1 = 4;
const int LEFT_IN2 = 7;
const int LEFT_PWM = 5;
const int RIGHT_IN1 = 8;
const int RIGHT_IN2 = 12;
const int RIGHT_PWM = 6;
const int STBY_PIN = 13;
const int BASIC_SPEED = 150;
const int TURN_SPEED = 150;
const int BACK_SPEED = 130;
const int SAFE_DISTANCE = 25;
const int MAX_DISTANCE = 200;
const int TURN_GAP = 5;
const int CENTER_ANGLE = 90;
const int LEFT_ANGLE = 150;
const int RIGHT_ANGLE = 30;
void setup() {
Serial.begin(9600);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
pinMode(LEFT_IN1, OUTPUT);
pinMode(LEFT_IN2, OUTPUT);
pinMode(LEFT_PWM, OUTPUT);
pinMode(RIGHT_IN1, OUTPUT);
pinMode(RIGHT_IN2, OUTPUT);
pinMode(RIGHT_PWM, OUTPUT);
pinMode(STBY_PIN, OUTPUT);
digitalWrite(STBY_PIN, HIGH);
headServo.attach(SERVO_PIN);
headServo.write(CENTER_ANGLE);
stopMotors();
delay(500);
}
void loop() {
int frontDistance = readDistanceAverage();
Serial.print("front: ");
Serial.print(frontDistance);
Serial.println(" cm");
if (frontDistance > SAFE_DISTANCE) {
forward(BASIC_SPEED);
} else {
avoidObstacle();
}
delay(30);
}
void avoidObstacle() {
stopMotors();
delay(150);
backward(BACK_SPEED);
delay(300);
stopMotors();
delay(150);
int leftDistance = lookAndRead(LEFT_ANGLE);
int rightDistance = lookAndRead(RIGHT_ANGLE);
headServo.write(CENTER_ANGLE);
delay(200);
Serial.print("left: ");
Serial.print(leftDistance);
Serial.print(" cm, right: ");
Serial.print(rightDistance);
Serial.println(" cm");
if (leftDistance > rightDistance + TURN_GAP) {
turnLeft(TURN_SPEED);
delay(450);
} else if (rightDistance > leftDistance + TURN_GAP) {
turnRight(TURN_SPEED);
delay(450);
} else {
turnRight(TURN_SPEED);
delay(600);
}
stopMotors();
delay(100);
}
int lookAndRead(int angle) {
headServo.write(angle);
delay(350);
return readDistanceAverage();
}
int readDistanceAverage() {
long sum = 0;
int count = 0;
for (int i = 0; i < 3; i++) {
int distance = readDistanceCm();
if (distance > 2 && distance < MAX_DISTANCE) {
sum += distance;
count++;
}
delay(20);
}
if (count == 0) {
return MAX_DISTANCE;
}
return sum / count;
}
int readDistanceCm() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
long duration = pulseIn(ECHO_PIN, HIGH, 25000);
if (duration == 0) {
return MAX_DISTANCE;
}
int distance = duration * 0.0343 / 2;
if (distance > MAX_DISTANCE) {
distance = MAX_DISTANCE;
}
return distance;
}
void driveLeftMotor(int speed) {
speed = constrain(speed, -255, 255);
if (speed > 0) {
digitalWrite(LEFT_IN1, HIGH);
digitalWrite(LEFT_IN2, LOW);
analogWrite(LEFT_PWM, speed);
} else if (speed < 0) {
digitalWrite(LEFT_IN1, LOW);
digitalWrite(LEFT_IN2, HIGH);
analogWrite(LEFT_PWM, -speed);
} else {
digitalWrite(LEFT_IN1, LOW);
digitalWrite(LEFT_IN2, LOW);
analogWrite(LEFT_PWM, 0);
}
}
void driveRightMotor(int speed) {
speed = constrain(speed, -255, 255);
if (speed > 0) {
digitalWrite(RIGHT_IN1, HIGH);
digitalWrite(RIGHT_IN2, LOW);
analogWrite(RIGHT_PWM, speed);
} else if (speed < 0) {
digitalWrite(RIGHT_IN1, LOW);
digitalWrite(RIGHT_IN2, HIGH);
analogWrite(RIGHT_PWM, -speed);
} else {
digitalWrite(RIGHT_IN1, LOW);
digitalWrite(RIGHT_IN2, LOW);
analogWrite(RIGHT_PWM, 0);
}
}
void forward(int speed) {
driveLeftMotor(speed);
driveRightMotor(speed);
}
void backward(int speed) {
driveLeftMotor(-speed);
driveRightMotor(-speed);
}
void turnLeft(int speed) {
driveLeftMotor(-speed);
driveRightMotor(speed);
}
void turnRight(int speed) {
driveLeftMotor(speed);
driveRightMotor(-speed);
}
void stopMotors() {
driveLeftMotor(0);
driveRightMotor(0);
}이 코드는 이해하기 쉽게 짧은 delay()를 사용했습니다. 서보가 왼쪽과 오른쪽으로 움직일 시간을 주어야 하기 때문입니다. 더 빠르게 반응하는 로봇을 만들고 싶다면 로봇의 판단 구조를 만들 때 배운 millis() 상태 머신으로 같은 흐름을 바꿀 수 있습니다.
먼저 센서값부터 확인하기
바퀴를 바닥에 내려놓기 전에 시리얼 모니터를 엽니다. 손을 센서 앞에 가까이 가져가면 front 값이 줄어들어야 합니다. 손을 멀리하면 값이 커져야 합니다.
왼쪽과 오른쪽 값도 확인합니다. 로봇 앞에 장애물을 가까이 두면 로봇이 멈추고, 서보가 좌우로 움직이며 left, right 값을 출력합니다.
만약 왼쪽에 공간이 넓은데 오른쪽으로 돈다면 두 가지를 확인합니다.
서보가 실제로 왼쪽을 볼 때
LEFT_ANGLE값이 맞는가?turnLeft()와turnRight()의 모터 방향이 실제 움직임과 맞는가?
서보가 반대로 움직이면 LEFT_ANGLE과 RIGHT_ANGLE 값을 서로 바꾸면 됩니다. 모터가 반대로 움직이면 해당 모터 선을 바꾸거나 driveLeftMotor(), driveRightMotor()의 HIGH와 LOW를 조정합니다.
실내 주행에서 조정할 것들
장애물 회피 로봇은 환경의 영향을 많이 받습니다. 같은 코드라도 방, 복도, 책상 아래에서 결과가 달라질 수 있습니다.
첫째, 벽면 재질을 확인합니다. 딱딱하고 평평한 벽은 초음파가 잘 반사됩니다. 커튼, 천, 비스듬한 물체는 초음파가 흩어져 거리값이 불안정할 수 있습니다. 의자 다리처럼 가느다란 물체도 놓치기 쉽습니다.
둘째, 센서 각도를 조정합니다. 센서가 약간 위를 향하면 낮은 상자나 문턱을 보지 못합니다. 너무 아래를 향하면 바닥을 장애물로 읽습니다. 정면에서 수평에 가깝게 맞추고, 필요하면 아주 조금 아래로 기울여 봅니다.
셋째, 속도를 낮춰 봅니다. 로봇이 장애물을 보고도 부딪힌다면 코드가 틀린 것이 아니라 멈출 시간이 부족한 것일 수 있습니다. BASIC_SPEED를 낮추거나 SAFE_DISTANCE를 키웁니다.
const int BASIC_SPEED = 130;
const int SAFE_DISTANCE = 30;넷째, 회전 시간을 조정합니다. 너무 조금 돌면 같은 장애물을 다시 만나고, 너무 많이 돌면 방향이 크게 틀어집니다. delay(450) 값을 50ms씩 바꾸며 적당한 값을 찾습니다.
자주 생기는 문제
로봇이 계속 멈춰 있으면 초음파 센서가 너무 가까운 값을 읽는지 확인합니다. 센서가 바닥을 보고 있거나, 전면 부품이 센서 앞을 가리고 있을 수 있습니다.
로봇이 계속 직진만 하면 frontDistance 값이 실제보다 크게 나오는 것입니다. TRIG와 ECHO 핀 연결, GND 연결, 시리얼 모니터 값을 차례로 확인합니다.
서보가 떨리고 아두이노가 재시작되면 전원 문제일 가능성이 큽니다. 서보와 모터는 순간적으로 큰 전류를 요구합니다. 배터리를 충분히 충전하고, 필요하면 아두이노와 모터 전원을 분리합니다. 단, GND는 반드시 함께 연결합니다.
로봇이 장애물을 피하지만 자꾸 한쪽으로 치우치면 좌우 모터 힘이 다를 수 있습니다. 빠른 쪽 속도를 조금 낮추면 직진이 좋아집니다.
자율 행동의 첫걸음
이번 장에서 만든 로봇은 단순히 정해진 순서대로 움직이지 않습니다. 앞을 보고, 위험하면 멈추고, 좌우를 비교한 뒤 더 넓은 쪽을 선택합니다.
이것이 자율 행동의 기본입니다. 복잡한 인공지능이 없어도 로봇은 센서값과 조건, 모터 동작의 조합으로 스스로 움직이는 것처럼 행동할 수 있습니다.
다음 장에서는 다른 방식의 자율주행을 배웁니다. 이번에는 장애물을 피하는 대신, 바닥의 선을 읽고 따라가는 라인트레이서 로봇을 만들 것입니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.