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). ![e0081dd3-8486-44c4-be3f-00312ca6851a](https://hackmd.io/_uploads/Bki0SXs_gl.jpg) **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.