Skip to main content

Startup sequence

From reset to main

Code runs before main() does. Knowing what has already happened explains most "why does this fail so early" questions.

  • The core loads the initial stack pointer and the reset vector from the two words at the start of flash, then jumps to Reset_Handler in the start-up file (startup_stm32xxxx.s).
  • Reset_Handler calls SystemInit(), defined in system_stm32xxxx.c. It performs the minimum needed to run: enabling the FPU when __FPU_PRESENT is set, setting SCB->VTOR to the vector table address, and resetting the RCC to a known state.
  • The start-up code then copies .data from flash to RAM, zeroes .bss, and runs C++ static constructors through __libc_init_array.
  • Finally it calls main().

Nothing before this point may rely on initialised globals — SystemInit() runs before .data is copied, so a global it writes to is overwritten moments later. This is a genuine trap when adding board setup there.

Order inside main

int main(void) {
MPU_Config(); // memory attributes, before the caches are enabled
CPU_CACHE_Enable(); // I-Cache and D-Cache
HAL_Init(); // HAL, NVIC grouping, SysTick
SystemClock_Config(); // PLL, bus dividers, flash latency
/* peripheral init, then the application */
}

The order is not arbitrary:

  • The MPU publishes the memory attributes that the caches obey, so it must be configured before the caches are enabled. Reversing the two leaves cache lines filled under the old attributes.
  • HAL_Init() must precede every other HAL call, because it establishes the time base that HAL_Delay() and every peripheral timeout depend on.
  • SystemClock_Config() comes after HAL_Init(), which is why the HAL reconfigures its tick when the clock changes.

HAL_Init

/**
* @brief This function is used to initialize the HAL Library; it must be the first
* instruction to be executed in the main program (before to call any other
* HAL function), it performs the following:
* Configures the SysTick to generate an interrupt each 1 millisecond,
* which is clocked by the HSI (at this stage, the clock is not yet
* configured and thus the system is running from the internal HSI at 16 MHz).
* Set NVIC Group Priority to 4.
* Calls the HAL_MspInit() callback function defined in user file
* "stm32h7xx_hal_msp.c" to do the global low level hardware initialization
*
* @note SysTick is used as time base for the HAL_Delay() function, the application
* need to ensure that the SysTick time base is always set to 1 millisecond
* to have correct HAL operation.
* @retval HAL status
*/
HAL_StatusTypeDef HAL_Init(void)
{

uint32_t common_system_clock;

#if defined(DUAL_CORE) && defined(CORE_CM4)
/* Configure Cortex-M4 Instruction cache through ART accelerator */
__HAL_RCC_ART_CLK_ENABLE(); /* Enable the Cortex-M4 ART Clock */
__HAL_ART_CONFIG_BASE_ADDRESS(0x08100000UL); /* Configure the Cortex-M4 ART Base address to the Flash Bank 2 : */
__HAL_ART_ENABLE(); /* Enable the Cortex-M4 ART */
#endif /* DUAL_CORE && CORE_CM4 */

/* Set Interrupt Group Priority */
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

/* Update the SystemCoreClock global variable */
#if defined(RCC_D1CFGR_D1CPRE)
common_system_clock = HAL_RCC_GetSysClockFreq() >> ((D1CorePrescTable[(RCC->D1CFGR & RCC_D1CFGR_D1CPRE)>> RCC_D1CFGR_D1CPRE_Pos]) & 0x1FU);
#else
common_system_clock = HAL_RCC_GetSysClockFreq() >> ((D1CorePrescTable[(RCC->CDCFGR1 & RCC_CDCFGR1_CDCPRE)>> RCC_CDCFGR1_CDCPRE_Pos]) & 0x1FU);
#endif

/* Update the SystemD2Clock global variable */
#if defined(RCC_D1CFGR_HPRE)
SystemD2Clock = (common_system_clock >> ((D1CorePrescTable[(RCC->D1CFGR & RCC_D1CFGR_HPRE)>> RCC_D1CFGR_HPRE_Pos]) & 0x1FU));
#else
SystemD2Clock = (common_system_clock >> ((D1CorePrescTable[(RCC->CDCFGR1 & RCC_CDCFGR1_HPRE)>> RCC_CDCFGR1_HPRE_Pos]) & 0x1FU));
#endif

#if defined(DUAL_CORE) && defined(CORE_CM4)
SystemCoreClock = SystemD2Clock;
#else
SystemCoreClock = common_system_clock;
#endif /* DUAL_CORE && CORE_CM4 */

/* Use systick as time base source and configure 1ms tick (default clock after Reset is HSI) */
if(HAL_InitTick(TICK_INT_PRIORITY) != HAL_OK)
{
return HAL_ERROR;
}

/* Init the low level hardware */
HAL_MspInit();

/* Return function status */
return HAL_OK;
}

Dual-core and the ART accelerator

When both DUAL_CORE and CORE_CM4 are defined, the code being built is the Cortex-M4 half of a dual-core part such as the STM32H747.

  • __HAL_RCC_ART_CLK_ENABLE() — clock the ART accelerator. ART (Adaptive Real-Time memory accelerator) caches and prefetches instructions for the M4, which has no I-Cache of its own.
  • __HAL_ART_CONFIG_BASE_ADDRESS(0x08100000UL) — point it at flash bank 2, where the M4 image lives.
  • __HAL_ART_ENABLE() — turn it on.

Interrupt priority grouping

HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4) assigns all four implemented priority bits to preemption priority and none to subpriority. Every interrupt can therefore preempt any lower-priority one, and subpriority only breaks ties between simultaneously pending interrupts of equal preemption priority.

This is ST's default across the HAL, and it is worth leaving alone: FreeRTOS and CMSIS-RTOS2 assume a known grouping when they compute configMAX_SYSCALL_INTERRUPT_PRIORITY. See Interrupts.

System clock variables

These lines read the current prescaler settings and update the globals the rest of the HAL depends on:

  • HAL_RCC_GetSysClockFreq() returns the frequency of the selected system clock source.
  • D1CorePrescTable converts a prescaler register field into a shift count; >> then applies the division.
  • The #if alternatives exist because the register names differ across H7 revisions (D1CFGR on early parts, CDCFGR1 on later ones).
  • SystemCoreClock and SystemD2Clock feed HAL_Delay(), the SysTick reload calculation and every peripheral baud-rate computation. Values that drift out of sync with the real clock produce timing that is wrong everywhere at once.

SystemCoreClockUpdate() recomputes these from the hardware. Call it after any clock change made outside the HAL, otherwise delays and baud rates silently scale by whatever factor the clock moved.

Time base

HAL_InitTick(TICK_INT_PRIORITY) configures SysTick to interrupt every millisecond and returns HAL_ERROR on failure, which HAL_Init propagates.

At this point the system still runs from the HSI — on the H7 that is a 64 MHz oscillator with a default divider of 4, hence the 16 MHz in the doc comment. SystemClock_Config() later switches to the PLL, so the tick has to be recomputed; the HAL does this inside HAL_RCC_ClockConfig.

Low-level hardware

HAL_MspInit() is a weak function you override in stm32xxxx_hal_msp.c. It handles board-specific setup the HAL cannot know about: peripheral clocks, GPIO alternate functions, DMA channels, interrupt priorities.

The MSP pattern repeats per peripheral — HAL_UART_MspInit, HAL_SPI_MspInit and so on — each called from the corresponding HAL_*_Init. See UART and virtual COM port for a worked example.

Return value

HAL_OK once everything above has completed. Check it — a failure here means the time base is not running, and every HAL_Delay() afterwards hangs forever.

HAL_GetTick always returns 0

The HAL time base is not running. HAL_GetTick() returns a counter incremented from a timer interrupt, so a value stuck at 0 means the interrupt never fires — and every HAL_Delay() in the application blocks indefinitely.

Under an RTOS the time base is usually moved off SysTick onto a basic timer, typically TIM6, because the kernel claims SysTick_Handler for itself. Three things must all be present:

TIM_HandleTypeDef htim6;

void TIM6_DAC_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim6);
}
  • The handler must be named exactly as in the vector table (TIM6_DAC_IRQHandler, not TIM6_IRQHandler), otherwise the weak default handler catches the interrupt and does nothing.
  • The interrupt must be enabled in the NVIC for HAL_TIM_IRQHandler to be reached at all.
  • HAL_InitTick must have been overridden to start TIM6 in the first place. Project ▸ Properties ▸ ... ▸ SYS ▸ Timebase Source in CubeMX generates this.

If SysTick is still the intended time base, the usual cause is that the RTOS took over SysTick_Handler and HAL_IncTick() is no longer called from it.

Error_Handler

CubeMX generates an Error_Handler() that disables interrupts and spins:

void Error_Handler(void) {
__disable_irq();
while (1) { }
}

That is correct as a default, and useless in the field. During bring-up, set a breakpoint on it — every HAL_* failure path lands here, and a hang with no output is otherwise indistinguishable from a clock or a fault problem. For production, record why it was reached before halting or resetting.