Compare commits

..

30 Commits

Author SHA1 Message Date
deadprogram dc6edbb694 ws2812: fix PIO TX FIFO overflow on rp2040/rp2350 dropping LEDs
PutRGB calls TxPut which is non-blocking and silently discards data
when the TX FIFO is full.

Wait for FIFO space before each PutRGB using runtime.Gosched() to
yield cooperatively, matching the pattern used by piolib.WriteRaw.

Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-07-06 20:21:35 +02:00
deadprogram 1d695a231a ws2812: add support for the ESP32-C3 processor
This adds support to the WS2812 for the ESP32-C3 processor
which is a RISC-V processor from Espressif.

Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-06-04 19:49:50 +02:00
deadprogram 04acd8e666 netlink: add Hostname field and some godocs comments
Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-05-17 08:51:06 +01:00
Joost bb2d365868 RP002-1.0.5 states that max TX power is 16 for eu868. Later, more subtle TX power tactics will be implemented. (#866)
Co-authored-by: Joost Helberg <joost@helberg.nl>
2026-05-17 08:00:40 +02:00
deadprogram f459992f3c ws2812: add support for 160MHz cortex-m processors
This adds hardware timing support for driving WS2812 LEDs on Cortex-M microcontrollers running at 160MHz (such as the STM32U585).

- Updated `go:generate` directive to include 160MHz.
- Generated the corresponding `ws2812_writeByte160` assembly routine.
- Added a switch case in `WriteByte` to handle generic 160MHz processors.

Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-05-17 06:48:52 +01:00
Joel Wetzell 62663c1832 add SPI driver for semtech sx128x chips (#864)
* add SPI driver for semtech sx128x chips

Co-authored-by: Copilot <copilot@github.com>

* handle busy loop better

* switch to time based busy timeout

Co-authored-by: Copilot <copilot@github.com>

* comment functions

* start on using types for function inputs

* use types where applicable and align with datasheet more

Co-authored-by: Copilot <copilot@github.com>

* work on exporting less constants

* only export "actionable" errors

* combine identical constants

* add crude lora rx and tx examples

* change from type aliases to local types

---------

Co-authored-by: Copilot <copilot@github.com>
2026-05-04 09:42:03 -03:00
deadprogram 8f372935ac Release 0.35.0
Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-04-21 09:44:44 +01:00
deadprogram fb3062433a license: remove year
Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-04-21 09:44:44 +01:00
deadprogram 9fc1c0aedc examples/ws2812: update to accomodate Arduino Uno rename for TinyGo 0.41.0 release
Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-04-21 09:44:44 +01:00
nobonobo 1c10dea443 add support extended id for mcp2515 (#857)
* add support extended id for mcp2515
* fix mcp2512 example code
* revert Pin control
2026-04-20 11:51:56 +02:00
Daniel Esteban e960a6ff57 fix issue #858 2026-04-17 10:27:04 +01:00
Daniel Esteban 2673cc1e9a fix st7789 scroll on rotated displays 2026-04-16 10:28:13 +01:00
Joel Wetzell 0034fc511a fill out more constants for lora device 2026-04-11 17:29:47 +01:00
deadprogram a0c5da601f unoqmatrix: LED matrix on the Arduino Uno Q
Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-04-11 17:07:17 +01:00
Carlos Henrique Guardão Gandarez 21a7d0a96a ws2812: add brightness control 2026-04-11 16:54:48 +01:00
Avinal Kumar e232a4f136 waveshare(ssd1680): Add driver for waveshare 2.9 inch v2 epaper
- Waveshare 2.9in v2 epaper uses a different IC (SSD1680) than the v1. This commit
takes the code from v1 and modifies them for v2.
- Datasheets:
    - 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
- Code reference: https://github.com/waveshareteam/e-Paper/raw/refs/heads/master/RaspberryPi_JetsonNano/c/lib/e-Paper/EPD_2in9_V2.c
- Fixes #627

Signed-off-by: Avinal Kumar <avinal.xlvii@gmail.com>
Assisted-by: Claude Code
2026-04-11 16:41:34 +01:00
Carlos Henrique Guardão Gandarez 4071028e85 ws2812: add PIO support for RP2040/RP2350
Integrate PIO support directly into the existing Device.
NewWS2812() now uses PIO for hardware-timed control
on RP2040/RP2350 and falls back to bit-banging on other platforms.

No changes to the exported API surface.
2026-04-07 21:38:57 +02:00
Daniel Esteban a514169c37 fix ST7789 driver when rotated 90º 2026-03-29 20:08:37 +02:00
deadprogram b480978e1a docs: update count of drivers
Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-02-25 11:13:21 +01:00
deadprogram 9eb95a4651 make: add tasks for counting the number of drivers, and displaying a list of them
Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-02-25 11:13:21 +01:00
deadprogram d6114c9f6d sponsorship: add explicit callout/link in README to help out TinyGo
Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-02-12 13:05:25 +01:00
deadprogram 0e2fb829ef gps: improvements and corrections for config commands
This contains some improvements and corrections for the gps driver
It adds some additional functions for different modes (automobile, bike, etc)
and also ignores the return results from any config commands.

Basically due to the fact that there is a constant stream of updates
coming from NMEA messages, the results from sending any UBX commands
are getting lost. Better to just ignore them for now.

Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-01-14 19:55:43 +00:00
deadprogram 892265b733 gps: export some errors for checking/supression from client
Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-01-08 13:05:41 +00:00
Pat Whittingslow b390e3225a Suggestions by pato for GPS UBX support (#831)
* apply suggestions by pato

* whoopsie on inverted condition
2026-01-08 13:05:41 +00:00
deadprogram 1513808425 gps: revamp validSentence() to avoid heap allocation for errors. This error can occur too frequently to allow for such allocations
Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-01-08 13:05:41 +00:00
deadprogram 0a41786a77 gps: improve implementation for UBX config commands
Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-01-08 13:05:41 +00:00
deadprogram c21cd39813 si5351: complete refactor for more complete interface
This completely refactors the interface and implementation
for the si5351 clock generator. The interface based on the
Arduino implementation was both somewhat hard to work with
and also missing a number of important features that are
needed to use this chip for RF communication.

Instead this new implementation draws inspiration from the
efforts of the Traquino community mostly using the rp2040
processor.

The TinyGo implementation is based on the patterns and code
in the drivers repo for other i2c devices. It also includes
some basic unit tests which are not comprehensive but at
least provide some coverage.

Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-01-07 19:58:27 +00:00
deadprogram a35786be70 sx127x: add functions used for FSK radio communication
Signed-off-by: deadprogram <ron@hybridgroup.com>
2026-01-03 16:49:21 +00:00
Yurii Soldak 2a42fa7cbb st7735: remove dependency on the machine package 2025-12-22 08:59:05 +00:00
deadprogram 5d96a56603 build: use the latest TinyGo release container instead of the dev container for builds.
Signed-off-by: deadprogram <ron@hybridgroup.com>
2025-12-17 10:28:24 +00:00
61 changed files with 6500 additions and 1293 deletions
+1
View File
@@ -1,2 +1,3 @@
# These are supported funding model platforms
open_collective: tinygo
+7 -6
View File
@@ -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
+37
View File
@@ -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 -1
View File
@@ -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
+14
View File
@@ -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
+4 -1
View File
@@ -3,11 +3,14 @@
[![PkgGoDev](https://pkg.go.dev/badge/tinygo.org/x/drivers)](https://pkg.go.dev/tinygo.org/x/drivers) [![Build](https://github.com/tinygo-org/drivers/actions/workflows/build.yml/badge.svg?branch=dev)](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
+18 -6
View File
@@ -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)
}
+1 -1
View File
@@ -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
View File
@@ -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)
}
}
+106
View File
@@ -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")
}
+96
View File
@@ -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
}
+54
View File
@@ -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()
}
}
+137
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
//go:build arduino
//go:build arduino || arduino_uno
package main
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !digispark && !arduino
//go:build !digispark && !arduino && !arduino_uno && !xiao_esp32c3
package main
+11
View File
@@ -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 -2
View File
@@ -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
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+353
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 (
+2 -2
View File
@@ -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 {
+3
View File
@@ -6,6 +6,9 @@ const (
RadioEventTimeout
RadioEventWatchdog
RadioEventCrcError
RadioEventValidHeader
RadioEventCadDone
RadioEventCadDetected
RadioEventUnhandled
)
+45 -41
View File
@@ -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)
+7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+214
View File
@@ -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)
}
}
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+6
View File
@@ -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
View File
@@ -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)
}
}
+7
View File
@@ -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)
+52
View File
@@ -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)
)
+356
View File
@@ -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)
)
+19
View File
@@ -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")
)
+69
View File
@@ -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)
)
+768
View File
@@ -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 0s and 1s 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
}
+290
View File
@@ -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()
}
}
+325
View File
@@ -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])
}
}
}
+36
View File
@@ -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
View File
@@ -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"
+465
View File
@@ -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])
}
+50
View File
@@ -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
View File
@@ -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)
+381
View File
@@ -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()
+396
View File
@@ -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 -383
View File
@@ -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
View File
@@ -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
}
+3
View File
@@ -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
+16
View File
@@ -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
}
}
+10
View File
@@ -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}
}
+39
View File
@@ -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
View File
@@ -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