Digital Control for a mobile heater with Statemachine

Hi everybody,

this is my first comment/question in this forum an more important, my first programming Project and my english is not perfect, so please be patient. If the project hub is more suitible, please let me know.

"The big picture":
I´m working on a controll-unit for a mobile heating system since 3 weeks. Until now the heater is controlled manually, this causes serveral problems. Like all mobile heaters or engines with diesel, it needs a propper preheating phase. If the delays are not taken into account correctly while heating up manually, the combustion chamber is fast filled with to much diesel und it smokes like engines 100 years ago... :confused: So I need a propper and stable System.

"Why do I need youre help / input:"
As I mentioned, my programming skills are very rudimentary. I try to learn as much as possible about the basics, like loops, hardware, const and variables. But it terms of programm-architecture (failsafe) and efficiency, I would be more then happy to get some feedback from experts. FOr example I´m wondering how it´s possible to implement a countdown in a for loop, wich is part of a state.

"Functions"
On / Off (debounced with millis() ) // if button pressed change Variable "mainSwitchInt" between 0 / 1
Automatic Preheating Phase{
if combustion chamber is cold{

  1. Fan on
  2. wait 4 sec
    if delayButton is pressed put the the countdown on hold for manual intervention
  3. Preheater on
  4. wait 10 sec und show countdown an display
    if delayButton is pressed put the the countdown on hold for manual intervention
  5. Magnetic Valve Diesel on
  6. wait 10 sec und show countdown an display
    if delayButton is pressed put the the countdown on hold for manual intervention
    }
    if combustion chamber hot go to state HEATING
    else (it is cold) go to state ERROR
    } // Preheating Phase end

"Test build":
For testing i connected a Arduino Uno with 6 LEDs, 3 buttons, 1 switch and a 16x2 display on a breadboard.

const int FAN_LED = 2;                    // displays aktive fan
const int DIESEL_LED = 3;                // display open magnetic valve for DIESEL
const int PREHEATING_LED = 4;         // displays aktive preheater
const int HEATING_LED = 5;              // displays case HEATING
const int ERROR_LED =6;                  // desplay ERROR (blinking)
const int AKTIVE_LED = 7;                // displays CPU-Activity

const int MAIN_SWITCH = 12;           // On / Off Main switch
const int RESET = 11;                      // Softwarereset (go to case OFF)
const int TEMP_SENSOR = 13;           // displays combustion chamber = hot
const int DELAY_MAIN_SWITCH = 10; // if switch is aktive, preheatingphases 
                                                   // are on hold for manual intervention

The next important Part was how to organize the different states like On, Off, Preheating, Heating, Error.
I used a Statemachine with some advantages und disadvantages. The main problem I have, is the architecture of Statemachines. As I understood they only go once in the State, if there is a impulse set a StateChange. In my case it is due to the if conditions, you will see below...

// Lists of States
enum State {OFF, PREHEATING, HEATING, SHUT_DOWN, ERROR};

State currentState;
State lastState = OFF;

And then in the void loop i started to organize the states via if conditions:

///////////////////////////////////////
////          ERROR STATES         ////
///////////////////////////////////////
// Diesel empty or comustion Chamber cold (during case == HEATING)
    if (currentState == HEATING && cold) {
      lcd.setCursor(0,1); // 
      lcd.print("                ");      
      lcd.setCursor(0,1); 
      lcd.print("Temp / Diesel ?");
     currentState = ERROR;}

// if RESET_BUTTON is presset, go to case == OFF
    if (currentState == ERROR && reset){currentState = OFF; mainSwitchInt = 0;}


///////////////////////////////////////
////       OPERATING STATES        ////
///////////////////////////////////////
    if(mainSwitchInt == 1) {
      if (currentState == OFF && (cold || hot))  {currentState = PREHEATING;}    
      if (currentState == SHUT_DOWN)  {mainSwitchInt = 0;}      
    }
  
    if(mainSwitchInt == 0) {
      if(currentState == PREHEATING)           {currentState = OFF;}
      if(currentState == HEATING     && hot)   {currentState = SHUT_DOWN;}
      if(currentState == SHUT_DOWN && cold)  {currentState = OFF;}
    }


///////////////////////////////////////
////       CPU-AKTIVITY-Test       ////
///////////////////////////////////////
// diplays the CPU-AKTIVITY
if (currentState != ERROR){
          if(millis() >= (resetTime + betriebsDelay)) {
            resetTime = millis();
              if(digitalRead(AKTIVE_LED) == LOW){digitalWrite(AKTIVE_LED, HIGH);}
              else{digitalWrite(AKTIVE_LED, LOW);}
          }
}
else{
  digitalWrite(AKTIVE_LED, LOW);
}

And now the states themselves:

///////////////////////////////////////
////         STATEMACHINE          ////
///////////////////////////////////////
// State-Change, if Main switch changes between 0 || 1
  if(currentState != lastState){
    lastState = currentState;
    switch(currentState) {

      case OFF:
        //easy LED stuff
      break;

      case PREHEATING:
        // see "Functions" above
      break;

      case HEATING:
        //easy LED stuff
      break;

      case SHUT_DOWN:
        //easy LED stuff
      break;

      case ERROR:
        //easy LED stuff
      break;

The problem I have is the state PREHEATING, because of the need of waiting loops with abort conditions if comustion chamber is hot. I tried to use millis(), but wasn´t succsessfull. I managed to get it working with for loops.

I try to attache the whole file. If it´s not working You will find it on github:

Thank you for new input, thoughts and Ideas, especially for the quite complex PREHEATING state!

Cheers

The essence of a state machine is that only the code for the current state plus any common code is executed. You need to determine how many states there are and what the condition(s) are that will make the program to a different state.

As to your PREHEATING state there are two exit conditions :
1 - 10 seconds have elapsed since the program entered that state
or
2 - the delay button has been pressed

Before setting the state to PREHEATING save the value of millis() as the start time
The code for the state would look something like this

case PREHEATING :
if (millis() - startTime >= 10000)
{
  //code here to set the destination state
}
else if (digitalRead(delayButton) == LOW)
{
  //code here to set the destination state
}
break;

I see.

I think I´ve forgotten to save the millis() at the beginning, when I tried something like youre recommendation. I will try that this evening, thanks!

The trick with state machines is to set the values needed on entry to a state before entering that state. You would normally do that on exit from the previous state then on exit from that state set the values for the next one and so on.

Do not use for loops inside states unless they run very quickly as that will cause the program to stall. Instead increment a counter at the start of the state code and use that value as you would a for loop variable

I tried to develope a case with millis() as a delay function within a case.
After some try and errors und Serial.print() various Variables I realized, that I had a switch-condition which is only reacting once if there is a state change. After this change, the if conditions wount be checked again.

But when I disable if(currentState != lastState){lastState = currentState; ...}} the Statemachine will act like a loop and the trick of only go through a case once is overridden.

const int LED1 = 3;                  // displays aktive fan
const int LED2 = 2;               // display open magnetic valve for DIESEL
const int BUTTON = 12;             // On / Off Main switch

int mainSwitchState;                    // actual Input MainSwitch
int lastmainSwitchState = HIGH;         // previous Input MainSwitch
int mainSwitchInt;                      // first State MainSwitch after Systemstart

unsigned long preheatStart;
unsigned long preheatDelay1 = 4000;
unsigned long preheatDelay2 = 4000;

enum State {ONE, TWO};

State currentState;
State lastState = TWO;

void setup() {
  pinMode (LED1, OUTPUT);
  pinMode (LED1, OUTPUT);
  pinMode (BUTTON, INPUT_PULLUP);

  Serial.begin(9600);
}

void loop() {
  Serial.println(preheatStart);
  mainSwitchInt = digitalRead(BUTTON);
  if(mainSwitchInt == 0) {currentState = ONE;} // gedrückt
  if(mainSwitchInt == 1) {currentState = TWO;} // offen

  if(currentState != lastState){
    lastState = currentState;
    
    switch(currentState) { 

      case ONE:
        digitalWrite(LED1, HIGH);   
        digitalWrite(LED2, LOW);
        preheatStart = millis(); 
      break;

      case TWO:
        if (millis() - preheatStart >= preheatDelay1){
        digitalWrite(LED1, LOW);
        digitalWrite(LED2, HIGH);
        Serial.println("TWO");  
      }
      break;     
    }  
  }    
}

So maby I should place the millis() loop in the void loop() itselfe or directly use another meathod than Statemachine?

Why have you taken PREHEATING out of the switch/case ?

A state machine is ideal for what you're trying to do. Take another look at reply #3. Your original state names were a lot better too.

Thank you for your advice!
... and I replaced the case names :slight_smile:

I outsourced the loops in the if conditions, wich are organizing the cases and it seems to work.
Now I try to implement all the other functionalities and Error cases.

Here you find the actual code:

const int FAN_LED = 2;                  // displays aktive fan
const int DIESEL_LED = 3;               // display open magnetic valve for DIESEL
const int PREHEATING_LED = 4;           // displays aktive preheater
const int HEATING_LED = 5;              // displays case HEATING
const int ERROR_LED =6;                 // desplay ERROR (blinking)
const int AKTIVE_LED = 7;               // displays CPU-Activity

const int MAIN_SWITCH = 12;             // On / Off Main switch
const int RESET = 11;                   // Softwarereset (go to case OFF)
const int TEMP_SENSOR = 13;             // displays combustion chamber = hot
const int DELAY_MAIN_SWITCH = 10;       // if switch is aktive, preheatingphases are on hold for manual intervention

int mainSwitchState;                    // actual Input MainSwitch
int lastmainSwitchState = LOW;          // previous Input MainSwitch
int mainSwitchInt;                      // first State MainSwitch after Systemstart

int preheatInt;                       
int preheatStart;
int preheatDelay1 = 3000;
int preheatDelay2 = 5000;
int preheatDelay3 = 8000;

int hot;                                // hot = digitalRead(TEMP_SENSOR) == 0;
int cold;                               // cold = digitalRead(TEMP_SENSOR) == 1;
int reset;                              // reset = digitalRead(RESET) == 0;

unsigned long lastDebounceTime = 0;     // last time stamp when main switch is on
unsigned long debounceDelay = 100;


enum State {OFF, PREHEATING_1, PREHEATING_2, PREHEATING_3, HEATING};

State currentState;
State lastState = OFF;

void setup() {
  pinMode (AKTIVE_LED, OUTPUT);
  pinMode (FAN_LED, OUTPUT);
  pinMode (DIESEL_LED, OUTPUT);
  pinMode (PREHEATING_LED, OUTPUT);
  pinMode (HEATING_LED, OUTPUT);
  pinMode (ERROR_LED, OUTPUT);
  
  pinMode (MAIN_SWITCH, INPUT_PULLUP);
  pinMode (TEMP_SENSOR, INPUT_PULLUP);    // Temp sensor (Flammenwächter), 0 or 24 V signal
  pinMode (RESET, INPUT_PULLUP);
  pinMode (DELAY_MAIN_SWITCH, INPUT_PULLUP);

  Serial.begin(9600);
}

void loop() {
//  Serial.println(preheatStart);

  hot = digitalRead(TEMP_SENSOR) == 0;
  cold = digitalRead(TEMP_SENSOR) == 1;

  // Debounce-Function with State-change 0 / 1 (safety)  
  int reading = digitalRead(MAIN_SWITCH);
  if (reading != lastmainSwitchState) {
    lastDebounceTime = millis();
  }
  if ((millis() - lastDebounceTime) > debounceDelay) {
    if (reading != mainSwitchState) {
      mainSwitchState = reading;
      if (mainSwitchState == LOW) {
        mainSwitchInt = !mainSwitchInt;
      }
    }
  }
  lastmainSwitchState = reading;

  if(mainSwitchInt == 0){
    if(currentState == OFF && (cold || hot)){
      currentState = PREHEATING_1;}
    
    if(currentState == PREHEATING_1 && preheatInt == 1){
      if(millis()- preheatStart > preheatDelay1){
      currentState = PREHEATING_2;}
    }
    
    if(currentState == PREHEATING_2 && preheatInt == 2){
      if(millis()- preheatStart > preheatDelay2){
      currentState = PREHEATING_3;}
    }
    
    if(currentState == PREHEATING_3 && preheatInt == 3){
      if(millis()- preheatStart > preheatDelay3){
      currentState = HEATING;}
    }
  }
  
  if(mainSwitchInt == 1) {
    if(currentState = HEATING){currentState = OFF;}
  }

  if(currentState != lastState){
    lastState = currentState;
    switch(currentState){ 

  case OFF:
  digitalWrite(FAN_LED, HIGH);   
  digitalWrite(DIESEL_LED, LOW);
  digitalWrite(PREHEATING_LED, LOW);
  digitalWrite(HEATING_LED, LOW);        
  digitalWrite(ERROR_LED, LOW);
  digitalWrite(AKTIVE_LED, LOW);
  Serial.println("OFF");
  delay(2000);
  break;

  case PREHEATING_1:
  digitalWrite(FAN_LED, HIGH);   
  digitalWrite(DIESEL_LED, LOW);
  digitalWrite(PREHEATING_LED, HIGH);
  digitalWrite(HEATING_LED, LOW);        
  digitalWrite(ERROR_LED, LOW);
  digitalWrite(AKTIVE_LED, HIGH);
  Serial.println("PREHEATING_1");
  preheatInt = 1;
  preheatStart = millis();
  break;

  case PREHEATING_2:
  digitalWrite(FAN_LED, HIGH);   
  digitalWrite(DIESEL_LED, HIGH);
  digitalWrite(PREHEATING_LED, HIGH);
  digitalWrite(HEATING_LED, LOW);        
  digitalWrite(ERROR_LED, LOW);
  digitalWrite(AKTIVE_LED, HIGH);
  Serial.println("PREHEATING_2");
  preheatInt = 2;
  preheatStart = millis();
  break;

  case PREHEATING_3:
  digitalWrite(FAN_LED, HIGH);   
  digitalWrite(DIESEL_LED, HIGH);
  digitalWrite(PREHEATING_LED, HIGH);
  digitalWrite(HEATING_LED, LOW);        
  digitalWrite(ERROR_LED, LOW);
  digitalWrite(AKTIVE_LED, HIGH);
  Serial.println("PREHEATING_3");
  preheatInt = 3;
  preheatStart = millis();
  break;

  case HEATING:
  digitalWrite(FAN_LED, HIGH);   
  digitalWrite(DIESEL_LED, HIGH);
  digitalWrite(PREHEATING_LED, LOW);
  digitalWrite(HEATING_LED, HIGH);        
  digitalWrite(ERROR_LED, LOW);
  digitalWrite(AKTIVE_LED, HIGH);
  Serial.println("HEATING");
  delay(2000);
  break;

  }  
 }    
}

The idea, and indeed the benefit of a state machine, is that the code lives in the states. You've got a curious variant where the state setting happens outside the state switch and precious little of interest happens inside.

It's also made your loop function all but incomprehensible (at least to me).

As suggested above you have created a program where very little happens inside the cases which has made the code very hard to follow. You can simplify it considerably

For instance instead of

    if (currentState == OFF && (cold || hot))
    {
      currentState = PREHEATING_1;
    }

being in loop() the test for cold || hot and change to the PREHEATING_1 state belongs in the OFF case of the switch/case and the same for other tests of the current state. Doing this avoids the need to check the current state as in the switch/case only code for that state will be executed.

I´m not sure, if I understood you right. Is the switch/case equal to the following code?

...
  if(currentState != lastState){
    lastState = currentState;
    switch(currentState){
...

Is the switch/case equal to the following code?

No

My point was that when you are in a case in switch/case you already know what the current state is so there is no need to test its value

Mhhh...but where to place the millis() if not within the if-conditions?

Everything, wich is placed under

  if(currentState != lastState){
    lastState = currentState;
    switch(currentState){ 
  ...

is only checked once. So I can put the if-conditions inside the switch/state, but without the millis() delay.
It will only go through the code once and then break, because last state = currentState.

The millis() delay will work, when I delete the if(currentState != lastState)-condition. But then the state is tested continously and I loose the advantage of not checking the state all the time.

//  if(currentState != lastState){
//    lastState = currentState;
    switch(currentState){ 
  ...

When I still dind´t get the point I´m sorry! Maby It´s because of my english, mayby my experience, or my brain :-)? In this case, please give me an example. I put the actual version here, feel free to distroy it!

https://github.com/Mirjka/diesel-heater-controll-v02

Take a look at this:

///////////////////////////////////////
////   Include LCD 16x2 DISPLAY    ////
///////////////////////////////////////
#include<Wire.h>
#include<LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27, 20, 4);

const int FAN_LED = 2;                  // displays aktive fan
const int DIESEL_LED = 3;               // display open magnetic valve for DIESEL
const int PREHEATING_LED = 4;           // displays aktive preheater
const int HEATING_LED = 5;              // displays case HEATING
const int ERROR_LED = 6;                // desplay ERROR (blinking)
const int AKTIVE_LED = 7;               // displays CPU-Activity

const int MAIN_SWITCH = 12;             // On / Off Main switch
const int RESET = 11;                   // Softwarereset (go to case OFF)
const int TEMP_SENSOR = 13;             // displays combustion chamber = hot
const int DELAY_MAIN_SWITCH = 10;       // if switch is aktive, preheatingphases are on hold for manual intervention

int startInt = 1;
int preheatInt;
unsigned long preheatStart;
unsigned long fanStart;
unsigned int FanRunTime = 4000;
unsigned int preheatRunTime = 10000;

bool hot;                                // hot = digitalRead(TEMP_SENSOR) == 0;
bool cold;                               // cold = digitalRead(TEMP_SENSOR) == 1;
bool reset;                              // reset = digitalRead(RESET) == 0;

unsigned long lastDebounceTime = 0;     // last time stamp when main switch is on
unsigned long debounceDelay = 100;

int reading;
enum State {OFF, RUNNING_FAN, PREHEATING, HEATING};

State currentState;
State lastState = OFF;

void setup()
{
  lcd.init();
  pinMode (AKTIVE_LED, OUTPUT);
  pinMode (FAN_LED, OUTPUT);
  pinMode (DIESEL_LED, OUTPUT);
  pinMode (PREHEATING_LED, OUTPUT);
  pinMode (HEATING_LED, OUTPUT);
  pinMode (ERROR_LED, OUTPUT);
  pinMode (MAIN_SWITCH, INPUT_PULLUP);
  pinMode (TEMP_SENSOR, INPUT_PULLUP);    // Temp sensor (Flammenwächter), 0 or 24 V signal
  pinMode (RESET, INPUT_PULLUP);
  pinMode (DELAY_MAIN_SWITCH, INPUT_PULLUP);
  digitalWrite(FAN_LED, LOW);
  digitalWrite(DIESEL_LED, LOW);
  digitalWrite(PREHEATING_LED, LOW);
  digitalWrite(HEATING_LED, LOW);
  digitalWrite(ERROR_LED, LOW);
  digitalWrite(AKTIVE_LED, LOW);
  lcd.backlight();
  Serial.begin(115200);
}

void loop()
{
  hot = digitalRead(TEMP_SENSOR) == 0;
  cold = digitalRead(TEMP_SENSOR) == 1;
  reset = digitalRead(RESET) == 0;
  // Only for first start
  if (startInt == 1)
  {
    lcd.setCursor(0, 0);
    lcd.print(">> Gebootet");
    lcd.setCursor(0, 1);
    lcd.print("Hallo 206!");
    startInt = 0;
  }
  switch (currentState)
  {
    case OFF:
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print(">> OFF");
      reading = digitalRead(MAIN_SWITCH);
      if (reading == LOW)
      {
        delay(20); //debounce
        currentState = RUNNING_FAN;
        preheatStart = millis();
        digitalWrite(FAN_LED, HIGH);
        digitalWrite(AKTIVE_LED, HIGH);
      }
      break;
    case RUNNING_FAN:
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print(">> Running fan");
      if (millis() - fanStart >= FanRunTime)
      {
        currentState = PREHEATING;
        preheatStart = millis();
        digitalWrite(PREHEATING_LED, HIGH);
      }
      break;
    case PREHEATING:
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print(">> PREHEATING");
      if (millis() - preheatStart >= preheatRunTime)
      {
        currentState = HEATING;
        digitalWrite(DIESEL_LED, HIGH);
        digitalWrite(PREHEATING_LED, LOW);
        digitalWrite(HEATING_LED, HIGH);
      }
      break;
    case HEATING:
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print(">> HEATING");
      break;
  }
}

It's not a complete execution of your requirements and has a number of flaws (rapidly flashing LCD for one). But it illustrates a common method of running a state machine. Note in particular that it visits the switch on every iteration of loop. Note also how it handles delays using millis.

Finally, note that as UKHeliBob said earlier, most of the work is done at the time of transition between states i.e. once the trigger for a new state is observed, set the pins for that new state once. This also helps with capturing millis for the next state's timing once only.

Hopefully, you can add your additional requirements to this framework, or something like it.

Cool, thanks alot!

This is a smart architecture for what i want to do. I think I got the idea about what UKHeliBob was telling me.
Iam looking forwoard to try it later at home with my hardware and to add my additional requirements.

I added all my requirements with trying taking into account what I was recommended to do by wildbill and UKHeliBob. And it seems to work very stable and well :).

First I deleted a condition in my previous code wich caused, that the code in the Statemachine is only checked, if some previous if-conditions registered a change. As I know now it´s not what a Statemachine should look like. Then I put the if cconditions into the states itselve. Every value is needed, is generated right before the state is entered, where it is needed. In this way there is no flaws (rapidly flashing LCD). Essentially everything is happening in the state which is activated. Nevertheless millis() and other variables are still usale (not like in for-loop, wich I used before...).

Because of the length you will find the new code under gitHub.