diff --git a/README.md b/README.md index ee41f3f5..6da963e8 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,8 @@ requires C99 mode to be enabled by default. - [RXTX](#rxtx) - [RXTX Polarity](#rxtx-polarity) - [Pin mapping](#pin-mapping) + - [Advanced initialization](#advanced-initialization) + - [HalConfiguration_t methods](#halconfiguration_t-methods) - [LoRa Nexus by Ideetron](#lora-nexus-by-ideetron) - [Example Sketches](#example-sketches) - [Timing](#timing) @@ -521,9 +523,9 @@ We have details for the following manually-configured boards here: - [LoRa Nexus by Ideetron](#lora-nexus-by-ideetron) -If you don't have the board documentation, you need to provide your own `lmic_pinmap` values. As described above, a variety of configurations are possible. To tell the LMIC library how your board is configured, you must declare a variable containing a pin mapping struct in the sketch file. +If your board is not configured, you need at least to provide your own `lmic_pinmap`. As described above, a variety of configurations are possible. To tell the LMIC library how your board is configured, you must declare a variable containing a pin mapping struct in your sketch file. If you call `os_init()` to initialize the LMIC, you must name this structure `lmic_pins`. If you call `os_init_ex()`, you may name the structure what you like, but you pass a pointer as the parameter to `os_init_ex()`. -For example, this could look like this: +Here's an example of a simple initialization: ```c++ lmic_pinmap lmic_pins = { @@ -552,7 +554,59 @@ respectively. Any pins that are not needed should be specified as potentially left out (depending on the environments and requirements, see the notes above for when a pin can or cannot be left out). -The name of the variable containing this struct must always be `lmic_pins`, which is a special name recognized by the library. +#### Advanced initialization + +In some boards require much more advanced management. The LMIC has a very flexible framework to support this, but it requires you to do some C++ work. + +1. You must define a new class derived from `Arduino_LMIC::HalConfiguration_t`. (We'll call this `cMyHalConfiguration_t`). + +2. This class *may* define overrides for several methods (discussed below). + +3. You must create an instance of your class, e.g. + + ```c++ + cMyHalConfiguration_t myHalConfigInstance; + ``` + +4. You add another entry in your `lmic_pinmap`, `pConfig = &myHalConfigInstance`, to link your pinmap to your object. + +The full example looks like this: + +```c++ +class cMyHlaConfiguration_t : public Arduino_LMIC::HalConfiguration_t + { +public: + // ... + // put your method function override declarations here. + + // this example uses RFO at 10 dBm or less, PA_BOOST up to 17 dBm, + // or the high-power mode above 17 dBm. In other words, it lets the + // LMIC-determined policy determine what's to be done. + + virutal TxPowerPolicy_t getTxPowerPolicy( + TxPowerPolicy_t policy, + int8_t requestedPower, + uint32_t frequency + ) override + { + return policy; + } + } +``` + +#### HalConfiguration_t methods + +- `ostime_t setModuleActive(bool state)` is called by the LMIC to make the module active or to deactivate it (the value of `state` is true to activate). The implementation must turn power to the module on and otherwise prepare for it to go to work, and must return the number of OS ticks to wait before starting to use the radio. + +- `void begin(void)` is called during intialization, and is your code's chance to do any early setup. + +- `void end(void)` is (to be) called during late shutdown. (Late shutdown is not implemented yet; but we wanted to add the API for consistency.) + +- `bool queryUsingTcxo(void)` shall return `true` if the module uses a TCXO; `false` otherwise. + +- `TxPowerPolicy_t getTxPowerPolicy(TxPowerPolicy_t policy, int8_t requestedPower, uint32_t frequency)` allows you to override the LMIC's selection of transmit power. If not provided, the default method forces the LMIC to use PA_BOOST mode. (We chose to do this becuase we found empirically that the Hope RF module doesn't support RFO, and because legacy LMIC code never used anything except PA_BOOST mode.) + +Caution: the LMIC has no way of knowing whether the mode you return makes sense. Use of 20 dBm mode without limiting duty cycle can over-stress your module. The LMIC currently does not have any code to duty-cycle US transmissions at 20 dBm. If properly limiting transmissions to 400 ms, 1% duty-cycle means at most one message every 40 seconds. This shoudln't be a problem in practice, but buggy upper level software still might do things more rapidly. #### LoRa Nexus by Ideetron diff --git a/src/arduino_lmic_hal_configuration.h b/src/arduino_lmic_hal_configuration.h index 29d1e16e..0da70bb5 100644 --- a/src/arduino_lmic_hal_configuration.h +++ b/src/arduino_lmic_hal_configuration.h @@ -62,7 +62,7 @@ struct HalPinmap_t { // Must include noise guardband! uint32_t spi_freq; // bytes 8..11: SPI freq in Hz. - // optional pointer to configuration object (byest 12..15) + // optional pointer to configuration object (bytes 12..15) HalConfiguration_t *pConfig; }; @@ -71,6 +71,14 @@ class HalConfiguration_t public: HalConfiguration_t() {}; + // these must match the constants in radio.c + enum class TxPowerPolicy_t : uint8_t + { + RFO, + PA_BOOST, + PA_BOOST_20dBm + }; + virtual ostime_t setModuleActive(bool state) { LMIC_API_PARAMETER(state); @@ -83,6 +91,20 @@ class HalConfiguration_t virtual void begin(void) {} virtual void end(void) {} virtual bool queryUsingTcxo(void) { return false; } + + // compute desired transmit power policy. HopeRF needs + // (and previous versions of this library always chose) + // PA_BOOST mode. So that's our default. Override this + // for the Murata module. + virtual TxPowerPolicy_t getTxPowerPolicy( + TxPowerPolicy_t policy, + int8_t requestedPower, + uint32_t frequency + ) + { + // default: use PA_BOOST exclusively + return TxPowerPolicy_t::PA_BOOST; + } }; bool hal_init_with_pinmap(const HalPinmap_t *pPinmap); diff --git a/src/hal/hal.cpp b/src/hal/hal.cpp index 098cf517..91b0b4f8 100644 --- a/src/hal/hal.cpp +++ b/src/hal/hal.cpp @@ -424,4 +424,16 @@ ostime_t hal_setModuleActive (bit_t val) { bit_t hal_queryUsingTcxo(void) { return pHalConfig->queryUsingTcxo(); -} \ No newline at end of file +} + +uint8_t hal_getTxPowerPolicy( + u1_t inputPolicy, + s1_t requestedPower, + u4_t frequency + ) { + return (uint8_t) pHalConfig->getTxPowerPolicy( + Arduino_LMIC::HalConfiguration_t::TxPowerPolicy_t(inputPolicy), + requestedPower, + frequency + ); +} diff --git a/src/lmic/hal.h b/src/lmic/hal.h index 8fadeb63..00a6e585 100644 --- a/src/lmic/hal.h +++ b/src/lmic/hal.h @@ -147,8 +147,27 @@ s1_t hal_getRssiCal (void); */ ostime_t hal_setModuleActive (bit_t val); +/* find out if we're using Tcxo */ bit_t hal_queryUsingTcxo(void); +/* represent the various radio TX power policy */ +enum { + LMICHAL_radio_tx_power_policy_rfo = 0, + LMICHAL_radio_tx_power_policy_paboost = 1, + LMICHAL_radio_tx_power_policy_20dBm = 2, +}; + +/* + * query the configuration as to the Tx Power Policy + * to be used on this board, given our desires and + * requested power. + */ +uint8_t hal_getTxPowerPolicy( + u1_t inputPolicy, + s1_t requestedPower, + u4_t freq + ); + #ifdef __cplusplus } // extern "C" #endif diff --git a/src/lmic/lmic.h b/src/lmic/lmic.h index 61c62f00..1983d87f 100644 --- a/src/lmic/lmic.h +++ b/src/lmic/lmic.h @@ -429,7 +429,11 @@ struct lmic_t { u1_t rxsyms; u1_t dndr; s1_t txpow; // transmit dBm (administrative) - s1_t radio_txpow; // the radio driver's copy of txpow, limited by adrTxPow. + s1_t radio_txpow; // the radio driver's copy of txpow, in dB limited by adrTxPow, and + // also adjusted for EIRP/antenna gain considerations. + // This is just the radio's idea of power. So if you are + // controlling EIRP, and you have 3 dB antenna gain, this + // needs to reduced by 3 dB. s1_t lbt_dbmax; // max permissible dB on our channel (eg -80) u1_t txChnl; // channel for next TX diff --git a/src/lmic/radio.c b/src/lmic/radio.c index 4d33cf07..a7f3db21 100644 --- a/src/lmic/radio.c +++ b/src/lmic/radio.c @@ -107,7 +107,9 @@ #define FSKRegSyncValue6 0x2D #define FSKRegSyncValue7 0x2E #define FSKRegSyncValue8 0x2F +#define LORARegIffReq1 0x2F #define FSKRegPacketConfig1 0x30 +#define LORARegIffReq2 0x30 #define FSKRegPacketConfig2 0x31 #define LORARegDetectOptimize 0x31 #define FSKRegPayloadLength 0x32 @@ -116,12 +118,14 @@ #define FSKRegBroadcastAdrs 0x34 #define FSKRegFifoThresh 0x35 #define FSKRegSeqConfig1 0x36 +#define LORARegHighBwOptimize1 0x36 #define FSKRegSeqConfig2 0x37 #define LORARegDetectionThreshold 0x37 #define FSKRegTimerResol 0x38 #define FSKRegTimer1Coef 0x39 #define LORARegSyncWord 0x39 #define FSKRegTimer2Coef 0x3A +#define LORARegHighBwOptimize2 0x3A #define FSKRegImageCal 0x3B #define FSKRegTemp 0x3C #define FSKRegLowBat 0x3D @@ -194,6 +198,28 @@ # define SX127X_MC1_IMPLICIT_HEADER_MODE_ON SX1272_MC1_IMPLICIT_HEADER_MODE_ON #endif +// transmit power configuration for RegPaConfig +#define SX1276_PAC_PA_SELECT_PA_BOOST 0x80 +#define SX1276_PAC_PA_SELECT_RFIO_PIN 0x00 +#define SX1276_PAC_MAX_POWER_MASK 0x70 + +// the bits to change for max power. +#define SX127X_PADAC_POWER_MASK 0x07 +#define SX127X_PADAC_POWER_NORMAL 0x04 +#define SX127X_PADAC_POWER_20dBm 0x07 + +// convert milliamperes to equivalent value for +// RegOcp; delivers conservative value. +#define SX127X_OCP_MAtoBITS(mA) \ + ((mA) < 45 ? 0 : \ + (mA) <= 120 ? ((mA) - 45) / 5 : \ + (mA) < 130 ? 0xF : \ + (mA) < 240 ? ((mA) - 130) / 10 + 0x10 : \ + 27) + +// bit in RegOcp that enables overcurrent protect. +#define SX127X_OCP_ENA 0x20 + // sx1276 RegModemConfig2 #define SX1276_MC2_RX_PAYLOAD_CRCON 0x04 @@ -214,7 +240,7 @@ //----------------------------------------- // Parameters for RSSI monitoring #define SX127X_FREQ_LF_MAX 525000000 // per datasheet 6.3 - + // per datasheet 5.5.3: #define SX127X_RSSI_ADJUST_LF -164 // add to rssi value to get dB (LF) #define SX127X_RSSI_ADJUST_HF -157 // add to rssi value to get dB (HF) @@ -296,7 +322,7 @@ static u1_t randbuf[16]; #ifdef CFG_sx1276_radio -#define LNA_RX_GAIN (0x20|0x1) +#define LNA_RX_GAIN (0x20|0x3) #elif CFG_sx1272_radio #define LNA_RX_GAIN (0x20|0x03) #else @@ -365,7 +391,9 @@ static void configLoraModem () { #ifdef CFG_sx1276_radio u1_t mc1 = 0, mc2 = 0, mc3 = 0; - switch (getBw(LMIC.rps)) { + bw_t const bw = getBw(LMIC.rps); + + switch (bw) { case BW125: mc1 |= SX1276_MC1_BW_125; break; case BW250: mc1 |= SX1276_MC1_BW_250; break; case BW500: mc1 |= SX1276_MC1_BW_500; break; @@ -395,10 +423,34 @@ static void configLoraModem () { writeReg(LORARegModemConfig2, mc2); mc3 = SX1276_MC3_AGCAUTO; - if ((sf == SF11 || sf == SF12) && getBw(LMIC.rps) == BW125) { + + if ( ((sf == SF11 || sf == SF12) && bw == BW125) || + ((sf == SF12) && bw == BW250) ) { mc3 |= SX1276_MC3_LOW_DATA_RATE_OPTIMIZE; } writeReg(LORARegModemConfig3, mc3); + + // Errata 2.1: Sensitivity optimization with 500 kHz bandwidth + u1_t rHighBwOptimize1; + u1_t rHighBwOptimize2; + + rHighBwOptimize1 = 0x03; + rHighBwOptimize2 = 0; + + if (bw == BW500) { + if (LMIC.freq > SX127X_FREQ_LF_MAX) { + rHighBwOptimize1 = 0x02; + rHighBwOptimize2 = 0x64; + } else { + rHighBwOptimize1 = 0x02; + rHighBwOptimize2 = 0x7F; + } + } + + writeReg(LORARegHighBwOptimize1, rHighBwOptimize1); + if (rHighBwOptimize2 != 0) + writeReg(LORARegHighBwOptimize2, rHighBwOptimize2); + #elif CFG_sx1272_radio u1_t mc1 = (getBw(LMIC.rps)<<6); @@ -446,37 +498,165 @@ static void configChannel () { writeReg(RegFrfLsb, (u1_t)(frf>> 0)); } - +// On the SX1276, we have several possible configs. +// 1) using RFO, MaxPower==0: in that case power is -4 to 11 dBm +// 2) using RFO, MaxPower==7: in that case, power is 0 to 14 dBm +// (can't select 15 dBm). +// note we can use -4..11 w/o Max and then 12..14 w/Max, and +// we really don't need to ask anybody. +// 3) using PA_BOOST, PaDac = 4: in that case power range is 2 to 17 dBm; +// use this for 15..17 if authorized. +// 4) using PA_BOOST, PaDac = 7, OutputPower=0xF: in that case, power is 20 dBm +// (and perhaps 0xE is 19, 0xD is 18 dBm, but datasheet isn't clear.) +// and duty cycle must be <= 1%. +// +// In addition, there are some boards for which PA_BOOST can only be used if the +// channel frequency is greater than SX127X_FREQ_LF_MAX. +// +// The SX1272 is similar but has no MaxPower bit: +// 1) using RFO: power is -1 to 13 dBm (datasheet implies max OutputPower value is 14 for 13 dBm) +// 2) using PA_BOOST, PaDac = 0x84: power is 2 to 17 dBm; +// use this for 14..17 if authorized +// 3) using PA_BOOST, PaDac = 0x87, OutptuPower = 0xF: power is 20dBm +// and duty cycle must be <= 1% +// +// The general policy is to use the lowest power variant that will get us where we +// need to be. +// static void configPower () { + // our input paramter -- might be different than LMIC.txpow! + s1_t const req_pw = (s1_t)LMIC.radio_txpow; + // the effective power + s1_t eff_pw; + // the policy; we're going to compute this. + u1_t policy; + // what we'll write to RegPaConfig + u1_t rPaConfig; + // what we'll write to RegPaDac + u1_t rPaDac; + // what we'll write to RegOcp + u1_t rOcp; + #ifdef CFG_sx1276_radio - // PA_BOOST output is assumed but not 20 dBm. - s1_t pw = (s1_t)LMIC.radio_txpow; - if(pw > 17) { - pw = 17; - } else if(pw < 2) { - pw = 2; + if (req_pw >= 20) { + policy = LMICHAL_radio_tx_power_policy_20dBm; + eff_pw = 20; + } else if (req_pw >= 14) { + policy = LMICHAL_radio_tx_power_policy_paboost; + if (req_pw > 17) { + eff_pw = 17; + } else { + eff_pw = req_pw; + } + } else { + policy = LMICHAL_radio_tx_power_policy_rfo; + if (req_pw < -4) { + eff_pw = -4; + } else { + eff_pw = req_pw; + } + } + + policy = hal_getTxPowerPolicy(policy, eff_pw, LMIC.freq); + + switch (policy) { + default: + case LMICHAL_radio_tx_power_policy_rfo: + rPaDac = SX127X_PADAC_POWER_NORMAL; + rOcp = SX127X_OCP_MAtoBITS(80); + + if (eff_pw > 14) + eff_pw = 14; + if (eff_pw > 11) { + // some Semtech code uses this down to eff_pw == 0. + rPaConfig = eff_pw | SX1276_PAC_MAX_POWER_MASK; + } else { + if (eff_pw < -4) + eff_pw = -4; + rPaConfig = eff_pw + 4; + } + break; + + // some radios (HopeRF RFM95W) don't support RFO well, + // so the policy might *raise* rfo to paboost. That means + // we have to re-check eff_pw, which might be too small. + // (And, of course, it might also be too large.) + case LMICHAL_radio_tx_power_policy_paboost: + rPaDac = SX127X_PADAC_POWER_NORMAL; + rOcp = SX127X_OCP_MAtoBITS(100); + if (eff_pw > 17) + eff_pw = 17; + else if (eff_pw < 2) + eff_pw = 2; + rPaConfig = (eff_pw - 2) | SX1276_PAC_PA_SELECT_PA_BOOST; + break; + + case LMICHAL_radio_tx_power_policy_20dBm: + rPaDac = SX127X_PADAC_POWER_20dBm; + rOcp = SX127X_OCP_MAtoBITS(130); + rPaConfig = 0xF | SX1276_PAC_PA_SELECT_PA_BOOST; + break; } - // 0x80 forces use of PA_BOOST; but we don't - // turn on 20 dBm mode. So powers are: - // 0000 => 2dBm, 0001 => 3dBm, ... 1111 => 17dBm - // But we also enforce that the high-power mode - // is off by writing RegPaDac. - writeReg(RegPaConfig, (u1_t)(0x80|(pw - 2))); - writeReg(RegPaDac, readReg(RegPaDac)|0x4); #elif CFG_sx1272_radio - // set PA config (2-17 dBm using PA_BOOST) - s1_t pw = (s1_t)LMIC.radio_txpow; - if(pw > 17) { - pw = 17; - } else if(pw < 2) { - pw = 2; + if (req_pw >= 20) { + policy = LMICHAL_radio_tx_power_policy_20dBm; + eff_pw = 20; + } else if (eff_pw >= 14) { + policy = LMICHAL_radio_tx_power_policy_paboost; + if (eff_pw > 17) { + eff_pw = 17; + } else { + eff_pw = req_pw; + } + } else { + policy = LMICHAL_radio_tx_power_policy_rfo; + if (req_pw < -1) { + eff_pw = -1; + } else { + eff_pw = req_pw; + } + } + + policy = hal_getTxPowerPolicy(policy, eff_pw, LMIC.freq); + + switch (policy) { + default: + case LMICHAL_radio_tx_power_policy_rfo: + rPaDac = SX127X_PADAC_POWER_NORMAL; + rOcp = SX127X_OCP_MAtoBITS(50); + + if (eff_pw > 13) + eff_pw = 13; + + rPaConfig = eff_pw + 1; + break; + + case LMICHAL_radio_tx_power_policy_paboost: + rPaDac = SX127X_PADAC_POWER_NORMAL; + rOcp = SX127X_OCP_MAtoBITS(100); + + if (eff_pw > 17) + eff_pw = 17; + + rPaConfig = (eff_pw - 2) | SX1272_PAC_PA_SELECT_PA_BOOST; + break; + + case LMICHAL_radio_tx_power_policy_20dBm: + rPaDac = SX127X_PADAC_POWER_20dBm; + rOcp = SX127X_OCP_MAtoBITS(130); + + rPaConfig = 0xF | SX1276_PAC_PA_SELECT_PA_BOOST; + break; } - writeReg(RegPaConfig, (u1_t)(0x80|(pw-2))); #else #error Missing CFG_sx1272_radio/CFG_sx1276_radio #endif /* CFG_sx1272_radio */ + + writeReg(RegPaConfig, rPaConfig); + writeReg(RegPaDac, (readReg(RegPaDac) & ~SX127X_PADAC_POWER_MASK) | rPaDac); + writeReg(RegOcp, rOcp | SX127X_OCP_ENA); } static void txfsk () { @@ -593,7 +773,7 @@ static void starttx () { oslmic_radio_rssi_t rssi; radio_monitor_rssi(LMIC.lbt_ticks, &rssi); #if LMIC_X_DEBUG_LEVEL > 0 - LMIC_X_DEBUG_PRINTF("LBT rssi max:min=%d:%d %d times in %d\n", rssi.max_rssi, rssi.min_rssi, rssi.n_rssi, LMIC.lbt_ticks); + LMIC_X_DEBUG_PRINTF("LBT rssi max:min=%d:%d %d times in %d\n", rssi.max_rssi, rssi.min_rssi, rssi.n_rssi, LMIC.lbt_ticks); #endif if (rssi.max_rssi >= LMIC.lbt_dbmax) { @@ -651,6 +831,18 @@ static void rxlora (u1_t rxmode) { writeReg(LORARegInvertIQ, readReg(LORARegInvertIQ)|(1<<6)); } #endif + + // Errata 2.3 - receiver spurious reception of a LoRa signal + bw_t const bw = getBw(LMIC.rps); + u1_t const rDetectOptimize = readReg(LORARegDetectOptimize); + if (bw < BW500) { + writeReg(LORARegDetectOptimize, rDetectOptimize & 0x7F); + writeReg(LORARegIffReq1, 0x40); + writeReg(LORARegIffReq2, 0x40); + } else { + writeReg(LORARegDetectOptimize, rDetectOptimize | 0x80); + } + // set symbol timeout (for single rx) writeReg(LORARegSymbTimeoutLsb, LMIC.rxsyms); // set sync word @@ -674,8 +866,8 @@ static void rxlora (u1_t rxmode) { hal_waitUntil(LMIC.rxtime); // busy wait until exact rx time opmode(OPMODE_RX_SINGLE); #if LMIC_DEBUG_LEVEL > 0 - ostime_t now = os_getTime(); - LMIC_DEBUG_PRINTF("start single rx: now-rxtime: %"LMIC_PRId_ostime_t"\n", now - LMIC.rxtime); + ostime_t now = os_getTime(); + LMIC_DEBUG_PRINTF("start single rx: now-rxtime: %"LMIC_PRId_ostime_t"\n", now - LMIC.rxtime); #endif } else { // continous rx (scan or rssi) opmode(OPMODE_RX); @@ -912,7 +1104,7 @@ void radio_monitor_rssi(ostime_t nTicks, oslmic_radio_rssi_t *pRssi) { rssiMin = rssiNow; rssiSum += rssiNow; ++rssiN; - // TODO(tmm@mcci.com) move this to os_getTime(). + // TODO(tmm@mcci.com) move this to os_getTime(). hal_enableIRQs(); now = os_getTime(); hal_disableIRQs(); @@ -967,7 +1159,7 @@ void radio_irq_handler_v2 (u1_t dio, ostime_t now) { #endif if( (readReg(RegOpMode) & OPMODE_LORA) != 0) { // LORA modem u1_t flags = readReg(LORARegIrqFlags); - LMIC.saveIrqFlags = flags; + LMIC.saveIrqFlags = flags; LMIC_X_DEBUG_PRINTF("IRQ=%02x\n", flags); if( flags & IRQ_LORA_TXDONE_MASK ) { // save exact tx time @@ -994,8 +1186,8 @@ void radio_irq_handler_v2 (u1_t dio, ostime_t now) { // indicate timeout LMIC.dataLen = 0; #if LMIC_DEBUG_LEVEL > 0 - ostime_t now2 = os_getTime(); - LMIC_DEBUG_PRINTF("rxtimeout: entry: %"LMIC_PRId_ostime_t" rxtime: %"LMIC_PRId_ostime_t" entry-rxtime: %"LMIC_PRId_ostime_t" now-entry: %"LMIC_PRId_ostime_t" rxtime-txend: %"LMIC_PRId_ostime_t"\n", entry, + ostime_t now2 = os_getTime(); + LMIC_DEBUG_PRINTF("rxtimeout: entry: %"LMIC_PRId_ostime_t" rxtime: %"LMIC_PRId_ostime_t" entry-rxtime: %"LMIC_PRId_ostime_t" now-entry: %"LMIC_PRId_ostime_t" rxtime-txend: %"LMIC_PRId_ostime_t"\n", entry, LMIC.rxtime, entry - LMIC.rxtime, now2 - entry, LMIC.rxtime-LMIC.txend); #endif }