Using State-Machines in Low-Power Embedded Systems

Introduction

I got requests from time to time asking how to integrate state-machines in a specific system design. Usually I can only give some general advice. But I like to present some design principles here which are hopefully useful for developers of deeply embedded systems in general and those of low power systems in particular.

The CPU in a low power system is usually only active from time to time to react on events triggered from outside (e.g. a pressed key) or triggered from inside the system (e.g. ADC ready, timer expired). After processing the event the CPU goes into low power mode again.

All this is done to keep the batteries running as long as possible. A good example for such a system is a wireless temperature sensor mounted outside the window sending temperature data from time to time to a central display, or a monitor attached to a bird to track the flight route or a bike computer etc. The longer the batteries last, the better.

But also systems connected to power all the time might benefit from a low-power design. Power consumption of a device is a real buying criteria nowadays.

System Design

The following example is based on a small MSP430F1232 header board with just 256 bytes of RAM and 8K of program memory. For the development the Code Composer Studio from TI was used. At the end the program needs <2k Flash and 182 bytes RAM incl. stack. The following block diagram shows the controller with its I/O connections.

Figure 1: MSP430F1232 block diagram

To not hide the design principles behind too much details the uC runs just two state machines fully generated from UML state diagrams. One machine is measuring the battery voltage every 2 seconds. The other one measures the temperature also on a cyclic basis using a TMP100 from TI connected via I2C to the uC. Temperature measurement happens only if battery voltage is high enough. Otherwise a LED is pulsed to indicate low voltage state (P1.3). The radio part which sends the temperature to a central server was omitted here.

The two state machines realizing the user application are shown in figure 2 below. The state machines were designed with the built-in Java UML editor. To change or extend the functionality just modify the state machines, regenerate the state-machine code and then compile and link as usual. Changes on I/O port 1 were added to verify the timing via oscilloscope (see below).

Figure 2: Application logic implemented as state machines. Top: Triggering the voltage measurement with a cyclic timeout. Bottom: Switching between two operation modes depending on supply voltage level. Single-shot timers are used for timing.

Reusable Library Layer

The design is based on the following library elements which can be reused from project to project:

A timer abstraction layer provides timers for the user application. The timer abstraction is based on a system tick realized with hardware timer A here. If a timer has expired the tick() routine returns true and the CPU is woken-up. Otherwise the low power mode is entered again.

__interrupt void Timer_A0(void) { bool retVal; P1OUT ^= BIT0; // toggling bit for debugging retVal = tick(); // timer service. Check if any timer has expired if(retVal){ // at least one timeout timer fired. // wake up main loop __bic_SR_register_on_exit(LPM0_bits); } }

Listing 1: Timer interrupt service routine calling the timer handler tick().

The user code can create multiple timers and receive time-out events in case they expire. During timer creation the event queue and the timer event has to be provided. If a timer expires this event will be stored in the given event queue. The timer implementation can be found in the library folder in files timer.c and timer.h. The tick() function is the core of the timer library and shown below:

bool tick(void){ bool timeoutAvailable=false; // Iterate over the timers and enque // timer events once a timeout has happend. for(uint8_t i=0; i<MAX_TIMERS; i++){ if(timers[i].timerStatus==ON){ timers[i].elapsedTicksSinceStart++; if(timers[i].elapsedTicksSinceStart==timers[i].preset){ // timeout time elapsed. timers[i].timerStatus=OFF; // stop timer fifoPut(timers[i].rbuf, timers[i].evTimeout); // enqueue timeout event timeoutAvailable=true; } } } return timeoutAvailable; } // starts timer // Returns either ON if started or OFF if not started (e.g. preset=0) TIMER_STATE_T timerStart(uint8_t t_id, uint16_t ticks); // stops timer void timerStop(uint8_t t_id); // either returns ON or OFF or PAUSE TIMER_STATE_T timerStatus(uint8_t t_id); // set timer timeout in ticks void timerSet(uint8_t t_id,uint16_t ticks); // Timer function to be called from the timer irq handler. // If at least on timer fired the function returns 1. Otherwise 0. bool tick(void); // pause timer void timerPause(uint8_t t_id); // resume timer void timerCont(uint8_t t_id); // Not yet implemented void timerErase(uint8_t t_id);

Listing 2: The timer interface and the tick handler. The tick handler is checking if any timer has elapsed. In this case the user provided event is stored in the corresponding queue.

A queue service. Queues are used to store events from timers or other event sources in fifo order. Multiple queues can be created usually one per state-machine. The queue code is available in the library folder in files ringbuf.c and ringbuf.h.

typedef struct Buffer { uint8_t *data; uint8_t mask; // size - 1 uint8_t read; // index of oldest element uint8_t write; // index of field to write to } FIFO_T; void fifoInit(FIFO_T * const buffer, uint8_t * const pByte, uint8_t size); bool fifoPut(FIFO_T * const buffer, uint8_t byte); bool fifoGet(FIFO_T * const buffer, uint8_t * const pByte); bool fifoPutHead(FIFO_T * const buffer, uint8_t byte); bool fifoIsEmpty(const FIFO_T * const buffer);

Listing 3: The queue interface. Multiple queues can exist. A state machine usually has one input queue to pick the next event from.

Weaving the state machines and the library part together

On top of the minimal abstraction layer (timers, queues) the application specific part is located. It is implemented as state-machines reacting on the events. The state machine code is fully generated. All is generated from the UML state machine diagrams. The main routine initializes the hardware and then enters a main-loop waiting in low-power mode. ADC and timer iqr handlers wake-up the CPU after they enqueued an event. When woken-up the main loop pulls out these events and calls the appropriate state-machine with the event as parameter. See the following code snippet for the implementation details:

/** * Main loop initializes the hardware and software * and then waits in low power mode until new events * are present. Events are then pulled out of the * queues and forwareded to the apropriate state-machine. */ void main( void ) { // Stop watchdog timer to prevent time out reset WDTCTL = WDTPW + WDTHOLD; // initialize timer TACCTL0 |= CCIE; // CCR0 interrupt enabled TACCR0 = 41U; TACTL = TASSEL_1 + MC_1 + ID_3; // SMCLK/8, upmode P1OUT &= 0x00U; // Shut down everything P1DIR &= 0x00U; P1DIR |= BIT0 + BIT1+BIT2+BIT3; // set pins to output the rest are input P1OUT |= BIT1 + BIT1+BIT2+BIT3; //Select pull-up mode fifoInit(&buf1, buf1data ,sizeof(buf1data)/sizeof(uint8_t)); fifoInit(&buf2, buf2data ,sizeof(buf2data)/sizeof(uint8_t)); timerInit(); init_uart(); radio_init(); initWindSensor(); // run once to init toggle(&smToggle, TOGGLE_NO_MSG); pwr(&smPwr, PWR_NO_MSG); while(1){ //Loop forever uint8_t retVal; uint8_t bufVal; do{ // first process all events for task A retVal = fifoGet(&buf1, &bufVal); if(retVal!=QUEUE_EMPTY){ toggle(&smToggle, bufVal); } }while(retVal!=QUEUE_EMPTY); do{ // then all evenets for task B retVal = fifoGet(&buf2, &bufVal); if(retVal!=QUEUE_EMPTY){ pwr(&smPwr, bufVal); } }while(retVal!=QUEUE_EMPTY); // more tasks could follow here __bis_SR_register(LPM3_bits + GIE); // Enter low power mode once __no_operation(); // no more events must be processed } }

Listing 4: The main routine waiting in low power mode until events are available. Then these events are forwarded to the state machine handlers.

How to treat with longer lasting tasks

Execution time for handling events should be as short as possible. Longer lasting tasks should be split into chunks. Take the temperature measurement as an example. The temperature sensor TMP100 requires about 320ms conversion time according to the data sheet. The uC shouldn't wait and burn CPU cycles but either wait in low-power mode or process other events. This can easily be done by starting the conversion in one state (Step1) and then start a timer and enter low-power mode. If the timer fires we pick-up the conversion result (Step2) and process it further.

Another example is the wake-up procedure of the radio. After finishing low-power mode it takes a while before it is ready. Readiness is signalled via a transition of the RTS pin from high to low. After switching on the radio in state Step2a we enter low-power mode again. The level change creates an irq and sends the event evFallingEdgeRTS. Now the radio is ready to transmit data.

Object Diagram

The software design is shown in the next figure as UML object diagram. You can clearly see how the queues decouple the event sources from the event consumers. The main-loop serves as event dispatcher being only active if events are available in one of the queues. The main loop can also be used to prioritize one state machine over an other or ensure the order in which state machines are executed.

Figure 3: Object diagram of the whole application.

To verify the overall system behavior some output port pins were used as follows:

Timer A interrupt routine shown als clk in the first line. The port pin is inverted in each interrupt i.e. every 100 ms.

The supply voltage goes down. As reaction the measurement stops (line 2) and the power fail LED starts flashing (line 4). The evPowerFail starts from state Running. As a result it can happen that the temperature measurement cycle just starts in case a evPowerFail event happens but can't finish. You can see this in the oscilloscope image. There is just one peak. To fix this let the evPowerFail transition start from state Step2. This would ensure that a measurement cycle always completes before state changes to PowerFail.

Line 3 shows the timing of voltage measurement state machine (cycle of 2 seconds).

Figure 4: Oscilloscope image showing the timing of the whole application. Line 1: system tick. Line 2: timing of the temperature measurement phases. Measurement only happens if the supply voltage is above a defined threshold. Line 2: Supply voltage measurement. Line 3: LED output plus: Analog line: battery voltage.

Discussion

This article shows the principle concepts for designing low power embedded systems using state-machines, timers and queues.

Queues and timers as basic services which are application independent and can be reused from system to system. Timer resolution and number of timers and queues can be adjusted to optimize memory needs.

The user application is modeled as state machines receiving events from the outside or inside sources via queues. I.e. decoupling is done with queues.

State-machine processing time must be short enough to ensure that no events get lost and overall response time is ensured.

The source code is just meant for demonstration purposes. I'm happy to receive suggestions for improvement which I will add and make available for others again. The code composer workspace is available for download here.

Hope you enjoyed this article. Let me know your feedback!

Peter Mueller