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
44 changes: 40 additions & 4 deletions docs/cell-m6.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ is no scan, no clock re-acquisition and no association exchange.
| piece | where | what |
|---|---|---|
| filtered measurements | `src/cell/Handover.h` `L3Filter`, `MeasTable` | 3GPP L3 filter per (cell, UE) over uplink RSSI/SNR, and EVM from the frames that carry one. Stale = absent, never weak. |
| measurement reports | F1 `Report.meas` / `heard` | each DU's own UEs and the other cells' UEs it hears, with sample ages; EVM since F1 v16 (none = a sentinel) |
| measurement reports | F1 `Report.meas` / `heard` | each DU's own UEs and the other cells' UEs it hears, with sample ages; EVM since F1 v16 (none = a sentinel); each own UE's TA since v17 |
| A3 (CHO) condition | `Handover.h` `A3Trigger`; `tools/ran-controller` | offset, hysteresis, per-neighbour time-to-trigger, dwell |
| the command | `src/cell/HoCommand.h` `HoCommand`, F1 `Type::Handover` | UE, target id and position, switch instant, TSF delta |
| the UE's copy | `HoIe` in the source DU's beacon | CRC and cell-key tag (forgery is inert); `HoReceiver` rejects a replay |
Expand Down Expand Up @@ -120,10 +120,10 @@ The first runs, single, measured with testrig `scripts/m5_ran_coord.sh` plus
| source | released the UE at switch + 21 | kept the UE (still heard as its own): 1092 / 1092 uplinks in slot after the switch |
| CU | `ran.ho_done` 5 superframes after the instant | `ran.ho_timeout` |

- **Timing advance:** the target's TA loop starts from 0 for the new UE.
- **Timing advance:** the target's TA loop started from 0 for the new UE.
The first arrivals at the target sat about −418 µs from the UL target, the
same offset the source's TA had converged to. Carrying the TA in the
command is a possible refinement.
same offset the source's TA had converged to. The command now carries the
TA (F1 v17, see "The timing advance in the handover command").
- **The UE's uplink in the fault case:** while no target map is in force,
the UE does not rebuild its uplink beacon. Its uplink never left the
source, which is part of why the fallback is seamless.
Expand Down Expand Up @@ -1660,6 +1660,42 @@ a standing difference defaults it to the medians of its passing runs:
**Result:** on the changed rig every M6 suite passes again: handover,
measgap, three_ho, daps, daps_dl, daps_xch, crossing3, crossing.

## The timing advance in the handover command

The UEs on this rig run a TA of about −420 µs: their inherent actuation
offset, the same at every cell. A target whose TA loop starts from 0 sees
the arriving UE that far off and halves it every 2 s (`CELL_TA_EVERY_S`,
gain 0.5). Since F1 v17 the CU carries the TA instead:
- each DU reports its own UEs' TA in `UeMeas.ta_us`: the TA of each UE's
grant in the map on air, taken when a map goes on air (a failed publish,
a released ID or a pending loop step never reaches the report);
- the CU puts the source's value in `HoCommand.ta_us` (`RAN_HO_TA`, on by
default; `ran.ho_cmd` logs it);
- the target starts the arriving UE's TA loop and its first grant there at
it (`cell.ho.ta_carried`). The UE's first uplink on the target's grant is
already advanced.

Rig, testrig `scripts/m6_ho_ta.sh`: UE1 cell 1 → 2, forced at 70 s once the
source's TA had converged, three interleaved A/B rounds, measured at the
target on the arriving UE's in-slot uplinks:

| | TA carried (µs) | arrival error, first 40 uplinks (median, µs) | uplinks until within 50 µs |
|---|---|---|---|
| `RAN_HO_TA=1` | −426, −426, −425 | −2, −1, −1 | 0, 0, 0 |
| `RAN_HO_TA=0` | 0 | −427, −428.5, −425.5 | 98, 84, 79 |

- **Slot hits do not change on this layout:** 38–39 of the first 40
uplinks are in slot either way, since 427 µs still lands in the UL slot
(the misses are single stale-phase beacons). What the carried TA buys is
the guard margin from the first frame. It decides slot hits only where
the UL slot and guard are tighter than the UE's offset.
- **The carried value is only as good as the source's loop.** With the A3
on the pinned levels, the handover fired ~4 s after the attach. The
command carried −207 to −211 µs (one TA step) or 0 (before the first),
and the target's first arrivals were −210 or −420 µs: the value is
applied exactly, but a UE handed over soon after attaching carries a
half-converged TA.

## The 802.11 roam baseline

The M6 issue measured the handover "vs the ~100 ms roam ghost bar", but no
Expand Down
27 changes: 25 additions & 2 deletions examples/cell/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1394,7 +1394,9 @@ int run_du(IRadio *dev, const Cfg &c) {
uint32_t id = 0;
uint8_t pos = 0;
uint64_t eff_sf = 0;
int16_t ta_us = 0; /* the UE's TA at its source (HoCommand v17) */
};
std::map<uint16_t, int16_t> ta_report; /* f1_mu: each own UE's TA in the map on air */
struct HoOut {
uint32_t id = 0;
uint64_t eff_sf = 0;
Expand Down Expand Up @@ -1886,6 +1888,9 @@ int run_du(IRadio *dev, const Cfg &c) {
if (auto h = ue_home.find(u.ue); h != ue_home.end() && r.homes.size() < f1::kMaxUes)
r.homes.push_back({u.ue, h->second});
f1::fill_report_measurements(r, c.cell, meas_tab.snapshot(now_ms()));
for (auto &q : r.meas) /* each own UE's TA now (carried on a handover, v17) */
if (auto t = ta_report.find(q.ue); t != ta_report.end())
q.ta_us = t->second;
/* The UEs' own measurements (their MR IEs): each entry's age is its
* sample's plus the report's since; each UE's serving entry first,
* whole UEs only against the cap (a truncation is logged). */
Expand Down Expand Up @@ -2142,7 +2147,7 @@ int run_du(IRadio *dev, const Cfg &c) {
const auto eff = static_cast<uint64_t>(static_cast<int64_t>(cmd.effective_net_sf) - off);
{
std::lock_guard<std::mutex> lk(f1_mu);
ho_in[cmd.target_ue] = {cmd.id, cmd.target_pos, eff};
ho_in[cmd.target_ue] = {cmd.id, cmd.target_pos, eff, cmd.ta_us};
}
/* Pre-admit under exactly the id the CU gave it (the registry
* hands out the lowest free one; the CU chose that one). */
Expand Down Expand Up @@ -2682,8 +2687,17 @@ int run_du(IRadio *dev, const Cfg &c) {
ho_eff || !synced)
continue; /* not yet (or another instant is being published, or no clock) */
set_position(g, hi->second.pos);
/* The UE's TA at its source: its clock and actuation phase do not
* change with the cell, so the target's loop starts there. */
g.ta_us = hi->second.ta_us;
next.grants.push_back(g);
reg.context(u.ue);
reg.context(u.ue).ta.set_ta(hi->second.ta_us);
if (hi->second.ta_us) {
char l[120];
snprintf(l, sizeof l, "{\"ev\":\"cell.ho.ta_carried\",\"ue\":%u,\"ta_us\":%d}", u.ue,
hi->second.ta_us);
emit(l);
}
/* Fresh DL state (an id reused after another UE left keeps
* nothing of it); its DL queue is the DAPS buffer: kept. */
dlue.insert_or_assign(u.ue, make_dlue(u.ue));
Expand Down Expand Up @@ -3041,6 +3055,15 @@ int run_du(IRadio *dev, const Cfg &c) {
}
prev_map = map;
map = next;
{
/* The TA each UE runs is its grant's in the map now on air: the
* report carries exactly that (a carried arrival's at once, a
* released ID's never, a failed publish's not at all). */
std::lock_guard<std::mutex> lk(f1_mu);
ta_report.clear();
for (const Grant &g : map.grants)
ta_report[g.ue] = g.ta_us;
}
if (f1_eff) {
std::lock_guard<std::mutex> lk(f1_mu);
/* due() released it this pass and the deadline was checked just
Expand Down
10 changes: 8 additions & 2 deletions src/cell/F1.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ namespace devourer::cell::f1 {
/* 12: Downlink (CU -> DU, the CU's DL PDUs; DAPS duplicates them). 13:
* Report carries the DU's suspend-gap period. */
/* 14: Uplink carries its reception instant (net_us). */
/* v14: Uplink.net_us; v15: ChannelSwitch; v16: EVM in HeardUe and UeMeas */
constexpr uint8_t kVersion = 16;
/* v14: Uplink.net_us; v15: ChannelSwitch; v16: EVM in HeardUe and UeMeas;
* v17: UeMeas.ta_us and HoCommand.ta_us (the TA carried in a handover) */
constexpr uint8_t kVersion = 17;
constexpr int8_t kNoEvm8 = INT8_MIN; /* HeardUe.evm_db: none measured */
constexpr int16_t kNoEvmQ = INT16_MIN; /* UeMeas.evm_qdb: none measured */
constexpr size_t kTagLen = 8;
Expand Down Expand Up @@ -144,6 +145,7 @@ struct UeMeas {
int16_t snr_qdb = 0;
uint16_t age_ms = 0; /* since its newest sample, at report time (saturates) */
int16_t evm_qdb = kNoEvmQ; /* filtered EVM, quarter dB; kNoEvmQ: none */
int16_t ta_us = 0; /* the DU's timing advance for this UE now */
bool operator==(const UeMeas &) const = default;
};

Expand Down Expand Up @@ -410,6 +412,7 @@ inline std::vector<uint8_t> encode_report(const Report &r, const Mac &mac) {
put(m, static_cast<uint16_t>(q.snr_qdb), 2);
put(m, q.age_ms, 2);
put(m, static_cast<uint16_t>(q.evm_qdb), 2);
put(m, static_cast<uint16_t>(q.ta_us), 2);
}
m.push_back(static_cast<uint8_t>(r.ue_meas.size()));
for (const UeCellMeas &q : r.ue_meas) {
Expand Down Expand Up @@ -492,6 +495,7 @@ inline std::optional<Report> decode_report(const uint8_t *p, size_t n, const Mac
q.snr_qdb = static_cast<int16_t>(static_cast<uint16_t>(r.get(2)));
q.age_ms = static_cast<uint16_t>(r.get(2));
q.evm_qdb = static_cast<int16_t>(static_cast<uint16_t>(r.get(2)));
q.ta_us = static_cast<int16_t>(static_cast<uint16_t>(r.get(2)));
x.meas.push_back(q);
}
const size_t nu = r.get(1);
Expand Down Expand Up @@ -599,6 +603,7 @@ inline std::vector<uint8_t> encode_handover(uint16_t du, const HoCommand &h, con
put(m, h.seq, 8);
m.insert(m.end(), h.ctx.begin(), h.ctx.end());
put_home(m, h.home);
put(m, static_cast<uint16_t>(h.ta_us), 2);
return seal(std::move(m), mac);
}

Expand All @@ -625,6 +630,7 @@ inline std::optional<std::pair<uint16_t, HoCommand>> decode_handover(const uint8
for (auto &b : h.ctx)
b = static_cast<uint8_t>(r.get(1));
h.home = get_home(r);
h.ta_us = static_cast<int16_t>(static_cast<uint16_t>(r.get(2)));
if (!r.ok || r.i != r.n)
return std::nullopt;
return std::make_pair(du, h);
Expand Down
5 changes: 5 additions & 0 deletions src/cell/HoCommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ struct HoCommand {
/* The UE's home identity (Keys.h): the target records it for the id it
* gives the UE and reports it on, for the UE's next handover. */
HomeId home{};
/* The UE's timing advance at the source (us; F1 v17): the target starts
* its TA loop and the UE's first grant there, instead of at 0. The TA
* corrects the UE's clock and actuation phase, which do not change with
* the cell. */
int16_t ta_us = 0;
bool operator==(const HoCommand &) const = default;
};

Expand Down
15 changes: 13 additions & 2 deletions tests/cell_m6_selftest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,16 @@ int main() {
CHECK(r.heard.size() == 1 && r.heard[0].evm_db == f1::kNoEvm8 + 1);
}

/* --- F1 v17: the DU's TA per own UE round-trips in its report. --- */
{
f1::Report r;
r.du = 1;
r.meas = {{1, -200, 120, 0, -124, -431}, {2, -220, 100, 5, f1::kNoEvmQ, 37}};
const auto w = f1::encode_report(r, {});
auto d = f1::decode_report(w.data(), w.size(), {});
CHECK(d && d->meas == r.meas && d->meas[0].ta_us == -431 && d->meas[1].ta_us == 37);
}

/* --- F1 v2: the measurement fields round-trip. --- */
{
f1::Report r;
Expand Down Expand Up @@ -850,11 +860,12 @@ int main() {
HoCommand hc;
hc.id = 9;
hc.home = HomeId{true, 0, 0, jm};
hc.ta_us = -428; /* v17: the source's TA travels with the UE */
const auto wh = f1::encode_handover(2, hc, fake_mac(0xF1));
auto dh = f1::decode_handover(wh.data(), wh.size(), fake_mac(0xF1));
CHECK(dh && dh->second.home == hc.home);
CHECK(dh && dh->second.home == hc.home && dh->second.ta_us == -428);
auto badk = wh;
badk[badk.size() - 8 - 7] = 2; /* home kind 2: unknown */
badk[badk.size() - 8 - 2 - 7] = 2; /* home kind 2: unknown (before the 2-byte TA) */
CHECK(!f1::decode_handover(badk.data(), badk.size(), Mac{}));
/* Per-UE area keys: A_u from the UE's key, wrapped for one DU. */
const auto au = ue_area_key(fake_mac(0x11));
Expand Down
12 changes: 10 additions & 2 deletions tools/ran-controller/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
// RAN_SPARE_CHANNELS channels the planner may propose (advisory)
// RAN_RETUNE_APPLY [0] 1: apply a proposal — move that running DU and
// its UEs (F1 ChannelSwitch, ChannelSwitch.h)
// RAN_HO_TA [1] carry the UE's TA at its source in the handover
// command (the target's TA loop starts there)
// RAN_CS_LEAD_SF [20] a move's instant, this many superframes ahead
// RAN_CS_BACKOFF_S [30] a DU whose move was refused or timed out is not
// asked again before this
Expand Down Expand Up @@ -237,6 +239,7 @@ int main() {
const int edge_dbm = static_cast<int>(envl("RAN_EDGE_DBM", -90, -127, 0));
const uint64_t lead_sf = static_cast<uint64_t>(envl("RAN_LEAD_SF", 12, 2, 100000));
const bool retune_apply = envl("RAN_RETUNE_APPLY", 0, 0, 1) != 0;
const bool ho_ta = envl("RAN_HO_TA", 1, 0, 1) != 0; /* carry the source's TA in a handover */
const uint64_t cs_lead_sf = static_cast<uint64_t>(envl("RAN_CS_LEAD_SF", 20, 4, 100000));
const int cs_backoff_s = static_cast<int>(envl("RAN_CS_BACKOFF_S", 30, 1, 3600));
const int plan_ms = static_cast<int>(envl("RAN_PLAN_MS", 1000, 50, 60000));
Expand Down Expand Up @@ -1110,6 +1113,11 @@ int main() {
cmd.target = *act;
cmd.target_ue = tue;
cmd.target_pos = tpos;
/* The UE's TA at the source (its report): the target starts there. */
if (ho_ta)
for (const f1::UeMeas &m : sr.meas)
if (m.ue == q.ue)
cmd.ta_us = m.ta_us;
cmd.target_channel = tr.channel; /* != the source's: inter-frequency */
const double age_us =
std::chrono::duration<double, std::micro>(now - dit->second.at).count();
Expand Down Expand Up @@ -1170,10 +1178,10 @@ int main() {
snprintf(b, sizeof b,
"{\"ev\":\"ran.ho_cmd\",\"id\":%u,\"ue\":%u,\"from\":%u,\"to\":%u,"
"\"target_ue\":%u,\"pos\":%u,\"effective_net_sf\":%llu,\"delta_us\":%lld,"
"\"ctx\":%d,\"home\":\"%s\"}",
"\"ctx\":%d,\"home\":\"%s\",\"ta_us\":%d}",
cmd.id, cmd.ue, cmd.source, cmd.target, cmd.target_ue, cmd.target_pos,
(unsigned long long)cmd.effective_net_sf, (long long)cmd.tsf_delta_us,
ctx_ok ? 1 : 0, home ? home_name(*home).c_str() : "none");
ctx_ok ? 1 : 0, home ? home_name(*home).c_str() : "none", cmd.ta_us);
emit(b);
}
}
Expand Down
Loading