I’ve been working on a new microcontroller project this week and it got me thinking about task schedulers and real time operating systems (RTOSs). I define a microcontroller as a small CPU with a little bit of RAM and FLASH such as a PIC or Cortex-M0. Software designers get carried away when it comes to managing tasks and think that a context switching RTOS makes sense because that’s how it’s done on regular computers (PCs). RTOS’s do make sense in large systems where anyone’s code can be executed and the product designer has no control. But in small microcontroller designs they are overkill.
My first product I ever wrote code for used a round robin task scheduler for an Atmel ATmega128. Back then microcontroller RTOS’s were very expensive and I didn’t know what they were anyway. The lead software designer at the company helped me design the basic software structure before I started and away I went. The design premise was that all non critical code (like RS232 protocols) would be processed in the main loop and that time critical code would execute in interrupts. A system timer interrupt would fire every 40ms and execute the time critical code. Seemed like a simple idea at the time, and it was. But the simplicity also brought a lot of complexity. Because large portions of code would run in an interrupt, all interrupts needed to be reentrant. There was 1 PWM interrupt that was extremely time critical and it was the highest priority. I have no way of knowing, but I bet there were times when interrupts were nested 4 deep. The other big problem was preventing data corruption since any operation could be interrupted at any moment. I remember spending over 6 months testing that firmware to make it work properly. But I didn’t know any better.
My next big microcontroller project was a couple of years later on a Motorola MCORE processor. The lead software developer designed a very simple interrupt scheme for buffer management and a main loop for all the other code. It was amazing how fast we wrote the code to make the product work. I think it was less than 2 weeks, even though it took much longer finish testing and debug it. We got away with a very simple design because we thought the external hardware could take care of it. But we were wrong and eventually we had to hack in some methods of scheduling based on time. That took longer than the initial code!
A couple of projects later and I was helping to write software for a Analog Devices Blackfin processor on a board I had designed. The tools came with a free RTOS like kernel and it was my first real exposure to that kind of design. It worked well and had a whole bunch of features that I never knew I needed until I saw them. The only issue I had was that you had to stick to the RTOS framework for everything.
A few more years later and now I had to write code for a MSP430. The particular chip I was using only had 512 bytes of RAM so I knew that I couldn’t use a context switching RTOS. So I went off and designed a basic task switcher with priorities and events that I later learned was a state machine task scheduler. It was a state machine because large pieces of code were broken down into several smaller chunks (states) that could then cooperatively multitask with the rest of the system. I used this method for years and even developed a couple of different generations of it. But it wasn’t perfect either. I realized that my scheduler made the code incomprehensible after the fact. I learned that the hard way when I had to add features a couple of years later.
My current project using a Cortex-M0 processor. It has much more capability than what I’ve been used to so I figured I might as well revamp my scheduler at the same time. But this time I had a new goal: to make the code readable after the fact. I used “Embedded Multitasking with Small Microcontrollers” by Keith E. Curtis as inspiration. I think that I’m closer to that goal, but only time will tell. The best way to explain my new design is with an example.
Most battery powered devices have several operating modes. Let’s assume that the operating states for this discussion are:
- PowerUp: This state initializes the device from a low power mode.
- Run: For discussion only – this would usually be broken up into smaller tasks.
- Error: An unrecoverable error state.
- PowerDown: Puts the device to sleep.
The main loop would then be:
int main(void) { int32 mode; mode = MODE_POWERUP; while (1) { switch (mode) { case MODE_POWERUP: mode = PowerUp(); break; case MODE_RUN: mode = Run(); break; case MODE_POWERDOWN: mode = PowerDown(); break; default: mode = Error(); } } return 0; }
Each state function is defined in a separate file and their return values are the next state of system. This way each state function can operate independently from each other and pass control to other states as required. Also, because the state functions operate in the main loop there’s no reentrancy problems to deal with. This has to be the simplest scheduler I’ve ever written :).
Each state function can also prioritize how functions are processed. For example:
int32 PowerUp(void) { funcA(); funcB(); funcC(); return MODE_RUN; } int32 Run(void) { funcC(); if (funcD() == 12) { return MODE_ERROR; } if (funcE() == 3) { return MODE_POWERDOWN; } return MODE_RUN; }
Here funcC is shared with PowerUp and Run, but Run doesn’t need funcA or funcB. Run also shows an example of controlling the next state of the device. My MSP430 version had a difficult time with different function priorities and operating modes.
But so far all I’ve done is made a big loop with no time controls. Usually time is controller with a system timer interrupt that decrements all of the timer variables that the interrupt knows about. The good thing about this technique is that all the timer variables are constantly updated in real time. The bad thing is how do you store all the timer data. I’ve tried fixed length arrays and linked lists which work, but they require a bunch of housekeeping functions to make them usable.
This time I’m doing something different. The system timer interrupt just increments a global variable and that’s it. No extra processing to go through all the other timers. No arrays or linked lists. You can have 1 timer or 1000, it doesn’t matter. That’s because the calling function queries the timer instead of the scheduler managing it. This is an unusual method, but it works.
Here’s an example:
int32 PowerUp(void) { initC(); return MODE_RUN; } int32 Run(void) { funcC(); return MODE_RUN; } void initC(void) { startTimer(&GlobalCTimer, 1000); } void funcC(void) { if (setTimer(&GlobalCTimer, 1000) != TIMER_EXPIRED) { return; } ... }
Functions initC and funcC are in their own module and share a static global variable GlobalCTimer. initC sets the timer for 1000 system ticks. funcC then uses setTimer to check if GlobalCTimer has expired and to reload it with 1000 ticks again at the same time. If the timer hasn’t finished then funcC exits early and waits until it can poll the timer again. Polling the timer adds overhead to the system but it’s probably cycles that weren’t being used anyways. The nice thing about polling is that the timers can do almost anything because they are controlled by the function and not an interrupt.
Here’s the code for the timer:
/* * Timers all use this structure. Count is the number of timer ticks * until the timer expires. An expired timer as a count of 0. Sample * is used by the timer functions to determine how many ticks have expired * since the last time the timer was updated. The timer module provides * accessor functions for the structure so that it doesn't (and shouldn't) * be manipulated manually. */ typedef struct { uint32 count; uint32 sample; } timer_t; /* * Start a new timer. This is different from setTimer because this * function will initialize all of the timer variables whereas setTimer * assumes that the timer variables have already been initialized. */ void startTimer(timer_t *timer, /* A pointer to the timer data structure */ uint32 count /* How many ticks should the timer count */ ) { timer->count = count; timer->sample = gSysTime; } /* * Test and reset an existing timer at the same time. This function is meant * to create a periodic timer easily. */ uint32 setTimer(timer_t *timer, /* A pointer to the timer data structure */ uint32 count /* The timer reload value */ ) /* Returns TIMER_EXPIRED when the timer has */ /* expired TIMER_RUNNING */ { uint32 SysTime, elapsed; /* Calculate the elapsed time since the last time the timer was updated. */ SysTime = gSysTime; if (SysTime >= timer->sample) { /* SysTime hasn't rolled over */ elapsed = SysTime - timer->sample; } else { /* SysTime has rolled over */ elapsed = ((uint32) (0xffffffff)) - timer->sample + SysTime; } /* Update the timer */ if (elapsed >= timer->count) { elapsed = elapsed - timer->count; timer->count = 0; } else { timer->count = timer->count - elapsed; elapsed = 0; } timer->sample = SysTime; /* test for an expired timer */ if (timer->count == 0) { if (count > 0) { timer->count = count - elapsed; } return TIMER_EXPIRED; } return TIMER_RUNNING; }
In this example gSysTime is the system tick interrupt time. As you can see, the code is simple and easy to implement. Other RTOS features like semaphores and messaging can be added in a similar manner.
I think that changing from an interrupt based structure to a polled/state machine method helps to simplify software development in microcontrollers because it reduces complexity. An RTOS doesn’t help reduce the software complexity because they usually do context switches with doesn’t simplify the overall software for small projects.