Skip to main content
Summary

장애물 회피는 로봇의 반사 신경이다

앞 장까지 우리는 목표 속도를 맞추고, 목표 위치까지 이동하며, 오차를 줄이는 제어 방법을 배웠다. 이제 로봇에게 조금 더 살아 있는 느낌을 줄 차례다. 바로 장애물 회피다.

장애물 회피 로봇은 앞에 벽이 나타나면 멈추고, 너무 가까우면 물러나고, 더 넓은 쪽으로 방향을 바꾼다. 말만 들으면 꽤 똑똑해 보이지만, 처음부터 복잡한 인공지능이나 지도가 필요한 것은 아니다. 거리 센서 하나와 몇 가지 규칙만 있어도 로봇은 스스로 주변 상황에 반응할 수 있다.

이 장에서 만들 제어는 거창한 계획형 제어가 아니라 반응형 제어에 가깝다. 반응형 제어란 현재 센서값을 보고 즉시 행동을 정하는 방식이다. 사람으로 치면 뜨거운 냄비를 만졌을 때 손을 재빨리 빼는 반사 신경과 비슷하다. 로봇이 세상을 깊이 이해하지는 못하더라도, 위험한 상황에 빠르게 반응하게 만들 수 있다.

장애물 회피 제어의 기본 흐름은 단순하다. 센서로 앞을 보고, 거리를 판단하고, 모터 명령을 바꾼다. 이 세 단계가 빠르게 반복되면 로봇은 마치 생각하며 움직이는 것처럼 보인다.

장애물 회피 제어

입력은 거리, 출력은 움직임이다

장애물 회피 로봇의 입력은 보통 거리 센서에서 나온다. 초음파 센서, 적외선 거리 센서, ToF 센서 모두 사용할 수 있다. 중요한 것은 센서가 반환하는 값이 "앞에 있는 물체까지 몇 cm인가"라는 형태로 해석될 수 있어야 한다는 점이다.

출력은 모터 명령이다. 바퀴가 두 개 달린 차동 구동 로봇이라면 왼쪽 모터와 오른쪽 모터의 속도를 각각 정하면 된다. 예를 들어 두 바퀴를 같은 속도로 앞으로 돌리면 전진하고, 왼쪽 바퀴는 앞으로 오른쪽 바퀴는 뒤로 돌리면 제자리에서 회전한다.

장애물 회피에서 자주 쓰는 동작은 다음과 같다.

  • 전진: 장애물이 충분히 멀 때 기본으로 수행하는 동작

  • 정지: 장애물이 가까워졌을 때 가장 먼저 해야 하는 안전 동작

  • 후진: 이미 너무 가까워졌을 때 공간을 확보하는 동작

  • 회전: 막힌 방향을 피하고 새로운 방향을 찾는 동작

여기서 핵심은 "센서값 하나를 모터 명령으로 바꾸는 규칙"을 만드는 것이다. 복잡한 수식보다 먼저 필요한 것은 안전한 판단 순서다. 로봇은 빠르게 달리는 것보다, 부딪히지 않고 예측 가능하게 움직이는 것이 먼저다.

가장 단순한 회피 규칙 만들기

가장 쉬운 규칙은 전방 거리 하나만 사용하는 방식이다. 로봇 앞에 거리 센서가 고정되어 있다고 생각해 보자. 전방 거리가 임계값보다 크면 전진하고, 임계값보다 작으면 정지한 뒤 후진하고 회전한다.

예를 들어 임계값을 20cm로 정할 수 있다. 전방 거리가 20cm 이상이면 계속 전진한다. 20cm보다 작아지면 장애물이 가까이 있다고 판단한다. 이때 바로 방향을 틀기보다 잠깐 멈춘 뒤, 조금 후진하고, 일정 시간 회전한 다음 다시 전진하면 된다.

아두이노나 ESP32 코드로 표현하면 다음과 같은 구조가 된다.

const float OBSTACLE_CM = 20.0;

void loop() {
  float front = readDistanceCm();

  if (front > 0 && front < OBSTACLE_CM) {
    stopMotors();
    delay(100);

    moveBackward(120);
    delay(300);

    turnRight(130);
    delay(450);

    stopMotors();
    delay(100);
  } else {
    moveForward(120);
  }
}

여기서 readDistanceCm()는 거리 센서값을 cm 단위로 읽어 오는 함수다. moveForward(120)은 두 모터를 PWM 120 정도로 앞으로 돌리는 함수라고 생각하면 된다. turnRight(130)은 왼쪽 바퀴와 오른쪽 바퀴에 서로 다른 방향 또는 다른 속도를 주어 오른쪽으로 회전시키는 함수다.

이 코드는 단순하지만 꽤 많은 것을 해낸다. 로봇은 평소에는 전진하다가 장애물을 만나면 멈추고, 뒤로 물러나고, 방향을 바꾼다. 처음 만드는 장애물 회피 로봇이라면 이 정도 규칙만으로도 충분히 재미있는 움직임을 볼 수 있다.

다만 이 방식에는 문제가 있다. 항상 같은 방향으로만 회전하기 때문에, 로봇이 어떤 상황에서는 같은 곳을 빙빙 돌 수 있다. 오른쪽이 벽인데도 계속 오른쪽으로 돌려고 하면 탈출이 어렵다. 그래서 다음 단계에서는 왼쪽과 오른쪽 중 어느 쪽이 더 나은지 판단하게 만들어야 한다.

임계값은 로봇의 성격을 결정한다

장애물 회피 제어에서 임계값은 매우 중요하다. 임계값을 너무 작게 잡으면 로봇이 장애물 바로 앞까지 돌진하다가 멈추지 못할 수 있다. 반대로 너무 크게 잡으면 장애물이 멀리 있어도 겁먹은 것처럼 자주 멈춘다.

로봇의 속도가 빠를수록 임계값은 커져야 한다. 모터에 정지 명령을 내려도 바퀴와 차체에는 관성이 있어서 즉시 멈추지 않는다. 바닥이 미끄럽거나 로봇이 무거우면 더 멀리 밀릴 수 있다. 따라서 장애물 회피 임계값은 센서의 측정 거리만 보고 정하는 것이 아니라, 로봇의 실제 정지 거리까지 고려해야 한다.

처음에는 다음처럼 세 구간으로 생각하면 좋다.

  • 안전 구간: 충분히 멀기 때문에 정상 속도로 전진한다.

  • 주의 구간: 장애물이 가까워지고 있으므로 속도를 낮춘다.

  • 위험 구간: 더 가면 충돌할 수 있으므로 정지, 후진, 회전을 수행한다.

예를 들어 전방 거리가 40cm보다 크면 정상 속도로 전진하고, 20~40cm 사이면 느리게 전진하고, 20cm보다 작으면 회피 동작을 수행할 수 있다.

const float SLOW_CM = 40.0;
const float STOP_CM = 20.0;

void loop() {
  float front = readDistanceCm();

  if (front > 0 && front < STOP_CM) {
    stopMotors();
    delay(100);
    moveBackward(120);
    delay(300);
    turnRight(130);
    delay(450);
  } else if (front > 0 && front < SLOW_CM) {
    moveForward(80);
  } else {
    moveForward(140);
  }
}

이렇게 하면 로봇이 장애물을 향해 갑자기 달려들지 않는다. 가까워질수록 먼저 속도를 줄이고, 정말 위험한 거리에서만 회피 행동을 한다. 단순한 규칙이지만 로봇의 움직임이 훨씬 자연스러워진다.

왼쪽과 오른쪽 중 더 넓은 곳 고르기

앞만 보는 로봇은 막힌 것을 알 수는 있지만 어디로 가야 할지는 잘 모른다. 더 나은 회피를 하려면 왼쪽과 오른쪽 거리도 비교해야 한다. 방법은 크게 두 가지다.

첫 번째 방법은 거리 센서를 여러 개 다는 것이다. 전방, 왼쪽, 오른쪽에 각각 센서를 달면 로봇은 동시에 여러 방향을 볼 수 있다. 구조는 단순하지만 센서 수가 늘어나고 배선도 복잡해진다.

두 번째 방법은 거리 센서 하나를 서보 모터 위에 올리는 것이다. 평소에는 센서가 정면을 보다가, 장애물을 만나면 서보가 왼쪽과 오른쪽으로 센서를 돌려 거리를 측정한다. 센서 하나로 여러 방향을 볼 수 있어 작은 로봇에 잘 어울린다. 대신 측정하는 동안 시간이 조금 걸리고, 로봇이 잠깐 멈춰 있어야 안정적이다.

서보를 사용하는 회피 흐름은 다음과 같다.

  • 정면 거리가 위험 임계값보다 작으면 일단 정지한다.

  • 센서를 왼쪽으로 돌려 왼쪽 거리를 측정한다.

  • 센서를 오른쪽으로 돌려 오른쪽 거리를 측정한다.

  • 더 넓은 쪽으로 회전한 뒤 다시 전진한다.

코드 구조는 다음처럼 만들 수 있다.

const float STOP_CM = 20.0;

void loop() {
  lookCenter();
  delay(80);
  float front = readDistanceCm();

  if (front > 0 && front < STOP_CM) {
    stopMotors();
    delay(100);

    moveBackward(120);
    delay(250);
    stopMotors();
    delay(100);

    lookLeft();
    delay(250);
    float left = readDistanceCm();

    lookRight();
    delay(250);
    float right = readDistanceCm();

    lookCenter();
    delay(100);

    if (left > right) {
      turnLeft(130);
      delay(450);
    } else {
      turnRight(130);
      delay(450);
    }

    stopMotors();
    delay(100);
  } else {
    moveForward(120);
  }
}

이제 로봇은 막힌 상황에서 무작정 한쪽으로 돌지 않는다. 왼쪽이 더 넓으면 왼쪽으로, 오른쪽이 더 넓으면 오른쪽으로 간다. 간단한 비교 하나가 로봇을 훨씬 영리하게 만든다.

다만 여기서도 주의할 점이 있다. 센서를 회전시킨 직후에는 값이 안정되기까지 짧은 시간이 필요할 수 있다. 서보가 아직 움직이는 중인데 거리값을 읽으면 엉뚱한 방향의 값을 가져올 수 있다. 그래서 delay(250)처럼 센서가 방향을 잡고 안정될 시간을 조금 주는 것이다. 실제 프로젝트에서는 이 시간을 줄이거나, millis()를 사용해 다른 작업과 함께 처리할 수도 있다.

센서값은 생각보다 자주 튄다

장애물 회피 로봇을 처음 만들면 이런 장면을 자주 보게 된다. 앞에는 아무것도 없는데 로봇이 갑자기 멈춘다. 벽이 가까운데도 한 번씩 멀다고 판단한다. 테이블 다리처럼 얇은 물체는 잘 보다가도 놓친다.

이것은 코드가 꼭 틀렸다는 뜻은 아니다. 거리 센서값은 환경에 따라 흔들릴 수 있다. 초음파 센서는 소리가 반사되어 돌아오는 시간을 재는데, 물체의 표면이 기울어져 있거나 부드러운 재질이면 반사가 약해질 수 있다. 적외선 센서는 주변 빛이나 물체 색의 영향을 받을 수 있다. ToF 센서도 측정 범위와 반사율의 영향을 받는다.

예를 들어 실제 거리는 80cm인데 센서값이 다음처럼 들어왔다고 해 보자.

79, 81, 80, 12, 82, 78, 80

여기서 12는 순간적으로 튄 값일 가능성이 크다. 그런데 코드가 이 값을 그대로 믿으면 로봇은 장애물이 갑자기 나타났다고 판단해 멈추거나 후진한다. 이런 반응이 반복되면 로봇이 덜컹거리며 불안정하게 움직인다.

그래서 장애물 회피에서는 센서값을 그대로 쓰기보다 약간 다듬어서 사용하는 것이 좋다. 이것을 필터링이라고 한다. 필터링은 복잡한 기술처럼 들리지만, 처음에는 최근 몇 번의 측정값을 평균 내거나, 가운데 값을 고르는 정도로도 충분히 효과가 있다.

이동 평균으로 부드럽게 만들기

가장 쉬운 필터는 이동 평균이다. 최근 측정값 여러 개를 저장해 두고 그 평균을 사용한다. 예를 들어 최근 5번의 거리값이 48, 51, 49, 50, 52라면 평균은 50cm다. 한 번 정도 값이 튀어도 평균은 크게 흔들리지 않는다.

float readFilteredDistanceCm() {
  const int N = 5;
  float sum = 0;
  int count = 0;

  for (int i = 0; i < N; i++) {
    float d = readDistanceCm();

    if (d > 2 && d < 300) {   // 센서의 유효 범위 안에 있는 값만 사용
      sum += d;
      count++;
    }

    delay(10);
  }

  if (count == 0) {
    return -1;                // 유효한 측정값이 없다는 뜻
  }

  return sum / count;
}

이 함수는 거리값을 5번 읽고 평균을 낸다. d > 2 && d < 300 조건은 너무 작거나 너무 큰 이상값을 버리기 위한 예시다. 센서마다 유효 범위가 다르므로 실제 사용하는 센서에 맞게 조정해야 한다.

이동 평균의 장점은 구현이 쉽고 값이 부드러워진다는 점이다. 단점은 반응이 조금 느려진다는 것이다. 로봇이 빠르게 움직이는데 평균을 너무 많이 내면 장애물에 늦게 반응할 수 있다. 그래서 처음에는 3~5개 정도의 작은 개수로 시작하는 것이 좋다.

가운데 값을 고르는 방법

센서값이 가끔 한 번씩 크게 튄다면 평균보다 중앙값 필터가 더 잘 맞을 때도 있다. 중앙값 필터는 여러 측정값을 크기순으로 정렬한 뒤 가운데 값을 사용한다.

예를 들어 측정값이 79, 81, 80, 12, 82라면 정렬하면 12, 79, 80, 81, 82가 된다. 이때 가운데 값은 80이다. 순간적으로 튄 12가 있어도 최종 값에는 큰 영향을 주지 않는다.

아주 작은 배열에서는 코드도 어렵지 않다.

float readMedianDistanceCm() {
  const int N = 5;
  float v[N];

  for (int i = 0; i < N; i++) {
    v[i] = readDistanceCm();
    delay(10);
  }

  for (int i = 0; i < N - 1; i++) {
    for (int j = i + 1; j < N; j++) {
      if (v[j] < v[i]) {
        float temp = v[i];
        v[i] = v[j];
        v[j] = temp;
      }
    }
  }

  return v[N / 2];
}

초보 단계에서는 이동 평균과 중앙값 필터 중 하나만 적용해도 로봇의 움직임이 훨씬 안정된다. 장애물 회피 로봇이 제멋대로 멈춘다면, 먼저 센서값을 시리얼 모니터에 출력해 보자. 숫자가 얼마나 흔들리는지 보면 필터가 필요한 이유가 바로 보인다.

히스테리시스: 들어갈 때와 나올 때의 기준을 다르게 한다

필터링을 적용해도 로봇이 임계값 근처에서 덜덜 떨 때가 있다. 예를 들어 장애물 회피 기준을 20cm로 정했다고 하자. 센서값이 19.8cm, 20.1cm, 19.9cm, 20.2cm처럼 오가면 로봇은 매번 판단을 바꾼다.

19.8cm에서는 "장애물이다!"라고 판단해 멈추고, 20.1cm에서는 "괜찮다!"라고 판단해 전진한다. 이 동작이 빠르게 반복되면 모터가 덜컥거리거나 로봇이 앞뒤로 떨 수 있다.

이 문제를 줄이는 간단한 방법이 히스테리시스다. 히스테리시스는 어떤 상태에 들어가는 기준과 빠져나오는 기준을 다르게 두는 방법이다.

예를 들어 이렇게 정할 수 있다.

  • 전진 상태에서 20cm보다 가까워지면 회피 상태로 들어간다.

  • 회피 상태에서는 28cm보다 멀어져야 다시 전진 상태로 돌아간다.

  • 20~28cm 사이에서는 현재 상태를 유지한다.

이렇게 하면 센서값이 20cm 근처에서 조금 흔들려도 로봇이 계속 판단을 바꾸지 않는다. 회피 상태에 들어갔다면 충분히 안전한 거리인 28cm 이상이 될 때까지 조심스럽게 행동한다.

코드로는 상태 변수를 사용하면 이해하기 쉽다.

const float ENTER_AVOID_CM = 20.0;
const float EXIT_AVOID_CM  = 28.0;

bool avoiding = false;

void loop() {
  float front = readFilteredDistanceCm();

  if (front < 0) {
    stopMotors();             // 센서값을 믿을 수 없으면 안전하게 정지
    return;
  }

  if (!avoiding && front < ENTER_AVOID_CM) {
    avoiding = true;
  }

  if (avoiding && front > EXIT_AVOID_CM) {
    avoiding = false;
  }

  if (avoiding) {
    stopMotors();
    delay(100);
    moveBackward(100);
    delay(250);
    turnRight(120);
    delay(350);
  } else {
    moveForward(120);
  }
}

이 코드는 매우 단순하지만, 임계값 하나만 사용할 때보다 훨씬 안정적이다. 특히 초음파 센서처럼 값이 조금씩 흔들리는 센서를 사용할 때 효과가 크다.

상태로 생각하면 코드가 깔끔해진다

장애물 회피 코드가 길어지면 if 문이 점점 복잡해진다. 전진 중인지, 후진 중인지, 회전 중인지, 센서를 왼쪽으로 보고 있는지, 오른쪽으로 보고 있는지 한꺼번에 처리하려고 하면 코드가 금방 엉킨다.

이럴 때는 로봇의 행동을 상태로 나누면 좋다. 상태란 로봇이 지금 무엇을 하고 있는지를 나타내는 이름이다. 예를 들어 FORWARD, BACKWARD, TURN_LEFT, TURN_RIGHT, STOP 같은 상태를 만들 수 있다.

상태 기반 코드는 로봇의 행동을 단계별로 읽을 수 있게 해 준다. "지금은 전진 상태다. 장애물이 가까우면 후진 상태로 바꾼다. 후진 시간이 끝나면 회전 상태로 바꾼다. 회전이 끝나면 다시 전진 상태로 돌아간다." 이런 식으로 흐름이 분명해진다.

간단한 상태 머신은 다음과 같은 모양이다.

enum RobotState {
  FORWARD,
  BACKWARD,
  TURNING
};

RobotState state = FORWARD;
unsigned long stateStartMs = 0;

const float STOP_CM = 20.0;

void changeState(RobotState next) {
  state = next;
  stateStartMs = millis();
}

void loop() {
  float front = readFilteredDistanceCm();
  unsigned long now = millis();

  if (state == FORWARD) {
    if (front > 0 && front < STOP_CM) {
      stopMotors();
      changeState(BACKWARD);
    } else {
      moveForward(120);
    }
  }

  else if (state == BACKWARD) {
    moveBackward(110);

    if (now - stateStartMs > 300) {
      changeState(TURNING);
    }
  }

  else if (state == TURNING) {
    turnRight(120);

    if (now - stateStartMs > 450) {
      changeState(FORWARD);
    }
  }
}

여기서는 delay() 대신 millis()를 사용했다. delay()를 쓰면 그 시간 동안 보드가 다른 일을 하지 못한다. 반면 millis()를 사용하면 시간이 지났는지 확인하면서 센서 읽기, 통신 처리, LED 표시 같은 다른 작업도 함께 할 수 있다. 처음 실습에서는 delay()가 이해하기 쉽지만, 로봇이 조금 복잡해지면 상태 머신과 millis() 방식이 더 안전하고 유연하다.

회피 동작은 속도 제어와 함께 생각한다

장애물 회피는 단순히 "갈까, 말까"의 문제가 아니다. 얼마나 빠르게 갈지도 중요하다. 전방 거리가 100cm일 때와 25cm일 때 같은 속도로 달리면 로봇은 너무 거칠게 움직인다. 앞에서 배운 모터 속도 제어를 여기서 다시 활용할 수 있다.

가장 단순한 방법은 거리에 따라 전진 속도를 단계적으로 바꾸는 것이다. 멀면 빠르게, 가까우면 느리게, 너무 가까우면 정지한다.

조금 더 부드럽게 만들고 싶다면 거리값을 PWM으로 변환할 수도 있다. 예를 들어 20cm에서는 PWM 0, 60cm 이상에서는 PWM 150이 되도록 만들면 장애물에 가까워질수록 자연스럽게 감속한다.

int speedFromDistance(float d) {
  if (d < 20) return 0;
  if (d > 60) return 150;

  return map((int)d, 20, 60, 60, 150);
}

void loop() {
  float front = readFilteredDistanceCm();

  if (front < 0) {
    stopMotors();
    return;
  }

  if (front < 20) {
    stopMotors();
    delay(100);
    moveBackward(100);
    delay(250);
    turnRight(120);
    delay(400);
  } else {
    int pwm = speedFromDistance(front);
    moveForward(pwm);
  }
}

이 방식은 로봇의 움직임을 훨씬 덜 공격적으로 만든다. 장애물 앞에서 부드럽게 속도를 줄이기 때문에 충돌 가능성이 줄고, 센서값이 조금 흔들려도 반응이 덜 거칠다.

물론 map()만으로 완벽한 제어가 되는 것은 아니다. 좌우 모터의 실제 속도 차이는 여전히 남을 수 있다. 더 안정적인 직진이 필요하다면 엔코더를 사용해 좌우 속도를 맞추고, 장애물 회피 명령은 목표 속도만 바꾸는 구조로 만들 수 있다. 즉, 상위 규칙은 "앞이 가까우니 천천히 가라"고 말하고, 하위 속도 제어는 "왼쪽과 오른쪽 바퀴가 실제로 같은 속도로 돌게 하라"고 처리하는 것이다.

회피 규칙을 조금 더 똑똑하게 만들기

기본 회피 규칙은 정지, 후진, 회전만으로도 동작한다. 하지만 몇 가지 조건을 추가하면 로봇이 훨씬 자연스럽게 움직인다.

예를 들어 왼쪽과 오른쪽 거리를 비교할 때 단순히 큰 쪽을 고르는 대신, 둘 다 너무 좁으면 더 오래 후진하게 만들 수 있다. 왼쪽이 18cm, 오른쪽이 16cm라면 오른쪽보다 왼쪽이 넓기는 하지만 둘 다 안전하지 않다. 이런 상황에서는 바로 회전하기보다 뒤로 물러나 공간을 확보하는 것이 낫다.

또 같은 방향으로 계속 회전하는 것을 피하기 위해 최근 회전 방향을 기억할 수도 있다. 방 안의 구석이나 의자 다리 사이에 들어가면 로봇이 한 방향으로만 돌다가 갇힐 수 있다. 이때 이전에 오른쪽으로 돌았다면 다음에는 왼쪽도 시도하게 만들면 탈출 가능성이 올라간다.

실습에서는 다음과 같은 규칙을 추가해 볼 수 있다.

  • 양쪽 거리가 모두 좁으면 회전하지 말고 먼저 더 길게 후진한다.

  • 왼쪽과 오른쪽 거리 차이가 작으면 이전과 반대 방향으로 회전한다.

  • 정면 거리가 가까워지는 속도가 빠르면 평소보다 일찍 감속한다.

  • 센서값이 유효하지 않으면 전진하지 말고 정지하거나 천천히 회전 탐색한다.

이 규칙들은 모두 인공지능이 아니다. 사람이 직접 정한 조건문이다. 하지만 실제 로봇에서는 이런 작은 규칙들이 쌓여 꽤 그럴듯한 행동을 만든다. 로봇 제어에서 중요한 것은 처음부터 완벽한 알고리즘을 만드는 것이 아니라, 현재 문제를 관찰하고 규칙을 조금씩 개선하는 태도다.

센서 장착 위치도 제어의 일부다

장애물 회피 알고리즘을 아무리 잘 만들어도 센서가 엉뚱한 곳을 보고 있으면 로봇은 제대로 움직이지 못한다. 센서는 로봇의 눈이다. 눈이 바닥만 보고 있거나 너무 높이 달려 있으면 의자 다리, 낮은 상자, 벽의 모서리를 놓칠 수 있다.

전방 거리 센서는 보통 로봇의 진행 방향을 향해 단단히 고정한다. 바퀴가 움직일 때 센서가 흔들리면 측정값도 흔들린다. 서보 위에 센서를 올릴 때도 센서 브래킷이 느슨하면 방향 측정이 부정확해진다.

센서의 높이도 중요하다. 너무 낮으면 바닥의 작은 요철을 장애물로 오해할 수 있고, 너무 높으면 낮은 물체를 보지 못한다. 작은 바퀴 로봇이라면 로봇 전면 중앙, 바닥에서 몇 cm 정도 띄운 위치에 장착한 뒤 실제 장애물을 놓고 테스트하면서 조정하는 것이 좋다.

또 하나 잊지 말아야 할 것은 로봇의 폭이다. 센서 하나가 정면 중앙만 보고 있으면 로봇 몸체의 왼쪽이나 오른쪽 끝이 장애물에 걸릴 수 있다. 로봇이 넓다면 전방 센서 하나만으로는 부족할 수 있다. 이때는 좌우 보조 센서를 추가하거나, 회전하기 전에 여유 공간을 더 크게 잡아야 한다.

규칙 기반 회피의 한계

규칙 기반 장애물 회피는 강력한 출발점이다. 만들기 쉽고, 디버깅하기 쉽고, 작은 로봇에서는 충분히 재미있는 결과를 보여 준다. 하지만 한계도 분명하다.

가장 큰 한계는 로봇이 전체 상황을 기억하지 못한다는 점이다. 반응형 로봇은 지금 센서가 보는 것에만 반응한다. 그래서 U자 모양의 막힌 공간에 들어가면 한참 동안 헤맬 수 있다. 앞이 막히면 돌아서고, 다시 전진하다가 또 막히고, 다시 돌아서는 행동을 반복할 수 있다.

두 번째 한계는 목표 지점이 없다는 것이다. 단순 장애물 회피 로봇은 "부딪히지 않고 움직이기"는 할 수 있지만, "저 문까지 가기"나 "복도를 따라 목적지까지 가기"는 어렵다. 목적지가 있으려면 현재 위치, 목표 위치, 이동 경로를 함께 생각해야 한다.

세 번째 한계는 센서 시야 밖의 장애물이다. 거리 센서가 정면만 본다면 측면에서 튀어나온 물체나 바닥의 낮은 장애물을 놓칠 수 있다. 카메라, 라이다, 여러 개의 ToF 센서, 범퍼 스위치 등을 함께 사용하면 이 문제를 줄일 수 있지만, 시스템은 더 복잡해진다.

이 한계를 넘어서려면 다음 단계의 기술로 확장해야 한다.

  • 지도 작성: 로봇이 지나온 공간과 장애물 위치를 저장한다.

  • 경로 계획: 현재 위치에서 목표 위치까지 갈 수 있는 경로를 계산한다.

  • 위치 추정: 엔코더, IMU, 카메라 등을 이용해 로봇이 어디에 있는지 추정한다.

  • ROS2 활용: 센서, 모터, 지도, 경로 계획을 여러 노드로 나누어 관리한다.

여기서 중요한 점은 규칙 기반 회피가 쓸모없어지는 것이 아니라는 점이다. 더 큰 로봇 시스템에서도 가까운 장애물에 즉시 반응하는 안전 계층은 여전히 필요하다. 경로 계획이 아무리 멋진 길을 계산해도, 갑자기 앞에 사람이 나타나면 로봇은 즉시 멈춰야 한다. 단순한 회피 규칙은 더 복잡한 시스템 안에서도 마지막 안전장치로 살아남는다.

작은 실험으로 감각 익히기

장애물 회피는 책상 위 코드만으로 완성되지 않는다. 실제 로봇을 바닥에 놓고 움직여 봐야 한다. 같은 코드라도 바닥 재질, 배터리 상태, 모터 힘, 센서 위치에 따라 전혀 다르게 보일 수 있다.

처음 실험할 때는 넓은 바닥에 상자 하나를 놓고 시작하자. 로봇이 상자를 향해 전진하다가 몇 cm 앞에서 멈추는지 확인한다. 그다음 임계값을 조금씩 바꿔 본다. 15cm, 20cm, 30cm로 바꾸면 로봇의 성격이 달라진다. 15cm는 대담하지만 위험하고, 30cm는 조심스럽지만 답답할 수 있다.

다음에는 상자를 두 개 놓고 좁은 통로를 만들어 보자. 로봇이 통로를 지나갈 수 있는지, 벽에 너무 가까워지면 어떻게 반응하는지 관찰한다. 센서가 중앙만 보기 때문에 몸체 옆면이 걸린다면, 임계값을 키우거나 좌우 센서를 추가해야 한다.

마지막으로 시리얼 모니터에 거리값과 상태를 함께 출력해 보자.

Serial.print("front=");
Serial.print(front);
Serial.print(" state=");
Serial.println(state);

숫자를 보면 로봇의 행동을 이해하기 쉬워진다. 갑자기 멈춘 순간에 거리값이 튀었는지, 회피 상태에서 빠져나오지 못하는지, 임계값이 너무 큰지 판단할 수 있다. 로봇 디버깅의 기본은 "느낌"이 아니라 "관찰 가능한 값"을 보는 것이다.

반응하는 로봇에서 판단하는 로봇으로

이 장에서 만든 장애물 회피 제어는 단순하다. 전방 거리가 가까우면 멈추고, 후진하고, 회전한다. 왼쪽과 오른쪽을 비교하면 더 넓은 쪽을 선택할 수 있다. 센서값이 튀면 필터링으로 부드럽게 만들고, 임계값 근처에서 흔들리면 히스테리시스로 안정시킨다.

이 정도만 구현해도 로봇은 더 이상 무작정 앞으로 달리는 장난감이 아니다. 주변을 보고, 위험을 감지하고, 자신의 행동을 바꾸는 작은 기계가 된다. 복잡한 AI 없이도 센서와 규칙만으로 충분히 "스스로 움직이는 느낌"을 만들 수 있다.

물론 이것은 끝이 아니라 출발점이다. 다음 단계에서는 이 원리를 실제 프로젝트에 적용할 수 있다. 서보 모터를 제어해 센서를 움직이고, 거리 측정 로봇을 만들고, 장애물 회피 자동차를 조립하면서 지금 배운 규칙을 현실의 하드웨어 위에서 시험하게 될 것이다. 로봇은 코드를 믿지 않는다. 로봇은 바닥 위에서 움직이며, 센서값과 모터 반응으로 우리에게 답을 준다. 이제 그 답을 읽을 준비가 되었다.

책 목록

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

전체 목차 보기

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

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

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

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

참여하기