[mac] simplify CSL receiver window calculation (#13591)

This commit simplifies the CSL sample window calculation in
`SubMac::CslReceiver`:

- Replaces `GetWindowEdges()` with `DetermineWindow()` taking an
  explicit sample time and returning a `Window` struct.
- Uses `Radio::SyncedTime` in `Window` to keep the radio start time
  and local timer fire time synchronized.
- Eliminates `GetNextCycleDrift()` by determining the next CSL
  window directly after advancing the sample time.
- Cleans up `ProcessRxFrame()` and `ProcessTxDone()`.
- Introduces `SubMac::HasAddress()` to check whether a MAC address
  matches any of the device's addresses (short, alternate short,
  or extended).
This commit is contained in:
Abtin Keshavarzian
2026-09-11 13:00:53 -07:00
committed by GitHub
parent a33e599cca
commit d45e676944
3 changed files with 145 additions and 102 deletions
+34 -2
View File
@@ -160,6 +160,35 @@ void SubMac::SetExtAddress(const ExtAddress &aExtAddress)
LogDebg("RadioExtAddress: %s", mExtAddress.ToString().AsCString());
}
bool SubMac::HasAddress(const Address &aAddress) const
{
bool matches = false;
switch (aAddress.GetType())
{
case Address::kTypeNone:
break;
case Address::kTypeShort:
if (aAddress.GetShort() == mShortAddress)
{
matches = true;
}
else if (mAlternateShortAddress != kShortAddrInvalid)
{
matches = (aAddress.GetShort() == mAlternateShortAddress);
}
break;
case Address::kTypeExtended:
matches = (aAddress.GetExtended() == mExtAddress);
break;
}
return matches;
}
void SubMac::SetRxOnWhenIdle(bool aRxOnWhenIdle)
{
mRxOnWhenIdle = aRxOnWhenIdle;
@@ -297,7 +326,10 @@ void SubMac::HandleReceiveDone(RxFrame *aFrame, Error aError)
}
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
mCslReceiver.UpdateLastSyncTimestamp(aFrame, aError);
if ((aFrame != nullptr) && (aError == kErrorNone))
{
mCslReceiver.ProcessRxFrame(*aFrame);
}
#endif
#if OPENTHREAD_CONFIG_MAC_FILTER_ENABLE
@@ -571,7 +603,7 @@ void SubMac::HandleTransmitDone(TxFrame &aFrame, RxFrame *aAckFrame, Error aErro
mCallbacks.RecordCcaStatus(ccaSuccess, aFrame.GetChannel());
}
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_ENABLE
mCslReceiver.UpdateLastSyncTimestamp(frameInfo, aAckFrame);
mCslReceiver.ProcessTxDone(frameInfo, aAckFrame);
#endif
break;
+20 -6
View File
@@ -287,6 +287,16 @@ public:
*/
void SetExtAddress(const ExtAddress &aExtAddress);
/**
* Indicates whether a given MAC address matches this device's short, alternate short, or extended address.
*
* @param[in] aAddress The MAC address to check.
*
* @retval TRUE If @p aAddress matches any of this device's addresses.
* @retval FALSE If @p aAddress does not match any of this device's addresses.
*/
bool HasAddress(const Address &aAddress) const;
/**
* Registers a callback to provide received packet capture for IEEE 802.15.4 frames.
*
@@ -642,8 +652,8 @@ private:
void Init(void);
void Stop(void) { mTimer.Stop(); }
void SetParams(uint16_t aPeriod, uint8_t aChannel, ShortAddress aShortAddr, const ExtAddress &aExtAddr);
void UpdateLastSyncTimestamp(const TxFrame::ParseInfo &aFrameInfo, RxFrame *aAckFrame);
void UpdateLastSyncTimestamp(RxFrame *aFrame, Error aError);
void ProcessTxDone(const TxFrame::ParseInfo &aFrameInfo, RxFrame *aAckFrame);
void ProcessRxFrame(const RxFrame &aFrame);
void HandleTimer(void);
const CslAccuracy &GetParentAccuracy(void) const { return mParentAccuracy; }
@@ -654,14 +664,18 @@ private:
static constexpr uint32_t kMinReceiveOnAfter = OPENTHREAD_CONFIG_MIN_RECEIVE_ON_AFTER;
static constexpr uint32_t kReceiveTimeAhead = OPENTHREAD_CONFIG_CSL_RECEIVE_TIME_AHEAD;
struct Window
{
Radio::SyncedTime mStartTime;
uint32_t mDuration;
};
void RestartTimerAfterSyncUpdate(void);
void SetLastSyncToNow(void);
void GetWindowEdges(uint32_t &aAhead, uint32_t &aAfter);
void DetermineWindow(const Radio::SyncedTime &aSampleTime, Window &aWindow) const;
uint32_t DetermineClockDrift(uint32_t aIntervalUs) const;
uint32_t GetNextCycleDrift(void) const;
bool IsEnabled(void) const { return mPeriod > 0; }
void LogWindow(Radio::Time64 aStart, uint32_t aDuration);
void LogReceived(RxFrame *aFrame);
void LogReceived(const RxFrame &aFrame);
using CslTimer = TimerMicroIn<SubMac, &SubMac::HandleCslReceiverTimer>;
+91 -94
View File
@@ -46,7 +46,6 @@ SubMac::CslReceiver::CslReceiver(Instance &aInstance)
: InstanceLocator(aInstance)
, mTimer(aInstance)
{
mParentAccuracy.Init();
Init();
}
@@ -55,6 +54,7 @@ void SubMac::CslReceiver::Init(void)
mPeriod = 0;
mChannel = 0;
mPeerShort = 0;
mParentAccuracy.Init();
mSampleTime.Clear();
mTimer.Stop();
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_LOCAL_TIME_SYNC
@@ -81,11 +81,12 @@ void SubMac::CslReceiver::RestartTimerAfterSyncUpdate(void)
}
}
void SubMac::CslReceiver::UpdateLastSyncTimestamp(const TxFrame::ParseInfo &aFrameInfo, RxFrame *aAckFrame)
void SubMac::CslReceiver::ProcessTxDone(const TxFrame::ParseInfo &aFrameInfo, RxFrame *aAckFrame)
{
// Actual synchronization timestamp should be from the sent frame instead of the current time.
// Assuming the error here since it is bounded and has very small effect on the final window duration.
VerifyOrExit(IsEnabled());
VerifyOrExit(aAckFrame != nullptr);
VerifyOrExit(aFrameInfo.mParsedFully);
VerifyOrExit(aFrameInfo.Has<CslIe>());
@@ -97,21 +98,21 @@ exit:
return;
}
void SubMac::CslReceiver::UpdateLastSyncTimestamp(RxFrame *aFrame, Error aError)
void SubMac::CslReceiver::ProcessRxFrame(const RxFrame &aFrame)
{
VerifyOrExit(aFrame != nullptr && aError == kErrorNone);
VerifyOrExit(IsEnabled());
#if OPENTHREAD_CONFIG_MAC_CSL_DEBUG_ENABLE
LogReceived(aFrame);
#endif
// Assuming the risk of the parent missing the Enh-ACK in favor of smaller CSL receive window
if ((mPeriod > 0) && aFrame->IsAckedWithSecEnhAck())
if (aFrame.IsAckedWithSecEnhAck())
{
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_LOCAL_TIME_SYNC
SetLastSyncToNow();
#else
mLastSync = aFrame->GetTimestamp();
mLastSync = aFrame.GetTimestamp();
#endif
RestartTimerAfterSyncUpdate();
}
@@ -158,70 +159,66 @@ exit:
void SubMac::CslReceiver::HandleTimer(void)
{
/*
* The handler will be called once per CSL period. When the handler is called, it will set the timer to
* fire at the next CSL sample time and call `ReceiveAt` to start sampling for the current CSL period.
* The timer fires some time before the next sample time.
*
* Timer fires Timer fires
* ^ ^
* x-|------------|-------------------------------------x-|------------|---------------------------------------|
* sample sleep sample sleep
*/
uint32_t periodUs = CslPeriodToUsec(mPeriod);
uint32_t timeAhead, timeAfter;
Radio::Time64 winStart;
uint32_t winDuration;
Window window;
GetWindowEdges(timeAhead, timeAfter);
DetermineWindow(mSampleTime, window);
Get<SubMac>().ReceiveAt(window.mStartTime.GetAsTime64(), window.mDuration, mChannel);
mTimer.FireAt(mSampleTime.GetAsLocalTimeMicro() + periodUs - timeAhead - kReceiveTimeAhead - GetNextCycleDrift());
LogDebg("CSL window start %lu, duration %lu", ToUlong(window.mStartTime.GetAsTime32()), ToUlong(window.mDuration));
winStart = mSampleTime.GetAsTime64() - timeAhead;
winDuration = timeAhead + timeAfter;
mSampleTime += periodUs;
// Advance sample time to the next CSL period, update the radio
// sample time on `Radio` and schedule the timer ahead of the
// next window's start time.
mSampleTime += CslPeriodToUsec(mPeriod);
Get<Radio::Radio>().UpdateCslSampleTime(mSampleTime.GetAsTime32());
Get<SubMac>().ReceiveAt(winStart, winDuration, mChannel);
LogWindow(winStart, winDuration);
DetermineWindow(mSampleTime, window);
mTimer.FireAt(window.mStartTime.GetAsLocalTimeMicro() - kReceiveTimeAhead);
}
void SubMac::CslReceiver::GetWindowEdges(uint32_t &aAhead, uint32_t &aAfter)
void SubMac::CslReceiver::DetermineWindow(const Radio::SyncedTime &aSampleTime, Window &aWindow) const
{
/*
* CSL sample timing diagram
* |<---------------------------------Sample--------------------------------->|<--------Sleep--------->|
* | | |
* |<--Ahead-->|<--UnCert-->|<--Drift-->|<--Drift-->|<--UnCert-->|<--MinWin-->| |
* | | | | | | | |
* ---|-----------|------------|-----------|-----------|------------|------------|----------//------------|---
* -timeAhead CslPhase +timeAfter -timeAhead
* CSL sample timing diagram:
*
* |<------------------------------------ Sample Window ----------------------------------->|
* | |
* |<--MinAhead-->|<-- Uncert -->|<-- Drift -->|<-- Drift -->|<-- Uncert -->|<--MinAfter-->|
* | |<---------- Guard --------->|<---------- Guard --------->| |
* --|--------------|--------------|-------------|-------------|--------------|---------------|---
* ^ ^ ^
* mStartTime SampleTime WindowEnd
* (SampleTime - ahead) (SampleTime + after)
*/
uint32_t semiPeriod = CslPeriodToUsec(mPeriod) / 2;
uint32_t elapsed = 0;
uint32_t semiWindow;
uint32_t halfPeriod = CslPeriodToUsec(mPeriod) / 2;
uint32_t guardInterval = 0;
uint16_t uncertainty;
uint32_t ahead;
uint32_t after;
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_LOCAL_TIME_SYNC
if (mSampleTime.GetAsLocalTimeMicro() > mLastSync)
if (aSampleTime.GetAsLocalTimeMicro() > mLastSync)
{
elapsed = mSampleTime.GetAsLocalTimeMicro() - mLastSync;
guardInterval = DetermineClockDrift(aSampleTime.GetAsLocalTimeMicro() - mLastSync);
}
#else
if (mSampleTime.GetAsTime64() > mLastSync)
if (aSampleTime.GetAsTime64() > mLastSync)
{
elapsed = ClampToUint32(mSampleTime.GetAsTime64() - mLastSync);
guardInterval = DetermineClockDrift(ClampToUint32(aSampleTime.GetAsTime64() - mLastSync));
}
#endif
semiWindow = DetermineClockDrift(elapsed);
semiWindow +=
Radio::ConvertUncertaintyToUsec(mParentAccuracy.GetUncertainty() + Get<Radio::Radio>().GetCslUncertainty());
uncertainty = mParentAccuracy.GetUncertainty() + Get<Radio::Radio>().GetCslUncertainty();
guardInterval += Radio::ConvertUncertaintyToUsec(uncertainty);
aAhead = Min(semiPeriod, semiWindow + kMinReceiveOnAhead);
aAfter = Min(semiPeriod, semiWindow + kMinReceiveOnAfter);
ahead = Min(guardInterval + kMinReceiveOnAhead, halfPeriod);
after = Min(guardInterval + kMinReceiveOnAfter, halfPeriod);
aWindow.mStartTime = aSampleTime;
aWindow.mStartTime -= ahead;
aWindow.mDuration = ahead + after;
}
uint32_t SubMac::CslReceiver::DetermineClockDrift(uint32_t aIntervalUs) const
@@ -231,8 +228,6 @@ uint32_t SubMac::CslReceiver::DetermineClockDrift(uint32_t aIntervalUs) const
return Radio::DetermineClockDrift(clockAccuracy, aIntervalUs);
}
uint32_t SubMac::CslReceiver::GetNextCycleDrift(void) const { return DetermineClockDrift(CslPeriodToUsec(mPeriod)); }
void SubMac::CslReceiver::SetLastSyncToNow(void)
{
#if OPENTHREAD_CONFIG_MAC_CSL_RECEIVER_LOCAL_TIME_SYNC
@@ -242,66 +237,68 @@ void SubMac::CslReceiver::SetLastSyncToNow(void)
#endif
}
#if OT_SHOULD_LOG_AT(OT_LOG_LEVEL_DEBG)
void SubMac::CslReceiver::LogWindow(Radio::Time64 aWinStart, uint32_t aWinDuration)
{
LogDebg("CSL window start %lu, duration %lu", ToUlong(Radio::ConvertTime64To32(aWinStart)), ToUlong(aWinDuration));
}
#else
void SubMac::CslReceiver::LogWindow(Radio::Time64, uint32_t) {}
#endif
#if OPENTHREAD_CONFIG_MAC_CSL_DEBUG_ENABLE
void SubMac::CslReceiver::LogReceived(RxFrame *aFrame)
void SubMac::CslReceiver::LogReceived(const RxFrame &aFrame)
{
static constexpr uint8_t kLogStringSize = 72;
RxFrame::ParseInfo frameInfo;
Radio::SyncedTime sampleTime;
Window window;
uint32_t margin;
Radio::Time64 timestamp;
uint32_t deviation;
char signChar;
LogLevel logLevel;
String<kLogStringSize> logString;
RxFrame::ParseInfo frameInfo;
int32_t deviation;
uint32_t sampleTime, ahead, after;
SuccessOrExit(frameInfo.ParseFrom(aFrame, Frame::kParseAddrFields));
IgnoreError(frameInfo.ParseFrom(*aFrame, Frame::kParseAddrFields));
VerifyOrExit((frameInfo.mAddrs.mDestination.IsShort() &&
frameInfo.mAddrs.mDestination.GetShort() == Get<SubMac>().GetShortAddress()) ||
(frameInfo.mAddrs.mDestination.IsExtended() &&
frameInfo.mAddrs.mDestination.GetExtended() == Get<SubMac>().GetExtAddress()));
VerifyOrExit(Get<SubMac>().HasAddress(frameInfo.mAddrs.mDestination));
LogDebg("Received frame in state %s, timestamp %lu", StateToString(Get<SubMac>().mState),
ToUlong(Radio::ConvertTime64To32(aFrame->GetTimestamp())));
ToUlong(Radio::ConvertTime64To32(aFrame.GetTimestamp())));
VerifyOrExit((Get<SubMac>().mState == kStateTimedReceive) || (Get<SubMac>().mState == kStateSleep));
GetWindowEdges(ahead, after);
ahead -= kMinReceiveOnAhead;
sampleTime = mSampleTime;
sampleTime -= CslPeriodToUsec(mPeriod);
sampleTime = mSampleTime.GetAsTime32() - CslPeriodToUsec(mPeriod);
deviation = Radio::ConvertTime64To32(aFrame->GetTimestamp()) + Radio::kHeaderPhrDuration - sampleTime;
DetermineWindow(sampleTime, window);
// This logs three values (all in microseconds):
// - Absolute sample time in which the CSL receiver expected the MHR of the received frame.
// - Allowed margin around that time accounting for accuracy and uncertainty from both devices.
// - Real deviation on the reception of the MHR with regards to expected sample time. This can
// be due to clocks drift and/or CSL Phase rounding error.
// This means that a deviation absolute value greater than the margin would result in the frame
// not being received out of the debug mode.
logString.Append("Expected sample time %lu, margin ±%lu, deviation %ld", ToUlong(sampleTime), ToUlong(ahead),
static_cast<long>(deviation));
// The `kMinReceiveOnAhead` is not considered for the margin since
// it has no impact on understanding possible deviation errors
// between transmitter and receiver.
// Treat as a warning when the deviation is not within the margins. Neither kReceiveTimeAhead
// or kMinReceiveOnAhead/kMinReceiveOnAfter are considered for the margin since they have no
// impact on understanding possible deviation errors between transmitter and receiver. So in this
// case only `ahead` is used, as an allowable max deviation in both +/- directions.
if ((deviation + ahead > 0) && (deviation < static_cast<int32_t>(ahead)))
margin = sampleTime.GetAsTime32() - window.mStartTime.GetAsTime32();
margin -= Min(margin, kMinReceiveOnAhead);
timestamp = aFrame.GetTimestamp() + Radio::kHeaderPhrDuration;
if (timestamp >= sampleTime.GetAsTime64())
{
LogDebg("%s", logString.AsCString());
deviation = ClampToUint32(timestamp - sampleTime.GetAsTime64());
signChar = '+';
}
else
{
LogWarn("%s", logString.AsCString());
deviation = ClampToUint32(sampleTime.GetAsTime64() - timestamp);
signChar = '-';
}
// Treat as a warning when the deviation is not within the allowable margin.
logLevel = (deviation <= margin) ? kLogLevelDebg : kLogLevelWarn;
// The log includes three values (all in microseconds):
// - Absolute sample time at which the CSL receiver expected the MHR
// of the received frame.
// - Allowed margin around that time accounting for clock drift and
// uncertainty from both devices.
// - Real deviation on the reception of the MHR with regards to
// expected sample time. This can be due to clock drift and/or
// CSL Phase rounding error.
LogAt(logLevel, "Expected sample time %lu, margin ±%lu, deviation %c%lu", ToUlong(sampleTime.GetAsTime32()),
ToUlong(margin), signChar, ToUlong(deviation));
exit:
return;
}