From 1f4593675c2c05dda22a6dc010a9030c45406244 Mon Sep 17 00:00:00 2001 From: Abtin Keshavarzian Date: Mon, 27 Jul 2026 14:07:00 -0700 Subject: [PATCH] [csl-tx-scheduler] simplify CSL window determination and scheduling (#13403) This commit refactors CSL transmission scheduling by storing the targeted 64-bit CSL window timestamp `mNeighborCslWindow` directly on `CslTxScheduler`. Specifically: - Adds `DetermineNextCslWindow()` method to `NeighborInfo` which calculates and returns the absolute 64-bit CSL window timestamp (`Radio::Time64`). - Updates `RescheduleCslTx()` to select the child with the earliest CSL window, storing `mNeighborCslWindow` and scheduling `mTimer`. - Updates `HandleFrameRequest()` to check if `mNeighborCslWindow` has passed (`mNeighborCslWindow < radioNow`), aborting if MAC was delayed by other operations. - Uses `mNeighborCslWindow` directly to calculate `TxDelay` when preparing the frame, avoiding redundant delay recalculations. --- src/core/thread/csl_tx_scheduler.cpp | 86 +++++++++++----------------- src/core/thread/csl_tx_scheduler.hpp | 22 ++++--- 2 files changed, 44 insertions(+), 64 deletions(-) diff --git a/src/core/thread/csl_tx_scheduler.cpp b/src/core/thread/csl_tx_scheduler.cpp index 698220846..04a76c383 100644 --- a/src/core/thread/csl_tx_scheduler.cpp +++ b/src/core/thread/csl_tx_scheduler.cpp @@ -40,6 +40,7 @@ CslTxScheduler::CslTxScheduler(Instance &aInstance) : InstanceLocator(aInstance) , mCslTxNeighbor(nullptr) , mCslTxMessage(nullptr) + , mNeighborCslWindow(0) , mFrameContext() , mTimer(aInstance) { @@ -119,50 +120,51 @@ void CslTxScheduler::Clear(void) mFrameContext.mMessageNextOffset = 0; mCslTxNeighbor = nullptr; mCslTxMessage = nullptr; + mNeighborCslWindow = 0; mTimer.Stop(); } -/** - * Always finds the most recent CSL tx among all children, - * and requests `Mac` to do CSL tx at specific time. It shouldn't be called - * when `Mac` is already starting to do the CSL tx (indicated by `mCslTxMessage`). - */ void CslTxScheduler::RescheduleCslTx(void) { - uint32_t minDelayTime = Time::kMaxDuration; - CslNeighbor *bestNeighbor = nullptr; + // Finds the CSL neighbor with the earliest upcoming CSL window + // and a pending indirect message, and schedules a timer for its + // transmission. + + Radio::Time64 radioNow = Get().GetNow(); + TimeMilli localNow = TimerMilli::GetNow(); + + mCslTxNeighbor = nullptr; #if OPENTHREAD_FTD for (Child &child : Get().Iterate(Child::kInStateAnyExceptInvalid)) { - uint32_t delay; - uint32_t cslTxDelay; + Radio::Time64 txWindow; if (!child.IsCslSynchronized() || child.GetIndirectMessageCount() == 0) { continue; } - delay = GetNextCslTransmissionDelay(child, cslTxDelay, mCslFrameRequestAheadUs); + txWindow = child.DetermineNextCslWindow(radioNow, mCslFrameRequestAheadUs); - if (delay < minDelayTime) + if ((mCslTxNeighbor == nullptr) || (txWindow < mNeighborCslWindow)) { - minDelayTime = delay; - bestNeighbor = &child; + mCslTxNeighbor = &child; + mNeighborCslWindow = txWindow; } } #endif - if (bestNeighbor != nullptr) + if (mCslTxNeighbor != nullptr) { - mTimer.Start(Time::UsecToMsec(minDelayTime)); + uint32_t interval = static_cast(mNeighborCslWindow - radioNow - mCslFrameRequestAheadUs); + + mTimer.FireAt(localNow + Time::UsecToMsec(interval)); } else { mTimer.Stop(); } - - mCslTxNeighbor = bestNeighbor; } void CslTxScheduler::HandleTimer(void) @@ -175,9 +177,7 @@ exit: return; } -uint32_t CslTxScheduler::GetNextCslTransmissionDelay(const CslNeighbor &aCslNeighbor, - uint32_t &aDelayFromLastRx, - uint32_t aAheadUs) const +Radio::Time64 CslTxScheduler::NeighborInfo::DetermineNextCslWindow(Radio::Time64 aRadioNow, uint32_t aLeadTime) const { // CSL phase is in units of 10 symbols from the first symbol of the frame // containing the CSL IE was transmitted until the next channel sample, @@ -220,23 +220,16 @@ uint32_t CslTxScheduler::GetNextCslTransmissionDelay(const CslNeighbor &aCslNeig // = nextTmh - phrDuration // = lastRxTimestamp + 160us * (n * cslPeriod + cslPhase) - Radio::Time64 radioNow = Get().GetNow(); - uint32_t periodInUs; - Radio::Time64 firstTxWindow; - Radio::Time64 nextTxWindow; + uint32_t periodInUs = GetCslPeriod() * Radio::kUsPerTenSymbols; + Radio::Time64 firstTxWindow = GetLastRxTimestamp() + GetCslPhase() * Radio::kUsPerTenSymbols; + Radio::Time64 nextTxWindow = aRadioNow - (aRadioNow % periodInUs) + (firstTxWindow % periodInUs); - periodInUs = aCslNeighbor.GetCslPeriod() * Radio::kUsPerTenSymbols; - firstTxWindow = aCslNeighbor.GetLastRxTimestamp() + aCslNeighbor.GetCslPhase() * Radio::kUsPerTenSymbols; - nextTxWindow = radioNow - (radioNow % periodInUs) + (firstTxWindow % periodInUs); - - while (nextTxWindow < radioNow + aAheadUs) + while (nextTxWindow < aRadioNow + aLeadTime) { nextTxWindow += periodInUs; } - aDelayFromLastRx = static_cast(nextTxWindow - aCslNeighbor.GetLastRxTimestamp()); - - return static_cast(nextTxWindow - radioNow - aAheadUs); + return nextTxWindow; } #if OPENTHREAD_CONFIG_RADIO_LINK_IEEE_802_15_4_ENABLE @@ -244,8 +237,6 @@ uint32_t CslTxScheduler::GetNextCslTransmissionDelay(const CslNeighbor &aCslNeig Mac::TxFrame *CslTxScheduler::HandleFrameRequest(Mac::TxFrames &aTxFrames) { Mac::TxFrame *frame = nullptr; - uint32_t txDelay; - uint32_t delay; VerifyOrExit(mCslTxNeighbor != nullptr); VerifyOrExit(mCslTxNeighbor->IsCslSynchronized()); @@ -289,27 +280,18 @@ Mac::TxFrame *CslTxScheduler::HandleFrameRequest(Mac::TxFrames &aTxFrames) frame->SetRxChannelAfterTxDone(Get().GetPanChannel()); } - delay = GetNextCslTransmissionDelay(*mCslTxNeighbor, txDelay, /* aAheadUs */ 0); + // Verify that the CSL target window has not already passed. + // If MAC was delayed by other operations and started the CSL TX + // operation late, the target window may be missed, so we abort. - // We make sure that delay is less than `mCslFrameRequestAheadUs` - // plus some guard time. Note that we used `mCslFrameRequestAheadUs` - // in `RescheduleCslTx()` when determining the next CSL delay to - // schedule CSL tx with `Mac` but here we calculate the delay with - // zero `aAheadUs`. All the timings are in usec but when passing - // delay to `Mac` we divide by `1000` (to convert to msec) which - // can round the value down and cause `Mac` to start operation a - // bit (some usec) earlier. This is covered by adding the guard - // time `kFramePreparationGuardInterval`. - // - // In general this check handles the case where `Mac` is busy with - // other operations and therefore late to start the CSL tx operation - // and by the time `HandleFrameRequest()` is invoked, we miss the - // current CSL window and move to the next window. + if (mNeighborCslWindow < Get().GetNow()) + { + frame = nullptr; + ExitNow(); + } - VerifyOrExit(delay <= mCslFrameRequestAheadUs + kFramePreparationGuardInterval, frame = nullptr); - - frame->SetTxDelay(txDelay); frame->SetTxDelayBaseTime(Radio::ConvertTime64To32(mCslTxNeighbor->GetLastRxTimestamp())); + frame->SetTxDelay(static_cast(mNeighborCslWindow - mCslTxNeighbor->GetLastRxTimestamp())); frame->SetCsmaCaEnabled(true); exit: diff --git a/src/core/thread/csl_tx_scheduler.hpp b/src/core/thread/csl_tx_scheduler.hpp index d308aa972..c01d33bdd 100644 --- a/src/core/thread/csl_tx_scheduler.hpp +++ b/src/core/thread/csl_tx_scheduler.hpp @@ -75,6 +75,8 @@ public: */ class NeighborInfo { + friend class CslTxScheduler; + public: /** * Returns the number of CSL triggered tx attempts. @@ -195,6 +197,8 @@ public: void SetLastRxTimestamp(Radio::Time64 aLastRxTimestamp) { mLastRxTimestamp = aLastRxTimestamp; } private: + Radio::Time64 DetermineNextCslWindow(Radio::Time64 aRadioNow, uint32_t aLeadTime) const; + uint8_t mCslTxAttempts : 7; bool mCslSynchronized : 1; uint8_t mCslChannel; @@ -232,18 +236,11 @@ public: void Clear(void); private: - // Guard time in usec to add when checking delay while preparing the CSL frame for tx. - static constexpr uint32_t kFramePreparationGuardInterval = 1500; - typedef IndirectSenderBase::FrameContext FrameContext; void RescheduleCslTx(void); void HandleTimer(void); - uint32_t GetNextCslTransmissionDelay(const CslNeighbor &aCslNeighbor, - uint32_t &aDelayFromLastRx, - uint32_t aAheadUs) const; - // Callbacks from `Mac` Mac::TxFrame *HandleFrameRequest(Mac::TxFrames &aTxFrames); void HandleSentFrame(const Mac::TxFrame &aFrame, Error aError); @@ -254,11 +251,12 @@ private: using CslTxTimer = TimerMilliIn; - uint32_t mCslFrameRequestAheadUs; - CslNeighbor *mCslTxNeighbor; - Message *mCslTxMessage; - FrameContext mFrameContext; - CslTxTimer mTimer; + uint32_t mCslFrameRequestAheadUs; + CslNeighbor *mCslTxNeighbor; + Message *mCslTxMessage; + Radio::Time64 mNeighborCslWindow; + FrameContext mFrameContext; + CslTxTimer mTimer; }; /**