AT+CFUN=20 (lte_lc_func_mode_set DEACTIVATE_LTE) blocks indefinitely under registration churn — software deadlock ruled out (nRF9151, mfw 2.0.4, NCS 3.3.0)

Environment

  • nRF9151 (Thingy:91X), modem firmware mfw_nrf91x1_2.0.4
  • nRF Connect SDK v3.3.0 (Zephyr 4.3.99)
  • LTE-M + GNSS: CONFIG_LTE_NETWORK_MODE_LTE_M_GPS=y
  • PSM requested (RPTAU 1800s, RAT 20s), no eDRX, no RAI, default GNSS use case, prio_mode not used

What the application does
Periodically I take the modem LTE-dark while keeping GNSS running (geofence-triggered "radio silent" state): close the MQTT/TLS socket, then call lte_lc_func_mode_set(LTE_LC_FUNC_MODE_DEACTIVATE_LTE). Later it reactivates LTE.

Problem
Intermittently, lte_lc_func_mode_set(DEACTIVATE_LTE) never returns — execution blocks inside the call, and ~58s later the watchdog resets the device. It succeeds most of the time. The hang correlates with live network conditions (areas with registration churn) and does not reliably reproduce stationary on a stable cell.

Software-level deadlock ruled out
I want to lead with what I've eliminated, since it's the obvious first suspect:

  • lte_lc_func_mode_set(DEACTIVATE_LTE) is called from my application main thread (a supervisor poll loop) — notfrom within the lte_lc event handler, and not from any nrf_modem_gnss callback or other modem-library thread context.
  • My lte_lc event handler (registered via lte_lc_connect_async) does no blocking work — it only sets atomics and posts a semaphore on NW_REG_STATUS / RRC_UPDATE. It never calls back into the modem mode-set API.
  • Geofence crossing is detected in the GNSS callback but only sets atomic flags; the actual deactivate is polled and executed on the main thread, fully decoupled from any callback context.

So the modem RX thread is free to deliver the AT+CFUN=20 response — a classic "blocking call from the RX/callback context deadlocks the modem library" scenario does not apply here structurally.

Log around the failure (device-uptime timestamps in brackets):

[00:14:02.654] telemetry: MQTT/TLS closed before radio-off
[00:14:02.654] app: rrc_idle=1 quiet_ms=110847, deactivating LTE (GNSS stays on)
[00:14:02.6xx] app: about to call lte_lc_func_mode_set(DEACTIVATE_LTE)   (main thread)
+CEREG: 1,"1405","09EE3D0F",7,,,"00001010","11100000"
*** Booting nRF Connect SDK v3.3.0 ***          <-- ~58s later, watchdog reset

The landmark that should print immediately after lte_lc_func_mode_set returns never appears — the next log line is the reboot banner. RRC was idle and the radio had been quiet for 110+ seconds before the call, so this is not residual data activity.

Current hypothesis (modem-firmware level)
With the software deadlock ruled out, my leading theory is that the block is inside the modem firmware: when AT+CFUN=20 arrives while the network is mid-(re)registration — note the +CEREG: 1 landing right at the call — the modem cannot immediately reach a quiesceable radio state and holds the AT command pending. If the network keeps re-activating the radio, the command may never complete, so the synchronous lte_lc_func_mode_set never returns. I have not confirmed this at the modem level yet (I can capture a modem trace).

Pre-empting likely questions

  • Sockets: All application MQTT/TLS sockets are explicitly closed and logged before the call. ⟦VERIFY: confirm no nRF Cloud A-GNSS REST socket is still open at deactivate time — CONFIG_NRF_CLOUD_AGNSS=y is set.⟧
  • Reset type: Clean MCUboot reboot ~58s after the hang (signature verify, boot slot 0), no hardfault/exception dump — consistent with a watchdog reset, not a modem fault or crash. ⟦VERIFY: insert your WDT timeout value and quote any "Reset cause:" line.⟧
  • Calling context: Confirmed — main application thread (supervisor poll loop), not a callback/RX-thread context (detailed above).
  • PSM/coexistence: LTE_M_GPS mode, PSM requested (RPTAU 1800s / RAT 20s), no eDRX, no RAI, default GNSS use case, prio_mode not used.
  • GNSS: Running concurrently throughout (LTE-dark with GNSS-on is the intended state). Not yet tested with GNSS stopped.
  • Reproducibility: Intermittent, tied to live network/registration churn; does not reliably reproduce stationary on a stable cell.
  • Modem FW: mfw_nrf91x1_2.0.4; not yet tested on a newer modem FW.

Questions

  1. Can AT+CFUN=20 / lte_lc_func_mode_set(DEACTIVATE_LTE) block indefinitely at the modem-firmware level if the network is mid-registration (e.g. a +CEREG URC arriving as the command is issued)? Is this a known interaction?
  2. If so, what's the recommended way to issue a bounded or abortable deactivate — is there a safe timeout-and-recover pattern, or a way to force the radio to a quiescent state first (e.g. a different CFUN value, or detach sequence) before requesting DEACTIVATE_LTE?
  3. Is there a recommended pattern for reliably taking the modem LTE-dark (GNSS-on) specifically in poor coverage where the network keeps attempting re-registration?
  4. Would a newer modem firmware than 2.0.4 contain relevant fixes for CFUN-transition behavior under registration churn?

I can provide a full modem trace captured during the hang if helpful.

  • In my experience, switching off the modem takes randomly some more time, but 120s should do it. (Background seems to be the EPS-detach procedure.) 

    Do have this randomly? Always? 

    There are also a couple of callbacks (e.g. NRF_MODEM_LIB_ON_CFUN), which may block on "locks" or whatever. If it's close to "always", you may check with a debugger, if there is an issue with a callback.  

  • Hopefully, any of this may be helpful. This was from an offline discussion with an engineer.

    "Closing the loop on our report of lte_lc_offline() / AT+CFUN=20 never returning on the nRF9151 (Thingy:91X, modem FW mfw_nrf91x1_2.0.2, NCS v3.3.0). I've completed my investigation and field validation, and no further support is needed — but I wanted to leave you a summary in case the pattern is useful to your modem team.

    What I found. The hang is strongly environment-dependent. At my original test site — dense coverage with roughly three overlapping cells of the same carrier — issuing CFUN=20 from RRC idle wedged reliably: +CEREG: 0 would arrive, but the command response never did, whether issued through lte_lc_offline() or directly via nrf_modem_at_cmd(). The calling thread blocks indefinitely inside the modem library; no error, no timeout. At a second site served by a single cell, the identical firmware performed the same transition cleanly three out of three times, with the OK response arriving in roughly 0.6–2 seconds. My working theory is a race in detach handling when the modem has multiple candidate cells at the moment of deactivation, but I did not root-cause beyond that boundary — it's inside the modem/library and beyond my visibility.

    How I resolved it. Since the wedge is unpreventable from application code, I made it survivable: a hardware watchdog supervised from my main loop catches the wedged state, a noinit RAM record carries intent across the reset, and firmware resumes the interrupted operation on reboot. With that architecture the transition now succeeds either immediately (clean environments) or after one watchdog cycle (overlap environments), and the feature built on it has passed multi-cycle field testing.

    If your modem team ever wants a repro: the signature is CFUN=1→20 from RRC idle in a multi-cell overlap zone; single-cell sites won't show it. Happy to share AT traces from both environments if useful, but please consider the ticket closed from my end."

Related