Improving PID logic and constructing a line maze solving robot

Hello, Arduino community! I’m working on an exciting line-following robot project and could use your expertise to take it to the next level. Currently, I’ve implemented a PID controller for line following, which helps the robot track the line accurately. While the robot performs well, it takes turns slightly late. I’m seeking advice to fine-tune the PID parameters (Kp, Ki, Kd) or improve the control logic to make the turns smoother and more responsive.

Additionally, I’m working towards enabling the robot to solve a maze. The plan is for the robot to conduct an initial exploratory run, where it remembers all the paths it traverses, identifies dead ends, and records valid routes. Once the maze is fully explored, I want the robot to compute the shortest path to the destination and then solve the maze efficiently during the final run.

To accomplish this, I aim to implement algorithms like depth-first search or similar techniques for path recording, with optimizations for selecting the shortest path. The robot should be capable of differentiating nodes, dead ends, and intersections during its exploration. My code currently handles basic node detection and turning decisions, but I need guidance on enhancing the PID for smoother turns and implementing the maze-solving and path-optimization logic effectively.

I’ve included my current code below for reference. I’d be incredibly grateful for any insights, suggestions, or resources that can help refine my robot’s performance and achieve these advanced capabilities. Thank you so much for your support!

Note : The maze will have only 90 degree turns and no curved path will be there.

#define RMotPin1 5
#define RMotPin2 6
#define LMotPin1 7
#define LMotPin2 8
#define LMotSpeedPin 9
#define RMotSpeedPin 10
int MOTOR_SPEED = 150;

#define IR_MAX 710
#define IR_MIN 15

unsigned long noLineStart = 0;

int leftMotorSpeed = MOTOR_SPEED;
int rightMotorSpeed = MOTOR_SPEED;

// PID constants
float Kp = 10; // Proportional constant
float Ki = 0.5; // Integral constant
float Kd = 1; // Derivative constant

float previousError = 0;
float integral = 0;

struct IRSensor
{
  IRSensor(int _pin, bool _flipped = false) : m_pin(_pin), m_flipped(_flipped) {}

  int value = 0;
  int update() { value = analogRead(m_pin); return value; }

  inline bool isBlack() { if(m_flipped) return value < THRESHOLD; else return value > THRESHOLD; }
  inline bool isWhite() { return !isBlack(); }

  static const int THRESHOLD = (IR_MIN + IR_MAX) * 0.5;

  private:
  int m_pin;
  bool m_flipped;
};

// IR Sensors
IRSensor IR1(A0); // Far left
IRSensor IR2(A1); // Left
IRSensor IR3(A2, true); // Center
IRSensor IR4(A3); // Right
IRSensor IR5(A4); // Far right

void updateIR()
{
  IR1.update();
  IR2.update();
  IR3.update();
  IR4.update();
  IR5.update();
}

void displayIR()
{
  Serial.println(String(IR1.value) + ", " + String(IR2.value) + ", " + String(IR3.value) + ", " + String(IR4.value) + ", " + String(IR5.value));
}

void rotateMotor(int rightMotorSpeed, int leftMotorSpeed)
{
  if (rightMotorSpeed < 0)
  {
    digitalWrite(RMotPin1, LOW);
    digitalWrite(RMotPin2, HIGH);
  }
  else if (rightMotorSpeed > 0)
  {
    digitalWrite(RMotPin1, HIGH);
    digitalWrite(RMotPin2, LOW);
  }
  else
  {
    digitalWrite(RMotPin1, LOW);
    digitalWrite(RMotPin2, LOW);
  }

  if (leftMotorSpeed < 0)
  {
    digitalWrite(LMotPin1, LOW);
    digitalWrite(LMotPin2, HIGH);
  }
  else if (leftMotorSpeed > 0)
  {
    digitalWrite(LMotPin1, HIGH);
    digitalWrite(LMotPin2, LOW);
  }
  else
  {
    digitalWrite(LMotPin1, LOW);
    digitalWrite(LMotPin2, LOW);
  }

  analogWrite(RMotSpeedPin, abs(rightMotorSpeed));
  analogWrite(LMotSpeedPin, abs(leftMotorSpeed));
}

void PID()
{
  int error = 0;

  // Error calculation: Weighted contributions for more accuracy
  if (IR1.isBlack()) error -= 2; // Far left sensor (higher weight)
  if (IR2.isBlack()) error -= 1; // Left sensor
  if (IR4.isBlack()) error += 1; // Right sensor
  if (IR5.isBlack()) error += 2; // Far right sensor (higher weight)

  // Proportional term
  float proportional = error * Kp;

  // Integral term with clamping to prevent windup
  integral += error;
  integral = constrain(integral, -50, 50); // Clamping the integral term
  float integralTerm = integral * Ki;

  // Derivative term
  float derivative = (error - previousError) * Kd;

  // PID output
  float correction = proportional + integralTerm + derivative;

  // Adjust motor speeds based on correction
  leftMotorSpeed = MOTOR_SPEED - correction;
  rightMotorSpeed = MOTOR_SPEED + correction;

  // Clamp motor speeds for better control
  leftMotorSpeed = constrain(leftMotorSpeed, 0, MOTOR_SPEED);
  rightMotorSpeed = constrain(rightMotorSpeed, 0, MOTOR_SPEED);

  rotateMotor(rightMotorSpeed, leftMotorSpeed);

  previousError = error;


}

void Left()
{
  // Improved left turn logic with sensor feedback
  rotateMotor(150, 0);
  delay(150); // Initial turn
  while (!IR3.isBlack()) updateIR(); // Continue turning until the center sensor detects the line
  rotateMotor(0, 0); // Stop motors
}

void Right()
{
  // Improved right turn logic with sensor feedback
  rotateMotor(0, 150);
  delay(150); // Initial turn
  while (!IR3.isBlack()) updateIR(); // Continue turning until the center sensor detects the line
  rotateMotor(0, 0); // Stop motors
}

void StepForward()
{
  rotateMotor(MOTOR_SPEED, MOTOR_SPEED); // Move forward
  delay(200); // Short step delay for stabilization
  rotateMotor(0, 0); // Stop motors
}

void UTurn()
{
  // Improved U-turn with feedback
  rotateMotor(-MOTOR_SPEED, MOTOR_SPEED);
  delay(300); // Start turning
  while (!IR3.isBlack()) updateIR(); // Complete U-turn when center sensor detects the line
  rotateMotor(0, 0);
  Serial.println("UTurn");
}


void detectNode()
{
  const bool farLeft = IR1.isBlack();
  const bool nearLeft = IR2.isBlack();
  const bool center = IR3.isBlack();
  const bool nearRight = IR4.isBlack();
  const bool farRight = IR5.isBlack();

  // Case: No line detected for all sensors
  if (!farLeft && !nearLeft && !center && !nearRight && !farRight)
  {
    static unsigned long noLineStart = 0;
    if (noLineStart == 0)
      noLineStart = millis();

    // If the line is not detected for over 300ms, perform a U-turn
    if (millis() - noLineStart > 300)
    {
      UTurn();
      delay(300);
      StepForward();
      noLineStart = 0; // Reset the timer
      return;
    }
  }
  else
  {
    noLineStart = 0; // Reset timer if any line is detected
  }

  // Node detection: When multiple sensors detect black
  const bool isNode = (farLeft + nearLeft + center + nearRight + farRight) > 2;

  if (!isNode)
  {
    PID(); // Continue normal line following
    return;
  }

  Serial.println("Node detected");

  // Handling turn decisions based on detected paths
  if (nearLeft){
      MOTOR_SPEED = 89;
  }
  else if (nearRight){
      MOTOR_SPEED = 89;
  }
  else if (farLeft && !farRight)
  {
    Left();        // Perform left turn
    Serial.println("Turning Left");
    MOTOR_SPEED = 150;
  }
  else if (farRight && !farLeft)
  {
    Right();       // Perform right turn
    Serial.println("Turning Right");
    MOTOR_SPEED = 150;
  }
  else
  {
    StepForward(); // Move forward if neither far left nor far right is strongly detected
    Serial.println("Moving Forward");
  }
}

void setup()
{
  Serial.begin(9600);
  Serial.println("Hello");

  pinMode(RMotSpeedPin, OUTPUT);
  pinMode(RMotPin1, OUTPUT);
  pinMode(RMotPin2, OUTPUT);

  pinMode(LMotSpeedPin, OUTPUT);
  pinMode(LMotPin1, OUTPUT);
  pinMode(LMotPin2, OUTPUT);

  rotateMotor(MOTOR_SPEED, MOTOR_SPEED);
}

void loop()
{
  updateIR();
  displayIR();
  detectNode();
}

Also for your information upon hardware here are the components i have used :

  1. Arduino UNO x1
  2. L298N Motor Driver x1
  3. 12V LIPO Battery x1
  4. IR sensors x5
  5. DC motors x2
  6. Three Wheel Chasis

And I am also attaching picture of my current robot , the hardware is all ready but not the code

Thanking You in Advance,
Rachit Porwal.

You have made a reasonable effort, but the unnecessary delay() statements in the code are a serious problem. So is all that printing, at the extremely slow baud rate of 9600.

As one consequence, the PID algorithm is not implemented correctly, because the error sampling and control intervals are not constant, as required for best performance of the PID algorithm.

Finally, use of String objects on an Uno will eventually cause the Arduino to malfunction or crash due to memory problems. Avoid using Strings, as they are never necessary.

 Serial.println(String(IR1.value) + ", " + String(IR2.value) + ", " + String(IR3.value) + ", " + String(IR4.value) + ", " + String(IR5.value));

Thank you for your response, i understood what are you trying to explain but actually i couldnt understand what to do on the code , shall i remove the delay() statements?

And also the PID part i will be able to improve it, but the problem is i want the bot to solve the maze then remember all path then find the shortest path and then again solve the maze with the shortest path, that is the challenging part, can you help me for it??

Are the IR sensors digital or analog? They seem to be coded as digital, but the analog pins are waiting for analog values.

They're Analog, connected to A0, A1, A2, A3, A4 (far left, near left, center, near right, far right) like this

Why do you use analog sensors for a boolean datatype?

  inline bool isBlack() { if(m_flipped) return value < THRESHOLD; else return value > THRESHOLD; }

My first pass conclusion is that the logic is strewn about the code making it hard to read, but that's just a high level flight at warp speed.

In addition to @jremington's observations, I can't yet say there isn't less obvious blocking happen all over the sketch.

I won't say yet it needs a massive rewrite, but structuing this using the IPO model would be my first suggestion.

The IPO Model.

It's been a minute since I've tossed thanks and a tip o' the hat to @paulpaulson, who gave me a name for what I had been doing since a long time without.

a7

1 Like

In my opinion, Uno is a poor choice for this part of the requirements. It's processing speed will probably not be sufficient, and it's memory capacity will be hopelessly inadequate. Uno might be ok for a simple "left hand rule" maze solver, but not all mazes can be solved that way, and it certainly won't give optimal solutions.

I would consider Raspberry Pi for this kind of task. It can be programmed in python for ease of development and re-use of libraries and other example code from the internet. A Pi Zero would probably be adequate. A Pi Zero W would also allow you to connect remotely to the robot to monitor and debug during maze runs.

I would also warn you that optimised maze solving is at least 10 times more difficult than what you have achieved so far.

If it's following a line, it's following a line. It's about as simple an autonomous nav system as you can get. Are the lines in your maze leading not only to the solution of the maze, but also to all the dead ends?

If all lines are alike, with the hardware you have so far, there's no storing and remembering the line of best fit to use on the return trip because it was trial-and-error to get there all along, with no differentiation (from what you've said) between the one line that leads to the solution of the maze (like when I cheat on paper and pen mazes by just doing the maze backwards) and all the possible dead ends.

If the correct line was different in some predictable way from all the dead ends, you might be able to work with that. With just one sensor reading one set of binary inputs? I can't think of how your return goal along the correct path is possible.

Consider drones: they use all sorts of sensors to know their orientation relative to a compass heading, GPS, lidar, barometric pressure (off the top of my head) to know where they are and how they got there.

When you hit the RTH (return to home) button on the drone transmitter, it uses the GPS and compass data to return from where it launched, along the shortest path.

It's important to remember it also increases altitude by some setpoint in order to avoid obstacles along that shortest path that it doesn't know anything about; it never sampled any data using other sensors that might be useful (such as the building it stopped 25' from on the way there that it maybe almost flew into but avoided).

Point is, just stumbling through a maze following lines even in the best case with additional sensor data to refer to for the return trip isn't nearly enough. What if your robot only took 30% of the dead ends on the way there and sure, avoided on the way back but instead took all the 70% of the remaining dead ends returning to home that it didn't know about?

Final note: as first mentioned by @jremington and reiterated by others, I'd like to add that when using delay(n mS);, it's very important to know why you're using it. It's no magic potion for speed control or anything like that, and it's a very easy trap to get into.

I'd remove them altogether and make extensive use of Serial.println(whatever); as you already have done for the most part it appears, to gain that better understanding of what's going on throughout your sketch.

On Serial, is it necessary for your debug printing to be so long? Serial.println("turning left"); is no more useful to you, the tinkerer, than Serial.println("L");.

Furthermore, since it's inconvenient to troubleshoot moving things using Serial, you might consider using an LED or three (left, straight ahead, right) that utilize different blink rates during travel to indicate visually if the robot is doing what you think you programmed it to do.

Here's a great page courtesy of Adafruit that you might find useful to do the LED thing should you choose to add it in, and it's non-blocking:

Work your way through it and it should be easy enough for you to see where to drop the constructor to call instances of the three blinky led state machines that vary the on/off time of the blink rate depending on what direction your robot is travelling.

In the simulator...

  • using analog sensors, the sketch waited for the analog data to reach a threshold before changes occurred
  • using digital sensors, data was random (assuming floating Analog pins) unless sensor was active

I made this code change to the "String" noted in post #2.

void displayIR() {
  Serial.print(IR1.value);
  Serial.print(", ");
  Serial.print(IR2.value);
  Serial.print(", ");
  Serial.print(IR3.value);
  Serial.print(", ");
  Serial.print(IR4.value);
  Serial.print(", ");
  Serial.println(IR5.value);
  // Serial.println(String(IR1.value) + ", " + String(IR2.value) + ", " + String(IR3.value) + ", " + String(IR4.value) + ", " + String(IR5.value));
}

There are a number of of code examples on line for how to do that.

Fix one problem at time, test the robot, then fix another.

Sorry all for not replying, i was busy somewhere else , thank you all for your replies and i would like to tell that i will be using the left hand rule for solving maze and this is because there are not many loops in the maze it will be a simple maze made in the area of 2.3m * 2.3m (l*b), and it can be solved by simple left hand rule, i will be storing the turn in a string whenever taking a turn or moving anywhere at a node, and a good news the PID is working now perfectly , here is the updated code :

#define RMotPin1 6
#define RMotPin2 5
#define LMotPin1 8
#define LMotPin2 7
#define LMotSpeedPin 10
#define RMotSpeedPin 9
int MOTOR_SPEED = 101;

#define IR_MAX 710
#define IR_MIN 15

unsigned long noLineStart = 0;

int leftMotorSpeed = MOTOR_SPEED;
int rightMotorSpeed = MOTOR_SPEED;

float Kp = 10.0; // Proportional constant
float Ki = 0.5; // Integral constant
float Kd = 1.0; // Derivative constant

float previousError = 0;
float integral = 0;

struct IRSensor
{
  IRSensor(int _pin, bool _flipped = false) : m_pin(_pin), m_flipped(_flipped) {}

  int value = 0;
  int update() { value = analogRead(m_pin); return value; }

  inline bool isBlack() { if(m_flipped) return value < THRESHOLD; else return value > THRESHOLD; }
  inline bool isWhite() { return !isBlack(); }

  static const int THRESHOLD = (IR_MIN + IR_MAX) * 0.5;

  private:
  int m_pin;
  bool m_flipped;
};

// IR Sensors
IRSensor IR1(A0); // Far left
IRSensor IR2(A1); // Left
IRSensor IR3(A2, true); // Center
IRSensor IR4(A3); // Right
IRSensor IR5(A4); // Far right

void updateIR()
{
  IR1.update();
  IR2.update();
  IR3.update();
  IR4.update();
  IR5.update();
}

void displayIR()
{
  Serial.println(String(IR1.value) + ", " + String(IR2.value) + ", " + String(IR3.value) + ", " + String(IR4.value) + ", " + String(IR5.value));
}

void rotateMotor(int rightMotorSpeed, int leftMotorSpeed)
{
  if (rightMotorSpeed < 0)
  {
    digitalWrite(RMotPin1, HIGH);
    digitalWrite(RMotPin2, LOW);
  }
  else if (rightMotorSpeed > 0)
  {
    digitalWrite(RMotPin1, LOW);
    digitalWrite(RMotPin2, HIGH);
  }
  else
  {
    digitalWrite(RMotPin1, LOW);
    digitalWrite(RMotPin2, LOW);
  }

  if (leftMotorSpeed < 0)
  {
    digitalWrite(LMotPin1, HIGH);
    digitalWrite(LMotPin2, LOW);
  }
  else if (leftMotorSpeed > 0)
  {
    digitalWrite(LMotPin1, LOW);
    digitalWrite(LMotPin2, HIGH);
  }
  else
  {
    digitalWrite(LMotPin1, LOW);
    digitalWrite(LMotPin2, LOW);
  }

  analogWrite(RMotSpeedPin, abs(rightMotorSpeed));
  analogWrite(LMotSpeedPin, abs(leftMotorSpeed));
}

void PID()
{
  int error = 0;

  if (IR1.isBlack()) error += 2; // Far left sensor (higher weight)
  if (IR2.isBlack()) error += 1; // Left sensor
  if (IR4.isBlack()) error -= 1; // Right sensor
  if (IR5.isBlack()) error -= 2; // Far right sensor (higher weight)

  Serial.println(error);

  // Integral term with clamping to prevent windup
  integral += error;
  integral = constrain(integral, -50, 50); // Clamping the integral term

  // PID output
  float correction = (error * Kp) + (integral * Ki) + ((error - previousError) * Kd);

  // Adjust motor speeds based on correction
  leftMotorSpeed = MOTOR_SPEED - correction;
  rightMotorSpeed = MOTOR_SPEED + correction;

  // Clamp motor speeds for better control
  leftMotorSpeed = constrain(leftMotorSpeed, 0, 255);
  rightMotorSpeed = constrain(rightMotorSpeed, 0, 255);

  rotateMotor(rightMotorSpeed, leftMotorSpeed);

  previousError = error;
}

void Left()
{
  // Improved left turn logic with sensor feedback
  rotateMotor(150, 0);
  delay(150); // Initial turn
  while (!IR3.isBlack()) updateIR(); // Continue turning until the center sensor detects the line
  rotateMotor(0, 0); // Stop motors
}

void Right()
{
  // Improved right turn logic with sensor feedback
  rotateMotor(0, 150);
  delay(150); // Initial turn
  while (!IR3.isBlack()) updateIR(); // Continue turning until the center sensor detects the line
  rotateMotor(0, 0); // Stop motors
}

void StepForward()
{
  rotateMotor(MOTOR_SPEED, MOTOR_SPEED); // Move forward
  delay(200); // Short step delay for stabilization
  rotateMotor(0, 0); // Stop motors
}

void UTurn()
{
  // Improved U-turn with feedback
  rotateMotor(MOTOR_SPEED, -MOTOR_SPEED);
  delay(300); // Start turning
  while (!IR3.isBlack()) updateIR(); // Complete U-turn when center sensor detects the line
  rotateMotor(0, 0);
  Serial.println("UTurn");
}


void detectNode()
{
  const bool farLeft = IR1.isBlack();
  const bool nearLeft = IR2.isBlack();
  const bool center = IR3.isBlack();
  const bool nearRight = IR4.isBlack();
  const bool farRight = IR5.isBlack();

  // Case: No line detected for all sensors
  if (!farLeft && !nearLeft && !center && !nearRight && !farRight)
  {
    static unsigned long noLineStart = 0;
    if (noLineStart == 0)
      noLineStart = millis();

    // If the line is not detected for over 300ms, perform a U-turn
    if (millis() - noLineStart > 300)
    {
      UTurn();
      delay(300);
      StepForward();
      noLineStart = 0; // Reset the timer
      return;
    }
  }
  else
  {
    noLineStart = 0; // Reset timer if any line is detected
  }

  // Node detection: When multiple sensors detect black
  const bool isNode = (farLeft + nearLeft + center + nearRight + farRight) > 2;

  if (!isNode)
  {
    PID(); // Continue normal line following
    return;
  }

  Serial.println("Node detected");

  // Handling turn decisions based on detected paths
  if (nearLeft){
      MOTOR_SPEED = 89;
  }
  else if (nearRight){
      MOTOR_SPEED = 89;
  }
  else if (farLeft && !farRight)
  {
    Left();        // Perform left turn
    Serial.println("Turning Left");
    MOTOR_SPEED = 150;
  }
  else if (farRight && !farLeft)
  {
    Right();       // Perform right turn
    Serial.println("Turning Right");
    MOTOR_SPEED = 150;
  }
  else
  {
    StepForward(); // Move forward if neither far left nor far right is strongly detected
    Serial.println("Moving Forward");
  }
}

void setup()
{
  Serial.begin(9600);
  Serial.println("Hello");

  pinMode(RMotSpeedPin, OUTPUT);
  pinMode(RMotPin1, OUTPUT);
  pinMode(RMotPin2, OUTPUT);

  pinMode(LMotSpeedPin, OUTPUT);
  pinMode(LMotPin1, OUTPUT);
  pinMode(LMotPin2, OUTPUT);

  rotateMotor(0, 0);
}

void loop()
{
  updateIR();
  //displayIR();
  detectNode();
}

the code you marked out is because i have one IR sensor which is very old around 4-5yrs old it is working fine but but the problem was it was giving the flipped value meaning the new IR sensor was giving values for black around 600-700 but the old IR sensor was giving 60-70 so i wrote a code to flip it, and it worked out