Nearly Complete PID project with 94% sram usage needs improvement

So, I have this PID project I have been working on for a while now. I've already posted this project once with a different problem, so you may have seen a lot of this project already. This PID heats 2 lbs to 20 lbs of lead from room temp (65-80 deg F) to between 600 and 800 deg F. It will also control a toaster oven that heats to 450 deg for setting powder coating. Later, I'd like to use it as a programmable temperature controller for a re-flow toaster oven.

The issues:
-I cannot get it tuned, the temperature levels out either 20 deg on either side of the set line, or the temperature oscillates +-30 deg to both sides. The effect is determined by the settings I set, and is repeatable.
-Extreme slow down for phase shift of melting lead (at about 450 deg F). Can this be accounted for in the PID without throwing the PID out of wack?
-Almost out of SRam, no room for more features, and concerns for stability (no noticed problems yet though)
-Ki requires an excessively low number (must be divided by at least 10000) to get an even remotely stable output temp, causing tuning issues.
-I want to add capability for programming. Eventually, this PID unit will control a re-flow toaster oven

My thoughts:
-I want to use the Auto-Tune library, but I cant get it to fit (SRAM, NOT FLASH), and don't know how to set it up for a relay system.
-Auto tuning might fix the state change slow down, but I was considering adding a conditional to turn the PID off and relay on until the temp is over 450, but when turning the PID back on, it becomes very unstable and cant hold a steady temp (unreliably levels off or oscillates)
-I have some thoughts on reducing memory, but I have reached the limits on my programming modification capabilities. I don't understand enough about how the ssd1306 library works to minimize its SRam usage, and the only other place I know to reduce ram is a minor amount from the EEPROMex that I could cut out if I could successfully divide an int and some doubles into individual bytes (failed with 3 methods)

-I had considered using the inverse of Ki and instead of using "Ki/10000" I'd use "1/Ki", but that leaves the potential for divide-by-zero, and removes the possibility for a 0 Ki value.
-With the inability to tune the system, and the serious lack of SRam, I have not put any thought into how to make this system programmable, so making this the brains of a re-flow toaster got put on the back burner. I still want to do this (in fact, it was the final stage of the project), but I'm not sure its possible.

Here is the code. The libraries should be standard. I did modifiy them at one point, but there were many problems, so I'm pretty sure I have set them all back. There is 4 lines I changed in "HardwareSerial.h" where I changed the buffer size (changed the 2x 32s to 8 and the 2x 64s to 16). I did not see any negative side effects in this project from the change, and it saved me a bit of SRam.

Ugh, "It's over 9000!" code to come....

#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include "max6675.h"
#include <PID_v1.h>
#include <EEPROMex.h>

#define OLED_RESET 4
Adafruit_SSD1306 display(OLED_RESET);
int GraphTop, GraphBot, GraphVal[128];
unsigned long Gtime = 0;
byte Setline = 1;
double GTScale = 15;

#define thermoDO 8
#define thermoCS 9
#define thermoCLK 6
MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO);
unsigned long Ttime = 0;
int temp = 100, Calibration = -2;
double tsamp = 0;
byte notsamps = 0;

#define RELAY_PIN 10
#define SAFTY 875
double Setpoint=600, Input, Output, Kp=3, Ki=0.2, Kd=0;
PID myPID(&Input, &Output, &Setpoint, Kp, Ki/10000, Kd, DIRECT);
int WindowSize = 1000;
unsigned long windowStartTime;
byte PIDMode = 1;

#define MENUITEMS 8
byte i = 0, ButtonMode = 0, MenuMode = 1;
bool bc7 = false, bc5 = false, bc4 = false, bc3 = false, bc2 = false;

#define addrSetpoint 0
#define addrKp 4
#define addrKi 8
#define addrKd 12
#define addrCalibration 16
#define addrGTScale 18

void setup()   {
  EEPROM.setMemPool(0, 1023);
  EEPROM.setMaxAllowedWrites(80);
  delay(100);
  Setpoint = EEPROM.readDouble(addrSetpoint);
  Kp = EEPROM.readDouble(addrKp);
  Ki = EEPROM.readDouble(addrKi);
  Kd = EEPROM.readDouble(addrKd);
  Calibration = EEPROM.readInt(addrCalibration);
  GTScale = EEPROM.readDouble(addrGTScale);

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);

  windowStartTime = millis();
  myPID.SetOutputLimits(0, WindowSize);
  if (PIDMode == 1)
    myPID.SetMode(AUTOMATIC);

  for (i = 0; i<128; i++) {
    GraphVal[i] = 100;
  }
  pinMode(7, INPUT);
  pinMode(5, INPUT);
  pinMode(4, INPUT);
  pinMode(3, INPUT);
  pinMode(2, INPUT);
  delay(500);
}


void loop() {
  Timers();
  AdjMenu();
  switch (PIDMode) {
    case 0:
      if ((myPID.GetMode() == AUTOMATIC))
        myPID.SetMode(MANUAL);
      digitalWrite(RELAY_PIN, LOW);
      break;
    case 1:
      if ((temp > SAFTY)){//|| (temp > 1.1 * Setpoint)) {
        myPID.SetMode(MANUAL);
        digitalWrite(RELAY_PIN, LOW);
      } else {
        if ((myPID.GetMode() == MANUAL))
          myPID.SetMode(AUTOMATIC);
        myPID.Compute();
        if (millis() - windowStartTime > WindowSize)
          windowStartTime += WindowSize;
        if (Output < millis() - windowStartTime)
          digitalWrite(RELAY_PIN, LOW);
        else
          digitalWrite(RELAY_PIN, HIGH);
      }
      break;
    case 2:
      if ((myPID.GetMode() == AUTOMATIC))
        myPID.SetMode(MANUAL);
      if (temp > SAFTY)
        digitalWrite(RELAY_PIN, LOW);
      else
        digitalWrite(RELAY_PIN, HIGH);
      break;
    default:
      PIDMode = 0;
      break;
  }

  ReDisp();
}

void Timers() {
  if (millis() > Ttime) {
    tsamp = tsamp + thermocouple.readFahrenheit()+Calibration;
    notsamps = notsamps + 1;
    if (notsamps == 4){
      Input = tsamp / 4;
      temp = tsamp / 4;
      tsamp = 0;
      notsamps = 0;
    }
    Ttime = millis() + 250;
  }
  if(millis() > Gtime) {
    GraphTop = GraphVal[1];
    GraphBot = GraphVal[1];
    for (i = 0; i < 127; i++) {
      GraphVal[i] = GraphVal[(i + 1)];
    }
    GraphVal[127] = temp;
    for (i = 0; i < 128; i++){
      if (GraphVal[i] > GraphTop)
        GraphTop = GraphVal[i];
      if (GraphVal[i] < GraphBot)
        GraphBot = GraphVal[i];
    }
    if (GraphTop-GraphBot<24) {
      GraphTop=GraphTop+((24-(GraphTop-GraphBot))/2);
      GraphBot=GraphTop-24;
    }
    Gtime=millis()+ ((GTScale * 60000) / 128);
  }
}

void AdjMenu() {
  if (digitalRead(7) == HIGH) {
    if (bc7 == false) {
      bc7 = true;
      MenuMode = (MenuMode - 1) / -1;
    }
  }else{
    bc7 = false;
  }
  if (MenuMode == 0) {
    if (digitalRead(5) == HIGH) {
      if (bc5 == false) {
        bc5 = true;
        switch (ButtonMode) {
          case 0:
            if (digitalRead(2) == HIGH)
              Setline = (Setline - 1) / -1;
          case 1:
            Setpoint = Setpoint + 10;
            break;
          case 2:
            Kp = Kp + 1;
            break;
          case 3:
            Ki = Ki + 1;
            break;
          case 4:
            Kd = Kd + 1;
            break;
          case 5:
            Calibration = Calibration + 10;
            break;
          case 6:
            GTScale = GTScale + 1;
            break;
        }
        myPID.SetTunings(Kp,Ki/10000,Kd);
      }
    }else{
      bc5 = false;
    }
    if (digitalRead(4) == HIGH) {
      if (bc4 == false) {
        bc4 = true;
        switch (ButtonMode) {
          case 0:
            PIDMode = PIDMode + 1;
            if (PIDMode > 2)
              PIDMode = 2;
            break;
          case 1:
            Setpoint = Setpoint + 1;
            break;
          case 2:
            Kp = Kp + .1;
            break;
          case 3:
            Ki = Ki + .1;
            break;
          case 4:
            Kd = Kd + .1;
            break;
          case 5:
            Calibration = Calibration + 1;
            break;
          case 6:
            GTScale = GTScale + .1;
            break;
          case 7:
            EEPROM.updateDouble(addrSetpoint, Setpoint);
            delay(20);
            EEPROM.updateDouble(addrKp, Kp);
            delay(20);
            EEPROM.updateDouble(addrKi, Ki);
            delay(20);
            EEPROM.updateDouble(addrKd, Kd);
            delay(20);
            EEPROM.updateInt(addrCalibration, Calibration);
            delay(20);
            EEPROM.updateDouble(addrGTScale, GTScale);
            delay(20);
            MenuMode = 1;
            ButtonMode = 7;
            break;
        }
        myPID.SetTunings(Kp,Ki/10000,Kd);
      }
    }else{
      bc4 = false;
    }
    if (digitalRead(3) == HIGH) {
      if (bc3 == false) {
        bc3 = true;
        switch (ButtonMode) {
          case 0:
            if (PIDMode == 0)
              PIDMode = 1;
            PIDMode = PIDMode - 1;
            break;
          case 1:
            Setpoint = Setpoint - 1;
            if (Setpoint < 0)
              Setpoint = 0;
            break;
          case 2:
            Kp = Kp - .1;
            if (Kp < 0)
              Kp = 0;
            break;
          case 3:
            Ki = Ki - .1;
            if (Ki < 0)
              Ki = 0;
            break;
          case 4:
            Kd = Kd - .1;
            if (Kd < 0)
              Kd = 0;
            break;
          case 5:
            Calibration = Calibration - 1;
            break;
          case 6:
            GTScale = GTScale - .1;
            if (GTScale < .1)
              GTScale = .1;
            break;
          case 7:
            Setpoint = EEPROM.readDouble(addrSetpoint);
            Kp = EEPROM.readDouble(addrKp);
            Ki = EEPROM.readDouble(addrKi);
            Kd = EEPROM.readDouble(addrKd);
            Calibration = EEPROM.readInt(addrCalibration);
            GTScale = EEPROM.readDouble(addrGTScale);
            MenuMode = 1;
            ButtonMode = 7;
            break;
        }
        myPID.SetTunings(Kp,Ki/10000,Kd);
      }
    }else{
      bc3 = false;
    }
    if (digitalRead(2) == HIGH) {
      if (bc2 == false) {
        bc2 = true;
        switch (ButtonMode) {
          case 0:
            if (digitalRead(5) == HIGH)
              Setline = (Setline - 1) / -1;
          case 1:
            Setpoint = Setpoint - 10;
            if (Setpoint < 0)
              Setpoint = 0;
            break;
          case 2:
            Kp = Kp - 1;
            if (Kp < 0)
              Kp = 0;
            break;
          case 3:
            Ki = Ki - 1;
            if (Ki < 0)
              Ki = 0;
            break;
          case 4:
            Kd = Kd - 1;
            if (Kd < 0)
              Kd = 0;
            break;
          case 5:
            Calibration = Calibration - 10;
            break;
          case 6:
            GTScale = GTScale - 1;
            if (GTScale < .1)
              GTScale = .1;
            break;
        }
        myPID.SetTunings(Kp,Ki/10000,Kd);
      }
    }else{
      bc2 = false;
    }
  }else{
    if (digitalRead(4) == HIGH) {
      if (bc4 == false) {
        bc4 = true;
        ButtonMode = ButtonMode + 1;
        if (ButtonMode == MENUITEMS)
          ButtonMode = 0;
      }
    }else{
      bc4 = false;
    }
    if (digitalRead(3) == HIGH) {
      if (bc3 == false) {
        bc3 = true;
        if (ButtonMode == 0)
          ButtonMode = MENUITEMS;
        ButtonMode = ButtonMode - 1;
      }
    }else{
      bc3 = false;
    }
  }
}

More code (the display section) to come…

void ReDisp() {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(0,0);
  display.print(F("Set: "));
  display.print((int)Setpoint);
  display.print((char)247);
  display.print(F("F"));
  display.setCursor(0,8);
  display.print(F("Temp: "));
  display.print(temp);
  display.print((char)247);
  display.print(F("F"));
  display.setCursor(0,16);
  display.print(GraphTop);
  display.setCursor(0,56);
  display.print(GraphBot);
  for (i = 0; i < 128; i++) {
    display.drawPixel(i,63 - (((GraphVal[i]-GraphBot)*47)/(GraphTop-GraphBot)), WHITE);
  }
  if ((GraphTop>Setpoint)&&(Setpoint>GraphBot)&&(Setline==1)) {
    for (i = 0; i < 128; i++) {
      display.drawPixel(i,63 - (((Setpoint-GraphBot)*47)/(GraphTop-GraphBot)), WHITE);
    }
  }
  if (MenuMode == 0) {
    display.setCursor(77,0);
    display.print(F("Adjust:"));
    display.setCursor(77,8);
    switch (ButtonMode) {
      case 0:
        display.print(F("M: "));
        switch (PIDMode) {
          case 0:
            display.print(F("Off"));
            break;
          case 1:
            display.print(F("Auto"));
            break;
          case 2:
            display.print(F("On"));
        }
        break;
      case 1:
        display.print(F("T: "));
        display.print((int)Setpoint);
        break;
      case 2:
        display.print(F("P: "));
        display.print(Kp);
        break;
      case 3:
        display.print(F("I: "));
        display.print(Ki);
        break;
      case 4:
        display.print(F("D: "));
        display.print(Kd);
        break;
      case 5:
        display.print(F("C: "));
        display.print(Calibration);
        break;
      case 6:
        display.print(F("S: "));
        display.print(GTScale);
        break;
      case 7:
        display.print(F("S:+, R:-"));
        break;
    }
  }else{
    display.setCursor(77,0);
    display.print(F("Menu:"));
    display.setCursor(77,8);
    switch (ButtonMode) {
      case 0:
        display.print(F("Mode"));
        break;
      case 1:
        display.print(F("Temp."));
        break;
      case 2:
        display.print(F("Kp Val."));
        break;
      case 3:
        display.print(F("Ki Val."));
        break;
      case 4:
        display.print(F("Kd Val."));
        break;
      case 5:
        display.print(F("Calibr."));
        break;
      case 6:
        display.print(F("T. Scale"));
        break;
      case 7:
        display.print(F("Memory"));
        break;
    }
  }
  display.display();
}

Thats the last of it (besides libraries).

Any help you guys can provide to help me reach my end goal would be greatly appreciated.

Please note: I am not looking for someone to fix my code. I would like some direction so I may achieve my goals myself. You know, the fish thing…give a man/teach a man…I’d rather learn.

EDIT: Oh yea, this is running on a Nano 328.

First of all I don't see a need for GraphBot and GraphTop arrays, but you seem to have fixed that already. As long as GraphVal is the only (obvious) variable where you can save memory, I'd look for an controller with more RAM; unfortunately I don't know of any board with the size of a Nano, and the RAM of a Mega.
Eventually a second board could help, one for temperature control and one for the display. Then move GraphVal to the board/sketch with the most free RAM, and transfer the values using SPI or similar communication.

Temperatures show slow response, perhaps beyond the ranges of the PID library. A state regulator (or bang-bang regulator) may be more suitable, but I don't know of such libraries or tutorials. A simple bang-bang regulator would estimate the amount of time, required to reach the new temperature, then stop or restart heating at or somewhat before that time, to prevent overshooting. Afterwards a PID can be used to hold the temperature (switch mode back to AUTOMATIC).

unfortunately I don't know of any board with the size of a Nano, and the RAM of a Mega

Teensy 2 has 2.5kB ram and Teensy 3.2 has 64kB ram (and 256kB flash). Both are similar in size to the NANO board. Worth a look http://www.pjrc.com/teensy/index.html

Pete

To be frank, I hadn't considered using a different mcu, except when I was looking at using an ATTiny84. It had enough pins, but I knew it could never handle the ram requirements. Right now, the nano is soldered into some perfboard with the rest of the electronics, so if possible, I'd like to find places to trim, use smaller libraries, or cut out unneeded features (like the serial buffer modification), to lower the ram requirement and make room for the features I'd still like to add. Im starting to think its not possible to do all that I want in this tiny chip, but that wont stop me from at least getting the first stage done on it (temp control of the lead melting).

I did a little checking on bang-bang regulators. It seems to apply to my situation (I seen it used to describe residential heating control), but I could not find any usable references, say, to a formula or algorithm on a quick search. I'll keep checking on that front to see if it can fix my temp stability issues.

As for the GraphVal array, it tracks a line graph on the oled, showing the temperatures over the time scale (GTScale, in minutes), I dont know of any other way to keep the graphs history when the screen clears every loop. While this isnt necessary, as you pointed out, its been instrumental to understanding why the PID system isnt working, but not really how to fix it. The GraphTop and GraphBot are just to keep the graph using as much of the screen as possible, we are talking about a 1" (SUPER SMALL) screen. Those 2 variables are worth their minimal cost (though I'm open to better ways to do this).

I did get to thinking on the reciprocal idea; What actually happens in these MCUs when you divide by zero? Can you think of a way to make a divide by 0 equal 0 without using an additional variable, kinda like I did to toggle MenuMode between 0 and 1?

Thanks for the suggestions so far, keep 'em coming!

BTW, I figured it was kind of implied, but feel free to use any/all of this for any project you would like. If you need the actual connections and electronics, let me know, however most of them can be changed to whatever you want to use, I tried to keep it as modular as possible.

You should check if every double var / function you use needs to be a double.
Some might be int and still be accurate enough

have a look at your divisions, some could be replaced by a multiplication (faster and slightly smaller)
e.g. x/1000 ==> x * 0.001;

I've learned about bang-bang controllers in satellite positioning: give it one bang into the desired direction, then another bang to stop it there. The trick, as opposed to simple hysteresis-based controllers, is the time of the second bang, that shall stop the system at the right value. Such an application requires a valid model for the prediction of the dynamic behaviour of the object, and turns out to be a special case of a state regulator.

In your case you should know how the temperature evolves with a certain (typically maximal) heating, and how it behaves (over time) when heating is turned off again. You can get an estimation of the delays by monitoring the temperature after the heater is turned on or off, and adjust the delays accordingly (dynamic adaptation).

With two temperatures, taken at the heater and the interesting distant position (lead surface), you can try to determine the exponential function e^-(t/tau) that predicts the temperature at the distant position. You may notice that this function varies depending on what "load" is applied to the surface.

In a simpler model you can determine the over/undershoot, occurring after the heater is turned on or off, and adjust the switch points (hysteresis) dynamically, to minimize the overshoots. This way you'll end up in kind of a PWM controller, with both adjustable frequency and duty cycle. But if you want proper feedback about the overshoots, the frequency should be low enough to allow for taking the dynamic behaviour of the controlled system. Here the uncertainty (noise...) of the temperature measurement should be taken into account, so that a reasonable temperature difference (several degrees or ADC steps) must be allowed. Then the low PWM frequency is an immediate result of the allowed deviation, deserving no further adjustment or calculations.

Perhaps this restriction (temperature measurement) causes trouble in your PID controller, and you could make it work by filtering the temperature readings before feeding them into the PID.

F("F")

Using the F() macro for a single character is a waste of space. Use single quotes around single characters.

If you can convert your floating point calculations to integers you will save a lot of space. You must do it everywhere. The floating point system uses functions so if you use a division operation once it must compile the division function but using it twice doesn't add another copy of the function.

  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(WHITE);
  display.setCursor(0,0);  <<<<<<<<<

the cleardisplay call probably sets the cursor to 0,0 so one call to many


just a thought

  for (i = 0; i < 128; i++) {
    display.drawPixel(i,63 - (((GraphVal[i]-GraphBot)*47)/(GraphTop-GraphBot)), WHITE);
  }
  if ((GraphTop>Setpoint)&&(Setpoint>GraphBot)&&(Setline==1)) {
    for (i = 0; i < 128; i++) {
      display.drawPixel(i,63 - (((Setpoint-GraphBot)*47)/(GraphTop-GraphBot)), WHITE);
    }
  }

==>

float f = 47.0/GraphTop-GraphBot;  // precalculated value 
  for (i = 0; i < 128; i++) {
    display.drawPixel(i,63 - (((GraphVal[i]-GraphBot)* f), WHITE);
  }
  if ((GraphTop>Setpoint)&&(Setpoint>GraphBot)&&(Setline==1)) {
    for (i = 0; i < 128; i++) {
      display.drawPixel(i, 63 - (((Setpoint-GraphBot)* f), WHITE);
    }
  }

    for (i = 0; i < 128; i++){
      if (GraphVal[i] > GraphTop)
        GraphTop = GraphVal[i];
      if (GraphVal[i] < GraphBot)
        GraphBot = GraphVal[i];
    }

==>

    for (i = 0; i < 128; i++){
      if (GraphVal[i] > GraphTop)
        GraphTop = GraphVal[i];
      else if (GraphVal[i] < GraphBot)   
        GraphBot = GraphVal[i];
    }

As they cannot be both true or?

DrDiettrich:
I've learned about bang-bang controllers in satellite positioning: give it one bang into the desired direction, then another bang to stop it there. The trick, as opposed to simple hysteresis-based controllers, is the time of the second bang, that shall stop the system at the right value. Such an application requires a valid model for the prediction of the dynamic behaviour of the object, and turns out to be a special case of a state regulator.

In your case you should know how the temperature evolves with a certain (typically maximal) heating, and how it behaves (over time) when heating is turned off again. You can get an estimation of the delays by monitoring the temperature after the heater is turned on or off, and adjust the delays accordingly (dynamic adaptation).

With two temperatures, taken at the heater and the interesting distant position (lead surface), you can try to determine the exponential function e^-(t/tau) that predicts the temperature at the distant position. You may notice that this function varies depending on what "load" is applied to the surface.

In a simpler model you can determine the over/undershoot, occurring after the heater is turned on or off, and adjust the switch points (hysteresis) dynamically, to minimize the overshoots. This way you'll end up in kind of a PWM controller, with both adjustable frequency and duty cycle. But if you want proper feedback about the overshoots, the frequency should be low enough to allow for taking the dynamic behaviour of the controlled system. Here the uncertainty (noise...) of the temperature measurement should be taken into account, so that a reasonable temperature difference (several degrees or ADC steps) must be allowed. Then the low PWM frequency is an immediate result of the allowed deviation, deserving no further adjustment or calculations.

Perhaps this restriction (temperature measurement) causes trouble in your PID controller, and you could make it work by filtering the temperature readings before feeding them into the PID.

So, bang-bang is essentially educated guessing (or trial and error) of timing of the on's and off's of the relay. If I'm understanding right, unfortunately, that kind of system wont work for my application unless additional sensors to determine the physical size of the load (amount of lead being heated), or someone monitors and adjusts it during use (defeats the purpose of the controller), as the load size varies during operation (2 to 20 lbs). As well, the PID will be used to control at least 2 devices, and it doesnt look like its practical to use the same bang-bang system for both devices

Thank you for your explanation, if that was a usable option for me, that would have been almost everything I needed to get a working program going.

I have since added another 0 to the Ki dividers in AdjMenu() and increased the definition in the Ki adjustments (+- .01 instead of +_ .1). This seems to have made tuning easier, but since the heat up time of the process is about 20-30 mins, it will be some time before I know if those adjustments make it good enough to use.

Still, the AutoTune library is probably the next addition I will make, once I have the SRAM freed up and I figure out how to make AutoTune work with a relay (I've put 0 effort into this so far, bigger fish).

I do kind of filter the temperature readings before feeding them to the PID, they get averaged, 4 a second, before being assigned to Input. Id like to make it faster (say, about 8-10 a second), but that is a limitation of the 6675, it wont properly return results faster than 200ms, and even that was iffy, so I made it 250ms instead, that fixed all trouble with bad returns. If you meant real filtering (tossing out numbers that dont fit in the expected range), I dont think its possible to setup a filter narrow enough to allow the real readings and discard the fluctuations, since the noise fluctuations are typically only 1 ADC step or less (does that even count as a noise fluctuation?)

MorganS:

F("F")

Using the F() macro for a single character is a waste of space. Use single quotes around single characters.

If you can convert your floating point calculations to integers you will save a lot of space. You must do it everywhere. The floating point system uses functions so if you use a division operation once it must compile the division function but using it twice doesn't add another copy of the function.

Thanks for the clarification on the F() macro, I didn't know it wasn't needed for single chars.

I'll give my code another good once-over to see if there are any more variables I can demote. Some of them can't be, they need the decimal resolution. The Kp, Ki, and Kd values for instance, need to be decimal values, and when I tried to make them anything but double or float, they basically got chopped to an int (not even any rounding, not that I'd expect it). I thought I remember reading not to use float, due to limitations of the mcu, so they ended up being double (not sure why it seems to be acceptable to use double in place of float, same thing, just bigger, isn't it?). I would REALLY appreciate a better solution to this, as I am almost certain I don't actually need the resolution of a double, really all the variables need to hold is "x.xx", two decimal places and one whole number.

robtillaart:
Ill check into the setCursor when I dive back into the oled library, your probably right, that seems like it would be a logical step when clearing the screen. I never actually checked it.

For the rest of it, "just a thought" and down, I don't understand how adding a variable helps me lower SRAM usage or makes it easier to add the features I'd like to add. It does make that section a little more understandable (with the proper changes to the operation orders), but seems to be a step backwards in regards to my goals.

The if statement change doesn't seem to add any benefit either, am I missing something deeper in the compiler that makes using the elseif version better? The results are identical with either method (both conditionals will never be true at the same time).

Thanks to all for your input so far. One thing to note: Program/Sketch size is not an issue, I am only using about 60% of the nano's flash memory. The big issue is SRAM, sitting at about 94% used. While making the sketch smaller cant hurt, I'd happily make it larger to decrease SRAM usage. Sorry if it was unclear that program size is not the issue I am having, hopefully this clears that up.

Elryk:
So, bang-bang is essentially educated guessing (or trial and error) of timing of the on's and off's of the relay. If I'm understanding right, unfortunately, that kind of system wont work for my application unless additional sensors to determine the physical size of the load (amount of lead being heated), or someone monitors and adjusts it during use (defeats the purpose of the controller), as the load size varies during operation (2 to 20 lbs). As well, the PID will be used to control at least 2 devices, and it doesnt look like its practical to use the same bang-bang system for both devices

Thank you for your explanation, if that was a usable option for me, that would have been almost everything I needed to get a working program going.

I have since added another 0 to the Ki dividers in AdjMenu() and increased the definition in the Ki adjustments (+- .01 instead of +_ .1). This seems to have made tuning easier, but since the heat up time of the process is about 20-30 mins, it will be some time before I know if those adjustments make it good enough to use.

As already mentioned, state regulators are based on a mathematical/physical model of the controlled system. The "educated guessing" is part of an adaptive regulator, that adjusts its parameters based on the actual behaviour of the system. A complete model (of higher degree) is not always required, when you e.g. exclude the heat-up stage and start regulation only when the working point is reached.

Your problems with the PID regulators, as well as the changing load size, indicate to me a need for an adaptive system. The AutoTune library seems to try the same for PID regulators. But in contrast to a one-stage (first order) PID regulator, a state regulator can also handle systems of higher degree. Also PID regulators are relics from former (analogue) times, state regulators are better suited for digital regulators. Unfortunately the construction of a suitable system model requires some skills in physics and maths, what's often beyond my own capabilities. That's why I tried to present a simplified and more intuitive way to attack your problem.

More sensors are not a must, they are required only if the state of the system can not be fully "observed" from less information. Using another temperature sensor at the heater allows to split the entire system into two parts, so that a second order PID or state regulator can be used.

WRT code optimization, an optimization at statement level is not really the right way to go, in detail if you have left enough program memory. Statement level optimization is fine to increase the readability of the code, so that structural errors become more obvious and can be eliminated.

If you need decimals but want to avoid the overhead of floats, use fixed point. This is like using cents to count dollars. Store 100 in a variable but print it as $1.00 when you need to show it to the outside world. Exactly the same method works for any physical measurement like degrees or volts.

This won't help much with SRAM usage. A float is 6 bytes and an int or long int is 2 or 4 but you will need more variables to handle the fixed point input or output.

DrDiettrich:
As already mentioned, state regulators are based on a mathematical/physical model of the controlled system. The "educated guessing" is part of an adaptive regulator, that adjusts its parameters based on the actual behaviour of the system. A complete model (of higher degree) is not always required, when you e.g. exclude the heat-up stage and start regulation only when the working point is reached.

Your problems with the PID regulators, as well as the changing load size, indicate to me a need for an adaptive system. The AutoTune library seems to try the same for PID regulators. But in contrast to a one-stage (first order) PID regulator, a state regulator can also handle systems of higher degree. Also PID regulators are relics from former (analogue) times, state regulators are better suited for digital regulators. Unfortunately the construction of a suitable system model requires some skills in physics and maths, what's often beyond my own capabilities. That's why I tried to present a simplified and more intuitive way to attack your problem.

More sensors are not a must, they are required only if the state of the system can not be fully "observed" from less information. Using another temperature sensor at the heater allows to split the entire system into two parts, so that a second order PID or state regulator can be used.

WRT code optimization, an optimization at statement level is not really the right way to go, in detail if you have left enough program memory. Statement level optimization is fine to increase the readability of the code, so that structural errors become more obvious and can be eliminated.

Wow, I really appreciate your dedication to my understanding this correctly. The trouble is, I'm still seeing that system as a trial and error and/or guessing, based on whats expected to happen. Even your second description here leads me to the conclusion that I should simply create a model of the process using full on then full off (for heat up and cool down) and hard code some temperature limits (switch times) based on that model. That may work perfectly well for one system, but this controller was originally intended to control a total of 3 devices (that may end up being only 2, powder coating oven and lead melting furnace). Adding the second device adds additional complexity that is not needed with PID, where I only need to save 2 sets of tuning values.

In my research, I have only found 3 useful resources. One is an extruder example, with no details, basically just a 1/2 page of "why use Bang Bang vs PID for extruders"...Not helpful... The other 2 require subscriptions to online libraries to view. None of the info I have found explains in any way how to turn the model into a workable process; No math, no physics, just a basic "this is why" with no "how".

Its clear now, I either need a bang bang formula to drop in place of the PID, or an existing bang bang lag dominated temperature process to examine and adapt to my needs. If you can provide a link to some definitive data on how to achieve this, I'd like to take a look at it, otherwise, I think I am just going to go back to my original plan, which is to lower SRAM usage enough to add the autotune library and attempt to figure out how to make it work with a relay PID system.

At the risk of sending us into a tangent before the refocus: I have no idea what you are trying to say in that last part, WRT, as in the router firmware (do a google search using WRT and anything, nothing but firmware results)? lol. Code optimization? Thats not a problem or concern at all for me right now. Program memory is not a problem or concern for me. SRAM is the problem and the biggest concern. 40%-ish free program memory, 6% free SRAM.

Lets try to veer this topic onto a single objective. I want to focus on areas to lower SRAM usage (NOT program memory!!!). Ill leave the other problems to resolve once I have the room to try other options. I thought I could combine all the issues I am having into a single thread, but that seems to be causing only confusion on my actual needs, so lets keep it simple: SRAM. I have already looked at all the variables I am using, none of them can be reduced without sacrificing functionality, or breaking things completely.

MorganS:
If you need decimals but want to avoid the overhead of floats, use fixed point. This is like using cents to count dollars. Store 100 in a variable but print it as $1.00 when you need to show it to the outside world. Exactly the same method works for any physical measurement like degrees or volts.

This won't help much with SRAM usage. A float is 6 bytes and an int or long int is 2 or 4 but you will need more variables to handle the fixed point input or output.

Does that work when you have an external formula that relies on decimal values? For example, if i I were to change the Kp value to int, Currently its set at 4.2, I could make it an int of 42, but when I divide it by 10 to perform the calculations, doesnt it lose the .2 and just become 4? All of my testing did exactly that.

...it wont properly return results faster than 200ms, and even that was iffy, so I made it 250ms instead...

So you have not determined the actual time constant. (I suspect 250ms is WAY too fast.)

As far as I can tell, one potential problem that you have not addressed is whether or not the system itself is stable. In my experience, controlling temperature with PID works very well. That you are having so much trouble indicates something other than PID is at play. For example, poorly positioning the temperature sensor is a mistake I have endured.

Determining tuning parameters over two small ranges will give an indication of what you face. Use...
https://www.google.com/search?q=ziegler+nichols+pid+tuning+example
...to tune from 150° F to 200° F. Is that stable? Are you able to change the setpoint within that range with a good / expected response?

Repeat that experiment for 250° F to 300° F. Same questions.

Are the tuning parameters different?

Try controlling in the high range using parameters from the low range. Vice versa.

As I have already stated, at the same time i mentioned the 250ms restriction, the 250ms has nothing to do with the PID, that restriction is on the MAX6675 (the thermocouple conversion module). Trying to read the MAX6675 faster than 200ms returns nothing or gibberish. Again, this restriction is on the number of times in a second (4) I can read the temperature. Increasing this delay will not help the PID, what it will actually do is make the final temperature measurement less accurate in relationship to time (the actual temp will change, but the reported temp will not).

I tried to use the ziegler method, but by my own experience, and the experiences of many other people, this method (and others like it) do not work well (if at all) with lag dominated systems. I tried, and failed with this method.

The thermocouple is properly placed and reads temperatures as expected. The original temperature control (bi-metal reed switch) works as expected when not mechanically disabled to allow for the PID controller. This tells me the heating portion of the system works properly as well. Those are the only 2 modifications to allow for the PID (relocating the temp probe to the load, rather than next to it, and disabling the reed switch so it doesn't interfere.

In any case, this isnt my primary problem, and I am no longer concerned with this issue until I can free up some SRAM to attempt to use the autotune library. Ill go back and remove those parts from the original post so this is more clear.

I was wondering if the heat of fusion of lead might be some of the reason the controller is having trouble with the temperature. While lead has a relatively low heat of fusion, it may be enough to cause a temperature control to overshoot during heating and under estimate during cooling. I don't know if this has anything to do with it just thought I would offer it up.
Tom

Hi,
How long does it take for a typical oven load to get from ambient to your target temperature with the heater ON all the time?

Have you tried the ultra simple.
If temp < setpoint+hysteresis, turn element ON.
If temp < setpoint-hysteresis turn elemrnt OFF.

Thanks… Tom… :slight_smile:

TKall:
I was wondering if the heat of fusion of lead might be some of the reason the controller is having trouble with the temperature. While lead has a relatively low heat of fusion, it may be enough to cause a temperature control to overshoot during heating and under estimate during cooling. I don’t know if this has anything to do with it just though I would offer it up.
Tom

It is a problem (mentioned in the OP as a “phase shift”), one that I had yet to overcome, however, as previously stated, this is not a problem I care about now, until I free up some SRAM. Autotune will be used once it will fit (SRAM, NOT FLASH). My hope is that the autotune library will do most/all of the tuning for me, but I still have the SRAM issue to contend with before I can try.

TomGeorge:
Hi,
How long does it take for a typical oven load to get from ambient to your target temperature with the heater ON all the time?

Have you tried the ultra simple.
If temp < setpoint+hysteresis, turn element ON.
If temp < setpoint-hysteresis turn elemrnt OFF.

Thanks… Tom… :slight_smile:

Temperature control is no longer an issue I am going to fight with. My concern is PURELY SRAM now.