// RH_SX126x.cpp // // Copyright (C) 2023 Mike McCauley // $Id: RH_SX126x.cpp,v 1.27 2020/07/05 08:52:21 mikem Exp $ // // UNFINISHED. TODO: // power setting for different device types // specific subclasses for 1261, 1262, 1268? #include // Maybe a mutex for multithreading on Raspberry Pi? #ifdef RH_USE_MUTEX RH_DECLARE_MUTEX(lock); #endif // Interrupt vectors for the 3 Arduino interrupt pins // Each interrupt can be handled by a different instance of RH_SX126x, allowing you to have // 2 or more LORAs per Arduino RH_SX126x* RH_SX126x::_deviceForInterrupt[RH_SX126x_NUM_INTERRUPTS] = {0, 0, 0}; // These are indexed by the values of ModemConfigChoice // Stored in flash (program) memory to save SRAM PROGMEM static const RH_SX126x::ModemConfig MODEM_CONFIG_TABLE[] = { // packetType, p1, p2, p3, p4, p5, p6, p7, p8 // LoRa_Bw125Cr45Sf128 Works with RH_RF95 { RH_SX126x::PacketTypeLoRa, RH_SX126x_LORA_SF_128, RH_SX126x_LORA_BW_125_0, RH_SX126x_LORA_CR_4_5, RH_SX126x_LORA_LOW_DATA_RATE_OPTIMIZE_OFF, 0, 0, 0, 0}, // LoRa_Bw500Cr45Sf128 Works with RH_RF95 { RH_SX126x::PacketTypeLoRa, RH_SX126x_LORA_SF_128, RH_SX126x_LORA_BW_500_0, RH_SX126x_LORA_CR_4_5, RH_SX126x_LORA_LOW_DATA_RATE_OPTIMIZE_OFF, 0, 0, 0, 0}, // LoRa_Bw31_25Cr48Sf512 no interop with RH_RF95 WHY???? { RH_SX126x::PacketTypeLoRa, RH_SX126x_LORA_SF_512, RH_SX126x_LORA_BW_31_25, RH_SX126x_LORA_CR_4_8, RH_SX126x_LORA_LOW_DATA_RATE_OPTIMIZE_ON, 0, 0, 0, 0}, // LoRa_Bw125Cr48Sf4096 Works with RH_RF95 { RH_SX126x::PacketTypeLoRa, RH_SX126x_LORA_SF_4096, RH_SX126x_LORA_BW_125_0, RH_SX126x_LORA_CR_4_8, RH_SX126x_LORA_LOW_DATA_RATE_OPTIMIZE_ON, 0, 0, 0, 0}, // LoRa_Bw125Cr45Sf2048 Works with RH_RF95 { RH_SX126x::PacketTypeLoRa, RH_SX126x_LORA_SF_2048, RH_SX126x_LORA_BW_125_0, RH_SX126x_LORA_CR_4_5, RH_SX126x_LORA_LOW_DATA_RATE_OPTIMIZE_ON, 0, 0, 0, 0}, }; RH_SX126x::RH_SX126x(uint8_t slaveSelectPin, uint8_t interruptPin, uint8_t busyPin, uint8_t resetPin, RHGenericSPI& spi, RadioPinConfig* radioPinConfig) : RHSPIDriver(slaveSelectPin, spi), _rxBufValid(0) { _interruptPin = interruptPin; _busyPin = busyPin; _resetPin = resetPin; _myInterruptIndex = 0xff; // Not allocated yet _enableCRC = true; // There should be (but may not be) a configuration structure toi tell us how to manage the // radio RF switch control pins setRadioPinConfig(radioPinConfig); } bool RH_SX126x::init() { if (!RHSPIDriver::init()) return false; #ifdef RH_USE_MUTEX if (RH_MUTEX_INIT(lock) != 0) { Serial.println("\n mutex init has failed\n"); return false; } #endif if (!setupInterruptHandler()) return false; // Reset the radio, if we know the reset pin: if (_resetPin != RH_INVALID_PIN) { pinMode(_resetPin, OUTPUT); digitalWrite(_resetPin, HIGH); digitalWrite(_resetPin, LOW); delay(2); digitalWrite(_resetPin, HIGH); // Expect to be busy for about 1ms after this delay(200); // After reset: else can get runt transmission during startup } // Configure the BUSY pin if available if (_busyPin != RH_INVALID_PIN) pinMode(_busyPin, INPUT); // No way to check the device type :-( // Can read data like 'SX1261 TKF 1A11', at register 0x0320 RH_SX126x_REG_VERSION_STRING but it does not change for SX1261 // Get the current status and guess if we are connected uint8_t status = getStatus(); if (status == 0x00 || status == 0xff) // Should never get this: probably not connected by SPI return false; setModeIdle(); setRegulatorMode(RH_SX126x_REGULATOR_DC_DC); // == SMPS mode clearDeviceErrors(); setRxFallbackMode(RH_SX126x_RX_TX_FALLBACK_MODE_STDBY_RC); calibrate(RH_SX126x_CALIBRATE_ALL); // All blocks get (re)calibrated when setFrequency() is called with calibrate true // This LoRa Sync word 0x1424 is compatible with single byte 0x12 default for RH_RF95. // https://forum.lora-developers.semtech.com/t/sx1272-and-sx1262-lora-sync-word-compatibility/988/13 setLoRaSyncWord(0x1424); // You may need to change these after init to suit your radio and its wiring: setDIO2AsRfSwitchCtrl(true); // Use the radios DIO2 pin control the transmitter. This is common in 3rd party RF modules setTCXO(3.3, 100); // Enable the TCXO. Typical values for the control voltage and delay // These are the interrupts we are willing to process setDioIrqParams(_irqMask, _irqMask, RH_SX126x_IRQ_NONE, RH_SX126x_IRQ_NONE); // Set up default configuration setModemConfig(LoRa_Bw125Cr45Sf128); // Radio default // setModemConfig(LoRa_Bw125Cr48Sf4096); // slow and reliable? // Default preamble length is 8, so dont set it here // An innocuous ISM frequency, in the HF range for the WiO-E5, compatible with RH_RF95 setFrequency(915.0); // Lowish power setTxPower(13); return true; } void RH_SX126x::enableCrcErrorIrq(bool enable) { if ( enable ) _irqMask |= RH_SX126x_IRQ_CRC_ERR; else _irqMask &= ~RH_SX126x_IRQ_CRC_ERR; setDioIrqParams(_irqMask, _irqMask, RH_SX126x_IRQ_NONE, RH_SX126x_IRQ_NONE); } void RH_SX126x::enableRawMode(bool enable) { _raw = enable; } // Some subclasses may need to override bool RH_SX126x::setupInterruptHandler() { // For some subclasses (eg RH_ABZ) we dont want to set up interrupt int interruptNumber = NOT_AN_INTERRUPT; if (_interruptPin != RH_INVALID_PIN) { // Determine the interrupt number that corresponds to the interruptPin interruptNumber = digitalPinToInterrupt(_interruptPin); if (interruptNumber == NOT_AN_INTERRUPT) return false; #ifdef RH_ATTACHINTERRUPT_TAKES_PIN_NUMBER interruptNumber = _interruptPin; #endif // Tell the low level SPI interface we will use SPI within this interrupt spiUsingInterrupt(interruptNumber); } if (_interruptPin != RH_INVALID_PIN) { // Add by Adrien van den Bossche for Teensy // ARM M4 requires the below. else pin interrupt doesn't work properly. // On all other platforms, its innocuous, belt and braces pinMode(_interruptPin, INPUT); // Set up interrupt handler // Since there are a limited number of interrupt glue functions isr*() available, // we can only support a limited number of devices simultaneously // ON some devices, notably most Arduinos, the interrupt pin passed in is actually the // interrupt number. You have to figure out the interruptnumber-to-interruptpin mapping // yourself based on knwledge of what Arduino board you are running on. if (_myInterruptIndex == 0xff) { static uint8_t interruptCount = 0; // Index into _deviceForInterrupt for next device // First run, no interrupt allocated yet if (interruptCount <= RH_SX126x_NUM_INTERRUPTS) _myInterruptIndex = interruptCount++; else return false; // Too many devices, not enough interrupt vectors } _deviceForInterrupt[_myInterruptIndex] = this; if (_myInterruptIndex == 0) attachInterrupt(interruptNumber, isr0, RISING); else if (_myInterruptIndex == 1) attachInterrupt(interruptNumber, isr1, RISING); else if (_myInterruptIndex == 2) attachInterrupt(interruptNumber, isr2, RISING); else return false; // Too many devices, not enough interrupt vectors } return true; } // C++ level interrupt handler for this instance // LORA is unusual in that it has several interrupt lines, and not a single, combined one. // On MiniWirelessLoRa, only one of the several interrupt lines (DI0) from the RFM95 is usefuly // connnected to the processor. // We use this to get RxDone and TxDone interrupts void RH_SX126x::handleInterrupt() { RH_MUTEX_LOCK(lock); // Multithreading support uint16_t interrupts = getIrqStatus(); _lastirq = interrupts; _iflag = true; // Debugging clearIrqStatus(interrupts); // Serial.print("int: "); // Serial.println(interrupts, HEX); if (_mode == RHModeRx && (interrupts & (RH_SX126x_IRQ_CRC_ERR | RH_SX126x_IRQ_HEADER_ERR))) { // CrcErr HeaderErr _rxBad++; clearRxBuf(); // If there was an error, the SX126x is now in standby mode. Need to force RH state back to idle too _mode = RHModeIdle; // setModeRx(); // Keep trying? } else if (_mode == RHModeRx && (interrupts & RH_SX126x_IRQ_RX_DONE)) { // RxDone // Should now be in STDBY // Get received packet length uint8_t rxbufferstatus[2]; // PayloadLengthRx, RxStartBufferPointer getCommand(RH_SX126x_CMD_GET_RX_BUFFER_STATUS, rxbufferstatus, sizeof(rxbufferstatus)); _bufLen = rxbufferstatus[0]; uint8_t offset = rxbufferstatus[1]; // Get the packet readBuffer(offset, _buf, _bufLen); uint8_t packetstatus[3]; getCommand(RH_SX126x_CMD_GET_PKT_STATUS, packetstatus, sizeof(packetstatus)); if (_packetType == PacketTypeLoRa) { _lastRssi = -(packetstatus[0] / 2); // dBm _lastSNR = (packetstatus[1] / 4); } else if (_packetType == PacketTypeGFSK) { _lastRssi = -(packetstatus[2] / 2); // dBm _lastSNR = 0; // Unobtainable } validateRxBuf(); if (_rxBufValid) setModeIdle(); // Got one } else if (_mode == RHModeTx && (interrupts & RH_SX126x_IRQ_TX_DONE)) { // TxDone // Should now be in STDBY _txGood++; setModeIdle(); } else if (_mode == RHModeCad && (interrupts & RH_SX126x_IRQ_CAD_DONE)) { // Serial.println("caddone"); // CadDone // Should now be in STDBY_RX setModeIdle(); } else if (_mode == RHModeCad && (interrupts & RH_SX126x_IRQ_CAD_DETECTED)) { // CadDetected: there was activity // Serial.println("caddetected"); _cad = true; setModeIdle(); } RH_MUTEX_UNLOCK(lock); } // These are low level functions that call the interrupt handler for the correct // instance of RH_SX126x. // 3 interrupts allows us to have 3 different devices void RH_INTERRUPT_ATTR RH_SX126x::isr0() { if (_deviceForInterrupt[0]) _deviceForInterrupt[0]->handleInterrupt(); } void RH_INTERRUPT_ATTR RH_SX126x::isr1() { if (_deviceForInterrupt[1]) _deviceForInterrupt[1]->handleInterrupt(); } void RH_INTERRUPT_ATTR RH_SX126x::isr2() { if (_deviceForInterrupt[2]) _deviceForInterrupt[2]->handleInterrupt(); } // Check whether the latest received message is complete and uncorrupted void RH_SX126x::validateRxBuf() { if (_bufLen < 4) return; // Too short to be a real message // Extract the 4 headers _rxHeaderTo = _buf[0]; _rxHeaderFrom = _buf[1]; _rxHeaderId = _buf[2]; _rxHeaderFlags = _buf[3]; if (_promiscuous || _rxHeaderTo == _thisAddress || _rxHeaderTo == RH_BROADCAST_ADDRESS) { _rxGood++; _rxBufValid = true; } } bool RH_SX126x::available() { RH_MUTEX_LOCK(lock); // Multithreading support if (_mode == RHModeTx) { RH_MUTEX_UNLOCK(lock); return false; } setModeRx(); RH_MUTEX_UNLOCK(lock); return _rxBufValid; // Will be set by the interrupt handler when a good message is received } void RH_SX126x::clearRxBuf() { waitUntilNotBusy(); ATOMIC_BLOCK_START; _rxBufValid = false; _bufLen = 0; ATOMIC_BLOCK_END; } bool RH_SX126x::recv(uint8_t* buf, uint8_t* len) { uint8_t hdr_len = RH_SX126x_HEADER_LEN; if (!available()) return false; if ( _raw ) hdr_len = 0; if (buf && len) { ATOMIC_BLOCK_START; // Skip the 4 headers that are at the beginning of the rxBuf if (*len > _bufLen-hdr_len) *len = _bufLen-hdr_len; memcpy(buf, _buf+hdr_len, *len); ATOMIC_BLOCK_END; } clearRxBuf(); // This message accepted and cleared RH_MUTEX_UNLOCK(lock); return true; } bool RH_SX126x::send(const uint8_t* data, uint8_t len) { if (len > RH_SX126x_MAX_MESSAGE_LEN) return false; waitPacketSent(); // Make sure we dont interrupt an outgoing message setModeIdle(); if (!waitCAD()) return false; // Check channel activity setBufferBaseAddress(0, 0); // The headers if ( _raw ) { // The message data writeBuffer(0, data, len); setPacketParametersLoRa(len); // Tell modem how much to send } else { uint8_t headers[RH_SX126x_HEADER_LEN] = {_txHeaderTo, _txHeaderFrom, _txHeaderId, _txHeaderFlags }; writeBuffer(0, headers, sizeof(headers)); // The message data writeBuffer(RH_SX126x_HEADER_LEN, data, len); setPacketParametersLoRa(len + RH_SX126x_HEADER_LEN); // Tell modem how much to send } RH_MUTEX_LOCK(lock); // Multithreading support setModeTx(); // Start the transmitter RH_MUTEX_UNLOCK(lock); // Hmmmm, some chips fail to enter TX mode the first time,and need to be tried again // Why? if (getStatus() == 0x2a) { // Retry Tx mode directly RH_MUTEX_LOCK(lock); // Multithreading support setTx(RH_SX126x_RX_TIMEOUT_NONE); // Timeout 0 RH_MUTEX_UNLOCK(lock); } // when Tx is done, interruptHandler will fire and radio mode will return to STANDBY return true; } uint8_t RH_SX126x::maxMessageLength() { return RH_SX126x_MAX_MESSAGE_LEN; } bool RH_SX126x::setFrequency(float centre, bool calibrate) { if (calibrate) { if (centre > 900.0) calibrateImage(RH_SX126x_CAL_IMG_902_MHZ_1, RH_SX126x_CAL_IMG_902_MHZ_2); else if (centre > 850.0) calibrateImage(RH_SX126x_CAL_IMG_863_MHZ_1, RH_SX126x_CAL_IMG_863_MHZ_2); else if (centre > 770.0) calibrateImage(RH_SX126x_CAL_IMG_779_MHZ_1, RH_SX126x_CAL_IMG_779_MHZ_2); else if (centre > 460.0) calibrateImage(RH_SX126x_CAL_IMG_470_MHZ_1, RH_SX126x_CAL_IMG_470_MHZ_2); else calibrateImage(RH_SX126x_CAL_IMG_430_MHZ_1, RH_SX126x_CAL_IMG_430_MHZ_2); } // Frf = FRF / FSTEP uint32_t frf = (centre * 1000000.0) / RH_SX126x_FSTEP; uint8_t setting[] = {static_cast(frf >> 24), static_cast(frf >> 16), static_cast(frf >> 8), static_cast(frf)}; return sendCommand(RH_SX126x_CMD_SET_RF_FREQUENCY, setting, sizeof(setting)); } void RH_SX126x::setModeIdle() { if (_mode != RHModeIdle) { modeWillChange(RHModeIdle); setStandby(RH_SX126x_STANDBY_RC); _mode = RHModeIdle; } } bool RH_SX126x::sleep() { if (_mode != RHModeSleep) { modeWillChange(RHModeSleep); setSleep(RH_SX126x_SLEEP_START_WARM); _mode = RHModeSleep; } return true; } void RH_SX126x::setModeRx() { if (_mode != RHModeRx) { modeWillChange(RHModeRx); setPacketParametersLoRa(RH_SX126x_MAX_PAYLOAD_LEN); setRx(RH_SX126x_RX_TIMEOUT_NONE); // Timeout 0 _mode = RHModeRx; } } void RH_SX126x::setModeTx() { if (_mode != RHModeTx) { modeWillChange(RHModeTx); setTx(RH_SX126x_RX_TIMEOUT_NONE); // Timeout 0 // Expect to be busy for about 0.5ms _mode = RHModeTx; } } // Sets registers from a canned modem configuration structure bool RH_SX126x::setModemRegisters(const ModemConfig* config) { _packetType = config->packetType; switch (_packetType) { case PacketTypeLoRa: sendCommand(RH_SX126x_CMD_SET_PKT_TYPE, RH_SX126x_PACKET_TYPE_LORA); break; case PacketTypeGFSK: sendCommand(RH_SX126x_CMD_SET_PKT_TYPE, RH_SX126x_PACKET_TYPE_GFSK); break; } return setModulationParameters( config->p1, config->p2, config->p3, config->p4, config->p5, config->p6, config->p7, config->p8); } // Set one of the canned FSK Modem configs // Returns true if its a valid choice bool RH_SX126x::setModemConfig(ModemConfigChoice index) { if (index > (signed int)(sizeof(MODEM_CONFIG_TABLE) / sizeof(ModemConfig))) return false; // Get it out of PROG_MEM ModemConfig cfg; memcpy_P(&cfg, &MODEM_CONFIG_TABLE[index], sizeof(RH_SX126x::ModemConfig)); return setModemRegisters(&cfg); } void RH_SX126x::setPreambleLength(uint16_t bytes) { _preambleLength = bytes; } bool RH_SX126x::isChannelActive() { // Set mode RHModeCad if (_mode != RHModeCad) { modeWillChange(RHModeCad); setCad(); _mode = RHModeCad; } while (_mode == RHModeCad) YIELD; return _cad; } int RH_SX126x::lastSNR() { return _lastSNR; } ////////////////////////////////////////////////// // Specialised SPI routines. // This device has slightly unusual SPI behaviour: including no RH_SPI_WRITE_MASK, so need to rewrite // most SPI access functions bool RH_SX126x::waitUntilNotBusy() { uint8_t busy_timeout_cnt = 0; if (_busyPin == RH_INVALID_PIN) return false; while (digitalRead(_busyPin)) { delay(1); busy_timeout_cnt++; // Need to wait up to 150us when in sleep mode, see table 8-1 if (busy_timeout_cnt > 10) // Should be configurable { Serial.println("ERROR: waitUntilNotBusy TIMEOUT"); return false; } } return true; // OK } uint8_t RH_SX126x::getStatus() { waitUntilNotBusy(); uint8_t status; ATOMIC_BLOCK_START; beginTransaction(); _spi.transfer(RH_SX126x_CMD_GET_STATUS); status = _spi.transfer(RH_SX126x_CMD_NOP); endTransaction(); ATOMIC_BLOCK_END; return status; } bool RH_SX126x::sendCommand(uint8_t command, uint8_t data[], uint8_t len) { #if 0 Serial.print("sendCommand "); Serial.print(command, HEX); Serial.print(" "); for (uint8_t i = 0; i < len; i++) { Serial.print(data[i], HEX); Serial.print(" "); } Serial.println(""); #endif ATOMIC_BLOCK_START; beginTransaction(); waitUntilNotBusy(); _spi.transfer(command); for (uint8_t i = 0; i < len; i++) _spi.transfer(data[i]); endTransaction(); ATOMIC_BLOCK_END; return true; } // Single value command bool RH_SX126x::sendCommand(uint8_t command, uint8_t value) { uint8_t data = value; return sendCommand(command, &data, sizeof(data)); } bool RH_SX126x::sendCommand(uint8_t command) { ATOMIC_BLOCK_START; beginTransaction(); waitUntilNotBusy(); _spi.transfer(command); endTransaction(); ATOMIC_BLOCK_END; return true; } bool RH_SX126x::getCommand(uint8_t command, uint8_t data[], uint8_t len) { ATOMIC_BLOCK_START; beginTransaction(); waitUntilNotBusy(); _spi.transfer(command); _spi.transfer(0); // Wait for data for (uint8_t i = 0; i < len; i++) data[i] = _spi.transfer(0); endTransaction(); ATOMIC_BLOCK_END; return true; } bool RH_SX126x::readRegisters(uint16_t address, uint8_t data[], uint8_t len) { ATOMIC_BLOCK_START; beginTransaction(); waitUntilNotBusy(); _spi.transfer(RH_SX126x_CMD_READ_REGISTER); _spi.transfer(static_cast(address >> 8)); _spi.transfer(static_cast(address)); _spi.transfer(RH_SX126x_CMD_NOP); // Wait for data for (uint8_t i = 0; i < len; i++) data[i] = _spi.transfer(0); endTransaction(); ATOMIC_BLOCK_END; return true; } uint8_t RH_SX126x::readRegister(uint16_t address) { uint8_t data = 0; readRegisters(address, &data, 1); return data; } bool RH_SX126x::writeRegisters(uint16_t address, uint8_t data[], uint8_t len) { #if 0 Serial.print("writeRegisters "); Serial.print(address, HEX); Serial.print(" "); for (uint8_t i = 0; i < len; i++) { Serial.print(data[i], HEX); Serial.print(" "); } Serial.println(""); #endif ATOMIC_BLOCK_START; beginTransaction(); waitUntilNotBusy(); _spi.transfer(RH_SX126x_CMD_WRITE_REGISTER); _spi.transfer(static_cast(address >> 8)); _spi.transfer(static_cast(address)); for (uint8_t i = 0; i < len; i++) _spi.transfer(data[i]); endTransaction(); ATOMIC_BLOCK_END; return true; } bool RH_SX126x::writeRegister(uint16_t address, uint8_t data) { uint8_t d = data; // Single register bye to write return writeRegisters(address, &d, 1); } bool RH_SX126x::writeBuffer(uint8_t offset, const uint8_t data[], uint8_t len) { // A bit different to sendCommand because of offset ATOMIC_BLOCK_START; beginTransaction(); waitUntilNotBusy(); _spi.transfer(RH_SX126x_CMD_WRITE_BUFFER); _spi.transfer(offset); for (uint8_t i = 0; i < len; i++) _spi.transfer(data[i]); endTransaction(); ATOMIC_BLOCK_END; return true; } bool RH_SX126x::writeBuffer(uint8_t offset, const char* text) { return writeBuffer(offset, (uint8_t*) text, strlen(text)); } bool RH_SX126x::readBuffer(uint8_t offset, uint8_t data[], uint8_t len) { // This is a bit different from getCommand, because of offset ATOMIC_BLOCK_START; beginTransaction(); waitUntilNotBusy(); _spi.transfer(RH_SX126x_CMD_READ_BUFFER); _spi.transfer(offset); _spi.transfer(RH_SX126x_CMD_NOP); // Wait for data for (uint8_t i = 0; i < len; i++) data[i] = _spi.transfer(0); endTransaction(); ATOMIC_BLOCK_END; return true; } bool RH_SX126x::setPaConfig(uint8_t paDutyCycle, uint8_t hpMax, uint8_t deviceSel, uint8_t paLut) { uint8_t settings[] = {paDutyCycle, hpMax, deviceSel, paLut}; return sendCommand(RH_SX126x_CMD_SET_PA_CFG, settings, sizeof(settings)); } bool RH_SX126x::setTxParams(uint8_t power, uint8_t rampTime) { uint8_t settings[] = {power, rampTime}; return sendCommand(RH_SX126x_CMD_SET_TX_PARAMS, settings, sizeof(settings)); } bool RH_SX126x::setTxPower(int8_t power) { // The device may have 2 transmitter power amps (PAs), low power and high power // However, depending on the specific model and how it is connected, one or the other might not be available // so we depend on configuration to tell what PAs are available // STM32WLE5JC has 2, but perhaps only 1 is connected to the antenna, depending on the actual circuit // SX1261 only has low power // SX1262 only has high power // SX1268 only has high power bool lp_supported = findRadioPinConfigEntry(RadioPinConfigMode_TX_LOW_POWER) != nullptr; bool hp_supported = findRadioPinConfigEntry(RadioPinConfigMode_TX_HIGH_POWER) != nullptr; bool useHP = false; // Whether we are to use the high or low power amp // REVISIT: needs more work for reduced powers // Depends on if 1261 or 1262 or 1268 if (hp_supported && lp_supported) { // -17 to +22 dBm power = constrain(power, -17, 22); if (power > 14) useHP = true; else useHP = false; } else if (!hp_supported && lp_supported) { // -17 to +15 dBm power = constrain(power, -17, 15); useHP = false; } else // Assume only HP is available { // -9 to +22 dBm power = constrain(power, -9, 22); useHP = true; } // Adjust for optimal settings per Table 13-1 // CAUTION: needs to change for SX1268 if (useHP) { // CAUTION: device power supply needs to be able to provide 3.3V at currents up to 118 mA with high power settings // -9 to 22 dBm SX1262 _requiredPAMode = RadioPinConfigMode_TX_HIGH_POWER; fixPAClamping(true); switch (power) { case 22: case 21: setPaConfig(0x04, 0x07, RH_SX126x_PA_CONFIG_DEVICE_SEL_SX1262, RH_SX126x_PA_CONFIG_PA_LUT); break; case 20: case 19: case 18: setPaConfig(0x03, 0x05, RH_SX126x_PA_CONFIG_DEVICE_SEL_SX1262, RH_SX126x_PA_CONFIG_PA_LUT); power = power + 2; break; case 17: case 16: case 15: setPaConfig(0x02, 0x03, RH_SX126x_PA_CONFIG_DEVICE_SEL_SX1262, RH_SX126x_PA_CONFIG_PA_LUT); power = power + 5; break; default: // 14 and less setPaConfig(0x02, 0x02, RH_SX126x_PA_CONFIG_DEVICE_SEL_SX1262, RH_SX126x_PA_CONFIG_PA_LUT); power = power + 8; break; } } else { // CAUTION: device power supply needs to be able to provide 3.3V at currents up to 32.5 mA with high power settings // -17 to 14 dBm SX1262 _requiredPAMode = RadioPinConfigMode_TX_LOW_POWER; switch (power) { case 15: setPaConfig(0x06, 0x00, RH_SX126x_PA_CONFIG_DEVICE_SEL_SX1261, RH_SX126x_PA_CONFIG_PA_LUT); power = 14; break; case 14: case 13: case 12: case 11: setPaConfig(0x04, 0x00, RH_SX126x_PA_CONFIG_DEVICE_SEL_SX1261, RH_SX126x_PA_CONFIG_PA_LUT); break; default: // 10 and less setPaConfig(0x01, 0x00, RH_SX126x_PA_CONFIG_DEVICE_SEL_SX1261, RH_SX126x_PA_CONFIG_PA_LUT); power = power + 3; break; } } // SetTxParams with the corrected power level return setTxParams(power, RH_SX126x_PA_RAMP_200U); // power, RampTime = 200us. REVISIT: should be configurable ramp time? } bool RH_SX126x::printRegisters(uint16_t address, uint8_t count) { #ifdef RH_HAVE_SERIAL uint8_t buf[256]; readRegisters(address, buf, count); Serial.print("registers starting at: "); Serial.println(address, HEX); for (uint8_t i = 0; i < count; i++) { Serial.println(buf[i], HEX); } #endif return true; } bool RH_SX126x::clearDeviceErrors() { uint8_t data[] = {0x00, 0x00}; return sendCommand(RH_SX126x_CMD_CLR_DEVICE_ERRORS, data, sizeof(data)); } bool RH_SX126x::setDIO2AsRfSwitchCtrl(bool value) { return sendCommand(RH_SX126x_CMD_SET_DIO2_AS_RF_SWITCH_CTRL, value); } bool RH_SX126x::setRxFallbackMode(uint8_t mode) { return sendCommand(RH_SX126x_CMD_SET_RX_TX_FALLBACK_MODE, mode); } bool RH_SX126x::setModulationParameters(uint8_t p1, uint8_t p2, uint8_t p3, uint8_t p4, uint8_t p5, uint8_t p6, uint8_t p7, uint8_t p8) { _lorabw500 = (p2 == RH_SX126x_LORA_BW_500_0); // Need to remember this for modulation quality workaround switch(p2) { case RH_SX126x_LORA_BW_7_8: _bandwidth = 7.8; break; case RH_SX126x_LORA_BW_10_4: _bandwidth = 10.4; break; case RH_SX126x_LORA_BW_15_6: _bandwidth = 15.6; break; case RH_SX126x_LORA_BW_20_8: _bandwidth = 20.8; break; case RH_SX126x_LORA_BW_31_25: _bandwidth = 31.25; break; case RH_SX126x_LORA_BW_41_7: _bandwidth = 41.7; break; case RH_SX126x_LORA_BW_62_5: _bandwidth = 62.5; break; case RH_SX126x_LORA_BW_125_0: _bandwidth = 125.0; break; case RH_SX126x_LORA_BW_250_0: _bandwidth = 250.0; break; case RH_SX126x_LORA_BW_500_0: _bandwidth = 500.0; break; default: _bandwidth = 0; } uint8_t params[] = {p1, p2, p3, p4, p5, p6, p7, p8}; return sendCommand(RH_SX126x_CMD_SET_MODULATION_PARAMS, params, sizeof(params)); } bool RH_SX126x::setModulationParametersLoRa(uint8_t sf, float bw, uint8_t cr, bool ldro) { _packetType = PacketTypeLoRa; // Set packet type LoRa. CAUTION: Must be done in STDBY_RC mode (ie in RH idle mode) sendCommand(RH_SX126x_CMD_SET_PKT_TYPE, RH_SX126x_PACKET_TYPE_LORA); // REVISIT: could automatically calculate LDRO based on symbollength = (1 << SF) / BW _bandwidth = bw; // sf must be 5 to 12 // bw must be 0.0 to 510.0 // cr must be 5 - 8 inclusive (represents 4/5, 4/6, 4/7, 4/8 respectively) uint8_t bw_div2 = bw / 2 + 0.01; uint8_t bwsetting = RH_SX126x_LORA_BW_125_0; switch (bw_div2) { case 3: // 7.8: bwsetting = RH_SX126x_LORA_BW_7_8; break; case 5: // 10.4: bwsetting = RH_SX126x_LORA_BW_10_4; break; case 7: // 15.6: bwsetting = RH_SX126x_LORA_BW_15_6; break; case 10: // 20.8: bwsetting = RH_SX126x_LORA_BW_20_8; break; case 15: // 31.25: bwsetting = RH_SX126x_LORA_BW_31_25; break; case 20: // 41.7: bwsetting = RH_SX126x_LORA_BW_41_7; break; case 31: // 62.5: bwsetting = RH_SX126x_LORA_BW_62_5; break; case 62: // 125.0: bwsetting = RH_SX126x_LORA_BW_125_0; break; case 125: // 250.0 bwsetting = RH_SX126x_LORA_BW_250_0; break; case 250: // 500.0 bwsetting = RH_SX126x_LORA_BW_500_0; break; } return setModulationParameters(sf, bwsetting, cr - 4, ldro, 0, 0, 0, 0); } bool RH_SX126x::setModulationParametersGFSK(uint32_t br, uint8_t sh, uint8_t rxBw, uint32_t freqDev) { _packetType = PacketTypeGFSK; return false; } bool RH_SX126x::calibrate(uint8_t calib_param) { return sendCommand(RH_SX126x_CMD_CALIBRATE, calib_param); } bool RH_SX126x::calibrateImage(uint8_t f1, uint8_t f2) { uint8_t frequencies[] = {f1, f2}; return sendCommand(RH_SX126x_CMD_CALIBRATE_IMAGE, frequencies, sizeof(frequencies)); } bool RH_SX126x::setLoRaSyncWord(uint16_t sync) { uint8_t sync_word[] = {static_cast(sync >> 8), static_cast(sync)}; return writeRegisters(RH_SX126x_REG_LR_SYNCWORD, sync_word, sizeof(sync_word)); } bool RH_SX126x::setOCPConfiguration(uint8_t setting) { return writeRegister(RH_SX126x_REG_OCP, setting); } bool RH_SX126x::setDIO3AsTcxoCtrl(uint8_t voltage, uint32_t delay) { uint8_t settings[] = {voltage, static_cast(delay >> 16), static_cast(delay >> 8), static_cast(delay)}; return sendCommand(RH_SX126x_CMD_SET_DIO3_AS_TCXO_CTRL, settings, sizeof(settings)); } bool RH_SX126x::setTCXO(float voltage, uint32_t delay) { uint8_t voltageValue = RH_SX126x_DIO3_OUTPUT_1_7; if (fabs(voltage - 1.6) <= 0.001) voltageValue = RH_SX126x_DIO3_OUTPUT_1_6; else if (fabs(voltage - 1.7) <= 0.001) voltageValue = RH_SX126x_DIO3_OUTPUT_1_7; else if (fabs(voltage - 1.8) <= 0.001) voltageValue = RH_SX126x_DIO3_OUTPUT_1_8; else if (fabs(voltage - 2.2) <= 0.001) voltageValue = RH_SX126x_DIO3_OUTPUT_2_2; else if (fabs(voltage - 2.4) <= 0.001) voltageValue = RH_SX126x_DIO3_OUTPUT_2_4; else if (fabs(voltage - 2.7) <= 0.001) voltageValue = RH_SX126x_DIO3_OUTPUT_2_7; else if (fabs(voltage - 3.0) <= 0.001) voltageValue = RH_SX126x_DIO3_OUTPUT_3_0; else if (fabs(voltage - 3.3) <= 0.001) voltageValue = RH_SX126x_DIO3_OUTPUT_3_3; uint32_t delayValue = (float)delay / 15.625; return setDIO3AsTcxoCtrl(voltageValue, delayValue); } bool RH_SX126x::setPacketParams(uint8_t p1, uint8_t p2, uint8_t p3, uint8_t p4, uint8_t p5, uint8_t p6, uint8_t p7, uint8_t p8, uint8_t p9) { uint8_t settings[] = {p1, p2, p3, p4, p5, p6, p7, p8, p9}; return sendCommand(RH_SX126x_CMD_SET_PKT_PARAMS, settings, sizeof(settings)); } bool RH_SX126x::setPacketParametersLoRa(uint8_t payload_length) { _packetType = PacketTypeLoRa; // IQ polarity fix per SX1262_datasheet.pdf section 15.4 uint8_t value = readRegister(RH_SX126x_REG_IQ_POLARITY); if (_invertIQ) value = value & ~0x04; else value = value | 0x04; writeRegister(RH_SX126x_REG_IQ_POLARITY, value); // _preambleLength, fixedlength = false, _enableCRC, invertIQ return setPacketParams(0, _preambleLength, RH_SX126x_LORA_PACKET_VARIABLE, payload_length, _enableCRC, _invertIQ, 0, 0, 0); } bool RH_SX126x::setBufferBaseAddress(uint8_t txbase, uint8_t rxbase) { uint8_t settings[] = {txbase, rxbase}; return sendCommand(RH_SX126x_CMD_SET_BUFFER_BASE_ADDRESS, settings, sizeof(settings)); } bool RH_SX126x::setSleep(uint8_t config) { setRadioPinsForMode(RadioPinConfigMode_IDLE); uint8_t sleep_config = config; return sendCommand(RH_SX126x_CMD_SET_SLEEP, &sleep_config, sizeof(sleep_config)); } bool RH_SX126x::setStandby(uint8_t config) { setRadioPinsForMode(RadioPinConfigMode_IDLE); uint8_t standby_config = config; // 0 == STDBY_RC, 1 == STDBY_XOSC return sendCommand(RH_SX126x_CMD_SET_STANDBY, &standby_config, sizeof(standby_config)); } bool RH_SX126x::setTx(uint32_t timeout) { // Need workaround for Modulation Qulity if LoRa BW is 500kHz // Per SX1262_datasheet.pdf section 15.1 if (_packetType == PacketTypeLoRa) { if (_lorabw500) { uint8_t value = readRegister(RH_SX126x_REG_TX_MODULATION); value &= 0xfb; writeRegister(RH_SX126x_REG_TX_MODULATION, value); } else { uint8_t value = readRegister(RH_SX126x_REG_TX_MODULATION); value |= 0x04; writeRegister(RH_SX126x_REG_TX_MODULATION, value); } } setRadioPinsForMode(_requiredPAMode); uint8_t settings[] = {static_cast(timeout >> 16), static_cast(timeout >> 8), static_cast(timeout)}; return sendCommand(RH_SX126x_CMD_SET_TX, settings, sizeof(settings)); } bool RH_SX126x::setTxContinuous() { setRadioPinsForMode(_requiredPAMode); return sendCommand(RH_SX126x_CMD_SET_TX_CONTINUOUS_WAVE); } bool RH_SX126x::setRx(uint32_t timeout) { setBufferBaseAddress(0, 0); setRadioPinsForMode(RadioPinConfigMode_RX); uint8_t settings[] = {static_cast(timeout >> 16), static_cast(timeout >> 8), static_cast(timeout)}; // Sigh, on some chips it doesnt enter RX mode on the first try, so try again. // Why? bool status = sendCommand(RH_SX126x_CMD_SET_RX, settings, sizeof(settings)); status = sendCommand(RH_SX126x_CMD_SET_RX, settings, sizeof(settings)); return status; } bool RH_SX126x::setCad() { // REVISIT: CAD is not yet working. See comments in datasheet: // Choosing the right value is not easy and the values selected must // be carefully tested to ensure a good detection at sensitivity level, and also to limit the number of false detections. // Application note AN1200.48 provides guidance for the selection of these parameters. // See Semtech AN1200.48, page 41. return false; if (_packetType == PacketTypeLoRa) { // REVISIT: detPeak depends on spreading factor uint8_t cadparams[] = {RH_SX126x_CAD_ON_2_SYMB, 22, RH_SX126x_CAD_PARAM_DET_MIN, RH_SX126x_CAD_GOTO_STDBY, 0, 0, 0}; sendCommand(RH_SX126x_CMD_SET_CAD_PARAMS, cadparams, sizeof(cadparams)); return sendCommand(RH_SX126x_CMD_SET_CAD); // Only available in LoRa mode } else return false; } bool RH_SX126x::setRxBoostMode(bool boost, bool retain) { if (boost) writeRegister(RH_SX126x_REG_RXGAIN, RH_SX126x_RX_GAIN_BOOSTED); else writeRegister(RH_SX126x_REG_RXGAIN, RH_SX126x_RX_GAIN_POWER_SAVING); if (retain) { // Include this register in the retention memory uint8_t settings[] = {0x01, 0x08, 0xac}; writeRegisters(RH_SX126x_REG_RETENTION_LIST_BASE_ADDRESS, settings, sizeof(settings)); } return true; } bool RH_SX126x::setRegulatorMode(uint8_t mode) { return sendCommand(RH_SX126x_CMD_SET_REGULATOR_MODE, mode); } bool RH_SX126x::setDioIrqParams(uint16_t irqmask, uint16_t dio1mask, uint16_t dio2mask, uint16_t dio3mask) { uint8_t settings[] = {static_cast(irqmask >> 8), static_cast(irqmask), static_cast(dio1mask >> 8), static_cast(dio1mask), static_cast(dio2mask >> 8), static_cast(dio2mask), static_cast(dio3mask >> 8), static_cast(dio3mask)}; return sendCommand(RH_SX126x_CMD_SET_DIO_IRQ_PARAMS, settings, sizeof(settings)); } bool RH_SX126x::clearIrqStatus(uint16_t mask) { uint8_t settings[] = {static_cast(mask >> 8), static_cast(mask)}; return sendCommand(RH_SX126x_CMD_CLR_IRQ_STATUS, settings, sizeof(settings)); } uint16_t RH_SX126x::getIrqStatus() { uint8_t status[2]; getCommand(RH_SX126x_CMD_GET_IRQ_STATUS, status, sizeof(status)); return ((uint16_t)status[0] << 8) | status[1]; } uint8_t RH_SX126x::getPacketType() { uint8_t ptype[1]; getCommand(RH_SX126x_CMD_GET_PKT_TYPE, ptype, sizeof(ptype)); return ptype[0]; } void RH_SX126x::setInvertIQ(bool invertIQ) { _invertIQ = invertIQ; } bool RH_SX126x::fixPAClamping(bool enable) { // Per SX1262_datasheet.pdf Rev 1.2 section 15.2 uint8_t clamp; clamp = readRegister(RH_SX126x_REG_TX_CLAMP_CFG); if (enable) clamp |= 0x1E; else clamp = (clamp & ~0x1E) | 0x08; return writeRegister(RH_SX126x_REG_TX_CLAMP_CFG, clamp); } bool RH_SX126x::setRadioPinsForMode(RadioPinConfigMode mode) { if (!_radioPinConfig) return false; RadioPinConfigEntry* entry = findRadioPinConfigEntry(mode); if (!entry) return false; for (uint8_t i = 0; i < RH_SX126x_MAX_RADIO_CONTROL_PINS ; i++) { #if 0 Serial.print("set pin "); Serial.print(_radioPinConfig->pinNumber[i]); Serial.print(" to "); Serial.print(entry->pinState[i]); Serial.println(""); #endif if (_radioPinConfig->pinNumber[i] != RH_INVALID_PIN) digitalWrite(_radioPinConfig->pinNumber[i], entry->pinState[i]); } return true; } void RH_SX126x::setRadioPinConfig(RadioPinConfig* config) { for (uint8_t i = 0; i < RH_SX126x_MAX_RADIO_CONTROL_PINS; i++) { if (config && config->pinNumber[i] != RH_INVALID_PIN) pinMode(config->pinNumber[i], OUTPUT); } _radioPinConfig = config; } // Can be overridden RH_SX126x::RadioPinConfigEntry* RH_SX126x::findRadioPinConfigEntry(RadioPinConfigMode mode) { // Sigh, linear search, max of 5 entries if (!_radioPinConfig) return nullptr; // No configurations, Not found for (uint8_t i = 0; i < RH_SX126x_MAX_RADIO_PIN_CONFIG_MODES; i++) { if (_radioPinConfig->configState[i].mode == RadioPinConfigMode_EOT) return nullptr; // End of table, Not found else if (_radioPinConfig->configState[i].mode == mode) return &_radioPinConfig->configState[i]; // Found the one we want } return nullptr; // Table too big, Not found } float RH_SX126x::getFrequencyError() { // check packetType if( _packetType != PacketTypeLoRa) return(0.0); // read the raw frequency error register values uint8_t regBytes[3] = {0}; regBytes[0] = readRegister(RH_SX126x_REG_FREQ_ERROR); regBytes[1] = readRegister(RH_SX126x_REG_FREQ_ERROR + 1); regBytes[2] = readRegister(RH_SX126x_REG_FREQ_ERROR + 2); uint32_t tmp = ((uint32_t) regBytes[0] << 16) | ((uint32_t) regBytes[1] << 8) | regBytes[2]; tmp &= 0x0FFFFF; float error = 0; // check the first bit if (tmp & 0x80000) { // frequency error is negative tmp |= (uint32_t) 0xFFF00000; tmp = ~tmp + 1; error = 1.55 * (float) tmp / (1600.0 / _bandwidth) * -1.0; } else { error = 1.55 * (float) tmp / (1600.0 / _bandwidth); } return(error); }