diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3507f01 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ +*.c +c/* + +.vscode \ No newline at end of file diff --git a/examples/transmitter/transmitter.go b/examples/transmitter/transmitter.go new file mode 100644 index 0000000..500bdc9 --- /dev/null +++ b/examples/transmitter/transmitter.go @@ -0,0 +1,54 @@ +package main + +import ( + "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) + 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, + }) + if err != nil { + panic(err.Error()) + } + println("config success") +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..b1dae3a --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/soypat/lora + +go 1.19 diff --git a/lora.go b/lora.go new file mode 100644 index 0000000..7f54dcf --- /dev/null +++ b/lora.go @@ -0,0 +1,130 @@ +package lora + +import "time" + +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 Frequency + // Length of the preamble preceding the header. Can be arbitrarily long. + // Longer preambles ensure more robust communications but can lead to congestion. + // + // Note: The preamble length on a receiver should be configured equal-to or greater + // 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 + // 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 + IQMode uint8 +} + +func (cfg *Config) TimeOnAir(payloadLength int) time.Duration { + if cfg.Bandwidth == 0 { + return 0 + } + crc := int64(b2u8(cfg.CRC)) + ih := int64(cfg.HeaderType) + ldr := int64(b2u8(cfg.LDRO)) + cr := int64(cfg.CodingRate) + spread := int64(cfg.SpreadFactor) + // Page 31 SX1276IMLTRT SEMTECH | Alldatasheet. + Npayload := 8*int64(payloadLength) - 4*spread + 28 + 16*crc - 20*ih + div := 4 * (spread - 2*ldr) + // Apply Ceil and max with minimal branching. + if Npayload < 0 || div <= 0 { + Npayload = 0 + } else if Npayload%div == 0 { + Npayload /= div + Npayload *= (cr + 4) + } else { + Npayload /= div + Npayload++ + Npayload *= (cr + 4) + } + Npayload += 8 + int64(cfg.PreambleLength) + 5 // Says 4.25 in manual but we round up. + // Calculate LoRa Transmission Parameter Relationship page 28. + Ts_us := 1000_000 * cfg.SpreadFactor.ChipsPerSymbol() / cfg.Bandwidth.Hertz() + return time.Microsecond * (time.Duration(Npayload * Ts_us)) +} + +type HeaderType uint8 + +const ( + HeaderExplicit HeaderType = 0 + HeaderImplicit HeaderType = 1 +) + +type CodingRate uint8 + +const ( + CR4_5 CodingRate = 1 + CR4_6 CodingRate = 2 + CR4_7 CodingRate = 3 + CR4_8 CodingRate = 4 +) + +type SpreadFactor uint8 + +const ( + SF6 SpreadFactor = iota + 6 + SF7 + SF8 + SF9 + SF10 + SF11 + SF12 +) + +func (sf SpreadFactor) ChipsPerSymbol() int64 { + return 1 << sf +} + +type Frequency int64 + +func (f Frequency) Hertz() int64 { return int64(f) } + +const ( + Hertz Frequency = 1 + KiloHertz Frequency = 1000 * Hertz + MegaHertz Frequency = 1000 * KiloHertz +) + +// Common LoRa bandwidths +const ( + BW125k = 125 * KiloHertz + BW250k = 250 * KiloHertz + BW500k = 500 * KiloHertz +) + +// Common LoRa frequencies +const ( + // 433.05MHz Low limit medical, scientific and industrial band. + Freq433_0M = 433050000 * Hertz + // 434.8MHz High limit medical, scientific and industrial band. + Freq434_8M = 434790000 * Hertz + Freq868_1M = 868100000 * Hertz + Freq868_5M = 868500000 * Hertz + Freq916_8M = 916800000 * Hertz + Freq923_3M = 923300000 * Hertz +) + +func max[T ~int | ~int64 | ~uint8](a, b T) T { + if a > b { + return a + } + return b +} + +func b2u8(b bool) uint8 { + if b { + return 1 + } + return 0 +} diff --git a/lora_test.go b/lora_test.go new file mode 100644 index 0000000..8be02ec --- /dev/null +++ b/lora_test.go @@ -0,0 +1,60 @@ +package lora_test + +import ( + "math" + "testing" + "time" + + "github.com/soypat/lora" +) + +func TestTimeOnAir(t *testing.T) { + const ( + loraWANCodeRate = lora.CR4_5 + loraWANPreambleLength = 12 + ) + testCases := []struct { + desc string + expected time.Duration + plen int + cfg lora.Config + }{ + { + desc: "LoRaWAN 240bytes", // https://www.thethingsnetwork.org/airtime-calculator + cfg: lora.Config{ + Bandwidth: lora.BW125k, + Frequency: 915e6, + SpreadFactor: lora.SF7, + HeaderType: lora.HeaderExplicit, + CodingRate: loraWANCodeRate, + CRC: true, + PreambleLength: loraWANPreambleLength, + }, + plen: 240, + expected: 394.5e3 * time.Microsecond, + }, + { + desc: "LoRaWAN 5byte BW=500kHz", // https://www.thethingsnetwork.org/airtime-calculator + cfg: lora.Config{ + Bandwidth: lora.BW500k, + Frequency: 915e6, + SpreadFactor: lora.SF7, + HeaderType: lora.HeaderExplicit, + CodingRate: loraWANCodeRate, + CRC: true, + PreambleLength: loraWANPreambleLength, + }, + plen: 10, + expected: 15.4e3 * time.Microsecond, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + got := tC.cfg.TimeOnAir(tC.plen) + expect := tC.expected.Seconds() + if math.Abs(got.Seconds()-expect) > 10*expect/100 { // TODO: improve accuracy of TimeOnAir + t.Errorf("%s: got %s, expected %s", tC.desc, got.String(), tC.expected.String()) + } + }) + } +} diff --git a/sx127x/registers.go b/sx127x/registers.go new file mode 100644 index 0000000..b72495c --- /dev/null +++ b/sx127x/registers.go @@ -0,0 +1,190 @@ +package sx127x + +import "github.com/soypat/lora" + +const ( + // 32MHz frequency for crystal oscillator is typical. + fXOSC = 32_000_000 + // PLL uses a 19-bit sigma-delta modulator whose frequency resolution, constant over the whole frequency range, is given by + fSTEP = fXOSC / (1 << 19) +) + +const ( + // registers + regFIFO = 0x00 + regOP_MODE = 0x01 + regFRF_MSB = 0x06 + regFRF_MID = 0x07 + regFRF_LSB = 0x08 + regPA_CONFIG = 0x09 + regPA_RAMP = 0x0a + regOCP = 0x0b + regLNA = 0x0c + regFIFO_ADDR_PTR = 0x0d + regFIFO_TX_BASE_ADDR = 0x0e + regFIFO_RX_BASE_ADDR = 0x0f + regFIFO_RX_CURRENT_ADDR = 0x10 + regIRQ_FLAGS_MASK = 0x11 + regIRQ_FLAGS = 0x12 + regRX_NB_BYTES = 0x13 + regPKT_SNR_VALUE = 0x19 + regPKT_RSSI_VALUE = 0x1a + regRSSI_VALUE = 0x1b + regMODEM_CONFIG_1 = 0x1d + regMODEM_CONFIG_2 = 0x1e + regSYMB_TIMEOUT_LSB = 0x1f + regPREAMBLE_MSB = 0x20 + regPREAMBLE_LSB = 0x21 + regPAYLOAD_LENGTH = 0x22 + regMAX_PAYLOAD_LENGTH = 0x23 + regHOP_PERIOD = 0x24 + regMODEM_CONFIG_3 = 0x26 + regFREQ_ERROR_MSB = 0x28 + regFREQ_ERROR_MID = 0x29 + regFREQ_ERROR_LSB = 0x2a + regRSSI_WIDEBAND = 0x2c + regDETECTION_OPTIMIZE = 0x31 + regINVERTIQ = 0x33 + regDETECTION_THRESHOLD = 0x37 + regSYNC_WORD = 0x39 + regINVERTIQ2 = 0x3b + regDIO_MAPPING_1 = 0x40 + regDIO_MAPPING_2 = 0x41 + regVERSION = 0x42 + regPA_DAC = 0x4d + // PA config + paBOOST = 0x80 + + expectedVersion = 0x12 + + // Bits masking the corresponding IRQs from the radio + irqRXTOUT_MASK uint8 = 0x80 + irqRXDONE_MASK uint8 = 0x40 + irqCRCERR_MASK uint8 = 0x20 + irqHEADER_MASK uint8 = 0x10 + irqTXDONE_MASK uint8 = 0x08 + irqCDDONE_MASK uint8 = 0x04 + irqFHSSCH_MASK uint8 = 0x02 + irqCDDETD_MASK uint8 = 0x01 + + // DIO function mappings D0D1D2D3 + mapDIO0_LORA_RXDONE uint8 = 0x00 // 00------ + mapDIO0_LORA_TXDONE uint8 = 0x40 // 01------ + mapDIO1_LORA_RXTOUT uint8 = 0x00 // --00---- + mapDIO1_LORA_NOP uint8 = 0x30 // --11---- + mapDIO2_LORA_NOP uint8 = 0xC0 // ----11-- + + // SX127X_PAYLOAD_LENGTH uint8 = 0x40 + + // Low Noise Amp + lnaMAX_GAIN uint8 = 0x23 + lnaOFF_GAIN uint8 = 0x00 + lnaLOW_GAIN uint8 = 0x20 + + // Bandwidth + bw7_8 uint8 = 0x00 + bw10_4 uint8 = 0x01 + bw15_6 uint8 = 0x02 + bw20_8 uint8 = 0x03 + bw31_25 uint8 = 0x04 + bw41_7 uint8 = 0x05 + bw62_5 uint8 = 0x06 + bw125_0 uint8 = 0x07 + bw250_0 uint8 = 0x08 + bw500_0 uint8 = 0x09 + // Automatic gain control + agcAUTO_OFF uint8 = 0x00 + agcAUTO_ON uint8 = 0x01 + + publicSyncword = 0x34 + privateSyncword = 0x14 +) + +func bwReg(bandwidth lora.Frequency) (value byte) { + switch { + case bandwidth <= 7.8e3*lora.Hertz: + value = bw7_8 + case bandwidth <= 10.4e3*lora.Hertz: + value = bw10_4 + case bandwidth <= 15.6e3*lora.Hertz: + value = bw15_6 + case bandwidth <= 20.8e3*lora.Hertz: + value = bw20_8 + case bandwidth <= 31.25e3*lora.Hertz: + value = bw31_25 + case bandwidth <= 41.7e3*lora.Hertz: + value = bw41_7 + case bandwidth <= 62.5e3*lora.Hertz: + value = bw62_5 + case bandwidth <= 125*lora.KiloHertz: + value = bw125_0 + case bandwidth <= 250e3*lora.KiloHertz: + value = bw250_0 + default: + value = bw500_0 + } + return value +} + +// Operation modes. +const ( + opmMASK uint8 = 0x07 + opmSLEEP uint8 = 0x00 + opmSTANDBY uint8 = 0x01 + opmFSTX uint8 = 0x02 + opmTX uint8 = 0x03 + opmFSRX uint8 = 0x04 + opmRX uint8 = 0x05 + opmRX_SINGLE uint8 = 0x06 + opmCAD uint8 = 0x07 + opmLORA uint8 = 0x80 +) + +// OpMode represents the available operation modes of the SX127x devices. +type OpMode uint8 + +// LoRa Operation modes. +const ( + // Sleep mode will sleep goroutine for 15ms on SetOpmode call. + OpSleep OpMode = iota + // Standby mode is the default mode when waiting to operate. + OpStandby + // Frequency synthesis TX (FSTX) + OpFSTx + // Transmit (TX) + OpTx + // Frequency synthesis RX (FSRX) + OpFSRx + // Receive continuous (RXCONTINUOUS) + OpRx + // receive single (RXSINGLE) + OpRxSingle + // Channel activity detection (CAD) + OpCAD + opLoRaBit = OpMode(1 << 7) +) + +func (op OpMode) String() (s string) { + op = op &^ opLoRaBit + switch op { + case OpSleep: + s = "sleep" + case OpStandby: + s = "standby" + case OpFSTx: + s = "fstx" + case OpTx: + s = "tx" + case OpFSRx: + s = "fsrx" + case OpRx: + s = "rx" + case OpRxSingle: + s = "rx-single" + case OpCAD: + s = "cad" + default: + s = "unknown" + } + return s +} diff --git a/sx127x/sx127x.go b/sx127x/sx127x.go new file mode 100644 index 0000000..6d5b259 --- /dev/null +++ b/sx127x/sx127x.go @@ -0,0 +1,213 @@ +package sx127x + +import ( + "encoding/binary" + "errors" + "time" + + "github.com/soypat/lora" +) + +// TODO: page 82: To minimize the current consumption of the SX1276/77/78/79, please ensure that the CLKOUT signal is disabled when not required. + +type PinOutput func(level bool) + +type SPI interface { + Tx(writeBuffer, readBuffer []byte) error +} + +type DeviceLoRa struct { + rst PinOutput + cs PinOutput + bus SPI +} + +func NewLoRa(bus SPI, cs, reset PinOutput) *DeviceLoRa { + d := DeviceLoRa{bus: bus, cs: cs, rst: reset} + return &d +} + +func (d *DeviceLoRa) Configure(cfg lora.Config) error { + switch { + case cfg.SpreadFactor < lora.SF6 || cfg.SpreadFactor > lora.SF12: + return errors.New("bad spread factor") + case cfg.SpreadFactor == lora.SF6 && cfg.HeaderType != lora.HeaderImplicit: + return errors.New("SF6 can only be used with implicit header type") // Page 30: Implicit Header Mode. + case cfg.PreambleLength < 6: + return errors.New("preamble length too short") + } + d.Reset() + // We need to be in sleep mode to set LoRa mode if in FSK/OOK. + d.write8(regOP_MODE, opmSLEEP) // No need to check error, do it in SetOpmode. + if !d.IsConnected() { + return errors.New("sx127x not detected") + } + err := d.SetOpMode(OpSleep) + if err != nil { + return err + } + err = d.setTxPower(cfg.TxPower) + if err != nil { + return err + } + return nil +} + +func (d *DeviceLoRa) SetOpMode(mode OpMode) error { + // We always write the LoRa mode bit + err := d.write8(regOP_MODE, byte(mode|opLoRaBit)) + if err != nil { + return err + } + if mode == OpSleep { + time.Sleep(15 * time.Millisecond) // TODO: do we need this sleep? + } + got, err := d.GetOpMode() + if err != nil { + return err + } + if got != mode { + return errors.New("tried to set opmode " + mode.String() + ", got " + got.String()) + } + return nil +} + +func (d *DeviceLoRa) GetOpMode() (OpMode, error) { + const invalidOpMode = 0xff + got, err := d.read8(regOP_MODE) + if err != nil { + return invalidOpMode, err + } + if got&byte(opLoRaBit) == 0 { + // if the LoRa mode bit is not set, which would mean the device is in + // FSK/OOK mode or disconnected. + return invalidOpMode, errors.New("device in FSK/OOK mode") + } + return OpMode(got & opmMASK), nil +} + +// setPreambleLength defines number of preamble +func (d *DeviceLoRa) setPreambleLength(pLen uint16) error { + var buf [2]byte + binary.BigEndian.PutUint16(buf[:], pLen) + d.write8(regPREAMBLE_MSB, buf[0]) + return d.write8(regPREAMBLE_LSB, buf[1]) +} + +// SetCrc Enable CRC generation and check on payload. +func (d *DeviceLoRa) enableCRC(enable bool) error { + return d.writeMasked8(regMODEM_CONFIG_2, 1<<2, b2u8(enable)<<2) +} + +// setOCP defines Overload Current Protection configuration. It receives +// the max current (Imax) in milliamperes. +func (d *DeviceLoRa) setOCP(mA uint8) error { + const ocpEnabledMask = 1 << 5 + if mA < 45 { + mA = 45 // Absolute minimum is 45mA. + } + var ocpTrim uint8 + switch { + case mA <= 120: // Imax [mA] = 45 +5*OcpTrim + ocpTrim = (mA - 45) / 5 + case mA <= 240: // Imax [mA] = -30 + 10*OcpTrim + ocpTrim = (mA + 30) / 10 + default: // Imax = 240mA + ocpTrim = 27 + } + return d.write8(regOCP, ocpEnabledMask|(0x1F&ocpTrim)) +} + +// setTxPower sets the transmit power without using te PA_BOOST. +func (d *DeviceLoRa) setTxPower(txPow int8) error { + if txPow >= 16 { + return errors.New("requested tx power exceeds capabilities without PA_BOOST") + } + // Pout=Pmax-(15-OutputPower) + // Pmax=10.8+0.6*MaxPower [dBm] + const Pmax = 0b111 // Use Pmax ceiling. + const PoutMask = 0b1111 + Pout := Pmax - (15 - txPow) + if Pout < 0 { + Pout = 0 + } + // This unsets PaSelect bit which switches mode of operation to RFO pin (limited to 14dBm power). + err := d.write8(regPA_CONFIG, (Pmax<<4)|(PoutMask&uint8(Pout))) + if err != nil { + return err + } + return d.write8(regOCP, 0) // TODO: Disable OCP? +} + +// setFrequency sets the radio frequency. +func (d *DeviceLoRa) setFrequency(freq uint32) error { + var freqReg [3]byte + frf := freq / fSTEP // Page 82, 5.3.3 PLL. + freqReg[0] = byte(frf >> 16) + freqReg[1] = byte(frf >> 8) + freqReg[2] = byte(frf >> 0) + d.write8(regFRF_MSB, freqReg[0]) + d.write8(regFRF_MID, freqReg[1]) + // Note pg82: A change in the center frequency will only be taken into account when the + // least significant byte FrfLsb in RegFrfLsb is written. + return d.write8(regFRF_LSB, freqReg[2]) // Write LSB last! +} + +func (d *DeviceLoRa) writeMasked8(addr uint8, mask, value byte) error { + if value != 0 && value&^mask != 0 { + panic("misuse of writeMasked8") // Bug in this package if hit. + } + existing, err := d.read8(addr) + 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) +} + +func (d *DeviceLoRa) read8(addr byte) (byte, error) { + readBuf := make([]byte, 2) + writeBuf := []byte{addr &^ (1 << 7), 0} // unset write bit. Should be pretty much useless though we play it safe. + d.csEnable(true) + err := d.bus.Tx(writeBuf, readBuf) + d.csEnable(false) + return readBuf[1], err +} + +func (d *DeviceLoRa) write8(addr, value byte) error { + readBuf := make([]byte, 2) + writeBuf := []byte{addr | (1 << 7), value} // set write bit. + d.csEnable(true) + err := d.bus.Tx(writeBuf, readBuf) + d.csEnable(false) + return err +} + +func (d *DeviceLoRa) csEnable(b bool) { + d.cs(!b) +} + +func (d *DeviceLoRa) Reset() { + d.rst(true) + time.Sleep(200 * time.Millisecond) + d.rst(false) + time.Sleep(200 * time.Millisecond) + d.rst(true) + time.Sleep(200 * time.Millisecond) +} + +func (d *DeviceLoRa) IsConnected() bool { + version, err := d.read8(regVERSION) + if version == expectedVersion && err == nil { + return true + } + return false +} + +func b2u8(b bool) uint8 { + if b { + return 1 + } + return 0 +}