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_Handlerin the start-up file (startup_stm32xxxx.s). Reset_HandlercallsSystemInit(), defined insystem_stm32xxxx.c. It performs the minimum needed to run: enabling the FPU when__FPU_PRESENTis set, settingSCB->VTORto the vector table address, and resetting the RCC to a known state.- The start-up code then copies
.datafrom 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 thatHAL_Delay()and every peripheral timeout depend on.SystemClock_Config()comes afterHAL_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.D1CorePrescTableconverts a prescaler register field into a shift count;>>then applies the division.- The
#ifalternatives exist because the register names differ across H7 revisions (D1CFGRon early parts,CDCFGR1on later ones). SystemCoreClockandSystemD2ClockfeedHAL_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, notTIM6_IRQHandler), otherwise the weak default handler catches the interrupt and does nothing. - The interrupt must be enabled in the NVIC for
HAL_TIM_IRQHandlerto be reached at all. HAL_InitTickmust have been overridden to start TIM6 in the first place.Project ▸ Properties ▸ ... ▸ SYS ▸ Timebase Sourcein 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.