State Machines

State machines provide a mechanism for allowing your application to respond to an event, such as a button press, in a way the depends on the current state of your application or machine. As an example of this consider a stop watch that is controlled using the two switches on the MSP430 Launchpad. The stopwatch can be in one of five possible states:

  1. Welcome State: In this state the S1 switch is used to zero the counter and put the stopwatch in the Zero state, S2 does nothing.
  2. Zero state: The stopwatch is ready to start counting. S1 starts the timer putting the machine into the running state , S2 returns the machine to the Welcome state.
  3. Running state: Whilst in this state the stopwatch updates the display with the current time. S1 Stops the timer and puts the machine into the pause state. S2 puts the machine into the lap state where the current display value is held.
  4. Lap state: S1 stops the timer and puts the machine into the pause state. S2 returns the timer to the running state.
  5. Pause state: S1 restarts the timer and puts the machine into the running state. S2 clears the time and puts the machine into the zero state.

The state diagram for this machine is shown below.

Stop watch state diagram

Stop watch state diagram

The state transitions and actions can be represented in code using a state transition table (transtable) and and action table (actionable)

/* States. */
#define MENU 0
#define RESET 1
#define RUN 2
#define LAP 3
#define PAUSE 4

/* Events */
#define SWITCH1 0
#define SWITCH2 1

/* Actions */
#define NACT 0
#define INIT 1
#define CLEAR 2
#define START 3
#define HOLD 4
#define STOP 5

char transtable[2][5] = /* transtab[Input][CurrentState] => NextState */
{
 //Menu Reset Run Lap Pause
 RESET, RUN, PAUSE, PAUSE, RUN, // Switch1
 MENU, MENU, LAP, RUN, RESET // Switch2
};

char actiontable[2][5] = /* transtab[Input][CurrentState] => NextState */
{
 //Menu Reset Run Lap Pause
 CLEAR, START, STOP, STOP, START, // Switch1
 NACT, INIT, HOLD, NACT, CLEAR, // Switch2
};

This code uses the two switch code to indicate whether or not a switch has been pressed and released. The variable PressRelease is used to indicate whether No press/release has been detected (PressRelease = 0), Switch one has been pressed (PressRelease = 1), or Switch 2 has been pressed (PressRelease = 2). On a valid press/release event i.e. PressRelease ~= 0  the following switch statement is used to determine the correct action based on the currentstate and the current input, i.e. Switch 1 or 2, and implement the action. After the action is implemented then the current state is updated using the state transition table (transtable)

if (PressRelease){
 switch (actiontable[PressRelease-1][currentstate])
 { // Call and process transition action
 case NACT:
 break;
 case INIT:
 UARTSendArray("STOPWATCH: Press S1 to start\r\n", 30);
 break;
 case CLEAR:
 tenths = 0;
 seconds = 0;
 minutes = 0;
 UpdateDisplay();
 break;
 case START:
 UARTSendArray("START: Press S2 to hold, S1 to stop\r\n", 37);
 TACCTL0 |= CCIE; // Enable counter interrupt to start counting
 break;
 case HOLD:
 UARTSendArray("LAP: Press S2 to resume, S1 to stop\r\n", 37);
 break;
 case STOP:
 UARTSendArray("STOP: Press S1 to resume, S2 to reset\r\n", 39);
 TACCTL0 &= ~CCIE; // Disable counter interrupt to stop counting
 break;
 }
 // Update state
 currentstate=transtable[PressRelease-1][currentstate];
 PressRelease &= ~(S1 + S2); // Clear switch flag to indicate that the switch press has been serviced
 }
 // Process Activities
 if ((currentstate == RUN) && update){
 update = 0;
 UpdateDisplay();
 }
 __bis_SR_register(LPM0_bits + GIE);
 }
}

As you can see most of the actions simply display the current state of the machine to the user. To start the stopwatch timing The START action simply enables the timer interrupt in order to provide a regular interrupt every 100ms. The timer has already been configured in the initialisation section of the main code. Similarly to stop the stopwatch timing the STOP action disables the timer interrupt.

The elapsed time is simply updated within the timer interrupt service routine shown below

// TimerA interrupt service routine
#pragma vector=TIMER0_A0_VECTOR
__interrupt void Timer_A (void)
{
 P1OUT ^= BIT0;        //Toggle LED to indicate timing
 if (++tenths == 10){  // Increment tenths
 tenths = 0;           // Reset to 0 after count of 9
 if (++seconds == 60){ // Increment seconds
 seconds = 0;          // Reset to 0 after count of 59
 minutes++;            // Increment minutes
 }
 }
 update = 1; //Set update flag
 __bic_SR_register_on_exit(LPM0_bits); //Wake CPU
}

It is also necessary to implement any activities that need to run continuously whilst the machine is in a particular state. In the stopwatch the only activity is the updating of the time whist the machine is in the running state. Because the display only needs to be updated when the time changes, in this design every 10th of a second, I use a an update flag that is set by the timer interrupt (see code above) to signal that the display should be updated.  Thus an if statement is used to check if the current state is RUN and update = 1. The Display is then updated and the update flag cleared to indicate the the update activity has been serviced.

It is also possible to put the UpdateDisplay function inside the timer interrupt, however, this would make the number of operations that need to be completed within the interrupt large and it is always a good idea to keep the code within an interrupt as short as possible to ensure that the servicing of an interrupt does not interfere with the next possible interrupt. If the interrupt processing takes to long then your application my miss important interrupts and the operation will not be as expected.

 // Process Activities
 if ((currentstate == RUN) && update){
 update = 0;
 UpdateDisplay();
 }
 __bis_SR_register(LPM0_bits + GIE);
 }
}

Complete stopwatch code listing

/* This example uses a statemachine to implement a two button stop watch on the MSP430 Launchpad
 * The time and state is displayed using the UART to send the values back over the serial port.
 * To observe the time and stop watch state you need to use the serial monitor or a terminal program.
 *
 * Author: Benn Thomsen 2013
 */

#include "msp430g2553.h"
#define FLIP_HOLD (0x3300 | WDTHOLD) // flip HOLD while preserving other bits

#define S1 1 // Switch 1 Flag Mask
#define S2 2 // Switch 2 Flag Mask
#define BOTH 3 // Both switches Flag Mask
#define PRESSDURATION 47 // Long press duration 47*32ms = 1.5s

#define RXD BIT1
#define TXD BIT2

/* States. */
#define MENU 0
#define RESET 1
#define RUN 2
#define LAP 3
#define PAUSE 4

/* Events */
#define SWITCH1 0
#define SWITCH2 1

/* Actions */
#define NACT 0
#define INIT 1
#define CLEAR 2
#define START 3
#define HOLD 4
#define STOP 5

char transtable[2][5] = /* transtab[Input][CurrentState] => NextState */
{
 //Menu Reset Run Lap Pause
 RESET, RUN, PAUSE, PAUSE, RUN, // Switch1
 MENU, MENU, LAP, RUN, RESET // Switch2
};

char actiontable[2][5] = /* transtab[Input][CurrentState] => NextState */
{
 //Menu Reset Run Lap Pause
 CLEAR, START, STOP, STOP, START, // Switch1
 NACT, INIT, HOLD, NACT, CLEAR, // Switch2
};

volatile char PressCountS1 = 0;
volatile char PressCountS2 = 0;
volatile char SwitchState = 0; // Flag used internally within the interrupts to store the current switch state
volatile char Pressed = 0; // Flag used to indicate a Switch Press
volatile char PressRelease = 0; // Flag used to indicate a Switch Press and Release
volatile char LongPress = 0; // Flag used to indicate a Switch Long Press
volatile char update = 0;

char currentstate = 0;

static volatile char tenths = 0;
static volatile char seconds = 0;
static volatile char minutes = 0;
char numberStr[5];

void ConfigureWDT(void);
void InitialiseSwitch2(void);
void UARTSendArray(unsigned char *TxArray, unsigned char ArrayLength);
void ConfigureUART(void);
char Int2DecStr(char *str, unsigned int value);
void ConfigureTimerA(void);
void UpdateDisplay(void);

void main (void)
{
 ConfigureWDT();
 BCSCTL1 = CALBC1_1MHZ; // Set DCO to 1MHz
 DCOCTL = CALDCO_1MHZ; // Set DCO to 1MHz
 ConfigureUART();
 ConfigureTimerA();

P1DIR |= (BIT0|BIT6); // Set the LEDs on P1.0, P1.1, P1.2 and P1.6 as outputs
 P1OUT |= (BIT0|BIT6); // Turn on P1.0 and P1.6 LEDs to indicate initial state

InitialiseSwitch2(); // Initialise Switch 2 which is attached to P1.3

while(1){

if (PressRelease){
 switch (actiontable[PressRelease-1][currentstate])
 { // Call and process transition action
 case NACT:
 break;
 case INIT:
 UARTSendArray("STOPWATCH: Press S1 to start\r\n", 30);
 break;
 case CLEAR:
 tenths = 0;
 seconds = 0;
 minutes = 0;
 UpdateDisplay();
 break;
 case START:
 UARTSendArray("START: Press S2 to hold, S1 to stop\r\n", 37);
 TACCTL0 |= CCIE; // Enable counter interrupt to start counting
 break;
 case HOLD:
 UARTSendArray("LAP: Press S2 to resume, S1 to stop\r\n", 37);
 break;
 case STOP:
 UARTSendArray("STOP: Press S1 to resume, S2 to reset\r\n", 39);
 TACCTL0 &= ~CCIE; // Disable counter interrupt to stop counting
 break;
 }
 // Update state
 currentstate=transtable[PressRelease-1][currentstate];
 PressRelease &= ~(S1 + S2); // Clear switch flag to indicate that the switch press has been serviced
 }
 // Process Activities
 if ((currentstate == RUN) && update){
 update = 0;
 UpdateDisplay();
 }
 __bis_SR_register(LPM0_bits + GIE);
 }
}

void ConfigureWDT(void){
 // The Watchdog Timer (WDT) will be used to debounce s1 and s2
 WDTCTL = WDTPW + WDTHOLD + WDTNMIES + WDTNMI; //WDT password + Stop WDT + detect RST button falling edge + set RST/NMI pin to NMI
 IFG1 &= ~(WDTIFG + NMIIFG); // Clear the WDT and NMI interrupt flags
 IE1 |= WDTIE + NMIIE; // Enable the WDT and NMI interrupts
}

/* This function configures the button so it will trigger interrupts
 * when pressed. Those interrupts will be handled by PORT1_ISR() */
void InitialiseSwitch2(void){
 P1DIR &= ~BIT3; // Set button pin as an input pin
 P1OUT |= BIT3; // Set pull up resistor on for button
 P1REN |= BIT3; // Enable pull up resistor for button to keep pin high until pressed
 P1IES |= BIT3; // Enable Interrupt to trigger on the falling edge (high (unpressed) to low (pressed) transition)
 P1IFG &= ~BIT3; // Clear the interrupt flag for the button
 P1IE |= BIT3; // Enable interrupts on port 1 for the button
}

// isr to detect make/break of s1 at the nRST/NMI pin
// Note the occurrence of an NMI interrupt automatically disables the NMI interrupt enable.
#pragma vector = NMI_VECTOR
__interrupt void nmi_isr(void)
{
 if (IFG1 & NMIIFG) // Check if NMI interrupt was caused by nRST/NMI pin
 {
 IFG1 &= ~NMIIFG; // clear NMI interrupt flag
 if (WDTCTL & WDTNMIES) // falling edge detected
 {
 P1OUT |= BIT6; // Turn on P1.0 red LED to indicate switch 1 is pressed
 SwitchState |= S1; // Set Switch 1 State to pressed
 Pressed |= S1; // Set S1 pressed flag
 PressCountS1 = 0; // Reset Switch 2 long press count
 WDTCTL = WDT_MDLY_32 | WDTNMI; // WDT 32ms delay + set RST/NMI pin to NMI
 // Note: WDT_MDLY_32 = WDTPW | WDTTMSEL | WDTCNTCL // WDT password + Interval mode + clear count
 // Note: this will also set the NMI interrupt to trigger on the rising edge

}
 else // rising edge detected
 {
 P1OUT &= ~(BIT6+BIT0); // Turn off P1.6 and P1.0 LEDs
 SwitchState &= ~S1; // Reset Switch 1 Pressed flag
 PressRelease |= S1; // Set Press and Released flag
 WDTCTL = WDT_MDLY_32 | WDTNMIES | WDTNMI; // WDT 32ms delay + falling edge + set RST/NMI pin to NMI
 }
 __bic_SR_register_on_exit(LPM0_bits); //Wake CPU
 } // Note that NMIIE is now cleared; the wdt_isr will set NMIIE 32ms later
 else {/* add code here to handle other kinds of NMI, if any */
 }
}

#pragma vector=PORT1_VECTOR
__interrupt void PORT1_ISR(void)
{
 if (P1IFG & BIT3)
 {
 P1IE &= ~BIT3; // Disable Button interrupt to avoid bounces
 P1IFG &= ~BIT3; // Clear the interrupt flag for the button
 if (P1IES & BIT3)
 { // Falling edge detected
 P1OUT |= BIT0; // Turn on P1.0 red LED to indicate switch 2 is pressed
 SwitchState |= S2; // Set S2 state to press
 Pressed |= S2; // Set Switch 2 Pressed flag
 PressCountS2 = 0; // Reset Switch 2 long press count
 }
 else
 { // Rising edge detected
 P1OUT &= ~(BIT0+BIT6); // Turn off P1.0 and P1.6 LEDs
 SwitchState &= ~S2; // Reset Switch 2 state
 PressRelease |= S2; // Set Press and Released flag
 }
 P1IES ^= BIT3; // Toggle edge detect
 IFG1 &= ~WDTIFG; // Clear the interrupt flag for the WDT
 WDTCTL = WDT_MDLY_32 | (WDTCTL & 0x007F); // Restart the WDT with the same NMI status as set by the NMI interrupt
 __bic_SR_register_on_exit(LPM0_bits); //Wake CPU
 }
 else {/* add code here to handle other PORT1 interrupts, if any */
 }
}

// WDT is used to debounce s1 and s2 by delaying the re-enable of the NMIIE and P1IE interrupts
// and to time the length of the press
#pragma vector = WDT_VECTOR
__interrupt void wdt_isr(void)
{
 if (SwitchState & S1) // Check if switch 1 is pressed
 {
 if (++PressCountS1 == PRESSDURATION ) // Long press duration 47*32ms = 1.5s
 {
 P1OUT |= BIT0; // Turn on the P1.1 LED to indicate long press
 LongPress |= S1; //Set S1 bit in long press flag
 __bic_SR_register_on_exit(LPM0_bits); //Wake CPU
 }
 }

if (SwitchState & S2) // Check if switch 2 is pressed
 {
 if (++PressCountS2 == PRESSDURATION ) // Long press duration 47*32ms = 1.5s
 {
 P1OUT |= BIT6; // Turn on the P1.2 LED to indicate long press
 LongPress |= S2; //Set S2 bit in long press flag
 __bic_SR_register_on_exit(LPM0_bits); //Wake CPU
 }
 }

IFG1 &= ~NMIIFG; // Clear the NMI interrupt flag (in case it has been set by bouncing)
 P1IFG &= ~BIT3; // Clear the button interrupt flag (in case it has been set by bouncing)
 IE1 |= NMIIE; // Re-enable the NMI interrupt to detect the next edge
 P1IE |= BIT3; // Re-enable interrupt for the button on P1.3
}

void UARTSendArray(unsigned char *TxArray, unsigned char ArrayLength){
 // Send number of bytes Specified in ArrayLength in the array at using the hardware UART 0
 // Example usage: UARTSendArray("Hello", 5);
 // int data[2]={1023, 235};
 // UARTSendArray(data, 4); // Note because the UART transmits bytes it is necessary to send two bytes for each integer hence the data length is twice the array length

while(ArrayLength--){ // Loop until StringLength == 0 and post decrement
 while(!(IFG2 & UCA0TXIFG)); // Wait for TX buffer to be ready for new data
 UCA0TXBUF = *TxArray; //Write the character at the location specified py the pointer
 TxArray++; //Increment the TxString pointer to point to the next character
 }
}

void ConfigureUART(void){
 /* Configure hardware UART */
 P1SEL |= RXD + TXD ; // P1.1 = RXD, P1.2=TXD
 P1SEL2 |= RXD + TXD ; // P1.1 = RXD, P1.2=TXD
 UCA0CTL1 |= UCSSEL_2; // Use SMCLK
 UCA0BR0 = 104; // Set baud rate to 9600 with 1MHz clock (Data Sheet 15.3.13)
 UCA0BR1 = 0; // Set baud rate to 9600 with 1MHz clock
 UCA0MCTL = UCBRS0; // Modulation UCBRSx = 1
 UCA0CTL1 &= ~UCSWRST; // Initialize USCI state machine
 IE2 |= UCA0RXIE; // Enable USCI_A0 RX interrupt
}

static const unsigned int dec[] = {
 10000, // +5
 1000, // +6
 100, // +7
 10, // +8
 1, // +9
 0
};

char Int2DecStr(char *str, unsigned int value){
 char c;
 char n=0;
 int *dp = dec;

while (value < *dp) dp++; // Move to correct decade
 do {
 n++;
 c = 0; // count binary
 while((value >= *dp) && (*dp!=0)) ++c, value -= *dp;
 *str++ = c+48; //convert to ASCII
 }
 while(*dp++ >1);
 return n;
}

// TimerA interrupt service routine
#pragma vector=TIMER0_A0_VECTOR
__interrupt void Timer_A (void)
{
 P1OUT ^= BIT0;
 if (++tenths == 10){
 tenths = 0;
 if (++seconds == 60){
 seconds = 0;
 minutes++;
 }
 }
 update = 1; //Set update flag
 __bic_SR_register_on_exit(LPM0_bits); //Wake CPU
}

void ConfigureLEDs(void){
 // Configure LEDs
 P1DIR |= (BIT0+BIT6); // P1.2 to output
 P1OUT |= (BIT0+BIT6); // Set the LEDs P1.0 and P1.6 to indicate correct configuration
}

void ConfigureTimerA(void){
 /* Configure timer A as a clock divider to generate timed interrupt */

TACTL = TASSEL_2 +ID_3 + MC_1; // Use the SMCLK to clock the counter, SMCLK/8, count up mode
 TACCR0 = 12500-1; // Set maximum count (Interrupt frequency 1MHz/8/10000 = 10Hz)
}

void UpdateDisplay(void){
 UARTSendArray(numberStr,Int2DecStr(numberStr, minutes));
 UARTSendArray(":", 1);
 UARTSendArray(numberStr,Int2DecStr(numberStr, seconds));
 UARTSendArray(":", 1);
 UARTSendArray(numberStr,Int2DecStr(numberStr, tenths));
 UARTSendArray("\r\n", 2);
}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s