nRF54L15: system clock stops permanently after a GRTC compare event is lost
Disclaimer
Claude Code (Opus 5) was used to assist with this investigation and with the drafting of this report. The failure itself was found by the author, on our own hardware. Every register capture, code path and conclusion below has been manually reviewed and verified by the author.
Summary
Under sustained BLE traffic the Zephyr system tick stops permanently. Every kernel timeout then stalls forever while the GRTC SYSCOUNTER keeps running.
We trace this to a race in the safe_setting branch of nrfx_grtc_syscounter_cc_abs_set(). That branch clears EVENTS_COMPARE in order to discard a stale event from the previous CC value; if the newly programmed compare fires between the counter sample and the clear, the clear destroys the legitimate event along with the stale one. Nothing regenerates it, and the driver is never called again to rewrite the CC, so the stall is permanent.
The state captured on the halted target is what identifies it:
| Register | Value | Meaning |
|---|---|---|
CC[6].CCEN |
0 | the compare fired |
EVENTS_COMPARE[6] |
0 | its event is nevertheless gone |
INTEN2 bit 6 |
1 | the interrupt was enabled the whole time |
We built a standalone reproducer, attached as GRTC-CompareLoss-Reproduction.zip and described under Standalone reproduction. It destroys COMPARE events inside nrfx_grtc_syscounter_cc_abs_set() on demand, on a private channel, with a control pass that differs only in the safe_setting argument and loses nothing; it kills the Zephyr system tick through the ordinary timeout API, ending in the register signature above; and it measures the CCEN behaviour the diagnosis rests on directly, on hardware.
Our fix, described under Our fix, is in nrfx rather than in Zephyr: the safe_setting branch now clears the event before arming the channel, the ordering nrfx's own sibling entry point nrfx_grtc_syscounter_cc_absolute_set() already uses, so that no clear can follow the new CC going live and no event it generates can be destroyed. We are filing this because the code path is unchanged in Zephyr main and in nrfx 4.5.0, and because the fix belongs upstream rather than in every application's patch directory.
Environment
| SoC | nRF54L15 |
| Board | nRF54L15-DK (1.0.0, 2025.51) |
| SDK | nRF Connect SDK v3.3.0 |
| Zephyr | fd9204a02d5 (2026-04-20) |
| nrfx | 4.2.1 |
| Driver | drivers/timer/nrf_grtc_timer.c |
Relevant configuration:
CONFIG_TICKLESS_KERNEL=y
CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC=1000000
CONFIG_SYS_CLOCK_TICKS_PER_SEC=31250 # CYC_PER_TICK = 32
CONFIG_NRF_GRTC_START_SYSCOUNTER=y
CONFIG_NRF_GRTC_TIMER_AUTO_KEEP_ALIVE=y
CONFIG_NRF_GRTC_TIMER_SOURCE_LFXO=y
CONFIG_BT=y # SoftDevice Controller, peripheral
CONFIG_MPSL=y
GRTC devicetree:
&grtc {
owned-channels = <0 1 2 3 4 5 6 7 8 9 10 11>;
child-owned-channels = <3 4 7 8 9 10 11>; /* 7-11 = MPSL */
};
Allowed application channel mask is 0x67 = {0,1,2,5,6}. nrfx_flag32_alloc() returns the highest free bit, so the system clock owns channel 6.
On secure cpuapp GRTC_IRQ_GROUP = 2, so the relevant registers are INTEN2 / INTPEND2 and the relevant per-domain register is SYSCOUNTER[2]. Zephyr uses IRQ 228 (GRTC_2); MPSL uses IRQ 229 (GRTC_3).
Symptom
After a period of BLE communication testing the device stops making forward progress. Breakpoints in sys_clock_timeout_handler() are never hit, while the GRTC SYSCOUNTER continues to increment. Halting the debugger always lands in the idle thread at __WFI.
Frequency: roughly twice per hour of intensive BLE testing, with many connect/disconnect cycles. Observed onset times range from ~30 s to ~2 h of uptime.
Because the CPU is asleep rather than spinning, a watchdog configured with WDT_OPT_PAUSE_IN_SLEEP never fires either, so the failure presents as a completely silent, indefinite hang.
State captured at the hang
Two reproductions were captured, in separate runs. Note up front that they are not two views of one event: the driver statics below come only from run 1, and the CC/CCEN registers only from run 2.
Reproduction 1 — established that no software path had suppressed the interrupt
| Item | Value | Note |
|---|---|---|
| CPU context | idle thread at __WFI |
normal for arch_cpu_idle() |
PRIMASK |
1 | normal, set by arch_cpu_idle() around WFI |
BASEPRI |
0 | nothing masked |
VECTACTIVE |
0 | thread mode, not stuck in an ISR |
ISER[7] |
0x30 |
IRQ 228 and 229 both enabled |
ISPR[7] |
0x20 |
only IRQ 229 pending (MPSL, fires during the halt, benign) |
INTEN2 |
0x40 |
channel 6 interrupt is enabled |
INTPEND2 |
0x00 |
no compare event pending -> EVENTS_COMPARE[6] == 0 |
| SYSCOUNTER | 1:0xD9DC3163 |
= 7,950,053,731 cycles ~= 2 h 12 min |
nrf_grtc_timer.c statics:
| Static | Value | Note |
|---|---|---|
cc_value |
57516636 | = 0x036DA25C |
expired_cc |
57516538 | previous expiry, 98 cycles = 98 us earlier |
int_mask |
0x67 |
equals the allowed mask; no channel interrupt-locked |
The 98-cycle figure is the distance from the previous expiry. Reconstructing the forward distance that was actually requested:
cc_value - expired_cc = last_elapsed + cyc - d = 98, d = expired_cc - last_count,
d in [0, 32) because the handler advances last_count by whole ticks
=> cyc <= 98 + d < 130, so the request was AT MOST 4 TICKS (128 us) ahead.
So the driver's software state froze at t ~= 57.5 s while the hardware counter ran on to t ~= 7950 s, with channel 6 holding a compare the counter had passed two hours earlier and its interrupt enabled (we intentionally let it run for some time even when frozen).
Reproduction 2 — identified the mechanism
This run had CONFIG_NRF_GRTC_ALWAYS_ON=y (see What we ruled out):
| Register | Value | Note |
|---|---|---|
SYSCOUNTER[2].ACTIVE |
0x1 |
the keep-awake request was in effect |
CC[6].CCL |
0x01CE1AC1 |
= 30,284,993, i.e. fired at ~30.28 s uptime |
CC[6].CCH |
0x00000000 |
|
CC[6].CCEN |
0 | the compare fired |
INTEN2 bit 6 |
1 | interrupt still enabled |
INTPEND2 |
0x00 |
EVENTS_COMPARE[6] == 0 |
| SYSCOUNTER | far beyond CC[6] |
CCEN is the reading the diagnosis rests on. Its behaviour is not described in the nRF54L15 Product Specification (see question 5), so phase A0 of the reproducer measures it, on a compare channel: it writes a CC and reads CCEN, then waits for the compare and reads CCEN and EVENTS_COMPARE again. On the DK, CCEN == 1 after the write, and CCEN == 0 with EVENTS_COMPARE == 1 after the compare fires. CCEN == 0 with EVENTS_COMPARE == 0 therefore means a fired-then-destroyed compare — we cannot construct another sequence that produces that pair — and the eliminations below that depend on it stand.
Mechanism
Confirmed directly, not merely reconstructed from the end state: phase A of the attached reproducer drives the race in isolation and counts the destroyed events, and phase B drives it through the ordinary Zephyr timeout API until the tick dies.
cc_value != expired_cc on entry to sys_clock_set_timeout() means the function ran twice with no expiry in between — a timeout was aborted and rescheduled. That takes the absolute path, and because prev_cc_val - now was under LATENCY_THR_TICKS, safe_setting was selected:
/* drivers/timer/nrf_grtc_timer.c - sys_clock_set_timeout() */
if (prev_cc_val < cc_value) {
safe_setting = (int64_t)(prev_cc_val - now) < LATENCY_THR_TICKS;
}
nrfx_grtc_syscounter_cc_abs_set(ch, cc_value, safe_setting);
/* nrfx_grtc.c - the safe_setting branch */
nrfy_grtc_sys_counter_cc_set(NRF_GRTC, channel, val); /* CCEN <- 1 */
if (nrfy_grtc_sys_counter_compare_event_check(NRF_GRTC, channel)) {
now = nrfx_grtc_syscounter_get(); /* sample */
if (val > now) {
nrfy_grtc_sys_counter_compare_event_clear(NRF_GRTC, channel); /* wipe */
}
}
The clear exists to discard a stale event left by the previous CC value. Between sampling now and executing the clear, the SYSCOUNTER can cross the newly programmed val; hardware then sets EVENTS_COMPARE and clears CCEN, and the clear wipes that legitimate event along with the stale one. In run 1 both CC values were in play within ~100 us of each other (the reschedule was at most 128 us ahead), which is what makes the overlap likely rather than exotic.
Independent of which interleaving occurred, the structural defect is plain in the code: the branch never verifies, after the write, that the CC still lies ahead of the SYSCOUNTER, nor that an event it is about to clear had not just been generated legitimately. It clears unconditionally on the strength of a counter sample taken before the clear.
Why it is permanent
Once the counter is past the CC with the event flag clear, nothing regenerates it. The handler never runs, so sys_clock_announce() never runs, so sys_clock_set_timeout() is never called again — the driver never gets a chance to rewrite the CC and recover. With a 52-bit SYSCOUNTER there is no wrap-around to save it either (~142 years at 1 MHz).
This is the structural difference from nrf_rtc_timer.c on earlier parts, where a lost compare self-heals after the 512 s RTC wrap.
Standalone reproduction
Attached as GRTC-CompareLoss-Reproduction.zip:
| Item | Contents |
|---|---|
grtc_bug_reproduction/ |
standalone Zephyr/NCS application |
nrfx_grtc-clear-before-write.patch |
our fix, as quoted under Our fix |
The application needs no Bluetooth, no MPSL and none of our own firmware. It is console-only and tickless on a stock nrf54l15dk/nrf54l15/cpuapp board target, built against an unmodified NCS v3.3.0:
west -z "$ZEPHYR_BASE" build -b nrf54l15dk/nrf54l15/cpuapp -d build
west flash -d build
Console is the DK's default UART at 115200 8N1. Its README.md carries the full derivation and the expected output; CMakePresets.json has one preset per configuration. The two phases are independent, each behind its own Kconfig symbol, and both reproduce.
Phase A — the nrfx race in isolation (CONFIG_GRTC_REPRO_ISOLATED)
Calls nrfx_grtc_syscounter_cc_abs_set(ch, val, true) directly, on a channel allocated to the application with no interrupt enabled, and classifies every attempt from the hardware registers alone: event present (fine), CCEN still set after the deadline (the compare never fired — a different anomaly, counted separately), or CCEN == 0 with EVENTS_COMPARE == 0, i.e. an event the hardware generated and the driver destroyed.
Each attempt first leaves a stale event on the channel — the state the safe_setting path exists to clean up — and then programs a new CC a swept 0..31 us ahead of a freshly read SYSCOUNTER, with interrupts locked across the call so the only window in play is the one inside nrfx. The identical loop then runs again with safe_setting = false as a control. The two passes differ in nothing else. The race pass destroys events; the control pass destroys none.
That isolates the loss to the event clear in the safe_setting branch, with the Zephyr timer driver, our firmware, BLE and MPSL all out of the picture.
Phase A0 additionally settles the two hardware properties the analysis needs and the PS does not state: the CCEN state machine (see State captured at the hang), and whether a CC written into the past generates an event at all. It does.
Phase B — the tick stall through the public API (CONFIG_GRTC_REPRO_TICK_STALL)
Reproduces the user-visible symptom with nothing but k_timer and k_sleep, and ends in the field signature: CCEN = 0, EVENTS_COMPARE = 0, INTEN bit set, CC far in the past, SYSCOUNTER running on, no thread ever scheduled again.
Working out how to reach the race from the timeout API also answers why the failure is rare in the field and absent from most applications. Three constraints must hold at once:
-
nowmust be stale.now = last_count + last_elapsed, andlast_elapsedis only refreshed throughsys_clock_elapsed().kernel/timeout.c'selapsed()returns 0 whileannounce_remaining != 0, so for the whole ofsys_clock_announce()nowstays frozen at the announce base while the SYSCOUNTER runs on. Every call from thread context samples the counter first and lands a full tick ahead of it, out of reach. -
It must be the last CC write before the compare would have fired; any later write re-arms
CCENand silently repairs the damage. Sincesys_clock_announce()always ends withsys_clock_set_timeout(next_timeout(0), false), a call from a timer callback is always followed by that one and can never be fatal. The fatal call is the closing one itself. -
That closing call must take the absolute path with
safe_setting = true, which needsnext_timeout()to be larger at the closing call than it was at an abort earlier in the same announce (z_abort_timeout()being the only thing that callssys_clock_set_timeout()from a callback, sincez_add_timeout()is suppressed whileannounce_remaining != 0).
Constraint 3 is why the obvious "start two timers and abort the earlier one" pattern reproduces nothing: z_abort_timeout() removes the timeout before evaluating next_timeout(), and remove_timeout() hands the removed entry's dticks to its successor, so next_timeout() is unchanged and the closing call rewrites the same cc_value with safe_setting = false. The exception is aborting a timeout whose dticks is 0 — one sharing the currently firing deadline. It contributes nothing to its successor, next_timeout() returns 0, cyc becomes 0, and the driver's relative path forces cyc = 1: CCADD += 1 puts the CC one microsecond in the past, which the hardware turns into an EVENTS_COMPARE immediately. That is the stale event the race needs. With a second timeout also on that deadline, the announce loop pops it after the callback returns and next_timeout() then jumps up to the surviving timeout, so the closing call sees
prev_cc_val = expired_cc + 1 one microsecond above the frozen announce base
cc_value = now + N ticks now being that same frozen base
i.e. prev_cc_val < cc_value with prev_cc_val - now == 1: absolute path, safe_setting true, EVENTS_COMPARE already set, and no CC write behind it.
The application constructs that state deliberately from four k_timers (one carrying the pattern, three no-op shields of which two share its deadline), then busy-waits until the SYSCOUNTER is a swept distance short of the CC the closing call is about to program, so that the counter crosses it in the few cycles between nrfx's sample and its clear. The distance is swept across a 0..40 us window in single busy-loop iterations, subdivided by NOPs so no offset is skipped; a full sweep takes roughly half a second, which makes this a sweep rather than a lottery. A per-round self-check confirms the abort really was the dticks == 0 one, so a run that does not reproduce can be told apart from a run that never reached the right call.
Two practical notes:
-
k_uptime_get()is not a stall detector.sys_clock_tick_get()addssys_clock_elapsed(), which samples the free-running SYSCOUNTER, so uptime keeps advancing perfectly correctly right through the stall. Everything that merely reads the time looks healthy; only the fact that no callback runs any more exposes the failure. Phase B therefore detects the stall from a watchdog on a private GRTC channel, armed throughz_nrf_grtc_timer_set()— which routes to the race-freenrfx_grtc_syscounter_cc_absolute_set(), so the watchdog cannot lose its own event — and reports from its ISR, since by then no thread will ever run again. -
Both phases require a stock
nrfx_grtc.c, and phase B additionally a stockdrivers/timer/nrf_grtc_timer.c— our fix removes the race outright, so there is nothing left to observe. The build warns at configure time if it detects either patch in the workspace.
What we ruled out
Software suppression of the interrupt
- Fatal error / assert halt —
arch_system_halt()spins with interrupts locked, which would show non-zeroBASEPRIand a PC inside the halt loop.BASEPRIis 0 and the CPU is in the idle thread. - Interrupts masked by a leaked lock —
BASEPRI0;PRIMASK1 is explained byarch_cpu_idle();VECTACTIVE0. - Stuck or starving ISR —
VECTACTIVE0, thread mode. - Channel interrupt disabled by
compare_int_lock()—INTEN2 = 0x40, andint_mask = 0x67agrees that all allowed channels are enabled. - NVIC interrupt disabled —
ISER[7] = 0x30, IRQ 228 enabled. - Empty timeout queue (legitimate tickless idle) — the application always has pending timeouts: a 250 ms watchdog tick timer and a 500 ms main-loop sleep.
SYSCOUNTER sleep / CC latch latency — ruled out by experiment
CONFIG_NRF_GRTC_ALWAYS_ON=y (keeping the SYSCOUNTER continuously active, so a CC write can never race a sleep/wake transition) does not prevent the hang: reproduction 2 ran with it enabled, and the hang still reproduced, with SYSCOUNTER[2].ACTIVE = 0x1 confirming the keep-awake request was in effect throughout.
Note the correct register: nrfx_grtc_active_request_set() writes GRTC_SYSCOUNTER.ACTIVE, which is SYSCOUNTER[NRF_GRTC_DOMAIN_INDEX] = SYSCOUNTER[GRTC_IRQ_GROUP] = SYSCOUNTER[2] on secure cpuapp. SYSCOUNTER[0] belongs to another domain and reads 0 regardless of this setting.
This is consistent with the actual mechanism: the race is between the CPU and the free-running counter, so counter sleep/wake latency is irrelevant to it.
CC written into the past, or lost in the CCL -> CCH write window — ruled out by the CCEN reading
Both are candidate mechanisms by which the compare might never fire at all, and both are excluded by CC[6].CCEN = 0: a compare that never matched would leave CCEN at 1 (the CCEN state machine as measured by phase A0). The past-CC candidate is doubly excluded: A0 also shows the hardware raises EVENTS_COMPARE for a CC written into the past, so that is not a way for a compare to go missing.
Other channel owners disabling CCEN on channel 6 — excluded by inspection of the source for this build configuration
nrf_sys_event—CONFIG_NRF_SYS_EVENTis not set, soUSE_SYS_EVENTis undefined and the GRTC-channel path insoc/nordic/common/nrf_sys_event.cis compiled out.- The
CONFIG_POWEROFFchannel sweep inz_nrf_grtc_wakeup_prepare(), which disables every allowed channel except its own —CONFIG_POWEROFFis not set. - The 802.15.4 platform glue, which calls
z_nrf_grtc_timer_abort()—CONFIG_NRF_802154_RADIO_DRIVERis not set. - The Zephyr LL's
radio.c/cntr.c— this build usesCONFIG_BT_LL_SOFTDEVICE, and those files use channels 10/11 in any case. nrfx_grtc_uninit()viasys_clock_disable()— it would leaveSYSCOUNTER[2].ACTIVE = 0, which reads 1.
MPSL interference — only partly excluded
The pending IRQ 229 is enabled and serviced normally, its channels 8/9 are in MPSL's reserved block, and CONFIG_BT_UNINIT_MPSL_ON_DISABLE is not set, so bt_disable() does not tear down MPSL's IRQ. But MPSL ships as a binary library, so unlike the cases above we cannot inspect it to confirm it never touches channel 6 or the group-2 registers. We have no positive evidence that it does; we simply cannot rule it out the way we ruled out the others, and the correlation with BLE traffic is the one thing that would be equally well explained by MPSL involvement. If you can confirm from the MPSL side that channel 6 is untouched, that would close the last alternative we are aware of.
The reproducer has no Bluetooth and no MPSL in the image, so MPSL is not needed to explain the failure — it remains possible as a second contributor to the field failures, but not as an alternative explanation.
Silicon anomalies
- L15_22 / L15_24 / L15_25 — these cover
EVENTS_COMPARE[n]on the TIMER peripheral, not the GRTC (per Nordic in DevZone 127458). - L15_30 ("CLOCK, GRTC: GRTC operates incorrectly at low temperature") — describes GRTC frequency drift when the system runs on HFINT. The failure here is a single lost compare event, not a drifting time base. Please confirm this is not a contributing factor.
Open points
Collected here rather than left buried in the prose above, so you can see exactly where the analysis is load-bearing and where it is not.
-
We did not measure the width of the window between the counter sample and the event clear, and cannot bound it from the documentation. The reproducer sweeps its offset until it hits the window instead of measuring it. What we can say is that phase A locks interrupts across the call, so the window is wide enough to lose events with nothing widening it.
-
MPSL cannot be inspected, so it alone is excluded by inference rather than by reading the code. Not being needed to reproduce the defect does not close it out as a second contributor in the field.
-
The two field captures are from different runs, so for the field failures "the CC was too close AND the event was lost" still joins two observations by argument. In the reproducer both halves are present in a single run.
-
Whether the interleaving the reproducer forces is the one that fires in the field. It is the same mechanism ending in the same register state, but the field path is necessarily inferred: our firmware does not construct that timer pattern deliberately, it arrives there under BLE load.
-
Whether the retry in our fix is necessary. That a spurious COMPARE event on this path would be misinterpreted is read off Zephyr's
sys_clock_timeout_handler()(see Our fix); we have not provoked one and watched the clock jump. The retry costs nothing when no event is pending, so we kept it either way.
Our fix
Applied locally as a patch to modules/hal/nordic/nrfx/drivers/src/nrfx_grtc.c. It supersedes a stopgap of ours in drivers/timer/nrf_grtc_timer.c, which detected the lost event after the fact (CCEN == 0 with EVENTS_COMPARE == 0) and re-raised it; that one recovered the system clock only, whereas the race is in nrfx and reaches every caller of nrfx_grtc_syscounter_cc_abs_set().
Status: the hang stopped reproducing under the BLE test load that previously triggered it within minutes to a couple of hours, first under that stopgap and since under this fix, and neither phase of the reproducer reproduces against it. The soak on this fix is still running; given the fault was intermittent we will report back if it recurs.
if (safe_setting)
{
bool retry;
do
{
nrfy_grtc_sys_counter_compare_event_clear(NRF_GRTC, channel);
nrfy_grtc_sys_counter_cc_set(NRF_GRTC, channel, val);
retry = nrfy_grtc_sys_counter_compare_event_check(NRF_GRTC, channel) &&
(val > nrfx_grtc_syscounter_get());
} while (retry);
}
Clearing before arming is the ordering nrfx_grtc_syscounter_cc_absolute_set() already uses, and it is what removes the defect: no clear can follow the new CC going live, so no event the new CC generates can be destroyed. It also needs no assumption about hardware timing.
The loop is there because the reordering alone would trade the lost event for a spurious one, and for this caller that is not a benign trade. The previous CC is still armed while the clear runs — that is precisely the situation safe_setting exists for — so it can expire between the clear and the CC write and leave behind an event that does not belong to the new CC. The nrfx ISR passes the handler m_cb.cc_value[], which this function has already advanced to the new value, so sys_clock_timeout_handler() would announce the whole new timeout immediately and set last_count past the SYSCOUNTER, after which counter_sub() in sys_clock_elapsed() underflows. A lost compare stops the clock; an unrepaired spurious one throws it forward.
The loop resolves that without reopening the original window:
- An event present after the CC write is either generated by the new CC —
valis already in the past, the event is due, keep it — or left by the previous CC. - The two are told apart by reading the SYSCOUNTER after the event check, not before:
val > nowthen proves the counter had not reachedvalat the moment the event was observed, so that event cannot belong to the new CC and is safe to clear. This is the ordering the original branch got wrong. - Every clear is immediately followed by a CC write that re-arms the channel, so no iteration can leave a fired compare without an event — the state that stalls the clock is unreachable from inside the loop.
- It terminates: after the first CC write only
valitself can generate an event, and such an event impliesnow >= valon the next pass.
Cost, on the path where nothing is pending, is one extra register write — the clear — and no SYSCOUNTER read, so the optimisation the safe_setting path exists for is kept. We did not add a critical section around the clear/write pair, since the function is documented as "must be called with interrupts locked"; nrfx_grtc_syscounter_cc_absolute_set() takes one, and if that is deliberate rather than defensive the same should be done here.
The alternative we did not take: phase A0 confirms the hardware raises EVENTS_COMPARE for a CC written into the past, so a lost compare can also be recovered by rewriting the CC. That is still a recovery after the fact, and it rests on a hardware property we measured on one DK rather than one that is documented, so we preferred the ordering fix.
Upstream status (checked 2026-09-08)
Zephyr
Upstream main still has the same call into the safe_setting path and no post-write verification. Commits to drivers/timer/nrf_grtc_timer.c after the revision in NCS v3.3.0:
| Commit | Date | Change |
|---|---|---|
f4a2a270715 |
2026-06-10 | kernel: timeout: make the system clock tick interface unsigned -> sys_clock_set_timeout(uint32_t ticks, ...) |
48f35d88c68 |
2026-06-23 | drivers: timer: clean up tick clamps for the unsigned interface -> for GRTC, only an (int32_t) cast removed |
aad2c65d69e |
2026-07-31 | release LFCLK before GRTC uninit |
839c682963f |
2026-08-21 | __ASSERT_ON migration |
None touches the compare path.
nrfx
NCS v3.3.0 ships nrfx 4.2.1. Upstream master is at 4.5.0 (2026-07-23), three releases newer. The GRTC entries in between are:
| Release | GRTC entry |
|---|---|
| 4.3.0 | "Fixed extended channel mask definition for devices supporting MINTERVAL feature in the GRTC driver." — unrelated, build-time mask definition |
| 4.4.0 | (no GRTC changes) |
| 4.5.0 | (no GRTC changes) |
More directly: nrfx_grtc_syscounter_cc_abs_set() on nrfx master is byte-for-byte identical to the 4.2.1 copy quoted above. The safe_setting branch is unchanged. Upgrading nrfx will not fix this.
History
-
nrfx 3.12.0 (2025-06-02) — "Added functions for setting compare value in an optimized way in the GRTC driver." This is where
nrfx_grtc_syscounter_cc_abs_set()first appears, with thesafe_settingbranch as quoted. -
Zephyr
af3c8b973ed(2024-05-22) — "drivers: timer: nrf_grtc_timer: Fix for random system hangs" restored an early return insys_clock_timeout_handler(), noting that without it "some stress tests which were using system timers heavily started to hang". -
Zephyr
d6fb9384ebf(2025-03-31) — "drivers: timer: nrf_grtc_timer: Optimize to reduce register access" introducedcc_value,expired_cc,LATENCY_THR_TICKSand thesafe_settingpath. Its commit message names the exact scenario that fails here:
If value in CC is earlier than the new CC value (if earlier timeout was aborted) then there is a risk of COMPARE event happening while it is being overwritten. That case requires long and safer procedure of setting CC.
The safer procedure prevents a spurious event. Nothing on either side prevents or detects a lost one.
The closest public report, DevZone 127458 ("NRF54L15 Timer interrupt not generating after few cycles"), describes the same shape and remains unanswered with no root cause.
Questions
-
Is the read-modify-write in the
safe_settingbranch ofnrfx_grtc_syscounter_cc_abs_set()considered safe as written? It samples the SYSCOUNTER, decides, and then clears an event register that the hardware can set in between. Phase A settles the empirical half, so what is left is what nrfx intends here. Specifically: is the clear-before-write ordering under Our fix — the onenrfx_grtc_syscounter_cc_absolute_set()already uses, plus the retry that repairs an event left by the previous CC — acceptable as a fix, and is our reading correct that the retry is needed, i.e. that a spurious event on this path would be misinterpreted by the Zephyr system clock rather than tolerated? -
Is there a documented minimum distance between a CC write and the SYSCOUNTER value at which the compare is guaranteed to fire?
nrf_rtc_timer.ccites such a figure for RTC ("when the current counter value is N, a value of N+2 written to the CC register is guaranteed to trigger a COMPARE event at N+2"), and that driver additionally loops, re-reading the counter and re-arming until the CC verifiably lands in the future. We can find no equivalent statement for GRTC — the GRTC chapter of the nRF54L15 Product Specification resolves to register and SPU navigation only, with no functional description of compare-event generation or CC write latching. Phases A0 and B0 measure that a CC written into the past raisesEVENTS_COMPAREanyway (including via theCCADD += 1form the Zephyr driver emits forcyc == 0), but that is a measurement on one DK rather than a guarantee we can rely on. -
Is
LATENCY_THR_TICKS = 200intended to bound this case? Despite the name it is compared against a cycle difference (prev_cc_val - now, in 1 MHz units), so it is a 200 us threshold, not 200 ticks (6.4 ms). Intended? -
Should
sys_clock_set_timeout()enforce a minimum forward distance on the absolute path? It currently computesnow = last_count + last_elapsed, a software estimate rather than a hardware read, and on the path out ofsys_clock_announce()that value is the expiry time of the timeout that just fired — stale by the interrupt latency plus every callback the announce loop ran. Combined withticks == 0being reachable,cc_value == nowis expressible. The pre-d6fb9384ebfcode read the counter at the point of the write and applied aMAX(..., 1)floor; both were removed. -
Can the GRTC's
CCENbehaviour be documented, or at least confirmed? The diagnosis above, and several of the eliminations, rest on it. Neither it, nor CC write latching, nor compare-event generation appears in the nRF54L15 Product Specification, and a measurement on a single DK is a thin foundation for a state machine.