Doing 3 Things at Once, or How to Implement a Software Timer? | STM32 on Registers #5

Let’s get to know one of the effective ways not to block the processor’s work by waiting. It will be a Software Timer. This is an absolute foundation for an embedded programmer. A real ABC.

You can often come across the term multitasking. For example, videos explaining the same mechanism on Arduino are often called multitasking on Arduino. This is both true and false at the same time.

👉 False, because on a processor, more than one task, one instruction cannot happen at the same time. We’re not running two or more tasks simultaneously here.

👉 So why is it true and works? It’s about the illusion of multitasking. Tasks are executed alternately and at such a fast pace that as humans we have the impression of simultaneity in their execution. That’s the whole magic.

The art now is to distribute these tasks over time on the processor in such a way that the whole big system gives the impression that all its elements are working simultaneously and correctly. In this series we won’t be building a big system. Today I’ll show you an example of multitasking and non-blocking code using a few LEDs as an example.

👉 Do you want to learn STM32 register-level programming in full? I invite you to my full online course (conducted in Polish) on this topic: https://stm32narejestrach.pl

The STM32 on Registers Series on YouTube

These posts are created in parallel with the series on my YouTube on the same topic. If you prefer the video version, I invite you there. These articles are a summary of what I show on YouTube.

Link to the entire YouTube playlist

What is a Software Timer?

We need to learn something called a Software Timer.

A Software Timer is a programming technique based on checking the elapsed time and unblocking an action if enough of it has passed. It probably doesn’t tell us that much…

Imagine the timeline of our program. If we take zero as the starting point, and we want to perform an action every 500 milliseconds, then at what time values do we have to perform the action?

500, 1000, 1500, 2000, 2500, etc. At those moments and nowhere in between.

If the clock that measures time indicates something else, then we do nothing (or other actions scheduled at a different time). That’s the ideal world. In our non-ideal world, the moment when the running time equals the action time will “unblock” the action to be performed, and we’ll do it at the nearest possible opportunity.

What will be our clock? We already configured it and used it in previous lessons. The SysTick Timer. It continuously measures our program runtime. In the background it counts every 1 millisecond how much time has already passed. It’s no coincidence it’s called SysTick – because it’s used to count “system time”, in other words – the time base.

So it’s enough to check how much time has passed and compare it with the moments when we want to trigger the action. How do we do that?

From the current global time marked in some way, let’s say with a “marker”, we wait a defined time interval. For example, those 500 ms from the beginning of the paragraph. If the action execution time “hits”, we execute the action and move our “marker” forward. And so on in a loop.

Still not clear? It will be clearer with the algorithm and code 🙂

Operating algorithm of a Software Timer

The general operating algorithm can be as follows:

  1. We create a variable to remember the current time – the “marker”.
  2. We “feed” the marker with the current time (the system time, because it is our reference).
  3. We repeat in a loop:
    1. We check whether the specified interval of time has elapsed
    2. If yes:
      1. We execute the planned action
      2. We overwrite the marker with the current time*

* there are several possibilities and moments for how to reload the marker with time. We can do it:

1) Before the action / After the action. Then we decide from which moment we count this “fixed” time interval

2) Reload with the current time / add a constant value. We decide whether the action execution time is included in the waiting time for the next action. A danger here may be that the action execution time will be longer than the interval between subsequent actions.

My favorite method is to reload with the current time before performing the action, and that’s what we’ll do. So far, this method has worked best for me. Maybe another one will suit you? Try it and see. There are no obligations with me 😉

Software Timer Code

The theory is behind us. So we must move on to practice. Our exercise will be blinking multiple LEDs at different frequencies.

More LEDs = more pin configuration. Luckily, we can already do that. Let’s connect the LEDs to pins adjacent to the LED on the NUCLEO-C031. LD4, which we blinked, was on PA5. So let’s connect LD5 to PA6 and LD6 to PA7. They are next to each other on the Arduino Uno header. Let’s make sure in the schematic that they aren’t connected to something else.

As we can see, these are free pins. They are simply routed to the Arduino header, so we will use them. Connect the microcontroller pins to a resistor of about 330 ohms, and the resistor to the LED cathode. The anode to 3.3V on the Nucleo. The exact schematic is as follows.

Now the code. First, we can copy the functions for LD4 and adjust them.

// LEDs control macros
#define LD4_ON GPIOA->BSRR = GPIO_BSRR_BS5
#define LD4_OFF GPIOA->BSRR = GPIO_BSRR_BR5
#define LD4_TOGGLE GPIOA->ODR ^= GPIO_ODR_OD5

#define LD5_ON GPIOA->BSRR = GPIO_BSRR_BS6
#define LD5_OFF GPIOA->BSRR = GPIO_BSRR_BR6
#define LD5_TOGGLE GPIOA->ODR ^= GPIO_ODR_OD6

#define LD6_ON GPIOA->BSRR = GPIO_BSRR_BS7
#define LD6_OFF GPIOA->BSRR = GPIO_BSRR_BR7
#define LD6_TOGGLE GPIOA->ODR ^= GPIO_ODR_OD7

// LEDs Configuration
void ConfigureLD4(void);
void ConfigureLD5(void);
void ConfigureLD6(void);

void ConfigureLD4(void)
{
	// Enable Clock for PORTA
	RCC->IOPENR |= RCC_IOPENR_GPIOAEN;

	// Configure GPIO Mode - Output
	GPIOA->MODER |= GPIO_MODER_MODE5_0;
	GPIOA->MODER &= ~(GPIO_MODER_MODE5_1);

	// Configure Output Mode - Push-pull
	GPIOA->OTYPER &= ~(GPIO_OTYPER_OT5);

	// Configure GPIO Speed - Low
	GPIOA->OSPEEDR &= ~(GPIO_OSPEEDR_OSPEED5);

	// Configure Pull-up/Pull-down - no PU/PD
	GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPD5);
}

void ConfigureLD5(void) // PA6
{
	// Enable Clock for PORTA
	RCC->IOPENR |= RCC_IOPENR_GPIOAEN;

	// Configure GPIO Mode - Output
	GPIOA->MODER |= GPIO_MODER_MODE6_0;
	GPIOA->MODER &= ~(GPIO_MODER_MODE6_1);

	// Configure Output Mode - Push-pull
	GPIOA->OTYPER &= ~(GPIO_OTYPER_OT6);

	// Configure GPIO Speed - Low
	GPIOA->OSPEEDR &= ~(GPIO_OSPEEDR_OSPEED6);

	// Configure Pull-up/Pull-down - no PU/PD
	GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPD6);
}

void ConfigureLD6(void) // PA7
{
	// Enable Clock for PORTA
	RCC->IOPENR |= RCC_IOPENR_GPIOAEN;

	// Configure GPIO Mode - Output
	GPIOA->MODER |= GPIO_MODER_MODE7_0;
	GPIOA->MODER &= ~(GPIO_MODER_MODE7_1);

	// Configure Output Mode - Push-pull
	GPIOA->OTYPER &= ~(GPIO_OTYPER_OT7);

	// Configure GPIO Speed - Low
	GPIOA->OSPEEDR &= ~(GPIO_OSPEEDR_OSPEED7);

	// Configure Pull-up/Pull-down - no PU/PD
	GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPD7);
}

Returning the current time

I will call our actions Tasks. Each Task should execute at a different interval. So each task will treat time differently. We need 3 variables (“markers”) so that each task can remember its reference point.

The timer variable type should be the same as the type of the variable counting the system time.

// Software Timers variables
uint32_t Timer_LD4;
uint32_t Timer_LD5;
uint32_t Timer_LD6;

We should load the current time into these variables. But where do we get it from? We could simply assign the Tick variable from the SysTick Timer, but THAT’S NOT HOW IT’S DONE!

In the future we will split our code into files, so let’s start preparing for that. We need a function that returns this time to us. We can’t be thinking each time about how we obtain the global time. Because what if we change the method of obtaining it? Will we rummage through the whole program and change every single line? No. It has to be wrapped in a convenient function.

// Tick for System Time
__IO uint32_t Tick;

uint32_t GetSystemTick(void)
{
	return Tick; // Return current System Time
}

With such a function returning the system time, it will be much easier and correct.

We can use it to feed the initial state of our timer variables. Let’s do it right after configuring the LEDs.

	// Configure LEDs
	ConfigureLD4();
	ConfigureLD5();
	ConfigureLD6();

	// Software Timers - first fill
	Timer_LD4 = GetSystemTick();
	Timer_LD5 = GetSystemTick();
	Timer_LD6 = GetSystemTick();

We have the first marker placement. Let’s first do one task on the LD4 LED we already know. In a blocking way, using Delay, we changed its state every 500 ms. Let’s now do it non-blocking.

First of all, a convenient definition of this waiting time would be useful.

// Constants for Software Timer's actions
#define LD4_TIMER 500

Having this time, we can finally write the code for the task triggering algorithm.

  1. We need to check whether the specified amount of time has passed. We compare the difference between the current time and the “marked” one. If this difference is greater than the required time between actions, we start calling the action.
  2. We can immediately reload the timer, i.e., mark the next reference point on the timeline from which we want the required time to pass before performing our action again. Placing the reload at this point gives us that the start of the action will always be every specified time regardless of how long the task takes to execute.

    Tasks can be different and take different amounts of time depending on current conditions. Such placement of the reload at the beginning is a very good choice.



  3. What to reload with? As I said – I prefer the current time.

Once we have all the matters around the timer operation sorted out, we can finally move on to executing our action. We are blinking an LED, but it can be any action. Refreshing a screen, reading a temperature, polling some chip for a measurement. Basically anything we need to do cyclically.

The code performing these actions might look like this, for example:

		// LD4
		if((GetSystemTick() - Timer_LD4) > LD4_TIMER) // Check if is time to make  action
		{
			Timer_LD4 = GetSystemTick(); // Refill action's timer
			LD4_TOGGLE; // ACTION!
		}

Simple, right? This mechanism is incredibly simple, and so useful!

Other LEDs

We do our remaining LED actions analogously, changing only the time between calls.

We can confidently copy the fragments for the first LED. We just have to remember to change the pins and definitions that relate to them. After all, these are different pins and we want to blink at a different frequency.

The complete code of our program will look as follows:

/**
 ******************************************************************************
 * @file           : main.c
 * @author         : Mateusz Salamon
 * @brief          : STM32 na Rejestrach
 ******************************************************************************
 ******************************************************************************

 	 	 	 	 https://msalamon.pl
 	 	 	 	 https://sklep.msalamon.pl
 	 	 	 	 https://kursstm32.pl
 	 	 	 	 https://stm32narejestrach.pl

 */
#include "main.h"

// LEDs control macros
#define LD4_ON GPIOA->BSRR = GPIO_BSRR_BS5
#define LD4_OFF GPIOA->BSRR = GPIO_BSRR_BR5
#define LD4_TOGGLE GPIOA->ODR ^= GPIO_ODR_OD5

#define LD5_ON GPIOA->BSRR = GPIO_BSRR_BS6
#define LD5_OFF GPIOA->BSRR = GPIO_BSRR_BR6
#define LD5_TOGGLE GPIOA->ODR ^= GPIO_ODR_OD6

#define LD6_ON GPIOA->BSRR = GPIO_BSRR_BS7
#define LD6_OFF GPIOA->BSRR = GPIO_BSRR_BR7
#define LD6_TOGGLE GPIOA->ODR ^= GPIO_ODR_OD7

// Constants for Software Timer's actions
#define LD4_TIMER 500
#define LD5_TIMER 222
#define LD6_TIMER 147

// Tick for System Time
__IO uint32_t Tick;

// LEDs Configuration
void ConfigureLD4(void);
void ConfigureLD5(void);
void ConfigureLD6(void);

// Get current System Time
uint32_t GetSystemTick(void);

// Software Timers variables
uint32_t Timer_LD4;
uint32_t Timer_LD5;
uint32_t Timer_LD6;

int main(void)
{

	// 1s = 12 000 000
	// 0,001 = X
	SysTick_Config(12000000 / 1000);

	// Configure LEDs
	ConfigureLD4();
	ConfigureLD5();
	ConfigureLD6();

	// Software Timers - first fill
	Timer_LD4 = GetSystemTick();
	Timer_LD5 = GetSystemTick();
	Timer_LD6 = GetSystemTick();

    /* Loop forever */
	while(1)
	{
		// LD4
		if((GetSystemTick() - Timer_LD4) > LD4_TIMER) // Check if is time to make  action
		{
			Timer_LD4 = GetSystemTick(); // Refill action's timer
			LD4_TOGGLE; // ACTION!
		}

		// LD5
		if((GetSystemTick() - Timer_LD5) > LD5_TIMER)
		{
			Timer_LD5 = GetSystemTick();
			LD5_TOGGLE;
		}

		// LD6
		if((GetSystemTick() - Timer_LD6) > LD6_TIMER)
		{
			Timer_LD6 = GetSystemTick();
			LD6_TOGGLE;
		}


	}
}

void ConfigureLD4(void)
{
	// Enable Clock for PORTA
	RCC->IOPENR |= RCC_IOPENR_GPIOAEN;

	// Configure GPIO Mode - Output
	GPIOA->MODER |= GPIO_MODER_MODE5_0;
	GPIOA->MODER &= ~(GPIO_MODER_MODE5_1);

	// Configure Output Mode - Push-pull
	GPIOA->OTYPER &= ~(GPIO_OTYPER_OT5);

	// Configure GPIO Speed - Low
	GPIOA->OSPEEDR &= ~(GPIO_OSPEEDR_OSPEED5);

	// Configure Pull-up/Pull-down - no PU/PD
	GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPD5);
}

void ConfigureLD5(void) // PA6
{
	// Enable Clock for PORTA
	RCC->IOPENR |= RCC_IOPENR_GPIOAEN;

	// Configure GPIO Mode - Output
	GPIOA->MODER |= GPIO_MODER_MODE6_0;
	GPIOA->MODER &= ~(GPIO_MODER_MODE6_1);

	// Configure Output Mode - Push-pull
	GPIOA->OTYPER &= ~(GPIO_OTYPER_OT6);

	// Configure GPIO Speed - Low
	GPIOA->OSPEEDR &= ~(GPIO_OSPEEDR_OSPEED6);

	// Configure Pull-up/Pull-down - no PU/PD
	GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPD6);
}

void ConfigureLD6(void) // PA7
{
	// Enable Clock for PORTA
	RCC->IOPENR |= RCC_IOPENR_GPIOAEN;

	// Configure GPIO Mode - Output
	GPIOA->MODER |= GPIO_MODER_MODE7_0;
	GPIOA->MODER &= ~(GPIO_MODER_MODE7_1);

	// Configure Output Mode - Push-pull
	GPIOA->OTYPER &= ~(GPIO_OTYPER_OT7);

	// Configure GPIO Speed - Low
	GPIOA->OSPEEDR &= ~(GPIO_OSPEEDR_OSPEED7);

	// Configure Pull-up/Pull-down - no PU/PD
	GPIOA->PUPDR &= ~(GPIO_PUPDR_PUPD7);
}

void SysTick_Handler(void)
{
	Tick++; // Increase system timer
}

uint32_t GetSystemTick(void)
{
	return Tick; // Return current System Time
}



If we have everything copied, we can run the program and see if it works. Of course, you already know how to do that 🙂

Summary

A Software Timer is a great way to implement tasks in an embedded system without blocking the processor. It works by checking elapsed time and triggering actions at the right moments. Thanks to this, we can create the illusion of multitasking and ensure smooth operation of many elements of the system.

The basis of operation is the SysTick Timer, which counts time in milliseconds in the background. It is enough to store a time “marker”, compare it with the current system time, and trigger an action at the right moments, e.g., blinking an LED. The key is proper reloading of the time marker, which helps avoid errors related to long task execution time.

Thanks to this method, we can independently control multiple LEDs with different blink frequencies, without the need to use blocking delay functions. This mechanism is universal and can be used to control any cyclic tasks – sensor readings, communication handling, or display refreshing.

In the next post we will deal with another element of practical register-level programming. It will be the first communication with the outside world. We will touch the UART interface 🙂

Let me know in the comments if you liked this post! Maybe you have a suggestion of what to show as part of the STM32 on Registers series? Share this article with your friends.

I also invite you to my store, where you can buy interesting modules for experiments, including NUCLEO-C031C6, which we use in this series:
🔗 https://sklep.msalamon.pl

You can find the project from this article at:
📂 https://github.com/lamik/stm32narejestrach_5

Podobne artykuły

.
Categories: STM32

0 Comments

Leave a Reply

Avatar placeholder

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