diff --git a/README.md b/README.md index cbe1538..11694d7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # lora -LoRa protocol for TinyGo- based on tinygo-org/drivers. +High quality LoRa library and drivers for the Go language. + + diff --git a/examples/receiver/receiver.go b/examples/receiver/receiver.go new file mode 100644 index 0000000..ec367ea --- /dev/null +++ b/examples/receiver/receiver.go @@ -0,0 +1,64 @@ +//go:build tinygo + +package main + +import ( + "context" + "io" + "machine" + "time" + + "github.com/soypat/lora" + "github.com/soypat/lora/sx127x" +) + +var ( + SX127X_SPI = machine.SPI0 +) + +const ( + SX127X_PIN_RST = machine.GP16 + // SPI definition for SX127x + + SX127X_PIN_SCK = machine.GP2 + SX127X_PIN_TX = machine.GP3 + SX127X_PIN_RX = machine.GP4 + SX127X_PIN_CS = machine.GP5 +) + +func main() { + time.Sleep(500 * time.Millisecond) + defer println("program end") + SX127X_PIN_RST.Configure(machine.PinConfig{Mode: machine.PinOutput}) + SX127X_PIN_CS.Configure(machine.PinConfig{Mode: machine.PinOutput}) + err := SX127X_SPI.Configure(machine.SPIConfig{ + Frequency: 100000, + SCK: SX127X_PIN_SCK, + SDO: SX127X_PIN_TX, + SDI: SX127X_PIN_RX, + }) + if err != nil { + panic(err.Error()) + } + dev := sx127x.NewLoRa(SX127X_SPI, SX127X_PIN_CS.Set, SX127X_PIN_RST.Set) + // Replace with your frequency: + err = dev.Configure(sx127x.DefaultConfig(lora.Freq433_0M)) + if err != nil { + panic(err.Error()) + } + println("config success; receiving messages") + var rx [256]byte + // This blocks forever reading messages received. + ctx := context.Background() + err = dev.RxContinuous(ctx, func(r io.Reader) (_ error) { + n, err := r.Read(rx[:]) + if err != nil { + return err + } + println("received LoRa:", string(rx[:n])) + return nil + }) + if err != nil { + panic(err) + } +} diff --git a/examples/transmitter/transmitter.go b/examples/transmitter/transmitter.go index 4bbe29b..af15247 100644 --- a/examples/transmitter/transmitter.go +++ b/examples/transmitter/transmitter.go @@ -40,16 +40,7 @@ func main() { panic(err.Error()) } dev := sx127x.NewLoRa(SX127X_SPI, SX127X_PIN_CS.Set, SX127X_PIN_RST.Set) - err = dev.Configure(lora.Config{ - Bandwidth: lora.BW125k, - Frequency: lora.Freq433_0M, - CodingRate: lora.CR4_5, - SpreadFactor: lora.SF7, - HeaderType: lora.HeaderExplicit, - TxPower: 0, - CRC: true, - PreambleLength: 12, - }) + err = dev.Configure(sx127x.DefaultConfig(lora.Freq433_0M)) if err != nil { panic(err.Error()) } @@ -57,7 +48,6 @@ func main() { randomU32, _ := dev.RandomU32() myName := fmt.Sprintf("user-%x", randomU32%0xffff) println("config success; sending messages as ", myName) - var rx [256]byte packet := []byte(myName + " says hello!") for { err = dev.Tx(packet) @@ -67,13 +57,5 @@ func main() { print(".") } time.Sleep(2 * time.Second) - for i := 0; i < 6; i++ { - n, err := dev.RxSingle(rx[:]) - if err != nil { - println("rx error:", err.Error()) - continue - } - println("got message: ", string(rx[:n])) - } } } diff --git a/lora.go b/lora.go index fb96e49..6254400 100644 --- a/lora.go +++ b/lora.go @@ -2,10 +2,15 @@ package lora import "time" +// Config is the generic configuration struct for a LoRa modem/radio that provides +// the most common configuration parameters needed to set up a channel with another +// radio using same parameters. type Config struct { // Bandwidth relates to data rate of communication. Double the bandwidth means // double the data rate, which implies faster communications and less energy usage. Bandwidth Frequency + // Frequency is the carrier frequency of the radio, a.k.a center frequency. + // It must match the modem's working frequency to ensure proper functioning. Frequency Frequency // Length of the preamble preceding the header. Can be arbitrarily long. // Longer preambles ensure more robust communications but can lead to congestion. @@ -14,11 +19,13 @@ type Config struct { // than the expected packet preamble length on the SX1278. PreambleLength uint16 HeaderType HeaderType - CodingRate CodingRate - SpreadFactor SpreadFactor - SyncWord uint8 - TxPower int8 // Tx Power in dBm. - CRC bool + // Must be set when working with implicit headers. + MaxImplicitPayloadLength uint8 + CodingRate CodingRate + SpreadFactor SpreadFactor + SyncWord uint8 + TxPower int8 // Tx Power in dBm. + CRC bool // Low data rate optimisation flag. The use of this flag is mandated when // the symbol duration exceeds 16ms. Increases reliability at high spreading factors. LDRO bool @@ -75,6 +82,10 @@ func (cfg *Config) TimeOnAir(payloadLength int) time.Duration { time.Duration(cfg.Bandwidth.Hertz()) } +// HeaderType defines the presence of a header in the LoRa packet. +// An explicit header means that the packet contains a header with a length +// field. An implicit header means that the packet does not contain a header +// and the length is implicitly known, i.e. agreed upon between two modems in advance. type HeaderType uint8 const ( @@ -82,6 +93,10 @@ const ( HeaderImplicit HeaderType = 1 ) +// CodingRate defines the error correction scheme used by the LoRa modem. +// Higher coding rates imply more robust communications at the expense of +// less data throughput. A CR of 4/5 means that for every 4 bits of data, +// 1 bit of error correction is added to the total transmitted bits. type CodingRate uint8 const ( @@ -95,8 +110,13 @@ const ( CR4_8 CodingRate = 4 ) +// SpreadFactor defines the number of chips per symbol. Higher spread factors +// imply longer transmission times but more robust communications. +// The number of chips per symbol is 2^SF, so a spread factor of 8 takes twice +// as long to transmit a symbol as a spread factor of 7. type SpreadFactor uint8 +// Common spread factors. Decide the number of chips per symbol. const ( SF6 SpreadFactor = iota + 6 SF7 @@ -111,6 +131,10 @@ func (sf SpreadFactor) ChipsPerSymbol() int64 { return 1 << sf } +// Frequency defines the center frequency of the LoRa channel. The center frequency +// serves to avoid interference from other channels and has minimal effect on +// the data rate. The frequency in Config must match the modem's working frequency +// or the modem may fail to transmit or receive packets or even be damaged. type Frequency int64 func (f Frequency) Hertz() int64 { return int64(f) } diff --git a/sx127x/registers.go b/sx127x/registers.go index d123c41..20e66e6 100644 --- a/sx127x/registers.go +++ b/sx127x/registers.go @@ -1,6 +1,11 @@ package sx127x -import "github.com/soypat/lora" +import ( + "bytes" + "fmt" + + "github.com/soypat/lora" +) const ( // 32MHz frequency for crystal oscillator is typical. @@ -9,6 +14,56 @@ const ( fSTEP = fXOSC / (1 << 19) ) +type irqFlagPos uint8 + +const ( + irqPosCADDetected irqFlagPos = iota + irqPosFHSSChange + irqPosCADDone + irqPosTxDone + irqPosValidHeader + irqPosPayloadCRC + irqPosRxDone + irqPosRxTimeout +) + +func (pos irqFlagPos) String() (str string) { + switch pos { + case irqPosCADDetected: + str = "CADDetected" + case irqPosFHSSChange: + str = "FHSSChange" + case irqPosCADDone: + str = "CADDone" + case irqPosTxDone: + str = "TxDone" + case irqPosValidHeader: + str = "ValidHeader" + case irqPosPayloadCRC: + str = "CRCError" + case irqPosRxDone: + str = "RxDone" + case irqPosRxTimeout: + str = "RxTimeout" + default: + str = "UnknownIRQ" + } + return str +} + +func irqFlagsString(irqFlags uint8) (str string) { + if irqFlags == 0 { + return "[]" + } + str = "[" + for i := irqFlagPos(0); i < 8; i++ { + if irqFlags&(1< 20: err = errors.New("bad tx power") } @@ -161,6 +190,9 @@ func (d *DeviceLoRa) Configure(cfg lora.Config) (err error) { if err != nil { return err } + if isImplicit { + d.setMaxPayloadLength(cfg.MaxImplicitPayloadLength) + } err = d.enableIQInversion(cfg.IQInversion) if err != nil { return err @@ -169,16 +201,25 @@ func (d *DeviceLoRa) Configure(cfg lora.Config) (err error) { if err != nil { return err } + err = d.enableLowDataRateOptimization(cfg.LDRO) + if err != nil { + return err + } err = d.SetSymbolTimeout(1023) // Set timeout to max value. if err != nil { return err } - const rxStart = 128 - d.write8(regFIFO_TX_BASE_ADDR, 0) - d.write8(regFIFO_RX_BASE_ADDR, rxStart) d.setHopPeriod(0) + // Go insto standby mode to write to FIFO related registers. + err = d.SetOpMode(OpStandby) + if err != nil { + return err + } + d.write8(regFIFO_TX_BASE_ADDR, 0) + d.write8(regFIFO_RX_BASE_ADDR, 0) + d.write8(regFIFO_ADDR_PTR, 0) d.headerType = cfg.HeaderType - return d.SetOpMode(OpStandby) + return d.SetOpMode(OpSleep) // Return to sleep mode to conserve power. } func (d *DeviceLoRa) Reset() { @@ -211,7 +252,7 @@ func (d *DeviceLoRa) SetOpMode(mode OpMode) error { } got, err := d.GetOpMode() if err != nil { - return err + return d.wrapErr(err) } if got != mode { return errors.New("tried to set opmode " + mode.String() + ", got " + got.String()) @@ -347,15 +388,21 @@ func (d *DeviceLoRa) RxSingle(dst []byte) (uint8, error) { // Loop until Rx received or timeout IRQ. var irq uint8 - for irq&(irqRXDONE_MASK|irqRXTOUT_MASK) == 0 { - runtime.Gosched() // Yield to scheduler. + for { irq, err = d.read8(regIRQ_FLAGS) if err != nil { return 0, err } + if irq&(irqRXDONE_MASK|irqRXTOUT_MASK) != 0 { + break + } + d.onNoPacket() } // Check for payload integrity and timeout interrupt. if irq&irqCRCERR_MASK != 0 { + if d.headerType == lora.HeaderImplicit { + return 0, errImplicitPacketTooLarge + } return 0, ErrCRC } else if irq&irqRXTOUT_MASK != 0 { return 0, ErrRxTimeout @@ -406,17 +453,34 @@ func (d *DeviceLoRa) RxContinuous(ctx context.Context, fn rxCallback) error { // Loop until Rx received or timeout IRQ. var irq uint8 + count := 0 + defer func() { + println("RxContinuous looped", count, "times") + }() for ctx.Err() == nil { - runtime.Gosched() // Yield to scheduler. irq, err = d.read8(regIRQ_FLAGS) if err != nil { return err } if irq&irqRXDONE_MASK != 0 { + nbBytes, _ := d.read8(regRX_NB_BYTES) + if nbBytes == 0 { + return fmt.Errorf("received packet with 0 bytes;irq=%08b; stat=%s", irq, d.statusString()) + } err = d.gotRxContinous(fn) if err != nil { return err } + } else { + d.onNoPacket() + } + count++ + op, err = d.GetOpMode() + if err != nil { + return err + } else if op != OpRx { + return errors.New("unexpected op mode change during RxContinuous to " + + op.String() + " with irqs:" + irqFlagsString(irq)) } } return ctx.Err() @@ -597,6 +661,9 @@ func (d *DeviceLoRa) ReadConfig() (cfg lora.Config, err error) { cfg.SyncWord = sync // Read Frequency. err = d.read(regFRF_MSB, buf[:3]) + if err != nil { + return cfg, err + } freq := uint64(buf[0])<<16 | uint64(buf[1])<<8 | uint64(buf[2]) cfg.Frequency = lora.Frequency((freq * 15625) >> 8) // Read IQ inversion. @@ -617,6 +684,13 @@ func (d *DeviceLoRa) setPreambleLength(pLen uint16) error { return d.write8(regPREAMBLE_LSB, buf[1]) } +func (d *DeviceLoRa) setMaxPayloadLength(plen uint8) error { + if plen == 0 { + return io.ErrShortBuffer + } + return d.write8(regPAYLOAD_LENGTH, plen) +} + func (d *DeviceLoRa) enableTxContinuousMode(enable bool) error { return d.writeMasked8(regMODEM_CONFIG_2, 1<<3, b2u8(enable)<<3) } @@ -729,7 +803,7 @@ func (d *DeviceLoRa) enableLowFrequencyMode(b bool) error { // enableCRC enables/disables CRC generation and checking. func (d *DeviceLoRa) enableCRC(b bool) error { - const crcMask = 1 << 2 + const crcMask = 0b11 << 2 return d.writeMasked8(regMODEM_CONFIG_2, crcMask, b2u8(b)<<2) } @@ -768,9 +842,14 @@ func (d *DeviceLoRa) writeMasked8(addr uint8, mask, value byte) error { if err != nil { return err } + existing &^= mask // remove mask bits from register value. existing |= mask & value // add value's bits as masked. - return d.write8(addr, existing) + err = d.write8(addr, existing) + if debugBufSize > 0 { + d.debugMask[addr] = mask + } + return err } func (d *DeviceLoRa) read8(addr byte) (byte, error) { @@ -779,6 +858,9 @@ func (d *DeviceLoRa) read8(addr byte) (byte, error) { d.csEnable(true) err := d.bus.Tx(writeBuf, readBuf) d.csEnable(false) + if debugBufSize > 0 { + d.debugRead[addr] = readBuf[1] + } return readBuf[1], err } @@ -788,6 +870,11 @@ func (d *DeviceLoRa) write8(addr, value byte) error { d.csEnable(true) err := d.bus.Tx(writeBuf, readBuf) d.csEnable(false) + if debugBufSize > 0 { + d.debugWrite[addr] = value + d.debugMask[addr] = 0xFF + d.read8(addr) // refresh address value. + } return err } @@ -834,7 +921,7 @@ func (d *DeviceLoRa) readerToLastPacket() (_ *fifoReader, err error) { runtime.Gosched() } if irqFlags&mustBeUnset != 0 { - return nil, errors.New("timeout waiting for IRQ before reading packet") + return nil, errors.New("timeout waiting for IRQ before reading packet:" + irqFlagsString(irqFlags)) } // We now know that the packet has been received succesfully. Proceed to read. @@ -846,6 +933,9 @@ func (d *DeviceLoRa) readerToLastPacket() (_ *fifoReader, err error) { if err != nil { return nil, err } + if numBytes == 0 { + return nil, errors.New("readerToNextPacket called with RX_NB_BYTES=0") + } err = d.write8(regFIFO_ADDR_PTR, curraddr) if err != nil { return nil, err