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.