mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-09-03 05:19:01 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f6f6bc66f | |||
| e8faab0368 | |||
| 6791c13a1e | |||
| 119afb933c | |||
| be556a971c | |||
| e08205477c | |||
| dc6edbb694 | |||
| 1d695a231a | |||
| 04acd8e666 | |||
| bb2d365868 | |||
| f459992f3c | |||
| 62663c1832 |
@@ -0,0 +1,10 @@
|
||||
## Communication style
|
||||
Use ASD-STE-100 Simplified Technical English when you speak to the operator.
|
||||
|
||||
## Comments
|
||||
Use ASD-STE100 Simplified Technical English for content outside of the code like comments, PR description, PR title, github comments, and issues.
|
||||
|
||||
Generally omit extra redundant comments. If a comment is absolutely needed, always keep it brief, no more than 2 lines. Avoid extra use of colons, semicolons, and dashes. When comments are required due to some specific requirements, include a reference such as to a datasheet or other definitive source to explain why this is the case, including the chapter/section number, page number, and/or URL.
|
||||
|
||||
## No coding tool attributions
|
||||
Never add "created by XXX" or any other attributions from coding tools to any PRs, issues, comments, or anywhere else.
|
||||
@@ -1,3 +1,31 @@
|
||||
0.36.0
|
||||
---
|
||||
- **new devices**
|
||||
- **gdew0154m09**
|
||||
- add e-paper display driver
|
||||
- **scd30**
|
||||
- add CO2 sensor driver
|
||||
- **sd**
|
||||
- redesign of the `sdcard` package (#639)
|
||||
- **sx128x**
|
||||
- add SPI driver for Semtech sx128x chips (#864)
|
||||
|
||||
- **enhancements**
|
||||
- **lora**
|
||||
- set the maximum TX power to 16 for eu868 as in RP002-1.0.5 (#866)
|
||||
- **netlink**
|
||||
- add Hostname field and some godoc comments
|
||||
- **ws2812**
|
||||
- add support for the 160MHz cortex-m processors
|
||||
- add support for the ESP32-C3 processor
|
||||
|
||||
- **bugfixes**
|
||||
- **uc8151**
|
||||
- fix LUT timing calculations
|
||||
- **ws2812**
|
||||
- fix PIO TX FIFO overflow on rp2040/rp2350 that drops LEDs
|
||||
|
||||
|
||||
0.35.0
|
||||
---
|
||||
- **new devices**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
[](https://pkg.go.dev/tinygo.org/x/drivers) [](https://github.com/tinygo-org/drivers/actions/workflows/build.yml)
|
||||
|
||||
|
||||
This package provides a collection of over 140 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 145 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/
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"device/esp"
|
||||
"image/color"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/gdew0154m09"
|
||||
"tinygo.org/x/tinyfont"
|
||||
"tinygo.org/x/tinyfont/freemono"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Hold the M5Stack CoreInk power rail on.
|
||||
machine.GPIO12.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
machine.GPIO12.High()
|
||||
|
||||
cs := machine.IO9
|
||||
dc := machine.GPIO15
|
||||
rst := machine.IO0
|
||||
busy := machine.IO4
|
||||
cs.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
dc.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
rst.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
busy.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
cs.High()
|
||||
dc.High()
|
||||
rst.High()
|
||||
|
||||
// Enable and reset the ESP32 VSPI peripheral before configuring SPI3.
|
||||
esp.DPORT.SetPERIP_RST_EN_SPI3_RST(1)
|
||||
esp.DPORT.SetPERIP_CLK_EN_SPI3_CLK_EN(1)
|
||||
esp.DPORT.SetPERIP_RST_EN_SPI3_RST(0)
|
||||
if err := machine.SPI3.Configure(machine.SPIConfig{
|
||||
Frequency: gdew0154m09.Baudrate,
|
||||
SCK: machine.IO18,
|
||||
SDO: machine.IO23,
|
||||
SDI: machine.NoPin,
|
||||
Mode: gdew0154m09.SPIMode,
|
||||
}); err != nil {
|
||||
println("could not configure SPI:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
display := gdew0154m09.New(machine.SPI3, cs, dc, rst, busy)
|
||||
if err := display.Configure(gdew0154m09.DefaultConfig); err != nil {
|
||||
println("could not configure display:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
black := color.RGBA{A: 0xff}
|
||||
tinyfont.WriteLine(display, &freemono.Bold18pt7b, 15, 90, "TinyGo", black)
|
||||
for x := int16(0); x < gdew0154m09.Width; x++ {
|
||||
display.SetPixel(x, 0, black)
|
||||
display.SetPixel(x, gdew0154m09.Height-1, black)
|
||||
}
|
||||
for y := int16(0); y < gdew0154m09.Height; y++ {
|
||||
display.SetPixel(0, y, black)
|
||||
display.SetPixel(gdew0154m09.Width-1, y, black)
|
||||
}
|
||||
|
||||
if err := display.Display(); err != nil {
|
||||
println("could not refresh display:", err.Error())
|
||||
}
|
||||
for {
|
||||
time.Sleep(time.Hour)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/scd30"
|
||||
)
|
||||
|
||||
var sensor = scd30.New(machine.I2C0)
|
||||
|
||||
func main() {
|
||||
// The SCD30 requires clock stretching and supports I2C speeds up to 100kHz.
|
||||
if err := machine.I2C0.Configure(machine.I2CConfig{Frequency: 50 * machine.KHz}); err != nil {
|
||||
println("could not configure I2C:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if !sensor.Connected() {
|
||||
println("SCD30 not detected")
|
||||
return
|
||||
}
|
||||
if err := sensor.Configure(scd30.DefaultConfig); err != nil {
|
||||
println("could not configure SCD30:", err.Error())
|
||||
return
|
||||
}
|
||||
if err := sensor.StartContinuousMeasurement(0); err != nil {
|
||||
println("could not start SCD30:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
ready, err := sensor.DataReady()
|
||||
if err != nil {
|
||||
println("could not read SCD30 status:", err.Error())
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
if !ready {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := sensor.Update(drivers.AllMeasurements); err != nil {
|
||||
println("could not read SCD30 measurement:", err.Error())
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
println("CO2 (ppm):", sensor.CO2())
|
||||
println("temperature (mC):", sensor.Temperature())
|
||||
println("humidity (0.01%):", sensor.Humidity())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"machine"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/sd"
|
||||
)
|
||||
|
||||
const (
|
||||
SPI_RX_PIN = machine.GP16
|
||||
SPI_TX_PIN = machine.GP19
|
||||
SPI_SCK_PIN = machine.GP18
|
||||
SPI_CS_PIN = machine.GP15
|
||||
)
|
||||
|
||||
var (
|
||||
spibus = machine.SPI0
|
||||
spicfg = machine.SPIConfig{
|
||||
Frequency: 250000,
|
||||
Mode: 0,
|
||||
SCK: SPI_SCK_PIN,
|
||||
SDO: SPI_TX_PIN,
|
||||
SDI: SPI_RX_PIN,
|
||||
}
|
||||
)
|
||||
|
||||
func main() {
|
||||
time.Sleep(time.Second)
|
||||
SPI_CS_PIN.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
err := spibus.Configure(spicfg)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
sdcard := sd.NewSPICard(spibus, SPI_CS_PIN.Set)
|
||||
println("start init")
|
||||
err = sdcard.Init()
|
||||
if err != nil {
|
||||
panic("sd card init:" + err.Error())
|
||||
}
|
||||
// After initialization it's safe to increase SPI clock speed.
|
||||
csd := sdcard.CSD()
|
||||
kbps := csd.TransferSpeed().RateKilobits()
|
||||
spicfg.Frequency = uint32(kbps * 1000)
|
||||
err = spibus.Configure(spicfg)
|
||||
|
||||
cid := sdcard.CID()
|
||||
fmt.Printf("name=%s\ncsd=\n%s\n", cid.ProductName(), csd.String())
|
||||
|
||||
bd, err := sd.NewBlockDevice(sdcard, csd.ReadBlockLen(), csd.NumberOfBlocks())
|
||||
if err != nil {
|
||||
panic("block device creation:" + err.Error())
|
||||
}
|
||||
var mc MemChecker
|
||||
|
||||
ok, badBlkIdx, err := mc.MemCheck(bd, 2, 100)
|
||||
if err != nil {
|
||||
panic("memcheck:" + err.Error())
|
||||
}
|
||||
if !ok {
|
||||
println("bad block", badBlkIdx)
|
||||
} else {
|
||||
println("memcheck ok")
|
||||
}
|
||||
}
|
||||
|
||||
type MemChecker struct {
|
||||
rdBuf []byte
|
||||
storeBuf []byte
|
||||
wrBuf []byte
|
||||
}
|
||||
|
||||
func (mc *MemChecker) MemCheck(bd *sd.BlockDevice, blockIdx, numBlocks int64) (memOK bool, badBlockIdx int64, err error) {
|
||||
size := bd.BlockSize() * numBlocks
|
||||
if len(mc.rdBuf) < int(size) {
|
||||
mc.rdBuf = make([]byte, size)
|
||||
mc.wrBuf = make([]byte, size)
|
||||
mc.storeBuf = make([]byte, size)
|
||||
for i := range mc.wrBuf {
|
||||
mc.wrBuf[i] = byte(i)
|
||||
}
|
||||
}
|
||||
// Start by storing the original block contents.
|
||||
_, err = bd.ReadAt(mc.storeBuf, blockIdx)
|
||||
if err != nil {
|
||||
return false, blockIdx, err
|
||||
}
|
||||
|
||||
// Write the test pattern.
|
||||
_, err = bd.WriteAt(mc.wrBuf, blockIdx)
|
||||
if err != nil {
|
||||
return false, blockIdx, err
|
||||
}
|
||||
// Read back the test pattern.
|
||||
_, err = bd.ReadAt(mc.rdBuf, blockIdx)
|
||||
if err != nil {
|
||||
return false, blockIdx, err
|
||||
}
|
||||
for j := 0; j < len(mc.rdBuf); j++ {
|
||||
// Compare the read back data with the test pattern.
|
||||
if mc.rdBuf[j] != mc.wrBuf[j] {
|
||||
badBlock := blockIdx + int64(j)/bd.BlockSize()
|
||||
return false, badBlock, nil
|
||||
}
|
||||
mc.rdBuf[j] = 0
|
||||
}
|
||||
// Leave the card in it's previous state.
|
||||
_, err = bd.WriteAt(mc.storeBuf, blockIdx)
|
||||
return true, -1, nil
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"machine"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/sx128x"
|
||||
)
|
||||
|
||||
var (
|
||||
// pin mapping specific to the lilygo t3s3, change as needed for your board
|
||||
sdoPin = machine.GPIO6
|
||||
sdiPin = machine.GPIO3
|
||||
sckPin = machine.GPIO5
|
||||
nssPin = machine.GPIO7
|
||||
busyPin = machine.GPIO36
|
||||
resetPin = machine.GPIO8
|
||||
dio1Pin = machine.GPIO9
|
||||
)
|
||||
|
||||
func setupPins() {
|
||||
nssPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
nssPin.Set(true)
|
||||
|
||||
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
resetPin.Set(true)
|
||||
|
||||
busyPin.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
dio1Pin.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
}
|
||||
|
||||
func main() {
|
||||
setupPins()
|
||||
|
||||
spi := machine.SPI0
|
||||
spi.Configure(machine.SPIConfig{
|
||||
Mode: 0,
|
||||
Frequency: 8 * 1e6,
|
||||
SDO: sdoPin,
|
||||
SDI: sdiPin,
|
||||
SCK: sckPin,
|
||||
})
|
||||
|
||||
radio := sx128x.New(
|
||||
spi,
|
||||
nssPin,
|
||||
resetPin,
|
||||
busyPin,
|
||||
)
|
||||
|
||||
radio.WaitWhileBusy(time.Second)
|
||||
SetupLora(radio)
|
||||
|
||||
for {
|
||||
data, err := Rx(radio)
|
||||
if err != nil {
|
||||
println("failed to receive:", err)
|
||||
} else {
|
||||
println("received:", string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SetupLora(radio *sx128x.Device) {
|
||||
radio.SetStandby(sx128x.STANDBY_RC)
|
||||
radio.SetPacketType(sx128x.PACKET_TYPE_LORA)
|
||||
radio.SetRegulatorMode(sx128x.REGULATOR_DC_DC)
|
||||
|
||||
radio.SetRfFrequency(2400000000) // 2.4Ghz
|
||||
radio.SetModulationParamsLoRa(sx128x.LORA_SF_9, sx128x.LORA_BW_1600, sx128x.LORA_CR_4_7)
|
||||
|
||||
// section 14.4.1 shows required register setting for setting up LoRa operations. These depend on the chosen spreading factor.
|
||||
radio.WriteRegister(0x925, []byte{0x32})
|
||||
radio.WriteRegister(0x93C, []byte{0x01})
|
||||
|
||||
radio.SetTxParams(13, sx128x.RADIO_RAMP_02_US)
|
||||
radio.SetPacketParamsLoRa(12, sx128x.LORA_HEADER_EXPLICIT, 0xFF, sx128x.LORA_CRC_DISABLE, sx128x.LORA_IQ_STD)
|
||||
radio.WriteRegister(sx128x.REG_LORA_SYNC_WORD_MSB, []byte{0x14, 0x24}) // full sync word is 0x1424
|
||||
|
||||
}
|
||||
|
||||
func Rx(radio *sx128x.Device) ([]byte, error) {
|
||||
radio.SetStandby(sx128x.STANDBY_RC)
|
||||
radio.SetDioIrqParams(sx128x.IRQ_RX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, sx128x.IRQ_RX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, 0x00, 0x00)
|
||||
radio.SetBufferBaseAddress(0, 0)
|
||||
radio.ClearIrqStatus(sx128x.IRQ_ALL_MASK)
|
||||
radio.SetRx(sx128x.PERIOD_BASE_4_MS, 250) // 4ms * 250 = 1s
|
||||
// busy wait for IRQ indication
|
||||
for dio1Pin.Get() == false {
|
||||
runtime.Gosched()
|
||||
}
|
||||
irqStatus, _ := radio.GetIrqStatus()
|
||||
if irqStatus&sx128x.IRQ_RX_DONE_MASK != 0 {
|
||||
payloadLength, bufferOffset, err := radio.GetRxBufferStatus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := radio.ReadBuffer(bufferOffset, payloadLength)
|
||||
return data, nil
|
||||
} else if irqStatus&sx128x.IRQ_RX_TX_TIMEOUT_MASK != 0 {
|
||||
return nil, errors.New("rx timeout")
|
||||
}
|
||||
return nil, errors.New("unexpected IRQ status")
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"machine"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers/sx128x"
|
||||
)
|
||||
|
||||
var (
|
||||
// pin mapping specific to the lilygo t3s3, change as needed for your board
|
||||
sdoPin = machine.GPIO6
|
||||
sdiPin = machine.GPIO3
|
||||
sckPin = machine.GPIO5
|
||||
nssPin = machine.GPIO7
|
||||
busyPin = machine.GPIO36
|
||||
resetPin = machine.GPIO8
|
||||
dio1Pin = machine.GPIO9
|
||||
)
|
||||
|
||||
func setupPins() {
|
||||
nssPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
nssPin.Set(true)
|
||||
|
||||
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
|
||||
resetPin.Set(true)
|
||||
|
||||
busyPin.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
dio1Pin.Configure(machine.PinConfig{Mode: machine.PinInput})
|
||||
}
|
||||
|
||||
func main() {
|
||||
setupPins()
|
||||
|
||||
spi := machine.SPI0
|
||||
spi.Configure(machine.SPIConfig{
|
||||
Mode: 0,
|
||||
Frequency: 8 * 1e6,
|
||||
SDO: sdoPin,
|
||||
SDI: sdiPin,
|
||||
SCK: sckPin,
|
||||
})
|
||||
|
||||
radio := sx128x.New(
|
||||
spi,
|
||||
nssPin,
|
||||
resetPin,
|
||||
busyPin,
|
||||
)
|
||||
|
||||
radio.WaitWhileBusy(time.Second)
|
||||
SetupLora(radio)
|
||||
|
||||
for {
|
||||
Tx(radio, []byte("Hello, world!"))
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func SetupLora(radio *sx128x.Device) {
|
||||
radio.SetStandby(sx128x.STANDBY_RC)
|
||||
radio.SetPacketType(sx128x.PACKET_TYPE_LORA)
|
||||
radio.SetRegulatorMode(sx128x.REGULATOR_DC_DC)
|
||||
|
||||
radio.SetRfFrequency(2400000000) // 2.4Ghz
|
||||
radio.SetModulationParamsLoRa(sx128x.LORA_SF_9, sx128x.LORA_BW_1600, sx128x.LORA_CR_4_7)
|
||||
|
||||
// section 14.4.1 shows required register setting for setting up LoRa operations. These depend on the chosen spreading factor.
|
||||
radio.WriteRegister(0x925, []byte{0x32})
|
||||
radio.WriteRegister(0x93C, []byte{0x01})
|
||||
|
||||
radio.SetTxParams(13, sx128x.RADIO_RAMP_02_US)
|
||||
radio.SetPacketParamsLoRa(12, sx128x.LORA_HEADER_EXPLICIT, 0xFF, sx128x.LORA_CRC_DISABLE, sx128x.LORA_IQ_STD)
|
||||
radio.WriteRegister(sx128x.REG_LORA_SYNC_WORD_MSB, []byte{0x14, 0x24}) // full sync word is 0x1424
|
||||
|
||||
}
|
||||
|
||||
func Tx(radio *sx128x.Device, data []byte) error {
|
||||
if len(data) > 255 {
|
||||
return errors.New("data length exceeds maximum of 255 bytes")
|
||||
}
|
||||
radio.SetStandby(sx128x.STANDBY_RC)
|
||||
radio.SetPacketParamsLoRa(12, sx128x.LORA_HEADER_EXPLICIT, uint8(len(data)&0xFF), sx128x.LORA_CRC_DISABLE, sx128x.LORA_IQ_STD)
|
||||
radio.SetBufferBaseAddress(0, 0)
|
||||
radio.WriteBuffer(0, data)
|
||||
radio.SetDioIrqParams(sx128x.IRQ_TX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, sx128x.IRQ_TX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, 0x00, 0x00)
|
||||
radio.ClearIrqStatus(sx128x.IRQ_ALL_MASK)
|
||||
radio.SetTx(sx128x.PERIOD_BASE_4_MS, 250) // 4ms * 250 = 1s
|
||||
// busy wait for IRQ indication
|
||||
for dio1Pin.Get() == false {
|
||||
runtime.Gosched()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !digispark && !arduino && !arduino_uno
|
||||
//go:build !digispark && !arduino && !arduino_uno && !xiao_esp32c3
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build xiao_esp32c3
|
||||
|
||||
package main
|
||||
|
||||
import "machine"
|
||||
|
||||
func init() {
|
||||
// Replace neo in the code below to match the pin
|
||||
// that you are using if different.
|
||||
neo = machine.D6
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
// Package gdew0154m09 provides a driver for the Good Display GDEW0154M09
|
||||
// 1.54-inch 200x200 monochrome e-paper panel used by the M5Stack CoreInk.
|
||||
//
|
||||
// The initialization sequence is based on the MIT-licensed M5Stack
|
||||
// M5Core-Ink library:
|
||||
// https://github.com/m5stack/M5Core-Ink
|
||||
package gdew0154m09 // import "tinygo.org/x/drivers/gdew0154m09"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
const (
|
||||
// Width and Height are the physical panel dimensions in pixels.
|
||||
Width = 200
|
||||
Height = 200
|
||||
|
||||
// Baudrate and SPIMode are the recommended SPI bus configuration.
|
||||
Baudrate = 10_000_000
|
||||
SPIMode = 3
|
||||
|
||||
frameBufferSize = Width * Height / 8
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBusyTimeout = errors.New("gdew0154m09: busy timeout")
|
||||
ErrInvalidBusyTimeout = errors.New("gdew0154m09: busy timeout must not be negative")
|
||||
)
|
||||
|
||||
// OutputPin is the interface required from an output pin. Pins must be
|
||||
// configured by the caller before they are passed to New.
|
||||
type OutputPin interface {
|
||||
Set(level bool)
|
||||
}
|
||||
|
||||
// InputPin is the interface required from an input pin. The pin must be
|
||||
// configured by the caller before it is passed to New.
|
||||
type InputPin interface {
|
||||
Get() bool
|
||||
}
|
||||
|
||||
// Config contains the display configuration.
|
||||
type Config struct {
|
||||
// BusyTimeout is the maximum time to wait for a panel operation. A zero
|
||||
// duration selects the default timeout.
|
||||
BusyTimeout time.Duration
|
||||
}
|
||||
|
||||
// DefaultConfig contains the recommended display configuration.
|
||||
var DefaultConfig = Config{
|
||||
BusyTimeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
// Device is a GDEW0154M09 display connected over SPI.
|
||||
type Device struct {
|
||||
bus drivers.SPI
|
||||
cs OutputPin
|
||||
dc OutputPin
|
||||
rst OutputPin
|
||||
busy InputPin
|
||||
|
||||
buffer [frameBufferSize]byte
|
||||
previous [frameBufferSize]byte
|
||||
tx [3]byte
|
||||
busyTimeout time.Duration
|
||||
}
|
||||
|
||||
var _ drivers.Displayer = (*Device)(nil)
|
||||
|
||||
// New returns a new GDEW0154M09 driver. The SPI bus and pins must already be
|
||||
// configured. New performs no I/O.
|
||||
func New(bus drivers.SPI, cs, dc, rst OutputPin, busy InputPin) *Device {
|
||||
d := &Device{
|
||||
bus: bus,
|
||||
cs: cs,
|
||||
dc: dc,
|
||||
rst: rst,
|
||||
busy: busy,
|
||||
busyTimeout: DefaultConfig.BusyTimeout,
|
||||
}
|
||||
d.ClearBuffer()
|
||||
fill(d.previous[:], 0xff)
|
||||
return d
|
||||
}
|
||||
|
||||
// Configure resets and initializes the display controller. It also clears the
|
||||
// current and previous framebuffers to white without refreshing the panel.
|
||||
func (d *Device) Configure(config Config) error {
|
||||
if config.BusyTimeout < 0 {
|
||||
return ErrInvalidBusyTimeout
|
||||
}
|
||||
if config.BusyTimeout == 0 {
|
||||
config.BusyTimeout = DefaultConfig.BusyTimeout
|
||||
}
|
||||
d.busyTimeout = config.BusyTimeout
|
||||
|
||||
d.hardwareReset()
|
||||
if err := d.waitUntilIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.sendCommand2(commandPanelSetting, 0xdf, 0x0e); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.sendCommand1(commandFITIInternalCode, 0x55); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.sendCommand1(commandUnknownAA, 0x0f); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.sendCommand1(commandUnknownE9, 0x02); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.sendCommand1(commandBoosterSoftStart, 0x11); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.sendCommand1(commandPowerSequence, 0x0a); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.sendCommand3(commandResolutionSetting, 0xc8, 0x00, 0xc8); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.sendCommand1(commandTCONSetting, 0x00); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.sendCommand1(commandVCOMDataInterval, defaultVCOMDataInterval); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.sendCommand1(commandPowerSaving, 0x00); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.sendCommand(commandPowerOn); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if err := d.waitUntilIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
d.ClearBuffer()
|
||||
fill(d.previous[:], 0xff)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Size returns the dimensions of the display in pixels.
|
||||
func (d *Device) Size() (width, height int16) {
|
||||
return Width, Height
|
||||
}
|
||||
|
||||
// SetPixel updates one pixel in the framebuffer. Transparent and light colors
|
||||
// are rendered white; opaque dark colors are rendered black.
|
||||
func (d *Device) SetPixel(x, y int16, c color.RGBA) {
|
||||
if x < 0 || x >= Width || y < 0 || y >= Height {
|
||||
return
|
||||
}
|
||||
index := int(y)*(Width/8) + int(x)/8
|
||||
mask := byte(0x80 >> uint(x%8))
|
||||
brightness := uint16(c.R) + uint16(c.G) + uint16(c.B)
|
||||
if c.A == 0 || brightness >= 3*128 {
|
||||
d.buffer[index] |= mask
|
||||
} else {
|
||||
d.buffer[index] &^= mask
|
||||
}
|
||||
}
|
||||
|
||||
// ClearBuffer sets every pixel in the framebuffer to white without refreshing
|
||||
// the panel.
|
||||
func (d *Device) ClearBuffer() {
|
||||
fill(d.buffer[:], 0xff)
|
||||
}
|
||||
|
||||
// Display sends the previous and current framebuffers to the panel and starts
|
||||
// a full refresh. It blocks until the refresh completes or the busy timeout is
|
||||
// reached.
|
||||
func (d *Device) Display() error {
|
||||
if err := d.sendCommand(commandDataStartOld); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.sendData(d.previous[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
if err := d.sendCommand(commandDataStartNew); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.sendData(d.buffer[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
|
||||
if err := d.sendCommand(commandDisplayRefresh); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.waitUntilIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
copy(d.previous[:], d.buffer[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearDisplay clears the framebuffer and refreshes the panel.
|
||||
func (d *Device) ClearDisplay() error {
|
||||
d.ClearBuffer()
|
||||
return d.Display()
|
||||
}
|
||||
|
||||
// DeepSleep powers off the panel and enters deep sleep. Configure must be
|
||||
// called to wake and reinitialize the controller.
|
||||
func (d *Device) DeepSleep() error {
|
||||
if err := d.sendCommand1(commandVCOMDataInterval, deepSleepVCOMDataInterval); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.waitUntilIdle(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := d.sendCommand(commandPowerOff); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.sendCommand1(commandDeepSleep, deepSleepCheckCode)
|
||||
}
|
||||
|
||||
// IsBusy reports whether the active-low busy signal is asserted.
|
||||
func (d *Device) IsBusy() bool {
|
||||
return !d.busy.Get()
|
||||
}
|
||||
|
||||
func (d *Device) hardwareReset() {
|
||||
d.rst.Set(true)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
d.rst.Set(false)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
d.rst.Set(true)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (d *Device) waitUntilIdle() error {
|
||||
deadline := time.Now().Add(d.busyTimeout)
|
||||
for d.IsBusy() {
|
||||
if time.Now().After(deadline) {
|
||||
return ErrBusyTimeout
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Device) sendCommandWithData(command byte, data []byte) error {
|
||||
if err := d.sendCommand(command); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.sendData(data)
|
||||
}
|
||||
|
||||
func (d *Device) sendCommand1(command, data byte) error {
|
||||
d.tx[0] = data
|
||||
return d.sendCommandWithData(command, d.tx[:1])
|
||||
}
|
||||
|
||||
func (d *Device) sendCommand2(command, data0, data1 byte) error {
|
||||
d.tx[0] = data0
|
||||
d.tx[1] = data1
|
||||
return d.sendCommandWithData(command, d.tx[:2])
|
||||
}
|
||||
|
||||
func (d *Device) sendCommand3(command, data0, data1, data2 byte) error {
|
||||
d.tx[0] = data0
|
||||
d.tx[1] = data1
|
||||
d.tx[2] = data2
|
||||
return d.sendCommandWithData(command, d.tx[:3])
|
||||
}
|
||||
|
||||
func (d *Device) sendCommand(command byte) error {
|
||||
d.dc.Set(false)
|
||||
d.cs.Set(false)
|
||||
_, err := d.bus.Transfer(command)
|
||||
d.cs.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Device) sendData(data []byte) error {
|
||||
d.dc.Set(true)
|
||||
d.cs.Set(false)
|
||||
err := d.bus.Tx(data, nil)
|
||||
d.cs.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
func fill(buffer []byte, value byte) {
|
||||
for index := range buffer {
|
||||
buffer[index] = value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
package gdew0154m09
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"image/color"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type fakeOutputPin struct {
|
||||
high bool
|
||||
changes []bool
|
||||
}
|
||||
|
||||
func (p *fakeOutputPin) Set(level bool) {
|
||||
p.high = level
|
||||
p.changes = append(p.changes, level)
|
||||
}
|
||||
|
||||
type fakeInputPin struct {
|
||||
high bool
|
||||
}
|
||||
|
||||
func (p *fakeInputPin) Get() bool {
|
||||
return p.high
|
||||
}
|
||||
|
||||
type spiOperation struct {
|
||||
command bool
|
||||
data []byte
|
||||
}
|
||||
|
||||
type fakeSPI struct {
|
||||
t *testing.T
|
||||
cs *fakeOutputPin
|
||||
dc *fakeOutputPin
|
||||
operations []spiOperation
|
||||
failAt int
|
||||
failErr error
|
||||
}
|
||||
|
||||
func (s *fakeSPI) Transfer(value byte) (byte, error) {
|
||||
s.t.Helper()
|
||||
if s.cs.high {
|
||||
s.t.Error("chip select was high during command transfer")
|
||||
}
|
||||
if s.dc.high {
|
||||
s.t.Error("data/command pin was high during command transfer")
|
||||
}
|
||||
s.operations = append(s.operations, spiOperation{command: true, data: []byte{value}})
|
||||
if len(s.operations) == s.failAt {
|
||||
return 0, s.failErr
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (s *fakeSPI) Tx(write, read []byte) error {
|
||||
s.t.Helper()
|
||||
if s.cs.high {
|
||||
s.t.Error("chip select was high during data transfer")
|
||||
}
|
||||
if !s.dc.high {
|
||||
s.t.Error("data/command pin was low during data transfer")
|
||||
}
|
||||
data := append([]byte(nil), write...)
|
||||
s.operations = append(s.operations, spiOperation{data: data})
|
||||
if len(s.operations) == s.failAt {
|
||||
return s.failErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newTestDevice(t *testing.T) (*Device, *fakeSPI, *fakeOutputPin, *fakeInputPin) {
|
||||
t.Helper()
|
||||
cs := &fakeOutputPin{high: true}
|
||||
dc := &fakeOutputPin{high: true}
|
||||
rst := &fakeOutputPin{high: true}
|
||||
busy := &fakeInputPin{high: true}
|
||||
bus := &fakeSPI{t: t, cs: cs, dc: dc}
|
||||
return New(bus, cs, dc, rst, busy), bus, rst, busy
|
||||
}
|
||||
|
||||
func TestNewStartsWithWhiteBuffers(t *testing.T) {
|
||||
device, _, _, _ := newTestDevice(t)
|
||||
for index := range device.buffer {
|
||||
if device.buffer[index] != 0xff || device.previous[index] != 0xff {
|
||||
t.Fatalf("pixel byte %d was not initialized white", index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSize(t *testing.T) {
|
||||
device, _, _, _ := newTestDevice(t)
|
||||
width, height := device.Size()
|
||||
if width != Width || height != Height {
|
||||
t.Fatalf("size = %dx%d, want %dx%d", width, height, Width, Height)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetPixel(t *testing.T) {
|
||||
device, _, _, _ := newTestDevice(t)
|
||||
black := color.RGBA{A: 0xff}
|
||||
white := color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
|
||||
|
||||
device.SetPixel(0, 0, black)
|
||||
device.SetPixel(199, 199, black)
|
||||
if device.buffer[0] != 0x7f {
|
||||
t.Fatalf("first byte = %#x, want 0x7f", device.buffer[0])
|
||||
}
|
||||
if device.buffer[len(device.buffer)-1] != 0xfe {
|
||||
t.Fatalf("last byte = %#x, want 0xfe", device.buffer[len(device.buffer)-1])
|
||||
}
|
||||
|
||||
device.SetPixel(0, 0, white)
|
||||
if device.buffer[0] != 0xff {
|
||||
t.Fatalf("first byte after white pixel = %#x, want 0xff", device.buffer[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetPixelIgnoresOutOfBoundsCoordinates(t *testing.T) {
|
||||
device, _, _, _ := newTestDevice(t)
|
||||
device.SetPixel(-1, 0, color.RGBA{A: 0xff})
|
||||
device.SetPixel(0, -1, color.RGBA{A: 0xff})
|
||||
device.SetPixel(Width, 0, color.RGBA{A: 0xff})
|
||||
device.SetPixel(0, Height, color.RGBA{A: 0xff})
|
||||
for index, value := range device.buffer {
|
||||
if value != 0xff {
|
||||
t.Fatalf("byte %d changed to %#x", index, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigure(t *testing.T) {
|
||||
device, bus, rst, _ := newTestDevice(t)
|
||||
if err := device.Configure(Config{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if device.busyTimeout != DefaultConfig.BusyTimeout {
|
||||
t.Fatalf("busy timeout = %v, want %v", device.busyTimeout, DefaultConfig.BusyTimeout)
|
||||
}
|
||||
|
||||
wantReset := []bool{true, false, true}
|
||||
if !equalBools(rst.changes, wantReset) {
|
||||
t.Fatalf("reset changes = %v, want %v", rst.changes, wantReset)
|
||||
}
|
||||
wantCommands := []byte{0x00, 0x4d, 0xaa, 0xe9, 0xb6, 0xf3, 0x61, 0x60, 0x50, 0xe3, 0x04}
|
||||
if got := commandBytes(bus.operations); !bytes.Equal(got, wantCommands) {
|
||||
t.Fatalf("commands = %#v, want %#v", got, wantCommands)
|
||||
}
|
||||
|
||||
wantData := [][]byte{
|
||||
{0xdf, 0x0e}, {0x55}, {0x0f}, {0x02}, {0x11}, {0x0a},
|
||||
{0xc8, 0x00, 0xc8}, {0x00}, {0xd7}, {0x00},
|
||||
}
|
||||
if got := dataTransfers(bus.operations); !equalByteSlices(got, wantData) {
|
||||
t.Fatalf("data transfers = %#v, want %#v", got, wantData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigurePropagatesSPIErrors(t *testing.T) {
|
||||
wantErr := errors.New("SPI failure")
|
||||
for _, operation := range []int{1, 2} {
|
||||
t.Run(operationName(operation), func(t *testing.T) {
|
||||
device, bus, _, _ := newTestDevice(t)
|
||||
bus.failAt = operation
|
||||
bus.failErr = wantErr
|
||||
if err := device.Configure(DefaultConfig); !errors.Is(err, wantErr) {
|
||||
t.Fatalf("error = %v, want %v", err, wantErr)
|
||||
}
|
||||
if !bus.cs.high {
|
||||
t.Error("chip select was not released after SPI error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureRejectsNegativeTimeout(t *testing.T) {
|
||||
device, bus, rst, _ := newTestDevice(t)
|
||||
err := device.Configure(Config{BusyTimeout: -time.Millisecond})
|
||||
if !errors.Is(err, ErrInvalidBusyTimeout) {
|
||||
t.Fatalf("error = %v, want %v", err, ErrInvalidBusyTimeout)
|
||||
}
|
||||
if len(bus.operations) != 0 || len(rst.changes) != 0 {
|
||||
t.Fatal("invalid configuration performed hardware I/O")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplaySendsOldAndNewFrames(t *testing.T) {
|
||||
device, bus, _, _ := newTestDevice(t)
|
||||
device.SetPixel(0, 0, color.RGBA{A: 0xff})
|
||||
if err := device.Display(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(bus.operations) != 5 {
|
||||
t.Fatalf("operation count = %d, want 5", len(bus.operations))
|
||||
}
|
||||
assertCommand(t, bus.operations[0], commandDataStartOld)
|
||||
if len(bus.operations[1].data) != frameBufferSize || bus.operations[1].data[0] != 0xff {
|
||||
t.Fatal("old frame was not initially white")
|
||||
}
|
||||
assertCommand(t, bus.operations[2], commandDataStartNew)
|
||||
if len(bus.operations[3].data) != frameBufferSize || bus.operations[3].data[0] != 0x7f {
|
||||
t.Fatal("new frame did not contain the black pixel")
|
||||
}
|
||||
assertCommand(t, bus.operations[4], commandDisplayRefresh)
|
||||
|
||||
bus.operations = nil
|
||||
device.SetPixel(0, 0, color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff})
|
||||
if err := device.Display(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bus.operations[1].data[0] != 0x7f {
|
||||
t.Fatal("second refresh did not send the first frame as old data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisplayPropagatesSPIErrors(t *testing.T) {
|
||||
wantErr := errors.New("SPI failure")
|
||||
for operation := 1; operation <= 5; operation++ {
|
||||
t.Run(operationName(operation), func(t *testing.T) {
|
||||
device, bus, _, _ := newTestDevice(t)
|
||||
bus.failAt = operation
|
||||
bus.failErr = wantErr
|
||||
if err := device.Display(); !errors.Is(err, wantErr) {
|
||||
t.Fatalf("error = %v, want %v", err, wantErr)
|
||||
}
|
||||
if !bus.cs.high {
|
||||
t.Error("chip select was not released after SPI error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBusyTimeout(t *testing.T) {
|
||||
device, _, _, busy := newTestDevice(t)
|
||||
busy.high = false
|
||||
device.busyTimeout = time.Millisecond
|
||||
if err := device.waitUntilIdle(); !errors.Is(err, ErrBusyTimeout) {
|
||||
t.Fatalf("error = %v, want %v", err, ErrBusyTimeout)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeepSleep(t *testing.T) {
|
||||
device, bus, _, _ := newTestDevice(t)
|
||||
if err := device.DeepSleep(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantCommands := []byte{commandVCOMDataInterval, commandPowerOff, commandDeepSleep}
|
||||
if got := commandBytes(bus.operations); !bytes.Equal(got, wantCommands) {
|
||||
t.Fatalf("commands = %#v, want %#v", got, wantCommands)
|
||||
}
|
||||
wantData := [][]byte{{deepSleepVCOMDataInterval}, {deepSleepCheckCode}}
|
||||
if got := dataTransfers(bus.operations); !equalByteSlices(got, wantData) {
|
||||
t.Fatalf("data transfers = %#v, want %#v", got, wantData)
|
||||
}
|
||||
}
|
||||
|
||||
func assertCommand(t *testing.T, operation spiOperation, command byte) {
|
||||
t.Helper()
|
||||
if !operation.command || len(operation.data) != 1 || operation.data[0] != command {
|
||||
t.Fatalf("operation = %#v, want command %#x", operation, command)
|
||||
}
|
||||
}
|
||||
|
||||
func commandBytes(operations []spiOperation) []byte {
|
||||
var commands []byte
|
||||
for _, operation := range operations {
|
||||
if operation.command {
|
||||
commands = append(commands, operation.data[0])
|
||||
}
|
||||
}
|
||||
return commands
|
||||
}
|
||||
|
||||
func dataTransfers(operations []spiOperation) [][]byte {
|
||||
var transfers [][]byte
|
||||
for _, operation := range operations {
|
||||
if !operation.command {
|
||||
transfers = append(transfers, operation.data)
|
||||
}
|
||||
}
|
||||
return transfers
|
||||
}
|
||||
|
||||
func equalBools(left, right []bool) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for index := range left {
|
||||
if left[index] != right[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func equalByteSlices(left, right [][]byte) bool {
|
||||
if len(left) != len(right) {
|
||||
return false
|
||||
}
|
||||
for index := range left {
|
||||
if !bytes.Equal(left[index], right[index]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func operationName(operation int) string {
|
||||
return "operation-" + string(rune('0'+operation))
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package gdew0154m09
|
||||
|
||||
const (
|
||||
commandPanelSetting = 0x00
|
||||
commandPowerOff = 0x02
|
||||
commandPowerOn = 0x04
|
||||
commandDeepSleep = 0x07
|
||||
commandDisplayRefresh = 0x12
|
||||
commandDataStartOld = 0x10
|
||||
commandDataStartNew = 0x13
|
||||
commandVCOMDataInterval = 0x50
|
||||
commandResolutionSetting = 0x61
|
||||
commandTCONSetting = 0x60
|
||||
commandPowerSaving = 0xe3
|
||||
commandPowerSequence = 0xf3
|
||||
commandBoosterSoftStart = 0xb6
|
||||
commandUnknownAA = 0xaa
|
||||
commandUnknownE9 = 0xe9
|
||||
commandFITIInternalCode = 0x4d
|
||||
deepSleepCheckCode = 0xa5
|
||||
defaultVCOMDataInterval = 0xd7
|
||||
deepSleepVCOMDataInterval = 0xf7
|
||||
)
|
||||
@@ -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 {
|
||||
|
||||
+12
-9
@@ -1,5 +1,4 @@
|
||||
// L2 data link layer
|
||||
|
||||
// package netlink provides an interface for L2 data link layer operations.
|
||||
package netlink
|
||||
|
||||
import (
|
||||
@@ -20,6 +19,7 @@ var (
|
||||
ErrNotSupported = errors.New("Not supported")
|
||||
)
|
||||
|
||||
// Event is a network event type passed to the callback registered with NetNotify.
|
||||
type Event int
|
||||
|
||||
// Network events
|
||||
@@ -38,6 +38,7 @@ const (
|
||||
ConnectModeAP // Connect as Wifi Access Point
|
||||
)
|
||||
|
||||
// AuthType is the type of WiFi authorization to use when connecting to an access point.
|
||||
type AuthType int
|
||||
|
||||
// Wifi authorization types. Used when setting up an access point, or
|
||||
@@ -49,10 +50,11 @@ const (
|
||||
AuthTypeWPA2Mixed // WPA2/WPA mixed authorization
|
||||
)
|
||||
|
||||
// DefaultConnectTimeout is the default timeout for connection attempts. This is used when ConnectParams.ConnectTimeout is zero.
|
||||
const DefaultConnectTimeout = 10 * time.Second
|
||||
|
||||
// ConnectParams is the set of parameters used to connect a Netlinker device to a network.
|
||||
type ConnectParams struct {
|
||||
|
||||
// Connect mode
|
||||
ConnectMode
|
||||
|
||||
@@ -81,22 +83,23 @@ type ConnectParams struct {
|
||||
// downed connection or hardware fault and try to recover the
|
||||
// connection. Set to zero to disable watchodog.
|
||||
WatchdogTimeout time.Duration
|
||||
|
||||
// Hostname to use for this device.
|
||||
Hostname string
|
||||
}
|
||||
|
||||
// Netlinker is TinyGo's OSI L2 data link layer interface. Network device
|
||||
// drivers implement Netlinker to expose the device's L2 functionality.
|
||||
|
||||
type Netlinker interface {
|
||||
|
||||
// Connect device to network
|
||||
// NetConnect connects the device to a network
|
||||
NetConnect(params *ConnectParams) error
|
||||
|
||||
// Disconnect device from network
|
||||
// NetDisconnect disconnects the device from the network
|
||||
NetDisconnect()
|
||||
|
||||
// Notify to register callback for network events
|
||||
// NetNotify registers a callback for network events
|
||||
NetNotify(cb func(Event))
|
||||
|
||||
// GetHardwareAddr returns device MAC address
|
||||
// GetHardwareAddr returns the device's MAC address
|
||||
GetHardwareAddr() (net.HardwareAddr, error)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package scd30
|
||||
|
||||
const (
|
||||
// Address is the default and only I2C address of the SCD30.
|
||||
Address uint16 = 0x61
|
||||
|
||||
commandStartContinuousMeasurement = 0x0010
|
||||
commandStopContinuousMeasurement = 0x0104
|
||||
commandDataReady = 0x0202
|
||||
commandReadMeasurement = 0x0300
|
||||
commandSetMeasurementInterval = 0x4600
|
||||
commandSetAutoCalibration = 0x5306
|
||||
)
|
||||
|
||||
const (
|
||||
minimumMeasurementInterval = 2
|
||||
maximumMeasurementInterval = 1800
|
||||
minimumAmbientPressure = 700
|
||||
maximumAmbientPressure = 1400
|
||||
)
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
// Package scd30 provides a driver for the Sensirion SCD30 CO2, temperature,
|
||||
// and humidity sensor.
|
||||
//
|
||||
// Datasheet: https://sensirion.com/media/documents/D7CEEF4A/6165372F/Sensirion_CO2_Sensors_SCD30_Interface_Description.pdf
|
||||
package scd30 // import "tinygo.org/x/drivers/scd30"
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
const readDelay = 4 * time.Millisecond
|
||||
|
||||
var (
|
||||
ErrCRC = errors.New("scd30: invalid CRC")
|
||||
|
||||
ErrInvalidInterval = errors.New("scd30: measurement interval must be between 2 and 1800 seconds")
|
||||
|
||||
ErrInvalidAmbientPressure = errors.New("scd30: ambient pressure must be zero or between 700 and 1400 mbar")
|
||||
)
|
||||
|
||||
// Config contains the SCD30 continuous measurement configuration.
|
||||
type Config struct {
|
||||
// MeasurementInterval is the interval between measurements in seconds and
|
||||
// must be between 2 and 1800.
|
||||
MeasurementInterval uint16
|
||||
|
||||
// AutomaticSelfCalibration enables or disables automatic self-calibration.
|
||||
AutomaticSelfCalibration bool
|
||||
}
|
||||
|
||||
// DefaultConfig contains the power-on defaults documented for the SCD30.
|
||||
var DefaultConfig = Config{
|
||||
MeasurementInterval: 2,
|
||||
AutomaticSelfCalibration: false,
|
||||
}
|
||||
|
||||
// Device is a Sensirion SCD30 sensor connected over I2C.
|
||||
type Device struct {
|
||||
bus drivers.I2C
|
||||
tx [5]byte
|
||||
rx [18]byte
|
||||
|
||||
co2 int32
|
||||
temperature int32
|
||||
humidity int32
|
||||
}
|
||||
|
||||
var _ drivers.Sensor = (*Device)(nil)
|
||||
|
||||
// New returns a new SCD30 driver. It performs no I/O.
|
||||
func New(bus drivers.I2C) *Device {
|
||||
return &Device{bus: bus}
|
||||
}
|
||||
|
||||
// Configure applies the continuous measurement interval and automatic
|
||||
// self-calibration settings. It does not start continuous measurement.
|
||||
func (d *Device) Configure(config Config) error {
|
||||
if err := d.SetMeasurementInterval(config.MeasurementInterval); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.SetAutomaticSelfCalibration(config.AutomaticSelfCalibration)
|
||||
}
|
||||
|
||||
// Connected reports whether an SCD30 responds with a valid data-ready status.
|
||||
func (d *Device) Connected() bool {
|
||||
_, err := d.DataReady()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// SetMeasurementInterval sets the continuous measurement interval in seconds.
|
||||
func (d *Device) SetMeasurementInterval(seconds uint16) error {
|
||||
if seconds < minimumMeasurementInterval || seconds > maximumMeasurementInterval {
|
||||
return ErrInvalidInterval
|
||||
}
|
||||
return d.writeCommandWithArgument(commandSetMeasurementInterval, seconds)
|
||||
}
|
||||
|
||||
// SetAutomaticSelfCalibration enables or disables automatic self-calibration.
|
||||
func (d *Device) SetAutomaticSelfCalibration(enabled bool) error {
|
||||
var value uint16
|
||||
if enabled {
|
||||
value = 1
|
||||
}
|
||||
return d.writeCommandWithArgument(commandSetAutoCalibration, value)
|
||||
}
|
||||
|
||||
// StartContinuousMeasurement begins periodic measurements. Ambient pressure
|
||||
// must be zero to disable pressure compensation, or between 700 and 1400 mbar.
|
||||
func (d *Device) StartContinuousMeasurement(ambientPressure uint16) error {
|
||||
if ambientPressure != 0 && (ambientPressure < minimumAmbientPressure || ambientPressure > maximumAmbientPressure) {
|
||||
return ErrInvalidAmbientPressure
|
||||
}
|
||||
return d.writeCommandWithArgument(commandStartContinuousMeasurement, ambientPressure)
|
||||
}
|
||||
|
||||
// StopContinuousMeasurement stops periodic measurements.
|
||||
func (d *Device) StopContinuousMeasurement() error {
|
||||
return d.writeCommand(commandStopContinuousMeasurement)
|
||||
}
|
||||
|
||||
// DataReady reports whether a new measurement can be read.
|
||||
func (d *Device) DataReady() (bool, error) {
|
||||
if err := d.readCommand(commandDataReady, d.rx[:3]); err != nil {
|
||||
return false, err
|
||||
}
|
||||
value, err := decodeWord(d.rx[:3])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return value != 0, nil
|
||||
}
|
||||
|
||||
// ReadMeasurement reads and caches the latest CO2, temperature, and humidity
|
||||
// measurement. Use DataReady before calling ReadMeasurement.
|
||||
func (d *Device) ReadMeasurement() error {
|
||||
if err := d.readCommand(commandReadMeasurement, d.rx[:18]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var data [12]byte
|
||||
for source, destination := 0, 0; source < 18; source, destination = source+3, destination+2 {
|
||||
value, err := decodeWord(d.rx[source : source+3])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
binary.BigEndian.PutUint16(data[destination:destination+2], value)
|
||||
}
|
||||
|
||||
co2 := decodeFloat32(data[0:4])
|
||||
temperature := decodeFloat32(data[4:8])
|
||||
humidity := decodeFloat32(data[8:12])
|
||||
|
||||
d.co2 = roundFixed(co2, 1)
|
||||
d.temperature = roundFixed(temperature, 1000)
|
||||
d.humidity = roundFixed(humidity, 100)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update reads and caches all measurements if any supported measurement was
|
||||
// requested. The SCD30 provides all three values in a single transaction.
|
||||
func (d *Device) Update(which drivers.Measurement) error {
|
||||
if which&(drivers.Concentration|drivers.Temperature|drivers.Humidity) == 0 {
|
||||
return nil
|
||||
}
|
||||
return d.ReadMeasurement()
|
||||
}
|
||||
|
||||
// CO2 returns the last read CO2 concentration in parts per million.
|
||||
func (d *Device) CO2() int32 {
|
||||
return d.co2
|
||||
}
|
||||
|
||||
// Temperature returns the last read temperature in millidegrees Celsius.
|
||||
func (d *Device) Temperature() int32 {
|
||||
return d.temperature
|
||||
}
|
||||
|
||||
// Humidity returns the last read relative humidity in hundredths of a percent.
|
||||
func (d *Device) Humidity() int32 {
|
||||
return d.humidity
|
||||
}
|
||||
|
||||
func (d *Device) readCommand(command uint16, response []byte) error {
|
||||
if err := d.writeCommand(command); err != nil {
|
||||
return err
|
||||
}
|
||||
// The datasheet requires a delay greater than 3ms before reading.
|
||||
time.Sleep(readDelay)
|
||||
return d.bus.Tx(Address, nil, response)
|
||||
}
|
||||
|
||||
func (d *Device) writeCommand(command uint16) error {
|
||||
binary.BigEndian.PutUint16(d.tx[:2], command)
|
||||
return d.bus.Tx(Address, d.tx[:2], nil)
|
||||
}
|
||||
|
||||
func (d *Device) writeCommandWithArgument(command, argument uint16) error {
|
||||
binary.BigEndian.PutUint16(d.tx[:2], command)
|
||||
binary.BigEndian.PutUint16(d.tx[2:4], argument)
|
||||
d.tx[4] = crc8(d.tx[2:4])
|
||||
return d.bus.Tx(Address, d.tx[:5], nil)
|
||||
}
|
||||
|
||||
func decodeWord(data []byte) (uint16, error) {
|
||||
if len(data) != 3 || crc8(data[:2]) != data[2] {
|
||||
return 0, ErrCRC
|
||||
}
|
||||
return binary.BigEndian.Uint16(data[:2]), nil
|
||||
}
|
||||
|
||||
func decodeFloat32(data []byte) float32 {
|
||||
return math.Float32frombits(binary.BigEndian.Uint32(data))
|
||||
}
|
||||
|
||||
func roundFixed(value float32, scale int32) int32 {
|
||||
scaled := value * float32(scale)
|
||||
if scaled < 0 {
|
||||
return int32(scaled - 0.5)
|
||||
}
|
||||
return int32(scaled + 0.5)
|
||||
}
|
||||
|
||||
func crc8(data []byte) byte {
|
||||
value := byte(0xff)
|
||||
for _, current := range data {
|
||||
value ^= current
|
||||
for bit := 0; bit < 8; bit++ {
|
||||
if value&0x80 != 0 {
|
||||
value = value<<1 ^ 0x31
|
||||
} else {
|
||||
value <<= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
package scd30
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
type transaction struct {
|
||||
write []byte
|
||||
response []byte
|
||||
err error
|
||||
}
|
||||
|
||||
type fakeBus struct {
|
||||
t *testing.T
|
||||
transactions []transaction
|
||||
next int
|
||||
wrongAddress bool
|
||||
extraTransfer bool
|
||||
}
|
||||
|
||||
func (b *fakeBus) Tx(address uint16, write, read []byte) error {
|
||||
b.t.Helper()
|
||||
if address != Address {
|
||||
b.wrongAddress = true
|
||||
b.t.Errorf("address = %#x, want %#x", address, Address)
|
||||
return nil
|
||||
}
|
||||
if b.next >= len(b.transactions) {
|
||||
b.extraTransfer = true
|
||||
b.t.Errorf("unexpected transaction: write=%#v read-len=%d", write, len(read))
|
||||
return nil
|
||||
}
|
||||
|
||||
want := b.transactions[b.next]
|
||||
b.next++
|
||||
if !bytes.Equal(write, want.write) {
|
||||
b.t.Errorf("write = %#v, want %#v", write, want.write)
|
||||
}
|
||||
if len(read) != len(want.response) {
|
||||
b.t.Errorf("read length = %d, want %d", len(read), len(want.response))
|
||||
}
|
||||
if want.err != nil {
|
||||
return want.err
|
||||
}
|
||||
copy(read, want.response)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *fakeBus) verify(t *testing.T) {
|
||||
t.Helper()
|
||||
if b.next != len(b.transactions) {
|
||||
t.Errorf("completed %d transactions, want %d", b.next, len(b.transactions))
|
||||
}
|
||||
if b.wrongAddress {
|
||||
t.Error("driver used an unexpected address")
|
||||
}
|
||||
if b.extraTransfer {
|
||||
t.Error("driver performed an unexpected transfer")
|
||||
}
|
||||
}
|
||||
|
||||
func newFakeBus(t *testing.T, transactions ...transaction) *fakeBus {
|
||||
t.Helper()
|
||||
bus := &fakeBus{t: t, transactions: transactions}
|
||||
t.Cleanup(func() { bus.verify(t) })
|
||||
return bus
|
||||
}
|
||||
|
||||
func TestConfigure(t *testing.T) {
|
||||
bus := newFakeBus(t,
|
||||
transaction{write: []byte{0x46, 0x00, 0x00, 0x0a, 0x5a}},
|
||||
transaction{write: []byte{0x53, 0x06, 0x00, 0x01, 0xb0}},
|
||||
)
|
||||
err := New(bus).Configure(Config{
|
||||
MeasurementInterval: 10,
|
||||
AutomaticSelfCalibration: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
if DefaultConfig.MeasurementInterval != 2 {
|
||||
t.Errorf("default interval = %d, want 2", DefaultConfig.MeasurementInterval)
|
||||
}
|
||||
if DefaultConfig.AutomaticSelfCalibration {
|
||||
t.Error("automatic self-calibration should be disabled by default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeasurementIntervalBounds(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
seconds uint16
|
||||
valid bool
|
||||
}{
|
||||
{name: "below minimum", seconds: 1},
|
||||
{name: "minimum", seconds: 2, valid: true},
|
||||
{name: "maximum", seconds: 1800, valid: true},
|
||||
{name: "above maximum", seconds: 1801},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var transactions []transaction
|
||||
if test.valid {
|
||||
argument := []byte{byte(test.seconds >> 8), byte(test.seconds)}
|
||||
transactions = append(transactions, transaction{write: []byte{
|
||||
0x46, 0x00, argument[0], argument[1], crc8(argument),
|
||||
}})
|
||||
}
|
||||
bus := newFakeBus(t, transactions...)
|
||||
err := New(bus).SetMeasurementInterval(test.seconds)
|
||||
if test.valid && err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !test.valid && !errors.Is(err, ErrInvalidInterval) {
|
||||
t.Fatalf("error = %v, want %v", err, ErrInvalidInterval)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAmbientPressureBounds(t *testing.T) {
|
||||
tests := []struct {
|
||||
pressure uint16
|
||||
valid bool
|
||||
}{
|
||||
{pressure: 0, valid: true},
|
||||
{pressure: 699},
|
||||
{pressure: 700, valid: true},
|
||||
{pressure: 1400, valid: true},
|
||||
{pressure: 1401},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(stringName(test.pressure), func(t *testing.T) {
|
||||
var transactions []transaction
|
||||
if test.valid {
|
||||
argument := []byte{byte(test.pressure >> 8), byte(test.pressure)}
|
||||
transactions = append(transactions, transaction{write: []byte{
|
||||
0x00, 0x10, argument[0], argument[1], crc8(argument),
|
||||
}})
|
||||
}
|
||||
bus := newFakeBus(t, transactions...)
|
||||
err := New(bus).StartContinuousMeasurement(test.pressure)
|
||||
if test.valid && err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !test.valid && !errors.Is(err, ErrInvalidAmbientPressure) {
|
||||
t.Fatalf("error = %v, want %v", err, ErrInvalidAmbientPressure)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopContinuousMeasurement(t *testing.T) {
|
||||
bus := newFakeBus(t, transaction{write: []byte{0x01, 0x04}})
|
||||
if err := New(bus).StopContinuousMeasurement(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataReady(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
value uint16
|
||||
ready bool
|
||||
}{
|
||||
{name: "not ready", value: 0},
|
||||
{name: "ready", value: 1, ready: true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
bus := newFakeBus(t,
|
||||
transaction{write: []byte{0x02, 0x02}},
|
||||
transaction{response: encodeWord(test.value)},
|
||||
)
|
||||
ready, err := New(bus).DataReady()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ready != test.ready {
|
||||
t.Errorf("ready = %v, want %v", ready, test.ready)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataReadyRejectsBadCRC(t *testing.T) {
|
||||
response := encodeWord(1)
|
||||
response[2]++
|
||||
bus := newFakeBus(t,
|
||||
transaction{write: []byte{0x02, 0x02}},
|
||||
transaction{response: response},
|
||||
)
|
||||
_, err := New(bus).DataReady()
|
||||
if !errors.Is(err, ErrCRC) {
|
||||
t.Fatalf("error = %v, want %v", err, ErrCRC)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMeasurement(t *testing.T) {
|
||||
response := appendFloat(nil, 800.5)
|
||||
response = appendFloat(response, 23.25)
|
||||
response = appendFloat(response, 48.75)
|
||||
bus := newFakeBus(t,
|
||||
transaction{write: []byte{0x03, 0x00}},
|
||||
transaction{response: response},
|
||||
)
|
||||
|
||||
device := New(bus)
|
||||
if err := device.ReadMeasurement(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := device.CO2(); got != 801 {
|
||||
t.Errorf("CO2 = %d, want 801 ppm", got)
|
||||
}
|
||||
if got := device.Temperature(); got != 23250 {
|
||||
t.Errorf("temperature = %d, want 23250 mC", got)
|
||||
}
|
||||
if got := device.Humidity(); got != 4875 {
|
||||
t.Errorf("humidity = %d, want 4875 hundredths of a percent", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMeasurementRoundsNegativeTemperature(t *testing.T) {
|
||||
response := appendFloat(nil, 400)
|
||||
response = appendFloat(response, -10.1236)
|
||||
response = appendFloat(response, 50)
|
||||
bus := newFakeBus(t,
|
||||
transaction{write: []byte{0x03, 0x00}},
|
||||
transaction{response: response},
|
||||
)
|
||||
|
||||
device := New(bus)
|
||||
if err := device.ReadMeasurement(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := device.Temperature(); got != -10124 {
|
||||
t.Errorf("temperature = %d, want -10124 mC", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMeasurementRejectsBadCRCWithoutChangingCache(t *testing.T) {
|
||||
for corruptWord := 0; corruptWord < 6; corruptWord++ {
|
||||
t.Run(stringName(uint16(corruptWord)), func(t *testing.T) {
|
||||
response := appendFloat(nil, 800.5)
|
||||
response = appendFloat(response, 23.25)
|
||||
response = appendFloat(response, 48.75)
|
||||
response[corruptWord*3+2]++
|
||||
bus := newFakeBus(t,
|
||||
transaction{write: []byte{0x03, 0x00}},
|
||||
transaction{response: response},
|
||||
)
|
||||
|
||||
device := New(bus)
|
||||
device.co2 = 500
|
||||
device.temperature = 21000
|
||||
device.humidity = 4000
|
||||
err := device.ReadMeasurement()
|
||||
if !errors.Is(err, ErrCRC) {
|
||||
t.Fatalf("error = %v, want %v", err, ErrCRC)
|
||||
}
|
||||
if device.CO2() != 500 || device.Temperature() != 21000 || device.Humidity() != 4000 {
|
||||
t.Errorf("cache changed after CRC error: CO2=%d temperature=%d humidity=%d",
|
||||
device.CO2(), device.Temperature(), device.Humidity())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateIgnoresUnsupportedMeasurements(t *testing.T) {
|
||||
bus := newFakeBus(t)
|
||||
if err := New(bus).Update(drivers.Pressure); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBusErrorsAreReturned(t *testing.T) {
|
||||
wantErr := errors.New("I2C failure")
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
transactions []transaction
|
||||
action func(*Device) error
|
||||
}{
|
||||
{
|
||||
name: "command write",
|
||||
transactions: []transaction{{write: []byte{0x02, 0x02}, err: wantErr}},
|
||||
action: func(device *Device) error {
|
||||
_, err := device.DataReady()
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "response read",
|
||||
transactions: []transaction{
|
||||
{write: []byte{0x02, 0x02}},
|
||||
{response: make([]byte, 3), err: wantErr},
|
||||
},
|
||||
action: func(device *Device) error {
|
||||
_, err := device.DataReady()
|
||||
return err
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
bus := newFakeBus(t, test.transactions...)
|
||||
if err := test.action(New(bus)); !errors.Is(err, wantErr) {
|
||||
t.Fatalf("error = %v, want %v", err, wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRC8(t *testing.T) {
|
||||
if got := crc8([]byte{0x00, 0x02}); got != 0xe3 {
|
||||
t.Fatalf("crc = %#x, want 0xe3", got)
|
||||
}
|
||||
}
|
||||
|
||||
func appendFloat(destination []byte, value float32) []byte {
|
||||
bits := math.Float32bits(value)
|
||||
destination = append(destination, encodeWord(uint16(bits>>16))...)
|
||||
destination = append(destination, encodeWord(uint16(bits))...)
|
||||
return destination
|
||||
}
|
||||
|
||||
func encodeWord(value uint16) []byte {
|
||||
result := []byte{byte(value >> 8), byte(value), 0}
|
||||
result[2] = crc8(result[:2])
|
||||
return result
|
||||
}
|
||||
|
||||
func stringName(value uint16) string {
|
||||
const digits = "0123456789"
|
||||
if value == 0 {
|
||||
return "0"
|
||||
}
|
||||
var buffer [5]byte
|
||||
position := len(buffer)
|
||||
for value > 0 {
|
||||
position--
|
||||
buffer[position] = digits[value%10]
|
||||
value /= 10
|
||||
}
|
||||
return string(buffer[position:])
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
## `sd` package
|
||||
|
||||
File map:
|
||||
* `blockdevice.go`: Contains logic for creating an `io.WriterAt` and `io.ReaderAt` with the `sd.BlockDevice` concrete type
|
||||
from the `sd.Card` interface which is intrinsically a blocked reader and writer.
|
||||
|
||||
* `spicard.go`: Contains the `sd.SpiCard` driver for controlling an SD card over SPI using the most commonly available circuit boards.
|
||||
|
||||
* `responses.go`: Contains a currently unused SD response implementations as per the latest specification.
|
||||
|
||||
* `definitions.go`: Contains SD Card specification definitions such as the CSD and CID types as well as encoding/decoding logic, as well as CRC logic.
|
||||
@@ -0,0 +1,231 @@
|
||||
package sd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"math/bits"
|
||||
)
|
||||
|
||||
var (
|
||||
errNegativeOffset = errors.New("sd: negative offset")
|
||||
)
|
||||
|
||||
// Compile time guarantee of interface implementation.
|
||||
var _ Card = (*SPICard)(nil)
|
||||
var _ io.ReaderAt = (*BlockDevice)(nil)
|
||||
var _ io.WriterAt = (*BlockDevice)(nil)
|
||||
|
||||
// Card is the interface implemented by SD card drivers such as [SPICard].
|
||||
// It provides block-aligned I/O over the card's contents. Use [NewBlockDevice]
|
||||
// to wrap a Card with byte-addressed [io.ReaderAt] and [io.WriterAt] interfaces.
|
||||
type Card interface {
|
||||
// WriteBlocks writes the given data to the card, starting at the given block index.
|
||||
// The data must be a multiple of the block size.
|
||||
WriteBlocks(data []byte, startBlockIdx int64) (int, error)
|
||||
// ReadBlocks reads the given number of blocks from the card, starting at the given block index.
|
||||
// The dst buffer must be a multiple of the block size.
|
||||
ReadBlocks(dst []byte, startBlockIdx int64) (int, error)
|
||||
// EraseBlocks erases blocks starting at startBlockIdx to startBlockIdx+numBlocks.
|
||||
EraseBlocks(startBlock, numBlocks int64) error
|
||||
}
|
||||
|
||||
// NewBlockDevice creates a new [BlockDevice] from a Card. blockSize must be a
|
||||
// power of 2. For an initialized [SPICard], blockSize is typically the CSD's
|
||||
// [CSD.ReadBlockLen] and numBlocks is [SPICard.NumberOfBlocks].
|
||||
func NewBlockDevice(card Card, blockSize int, numBlocks int64) (*BlockDevice, error) {
|
||||
if card == nil || blockSize <= 0 || numBlocks <= 0 {
|
||||
return nil, errors.New("invalid argument(s)")
|
||||
}
|
||||
blk, err := makeBlockIndexer(blockSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bd := &BlockDevice{
|
||||
card: card,
|
||||
blockbuf: make([]byte, blockSize),
|
||||
blk: blk,
|
||||
numblocks: int64(numBlocks),
|
||||
}
|
||||
return bd, nil
|
||||
}
|
||||
|
||||
// BlockDevice implements the tinyfs.BlockDevice interface for a [Card],
|
||||
// providing byte-addressed reads and writes at arbitrary offsets by buffering
|
||||
// non-block-aligned accesses through an internal single-block buffer.
|
||||
// BlockDevice is not safe for concurrent use.
|
||||
type BlockDevice struct {
|
||||
card Card
|
||||
blockbuf []byte
|
||||
blk blkIdxer
|
||||
numblocks int64
|
||||
}
|
||||
|
||||
// ReadAt implements the [io.ReaderAt] interface for an SD card.
|
||||
// Reads need not be aligned to block boundaries.
|
||||
func (bd *BlockDevice) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
if off < 0 {
|
||||
return 0, errNegativeOffset
|
||||
}
|
||||
|
||||
blockIdx := bd.blk.idx(off)
|
||||
blockOff := bd.blk.off(off)
|
||||
if blockOff != 0 {
|
||||
// Non-aligned first block case.
|
||||
if _, err = bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
|
||||
return n, err
|
||||
}
|
||||
n += copy(p, bd.blockbuf[blockOff:])
|
||||
p = p[n:]
|
||||
blockIdx++
|
||||
}
|
||||
|
||||
fullBlocksToRead := bd.blk.idx(int64(len(p)))
|
||||
if fullBlocksToRead > 0 {
|
||||
// 1 or more full blocks case.
|
||||
endOffset := fullBlocksToRead * bd.blk.size()
|
||||
ngot, err := bd.card.ReadBlocks(p[:endOffset], blockIdx)
|
||||
if err != nil {
|
||||
return n + ngot, err
|
||||
}
|
||||
p = p[endOffset:]
|
||||
n += ngot
|
||||
blockIdx += fullBlocksToRead
|
||||
}
|
||||
|
||||
if len(p) > 0 {
|
||||
// Non-aligned last block case.
|
||||
if _, err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
|
||||
return n, err
|
||||
}
|
||||
n += copy(p, bd.blockbuf)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// WriteAt implements the [io.WriterAt] interface for an SD card. Writes need
|
||||
// not be aligned to block boundaries: partial blocks are read, modified and
|
||||
// written back.
|
||||
func (bd *BlockDevice) WriteAt(p []byte, off int64) (n int, err error) {
|
||||
if off < 0 {
|
||||
return 0, errNegativeOffset
|
||||
}
|
||||
|
||||
blockIdx := bd.blk.idx(off)
|
||||
blockOff := bd.blk.off(off)
|
||||
if blockOff != 0 {
|
||||
// Non-aligned first block case.
|
||||
if _, err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
|
||||
return n, err
|
||||
}
|
||||
nexpect := copy(bd.blockbuf[blockOff:], p)
|
||||
ngot, err := bd.card.WriteBlocks(bd.blockbuf, blockIdx)
|
||||
if err != nil {
|
||||
return n, err
|
||||
} else if ngot != len(bd.blockbuf) {
|
||||
return n, io.ErrShortWrite
|
||||
}
|
||||
n += nexpect
|
||||
p = p[nexpect:]
|
||||
blockIdx++
|
||||
}
|
||||
|
||||
fullBlocksToWrite := bd.blk.idx(int64(len(p)))
|
||||
if fullBlocksToWrite > 0 {
|
||||
// 1 or more full blocks case.
|
||||
endOffset := fullBlocksToWrite * bd.blk.size()
|
||||
ngot, err := bd.card.WriteBlocks(p[:endOffset], blockIdx)
|
||||
n += ngot
|
||||
if err != nil {
|
||||
return n, err
|
||||
} else if ngot != int(endOffset) {
|
||||
return n, io.ErrShortWrite
|
||||
}
|
||||
p = p[ngot:]
|
||||
blockIdx += fullBlocksToWrite
|
||||
}
|
||||
|
||||
if len(p) > 0 {
|
||||
// Non-aligned last block case.
|
||||
if _, err := bd.card.ReadBlocks(bd.blockbuf, blockIdx); err != nil {
|
||||
return n, err
|
||||
}
|
||||
copy(bd.blockbuf, p)
|
||||
ngot, err := bd.card.WriteBlocks(bd.blockbuf, blockIdx)
|
||||
if err != nil {
|
||||
return n, err
|
||||
} else if ngot != len(bd.blockbuf) {
|
||||
return n, io.ErrShortWrite
|
||||
}
|
||||
n += len(p)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Size returns the number of bytes in this block device.
|
||||
func (bd *BlockDevice) Size() int64 {
|
||||
return bd.BlockSize() * bd.numblocks
|
||||
}
|
||||
|
||||
// BlockSize returns the size of a block in bytes.
|
||||
func (bd *BlockDevice) BlockSize() int64 {
|
||||
return bd.blk.size()
|
||||
}
|
||||
|
||||
// EraseBlocks erases the given number of blocks. An implementation may
|
||||
// transparently coalesce ranges of blocks into larger bundles if the chip
|
||||
// supports this. The start and len parameters are in block numbers, use
|
||||
// EraseBlockSize to map addresses to blocks.
|
||||
func (bd *BlockDevice) EraseBlocks(startEraseBlockIdx, len int64) error {
|
||||
return bd.card.EraseBlocks(startEraseBlockIdx, len)
|
||||
}
|
||||
|
||||
// blkIdxer is a helper for calculating block indices and offsets.
|
||||
type blkIdxer struct {
|
||||
blockshift int64
|
||||
blockmask int64
|
||||
}
|
||||
|
||||
// makeBlockIndexer returns a blkIdxer for the given block size,
|
||||
// which must be a power of 2.
|
||||
func makeBlockIndexer(blockSize int) (blkIdxer, error) {
|
||||
if blockSize <= 0 {
|
||||
return blkIdxer{}, errNoblocks
|
||||
}
|
||||
tz := bits.TrailingZeros(uint(blockSize))
|
||||
if blockSize>>tz != 1 {
|
||||
return blkIdxer{}, errors.New("blockSize must be a power of 2")
|
||||
}
|
||||
blk := blkIdxer{
|
||||
blockshift: int64(tz),
|
||||
blockmask: (1 << tz) - 1,
|
||||
}
|
||||
return blk, nil
|
||||
}
|
||||
|
||||
// size returns the size of a block in bytes.
|
||||
func (blk *blkIdxer) size() int64 {
|
||||
return 1 << blk.blockshift
|
||||
}
|
||||
|
||||
// off gets the offset of the byte at byteIdx from the start of its block.
|
||||
//
|
||||
//go:inline
|
||||
func (blk *blkIdxer) off(byteIdx int64) int64 {
|
||||
return blk._moduloBlockSize(byteIdx)
|
||||
}
|
||||
|
||||
// idx gets the block index that contains the byte at byteIdx.
|
||||
//
|
||||
//go:inline
|
||||
func (blk *blkIdxer) idx(byteIdx int64) int64 {
|
||||
return blk._divideBlockSize(byteIdx)
|
||||
}
|
||||
|
||||
// modulo and divide are defined in terms of bit operations for speed since
|
||||
// blockSize is a power of 2.
|
||||
|
||||
//go:inline
|
||||
func (blk *blkIdxer) _moduloBlockSize(n int64) int64 { return n & blk.blockmask }
|
||||
|
||||
//go:inline
|
||||
func (blk *blkIdxer) _divideBlockSize(n int64) int64 { return n >> blk.blockshift }
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
package sd
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCRC16(t *testing.T) {
|
||||
tests := []struct {
|
||||
block string
|
||||
wantcrc uint16
|
||||
}{
|
||||
{
|
||||
block: "fa33c08ed0bc007c8bf45007501ffbfcbf0006b90001f2a5ea1d060000bebe07b304803c80740e803c00751c83c610fecb75efcd188b148b4c028bee83c610fecb741a803c0074f4be8b06ac3c00740b56bb0700b40ecd105eebf0ebfebf0500bb007cb8010257cd135f730c33c0cd134f75edbea306ebd3bec206bffe7d813d55aa75c78bf5ea007c0000496e76616c696420706172746974696f6e207461626c65004572726f72206c6f6164696e67206f7065726174696e672073797374656d004d697373696e67206f7065726174696e672073797374656d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000094c6dffd0000000401040cfec2ff000800000000f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000055aa",
|
||||
wantcrc: 0x52ce,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
b, err := hex.DecodeString(tt.block)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gotcrc := CRC16(b)
|
||||
if gotcrc != tt.wantcrc {
|
||||
t.Errorf("calculateCRC(%s) = %#x, want %#x", tt.block, gotcrc, tt.wantcrc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRC7(t *testing.T) {
|
||||
const cmdSendMask = 0x40
|
||||
tests := []struct {
|
||||
data []byte
|
||||
wantCRC uint8
|
||||
}{
|
||||
{ // See CRC7 Examples from section 4.5 of the SD Card Physical Layer Simplified Specification.
|
||||
data: []byte{cmdSendMask, 4: 0}, // CMD0, arg=0
|
||||
wantCRC: 0b1001010,
|
||||
},
|
||||
{
|
||||
data: []byte{cmdSendMask | 17, 4: 0}, // CMD17, arg=0
|
||||
wantCRC: 0b0101010,
|
||||
},
|
||||
{
|
||||
data: []byte{17, 3: 0b1001, 4: 0}, // Response of CMD17
|
||||
wantCRC: 0b0110011,
|
||||
},
|
||||
{ // CSD for a 8GB card.
|
||||
data: []byte{64, 14, 0, 50, 83, 89, 0, 0, 60, 1, 127, 128, 10, 64, 0},
|
||||
wantCRC: 0b1110010,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
gotcrc := CRC7(tt.data[:])
|
||||
if gotcrc != tt.wantCRC {
|
||||
t.Errorf("got crc=%#b, want=%#b for %#b", gotcrc, tt.wantCRC, tt.data)
|
||||
}
|
||||
}
|
||||
|
||||
cmdTests := []struct {
|
||||
cmd command
|
||||
arg uint32
|
||||
wantCRC uint8
|
||||
}{
|
||||
{
|
||||
cmd: cmdGoIdleState,
|
||||
arg: 0,
|
||||
wantCRC: 0x95,
|
||||
},
|
||||
{
|
||||
cmd: cmdSendIfCond,
|
||||
arg: 0x1AA,
|
||||
wantCRC: 0x87,
|
||||
},
|
||||
}
|
||||
var dst [6]byte
|
||||
for _, test := range cmdTests {
|
||||
putCmd(dst[:], test.cmd, test.arg)
|
||||
gotcrc := dst[5]
|
||||
if gotcrc != test.wantCRC {
|
||||
t.Errorf("got crc=%#x, want=%#x", gotcrc, test.wantCRC)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCSDv2Capacity checks CSDv2 C_SIZE decoding and the capacity formula.
|
||||
//
|
||||
// Field layout and formula are from the SD Physical Layer Simplified
|
||||
// Specification Version 9.10, section 5.3.3 "CSD Register (CSD Version 2.0)":
|
||||
// C_SIZE occupies CSD bits [69:48] and user memory capacity is
|
||||
//
|
||||
// memory capacity = (C_SIZE+1) * 512KByte (512KByte = 524288 bytes)
|
||||
//
|
||||
// Specification download: https://www.sdcard.org/downloads/pls/
|
||||
//
|
||||
// Regression test for two past bugs in CSDv2:
|
||||
// - csize() read byte 7 as data[7]>>2 instead of data[7]&0x3F, dropping
|
||||
// C_SIZE bits [17:16] (misdecoded cards > 32GiB).
|
||||
// - DeviceCapacity() computed csize*512000 instead of (csize+1)*524288.
|
||||
func TestCSDv2Capacity(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
csd []byte
|
||||
wantCSize uint32
|
||||
wantCap int64
|
||||
}{
|
||||
{
|
||||
// Same 8GB-card register as in TestCRC7 above, CRC byte appended
|
||||
// (CRC7=0b1110010 per that test, stored as crc<<1|always1).
|
||||
// C_SIZE = 0x003C01 = 15361 -> 15362 * 524288 = 8054112256 bytes.
|
||||
name: "8GB card (in-repo vector)",
|
||||
csd: []byte{64, 14, 0, 50, 83, 89, 0, 0, 60, 1, 127, 128, 10, 64, 0, 0b1110010<<1 | 1},
|
||||
wantCSize: 0x003C01,
|
||||
wantCap: 8054112256,
|
||||
},
|
||||
// {
|
||||
// name: "8GB card",
|
||||
// csd: csdv2Bytes(0x003C01),
|
||||
// wantCSize: 0x003FFF,
|
||||
// wantCap: 2 << 40,
|
||||
// },
|
||||
{
|
||||
// Synthetic: C_SIZE = 0x01FFFF -> 131072 * 524288 = 64GiB.
|
||||
// Exercises C_SIZE bits [17:16], stored in CSD byte 7 (CSD bits [65:64]).
|
||||
name: "64GiB synthetic",
|
||||
csd: csdv2Bytes(0x01FFFF),
|
||||
wantCSize: 0x01FFFF,
|
||||
wantCap: 64 << 30,
|
||||
},
|
||||
{
|
||||
// Synthetic: maximum v2 C_SIZE = 0x3FFFFF -> 4194304 * 524288 = 2TiB,
|
||||
// the SDXC upper bound per section 5.3.3.
|
||||
name: "2TiB synthetic (max C_SIZE)",
|
||||
csd: csdv2Bytes(0x3FFFFF),
|
||||
wantCSize: 0x3FFFFF,
|
||||
wantCap: 2 << 40,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
csd, err := DecodeCSD(tt.csd)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: DecodeCSD: %v", tt.name, err)
|
||||
}
|
||||
v2 := csd.MustV2()
|
||||
if got := v2.csize(); got != tt.wantCSize {
|
||||
t.Errorf("%s: csize() = %#x, want %#x", tt.name, got, tt.wantCSize)
|
||||
}
|
||||
if got := csd.DeviceCapacity(); got != tt.wantCap {
|
||||
t.Errorf("%s: DeviceCapacity() = %d, want %d", tt.name, got, tt.wantCap)
|
||||
}
|
||||
wantBlocks := tt.wantCap / int64(csd.ReadBlockLen())
|
||||
if got := csd.NumberOfBlocks(); got != wantBlocks {
|
||||
t.Errorf("%s: NumberOfBlocks() = %d, want %d", tt.name, got, wantBlocks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// csdv2Bytes returns a 16-byte CSD v2 register with csize spliced into
|
||||
// CSD bits [69:48] and a freshly computed CRC7+always1 last byte.
|
||||
// Non-capacity fields are copied from the 8GB-card vector above.
|
||||
func csdv2Bytes(csize uint32) []byte {
|
||||
b := []byte{64, 14, 0, 50, 83, 89, 0, 0, 60, 1, 127, 128, 10, 64, 0}
|
||||
b[7] = byte(csize >> 16 & 0x3F)
|
||||
b[8] = byte(csize >> 8)
|
||||
b[9] = byte(csize)
|
||||
return append(b, crc7noshift(b)|1)
|
||||
}
|
||||
|
||||
func putCmd(dst []byte, cmd command, arg uint32) {
|
||||
dst[0] = byte(cmd) | (1 << 6)
|
||||
dst[1] = byte(arg >> 24)
|
||||
dst[2] = byte(arg >> 16)
|
||||
dst[3] = byte(arg >> 8)
|
||||
dst[4] = byte(arg)
|
||||
dst[5] = crc7noshift(dst[:5]) | 1 // Stop bit added.
|
||||
}
|
||||
@@ -0,0 +1,585 @@
|
||||
package sd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// For reference of CID/CSD structs see:
|
||||
// See https://github.com/arduino-libraries/SD/blob/1c56f58252553c7537f7baf62798cacc625aa543/src/utility/SdInfo.h#L110
|
||||
|
||||
// CardKind classifies an SD card by its capacity class and specification
|
||||
// version, as discovered during card initialization.
|
||||
type CardKind uint8
|
||||
|
||||
// isTimeout reports whether err is one of the package's timeout errors.
|
||||
func isTimeout(err error) bool {
|
||||
return err == errReadTimeout || err == errWriteTimeout || err == errBusyTimeout
|
||||
}
|
||||
|
||||
const (
|
||||
// card types
|
||||
TypeSD1 CardKind = 1 // Standard capacity V1 SD card
|
||||
TypeSD2 CardKind = 2 // Standard capacity V2 SD card
|
||||
TypeSDHC CardKind = 3 // High Capacity SD card
|
||||
)
|
||||
|
||||
// CID is the Card Identification register, a 128-bit (16-byte) read-only
|
||||
// register holding the card's identification information: manufacturer,
|
||||
// product name, serial number and manufacturing date, among other data.
|
||||
// It is programmed during card manufacture and cannot be changed.
|
||||
type CID struct {
|
||||
data [16]byte
|
||||
}
|
||||
|
||||
// DecodeCID decodes a CID from the first 16 bytes of b. It returns an error
|
||||
// if b is too short or if the CRC7/always-1 fields are invalid.
|
||||
func DecodeCID(b []byte) (cid CID, _ error) {
|
||||
if len(b) < 16 {
|
||||
return CID{}, io.ErrShortBuffer
|
||||
}
|
||||
copy(cid.data[:], b)
|
||||
if !cid.IsValid() {
|
||||
return cid, errBadCSDCID
|
||||
}
|
||||
return cid, nil
|
||||
}
|
||||
|
||||
// RawCopy returns a copy of the raw CID data.
|
||||
func (c *CID) RawCopy() [16]byte { return c.data }
|
||||
|
||||
// ManufacturerID is an 8-bit binary number that identifies the card manufacturer. The MID number is controlled, defined, and allocated to a SD Memory Card manufacturer by the SD-3C, LLC.
|
||||
func (c *CID) ManufacturerID() uint8 { return c.data[0] }
|
||||
|
||||
// OEMApplicationID A 2-character ASCII string that identifies the card OEM and/or the card contents (when used as a
|
||||
// distribution media either on ROM or FLASH cards). The OID number is controlled, defined, and allocated
|
||||
// to a SD Memory Card manufacturer by the SD-3C, LLC
|
||||
func (c *CID) OEMApplicationID() uint16 {
|
||||
return binary.BigEndian.Uint16(c.data[1:3])
|
||||
}
|
||||
|
||||
// ProductName returns the product name, an ASCII string of up to 5 characters.
|
||||
func (c *CID) ProductName() string {
|
||||
return string(upToNull(c.data[3:8]))
|
||||
}
|
||||
|
||||
// ProductRevision is composed of two Binary Coded Decimal (BCD) digits, four bits each, representing
|
||||
// an "n.m" revision number. The "n" is the most significant nibble and "m" is the least significant nibble.
|
||||
// As an example, the PRV binary value field for product revision "6.2" will be: 0110 0010b
|
||||
func (c *CID) ProductRevision() (n, m uint8) {
|
||||
rev := c.data[8]
|
||||
return rev >> 4, rev & 0x0F
|
||||
}
|
||||
|
||||
// ProductSerialNumber returns the product serial number, a 32-bit binary number.
|
||||
func (c *CID) ProductSerialNumber() uint32 {
|
||||
return binary.BigEndian.Uint32(c.data[9:13])
|
||||
}
|
||||
|
||||
// ManufacturingDate returns the manufacturing date of the card,
|
||||
// e.g. year=2023, month=4 for April 2023.
|
||||
func (c *CID) ManufacturingDate() (year uint16, month uint8) {
|
||||
date := binary.BigEndian.Uint16(c.data[13:15])
|
||||
return (date >> 4) + 2000, uint8(date & 0x0F)
|
||||
}
|
||||
|
||||
// CRC7 returns the CRC7 checksum for this CID. May be invalid. Use [IsValid] to check validity of CRC7+Always1 fields.
|
||||
func (c *CID) CRC7() uint8 { return c.data[15] >> 1 }
|
||||
|
||||
// Always1 checks the presence of the Always 1 bit. Should return true for valid CIDs.
|
||||
func (c *CID) Always1() bool { return c.data[15]&1 != 0 }
|
||||
|
||||
// IsValid checks if the CRC and always1 fields are expected values.
|
||||
func (c *CID) IsValid() bool { return c.Always1() && CRC7(c.data[:15]) == c.CRC7() }
|
||||
|
||||
// CSD is the Card Specific Data register, a 128-bit (16-byte) register that defines how
|
||||
// the SD card standard communicates with the memory field or register. This type is
|
||||
// shared among V1 and V2 type devices.
|
||||
type CSD struct {
|
||||
data [16]byte
|
||||
}
|
||||
|
||||
// CSDv1 is the Card Specific Data register for V1 devices. See [CSD] for more info.
|
||||
type CSDv1 struct {
|
||||
CSD
|
||||
}
|
||||
|
||||
// CSDv2 is the Card Specific Data register for V2 devices. See [CSD] for more info.
|
||||
type CSDv2 struct {
|
||||
CSD
|
||||
}
|
||||
|
||||
// DecodeCSD decodes the CSD from a 16-byte slice.
|
||||
func DecodeCSD(b []byte) (CSD, error) {
|
||||
if len(b) < 16 {
|
||||
return CSD{}, io.ErrShortBuffer
|
||||
}
|
||||
csd := CSD{}
|
||||
copy(csd.data[:], b)
|
||||
if !csd.IsValid() {
|
||||
return csd, errBadCSDCID
|
||||
}
|
||||
return csd, nil
|
||||
}
|
||||
|
||||
// csdStructure returns the version of the CSD structure.
|
||||
func (c *CSD) csdStructure() uint8 { return c.data[0] >> 6 }
|
||||
|
||||
// Version returns the version of the CSD structure. Effectively returns 1+CSDStructure.
|
||||
func (c *CSD) Version() uint8 { return 1 + c.csdStructure() }
|
||||
|
||||
// MustV1 returns the CSD as a CSDv1. Panics if the CSD is not version 1.0.
|
||||
func (c CSD) MustV1() CSDv1 {
|
||||
if c.csdStructure() != 0 {
|
||||
panic("CSD is not version 1.0")
|
||||
}
|
||||
return CSDv1{CSD: c}
|
||||
}
|
||||
|
||||
// MustV2 returns the CSD as a CSDv2. Panics if the CSD is not version 2.0.
|
||||
func (c CSD) MustV2() CSDv2 {
|
||||
if c.csdStructure() != 1 {
|
||||
panic("CSD is not version 2.0")
|
||||
}
|
||||
return CSDv2{CSD: c}
|
||||
}
|
||||
|
||||
// RawCopy returns a copy of the raw CSD data.
|
||||
func (c *CSD) RawCopy() [16]byte { return c.data }
|
||||
|
||||
// TAAC returns the Time Access Attribute Class (data read access-time-1).
|
||||
func (c *CSD) TAAC() TAAC { return TAAC(c.data[1]) }
|
||||
|
||||
// NSAC returns the Data Read Access-time 2 in CLK cycles (NSAC*100).
|
||||
func (c *CSD) NSAC() NSAC { return NSAC(c.data[2]) }
|
||||
|
||||
// TransferSpeed returns the Max Data Transfer Rate. Either 0x32 or 0x5A.
|
||||
func (c *CSD) TransferSpeed() TransferSpeed { return TransferSpeed(c.data[3]) }
|
||||
|
||||
// CommandClasses returns the supported Card Command Classes as a bitfield;
|
||||
// bit position i set means command class i is supported by the card.
|
||||
func (c *CSD) CommandClasses() CommandClasses {
|
||||
return CommandClasses(uint16(c.data[4])<<4 | uint16(c.data[5]&0xf0)>>4)
|
||||
}
|
||||
|
||||
// ReadBlockLen returns the Max Read Data Block Length in bytes.
|
||||
func (c *CSD) ReadBlockLen() int { return 1 << c.ReadBlockLenShift() }
|
||||
|
||||
// ReadBlockLenShift returns the base-2 logarithm of [CSD.ReadBlockLen] (READ_BL_LEN field).
|
||||
func (c *CSD) ReadBlockLenShift() uint8 { return c.data[5] & 0x0F }
|
||||
|
||||
// AllowsReadBlockPartial indicates that partial block reads (down to a
|
||||
// single byte) are allowed. Always true for SD cards.
|
||||
func (c *CSD) AllowsReadBlockPartial() bool { return c.data[6]&(1<<7) != 0 }
|
||||
|
||||
// AllowsWriteBlockMisalignment defines if the data block to be written by one command
|
||||
// can be spread over more than one physical block of the memory device.
|
||||
func (c *CSD) AllowsWriteBlockMisalignment() bool { return c.data[6]&(1<<6) != 0 }
|
||||
|
||||
// AllowsReadBlockMisalignment defines if the data block to be read by one command
|
||||
// can be spread over more than one physical block of the memory device.
|
||||
func (c *CSD) AllowsReadBlockMisalignment() bool { return c.data[6]&(1<<5) != 0 }
|
||||
|
||||
// CRC7 returns the CRC read for this CSD. May be invalid. Use [IsValid] to check validity of CRC7+Always1 fields.
|
||||
func (c *CSD) CRC7() uint8 { return c.data[15] >> 1 }
|
||||
|
||||
// Always1 checks the Always 1 bit. Should always evaluate to true for valid CSDs.
|
||||
func (c *CSD) Always1() bool { return c.data[15]&1 != 0 }
|
||||
|
||||
// IsValid checks if the CRC and always1 fields are expected values.
|
||||
func (c *CSD) IsValid() bool {
|
||||
// Compare last byte with CRC and also the always1 bit.
|
||||
return c.Always1() && CRC7(c.data[:15]) == c.CRC7()
|
||||
}
|
||||
|
||||
// ImplementsDSR defines if the configurable driver stage is integrated on the card.
|
||||
func (c *CSD) ImplementsDSR() bool { return c.data[6]&(1<<4) != 0 }
|
||||
|
||||
// EraseSectorSizeInBytes returns how much memory is erased by a single
|
||||
// erase command, in bytes (SectorSize multiplied by the write block length).
|
||||
func (c *CSDv1) EraseSectorSizeInBytes() int64 {
|
||||
blklen := c.WriteBlockLen()
|
||||
numblocks := c.SectorSize()
|
||||
return int64(numblocks) * blklen
|
||||
}
|
||||
|
||||
// SectorSize returns the size of an erasable sector in units of write blocks
|
||||
// (SECTOR_SIZE field, range 1..128). Its meaning varies with the CSD version:
|
||||
// for V1 it is the erase unit when [CSD.EraseBlockEnabled] is false; for V2
|
||||
// it is fixed to 64KiB and does not reflect the real erase unit.
|
||||
func (c *CSD) SectorSize() uint8 {
|
||||
return 1 + ((c.data[10]&0b11_1111)<<1 | (c.data[11] >> 7))
|
||||
}
|
||||
|
||||
// EraseBlockEnabled defines granularity of unit size of data to be erased.
|
||||
// If enabled the erase operation can erase either one or multiple units of 512 bytes.
|
||||
func (c *CSD) EraseBlockEnabled() bool { return (c.data[10]>>6)&1 != 0 }
|
||||
|
||||
// ReadToWriteFactor returns the typical write time as a power-of-2 multiple of
|
||||
// the read access time (R2W_FACTOR field), i.e. writeTime = readTime << factor.
|
||||
func (c *CSD) ReadToWriteFactor() uint8 { return (c.data[12] >> 2) & 0b111 }
|
||||
|
||||
// WriteProtectGroupSizeInSectors indicates the size of a write protected
|
||||
// group in multiple of erasable sectors.
|
||||
func (c *CSD) WriteProtectGroupSizeInSectors() uint8 {
|
||||
return 1 + (c.data[11] & 0b111_1111)
|
||||
}
|
||||
|
||||
// WriteBlockLen represents maximum write data block length in bytes.
|
||||
func (c *CSD) WriteBlockLen() int64 {
|
||||
return 1 << ((c.data[12]&0b11)<<2 | (c.data[13] >> 6))
|
||||
}
|
||||
|
||||
// WriteGroupEnabled indicates if write group protection is available.
|
||||
func (c *CSD) WriteGroupEnabled() bool { return c.data[12]&(1<<7) != 0 }
|
||||
|
||||
// AllowsWritePartial Defines whether partial block sizes can be used in write block sizes.
|
||||
func (c *CSD) AllowsWritePartial() bool { return c.data[13]&(1<<5) != 0 }
|
||||
|
||||
// FileFormat returns the file format on the card. This field is read-only for ROM.
|
||||
func (c *CSD) FileFormat() FileFormat { return FileFormat(c.data[14]>>2) & 0b11 }
|
||||
|
||||
// TmpWriteProtected indicates temporary protection over the entire card content from being overwritten or erased.
|
||||
func (c *CSD) TmpWriteProtected() bool { return c.data[14]&(1<<4) != 0 }
|
||||
|
||||
// PermWriteProtected indicates permanent protecttion of entire card content against overwriting or erasing (write+erase permanently disabled).
|
||||
func (c *CSD) PermWriteProtected() bool { return c.data[14]&(1<<5) != 0 }
|
||||
|
||||
// IsCopy whether contents are original or have been copied.
|
||||
func (c *CSD) IsCopy() bool { return c.data[14]&(1<<6) != 0 }
|
||||
|
||||
// FileFormatGroup returns the file format group bit, which selects between
|
||||
// the two [FileFormat] tables. Interpret together with [CSD.FileFormat].
|
||||
func (c *CSD) FileFormatGroup() bool { return c.data[14]&(1<<7) != 0 }
|
||||
|
||||
// DeviceCapacity returns the total device capacity in bytes, dispatching on
|
||||
// the CSD version. Returns 0 for unknown CSD versions.
|
||||
func (c *CSD) DeviceCapacity() (size int64) {
|
||||
switch c.csdStructure() {
|
||||
case 0:
|
||||
v1 := c.MustV1()
|
||||
size = int64(v1.DeviceCapacity())
|
||||
case 1:
|
||||
v2 := c.MustV2()
|
||||
size = v2.DeviceCapacity()
|
||||
}
|
||||
return size
|
||||
}
|
||||
|
||||
// NumberOfBlocks returns amount of readable blocks in the device given by Capacity/ReadBlockLength.
|
||||
func (c *CSD) NumberOfBlocks() (numBlocks int64) {
|
||||
rblocks := c.ReadBlockLen()
|
||||
if rblocks == 0 {
|
||||
return 0
|
||||
}
|
||||
return c.DeviceCapacity() / int64(rblocks)
|
||||
}
|
||||
|
||||
// After byte 5 CSDv1 and CSDv2 differ in structure at some fields.
|
||||
|
||||
// DeviceCapacity returns the device capacity in bytes:
|
||||
// (C_SIZE+1) * 512KiB, as per section 5.3.3 of the SD Simplified Specification.
|
||||
func (c *CSDv2) DeviceCapacity() int64 {
|
||||
csize := c.csize()
|
||||
return (int64(csize) + 1) * (512 * 1024)
|
||||
}
|
||||
|
||||
// csize returns the 22-bit C_SIZE field (CSD bits 69:48).
|
||||
func (c *CSDv2) csize() uint32 {
|
||||
return uint32(c.data[7]&0x3F)<<16 | uint32(c.data[8])<<8 | uint32(c.data[9])
|
||||
}
|
||||
|
||||
// DeviceCapacity returns the total memory capacity of the SDCard in bytes. Max is 2GB for V1.
|
||||
func (c *CSDv1) DeviceCapacity() uint32 {
|
||||
mult := c.mult()
|
||||
csize := c.csize()
|
||||
blklen := c.ReadBlockLen()
|
||||
blockNR := uint32(csize+1) * uint32(mult)
|
||||
return blockNR * uint32(blklen)
|
||||
}
|
||||
|
||||
func (c *CSDv1) csize() uint16 {
|
||||
// Jesus, why did SD make this so complicated?
|
||||
return uint16(c.data[8]>>6) | uint16(c.data[7])<<2 | uint16(c.data[6]&0b11)<<10
|
||||
}
|
||||
|
||||
// mult is a factor for computing total device size with csize and csizemult.
|
||||
func (c *CSDv1) mult() uint16 { return 1 << (2 + c.csizemult()) }
|
||||
|
||||
func (c *CSDv1) csizemult() uint8 {
|
||||
return (c.data[9]&0b11)<<1 | (c.data[10] >> 7)
|
||||
}
|
||||
|
||||
// VddReadCurrent indicates min and max values for read power supply currents.
|
||||
// - values min: 0=0.5mA; 1=1mA; 2=5mA; 3=10mA; 4=25mA; 5=35mA; 6=60mA; 7=100mA
|
||||
// - values max: 0=1mA; 1=5mA; 2=10mA; 3=25mA; 4=35mA; 5=45mA; 6=80mA; 7=200mA
|
||||
func (c *CSDv1) VddReadCurrent() (min, max uint8) {
|
||||
return (c.data[8] >> 3) & 0b111, c.data[8] & 0b111
|
||||
}
|
||||
|
||||
// VddWriteCurrent indicates min and max values for write power supply currents.
|
||||
// - values min: 0=0.5mA; 1=1mA; 2=5mA; 3=10mA; 4=25mA; 5=35mA; 6=60mA; 7=100mA
|
||||
// - values max: 0=1mA; 1=5mA; 2=10mA; 3=25mA; 4=35mA; 5=45mA; 6=80mA; 7=200mA
|
||||
func (c *CSDv1) VddWriteCurrent() (min, max uint8) {
|
||||
return c.data[9] >> 5, (c.data[9] >> 3) & 0b111
|
||||
}
|
||||
|
||||
// String returns a human-readable multi-line summary of the CSD fields.
|
||||
func (c *CSD) String() string {
|
||||
version := c.csdStructure() + 1
|
||||
if version > 2 {
|
||||
return "<unsupported CSD version>"
|
||||
}
|
||||
const delim = '\n'
|
||||
buf := make([]byte, 0, 64)
|
||||
buf = c.appendf(buf, delim)
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
func (c *CSDv1) String() string { return c.CSD.String() }
|
||||
|
||||
func (c *CSDv2) String() string { return c.CSD.String() }
|
||||
|
||||
func (c *CSD) appendf(b []byte, delim byte) []byte {
|
||||
b = appendnum(b, "Version", uint64(c.Version()), delim)
|
||||
b = appendnum(b, "Capacity(bytes)", uint64(c.DeviceCapacity()), delim)
|
||||
b = appendnum(b, "TimeAccess_ns", uint64(c.TAAC().AccessTime()), delim)
|
||||
b = appendnum(b, "NSAC", uint64(c.NSAC()), delim)
|
||||
b = appendnum(b, "Tx_kb/s", uint64(c.TransferSpeed().RateKilobits()), delim)
|
||||
b = appendnum(b, "CCC", uint64(c.CommandClasses()), delim)
|
||||
b = appendnum(b, "ReadBlockLen", uint64(c.ReadBlockLen()), delim)
|
||||
b = appendbit(b, "ReadBlockPartial", c.AllowsReadBlockPartial(), delim)
|
||||
b = appendbit(b, "AllowWriteBlockMisalignment", c.AllowsWriteBlockMisalignment(), delim)
|
||||
b = appendbit(b, "AllowReadBlockMisalignment", c.AllowsReadBlockMisalignment(), delim)
|
||||
b = appendbit(b, "ImplementsDSR", c.ImplementsDSR(), delim)
|
||||
b = appendnum(b, "WProtectNumSectors", uint64(c.WriteProtectGroupSizeInSectors()), delim)
|
||||
b = appendnum(b, "WriteBlockLen", uint64(c.WriteBlockLen()), delim)
|
||||
b = appendbit(b, "WGrpEnable", c.WriteGroupEnabled(), delim)
|
||||
b = appendbit(b, "WPartialAllow", c.AllowsWritePartial(), delim)
|
||||
b = append(b, "FileFmt:"...)
|
||||
b = append(b, c.FileFormat().String()...)
|
||||
b = append(b, delim)
|
||||
b = appendbit(b, "TmpWriteProtect", c.TmpWriteProtected(), delim)
|
||||
b = appendbit(b, "PermWriteProtect", c.PermWriteProtected(), delim)
|
||||
b = appendbit(b, "IsCopy", c.IsCopy(), delim)
|
||||
b = appendbit(b, "FileFormatGrp", c.FileFormatGroup(), delim)
|
||||
return b
|
||||
}
|
||||
|
||||
func appendnum(b []byte, label string, n uint64, delim byte) []byte {
|
||||
b = append(b, label...)
|
||||
b = append(b, ':')
|
||||
b = strconv.AppendUint(b, n, 10)
|
||||
b = append(b, delim)
|
||||
return b
|
||||
}
|
||||
|
||||
func appendbit(b []byte, label string, n bool, delim byte) []byte {
|
||||
b = append(b, label...)
|
||||
b = append(b, ':')
|
||||
b = append(b, '0'+b2u8(n))
|
||||
b = append(b, delim)
|
||||
return b
|
||||
}
|
||||
|
||||
func upToNull(buf []byte) []byte {
|
||||
nullIdx := bytes.IndexByte(buf, 0)
|
||||
if nullIdx < 0 {
|
||||
return buf
|
||||
}
|
||||
return buf[:nullIdx]
|
||||
}
|
||||
|
||||
type (
|
||||
command byte
|
||||
appcommand byte
|
||||
)
|
||||
|
||||
// SD commands and application commands.
|
||||
const (
|
||||
cmdGoIdleState command = 0
|
||||
cmdSendOpCnd command = 1
|
||||
cmdAllSendCID command = 2
|
||||
cmdSendRelativeAddr command = 3
|
||||
cmdSetDSR command = 4
|
||||
cmdSwitchFunc command = 6
|
||||
cmdSelectDeselectCard command = 7
|
||||
cmdSendIfCond command = 8
|
||||
cmdSendCSD command = 9
|
||||
cmdSendCID command = 10
|
||||
cmdStopTransmission command = 12
|
||||
cmdSendStatus command = 13
|
||||
cmdGoInactiveState command = 15
|
||||
cmdSetBlocklen command = 16
|
||||
cmdReadSingleBlock command = 17
|
||||
cmdReadMultipleBlock command = 18
|
||||
cmdWriteBlock command = 24
|
||||
cmdWriteMultipleBlock command = 25
|
||||
cmdProgramCSD command = 27
|
||||
cmdSetWriteProt command = 28
|
||||
cmdClrWriteProt command = 29
|
||||
cmdSendWriteProt command = 30
|
||||
cmdEraseWrBlkStartAddr command = 32
|
||||
cmdEraseWrBlkEndAddr command = 33
|
||||
cmdErase command = 38
|
||||
cmdLockUnlock command = 42
|
||||
cmdAppCmd command = 55
|
||||
cmdGenCmd command = 56
|
||||
cmdReadOCR command = 58
|
||||
cmdCRCOnOff command = 59
|
||||
|
||||
acmdSET_BUS_WIDTH appcommand = 6
|
||||
acmdSD_STATUS appcommand = 13
|
||||
acmdSEND_NUM_WR_BLOCKS appcommand = 22
|
||||
acmdSET_WR_BLK_ERASE_COUNT appcommand = 23
|
||||
acmdSD_APP_OP_COND appcommand = 41
|
||||
acmdSET_CLR_CARD_DETECT appcommand = 42
|
||||
acmdSEND_SCR appcommand = 51
|
||||
acmdSECURE_READ_MULTI_BLOCK appcommand = 18
|
||||
acmdSECURE_WRITE_MULTI_BLOCK appcommand = 25
|
||||
acmdSECURE_WRITE_MKB appcommand = 26
|
||||
acmdSECURE_ERASE appcommand = 38
|
||||
acmdGET_MKB appcommand = 43
|
||||
acmdGET_MID appcommand = 44
|
||||
acmdSET_CER_RN1 appcommand = 45
|
||||
acmdSET_CER_RN2 appcommand = 46
|
||||
acmdSET_CER_RES2 appcommand = 47
|
||||
acmdSET_CER_RES1 appcommand = 48
|
||||
acmdCHANGE_SECURE_AREA appcommand = 49
|
||||
)
|
||||
|
||||
// CSD field types.
|
||||
type (
|
||||
// TransferSpeed is the TRAN_SPEED CSD field: the maximum data transfer
|
||||
// rate encoded as a rate unit (lower 3 bits) and time value multiplier.
|
||||
TransferSpeed uint8
|
||||
// TAAC is the data read access time CSD field, encoded as a time unit
|
||||
// (lower 3 bits) and time value multiplier.
|
||||
TAAC uint8
|
||||
// FileFormat is the format of the data stored on the card. See the
|
||||
// FileFmt* constants for possible values.
|
||||
FileFormat uint8
|
||||
// CommandClasses is the CCC CSD field, a bitfield where bit position i
|
||||
// set means command class i is supported.
|
||||
CommandClasses uint16
|
||||
// NSAC is the data read access time 2 CSD field, given in units of
|
||||
// 100 clock cycles.
|
||||
NSAC uint8
|
||||
)
|
||||
|
||||
const (
|
||||
FileFmtPartition FileFormat = iota // Hard disk like file system with partition table.
|
||||
FileFmtDOSFAT // DOS FAT (floppy like)
|
||||
FileFmtUFF // Universal File Format
|
||||
FileFmtUnknown
|
||||
)
|
||||
|
||||
// String returns a human-readable name for the file format.
|
||||
func (ff FileFormat) String() (s string) {
|
||||
switch ff {
|
||||
case FileFmtPartition:
|
||||
s = "partition"
|
||||
case FileFmtDOSFAT:
|
||||
s = "DOS/FAT"
|
||||
case FileFmtUFF:
|
||||
s = "UFF"
|
||||
case FileFmtUnknown:
|
||||
s = "unknown"
|
||||
default:
|
||||
s = "<invalid format>"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
var log10table = [...]int64{
|
||||
1,
|
||||
10,
|
||||
100,
|
||||
1000,
|
||||
10000,
|
||||
100000,
|
||||
1000000,
|
||||
}
|
||||
|
||||
// RateKilobits returns the transfer rate in kilobits per second.
|
||||
func (t TransferSpeed) RateKilobits() int64 {
|
||||
return 100 * log10table[t&0b111]
|
||||
}
|
||||
|
||||
// AccessTime returns the asynchronous part of the data access time.
|
||||
func (t TAAC) AccessTime() (d time.Duration) {
|
||||
return time.Duration(log10table[t&0b111]) * time.Nanosecond
|
||||
}
|
||||
|
||||
func b2u8(b bool) uint8 {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// CRC16 computes the CRC16 checksum for a given payload using the CRC-16-CCITT polynomial.
|
||||
func CRC16(buf []byte) (crc uint16) {
|
||||
const poly uint16 = 0x1021 // Generator polynomial G(x) = x^16 + x^12 + x^5 + 1
|
||||
for _, b := range buf {
|
||||
crc ^= (uint16(b) << 8) // Shift byte into MSB of crc
|
||||
for i := 0; i < 8; i++ { // Process each bit
|
||||
if crc&0x8000 != 0 {
|
||||
crc = (crc << 1) ^ poly
|
||||
} else {
|
||||
crc <<= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc
|
||||
}
|
||||
|
||||
// CRC7 computes the CRC7 checksum for a given payload using the polynomial x^7 + x^3 + 1.
|
||||
func CRC7(data []byte) (crc uint8) {
|
||||
return crc7noshift(data) >> 1
|
||||
}
|
||||
|
||||
func crc7noshift(data []byte) (crc uint8) {
|
||||
for _, b := range data {
|
||||
crc = crc7_table[crc^b]
|
||||
}
|
||||
return crc
|
||||
}
|
||||
|
||||
var crc7_table = [256]byte{
|
||||
0x00, 0x12, 0x24, 0x36, 0x48, 0x5a, 0x6c, 0x7e,
|
||||
0x90, 0x82, 0xb4, 0xa6, 0xd8, 0xca, 0xfc, 0xee,
|
||||
0x32, 0x20, 0x16, 0x04, 0x7a, 0x68, 0x5e, 0x4c,
|
||||
0xa2, 0xb0, 0x86, 0x94, 0xea, 0xf8, 0xce, 0xdc,
|
||||
0x64, 0x76, 0x40, 0x52, 0x2c, 0x3e, 0x08, 0x1a,
|
||||
0xf4, 0xe6, 0xd0, 0xc2, 0xbc, 0xae, 0x98, 0x8a,
|
||||
0x56, 0x44, 0x72, 0x60, 0x1e, 0x0c, 0x3a, 0x28,
|
||||
0xc6, 0xd4, 0xe2, 0xf0, 0x8e, 0x9c, 0xaa, 0xb8,
|
||||
0xc8, 0xda, 0xec, 0xfe, 0x80, 0x92, 0xa4, 0xb6,
|
||||
0x58, 0x4a, 0x7c, 0x6e, 0x10, 0x02, 0x34, 0x26,
|
||||
0xfa, 0xe8, 0xde, 0xcc, 0xb2, 0xa0, 0x96, 0x84,
|
||||
0x6a, 0x78, 0x4e, 0x5c, 0x22, 0x30, 0x06, 0x14,
|
||||
0xac, 0xbe, 0x88, 0x9a, 0xe4, 0xf6, 0xc0, 0xd2,
|
||||
0x3c, 0x2e, 0x18, 0x0a, 0x74, 0x66, 0x50, 0x42,
|
||||
0x9e, 0x8c, 0xba, 0xa8, 0xd6, 0xc4, 0xf2, 0xe0,
|
||||
0x0e, 0x1c, 0x2a, 0x38, 0x46, 0x54, 0x62, 0x70,
|
||||
0x82, 0x90, 0xa6, 0xb4, 0xca, 0xd8, 0xee, 0xfc,
|
||||
0x12, 0x00, 0x36, 0x24, 0x5a, 0x48, 0x7e, 0x6c,
|
||||
0xb0, 0xa2, 0x94, 0x86, 0xf8, 0xea, 0xdc, 0xce,
|
||||
0x20, 0x32, 0x04, 0x16, 0x68, 0x7a, 0x4c, 0x5e,
|
||||
0xe6, 0xf4, 0xc2, 0xd0, 0xae, 0xbc, 0x8a, 0x98,
|
||||
0x76, 0x64, 0x52, 0x40, 0x3e, 0x2c, 0x1a, 0x08,
|
||||
0xd4, 0xc6, 0xf0, 0xe2, 0x9c, 0x8e, 0xb8, 0xaa,
|
||||
0x44, 0x56, 0x60, 0x72, 0x0c, 0x1e, 0x28, 0x3a,
|
||||
0x4a, 0x58, 0x6e, 0x7c, 0x02, 0x10, 0x26, 0x34,
|
||||
0xda, 0xc8, 0xfe, 0xec, 0x92, 0x80, 0xb6, 0xa4,
|
||||
0x78, 0x6a, 0x5c, 0x4e, 0x30, 0x22, 0x14, 0x06,
|
||||
0xe8, 0xfa, 0xcc, 0xde, 0xa0, 0xb2, 0x84, 0x96,
|
||||
0x2e, 0x3c, 0x0a, 0x18, 0x66, 0x74, 0x42, 0x50,
|
||||
0xbe, 0xac, 0x9a, 0x88, 0xf6, 0xe4, 0xd2, 0xc0,
|
||||
0x1c, 0x0e, 0x38, 0x2a, 0x54, 0x46, 0x70, 0x62,
|
||||
0x8c, 0x9e, 0xa8, 0xba, 0xc4, 0xd6, 0xe0, 0xf2,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Package sd implements SD card drivers and the SD card specification:
|
||||
// CID and CSD register decoding, command definitions and CRC7/CRC16 checksums.
|
||||
//
|
||||
// The [SPICard] type drives an SD card over a SPI bus and implements the
|
||||
// [Card] interface, which exposes block-aligned I/O. [BlockDevice] wraps any
|
||||
// [Card] with byte-addressed [io.ReaderAt]/[io.WriterAt] implementations
|
||||
// suitable for filesystem libraries such as tinyfs.
|
||||
package sd
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
package sd
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
const (
|
||||
_CMD_TIMEOUT = 100
|
||||
|
||||
_R1_IDLE_STATE = 1 << 0
|
||||
_R1_ERASE_RESET = 1 << 1
|
||||
_R1_ILLEGAL_COMMAND = 1 << 2
|
||||
_R1_COM_CRC_ERROR = 1 << 3
|
||||
_R1_ERASE_SEQUENCE_ERROR = 1 << 4
|
||||
_R1_ADDRESS_ERROR = 1 << 5
|
||||
_R1_PARAMETER_ERROR = 1 << 6
|
||||
|
||||
_DATA_RES_MASK = 0x1F
|
||||
_DATA_RES_ACCEPTED = 0x05
|
||||
)
|
||||
|
||||
// response1 is the R1 response token returned by the card in SPI mode
|
||||
// after every command; a bitfield of error and idle-state flags.
|
||||
type response1 uint8
|
||||
|
||||
func (r response1) IsIdle() bool { return r&_R1_IDLE_STATE != 0 }
|
||||
func (r response1) IllegalCmdError() bool { return r&_R1_ILLEGAL_COMMAND != 0 }
|
||||
func (r response1) CRCError() bool { return r&_R1_COM_CRC_ERROR != 0 }
|
||||
func (r response1) EraseReset() bool { return r&_R1_ERASE_RESET != 0 }
|
||||
func (r response1) EraseSeqError() bool { return r&_R1_ERASE_SEQUENCE_ERROR != 0 }
|
||||
func (r response1) AddressError() bool { return r&_R1_ADDRESS_ERROR != 0 }
|
||||
func (r response1) ParamError() bool { return r&_R1_PARAMETER_ERROR != 0 }
|
||||
|
||||
// response1Err wraps a non-zero response1 status as an error.
|
||||
type response1Err struct {
|
||||
context string
|
||||
status response1
|
||||
}
|
||||
|
||||
func (e response1Err) Error() string {
|
||||
if e.context != "" {
|
||||
return "sd:" + e.context + " " + e.status.Response()
|
||||
}
|
||||
return e.status.Response()
|
||||
}
|
||||
|
||||
func (e response1) Response() string {
|
||||
b := make([]byte, 0, 8)
|
||||
return string(e.appendf(b))
|
||||
}
|
||||
|
||||
func (r response1) appendf(b []byte) []byte {
|
||||
b = append(b, '[')
|
||||
if r.IsIdle() {
|
||||
b = append(b, "idle,"...)
|
||||
}
|
||||
if r.EraseReset() {
|
||||
b = append(b, "erase-rst,"...)
|
||||
}
|
||||
if r.EraseSeqError() {
|
||||
b = append(b, "erase-seq,"...)
|
||||
}
|
||||
if r.CRCError() {
|
||||
b = append(b, "crc-err,"...)
|
||||
}
|
||||
if r.AddressError() {
|
||||
b = append(b, "addr-err,"...)
|
||||
}
|
||||
if r.ParamError() {
|
||||
b = append(b, "param-err,"...)
|
||||
}
|
||||
if r.IllegalCmdError() {
|
||||
b = append(b, "illegal-cmd,"...)
|
||||
}
|
||||
if len(b) > 1 {
|
||||
b = b[:len(b)-1]
|
||||
}
|
||||
b = append(b, ']')
|
||||
return b
|
||||
}
|
||||
|
||||
func makeResponseError(status response1) error {
|
||||
return response1Err{
|
||||
status: status,
|
||||
}
|
||||
}
|
||||
|
||||
// Commands used to help generate this file:
|
||||
// - stringer -type=state -trimprefix=state -output=state_string.go
|
||||
// - stringer -type=status -trimprefix=status -output=status_string.go
|
||||
|
||||
// Tokens that are sent by card during polling.
|
||||
// https://github.com/arduino-libraries/SD/blob/master/src/utility/SdInfo.h
|
||||
const (
|
||||
tokSTART_BLOCK = 0xfe
|
||||
tokSTOP_TRAN = 0xfd
|
||||
tokWRITE_MULT = 0xfc
|
||||
)
|
||||
|
||||
// state is the card state machine state as encoded in the
|
||||
// CURRENT_STATE bits of the Card Status register (section 4.10.1).
|
||||
type state uint8
|
||||
|
||||
const (
|
||||
stateIdle state = iota
|
||||
stateReady
|
||||
stateIdent
|
||||
stateStby
|
||||
stateTran
|
||||
stateData
|
||||
stateRcv
|
||||
statePrg
|
||||
stateDis
|
||||
)
|
||||
|
||||
// status represents the Card Status Register (R1), as per section 4.10.1.
|
||||
type status uint32
|
||||
|
||||
func (s status) state() state {
|
||||
return state(s >> 9 & 0xf)
|
||||
}
|
||||
|
||||
// First status bits.
|
||||
const (
|
||||
statusRsvd0 status = iota
|
||||
statusRsvd1
|
||||
statusRsvd2
|
||||
statusAuthSeqError
|
||||
statusRsvdSDIO
|
||||
statusAppCmd
|
||||
statusFXEvent
|
||||
statusRsvd7
|
||||
statusReadyForData
|
||||
)
|
||||
|
||||
// Upper bound status bits.
|
||||
const (
|
||||
statusEraseReset status = iota + 13
|
||||
statusECCDisabled
|
||||
statusWPEraseSkip
|
||||
statusCSDOverwrite
|
||||
_
|
||||
_
|
||||
statusGenericError
|
||||
statusControllerError // internal card controller error
|
||||
statusECCFailed
|
||||
statusIllegalCommand
|
||||
statusComCRCError // CRC check of previous command failed
|
||||
statusLockUnlockFailed
|
||||
statusCardIsLocked // Signals that the card is locked by the host.
|
||||
statusWPViolation // Write protected violation
|
||||
statusEraseParamError // invalid write block selection for erase
|
||||
statusEraseSeqError // error in erase sequence
|
||||
statusBlockLenError // tx block length not allowed
|
||||
statusAddrError // misaligned address
|
||||
statusAddrOutOfRange // address out of range
|
||||
)
|
||||
|
||||
// r1 is the normal 48-bit response to a command in SD-bus mode,
|
||||
// as per section 4.9.1. It carries the 32-bit Card Status register.
|
||||
type r1 struct {
|
||||
data [48 / 8]byte // 48 bits of response.
|
||||
}
|
||||
|
||||
func (r *r1) RawCopy() [6]byte { return r.data }
|
||||
func (r *r1) startbit() bool {
|
||||
return r.data[0]&(1<<7) != 0
|
||||
}
|
||||
func (r *r1) txbit() bool {
|
||||
return r.data[0]&(1<<6) != 0
|
||||
}
|
||||
func (r *r1) cmdidx() uint8 {
|
||||
return r.data[0] & 0b11_1111
|
||||
}
|
||||
func (r *r1) cardstatus() status {
|
||||
return status(binary.BigEndian.Uint32(r.data[1:5]))
|
||||
}
|
||||
func (r *r1) CRC7() uint8 { return r.data[5] >> 1 }
|
||||
func (r *r1) endbit() bool { return r.data[5]&1 != 0 }
|
||||
|
||||
func (r *r1) IsValid() bool {
|
||||
return r.endbit() && CRC7(r.data[:5]) == r.CRC7()
|
||||
}
|
||||
|
||||
// r6 is the 48-bit Published RCA response, as per section 4.9.5. It carries
|
||||
// the card's new Relative Card Address and a subset of the Card Status bits.
|
||||
type r6 struct {
|
||||
data [48 / 8]byte
|
||||
}
|
||||
|
||||
func (r *r6) RawCopy() [6]byte { return r.data }
|
||||
func (r *r6) startbit() bool {
|
||||
return r.data[0]&(1<<7) != 0
|
||||
}
|
||||
func (r *r6) txbit() bool {
|
||||
return r.data[0]&(1<<6) != 0
|
||||
}
|
||||
func (r *r6) cmdidx() uint8 {
|
||||
return r.data[0] & 0b11_1111
|
||||
}
|
||||
func (r *r6) rca() uint16 {
|
||||
return binary.BigEndian.Uint16(r.data[1:3])
|
||||
}
|
||||
func (r *r6) CardStatus() status {
|
||||
moveBit := func(b status, from, to uint) status {
|
||||
return (b & (1 << from)) >> from << to
|
||||
}
|
||||
// See 4.9.5 R6 (Published RCA response) of the SD Simplified Specification.
|
||||
s := status(binary.BigEndian.Uint16(r.data[1:5]))
|
||||
s = moveBit(s, 13, 19)
|
||||
s = moveBit(s, 14, 22)
|
||||
s = moveBit(s, 15, 23)
|
||||
return s
|
||||
}
|
||||
func (r *r6) CRC7() uint8 { return r.data[5] >> 1 }
|
||||
func (r *r6) endbit() bool { return r.data[5]&1 != 0 }
|
||||
|
||||
func (r *r6) IsValid() bool {
|
||||
return r.endbit() && CRC7(r.data[:5]) == r.CRC7()
|
||||
}
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[statusRsvd0-0]
|
||||
_ = x[statusRsvd1-1]
|
||||
_ = x[statusRsvd2-2]
|
||||
_ = x[statusAuthSeqError-3]
|
||||
_ = x[statusRsvdSDIO-4]
|
||||
_ = x[statusAppCmd-5]
|
||||
_ = x[statusFXEvent-6]
|
||||
_ = x[statusRsvd7-7]
|
||||
_ = x[statusReadyForData-8]
|
||||
_ = x[statusEraseReset-13]
|
||||
_ = x[statusECCDisabled-14]
|
||||
_ = x[statusWPEraseSkip-15]
|
||||
_ = x[statusCSDOverwrite-16]
|
||||
_ = x[statusGenericError-19]
|
||||
_ = x[statusControllerError-20]
|
||||
_ = x[statusECCFailed-21]
|
||||
_ = x[statusIllegalCommand-22]
|
||||
_ = x[statusComCRCError-23]
|
||||
_ = x[statusLockUnlockFailed-24]
|
||||
_ = x[statusCardIsLocked-25]
|
||||
_ = x[statusWPViolation-26]
|
||||
_ = x[statusEraseParamError-27]
|
||||
_ = x[statusEraseSeqError-28]
|
||||
_ = x[statusBlockLenError-29]
|
||||
_ = x[statusAddrError-30]
|
||||
_ = x[statusAddrOutOfRange-31]
|
||||
}
|
||||
|
||||
const (
|
||||
_status_name_0 = "Rsvd0Rsvd1Rsvd2AuthSeqErrorRsvdSDIOAppCmdFXEventRsvd7ReadyForData"
|
||||
_status_name_1 = "EraseResetECCDisabledWPEraseSkipCSDOverwrite"
|
||||
_status_name_2 = "GenericErrorControllerErrorECCFailedIllegalCommandComCRCErrorLockUnlockFailedCardIsLockedWPViolationEraseParamErrorEraseSeqErrorBlockLenErrorAddrErrorAddrOutOfRange"
|
||||
)
|
||||
|
||||
var (
|
||||
_status_index_0 = [...]uint8{0, 5, 10, 15, 27, 35, 41, 48, 53, 65}
|
||||
_status_index_1 = [...]uint8{0, 10, 21, 32, 44}
|
||||
_status_index_2 = [...]uint8{0, 12, 27, 36, 50, 61, 77, 89, 100, 115, 128, 141, 150, 164}
|
||||
)
|
||||
|
||||
func (i status) string() string {
|
||||
switch {
|
||||
case i <= 8:
|
||||
return _status_name_0[_status_index_0[i]:_status_index_0[i+1]]
|
||||
case 13 <= i && i <= 16:
|
||||
i -= 13
|
||||
return _status_name_1[_status_index_1[i]:_status_index_1[i+1]]
|
||||
case 19 <= i && i <= 31:
|
||||
i -= 19
|
||||
return _status_name_2[_status_index_2[i]:_status_index_2[i+1]]
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s status) String() string {
|
||||
return string(s.appendf(nil, ','))
|
||||
}
|
||||
|
||||
func (s status) appendf(b []byte, delim byte) []byte {
|
||||
b = append(b, s.state().String()...)
|
||||
b = append(b, '[')
|
||||
if s == 0 {
|
||||
return append(b, ']')
|
||||
}
|
||||
for bit := 0; bit < 32; bit++ {
|
||||
if s&(1<<bit) != 0 {
|
||||
b = append(b, status(bit).string()...)
|
||||
b = append(b, delim)
|
||||
}
|
||||
}
|
||||
b = append(b[:len(b)-1], ']')
|
||||
return b
|
||||
}
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[stateIdle-0]
|
||||
_ = x[stateReady-1]
|
||||
_ = x[stateIdent-2]
|
||||
_ = x[stateStby-3]
|
||||
_ = x[stateTran-4]
|
||||
_ = x[stateData-5]
|
||||
_ = x[stateRcv-6]
|
||||
_ = x[statePrg-7]
|
||||
_ = x[stateDis-8]
|
||||
}
|
||||
|
||||
const _state_name = "IdleReadyIdentStbyTranDataRcvPrgDis"
|
||||
|
||||
var _state_index = [...]uint8{0, 4, 9, 14, 18, 22, 26, 29, 32, 35}
|
||||
|
||||
func (i state) String() string {
|
||||
if i >= state(len(_state_index)-1) {
|
||||
return "<reserved state>"
|
||||
}
|
||||
return _state_name[_state_index[i]:_state_index[i+1]]
|
||||
}
|
||||
+558
@@ -0,0 +1,558 @@
|
||||
package sd
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
)
|
||||
|
||||
var (
|
||||
errBadCSDCID = errors.New("sd:bad CSD/CID in CRC or always1")
|
||||
errNoSDCard = errors.New("sd:no card")
|
||||
errCardNotSupported = errors.New("sd:card not supported")
|
||||
errWaitStartBlock = errors.New("sd:did not find start block token")
|
||||
errNeedBlockLenMultiple = errors.New("sd:need blocksize multiple for I/O")
|
||||
errWrite = errors.New("sd:write")
|
||||
errWriteTimeout = errors.New("sd:write timeout")
|
||||
errReadTimeout = errors.New("sd:read timeout")
|
||||
errBusyTimeout = errors.New("sd:busy card timeout")
|
||||
errOOB = errors.New("sd:oob block access")
|
||||
errNoblocks = errors.New("sd:no readable blocks")
|
||||
)
|
||||
|
||||
// digitalPinout sets the logic level of an output pin; true for high, false for low.
|
||||
type digitalPinout = func(b bool)
|
||||
|
||||
// SPICard is a SPI-mode SD card driver. It implements the [Card] interface
|
||||
// and is initialized with [NewSPICard] followed by a call to [SPICard.Init].
|
||||
// SPICard is not safe for concurrent use.
|
||||
type SPICard struct {
|
||||
bus drivers.SPI
|
||||
cs digitalPinout
|
||||
|
||||
timers [2]timer
|
||||
timeout time.Duration
|
||||
wait time.Duration
|
||||
// Card Identification Register.
|
||||
cid CID
|
||||
// Card Specific Register.
|
||||
csd CSD
|
||||
bufcmd [6]byte
|
||||
kind CardKind
|
||||
// block indexing helper based on block size.
|
||||
blk blkIdxer
|
||||
lastCRC uint16
|
||||
}
|
||||
|
||||
// NewSPICard returns a new [SPICard] that communicates over spi using cs as
|
||||
// the chip select pin. The returned card must be initialized with [SPICard.Init]
|
||||
// before use.
|
||||
func NewSPICard(spi drivers.SPI, cs digitalPinout) *SPICard {
|
||||
const defaultTimeout = 300 * time.Millisecond
|
||||
s := &SPICard{
|
||||
bus: spi,
|
||||
cs: cs,
|
||||
}
|
||||
s.setTimeout(defaultTimeout)
|
||||
return s
|
||||
}
|
||||
|
||||
// setTimeout sets the timeout for all operations and the wait time between each yield during busy spins.
|
||||
func (c *SPICard) setTimeout(timeout time.Duration) {
|
||||
if timeout <= 0 {
|
||||
panic("timeout must be positive")
|
||||
}
|
||||
c.timeout = timeout
|
||||
c.wait = timeout / 512
|
||||
}
|
||||
|
||||
// LastReadCRC returns the CRC for the last ReadBlock operation.
|
||||
func (c *SPICard) LastReadCRC() uint16 { return c.lastCRC }
|
||||
|
||||
// Init initializes the SD card. This routine should be performed with a SPI clock
|
||||
// speed of around 100..400kHz. One may increase the clock speed after initialization.
|
||||
func (d *SPICard) Init() error {
|
||||
return d.initRs()
|
||||
}
|
||||
|
||||
// NumberOfBlocks returns the number of readable and writable blocks on the card,
|
||||
// as calculated from the CSD read during [SPICard.Init].
|
||||
func (d *SPICard) NumberOfBlocks() int64 {
|
||||
return d.csd.NumberOfBlocks()
|
||||
}
|
||||
|
||||
// CID returns a copy of the Card Identification Register value last read.
|
||||
func (d *SPICard) CID() CID { return d.cid }
|
||||
|
||||
// CSD returns a copy of the Card Specific Data Register value last read.
|
||||
func (d *SPICard) CSD() CSD { return d.csd }
|
||||
|
||||
func (d *SPICard) yield() { time.Sleep(d.wait) }
|
||||
|
||||
type timer struct {
|
||||
deadline time.Time
|
||||
}
|
||||
|
||||
func (t *timer) setTimeout(timeout time.Duration) *timer {
|
||||
t.deadline = time.Now().Add(timeout)
|
||||
return t
|
||||
}
|
||||
|
||||
func (t timer) expired() bool {
|
||||
return time.Since(t.deadline) >= 0
|
||||
}
|
||||
|
||||
// Reference for this implementation:
|
||||
// https://github.com/embassy-rs/embedded-sdmmc-rs/blob/master/src/sdmmc.rs
|
||||
|
||||
// Not used currently. We'd want to switch over to one way of doing things, Rust way.
|
||||
func (d *SPICard) initRs() error {
|
||||
// Supply minimum of 74 clock cycles with CS high.
|
||||
d.csEnable(true)
|
||||
for i := 0; i < 10; i++ {
|
||||
d.send(0xff)
|
||||
}
|
||||
d.csEnable(false)
|
||||
for i := 0; i < 512; i++ {
|
||||
d.receive()
|
||||
}
|
||||
d.csEnable(true)
|
||||
defer d.csEnable(false)
|
||||
// Enter SPI mode
|
||||
const maxRetries = 32
|
||||
retries := maxRetries
|
||||
tm := d.timers[0].setTimeout(2 * time.Second)
|
||||
for retries > 0 {
|
||||
stat, err := d.card_command(cmdGoIdleState, 0) // CMD0.
|
||||
if err != nil {
|
||||
if isTimeout(err) {
|
||||
retries--
|
||||
continue // Try again!
|
||||
}
|
||||
return err
|
||||
}
|
||||
if stat == _R1_IDLE_STATE {
|
||||
break
|
||||
} else if tm.expired() {
|
||||
retries = 0
|
||||
break
|
||||
}
|
||||
retries--
|
||||
}
|
||||
if retries <= 0 {
|
||||
return errNoSDCard
|
||||
}
|
||||
const enableCRC = true
|
||||
if enableCRC {
|
||||
stat, err := d.card_command(cmdCRCOnOff, 1) // CMD59.
|
||||
if err != nil {
|
||||
return err
|
||||
} else if stat != _R1_IDLE_STATE {
|
||||
return errors.New("sd:cant enable CRC")
|
||||
}
|
||||
}
|
||||
|
||||
tm.setTimeout(time.Second)
|
||||
for {
|
||||
stat, err := d.card_command(cmdSendIfCond, 0x1AA) // CMD8.
|
||||
if err != nil {
|
||||
return err
|
||||
} else if stat == (_R1_ILLEGAL_COMMAND | _R1_IDLE_STATE) {
|
||||
d.kind = TypeSD1
|
||||
break
|
||||
}
|
||||
d.receive()
|
||||
d.receive()
|
||||
d.receive()
|
||||
status, err := d.receive()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status == 0xaa {
|
||||
d.kind = TypeSD2
|
||||
break
|
||||
}
|
||||
d.yield()
|
||||
}
|
||||
|
||||
var arg uint32
|
||||
if d.kind != TypeSD1 {
|
||||
arg = 0x4000_0000
|
||||
}
|
||||
tm.setTimeout(time.Second)
|
||||
for !tm.expired() {
|
||||
stat, err := d.card_acmd(acmdSD_APP_OP_COND, arg)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if stat == 0 { // READY state.
|
||||
break
|
||||
}
|
||||
d.yield()
|
||||
}
|
||||
err := d.updateCSDCID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.kind != TypeSD2 {
|
||||
return nil // Done if not SD2.
|
||||
}
|
||||
|
||||
// Discover if card is high capacity.
|
||||
stat, err := d.card_command(cmdReadOCR, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if stat != 0 {
|
||||
return makeResponseError(response1(stat))
|
||||
}
|
||||
ocr, err := d.receive()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if ocr&0xc0 == 0xc0 {
|
||||
d.kind = TypeSDHC
|
||||
}
|
||||
// Discard next 3 bytes.
|
||||
d.receive()
|
||||
d.receive()
|
||||
d.receive()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *SPICard) updateCSDCID() (err error) {
|
||||
// read CID
|
||||
d.cid, err = d.read_cid()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.csd, err = d.read_csd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blklen := d.csd.ReadBlockLen()
|
||||
d.blk, err = makeBlockIndexer(int(blklen))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadBlocks reads card data into dst beginning at the block index startBlockIdx.
|
||||
// len(dst) must be a multiple of the card's block size (see [CSD.ReadBlockLen]).
|
||||
// It returns the number of bytes read into dst and any error encountered.
|
||||
func (d *SPICard) ReadBlocks(dst []byte, startBlockIdx int64) (int, error) {
|
||||
numblocks, err := d.checkBounds(startBlockIdx, len(dst))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if d.kind != TypeSDHC {
|
||||
startBlockIdx <<= 9 // Multiply by 512 for non high capacity SD cards.
|
||||
}
|
||||
|
||||
d.csEnable(true)
|
||||
defer d.csEnable(false)
|
||||
|
||||
if numblocks == 1 {
|
||||
return d.read_block_single(dst, startBlockIdx)
|
||||
|
||||
} else if numblocks > 1 {
|
||||
// TODO: implement multi block transaction reading.
|
||||
// Rust code is failing here.
|
||||
blocksize := int(d.blk.size())
|
||||
for i := 0; i < numblocks; i++ {
|
||||
dataoff := i * blocksize
|
||||
d.csEnable(true)
|
||||
_, err := d.read_block_single(dst[dataoff:dataoff+blocksize], int64(i)+startBlockIdx)
|
||||
if err != nil {
|
||||
return dataoff, err
|
||||
}
|
||||
d.csEnable(false)
|
||||
}
|
||||
return len(dst), nil
|
||||
}
|
||||
panic("unreachable numblocks<=0")
|
||||
}
|
||||
|
||||
// EraseBlocks erases numberOfBlocks blocks beginning at startBlock.
|
||||
// It always returns an error since erase is not yet implemented for SPICard.
|
||||
func (d *SPICard) EraseBlocks(startBlock, numberOfBlocks int64) error {
|
||||
return errors.New("sd:erase not implemented")
|
||||
}
|
||||
|
||||
// WriteBlocks writes data to the card beginning at the block index startBlockIdx.
|
||||
// len(data) must be a multiple of the card's block size (see [CSD.WriteBlockLen]).
|
||||
// It returns the number of bytes written and any error encountered.
|
||||
func (d *SPICard) WriteBlocks(data []byte, startBlockIdx int64) (int, error) {
|
||||
numblocks, err := d.checkBounds(startBlockIdx, len(data))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if d.kind != TypeSDHC {
|
||||
startBlockIdx <<= 9 // Multiply by 512 for non high capacity SD cards.
|
||||
}
|
||||
d.csEnable(true)
|
||||
defer d.csEnable(false)
|
||||
|
||||
writeTimeout := 2 * d.timeout
|
||||
if numblocks == 1 {
|
||||
return d.write_block_single(data, startBlockIdx)
|
||||
|
||||
} else if numblocks > 1 {
|
||||
// Start multi block write.
|
||||
blocksize := int(d.blk.size())
|
||||
_, err = d.card_command(cmdWriteMultipleBlock, uint32(startBlockIdx))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
for i := 0; i < numblocks; i++ {
|
||||
offset := i * blocksize
|
||||
err = d.wait_not_busy(writeTimeout)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = d.write_data(tokWRITE_MULT, data[offset:offset+blocksize])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
// Stop the multi write operation.
|
||||
err = d.wait_not_busy(writeTimeout)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = d.send(tokSTOP_TRAN)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
_, err = d.card_command(cmdStopTransmission, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(data), nil
|
||||
}
|
||||
panic("unreachable numblocks<=0")
|
||||
}
|
||||
|
||||
func (d *SPICard) read_block_single(dst []byte, startBlockIdx int64) (int, error) {
|
||||
_, err := d.card_command(cmdReadSingleBlock, uint32(startBlockIdx))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = d.read_data(dst)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(dst), nil
|
||||
}
|
||||
|
||||
func (d *SPICard) write_block_single(data []byte, startBlockIdx int64) (_ int, err error) {
|
||||
_, err = d.card_command(cmdWriteBlock, uint32(startBlockIdx))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = d.write_data(tokSTART_BLOCK, data)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = d.wait_not_busy(2 * d.timeout)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
status, err := d.card_command(cmdSendStatus, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if status != 0 {
|
||||
return 0, makeResponseError(response1(status))
|
||||
}
|
||||
status, err = d.receive()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
} else if status != 0 {
|
||||
return 0, errWrite
|
||||
}
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (d *SPICard) checkBounds(startBlockIdx int64, datalen int) (numblocks int, err error) {
|
||||
if startBlockIdx >= d.NumberOfBlocks() {
|
||||
return 0, errOOB
|
||||
} else if startBlockIdx > math.MaxUint32 {
|
||||
return 0, errCardNotSupported
|
||||
}
|
||||
if d.blk.off(int64(datalen)) > 0 {
|
||||
return 0, errNeedBlockLenMultiple
|
||||
}
|
||||
numblocks = int(d.blk.idx(int64(datalen)))
|
||||
if numblocks == 0 {
|
||||
return 0, io.ErrShortBuffer
|
||||
}
|
||||
return numblocks, nil
|
||||
}
|
||||
|
||||
func (d *SPICard) read_cid() (cid CID, err error) {
|
||||
err = d.cmd_read(cmdSendCID, 0, d.cid.data[:16]) // CMD10.
|
||||
if err != nil {
|
||||
return cid, err
|
||||
}
|
||||
if !d.cid.IsValid() {
|
||||
return cid, errBadCSDCID
|
||||
}
|
||||
return d.cid, nil
|
||||
}
|
||||
|
||||
func (d *SPICard) read_csd() (csd CSD, err error) {
|
||||
err = d.cmd_read(cmdSendCSD, 0, d.csd.data[:16]) // CMD9.
|
||||
if err != nil {
|
||||
return csd, err
|
||||
}
|
||||
if !d.csd.IsValid() {
|
||||
return csd, errBadCSDCID
|
||||
}
|
||||
return d.csd, nil
|
||||
}
|
||||
|
||||
func (d *SPICard) cmd_read(cmd command, args uint32, buf []byte) error {
|
||||
status, err := d.card_command(cmd, args)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != 0 {
|
||||
return makeResponseError(response1(status))
|
||||
}
|
||||
return d.read_data(buf)
|
||||
}
|
||||
|
||||
func (d *SPICard) card_acmd(acmd appcommand, args uint32) (uint8, error) {
|
||||
_, err := d.card_command(cmdAppCmd, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return d.card_command(command(acmd), args)
|
||||
}
|
||||
|
||||
func (d *SPICard) card_command(cmd command, args uint32) (uint8, error) {
|
||||
const transmitterBit = 1 << 6
|
||||
err := d.wait_not_busy(d.timeout)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
buf := d.bufcmd[:6]
|
||||
// Start bit is always zero; transmitter bit is one since we are Host.
|
||||
|
||||
buf[0] = transmitterBit | byte(cmd)
|
||||
binary.BigEndian.PutUint32(buf[1:5], args)
|
||||
buf[5] = crc7noshift(buf[:5]) | 1 // CRC and end bit which is always 1.
|
||||
|
||||
err = d.bus.Tx(buf, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if cmd == cmdStopTransmission {
|
||||
d.receive() // skip stuff byte for stop read.
|
||||
}
|
||||
|
||||
for i := 0; i < 512; i++ {
|
||||
result, err := d.receive()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if result&0x80 == 0 {
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
return 0, errReadTimeout
|
||||
}
|
||||
|
||||
func (d *SPICard) read_data(data []byte) (err error) {
|
||||
var status uint8
|
||||
tm := d.timers[1].setTimeout(d.timeout)
|
||||
for !tm.expired() {
|
||||
status, err = d.receive()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if status != 0xff {
|
||||
break
|
||||
} else if tm.expired() {
|
||||
return errReadTimeout
|
||||
}
|
||||
d.yield()
|
||||
}
|
||||
if status != tokSTART_BLOCK {
|
||||
return errWaitStartBlock
|
||||
}
|
||||
err = d.bus.Tx(nil, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// CRC16 is always sent on a data block.
|
||||
crchi, _ := d.receive()
|
||||
crclo, _ := d.receive()
|
||||
d.lastCRC = uint16(crclo) | uint16(crchi)<<8
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SPICard) wait_not_busy(timeout time.Duration) error {
|
||||
tm := s.timers[1].setTimeout(timeout)
|
||||
for {
|
||||
tok, err := s.receive()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if tok == 0xff {
|
||||
break
|
||||
} else if tm.expired() {
|
||||
return errBusyTimeout
|
||||
}
|
||||
s.yield()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SPICard) write_data(tok byte, data []byte) error {
|
||||
if len(data) > 512 {
|
||||
return errors.New("data too long for write_data")
|
||||
}
|
||||
crc := CRC16(data)
|
||||
err := s.send(tok)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.bus.Tx(data, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.send(byte(crc >> 8))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.send(byte(crc))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
status, err := s.receive()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status&_DATA_RES_MASK != _DATA_RES_ACCEPTED {
|
||||
return makeResponseError(response1(status))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SPICard) receive() (byte, error) {
|
||||
return s.bus.Transfer(0xFF)
|
||||
}
|
||||
|
||||
func (s *SPICard) send(b byte) error {
|
||||
_, err := s.bus.Transfer(b)
|
||||
return err
|
||||
}
|
||||
func (c *SPICard) csEnable(b bool) {
|
||||
// SD Card initialization issues with misbehaving SD cards requires clocking the card.
|
||||
// https://electronics.stackexchange.com/questions/303745/sd-card-initialization-problem-cmd8-wrong-response
|
||||
c.bus.Transfer(0xff)
|
||||
c.cs(!b)
|
||||
c.bus.Transfer(0xff)
|
||||
}
|
||||
@@ -29,6 +29,7 @@ tinygo build -size short -o ./build/test.hex -target=microbit ./examples/easyste
|
||||
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/flash/console/spi
|
||||
tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/flash/console/qspi
|
||||
tinygo build -size short -o ./build/test.hex -target=microbit ./examples/gc9a01/main.go
|
||||
tinygo build -size short -o ./build/test.bin -target=esp32-coreboard-v2 ./examples/gdew0154m09/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/gps/i2c/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/gps/uart/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/hcsr04/main.go
|
||||
@@ -93,6 +94,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
|
||||
@@ -129,6 +131,7 @@ tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/ssd1289/mai
|
||||
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/scd30/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
|
||||
@@ -150,6 +153,7 @@ tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples
|
||||
tinygo build -size short -o ./build/test.hex -target=pico ./examples/ens160/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=pico ./examples/si5351/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=pico ./examples/w5500/main.go
|
||||
tinygo build -size short -o ./build/test.hex -target=pico ./examples/sd
|
||||
# network examples (espat)
|
||||
tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/
|
||||
# network examples (wifinina)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# SX128x Radio
|
||||
Radio from Semtech in the 2.4 GHz band. This driver uses SPI to communicate with the radio instead of the alternative UART interface.
|
||||
|
||||
## Supported Chips
|
||||
- [SX1280](https://www.semtech.com/products/wireless-rf/lora-connect/sx1280)
|
||||
- [SX1281](https://www.semtech.com/products/wireless-rf/lora-connect/sx1281)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package sx128x
|
||||
|
||||
const (
|
||||
// SX128X SPI commands
|
||||
cmdGetStatus = uint8(0xC0)
|
||||
|
||||
// Register Access Operations
|
||||
cmdWriteRegister = uint8(0x18)
|
||||
cmdReadRegister = uint8(0x19)
|
||||
|
||||
// Data Buffer Operations
|
||||
cmdWriteBuffer = uint8(0x1A)
|
||||
cmdReadBuffer = uint8(0x1B)
|
||||
|
||||
// Radio Operation Modes
|
||||
cmdSetSleep = uint8(0x84)
|
||||
cmdSetStandby = uint8(0x80)
|
||||
cmdSetFS = uint8(0xC1)
|
||||
cmdSetTx = uint8(0x83)
|
||||
cmdSetRx = uint8(0x82)
|
||||
cmdSetRxDutyCycle = uint8(0x94)
|
||||
cmdSetLongPreamble = uint8(0x9B)
|
||||
cmdSetCAD = uint8(0xC5)
|
||||
cmdSetTxContinuousWave = uint8(0xD1)
|
||||
cmdSetContinuousPreamble = uint8(0xD2)
|
||||
cmdSetAutoTx = uint8(0x98)
|
||||
cmdSetAutoFS = uint8(0x9E)
|
||||
|
||||
// Radio Configuration
|
||||
cmdSetPacketType = uint8(0x8A)
|
||||
cmdGetPacketType = uint8(0x03)
|
||||
cmdSetRFFrequency = uint8(0x86)
|
||||
cmdSetTxParams = uint8(0x8E)
|
||||
cmdSetCADParams = uint8(0x88)
|
||||
cmdSetBufferBaseAddress = uint8(0x8F)
|
||||
cmdSetModulationParams = uint8(0x8B)
|
||||
cmdSetPacketParams = uint8(0x8C)
|
||||
|
||||
// Communication Status Information
|
||||
cmdGetRxBufferStatus = uint8(0x17)
|
||||
cmdGetPacketStatus = uint8(0x1D)
|
||||
cmdGetRSSIInst = uint8(0x1F)
|
||||
|
||||
// IRQ Handling
|
||||
cmdSetDIOIRQParams = uint8(0x8D)
|
||||
cmdGetIRQStatus = uint8(0x15)
|
||||
cmdClearIRQStatus = uint8(0x97)
|
||||
|
||||
// Miscellaneous
|
||||
cmdSetRegulatorMode = uint8(0x96)
|
||||
cmdSetSaveContext = uint8(0xD5)
|
||||
)
|
||||
@@ -0,0 +1,356 @@
|
||||
package sx128x
|
||||
|
||||
type SleepConfig uint8
|
||||
type StandbyConfig uint8
|
||||
type PeriodBase uint8
|
||||
type PacketType uint8
|
||||
type RadioRampTime uint8
|
||||
type CadSymbolNum uint8
|
||||
|
||||
// GFSK Modulation Params
|
||||
type GFSKBLEBitrateBandwidth uint8
|
||||
type ModulationIndex uint8
|
||||
type ModulationShaping uint8
|
||||
|
||||
// GFSK Packet Params
|
||||
type GFSKPreambleLength uint8
|
||||
type GFSKSyncWordLength uint8
|
||||
type GFSKSyncWordMatch uint8
|
||||
type GFSKHeaderType uint8
|
||||
type GFSKCrcType uint8
|
||||
|
||||
// BLE Packet Params
|
||||
type BLEConnectionState uint8
|
||||
type BLECrcType uint8
|
||||
type BLETestPayload uint8
|
||||
|
||||
// FLRC Modulation Params
|
||||
type FLRCBitrateBandwidth uint8
|
||||
type FLRCCodingRate uint8
|
||||
|
||||
// FLRC Packet Params
|
||||
type FLRCPreambleLength uint8
|
||||
type FLRCSyncWordLength uint8
|
||||
type FLRCSyncWordMatch uint8
|
||||
type FLRCHeaderType uint8
|
||||
type FLRCCrcType uint8
|
||||
|
||||
// LoRa Modulation Params
|
||||
type LoRaSpreadingFactor uint8
|
||||
type LoRaBandwidth uint8
|
||||
type LoRaCodingRate uint8
|
||||
|
||||
// LoRa Packet Params
|
||||
type LoRaHeaderType uint8
|
||||
type LoRaCrcType uint8
|
||||
type LoRaIqType uint8
|
||||
|
||||
// Misc
|
||||
type RegulatorMode uint8
|
||||
type IRQMask = uint16
|
||||
type CircuitMode uint8
|
||||
type CommandStatus uint8
|
||||
|
||||
// Packet Status
|
||||
type GFSKPacketInfo uint8
|
||||
type BLEPacketInfo uint8
|
||||
type FLRCPacketInfo uint8
|
||||
|
||||
const (
|
||||
whiteningDisable = 0x00
|
||||
whiteningEnable = 0x08
|
||||
|
||||
// Circuit Mode
|
||||
circuitModeMask = uint8(0b11100000)
|
||||
CIRCUIT_MODE_STDBY_RC = CircuitMode(0x2)
|
||||
CIRCUIT_MODE_STDBY_XOSC = CircuitMode(0x3)
|
||||
CIRCUIT_MODE_FS = CircuitMode(0x4)
|
||||
CIRCUIT_MODE_RX = CircuitMode(0x5)
|
||||
CIRCUIT_MODE_TX = CircuitMode(0x6)
|
||||
|
||||
// Command Status
|
||||
commandStatusMask = uint8(0b00011100)
|
||||
COMMAND_STATUS_SUCCESS = CommandStatus(0x1)
|
||||
COMMAND_STATUS_DATA_AVAILABLE = CommandStatus(0x2)
|
||||
COMMAND_STATUS_TIMEOUT = CommandStatus(0x3)
|
||||
COMMAND_STATUS_PROCESSING_ERROR = CommandStatus(0x4)
|
||||
COMMAND_STATUS_EXECUTION_ERROR = CommandStatus(0x5)
|
||||
COMMAND_STATUS_TX_DONE = CommandStatus(0x6)
|
||||
|
||||
// SleepConfig
|
||||
SLEEP_DATA_BUFFER_RETAIN = SleepConfig(2)
|
||||
SLEEP_DATA_RAM_RETAIN = SleepConfig(1)
|
||||
|
||||
// StandbyConfig
|
||||
STANDBY_RC = StandbyConfig(0)
|
||||
STANDBY_XOSC = StandbyConfig(1)
|
||||
|
||||
// PeriodBase
|
||||
PERIOD_BASE_15_625_US = PeriodBase(0)
|
||||
PERIOD_BASE_62_5_US = PeriodBase(1)
|
||||
PERIOD_BASE_1_MS = PeriodBase(2)
|
||||
PERIOD_BASE_4_MS = PeriodBase(3)
|
||||
|
||||
// PacketType
|
||||
PACKET_TYPE_GFSK = PacketType(0x00) // default
|
||||
PACKET_TYPE_LORA = PacketType(0x01)
|
||||
PACKET_TYPE_RANGING = PacketType(0x02)
|
||||
PACKET_TYPE_FLRC = PacketType(0x03)
|
||||
PACKET_TYPE_BLE = PacketType(0x04)
|
||||
|
||||
// RampTime
|
||||
RADIO_RAMP_02_US = RadioRampTime(0x00)
|
||||
RADIO_RAMP_04_US = RadioRampTime(0x20)
|
||||
RADIO_RAMP_06_US = RadioRampTime(0x40)
|
||||
RADIO_RAMP_08_US = RadioRampTime(0x60)
|
||||
RADIO_RAMP_10_US = RadioRampTime(0x80)
|
||||
RADIO_RAMP_12_US = RadioRampTime(0xA0)
|
||||
RADIO_RAMP_16_US = RadioRampTime(0xC0)
|
||||
RADIO_RAMP_20_US = RadioRampTime(0xE0)
|
||||
|
||||
// CadSymbolNum
|
||||
LORA_CAD_01_SYMBOL = CadSymbolNum(0x00)
|
||||
LORA_CAD_02_SYMBOLS = CadSymbolNum(0x20)
|
||||
LORA_CAD_04_SYMBOLS = CadSymbolNum(0x40)
|
||||
LORA_CAD_08_SYMBOLS = CadSymbolNum(0x60)
|
||||
LORA_CAD_16_SYMBOLS = CadSymbolNum(0x80)
|
||||
|
||||
// GFSK Modulation Params
|
||||
// Bitrate + Bandwidth - same for BLE
|
||||
GFSK_BLE_BR_2_000_BW_2_4 = GFSKBLEBitrateBandwidth(0x04)
|
||||
GFSK_BLE_BR_1_600_BW_2_4 = GFSKBLEBitrateBandwidth(0x28)
|
||||
GFSK_BLE_BR_1_000_BW_2_4 = GFSKBLEBitrateBandwidth(0x4C)
|
||||
GFSK_BLE_BR_1_000_BW_1_2 = GFSKBLEBitrateBandwidth(0x45)
|
||||
GFSK_BLE_BR_0_800_BW_2_4 = GFSKBLEBitrateBandwidth(0x70)
|
||||
GFSK_BLE_BR_0_800_BW_1_2 = GFSKBLEBitrateBandwidth(0x69)
|
||||
GFSK_BLE_BR_0_500_BW_1_2 = GFSKBLEBitrateBandwidth(0x8D)
|
||||
GFSK_BLE_BR_0_500_BW_0_6 = GFSKBLEBitrateBandwidth(0x86)
|
||||
GFSK_BLE_BR_0_400_BW_1_2 = GFSKBLEBitrateBandwidth(0xB1)
|
||||
GFSK_BLE_BR_0_400_BW_0_6 = GFSKBLEBitrateBandwidth(0xAA)
|
||||
GFSK_BLE_BR_0_250_BW_0_6 = GFSKBLEBitrateBandwidth(0xCE)
|
||||
GFSK_BLE_BR_0_250_BW_0_3 = GFSKBLEBitrateBandwidth(0xC7)
|
||||
GFSK_BLE_BR_0_125_BW_0_3 = GFSKBLEBitrateBandwidth(0xEF)
|
||||
|
||||
// Modulation Index - same for BLE
|
||||
MOD_IND_0_35 = ModulationIndex(0x00)
|
||||
MOD_IND_0_5 = ModulationIndex(0x01)
|
||||
MOD_IND_0_75 = ModulationIndex(0x02)
|
||||
MOD_IND_1_00 = ModulationIndex(0x03)
|
||||
MOD_IND_1_25 = ModulationIndex(0x04)
|
||||
MOD_IND_1_50 = ModulationIndex(0x05)
|
||||
MOD_IND_1_75 = ModulationIndex(0x06)
|
||||
MOD_IND_2_00 = ModulationIndex(0x07)
|
||||
MOD_IND_2_25 = ModulationIndex(0x08)
|
||||
MOD_IND_2_50 = ModulationIndex(0x09)
|
||||
MOD_IND_2_75 = ModulationIndex(0x0A)
|
||||
MOD_IND_3_00 = ModulationIndex(0x0B)
|
||||
MOD_IND_3_25 = ModulationIndex(0x0C)
|
||||
MOD_IND_3_50 = ModulationIndex(0x0D)
|
||||
MOD_IND_3_75 = ModulationIndex(0x0E)
|
||||
MOD_IND_4_00 = ModulationIndex(0x0F)
|
||||
|
||||
// Modulation Shaping - same for BLE and FLRC
|
||||
MOD_SHAPING_OFF = ModulationShaping(0x00)
|
||||
MOD_SHAPING_1_0 = ModulationShaping(0x10)
|
||||
MOD_SHAPING_0_5 = ModulationShaping(0x20)
|
||||
|
||||
// GFSK Packet Params
|
||||
// Preamble Length
|
||||
GFSK_PREAMBLE_LENGTH_04_BITS = GFSKPreambleLength(0x00)
|
||||
GFSK_PREAMBLE_LENGTH_08_BITS = GFSKPreambleLength(0x10)
|
||||
GFSK_PREAMBLE_LENGTH_12_BITS = GFSKPreambleLength(0x20)
|
||||
GFSK_PREAMBLE_LENGTH_16_BITS = GFSKPreambleLength(0x30)
|
||||
GFSK_PREAMBLE_LENGTH_20_BITS = GFSKPreambleLength(0x40)
|
||||
GFSK_PREAMBLE_LENGTH_24_BITS = GFSKPreambleLength(0x50)
|
||||
GFSK_PREAMBLE_LENGTH_28_BITS = GFSKPreambleLength(0x60)
|
||||
GFSK_PREAMBLE_LENGTH_32_BITS = GFSKPreambleLength(0x70)
|
||||
|
||||
// Sync Word Length
|
||||
GFSK_SYNC_WORD_LEN_1_B = GFSKSyncWordLength(0x00)
|
||||
GFSK_SYNC_WORD_LEN_2_B = GFSKSyncWordLength(0x02)
|
||||
GFSK_SYNC_WORD_LEN_3_B = GFSKSyncWordLength(0x04)
|
||||
GFSK_SYNC_WORD_LEN_4_B = GFSKSyncWordLength(0x06)
|
||||
GFSK_SYNC_WORD_LEN_5_B = GFSKSyncWordLength(0x08)
|
||||
|
||||
// Sync Word Match
|
||||
GFSK_SYNCWORD_MATCH_OFF = GFSKSyncWordMatch(0x00)
|
||||
GFSK_SYNCWORD_MATCH_1 = GFSKSyncWordMatch(0x10)
|
||||
GFSK_SYNCWORD_MATCH_2 = GFSKSyncWordMatch(0x20)
|
||||
GFSK_SYNCWORD_MATCH_1_2 = GFSKSyncWordMatch(0x30)
|
||||
GFSK_SYNCWORD_MATCH_3 = GFSKSyncWordMatch(0x40)
|
||||
GFSK_SYNCWORD_MATCH_1_3 = GFSKSyncWordMatch(0x50)
|
||||
GFSK_SYNCWORD_MATCH_2_3 = GFSKSyncWordMatch(0x60)
|
||||
GFSK_SYNCWORD_MATCH_1_2_3 = GFSKSyncWordMatch(0x70)
|
||||
|
||||
// GFSK Header Type
|
||||
GFSK_HEADER_FIXED_LENGTH = GFSKHeaderType(0x00)
|
||||
GFSK_HEADER_VARIABLE_LENGTH = GFSKHeaderType(0x20)
|
||||
|
||||
// GFSK CRC Type
|
||||
GFSK_CRC_OFF = GFSKCrcType(0x00)
|
||||
GFSK_CRC_1_BYTE = GFSKCrcType(0x10)
|
||||
GFSK_CRC_2_BYTES = GFSKCrcType(0x20)
|
||||
|
||||
// BLE Packet Params
|
||||
// Connection State
|
||||
BLE_MASTER_SLAVE = BLEConnectionState(0x00)
|
||||
BLE_ADVERTISER = BLEConnectionState(0x02)
|
||||
BLE_TX_TEST_MODE = BLEConnectionState(0x04)
|
||||
BLE_RX_TEST_MODE = BLEConnectionState(0x06)
|
||||
BLE_RXTX_TEST_MODE = BLEConnectionState(0x08)
|
||||
|
||||
// CRC Type
|
||||
BLE_CRC_OFF = BLECrcType(0x00)
|
||||
BLE_CRC_3_BYTES = BLECrcType(0x10)
|
||||
|
||||
// BLE Test Payload
|
||||
BLE_PAYLOAD_PRBS_9 = BLETestPayload(0x00)
|
||||
BLE_PAYLOAD_EYELONG_1_0 = BLETestPayload(0x04)
|
||||
BLE_PAYLOAD_EYESHORT_1_0 = BLETestPayload(0x08)
|
||||
BLE_PAYLOAD_PRBS_15 = BLETestPayload(0x0C)
|
||||
BLE_PAYLOAD_ALL_1 = BLETestPayload(0x10)
|
||||
BLE_PAYLOAD_ALL_0 = BLETestPayload(0x14)
|
||||
BLE_PAYLOAD_EYELONG_0_1 = BLETestPayload(0x18)
|
||||
BLE_PAYLOAD_EYESHORT_0_1 = BLETestPayload(0x1C)
|
||||
|
||||
// FLRC Modulation Params
|
||||
// Bitrate + Bandwidth
|
||||
FLRC_BR_1_300_BW_1_2 = FLRCBitrateBandwidth(0x45)
|
||||
FLRC_BR_1_000_BW_1_2 = FLRCBitrateBandwidth(0x69)
|
||||
FLRC_BR_0_650_BW_0_6 = FLRCBitrateBandwidth(0x86)
|
||||
FLRC_BR_0_520_BW_0_6 = FLRCBitrateBandwidth(0xAA)
|
||||
FLRC_BR_0_325_BW_0_3 = FLRCBitrateBandwidth(0xC7)
|
||||
FLRC_BR_0_260_BW_0_3 = FLRCBitrateBandwidth(0xEB)
|
||||
|
||||
// Coding Rate
|
||||
FLRC_CR_1_2 = FLRCCodingRate(0x00) // 1/2
|
||||
FLRC_CR_3_4 = FLRCCodingRate(0x02) // 3/4
|
||||
FLRC_CR_1_0 = FLRCCodingRate(0x04) // 1
|
||||
|
||||
// FLRC Packet Params
|
||||
// Preamble Length
|
||||
FLRC_PREAMBLE_LENGTH_4_BITS = FLRCPreambleLength(0x00)
|
||||
FLRC_PREAMBLE_LENGTH_8_BITS = FLRCPreambleLength(0x10)
|
||||
FLRC_PREAMBLE_LENGTH_12_BITS = FLRCPreambleLength(0x20)
|
||||
FLRC_PREAMBLE_LENGTH_16_BITS = FLRCPreambleLength(0x30)
|
||||
FLRC_PREAMBLE_LENGTH_20_BITS = FLRCPreambleLength(0x40)
|
||||
FLRC_PREAMBLE_LENGTH_24_BITS = FLRCPreambleLength(0x50)
|
||||
FLRC_PREAMBLE_LENGTH_28_BITS = FLRCPreambleLength(0x60)
|
||||
FLRC_PREAMBLE_LENGTH_32_BITS = FLRCPreambleLength(0x70)
|
||||
|
||||
// Sync Word Length
|
||||
FLRC_SYNC_WORD_LEN_0 = FLRCSyncWordLength(0x00)
|
||||
FLRC_SYNC_WORD_LEN_32_BITS = FLRCSyncWordLength(0x04)
|
||||
|
||||
// Sync Word Match
|
||||
FLRC_SYNC_WORD_MATCH_DISABLE = FLRCSyncWordMatch(0x00) // Disable Sync Word
|
||||
FLRC_SYNC_WORD_MATCH_1 = FLRCSyncWordMatch(0x10) // Sync Word 1
|
||||
FLRC_SYNC_WORD_MATCH_2 = FLRCSyncWordMatch(0x20) // Sync Word 2
|
||||
FLRC_SYNC_WORD_MATCH_1_2 = FLRCSyncWordMatch(0x30) // Sync Word 1 or Sync Word 2
|
||||
FLRC_SYNC_WORD_MATCH_3 = FLRCSyncWordMatch(0x40) // Sync Word 3
|
||||
FLRC_SYNC_WORD_MATCH_1_3 = FLRCSyncWordMatch(0x50) // Sync Word 1 or Sync Word 3
|
||||
FLRC_SYNC_WORD_MATCH_2_3 = FLRCSyncWordMatch(0x60) // Sync Word 2 or Sync Word 3
|
||||
FLRC_SYNC_WORD_MATCH_1_2_3 = FLRCSyncWordMatch(0x70) // Sync Word 1 or Sync Word 2 or Sync Word 3
|
||||
|
||||
// Header Type
|
||||
FLRC_HEADER_FIXED_LENGTH = FLRCHeaderType(0x00)
|
||||
FLRC_HEADER_VARIABLE_LENGTH = FLRCHeaderType(0x20)
|
||||
|
||||
// CRC Type
|
||||
FLRC_CRC_OFF = FLRCCrcType(0x00)
|
||||
FLRC_CRC_1_BYTE = FLRCCrcType(0x10)
|
||||
FLRC_CRC_2_BYTES = FLRCCrcType(0x20)
|
||||
FLRC_CRC_3_BYTES = FLRCCrcType(0x30)
|
||||
|
||||
// LoRa Modulation Params
|
||||
|
||||
// SpreadingFactor
|
||||
LORA_SF_5 = LoRaSpreadingFactor(0x50)
|
||||
LORA_SF_6 = LoRaSpreadingFactor(0x60)
|
||||
LORA_SF_7 = LoRaSpreadingFactor(0x70)
|
||||
LORA_SF_8 = LoRaSpreadingFactor(0x80)
|
||||
LORA_SF_9 = LoRaSpreadingFactor(0x90)
|
||||
LORA_SF_10 = LoRaSpreadingFactor(0xA0)
|
||||
LORA_SF_11 = LoRaSpreadingFactor(0xB0)
|
||||
LORA_SF_12 = LoRaSpreadingFactor(0xC0)
|
||||
|
||||
// Bandwidth
|
||||
LORA_BW_1600 = LoRaBandwidth(0x0A)
|
||||
LORA_BW_800 = LoRaBandwidth(0x18)
|
||||
LORA_BW_400 = LoRaBandwidth(0x26)
|
||||
LORA_BW_200 = LoRaBandwidth(0x34)
|
||||
|
||||
// CodingRate
|
||||
LORA_CR_4_5 = LoRaCodingRate(0x01)
|
||||
LORA_CR_4_6 = LoRaCodingRate(0x02)
|
||||
LORA_CR_4_7 = LoRaCodingRate(0x03)
|
||||
LORA_CR_4_8 = LoRaCodingRate(0x04)
|
||||
LORA_CR_LI_4_5 = LoRaCodingRate(0x05)
|
||||
LORA_CR_LI_4_6 = LoRaCodingRate(0x06)
|
||||
LORA_CR_LI_4_8 = LoRaCodingRate(0x07)
|
||||
|
||||
// LoraPacketParams
|
||||
// HeaderType
|
||||
LORA_HEADER_EXPLICIT = LoRaHeaderType(0x00)
|
||||
LORA_HEADER_IMPLICIT = LoRaHeaderType(0x80)
|
||||
|
||||
// CRC Type
|
||||
LORA_CRC_ENABLE = LoRaCrcType(0x20)
|
||||
LORA_CRC_DISABLE = LoRaCrcType(0x00)
|
||||
|
||||
// IQ Type
|
||||
LORA_IQ_INVERTED = LoRaIqType(0x00)
|
||||
LORA_IQ_STD = LoRaIqType(0x40)
|
||||
|
||||
// RegulatorMode
|
||||
REGULATOR_LDO = RegulatorMode(0)
|
||||
REGULATOR_DC_DC = RegulatorMode(1)
|
||||
|
||||
// IRQ masks
|
||||
IRQ_ALL_MASK = IRQMask(0xFFFF)
|
||||
IRQ_NONE_MASK = IRQMask(0x0000)
|
||||
IRQ_TX_DONE_MASK = IRQMask(0b0000000000000001)
|
||||
IRQ_RX_DONE_MASK = IRQMask(0b0000000000000010)
|
||||
IRQ_SYNC_WORD_VALID_MASK = IRQMask(0b0000000000000100)
|
||||
IRQ_SYNC_WORD_ERROR_MASK = IRQMask(0b0000000000001000)
|
||||
IRQ_HEADER_VALID_MASK = IRQMask(0b0000000000010000)
|
||||
IRQ_HEADER_ERROR_MASK = IRQMask(0b0000000000100000)
|
||||
IRQ_CRC_ERROR_MASK = IRQMask(0b0000000001000000)
|
||||
IRQ_RANGING_SLAVE_RESPONSE_DONE_MASK = IRQMask(0b0000000010000000)
|
||||
IRQ_RANGING_SLAVE_RESPONSE_DISCARD_MASK = IRQMask(0b0000000100000000)
|
||||
IRQ_RANGING_MASTER_RESULT_VALID_MASK = IRQMask(0b0000001000000000)
|
||||
IRQ_RANGING_MASTER_TIMEOUT_MASK = IRQMask(0b0000010000000000)
|
||||
IRQ_RANGING_SLAVE_REQUEST_VALID_MASK = IRQMask(0b0000100000000000)
|
||||
IRQ_CAD_DONE_MASK = IRQMask(0b0001000000000000)
|
||||
IRQ_CAD_DETECTED_MASK = IRQMask(0b0010000000000000)
|
||||
IRQ_RX_TX_TIMEOUT_MASK = IRQMask(0b0100000000000000)
|
||||
IRQ_PREAMBLE_DETECTED_MASK = IRQMask(0b1000000000000000)
|
||||
IRQ_ADVANCED_RANGING_DONE_MASK = IRQMask(0b1000000000000000)
|
||||
|
||||
// GFSK Packet Info
|
||||
GFSK_SYNC_ERROR = GFSKPacketInfo(0b1000000)
|
||||
GFSK_LENGTH_ERROR = GFSKPacketInfo(0b0100000)
|
||||
GFSK_CRC_ERROR = GFSKPacketInfo(0b0010000)
|
||||
GFSK_ABORT_ERROR = GFSKPacketInfo(0b0001000)
|
||||
GFSK_HEADER_RECEIVED = GFSKPacketInfo(0b0000100)
|
||||
GFSK_PACKET_RECEIVED = GFSKPacketInfo(0b0000010)
|
||||
GFSK_PACKET_CRTL_BUSY = GFSKPacketInfo(0b0000001)
|
||||
|
||||
// BLE Packet Info
|
||||
BLE_SYNC_ERROR = BLEPacketInfo(0b1000000)
|
||||
BLE_LENGTH_ERROR = BLEPacketInfo(0b0100000)
|
||||
BLE_CRC_ERROR = BLEPacketInfo(0b0010000)
|
||||
BLE_ABORT_ERROR = BLEPacketInfo(0b0001000)
|
||||
BLE_HEADER_RECEIVED = BLEPacketInfo(0b0000100)
|
||||
BLE_PACKET_RECEIVED = BLEPacketInfo(0b0000010)
|
||||
BLE_PACKET_CRTL_BUSY = BLEPacketInfo(0b0000001)
|
||||
|
||||
// FLRC Packet Info
|
||||
FLRC_SYNC_ERROR = FLRCPacketInfo(0b1000000)
|
||||
FLRC_LENGTH_ERROR = FLRCPacketInfo(0b0100000)
|
||||
FLRC_CRC_ERROR = FLRCPacketInfo(0b0010000)
|
||||
FLRC_ABORT_ERROR = FLRCPacketInfo(0b0001000)
|
||||
FLRC_HEADER_RECEIVED = FLRCPacketInfo(0b0000100)
|
||||
FLRC_PACKET_RECEIVED = FLRCPacketInfo(0b0000010)
|
||||
FLRC_PACKET_CRTL_BUSY = FLRCPacketInfo(0b0000001)
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
package sx128x
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrBusyPinTimeout = errors.New("busy pin timeout")
|
||||
errDataTooLong = errors.New("data over 256 bytes")
|
||||
errInvalidSleepConfig = errors.New("invalid sleep config")
|
||||
errInvalidStandbyConfig = errors.New("invalid standby config")
|
||||
errFrequencyTooLow = errors.New("frequency below 2.4Ghz")
|
||||
errFrequencyTooHigh = errors.New("frequency above 2.5Ghz")
|
||||
errPowerTooLow = errors.New("power level below -18dBm")
|
||||
errPowerTooHigh = errors.New("power level above 13dBm")
|
||||
errInvalidPeriodBase = errors.New("invalid period base")
|
||||
errInvalidPacketType = errors.New("invalid packet type")
|
||||
errInvalidRegulatorMode = errors.New("invalid regulator mode")
|
||||
errPayloadLengthTooShort = errors.New("payload length too short")
|
||||
errPayloadLengthTooLong = errors.New("payload length too long")
|
||||
)
|
||||
@@ -0,0 +1,69 @@
|
||||
package sx128x
|
||||
|
||||
const (
|
||||
|
||||
// SX128X register map
|
||||
REG_FIRMWARE_VERSIONS = uint16(0x153)
|
||||
REG_RX_GAIN = uint16(0x891)
|
||||
REG_MANUAL_GAIN_SETTING = uint16(0x895)
|
||||
REG_LNA_GAIN_VALUE = uint16(0x89E)
|
||||
REG_LNA_GAIN_CONTROL = uint16(0x89F)
|
||||
REG_SYNCH_PEAK_ATTENUATION = uint16(0x8C2)
|
||||
REG_PAYLOAD_LENGTH = uint16(0x901)
|
||||
REG_LORA_HEADER_MODE = uint16(0x903)
|
||||
REG_RANGING_REQUEST_ADDRESS_BYTE_3 = uint16(0x912)
|
||||
REG_RANGING_REQUEST_ADDRESS_BYTE_2 = uint16(0x913)
|
||||
REG_RANGING_REQUEST_ADDRESS_BYTE_1 = uint16(0x914)
|
||||
REG_RANGING_REQUEST_ADDRESS_BYTE_0 = uint16(0x915)
|
||||
REG_RANGING_DEVICE_ADDRESS_BYTE_3 = uint16(0x916)
|
||||
REG_RANGING_DEVICE_ADDRESS_BYTE_2 = uint16(0x917)
|
||||
REG_RANGING_DEVICE_ADDRESS_BYTE_1 = uint16(0x918)
|
||||
REG_RANGING_DEVICE_ADDRESS_BYTE_0 = uint16(0x919)
|
||||
REG_RANGING_FILTER_WINDOW_SIZE = uint16(0x91E)
|
||||
REG_RESET_RANGING_FILTER = uint16(0x923)
|
||||
REG_RANGING_RESULT_MUX = uint16(0x924)
|
||||
REG_SF_ADDITIONAL_CONFIGURATION = uint16(0x925)
|
||||
REG_RANGING_CALIBRATION_BYTE_2 = uint16(0x92B)
|
||||
REG_RANGING_CALIBRATION_BYTE_1 = uint16(0x92C)
|
||||
REG_RANGING_CALIBRATION_BYTE_0 = uint16(0x92D)
|
||||
REG_RANGING_ID_CHECK_LENGTH = uint16(0x931)
|
||||
REG_FREQUENCY_ERROR_CORRECTION = uint16(0x93C)
|
||||
REG_CAD_DETECT_PEAK = uint16(0x942)
|
||||
REG_LORA_SYNC_WORD_MSB = uint16(0x944)
|
||||
REG_LORA_SYNC_WORD_LSB = uint16(0x945)
|
||||
REG_HEADER_CRC = uint16(0x954)
|
||||
REG_CODING_RATE = uint16(0x950)
|
||||
REG_FEI_BYTE_2 = uint16(0x954)
|
||||
REG_FEI_BYTE_1 = uint16(0x955)
|
||||
REG_FEI_BYTE_0 = uint16(0x956)
|
||||
REG_RANGING_RESULT_BYTE_2 = uint16(0x961)
|
||||
REG_RANGING_RESULT_BYTE_1 = uint16(0x962)
|
||||
REG_RANGING_RESULT_BYTE_0 = uint16(0x963)
|
||||
REG_RANGING_RSSI = uint16(0x964)
|
||||
REG_FREEZE_RANGING_RESULT = uint16(0x97F)
|
||||
REG_PACKET_PREAMBLE_SETTINGS = uint16(0x9C1)
|
||||
REG_WHITENING_INITIAL_VALUE = uint16(0x9C5)
|
||||
REG_CRC_POLYNOMIAL_DEFINITION_MSB = uint16(0x9C6)
|
||||
REG_CRC_POLYNOMIAL_DEFINITION_LSB = uint16(0x9C7)
|
||||
REG_CRC_POLYNOMIAL_SEED_BYTE_2 = uint16(0x9C7)
|
||||
REG_CRC_POLYNOMIAL_SEED_BYTE_1 = uint16(0x9C8)
|
||||
REG_CRC_POLYNOMIAL_SEED_BYTE_0 = uint16(0x9C9)
|
||||
REG_CRC_MSB_INITIAL_VALUE = uint16(0x9C8)
|
||||
REG_CRC_LSB_INITIAL_VALUE = uint16(0x9C9)
|
||||
REG_SYNC_ADDRESS_CONTROL = uint16(0x9CD)
|
||||
REG_SYNC_ADDRESS_1_BYTE_4 = uint16(0x9CE)
|
||||
REG_SYNC_ADDRESS_1_BYTE_3 = uint16(0x9CF)
|
||||
REG_SYNC_ADDRESS_1_BYTE_2 = uint16(0x9D0)
|
||||
REG_SYNC_ADDRESS_1_BYTE_1 = uint16(0x9D1)
|
||||
REG_SYNC_ADDRESS_1_BYTE_0 = uint16(0x9D2)
|
||||
REG_SYNC_ADDRESS_2_BYTE_4 = uint16(0x9D3)
|
||||
REG_SYNC_ADDRESS_2_BYTE_3 = uint16(0x9D4)
|
||||
REG_SYNC_ADDRESS_2_BYTE_2 = uint16(0x9D5)
|
||||
REG_SYNC_ADDRESS_2_BYTE_1 = uint16(0x9D6)
|
||||
REG_SYNC_ADDRESS_2_BYTE_0 = uint16(0x9D7)
|
||||
REG_SYNC_ADDRESS_3_BYTE_4 = uint16(0x9D8)
|
||||
REG_SYNC_ADDRESS_3_BYTE_3 = uint16(0x9D9)
|
||||
REG_SYNC_ADDRESS_3_BYTE_2 = uint16(0x9DA)
|
||||
REG_SYNC_ADDRESS_3_BYTE_1 = uint16(0x9DB)
|
||||
REG_SYNC_ADDRESS_3_BYTE_0 = uint16(0x9DC)
|
||||
)
|
||||
@@ -0,0 +1,768 @@
|
||||
package sx128x
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tinygo.org/x/drivers"
|
||||
"tinygo.org/x/drivers/internal/pin"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
spi drivers.SPI
|
||||
nssPin pin.Output
|
||||
resetPin pin.Output
|
||||
busyPin pin.Input
|
||||
spiTxBuf []byte
|
||||
spiRxBuf []byte
|
||||
}
|
||||
|
||||
func New(spi drivers.SPI, nssPin pin.Output, resetPin pin.Output, busyPin pin.Input) *Device {
|
||||
return &Device{
|
||||
spi: spi,
|
||||
nssPin: nssPin,
|
||||
resetPin: resetPin,
|
||||
busyPin: busyPin,
|
||||
spiTxBuf: make([]byte, 256), // TODO: optimize buffer size
|
||||
spiRxBuf: make([]byte, 256),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) Reset() {
|
||||
d.resetPin.Set(false)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
d.resetPin.Set(true)
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
func (d *Device) WaitWhileBusy(timeout time.Duration) error {
|
||||
// largest busy period is on boot with around ~400ish this should be more than enough
|
||||
now := time.Now()
|
||||
for d.busyPin.Get() {
|
||||
if time.Since(now) > timeout {
|
||||
return ErrBusyPinTimeout
|
||||
}
|
||||
runtime.Gosched()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get tranceiver status, returns circuit mode and command status
|
||||
func (d *Device) GetStatus() (CircuitMode, CommandStatus, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
status, err := d.spi.Transfer(cmdGetStatus)
|
||||
d.nssPin.Set(true)
|
||||
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
circuitMode := (status & circuitModeMask) >> 5
|
||||
commandStatus := (status & commandStatusMask) >> 2
|
||||
return CircuitMode(circuitMode), CommandStatus(commandStatus), nil
|
||||
}
|
||||
|
||||
func (d *Device) WriteRegister(addr uint16, data []byte) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdWriteRegister, uint8((addr>>8)&0xFF), uint8(addr&0xFF))
|
||||
d.spiTxBuf = append(d.spiTxBuf, data...)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Device) ReadRegister(addr uint16) (uint8, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdReadRegister, uint8((addr&0xFF00)>>8), uint8(addr&0x00FF), 0x00, 0x00)
|
||||
d.spiRxBuf = d.spiRxBuf[:5]
|
||||
err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
d.nssPin.Set(true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return d.spiRxBuf[4], nil
|
||||
}
|
||||
|
||||
func (d *Device) WriteBuffer(offset uint8, data []byte) error {
|
||||
if len(data) > 256 {
|
||||
return errDataTooLong
|
||||
}
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdWriteBuffer, offset)
|
||||
d.spiTxBuf = append(d.spiTxBuf, data...)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Read data from the payload buffer starting at the given offset with the given length
|
||||
func (d *Device) ReadBuffer(offset uint8, length uint8) ([]byte, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdReadBuffer, offset, 0x00)
|
||||
for i := uint8(0); i < length; i++ {
|
||||
d.spiTxBuf = append(d.spiTxBuf, 0x00)
|
||||
}
|
||||
d.spiRxBuf = d.spiRxBuf[:len(d.spiTxBuf)]
|
||||
err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
d.nssPin.Set(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return d.spiRxBuf[3:], nil
|
||||
}
|
||||
|
||||
// Set the device into sleep mode with the given configuration: 0 (no retention), 1 (ram retentation), 2 (buffer retention) or 3 (ram and buffer retention)
|
||||
func (d *Device) SetSleep(sleepConfig SleepConfig) error {
|
||||
if sleepConfig > (SLEEP_DATA_BUFFER_RETAIN | SLEEP_DATA_RAM_RETAIN) {
|
||||
return errInvalidSleepConfig
|
||||
}
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetSleep, uint8(sleepConfig))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Put device into standby mode, 0 (RC) or 1 (XOSC)
|
||||
func (d *Device) SetStandby(standbyConfig StandbyConfig) error {
|
||||
if standbyConfig > STANDBY_XOSC { // XOSC is the highest standby config anything higher is invalid
|
||||
return errInvalidStandbyConfig
|
||||
}
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetStandby, uint8(standbyConfig))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the device into Frequency Synthesizer mode
|
||||
func (d *Device) SetFs() error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetFS)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
func checkPeriodBase(periodBase PeriodBase) error {
|
||||
if periodBase > PERIOD_BASE_4_MS { // 4ms is the highest period base anything higher is invalid
|
||||
return errInvalidPeriodBase
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sets the device in transmit mode, the IRQ status should be cleared before using this command
|
||||
// timout is determined by periodBase * periodBaseCount
|
||||
func (d *Device) SetTx(periodBase PeriodBase, periodBaseCount uint16) error {
|
||||
err := checkPeriodBase(periodBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetTx, uint8(periodBase), uint8((periodBaseCount>>8)&0xFF), uint8(periodBaseCount&0xFF))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Sets the device in receive mode, the IRQ status should be cleared before using this command
|
||||
// timeout is determined by periodBase * periodBaseCount
|
||||
func (d *Device) SetRx(periodBase PeriodBase, periodBaseCount uint16) error {
|
||||
err := checkPeriodBase(periodBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetRx, uint8(periodBase), uint8((periodBaseCount>>8)&0xFF), uint8(periodBaseCount&0xFF))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Sets the device in a continuous receive mode, it enters receive mode with a timeout of periodBase * rxPeriodBaseCount.
|
||||
// If no packet is received it will enter sleep mode for periodBase * sleepPeriodBaseCount before re-entering receive mode.
|
||||
// The loop is exited when a packet is received or the device is put into standby mode.
|
||||
func (d *Device) SetRxDutyCycle(periodBase PeriodBase, rxPeriodBaseCount uint16, sleepPeriodBaseCount uint16) error {
|
||||
err := checkPeriodBase(periodBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetRxDutyCycle, uint8(periodBase), uint8((rxPeriodBaseCount&0xFF00)>>8), uint8(rxPeriodBaseCount&0x00FF))
|
||||
d.spiTxBuf = append(d.spiTxBuf, uint8((sleepPeriodBaseCount&0xFF00)>>8), uint8(sleepPeriodBaseCount&0x00FF))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Sets the transceiver into Long Preamble mode, and can only be used with either the LoRa mode and GFSK mode
|
||||
func (d *Device) SetLongPreamble(enable bool) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetLongPreamble)
|
||||
if enable {
|
||||
d.spiTxBuf = append(d.spiTxBuf, 1)
|
||||
} else {
|
||||
d.spiTxBuf = append(d.spiTxBuf, 0)
|
||||
}
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Channel activity detection (CAD) is a LoRa specific mode of operation where the device searches for a LoRa signal.
|
||||
// After search has completed, the device returns to STDBY_RC mode. The length of the search is configured via the SetCadParams() command.
|
||||
func (d *Device) SetCAD() error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetCAD)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Test command to generate a Continuous Wave (RF tone) at a selected frequency and output power
|
||||
// The device remains in Tx Continuous Wave until the host sends a mode configuration command.
|
||||
func (d *Device) SetTxContinuousWave() error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetTxContinuousWave)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Test command to generate an infinite sequence of alternating ‘0’s and ‘1’s in
|
||||
// GFSK modulation and symbol 0 in LoRa. The device remains in transmit until the host sends a mode configuration command.
|
||||
func (d *Device) SetTxContinuousPreamble() error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetContinuousPreamble)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// This command allows the transceiver to send a packet at a user programmable time after the end of a packet reception.
|
||||
// This is useful for Bluetooth Low Energy (BLE) compatibility which requires the transceiver to be able to send back a response 150µs after a packet reception.
|
||||
func (d *Device) SetAutoTx(timeUs uint16) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetAutoTx, uint8((timeUs&0xFF00)>>8), uint8(timeUs&0x00FF))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Modifies the chip behavior so that the state following a Rx or Tx operation is FS and not standby.
|
||||
// This allows for faster transitions between Rx and/or Tx.
|
||||
func (d *Device) SetAutoFs(enable bool) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetAutoFS)
|
||||
if enable {
|
||||
d.spiTxBuf = append(d.spiTxBuf, 1)
|
||||
} else {
|
||||
d.spiTxBuf = append(d.spiTxBuf, 0)
|
||||
}
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Choose between GFSK, LoRa, Ranging, FLRC or BLE packet types, this will affect the available configuration parameters and the structure of the packet
|
||||
func (d *Device) SetPacketType(packetType PacketType) error {
|
||||
if packetType > PACKET_TYPE_BLE { // BLE is the highest packet type anything higher is invalid.
|
||||
return errInvalidPacketType
|
||||
}
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetPacketType, uint8(packetType))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the currently configured packet type, this will be 0 (GFSK), 1 (LoRa), 2 (Ranging), 3 (FLRC) or 4 (BLE)
|
||||
func (d *Device) GetPacketType() (PacketType, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdGetPacketType, 0x00, 0x00)
|
||||
d.spiRxBuf = d.spiRxBuf[:3]
|
||||
err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
d.nssPin.Set(true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return PacketType(d.spiRxBuf[2]), nil
|
||||
}
|
||||
|
||||
// Set the RF frequency in Hz, must be between 2.4 GHz and 2.5 GHz
|
||||
func (d *Device) SetRfFrequency(frequencyHz uint32) error {
|
||||
if frequencyHz < 2400000000 {
|
||||
return errFrequencyTooLow
|
||||
}
|
||||
if frequencyHz > 2500000000 {
|
||||
return errFrequencyTooHigh
|
||||
}
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
rfFrequency := uint32((uint64(frequencyHz) << 18) / 52000000)
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetRFFrequency, uint8((rfFrequency>>16)&0xFF), uint8((rfFrequency>>8)&0xFF), uint8(rfFrequency&0xFF))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the output power in dBm, must be between -18 and 13 dBm, and the ramp time
|
||||
func (d *Device) SetTxParams(powerdBm int8, rampTime RadioRampTime) error {
|
||||
if powerdBm < -18 {
|
||||
return errPowerTooLow
|
||||
}
|
||||
if powerdBm > 13 {
|
||||
return errPowerTooHigh
|
||||
}
|
||||
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
adjustedPower := uint8(powerdBm + 18)
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetTxParams, adjustedPower, uint8(rampTime))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the number of symbols used for channel activity detection which determines the sensitivity of the detection.
|
||||
// This is only applicable in LoRa mode.
|
||||
func (d *Device) SetCadParams(cadSymbolNum uint8) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetCADParams, cadSymbolNum)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the base address for the internal buffer for Tx and Rx operations.
|
||||
// When transmitting or receiving data is read from or written to the buffer starting at the given offset.
|
||||
func (d *Device) SetBufferBaseAddress(txBase uint8, rxBase uint8) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetBufferBaseAddress, txBase, rxBase)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// The arguments to this function depend on the packet type. It is recommended to use the mode specific functions for a better experience.
|
||||
// BLE & GFSK: BitrateBandwidth, ModulationIndex, ModulationShaping
|
||||
// FLRC: BitrateBandwidth, CodingRate, ModulationShaping
|
||||
// LoRa & Ranging: SpreadingFactor, Bandwidth, CodingRate
|
||||
func (d *Device) SetModulationParams(modParam1, modParam2, modParam3 uint8) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetModulationParams, modParam1, modParam2, modParam3)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *Device) SetModulationParamsBLE(bitrateBandwidth GFSKBLEBitrateBandwidth, modulationIndex ModulationIndex, modulationShaping ModulationShaping) error {
|
||||
return d.SetModulationParams(uint8(bitrateBandwidth), uint8(modulationIndex), uint8(modulationShaping))
|
||||
}
|
||||
|
||||
func (d *Device) SetModulationParamsGFSK(bitrateBandwidth GFSKBLEBitrateBandwidth, modulationIndex ModulationIndex, modulationShaping ModulationShaping) error {
|
||||
return d.SetModulationParams(uint8(bitrateBandwidth), uint8(modulationIndex), uint8(modulationShaping))
|
||||
}
|
||||
|
||||
func (d *Device) SetModulationParamsFLRC(bitrateBandwidth FLRCBitrateBandwidth, codingRate FLRCCodingRate, modulationShaping ModulationShaping) error {
|
||||
return d.SetModulationParams(uint8(bitrateBandwidth), uint8(codingRate), uint8(modulationShaping))
|
||||
}
|
||||
|
||||
func (d *Device) SetModulationParamsLoRa(spreadingFactor LoRaSpreadingFactor, bandwidth LoRaBandwidth, codingRate LoRaCodingRate) error {
|
||||
return d.SetModulationParams(uint8(spreadingFactor), uint8(bandwidth), uint8(codingRate))
|
||||
}
|
||||
|
||||
// The arguments to this function depend on the packet type. It is recommended to use the mode specific functions for a better experience.
|
||||
// GFSK & FLRC: PreambleLength, SyncWordLength, SyncWordMatch, HeaderType, PayloadLength, CrcLength, Whitening
|
||||
// BLE: ConnectionState, CrcLength, BleTestPayload, Whitening
|
||||
// LoRa & Ranging: PreambleLength, HeaderType, PayloadLength, CRC, InvertIQ/chirp invert
|
||||
func (d *Device) SetPacketParams(param1, param2, param3, param4, param5, param6, param7 uint8) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetPacketParams, param1, param2, param3, param4, param5, param6, param7)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Set GFSK related packet parameters, this assumes the packet type is already set to GFSK.
|
||||
// - payloadLength: range of 0-255
|
||||
func (d *Device) SetPacketParamsGFSK(preambleLength GFSKPreambleLength, syncWordLength GFSKSyncWordLength, syncWordMatch GFSKSyncWordMatch, headerType GFSKHeaderType, payloadLength uint8, crcLength GFSKCrcType, whitening bool) error {
|
||||
var whiteningVal uint8
|
||||
if whitening {
|
||||
whiteningVal = whiteningEnable
|
||||
} else {
|
||||
whiteningVal = whiteningDisable
|
||||
}
|
||||
return d.SetPacketParams(uint8(preambleLength), uint8(syncWordLength), uint8(syncWordMatch), uint8(headerType), payloadLength, uint8(crcLength), whiteningVal)
|
||||
}
|
||||
|
||||
// Set FLRC related packet parameters, this assumes the packet type is already set to FLRC.
|
||||
// - payloadLength: range of 6-127
|
||||
func (d *Device) SetPacketParamsFLRC(preambleLength FLRCPreambleLength, syncWordLength FLRCSyncWordLength, syncWordMatch FLRCSyncWordMatch, headerType FLRCHeaderType, payloadLength uint8, crcLength FLRCCrcType) error {
|
||||
if payloadLength < 6 {
|
||||
return errPayloadLengthTooShort
|
||||
}
|
||||
if payloadLength > 127 {
|
||||
return errPayloadLengthTooLong
|
||||
}
|
||||
return d.SetPacketParams(uint8(preambleLength), uint8(syncWordLength), uint8(syncWordMatch), uint8(headerType), payloadLength, uint8(crcLength), whiteningDisable)
|
||||
}
|
||||
|
||||
// Set BLE related packet parameters, this assumes the packet type is already set to BLE.
|
||||
func (d *Device) SetPacketParamsBLE(connectionState BLEConnectionState, crcLength BLECrcType, bleTestPayload BLETestPayload, whitening bool) error {
|
||||
var whiteningVal uint8
|
||||
if whitening {
|
||||
whiteningVal = whiteningEnable
|
||||
} else {
|
||||
whiteningVal = whiteningDisable
|
||||
}
|
||||
return d.SetPacketParams(uint8(connectionState), uint8(crcLength), uint8(bleTestPayload), whiteningVal, 0, 0, 0)
|
||||
}
|
||||
|
||||
// Set LoRa related packet parameters, this assumes the packet type is already set to LoRa.
|
||||
// - payloadLength: range of 1-255
|
||||
func (d *Device) SetPacketParamsLoRa(preambleLength uint32, headerType LoRaHeaderType, payloadLength uint8, crcType LoRaCrcType, iqType LoRaIqType) error {
|
||||
if payloadLength == 0 {
|
||||
return errPayloadLengthTooShort
|
||||
}
|
||||
exponent, mantissa := getExponentAndMantissa(preambleLength)
|
||||
return d.SetPacketParams(uint8(exponent<<4)|mantissa, uint8(headerType), payloadLength, uint8(crcType), uint8(iqType), 0, 0)
|
||||
}
|
||||
|
||||
func getExponentAndMantissa(value uint32) (uint8, uint8) {
|
||||
// pulled from RadioLib https://github.com/jgromes/RadioLib/blob/master/src/modules/SX128x/SX128x.cpp
|
||||
e := uint8(1)
|
||||
m := uint8(1)
|
||||
len := uint32(0)
|
||||
for e = uint8(1); e <= 15; e++ {
|
||||
for m = uint8(1); m <= 15; m++ {
|
||||
len = uint32(m) * (uint32(1 << e))
|
||||
if len >= value {
|
||||
break
|
||||
}
|
||||
}
|
||||
if len >= value {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return e, m
|
||||
}
|
||||
|
||||
// Get information about the most recent packet received.
|
||||
// Return the payload length, the offset in the buffer where the payload starts.
|
||||
func (d *Device) GetRxBufferStatus() (uint8, uint8, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdGetRxBufferStatus, 0x00, 0x00, 0x00)
|
||||
d.spiRxBuf = d.spiRxBuf[:4]
|
||||
err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
d.nssPin.Set(true)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return d.spiRxBuf[2], d.spiRxBuf[3], nil
|
||||
}
|
||||
|
||||
// The return type of this function depends on the packet type. Use mode specific function for typed returns.
|
||||
// BLE, GFSK & FLRC: unused, rssiSync, errors, status, sync
|
||||
// LoRa & Ranging: rssiSync, SNR
|
||||
func (d *Device) GetPacketStatus() (uint8, uint8, uint8, uint8, uint8, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdGetPacketStatus, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
|
||||
d.spiRxBuf = d.spiRxBuf[:7]
|
||||
err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
d.nssPin.Set(true)
|
||||
if err != nil {
|
||||
return 0, 0, 0, 0, 0, err
|
||||
}
|
||||
return d.spiRxBuf[2], d.spiRxBuf[3], d.spiRxBuf[4], d.spiRxBuf[5], d.spiRxBuf[6], nil
|
||||
}
|
||||
|
||||
// Get information about the most recent GFSK packet received or transmitted:
|
||||
// - RSSI of last received packet
|
||||
// - packet information (each bit represents a different error or status flag)
|
||||
// - whether the last packet transmission has ended
|
||||
// - the sync word that was used for the last packet reception (0-3)
|
||||
func (d *Device) GetPacketStatusGFSK() (float32, GFSKPacketInfo, bool, uint8, error) {
|
||||
_, rssiSync, packetInfo, status, sync, err := d.GetPacketStatus()
|
||||
if err != nil {
|
||||
return 0, 0, false, 0, err
|
||||
}
|
||||
return float32(int8(rssiSync)) / 2 * -1, GFSKPacketInfo(packetInfo), status != 0, sync, nil
|
||||
}
|
||||
|
||||
// Get information about the most recent BLE packet received or transmitted:
|
||||
// - RSSI of last received packet
|
||||
// - packet information (each bit represents a different error or status flag)
|
||||
// - whether the last packet transmission has ended
|
||||
// - the sync word that was used for the last packet reception (0-1)
|
||||
func (d *Device) GetPacketStatusBLE() (float32, BLEPacketInfo, bool, uint8, error) {
|
||||
_, rssiSync, packetInfo, status, sync, err := d.GetPacketStatus()
|
||||
if err != nil {
|
||||
return 0, 0, false, 0, err
|
||||
}
|
||||
return float32(int8(rssiSync)) / 2 * -1, BLEPacketInfo(packetInfo), status != 0, sync, nil
|
||||
}
|
||||
|
||||
// Get information about the most recent BLE packet received or transmitted:
|
||||
// - RSSI of last received packet
|
||||
// - packet information (each bit represents a different error or status flag)
|
||||
// - PID field of the received packet
|
||||
// - NO_ACK field of the received packet
|
||||
// - PID check status of the current packet
|
||||
// - whether the last packet transmission has ended
|
||||
// - the sync word that was used for the last packet reception (0-1)
|
||||
func (d *Device) GetPacketStatusFLRC() (float32, FLRCPacketInfo, uint8, bool, bool, bool, uint8, error) {
|
||||
_, rawRSSI, packetInfo, rxTxInfo, sync, err := d.GetPacketStatus()
|
||||
|
||||
rxPid := (rxTxInfo & 0b11000000) >> 6
|
||||
noAck := (rxTxInfo & 0b00100000) != 0
|
||||
pidCheck := (rxTxInfo & 0b00010000) != 0
|
||||
txDone := (rxTxInfo & 0b00000001) != 0
|
||||
|
||||
if err != nil {
|
||||
return 0, 0, 0, false, false, false, 0, err
|
||||
}
|
||||
return float32(int8(rawRSSI)) / 2 * -1, FLRCPacketInfo(packetInfo), rxPid, noAck, pidCheck, txDone, sync, nil
|
||||
}
|
||||
|
||||
// Get information about the most recent LoRa packet received:
|
||||
// - RSSI of last received packet
|
||||
// - signal-to-noise ratio (SNR) of last received packet
|
||||
func (d *Device) GetPacketStatusLoRa() (float32, float32, error) {
|
||||
rawRSSI, rawSnr, _, _, _, err := d.GetPacketStatus()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return float32(int8(rawRSSI)) / 2 * -1, float32(int8(rawSnr)) / 4, nil
|
||||
}
|
||||
|
||||
// Get the instantaneous RSSI value during reception of the packet
|
||||
func (d *Device) GetRssiInst() (float32, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdGetRSSIInst, 0x00, 0x00)
|
||||
d.spiRxBuf = d.spiRxBuf[:3]
|
||||
err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
d.nssPin.Set(true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float32(int8(d.spiRxBuf[2])) / 2 * -1, nil
|
||||
}
|
||||
|
||||
// Configure the overall IRQ mask and the mapping of individual IRQs to the DIO1, DIO2 and DIO3 pins
|
||||
func (d *Device) SetDioIrqParams(irqMask IRQMask, dio1Mask IRQMask, dio2Mask IRQMask, dio3Mask IRQMask) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetDIOIRQParams, uint8((irqMask&0xFF00)>>8), uint8(irqMask&0x00FF))
|
||||
d.spiTxBuf = append(d.spiTxBuf, uint8((dio1Mask&0xFF00)>>8), uint8(dio1Mask&0x00FF))
|
||||
d.spiTxBuf = append(d.spiTxBuf, uint8((dio2Mask&0xFF00)>>8), uint8(dio2Mask&0x00FF))
|
||||
d.spiTxBuf = append(d.spiTxBuf, uint8((dio3Mask&0xFF00)>>8), uint8(dio3Mask&0x00FF))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the current IRQ status.
|
||||
func (d *Device) GetIrqStatus() (IRQMask, error) {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdGetIRQStatus, 0x00, 0x00, 0x00)
|
||||
d.spiRxBuf = d.spiRxBuf[:4]
|
||||
err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf)
|
||||
d.nssPin.Set(true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint16(d.spiRxBuf[2])<<8 | uint16(d.spiRxBuf[3]), err
|
||||
}
|
||||
|
||||
// Clear the IRQ bits specified in the irqMask.
|
||||
func (d *Device) ClearIrqStatus(irqMask IRQMask) error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdClearIRQStatus, uint8((irqMask&0xFF00)>>8), uint8(irqMask&0x00FF))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Switch between the low-dropout regulator (LDO) and the DC-DC converter for internal power regulation.
|
||||
func (d *Device) SetRegulatorMode(mode RegulatorMode) error {
|
||||
if mode > REGULATOR_DC_DC { // DC-DC is the highest regulator mode anything higher is invalid
|
||||
return errInvalidRegulatorMode
|
||||
}
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetRegulatorMode, uint8(mode))
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
|
||||
// Stores the present context of the radio register values to the Data RAM which will be restored when the device wakes up from sleep mode.
|
||||
func (d *Device) SetSaveContext() error {
|
||||
err := d.WaitWhileBusy(time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.nssPin.Set(false)
|
||||
d.spiTxBuf = d.spiTxBuf[:0]
|
||||
d.spiTxBuf = append(d.spiTxBuf, cmdSetSaveContext)
|
||||
err = d.spi.Tx(d.spiTxBuf, nil)
|
||||
d.nssPin.Set(true)
|
||||
return err
|
||||
}
|
||||
+9
-4
@@ -412,21 +412,26 @@ func (d *Device) Invert(invert bool) {
|
||||
func (d *Device) SetLUT(speed Speed, flickerFree bool) error {
|
||||
var lut LUTSet
|
||||
|
||||
if speed == DEFAULT {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Num. of frames for single direction change.
|
||||
period := 64
|
||||
p := uint8(period / (2 ^ (int(speed) - 1)))
|
||||
scale := 1 << (int(speed) - 1)
|
||||
p := uint8(period / scale)
|
||||
if p < 1 {
|
||||
p = 1
|
||||
}
|
||||
|
||||
// Num. of frames for back-and-forth change.
|
||||
hperiod := period % 2
|
||||
hp := uint8(hperiod / (2 ^ (int(speed) - 1)))
|
||||
hperiod := period / 2
|
||||
hp := uint8(hperiod / scale)
|
||||
if hp < 1 {
|
||||
hp = 1
|
||||
}
|
||||
|
||||
if speed < FAST && !flickerFree {
|
||||
if speed <= FAST && !flickerFree {
|
||||
// For low speed everything is charge-neutral, even WB/BW.
|
||||
|
||||
// Phase 1: long go-inverted-color.
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@ package drivers
|
||||
|
||||
// Version returns a user-readable string showing the version of the drivers package for support purposes.
|
||||
// Update this value before release of new version of software.
|
||||
const Version = "0.35.0"
|
||||
const Version = "0.36.0"
|
||||
|
||||
+49
-5
@@ -17,7 +17,8 @@ import (
|
||||
// the new assembly implementation - no fiddly timings to calculate and no nops
|
||||
// to count!
|
||||
//
|
||||
// Right now this is specific to Cortex-M chips and assume the following things:
|
||||
// Right now this is specific to specific chips:
|
||||
// On Cortex-M chips it assume the following things:
|
||||
// - Arithmetic operations (shift, add, sub) take up 1 clock cycle.
|
||||
// - The nop instruction also takes up 1 clock cycle.
|
||||
// - Store instructions (to the GPIO pins) take up 2 clock cycles.
|
||||
@@ -25,8 +26,15 @@ import (
|
||||
// depends on whether the branch is taken or not. On the M4, the documentation
|
||||
// is less clear but it appears the instruction is still 1 to 3 cycles
|
||||
// (possibly including some branch prediction).
|
||||
// It is certainly possible to extend this to other architectures, such as AVR
|
||||
// and RISC-V if needed.
|
||||
// On RISC-V chips it assumes the following things:
|
||||
// - Arithmetic operations (shift, add, sub) take up 1 clock cycle.
|
||||
// - The nop instruction also takes up 1 clock cycle.
|
||||
// - Store instructions (to the GPIO pins) take up 1 clock cycle.
|
||||
// - Branch instructions can take up 1 or 3 clock cycles, depending on branch
|
||||
// prediction. This is based on the SiFive FE310 CPU, but hopefully it
|
||||
// generalizes to other RISC-V chips as well.
|
||||
|
||||
// It is certainly possible to extend this to other architectures, such as AVR as needed.
|
||||
//
|
||||
// Here are two important resources. For the timings:
|
||||
// https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/
|
||||
@@ -45,6 +53,7 @@ type architectureImpl struct {
|
||||
maxBaseCyclesT1H int
|
||||
minBaseCyclesTLD int
|
||||
valueTemplate string // template for how to pass the 'c' byte to assembly
|
||||
funcAttr string // C function attribute (default: always_inline)
|
||||
template string // assembly template
|
||||
}
|
||||
|
||||
@@ -83,7 +92,7 @@ var architectures = map[string]architectureImpl{
|
||||
// - branches are 1 or 3 cycles, depending on branch prediction
|
||||
// - ALU operations are 1 cycle (as on most CPUs)
|
||||
// Hopefully this generalizes to other chips.
|
||||
buildTag: "tinygo.riscv32",
|
||||
buildTag: "tinygo.riscv32 && !esp32c3",
|
||||
minBaseCyclesT0H: 1 + 1 + 1, // shift + branch (not taken) + store
|
||||
maxBaseCyclesT0H: 1 + 3 + 1, // shift + branch (not taken) + store
|
||||
minBaseCyclesT1H: 1 + 1 + 1, // shift + branch (taken) + store
|
||||
@@ -103,6 +112,37 @@ var architectures = map[string]architectureImpl{
|
||||
@DELAY3
|
||||
addi %[i], %[i], -1 // [1]
|
||||
bnez %[i], 1b // [1/3] send_bit
|
||||
`,
|
||||
},
|
||||
"esp32c3": {
|
||||
// ESP32-C3 RISC-V core:
|
||||
// - stores are 1 cycle
|
||||
// - branches are 1 or 3 cycles
|
||||
// - ALU operations are 1 cycle
|
||||
// Uses the same instruction timing as the SiFive FE310, but the
|
||||
// function is placed in IRAM instead of flash to avoid instruction
|
||||
// cache miss stalls that would destroy WS2812 timing.
|
||||
buildTag: "esp32c3",
|
||||
minBaseCyclesT0H: 1 + 1 + 1, // shift + branch (not taken) + store
|
||||
maxBaseCyclesT0H: 1 + 3 + 1, // shift + branch (not taken) + store
|
||||
minBaseCyclesT1H: 1 + 1 + 1, // shift + branch (taken) + store
|
||||
maxBaseCyclesT1H: 1 + 3 + 1, // shift + branch (taken) + store
|
||||
minBaseCyclesTLD: 1 + 1 + 1, // subtraction + branch + store (in next cycle)
|
||||
valueTemplate: "(uint32_t)c << 23",
|
||||
funcAttr: `__attribute__((section(".iram1"), noinline))`,
|
||||
template: `
|
||||
1: // send_bit
|
||||
sw %[maskSet], %[portSet] // [1] T0H and T0L start here
|
||||
@DELAY1
|
||||
slli %[value], %[value], 1 // [1] shift value left by 1
|
||||
bltz %[value], 2f // [1/3] skip_store
|
||||
sw %[maskClear], %[portClear] // [1] T0H -> T0L transition
|
||||
2: // skip_store
|
||||
@DELAY2
|
||||
sw %[maskClear], %[portClear] // [1] T1H -> T1L transition
|
||||
@DELAY3
|
||||
addi %[i], %[i], -1 // [1]
|
||||
bnez %[i], 1b // [1/3] send_bit
|
||||
`,
|
||||
},
|
||||
}
|
||||
@@ -208,7 +248,11 @@ func writeCAssembly(f *os.File, arch string, megahertz int) error {
|
||||
// ignore I/O errors.
|
||||
buf := &bytes.Buffer{}
|
||||
fmt.Fprintf(buf, "\n")
|
||||
fmt.Fprintf(buf, "__attribute__((always_inline))\nvoid ws2812_writeByte%d(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {\n", megahertz)
|
||||
funcAttr := archImpl.funcAttr
|
||||
if funcAttr == "" {
|
||||
funcAttr = "__attribute__((always_inline))"
|
||||
}
|
||||
fmt.Fprintf(buf, "%s\nvoid ws2812_writeByte%d(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {\n", funcAttr, megahertz)
|
||||
fmt.Fprintf(buf, " // Timings:\n")
|
||||
fmt.Fprintf(buf, " // T0H: %2d - %2d cycles or %.1fns - %.1fns\n", actualMinCyclesT0H, actualMaxCyclesT0H, actualMinNanosecondsT0H, actualMaxNanosecondsT0H)
|
||||
fmt.Fprintf(buf, " // T1H: %2d - %2d cycles or %.1fns - %.1fns\n", actualMinCyclesT1H, actualMaxCyclesT1H, actualMinNanosecondsT1H, actualMaxNanosecondsT1H)
|
||||
|
||||
@@ -1281,6 +1281,377 @@ void ws2812_writeByte150(char c, uint32_t *portSet, uint32_t *portClear, uint32_
|
||||
[portClear]"m"(*portClear));
|
||||
}
|
||||
|
||||
__attribute__((always_inline))
|
||||
void ws2812_writeByte160(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {
|
||||
// Timings:
|
||||
// T0H: 56 - 58 cycles or 350.0ns - 362.5ns
|
||||
// T1H: 168 - 170 cycles or 1050.0ns - 1062.5ns
|
||||
// TLD: 184 - cycles or 1150.0ns -
|
||||
uint32_t value = (uint32_t)c << 24;
|
||||
char i = 8;
|
||||
__asm__ __volatile__(
|
||||
"1: @ send_bit\n"
|
||||
"\t str %[maskSet], %[portSet] @ [2] T0H and T0L start here\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t lsls %[value], #1 @ [1]\n"
|
||||
"\t bcs.n 2f @ [1/3] skip_store\n"
|
||||
"\t str %[maskClear], %[portClear] @ [2] T0H -> T0L transition\n"
|
||||
"\t2: @ skip_store\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t str %[maskClear], %[portClear] @ [2] T1H -> T1L transition\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t subs %[i], #1 @ [1]\n"
|
||||
"\t beq.n 3f @ [1/3] end\n"
|
||||
"\t b 1b @ [1/3] send_bit\n"
|
||||
"\t3: @ end\n"
|
||||
: [value]"+r"(value),
|
||||
[i]"+r"(i)
|
||||
: [maskSet]"r"(maskSet),
|
||||
[portSet]"m"(*portSet),
|
||||
[maskClear]"r"(maskClear),
|
||||
[portClear]"m"(*portClear));
|
||||
}
|
||||
|
||||
__attribute__((always_inline))
|
||||
void ws2812_writeByte168(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {
|
||||
// Timings:
|
||||
@@ -2192,6 +2563,16 @@ func (d Device) writeByte150(c byte) {
|
||||
interrupt.Restore(mask)
|
||||
}
|
||||
|
||||
func (d Device) writeByte160(c byte) {
|
||||
portSet, maskSet := d.Pin.PortMaskSet()
|
||||
portClear, maskClear := d.Pin.PortMaskClear()
|
||||
|
||||
mask := interrupt.Disable()
|
||||
C.ws2812_writeByte160(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
|
||||
|
||||
interrupt.Restore(mask)
|
||||
}
|
||||
|
||||
func (d Device) writeByte168(c byte) {
|
||||
portSet, maskSet := d.Pin.PortMaskSet()
|
||||
portClear, maskClear := d.Pin.PortMaskClear()
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
//go:build esp32c3
|
||||
|
||||
package ws2812
|
||||
|
||||
// Warning: autogenerated file. Instead of modifying this file, change
|
||||
// gen-ws2812.go and run "go generate".
|
||||
|
||||
import "runtime/interrupt"
|
||||
import "unsafe"
|
||||
|
||||
/*
|
||||
#include <stdint.h>
|
||||
|
||||
__attribute__((section(".iram1"), noinline))
|
||||
void ws2812_writeByte160(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {
|
||||
// Timings:
|
||||
// T0H: 56 - 58 cycles or 350.0ns - 362.5ns
|
||||
// T1H: 168 - 170 cycles or 1050.0ns - 1062.5ns
|
||||
// TLD: 184 - cycles or 1150.0ns -
|
||||
uint32_t value = (uint32_t)c << 23;
|
||||
char i = 8;
|
||||
__asm__ __volatile__(
|
||||
"1: // send_bit\n"
|
||||
"\t sw %[maskSet], %[portSet] // [1] T0H and T0L start here\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t slli %[value], %[value], 1 // [1] shift value left by 1\n"
|
||||
"\t bltz %[value], 2f // [1/3] skip_store\n"
|
||||
"\t sw %[maskClear], %[portClear] // [1] T0H -> T0L transition\n"
|
||||
"\t2: // skip_store\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t sw %[maskClear], %[portClear] // [1] T1H -> T1L transition\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t addi %[i], %[i], -1 // [1]\n"
|
||||
"\t bnez %[i], 1b // [1/3] send_bit\n"
|
||||
: [value]"+r"(value),
|
||||
[i]"+r"(i)
|
||||
: [maskSet]"r"(maskSet),
|
||||
[portSet]"m"(*portSet),
|
||||
[maskClear]"r"(maskClear),
|
||||
[portClear]"m"(*portClear));
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func (d Device) writeByte160(c byte) {
|
||||
portSet, maskSet := d.Pin.PortMaskSet()
|
||||
portClear, maskClear := d.Pin.PortMaskClear()
|
||||
|
||||
mask := interrupt.Disable()
|
||||
C.ws2812_writeByte160(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
|
||||
|
||||
interrupt.Restore(mask)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build tinygo.riscv32
|
||||
//go:build tinygo.riscv32 && !esp32c3
|
||||
|
||||
package ws2812
|
||||
|
||||
@@ -11,378 +11,6 @@ import "unsafe"
|
||||
/*
|
||||
#include <stdint.h>
|
||||
|
||||
__attribute__((always_inline))
|
||||
void ws2812_writeByte160(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {
|
||||
// Timings:
|
||||
// T0H: 56 - 58 cycles or 350.0ns - 362.5ns
|
||||
// T1H: 168 - 170 cycles or 1050.0ns - 1062.5ns
|
||||
// TLD: 184 - cycles or 1150.0ns -
|
||||
uint32_t value = (uint32_t)c << 23;
|
||||
char i = 8;
|
||||
__asm__ __volatile__(
|
||||
"1: // send_bit\n"
|
||||
"\t sw %[maskSet], %[portSet] // [1] T0H and T0L start here\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t slli %[value], %[value], 1 // [1] shift value left by 1\n"
|
||||
"\t bltz %[value], 2f // [1/3] skip_store\n"
|
||||
"\t sw %[maskClear], %[portClear] // [1] T0H -> T0L transition\n"
|
||||
"\t2: // skip_store\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t sw %[maskClear], %[portClear] // [1] T1H -> T1L transition\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t nop\n"
|
||||
"\t addi %[i], %[i], -1 // [1]\n"
|
||||
"\t bnez %[i], 1b // [1/3] send_bit\n"
|
||||
: [value]"+r"(value),
|
||||
[i]"+r"(i)
|
||||
: [maskSet]"r"(maskSet),
|
||||
[portSet]"m"(*portSet),
|
||||
[maskClear]"r"(maskClear),
|
||||
[portClear]"m"(*portClear));
|
||||
}
|
||||
|
||||
__attribute__((always_inline))
|
||||
void ws2812_writeByte320(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {
|
||||
// Timings:
|
||||
@@ -1109,16 +737,6 @@ void ws2812_writeByte320(char c, uint32_t *portSet, uint32_t *portClear, uint32_
|
||||
*/
|
||||
import "C"
|
||||
|
||||
func (d Device) writeByte160(c byte) {
|
||||
portSet, maskSet := d.Pin.PortMaskSet()
|
||||
portClear, maskClear := d.Pin.PortMaskClear()
|
||||
|
||||
mask := interrupt.Disable()
|
||||
C.ws2812_writeByte160(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear))
|
||||
|
||||
interrupt.Restore(mask)
|
||||
}
|
||||
|
||||
func (d Device) writeByte320(c byte) {
|
||||
portSet, maskSet := d.Pin.PortMaskSet()
|
||||
portClear, maskClear := d.Pin.PortMaskClear()
|
||||
|
||||
+3
-2
@@ -4,8 +4,9 @@
|
||||
// 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"
|
||||
|
||||
@@ -34,6 +34,9 @@ func (d Device) WriteByte(c byte) error {
|
||||
case 150_000_000: // 150MHz, e.g. rp2350
|
||||
d.writeByte150(c)
|
||||
return nil
|
||||
case 160_000_000: // 160MHz, e.g. stm32u585
|
||||
d.writeByte160(c)
|
||||
return nil
|
||||
case 168_000_000: // 168MHz, e.g. stm32f405
|
||||
d.writeByte168(c)
|
||||
return nil
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
//go:build esp32c3
|
||||
|
||||
package ws2812
|
||||
|
||||
import "machine"
|
||||
|
||||
// Send a single byte using the WS2812 protocol.
|
||||
func (d Device) WriteByte(c byte) error {
|
||||
switch machine.CPUFrequency() {
|
||||
case 160_000_000: // 160MHz
|
||||
d.writeByte160(c)
|
||||
return nil
|
||||
default:
|
||||
return errUnknownClockSpeed
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ package ws2812
|
||||
import (
|
||||
"image/color"
|
||||
"machine"
|
||||
"runtime"
|
||||
|
||||
pio "github.com/tinygo-org/pio/rp2-pio"
|
||||
"github.com/tinygo-org/pio/rp2-pio/piolib"
|
||||
@@ -27,6 +28,9 @@ func newWS2812Device(pin machine.Pin) Device {
|
||||
writeColorFunc: func(_ Device, buf []color.RGBA, brightness uint8) error {
|
||||
for _, c := range buf {
|
||||
r, g, b := applyBrightness(c, brightness)
|
||||
for ws.IsQueueFull() {
|
||||
runtime.Gosched()
|
||||
}
|
||||
ws.PutRGB(r, g, b)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build tinygo.riscv32
|
||||
//go:build tinygo.riscv32 && !esp32c3
|
||||
|
||||
package ws2812
|
||||
|
||||
@@ -7,9 +7,6 @@ import "machine"
|
||||
// Send a single byte using the WS2812 protocol.
|
||||
func (d Device) WriteByte(c byte) error {
|
||||
switch machine.CPUFrequency() {
|
||||
case 160_000_000: // 160MHz, e.g. esp32c3
|
||||
d.writeByte160(c)
|
||||
return nil
|
||||
case 320_000_000: // 320MHz, e.g. fe310
|
||||
d.writeByte320(c)
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user