HAL (Hardware Abstraction Layer) Functions Reference Guide
Table of contents
- HAL (Hardware Abstraction Layer) Functions Reference Guide
- What is HAL?
- Initialization Functions
- GPIO (General Purpose Input/Output) Functions
- Timing Functions
- UART (Serial Communication) Functions
- ADC (Analog-to-Digital Converter) Functions
- RNG (Random Number Generator) Functions
- Expanded System Clock Configuration
- Power Management Functions
- Common HAL Status Values
- HAL in Your Project: Usage Summary
- Important HAL Patterns
- Related: The LCD Driver Functions
- Timer & PWM Functions
- Understanding How Timer Interrupts Work
- Timer Helper Macros
- Complete Timer Interrupt Example
- Complete PWM Example
- Troubleshooting HAL Issues
- 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()andHAL_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_SETif the pin is HIGH (3.3V)GPIO_PIN_RESETif 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 sendSize- Number of bytes to sendTimeout- Maximum time to wait (in ms, useHAL_MAX_DELAYfor 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 handleTimeout- Maximum time to wait in milliseconds (useHAL_MAX_DELAYfor 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 handlerandom32bit- 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
}
Related: The LCD Driver Functions
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 handleChannel- 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) { ... } |