This problem has been resolved. It turns out there is a bug in the freeRTOS port for the STM32N6.
This is what has been done to fix this:
- In main.c:
/* USER CODE BEGIN 4 */
/* ==========================================================================
* HAL time base - overrides of the __weak HAL implementations.
*
* WHY THIS EXISTS
* ---------------
* On this port (FreeRTOS ARM_CM55_NTZ) the kernel defines SysTick_Handler
* itself, as a strong symbol, in port.c. That handler calls
* xTaskIncrementTick() unconditionally - it has no "is the scheduler
* running yet?" guard, because the kernel assumes it owns SysTick and only
* starts it from vPortSetupTimerInterrupt() inside xPortStartScheduler().
*
* The stock HAL_InitTick(), however, starts SysTick AND enables its
* interrupt from HAL_Init() - i.e. right at the top of main(), long before
* osKernelInitialize(). The first tick then lands in port.c's
* SysTick_Handler while the kernel's task lists are still all-zero BSS, so
* xTaskIncrementTick() dereferences pxDelayedTaskList == NULL and the core
* takes a HardFault somewhere in the middle of the MX_*_Init() calls.
*
* Note that cmsis_os2.c does contain a properly guarded SysTick_Handler,
* but it is compiled out because USE_CUSTOM_SYSTICK_HANDLER_IMPLEMENTATION
* is 1 - and it has to be, since it would otherwise clash with port.c's
* handler and reference xPortSysTickHandler(), which this port does not
* provide. So the guard cannot come from there.
*
* WHAT THIS DOES
* --------------
* Leaves SysTick's interrupt disabled until the kernel arms it:
*
* before osKernelStart() SysTick counts, no IRQ. HAL_GetTick() advances
* uwTick by polling the COUNTFLAG bit, which
* latches once per 1 ms wrap.
* after osKernelStart() vPortSetupTimerInterrupt() reprograms SysTick
* (LOAD/VAL/CTRL) and enables the IRQ, so the
* kernel drives the tick. vApplicationTickHook()
* in app_freertos.c keeps uwTick advancing.
*
* uwTick is the single source of truth throughout, so HAL_GetTick() stays
* monotonic across the handover - no backward jump for any HAL timeout that
* happens to straddle osKernelStart().
*
* A cleaner alternative, if you would rather not carry this code: in CubeMX
* set SYS -> Timebase Source to a hardware timer instead of SysTick. CubeMX
* then generates stm32n6xx_hal_timebase_tim.c, adds stm32n6xx_hal_tim.c to
* the project, and SysTick belongs to FreeRTOS alone. Delete this block if
* you go that route.
* ========================================================================== */
/**
* @brief Configure the HAL time base without taking the SysTick interrupt.
* @param TickPriority Requested tick interrupt priority (recorded only).
* @retval HAL status
*/
HAL_StatusTypeDef HAL_InitTick(uint32_t TickPriority)
{
uint32_t ulReload;
if ((uint32_t)uwTickFreq == 0UL)
{
return HAL_ERROR;
}
if (TickPriority >= (1UL << __NVIC_PRIO_BITS))
{
return HAL_ERROR;
}
ulReload = (SystemCoreClock / (1000UL / (uint32_t)uwTickFreq)) - 1UL;
/* HAL_RCC_ClockConfig() calls back into HAL_InitTick() after it updates
SystemCoreClock, which is how the reload value tracks the clock change.
If that happens once the scheduler is up, only the reload may be touched:
rewriting CTRL would clear TICKINT and stop the RTOS tick dead. */
if ((SysTick->CTRL & SysTick_CTRL_TICKINT_Msk) != 0UL)
{
SysTick->LOAD = ulReload;
}
else
{
/* Free-running 1 ms reload, processor clock, interrupt DELIBERATELY off.
FreeRTOS re-programs all three registers in vPortSetupTimerInterrupt()
when the scheduler starts. */
SysTick->LOAD = ulReload;
SysTick->VAL = 0UL;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_ENABLE_Msk;
}
uwTickPrio = TickPriority;
return HAL_OK;
}
/**
* @brief Return the current HAL tick in ms.
* @note Pre-scheduler this polls SysTick's COUNTFLAG, which is set on each
* wrap and cleared by the read. HAL only ever calls HAL_GetTick()
* from tight polling loops (HAL_Delay and the various
* "while (!ready) { if (HAL_GetTick() - start > TIMEOUT) }" loops),
* so wraps are not missed in practice. If one ever were, uwTick
* would lag and a timeout would take longer than nominal - it can
* never expire early, so the failure mode is safe.
* @retval Tick count in ms.
*/
uint32_t HAL_GetTick(void)
{
/* ONE read only - reading CTRL clears COUNTFLAG, so testing the two bits
with two separate reads would throw the wrap away. TICKINT tells us
whether the kernel has taken SysTick over yet, which keeps this function
free of any FreeRTOS dependency and safe to call from an ISR. */
uint32_t ulCtrl = SysTick->CTRL;
if (((ulCtrl & SysTick_CTRL_TICKINT_Msk) == 0UL) &&
((ulCtrl & SysTick_CTRL_COUNTFLAG_Msk) != 0UL))
{
uwTick++;
}
return uwTick;
}
/**
* @brief No-op. Suspending the tick would disable the kernel's SysTick
* interrupt once the scheduler owns it.
*/
void HAL_SuspendTick(void)
{
}
/**
* @brief No-op counterpart to HAL_SuspendTick().
*/
void HAL_ResumeTick(void)
{
}
- In app_freertos.c:
/**
* @brief Called from the kernel's SysTick_Handler on every tick.
* Requires configUSE_TICK_HOOK == 1.
*
* This is what keeps the HAL time base alive once the scheduler is
* running: port.c owns SysTick_Handler on this port and never calls
* HAL_IncTick(), so uwTick would otherwise freeze at osKernelStart()
* and every HAL_Delay() / HAL timeout would block forever.
* See the HAL_InitTick() / HAL_GetTick() overrides in main.c.
*
* @note Runs in interrupt context at the lowest priority. Keep it short and
* use only "FromISR" API here.
*/
void vApplicationTickHook(void)
{
HAL_IncTick();
}