Understanding state machine example

I'm trying to set up a state machine that works off a timer, so that it runs on its own, without requiring a button press or other input. I found wildbill's example in another thread:
http://forum.arduino.cc/index.php/topic,59511.0.html

Since it was set up to use servos, I removed all the servo code so it's just the timer and the state machine, and commented it for my own understanding:

// State Machine example from Arduino forum, timer controls state machine
// http://forum.arduino.cc/index.php/topic,59511.0.html
// Original author: wildbill

// States are named for ease of use
#define START   0  
#define SCANNING   1
#define RESULTS  2
#define RESET 3

// Timing variables
int StartTime = 500L;
int ScanTime = 1500L;
int RESULTSTime = 1500L;
int ResetTime = 2000L;

int state=START;  // initial state
unsigned long NextAction=0L;  // NextAction set to 0.  "L" = long data format, used for timer.

void setup() {
  Serial.begin(9600);    // initialize serial communication for debugging
}

void loop()
{
 static int PulseLength=0;  // Starts at zero.  Declared "static" so it persists.  
  if(millis()>NextAction)    // If time since start (millis) is greater than NextAction...
    PulseLength=ChangeState();  // ...then PulseLength is updated to ChangeState value.
 }

int ChangeState()  // 2 byte value
{
//  int PulseLength=0;
  switch (state)
  {
  case START:
    Serial.println("case START");
    NextAction+=StartTime;  // NextAction is incremented 
//    PulseLength=1;
    state=SCANNING;
    break;
  case SCANNING:
    Serial.println("case SCANNING");
    NextAction+=ScanTime; // NextAction is incremented
//    PulseLength=2;
    state=RESULTS;
    break;
  case RESULTS:
    Serial.println("case RESULTS");
    NextAction+=RESULTSTime;  // NextAction is incremented
//    PulseLength=3;
    state=RESET;
    break;
  case RESET:
    Serial.println("case RESET");  
    NextAction+=ResetTime;  // NextAction is incremented
//    PulseLength=4;
    state=START;
    break;
  }
//  return PulseLength;
}

This works - you can open the Serial Monitor and see the states changing, based on what was set in the timing variables. But, there are a few things about the logic that I don't understand. It's the loop:

void loop()
{
 static int PulseLength=0;  // Starts at zero.  Declared "static" so it persists.  
  if(millis()>NextAction)    // If time since start (millis) is greater than NextAction...
    PulseLength=ChangeState();  // ...then PulseLength is updated to ChangeState value.
 }

int ChangeState()  // 2 byte value
  • Why is ChangeState initialized outside the loop?
  • I have commented out all the instances of PulseLength later on (since it was used to control a servo), but exactly how is it being used to manage the state machine?

I get the sense that this could be simplified but right now I can't figure it out.

ChangeState is a function (notice the parentheses after the name). It is not "initialized", it is defined. C and C++ require this definition to be outside the definition of any other functions (such as loop).

This is suspicious:

// Timing variables
int StartTime = 500L;
int ScanTime = 1500L;
int RESULTSTime = 1500L;
int ResetTime = 2000L;

Long int constants are used to initialize (not-long) int variables. It works, but the moment that you try something like:

int ResetTime = 32768L;

you are going to have a problem. 32768L is a legal long int constant but it is not a legitimate value for a (non-long) int variable. I am not sure whether it is going to cause a compiler error message or wrap around to -1, but it is going to be annoying.

Long int constants are used to initialize (not-long) int variables.

Thanks for the catch on that. I've changed them to long.

It would be better to change them to be:

const int StartTime = 500;
const int ScanTime = 1500;
const int RESULTSTime = 1500;
const int ResetTime = 2000;

Since they won't ever change (hence the 'const' keyword) and they are much smaller than a long you can save some memory by keeping them ints.

FWIW,

Brad
KF7FER

Thanks, Brad. It does save memory:

const int - 2,838 bytes
long - 2,918 bytes

OK, I've done some more reading on C++ functions. If you follow the examples here:

then it's very easy to see how a function is called, and (2 * 3) is 6.

What I still don't quite get is how ChangeState() is called below. Is that from the first line in the loop, the static int, once PulseLength has been updated?

// State Machine example from Arduino forum, timer controls state machine
// http://forum.arduino.cc/index.php/topic,59511.0.html
// Original author: wildbill

// States are named for ease of use
#define START   0  
#define SCANNING   1
#define RESULTS  2
#define RESET 3

// Timing variables
const int StartTime = 2500;
const int ScanTime = 1500;
const int RESULTSTime = 1500;
const int ResetTime = 2000;

int state=START;  // initial state
unsigned long NextAction=0L;  // NextAction set to 0.  "L" = long data format, used for timer.

void setup() {
  Serial.begin(9600);    // initialize serial communication for debugging
}

void loop()
{
 static int PulseLength=0;  // Declared "static" so it persists and is visible only to loop function.
  if(millis()>NextAction)    // If time since start (millis) is greater than NextAction...
    PulseLength=ChangeState();  // ...then PulseLength is updated to ChangeState value.
  Serial.println(PulseLength);  
 }

int ChangeState()  // Function declared outside of the main loop, for managing the state
{
//  int PulseLength=0;
  switch (state)
  {
  case START:
    Serial.println("case START");
    NextAction+=StartTime;  // NextAction is incremented 
//    PulseLength=1;
    state=SCANNING;
    break;
  case SCANNING:
    Serial.println("case SCANNING");
    NextAction+=ScanTime; // NextAction is incremented
//    PulseLength=2;
    state=RESULTS;
    break;
  case RESULTS:
    Serial.println("case RESULTS");
    NextAction+=RESULTSTime;  // NextAction is incremented
//    PulseLength=3;
    state=RESET;
    break;
  case RESET:
    Serial.println("case RESET");  
    NextAction+=ResetTime;  // NextAction is incremented
//    PulseLength=4;
    state=START;
    break;
  }
//  return PulseLength;
}

I wrote an extended demo based on the principles of the Blink Without Delay example sketch here. It also uses global variables to manage timing so it may be useful to study it in parallel with the code you have.

In the code you already have "PulseLength=ChangeState();" is the line that calls the ChangeState() function.

...R

Thanks, I appreciate the tip. I'll go over that long thread tomorrow - it looks like a good resource!

I re-read wildbill's explanation on the other page. I think I get it now...

NextAction is the time in millis when the next state change will occur. All your state changes are time based, so we just check on every iteration of loop to see whether that time has been passed. If so, we call ChangeState to set up what to do next.

  static int PulseLength=0;  
  if(millis()>NextAction)    
    PulseLength=ChangeState();

So PulseLength=ChangeState(); is the function call. There are no parameters passed to it.

The example is attached to the first post. A lot of the discussion was about an esoteric point about timing which is now incorporated in the example.

…R

For some reason, I didn't bother making that code deal with rollover so if you're planning on running this for a long time (i.e. more than 49 days) it'll need a little restructuring. Search for millis and subtraction and you'll likely find one of the many discussions on the topic.

Thanks wildbill. I read several threads on this, I get the point mentioned - always use subtraction when computing time to avoid rollover after 49 days.

use the idiom (a - b > interval)

So this is the problematic code:

  if(millis()>NextAction)

And this fixes it?

  BootTime = millis();  // Capture time at start

...

  CurrentTime = millis();  // Get the current millisecond count
   if (CurrentTime - BootTime > NextAction)

The addition of time to NextAction is problematic too. Here's a refactor that should be better:

// States are named for ease of use
#define START   0  
#define SCANNING   1
#define RESULTS  2
#define RESET 3

// Timing variables
const int StartTime = 2500;
const int ScanTime = 1500;
const int RESULTSTime = 1500;
const int ResetTime = 2000;

int state=START;  // initial state
unsigned long StateStartTime=0L;
unsigned long StateDuration=0L;

void setup() 
{
Serial.begin(9600);    // initialize serial communication for debugging
state=START; 
StateStartTime=millis();
StateDuration=StartTime; 
}

void loop()
{
if(millis()-StateStartTime > StateDuration)
  ChangeState();  
 }

void ChangeState()  // Function declared outside of the main loop, for managing the state
{
StateStartTime=millis();
switch (state)
  {
  case START:
    Serial.println("case START");
    StateDuration=StartTime; 
    state=SCANNING;
    break;
  case SCANNING:
    Serial.println("case SCANNING");
    StateDuration=ScanTime; 
    state=RESULTS;
    break;
  case RESULTS:
    Serial.println("case RESULTS");
    StateDuration=RESULTSTime;  
    state=RESET;
    break;
  case RESET:
    Serial.println("case RESET");  
    StateDuration=ResetTime; 
    state=START;
    break;
  }
}

Thank you for the update!

How is adding time to NextAction problematic? Is it the same risk of rollover?

Yes.

If you have the stamina to wade through the long discussion in the link I posted you will see that the best way to prepare for the next time check is with

StateStartTime += StateDuration

rather than

StateStartTime=millis();

This is because you can't be certain that the code which (in your case) calls ChangeState() always does so at precisely the right time. Adding the duration means that successive intervals are always referenced to the original starting time.

...R

Thanks, I haven't yet had the time to go through the entire thread.

I just got the Parola libraries for the MAX 7219 modules working in the state machine, and was pulling my hair out until I realized that one of the actions happening in the state machine was keeping Parola from updating, so the display wasn't changing when I expected. Once I moved that into its own state then everything went well.