Blog for my various projects, experiments, and learnings

“Bare Metal” STM32 Programming (Part 6): Multitasking With FreeRTOS

In a previous tutorial, we walked through the process of setting up a hardware interrupt to run a function when a button was pressed or a dial was turned. Most chips have a broad range of hardware interrupts, including ones associated with communication and timer peripherals. There is also a basic ‘SysTick’ timer included in most ARM Cortex-M cores for providing a consistent timing without needing to count CPU cycles.

One good use of that evenly-spaced timing is to run a Real-Time Operating System (often called an ‘RTOS’) to process several tasks in parallel. As you add more parts to your projects, it will become awkward to communicate with them all using a simple ‘while’ loop. And while hardware interrupts can help, it’s usually good to do as little as possible inside of an interrupt handler function.

So let’s use FreeRTOS, an MIT-licensed RTOS, to run a couple of tasks at the same time. To demonstrate the concept, we’ll run two ‘toggle LED’ tasks with different delay timings to blink an LED in an irregular pattern.

Example LED Timing

Example LED timing with two ‘toggle’ tasks delaying for different amounts of time.

This example will also add support for some faster microcontrollers; the STM32F103C8 and STM32F303K8, which are Cortex-M3 and -M4F cores respectively. The F103C8 is available on cheap ‘blue pill‘ and ‘black pill‘ boards, and ST sells a ‘Nucleo’ board with the F303K8. Both of those chips can run at up to 72MHz, and they can also get more done per cycle since they have larger instruction sets.  And as usual, there is example code demonstrating these concepts on Github.

FreeRTOS: How’s That Work?

Good question! In my limited understanding, FreeRTOS is essentially a task scheduler with some built-in helper structures such as message queues which let you safely communicate data between processes. It also seems to have an ecosystem of functionality like filesystems and networking that has grown around it. It’s a way to run multiple threads on an embedded device while retaining the ability to react quickly to important realtime signals.

In a nutshell, you create a number of ‘Tasks’, each with its own priority and function to run. When a Task is active, it will start running its function wherever it last left off. If a higher-priority Task becomes available, FreeRTOS will interrupt the lower-priority task and jump to the higher-priority one. Tasks can also call FreeRTOS functions such as ‘vTaskYield’ and ‘vTaskDelay’ to relinquish their active state for a period of time and allow lower-priority Tasks to run.

That simplicity is nice, but you also have to be a bit careful. If you don’t yield or delay in high-priority tasks, you can run into a situation where FreeRTOS only runs its highest-priority tasks and completely starves the lower-priority ones of CPU time.

But in this simple example, we will only have two tasks with equal priority and they will both call ‘vTaskDelay’, so that sort of scheduling conflict won’t be an issue. In fact, the CPU will sit in an ‘idle’ state most of the time.

Project Structure

Before we can get back to the exciting world of blinking LEDs, we need to set up the core structure of our FreeRTOS application. It will look very similar to the past examples; there’s a vector table, a simple boot script to copy/clear the .data/.bss memory sections, a linker script, some device-specific header files provided by ST, and finally some C code with the program’s “main” method and application logic.

Fortunately, it is easy to add FreeRTOS to a project like this. We just need to download the source code, and include a few C source and header files in our build. Some files, such as port.c, will define functions which are ‘portable’ across different types of chips, and FreeRTOS ships with such files for a variety of platforms including the ARM Cortex-M cores used by our STM32 chips.

We’ll also need to provide a FreeRTOSConfig.h file to define values specific to our project, like how fast the core clock speed will be and which FreeRTOS functions to actually include in the build. Instructions on creating a configuration file can be found here, or you can look at the example one in this tutorial’s Github repository.

Instructions for downloading the FreeRTOS kernel can be found on the FreeRTOS website’s download page. The most recent version (10.0.1 as of the time of writing) is available under the MIT license, which is slightly more permissive than the license that shipped with previous versions.

Once you extract the archive, you will see a few different directories. This may vary slightly depending on where you downloaded it from, but this example project will only use the core kernel files – they’re probably located in either FreeRTOSv10.0.1/FreeRTOS/Source/ or lib/FreeRTOS/. Be aware that most of the code in the Source/portable directory is not applicable to this STM32 project; we only need the GCC/ARM_CM[x] and MemMang directories for the platform-specific stuff and memory management, respectively. You can see this reflected in the portable/ files included in this tutorial’s Github repository.

Once you have created a freertos directory in your project and copied those files over, we can finally start writing some code. You’ll also need to add the directories and C files used by the project to your project’s Makefile, but this isn’t a Make tutorial and you can refer to the Github repository for those details.

Minimal FreeRTOS Application

The first thing we need to do is something which took me a few days to figure out. It looks like FreeRTOS sets up some core ARM interrupts such as the ‘SysTick’ mentioned earlier, but it does not know what those interrupts will be called in a given application. In these example projects, we have been assigning each entry in the vector table to a default ‘infinite loop’ method by default. If you run a FreeRTOS application without shimming those interrupts call their corresponding FreeRTOS methods, it looks like the application will quickly jump to that ‘infinite loop’ handler and, you know, loop infinitely.

To get around that, we can add some macros to our FreeRTOSConfig.h file to make sure they get included by the FreeRTOS files. If you’re using a different vector table, your interrupt handlers (SVC_handler, etc) may have different names:

/* Redirect FreeRTOS port interrupts. */
#define vPortSVCHandler     SVC_handler
#define xPortPendSVHandler  pending_SV_handler
#define xPortSysTickHandler SysTick_handler

There are a lot of other settings in the example FreeRTOSConfig.h file in this example’s Github repository, and explanations of each one can be found here. Most of this project’s example config file was just copied from that page.

The next step is to declare some functions for our FreeRTOS tasks to run. A Task’s function should have the following signature:

static void your_task_function(void* args) { ... }

So for this example project which blinks an LED in an irregular pattern, we can define a task which toggles the LED’s GPIO pin before delaying for an amount of time that depends on the parameters passed in:

/**
 * 'Blink LED' task.
 */
static void led_task(void *args) {
  int delay_ms = *(int*)args;

  while (1) {
    // Toggle the LED.
    LED_BANK->ODR ^= (1 << LED_PIN);
    // Delay for a second-ish.
    vTaskDelay(pdMS_TO_TICKS(delay_ms));
  };
}

In this task, we assume that the ‘args’ parameter points to an integer. The pdMS_TO_TICKS macro is provided by FreeRTOS, and it calculates how cycles to wait based on the clock speed set in our FreeRTOSConfig.h file. And like I mentioned earlier, the vTaskDelay method yields for other tasks until the requested amount of time has elapsed.

To create the tasks, we can simply make a call to xTaskCreate. You can check the documentation for that function here, but in a nutshell we pass in a function to run, a pointer to its parameters, a name, a priority, and how many ‘words’ of memory FreeRTOS should allocate for the Task:

// (Defined somewhere)
static const int led_delay_1 = 1111;
static const int led_delay_2 = 789;

int main(void) {
  // (Setup clocks, GPIO pins, etc)
  // (...)

  // Create the LED task.
  xTaskCreate(led_task, "LED_blink_1", 128, (void*)&led_delay_1,
              configMAX_PRIORITIES-1, NULL);
  xTaskCreate(led_task, "LED_blink_2", 128, (void*)&led_delay_2,
              configMAX_PRIORITIES-1, NULL);

The NULL argument is where you can pass in a pointer that you want to store the created Task in for referring to later. We won’t need a reference to our tasks for this example, though.

With our Tasks created, the only thing left to do is start the FreeRTOS scheduler with the vTaskStartScheduler() function. This function should never return; when we call it, we are handing control of the program’s flow over to the FreeRTOS scheduler, which will prioritize and run the Tasks that we have created.

  // Start the scheduler.
  vTaskStartScheduler();

  // This should never be reached; the FreeRTOS scheduler
  // should be in charge of the program's execution after starting.
  while (1) {}
  return 0;
}

You can also create and delete Tasks during a program’s execution to modify its behavior, even after starting the scheduler. But for this simple example, you can just build the project and flash it to your board the same way as in previous tutorials; make followed by st-flash write main.bin 0x08000000.

Your LED should start to blink with irregular timing, as the two parallel tasks toggle it at different time intervals.

Conclusions

That’s about all there is to it; creating an application with FreeRTOS is a fairly simple task once you understand the basics and get the interrupts set up properly. If your project seems to do nothing and your debugger shows that it jumps to a default blocking interrupt handler, try double-checking the shims that redirect the SysTick, SVC, and pending_SV handlers to their port.c counterparts.

FreeRTOS looks like a promising way to handle communication between a microcontroller and several external components at once, so I’m looking forwards to learning more about using it in more complex applications.

And of course, these views and opinions are solely my own and not my employer’s, so please don’t skewer me if I got something wrong.

Leave a Reply

Your email address will not be published. Required fields are marked *