mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-07-26 10:38:41 +00:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dc6edbb694 | |||
| 1d695a231a | |||
| 04acd8e666 | |||
| bb2d365868 | |||
| f459992f3c | |||
| 62663c1832 | |||
| 8f372935ac | |||
| fb3062433a | |||
| 9fc1c0aedc | |||
| 1c10dea443 | |||
| e960a6ff57 | |||
| 2673cc1e9a | |||
| 0034fc511a | |||
| a0c5da601f | |||
| 21a7d0a96a | |||
| e232a4f136 | |||
| 4071028e85 | |||
| a514169c37 | |||
| b480978e1a | |||
| 9eb95a4651 | |||
| d6114c9f6d | |||
| 0e2fb829ef | |||
| 892265b733 | |||
| b390e3225a | |||
| 1513808425 | |||
| 0a41786a77 | |||
| c21cd39813 | |||
| a35786be70 | |||
| 2a42fa7cbb | |||
| 5d96a56603 |
@@ -1,2 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
open_collective: tinygo
|
||||
|
||||
@@ -11,13 +11,12 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container: ghcr.io/tinygo-org/tinygo-dev:latest
|
||||
container:
|
||||
image: ghcr.io/tinygo-org/tinygo:latest
|
||||
options: --user root
|
||||
steps:
|
||||
- name: Work around CVE-2022-24765
|
||||
# We're not on a multi-user machine, so this is safe.
|
||||
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
- name: TinyGo version check
|
||||
run: tinygo version
|
||||
- name: Enforce Go Formatted Code
|
||||
@@ -25,4 +24,6 @@ jobs:
|
||||
- name: Run unit tests
|
||||
run: make unit-test
|
||||
- name: Run build and smoke tests
|
||||
run: make smoke-test
|
||||
run: |
|
||||
go env -w GOFLAGS=-buildvcs=false
|
||||
make smoke-test
|
||||
|
||||
@@ -1,3 +1,40 @@
|
||||
0.35.0
|
||||
---
|
||||
- **new devices**
|
||||
- **unoqmatrix**
|
||||
- LED matrix on the Arduino Uno Q
|
||||
- **waveshare-epd (ssd1680)**
|
||||
- Add driver for Waveshare 2.9 inch v2 e-paper display
|
||||
|
||||
- **enhancements**
|
||||
- **gps**
|
||||
- add UBX config command support (#831)
|
||||
- improve implementation for UBX config commands
|
||||
- revamp validSentence() to avoid heap allocation for errors
|
||||
- export some errors for checking/suppression from client
|
||||
- improvements and corrections for config commands
|
||||
- **lora**
|
||||
- fill out more constants for lora device
|
||||
- **mcp2515**
|
||||
- add support for extended CAN IDs (#857)
|
||||
- **si5351**
|
||||
- complete refactor for more complete interface
|
||||
- **st7735**
|
||||
- remove dependency on the machine package
|
||||
- **sx127x**
|
||||
- add functions used for FSK radio communication
|
||||
- **ws2812**
|
||||
- add brightness control
|
||||
- add PIO support for RP2040/RP2350
|
||||
|
||||
- **bugfixes**
|
||||
- **st7789**
|
||||
- fix scroll on rotated displays
|
||||
- fix driver when rotated 90º
|
||||
- **ws2812**
|
||||
- fix brightness control issues (#858)
|
||||
|
||||
|
||||
0.34.0
|
||||
---
|
||||
- **core**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2018-2025 The TinyGo Authors. All rights reserved.
|
||||
Copyright The TinyGo Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
|
||||
@@ -26,3 +26,17 @@ unit-test:
|
||||
@go test -v $(addprefix ./,$(TESTS))
|
||||
|
||||
test: clean fmt-check unit-test smoke-test
|
||||
|
||||
EXCLUDE_DIRS = build cmd examples internal lora ndir netdev netlink tester
|
||||
|
||||
drivers-count:
|
||||
@root_count=$$(find . -mindepth 1 -maxdepth 1 -type d | grep -vE '^\./($(subst $(space),|,$(EXCLUDE_DIRS)))$$' | wc -l); \
|
||||
epd_count=$$(find ./waveshare-epd -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l); \
|
||||
total=$$((root_count + epd_count)); \
|
||||
echo "Total drivers: $$total (root: $$root_count, waveshare-epd: $$epd_count)"
|
||||
|
||||
drivers-list:
|
||||
@{ \
|
||||
find . -mindepth 1 -maxdepth 1 -type d | grep -vE '^\./($(subst $(space),|,$(EXCLUDE_DIRS)))$$'; \
|
||||
if [ -d ./waveshare-epd ]; then find ./waveshare-epd -mindepth 1 -maxdepth 1 -type d; fi; \
|
||||
} | sed 's|^\./||' | sort
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
[](https://pkg.go.dev/tinygo.org/x/drivers) [](https://github.com/tinygo-org/drivers/actions/workflows/build.yml)
|
||||
|
||||
|
||||
This package provides a collection of over 130 different hardware drivers for devices such as sensors, displays, wireless adaptors, and actuators, that can be used together with [TinyGo](https://tinygo.org).
|
||||
This package provides a collection of over 140 different hardware drivers for devices such as sensors, displays, wireless adaptors, and actuators, that can be used together with [TinyGo](https://tinygo.org).
|
||||
|
||||
For the complete list, please see:
|
||||
https://tinygo.org/docs/reference/devices/
|
||||
|
||||
> [!IMPORTANT]
|
||||
> You can help TinyGo with a financial contribution using OpenCollective. Please see https://opencollective.com/tinygo for more information. Thank you!
|
||||
|
||||
## Installing
|
||||
|
||||
```shell
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
println("GPS UART Example")
|
||||
machine.UART1.Configure(machine.UARTConfig{BaudRate: 9600})
|
||||
ublox := gps.NewUART(machine.UART1)
|
||||
parser := gps.NewParser()
|
||||
@@ -16,14 +15,24 @@ func main() {
|
||||
for {
|
||||
s, err := ublox.NextSentence()
|
||||
if err != nil {
|
||||
println(err)
|
||||
continue
|
||||
switch err {
|
||||
case gps.ErrUnknownNMEASentence, gps.ErrInvalidNMEASentence, gps.ErrInvalidNMEASentenceLength:
|
||||
continue
|
||||
default:
|
||||
println("sentence error:", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
fix, err = parser.Parse(s)
|
||||
if err != nil {
|
||||
println(err)
|
||||
continue
|
||||
switch err {
|
||||
case gps.ErrUnknownNMEASentence, gps.ErrInvalidNMEASentence, gps.ErrInvalidNMEASentenceLength:
|
||||
continue
|
||||
default:
|
||||
println("parse error:", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if fix.Valid {
|
||||
print(fix.Time.Format("15:04:05"))
|
||||
@@ -43,7 +52,10 @@ func main() {
|
||||
}
|
||||
println()
|
||||
} else {
|
||||
println("No fix")
|
||||
if fix.Type == gps.GSV {
|
||||
// GSV sentence provides satellite count even if no fix yet
|
||||
println(fix.Satellites, "satellites visible")
|
||||
}
|
||||
}
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ func main() {
|
||||
SDI: machine.SPI0_SDI_PIN,
|
||||
Mode: 0})
|
||||
can := mcp2515.New(spi, csPin)
|
||||
can.Configure()
|
||||
can.Configure(mcp2515.Configuration{})
|
||||
err := can.Begin(mcp2515.CAN500kBps, mcp2515.Clock8MHz)
|
||||
if err != nil {
|
||||
failMessage(err.Error())
|
||||
|
||||
+33
-65
@@ -29,92 +29,60 @@ func main() {
|
||||
// Create driver instance
|
||||
clockgen := si5351.New(machine.I2C0)
|
||||
|
||||
// Verify device wired properly
|
||||
connected, err := clockgen.Connected()
|
||||
if err != nil {
|
||||
println("Unable to read device status")
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
if !connected {
|
||||
for {
|
||||
println("Unable to detect si5351 device")
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
// Initialize device
|
||||
cnf := si5351.Config{
|
||||
Capacitance: si5351.CrystalLoad10PF,
|
||||
}
|
||||
|
||||
// Initialise device
|
||||
clockgen.Configure()
|
||||
if err := clockgen.Configure(cnf); err != nil {
|
||||
println("Failed to configure Si5351:", err.Error())
|
||||
return
|
||||
}
|
||||
println("Si5351 configured")
|
||||
|
||||
// Now configue the PLLs and clock outputs.
|
||||
// The PLLs can be configured with a multiplier and division of the on-board
|
||||
// 25mhz reference crystal. For example configure PLL A to 900mhz by multiplying
|
||||
// by 36. This uses an integer multiplier which is more accurate over time
|
||||
// but allows less of a range of frequencies compared to a fractional
|
||||
// multiplier shown next.
|
||||
clockgen.ConfigurePLL(si5351.PLL_A, 36, 0, 1) // Multiply 25mhz by 36
|
||||
println("PLL A frequency: 900mhz")
|
||||
|
||||
// And next configure PLL B to 616.6667mhz by multiplying 25mhz by 24.667 using
|
||||
// the fractional multiplier configuration. Notice you specify the integer
|
||||
// multiplier and then a numerator and denominator as separate values, i.e.
|
||||
// numerator 2 and denominator 3 means 2/3 or 0.667. This fractional
|
||||
// configuration is susceptible to some jitter over time but can set a larger
|
||||
// range of frequencies.
|
||||
clockgen.ConfigurePLL(si5351.PLL_B, 24, 2, 3) // Multiply 25mhz by 24.667 (24 2/3)
|
||||
println("PLL B frequency: 616.6667mhz")
|
||||
|
||||
// Now configure the clock outputs. Each is driven by a PLL frequency as input
|
||||
// and then further divides that down to a specific frequency.
|
||||
// Configure clock 0 output to be driven by PLL A divided by 8, so an output
|
||||
// of 112.5mhz (900mhz / 8). Again this uses the most precise integer division
|
||||
// but can't set as wide a range of values.
|
||||
clockgen.ConfigureMultisynth(0, si5351.PLL_A, 8, 0, 1) // Divide by 8 (8 0/1)
|
||||
// Now configure the clock outputs.
|
||||
clockgen.SetFrequency(si5351.Clock0, 112_500_000)
|
||||
println("Clock 0: 112.5mhz")
|
||||
|
||||
// Next configure clock 1 to be driven by PLL B divided by 45.5 to get
|
||||
// 13.5531mhz (616.6667mhz / 45.5). This uses fractional division and again
|
||||
// notice the numerator and denominator are explicitly specified. This is less
|
||||
// precise but allows a large range of frequencies.
|
||||
clockgen.ConfigureMultisynth(1, si5351.PLL_B, 45, 1, 2) // Divide by 45.5 (45 1/2)
|
||||
// Next configure clock 1 for 13.5531mhz (616.6667mhz / 45.5).
|
||||
// This uses fractional division.
|
||||
clockgen.SetFrequency(si5351.Clock1, 13_553_125)
|
||||
println("Clock 1: 13.5531mhz")
|
||||
|
||||
// Finally configure clock 2 to be driven by PLL B divided once by 900 to get
|
||||
// down to 685.15 khz and then further divided by a special R divider that
|
||||
// divides 685.15 khz by 64 to get a final output of 10.706khz.
|
||||
clockgen.ConfigureMultisynth(2, si5351.PLL_B, 900, 0, 1) // Divide by 900 (900 0/1)
|
||||
// Set the R divider, this can be a value of:
|
||||
// - R_DIV_1: divider of 1
|
||||
// - R_DIV_2: divider of 2
|
||||
// - R_DIV_4: divider of 4
|
||||
// - R_DIV_8: divider of 8
|
||||
// - R_DIV_16: divider of 16
|
||||
// - R_DIV_32: divider of 32
|
||||
// - R_DIV_64: divider of 64
|
||||
// - R_DIV_128: divider of 128
|
||||
clockgen.ConfigureRdiv(2, si5351.R_DIV_64)
|
||||
// Finally configure clock 2 to output of 10.706khz.
|
||||
clockgen.SetFrequency(si5351.Clock2, 10_706)
|
||||
println("Clock 2: 10.706khz")
|
||||
|
||||
// After configuring PLLs and clocks, enable the outputs.
|
||||
clockgen.EnableOutputs()
|
||||
// After configuring the clocks enable the outputs.
|
||||
clockgen.EnableOutput(si5351.Clock0, true)
|
||||
clockgen.EnableOutput(si5351.Clock1, true)
|
||||
clockgen.EnableOutput(si5351.Clock2, true)
|
||||
println("All outputs enabled")
|
||||
|
||||
time.Sleep(time.Second)
|
||||
|
||||
clockgen.DisableOutputs()
|
||||
clockgen.EnableOutput(si5351.Clock0, false)
|
||||
clockgen.EnableOutput(si5351.Clock1, false)
|
||||
clockgen.EnableOutput(si5351.Clock2, false)
|
||||
println("All outputs disabled for 5 seconds")
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// Now use SetFrequency to re-set the frequencies of the outputs
|
||||
// Now turn clock outputs on and off repeatedly
|
||||
on := false
|
||||
for {
|
||||
if on {
|
||||
println("Setting Clock 0 output off")
|
||||
clockgen.OutputEnable(0, false)
|
||||
println("Setting clock outputs off")
|
||||
clockgen.EnableOutput(si5351.Clock0, false)
|
||||
clockgen.EnableOutput(si5351.Clock1, false)
|
||||
clockgen.EnableOutput(si5351.Clock2, false)
|
||||
on = false
|
||||
} else {
|
||||
println("Setting Clock 0 output to 100mhz")
|
||||
clockgen.SetFrequency(100*machine.MHz, 0, si5351.PLL_A)
|
||||
println("Setting clock outputs on")
|
||||
clockgen.EnableOutput(si5351.Clock0, true)
|
||||
clockgen.EnableOutput(si5351.Clock1, true)
|
||||
clockgen.EnableOutput(si5351.Clock2, true)
|
||||
on = true
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"machine"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/sx128x"
|
||||
)
|
||||
|
||||
var (
|
||||
// pin mapping specific to the lilygo t3s3, change as needed for your board
|
||||
sdoPin = machine.GPIO6
|
||||
sdiPin = machine.GPIO3
|
||||
sckPin = machine.GPIO5
|
||||
nssPin = machine.GPIO7
|
||||
busyPin = machine.GPIO36
|
||||
resetPin = machine.GPIO8
|
||||
dio1Pin = machine.GPIO9
|
||||
)
|
||||
|
||||
func setupPins() {
|
||||
nssPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
nssPin.Set(true)
|
||||
|
||||
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
resetPin.Set(true)
|
||||
|
||||
busyPin.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
dio1Pin.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
}
|
||||
|
||||
func main() {
|
||||
setupPins()
|
||||
|
||||
spi := machine.SPI0
|
||||
spi.Configure(machine.SPIConfig{
|
||||
Mode: 0,
|
||||
Frequency: 8 * 1e6,
|
||||
SDO: sdoPin,
|
||||
SDI: sdiPin,
|
||||
SCK: sckPin,
|
||||
})
|
||||
|
||||
radio := sx128x.New(
|
||||
spi,
|
||||
nssPin,
|
||||
resetPin,
|
||||
busyPin,
|
||||
)
|
||||
|
||||
radio.WaitWhileBusy(time.Second)
|
||||
SetupLora(radio)
|
||||
|
||||
for {
|
||||
data, err := Rx(radio)
|
||||
if err != nil {
|
||||
println("failed to receive:", err)
|
||||
} else {
|
||||
println("received:", string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SetupLora(radio *sx128x.Device) {
|
||||
radio.SetStandby(sx128x.STANDBY_RC)
|
||||
radio.SetPacketType(sx128x.PACKET_TYPE_LORA)
|
||||
radio.SetRegulatorMode(sx128x.REGULATOR_DC_DC)
|
||||
|
||||
radio.SetRfFrequency(2400000000) // 2.4Ghz
|
||||
radio.SetModulationParamsLoRa(sx128x.LORA_SF_9, sx128x.LORA_BW_1600, sx128x.LORA_CR_4_7)
|
||||
|
||||
// section 14.4.1 shows required register setting for setting up LoRa operations. These depend on the chosen spreading factor.
|
||||
radio.WriteRegister(0x925, []byte{0x32})
|
||||
radio.WriteRegister(0x93C, []byte{0x01})
|
||||
|
||||
radio.SetTxParams(13, sx128x.RADIO_RAMP_02_US)
|
||||
radio.SetPacketParamsLoRa(12, sx128x.LORA_HEADER_EXPLICIT, 0xFF, sx128x.LORA_CRC_DISABLE, sx128x.LORA_IQ_STD)
|
||||
radio.WriteRegister(sx128x.REG_LORA_SYNC_WORD_MSB, []byte{0x14, 0x24}) // full sync word is 0x1424
|
||||
|
||||
}
|
||||
|
||||
func Rx(radio *sx128x.Device) ([]byte, error) {
|
||||
radio.SetStandby(sx128x.STANDBY_RC)
|
||||
radio.SetDioIrqParams(sx128x.IRQ_RX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, sx128x.IRQ_RX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, 0x00, 0x00)
|
||||
radio.SetBufferBaseAddress(0, 0)
|
||||
radio.ClearIrqStatus(sx128x.IRQ_ALL_MASK)
|
||||
radio.SetRx(sx128x.PERIOD_BASE_4_MS, 250) // 4ms * 250 = 1s
|
||||
// busy wait for IRQ indication
|
||||
for dio1Pin.Get() == false {
|
||||
runtime.Gosched()
|
||||
}
|
||||
irqStatus, _ := radio.GetIrqStatus()
|
||||
if irqStatus&sx128x.IRQ_RX_DONE_MASK != 0 {
|
||||
payloadLength, bufferOffset, err := radio.GetRxBufferStatus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := radio.ReadBuffer(bufferOffset, payloadLength)
|
||||
return data, nil
|
||||
} else if irqStatus&sx128x.IRQ_RX_TX_TIMEOUT_MASK != 0 {
|
||||
return nil, errors.New("rx timeout")
|
||||
}
|
||||
return nil, errors.New("unexpected IRQ status")
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"machine"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/sx128x"
|
||||
)
|
||||
|
||||
var (
|
||||
// pin mapping specific to the lilygo t3s3, change as needed for your board
|
||||
sdoPin = machine.GPIO6
|
||||
sdiPin = machine.GPIO3
|
||||
sckPin = machine.GPIO5
|
||||
nssPin = machine.GPIO7
|
||||
busyPin = machine.GPIO36
|
||||
resetPin = machine.GPIO8
|
||||
dio1Pin = machine.GPIO9
|
||||
)
|
||||
|
||||
func setupPins() {
|
||||
nssPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
nssPin.Set(true)
|
||||
|
||||
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
resetPin.Set(true)
|
||||
|
||||
busyPin.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
dio1Pin.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
}
|
||||
|
||||
func main() {
|
||||
setupPins()
|
||||
|
||||
spi := machine.SPI0
|
||||
spi.Configure(machine.SPIConfig{
|
||||
Mode: 0,
|
||||
Frequency: 8 * 1e6,
|
||||
SDO: sdoPin,
|
||||
SDI: sdiPin,
|
||||
SCK: sckPin,
|
||||
})
|
||||
|
||||
radio := sx128x.New(
|
||||
spi,
|
||||
nssPin,
|
||||
resetPin,
|
||||
busyPin,
|
||||
)
|
||||
|
||||
radio.WaitWhileBusy(time.Second)
|
||||
SetupLora(radio)
|
||||
|
||||
for {
|
||||
Tx(radio, []byte("Hello, world!"))
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func SetupLora(radio *sx128x.Device) {
|
||||
radio.SetStandby(sx128x.STANDBY_RC)
|
||||
radio.SetPacketType(sx128x.PACKET_TYPE_LORA)
|
||||
radio.SetRegulatorMode(sx128x.REGULATOR_DC_DC)
|
||||
|
||||
radio.SetRfFrequency(2400000000) // 2.4Ghz
|
||||
radio.SetModulationParamsLoRa(sx128x.LORA_SF_9, sx128x.LORA_BW_1600, sx128x.LORA_CR_4_7)
|
||||
|
||||
// section 14.4.1 shows required register setting for setting up LoRa operations. These depend on the chosen spreading factor.
|
||||
radio.WriteRegister(0x925, []byte{0x32})
|
||||
radio.WriteRegister(0x93C, []byte{0x01})
|
||||
|
||||
radio.SetTxParams(13, sx128x.RADIO_RAMP_02_US)
|
||||
radio.SetPacketParamsLoRa(12, sx128x.LORA_HEADER_EXPLICIT, 0xFF, sx128x.LORA_CRC_DISABLE, sx128x.LORA_IQ_STD)
|
||||
radio.WriteRegister(sx128x.REG_LORA_SYNC_WORD_MSB, []byte{0x14, 0x24}) // full sync word is 0x1424
|
||||
|
||||
}
|
||||
|
||||
func Tx(radio *sx128x.Device, data []byte) error {
|
||||
if len(data) > 255 {
|
||||
return errors.New("data length exceeds maximum of 255 bytes")
|
||||
}
|
||||
radio.SetStandby(sx128x.STANDBY_RC)
|
||||
radio.SetPacketParamsLoRa(12, sx128x.LORA_HEADER_EXPLICIT, uint8(len(data)&0xFF), sx128x.LORA_CRC_DISABLE, sx128x.LORA_IQ_STD)
|
||||
radio.SetBufferBaseAddress(0, 0)
|
||||
radio.WriteBuffer(0, data)
|
||||
radio.SetDioIrqParams(sx128x.IRQ_TX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, sx128x.IRQ_TX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, 0x00, 0x00)
|
||||
radio.ClearIrqStatus(sx128x.IRQ_ALL_MASK)
|
||||
radio.SetTx(sx128x.PERIOD_BASE_4_MS, 250) // 4ms * 250 = 1s
|
||||
// busy wait for IRQ indication
|
||||
for dio1Pin.Get() == false {
|
||||
runtime.Gosched()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
"image/color"
|
||||
"math/rand"
|
||||
|
||||
"tinygo.org/x/drivers/unoqmatrix"
|
||||
)
|
||||
|
||||
var on = color.RGBA{255, 255, 255, 255}
|
||||
|
||||
func main() {
|
||||
display := unoqmatrix.NewFromBasePin(machine.PF0)
|
||||
display.ClearDisplay()
|
||||
|
||||
w, h := display.Size()
|
||||
x := int16(0)
|
||||
y := int16(0)
|
||||
deltaX := int16(1)
|
||||
deltaY := int16(1)
|
||||
|
||||
for {
|
||||
pixel := display.GetPixel(x, y)
|
||||
if pixel.R != 0 || pixel.G != 0 || pixel.B != 0 {
|
||||
display.ClearDisplay()
|
||||
x = 1 + int16(rand.Int31n(3))
|
||||
y = 1 + int16(rand.Int31n(3))
|
||||
deltaX = 1
|
||||
deltaY = 1
|
||||
if rand.Int31n(2) == 0 {
|
||||
deltaX = -1
|
||||
}
|
||||
if rand.Int31n(2) == 0 {
|
||||
deltaY = -1
|
||||
}
|
||||
}
|
||||
display.SetPixel(x, y, on)
|
||||
|
||||
x += deltaX
|
||||
y += deltaY
|
||||
|
||||
if x == 0 || x == w-1 {
|
||||
deltaX = -deltaX
|
||||
}
|
||||
|
||||
if y == 0 || y == h-1 {
|
||||
deltaY = -deltaY
|
||||
}
|
||||
|
||||
display.Display()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/waveshare-epd/epd2in9v2"
|
||||
)
|
||||
|
||||
var display epd2in9v2.Device
|
||||
|
||||
func main() {
|
||||
machine.SPI0.Configure(machine.SPIConfig{
|
||||
Frequency: 12000000,
|
||||
SCK: machine.EPD_SCK_PIN,
|
||||
SDO: machine.EPD_SDO_PIN,
|
||||
})
|
||||
|
||||
display = epd2in9v2.New(
|
||||
machine.SPI0,
|
||||
machine.EPD_CS_PIN,
|
||||
machine.EPD_DC_PIN,
|
||||
machine.EPD_RESET_PIN,
|
||||
machine.EPD_BUSY_PIN,
|
||||
)
|
||||
display.Configure(epd2in9v2.Config{
|
||||
Rotation: epd2in9v2.ROTATION_270,
|
||||
Speed: epd2in9v2.SPEED_DEFAULT,
|
||||
Blocking: true,
|
||||
})
|
||||
|
||||
black := color.RGBA{0, 0, 0, 255}
|
||||
white := color.RGBA{255, 255, 255, 255}
|
||||
|
||||
// --- Step 1: clear to white ---
|
||||
println("epd2in9v2: clearing display")
|
||||
display.ClearBuffer()
|
||||
display.Display()
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// --- Step 2: full refresh checkerboard ---
|
||||
println("epd2in9v2: drawing checkerboard (full refresh)")
|
||||
w, h := display.Size()
|
||||
for i := int16(0); i < w/8; i++ {
|
||||
for j := int16(0); j < h/8; j++ {
|
||||
if (i+j)%2 == 0 {
|
||||
fillRect(i*8, j*8, 8, 8, black)
|
||||
}
|
||||
}
|
||||
}
|
||||
display.Display()
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// --- Step 3: fast refresh - draw border and diagonal cross ---
|
||||
println("epd2in9v2: switching to fast refresh")
|
||||
display.SetSpeed(epd2in9v2.SPEED_FAST)
|
||||
display.ClearBuffer()
|
||||
|
||||
for x := int16(0); x < w; x++ {
|
||||
display.SetPixel(x, 0, black)
|
||||
display.SetPixel(x, h-1, black)
|
||||
}
|
||||
for y := int16(0); y < h; y++ {
|
||||
display.SetPixel(0, y, black)
|
||||
display.SetPixel(w-1, y, black)
|
||||
}
|
||||
for i := int16(0); i < w && i < h; i++ {
|
||||
display.SetPixel(i, i*h/w, black)
|
||||
display.SetPixel(w-1-i, i*h/w, black)
|
||||
}
|
||||
|
||||
display.Display()
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// --- Step 4: partial refresh counter ---
|
||||
println("epd2in9v2: partial refresh demo")
|
||||
display.SetSpeed(epd2in9v2.SPEED_DEFAULT)
|
||||
display.ClearBuffer()
|
||||
|
||||
println("epd2in9v2: setting base image")
|
||||
display.DisplayWithBase()
|
||||
|
||||
for count := 0; count < 10; count++ {
|
||||
cx := int16(120)
|
||||
cy := int16(50)
|
||||
fillRect(cx, cy, 60, 20, white)
|
||||
|
||||
digit := int16(count % 10)
|
||||
drawDigit(cx+22, cy+2, digit, black)
|
||||
|
||||
display.DisplayPartial()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// --- Step 5: sleep ---
|
||||
println("epd2in9v2: entering deep sleep")
|
||||
display.ClearBuffer()
|
||||
display.Display()
|
||||
display.Sleep()
|
||||
println("epd2in9v2: done, you can remove power")
|
||||
}
|
||||
|
||||
func fillRect(x, y, w, h int16, c color.RGBA) {
|
||||
for i := x; i < x+w; i++ {
|
||||
for j := y; j < y+h; j++ {
|
||||
display.SetPixel(i, j, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// drawDigit draws a simple 3x5-pixel-block digit (each block 4x3 px) at position (x,y).
|
||||
func drawDigit(x, y, digit int16, c color.RGBA) {
|
||||
segments := [10][5]uint8{
|
||||
{0x7, 0x5, 0x5, 0x5, 0x7}, // 0
|
||||
{0x2, 0x2, 0x2, 0x2, 0x2}, // 1
|
||||
{0x7, 0x1, 0x7, 0x4, 0x7}, // 2
|
||||
{0x7, 0x1, 0x7, 0x1, 0x7}, // 3
|
||||
{0x5, 0x5, 0x7, 0x1, 0x1}, // 4
|
||||
{0x7, 0x4, 0x7, 0x1, 0x7}, // 5
|
||||
{0x7, 0x4, 0x7, 0x5, 0x7}, // 6
|
||||
{0x7, 0x1, 0x1, 0x1, 0x1}, // 7
|
||||
{0x7, 0x5, 0x7, 0x5, 0x7}, // 8
|
||||
{0x7, 0x5, 0x7, 0x1, 0x7}, // 9
|
||||
}
|
||||
if digit < 0 || digit > 9 {
|
||||
return
|
||||
}
|
||||
for row := int16(0); row < 5; row++ {
|
||||
for col := int16(0); col < 3; col++ {
|
||||
if segments[digit][row]&(0x4>>uint(col)) != 0 {
|
||||
fillRect(x+col*4, y+row*3, 4, 3, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build arduino
|
||||
//go:build arduino || arduino_uno
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !digispark && !arduino
|
||||
//go:build !digispark && !arduino && !arduino_uno && !xiao_esp32c3
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build xiao_esp32c3
|
||||
|
||||
package main
|
||||
|
||||
import "machine"
|
||||
|
||||
func init() {
|
||||
// Replace neo in the code below to match the pin
|
||||
// that you are using if different.
|
||||
neo = machine.D6
|
||||
}
|
||||
@@ -1,17 +1,16 @@
|
||||
module tinygo.org/x/drivers
|
||||
|
||||
|
||||
go 1.22.1
|
||||
|
||||
toolchain go1.23.1
|
||||
|
||||
|
||||
require (
|
||||
github.com/eclipse/paho.mqtt.golang v1.2.0
|
||||
github.com/frankban/quicktest v1.10.2
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||
github.com/orsinium-labs/tinymath v1.1.0
|
||||
github.com/soypat/natiu-mqtt v0.5.1
|
||||
github.com/tinygo-org/pio v0.3.0
|
||||
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d
|
||||
golang.org/x/net v0.33.0
|
||||
tinygo.org/x/tinyfont v0.3.0
|
||||
|
||||
@@ -17,6 +17,8 @@ github.com/orsinium-labs/tinymath v1.1.0 h1:KomdsyLHB7vE3f1nRAJF2dyf1m/gnM2HxfTe
|
||||
github.com/orsinium-labs/tinymath v1.1.0/go.mod h1:WPXX6ei3KSXG7JfA03a+ekCYaY9SWN4I+JRl2p6ck+A=
|
||||
github.com/soypat/natiu-mqtt v0.5.1 h1:rwaDmlvjzD2+3MCOjMZc4QEkDkNwDzbct2TJbpz+TPc=
|
||||
github.com/soypat/natiu-mqtt v0.5.1/go.mod h1:xEta+cwop9izVCW7xOx2W+ct9PRMqr0gNVkvBPnQTc4=
|
||||
github.com/tinygo-org/pio v0.3.0 h1:opEnOtw58KGB4RJD3/n/Rd0/djYGX3DeJiXLI6y/yDI=
|
||||
github.com/tinygo-org/pio v0.3.0/go.mod h1:wf6c6lKZp+pQOzKKcpzchmRuhiMc27ABRuo7KVnaMFU=
|
||||
github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
|
||||
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
|
||||
+11
-34
@@ -11,37 +11,18 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidNMEASentenceLength = errors.New("invalid NMEA sentence length")
|
||||
errInvalidNMEAChecksum = errors.New("invalid NMEA sentence checksum")
|
||||
errEmptyNMEASentence = errors.New("cannot parse empty NMEA sentence")
|
||||
errUnknownNMEASentence = errors.New("unsupported NMEA sentence type")
|
||||
ErrInvalidNMEASentenceLength = errors.New("invalid NMEA sentence length")
|
||||
ErrInvalidNMEASentence = errors.New("invalid NMEA sentence format")
|
||||
ErrEmptyNMEASentence = errors.New("cannot parse empty NMEA sentence")
|
||||
ErrUnknownNMEASentence = errors.New("unsupported NMEA sentence type")
|
||||
errInvalidGSVSentence = errors.New("invalid GSV NMEA sentence")
|
||||
errInvalidGGASentence = errors.New("invalid GGA NMEA sentence")
|
||||
errInvalidRMCSentence = errors.New("invalid RMC NMEA sentence")
|
||||
errInvalidGLLSentence = errors.New("invalid GLL NMEA sentence")
|
||||
errGPSCommandRejected = errors.New("GPS command rejected (NAK)")
|
||||
errNoACKToGPSCommand = errors.New("no ACK to GPS command")
|
||||
)
|
||||
|
||||
type GPSError struct {
|
||||
Err error
|
||||
Info string
|
||||
Sentence string
|
||||
}
|
||||
|
||||
func newGPSError(err error, sentence string, info string) GPSError {
|
||||
return GPSError{
|
||||
Info: info,
|
||||
Err: err,
|
||||
Sentence: sentence,
|
||||
}
|
||||
}
|
||||
|
||||
func (ge GPSError) Error() string {
|
||||
return ge.Err.Error() + " " + ge.Info + " " + ge.Sentence
|
||||
}
|
||||
|
||||
func (ge GPSError) Unwrap() error {
|
||||
return ge.Err
|
||||
}
|
||||
|
||||
const (
|
||||
minimumNMEALength = 7
|
||||
startingDelimiter = '$'
|
||||
@@ -50,19 +31,18 @@ const (
|
||||
|
||||
// Device wraps a connection to a GPS device.
|
||||
type Device struct {
|
||||
buffer []byte
|
||||
bufIdx int
|
||||
sentence strings.Builder
|
||||
uart drivers.UART
|
||||
bus drivers.I2C
|
||||
address uint16
|
||||
buffer [bufferSize]byte
|
||||
}
|
||||
|
||||
// NewUART creates a new UART GPS connection. The UART must already be configured.
|
||||
func NewUART(uart drivers.UART) Device {
|
||||
return Device{
|
||||
uart: uart,
|
||||
buffer: make([]byte, bufferSize),
|
||||
bufIdx: bufferSize,
|
||||
sentence: strings.Builder{},
|
||||
}
|
||||
@@ -79,7 +59,6 @@ func NewI2CWithAddress(bus drivers.I2C, i2cAddress uint16) Device {
|
||||
return Device{
|
||||
bus: bus,
|
||||
address: i2cAddress,
|
||||
buffer: make([]byte, bufferSize),
|
||||
bufIdx: bufferSize,
|
||||
sentence: strings.Builder{},
|
||||
}
|
||||
@@ -172,17 +151,15 @@ func (gps *Device) WriteBytes(bytes []byte) {
|
||||
// It has to end with a '*' character following by a checksum.
|
||||
func validSentence(sentence string) error {
|
||||
if len(sentence) < minimumNMEALength || sentence[0] != startingDelimiter || sentence[len(sentence)-3] != checksumDelimiter {
|
||||
return errInvalidNMEASentenceLength
|
||||
return ErrInvalidNMEASentenceLength
|
||||
}
|
||||
var cs byte = 0
|
||||
for i := 1; i < len(sentence)-3; i++ {
|
||||
cs ^= sentence[i]
|
||||
}
|
||||
checksum := strings.ToUpper(hex.EncodeToString([]byte{cs}))
|
||||
if checksum != sentence[len(sentence)-2:len(sentence)] {
|
||||
return newGPSError(errInvalidNMEAChecksum, sentence,
|
||||
"expected "+sentence[len(sentence)-2:len(sentence)]+
|
||||
" got "+checksum)
|
||||
if checksum != sentence[len(sentence)-2:] {
|
||||
return ErrInvalidNMEASentence
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
+39
-3
@@ -6,12 +6,28 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type NMEASentenceType string
|
||||
|
||||
const (
|
||||
GSA NMEASentenceType = "GSA"
|
||||
GGA NMEASentenceType = "GGA"
|
||||
GLL NMEASentenceType = "GLL"
|
||||
GSV NMEASentenceType = "GSV"
|
||||
RMC NMEASentenceType = "RMC"
|
||||
VTG NMEASentenceType = "VTG"
|
||||
ZDA NMEASentenceType = "ZDA"
|
||||
TXT NMEASentenceType = "TXT"
|
||||
)
|
||||
|
||||
// Parser for GPS NMEA sentences.
|
||||
type Parser struct {
|
||||
}
|
||||
|
||||
// Fix is a GPS location fix
|
||||
type Fix struct {
|
||||
// Type is the NMEA sentence type that provided this fix.
|
||||
Type NMEASentenceType
|
||||
|
||||
// Valid if the fix was valid.
|
||||
Valid bool
|
||||
|
||||
@@ -46,13 +62,30 @@ func NewParser() Parser {
|
||||
func (parser *Parser) Parse(sentence string) (Fix, error) {
|
||||
var fix Fix
|
||||
if sentence == "" {
|
||||
return fix, errEmptyNMEASentence
|
||||
return fix, ErrEmptyNMEASentence
|
||||
}
|
||||
if len(sentence) < 6 {
|
||||
return fix, errInvalidNMEASentenceLength
|
||||
return fix, ErrInvalidNMEASentenceLength
|
||||
}
|
||||
typ := sentence[3:6]
|
||||
switch typ {
|
||||
case "GSV":
|
||||
// https://docs.novatel.com/OEM7/Content/Logs/GPGSV.htm
|
||||
fields := strings.Split(sentence, ",")
|
||||
// GSV sentences have at least 4 fields, but typically 8, 12, 16, or 20 depending on satellites in view
|
||||
if len(fields) < 4 {
|
||||
return fix, errInvalidGSVSentence
|
||||
}
|
||||
|
||||
fix.Type = GSV
|
||||
|
||||
// Number of satellites in view is always field 3
|
||||
fix.Satellites = findSatellites(fields[3])
|
||||
|
||||
// GSV does not provide position, time, or fix validity
|
||||
fix.Valid = false
|
||||
|
||||
return fix, nil
|
||||
case "GGA":
|
||||
// https://docs.novatel.com/OEM7/Content/Logs/GPGGA.htm
|
||||
fields := strings.Split(sentence, ",")
|
||||
@@ -60,6 +93,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) {
|
||||
return fix, errInvalidGGASentence
|
||||
}
|
||||
|
||||
fix.Type = GGA
|
||||
fix.Time = findTime(fields[1])
|
||||
fix.Latitude = findLatitude(fields[2], fields[3])
|
||||
fix.Longitude = findLongitude(fields[4], fields[5])
|
||||
@@ -75,6 +109,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) {
|
||||
return fix, errInvalidGLLSentence
|
||||
}
|
||||
|
||||
fix.Type = GLL
|
||||
fix.Latitude = findLatitude(fields[1], fields[2])
|
||||
fix.Longitude = findLongitude(fields[3], fields[4])
|
||||
fix.Time = findTime(fields[5])
|
||||
@@ -89,6 +124,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) {
|
||||
return fix, errInvalidRMCSentence
|
||||
}
|
||||
|
||||
fix.Type = RMC
|
||||
fix.Time = findTime(fields[1])
|
||||
fix.Valid = (fields[2] == "A")
|
||||
fix.Latitude = findLatitude(fields[3], fields[4])
|
||||
@@ -104,7 +140,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) {
|
||||
return fix, nil
|
||||
}
|
||||
|
||||
return fix, newGPSError(errUnknownNMEASentence, sentence, typ)
|
||||
return fix, ErrUnknownNMEASentence
|
||||
}
|
||||
|
||||
// findTime returns the time from an NMEA sentence:
|
||||
|
||||
+18
-2
@@ -8,13 +8,29 @@ import (
|
||||
)
|
||||
|
||||
func TestParseUnknownSentence(t *testing.T) {
|
||||
p := NewParser()
|
||||
|
||||
val := "$GPVTG,89.68,T,,M,0.00,N,0.0,K*5F"
|
||||
_, err := p.Parse(val)
|
||||
if err == nil {
|
||||
t.Error("should have unknown sentence err")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGSV(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
p := NewParser()
|
||||
|
||||
val := "$GPGSV,3,1,09,07,14,317,22,08,31,284,25,10,32,133,39,16,85,232,29*7F"
|
||||
_, err := p.Parse(val)
|
||||
c.Assert(err.Error(), qt.Contains, "unsupported NMEA sentence type")
|
||||
fix, err := p.Parse(val)
|
||||
if err != nil {
|
||||
t.Error("should have parsed")
|
||||
}
|
||||
|
||||
c.Assert(fix.Type, qt.Equals, GSV)
|
||||
c.Assert(fix.Satellites, qt.Equals, int16(9))
|
||||
c.Assert(fix.Valid, qt.Equals, false)
|
||||
}
|
||||
|
||||
func TestParseGGA(t *testing.T) {
|
||||
|
||||
+240
-39
@@ -1,53 +1,254 @@
|
||||
package gps
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// flight mode disables the GPS COCOM limits
|
||||
var flight_mode_cmd = [...]byte{
|
||||
0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0xFF, 0xFF, 0x06, 0x03, 0x00, 0x00, 0x00,
|
||||
0x00, 0x10, 0x27, 0x00, 0x00, 0x05, 0x00, 0xFA, 0x00, 0xFA, 0x00, 0x64, 0x00,
|
||||
0x2C, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x16, 0xDC}
|
||||
|
||||
// Sets CFG-GNSS to disable everything other than GPS GNSS
|
||||
// solution. Failure to do this means GPS power saving
|
||||
// doesn't work. Not needed for MAX7, needed for MAX8's
|
||||
var cfg_gnss_cmd = [...]byte{
|
||||
0xB5, 0x62, 0x06, 0x3E, 0x2C, 0x00, 0x00, 0x00,
|
||||
0x20, 0x05, 0x00, 0x08, 0x10, 0x00, 0x01, 0x00,
|
||||
0x01, 0x01, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00,
|
||||
0x01, 0x01, 0x03, 0x08, 0x10, 0x00, 0x00, 0x00,
|
||||
0x01, 0x01, 0x05, 0x00, 0x03, 0x00, 0x00, 0x00,
|
||||
0x01, 0x01, 0x06, 0x08, 0x0E, 0x00, 0x00, 0x00,
|
||||
0x01, 0x01, 0xFC, 0x11}
|
||||
|
||||
func FlightMode(d Device) (err error) {
|
||||
err = sendCommand(d, flight_mode_cmd[:])
|
||||
return err
|
||||
// FlightModeCmd is a UBX-CFG-NAV5 command
|
||||
var nav5Cmd = CfgNav5{
|
||||
Mask: CfgNav5Dyn | CfgNav5MinEl | CfgNav5PosFixMode,
|
||||
DynModel: DynModeAirborne1g, // Airborne with <1g acceleration
|
||||
FixMode: FixModeAuto, // Auto 2D/3D
|
||||
MinElev_deg: 5, // Minimum elevation 5 degrees
|
||||
FixedAlt_me2: 0, // Not used
|
||||
FixedAltVar_m2e4: 0, // Not used
|
||||
PDop: 100, // 10.0
|
||||
TDop: 100, // 10.0
|
||||
PAcc_m: 5000, // 5 meters
|
||||
TAcc_m: 5000, // 5 meters
|
||||
StaticHoldThresh_cm_s: 0, // Not used
|
||||
DgnssTimeout_s: 0, // Not used
|
||||
CnoThreshNumSVs: 0, // Not used
|
||||
CnoThresh_dbhz: 0, // Not used
|
||||
StaticHoldMaxDist_m: 0, // Not used
|
||||
UtcStandard: 0, // Automatic
|
||||
Reserved1: [2]byte{},
|
||||
Reserved2: [5]byte{},
|
||||
}
|
||||
|
||||
func SetCfgGNSS(d Device) (err error) {
|
||||
err = sendCommand(d, cfg_gnss_cmd[:])
|
||||
return err
|
||||
// SetFlightMode sends UBX-CFG-NAV5 command to set GPS into flight mode
|
||||
func (d *Device) SetFlightMode() (err error) {
|
||||
nav5Cmd.DynModel = DynModeAirborne1g
|
||||
nav5Cmd.FixMode = FixModeAuto
|
||||
nav5Cmd.Put42Bytes(d.buffer[:])
|
||||
|
||||
return d.SendCommand(d.buffer[:42])
|
||||
}
|
||||
|
||||
func sendCommand(d Device, command []byte) (err error) {
|
||||
d.WriteBytes(command)
|
||||
// SetPedestrianMode sends UBX-CFG-NAV5 command to set GPS into pedestrian mode
|
||||
func (d *Device) SetPedestrianMode() (err error) {
|
||||
nav5Cmd.DynModel = DynModePedestrian
|
||||
nav5Cmd.FixMode = FixModeAuto
|
||||
nav5Cmd.Put42Bytes(d.buffer[:])
|
||||
|
||||
return d.SendCommand(d.buffer[:42])
|
||||
}
|
||||
|
||||
// SetAutomotiveMode sends UBX-CFG-NAV5 command to set GPS into automotive mode
|
||||
func (d *Device) SetAutomotiveMode() (err error) {
|
||||
nav5Cmd.DynModel = DynModeAutomotive
|
||||
nav5Cmd.FixMode = FixModeAuto
|
||||
nav5Cmd.Put42Bytes(d.buffer[:])
|
||||
|
||||
return d.SendCommand(d.buffer[:42])
|
||||
}
|
||||
|
||||
// SetBikeMode sends UBX-CFG-NAV5 command to set GPS into bike mode
|
||||
func (d *Device) SetBikeMode() (err error) {
|
||||
nav5Cmd.DynModel = DynModeBike
|
||||
nav5Cmd.FixMode = FixModeAuto
|
||||
nav5Cmd.Put42Bytes(d.buffer[:])
|
||||
|
||||
return d.SendCommand(d.buffer[:42])
|
||||
}
|
||||
|
||||
var (
|
||||
// GGA (time, lat/lng, altitude)
|
||||
messageRateGGACmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x00,
|
||||
Rate: 1, // Every position fix
|
||||
}
|
||||
// GLL (time, lat/lng)
|
||||
messageRateGLLCmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x01,
|
||||
Rate: 1, // Every position fix
|
||||
}
|
||||
// GSA (satellite id list)
|
||||
messageRateGSACmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x02,
|
||||
Rate: 0, // Disabled
|
||||
}
|
||||
// GSV (satellite locations)
|
||||
messageRateGSVCmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x03,
|
||||
Rate: 0, // Every position fix
|
||||
}
|
||||
// RMC (time, lat/lng, speed, course)
|
||||
messageRateRMCCmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x04,
|
||||
Rate: 1, // Every position fix
|
||||
}
|
||||
// VTG (speed, course)
|
||||
messageRateVTGCmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x05,
|
||||
Rate: 0, // Disabled
|
||||
}
|
||||
// ZDA (time, timezone)
|
||||
messageRateZDACmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x08,
|
||||
Rate: 0, // Disabled
|
||||
}
|
||||
// TXT (text transmission)
|
||||
messageRateTXTCmd = CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x41,
|
||||
Rate: 0, // Disabled
|
||||
}
|
||||
)
|
||||
|
||||
// SetMessageRatesMinimal configures the GPS to output a minimal set of NMEA sentences:
|
||||
// GSV, GGA, GLL, and RMC only.
|
||||
func SetMessageRatesMinimal(d *Device) (err error) {
|
||||
commands := []CfgMsg1{
|
||||
messageRateGSACmd,
|
||||
messageRateGLLCmd,
|
||||
messageRateVTGCmd,
|
||||
messageRateZDACmd,
|
||||
messageRateTXTCmd,
|
||||
}
|
||||
for i := range commands {
|
||||
commands[i].Rate = 0 // Disable
|
||||
}
|
||||
return setCfg1s(d, commands)
|
||||
}
|
||||
|
||||
// SetMessageRatesAllEnabled configures the GPS to output all NMEA sentences
|
||||
func SetMessageRatesAllEnabled(d *Device) (err error) {
|
||||
commands := []CfgMsg1{
|
||||
messageRateGSACmd,
|
||||
messageRateGGACmd,
|
||||
messageRateGLLCmd,
|
||||
messageRateGSVCmd,
|
||||
messageRateRMCCmd,
|
||||
messageRateVTGCmd,
|
||||
messageRateZDACmd,
|
||||
messageRateTXTCmd,
|
||||
}
|
||||
for i := range commands {
|
||||
commands[i].Rate = 1 // Enable
|
||||
}
|
||||
return setCfg1s(d, commands)
|
||||
}
|
||||
|
||||
func setCfg1s(d *Device, commands []CfgMsg1) (err error) {
|
||||
var buf [9]byte
|
||||
for _, cmd := range commands {
|
||||
cmd.Put9Bytes(buf[:])
|
||||
// TODO handle errors differently here?
|
||||
// This implementation just saves the last error and continues.
|
||||
// Due to the GPS modules sending updates asynchronously
|
||||
// the response is interleaved along with regular ASCII
|
||||
// NMEA messages.
|
||||
err = d.SendCommand(buf[:])
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// gnssDisableCmd is a UBX-CFG-GNSS command to disable all GNSS but GPS
|
||||
// Needed for MAX8's, not needed for MAX7
|
||||
var gnssDisableCmd = CfgGnss{
|
||||
MsgVer: 0x00,
|
||||
NumTrkChHw: 0x20, // 32 channels
|
||||
NumTrkChUse: 0x20,
|
||||
ConfigBlocks: []CfgGnssConfigBlocksType{
|
||||
{GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000}, // GPS enabled
|
||||
{GnssId: 1, ResTrkCh: 1, MaxTrkCh: 3, Flags: 0x010000}, // SBAS disabled
|
||||
{GnssId: 3, ResTrkCh: 8, MaxTrkCh: 16, Flags: 0x010000}, // BeiDou disabled
|
||||
{GnssId: 5, ResTrkCh: 0, MaxTrkCh: 3, Flags: 0x010000}, // QZSS disabled
|
||||
{GnssId: 6, ResTrkCh: 8, MaxTrkCh: 14, Flags: 0x010000}, // GLONASS disabled
|
||||
},
|
||||
}
|
||||
|
||||
// SetGNSSDisable sends UBX-CFG-GNSS command to disable all GNSS but GPS
|
||||
func (d *Device) SetGNSSDisable() (err error) {
|
||||
err = gnssDisableCmd.Put(d.buffer[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return d.SendCommand(d.buffer[:])
|
||||
}
|
||||
|
||||
// SendCommand sends a UBX command and waits for ACK/NAK response
|
||||
func (d *Device) SendCommand(command []byte) error {
|
||||
// Calculate and append checksum
|
||||
checksummed := appendChecksum(command)
|
||||
d.WriteBytes(checksummed)
|
||||
|
||||
start := time.Now()
|
||||
for time.Now().Sub(start) < 1000 {
|
||||
if d.readNextByte() == '\n' {
|
||||
if d.readNextByte() == 0xB5 {
|
||||
d.readNextByte()
|
||||
if d.readNextByte() == 0x05 {
|
||||
if d.readNextByte() == 0x01 {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
for time.Since(start) < time.Second {
|
||||
// Look for UBX sync sequence
|
||||
if d.readNextByte() != ubxSyncChar1 {
|
||||
continue
|
||||
}
|
||||
if d.readNextByte() != ubxSyncChar2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read message class and ID
|
||||
msgClass := d.readNextByte()
|
||||
msgID := d.readNextByte()
|
||||
|
||||
// Check if it's an ACK class message
|
||||
if msgClass != ubxClassACK {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read length (2 bytes, little-endian) - ACK is always 2 bytes payload
|
||||
lenLo := d.readNextByte()
|
||||
lenHi := d.readNextByte()
|
||||
length := uint16(lenLo) | uint16(lenHi)<<8
|
||||
|
||||
if length != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Read ACK payload: class and ID of acknowledged message
|
||||
ackClass := d.readNextByte()
|
||||
ackID := d.readNextByte()
|
||||
|
||||
// Verify ACK is for our command (command[2] = class, command[3] = ID)
|
||||
if ackClass != command[2] || ackID != command[3] {
|
||||
continue
|
||||
}
|
||||
|
||||
if msgID == ubxACK_ACK {
|
||||
return nil
|
||||
}
|
||||
if msgID == ubxACK_NAK {
|
||||
return errGPSCommandRejected
|
||||
}
|
||||
}
|
||||
return errors.New("no ACK to GPS command")
|
||||
|
||||
return errNoACKToGPSCommand
|
||||
}
|
||||
|
||||
// appendChecksum calculates UBX checksum and appends it to the message
|
||||
func appendChecksum(msg []byte) []byte {
|
||||
var ckA, ckB byte
|
||||
// Checksum covers class, ID, length, and payload (skip sync chars)
|
||||
for i := 2; i < len(msg); i++ {
|
||||
ckA += msg[i]
|
||||
ckB += ckA
|
||||
}
|
||||
return append(msg, ckA, ckB)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
package gps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAppendChecksum(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input []byte
|
||||
expected []byte
|
||||
}{
|
||||
{
|
||||
name: "simple message",
|
||||
input: []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00},
|
||||
expected: []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00, 0x2A, 0x84},
|
||||
},
|
||||
{
|
||||
name: "CFG-NAV5 header only",
|
||||
input: []byte{0xB5, 0x62, 0x06, 0x24, 0x24, 0x00},
|
||||
expected: []byte{0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0x4E, 0xCC},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := appendChecksum(tc.input)
|
||||
|
||||
if len(result) != len(tc.expected) {
|
||||
t.Errorf("expected length %d, got %d", len(tc.expected), len(result))
|
||||
return
|
||||
}
|
||||
|
||||
// Check checksum bytes (last two bytes)
|
||||
ckA := result[len(result)-2]
|
||||
ckB := result[len(result)-1]
|
||||
expectedCkA := tc.expected[len(tc.expected)-2]
|
||||
expectedCkB := tc.expected[len(tc.expected)-1]
|
||||
|
||||
if ckA != expectedCkA || ckB != expectedCkB {
|
||||
t.Errorf("expected checksum 0x%02X 0x%02X, got 0x%02X 0x%02X",
|
||||
expectedCkA, expectedCkB, ckA, ckB)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendChecksumPreservesOriginal(t *testing.T) {
|
||||
input := []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00}
|
||||
original := make([]byte, len(input))
|
||||
copy(original, input)
|
||||
|
||||
result := appendChecksum(input)
|
||||
|
||||
// Verify original bytes are preserved
|
||||
for i := range input {
|
||||
if result[i] != original[i] {
|
||||
t.Errorf("byte %d changed: expected 0x%02X, got 0x%02X", i, original[i], result[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Verify two bytes were appended
|
||||
if len(result) != len(input)+2 {
|
||||
t.Errorf("expected length %d, got %d", len(input)+2, len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNav5CmdConfig(t *testing.T) {
|
||||
// Verify nav5Cmd has expected values
|
||||
if nav5Cmd.DynModel != 6 {
|
||||
t.Errorf("expected DynModel 6 (airborne <1g), got %d", nav5Cmd.DynModel)
|
||||
}
|
||||
|
||||
if nav5Cmd.FixMode != 3 {
|
||||
t.Errorf("expected FixMode 3 (auto 2D/3D), got %d", nav5Cmd.FixMode)
|
||||
}
|
||||
|
||||
expectedMask := CfgNav5Dyn | CfgNav5MinEl | CfgNav5PosFixMode
|
||||
if nav5Cmd.Mask != expectedMask {
|
||||
t.Errorf("expected Mask 0x%04X, got 0x%04X", expectedMask, nav5Cmd.Mask)
|
||||
}
|
||||
|
||||
if nav5Cmd.MinElev_deg != 5 {
|
||||
t.Errorf("expected MinElev_deg 5, got %d", nav5Cmd.MinElev_deg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGNSSDisableCmdConfig(t *testing.T) {
|
||||
// Verify GNSSDisableCmd has expected structure
|
||||
if gnssDisableCmd.MsgVer != 0 {
|
||||
t.Errorf("expected MsgVer 0, got %d", gnssDisableCmd.MsgVer)
|
||||
}
|
||||
|
||||
if gnssDisableCmd.NumTrkChHw != 0x20 {
|
||||
t.Errorf("expected NumTrkChHw 0x20, got 0x%02X", gnssDisableCmd.NumTrkChHw)
|
||||
}
|
||||
|
||||
if len(gnssDisableCmd.ConfigBlocks) != 5 {
|
||||
t.Errorf("expected 5 config blocks, got %d", len(gnssDisableCmd.ConfigBlocks))
|
||||
return
|
||||
}
|
||||
|
||||
// Verify GPS is enabled
|
||||
gpsBlock := gnssDisableCmd.ConfigBlocks[0]
|
||||
if gpsBlock.GnssId != 0 {
|
||||
t.Errorf("expected first block GnssId 0 (GPS), got %d", gpsBlock.GnssId)
|
||||
}
|
||||
if gpsBlock.Flags&CfgGnssEnable == 0 {
|
||||
t.Error("expected GPS to be enabled")
|
||||
}
|
||||
|
||||
// Verify other GNSS are disabled
|
||||
for i := 1; i < len(gnssDisableCmd.ConfigBlocks); i++ {
|
||||
block := gnssDisableCmd.ConfigBlocks[i]
|
||||
if block.Flags&CfgGnssEnable != 0 {
|
||||
t.Errorf("expected block %d (GnssId %d) to be disabled", i, block.GnssId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNav5CmdWrite(t *testing.T) {
|
||||
buf := make([]byte, 64)
|
||||
nav5Cmd.Put42Bytes(buf)
|
||||
|
||||
// Verify sync chars
|
||||
if buf[0] != 0xB5 || buf[1] != 0x62 {
|
||||
t.Errorf("expected sync 0xB5 0x62, got 0x%02X 0x%02X", buf[0], buf[1])
|
||||
}
|
||||
|
||||
// Verify class/id
|
||||
if buf[2] != 0x06 || buf[3] != 0x24 {
|
||||
t.Errorf("expected class/id 0x06 0x24, got 0x%02X 0x%02X", buf[2], buf[3])
|
||||
}
|
||||
|
||||
// Verify DynModel at offset 8
|
||||
if buf[8] != 6 {
|
||||
t.Errorf("expected DynModel 6, got %d", buf[8])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGNSSDisableCmdWrite(t *testing.T) {
|
||||
buf := make([]byte, 64)
|
||||
err := gnssDisableCmd.Put(buf)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error, likely buffer too short for data: %v", err)
|
||||
}
|
||||
|
||||
// 6 header + 4 payload header + 5*8 blocks = 50 bytes
|
||||
const expectedLen = 6 + 4 + 5*8
|
||||
sz := gnssDisableCmd.Size()
|
||||
if sz != expectedLen {
|
||||
t.Errorf("expected %d bytes, got %d", expectedLen, sz)
|
||||
}
|
||||
|
||||
// Verify sync chars
|
||||
if buf[0] != 0xB5 || buf[1] != 0x62 {
|
||||
t.Errorf("expected sync 0xB5 0x62, got 0x%02X 0x%02X", buf[0], buf[1])
|
||||
}
|
||||
|
||||
// Verify class/id
|
||||
if buf[2] != 0x06 || buf[3] != 0x3E {
|
||||
t.Errorf("expected class/id 0x06 0x3E, got 0x%02X 0x%02X", buf[2], buf[3])
|
||||
}
|
||||
|
||||
// Verify number of blocks
|
||||
if buf[9] != 5 {
|
||||
t.Errorf("expected 5 blocks, got %d", buf[9])
|
||||
}
|
||||
}
|
||||
|
||||
func TestChecksumCalculation(t *testing.T) {
|
||||
// Test with known UBX message and expected checksum
|
||||
// This is a minimal CFG-NAV5 poll message
|
||||
msg := []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00}
|
||||
|
||||
result := appendChecksum(msg)
|
||||
|
||||
// Verify checksum by recalculating
|
||||
var ckA, ckB byte
|
||||
for i := 2; i < len(msg); i++ {
|
||||
ckA += msg[i]
|
||||
ckB += ckA
|
||||
}
|
||||
|
||||
if result[6] != ckA || result[7] != ckB {
|
||||
t.Errorf("checksum mismatch: expected 0x%02X 0x%02X, got 0x%02X 0x%02X",
|
||||
ckA, ckB, result[6], result[7])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageRateCmdConfigs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
cmd CfgMsg1
|
||||
msgClass byte
|
||||
msgID byte
|
||||
rate byte
|
||||
}{
|
||||
{"GGA", messageRateGGACmd, 0xF0, 0x00, 1},
|
||||
{"GLL", messageRateGLLCmd, 0xF0, 0x01, 1},
|
||||
{"GSA", messageRateGSACmd, 0xF0, 0x02, 0},
|
||||
{"GSV", messageRateGSVCmd, 0xF0, 0x03, 0},
|
||||
{"RMC", messageRateRMCCmd, 0xF0, 0x04, 1},
|
||||
{"VTG", messageRateVTGCmd, 0xF0, 0x05, 0},
|
||||
{"ZDA", messageRateZDACmd, 0xF0, 0x08, 0},
|
||||
{"TXT", messageRateTXTCmd, 0xF0, 0x41, 0},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if tc.cmd.MsgClass != tc.msgClass {
|
||||
t.Errorf("expected MsgClass 0x%02X, got 0x%02X", tc.msgClass, tc.cmd.MsgClass)
|
||||
}
|
||||
if tc.cmd.MsgID != tc.msgID {
|
||||
t.Errorf("expected MsgID 0x%02X, got 0x%02X", tc.msgID, tc.cmd.MsgID)
|
||||
}
|
||||
if tc.cmd.Rate != tc.rate {
|
||||
t.Errorf("expected Rate %d, got %d", tc.rate, tc.cmd.Rate)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCfgMsg1Write(t *testing.T) {
|
||||
cmd := CfgMsg1{
|
||||
MsgClass: 0xF0,
|
||||
MsgID: 0x00,
|
||||
Rate: 1,
|
||||
}
|
||||
|
||||
buf := make([]byte, 16)
|
||||
cmd.Put9Bytes(buf)
|
||||
// Verify sync chars
|
||||
if buf[0] != 0xB5 || buf[1] != 0x62 {
|
||||
t.Errorf("expected sync 0xB5 0x62, got 0x%02X 0x%02X", buf[0], buf[1])
|
||||
}
|
||||
|
||||
// Verify class/id (0x06 0x01 for CFG-MSG)
|
||||
if buf[2] != 0x06 || buf[3] != 0x01 {
|
||||
t.Errorf("expected class/id 0x06 0x01, got 0x%02X 0x%02X", buf[2], buf[3])
|
||||
}
|
||||
|
||||
// Verify length (3 bytes payload)
|
||||
if buf[4] != 3 || buf[5] != 0 {
|
||||
t.Errorf("expected length 3, got %d", uint16(buf[4])|uint16(buf[5])<<8)
|
||||
}
|
||||
|
||||
// Verify payload
|
||||
if buf[6] != 0xF0 {
|
||||
t.Errorf("expected MsgClass 0xF0, got 0x%02X", buf[6])
|
||||
}
|
||||
if buf[7] != 0x00 {
|
||||
t.Errorf("expected MsgID 0x00, got 0x%02X", buf[7])
|
||||
}
|
||||
if buf[8] != 1 {
|
||||
t.Errorf("expected Rate 1, got %d", buf[8])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCfgMsg1ClassID(t *testing.T) {
|
||||
cmd := CfgMsg1{}
|
||||
if got := cmd.classID(); got != 0x0106 {
|
||||
t.Errorf("expected 0x0106, got 0x%04x", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMinimalMessageRatesConfig(t *testing.T) {
|
||||
// Verify the minimal config has correct rates set
|
||||
// GGA and RMC should be enabled (rate=1), others disabled (rate=0)
|
||||
expectedRates := map[byte]byte{
|
||||
0x00: 1, // GGA - enabled
|
||||
0x01: 1, // GLL - enabled
|
||||
0x02: 0, // GSA - disabled
|
||||
0x03: 0, // GSV - disabled
|
||||
0x04: 1, // RMC - enabled
|
||||
0x05: 0, // VTG - disabled
|
||||
0x08: 0, // ZDA - disabled
|
||||
0x41: 0, // TXT - disabled
|
||||
}
|
||||
|
||||
commands := []CfgMsg1{
|
||||
messageRateGGACmd,
|
||||
messageRateGLLCmd,
|
||||
messageRateGSACmd,
|
||||
messageRateGSVCmd,
|
||||
messageRateRMCCmd,
|
||||
messageRateVTGCmd,
|
||||
messageRateZDACmd,
|
||||
messageRateTXTCmd,
|
||||
}
|
||||
|
||||
for _, cmd := range commands {
|
||||
expectedRate, ok := expectedRates[cmd.MsgID]
|
||||
if !ok {
|
||||
t.Errorf("unexpected MsgID 0x%02X", cmd.MsgID)
|
||||
continue
|
||||
}
|
||||
if cmd.Rate != expectedRate {
|
||||
t.Errorf("MsgID 0x%02X: expected rate %d, got %d", cmd.MsgID, expectedRate, cmd.Rate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllMessageRatesWriteCorrectBytes(t *testing.T) {
|
||||
// Test that each message rate command writes the correct bytes
|
||||
commands := []CfgMsg1{
|
||||
messageRateGGACmd,
|
||||
messageRateGLLCmd,
|
||||
messageRateGSACmd,
|
||||
messageRateGSVCmd,
|
||||
messageRateRMCCmd,
|
||||
messageRateVTGCmd,
|
||||
messageRateZDACmd,
|
||||
messageRateTXTCmd,
|
||||
}
|
||||
|
||||
for _, cmd := range commands {
|
||||
buf := make([]byte, 16)
|
||||
cmd.Put9Bytes(buf)
|
||||
|
||||
// Verify MsgClass in payload
|
||||
if buf[6] != 0xF0 {
|
||||
t.Errorf("MsgID 0x%02X: expected MsgClass 0xF0, got 0x%02X", cmd.MsgID, buf[6])
|
||||
}
|
||||
|
||||
// Verify MsgID in payload
|
||||
if buf[7] != cmd.MsgID {
|
||||
t.Errorf("expected MsgID 0x%02X in payload, got 0x%02X", cmd.MsgID, buf[7])
|
||||
}
|
||||
|
||||
// Verify Rate in payload
|
||||
if buf[8] != cmd.Rate {
|
||||
t.Errorf("MsgID 0x%02X: expected Rate %d, got %d", cmd.MsgID, cmd.Rate, buf[8])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetMessageRatesAllEnabledModifiesRate(t *testing.T) {
|
||||
// Verify that when we copy a command and set Rate=1, it works correctly
|
||||
cmd := messageRateGSACmd // This one is disabled by default
|
||||
if cmd.Rate != 0 {
|
||||
t.Errorf("expected GSA default rate 0, got %d", cmd.Rate)
|
||||
}
|
||||
|
||||
// Simulate what SetMessageRatesAllEnabled does
|
||||
cmd.Rate = 1
|
||||
|
||||
buf := make([]byte, 16)
|
||||
cmd.Put9Bytes(buf)
|
||||
if buf[8] != 1 {
|
||||
t.Errorf("expected Rate 1 in buffer, got %d", buf[8])
|
||||
}
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
package gps
|
||||
|
||||
import "io"
|
||||
|
||||
// UBX message classes
|
||||
const (
|
||||
ubxClassACK = 0x05
|
||||
)
|
||||
|
||||
// UBX ACK message IDs
|
||||
const (
|
||||
ubxACK_NAK = 0x00 // Message not acknowledged
|
||||
ubxACK_ACK = 0x01 // Message acknowledged
|
||||
)
|
||||
|
||||
// UBX sync characters
|
||||
const (
|
||||
ubxSyncChar1 = 0xB5
|
||||
ubxSyncChar2 = 0x62
|
||||
)
|
||||
|
||||
const (
|
||||
DynModePortable = 0
|
||||
DynModeStationary = 2
|
||||
DynModePedestrian = 3
|
||||
DynModeAutomotive = 4
|
||||
DynModeSea = 5
|
||||
DynModeAirborne1g = 6
|
||||
DynModeAirborne2g = 7
|
||||
DynModeAirborne4g = 8
|
||||
DynModeWristWatch = 9
|
||||
DynModeBike = 10
|
||||
)
|
||||
|
||||
const (
|
||||
FixMode2D = 1
|
||||
FixMode3D = 2
|
||||
FixModeAuto = 3
|
||||
)
|
||||
|
||||
// from https://github.com/daedaleanai/ublox/blob/main/ubx/messages.go
|
||||
|
||||
// Message ubx-cfg-nav5
|
||||
|
||||
// CfgNav5 (Get/set) Navigation engine settings
|
||||
// Class/Id 0x06 0x24 (36 bytes)
|
||||
// See the Navigation Configuration Settings Description for a detailed description of how these settings affect receiver operation.
|
||||
type CfgNav5 struct {
|
||||
Mask CfgNav5Mask // Parameters bitmask. Only the masked parameters will be applied.
|
||||
DynModel byte // Dynamic platform model: 0: portable 2: stationary 3: pedestrian 4: automotive 5: sea 6: airborne with <1g acceleration 7: airborne with <2g acceleration 8: airborne with <4g acceleration 9: wrist-worn watch (not supported in protocol versions less than 18) 10: bike (supported in protocol versions 19. 2)
|
||||
FixMode byte // Position fixing mode: 1: 2D only 2: 3D only 3: auto 2D/3D
|
||||
FixedAlt_me2 int32 // [1e-2 m] Fixed altitude (mean sea level) for 2D fix mode
|
||||
FixedAltVar_m2e4 uint32 // [1e-4 m^2] Fixed altitude variance for 2D mode
|
||||
MinElev_deg int8 // [deg] Minimum elevation for a GNSS satellite to be used in NAV
|
||||
DrLimit_s byte // [s] Reserved
|
||||
PDop uint16 // Position DOP mask to use
|
||||
TDop uint16 // Time DOP mask to use
|
||||
PAcc_m uint16 // [m] Position accuracy mask
|
||||
TAcc_m uint16 // [m] Time accuracy mask
|
||||
StaticHoldThresh_cm_s byte // [cm/s] Static hold threshold
|
||||
DgnssTimeout_s byte // [s] DGNSS timeout
|
||||
CnoThreshNumSVs byte // Number of satellites required to have C/N0 above cnoThresh for a fix to be attempted
|
||||
CnoThresh_dbhz byte // [dBHz] C/N0 threshold for deciding whether to attempt a fix
|
||||
Reserved1 [2]byte // Reserved
|
||||
StaticHoldMaxDist_m uint16 // [m] Static hold distance threshold (before quitting static hold)
|
||||
UtcStandard byte // UTC standard to be used: 0: Automatic; receiver selects based on GNSS configuration (see GNSS time bases) 3: UTC as operated by the U.S. Naval Observatory (USNO); derived from GPS time 5: UTC as combined from multiple European laboratories; derived from Galileo time 6: UTC as operated by the former Soviet Union (SU); derived from GLONASS time 7: UTC as operated by the National Time Service Center (NTSC), China; derived from BeiDou time (not supported in protocol versions less than 16).
|
||||
Reserved2 [5]byte // Reserved
|
||||
}
|
||||
|
||||
func (CfgNav5) classID() uint16 { return 0x2406 }
|
||||
|
||||
type CfgNav5Mask uint16
|
||||
|
||||
var _ io.WriterTo = CfgNav5{} // compile time guarantee of interface implementation.
|
||||
|
||||
const (
|
||||
CfgNav5Dyn CfgNav5Mask = 0x1 // Apply dynamic model settings
|
||||
CfgNav5MinEl CfgNav5Mask = 0x2 // Apply minimum elevation settings
|
||||
CfgNav5PosFixMode CfgNav5Mask = 0x4 // Apply fix mode settings
|
||||
CfgNav5DrLim CfgNav5Mask = 0x8 // Reserved
|
||||
CfgNav5PosMask CfgNav5Mask = 0x10 // Apply position mask settings
|
||||
CfgNav5TimeMask CfgNav5Mask = 0x20 // Apply time mask settings
|
||||
CfgNav5StaticHoldMask CfgNav5Mask = 0x40 // Apply static hold settings
|
||||
CfgNav5DgpsMask CfgNav5Mask = 0x80 // Apply DGPS settings
|
||||
CfgNav5CnoThreshold CfgNav5Mask = 0x100 // Apply CNO threshold settings (cnoThresh, cnoThreshNumSVs)
|
||||
CfgNav5Utc CfgNav5Mask = 0x400 // Apply UTC settings (not supported in protocol versions less than 16).
|
||||
)
|
||||
|
||||
func (cfg CfgNav5) Append(dst []byte) []byte {
|
||||
var buf [42]byte
|
||||
cfg.Put42Bytes(buf[:])
|
||||
dst = append(dst, buf[:]...)
|
||||
return dst
|
||||
}
|
||||
|
||||
func (cfg CfgNav5) WriteTo(w io.Writer) (int64, error) {
|
||||
var buf [42]byte
|
||||
cfg.Put42Bytes(buf[:])
|
||||
n, err := w.Write(buf[:])
|
||||
return int64(n), err
|
||||
}
|
||||
|
||||
// Write CfgNav5 message to buffer
|
||||
func (cfg CfgNav5) Put42Bytes(buf []byte) {
|
||||
_ = buf[41]
|
||||
copy(buf, []byte{0xb5, 0x62, byte(cfg.classID()), byte(cfg.classID() >> 8), 36, 0})
|
||||
|
||||
buf[6] = byte(cfg.Mask)
|
||||
buf[7] = byte(cfg.Mask >> 8)
|
||||
buf[8] = cfg.DynModel
|
||||
buf[9] = cfg.FixMode
|
||||
buf[10] = byte(cfg.FixedAlt_me2)
|
||||
buf[11] = byte(cfg.FixedAlt_me2 >> 8)
|
||||
buf[12] = byte(cfg.FixedAlt_me2 >> 16)
|
||||
buf[13] = byte(cfg.FixedAlt_me2 >> 24)
|
||||
buf[14] = byte(cfg.FixedAltVar_m2e4)
|
||||
buf[15] = byte(cfg.FixedAltVar_m2e4 >> 8)
|
||||
buf[16] = byte(cfg.FixedAltVar_m2e4 >> 16)
|
||||
buf[17] = byte(cfg.FixedAltVar_m2e4 >> 24)
|
||||
buf[18] = byte(cfg.MinElev_deg)
|
||||
buf[19] = cfg.DrLimit_s
|
||||
buf[20] = byte(cfg.PDop)
|
||||
buf[21] = byte(cfg.PDop >> 8)
|
||||
buf[22] = byte(cfg.TDop)
|
||||
buf[23] = byte(cfg.TDop >> 8)
|
||||
buf[24] = byte(cfg.PAcc_m)
|
||||
buf[25] = byte(cfg.PAcc_m >> 8)
|
||||
buf[26] = byte(cfg.TAcc_m)
|
||||
buf[27] = byte(cfg.TAcc_m >> 8)
|
||||
buf[28] = cfg.StaticHoldThresh_cm_s
|
||||
buf[29] = cfg.DgnssTimeout_s
|
||||
buf[30] = cfg.CnoThreshNumSVs
|
||||
buf[31] = cfg.CnoThresh_dbhz
|
||||
copy(buf[32:34], cfg.Reserved1[:])
|
||||
buf[34] = byte(cfg.StaticHoldMaxDist_m)
|
||||
buf[35] = byte(cfg.StaticHoldMaxDist_m >> 8)
|
||||
buf[36] = cfg.UtcStandard
|
||||
copy(buf[37:42], cfg.Reserved2[:])
|
||||
}
|
||||
|
||||
// Message ubx-cfg-msg
|
||||
|
||||
// CfgMsg1 (Get/set) Set message rate
|
||||
// Class/Id 0x06 0x01 (3 bytes)
|
||||
// Set message rate configuration for the current port. See also section How to change between protocols.
|
||||
type CfgMsg1 struct {
|
||||
MsgClass byte // Message class
|
||||
MsgID byte // Message identifier
|
||||
Rate byte // Send rate on current port
|
||||
}
|
||||
|
||||
func (CfgMsg1) classID() uint16 { return 0x0106 }
|
||||
|
||||
func (cfg CfgMsg1) Put9Bytes(buf []byte) {
|
||||
copy(buf, []byte{0xb5, 0x62, byte(cfg.classID()), byte(cfg.classID() >> 8), 3, 0})
|
||||
buf[6] = cfg.MsgClass
|
||||
buf[7] = cfg.MsgID
|
||||
buf[8] = cfg.Rate
|
||||
}
|
||||
|
||||
// Message ubx-cfg-gnss
|
||||
|
||||
// CfgGnss (Get/set) GNSS system configuration
|
||||
// Class/Id 0x06 0x3e (4 + N*8 bytes)
|
||||
// Gets or sets the GNSS system channel sharing configuration. If the receiver is sent a valid new configuration, it will respond with a UBX-ACK- ACK message and immediately change to the new configuration. Otherwise the receiver will reject the request, by issuing a UBX-ACK-NAK and continuing operation with the previous configuration. Configuration requirements: It is necessary for at least one major GNSS to be enabled, after applying the new configuration to the current one. It is also required that at least 4 tracking channels are available to each enabled major GNSS, i.e. maxTrkCh must have a minimum value of 4 for each enabled major GNSS. The number of tracking channels in use must not exceed the number of tracking channels available in hardware, and the sum of all reserved tracking channels needs to be less than or equal to the number of tracking channels in use. Notes: To avoid cross-correlation issues, it is recommended that GPS and QZSS are always both enabled or both disabled. Polling this message returns the configuration of all supported GNSS, whether enabled or not; it may also include GNSS unsupported by the particular product, but in such cases the enable flag will always be unset. See section GNSS Configuration for a discussion of the use of this message. See section Satellite Numbering for a description of the GNSS IDs available. Configuration specific to the GNSS system can be done via other messages (e. g. UBX-CFG-SBAS).
|
||||
type CfgGnss struct {
|
||||
MsgVer byte // Message version (0x00 for this version)
|
||||
NumTrkChHw byte // Number of tracking channels available in hardware (read only)
|
||||
NumTrkChUse byte // (Read only in protocol versions greater than 23) Number of tracking channels to use. Must be > 0, <= numTrkChHw. If 0xFF, then number of tracking channels to use will be set to numTrkChHw.
|
||||
NumConfigBlocks byte `len:"ConfigBlocks"` // Number of configuration blocks following
|
||||
ConfigBlocks []CfgGnssConfigBlocksType // len: NumConfigBlocks
|
||||
}
|
||||
|
||||
func (CfgGnss) classID() uint16 { return 0x3e06 }
|
||||
|
||||
type CfgGnssConfigBlocksType struct {
|
||||
GnssId byte // System identifier (see Satellite Numbering )
|
||||
ResTrkCh byte // (Read only in protocol versions greater than 23) Number of reserved (minimum) tracking channels for this system.
|
||||
MaxTrkCh byte // (Read only in protocol versions greater than 23) Maximum number of tracking channels used for this system. Must be > 0, >= resTrkChn, <= numTrkChUse and <= maximum number of tracking channels supported for this system.
|
||||
Reserved1 byte // Reserved
|
||||
Flags CfgGnssFlags // Bitfield of flags. At least one signal must be configured in every enabled system.
|
||||
}
|
||||
|
||||
type CfgGnssFlags uint32
|
||||
|
||||
const (
|
||||
CfgGnssEnable CfgGnssFlags = 0x1 // Enable this system
|
||||
CfgGnssSigCfgMask CfgGnssFlags = 0xff0000 // Signal configuration mask When gnssId is 0 (GPS) 0x01 = GPS L1C/A 0x10 = GPS L2C 0x20 = GPS L5 When gnssId is 1 (SBAS) 0x01 = SBAS L1C/A When gnssId is 2 (Galileo) 0x01 = Galileo E1 (not supported in protocol versions less than 18) 0x10 = Galileo E5a 0x20 = Galileo E5b When gnssId is 3 (BeiDou) 0x01 = BeiDou B1I 0x10 = BeiDou B2I 0x80 = BeiDou B2A When gnssId is 4 (IMES) 0x01 = IMES L1 When gnssId is 5 (QZSS) 0x01 = QZSS L1C/A 0x04 = QZSS L1S 0x10 = QZSS L2C 0x20 = QZSS L5 When gnssId is 6 (GLONASS) 0x01 = GLONASS L1 0x10 = GLONASS L2
|
||||
)
|
||||
|
||||
// Write CfgGnss message to buffer
|
||||
func (cfg CfgGnss) Put(buf []byte) error {
|
||||
sz := cfg.Size()
|
||||
if sz > len(buf) {
|
||||
return io.ErrShortBuffer
|
||||
}
|
||||
copy(buf, []byte{0xb5, 0x62, byte(cfg.classID()), byte(cfg.classID() >> 8), 4 + byte(len(cfg.ConfigBlocks))*8, 0})
|
||||
buf[6] = cfg.MsgVer
|
||||
buf[7] = cfg.NumTrkChHw
|
||||
buf[8] = cfg.NumTrkChUse
|
||||
buf[9] = byte(len(cfg.ConfigBlocks))
|
||||
offset := 10
|
||||
for _, block := range cfg.ConfigBlocks {
|
||||
buf[offset] = block.GnssId
|
||||
buf[offset+1] = block.ResTrkCh
|
||||
buf[offset+2] = block.MaxTrkCh
|
||||
buf[offset+3] = block.Reserved1
|
||||
buf[offset+4] = byte(block.Flags)
|
||||
buf[offset+5] = byte(block.Flags >> 8)
|
||||
buf[offset+6] = byte(block.Flags >> 16)
|
||||
buf[offset+7] = byte(block.Flags >> 24)
|
||||
offset += 8
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Size returns length of CfgGnss in bytes when sent over the wire.
|
||||
func (cfg CfgGnss) Size() int {
|
||||
return 10 + 8*len(cfg.ConfigBlocks)
|
||||
}
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
package gps
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCfgNav5ClassID(t *testing.T) {
|
||||
cfg := CfgNav5{}
|
||||
if got := cfg.classID(); got != 0x2406 {
|
||||
t.Errorf("expected 0x2406, got 0x%04x", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCfgNav5Write(t *testing.T) {
|
||||
cfg := CfgNav5{
|
||||
Mask: CfgNav5Dyn | CfgNav5MinEl,
|
||||
DynModel: 4,
|
||||
FixMode: 3,
|
||||
FixedAlt_me2: 10000,
|
||||
FixedAltVar_m2e4: 10000,
|
||||
MinElev_deg: 5,
|
||||
DrLimit_s: 0,
|
||||
PDop: 250,
|
||||
TDop: 250,
|
||||
PAcc_m: 100,
|
||||
TAcc_m: 300,
|
||||
StaticHoldThresh_cm_s: 50,
|
||||
DgnssTimeout_s: 60,
|
||||
CnoThreshNumSVs: 3,
|
||||
CnoThresh_dbhz: 35,
|
||||
Reserved1: [2]byte{0, 0},
|
||||
StaticHoldMaxDist_m: 200,
|
||||
UtcStandard: 0,
|
||||
Reserved2: [5]byte{0, 0, 0, 0, 0},
|
||||
}
|
||||
|
||||
buf := make([]byte, 64)
|
||||
cfg.Put42Bytes(buf)
|
||||
|
||||
// Check sync chars
|
||||
if buf[0] != 0xb5 || buf[1] != 0x62 {
|
||||
t.Errorf("expected sync chars 0xb5 0x62, got 0x%02x 0x%02x", buf[0], buf[1])
|
||||
}
|
||||
|
||||
// Check class/id (little-endian)
|
||||
if buf[2] != 0x06 || buf[3] != 0x24 {
|
||||
t.Errorf("expected class/id 0x06 0x24, got 0x%02x 0x%02x", buf[2], buf[3])
|
||||
}
|
||||
|
||||
// Check length
|
||||
if buf[4] != 36 || buf[5] != 0 {
|
||||
t.Errorf("expected length 36, got %d", uint16(buf[4])|uint16(buf[5])<<8)
|
||||
}
|
||||
|
||||
// Check Mask (little-endian)
|
||||
mask := uint16(buf[6]) | uint16(buf[7])<<8
|
||||
if mask != uint16(CfgNav5Dyn|CfgNav5MinEl) {
|
||||
t.Errorf("expected mask 0x03, got 0x%04x", mask)
|
||||
}
|
||||
|
||||
// Check DynModel
|
||||
if buf[8] != 4 {
|
||||
t.Errorf("expected DynModel 4, got %d", buf[8])
|
||||
}
|
||||
|
||||
// Check FixMode
|
||||
if buf[9] != 3 {
|
||||
t.Errorf("expected FixMode 3, got %d", buf[9])
|
||||
}
|
||||
|
||||
// Check FixedAlt_me2 (little-endian int32)
|
||||
fixedAlt := int32(buf[10]) | int32(buf[11])<<8 | int32(buf[12])<<16 | int32(buf[13])<<24
|
||||
if fixedAlt != 10000 {
|
||||
t.Errorf("expected FixedAlt_me2 10000, got %d", fixedAlt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCfgGnssClassID(t *testing.T) {
|
||||
cfg := CfgGnss{}
|
||||
if got := cfg.classID(); got != 0x3e06 {
|
||||
t.Errorf("expected 0x3e06, got 0x%04x", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCfgGnssWrite(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
cfg CfgGnss
|
||||
expectedLen int
|
||||
expectedBlocks byte
|
||||
}{
|
||||
{
|
||||
name: "no config blocks",
|
||||
cfg: CfgGnss{
|
||||
MsgVer: 0,
|
||||
NumTrkChHw: 32,
|
||||
NumTrkChUse: 32,
|
||||
ConfigBlocks: nil,
|
||||
},
|
||||
expectedLen: 10,
|
||||
expectedBlocks: 0,
|
||||
},
|
||||
{
|
||||
name: "one config block",
|
||||
cfg: CfgGnss{
|
||||
MsgVer: 0,
|
||||
NumTrkChHw: 32,
|
||||
NumTrkChUse: 32,
|
||||
ConfigBlocks: []CfgGnssConfigBlocksType{
|
||||
{GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000},
|
||||
},
|
||||
},
|
||||
expectedLen: 18,
|
||||
expectedBlocks: 1,
|
||||
},
|
||||
{
|
||||
name: "two config blocks",
|
||||
cfg: CfgGnss{
|
||||
MsgVer: 0,
|
||||
NumTrkChHw: 32,
|
||||
NumTrkChUse: 32,
|
||||
ConfigBlocks: []CfgGnssConfigBlocksType{
|
||||
{GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000},
|
||||
{GnssId: 6, ResTrkCh: 8, MaxTrkCh: 14, Flags: CfgGnssEnable | 0x010000},
|
||||
},
|
||||
},
|
||||
expectedLen: 26,
|
||||
expectedBlocks: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
buf := make([]byte, 64)
|
||||
err := tc.cfg.Put(buf)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error, data too long?: %v", err)
|
||||
}
|
||||
// Check sync chars
|
||||
if buf[0] != 0xb5 || buf[1] != 0x62 {
|
||||
t.Errorf("expected sync chars 0xb5 0x62, got 0x%02x 0x%02x", buf[0], buf[1])
|
||||
}
|
||||
|
||||
// Check class/id (little-endian)
|
||||
if buf[2] != 0x06 || buf[3] != 0x3e {
|
||||
t.Errorf("expected class/id 0x06 0x3e, got 0x%02x 0x%02x", buf[2], buf[3])
|
||||
}
|
||||
|
||||
// Check number of config blocks
|
||||
if buf[9] != tc.expectedBlocks {
|
||||
t.Errorf("expected %d config blocks, got %d", tc.expectedBlocks, buf[9])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCfgGnssWriteBlockContent(t *testing.T) {
|
||||
cfg := CfgGnss{
|
||||
MsgVer: 0,
|
||||
NumTrkChHw: 32,
|
||||
NumTrkChUse: 32,
|
||||
ConfigBlocks: []CfgGnssConfigBlocksType{
|
||||
{GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Reserved1: 0, Flags: CfgGnssEnable | 0x010000},
|
||||
},
|
||||
}
|
||||
|
||||
buf := make([]byte, 64)
|
||||
err := cfg.Put(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Check first block at offset 10
|
||||
if buf[10] != 0 {
|
||||
t.Errorf("expected GnssId 0, got %d", buf[10])
|
||||
}
|
||||
if buf[11] != 8 {
|
||||
t.Errorf("expected ResTrkCh 8, got %d", buf[11])
|
||||
}
|
||||
if buf[12] != 16 {
|
||||
t.Errorf("expected MaxTrkCh 16, got %d", buf[12])
|
||||
}
|
||||
|
||||
// Check flags (little-endian uint32)
|
||||
flags := uint32(buf[14]) | uint32(buf[15])<<8 | uint32(buf[16])<<16 | uint32(buf[17])<<24
|
||||
expectedFlags := uint32(CfgGnssEnable | 0x010000)
|
||||
if flags != expectedFlags {
|
||||
t.Errorf("expected flags 0x%08x, got 0x%08x", expectedFlags, flags)
|
||||
}
|
||||
}
|
||||
+14
-10
@@ -60,16 +60,20 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
Bandwidth_7_8 = iota // 7.8 kHz
|
||||
Bandwidth_10_4 // 10.4 kHz
|
||||
Bandwidth_15_6 // 15.6 kHz
|
||||
Bandwidth_20_8 // 20.8 kHz
|
||||
Bandwidth_31_25 // 31.25 kHz
|
||||
Bandwidth_41_7 // 41.7 kHz
|
||||
Bandwidth_62_5 // 62.5 kHz
|
||||
Bandwidth_125_0 // 125.0 kHz
|
||||
Bandwidth_250_0 // 250.0 kHz
|
||||
Bandwidth_500_0 // 500.0 kHz
|
||||
Bandwidth_7_8 = iota // 7.8 kHz
|
||||
Bandwidth_10_4 // 10.4 kHz
|
||||
Bandwidth_15_6 // 15.6 kHz
|
||||
Bandwidth_20_8 // 20.8 kHz
|
||||
Bandwidth_31_25 // 31.25 kHz
|
||||
Bandwidth_41_7 // 41.7 kHz
|
||||
Bandwidth_62_5 // 62.5 kHz
|
||||
Bandwidth_125_0 // 125.0 kHz
|
||||
Bandwidth_203_125 // 203.125 kHz
|
||||
Bandwidth_250_0 // 250.0 kHz
|
||||
Bandwidth_406_25 // 406.25 kHz
|
||||
Bandwidth_500_0 // 500.0 kHz
|
||||
Bandwidth_812_5 // 812.5 kHz
|
||||
Bandwidth_1625_0 // 1625 kHz
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -3,8 +3,8 @@ package region
|
||||
import "tinygo.org/x/drivers/lora"
|
||||
|
||||
const (
|
||||
EU868_DEFAULT_PREAMBLE_LEN = 8
|
||||
EU868_DEFAULT_TX_POWER_DBM = 20
|
||||
EU868_DEFAULT_PREAMBLE_LEN = 8 // page 103 RP002-1.0.5
|
||||
EU868_DEFAULT_TX_POWER_DBM = 16 // page 36 RP002-1.0.5, 16 is the max
|
||||
)
|
||||
|
||||
type ChannelEU struct {
|
||||
|
||||
@@ -6,6 +6,9 @@ const (
|
||||
RadioEventTimeout
|
||||
RadioEventWatchdog
|
||||
RadioEventCrcError
|
||||
RadioEventValidHeader
|
||||
RadioEventCadDone
|
||||
RadioEventCadDetected
|
||||
RadioEventUnhandled
|
||||
)
|
||||
|
||||
|
||||
+45
-41
@@ -6,6 +6,7 @@
|
||||
package mcp2515 // import "tinygo.org/x/drivers/mcp2515"
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
@@ -15,15 +16,30 @@ import (
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNothingIsReceived = errors.New("readMsg: nothing is received")
|
||||
ErrRequestNewModeMaxTimeEx = errors.New("requestNewMode max time expired")
|
||||
ErrLengthIsLongerThanCapacity = errors.New("length is longer than capacity")
|
||||
ErrTxTimeout = errors.New("Tx: Tx timeout")
|
||||
ErrInvalidDirection = errors.New("invalid direction")
|
||||
ErrInvalidParameter = errors.New("invalid parameter")
|
||||
ErrCannotExpandBuffer = errors.New("cannot expand buffer (to avoid memory allocation)")
|
||||
)
|
||||
|
||||
// Device wraps MCP2515 SPI CAN Module.
|
||||
type Device struct {
|
||||
spi SPI
|
||||
cs pin.OutputFunc
|
||||
msg *CANMsg
|
||||
extended bool
|
||||
mcpMode byte
|
||||
configurePins func()
|
||||
}
|
||||
|
||||
type Configuration struct {
|
||||
Extended bool
|
||||
}
|
||||
|
||||
// CANMsg stores CAN message fields.
|
||||
type CANMsg struct {
|
||||
ID uint32
|
||||
@@ -56,10 +72,11 @@ func New(b drivers.SPI, csPin pin.Output) *Device {
|
||||
}
|
||||
|
||||
// Configure sets up the device for communication.
|
||||
func (d *Device) Configure() {
|
||||
func (d *Device) Configure(cfg Configuration) {
|
||||
if d.configurePins == nil {
|
||||
panic(legacy.ErrConfigBeforeInstantiated)
|
||||
}
|
||||
d.extended = cfg.Extended
|
||||
d.configurePins()
|
||||
}
|
||||
|
||||
@@ -117,9 +134,13 @@ func (d *Device) Tx(canid uint32, dlc uint8, data []byte) error {
|
||||
timeoutCount++
|
||||
}
|
||||
if timeoutCount == timeoutvalue {
|
||||
return fmt.Errorf("Tx: Tx timeout")
|
||||
return ErrTxTimeout
|
||||
}
|
||||
err = d.writeCANMsg(bufNum, canid, 0, 0, dlc, data)
|
||||
ext := byte(0)
|
||||
if d.extended {
|
||||
ext = 1
|
||||
}
|
||||
err = d.writeCANMsg(bufNum, canid, ext, 0, dlc, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -389,7 +410,7 @@ func (d *Device) configRate(speed, clock byte) error {
|
||||
set = false
|
||||
}
|
||||
if !set {
|
||||
return errors.New("invalid parameter")
|
||||
return ErrInvalidParameter
|
||||
}
|
||||
if err := d.setRegister(mcpCNF1, cfg1); err != nil {
|
||||
return err
|
||||
@@ -449,7 +470,7 @@ func (d *Device) readMsg() error {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("readMsg: nothing is received")
|
||||
return ErrNothingIsReceived
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -556,39 +577,22 @@ func (d *Device) writeCANMsg(bufNum uint8, canid uint32, ext, rtrBit, dlc uint8,
|
||||
}
|
||||
|
||||
func (s *SPI) setTxBufData(canid uint32, ext, rtrBit, dlc uint8, data []byte) error {
|
||||
canid = canid & 0x0FFFF
|
||||
var id [4]byte
|
||||
if ext == 1 {
|
||||
// TODO: add Extended ID
|
||||
err := s.setTxData(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.setTxData(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.setTxData(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.setTxData(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
canid = canid & extidBottom29Mask
|
||||
extended_id := canid
|
||||
high_11 := extended_id & extidTop11WriteMask
|
||||
low_18 := extended_id & extidBottom18Mask
|
||||
high_11 <<= 3
|
||||
extended_id_shifted := high_11 | low_18
|
||||
canid = extended_id_shifted | extidFlagMask
|
||||
} else {
|
||||
err := s.setTxData(byte(canid >> 3))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.setTxData(byte((canid & 0x07) << 5))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.setTxData(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.setTxData(0)
|
||||
canid = canid & stdidBottom11Mask
|
||||
canid <<= 16 + 5
|
||||
}
|
||||
binary.BigEndian.PutUint32(id[:], canid)
|
||||
for _, b := range id {
|
||||
err := s.setTxData(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -785,7 +789,7 @@ func (d *Device) requestNewMode(newMode byte) error {
|
||||
if r&modeMask == newMode {
|
||||
return nil
|
||||
} else if e := time.Now(); e.Sub(s) > 200*time.Millisecond {
|
||||
return errors.New("requestNewMode max time expired")
|
||||
return ErrRequestNewModeMaxTimeEx
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -861,23 +865,23 @@ func (s *SPI) clearBuffer(dir int) error { return s.setBufferLength(0, dir) }
|
||||
func (s *SPI) setBufferLength(length int, dir int) error {
|
||||
if dir == tx {
|
||||
if length > cap(s.tx) {
|
||||
return fmt.Errorf("length is longer than capacity")
|
||||
return ErrLengthIsLongerThanCapacity
|
||||
}
|
||||
s.tx = s.tx[:length]
|
||||
} else if dir == rx {
|
||||
if length > cap(s.rx) {
|
||||
return fmt.Errorf("length is longer than capacity")
|
||||
return ErrLengthIsLongerThanCapacity
|
||||
}
|
||||
s.rx = s.rx[:length]
|
||||
} else {
|
||||
return fmt.Errorf("invalid direction")
|
||||
return ErrInvalidDirection
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SPI) setTxData(data byte) error {
|
||||
if len(s.tx) >= bufferSize {
|
||||
return fmt.Errorf("cannot expand buffer (to avoid memory allocation)")
|
||||
return ErrCannotExpandBuffer
|
||||
}
|
||||
s.tx = append(s.tx, data)
|
||||
|
||||
|
||||
@@ -417,4 +417,11 @@ const (
|
||||
canFail = 0xff
|
||||
|
||||
canMaxCharInMessage = 8
|
||||
|
||||
// for extended id
|
||||
extidTop11WriteMask = 0x1FFC0000
|
||||
extidBottom29Mask = (1 << 29) - 1 // extended id bits
|
||||
extidBottom18Mask = (1 << 18) - 1 // bottom 18 bits
|
||||
stdidBottom11Mask = 0x7FF
|
||||
extidFlagMask = 1 << 19
|
||||
)
|
||||
|
||||
+12
-9
@@ -1,5 +1,4 @@
|
||||
// L2 data link layer
|
||||
|
||||
// package netlink provides an interface for L2 data link layer operations.
|
||||
package netlink
|
||||
|
||||
import (
|
||||
@@ -20,6 +19,7 @@ var (
|
||||
ErrNotSupported = errors.New("Not supported")
|
||||
)
|
||||
|
||||
// Event is a network event type passed to the callback registered with NetNotify.
|
||||
type Event int
|
||||
|
||||
// Network events
|
||||
@@ -38,6 +38,7 @@ const (
|
||||
ConnectModeAP // Connect as Wifi Access Point
|
||||
)
|
||||
|
||||
// AuthType is the type of WiFi authorization to use when connecting to an access point.
|
||||
type AuthType int
|
||||
|
||||
// Wifi authorization types. Used when setting up an access point, or
|
||||
@@ -49,10 +50,11 @@ const (
|
||||
AuthTypeWPA2Mixed // WPA2/WPA mixed authorization
|
||||
)
|
||||
|
||||
// DefaultConnectTimeout is the default timeout for connection attempts. This is used when ConnectParams.ConnectTimeout is zero.
|
||||
const DefaultConnectTimeout = 10 * time.Second
|
||||
|
||||
// ConnectParams is the set of parameters used to connect a Netlinker device to a network.
|
||||
type ConnectParams struct {
|
||||
|
||||
// Connect mode
|
||||
ConnectMode
|
||||
|
||||
@@ -81,22 +83,23 @@ type ConnectParams struct {
|
||||
// downed connection or hardware fault and try to recover the
|
||||
// connection. Set to zero to disable watchodog.
|
||||
WatchdogTimeout time.Duration
|
||||
|
||||
// Hostname to use for this device.
|
||||
Hostname string
|
||||
}
|
||||
|
||||
// Netlinker is TinyGo's OSI L2 data link layer interface. Network device
|
||||
// drivers implement Netlinker to expose the device's L2 functionality.
|
||||
|
||||
type Netlinker interface {
|
||||
|
||||
// Connect device to network
|
||||
// NetConnect connects the device to a network
|
||||
NetConnect(params *ConnectParams) error
|
||||
|
||||
// Disconnect device from network
|
||||
// NetDisconnect disconnects the device from the network
|
||||
NetDisconnect()
|
||||
|
||||
// Notify to register callback for network events
|
||||
// NetNotify registers a callback for network events
|
||||
NetNotify(cb func(Event))
|
||||
|
||||
// GetHardwareAddr returns device MAC address
|
||||
// GetHardwareAddr returns the device's MAC address
|
||||
GetHardwareAddr() (net.HardwareAddr, error)
|
||||
}
|
||||
|
||||
+145
-66
@@ -5,75 +5,154 @@ const AddressDefault = 0x60 // Assumes ADDR pin is low
|
||||
const AddressAlternative = 0x61 // Assumes ADDR pin is high
|
||||
|
||||
const (
|
||||
OUTPUT_ENABLE_CONTROL = 3
|
||||
XTAL_FREQ = 25000000
|
||||
PLL_FIXED = 80000000000
|
||||
FREQ_MULT = 100
|
||||
DEFAULT_CLK = 1000000000
|
||||
|
||||
CLK0_CONTROL = 16
|
||||
CLK1_CONTROL = 17
|
||||
CLK2_CONTROL = 18
|
||||
CLK3_CONTROL = 19
|
||||
CLK4_CONTROL = 20
|
||||
CLK5_CONTROL = 21
|
||||
CLK6_CONTROL = 22
|
||||
CLK7_CONTROL = 23
|
||||
PLL_VCO_MIN = 600000000
|
||||
PLL_VCO_MAX = 900000000
|
||||
MULTISYNTH_MIN_FREQ = 500000
|
||||
MULTISYNTH_DIVBY4_FREQ = 150000000
|
||||
MULTISYNTH_MAX_FREQ = 225000000
|
||||
MULTISYNTH_SHARE_MAX = 100000000
|
||||
MULTISYNTH_SHARE_MIN = 1024000
|
||||
MULTISYNTH67_MAX_FREQ = MULTISYNTH_DIVBY4_FREQ
|
||||
CLKOUT_MIN_FREQ = 4000
|
||||
CLKOUT_MAX_FREQ = MULTISYNTH_MAX_FREQ
|
||||
CLKOUT67_MS_MIN = PLL_VCO_MIN / MULTISYNTH67_A_MAX
|
||||
CLKOUT67_MIN_FREQ = CLKOUT67_MS_MIN / 128
|
||||
CLKOUT67_MAX_FREQ = MULTISYNTH67_MAX_FREQ
|
||||
|
||||
MULTISYNTH0_PARAMETERS_1 = 42
|
||||
MULTISYNTH0_PARAMETERS_3 = 44
|
||||
MULTISYNTH1_PARAMETERS_1 = 50
|
||||
MULTISYNTH1_PARAMETERS_3 = 52
|
||||
MULTISYNTH2_PARAMETERS_1 = 58
|
||||
MULTISYNTH2_PARAMETERS_3 = 60
|
||||
PLL_A_MIN = 15
|
||||
PLL_A_MAX = 90
|
||||
PLL_B_MAX = PLL_C_MAX - 1
|
||||
PLL_C_MAX = 1048575
|
||||
MULTISYNTH_A_MIN = 6
|
||||
MULTISYNTH_A_MAX = 1800
|
||||
MULTISYNTH67_A_MAX = 254
|
||||
MULTISYNTH_B_MAX = MULTISYNTH_C_MAX - 1
|
||||
MULTISYNTH_C_MAX = 1048575
|
||||
MULTISYNTH_P1_MAX = (1 << 18) - 1
|
||||
MULTISYNTH_P2_MAX = (1 << 20) - 1
|
||||
MULTISYNTH_P3_MAX = (1 << 20) - 1
|
||||
VCXO_PULL_MIN = 30
|
||||
VCXO_PULL_MAX = 240
|
||||
VCXO_MARGIN = 103
|
||||
|
||||
SPREAD_SPECTRUM_PARAMETERS = 149
|
||||
DEVICE_STATUS = 0
|
||||
INTERRUPT_STATUS = 1
|
||||
INTERRUPT_MASK = 2
|
||||
STATUS_SYS_INIT = 1 << 7
|
||||
STATUS_LOL_B = 1 << 6
|
||||
STATUS_LOL_A = 1 << 5
|
||||
STATUS_LOS = 1 << 4
|
||||
OUTPUT_ENABLE_CTRL = 3
|
||||
OEB_PIN_ENABLE_CTRL = 9
|
||||
PLL_INPUT_SOURCE = 15
|
||||
CLKIN_DIV_MASK = 3 << 6
|
||||
CLKIN_DIV_1 = 0 << 6
|
||||
CLKIN_DIV_2 = 1 << 6
|
||||
CLKIN_DIV_4 = 2 << 6
|
||||
CLKIN_DIV_8 = 3 << 6
|
||||
PLLB_SOURCE = 1 << 3
|
||||
PLLA_SOURCE = 1 << 2
|
||||
|
||||
PLL_RESET = 177
|
||||
CLK0_CTRL = 16
|
||||
CLK1_CTRL = 17
|
||||
CLK2_CTRL = 18
|
||||
CLK3_CTRL = 19
|
||||
CLK4_CTRL = 20
|
||||
CLK5_CTRL = 21
|
||||
CLK6_CTRL = 22
|
||||
CLK7_CTRL = 23
|
||||
CLK_POWERDOWN = 1 << 7
|
||||
CLK_INTEGER_MODE = 1 << 6
|
||||
CLK_PLL_SELECT = 1 << 5
|
||||
CLK_INVERT = 1 << 4
|
||||
CLK_INPUT_MASK = 3 << 2
|
||||
CLK_INPUT_XTAL = 0 << 2
|
||||
CLK_INPUT_CLKIN = 1 << 2
|
||||
CLK_INPUT_MULTISYNTH_0_4 = 2 << 2
|
||||
CLK_INPUT_MULTISYNTH_N = 3 << 2
|
||||
CLK_DRIVE_STRENGTH_MASK = 3 << 0
|
||||
CLK_DRIVE_STRENGTH_2MA = 0 << 0
|
||||
CLK_DRIVE_STRENGTH_4MA = 1 << 0
|
||||
CLK_DRIVE_STRENGTH_6MA = 2 << 0
|
||||
CLK_DRIVE_STRENGTH_8MA = 3 << 0
|
||||
|
||||
CRYSTAL_INTERNAL_LOAD_CAPACITANCE = 183
|
||||
)
|
||||
|
||||
const (
|
||||
CRYSTAL_LOAD_6PF = (1 << 6)
|
||||
CRYSTAL_LOAD_8PF = (2 << 6)
|
||||
CRYSTAL_LOAD_10PF = (3 << 6)
|
||||
)
|
||||
|
||||
const (
|
||||
CRYSTAL_FREQ_25MHZ = 25000000
|
||||
CRYSTAL_FREQ_27MHZ = 27000000
|
||||
)
|
||||
|
||||
const (
|
||||
PLL_A = iota
|
||||
PLL_B
|
||||
)
|
||||
|
||||
const (
|
||||
R_DIV_1 = iota
|
||||
R_DIV_2
|
||||
R_DIV_4
|
||||
R_DIV_8
|
||||
R_DIV_16
|
||||
R_DIV_32
|
||||
R_DIV_64
|
||||
R_DIV_128
|
||||
)
|
||||
|
||||
const (
|
||||
MULTISYNTH_DIV_4 = 4
|
||||
MULTISYNTH_DIV_6 = 6
|
||||
MULTISYNTH_DIV_8 = 8
|
||||
)
|
||||
|
||||
// Frequency constants (in Hz)
|
||||
const (
|
||||
CLKOUT_MIN_FREQ = 8000 // 8 kHz
|
||||
CLKOUT_MAX_FREQ = 150000000 // 150 MHz
|
||||
MULTISYNTH_MAX_FREQ = 150000000 // 150 MHz
|
||||
MULTISYNTH_SHARE_MAX = 100000000 // 100 MHz
|
||||
MULTISYNTH_DIVBY4_FREQ = 150000000 // 150 MHz
|
||||
PLL_VCO_MIN = 600000000 // 600 MHz
|
||||
PLL_VCO_MAX = 900000000 // 900 MHz
|
||||
)
|
||||
|
||||
const (
|
||||
SI5351_PLL_C_MAX = 1048575
|
||||
CLK3_0_DISABLE_STATE = 24
|
||||
CLK7_4_DISABLE_STATE = 25
|
||||
CLK_DISABLE_STATE_MASK = 3
|
||||
CLK_DISABLE_STATE_LOW = 0
|
||||
CLK_DISABLE_STATE_HIGH = 1
|
||||
CLK_DISABLE_STATE_FLOAT = 2
|
||||
CLK_DISABLE_STATE_NEVER = 3
|
||||
|
||||
PARAMETERS_LENGTH = 8
|
||||
PLLA_PARAMETERS = 26
|
||||
PLLB_PARAMETERS = 34
|
||||
CLK0_PARAMETERS = 42
|
||||
CLK1_PARAMETERS = 50
|
||||
CLK2_PARAMETERS = 58
|
||||
CLK3_PARAMETERS = 66
|
||||
CLK4_PARAMETERS = 74
|
||||
CLK5_PARAMETERS = 82
|
||||
CLK6_PARAMETERS = 90
|
||||
CLK7_PARAMETERS = 91
|
||||
CLK6_7_OUTPUT_DIVIDER = 92
|
||||
OUTPUT_CLK_DIV_MASK = 7 << 4
|
||||
OUTPUT_CLK6_DIV_MASK = 7 << 0
|
||||
OUTPUT_CLK_DIV_SHIFT = 4
|
||||
OUTPUT_CLK_DIV6_SHIFT = 0
|
||||
OUTPUT_CLK_DIV_1 = 0
|
||||
OUTPUT_CLK_DIV_2 = 1
|
||||
OUTPUT_CLK_DIV_4 = 2
|
||||
OUTPUT_CLK_DIV_8 = 3
|
||||
OUTPUT_CLK_DIV_16 = 4
|
||||
OUTPUT_CLK_DIV_32 = 5
|
||||
OUTPUT_CLK_DIV_64 = 6
|
||||
OUTPUT_CLK_DIV_128 = 7
|
||||
OUTPUT_CLK_DIVBY4 = 3 << 2
|
||||
|
||||
SSC_PARAM0 = 149
|
||||
SSC_PARAM1 = 150
|
||||
SSC_PARAM2 = 151
|
||||
SSC_PARAM3 = 152
|
||||
SSC_PARAM4 = 153
|
||||
SSC_PARAM5 = 154
|
||||
SSC_PARAM6 = 155
|
||||
SSC_PARAM7 = 156
|
||||
SSC_PARAM8 = 157
|
||||
SSC_PARAM9 = 158
|
||||
SSC_PARAM10 = 159
|
||||
SSC_PARAM11 = 160
|
||||
SSC_PARAM12 = 161
|
||||
|
||||
VXCO_PARAMETERS_LOW = 162
|
||||
VXCO_PARAMETERS_MID = 163
|
||||
VXCO_PARAMETERS_HIGH = 164
|
||||
|
||||
CLK0_PHASE_OFFSET = 165
|
||||
CLK1_PHASE_OFFSET = 166
|
||||
CLK2_PHASE_OFFSET = 167
|
||||
CLK3_PHASE_OFFSET = 168
|
||||
CLK4_PHASE_OFFSET = 169
|
||||
CLK5_PHASE_OFFSET = 170
|
||||
|
||||
PLL_RESET = 177
|
||||
PLL_RESET_B = 1 << 7
|
||||
PLL_RESET_A = 1 << 5
|
||||
|
||||
CRYSTAL_LOAD = 183
|
||||
CRYSTAL_LOAD_MASK = 3 << 6
|
||||
CRYSTAL_LOAD_0PF = 0 << 6
|
||||
CRYSTAL_LOAD_6PF = 1 << 6
|
||||
CRYSTAL_LOAD_8PF = 2 << 6
|
||||
CRYSTAL_LOAD_10PF = 3 << 6
|
||||
|
||||
FANOUT_ENABLE = 187
|
||||
CLKIN_ENABLE = 1 << 7
|
||||
XTAL_ENABLE = 1 << 6
|
||||
MULTISYNTH_ENABLE = 1 << 4
|
||||
)
|
||||
|
||||
+996
-556
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,214 @@
|
||||
package si5351
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSelectRDiv(t *testing.T) {
|
||||
d := &Device{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
freq Frequency
|
||||
wantDiv uint8
|
||||
wantFreq Frequency
|
||||
}{
|
||||
{"4kHz", 4000 * FREQ_MULT, OUTPUT_CLK_DIV_128, 4000 * FREQ_MULT * 128},
|
||||
{"8kHz", 8000 * FREQ_MULT, OUTPUT_CLK_DIV_64, 8000 * FREQ_MULT * 64},
|
||||
{"16kHz", 16000 * FREQ_MULT, OUTPUT_CLK_DIV_32, 16000 * FREQ_MULT * 32},
|
||||
{"32kHz", 32000 * FREQ_MULT, OUTPUT_CLK_DIV_16, 32000 * FREQ_MULT * 16},
|
||||
{"64kHz", 64000 * FREQ_MULT, OUTPUT_CLK_DIV_8, 64000 * FREQ_MULT * 8},
|
||||
{"128kHz", 128000 * FREQ_MULT, OUTPUT_CLK_DIV_4, 128000 * FREQ_MULT * 4},
|
||||
{"256kHz", 256000 * FREQ_MULT, OUTPUT_CLK_DIV_2, 256000 * FREQ_MULT * 2},
|
||||
{"512kHz", 512000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 512000 * FREQ_MULT},
|
||||
{"1MHz", 1000000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 1000000 * FREQ_MULT},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
freq := tt.freq
|
||||
freq, gotDiv := d.selectRDiv(freq)
|
||||
if gotDiv != tt.wantDiv {
|
||||
t.Errorf("selectRDiv() div = %v, want %v", gotDiv, tt.wantDiv)
|
||||
}
|
||||
if freq != tt.wantFreq {
|
||||
t.Errorf("selectRDiv() freq = %v, want %v", freq, tt.wantFreq)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectRDivMS67(t *testing.T) {
|
||||
d := &Device{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
freq Frequency
|
||||
wantDiv uint8
|
||||
wantFreq Frequency
|
||||
}{
|
||||
{"4kHz", 4000 * FREQ_MULT, OUTPUT_CLK_DIV_128, 4000 * FREQ_MULT * 128},
|
||||
{"8kHz", 8000 * FREQ_MULT, OUTPUT_CLK_DIV_64, 8000 * FREQ_MULT * 64},
|
||||
{"16kHz", 16000 * FREQ_MULT, OUTPUT_CLK_DIV_32, 16000 * FREQ_MULT * 32},
|
||||
{"64kHz", 64000 * FREQ_MULT, OUTPUT_CLK_DIV_8, 64000 * FREQ_MULT * 8},
|
||||
{"256kHz", 256000 * FREQ_MULT, OUTPUT_CLK_DIV_2, 256000 * FREQ_MULT * 2},
|
||||
{"512kHz", 512000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 512000 * FREQ_MULT},
|
||||
{"1MHz", 1000000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 1000000 * FREQ_MULT},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
freq := tt.freq
|
||||
freq, gotDiv := d.selectRDivMS67(freq)
|
||||
if gotDiv != tt.wantDiv {
|
||||
t.Errorf("selectRDivMS67() div = %v, want %v", gotDiv, tt.wantDiv)
|
||||
}
|
||||
if freq != tt.wantFreq {
|
||||
t.Errorf("selectRDivMS67() freq = %v, want %v", freq, tt.wantFreq)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculatePLL(t *testing.T) {
|
||||
d := &Device{}
|
||||
d.crystalFreq[0] = 25000000
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
freq Frequency
|
||||
wantMin Frequency
|
||||
wantMax Frequency
|
||||
}{
|
||||
{"600MHz", 600000000 * FREQ_MULT, 599000000 * FREQ_MULT, 601000000 * FREQ_MULT},
|
||||
{"750MHz", 750000000 * FREQ_MULT, 749000000 * FREQ_MULT, 751000000 * FREQ_MULT},
|
||||
{"900MHz", 900000000 * FREQ_MULT, 899000000 * FREQ_MULT, 901000000 * FREQ_MULT},
|
||||
{"BelowMin", 500000000 * FREQ_MULT, 600000000 * FREQ_MULT, 600000000 * FREQ_MULT},
|
||||
{"AboveMax", 1000000000 * FREQ_MULT, 900000000 * FREQ_MULT, 900000000 * FREQ_MULT},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, reg := d.CalculatePLL(PLL_A, tt.freq, 0, false)
|
||||
if got < tt.wantMin || got > tt.wantMax {
|
||||
t.Errorf("CalculatePLL() = %v, want between %v and %v", got, tt.wantMin, tt.wantMax)
|
||||
}
|
||||
if reg.p1 == 0 || reg.p3 == 0 {
|
||||
t.Errorf("CalculatePLL() invalid register values: p1=%v, p2=%v, p3=%v", reg.p1, reg.p2, reg.p3)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateMultisynth(t *testing.T) {
|
||||
d := &Device{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
freq Frequency
|
||||
pllFreq Frequency
|
||||
wantDiv bool
|
||||
}{
|
||||
{"10MHz from 800MHz", 10000000 * FREQ_MULT, 800000000 * FREQ_MULT, false},
|
||||
{"1MHz from 800MHz", 1000000 * FREQ_MULT, 800000000 * FREQ_MULT, false},
|
||||
{"Auto PLL 10MHz", 10000000 * FREQ_MULT, 0, false},
|
||||
{"150MHz DivBy4", 150000000 * FREQ_MULT, 600000000 * FREQ_MULT, true},
|
||||
{"BelowMin", 100000 * FREQ_MULT, 800000000 * FREQ_MULT, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, reg := d.CalculateMultisynth(tt.freq, tt.pllFreq)
|
||||
if tt.pllFreq == 0 {
|
||||
// Auto mode should return a valid PLL frequency
|
||||
if got < PLL_VCO_MIN*FREQ_MULT || got > PLL_VCO_MAX*FREQ_MULT {
|
||||
t.Errorf("CalculateMultisynth() returned invalid PLL freq %v", got)
|
||||
}
|
||||
}
|
||||
if reg.p3 == 0 {
|
||||
t.Errorf("CalculateMultisynth() p3 should not be 0")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultisynth67Calc(t *testing.T) {
|
||||
d := &Device{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
freq Frequency
|
||||
pllFreq Frequency
|
||||
wantErr bool
|
||||
}{
|
||||
{"10MHz Auto", 10000000 * FREQ_MULT, 0, false},
|
||||
{"100MHz Auto", 100000000 * FREQ_MULT, 0, false},
|
||||
{"100MHz from 800MHz", 100000000 * FREQ_MULT, 800000000 * FREQ_MULT, false},
|
||||
{"Invalid Division", 10000000 * FREQ_MULT, 777000000 * FREQ_MULT, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, reg := d.multisynth67Calc(tt.freq, tt.pllFreq)
|
||||
if tt.pllFreq == 0 {
|
||||
if got < PLL_VCO_MIN*FREQ_MULT || got > PLL_VCO_MAX*FREQ_MULT {
|
||||
t.Errorf("multisynth67Calc() returned invalid PLL freq %v", got)
|
||||
}
|
||||
} else if tt.wantErr {
|
||||
if got != 0 {
|
||||
t.Errorf("multisynth67Calc() should return 0 for invalid division, got %v", got)
|
||||
}
|
||||
}
|
||||
if reg.p1 == 0 && !tt.wantErr {
|
||||
t.Errorf("multisynth67Calc() p1 should not be 0")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCorrection(t *testing.T) {
|
||||
// Skip this test as it requires a mock I2C bus
|
||||
t.Skip("Requires mock I2C bus implementation")
|
||||
}
|
||||
|
||||
func TestSetRefFreq(t *testing.T) {
|
||||
d := &Device{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
freq CrystalFrequency
|
||||
wantFreq CrystalFrequency
|
||||
wantDiv uint8
|
||||
}{
|
||||
{"25MHz", 25000000, 25000000, CLKIN_DIV_1},
|
||||
{"50MHz", 50000000, 25000000, CLKIN_DIV_2},
|
||||
{"100MHz", 100000000, 25000000, CLKIN_DIV_4},
|
||||
{"30MHz", 30000000, 30000000, CLKIN_DIV_1},
|
||||
{"60MHz", 60000000, 30000000, CLKIN_DIV_2},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
d.SetReferenceFrequency(PLLInputClockIn, tt.freq)
|
||||
if d.crystalFreq[PLLInputClockIn] != tt.wantFreq {
|
||||
t.Errorf("SetReferenceFrequency() freq = %v, want %v", d.crystalFreq[PLLInputClockIn], tt.wantFreq)
|
||||
}
|
||||
if d.clkinDiv != tt.wantDiv {
|
||||
t.Errorf("SetReferenceFrequency() clkinDiv = %v, want %v", d.clkinDiv, tt.wantDiv)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCorrection(t *testing.T) {
|
||||
d := &Device{}
|
||||
d.refCorrection[PLLInputXO] = 5000
|
||||
d.refCorrection[PLLInputClockIn] = -3000
|
||||
|
||||
if got := d.GetCorrection(PLLInputXO); got != 5000 {
|
||||
t.Errorf("GetCorrection(PLLInputXO) = %v, want 5000", got)
|
||||
}
|
||||
if got := d.GetCorrection(PLLInputClockIn); got != -3000 {
|
||||
t.Errorf("GetCorrection(PLLInputClockIn) = %v, want -3000", got)
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,7 @@ tinygo build -size short -o ./build/test.bin -target=m5stamp-c3 ./examp
|
||||
tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/is31fl3731/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=arduino ./examples/ws2812
|
||||
tinygo build -size short -o ./build/test.hex -target=digispark ./examples/ws2812
|
||||
tinygo build -size short -o ./build/test.bin -target=xiao-esp32c3 ./examples/ws2812
|
||||
tinygo build -size short -o ./build/test.hex -target=trinket-m0 ./examples/bme280/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/microphone/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/buzzer/main.go
|
||||
@@ -124,9 +125,11 @@ tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/xpt2046/mai
|
||||
tinygo build -size short -o ./build/test.elf -target=m5stack-core2 ./examples/ft6336/basic/
|
||||
tinygo build -size short -o ./build/test.elf -target=m5stack-core2 ./examples/ft6336/touchpaint/
|
||||
tinygo build -size short -o ./build/test.hex -target=nucleo-wl55jc ./examples/sx126x/lora_rxtx/
|
||||
tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/sx127x/lora_rxtx/
|
||||
tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/ssd1289/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=pico ./examples/irremote/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=badger2040 ./examples/uc8151/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=badger2040 ./examples/waveshare-epd/epd2in9v2/main.go
|
||||
tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/scd4x/main.go
|
||||
tinygo build -size short -o ./build/test.uf2 -target=circuitplay-express ./examples/makeybutton/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/ds18b20/main.go
|
||||
|
||||
+19
-16
@@ -5,12 +5,13 @@ package st7735 // import "tinygo.org/x/drivers/st7735"
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"errors"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/legacy"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
"tinygo.org/x/drivers/pixel"
|
||||
)
|
||||
|
||||
@@ -39,10 +40,10 @@ type Device = DeviceOf[pixel.RGB565BE]
|
||||
// formats.
|
||||
type DeviceOf[T Color] struct {
|
||||
bus drivers.SPI
|
||||
dcPin machine.Pin
|
||||
resetPin machine.Pin
|
||||
csPin machine.Pin
|
||||
blPin machine.Pin
|
||||
dcPin pin.OutputFunc
|
||||
resetPin pin.OutputFunc
|
||||
csPin pin.OutputFunc
|
||||
blPin pin.OutputFunc
|
||||
width int16
|
||||
height int16
|
||||
columnOffset int16
|
||||
@@ -65,23 +66,25 @@ type Config struct {
|
||||
}
|
||||
|
||||
// New creates a new ST7735 connection. The SPI wire must already be configured.
|
||||
func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device {
|
||||
func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin pin.Output) Device {
|
||||
return NewOf[pixel.RGB565BE](bus, resetPin, dcPin, csPin, blPin)
|
||||
}
|
||||
|
||||
// NewOf creates a new ST7735 connection with a particular pixel format. The SPI
|
||||
// wire must already be configured.
|
||||
func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) DeviceOf[T] {
|
||||
dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
blPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin pin.Output) DeviceOf[T] {
|
||||
// IMPORTANT: pin configuration should really be done outside of this driver,
|
||||
// but for backwards compatibility with existing code, we do it here.
|
||||
legacy.ConfigurePinOut(dcPin)
|
||||
legacy.ConfigurePinOut(resetPin)
|
||||
legacy.ConfigurePinOut(csPin)
|
||||
legacy.ConfigurePinOut(blPin)
|
||||
return DeviceOf[T]{
|
||||
bus: bus,
|
||||
dcPin: dcPin,
|
||||
resetPin: resetPin,
|
||||
csPin: csPin,
|
||||
blPin: blPin,
|
||||
dcPin: dcPin.Set,
|
||||
resetPin: resetPin.Set,
|
||||
csPin: csPin.Set,
|
||||
blPin: blPin.Set,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,7 +426,7 @@ func (d *DeviceOf[T]) Data(data uint8) {
|
||||
|
||||
// Tx sends data to the display
|
||||
func (d *DeviceOf[T]) Tx(data []byte, isCommand bool) {
|
||||
d.dcPin.Set(!isCommand)
|
||||
d.dcPin(!isCommand)
|
||||
d.bus.Tx(data, nil)
|
||||
}
|
||||
|
||||
|
||||
+25
-5
@@ -513,8 +513,8 @@ func (d *DeviceOf[T]) setRotation(rotation Rotation) error {
|
||||
d.columnOffset = 0
|
||||
case drivers.Rotation90:
|
||||
madctl = MADCTL_MX | MADCTL_MV
|
||||
d.rowOffset = 0
|
||||
d.columnOffset = 0
|
||||
d.rowOffset = d.columnOffsetCfg
|
||||
d.columnOffset = d.rowOffsetCfg
|
||||
case drivers.Rotation180:
|
||||
madctl = MADCTL_MX | MADCTL_MY
|
||||
d.rowOffset = d.rowOffsetCfg
|
||||
@@ -593,8 +593,18 @@ func (d *DeviceOf[T]) SetScrollArea(topFixedArea, bottomFixedArea int16) {
|
||||
// The screen doesn't use the full 320 pixel height.
|
||||
// Enlarge the bottom fixed area to fill the 320 pixel height, so that
|
||||
// bottomFixedArea starts from the visible bottom of the screen.
|
||||
topFixedArea += d.rowOffset
|
||||
bottomFixedArea += (320 - d.height) - d.rowOffset
|
||||
//
|
||||
// VSCRDEF/VSCRSADD always operate on physical frame memory rows (0-319),
|
||||
// regardless of MADCTL. For rotations with MV set (90°/270°), CASET
|
||||
// addresses physical rows due to row/column exchange, so the physical row
|
||||
// offset is d.columnOffset (= rowOffsetCfg). For other rotations,
|
||||
// d.rowOffset is the physical row offset.
|
||||
physRowOffset := d.rowOffset
|
||||
if d.rotation == drivers.Rotation90 || d.rotation == drivers.Rotation270 {
|
||||
physRowOffset = d.columnOffset
|
||||
}
|
||||
topFixedArea += physRowOffset
|
||||
bottomFixedArea += (320 - d.height) - physRowOffset
|
||||
}
|
||||
if d.rotation == drivers.Rotation180 {
|
||||
// The screen is rotated by 180°, so we have to switch the top and
|
||||
@@ -613,10 +623,20 @@ func (d *DeviceOf[T]) SetScrollArea(topFixedArea, bottomFixedArea int16) {
|
||||
|
||||
// SetScroll sets the vertical scroll address of the display.
|
||||
func (d *DeviceOf[T]) SetScroll(line int16) {
|
||||
if d.rotation == drivers.Rotation180 {
|
||||
switch d.rotation {
|
||||
case drivers.Rotation90:
|
||||
// With MV set, hardware scroll operates on physical rows, which map to the
|
||||
// visual X axis. Add the physical row offset (d.columnOffset = rowOffsetCfg)
|
||||
// so that line=0 addresses the first visible physical row.
|
||||
line = line + d.columnOffset
|
||||
case drivers.Rotation180:
|
||||
// The screen is rotated by 180°, so we have to invert the scroll line
|
||||
// (taking care of the RowOffset).
|
||||
line = (319 - d.rowOffset) - line
|
||||
case drivers.Rotation270:
|
||||
// With MV+MY, physical rows map to the visual X axis in reverse direction.
|
||||
// line=0 addresses the last physical row of the visible area.
|
||||
line = (d.columnOffset + d.height - 1) - line
|
||||
}
|
||||
d.buf[0] = uint8(line >> 8)
|
||||
d.buf[1] = uint8(line)
|
||||
|
||||
@@ -96,6 +96,12 @@ const (
|
||||
SX127X_OPMODE_RX_SINGLE = uint8(0x06)
|
||||
SX127X_OPMODE_CAD = uint8(0x07)
|
||||
|
||||
SX127X_OPMODE_LOW_FREQUENCY = uint8(0x4)
|
||||
|
||||
SX127X_OPMODE_MODULATION_MASK = uint8(0x60)
|
||||
SX127X_OPMODE_MODULATION_FSK = uint8(0x0)
|
||||
SX127X_OPMODE_MODULATION_OOK = uint8(0x20)
|
||||
|
||||
SX127X_LORA_MAC_PUBLIC_SYNCWORD = 0x34
|
||||
SX127X_LORA_MAC_PRIVATE_SYNCWORD = 0x14
|
||||
)
|
||||
|
||||
+38
-11
@@ -25,9 +25,9 @@ type Device struct {
|
||||
rstPin machine.Pin // GPIO for reset
|
||||
radioEventChan chan lora.RadioEvent // Channel for Receiving events
|
||||
loraConf lora.Config // Current Lora configuration
|
||||
controller RadioController // to manage interactions with the radio
|
||||
controller RadioController // to manage interrupts with the radio
|
||||
deepSleep bool // Internal Sleep state
|
||||
deviceType int // sx1261,sx1262,sx1268 (defaults sx1261)
|
||||
deviceType int // sx1272, sx1273, sx1276, sx1279 (defaults sx1276)
|
||||
spiTxBuf []byte // global Tx buffer to avoid heap allocations in interrupt
|
||||
spiRxBuf []byte // global Rx buffer to avoid heap allocations in interrupt
|
||||
}
|
||||
@@ -65,6 +65,11 @@ func (d *Device) SetRadioController(rc RadioController) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Specify device type (sx1272, sx1273, sx1276, sx1279)
|
||||
func (d *Device) SetDeviceType(devType int) {
|
||||
d.deviceType = devType
|
||||
}
|
||||
|
||||
// Reset re-initialize the sx127x device
|
||||
func (d *Device) Reset() {
|
||||
d.rstPin.Low()
|
||||
@@ -81,9 +86,11 @@ func (d *Device) DetectDevice() bool {
|
||||
|
||||
// ReadRegister reads register value
|
||||
func (d *Device) ReadRegister(reg uint8) uint8 {
|
||||
d.controller.SetNss(false)
|
||||
if d.controller != nil {
|
||||
d.controller.SetNss(false)
|
||||
}
|
||||
|
||||
// Send register
|
||||
//d.spiTxBuf = []byte{reg & 0x7f}
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, byte(reg&0x7f))
|
||||
d.spi.Tx(d.spiTxBuf, nil)
|
||||
@@ -91,13 +98,19 @@ func (d *Device) ReadRegister(reg uint8) uint8 {
|
||||
d.spiRxBuf = d.spiRxBuf[:0]
|
||||
d.spiRxBuf = append(d.spiRxBuf, 0)
|
||||
d.spi.Tx(nil, d.spiRxBuf)
|
||||
d.controller.SetNss(true)
|
||||
if d.controller != nil {
|
||||
d.controller.SetNss(true)
|
||||
}
|
||||
|
||||
return d.spiRxBuf[0]
|
||||
}
|
||||
|
||||
// WriteRegister writes value to register
|
||||
func (d *Device) WriteRegister(reg uint8, value uint8) uint8 {
|
||||
d.controller.SetNss(false)
|
||||
if d.controller != nil {
|
||||
d.controller.SetNss(false)
|
||||
}
|
||||
|
||||
// Send register
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, byte(reg|0x80))
|
||||
@@ -108,7 +121,10 @@ func (d *Device) WriteRegister(reg uint8, value uint8) uint8 {
|
||||
d.spiRxBuf = d.spiRxBuf[:0]
|
||||
d.spiRxBuf = append(d.spiRxBuf, 0)
|
||||
d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
d.controller.SetNss(true)
|
||||
if d.controller != nil {
|
||||
d.controller.SetNss(true)
|
||||
}
|
||||
|
||||
return d.spiRxBuf[0]
|
||||
}
|
||||
|
||||
@@ -119,9 +135,20 @@ func (d *Device) SetOpMode(mode uint8) {
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, new)
|
||||
}
|
||||
|
||||
// SetOpMode changes the sx1276 mode
|
||||
// SetOpModeLora changes the sx1276 mode to lora.
|
||||
func (d *Device) SetOpModeLora() {
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, SX127X_OPMODE_LORA)
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)|SX127X_OPMODE_LORA)
|
||||
}
|
||||
|
||||
// SetOpModeFsk changes the sx1276 mode to fsk/ook.
|
||||
func (d *Device) SetOpModeFsk() {
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)&^SX127X_OPMODE_LORA)
|
||||
}
|
||||
|
||||
// SetModulationType changes the modulation type (SX127X_OPMODE_MODULATION_FSK, SX127X_OPMODE_MODULATION_OOK)
|
||||
func (d *Device) SetModulationType(typ uint8) {
|
||||
cleared := d.ReadRegister(SX127X_REG_OP_MODE) &^ SX127X_OPMODE_MODULATION_MASK
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, cleared|typ)
|
||||
}
|
||||
|
||||
// GetVersion returns hardware version of sx1276 chipset
|
||||
@@ -244,9 +271,9 @@ func (d *Device) SetLowDataRateOptim(val uint8) {
|
||||
// SetLowFrequencyModeOn enables Low Data Rate Optimization
|
||||
func (d *Device) SetLowFrequencyModeOn(val bool) {
|
||||
if val {
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)|0x04)
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)|SX127X_OPMODE_LOW_FREQUENCY)
|
||||
} else {
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)&0xfb)
|
||||
d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)&^SX127X_OPMODE_LOW_FREQUENCY)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# SX128x Radio
|
||||
Radio from Semtech in the 2.4 GHz band. This driver uses SPI to communicate with the radio instead of the alternative UART interface.
|
||||
|
||||
## Supported Chips
|
||||
- [SX1280](https://www.semtech.com/products/wireless-rf/lora-connect/sx1280)
|
||||
- [SX1281](https://www.semtech.com/products/wireless-rf/lora-connect/sx1281)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package sx128x
|
||||
|
||||
const (
|
||||
// SX128X SPI commands
|
||||
cmdGetStatus = uint8(0xC0)
|
||||
|
||||
// Register Access Operations
|
||||
cmdWriteRegister = uint8(0x18)
|
||||
cmdReadRegister = uint8(0x19)
|
||||
|
||||
// Data Buffer Operations
|
||||
cmdWriteBuffer = uint8(0x1A)
|
||||
cmdReadBuffer = uint8(0x1B)
|
||||
|
||||
// Radio Operation Modes
|
||||
cmdSetSleep = uint8(0x84)
|
||||
cmdSetStandby = uint8(0x80)
|
||||
cmdSetFS = uint8(0xC1)
|
||||
cmdSetTx = uint8(0x83)
|
||||
cmdSetRx = uint8(0x82)
|
||||
cmdSetRxDutyCycle = uint8(0x94)
|
||||
cmdSetLongPreamble = uint8(0x9B)
|
||||
cmdSetCAD = uint8(0xC5)
|
||||
cmdSetTxContinuousWave = uint8(0xD1)
|
||||
cmdSetContinuousPreamble = uint8(0xD2)
|
||||
cmdSetAutoTx = uint8(0x98)
|
||||
cmdSetAutoFS = uint8(0x9E)
|
||||
|
||||
// Radio Configuration
|
||||
cmdSetPacketType = uint8(0x8A)
|
||||
cmdGetPacketType = uint8(0x03)
|
||||
cmdSetRFFrequency = uint8(0x86)
|
||||
cmdSetTxParams = uint8(0x8E)
|
||||
cmdSetCADParams = uint8(0x88)
|
||||
cmdSetBufferBaseAddress = uint8(0x8F)
|
||||
cmdSetModulationParams = uint8(0x8B)
|
||||
cmdSetPacketParams = uint8(0x8C)
|
||||
|
||||
// Communication Status Information
|
||||
cmdGetRxBufferStatus = uint8(0x17)
|
||||
cmdGetPacketStatus = uint8(0x1D)
|
||||
cmdGetRSSIInst = uint8(0x1F)
|
||||
|
||||
// IRQ Handling
|
||||
cmdSetDIOIRQParams = uint8(0x8D)
|
||||
cmdGetIRQStatus = uint8(0x15)
|
||||
cmdClearIRQStatus = uint8(0x97)
|
||||
|
||||
// Miscellaneous
|
||||
cmdSetRegulatorMode = uint8(0x96)
|
||||
cmdSetSaveContext = uint8(0xD5)
|
||||
)
|
||||
@@ -0,0 +1,356 @@
|
||||
package sx128x
|
||||
|
||||
type SleepConfig uint8
|
||||
type StandbyConfig uint8
|
||||
type PeriodBase uint8
|
||||
type PacketType uint8
|
||||
type RadioRampTime uint8
|
||||
type CadSymbolNum uint8
|
||||
|
||||
// GFSK Modulation Params
|
||||
type GFSKBLEBitrateBandwidth uint8
|
||||
type ModulationIndex uint8
|
||||
type ModulationShaping uint8
|
||||
|
||||
// GFSK Packet Params
|
||||
type GFSKPreambleLength uint8
|
||||
type GFSKSyncWordLength uint8
|
||||
type GFSKSyncWordMatch uint8
|
||||
type GFSKHeaderType uint8
|
||||
type GFSKCrcType uint8
|
||||
|
||||
// BLE Packet Params
|
||||
type BLEConnectionState uint8
|
||||
type BLECrcType uint8
|
||||
type BLETestPayload uint8
|
||||
|
||||
// FLRC Modulation Params
|
||||
type FLRCBitrateBandwidth uint8
|
||||
type FLRCCodingRate uint8
|
||||
|
||||
// FLRC Packet Params
|
||||
type FLRCPreambleLength uint8
|
||||
type FLRCSyncWordLength uint8
|
||||
type FLRCSyncWordMatch uint8
|
||||
type FLRCHeaderType uint8
|
||||
type FLRCCrcType uint8
|
||||
|
||||
// LoRa Modulation Params
|
||||
type LoRaSpreadingFactor uint8
|
||||
type LoRaBandwidth uint8
|
||||
type LoRaCodingRate uint8
|
||||
|
||||
// LoRa Packet Params
|
||||
type LoRaHeaderType uint8
|
||||
type LoRaCrcType uint8
|
||||
type LoRaIqType uint8
|
||||
|
||||
// Misc
|
||||
type RegulatorMode uint8
|
||||
type IRQMask = uint16
|
||||
type CircuitMode uint8
|
||||
type CommandStatus uint8
|
||||
|
||||
// Packet Status
|
||||
type GFSKPacketInfo uint8
|
||||
type BLEPacketInfo uint8
|
||||
type FLRCPacketInfo uint8
|
||||
|
||||
const (
|
||||
whiteningDisable = 0x00
|
||||
whiteningEnable = 0x08
|
||||
|
||||
// Circuit Mode
|
||||
circuitModeMask = uint8(0b11100000)
|
||||
CIRCUIT_MODE_STDBY_RC = CircuitMode(0x2)
|
||||
CIRCUIT_MODE_STDBY_XOSC = CircuitMode(0x3)
|
||||
CIRCUIT_MODE_FS = CircuitMode(0x4)
|
||||
CIRCUIT_MODE_RX = CircuitMode(0x5)
|
||||
CIRCUIT_MODE_TX = CircuitMode(0x6)
|
||||
|
||||
// Command Status
|
||||
commandStatusMask = uint8(0b00011100)
|
||||
COMMAND_STATUS_SUCCESS = CommandStatus(0x1)
|
||||
COMMAND_STATUS_DATA_AVAILABLE = CommandStatus(0x2)
|
||||
COMMAND_STATUS_TIMEOUT = CommandStatus(0x3)
|
||||
COMMAND_STATUS_PROCESSING_ERROR = CommandStatus(0x4)
|
||||
COMMAND_STATUS_EXECUTION_ERROR = CommandStatus(0x5)
|
||||
COMMAND_STATUS_TX_DONE = CommandStatus(0x6)
|
||||
|
||||
// SleepConfig
|
||||
SLEEP_DATA_BUFFER_RETAIN = SleepConfig(2)
|
||||
SLEEP_DATA_RAM_RETAIN = SleepConfig(1)
|
||||
|
||||
// StandbyConfig
|
||||
STANDBY_RC = StandbyConfig(0)
|
||||
STANDBY_XOSC = StandbyConfig(1)
|
||||
|
||||
// PeriodBase
|
||||
PERIOD_BASE_15_625_US = PeriodBase(0)
|
||||
PERIOD_BASE_62_5_US = PeriodBase(1)
|
||||
PERIOD_BASE_1_MS = PeriodBase(2)
|
||||
PERIOD_BASE_4_MS = PeriodBase(3)
|
||||
|
||||
// PacketType
|
||||
PACKET_TYPE_GFSK = PacketType(0x00) // default
|
||||
PACKET_TYPE_LORA = PacketType(0x01)
|
||||
PACKET_TYPE_RANGING = PacketType(0x02)
|
||||
PACKET_TYPE_FLRC = PacketType(0x03)
|
||||
PACKET_TYPE_BLE = PacketType(0x04)
|
||||
|
||||
// RampTime
|
||||
RADIO_RAMP_02_US = RadioRampTime(0x00)
|
||||
RADIO_RAMP_04_US = RadioRampTime(0x20)
|
||||
RADIO_RAMP_06_US = RadioRampTime(0x40)
|
||||
RADIO_RAMP_08_US = RadioRampTime(0x60)
|
||||
RADIO_RAMP_10_US = RadioRampTime(0x80)
|
||||
RADIO_RAMP_12_US = RadioRampTime(0xA0)
|
||||
RADIO_RAMP_16_US = RadioRampTime(0xC0)
|
||||
RADIO_RAMP_20_US = RadioRampTime(0xE0)
|
||||
|
||||
// CadSymbolNum
|
||||
LORA_CAD_01_SYMBOL = CadSymbolNum(0x00)
|
||||
LORA_CAD_02_SYMBOLS = CadSymbolNum(0x20)
|
||||
LORA_CAD_04_SYMBOLS = CadSymbolNum(0x40)
|
||||
LORA_CAD_08_SYMBOLS = CadSymbolNum(0x60)
|
||||
LORA_CAD_16_SYMBOLS = CadSymbolNum(0x80)
|
||||
|
||||
// GFSK Modulation Params
|
||||
// Bitrate + Bandwidth - same for BLE
|
||||
GFSK_BLE_BR_2_000_BW_2_4 = GFSKBLEBitrateBandwidth(0x04)
|
||||
GFSK_BLE_BR_1_600_BW_2_4 = GFSKBLEBitrateBandwidth(0x28)
|
||||
GFSK_BLE_BR_1_000_BW_2_4 = GFSKBLEBitrateBandwidth(0x4C)
|
||||
GFSK_BLE_BR_1_000_BW_1_2 = GFSKBLEBitrateBandwidth(0x45)
|
||||
GFSK_BLE_BR_0_800_BW_2_4 = GFSKBLEBitrateBandwidth(0x70)
|
||||
GFSK_BLE_BR_0_800_BW_1_2 = GFSKBLEBitrateBandwidth(0x69)
|
||||
GFSK_BLE_BR_0_500_BW_1_2 = GFSKBLEBitrateBandwidth(0x8D)
|
||||
GFSK_BLE_BR_0_500_BW_0_6 = GFSKBLEBitrateBandwidth(0x86)
|
||||
GFSK_BLE_BR_0_400_BW_1_2 = GFSKBLEBitrateBandwidth(0xB1)
|
||||
GFSK_BLE_BR_0_400_BW_0_6 = GFSKBLEBitrateBandwidth(0xAA)
|
||||
GFSK_BLE_BR_0_250_BW_0_6 = GFSKBLEBitrateBandwidth(0xCE)
|
||||
GFSK_BLE_BR_0_250_BW_0_3 = GFSKBLEBitrateBandwidth(0xC7)
|
||||
GFSK_BLE_BR_0_125_BW_0_3 = GFSKBLEBitrateBandwidth(0xEF)
|
||||
|
||||
// Modulation Index - same for BLE
|
||||
MOD_IND_0_35 = ModulationIndex(0x00)
|
||||
MOD_IND_0_5 = ModulationIndex(0x01)
|
||||
MOD_IND_0_75 = ModulationIndex(0x02)
|
||||
MOD_IND_1_00 = ModulationIndex(0x03)
|
||||
MOD_IND_1_25 = ModulationIndex(0x04)
|
||||
MOD_IND_1_50 = ModulationIndex(0x05)
|
||||
MOD_IND_1_75 = ModulationIndex(0x06)
|
||||
MOD_IND_2_00 = ModulationIndex(0x07)
|
||||
MOD_IND_2_25 = ModulationIndex(0x08)
|
||||
MOD_IND_2_50 = ModulationIndex(0x09)
|
||||
MOD_IND_2_75 = ModulationIndex(0x0A)
|
||||
MOD_IND_3_00 = ModulationIndex(0x0B)
|
||||
MOD_IND_3_25 = ModulationIndex(0x0C)
|
||||
MOD_IND_3_50 = ModulationIndex(0x0D)
|
||||
MOD_IND_3_75 = ModulationIndex(0x0E)
|
||||
MOD_IND_4_00 = ModulationIndex(0x0F)
|
||||
|
||||
// Modulation Shaping - same for BLE and FLRC
|
||||
MOD_SHAPING_OFF = ModulationShaping(0x00)
|
||||
MOD_SHAPING_1_0 = ModulationShaping(0x10)
|
||||
MOD_SHAPING_0_5 = ModulationShaping(0x20)
|
||||
|
||||
// GFSK Packet Params
|
||||
// Preamble Length
|
||||
GFSK_PREAMBLE_LENGTH_04_BITS = GFSKPreambleLength(0x00)
|
||||
GFSK_PREAMBLE_LENGTH_08_BITS = GFSKPreambleLength(0x10)
|
||||
GFSK_PREAMBLE_LENGTH_12_BITS = GFSKPreambleLength(0x20)
|
||||
GFSK_PREAMBLE_LENGTH_16_BITS = GFSKPreambleLength(0x30)
|
||||
GFSK_PREAMBLE_LENGTH_20_BITS = GFSKPreambleLength(0x40)
|
||||
GFSK_PREAMBLE_LENGTH_24_BITS = GFSKPreambleLength(0x50)
|
||||
GFSK_PREAMBLE_LENGTH_28_BITS = GFSKPreambleLength(0x60)
|
||||
GFSK_PREAMBLE_LENGTH_32_BITS = GFSKPreambleLength(0x70)
|
||||
|
||||
// Sync Word Length
|
||||
GFSK_SYNC_WORD_LEN_1_B = GFSKSyncWordLength(0x00)
|
||||
GFSK_SYNC_WORD_LEN_2_B = GFSKSyncWordLength(0x02)
|
||||
GFSK_SYNC_WORD_LEN_3_B = GFSKSyncWordLength(0x04)
|
||||
GFSK_SYNC_WORD_LEN_4_B = GFSKSyncWordLength(0x06)
|
||||
GFSK_SYNC_WORD_LEN_5_B = GFSKSyncWordLength(0x08)
|
||||
|
||||
// Sync Word Match
|
||||
GFSK_SYNCWORD_MATCH_OFF = GFSKSyncWordMatch(0x00)
|
||||
GFSK_SYNCWORD_MATCH_1 = GFSKSyncWordMatch(0x10)
|
||||
GFSK_SYNCWORD_MATCH_2 = GFSKSyncWordMatch(0x20)
|
||||
GFSK_SYNCWORD_MATCH_1_2 = GFSKSyncWordMatch(0x30)
|
||||
GFSK_SYNCWORD_MATCH_3 = GFSKSyncWordMatch(0x40)
|
||||
GFSK_SYNCWORD_MATCH_1_3 = GFSKSyncWordMatch(0x50)
|
||||
GFSK_SYNCWORD_MATCH_2_3 = GFSKSyncWordMatch(0x60)
|
||||
GFSK_SYNCWORD_MATCH_1_2_3 = GFSKSyncWordMatch(0x70)
|
||||
|
||||
// GFSK Header Type
|
||||
GFSK_HEADER_FIXED_LENGTH = GFSKHeaderType(0x00)
|
||||
GFSK_HEADER_VARIABLE_LENGTH = GFSKHeaderType(0x20)
|
||||
|
||||
// GFSK CRC Type
|
||||
GFSK_CRC_OFF = GFSKCrcType(0x00)
|
||||
GFSK_CRC_1_BYTE = GFSKCrcType(0x10)
|
||||
GFSK_CRC_2_BYTES = GFSKCrcType(0x20)
|
||||
|
||||
// BLE Packet Params
|
||||
// Connection State
|
||||
BLE_MASTER_SLAVE = BLEConnectionState(0x00)
|
||||
BLE_ADVERTISER = BLEConnectionState(0x02)
|
||||
BLE_TX_TEST_MODE = BLEConnectionState(0x04)
|
||||
BLE_RX_TEST_MODE = BLEConnectionState(0x06)
|
||||
BLE_RXTX_TEST_MODE = BLEConnectionState(0x08)
|
||||
|
||||
// CRC Type
|
||||
BLE_CRC_OFF = BLECrcType(0x00)
|
||||
BLE_CRC_3_BYTES = BLECrcType(0x10)
|
||||
|
||||
// BLE Test Payload
|
||||
BLE_PAYLOAD_PRBS_9 = BLETestPayload(0x00)
|
||||
BLE_PAYLOAD_EYELONG_1_0 = BLETestPayload(0x04)
|
||||
BLE_PAYLOAD_EYESHORT_1_0 = BLETestPayload(0x08)
|
||||
BLE_PAYLOAD_PRBS_15 = BLETestPayload(0x0C)
|
||||
BLE_PAYLOAD_ALL_1 = BLETestPayload(0x10)
|
||||
BLE_PAYLOAD_ALL_0 = BLETestPayload(0x14)
|
||||
BLE_PAYLOAD_EYELONG_0_1 = BLETestPayload(0x18)
|
||||
BLE_PAYLOAD_EYESHORT_0_1 = BLETestPayload(0x1C)
|
||||
|
||||
// FLRC Modulation Params
|
||||
// Bitrate + Bandwidth
|
||||
FLRC_BR_1_300_BW_1_2 = FLRCBitrateBandwidth(0x45)
|
||||
FLRC_BR_1_000_BW_1_2 = FLRCBitrateBandwidth(0x69)
|
||||
FLRC_BR_0_650_BW_0_6 = FLRCBitrateBandwidth(0x86)
|
||||
FLRC_BR_0_520_BW_0_6 = FLRCBitrateBandwidth(0xAA)
|
||||
FLRC_BR_0_325_BW_0_3 = FLRCBitrateBandwidth(0xC7)
|
||||
FLRC_BR_0_260_BW_0_3 = FLRCBitrateBandwidth(0xEB)
|
||||
|
||||
// Coding Rate
|
||||
FLRC_CR_1_2 = FLRCCodingRate(0x00) // 1/2
|
||||
FLRC_CR_3_4 = FLRCCodingRate(0x02) // 3/4
|
||||
FLRC_CR_1_0 = FLRCCodingRate(0x04) // 1
|
||||
|
||||
// FLRC Packet Params
|
||||
// Preamble Length
|
||||
FLRC_PREAMBLE_LENGTH_4_BITS = FLRCPreambleLength(0x00)
|
||||
FLRC_PREAMBLE_LENGTH_8_BITS = FLRCPreambleLength(0x10)
|
||||
FLRC_PREAMBLE_LENGTH_12_BITS = FLRCPreambleLength(0x20)
|
||||
FLRC_PREAMBLE_LENGTH_16_BITS = FLRCPreambleLength(0x30)
|
||||
FLRC_PREAMBLE_LENGTH_20_BITS = FLRCPreambleLength(0x40)
|
||||
FLRC_PREAMBLE_LENGTH_24_BITS = FLRCPreambleLength(0x50)
|
||||
FLRC_PREAMBLE_LENGTH_28_BITS = FLRCPreambleLength(0x60)
|
||||
FLRC_PREAMBLE_LENGTH_32_BITS = FLRCPreambleLength(0x70)
|
||||
|
||||
// Sync Word Length
|
||||
FLRC_SYNC_WORD_LEN_0 = FLRCSyncWordLength(0x00)
|
||||
FLRC_SYNC_WORD_LEN_32_BITS = FLRCSyncWordLength(0x04)
|
||||
|
||||
// Sync Word Match
|
||||
FLRC_SYNC_WORD_MATCH_DISABLE = FLRCSyncWordMatch(0x00) // Disable Sync Word
|
||||
FLRC_SYNC_WORD_MATCH_1 = FLRCSyncWordMatch(0x10) // Sync Word 1
|
||||
FLRC_SYNC_WORD_MATCH_2 = FLRCSyncWordMatch(0x20) // Sync Word 2
|
||||
FLRC_SYNC_WORD_MATCH_1_2 = FLRCSyncWordMatch(0x30) // Sync Word 1 or Sync Word 2
|
||||
FLRC_SYNC_WORD_MATCH_3 = FLRCSyncWordMatch(0x40) // Sync Word 3
|
||||
FLRC_SYNC_WORD_MATCH_1_3 = FLRCSyncWordMatch(0x50) // Sync Word 1 or Sync Word 3
|
||||
FLRC_SYNC_WORD_MATCH_2_3 = FLRCSyncWordMatch(0x60) // Sync Word 2 or Sync Word 3
|
||||
FLRC_SYNC_WORD_MATCH_1_2_3 = FLRCSyncWordMatch(0x70) // Sync Word 1 or Sync Word 2 or Sync Word 3
|
||||
|
||||
// Header Type
|
||||
FLRC_HEADER_FIXED_LENGTH = FLRCHeaderType(0x00)
|
||||
FLRC_HEADER_VARIABLE_LENGTH = FLRCHeaderType(0x20)
|
||||
|
||||
// CRC Type
|
||||
FLRC_CRC_OFF = FLRCCrcType(0x00)
|
||||
FLRC_CRC_1_BYTE = FLRCCrcType(0x10)
|
||||
FLRC_CRC_2_BYTES = FLRCCrcType(0x20)
|
||||
FLRC_CRC_3_BYTES = FLRCCrcType(0x30)
|
||||
|
||||
// LoRa Modulation Params
|
||||
|
||||
// SpreadingFactor
|
||||
LORA_SF_5 = LoRaSpreadingFactor(0x50)
|
||||
LORA_SF_6 = LoRaSpreadingFactor(0x60)
|
||||
LORA_SF_7 = LoRaSpreadingFactor(0x70)
|
||||
LORA_SF_8 = LoRaSpreadingFactor(0x80)
|
||||
LORA_SF_9 = LoRaSpreadingFactor(0x90)
|
||||
LORA_SF_10 = LoRaSpreadingFactor(0xA0)
|
||||
LORA_SF_11 = LoRaSpreadingFactor(0xB0)
|
||||
LORA_SF_12 = LoRaSpreadingFactor(0xC0)
|
||||
|
||||
// Bandwidth
|
||||
LORA_BW_1600 = LoRaBandwidth(0x0A)
|
||||
LORA_BW_800 = LoRaBandwidth(0x18)
|
||||
LORA_BW_400 = LoRaBandwidth(0x26)
|
||||
LORA_BW_200 = LoRaBandwidth(0x34)
|
||||
|
||||
// CodingRate
|
||||
LORA_CR_4_5 = LoRaCodingRate(0x01)
|
||||
LORA_CR_4_6 = LoRaCodingRate(0x02)
|
||||
LORA_CR_4_7 = LoRaCodingRate(0x03)
|
||||
LORA_CR_4_8 = LoRaCodingRate(0x04)
|
||||
LORA_CR_LI_4_5 = LoRaCodingRate(0x05)
|
||||
LORA_CR_LI_4_6 = LoRaCodingRate(0x06)
|
||||
LORA_CR_LI_4_8 = LoRaCodingRate(0x07)
|
||||
|
||||
// LoraPacketParams
|
||||
// HeaderType
|
||||
LORA_HEADER_EXPLICIT = LoRaHeaderType(0x00)
|
||||
LORA_HEADER_IMPLICIT = LoRaHeaderType(0x80)
|
||||
|
||||
// CRC Type
|
||||
LORA_CRC_ENABLE = LoRaCrcType(0x20)
|
||||
LORA_CRC_DISABLE = LoRaCrcType(0x00)
|
||||
|
||||
// IQ Type
|
||||
LORA_IQ_INVERTED = LoRaIqType(0x00)
|
||||
LORA_IQ_STD = LoRaIqType(0x40)
|
||||
|
||||
// RegulatorMode
|
||||
REGULATOR_LDO = RegulatorMode(0)
|
||||
REGULATOR_DC_DC = RegulatorMode(1)
|
||||
|
||||
// IRQ masks
|
||||
IRQ_ALL_MASK = IRQMask(0xFFFF)
|
||||
IRQ_NONE_MASK = IRQMask(0x0000)
|
||||
IRQ_TX_DONE_MASK = IRQMask(0b0000000000000001)
|
||||
IRQ_RX_DONE_MASK = IRQMask(0b0000000000000010)
|
||||
IRQ_SYNC_WORD_VALID_MASK = IRQMask(0b0000000000000100)
|
||||
IRQ_SYNC_WORD_ERROR_MASK = IRQMask(0b0000000000001000)
|
||||
IRQ_HEADER_VALID_MASK = IRQMask(0b0000000000010000)
|
||||
IRQ_HEADER_ERROR_MASK = IRQMask(0b0000000000100000)
|
||||
IRQ_CRC_ERROR_MASK = IRQMask(0b0000000001000000)
|
||||
IRQ_RANGING_SLAVE_RESPONSE_DONE_MASK = IRQMask(0b0000000010000000)
|
||||
IRQ_RANGING_SLAVE_RESPONSE_DISCARD_MASK = IRQMask(0b0000000100000000)
|
||||
IRQ_RANGING_MASTER_RESULT_VALID_MASK = IRQMask(0b0000001000000000)
|
||||
IRQ_RANGING_MASTER_TIMEOUT_MASK = IRQMask(0b0000010000000000)
|
||||
IRQ_RANGING_SLAVE_REQUEST_VALID_MASK = IRQMask(0b0000100000000000)
|
||||
IRQ_CAD_DONE_MASK = IRQMask(0b0001000000000000)
|
||||
IRQ_CAD_DETECTED_MASK = IRQMask(0b0010000000000000)
|
||||
IRQ_RX_TX_TIMEOUT_MASK = IRQMask(0b0100000000000000)
|
||||
IRQ_PREAMBLE_DETECTED_MASK = IRQMask(0b1000000000000000)
|
||||
IRQ_ADVANCED_RANGING_DONE_MASK = IRQMask(0b1000000000000000)
|
||||
|
||||
// GFSK Packet Info
|
||||
GFSK_SYNC_ERROR = GFSKPacketInfo(0b1000000)
|
||||
GFSK_LENGTH_ERROR = GFSKPacketInfo(0b0100000)
|
||||
GFSK_CRC_ERROR = GFSKPacketInfo(0b0010000)
|
||||
GFSK_ABORT_ERROR = GFSKPacketInfo(0b0001000)
|
||||
GFSK_HEADER_RECEIVED = GFSKPacketInfo(0b0000100)
|
||||
GFSK_PACKET_RECEIVED = GFSKPacketInfo(0b0000010)
|
||||
GFSK_PACKET_CRTL_BUSY = GFSKPacketInfo(0b0000001)
|
||||
|
||||
// BLE Packet Info
|
||||
BLE_SYNC_ERROR = BLEPacketInfo(0b1000000)
|
||||
BLE_LENGTH_ERROR = BLEPacketInfo(0b0100000)
|
||||
BLE_CRC_ERROR = BLEPacketInfo(0b0010000)
|
||||
BLE_ABORT_ERROR = BLEPacketInfo(0b0001000)
|
||||
BLE_HEADER_RECEIVED = BLEPacketInfo(0b0000100)
|
||||
BLE_PACKET_RECEIVED = BLEPacketInfo(0b0000010)
|
||||
BLE_PACKET_CRTL_BUSY = BLEPacketInfo(0b0000001)
|
||||
|
||||
// FLRC Packet Info
|
||||
FLRC_SYNC_ERROR = FLRCPacketInfo(0b1000000)
|
||||
FLRC_LENGTH_ERROR = FLRCPacketInfo(0b0100000)
|
||||
FLRC_CRC_ERROR = FLRCPacketInfo(0b0010000)
|
||||
FLRC_ABORT_ERROR = FLRCPacketInfo(0b0001000)
|
||||
FLRC_HEADER_RECEIVED = FLRCPacketInfo(0b0000100)
|
||||
FLRC_PACKET_RECEIVED = FLRCPacketInfo(0b0000010)
|
||||
FLRC_PACKET_CRTL_BUSY = FLRCPacketInfo(0b0000001)
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
package sx128x
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrBusyPinTimeout = errors.New("busy pin timeout")
|
||||
errDataTooLong = errors.New("data over 256 bytes")
|
||||
errInvalidSleepConfig = errors.New("invalid sleep config")
|
||||
errInvalidStandbyConfig = errors.New("invalid standby config")
|
||||
errFrequencyTooLow = errors.New("frequency below 2.4Ghz")
|
||||
errFrequencyTooHigh = errors.New("frequency above 2.5Ghz")
|
||||
errPowerTooLow = errors.New("power level below -18dBm")
|
||||
errPowerTooHigh = errors.New("power level above 13dBm")
|
||||
errInvalidPeriodBase = errors.New("invalid period base")
|
||||
errInvalidPacketType = errors.New("invalid packet type")
|
||||
errInvalidRegulatorMode = errors.New("invalid regulator mode")
|
||||
errPayloadLengthTooShort = errors.New("payload length too short")
|
||||
errPayloadLengthTooLong = errors.New("payload length too long")
|
||||
)
|
||||
@@ -0,0 +1,69 @@
|
||||
package sx128x
|
||||
|
||||
const (
|
||||
|
||||
// SX128X register map
|
||||
REG_FIRMWARE_VERSIONS = uint16(0x153)
|
||||
REG_RX_GAIN = uint16(0x891)
|
||||
REG_MANUAL_GAIN_SETTING = uint16(0x895)
|
||||
REG_LNA_GAIN_VALUE = uint16(0x89E)
|
||||
REG_LNA_GAIN_CONTROL = uint16(0x89F)
|
||||
REG_SYNCH_PEAK_ATTENUATION = uint16(0x8C2)
|
||||
REG_PAYLOAD_LENGTH = uint16(0x901)
|
||||
REG_LORA_HEADER_MODE = uint16(0x903)
|
||||
REG_RANGING_REQUEST_ADDRESS_BYTE_3 = uint16(0x912)
|
||||
REG_RANGING_REQUEST_ADDRESS_BYTE_2 = uint16(0x913)
|
||||
REG_RANGING_REQUEST_ADDRESS_BYTE_1 = uint16(0x914)
|
||||
REG_RANGING_REQUEST_ADDRESS_BYTE_0 = uint16(0x915)
|
||||
REG_RANGING_DEVICE_ADDRESS_BYTE_3 = uint16(0x916)
|
||||
REG_RANGING_DEVICE_ADDRESS_BYTE_2 = uint16(0x917)
|
||||
REG_RANGING_DEVICE_ADDRESS_BYTE_1 = uint16(0x918)
|
||||
REG_RANGING_DEVICE_ADDRESS_BYTE_0 = uint16(0x919)
|
||||
REG_RANGING_FILTER_WINDOW_SIZE = uint16(0x91E)
|
||||
REG_RESET_RANGING_FILTER = uint16(0x923)
|
||||
REG_RANGING_RESULT_MUX = uint16(0x924)
|
||||
REG_SF_ADDITIONAL_CONFIGURATION = uint16(0x925)
|
||||
REG_RANGING_CALIBRATION_BYTE_2 = uint16(0x92B)
|
||||
REG_RANGING_CALIBRATION_BYTE_1 = uint16(0x92C)
|
||||
REG_RANGING_CALIBRATION_BYTE_0 = uint16(0x92D)
|
||||
REG_RANGING_ID_CHECK_LENGTH = uint16(0x931)
|
||||
REG_FREQUENCY_ERROR_CORRECTION = uint16(0x93C)
|
||||
REG_CAD_DETECT_PEAK = uint16(0x942)
|
||||
REG_LORA_SYNC_WORD_MSB = uint16(0x944)
|
||||
REG_LORA_SYNC_WORD_LSB = uint16(0x945)
|
||||
REG_HEADER_CRC = uint16(0x954)
|
||||
REG_CODING_RATE = uint16(0x950)
|
||||
REG_FEI_BYTE_2 = uint16(0x954)
|
||||
REG_FEI_BYTE_1 = uint16(0x955)
|
||||
REG_FEI_BYTE_0 = uint16(0x956)
|
||||
REG_RANGING_RESULT_BYTE_2 = uint16(0x961)
|
||||
REG_RANGING_RESULT_BYTE_1 = uint16(0x962)
|
||||
REG_RANGING_RESULT_BYTE_0 = uint16(0x963)
|
||||
REG_RANGING_RSSI = uint16(0x964)
|
||||
REG_FREEZE_RANGING_RESULT = uint16(0x97F)
|
||||
REG_PACKET_PREAMBLE_SETTINGS = uint16(0x9C1)
|
||||
REG_WHITENING_INITIAL_VALUE = uint16(0x9C5)
|
||||
REG_CRC_POLYNOMIAL_DEFINITION_MSB = uint16(0x9C6)
|
||||
REG_CRC_POLYNOMIAL_DEFINITION_LSB = uint16(0x9C7)
|
||||
REG_CRC_POLYNOMIAL_SEED_BYTE_2 = uint16(0x9C7)
|
||||
REG_CRC_POLYNOMIAL_SEED_BYTE_1 = uint16(0x9C8)
|
||||
REG_CRC_POLYNOMIAL_SEED_BYTE_0 = uint16(0x9C9)
|
||||
REG_CRC_MSB_INITIAL_VALUE = uint16(0x9C8)
|
||||
REG_CRC_LSB_INITIAL_VALUE = uint16(0x9C9)
|
||||
REG_SYNC_ADDRESS_CONTROL = uint16(0x9CD)
|
||||
REG_SYNC_ADDRESS_1_BYTE_4 = uint16(0x9CE)
|
||||
REG_SYNC_ADDRESS_1_BYTE_3 = uint16(0x9CF)
|
||||
REG_SYNC_ADDRESS_1_BYTE_2 = uint16(0x9D0)
|
||||
REG_SYNC_ADDRESS_1_BYTE_1 = uint16(0x9D1)
|
||||
REG_SYNC_ADDRESS_1_BYTE_0 = uint16(0x9D2)
|
||||
REG_SYNC_ADDRESS_2_BYTE_4 = uint16(0x9D3)
|
||||
REG_SYNC_ADDRESS_2_BYTE_3 = uint16(0x9D4)
|
||||
REG_SYNC_ADDRESS_2_BYTE_2 = uint16(0x9D5)
|
||||
REG_SYNC_ADDRESS_2_BYTE_1 = uint16(0x9D6)
|
||||
REG_SYNC_ADDRESS_2_BYTE_0 = uint16(0x9D7)
|
||||
REG_SYNC_ADDRESS_3_BYTE_4 = uint16(0x9D8)
|
||||
REG_SYNC_ADDRESS_3_BYTE_3 = uint16(0x9D9)
|
||||
REG_SYNC_ADDRESS_3_BYTE_2 = uint16(0x9DA)
|
||||
REG_SYNC_ADDRESS_3_BYTE_1 = uint16(0x9DB)
|
||||
REG_SYNC_ADDRESS_3_BYTE_0 = uint16(0x9DC)
|
||||
)
|
||||
@@ -0,0 +1,768 @@
|
||||
package sx128x
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
spi drivers.SPI
|
||||
nssPin pin.Output
|
||||
resetPin pin.Output
|
||||
busyPin pin.Input
|
||||
spiTxBuf []byte
|
||||
spiRxBuf []byte
|
||||
}
|
||||
|
||||
func New(spi drivers.SPI, nssPin pin.Output, resetPin pin.Output, busyPin pin.Input) *Device {
|
||||
return &Device{
|
||||
spi: spi,
|
||||
nssPin: nssPin,
|
||||
resetPin: resetPin,
|
||||
busyPin: busyPin,
|
||||
spiTxBuf: make([]byte, 256), // TODO: optimize buffer size
|
||||
spiRxBuf: make([]byte, 256),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) Reset() {
|
||||
d.resetPin.Set(false)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
d.resetPin.Set(true)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (d *Device) WaitWhileBusy(timeout time.Duration) error {
|
||||
// largest busy period is on boot with around ~400ish this should be more than enough
|
||||
now := time.Now()
|
||||
for d.busyPin.Get() {
|
||||
if time.Since(now) > timeout {
|
||||
return ErrBusyPinTimeout
|
||||
}
|
||||
runtime.Gosched()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get tranceiver status, returns circuit mode and command status
|
||||
func (d *Device) GetStatus() (CircuitMode, CommandStatus, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
status, err := d.spi.Transfer(cmdGetStatus)
|
||||
d.nssPin.Set(true)
|
||||
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
circuitMode := (status & circuitModeMask) >> 5
|
||||
commandStatus := (status & commandStatusMask) >> 2
|
||||
return CircuitMode(circuitMode), CommandStatus(commandStatus), nil
|
||||
}
|
||||
|
||||
func (d *Device) WriteRegister(addr uint16, data []byte) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdWriteRegister, uint8((addr>>8)&0xFF), uint8(addr&0xFF))
|
||||
d.spiTxBuf = append(d.spiTxBuf, data...)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Device) ReadRegister(addr uint16) (uint8, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdReadRegister, uint8((addr&0xFF00)>>8), uint8(addr&0x00FF), 0x00, 0x00)
|
||||
d.spiRxBuf = d.spiRxBuf[:5]
|
||||
err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
d.nssPin.Set(true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return d.spiRxBuf[4], nil
|
||||
}
|
||||
|
||||
func (d *Device) WriteBuffer(offset uint8, data []byte) error {
|
||||
if len(data) > 256 {
|
||||
return errDataTooLong
|
||||
}
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdWriteBuffer, offset)
|
||||
d.spiTxBuf = append(d.spiTxBuf, data...)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Read data from the payload buffer starting at the given offset with the given length
|
||||
func (d *Device) ReadBuffer(offset uint8, length uint8) ([]byte, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdReadBuffer, offset, 0x00)
|
||||
for i := uint8(0); i < length; i++ {
|
||||
d.spiTxBuf = append(d.spiTxBuf, 0x00)
|
||||
}
|
||||
d.spiRxBuf = d.spiRxBuf[:len(d.spiTxBuf)]
|
||||
err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
d.nssPin.Set(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.spiRxBuf[3:], nil
|
||||
}
|
||||
|
||||
// Set the device into sleep mode with the given configuration: 0 (no retention), 1 (ram retentation), 2 (buffer retention) or 3 (ram and buffer retention)
|
||||
func (d *Device) SetSleep(sleepConfig SleepConfig) error {
|
||||
if sleepConfig > (SLEEP_DATA_BUFFER_RETAIN | SLEEP_DATA_RAM_RETAIN) {
|
||||
return errInvalidSleepConfig
|
||||
}
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetSleep, uint8(sleepConfig))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Put device into standby mode, 0 (RC) or 1 (XOSC)
|
||||
func (d *Device) SetStandby(standbyConfig StandbyConfig) error {
|
||||
if standbyConfig > STANDBY_XOSC { // XOSC is the highest standby config anything higher is invalid
|
||||
return errInvalidStandbyConfig
|
||||
}
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetStandby, uint8(standbyConfig))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the device into Frequency Synthesizer mode
|
||||
func (d *Device) SetFs() error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetFS)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
func checkPeriodBase(periodBase PeriodBase) error {
|
||||
if periodBase > PERIOD_BASE_4_MS { // 4ms is the highest period base anything higher is invalid
|
||||
return errInvalidPeriodBase
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sets the device in transmit mode, the IRQ status should be cleared before using this command
|
||||
// timout is determined by periodBase * periodBaseCount
|
||||
func (d *Device) SetTx(periodBase PeriodBase, periodBaseCount uint16) error {
|
||||
err := checkPeriodBase(periodBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetTx, uint8(periodBase), uint8((periodBaseCount>>8)&0xFF), uint8(periodBaseCount&0xFF))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Sets the device in receive mode, the IRQ status should be cleared before using this command
|
||||
// timeout is determined by periodBase * periodBaseCount
|
||||
func (d *Device) SetRx(periodBase PeriodBase, periodBaseCount uint16) error {
|
||||
err := checkPeriodBase(periodBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetRx, uint8(periodBase), uint8((periodBaseCount>>8)&0xFF), uint8(periodBaseCount&0xFF))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Sets the device in a continuous receive mode, it enters receive mode with a timeout of periodBase * rxPeriodBaseCount.
|
||||
// If no packet is received it will enter sleep mode for periodBase * sleepPeriodBaseCount before re-entering receive mode.
|
||||
// The loop is exited when a packet is received or the device is put into standby mode.
|
||||
func (d *Device) SetRxDutyCycle(periodBase PeriodBase, rxPeriodBaseCount uint16, sleepPeriodBaseCount uint16) error {
|
||||
err := checkPeriodBase(periodBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetRxDutyCycle, uint8(periodBase), uint8((rxPeriodBaseCount&0xFF00)>>8), uint8(rxPeriodBaseCount&0x00FF))
|
||||
d.spiTxBuf = append(d.spiTxBuf, uint8((sleepPeriodBaseCount&0xFF00)>>8), uint8(sleepPeriodBaseCount&0x00FF))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Sets the transceiver into Long Preamble mode, and can only be used with either the LoRa mode and GFSK mode
|
||||
func (d *Device) SetLongPreamble(enable bool) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetLongPreamble)
|
||||
if enable {
|
||||
d.spiTxBuf = append(d.spiTxBuf, 1)
|
||||
} else {
|
||||
d.spiTxBuf = append(d.spiTxBuf, 0)
|
||||
}
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Channel activity detection (CAD) is a LoRa specific mode of operation where the device searches for a LoRa signal.
|
||||
// After search has completed, the device returns to STDBY_RC mode. The length of the search is configured via the SetCadParams() command.
|
||||
func (d *Device) SetCAD() error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetCAD)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Test command to generate a Continuous Wave (RF tone) at a selected frequency and output power
|
||||
// The device remains in Tx Continuous Wave until the host sends a mode configuration command.
|
||||
func (d *Device) SetTxContinuousWave() error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetTxContinuousWave)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Test command to generate an infinite sequence of alternating ‘0’s and ‘1’s in
|
||||
// GFSK modulation and symbol 0 in LoRa. The device remains in transmit until the host sends a mode configuration command.
|
||||
func (d *Device) SetTxContinuousPreamble() error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetContinuousPreamble)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// This command allows the transceiver to send a packet at a user programmable time after the end of a packet reception.
|
||||
// This is useful for Bluetooth Low Energy (BLE) compatibility which requires the transceiver to be able to send back a response 150µs after a packet reception.
|
||||
func (d *Device) SetAutoTx(timeUs uint16) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetAutoTx, uint8((timeUs&0xFF00)>>8), uint8(timeUs&0x00FF))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Modifies the chip behavior so that the state following a Rx or Tx operation is FS and not standby.
|
||||
// This allows for faster transitions between Rx and/or Tx.
|
||||
func (d *Device) SetAutoFs(enable bool) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetAutoFS)
|
||||
if enable {
|
||||
d.spiTxBuf = append(d.spiTxBuf, 1)
|
||||
} else {
|
||||
d.spiTxBuf = append(d.spiTxBuf, 0)
|
||||
}
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Choose between GFSK, LoRa, Ranging, FLRC or BLE packet types, this will affect the available configuration parameters and the structure of the packet
|
||||
func (d *Device) SetPacketType(packetType PacketType) error {
|
||||
if packetType > PACKET_TYPE_BLE { // BLE is the highest packet type anything higher is invalid.
|
||||
return errInvalidPacketType
|
||||
}
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetPacketType, uint8(packetType))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the currently configured packet type, this will be 0 (GFSK), 1 (LoRa), 2 (Ranging), 3 (FLRC) or 4 (BLE)
|
||||
func (d *Device) GetPacketType() (PacketType, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdGetPacketType, 0x00, 0x00)
|
||||
d.spiRxBuf = d.spiRxBuf[:3]
|
||||
err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
d.nssPin.Set(true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return PacketType(d.spiRxBuf[2]), nil
|
||||
}
|
||||
|
||||
// Set the RF frequency in Hz, must be between 2.4 GHz and 2.5 GHz
|
||||
func (d *Device) SetRfFrequency(frequencyHz uint32) error {
|
||||
if frequencyHz < 2400000000 {
|
||||
return errFrequencyTooLow
|
||||
}
|
||||
if frequencyHz > 2500000000 {
|
||||
return errFrequencyTooHigh
|
||||
}
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
rfFrequency := uint32((uint64(frequencyHz) << 18) / 52000000)
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetRFFrequency, uint8((rfFrequency>>16)&0xFF), uint8((rfFrequency>>8)&0xFF), uint8(rfFrequency&0xFF))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the output power in dBm, must be between -18 and 13 dBm, and the ramp time
|
||||
func (d *Device) SetTxParams(powerdBm int8, rampTime RadioRampTime) error {
|
||||
if powerdBm < -18 {
|
||||
return errPowerTooLow
|
||||
}
|
||||
if powerdBm > 13 {
|
||||
return errPowerTooHigh
|
||||
}
|
||||
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
adjustedPower := uint8(powerdBm + 18)
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetTxParams, adjustedPower, uint8(rampTime))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the number of symbols used for channel activity detection which determines the sensitivity of the detection.
|
||||
// This is only applicable in LoRa mode.
|
||||
func (d *Device) SetCadParams(cadSymbolNum uint8) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetCADParams, cadSymbolNum)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the base address for the internal buffer for Tx and Rx operations.
|
||||
// When transmitting or receiving data is read from or written to the buffer starting at the given offset.
|
||||
func (d *Device) SetBufferBaseAddress(txBase uint8, rxBase uint8) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetBufferBaseAddress, txBase, rxBase)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// The arguments to this function depend on the packet type. It is recommended to use the mode specific functions for a better experience.
|
||||
// BLE & GFSK: BitrateBandwidth, ModulationIndex, ModulationShaping
|
||||
// FLRC: BitrateBandwidth, CodingRate, ModulationShaping
|
||||
// LoRa & Ranging: SpreadingFactor, Bandwidth, CodingRate
|
||||
func (d *Device) SetModulationParams(modParam1, modParam2, modParam3 uint8) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetModulationParams, modParam1, modParam2, modParam3)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Device) SetModulationParamsBLE(bitrateBandwidth GFSKBLEBitrateBandwidth, modulationIndex ModulationIndex, modulationShaping ModulationShaping) error {
|
||||
return d.SetModulationParams(uint8(bitrateBandwidth), uint8(modulationIndex), uint8(modulationShaping))
|
||||
}
|
||||
|
||||
func (d *Device) SetModulationParamsGFSK(bitrateBandwidth GFSKBLEBitrateBandwidth, modulationIndex ModulationIndex, modulationShaping ModulationShaping) error {
|
||||
return d.SetModulationParams(uint8(bitrateBandwidth), uint8(modulationIndex), uint8(modulationShaping))
|
||||
}
|
||||
|
||||
func (d *Device) SetModulationParamsFLRC(bitrateBandwidth FLRCBitrateBandwidth, codingRate FLRCCodingRate, modulationShaping ModulationShaping) error {
|
||||
return d.SetModulationParams(uint8(bitrateBandwidth), uint8(codingRate), uint8(modulationShaping))
|
||||
}
|
||||
|
||||
func (d *Device) SetModulationParamsLoRa(spreadingFactor LoRaSpreadingFactor, bandwidth LoRaBandwidth, codingRate LoRaCodingRate) error {
|
||||
return d.SetModulationParams(uint8(spreadingFactor), uint8(bandwidth), uint8(codingRate))
|
||||
}
|
||||
|
||||
// The arguments to this function depend on the packet type. It is recommended to use the mode specific functions for a better experience.
|
||||
// GFSK & FLRC: PreambleLength, SyncWordLength, SyncWordMatch, HeaderType, PayloadLength, CrcLength, Whitening
|
||||
// BLE: ConnectionState, CrcLength, BleTestPayload, Whitening
|
||||
// LoRa & Ranging: PreambleLength, HeaderType, PayloadLength, CRC, InvertIQ/chirp invert
|
||||
func (d *Device) SetPacketParams(param1, param2, param3, param4, param5, param6, param7 uint8) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetPacketParams, param1, param2, param3, param4, param5, param6, param7)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Set GFSK related packet parameters, this assumes the packet type is already set to GFSK.
|
||||
// - payloadLength: range of 0-255
|
||||
func (d *Device) SetPacketParamsGFSK(preambleLength GFSKPreambleLength, syncWordLength GFSKSyncWordLength, syncWordMatch GFSKSyncWordMatch, headerType GFSKHeaderType, payloadLength uint8, crcLength GFSKCrcType, whitening bool) error {
|
||||
var whiteningVal uint8
|
||||
if whitening {
|
||||
whiteningVal = whiteningEnable
|
||||
} else {
|
||||
whiteningVal = whiteningDisable
|
||||
}
|
||||
return d.SetPacketParams(uint8(preambleLength), uint8(syncWordLength), uint8(syncWordMatch), uint8(headerType), payloadLength, uint8(crcLength), whiteningVal)
|
||||
}
|
||||
|
||||
// Set FLRC related packet parameters, this assumes the packet type is already set to FLRC.
|
||||
// - payloadLength: range of 6-127
|
||||
func (d *Device) SetPacketParamsFLRC(preambleLength FLRCPreambleLength, syncWordLength FLRCSyncWordLength, syncWordMatch FLRCSyncWordMatch, headerType FLRCHeaderType, payloadLength uint8, crcLength FLRCCrcType) error {
|
||||
if payloadLength < 6 {
|
||||
return errPayloadLengthTooShort
|
||||
}
|
||||
if payloadLength > 127 {
|
||||
return errPayloadLengthTooLong
|
||||
}
|
||||
return d.SetPacketParams(uint8(preambleLength), uint8(syncWordLength), uint8(syncWordMatch), uint8(headerType), payloadLength, uint8(crcLength), whiteningDisable)
|
||||
}
|
||||
|
||||
// Set BLE related packet parameters, this assumes the packet type is already set to BLE.
|
||||
func (d *Device) SetPacketParamsBLE(connectionState BLEConnectionState, crcLength BLECrcType, bleTestPayload BLETestPayload, whitening bool) error {
|
||||
var whiteningVal uint8
|
||||
if whitening {
|
||||
whiteningVal = whiteningEnable
|
||||
} else {
|
||||
whiteningVal = whiteningDisable
|
||||
}
|
||||
return d.SetPacketParams(uint8(connectionState), uint8(crcLength), uint8(bleTestPayload), whiteningVal, 0, 0, 0)
|
||||
}
|
||||
|
||||
// Set LoRa related packet parameters, this assumes the packet type is already set to LoRa.
|
||||
// - payloadLength: range of 1-255
|
||||
func (d *Device) SetPacketParamsLoRa(preambleLength uint32, headerType LoRaHeaderType, payloadLength uint8, crcType LoRaCrcType, iqType LoRaIqType) error {
|
||||
if payloadLength == 0 {
|
||||
return errPayloadLengthTooShort
|
||||
}
|
||||
exponent, mantissa := getExponentAndMantissa(preambleLength)
|
||||
return d.SetPacketParams(uint8(exponent<<4)|mantissa, uint8(headerType), payloadLength, uint8(crcType), uint8(iqType), 0, 0)
|
||||
}
|
||||
|
||||
func getExponentAndMantissa(value uint32) (uint8, uint8) {
|
||||
// pulled from RadioLib https://github.com/jgromes/RadioLib/blob/master/src/modules/SX128x/SX128x.cpp
|
||||
e := uint8(1)
|
||||
m := uint8(1)
|
||||
len := uint32(0)
|
||||
for e = uint8(1); e <= 15; e++ {
|
||||
for m = uint8(1); m <= 15; m++ {
|
||||
len = uint32(m) * (uint32(1 << e))
|
||||
if len >= value {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len >= value {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return e, m
|
||||
}
|
||||
|
||||
// Get information about the most recent packet received.
|
||||
// Return the payload length, the offset in the buffer where the payload starts.
|
||||
func (d *Device) GetRxBufferStatus() (uint8, uint8, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdGetRxBufferStatus, 0x00, 0x00, 0x00)
|
||||
d.spiRxBuf = d.spiRxBuf[:4]
|
||||
err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
d.nssPin.Set(true)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return d.spiRxBuf[2], d.spiRxBuf[3], nil
|
||||
}
|
||||
|
||||
// The return type of this function depends on the packet type. Use mode specific function for typed returns.
|
||||
// BLE, GFSK & FLRC: unused, rssiSync, errors, status, sync
|
||||
// LoRa & Ranging: rssiSync, SNR
|
||||
func (d *Device) GetPacketStatus() (uint8, uint8, uint8, uint8, uint8, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdGetPacketStatus, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
||||
d.spiRxBuf = d.spiRxBuf[:7]
|
||||
err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
d.nssPin.Set(true)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, err
|
||||
}
|
||||
return d.spiRxBuf[2], d.spiRxBuf[3], d.spiRxBuf[4], d.spiRxBuf[5], d.spiRxBuf[6], nil
|
||||
}
|
||||
|
||||
// Get information about the most recent GFSK packet received or transmitted:
|
||||
// - RSSI of last received packet
|
||||
// - packet information (each bit represents a different error or status flag)
|
||||
// - whether the last packet transmission has ended
|
||||
// - the sync word that was used for the last packet reception (0-3)
|
||||
func (d *Device) GetPacketStatusGFSK() (float32, GFSKPacketInfo, bool, uint8, error) {
|
||||
_, rssiSync, packetInfo, status, sync, err := d.GetPacketStatus()
|
||||
if err != nil {
|
||||
return 0, 0, false, 0, err
|
||||
}
|
||||
return float32(int8(rssiSync)) / 2 * -1, GFSKPacketInfo(packetInfo), status != 0, sync, nil
|
||||
}
|
||||
|
||||
// Get information about the most recent BLE packet received or transmitted:
|
||||
// - RSSI of last received packet
|
||||
// - packet information (each bit represents a different error or status flag)
|
||||
// - whether the last packet transmission has ended
|
||||
// - the sync word that was used for the last packet reception (0-1)
|
||||
func (d *Device) GetPacketStatusBLE() (float32, BLEPacketInfo, bool, uint8, error) {
|
||||
_, rssiSync, packetInfo, status, sync, err := d.GetPacketStatus()
|
||||
if err != nil {
|
||||
return 0, 0, false, 0, err
|
||||
}
|
||||
return float32(int8(rssiSync)) / 2 * -1, BLEPacketInfo(packetInfo), status != 0, sync, nil
|
||||
}
|
||||
|
||||
// Get information about the most recent BLE packet received or transmitted:
|
||||
// - RSSI of last received packet
|
||||
// - packet information (each bit represents a different error or status flag)
|
||||
// - PID field of the received packet
|
||||
// - NO_ACK field of the received packet
|
||||
// - PID check status of the current packet
|
||||
// - whether the last packet transmission has ended
|
||||
// - the sync word that was used for the last packet reception (0-1)
|
||||
func (d *Device) GetPacketStatusFLRC() (float32, FLRCPacketInfo, uint8, bool, bool, bool, uint8, error) {
|
||||
_, rawRSSI, packetInfo, rxTxInfo, sync, err := d.GetPacketStatus()
|
||||
|
||||
rxPid := (rxTxInfo & 0b11000000) >> 6
|
||||
noAck := (rxTxInfo & 0b00100000) != 0
|
||||
pidCheck := (rxTxInfo & 0b00010000) != 0
|
||||
txDone := (rxTxInfo & 0b00000001) != 0
|
||||
|
||||
if err != nil {
|
||||
return 0, 0, 0, false, false, false, 0, err
|
||||
}
|
||||
return float32(int8(rawRSSI)) / 2 * -1, FLRCPacketInfo(packetInfo), rxPid, noAck, pidCheck, txDone, sync, nil
|
||||
}
|
||||
|
||||
// Get information about the most recent LoRa packet received:
|
||||
// - RSSI of last received packet
|
||||
// - signal-to-noise ratio (SNR) of last received packet
|
||||
func (d *Device) GetPacketStatusLoRa() (float32, float32, error) {
|
||||
rawRSSI, rawSnr, _, _, _, err := d.GetPacketStatus()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return float32(int8(rawRSSI)) / 2 * -1, float32(int8(rawSnr)) / 4, nil
|
||||
}
|
||||
|
||||
// Get the instantaneous RSSI value during reception of the packet
|
||||
func (d *Device) GetRssiInst() (float32, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdGetRSSIInst, 0x00, 0x00)
|
||||
d.spiRxBuf = d.spiRxBuf[:3]
|
||||
err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
d.nssPin.Set(true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float32(int8(d.spiRxBuf[2])) / 2 * -1, nil
|
||||
}
|
||||
|
||||
// Configure the overall IRQ mask and the mapping of individual IRQs to the DIO1, DIO2 and DIO3 pins
|
||||
func (d *Device) SetDioIrqParams(irqMask IRQMask, dio1Mask IRQMask, dio2Mask IRQMask, dio3Mask IRQMask) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetDIOIRQParams, uint8((irqMask&0xFF00)>>8), uint8(irqMask&0x00FF))
|
||||
d.spiTxBuf = append(d.spiTxBuf, uint8((dio1Mask&0xFF00)>>8), uint8(dio1Mask&0x00FF))
|
||||
d.spiTxBuf = append(d.spiTxBuf, uint8((dio2Mask&0xFF00)>>8), uint8(dio2Mask&0x00FF))
|
||||
d.spiTxBuf = append(d.spiTxBuf, uint8((dio3Mask&0xFF00)>>8), uint8(dio3Mask&0x00FF))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the current IRQ status.
|
||||
func (d *Device) GetIrqStatus() (IRQMask, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdGetIRQStatus, 0x00, 0x00, 0x00)
|
||||
d.spiRxBuf = d.spiRxBuf[:4]
|
||||
err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
d.nssPin.Set(true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint16(d.spiRxBuf[2])<<8 | uint16(d.spiRxBuf[3]), err
|
||||
}
|
||||
|
||||
// Clear the IRQ bits specified in the irqMask.
|
||||
func (d *Device) ClearIrqStatus(irqMask IRQMask) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdClearIRQStatus, uint8((irqMask&0xFF00)>>8), uint8(irqMask&0x00FF))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Switch between the low-dropout regulator (LDO) and the DC-DC converter for internal power regulation.
|
||||
func (d *Device) SetRegulatorMode(mode RegulatorMode) error {
|
||||
if mode > REGULATOR_DC_DC { // DC-DC is the highest regulator mode anything higher is invalid
|
||||
return errInvalidRegulatorMode
|
||||
}
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetRegulatorMode, uint8(mode))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Stores the present context of the radio register values to the Data RAM which will be restored when the device wakes up from sleep mode.
|
||||
func (d *Device) SetSaveContext() error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetSaveContext)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
// Package unoqmatrix provides a driver for the UnoQMatrix LED matrix display.
|
||||
//
|
||||
// The UnoQMatrix is an 8x13 LED matrix display that can be controlled using a single pin.
|
||||
// It uses a multiplexing technique to control the LEDs, which allows for a large number of LEDs to be controlled with fewer pins.
|
||||
//
|
||||
// This driver provides basic functionality to set individual pixels, clear the display, and refresh the display.
|
||||
//
|
||||
// Note: The UnoQMatrix does not support brightness control or color depth. Each pixel can only be turned on or off.
|
||||
// Could it suppport brightness control by using PWM on the pin? To be investigated.
|
||||
package unoqmatrix
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
pin "tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// Rotation of the LED matrix.
|
||||
Rotation uint8
|
||||
}
|
||||
|
||||
// Valid values:
|
||||
//
|
||||
// 0: regular orientation, (0 degree rotation)
|
||||
// 1: 90 degree rotation clockwise
|
||||
// 2: 180 degree rotation clockwise
|
||||
// 3: 270 degree rotation clockwise
|
||||
const (
|
||||
RotationNormal = 0
|
||||
Rotation90 = 1
|
||||
Rotation180 = 2
|
||||
Rotation270 = 3
|
||||
)
|
||||
|
||||
const (
|
||||
ledRows = 8
|
||||
ledCols = 13
|
||||
|
||||
pixelRefreshDelay = 10 * time.Microsecond
|
||||
)
|
||||
|
||||
// CharlieplexPin represents a pin used for charlieplexing.
|
||||
// It must be able to drive high/low (output mode) and float (high-impedance/input mode).
|
||||
//
|
||||
// Example construction from a machine.Pin using the pin HAL pattern:
|
||||
//
|
||||
// var isOutput bool
|
||||
// cp := unoqmatrix.CharlieplexPin{
|
||||
// Set: pin.OutputFunc(func(level bool) {
|
||||
// if !isOutput {
|
||||
// p.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
// isOutput = true
|
||||
// }
|
||||
// p.Set(level)
|
||||
// }),
|
||||
// Float: func() {
|
||||
// if isOutput {
|
||||
// p.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
// isOutput = false
|
||||
// }
|
||||
// },
|
||||
// }
|
||||
type CharlieplexPin struct {
|
||||
Set pin.OutputFunc // Drive pin high (true) or low (false); auto-configures to output mode.
|
||||
Float func() // Put pin into high-impedance (input) mode.
|
||||
}
|
||||
|
||||
const numPins = 11
|
||||
|
||||
// Device represents the UnoQMatrix LED matrix display.
|
||||
type Device struct {
|
||||
pins [numPins]CharlieplexPin
|
||||
buffer [ledRows][ledCols]color.RGBA
|
||||
rotation uint8
|
||||
}
|
||||
|
||||
// New returns a new unoqmatrix driver.
|
||||
// The provided pins are the 11 charlieplex pins used to control the LED matrix.
|
||||
func New(pins [numPins]CharlieplexPin) Device {
|
||||
return Device{pins: pins}
|
||||
}
|
||||
|
||||
// Configure sets up the device.
|
||||
func (d *Device) Configure(cfg Config) {
|
||||
d.SetRotation(cfg.Rotation)
|
||||
}
|
||||
|
||||
// SetRotation changes the rotation of the LED matrix.
|
||||
//
|
||||
// Valid values for rotation:
|
||||
//
|
||||
// 0: regular orientation, (0 degree rotation)
|
||||
// 1: 90 degree rotation clockwise
|
||||
// 2: 180 degree rotation clockwise
|
||||
// 3: 270 degree rotation clockwise
|
||||
func (d *Device) SetRotation(rotation uint8) {
|
||||
d.rotation = rotation % 4
|
||||
}
|
||||
|
||||
// SetPixel sets the color of a specific pixel.
|
||||
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
|
||||
d.buffer[y][x] = c
|
||||
}
|
||||
|
||||
// GetPixel returns the color of a specific pixel.
|
||||
func (d *Device) GetPixel(x int16, y int16) color.RGBA {
|
||||
return d.buffer[y][x]
|
||||
}
|
||||
|
||||
// Display sends the buffer (if any) to the screen.
|
||||
// Only lights active (non-black) pixels, and resets only the 2 previously
|
||||
// driven pins between LEDs instead of all 11, making each refresh cycle
|
||||
// proportional to the number of lit LEDs.
|
||||
func (d *Device) Display() error {
|
||||
d.clearDisplay()
|
||||
|
||||
var lastIdx0, lastIdx1 uint8
|
||||
hasLast := false
|
||||
|
||||
for row := 0; row < ledRows; row++ {
|
||||
for col := 0; col < ledCols; col++ {
|
||||
c := d.buffer[row][col]
|
||||
if c.R == 0 && c.G == 0 && c.B == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
idx := row*ledCols + col
|
||||
if idx < 0 || idx >= len(pinMapping) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Float only the two pins that were driving the previous LED.
|
||||
if hasLast {
|
||||
d.pins[lastIdx0].Float()
|
||||
d.pins[lastIdx1].Float()
|
||||
}
|
||||
hasLast = true
|
||||
|
||||
idx0 := pinMapping[idx][0]
|
||||
idx1 := pinMapping[idx][1]
|
||||
d.pins[idx0].Set.High()
|
||||
d.pins[idx1].Set.Low()
|
||||
lastIdx0 = idx0
|
||||
lastIdx1 = idx1
|
||||
|
||||
time.Sleep(pixelRefreshDelay)
|
||||
}
|
||||
}
|
||||
|
||||
// Float the last driven LED.
|
||||
if hasLast {
|
||||
d.pins[lastIdx0].Float()
|
||||
d.pins[lastIdx1].Float()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearDisplay turns off all the LEDs on the display.
|
||||
func (d *Device) ClearDisplay() {
|
||||
for row := 0; row < ledRows; row++ {
|
||||
for col := 0; col < ledCols; col++ {
|
||||
d.buffer[row][col] = color.RGBA{0, 0, 0, 255}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Size returns the current size of the display.
|
||||
func (d *Device) Size() (w, h int16) {
|
||||
return ledCols, ledRows
|
||||
}
|
||||
|
||||
// pinMapping defines the mapping of LED indices to pin pairs. Each entry corresponds
|
||||
// to an LED index (0-104) and contains the two pin numbers that need to be set to turn on that LED.
|
||||
// based on https://github.com/arduino/ArduinoCore-zephyr/blob/main/loader/matrix.inc#L13
|
||||
var pinMapping = [][2]uint8{
|
||||
{0, 1}, // 0
|
||||
{1, 0},
|
||||
{0, 2},
|
||||
{2, 0},
|
||||
{1, 2},
|
||||
{2, 1},
|
||||
{0, 3},
|
||||
{3, 0},
|
||||
{1, 3},
|
||||
{3, 1},
|
||||
{2, 3}, // 10
|
||||
{3, 2},
|
||||
{0, 4},
|
||||
{4, 0},
|
||||
{1, 4},
|
||||
{4, 1},
|
||||
{2, 4},
|
||||
{4, 2},
|
||||
{3, 4},
|
||||
{4, 3},
|
||||
{0, 5}, // 20
|
||||
{5, 0},
|
||||
{1, 5},
|
||||
{5, 1},
|
||||
{2, 5},
|
||||
{5, 2},
|
||||
{3, 5},
|
||||
{5, 3},
|
||||
{4, 5},
|
||||
{5, 4},
|
||||
{0, 6}, // 30
|
||||
{6, 0},
|
||||
{1, 6},
|
||||
{6, 1},
|
||||
{2, 6},
|
||||
{6, 2},
|
||||
{3, 6},
|
||||
{6, 3},
|
||||
{4, 6},
|
||||
{6, 4},
|
||||
{5, 6}, // 40
|
||||
{6, 5},
|
||||
{0, 7},
|
||||
{7, 0},
|
||||
{1, 7},
|
||||
{7, 1},
|
||||
{2, 7},
|
||||
{7, 2},
|
||||
{3, 7},
|
||||
{7, 3},
|
||||
{4, 7}, // 50
|
||||
{7, 4},
|
||||
{5, 7},
|
||||
{7, 5},
|
||||
{6, 7},
|
||||
{7, 6},
|
||||
{0, 8},
|
||||
{8, 0},
|
||||
{1, 8},
|
||||
{8, 1},
|
||||
{2, 8}, // 60
|
||||
{8, 2},
|
||||
{3, 8},
|
||||
{8, 3},
|
||||
{4, 8},
|
||||
{8, 4},
|
||||
{5, 8},
|
||||
{8, 5},
|
||||
{6, 8},
|
||||
{8, 6},
|
||||
{7, 8}, // 70
|
||||
{8, 7},
|
||||
{0, 9},
|
||||
{9, 0},
|
||||
{1, 9},
|
||||
{9, 1},
|
||||
{2, 9},
|
||||
{9, 2},
|
||||
{3, 9},
|
||||
{9, 3},
|
||||
{4, 9}, // 80
|
||||
{9, 4},
|
||||
{5, 9},
|
||||
{9, 5},
|
||||
{6, 9},
|
||||
{9, 6},
|
||||
{7, 9},
|
||||
{9, 7},
|
||||
{8, 9},
|
||||
{9, 8},
|
||||
{0, 10}, // 90
|
||||
{10, 0},
|
||||
{1, 10},
|
||||
{10, 1},
|
||||
{2, 10},
|
||||
{10, 2},
|
||||
{3, 10},
|
||||
{10, 3},
|
||||
{4, 10},
|
||||
{10, 4},
|
||||
{5, 10}, // 100
|
||||
{10, 5},
|
||||
{6, 10},
|
||||
{10, 6},
|
||||
}
|
||||
|
||||
// clearDisplay turns off all the LEDs on the display by floating all pins.
|
||||
func (d *Device) clearDisplay() {
|
||||
for i := range d.pins {
|
||||
d.pins[i].Float()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
package unoqmatrix
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"testing"
|
||||
|
||||
pin "tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
// pinState tracks the state of a mock charlieplex pin.
|
||||
type pinState struct {
|
||||
level bool // true=high, false=low
|
||||
isOutput bool // true=output mode, false=floating (high-Z)
|
||||
}
|
||||
|
||||
// mockPins creates 11 mock CharlieplexPins and returns them along with their observable state.
|
||||
func mockPins() ([numPins]CharlieplexPin, *[numPins]pinState) {
|
||||
var pins [numPins]CharlieplexPin
|
||||
var states [numPins]pinState
|
||||
for i := range pins {
|
||||
idx := i // capture
|
||||
pins[i] = CharlieplexPin{
|
||||
Set: pin.OutputFunc(func(level bool) {
|
||||
states[idx].isOutput = true
|
||||
states[idx].level = level
|
||||
}),
|
||||
Float: func() {
|
||||
states[idx].isOutput = false
|
||||
states[idx].level = false
|
||||
},
|
||||
}
|
||||
}
|
||||
return pins, &states
|
||||
}
|
||||
|
||||
func newTestDevice() (Device, *[numPins]pinState) {
|
||||
pins, states := mockPins()
|
||||
d := New(pins)
|
||||
return d, states
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
d, _ := newTestDevice()
|
||||
w, h := d.Size()
|
||||
if w != ledCols || h != ledRows {
|
||||
t.Errorf("Size() = (%d, %d), want (%d, %d)", w, h, ledCols, ledRows)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSize(t *testing.T) {
|
||||
d, _ := newTestDevice()
|
||||
w, h := d.Size()
|
||||
if w != 13 {
|
||||
t.Errorf("width = %d, want 13", w)
|
||||
}
|
||||
if h != 8 {
|
||||
t.Errorf("height = %d, want 8", h)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetGetPixel(t *testing.T) {
|
||||
d, _ := newTestDevice()
|
||||
c := color.RGBA{R: 255, G: 128, B: 64, A: 255}
|
||||
|
||||
d.SetPixel(3, 2, c)
|
||||
got := d.GetPixel(3, 2)
|
||||
if got != c {
|
||||
t.Errorf("GetPixel(3,2) = %v, want %v", got, c)
|
||||
}
|
||||
|
||||
// Unset pixel should be zero-value.
|
||||
got = d.GetPixel(0, 0)
|
||||
if got != (color.RGBA{}) {
|
||||
t.Errorf("GetPixel(0,0) = %v, want zero", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClearDisplay(t *testing.T) {
|
||||
d, _ := newTestDevice()
|
||||
on := color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
||||
off := color.RGBA{A: 255}
|
||||
|
||||
d.SetPixel(0, 0, on)
|
||||
d.SetPixel(5, 3, on)
|
||||
d.ClearDisplay()
|
||||
|
||||
for y := int16(0); y < ledRows; y++ {
|
||||
for x := int16(0); x < ledCols; x++ {
|
||||
got := d.GetPixel(x, y)
|
||||
if got != off {
|
||||
t.Errorf("after ClearDisplay, GetPixel(%d,%d) = %v, want %v", x, y, got, off)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetRotation(t *testing.T) {
|
||||
d, _ := newTestDevice()
|
||||
|
||||
tests := []struct {
|
||||
input uint8
|
||||
want uint8
|
||||
}{
|
||||
{0, 0},
|
||||
{1, 1},
|
||||
{2, 2},
|
||||
{3, 3},
|
||||
{4, 0}, // wraps
|
||||
{7, 3}, // wraps
|
||||
}
|
||||
for _, tt := range tests {
|
||||
d.SetRotation(tt.input)
|
||||
if d.rotation != tt.want {
|
||||
t.Errorf("SetRotation(%d): rotation = %d, want %d", tt.input, d.rotation, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigure(t *testing.T) {
|
||||
d, _ := newTestDevice()
|
||||
d.Configure(Config{Rotation: 2})
|
||||
if d.rotation != 2 {
|
||||
t.Errorf("Configure(Rotation:2): rotation = %d, want 2", d.rotation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplayEmptyBuffer(t *testing.T) {
|
||||
d, states := newTestDevice()
|
||||
|
||||
err := d.Display()
|
||||
if err != nil {
|
||||
t.Fatalf("Display() error: %v", err)
|
||||
}
|
||||
|
||||
// All pins should be floating after displaying an empty buffer.
|
||||
for i, s := range states {
|
||||
if s.isOutput {
|
||||
t.Errorf("pin %d still in output mode after empty Display()", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplaySinglePixel(t *testing.T) {
|
||||
d, states := newTestDevice()
|
||||
on := color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
||||
|
||||
// LED index 0 -> pinMapping[0] = {0, 1}: pin 0 high, pin 1 low.
|
||||
d.SetPixel(0, 0, on)
|
||||
err := d.Display()
|
||||
if err != nil {
|
||||
t.Fatalf("Display() error: %v", err)
|
||||
}
|
||||
|
||||
// After Display completes, all pins should be floating (last LED turned off).
|
||||
for i, s := range states {
|
||||
if s.isOutput {
|
||||
t.Errorf("pin %d still in output mode after Display()", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplayMultiplePixels(t *testing.T) {
|
||||
d, states := newTestDevice()
|
||||
on := color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
||||
|
||||
d.SetPixel(0, 0, on) // idx 0 -> pins {0,1}
|
||||
d.SetPixel(1, 0, on) // idx 1 -> pins {1,0}
|
||||
d.SetPixel(2, 0, on) // idx 2 -> pins {0,2}
|
||||
|
||||
err := d.Display()
|
||||
if err != nil {
|
||||
t.Fatalf("Display() error: %v", err)
|
||||
}
|
||||
|
||||
// All pins floating after display completes.
|
||||
for i, s := range states {
|
||||
if s.isOutput {
|
||||
t.Errorf("pin %d still in output mode after Display()", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pinEvent records a single pin action during Display().
|
||||
type pinEvent struct {
|
||||
pinIdx int
|
||||
action string // "high", "low", or "float"
|
||||
}
|
||||
|
||||
// traceDevice creates a device that records every pin event for verification.
|
||||
func traceDevice() (Device, *[]pinEvent) {
|
||||
var pins [numPins]CharlieplexPin
|
||||
events := &[]pinEvent{}
|
||||
for i := range pins {
|
||||
idx := i
|
||||
pins[i] = CharlieplexPin{
|
||||
Set: pin.OutputFunc(func(level bool) {
|
||||
action := "low"
|
||||
if level {
|
||||
action = "high"
|
||||
}
|
||||
*events = append(*events, pinEvent{pinIdx: idx, action: action})
|
||||
}),
|
||||
Float: func() {
|
||||
*events = append(*events, pinEvent{pinIdx: idx, action: "float"})
|
||||
},
|
||||
}
|
||||
}
|
||||
d := New(pins)
|
||||
return d, events
|
||||
}
|
||||
|
||||
func TestDisplayDrivesCorrectPins(t *testing.T) {
|
||||
d, events := traceDevice()
|
||||
on := color.RGBA{R: 255, A: 255}
|
||||
|
||||
// Set pixel at (0,0) -> LED index 0 -> pinMapping[0] = {0, 1}.
|
||||
d.SetPixel(0, 0, on)
|
||||
d.Display()
|
||||
|
||||
// Expected sequence:
|
||||
// 1. clearDisplay: float pins 0..10
|
||||
// 2. Drive LED 0: pin 0 high, pin 1 low
|
||||
// 3. Cleanup: float pin 0, float pin 1
|
||||
|
||||
// Find the high/low events (skip initial floats from clearDisplay).
|
||||
var driveEvents []pinEvent
|
||||
for _, e := range *events {
|
||||
if e.action == "high" || e.action == "low" {
|
||||
driveEvents = append(driveEvents, e)
|
||||
}
|
||||
}
|
||||
|
||||
if len(driveEvents) != 2 {
|
||||
t.Fatalf("expected 2 drive events, got %d: %v", len(driveEvents), driveEvents)
|
||||
}
|
||||
if driveEvents[0].pinIdx != 0 || driveEvents[0].action != "high" {
|
||||
t.Errorf("first drive event = %v, want pin 0 high", driveEvents[0])
|
||||
}
|
||||
if driveEvents[1].pinIdx != 1 || driveEvents[1].action != "low" {
|
||||
t.Errorf("second drive event = %v, want pin 1 low", driveEvents[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplaySkipsBlackPixels(t *testing.T) {
|
||||
d, events := traceDevice()
|
||||
on := color.RGBA{R: 255, A: 255}
|
||||
|
||||
// Only set one pixel in the middle of the matrix.
|
||||
d.SetPixel(4, 1, on) // idx = 1*13+4 = 17 -> pinMapping[17] = {4,2}
|
||||
d.Display()
|
||||
|
||||
var driveEvents []pinEvent
|
||||
for _, e := range *events {
|
||||
if e.action == "high" || e.action == "low" {
|
||||
driveEvents = append(driveEvents, e)
|
||||
}
|
||||
}
|
||||
|
||||
// Should only drive one LED's worth of pin events.
|
||||
if len(driveEvents) != 2 {
|
||||
t.Fatalf("expected 2 drive events for 1 lit pixel, got %d", len(driveEvents))
|
||||
}
|
||||
if driveEvents[0].pinIdx != 4 || driveEvents[0].action != "high" {
|
||||
t.Errorf("expected pin 4 high, got %v", driveEvents[0])
|
||||
}
|
||||
if driveEvents[1].pinIdx != 2 || driveEvents[1].action != "low" {
|
||||
t.Errorf("expected pin 2 low, got %v", driveEvents[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplayFloatsBetweenLEDs(t *testing.T) {
|
||||
d, events := traceDevice()
|
||||
on := color.RGBA{R: 255, A: 255}
|
||||
|
||||
d.SetPixel(0, 0, on) // idx 0 -> {0,1}
|
||||
d.SetPixel(1, 0, on) // idx 1 -> {1,0}
|
||||
d.Display()
|
||||
|
||||
// After the initial clearDisplay floats, the sequence for two LEDs should be:
|
||||
// drive LED0 (pin0 high, pin1 low)
|
||||
// float pin0, float pin1 (between LEDs)
|
||||
// drive LED1 (pin1 high, pin0 low)
|
||||
// float pin1, float pin0 (cleanup)
|
||||
|
||||
// Skip the initial 11 float events from clearDisplay.
|
||||
postClear := (*events)[numPins:]
|
||||
|
||||
// Verify pin 0 and 1 are floated between the two LEDs.
|
||||
foundFloatBetween := false
|
||||
driveCount := 0
|
||||
for _, e := range postClear {
|
||||
if e.action == "high" || e.action == "low" {
|
||||
driveCount++
|
||||
}
|
||||
// After the first pair of drive events, we should see floats before the next pair.
|
||||
if driveCount == 2 && e.action == "float" {
|
||||
foundFloatBetween = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundFloatBetween {
|
||||
t.Error("expected float events between LED drives, found none")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPinMappingLength(t *testing.T) {
|
||||
expected := 104 // 8x13 matrix = 104 LEDs
|
||||
if len(pinMapping) != expected {
|
||||
t.Errorf("pinMapping has %d entries, want %d", len(pinMapping), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPinMappingIndicesInRange(t *testing.T) {
|
||||
for i, pair := range pinMapping {
|
||||
if pair[0] >= numPins {
|
||||
t.Errorf("pinMapping[%d][0] = %d, exceeds numPins (%d)", i, pair[0], numPins)
|
||||
}
|
||||
if pair[1] >= numPins {
|
||||
t.Errorf("pinMapping[%d][1] = %d, exceeds numPins (%d)", i, pair[1], numPins)
|
||||
}
|
||||
if pair[0] == pair[1] {
|
||||
t.Errorf("pinMapping[%d] has same pin for both: %d", i, pair[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//go:build baremetal
|
||||
|
||||
package unoqmatrix
|
||||
|
||||
import (
|
||||
"machine"
|
||||
|
||||
pin "tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
// NewFromBasePin creates a Device from a base machine.Pin.
|
||||
// It constructs 11 CharlieplexPin values from consecutive pins starting at basePin.
|
||||
// Each pin lazily switches between output and input mode as needed.
|
||||
func NewFromBasePin(basePin machine.Pin) Device {
|
||||
var pins [numPins]CharlieplexPin
|
||||
for i := range pins {
|
||||
p := basePin + machine.Pin(i)
|
||||
var isOutput bool
|
||||
pins[i] = CharlieplexPin{
|
||||
Set: pin.OutputFunc(func(level bool) {
|
||||
if !isOutput {
|
||||
p.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
isOutput = true
|
||||
}
|
||||
p.Set(level)
|
||||
}),
|
||||
Float: func() {
|
||||
if isOutput {
|
||||
p.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
isOutput = false
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
return New(pins)
|
||||
}
|
||||
+1
-1
@@ -2,4 +2,4 @@ package drivers
|
||||
|
||||
// Version returns a user-readable string showing the version of the drivers package for support purposes.
|
||||
// Update this value before release of new version of software.
|
||||
const Version = "0.34.0"
|
||||
const Version = "0.35.0"
|
||||
|
||||
@@ -0,0 +1,465 @@
|
||||
// Package epd2in9v2 implements a driver for the Waveshare 2.9in V2 black and white e-paper display.
|
||||
//
|
||||
// This is for the V2 device using the SSD1680 chipset. For the V1 device (using IL3820),
|
||||
// use the epd2in9 package instead.
|
||||
//
|
||||
// Datasheet:
|
||||
// https://files.waveshare.com/upload/7/79/2.9inch-e-paper-v2-specification.pdf
|
||||
// https://cdn-learn.adafruit.com/assets/assets/000/097/631/original/SSD1680_Datasheet.pdf?1607625960
|
||||
//
|
||||
// Reference: https://github.com/waveshareteam/e-Paper/tree/master/RaspberryPi_JetsonNano/c/lib/e-Paper
|
||||
package epd2in9v2 // import "tinygo.org/x/drivers/waveshare-epd/epd2in9v2"
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Width int16
|
||||
Height int16
|
||||
Rotation Rotation
|
||||
Speed Speed
|
||||
Blocking bool
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
bus drivers.SPI
|
||||
cs machine.Pin
|
||||
dc machine.Pin
|
||||
rst machine.Pin
|
||||
busy machine.Pin
|
||||
width int16
|
||||
height int16
|
||||
buffer []uint8
|
||||
bufferLength uint32
|
||||
rotation Rotation
|
||||
speed Speed
|
||||
blocking bool
|
||||
}
|
||||
|
||||
type Rotation uint8
|
||||
type Speed uint8
|
||||
|
||||
// LUT for normal full refresh (~2s)
|
||||
var lutDefault = [159]uint8{
|
||||
0x80, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0,
|
||||
0x10, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0,
|
||||
0x80, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0,
|
||||
0x10, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x20, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x14, 0x8, 0x0, 0x0, 0x0, 0x0, 0x1,
|
||||
0xA, 0xA, 0x0, 0xA, 0xA, 0x0, 0x1,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x14, 0x8, 0x0, 0x1, 0x0, 0x0, 0x1,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x0, 0x0, 0x0,
|
||||
0x22, 0x17, 0x41, 0x0, 0x32, 0x36,
|
||||
}
|
||||
|
||||
// LUT for fast full refresh (~1s)
|
||||
var lutFast = [159]uint8{
|
||||
0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x90, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x19, 0x19, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x24, 0x42, 0x22, 0x22, 0x23, 0x32, 0x00, 0x00, 0x00,
|
||||
0x22, 0x17, 0x41, 0xAE, 0x32, 0x38,
|
||||
}
|
||||
|
||||
// LUT for partial refresh
|
||||
var lutPartial = [159]uint8{
|
||||
0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0A, 0x0, 0x0, 0x0, 0x0, 0x0, 0x2,
|
||||
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
|
||||
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0,
|
||||
0x22, 0x17, 0x41, 0xB0, 0x32, 0x36,
|
||||
}
|
||||
|
||||
// New returns a new epd2in9v2 driver. Pass in a fully configured SPI bus.
|
||||
func New(bus drivers.SPI, csPin, dcPin, rstPin, busyPin machine.Pin) Device {
|
||||
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
rstPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
busyPin.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
return Device{
|
||||
bus: bus,
|
||||
cs: csPin,
|
||||
dc: dcPin,
|
||||
rst: rstPin,
|
||||
busy: busyPin,
|
||||
}
|
||||
}
|
||||
|
||||
// Configure sets up the device.
|
||||
func (d *Device) Configure(cfg Config) {
|
||||
if cfg.Width != 0 {
|
||||
d.width = cfg.Width
|
||||
} else {
|
||||
d.width = EPD_WIDTH
|
||||
}
|
||||
if cfg.Height != 0 {
|
||||
d.height = cfg.Height
|
||||
} else {
|
||||
d.height = EPD_HEIGHT
|
||||
}
|
||||
d.rotation = cfg.Rotation
|
||||
d.speed = cfg.Speed
|
||||
d.blocking = cfg.Blocking
|
||||
d.bufferLength = (uint32(d.width) * uint32(d.height)) / 8
|
||||
d.buffer = make([]uint8, d.bufferLength)
|
||||
for i := uint32(0); i < d.bufferLength; i++ {
|
||||
d.buffer[i] = 0xFF
|
||||
}
|
||||
|
||||
d.Reset()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
d.WaitUntilIdle()
|
||||
d.SendCommand(SW_RESET)
|
||||
d.WaitUntilIdle()
|
||||
|
||||
d.SendCommand(DRIVER_OUTPUT_CONTROL)
|
||||
d.SendData(uint8((d.height - 1) & 0xFF))
|
||||
d.SendData(uint8((d.height - 1) >> 8))
|
||||
d.SendData(0x00)
|
||||
|
||||
d.SendCommand(DATA_ENTRY_MODE)
|
||||
d.SendData(0x03)
|
||||
|
||||
d.setWindow(0, 0, d.width-1, d.height-1)
|
||||
|
||||
if cfg.Speed == SPEED_FAST {
|
||||
d.SendCommand(BORDER_WAVEFORM_CONTROL)
|
||||
d.SendData(0x05)
|
||||
}
|
||||
|
||||
d.SendCommand(DISPLAY_UPDATE_CONTROL_1)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x80)
|
||||
|
||||
d.setCursor(0, 0)
|
||||
d.WaitUntilIdle()
|
||||
|
||||
switch cfg.Speed {
|
||||
case SPEED_FAST:
|
||||
d.setLUTByHost(&lutFast)
|
||||
default:
|
||||
d.setLUTByHost(&lutDefault)
|
||||
}
|
||||
}
|
||||
|
||||
// HardwareReset resets the device via the RST pin.
|
||||
func (d *Device) Reset() {
|
||||
d.rst.High()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
d.rst.Low()
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
d.rst.High()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
// SendCommand sends a command byte to the display.
|
||||
func (d *Device) SendCommand(command uint8) {
|
||||
d.dc.Low()
|
||||
d.cs.Low()
|
||||
d.bus.Transfer(command)
|
||||
d.cs.High()
|
||||
}
|
||||
|
||||
// SendData sends a data byte to the display.
|
||||
func (d *Device) SendData(data uint8) {
|
||||
d.dc.High()
|
||||
d.cs.Low()
|
||||
d.bus.Transfer(data)
|
||||
d.cs.High()
|
||||
}
|
||||
|
||||
// WaitUntilIdle waits until the display is ready.
|
||||
// On SSD1680, BUSY pin is HIGH when busy, LOW when idle.
|
||||
func (d *Device) WaitUntilIdle() {
|
||||
for d.busy.Get() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
|
||||
// IsBusy returns the busy status of the display.
|
||||
func (d *Device) IsBusy() bool {
|
||||
return d.busy.Get()
|
||||
}
|
||||
|
||||
// SetPixel modifies the internal buffer in a single pixel.
|
||||
// Uses color.RGBA where black (0,0,0) = black pixel, anything else = white pixel.
|
||||
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
|
||||
x, y = d.xy(x, y)
|
||||
|
||||
if x < 0 || x >= d.width || y < 0 || y >= d.height {
|
||||
return
|
||||
}
|
||||
byteIndex := (y * (d.width / 8)) + (x / 8)
|
||||
if c.R == 0 && c.G == 0 && c.B == 0 {
|
||||
d.buffer[byteIndex] &^= 0x80 >> uint8(x%8)
|
||||
} else {
|
||||
d.buffer[byteIndex] |= 0x80 >> uint8(x%8)
|
||||
}
|
||||
}
|
||||
|
||||
// Display sends the buffer to the screen.
|
||||
func (d *Device) Display() error {
|
||||
if d.blocking {
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
|
||||
d.setCursor(0, 0)
|
||||
d.SendCommand(WRITE_RAM_BW)
|
||||
for i := uint32(0); i < d.bufferLength; i++ {
|
||||
d.SendData(d.buffer[i])
|
||||
}
|
||||
|
||||
d.turnOnDisplay()
|
||||
|
||||
if d.blocking {
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisplayWithBase writes the buffer to both BW and RED RAM then refreshes.
|
||||
// This is useful before partial updates to set the base image.
|
||||
func (d *Device) DisplayWithBase() error {
|
||||
if d.blocking {
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
|
||||
d.setCursor(0, 0)
|
||||
d.SendCommand(WRITE_RAM_BW)
|
||||
for i := uint32(0); i < d.bufferLength; i++ {
|
||||
d.SendData(d.buffer[i])
|
||||
}
|
||||
|
||||
d.setCursor(0, 0)
|
||||
d.SendCommand(WRITE_RAM_RED)
|
||||
for i := uint32(0); i < d.bufferLength; i++ {
|
||||
d.SendData(d.buffer[i])
|
||||
}
|
||||
|
||||
d.turnOnDisplay()
|
||||
|
||||
if d.blocking {
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisplayPartial performs a partial refresh of the display.
|
||||
// Call DisplayWithBase first to set the base image before using partial updates.
|
||||
func (d *Device) DisplayPartial() error {
|
||||
d.rst.Low()
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
d.rst.High()
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
d.setLUT(&lutPartial)
|
||||
|
||||
d.SendCommand(OTP_SELECTION_CONTROL)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x40)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x00)
|
||||
d.SendData(0x00)
|
||||
|
||||
d.SendCommand(BORDER_WAVEFORM_CONTROL)
|
||||
d.SendData(0x80)
|
||||
|
||||
d.SendCommand(DISPLAY_UPDATE_CONTROL_2)
|
||||
d.SendData(0xC0)
|
||||
d.SendCommand(MASTER_ACTIVATION)
|
||||
d.WaitUntilIdle()
|
||||
|
||||
d.setWindow(0, 0, d.width-1, d.height-1)
|
||||
d.setCursor(0, 0)
|
||||
|
||||
d.SendCommand(WRITE_RAM_BW)
|
||||
for i := uint32(0); i < d.bufferLength; i++ {
|
||||
d.SendData(d.buffer[i])
|
||||
}
|
||||
|
||||
d.turnOnDisplayPartial()
|
||||
d.WaitUntilIdle()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearDisplay erases the display.
|
||||
func (d *Device) ClearDisplay() {
|
||||
d.ClearBuffer()
|
||||
d.Display()
|
||||
}
|
||||
|
||||
// ClearBuffer sets the buffer to 0xFF (white).
|
||||
func (d *Device) ClearBuffer() {
|
||||
for i := uint32(0); i < d.bufferLength; i++ {
|
||||
d.buffer[i] = 0xFF
|
||||
}
|
||||
}
|
||||
|
||||
// Size returns the current size of the display.
|
||||
func (d *Device) Size() (w, h int16) {
|
||||
if d.rotation == ROTATION_90 || d.rotation == ROTATION_270 {
|
||||
return d.height, d.width
|
||||
}
|
||||
return d.width, d.height
|
||||
}
|
||||
|
||||
// SetRotation changes the rotation (clock-wise) of the device.
|
||||
func (d *Device) SetRotation(rotation Rotation) {
|
||||
d.rotation = rotation
|
||||
}
|
||||
|
||||
// SetBlocking changes the blocking flag of the device.
|
||||
func (d *Device) SetBlocking(blocking bool) {
|
||||
d.blocking = blocking
|
||||
}
|
||||
|
||||
// SetSpeed changes the refresh speed and reconfigures the device.
|
||||
func (d *Device) SetSpeed(speed Speed) {
|
||||
d.Configure(Config{
|
||||
Width: d.width,
|
||||
Height: d.height,
|
||||
Rotation: d.rotation,
|
||||
Speed: speed,
|
||||
Blocking: d.blocking,
|
||||
})
|
||||
}
|
||||
|
||||
// Sleep puts the display into deep sleep mode. A hardware reset is needed to wake it.
|
||||
func (d *Device) Sleep() {
|
||||
d.SendCommand(DEEP_SLEEP_MODE)
|
||||
d.SendData(0x01)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
// PowerOff disables the display analog/clock. Lighter than Sleep.
|
||||
func (d *Device) PowerOff() {
|
||||
d.SendCommand(DISPLAY_UPDATE_CONTROL_2)
|
||||
d.SendData(0x03)
|
||||
d.SendCommand(MASTER_ACTIVATION)
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
|
||||
func (d *Device) xy(x, y int16) (int16, int16) {
|
||||
switch d.rotation {
|
||||
case NO_ROTATION:
|
||||
return x, y
|
||||
case ROTATION_90:
|
||||
return d.width - y - 1, x
|
||||
case ROTATION_180:
|
||||
return d.width - x - 1, d.height - y - 1
|
||||
case ROTATION_270:
|
||||
return y, d.height - x - 1
|
||||
}
|
||||
return x, y
|
||||
}
|
||||
|
||||
func (d *Device) setWindow(xStart, yStart, xEnd, yEnd int16) {
|
||||
d.SendCommand(SET_RAM_X_ADDRESS)
|
||||
d.SendData(uint8((xStart >> 3) & 0xFF))
|
||||
d.SendData(uint8((xEnd >> 3) & 0xFF))
|
||||
|
||||
d.SendCommand(SET_RAM_Y_ADDRESS)
|
||||
d.SendData(uint8(yStart & 0xFF))
|
||||
d.SendData(uint8((yStart >> 8) & 0xFF))
|
||||
d.SendData(uint8(yEnd & 0xFF))
|
||||
d.SendData(uint8((yEnd >> 8) & 0xFF))
|
||||
}
|
||||
|
||||
func (d *Device) setCursor(x, y int16) {
|
||||
d.SendCommand(SET_RAM_X_COUNTER)
|
||||
d.SendData(uint8(x & 0xFF))
|
||||
|
||||
d.SendCommand(SET_RAM_Y_COUNTER)
|
||||
d.SendData(uint8(y & 0xFF))
|
||||
d.SendData(uint8((y >> 8) & 0xFF))
|
||||
}
|
||||
|
||||
func (d *Device) turnOnDisplay() {
|
||||
d.SendCommand(DISPLAY_UPDATE_CONTROL_2)
|
||||
d.SendData(0xC7)
|
||||
d.SendCommand(MASTER_ACTIVATION)
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
|
||||
func (d *Device) turnOnDisplayPartial() {
|
||||
d.SendCommand(DISPLAY_UPDATE_CONTROL_2)
|
||||
d.SendData(0x0F)
|
||||
d.SendCommand(MASTER_ACTIVATION)
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
|
||||
func (d *Device) setLUT(lut *[159]uint8) {
|
||||
d.SendCommand(WRITE_LUT_REGISTER)
|
||||
for i := 0; i < 153; i++ {
|
||||
d.SendData(lut[i])
|
||||
}
|
||||
d.WaitUntilIdle()
|
||||
}
|
||||
|
||||
func (d *Device) setLUTByHost(lut *[159]uint8) {
|
||||
d.setLUT(lut)
|
||||
|
||||
d.SendCommand(END_OPTION)
|
||||
d.SendData(lut[153])
|
||||
|
||||
d.SendCommand(GATE_DRIVING_VOLTAGE)
|
||||
d.SendData(lut[154])
|
||||
|
||||
d.SendCommand(SOURCE_DRIVING_VOLTAGE)
|
||||
d.SendData(lut[155])
|
||||
d.SendData(lut[156])
|
||||
d.SendData(lut[157])
|
||||
|
||||
d.SendCommand(WRITE_VCOM_REGISTER)
|
||||
d.SendData(lut[158])
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package epd2in9v2
|
||||
|
||||
// Commands from SSD1680 datasheet
|
||||
const (
|
||||
EPD_WIDTH = 128
|
||||
EPD_HEIGHT = 296
|
||||
|
||||
DRIVER_OUTPUT_CONTROL = 0x01
|
||||
GATE_DRIVING_VOLTAGE = 0x03
|
||||
SOURCE_DRIVING_VOLTAGE = 0x04
|
||||
DEEP_SLEEP_MODE = 0x10
|
||||
DATA_ENTRY_MODE = 0x11
|
||||
SW_RESET = 0x12
|
||||
MASTER_ACTIVATION = 0x20
|
||||
DISPLAY_UPDATE_CONTROL_1 = 0x21
|
||||
DISPLAY_UPDATE_CONTROL_2 = 0x22
|
||||
WRITE_RAM_BW = 0x24
|
||||
WRITE_RAM_RED = 0x26
|
||||
VCOM_SENSE = 0x28
|
||||
VCOM_SENSE_DURATION = 0x29
|
||||
PROGRAM_VCOM_OTP = 0x2A
|
||||
WRITE_VCOM_CONTROL = 0x2B
|
||||
WRITE_VCOM_REGISTER = 0x2C
|
||||
OTP_READ_DISPLAY_OPTION = 0x2D
|
||||
USER_ID_READ = 0x2E
|
||||
PROGRAM_WS_OTP = 0x30
|
||||
LOAD_WS_OTP = 0x31
|
||||
WRITE_LUT_REGISTER = 0x32
|
||||
PROGRAM_OTP_SELECTION = 0x36
|
||||
OTP_SELECTION_CONTROL = 0x37
|
||||
WRITE_USER_ID = 0x38
|
||||
OTP_PROGRAM_MODE = 0x39
|
||||
BORDER_WAVEFORM_CONTROL = 0x3C
|
||||
END_OPTION = 0x3F
|
||||
SET_RAM_X_ADDRESS = 0x44
|
||||
SET_RAM_Y_ADDRESS = 0x45
|
||||
SET_RAM_X_COUNTER = 0x4E
|
||||
SET_RAM_Y_COUNTER = 0x4F
|
||||
SET_ANALOG_BLOCK_CONTROL = 0x74
|
||||
SET_DIGITAL_BLOCK_CONTROL = 0x7E
|
||||
|
||||
NO_ROTATION Rotation = 0
|
||||
ROTATION_90 Rotation = 1
|
||||
ROTATION_180 Rotation = 2
|
||||
ROTATION_270 Rotation = 3
|
||||
|
||||
SPEED_DEFAULT Speed = 0
|
||||
SPEED_FAST Speed = 1
|
||||
SPEED_PARTIAL Speed = 2
|
||||
)
|
||||
+49
-5
@@ -17,7 +17,8 @@ import (
|
||||
// the new assembly implementation - no fiddly timings to calculate and no nops
|
||||
// to count!
|
||||
//
|
||||
// Right now this is specific to Cortex-M chips and assume the following things:
|
||||
// Right now this is specific to specific chips:
|
||||
// On Cortex-M chips it assume the following things:
|
||||
// - Arithmetic operations (shift, add, sub) take up 1 clock cycle.
|
||||
// - The nop instruction also takes up 1 clock cycle.
|
||||
// - Store instructions (to the GPIO pins) take up 2 clock cycles.
|
||||
@@ -25,8 +26,15 @@ import (
|
||||
// depends on whether the branch is taken or not. On the M4, the documentation
|
||||
// is less clear but it appears the instruction is still 1 to 3 cycles
|
||||
// (possibly including some branch prediction).
|
||||
// It is certainly possible to extend this to other architectures, such as AVR
|
||||
// and RISC-V if needed.
|
||||
// On RISC-V chips it assumes the following things:
|
||||
// - Arithmetic operations (shift, add, sub) take up 1 clock cycle.
|
||||
// - The nop instruction also takes up 1 clock cycle.
|
||||
// - Store instructions (to the GPIO pins) take up 1 clock cycle.
|
||||
// - Branch instructions can take up 1 or 3 clock cycles, depending on branch
|
||||
// prediction. This is based on the SiFive FE310 CPU, but hopefully it
|
||||
// generalizes to other RISC-V chips as well.
|
||||
|
||||
// It is certainly possible to extend this to other architectures, such as AVR as needed.
|
||||
//
|
||||
// Here are two important resources. For the timings:
|
||||
// https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/
|
||||
@@ -45,6 +53,7 @@ type architectureImpl struct {
|
||||
maxBaseCyclesT1H int
|
||||
minBaseCyclesTLD int
|
||||
valueTemplate string // template for how to pass the 'c' byte to assembly
|
||||
funcAttr string // C function attribute (default: always_inline)
|
||||
template string // assembly template
|
||||
}
|
||||
|
||||
@@ -83,7 +92,7 @@ var architectures = map[string]architectureImpl{
|
||||
// - branches are 1 or 3 cycles, depending on branch prediction
|
||||
// - ALU operations are 1 cycle (as on most CPUs)
|
||||
// Hopefully this generalizes to other chips.
|
||||
buildTag: "tinygo.riscv32",
|
||||
buildTag: "tinygo.riscv32 && !esp32c3",
|
||||
minBaseCyclesT0H: 1 + 1 + 1, // shift + branch (not taken) + store
|
||||
maxBaseCyclesT0H: 1 + 3 + 1, // shift + branch (not taken) + store
|
||||
minBaseCyclesT1H: 1 + 1 + 1, // shift + branch (taken) + store
|
||||
@@ -103,6 +112,37 @@ var architectures = map[string]architectureImpl{
|
||||
@DELAY3
|
||||
addi %[i], %[i], -1 // [1]
|
||||
bnez %[i], 1b // [1/3] send_bit
|
||||
`,
|
||||
},
|
||||
"esp32c3": {
|
||||
// ESP32-C3 RISC-V core:
|
||||
// - stores are 1 cycle
|
||||
// - branches are 1 or 3 cycles
|
||||
// - ALU operations are 1 cycle
|
||||
// Uses the same instruction timing as the SiFive FE310, but the
|
||||
// function is placed in IRAM instead of flash to avoid instruction
|
||||
// cache miss stalls that would destroy WS2812 timing.
|
||||
buildTag: "esp32c3",
|
||||
minBaseCyclesT0H: 1 + 1 + 1, // shift + branch (not taken) + store
|
||||
maxBaseCyclesT0H: 1 + 3 + 1, // shift + branch (not taken) + store
|
||||
minBaseCyclesT1H: 1 + 1 + 1, // shift + branch (taken) + store
|
||||
maxBaseCyclesT1H: 1 + 3 + 1, // shift + branch (taken) + store
|
||||
minBaseCyclesTLD: 1 + 1 + 1, // subtraction + branch + store (in next cycle)
|
||||
valueTemplate: "(uint32_t)c << 23",
|
||||
funcAttr: `__attribute__((section(".iram1"), noinline))`,
|
||||
template: `
|
||||
1: // send_bit
|
||||
sw %[maskSet], %[portSet] // [1] T0H and T0L start here
|
||||
@DELAY1
|
||||
slli %[value], %[value], 1 // [1] shift value left by 1
|
||||
bltz %[value], 2f // [1/3] skip_store
|
||||
sw %[maskClear], %[portClear] // [1] T0H -> T0L transition
|
||||
2: // skip_store
|
||||
@DELAY2
|
||||
sw %[maskClear], %[portClear] // [1] T1H -> T1L transition
|
||||
@DELAY3
|
||||
addi %[i], %[i], -1 // [1]
|
||||
bnez %[i], 1b // [1/3] send_bit
|
||||
`,
|
||||
},
|
||||
}
|
||||
@@ -208,7 +248,11 @@ func writeCAssembly(f *os.File, arch string, megahertz int) error {
|
||||
// ignore I/O errors.
|
||||
buf := &bytes.Buffer{}
|
||||
fmt.Fprintf(buf, "\n")
|
||||
fmt.Fprintf(buf, "__attribute__((always_inline))\nvoid ws2812_writeByte%d(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {\n", megahertz)
|
||||
funcAttr := archImpl.funcAttr
|
||||
if funcAttr == "" {
|
||||
funcAttr = "__attribute__((always_inline))"
|
||||
}
|
||||
fmt.Fprintf(buf, "%s\nvoid ws2812_writeByte%d(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {\n", funcAttr, megahertz)
|
||||
fmt.Fprintf(buf, " // Timings:\n")
|
||||
fmt.Fprintf(buf, " // T0H: %2d - %2d cycles or %.1fns - %.1fns\n", actualMinCyclesT0H, actualMaxCyclesT0H, actualMinNanosecondsT0H, actualMaxNanosecondsT0H)
|
||||
fmt.Fprintf(buf, " // T1H: %2d - %2d cycles or %.1fns - %.1fns\n", actualMinCyclesT1H, actualMaxCyclesT1H, actualMinNanosecondsT1H, actualMaxNanosecondsT1H)
|
||||
|
||||
@@ -1281,6 +1281,377 @@ void ws2812_writeByte150(char c, uint32_t *portSet, uint32_t *portClear, uint32_
|
||||
[portClear]"m"(*portClear));
|
||||
}
|
||||
|
||||
__attribute__((always_inline))
|
||||
void ws2812_writeByte160(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {
|
||||
// Timings:
|
||||
// T0H: 56 - 58 cycles or 350.0ns - 362.5ns
|
||||
// T1H: 168 - 170 cycles or 1050.0ns - 1062.5ns
|
||||
// TLD: 184 - cycles or 1150.0ns -
|
||||
uint32_t value = (uint32_t)c << 24;
|
||||
char i = 8;
|
||||
__asm__ __volatile__(
|
||||
"1: @ send_bit\n"
|
||||
"\t str %[maskSet], %[portSet] @ [2] T0H and T0L start here\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t lsls %[value], #1 @ [1]\n"
|
||||
"\t bcs.n 2f @ [1/3] skip_store\n"
|
||||
"\t str %[maskClear], %[portClear] @ [2] T0H -> T0L transition\n"
|
||||
"\t2: @ skip_store\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t str %[maskClear], %[portClear] @ [2] T1H -> T1L transition\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t subs %[i], #1 @ [1]\n"
|
||||
"\t beq.n 3f @ [1/3] end\n"
|
||||
"\t b 1b @ [1/3] send_bit\n"
|
||||
"\t3: @ end\n"
|
||||
: [value]"+r"(value),
|
||||
[i]"+r"(i)
|
||||
: [maskSet]"r"(maskSet),
|
||||
[portSet]"m"(*portSet),
|
||||
[maskClear]"r"(maskClear),
|
||||
[portClear]"m"(*portClear));
|
||||
}
|
||||
|
||||
__attribute__((always_inline))
|
||||
void ws2812_writeByte168(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {
|
||||
// Timings:
|
||||
@@ -2192,6 +2563,16 @@ func (d Device) writeByte150(c byte) {
|
||||
interrupt.Restore(mask)
|
||||
}
|
||||
|
||||
func (d Device) writeByte160(c byte) {
|
||||
portSet, maskSet := d.Pin.PortMaskSet()
|
||||
portClear, maskClear := d.Pin.PortMaskClear()
|
||||
|
||||
mask := interrupt.Disable()
|
||||
C.ws2812_writeByte160(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
|
||||
|
||||
interrupt.Restore(mask)
|
||||
}
|
||||
|
||||
func (d Device) writeByte168(c byte) {
|
||||
portSet, maskSet := d.Pin.PortMaskSet()
|
||||
portClear, maskClear := d.Pin.PortMaskClear()
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
//go:build esp32c3
|
||||
|
||||
package ws2812
|
||||
|
||||
// Warning: autogenerated file. Instead of modifying this file, change
|
||||
// gen-ws2812.go and run "go generate".
|
||||
|
||||
import "runtime/interrupt"
|
||||
import "unsafe"
|
||||
|
||||
/*
|
||||
#include <stdint.h>
|
||||
|
||||
__attribute__((section(".iram1"), noinline))
|
||||
void ws2812_writeByte160(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {
|
||||
// Timings:
|
||||
// T0H: 56 - 58 cycles or 350.0ns - 362.5ns
|
||||
// T1H: 168 - 170 cycles or 1050.0ns - 1062.5ns
|
||||
// TLD: 184 - cycles or 1150.0ns -
|
||||
uint32_t value = (uint32_t)c << 23;
|
||||
char i = 8;
|
||||
__asm__ __volatile__(
|
||||
"1: // send_bit\n"
|
||||
"\t sw %[maskSet], %[portSet] // [1] T0H and T0L start here\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t slli %[value], %[value], 1 // [1] shift value left by 1\n"
|
||||
"\t bltz %[value], 2f // [1/3] skip_store\n"
|
||||
"\t sw %[maskClear], %[portClear] // [1] T0H -> T0L transition\n"
|
||||
"\t2: // skip_store\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t sw %[maskClear], %[portClear] // [1] T1H -> T1L transition\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t addi %[i], %[i], -1 // [1]\n"
|
||||
"\t bnez %[i], 1b // [1/3] send_bit\n"
|
||||
: [value]"+r"(value),
|
||||
[i]"+r"(i)
|
||||
: [maskSet]"r"(maskSet),
|
||||
[portSet]"m"(*portSet),
|
||||
[maskClear]"r"(maskClear),
|
||||
[portClear]"m"(*portClear));
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func (d Device) writeByte160(c byte) {
|
||||
portSet, maskSet := d.Pin.PortMaskSet()
|
||||
portClear, maskClear := d.Pin.PortMaskClear()
|
||||
|
||||
mask := interrupt.Disable()
|
||||
C.ws2812_writeByte160(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
|
||||
|
||||
interrupt.Restore(mask)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build tinygo.riscv32
|
||||
//go:build tinygo.riscv32 && !esp32c3
|
||||
|
||||
package ws2812
|
||||
|
||||
@@ -11,378 +11,6 @@ import "unsafe"
|
||||
/*
|
||||
#include <stdint.h>
|
||||
|
||||
__attribute__((always_inline))
|
||||
void ws2812_writeByte160(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {
|
||||
// Timings:
|
||||
// T0H: 56 - 58 cycles or 350.0ns - 362.5ns
|
||||
// T1H: 168 - 170 cycles or 1050.0ns - 1062.5ns
|
||||
// TLD: 184 - cycles or 1150.0ns -
|
||||
uint32_t value = (uint32_t)c << 23;
|
||||
char i = 8;
|
||||
__asm__ __volatile__(
|
||||
"1: // send_bit\n"
|
||||
"\t sw %[maskSet], %[portSet] // [1] T0H and T0L start here\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t slli %[value], %[value], 1 // [1] shift value left by 1\n"
|
||||
"\t bltz %[value], 2f // [1/3] skip_store\n"
|
||||
"\t sw %[maskClear], %[portClear] // [1] T0H -> T0L transition\n"
|
||||
"\t2: // skip_store\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t sw %[maskClear], %[portClear] // [1] T1H -> T1L transition\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t addi %[i], %[i], -1 // [1]\n"
|
||||
"\t bnez %[i], 1b // [1/3] send_bit\n"
|
||||
: [value]"+r"(value),
|
||||
[i]"+r"(i)
|
||||
: [maskSet]"r"(maskSet),
|
||||
[portSet]"m"(*portSet),
|
||||
[maskClear]"r"(maskClear),
|
||||
[portClear]"m"(*portClear));
|
||||
}
|
||||
|
||||
__attribute__((always_inline))
|
||||
void ws2812_writeByte320(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {
|
||||
// Timings:
|
||||
@@ -1109,16 +737,6 @@ void ws2812_writeByte320(char c, uint32_t *portSet, uint32_t *portClear, uint32_
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func (d Device) writeByte160(c byte) {
|
||||
portSet, maskSet := d.Pin.PortMaskSet()
|
||||
portClear, maskClear := d.Pin.PortMaskClear()
|
||||
|
||||
mask := interrupt.Disable()
|
||||
C.ws2812_writeByte160(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
|
||||
|
||||
interrupt.Restore(mask)
|
||||
}
|
||||
|
||||
func (d Device) writeByte320(c byte) {
|
||||
portSet, maskSet := d.Pin.PortMaskSet()
|
||||
portClear, maskClear := d.Pin.PortMaskClear()
|
||||
|
||||
+42
-22
@@ -1,8 +1,12 @@
|
||||
// Package ws2812 implements a driver for WS2812 and SK6812 RGB LED strips.
|
||||
//
|
||||
// On most platforms NewWS2812 uses bit-banging.
|
||||
// On RP2040/RP2350 it uses PIO for hardware-timed control.
|
||||
package ws2812 // import "tinygo.org/x/drivers/ws2812"
|
||||
|
||||
//go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 150 168 200
|
||||
//go:generate go run gen-ws2812.go -arch=tinygoriscv 160 320
|
||||
//go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 150 160 168 200
|
||||
//go:generate go run gen-ws2812.go -arch=tinygoriscv 320
|
||||
//go:generate go run gen-ws2812.go -arch=esp32c3 160
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -15,7 +19,8 @@ var errUnknownClockSpeed = errors.New("ws2812: unknown CPU clock speed")
|
||||
// Device wraps a pin object for an easy driver interface.
|
||||
type Device struct {
|
||||
Pin machine.Pin
|
||||
writeColorFunc func(Device, []color.RGBA) error
|
||||
brightness uint8
|
||||
writeColorFunc func(Device, []color.RGBA, uint8) error
|
||||
}
|
||||
|
||||
// deprecated, use NewWS2812 or NewSK6812 depending on which device you want.
|
||||
@@ -24,26 +29,30 @@ func New(pin machine.Pin) Device {
|
||||
return NewWS2812(pin)
|
||||
}
|
||||
|
||||
// New returns a new WS2812(RGB) driver.
|
||||
// It does not touch the pin object: you have
|
||||
// to configure it as an output pin before calling New.
|
||||
// NewWS2812 returns a new WS2812(RGB) driver.
|
||||
// On RP2040/RP2350, it uses PIO for hardware-timed control.
|
||||
// On other platforms, you must configure the pin as output before calling this.
|
||||
func NewWS2812(pin machine.Pin) Device {
|
||||
return Device{
|
||||
Pin: pin,
|
||||
writeColorFunc: writeColorsRGB,
|
||||
}
|
||||
return newWS2812Device(pin)
|
||||
}
|
||||
|
||||
// New returns a new SK6812(RGBA) driver.
|
||||
// It does not touch the pin object: you have
|
||||
// to configure it as an output pin before calling New.
|
||||
// NewSK6812 returns a new SK6812W/RGBW driver (4 channels, GRBW order, 32-bit protocol), for the 3 channels version use NewWS2812
|
||||
// Use this for SK6812W strips that have a dedicated white channel controlled via color.A.
|
||||
// It does not touch the pin object: you have to configure it as an output pin before
|
||||
// calling this.
|
||||
func NewSK6812(pin machine.Pin) Device {
|
||||
return Device{
|
||||
Pin: pin,
|
||||
brightness: 255,
|
||||
writeColorFunc: writeColorsRGBA,
|
||||
}
|
||||
}
|
||||
|
||||
// SetBrightness sets the global brightness (0-255).
|
||||
func (d *Device) SetBrightness(b uint8) {
|
||||
d.brightness = b
|
||||
}
|
||||
|
||||
// Write the raw bitstring out using the WS2812 protocol.
|
||||
func (d Device) Write(buf []byte) (n int, err error) {
|
||||
for _, c := range buf {
|
||||
@@ -55,24 +64,35 @@ func (d Device) Write(buf []byte) (n int, err error) {
|
||||
// Write the given color slice out using the WS2812 protocol.
|
||||
// Colors are sent out in the usual GRB(A) format.
|
||||
func (d Device) WriteColors(buf []color.RGBA) (err error) {
|
||||
return d.writeColorFunc(d, buf)
|
||||
return d.writeColorFunc(d, buf, d.brightness)
|
||||
}
|
||||
|
||||
func writeColorsRGB(d Device, buf []color.RGBA) (err error) {
|
||||
func writeColorsRGB(d Device, buf []color.RGBA, brightness uint8) (err error) {
|
||||
for _, color := range buf {
|
||||
d.WriteByte(color.G) // green
|
||||
d.WriteByte(color.R) // red
|
||||
err = d.WriteByte(color.B) // blue
|
||||
r, g, b := applyBrightness(color, brightness)
|
||||
d.WriteByte(g) // green
|
||||
d.WriteByte(r) // red
|
||||
err = d.WriteByte(b) // blue
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func writeColorsRGBA(d Device, buf []color.RGBA) (err error) {
|
||||
func writeColorsRGBA(d Device, buf []color.RGBA, brightness uint8) (err error) {
|
||||
for _, color := range buf {
|
||||
d.WriteByte(color.G) // green
|
||||
d.WriteByte(color.R) // red
|
||||
d.WriteByte(color.B) // blue
|
||||
r, g, b := applyBrightness(color, brightness)
|
||||
|
||||
d.WriteByte(g) // green
|
||||
d.WriteByte(r) // red
|
||||
d.WriteByte(b) // blue
|
||||
err = d.WriteByte(color.A) // alpha
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// applyBrightness scales a color by the brightness value.
|
||||
func applyBrightness(c color.RGBA, brightness uint8) (r, g, b uint8) {
|
||||
r = uint8((uint16(c.R) * uint16(brightness)) >> 8)
|
||||
g = uint8((uint16(c.G) * uint16(brightness)) >> 8)
|
||||
b = uint8((uint16(c.B) * uint16(brightness)) >> 8)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ func (d Device) WriteByte(c byte) error {
|
||||
case 150_000_000: // 150MHz, e.g. rp2350
|
||||
d.writeByte150(c)
|
||||
return nil
|
||||
case 160_000_000: // 160MHz, e.g. stm32u585
|
||||
d.writeByte160(c)
|
||||
return nil
|
||||
case 168_000_000: // 168MHz, e.g. stm32f405
|
||||
d.writeByte168(c)
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build esp32c3
|
||||
|
||||
package ws2812
|
||||
|
||||
import "machine"
|
||||
|
||||
// Send a single byte using the WS2812 protocol.
|
||||
func (d Device) WriteByte(c byte) error {
|
||||
switch machine.CPUFrequency() {
|
||||
case 160_000_000: // 160MHz
|
||||
d.writeByte160(c)
|
||||
return nil
|
||||
default:
|
||||
return errUnknownClockSpeed
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//go:build !rp2040 && !rp2350
|
||||
|
||||
package ws2812
|
||||
|
||||
import "machine"
|
||||
|
||||
// newWS2812Device creates a WS2812 device using the bit-bang driver.
|
||||
func newWS2812Device(pin machine.Pin) Device {
|
||||
return Device{Pin: pin, brightness: 255, writeColorFunc: writeColorsRGB}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
//go:build rp2040 || rp2350
|
||||
|
||||
package ws2812
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"machine"
|
||||
"runtime"
|
||||
|
||||
pio "github.com/tinygo-org/pio/rp2-pio"
|
||||
"github.com/tinygo-org/pio/rp2-pio/piolib"
|
||||
)
|
||||
|
||||
// newWS2812Device creates a WS2812 device using PIO for hardware-timed control.
|
||||
// If PIO initialization fails, it falls back to the bit-bang driver.
|
||||
func newWS2812Device(pin machine.Pin) Device {
|
||||
sm, err := pio.PIO0.ClaimStateMachine()
|
||||
if err != nil {
|
||||
return Device{Pin: pin, brightness: 255, writeColorFunc: writeColorsRGB}
|
||||
}
|
||||
ws, err := piolib.NewWS2812B(sm, pin)
|
||||
if err != nil {
|
||||
return Device{Pin: pin, brightness: 255, writeColorFunc: writeColorsRGB}
|
||||
}
|
||||
return Device{
|
||||
Pin: pin,
|
||||
brightness: 255,
|
||||
writeColorFunc: func(_ Device, buf []color.RGBA, brightness uint8) error {
|
||||
for _, c := range buf {
|
||||
r, g, b := applyBrightness(c, brightness)
|
||||
for ws.IsQueueFull() {
|
||||
runtime.Gosched()
|
||||
}
|
||||
ws.PutRGB(r, g, b)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build tinygo.riscv32
|
||||
//go:build tinygo.riscv32 && !esp32c3
|
||||
|
||||
package ws2812
|
||||
|
||||
@@ -7,9 +7,6 @@ import "machine"
|
||||
// Send a single byte using the WS2812 protocol.
|
||||
func (d Device) WriteByte(c byte) error {
|
||||
switch machine.CPUFrequency() {
|
||||
case 160_000_000: // 160MHz, e.g. esp32c3
|
||||
d.writeByte160(c)
|
||||
return nil
|
||||
case 320_000_000: // 320MHz, e.g. fe310
|
||||
d.writeByte320(c)
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user