Skip to main content
Summary

로봇의 판단은 생각보다 단순하다

우리는 로봇이 스스로 움직이면 "생각한다"고 말하곤 합니다. 하지만 처음 만드는 로봇은 사람처럼 깊이 생각하지 않습니다. 대부분은 센서값을 읽고, 조건을 비교하고, 정해 둔 행동 중 하나를 고르는 방식으로 움직입니다.

예를 들어 바퀴 로봇 앞에 장애물이 있다고 해 봅시다. 로봇이 해야 할 판단은 복잡하지 않습니다.

  • 앞의 거리가 20cm보다 멀다 → 앞으로 간다.

  • 앞의 거리가 20cm보다 가깝다 → 멈춘다.

  • 멈춘 뒤 잠깐 뒤로 간다.

  • 방향을 틀고 다시 거리를 확인한다.

이 흐름은 앞에서 배운 입력-처리-출력 구조와 같습니다.

  • 입력: 초음파 센서가 거리를 읽는다.

  • 처리: 아두이노가 거리값을 조건문으로 판단한다.

  • 출력: 모터가 전진, 정지, 회전 중 하나를 실행한다.

이번 장에서는 이 구조를 코드로 정리해 보겠습니다. 핵심은 "로봇에게 모든 상황을 한 번에 해결하게 만드는 것"이 아니라, 작은 행동을 상태로 나누고 순서대로 연결하는 것입니다.

로봇의 판단 구조 만들기AI 생성 이미지

감지-판단-동작 흐름 만들기

가장 기본적인 판단 코드는 if 조건문으로 만들 수 있습니다. 초음파 센서가 읽은 거리가 충분히 멀면 앞으로 가고, 가까우면 멈추는 구조입니다.

const int TRIG_PIN = 2;
const int ECHO_PIN = 3;

const int SAFE_DISTANCE = 20; // cm

void loop() {
  int distance = readDistanceCm();

  if (distance > SAFE_DISTANCE) {
    forward(180);
  } else {
    stopMotors();
  }
}

여기서 SAFE_DISTANCE는 안전거리입니다. 로봇이 장애물과 너무 가까워지기 전에 멈추도록 기준값을 정한 것입니다. 숫자 20은 절대적인 정답이 아닙니다. 로봇 속도, 바퀴 크기, 바닥 상태에 따라 15cm가 적당할 수도 있고 30cm가 필요할 수도 있습니다.

거리 읽기 함수는 다음처럼 만들 수 있습니다.

int 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; // 측정 실패 시 아주 먼 거리로 처리
  }

  int distance = duration * 0.0343 / 2;
  return distance;
}

초음파 센서는 소리를 내보내고 되돌아오는 시간을 이용해 거리를 계산합니다. 소리가 물체까지 갔다가 다시 돌아오는 왕복 시간이므로 마지막에 2로 나눕니다. pulseIn()의 세 번째 값 30000은 신호를 너무 오래 기다리지 않도록 정한 제한 시간입니다.

이 예제에서는 측정에 실패했을 때 999cm처럼 아주 먼 거리로 처리했습니다. 실습 중 값이 가끔 튀어도 로봇이 멈춘 채 굳어 버리지 않게 하려는 단순한 처리입니다. 다만 실제 주행에서 거리값이 계속 999로 나온다면 센서 연결을 반드시 확인해야 합니다.

이제 로봇은 "가까우면 멈춘다"는 간단한 판단을 할 수 있습니다. 하지만 실제 로봇이라면 멈춘 뒤 다시 움직일 방법도 필요합니다.

상태 변수로 로봇의 현재 모드 표현하기

로봇이 할 수 있는 행동이 많아지면 단순한 if문만으로는 코드가 금방 복잡해집니다. 이때 필요한 것이 상태 변수입니다.

상태 변수는 로봇이 지금 어떤 모드에 있는지 기억하는 변수입니다. 사람으로 비유하면 "지금 걷는 중인지, 멈춘 상태인지, 방향을 바꾸는 중인지"를 표시하는 이름표와 같습니다.

int robotState = 0;

const int STATE_STOP = 0;
const int STATE_FORWARD = 1;
const int STATE_AVOID = 2;
const int STATE_SEARCH = 3;

이렇게 숫자로 상태를 표현할 수 있습니다. 하지만 숫자만 보면 의미를 알아보기 어렵습니다. 그래서 이름을 붙여 사용합니다.

  • STATE_STOP: 정지

  • STATE_FORWARD: 전진

  • STATE_AVOID: 회피

  • STATE_SEARCH: 탐색

상태를 사용하면 코드를 다음처럼 읽을 수 있습니다.

if (robotState == STATE_FORWARD) {
  forward(180);
}

if (robotState == STATE_STOP) {
  stopMotors();
}

이 방식의 장점은 로봇의 행동을 한곳에서 정리할 수 있다는 점입니다. 센서값은 상태를 바꾸고, 상태는 모터 행동을 결정합니다.

int distance = readDistanceCm();

if (distance <= SAFE_DISTANCE) {
  robotState = STATE_AVOID;
} else {
  robotState = STATE_FORWARD;
}

즉, 센서가 직접 모터를 움직이는 것이 아닙니다. 센서는 현재 상황을 알려 주고, 아두이노는 상태를 바꾸며, 모터는 그 상태에 맞게 움직입니다. 이 구분이 로봇 코드를 깔끔하게 만드는 첫걸음입니다.

delay에만 의존하면 생기는 문제

초보자는 시간 조절을 할 때 delay()를 자주 사용합니다.

stopMotors();
delay(300);
backward(150);
delay(500);
turnRight(180);
delay(600);

이 코드는 이해하기 쉽습니다. 0.3초 멈추고, 0.5초 뒤로 가고, 0.6초 오른쪽으로 돕니다. 하지만 문제가 있습니다. delay()가 실행되는 동안 아두이노는 다른 일을 거의 하지 못합니다.

로봇으로 비유하면 눈을 감고 시간을 세는 것과 비슷합니다. 뒤로 가는 0.5초 동안 새로운 장애물을 확인하지 못합니다. 오른쪽으로 도는 0.6초 동안 바닥의 선이나 버튼 입력도 놓칠 수 있습니다.

작은 실습에서는 delay()가 편합니다. 하지만 여러 센서와 모터를 함께 쓰는 로봇에서는 delay()가 길어질수록 반응이 둔해집니다. 그래서 로봇의 판단 구조에서는 millis()를 자주 사용합니다.

millis()는 아두이노가 켜진 뒤 흐른 시간을 밀리초 단위로 알려 주는 함수입니다. 1초는 1000밀리초입니다.

unsigned long now = millis();

delay()가 "그 자리에서 기다리기"라면, millis()는 "시계를 보면서 시간이 지났는지 확인하기"입니다. 시계를 확인하는 동안에도 센서값을 읽고 조건을 판단할 수 있습니다.

millis로 시간 흐름 제어하기

millis()를 사용할 때는 보통 시작 시간을 저장해 둡니다.

unsigned long stateStartTime = 0;

상태가 바뀌는 순간 현재 시간을 저장합니다.

robotState = STATE_AVOID;
stateStartTime = millis();

그리고 loop()가 반복될 때마다 얼마나 시간이 지났는지 확인합니다.

if (millis() - stateStartTime > 500) {
  robotState = STATE_SEARCH;
  stateStartTime = millis();
}

이 코드는 "현재 상태가 시작된 지 0.5초가 지나면 다음 상태로 넘어간다"는 뜻입니다. 기다리는 동안에도 loop()는 계속 돌기 때문에 센서 확인이나 시리얼 출력 같은 다른 작업을 넣을 수 있습니다.

중요한 점은 millis() 값을 저장하는 변수는 unsigned long 자료형을 사용한다는 것입니다. millis()는 시간이 계속 커지는 값이므로 int에 담기에는 적합하지 않습니다.

간단한 상태 머신 만들기

상태 머신은 이름이 어렵지만 뜻은 단순합니다. 정해진 상태들이 있고, 조건에 따라 다음 상태로 이동하는 구조입니다. 자동문을 떠올리면 쉽습니다. 닫힘, 열림, 대기 같은 상태가 있고, 사람이 감지되면 열림 상태로 바뀝니다.

장애물을 피하는 바퀴 로봇의 기본 상태를 이렇게 정해 보겠습니다.

  1. STATE_FORWARD: 앞으로 간다.

  2. STATE_AVOID_BACK: 장애물이 가까우면 잠깐 뒤로 간다.

  3. STATE_AVOID_TURN: 방향을 바꾼다.

  4. 다시 STATE_FORWARD로 돌아간다.

아래 코드는 이 흐름을 delay() 없이 구성한 예입니다. 모터 제어 부분은 앞에서 만든 바퀴 로봇의 주행 함수와 같은 방식으로 정리했습니다. 초음파 센서의 TRIG와 ECHO는 모터 핀과 겹치지 않도록 D2, D3에 연결한다고 가정합니다.

const int TRIG_PIN = 2;
const int ECHO_PIN = 3;

const int LEFT_IN1 = 4;
const int LEFT_IN2 = 5;
const int LEFT_PWM = 6;

const int RIGHT_IN1 = 7;
const int RIGHT_IN2 = 8;
const int RIGHT_PWM = 9;

const int STBY_PIN = 10;

const int SAFE_DISTANCE = 20;
const int MOTOR_SPEED = 170;
const int BACK_SPEED = 150;
const int TURN_SPEED = 170;

const int STATE_FORWARD = 1;
const int STATE_AVOID_BACK = 2;
const int STATE_AVOID_TURN = 3;

int robotState = STATE_FORWARD;
unsigned long stateStartTime = 0;

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);

  stopMotors();
  stateStartTime = millis();
}

void loop() {
  int distance = readDistanceCm();

  Serial.print("distance: ");
  Serial.print(distance);
  Serial.print(" cm, state: ");
  Serial.println(robotState);

  if (robotState == STATE_FORWARD) {
    if (distance <= SAFE_DISTANCE) {
      robotState = STATE_AVOID_BACK;
      stateStartTime = millis();
      stopMotors();
    } else {
      forward(MOTOR_SPEED);
    }
  }

  else if (robotState == STATE_AVOID_BACK) {
    backward(BACK_SPEED);

    if (millis() - stateStartTime > 400) {
      robotState = STATE_AVOID_TURN;
      stateStartTime = millis();
      stopMotors();
    }
  }

  else if (robotState == STATE_AVOID_TURN) {
    turnRight(TURN_SPEED);

    if (millis() - stateStartTime > 600) {
      robotState = STATE_FORWARD;
      stateStartTime = millis();
      stopMotors();
    }
  }
}

int 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;
  }

  int distance = duration * 0.0343 / 2;
  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);
}

이 코드는 한 번에 모든 행동을 실행하지 않습니다. 현재 상태를 확인하고, 그 상태에서 해야 할 일만 실행합니다. 시간이 지나거나 센서 조건이 바뀌면 다음 상태로 이동합니다.

여기서 중요한 흐름은 다음과 같습니다.

  • 전진 중 장애물이 가까워진다.

  • 상태를 STATE_AVOID_BACK으로 바꾼다.

  • 0.4초 동안 뒤로 간다.

  • 상태를 STATE_AVOID_TURN으로 바꾼다.

  • 0.6초 동안 오른쪽으로 돈다.

  • 다시 전진 상태로 돌아간다.

이처럼 상태를 나누면 여러 행동이 서로 충돌하지 않습니다. 전진하면서 동시에 후진하는 이상한 명령을 줄 가능성도 줄어듭니다.

예상과 다르게 움직일 때 점검 순서

로봇은 코드가 맞아 보여도 실제로는 다르게 움직일 수 있습니다. 이때 무작정 코드를 고치면 문제가 더 복잡해집니다. 항상 입력, 처리, 출력 순서로 확인하세요.

첫째, 센서값을 확인합니다. 시리얼 모니터에 거리값을 출력해 보세요. 손을 가까이 가져갔을 때 값이 줄어드는지 확인합니다. 값이 계속 999로 나오면 TRIG, ECHO 핀 연결이나 전원, GND를 점검해야 합니다.

둘째, 조건식을 확인합니다. 거리값은 정상인데 로봇이 멈추지 않는다면 distance <= SAFE_DISTANCE 조건이 맞는지 봅니다. 안전거리 값이 너무 작으면 로봇이 장애물을 늦게 발견합니다.

셋째, 상태 변화를 확인합니다. 시리얼 모니터에 robotState를 함께 출력하면 로봇이 어떤 상태에 있는지 볼 수 있습니다. 장애물이 가까운데도 상태가 바뀌지 않으면 조건문 위치를 확인합니다.

넷째, 모터 방향을 확인합니다. 전진 명령을 줬는데 한쪽 바퀴가 반대로 돈다면 해당 모터의 두 선을 바꾸거나, 코드에서 그 모터의 HIGH와 LOW를 반대로 설정합니다. 좌회전과 우회전이 바뀌어도 같은 방식으로 수정할 수 있습니다.

다섯째, 전원과 GND를 확인합니다. 모터가 떨리기만 하거나 아두이노가 재시작된다면 전원이 부족할 가능성이 있습니다. 모터 전원과 아두이노 전원을 구분하고, 두 회로의 GND는 반드시 공통으로 연결해야 합니다.

판단 구조를 만들었다는 것의 의미

이번 장에서 만든 로봇은 아직 복잡한 지능을 가진 로봇은 아닙니다. 하지만 중요한 단계에 도달했습니다. 이제 로봇은 단순히 명령받은 대로만 움직이지 않습니다. 센서값을 읽고, 조건을 판단하고, 현재 상태에 따라 행동을 바꿉니다.

로봇의 판단은 거창한 생각이 아니라 작은 규칙의 모음입니다. 거리가 가까우면 회피하고, 선이 보이면 따라가고, 신호가 오면 멈추는 식입니다. 이 규칙들을 상태로 정리하면 더 복잡한 로봇도 차근차근 만들 수 있습니다.

다음 장에서는 이 판단 구조를 실제 프로젝트로 확장합니다. 초음파 센서와 바퀴 로봇을 결합해 장애물을 피하며 움직이는 자율주행 로봇을 만들어 보겠습니다.

책 목록

아두이노로 배우는 로봇의 기본

전체 목차 보기

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

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

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

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

참여하기