PID control altering PWM frequency output

Hello there,

I'm currently trying to control a DC motor with the Arduino Uno and PID Library.

My input comes from a 6ppr encoder. I'm using pulseIn to detect the pulse-width.
Afterwards I convert it into frequency.

The motor is driven by PWM input and 4 kHz square pulse.
Manually providing pulses to control the motor in open loop works fine.

When I'm trying to run PID control, the frequency output of my PWM signal changes rapidly.
Moreover, the pulse-width is not changing and stays at 50%. Only frequency changes, when I apply some load.
I have attached one scope showing that the frequency changed to 30Hz.
It is shortly 4 kHz, as it should be, seen in the end of the pulse.

I read about the library and guess it comes from the change in sample time.
Can I prevent this?

Or is there any option to hold the frequency output stable?

Best regards

Post your code, using code tags ("</>") button.

Hi jremington,

here is my code.
The filter is used because encoder output is bouncing.

/********************************************************
 * Modified PID Basic Example
 ********************************************************/

#include <PID_v1.h>
#define PIN_INPUT 7
#define PIN_OUTPUT 9

//Define Variables we'll be connecting to
double Setpoint, Input, Output,frequency,testfreq;

int speeddata[7],speedmax,i; //Define filter variable

//Specify the links and initial tuning parameters
// Simulink PID simulation - no movement
//double Kp=0.230141242776802, Ki=2.68080663902755, Kd=-0.000130613000710166;

double Kp=1, Ki=0.001, Kd=5;

PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);

void setup()
{
  TCCR1B = TCCR1B & B11111000 | B00000010; //3.9 kHz on Pin 9,10

  // Setup Serial connection
  Serial.begin(115200);

  // Fill filter with zeros
   for(i=6;i>=0;i--){speeddata[i]=0;}

  //initialize the variables we're linked to
  testfreq=pulseIn(PIN_INPUT,HIGH,250000);
  if (testfreq==0){frequency=1;}
  else{frequency = 1/(2*testfreq*pow(10,-6));}//generate frequency 
  
  Input=map(frequency,0,150,0,255); // map input 

  Setpoint = 100; // Aim in Hz - doubled due to reason before

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

void loop()
{
 testfreq=pulseIn(PIN_INPUT,HIGH,250000);
  if (testfreq==0){frequency=1;} // avoid inf frequency
  else{frequency = 1/(2*testfreq*pow(10,-6));} //generate frequency 
  
  // PEAK FILTER STARTS
   for(i=6;i>=0;i--){speeddata[i]=speeddata[i-1];}

   // Fill first space with new value
   speeddata[0]=frequency; // Fill first array with new value
   speedmax=findmaxspeed();
   // PEAK FILTER ENDS

   // Apply speed to pid input
   Input=map(frequency,0,150,0,255); // map input 
  
  myPID.Compute();
  
  analogWrite(PIN_OUTPUT, Output); //generate PWM output
  

  // Debug purposes
  Serial.print("In: ");
  Serial.println(Input);
  Serial.print("Out: ");
  Serial.println(round(Output));
  
}

int findmaxspeed(){
  speedmax=speeddata[0]; // Newest value is set to be max
  for(i=0;i<=6;i++){ //Check if any value is still greater than new value
  if (speeddata[i]>speedmax){speedmax=speeddata[i];}
  }
  return speedmax;
}

I don't know why the frequency is changing, but then not much about that program makes sense -- I suggest you add comments.

For example:

  1. Why are you setting TCCR1B?

  2. I have no idea what pulseIn() returns, but the default ( for testfrequency == 0) is 50000.0, which you then expect to be in the interval 0 to 150 (see b. below).

 if (testfreq==0){frequency=1;}
  else{frequency = 1/(2*testfreq*pow(10,-6));}//generate frequency
 
  Input=map(frequency,0,150,0,255); // map input

Notes on above:

a. You can write pow(10,-6) as 1.0e-6.

b. The map function works only for integers and does not constrain out-of-bound values. In your case it can be replaced by Input = frequency*255./150.;

  1. AnalogWrite() generates a frequency of about 490 Hz if you don't alter the timer.

  2. Why is your scope trace so noisy? What is the probe connected to?

 // Apply speed to pid input
   Input=map(frequency,0,150,0,255); // map input

You have set up a filter for the last 7 values and assign the max value to speedmax. You are not using that value for the input, but just using the last reading returned from the mapping of pulseIn().

I think you want this

Input=map(speedmax,0,150,0,255); // map input

What does your serial print show for input and output?

Cattledog already found the biggest bug in your code.

I'd think that you should measure more than 1 motor revolution, in order to get the motor speed. Usually this is done by counting pulses over a fixed time, or determination of the time for x pulses, where x should be a multiple of the ppr to filter out bouncing (uneven pulses). All that with only integer math and variables. Your filter instead prefers the shortest of the encoder pulses, i.e. effectively you measure the speed only once per rev. Due to the long computations you may miss encoder pulses, so that speedmax may come from different encoder segments with each sample, creating noise instead of reducing it.

Which motor rpm (excluding gearbox) corresponds to the setpoint=100?

What I think I am seeing in you posted scope image is alternating 5V, 0V output at the 100 ms default output timing of the library. I see 50 ms/div base. I don't know where the 31Hz is coming from.

At the far right of the image you see the 4KHz pwm, and at that point output was not 0 or 255. analogWrite(pin,0) defaults to digitalWrite(LOW), and analogWrite(pin,255) defaults to digitalWrite(HIGH).

This is more evidence to me that your inputs are broken and the output is banging between the HIGH/LOW limits. I would focus on why your inputs are driving the output high and low.

pulseIn() from a 6ppr encoder is a very poor way to determine rotational frequency of the motor. It is a blocking function. It looks like you are wanting 100 rpm, which at 6 ppr which puts the high pulse length at 50ms. pulseIn() can be blocking for twice that as it blocks waiting to see a low/high transition. It's very likely to mess up the pid library sampling/compute period.

DrDiettrich suggested other non blocking methods of determining rotation speed and I would recommend them instead of pulse in..

Thanks for all your help and advice!

@cattledog - Thats totally correct, just fixed that, thanks!
Moreover you were kind of right according to the inputs..!

My main problem was to understand how the library works.
It is highly important to feed the setpoint with a value from 0 to 255 at first...
Secondly, the frequency didn't fluctuate, it was due to wrong horizontal settings in my osci.
Moreover, I had to filter some ripple glitches from my encoder which were highly effecting the PID calculation.
My controller runs now perfectly and my Simulink parameter are just gold!

The reason why I've chosen the pulseIn instead of counter over fixed period is simply that I want to capture the rising part of the motor as well and this is just 350 ms. Assuming I want to have at least 10 samples during this period leads to a fixed time period of 35 ms for the method of counting pulses.
I know that 6 ppr are really really bad resolution, but it's my project hardware so I have to get over it.
So now let's say we count for 35 ms pulses. My maximal speed is 1500 rpm represented by 150 Hz, which are 6.6 ms. My "minimal" speed at rising time its lets say 100 rpm, or 10 Hz. That is already 100 ms, which is not capable with the counter method. Moreover the speed detection with such a small measure period is highly restricted to a few different speed values...
I know the pulseIn won't work proper as well, but still better because it does detect the rising part.

I have attached one image showing the rise time of my motor for those who are interested in it.

The huge noise comes from the machine as well. I've tried decoupling and Z-Diode + Schmitt Trigger, but thats not part of this topic.

Best,
guesswhoiam

I've chosen the pulseIn instead of counter over fixed period

Those are not your only two options. Switch your thinking from a counter over a fixed period to thinking about determining the period for a fixed number of pulses even if that number is only one.

I'm not certain if you are using a quadrature encoder or or what the high/low pulse times are with the 6ppr encoder, but anything you can do with pulseIn() you can do with not blocking, interrupt based, methods which will return the same data as pulseIn(). If you use timer based input capture interrupts for your pulse timing you can likely have more precise resolution.

As well as measuring the high time of a single pulse, you can be measuring the period for some number of pulses which will have the benefit of built in averaging.

Hey guys, just want to thank again for the help.
I was really successful with my university project.

I have created a video about the whole thing, which you can watch here:

I hope you like it!

Great community!