operate LoRa radio over I2C

Dependencies:   TimeoutAbs lib_i2c_slave_block sx12xx_hal

radio chip selection

Radio chip driver is not included, allowing choice of radio device.
If you're using SX1272 or SX1276, then import sx127x driver into your program.
if you're using SX1261 or SX1262, then import sx126x driver into your program.
if you're using SX1280, then import sx1280 driver into your program.
If you're using NAmote72 or Murata discovery, then you must import only sx127x driver.

This project is used as slave device with i2c_lora_master on raspberry pi. This i2c_lora_slave offloads the real-time requirements onto microcontroller. Also permits multiple slave radio devices connected to master. Radio MAC layer exists on I2C master, along with application layer.

If beacon operation is enabled, I2C functions which access radio chip are blocked while beacon is loaded and transmitted.
See lib_i2c_slave_block for wiring connections.

main.cpp

Committer:
Wayne Roberts
Date:
2019-02-08
Revision:
0:9eb5b8bf9f7b

File content as of revision 0:9eb5b8bf9f7b:


#include <mbed.h>
#include "LowPowerTimeoutAbs.h"
#include "radio_device.h"

#define IRQ_OUT_PIN     D6

#define BEACON_PRELOAD_us               500000

typedef struct {
    uint8_t beacon_enabled : 1; // 0
    uint8_t beacon_guard   : 1; // 1
    uint8_t beacon_loaded  : 1; // 2
    uint8_t beacon_test    : 1; // 3
    uint8_t req_type       : 4; // 4,5,6,7
} flags_t;

const int SLAVE_ADDRESS = 0xA0;

RawSerial pc(USBTX, USBRX);

EventQueue queue;
uint8_t skip_beacon_cnt = 0;
uint16_t beaconPreambleSymbs;
uint16_t preambleSymbs;
unsigned beaconInterval_us;
uint8_t beaconSizeBytes;
/* low power timer used because hi-speed timer doesnt have crystal, and 32Khz crystal is 20ppm */
LowPowerTimeoutAbs send_beaconTimer;
LowPowerTimeoutAbs load_beaconTimer;
uint8_t beacon_payload[4];
cfg_t cfg;
uint8_t get_n_rssi;

volatile flags_t flags;
volatile uint8_t bs;
volatile uint16_t rx_slot;
volatile us_timestamp_t beaconAt;
volatile uint8_t blocked_cmd;

DigitalOut irqOutPin(IRQ_OUT_PIN);
irq_t irq;

uint8_t toI2C_rx_pkt[256];
volatile uint16_t toI2C_rx_pkt_idx;   // incremented by 32: will overflow 8bit

int tx_pkt_len;
uint8_t tx_pkt_idx;

void console()
{
    char c;
    if (pc.readable() != 1)
        return;

    c = pc.getc();

    if (c == '.') {
        pc.printf("beacon_guard%u ", flags.beacon_guard);
        pc.printf("beacon_loaded%u ", flags.beacon_loaded);
        pc.printf("bs:%u\r\n", bs);
    }
}

void
send_beacon()
{
    bs = 3;
    if (!flags.beacon_loaded)
        return;

    bs = 4;
    Radio::Send(beaconSizeBytes, 0, 0, 0);
    flags.beacon_loaded = false;
}

static uint16_t beacon_crc( uint8_t *buffer, uint16_t length )
{
    // The CRC calculation follows CCITT
    const uint16_t polynom = 0x1021;
    // CRC initial value
    uint16_t crc = 0x0000;

    if( buffer == NULL )
    {
        return 0;
    }

    for( uint16_t i = 0; i < length; ++i )
    {
        crc ^= ( uint16_t ) buffer[i] << 8;
        for( uint16_t j = 0; j < 8; ++j )
        {
            crc = ( crc & 0x8000 ) ? ( crc << 1 ) ^ polynom : ( crc << 1 );
        }
    }

    return crc;
}

void
_load_beacon()
{
    bool inv_iq = false;
    uint16_t crc;

    bs = 2;
    Radio::Standby( );

    if (skip_beacon_cnt > 0) {
        inv_iq = true;
        skip_beacon_cnt--;
    }
                       //    preambleLen, fixLen, crcOn, invIQ
    Radio::LoRaPacketConfig(beaconPreambleSymbs, true, false, inv_iq);

    Radio::SetFixedPayloadLength(beaconSizeBytes);

    Radio::radio.tx_buf[0] = beacon_payload[0];
    Radio::radio.tx_buf[1] = beacon_payload[1];
    Radio::radio.tx_buf[2] = beacon_payload[2];
    Radio::radio.tx_buf[3] = beacon_payload[3];
    //beacon_payload[0] = CMD_NONE;

    crc = beacon_crc(Radio::radio.tx_buf, 4);
    Radio::radio.tx_buf[4] = crc & 0xff;
    Radio::radio.tx_buf[5] = crc >> 8;

    flags.beacon_loaded = true;
}

uint16_t tim_get_current_slot()
{
    us_timestamp_t us_until = beaconAt - send_beaconTimer.read_us();
    return us_until / 30000;
}

void load_beacon()
{
    if (flags.beacon_enabled) {
        flags.beacon_guard = true;
        bs = 1;
        queue.call_in(200, _load_beacon);
    }
}

void get_random()
{
    uint32_t r;
    Radio::Rx(0);
    wait(0.01);
    r = Radio::Random();

    irq.buf[1] = r & 0xff;
    r >>= 8;
    irq.buf[2] = r & 0xff; 
    r >>= 8;
    irq.buf[3] = r & 0xff;
    r >>= 8;
    irq.buf[4] = r & 0xff;

    irq.fields.flags.irq_type = IRQ_TYPE_RANDOM;
    irqOutPin = 1;
}

void cf_read()
{
    uint32_t hz;
    float MHz;
#ifdef SX127x_H
    MHz = Radio::radio.get_frf_MHz();
#else
    MHz = Radio::radio.getMHz();
#endif
    
    hz = MHz * 1000000;

    irq.buf[1] = hz & 0xff;
    hz >>= 8;
    irq.buf[2] = hz & 0xff; 
    hz >>= 8;
    irq.buf[3] = hz & 0xff;
    hz >>= 8;
    irq.buf[4] = hz & 0xff;

    irq.fields.flags.irq_type = IRQ_TYPE_CF;
    irqOutPin = 1;
}

void measure_ambient(uint8_t n_samples)
{
    int i, acc = 0;
    float bg_rssi;
    for (i = 0; i < n_samples; i++) {
        int rssi = Radio::GetRssiInst();
        acc += rssi;
        wait(0.01);
    }
    bg_rssi = acc / (float)n_samples;
    pc.printf("bg_rssi:%.1fdBm ", bg_rssi);
    
    irq.fields.rssi = bg_rssi * 10;
    irq.fields.flags.irq_type = IRQ_TYPE_RSSI;
    irqOutPin = 1;
}

void txDoneCB()
{
    if (flags.beacon_test) {
        flags.beacon_test = 0;
        return;
    }

    if (cfg.fields.flags.rxOnTxDone) {
               //    preambleLen, fixLen, crcOn, invIQ
        Radio::LoRaPacketConfig(preambleSymbs, false, true, false);
        
        Radio::Rx(0);

        bs = 5;
        if (flags.beacon_guard) {   // beacon done transmitting
            measure_ambient(cfg.fields.n_rssi_samples);
            flags.beacon_guard = false;
            bs = 6;

            beaconAt += beaconInterval_us;
            load_beaconTimer.attach_us(load_beacon, beaconAt - BEACON_PRELOAD_us);
            send_beaconTimer.attach_us(send_beacon, beaconAt);
        }
    }

}

void send_downlink()
{
    if (tx_pkt_len > 0 && tx_pkt_idx >= tx_pkt_len) {
        Radio::Send(tx_pkt_len,
            cfg.fields.maxListenTime,
            cfg.fields.channelFreeTime,
            cfg.fields.rssiThresh
        );

        tx_pkt_len = 0; // packet only sent once, as requested by host
    }
}

void rxDoneCB(uint8_t size, float rssi, float snr)
{
    if (cfg.fields.flags.rxOnTxDone) {
        queue.call_in(cfg.fields.downlinkOffset, send_downlink);
    }

    if (irq.fields.flags.rx_pkt == 0) {
        if (flags.beacon_enabled)
            irq.fields.rx_slot = tim_get_current_slot();
        else
            irq.fields.rx_slot = 0;

        irq.fields.rssi = rssi * 10;
        irq.fields.pkt_snr = snr;
        irq.fields.pkt_len = size;
        memcpy(toI2C_rx_pkt, Radio::radio.rx_buf, size);
        irq.fields.flags.rx_pkt = 1;
        irqOutPin = 1;
        pc.printf("rxlen%u snr:%.1f rssi:%.1f slot:%u\r\n", size, snr, rssi, irq.fields.rx_slot);
    } else {
        pc.printf("rx_pkt_overrun\r\n");
        irq.fields.flags.rx_pkt_overrun = 1;
    }

}

void tim_init()
{
    beaconAt = send_beaconTimer.read_us() + beaconInterval_us;
    load_beaconTimer.attach_us(load_beacon, beaconAt - BEACON_PRELOAD_us);
    send_beaconTimer.attach_us(send_beacon, beaconAt);
}

volatile uint8_t cnt = 0;

void fill_tx_buf(uint8_t cmd)     // called from isr
{
    unsigned i;

    /* Master is reading from slave
     * Called from ISR; must return immediately */

    if (flags.beacon_guard && cmd >= CMD_RADIO) {
        blocked_cmd = cmd;
        return;
    }

    switch (cmd) {
        case CMD_TEST:
            for (i = 0; i < cmd_to_length[CMD_TEST]; i++)
                i2c.tx_buf[i] = i + cnt;
            cnt++;
            break;
        case CMD_TEST32:
            for (i = 0; i < cmd_to_length[CMD_TEST32]; i++)
                i2c.tx_buf[i] = i + cnt;
            cnt++;
            break;
        case CMD_RX_PAYLOAD:
            memcpy(i2c.tx_buf, toI2C_rx_pkt+toI2C_rx_pkt_idx, sizeof(i2c.tx_buf));

            toI2C_rx_pkt_idx += cmd_to_length[CMD_RX_PAYLOAD];
            if (toI2C_rx_pkt_idx >= irq.fields.pkt_len) {
                irq.fields.pkt_len = 0;
                irq.fields.flags.rx_pkt = 0;
                if (irq.buf[0] == 0)
                    irqOutPin = 0;
            }
            break;
        case CMD_SKIP_BEACONS:
            i2c.tx_buf[0] = skip_beacon_cnt;
            break;
        case CMD_CURRENT_SLOT:
            {
                uint16_t *u16ptr = (uint16_t*)i2c.tx_buf;
                *u16ptr = tim_get_current_slot();
            }
            break;
        case CMD_REQ_RANDOM:
            flags.req_type = IRQ_TYPE_RANDOM;
            break;
        case CMD_IRQ:
            if (irq.fields.flags.rx_pkt)
                toI2C_rx_pkt_idx = 0;   // packet reading expected to start

            for (i = 0; i < cmd_to_length[CMD_IRQ]; i++)
                i2c.tx_buf[i] = irq.buf[i];

            irq.fields.flags.rx_pkt_overrun = 0;
            irq.fields.flags.irq_type = 0;
            if (irq.buf[0] == 0)
                irqOutPin = 0;

            break;
    }

} // ..fill_tx_buf()

void service_i2c_write(uint8_t cmd, uint8_t len, const uint8_t* req)
{
    uint32_t u32;
    uint8_t s8;

    if (flags.beacon_guard && cmd >= CMD_RADIO) {
        blocked_cmd = cmd;
        return;
    }

    switch (cmd) {
        case CMD_TEST:
        case CMD_TEST32:
            pc.printf("test:");
            for (s8 = 0; s8 < cmd_to_length[cmd]; s8++)
                pc.printf("%02x ", req[s8]);
            pc.printf("\r\n");
            break;
        case CMD_CFHZ_REQ:
            flags.req_type = IRQ_TYPE_CF;
            break;
        case CMD_FSK_MODEM_CFG_REQ:
            flags.req_type = IRQ_TYPE_FSK_MODEM;
            break;
        case CMD_FSK_PKT_CFG_REQ:
            flags.req_type = IRQ_TYPE_FSK_PKT;
            break;
        case CMD_FSK_SYNC_REQ:
            flags.req_type = IRQ_TYPE_FSK_SYNC;
            break;
        case CMD_TXDBM_REQ:
            flags.req_type = IRQ_TYPE_TXDBM;
            break;
        case CMD_LORA_MODEM_CFG_REQ:
            flags.req_type = IRQ_TYPE_LORA_MODEM;
            break;
        case CMD_LORA_PKT_CFG_REQ:
            flags.req_type = IRQ_TYPE_LORA_PKT;
            break;
        case CMD_OPMODE_REQ:
            flags.req_type = IRQ_TYPE_OPMODE;
            break;
        case CMD_RADIO_RESET:
            load_beaconTimer.detach();
            send_beaconTimer.detach();
            flags.beacon_enabled = 0;
            radio_reset();
            break;
        case CMD_CFHZ_WRITE:
            u32 = req[3];
            u32 <<= 8;
            u32 |= req[2];
            u32 <<= 8;
            u32 |= req[1];
            u32 <<= 8;
            u32 |= req[0];
            pc.printf("setCh %luhz\r\n", u32);
            Radio::SetChannel(u32);
            break;
        case CMD_TXDBM_WRITE:
            s8 = req[0];
            Radio::set_tx_dbm(s8);
            pc.printf("set tx dBm:%d\r\n", s8);
            break;
        case CMD_LORA_MODEM_CFG_WRITE:
            {
                uint8_t sf, cr;

                u32 = req[1];
                u32 <<= 8;
                u32 |= req[0];
                pc.printf("(%04lx) ", u32);
                sf = req[2];
                cr = req[3];
                pc.printf("modemcfg %luKhz sf%u cr%u\r\n", u32, sf, cr);
                Radio::LoRaModemConfig(u32, sf, cr);
            }
            break;
        case CMD_RX_START:
            u32 = req[3];
            u32 <<= 8;
            u32 |= req[2];
            u32 <<= 8;
            u32 |= req[1];
            u32 <<= 8;
            u32 |= req[0];
            Radio::Rx(u32);
            pc.printf("rx(%lu)\r\n", u32);
            break;
        case CMD_LORA_PKT_CFG_WRITE:
            {
                pktcfg_t pktcfg;
                u32 = req[1];
                u32 <<= 8;
                u32 |= req[0];
                preambleSymbs = u32;
                pktcfg.octet = req[2];
                pc.printf("pktcfg pre%lu fixlen%u crc%u inv%u\r\n", u32, pktcfg.bits.fixLen, pktcfg.bits.crcOn, pktcfg.bits.invIQ);
                Radio::LoRaPacketConfig(u32, pktcfg.bits.fixLen, pktcfg.bits.crcOn, pktcfg.bits.invIQ);
            }
            break;
        case CMD_BEACON_CFG_WRITE:
            {   
                us_timestamp_t txStartAt;

                u32 = req[1];
                u32 <<= 8;
                u32 |= req[0];
                pc.printf("bi%lu ", u32);
                beaconInterval_us = u32 * 1000000;
                pc.printf("{%u} ", beaconInterval_us);

                u32 = req[3];
                u32 <<= 8;
                u32 |= req[2];
                pc.printf("ps%lu ", u32);
                beaconPreambleSymbs = u32;

                beaconSizeBytes = req[4];
                pc.printf("len%u\r\n", beaconSizeBytes);

                if (beaconInterval_us > 0) {
                    _load_beacon();
                    send_beacon();
                    txStartAt = Radio::lpt.read_us();
                    flags.beacon_test = 1;
                    while (flags.beacon_test)
                        Radio::service();

                    u32 = Radio::irqAt - txStartAt;
                    pc.printf("beaconDur:%lu, 0x%lx\r\n", u32, u32);

                    tim_init();
                    flags.beacon_enabled = 1;

                    irq.buf[1] = u32 & 0xff;
                    u32 >>= 8;
                    irq.buf[2] = u32 & 0xff; 
                    u32 >>= 8;
                    irq.buf[3] = u32 & 0xff;
                    u32 >>= 8;
                    irq.buf[4] = u32 & 0xff;
                    irq.fields.flags.irq_type = IRQ_TYPE_BEACON_DUR;
                    //pc.printf("BDirq.buf[0]:%02x\r\n", irq.buf[0]);
                    irqOutPin = 1;
                } else {
                    load_beaconTimer.detach();
                    send_beaconTimer.detach();
                    flags.beacon_enabled = 0;
                }
            }
            break;
        case CMD_BEACON_PAYLOAD:
            beacon_payload[0] = req[0];
            beacon_payload[1] = req[1];
            beacon_payload[2] = req[2];
            beacon_payload[3] = req[3];
            break;
        case CMD_CFG:
            memcpy(cfg.buf, req, sizeof(cfg_t));
            pc.printf("cfg %u\r\n", cfg.fields.n_rssi_samples);
            break;
        case CMD_STANDBY:
            Radio::Standby();
            break;
        case CMD_PUBLIC_NET:
            pc.printf("pubnet%02x\r\n", req[0]);
            Radio::SetPublicNetwork(req[0] == 0 ? false : true);
            break;
        case CMD_MAX_PAYLEN_WRITE:
            pc.printf("maxpay%02x\r\n", req[0]);
            Radio::SetRxMaxPayloadLength(req[0]);
            break;
        case CMD_TX_BUF_START:
            if (cfg.fields.flags.rxOnTxDone) {
                /* turn off receiver in preparation for downlink */
                Radio::Standby();   

                //                         preambleLen, fixLen, crcOn, invIQ
                Radio::LoRaPacketConfig(preambleSymbs, false, false, true);
            }

            tx_pkt_len = req[0];
            memcpy(Radio::radio.tx_buf, req+1, 31);
            tx_pkt_idx = 31;
            break;
        case CMD_TX_BUF:
            {
                uint8_t len = 32;
                uint8_t end = (tx_pkt_idx + 32) & 0xff;
                if (end < 32)
                    len -= end;
                memcpy(Radio::radio.tx_buf + tx_pkt_idx, req, len);
                tx_pkt_idx += len;
            }
            break;
        case CMD_RSSI_REQ:
            get_n_rssi = req[0];
            break;
        case CMD_SEND:
            Radio::Send(tx_pkt_len,
                cfg.fields.maxListenTime,
                cfg.fields.channelFreeTime,
                cfg.fields.rssiThresh
            );
            break;
        case CMD_SKIP_BEACONS:
            skip_beacon_cnt = req[0];
            break;
        case CMD_LORA_SYMBTO_WRITE: // 1  byte
            Radio::SetLoRaSymbolTimeout(req[0]);
            break;
        case CMD_FSK_MODEM_CFG_WRITE: // 10bytes
            {
                uint32_t bps, fdev_hz;
                uint16_t khz_bw;
                u32 = req[3];
                u32 <<= 8;
                u32 |= req[2];
                u32 <<= 8;
                u32 |= req[1];
                u32 <<= 8;
                u32 |= req[0];
                bps = u32;

                u32 = req[5];
                u32 <<= 8;
                u32 |= req[4];
                khz_bw = u32;

                u32 = req[9];
                u32 <<= 8;
                u32 |= req[8];
                u32 <<= 8;
                u32 |= req[7];
                u32 <<= 8;
                u32 |= req[6];
                fdev_hz = u32;
                Radio::GFSKModemConfig(bps, khz_bw, fdev_hz);
            }
            break;
        case CMD_FSK_PKT_CFG_WRITE: // 4 bytes
            u32 = req[1];
            u32 <<= 8;
            u32 |= req[0];
            Radio::GFSKPacketConfig(u32, req[2], req[3]);
            break;
        case CMD_FSK_SYNC_WRITE:
            /* TODO */
            break;
#ifdef DEBUG_SMBUS
        case CMD_ISR:
            pc.printf("i%02x ", req[0]);    // lsbyte of I2C_ISR
            break;
        case CMD_STOPF:
            pc.printf("STOPF\r\n");
            break;
        case CMD_AF:
            pc.printf("AF%u ", req[0]); // req[0] is dma tx cndtr
            break;
#endif /* DEBUG_SMBUS */
        case CMD_BUSERR:
            pc.printf("BUSERR%u\r\n", req[0]);
            break;
        /* failures: */
        case CMD_ARLO:
            pc.printf("ARLO%u,%u\r\n", toI2C_rx_pkt_idx, req[0]); // req[0] tx_cndtr
            break;
        case CMD_TIMEOUT:
            pc.printf("TIMEOUT(tx%u,rx%u)\r\n", req[0], req[1]);    // req[0] tx_cndtr
            break;
        default:
            pc.printf("??%02x??\r\n", cmd);
            break;
    } // ..switch (cmd)

} // ..service_i2c_write()

const RadioEvents_t rev = {
    /* Dio0_top_half */     NULL,
    /* TxDone_topHalf */    NULL,
    /* TxDone_botHalf */    txDoneCB,
    /* TxTimeout  */        NULL,
    /* RxDone  */           rxDoneCB,
    /* RxTimeout  */        NULL,
    /* RxError  */          NULL,
    /* FhssChangeChannel  */NULL,
    /* CadDone  */          NULL
};

int main()
{
    Thread eventThread(osPriorityNormal, 512);
    int res;

    pc.baud(115200);
    pc.printf("\r\nreset\r\n");

    Radio::Init(&rev);
    radio_device_init();

    res = smbus_init(I2C_SDA, I2C_SCL, SLAVE_ADDRESS);
    pc.printf("%d = smbus_init()\r\n", res);
    if (res < 0) {
        for (;;) asm("nop");
    }

    eventThread.start(callback(&queue, &EventQueue::dispatch_forever));

    while (1) {
        if (i2c.c_overrun) {
            pc.printf("c_overrun\r\n");
            for (;;) asm("nop");
        }

        service_i2c();
        console();

        if (get_n_rssi > 0) {
            measure_ambient(get_n_rssi);
            get_n_rssi = 0;
        }

        if (flags.req_type != IRQ_TYPE_PKT) {
            switch (flags.req_type) {
                case IRQ_TYPE_CF:
                    cf_read();
                    break;
                case IRQ_TYPE_RANDOM:
                    get_random();
                    break;
                case IRQ_TYPE_FSK_MODEM:
                    get_fsk_modem();
                    break;
                case IRQ_TYPE_FSK_PKT:
                    get_fsk_packet();
                    break;
                case IRQ_TYPE_FSK_SYNC:
                    get_fsk_sync();
                    break;
                case IRQ_TYPE_TXDBM:
                    get_tx_dbm();
                    break;
                case IRQ_TYPE_LORA_MODEM:
                    get_lora_modem();
                    break;
                case IRQ_TYPE_LORA_PKT:
                    get_lora_packet();
                    break;
                case IRQ_TYPE_OPMODE:
                    get_opmode();
                    break;
            } // ..switch (flags.req_type)
            flags.req_type = IRQ_TYPE_PKT;
        }

        if (blocked_cmd) {
            pc.printf("blocked_cmd:%02x\r\n", blocked_cmd);
            blocked_cmd = 0;
        }

        Radio::service();
    } // ..while (1)
}