Questions on PID and milli timing

I'm working on a project that uses 6 PID controllers, and I want to have them update every 30ms. There are other devices besides the PID's in this project, so I have to keep that in mind as well. I have added some code to use millis, but I'm trying to figure out if I need to time the PID's individually, or as one in the loop. I'm also not sure how to code that. I'll add the 1st part of my code that includes all the way through the 1st PID controller. I have read, and watched, a couple "tutorials" on milli timing, but can't figure out how to cross those over to my needs. Do I need the SetSampleTime lines at all?

#include <Wire.h> //for I2C
#include <ADS1X15.h>
#include <PID_v2.h>

// #define TCA9548 0x70

#define Motor1aPin 52
#define Motor2aPin 50
#define Motor3aPin 48
#define Motor4aPin 46
#define Motor5aPin 44
#define Motor6aPin 42
#define Motor1bPin 51
#define Motor2bPin 49
#define Motor3bPin 47
#define Motor4bPin 45
#define Motor5bPin 43
#define Motor6bPin 41

int actual1;
int demand1;
int actual2;
int demand2;
int actual3;
int demand3;
int actual4;
int demand4;
int actual5;
int demand5;
int actual6;
int demand6;

double Pk = 10; //speed it gets there
double Ik = 1;
double Dk = 1;
double Setpoint1, Input1, Output1;
double Setpoint2, Input2, Output2;
double Setpoint3, Input3, Output3;
double Setpoint4, Input4, Output4;
double Setpoint5, Input5, Output5;
double Setpoint6, Input6, Output6;

PID PID1(&Input1, &Output1, &Setpoint1, Pk, Ik, Dk, DIRECT);
PID PID2(&Input2, &Output2, &Setpoint2, Pk, Ik, Dk, DIRECT);
PID PID3(&Input3, &Output3, &Setpoint3, Pk, Ik, Dk, DIRECT);
PID PID4(&Input4, &Output4, &Setpoint4, Pk, Ik, Dk, DIRECT);
PID PID5(&Input5, &Output5, &Setpoint5, Pk, Ik, Dk, DIRECT);
PID PID6(&Input6, &Output6, &Setpoint6, Pk, Ik, Dk, DIRECT);

unsigned long startMillis;  
unsigned long currentMillis;
unsigned long prevMillis = 0;
const unsigned long period = 10;  //the value is a number of milliseconds

void setup() {

  startMillis = millis();  //initial start time
  
  pinMode (A0, INPUT);
  pinMode (A1, INPUT);
  pinMode (A2, INPUT);
  pinMode (A3, INPUT);
  pinMode (A4, INPUT);
  pinMode (A5, INPUT);
  pinMode (A6, INPUT);
  pinMode (A7, INPUT);
  pinMode (3, OUTPUT);
  pinMode (4, OUTPUT);
  pinMode (5, OUTPUT);
  pinMode (6, OUTPUT);
  pinMode (7, OUTPUT);
  pinMode (8, OUTPUT);
  pinMode (9, OUTPUT);
  pinMode (10, OUTPUT);
  
  Serial.begin(115200);
  
  PID1.SetMode(AUTOMATIC);
  PID1.SetOutputLimits(-255,255);
  PID1.SetSampleTime(10);
  PID2.SetMode(AUTOMATIC);
  PID2.SetOutputLimits(-255,255);
  PID2.SetSampleTime(10);
  PID3.SetMode(AUTOMATIC);
  PID3.SetOutputLimits(-255,255);
  PID3.SetSampleTime(10);
  PID4.SetMode(AUTOMATIC);
  PID4.SetOutputLimits(-255,255);
  PID4.SetSampleTime(10);
  PID5.SetMode(AUTOMATIC);
  PID5.SetOutputLimits(-255,255);
  PID5.SetSampleTime(10);
  PID6.SetMode(AUTOMATIC);
  PID6.SetOutputLimits(-255,255);
  PID6.SetSampleTime(10);
}

void TCA9548(uint8_t bus)
{
  Wire.beginTransmission(0x70);
  Wire.write(1 << bus);
  Wire.endTransmission();
}

void loop() {

currentMillis = millis();  //get the current "time" (actually the number of milliseconds since the program started)
  if (currentMillis - startMillis >= period)  //test whether the period has elapsed
 { 
  // **  start PID 1 **
  actual1 = analogRead(A0);
  demand1 = analogRead(A1);

  actual1 = map(actual1,0,1023,341,682);

  actual1 = map(actual1,341,682,-255,255);
  demand1 = map(demand1,0,1023,-255,255);

  Input1 = actual1;
  Setpoint1 = demand1;

  PID1.Compute();

if (Output1 < 0) {
  Output1 = abs(Output1);
  analogWrite(52,Output1);
  analogWrite(51,0);
}
else if (Output1 >= 0) {
  Output1 = abs(Output1);
  analogWrite(51,Output1);
  analogWrite(52,0);
}
else {
  analogWrite(51,0);
  analogWrite(52,0);
}```

Thanks for the well posted code.

Can You post a kind of system description showing the total build?
The needs for accuracy in the PID control is one question.
The second question is what controller would be needed.
The code must be free from delays().
Do all 6 channels have the same demands regarding the control?

1 Like

It might help you to read up on array[ ]s and structs.
It will consolidate your code and possibly make it easier to modify.

1 Like

The PID library takes care of the sample interval - you can call run as often as you like and it will just do nothing if it isn't time.

1 Like

It's an animatronic system. Arduino Mega 2560, 6 makeshift servo's (AS5600 sensor, gearmotor, and H bridge motor controller) , Input values come from a Node-Red MQTT broker.

I want to avoid delays since this project will have a few other items. (16 digital servo's, lights, etc) I don't want to hold the process up every time it cycles.
The 6 channels will need to be tuned individually to get the correct "motion". My code doesn't show this at the moment, but I will have to use different Pk, Ik, and Dk values for each.

This is my attempt to write my own code. I know it's ugly, but if it works, I can learn from it.

Thanks, but I think I need to avoid arrays for the most part. These will all need tuned individually to make sure I get the desired motion. But if there is a way I could do both...?

Hmm. I may be confused then. If I only want the PID to read the input and update every 30ms, is that the sample interval? Or is that a different setting?

It is indeed the sample interval although I thought it was set with SetSampleTime. If you're struggling to get everything done in time, you may want to use millis to avoid reading your sensors needlessly.

.... yes..... Give us an overview of the project, not lots of words. No engineer quotes Shakespeare to describe engineering plans.

Just trying to help…
Hint: arrays don’t change your code, simply make it more structured (no pun intended)…
structs do the same, but whatever you’re happy with.
Combined, they often shorten physical code by >50% and leave less room for bugs to creep into repeated code blocks.

1 Like

You'll need to set the sample time to 30ms because if not specified, the default is 100ms.

No need to worry about the millis time keeping because the compute function does it all for you. If you use PID1.SetSampleTime(30); compute will return true only when 30ms or greater has expired and won't run again until the next 30ms interval has expired. Compute also accounts for any timing overrun like 31ms or greater in its calculations.

You could run compute and your code only once each 30ms interval using something like this (your first PID only is shown) ...

#include <Wire.h> //for I2C
#include <ADS1X15.h>
#include <PID_v2.h>

#define Motor1aPin 52
#define Motor1bPin 51

int actual1;
int demand1;

double Pk = 10; //speed it gets there
double Ik = 1;
double Dk = 1;
double Setpoint1, Input1, Output1;

PID PID1(&Input1, &Output1, &Setpoint1, Pk, Ik, Dk, DIRECT);

void TCA9548(uint8_t bus)
{
  Wire.beginTransmission(0x70);
  Wire.write(1 << bus);
  Wire.endTransmission();
}

void setup() {

  pinMode (A0, INPUT);
  pinMode (A1, INPUT);

  Serial.begin(115200);

  PID1.SetMode(AUTOMATIC);
  PID1.SetOutputLimits(-255, 255);
  PID1.SetSampleTime(30); // 30ms
}

void loop() {

  if (PID1.Compute()) { // if 30ms has elapsed, run compute, then ...
     // update Output
    if (Output1 < 0) {
      Output1 = abs(Output1);
      analogWrite(52, Output1);
      analogWrite(51, 0);
    }
    else if (Output1 >= 0) {
      Output1 = abs(Output1);
      analogWrite(51, Output1);
      analogWrite(52, 0);
    }
    else {
      analogWrite(51, 0);
      analogWrite(52, 0);
    }
    // update Input and Setpoint for the next compute cycle
    actual1 = analogRead(A0);
    demand1 = analogRead(A1);
    actual1 = map(actual1, 0, 1023, 341, 682);
    actual1 = map(actual1, 341, 682, -255, 255);
    demand1 = map(demand1, 0, 1023, -255, 255);
    Input1 = actual1;
    Setpoint1 = demand1;
  } // PID1
   // your code for PID2, etc.
}

Note: You might want to do the Input and Setpoint updates just prior to running compute for improved response.

1 Like

Ok, that makes sense. Knowing that the PID compute does it automatically helps a lot. I think I should probably let the PID use the default, and read my input on their own interval.

No reason to think that the default is perfect for your application - it's just another thing for you to tune.

1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.