nRF54L15 hangs in MCUBoot

On nRF54L15, MCUboot prints Jumping to the first image slot and then spins forever, never starting the application:

sys_clock_disable() switches the GRTC SYSCOUNTER off in nrfx_grtc_uninit() and only then releases the LFCLK, which aborts the LFCLK RC-calibration k_timer and makes z_abort_timeout() read the counter that was just stopped — an unbounded retry loop on SYSCOUNTERH.BUSY that nothing can terminate, since only TASKS_START restarts a stopped counter.

It takes three prerequisites:

  • no 32.768 kHz crystal (LFXO disabled, so the LF clock runs on the internal RC with CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC_CALIBRATION=y)
  • CONFIG_MULTITHREADING=y in the MCUboot image, and the calibration backoff timer being the nearest pending kernel timeout
  • which we force with CONFIG_TIMESLICE_SIZE=0, since z_abort_timeout() only consults the clock when the aborted timeout is first in the list

Reproduced on a stock nRF54L15 DK under nRF Connect SDK v3.4.0 (Zephyr 4.4.0) with a minimal sysbuild project: it hangs on the very first boot after a plain pin reset (RESETREAS = 0x1, MODE = 0, SYSCOUNTER frozen at 97.9 ms), and because a watchdog reset re-enters the identical code path, a watchdog bite freezes the unit instead of recovering it.

nrf54l15-wdt-grtc-repro.zip

<h1>nRF54L15: MCUboot hangs forever in <code>sys_clock_disable()</code> &mdash; unbounded GRTC SYSCOUNTER read after <code>nrfx_grtc_uninit()</code></h1>

<p><strong>Attachment:</strong> <code>nrf54l15-wdt-grtc-repro.zip</code> (sysbuild project, builds and reproduces on a stock nRF54L15 DK)</p>

<h2>Environment</h2>

<table border="1" cellpadding="6" cellspacing="0">
  <tbody>
    <tr>
      <td><strong>SDK</strong></td>
      <td>nRF Connect SDK v3.4.0 (Zephyr v4.4.0, MCUboot 2.3.0-dev)</td>
    </tr>
    <tr>
      <td><strong>Toolchain</strong></td>
      <td>NCS toolchain bundle for v3.4.0</td>
    </tr>
    <tr>
      <td><strong>Boards</strong></td>
      <td><code>nrf54l15dk/nrf54l15/cpuapp</code> (PCA10156), silicon <code>NRF54L15_xxAA_REV1</code></td>
    </tr>
    <tr>
      <td></td>
      <td>Ezurio BL54L15u DVK, silicon <code>NRF54L15_xxAA_ENGB</code></td>
    </tr>
    <tr>
      <td></td>
      <td>custom board with the Ezurio BL54L15u module (453-00223), silicon <code>NRF54L15_xxAA_REV1</code></td>
    </tr>
    <tr>
      <td><strong>Reproduces</strong></td>
      <td>on all three, identically</td>
    </tr>
  </tbody>
</table>

<h2>Summary</h2>

<p><code>sys_clock_disable()</code> in <code>zephyr/drivers/timer/nrf_grtc_timer.c</code> switches the GRTC SYSCOUNTER off <strong>before</strong> it releases the LFCLK. Releasing the LFCLK synchronously aborts the LFCLK RC-calibration backoff <code>k_timer</code>, and aborting a <code>k_timer</code> makes the kernel ask the system clock what time it is. That read targets the counter that was just switched off, returns <code>SYSCOUNTERH.BUSY = 1</code> forever, and the driver's retry loop is unbounded &mdash; so MCUboot spins in it and never chain-loads the application. No reset recovers the device: the next boot runs the same code and wedges in the same place.</p>

<h2>What the user sees</h2>

<p>MCUboot prints its full log, ending in</p>

<pre><code>I: Jumping to the first image slot</code></pre>

<p>and then nothing. The application never starts. In the field this is triggered by a watchdog reset, which turns the watchdog from a recovery mechanism into a brick: every bite parks the unit in the bootloader until someone power-cycles it &mdash; and the power cycle does not help either once the build is affected.</p>

<h2>Root cause</h2>

<p><code>zephyr/drivers/timer/nrf_grtc_timer.c:510</code>:</p>

<pre><code>void sys_clock_disable(void)
{
    nrfx_grtc_uninit();          /* 1: clears MODE.SYSCOUNTEREN -&gt; counter off */
#if defined(CONFIG_CLOCK_CONTROL_NRF)
    int err;
    struct onoff_manager *mgr =
        z_nrf_clock_control_get_onoff((clock_control_subsys_t)CLOCK_CONTROL_NRF_TYPE_LFCLK);

    err = onoff_release(mgr);    /* 2: releases the LFCLK */
    __ASSERT_NO_MSG(err &gt;= 0);

    nrfx_coredep_delay_us(1000);
#endif
}</code></pre>

<p>Step 1: <code>nrfx_grtc_uninit()</code> calls <code>nrfy_grtc_sys_counter_set(NRF_GRTC, false)</code>, which clears <code>MODE.SYSCOUNTEREN</code>. The counter stops. (MCUboot already gets <code>CONFIG_NRF_GRTC_TIMER_STOP_AT_UNINIT=n</code> &mdash; Zephyr defaults it to <code>y if !MCUBOOT</code> &mdash; so no <code>TASKS_STOP</code> is issued, but that mitigation is ineffective: clearing MODE stops the counter just as well.)</p>

<p>Step 2 then re-enters the kernel:</p>

<pre><code>onoff_release                              lib/utils/onoff.c:501
  -&gt; onoff_stop                            drivers/clock_control/clock_control_nrf.c:548
  -&gt; lfclk_stop
  -&gt; z_nrf_clock_calibration_lfclk_stopped()
  -&gt; k_timer_stop(&amp;backoff_timer)          drivers/clock_control/nrf_clock_calibration.c:268
  -&gt; z_impl_k_timer_stop                   kernel/timer.c:250
  -&gt; z_abort_timeout                       kernel/timeout.c:159
  -&gt; elapsed()                             kernel/timeout.c:70
  -&gt; sys_clock_elapsed()                   drivers/timer/nrf_grtc_timer.c:496
  -&gt; counter() -&gt; nrfx_grtc_syscounter_get()
  -&gt; nrfy_grtc_sys_counter_get()           modules/hal/nordic/nrfx/haly/nrfy_grtc.h:329</code></pre>

<p>and the loop at the end of that chain has no bound:</p>

<pre><code>do {
    counter = nrf_grtc_sys_counter_get(p_reg);
} while (counter &amp; NRFY_GRTC_SYSCOUNTER_RETRY_MASK);   /* BUSY | OVERFLOW */</code></pre>

<p>A sleeping SYSCOUNTER clears BUSY within about one 32&nbsp;kHz cycle. A <em>disabled</em> one never does, so this spins forever.</p>

<h2>Preconditions</h2>

<ol>
  <li><code>CONFIG_MULTITHREADING=y</code> in the MCUboot image. MCUboot defaults it to <code>n</code>, but real bootloaders enable it &mdash; most commonly because the Zephyr SPI NOR driver requires it when the secondary slot is on external flash. The flash driver itself is not involved; the hang reproduces with both slots in internal RRAM and no SPI at all.</li>
  <li>LFCLK from the internal RC with calibration enabled (<code>CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC_CALIBRATION</code>, default <code>y</code>) &mdash; i.e. any board without a 32.768&nbsp;kHz crystal. This is what puts a <code>k_timer</code> in <code>sys_clock_disable()</code>'s path.</li>
  <li>The first LFRC calibration completes within MCUboot's lifetime, so the backoff <code>k_timer</code> is armed when <code>do_boot()</code> runs. <strong>Measured to be true on all three boards above</strong> &mdash; MCUboot's lifetime is ~98&nbsp;ms on the DK and DVK, ~51&nbsp;ms on the custom board, and the calibration completes well inside that.</li>
  <li>The backoff timer is the <strong>first</strong> (nearest-expiring) entry in the kernel timeout list. <code>z_abort_timeout()</code> only consults the clock on the <code>is_first</code> branch:
<pre><code>if (sys_dnode_is_linked(&amp;to-&gt;node)) {
        bool is_first = (to == first());
        remove_timeout(to);
        to-&gt;dticks = TIMEOUT_DTICKS_ABORTED;
        ret = 0;
        if (is_first) {
                sys_clock_set_timeout(next_timeout(elapsed()), false);
        }
}</code></pre>
  </li>
</ol>

<p>Condition 4 is what decides whether a given build is affected, and it is invisible in any configuration review. With MCUboot's stock <code>CONFIG_TIMESLICE_SIZE=20</code> the time-slice timeout is always nearer than the 4000&nbsp;ms (<code>CONFIG_CLOCK_CONTROL_NRF_CALIBRATION_PERIOD</code>) backoff timer, so the read never happens. Remove the slice timeout and the hang is immediate and total.</p>

<h2>Minimal reproduction (stock nRF54L15 DK, no patches)</h2>

<p>From the attached project:</p>

<pre><code>west build -d build -p -b nrf54l15dk/nrf54l15/cpuapp --sysbuild
west flash -d build</code></pre>

<p>Expected: the application banner appears after each reset.<br>
Actual: MCUboot prints <code>Jumping to the first image slot</code> and the board is dead &mdash; on the very first boot, and after every subsequent reset of any kind.</p>

<p>The project is deliberately minimal. The application arms <code>watchdog0</code>, feeds it six times, stops feeding, and prints the reset cause on each boot. The only board overlay disables the LFXO, which is what forces RC + calibration as on crystal-less hardware. <code>sysbuild/mcuboot.conf</code> sets exactly three things that matter:</p>

<pre><code>CONFIG_MULTITHREADING=y     # what a real bootloader with external-flash DFU has
CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC=y
CONFIG_TIMESLICE_SIZE=0     # makes the backoff timer first in the timeout list</code></pre>

<p>Comment out <code>CONFIG_TIMESLICE_SIZE=0</code> and rebuild to get the non-hanging baseline; nothing else about the build changes. That single line is the whole difference between "boots forever" and "never boots again".</p>

<p><strong>The watchdog is not required.</strong> The hang occurs on the very first boot after a plain pin reset (<code>RESETREAS = 0x1</code>), with no application banner at all &mdash; so the application never reached <code>wdt_setup()</code> and the watchdog was never armed. The watchdog is in the reproducer only because it demonstrates why this matters in the field: a reset that was supposed to recover the unit re-enters the identical code path.</p>

<p>Console note: <code>printk</code> output is on the DK's <strong>second</strong> VCOM port (vcom 1), 115200 8N1.</p>

<h2>Forensics on the hung board (SWD, <code>nrfutil device</code>)</h2>

<table border="1" cellpadding="6" cellspacing="0">
  <tbody>
    <tr>
      <td><strong>PC</strong></td>
      <td>in the inlined <code>elapsed()</code> &rarr; <code>sys_clock_elapsed()</code> &rarr; <code>counter()</code> &rarr; <code>nrfx_grtc_syscounter_get()</code> &rarr; <code>nrfy_grtc_sys_counter_get()</code> BUSY retry loop</td>
    </tr>
    <tr>
      <td><strong><code>GRTC.MODE</code></strong> (<code>0x500E2510</code>)</td>
      <td><code>0x00000000</code> &mdash; SYSCOUNTEREN and AUTOEN both cleared</td>
    </tr>
    <tr>
      <td><strong><code>SYSCOUNTERH</code></strong> (<code>0x500E2744</code>)</td>
      <td><code>0x40000000</code> &mdash; BUSY set, permanently</td>
    </tr>
    <tr>
      <td><strong><code>SYSCOUNTERL</code></strong> (<code>0x500E2740</code>)</td>
      <td>frozen at 97.9&nbsp;ms (DK) / 98.1&nbsp;ms (DVK) / 50.6&nbsp;ms (custom board) &mdash; i.e. exactly MCUboot's lifetime; the counter ran normally all through boot and stopped at the uninit</td>
    </tr>
    <tr>
      <td><strong><code>RESETREAS</code></strong> (<code>0x5010E600</code>)</td>
      <td><code>0x1</code> &mdash; plain <strong>pin</strong> reset</td>
    </tr>
  </tbody>
</table>

<p><em>(Addresses are for the application core in secure mode: <code>SYSCOUNTER[2]</code>, domain index <code>GRTC_IRQ_GROUP = 2</code>. The SYSCOUNTER runs at 1&nbsp;MHz, so its raw value is microseconds.)</em></p>

<h2>What does and does not revive the counter</h2>

<p>The nRF54L15 datasheet, <em>GRTC &gt; SYSCOUNTER &gt; Recommendation on reading SYSCOUNTER</em>, specifies four steps: set <code>SYSCOUNTER[m].ACTIVE</code>, wait until <code>SYSCOUNTERH.BUSY</code> is cleared, read <code>SYSCOUNTERL/H</code>, then clear <code>ACTIVE</code>. <code>nrfy_grtc_sys_counter_get()</code> implements steps 2 and 3 only &mdash; it never touches <code>ACTIVE</code> &mdash; and <code>nrfx_grtc_uninit()</code> separately clears <code>MODE.AUTOEN</code>, <code>MODE.SYSCOUNTEREN</code> and <code>ACTIVE</code> (the last via <code>nrfy_grtc_sys_counter_active_set(NRF_GRTC, false)</code>). Read back on the hung DK:</p>

<pre><code>GRTC.MODE                 0x500E2510 = 0x00000000   AUTOEN = 0, SYSCOUNTEREN = 0
SYSCOUNTER[2].ACTIVE      0x500E2748 = 0x00000000
SYSCOUNTER[2].SYSCOUNTERH 0x500E2744 = 0x40000000   BUSY = 1, forever
GRTC.KEEPRUNNING          0x500E2508 = 0x00000000</code></pre>

<p>It is tempting to conclude that the skipped step 1 is the cause. It is not &mdash; I tested that on the hung board rather than assuming it, poking one register at a time:</p>

<table border="1" cellpadding="6" cellspacing="0">
  <thead>
    <tr><th>Poked over SWD on the hung board</th><th>BUSY</th><th>SYSCOUNTER</th></tr>
  </thead>
  <tbody>
    <tr><td>nothing (baseline)</td><td>set</td><td>frozen</td></tr>
    <tr><td><code>ACTIVE = 1</code> (datasheet step 1)</td><td>still set</td><td>still frozen</td></tr>
    <tr><td><code>ACTIVE = 1</code> + <code>MODE = 0x3</code></td><td>still set</td><td>still frozen</td></tr>
    <tr><td>&hellip; + <code>TASKS_START</code> (<code>0x500E2060</code>)</td><td><strong>clears</strong></td><td><strong>runs</strong></td></tr>
  </tbody>
</table>

<p>So the missing steps are a real deviation from the documented procedure, but implementing them would not have prevented this hang. Once the counter has been switched off, only <code>TASKS_START</code> revives it &mdash; which is why the ordering fix, not the read procedure, is the substantive one. The datasheet's remark on the same page that "the internal low frequency timer must be started for the proper operation of the SYSCOUNTER" points the same way: <code>sys_clock_disable()</code> tears that down while a clock read is still reachable.</p>

<p>Incidentally, poking <code>TASKS_START</code> on the hung board resumes the boot: MCUboot's pending read completes, it chainloads the application, the application arms and then starves the watchdog, and the resulting reset lands back in the identical hang &mdash; <code>RESETREAS = 0x4</code> (DOG1), counter frozen at 97.9&nbsp;ms again. That is the field failure loop in full.</p>

<h2>Bounded probe: the defective read is universal</h2>

<p><code>mcuboot-grtc-read-probe.patch</code> in the attachment (against <code>bootloader/mcuboot/boot/zephyr/main.c</code>) performs the same SYSCOUNTER read immediately after <code>sys_clock_disable()</code>, but with a 10,000,000-iteration bound and a park loop instead of a silent hang. On all three boards, after a pin reset:</p>

<pre><code>grtc_probe_mode  = 0x00000000    /* MODE cleared by nrfx_grtc_uninit()  */
grtc_probe_tries = 10000000      /* bound exhausted                     */
grtc_probe_sch   = 0x40000000    /* BUSY still set                      */</code></pre>

<p>This isolates the defect from the timeout-ordering question entirely: <strong>any</strong> read of the SYSCOUNTER after <code>nrfx_grtc_uninit()</code> hangs, on every board tested, regardless of silicon revision or reset type.</p>

<h2>Why applications appear immune</h2>

<p>The application's own <code>sys_clock_driver_init()</code> &rarr; <code>nrfx_grtc_init()</code> &rarr; <code>nrfy_grtc_prepare()</code> triggers <code>TASKS_START</code> unconditionally, repairing the counter before any application code can read it. Only code that reads the system clock <em>between</em> <code>sys_clock_disable()</code> and the next <code>TASKS_START</code> is exposed &mdash; in practice, MCUboot's own cleanup.</p>

<h2>Workarounds (verified against the deterministic case above)</h2>

<p>Either of these in the MCUboot image config makes the board boot cleanly through every reset instead of hanging on the first one:</p>

<pre><code>CONFIG_CLOCK_CONTROL_NRF_K32SRC_RC_CALIBRATION=n   # preferred</code></pre>

<p>or <code>CONFIG_MULTITHREADING=n</code>, where the bootloader permits it (not an option with the secondary slot on SPI flash).</p>

<p>Neither is a fix &mdash; they remove the <em>caller</em>, not the unbounded read. Any other kernel timeout aborted after <code>nrfx_grtc_uninit()</code> hits the same wall.</p>

<h2>Suggested fix</h2>

<ol>
  <li><strong>Ordering (the substantive one).</strong> In <code>sys_clock_disable()</code>, release the LFCLK <em>before</em> calling <code>nrfx_grtc_uninit()</code>. That stops the calibration timer while the SYSCOUNTER is still running, so the abort's clock read succeeds normally.</li>
  <li><strong>Fail-safe read (containment).</strong> Make the GRTC read path bound its retry or early-out once the driver is uninitialized (<code>nrfx_grtc_init_check()</code> already exists). A peripheral read that can spin forever on a register bit is a hazard regardless of who calls it &mdash; see case 126653 below, where the same loop wedges an application at <code>PRE_KERNEL_1</code> for an entirely different reason. As measured above, no read-side change can obtain a value from a counter that has been switched off, so this converts a silent hang into a detectable error rather than preventing it.</li>
</ol>

<p>The existing <code>CONFIG_NRF_GRTC_TIMER_STOP_AT_UNINIT=n</code> default for MCUboot suggests the ordering hazard was already noticed, but it is not sufficient, because <code>nrfx_grtc_uninit()</code> clears <code>MODE.SYSCOUNTEREN</code> regardless of that option.</p>

<h2>Related DevZone cases</h2>

<p>Checked against the current threads; none is a duplicate of this report, but two are adjacent enough to be worth pre-empting.</p>

<ul>
  <li><strong>124602</strong> &mdash; "GRTC resets after <code>sys_reboot()</code> when using MCUboot (NCS v3.1.0)". Different symptom (the counter <em>value</em> being cleared, not a hang), same uninit code path. The fix from that thread is present in v3.4.0 as <code>CONFIG_NRF_GRTC_TIMER_STOP_AT_UNINIT</code> / <code>..._CLEAR_AT_UNINIT</code> defaulting to <code>y if !MCUBOOT</code>, and my MCUboot build has both at <code>n</code>. <strong>It does not prevent this hang</strong>, because <code>nrfx_grtc_uninit()</code> stops the counter by clearing <code>MODE.SYSCOUNTEREN</code> whether or not <code>TASKS_STOP</code> is issued. Please do not close this as covered by that fix.</li>
  <li><strong>126653</strong> &mdash; "Getting internal clocks to run on nRF54L15". Different trigger, <em>same unbounded loop</em>: the reporter wedges in <code>nrfx_grtc_syscounter_get()</code> at <code>nrfx_grtc.c:282</code> during <code>PRE_KERNEL_1</code> device init, reached via <code>sys_clock_cycle_get_32()</code> from <code>z_log_msg_commit()</code> inside <code>psa_crypto_init()</code> &mdash; i.e. an application reading the SYSCOUNTER before the clock is up rather than after it is torn down. Evidence that the unbounded retry is a general hazard with more than one caller, which is the argument for fix 2.</li>
  <li><strong>124294</strong> &mdash; "Watchdog timer resets my GRTC". Nordic confirmed the datasheet reset table is wrong: "the WDT is indeed supposed to reset the GRTC, so what you're seeing is the intended behavior. We will update the datasheet to reflect this as soon as we can." Note that the reset overview table in the current nRF54L15 datasheet still shows no <code>x</code> in the <code>GRTC.SYSCOUNTER</code> column for "Watchdog timer reset", so that correction does not appear to have landed yet. Unrelated to this hang, but it misleads anyone reasoning about GRTC state across a watchdog reset.</li>
  <li><strong>124693</strong> &mdash; "mcu hang at <code>nrf_grtc_sys_counter_low_get</code>". Superficially similar, but <em>not</em> this bug: NCS 3.0.2, no MCUboot involved, and the reporter had selected <code>CONFIG_CLOCK_CONTROL_NRF_K32SRC_SYNTH=y</code>, which depends on the HFXO. Nordic diagnosed a 32&nbsp;MHz crystal start-up problem on that board. Listed here only so it is not mistaken for a prior report of this issue.</li>
</ul>

<h2>Note on the attachment</h2>

<p>The zip contains board overlays for <code>nrf54l15dk</code>, <code>bl54l15u_dvk</code> and <code>bl54l15_dvk</code> only. The third board in the tables above is a private custom design; its board files are deliberately not included and nothing in the project depends on them. The <code>nrf54l15dk/nrf54l15/cpuapp</code> target reproduces everything on stock hardware.</p>

Related