라인트레이서는 선을 읽는 로봇이다
장애물 회피 로봇은 앞에 있는 물체와의 거리를 보고 움직였다. 이번에는 바닥을 본다. 라인트레이서 로봇은 바닥에 그어진 검은 선을 감지하고, 그 선을 따라 달리는 로봇이다.
원리는 단순하다.
적외선 센서가 바닥을 비춘다.
바닥에서 반사되어 돌아오는 빛의 양을 읽는다.
검은 선인지 흰 바닥인지 판단한다.
선이 왼쪽에 있으면 왼쪽으로, 오른쪽에 있으면 오른쪽으로 방향을 고친다.
여기서 중요한 점은 "선을 발견했다"에서 끝나지 않는다는 것이다. 로봇은 계속 흔들리며 움직인다. 선에서 조금 벗어났을 때 얼마나 빠르게, 얼마나 부드럽게 돌아오느냐가 라인트레이서의 성능을 결정한다. 그래서 라인트레이서는 센서 보정과 모터 제어의 중요성을 가장 분명하게 보여주는 프로젝트다.
AI 생성 이미지
검은 선과 흰 바닥은 왜 다르게 보일까
적외선 반사 센서는 작은 손전등과 눈이 붙어 있는 부품이라고 생각하면 쉽다. 센서 안의 적외선 LED가 바닥에 빛을 보내고, 옆의 수광부가 되돌아오는 빛을 읽는다.
흰 바닥은 빛을 많이 반사한다. 검은 선은 빛을 많이 흡수한다. 그래서 보통 흰 바닥에서는 센서값이 크고, 검은 선에서는 센서값이 작게 나온다. 다만 센서 모듈의 종류와 회로에 따라 반대로 나올 수도 있다. 어떤 모듈은 검은색에서 큰 값, 흰색에서 작은 값을 출력한다.
따라서 외우지 말고 직접 재야 한다.
센서값 확인 순서는 다음과 같다.
적외선 센서를 아두이노에 연결한다.
흰 바닥 위에 센서를 올리고 값을 읽는다.
검은 선 위에 센서를 올리고 값을 읽는다.
두 값의 중간쯤을 임계값으로 정한다.
임계값은 판단 기준이다. 예를 들어 흰 바닥에서 850, 검은 선에서 220이 나온다면 두 값의 중간인 535 정도를 기준으로 삼을 수 있다. 이 경우 센서값이 535보다 작으면 검은 선, 크면 흰 바닥으로 판단한다.
센서 연결하기
이 장에서는 적외선 반사 센서 3개를 사용한다. 왼쪽, 가운데, 오른쪽 센서가 바닥을 나누어 본다.
준비물은 다음과 같다.
아두이노 우노
바퀴 로봇 차체
DC 모터 2개
TB6612FNG 또는 L298N 모터 드라이버
적외선 반사 센서 3개
모터용 배터리
점퍼선
검은 전기테이프 또는 검은색 선이 있는 주행판
센서 연결 예시는 다음과 같다.
| 센서 | 아두이노 핀 |
|---|---|
| 왼쪽 센서 AO | A0 |
| 가운데 센서 AO | A1 |
| 오른쪽 센서 AO | A2 |
| 센서 VCC | 5V |
| 센서 GND | GND |
이 장의 예제는 센서의 아날로그 출력인 AO를 사용한다. 일부 센서 모듈에는 DO라는 디지털 출력도 있지만, DO는 HIGH 또는 LOW만 읽기 때문에 아래의 아날로그 예제와는 다르게 동작할 수 있다.
모터 드라이버는 이전 장에서 사용한 바퀴 로봇 연결을 그대로 사용한다. 이 장의 예제 코드는 TB6612FNG 기준으로 작성한다.
| TB6612FNG 핀 | 아두이노 핀 |
|---|---|
| PWMA | D5 |
| AIN1 | D7 |
| AIN2 | D8 |
| PWMB | D6 |
| BIN1 | D9 |
| BIN2 | D10 |
| STBY | D4 |
모터 전원은 모터용 배터리에서 공급한다. 아두이노의 GND와 모터 드라이버의 GND, 센서의 GND는 반드시 함께 연결해야 한다. GND가 같아야 아두이노가 센서값과 모터 드라이버 신호를 같은 기준으로 이해할 수 있다.
먼저 센서값을 확인하자
라인트레이서를 바로 주행시키면 문제가 생겼을 때 원인을 찾기 어렵다. 먼저 센서값만 확인하자.
const int LEFT_SENSOR = A0;
const int CENTER_SENSOR = A1;
const int RIGHT_SENSOR = A2;
void setup() {
Serial.begin(9600);
}
void loop() {
int leftValue = analogRead(LEFT_SENSOR);
int centerValue = analogRead(CENTER_SENSOR);
int rightValue = analogRead(RIGHT_SENSOR);
Serial.print("L: ");
Serial.print(leftValue);
Serial.print(" C: ");
Serial.print(centerValue);
Serial.print(" R: ");
Serial.println(rightValue);
delay(200);
}업로드한 뒤 시리얼 모니터를 연다. 센서를 흰 바닥 위에 올렸을 때와 검은 선 위에 올렸을 때 값을 비교한다.
예를 들어 다음처럼 나올 수 있다.
| 위치 | 흰 바닥 | 검은 선 |
|---|---|---|
| 왼쪽 센서 | 820 | 240 |
| 가운데 센서 | 850 | 260 |
| 오른쪽 센서 | 810 | 230 |
이런 경우 임계값은 500 정도로 시작하면 된다. 센서마다 값이 조금 다르므로 완전히 같을 필요는 없다. 중요한 것은 흰 바닥 값과 검은 선 값 사이에 충분한 차이가 있는지 확인하는 것이다.
선의 위치를 네 가지 상태로 나누기
센서 3개를 사용하면 선의 위치를 간단히 나눌 수 있다.
| 센서 상태 | 의미 | 로봇의 행동 |
|---|---|---|
| 가운데 센서가 선을 봄 | 선이 중앙에 있음 | 전진 |
| 왼쪽 센서가 선을 봄 | 선이 로봇의 왼쪽에 있음 | 왼쪽으로 보정 |
| 오른쪽 센서가 선을 봄 | 선이 로봇의 오른쪽에 있음 | 오른쪽으로 보정 |
| 아무 센서도 선을 못 봄 | 선을 놓침 | 마지막 방향으로 탐색 |
여기서 "왼쪽 센서가 선을 본다"는 말은 로봇이 선보다 오른쪽으로 밀려났다는 뜻이다. 선이 로봇의 왼쪽 아래에 있으므로, 로봇은 왼쪽으로 방향을 고쳐야 한다.
반대로 오른쪽 센서가 선을 보면 선이 오른쪽 아래에 있으므로 오른쪽으로 방향을 고친다.
실제로는 가운데 센서와 왼쪽 센서가 동시에 선을 보거나, 가운데 센서와 오른쪽 센서가 동시에 선을 볼 때도 있다. 처음에는 너무 복잡하게 나누지 말고, 가운데 센서가 선을 보면 우선 전진하도록 만들면 이해하기 쉽다. 나중에 주행이 익숙해지면 동시에 감지되는 경우까지 나누어 더 부드럽게 조정할 수 있다.
단순 ON/OFF 제어로 따라가기
먼저 가장 단순한 방식으로 만들어 보자. ON/OFF 제어는 조건에 따라 정해진 동작을 바로 선택하는 방식이다.
중앙이면 전진
왼쪽이면 왼쪽 회전
오른쪽이면 오른쪽 회전
선을 잃으면 잠시 정지 또는 마지막 방향으로 회전
이 방식은 이해하기 쉽다. 하지만 방향 전환이 거칠어 로봇이 지그재그로 움직일 수 있다.
const int LEFT_SENSOR = A0;
const int CENTER_SENSOR = A1;
const int RIGHT_SENSOR = A2;
const int PWMA = 5;
const int AIN1 = 7;
const int AIN2 = 8;
const int PWMB = 6;
const int BIN1 = 9;
const int BIN2 = 10;
const int STBY = 4;
const int THRESHOLD = 500;
// 센서값이 검은 선에서 작게 나오는 경우 true
// 만약 검은 선에서 값이 더 크게 나온다면 false로 바꾼다.
const bool BLACK_IS_LOW = true;
int lastDirection = 0; // -1: 왼쪽, 0: 중앙, 1: 오른쪽
bool isBlack(int value) {
if (BLACK_IS_LOW) {
return value < THRESHOLD;
} else {
return value > THRESHOLD;
}
}
void setMotor(int leftSpeed, int rightSpeed) {
leftSpeed = constrain(leftSpeed, -255, 255);
rightSpeed = constrain(rightSpeed, -255, 255);
if (leftSpeed >= 0) {
digitalWrite(AIN1, HIGH);
digitalWrite(AIN2, LOW);
} else {
digitalWrite(AIN1, LOW);
digitalWrite(AIN2, HIGH);
}
if (rightSpeed >= 0) {
digitalWrite(BIN1, HIGH);
digitalWrite(BIN2, LOW);
} else {
digitalWrite(BIN1, LOW);
digitalWrite(BIN2, HIGH);
}
analogWrite(PWMA, abs(leftSpeed));
analogWrite(PWMB, abs(rightSpeed));
}
void stopMotors() {
analogWrite(PWMA, 0);
analogWrite(PWMB, 0);
}
void setup() {
pinMode(PWMA, OUTPUT);
pinMode(AIN1, OUTPUT);
pinMode(AIN2, OUTPUT);
pinMode(PWMB, OUTPUT);
pinMode(BIN1, OUTPUT);
pinMode(BIN2, OUTPUT);
pinMode(STBY, OUTPUT);
digitalWrite(STBY, HIGH);
Serial.begin(9600);
}
void loop() {
int leftValue = analogRead(LEFT_SENSOR);
int centerValue = analogRead(CENTER_SENSOR);
int rightValue = analogRead(RIGHT_SENSOR);
bool leftBlack = isBlack(leftValue);
bool centerBlack = isBlack(centerValue);
bool rightBlack = isBlack(rightValue);
if (centerBlack) {
setMotor(120, 120);
lastDirection = 0;
} else if (leftBlack) {
setMotor(70, 140);
lastDirection = -1;
} else if (rightBlack) {
setMotor(140, 70);
lastDirection = 1;
} else {
// 선을 놓쳤을 때 마지막으로 보정하던 방향으로 천천히 찾는다.
if (lastDirection < 0) {
setMotor(60, 130);
} else if (lastDirection > 0) {
setMotor(130, 60);
} else {
stopMotors();
}
}
}왼쪽으로 보정할 때 왼쪽 모터를 느리게, 오른쪽 모터를 빠르게 돌린다. 그러면 로봇의 앞부분이 왼쪽으로 기울어진다. 오른쪽 보정은 반대로 한다.
여기서 중요한 것은 한쪽 바퀴를 무조건 멈추지 않는 것이다. 한쪽 바퀴를 완전히 멈추면 방향은 빨리 바뀌지만 움직임이 거칠어진다. 처음에는 느린 바퀴 70, 빠른 바퀴 140 정도로 시작하고, 로봇이 너무 둔하면 차이를 키운다.
부드러운 주행을 위한 비례 제어
ON/OFF 제어는 판단이 단순하다. 선이 왼쪽이면 항상 같은 크기로 왼쪽 보정을 한다. 선이 조금 왼쪽에 있어도, 많이 왼쪽에 있어도 같은 동작을 한다.
비례 제어는 다르다. 비례 제어는 벗어난 정도에 비례해서 보정량을 바꾼다. 조금 벗어나면 조금만 고치고, 많이 벗어나면 크게 고친다.
생활 속 예로 생각해 보자. 자전거를 탈 때 방향이 살짝 틀어졌다고 핸들을 크게 꺾지는 않는다. 조금 틀어졌으면 조금만 고친다. 많이 벗어났을 때만 크게 고친다. 라인트레이서도 같은 방식이 더 안정적이다.
다음 코드는 왼쪽 센서와 오른쪽 센서의 차이를 이용해 보정량을 계산한다. 검은 선에 가까울수록 blackStrength() 값이 커지도록 만든 뒤, 왼쪽 값과 오른쪽 값의 차이로 방향을 정한다.
const int LEFT_SENSOR = A0;
const int CENTER_SENSOR = A1;
const int RIGHT_SENSOR = A2;
const int PWMA = 5;
const int AIN1 = 7;
const int AIN2 = 8;
const int PWMB = 6;
const int BIN1 = 9;
const int BIN2 = 10;
const int STBY = 4;
const bool BLACK_IS_LOW = true;
const int BASE_SPEED = 120;
const float KP = 0.08;
const int LINE_LOST_LEVEL = 250;
int blackStrength(int value) {
if (BLACK_IS_LOW) {
return 1023 - value;
} else {
return value;
}
}
void setMotor(int leftSpeed, int rightSpeed) {
leftSpeed = constrain(leftSpeed, -255, 255);
rightSpeed = constrain(rightSpeed, -255, 255);
digitalWrite(AIN1, leftSpeed >= 0 ? HIGH : LOW);
digitalWrite(AIN2, leftSpeed >= 0 ? LOW : HIGH);
digitalWrite(BIN1, rightSpeed >= 0 ? HIGH : LOW);
digitalWrite(BIN2, rightSpeed >= 0 ? LOW : HIGH);
analogWrite(PWMA, abs(leftSpeed));
analogWrite(PWMB, abs(rightSpeed));
}
void setup() {
pinMode(PWMA, OUTPUT);
pinMode(AIN1, OUTPUT);
pinMode(AIN2, OUTPUT);
pinMode(PWMB, OUTPUT);
pinMode(BIN1, OUTPUT);
pinMode(BIN2, OUTPUT);
pinMode(STBY, OUTPUT);
digitalWrite(STBY, HIGH);
Serial.begin(9600);
}
void loop() {
int leftRaw = analogRead(LEFT_SENSOR);
int centerRaw = analogRead(CENTER_SENSOR);
int rightRaw = analogRead(RIGHT_SENSOR);
int leftBlack = blackStrength(leftRaw);
int centerBlack = blackStrength(centerRaw);
int rightBlack = blackStrength(rightRaw);
int error = leftBlack - rightBlack;
int correction = KP * error;
int leftSpeed = BASE_SPEED - correction;
int rightSpeed = BASE_SPEED + correction;
// 세 센서 모두 선을 거의 보지 못하면 잠시 속도를 낮춘다.
if (leftBlack < LINE_LOST_LEVEL && centerBlack < LINE_LOST_LEVEL && rightBlack < LINE_LOST_LEVEL) {
setMotor(70, 70);
} else {
setMotor(leftSpeed, rightSpeed);
}
Serial.print("L:");
Serial.print(leftBlack);
Serial.print(" C:");
Serial.print(centerBlack);
Serial.print(" R:");
Serial.print(rightBlack);
Serial.print(" error:");
Serial.println(error);
}KP는 비례 제어의 세기다. 값이 너무 작으면 로봇이 늦게 반응한다. 값이 너무 크면 좌우로 흔들리며 달린다. 처음에는 0.05에서 0.10 사이로 시작해 보자. 로봇이 커브를 놓치면 KP를 조금 키우고, 지그재그가 심하면 조금 줄인다.
LINE_LOST_LEVEL은 "선이 거의 보이지 않는다"고 판단하는 기준이다. 위 예제에서는 흰 바닥에서 blackStrength() 값이 200 안팎으로 나온다고 보고 250으로 두었다. 자신의 주행판에서 흰 바닥 값이 이보다 훨씬 크거나 작다면, 시리얼 모니터를 보면서 이 값도 함께 조정해야 한다.
센서 보정이 주행을 바꾼다
라인트레이서는 코드만으로 완성되지 않는다. 같은 코드라도 바닥, 조명, 센서 높이에 따라 전혀 다르게 움직인다.
먼저 센서 높이를 확인하자. 적외선 반사 센서는 바닥과 너무 멀면 반사광을 제대로 읽지 못한다. 너무 가까우면 바닥에 닿거나 작은 흔들림에도 값이 크게 변한다. 보통 바닥에서 몇 밀리미터 정도 떨어진 위치에서 안정적인 값을 찾는다.
센서 간격도 중요하다. 세 센서가 너무 가까우면 선이 어느 쪽으로 벗어났는지 구분하기 어렵다. 너무 멀면 가운데 센서와 양쪽 센서 사이의 빈 공간에서 선을 놓칠 수 있다. 검은 선의 폭보다 약간 넓게 배치해, 선이 중앙에서 벗어날 때 왼쪽 또는 오른쪽 센서가 자연스럽게 감지하도록 맞춘다.
바닥 재질도 확인해야 한다. 광택이 강한 바닥은 빛이 예상과 다르게 반사될 수 있다. 검은 전기테이프도 종류에 따라 반사율이 다르다. 그래서 주행판을 바꾸면 임계값을 다시 확인하는 것이 좋다.
조명 변화도 영향을 준다. 햇빛이 강한 곳, 그림자가 생기는 곳, 형광등 바로 아래에서는 센서값이 달라질 수 있다. 시리얼 모니터로 흰 바닥과 검은 선 값을 다시 확인하고 임계값을 조정하자.
자주 생기는 문제와 해결 방법
로봇이 선을 따라가지 못하고 반대로 도는 경우가 있다. 이때는 왼쪽 센서와 오른쪽 센서가 코드에서 바뀌었거나, 모터 방향이 반대로 연결되었을 가능성이 크다. 먼저 로봇을 손으로 들어 올린 뒤 왼쪽 보정 상황에서 어느 바퀴가 더 빠르게 도는지 확인하자.
로봇이 계속 지그재그로 움직인다면 속도가 너무 빠르거나 보정량이 너무 크다. BASE_SPEED를 낮추거나 KP를 줄인다. ON/OFF 제어를 사용 중이라면 느린 바퀴와 빠른 바퀴의 속도 차이를 줄인다.
커브에서 선을 놓친다면 속도가 빠르거나 센서 간격이 맞지 않을 수 있다. 먼저 속도를 낮춘다. 그래도 놓친다면 센서가 선을 충분히 넓게 볼 수 있도록 위치를 조정한다.
센서값 차이가 거의 없다면 센서 높이, 바닥 색, 배선을 확인한다. VCC와 GND가 제대로 연결되었는지, 아날로그 출력 AO를 아두이노의 A0, A1, A2에 연결했는지도 확인한다. 디지털 출력 DO를 사용하는 센서라면 아날로그 값이 아니라 HIGH/LOW만 읽히므로 이 장의 코드와 맞지 않을 수 있다.
이번 장에서 완성한 것
이번 장에서는 적외선 반사 센서로 검은 선과 흰 바닥을 구분하고, 그 결과에 따라 바퀴 로봇의 방향을 바꾸었다.
핵심은 세 가지다.
첫째, 센서값은 외우는 것이 아니라 측정하는 것이다. 검은 선과 흰 바닥에서 실제 값이 어떻게 나오는지 확인한 뒤 임계값을 정해야 한다.
둘째, 선의 위치를 상태로 나누면 판단이 쉬워진다. 중앙이면 전진, 왼쪽이면 왼쪽 보정, 오른쪽이면 오른쪽 보정이라는 구조만으로도 라인트레이서의 기본 동작을 만들 수 있다.
셋째, 좋은 주행은 모터 속도 조절에서 나온다. 단순 ON/OFF 제어는 이해하기 쉽지만 거칠고, 비례 제어는 벗어난 정도에 따라 부드럽게 복귀할 수 있다.
이제 로봇은 장애물을 피할 수도 있고, 바닥의 선을 따라갈 수도 있다. 다음 단계에서는 로봇을 직접 손으로 명령하는 방법, 즉 원격 조종과 무선 통신의 기초를 다룬다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.