Here’s a clean, practical guide to using [STM32](https://www.ampheo.com/search/STM32) timers for delays and periodic tasks—with HAL/CubeMX, plus low-level notes so you can adapt to any [STM32](https://www.onzuu.com/search/STM32).

**The toolbox (when to use what)**
* SysTick / HAL_Delay(ms): easy millisecond delays, but blocking (CPU idle) and coarse. Good for setup only.
* General-purpose timers (TIM2/3/4/…): precise periodic interrupts and µs-scale delays. Your go-to for most work.
* One-pulse / output compare: accurate one-shot delays without busy-waiting.
* LPTIM (low-power timer): long, low-power periodic wakeups (e.g., 1 Hz from 32.768 kHz LSE).
* DWT cycle counter (Cortex-M, if present): ultra-short, CPU-busy µs/ns delays; great for bit-banging or timing code.
**Key formulas (works on all STM32)**
Timer clock (on APB domain):
`TIMxCLK = PCLKx * (APB prescaler > 1 ? 2 : 1) // for classic STM32 timers`
Timebase:
```
tick_freq = TIMxCLK / (PSC + 1)
period_freq = tick_freq / (ARR + 1)
period_time = (ARR + 1) / tick_freq
```
Choose PSC and ARR to hit your target.
Tip: Get clocks at runtime:
```
uint32_t pclk1 = HAL_RCC_GetPCLK1Freq();
uint32_t timclk = pclk1;
if ((RCC->CFGR & RCC_CFGR_PPRE1) != RCC_CFGR_PPRE1_DIV1) timclk *= 2;
```
**Recipe A — Periodic task via interrupt (e.g., 1 kHz “system tick”)**
Goal: call a callback every 1 ms (toggle LED, run a scheduler, sample ADC, etc.)
**CubeMX:**
1. Enable TIM3 (any GP timer).
2. Set Clock Source = Internal.
3. Compute PSC/ARR for 1 kHz update (example code below auto-computes).
4. Enable NVIC for TIM3 global interrupt.
**Code (HAL):**
```
TIM_HandleTypeDef htim3;
static void TIM3_Init_1kHz(void){
__HAL_RCC_TIM3_CLK_ENABLE();
// Compute prescaler & ARR for 1 kHz
uint32_t pclk = HAL_RCC_GetPCLK1Freq();
uint32_t timclk = pclk;
if ((RCC->CFGR & RCC_CFGR_PPRE1) != RCC_CFGR_PPRE1_DIV1) timclk *= 2;
// Aim for tick = 1 MHz (1 µs), then ARR = 1000-1 -> 1 kHz
uint32_t target_tick = 1000000; // 1 MHz
uint32_t psc = (timclk / target_tick) - 1;
uint32_t arr = (target_tick / 1000) - 1; // 1 kHz period
htim3.Instance = TIM3;
htim3.Init.Prescaler = psc;
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
htim3.Init.Period = arr;
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
#if defined(TIM_AUTORELOAD_PRELOAD_DISABLE)
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
#endif
HAL_TIM_Base_Init(&htim3);
HAL_TIM_Base_Start_IT(&htim3);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){
if (htim->Instance == TIM3){
// Runs every 1 ms
// Keep this *very* short; defer work to main loop/RTOS task/queue.
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
}
}
```
Keep ISR work short (set flags, push to a queue). Long work → jitter & missed deadlines.
**Recipe B — Microsecond delay with a hardware timer (non-blocking CPU while waiting in main)**
Goal: accurate µs-scale delays without using HAL_Delay.
Idea: configure a timer to 1 MHz tick and wait on its counter (or output-compare).
**Init (reuse 1 MHz from above, e.g., TIM2):**
```
TIM_HandleTypeDef htim2;
static void TIM2_Init_1MHz(void){
__HAL_RCC_TIM2_CLK_ENABLE();
uint32_t pclk = HAL_RCC_GetPCLK1Freq();
uint32_t timclk = pclk;
if ((RCC->CFGR & RCC_CFGR_PPRE1) != RCC_CFGR_PPRE1_DIV1) timclk *= 2;
uint32_t psc = (timclk / 1000000UL) - 1; // 1 MHz
htim2.Instance = TIM2;
htim2.Init.Prescaler = psc;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 0xFFFFFFFF; // free-running
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
HAL_TIM_Base_Start(&htim2);
}
void delay_us(uint32_t us){
uint32_t start = __HAL_TIM_GET_COUNTER(&htim2);
while ((uint32_t)(__HAL_TIM_GET_COUNTER(&htim2) - start) < us) {
// idle; optionally __WFI() if you have a tick/interrupt to wake
}
}
```
For ultra-short delays and benchmarking, you can also use the DWT cycle counter (if present) for sub-µs waits.
**Recipe C — One-shot (precise) delay using output-compare or one-pulse**
Goal: fire a single ISR after N microseconds (no busy-wait).
```
// Assume TIM2 ticking at 1 MHz as above
void timer_one_shot_start(uint32_t us){
__HAL_TIM_DISABLE_IT(&htim2, TIM_IT_CC1);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, __HAL_TIM_GET_COUNTER(&htim2) + us);
__HAL_TIM_CLEAR_IT(&htim2, TIM_IT_CC1);
__HAL_TIM_ENABLE_IT(&htim2, TIM_IT_CC1); // enable compare match IRQ
}
void HAL_TIM_OC_DelayElapsedCallback(TIM_HandleTypeDef* htim){
if (htim->Instance == TIM2 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1){
// One-shot event fired
// do your task...
__HAL_TIM_DISABLE_IT(&htim2, TIM_IT_CC1); // stop further events
}
}
```
Alternatively set One-Pulse Mode (OPM=1), so the timer stops itself after the event.
**Recipe D — Low-power periodic wake with LPTIM (e.g., 1 Hz from LSE)**
Why: keep RTC/LSE running in Stop mode, wake periodically with tiny current.
```
// LPTIM1 clocked from LSE (32.768 kHz), period 1 s
static void LPTIM1_Init_1Hz(void){
__HAL_RCC_LPTIM1_CLK_ENABLE();
// Choose LSE as LPTIM1 clock in RCC (CubeMX does this for you)
LPTIM_HandleTypeDef hlptim1 = { .Instance = LPTIM1 };
hlptim1.Init.Clock.Source = LPTIM_CLOCKSOURCE_APBCLOCK_LPOSC;
hlptim1.Init.Prescaler = LPTIM_PRESCALER_DIV1;
hlptim1.Init.CounterSource = LPTIM_COUNTERSOURCE_INTERNAL;
hlptim1.Init.UpdateMode = LPTIM_UPDATE_ENDOFPERIOD;
HAL_LPTIM_Init(&hlptim1);
// 32768 ticks per second
HAL_LPTIM_TimeOut_Start_IT(&hlptim1, 32768-1, 0); // periodic
}
void HAL_LPTIM_AutoReloadMatchCallback(LPTIM_HandleTypeDef *hlptim){
// fires every 1 s; device can run in Stop mode between wakes
}
```
**Common pitfalls (read this!)**
* APB prescaler gotcha: If APB prescaler > 1, many [STM32](https://www.ampheoelec.de/search/STM32) families double the timer clock. Always compute TIMxCLK correctly.
* NVIC not enabled: Timer won’t interrupt until its IRQ is enabled and priority set.
* ISR too heavy: Do minimal work in callbacks; use flags/queues for main/RTOS tasks.
* 16-bit timers & long periods: If ARR must exceed 65535, raise the prescaler or use a 32-bit timer (e.g., TIM2/TIM5 on many parts).
* Debug halts timers: With “Debug → Auto-reload stop” options, timers may pause when stepping. Disable if unwanted.
* HAL_Delay under FreeRTOS: After scheduler starts, HAL_Delay maps to osDelay (non-blocking). Before scheduler, it blocks.
* Clock changes: If you change system clocks after timer init, recompute PSC/ARR.
**Quick starter sequence**
1. Pick a timer on the right APB domain (TIM2/3/… or LPTIM).
2. Compute PSC/ARR using the formulas (or set tick to 1 MHz and derive from that).
3. Enable interrupt (HAL_TIM_Base_Start_IT) for periodic tasks, or use OC/OPM for one-shots.
4. Keep ISRs short; move work to main/RTOS.
5. Verify with a scope/logic analyzer (toggle a pin in the ISR) to confirm frequency/jitter.