2025/01/15
Small microcontrollers are often required to handle several tasks, sometimes with overlapping phases. This paper will lead you through the creation of a small operating system which can be implemented on a tiny microcontroller, without the fancy time-slicing that is offered by sophisticated operating systems like Linux and FreeRTos.
The core of this technique is covered in Part 1 of this paper. If you just want to see how it all comes together, jump to Final Implementation.
Part 2 contains enhancements and variations, probably useful reading if you decide to adopt this programming technique in your projects.
This paper will take you through the creation of a tiny operating system that can be implemented on a small microcontroller.
These techniques can be applied by anyone with experience in C, Python, Rust or any modern computer language. It helps to have a passing knowledge of how to connect a transducer (button, LED, buzzer, etc) to a microcontroller.
The Reality Check dropdowns in this article provide extra, often practical supplementary reading.
The technique described here is also called event driven programming.
This technique was used in the original Windows (1995), and includes
some of the system calls used in this paper:
sendMessage(), postMessage() and setTimer()
.
This event-driven technique is applicable to a whole array of small microcontrollers, including
Some of the really tiny ones probably don’t have enough stack space to implement the technique described in this paper (Puolop PB150, Padauk PxS15x, Bojuxing BJ8P, Yspring MDT1x, EastSoft HR7P, Holtek Ht68.)
This is a
typical board hosting 12 buttons, 12 RGB LEDs, all hosted by an STM32.
Every light and button can be controlled separately and simultaneously,
using the technique described in this paper.
Let’s suppose we want to flash an LED at 1 flash/2 sec, and independently, respond to a push button by operating a different LED for 1.5 seconds. Both operations must operate separately. How do we structure the code for the microcontroller to make this happen?
Let’s look at the pseudocode overview of what we want to do:
void initialize(void) {
();
setup_hardware();
setup_interrupts}
void irq(void) {
INTERRUPT ();
create_event();
acknowledge_interrupt}
void main(void) {
();
initialize
while (1) {
if (event) {
(event);
flashLedTask(event);
respondToButtonTask}
}
}
That’s about it.
Of course, we will need to write the code for
setup_hardware()
and setup_interrupts()
. And
flashLedTask()
and respondToButtonTask()
. And
create the magic that allows event
information to
flow. All this will be laid out in the following pages.
If you’re concerned with complexity, feel free to jump ahead about 10 pages to the final implementation page to see real, tested, code.
Between here and there, I’ll walk you step-by-step through building the structure and implementation.
This paper continues after that though, to show you how to expand upon a build as the project-definition changes. There are also several examples of state machines, and some discussion of practical matters, refactoring and project structure.
In order to have a responsive system, it would make sense to use the interrupt capabilities of these small micrcontrollers.
In the task described, we need to respond to a button press. So let’s connect the button to an input pin and enable it to respond to a button press with interrupt code.
We also need to keep track of time…..so let’s hook up a system timer to another interrupt.
Each interrupt causes the execution of interrupt handler code. For this project, it might look something like this:
enum {EVT_NONE=0, EVT_TICK, EVT_BUTTON};
(void) {
INTERRUPT timer_isr(EVT_TICK);
newEvent}
(void) {
INTERRUPT button_isr(EVT_BUTTON); // see notes about button bounce and acknowledgement
newEvent}
The interrupt handlers are both very simple. They just create a unique event and let the system know about it. Next, let’s talk about events.
In some microcontrollers, interrupts must be acknowledged.
Sometimes that means setting a hardware flag to indicate to the device
that you’re ready for another interrupt of the same type. And in some
cases, there’s no need to ack. Specifically, the SYS_TICK interrupt in
the ARM Cortex processors does not need an ack, so the above
code example for timer_isr()
is complete.
When a pushbutton or switch is connected to a microcontroller, the
first bit of activity will cause the interrupt and execute the
button_isr()
code. However, real buttons produce about
5msec of bounce and this will cause subsequent interrupts
unless they are somehow filtered out. There are lots of ways to handle
bounce, and I’ll let you read about that elsewhere. Most techniques boil down to either
ignoring subsequent interrupts (from the same button) for about 5 msec,
or disabling that specific interrupt until the 5msec has passed.
As a general rule, input pins should be observed either by interrupt service routine (ISR), or scanned periodically by the timer ISR. Outputs should be controlled in the task code, which we’ll see below.
An event in this context is a small bit of information that appears asynchronously in the system. Implemented, it can be a byte, or an int or an even larger structure. But in these small microcontrollers, let’s use a byte.
volatile uchar event;
These events will be created in interrupt level of the code,
and processed at the main level. We use the event
object to
send information between these levels, so we have to mark it volatile
.
In this paper, the words event and message are equivalent. Event has the sense of “something external just happened” and message has the sense of “a bit of communication from another part of the program” At this point in the design, they both mean the same thing.
By convention, let’s use zero to indicate the absence of an event/message, and non-zero to represent an event/message.
volatile uchar event;
enum {EVT_NONE=0, EVT_TICK, EVT_BUTTON}; // 0,1,2
So now we start to get an idea of what kind of information the
interrupt handler code generates. Now let’s look at how that
information gets sent to the rest of the code:
newEvent()
.
Here’s a block diagram of the message flow
How do we send the information (events/messages) from the interrupt
service routine to the main()
code? We used shared
memory.
One way would be to have a global volatile uchar
location into which we drop the event information. But having
only one socket for that would be a bit naive; what happens if a timer
tick and a button press happen very close in time? What happens if the
timer tick events start to stack up?
It makes more sense to have an array:
volatile uchar events[NUM_EVENTS]
where NUM_EVENTS is on
the order of 5..10. That would give us 50-100msec to catch up in case
there’s a pileup of events/messages.
At the beginning, before anything happens, we need to make sure the
events[]
is full of zeros (EVT_NONE), indicating that it’s
empty.
The newEvent(evt)
routine simply adds the
evt
to the array events[]
.
Something like this might work:
void newEvent(uchar evt) {
static uchar nextEvent;
[nextEvent++] = evt;
eventsif (nextEvent == NUM_EVENTS)
= 0;
nextEvent }
There is a problem with the above code. What happens if a
key_isr()
is running, and is halfway through its call to
newEvent()
when a timer_isr()
happens, and
reenters the newEvent()
routine. This will get really
messed up. So we need to wrap this particular code in a
critical section.
Here’s a more realistic version:
void newEvent(char evt) {
static uchar nextEvent; // keep track of where we are in queue
(); // critical section
disable_irq[nextEvent++] = evt; // insert event into queue
eventsif (nextEvent == NUM_EVENTS) // loop back to the start of queue
= 0;
nextEvent (); // end critical section, probably <100us of blockage
enable_irq}
In the next section, we’ll show how the main()
code
pulls the events out of the array, and leaves an EVT_NONE in its
place.
The main()
code can simply watch the
events[]
to see when an entry goes non-zero (!=EVT_NONE).
When that happens, main()
will pull out the
event/message from the array, and call the task subroutines. In
this case, flashLedTask()
and
respondToButtonTask()
.
To see the real code for the dispatcher, jump ahead.
The main()
code illustrated here calls the tasks as
subroutines, sending each one a copy of the event number.
For this paper, these are equivalent
It may seem wasteful to send all events to all tasks. Probably some tasks don’t care about certain classes of events. But classifying, sorting and filtering the events takes real time, and code space, and probably isn’t worth it for these small microcontrollers.
More sophisticated event systems do, in fact, filter and sort events.
For example, Windows only sends MOUSE_MOVE
events to the
code belonging to the window over which the mouse is travelling. All the
other windows don’t get the event, speeding up the whole system.
In this environment, the code that implements a task is simply a subroutine that accepts an event/message.
void taskCode(uchar event) {
... process the event information ...
}
The subroutine should be designed to flow-through as quickly as possible, without any pauses/waits/delays.
If the problem that you’re trying to solve involves the passage of time, or any delay, then you must break down the actions into individual items, and build a state machine.
A state machine in this environment is a subroutine which can be called many times, and it remembers in which state it was left from the previous call. Some invocations of this subroutine may cause it to change state, which can be represented in a net diagram.
Here’s a state diagram for the task which reacts to a button press by flashing an LED.
How does the state code remember what state it’s in between
invocations? We can use a static
variable. A
static
is stored in main memory (not on the
stack), persists between calls and is initialized to zero.
The above diagram can be implemented in this code:
enum {RTB_IDLE, RTB_ON}; // RTB_IDLE=0, RTB_ON=1
static uchar rtbState = RTB_IDLE;
static uint16 rtbTimerCount = 0;
const uint16 TIMER_LIMIT = 150;
void respondToButtonTask(uchar evt) {
switch(rtbState) {
case RTB_IDLE:
if (evt == EVT_BUTTON) {
= RTB_ON;
rtbState = TIMER_LIMIT;
rtbTimerCount (LED, ON);
gpio_set}
break;
case RTB_ON:
if (evt == EVT_TICK) {
if (--rtbTimerCount == 0) {
(LED, OFF);
gpio_set= RTB_IDLE;
rtbState }
}
break;
}
}
Each time this routine is called, it first forks, depending on the current state. Then it checks the event that it was given, and sometimes processes it. And sometimes it changes state.
Here’s a few things to notice:
if
statementThe state variable must be static
or in the
global memory space. NEVER put a state variable on the stack!
i.e. as a local variable.
The other task of this project simply flashes the LED on and off.
The code for this might be:
enum {LED_ON, LED_OFF};
static uchar ledState = LED_OFF;
static uint16 ledTimerCount = 0;
const uint16 LED_ON_TIME = 100;
const uint16 LED_OFF_TIME = 100;
void ledTask(uchar evt) {
switch(ledState) {
case LED_OFF:
if (evt == EVT_TICK) {
if (--ledTimerCount == 0) {
(LED, ON);
gpio_set= LED_ON_TIME;
ledTimerCount = LED_ON;
ledState }
}
break;
case LED_ON:
if (evt == EVT_TICK) {
if (--ledTimerCount == 0) {
(LED, OFF);
gpio_set= LED_OFF_TIME;
ledTimerCount = LED_OFF;
ledState }
}
break;
}
}
Perhaps a more accurate state diagram might be:
As you build the code from the diagram, I strongly suggest
that you use a switch(currentState) ....case STATE_1
approach to the task routine. If you try and code it with multiple
if(currentState==STATE_1)
, you will find yourself in a
tangle of spaghetti; and if you forget a critical else
,
nothing will work as you expect.
if (state==1) {
if (event = EVT_1) {
= 27; // turn on some hardware
P3OUT = 2; // and change state
state }
}
if (state==2) { // <<<----- BAD BAD
= 14;
P3OUT = 3; // <<<--- dont' change state twice in the same event
state }
...
rather
switch(state) {
case 1:
if (event = EVT_1) {
= 27; // turn on some hardware
P3OUT = 2;
state }
break;
case 2:
= 14;
P3OUT = 3;
state }
...
}
As a general rule, try to avoid changing states more than once per event; this kind of discipline will help with debugging a complex project.
The last piece of the puzzle is the main()
code which
observes the events[]
array and calls the tasks.
The events[]
array is designed so that a 0 means ‘no
event’, and the non-zero events are dropped into the array in order…so
pulling them out is pretty straight forward.
void main() {
();
initialize
while(1) {
int i;
for (i=0; i<NUM_EVENTS; i++) {
while (events[i]==EVT_NONE)
{}
(events[i]);
ledTask(events[i]);
respondToButtonTask[i] = EVT_NONE;
events}
}
}
Each non-zero event is “sent” to each task…..by means of a subroutine
call. Once all the tasks have been invoked, the event
is
thrown away and its slot in the array is set to zero.
The main()
code above needs to run with interrupts
enabled.
There’s lots of ways to structure the ‘wait for event’ loop. For
example, when you detect that the events[]
array is empty,
you could power-down the microcontroller. In most microprocessors, an
interrupt will wake the CPU again, and deliver a non-zero event into the
array, so you might as well power-down while you’re waiting.
Let’s put all of the above together, for an SMT32F Cortex M0, in C code.
/***** declarations ****/
#define NUM_EVENTS 10
volatile uchar events[NUM_EVENTS];
void newEvent(uchar e);
void ledTask(uchar evt);
void respondToButtonTask(uchar evt);
/********** interrupts **************/
void timer_isr(void) {
(EVT_TICK);
newEvent// this interrupt is auto-ack'd
}
void button_isr(void) {
(EVT_BUTTON);
newEvent->PR |= KEY_IRQ_ACK_MASK; // the hardware requires that we acknowledge
EXTI}
/** newEvent
* add the event to the event queue
* wrapped in critical section
*
* @param the event
*/
void newEvent(uchar e) {
static uchar nextEvent;
(); // critical section
dint[nextEvent++] = e;
eventsif (nextEvent==NUM_EVENTS)
= 0;
nextEvent ();
eint}
/****** main() and dispatcher *********/
void main(void) {
();
eint
// dispatcher loop
while(1) {
int j;
for (j=0; j<NUM_EVENTS; j++) {
while (events[j]==EVT_NONE)
{}
(events[j]);
ledTask(events[j]);
respondToButtonTask[j] = EVT_NONE;
events}
}
}
/*********** task code, with states ************/
enum {LED_ON, LED_OFF};
enum {RTB_IDLE, RTB_ON}; // states
static uchar rtbState = RTB_IDLE;
static uint16 rtbTimerCount = 0;
const uint16 LED_ON_TIME = 150;
void respondToButtonTask(uchar evt) {
switch(rtbState) {
case RTB_IDLE:
if (evt == EVT_BUTTON) {
= 0;
rtbTimerCount = RTB_ON;
rtbState (LED, LED_ON);
gpio_set}
break;
case RTB_ON:
if (evt == EVT_TICK) {
if (++rtbTimerCount > LED_ON_TIME) {
(LED, LED_OFF);
gpio_set= RTB_IDLE;
rtbState }
}
break;
}
}
const uint16 LED_ON_TIME = 150;
const uint16 LED_OFF_TIME = 50;
static uchar ledState = LED_OFF;
static uint16 ledTimerCount = 0;
void ledTask(uchar evt) {
switch(ledState) {
case LED_OFF:
if (evt == EVT_TICK) {
if (++ledTimerCount > LED_OFF_TIME) {
(LED2, LED_ON);
gpio_set= 0;
ledTimerCount = LED_ON;
ledState }
}
break;
case LED_ON:
if (evt == EVT_TICK) {
if (++ledTimerCount > LED_ON_TIME) {
(LED2, LED_OFF);
gpio_set= 0;
ledTimerCount = LED_OFF;
ledState }
}
break;
}
}
This is the end of the main presentation. With the above techniques, you can make a tiny microprocessor appear to multitask.
For further tips, read the next section. The interesting topics are:
What’s missing from this code sample is the initialization of the
hardware……..some kind of setup()
routine. I left it out
because it’s going to vary so much between processors, and only
distracts from the point of this paper. If you want a detailed, tested
copy of this code, see it here or download it
here
Now that the infrastructure is in place, it’s easy to expand or modify the code for changes in the project definition.
For example, suppose we want the respondToButtonTask()
to restart the LED timer on each key press:
void rtbTask(uchar event) {
switch(rtbState) {
case RTB_IDLE:
if (event == EVT_BUTTON) {
(LED, LED_ON);
gpio_set= RTB_ON;
rtbState = RTB_TIMEOUT;
rtbTimer }
break;
case RTB_ON:
if (event == EVT_BUTTON) {
= RTB_TIMEOUT;
rtbTimer }
if (event == EVT_TICK) {
if (--rtbTimer == 0) {
(LED, LED_OFF);
gpio_set= RTB_IDLE;
rtbState }
}
break;
}
}
Or have a 2nd press of the button cause the LED to extinguish early:
void rtbTask(uchar event) {
switch(rtbState) {
case RTB_IDLE:
if (event == EVT_BUTTON) {
(LED, LED_ON);
gpio_set= RTB_ON;
rtbState = RTB_TIMEOUT;
rtbTimer }
break;
case RTB_ON:
if (event == EVT_BUTTON) {
(LED, LED_OFF);
gpio_set= RTB_IDLE;
rtbState }
if (event == EVT_TICK) {
if (--rtbTimer == 0) {
(LED, LED_OFF);
gpio_set= RTB_IDLE;
rtbState }
}
break;
}
}
How about have the button start a flash sequence, and a 2nd press stops it: (see also substates)
void rtbTask(uchar event) {
switch(rtbState) {
case RTB_IDLE:
if (event == EVT_BUTTON) {
(LED, LED_ON);
gpio_set= RTB_ON;
rtbState = RTB_TIMEOUT;
rtbTimer }
break;
case RTB_ON:
if (event == EVT_BUTTON) {
(LED, LED_OFF);
gpio_set= RTB_IDLE;
rtbState }
if (event == EVT_TICK) {
if (--rtbTimer == 0) {
(LED, LED_OFF);
gpio_set= RTB_FLASH_TIME;
rtbTimer = RTB_OFF;
rtbState }
}
break;
case RTB_OFF:
if (event == EVT_BUTTON) {
(LED, LED_OFF);
gpio_set= RTB_IDLE;
rtbState }
if (event == EVT_TICK) {
if (--rtbTimer == 0) {
(LED, LED_ON);
gpio_set= RTB_FLASH_TIME;
rtbTimer = RTB_ON;
rtbState }
}
break;
}
}
Each of these diagrams corresponds to trivial changes in the state code.
If you’re from the Arduino world, you have no doubt seen the similarity between this OS plan and the Arduino infrastructure.
()
setup{} // initialize and start up the devices and services
()
loop{} // code for continuous operation
You can certainly merge this paper’s operating system into the Arduino architecture:
char events[];
() {
interrupt_service_routines(evt); // insert events/messages into events[]
newEvent}
static int nextEvent;
void newEvent(char evt)
{} // same as above; put evt into the events[]
()
setup{}
() {
loopstatic int nextTaskEvent;
if (events[nextTaskEvent]!=EVT_NONE) { // check for non-zero events[],
(events[nextTaskEvent]); // and call the task routines
task1(events[nextTaskEvent]);
task2[nextTaskEvent) = EVT_NONE; // free the slot, fill with 0
eventsif (++nextTaskEvent > NUM_EVENTS)
= 0; // and loop through the array
nextTaskEvent }
}
void task1(char evt) {}
void task2(char evt) {}
The fundamental guideline for tasks is that they do not stop. Control flows through and out the bottom, returning to the dispatcher quickly.
In practical terms, if you have a timer tick that runs at every 10ms, and about 5 tasks, then if you can keep each task under 2ms, you won’t lose any events.
If a task occasionally runs into the 10’s of msec, the event queue will handle buffering the events until they can be processed.
Under no circumstances should a task take more than 100msec. Use a new state, and return from the task. Process the new state later.Suppose we want to control the power window on a car? For this problem, we have an up/down button, a motor to drive the window up or down, and a motor-overload sensor to detect when the motor is straining. So the buttons & overload sensor are inputs, and the motor drive is outputs.
When the user presses the “up” button, we should start the motor moving upward. When the overload sensor detects a strain, then either the window is all the way up….or it has encountered a finger or hand; in either case, we need to turn the motor drive off.
Here’s a possible state diagram.
And here’s the matching code. Note the correspondence between the
diagram and the code: arrows leaving a state correspond to an
if()
phrase.
enum {WINDOW_IDLE, WINDOW_UP, WINDOW_DOWN};
static uchar windowState = WINDOW_IDLE;
void windowTask(uchar evt) {
switch(windowState) {
case WINDOW_IDLE:
if (evt == EVT_BUTTON_UP) {
(MOTOR, UP);
gpio_set= WINDOW_UP;
windowState }
if (evt == EVT_BUTTON_DOWN) {
(MOTOR, DOWN);
gpio_set= WINDOW_DOWN;
windowState }
break;
case WINDOW_UP:
if (evt == EVT_MOTOR_SENSE ||
== EVT_BUTTON_RELEASE) {
evt (MOTOR, OFF);
gpio_set= WINDOW_IDLE;
windowState }
break;
case WINDOW_DOWN:
if (evt == EVT_MOTOR_SENSE ||
== EVT_BUTTON_RELEASE) {
evt (MOTOR, OFF);
gpio_set= WINDOW_IDLE;
windowState }
}
}
Now, suppose the problem definition is changed: if the user presses the “up” button, the motor should only operate while the button is pressed; stop on release. But if the user presses the “up” button a second time within 1 second of the first release, the motor should drive the window all the way up (auto-close).
Here is a possible state diagram. Notice the significant re-use of code from the previous version.
This is such a simple task, with only a few I/O pins involved. In theory, a cheap microcontroller could control a dozen windows, each appearing to operate independently.
In the code, one wouldn’t need to create a dozen tasks……just create an index into the same code and invoke it in a way that makes it appear as an independent task:
void main(void) {
eint();
while(1) {
for (i=0; i<NUM_EVENTS; i++) {
while (events[i]==EVT_NONE)
{}
taskWindow(events[i],0);
taskWindow(events[i],1);
// ...
taskWindow(events[i],NUM_WINDOWS-1);
events[i] = EVT_NONE;
}
}
}
// window management task
int windowState[NUM_WINDOWS];
void taskWindow(char event, int windowIndex) {
// find the state from the windowState[windowIndex]
// run through the state machine
// any I/O will require an array of I/O addresses, use 'windowIndex'
switch(windowState[windowIndex]) {
case WS_IDLE:
...
...
}
}
A simple state machine can control the interior light of a fridge. Here’s the use-case:
The problem has one input (door open) and two outputs (light and audible alarm). If the door is open, turn on the light and start a timer for 90 seconds. If the door is still open at the end of the 90 seconds, start an audible alarm. If the door closes, stop the timer and turn off the light and alarm. And if the door closes during the 90 seconds, turn off the light.
Here is the state diagram.
And here is the corresponding code.
enum {FRIDGE_CLOSED, FRIDGE_OPEN, FRIDGE_BEEPING};
uchar fridgeState;
uint16 fridgeTimer;
const uint16 FRIDGE_OPEN_LIMIT = 9000; // 90 seconds at 10msec tick
void fridgeTask(char event) {
switch(fridgeState) {
case FRIDGE_CLOSED:
if (event == EVT_OPEN) {
(LIGHT, ON);
set_io= FRIDGE_OPEN_LIMIT;
fridgeTimer = FRIDGE_OPEN;
fridgeState }
break;
case FRIDGE_OPEN:
if (event == EVT_CLOSE) {
(LIGHT, OFF);
set_io= FRIDGE_CLOSED;
fridgeState }
if (evt == EVT_TICK) {
if (--fridgeTimer == 0) {
(ALARM, ON);
set_io= FRIDGE_BEEPING;
fridgeState }
}
break;
case FRIDGE_BEEPING:
if (event == EVT_CLOSE) {
(ALARM, OFF);
set_io(LIGHT, OFF);
set_io= FRIDGE_CLOSED;
fridgeState }
break;
}
}
changeState()
Notice on the state diagram, the arrows heads and tails cluster, and similar actions happen for multiple arrows. Perhaps we should write a function that just deals with all the actions required when leaving or entering a state. Then the task code would only have to manage the arrows of the state diagram. Like this:
// this outer code deals with the arrows on the state diagram
void fridgeTask(char event) {
switch (fridgeState) {
case FRIDGE_CLOSED:
if (event == EVT_OPEN) changeFridgeState(FRIDGE_OPEN);
break;
case FRIDGE_OPEN:
if (event == EVT_CLOSE) changeFridgeState(FRIDGE_CLOSED);
if (event == EVT_FRIDGE_TIMEOUT) changeFridgeState(FRIDGE_BEEPING);
break;
case FRIDGE_BEEPING:
if (event == EVT_CLOSE) changeFridgeState(FRIDGE_CLOSED);
break;
}
}
while the inner code deals with the actions required for entry and exit from each state
void changeFridgeState(char newState) {
static char oldState = FRIDGE_CLOSED;
// do all the state-leaving actions
switch(oldState) {
case FRIDGE_CLOSED:
(LIGHT, ON);
set_io(FRIDGE_TIMER, FRIDGE_OPEN_LIMIT);
setTimerbreak;
case FRIDGE_OPEN:
break;
case FRIDGE_BEEPING:
(ALARM, OFF);
set_iobreak;
}
// change state
= oldState = newState;
fridgeState // and do the state-entry code
switch(newState) {
case FRIDGE_CLOSED:
(LIGHT, OFF);
set_io(FRIDGE_TIMER, 0);
setTimerbreak;
case FRIDGE_OPEN:
break;
case FRIDGE_BEEPING:
(ALARM, ON);
set_iobreak;
}
}
Suppose we have a power lock on a door, using a solenoid, and an RFID tag detector on the “outside” and a push button on the “inside”. There is also a WiFi connection to a server, by which we report door openings. When the RFID tag sends us a message, it will contain a serial number. If the number matches a known record, then operate the door-opener solenoid for 4 seconds. If the “inside” button is pushed, operate the door-opener for 4 seconds; if the “inside” button is pressed during the 4 seconds, restart the 4 second timer.
After the door is locked, send a report to the master control via the WiFi.
Here’s the state diagram:
Suppose now that the serial number needs to be verified by a central service. So when an RFID tag is detected, send a request to the master control and wait for an ACK or NAK response. In the case of an ACK, open the door solenoid for 4 seconds. The rest of the problem is as stated above.
I use EVT_
type events to indicate that they originate
in hardware, probably at the interrupt level; and MSG_
type
events to indicate they come from a software source, perhaps a sibling
task.
Here’s the modified state diagram:
Suppose we have to move a servo motor to lift the lid from a brewing vat, to release excess pressure.
Inputs: pressure valve, manual operation button Outputs: servo, LED, beeper
If high pressure is detected, flash the LED for 10 seconds, then operate the beeper for 5 seconds, then operate servo; hold it open for 5 seconds and return the servo, LED and beeper to idle.
If the manual operation button is pressed, go directly to “operate
servo” as above.
If the manual button is pressed while the lid is open, close immediately.
When idle, wait for payment tap. If selection button is pressed before payment, display cost to inform the user for 3 seconds.
After payment tap, request the user select item.
Operate dispense motor.
Wait for object to be removed from output bin.
In all the above, events were implemented as a simple
unsigned char
, allowing 255 different event types.
There’s no reason we couldn’t use an unsigned short
or
even an int
. Further, a 16 bit event number could be
designed to be 8 bits of event type, and 8 bits of supplementary event
information:
typedef struct Event {
unsigned char type;
unsigned char info;
} Event;
= {MSG_KEYPRESS, KEY_A}; Event e
In the old Windows system, events/messages were realized as a 16 bit number, with extra 16 and 32 bit numbers glued to it for extra information.
typedef struct MSG {
;
UINT message;
WPARAM wParam;
LPARAM lParam}; // some extra detail removed
message == WM_CHAR == 0x0102
indicates that a
key was pressed, with the wParam
carrying the information
about which key.
The simplest state timer is made with a static variable associated with the state code.
To start the timer, simply initialize the static variable. On timer ticks, decrement (or increment if you prefer) this variable until it hits a limit, and then make a state change.
For instance, to create a timer that waits 100 timer ticks, you could use:
static int state;
static int stateTimer;
void stateCode(char event) {
switch(state) {
case STATE_IDLE:
if (event = EVENT_TRIGGER) {
= 100;
stateTimer = STATE_DELAY;
state }
break;
case STATE_DELAY:
if (event = EVENT_TICK) {
if (--stateTimer == 0) {
// the timer is finished
= STATE_NEXT;
state }
}
break;
// ....
}
}
In the above example, you could equally well have set the timer to start at zero and increment until it hits the desired limit (100 in this case).
In a moderate sized project, timers like this will proliferate throughout the code, making it awkward to read. One solution to this is to centralize the timers.
In all the above examples, the timer_isr()
code is
trivial, just newEvent(EVT_TICK)
. Suppose we add code to
the timer_isr()
so that it can process timer counting on
behalf of the tasks……
Let’s create a centralized service called
setTimer(timer_index, timer_count)
.
A task can call this service with a unique timer_index
and a requested count. The timer_isr()
uses a pool of timer
registers, and will count out the ticks on behalf of the task, and when
the tick count is finished, the timer_isr()
code can
generate a unique event, perhaps EVT_TIMER_n
.
So the state code can then look something like this:
static int state;
void stateCode(char event) {
switch(state) {
case STATE_IDLE:
if (event = EVENT_TRIGGER) {
(TIMER_1,100);
setTimer= STATE_DELAY;
state }
break;
case STATE_DELAY:
if (event = EVENT_TIMER_1) {
// the timer is finished
= STATE_NEXT;
state }
}
break;
// ....
}
}
This makes the state code simpler to read, hiding all the increments/decrements and limit testing.
The overhead for this ends up in the timer_isr()
code,
and might look something like this:
static uint16 timers[NUM_TIMERS];
#define EVT_TIMER_OFFSET 100
enum {EVENT_TIMER_1=EVT_TIMER_OFFSET, EVENT_TIMER_2, EVENT_TIMER_3}; // 100, 101...
enum {TIMER_1, TIMER_2, TIMER_3}; // 0,1,2,....
void timer_isr() {
(EVT_TICK); // the main tick, fires every time
newEventfor (i=0; i<NUM_TIMERS) { // the system timers, fires on completion
if (timers[i]>0) {
if (--timers[i]==0)
(i + EVT_TIMER_OFFSET);
newEvent}
}
}
/* common service, available to all tasks */
void setTimer(int timerIndex, unsigned int timerCount) {
[timerIndex] = timerCount;
timers}
On a typical microcontroller running at 24MHz, with 5 timers, this
adds about 2 microseconds of extra time to the timer_isr()
code, which typically runs every 10 or 100msec. It simplifies the task
code, makes it more legible and probably reduces bugs that may appear by
duplication of code.
Another possible design for timers is to have the main
timer_isr()
increment a global atomic
voloatile int timer
variable, and the tasks can observe
this timer and set a target based on that.
volatile int timer; // on most microcontrollers, access to an int
// will be atomic
(void) {
INTERRUPT timer_isr++;
timer(EVT_TICK);
newEvent}
// .... state code ...
static int targetTime;
void stateCode(uchar event) {
switch(state) {
case STATE1:
//..... set a target
= time + DELAY_TIME;
targetTimer = state2;
state //.....
case STATE2:
if (event == EVT_TICK) {
if (time == targetTimer) {
//.... we've waited long enough
}
}
//...
}
}
Here is an example of a complete and tested project on a small STM32F030 board. Open all the little arrows to see the complete code.
#include "stm32f030.h"
/****** project hardware ******/
// on this demo board, there is a push button on PB13 and
// an LED on PA4 and PF5
#define LED 0x10 // port A bit 4 these LED are LOW=lit-up
#define LED2 0x50020 // port F bit 5
* GROUP[] = {GPIOA, GPIOB, GPIOC, GPIOD, 0, GPIOF}; // 0,0x10000,0x20000, etc
GPIO_TypeDef void gpio_set(uint32 bitPosition, bool value) {
* group = &((GROUP[bitPosition >> 16])->ODR);
vu32&= 0xFFFF;
bitPosition if (value)
*group |= bitPosition;
else
*group &= ~bitPosition;
}
/***** events ****/
#define NUM_EVENTS 10
volatile uchar events[NUM_EVENTS];
enum { EVT_NONE,
,
EVT_TICK};
EVT_BUTTONvoid newEvent(uchar e);
/********** tasks ***********/
void ledTask(uchar evt);
void respondToButtonTask(uchar evt);
/********** interrupts **************/
volatile uint32 tick; // increasing at 100 ticks/sec
// takes a year to roll-over
void timer_isr(void) {
++;
tick(EVT_TICK);
newEvent// this interrupt is auto-ack'd
}
void button_isr(void) {
(EVT_BUTTON);
newEvent->PR |= 0x3001; // ack it
EXTI}
/** newEvent
* add the event to the event queue
* wrapped in critical section
*
* @param the event
*/
void newEvent(char e) {
static uint nextEvent;
(); // critical section
dint[nextEvent++] = e;
eventsif (nextEvent==NUM_EVENTS)
= 0;
nextEvent ();
eint}
/***** init ******/
/* called by newlib startup */
void _init(void) {
// startup code
// use default clocks
// turn on all the GPIO's
->AHBENR = 0x005e0014;
RCC// enable SysCfg
->APB2ENR = 0x00004801;
RCC
// enable the two LEDs as outputs
->MODER = (GPIOA->MODER & 0xFFFFFCFF) | 0x00000100; // port A bit 4
GPIOA->MODER = (GPIOF->MODER & 0xFFFFF3FF) | 0x00000400; // port F bit 5
GPIOF// and the push button as input + pulldown
->PUPDR = (GPIOB->PUPDR & 0xF3FFFFFF) | 0x08000000; // pulldown on 13
GPIOB
// keep the clocking system simple: just use the 8MHz HSI everywhere
->LOAD = 10000; // 10 msec
SysTick->VAL = 0;
SysTick->CTRL = 3; // count at 1usec, use interrupts
SysTick
/* to configure an interrupt on the stm32f0xx,
- enable the EXTI->IMR for the pin
- set EXTI->RTSR for select rising edge
- set the SYSCFG->EXTICRx pin to route it
- enable the gating bit in the NVIC register
- don't forget to ack each interrupt at EXTI->PR
*/
->IMR = 0x2000; // enable interrupt from line 13
EXTI->RTSR = 0x2000; // interrupt on rising edge
EXTI->EXTICR[3] = 0x0010; // select prot B for exti-13
SYSCFG->ISER[0] = 0x00E1; // enable in NVIC: gpio & watchdog
NVIC}
void main(void) {
();
eint
// dispatcher loop
while(1) {
int j;
for (j=0; j<NUM_EVENTS; j++) {
while (events[j]==EVT_NONE)
{}
(events[j]);
ledTask(events[j]);
respondToButtonTask[j] = EVT_NONE;
events}
}
}
/*********** task code, with states ************/
enum {LED_ON, LED_OFF};
enum {RTB_IDLE, RTB_ON}; // states
static uchar rtbState = RTB_IDLE;
static uint16 rtbTimerCount = 0;
const uint16 BUTTON_LED_ON_TIME = 150;
void respondToButtonTask(uchar evt) {
switch(rtbState) {
case RTB_IDLE:
if (evt == EVT_BUTTON) {
= RTB_ON;
rtbState = BUTTON_LED_ON_TIME;
rtbTimerCount (LED, LED_ON);
gpio_set}
break;
case RTB_ON:
if (evt == EVT_TICK) {
if (--rtbTimerCount == 0) {
(LED, LED_OFF);
gpio_set= RTB_IDLE;
rtbState }
}
break;
}
}
const int LED_ON_TIME = 150;
const int LED_OFF_TIME = 50;
static uchar ledState = LED_OFF;
static uint16 ledTimerCount = 0;
void ledTask(uchar evt) {
switch(ledState) {
case LED_OFF:
if (evt == EVT_TICK) {
if (++ledTimerCount > LED_OFF_TIME) {
(LED2, LED_ON);
gpio_set= LED_ON_TIME;
ledTimerCount = LED_ON;
ledState }
}
break;
case LED_ON:
if (evt == EVT_TICK) {
if (--ledTimerCount == 0) {
(LED2, LED_OFF);
gpio_set= 0;
ledTimerCount = LED_OFF;
ledState }
}
break;
}
}
/* vector table */
#define STACK_TOP 0x20002000
void default_isr(void) {}
extern void _start(void);
void (*myvectors[])(void) __attribute__ ((section(".vectors")))= {
(void(*)(void)) STACK_TOP, // stack pointer
, // code entry point
_start, // handle non-maskable interrupts
default_isr, // handle hard faults
default_isr0,0,0,0, /* 10...1f */
0,0,0,0, /* 20...2f */
0,0,0,timer_isr, /* 30...3f */
0,0,0,0,
0,button_isr,button_isr,button_isr, /* 50...5f */
0,0,0,0, /* 60...6f */
0,0,0,0, /* 70...7f */
0,0,0,0, /* 80...8f */
0,0,0,0, /* 90...9f */
0,0,0,0, /* a0...af */
0,0,0,0, /* b0...bf */
0,0,0,0, /* c0...cf */
0,0,0,0, /* d0...df */
0,0,0,0, /* e0...ef */
0,0,0,0, /* f0...ff */
0,0,0,0, /* 100.10f */
0,0,0,0 /* 110.11f */
};
For things like flashing LED’s, you could create two states, and toggle between them, like this:
Or, you could simply have an internal (static, not-on-the-stack) variable which can keep track of the LED toggle. The state diagram then simplifies:
uchar rtbState = RTB_IDLE; // major state, RTB_IDLE/RTB_FLASHING
uchar rtbSubState = RTB_FLASH_OFF; // minor state, toggles LED on/off
Alternatively, you could use the timer counter variable, and make changes at the half-way point through the count. This simplifies the substate design to this:
const uint16 FLASH_CYCLE_TIME 150;
const uint16 FLASH_ON_TIME 40;
uchar rtbState = STATE_IDLE;
void rtbTaskCode(char event) {
static uint16 flashCount=0;
switch(taskState) {
case RTB_IDLE:
if (event == EVT_BUTTON) {
= STATE_FLASHING;
rtbState = FLASH_CYCLE_TIME;
flashCount }
break;
case STATE_FLASHING:
// count down flashCount, toggle LED halfway through
// effectively creating a substate
if (event == EVT_TICK) {
if (--flashCount == 0) {
(OFF);
setLED= FLASH_CYCLE_TIME;
flashCount } else if (flashCount == FLASH_ON_TIME) {
(ON);
setLED}
// ... make sure to turn LED off when leaving this state
if (event == EVT_BUTTON) {
(OFF);
setLED= RTB_IDLE;
rtbState }
break;
}
}
}
Notice that there’s no initialization of states in this code. It
would be very handy if we knew when to initialize all the
lights and buzzers and match up the various states. Perhaps the
main()
code could add a single event into the event queue
at power-up, perhaps EVT_INIT
. It might be added like
this:
void main(void) {
(EVT_INIT); // early, before interrupts, so we know it's first in line
newEvent();
eintwhile(1) {
... dispatcher code ...
}
}
Then, in the state code, you can catch that event and set up whatever might be required
uchar myState;
void stateCode(uchar event) {
if (event==EVT_INIT) {
// ... do setup code
= FIRST_STATE;
myState return;
}
// ... regular state machine code
}
Now that we have an event/dispatcher system, we can also also use it to send information asychronously between tasks. For example, if a keyboard processing task needs to inform its siblings, it can create messages which can be injected into the event queue.
Notice the change of terminology, where message indicates that the entry was created by code procesing, rather than an interrupt.
We need a service routine to add messages to the event queue:
postMessage()
. This call is very smimilar
newEvent()
which should only be called from interrupts.
void postMessage(uchar message);
Notice that postMessage
can’t return any information,
because it’s not processed immediately; the message is added to
the event queue to be processed at a later time.
postMessage()
is exactly the same as
newEvent()
. The reason that I use different names is that
some more sophisticated operating systems require that these two
functions are not identical.
In some cases we may want the sibling task to process the information immediately. This means:
To return a value means that the prototype for task functions would change from
void taskCode(uchar event);
touchar taskCode(uchar event);
The service routine to send a message like this would look like:
uchar SendMessage(int8 taskPointer(uchar), uchar message) {
return taskPointer(message);
}
Why might one want to send a message between tasks?
PostMessage() Suppose you have a rotary encoder, which sends quadrant signals, which need to be interpreted as clockwise and counter clockwise. You could have one task devoted to determining the direction of the knob (and debouncing), and have it send (or post) clean EVT_CW and EVT_CCW messages to its sibgling tasks.
Another possible use of messages is to create alternate souces of input. For example, a device which has a temperature set by “up/down” buttons on a front panel could receive the same controls from an infra-red remote, or even a serial port (perhaps for testing).
SendMessage() Perhaps you need to have a sibling
task change states, or process information before you continue
in your task. Imagine there is a slave WiFi chip that needs to be
powered up before you send it a request….you could use
sendMessage()
to activate the power, and then continue,
knowing that your peripheral is available.
I taught this coding technique at the college level, and a typical end-of-term assignment was to code up a simulated automobile entertainment system, which included about 6-10 concurrent tasks:
Each list item above corresponds to one task. My students were able to code this up with about 35k of source, which compiles to about 8k of code and 256B of RAM (on an STM32)
Suppose we have a +
and -
button for tuning
a radio. A press of either will move the radio frequency by one step. If
the button is held for more than 0.5 seconds, the radio should change to
a rapid scroll through all available frequencies.
Suppose we have events coming at 100msec, and that the radio frequencies are 88.7 up to 107.9, with steps at 0.1. And finally, suppose there are 5 available events:
enum {EVT_NONE, EVT_TICK, EVT_UP_PRESS, EVT_UP_RELEASE, EVT_DN_PRESS, EVT_DN_RELEASE};
const float MIN_FREQ=88.7, MAX_FREQ=107.9, INC_FREQ=0.1;
float radioFreq = MIN_FREQ;
uint16 radioTimer = 0;
const uint16 radioTimeout = 5; // 0.5 seconds
enum {IDLE, FREQ_UP, FREQ_UP_FAST, FREQ_DN, FREQ_DN_FAST};
uchar state = IDLE;
void radioTask(uchar event) {
switch(state) {
case IDLE:
if (event == EVT_UP_PRESS) {
+= INC_FREQ;
radioFreq ();
checkRadioFreq= radioTimeout;
radioTimer = FREQ_UP;
state }
if (event == EVT_DN_PRESS) {
-= INC_FREQ;
radioFreq ();
checkRadioFreq= radioTimeout;
radioTimer = FREQ_DN;
state }
break;
case FREQ_UP:
if (event == EVT_UP_RELEASE)
= IDLE;
state if (event == EVT_TICK) {
if (--radioTimer == 0)
= FREQ_UP_FAST;
state }
break;
case FREQ_UP_FAST:
if (event == EVT_TICK) {
+= INC_FREQ;
radioFreq ();
checkRadioFreq}
if (event == EVT_UP_RELEASE)
= IDLE;
state break;
// and the same for FREQ_DN and FREQ_DN_FAST
}
}
// manage rollover
void checkRadioFreq() {
if (radioFreq > MAX_FREQ)
= MIN_FREQ;
radioFreq if (radioFreq < MIN_FREQ)
= MAX_FREQ;
radioFreq }
This may seem like a lot of code, but it compiles to something quite small. It is non-blocking, and easily maintained and modified. And it works.
Another example would be a user interface that had two buttons, marked “up” and “down”, an LED marked “setting”, some sort of numeric display, and some output device which needs to be controlled, such as a heater element, a motor on a mixer or even a volume setting on a sound system.
The user presses the “up” or “down” button, and the “setting: LED lights, to inform the user. Further presses of”up” or “down” modify the numeric display, but don’t yet affect the device which is being controlled.
When the users pauses for 1.5 seconds, the LED is extinguished and the new setting is used to control the output device.
The state diagram might look something like
It’s pretty common for these small microprocessors to have a UART service builtin. And it’s also pretty common for these small micros to be used as a slave to a more sophisticated master computer.
So I have written a UART streaming service as a task which fits into this Cooperative Multitasking (event driven) operating system.