HAL (Hardware Abstraction Layer) Functions Reference Guide

Table of contents
  1. HAL (Hardware Abstraction Layer) Functions Reference Guide
    1. What is HAL?
    2. Initialization Functions
      1. HAL_Init()
      2. SystemClock_Config()
      3. MX_GPIO_Init()
      4. MX_USART2_UART_Init()
    3. GPIO (General Purpose Input/Output) Functions
      1. HAL_GPIO_WritePin()
      2. HAL_GPIO_ReadPin()
      3. HAL_GPIO_TogglePin()
    4. Timing Functions
      1. HAL_Delay()
      2. HAL_GetTick()
    5. UART (Serial Communication) Functions
      1. HAL_UART_Transmit()
    6. ADC (Analog-to-Digital Converter) Functions
      1. HAL_ADC_Start()
      2. HAL_ADC_PollForConversion()
      3. HAL_ADC_GetValue()
      4. HAL_ADC_Stop()
      5. MX_ADC1_Init()
    7. RNG (Random Number Generator) Functions
      1. HAL_RNG_Init()
      2. HAL_RNG_GenerateRandomNumber()
      3. MX_RNG_Init()
    8. Expanded System Clock Configuration
      1. HAL_RCC_OscConfig()
      2. HAL_RCC_ClkConfig()
      3. HAL_RCCEx_PeriphCLKConfig()
      4. PeriphCommonClock_Config()
    9. Power Management Functions
      1. HAL_PWREx_ControlVoltageScaling()
    10. Common HAL Status Values
    11. HAL in Your Project: Usage Summary
    12. Important HAL Patterns
      1. 1. Always Initialize First
      2. 2. Use Handles for Peripherals
      3. 3. Check Return Values
    13. Related: The LCD Driver Functions
    14. Timer & PWM Functions
      1. MX_TIM4_Init()
      2. HAL_TIM_Base_Init()
      3. HAL_TIM_Base_Start_IT()
      4. HAL_TIM_Base_Stop_IT()
      5. HAL_TIM_PWM_Start()
      6. HAL_TIM_PWM_Stop()
      7. HAL_TIM_PeriodElapsedCallback()
    15. Understanding How Timer Interrupts Work
      1. The Interrupt Flow
      2. The Generated Interrupt Handler
      3. Inside HAL_TIM_IRQHandler()
      4. Why Check htim->Instance?
      5. Different Callback Types
      6. The NVIC: Multiple Interrupts at Once
      7. Critical: Keep Callbacks Fast
    16. Timer Helper Macros
      1. __HAL_TIM_SET_COMPARE()
      2. __HAL_TIM_GET_AUTORELOAD()
      3. __HAL_TIM_SET_AUTORELOAD()
      4. __HAL_TIM_ENABLE() and __HAL_TIM_DISABLE()
      5. __WFI()
      6. __HAL_TIM_ENABLE_IT() and __HAL_TIM_DISABLE_IT()
    17. Complete Timer Interrupt Example
    18. Complete PWM Example
    19. Troubleshooting HAL Issues
    20. Quick Reference Table

This guide explains the Hardware Abstraction Layer (HAL) functions used in the Unit 3 LCD Test project. HAL functions provide easy access to the microcontroller’s hardware without needing to know low-level register details. You’ll find explanations of the most commonly used HAL functions, their syntax, parameters, and examples of how they work in your project.

You are not expected to memorise all these functions! Instead, this guide serves as a reference to understand what each function does and how it fits into your project. The key takeaway is that HAL functions allow you to control the hardware (GPIO, ADC, UART, etc.) in a simple and consistent way.

What is HAL?

The Hardware Abstraction Layer is a library provided by STMicroelectronics that abstracts away the complexity of the STM32 microcontroller. Instead of writing direct register access code, you call simple functions like HAL_GPIO_WritePin().

Your Code                HAL Library              Microcontroller Registers
┌──────────────────┐    ┌───────────────────┐    ┌──────────────────────┐
│ HAL_GPIO_Toggle  │───>│ Translation to    │───>│ GPIOA->BSRR = ...    │
│ HAL_Delay        │    │ register commands │    │ TIM2->CR1 |= TIM_... │
│ HAL_UART_Transmit│    │ (abstraction)     │    │ SPI2->DR = ...       │
└──────────────────┘    └───────────────────┘    └──────────────────────┘

Initialization Functions

HAL_Init()

Purpose: Initialize the HAL library. Must be called first in your program.

Syntax:

void HAL_Init(void);

What it does:

  • Sets up the Systick timer (for HAL_GetTick() and HAL_Delay())
  • Initializes priority grouping which handles interrupts
  • Prepares the HAL for general use

Example:

int main(void) {
    HAL_Init();  // Must be called first!
    
    SystemClock_Config();
    MX_GPIO_Init();
    // ... rest of initialization
}

Important: Call this before any other HAL functions.


SystemClock_Config()

Purpose: Configure the system clock frequency for optimal performance.

Syntax:

void SystemClock_Config(void);

What it does:

  • Enables the PLL (Phase-Locked Loop) oscillator
  • Sets the system clock to 80 MHz (the maximum for STM32L476)
  • Configures the AHB and APB buses

In your project: The system runs at 80 MHz, which means:

  • SPI2 (for LCD) gets 80 MHz
  • UART2 (for serial) gets 80 MHz
  • All timers and peripherals get the full clock speed

You don’t need to understand the details - it’s auto-generated by CubeMX and just needs to be called once at startup.


MX_GPIO_Init()

Purpose: Initialize all GPIO pins as configured in CubeMX.

Syntax:

void MX_GPIO_Init(void);

What it does:

  • Configures each GPIO pin as input or output
  • Sets pull-up/pull-down resistors
  • Sets the speed and alternate functions
  • Enables the GPIO port clocks

In your project: This sets up:

  • LED pin (PA5) as output
  • Button pin as input
  • SPI pins for LCD communication

Auto-generated: CubeMX creates this function based on your .ioc file configuration.


MX_USART2_UART_Init()

Purpose: Initialize UART2 (serial communication) for the serial console.

Syntax:

void MX_USART2_UART_Init(void);

What it does:

  • Configures UART2 with 115200 baud rate
  • Sets up TX (transmit) and RX (receive) pins
  • Enables the UART peripheral clock

Result: This enables printf() to work by redirecting serial output.

Auto-generated: CubeMX creates this based on your configuration.


GPIO (General Purpose Input/Output) Functions

GPIO functions control digital input and output pins.

HAL_GPIO_WritePin()

Purpose: Set a GPIO pin to HIGH (3.3V) or LOW (0V).

Syntax:

void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);

Parameters:

  • GPIOx - Which port (GPIOA, GPIOB, etc.)
  • GPIO_Pin - Which pin (GPIO_PIN_0, GPIO_PIN_1, etc.)
  • PinState - GPIO_PIN_SET (HIGH/3.3V) or GPIO_PIN_RESET (LOW/0V)

Example: Turn on LED

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);  // LED on (3.3V)
HAL_Delay(500);  // Wait 500ms
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);  // LED off (0V)

HAL_GPIO_ReadPin()

Purpose: Read the current state of a GPIO input pin.

Syntax:

GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

Returns:

  • GPIO_PIN_SET if the pin is HIGH (3.3V)
  • GPIO_PIN_RESET if the pin is LOW (0V)

Example: Read button state

GPIO_PinState button_state = HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13);

if (button_state == GPIO_PIN_SET) {
    printf("Button is pressed\n");
} else {
    printf("Button is not pressed\n");
}

In your project: Used to read the status of the blue button (B1) during the Magic 8Ball lab.


HAL_GPIO_TogglePin()

Purpose: Toggle a GPIO pin (switch it to the opposite state).

Syntax:

void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

What it does:

  • If pin is HIGH, set it LOW
  • If pin is LOW, set it HIGH

Example: Blinking LED

while(1) {
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);  // Toggle LED
    HAL_Delay(500);  // Wait 500ms
}
// Result: LED blinks every 500ms

In your project: Used to flash the green LED (LD2) during the welcome message.


Timing Functions

HAL_Delay()

Purpose: Pause execution for a specified number of milliseconds.

Syntax:

void HAL_Delay(uint32_t Delay);

Parameters:

  • Delay - Number of milliseconds to wait (0-4,294,967,295)

Example:

printf("Starting countdown...\n");
HAL_Delay(1000);  // Wait 1 second
printf("Go!\n");

In your project: Used extensively for delays between LCD refreshes and animations:

LCD_Fill_Buffer(0);
LCD_printString("Welcome", 20, 10, 1, 5);
LCD_Refresh(&cfg0);
HAL_Delay(200);  // Wait 200ms before next text

Performance note: Blocking delay - the CPU does nothing else while waiting. For a 500ms delay, the CPU sits idle for 500ms.


HAL_GetTick()

Purpose: Get the current system tick count (milliseconds since startup).

Syntax:

uint32_t HAL_GetTick(void);

Returns: Number of milliseconds since HAL_Init() was called.

Example: Measure elapsed time

uint32_t start_time = HAL_GetTick();

// Do some work...

uint32_t elapsed = HAL_GetTick() - start_time;
printf("Operation took %lu ms\n", elapsed);

Use case: Game timing

uint32_t last_frame_time = 0;

while(1) {
    uint32_t current_time = HAL_GetTick();
    
    if (current_time - last_frame_time >= 50) {  // 50ms = 20 FPS
        // Update game frame
        last_frame_time = current_time;
    }
}

UART (Serial Communication) Functions

HAL_UART_Transmit()

Purpose: Send data over the serial port (UART).

Syntax:

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, 
                                     uint16_t Size, uint32_t Timeout);

Parameters:

  • huart - UART handle (in your case, &huart2)
  • pData - Pointer to data to send
  • Size - Number of bytes to send
  • Timeout - Maximum time to wait (in ms, use HAL_MAX_DELAY for infinite)

Example: Send a string

char message[] = "Hello World\r\n";
HAL_UART_Transmit(&huart2, (uint8_t*)message, sizeof(message)-1, HAL_MAX_DELAY);

In your project: Used indirectly by printf(). The _write() function (in main.c) redirects printf output to this function:

int _write(int file, char *ptr, int len) {
    HAL_UART_Transmit(&huart2, (uint8_t*)ptr, len, HAL_MAX_DELAY);
    return len;
}

This is why printf() works and prints to the serial monitor!

Blocking: This function waits for transmission to complete before returning.


ADC (Analog-to-Digital Converter) Functions

ADC functions allow you to read analog voltages from the joystick and convert them to digital values.

HAL_ADC_Start()

Purpose: Start ADC conversion in polling mode (blocking).

Syntax:

HAL_StatusTypeDef HAL_ADC_Start(ADC_HandleTypeDef *hadc);

Parameters:

  • hadc - ADC handle (in your case, &hadc1)

Returns: HAL_OK if successful, HAL_ERROR otherwise

Example:

if (HAL_ADC_Start(&hadc1) != HAL_OK) {
    printf("ADC failed to start\n");
    while(1);  // Error
}

In your project: Used to start reading analog values from joystick pins (X and Y axes).


HAL_ADC_PollForConversion()

Purpose: Wait for ADC conversion to complete (blocking call).

Syntax:

HAL_StatusTypeDef HAL_ADC_PollForConversion(ADC_HandleTypeDef *hadc, uint32_t Timeout);

Parameters:

  • hadc - ADC handle
  • Timeout - Maximum time to wait in milliseconds (use HAL_MAX_DELAY for infinite)

Returns: HAL_OK if conversion completed, HAL_TIMEOUT if timeout occurred

Example: Read joystick X axis

HAL_ADC_Start(&hadc1);  // Start conversion

// Wait for conversion to finish
if (HAL_ADC_PollForConversion(&hadc1, 100) == HAL_OK) {
    uint32_t adc_value = HAL_ADC_GetValue(&hadc1);
    printf("X-axis: %lu\n", adc_value);
} else {
    printf("Conversion timeout\n");
}

In your project: Essential for reading joystick analog values in the main game loop.


HAL_ADC_GetValue()

Purpose: Retrieve the last ADC conversion result.

Syntax:

uint32_t HAL_ADC_GetValue(const ADC_HandleTypeDef *hadc);

Returns: The ADC conversion result (typically 0-4095 for 12-bit ADC)

Example: Read and normalize joystick value

uint32_t raw_value = HAL_ADC_GetValue(&hadc1);

// Normalize to -1.0 to 1.0 range
float normalized = (raw_value - 2048.0f) / 2048.0f;
printf("Normalized X: %.2f\n", normalized);

In your project: Called after HAL_ADC_PollForConversion() to get the actual analog reading.


HAL_ADC_Stop()

Purpose: Stop ADC conversion.

Syntax:

HAL_StatusTypeDef HAL_ADC_Stop(ADC_HandleTypeDef *hadc);

Returns: HAL_OK if successful

Example:

HAL_ADC_Stop(&hadc1);  // Stop reading ADC

In your project: Called during shutdown or when you want to stop reading joystick input.


MX_ADC1_Init()

Purpose: Initialize ADC1 peripheral with CubeMX settings.

Syntax:

void MX_ADC1_Init(void);

What it does:

  • Enables ADC1 clock
  • Configures analog input pins for joystick
  • Sets ADC resolution (12-bit)
  • Configures sampling time and conversion rate

Auto-generated: CubeMX creates this function based on your .ioc configuration.

In your project: Called during initialization to set up the ADC for reading joystick X, Y, and button values.


RNG (Random Number Generator) Functions

RNG functions provide hardware-based random number generation for better randomness than software-based rand().

HAL_RNG_Init()

Purpose: Initialize the RNG peripheral.

Syntax:

HAL_StatusTypeDef HAL_RNG_Init(RNG_HandleTypeDef *hrng);

Parameters:

  • hrng - RNG handle (in your case, &hrng)

Returns: HAL_OK if successful

Example:

if (HAL_RNG_Init(&hrng) != HAL_OK) {
    printf("RNG initialization failed\n");
    while(1);
}

In your project: Called during system initialization to enable hardware random number generation.


HAL_RNG_GenerateRandomNumber()

Purpose: Generate a 32-bit random number using the hardware RNG.

Syntax:

HAL_StatusTypeDef HAL_RNG_GenerateRandomNumber(RNG_HandleTypeDef *hrng, uint32_t *random32bit);

Parameters:

  • hrng - RNG handle
  • random32bit - Pointer to store the generated random number

Returns: HAL_OK if successful

Example: Generate random number in range [0, max]

uint32_t random_value;

if (HAL_RNG_GenerateRandomNumber(&hrng, &random_value) == HAL_OK) {
    uint16_t random_in_range = random_value % max;  // 0 to max-1
    printf("Random number: %u\n", random_in_range);
}

In your project: Used in the Joystick Game to randomly generate game events, AI behaviour, or puzzle elements.

Advantage over rand(): Hardware RNG provides better randomness and is less predictable than software pseudorandom number generators.


MX_RNG_Init()

Purpose: Initialize RNG peripheral with CubeMX settings.

Syntax:

void MX_RNG_Init(void);

What it does:

  • Enables RNG clock
  • Configures RNG for maximum randomness

Auto-generated: CubeMX creates this function.

In your project: Called during system initialization.


Expanded System Clock Configuration

Beyond SystemClock_Config(), you may need to understand the underlying RCC functions for advanced configurations.

HAL_RCC_OscConfig()

Purpose: Configure RCC oscillators (crystal oscillator or PLL).

Syntax:

HAL_StatusTypeDef HAL_RCC_OscConfig(RCC_OscInitTypeDef *RCC_OscInitStruct);

What it does:

  • Configures HSI (Internal clock) or HSE (External crystal)
  • Sets up PLL multiplier and divider
  • Enables/disables oscillators

Note: This is typically called by the auto-generated SystemClock_Config() function and you won’t need to call it directly.


HAL_RCC_ClkConfig()

Purpose: Configure system clock distribution and prescalers.

Syntax:

HAL_StatusTypeDef HAL_RCC_ClkConfig(RCC_ClkInitTypeDef *RCC_ClkInitStruct, uint32_t FLatency);

What it does:

  • Selects clock source (HSI, HSE, or PLL)
  • Sets AHB prescaler (affects CPU speed)
  • Sets APB1 and APB2 prescalers (affects peripheral speeds)

Note: Called by SystemClock_Config() to set your system to 80 MHz.


HAL_RCCEx_PeriphCLKConfig()

Purpose: Configure peripheral-specific clock sources.

Syntax:

HAL_StatusTypeDef HAL_RCCEx_PeriphCLKConfig(RCC_PeriphCLKInitTypeDef *PeriphClkInit);

What it does:

  • Configures ADC clock source and prescaler
  • Configures RNG clock
  • Configures UART/SPI clock sources

In your project: Used to set ADC and RNG clock frequencies.


PeriphCommonClock_Config()

Purpose: Initialize common peripheral clocks (auto-generated by CubeMX).

Syntax:

void PeriphCommonClock_Config(void);

What it does:

  • Enables clocks for ADC, RNG, and other peripherals
  • Calls HAL_RCCEx_PeriphCLKConfig() internally

In your project: Called during system initialization to set up clocks for ADC and RNG.


Power Management Functions

HAL_PWREx_ControlVoltageScaling()

Purpose: Control the internal voltage regulator output level.

Syntax:

HAL_StatusTypeDef HAL_PWREx_ControlVoltageScaling(uint32_t VoltageScaling);

Parameters:

  • VoltageScaling - Voltage level (PWR_REGULATOR_VOLTAGE_SCALE1 for 80 MHz operation)

Returns: HAL_OK if successful

Example:

if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK) {
    printf("Voltage scaling failed\n");
}

In your project: Called by SystemClock_Config() to set the correct voltage for 80 MHz operation. Higher clock speeds require higher voltage.

Important: Must be called before increasing the system clock frequency.


Common HAL Status Values

Many HAL functions return a status to indicate success or failure:

HAL_StatusTypeDef status = HAL_SPI_Transmit(&hspi2, data, length, timeout);

if (status == HAL_OK) {
    // Success!
} else if (status == HAL_ERROR) {
    // Error occurred
} else if (status == HAL_BUSY) {
    // Peripheral is busy
} else if (status == HAL_TIMEOUT) {
    // Timeout waiting for operation
}

HAL in Your Project: Usage Summary

Here’s how the HAL functions work together in your LCD test:

int main(void) {
    // 1. Initialize HAL
    HAL_Init();
    
    // 2. Configure system clock for 80 MHz
    SystemClock_Config();
    
    // 3. Initialize GPIO pins
    MX_GPIO_Init();
    
    // 4. Initialize serial port
    MX_USART2_UART_Init();
    
    // 5. Now you can use HAL functions
    while(1) {
        // LED control
        HAL_GPIO_TogglePin(LD2_GPIO_Port, LD2_Pin);
        
        // Timing
        HAL_Delay(500);
        
        // Serial output (via printf)
        printf("Elapsed: %lu ms\n", HAL_GetTick());
    }
}

Important HAL Patterns

1. Always Initialize First

// Wrong - HAL functions won't work yet
HAL_Delay(1000);
HAL_Init();

// Correct - Initialize before using
HAL_Init();
SystemClock_Config();
HAL_Delay(1000);  // Now this works

2. Use Handles for Peripherals

Most HAL functions take a “handle” - a structure containing peripheral configuration:

// UART handle (created by CubeMX)
extern UART_HandleTypeDef huart2;

// Use the handle to access the peripheral
HAL_UART_Transmit(&huart2, data, length, timeout);

3. Check Return Values

For critical operations, check if they succeeded:

HAL_StatusTypeDef status = HAL_UART_Transmit(&huart2, data, 100, 1000);

if (status != HAL_OK) {
    printf("Serial transmission failed!\n");
    while(1);  // Hang if critical error
}

Note that the LCD functions (like LCD_Fill_Buffer() and LCD_Refresh()) are not HAL functions - they’re part of the ST7789V2 driver library. However, they internally use HAL functions like:

// Inside LCD driver
void LCD_Draw_Circle(...) {
    // Uses HAL_Delay() for timing
    // Uses HAL_SPI functions for communication
    // Uses HAL_GPIO functions for control signals
}

Timer & PWM Functions

Timers are fundamental to PWM control, LED brightness, motor speed control, and precise timing. The STM32 HAL provides several functions to initialize, start, and control timers in different modes.

MX_TIM4_Init()

Purpose: Initialize TIM4 (or any timer) with CubeMX settings.

Syntax:

void MX_TIM4_Init(void);

What it does:

  • Enables the timer clock
  • Configures prescaler and period values
  • Sets up timer channels for PWM or output compare
  • Maps pins to timer channels (e.g., PB6 for TIM4_CH1)

In your project: Used during initialization to set up PWM on TIM4 Channel 1 for LED brightness control.

Auto-generated: CubeMX creates this function based on your .ioc timer configuration.


HAL_TIM_Base_Init()

Purpose: Initialize a hardware timer in basic counting mode.

Syntax:

HAL_StatusTypeDef HAL_TIM_Base_Init(TIM_HandleTypeDef *htim);

Returns: HAL_OK if successful, HAL_ERROR otherwise

What it does:

  • Configures the timer’s prescaler and period
  • Sets up the clock source
  • Prepares the timer for operation (but doesn’t start it yet)

Example:

if (HAL_TIM_Base_Init(&htim6) != HAL_OK) {
    printf("Timer initialization failed\n");
    while(1);  // Error
}

HAL_TIM_Base_Start_IT()

Purpose: Start a timer with interrupt enabled (calls ISR when period elapses).

Syntax:

HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim);

Returns: HAL_OK if successful

What it does:

  • Enables the timer’s update interrupt in the NVIC
  • Starts the timer counting

Example: 100ms interrupt timer

HAL_TIM_Base_Start_IT(&htim6);  // Start TIM6 with interrupts enabled
// Now HAL_TIM_PeriodElapsedCallback is called every 100ms

In your project: Used in the Unit_3_3_LED_Timer lab to toggle an LED at regular intervals.


HAL_TIM_Base_Stop_IT()

Purpose: Stop a timer and disable its interrupts.

Syntax:

HAL_StatusTypeDef HAL_TIM_Base_Stop_IT(TIM_HandleTypeDef *htim);

Returns: HAL_OK if successful

Example:

HAL_TIM_Base_Stop_IT(&htim6);  // Stop timer and disable interrupts

HAL_TIM_PWM_Start()

Purpose: Start PWM signal generation on a timer channel.

Syntax:

HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);

Parameters:

  • htim - Timer handle (e.g., &htim4)
  • Channel - Which timer channel (TIM_CHANNEL_1, TIM_CHANNEL_2, etc.)

Returns: HAL_OK if successful

What it does:

  • Enables PWM output on the specified channel
  • The PWM signal appears on the GPIO pin mapped to that channel
  • Frequency and duty cycle are controlled by the period and compare values set in CubeMX

Example: Start PWM on TIM4 Channel 1

if (HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1) != HAL_OK) {
    printf("PWM failed to start\n");
    while(1);
}
// PWM signal now appears on PB6

In your project: Called in main() to start PWM output on PB6 for LED brightness control.


HAL_TIM_PWM_Stop()

Purpose: Stop PWM signal generation.

Syntax:

HAL_StatusTypeDef HAL_TIM_PWM_Stop(TIM_HandleTypeDef *htim, uint32_t Channel);

Example:

HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_1);  // Stop PWM on PB6

HAL_TIM_PeriodElapsedCallback()

Purpose: Interrupt Service Routine (ISR) called when timer period elapses.

Syntax:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim);

What it does:

  • Called automatically by the HAL library when a timer interrupt fires
  • This is where you put fast, time-sensitive operations

Example: Toggle LED every timer period

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6) {
        // Called every 100ms (if TIM6 period = 10000 ticks @ 100kHz)
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);  // Toggle LED
        printf("Timer fired!\n");
    }
}

Important: Keep this function short! Long operations block other interrupts.


Understanding How Timer Interrupts Work

The interrupt system can seem mysterious at first. Here’s how it actually works under the hood:

The Interrupt Flow

When you call HAL_TIM_Base_Start_IT(&htim6), here’s the chain of events:

1. Timer counts in hardware
   └─→ Timer reaches period value
       └─→ Timer hardware generates interrupt signal
           └─→ NVIC (Nested Vectored Interrupt Controller) receives it
               └─→ CPU stops whatever it's doing
                   └─→ CPU calls TIM6_IRQHandler() [auto-generated by CubeMX]
                       └─→ TIM6_IRQHandler() calls HAL_TIM_IRQHandler(&htim6)
                           └─→ HAL_TIM_IRQHandler() checks interrupt type
                               └─→ If it's a period elapsed interrupt:
                                   └─→ HAL_TIM_IRQHandler() calls YOUR callback
                                       └─→ HAL_TIM_PeriodElapsedCallback(&htim6)
                                           └─→ Your code runs (toggle LED, set flag, etc.)
                                               └─→ Returns from callback
                                                   └─→ Returns from HAL_TIM_IRQHandler()
                                                       └─→ Returns from TIM6_IRQHandler()
                                                           └─→ CPU resumes main loop where it left off

The Generated Interrupt Handler

CubeMX auto-generates the low-level interrupt handler (TIM6_IRQHandler()) in stm32l4xx_it.c:

// In stm32l4xx_it.c (auto-generated by CubeMX)
void TIM6_IRQHandler(void)
{
    // Call the HAL interrupt handler
    HAL_TIM_IRQHandler(&htim6);
}

This handler exists for every interrupt on your microcontroller (UART, SPI, GPIO, etc.). The NVIC automatically calls the right one based on the interrupt source.

Inside HAL_TIM_IRQHandler()

The HAL library provides a generic HAL_TIM_IRQHandler() that does the heavy lifting:

// In stm32l4xx_hal_tim.c (HAL library - you don't modify this)
void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim)
{
    // Check what kind of interrupt occurred
    if ((htim->Instance->SR & TIM_IT_UPDATE) != RESET) {
        // Period elapsed (update) interrupt
        __HAL_TIM_CLEAR_IT(htim, TIM_IT_UPDATE);  // Clear the interrupt flag
        
        // Call YOUR callback
        HAL_TIM_PeriodElapsedCallback(htim);
    }
    
    if ((htim->Instance->SR & TIM_IT_CC1) != RESET) {
        // Compare/Capture channel 1 interrupt
        __HAL_TIM_CLEAR_IT(htim, TIM_IT_CC1);
        
        // Call a different callback for this event
        HAL_TIM_IC_CaptureCallback(htim);
    }
    // ... more checks for other interrupt types
}

Why Check htim->Instance?

If you have multiple timers (e.g., TIM4, TIM6, TIM7) all generating interrupts, they all call the same callback function with different htim handles. You must check which timer fired:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    // Multiple timers can call this callback!
    
    if (htim->Instance == TIM4) {
        // TIM4 fired - do TIM4-specific work
        HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_6);  // Blink LED on TIM4
    } 
    else if (htim->Instance == TIM6) {
        // TIM6 fired - do TIM6-specific work
        printf("TIM6 period elapsed\n");
    }
    else if (htim->Instance == TIM7) {
        // TIM7 fired - do TIM7-specific work
        pwm_duty_cycle++;
    }
}

The htim->Instance field points to the timer’s memory-mapped registers (e.g., TIM4, TIM6, TIM7 are defined memory addresses). By checking this, you know which hardware timer triggered the interrupt.

Different Callback Types

Different timer events have different callbacks:

Event Callback When it’s called
Period elapses HAL_TIM_PeriodElapsedCallback() When counter reaches period (update interrupt)
Output Compare HAL_TIM_OC_DelayElapsedCallback() When counter matches compare value
Input Capture HAL_TIM_IC_CaptureCallback() When signal detected on input pin
PWM Pulse Complete HAL_TIM_PWM_PulseFinishedCallback() When PWM pulse finishes (if using DMA)

Timers can generate multiple types of interrupts, so HAL_TIM_IRQHandler() checks all of them and calls the appropriate callback.

The NVIC: Multiple Interrupts at Once

What if two interrupts happen at the same time? The NVIC (Nested Vectored Interrupt Controller) handles this:

Scenario: Main loop running, then both TIM4 and UART2 interrupt
┌─────────────────────────────────────────────────────────────┐
│ Main loop code running...                                   │
├─────────────────────────────────────────────────────────────┤
│ [TIM4 interrupt occurs - higher priority]                   │
│   → CPU calls TIM4_IRQHandler()                             │
│   → HAL_TIM_PeriodElapsedCallback(&htim4)  [YOUR CODE]     │
│   → Returns from interrupt                                  │
├─────────────────────────────────────────────────────────────┤
│ [UART2 interrupt occurs - lower priority]                   │
│   → CPU calls USART2_IRQHandler()                           │
│   → HAL_UART_IRQHandler(&huart2)                            │
│   → Returns from interrupt                                  │
├─────────────────────────────────────────────────────────────┤
│ Main loop continues from where it was interrupted           │
└─────────────────────────────────────────────────────────────┘

Interrupts can nest: if a higher-priority interrupt occurs during a lower-priority interrupt, the CPU will pause the lower one to handle the higher one.

Critical: Keep Callbacks Fast

Since callbacks interrupt the main loop, keep them short and fast:

// ❌ BAD - slow callback that blocks everything
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6) {
        HAL_Delay(100);           // NEVER DELAY IN ISR!
        HAL_UART_Transmit(...);   // UART is too slow
        for (int i = 0; i < 1000000; i++) {  // NEVER LOOP!
            // ... heavy computation
        }
    }
}

// ✓ GOOD - fast callback that just sets a flag
volatile int g_timer_flag = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6) {
        g_timer_flag = 1;  // Just set a flag - done!
    }
}

// Handle the actual work in main loop
while (1) {
    if (g_timer_flag) {
        g_timer_flag = 0;
        // Now you can safely do slow operations
        HAL_Delay(100);
        HAL_UART_Transmit(...);
    }
    __WFI();  // Sleep
}

Rule of thumb: ISR callbacks should take < 1 millisecond. If your task is slow, use the callback to set a flag and handle the work in the main loop.


Timer Helper Macros

These macros allow you to manipulate timer registers directly.

__HAL_TIM_SET_COMPARE()

Purpose: Change the PWM duty cycle and output compare value at runtime.

Syntax:

__HAL_TIM_SET_COMPARE(htim, Channel, Compare);

Parameters:

  • htim - Timer handle
  • Channel - TIM_CHANNEL_1, TIM_CHANNEL_2, etc.
  • Compare - New compare value (0 to period ARR)

What it does:

  • Updates the CCR (Compare/Capture Register) for the channel
  • Changes the duty cycle without stopping the timer

Example: Set duty cycle to 50%

uint32_t arr = __HAL_TIM_GET_AUTORELOAD(&htim4);  // Get period
uint32_t pulse = arr / 2;  // 50% duty cycle
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, pulse);
// LED brightness changes immediately to 50%

In your project: Used in the PWM brightness control loop to smoothly dim/brighten the LED.


__HAL_TIM_GET_AUTORELOAD()

Purpose: Read the current timer period (ARR value).

Syntax:

uint32_t period = __HAL_TIM_GET_AUTORELOAD(htim);

Returns: The ARR (Auto-Reload Register) value

Example: Get period to calculate duty cycle

uint32_t period = __HAL_TIM_GET_AUTORELOAD(&htim4);
uint32_t pulse_20_percent = (period * 20) / 100;
__HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, pulse_20_percent);

__HAL_TIM_SET_AUTORELOAD()

Purpose: Change the timer period at runtime.

Syntax:

__HAL_TIM_SET_AUTORELOAD(htim, Autoreload);

What it does:

  • Updates the ARR (Auto-Reload Register)
  • Changes the frequency of PWM or timer interrupts
  • Takes effect on the next timer cycle

Example: Increase PWM frequency

uint32_t new_period = 500;  // Smaller period = higher frequency
__HAL_TIM_SET_AUTORELOAD(&htim4, new_period);

__HAL_TIM_ENABLE() and __HAL_TIM_DISABLE()

Purpose: Start or stop a timer at the register level.

Syntax:

__HAL_TIM_ENABLE(htim);   // Start timer
__HAL_TIM_DISABLE(htim);  // Stop timer

Example:

__HAL_TIM_DISABLE(&htim4);  // Stop PWM
HAL_Delay(1000);
__HAL_TIM_ENABLE(&htim4);   // Resume PWM

__WFI()

Purpose: “Wait For Interrupt” - put the CPU into low-power sleep mode until an interrupt wakes it.

Syntax:

__WFI();

What it does:

  • Pauses CPU execution
  • Reduces power consumption significantly
  • Wakes up immediately when any interrupt occurs
  • Better than HAL_Delay() when waiting for events

Example: Sleep until timer fires

while(1) {
    if (g_timer_flag) {
        g_timer_flag = 0;
        // Handle timer event
    }
    __WFI();  // Sleep until next interrupt
}

In your project: Used in the timer interrupt example to conserve power while waiting for the next timer event.


__HAL_TIM_ENABLE_IT() and __HAL_TIM_DISABLE_IT()

Purpose: Enable or disable timer interrupts without stopping the timer.

Syntax:

__HAL_TIM_ENABLE_IT(htim, Interrupt);   // Enable interrupt
__HAL_TIM_DISABLE_IT(htim, Interrupt);  // Disable interrupt

Parameters:

  • Interrupt - TIM_IT_UPDATE (period elapsed), TIM_IT_CC1 (capture/compare), etc.

Example: Disable timer interrupt temporarily

__HAL_TIM_DISABLE_IT(&htim6, TIM_IT_UPDATE);  // Stop getting period elapse interrupts
// Timer still counts, but no interrupt fired
__HAL_TIM_ENABLE_IT(&htim6, TIM_IT_UPDATE);   // Re-enable interrupts

Complete Timer Interrupt Example

This mirrors the workflow from timers_HAL.md:

#include "main.h"
#include "tim.h"

volatile int g_timer_flag = 0;
int led_state = 0;

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM6_Init();  // 100ms timer
    
    // Start timer with interrupts
    HAL_TIM_Base_Start_IT(&htim6);
    
    while (1) {
        if (g_timer_flag) {
            g_timer_flag = 0;
            
            // Toggle LED
            led_state = !led_state;
            HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, 
                             led_state ? GPIO_PIN_SET : GPIO_PIN_RESET);
        }
        
        __WFI();  // Sleep until next interrupt
    }
}

// Called by HAL every time TIM6 period elapses
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM6) {
        g_timer_flag = 1;
    }
}

Complete PWM Example

This mirrors the workflow from pwm_HAL.md:

#include "main.h"
#include "tim.h"

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM4_Init();  // PWM on TIM4 CH1 (PB6)
    
    // Start PWM output
    HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);
    
    while (1) {
        // Ramp brightness up and down
        uint32_t arr = __HAL_TIM_GET_AUTORELOAD(&htim4);
        
        // Brightness up (0 to 100%)
        for (uint32_t duty = 0; duty <= 100; duty++) {
            uint32_t pulse = (arr + 1) * duty / 100;
            __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, pulse);
            HAL_Delay(10);
        }
        
        // Brightness down (100 to 0%)
        for (uint32_t duty = 100; duty >= 0; duty--) {
            uint32_t pulse = (arr + 1) * duty / 100;
            __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, pulse);
            HAL_Delay(10);
        }
    }
}

Troubleshooting HAL Issues

Problem Likely Cause Solution
HAL_Delay() doesn’t work Forgot HAL_Init() Call HAL_Init() first
Serial output doesn’t appear UART not initialized Call MX_USART2_UART_Init()
LED doesn’t toggle Wrong pin name Check GPIO pin definitions in main.h
Code hangs on HAL_Delay() Interrupt conflict Check if Systick interrupt is enabled
GPIO pin stuck on Hardware issue Check connections and pin configuration
PWM doesn’t appear on pin Timer not started Call HAL_TIM_PWM_Start()
Timer interrupt not firing Interrupt not enabled Call HAL_TIM_Base_Start_IT() instead of HAL_TIM_Base_Start()
Duty cycle doesn’t change Wrong compare value Check __HAL_TIM_SET_COMPARE() calculation
CPU not sleeping in __WFI() Interrupts disabled Check NVIC settings for timer interrupt

Quick Reference Table

Function Purpose Example
HAL_Init() Initialize HAL HAL_Init();
SystemClock_Config() Set system clock to 80 MHz SystemClock_Config();
MX_GPIO_Init() Initialize GPIO pins MX_GPIO_Init();
MX_USART2_UART_Init() Initialize serial port MX_USART2_UART_Init();
HAL_GPIO_WritePin() Set pin HIGH or LOW HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET);
HAL_GPIO_ReadPin() Read pin state if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_SET) { ... }
HAL_GPIO_TogglePin() Toggle pin state HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay() Wait in milliseconds HAL_Delay(500);
HAL_GetTick() Get elapsed milliseconds uint32_t ms = HAL_GetTick();
HAL_UART_Transmit() Send serial data HAL_UART_Transmit(&huart2, data, len, HAL_MAX_DELAY);
HAL_ADC_Start() Start ADC conversion HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion() Wait for ADC conversion HAL_ADC_PollForConversion(&hadc1, 100);
HAL_ADC_GetValue() Get ADC result uint32_t val = HAL_ADC_GetValue(&hadc1);
HAL_ADC_Stop() Stop ADC conversion HAL_ADC_Stop(&hadc1);
HAL_RNG_Init() Initialize RNG HAL_RNG_Init(&hrng);
HAL_RNG_GenerateRandomNumber() Generate random number HAL_RNG_GenerateRandomNumber(&hrng, &rand);
MX_ADC1_Init() Initialize ADC1 MX_ADC1_Init();
MX_RNG_Init() Initialize RNG MX_RNG_Init();
     
Timer & PWM Functions    
MX_TIM4_Init() Initialize TIM4 with CubeMX settings MX_TIM4_Init();
HAL_TIM_Base_Init() Initialize timer in counting mode HAL_TIM_Base_Init(&htim6);
HAL_TIM_Base_Start_IT() Start timer with interrupt enabled HAL_TIM_Base_Start_IT(&htim6);
HAL_TIM_Base_Stop_IT() Stop timer and disable interrupt HAL_TIM_Base_Stop_IT(&htim6);
HAL_TIM_PWM_Start() Start PWM signal on channel HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);
HAL_TIM_PWM_Stop() Stop PWM signal HAL_TIM_PWM_Stop(&htim4, TIM_CHANNEL_1);
__HAL_TIM_SET_COMPARE() Set PWM duty cycle at runtime __HAL_TIM_SET_COMPARE(&htim4, TIM_CHANNEL_1, 500);
__HAL_TIM_GET_AUTORELOAD() Get timer period value uint32_t arr = __HAL_TIM_GET_AUTORELOAD(&htim4);
__HAL_TIM_SET_AUTORELOAD() Change timer period at runtime __HAL_TIM_SET_AUTORELOAD(&htim4, 1000);
__HAL_TIM_ENABLE() Start timer __HAL_TIM_ENABLE(&htim4);
__HAL_TIM_DISABLE() Stop timer __HAL_TIM_DISABLE(&htim4);
__HAL_TIM_ENABLE_IT() Enable timer interrupt __HAL_TIM_ENABLE_IT(&htim6, TIM_IT_UPDATE);
__HAL_TIM_DISABLE_IT() Disable timer interrupt __HAL_TIM_DISABLE_IT(&htim6, TIM_IT_UPDATE);
__WFI() Sleep CPU until interrupt __WFI();
HAL_TIM_PeriodElapsedCallback() Timer ISR callback void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { ... }

This site uses Just the Docs, a documentation theme for Jekyll.