A little engineering I: STM32, FreeRTOS

Building a servo stabilizer.

Like many others my first contact with embedded programming were RPis and Arduinos. We got whole boxes full of them and RP2040s at our local Hackerspace. They are inexpensive, easy to use and versatile for home projects. They’re great and you can get much done with them. Shoutouts to the gp2040 project.

But since they are so easy to program and use they also hide a lot of the ugly yet interesting parts, everything has a neat library readily available. So why not take a closer look at one of the “big boy” microntrollers as a friend of mine likes to call them. So I got myself a STM32 devboard.

I’ll admit one of the biggest motivations for me to start this project was to learn some RTOS programming while at it. I chose FreeRTOS, but another popular one seems to be Zephyr which apparently comes with more bells andwhistles.

Getting blinky

First things first, let’s get some code running on the device. Flashing one of the onboard LED’s is kind of the embedded programmers equivalent to ‘Hello world’, or so I’m told.

Setting up the devtools was kind of a headache so I’ll just use the Cube IDE. For now.

Upgrading the debugger to J-Link

First things first, let’s replace the devboards onboard ST debug ST-Lnk with SEGGER’s J-Link firmware instead so we can use Ozone and SystemView.

  1. Ensure the system has a ST-Link USB driver
  2. Install SEGGER’s J-link utilities
  3. Convert ST-Link on the STM32 board to J-Link using SEGGER’s reflash utility
  4. Install Ozone and SystemView

Ozone is software agnostic, which is neat, especially since I’d like to drop STM32CubeIDE later on.

Running our first code

After setting everything up properly I was now able to upload a simple program that flashed an LED once per second. It’s main function looks like this:

int main(void)
{
  uint32_t loopCounter = 0;
  HAL_Init();
  SystemClock_Config();

  MX_GPIO_Init();
  MX_USART3_UART_Init();
  MX_USB_OTG_FS_PCD_Init();

  SEGGER_SYSVIEW_Conf();

  while (1)
  {
    loopCounter++;

    SEGGER_SYSVIEW_PrintfHost("Starting loop-iter: %u\n", loopCounter);

    // Turn on green LED
    HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
    // Wait for 1000ms
    HAL_Delay(1000);

    // Write to SystemView
    SEGGER_SYSVIEW_PrintfHost("Ending loop-iter: %d\n", loopCounter);
  }
}

And with that we got “blinky”.

A simple flashing LED

Running FreeRTOS

How RTOS task programming works

With regular “bare metal” embedded programming, the C code runs directly on the microcontroller. Hardware gets initialized, runs from start to finish (and exits) or enters a loop of some kind where we toggle an LED for example, as above.

With FreeRTOS we can split our program into tasks, which basically work like threads. The FreeRTOS scheduler then switches CPU time between these tasks based on timing and priority instead of us manually coordinating everything inside one big loop, we can write each task as its own loop and use functions like vTaskDelay() to sleep while a different task does its thing.

More LEDs with FreeRTOS

Now that everything is in place we can give our first FreeRTOS program a try. Before doing anything too exciting let’s try again to turn on some LED’s based on FreeRTOS tasks and some kind of input, because if we don’t at least do some scheduling, what even is the point.

void Task1(void *argument)
{
  uint32_t lookBusyIterations;
  uint32_t iterationCount = 0;

  BlueLed.On();

  // Wait until SystemView's record mode is started
  while(SEGGER_SYSVIEW_IsStarted()==0){lookBusy(iterationsPerMilliSecond);}
  SEGGER_SYSVIEW_PrintfHost("Task1: starting\n");

  lookBusyIterations = iterationsPerMilliSecond / 4;

  while(1)
  {
      // Prevent overflow of Systemview
	  iterationCount++;
	  if ((iterationCount % 100) == 1)
	  {
      SEGGER_SYSVIEW_PrintfHost("Task1. Iteration: %u\n", iterationCount);
	  }
      // Do "something" like useful processing etc.
	  lookBusy(lookBusyIterations);
      // delay for 5 SysTicks
	  vTaskDelay(5);
  }
}


void Task2( void* argument )
{
  uint32_t lookBusyIterations;
  uint32_t iterationCount = 0;

  GreenLed.On();
  SEGGER_SYSVIEW_PrintfHost("Task2: starting\n");
  // For Task2, lookBusy() needs to spin for 1/2 of a ms (processor time).
  lookBusyIterations = iterationsPerMilliSecond / 2;


	while(1)
	{
    iterationCount++;
    if ((iterationCount % 100) == 1)
    {
      SEGGER_SYSVIEW_PrintfHost("Task2. Iteration: %u\n", iterationCount);
    }

    lookBusy(lookBusyIterations);

    // Task 1 delayed? Resume Task 2
    // Task 1 not delayed? Run Task 1
    vTaskDelay(1);
	}
}


void Task3( void* argument )
{
  uint32_t lookBusyIterations;
  uint32_t iterationCount = 0;

  RedLed.On();
  SEGGER_SYSVIEW_PrintfHost("Task3: starting\n");

  lookBusyIterations = iterationsPerMilliSecond * 2;

  while(1)
	{
    iterationCount++;
    if ((iterationCount % 100) == 1)
    {
      SEGGER_SYSVIEW_PrintfHost("Task3. Iteration: %u\n", iterationCount);
    }
    // Simulate useful processing. Spin for 2 ms  (processor time).
    lookBusy(lookBusyIterations);
	}
}

This turns on the blue LED when FreeRTOS is ready, waiting for SystemView to record. Once SystemView is running we continue with our tasks, and the other two LEDs will be turned on, as desired.

What’s next

With the boilerplate under control we can start interacting with the world and read some sensor data.