diff --git a/docs/software/nrf52-TODO.md b/docs/software/nrf52-TODO.md index 6e6555083..cdf57fac9 100644 --- a/docs/software/nrf52-TODO.md +++ b/docs/software/nrf52-TODO.md @@ -47,6 +47,9 @@ Needed to be fully functional at least at the same level of the ESP32 boards. At ## Items to be 'feature complete' +- figure out what the correct current limit should be for the sx1262, currently we just use the default 100 +- use SX126x::startReceiveDutyCycleAuto to save power by sleeping and briefly waking to check for preamble bits. Change xmit rules to have more preamble bits. +- put sx1262 in sleepmode when processor gets shutdown (or rebooted), ideally even for critical faults (to keep power draw low). repurpose deepsleep state for this. - good power management tips: https://devzone.nordicsemi.com/nordic/nordic-blog/b/blog/posts/optimizing-power-on-nrf52-designs - call PMU set_ADC_CONV(0) during sleep, to stop reading PMU adcs and decrease current draw - do final power measurements diff --git a/src/rf95/CustomRF95.cpp b/src/rf95/CustomRF95.cpp index 59eb38a78..612cf8a29 100644 --- a/src/rf95/CustomRF95.cpp +++ b/src/rf95/CustomRF95.cpp @@ -7,11 +7,7 @@ #ifdef RF95_IRQ_GPIO -/// A temporary buffer used for sending/receving packets, sized to hold the biggest buffer we might need -#define MAX_RHPACKETLEN 251 -static uint8_t radiobuf[MAX_RHPACKETLEN]; - -CustomRF95::CustomRF95() : RH_RF95(NSS_GPIO, RF95_IRQ_GPIO), txQueue(MAX_TX_QUEUE) {} +CustomRF95::CustomRF95() : RH_RF95(NSS_GPIO, RF95_IRQ_GPIO) {} bool CustomRF95::canSleep() { @@ -52,13 +48,11 @@ ErrorCode CustomRF95::send(MeshPacket *p) // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one). // To do otherwise would be doubly bad because not only would we drop the packet that was on the way in, // we almost certainly guarantee no one outside will like the packet we are sending. - if (_mode == RHModeIdle || (_mode == RHModeRx && !isReceiving())) { + if (_mode == RHModeIdle || !isReceiving()) { // if the radio is idle, we can send right away DEBUG_MSG("immediate send on mesh fr=0x%x,to=0x%x,id=%d\n (txGood=%d,rxGood=%d,rxBad=%d)\n", p->from, p->to, p->id, txGood(), rxGood(), rxBad()); - waitPacketSent(); // Make sure we dont interrupt an outgoing message - if (!waitCAD()) return false; // Check channel activity @@ -159,29 +153,20 @@ void CustomRF95::handleIdleISR() /// This routine might be called either from user space or ISR void CustomRF95::startSend(MeshPacket *txp) { - assert(!sendingPacket); - - // DEBUG_MSG("sending queued packet on mesh (txGood=%d,rxGood=%d,rxBad=%d)\n", rf95.txGood(), rf95.rxGood(), rf95.rxBad()); - assert(txp->has_payload); - - lastTxStart = millis(); - - size_t numbytes = pb_encode_to_bytes(radiobuf, sizeof(radiobuf), SubPacket_fields, &txp->payload); - - sendingPacket = txp; + size_t numbytes = beginSending(txp); setHeaderTo(txp->to); setHeaderId(txp->id); // if the sender nodenum is zero, that means uninitialized - assert(txp->from); setHeaderFrom(txp->from); // We must do this before each send, because we might have just changed our nodenum assert(numbytes <= 251); // Make sure we don't overflow the tiny max packet size // uint32_t start = millis(); // FIXME, store this in the class - int res = RH_RF95::send(radiobuf, numbytes); + // This legacy implementation doesn't use our inserted packet header + int res = RH_RF95::send(radiobuf + sizeof(PacketHeader), numbytes - sizeof(PacketHeader)); assert(res); } diff --git a/src/rf95/CustomRF95.h b/src/rf95/CustomRF95.h index 5582ab830..e40c1f020 100644 --- a/src/rf95/CustomRF95.h +++ b/src/rf95/CustomRF95.h @@ -13,10 +13,6 @@ class CustomRF95 : public RH_RF95, public RadioInterface { friend class MeshRadio; // for debugging we let that class touch pool - PointerQueue txQueue; - - uint32_t lastTxStart = 0L; - public: /** pool is the pool we will alloc our rx packets from * rxDest is where we will send any rx packets, it becomes receivers responsibility to return packet to the pool diff --git a/src/rf95/RadioInterface.cpp b/src/rf95/RadioInterface.cpp index c2f555c32..fa618f1cd 100644 --- a/src/rf95/RadioInterface.cpp +++ b/src/rf95/RadioInterface.cpp @@ -6,7 +6,10 @@ #include #include -RadioInterface::RadioInterface() {} +RadioInterface::RadioInterface() : txQueue(MAX_TX_QUEUE) +{ + assert(sizeof(PacketHeader) == 4); // make sure the compiler did what we expected +} ErrorCode SimRadio::send(MeshPacket *p) { @@ -21,3 +24,32 @@ void RadioInterface::deliverToReceiver(MeshPacket *p) assert(rxDest->enqueue(p, 0)); // NOWAIT - fixme, if queue is full, delete older messages } +/*** + * given a packet set sendingPacket and decode the protobufs into radiobuf. Returns # of payload bytes to send + */ +size_t RadioInterface::beginSending(MeshPacket *p) +{ + assert(!sendingPacket); + + // DEBUG_MSG("sending queued packet on mesh (txGood=%d,rxGood=%d,rxBad=%d)\n", rf95.txGood(), rf95.rxGood(), rf95.rxBad()); + assert(p->has_payload); + + lastTxStart = millis(); + + PacketHeader *h = (PacketHeader *)radiobuf; + + h->from = p->from; + h->to = p->to; + h->flags = 0; + h->id = p->id; + + // if the sender nodenum is zero, that means uninitialized + assert(h->from); + + size_t numbytes = pb_encode_to_bytes(radiobuf + sizeof(PacketHeader), sizeof(radiobuf), SubPacket_fields, &p->payload) + sizeof(PacketHeader); + + assert(numbytes <= MAX_RHPACKETLEN); + + sendingPacket = p; + return numbytes; +} \ No newline at end of file diff --git a/src/rf95/RadioInterface.h b/src/rf95/RadioInterface.h index 612c25a65..09fd960b8 100644 --- a/src/rf95/RadioInterface.h +++ b/src/rf95/RadioInterface.h @@ -8,6 +8,18 @@ #define MAX_TX_QUEUE 16 // max number of packets which can be waiting for transmission +#define MAX_RHPACKETLEN 256 + +/** + * This structure has to exactly match the wire layout when sent over the radio link. Used to keep compatibility + * wtih the old radiohead implementation. + */ +typedef struct { + uint8_t to, from, id, flags; +} PacketHeader; + + + /** * Basic operations all radio chipsets must implement. * @@ -20,6 +32,13 @@ class RadioInterface protected: MeshPacket *sendingPacket = NULL; // The packet we are currently sending + PointerQueue txQueue; + uint32_t lastTxStart = 0L; + + /** + * A temporary buffer used for sending/receving packets, sized to hold the biggest buffer we might need + * */ + uint8_t radiobuf[MAX_RHPACKETLEN]; /** * Enqueue a received packet for the registered receiver @@ -82,6 +101,14 @@ class RadioInterface /// Make sure the Driver is properly configured before calling init(). /// \return true if initialisation succeeded. virtual bool reconfigure() = 0; + + protected: + /*** + * given a packet set sendingPacket and decode the protobufs into radiobuf. Returns # of bytes to send (including the PacketHeader & payload). + * + * Used as the first step of + */ + size_t beginSending(MeshPacket *p); }; class SimRadio : public RadioInterface diff --git a/src/rf95/RadioLibInterface.cpp b/src/rf95/RadioLibInterface.cpp index c9f59b05d..9c772fc66 100644 --- a/src/rf95/RadioLibInterface.cpp +++ b/src/rf95/RadioLibInterface.cpp @@ -8,8 +8,26 @@ RadioLibInterface::RadioLibInterface(RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq SPIClass &spi, PhysicalLayer *_iface) : module(cs, irq, rst, busy, spi, spiSettings), iface(*_iface) { + assert(!instance); // We assume only one for now + instance = this; } +void INTERRUPT_ATTR RadioLibInterface::isrRxLevel0() +{ + instance->pending = ISR_RX; + instance->disableInterrupt(); +} + +void INTERRUPT_ATTR RadioLibInterface::isrTxLevel0() +{ + instance->pending = ISR_TX; + instance->disableInterrupt(); +} + +/** Our ISR code currently needs this to find our active instance + */ +RadioLibInterface *RadioLibInterface::instance; + /** * Convert our modemConfig enum into wf, sf, etc... */ @@ -41,9 +59,144 @@ void RadioLibInterface::applyModemConfig() } } +/// Send a packet (possibly by enquing in a private fifo). This routine will +/// later free() the packet to pool. This routine is not allowed to stall because it is called from +/// bluetooth comms code. If the txmit queue is empty it might return an error ErrorCode RadioLibInterface::send(MeshPacket *p) { - return ERR_NONE; + // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one). + // To do otherwise would be doubly bad because not only would we drop the packet that was on the way in, + // we almost certainly guarantee no one outside will like the packet we are sending. + if (canSendImmediately()) { + // if the radio is idle, we can send right away + DEBUG_MSG("immediate send on mesh fr=0x%x,to=0x%x,id=%d\n (txGood=%d,rxGood=%d,rxBad=%d)\n", p->from, p->to, p->id, -1, + -1, -1); + + startSend(p); + return ERRNO_OK; + } else { + DEBUG_MSG("enqueuing packet for send from=0x%x, to=0x%x\n", p->from, p->to); + ErrorCode res = txQueue.enqueue(p, 0) ? ERRNO_OK : ERRNO_UNKNOWN; + + if (res != ERRNO_OK) // we weren't able to queue it, so we must drop it to prevent leaks + packetPool.release(p); + + return res; + } +} + +void RadioLibInterface::loop() +{ + PendingISR wasPending = pending; // atomic read + if (wasPending) { + pending = ISR_NONE; // If the flag was set, it is _guaranteed_ the ISR won't be running, because it masked itself + + if (wasPending == ISR_TX) + handleTransmitInterrupt(); + else if (wasPending == ISR_RX) + handleReceiveInterrupt(); + else + assert(0); + + // First send any outgoing packets we have ready + MeshPacket *txp = txQueue.dequeuePtr(0); + if (txp) + startSend(txp); + else { + // Nothing to send, let's switch back to receive mode + // FIXME - RH_RF95::setModeRx(); + } + } +} + +void RadioLibInterface::handleTransmitInterrupt() +{ + assert(sendingPacket); // Were we sending? + + // FIXME - check result code from ISR + + // We are done sending that packet, release it + packetPool.release(sendingPacket); + sendingPacket = NULL; + // DEBUG_MSG("Done with send\n"); +} + +void RadioLibInterface::handleReceiveInterrupt() +{ + // FIXME +} + +#if 0 +// After doing standard behavior, check to see if a new packet arrived or one was sent and start a new send or receive as +// necessary +void CustomRF95::handleInterrupt() +{ + RH_RF95::handleInterrupt(); + enableInterrupt(); // Let ISR run again + + if (_mode == RHModeIdle) // We are now done sending or receiving + { + + // If we just finished receiving a packet, forward it into a queue + if (_rxBufValid) { + // We received a packet + + // Skip the 4 headers that are at the beginning of the rxBuf + size_t payloadLen = _bufLen - RH_RF95_HEADER_LEN; + uint8_t *payload = _buf + RH_RF95_HEADER_LEN; + + // FIXME - throws exception if called in ISR context: frequencyError() - probably the floating point math + int32_t freqerr = -1, snr = lastSNR(); + // DEBUG_MSG("Received packet from mesh src=0x%x,dest=0x%x,id=%d,len=%d rxGood=%d,rxBad=%d,freqErr=%d,snr=%d\n", + // srcaddr, destaddr, id, rxlen, rf95.rxGood(), rf95.rxBad(), freqerr, snr); + + MeshPacket *mp = packetPool.allocZeroed(); + + SubPacket *p = &mp->payload; + + mp->from = _rxHeaderFrom; + mp->to = _rxHeaderTo; + mp->id = _rxHeaderId; + + //_rxHeaderId = _buf[2]; + //_rxHeaderFlags = _buf[3]; + + // If we already have an entry in the DB for this nodenum, goahead and hide the snr/freqerr info there. + // Note: we can't create it at this point, because it might be a bogus User node allocation. But odds are we will + // already have a record we can hide this debugging info in. + NodeInfo *info = nodeDB.getNode(mp->from); + if (info) { + info->snr = snr; + info->frequency_error = freqerr; + } + + if (!pb_decode_from_bytes(payload, payloadLen, SubPacket_fields, p)) { + packetPool.release(mp); + } else { + // parsing was successful, queue for our recipient + mp->has_payload = true; + + deliverToReceiver(mp); + } + + clearRxBuf(); // This message accepted and cleared + } + + handleIdleISR(); + } +} +#endif + +/** start an immediate transmit */ +void RadioLibInterface::startSend(MeshPacket *txp) +{ + size_t numbytes = beginSending(txp); + + int res = iface.startTransmit(radiobuf, numbytes); + assert(res); + + // Must be done AFTER, starting transmit, because startTransmit clears (possibly stale) interrupt pending register bits + enableInterrupt(isrTxLevel0); } /** @@ -53,39 +206,178 @@ ErrorCode RadioLibInterface::send(MeshPacket *p) // include the library +// save transmission state between loops +int transmissionState = ERR_NONE; -void loop() { - Serial.print(F("[SX1262] Transmitting packet ... ")); +void setup() { + Serial.begin(9600); + + // initialize SX1262 with default settings + Serial.print(F("[SX1262] Initializing ... ")); + // carrier frequency: 434.0 MHz + // bandwidth: 125.0 kHz + // spreading factor: 9 + // coding rate: 7 + // sync word: 0x12 (private network) + // output power: 14 dBm + // current limit: 60 mA + // preamble length: 8 symbols + // TCXO voltage: 1.6 V (set to 0 to not use TCXO) + // regulator: DC-DC (set to true to use LDO) + // CRC: enabled + int state = lora.begin(); + if (state == ERR_NONE) { + Serial.println(F("success!")); + } else { + Serial.print(F("failed, code ")); + Serial.println(state); + while (true); + } + + // set the function that will be called + // when packet transmission is finished + lora.setDio1Action(setFlag); + + // start transmitting the first packet + Serial.print(F("[SX1262] Sending first packet ... ")); // you can transmit C-string or Arduino string up to // 256 characters long - // NOTE: transmit() is a blocking method! - // See example SX126x_Transmit_Interrupt for details - // on non-blocking transmission method. - int state = lora.transmit("Hello World!"); + transmissionState = lora.startTransmit("Hello World!"); // you can also transmit byte array up to 256 bytes long - byte byteArr[] = {0x01, 0x23, 0x45, 0x56, 0x78, 0xAB, 0xCD, 0xEF}; - int state = lora.transmit(byteArr, 8); + byte byteArr[] = {0x01, 0x23, 0x45, 0x67, + 0x89, 0xAB, 0xCD, 0xEF}; + state = lora.startTransmit(byteArr, 8); + +} + +// flag to indicate that a packet was sent +volatile bool transmittedFlag = false; + +// disable interrupt when it's not needed +volatile bool enableInterrupt = true; + +// this function is called when a complete packet +// is transmitted by the module +// IMPORTANT: this function MUST be 'void' type +// and MUST NOT have any arguments! +void setFlag(void) +{ + // check if the interrupt is enabled + if (!enableInterrupt) { + return; + } + + // we sent a packet, set the flag + transmittedFlag = true; +} + +void loop() +{ + // check if the previous transmission finished + if (transmittedFlag) { + // disable the interrupt service routine while + // processing the data + enableInterrupt = false; + + // reset flag + transmittedFlag = false; + + if (transmissionState == ERR_NONE) { + // packet was successfully sent + Serial.println(F("transmission finished!")); + + // NOTE: when using interrupt-driven transmit method, + // it is not possible to automatically measure + // transmission data rate using getDataRate() + + } else { + Serial.print(F("failed, code ")); + Serial.println(transmissionState); + } + + // wait a second before transmitting again + delay(1000); + + // send another one + Serial.print(F("[SX1262] Sending another packet ... ")); + + // you can transmit C-string or Arduino string up to + // 256 characters long + transmissionState = lora.startTransmit("Hello World!"); + + // you can also transmit byte array up to 256 bytes long + + byte byteArr[] = {0x01, 0x23, 0x45, 0x67, + 0x89, 0xAB, 0xCD, 0xEF}; + int state = lora.startTransmit(byteArr, 8); + + +// we're ready to send more packets, +// enable interrupt service routine +enableInterrupt = true; +} +} + +// this function is called when a complete packet +// is received by the module +// IMPORTANT: this function MUST be 'void' type +// and MUST NOT have any arguments! +void setFlag(void) +{ + // check if the interrupt is enabled + if (!enableInterrupt) { + return; + } + + // we got a packet, set the flag + receivedFlag = true; +} + +void loop() +{ + // check if the flag is set + if (receivedFlag) { + // disable the interrupt service routine while + // processing the data + enableInterrupt = false; + + // reset flag + receivedFlag = false; + + // you can read received data as an Arduino String + String str; + int state = lora.readData(str); + + // you can also read received data as byte array + + byte byteArr[8]; + int state = lora.readData(byteArr, 8); if (state == ERR_NONE) { - // the packet was successfully transmitted - Serial.println(F("success!")); + // packet was successfully received + Serial.println(F("[SX1262] Received packet!")); - // print measured data rate - Serial.print(F("[SX1262] Datarate:\t")); - Serial.print(lora.getDataRate()); - Serial.println(F(" bps")); + // print data of the packet + Serial.print(F("[SX1262] Data:\t\t")); + Serial.println(str); -} else if (state == ERR_PACKET_TOO_LONG) { - // the supplied packet was longer than 256 bytes - Serial.println(F("too long!")); + // print RSSI (Received Signal Strength Indicator) + Serial.print(F("[SX1262] RSSI:\t\t")); + Serial.print(lora.getRSSI()); + Serial.println(F(" dBm")); -} else if (state == ERR_TX_TIMEOUT) { - // timeout occured while transmitting packet - Serial.println(F("timeout!")); + // print SNR (Signal-to-Noise Ratio) + Serial.print(F("[SX1262] SNR:\t\t")); + Serial.print(lora.getSNR()); + Serial.println(F(" dB")); + +} else if (state == ERR_CRC_MISMATCH) { + // packet was received, but is malformed + Serial.println(F("CRC error!")); } else { // some other error occurred @@ -93,7 +385,12 @@ if (state == ERR_NONE) { Serial.println(state); } -// wait for a second before transmitting again -delay(1000); +// put module back to listen mode +lora.startReceive(); + +// we're ready to receive more packets, +// enable interrupt service routine +enableInterrupt = true; } - */ \ No newline at end of file +} +*/ \ No newline at end of file diff --git a/src/rf95/RadioLibInterface.h b/src/rf95/RadioLibInterface.h index 9ca0f0de7..77ee11eb4 100644 --- a/src/rf95/RadioLibInterface.h +++ b/src/rf95/RadioLibInterface.h @@ -4,8 +4,30 @@ #include +// ESP32 has special rules about ISR code +#ifdef ARDUINO_ARCH_ESP32 +#define INTERRUPT_ATTR IRAM_ATTR +#else +#define INTERRUPT_ATTR +#endif + class RadioLibInterface : public RadioInterface { + enum PendingISR { ISR_NONE = 0, ISR_RX, ISR_TX }; + + /** + * What sort of interrupt do we expect our helper thread to now handle */ + volatile PendingISR pending; + + /** Our ISR code currently needs this to find our active instance + */ + static RadioLibInterface *instance; + + /** + * Raw ISR handler that just calls our polymorphic method + */ + static void isrRxLevel0(), isrTxLevel0(); + protected: float bw = 125; uint8_t sf = 9; @@ -27,6 +49,16 @@ class RadioLibInterface : public RadioInterface */ PhysicalLayer &iface; + /** + * Glue functions called from ISR land + */ + virtual void disableInterrupt() = 0; + + /** + * Enable a particular ISR callback glue function + */ + virtual void enableInterrupt(void (*)()) = 0; + public: RadioLibInterface(RADIOLIB_PIN_TYPE cs, RADIOLIB_PIN_TYPE irq, RADIOLIB_PIN_TYPE rst, RADIOLIB_PIN_TYPE busy, SPIClass &spi, PhysicalLayer *iface); @@ -51,9 +83,20 @@ class RadioLibInterface : public RadioInterface /// \return true if initialisation succeeded. virtual bool init() { return true; } + virtual void loop(); // Idle processing + protected: /** * Convert our modemConfig enum into wf, sf, etc... */ void applyModemConfig(); + + /** Could we send right now (i.e. either not actively receiving or transmitting)? */ + virtual bool canSendImmediately() = 0; + + /** start an immediate transmit */ + void startSend(MeshPacket *txp); + + void handleTransmitInterrupt(); + void handleReceiveInterrupt(); }; \ No newline at end of file diff --git a/src/rf95/SX1262Interface.cpp b/src/rf95/SX1262Interface.cpp index bd951c46a..7d5bc4f22 100644 --- a/src/rf95/SX1262Interface.cpp +++ b/src/rf95/SX1262Interface.cpp @@ -27,6 +27,9 @@ bool SX1262Interface::init() int res = lora.begin(freq, bw, sf, cr, syncWord, power, currentLimit, preambleLength, tcxoVoltage, useRegulatorLDO); DEBUG_MSG("LORA init result %d\n", res); + if (res != ERR_NONE) + res = lora.setCRC(SX126X_LORA_CRC_ON); + return res == ERR_NONE; } @@ -69,3 +72,19 @@ bool SX1262Interface::reconfigure() return true; } + +/** Could we send right now (i.e. either not actively receving or transmitting)? */ +bool SX1262Interface::canSendImmediately() +{ + return true; // FIXME +#if 0 + // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one). + // To do otherwise would be doubly bad because not only would we drop the packet that was on the way in, + // we almost certainly guarantee no one outside will like the packet we are sending. + if (_mode == RHModeIdle || isReceiving()) { + // if the radio is idle, we can send right away + DEBUG_MSG("immediate send on mesh fr=0x%x,to=0x%x,id=%d\n (txGood=%d,rxGood=%d,rxBad=%d)\n", p->from, p->to, p->id, + txGood(), rxGood(), rxBad()); + } +#endif +} \ No newline at end of file diff --git a/src/rf95/SX1262Interface.h b/src/rf95/SX1262Interface.h index c700145ef..f480415ff 100644 --- a/src/rf95/SX1262Interface.h +++ b/src/rf95/SX1262Interface.h @@ -18,4 +18,18 @@ class SX1262Interface : public RadioLibInterface /// Make sure the Driver is properly configured before calling init(). /// \return true if initialisation succeeded. virtual bool reconfigure(); + + protected: + /** + * Glue functions called from ISR land + */ + virtual void INTERRUPT_ATTR disableInterrupt() { lora.clearDio1Action(); } + + /** + * Enable a particular ISR callback glue function + */ + virtual void enableInterrupt(void (*callback)()) { lora.setDio1Action(callback); } + + /** Could we send right now (i.e. either not actively receiving or transmitting)? */ + virtual bool canSendImmediately(); }; \ No newline at end of file