From 452669bb7f3efd4182198b5433343dec9f039f83 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sun, 19 Apr 2026 11:53:36 -0500 Subject: [PATCH] basic working sx126x skeleton --- lilygo-t3s3-sx126x/go.mod | 9 ++ lilygo-t3s3-sx126x/go.sum | 2 + lilygo-t3s3-sx126x/main.go | 287 +++++++++++++++++++++++++++++++++++++ 3 files changed, 298 insertions(+) create mode 100644 lilygo-t3s3-sx126x/go.mod create mode 100644 lilygo-t3s3-sx126x/go.sum create mode 100644 lilygo-t3s3-sx126x/main.go diff --git a/lilygo-t3s3-sx126x/go.mod b/lilygo-t3s3-sx126x/go.mod new file mode 100644 index 0000000..ec8208e --- /dev/null +++ b/lilygo-t3s3-sx126x/go.mod @@ -0,0 +1,9 @@ +module lilygot3s3sx128x + +go 1.26.2 + +require tinygo.org/x/drivers v0.34.0 + +require github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + +replace tinygo.org/x/drivers => /Users/jwetzell/Projects/drivers diff --git a/lilygo-t3s3-sx126x/go.sum b/lilygo-t3s3-sx126x/go.sum new file mode 100644 index 0000000..0be9157 --- /dev/null +++ b/lilygo-t3s3-sx126x/go.sum @@ -0,0 +1,2 @@ +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= diff --git a/lilygo-t3s3-sx126x/main.go b/lilygo-t3s3-sx126x/main.go new file mode 100644 index 0000000..4c89b9e --- /dev/null +++ b/lilygo-t3s3-sx126x/main.go @@ -0,0 +1,287 @@ +package main + +import ( + "machine" + "sync/atomic" + "time" + + "tinygo.org/x/drivers/lora" + "tinygo.org/x/drivers/sx126x" +) + +func main() { + time.Sleep(3 * time.Second) + led := machine.GPIO37 + led.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + spi := machine.SPI0 + spi.Configure(machine.SPIConfig{ + Mode: 0, + Frequency: 8 * 1e6, + SDO: machine.GPIO6, + SDI: machine.GPIO3, + SCK: machine.GPIO5, + }) + + nssPin := machine.GPIO7 + nssPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + nssPin.Set(true) + + resetPin := machine.GPIO8 + resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + resetPin.Set(true) + + busyPin := machine.GPIO34 + busyPin.Configure(machine.PinConfig{Mode: machine.PinInput}) + + button := machine.GPIO0 + button.Configure(machine.PinConfig{Mode: machine.PinInput}) + button.SetInterrupt(machine.PinFalling, func(machine.Pin) { + println("button interrupt") + }) + + println("initializing radio...") + + radio := sx126x.New( + spi, + nssPin, + resetPin, + busyPin, + ) + + dio1 := machine.GPIO33 + var txDone atomic.Bool + txDone.Store(true) + var rxDone atomic.Bool + rxDone.Store(true) + var timeout atomic.Bool + timeout.Store(true) + + dio1.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) + dio1.SetInterrupt(machine.PinRising, func(machine.Pin) { + irqStatus := radio.GetIrqStatus() + println("irq status:", irqStatus) + + if irqStatus&sx126x.IRQ_TX_DONE_MASK != 0 { + println("tx done") + txDone.Store(true) + timeout.Store(false) + led.Set(false) + } + if irqStatus&sx126x.IRQ_RX_DONE_MASK != 0 { + println("rx done") + rxDone.Store(true) + timeout.Store(false) + led.Set(true) + } + if irqStatus&sx126x.IRQ_TIMEOUT_MASK != 0 { + println("timeout") + txDone.Store(true) + rxDone.Store(true) + timeout.Store(true) + } + }) + + loraConfig := lora.Config{ + Freq: lora.MHZ_915_0, + Bw: lora.Bandwidth_125_0, + Sf: lora.SpreadingFactor9, + Cr: lora.CodingRate4_7, + HeaderType: lora.HeaderExplicit, + Preamble: 8, + Ldr: lora.LowDataRateOptimizeOff, + Iq: lora.IQStandard, + Crc: lora.CRCOff, + SyncWord: 0x1424, + LoraTxPowerDBm: 2, + } + + radio.WaitWhileBusy() + SetupLora(radio, loraConfig) + + for { + // if txDone.Load() { + // txDone.Store(false) + // println("cleaning up prior to transmit...") + // radio.ClearIrqStatus(sx126x.IRQ_ALL_MASK) + // checkStatus(radio, "ClearIrqStatus") + // radio.SetStandby(sx126x.STANDBY_RC) + // checkStatus(radio, "SetStandby") + // println("waiting 1s before transmitting...") + // time.Sleep(1000 * time.Millisecond) + // led.Set(true) + // err := Tx(radio, loraConfig, []byte("hello world")) + // if err != nil { + // println("failed to enter transmit:", err) + // } + // println("transmit started") + // } + + if rxDone.Load() { + rxDone.Store(false) + if !timeout.Load() { + println("receive completed without timeout, reading buffer...") + rxLength, rxPointer := radio.GetRxBufferStatus() + println("rx length:", rxLength, "rx pointer:", rxPointer) + rxData := radio.ReadBuffer(rxPointer, rxLength) + println("rx data:", string(rxData)) + led.Set(false) + } else { + println("receive timed out") + } + Rx(radio, loraConfig) + println("receive started") + } + } +} + +func SetupLora(radio *sx126x.Device, config lora.Config) { + // Switch to standby prior to configuration changes + + chipMode, _ := radio.GetStatus() + if chipMode != sx126x.CHIP_MODE_STBY_RC { + radio.SetStandby(sx126x.STANDBY_RC) + checkStatus(radio, "SetStandby") + } + radio.ClearDeviceErrors() + // Clear errors, disable radio interrupts for the moment + radio.SetPacketType(sx126x.PACKET_TYPE_LORA) + checkStatus(radio, "SetPacketType") + radio.SetRfFrequency(config.Freq) + checkStatus(radio, "SetRfFrequency") + radio.SetModulationParamsLoRa(spreadingFactor(config.Sf), bandwidth(config.Bw), codingRate(config.Cr), config.Ldr) + checkStatus(radio, "SetModulationParamsLoRa") + radio.SetPaConfig(0x04, 0x07, sx126x.DEVICE_SEL_SX1262) + checkStatus(radio, "SetPaConfig") + radio.SetTxParams(config.LoraTxPowerDBm, sx126x.RADIO_RAMP_200U) + checkStatus(radio, "SetTxParams") + var syncWord [2]uint8 + syncWord[0] = uint8(config.SyncWord >> 8) + syncWord[1] = uint8(config.SyncWord & 0x00FF) + + radio.WriteRegister(sx126x.REG_LORA_SYNC_WORD_MSB, syncWord[:]) + checkStatus(radio, "WriteRegister") + radio.SetRxTxFallbackMode(sx126x.FALLBACK_MODE_STDBY_RC) + checkStatus(radio, "SetRxTxFallbackMode") + radio.SetDIO2AsRfSwitchCtrl(true) + checkStatus(radio, "SetDIO2AsRfSwitchCtrl") + radio.SetDIO3AsTCXOCtrl(sx126x.TCXO_VOLTAGE_1_6V, 5000) + checkStatus(radio, "SetDIO3AsTCXOCtrl") +} + +func printRegister(radio *sx126x.Device, address uint16) { + data := radio.ReadRegister(address) + println("register:", address, "data:", data) +} + +func checkStatus(radio *sx126x.Device, prefix string) { + chipMode, commandStatus := radio.GetStatus() + if commandStatus == sx126x.COMMAND_STATUS_COMMAND_PROCESSING_ERROR || commandStatus == sx126x.COMMAND_STATUS_COMMAND_TIMEOUT || commandStatus == sx126x.COMMAND_STATUS_FAILURE_TO_EXECUTE_COMMAND { + println(prefix, "->", "command failed with status:", commandStatus) + } + if chipMode != sx126x.CHIP_MODE_STBY_RC && chipMode != sx126x.CHIP_MODE_STBY_XOSC { + println(prefix, "->", "exited standby:", chipMode) + } +} + +func checkDeviceErrors(radio *sx126x.Device, prefix string) { + err := radio.GetDeviceErrors() + if err == 0 { + return + } + println(prefix, "->", "error:", err) +} + +func Tx(radio *sx126x.Device, loraConfig lora.Config, data []byte) error { + radio.SetStandby(sx126x.STANDBY_RC) + radio.SetBufferBaseAddress(0, 0) + radio.SetDioIrqParams(sx126x.IRQ_TX_DONE_MASK|sx126x.IRQ_TIMEOUT_MASK, sx126x.IRQ_TX_DONE_MASK|sx126x.IRQ_TIMEOUT_MASK, 0x00, 0x00) + if len(data) > 255 { + return nil + } + radio.SetPacketParamsLoRa(loraConfig.Preamble, loraConfig.HeaderType, uint8(len(data)&0xFF), loraConfig.Crc, loraConfig.Iq) + radio.WriteBuffer(0, data) + radio.ClearIrqStatus(sx126x.IRQ_ALL_MASK) + radio.SetTx(1000) + return nil +} + +func Rx(radio *sx126x.Device, loraConfig lora.Config) { + radio.SetStandby(sx126x.STANDBY_RC) + radio.ClearIrqStatus(sx126x.IRQ_ALL_MASK) + radio.SetBufferBaseAddress(0, 0) + radio.SetRfFrequency(loraConfig.Freq) + radio.SetPacketParamsLoRa(loraConfig.Preamble, loraConfig.HeaderType, 0xFF, loraConfig.Crc, loraConfig.Iq) + radio.SetDioIrqParams(sx126x.IRQ_RX_DONE_MASK|sx126x.IRQ_TIMEOUT_MASK, sx126x.IRQ_RX_DONE_MASK|sx126x.IRQ_TIMEOUT_MASK, 0x00, 0x00) + radio.SetRx(1000) +} + +func codingRate(cr uint8) uint8 { + switch cr { + case lora.CodingRate4_5: + return sx126x.LORA_CR_4_5 + case lora.CodingRate4_6: + return sx126x.LORA_CR_4_6 + case lora.CodingRate4_7: + return sx126x.LORA_CR_4_7 + case lora.CodingRate4_8: + return sx126x.LORA_CR_4_8 + default: + return 0 + } +} + +func spreadingFactor(sf uint8) uint8 { + switch sf { + case lora.SpreadingFactor5: + return sx126x.LORA_SF_5 + case lora.SpreadingFactor6: + return sx126x.LORA_SF_6 + case lora.SpreadingFactor7: + return sx126x.LORA_SF_7 + case lora.SpreadingFactor8: + return sx126x.LORA_SF_8 + case lora.SpreadingFactor9: + return sx126x.LORA_SF_9 + case lora.SpreadingFactor10: + return sx126x.LORA_SF_10 + case lora.SpreadingFactor11: + return sx126x.LORA_SF_11 + case lora.SpreadingFactor12: + return sx126x.LORA_SF_12 + default: + return 0 + } +} + +func bandwidth(bw uint8) uint8 { + switch bw { + case lora.Bandwidth_7_8: + return sx126x.LORA_BW_7 + case lora.Bandwidth_10_4: + return sx126x.LORA_BW_10 + case lora.Bandwidth_15_6: + return sx126x.LORA_BW_15 + case lora.Bandwidth_20_8: + return sx126x.LORA_BW_20 + case lora.Bandwidth_31_25: + return sx126x.LORA_BW_31 + case lora.Bandwidth_41_7: + return sx126x.LORA_BW_41 + case lora.Bandwidth_62_5: + return sx126x.LORA_BW_62 + case lora.Bandwidth_125_0: + return sx126x.LORA_BW_125 + case lora.Bandwidth_250_0: + return sx126x.LORA_BW_250 + case lora.Bandwidth_500_0: + return sx126x.LORA_BW_500 + default: + return 0 + } +} + +func timeoutMsToRtcSteps(timeoutMs uint32) uint32 { + r := uint32(timeoutMs * (64000 / 1000)) + return r +}