Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 9

Configuring SysTick for a 1ms Time Base

Recommended resources

Hello! Welcome back to our course.

In the last lesson, we established a stable and precise 72 MHz system clock for our Blue Pill board. This was a critical step, as this "heartbeat" provides the timing reference for every operation the microcontroller performs.

Today, we'll leverage that stable clock to build another fundamental component of any embedded system: a reliable time base. We will focus on a special peripheral that's standard across all ARM Cortex-M processors: the System Timer, or SysTick.

This lesson directly addresses the learning outcome: Configure the SysTick timer to generate a precise 1ms time base for delay functions. By the end of this session, you will have created your own delay_ms() function, a tool you will use in nearly every future project.

1. The Role of the SysTick Timer

Before we dive into registers, it's important to understand what SysTick is and why it's so useful. Unlike timers that are specific to STMicroelectronics (like TIM1, TIM2, etc.), SysTick is part of the ARM Cortex-M processor core itself. This makes code that uses SysTick highly portable across different manufacturers' MCUs.

Its primary purpose is to generate an interrupt at a regular, programmable interval. This makes it perfect for:

  • Creating simple delay functions.
  • Implementing periodic polling of sensors or inputs.
  • Providing the "heartbeat" or "tick" for a Real-Time Operating System (RTOS) scheduler, a topic we will cover later.

Lecture 12: System Timer (SysTick)

To start, watch this introductory segment from a lecture by Embedded Systems and Deep Learning. It gives a concise overview of the SysTick timer and its common applications.

Watch from the beginning until 01:21. Focus on understanding the purpose of the SysTick timer as a source of periodic interrupts.


2. How SysTick Works: Registers and Operation

At its core, the SysTick timer is a simple 24-bit down-counter. It operates in a very predictable way:

  1. A "reload" value is loaded into the SysTick_LOAD register.
  2. The timer is enabled.
  3. On each clock cycle, the counter in the SysTick_VAL register decrements by one.
  4. When the counter reaches 0, two things happen:
    • It triggers an interrupt (if enabled).
    • On the next clock cycle, it automatically reloads the value from SysTick_LOAD and starts counting down again.

This means that the total number of clock cycles for one full period is RELOAD + 1. This "+1" is a common source of off-by-one errors, so it's important to remember.

The timer's behavior is controlled by just a few bits in the SysTick_CTRL (Control and Status) register.

SysTick Timer Block Diagram
This block diagram illustrates the SysTick timer's architecture. You can see the clock source selection, the 24-bit down-counter, the reload register (`SysTick_LOAD`), and the control logic that generates the interrupt.

Lecture 12: System Timer (SysTick)

The same lecture provides an excellent, detailed walkthrough of the SysTick registers and the down-counting mechanism. This will solidify your understanding of how it operates.

Watch the following segments: 01:36 - 02:56: Explains the 24-bit down-counter and the RELOAD + 1 timing formula. 02:56 - 05:06: Details the SysTick_CTRL register, covering the ENABLE, TICKINT (interrupt enable), and CLKSOURCE bits. 05:06 - 06:50: Describes the SysTick_LOAD and SysTick_VAL registers.


3. Calculating the Reload Value for a 1ms Tick

Our goal is to make the SysTick interrupt fire every 1 millisecond. To do this, we need to calculate the correct value to put in the SysTick_LOAD register.

The formula is:

This is more easily expressed using frequency:

In our previous lesson, we configured the system clock (HCLK) to 72 MHz. The SysTick timer runs on this clock. Therefore:

  • Desired Time Interval = 1 ms = 0.001 s
  • Clock Frequency = 72,000,000 Hz

Plugging these into the formula:

So, 71,999 is the magic number we need to load into the SysTick_LOAD register to get a perfect 1ms tick.


4. Practical Implementation with CMSIS

While we could write to the registers directly, the ARM Cortex Microcontroller Software Interface Standard (CMSIS) provides a convenient function that does all the work for us: SysTick_Config(). This function is the standard, professional way to set up the SysTick timer.

The function takes one argument: the number of clock ticks between interrupts. Based on our calculation, this is 72,000.

A common way to write this is:
SysTick_Config(SystemCoreClock / 1000);

Here, SystemCoreClock is a global variable provided by the STM32 HAL/CMSIS libraries that automatically holds the current system clock frequency (72,000,000 for us). Dividing by 1000 gives us the required 72,000 ticks for a 1ms interval. The SysTick_Config function handles the "-1" part internally.

Now, let's look at the complete implementation, which involves three parts:

  1. Configuration: Calling SysTick_Config().
  2. Interrupt Handler: A function named SysTick_Handler() that will be executed automatically every 1ms.
  3. Delay Function: A function that uses the handler's output to create a blocking delay.

STM32F103 blue pill interrupts from scratch

This Stack Overflow answer provides the exact C code needed to implement a 1ms tick. It shows the SysTick_Config call and the handler, and even provides the source code for the SysTick_Config function itself, demystifying the abstraction.

Study the first code block showing the msTicks variable, the SysTick_Handler, and the call to SysTick_Config. Then, look at the second block which shows the implementation of SysTick_Config. Notice how it sets the LOAD, VAL, and CTRL registers just as we discussed.


5. Hands-On Task: Building delay_ms()

It's time to put this into practice. We will modify your LED blinking project from Module 1 to use a precise delay based on the SysTick timer.

Step 1: Declare a Global Tick Counter

At the top of your main.c file, declare a global volatile variable. This variable will be incremented by our interrupt handler.

/* Private variables ---------------------------------------------------------*/
volatile uint32_t g_tick_count = 0;

The volatile keyword is essential. It tells the compiler that the value of g_tick_count can change at any time, preventing it from making optimizations that could break our delay logic.

Step 2: Implement the SysTick_Handler

This is the Interrupt Service Routine (ISR). The name SysTick_Handler is special; the hardware is designed to automatically call this function when a SysTick interrupt occurs. Add this function to your main.c.

void SysTick_Handler(void)
{
  g_tick_count++;
}

Step 3: Configure SysTick in main()

Inside your main() function, immediately after the call to SystemClock_Config(), add the call to configure the SysTick timer.

int main(void)
{
  /* MCU Configuration--------------------------------------------------------*/
  HAL_Init(); // This may already be there
  SystemClock_Config();

  /* NEW: Configure the SysTick to have a 1ms interrupt */
  SysTick_Config(SystemCoreClock / 1000);

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  // ... rest of your code
}

Step 4: Create the delay_ms() Function

Now, create a function that performs a blocking wait for a specified number of milliseconds.

void delay_ms(uint32_t delay)
{
  uint32_t start_tick = g_tick_count;
  while ((g_tick_count - start_tick) < delay)
  {
    // Wait
  }
}

This function works by capturing the current tick count and then waiting in a loop until the desired number of ticks have passed. The subtraction g_tick_count - start_tick correctly handles the case where g_tick_count wraps around from its maximum value back to zero, thanks to the properties of unsigned integer arithmetic.

Step 5: Use Your New Delay Function

Finally, modify the while(1) loop in main() to use your new, precise delay function.

while (1)
{
  HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
  delay_ms(500); // Wait for 500 milliseconds
}

Compile, flash, and run your code. Your Blue Pill's onboard LED should now be blinking with a 1-second period (500ms on, 500ms off), far more accurately than with a simple for loop.


Conclusion

Excellent work. You have configured one of the most fundamental components of an embedded system. The creation of a stable, 1ms time base is a cornerstone of firmware development.

Key Takeaways:

  • SysTick is a standard 24-bit down-counter in the ARM Cortex-M core, used for creating a system-wide time base.
  • The interrupt period is determined by (Reload Value + 1) * Clock Period.
  • For a 1ms tick with a 72MHz clock, the reload value is 71,999, which corresponds to 72,000 ticks.
  • The CMSIS function SysTick_Config(SystemCoreClock / 1000) is the standard way to configure a 1ms tick.
  • An Interrupt Service Routine (SysTick_Handler) is used to increment a global volatile tick counter.
  • This tick counter can then be used to build precise, blocking delay functions.

Preview of the Next Lesson:
We now have reliable control over output (LEDs) and timing (delays). The next logical step is to handle input. In the next lesson, we will learn how to configure a GPIO pin as an input to read the state of a push-button. We will also confront a common real-world hardware problem: "contact bounce," and use our new delay_ms function to implement a software debouncing algorithm.