[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.
This commit is contained in:
Abtin Keshavarzian
2026-07-27 14:07:00 -07:00
committed by GitHub
parent db7827b3f7
commit 1f4593675c
2 changed files with 44 additions and 64 deletions
+34 -52
View File
@@ -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<Radio::Radio>().GetNow();
TimeMilli localNow = TimerMilli::GetNow();
mCslTxNeighbor = nullptr;
#if OPENTHREAD_FTD
for (Child &child : Get<ChildTable>().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<uint32_t>(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<Radio::Radio>().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<uint32_t>(nextTxWindow - aCslNeighbor.GetLastRxTimestamp());
return static_cast<uint32_t>(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<Mac::Mac>().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<Radio::Radio>().GetNow())
{
frame = nullptr;
ExitNow();
}
VerifyOrExit(delay <= mCslFrameRequestAheadUs + kFramePreparationGuardInterval, frame = nullptr);
frame->SetTxDelay(txDelay);
frame->SetTxDelayBaseTime(Radio::ConvertTime64To32(mCslTxNeighbor->GetLastRxTimestamp()));
frame->SetTxDelay(static_cast<uint32_t>(mNeighborCslWindow - mCslTxNeighbor->GetLastRxTimestamp()));
frame->SetCsmaCaEnabled(true);
exit:
+10 -12
View File
@@ -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<CslTxScheduler, &CslTxScheduler::HandleTimer>;
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;
};
/**