아두이노 나노 기반 AI 라인 팔로잉 로봇: 미로 탐색 및 PID 제어
부품 및 소모품
| 부품 |
수량 |
| 아두이노 나노 |
1 |
| Seeed 무선 블루투스 RF 트랜시버 마스터/슬레이브 |
1 |
| IR 적외선 센서 모듈 반사형 포토일렉트릭 |
1 |
| 연속 회전 서보 (소니) |
1 |
| 4.8V 배터리 (안드로이드) |
1 |
앱 및 온라인 서비스
프로젝트 소개
아두이노 기반 픽 앤 플레이스 로봇 개발 후, 인공지능 기반 라인 팔로잉 로봇 개발로 이어졌습니다. 초보자를 위한 단순한 라인 팔로워도 좋지만, 서보 모터 대신 적외선 센서를 추가해 고급 수준에서 내 실력을 테스트해보고 싶었습니다.
어려운 과제였지만 온라인 자료 조사로 많은 도움을 받았습니다.
왜 AI 기반 라인 팔로잉 로봇이 필요한가?
AI는 세상을 바꿀 잠재력을 가진 최신 기술로, 로봇에 지능을 부여해 현실 문제를 이해하고 해결하게 하는 꿈을 실현하는 열쇠입니다.
- 조립/생산 라인 활용
- 승객 픽업 버스 등 자율 주행
내 프로토타입은 초보적 수준의 지능이지만, AI 프로젝트로 나아가는 첫걸음입니다. 단계별로 알기 쉽게 설명하고, 아두이노 IDE에서 바로 작동하는 정확한 코드를 제공하려 합니다. 자원 부족으로 로봇을 직접 제작하진 못했지만, 연구를 통해 어떻게 만들지 잘 설명하겠습니다.
적외선 센서 활용
프로젝트에는 총 7개 적외선 센서를 사용합니다(다이어그램 참조).
- 5개: PID 제어용
- 1개: 좌측 감지용
- 1개: 우측 감지용
5개 PID 제어 센서 역할
이 5개 센서는 디지털 출력(HIGH/LOW, 1/0)을 생성합니다. 검은 선 중앙에 있는 센서만 HIGH(1) 출력. 가능한 패턴:
- 10000, 11000, 01000, 01100, 00100, 00110, 00010, 00011, 00001, 00000, 11111
좌/우 감지 센서
아날로그 출력 HIGH/LOW. 5개 센서 값 저장을 위한 배열, 좌/우 센서용 정수 변수 선언:
int LFSensor[5] = {1,1,1,1,1};
int farLeftSensor = 0;
int farRightSensor = 0;
// 센서 읽기
LFSensor[0] = digitalRead(lineFollowSensor0);
LFSensor[1] = digitalRead(lineFollowSensor1);
LFSensor[2] = digitalRead(lineFollowSensor2);
LFSensor[3] = digitalRead(lineFollowSensor3);
LFSensor[4] = digitalRead(lineFollowSensor4);
farRightSensor = analogRead(farRightSensorPin);
farLeftSensor = analogRead(farLeftSensorPin);
미로 좌수 법칙 (Left-Hand Rule)
미로 탈출 알고리즘의 핵심입니다.
- 왼손을 벽에 댄다
- 앞으로 걷기 시작
- 모든 교차로에서 왼손이 왼쪽 벽에 닿아 있도록 유지
- 결국 미로 끝에 도달 (최단 경로는 아닐 수 있음)
2D 미로에서 8가지 교차로 유형이 존재하며, 좌수 법칙 적용 시 각 교차로에서의 행동이 하나로 정해집니다:
- 십자 교차로 → 좌회전
- T자 교차로 → 좌회전
- 우회전만 가능 → 우회전
- 좌회전만 가능 → 좌회전
- 직진 또는 좌회전 → 좌회전
- 직진 또는 우회전 → 직진
- 막다른 길 → U턴 (후진)
- 미로 끝 → 정지
문제는 로봇이 "라인"을 만났을 때, 이것이 십자인지 T자인지 구분이 필요하고, "좌/우 회전" 시에도 단순 회전인지 직진 가능인지 구분이 필요하다는 점입니다. 이를 위해 "한 걸음 더 나아가기(extra inch)" 전략을 사용합니다: 교차로 진입 전 약간 더 전진해 다음 상황을 확인합니다.
알고리즘 흐름
막다른 길: U턴
라인 감지: 한 걸음 더 나아가기 → 라인 있으면 십자→좌회전, 없으면 T자→좌회전, 또 다른 라인이면 미로 끝→정지
우회전 감지: 한 걸음 더 나아가기 → 라인 있으면 직진 또는 우회전→직진, 없으면 우회전만→우회전
좌회전 감지: 한 걸음 더 나아가기 → 라인 있으면 직진 또는 좌회전→좌회전, 없으면 좌회전만→좌회전 (좌회전 경우는 테스트 생략 가능)
아두이노 드라이버 이슈 해결
아두이노 드라이버 문제 해결 영상 참고.
코드
RoBot_Maze_Solve_2 (C/C++)
/* 7센서 스마트 로봇 - 미로 해결사 & 라인 팔로워
프로그래머블 PID 컨트롤러 (BT 제어)
기반: Michael Backus Nano Mouse Robot, samvrit PID 튜토리얼
경로 해결 코드: Patrick McCabe
Marcelo Jose Rovai - 2016.04.23 - https://mjrobot.org */
#include <Servo.h>
#include "robotDefines.h"
String command, device;
#include <SoftwareSerial.h>
SoftwareSerial BT1(10, 11); // Rx=10, Tx=11
void setup() {
Serial.begin(9600); BT1.begin(9600);
pinMode(ledPin, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP);
pinMode(lineFollowSensor0, INPUT);
pinMode(lineFollowSensor1, INPUT);
pinMode(lineFollowSensor2, INPUT);
pinMode(lineFollowSensor3, INPUT);
pinMode(lineFollowSensor4, INPUT);
leftServo.attach(5); rightServo.attach(3);
BT1.print("check the PID constants to be sent to Robot");
Serial.print("check the PID constants to be sent to Robot");
BT1.println('\n');
while (digitalRead(buttonPin) && !mode) {
checkBTcmd(); manualCmd(); command = "";
}
checkPIDvalues();
mode = STOPPED; status = 0;
}
void loop() {
ledBlink(1);
BT1.println("Start First Pass"); Serial.println("Start First Pass");
readLFSsensors(); mazeSolve();
ledBlink(2);
BT1.println("End First Pass"); Serial.println("End First Pass");
while (digitalRead(buttonPin) && !mode) {
checkBTcmd(); manualCmd(); command = "";
}
BT1.println("Starting 2nd Pass"); Serial.println("Starting 2nd Pass");
pathIndex = 0; status = 0; mazeOptimization();
ledBlink(3);
BT1.println("End 2nd Pass"); Serial.println("End 2nd Pass");
while (digitalRead(buttonPin) && !mode) {
checkBTcmd(); manualCmd(); command = "";
}
mode = STOPPED; status = 0; pathIndex = 0; pathLength = 0;
}
Functions (C/C++)
void mazeSolve(void) {
while (!status) {
readLFSsensors();
switch (mode) {
case NO_LINE: motorStop(); goAndTurn(LEFT, 180); recIntersection('B'); break;
case CONT_LINE: runExtraInch(); readLFSsensors();
if (mode != CONT_LINE) { goAndTurn(LEFT, 90); recIntersection('L'); } else mazeEnd(); break;
case RIGHT_TURN: runExtraInch(); readLFSsensors();
if (mode == NO_LINE) { goAndTurn(RIGHT, 90); recIntersection('R'); } else recIntersection('S'); break;
case LEFT_TURN: goAndTurn(LEFT, 90); recIntersection('L'); break;
case FOLLOWING_LINE: followingLine(); break;
}
}
}
void recIntersection(char direction) {
path[pathLength] = direction; pathLength++; simplifyPath();
}
void mazeEnd(void) {
motorStop();
BT1.print("The End ==> Path: "); for(int i=0;i "); Serial.println(pathLength);
status = 1; mode = STOPPED;
}
void followingLine(void) { calculatePID(); motorPIDcontrol(); }
void simplifyPath() {
if(pathLength < 3 || path[pathLength-2] != 'B') return;
int totalAngle = 0;
for(int i=1;i<=3;i++) {
switch(path[pathLength-i]) {
case 'R': totalAngle += 90; break;
case 'L': totalAngle += 270; break;
case 'B': totalAngle += 180; break;
}
}
totalAngle %= 360;
switch(totalAngle) {
case 0: path[pathLength-3] = 'S'; break;
case 90: path[pathLength-3] = 'R'; break;
case 180: path[pathLength-3] = 'B'; break;
case 270: path[pathLength-3] = 'L'; break;
}
pathLength -= 2;
}
void mazeOptimization(void) {
while (!status) {
readLFSsensors();
switch (mode) {
case FOLLOWING_LINE: followingLine(); break;
case CONT_LINE: case LEFT_TURN: case RIGHT_TURN:
if (pathIndex >= pathLength) mazeEnd();
else { mazeTurn(path[pathIndex]); pathIndex++; }
break;
}
}
}
void mazeTurn(char dir) {
switch(dir) {
case 'L': goAndTurn(LEFT, 90); break;
case 'R': goAndTurn(RIGHT, 90); break;
case 'B': goAndTurn(RIGHT, 800); break;
case 'S': runExtraInch(); break;
}
}
GeneralFunctions (C/C++)
void ledBlink(int times) { for(int i=0;i0) { Serial.print("Command from BT ==> "); Serial.println(device); command=device; device=""; BT1.flush(); }
}
void manualCmd() {
switch(command[0]) {
case 'g': mode=FOLLOWING_LINE; break;
case 's': motorStop(); break;
case 'f': motorForward(); break;
case 'r': motorTurn(RIGHT,30); motorStop(); break;
case 'l': motorTurn(LEFT,30); motorStop(); break;
case 'b': motorBackward(); break;
case 'p': Kp=command[2]; break;
case 'i': Ki=command[2]; break;
case 'd': Kd=command[2]; break;
}
}
void sendBTdata(int data) { digitalWrite(ledPin,HIGH); BT1.print("Data from Arduino"); BT1.print(data); BT1.print(" xx"); BT1.println('\n'); digitalWrite(ledPin,LOW); }
void calculatePID() { P=error; I+=error; D=error-previousError; PIDvalue=(Kp*P)+(Ki*I)+(Kd*D); previousError=error; }
void checkPIDvalues() { BT1.print("PID: "); BT1.print(Kp); BT1.print(" - "); BT1.print(Ki); BT1.print(" - "); BT1.println(Kd); Serial.print("PID: "); Serial.print(Kp); Serial.print(" - "); Serial.print(Ki); Serial.print(" - "); Serial.println(Kd); }
void testLineFollowSensors() {
int LFS0=digitalRead(lineFollowSensor0), LFS1=digitalRead(lineFollowSensor1), LFS2=digitalRead(lineFollowSensor2);
int LFS3=digitalRead(lineFollowSensor3), LFS4=digitalRead(lineFollowSensor4);
Serial.print("LFS: L 0 1 2 3 4 R ==> "); Serial.print(LFS0); Serial.print(" "); Serial.print(LFS1); Serial.print(" ");
Serial.print(LFS2); Serial.print(" "); Serial.print(LFS3); Serial.print(" "); Serial.print(LFS4);
Serial.print(" ==> P: "); Serial.print(P); Serial.print(" I: "); Serial.print(I); Serial.print(" D: "); Serial.print(D);
Serial.print(" PID: "); Serial.println(PIDvalue);
}
SensorFunctions (C/C++)
void readLFSsensors() {
LFSensor[0]=digitalRead(lineFollowSensor0); LFSensor[1]=digitalRead(lineFollowSensor1);
LFSensor[2]=digitalRead(lineFollowSensor2); LFSensor[3]=digitalRead(lineFollowSensor3);
LFSensor[4]=digitalRead(lineFollowSensor4);
farRightSensor=analogRead(farRightSensorPin); farLeftSensor=analogRead(farLeftSensorPin);
if(LFSensor[0]==1 && LFSensor[1]==1 && LFSensor[2]==1 && LFSensor[3]==1 && LFSensor[4]==1) { mode=CONT_LINE; error=0; }
else if(LFSensor[0]==0 && farRightSensor < THRESHOLD) { mode=RIGHT_TURN; error=0; }
else if(farLeftSensor < THRESHOLD && LFSensor[4]==0) { mode=LEFT_TURN; error=0; }
else if(LFSensor[0]==0 && LFSensor[1]==0 && LFSensor[2]==0 && LFSensor[3]==0 && LFSensor[4]==0) { mode=NO_LINE; error=0; }
else if(LFSensor[0]==0 && LFSensor[1]==0 && LFSensor[2]==0 && LFSensor[3]==0 && LFSensor[4]==1) { mode=FOLLOWING_LINE; error=4; }
else if(LFSensor[0]==0 && LFSensor[1]==0 && LFSensor[2]==0 && LFSensor[3]==1 && LFSensor[4]==1) { mode=FOLLOWING_LINE; error=3; }
else if(LFSensor[0]==0 && LFSensor[1]==0 && LFSensor[2]==0 && LFSensor[3]==1 && LFSensor[4]==0) { mode=FOLLOWING_LINE; error=2; }
else if(LFSensor[0]==0 && LFSensor[1]==0 && LFSensor[2]==1 && LFSensor[3]==1 && LFSensor[4]==0) { mode=FOLLOWING_LINE; error=1; }
else if(LFSensor[0]==0 && LFSensor[1]==0 && LFSensor[2]==1 && LFSensor[3]==0 && LFSensor[4]==0) { mode=FOLLOWING_LINE; error=0; }
else if(LFSensor[0]==0 && LFSensor[1]==1 && LFSensor[2]==1 && LFSensor[3]==0 && LFSensor[4]==0) { mode=FOLLOWING_LINE; error=-1; }
else if(LFSensor[0]==0 && LFSensor[1]==1 && LFSensor[2]==0 && LFSensor[3]==0 && LFSensor[4]==0) { mode=FOLLOWING_LINE; error=-2; }
else if(LFSensor[0]==1 && LFSensor[1]==1 && LFSensor[2]==0 && LFSensor[3]==0 && LFSensor[4]==0) { mode=FOLLOWING_LINE; error=-3; }
else if(LFSensor[0]==1 && LFSensor[1]==0 && LFSensor[2]==0 && LFSensor[3]==0 && LFSensor[4]==0) { mode=FOLLOWING_LINE; error=-4; }
Serial.print(farLeftSensor); Serial.print(" <== LEFT RIGH==> "); Serial.print(farRightSensor);
Serial.print(" mode: "); Serial.print(mode); Serial.print(" error:"); Serial.println(error);
}
void readLateralSensors(void) { }
RobotDefines.h (C/C++)
int mode = 0;
#define STOPPED 0
#define FOLLOWING_LINE 1
#define NO_LINE 2
#define CONT_LINE 3
#define POS_LINE 4
#define RIGHT_TURN 5
#define LEFT_TURN 6
const int power = 250, iniMotorPower = 250, adj = 0;
float adjTurn = 8;
int extraInch = 200, adjGoAndTurn = 800;
const int ledPin = 13, buttonPin = 9;
const int lineFollowSensor0 = 12, lineFollowSensor1 = 18, lineFollowSensor2 = 17;
const int lineFollowSensor3 = 16, lineFollowSensor4 = 19;
const int farRightSensorPin = 0, farLeftSensorPin = 1;
const int THRESHOLD = 150;
int farRightSensor = 0, farLeftSensor = 0, LFSensor[5] = {0,0,0,0,0};
float Kp=50, Ki=0, Kd=0;
float error=0, P=0, I=0, D=0, PIDvalue=0, previousError=0, previousI=0;
#define RIGHT 1, LEFT -1
Servo leftServo, rightServo;
unsigned char dir;
char path[100] = "";
unsigned char pathLength = 0, pathIndex = 0, status = 0;
MotorFunctions (C/C++)
void motorStop() { leftServo.writeMicroseconds(1500); rightServo.writeMicroseconds(1500); delay(200); }
void motorForward() { leftServo.writeMicroseconds(1500-(power+adj)); rightServo.writeMicroseconds(1500+power); }
void motorBackward() { leftServo.writeMicroseconds(1500+power); rightServo.writeMicroseconds(1500-power); }
void motorFwTime(unsigned int time) { motorForward(); delay(time); motorStop(); }
void motorBwTime(unsigned int time) { motorBackward(); delay(time); motorStop(); }
void motorTurn(int direction, int degrees) {
leftServo.writeMicroseconds(1500-(iniMotorPower+adj)*direction);
rightServo.writeMicroseconds(1500-iniMotorPower*direction);
delay(round(adjTurn*degrees+20)); motorStop();
}
void motorPIDcontrol() {
int leftMotorSpeed = 1500-(iniMotorPower+adj)-PIDvalue;
int rightMotorSpeed = 1500+iniMotorPower-PIDvalue;
constrain(leftMotorSpeed, 1000, 2000);
constrain(rightMotorSpeed, 1000, 2000);
leftServo.writeMicroseconds(leftMotorSpeed);
rightServo.writeMicroseconds(rightMotorSpeed);
}
void runExtraInch(void) { motorPIDcontrol(); delay(extraInch); motorStop(); }
void goAndTurn(int direction, int degrees) { motorPIDcontrol(); delay(adjGoAndTurn); motorTurn(direction, degrees); }