Skip to content

Commit

Permalink
Add Listen Before Talk for 2.4GHz band - EU compliance (ExpressLRS#1243)
Browse files Browse the repository at this point in the history
* sx1280Driver: Add getRssiInst function

* Clear Channel Assessment aka Listen Before Talk, first working prototype. 500hz, hardcoded rxtimeout and threshold

* Repurposed downlink link quality for LBT success ratio

* Add dynamic GetRssiInst() valid delay depending on Lora SF mode
Add dynamic listen before talk rssi dBm threshold depending in tx power

* use currPower instead of max dynamic power for LBT threshold

* fix currPower() for rssi threshold

* Listen Before Talk
Got rid of while loop waiting for rx valid by enabling rx for LBT early in TX interrupt
Changed LBT RSSI thresholds to not adjusted for receiver bandwidth.
This interpretation of the thresholds assumes RSSI dBm values from sx1280 already
are correct for the configure receiver bandwidth configuration.

* removed unused valid rssi delay

* restart RX to clear potential packets received during listen before talk from rx buffer

* restore downlink LQ to normal operation, keep LQ calc for LBT for future use

* work in progress, LBT on rx side

* document polling loop on getRssiInst, restore downlink LQ to indicate downlink LQ, not LBT success rate

* add -DRegulatory_Domain_EU_CE_LBT_2400 define for enabling LBT

* add adjustment offset #define for rssi reading

* Exclude LBT code from radio_900 builds

* move LBT clear channel assessment closer to where TX happens to simplify rx_main

* add us wait delay for valid RSSI reading instead of just waiting for rssi to be above -127db, fixes issue with too low rssi for rx target, where rssi is read very soon after lbt rx enable

* attempt at more clever way to select LBT mode if minpower is higher than non-LBT 10mW limit

* Single EU_CE regulatory option
Add preprocessor switch between LBT or non-LBT operation based on configured maxPower

* remove debug #warning for LBT_ACTIVE

* remove LBT regulatory define from user_defines

* use delayMicroseconds instead of while-loop in LBT waiting for readability

* Auto switch LBT based on power config (#1)

* auto switch LBT based on config

* changes after review

Co-authored-by: Anders G <andlier@users.noreply.github.com>

* add safe runtime disabling of LBT, since LBT routines fiddles with interrupt enable flags

* make LBTEnable if else formatting more concise

* Move LBTEnable flag and if-else logic into LBT.cpp, keeps tx/rx_main less cluttered

* fixes after rebasing on master

* Using flag to skip ISR instead of disabling interrupts while doing LBT

* simplified LBT, attempts to be compatible with rx-auto-off timeout

* add lbtstarted flag to allow calling beginCCA twice without restarting RX and timer

* attempt at fixing listen before talk for 1:2 rate telemetry

* Remove unused GetMode function in sx1280 driver and rename LBT flag variable for delayed call of TXdoneCallback.

* Improvements after review
LBT.h/cpp: remove uneccessary volatiles, add static to LBT internal variables.
Remove LBTChannelBusy variable, use LQCALC currentIsSet for LBT success instead.
Wrap LBT.h/cpp in #if defined REG_DOMAIN

* POWERMGNT.h/cpp: Inline currPower

* LBT: simplify calculation of remaining validRssiDelay wait time

* Repurpose downlink LQ for LBT success ratio when EU reg domain defined

* simplified LBTEnable functionality, does not need to be synchronized disabling

Co-authored-by: Alexander van Saase <avsaase@gmail.com>
Co-authored-by: Paul Kendall <pkendall64@gmail.com>
  • Loading branch information
3 people authored Feb 6, 2022
1 parent 0dbba4e commit d543a81
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 15 deletions.
4 changes: 4 additions & 0 deletions src/include/targets.h
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@
#endif
#endif

#if defined(Regulatory_Domain_EU_CE_2400) && !defined(Regulatory_Domain_ISM_2400)
#define Regulatory_Domain_ISM_2400
#endif

#if defined(Regulatory_Domain_ISM_2400)
// ISM 2400 band is use => undefine other requlatory domain defines
#undef Regulatory_Domain_AU_915
Expand Down
127 changes: 127 additions & 0 deletions src/lib/LBT/LBT.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#if defined(Regulatory_Domain_EU_CE_2400)
#include "common.h"
#include "logging.h"
#include "LBT.h"

extern SX1280Driver Radio;

LQCALC<100> LBTSuccessCalc;
static uint32_t rxStartTime;

#if !defined(LBT_RSSI_THRESHOLD_OFFSET_DB)
#define LBT_RSSI_THRESHOLD_OFFSET_DB 0
#endif

bool LBTEnabled = false;
static bool LBTStarted = false;

uint32_t ICACHE_RAM_ATTR SpreadingFactorToRSSIvalidDelayUs(SX1280_RadioLoRaSpreadingFactors_t SF)
{
// The necessary wait time from RX start to valid instant RSSI reading
// changes with the spreading factor setting.
// The worst case necessary wait time is TX->RX switch time + Lora symbol time
// This assumes the radio switches from either TX, RX or FS (Freq Synth mode)
// TX to RX switch time is 60us for sx1280
// Lora symbol time for the relevant spreading factors is:
// SF5: 39.4us
// SF6: 78.8us
// SF7: 157.6us
// SF9: 630.5us
// However, by measuring when the RSSI reading is stable and valid, it was found that
// actual necessary wait times are:
// SF5 ~100us (60us + SF5 symbol time)
// SF6 ~141us (60us + SF6 symbol time)
// SF7 ~218us (60us + SF7 symbol time)
// SF9 ~218us (Odd one out, measured to same as SF7 wait time)

switch(SF)
{
case SX1280_LORA_SF5: return 100;
case SX1280_LORA_SF6: return 141;
case SX1280_LORA_SF7: return 218;
case SX1280_LORA_SF9: return 218;
default: return 218; // Values above 100mW are not relevant, default to 100mW threshold
}
}

int8_t ICACHE_RAM_ATTR PowerEnumToLBTLimit(PowerLevels_e txPower)
{
// Calculated from EN 300 328, adjusted for 800kHz BW for sx1280
// TL = -70 dBm/MHz + 10*log10(0.8MHz) + 10 × log10 (100 mW / Pout) (Pout in mW e.i.r.p.)
// This threshold should be offset with a #define config for each HW that corrects for antenna gain,
// different RF frontends.
// TODO: Maybe individual adjustment offset for differences in
// rssi reading between bandwidth setting is also necessary when other BW than 0.8MHz are used.

switch(txPower)
{
case PWR_10mW: return -61 + LBT_RSSI_THRESHOLD_OFFSET_DB;
case PWR_25mW: return -65 + LBT_RSSI_THRESHOLD_OFFSET_DB;
case PWR_50mW: return -68 + LBT_RSSI_THRESHOLD_OFFSET_DB;
case PWR_100mW: return -71 + LBT_RSSI_THRESHOLD_OFFSET_DB;
// Values above 100mW are not relevant, default to 100mW threshold
default: return -71 + LBT_RSSI_THRESHOLD_OFFSET_DB;
}
}

void ICACHE_RAM_ATTR BeginClearChannelAssessment(void)
{
if (!LBTEnabled)
{
return;
}

if(!LBTStarted)
{
Radio.RXnb();
rxStartTime = micros();
LBTStarted = true;
}
}

bool ICACHE_RAM_ATTR ChannelIsClear(void)
{
LBTSuccessCalc.inc(); // Increment count for every channel check
if (!LBTEnabled)
{
LBTSuccessCalc.add();
return true;
}

LBTStarted = false;

// Read rssi after waiting the minimum RSSI valid delay.
// If this function is called long enough after RX enable,
// this will always be ok on first try as is the case for TX.

// TODO: Better way than busypolling this for RX?
// this loop should only run for RX, where listen before talk RX is started right after FHSS hop
// so there is no dead-time to run RX before telemetry TX is supposed to happen.
// if flipping the logic, so telemetry TX happens right before FHSS hop, then TX-side ends up with polling here instead?
// Maybe it could be skipped if there was only TX of telemetry happening when FHSShop does not happen.
// Then RX for LBT could stay enabled from last received packet, and RSSI would be valid instantly.
// But for now, FHSShops and telemetry rates does not divide evenly, so telemetry will some times happen
// right after FHSS and we need wait here.

uint32_t validRSSIdelayUs = SpreadingFactorToRSSIvalidDelayUs((SX1280_RadioLoRaSpreadingFactors_t)ExpressLRS_currAirRate_Modparams->sf);
uint32_t elapsed = micros() - rxStartTime;
if(elapsed < validRSSIdelayUs)
{
delayMicroseconds(validRSSIdelayUs - elapsed);
}

int8_t rssiInst = Radio.GetRssiInst();
Radio.SetTxIdleMode();
bool channelClear = rssiInst < PowerEnumToLBTLimit((PowerLevels_e)POWERMGNT::currPower());

// Useful to debug if and how long the rssi wait is, and rssi threshold level
// DBGLN("wait: %d, rssi: %d", delayRemaining, rssiInst);

if(channelClear)
{
LBTSuccessCalc.add(); // Add success only when actually preparing for TX
}

return channelClear;
}
#endif
15 changes: 15 additions & 0 deletions src/lib/LBT/LBT.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#if defined(Regulatory_Domain_EU_CE_2400)
#pragma once

#include "POWERMGNT.h"
#include "LQCALC.h"
#include "SX1280Driver.h"

extern LQCALC<100> LBTSuccessCalc;
extern bool LBTEnabled;

uint32_t SpreadingFactorToRSSIvalidDelayUs(SX1280_RadioLoRaSpreadingFactors_t SF);
int8_t ICACHE_RAM_ATTR PowerEnumToLBTLimit(PowerLevels_e txPower);
void ICACHE_RAM_ATTR BeginClearChannelAssessment(void);
bool ICACHE_RAM_ATTR ChannelIsClear(void);
#endif
2 changes: 1 addition & 1 deletion src/lib/LUA/devLUA.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ static struct luaItem_selection luaFanThreshold = {

#if defined(Regulatory_Domain_EU_CE_2400)
static struct luaItem_string luaCELimit = {
{"10mW CE LIMIT", CRSF_INFO},
{"100mW CE LIMIT", CRSF_INFO},
emptySpace
};
#endif
Expand Down
5 changes: 0 additions & 5 deletions src/lib/POWERMGNT/POWERMGNT.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,6 @@ PowerLevels_e POWERMGNT::decPower()
return CurrentPower;
}

PowerLevels_e POWERMGNT::currPower()
{
return CurrentPower;
}

void POWERMGNT::incSX1280Ouput()
{
if (CurrentSX1280Power < 13)
Expand Down
9 changes: 3 additions & 6 deletions src/lib/POWERMGNT/POWERMGNT.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,12 @@
#endif

#if defined(Regulatory_Domain_EU_CE_2400)
#undef MinPower
#define MinPower PWR_10mW

#undef MaxPower
#define MaxPower PWR_10mW
#define MaxPower PWR_100mW

#if defined(HighPower)
#undef HighPower
#define HighPower PWR_10mW
#define HighPower MaxPower
#endif
#endif

Expand Down Expand Up @@ -73,7 +70,7 @@ class POWERMGNT
static void setPower(PowerLevels_e Power);
static PowerLevels_e incPower();
static PowerLevels_e decPower();
static PowerLevels_e currPower();
static PowerLevels_e currPower() { return CurrentPower; }
static void incSX1280Ouput();
static void decSX1280Ouput();
static int8_t currentSX1280Ouput();
Expand Down
8 changes: 8 additions & 0 deletions src/lib/SX1280Driver/SX1280.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,14 @@ bool ICACHE_RAM_ATTR SX1280Driver::GetFrequencyErrorbool()
return 0;
}

int8_t ICACHE_RAM_ATTR SX1280Driver::GetRssiInst()
{
uint8_t status = 0;

hal.ReadCommand(SX1280_RADIO_GET_RSSIINST, (uint8_t *)&status, 1);
return -(int8_t)(status / 2);
}

void ICACHE_RAM_ATTR SX1280Driver::GetLastPacketStats()
{
uint8_t status[2];
Expand Down
1 change: 1 addition & 0 deletions src/lib/SX1280Driver/SX1280.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ class SX1280Driver

bool GetFrequencyErrorbool();
uint8_t GetRxBufferAddr();
int8_t GetRssiInst();
void GetLastPacketStats();

private:
Expand Down
24 changes: 23 additions & 1 deletion src/src/rx_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
SX127xDriver Radio;
#elif defined(Regulatory_Domain_ISM_2400)
#include "SX1280Driver.h"
#include "LBT.h"
SX1280Driver Radio;
#else
#error "Radio configuration is not valid!"
Expand Down Expand Up @@ -298,6 +299,10 @@ bool ICACHE_RAM_ATTR HandleSendTelemetryResponse()
return false; // don't bother sending tlm if disconnected or TLM is off
}

#if defined(Regulatory_Domain_EU_CE_2400)
BeginClearChannelAssessment();
#endif

alreadyTLMresp = true;
Radio.TXdataBuffer[0] = TLM_PACKET;

Expand Down Expand Up @@ -342,7 +347,12 @@ bool ICACHE_RAM_ATTR HandleSendTelemetryResponse()
Radio.TXdataBuffer[0] |= (crc >> 6) & 0b11111100;
Radio.TXdataBuffer[7] = crc & 0xFF;

Radio.TXnb();
#if defined(Regulatory_Domain_EU_CE_2400)
if (ChannelIsClear())
#endif
{
Radio.TXnb();
}
return true;
}

Expand Down Expand Up @@ -507,6 +517,14 @@ static void ICACHE_RAM_ATTR updateDiversity()

void ICACHE_RAM_ATTR HWtimerCallbackTock()
{
#if defined(Regulatory_Domain_EU_CE_2400)
// Emulate that TX just happened, even if it didn't because channel is not clear
if(!LBTSuccessCalc.currentIsSet())
{
Radio.TXdoneCallback();
}
#endif

PFDloop.intEvent(micros()); // our internal osc just fired

updateDiversity();
Expand Down Expand Up @@ -992,6 +1010,10 @@ static void setupRadio()
// Set transmit power to maximum
POWERMGNT.setPower(MaxPower);

#if defined(Regulatory_Domain_EU_CE_2400)
LBTEnabled = (MaxPower > PWR_10mW);
#endif

Radio.RXdoneCallback = &RXdoneISR;
Radio.TXdoneCallback = &TXdoneISR;

Expand Down
42 changes: 40 additions & 2 deletions src/src/tx_main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
SX127xDriver Radio;
#elif defined(Regulatory_Domain_ISM_2400)
#include "SX1280Driver.h"
#include "LBT.h"
SX1280Driver Radio;
#endif

Expand Down Expand Up @@ -474,14 +475,26 @@ void ICACHE_RAM_ATTR SendRCdataToRF()
Radio.TXdataBuffer[0] = (Radio.TXdataBuffer[0] & 0b11) | ((crc >> 6) & 0b11111100);
Radio.TXdataBuffer[7] = crc & 0xFF;

Radio.TXnb();
#if defined(Regulatory_Domain_EU_CE_2400)
if (ChannelIsClear())
#endif
{
Radio.TXnb();
}
}

/*
* Called as the TOCK timer ISR when there is a CRSF connection from the handset
*/
void ICACHE_RAM_ATTR timerCallbackNormal()
{
#if defined(Regulatory_Domain_EU_CE_2400)
if(!LBTSuccessCalc.currentIsSet())
{
Radio.TXdoneCallback();
}
#endif

// Sync OpenTX to this point
crsf.JustSentRFpacket();

Expand All @@ -494,11 +507,20 @@ void ICACHE_RAM_ATTR timerCallbackNormal()
if (TelemetryRcvPhase == ttrpInReceiveMode)
{
TelemetryRcvPhase = ttrpTransmitting;
#if defined(Regulatory_Domain_EU_CE_2400)
// Use downlink LQ for LBT success ratio instead for EU/CE reg domain
crsf.LinkStatistics.downlink_Link_quality = LBTSuccessCalc.getLQ();
#else
crsf.LinkStatistics.downlink_Link_quality = LQCalc.getLQ();
#endif
LQCalc.inc();
return;
}

#if defined(Regulatory_Domain_EU_CE_2400)
BeginClearChannelAssessment(); // Get RSSI reading here, used also for next TX if in receiveMode.
#endif

// Do not send a stale channels packet to the RX if one has not been received from the handset
// *Do* send data if a packet has never been received from handset and the timer is running
// this is the case when bench testing and TXing without a handset
Expand Down Expand Up @@ -549,6 +571,9 @@ static void ChangeRadioParams()
// Dynamic Power starts at MinPower and will boost if switch is set or IsArmed and disconnected
POWERMGNT.setPower(config.GetDynamicPower() ? MinPower : (PowerLevels_e)config.GetPower());
// TLM interval is set on the next SYNC packet
#if defined(Regulatory_Domain_EU_CE_2400)
LBTEnabled = (config.GetPower() > PWR_10mW);
#endif
}

void ICACHE_RAM_ATTR ModelUpdateReq()
Expand Down Expand Up @@ -605,7 +630,7 @@ static void CheckConfigChangePending()
#endif
hwTimer.callbackTock = &timerCallbackIdle;
// If telemetry expected in the next interval, the radio is in RX mode
// and will skip sending the next packet when the tiemr resumes.
// and will skip sending the next packet when the timer resumes.
// Return to normal send mode because if the skipped packet happened
// to be on the last slot of the FHSS the skip will prevent FHSS
if (TelemetryRcvPhase == ttrpInReceiveMode)
Expand All @@ -626,6 +651,16 @@ void ICACHE_RAM_ATTR TXdoneISR()
{
HandleFHSS();
HandlePrepareForTLM();
#if defined(Regulatory_Domain_EU_CE_2400)
if(TelemetryRcvPhase != ttrpInReceiveMode)
{
// Start RX for Listen Before Talk early because it takes about 100us
// from RX enable to valid instant RSSI values are returned.
// If rx was already started by TLM prepare above, this call will let RX
// continue as normal.
BeginClearChannelAssessment();
}
#endif // non-CE
busyTransmitting = false;
}

Expand Down Expand Up @@ -986,6 +1021,9 @@ void setup()
// Set the pkt rate, TLM ratio, and power from the stored eeprom values
ChangeRadioParams();

#if defined(Regulatory_Domain_EU_CE_2400)
BeginClearChannelAssessment();
#endif
hwTimer.init();
connectionState = noCrossfire;
}
Expand Down

0 comments on commit d543a81

Please sign in to comment.