ZYNQ7000 XPAR_XILTIMER_ENABLED Macro definition prevents initialization of interrupt controller

In zynq7000, the interrupt controller scugic is defined by freertos as a global variable xInterruptController. And in the task scheduling start function, FreeRTOS_ The SetupTickInterrupt function is initialized. Since most tutorials are based on bare metal, I used the interrupt registration function for bare metal before. If it is initialized correctly, there will be no problem. But if you use macros to define XPARs_ XILTIMER_ ENABLED (possibly timer turned on), FreeRTOS_ The SetupTickInterrupt function will not initialize scugic, and I think this should be modified. Otherwise, there will be a situation where the logic has already been initialized and the interrupt function result directly fails to connect. I haven’t found any other places to initialize the interrupt controller either.

Also, there are no examples of interrupts or documentation on freertos interrupts in Vitis’ freertos, and many tutorials are also based on bare metal implementation of interrupts. So it’s still a bit difficult to directly use the freertos interface.

HI @ xiuyangzhang,

Welcome to the FreeRTOS community.
The idea is to only touch the xInterruptController struct AFTER the scheduler is started.
Please refer to the thread Zynq 7020 interrupts under FreeRTOS v10.1.1 - #4 by simon for more details.

@xiuyangzhang @Shub

I hit the exact same issue in 2026 (Vitis 2025.2.1, SDT flow, Zynq-7020/Zybo Z7-20): with XPAR_XILTIMER_ENABLED defined, the tick never fired, vTaskDelay() never returned, and the interrupt controller was never initialized. After weeks of tracing, I found the real root cause — and it is not a missing GIC init in the port.

Actual root cause:

The xiltimer library keeps its tick-timer instance TimerInst as a static variable in .bss (initialized by the xtimerinit() constructor). On our build, the linker placed TimerInst immediately adjacent to the start of the FreeRTOS heap (ucHeap). Because configSUPPORT_STATIC_ALLOCATION == 0, xTaskCreate() allocates the first task stack from the beginning of the heap — and FreeRTOS fills the new task stack with the 0xa5a5a5a5 canary pattern. That fill overwrites TimerInst’s function pointers with zero.

When the scheduler then calls FreeRTOS_SetupTickInterrupt() → XTimer_SetInterval() → XTimer_TtcTickInterval(), the bxeq lr NULL-pointer branch silently returns: the TTC is never configured and the GIC (IRQ 42) is never enabled. Hence your exact symptom: “the logic has already been initialized and the interrupt function result directly fails to connect” — but the failure is upstream of the GIC.

The fix (application side, 2 lines): re-run the constructor after task creation, just before vTaskStartScheduler():

extern void xtimerinit(void); /* xiltimer.c constructor /
/
… xTaskCreate() … /
xtimerinit(); /
restore TimerInst pointers overwritten by task-stack init */
vTaskStartScheduler();

Diagnostic that proved it (worth adding if you debug similar issues): print TimerInst.XTimer_TickInterval inside FreeRTOS_SetupTickInterrupt() — it was 0x0 at scheduler start while non-NULL in main() before task creation.

Verified on Zybo Z7-20 / Vitis 2025.2.1: after the fix, TTC0 is configured (CLK_CNTRL=0x09, CNT_CNTRL=0x22, IER=0x01), GIC ISENABLER1 bit 10 (IRQ 42) = 1, tick counts, vTaskDelay() works.

The task creation code should only fill the stack memory allocated for the task stack and should not corrupt anything adjacent to it. How did you verify that the task creation code is corrupting these pointers?

I think you should find the root cause of the corruption as you may just be addressing the partial symptom of the real problem.

Hi Gaurav — you’re right, and thanks for pushing on it. My earlier post got the mechanism wrong, and the actual root cause turned out to be much more mundane.

Re-tracing with binary-search probes: printing TimerInst’s pointers at each stage of startup showed them go from valid (the constructor had run — .init_array is fine on this BSP) to zero somewhere between the early peripheral init and the scaler setup. Narrowing further, it was the memset of a Xilinx driver instance. sizeof(XV_Hscaler_l2) is 132,648 bytes — the struct embeds two u64 phase tables of 8192 entries each (phasesH, phasesH_H). My application code had declared it as a stack local, on the default 8 KB stack (0x2000 in every zynq linker script Vitis generates). The 132 KB frame pushed the stack far down into .bss, the instance landed directly above TimerInst, and memset(&HscInst, 0, sizeof(HscInst)) covered it. So the function pointers were zeroed by a memset of a mis-placed driver struct, not by FreeRTOS stack filling. The bytes were 0, never 0xa5a5a5a5, and in our build TimerInst sits ~2.9 KB away from ucHeap in .bss — the canary story never held up, and you called it correctly from the start.

Why did re-running xtimerinit() appear to fix it? Because the clobbering memset happens early in the application’s display-path initialization, and my second xtimerinit() call happened to come after it — so it re-initialized pointers that had already been zeroed. It was masking the real problem, exactly as you suspected. The fix is to keep such driver instances in static or file-scope storage (one-line change), and the workaround is now removed entirely. Verified without it: TimerInst stays valid through task creation, and the tick hardware comes up correctly — GIC ISENABLER1 bit 10 set, TTC CLK_CNTRL=0x09, CNT_CNTRL=0x22, IER=0x01 — the same register state I reported before, which is why my earlier diagnosis felt so convincing.

Two corrections to my own post while I’m at it. TimerInst is a plain global in xiltimer.c (XTimer TimerInst;), not static, and xtimerinit is an unconditional __attribute__((constructor)). And the allocation convention was there all along — the official vprocss example declares its XVprocSs instance (which embeds the same 132 KB XV_Hscaler_l2) as a file-scope global. I simply didn’t follow it.

That also explains the original symptom. With XPAR_XILTIMER_ENABLED defined, FreeRTOS_SetupTickInterrupt takes the xiltimer path in portZynq7000.c, and XTimer_SetInterval dereferences TimerInst through a plain NULL guard (if (InstancePtr->XTimer_TickInterval)). If those pointers are garbage, the call silently returns and the GIC is never touched — the failure really is upstream of the GIC, as the original poster observed. The corruption can come from anywhere in the application’s address space; it just needs to hit TimerInst before the scheduler starts, and the port is never involved.

One observation that might help others: the L2 API ships with no usage example anywhere in embeddedsw, and the header only says “the user is required to allocate a variable of this type” — with no hint that it’s 132 KB and shouldn’t live on the default 8 KB stack. A 132 KB struct, an 8 KB default stack, and no example is a trap that would be cheap for the vendor to document. The FreeRTOS port itself was never the problem.

Thank you for sharing your investigation and glad that you figured it!