센서가 행동으로 바뀌는 첫 로봇
지금까지 우리는 센서, 모터, 통신, 제어를 각각 따로 배웠다. 초음파 센서로 거리를 읽고, DC 모터를 드라이버로 움직이고, 조건문으로 판단하고, LED나 부저로 상태를 표시하는 방법도 익혔다. 이번 장에서는 이 조각들을 하나로 묶어 실제로 반응하는 작은 로봇을 만든다.
목표는 단순하다. 로봇이 앞으로 움직이다가 앞에 물체가 가까워지면 멈춘다. 조금 가까워지면 LED가 깜빡이고 부저가 천천히 울리며, 더 가까워질수록 경고음 간격이 짧아진다. 손을 가까이 가져가면 "삐… 삐… 삐삐삐!" 하다가 멈추는 로봇이다.
겉으로 보면 작은 장난감 같지만, 구조는 꽤 중요하다. 이 프로젝트에는 로봇 제어의 가장 기본적인 흐름이 모두 들어 있다.
센서가 현재 상태를 측정한다. 제어 보드는 측정값을 기준값과 비교한다. 조건에 따라 모터, LED, 부저를 제어한다. 바로 이것이 입력, 판단, 출력으로 이어지는 로봇의 기본 행동 루프다.

이번 프로젝트의 전체 구조
이번 장에서 만들 로봇은 초음파 거리 센서를 앞쪽에 달고, 두 개의 DC 모터로 움직이는 작은 바퀴 로봇을 기준으로 설명한다. 아직 바퀴 프레임이 없다면 모터 연결 부분을 빼고, 센서와 LED, 부저만으로도 실습할 수 있다. 손을 가까이 대면 LED가 켜지고 부저가 빨라지는 탁상용 거리 경보기로 먼저 만들어도 좋다.
동작 규칙은 다음과 같이 잡아보자.
| 거리 | 로봇 동작 | 표시 동작 |
|---|---|---|
| 35cm보다 멀다 | 정상 속도로 전진 | LED 꺼짐, 부저 꺼짐 |
| 18~35cm | 느리게 전진 | LED 깜빡임, 부저 느리게 울림 |
| 18cm 이하 | 정지 | LED 켜짐, 부저 빠르게 울림 |
| 거리 측정 실패 | 안전을 위해 정지 | LED 켜짐, 경고음 |
마지막 줄이 중요하다. 센서값을 읽지 못했을 때 "장애물이 없나 보다"라고 판단하면 로봇이 계속 달릴 수 있다. 실제 로봇에서는 알 수 없는 상태를 안전한 상태로 처리하는 습관이 필요하다. 이 프로젝트에서는 거리 측정이 실패하면 일단 멈추도록 만들겠다.
준비물
기본 구성은 아두이노 우노 또는 나노를 기준으로 한다. ESP32를 사용해도 원리는 같지만, 초음파 센서의 Echo 신호 전압에 주의해야 한다.
아두이노 우노, 나노 또는 ESP32 보드 1개
HC-SR04 초음파 거리 센서 1개
2륜 로봇 프레임, DC 기어드 모터 2개, 바퀴 2개
모터 드라이버 1개, 예를 들어 TB6612FNG 또는 L298N 계열
LED 1개와 220Ω 정도의 저항 1개
부저 1개
모터용 배터리와 보드용 전원
점퍼선, 브레드보드, 필요하면 스위치
부저는 능동 부저와 수동 부저가 있다. 능동 부저는 전원을 주면 자체적으로 소리가 나는 부품이고, 수동 부저는 보드에서 빠르게 ON/OFF 신호를 만들어 주어야 소리가 난다. 이 장의 예제 코드는 tone() 함수를 사용하므로 수동 부저에 잘 맞는다. 능동 부저를 사용해도 소리는 날 수 있지만, 음높이를 세밀하게 바꾸는 느낌은 다를 수 있다.
초음파 센서 연결하기
HC-SR04 초음파 센서에는 보통 네 개의 핀이 있다. VCC, Trig, Echo, GND다. Trig는 보드가 센서에게 "초음파를 쏴!"라고 명령하는 출력 핀이고, Echo는 초음파가 돌아오기까지 걸린 시간을 알려주는 입력 핀이다.
아두이노 우노 기준 연결은 다음과 같다.
| HC-SR04 핀 | 연결 |
|---|---|
| VCC | 5V |
| GND | GND |
| Trig | D8 |
| Echo | D7 |
ESP32를 사용할 때는 한 가지를 꼭 확인해야 한다. ESP32의 GPIO 입력은 보통 3.3V 기준이다. HC-SR04를 5V로 구동하면 Echo 핀에서 5V 신호가 나올 수 있으므로, Echo 신호를 그대로 ESP32에 넣으면 보드에 무리가 갈 수 있다. 이때는 저항 두 개로 전압 분배 회로를 만들거나, 3.3V 호환 초음파 센서를 사용하자. 예를 들어 Echo와 ESP32 입력 핀 사이에 1kΩ 저항을 넣고, 입력 핀에서 GND로 2kΩ 저항을 연결하면 약 3.3V 수준으로 낮출 수 있다.
아두이노에서는 Echo를 D7에 바로 연결해도 된다. 다만 어떤 보드를 쓰든 GND는 반드시 서로 연결해야 한다. 센서 GND, 제어 보드 GND, 모터 드라이버 GND가 같은 기준을 가져야 신호가 제대로 해석된다.
모터 드라이버 연결하기
이번 예제 코드는 TB6612FNG 계열 모터 드라이버를 기준으로 작성한다. L298N을 사용해도 원리는 같다. PWM 핀은 속도 조절에 쓰고, IN 핀들은 방향을 정하는 데 쓴다.
TB6612FNG 기준 연결 예시는 다음과 같다.
| 모터 드라이버 핀 | 연결 |
|---|---|
| VM | 모터용 배터리 + |
| VCC | 아두이노 5V |
| GND | 아두이노 GND, 배터리 -와 공통 연결 |
| STBY | D2 |
| PWMA | D5 |
| AIN1 | D9 |
| AIN2 | D10 |
| PWMB | D6 |
| BIN1 | D11 |
| BIN2 | D12 |
| AO1, AO2 | 왼쪽 모터 |
| BO1, BO2 | 오른쪽 모터 |
여기서 가장 조심할 부분은 전원이다. 모터는 생각보다 큰 전류를 순간적으로 요구한다. 보드의 5V 핀에서 모터 전원을 직접 가져오면 보드가 리셋되거나 센서값이 이상해질 수 있다. 모터용 전원은 모터 드라이버의 VM으로 따로 넣고, 제어 보드와 모터 드라이버의 GND만 공통으로 묶는다.
LED와 부저는 다음처럼 연결한다.
| 부품 | 연결 |
|---|---|
| LED 긴 다리 | 220Ω 저항을 거쳐 D4 |
| LED 짧은 다리 | GND |
| 부저 + | D3 |
| 부저 - | GND |
이제 로봇은 눈앞의 거리를 보고, 바퀴를 움직이고, 빛과 소리로 상태를 알려줄 준비가 되었다.
초음파 거리 측정 원리
초음파 센서는 아주 짧은 소리 파동을 앞으로 보낸다. 이 소리는 사람 귀에는 들리지 않을 정도로 높은 주파수다. 앞에 물체가 있으면 소리가 물체에 부딪혀 되돌아오고, 센서는 되돌아오는 데 걸린 시간을 잰다.
보드가 Trig 핀에 약 10마이크로초 동안 HIGH 신호를 주면 센서는 초음파를 내보낸다. 그 뒤 Echo 핀이 HIGH로 유지되는 시간이 생기는데, 이 시간이 초음파가 나갔다가 돌아오는 데 걸린 시간이다.
거리 계산식은 다음과 같다.
거리(cm) = Echo 시간이 유지된 마이크로초 × 0.0343 ÷ 2여기서 0.0343은 소리가 1마이크로초 동안 대략 몇 cm를 이동하는지를 나타낸 값이다. 마지막에 2로 나누는 이유는 초음파가 물체까지 한 번 가고, 다시 센서까지 돌아오기 때문이다. Echo 시간에는 왕복 시간이 들어 있으므로 실제 거리는 그 절반이다.
1단계: 센서값만 먼저 확인하기
처음부터 모터를 같이 움직이면 문제가 생겼을 때 원인을 찾기 어렵다. 먼저 센서만 연결하고, 시리얼 모니터에 거리값이 제대로 나오는지 확인하자.
const int TRIG_PIN = 8;
const int ECHO_PIN = 7;
void setup() {
Serial.begin(9600);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
}
void loop() {
float distance = readDistanceCm();
if (distance < 0) {
Serial.println("invalid");
} else {
Serial.print(distance);
Serial.println(" cm");
}
delay(200);
}
float readDistanceCm() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
unsigned long duration = pulseIn(ECHO_PIN, HIGH, 30000UL);
if (duration == 0) {
return -1.0;
}
float distance = duration * 0.0343 / 2.0;
if (distance < 2.0 || distance > 300.0) {
return -1.0;
}
return distance;
}시리얼 모니터를 9600bps로 열고 손을 센서 앞에서 천천히 움직여보자. 손이 가까워지면 숫자가 작아지고, 멀어지면 숫자가 커져야 한다. 값이 계속 invalid로 나온다면 Trig와 Echo가 바뀌었거나, GND 연결이 빠졌거나, 센서 전원이 제대로 들어오지 않았을 가능성이 크다.
센서 앞에 비스듬한 물체를 두면 값이 튈 수 있다. 초음파가 거울처럼 옆으로 튕겨 나가면 센서가 반사파를 받지 못하기 때문이다. 스펀지나 천처럼 소리를 흡수하는 물체도 측정이 불안정할 수 있다. 이것은 코드 문제가 아니라 초음파 센서의 특성이다.
2단계: 가까우면 LED 켜기
센서값이 확인되었다면 이제 판단을 넣을 차례다. 가장 쉬운 행동은 LED다. 18cm보다 가까우면 LED를 켜고, 멀면 끈다.
핵심 코드는 다음과 같다.
const int LED_PIN = 4;
const float STOP_DISTANCE_CM = 18.0;
if (distance > 0 && distance <= STOP_DISTANCE_CM) {
digitalWrite(LED_PIN, HIGH);
} else {
digitalWrite(LED_PIN, LOW);
}이 짧은 조건문이 로봇 행동의 출발점이다. 센서값을 읽고, 기준과 비교하고, 출력 장치를 바꾼다. 나중에 LED 대신 모터를 멈추거나, 서보를 돌리거나, 무선 메시지를 보내도 구조는 같다.
3단계: 모터를 멈추는 조건 만들기
이제 LED 대신 모터를 제어해보자. 로봇이 앞으로 가다가 장애물이 가까워지면 정지해야 한다. 단, 거리값이 한 번 튀었다고 바로 급정지하면 움직임이 불안정해질 수 있다. 그래서 이번 완성 코드에서는 여러 번 측정한 값의 평균을 사용한다.
또 하나 생각해야 할 점은 너무 늦게 멈추지 않도록 여유 거리를 잡는 것이다. 로봇은 명령을 받는 순간 바로 멈추지 않는다. 모터와 바퀴에는 관성이 있고, 바닥 상태에 따라 조금 더 밀릴 수 있다. 책상 위에서 실험할 때는 특히 조심해야 한다. 처음에는 바퀴를 공중에 띄운 상태로 테스트하고, 실제 바닥 주행은 낮은 속도부터 시작하자.
이번 장에서는 35cm부터 주의 구간으로 보고, 18cm 이하에서 정지하도록 설정한다. 로봇 속도가 빠르거나 무거우면 정지 거리를 더 크게 잡아야 한다.
완성 코드: 가까우면 멈추고, 더 가까우면 빠르게 울리기
다음 코드는 초음파 센서, 모터 드라이버, LED, 부저를 모두 연결한 완성 예제다. 아두이노 우노 기준 핀 번호를 사용했다.
const int TRIG_PIN = 8;
const int ECHO_PIN = 7;
const int LED_PIN = 4;
const int BUZZER_PIN = 3;
const int STBY_PIN = 2;
const int PWMA_PIN = 5;
const int AIN1_PIN = 9;
const int AIN2_PIN = 10;
const int PWMB_PIN = 6;
const int BIN1_PIN = 11;
const int BIN2_PIN = 12;
const int NORMAL_SPEED = 150;
const int SLOW_SPEED = 90;
const float STOP_DISTANCE_CM = 18.0;
const float CAUTION_DISTANCE_CM = 35.0;
const float MIN_VALID_CM = 2.0;
const float MAX_VALID_CM = 300.0;
const int SAMPLE_COUNT = 5;
unsigned long lastBeepTime = 0;
unsigned long beepStartTime = 0;
bool beepOn = false;
void setup() {
Serial.begin(9600);
pinMode(TRIG_PIN, OUTPUT);
pinMode(ECHO_PIN, INPUT);
pinMode(LED_PIN, OUTPUT);
pinMode(BUZZER_PIN, OUTPUT);
pinMode(STBY_PIN, OUTPUT);
pinMode(PWMA_PIN, OUTPUT);
pinMode(AIN1_PIN, OUTPUT);
pinMode(AIN2_PIN, OUTPUT);
pinMode(PWMB_PIN, OUTPUT);
pinMode(BIN1_PIN, OUTPUT);
pinMode(BIN2_PIN, OUTPUT);
digitalWrite(STBY_PIN, HIGH);
stopMotors();
}
void loop() {
float distance = readDistanceAverageCm();
Serial.print("distance: ");
if (distance < 0) {
Serial.println("invalid");
stopMotors();
digitalWrite(LED_PIN, HIGH);
updateBuzzer(STOP_DISTANCE_CM);
delay(20);
return;
}
Serial.print(distance);
Serial.println(" cm");
if (distance <= STOP_DISTANCE_CM) {
stopMotors();
digitalWrite(LED_PIN, HIGH);
} else if (distance <= CAUTION_DISTANCE_CM) {
driveForward(SLOW_SPEED);
digitalWrite(LED_PIN, (millis() / 150) % 2 == 0);
} else {
driveForward(NORMAL_SPEED);
digitalWrite(LED_PIN, LOW);
}
updateBuzzer(distance);
delay(20);
}
float readDistanceAverageCm() {
float sum = 0.0;
int validCount = 0;
for (int i = 0; i < SAMPLE_COUNT; i++) {
float distance = readDistanceSingleCm();
if (distance >= MIN_VALID_CM && distance <= MAX_VALID_CM) {
sum += distance;
validCount++;
}
delay(8);
}
if (validCount == 0) {
return -1.0;
}
return sum / validCount;
}
float readDistanceSingleCm() {
digitalWrite(TRIG_PIN, LOW);
delayMicroseconds(2);
digitalWrite(TRIG_PIN, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG_PIN, LOW);
unsigned long duration = pulseIn(ECHO_PIN, HIGH, 30000UL);
if (duration == 0) {
return -1.0;
}
float distance = duration * 0.0343 / 2.0;
if (distance < MIN_VALID_CM || distance > MAX_VALID_CM) {
return -1.0;
}
return distance;
}
void driveForward(int speedValue) {
speedValue = constrain(speedValue, 0, 255);
digitalWrite(STBY_PIN, HIGH);
digitalWrite(AIN1_PIN, HIGH);
digitalWrite(AIN2_PIN, LOW);
analogWrite(PWMA_PIN, speedValue);
digitalWrite(BIN1_PIN, HIGH);
digitalWrite(BIN2_PIN, LOW);
analogWrite(PWMB_PIN, speedValue);
}
void stopMotors() {
analogWrite(PWMA_PIN, 0);
analogWrite(PWMB_PIN, 0);
digitalWrite(AIN1_PIN, LOW);
digitalWrite(AIN2_PIN, LOW);
digitalWrite(BIN1_PIN, LOW);
digitalWrite(BIN2_PIN, LOW);
}
void updateBuzzer(float distanceCm) {
if (distanceCm > CAUTION_DISTANCE_CM) {
noTone(BUZZER_PIN);
beepOn = false;
return;
}
float limitedDistance = constrain(distanceCm, STOP_DISTANCE_CM, CAUTION_DISTANCE_CM);
int interval = map(
(long)(limitedDistance * 10),
(long)(STOP_DISTANCE_CM * 10),
(long)(CAUTION_DISTANCE_CM * 10),
120,
650
);
unsigned long now = millis();
if (!beepOn && now - lastBeepTime >= interval) {
tone(BUZZER_PIN, 2000);
beepOn = true;
beepStartTime = now;
lastBeepTime = now;
}
if (beepOn && now - beepStartTime >= 50) {
noTone(BUZZER_PIN);
beepOn = false;
}
}업로드한 뒤에는 바로 바닥에 내려놓지 말고 바퀴가 공중에 뜬 상태에서 먼저 확인하자. 손을 센서 앞에 가져가면 바퀴가 느려지다가 멈추고, LED와 부저가 반응해야 한다. 방향이 반대로 돈다면 해당 모터의 두 선을 서로 바꾸거나, 코드에서 HIGH와 LOW 조합을 바꾸면 된다.
코드가 하는 일을 천천히 뜯어보기
이 코드는 크게 네 부분으로 나눌 수 있다. 첫 번째는 거리 측정이다. readDistanceSingleCm() 함수는 Trig 신호를 만들고 Echo 시간을 읽어 거리로 바꾼다. 두 번째는 평균 처리다. readDistanceAverageCm() 함수는 여러 번 측정한 값 중 유효한 값만 골라 평균을 낸다.
세 번째는 판단이다. loop() 안에서 거리값을 기준으로 세 가지 행동을 선택한다. 멀면 전진하고, 주의 거리 안으로 들어오면 느리게 전진하고, 정지 거리 안으로 들어오면 멈춘다. 네 번째는 출력이다. driveForward(), stopMotors(), updateBuzzer()가 실제 모터와 부저를 움직인다.
여기서 가장 중요한 부분은 조건문이다.
if (distance <= STOP_DISTANCE_CM) {
stopMotors();
} else if (distance <= CAUTION_DISTANCE_CM) {
driveForward(SLOW_SPEED);
} else {
driveForward(NORMAL_SPEED);
}로봇 제어 코드는 복잡해 보일 때도 있지만, 시작은 대부분 이런 구조다. 현재값을 읽고, 기준값과 비교하고, 출력값을 정한다. PID 제어나 위치 제어도 결국 이 구조를 더 정교하게 만든 것이다.
평균 처리와 유효 범위가 필요한 이유
초음파 센서값은 항상 깨끗하게 나오지 않는다. 앞에 물체가 있어도 각도가 맞지 않으면 반사파가 돌아오지 않을 수 있고, 모터 노이즈 때문에 순간적으로 이상한 값이 나올 수도 있다. 아무것도 없는데 1cm가 나오거나, 바로 앞에 손이 있는데 300cm 이상으로 튈 때도 있다.
그래서 코드에서는 두 가지 안전장치를 넣었다.
첫째, 유효 범위를 정했다. 2cm보다 작거나 300cm보다 큰 값은 이번 프로젝트에서 믿지 않기로 했다. 센서의 이론적인 측정 범위가 더 넓더라도, 작은 로봇의 장애물 감지에는 300cm까지 필요하지 않다. 가까운 장애물을 안정적으로 감지하는 것이 더 중요하다.
둘째, 여러 번 측정해서 평균을 냈다. 한 번 측정한 값만 믿으면 순간적인 튐에 민감해진다. 다섯 번 측정한 뒤 유효한 값만 평균내면 움직임이 훨씬 차분해진다.
물론 평균 처리에도 단점은 있다. 측정 횟수가 많을수록 반응이 조금 늦어진다. 빠르게 달리는 로봇이라면 평균 횟수를 줄이거나, 정지 거리를 더 크게 잡아야 한다. 로봇 제어에서는 항상 안정성과 반응 속도 사이의 균형을 잡아야 한다.
부저 간격으로 거리감을 표현하기
이 프로젝트의 재미있는 부분은 부저다. 단순히 가까우면 "삐" 하고 한 번 울리는 것이 아니라, 가까워질수록 경고음 간격이 짧아진다. 자동차 후방 주차 센서와 비슷한 느낌이다.
코드의 updateBuzzer() 함수는 거리값을 경고음 간격으로 바꾼다. 35cm 근처에서는 약 650ms마다 한 번씩 울리고, 18cm 근처에서는 약 120ms마다 빠르게 울린다. 이때 delay()로 부저를 제어하지 않고 millis()를 사용했다.
delay()를 오래 쓰면 보드가 그 시간 동안 다른 일을 하지 못한다. 부저를 울리느라 거리 측정을 멈추면 로봇 반응이 늦어진다. 반면 millis()를 사용하면 시간을 확인하면서 필요한 순간에만 부저를 켜고 끌 수 있다. 초보 단계에서는 조금 낯설 수 있지만, 움직이는 로봇에서는 매우 중요한 습관이다.
테스트 순서
실제 로봇은 단계적으로 테스트해야 한다. 한 번에 모든 부품을 연결하고 "왜 안 되지?"라고 고민하면 원인을 찾기 어렵다.
센서만 연결하고 시리얼 모니터에서 거리값을 확인한다.
LED를 추가하고 가까운 거리에서 켜지는지 확인한다.
부저를 추가하고 거리 변화에 따라 울림 간격이 달라지는지 확인한다.
모터 드라이버를 연결하되, 바퀴를 공중에 띄운 상태로 테스트한다.
낮은 속도로 바닥에서 주행시키고 정지 거리를 조정한다.
배터리 전압이 떨어졌을 때 센서값이나 모터 동작이 불안정해지는지 확인한다.
이 순서를 지키면 문제를 훨씬 빨리 찾을 수 있다. 센서값이 이상한 상태에서 모터를 연결하면 로봇이 갑자기 움직일 수 있다. 모터가 불안정한 상태에서 부저까지 연결하면 전원 문제인지 코드 문제인지 헷갈린다. 로봇 제작에서 좋은 실력은 멋진 코드를 한 번에 쓰는 능력보다, 문제를 작게 나누어 확인하는 습관에서 나온다.
자주 발생하는 문제와 해결 방법
| 증상 | 가능한 원인 | 확인할 것 |
|---|---|---|
거리값이 계속 invalid로 나온다 | Trig/Echo 배선 오류, GND 미연결, 센서 전원 문제 | 핀 번호와 배선, 5V와 GND 확인 |
| 값이 심하게 튄다 | 물체 각도, 반사 재질, 모터 노이즈 | 센서를 정면으로 고정, 평균 횟수 조정, 전원 분리 |
| 모터가 돌 때 보드가 리셋된다 | 모터 전류 때문에 전압이 떨어짐 | 모터 전원 분리, 배터리 용량 확인, GND 공통 연결 |
| 한쪽 바퀴만 돈다 | 모터 배선 또는 드라이버 입력 핀 오류 | A/B 채널 핀, PWM 핀, 모터 단자 확인 |
| 로봇이 장애물에 너무 가까이 가서 멈춘다 | 속도에 비해 정지 거리 부족 | STOP_DISTANCE_CM 값을 키우고 속도를 낮추기 |
| 부저가 소리 나지 않는다 | 부저 극성, 핀 번호, 부저 종류 문제 | +와 - 확인, D3 연결 확인, 능동/수동 부저 확인 |
특히 모터가 움직일 때만 센서값이 이상해진다면 전원 문제일 가능성이 크다. 모터는 켜지는 순간 큰 전류를 가져가고, 그때 보드 전압이 순간적으로 떨어질 수 있다. 이 현상이 생기면 센서값이 튀거나 보드가 재시작된다. 모터 전원과 보드 전원을 분리하고, GND를 공통으로 묶는 기본 원칙을 다시 확인하자.
정지 거리 조정하기
완성 코드를 그대로 사용해도 되지만, 로봇마다 적절한 기준 거리는 다르다. 바퀴 크기, 모터 속도, 배터리 전압, 바닥 마찰이 모두 다르기 때문이다.
처음에는 다음 두 값을 조정해보자.
const float STOP_DISTANCE_CM = 18.0;
const float CAUTION_DISTANCE_CM = 35.0;로봇이 너무 늦게 멈추면 STOP_DISTANCE_CM을 25cm 정도로 키운다. 너무 일찍 멈춘다면 15cm 정도로 줄여도 된다. 다만 빠르게 달리는 로봇에서는 너무 작은 정지 거리를 쓰면 충돌할 수 있다.
주의 구간인 CAUTION_DISTANCE_CM은 "이제 조심해야 한다"는 거리다. 이 값을 크게 잡으면 로봇이 멀리서부터 속도를 줄이고, 작게 잡으면 더 과감하게 접근한다. 처음에는 보수적으로 크게 잡는 편이 안전하다.
더 안정적으로 만들기
이번 예제는 조건문 기반 제어다. 구조가 단순하고 이해하기 쉽다. 하지만 실제 환경에서는 조금 더 보완할 수 있다.
예를 들어, 거리값이 정지 거리 근처에서 17cm, 19cm, 18cm처럼 흔들리면 로봇이 멈췄다 움직였다를 반복할 수 있다. 이를 줄이려면 멈추는 기준과 다시 움직이는 기준을 다르게 잡는 방법이 있다. 예를 들어 18cm 이하에서 멈추고, 다시 25cm 이상으로 멀어졌을 때만 출발하게 만드는 식이다. 이런 방식을 히스테리시스라고 한다. 어려운 말처럼 보이지만, 실제 의미는 "한 번 멈췄으면 조금 더 안전해질 때까지 기다리자"에 가깝다.
또 다른 방법은 중앙값 필터를 쓰는 것이다. 여러 번 측정한 값 중 평균을 내는 대신, 가운데 값을 선택한다. 평균은 큰 이상값 하나에 영향을 받을 수 있지만, 중앙값은 순간적으로 튄 값에 더 강하다. 지금은 평균 처리만으로 충분하지만, 센서값이 계속 튄다면 중앙값 방식도 시도해볼 만하다.
프로젝트 확장 아이디어
이 로봇은 아주 단순하지만 확장하기 좋다. 앞에 고정된 거리 센서 하나만 달아도 멈추는 로봇이 되지만, 센서를 서보 모터 위에 올리면 주변을 스캔할 수 있다. 왼쪽, 가운데, 오른쪽 거리를 차례로 측정하면 어느 쪽이 더 넓은지 판단할 수 있고, 그 방향으로 회전하는 장애물 회피 로봇을 만들 수 있다.
또 LED 하나 대신 RGB LED를 사용하면 거리에 따라 초록, 노랑, 빨강으로 상태를 표시할 수 있다. OLED 디스플레이를 연결하면 현재 거리값을 화면에 표시할 수도 있다. ESP32를 사용한다면 Wi-Fi로 거리값을 웹 페이지에 띄우거나, 스마트폰에서 정지 거리 값을 바꾸는 것도 가능하다.
하지만 확장하기 전에 먼저 지금 만든 기본 구조를 확실히 이해하자. 센서 입력이 들어오고, 기준값과 비교하고, 모터와 부저를 제어한다. 이 단순한 흐름이 앞으로 만들 장애물 회피 자동차, 라인트레이서, 카메라 기반 이동 로봇의 출발점이다.
마무리
이번 장에서는 초음파 거리 센서를 이용해 앞의 물체를 감지하고, 거리 조건에 따라 로봇을 멈추거나 경고음을 울리는 프로젝트를 완성했다. 중요한 것은 센서 자체보다 센서값을 행동으로 연결하는 과정이다.
로봇은 값을 읽기만 해서는 로봇답지 않다. 읽은 값을 바탕으로 판단하고, 그 판단을 모터와 소리와 빛으로 표현할 때 비로소 환경에 반응하는 기계가 된다. 이제 로봇은 앞에 무엇인가 있다는 사실을 알고 멈출 수 있다. 다음 단계에서는 여기서 한 걸음 더 나아가, 멈추는 것에 그치지 않고 스스로 피해서 움직이는 장애물 회피 자동차를 만들어볼 수 있다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.