|
@@ -0,0 +1,1858 @@
|
|
|
+---
|
|
|
+title: Cooperative Multitasking
|
|
|
+author: Pat Beirne
|
|
|
+email: <patb@pbeirne.com>
|
|
|
+date: 2025/01/15
|
|
|
+license: MIT
|
|
|
+---
|
|
|
+
|
|
|
+# Part 1: Cooperative Multitasking
|
|
|
+
|
|
|
+<!-- ## Introduction -->
|
|
|
+
|
|
|
+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](https://linux.org) and [FreeRTos](https://freertos.org).
|
|
|
+
|
|
|
+The core of *this* technique is covered in
|
|
|
+[Part 1](#part-1-cooperative-multitasking)
|
|
|
+of this paper.
|
|
|
+If you just want to see how it all comes together,
|
|
|
+jump to [Final Implementaion](#final-implementation).
|
|
|
+
|
|
|
+[Part 2](#part-2) contains enhancements and variations,
|
|
|
+probably useful reading if you decide to adopt
|
|
|
+this programming technique in your projects.
|
|
|
+
|
|
|
+## Intro
|
|
|
+
|
|
|
+### Abstract
|
|
|
+
|
|
|
+This paper will take you through the creation of a tiny *operating system*
|
|
|
+that can be implemented on a small microcontroller.
|
|
|
+
|
|
|
+### Audience
|
|
|
+
|
|
|
+These techniques can be applied by anyone with experience in ***C***,
|
|
|
+***Python*** 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.
|
|
|
+
|
|
|
+<details><summary>Reality Check</summary>
|
|
|
+The technique described here is also called ***event driven programming***.
|
|
|
+
|
|
|
+This technique was used in the original Window (1995),
|
|
|
+including some of the system calls: `sendMessage(), postMessage() and setTimer()`.
|
|
|
+
|
|
|
+This event-driven technique is applicable to a whole host
|
|
|
+of small microcontrollers, including
|
|
|
+
|
|
|
+- MSP430
|
|
|
+- Cortex M0, M0+ (SAM, STM32, PY32, Cypress, Kinetis, HT32, XMC, LPC81x)
|
|
|
+- AtMega, AtTiny
|
|
|
+- 8051 (SiliconLabs, Nuvoton, HT85)
|
|
|
+- RL78 (Renesas)
|
|
|
+- Pic 12/14/16
|
|
|
+- Risc (ch32v)
|
|
|
+- STM8
|
|
|
+
|
|
|
+*Some of the really tiny ones don't have enough stack space to implement these
|
|
|
+techniques (Puolop PB150, Padauk PxS15x, Bojuxing BJ8P,
|
|
|
+Yspring MDT1x, EastSoft HR7P, Holtek Ht68.)*
|
|
|
+
|
|
|
+</details>
|
|
|
+
|
|
|
+## The Problem
|
|
|
+
|
|
|
+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?
|
|
|
+
|
|
|
+## First Page
|
|
|
+
|
|
|
+Let's look an a pseudocode overview of what we want to do:
|
|
|
+
|
|
|
+```C
|
|
|
+void initialize(void) {
|
|
|
+ setup_hardware();
|
|
|
+ setup_interrupts();
|
|
|
+}
|
|
|
+
|
|
|
+INTERRUPT void irq(void) {
|
|
|
+ create_event();
|
|
|
+ acknowledge_interrupt();
|
|
|
+}
|
|
|
+
|
|
|
+void main(void) {
|
|
|
+ initialize();
|
|
|
+
|
|
|
+ while (1) {
|
|
|
+ if (event) {
|
|
|
+ flashLedTask(event);
|
|
|
+ respondToButtonTask(event);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+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.
|
|
|
+Don't worry, it will all be laid out in the following pages.
|
|
|
+
|
|
|
+If you're concerned with complexity, feel free to jump ahead to the
|
|
|
+[final implementation](#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.
|
|
|
+
|
|
|
+## Interrupts
|
|
|
+
|
|
|
+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 repsond 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
|
|
|
+somthing like this:
|
|
|
+
|
|
|
+```C
|
|
|
+enum {EVT_NONE, EVT_TICK, EVT_BUTTON};
|
|
|
+
|
|
|
+INTERRUPT timer_isr(void) {
|
|
|
+ newEvent(EVT_TICK);
|
|
|
+}
|
|
|
+
|
|
|
+INTERRUPT button_isr(void) {
|
|
|
+ newEvent(EVT_BUTTON); // see notes about button bounce and acknowledgement
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+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*.
|
|
|
+
|
|
|
+<details>
|
|
|
+<summary>Reality check</summary>
|
|
|
+
|
|
|
+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]](#debouncing). 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.
|
|
|
+
|
|
|
+</details>
|
|
|
+
|
|
|
+## Events
|
|
|
+
|
|
|
+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*.
|
|
|
+
|
|
|
+```C
|
|
|
+volatile uint8 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`](https://en.wikipedia.org/wiki/Volatile_(computer_programming)).
|
|
|
+
|
|
|
+In this paper, the word *message* and *event* are equivalent.
|
|
|
+*Message* has the sense of
|
|
|
+"a bit of communication from another part of the program"
|
|
|
+and *event* has the sense of "something
|
|
|
+external just happened".
|
|
|
+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.
|
|
|
+
|
|
|
+```C
|
|
|
+volatile uint8 event;
|
|
|
+enum {EVT_NONE, EVT_TICK, EVT_BUTTON}; // 0,1,2
|
|
|
+```
|
|
|
+
|
|
|
+<details>
|
|
|
+<summary>Reality check</summary>
|
|
|
+
|
|
|
+<hr>
|
|
|
+For this project, let's suppose the timer ticks happen every 10ms (100 per second).
|
|
|
+<hr>
|
|
|
+
|
|
|
+</details>
|
|
|
+
|
|
|
+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()`.
|
|
|
+
|
|
|
+## newEvent()
|
|
|
+
|
|
|
+Here's a block diagram of the message flow ![](dispatch1.jpg)
|
|
|
+
|
|
|
+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 uint8` 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 uint8 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:
|
|
|
+
|
|
|
+```C
|
|
|
+void newEvent(uint8r evt) {
|
|
|
+ static unsigned uint8 nextEvent;
|
|
|
+ events[nextEvent++] = evt;
|
|
|
+ if (nextEvent == NUM_EVENTS)
|
|
|
+ nextEvent = 0;
|
|
|
+}
|
|
|
+```
|
|
|
+<details>
|
|
|
+<summary>Reality check</summary>
|
|
|
+
|
|
|
+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:
|
|
|
+
|
|
|
+```C
|
|
|
+void newEvent(char evt) {
|
|
|
+ static unsigned char nextEvent; // keep track of where we are in queue
|
|
|
+ disable_irq(); // critical section
|
|
|
+ events[nextEvent++] = evt; // insert event into queue
|
|
|
+ if (nextEvent == NUM_EVENTS) // loop back to the start of queue
|
|
|
+ nextEvent = 0;
|
|
|
+ enable_irq(); // end critical section, probably <100us of blockage
|
|
|
+}
|
|
|
+```
|
|
|
+</details>
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+
|
|
|
+## Dispatch
|
|
|
+
|
|
|
+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](#dispatcher-details).
|
|
|
+
|
|
|
+<details><summary>Terminology</summary>
|
|
|
+
|
|
|
+The `main()` code illustrated here calls the tasks
|
|
|
+as subroutines, sending each one a copy of the event number.
|
|
|
+
|
|
|
+As I mentioned earlier, *events* can also be refered to as *messages*.
|
|
|
+We can also refer to this process "sending
|
|
|
+a message to the task"
|
|
|
+
|
|
|
+So, for this paper, these are equivalent
|
|
|
+
|
|
|
+- calling a task subroutine with the event information
|
|
|
+- sending a message to a task
|
|
|
+
|
|
|
+</details>
|
|
|
+
|
|
|
+![Call task routines with event](dispatch2.jpg)
|
|
|
+
|
|
|
+<details>
|
|
|
+
|
|
|
+<summary>Reality check</summary>
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+</details>
|
|
|
+
|
|
|
+## Tasks
|
|
|
+
|
|
|
+In this environment, the code that impliments a *task* is simply a
|
|
|
+subroutine that accepts an *event/message*.
|
|
|
+
|
|
|
+```C
|
|
|
+void taskCode(uint8 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](#state-machine).
|
|
|
+
|
|
|
+## 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.
|
|
|
+
|
|
|
+![simple state diagram](rtb_state.png)
|
|
|
+
|
|
|
+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:
|
|
|
+
|
|
|
+```C
|
|
|
+enum {RTB_IDLE, RTB_ON}; // RTB_IDLE=0, RTB_ON=1
|
|
|
+static uint8 rtbState = RTB_IDLE;
|
|
|
+static uint16 rtbTimerCount = 0;
|
|
|
+const uint16 TIMER_LIMIT = 150;
|
|
|
+
|
|
|
+void respondToButtonTask(uint8 evt) {
|
|
|
+ switch(rtbState) {
|
|
|
+ case RTB_IDLE:
|
|
|
+ if (evt == EVT_BUTTON) {
|
|
|
+ rtbState = RTB_ON;
|
|
|
+ rtbTimerCount = TIMER_LIMIT;
|
|
|
+ gpio_set(LED, ON);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case RTB_ON:
|
|
|
+ if (evt == EVT_TICK) {
|
|
|
+ if (--rtbTimerCount == 0) {
|
|
|
+ gpio_set(LED, OFF);
|
|
|
+ rtbState = RTB_IDLE;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Each time this routine is called, it checks the *event* that it was given,
|
|
|
+and sometimes processes it. And sometimes it changes state.
|
|
|
+
|
|
|
+Here's a few things to notice:
|
|
|
+
|
|
|
+- The code *flows through*. It does not stop or wait for anything.
|
|
|
+- Each arrow in the net diagram corresponds to a phrase in the code. The *tail* of the arrow corresponds
|
|
|
+to an `if` statement
|
|
|
+- The code ignores *events* that are not relevant.
|
|
|
+
|
|
|
+<details>
|
|
|
+<summary>Reality check</summary>
|
|
|
+
|
|
|
+The *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 tick counter could equally well count up from zero to threshold. See the
|
|
|
+discussion about [shared timers](#timers-as-a-resource)
|
|
|
+</details>
|
|
|
+
|
|
|
+
|
|
|
+## Another State Machine
|
|
|
+
|
|
|
+The other task of this project simply flashes the LED on and off.
|
|
|
+
|
|
|
+
|
|
|
+![](led_state_simple.png)
|
|
|
+
|
|
|
+The code for this might be:
|
|
|
+
|
|
|
+
|
|
|
+```C
|
|
|
+enum {LED_ON, LED_OFF};
|
|
|
+static uint8 ledState = LED_OFF;
|
|
|
+static uint16 ledTimerCount = 0;
|
|
|
+const uint16 LED_ON_TIME = 100;
|
|
|
+const uint16 LED_OFF_TIME = 100;
|
|
|
+
|
|
|
+void ledTask(uint8 evt) {
|
|
|
+ switch(ledState) {
|
|
|
+ case LED_OFF:
|
|
|
+ if (evt == EVT_TICK) {
|
|
|
+ if (--ledTimerCount == 0) {
|
|
|
+ gpio_set(LED, ON);
|
|
|
+ ledTimerCount = LED_ON_TIME;
|
|
|
+ ledState = LED_ON;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case LED_ON:
|
|
|
+ if (evt == EVT_TICK) {
|
|
|
+ if (--ledTimerCount == 0) {
|
|
|
+ gpio_set(LED, OFF);
|
|
|
+ ledTimerCount = LED_OFF_TIME;
|
|
|
+ ledState = LED_OFF;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+<details><summary>Discussion</summary>
|
|
|
+
|
|
|
+Perhaps a more accurate state diagram might be:
|
|
|
+
|
|
|
+![](led_state.png)
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+```C
|
|
|
+if (state==1) {
|
|
|
+ if (event = EVT_1) {
|
|
|
+ P3OUT = 27; // turn on some hardware
|
|
|
+ state = 2; // and change state
|
|
|
+ }
|
|
|
+}
|
|
|
+if (state==2) { // <<<----- BAD BAD
|
|
|
+ P3OUT = 14;
|
|
|
+ state = 3; // <<<--- dont' change state twice in the same event
|
|
|
+}
|
|
|
+...
|
|
|
+```
|
|
|
+rather
|
|
|
+```C
|
|
|
+switch(state) {
|
|
|
+ case 1:
|
|
|
+ if (event = EVT_1) {
|
|
|
+ P3OUT = 27; // turn on some hardware
|
|
|
+ state = 2;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case 2:
|
|
|
+ P3OUT = 14;
|
|
|
+ state = 3;
|
|
|
+ }
|
|
|
+ ...
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+</details>
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+## Dispatcher Details
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+```C
|
|
|
+void main() {
|
|
|
+ initialize();
|
|
|
+
|
|
|
+ while(1) {
|
|
|
+ int i;
|
|
|
+ for (i=0; i<NUM_EVENTS; i++) {
|
|
|
+ while (events[i]==EVT_NONE)
|
|
|
+ {}
|
|
|
+ ledTask(events[i]);
|
|
|
+ respondToButtonTask(events[i]);
|
|
|
+ events[i] = EVT_NONE;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+<details>
|
|
|
+<summary>Reality check</summary>
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+</details>
|
|
|
+
|
|
|
+
|
|
|
+## Final Implementation
|
|
|
+
|
|
|
+Let's put all of the above together, for an SMT32F Cortex M0, in *C* code.
|
|
|
+
|
|
|
+```C
|
|
|
+/***** declarations ****/
|
|
|
+#define NUM_EVENTS 10
|
|
|
+volatile uint8 events[NUM_EVENTS];
|
|
|
+
|
|
|
+void newEvent(uint8 e);
|
|
|
+void ledTask(uint8 evt);
|
|
|
+void respondToButtonTask(uint8 evt);
|
|
|
+
|
|
|
+/********** interrupts **************/
|
|
|
+
|
|
|
+void timer_isr(void) {
|
|
|
+ newEvent(EVT_TICK);
|
|
|
+ // this interrupt is auto-ack'd
|
|
|
+}
|
|
|
+
|
|
|
+void button_isr(void) {
|
|
|
+ newEvent(EVT_BUTTON);
|
|
|
+ EXTI->PR |= KEY_IRQ_ACK_MASK; // the hardware requires that we acknowledge
|
|
|
+}
|
|
|
+
|
|
|
+/** newEvent
|
|
|
+ * add the event to the event queue
|
|
|
+ * wrapped in critical section
|
|
|
+ *
|
|
|
+ * @param the event
|
|
|
+ */
|
|
|
+void newEvent(uint8 e) {
|
|
|
+ static uint8 nextEvent;
|
|
|
+ dint(); // critical section
|
|
|
+ events[nextEvent++] = e;
|
|
|
+ if (nextEvent==NUM_EVENTS)
|
|
|
+ nextEvent = 0;
|
|
|
+ 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)
|
|
|
+ {}
|
|
|
+ ledTask(events[j]);
|
|
|
+ respondToButtonTask(events[j]);
|
|
|
+ events[j] = EVT_NONE;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/*********** task code, with states ************/
|
|
|
+enum {LED_ON, LED_OFF};
|
|
|
+enum {RTB_IDLE, RTB_ON}; // states
|
|
|
+static uint8 rtbState = RTB_IDLE;
|
|
|
+static uint16 rtbTimerCount = 0;
|
|
|
+const uint16 LED_ON_TIME = 150;
|
|
|
+
|
|
|
+void respondToButtonTask(uint8 evt) {
|
|
|
+ switch(rtbState) {
|
|
|
+ case RTB_IDLE:
|
|
|
+ if (evt == EVT_BUTTON) {
|
|
|
+ rtbTimerCount = 0;
|
|
|
+ rtbState = RTB_ON;
|
|
|
+ gpio_set(LED, LED_ON);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case RTB_ON:
|
|
|
+ if (evt == EVT_TICK) {
|
|
|
+ if (++rtbTimerCount > LED_ON_TIME) {
|
|
|
+ gpio_set(LED, LED_OFF);
|
|
|
+ rtbState = RTB_IDLE;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const uint16 LED_ON_TIME = 150;
|
|
|
+const uint16 LED_OFF_TIME = 50;
|
|
|
+static uint8 ledState = LED_OFF;
|
|
|
+static uint16 ledTimerCount = 0;
|
|
|
+
|
|
|
+void ledTask(uint8 evt) {
|
|
|
+ switch(ledState) {
|
|
|
+ case LED_OFF:
|
|
|
+ if (evt == EVT_TICK) {
|
|
|
+ if (++ledTimerCount > LED_OFF_TIME) {
|
|
|
+ gpio_set(LED2, LED_ON);
|
|
|
+ ledTimerCount = 0;
|
|
|
+ ledState = LED_ON;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case LED_ON:
|
|
|
+ if (evt == EVT_TICK) {
|
|
|
+ if (++ledTimerCount > LED_ON_TIME) {
|
|
|
+ gpio_set(LED2, LED_OFF);
|
|
|
+ ledTimerCount = 0;
|
|
|
+ ledState = LED_OFF;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ 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:
|
|
|
+
|
|
|
+- [more state code examples](#state-machine-examples)
|
|
|
+ - with implementation
|
|
|
+- [how to initialize a state machine](#state-machine-initialization)
|
|
|
+- [inter-task communication](#messages)
|
|
|
+- [unified timers](#timers)
|
|
|
+
|
|
|
+<details><summary>Reality Check</summary>
|
|
|
+
|
|
|
+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 [here](build/demo.c)
|
|
|
+
|
|
|
+The above example only shows 2 tasks, so all the formal structure may
|
|
|
+seem a bit silly. But once you have the infrastructure in place, you can
|
|
|
+easily handle *dozens* of tasks, even on sub-$1 processors.
|
|
|
+</details>
|
|
|
+
|
|
|
+# Part 2
|
|
|
+
|
|
|
+## Variations
|
|
|
+
|
|
|
+<style>
|
|
|
+.imageleft {float: left;}
|
|
|
+.imageright {float: right;}
|
|
|
+</style>
|
|
|
+
|
|
|
+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:
|
|
|
+
|
|
|
+![](rtb_state_2.png){.imageleft}
|
|
|
+
|
|
|
+```C
|
|
|
+void rtbTask(uint8 event) {
|
|
|
+ switch(rtbState) {
|
|
|
+ case RTB_IDLE:
|
|
|
+ if (event == EVT_BUTTON) {
|
|
|
+ gpio_set(LED, LED_ON);
|
|
|
+ rtbState = RTB_ON;
|
|
|
+ rtbTimer = RTB_TIMEOUT;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case RTB_ON:
|
|
|
+ if (event == EVT_BUTTON) {
|
|
|
+ rtbTimer = RTB_TIMEOUT;
|
|
|
+ }
|
|
|
+ if (event == EVT_TICK) {
|
|
|
+ if (--rtbTimer == 0) {
|
|
|
+ gpio_set(LED, LED_OFF);
|
|
|
+ rtbState = RTB_IDLE;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+<hr>
|
|
|
+
|
|
|
+Or have a 2nd press of the button cause the LED to extinguish early:
|
|
|
+
|
|
|
+![](rtb_state_3.png){.imageleft}
|
|
|
+
|
|
|
+```C
|
|
|
+void rtbTask(uint8 event) {
|
|
|
+ switch(rtbState) {
|
|
|
+ case RTB_IDLE:
|
|
|
+ if (event == EVT_BUTTON) {
|
|
|
+ gpio_set(LED, LED_ON);
|
|
|
+ rtbState = RTB_ON;
|
|
|
+ rtbTimer = RTB_TIMEOUT;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case RTB_ON:
|
|
|
+ if (event == EVT_BUTTON) {
|
|
|
+ gpio_set(LED, LED_OFF);
|
|
|
+ rtbState = RTB_IDLE;
|
|
|
+ }
|
|
|
+ if (event == EVT_TICK) {
|
|
|
+ if (--rtbTimer == 0) {
|
|
|
+ gpio_set(LED, LED_OFF);
|
|
|
+ rtbState = RTB_IDLE;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+<hr>
|
|
|
+
|
|
|
+
|
|
|
+How about have the button start a flash sequence, and a 2nd press stops
|
|
|
+it: (see also [substates](#substates))
|
|
|
+
|
|
|
+![](rtb_state_4.png){.imageright}
|
|
|
+
|
|
|
+```C
|
|
|
+void rtbTask(uint8 event) {
|
|
|
+ switch(rtbState) {
|
|
|
+ case RTB_IDLE:
|
|
|
+ if (event == EVT_BUTTON) {
|
|
|
+ gpio_set(LED, LED_ON);
|
|
|
+ rtbState = RTB_ON;
|
|
|
+ rtbTimer = RTB_TIMEOUT;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case RTB_ON:
|
|
|
+ if (event == EVT_BUTTON) {
|
|
|
+ gpio_set(LED, LED_OFF);
|
|
|
+ rtbState = RTB_IDLE;
|
|
|
+ }
|
|
|
+ if (event == EVT_TICK) {
|
|
|
+ if (--rtbTimer == 0) {
|
|
|
+ gpio_set(LED, LED_OFF);
|
|
|
+ rtbTimer = RTB_FLASH_TIME;
|
|
|
+ rtbState = RTB_OFF;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case RTB_OFF:
|
|
|
+ if (event == EVT_BUTTON) {
|
|
|
+ gpio_set(LED, LED_OFF);
|
|
|
+ rtbState = RTB_IDLE;
|
|
|
+ }
|
|
|
+ if (event == EVT_TICK) {
|
|
|
+ if (--rtbTimer == 0) {
|
|
|
+ gpio_set(LED, LED_ON);
|
|
|
+ rtbTimer = RTB_FLASH_TIME;
|
|
|
+ rtbState = RTB_ON;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+<hr>
|
|
|
+
|
|
|
+Each of these diagrams corresponds to trivial changes in the state code.
|
|
|
+
|
|
|
+
|
|
|
+## Working with Arduino
|
|
|
+
|
|
|
+If you're from the Arduino world, you have no doubt seen the similarity
|
|
|
+between this OS plan and the Arduino infrastructure.
|
|
|
+
|
|
|
+```C
|
|
|
+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:
|
|
|
+
|
|
|
+```C
|
|
|
+char events[];
|
|
|
+
|
|
|
+interrupt_service_routines() {
|
|
|
+ newEvent(evt); // insert events/messages into events[]
|
|
|
+}
|
|
|
+
|
|
|
+static int nextEvent;
|
|
|
+void newEvent(char evt)
|
|
|
+ {} // same as above; put evt into the events[]
|
|
|
+
|
|
|
+setup()
|
|
|
+ {}
|
|
|
+
|
|
|
+loop() {
|
|
|
+ static int nextTaskEvent;
|
|
|
+ if (events[nextTaskEvent]!=EVT_NONE) { // check for non-zero events[],
|
|
|
+ task1(events[nextTaskEvent]); // and call the task routines
|
|
|
+ task2(events[nextTaskEvent]);
|
|
|
+ events[nextTaskEvent) = EVT_NONE; // free the slot, fill with 0
|
|
|
+ if (++nextTaskEvent > NUM_EVENTS)
|
|
|
+ nextTaskEvent = 0; // and loop through the array
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+void task1(char evt) {}
|
|
|
+void task2(char evt) {}
|
|
|
+
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+## Tasks
|
|
|
+
|
|
|
+The fundamental guideline for tasks is that *they do not stop*.
|
|
|
+Control flows through and out
|
|
|
+the bottom, returning to the dispatcher quickly.
|
|
|
+
|
|
|
+<details><summary>Reality Check</summary>
|
|
|
+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 tast. Process the new state later.
|
|
|
+<hr>
|
|
|
+</details>
|
|
|
+
|
|
|
+
|
|
|
+## State Machine Examples
|
|
|
+
|
|
|
+### Car Window
|
|
|
+
|
|
|
+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.
|
|
|
+![window motor control](window_state.png)
|
|
|
+
|
|
|
+And here's the matching code. Note the correspondence between the diagram
|
|
|
+and the code: arrows leaving a state correspond to an `if()` phrase.
|
|
|
+
|
|
|
+```C
|
|
|
+enum {WINDOW_IDLE, WINDOW_UP, WINDOW_DOWN};
|
|
|
+static uint8 windowState = WINDOW_IDLE;
|
|
|
+
|
|
|
+void windowTask(uint8 evt) {
|
|
|
+ switch(windowState) {
|
|
|
+ case WINDOW_IDLE:
|
|
|
+ if (evt == EVT_BUTTON_UP) {
|
|
|
+ gpio_set(MOTOR, UP);
|
|
|
+ windowState = WINDOW_UP;
|
|
|
+ }
|
|
|
+ if (evt == EVT_BUTTON_DOWN) {
|
|
|
+ gpio_set(MOTOR, DOWN);
|
|
|
+ windowState = WINDOW_DOWN;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case WINDOW_UP:
|
|
|
+ if (evt == EVT_MOTOR_SENSE ||
|
|
|
+ evt == EVT_BUTTON_RELEASE) {
|
|
|
+ gpio_set(MOTOR, OFF);
|
|
|
+ windowState = WINDOW_IDLE;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case WINDOW_DOWN:
|
|
|
+ if (evt == EVT_MOTOR_SENSE ||
|
|
|
+ evt == EVT_BUTTON_RELEASE) {
|
|
|
+ gpio_set(MOTOR, OFF);
|
|
|
+ windowState = WINDOW_IDLE;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+![window control with auto open/close, (_ma) motor activated (_ms) motor stopped](window_complex.png)
|
|
|
+
|
|
|
+<details><summary>Reality Check</summary>
|
|
|
+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
|
|
|
+independantly.
|
|
|
+
|
|
|
+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:
|
|
|
+ ...
|
|
|
+ ...
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+</details>
|
|
|
+
|
|
|
+
|
|
|
+### Fridge Door
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+![](fridge_state.png)
|
|
|
+
|
|
|
+And here is the corresponding code.
|
|
|
+
|
|
|
+```C
|
|
|
+enum {FRIDGE_CLOSED, FRIDGE_OPEN, FRIDGE_BEEPING};
|
|
|
+uint8 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) {
|
|
|
+ set_io(LIGHT, ON);
|
|
|
+ fridgeTimer = FRIDGE_OPEN_LIMIT;
|
|
|
+ fridgeState = FRIDGE_OPEN;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case FRIDGE_OPEN:
|
|
|
+ if (event == EVT_CLOSE) {
|
|
|
+ set_io(LIGHT, OFF);
|
|
|
+ fridgeState = FRIDGE_CLOSED;
|
|
|
+ }
|
|
|
+ if (evt == EVT_TICK) {
|
|
|
+ if (--fridgeTimer == 0) {
|
|
|
+ set_io(ALARM, ON);
|
|
|
+ fridgeState = FRIDGE_BEEPING;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case FRIDGE_BEEPING:
|
|
|
+ if (event == EVT_CLOSE) {
|
|
|
+ set_io(ALARM, OFF);
|
|
|
+ set_io(LIGHT, OFF);
|
|
|
+ fridgeState = FRIDGE_CLOSED;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+#### Refactor for `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:
|
|
|
+
|
|
|
+```C
|
|
|
+// 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
|
|
|
+```C
|
|
|
+void changeFridgeState(char newState) {
|
|
|
+ static char oldState = FRIDGE_CLOSED;
|
|
|
+ // do all the state-leaving actions
|
|
|
+ switch(oldState) {
|
|
|
+ case FRIDGE_CLOSED:
|
|
|
+ set_io(LIGHT, ON);
|
|
|
+ setTimer(FRIDGE_TIMER, FRIDGE_OPEN_LIMIT);
|
|
|
+ break;
|
|
|
+ case FRIDGE_OPEN:
|
|
|
+ break;
|
|
|
+ case FRIDGE_BEEPING:
|
|
|
+ set_io(ALARM, OFF);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ // change state
|
|
|
+ fridgeState = oldState = newState;
|
|
|
+ // and do the state-entry code
|
|
|
+ switch(newState) {
|
|
|
+ case FRIDGE_CLOSED:
|
|
|
+ set_io(LIGHT, OFF);
|
|
|
+ setTimer(FRIDGE_TIMER, 0);
|
|
|
+ break;
|
|
|
+ case FRIDGE_OPEN:
|
|
|
+ break;
|
|
|
+ case FRIDGE_BEEPING:
|
|
|
+ set_io(ALARM, ON);
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+
|
|
|
+### Door Opener
|
|
|
+
|
|
|
+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:
|
|
|
+
|
|
|
+![RFID operated door lock](door.png)
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+Here's the modified state diagram:
|
|
|
+
|
|
|
+![RFID operated door, with remove key verify](door2.png)
|
|
|
+
|
|
|
+### Beer Vat
|
|
|
+
|
|
|
+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.
|
|
|
+![state machine for beer vat pressure release valve](beer_vat.png)
|
|
|
+
|
|
|
+If the manual button is pressed while the lid is open, close immediately.
|
|
|
+
|
|
|
+### Vending Machine
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+![states for vending machine](vending.png)
|
|
|
+
|
|
|
+
|
|
|
+## Events
|
|
|
+
|
|
|
+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:
|
|
|
+
|
|
|
+```C
|
|
|
+typedef struct Event {
|
|
|
+ unsigned char type;
|
|
|
+ unsigned char info;
|
|
|
+} Event;
|
|
|
+
|
|
|
+Event e = {MSG_KEYPRESS, KEY_A};
|
|
|
+```
|
|
|
+
|
|
|
+In the old Windows system, *events/messages* were realized as a 16 bit number,
|
|
|
+with an extra 32 bit number glued to it for extra information.
|
|
|
+
|
|
|
+```C
|
|
|
+typedef struct MSG {
|
|
|
+ UINT message;
|
|
|
+ LPARAM lParam;
|
|
|
+}; // some extra detail removed
|
|
|
+```
|
|
|
+For example,
|
|
|
+`WM_CHAR=0x0102` indicates that a key was pressed, with the extra 32bit `lParam`
|
|
|
+carrying the information about *which* key.
|
|
|
+<hr>
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+## Timers
|
|
|
+
|
|
|
+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:
|
|
|
+
|
|
|
+```C
|
|
|
+static int state;
|
|
|
+static int stateTimer;
|
|
|
+void stateCode(char event) {
|
|
|
+ switch(state) {
|
|
|
+ case STATE_IDLE:
|
|
|
+ if (event = EVENT_TRIGGER) {
|
|
|
+ stateTimer = 100;
|
|
|
+ state = STATE_DELAY;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case STATE_DELAY:
|
|
|
+ if (event = EVENT_TICK) {
|
|
|
+ if (--stateTimer == 0) {
|
|
|
+ // the timer is finished
|
|
|
+ state = STATE_NEXT;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ 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_irq()` code is trivial, just `newEvent(EVT_TICK)`.
|
|
|
+Suppose we add code to the `timer_irq()` so that it can process timer counting on
|
|
|
+behalf of the tasks......
|
|
|
+
|
|
|
+### Timers as a Resource
|
|
|
+
|
|
|
+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_irq()` will then count out the ticks on behalf of the task, and when the tick
|
|
|
+count is finished, the `timer_irq()` code can generate a unique event, perhaps
|
|
|
+`EVT_TIMER_n`.
|
|
|
+
|
|
|
+So the state code can then look something like this:
|
|
|
+
|
|
|
+```C
|
|
|
+static int state;
|
|
|
+void stateCode(char event) {
|
|
|
+ switch(state) {
|
|
|
+ case STATE_IDLE:
|
|
|
+ if (event = EVENT_TRIGGER) {
|
|
|
+ setTimer(TIMER_1,100);
|
|
|
+ state = STATE_DELAY;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case STATE_DELAY:
|
|
|
+ if (event = EVENT_TIMER_1) {
|
|
|
+ // the timer is finished
|
|
|
+ state = STATE_NEXT;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ // ....
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+This makes the state code much simpler to read,
|
|
|
+hiding all the increments/decrements and limit testing.
|
|
|
+
|
|
|
+The overhead for this ends up in the `timer_irq()` code,
|
|
|
+and might look something like this:
|
|
|
+
|
|
|
+```C
|
|
|
+static unsigned int 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_irq() {
|
|
|
+ newEvent(EVT_TICK); // the main tick, fires every time
|
|
|
+ for (i=0; i<NUM_TIMERS) { // the system timers, fires on completion
|
|
|
+ if (timers[i]>0) {
|
|
|
+ if (--timers[i]==0)
|
|
|
+ newEvent(i + EVT_TIMER_OFFSET);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* common service, available to all tasks */
|
|
|
+void setTimer(int timerIndex, unsigned int timerCount) {
|
|
|
+ timers[timerIndex] = timerCount;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+<details><summary>Reality Check</summary>
|
|
|
+On a typical microcontroller running at 24MHz, with 5 timers, this adds about
|
|
|
+2 microseconds of extra time to the `timer_irq()` code, which typically runs every
|
|
|
+10 or 100msec. It considerably 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.
|
|
|
+
|
|
|
+```C
|
|
|
+volatile int timer; // on most microcontrollers, access to an int
|
|
|
+ // will be atomic
|
|
|
+INTERRUPT timer_isr(void) {
|
|
|
+ timer++;
|
|
|
+ newEvent(EVT_TICK);
|
|
|
+}
|
|
|
+
|
|
|
+// .... state code ...
|
|
|
+static int targetTime;
|
|
|
+void stateCode(uint8 event) {
|
|
|
+ switch(state) {
|
|
|
+ case STATE1:
|
|
|
+ //..... set a target
|
|
|
+ targetTimer = time + DELAY_TIME;
|
|
|
+ state = state2;
|
|
|
+ //.....
|
|
|
+ case STATE2:
|
|
|
+ if (event == EVT_TICK) {
|
|
|
+ if (time == targetTimer) {
|
|
|
+ //.... we've waited long enough
|
|
|
+ }
|
|
|
+ }
|
|
|
+ //...
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+```
|
|
|
+
|
|
|
+</details>
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+## Real Code
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+<details><summary>board specific defines</summary>
|
|
|
+
|
|
|
+```C
|
|
|
+#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
|
|
|
+GPIO_TypeDef * GROUP[] = {GPIOA, GPIOB, GPIOC, GPIOD, 0, GPIOF}; // 0,0x10000,0x20000, etc
|
|
|
+void gpio_set(uint32 bitPosition, bool value) {
|
|
|
+ vu32* group = &((GROUP[bitPosition >> 16])->ODR);
|
|
|
+ bitPosition &= 0xFFFF;
|
|
|
+ if (value)
|
|
|
+ *group |= bitPosition;
|
|
|
+ else
|
|
|
+ *group &= ~bitPosition;
|
|
|
+}
|
|
|
+```
|
|
|
+</details>
|
|
|
+
|
|
|
+```C
|
|
|
+/***** events ****/
|
|
|
+#define NUM_EVENTS 10
|
|
|
+volatile uint8 events[NUM_EVENTS];
|
|
|
+enum { EVT_NONE,
|
|
|
+ EVT_TICK,
|
|
|
+ EVT_BUTTON};
|
|
|
+void newEvent(uint8 e);
|
|
|
+
|
|
|
+/********** tasks ***********/
|
|
|
+void ledTask(uint8 evt);
|
|
|
+void respondToButtonTask(uint8 evt);
|
|
|
+
|
|
|
+/********** interrupts **************/
|
|
|
+volatile uint32 tick; // increasing at 100 ticks/sec
|
|
|
+ // takes a year to roll-over
|
|
|
+
|
|
|
+void timer_isr(void) {
|
|
|
+ tick++;
|
|
|
+ newEvent(EVT_TICK);
|
|
|
+ // this interrupt is auto-ack'd
|
|
|
+}
|
|
|
+
|
|
|
+void button_isr(void) {
|
|
|
+ newEvent(EVT_BUTTON);
|
|
|
+ EXTI->PR |= 0x3001; // ack it
|
|
|
+}
|
|
|
+
|
|
|
+/** newEvent
|
|
|
+ * add the event to the event queue
|
|
|
+ * wrapped in critical section
|
|
|
+ *
|
|
|
+ * @param the event
|
|
|
+ */
|
|
|
+void newEvent(char e) {
|
|
|
+ static uint nextEvent;
|
|
|
+ dint(); // critical section
|
|
|
+ events[nextEvent++] = e;
|
|
|
+ if (nextEvent==NUM_EVENTS)
|
|
|
+ nextEvent = 0;
|
|
|
+ eint();
|
|
|
+}
|
|
|
+```
|
|
|
+</details>
|
|
|
+
|
|
|
+<details><summary>hardware initialization</summary>
|
|
|
+
|
|
|
+```C
|
|
|
+/***** init ******/
|
|
|
+/* called by newlib startup */
|
|
|
+void _init(void) {
|
|
|
+ // startup code
|
|
|
+
|
|
|
+ // use default clocks
|
|
|
+ // turn on all the GPIO's
|
|
|
+ RCC->AHBENR = 0x005e0014;
|
|
|
+ // enable SysCfg
|
|
|
+ RCC->APB2ENR = 0x00004801;
|
|
|
+
|
|
|
+
|
|
|
+ // enable the two LEDs as outputs
|
|
|
+ GPIOA->MODER = (GPIOA->MODER & 0xFFFFFCFF) | 0x00000100; // port A bit 4
|
|
|
+ GPIOF->MODER = (GPIOF->MODER & 0xFFFFF3FF) | 0x00000400; // port F bit 5
|
|
|
+ // and the push button as input + pulldown
|
|
|
+ GPIOB->PUPDR = (GPIOB->PUPDR & 0xF3FFFFFF) | 0x08000000; // pulldown on 13
|
|
|
+
|
|
|
+ // keep the clocking system simple: just use the 8MHz HSI everywhere
|
|
|
+ SysTick->LOAD = 10000; // 10 msec
|
|
|
+ SysTick->VAL = 0;
|
|
|
+ SysTick->CTRL = 3; // count at 1usec, use interrupts
|
|
|
+
|
|
|
+ /* 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
|
|
|
+ */
|
|
|
+ EXTI->IMR = 0x2000; // enable interrupt from line 13
|
|
|
+ EXTI->RTSR = 0x2000; // interrupt on rising edge
|
|
|
+ SYSCFG->EXTICR[3] = 0x0010; // select prot B for exti-13
|
|
|
+ NVIC->ISER[0] = 0x00E1; // enable in NVIC: gpio & watchdog
|
|
|
+}
|
|
|
+```
|
|
|
+</details>
|
|
|
+
|
|
|
+```C
|
|
|
+void main(void) {
|
|
|
+
|
|
|
+ eint();
|
|
|
+
|
|
|
+ // dispatcher loop
|
|
|
+ while(1) {
|
|
|
+ int j;
|
|
|
+ for (j=0; j<NUM_EVENTS; j++) {
|
|
|
+ while (events[j]==EVT_NONE)
|
|
|
+ {}
|
|
|
+ ledTask(events[j]);
|
|
|
+ respondToButtonTask(events[j]);
|
|
|
+ events[j] = EVT_NONE;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/*********** task code, with states ************/
|
|
|
+enum {LED_ON, LED_OFF};
|
|
|
+enum {RTB_IDLE, RTB_ON}; // states
|
|
|
+static uint8 rtbState = RTB_IDLE;
|
|
|
+static uint16 rtbTimerCount = 0;
|
|
|
+const uint16 BUTTON_LED_ON_TIME = 150;
|
|
|
+
|
|
|
+void respondToButtonTask(uint8 evt) {
|
|
|
+ switch(rtbState) {
|
|
|
+ case RTB_IDLE:
|
|
|
+ if (evt == EVT_BUTTON) {
|
|
|
+ rtbState = RTB_ON;
|
|
|
+ rtbTimerCount = 0;
|
|
|
+ gpio_set(LED, LED_ON);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case RTB_ON:
|
|
|
+ if (evt == EVT_TICK) {
|
|
|
+ if (++rtbTimerCount > BUTTON_LED_ON_TIME) {
|
|
|
+ gpio_set(LED, LED_OFF);
|
|
|
+ rtbState = RTB_IDLE;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const uint16 LED_ON_TIME = 150;
|
|
|
+const uint16 LED_OFF_TIME = 50;
|
|
|
+static uint8 ledState = LED_OFF;
|
|
|
+static uint16 ledTimerCount = 0;
|
|
|
+
|
|
|
+void ledTask(uint8 evt) {
|
|
|
+ switch(ledState) {
|
|
|
+ case LED_OFF:
|
|
|
+ if (evt == EVT_TICK) {
|
|
|
+ if (++ledTimerCount > LED_OFF_TIME) {
|
|
|
+ gpio_set(LED2, LED_ON);
|
|
|
+ ledTimerCount = 0;
|
|
|
+ ledState = LED_ON;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case LED_ON:
|
|
|
+ if (evt == EVT_TICK) {
|
|
|
+ if (++ledTimerCount > LED_ON_TIME) {
|
|
|
+ gpio_set(LED2, LED_OFF);
|
|
|
+ ledTimerCount = 0;
|
|
|
+ ledState = LED_OFF;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+```
|
|
|
+
|
|
|
+<details><summary>vector table</summary>
|
|
|
+
|
|
|
+```C
|
|
|
+/* 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
|
|
|
+ _start, // code entry point
|
|
|
+ default_isr, // handle non-maskable interrupts
|
|
|
+ default_isr, // handle hard faults
|
|
|
+ 0,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 */
|
|
|
+};
|
|
|
+```
|
|
|
+</details>
|
|
|
+
|
|
|
+## Substates
|
|
|
+
|
|
|
+For things like flashing LED's, you could create two states,
|
|
|
+and toggle between them, like this:
|
|
|
+
|
|
|
+![](rtb_state_4.png)
|
|
|
+
|
|
|
+<hr>
|
|
|
+
|
|
|
+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:
|
|
|
+
|
|
|
+
|
|
|
+```C
|
|
|
+uint8 rtbState = RTB_IDLE; // major state, RTB_IDLE/RTB_FLASHING
|
|
|
+uint8 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:
|
|
|
+
|
|
|
+![](rtb_state_5.png){.imageright}
|
|
|
+
|
|
|
+```C
|
|
|
+const uint16 FLASH_CYCLE_TIME 150;
|
|
|
+const uint16 FLASH_ON_TIME 40;
|
|
|
+uint8 rtbState = STATE_IDLE;
|
|
|
+void rtbTaskCode(char event) {
|
|
|
+ static uint16 flashCount=0;
|
|
|
+
|
|
|
+ switch(taskState) {
|
|
|
+ case RTB_IDLE:
|
|
|
+ if (event == EVT_BUTTON) {
|
|
|
+ rtbState = STATE_FLASHING;
|
|
|
+ flashCount = FLASH_CYCLE_TIME;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case STATE_FLASHING:
|
|
|
+ // count down flashCount, toggle LED halfway through
|
|
|
+ if (event == EVT_TICK) {
|
|
|
+ if (--flashCount == 0) {
|
|
|
+ setLED(OFF);
|
|
|
+ flashCount = FLASH_CYCLE_TIME;
|
|
|
+ } else if (flashCount == FLASH_ON_TIME) {
|
|
|
+ setLED(ON);
|
|
|
+ }
|
|
|
+ // ... make sure to turn LED off when leaving this state
|
|
|
+ if (event == EVT_BUTTON) {
|
|
|
+ setLED(OFF);
|
|
|
+ rtbState = RTB_IDLE;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+```
|
|
|
+
|
|
|
+## State Machine Initialization
|
|
|
+
|
|
|
+
|
|
|
+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:
|
|
|
+
|
|
|
+```C
|
|
|
+void main(void) {
|
|
|
+ newEvent(EVT_INIT); // early, before interrupts, so we know it's first in line
|
|
|
+ eint();
|
|
|
+ while(1) {
|
|
|
+ ... dispatcher code ...
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Then, in the state code, you can catch that event and set up whatever might
|
|
|
+be required
|
|
|
+
|
|
|
+```C
|
|
|
+uint8 myState;
|
|
|
+void stateCode(uint8 event) {
|
|
|
+ if (event==EVT_INIT) {
|
|
|
+ // ... do setup code
|
|
|
+ myState = FIRST_STATE;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // ... regular state machine code
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## Messages
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+### PostMessage
|
|
|
+
|
|
|
+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.
|
|
|
+
|
|
|
+![cd-eject task uses postMessage() to broadcast a message](postMessage.png)
|
|
|
+
|
|
|
+```C
|
|
|
+void postMessage(uint8 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.
|
|
|
+
|
|
|
+<details><summary>Reality Check</summary>
|
|
|
+In this tiny system, `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.
|
|
|
+</details>
|
|
|
+
|
|
|
+### SendMessage
|
|
|
+
|
|
|
+In some cases we may want the sibling task to process the information immediately.
|
|
|
+This means:
|
|
|
+
|
|
|
+- we can be assured that the sibling task has fully processed the information
|
|
|
+- does not involve the dispatcher; but does require extra room on the stack
|
|
|
+- we *could* get a return value from the sibling task; since it's implemented as
|
|
|
+a subroutine, it's allowed to return a value
|
|
|
+
|
|
|
+![cd-eject task uses sendMessage() to call a sibling task](sendMessage.png)
|
|
|
+
|
|
|
+This of course means that the prototype for task functions would change from
|
|
|
+
|
|
|
+```C
|
|
|
+void taskCode(uint8 event);
|
|
|
+to
|
|
|
+uint8 taskCode(uint8 event);
|
|
|
+```
|
|
|
+
|
|
|
+The service routine to send a message like this would look like:
|
|
|
+
|
|
|
+```C
|
|
|
+uint8 SendMessage(int8 taskPointer(uint8), uint8 message) {
|
|
|
+ return taskPointer(message);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### Usage
|
|
|
+
|
|
|
+Why might one want to send a *message* between tasks?
|
|
|
+
|
|
|
+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](#debouncing)), and have it send 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).
|
|
|
+
|
|
|
+And why `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.
|
|
|
+
|
|
|
+## Ideas for Tasks
|
|
|
+
|
|
|
+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:
|
|
|
+
|
|
|
+- task blue light to show bluetooth connectivity
|
|
|
+- volume up/down sense and display
|
|
|
+- radio channel select buttons
|
|
|
+ - step single freq, or high-speed scan
|
|
|
+- power on/off management
|
|
|
+- remote control of volume/radio-select from the steering wheel (uart link)
|
|
|
+- backlight for night-time viewing (auto sense from a light sensor)
|
|
|
+- service-required LED, flashing
|
|
|
+- a beeper to confirm keypresses (30ms) and alarm (700ms/100ms)
|
|
|
+- a clock up/down set button
|
|
|
+ - 3 buttons: "set min/hr" and "+" and "-"
|
|
|
+
|
|
|
+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)
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+## More State Machines
|
|
|
+
|
|
|
+### Radio Tuner
|
|
|
+
|
|
|
+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:
|
|
|
+
|
|
|
+```C
|
|
|
+enum {EVT_NONE, EVT_TICK, EVT_UP_PRESS, EVT_UP_RELEASE, EVT_DN_PRESS, EVT_DN_RELEASE};
|
|
|
+```
|
|
|
+
|
|
|
+![state machine for radio tuner](radio_tuner.png)
|
|
|
+
|
|
|
+```C
|
|
|
+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};
|
|
|
+uint8 state = IDLE;
|
|
|
+void radioTask(uint8 event) {
|
|
|
+ switch(state) {
|
|
|
+ case IDLE:
|
|
|
+ if (event == EVT_UP_PRESS) {
|
|
|
+ radioFreq += INC_FREQ;
|
|
|
+ checkRadioFreq();
|
|
|
+ radioTimer = radioTimeout;
|
|
|
+ state = FREQ_UP;
|
|
|
+ }
|
|
|
+ if (event == EVT_DN_PRESS) {
|
|
|
+ radioFreq -= INC_FREQ;
|
|
|
+ checkRadioFreq();
|
|
|
+ radioTimer = radioTimeout;
|
|
|
+ state = FREQ_DN;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case FREQ_UP:
|
|
|
+ if (event == EVT_UP_RELEASE)
|
|
|
+ state = IDLE;
|
|
|
+ if (event == EVT_TICK) {
|
|
|
+ if (--radioTimer == 0)
|
|
|
+ state = FREQ_UP_FAST;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ case FREQ_UP_FAST:
|
|
|
+ if (event == EVT_TICK) {
|
|
|
+ radioFreq += INC_FREQ;
|
|
|
+ checkRadioFreq();
|
|
|
+ }
|
|
|
+ if (event == EVT_UP_RELEASE)
|
|
|
+ state = IDLE;
|
|
|
+ break;
|
|
|
+ // and the same for FREQ_DN and FREQ_DN_FAST
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// manage rollover
|
|
|
+void checkRadioFreq() {
|
|
|
+ if (radioFreq > MAX_FREQ)
|
|
|
+ radioFreq = MIN_FREQ;
|
|
|
+ if (radioFreq < MIN_FREQ)
|
|
|
+ radioFreq = MAX_FREQ;
|
|
|
+}
|
|
|
+```
|
|
|
+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.
|
|
|
+
|