산업 제조
산업용 사물 인터넷 | 산업자재 | 장비 유지 보수 및 수리 | 산업 프로그래밍 |
home  MfgRobots >> 산업 제조 >  >> Manufacturing Technology >> 제조공정

아두이노 나노 기반 AI 라인 팔로잉 로봇: 미로 탐색 및 PID 제어

부품 및 소모품

부품 수량
아두이노 나노 1
Seeed 무선 블루투스 RF 트랜시버 마스터/슬레이브 1
IR 적외선 센서 모듈 반사형 포토일렉트릭 1
연속 회전 서보 (소니) 1
4.8V 배터리 (안드로이드) 1

앱 및 온라인 서비스

프로젝트 소개

아두이노 기반 픽 앤 플레이스 로봇 개발 후, 인공지능 기반 라인 팔로잉 로봇 개발로 이어졌습니다. 초보자를 위한 단순한 라인 팔로워도 좋지만, 서보 모터 대신 적외선 센서를 추가해 고급 수준에서 내 실력을 테스트해보고 싶었습니다.

어려운 과제였지만 온라인 자료 조사로 많은 도움을 받았습니다.

왜 AI 기반 라인 팔로잉 로봇이 필요한가?

AI는 세상을 바꿀 잠재력을 가진 최신 기술로, 로봇에 지능을 부여해 현실 문제를 이해하고 해결하게 하는 꿈을 실현하는 열쇠입니다.

내 프로토타입은 초보적 수준의 지능이지만, AI 프로젝트로 나아가는 첫걸음입니다. 단계별로 알기 쉽게 설명하고, 아두이노 IDE에서 바로 작동하는 정확한 코드를 제공하려 합니다. 자원 부족으로 로봇을 직접 제작하진 못했지만, 연구를 통해 어떻게 만들지 잘 설명하겠습니다.

적외선 센서 활용

프로젝트에는 총 7개 적외선 센서를 사용합니다(다이어그램 참조).

5개 PID 제어 센서 역할

이 5개 센서는 디지털 출력(HIGH/LOW, 1/0)을 생성합니다. 검은 선 중앙에 있는 센서만 HIGH(1) 출력. 가능한 패턴:

좌/우 감지 센서

아날로그 출력 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자인지 구분이 필요하고, "좌/우 회전" 시에도 단순 회전인지 직진 가능인지 구분이 필요하다는 점입니다. 이를 위해 "한 걸음 더 나아가기(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); }

제조공정

  1. 라즈베리 파이로 벽 회피 로봇 만들기 - 초보자용 단계별 가이드
  2. 라즈베리 파이 기반 비전 기반 장애물 회피 모바일 로봇
  3. 스타 오토메이션, NPE2018서 붓 휘두르는 로봇으로 일본 서예 시연
  4. 비트만 바텐펠트, 고강도 사출 성형 자동화용 WX153 신규 로봇 라인 출시
  5. Bosch, 하노버 메세에서 AI 기반 제조 솔루션 공개
  6. FANUC, SCARA 로봇 라인 확대: 6개 모델, 3~20kg 페이로드, 400~1,100mm 리치
  7. 리테일 인공지능: 일시적 유행 아닌 검증된 혜택
  8. 촉각·시각 융합 AI로 90종 이상 물체 인식하는 로봇 손 개발
  9. AI 기반 로보틱스: 최첨단 지능형 기계
  10. AI: 비즈니스 이점, 위험, 산업 영향 총정리