Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 65 additions & 7 deletions docs/cell-m6.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ pass; both cells on ch149).

| case | steps followed | UE skew, max \|ppm\| | DL frames in the DL area after | uplinks off-slot, 20 sf after |
|---|---|---|---|---|
| restart, detection on | the restart jump (−30.5 to −31.1 s) and the acquisition steps | 8.0–10.1 | 100 % | not judged (below) |
| restart, detection on | the restart jump (−30.5 to −31.1 s) and the acquisition steps | 8.0–10.1 | 100 % | see re-attachment (below) |
| restart, detection off (1 run) | — | 996 125 | 11 % | not judged |
| transient, detection on | none (5 beacons held, then dropped) | 8.4–11.9 | 100 % | 0 |

Expand All @@ -392,7 +392,62 @@ pass; both cells on ch149).
undo), the UE followed the transient out and back, 3 beacons late each way.
That put 4–5 of the next 20 uplinks off-slot, in 3 of 3 runs.

## DAPS: the duplicated uplink (same channel)## DAPS: the duplicated uplink (same channel)
## Re-attachment after a DU restart

A restarted DU's clock is followed (above), but its grant maps begin again
at a low epoch on the new superframe count. A UE's grant receiver refuses
them as old, so without re-attachment the UE keeps its old map and is never
heard in the new grid.

- **The rule** (`GrantMapReceiver::offer_after_restart`): a map at a strictly
lower epoch than the newest accepted, but due within 16 superframes of the
beacon carrying it, starts the receiver over. A stale map from before the
restart is due far from the new count, so it is still refused. The same
epoch again is the live map repeated, not a restart.
- **When:** only for `CELL_UE_REATTACH_SF` (200) superframes after the UE's
clock fit confirms a step of the DU's TSF. A DU that never jumped is never
re-attached to.
- **What else resets** (`cell.ue.reattach`): what belonged to the old DU
instance, as on a handover switch. That is the DL HARQ sequence space, the
MR generation (the DU checks it against its own new count), the neighbour
list and gap parking, and any handover IE the old instance aired.
- **The exposure:** grant maps carry no tag (they never did), so the epoch
rule orders maps; it does not authenticate them. A forged map could
already redirect a UE's slots. The restart path widens that: within the
re-attach window after a clock step, a forged lower-epoch map due near the
beacon's count also resets the UE's per-DU state (HARQ, MR generation,
neighbours, pending handover IEs). Grant-map authentication is open
(below).

**Rig result** (testrig `scripts/m6_fitstep.sh`, restart case, 3 runs, all
pass).
- **The case:** DU1 is killed 30 s in and restarted at once, under its
attached UE. Service start is the restarted DU's first in-loop discipline
pass.

| KPI (3 runs) | value |
|---|---|
| re-attachments per run | 1 (old epoch 7–11 → epoch 1, at the new DU's superframe 6) |
| uplink heard by the restarted DU, from 10 sf after service start | 99.8–100 % of superframes |
| of them in slot | 99.8 % |
| first in-slot uplink after service start | superframe 0 |
| UE DL frames in their granted slot after the re-attach | 100 % |

- **Before the restarted DU serves,** its grid is still moving: it is
acquiring, with its beacon on. The UE has already re-attached to it, and
its uplinks in that time are off-slot against the moving grid. They are
not counted.
- **With detection off,** the UE's clock fit breaks (above). Re-attachment
is never armed, and the restarted DU never hears the UE.
- **No spurious re-attachment** was seen in the transient runs or in the
regression runs. Those were same-channel handover 2/2 plus fallback 1/1,
measurement-gap handover 1/1 and phase planning 1/1, all passing.
- **A logging fix found on the way:** the UE judged its DL frames against
the slot matching its id, not the slot of its grant. A CU move to another
position showed as DL out of slot (0.05 in slot); the frames themselves
were right. It now uses the grant in force.

## DAPS: the duplicated uplink (same channel)

**Common sequence.** A UE uplinks once per superframe. Every DU that hears
one transmission hears it at the same instant on the shared PTP timebase,
Expand Down Expand Up @@ -492,11 +547,14 @@ superframe when there is nothing to serve.
running ones, which never move. With three or more cells on different
channels, the 26 ms retune leaves no layout where every pair can measure
both ways. Re-phasing live cells, or longer superframes, is open.
- **A DU restart under attached UEs:** the UE's clock now follows it, but
its grant state does not. The restarted DU's grant maps start again at a
low epoch and another superframe count. The UE keeps its old map and is
not heard in the new grid. Re-attachment after a DU restart (a fresh map
epoch base, or the UE dropping a map no longer in force) is open.
- **Grant-map authentication:** the grant map IE has no tag, unlike the
NL, MR and handover IEs. A tag keyed per cell (as `CELL_PSK`), checked
before any `offer`, would close the forged-map exposure, including the
restart path. It is a wire change for the DU, UE and witness.
- **A DU restart under RACH-joined UEs:** re-attachment (above) is for the
rig's static UEs, whose id the restarted DU grants again. A UE that joined
through the RACH holds an id the new DU instance does not know; re-joining
it is open.
- **No crossing test:** every handover result here is from stationary UEs.
The trigger is either forced or the measured standing difference between
two cells. No A→B path-loss ramp has been run, so none of these numbers
Expand Down
74 changes: 66 additions & 8 deletions examples/cell/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@
// beacons outlast the DU's discipline pass
// (CELL_SYNC_LOOP_MS 500): a jump the DU undoes
// itself is never followed, only a lasting one.
// CELL_UE_REATTACH_SF [200] UE: for this long after a confirmed step of the
// DU's clock, a grant map at a LOWER epoch that is
// due near its beacon's superframe means the DU
// restarted: the grant state starts over
// (cell.ue.reattach). 0 = never.
// CELL_DU_TEST_TSF_STEP DU, test only: US@SF — add US to the TSF once at
// local superframe SF, behind the discipline's back
// (cell.test.tsf_step); the discipline undoes it.
Expand Down Expand Up @@ -2191,6 +2196,16 @@ int run_ue(IRadio *dev, const Cfg &c) {
sync.SetStepDetect(envl("CELL_UE_FIT_STEP_US", 200, 0, 1000000),
static_cast<int>(envl("CELL_UE_FIT_CONFIRM", 6, 1, 50)));
long long fit_steps_seen = 0, fit_rejected_seen = 0;
/* Re-attachment after a DU restart (mu): after a confirmed step of the
* DU's clock, for CELL_UE_REATTACH_SF superframes, a grant map at a lower
* epoch but due near the beacon's own superframe starts the UE's grant
* state over (GrantMapReceiver::offer_after_restart); the main loop then
* resets what belonged to the old DU instance. */
const int64_t reattach_sf = envl("CELL_UE_REATTACH_SF", 200, 0, 100000);
long long reattach_armed_steps = 0; /* sync.Steps() at the arming */
int64_t reattach_until_ms = 0;
bool reattached = false;
uint32_t reattach_old_epoch = 0, reattach_new_epoch = 0;
Wrap32 wrap;
GrantMapReceiver rxr(c.cell);
/* DL HARQ receiver + the DL SNR since the last feedback publish (mu). */
Expand Down Expand Up @@ -2345,8 +2360,20 @@ int run_ue(IRadio *dev, const Cfg &c) {
static_cast<uint16_t>(me_a.load()), auth))
ho_seen = h;
wrap.feed(p.RxAtrib.tsfl);
if (auto m = devourer::cell::find_grant_map(p.Data.data(), p.Data.size()))
rxr.offer(*m);
if (reattach_sf > 0 && sync.Steps() != reattach_armed_steps) {
reattach_armed_steps = sync.Steps();
reattach_until_ms = ue_ms() + reattach_sf * P / 1000;
}
if (auto m = devourer::cell::find_grant_map(p.Data.data(), p.Data.size())) {
const uint32_t old_epoch = rxr.newest_epoch();
if (!rxr.offer(*m) && ue_ms() < reattach_until_ms &&
rxr.offer_after_restart(*m, static_cast<uint64_t>(*eg) / static_cast<uint64_t>(P))) {
reattached = true;
reattach_until_ms = 0;
reattach_old_epoch = old_epoch;
reattach_new_epoch = m->epoch;
}
}
if (me_a == 0) {
const auto rb = find_rach_responses(p.Data.data(), p.Data.size());
if (rb.first) {
Expand Down Expand Up @@ -2379,19 +2406,23 @@ int run_ue(IRadio *dev, const Cfg &c) {
return;
const int64_t du_at =
sync.RemoteForLocal(static_cast<uint64_t>(wrap.extend(p.RxAtrib.tsfl)));
want = grid->find(SlotKind::Downlink, static_cast<uint16_t>(me));
pos = grid->at(du_at);
/* The DL slot of our grant in the map in force (the CU may have
* moved us off the position matching our id). */
if (const GrantMap *gm = rxr.current(static_cast<uint64_t>(std::max<int64_t>(pos.superframe, 0))))
if (const Grant *g = gm->find(static_cast<uint16_t>(me)))
want = g->dl_slot;
}
if (!want)
return;
/* No grant in force: still logged, as out of slot (granted 0). */
char l[260];
snprintf(l, sizeof l,
"{\"ev\":\"cell.dl.rx\",\"ue\":%u,\"sf\":%llu,\"seq\":%u,\"attempt\":%u,"
"\"new\":%d,\"rx_sf\":%lld,\"phase_us\":%lld,\"in_slot\":%d,\"rssi\":%d,"
"\"snr_half_db\":%d}",
"\"new\":%d,\"rx_sf\":%lld,\"phase_us\":%lld,\"in_slot\":%d,\"granted\":%d,"
"\"rssi\":%d,\"snr_half_db\":%d}",
me, (unsigned long long)d->sf, d->seq, d->harq.attempt, fresh ? 1 : 0,
(long long)pos.superframe, (long long)pos.into_us,
pos.slot && *pos.slot == *want ? 1 : 0, p.RxAtrib.rssi[0] - 110, p.RxAtrib.snr[0]);
want && pos.slot && *pos.slot == *want ? 1 : 0, want ? 1 : 0,
p.RxAtrib.rssi[0] - 110, p.RxAtrib.snr[0]);
emit(l);
});
});
Expand Down Expand Up @@ -2441,6 +2472,33 @@ int run_ue(IRadio *dev, const Cfg &c) {
fit_steps_seen = sync.Steps();
fit_rejected_seen = sync.Rejected();
}
/* Re-attached to a restarted DU (the RX path started the grant state
* over): what belonged to the old DU instance goes, as on a switch. */
if (reattached) {
reattached = false;
hrx = HarqReceiver{}; /* its DL sequence space starts again */
mr_gen = 0; /* its MR window is its new superframe count */
nbrs.clear();
have_nbrs_gen = false;
gap_parked.clear();
next_gap_sf = -1; /* deadlines on the old count: re-set on the new one */
no_step_until_sf = -1;
ho_seen.reset(); /* commands aired by the old instance */
ho_pending.reset();
/* A target that restarted mid-handover lost the staged UE: the way
* back stays, its deadline re-based on the new count. */
if (ho_back)
ho_back->deadline_sf = du_now / P + ho_fallback_sf;
force_beacon = true;
listen_from = -1;
char l[200];
snprintf(l, sizeof l,
"{\"ev\":\"cell.ue.reattach\",\"ue\":%d,\"cell\":%u,\"old_epoch\":%u,"
"\"new_epoch\":%u,\"sf\":%lld}",
me_a.load(), cell_a.load(), reattach_old_epoch, reattach_new_epoch,
(long long)(du_now / P));
emit(l);
}
int64_t sfn = du_now >= 0 ? du_now / P : 0;
/* M6: a fresh handover command is accepted once (HoReceiver: newer
* id, instant ahead); at its instant the UE switches: the clock is
Expand Down
18 changes: 18 additions & 0 deletions src/cell/GrantMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ class GrantMapReceiver {

explicit GrantMapReceiver(uint16_t cell_id) : cell_id_(cell_id) {}
uint16_t cell_id() const { return cell_id_; }
uint32_t newest_epoch() const { return newest_epoch_; }

/* True when `m` was accepted (queued). */
bool offer(const GrantMap &m) {
Expand Down Expand Up @@ -221,6 +222,23 @@ class GrantMapReceiver {
}
return current_ ? &*current_ : nullptr;
}
/* A restarted DU: its maps begin again at a low epoch, on another
* superframe count. A map offer() would refuse as old, but due within
* `window` superframes of the beacon carrying it (`beacon_sf`, that beacon's
* TSF / period: the DU's count NOW), starts the receiver over — the caller
* asks only once it has seen the DU's clock jump (a TsfSync step). A stale
* map from before the restart is due far from the new count and still
* refused. True when it started over. */
bool offer_after_restart(const GrantMap &m, uint64_t beacon_sf, uint64_t window = 16) {
/* Strictly lower: the same epoch again is the live map repeated. */
if (m.cell_id != cell_id_ || !have_any_ || m.epoch >= newest_epoch_)
return false;
const uint64_t d = m.effective_sf > beacon_sf ? m.effective_sf - beacon_sf : beacon_sf - m.effective_sf;
if (d > window)
return false;
rebind(cell_id_);
return offer(m);
}
/* Serve another cell from scratch (handover, re-association). */
void rebind(uint16_t cell_id) {
cell_id_ = cell_id;
Expand Down
34 changes: 34 additions & 0 deletions tests/cell_m6_selftest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <vector>

#include "cell/F1.h"
#include "cell/GrantMap.h"
#include "cell/Handover.h"
#include "cell/HoCommand.h"
#include "cell/MeasGap.h"
Expand Down Expand Up @@ -589,6 +590,39 @@ int main() {
CHECK(disc.phase() == 51200);
}

/* --- Re-attachment after a DU restart (GrantMap.h offer_after_restart). --- */
{
GrantMapReceiver rx(1);
GrantMap m;
m.cell_id = 1;
m.epoch = 13;
m.effective_sf = 400;
m.grants = {{}};
m.grants[0].ue = 1;
CHECK(rx.offer(m) && rx.newest_epoch() == 13);
/* The restarted DU: epoch 2, due at 97 while its beacons read sf 94. */
GrantMap n = m;
n.epoch = 2;
n.effective_sf = 97;
CHECK(!rx.offer(n)); /* an old epoch: refused */
CHECK(!rx.offer_after_restart(n, 400)); /* not near the beacon's count */
GrantMap stale = m;
stale.epoch = 12;
stale.effective_sf = 390;
CHECK(!rx.offer_after_restart(stale, 94)); /* a map from before the restart */
GrantMap other = n;
other.cell_id = 2;
CHECK(!rx.offer_after_restart(other, 94)); /* another cell's */
CHECK(rx.offer_after_restart(n, 94)); /* starts over */
CHECK(rx.newest_epoch() == 2 && rx.current(97) && rx.current(97)->epoch == 2);
GrantMap n2 = n;
n2.epoch = 3;
n2.effective_sf = 115;
CHECK(rx.offer(n2) && rx.current(115)->epoch == 3); /* and runs on normally */
CHECK(!rx.offer_after_restart(m, 115)); /* never back to a higher epoch this way */
CHECK(!rx.offer_after_restart(n2, 115)); /* the live map repeated: no restart */
}

if (failures == 0)
std::printf("cell_m6_selftest: all checks passed\n");
return failures == 0 ? 0 : 1;
Expand Down
Loading