Hi,
I am a beginner in Arduino programming. I want to control a constant speed of a DC motor with PID controller and encoder
my idea is:
Arduino controls the motor driver of the DC motor (target speed).
Encoder reports speed to Arduino (actual speed).
PID controller regulates the speed constantly.
I have this code below:
#include <Encoder.h> //encoder library
#include <PID_v1.h> // PID library
// DC24V40RPM DC motor and MD30C R2 30A DC Motor Driver
#define DIR 10 // DIR motor Driver
#define PWM_OUTPUT 12 // PWM motor Driver
// PIR
double Setpoint, Input, Output;
double Kp=2, Ki=5, Kd=1;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);
const int PPR = 100; //define number of pulses in one round of encoder
// Rotary encoder Type: HQK38H6-100N-G5-24
#define EN_A 2
#define EN_B 3
Encoder Enc(EN_A, EN_B);
//Time variables
long previousMillis = 0;
long currentMillis = 0;
//Encoder variables
volatile long currentEncoder;
volatile long previousEncoder = 0;
volatile long oldPosition = 0;
volatile long newPosition;
float old_rot_speed=0;
float rot_speed;
void setup()
{
//Input = analogRead(PIN_INPUT);
Setpoint = 100;
Serial.begin(9600);
// pinMode(DIR, OUTPUT);
pinMode(EN_A, INPUT_PULLUP);
pinMode(EN_B, INPUT_PULLUP);
Serial.println("TwoKnobs Encoder Test:");
//turn the PID on
myPID.SetMode(AUTOMATIC);
myPID.SetOutputLimits(0,255);
}
float read_speed(void)
{
const int PPR = 100; //define number of pulses in one round of encoder
currentEncoder = Enc.read();
const int interval = 1000; //choose interval is 1 second (1000 milliseconds)
currentMillis = millis();
if ((currentMillis - previousMillis) > interval){
previousMillis = currentMillis;
rot_speed = ((currentEncoder - previousEncoder)*60)/PPR; //rotating speed in rpm
previousEncoder = currentEncoder;
return rot_speed;
}
}
void loop() {
//Input = analogRead(PIN_INPUT);
Input = read_speed();
myPID.Compute();
analogWrite(PWM_OUTPUT, Output);
Serial.print("Input = ");
Serial.print(Input);
Serial.println();
Serial.print("Output = ");
Serial.print(Output);
Serial.println();
}
Unfortunately the motor doesn't turn properly. sometimes spins at 255 and sometimes at 150... and sometimes doesn't spin. but ı cann't find whats wrong
Could you help my in this issue ?
Please show your output. We don't have your motor (they vary wildly in size, speed, and inertia) or wiring here, so it's hard to tell exactly what is wrong.
You might try reducing these parameters:
I'd try "Kp = 1, Ki =0, Kd =0; " to eliminate integral windup as a possibility.
The Kp is the proportional constant, and should do most of the ramping-up work to dial up toward whatever is the right PWM for 100.
I believe that this read_speed() function is responsible for the problem of the motor spinning and then constantly stopping For example if I increase interval = 2000 -->motor spins 2 second then stops 2 second steadily. do you know how to optimize this function read_speed()?
Yes, if you don't have a reliable speed measurement, you can't use it for control.
Looking deeper, when you got speed results, you get 138RPM, which would mean 138*100*60/60 = 13800 pulses per second\\\minute. At target speed, that's 10000\\\167 pulses per second. You could sample much faster than once per second. I'd include the sampling interval in the calculation, and shorten the interval significantly. (And do a smoothing.)
I'd separate the reporting from the updating and maintain a state variable for motor speed.
float read_speed(void)
{
static float currentSpeed = 0;
const float alpha = 0.2 ; // EWMA smoothing constant;
const int PPR = 100; //define number of pulses in one round of encoder
const int interval = 100; //choose interval is 1 second (1000 milliseconds)
currentMillis = millis();
if ((currentMillis - previousMillis) > interval){
previousMillis = currentMillis;
currentEncoder = Enc.read();
rot_speed = ((currentEncoder - previousEncoder)*60*1000 )/PPR/interval; //rotating speed in rpm
previousEncoder = currentEncoder;
//return rot_speed;
currentSpeed += alpha * (rot_speed - currentSpeed); // EWMA smoothing
}
return currentSpeed;
}
I'd also wrap the reporting in a millis() conditional, and speed up loop:
To increase responsiveness of the read_speed, instead of commenting out the if statement completely out, bump the interval down to 10 or 5 or 1ms. And maybe increase the EWMA parameter up to 0.5 or 0.8 or all the way to 1.0.
If the motor is cycling on and off at 100ms, speed up the reporting interval to 50ms (you may need to up the baud rate like Serial.begin(115200);)
Another thing to try before going too far with PID, is to try controlling it manually with a potentiometer (or with hardcoded constants if you don't have a potentiometer) to see how your motor runs:
If the system is too touchy, it could be hard to tune a PID to control it properly. With a manual potentiometer, you should be able to find at what PWM level the motor starts turning, the motor's top speed , and maybe the value that the PID should output for steady state at 100RPM.
Oh, these aren't the problem, but they should be type "unsigned long" for storing millis() results
(Edited code to uncomment overriding analogWrite(PWM_OUTPUT,Output); )
Here is my working version of a similar sketch to the one you wrote
// This version of Tacometer auto adjusts for difference in pulses and has Stall Recovery
#include <PID_v3.h> // uses my new version of PID_v1
// Adjust these variables:
int PulsesPerRevolution = 400;// From Tachometer How Many Pulses Pur Revolution
int SampleDuration = 30; // in Milliseconds -- How often do you want to trigger the PID Lower the number the more detailed control but the more the arduino processor time is used up
double consKp = 1, consKi = 0.01, consKd = 1;
double Setpoint = 3000;// RPM
#define PWMpin 3
#define interruptPin 2
#define HBridgeEnable pinMode(4, OUTPUT);digitalWrite(4, HIGH); // enables my H-Bridge Remove line 35 also
//Define Variables we'll be connecting to
double Input, Output;
//Specify the links and initial tuning parameters
PID myPID(&Input, &Output, &Setpoint, consKp, consKi, consKd, DIRECT);
// Varables used for Calculations
volatile unsigned long timeX = 1;
unsigned long StallTimer = SampleDuration *3;
volatile int Counts = 1;
double PulsesPerMinute;
volatile unsigned long LastTime;
volatile int PulseCtrX;
int PulseCtr;
unsigned long Counter;
unsigned long Time;
void setup() {
Serial.begin(115200);
Serial.println("Test Tachometer");
PulsesPerMinute = (60 * 1000000) / (PulsesPerRevolution / Counts);// initialize
pinMode(interruptPin, INPUT_PULLUP);
pinMode(PWMpin, OUTPUT);
HBridgeEnable // Remove this if you don't need it
attachInterrupt(digitalPinToInterrupt(interruptPin), sensorInterrupt, FALLING);
myPID.SetSampleTime(1);
myPID.SetOutputLimits(0, 255); // Standard PWM Range
myPID.SetMode(AUTOMATIC);
}
void loop() {
readRpm();
static unsigned long SpamTimer;
if ((unsigned long)(millis() - SpamTimer) >= 15000) {
SpamTimer = millis();
Setpoint = (Setpoint < 5000) ? 7000 : 3000;
}
}
// New version of sensorInterrupt
void sensorInterrupt()
{
static int Ctr;
unsigned long Time;
Ctr++;
if (Ctr >= Counts) { // so we are taking an average of "Counts" readings to use in our calculations
Time = micros();
timeX += (Time - LastTime); // this time is accumulative ovrer those "Counts" readings
LastTime = Time;
PulseCtrX ++; // will usually be 1 unless something else delays the sample
Ctr = 0;
}
}
void readRpm()
{
static unsigned long STime;
if (!PulseCtrX) {
if ((unsigned long)(millis() - STime) >= StallTimer) {
Time = micros();
timeX += (Time - LastTime); // this time is accumulative ovrer those "Counts" readings
LastTime = Time;
PulseCtrX ++; // will usually be 1 unless something else delays the sample
Serial.print(" Stall \t");
//Setpoint +=100;
} else return; // << Added lets not stop interrupts unless we know we are ready (keep other code happy).
}
cli (); // clear interrupts flag
Time = timeX; // Make a copy so if an interrupt occurs timeX can be altered and not affect the results.
timeX = 0;
PulseCtr = PulseCtrX ;
PulseCtrX = 0;
sei (); // set interrupts flag
if (PulseCtr > 0) {
Input = (double) (PulsesPerMinute / (double)(( (unsigned long)Time ) * (unsigned long)PulseCtr)); // double has more percision
// PulseCtr = 0; // set pulse Ctr to zero
AverageCapture(Input);
debug();
myPID.Compute();
analogWrite(PWMpin, Output);
long DSDur = ((long) Time * (long)PulseCtr) - ((long) SampleDuration * 1000L);
Counts -= DSDur * .015;
Counts = max(Counts, max(1, PulsesPerRevolution * .25));
Time = 0; // set time to zero to wait for the next rpm trigger.
Counter += PulseCtr;
PulseCtr = 0; // set pulse Ctr to zero
PulsesPerMinute = (60.0 * 1000000.0) / (double)((double)PulsesPerRevolution / (double)Counts);
STime = millis();
}
}
float AvgArray[100];
int Readings = 0;
void AverageCapture(float in) {
static int Position = 0;
AvgArray[Position] = in;
Position++;
Readings++;
Readings = min (100, Readings); // 100 readings 0-99;
if (Position >= 100)Position = 0;//99 spots
}
float AverageValue() {
float Total = 0;
float Average;
if (!Readings)return (0.0);
for (int Position = 0; Position < Readings; Position++) {
Total += AvgArray[Position];
}
Average = Total / Readings;
return (Average);
}
void debug(){
char S[20];
for (static long QTimer = millis(); (long)( millis() - QTimer ) >= 100; QTimer = millis() ) { // one line Spam Delay at 100 miliseconds
Serial.print("Counts: "); Serial.print(Counts );
// Serial.print(" Target RPM: ");Serial.print(RPM );
// Serial.print(" Counts: "); Serial.print(Counts );
// Serial.print(" time: "); Serial.print(Time );
// Serial.print(" DeltaT: "); Serial.print(Time * PulseCtr);
// Serial.print(" PulseCtr: "); Serial.print(PulseCtr );
// Serial.print(" PulsesPerMinute: "); Serial.print(dtostrf(PulsesPerMinute, 6, 1, S));
Serial.print("\t Average: "); Serial.print(dtostrf(AverageValue(), 6, 1, S));
// Serial.print(" Setpoint: "); Serial.print(dtostrf(Setpoint, 6, 1, S) );
// Serial.print(" Calculated RPM: "); Serial.print(dtostrf(Input, 6, 1, S));
// Serial.print(" Output: "); Serial.print(dtostrf(Output, 6, 2, S));
Serial.print("\t ");
// Serial.println();
}
}
the library PID_v3
has no timing issues
This drove a small motor attached directly to a 400 pulse per revolution encoder
the motor controller is just an H-bridge
Thanks very much. I have a question.
if i change Setpoint value, the speed doesn't change.
in your sketch Setpoint is equal to 3000. is not speed from 0 to 255?
how can I determine the PID parameters double consKp = 1, consKi = 0.01, consKd = 1;?
The pid parameters all convert input error units to output units, with the Ki and Kd terms including the time. Setting them is “tuning” and depends on the dynamic response of your system.
If a PWM output of 147 counts gets you a speed measure of 100RPM, then Kp is in units of counts/rpm an can specify where you start tapering down from full power. If you want full power up to 50 rpm and then to taper down to 100RPM then kp =255counts/50rpm=5.1count/rpm.
Ki converts integrated error to output units, and at steady state at target, is the sole term that is providing the output signal.
To reach an output of 147 counts with a
Ki=0.1counts/(rpm seconds) would mean it needed to integrate 1470 rpm*sec worth of error to reach steady state. If you go too aggressive with ki, it will oscillate around the target.
Kd is in units of counts/(rpm/sec) and tries to look forward and counteract rapid changes. Most of the time you can ignore it
Since the terms interact and are dependent on sampling time, it’s very important to know how your system reacts and how you want it to respond.
If your system hits full speed in 50ms, that’s important.
I start with getting my delta-t and kp close, then fiddle with ki.
What to do next:
You will want to set the pulses per revolution to 100 to match your encoder
next, if you have a fixed setpoint remove the setpoint changing routine I placed in the loop()
I found it easier to tune with the motor speed changing every 15 seconds so I could see how the integral response and the derivative reacts to the change.
Start your PID tuning with Kp = 0 and Kd = 0 Ki= 0.1 and see how long it takes to reach the setpoint. Ki of 0.1 may be too much if it oscillates start at 0.01.
note that input is the actual RPM and the PID magically converts it into a number between 0 and 255 for the analogWrite() myPID.SetOutputLimits(0, 255); // Standard PWM Range
The PID:
Proportional is instantaneous and no time factor is required to adjust this value.
if your error from the setpoint is 0 (you are on setpoint) Proportional output is ZERO. so Proportional is not good at controlling the setpoint of a motor that needs something other than zero to keep the speed at setpoint.
Derivative requires change between samples.
My Sample duration is: int SampleDuration = 30; // in Milliseconds --
If there is no change between samples the output is ZERO so Derivative can't be our primary control method.
Integral on the other hand is perfect to control our motor to achieve setpoint. each 30 mS cycle a portion of the error is added to the output. If the error is negative a positive value is added to the output (This could cause windup if setpoint can't be achieved)
P + I + D values = Output
The Ki and Kd values are converted to seconds even though we are sampling at 30ms so in other words, if you have an error of -1 and your Ki is equal to 1, the output would change by +1 every second, and with an input error of -10 with a Ki of 1, the out would change by +10 every second.
the derivative "kicker I call it" is weird and you should add it to help lock setpoint quickly after everything else is adjusted too much derivative influence the output becomes jittery, so add it a little at a time at the very end of tuning to just make things lockdown faster.
Start with integral and increase it till the motor starts to oscillate at setpoint and then back it off till it stops.
now add some proportional control to speed up the landing and help it stay on the setpoint.
once you are happy add some derivative and when loads change the extra kick of derivative will improve response times to the change.
ZHomeSlice
A little note: The goal of this was to stress test at the max RPM and Pulse per minute the Arduino UNO could handle. I was able to achieve setpoint and maintain it between 3000RPM and 7000RPM with an encoder that has 400 pulses per revolution! At 7000RPM the pulses were arriving at 46,666 pulses per second! And the Arduino UNO handled it!
This is a nice way to approach tuning--it's sort of thinking about it in the velocity form. I'd always thought of the Proportional term doing most of the work of adjusting to the proper control value, but as one approaches the setpoint, the source for the entire output signal must depend on the integral term. Tuning the Ki parameter first makes excellent sense.
@zhomeslice@DaveX
many thanks for your help. I still have questions:
even though motor is not running yet, does the serial monitor show that Input is ~164?