Skip to content

kestrel: drain bulk-IN during bring-up on USB; redo a bring-up the 8852C fw halted - #496

Merged
josephnef merged 2 commits into
OpenIPC:masterfrom
agustinglez:kestrel-8852c-bringup-drain
Oct 21, 2026
Merged

josephnef merged 2 commits into
OpenIPC:masterfrom
agustinglez:kestrel-8852c-bringup-drain

Conversation

@agustinglez

@agustinglez agustinglez commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

In short

On the 8852C (TP-Link TX50UH), a bring-up could leave the chip's firmware crashed while every step reported success. The adapter then worked for a while, and minutes later it stopped sending anything (every USB send timed out). In a long AirDrop transfer this looked like a random mid-transfer hang.

The cause: while the chip is being set up, it already starts handing received data to the host over USB, but devourer only started reading that data at the very end of the bring-up. On the 8852C the chip's receive path counts that as "stuck", raises an internal error, and the firmware crashes a moment later. The 8852B tolerates it.

The fix has two parts:

  1. Start reading the incoming USB data early, as soon as the receive path is switched on, instead of at the end of the bring-up. This is the actual fix.
  2. Check for a crashed firmware at the end of every bring-up, and if it crashed, log why and redo the bring-up (up to 3 times on USB). This is a safety net for causes not seen yet; it has not been triggered with fix 1 in place.

Tested on both chips, many bring-ups in a row, in two sessions on different days:

firmware crashed
8852C, without fix 1 17 of 30 runs on the first day (3 of them crashed on all 3 attempts, so even the retry could not save them); 7 of 66 on the second
8852C, with fix 1 0 of 30 and 0 of 50
8852B, without / with fix 1 0 of 20 / 0 of 20

How often it crashes without the fix varies a lot from one session to the next, but with the fix it never did. A new script, tests/kestrel_bringup_halt.sh, counts the crashes so anyone can repeat this.

Start-up takes the same time with and without the fix on both chips (a full test run: 4.7 s on the 8852C, 7.9 s on the 8852B).

Technical detail

Failure chain

intf_init arms the USB RX path, but nothing reads the bulk-IN until InitWrite starts the WP-release drain (or Init starts the RX loop) at the end. During the H2C-heavy BB/RF table load the 8852C then raises:

HAXI_IDCT bit 1 (B_AX_RXMDA_STUCK_IDCT) -> DMAC_ERR_ISR B_AX_HAXIDMA_ERR_FLAG (0x4000) -> L1 pre-error 0x999 (L1_PREERR_DMAC) posted in HALT_C2H -> with no host L1 handshake, fw assert 0x30000024.

Nothing in the bring-up reads HALT_C2H after the first steps, so the bring-up returns success. TX keeps working until the TX queues stop draining (bulk-OUT timeouts on every send).

Changes

  • BringUpMonitorOnce() starts the WP/bulk-IN drain right after PowerOnFwAndTrx() on USB. StartRxLoop() already stops the drain before taking the endpoint, and InitWrite's later start_wp_drain() is a no-op when it is already running, so the handoffs are unchanged. No bring-up step reads the bulk-IN synchronously, so the drain cannot steal a response. PCIe gets no drain thread (its release reports arrive on the RPQ ring).
  • BringUpCheckingFwHalt() wraps the monitor bring-up (Init) and the whole TX bring-up (InitWrite, so a halt in its TX half also redoes everything). After each pass it calls fw_err_state(); on a halt it logs a warning, stops the drain, calls HalKestrel::reset_for_rebringup() (destroys the halbb/halrf contexts and clears their one-shot flags, the fast-retune caches and the H2C sequence number) and runs the bring-up again from power_on, which forces the MAC off. Up to 3 attempts on USB; InitWrite throws after the third, as it does for any refused bring-up. On PCIe the redo (and the ring re-init under it) is unverified, so there a halt fails the bring-up at once instead of being retried. A pass that fails or throws stops the drain before returning.
  • fw_err_state() now also logs DMAC_ERR_ISR and SER_DBG_INFO (both dies), plus the HAXI registers on the 8852C only, matching the vendor's dump_err_status_haxidma_ax, which is built for the AXI dies only. Once HALT_C2H_CTRL says the firmware halted, the rest is best-effort: if the error code reads 0 or the diagnostic reads fail (the USB transport throws), the failure is logged and HALT_C2H_CTRL is returned, so the caller still sees the halt and the USB redo still happens (a zero code was never observed: every halt in our logs carried 0x999 or 0x30000024). The register names are added to MacRegAx.h (mac_reg_ax.h values).
  • docs/8852c-quirks.md: a new entry in the file's usual shape (what the chip does, what devourer does, residual cost, reproducer). src/kestrel/CLAUDE.md: under "Dispatch and cold boot", only the rule and a pointer to that entry.
  • tests/kestrel_bringup_halt.sh: N back-to-back txdemo bring-ups at log level warn, counts the halted attempts from the halt check's log line, exits 1 on any halt, and also on any run where txdemo failed without one (adapter absent, open error, timeout), since such a run tested nothing. For the A/B, run it against a build with the early start_wp_drain() removed.

Measurements

Windows 11, MSVC Release build, rear USB 3 port. txdemo, 200 canonical beacons on ch149, DEVOURER_LOG_LEVEL=warn (the halt is timing-sensitive: debug logging slows the H2C stream and hides it). Back-to-back processes, no power cycle between them. "Without fix 1" is this branch with only the start_wp_drain() call in BringUpMonitorOnce() removed, so the same halt check counts the halts (master has no check at the end of bring-up, so it cannot count them).

Session 1, 2026-10-18 16:46-17:00 local time:

adapter arm runs runs with a halt halted attempts runs failed (3/3 halted) txdemo run time
TX50UH (8852C) without fix 1 30 17 24 of 51 (14x 0x30000024, 10x 0x999) 3 4.6-4.7 s without a retry, 6.2 s with one, 7.7 s with two
TX50UH (8852C) this PR 30 0 0 of 30 0 4.6-4.7 s
TX20U Nano (8852B) without fix 1 20 0 0 of 20 0 7.9 s
TX20U Nano (8852B) this PR 20 0 0 of 20 0 7.9 s

All 24 halts read HAXI_IDCT=0x00000002 (RX DMA stuck) and DMAC_ERR_ISR=0x00004000. The retry recovered 14 of the 17 halted runs, which is why it stays only as a safety net. RX: 3 rxdemo bring-ups per die with the fix received frames after the drain handed the bulk-IN over to the RX loop.

Session 2, 2026-10-20 07:56-08:07 local time, TX50UH only, on 35fe35e (partly through tests/kestrel_bringup_halt.sh): without fix 1, 7 of 66 runs halted (8 of 74 attempts, none failed all 3; again every one HAXI_IDCT=0x2, DMAC_ERR_ISR=0x4000); with it, 0 of 50. The rate without the fix was much lower than in session 1. The halt is timing-sensitive, and how much traffic the radio hears during the bring-up may also matter (the bulk-IN only fills if frames arrive); I have not isolated that. The script reproduced the halt (and exited 1) in the undrained arm.

Earlier, in the AirDrop receiver on the same TX50UH (2026-10-16): 8 of 48 bring-ups halted without fix 1, 0 of 80 with it. The rate differs from txdemo's because the timing differs; both point the same way.

Not covered: the 8852CE on PCIe (no drain thread there; the halt check runs but does not retry), and Linux hosts (the halt depends on timing, and I only have this Windows bench). The two Kestrel coex: LTE interface not ready errors the 8852C logs at every bring-up are unrelated and also appear on master.

Nothing read the USB bulk-IN between intf_init arming the RX path and
the end of bring-up. On the 8852C the RX DMA then flags itself stuck
during the H2C-heavy BB/RF table load (HAXI_IDCT B_AX_RXMDA_STUCK_IDCT
-> DMAC_ERR_ISR HAXIDMA_ERR -> L1 pre-error 0x999 -> fw assert
0x30000024). Every step still reports success; the radio works for a
while and then its TX queues stop draining for good (bulk-OUT timeouts).
BringUpMonitorOnce() now starts the WP/bulk-IN drain right after
PowerOnFwAndTrx instead of at the end of the TX bring-up.

As a safety net, BringUpCheckingFwHalt() reads HALT_C2H_CTRL after a
monitor or TX bring-up and, if the fw halted, logs the DMAC error
registers (plus the HAXI block on the 8852C, as the vendor's
dump_err_status_haxidma_ax does). On USB it then redoes the whole
bring-up from power_on, up to 3 times, after reset_for_rebringup()
drops the host state one bring-up leaves behind (halbb/halrf contexts,
fast-retune caches, H2C sequence). On PCIe the redo is unverified, so
a halt fails the bring-up there. A failed or throwing pass stops the
drain; a posted halt whose error code reads 0 counts as a halt.

tests/kestrel_bringup_halt.sh counts halted bring-ups; the entry in
docs/8852c-quirks.md has the mechanism and the numbers.

Measured on the TX50UH (8852C), back-to-back txdemo bring-ups (200
frames, ch149, log level warn), same halt check in both arms, the
undrained arm with only the early start_wp_drain() removed:
- 2026-10-18 16:46-17:00: undrained 17 of 30 runs halted (3 failed
  all 3 attempts); drained 0 of 30.
- 2026-10-20 07:56-08:07: undrained 7 of 66; drained 0 of 50.
Every halt read HAXI_IDCT 0x2. TX20U Nano (8852B): 0 of 20 both ways,
same bring-up time.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Prevent 8852C firmware halts during USB bring-up

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Drain USB bulk-IN during bring-up to prevent 8852C RX-DMA-stuck firmware halts.
• Detect silent firmware halts; retry full bring-up on USB and fail promptly on PCIe.
• Document the failure and add a repeatable bring-up halt counter.
Diagram

graph TD
  Start["Monitor or TX init"] --> Setup["Arm RX path"] -->|USB| Drain["Drain bulk-IN"] --> Load["Complete bring-up"] --> Probe{"Firmware halted?"} -->|No| Ready["Bring-up ready"]
  Setup -->|PCIe| Load
  Probe -->|USB retry| Reset["Stop drain; reset state"] --> Setup
  Probe -->|PCIe or exhausted| Fail["Bring-up fails"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Retry without early draining
  • ➕ Avoids changing USB bulk-IN ownership during bring-up.
  • ➖ Leaves the RX-DMA-stuck cause in place.
  • ➖ Measured retries did not recover every halted run.
2. Start the normal RX loop during bring-up
  • ➕ Could use one bulk-IN consumer throughout initialization and operation.
  • ➖ Requires earlier RX-loop and callback lifecycle setup.
  • ➖ Makes ownership and teardown during failed bring-ups more complex.

Recommendation: Keep the early drain as the primary fix and the bounded USB retry as a safety net. The existing drain-to-RX-loop handoff avoids a new receive lifecycle, while retry alone cannot reliably prevent this failure. Retaining fail-fast behavior on PCIe is appropriate until its reinitialization path is validated.

Files changed (9) +241 / -3

Bug fix (6) +135 / -3
HalKestrel.cppExpand firmware-halt diagnostics and reset retry state +31/-1

Expand firmware-halt diagnostics and reset retry state

• Logs DMAC and SER status on a posted halt, plus HAXI diagnostics on the 8852C, and treats a posted halt with a zero error code as a failure. Adds cleanup of HALBB/HALRF contexts, one-shot flags, retune caches, and the H2C sequence before re-bring-up.

src/kestrel/HalKestrel.cpp

HalKestrel.hDeclare host-state reset for re-bring-up +8/-1

Declare host-state reset for re-bring-up

• Exposes the retry-state reset method and clarifies that the firmware-halt probe returns a nonzero result even when the posted error code is zero.

src/kestrel/HalKestrel.h

KestrelFw.hAllow H2C sequence reset after firmware reload +5/-0

Allow H2C sequence reset after firmware reload

• Adds a mutex-protected sequence reset so a retried bring-up starts its firmware command numbering afresh.

src/kestrel/KestrelFw.h

MacRegAx.hDefine registers for halt-cause diagnostics +10/-0

Define registers for halt-cause diagnostics

• Adds DMAC and SER debug register addresses for both dies and HAXI diagnostic addresses used for 8852C halt logging.

src/kestrel/MacRegAx.h

RtlKestrelDevice.cppDrain USB RX early and check completed bring-ups +66/-1

Drain USB RX early and check completed bring-ups

• Starts the USB bulk-IN drain immediately after RX-path initialization. Wraps full monitor and TX bring-ups with a firmware-halt check, stopping the drain on failure and retrying halted USB passes up to three times; PCIe halts fail without retry.

src/kestrel/RtlKestrelDevice.cpp

RtlKestrelDevice.hDeclare bring-up passes and halt-check wrapper +15/-0

Declare bring-up passes and halt-check wrapper

• Declares the single-pass monitor initialization, TX tail, and shared halt-check wrapper, including the three-attempt USB limit.

src/kestrel/RtlKestrelDevice.h

Tests (1) +45 / -0
kestrel_bringup_halt.shAdd repeated bring-up halt reproducer +45/-0

Add repeated bring-up halt reproducer

• Runs configurable back-to-back txdemo sessions at warn log level, counts halted attempts and failed runs from logs, and exits unsuccessfully if any halt occurs. Supports comparison against a build with the early drain removed.

tests/kestrel_bringup_halt.sh

Documentation (2) +61 / -0
8852c-quirks.mdDocument the undrained bulk-IN firmware halt +53/-0

Document the undrained bulk-IN firmware halt

• Explains the 8852C RX-DMA-stuck failure chain, the early drain and retry behavior, comparative measurements, limitations, and how to reproduce the halt.

docs/8852c-quirks.md

CLAUDE.mdRecord the Kestrel bring-up drain requirement +8/-0

Record the Kestrel bring-up drain requirement

• Adds a cold-boot note describing why bulk-IN must be drained early and how USB and PCIe handle a detected firmware halt. Links to the detailed quirk entry.

src/kestrel/CLAUDE.md

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Cross-cutting guide repeats chip details ✓ Resolved
Description
src/kestrel/CLAUDE.md repeats the 8852C halt mechanism, recovery limits, and transport-specific
behavior that are already documented in docs/8852c-quirks.md. Future changes to the bring-up
workaround or its measurements must update both locations, while the cross-cutting guide could
instead retain only the pointer and a brief invariant.
Code

src/kestrel/CLAUDE.md[R51-57]

+USB bulk-IN is drained from right after `intf_init` (`BringUpMonitorOnce`),
+not from the end of bring-up: left unread, the 8852C flags its RX DMA stuck
+and its firmware halts while every bring-up step still reports success; TX
+dies minutes later. `BringUpCheckingFwHalt` catches a halt that still gets
+through (HALT_C2H after the bring-up; USB: full redo, 3 attempts; PCIe: no
+redo, the bring-up fails). Mechanism, numbers and reproducer:
+`docs/8852c-quirks.md`.
Evidence
The new guide paragraph describes the 8852C-specific RX-DMA halt, firmware error check, USB retry
count, PCIe behavior, and documentation location, while the newly added quirks entry already
contains those same details and the reproducer. This conflicts with the checklist requirement to
place subsystem-specific facts in the narrowest applicable documentation file.

CLAUDE.md: Keep Cross-Cutting Documentation Nonduplicative and Evidence Balanced: CLAUDE.md: Keep Cross-Cutting Documentation Nonduplicative and Evidence Balanced: CLAUDE.md: Keep Cross-Cutting Documentation Nonduplicative and Evidence Balanced: CLAUDE.md: Keep Cross-Cutting Documentation Nonduplicative and Evidence Balanced
src/kestrel/CLAUDE.md[51-57]
docs/8852c-quirks.md[100-119]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The cross-cutting Kestrel guide duplicates chip-specific bring-up behavior and recovery details that belong in `docs/8852c-quirks.md`, creating two sources that can drift.
## Fix Focus Areas
- src/kestrel/CLAUDE.md[51-57]
## Recommended Fix
Replace the detailed halt mechanism, retry limits, and transport behavior with a concise cross-cutting note and a pointer to `docs/8852c-quirks.md`; keep the chip-specific mechanism, measurements, and reproducer only in that narrow documentation file.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Halt diagnostics can prevent USB retries ✓ Resolved
Description
fw_err_state() performs additional register reads after detecting a halt, but those reads can
throw before it returns the halt code. If a USB control read fails during diagnosis, the exception
escapes the halt check outside its try block, skipping both drain cleanup and the remaining
bring-up attempts.
Code

src/kestrel/HalKestrel.cpp[R826-828]

+  auto rd = [&](uint16_t a) { return _device.rtw_read32(a); };
+  _logger->error("Kestrel FW-SER dmac_isr=0x{:08x} ser_dbg=0x{:08x}",
+                 rd(r::R_AX_DMAC_ERR_ISR), rd(r::R_AX_SER_DBG_INFO));
Evidence
USB register reads throw on control-transfer failure; the new diagnostic reads occur after the halt
is established, while the wrapper's catch covers only once() and not fw_err_state().

src/kestrel/HalKestrel.cpp[815-838]
src/kestrel/RtlKestrelDevice.cpp[203-215]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Optional register reads can throw after a firmware halt is detected, bypassing drain cleanup and USB retries.
## Fix Focus Areas
- src/kestrel/HalKestrel.cpp[823-838]
- src/kestrel/RtlKestrelDevice.cpp[203-215]
## Recommended Fix
Catch failures from the optional diagnostic reads and still return the detected halt code. Also include the halt probe in the wrapper's cleanup-on-exception scope so any remaining read failure stops the drain.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Missing adapters pass the halt test ✓ Resolved
Description
kestrel_bringup_halt.sh records the demo's exit status but determines success solely from the
number of halt messages. If the adapter is absent or the demo fails before bring-up, the run
contributes a supposedly clean attempt and the script exits successfully.
Code

tests/kestrel_bringup_halt.sh[R33-36]

+  rc=$?
+  h=$(grep -c "firmware halted during" "$log" || true)
+  f=$(grep -c "firmware halted on all" "$log" || true)
+  attempts=$((attempts + h + 1 - f))
Evidence
The script ignores rc when counting attempts and bases its final exit status only on halted
attempts; txdemo returns an error before bring-up when no device is found.

tests/kestrel_bringup_halt.sh[30-45]
examples/tx/main.cpp[627-630]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The reproducer reports a clean result when txdemo exits before it can test a bring-up.
## Fix Focus Areas
- tests/kestrel_bringup_halt.sh[30-45]
## Recommended Fix
Treat a nonzero txdemo exit status as a failed test run, even when no halt message was logged. Do not add that run to the completed-attempt count.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread src/kestrel/CLAUDE.md Outdated
Comment thread src/kestrel/HalKestrel.cpp Outdated
Comment thread tests/kestrel_bringup_halt.sh Outdated
Review follow-ups for the bring-up halt check:

- fw_err_state(): once HALT_C2H_CTRL says the fw halted, the error
  code and the DMAC/HAXI diagnostic reads are best-effort. A failed USB
  read there (the transport throws) is now caught and logged, and the
  halt is still returned, so the USB redo still happens. The probe
  itself now runs inside BringUpCheckingFwHalt's cleanup scope, so a
  failing HALT_C2H_CTRL read stops the bulk-IN drain before it
  propagates.
- tests/kestrel_bringup_halt.sh: a run where txdemo exits non-zero
  without the "halted on all" line (adapter absent, open or bring-up
  error, timeout) tested nothing: it is reported as an error, adds no
  clean attempt, and makes the script exit 1.
- src/kestrel/CLAUDE.md: the note is reduced to the rule and a pointer;
  mechanism, numbers and reproducer stay in docs/8852c-quirks.md only.

Tested 2026-10-20 on the TX50UH (8852C) with a test build that fakes a
halt on the first check and throws on every diagnostic read: 3 of 3
runs logged the halt as "error registers unreadable", redid the
bring-up and exited 0. Stock build: 20 of 20 (8852C) and 10 of 10
(8852B) clean bring-ups through the script, RX on both after the
handoff; the script exits 1 with the adapter absent.

@josephnef josephnef left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 87eab7f. Code read against the drain/RX-loop handoff, the redo's host-state reset and every fw_err_state caller; built with the default options and ctest (83/83); then reproduced on a Linux host, which the PR lists as not covered.

Linux reproduction (Arch, kernel 6.18, TX50UH 35bc:0101 on a USB 2 hub port, no rtw89 loaded). tests/kestrel_bringup_halt.sh, N=30 per arm, ch149, 200 frames, back-to-back, same build with only the early start_wp_drain() removed for the undrained arm:

arm runs halted halted attempts runs failed (3/3)
undrained 30 of 30 90 of 90 30
this PR 0 of 30 0 of 30 0

Every halt read err=0x30000024, dmac_isr=0x4000, haxi_idct=0x2, the same chain as on your bench. Two things differ from the Windows numbers and belong in docs/8852c-quirks.md as the adversarial counterpart to the 17-of-30 row: here the halt is deterministic, and the redo recovered none of the 30 halted runs (the chip side does clear: the drained arm's first run came up clean straight after the undrained arm left the fw asserted). So on Linux fix 1 is the whole fix and the retry has no measured value yet; fine to keep as a net, but please don't let the doc read as though it recovers most halts in general.

Also checked on this host: rxdemo (Init path, where no drain thread existed before) hands the bulk-IN from the drain to the RX loop and receives frames on ch6; a 200-frame txdemo completes with the drain started before the table load.

Two non-blocking nits:

  1. tests/kestrel_bringup_halt.sh: the env line does not pass DEVOURER_USB_BUS / DEVOURER_USB_PORT, and sudo strips them, so two identical adapters (this rig has two TX50UH) cannot be told apart without SUDO="sudo --preserve-env=...". Forwarding both in the env line when set would match the other kestrel scripts' knob style.
  2. The doc's "Not covered: ... Linux hosts" can be replaced by the table above.

Approving; the nits can ride a follow-up.

@josephnef
josephnef enabled auto-merge (squash) October 21, 2026 06:31
@josephnef josephnef added the skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision) label Oct 21, 2026
@josephnef
josephnef merged commit 0bf1957 into OpenIPC:master Oct 21, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants