diff --git a/.gitignore b/.gitignore index 3507f01..1e58d2f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,5 +15,5 @@ # vendor/ *.c c/* - +*.log .vscode \ No newline at end of file diff --git a/examples/receiver/receiver.go b/examples/receiver/receiver.go index 72be08a..208b56d 100644 --- a/examples/receiver/receiver.go +++ b/examples/receiver/receiver.go @@ -51,6 +51,8 @@ func main() { // This blocks forever reading messages received. ctx := context.Background() for { + value, err := dev.ReadTemperature() + println("temperature:", value, err) time.Sleep(10 * time.Second) err = dev.RxContinuous(ctx, func(r io.Reader) (_ error) { n, err := r.Read(rx[:]) @@ -60,6 +62,7 @@ func main() { println("received LoRa:", string(rx[:n])) return nil }) + if err != nil { println(err.Error()) } else { diff --git a/lora_test.go b/lora_test.go index 8be02ec..2d3161a 100644 --- a/lora_test.go +++ b/lora_test.go @@ -58,3 +58,24 @@ func TestTimeOnAir(t *testing.T) { }) } } + +func TestTemperature(t *testing.T) { + for _, cs := range []struct { + tempVal uint8 + expect uint8 + celsius int + }{ + {tempVal: 63, expect: 0, celsius: -40}, + {tempVal: 33, expect: 36, celsius: -15}, + {tempVal: 0, expect: 100, celsius: 15}, + {tempVal: 245, expect: 128, celsius: 10}, + {tempVal: 211, expect: 220, celsius: 55}, + {tempVal: 181, expect: 255, celsius: 85}, + } { + val := cs.tempVal + val -= 64 + val = uint8(255 - 14*uint16(val)/8) + // val += 128 + t.Errorf("regTemp=%d val=%d expect=%d celsius=%d", cs.tempVal, val, cs.expect, cs.celsius) + } +} diff --git a/sx127x/registers.go b/sx127x/registers.go index 20e66e6..de5bd59 100644 --- a/sx127x/registers.go +++ b/sx127x/registers.go @@ -156,6 +156,17 @@ const ( privateSyncword = 0x14 ) +// FSK/OOK registers. +const ( + regImageCal = 0x3b + regTemp = 0x3c + + // Values that would be read from regTemp: + lowTempVal uint8 = 64 // -40 C + medTempVal uint8 = 245 // +25 C + hiTempVal uint8 = 181 // +85 C +) + var bandwidths = [bw500_0 + 1]lora.Frequency{ bw7_8: 7.8e3 * lora.Hertz, bw10_4: 10.4e3 * lora.Hertz, diff --git a/sx127x/sx127x.go b/sx127x/sx127x.go index e8a97da..e5b094f 100644 --- a/sx127x/sx127x.go +++ b/sx127x/sx127x.go @@ -89,7 +89,7 @@ func DefaultConfig(freq lora.Frequency) lora.Config { MaxImplicitPayloadLength: 0, // No need to be set when working with explicit headers. CRC: true, SyncWord: publicSyncword, - TxPower: 0, + TxPower: 0, // Low power by default. LDRO: false, IQInversion: false, } @@ -107,7 +107,8 @@ var ( errSF6Implicit = errors.New("SF6 can only be used with implicit header type") // Page 30: Implicit Header Mode. errPreambleTooShort = errors.New("preamble length too short") ErrNotDetected = errors.New("sx127x not detected") - errBadMode = errors.New("bad mode: sx127x in FSK/OOK mode, not LoRa or viceversa") + errBadMode = errors.New("bad op mode: sx127x in FSK/OOK mode, not LoRa or viceversa") + errSharedMode = errors.New("bad op mode: sx127x in shared access mode") errBadCodingRate = errors.New("bad coding rate") errUnsupportedBandwidth = errors.New("bandwidth too high for frequency around 169MHz") errIRQNotCleared = errors.New("IRQs not cleared") @@ -133,8 +134,8 @@ func (d *DeviceLoRa) Configure(cfg lora.Config) (err error) { err = errors.New("bad header type") case cfg.HeaderType == lora.HeaderImplicit && cfg.MaxImplicitPayloadLength == 0: err = errors.New("MaxImplicitPayloadLength parameter must be set when working with implicit headers") - case cfg.TxPower > 20: - err = errors.New("bad tx power") + case cfg.TxPower > 20 || cfg.TxPower < -4: + err = errors.New("tx power not in operating range -4..20") } if err != nil { return err @@ -268,8 +269,13 @@ func (d *DeviceLoRa) GetOpMode() (OpMode, error) { if err != nil { return invalidOpMode, err } - if got&byte(opLoRaBit) == 0 || // LongRangeMode bit influences operation. - got&(1<<6) != 0 { // AccessSharedReg bit allows access to FSK registers in LoRa mode, should not be set. + // We expect the LoRa bit to be set, access shared reg bit to be unset. + const forbiddenBits = opLoRaBit | (1 << 6) + if got&byte(forbiddenBits) != 0b10<<6 { + // d.write8(regOP_MODE, (got&opmMASK)|byte(opLoRaBit))// We force set a "correct" mode before returning the error. + if got&(1<<6) != 0 { + return invalidOpMode, errSharedMode // AccessSharedReg bit allows access to FSK registers in LoRa mode, should not be set. + } // if the LoRa mode bit is not set, which would mean the device is in // FSK/OOK mode or disconnected. return invalidOpMode, errBadMode @@ -695,40 +701,19 @@ func (d *DeviceLoRa) enableTxContinuousMode(enable bool) error { return d.writeMasked8(regMODEM_CONFIG_2, 1<<3, b2u8(enable)<<3) } -// 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)) -} +// PA_HF and PA_LF are high efficiency amplifiers capable of yielding RF power programmable in 1 dB steps from -4 to +// +14dBm directly into a 50 ohm load with low current consumption. PA_LF covers the lower bands (up to 525 MHz), whilst +// PA_HF will cover the upper bands (from 779 MHz). The output power is sensitive to the power supply voltage, and typically +// their performance is expressed at 3.3V. // 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 - } + MaxPower, OutputPower := rfoPowReg(txPow) // 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))) + err := d.write8(regPA_CONFIG, ((0b111&MaxPower)<<4)|(OutputPower&0b1111)) if err != nil { return err } @@ -737,6 +722,56 @@ func (d *DeviceLoRa) setTxPower(txPow int8) error { // return d.write8(regOCP, 0) // TODO: Disable OCP? } +func rfoPowReg(txPow int8) (MaxPower, OutputPower uint8) { + // To get the desired power level we must balance two register values, MaxPower and OutputPower. + // The main equation is: + // Pout = Pmax - (15 - OutputPower) => OutputPower = 15 - (Pmax - Pout) + // Where Pmax=10.8+0.6*MaxPower [dBm] + // To solve this combined equation we set MaxPower depending on txPow + // and solve from there on. The result is error margin +/- 0.3dBm. See tests. + switch { + case txPow < 2: + MaxPower = 0b000 + case txPow < 10: + MaxPower = 0b010 + case txPow < 14: + MaxPower = 0b100 + default: + MaxPower = 0b111 + } + Pmax := 11 + MaxPower*6/10 + OutputPower = 15 - (Pmax - uint8(txPow)) + return MaxPower, OutputPower +} + +func paBoostPowReg(txPow int8) (OutputPower uint8) { + OutputPower = 15 - (17 - uint8(txPow)) + return OutputPower +} + +// 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 + return d.write8(regOCP, ocpEnabledMask|(0x1F&ocpTrim(mA))) +} + +func ocpTrim(imax uint8) uint8 { + if imax < 45 { + imax = 45 // Absolute minimum is 45mA. + } + var ocpTrim uint8 + switch { + case imax <= 120: // Imax [mA] = 45 +5*OcpTrim + ocpTrim = (imax - 45) / 5 + case imax <= 240: // Imax [mA] = -30 + 10*OcpTrim + ocpTrim = uint8((uint16(imax) + 30) / 10) + default: // Imax = 240mA + ocpTrim = 27 + } + return ocpTrim +} + // setFrequency sets the center radio frequency. func (d *DeviceLoRa) setFrequency(freq lora.Frequency) error { var freqReg [3]byte @@ -904,6 +939,32 @@ func (d *DeviceLoRa) read(addr uint8, buf []byte) error { return err } +// ReadTemperature returns the temperature of the chip in degrees Celsius. +func (d *DeviceLoRa) ReadTemperature() (celsius int8, err error) { + // We do things dirtily in this function since we are + // switching to FSK/OOK mode and back to LoRa mode. + defer func() { + // Put chip in sleep to let us switch back to LoRa mode. + d.write8(regOP_MODE, opmSLEEP) + time.Sleep(15 * time.Millisecond) + err = d.SetOpMode(OpSleep) // Back to LoRa mode. Only error that matters. + }() + // Sequence taken from page 89. + d.write8(regOP_MODE, opmSLEEP) // Put chip in sleep to let us switch to FSK/OOK mode. + time.Sleep(15 * time.Millisecond) + d.write8(regOP_MODE, opmSTANDBY) + time.Sleep(1000 * time.Microsecond) // Wait for oscillator startup. + d.write8(regOP_MODE, opmFSRX) + d.writeMasked8(regImageCal, 1, 0) // Set TempMonitorOff = 0 (enables the sensor). It is not required to wait for the PLL Lock indication + time.Sleep(140 * time.Microsecond) + d.writeMasked8(regImageCal, 1, 1) // Set TempMonitorOff = 1 (disables the sensor). + d.write8(regOP_MODE, opmSLEEP) + value, _ := d.read8(regTemp) + // Following calculation maps + value = 255 - 2*(value-64) + return int8(value), err +} + func (d *DeviceLoRa) readerToLastPacket() (_ *fifoReader, err error) { // IRQ check according to page 41 of the datasheet. // Flags must not be asserted in order to ensure packet reception has terminated succesfully. diff --git a/sx127x/sx127x_test.go b/sx127x/sx127x_test.go new file mode 100644 index 0000000..63d12bb --- /dev/null +++ b/sx127x/sx127x_test.go @@ -0,0 +1,51 @@ +package sx127x + +import ( + "math" + "testing" +) + +func TestRFOPowReg(t *testing.T) { + for txPow := int8(-4); txPow < 17; txPow++ { + maxPow, outPow := rfoPowReg(txPow) + Pmax := 10.8 + 0.6*float64(maxPow) + Pout := Pmax - (15 - float64(outPow)) + diff := math.Abs(Pout - float64(txPow)) + if diff > .3 { + t.Errorf("wanted %d, got %f", txPow, Pout) + } + } +} + +func TestPAPowReg(t *testing.T) { + for txPow := int8(2); txPow < 20; txPow++ { + outPow := paBoostPowReg(txPow) + Pout := 17.0 - (15 - float64(outPow)) + diff := math.Abs(Pout - float64(txPow)) + if diff > .01 { + t.Errorf("wanted %d, got %f", txPow, Pout) + } + } +} + +func TestOCPTrim(t *testing.T) { + for imax := uint8(45); imax < 241; imax++ { + var gotImax float64 + trim := ocpTrim(imax) + switch { + case trim <= 15: + gotImax = 45.0 + 5.0*float64(trim) + case trim <= 27: + gotImax = -30.0 + 10.0*float64(trim) + default: + gotImax = 240.0 + } + if trim > 27 { + t.Error("trim > 27") + } + diff := math.Abs(gotImax - float64(imax)) + if diff > 11 { + t.Errorf("wanted %d, got %f", imax, gotImax) + } + } +}