Compare commits

...

16 Commits

Author SHA1 Message Date
deadprogram 85e123eeb2 st7789: work in progress on display refactor
Signed-off-by: deadprogram <ron@hybridgroup.com>
2025-12-20 10:29:55 +01:00
deadprogram 51561d4eab st7789: add Bus interface to accomodate both SPI and parallel connections to display
Signed-off-by: deadprogram <ron@hybridgroup.com>
2025-12-20 10:29:55 +01:00
deadprogram b561dc36f2 examples: switch st7789 example to use the Adafruit Clue since that device actually comes with one.
Signed-off-by: deadprogram <ron@hybridgroup.com>
2025-12-20 10:29:55 +01:00
deadprogram 85b24b3418 st7789: remove dependency on the machine package by using internal/pin instead
Signed-off-by: deadprogram <ron@hybridgroup.com>
2025-12-20 10:29:55 +01:00
deadprogram 5d96a56603 build: use the latest TinyGo release container instead of the dev container for builds.
Signed-off-by: deadprogram <ron@hybridgroup.com>
2025-12-17 10:28:24 +00:00
deadprogram f931ad44fb Release 0.34
Signed-off-by: deadprogram <ron@hybridgroup.com>
2025-12-15 15:13:56 +00:00
deadprogram 50778af656 si5351: add many missing functions needed for convenient use.
This adds many missing functions needed for convenient use that are
reinterpreted from the C code in https://github.com/dmalnati/picoinf

Thank you!

Signed-off-by: deadprogram <ron@hybridgroup.com>
2025-12-11 17:59:07 +00:00
Mike Hughes 936a255df9 Add support for CEVA BNO08x 9DoF sensor (#809)
* Add support for CEVA BNO08x 9DoF sensor. Also includes implementation of CEVA SH-2 and SHTP protocols.

* Replace machine.I2C with drivers.I2C interface to remove dependency on machine package

* Replace Pin functionality with that provided by tinygo.org/x/drivers/internal/pin

* Unexport fields on SensorValue and replace with accessor methods. Add check for correct SensorID validation

* Add example to smoketest.sh

* Change build target for smoketest to match development environment.. Probably not important, but matches reality.

* Fix decoding of some sensor data: Step Counter, Tap Detector, Flip Detector. These are experimental.

* Refactor to allow SPI/UART etc. SPI is currently under development, but is omitted from this commit.

Example code has been moved to i2c subdirectory.

This commit introduces some major refactoring changes. It introduces a "Buser" interface and tries to remove any I2C specific code from the core. It still retains a couple of I2C specific fields in the "Config" struct ("Address" and "ReadChunk") but they are ignored in the as yet uncommited SPI code.

* Fix CRLF -> LF for gofmt

* Update smoketest to point to new example file
2025-12-10 19:07:04 +01:00
Ayke van Laethem 9ed648f4a5 scd4x: add support for SCD41 single-shot measurements
I have a SCD41, and would like to use it for single shot measurements
instead of "low power periodic" measurements to achieve much lower
average current consumption.
2025-12-04 11:25:41 +00:00
Ayke van Laethem fab688a701 scd4x: remove dead code 2025-12-01 08:56:40 +00:00
Ayke van Laethem d4654668f2 scd4x: update package to use standard methods
This deprecates the older Read* methods and uses standard Update() and
sensor data getters instead.

Apart from being more efficient, this also makes the driver more
efficient when reading multiple sensors (since the SCD4x sensor won't
get polled for new data at each Read* method call).
2025-12-01 08:56:40 +00:00
sago35 253f8e3220 pixel: add Grayscale2bit color (#817)
* pixel: add GrayScale2bit color
* pixel: fix spelling of 'GrayScale' to 'Grayscale'
2025-11-15 09:02:55 +01:00
Yurii Soldak c710cc8236 ssd1xxx: break dependency from machine package (#812)
* ssd1xxx: break dependency from machine package
* fix ssd1289 example
* simplify
2025-11-12 09:40:34 +01:00
Nicholas Wiersma 4b831af52b w5500: initial version the driver (#788)
Thank you, @nrwiersma for working on this and @soypat for review. Now merging.
2025-11-12 09:00:21 +01:00
Patricio Whittingslow 09fd01340b add simplest driver ports 2025-11-11 10:42:28 +01:00
gkits 23833e69c7 DS3231 Alarm features (#805)
* Added alarm features
* more functions
* chore: fix typo
* feat(ds3231): add Alarm2Mode type and mode consts
* docs(ds3231): add docstrings for SQW functions
* feat(ds3231): add methods for Alarm2
* fix(ds3231): use Alarm2Mode for SetAlarm2 method
* docs(ds3231): add example for alarms
* docs(ds3231): refactor alarms example
* make main function more concise to avoid llvm error for pico
* ci(ds3231): split basic and alarm tests
* style(ds3231): reorder private funcs to the bottom
* docs(ds3231): add docstring for alarm modes
* docs(ds3231): make alarm docstrings more descriptive
* chore(ds3231): fix typo in docstring
* style(ds3231): reorder public functions
* style(ds3231): reorder private methods
* chore(ds3231): add missing error handling
* feat(ds3231): use setter funcs for en/disabling instead of separate funcs
* fix(ds3231): correctly enable alarms in example
* style(ds3231): rename SetEnable32K to SetEnabled32K for consistency
* refactor(ds3231): replace legacy with regmap package
* refactor(ds3231): use Write32 instead of Tx for SetAlarm1
* chore(ds3231): remove fmt deps and and use println in examples
* refactor(ds3231): read temperature as uint16

---------

Co-authored-by: Matthias Fulz <mfulz@olznet.de>
2025-11-11 10:22:28 +01:00
52 changed files with 4401 additions and 486 deletions
+7 -6
View File
@@ -11,13 +11,12 @@ on:
jobs:
build:
runs-on: ubuntu-latest
container: ghcr.io/tinygo-org/tinygo-dev:latest
container:
image: ghcr.io/tinygo-org/tinygo:latest
options: --user root
steps:
- name: Work around CVE-2022-24765
# We're not on a multi-user machine, so this is safe.
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v6
- name: TinyGo version check
run: tinygo version
- name: Enforce Go Formatted Code
@@ -25,4 +24,6 @@ jobs:
- name: Run unit tests
run: make unit-test
- name: Run build and smoke tests
run: make smoke-test
run: |
go env -w GOFLAGS=-buildvcs=false
make smoke-test
+53
View File
@@ -1,3 +1,56 @@
0.34.0
---
- **core**
- add regmap package to facilitate heapless driver development
- PinInput+PinOutput HAL (#753, reloaded) (#795)
- Add Device8I2C/SPI types and their logic (#801)
- **new devices**
- **bno8x**
- Add support for CEVA BNO08x 9DoF sensor (#809)
- **hineyhsc**
- Add Honeywell HSC TruStability SPI+I2C pressure sensor driver (#799)
- **p25q16h**
- added support for P25Q16H flash chip for xiao-ble target
- **si5351**
- add support for si5351 (#810)
- **w25q80dv**
- added support for W25Q80DV flash chip for xiao-ble target
- **w5500**
- initial version the driver (#788)
- **enhancements**
- **ds3231**
- DS3231 Alarm features (#805)
- **general**
- add simplest driver ports
- **lis3dh**
- add Update and Acceleration calls
- use correct error handling and make configurable
- **lsm9ds1**
- avoid unnecessary heap allocations
- **pixel**
- add Grayscale2bit color (#817)
- **scd4x**
- add support for SCD41 single-shot measurements
- remove dead code
- update package to use standard methods
- **si5351**
- add many missing functions needed for convenient use.
- **ssd1xxx**
- break dependency from machine package (#812)
- **test**
- Add TestImageRGB888 and TestImageRGB555
- **bugfixes**
- **quadrature**
- add RP2350 to quadrature_interrupt.go
- **pixel**
- correct logic error in image size checks in pixel's tests
- correct logic error in image size checks in pixel's tests (Monochrome)
- correct RGB555 to RGBA conversion logic
0.33.0
---
- **new devices**
+1 -1
View File
@@ -3,7 +3,7 @@
[![PkgGoDev](https://pkg.go.dev/badge/tinygo.org/x/drivers)](https://pkg.go.dev/tinygo.org/x/drivers) [![Build](https://github.com/tinygo-org/drivers/actions/workflows/build.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/drivers/actions/workflows/build.yml)
This package provides a collection of over 100 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 130 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/
+7 -3
View File
@@ -5,9 +5,10 @@ package apa102 // import "tinygo.org/x/drivers/apa102"
import (
"image/color"
"machine"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/legacy"
"tinygo.org/x/drivers/internal/pin"
)
const (
@@ -37,8 +38,11 @@ func New(b drivers.SPI) *Device {
// NewSoftwareSPI returns a new APA102 driver that will use a software based
// implementation of the SPI protocol.
func NewSoftwareSPI(sckPin, sdoPin machine.Pin, delay uint32) *Device {
return New(&bbSPI{SCK: sckPin, SDO: sdoPin, Delay: delay})
func NewSoftwareSPI(sckPin, sdoPin pin.Output, delay uint32) *Device {
return New(&bbSPI{SCK: sckPin.Set, SDO: sdoPin.Set, Delay: delay, configurePins: func() {
legacy.ConfigurePinOut(sckPin)
legacy.ConfigurePinOut(sdoPin)
}})
}
// WriteColors writes the given RGBA color slice out using the APA102 protocol.
+12 -6
View File
@@ -1,6 +1,9 @@
package apa102
import "machine"
import (
"tinygo.org/x/drivers/internal/legacy"
"tinygo.org/x/drivers/internal/pin"
)
// bbSPI is a dumb bit-bang implementation of SPI protocol that is hardcoded
// to mode 0 and ignores trying to receive data. Just enough for the APA102.
@@ -8,15 +11,18 @@ import "machine"
// most purposes other than the APA102 package. It might be desirable to make
// this more generic and include it in the TinyGo "machine" package instead.
type bbSPI struct {
SCK machine.Pin
SDO machine.Pin
Delay uint32
SCK pin.OutputFunc
SDO pin.OutputFunc
Delay uint32
configurePins func()
}
// Configure sets up the SCK and SDO pins as outputs and sets them low
func (s *bbSPI) Configure() {
s.SCK.Configure(machine.PinConfig{Mode: machine.PinOutput})
s.SDO.Configure(machine.PinConfig{Mode: machine.PinOutput})
if s.configurePins == nil {
panic(legacy.ErrConfigBeforeInstantiated)
}
s.configurePins()
s.SCK.Low()
s.SDO.Low()
if s.Delay == 0 {
+31 -24
View File
@@ -1,31 +1,36 @@
package bmi160
import (
"machine"
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/legacy"
"tinygo.org/x/drivers/internal/pin"
)
// DeviceSPI is the SPI interface to a BMI160 accelerometer/gyroscope. There is
// also an I2C interface, but it is not yet supported.
type DeviceSPI struct {
// Chip select pin
CSB machine.Pin
csb pin.OutputFunc
buf [7]byte
// SPI bus (requires chip select to be usable).
Bus drivers.SPI
bus drivers.SPI
configurePins func()
}
// NewSPI returns a new device driver. The pin and SPI interface are not
// touched, provide a fully configured SPI object and call Configure to start
// using this device.
func NewSPI(csb machine.Pin, spi drivers.SPI) *DeviceSPI {
func NewSPI(csb pin.Output, spi drivers.SPI) *DeviceSPI {
return &DeviceSPI{
CSB: csb, // chip select
Bus: spi,
csb: csb.Set, // chip select
bus: spi,
configurePins: func() {
legacy.ConfigurePinOut(csb)
},
}
}
@@ -33,9 +38,11 @@ func NewSPI(csb machine.Pin, spi drivers.SPI) *DeviceSPI {
// configures the BMI160, but it does not configure the SPI interface (it is
// assumed to be up and running).
func (d *DeviceSPI) Configure() error {
d.CSB.Configure(machine.PinConfig{Mode: machine.PinOutput})
d.CSB.High()
if d.configurePins == nil {
return legacy.ErrConfigBeforeInstantiated
}
d.configurePins()
d.csb.High()
// The datasheet recommends doing a register read from address 0x7F to get
// SPI communication going:
// > If CSB sees a rising edge after power-up, the BMI160 interface switches
@@ -86,9 +93,9 @@ func (d *DeviceSPI) ReadTemperature() (temperature int32, err error) {
data[0] = 0x80 | reg_TEMPERATURE_0
data[1] = 0
data[2] = 0
d.CSB.Low()
err = d.Bus.Tx(data, data)
d.CSB.High()
d.csb.Low()
err = d.bus.Tx(data, data)
d.csb.High()
if err != nil {
return
}
@@ -123,9 +130,9 @@ func (d *DeviceSPI) ReadAcceleration() (x int32, y int32, z int32, err error) {
for i := 1; i < len(data); i++ {
data[i] = 0
}
d.CSB.Low()
err = d.Bus.Tx(data, data)
d.CSB.High()
d.csb.Low()
err = d.bus.Tx(data, data)
d.csb.High()
if err != nil {
return
}
@@ -153,9 +160,9 @@ func (d *DeviceSPI) ReadRotation() (x int32, y int32, z int32, err error) {
for i := 1; i < len(data); i++ {
data[i] = 0
}
d.CSB.Low()
err = d.Bus.Tx(data, data)
d.CSB.High()
d.csb.Low()
err = d.bus.Tx(data, data)
d.csb.High()
if err != nil {
return
}
@@ -201,9 +208,9 @@ func (d *DeviceSPI) readRegister(address uint8) uint8 {
data := d.buf[:2]
data[0] = 0x80 | address
data[1] = 0
d.CSB.Low()
d.Bus.Tx(data, data)
d.CSB.High()
d.csb.Low()
d.bus.Tx(data, data)
d.csb.High()
return data[1]
}
@@ -217,7 +224,7 @@ func (d *DeviceSPI) writeRegister(address, data uint8) {
buf[0] = address
buf[1] = data
d.CSB.Low()
d.Bus.Tx(buf, buf)
d.CSB.High()
d.csb.Low()
d.bus.Tx(buf, buf)
d.csb.High()
}
+256
View File
@@ -0,0 +1,256 @@
// Package bno08x provides a TinyGo driver for the Adafruit BNO08x 9-DOF IMU sensors.
//
// This driver implements the CEVA SH-2 protocol over the SHTP transport layer,
// providing access to orientation, motion, and environmental sensors.
//
// Datasheet: https://www.ceva-ip.com/wp-content/uploads/BNO080_085-Datasheet.pdf
package bno08x
import (
"time"
"tinygo.org/x/drivers/internal/pin"
)
// Buser is the interface that wraps I2C or SPI bus operations.
type Buser interface {
configure(address uint16, readChunk int) error
read(target []byte) (int, uint32, error)
write(data []byte) error
softReset() error
}
// Device represents a BNO08x sensor device.
type Device struct {
bus Buser
resetPin pin.OutputFunc
hal *hal
shtp *shtp
sh2 *sh2Protocol
queue [8]SensorValue
queueHead int
queueTail int
queueCount int
productIDs ProductIDs
lastReset bool
}
// Config holds configuration options for the device.
type Config struct {
// Address is the I2C address (used only for I2C bus).
Address uint16
// ResetPin is the optional hardware reset pin.
ResetPin pin.OutputFunc
// ReadChunk is the I2C read chunk size (used only for I2C bus).
ReadChunk int
// StartupDelay is the delay after reset (default: 100ms).
StartupDelay time.Duration
}
// Configure initializes the sensor and prepares it for use.
func (d *Device) Configure(cfg Config) error {
// Configure bus-specific settings
if err := d.bus.configure(cfg.Address, cfg.ReadChunk); err != nil {
return err
}
if cfg.ResetPin != nil {
d.resetPin = cfg.ResetPin
}
if cfg.StartupDelay <= 0 {
cfg.StartupDelay = 100 * time.Millisecond
}
d.hal = newHAL(d)
d.shtp = newSHTP(d.hal)
d.sh2 = newSH2Protocol(d)
d.queueHead = 0
d.queueTail = 0
d.queueCount = 0
d.productIDs = ProductIDs{}
d.lastReset = false
if err := d.hal.open(); err != nil {
return err
}
// Now that handlers are registered, perform reset
// Try hardware reset first if available
if d.resetPin != nil {
d.hardwareReset()
time.Sleep(cfg.StartupDelay)
} else {
// No hardware reset pin - try soft reset via bus
if err := d.bus.softReset(); err != nil {
// If that fails, try soft reset via SHTP protocol
_ = d.sh2.softReset()
time.Sleep(50 * time.Millisecond)
}
}
// Wait for reset notification by actively polling
// The sensor should send reset complete message shortly after reset
deadline := time.Now().Add(1000 * time.Millisecond)
pollCount := 0
for time.Now().Before(deadline) {
pollCount++
if err := d.service(); err != nil {
// Ignore errors during initial polling - sensor might not be ready
time.Sleep(1 * time.Millisecond)
continue
}
if d.lastReset {
break
}
time.Sleep(1 * time.Millisecond)
}
if !d.lastReset {
return errTimeout
}
// NOTE: We intentionally skip the Initialize command (sh2_initialize)
// Testing revealed that sending the Initialize command (0xF2 0x00 0x04 0x01...)
// prevents the BNO08x from sending sensor reports on channel 3.
// The sensor works correctly without this command after a soft reset.
// The Arduino library likely works because it does a hardware reset which
// may put the sensor in a different state, or their initialization sequence
// differs in a way that doesn't trigger this issue.
// Request product IDs
if err := d.sh2.requestProductIDs(); err != nil {
return err
}
// Wait for product IDs with polling delay
deadline = time.Now().Add(500 * time.Millisecond)
for time.Now().Before(deadline) {
if err := d.service(); err != nil {
time.Sleep(10 * time.Millisecond)
continue
}
if d.productIDs.NumEntries > 0 {
break
}
time.Sleep(10 * time.Millisecond)
}
if d.productIDs.NumEntries == 0 {
return errTimeout
}
return nil
}
// EnableReport enables a specific sensor report at the given interval.
func (d *Device) EnableReport(id SensorID, intervalUs uint32) error {
err := d.sh2.enableReport(id, intervalUs)
if err != nil {
return err
}
// Poll a few times to let the sensor process the command
// and potentially send acknowledgment
for i := 0; i < 10; i++ {
_ = d.service()
time.Sleep(10 * time.Millisecond)
}
return nil
}
// GetSensorConfig retrieves the current configuration for a sensor.
func (d *Device) GetSensorConfig(id SensorID) (SensorConfig, error) {
return d.sh2.getSensorConfig(id)
}
// SetSensorConfig sets the configuration for a sensor.
func (d *Device) SetSensorConfig(id SensorID, config SensorConfig) error {
return d.sh2.setSensorConfig(id, config)
}
// WasReset returns true if the sensor signaled a reset since the last call.
func (d *Device) WasReset() bool {
if d.lastReset {
d.lastReset = false
return true
}
return false
}
// GetSensorEvent retrieves the next available sensor event if present.
func (d *Device) GetSensorEvent() (SensorValue, bool) {
if d.queueCount == 0 {
if err := d.service(); err != nil {
return SensorValue{}, false
}
if d.queueCount == 0 {
return SensorValue{}, false
}
}
value := d.queue[d.queueHead]
d.queueHead = (d.queueHead + 1) % len(d.queue)
d.queueCount--
return value, true
}
// ProductIDs returns the cached product identification information.
func (d *Device) ProductIDs() ProductIDs {
return d.productIDs
}
// Service processes pending sensor data.
// This is called automatically by GetSensorEvent but can be called manually
// for more control over timing.
func (d *Device) Service() error {
return d.service()
}
func (d *Device) enqueue(value SensorValue) {
next := (d.queueTail + 1) % len(d.queue)
if d.queueCount == len(d.queue) {
// Queue full, drop oldest
d.queueHead = (d.queueHead + 1) % len(d.queue)
d.queueCount--
}
d.queue[d.queueTail] = value
d.queueTail = next
d.queueCount++
}
func (d *Device) service() error {
if d.shtp == nil {
return nil
}
for {
processed, err := d.shtp.poll()
if err != nil {
return err
}
if !processed {
break
}
}
return nil
}
func (d *Device) hardwareReset() {
if d.resetPin == nil {
return
}
d.resetPin.High()
time.Sleep(10 * time.Millisecond)
d.resetPin.Low()
time.Sleep(10 * time.Millisecond)
d.resetPin.High()
time.Sleep(10 * time.Millisecond)
}
+173
View File
@@ -0,0 +1,173 @@
package bno08x
import (
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/pin"
)
// I2CConfig holds I2C-specific configuration options.
type I2CConfig struct {
// Address is the I2C address (default: 0x4A).
Address uint16
// ResetPin is the optional hardware reset pin.
ResetPin pin.OutputFunc
// ReadChunk is the I2C read chunk size (default: 32 bytes).
ReadChunk int
}
const (
// DefaultAddress is the default I2C address.
DefaultAddress = 0x4A
)
// NewI2C creates a new BNO08x device using I2C communication.
func NewI2C(bus drivers.I2C) *Device {
return &Device{
bus: &I2CBus{
wire: bus,
address: DefaultAddress,
readChunk: i2cDefaultChunk,
},
}
}
// I2CBus implements the Buser interface for I2C communication.
type I2CBus struct {
wire drivers.I2C
address uint16
readChunk int
scratch []byte
header [shtpHeaderLength]byte
}
// configure sets up the I2C bus with the specified address and chunk size.
func (b *I2CBus) configure(address uint16, readChunk int) error {
if address != 0 {
b.address = address
}
if readChunk > 0 {
b.readChunk = readChunk
}
chunk := b.readChunk
if chunk < shtpHeaderLength {
chunk = shtpHeaderLength
}
b.scratch = make([]byte, chunk)
return nil
}
// read reads data from the I2C bus.
func (b *I2CBus) read(target []byte) (int, uint32, error) {
// Read SHTP header (4 bytes) to get packet length
// Use pre-allocated header buffer to avoid allocations
err := b.wire.Tx(b.address, nil, b.header[:])
if err != nil {
return 0, 0, err
}
// Parse packet length from header
packetLen := uint16(b.header[0]) | (uint16(b.header[1]) << 8)
// Check if continuation bit is set (0x8000)
// This means no data is available yet
if packetLen&continueMask != 0 {
return 0, 0, nil
}
// No continuation bit, check for actual data
if packetLen == 0 {
return 0, 0, nil
}
if int(packetLen) > len(target) {
return 0, 0, errBufferTooSmall
}
// Now read the full packet in chunks, re-reading the header in first chunk
// This follows Arduino's approach: initial header read is just to get size,
// actual packet data (including header) is read in the loop
cargoRemaining := int(packetLen)
offset := 0
firstRead := true
for cargoRemaining > 0 {
var request int
if firstRead {
// First read: get the full packet including header (up to chunkSize)
request = b.readChunk
if request > cargoRemaining {
request = cargoRemaining
}
} else {
// Subsequent reads: each chunk has a 4-byte header we need to skip
request = b.readChunk
if request > cargoRemaining+shtpHeaderLength {
request = cargoRemaining + shtpHeaderLength
}
}
// Ensure scratch buffer is large enough
if request > len(b.scratch) {
b.scratch = make([]byte, request)
}
buf := b.scratch[:request]
// Read chunk
err = b.wire.Tx(b.address, nil, buf)
if err != nil {
return 0, 0, err
}
var cargoRead int
if firstRead {
// First read: copy everything including header
cargoRead = request
copy(target[offset:], buf[:cargoRead])
firstRead = false
} else {
// Subsequent reads: skip the 4-byte header
cargoRead = request - shtpHeaderLength
copy(target[offset:], buf[shtpHeaderLength:shtpHeaderLength+cargoRead])
}
offset += cargoRead
cargoRemaining -= cargoRead
}
// Extract timestamp from the header in the target buffer
timestamp := uint32(target[2]) | (uint32(target[3]) << 8)
return int(packetLen), timestamp, nil
}
// write sends data over the I2C bus.
func (b *I2CBus) write(data []byte) error {
return b.wire.Tx(b.address, data, nil)
}
// softReset sends a soft reset command via I2C.
func (b *I2CBus) softReset() error {
// Send soft reset packet via I2C as per Adafruit implementation
// Format: [length_low, length_high, channel, sequence, command]
// This is: 5 bytes total, channel 1 (executable), command 1 (reset)
softResetPacket := []byte{5, 0, 1, 0, 1}
// Try up to 5 times
var err error
for i := 0; i < 5; i++ {
err = b.wire.Tx(b.address, softResetPacket, nil)
if err == nil {
// Success - wait for sensor to process reset
time.Sleep(300 * time.Millisecond)
return nil
}
time.Sleep(30 * time.Millisecond)
}
return err
}
+179
View File
@@ -0,0 +1,179 @@
package bno08x
// I2C and protocol constants
const (
shtpHeaderLength = 4
maxTransferOut = 256
maxTransferIn = 384
i2cDefaultChunk = 32
continueMask = 0x8000
)
// SHTP channel numbers
const (
channelCommand = 0
channelExecutable = 1
channelControl = 2
channelSensorReport = 3
channelWakeReport = 4
channelGyroRV = 5
)
// SH-2 report IDs
const (
reportProdIDReq = 0xF9
reportProdIDResp = 0xF8
reportSetFeature = 0xFD
reportGetFeature = 0xFE
reportGetFeatureResp = 0xFC
reportCommandReq = 0xF2
reportCommandResp = 0xF1
reportFRSWriteReq = 0xF7
reportFRSWriteData = 0xF6
reportFRSReadReq = 0xF4
reportFRSReadResp = 0xF3
reportBaseTimestamp = 0xFB
reportTimestampReuse = 0xFA
reportForceFlush = 0xF0
reportFlushCompleted = 0xEF
reportResetReq = 0xF1
reportResetResp = 0xF0
)
// SH-2 commands
const (
cmdErrors = 0x01
cmdCounts = 0x02
cmdTare = 0x03
cmdInitialize = 0x04
cmdFRS = 0x05
cmdDCD = 0x06
cmdMECal = 0x07
cmdProdIDReq = 0x07
cmdDCDSave = 0x09
cmdGetOscType = 0x0A
cmdClearDCDReset = 0x0B
cmdCal = 0x0C
cmdBootloader = 0x0D
cmdInteractiveZRO = 0x0E
// Command parameters
initSystem = 0x01
initUnsolicited = 0x80
countsClearCounts = 0x01
countsGetCounts = 0x00
tareTareNow = 0x00
tarePersist = 0x01
tareSetReorientation = 0x02
calStart = 0x00
calFinish = 0x01
commandParamCount = 9
responseValueCount = 11
)
// Feature report flags
const (
featChangeSensitivityRelative = 0x01
featChangeSensitivityEnabled = 0x02
featWakeEnabled = 0x04
featAlwaysOnEnabled = 0x08
)
// Scaling factors for sensor data
// These are derived from the Q-point encoding in the SH-2 specification
const (
scaleQuat = 1.0 / 16384.0 // Q14
scaleAccel = 1.0 / 256.0 // Q8
scaleGyro = 1.0 / 512.0 // Q9
scaleMag = 1.0 / 16.0 // Q4
scaleAccuracy = 1.0 / 4096.0 // Q12
scalePressure = 1.0 / 1048576.0 // Q20
scaleLight = 1.0 / 256.0 // Q8
scaleHumidity = 1.0 / 256.0 // Q8
scaleProximity = 1.0 / 16.0 // Q4
scaleTemperature = 1.0 / 128.0 // Q7
scaleAngle = 1.0 / 16.0 // Q4
scaleHeartRate = 1.0 / 16.0 // Q4
)
// Activity classifier codes (extended beyond standard SH-2)
const (
ActivityUnknown = 0
ActivityInVehicle = 1
ActivityOnBicycle = 2
ActivityOnFoot = 3
ActivityStill = 4
ActivityTilting = 5
ActivityWalking = 6
ActivityRunning = 7
ActivityOnStairs = 8
ActivityOptionCount = 9
)
// Stability classifier values
const (
StabilityUnknown = 0
StabilityOnTable = 1
StabilityStationary = 2
StabilityStable = 3
StabilityMotion = 4
)
// Tap detector flags
const (
TapX = 0x01 // 1 - X axis tapped
TapXPos = 0x02 // 2 - X positive direction
TapY = 0x04 // 4 - Y axis tapped
TapYPos = 0x08 // 8 - Y positive direction
TapZ = 0x10 // 16 - Z axis tapped
TapZPos = 0x20 // 32 - Z positive direction
TapDouble = 0x40 // 64 - Double tap occurred
)
// GUID values for SHTP
const (
guidSHTP = 0
guidExecutable = 1
guidSensorHub = 2
)
// Advertisement tags
const (
tagNull = 0
tagGUID = 1
tagMaxCargoHeaderWrite = 2
tagMaxCargoHeaderRead = 3
tagMaxTransferWrite = 4
tagMaxTransferRead = 5
tagNormalChannel = 6
tagWakeChannel = 7
tagAppName = 8
tagChannelName = 9
tagAdvCount = 10
tagAppSpecific = 0x80
tagSH2Version = 0x80
tagSH2ReportLengths = 0x81
)
// Timeouts
const (
advertTimeout = 200000 // microseconds
commandTimeout = 300000 // microseconds
)
// Executable device commands
const (
execDeviceCmdReset = 1
execDeviceCmdOn = 2
execDeviceCmdSleep = 3
)
// Executable device responses
const (
execDeviceRespResetComplete = 1
)
+316
View File
@@ -0,0 +1,316 @@
package bno08x
import "encoding/binary"
// decodeSensor decodes a sensor report payload into a SensorValue.
func decodeSensor(payload []byte, timestamp uint32) (SensorValue, bool) {
if len(payload) < 4 {
return SensorValue{}, false
}
value := SensorValue{
id: SensorID(payload[0]),
sequence: payload[1],
status: payload[2] & 0x03,
delay: payload[3],
timestamp: uint64(timestamp),
}
data := payload[4:]
switch value.id {
case SensorRawAccelerometer:
if len(data) >= 10 {
value.rawAccelerometer = RawVector3{
X: int16(binary.LittleEndian.Uint16(data[0:])),
Y: int16(binary.LittleEndian.Uint16(data[2:])),
Z: int16(binary.LittleEndian.Uint16(data[4:])),
Timestamp: binary.LittleEndian.Uint32(data[6:]),
}
}
case SensorAccelerometer:
if len(data) >= 6 {
value.accelerometer = Vector3{
X: qToFloat(data[0:], scaleAccel),
Y: qToFloat(data[2:], scaleAccel),
Z: qToFloat(data[4:], scaleAccel),
}
}
case SensorLinearAcceleration:
if len(data) >= 6 {
value.linearAcceleration = Vector3{
X: qToFloat(data[0:], scaleAccel),
Y: qToFloat(data[2:], scaleAccel),
Z: qToFloat(data[4:], scaleAccel),
}
}
case SensorGravity:
if len(data) >= 6 {
value.gravity = Vector3{
X: qToFloat(data[0:], scaleAccel),
Y: qToFloat(data[2:], scaleAccel),
Z: qToFloat(data[4:], scaleAccel),
}
}
case SensorRawGyroscope:
if len(data) >= 12 {
value.rawGyroscope = RawGyroscope{
X: int16(binary.LittleEndian.Uint16(data[0:])),
Y: int16(binary.LittleEndian.Uint16(data[2:])),
Z: int16(binary.LittleEndian.Uint16(data[4:])),
Temperature: int16(binary.LittleEndian.Uint16(data[6:])),
Timestamp: binary.LittleEndian.Uint32(data[8:]),
}
}
case SensorGyroscope:
if len(data) >= 6 {
value.gyroscope = Vector3{
X: qToFloat(data[0:], scaleGyro),
Y: qToFloat(data[2:], scaleGyro),
Z: qToFloat(data[4:], scaleGyro),
}
}
case SensorGyroscopeUncalibrated:
if len(data) >= 12 {
value.gyroscopeUncal = GyroscopeUncalibrated{
X: qToFloat(data[0:], scaleGyro),
Y: qToFloat(data[2:], scaleGyro),
Z: qToFloat(data[4:], scaleGyro),
BiasX: qToFloat(data[6:], scaleGyro),
BiasY: qToFloat(data[8:], scaleGyro),
BiasZ: qToFloat(data[10:], scaleGyro),
}
}
case SensorRawMagnetometer:
if len(data) >= 10 {
value.rawMagnetometer = RawVector3{
X: int16(binary.LittleEndian.Uint16(data[0:])),
Y: int16(binary.LittleEndian.Uint16(data[2:])),
Z: int16(binary.LittleEndian.Uint16(data[4:])),
Timestamp: binary.LittleEndian.Uint32(data[6:]),
}
}
case SensorMagneticField:
if len(data) >= 6 {
value.magneticField = Vector3{
X: qToFloat(data[0:], scaleMag),
Y: qToFloat(data[2:], scaleMag),
Z: qToFloat(data[4:], scaleMag),
}
}
case SensorMagneticFieldUncalibrated:
if len(data) >= 12 {
value.magneticFieldUncal = MagneticFieldUncalibrated{
X: qToFloat(data[0:], scaleMag),
Y: qToFloat(data[2:], scaleMag),
Z: qToFloat(data[4:], scaleMag),
BiasX: qToFloat(data[6:], scaleMag),
BiasY: qToFloat(data[8:], scaleMag),
BiasZ: qToFloat(data[10:], scaleMag),
}
}
case SensorRotationVector:
if len(data) >= 10 {
value.quaternion = Quaternion{
I: qToFloat(data[0:], scaleQuat),
J: qToFloat(data[2:], scaleQuat),
K: qToFloat(data[4:], scaleQuat),
Real: qToFloat(data[6:], scaleQuat),
}
value.quaternionAccuracy = qToFloat(data[8:], scaleAccuracy)
}
case SensorGameRotationVector:
if len(data) >= 8 {
value.quaternion = Quaternion{
I: qToFloat(data[0:], scaleQuat),
J: qToFloat(data[2:], scaleQuat),
K: qToFloat(data[4:], scaleQuat),
Real: qToFloat(data[6:], scaleQuat),
}
}
case SensorGeomagneticRotationVector:
if len(data) >= 10 {
value.quaternion = Quaternion{
I: qToFloat(data[0:], scaleQuat),
J: qToFloat(data[2:], scaleQuat),
K: qToFloat(data[4:], scaleQuat),
Real: qToFloat(data[6:], scaleQuat),
}
value.quaternionAccuracy = qToFloat(data[8:], scaleAccuracy)
}
case SensorARVRStabilizedRV:
if len(data) >= 10 {
value.quaternion = Quaternion{
I: qToFloat(data[0:], scaleQuat),
J: qToFloat(data[2:], scaleQuat),
K: qToFloat(data[4:], scaleQuat),
Real: qToFloat(data[6:], scaleQuat),
}
value.quaternionAccuracy = qToFloat(data[8:], scaleAccuracy)
}
case SensorARVRStabilizedGRV:
if len(data) >= 8 {
value.quaternion = Quaternion{
I: qToFloat(data[0:], scaleQuat),
J: qToFloat(data[2:], scaleQuat),
K: qToFloat(data[4:], scaleQuat),
Real: qToFloat(data[6:], scaleQuat),
}
}
case SensorGyroIntegratedRV:
if len(data) >= 10 {
value.quaternion = Quaternion{
I: qToFloat(data[0:], scaleQuat),
J: qToFloat(data[2:], scaleQuat),
K: qToFloat(data[4:], scaleQuat),
Real: qToFloat(data[6:], scaleQuat),
}
// Angular velocity X at data[8:10]
}
case SensorPressure:
if len(data) >= 4 {
value.pressure = float32(int32(binary.LittleEndian.Uint32(data[0:]))) * scalePressure
}
case SensorAmbientLight:
if len(data) >= 4 {
value.ambientLight = float32(int32(binary.LittleEndian.Uint32(data[0:]))) * scaleLight
}
case SensorHumidity:
if len(data) >= 2 {
value.humidity = qToFloat(data[0:], scaleHumidity)
}
case SensorProximity:
if len(data) >= 2 {
value.proximity = qToFloat(data[0:], scaleProximity)
}
case SensorTemperature:
if len(data) >= 2 {
value.temperature = qToFloat(data[0:], scaleTemperature)
}
case SensorTapDetector:
if len(data) >= 1 {
value.tapDetector = TapDetector{
Flags: data[0],
}
}
case SensorStepDetector:
if len(data) >= 4 {
value.stepDetector = StepDetector{
Latency: binary.LittleEndian.Uint32(data[0:]),
}
}
case SensorStepCounter:
if len(data) >= 8 {
value.stepCounter = StepCounter{
Count: uint16(binary.LittleEndian.Uint32(data[4:8])),
Latency: binary.LittleEndian.Uint32(data[0:4]),
}
}
case SensorSignificantMotion:
if len(data) >= 2 {
value.significantMotion = SignificantMotion{
Motion: binary.LittleEndian.Uint16(data[0:]),
}
}
case SensorStabilityClassifier:
if len(data) >= 1 {
value.stabilityClassifier = StabilityClassifier{
Classification: data[0],
}
}
case SensorStabilityDetector:
if len(data) >= 1 {
value.stabilityDetector = data[0]
}
case SensorShakeDetector:
if len(data) >= 2 {
value.shakeDetector = ShakeDetector{
Shake: binary.LittleEndian.Uint16(data[0:]),
}
}
case SensorFlipDetector:
if len(data) >= 2 {
value.flipDetector = binary.LittleEndian.Uint16(data[0:2])
}
case SensorPickupDetector:
if len(data) >= 2 {
// Pickup detected at data[0:2]
}
case SensorPersonalActivityClassifier:
if len(data) >= 16 {
value.personalActivityClassifier = PersonalActivityClassifier{
Page: data[0],
MostLikelyState: data[1],
EndOfPage: data[15],
}
for i := 0; i < 10 && i+2 < len(data); i++ {
value.personalActivityClassifier.Confidence[i] = data[2+i]
}
}
case SensorSleepDetector:
if len(data) >= 1 {
value.sleepDetector = data[0]
}
case SensorTiltDetector:
if len(data) >= 1 {
value.tiltDetector = data[0]
}
case SensorPocketDetector:
if len(data) >= 1 {
value.pocketDetector = data[0]
}
case SensorCircleDetector:
if len(data) >= 1 {
value.circleDetector = data[0]
}
case SensorHeartRateMonitor:
if len(data) >= 2 {
value.heartRateMonitor = binary.LittleEndian.Uint16(data[0:])
}
}
return value, true
}
// qToFloat converts a Q-point fixed-point value to float32.
func qToFloat(data []byte, scale float32) float32 {
if len(data) < 2 {
return 0
}
return float32(int16(binary.LittleEndian.Uint16(data))) * scale
}
+43
View File
@@ -0,0 +1,43 @@
package bno08x
import (
"time"
)
// hal implements the hardware abstraction layer for bus communication.
type hal struct {
device *Device
}
func newHAL(dev *Device) *hal {
return &hal{
device: dev,
}
}
func (h *hal) open() error {
// HAL is now open and ready for communication
// Soft reset will be sent after handlers are registered
return nil
}
func (h *hal) close() {}
func (h *hal) read(target []byte) (int, uint32, error) {
return h.device.bus.read(target)
}
func (h *hal) write(frame []byte) (int, error) {
if len(frame) > maxTransferOut {
return 0, errFrameTooLarge
}
err := h.device.bus.write(frame)
if err != nil {
return 0, err
}
return len(frame), nil
}
func (h *hal) getTimeUs() uint32 {
return uint32(time.Now().UnixNano() / 1000)
}
+387
View File
@@ -0,0 +1,387 @@
// SH-2 specification found at https://www.ceva-ip.com/wp-content/uploads/SH-2-Reference-Manual.pdf
package bno08x
import (
"encoding/binary"
"time"
)
// getReportLen returns the length in bytes of a sensor report given its ID.
// Returns 0 for unknown report IDs.
func getReportLen(reportID byte) int {
switch reportID {
case 0xF1: // FLUSH_COMPLETED
return 6
case 0xFA: // TIMESTAMP_REBASE
return 5
case 0xFB: // BASE_TIMESTAMP_REF
return 5
case 0xFC: // GET_FEATURE_RESP
return 17
case 0x01: // Accelerometer (calibrated)
return 10
case 0x02: // Gyroscope (calibrated)
return 10
case 0x03: // Magnetic field (calibrated)
return 10
case 0x04: // Linear acceleration
return 10
case 0x05: // Rotation vector
return 14
case 0x06: // Gravity
return 10
case 0x07: // Gyroscope uncalibrated
return 16
case 0x08: // Game rotation vector
return 12
case 0x09: // Geomagnetic rotation vector
return 14
case 0x0A: // Pressure
return 10
case 0x0B: // Ambient light
return 10
case 0x0C: // Humidity
return 10
case 0x0D: // Proximity
return 10
case 0x0E: // Temperature
return 10
case 0x0F: // Magnetic field uncalibrated
return 16
case 0x10: // Tap detector
return 5
case 0x11: // Step counter
return 12
case 0x12: // Significant motion
return 6
case 0x13: // Stability classifier
return 5
case 0x14: // Raw accelerometer
return 16
case 0x15: // Raw gyroscope
return 16
case 0x16: // Raw magnetometer
return 16
case 0x18: // Step detector
return 8
case 0x19: // Shake detector
return 6
case 0x1A: // Flip detector
return 6
case 0x1B: // Pickup detector
return 6
case 0x1C: // Stability detector
return 6
case 0x1E: // Personal activity classifier
return 16
default:
// For most sensor reports, they are typically 10-16 bytes
// If we don't know the exact length, return a safe default
// that covers most cases (the handler will bounds-check)
if reportID < 0xF0 {
return 10 // Most sensor reports are at least this long
}
return 0
}
}
// sh2Protocol implements the Sensor Hub 2 (SH-2) application protocol.
type sh2Protocol struct {
device *Device
transport *shtp
cmdSeq uint8
waiting bool
lastCmd uint8
pendingConfigRequest bool
pendingConfigSensor SensorID
receivedConfig SensorConfig
configReady bool
configBuf [17]byte // Reusable buffer for setSensorConfig
commandBuf [3 + commandParamCount]byte // Reusable buffer for sendCommand
}
func newSH2Protocol(device *Device) *sh2Protocol {
proto := &sh2Protocol{
device: device,
transport: device.shtp,
}
// Register handlers for each channel
device.shtp.register(channelControl, proto.handleControl)
device.shtp.register(channelSensorReport, proto.handleSensor)
device.shtp.register(channelWakeReport, proto.handleSensor)
device.shtp.register(channelGyroRV, proto.handleSensor)
device.shtp.register(channelExecutable, proto.handleExecutable)
return proto
}
// softReset sends a software reset command to the sensor.
func (s *sh2Protocol) softReset() error {
payload := []byte{execDeviceCmdReset}
return s.transport.send(channelExecutable, payload)
}
// initialize sends the initialize command to the sensor.
func (s *sh2Protocol) initialize() error {
return s.sendCommand(cmdInitialize, []byte{initSystem})
}
// requestProductIDs requests product identification information.
func (s *sh2Protocol) requestProductIDs() error {
payload := []byte{reportProdIDReq, 0x00}
return s.transport.send(channelControl, payload)
}
// enableReport enables a sensor report at the specified interval.
func (s *sh2Protocol) enableReport(id SensorID, intervalUs uint32) error {
config := SensorConfig{
ReportInterval: intervalUs,
}
return s.setSensorConfig(id, config)
}
// getSensorConfig retrieves the configuration for a sensor.
// This method sends a GET_FEATURE request and waits for the response
// by polling the device. It will timeout after approximately 1 second.
func (s *sh2Protocol) getSensorConfig(id SensorID) (SensorConfig, error) {
// Mark that we're waiting for a config response
s.pendingConfigRequest = true
s.pendingConfigSensor = id
s.configReady = false
payload := []byte{reportGetFeature, byte(id)}
err := s.transport.send(channelControl, payload)
if err != nil {
s.pendingConfigRequest = false
return SensorConfig{}, err
}
// Poll for response with timeout
maxAttempts := 100 // ~1 second with 10ms delays
for i := 0; i < maxAttempts; i++ {
// Service the device to process incoming messages
s.device.shtp.poll()
if s.configReady {
s.pendingConfigRequest = false
s.configReady = false
return s.receivedConfig, nil
}
// Small delay between polls
time.Sleep(10 * time.Millisecond)
}
s.pendingConfigRequest = false
return SensorConfig{}, errTimeout
}
// setSensorConfig configures a sensor.
func (s *sh2Protocol) setSensorConfig(id SensorID, config SensorConfig) error {
// Use pre-allocated buffer to avoid allocations
payload := s.configBuf[:]
payload[0] = reportSetFeature
payload[1] = byte(id)
// Build feature flags
var flags uint8
if config.ChangeSensitivityEnabled {
flags |= featChangeSensitivityEnabled
}
if config.ChangeSensitivityRelative {
flags |= featChangeSensitivityRelative
}
if config.WakeupEnabled {
flags |= featWakeEnabled
}
if config.AlwaysOnEnabled {
flags |= featAlwaysOnEnabled
}
payload[2] = flags
binary.LittleEndian.PutUint16(payload[3:5], config.ChangeSensitivity)
binary.LittleEndian.PutUint32(payload[5:9], config.ReportInterval)
binary.LittleEndian.PutUint32(payload[9:13], config.BatchInterval)
binary.LittleEndian.PutUint32(payload[13:17], config.SensorSpecific)
return s.transport.send(channelControl, payload)
}
// sendCommand sends a command with parameters to the sensor.
func (s *sh2Protocol) sendCommand(command byte, params []byte) error {
// Use pre-allocated buffer to avoid allocations
payload := s.commandBuf[:]
payload[0] = reportCommandReq
payload[1] = s.cmdSeq
payload[2] = command
s.cmdSeq++
s.lastCmd = command
s.waiting = true
for i := 0; i < commandParamCount && i < len(params); i++ {
payload[3+i] = params[i]
}
return s.transport.send(channelControl, payload[:3+commandParamCount])
}
// handleControl processes control channel messages.
func (s *sh2Protocol) handleControl(payload []byte, timestamp uint32) {
if len(payload) == 0 {
return
}
reportID := payload[0]
switch reportID {
case reportProdIDResp:
s.handleProdID(payload, timestamp)
case reportCommandResp:
s.handleCommandResp(payload, timestamp)
case reportGetFeatureResp:
s.handleGetFeatureResp(payload, timestamp)
case reportFRSReadResp:
// FRS (Flash Record System) read response
// Not implemented in basic version
}
}
// handleProdID processes product ID responses.
func (s *sh2Protocol) handleProdID(payload []byte, timestamp uint32) {
if len(payload) < 16 {
return
}
entry := ProductID{
ResetCause: payload[1],
VersionMajor: payload[2],
VersionMinor: payload[3],
PartNumber: binary.LittleEndian.Uint32(payload[4:8]),
BuildNumber: binary.LittleEndian.Uint32(payload[8:12]),
VersionPatch: binary.LittleEndian.Uint16(payload[12:14]),
Reserved0: payload[14],
Reserved1: payload[15],
}
// Store in first slot
s.device.productIDs.Entries[0] = entry
s.device.productIDs.NumEntries = 1
}
// handleCommandResp processes command responses.
func (s *sh2Protocol) handleCommandResp(payload []byte, timestamp uint32) {
if len(payload) < 16 {
return
}
// seq := payload[1]
command := payload[2]
// commandSeq := payload[3]
// respSeq := payload[4]
// Check if this response is for our command
if s.waiting && command == s.lastCmd {
s.waiting = false
// Status is in payload[6]
// For now, we just acknowledge receipt
}
}
// handleGetFeatureResp processes get feature responses.
func (s *sh2Protocol) handleGetFeatureResp(payload []byte, timestamp uint32) {
if len(payload) < 17 {
return
}
// Parse the response
sensorID := SensorID(payload[1])
flags := payload[2]
changeSensitivity := binary.LittleEndian.Uint16(payload[3:5])
reportInterval := binary.LittleEndian.Uint32(payload[5:9])
batchInterval := binary.LittleEndian.Uint32(payload[9:13])
sensorSpecific := binary.LittleEndian.Uint32(payload[13:17])
// If we're waiting for this sensor's config, store it
if s.pendingConfigRequest && s.pendingConfigSensor == sensorID {
s.receivedConfig = SensorConfig{
ChangeSensitivityEnabled: flags&featChangeSensitivityEnabled != 0,
ChangeSensitivityRelative: flags&featChangeSensitivityRelative != 0,
WakeupEnabled: flags&featWakeEnabled != 0,
AlwaysOnEnabled: flags&featAlwaysOnEnabled != 0,
ChangeSensitivity: changeSensitivity,
ReportInterval: reportInterval,
BatchInterval: batchInterval,
SensorSpecific: sensorSpecific,
}
s.configReady = true
}
}
// handleSensor processes sensor report messages.
// The payload can contain multiple sensor reports batched together.
func (s *sh2Protocol) handleSensor(payload []byte, timestamp uint32) {
cursor := 0
var referenceDelta uint32
for cursor < len(payload) {
if cursor >= len(payload) {
break
}
reportID := payload[cursor]
reportLen := getReportLen(reportID)
if reportLen == 0 {
// Unknown report ID
break
}
if cursor+reportLen > len(payload) {
// Not enough data for this report
break
}
// Handle special report types
switch reportID {
case 0xFB: // SENSORHUB_BASE_TIMESTAMP_REF
if reportLen >= 5 {
// Extract timebase (little-endian uint32)
timebase := binary.LittleEndian.Uint32(payload[cursor+1 : cursor+5])
referenceDelta = -timebase // Store negative for delta calculation
}
case 0xFA: // SENSORHUB_TIMESTAMP_REBASE
if reportLen >= 5 {
timebase := binary.LittleEndian.Uint32(payload[cursor+1 : cursor+5])
referenceDelta += timebase
}
case 0xF1: // SENSORHUB_FLUSH_COMPLETED
// Route to control handler
s.handleControl(payload[cursor:cursor+reportLen], timestamp)
default:
// Regular sensor report
value, ok := decodeSensor(payload[cursor:cursor+reportLen], timestamp)
if ok {
s.device.enqueue(value)
}
}
cursor += reportLen
}
} // handleExecutable processes executable channel messages.
func (s *sh2Protocol) handleExecutable(payload []byte, timestamp uint32) {
if len(payload) == 0 {
return
}
reportID := payload[0]
switch reportID {
case execDeviceRespResetComplete:
s.device.lastReset = true
}
}
+83
View File
@@ -0,0 +1,83 @@
// SHTP specification found at https://www.ceva-ip.com/wp-content/uploads/SH-2-SHTP-Reference-Manual.pdf
package bno08x
import "encoding/binary"
// shtpHandler is a callback for handling SHTP channel data.
type shtpHandler func(payload []byte, timestamp uint32)
// shtp implements the Sensor Hub Transport Protocol layer.
type shtp struct {
hal *hal
handlers map[uint8]shtpHandler
seq [8]uint8
rx [maxTransferIn]byte // Reusable receive buffer
tx [maxTransferOut]byte // Reusable transmit buffer
}
func newSHTP(hal *hal) *shtp {
return &shtp{
hal: hal,
handlers: make(map[uint8]shtpHandler),
}
}
// register registers a handler for a specific SHTP channel.
func (s *shtp) register(channel uint8, handler shtpHandler) {
if handler == nil {
delete(s.handlers, channel)
return
}
s.handlers[channel] = handler
}
// send transmits a payload on the specified channel.
func (s *shtp) send(channel uint8, payload []byte) error {
total := len(payload) + shtpHeaderLength
if total > maxTransferOut {
return errFrameTooLarge
}
// Use pre-allocated transmit buffer to avoid allocations
frame := s.tx[:total]
binary.LittleEndian.PutUint16(frame[0:2], uint16(total))
frame[2] = channel
frame[3] = s.seq[channel]
s.seq[channel]++
copy(frame[shtpHeaderLength:], payload)
_, err := s.hal.write(frame)
return err
}
// poll checks for and processes incoming SHTP packets.
// Returns true if a packet was processed, false if no data available.
func (s *shtp) poll() (bool, error) {
n, timestamp, err := s.hal.read(s.rx[:])
if err != nil {
return false, err
}
if n == 0 {
return false, nil
}
packet := s.rx[:n]
length := int(binary.LittleEndian.Uint16(packet[0:2]) & ^uint16(continueMask))
if length > n {
length = n
}
if length < shtpHeaderLength {
return false, nil
}
channel := packet[2]
// seq := packet[3] // sequence number, not currently validated
payload := packet[shtpHeaderLength:length]
if handler := s.handlers[channel]; handler != nil {
handler(payload, timestamp)
}
return true, nil
}
+572
View File
@@ -0,0 +1,572 @@
package bno08x
// SensorID identifies a specific sensor type.
type SensorID uint8
// Sensor IDs as defined in the SH-2 specification.
const (
SensorRawAccelerometer SensorID = 0x14
SensorAccelerometer SensorID = 0x01
SensorLinearAcceleration SensorID = 0x04
SensorGravity SensorID = 0x06
SensorRawGyroscope SensorID = 0x15
SensorGyroscope SensorID = 0x02
SensorGyroscopeUncalibrated SensorID = 0x07
SensorRawMagnetometer SensorID = 0x16
SensorMagneticField SensorID = 0x03
SensorMagneticFieldUncalibrated SensorID = 0x0F
SensorRotationVector SensorID = 0x05
SensorGameRotationVector SensorID = 0x08
SensorGeomagneticRotationVector SensorID = 0x09
SensorPressure SensorID = 0x0A
SensorAmbientLight SensorID = 0x0B
SensorHumidity SensorID = 0x0C
SensorProximity SensorID = 0x0D
SensorTemperature SensorID = 0x0E
SensorReserved SensorID = 0x17
SensorTapDetector SensorID = 0x10
SensorStepDetector SensorID = 0x18
SensorStepCounter SensorID = 0x11
SensorSignificantMotion SensorID = 0x12
SensorStabilityClassifier SensorID = 0x13
SensorShakeDetector SensorID = 0x19
SensorFlipDetector SensorID = 0x1A
SensorPickupDetector SensorID = 0x1B
SensorStabilityDetector SensorID = 0x1C
SensorPersonalActivityClassifier SensorID = 0x1E
SensorSleepDetector SensorID = 0x1F
SensorTiltDetector SensorID = 0x20
SensorPocketDetector SensorID = 0x21
SensorCircleDetector SensorID = 0x22
SensorHeartRateMonitor SensorID = 0x23
SensorARVRStabilizedRV SensorID = 0x28
SensorARVRStabilizedGRV SensorID = 0x29
SensorGyroIntegratedRV SensorID = 0x2A
SensorIZROMotionRequest SensorID = 0x2B
SensorMaxID SensorID = 0x2B
)
// ProductID contains firmware information from the sensor.
type ProductID struct {
ResetCause uint8
VersionMajor uint8
VersionMinor uint8
PartNumber uint32
BuildNumber uint32
VersionPatch uint16
Reserved0 uint8
Reserved1 uint8
}
// ProductIDs holds all product ID entries returned by the sensor.
type ProductIDs struct {
Entries [5]ProductID
NumEntries uint8
}
// Vector3 represents a 3D vector.
type Vector3 struct {
X float32
Y float32
Z float32
}
// Quaternion represents a quaternion in (real, i, j, k) format.
// Note: This maps to (w, x, y, z) convention where w=real, x=i, y=j, z=k.
type Quaternion struct {
Real float32
I float32
J float32
K float32
}
// RawVector3 contains raw ADC counts with timestamp.
type RawVector3 struct {
X int16
Y int16
Z int16
Timestamp uint32
}
// RawGyroscope contains raw gyro readings with temperature and timestamp.
type RawGyroscope struct {
X int16
Y int16
Z int16
Temperature int16
Timestamp uint32
}
// GyroscopeUncalibrated contains uncalibrated gyroscope data with bias.
type GyroscopeUncalibrated struct {
X float32
Y float32
Z float32
BiasX float32
BiasY float32
BiasZ float32
}
// MagneticFieldUncalibrated contains uncalibrated magnetometer data with bias.
type MagneticFieldUncalibrated struct {
X float32
Y float32
Z float32
BiasX float32
BiasY float32
BiasZ float32
}
// TapDetector contains tap/double-tap detection flags.
type TapDetector struct {
Flags uint8
}
// StepDetector contains step detection with latency.
type StepDetector struct {
Latency uint32
}
// StepCounter contains step count with latency.
type StepCounter struct {
Count uint16
Latency uint32
}
// SignificantMotion indicates significant motion was detected.
type SignificantMotion struct {
Motion uint16
}
// ActivityClassification contains activity classification data.
type ActivityClassification struct {
Page uint8
MostLikelyState uint8
Classification [10]uint8
EndOfPage uint8
}
// ShakeDetector contains shake detection data.
type ShakeDetector struct {
Shake uint16
}
// StabilityClassifier contains stability classification.
type StabilityClassifier struct {
Classification uint8
}
// PersonalActivityClassifier contains personal activity data.
type PersonalActivityClassifier struct {
Page uint8
MostLikelyState uint8
Confidence [10]uint8
EndOfPage uint8
}
// SensorValue contains decoded sensor data for all sensor types.
type SensorValue struct {
id SensorID
status uint8
sequence uint8
delay uint8
timestamp uint64
// Orientation data (quaternions)
quaternion Quaternion
quaternionAccuracy float32
// Linear measurements
accelerometer Vector3
linearAcceleration Vector3
gravity Vector3
gyroscope Vector3
gyroscopeUncal GyroscopeUncalibrated
magneticField Vector3
magneticFieldUncal MagneticFieldUncalibrated
// Raw sensor data
rawAccelerometer RawVector3
rawGyroscope RawGyroscope
rawMagnetometer RawVector3
// Environmental sensors
pressure float32 // hPa
ambientLight float32 // lux
humidity float32 // %
proximity float32 // cm
temperature float32 // °C
// Activity detection
tapDetector TapDetector
stepCounter StepCounter
stepDetector StepDetector
significantMotion SignificantMotion
shakeDetector ShakeDetector
flipDetector uint16
stabilityClassifier StabilityClassifier
stabilityDetector uint8
activityClassifier ActivityClassification
personalActivityClassifier PersonalActivityClassifier
sleepDetector uint8
tiltDetector uint8
pocketDetector uint8
circleDetector uint8
heartRateMonitor uint16
}
// SensorConfig holds configuration settings for a sensor.
type SensorConfig struct {
ChangeSensitivityEnabled bool
ChangeSensitivityRelative bool
WakeupEnabled bool
AlwaysOnEnabled bool
ChangeSensitivity uint16
ReportInterval uint32 // microseconds
BatchInterval uint32 // microseconds
SensorSpecific uint32
}
// Error represents a driver error.
type Error string
func (e Error) Error() string { return string(e) }
// Error constants.
var (
errBufferTooSmall = Error("bno08x: buffer too small")
errNoEvent = Error("bno08x: no sensor event available")
errTimeout = Error("bno08x: operation timed out")
errFrameTooLarge = Error("bno08x: frame exceeds maximum size")
errNoBus = Error("bno08x: I2C bus not configured")
errInvalidParam = Error("bno08x: invalid parameter")
errHubError = Error("bno08x: sensor hub error")
errIO = Error("bno08x: I/O error")
)
// Metadata accessor methods (always available for any sensor type)
// ID returns the sensor ID.
func (sv SensorValue) ID() SensorID {
return sv.id
}
// Status returns the sensor status flags.
func (sv SensorValue) Status() uint8 {
return sv.status
}
// Sequence returns the sequence number.
func (sv SensorValue) Sequence() uint8 {
return sv.sequence
}
// Delay returns the sensor delay value.
func (sv SensorValue) Delay() uint8 {
return sv.delay
}
// Timestamp returns the sensor timestamp.
func (sv SensorValue) Timestamp() uint64 {
return sv.timestamp
}
// Orientation data accessor methods
// Quaternion returns the quaternion value for rotation vector sensors.
// Panics if called on a sensor type that doesn't provide quaternion data.
func (sv SensorValue) Quaternion() Quaternion {
switch sv.id {
case SensorRotationVector, SensorGameRotationVector, SensorGeomagneticRotationVector,
SensorARVRStabilizedRV, SensorARVRStabilizedGRV, SensorGyroIntegratedRV:
return sv.quaternion
default:
panic("bno08x: Quaternion() called on non-rotation sensor type")
}
}
// QuaternionAccuracy returns the quaternion accuracy estimate.
// Panics if called on a sensor type that doesn't provide quaternion accuracy.
func (sv SensorValue) QuaternionAccuracy() float32 {
switch sv.id {
case SensorRotationVector, SensorGeomagneticRotationVector, SensorARVRStabilizedRV:
return sv.quaternionAccuracy
default:
panic("bno08x: QuaternionAccuracy() called on sensor type without accuracy data")
}
}
// Linear measurement accessor methods
// Accelerometer returns the accelerometer vector.
// Panics if called on a sensor type other than SensorAccelerometer.
func (sv SensorValue) Accelerometer() Vector3 {
if sv.id != SensorAccelerometer {
panic("bno08x: Accelerometer() called on non-accelerometer sensor type")
}
return sv.accelerometer
}
// LinearAcceleration returns the linear acceleration vector.
// Panics if called on a sensor type other than SensorLinearAcceleration.
func (sv SensorValue) LinearAcceleration() Vector3 {
if sv.id != SensorLinearAcceleration {
panic("bno08x: LinearAcceleration() called on wrong sensor type")
}
return sv.linearAcceleration
}
// Gravity returns the gravity vector.
// Panics if called on a sensor type other than SensorGravity.
func (sv SensorValue) Gravity() Vector3 {
if sv.id != SensorGravity {
panic("bno08x: Gravity() called on non-gravity sensor type")
}
return sv.gravity
}
// Gyroscope returns the gyroscope vector.
// Panics if called on a sensor type other than SensorGyroscope.
func (sv SensorValue) Gyroscope() Vector3 {
if sv.id != SensorGyroscope {
panic("bno08x: Gyroscope() called on non-gyroscope sensor type")
}
return sv.gyroscope
}
// GyroscopeUncal returns the uncalibrated gyroscope data.
// Panics if called on a sensor type other than SensorGyroscopeUncalibrated.
func (sv SensorValue) GyroscopeUncal() GyroscopeUncalibrated {
if sv.id != SensorGyroscopeUncalibrated {
panic("bno08x: GyroscopeUncal() called on wrong sensor type")
}
return sv.gyroscopeUncal
}
// MagneticField returns the magnetic field vector.
// Panics if called on a sensor type other than SensorMagneticField.
func (sv SensorValue) MagneticField() Vector3 {
if sv.id != SensorMagneticField {
panic("bno08x: MagneticField() called on wrong sensor type")
}
return sv.magneticField
}
// MagneticFieldUncal returns the uncalibrated magnetic field data.
// Panics if called on a sensor type other than SensorMagneticFieldUncalibrated.
func (sv SensorValue) MagneticFieldUncal() MagneticFieldUncalibrated {
if sv.id != SensorMagneticFieldUncalibrated {
panic("bno08x: MagneticFieldUncal() called on wrong sensor type")
}
return sv.magneticFieldUncal
}
// Raw sensor data accessor methods
// RawAccelerometer returns the raw accelerometer data.
// Panics if called on a sensor type other than SensorRawAccelerometer.
func (sv SensorValue) RawAccelerometer() RawVector3 {
if sv.id != SensorRawAccelerometer {
panic("bno08x: RawAccelerometer() called on wrong sensor type")
}
return sv.rawAccelerometer
}
// RawGyroscope returns the raw gyroscope data.
// Panics if called on a sensor type other than SensorRawGyroscope.
func (sv SensorValue) RawGyroscope() RawGyroscope {
if sv.id != SensorRawGyroscope {
panic("bno08x: RawGyroscope() called on wrong sensor type")
}
return sv.rawGyroscope
}
// RawMagnetometer returns the raw magnetometer data.
// Panics if called on a sensor type other than SensorRawMagnetometer.
func (sv SensorValue) RawMagnetometer() RawVector3 {
if sv.id != SensorRawMagnetometer {
panic("bno08x: RawMagnetometer() called on wrong sensor type")
}
return sv.rawMagnetometer
}
// Environmental sensor accessor methods
// Pressure returns the pressure reading in hPa.
// Panics if called on a sensor type other than SensorPressure.
func (sv SensorValue) Pressure() float32 {
if sv.id != SensorPressure {
panic("bno08x: Pressure() called on non-pressure sensor type")
}
return sv.pressure
}
// AmbientLight returns the ambient light reading in lux.
// Panics if called on a sensor type other than SensorAmbientLight.
func (sv SensorValue) AmbientLight() float32 {
if sv.id != SensorAmbientLight {
panic("bno08x: AmbientLight() called on wrong sensor type")
}
return sv.ambientLight
}
// Humidity returns the humidity reading in percent.
// Panics if called on a sensor type other than SensorHumidity.
func (sv SensorValue) Humidity() float32 {
if sv.id != SensorHumidity {
panic("bno08x: Humidity() called on non-humidity sensor type")
}
return sv.humidity
}
// Proximity returns the proximity reading in cm.
// Panics if called on a sensor type other than SensorProximity.
func (sv SensorValue) Proximity() float32 {
if sv.id != SensorProximity {
panic("bno08x: Proximity() called on non-proximity sensor type")
}
return sv.proximity
}
// Temperature returns the temperature reading in °C.
// Panics if called on a sensor type other than SensorTemperature.
func (sv SensorValue) Temperature() float32 {
if sv.id != SensorTemperature {
panic("bno08x: Temperature() called on non-temperature sensor type")
}
return sv.temperature
}
// Activity detection accessor methods
// TapDetector returns the tap detector data.
// Panics if called on a sensor type other than SensorTapDetector.
func (sv SensorValue) TapDetector() TapDetector {
if sv.id != SensorTapDetector {
panic("bno08x: TapDetector() called on wrong sensor type")
}
return sv.tapDetector
}
// StepCounter returns the step counter value.
// Panics if called on a sensor type other than SensorStepCounter.
func (sv SensorValue) StepCounter() StepCounter {
if sv.id != SensorStepCounter {
panic("bno08x: StepCounter() called on wrong sensor type")
}
return sv.stepCounter
}
// StepDetector returns the step detector data.
// Panics if called on a sensor type other than SensorStepDetector.
func (sv SensorValue) StepDetector() StepDetector {
if sv.id != SensorStepDetector {
panic("bno08x: StepDetector() called on wrong sensor type")
}
return sv.stepDetector
}
// SignificantMotion returns the significant motion data.
// Panics if called on a sensor type other than SensorSignificantMotion.
func (sv SensorValue) SignificantMotion() SignificantMotion {
if sv.id != SensorSignificantMotion {
panic("bno08x: SignificantMotion() called on wrong sensor type")
}
return sv.significantMotion
}
// ShakeDetector returns the shake detector data.
// Panics if called on a sensor type other than SensorShakeDetector.
func (sv SensorValue) ShakeDetector() ShakeDetector {
if sv.id != SensorShakeDetector {
panic("bno08x: ShakeDetector() called on wrong sensor type")
}
return sv.shakeDetector
}
// FlipDetector returns the flip detector data.
// Panics if called on a sensor type other than SensorFlipDetector.
func (sv SensorValue) FlipDetector() uint16 {
if sv.id != SensorFlipDetector {
panic("bno08x: FlipDetector() called on wrong sensor type")
}
return sv.flipDetector
}
// StabilityClassifier returns the stability classifier data.
// Panics if called on a sensor type other than SensorStabilityClassifier.
func (sv SensorValue) StabilityClassifier() StabilityClassifier {
if sv.id != SensorStabilityClassifier {
panic("bno08x: StabilityClassifier() called on wrong sensor type")
}
return sv.stabilityClassifier
}
// StabilityDetector returns the stability detector value.
// Panics if called on a sensor type other than SensorStabilityDetector.
func (sv SensorValue) StabilityDetector() uint8 {
if sv.id != SensorStabilityDetector {
panic("bno08x: StabilityDetector() called on wrong sensor type")
}
return sv.stabilityDetector
}
// ActivityClassifier returns the activity classification data.
// Note: This field appears unused in decode.go, keeping for API compatibility.
func (sv SensorValue) ActivityClassifier() ActivityClassification {
return sv.activityClassifier
}
// PersonalActivityClassifier returns the personal activity classifier data.
// Panics if called on a sensor type other than SensorPersonalActivityClassifier.
func (sv SensorValue) PersonalActivityClassifier() PersonalActivityClassifier {
if sv.id != SensorPersonalActivityClassifier {
panic("bno08x: PersonalActivityClassifier() called on wrong sensor type")
}
return sv.personalActivityClassifier
}
// SleepDetector returns the sleep detector value.
// Panics if called on a sensor type other than SensorSleepDetector.
func (sv SensorValue) SleepDetector() uint8 {
if sv.id != SensorSleepDetector {
panic("bno08x: SleepDetector() called on wrong sensor type")
}
return sv.sleepDetector
}
// TiltDetector returns the tilt detector value.
// Panics if called on a sensor type other than SensorTiltDetector.
func (sv SensorValue) TiltDetector() uint8 {
if sv.id != SensorTiltDetector {
panic("bno08x: TiltDetector() called on wrong sensor type")
}
return sv.tiltDetector
}
// PocketDetector returns the pocket detector value.
// Panics if called on a sensor type other than SensorPocketDetector.
func (sv SensorValue) PocketDetector() uint8 {
if sv.id != SensorPocketDetector {
panic("bno08x: PocketDetector() called on wrong sensor type")
}
return sv.pocketDetector
}
// CircleDetector returns the circle detector value.
// Panics if called on a sensor type other than SensorCircleDetector.
func (sv SensorValue) CircleDetector() uint8 {
if sv.id != SensorCircleDetector {
panic("bno08x: CircleDetector() called on wrong sensor type")
}
return sv.circleDetector
}
// HeartRateMonitor returns the heart rate monitor value.
// Panics if called on a sensor type other than SensorHeartRateMonitor.
func (sv SensorValue) HeartRateMonitor() uint16 {
if sv.id != SensorHeartRateMonitor {
panic("bno08x: HeartRateMonitor() called on wrong sensor type")
}
return sv.heartRateMonitor
}
+7 -7
View File
@@ -2,22 +2,22 @@
package buzzer // import "tinygo.org/x/drivers/buzzer"
import (
"machine"
"time"
"tinygo.org/x/drivers/internal/pin"
)
// Device wraps a GPIO connection to a buzzer.
type Device struct {
pin machine.Pin
pin pin.OutputFunc
High bool
BPM float64
}
// New returns a new buzzer driver given which pin to use
func New(pin machine.Pin) Device {
func New(pin pin.Output) Device {
return Device{
pin: pin,
pin: pin.Set,
High: false,
BPM: 96.0,
}
@@ -25,14 +25,14 @@ func New(pin machine.Pin) Device {
// On sets the buzzer to a high state.
func (l *Device) On() (err error) {
l.pin.Set(true)
l.pin.High()
l.High = true
return
}
// Off sets the buzzer to a low state.
func (l *Device) Off() (err error) {
l.pin.Set(false)
l.pin.Low()
l.High = false
return
}
+288 -36
View File
@@ -5,10 +5,12 @@
package ds3231 // import "tinygo.org/x/drivers/ds3231"
import (
"encoding/binary"
"errors"
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/legacy"
"tinygo.org/x/drivers/internal/regmap"
)
type Mode uint8
@@ -17,6 +19,7 @@ type Mode uint8
type Device struct {
bus drivers.I2C
Address uint16
d regmap.Device8I2C
}
// New creates a new DS3231 connection. The I2C bus must already be
@@ -24,54 +27,50 @@ type Device struct {
//
// This function only creates the Device object, it does not touch the device.
func New(bus drivers.I2C) Device {
return Device{
d := Device{
bus: bus,
Address: Address,
}
d.Configure()
return d
}
// Configure sets up the device for communication
func (d *Device) Configure() bool {
d.d.SetBus(d.bus, d.Address, binary.BigEndian)
return true
}
// IsTimeValid return true/false is the time in the device is valid
func (d *Device) IsTimeValid() bool {
data := []byte{0}
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_STATUS, data)
status, err := d.d.Read8(REG_STATUS)
if err != nil {
return false
}
return (data[0] & (1 << OSF)) == 0x00
return (status & (1 << OSF)) == 0x00
}
// IsRunning returns if the oscillator is running
func (d *Device) IsRunning() bool {
data := []uint8{0}
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CONTROL, data)
control, err := d.d.Read8(REG_CONTROL)
if err != nil {
return false
}
return (data[0] & (1 << EOSC)) == 0x00
return (control & (1 << EOSC)) == 0x00
}
// SetRunning starts the internal oscillator
func (d *Device) SetRunning(isRunning bool) error {
data := []uint8{0}
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CONTROL, data)
control, err := d.d.Read8(REG_CONTROL)
if err != nil {
return err
}
if isRunning {
data[0] &^= uint8(1 << EOSC)
control &^= uint8(1 << EOSC)
} else {
data[0] |= 1 << EOSC
control |= 1 << EOSC
}
err = legacy.WriteRegister(d.bus, uint8(d.Address), REG_CONTROL, data)
if err != nil {
return err
}
return nil
return d.d.Write8(REG_CONTROL, control)
}
// SetTime sets the date and time in the DS3231. The DS3231 hardware supports
@@ -86,18 +85,16 @@ func (d *Device) SetRunning(isRunning bool) error {
// 2100 as a leap year, causing it to increment from 2100-02-28 to 2100-02-29
// instead of 2100-03-01.
func (d *Device) SetTime(dt time.Time) error {
data := []byte{0}
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_STATUS, data)
status, err := d.d.Read8(REG_STATUS)
if err != nil {
return err
}
data[0] &^= 1 << OSF
err = legacy.WriteRegister(d.bus, uint8(d.Address), REG_STATUS, data)
if err != nil {
status &^= 1 << OSF
if err = d.d.Write8(REG_STATUS, status); err != nil {
return err
}
data = make([]uint8, 7)
data := make([]uint8, 7)
data[0] = uint8ToBCD(uint8(dt.Second()))
data[1] = uint8ToBCD(uint8(dt.Minute()))
data[2] = uint8ToBCD(uint8(dt.Hour()))
@@ -118,21 +115,16 @@ func (d *Device) SetTime(dt time.Time) error {
data[5] = uint8ToBCD(uint8(dt.Month()) | centuryFlag)
data[6] = uint8ToBCD(year)
err = legacy.WriteRegister(d.bus, uint8(d.Address), REG_TIMEDATE, data)
if err != nil {
return err
}
return nil
return d.bus.Tx(d.Address, append([]byte{REG_TIMEDATE}, data...), nil)
}
// ReadTime returns the date and time
func (d *Device) ReadTime() (dt time.Time, err error) {
data := make([]uint8, 7)
err = legacy.ReadRegister(d.bus, uint8(d.Address), REG_TIMEDATE, data)
if err != nil {
if err = d.d.ReadData(REG_TIMEDATE, data); err != nil {
return
}
second := bcdToInt(data[0] & 0x7F)
minute := bcdToInt(data[1])
hour := hoursBCDToInt(data[2])
@@ -150,12 +142,264 @@ func (d *Device) ReadTime() (dt time.Time, err error) {
// ReadTemperature returns the temperature in millicelsius (mC)
func (d *Device) ReadTemperature() (int32, error) {
data := make([]uint8, 2)
err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_TEMP, data)
temp, err := d.d.Read16(REG_TEMP)
if err != nil {
return 0, err
}
return milliCelsius(data[0], data[1]), nil
return milliCelsius(temp), nil
}
// GetSqwPinMode returns the current square wave output frequency
func (d *Device) GetSqwPinMode() SqwPinMode {
control, err := d.d.Read8(REG_CONTROL)
if err != nil {
return SQW_OFF
}
control &= 0x1C // turn off INTCON
if control&0x04 != 0 {
return SQW_OFF
}
return SqwPinMode(control)
}
// SetSqwPinMode sets the square wave output mode to the given frequency
func (d *Device) SetSqwPinMode(mode SqwPinMode) error {
control, err := d.d.Read8(REG_CONTROL)
if err != nil {
return err
}
control &^= 0x04 // turn off INTCON
control &^= 0x18 // set freq bits to 0
control |= uint8(mode)
return d.d.Write8(REG_CONTROL, control)
}
// SetAlarm1 sets alarm1 to the given time and mode
func (d *Device) SetAlarm1(dt time.Time, mode Alarm1Mode) error {
control, err := d.d.Read8(REG_CONTROL)
if err != nil {
return err
}
if control&(1<<INTCN) == 0x00 {
return errors.New("INTCN has to be disabled")
}
A1M1 := uint8((mode & 0x01) << 7)
A1M2 := uint8((mode & 0x02) << 6)
A1M3 := uint8((mode & 0x04) << 5)
A1M4 := uint8((mode & 0x08) << 4)
DY_DT := uint8((mode & 0x10) << 2)
day := dt.Day()
if DY_DT > 0 {
day = dowToDS3231(int(dt.Weekday()))
}
alarm1 := uint32(uint8ToBCD(uint8(dt.Second()))|A1M1) << 24
alarm1 |= uint32(uint8ToBCD(uint8(dt.Minute()))|A1M2) << 16
alarm1 |= uint32(uint8ToBCD(uint8(dt.Hour()))|A1M3) << 8
alarm1 |= uint32(uint8ToBCD(uint8(day)) | A1M4 | DY_DT)
if err := d.d.Write32(REG_ALARMONE, alarm1); err != nil {
return err
}
control |= AlarmFlag_Alarm1
return d.d.Write8(REG_CONTROL, control)
}
// ReadAlarm1 returns the alarm1 time
func (d *Device) ReadAlarm1() (dt time.Time, err error) {
data := make([]uint8, 4)
if err = d.d.ReadData(REG_ALARMONE, data); err != nil {
return
}
second := bcdToInt(data[0] & 0x7F)
minute := bcdToInt(data[1] & 0x7F)
hour := hoursBCDToInt(data[2] & 0x3F)
isDayOfWeek := (data[3] & 0x40) >> 6
var day int
if isDayOfWeek > 0 {
day = bcdToInt(data[3] & 0x0F)
} else {
day = bcdToInt(data[3] & 0x3F)
}
dt = time.Date(2000, 5, day, hour, minute, second, 0, time.UTC)
return
}
// SetAlarm2 sets alarm2 to the given time and mode
func (d *Device) SetAlarm2(dt time.Time, mode Alarm2Mode) error {
control, err := d.d.Read8(REG_CONTROL)
if err != nil {
return err
}
if control&(1<<INTCN) == 0x00 {
return errors.New("INTCN has to be disabled")
}
A2M2 := uint8((mode & 0x01) << 7)
A2M3 := uint8((mode & 0x02) << 6)
A2M4 := uint8((mode & 0x04) << 5)
DY_DT := uint8((mode & 0x08) << 3)
day := dt.Day()
if DY_DT > 0 {
day = dowToDS3231(int(dt.Weekday()))
}
data := make([]uint8, 4)
data[0] = uint8ToBCD(uint8(dt.Minute())) | A2M2
data[1] = uint8ToBCD(uint8(dt.Hour())) | A2M3
data[2] = uint8ToBCD(uint8(day)) | A2M4 | DY_DT
if err = d.bus.Tx(d.Address, append([]byte{REG_ALARMTWO}, data...), nil); err != nil {
return err
}
control |= AlarmFlag_Alarm2
return d.d.Write8(REG_CONTROL, control)
}
// ReadAlarm2 returns the alarm2 time
func (d *Device) ReadAlarm2() (dt time.Time, err error) {
data := make([]uint8, 3)
if err = d.d.ReadData(REG_ALARMTWO, data); err != nil {
return
}
minute := bcdToInt(data[0] & 0x7F)
hour := hoursBCDToInt(data[1] & 0x3F)
isDayOfWeek := (data[2] & 0x40) >> 6
var day int
if isDayOfWeek > 0 {
day = bcdToInt(data[2] & 0x0F)
} else {
day = bcdToInt(data[2] & 0x3F)
}
dt = time.Date(2000, 5, day, hour, minute, 0, 0, time.UTC)
return
}
// IsEnabledAlarm1 returns true when alarm1 is enabled
func (d *Device) IsEnabledAlarm1() bool {
return d.isEnabledAlarm(1)
}
// SetEnabledAlarm1 sets the enabled status of alarm1
func (d *Device) SetEnabledAlarm1(enable bool) error {
if enable {
return d.enableAlarm(1)
}
return d.disableAlarm(1)
}
// IsEnabledAlarm2 returns true when alarm2 is enabled
func (d *Device) IsEnabledAlarm2() bool {
return d.isEnabledAlarm(2)
}
// SetEnabledAlarm2 sets the enabled status of alarm2
func (d *Device) SetEnabledAlarm2(enable bool) error {
if enable {
return d.enableAlarm(2)
}
return d.disableAlarm(2)
}
// ClearAlarm1 clears status of alarm1
func (d *Device) ClearAlarm1() error {
return d.clearAlarm(1)
}
// ClearAlarm2 clears status of alarm2
func (d *Device) ClearAlarm2() error {
return d.clearAlarm(2)
}
// IsAlarm1Fired returns true when alarm1 is firing
func (d *Device) IsAlarm1Fired() bool {
return d.isAlarmFired(1)
}
// IsAlarm2Fired returns true when alarm2 is firing
func (d *Device) IsAlarm2Fired() bool {
return d.isAlarmFired(2)
}
// SetEnabled32K sets the enabled status of the 32KHz output
func (d *Device) SetEnabled32K(enable bool) error {
status, err := d.d.Read8(REG_STATUS)
if err != nil {
return err
}
if enable {
status |= 1 << EN32KHZ
} else {
status &^= 1 << EN32KHZ
}
return d.d.Write8(REG_STATUS, status)
}
// IsEnabled32K returns true when the 32KHz output is enabled
func (d *Device) IsEnabled32K() bool {
status, err := d.d.Read8(REG_STATUS)
if err != nil {
return false
}
return (status & (1 << EN32KHZ)) != 0x00
}
func (d *Device) disableAlarm(alarm_num uint8) error {
control, err := d.d.Read8(REG_CONTROL)
if err != nil {
return err
}
control &^= (1 << (alarm_num - 1))
return d.d.Write8(REG_CONTROL, control)
}
func (d *Device) enableAlarm(alarm_num uint8) error {
control, err := d.d.Read8(REG_CONTROL)
if err != nil {
return err
}
control |= (1 << (alarm_num - 1))
return d.d.Write8(REG_CONTROL, control)
}
func (d *Device) isEnabledAlarm(alarm_num uint8) bool {
control, err := d.d.Read8(REG_CONTROL)
if err != nil {
return false
}
return (control & (1 << (alarm_num - 1))) != 0x00
}
func (d *Device) clearAlarm(alarm_num uint8) error {
status, err := d.d.Read8(REG_STATUS)
if err != nil {
return err
}
status &^= (1 << (alarm_num - 1))
return d.d.Write8(REG_STATUS, status)
}
func (d *Device) isAlarmFired(alarm_num uint8) bool {
status, err := d.d.Read8(REG_STATUS)
if err != nil {
return false
}
return (status & (1 << (alarm_num - 1))) != 0x00
}
// milliCelsius converts the raw temperature bytes (msb and lsb) from the DS3231
@@ -172,8 +416,8 @@ func (d *Device) ReadTemperature() (int32, error) {
// 16-bit signed integer in units of centi Celsius (1/100 deg C) with no loss of
// precision or dynamic range. But for backwards compatibility, let's instead
// convert this into a 32-bit signed integer in units of milli Celsius.
func milliCelsius(msb uint8, lsb uint8) int32 {
t256 := int16(uint16(msb)<<8 | uint16(lsb))
func milliCelsius(tempBytes uint16) int32 {
t256 := int16(uint16(tempBytes>>8)<<8 | uint16(tempBytes&0xFF))
t1000 := int32(t256) / 64 * 250
return t1000
}
@@ -200,3 +444,11 @@ func hoursBCDToInt(value uint8) (hour int) {
}
return
}
// dowToDS3231 converts the day of the week to internal DS3231 format
func dowToDS3231(d int) int {
if d == 0 {
return 7
}
return d
}
+13 -13
View File
@@ -5,71 +5,71 @@ import (
)
func TestPositiveMilliCelsius(t *testing.T) {
t1000 := milliCelsius(0, 0)
t1000 := milliCelsius(0)
if t1000 != 0 {
t.Fatal(t1000)
}
t1000 = milliCelsius(0, 0b01000000)
t1000 = milliCelsius(0b0000000001000000)
if t1000 != 250 {
t.Fatal(t1000)
}
t1000 = milliCelsius(0, 0b10000000)
t1000 = milliCelsius(0b0000000010000000)
if t1000 != 500 {
t.Fatal(t1000)
}
t1000 = milliCelsius(0, 0b11000000)
t1000 = milliCelsius(0b0000000011000000)
if t1000 != 750 {
t.Fatal(t1000)
}
t1000 = milliCelsius(1, 0b00000000)
t1000 = milliCelsius(0b0000000100000000)
if t1000 != 1000 {
t.Fatal(t1000)
}
t1000 = milliCelsius(2, 0b00000000)
t1000 = milliCelsius(0b0000001000000000)
if t1000 != 2000 {
t.Fatal(t1000)
}
// highest temperature is 127.750C
t1000 = milliCelsius(0x7f, 0b11000000)
t1000 = milliCelsius(0b0111111111000000)
if t1000 != 127750 {
t.Fatal(t1000)
}
}
func TestNegativeMilliCelsius(t *testing.T) {
t1000 := milliCelsius(0xff, 0b11000000)
t1000 := milliCelsius(0b1111111111000000)
if t1000 != -250 {
t.Fatal(t1000)
}
t1000 = milliCelsius(0xff, 0b10000000)
t1000 = milliCelsius(0b1111111110000000)
if t1000 != -500 {
t.Fatal(t1000)
}
t1000 = milliCelsius(0xff, 0b01000000)
t1000 = milliCelsius(0b1111111101000000)
if t1000 != -750 {
t.Fatal(t1000)
}
t1000 = milliCelsius(0xff, 0b00000000)
t1000 = milliCelsius(0b1111111100000000)
if t1000 != -1000 {
t.Fatal(t1000)
}
t1000 = milliCelsius(0xfe, 0b00000000)
t1000 = milliCelsius(0b1111111000000000)
if t1000 != -2000 {
t.Fatal(t1000)
}
// lowest temperature is -128.000C
t1000 = milliCelsius(0x80, 0b00000000)
t1000 = milliCelsius(0b1000000000000000)
if t1000 != -128000 {
t.Fatal(t1000)
}
+49
View File
@@ -46,3 +46,52 @@ const (
AlarmTwo Mode = 4
ModeAlarmBoth Mode = 5
)
// SQW Pin Modes
type SqwPinMode uint8
const (
SQW_OFF SqwPinMode = 0x1C
SQW_1HZ SqwPinMode = 0x00
SQW_1KHZ SqwPinMode = 0x08
SQW_4KHZ SqwPinMode = 0x10
SQW_8KHZ SqwPinMode = 0x18
)
// Alarm1 Modes define which parts of the set alarm time has to match the current timestamp of the clock device for
// alarm1 to fire
type Alarm1Mode uint8
const (
// Alarm1 fires every second
A1_PER_SECOND Alarm1Mode = 0x0F
// Alarm1 fires when the seconds match
A1_SECOND Alarm1Mode = 0x0E
// Alarm1 fires when both seconds and minutes match
A1_MINUTE Alarm1Mode = 0x0C
// Alarm1 fires when seconds, minutes and hours match
A1_HOUR Alarm1Mode = 0x08
// Alarm1 fires when seconds, minutes, hours and the day of the month match
A1_DATE Alarm1Mode = 0x00
// Alarm1 fires when seconds, minutes, hours and the day of the week match
A1_DAY Alarm1Mode = 0x10
)
// Alarm2 Modes define which parts of the set alarm time has to match the current timestamp of the clock device for
// alarm2 to fire.
//
// Alarm2 only supports matching down to the minute unlike alarm1 which supports matching down to the second.
type Alarm2Mode uint8
const (
// Alarm2 fires every minute
A2_PER_MINUTE Alarm2Mode = 0x07
// Alarm2 fires when the minutes match
A2_MINUTE Alarm2Mode = 0x06
// Alarm2 fires when both minutes and hours match
A2_HOUR Alarm2Mode = 0x04
// Alarm2 fires when minutes, hours and the day of the month match
A2_DATE Alarm2Mode = 0x00
// Alarm2 fires when minutes, hours and the day of the week match
A2_DAY Alarm2Mode = 0x08
)
+66
View File
@@ -0,0 +1,66 @@
// Package main provides a basic example of using the BNO08x driver
// to read rotation vector (quaternion) data from the sensor.
package main
import (
"machine"
"time"
"tinygo.org/x/drivers/bno08x"
)
func main() {
time.Sleep(2 * time.Second) // Wait for sensor to power up
// Initialize I2C bus
i2c := machine.I2C0
err := i2c.Configure(machine.I2CConfig{
Frequency: 400 * machine.KHz,
})
if err != nil {
println("Failed to configure I2C:", err.Error())
return
}
println("Initializing BNO08x sensor...")
// Create and configure sensor using I2C
sensor := bno08x.NewI2C(i2c)
err = sensor.Configure(bno08x.Config{})
if err != nil {
println("Failed to configure sensor:", err.Error())
return
}
println("Sensor initialized successfully")
// Enable Game Rotation Vector reports at 100Hz (10000 microseconds = 10ms interval)
// Using Game Rotation Vector (0x08) to match the working channel_debug test
err = sensor.EnableReport(bno08x.SensorGameRotationVector, 10000)
if err != nil {
println("Failed to enable game rotation vector:", err.Error())
return
}
println("Reading rotation vectors...")
println("Format: Real I J K Accuracy")
// Add a delay after enabling reports (Arduino does this)
time.Sleep(100 * time.Millisecond)
// Main loop - read and display quaternion data
for {
event, ok := sensor.GetSensorEvent()
if ok && (event.ID() == bno08x.SensorRotationVector || event.ID() == bno08x.SensorGameRotationVector) {
q := event.Quaternion()
if event.ID() == bno08x.SensorRotationVector {
println(q.Real, q.I, q.J, q.K, event.QuaternionAccuracy())
} else {
// GameRotationVector doesn't have accuracy
println(q.Real, q.I, q.J, q.K)
}
}
// Arduino uses 10ms delay in loop
time.Sleep(10 * time.Millisecond)
}
}
+74
View File
@@ -0,0 +1,74 @@
// Connects to an DS3231 I2C Real Time Clock (RTC) and sets both alarms. It then repeatedly checks
// if the alarms are firing and prints out a message if that is the case.
package main
import (
"machine"
"time"
"tinygo.org/x/drivers/ds3231"
)
func main() {
machine.I2C0.Configure(machine.I2CConfig{})
rtc := ds3231.New(machine.I2C0)
rtc.Configure()
valid := rtc.IsTimeValid()
if !valid {
date := time.Date(2019, 12, 05, 20, 34, 12, 0, time.UTC)
rtc.SetTime(date)
}
// Set alarm1 so it triggers when the seconds match 59 => repeats every minute at dd:hh:mm:59
if err := rtc.SetAlarm1(time.Date(0, 0, 0, 0, 0, 59, 0, time.UTC), ds3231.A1_SECOND); err != nil {
println("Error while setting Alarm1")
}
if err := rtc.SetEnabledAlarm1(true); err != nil {
println("Error while enabling Alarm1")
}
// Set alarm2 so it triggers when the minutes match 35 => repeats every hour at dd:hh:35:ss
if err := rtc.SetAlarm2(time.Date(0, 0, 0, 0, 35, 0, 0, time.UTC), ds3231.A2_MINUTE); err != nil {
println("Error while setting Alarm2")
}
if err := rtc.SetEnabledAlarm2(true); err != nil {
println("Error while enabling Alarm2")
}
running := rtc.IsRunning()
if !running {
err := rtc.SetRunning(true)
if err != nil {
println("Error configuring RTC")
}
}
for {
dt, err := rtc.ReadTime()
if err != nil {
println("Error reading date:", err)
continue
}
a1 := rtc.IsAlarm1Fired()
a2 := rtc.IsAlarm2Fired()
println(dt.Format(time.DateTime), "A1:", a1, "A2:", a2)
if a1 {
if err := rtc.ClearAlarm1(); err != nil {
println("Error while clearing alarm1")
}
}
if a2 {
if err := rtc.ClearAlarm2(); err != nil {
println("Error while clearing alarm2")
}
}
time.Sleep(time.Second * 1)
}
}
@@ -3,10 +3,9 @@ package main
import (
"machine"
"strconv"
"time"
"fmt"
"tinygo.org/x/drivers/ds3231"
)
@@ -26,19 +25,19 @@ func main() {
if !running {
err := rtc.SetRunning(true)
if err != nil {
fmt.Println("Error configuring RTC")
println("Error configuring RTC")
}
}
for {
dt, err := rtc.ReadTime()
if err != nil {
fmt.Println("Error reading date:", err)
println("Error reading date:", err)
} else {
fmt.Printf("Date: %d/%s/%02d %02d:%02d:%02d \r\n", dt.Year(), dt.Month(), dt.Day(), dt.Hour(), dt.Minute(), dt.Second())
println(dt.Format(time.DateTime))
}
temp, _ := rtc.ReadTemperature()
fmt.Printf("Temperature: %.2f °C \r\n", float32(temp)/1000)
println("Temperature:", strconv.FormatFloat(float64(temp)/1000, 'f', -1, 32), "°C")
time.Sleep(time.Second * 1)
}
+17 -4
View File
@@ -97,11 +97,24 @@ func main() {
// After configuring PLLs and clocks, enable the outputs.
clockgen.EnableOutputs()
time.Sleep(time.Second)
clockgen.DisableOutputs()
println("All outputs disabled for 5 seconds")
time.Sleep(5 * time.Second)
// Now use SetFrequency to re-set the frequencies of the outputs
on := false
for {
if on {
println("Setting Clock 0 output off")
clockgen.OutputEnable(0, false)
on = false
} else {
println("Setting Clock 0 output to 100mhz")
clockgen.SetFrequency(100*machine.MHz, 0, si5351.PLL_A)
on = true
}
time.Sleep(5 * time.Second)
println()
println("Clock 0: 112.5mhz")
println("Clock 1: 13.5531mhz")
println("Clock 2: 10.706khz")
}
}
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"machine"
"math/rand"
"tinygo.org/x/drivers/internal/pin"
"tinygo.org/x/drivers/ssd1289"
)
@@ -16,7 +17,7 @@ func main() {
//consider creating a more efficient bus implementation that uses
//your microcontrollers built in "ports"
//see rp2040bus.go for an example for the rapsberry pi pico
bus := ssd1289.NewPinBus([16]machine.Pin{
bus := ssd1289.NewPinBus([16]pin.Output{
machine.GP4, //DB0
machine.GP5, //DB1
machine.GP6, //DB2
+4 -4
View File
@@ -29,10 +29,10 @@ func main() {
Mode: 0,
})
display := st7789.New(machine.SPI0,
machine.P6, // TFT_RESET
machine.P7, // TFT_DC
machine.P8, // TFT_CS
machine.P9) // TFT_LITE
machine.TFT_RESET, // TFT_RESET
machine.TFT_DC, // TFT_DC
machine.TFT_CS, // TFT_CS
machine.TFT_LITE) // TFT_LITE
display.Configure(st7789.Config{
Rotation: st7789.NO_ROTATION,
+37
View File
@@ -0,0 +1,37 @@
package main
import (
"machine"
"net"
"net/netip"
"time"
"tinygo.org/x/drivers/netdev"
"tinygo.org/x/drivers/w5500"
)
func main() {
machine.SPI0.Configure(machine.SPIConfig{
Frequency: 33 * machine.MHz,
})
machine.GPIO17.Configure(machine.PinConfig{Mode: machine.PinOutput})
eth := w5500.New(machine.SPI0, machine.GPIO17)
eth.Configure(w5500.Config{
MAC: net.HardwareAddr{0xee, 0xbe, 0xe9, 0xa9, 0xb6, 0x4f},
IP: netip.AddrFrom4([4]byte{192, 168, 1, 2}),
SubnetMask: netip.AddrFrom4([4]byte{255, 255, 255, 0}),
Gateway: netip.AddrFrom4([4]byte{192, 168, 1, 1}),
})
netdev.UseNetdev(eth)
for {
if eth.LinkStatus() != w5500.LinkStatusUp {
println("Waiting for link to be up")
time.Sleep(1 * time.Second)
continue
}
break
}
}
+3
View File
@@ -1,3 +1,6 @@
// Guarded because still unsure of how to deal with interrupt drivers.
//go:build tinygo
// Package ft6336 provides a driver for the FT6336 I2C Self-Capacitive touch
// panel controller.
//
+5 -5
View File
@@ -210,7 +210,7 @@ func (d *Device) SendCommand(command byte) {
d.bus.SetCommandMode(true)
d.bus.Write([]byte{command})
for d.busy(command == DISPLAY_CLEAR || command == CURSOR_HOME) {
for d.isBusy(command == DISPLAY_CLEAR || command == CURSOR_HOME) {
}
}
@@ -219,7 +219,7 @@ func (d *Device) sendData(data byte) {
d.bus.SetCommandMode(false)
d.bus.Write([]byte{data})
for d.busy(false) {
for d.isBusy(false) {
}
}
@@ -231,9 +231,9 @@ func (d *Device) CreateCharacter(cgramAddr uint8, data []byte) {
}
}
// busy returns true when hd447890 is busy
// isBusy returns true when hd447890 is isBusy
// or after the timeout specified
func (d *Device) busy(longDelay bool) bool {
func (d *Device) isBusy(longDelay bool) bool {
if d.bus.WriteOnly() {
// Can't read busy flag if write only, so sleep a bit then return
if longDelay {
@@ -261,7 +261,7 @@ func (d *Device) busy(longDelay bool) bool {
// Busy returns true when hd447890 is busy
func (d *Device) Busy() bool {
return d.busy(false)
return d.isBusy(false)
}
// Size returns the current size of the display.
+4 -4
View File
@@ -3,9 +3,9 @@ package max6675
import (
"errors"
"machine"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/pin"
)
// ErrThermocoupleOpen is returned when the thermocouple input is open.
@@ -14,16 +14,16 @@ var ErrThermocoupleOpen = errors.New("thermocouple input open")
type Device struct {
bus drivers.SPI
cs machine.Pin
cs pin.OutputFunc
}
// Create a new Device to read from a MAX6675 thermocouple.
// Pins must be configured before use. Frequency for SPI
// should be 4.3MHz maximum.
func NewDevice(bus drivers.SPI, cs machine.Pin) *Device {
func NewDevice(bus drivers.SPI, cs pin.Output) *Device {
return &Device{
bus: bus,
cs: cs,
cs: cs.Set,
}
}
+14 -9
View File
@@ -3,31 +3,36 @@
package max72xx
import (
"machine"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/legacy"
"tinygo.org/x/drivers/internal/pin"
)
type Device struct {
bus drivers.SPI
cs machine.Pin
bus drivers.SPI
cs pin.OutputFunc
configurePins func()
}
// NewDriver creates a new max7219 connection. The SPI wire must already be configured
// The SPI frequency must not be higher than 10MHz.
// parameter cs: the datasheet also refers to this pin as "load" pin.
func NewDevice(bus drivers.SPI, cs machine.Pin) *Device {
func NewDevice(bus drivers.SPI, cs pin.Output) *Device {
return &Device{
bus: bus,
cs: cs,
cs: cs.Set,
configurePins: func() {
legacy.ConfigurePinOut(cs)
},
}
}
// Configure setups the pins.
func (driver *Device) Configure() {
outPutConfig := machine.PinConfig{Mode: machine.PinOutput}
driver.cs.Configure(outPutConfig)
if driver.configurePins == nil {
panic(legacy.ErrConfigBeforeInstantiated)
}
driver.configurePins()
}
// SetScanLimit sets the scan limit. Maximum is 8.
+16 -8
View File
@@ -8,18 +8,20 @@ package mcp2515 // import "tinygo.org/x/drivers/mcp2515"
import (
"errors"
"fmt"
"machine"
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/legacy"
"tinygo.org/x/drivers/internal/pin"
)
// Device wraps MCP2515 SPI CAN Module.
type Device struct {
spi SPI
cs machine.Pin
msg *CANMsg
mcpMode byte
spi SPI
cs pin.OutputFunc
msg *CANMsg
mcpMode byte
configurePins func()
}
// CANMsg stores CAN message fields.
@@ -36,15 +38,18 @@ const (
)
// New returns a new MCP2515 driver. Pass in a fully configured SPI bus.
func New(b drivers.SPI, csPin machine.Pin) *Device {
func New(b drivers.SPI, csPin pin.Output) *Device {
d := &Device{
spi: SPI{
bus: b,
tx: make([]byte, 0, bufferSize),
rx: make([]byte, 0, bufferSize),
},
cs: csPin,
cs: csPin.Set,
msg: &CANMsg{},
configurePins: func() {
legacy.ConfigurePinOut(csPin)
},
}
return d
@@ -52,7 +57,10 @@ func New(b drivers.SPI, csPin machine.Pin) *Device {
// Configure sets up the device for communication.
func (d *Device) Configure() {
d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput})
if d.configurePins == nil {
panic(legacy.ErrConfigBeforeInstantiated)
}
d.configurePins()
}
const beginTimeoutValue int = 10
+8 -8
View File
@@ -6,18 +6,18 @@ package pcd8544 // import "tinygo.org/x/drivers/pcd8544"
import (
"errors"
"image/color"
"machine"
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/pin"
)
// Device wraps an SPI connection.
type Device struct {
bus drivers.SPI
dcPin machine.Pin
rstPin machine.Pin
scePin machine.Pin
dcPin pin.OutputFunc
rstPin pin.OutputFunc
scePin pin.OutputFunc
buffer []byte
width int16
height int16
@@ -30,12 +30,12 @@ type Config struct {
}
// New creates a new PCD8544 connection. The SPI bus must already be configured.
func New(bus drivers.SPI, dcPin, rstPin, scePin machine.Pin) *Device {
func New(bus drivers.SPI, dcPin, rstPin, scePin pin.Output) *Device {
return &Device{
bus: bus,
dcPin: dcPin,
rstPin: rstPin,
scePin: scePin,
dcPin: dcPin.Set,
rstPin: rstPin.Set,
scePin: scePin.Set,
}
}
+21
View File
@@ -149,6 +149,20 @@ func (img Image[T]) setPixel(index int, c T) {
}
return
case zeroColor.BitsPerPixel() == 2:
// Grayscale2bit.
offset := index / 4 // 4 pixels per byte
shift := 6 - (index%4)*2 // bits: 6, 4, 2, 0
ptr := (*byte)(unsafe.Add(img.data, offset))
raw := *(*uint8)(unsafe.Pointer(&c))
gray := raw & 0b11
mask := byte(0b11 << shift)
*ptr = (*ptr &^ mask) | (gray << shift)
return
case zeroColor.BitsPerPixel()%8 == 0:
// Each color starts at a whole byte offset.
// This is the easy case.
@@ -206,6 +220,13 @@ func (img Image[T]) Get(x, y int) T {
ptr := (*byte)(unsafe.Add(img.data, offset))
c = ((*ptr >> (7 - uint8(bits))) & 0x1) > 0
return any(c).(T)
case zeroColor.BitsPerPixel() == 2:
// Grayscale2bit.
offset := index / 4 // 4 pixels per byte
shift := 6 - (index%4)*2 // bits: 6, 4, 2, 0
ptr := (*byte)(unsafe.Add(img.data, offset))
value := ((*ptr) >> shift) & 0b11
return any(Grayscale2bit(value)).(T)
case zeroColor.BitsPerPixel()%8 == 0:
// Colors like RGB565, RGB888, etc.
offset := index * int(unsafe.Sizeof(zeroColor))
+63
View File
@@ -107,6 +107,66 @@ func TestImageRGB444BE(t *testing.T) {
}
}
func TestImageGrayscale2bit(t *testing.T) {
image := pixel.NewImage[pixel.Grayscale2bit](128, 64)
if width, height := image.Size(); width != 128 || height != 64 {
t.Errorf("image.Size(): expected 128, 64 but got %d, %d", width, height)
}
// Define test colors representing 4 Grayscale levels.
testColors := []color.RGBA{
{R: 0x00, G: 0x00, B: 0x00, A: 0xff}, // black
{R: 0x55, G: 0x55, B: 0x55, A: 0xff}, // dark gray
{R: 0xaa, G: 0xaa, B: 0xaa, A: 0xff}, // light gray
{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, // white
}
// Single pixel roundtrip test at a fixed coordinate.
for _, c := range testColors {
encoded := pixel.NewColor[pixel.Grayscale2bit](c.R, c.G, c.B)
image.Set(5, 3, encoded)
actual := image.Get(5, 3).RGBA()
if actual != c {
t.Errorf("failed to roundtrip color: expected %v but got %v", c, actual)
}
}
// Multi-coordinate test across the image.
for x := 0; x < 8; x++ {
for y, c := range testColors {
encoded := pixel.NewColor[pixel.Grayscale2bit](c.R, c.G, c.B)
image.Set(x, y, encoded)
actual := image.Get(x, y).RGBA()
if actual != c {
t.Errorf("Set/Get mismatch at (%d,%d): expected %v but got %v", x, y, c, actual)
}
}
}
}
func TestNewGrayscale2bitMapping(t *testing.T) {
testCases := []struct {
input color.RGBA
expect pixel.Grayscale2bit
}{
{color.RGBA{R: 0x00, G: 0x00, B: 0x00}, 0}, // 0
{color.RGBA{R: 0x3F, G: 0x3F, B: 0x3F}, 0}, // 63
{color.RGBA{R: 0x40, G: 0x40, B: 0x40}, 1}, // 64
{color.RGBA{R: 0x7F, G: 0x7F, B: 0x7F}, 1}, // 127
{color.RGBA{R: 0x80, G: 0x80, B: 0x80}, 2}, // 128
{color.RGBA{R: 0xBF, G: 0xBF, B: 0xBF}, 2}, // 191
{color.RGBA{R: 0xC0, G: 0xC0, B: 0xC0}, 3}, // 192
{color.RGBA{R: 0xFF, G: 0xFF, B: 0xFF}, 3}, // 255
}
for _, tc := range testCases {
actual := pixel.NewColor[pixel.Grayscale2bit](tc.input.R, tc.input.G, tc.input.B)
if actual != tc.expect {
t.Errorf("NewGrayscale2bit(%#v) = %d, want %d", tc.input, actual, tc.expect)
}
}
}
func TestImageMonochrome(t *testing.T) {
image := pixel.NewImage[pixel.Monochrome](128, 64)
if width, height := image.Size(); width != 128 || height != 64 {
@@ -236,6 +296,9 @@ func TestImageNoise(t *testing.T) {
t.Run("RGB444BE", func(t *testing.T) {
testImageNoiseN[pixel.RGB444BE](t)
})
t.Run("Grayscale2bit", func(t *testing.T) {
testImageNoiseN[pixel.Grayscale2bit](t)
})
t.Run("Monochrome", func(t *testing.T) {
testImageNoiseN[pixel.Monochrome](t)
})
+31 -1
View File
@@ -16,7 +16,7 @@ import (
// particular display. Each pixel is at least 1 byte in size.
// The color format is sRGB (or close to it) in all cases except for 1-bit.
type Color interface {
RGB888 | RGB565BE | RGB555 | RGB444BE | Monochrome
RGB888 | RGB565BE | RGB555 | RGB444BE | Grayscale2bit | Monochrome
BaseColor
}
@@ -50,6 +50,8 @@ func NewColor[T Color](r, g, b uint8) T {
return any(NewRGB555(r, g, b)).(T)
case RGB444BE:
return any(NewRGB444BE(r, g, b)).(T)
case Grayscale2bit:
return any(NewGrayscale2bit(r, g, b)).(T)
case Monochrome:
return any(NewMonochrome(r, g, b)).(T)
default:
@@ -204,6 +206,34 @@ func (c RGB444BE) RGBA() color.RGBA {
return color
}
// Grayscale2bit represents a 2-bit Grayscale value (4 levels: black, dark gray, light gray, white).
type Grayscale2bit uint8
func NewGrayscale2bit(r, g, b uint8) Grayscale2bit {
// Convert RGB to luminance using standard weights (approximation of human perception)
// Use shift-based operations to reduce processing time.
// luminance := (299*uint32(r) + 587*uint32(g) + 114*uint32(b)) / 1000
luminance := (77*uint32(r) + 150*uint32(g) + 29*uint32(b)) >> 8
// Map to 2-bit value: 063 => 0, 64127 => 1, 128191 => 2, 192255 => 3
return Grayscale2bit((luminance >> 6) & 0b11)
}
func (c Grayscale2bit) BitsPerPixel() int {
return 2
}
func (c Grayscale2bit) RGBA() color.RGBA {
// Expand 2-bit Grayscale back to 8-bit (0255) using multiplication
// 0 → 0x00, 1 → 0x55, 2 → 0xAA, 3 → 0xFF (i.e., multiply by 85)
gray := uint8(c&0b11) * 85
return color.RGBA{
R: gray,
G: gray,
B: gray,
A: 255,
}
}
type Monochrome bool
func NewMonochrome(r, g, b uint8) Monochrome {
+1
View File
@@ -22,4 +22,5 @@ const (
CmdStartLowPowerPeriodicMeasurement = 0x21AC
CmdStartPeriodicMeasurement = 0x21B1
CmdStopPeriodicMeasurement = 0x3F86
CmdMeasureSingleShot = 0x219D
)
+46 -8
View File
@@ -82,6 +82,13 @@ func (d *Device) StartLowPowerPeriodicMeasurement() error {
return d.sendCommand(CmdStartLowPowerPeriodicMeasurement)
}
// MeasureSingleShot starts a single measurement cycle (SCD41 only). After this
// command is complete, the caller should wait for 5000ms before trying to read
// the result.
func (d *Device) MeasureSingleShot() error {
return d.sendCommand(CmdMeasureSingleShot)
}
// ReadData reads the data from the sensor and caches it.
func (d *Device) ReadData() error {
if err := d.sendCommandWithResult(CmdReadMeasurement, d.rx[0:9]); err != nil {
@@ -93,7 +100,18 @@ func (d *Device) ReadData() error {
return nil
}
// Update reads new data from the sensor (if new data is available) and caches
// it for reading in the CO2, Temperature, and Humidity methods.
func (d *Device) Update(measurements drivers.Measurement) error {
if measurements&(drivers.Temperature|drivers.Humidity|drivers.Concentration) != 0 {
return d.ReadData()
}
return nil
}
// ReadCO2 returns the CO2 concentration in PPM (parts per million).
//
// Deprecated: use Update() and CO2() instead.
func (d *Device) ReadCO2() (co2 int32, err error) {
ok, err := d.DataReady()
if err != nil {
@@ -105,7 +123,14 @@ func (d *Device) ReadCO2() (co2 int32, err error) {
return int32(d.co2), err
}
// CO2 returns last read the CO2 concentration in PPM (parts per million).
func (d *Device) CO2() int32 {
return int32(d.co2)
}
// ReadTemperature returns the temperature in celsius milli degrees (°C/1000)
//
// Deprecated: use Update() and Temperature() instead.
func (d *Device) ReadTemperature() (temperature int32, err error) {
ok, err := d.DataReady()
if err != nil {
@@ -114,8 +139,14 @@ func (d *Device) ReadTemperature() (temperature int32, err error) {
if ok {
err = d.ReadData()
}
return d.Temperature(), err
}
// Temperature returns the last read temperature in celsius milli degrees
// (°C/1000).
func (d *Device) Temperature() int32 {
// temp = -45 + 175 * value / 2¹⁶
return (-1 * 45000) + (21875 * (int32(d.temperature)) / 8192), err
return (-1 * 45000) + (21875 * (int32(d.temperature)) / 8192)
}
// ReadTempC returns the value in the temperature value in Celsius.
@@ -130,6 +161,11 @@ func (d *Device) ReadTempF() float32 {
}
// ReadHumidity returns the current relative humidity in %rH.
//
// Warning: the value returned here is less precise than the humidity returned
// from Humidity()!
//
// Deprecated: use Update() and Temperature() instead.
func (d *Device) ReadHumidity() (humidity int32, err error) {
ok, err := d.DataReady()
if err != nil {
@@ -142,18 +178,20 @@ func (d *Device) ReadHumidity() (humidity int32, err error) {
return (25 * int32(d.humidity)) / 16384, err
}
// Humidity returns the relative humidity in hundredths of a percent (in other
// words, with a range 0..10_000).
//
// Warning: the value returned here is of a different scale (more precise) than
// ReadHumidity()!
func (d *Device) Humidity() int32 {
return (2500 * int32(d.humidity)) / 16384
}
func (d *Device) sendCommand(command uint16) error {
binary.BigEndian.PutUint16(d.tx[0:], command)
return d.bus.Tx(uint16(d.Address), d.tx[0:2], nil)
}
func (d *Device) sendCommandWithValue(command, value uint16) error {
binary.BigEndian.PutUint16(d.tx[0:], command)
binary.BigEndian.PutUint16(d.tx[2:], value)
d.tx[4] = crc8(d.tx[2:4])
return d.bus.Tx(uint16(d.Address), d.tx[0:5], nil)
}
func (d *Device) sendCommandWithResult(command uint16, result []byte) error {
binary.BigEndian.PutUint16(d.tx[0:], command)
if err := d.bus.Tx(uint16(d.Address), d.tx[0:2], nil); err != nil {
+15
View File
@@ -62,3 +62,18 @@ const (
MULTISYNTH_DIV_6 = 6
MULTISYNTH_DIV_8 = 8
)
// Frequency constants (in Hz)
const (
CLKOUT_MIN_FREQ = 8000 // 8 kHz
CLKOUT_MAX_FREQ = 150000000 // 150 MHz
MULTISYNTH_MAX_FREQ = 150000000 // 150 MHz
MULTISYNTH_SHARE_MAX = 100000000 // 100 MHz
MULTISYNTH_DIVBY4_FREQ = 150000000 // 150 MHz
PLL_VCO_MIN = 600000000 // 600 MHz
PLL_VCO_MAX = 900000000 // 900 MHz
)
const (
SI5351_PLL_C_MAX = 1048575
)
+310 -129
View File
@@ -134,6 +134,23 @@ func (d *Device) DisableOutputs() error {
return d.rw.Write8(OUTPUT_ENABLE_CONTROL, 0xFF)
}
// packRegSet packs P1, P2, P3 values into the 8-byte register format
// used by both PLL and Multisynth configuration.
// For multisynth, rDivBits should contain the R divider value shifted left by 4.
// For PLL, rDivBits should be 0.
func packRegSet(p1, p2, p3 uint32, rDivBits uint8) [8]byte {
var data [8]byte
data[0] = uint8((p3 & 0xFF00) >> 8)
data[1] = uint8(p3 & 0xFF)
data[2] = uint8((p1&0x30000)>>16) | rDivBits
data[3] = uint8((p1 & 0xFF00) >> 8)
data[4] = uint8(p1 & 0xFF)
data[5] = uint8(((p3 & 0xF0000) >> 12) | ((p2 & 0xF0000) >> 16))
data[6] = uint8((p2 & 0xFF00) >> 8)
data[7] = uint8(p2 & 0xFF)
return data
}
// ConfigurePLL sets the multiplier for the specified PLL
// pll The PLL to configure, which must be one of the following:
// - PLL_A
@@ -163,42 +180,24 @@ func (d *Device) DisableOutputs() error {
// See: http://www.silabs.com/Support%20Documents/TechnicalDocs/AN619.pdf
func (d *Device) ConfigurePLL(pll uint8, mult uint8, num uint32, denom uint32) error {
// Basic validation
if !d.initialised {
switch {
case !d.initialised:
return ErrNotInitialised
}
// mult = 15..90
if !((mult > 14) && (mult < 91)) {
case !((mult > 14) && (mult < 91)):
return ErrInvalidParameter
}
// Avoid divide by zero
if !(denom > 0) {
case !(denom > 0):
return ErrInvalidParameter
}
// 20-bit limit
if !(num <= 0xFFFFF) {
case !(num <= 0xFFFFF):
return ErrInvalidParameter
}
// 20-bit limit
if !(denom <= 0xFFFFF) {
case !(denom <= 0xFFFFF):
return ErrInvalidParameter
}
// PLL Multiplier Equations
//
// P1 register is an 18-bit value using following formula:
//
// P1[17:0] = 128 * mult + floor(128*(num/denom)) - 512
//
// P2 register is a 20-bit value using the following formula:
//
// P2[19:0] = 128 * num - denom * floor(128*(num/denom))
//
// P3 register is a 20-bit value using the following formula:
//
// P3[19:0] = denom
//
// Set PLL config registers
// Calculate PLL register values
var p1, p2, p3 uint32
if num == 0 {
// Integer mode
@@ -218,16 +217,8 @@ func (d *Device) ConfigurePLL(pll uint8, mult uint8, num uint32, denom uint32) e
baseaddr = 34
}
// The datasheet is a nightmare of typos and inconsistencies here!
data := [8]byte{}
data[0] = uint8((p3 & 0x0000FF00) >> 8)
data[1] = uint8(p3 & 0x000000FF)
data[2] = uint8((p1 & 0x00030000) >> 16)
data[3] = uint8((p1 & 0x0000FF00) >> 8)
data[4] = uint8(p1 & 0x000000FF)
data[5] = uint8(((p3 & 0x000F0000) >> 12) | ((p2 & 0x000F0000) >> 16))
data[6] = uint8((p2 & 0x0000FF00) >> 8)
data[7] = uint8(p2 & 0x000000FF)
// Pack and write registers
data := packRegSet(p1, p2, p3, 0)
if err := d.bus.Tx(uint16(baseaddr), data[:], nil); err != nil {
return err
}
@@ -305,120 +296,58 @@ func (d *Device) ConfigurePLL(pll uint8, mult uint8, num uint32, denom uint32) e
// used, but this isn't currently implemented in the driver.
func (d *Device) ConfigureMultisynth(output uint8, pll uint8, div uint32, num uint32, denom uint32) error {
// Basic validation
if !d.initialised {
switch {
case !d.initialised:
return ErrNotInitialised
}
// Channel range
if !(output < 3) {
case !(output < 3):
return fmt.Errorf("output channel must be between 0 and 2")
}
// Divider integer value
if !((div > 3) && (div < 2049)) {
case !((div > 3) && (div < 2049)):
return ErrInvalidParameter
}
// Avoid divide by zero
if !(denom > 0) {
case !(denom > 0):
return ErrInvalidParameter
}
// 20-bit limit
if !(num <= 0xFFFFF) {
case !(num <= 0xFFFFF):
return ErrInvalidParameter
}
// 20-bit limit
if !(denom <= 0xFFFFF) {
case !(denom <= 0xFFFFF):
return ErrInvalidParameter
// Make sure the requested PLL has been initialised
case pll == PLL_A && !d.pllaConfigured:
return ErrInvalidParameter
case pll == PLL_B && !d.pllbConfigured:
return ErrInvalidParameter
}
// Make sure the requested PLL has been initialised
if pll == PLL_A && !d.pllaConfigured {
return ErrInvalidParameter
}
if pll == PLL_B && !d.pllbConfigured {
return ErrInvalidParameter
}
// Output Multisynth Divider Equations
//
// where: a = div, b = num and c = denom
//
// P1 register is an 18-bit value using following formula:
//
// P1[17:0] = 128 * a + floor(128*(b/c)) - 512
//
// P2 register is a 20-bit value using the following formula:
//
// P2[19:0] = 128 * b - c * floor(128*(b/c))
//
// P3 register is a 20-bit value using the following formula:
//
// P3[19:0] = c
//
// Set PLL config registers
var p1, p2, p3 uint32
if num == 0 {
// Calculate register values
var reg si5351RegSet
switch {
case num == 0:
// Integer mode
p1 = 128*div - 512
p2 = 0
p3 = denom
} else if denom == 1 {
reg.p1 = 128*div - 512
reg.p2 = 0
reg.p3 = denom
case denom == 1:
// Fractional mode, simplified calculations
p1 = 128*div + 128*num - 512
p2 = 128*num - 128
p3 = 1
} else {
reg.p1 = 128*div + 128*num - 512
reg.p2 = 128*num - 128
reg.p3 = 1
default:
// Fractional mode
p1 = uint32(128*float64(div) + math.Floor(128*(float64(num)/float64(denom))) - 512)
p2 = uint32(128*float64(num) - float64(denom)*math.Floor(128*(float64(num)/float64(denom))))
p3 = denom
reg.p1 = uint32(128*float64(div) + math.Floor(128*(float64(num)/float64(denom))) - 512)
reg.p2 = uint32(128*float64(num) - float64(denom)*math.Floor(128*(float64(num)/float64(denom))))
reg.p3 = denom
}
// Get the appropriate starting point for the PLL registers
baseaddr := uint8(0)
switch output {
case 0:
baseaddr = MULTISYNTH0_PARAMETERS_1
case 1:
baseaddr = MULTISYNTH1_PARAMETERS_1
case 2:
baseaddr = MULTISYNTH2_PARAMETERS_1
}
// Determine if we should use integer mode
intMode := num == 0
// Set the MSx config registers
data := [8]byte{}
data[0] = uint8((p3 & 0xFF00) >> 8)
data[1] = uint8(p3 & 0xFF)
data[2] = uint8(((p1 & 0x30000) >> 16)) | d.lastRdivValue[output]
data[3] = uint8((p1 & 0xFF00) >> 8)
data[4] = uint8(p1 & 0xFF)
data[5] = uint8(((p3 & 0xF0000) >> 12) | ((p2 & 0xF0000) >> 16))
data[6] = uint8((p2 & 0xFF00) >> 8)
data[7] = uint8(p2 & 0xFF)
if err := d.bus.Tx(uint16(baseaddr), data[:], nil); err != nil {
return err
}
// Use existing R divider value (0 if not previously set)
rDiv := d.lastRdivValue[output] >> 4
// Configure the clk control and enable the output
// TODO: Check if the clk control byte needs to be updated.
clkControlReg := uint8(0x0F) // 8mA drive strength, MS0 as CLK0 source, Clock not inverted, powered up
if pll == PLL_B {
clkControlReg |= (1 << 5) // Uses PLLB
}
if num == 0 {
clkControlReg |= (1 << 6) // Integer mode
}
var register uint8
switch output {
case 0:
register = CLK0_CONTROL
case 1:
register = CLK1_CONTROL
case 2:
register = CLK2_CONTROL
}
return d.rw.Write8(register, clkControlReg)
return d.setMS(output, reg, intMode, rDiv, pll)
}
func (d *Device) ConfigureRdiv(output uint8, div uint8) error {
@@ -446,3 +375,255 @@ func (d *Device) ConfigureRdiv(output uint8, div uint8) error {
data = (data & 0x0F) | d.lastRdivValue[output]
return d.rw.Write8(register, data)
}
// si5351RegSet holds the register values for multisynth configuration
type si5351RegSet struct {
p1 uint32
p2 uint32
p3 uint32
}
var ErrFrequencyOutOfRange = errors.New("Si5351 frequency out of range")
var ErrClockConflict = errors.New("Si5351 clock conflict with existing configuration")
// SetFrequency sets the clock frequency of the specified CLK output.
// Frequency range is 8 kHz to 150 MHz.
//
// freq - Output frequency in Hz
// output - Clock output (0, 1, or 2 for this driver)
// pll - The PLL to use (PLL_A or PLL_B)
func (d *Device) SetFrequency(freq uint64, output uint8, pll uint8) error {
switch {
case !d.initialised:
return ErrNotInitialised
case output > 2:
return ErrInvalidParameter
}
switch {
// Lower bounds check
case freq < CLKOUT_MIN_FREQ:
freq = CLKOUT_MIN_FREQ
// Upper bounds check
case freq > MULTISYNTH_MAX_FREQ:
freq = MULTISYNTH_MAX_FREQ
}
// Select the proper R divider value for low frequencies
rDiv := d.selectRDiv(&freq)
// Calculate PLL and multisynth parameters
var pllFreq uint64
switch {
case pll == PLL_A && d.pllaConfigured:
pllFreq = uint64(d.pllaFreq)
case pll == PLL_B && d.pllbConfigured:
pllFreq = uint64(d.pllbFreq)
default:
// PLL not configured, calculate optimal PLL frequency
pllFreq = d.calculatePLLFreq(freq)
}
// Calculate multisynth divider parameters
msReg := d.multisynthCalc(freq, pllFreq)
// Determine if we should use integer mode
intMode := msReg.p2 == 0
// Configure PLL if not already configured or if we need a new frequency
if (pll == PLL_A && !d.pllaConfigured) || (pll == PLL_B && !d.pllbConfigured) {
if err := d.setPLL(pllFreq, pll); err != nil {
return err
}
}
// Set multisynth registers
if err := d.setMS(output, msReg, intMode, rDiv, pll); err != nil {
return err
}
// Enable output
return d.OutputEnable(output, true)
}
// selectRDiv selects the appropriate R divider for low frequencies
// and modifies the frequency accordingly
func (d *Device) selectRDiv(freq *uint64) uint8 {
var rDiv uint8 = 0
if *freq >= CLKOUT_MIN_FREQ && *freq < CLKOUT_MIN_FREQ*2 {
rDiv = R_DIV_128
*freq *= 128
} else if *freq >= CLKOUT_MIN_FREQ*2 && *freq < CLKOUT_MIN_FREQ*4 {
rDiv = R_DIV_64
*freq *= 64
} else if *freq >= CLKOUT_MIN_FREQ*4 && *freq < CLKOUT_MIN_FREQ*8 {
rDiv = R_DIV_32
*freq *= 32
} else if *freq >= CLKOUT_MIN_FREQ*8 && *freq < CLKOUT_MIN_FREQ*16 {
rDiv = R_DIV_16
*freq *= 16
} else if *freq >= CLKOUT_MIN_FREQ*16 && *freq < CLKOUT_MIN_FREQ*32 {
rDiv = R_DIV_8
*freq *= 8
} else if *freq >= CLKOUT_MIN_FREQ*32 && *freq < CLKOUT_MIN_FREQ*64 {
rDiv = R_DIV_4
*freq *= 4
} else if *freq >= CLKOUT_MIN_FREQ*64 && *freq < CLKOUT_MIN_FREQ*128 {
rDiv = R_DIV_2
*freq *= 2
}
return rDiv
}
// calculatePLLFreq calculates an optimal PLL frequency for the given output frequency
func (d *Device) calculatePLLFreq(freq uint64) uint64 {
// Try to find an integer divider that puts PLL in valid range (600-900 MHz)
// Start with a divider that gives us a PLL freq near 750 MHz (middle of range)
targetPLL := uint64(750000000)
divider := targetPLL / freq
// Ensure divider is in valid range (8-900 for fractional, 4/6/8 for integer)
switch {
case divider < 8:
divider = 8
case divider > 900:
divider = 900
}
pllFreq := freq * divider
// Ensure PLL frequency is in valid range
switch {
case pllFreq < PLL_VCO_MIN:
pllFreq = PLL_VCO_MIN
case pllFreq > PLL_VCO_MAX:
pllFreq = PLL_VCO_MAX
}
return pllFreq
}
// multisynthCalc calculates the multisynth register values
func (d *Device) multisynthCalc(freq, pllFreq uint64) si5351RegSet {
var reg si5351RegSet
// Calculate the division ratio
// divider = pllFreq / freq
a := uint32(pllFreq / freq)
remainder := pllFreq % freq
// Calculate b and c for fractional part
// We use c = SI5351_PLL_C_MAX (max 20-bit value) for best resolution
c := uint32(SI5351_PLL_C_MAX)
b := uint32((uint64(remainder) * uint64(c)) / freq)
// Calculate P1, P2, P3
// P1 = 128 * a + floor(128 * b / c) - 512
// P2 = 128 * b - c * floor(128 * b / c)
// P3 = c
floor128bc := uint32((128 * uint64(b)) / uint64(c))
reg.p1 = 128*a + floor128bc - 512
reg.p2 = 128*b - c*floor128bc
reg.p3 = c
return reg
}
// setPLL configures the PLL with the specified frequency
func (d *Device) setPLL(pllFreq uint64, pll uint8) error {
// Calculate PLL multiplier from crystal frequency
// pllFreq = crystalFreq * (a + b/c)
xtalFreq := uint64(d.crystalFreq)
a := uint32(pllFreq / xtalFreq)
remainder := pllFreq % xtalFreq
// Use max denominator for best resolution
c := uint32(SI5351_PLL_C_MAX)
b := uint32((remainder * uint64(c)) / xtalFreq)
return d.ConfigurePLL(pll, uint8(a), b, c)
}
// setMS sets the multisynth registers for the specified output
func (d *Device) setMS(output uint8, reg si5351RegSet, intMode bool, rDiv uint8, pll uint8) error {
// Get the appropriate starting point for the registers
var baseaddr uint8
switch output {
case 0:
baseaddr = MULTISYNTH0_PARAMETERS_1
case 1:
baseaddr = MULTISYNTH1_PARAMETERS_1
case 2:
baseaddr = MULTISYNTH2_PARAMETERS_1
default:
return ErrInvalidParameter
}
// Store R divider value
d.lastRdivValue[output] = (rDiv & 0x07) << 4
// Pack and write registers
data := packRegSet(reg.p1, reg.p2, reg.p3, d.lastRdivValue[output])
if err := d.bus.Tx(uint16(baseaddr), data[:], nil); err != nil {
return err
}
// Configure the clk control register
clkControlReg := uint8(0x0F) // 8mA drive strength, powered up
if pll == PLL_B {
clkControlReg |= (1 << 5) // Use PLLB
}
if intMode {
clkControlReg |= (1 << 6) // Integer mode
}
var clkReg uint8
switch output {
case 0:
clkReg = CLK0_CONTROL
case 1:
clkReg = CLK1_CONTROL
case 2:
clkReg = CLK2_CONTROL
}
return d.rw.Write8(clkReg, clkControlReg)
}
// GetFreqStep returns the frequency step size of the radio in Hz.
// This is the smallest frequency increment that can be achieved,
// determined by the PLL frequency and denominator resolution.
// If pll is PLL_A, uses PLLA settings; if PLL_B, uses PLLB settings.
// Returns 0 if the specified PLL is not configured.
func (d *Device) GetFreqStep(pll uint8) uint64 {
// The frequency step at the output is:
// step = pllFreq / (SI5351_PLL_C_MAX * multisynth_divider)
//
// However, since multisynth divider varies per output, we return
// the base step from the PLL, which is:
// step = pllFreq / SI5351_PLL_C_MAX
var pllFreq uint64
switch pll {
case PLL_A:
if !d.pllaConfigured {
return 0
}
pllFreq = uint64(d.pllaFreq)
case PLL_B:
if !d.pllbConfigured {
return 0
}
pllFreq = uint64(d.pllbFreq)
default:
return 0
}
return pllFreq / SI5351_PLL_C_MAX
}
+5 -2
View File
@@ -20,9 +20,11 @@ tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmi
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmp180/main.go
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/bmp280/main.go
tinygo build -size short -o ./build/test.hex -target=trinket-m0 ./examples/bmp388/main.go
tinygo build -size short -o ./build/test.hex -target=metro-rp2350 ./examples/bno08x/i2c/main.go
tinygo build -size short -o ./build/test.hex -target=bluepill ./examples/ds1307/sram/main.go
tinygo build -size short -o ./build/test.hex -target=bluepill ./examples/ds1307/time/main.go
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/ds3231/main.go
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/ds3231/alarms/main.go
tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/ds3231/basic/main.go
tinygo build -size short -o ./build/test.hex -target=microbit ./examples/easystepper/main.go
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
@@ -72,7 +74,7 @@ tinygo build -size short -o ./build/test.hex -target=xiao-rp2040 ./examples/ssd1
tinygo build -size short -o ./build/test.hex -target=thumby ./examples/ssd1306/
tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ssd1331/main.go
tinygo build -size short -o ./build/test.hex -target=microbit ./examples/st7735/main.go
tinygo build -size short -o ./build/test.hex -target=microbit ./examples/st7789/main.go
tinygo build -size short -o ./build/test.hex -target=clue ./examples/st7789/main.go
tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/thermistor/main.go
tinygo build -size short -o ./build/test.hex -target=circuitplay-bluefruit ./examples/tone
tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/tm1637/main.go
@@ -145,6 +147,7 @@ tinygo build -size short -o ./build/test.uf2 -target=nicenano ./examples/sharpme
tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/max6675/main.go
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
# network examples (espat)
tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/
# network examples (wifinina)
+11 -20
View File
@@ -1,15 +1,19 @@
package ssd1289
import "machine"
import (
"tinygo.org/x/drivers/internal/legacy"
"tinygo.org/x/drivers/internal/pin"
)
type pinBus struct {
pins [16]machine.Pin
pins [16]pin.Output
}
func NewPinBus(pins [16]machine.Pin) pinBus {
func NewPinBus(pins [16]pin.Output) pinBus {
// configure GPIO pins (on baremetal targets only, for backwards compatibility)
for i := 0; i < 16; i++ {
pins[i].Configure(machine.PinConfig{Mode: machine.PinOutput})
legacy.ConfigurePinOut(pins[i])
}
return pinBus{
@@ -18,20 +22,7 @@ func NewPinBus(pins [16]machine.Pin) pinBus {
}
func (b pinBus) Set(data uint16) {
b.pins[15].Set((data & (1 << 15)) != 0)
b.pins[14].Set((data & (1 << 14)) != 0)
b.pins[13].Set((data & (1 << 13)) != 0)
b.pins[12].Set((data & (1 << 12)) != 0)
b.pins[11].Set((data & (1 << 11)) != 0)
b.pins[10].Set((data & (1 << 10)) != 0)
b.pins[9].Set((data & (1 << 9)) != 0)
b.pins[8].Set((data & (1 << 8)) != 0)
b.pins[7].Set((data & (1 << 7)) != 0)
b.pins[6].Set((data & (1 << 6)) != 0)
b.pins[5].Set((data & (1 << 5)) != 0)
b.pins[4].Set((data & (1 << 4)) != 0)
b.pins[3].Set((data & (1 << 3)) != 0)
b.pins[2].Set((data & (1 << 2)) != 0)
b.pins[1].Set((data & (1 << 1)) != 0)
b.pins[0].Set((data & (1 << 0)) != 0)
for i := 15; i >= 0; i-- {
b.pins[i].Set((data & (1 << i)) != 0)
}
}
+21 -18
View File
@@ -5,8 +5,10 @@ package ssd1289
import (
"image/color"
"machine"
"time"
"tinygo.org/x/drivers/internal/legacy"
"tinygo.org/x/drivers/internal/pin"
)
type Bus interface {
@@ -14,33 +16,34 @@ type Bus interface {
}
type Device struct {
rs machine.Pin
wr machine.Pin
cs machine.Pin
rst machine.Pin
rs pin.OutputFunc
wr pin.OutputFunc
cs pin.OutputFunc
rst pin.OutputFunc
bus Bus
}
const width = int16(240)
const height = int16(320)
func New(rs machine.Pin, wr machine.Pin, cs machine.Pin, rst machine.Pin, bus Bus) Device {
d := Device{
rs: rs,
wr: wr,
cs: cs,
rst: rst,
func New(rs, wr, cs, rst pin.Output, bus Bus) *Device {
d := &Device{
rs: rs.Set,
wr: wr.Set,
cs: cs.Set,
rst: rst.Set,
bus: bus,
}
rs.Configure(machine.PinConfig{Mode: machine.PinOutput})
wr.Configure(machine.PinConfig{Mode: machine.PinOutput})
cs.Configure(machine.PinConfig{Mode: machine.PinOutput})
rst.Configure(machine.PinConfig{Mode: machine.PinOutput})
// configure GPIO pins (only on baremetal targets, for backwards compatibility)
legacy.ConfigurePinOut(rs)
legacy.ConfigurePinOut(wr)
legacy.ConfigurePinOut(cs)
legacy.ConfigurePinOut(rst)
cs.High()
rst.High()
wr.High()
d.cs.High()
d.rst.High()
d.wr.High()
return d
}
+14 -12
View File
@@ -1,31 +1,33 @@
package ssd1306
import (
"machine"
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/legacy"
"tinygo.org/x/drivers/internal/pin"
)
type SPIBus struct {
wire drivers.SPI
dcPin machine.Pin
resetPin machine.Pin
csPin machine.Pin
dcPin pin.OutputFunc
resetPin pin.OutputFunc
csPin pin.OutputFunc
buffer []byte // buffer to avoid heap allocations
}
// NewSPI creates a new SSD1306 connection. The SPI wire must already be configured.
func NewSPI(bus drivers.SPI, dcPin, resetPin, csPin machine.Pin) *Device {
dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
func NewSPI(bus drivers.SPI, dcPin, resetPin, csPin pin.Output) *Device {
// configure GPIO pins (on baremetal targets only, for backwards compatibility)
legacy.ConfigurePinOut(dcPin)
legacy.ConfigurePinOut(resetPin)
legacy.ConfigurePinOut(csPin)
return &Device{
bus: &SPIBus{
wire: bus,
dcPin: dcPin,
resetPin: resetPin,
csPin: csPin,
dcPin: dcPin.Set,
resetPin: resetPin.Set,
csPin: csPin.Set,
},
}
}
@@ -60,7 +62,7 @@ func (b *SPIBus) flush() error {
// tx sends data to the display
func (b *SPIBus) tx(data []byte, isCommand bool) error {
b.csPin.High()
b.dcPin.Set(!isCommand)
b.dcPin(!isCommand)
b.csPin.Low()
err := b.wire.Tx(data, nil)
b.csPin.High()
+14 -12
View File
@@ -5,12 +5,13 @@ package ssd1331 // import "tinygo.org/x/drivers/ssd1331"
import (
"image/color"
"machine"
"errors"
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/legacy"
"tinygo.org/x/drivers/internal/pin"
)
type Model uint8
@@ -19,9 +20,9 @@ type Rotation uint8
// Device wraps an SPI connection.
type Device struct {
bus drivers.SPI
dcPin machine.Pin
resetPin machine.Pin
csPin machine.Pin
dcPin pin.OutputFunc
resetPin pin.OutputFunc
csPin pin.OutputFunc
width int16
height int16
batchLength int16
@@ -36,15 +37,16 @@ type Config struct {
}
// New creates a new SSD1331 connection. The SPI wire must already be configured.
func New(bus drivers.SPI, resetPin, dcPin, csPin machine.Pin) Device {
dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
func New(bus drivers.SPI, resetPin, dcPin, csPin pin.Output) Device {
// configure GPIO pins (on baremetal targets only, for backwards compatibility)
legacy.ConfigurePinOut(dcPin)
legacy.ConfigurePinOut(resetPin)
legacy.ConfigurePinOut(csPin)
return Device{
bus: bus,
dcPin: dcPin,
resetPin: resetPin,
csPin: csPin,
dcPin: dcPin.Set,
resetPin: resetPin.Set,
csPin: csPin.Set,
}
}
@@ -251,7 +253,7 @@ func (d *Device) Data(data uint8) {
// Tx sends data to the display
func (d *Device) Tx(data []byte, isCommand bool) {
d.dcPin.Set(!isCommand)
d.dcPin(!isCommand)
d.bus.Tx(data, nil)
}
+30 -25
View File
@@ -6,10 +6,11 @@ package ssd1351 // import "tinygo.org/x/drivers/ssd1351"
import (
"errors"
"image/color"
"machine"
"time"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/legacy"
"tinygo.org/x/drivers/internal/pin"
)
var (
@@ -19,17 +20,18 @@ var (
// Device wraps an SPI connection.
type Device struct {
bus drivers.SPI
dcPin machine.Pin
resetPin machine.Pin
csPin machine.Pin
enPin machine.Pin
rwPin machine.Pin
width int16
height int16
rowOffset int16
columnOffset int16
bufferLength int16
bus drivers.SPI
dcPin pin.OutputFunc
resetPin pin.OutputFunc
csPin pin.OutputFunc
enPin pin.OutputFunc
rwPin pin.OutputFunc
width int16
height int16
rowOffset int16
columnOffset int16
bufferLength int16
configurePins func()
}
// Config is the configuration for the display
@@ -41,14 +43,21 @@ type Config struct {
}
// New creates a new SSD1351 connection. The SPI wire must already be configured.
func New(bus drivers.SPI, resetPin, dcPin, csPin, enPin, rwPin machine.Pin) Device {
func New(bus drivers.SPI, resetPin, dcPin, csPin, enPin, rwPin pin.Output) Device {
return Device{
bus: bus,
dcPin: dcPin,
resetPin: resetPin,
csPin: csPin,
enPin: enPin,
rwPin: rwPin,
dcPin: dcPin.Set,
resetPin: resetPin.Set,
csPin: csPin.Set,
enPin: enPin.Set,
rwPin: rwPin.Set,
configurePins: func() {
legacy.ConfigurePinOut(dcPin)
legacy.ConfigurePinOut(resetPin)
legacy.ConfigurePinOut(csPin)
legacy.ConfigurePinOut(enPin)
legacy.ConfigurePinOut(rwPin)
},
}
}
@@ -72,12 +81,8 @@ func (d *Device) Configure(cfg Config) {
d.bufferLength = d.height
}
// configure GPIO pins
d.dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
d.resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
d.csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
d.enPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
d.rwPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
// configure GPIO pins (on baremetal targets only, for backwards compatibility)
d.configurePins()
// reset the device
d.resetPin.High()
@@ -278,7 +283,7 @@ func (d *Device) Data(data uint8) {
// Tx sends data to the display
func (d *Device) Tx(data []byte, isCommand bool) {
d.dcPin.Set(!isCommand)
d.dcPin(!isCommand)
d.csPin.Low()
d.bus.Tx(data, nil)
d.csPin.High()
+8
View File
@@ -0,0 +1,8 @@
package st7789
// Bus is the interface that wraps the basic Tx and Transfer methods
// for communication buses like SPI or Parallel.
type Bus interface {
Tx(w, r []byte) error
Transfer(w byte) (byte, error)
}
+57 -50
View File
@@ -4,56 +4,63 @@ import "tinygo.org/x/drivers"
// Registers
const (
NOP = 0x00
SWRESET = 0x01
RDDID = 0x04
RDDST = 0x09
SLPIN = 0x10
SLPOUT = 0x11
PTLON = 0x12
NORON = 0x13
INVOFF = 0x20
INVON = 0x21
DISPOFF = 0x28
DISPON = 0x29
CASET = 0x2A
RASET = 0x2B
RAMWR = 0x2C
RAMRD = 0x2E
PTLAR = 0x30
COLMOD = 0x3A
MADCTL = 0x36
MADCTL_MY = 0x80
MADCTL_MX = 0x40
MADCTL_MV = 0x20
MADCTL_ML = 0x10
MADCTL_RGB = 0x00
MADCTL_BGR = 0x08
MADCTL_MH = 0x04
RDID1 = 0xDA
RDID2 = 0xDB
RDID3 = 0xDC
RDID4 = 0xDD
FRMCTR1 = 0xB1
RGBCTRL = 0xB1
FRMCTR2 = 0xB2
PORCTRL = 0xB2
FRMCTR3 = 0xB3
INVCTR = 0xB4
DISSET5 = 0xB6
PWCTR1 = 0xC0
PWCTR2 = 0xC1
PWCTR3 = 0xC2
PWCTR4 = 0xC3
PWCTR5 = 0xC4
VMCTR1 = 0xC5
FRCTRL2 = 0xC6
PWCTR6 = 0xFC
GMCTRP1 = 0xE0
GMCTRN1 = 0xE1
GSCAN = 0x45
VSCRDEF = 0x33
VSCRSADD = 0x37
NOP = 0x00
SWRESET = 0x01
RDDID = 0x04
RDDST = 0x09
SLPIN = 0x10
SLPOUT = 0x11
PTLON = 0x12
NORON = 0x13
INVOFF = 0x20
INVON = 0x21
DISPOFF = 0x28
DISPON = 0x29
CASET = 0x2A
RASET = 0x2B
RAMWR = 0x2C
RAMRD = 0x2E
PTLAR = 0x30
VSCRDEF = 0x33
TEOFF = 0x34
TEON = 0x35
VSCRSADD = 0x37
COLMOD = 0x3A
MADCTL = 0x36
GSCAN = 0x45
PWCTRL1 = 0xD0
RDID1 = 0xDA
RDID2 = 0xDB
RDID3 = 0xDC
RDID4 = 0xDD
RAMCTRL = 0xB0
FRMCTR1 = 0xB1
RGBCTRL = 0xB1
FRMCTR2 = 0xB2
PORCTRL = 0xB2
FRMCTR3 = 0xB3
INVCTR = 0xB4
DISSET5 = 0xB6
GCTRL = 0xB7
VCOMS = 0xBB
LCMCTRL = 0xC0
PWCTR2 = 0xC1
VDVVRHEN = 0xC2
VRHS = 0xC3
VDVS = 0xC4
VMCTR1 = 0xC5
FRCTRL2 = 0xC6
PVGAMCTRL = 0xE0
NVGAMCTRL = 0xE1
PWCTR6 = 0xFC
MADCTL_RGB = 0x00
MADCTL_ROWORDER = 0x80
MADCTL_COLORDER = 0x40
MADCTL_SWAPXY = 0x20
MADCTL_SCANORDER = 0x10
MADCTL_BGR = 0x08
MADCTL_MH = 0x04
ColorRGB444 ColorFormat = 0b011
ColorRGB565 ColorFormat = 0b101
+136 -63
View File
@@ -7,13 +7,14 @@ package st7789 // import "tinygo.org/x/drivers/st7789"
import (
"image/color"
"machine"
"math"
"time"
"errors"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/legacy"
"tinygo.org/x/drivers/internal/pin"
"tinygo.org/x/drivers/pixel"
)
@@ -45,11 +46,11 @@ type Device = DeviceOf[pixel.RGB565BE]
// DeviceOf is a generic version of Device. It supports multiple different pixel
// formats.
type DeviceOf[T Color] struct {
bus drivers.SPI
dcPin machine.Pin
resetPin machine.Pin
csPin machine.Pin
blPin machine.Pin
bus Bus
dcPin pin.OutputFunc
resetPin pin.OutputFunc
csPin pin.OutputFunc
blPin pin.OutputFunc
width int16
height int16
columnOffsetCfg int16
@@ -68,13 +69,15 @@ type DeviceOf[T Color] struct {
// Config is the configuration for the display
type Config struct {
Width int16
Height int16
Rotation drivers.Rotation
RowOffset int16
ColumnOffset int16
FrameRate FrameRate
VSyncLines int16
Width int16
Height int16
Rotation drivers.Rotation
RowOffset int16
ColumnOffset int16
FrameRate FrameRate
VSyncLines int16
IdleModePorch byte
PartialModePorch byte
// Gamma control. Look in the LCD panel datasheet or provided example code
// to find these values. If not set, the defaults will be used.
@@ -83,23 +86,27 @@ type Config struct {
}
// New creates a new ST7789 connection. The SPI wire must already be configured.
func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device {
func New(bus Bus, resetPin, dcPin, csPin, blPin pin.Output) Device {
return NewOf[pixel.RGB565BE](bus, resetPin, dcPin, csPin, blPin)
}
// NewOf creates a new ST7789 connection with a particular pixel format. The SPI
// wire must already be configured.
func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) DeviceOf[T] {
dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
blPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
func NewOf[T Color](bus Bus, resetPin, dcPin, csPin, blPin pin.Output) DeviceOf[T] {
// IMPORTANT: pin configuration should really be done outside of this
// driver, but for backwards compatibility with existing code, we do it
// here.
legacy.ConfigurePinOut(dcPin)
legacy.ConfigurePinOut(resetPin)
legacy.ConfigurePinOut(csPin)
legacy.ConfigurePinOut(blPin)
return DeviceOf[T]{
bus: bus,
dcPin: dcPin,
resetPin: resetPin,
csPin: csPin,
blPin: blPin,
dcPin: dcPin.Set,
resetPin: resetPin.Set,
csPin: csPin.Set,
blPin: blPin.Set,
}
}
@@ -129,7 +136,7 @@ func (d *DeviceOf[T]) Configure(cfg Config) {
if cfg.VSyncLines >= 2 && cfg.VSyncLines <= 254 {
d.vSyncLines = cfg.VSyncLines
} else {
d.vSyncLines = 16
d.vSyncLines = 0 // Default: no VSYNC pause
}
d.batchLength = int32(d.width)
@@ -139,21 +146,18 @@ func (d *DeviceOf[T]) Configure(cfg Config) {
d.batchLength += d.batchLength & 1
// Reset the device
d.resetPin.High()
time.Sleep(50 * time.Millisecond)
d.resetPin.Low()
time.Sleep(50 * time.Millisecond)
d.resetPin.High()
time.Sleep(50 * time.Millisecond)
d.Reset()
// Common initialization
d.startWrite()
d.sendCommand(SWRESET, nil) // Soft reset
d.endWrite()
time.Sleep(150 * time.Millisecond) //
d.startWrite()
d.sendCommand(SLPOUT, nil) // Exit sleep mode
time.Sleep(150 * time.Millisecond) //
d.startWrite()
// enable frame sync signal if used
d.sendCommand(TEON, nil)
// Memory initialization
var zeroColor T
@@ -164,54 +168,121 @@ func (d *DeviceOf[T]) Configure(cfg Config) {
// Use default RGB565 color format.
d.setColorFormat(ColorRGB565) // 16 bits per pixel
}
time.Sleep(10 * time.Millisecond)
d.setRotation(d.rotation) // Memory orientation
d.setWindow(0, 0, d.width, d.height) // Full draw window
d.fillScreen(color.RGBA{0, 0, 0, 255}) // Clear screen
// Framerate
d.sendCommand(FRCTRL2, []byte{byte(d.frameRate)}) // Frame rate for normal mode (default 60Hz)
// Frame vertical sync and "porch"
//
// Front and back porch controls vertical scanline sync time before and after
// a frame, where memory can be safely written without tearing.
//
fp := uint8(d.vSyncLines / 2) // Split the desired pause half and half
bp := uint8(d.vSyncLines - int16(fp)) // between front and back porch.
// TODO: is this correct? fp := uint8(d.vSyncLines / 2) // Split the desired pause half and half
fp := byte(0x0c)
// TODO: is this correct? bp := uint8(d.vSyncLines - int16(fp)) // between front and back porch.
bp := byte(0x0c)
if cfg.IdleModePorch == 0 {
cfg.IdleModePorch = 0x22 // Default value
}
if cfg.PartialModePorch == 0 {
cfg.PartialModePorch = 0x22 // Default value
}
d.sendCommand(PORCTRL, []byte{
bp, // Back porch 5bit (0x7F max 0x08 default)
fp, // Front porch 5bit (0x7F max 0x08 default)
0x00, // Seprarate porch (TODO: what is this?)
0x22, // Idle mode porch (4bit-back 4bit-front 0x22 default)
0x22, // Partial mode porch (4bit-back 4bit-front 0x22 default)
bp, // Back porch 5bit (0x7F max 0x08 default)
fp, // Front porch 5bit (0x7F max 0x08 default)
0x00, // Separate porch (TODO: what is this?)
cfg.IdleModePorch, // Idle mode porch (4bit-back 4bit-front 0x22 default)
cfg.PartialModePorch, // Partial mode porch (4bit-back 4bit-front 0x22 default)
})
// Ready to display
d.sendCommand(INVON, nil) // Inversion ON
time.Sleep(10 * time.Millisecond) //
// LCM control, power on sequence
d.sendCommand(LCMCTRL, []byte{0x2C})
// VDV and VRH Command Enable - 0x01 power on sequence
d.sendCommand(VDVVRHEN, []byte{0x01})
// VRH Set - 0x12 5.3+( vcom+vcom offset+vdv)
d.sendCommand(VRHS, []byte{0x12})
// VDV Set - 0x20 0V
d.sendCommand(VDVS, []byte{0x20})
// PWCTRL1 Power control 1 - power on sequence
d.sendCommand(PWCTRL1, []byte{0xA4, 0xA1})
// Framerate
d.sendCommand(FRCTRL2, []byte{byte(d.frameRate)}) // Frame rate for normal mode (default 60Hz)
// As noted in https://github.com/pimoroni/pimoroni-pico/issues/1040
// this is required to avoid a weird light grey banding issue with low brightness green.
// The banding is not visible without tweaking gamma settings (GMCTRP1 & GMCTRN1) but
// it makes sense to fix it anyway.
d.sendCommand(RAMCTRL, []byte{0x00, 0xC0})
// Set gamma tables, if configured.
if len(cfg.PVGAMCTRL) == 14 {
d.sendCommand(GMCTRP1, cfg.PVGAMCTRL) // PVGAMCTRL: Positive Voltage Gamma Control
d.sendCommand(PVGAMCTRL, cfg.PVGAMCTRL) // PVGAMCTRL: Positive Voltage Gamma Control
}
if len(cfg.NVGAMCTRL) == 14 {
d.sendCommand(GMCTRN1, cfg.NVGAMCTRL) // NVGAMCTRL: Negative Voltage Gamma Control
d.sendCommand(NVGAMCTRL, cfg.NVGAMCTRL) // NVGAMCTRL: Negative Voltage Gamma Control
}
d.sendCommand(NORON, nil) // Normal mode ON
time.Sleep(10 * time.Millisecond) //
switch {
case d.width == 240 && d.height == 240:
// command(reg::GCTRL, 1, "\x14");
// Gate Control - power on sequence for 240x240
d.sendCommand(GCTRL, []byte{0x35})
d.sendCommand(DISPON, nil) // Screen ON
time.Sleep(10 * time.Millisecond) //
// command(reg::VCOMS, 1, "\x37");
// VCOM Setting
d.sendCommand(VCOMS, []byte{0x37})
// command(reg::GMCTRP1, 14, "\xD0\x04\x0D\x11\x13\x2B\x3F\x54\x4C\x18\x0D\x0B\x1F\x23");
d.sendCommand(PVGAMCTRL, []byte{0xD0, 0x04, 0x0D, 0x11, 0x13, 0x2B, 0x3F, 0x54, 0x4C, 0x18, 0x0D, 0x0B, 0x1F, 0x23})
// command(reg::GMCTRN1, 14, "\xD0\x04\x0C\x11\x13\x2C\x3F\x44\x51\x2F\x1F\x1F\x20\x23");
d.sendCommand(NVGAMCTRL, []byte{0xD0, 0x04, 0x0C, 0x11, 0x13, 0x2C, 0x3F, 0x44, 0x51, 0x2F, 0x1F, 0x1F, 0x20, 0x23})
case d.width == 320 && d.height == 240:
// Gate Control - power on sequence for 320x240
d.sendCommand(GCTRL, []byte{0x35})
// VCOM Setting - 0.875V
d.sendCommand(VCOMS, []byte{0x1f})
// command(reg::GMCTRP1, 14, "\xD0\x08\x11\x08\x0C\x15\x39\x33\x50\x36\x13\x14\x29\x2D");
d.sendCommand(PVGAMCTRL, []byte{0xD0, 0x08, 0x11, 0x08, 0x0C, 0x15, 0x39, 0x33, 0x50, 0x36, 0x13, 0x14, 0x29, 0x2D})
// command(reg::GMCTRN1, 14, "\xD0\x08\x10\x08\x06\x06\x39\x44\x51\x0B\x16\x14\x2F\x31");
d.sendCommand(NVGAMCTRL, []byte{0xD0, 0x08, 0x10, 0x08, 0x06, 0x06, 0x39, 0x44, 0x51, 0x0B, 0x16, 0x14, 0x2F, 0x31})
}
// Ready to display
d.sendCommand(INVON, nil) // Inversion ON
d.sendCommand(SLPOUT, nil) // Exit sleep mode
d.sendCommand(DISPON, nil) // Screen ON
d.endWrite()
time.Sleep(100 * time.Millisecond)
d.startWrite()
d.setRotation(d.rotation) // Memory orientation
d.setWindow(0, 0, d.width, d.height) // Full draw window
d.fillScreen(color.RGBA{0, 0, 0, 255}) // Clear screen
d.endWrite()
time.Sleep(50 * time.Millisecond)
d.blPin.High() // Backlight ON
}
// Reset performs a hardware reset of the display.
func (d *DeviceOf[T]) Reset() {
d.resetPin.High()
time.Sleep(50 * time.Millisecond)
d.resetPin.Low()
time.Sleep(50 * time.Millisecond)
d.resetPin.High()
time.Sleep(50 * time.Millisecond)
}
// Send a command with data to the display. It does not change the chip select
// pin (it must be low when calling). The DC pin is left high after return,
// meaning that data can be sent right away.
@@ -229,7 +300,7 @@ func (d *DeviceOf[T]) sendCommand(command uint8, data []byte) error {
// startWrite must be called at the beginning of all exported methods to set the
// chip select pin low.
func (d *DeviceOf[T]) startWrite() {
if d.csPin != machine.NoPin {
if d.csPin != nil {
d.csPin.Low()
}
}
@@ -237,7 +308,7 @@ func (d *DeviceOf[T]) startWrite() {
// endWrite must be called at the end of all exported methods to set the chip
// select pin high.
func (d *DeviceOf[T]) endWrite() {
if d.csPin != machine.NoPin {
if d.csPin != nil {
d.csPin.High()
}
}
@@ -509,18 +580,20 @@ func (d *DeviceOf[T]) setRotation(rotation Rotation) error {
madctl := uint8(0)
switch rotation % 4 {
case drivers.Rotation0:
madctl = MADCTL_COLORDER
madctl |= MADCTL_SWAPXY | MADCTL_SCANORDER
d.rowOffset = 0
d.columnOffset = 0
case drivers.Rotation90:
madctl = MADCTL_MX | MADCTL_MV
madctl = MADCTL_COLORDER | MADCTL_SWAPXY
d.rowOffset = 0
d.columnOffset = 0
case drivers.Rotation180:
madctl = MADCTL_MX | MADCTL_MY
madctl = MADCTL_COLORDER | MADCTL_ROWORDER
d.rowOffset = d.rowOffsetCfg
d.columnOffset = d.columnOffsetCfg
case drivers.Rotation270:
madctl = MADCTL_MY | MADCTL_MV
madctl = MADCTL_ROWORDER | MADCTL_SWAPXY
d.rowOffset = d.columnOffsetCfg
d.columnOffset = d.rowOffsetCfg
}
+1 -1
View File
@@ -2,4 +2,4 @@ package drivers
// Version returns a user-readable string showing the version of the drivers package for support purposes.
// Update this value before release of new version of software.
const Version = "0.33.0"
const Version = "0.34.0"
+104
View File
@@ -0,0 +1,104 @@
package w5500
import "time"
func (d *Device) irqPoll(sockn uint8, state uint8, deadline time.Time) uint8 {
waitTime := 500 * time.Microsecond
for {
if !deadline.IsZero() && time.Now().After(deadline) {
// If a deadline is set and it has passed, return 0.
return sockIntUnknown
}
irq := d.readByte(sockInt, sockAddr(sockn)) & 0b00011111
if got := irq & state; got != 0 {
// Acknowledge the interrupt.
d.writeByte(sockInt, sockAddr(sockn), got)
return got
}
d.mu.Unlock()
time.Sleep(waitTime)
// Exponential backoff for polling.
waitTime *= 2
if waitTime > 10*time.Millisecond {
waitTime = 10 * time.Millisecond
}
d.mu.Lock()
}
}
func (d *Device) read(addr uint16, bsb uint8, p []byte) {
d.cs(false)
if len(p) == 0 {
return
}
d.sendReadHeader(addr, bsb)
_ = d.bus.Tx(nil, p)
d.cs(true)
}
func (d *Device) readUint16(addr uint16, bsb uint8) uint16 {
d.cs(false)
d.sendReadHeader(addr, bsb)
buf := d.cmdBuf
_ = d.bus.Tx(nil, buf[:2])
d.cs(true)
return uint16(buf[1]) | uint16(buf[0])<<8
}
func (d *Device) readByte(addr uint16, bsb uint8) byte {
d.cs(false)
d.sendReadHeader(addr, bsb)
r, _ := d.bus.Transfer(byte(0))
d.cs(true)
return r
}
func (d *Device) write(addr uint16, bsb uint8, p []byte) {
d.cs(false)
if len(p) == 0 {
return
}
d.sendWriteHeader(addr, bsb)
_ = d.bus.Tx(p, nil)
d.cs(true)
}
func (d *Device) writeUint16(addr uint16, bsb uint8, v uint16) {
d.cs(false)
d.sendWriteHeader(addr, bsb)
buf := d.cmdBuf
buf[0] = byte(v >> 8)
buf[1] = byte(v & 0xff)
_ = d.bus.Tx(buf[:2], nil)
d.cs(true)
}
func (d *Device) writeByte(addr uint16, bsb uint8, b byte) {
d.cs(false)
d.sendWriteHeader(addr, bsb)
_, _ = d.bus.Transfer(b)
d.cs(true)
}
func (d *Device) sendReadHeader(addr uint16, bsb uint8) {
buf := d.cmdBuf
buf[0] = byte(addr >> 8)
buf[1] = byte(addr & 0xff)
buf[2] = bsb << 3
_ = d.bus.Tx(buf[:], nil)
}
func (d *Device) sendWriteHeader(addr uint16, bsb uint8) {
buf := d.cmdBuf
buf[0] = byte(addr >> 8)
buf[1] = byte(addr & 0xff)
buf[2] = bsb<<3 | 0b100
_ = d.bus.Tx(buf[:], nil)
}
+445
View File
@@ -0,0 +1,445 @@
package w5500
import (
"errors"
"net"
"net/netip"
"os"
"runtime"
"time"
"tinygo.org/x/drivers/netdev"
)
type socket struct {
sockn uint8
protocol uint8
port uint16
inUse bool
closed bool
}
func (s *socket) setProtocol(proto byte) *socket {
s.protocol = proto
return s
}
func (s *socket) setPort(port uint16) *socket {
s.port = port
return s
}
func (s *socket) setInUse(inUse bool) *socket {
s.inUse = inUse
return s
}
func (s *socket) setClosed(closed bool) *socket {
s.closed = closed
return s
}
func (s *socket) reset() {
s.protocol = 0
s.port = 0
s.inUse = false
s.closed = false
}
// GetHostByName resolves the given host name to an IP address.
func (d *Device) GetHostByName(name string) (netip.Addr, error) {
d.mu.Lock()
dns := d.dns
d.mu.Unlock()
if dns == nil {
return netip.Addr{}, netdev.ErrNotSupported
}
return dns(name)
}
func (d *Device) Socket(domain int, stype int, protocol int) (int, error) {
if domain != netdev.AF_INET {
return -1, netdev.ErrFamilyNotSupported
}
switch {
case stype == netdev.SOCK_STREAM && protocol == netdev.IPPROTO_TCP:
case stype == netdev.SOCK_DGRAM && protocol == netdev.IPPROTO_UDP:
default:
return -1, errors.New("unsupported combination of socket type and protocol")
}
var proto byte
switch protocol {
case netdev.IPPROTO_TCP:
proto = 1 // TCP
case netdev.IPPROTO_UDP:
proto = 2 // UDP
default:
return -1, netdev.ErrNotSupported
}
d.mu.Lock()
defer d.mu.Unlock()
sockfd, sock, err := d.nextSocket()
if err != nil {
return -1, err
}
d.openSocket(sock.sockn, proto)
sock.setProtocol(proto).setInUse(true)
return sockfd, nil
}
func (d *Device) openSocket(sockn uint8, proto byte) {
d.writeByte(sockMode, sockAddr(sockn), proto&0x0F)
}
func (d *Device) Bind(sockfd int, ip netip.AddrPort) error {
// The IP address is irrelevant. The configured ip will always be used.
port := ip.Port()
d.mu.Lock()
defer d.mu.Unlock()
sock, err := d.socket(sockfd)
if err != nil {
return err
}
if err = d.bindSocket(sock.sockn, port); err != nil {
return errors.New("could not set socket port: " + err.Error())
}
sock.setPort(port)
return nil
}
func (d *Device) bindSocket(sockn uint8, port uint16) error {
d.writeUint16(sockSrcPort, sockAddr(sockn), port)
d.socketSendCmd(sockn, sockCmdOpen)
if d.sockStatus(sockn) == sockStatusClosed {
return errors.New("socket is closed after binding")
}
return nil
}
// SetSockOpt sets the socket option for the given socket file descriptor.
// It is not supported by the W5500, so it always returns an error.
func (d *Device) SetSockOpt(int, int, int, any) error {
return netdev.ErrNotSupported
}
// Connect establishes a connection to the specified host and port or ip and port.
//
// If the host is an empty string, it will use the provided ip address and port,
// otherwise it will resolve the host name to an IP address.
func (d *Device) Connect(sockfd int, host string, ip netip.AddrPort) error {
destIP := ip.Addr()
if host != "" {
var err error
destIP, err = d.GetHostByName(host)
if err != nil {
return errors.New("could not resolve host " + host + ":" + err.Error())
}
}
if !destIP.IsValid() || !destIP.Is4() {
return errors.New("invalid destination IP address: " + destIP.String())
}
port := ip.Port()
d.mu.Lock()
defer d.mu.Unlock()
sock, err := d.socket(sockfd)
if err != nil {
return err
}
d.write(sockDestIP, sockAddr(sock.sockn), destIP.AsSlice())
d.writeUint16(sockDestPort, sockAddr(sock.sockn), port)
d.socketSendCmd(sock.sockn, sockCmdOpen)
return nil
}
// Listen sets the socket to listen for incoming connections on the specified socket file descriptor.
//
// The backlog parameter is ignored, as the W5500 does not support it.
func (d *Device) Listen(sockfd int, _ int) error {
d.mu.Lock()
defer d.mu.Unlock()
sock, err := d.socket(sockfd)
if err != nil {
return err
}
if sock.protocol != 1 { // Only TCP sockets can listen
return errors.New("not a TCP socket")
}
if err = d.listen(sock.sockn); err != nil {
return errors.New("could not send listen command: " + err.Error())
}
return nil
}
func (d *Device) listen(sockn uint8) error {
state := d.sockStatus(sockn)
if state != sockStatusInit {
return errors.New("socket is not in the initial state")
}
d.socketSendCmd(sockn, sockCmdListen)
return nil
}
// Accept waits for an incoming connection on the specified socket file descriptor.
func (d *Device) Accept(sockfd int) (int, netip.AddrPort, error) {
d.mu.Lock()
defer d.mu.Unlock()
lsock, err := d.socket(sockfd)
if err != nil {
return -1, netip.AddrPort{}, errors.New("could not get socket: " + err.Error())
}
if err = d.waitForEstablished(lsock.sockn); err != nil {
return -1, netip.AddrPort{}, err
}
// Acquire a new socket for the listening connection.
csockfd, csock, err := d.nextSocket()
if err != nil {
return -1, netip.AddrPort{}, err
}
// Swap the socket numbers of the client and listening sockets.
lsock.sockn, csock.sockn = csock.sockn, lsock.sockn
// Rebind the listening socket to the local address and port and start listening.
d.openSocket(lsock.sockn, lsock.protocol)
if err = d.bindSocket(lsock.sockn, lsock.port); err != nil {
return -1, netip.AddrPort{}, errors.New("could not bind listening socket: " + err.Error())
}
if err = d.listen(lsock.sockn); err != nil {
return -1, netip.AddrPort{}, errors.New("could not set listening socket: " + err.Error())
}
csock.setInUse(true)
remoteIP := d.remoteIP(csock.sockn)
return csockfd, remoteIP, nil
}
func (d *Device) waitForEstablished(sockn uint8) error {
for {
status := d.sockStatus(sockn)
switch status {
case sockStatusEstablished:
return nil
case sockStatusClosed:
return net.ErrClosed
case sockStatusCloseWait:
// The server closed the connection, so we need to reset the socket
// and set it to listen again.
if err := d.listen(sockn); err != nil {
return errors.New("could not set socket to listen: " + err.Error())
}
}
d.irqPoll(sockn, sockIntConnect|sockIntDisconnect, time.Time{})
}
}
func (d *Device) remoteIP(sockn uint8) netip.AddrPort {
var rip [4]byte
d.read(sockDestIP, sockAddr(sockn), rip[:])
var rport [2]byte
d.read(sockDestPort, sockAddr(sockn), rport[:])
return netip.AddrPortFrom(netip.AddrFrom4(rip), uint16(rport[0])<<8|uint16(rport[1]))
}
// Send sends data to the socket with the given file descriptor.
// It blocks until all data is sent or the deadline is reached.
func (d *Device) Send(sockfd int, buf []byte, _ int, deadline time.Time) (int, error) {
bufLen := len(buf)
if bufLen <= d.maxSockSize {
// Fast path for small buffers.
return d.sendChunk(sockfd, buf, deadline)
}
var n int
for i := 0; i < bufLen; i += d.maxSockSize {
end := i + d.maxSockSize
if end > bufLen {
end = bufLen
}
sent, err := d.sendChunk(sockfd, buf[i:end], deadline)
if err != nil {
return n, errors.New("could not send chunk: " + err.Error())
}
n += sent
}
return n, nil
}
func (d *Device) sendChunk(sockfd int, buf []byte, deadline time.Time) (int, error) {
d.mu.Lock()
defer d.mu.Unlock()
sock, err := d.socket(sockfd)
if err != nil {
return 0, errors.New("could not get socket: " + err.Error())
}
if sock.closed {
return 0, os.ErrClosed
}
bufLen := uint16(len(buf))
if err = d.waitForFreeBuffer(sock.sockn, bufLen, deadline); err != nil {
return 0, err
}
sendPtr := d.readUint16(sockTXWritePtr, sockAddr(sock.sockn))
d.write(sendPtr, sock.sockn<<2|0b10, buf)
d.writeUint16(sockTXWritePtr, sockAddr(sock.sockn), sendPtr+bufLen)
d.writeByte(sockCmd, sockAddr(sock.sockn), sockCmdSend)
irq := d.irqPoll(sock.sockn, sockIntSendOK|sockIntDisconnect|sockIntTimeout, deadline)
switch {
case irq == sockIntUnknown:
return 0, os.ErrDeadlineExceeded
case irq&sockIntDisconnect != 0:
sock.setClosed(true)
return 0, net.ErrClosed
case irq&sockIntTimeout != 0:
return 0, netdev.ErrTimeout
default:
return int(bufLen), nil
}
}
func (d *Device) waitForFreeBuffer(sockn uint8, len uint16, deadline time.Time) error {
for {
freeSize := d.readUint16(sockTXFreeSize, sockAddr(sockn))
if freeSize >= len {
return nil
}
if !deadline.IsZero() && time.Now().After(deadline) {
return netdev.ErrTimeout
}
status := d.sockStatus(sockn)
switch status {
case sockStatusEstablished, sockStatusCloseWait:
default:
return errors.New("socket is not in a valid state for sending data")
}
d.mu.Unlock()
time.Sleep(time.Millisecond)
d.mu.Lock()
}
}
// Recv reads data from the socket with the given file descriptor into the provided buffer.
// It blocks until data is available or the deadline is reached.
func (d *Device) Recv(sockfd int, buf []byte, _ int, deadline time.Time) (int, error) {
d.mu.Lock()
defer d.mu.Unlock()
sock, err := d.socket(sockfd)
if err != nil {
return 0, errors.New("could not get socket: " + err.Error())
}
if sock.closed {
return 0, os.ErrClosed
}
size, err := d.waitForData(sock, deadline)
if err != nil {
return 0, err
}
recvPtr := d.readUint16(sockRXReadPtr, sockAddr(sock.sockn))
buf = buf[:min(size, len(buf))]
d.read(recvPtr, sock.sockn<<2|0b00011, buf)
d.writeUint16(sockRXReadPtr, sockAddr(sock.sockn), recvPtr+uint16(len(buf)))
d.socketSendCmd(sock.sockn, sockCmdRecv)
return len(buf), nil
}
func (d *Device) waitForData(sock *socket, deadline time.Time) (int, error) {
for {
recvdSize := d.readUint16(sockRXReceivedSize, sockAddr(sock.sockn))
if recvdSize > 0 {
return int(recvdSize), nil
}
irq := d.irqPoll(sock.sockn, sockIntReceive|sockIntDisconnect, deadline)
switch {
case irq == sockIntUnknown:
return 0, os.ErrDeadlineExceeded
case irq&sockIntDisconnect != 0:
sock.setClosed(true)
return 0, net.ErrClosed
}
}
}
// Close closes the socket with the given file descriptor.
func (d *Device) Close(sockfd int) error {
d.mu.Lock()
defer d.mu.Unlock()
sock, err := d.socket(sockfd)
if err != nil {
return err
}
d.socketSendCmd(sock.sockn, sockCmdClose)
sock.reset()
return nil
}
func (d *Device) nextSocket() (int, *socket, error) {
for i, sock := range d.sockets {
if sock.inUse {
continue
}
return i, sock, nil
}
return -1, nil, netdev.ErrNoMoreSockets
}
func (d *Device) socket(sockfd int) (*socket, error) {
if sockfd < 0 || sockfd >= len(d.sockets) {
return nil, netdev.ErrInvalidSocketFd
}
return d.sockets[sockfd], nil
}
func (d *Device) socketSendCmd(sockn uint8, cmd byte) {
d.writeByte(sockCmd, sockAddr(sockn), cmd)
for d.readByte(sockCmd, sockAddr(sockn)) != 0 {
runtime.Gosched()
}
}
func (d *Device) sockStatus(sockn uint8) int {
return int(d.readByte(sockStatus, sockAddr(sockn)))
}
func sockAddr(sockn uint8) uint8 {
return sockn<<2 | 0b0001
}
+88
View File
@@ -0,0 +1,88 @@
package w5500
// Common Registers.
const (
regMode = 0x0000
regGatewayAddr = 0x0001
regSubnetMask = 0x0005
regMAC = 0x0009
regIPAddr = 0x000F
regIntLevel = 0x0013
regInt = 0x0015
regIntMask = 0x0016
regSockInt = 0x0017
regSockIntMask = 0x0018
regRetryTime = 0x0019
regRetryN = 0x001B
// ... PPP registers, not needed
regPHYCfg = 0x002E
regChipVer = 0x0039
)
// Socket Registers.
const (
sockMode = 0x0000
sockCmd = 0x0001
sockInt = 0x0002
sockStatus = 0x0003
sockSrcPort = 0x0004
sockDestMAC = 0x0006
sockDestIP = 0x000C
sockDestPort = 0x0010
sockMaxSegSize = 0x0012
sockIPTOS = 0x0015
sockIPTTL = 0x0016
sockRXBUFSize = 0x001E
sockTXBUFSize = 0x001F
sockTXFreeSize = 0x0020
sockTXReadPtr = 0x0022
sockTXWritePtr = 0x0024
sockRXReceivedSize = 0x0026
sockRXReadPtr = 0x0028
sockRXWritePtr = 0x002A
sockIntMask = 0x002C
sockKeepInt = 0x002F
)
// Socket Commands.
const (
sockCmdOpen = 0x01
sockCmdClose = 0x10
sockCmdListen = 0x02
sockCmdConnect = 0x04
sockCmdDisconnect = 0x08
sockCmdSend = 0x20
sockCmdSendMacRaw = 0x21
sockCmdSendKeep = 0x22
sockCmdRecv = 0x40
)
// Socket Statuses.
const (
sockStatusClosed = 0x00
sockStatusInit = 0x13
sockStatusListen = 0x14
sockStatusEstablished = 0x17
sockStatusCloseWait = 0x1C
sockStatusUdp = 0x22
sockStatusMacRaw = 0x42
// Temporary TCP states
sockStatusSynSent = 0x15
sockStatusSynRecv = 0x16
sockStatusFinWait = 0x18
sockStatusClosing = 0x1A
sockStatusTimeWait = 0x1B
sockStatusLastAck = 0x1D
sockStatusUnknown = 0xFF
)
// Socket Interrupts.
const (
sockIntConnect uint8 = 1 << iota
sockIntDisconnect
sockIntReceive
sockIntTimeout
sockIntSendOK
sockIntUnknown uint8 = 0
)
+248
View File
@@ -0,0 +1,248 @@
// Package w5500 implements a driver for the W5500 Ethernet controller.
//
// The driver supports basic network functionality including TCP and UDP sockets.
// It currently does not use the IRQ or RST pins.
//
// Datasheet: https://docs.wiznet.io/img/products/w5500/W5500_ds_v110e.pdf
// Product Page: https://wiznet.io/products/ethernet-chips/w5500
package w5500
import (
"errors"
"net"
"net/netip"
"sync"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/pin"
"tinygo.org/x/drivers/netdev"
)
var _ netdev.Netdever = &Device{}
// Resolver is a function that resolves a hostname to an IP address.
type Resolver func(host string) (netip.Addr, error)
// Device is a driver for the W5500 Ethernet controller.
type Device struct {
maxSockets int
maxSockSize int
mu sync.Mutex
bus drivers.SPI
cs pin.OutputFunc
dns Resolver
sockets []*socket
laddr netip.Addr
cmdBuf [3]byte
}
// New returns a new w5500 driver.
func New(bus drivers.SPI, csPin pin.Output) *Device {
return &Device{
bus: bus,
cs: csPin.Set,
}
}
// Config is the configuration for the device.
//
// The SPI bus must be fully configured.
type Config struct {
DNS Resolver
MAC net.HardwareAddr
IP netip.Addr
SubnetMask netip.Addr
Gateway netip.Addr
// Optional, default is 8.
MaxSockets int
}
// Configure sets up the device.
//
// MAC address must be provided. The other fields are optional.
func (d *Device) Configure(cfg Config) error {
d.cs(true)
d.mu.Lock()
defer d.mu.Unlock()
d.dns = cfg.DNS
d.reset()
if err := d.setupSockets(cfg.MaxSockets); err != nil {
return errors.New("could not setup sockets: " + err.Error())
}
// Set the MAC address and IP configuration.
d.write(regMAC, 0, cfg.MAC)
d.write(regIPAddr, 0, cfg.IP.AsSlice())
d.write(regSubnetMask, 0, cfg.SubnetMask.AsSlice())
d.write(regGatewayAddr, 0, cfg.Gateway.AsSlice())
d.laddr = cfg.IP
return nil
}
func (d *Device) setupSockets(maxSockets int) error {
if maxSockets == 0 {
maxSockets = 8 // Default to 8 sockets if not specified.
}
switch maxSockets {
case 1, 2, 4, 8:
// Valid socket counts.
default:
return errors.New("invalid number of sockets, must be one of 1, 2, 4, or 8")
}
socks := make([]*socket, maxSockets)
for i := range socks {
socks[i] = &socket{
sockn: uint8(i),
}
}
d.maxSockets = maxSockets
d.maxSockSize = 16 * 1024 / maxSockets
d.sockets = socks
// Set the RX and TX buffer sizes for each socket.
for i := 0; i < 8; i++ {
size := byte(d.maxSockSize >> 10)
if i >= maxSockets {
size = 0
}
d.writeByte(sockRXBUFSize, sockAddr(uint8(i)), size)
d.writeByte(sockTXBUFSize, sockAddr(uint8(i)), size)
}
mask := byte(0b11111111)
switch maxSockets {
case 1:
mask = 0b00000001
case 2:
mask = 0b00000011
case 4:
mask = 0b00001111
}
d.writeByte(regSockIntMask, 0, mask)
d.writeByte(regIntMask, 0, 0)
return nil
}
// Reset performs a soft reset.
func (d *Device) Reset() {
d.mu.Lock()
defer d.mu.Unlock()
d.reset()
}
func (d *Device) reset() {
// RST is bit 7 of regMode.
d.writeByte(regMode, 0, 0x80)
}
// GetHardwareAddr returns the hardware address of the device.
func (d *Device) GetHardwareAddr() (net.HardwareAddr, error) {
d.mu.Lock()
defer d.mu.Unlock()
mac := make([]byte, 6)
d.read(regMAC, 0, mac)
return mac, nil
}
// Addr returns the IP address of the device.
func (d *Device) Addr() (netip.Addr, error) {
d.mu.Lock()
defer d.mu.Unlock()
var ip [4]byte
d.read(regIPAddr, 0, ip[:])
return netip.AddrFrom4(ip), nil
}
// SetAddr sets the IP address of the device.
//
// The IP address must be a valid IPv4 address.
func (d *Device) SetAddr(ip netip.Addr) error {
if err := d.setAddress(regIPAddr, ip); err != nil {
return errors.New("could not set IP address: " + err.Error())
}
d.mu.Lock()
defer d.mu.Unlock()
d.laddr = ip
return nil
}
// SetSubnetMask sets the subnet mask of the device.
//
// The subnet mask must be a valid IPv4 address.
// It is not checked if the subnet mask is valid for the device's IP address.
func (d *Device) SetSubnetMask(mask netip.Addr) error {
return d.setAddress(regSubnetMask, mask)
}
// SetGateway sets the gateway address of the device.
//
// The gateway must be a valid IPv4 address.
// It is not checked if the gateway is in the same subnet as the device.
func (d *Device) SetGateway(gateway netip.Addr) error {
return d.setAddress(regGatewayAddr, gateway)
}
func (d *Device) setAddress(addr uint16, ip netip.Addr) error {
if !ip.IsValid() || !ip.Is4() {
return errors.New("invalid IP address: " + ip.String())
}
d.mu.Lock()
defer d.mu.Unlock()
d.write(addr, 0, ip.AsSlice())
return nil
}
// LinkStatus is the link status of the device.
type LinkStatus = uint8
// LinkStatus values.
const (
LinkStatusDown LinkStatus = iota
LinkStatusUp
)
// LinkStatus returns the current link status of the device.
func (d *Device) LinkStatus() LinkStatus {
d.mu.Lock()
defer d.mu.Unlock()
return d.readByte(regPHYCfg, 0) & 0b00000001
}
// LinkInfo returns the current link information of the device.
func (d *Device) LinkInfo() string {
d.mu.Lock()
defer d.mu.Unlock()
linkInfo := d.readByte(regPHYCfg, 0) & 0b00000110
speed := "10Mbps"
if linkInfo&0b00000010 != 0 {
speed = "100Mbps"
}
duplex := "Half Duplex"
if linkInfo&0b00000100 != 0 {
duplex = "Full Duplex"
}
return speed + " " + duplex
}