If a Channel Sounding procedure is aborted while in flight and the ACL is then disconnected, the device settles at a flat ~1 mA with the CPU asleep in the idle thread and the radio off. The state persists until reset. It does not clear on its own, and it is not cleared by later CS activity. Sessions that run to completion never produce it.
We have tried three application-side teardown sequences, including one that waits for the controller to report the procedure has ended. None prevent it. The hold is not reflected in any application-readable status register we have been able to check, and it is not held through Zephyr's clock-control API. We believe a power or clock release is being missed on the aborted-teardown path inside the SoftDevice Controller, and we are asking either for the correct supported teardown sequence or for confirmation of a controller-side defect.
These are battery units on ~200 mAh cells. At 1 mA a parked device is flat in about a week, and it stays connectable and answers keepalives until the cell collapses, so the failure is silent rather than obvious.
Checked before filing: v3.4.0 is the current release, and the SoftDevice Controller changelog on main contains no fix for CS teardown or residual current.
ENVIRONMENT
SoC: nRF54L15, HW variant 0x0005, Holyiot 25008 module (holyiot_25008/nrf54l15/cpuapp)
Silicon: FICR INFO gives PART 0x00054B15, VARIANT 0x41414330 ("AAC0"), PACKAGE 0x00005146 ("QF") -- so QFAA, revision C0
SDK: NCS v3.4.0 (Zephyr ncs-v3.4.0)
Controller: Standard Bluetooth controller (0x00), version 200.12506, build 888207768, HCI 6.3, SDC transport
Application: Channel Sounding initiator derived from ras_initiator. CS parameters as the sample, notably max_procedure_count = 0, so procedures run continuously until disabled.
Measurement: PPK2 in source-meter mode at 3.0 V in place of the cell; register and RTT readback over SWD.
After an affected session the current steps to a flat 1015-1096 µA and stays there for the remainder of every capture, which in our case runs to several minutes. The normal idle floor on the same units is 10-28 µA. Current returns to that floor only after a reset.
Read over SWD in the leaking state, without halting or resetting:
- CPU asleep in the idle thread (_kernel.current == idle_thread), kernel tick healthy. Not a crash and not a busy-spin.
- RADIO.STATE, SAADC and SPIM00 all disabled or off. Constant-latency mode not active.
- Zephyr's generic clock-user bitmask (hfclk_users) is 0, so nothing is holding the request through the Zephyr clock-control API.
Our initiator ranges in sessions gated by an accelerometer. When motion is detected mid-session the application leaves the sample loop and tears the anchor down: bt_le_cs_procedure_enable(enable=0), then disconnect. Because procedures are continuous, a procedure is always in flight, so this teardown is always mid-procedure.
To reproduce: range against a single reflector, move the device continuously for a few seconds while ranging so the session aborts mid-procedure, then let it settle. A fraction of such aborts leave the device at ~1 mA.
The leak follows aborted sessions only. RTT around a leaking teardown shows the running procedure's subevents aborting ("All subevents in ranging counter N were aborted", "Dropped subevent results") immediately before the disconnect.
In several captures the floor rises to ~1077 µA while ranging is still in progress, tens of seconds before our disconnect. That suggests the trigger is the controller's own subevent aborts rather than the application's teardown call.
TEARDOWN SEQUENCES TRIED, ALL OF WHICH STILL LEAK
1. Disable, wait for the disable command-complete, then disconnect.
2. As above, and additionally wait for the CS-enable command-complete before sampling, and retry a refused disable. An abort during CS bring-up had produced LE CS Procedure Enable returning Command Disallowed (opcode 0x2094, status 0x0c) because an enable was still in flight, after which the link was dropped with a procedure still enabled. This closed that window.
3. As above, and additionally wait for the controller to report the procedure has actually ended (procedure_done_status COMPLETE or ABORTED via the CS Subevent Result callback) plus a settle delay, before disconnecting. Still leaked, from a fresh boot, at ~1015 µA.
We have found no HCI-level ordering that avoids it, short of never disconnecting with CS active.
NARROWING THE HELD RESOURCE
We first suspected HFXO: one SWD readback showed CLOCK.XO.RUN = 1 with XO.STAT.STATE = 0 while the CPU was idle. We could not reproduce that under sustained logging. A firmware logger sampling CLOCK.XO.RUN and RADIO.STATE once a second across ~38 minutes, with several induced leaks, found XO.RUN = 1 only during active CS sessions. Every idle sample, including the confirmed ~1 mA episodes, read XO.RUN = 0 and RADIO = 0. XO.RUN marks an active session, not the leak, and the original one-shot readback was most likely a session tail.
A held PLL is also ruled out. We suspected erratum [39], where a teardown skipping TASKS_PLLSTOP would give XO.RUN = 0 alongside a steady draw. With CLOCK.PLL.RUN and PLL.STAT added to the per-second logger, PLL.RUN tracks XO.RUN exactly: set only during active CS, and 0 in every idle sample including the leak.
So the hold is not visible in any application-readable digital status register we checked -- XO.RUN, XO.STAT, PLL, RADIO, constant-latency, SPIM all read their idle values during the leak. The remaining candidates are analog or power-domain state we cannot observe from the application, which is why we are bringing it to you.
One specific candidate: the controller documents a CS control parameter SDC_HCI_VS_CS_PARAM_TYPE_CS_VREG_MODE_SET, for setting the voltage regulator mode used during a Channel Sounding procedure. That the controller switches a regulator around a CS procedure is the relevant part. If an aborted teardown does not restore the pre-CS mode, that would account for a steady ~1 mA that no digital status register reflects. We note this parameter is documented on SDC main and is not present in our v3.4.0 controller, so we cannot test it here.
For scale, Nordic's published figures are ~8.2 mA for an initiator and ~5.1 mA for a reflector during continuous CS. A steady 1 mA is consistent with a single held resource rather than CS still running.
RELATED KNOWN ISSUES
We searched for a public match and did not find one. Two entries in your tracker are release-path defects of the same shape:
- NCSDK-34404 -- nRF54L NFC, an HF peripheral clock left running after the field is lost, current stays high. Fixed in v3.2.0.
- DRGN-29277 -- MPSL mpsl_clock_hfclk_src_release() leaves HFCLK24M running, raising sleep current. Open through v3.4.0.
WHAT WE HAVE NOT DONE
We have not yet reproduced this on a stock nRF54L15 DK with unmodified ras_initiator and ras_reflector. Our initiator disconnects per anchor after a graceful CS disable; the stock sample cold-reboots on every disconnect and never disables CS, so this teardown path is only exercised by our code. We have built the stock-DK reproduction (a 14-line diff to ras_initiator to abort mid-procedure, plus a stock reflector) and will report the result. A Nordic-side reproduction or confirmation would make it unnecessary.
Our confidence is therefore high but the attribution is circumstantial: the behaviour is measured and repeatable, a clean session never causes it, and Zephyr's clock API is not holding anything. That points to a release the controller or MPSL should perform on an aborted CS teardown and does not.
QUESTIONS
1. Is there a known issue in SDC 200.12506 or MPSL where an aborted CS procedure, or issuing LE CS Procedure Enable (disable) or disconnecting while a procedure is in flight, leaves a clock or power-domain request asserted at roughly 1 mA until reset?
2. What is the correct supported teardown order to stop CS and disconnect so the controller reliably releases all CS resources? Is there an event or callback that guarantees the release has happened, as distinct from the procedure having ended?
3. Does max_procedure_count = 0 interact badly with an application-initiated mid-procedure teardown, and is a bounded procedure count the recommended pattern?
4. Is the CS voltage-regulator mode restored on an aborted CS teardown, and is there a vendor-specific command to read it back? If an unrestored mode is the cause, which release carries the change, and is the restore path itself fixed there or only made configurable?
5. We measure CLOCK.PLL.RUN = 0 throughout the leak, so an omitted TASKS_PLLSTOP is not the cause. Is there a path where the PLL or its bias network stays powered despite RUN = 0? That would not be observable from the application.
ATTACHED EVIDENCE
nordic-cs-abort-leak-evidence-2026-09-03.zip (about 1 MB) contains the measurements behind every figure above, so nothing here has to be taken on trust. Its README maps each claim to the file that shows it. In summary:
- Six annotated current traces, as plots and as data: the leak on an unpatched build; a completed session on the same build returning to a 10 µA floor as a control; both outcomes on a single boot; and the hold engaging mid-ranging under teardown fixes 1 and 2.
- Eight RTT ring snapshots decoded to text, including the 0x2094 / 0x0c refused-disable sequence, plus our stdlib-only ring parser so they can be re-read from the raw .bin independently.
- The three teardown patches, in the order we tried them.
- The two SWD readback scripts used to sample CLOCK.XO.RUN, XO.STAT, PLL, RADIO and the current-thread pointer in the leaking state.
- The 14-line diff that turns the stock ras_initiator into the DK reproduction described above.
The traces in the bundle are decimated to per-100 ms mean, min and max so it stays small enough to attach; no capture is trimmed and no window is selected. The unmodified 10 kHz captures are 2.2 GB across 45 files, and we will send any of them, along with the 14 bench firmware builds the captures were taken on, so any trace can be tied to the exact binary that produced it. Our full internal experiment log, including the trials we voided and why, is also available.