Project tutorial
PID Controlled DC Engine Test Rig

PID Controlled DC Engine Test Rig © GPL3+

This is an example problem to illustrate the function of a PID controller. You will learn the basics to control the speed of a DC motor.

  • 609 views
  • 0 comments
  • 6 respects

Components and supplies

Necessary tools and machines

3drag
3D Printer (generic)

Apps and online services

About this project

Hello,

The main idea behind this project is to control the rotational speed of a DC motor by using a PID controller. Maybe you think that this is not a big deal since the speed of a DC engine can be influenced by the voltage. So if I want to increase the speed I just have to increase the voltage. This is true, but the problem is that the voltage does not define the speed. If for example, the voltage of a DC engine of an electric car is constant the velocity will also be constant as long as no mountain appears. In the case of a changing slope, the voltage also has to be changed to guarantee constant speed. Everyone who ever drives a bicycle knows this. If we want to control the speed of a DC motor the first we have to do is measure the actual speed. Let us do this in rounds per minute and we call this value "rpm". After this, we can define the target speed which we call "Setpoint". Now we have to think about how it is possible that the actual speed becomes the target speed. Since we know that an increase of the voltage results in an increase of the actual speed the solution can look like this. If "rpm" is smaller than "Setpoint" the voltage has to be higher and if "rpm" is higher than "Setpoint" the voltage has to be lower. Mathematically this can be written as:

voltage = P * (Setpoint-rpm)

or

voltage = P * error.

This is a so-called P controller since the control value (voltage) is proportional to the error wich is (Setpoint-rpm). The PID controller also takes into account the integral (I) and the derivative (D) of the error. To integrate the error over time means to calculate the sum over time. Therefore this part of the controller is well to handle situations where the error is small but over a long period of time. The derivative of the error indicates how fast it changes. By applying this part to the controller it is possible to add damping to the system. This can help if the system tends to oscillate. The whole PID controller is given by the equation

.voltage = P * error + I * Integral(error)+ D * Derivative(error).

For a much better description of the PID controller please take a look at Wiki

https://en.wikipedia.org/wiki/PID_controller

Let us now go back to the example. In the next section, I will show you how the test rig is assembled, how the sensor works and how we implement a PID controller in the Arduino.

Assembling of the test rig

All parts of the frame are 3D printed and can be downloaded here:

https://www.thingiverse.com/thing:3921744

If you don’t have a 3D Printer please feel free to build a similar frame of wood or something else. All information how the parts should be printed is written on Thingiverse. How the parts will be assembled is shown in the next pictures.

Wiring

Code

To run the code two external libraries are needed.

For the PID controller:

https://github.com/br3ttb/Arduino-PID-Library/

For the LCD display:

https://www.arduinolibraries.info/libraries/liquid-crystal-i2-c

If you are not familiar with the installation of external libaries look here:

https://www.arduino.cc/en/Guide/Libraries#toc4

The whole program description:

//////////PID///////////
#include <PID_v1.h>
//////////Display///////////
#include <LiquidCrystal_I2C.h>
//////////PID///////////
//Define variable
double Setpoint = 0, Input = 0, Output = 0;
//Define the PID named myPID
PID myPID(&Input, &Output, &Setpoint, 1, 1, 0, DIRECT);
//////////Display///////////
LiquidCrystal_I2C lcd(0x27, 20, 4); // set the LCD address to 0x27 for a 16 chars and 2 line display
unsigned long t3 = 0;
unsigned long update_time = 200;
/////Sensor////
int LDR_Pin = A0;
unsigned long t1, t2;
unsigned long braketime = 300000;
unsigned long dt = 0;
unsigned long rpm = 0;
int wait_time = 2;
/////Driver////
//channels
const int pwm = 9;
const int in_1 = 5;
const int in_2 = 6;
const int led_pin = 13;
//////Poti/////
int Pot_pin = A2;
void setup() {
 lcd.init();                      // initialize the lcd
 lcd.backlight();
 myPID.SetMode(AUTOMATIC);       //turn the PID on
 Serial.begin(9600);
 pinMode(pwm, OUTPUT);           //define channels
 pinMode(in_1, OUTPUT);
 pinMode(in_2, OUTPUT);
 pinMode(led_pin, OUTPUT);
 digitalWrite(led_pin, HIGH);    //turn led on
}
void loop() {
 //Define the Setpoint
 //Setpoint = (sin(3.0 * millis() / (1000.0 * 2 * 3.14)) + 2.0 ) * 500.0;
 //Setpoint = 0;
 Setpoint = analogRead(Pot_pin) * 5.0;
 ////////measure and calculate rpm////////
 /////////////////////////////////////////
 //detecting the time dt which passes between two holes
 //1. starting at an unknown state and wait until the photoresistor indicates that the light barrier is blocked
 t2 = micros();                          //t2 indicates the starting time of the loop
 while (analogRead(LDR_Pin) < 850) {     //The loop runs until the LDR signal is lower than the threshold of 850
   if (micros() - t2 > braketime) {      //If the loop runs longer than braketime e.g. the rotor did not move the loop stopped
     break;
   }
 }
 delay(wait_time);                       //this delay time is needed due to the noise in the signal
 //2. wait until the next hole appears and save this time to the variable t1
 t2 = micros();                        
 while (analogRead(LDR_Pin) > 850) {     //The loop runs until the LDR signal is higher than the threshold of 850
   if (micros() - t2 > braketime) {      
     break;
   }
 }
 t1 = micros();                          //save the time when the first hole appear to t1
 delay(wait_time);                       //this delay time is needed due to the noise in the signal
 //3. wait until the hole disappears
 t2 = micros();                        
 while (analogRead(LDR_Pin) < 850) {     //The loop runs until the LDR signal is lower than the trashhold of 850
   if (micros() - t2 > braketime) {      
     break;
   }
 }
 delay(wait_time);                       //this delay time is needed due to the noise in the signal
 //4.wait until the next hole appears
 t2 = micros();                          
 while (analogRead(LDR_Pin) > 850) {     //The loop runs until the LDR signal is higher than the trashhold of 850
   if (micros() - t2 > braketime) {
     break;
   }
 }
 dt = micros() - t1;                     //calculate the time between the two holes
 delay(wait_time);         
 rpm = (1.0 / (dt / 60000000.0)) / 2.0;  //calculate the rpm 
 //use the PID controller 
 Input = rpm;
 myPID.Compute();
 digitalWrite(in_1, HIGH);               //Defining the turning direktion
 digitalWrite(in_2, LOW);
 analogWrite(pwm, Output);               //apply the Output to the DC motor
 Serial.print(rpm - Setpoint);           
 Serial.print(",");
 Serial.print(rpm);
 Serial.print(",");
 Serial.print(Setpoint);
 Serial.print(",");
 Serial.println(Output);
 //show results at the LCD
 if (millis() - t3 > update_time)
 {
   t3 = millis();
   lcd.clear();
   lcd.setCursor(0, 0);
   lcd.print("set rpm");
   lcd.setCursor(8, 0);
   lcd.print(int(Setpoint));
   lcd.setCursor(13, 0);
   lcd.print("U/m");
   lcd.setCursor(0, 1);
   lcd.print("act rpm");
   lcd.setCursor(8, 1);
   lcd.print(int(rpm));
   lcd.setCursor(13, 1);
   lcd.print("U/m");
 }
} 

How the sensor works:

The sensor to measure the speed or the revolutions per minute (rpm) is designed as a light barrier. Therefore an LED is placed towards a photoresistor (LDR) which detects the light coming from the LED. Between the LED and the LDR, the rotor is placed. The Rotor has two holes which allow the light passing to the LDR two times per revolution. If we wire the LED and the LDR as shown in the circuit diagram and plot the output of channel A0 by

Serial.println(analogRead(A0));

we see this at the plot monitor:

Every time when one of the holes passes the light barrier the value jumps to approximately 930 and if the light is blocked by the rotor the value goes down to under 800. The time between two pikes indicates the time "dt" which the rotor needs for a half rotation. So if we know this time for example in microseconds we can calculate the actual revolutions per minute by

rpm = (60 * 10^6)/(2 * dt)

To get the time between two pikes the following code is used which includes 4 steps.

The 1. step is to wait until we are between two peaks. This is done by the while loop with the condition that the Chanel A0 is lower than 850. To ensure that this loop will not run forever in the case of a still, stand of the rotor the loop break after the previously defined "braketime". In the 2. step the program waits until the next peak appears and then saves this time as t1. Then in the 3. step, the program waits until this peak disappears. Finally, in the 4. step we wait until the next peak appears and save the actual time minus t1 as "dt". Maybe you are supreased about all this delay time in the main loop. The problem is that if you just check the condition of the LDR ones in the main loop it is possible that the loop time is too long and you miss one peak. another possibility is to use an interrupt.

////////measure and calculate rpm////////
 /////////////////////////////////////////
 //detecting the time dt which passes between two holes
 //1. starting at an unknown state and wait until the photoresistor indicates that the light barrier is blocked
 t2 = micros();                          //t2 indicates the starting time of the loop
 while (analogRead(LDR_Pin) < 850) {     //The loop runs until the LDR signal is lower than the threshold of 850
   if (micros() - t2 > braketime) {      //If the loop runs longer than braketime e.g. the rotor did not move the loop stopped
     break;
   }
 }
 delay(wait_time);                       //this delay time is needed due to the noise in the signal
 //2. wait until the next hole appears and save this time to the variable t1
 t2 = micros();                        
 while (analogRead(LDR_Pin) > 850) {     //The loop runs until the LDR signal is higher than the threshold of 850
   if (micros() - t2 > braketime) {      
     break;
   }
 }
 t1 = micros();                          //save the time when the first hole appear to t1
 delay(wait_time);                       //this delay time is needed due to the noise in the signal
 //3. wait until the hole disappears
 t2 = micros();                        
 while (analogRead(LDR_Pin) < 850) {     //The loop runs until the LDR signal is lower than the trashhold of 850
   if (micros() - t2 > braketime) {      
     break;
   }
 }
 delay(wait_time);                       //this delay time is needed due to the noise in the signal
 //4.wait until the next hole appears
 t2 = micros();                          
 while (analogRead(LDR_Pin) > 850) {     //The loop runs until the LDR signal is higher than the trashhold of 850
   if (micros() - t2 > braketime) {
     break;
   }
 }
 dt = micros() - t1;                     //calculate the time between the two holes
 delay(wait_time);         
 rpm = (1.0 / (dt / 60000000.0)) / 2.0;  //calculate the rpm 

How the PID controller is implemented:

With

#include <PID_v1.h>

The library https://github.com/br3ttb/Arduino-PID-Library/ of the PID controller is included. More about this library can be found here: http://playground.arduino.cc/Code/PIDLibrary. To define our PID controller we us these two lines before the setup loop.

double Setpoint = 0, Input = 0, Output = 0;

PID myPID(&Input, &Output, &Setpoint, 1, 1, 0, DIRECT);

In the first line, 3 variables are defined. Setpoint is equivalent to our target speed which we called. Input is the measured speed and Output is the voltage or the control variable. In the second line, the PID controller is initialized and called myPID. The first 3 arguments are the storage addresses of the 3 variables we defined before. If we later change the values of those variables the values in the PID controller are automatically updated. The next 3 arguments are P, I and D parameters of the PID controller. Now it is set to P=1, I=1, and D=0. Please feel free to adjust these parameters.

In the setup loop the PID controller has to be turned on by using

myPID.SetMode(AUTOMATIC);

Finally in the main loop we write:

Input = rpm;

myPID.Compute();

digitalWrite(in_1, HIGH);

digitalWrite(in_2, LOW);

analogWrite(pwm, Output);

First the measured rpm is saved as Input and then the Output is computed by myPID.Compute();. Output indicates now how much voltage we have to apply to the DC motor.

To apply the voltage to the DC motor we used a H-Bridge (L293D). This device is able to control two DC engines but we only need one. If the Bridge is wired as shown in the figure at the beginning the channels in_1=5 and in_2=6 defined the turning direction and channel pwm=9 the voltage. With

analogWrite(pwm, Output);

the calculated Output is then applied to the DC motor. For more information about the L293D H-Bridge see

https://starthardware.org/motorsteuerung-mit-einem-h-bridge-ic/

How the target speed is defined:

To Defining the target speed a rotary potentiometer is used. For more information how this is done see https://www.arduino.cc/en/tutorial/potentiometer. In this project, we need just one line to do this.

Setpoint = analogRead(Pot_pin) * 5.0;

The analog pin Pot_pin=A3 is read multiplied by 5 and then stored as "Setpoint". "Setpoint" was previously defined as target speed for the PID controller.

Producing nice results:

To show the results an LCD display is used. The display shows the target speed and the actual speed. This is done by using the library. https://www.arduinolibraries.info/libraries/liquid-crystal-i2-c

To understand how this works please just look at the examples in the library.

To see more results the serial plotter from the Arduino IDE is used.

Serial.print(rpm - Setpoint);

Serial.print(",");

Serial.print(rpm);

Serial.print(",");

Serial.print(Setpoint);

Serial.print(",");

Serial.println(Output);

The figure shows the target speed (Setpoint) in green, the actual speed (rpm) in red, the error (rpm – Setpoint) in blue and the Output in orange. You can see how the red line always try to follow the green line but if the target speed changes too fast there’s a delay.

now have fun!

Code

PID Controlled DC motorC/C++
//////////PID///////////
#include <PID_v1.h>

//////////Display///////////
#include <LiquidCrystal_I2C.h>

//////////PID///////////
//Define variable
double Setpoint = 0, Input = 0, Output = 0;
//Define the PID named myPID
PID myPID(&Input, &Output, &Setpoint, 1, 1, 0, DIRECT);

//////////Display///////////
LiquidCrystal_I2C lcd(0x27, 20, 4); // set the LCD address to 0x27 for a 16 chars and 2 line display
unsigned long t3 = 0;
unsigned long update_time = 200;

/////Sensor////
int LDR_Pin = A0;
unsigned long t1, t2;
unsigned long braketime = 300000;
unsigned long dt = 0;
unsigned long rpm = 0;
int wait_time = 2;


/////Driver////
//channels
const int pwm = 9;
const int in_1 = 5;
const int in_2 = 6;
const int led_pin = 13;

//////Poti/////
int Pot_pin = A2;

void setup() {

  lcd.init();                      // initialize the lcd
  lcd.backlight();

  myPID.SetMode(AUTOMATIC);       //turn the PID on

  Serial.begin(9600);
  
  pinMode(pwm, OUTPUT);           //define channels
  pinMode(in_1, OUTPUT);
  pinMode(in_2, OUTPUT);
  pinMode(led_pin, OUTPUT);

  digitalWrite(led_pin, HIGH);    //turn led on
}

void loop() {

  //Define the Setpoint
  //Setpoint = (sin(3.0 * millis() / (1000.0 * 2 * 3.14)) + 2.0 ) * 500.0;
  //Setpoint = 0;
  Setpoint = analogRead(Pot_pin) * 5.0;

  ////////measure and calculate rpm////////
  /////////////////////////////////////////
  //detecting the time dt which passes between two holes
  
  //1. starting at an unknown state and wait until the photoresistor indicates that the light barrier is blocked
  t2 = micros();                          //t2 indicates the starting time of the loop
  while (analogRead(LDR_Pin) < 850) {     //The loop runs until the LDR signal is lower than the threshold of 850
    if (micros() - t2 > braketime) {      //If the loop runs longer than braketime e.g. the rotor did not move the loop stopped
      break;
    }
  }
  delay(wait_time);                       //this delay time is needed due to the noise in the signal
  //2. wait until the next hole appears and save this time to the variable t1
  t2 = micros();                        
  while (analogRead(LDR_Pin) > 850) {     //The loop runs until the LDR signal is higher than the threshold of 850
    if (micros() - t2 > braketime) {      
      break;
    }
  }
  t1 = micros();                          //save the time when the first hole appear to t1
  delay(wait_time);                       //this delay time is needed due to the noise in the signal
  //3. wait until the hole disappears
  t2 = micros();                        
  while (analogRead(LDR_Pin) < 850) {     //The loop runs until the LDR signal is lower than the trashhold of 850
    if (micros() - t2 > braketime) {      
      break;
    }
  }
  delay(wait_time);                       //this delay time is needed due to the noise in the signal

  //4.wait until the next hole appears
  t2 = micros();                          
  while (analogRead(LDR_Pin) > 850) {     //The loop runs until the LDR signal is higher than the trashhold of 850
    if (micros() - t2 > braketime) {
      break;
    }
  }
  dt = micros() - t1;                     //calculate the time between the two holes
  delay(wait_time);         
  rpm = (1.0 / (dt / 60000000.0)) / 2.0;  //calculate the rpm 

  //use the PID controller 
  Input = rpm;
  myPID.Compute();
  digitalWrite(in_1, HIGH);               //Defining the turning direktion
  digitalWrite(in_2, LOW);
  analogWrite(pwm, Output);               //apply the Output to the DC motor

  Serial.print(rpm - Setpoint);           
  Serial.print(",");
  Serial.print(rpm);
  Serial.print(",");
  Serial.print(Setpoint);
  Serial.print(",");
  Serial.println(Output);

  //show results at the LCD
  if (millis() - t3 > update_time)
  {
    t3 = millis();
    lcd.clear();
    lcd.setCursor(0, 0);
    lcd.print("set rpm");
    lcd.setCursor(8, 0);
    lcd.print(int(Setpoint));
    lcd.setCursor(13, 0);
    lcd.print("U/m");
    lcd.setCursor(0, 1);
    lcd.print("act rpm");
    lcd.setCursor(8, 1);
    lcd.print(int(rpm));
    lcd.setCursor(13, 1);
    lcd.print("U/m");
  }
}

Schematics

wiring
All
Wiring hnrw9h3ll2

Comments

Similar projects you might like

Clap Controlled DC Motor

Project tutorial by Nakshatra401

  • 1,461 views
  • 1 comment
  • 7 respects

Touch Controlled Light Using Arduino

Project tutorial by Amal Mathew

  • 10,857 views
  • 5 comments
  • 14 respects

Alexa Controlled LEDs Through Raspberry Pi

Project tutorial by vincent wong

  • 6,847 views
  • 6 comments
  • 28 respects

BLDC Brushless DC Motor and Slipring

Project showcase by mbarraus

  • 6,007 views
  • 3 comments
  • 11 respects

Tricks for Controlling DC Motors

Project tutorial by tolgadurudogan

  • 26,631 views
  • 7 comments
  • 90 respects

Motor Controlled with Arduino

by Benjamin Larralde

  • 25,276 views
  • 20 comments
  • 68 respects
Add projectSign up / Login