Change RF95 to deliver packets straight from ISR and no polling for anything

This commit is contained in:
geeksville 2020-02-18 20:06:01 -08:00
parent bf491efddf
commit acce254685
11 changed files with 324 additions and 62 deletions

13
TODO.md
View File

@ -15,6 +15,7 @@ Items to complete before the first alpha release.
# Medium priority
Items to complete before the first beta release.
* GUI on oled hangs for a few seconds occasionally, but comes back
* assign every "channel" a random shared 8 bit sync word (per 4.2.13.6 of datasheet) - use that word to filter packets before even checking CRC. This will ensure our CPU will only wake for packets on our "channel"
* Note: we do not do address filtering at the chip level, because we might need to route for the mesh
* Use the Periodic class for both position and user periodic broadcasts
@ -38,14 +39,12 @@ Items to complete before the first beta release.
General ideas to hit the power draws our spreadsheet predicts. Do the easy ones before beta, the last 15% can be done after 1.0.
* lower BT announce interval to save battery
* change to use RXcontinuous mode and config to drop packets with bad CRC (see section 6.4 of datasheet)
* we currently poll the lora radio from loop(), which is really bad because it means we run loop every 10ms. Instead have the rf95 driver enqueue received messages from the ISR.
* change to use RXcontinuous mode and config to drop packets with bad CRC (see section 6.4 of datasheet) - I think this is already the case
* have mesh service run in a thread that stays blocked until a packet arrives from the RF95
* platformio sdkconfig CONFIG_PM and turn on modem sleep mode
* keep cpu 100% in deepsleep until irq from radio wakes it. Then stay awake for 30 secs to attempt delivery to phone.
* have radiohead ISR send messages to RX queue directly, to allow that thread to block until we have something to send
* use https://lastminuteengineers.com/esp32-sleep-modes-power-consumption/ association sleep pattern to save power - but see https://github.com/espressif/esp-idf/issues/2070 and https://esp32.com/viewtopic.php?f=13&t=12182 it seems with BLE on the 'easy' draw people are getting is 80mA
* stop using loop() instead use a job queue and let cpu sleep
* move lora rx/tx to own thread and block on IO
* measure power consumption and calculate battery life assuming no deep sleep
* do lowest sleep level possible where BT still works during normal sleeping, make sure cpu stays in that mode unless lora rx packet happens, bt rx packet happens or button press happens
* optionally do lora messaging only during special scheduled intervals (unless nodes are told to go to low latency mode), then deep sleep except during those intervals - before implementing calculate what battery life would be with this feature
@ -56,6 +55,8 @@ General ideas to hit the power draws our spreadsheet predicts. Do the easy ones
# Pre-beta priority
During the beta timeframe the following improvements 'would be nice' (and yeah - I guess some of these items count as features, but it is a hobby project ;-) )
* fix the frequency error reading in the RF95 RX code (can't do floating point math in an ISR ;-)
* See CustomRF95::send and fix the problem of dropping partially received packets if we want to start sending
* swap out speck for hw-accelerated full AES https://github.com/espressif/arduino-esp32/blob/master/tools/sdk/include/esp32/hwcrypto/aes.h
* use variable length arduino Strings in protobufs (instead of current fixed buffers)
* don't even power on bluetooth until we have some data to send to the android phone. Most of the time we should be sleeping in a lowpower "listening for lora" only mode. Once we have some packets for the phone, then power on bluetooth
@ -146,4 +147,6 @@ Items after the first final candidate release.
* add receive timestamps to messages, inserted by esp32 when message is received but then shown on the phone
* update build to generate both board types
* have node info screen show real info (including distance and heading)
* blink the power led less often
* blink the power led less often
* have radiohead ISR send messages to RX queue directly, to allow that thread to block until we have something to send
* move lora rx/tx to own thread and block on IO

168
src/CustomRF95.cpp Normal file
View File

@ -0,0 +1,168 @@
#include "CustomRF95.h"
#include <pb_encode.h>
#include <pb_decode.h>
#include "configuration.h"
#include "assert.h"
#include "NodeDB.h"
#define MAX_TX_QUEUE 8 // max number of packets which can be waiting for transmission
/// 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(MemoryPool<MeshPacket> &_pool, PointerQueue<MeshPacket> &_rxDest)
: RH_RF95(NSS_GPIO, DIO0_GPIO),
pool(_pool),
rxDest(_rxDest),
txQueue(MAX_TX_QUEUE),
sendingPacket(NULL)
{
}
bool CustomRF95::init()
{
bool ok = RH_RF95::init();
return ok;
}
/// 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 CustomRF95::send(MeshPacket *p)
{
// FIXME - we currently just slam over into send mode if the RF95 is in RX mode. This is _probably_ safe given
// how quiet our network is, bu it would be better to wait _if_ we are partially though receiving a packet (rather than
// just merely waiting for one).
// This is doubly bad because not only do 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)
{
// if the radio is idle, we can send right away
DEBUG_MSG("immedate send on mesh (txGood=%d,rxGood=%d,rxBad=%d)\n", txGood(), rxGood(), rxBad());
startSend(p);
return ERRNO_OK;
}
else
{
DEBUG_MSG("enquing packet for send from=0x%x, to=0x%x\n", p->from, p->to);
return txQueue.enqueue(p, 0) ? ERRNO_OK : ERRNO_UNKNOWN; // nowait
}
}
// 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();
BaseType_t higherPriWoken = false;
if (_mode == RHModeIdle) // We are now done sending or receiving
{
if (sendingPacket) // Were we sending?
{
// We are done sending that packet, release it
pool.releaseFromISR(sendingPacket, &higherPriWoken);
sendingPacket = NULL;
// DEBUG_MSG("Done with send\n");
}
// 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 = pool.allocZeroed();
SubPacket *p = &mp->payload;
mp->from = _rxHeaderFrom;
mp->to = _rxHeaderTo;
//_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))
{
pool.releaseFromISR(mp, &higherPriWoken);
}
else
{
// parsing was successful, queue for our recipient
mp->has_payload = true;
int res = rxDest.enqueueFromISR(mp, &higherPriWoken); // NOWAIT - fixme, if queue is full, delete older messages
assert(res == pdTRUE);
}
clearRxBuf(); // This message accepted and cleared
}
higherPriWoken |= handleIdleISR();
}
// If we call this _IT WILL NOT RETURN_
if (higherPriWoken)
portYIELD_FROM_ISR();
}
/// Return true if a higher pri task has woken
bool CustomRF95::handleIdleISR()
{
BaseType_t higherPriWoken = false;
// First send any outgoing packets we have ready
MeshPacket *txp = txQueue.dequeuePtrFromISR(0);
if (txp)
startSend(txp);
else
{
// Nothing to send, let's switch back to receive mode
setModeRx();
}
return higherPriWoken;
}
/// 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);
size_t numbytes = pb_encode_to_bytes(radiobuf, sizeof(radiobuf), SubPacket_fields, &txp->payload);
sendingPacket = txp;
setHeaderTo(txp->to);
setHeaderFrom(nodeDB.getNodeNum()); // We must do this before each send, because we might have just changed our nodenum
// setHeaderId(0);
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);
assert(res);
}

43
src/CustomRF95.h Normal file
View File

@ -0,0 +1,43 @@
#pragma once
#include <RH_RF95.h>
#include <RHMesh.h>
#include "MemoryPool.h"
#include "mesh.pb.h"
#include "PointerQueue.h"
#include "MeshTypes.h"
/**
* A version of the RF95 driver which is smart enough to manage packets via queues (no polling or blocking in user threads!)
*/
class CustomRF95 : public RH_RF95
{
MemoryPool<MeshPacket> &pool;
PointerQueue<MeshPacket> &rxDest;
PointerQueue<MeshPacket> txQueue;
MeshPacket *sendingPacket; // The packet we are currently sending
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
*/
CustomRF95(MemoryPool<MeshPacket> &pool, PointerQueue<MeshPacket> &rxDest);
/// 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 send(MeshPacket *p);
bool init();
protected:
// 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
virtual void handleInterrupt();
private:
/// Send a new packet - this low level call can be called from either ISR or userspace
void startSend(MeshPacket *txp);
/// Return true if a higher pri task has woken
bool handleIdleISR();
};

View File

@ -1,4 +1,4 @@
#pragma once
#pragma once
#include <Arduino.h>
#include <assert.h>
@ -10,58 +10,72 @@
*
* Eventually this routine will even be safe for ISR use...
*/
template <class T> class MemoryPool {
template <class T>
class MemoryPool
{
PointerQueue<T> dead;
T *buf; // our large raw block of memory
size_t maxElements;
public:
MemoryPool(size_t _maxElements): dead(_maxElements), maxElements(_maxElements) {
MemoryPool(size_t _maxElements) : dead(_maxElements), maxElements(_maxElements)
{
buf = new T[maxElements];
// prefill dead
for(int i = 0; i < maxElements; i++)
for (int i = 0; i < maxElements; i++)
release(&buf[i]);
}
~MemoryPool() {
~MemoryPool()
{
delete[] buf;
}
/// Return a queable object which has been prefilled with zeros. Panic if no buffer is available
T *allocZeroed() {
T *allocZeroed()
{
T *p = allocZeroed(0);
assert(p); // FIXME panic instead
return p;
}
/// Return a queable object which has been prefilled with zeros - allow timeout to wait for available buffers (you probably don't want this version)
T *allocZeroed(TickType_t maxWait) {
T *allocZeroed(TickType_t maxWait)
{
T *p = dead.dequeuePtr(maxWait);
if(p)
if (p)
memset(p, 0, sizeof(T));
return p;
}
/// Return a queable object which is a copy of some other object
T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY) {
T *allocCopy(const T &src, TickType_t maxWait = portMAX_DELAY)
{
T *p = dead.dequeuePtr(maxWait);
if(p)
if (p)
*p = src;
return p;
}
/// Return a buffer for use by others
void release(T *p) {
void release(T *p)
{
int res = dead.enqueue(p, 0);
assert(res == pdTRUE);
assert(p >= buf && (p - buf) < maxElements); // sanity check to make sure a programmer didn't free something that didn't come from this pool
}
};
/// Return a buffer from an ISR, if higherPriWoken is set to true you have some work to do ;-)
void releaseFromISR(T *p, BaseType_t *higherPriWoken)
{
int res = dead.enqueueFromISR(p, higherPriWoken);
assert(res == pdTRUE);
assert(p >= buf && (p - buf) < maxElements); // sanity check to make sure a programmer didn't free something that didn't come from this pool
}
};

View File

@ -11,9 +11,8 @@
#define DEFAULT_CHANNEL_NUM 3 // we randomly pick one
/// 16 bytes of random PSK for our _public_ default channel that all devices power up on
static const uint8_t defaultpsk[] = { 0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0xbf };
static const uint8_t defaultpsk[] = {0xd4, 0xf1, 0xbb, 0x3a, 0x20, 0x29, 0x07, 0x59, 0xf0, 0xbc, 0xff, 0xab, 0xcf, 0x4e, 0x69, 0xbf};
/**
* ## LoRaWAN for North America
@ -27,11 +26,8 @@ Channel zero starts at 903.08 MHz center frequency.
*/
MeshRadio::MeshRadio(MemoryPool<MeshPacket> &_pool, PointerQueue<MeshPacket> &_rxDest)
: rf95(NSS_GPIO, DIO0_GPIO),
manager(rf95),
pool(_pool),
rxDest(_rxDest),
txQueue(MAX_TX_QUEUE)
: rf95(_pool, _rxDest),
manager(rf95)
{
myNodeInfo.num_channels = NUM_CHANNELS;
@ -81,15 +77,15 @@ bool MeshRadio::init()
void MeshRadio::reloadConfig()
{
rf95.setModeIdle();
rf95.setModeIdle(); // Need to be idle before doing init
// Set up default configuration
// No Sync Words in LORA mode.
rf95.setModemConfig((RH_RF95::ModemConfigChoice)channelSettings.modem_config); // Radio default
// setModemConfig(Bw125Cr48Sf4096); // slow and reliable?
// setModemConfig(Bw125Cr48Sf4096); // slow and reliable?
// rf95.setPreambleLength(8); // Default is 8
assert(channelSettings.channel_num < NUM_CHANNELS); // If the phone tries to tell us to use an illegal channel then panic
assert(channelSettings.channel_num < NUM_CHANNELS); // If the phone tries to tell us to use an illegal channel then panic
// Defaults after init are 434.0MHz, modulation GFSK_Rb250Fd250, +13dbM
float center_freq = CH0 + CH_SPACING * channelSettings.channel_num;
@ -108,11 +104,14 @@ void MeshRadio::reloadConfig()
rf95.setTxPower(channelSettings.tx_power, false);
DEBUG_MSG("Set radio: name=%s. config=%u, ch=%d, txpower=%d\n", channelSettings.name, channelSettings.modem_config, channelSettings.channel_num, channelSettings.tx_power);
// Done with init tell radio to start receiving
rf95.setModeRx();
}
void MeshRadio::sleep() {
// we no longer care about interrupts from this device
void MeshRadio::sleep()
{
// we no longer care about interrupts from this device
rf95.prepareDeepSleep();
// FIXME - leave the device state in rx mode instead
@ -121,10 +120,15 @@ void MeshRadio::sleep() {
ErrorCode MeshRadio::send(MeshPacket *p)
{
#if 1
return rf95.send(p);
#else
DEBUG_MSG("enquing packet for send from=0x%x, to=0x%x\n", p->from, p->to);
return txQueue.enqueue(p, 0); // nowait
#endif
}
#if 0
ErrorCode MeshRadio::sendTo(NodeNum dest, const uint8_t *buf, size_t len)
{
// We must do this before each send, because we might have just changed our nodenum
@ -154,6 +158,7 @@ void MeshRadio::handleReceive(MeshPacket *mp)
int res = rxDest.enqueue(mp, 0); // NOWAIT - fixme, if queue is full, delete older messages
assert(res == pdTRUE);
}
#endif
void MeshRadio::loop()
{
@ -168,8 +173,9 @@ static int16_t packetnum = 0; // packet counter, we increment per xmission
assert(sendTo(NODENUM_BROADCAST, (uint8_t *)radiopacket, sizeof(radiopacket)) == ERRNO_OK);
#endif
#if 0
/// A temporary buffer used for sending/receving packets, sized to hold the biggest buffer we might need
#define MAX_RHPACKETLEN 251
#define MAX_RHPACKETLEN 251
static uint8_t radiobuf[MAX_RHPACKETLEN];
uint8_t rxlen;
uint8_t srcaddr, destaddr, id, flags;
@ -213,7 +219,9 @@ static int16_t packetnum = 0; // packet counter, we increment per xmission
handleReceive(mp);
}
}
#endif
#if 0
// Poll to see if we need to send any packets
MeshPacket *txp = txQueue.dequeuePtr(0); // nowait
if (txp)
@ -234,4 +242,5 @@ static int16_t packetnum = 0; // packet counter, we increment per xmission
DEBUG_MSG("Done with send\n");
}
#endif
}

View File

@ -1,6 +1,6 @@
#pragma once
#include <RH_RF95.h>
#include "CustomRF95.h"
#include <RHMesh.h>
#include "MemoryPool.h"
#include "mesh.pb.h"
@ -80,17 +80,13 @@ public:
void reloadConfig();
private:
RH_RF95 rf95; // the raw radio interface
CustomRF95 rf95; // the raw radio interface
// RHDatagram manager;
// RHReliableDatagram manager; // don't use mesh yet
RHMesh manager;
// MeshRXHandler rxHandler;
MemoryPool<MeshPacket> &pool;
PointerQueue<MeshPacket> &rxDest;
PointerQueue<MeshPacket> txQueue;
/// low level send, might block for mutiple seconds
ErrorCode sendTo(NodeNum dest, const uint8_t *buf, size_t len);

View File

@ -218,7 +218,7 @@ void MeshService::handleToRadio(std::string s)
void MeshService::sendToMesh(MeshPacket *p)
{
nodeDB.updateFrom(*p); // update our local DB for this packet (because phone might have sent position packets etc...)
assert(radio.send(p) == pdTRUE);
assert(radio.send(p) == ERRNO_OK);
}
MeshPacket *MeshService::allocForSending()

View File

@ -292,6 +292,7 @@ void NodeDB::updateFrom(const MeshPacket &mp)
}
/// Find a node in our DB, return null for missing
/// NOTE: This function might be called from an ISR
NodeInfo *NodeDB::getNode(NodeNum n)
{
for (int i = 0; i < *numNodes; i++)

View File

@ -1,21 +1,31 @@
#pragma once
#pragma once
#include "TypedQueue.h"
/**
* A wrapper for freertos queues that assumes each element is a pointer
*/
template <class T> class PointerQueue: public TypedQueue<T *> {
template <class T>
class PointerQueue : public TypedQueue<T *>
{
public:
PointerQueue(int maxElements) : TypedQueue<T *>(maxElements) {
PointerQueue(int maxElements) : TypedQueue<T *>(maxElements)
{
}
// preturns a ptr or null if the queue was empty
T *dequeuePtr(TickType_t maxWait = portMAX_DELAY) {
// returns a ptr or null if the queue was empty
T *dequeuePtr(TickType_t maxWait = portMAX_DELAY)
{
T *p;
return this->dequeue(&p, maxWait) == pdTRUE ? p : NULL;
return this->dequeue(&p, maxWait) == pdTRUE ? p : NULL;
}
// returns a ptr or null if the queue was empty
T *dequeuePtrFromISR(BaseType_t *higherPriWoken)
{
T *p;
return this->dequeueFromISR(&p, higherPriWoken) == pdTRUE ? p : NULL;
}
};

View File

@ -1,4 +1,4 @@
#pragma once
#pragma once
#include <Arduino.h>
#include <assert.h>
@ -7,29 +7,47 @@
* A wrapper for freertos queues. Note: each element object must be quite small, so T should be only
* pointer types or ints
*/
template <class T> class TypedQueue {
template <class T>
class TypedQueue
{
QueueHandle_t h;
public:
TypedQueue(int maxElements) {
TypedQueue(int maxElements)
{
h = xQueueCreate(maxElements, sizeof(T));
assert(h);
}
~TypedQueue() {
~TypedQueue()
{
vQueueDelete(h);
}
int numFree() {
int numFree()
{
return uxQueueSpacesAvailable(h);
}
// pdTRUE for success else failure
BaseType_t enqueue(T x, TickType_t maxWait = portMAX_DELAY) {
BaseType_t enqueue(T x, TickType_t maxWait = portMAX_DELAY)
{
return xQueueSendToBack(h, &x, maxWait);
}
BaseType_t enqueueFromISR(T x, BaseType_t *higherPriWoken)
{
return xQueueSendToBackFromISR(h, &x, higherPriWoken);
}
// pdTRUE for success else failure
BaseType_t dequeue(T *p, TickType_t maxWait = portMAX_DELAY) {
BaseType_t dequeue(T *p, TickType_t maxWait = portMAX_DELAY)
{
return xQueueReceive(h, p, maxWait);
}
BaseType_t dequeueFromISR(T *p, BaseType_t *higherPriWoken)
{
return xQueueReceiveFromISR(h, p, higherPriWoken);
}
};

View File

@ -43,8 +43,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
// Select which board is being used. If the outside build environment has sent a choice, just use that
#if !defined(T_BEAM_V10) && !defined(HELTEC_LORA32)
#define T_BEAM_V10 // AKA Rev1 (second board released)
//#define HELTEC_LORA32
//#define T_BEAM_V10 // AKA Rev1 (second board released)
#define HELTEC_LORA32
#define HW_VERSION_US // We encode the hardware freq range in the hw version string, so sw update can eventually install the correct build
#endif