Teensy 3.5 irregular output from PID control loop in 5800ms intervals

Status
Not open for further replies.

b3nton

New member
I am using the PID control library to command a powertrain using position feedback. My position signal comes from an optical encoder that is sensing at 20000 CPR (counts per revolution). The encoder channels (A and B) are tied to interrupts to ensure that each pulse is captured during rotation. During my initial tests, before including data writes to an SD Card, the Teensy seems to be keeping up with the encoder, event during rapid movement. In spite of frequent interrupts, it appears that the Teensy 3.5 is running fast enough to run the rest of my code below 1ms/loop cycle when the data acquisition portion of the code (lines 229 - 251) is commented out. It is also worth mentioning that my SD card has pretty decent performance (SanDisk Ultra, class 10). I've printed results from the bench example sketch below.

EDIT: After running this code, I have noticed that I am using the SD library in my control/DAQ program and not SDfat, so I suppose that may be a cause of my latentcy.

Code:
SdFatSdioEX uses extended multi-block transfers without DMA.
SdFatSdio uses a traditional DMA SDIO implementation.
Note the difference is speed and busy yield time.

Type '1' for SdFatSdioEX or '2' for SdFatSdio

(using '2', SdFatSdioEX)

size,write,read
bytes,KB/sec,KB/sec
512,14197.91,15565.36
1024,14461.42,15814.28
2048,14421.82,16110.50
4096,15260.28,16312.63
8192,14606.97,16365.62
16384,15397.14,16458.03
32768,14951.92,16460.90

totalMicros  7618593
yieldMicros  187990
yieldCalls   176
yieldMaxUsec 1314
kHzSdClk     40000
Done
Type '1' for SdFatSdioEX or '2' for SdFatSdio

(using '2', SdFatSdio)

size,write,read
bytes,KB/sec,KB/sec
512,419.59,1944.20
1024,715.84,2843.06
2048,1518.90,4802.62
4096,4063.64,8758.51
8192,5787.70,10988.64
16384,9049.69,14955.97
32768,12173.33,16874.68

totalMicros  54154586
yieldMicros  53148423
yieldCalls   81366
yieldMaxUsec 140545
kHzSdClk     40000
Done

The beginning of my main loop is a series of "If/else" statements forming a position recipe that I need the powertrain to follow. During the loop cycle, I also have code to write the current millis time step, setpoint, endcoder postion, and error (SP - EP) to the SD card on each loop cycle. My goal is to use this system as both a closed loop controller to the powertrain as well as a data logger of the system performance (for both tuning the PID as well as future data acquisition). In spite of my noob skills, everything is working fairly well with one big exception. What I have noticed in the output is an output error every 5800ms. Teh controller seems to send a command signal to the motor controller and gets stuck in the full "on" mode while some other process is takes the wheel for about 500ms. This problem seems to repeat every 5.8 seconds. My first guess is that a write cycle is occurring and killing my performance? The truth is, I know I should probably be using a data buffer but I haven't set one up yet, as I don't know how to set them up properly yet. In any event, I also know that I am casting my int data to char() before writing, and maybe this is causing delays as well? I am still very new to writing my own code to write to SD, so I am very open to constructive criticism. Below you'll find my full controller source code. I guess the question that I'm trying to answer is "is it obvious from my implementation below why this might be happening?" and "how can I optimize my code to prevent it?"

Code:
#include <TimerOne.h>
#include <PID_v1.h>
#include <SD.h>
#include <SPI.h>

// Set up elapsedMillis variables
elapsedMillis delTime;
int mState = 1;
int cnt = 0;
int dither = 275;
int rap45 = 2500;
int rap90 = 5000;
int ramp = 5000;

// Set up logger variables+
const int chipSelect = BUILTIN_SDCARD;
long startTime = 0;
long tm = 0;

//Define PID Variables
double Setpoint, Input, Output;
double Kp = 0.5, Ki = 0.0, Kd = 0;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);

// Quadrature encoder
const int encoderInterruptA = 24;
const int encoderInterruptB = 25;
const int encoderPinA = 24;
const int encoderPinB = 25;
volatile bool encoderASet;
volatile bool encoderBSet;
volatile long encoderTicks = 0;

// Define PWM output pins
const int forward = 27;
const int reverse = 28;
const int spd = 29;

void setup()
{
  // Quadrature encoder setup
  pinMode(encoderPinA, INPUT);      // sets pin A as input
  digitalWrite(encoderPinA, LOW);  // turn on pullup resistors
  pinMode(encoderPinB, INPUT);      // sets pin B as input
  digitalWrite(encoderPinB, LOW);  // turn on pullup resistors
  attachInterrupt(digitalPinToInterrupt(encoderPinA), HandleInterruptA, CHANGE);
  attachInterrupt(digitalPinToInterrupt(encoderPinB), HandleInterruptB, CHANGE);

  //setup direction and speed pins
  pinMode(forward, OUTPUT);
  digitalWrite(forward, LOW);
  pinMode(reverse, OUTPUT);
  digitalWrite(reverse, LOW);
  pinMode(spd, OUTPUT);
  digitalWrite(spd, LOW);
  
  //turn the PID on
  myPID.SetOutputLimits(-255, 255);
  myPID.SetMode(AUTOMATIC);
  
  // Open serial communications and wait for port to open:
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for Leonardo only
  }

  // see if the card is present and can be initialized:
  Serial.print("Initializing SD card...");
  if (!SD.begin(chipSelect)) {
    Serial.println("Card failed, or not present");
    // don't do anything more:
    return;
  }
  Serial.println("card initialized.");

  // setup base timer
  startTime = millis();
}
 
void loop()
{
  if (mState == 1) {        // State 1: 30x 5 deg dither cycles
    if (delTime <= 250) {
      if (delTime <= 125) {   // ramp up if <125ms, ramp down if >125ms
        Setpoint = map((int)delTime,0,125,0,dither);
      }
      else {
        Setpoint = map((int)delTime,125,250,dither,0);
      }
    }
    else {
      cnt++;
      delTime = 0;
      if (cnt >= 30) {   // handle dither cycle count
        cnt = 0;
        mState = 2;
        delTime = 0;
      }
    }
  }
  else if (mState == 2) {   // State 2: 45 deg Rapids (2x)
    if (delTime <= 500) {
      if (delTime <= 250) {   // ramp up if <250ms, ramp down if >250ms
        Setpoint = map((int)delTime,0,250,0,rap45);
      }
      else {
        Setpoint = map((int)delTime,250,500,rap45,0);
      }
    }
    else {
      cnt++;
      delTime = 0;
      if (cnt >= 2) {   // handle rapids cycle count
        cnt = 0;
        mState = 3;
        delTime = 0;
      }
    }
  }
  else if (mState == 3) {   // State 3: 90 deg fast rapids (1x)
    if (delTime <= 500) {
      if (delTime <= 250) {   // ramp up if <250ms, ramp down if >250ms
        Setpoint = map((int)delTime,0,250,0,rap90);
      }
      else {
        Setpoint = map((int)delTime,250,500,rap90,0);
      }
    }
    else {
      cnt++;
      delTime = 0;
      if (cnt >= 1) {   // handle rapids cycle count
        cnt = 0;
        mState = 4;
        delTime = 0;
      }
    }
  }
  else if (mState == 4) {   // State 4: 90 deg fast ramp (1x)
    if (delTime <= 5000) {
      if (delTime <= 2500) {   // ramp up if <2500ms, ramp down if >2500ms
        Setpoint = map((int)delTime,0,2500,0,ramp);
      }
      else {
        Setpoint = map((int)delTime,2500,5000,ramp,0);
      }
    }
    else {
      cnt++;
      delTime = 0;
      if (cnt >= 1) {   // handle rapids cycle count
        cnt = 0;
        mState = 5;
        delTime = 0;
      }
    }
  }
  else if (mState == 5) {   // State 5: 90 deg slow rapids (2x)
    if (delTime <= 1000) {
      if (delTime <= 500) {   // ramp up if <500ms, ramp down if >500ms
        Setpoint = map((int)delTime,0,500,0,rap90);
      }
      else {
        Setpoint = map((int)delTime,500,1000,rap90,0);
      }
    }
    else {
      cnt++;
      delTime = 0;
      if (cnt >= 2) {   // handle rapids cycle count
        cnt = 0;
        mState = 6;
        delTime = 0;
      }
    }
  }
  else if (mState == 6) {   // State 6: 90 deg slow ramp (1x)
        if (delTime <= 10000) {
      if (delTime <= 5000) {   // ramp up if <125ms, ramp down if >125ms
        Setpoint = map((int)delTime,0,5000,0,ramp);
      }
      else {
        Setpoint = map((int)delTime,5000,10000,ramp,0);
      }
    }
    else {
      cnt++;
      delTime = 0;
      if (cnt >= 1) {   // handle rapids cycle count
        cnt = 0;
        mState = 7;
        delTime = 0;
      }
    }
  }
  else if (mState == 7) {   // State7: 90 deg slow rapids (2x)
    if (delTime <= 1000) {
      if (delTime <= 500) {   // ramp up if <500ms, ramp down if >500ms
        Setpoint = map((int)delTime,0,500,0,rap90);
      }
      else {
        Setpoint = map((int)delTime,500,1000,rap90,0);
      }
    }
    else {
      cnt++;
      delTime = 0;
      if (cnt >= 1) {   // handle rapids cycle count
        cnt = 0;
        mState = 8;
        delTime = 0;
      }
    }
  }

  
  noInterrupts();
  Input = encoderTicks;
  interrupts();
  
  myPID.Compute();
  
  // send data to SD card
  // make a string for assembling the data to log:
  String dataString = "";
  
  // read three sensors and append to the string:
  tm = millis()-startTime;
  dataString += String(tm);
  dataString += ",";
  dataString += String(Setpoint);
  dataString += ",";
  dataString += String(encoderTicks);
  dataString += ",";
  dataString += String(Setpoint - encoderTicks);
  
  // open the file. note that only one file can be open at a time,
  // so you have to close this one before opening another.
  File dataFile = SD.open("datalog2.txt", FILE_WRITE);
  
  // if the file is available, write to it:
  if (dataFile) {
    dataFile.println(dataString);
    dataFile.close();
    // print to the serial port too:
    Serial.println(dataString);
  }  
  // if the file isn't open, pop up an error:
  else {
    Serial.println("error opening datalog.txt");
  } 
  
  gearBox(Output);
}

// gearBox motion function
void gearBox(double sign4l)
{
  // H-bridge output to motor
  if(sign4l > 0)
  {
    // go go forward
    digitalWrite(forward, HIGH);
    digitalWrite(reverse, LOW);
    analogWrite(spd, sign4l);
  }
  else if(sign4l < 0)
  {
    // go reverse
    digitalWrite(forward, LOW);
    digitalWrite(reverse, HIGH);
    analogWrite(spd, abs(sign4l));
  }
  else
  {
    // no go
    digitalWrite(forward, LOW);
    digitalWrite(reverse, LOW);
    analogWrite(spd,0);
  }  
}

// Interrupt service routines for the quadrature encoder
void HandleInterruptA()
{
  // Test transition
  encoderASet = digitalRead(encoderPinA);   // read the input pin
  encoderBSet = digitalRead(encoderPinB);   // read the input pin
 
  // and adjust counter + if A leads B
  if(encoderASet)
  {
    encoderTicks += encoderBSet ? -1 : +1;
  }
  else
  {
    encoderTicks -= encoderBSet ? -1 : +1;
  }
}

// Interrupt service routines for the quadrature encoder
void HandleInterruptB()
{
  // Test transition; since the interrupt will only fire on 'rising' we don't need to read pin A
  encoderASet = digitalRead(encoderPinA);   // read the input pin
  encoderBSet = digitalRead(encoderPinB);   // read the input pin
 
  // and adjust counter + if B leads A
  if(encoderBSet)
  {
    encoderTicks -= encoderASet ? -1 : +1;
  }
  else
  {
    encoderTicks += encoderASet ? -1 : +1;
  }
}

Also, here is a link to an image of my plotted results, showing the large spikes at repeating 5800ms intervals.

https://imgur.com/a/c1Wk9
 
The code you posted cannot be what is running because

mState = 8;

is going to crash your program at line 210.

PS: 5800ms is close enough to 5000+275+275+275 for me to start looking there...
 
Thank you for having a look! Yes, the change in my machine state variable, mState = 8 is not an elegant solution to break out of my routine, but it is what I had been using to get some results quickly. After breaking out of the if structure, the rest of the code leaves my powertrain freewheeling until I powered it off. Since yesterday, I've improved the functionality of my code to provide a better method to turn off the control loop after machine state 8 is reached. The revides code is posted below.
Code:
#include <TimerOne.h>
#include <PID_v1.h>
#include <SD.h>
#include <SPI.h>

// Set up elapsedMillis variables
elapsedMillis delTime;
int mState = 1;
int cnt = 0;
int dither = 275;
int rap45 = 2500;
int rap90 = 5000;
int ramp = 5000;

// Set up logger variables+
const int chipSelect = BUILTIN_SDCARD;
long startTime = 0;
long tm = 0;
int onBit = 1;

//Define PID Variables
double Setpoint, Input, Output;
double Kp = 0.45, Ki = 0.045, Kd = 0.045;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki, Kd, DIRECT);

// Quadrature encoder
const int encoderInterruptA = 24;
const int encoderInterruptB = 25;
const int encoderPinA = 24;
const int encoderPinB = 25;
volatile bool encoderASet;
volatile bool encoderBSet;
volatile long encoderTicks = 0;

// Define PWM output pins
const int forward = 27;
const int reverse = 28;
const int spd = 29;

void setup()
{
  // Quadrature encoder setup
  pinMode(encoderPinA, INPUT);      // sets pin A as input
  digitalWrite(encoderPinA, LOW);  // turn on pullup resistors
  pinMode(encoderPinB, INPUT);      // sets pin B as input
  digitalWrite(encoderPinB, LOW);  // turn on pullup resistors
  attachInterrupt(digitalPinToInterrupt(encoderPinA), HandleInterruptA, CHANGE);
  attachInterrupt(digitalPinToInterrupt(encoderPinB), HandleInterruptB, CHANGE);

  //setup direction and speed pins
  pinMode(forward, OUTPUT);
  digitalWrite(forward, LOW);
  pinMode(reverse, OUTPUT);
  digitalWrite(reverse, LOW);
  pinMode(spd, OUTPUT);
  digitalWrite(spd, LOW);
  
  //turn the PID on
  myPID.SetOutputLimits(-255, 255);
  myPID.SetMode(AUTOMATIC);
  
  // Open serial communications and wait for port to open:
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for Leonardo only
  }

  // see if the card is present and can be initialized:
  Serial.print("Initializing SD card...");
  if (!SD.begin(chipSelect)) {
    Serial.println("Card failed, or not present");
    // don't do anything more:
    return;
  }
  Serial.println("card initialized.");

  // setup base timer
  startTime = millis();
}
 
void loop()
{
  if (mState == 1) {        // State 1: 30x 5 deg dither cycles
    if (delTime <= 250) {
      if (delTime <= 125) {   // ramp up if <125ms, ramp down if >125ms
        Setpoint = map((int)delTime,0,125,0,dither);
      }
      else {
        Setpoint = map((int)delTime,125,250,dither,0);
      }
    }
    else {
      cnt++;
      delTime = 0;
      if (cnt >= 30) {   // handle dither cycle count
        cnt = 0;
        mState = 2;
        delTime = 0;
      }
    }
  }
  else if (mState == 2) {   // State 2: 45 deg Rapids (2x)
    if (delTime <= 500) {
      if (delTime <= 250) {   // ramp up if <250ms, ramp down if >250ms
        Setpoint = map((int)delTime,0,250,0,rap45);
      }
      else {
        Setpoint = map((int)delTime,250,500,rap45,0);
      }
    }
    else {
      cnt++;
      delTime = 0;
      if (cnt >= 2) {   // handle rapids cycle count
        cnt = 0;
        mState = 3;
        delTime = 0;
      }
    }
  }
  else if (mState == 3) {   // State 3: 90 deg fast rapids (1x)
    if (delTime <= 500) {
      if (delTime <= 250) {   // ramp up if <250ms, ramp down if >250ms
        Setpoint = map((int)delTime,0,250,0,rap90);
      }
      else {
        Setpoint = map((int)delTime,250,500,rap90,0);
      }
    }
    else {
      cnt++;
      delTime = 0;
      if (cnt >= 1) {   // handle rapids cycle count
        cnt = 0;
        mState = 4;
        delTime = 0;
      }
    }
  }
  else if (mState == 4) {   // State 4: 90 deg fast ramp (1x)
    if (delTime <= 5000) {
      if (delTime <= 2500) {   // ramp up if <2500ms, ramp down if >2500ms
        Setpoint = map((int)delTime,0,2500,0,ramp);
      }
      else {
        Setpoint = map((int)delTime,2500,5000,ramp,0);
      }
    }
    else {
      cnt++;
      delTime = 0;
      if (cnt >= 1) {   // handle rapids cycle count
        cnt = 0;
        mState = 5;
        delTime = 0;
      }
    }
  }
  else if (mState == 5) {   // State 5: 90 deg slow rapids (2x)
    if (delTime <= 1000) {
      if (delTime <= 500) {   // ramp up if <500ms, ramp down if >500ms
        Setpoint = map((int)delTime,0,500,0,rap90);
      }
      else {
        Setpoint = map((int)delTime,500,1000,rap90,0);
      }
    }
    else {
      cnt++;
      delTime = 0;
      if (cnt >= 2) {   // handle rapids cycle count
        cnt = 0;
        mState = 6;
        delTime = 0;
      }
    }
  }
  else if (mState == 6) {   // State 6: 90 deg slow ramp (1x)
        if (delTime <= 10000) {
      if (delTime <= 5000) {   // ramp up if <125ms, ramp down if >125ms
        Setpoint = map((int)delTime,0,5000,0,ramp);
      }
      else {
        Setpoint = map((int)delTime,5000,10000,ramp,0);
      }
    }
    else {
      cnt++;
      delTime = 0;
      if (cnt >= 1) {   // handle rapids cycle count
        cnt = 0;
        mState = 7;
        delTime = 0;
      }
    }
  }
  else if (mState == 7) {   // State7: 90 deg slow rapids (2x)
    if (delTime <= 1000) {
      if (delTime <= 500) {   // ramp up if <500ms, ramp down if >500ms
        Setpoint = map((int)delTime,0,500,0,rap90);
      }
      else {
        Setpoint = map((int)delTime,500,1000,rap90,0);
      }
    }
    else {
      cnt++;
      delTime = 0;
      if (cnt >= 1) {   // handle rapids cycle count
        cnt = 0;
        mState = 8;
        Setpoint = 0;
        delTime = 0;
        myPID.SetMode(MANUAL);
        Output = 0;
        onBit = 0;
      }
    }
  }

  
  noInterrupts();
  Input = encoderTicks;
  interrupts();
  
  myPID.Compute();
  
  // send data to SD card
  // make a string for assembling the data to log:
  String dataString = "";
  
  // read three sensors and append to the string:
  tm = millis()- startTime;
  dataString += String(tm);
  dataString += ",";
  dataString += String(Setpoint);
  dataString += ",";
  dataString += String(encoderTicks);
  dataString += ",";
  dataString += String(Setpoint - encoderTicks);

  if (onBit == 1) {      // only write to file while test is running
    // open the file. note that only one file can be open at a time,
    // so you have to close this one before opening another.
    File dataFile = SD.open("data8_2.txt", FILE_WRITE);
    
    // if the file is available, write to it:
    if (dataFile) {
      dataFile.println(dataString);
      dataFile.close();
      // print to the serial port too:
      Serial.println(dataString);
    }
    // if the file isn't open, pop up an error:
    else {
      Serial.println("error opening datalog.txt");
    } 
  }
  gearBox(Output);
}

// gearBox motion function
void gearBox(double sign4l)
{
  // H-bridge output to motor
  if(sign4l > 0)
  {
    // go go forward
    digitalWrite(forward, HIGH);
    digitalWrite(reverse, LOW);
    analogWrite(spd, sign4l);
  }
  else if(sign4l < 0)
  {
    // go reverse
    digitalWrite(forward, LOW);
    digitalWrite(reverse, HIGH);
    analogWrite(spd, abs(sign4l));
  }
  else
  {
    // no go
    digitalWrite(forward, LOW);
    digitalWrite(reverse, LOW);
    analogWrite(spd,0);
  }  
}

// Interrupt service routines for the quadrature encoder
void HandleInterruptA()
{
  // Test transition
  encoderASet = digitalRead(encoderPinA);   // read the input pin
  encoderBSet = digitalRead(encoderPinB);   // read the input pin
 
  // and adjust counter + if A leads B
  if(encoderASet)
  {
    encoderTicks += encoderBSet ? -1 : +1;
  }
  else
  {
    encoderTicks -= encoderBSet ? -1 : +1;
  }
}

// Interrupt service routines for the quadrature encoder
void HandleInterruptB()
{
  // Test transition; since the interrupt will only fire on 'rising' we don't need to read pin A
  encoderASet = digitalRead(encoderPinA);   // read the input pin
  encoderBSet = digitalRead(encoderPinB);   // read the input pin
 
  // and adjust counter + if B leads A
  if(encoderBSet)
  {
    encoderTicks -= encoderASet ? -1 : +1;
  }
  else
  {
    encoderTicks += encoderASet ? -1 : +1;
  }
}
The changes are a series of variable changes at lines 214-216 to turn off the PID control, and an if statement around my data logging code (lines 248-258) to only run when the PID loop is running.

This morning, I'll take a look at the code and see if I can figure out what is happening at the time you mention. I still think that it has something to do with the data write to the SD card, but my data suggest that write is taking the better part of 200-500ms, and that doesn't sound right at all.
 
Status
Not open for further replies.
Back
Top