How to run your RTOS on a chip

by Wolfgang Engelhard (comments: 0)

It is common knowlege that porting1 a kernel scheduler for a Real-Time Operating System (RTOS) is a quick and easy thing, right? Let's see. With this text, I try to outline the basic steps to get a pre-emptive scheduler running on an embedded device. As hardware, I chose a very common 32bit microcontroller, the Cortex-M3.

ARM and Cortex-M are trademarks of ARM Ltd. and used within this text without special emphasis.

So, before we start, we should think about how our progress can be measured. Then we start off with functionality that does not require inclusion of the RTOS right away. Implementing something unrelated to the RTOS helps familiarize with the microcontroller and accompanying documentation. Then we proceed with steps to integrate the RTOS of choice. And last, we take a look at the compiler and linker.

The following list suggests the order of steps and also represents a checklist.

  1. Environment
  2. Timer
  3. Software interrupt
  4. Stack
  5. Context Switch & Non-interruptible section
  6. Compile & link

Environment

RTOS exist for a wide range of microcontrollers, very small ones like 8bit uC with few kBytes of RAM and ROM to very big 32bit uC with Megabytes of RAM and ROM.

However, there are some fundamentally important features the selected hardware has to support, like program counter manipulation or software interrupt. So make sure your hardware is "fit for the job". If it can do, what is described in this text, you should be good to go.
Knowing what is going on in your hardware is important, you should be able to "debug" your environment, be it a LED, serial interface, debug interface, or JTAG.
Familiarize yourself with the microcontroller, datasheets, and technical reference manuals by starting small. Get a LED blinking! This provides you already with insight into powering busses, interrupt controller and register access. Also, the C-runtime environment and a timer are already up and running as a bonus (you are going to use a timer w/ interrupts?).

Timer

Setting up a timer can vary from easy to difficult. So it usually pays off to look for an implementation out in the web, or to the vendor, who may provide a library to get you up and running fast. The ARM family of microcontrollers provides a dedicated system timer (SysTick) for use with RTOS. That timer triggers an exception when underflowing and reloads with the provided counter value. Since that timer is part of the ARM core, this code works on every Cortex-M microcontroller. Setup of the timer with 1ms period is rather uncomplicated 2

SYST_RVR = (MCU_FREQ / TICK_1MS)   /* TICK_1MS = 1000 */
SYST_CSR = (1 << ENABLE) | (1 << TICKINT) | (1 << CLKSOURCE_MCU)

The only missing piece now is the setup of the interrupt controller, so that the SysTick exception is actually triggered.
Unlike for General Purpose Timers, this is easy for the SysTick timer, you just have to generally enable interrupts. The SysTick is treated as an exception, rather than an interrupt.

CPSIE I (assembler) 
__enable_irq() (c-runtime environment)

Note: __enable_irq() is only available, if you are using the CMSIS3 library. It is also worth checking the compiler manual, because there might be an intrinsic available for enabling/disabling interrupts.

At the end of the day, the following exception handler has to be executed at 1ms interval.

handler {
    rtosTick()
}

Software Interrupt

Finding out, if your hardware supports a software interrupt is a bit more challenging, because this feature is not advertised on the front page. You have to look deep inside the datasheet (or sometimes the third referenced technical manual) where the interrupt controller is described. Sometimes you can/must get creative here, for example by configuring a pin as output and making it trigger an interrupt, when the pin level changes. A write to the pin then triggers the "software interrupt".

The Cortex-M3 does not require such solutions. It provides non-maskable interrupts4, where one of them is a software triggered interrupt, called system service call. In code, triggering is just one assembler instruction __asm("svc 1"); however finding that information is unexpectedly difficult. The ARM Information Center, does not readily tell you this secret, unless you already know it. Only resorting to the "Definitive Guide to the Cortex-M3"5 turned up that piece of information for me, so seeking help in third-party literature is, well, helpful.

At the end of the day, the following exception handler has to be executed.

handler {
    ctxSwitch() /* ctxSwitch is typically an assembler function */
}

Stack

The stack is arguably the most difficult part of getting a RTOS up and running. Not only do you have to know the size of each register (e.g. 16bit/32bit), what registers you need to include in your context (floating point?), but also, if the hardware is already doing part of saving/restoring the context. You also have to look out for modes (Handler/Thread), contexts (interrupt/non-interrupt), or pages.

First a rundown of what elements comprise the stack 6 : - General Purpose Registers - Stack pointer
- Link Register (LR) - Program counter (PC) - Processor Status Register (often also known as Machine State Register) depending on the microcontroller, registers might have a different name and additional registers may be part of a context, like floating point registers.

So how does it look like on a Cortex-M3? The Cortex-M3 is handling part of the context switch and saves/restores the following registers itself on interrupt entry/exit:

PSR
R15 (PC)
R14 (LR)
R12
R3
R2
R1
R0

so, these registers remain

R13 (SP)
R4 - R11

At this point, it is worth knowing that the ARM Cortex family is compliant to the EABI (Embedded Application Binary Interface) specification, AAPCS (ARM Architecture Procedure Calling Standard) to be precise. This tells you, which register is holding the function argument pointer. Now we have enough information to write the stack initialization for the first task to run.

At the end of the day, the stack initialization should look as follows.

StackInit {
    /* stkPtr is task stack end address */
    stkPtr[END-16] = 0x01000000 /* xPSR (Program Status register) */
    stkPtr[END-15] = task /* R15 (PC) = pointer to first task */
    stkPtr[END-14] = 0xFFFFFFFE /* R14 (LR) trigger fault when task returns */
    stkPtr[END-13] = R12    
    stkPtr[END-12] = R3      
    stkPtr[END-11] = R2      
    stkPtr[END-10] = R1      
    stkPtr[END-9] = pArg /* R0 holds argument pointer */
    stkPtr[END-8] = R11
    stkPtr[END-7] = R10
    stkPtr[END-6] = R9 
    stkPtr[END-5] = R8 
    stkPtr[END-4] = R7 
    stkPtr[END-3] = R6 
    stkPtr[END-2] = R5 
    stkPtr[END-1] = R4 
}

The saving and restoring the context while the RTOS is running is discussed in the section Context Switch.

Non-interruptible Sections

Before we continue with the stack, we have to look into atomic operations. Ensuring atomic operations is done with non-interruptible sections or critical sections where interrupts are disabled.
Disabling and enabling of interrupts is usually done on a low level (i.e. assembler).

CPSID I /* Enter uninterruptible section */
CPSIE I /* Exit uninterruptible section */

However, things are not as simple, when you use the Memory Protection Unit (MPU). In this case, you must be more flexible and still allow the Memory Management Fault. This is accomplished by giving the Memory Management Fault the highest interrupt priority and setting BASEPRI register to block all lower interrupt priorities.

Context Switch

Now back to the context switch. At this point, you want to integrate the RTOS and be able to reach the first task. This can be achieved by either writing a dedicated function for the first context switch or using the regular context switch function shown at the end of this chapter with some preparatory steps. Since there is no previous task stack when executing the first context switch, the following registers and variables should be initialized to a memory location that is unused further on:

  1. Process Stack Pointer (PSP)
  2. oldStkPtr

That memory location should simulate a stack frame and therefore have the size of one stack frame.
Furthermore, the link register would jump to the task in Handler mode. Since we want the task to run in Thread mode, we have to modify the link register with this instruction
ORR LR, LR, #0x04

As mentioned before, due to the hardware support, only registers R11 to R4 have to be stored on stack.

At the end of the day, the context switch should look as follows.

CPSID I /* disable interrupt */

MRS R0, PSP /* get stack pointer (process) */
SUBS R0, R0, #0x20 /* reserve 32 bytes on stack (8x 32bit) */
STM R0, {R4-R11} /* store registers */
LDR R1, =oldStkPtr /* get task stack pointer for task to switch from */
STR R0, [R1]  /* and save stack pointer itself */

/* at this point the new task is prepared */
/* how this is done depends on the OS */

LDR R2, =newStkPtr
LDR R0, [R2] /* load stack pointer address */
LDM R0, {R4-R11} /* restore registers */
ADDS R0, R0, #0x20 /* update stack pointer address */
MSR PSP, R0 /* load stack pointer with new address */
ISB
ORR LR, LR, #0x04 /* force use of process stack on return */

CPSIE I /* re-enable interrupts */
BX LR  /* branch to new task and hardware restores missing registers */

Now this only works, when in Handler mode (as opposed to Thread mode while the task is running). Otherwise, access to the registers would be limited. Fortunately, triggering the service call automatically puts us in Handler mode. Additionally, using a service call allows us to re-use the "switching from interrupt" routine for "switching from task".

Compiler & Linker Differences

Although I believe most developers are using open source/freely available compiler (gcc), there are still a lot of commercial compiler vendors on the market, like IAR.
So why do I point out IAR? Because unlike gcc, IAR does handle global variable initialization itself, whereas with gcc, you have to manage linker sections and initialization code yourself7.

Cortex-M Peculiarities

Working with the Cortex-M might pose some interesting challenges. First of all, not everything can be coded in C while doing a port, so learning Thumb assembler basics is necessary. Noteworthy topics are accessing C variables, making assembler routines public and jumping between assembler routines without modifying the link register (but keeping return flexible, still).
As you have seen in the example code of the context switch, working with the stack pointer register is not trivial, as there are a) several of them and b) it depends on the current mode whether Main Stack Pointer (MSP) or PSP is in R13.
Unlike other architectures that have the reset vector at address 0x00000000, the Cortex-M reads the stackpointer from this location. The reset vector occupies the second address (0x00000004). So make sure the vector table is at the right place (and the STVOR register agrees with that location).

Conclusion

As you have seen, there are quite a lot of things to consider, and Murphy's law has plenty of opportunities to apply. Part of this text is based on my experiences porting uC/OS-II from scratch for the first time. Getting to know the Cortex-M and making use of its advantages took quite some time and I'm having an intimate knowledge of the Hard Fault now.

For a basic RTOS kernel port, we did all required steps. Advanced topics would be nested interrupt handling, floating point handling, cache handling and memory protection unit handling.
However, that is it so far from my side.

Do you have experience in porting a RTOS kernel and did you manage to avoid Murphy's law?

Thanks for reading.


  1. the process of implementing hardware specific code to get software to run on a specific hardware ↩︎

  2. http://infocenter.arm.com/help/topic/com.arm.doc.dui0552a/Babieigh.html ↩︎

  3. http://www.arm.com/products/processors/cortex-m/cortex-microcontroller-software-interface-standard.php ↩︎

  4. Cortex-M3 Technical Reference Manual, ARM Ltd. ↩︎

  5. Definitive Guide to the Cortex-M3, Joseph Yiu ↩︎

  6. Cortex-M3 Technical Reference Manual - Processor core register summary, ARM Ltd. ↩︎

  7. http://www.keil.com/support/docs/2821.htm ↩︎

Go back

Add a comment

Update Notification

For an automatic notification on new blog articles, just register your EMail address.

Subscription

We are the Blogger:

Andrea Dorn

After my study of industrial engineering I worked at an engineering service provider. As team leader and sales representative, I was responsible for customers from aviation and mechanical engineering. I am part of the Embedded Office team since 2010. Here I am responsible for the Sales and Marketing activities. I love being outside for hiking, riding or walking no matter the weather.

Fridolin Kolb

I have more than 20 years experience in developing safety critical software as developer and project manager in medical, aerospace and automotive industries. I am always keen on finding a solution for any problem. The statement “This won’t never work”, will never work for me. In my spare time You can find me playing the traverse flute in our local music association, spending time with my family, or in a session as member of our local council and member of the local church council. So obviously I am lacking the ability to say “No” to any challenge ;-).

Michael Hillmann

I have been working for 20 years in safety critical software development. Discussing and solving challenges with customers and colleagues excites me again and again. In my spare time I can be found while hiking, spending time with my family, having a sauna with friends - or simply reading a good book.

Wolfgang Engelhard

I’m a functional safety engineer with over 10 years of experience in software development. I’m most concerned with creating accurate documentation for safety critical software, but lately found joy in destruction of software (meaning I’m testing). Spare time activities range from biking to mentoring a local robotics group of young kids.

Matthias Riegel

Since finishing my master in computer science (focus on Embedded Systems and IT-Security), I’ve been working at Embedded Office. Before that, I worked with databases, and learned many unusual languages (like lisp, clojure, smalltalk, io, prolog, …). In my spare time I’m often on my bike, at the lathe or watching my bees.