Skip to content

cell M6: the DL cadence at a DAPS switch - #561

Open
josephnef wants to merge 2 commits into
masterfrom
feat/m6-dl-cadence
Open

josephnef wants to merge 2 commits into
masterfrom
feat/m6-dl-cadence

Conversation

@josephnef

Copy link
Copy Markdown
Collaborator

Part of #260 (M6), epic #254. Follows #560.

After #560, about 1 visit run in 4 still had a 225 ms DL gap across an inter-frequency DAPS switch. Timestamps on the target DU's clock found three causes:

  • DU picked a frame's PDUs a superframe early. The DL loop built frame N right after sending N-1, then slept to the slot, so a PDU arriving in those ~100 ms waited for N+1. Now the frame is picked and built CELL_DL_PICK_LEAD_US (3 ms) before its slot. DL send lateness is unchanged (p50 3 µs, p99 8 µs).
  • CU sent PDUs up to 50 ms late. poll(…, 50) meant a due PDU went out only when something woke the loop. The poll now also wakes for the next PDU; spacing at the DU is 99–102 ms in every run (one gap was >119 ms before).
  • The fresh PDU didn't fit. At the switch the target's queue holds ~41 sns (the 16-PDU backlog plus every PDU since the command). Every PDU more than the reorder window behind the fresh one goes first (cell M6: the switch's own interruption (inter-frequency DAPS) #560's review fix), and up to 8 of them filled the 8-PDU frame. In one run the fresh PDU had arrived 51 ms before the pick. kDlMaxPdus 8 → 32 (a fixed-length frame: +96 bytes, ~32 µs at 24 Mb/s), with a static_assert that the queue's out-of-window maximum plus the fresh one fit.

New logs: cell.dl.pdu_in (a DAPS target's PDU arrivals), pick_ms on cell.dl, t_ms on cell.daps.visit_dl.

Rig (testrig m6_daps_xch.sh, 4 runs per case, cells 149 → 157):

case UL lost before the switch DL gap across the switch
source dies, visits 0 (all 4) 122–123 ms (all 4)
source dies, no visits 12–15 1395–1703 ms
source alive, visits 0 (all 4) 122–123 ms ×3, 166 ms ×1

The 166 ms run: the source's own frame had delivered the newest PDU before the last answer; the target's first frame carried the next one. Every first target frame carried the fresh PDU. Before: 6 of 24 visit runs at 225 ms. Regression on the rig passes: measgap, three_ho, crossing3, handover, daps, daps_dl, crossing.

🤖 Generated with Claude Code

…their slot, CU PDUs on time, 32 PDUs per DL frame

- DU: a DL frame's PDUs are picked and the frame built CELL_DL_PICK_LEAD_US
  (3 ms) before its slot, not right after the previous frame (a PDU arriving
  in between waited a superframe).
- CU: the main loop's poll also wakes for the DL stream's next PDU (a fixed
  50 ms let PDUs leave up to 50 ms late): spacing at the DU 99-102 ms.
- kDlMaxPdus 8 -> 32: after a switch the out-of-window offered PDUs (~8 on
  the rig) filled the frame and held the fresh one a superframe.
- cell.dl.pdu_in (a DAPS target's PDU arrivals), cell.dl pick_ms, answer
  t_ms: the timestamps that found these.

Rig (m6_daps_xch, 4 runs per case): DL gap across the switch 122-123 ms in
7 of 8 visit runs, 166 in one (the source's frame had the newest PDU);
before, 6 of 24 at 225 ms.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Reduce downlink gaps at inter-frequency DAPS switches

🐞 Bug fix 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Pick target DU downlink frames near their slots so newly arrived PDUs are included.
• Wake the CU for scheduled PDUs and expand frames so fresh PDUs fit alongside backlog.
• Add timing logs and document rig results showing improved switch gaps.
Diagram

graph TD
  CU["Timed DL stream"] --> Q[("Target DL queue")] --> PICK["Pre-slot pick"] --> FRAME["32-PDU frame"] --> UE["UE reorder"]
  SLOT["Slot clock"] --> PICK
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Dedicated CU timer
  • ➕ Could schedule PDU emission independently of incoming datagrams.
  • ➖ Adds timer or thread coordination to an existing single-loop scheduler without a demonstrated need.

Recommendation: Keep the deadline-aware poll: it addresses CU lateness within the existing loop. Combined with a pre-slot DU pick and bounded frame-capacity increase, it targets all three observed causes without introducing another scheduler.

Files changed (4) +90 / -13

Bug fix (3) +42 / -5
cell.hExpand fixed-length downlink frames to 32 PDUs +6/-1

Expand fixed-length downlink frames to 32 PDUs

• Raises the per-frame PDU capacity from 8 to 32 so out-of-window backlog can precede a fresh PDU without delaying it another superframe. The fixed-length DL body grows accordingly.

examples/cell/cell.h

main.cppPick DU frames near their slots and log DAPS timing +24/-3

Pick DU frames near their slots and log DAPS timing

• Moves downlink PDU selection and frame construction to a configurable lead time before the slot, defaulting to 3 ms. Adds a compile-time capacity check and timestamps for target PDU arrivals, frame picks, and visit answers.

examples/cell/main.cpp

main.cppWake CU polling for the next scheduled DL PDU +12/-1

Wake CU polling for the next scheduled DL PDU

• Caps the controller's poll timeout by the time until the next downlink PDU, rather than always waiting up to 50 ms. This reduces lateness in the PDU stream delivered to DUs.

tools/ran-controller/main.cpp

Documentation (1) +48 / -8
cell-m6.mdDocument switch-gap causes and rig outcomes +48/-8

Document switch-gap causes and rig outcomes

• Corrects the earlier interpretation of the remaining 225 ms gaps and explains the DU timing, CU pacing, and frame-capacity fixes. Records switch-gap measurements, first-frame contents, and regression results.

docs/cell-m6.md

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

qodo-free-for-open-source-projects Bot commented Nov 11, 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. The first target frame can miss fresh data ✓ Resolved

Resolving commit: 78bbfdc

Resolution reason
pick_dl_pdus now tracks fresh_in and only takes an urgent old PDU in a slot when a fresh PDU is
already in the frame or there is room for one more (out.size() + 1 < max), so the first fresh PDU
always keeps its place in the frame even when the queue has gaps. The comment and static_assert were
reworded to match, and a selftest covers the offered 1..32 then 101 case (gf must contain 101 and
front 1). This is a different implementation from the suggested fix (a reserved slot rule rather
than a general reselection), but it addresses the same issue. The static_assert was kept, now with a
comment that it covers the contiguous case.
Description
kDlMaxPdus and the new assertion assume that no more than 31 offered PDUs can precede a fresh one,
but pick_dl_pdus can select 32 offered PDUs first when sequence numbers have gaps. If a target has
offered SNs 1–32 and then receives SN 101 after intervening CU datagrams are lost, all 32 older
entries are selected as urgent and SN 101 waits for another frame.

2. Downlink frames can miss their slot ✓ Resolved

Resolving commit: 78bbfdc

Resolution reason
Added a late-submission guard: after sleep_until_tsf, frames whose submission would start more than
dl_guard_us (500 µs) into their slot are skipped and their PDUs/HARQ attempt returned, with a
cell.dl.late_skip event. Also serves UEs in DL-slot order so the late case is less likely to hit.
Description
run_du now waits until start - dl_pick_lead_us before taking the queue lock, selecting HARQ
data, and building the frame. If that work exceeds the configured lead, which can be as little as
500 µs, sleep_until_tsf(dev, start) returns immediately and the radio submission is late.

Grey Divider

Tip of the day
💡 Did you know, you can import rules and skills only from the repositories your team follows

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

…e, a frame too late for its slot is skipped; DUs serve DL in slot order

- pick_dl_pdus: out-of-window offered PDUs never take the first fresh one's
  place (a queue with gaps could fill a frame with them); selftest 1..32 + 101.
- A DL frame whose submission would start more than CELL_DL_GUARD_US into
  its slot is not aired (cell.dl.late_skip); PDUs and HARQ attempt go back.
- That skip found a DU serving its UEs in grant order: with two UEs the one
  whose slot comes first was aired ~4 ms late into the other's slot, every
  frame. Now in DL-slot order. The m6_daps_dl crossing's longest DL silence
  went from 3.6-4.3 s to 0.39-1.03 s.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@josephnef

Copy link
Copy Markdown
Collaborator Author

Review fixes pushed:

  1. First target frame can miss fresh data — fixed. pick_dl_pdus now keeps the first fresh PDU's place: out-of-window offered PDUs fill the rest of the frame, and once a fresh one is in, they go first again as before. The assertion's comment now says what it guarantees (a contiguous queue fits whole) and that the selector keeps the fresh slot in any case. New test: offered 1..32 then 101, 32-PDU frame → 101 rides with 1..31.
  2. Frames can miss their slot — handled explicitly. A frame whose submission would start more than CELL_DL_GUARD_US (500 µs, the slot budget's guard) into its slot is not aired (cell.dl.late_skip); its PDUs and HARQ attempt go back. The lead stays at 3 ms; the pick and build take microseconds, and the rare few-hundred-µs outliers predate this change (one 319 µs on the old code).

What the skip found: a DU with two UEs served them in grant order, not slot order. The UE whose slot comes first, a handed-over UE at position 1 next to the DU's own at 2, had every frame aired ~4.1 ms late into the other UE's slot. That was 404 of 404 frames in a crossing on the merged code; with the skip they were dropped, 440 per run. The DU now serves in DL-slot order.

Rig on the final build: every M6 regression passes with 0 late skips (daps_dl, three_ho, crossing3, crossing, measgap, handover, daps), and so does m6_daps_xch (switch gap 122–123 ms, 166 where the source had the newest PDU). The daps_dl crossing's longest DL silence dropped from 3.6–4.3 s to 0.39–1.03 s.

@josephnef josephnef added the skip-qodo-gate Bypass the Qodo review gate (outage / maintainer decision) label Nov 11, 2026
@josephnef

Copy link
Copy Markdown
Collaborator Author

Qodo's re-review: both findings resolved, 0 new; only alternatives remain. Adding skip-qodo-gate.

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.

1 participant