diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..869e436 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +open_collective: tinygo diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c0c4e7f..e3fa5c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 239e95b..cce73ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,336 @@ +0.35.0 +--- +- **new devices** + - **unoqmatrix** + - LED matrix on the Arduino Uno Q + - **waveshare-epd (ssd1680)** + - Add driver for Waveshare 2.9 inch v2 e-paper display + +- **enhancements** + - **gps** + - add UBX config command support (#831) + - improve implementation for UBX config commands + - revamp validSentence() to avoid heap allocation for errors + - export some errors for checking/suppression from client + - improvements and corrections for config commands + - **lora** + - fill out more constants for lora device + - **mcp2515** + - add support for extended CAN IDs (#857) + - **si5351** + - complete refactor for more complete interface + - **st7735** + - remove dependency on the machine package + - **sx127x** + - add functions used for FSK radio communication + - **ws2812** + - add brightness control + - add PIO support for RP2040/RP2350 + +- **bugfixes** + - **st7789** + - fix scroll on rotated displays + - fix driver when rotated 90º + - **ws2812** + - fix brightness control issues (#858) + + +0.34.0 +--- +- **core** + - 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** + - **ens160** + - Add ens160 i2c driver + - **lsm303dlhc** + - added support for LSM303DLHC e-Compass; (#783) + - **seesaw** + - add support for Adafruit Seesaw encoders + +- **enhancements** + - **ws2812** + - add RP2350 support + - **ssd1306** + - avoid unnecessary heap allocations (#767) + - **gps** + - allow gps init with address + - **lsm6ds3tr** + - avoid unnecessary heap allocations (#766) + +- **bugfixes** + - **gps** + - Fix gps time calculation (#785) + + +0.32.0 +--- +- **enhancements** + - **bmp280** + - remove alloc on read sensor data + - **ws2812** + - add 200MHz support for the Cortex-M0/rp2040 + +- **bugfixes** + - **ssd1306** + - remove time.Sleep from SSD1306 SPI transfer code + - **tmc2209** + - tmc2209 bug fixes (#755) + +- **docs** + - **contributing** + - add driver design pointer to CONTRIBUTING.md + + +0.31.0 +--- +--- +- **enhancements** + - **spi** + - update all SPI usage to use either *machine.SPI or drivers.SPI + + +0.30.0 +--- +- **new devices** + - **comboat** + - Add wifi driver comboat for Elecrow W5 rp2040 and rp2350 devices (#741) + - **max6675** + - Add MAX6675 device + - **TMC2209** + - Added TMC2209 support (#727) + - **TMC5160** + - Added TMC5160 support (#725) + - **sharpmem** + - Add sharpmem (#724) + +- **enhancements** + - **net** + - move to latest golang.org/x/net v0.33.0 (#732) + - **microphone** + - update microphone driver to use latest i2s interface + +- **bugfixes** + - **net** + - fix typo in DHCP error message + - **aht20** + - Fixed bug in aht20 driver + - **hub75** + - fix data buffering + + +0.29.0 +--- +- **new devices** + - **epd1in54** + - Waveshare 1.54inch B/W e-Paper display (#704) + - **touch** + - add capacitive touch sensing on normal GPIO pins + - **INA219** + - I2C INA219 driver (#705) + - **pcf8591** + - add ADC only implementation for I2C ADC/DAC (#690) + +- **enhancements** + - **pixel** + - add NewImageFromBytes() function to allow creating image from existing slice + - **servo** + - Add function `SetAngleWithMicroseconds` (#695) + - **onewire** + - onewire improvements + - **ssd1306** + - Add function `SetFlip` and `GetFlip` (#702) + - **uc8151** + - add FillRectangle() and SetScroll() functions to satisfy tinyterm.Displayer interface + - **ssd1306** + - add FillRectangle() and SetScroll() functions to satisfy tinyterm.Displayer interface + +- **bugfixes** + - **pixel** + - fix Monochrome setPixel + +- **docs** + - **readme** + - discuss need to change variables in examples + - **sponsor** + - Add sponsor button to key repositories + + +0.28.0 +--- +- **new devices** + - **epd2in66b** + - Waveshare 2.66inch E-Paper Display Module (B) for Raspberry Pi Pico (#673) + - **mcp9808** + - Add driver for MCP9808 i2c temperature sensor (#676) + +- **enhancements** + - **encoders** + - add atsamd21, atsamd51, atsame5x + - **pixel** + - add support for Monochrome types such as the SSD1306 display + - **rtl8720dn** + - implement ConnectModeAP + - **servo** + - add function SetAngle() to simplify API for most common use case + - **ssd1306** + - add DrawBitmap() function to complete Displayer interface + - add rotation functions for Displayer interface + - add Sleep() function for Displayer interface + - **uc8151** + - improvements to speed and also add flicker-free mode based on @antirez code example + - update to support all functions needed by tinygl and board package Displayer interface + - **wifinina** + - implement ConnectModeAP + +- **bugfixes** + - **ft6336** + - ignore bogus touch events + - **pixel** + - fix Image[Monochrome].Set for larger images + - **uc8151** + - correct DrawBitmap() also refactor SendCommand() and SendData() for clarity + - **ws2812** + - Fix typo and move initialization of neo to init() + +- **examples** + - **ws2812** + - Simplify examples/ws2812 + + +0.27.0 +--- +- **core** + - prepare for CGo changes in TinyGo + +- **new devices** + - **adafruit4650** + - support for Adafruit 4650 feather OLED + - **net** + - new networking support based on tinygo net package + - **pixel** + - add package for efficiently working with raw pixel buffers + - **rotary** + - Adding driver for rotary encoder support + - **seesaw** + - Adding support for Adafruit Seesaw platform + - **sgp30** + - add SGP30 air quality sensor + - **sk6812** + - added support for SK6812 to WS2812 device (#610) + +- **enhancements** + - **epd2in13** + - add Sleep method like other displays + - unify rotation configuration with other displays + - use better black/white approximation + - **ili9341** + - add DrawBitmap method + - **lora/lorawan** + - LoRa WAN US915 Support + - LoRa WAN add setter functions + - refactor shared functionality for channels/regions + - **mcp2515** + - Add more line speeds to mcp2515.go (#626) + - **rtl8720dn** + - use drivers package version as the driver version + - **ssd1306** + - improvements needed for Thumby SPI display + - **st7735** + - make the display generic over RGB565 and RGB444 + - **st7789** + - add DrawBitmap method + - make the display generic over RGB565 and RGB444 + - **wifinina** + - add ResetIsHigh cfg switch for MKR 1010 (copied from #561) + - maintenence. Also see PR #4085 in the main TinyGo repo + - use drivers package version as the driver version + +- **bugfixes** + - **adxl345** + - Use int16 for ADXL345 readings (#656) + - **at24cx** + - fixed the description of the device struct + - **rtl8720dn** + - allow connecting to open wifi access points + - fix check for bad Wifi connect + - **sh1106** + - fix I2C interface and add smoketest + - fixed the description of the device struct + - **wifinina** + - add 'unknown failure' reason code for AP connect + - fix concurrency issues with multiple sockets + - fix wifinina UDP send + +- **examples** + - **ds3231** + - fix the description in the example + - **lorawan** + - add missing functions for simulated interface + - modify atcmd and basic demo to support choosing any one of the supported regions at compile time by using ldflags + - **net** + - all networking examples now using netdev and netlink. + +- **build** + - **all** + - fix broken testrunner + - migrated legacy I2C + - add natiu package for tests + - **smoketest** + - add stack-size param for net tests. + - allow stack-size flag as it is needed for net examples + + 0.26.0 --- - **core** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d77648..10c7f8b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,6 +8,9 @@ We would like your help to make this project better, so we appreciate any contri We'd love to get your feedback on getting started with TinyGo. Run into any difficulty, confusion, or anything else? You are not alone. We want to know about your experience, so we can help the next people. Please open a Github issue with your questions, or you can also get in touch directly with us on our Slack channel at [https://gophers.slack.com/messages/CDJD3SUP6](https://gophers.slack.com/messages/CDJD3SUP6). +### Driver design +Before porting or writing a driver from scratch please read **[Driver Design for TinyGo](https://tinygo.org/docs/guides/driver-design)**. + ### One of the TinyGo drivers is not working as you expect Please open a Github issue with your problem, and we will be happy to assist. diff --git a/LICENSE b/LICENSE index 658115a..398a54a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2018-2023 The TinyGo Authors. All rights reserved. +Copyright The TinyGo Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff --git a/Makefile b/Makefile index 112edbb..0a6915e 100644 --- a/Makefile +++ b/Makefile @@ -19,10 +19,24 @@ rwildcard=$(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2) $(filter $(subst # Recursively find all *_test.go files from cwd & reduce to unique dir names HAS_TESTS = $(sort $(dir $(call rwildcard,,*_test.go))) # Exclude anything we explicitly don't want to test for whatever reason -EXCLUDE_TESTS = image +EXCLUDE_TESTS = image waveshare-epd/epd2in66b TESTS = $(filter-out $(addsuffix /%,$(EXCLUDE_TESTS)),$(HAS_TESTS)) unit-test: @go test -v $(addprefix ./,$(TESTS)) test: clean fmt-check unit-test smoke-test + +EXCLUDE_DIRS = build cmd examples internal lora ndir netdev netlink tester + +drivers-count: + @root_count=$$(find . -mindepth 1 -maxdepth 1 -type d | grep -vE '^\./($(subst $(space),|,$(EXCLUDE_DIRS)))$$' | wc -l); \ + epd_count=$$(find ./waveshare-epd -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l); \ + total=$$((root_count + epd_count)); \ + echo "Total drivers: $$total (root: $$root_count, waveshare-epd: $$epd_count)" + +drivers-list: + @{ \ + find . -mindepth 1 -maxdepth 1 -type d | grep -vE '^\./($(subst $(space),|,$(EXCLUDE_DIRS)))$$'; \ + if [ -d ./waveshare-epd ]; then find ./waveshare-epd -mindepth 1 -maxdepth 1 -type d; fi; \ + } | sed 's|^\./||' | sort diff --git a/README.md b/README.md index b77dcb4..fbcef08 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,14 @@ [![PkgGoDev](https://pkg.go.dev/badge/tinygo.org/x/drivers)](https://pkg.go.dev/tinygo.org/x/drivers) [![Build](https://github.com/tinygo-org/drivers/actions/workflows/build.yml/badge.svg?branch=dev)](https://github.com/tinygo-org/drivers/actions/workflows/build.yml) -This package provides a collection of 101 different hardware drivers for devices such as sensors and displays that can be used together with [TinyGo](https://tinygo.org). +This package provides a collection of over 140 different hardware drivers for devices such as sensors, displays, wireless adaptors, and actuators, that can be used together with [TinyGo](https://tinygo.org). For the complete list, please see: https://tinygo.org/docs/reference/devices/ +> [!IMPORTANT] +> You can help TinyGo with a financial contribution using OpenCollective. Please see https://opencollective.com/tinygo for more information. Thank you! + ## Installing ```shell @@ -16,7 +19,7 @@ go get tinygo.org/x/drivers ## How to use -Here is an example in TinyGo that uses the BMP180 digital barometer: +Here is an example in TinyGo that uses the BMP180 digital barometer. This example should work on any board that supports I2C: ```go package main @@ -53,6 +56,28 @@ func main() { } ``` +## Examples Using GPIO or SPI + +If compiling these examples directly you are likely to need to make minor changes to the defined variables to map the pins for the board you are using. For example, this block in main.go: + +```golang +var ( + spi = machine.SPI0 + csPin = machine.D5 +) +``` + +It might not be obvious, but you need to change these to match how you wired your specific board. Constants are [defined for each supported microcontroller](https://tinygo.org/docs/reference/microcontrollers/). + +For example, to change the definitions for use on a Raspberry Pi Pico using typical wiring, you might need to do this: + +```golang +var ( + spi = machine.SPI0 + csPin = machine.GP17 +) +``` + ## Contributing Your contributions are welcome! diff --git a/adxl345/adxl345.go b/adxl345/adxl345.go index aa9e625..e6fbc38 100644 --- a/adxl345/adxl345.go +++ b/adxl345/adxl345.go @@ -95,16 +95,16 @@ func (d *Device) Restart() { func (d *Device) ReadAcceleration() (x int32, y int32, z int32, err error) { rx, ry, rz := d.ReadRawAcceleration() - x = d.dataFormat.convertToIS(rx) - y = d.dataFormat.convertToIS(ry) - z = d.dataFormat.convertToIS(rz) + x = int32(d.dataFormat.convertToIS(rx)) + y = int32(d.dataFormat.convertToIS(ry)) + z = int32(d.dataFormat.convertToIS(rz)) return } // ReadRawAcceleration reads the sensor values and returns the raw x, y and z axis // from the adxl345. -func (d *Device) ReadRawAcceleration() (x int32, y int32, z int32) { +func (d *Device) ReadRawAcceleration() (x int16, y int16, z int16) { data := []byte{0, 0, 0, 0, 0, 0} legacy.ReadRegister(d.bus, uint8(d.Address), REG_DATAX0, data) @@ -140,7 +140,7 @@ func (d *Device) SetRange(sensorRange Range) bool { } // convertToIS adjusts the raw values from the adxl345 with the range configuration -func (d *dataFormat) convertToIS(rawValue int32) int32 { +func (d *dataFormat) convertToIS(rawValue int16) int16 { switch d.sensorRange { case RANGE_2G: return rawValue * 4 // rawValue * 2 * 1000 / 512 @@ -190,6 +190,6 @@ func (b *bwRate) toByte() (bits uint8) { } // readInt converts two bytes to int16 -func readIntLE(msb byte, lsb byte) int32 { - return int32(uint16(msb) | uint16(lsb)<<8) +func readIntLE(msb byte, lsb byte) int16 { + return int16(uint16(msb) | uint16(lsb)<<8) } diff --git a/aht20/aht20.go b/aht20/aht20.go index 608f5ef..7713d27 100644 --- a/aht20/aht20.go +++ b/aht20/aht20.go @@ -29,7 +29,7 @@ func New(bus drivers.I2C) Device { func (d *Device) Configure() { // Check initialization state status := d.Status() - if status&0x08 == 1 { + if status&STATUS_CALIBRATED == 1 { // Device is initialized return } @@ -69,7 +69,7 @@ func (d *Device) Read() error { } // If measurement complete, store values - if data[0]&0x04 != 0 && data[0]&0x80 == 0 { + if data[0]&STATUS_CALIBRATED != 0 && data[0]&STATUS_BUSY == 0 { d.humidity = uint32(data[1])<<12 | uint32(data[2])<<4 | uint32(data[3])>>4 d.temp = (uint32(data[3])&0xF)<<16 | uint32(data[4])<<8 | uint32(data[5]) return nil diff --git a/apa102/apa102.go b/apa102/apa102.go index 98253bf..0cc22e0 100644 --- a/apa102/apa102.go +++ b/apa102/apa102.go @@ -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. diff --git a/apa102/softspi.go b/apa102/softspi.go index 89e8a85..113136c 100644 --- a/apa102/softspi.go +++ b/apa102/softspi.go @@ -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 { diff --git a/bmi160/bmi160.go b/bmi160/bmi160.go index cd3e882..71e602e 100644 --- a/bmi160/bmi160.go +++ b/bmi160/bmi160.go @@ -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() } diff --git a/bmp280/bmp280.go b/bmp280/bmp280.go index 2e769c1..e3b21e5 100644 --- a/bmp280/bmp280.go +++ b/bmp280/bmp280.go @@ -23,6 +23,7 @@ type Filter uint type Device struct { bus drivers.I2C Address uint16 + buf [6]byte cali calibrationCoefficients Temperature Oversampling Pressure Oversampling @@ -134,8 +135,8 @@ func (d *Device) PrintCali() { // ReadTemperature returns the temperature in celsius milli degrees (°C/1000). func (d *Device) ReadTemperature() (temperature int32, err error) { - data, err := d.readData(REG_TEMP, 3) - if err != nil { + data := d.buf[:3] + if err = d.readData(REG_TEMP, data); err != nil { return } @@ -158,8 +159,8 @@ func (d *Device) ReadTemperature() (temperature int32, err error) { // ReadPressure returns the pressure in milli pascals (mPa). func (d *Device) ReadPressure() (pressure int32, err error) { // First 3 bytes are Pressure, last 3 bytes are Temperature - data, err := d.readData(REG_PRES, 6) - if err != nil { + data := d.buf[:6] + if err = d.readData(REG_PRES, data); err != nil { return } @@ -203,7 +204,7 @@ func (d *Device) ReadPressure() (pressure int32, err error) { } // readData reads n number of bytes of the specified register -func (d *Device) readData(register int, n int) ([]byte, error) { +func (d *Device) readData(register int, data []byte) error { // If not in normal mode, set the mode to FORCED mode, to prevent incorrect measurements // After the measurement in FORCED mode, the sensor will return to SLEEP mode if d.Mode != MODE_NORMAL { @@ -218,9 +219,7 @@ func (d *Device) readData(register int, n int) ([]byte, error) { } // Read the requested register - data := make([]byte, n) - err := legacy.ReadRegister(d.bus, uint8(d.Address), uint8(register), data[:]) - return data, err + return legacy.ReadRegister(d.bus, uint8(d.Address), uint8(register), data[:]) } // convert3Bytes converts three bytes to int32 diff --git a/bno08x/bno08x.go b/bno08x/bno08x.go new file mode 100644 index 0000000..95cc3df --- /dev/null +++ b/bno08x/bno08x.go @@ -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) +} diff --git a/bno08x/bno08x_i2c.go b/bno08x/bno08x_i2c.go new file mode 100644 index 0000000..7546025 --- /dev/null +++ b/bno08x/bno08x_i2c.go @@ -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 +} diff --git a/bno08x/constants.go b/bno08x/constants.go new file mode 100644 index 0000000..7073f1f --- /dev/null +++ b/bno08x/constants.go @@ -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 +) diff --git a/bno08x/decode.go b/bno08x/decode.go new file mode 100644 index 0000000..0ec2a15 --- /dev/null +++ b/bno08x/decode.go @@ -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 +} diff --git a/bno08x/hal.go b/bno08x/hal.go new file mode 100644 index 0000000..0e939d2 --- /dev/null +++ b/bno08x/hal.go @@ -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) +} diff --git a/bno08x/sh2.go b/bno08x/sh2.go new file mode 100644 index 0000000..d756107 --- /dev/null +++ b/bno08x/sh2.go @@ -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 + } +} diff --git a/bno08x/shtp.go b/bno08x/shtp.go new file mode 100644 index 0000000..20cafd8 --- /dev/null +++ b/bno08x/shtp.go @@ -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 +} diff --git a/bno08x/types.go b/bno08x/types.go new file mode 100644 index 0000000..182774d --- /dev/null +++ b/bno08x/types.go @@ -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 +} diff --git a/buzzer/buzzer.go b/buzzer/buzzer.go index 04c1129..11b054d 100644 --- a/buzzer/buzzer.go +++ b/buzzer/buzzer.go @@ -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 } diff --git a/comboat/comboat.go b/comboat/comboat.go new file mode 100644 index 0000000..2687c36 --- /dev/null +++ b/comboat/comboat.go @@ -0,0 +1,711 @@ +// Package comboat implements WiFi driver for the Aithinker-Combo-AT WiFi +// device found on the Elecrow W5 rp2040 and rp2350 devices. Ths WiFi device +// is a RTL8720d variant. The driver interface is via AT command set over UART +// (see reference docs below). +// +// NOTE: the driver doesn't support UDP/TCP server connections in STA mode, +// currently. UDP/TCP/TLS client connections are supported in STA mode. +// +// https://aithinker-combo-guide.readthedocs.io/en/latest/docs/instruction/index.html +// https://aithinker-combo-guide.readthedocs.io/en/latest/docs/command-set/index.html +// https://aithinker-combo-guide.readthedocs.io/en/latest/docs/command-examples/index.html + +package comboat // import "tinygo.org/x/drivers/comboat" + +import ( + "bytes" + "errors" + "fmt" + "io" + "machine" + "net" + "net/netip" + "strconv" + "sync" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/netdev" + "tinygo.org/x/drivers/netlink" +) + +type Config struct { + BaudRate uint32 + Uart *machine.UART + Tx machine.Pin + Rx machine.Pin +} + +type socket struct { + protocol int + id string + rx chan []byte + remainder []byte + laddr netip.AddrPort // Set in Bind() +} + +type device struct { + cfg *Config + uart *machine.UART + uartMu sync.Mutex + mac net.HardwareAddr + ip netip.Addr + gateway netip.Addr + buf [1500]byte + pos int + last []byte + ok chan bool + txReady chan bool + accept chan string + err chan error + sockets [8]*socket + sync.Mutex +} + +func NewDevice(cfg *Config) *device { + return &device{ + cfg: cfg, + ok: make(chan bool), + txReady: make(chan bool), + accept: make(chan string), + err: make(chan error), + } +} + +func logDebug(msg string) { + //println("[DEBUG] " + msg) +} + +func logError(msg string) { + println("[ERROR] " + msg) +} + +func split(resp []byte, part int, del, on string) string { + parts := bytes.Split(resp, []byte(del)) + if part >= len(parts) { + return "Split parts error getting " + on + } + return string(parts[part]) +} + +func (d *device) getFWVersion() string { + return split(d.last, 1, ":", "FW version") +} + +func (d *device) saveMAC() { + raw := split(d.last, 1, ":", "MAC") + if len(raw) > 11 { + macStr := fmt.Sprintf("%s:%s:%s:%s:%s:%s", + raw[0:2], raw[2:4], raw[4:6], + raw[6:8], raw[8:10], raw[10:12]) + d.mac, _ = net.ParseMAC(macStr) + } +} + +var countryCodes = map[int]string{ + 1: "JP Japan", + 2: "American Samoa", + 3: "CA Canada", + 4: "US", + 5: "CN China", + 6: "Hong Kong, China", + 7: "Taiwan, China", + 8: "MO Macau, China", + 9: "IL Israel", + 10: "Singapore", + 11: "KR South Korea", + 12: "TR Türkiye", + 13: "AU Australia", + 14: "ZA South Africa", + 15: "BR Brazil", +} + +func (d *device) getCountry() (code string) { + code = split(d.last, 1, ":", "county code") + codeNum, err := strconv.Atoi(code) + if err != nil { + return + } + if val, ok := countryCodes[codeNum]; ok { + code = val + } + return +} + +func (d *device) saveIP() { + ipStr := split(d.last, 7, ",", "IP address") + gwStr := split(d.last, 8, ",", "gateway address") + d.ip, _ = netip.ParseAddr(ipStr) + d.gateway, _ = netip.ParseAddr(gwStr) +} + +func (d *device) execute(cmd string, timeout int) (err error) { + logDebug("EXECUTE " + cmd) + + d.uartMu.Lock() + _, err = d.uart.Write([]byte(cmd + "\r\n")) + d.uartMu.Unlock() + + if err != nil { + return + } + + t := time.NewTicker(time.Duration(timeout) * time.Millisecond) + defer t.Stop() + + select { + case <-t.C: + return errors.New("Timed out") + case <-d.ok: + return + case err = <-d.err: + return + } +} + +func (d *device) send(cmd string, timeout int) (err error) { + logDebug("EXECUTE " + cmd) + + d.uartMu.Lock() + _, err = d.uart.Write([]byte(cmd + "\r\n")) + d.uartMu.Unlock() + + if err != nil { + return + } + + t := time.NewTicker(time.Duration(timeout) * time.Millisecond) + defer t.Stop() + + select { + case <-t.C: + return errors.New("Timed out") + case <-d.txReady: + return + case err = <-d.err: + return + } +} + +func (d *device) findSocket(id string) (*socket, error) { + for _, s := range d.sockets { + if s.id == id { + return s, nil + } + } + return nil, errors.New("Socket not found with id: " + id) +} + +func (d *device) getSocket(sockfd int) (*socket, error) { + if sockfd < 0 || sockfd+1 > len(d.sockets) { + return nil, netdev.ErrInvalidSocketFd + } + if d.sockets[sockfd] == nil { + return nil, netdev.ErrInvalidSocketFd + } + return d.sockets[sockfd], nil +} + +func (d *device) handle(event []byte) { + logDebug("GOT EVENT " + string(event)) + switch { + + // SocketDisconnect, + case bytes.HasPrefix(event, []byte("SocketDisconnect")): + id := split(event, 1, ",", "SocketDisconnect") + s, err := d.findSocket(id) + if err == nil { + close(s.rx) // Sends io.EOF + } + + // SocketSeed,, + case bytes.HasPrefix(event, []byte("SocketSeed,2,1")): + //d.uart.Write([]byte("AT+SOCKET?" + "\r\n")) + } +} + +func (d *device) processUART() { + + if d.pos == 1 && d.buf[0] == '>' { + d.pos = 0 + logDebug("GOT >") + d.txReady <- true + } + + sofar := d.buf[:d.pos] + + if !bytes.HasSuffix(sofar, []byte("\r\n")) { + return + } + + // Strip CR/LF off end + sofar = sofar[:len(sofar)-2] + + switch { + + case bytes.HasPrefix(sofar, []byte("+EVENT:SocketDown")): + // +EVENT:SocketDown,,, + parts := bytes.SplitN(sofar, []byte(","), 4) + if len(parts) != 4 { + logError("Error parsing +EVENT:SocketDown: " + string(sofar)) + d.pos = 0 + return + } + id := string(parts[1]) + length, err := strconv.Atoi(string(parts[2])) + if err != nil { + logError("Error parsing length from: " + string(parts[2])) + d.pos = 0 + return + } + if length != len(parts[3]) { + // This can happen if actually contains a CR/LF. + // Return without resetting d.pos to continue reading + // in the full . + return + } + s, err := d.findSocket(id) + if err != nil { + logError(err.Error()) + d.pos = 0 + return + } + logDebug("GOT +EVENT:SocketDown," + id + "," + string(parts[2])) + d.pos = 0 + data := make([]byte, len(parts[3])) + copy(data, parts[3]) + s.rx <- data + + case bytes.HasPrefix(sofar, []byte("OK")): + d.pos = 0 + logDebug("GOT OK") + d.ok <- true + + case bytes.HasPrefix(sofar, []byte("ERROR")): + d.pos = 0 + logDebug("GOT ERROR") + errStr := getErrStr(d.last) + d.err <- errors.New(errStr) + + case bytes.HasPrefix(sofar, []byte("+EVENT:")): + d.pos = 0 + event := sofar[len("+EVENT:"):] + d.handle(event) + + default: + // Catch everything else and store in d.last + d.pos = 0 + size := len(sofar) + if size > 0 { + d.last = make([]byte, size) + copy(d.last, sofar[:size]) + logDebug("GOT LINE " + string(d.last)) + } + } +} + +func (d *device) serviceUART() { + for { + d.uartMu.Lock() + for d.uart.Buffered() > 0 { + if d.pos >= len(d.buf) { + println("Trying to write past buffer") + d.pos = 0 + break + } + var err error + d.buf[d.pos], err = d.uart.ReadByte() + if err == nil { + d.pos++ + d.processUART() + } + } + d.uartMu.Unlock() + time.Sleep(10 * time.Millisecond) + } +} + +func (d *device) NetConnect(params *netlink.ConnectParams) error { + + d.Lock() + defer d.Unlock() + + d.uart = d.cfg.Uart + d.uart.Configure(machine.UARTConfig{ + BaudRate: d.cfg.BaudRate, + TX: d.cfg.Tx, + RX: d.cfg.Rx, + }) + + go d.serviceUART() + + fmt.Printf("\r\n") + fmt.Printf("TinyGo Combo-AT WiFi network device driver\r\n") + + fmt.Printf("\r\n") + fmt.Printf("Driver version : %s\r\n", drivers.Version) + + if len(params.Ssid) == 0 { + return netlink.ErrMissingSSID + } + + // AT Test to see if device is alive + if err := d.execute("AT", 1000); err != nil { + return err + } + + // Disable echo + if err := d.execute("ATE0", 1000); err != nil { + return err + } + + // Get FW version + if err := d.execute("AT+GMR", 1000); err != nil { + return err + } + fmt.Printf("Combo-AT firmware version : %s\r\n", d.getFWVersion()) + + // Get/save MAC addresses + if err := d.execute("AT+CIPSTAMAC_DEF?", 1000); err != nil { + return err + } + d.saveMAC() + fmt.Printf("MAC address : %s\r\n", d.mac.String()) + + // Set country code US + if err := d.execute("AT+WCOUNTRY=4", 1000); err != nil { + return err + } + + // Get country code + if err := d.execute("AT+WCOUNTRY?", 1000); err != nil { + return err + } + fmt.Printf("WiFi country code : %s\r\n", d.getCountry()) + + // Set Wi-Fi working mode to STA and save to flash + if err := d.execute("AT+WMODE=1,1", 1000); err != nil { + return err + } + + // Connect to Wifi AP (keep trying until connected) + fmt.Printf("\r\n") + cmd := "AT+WJAP=" + params.Ssid + "," + params.Passphrase + + for { + fmt.Printf("Connecting to WiFi SSID '%s'...", params.Ssid) + if err := d.execute(cmd, 20000); err != nil { + fmt.Printf("FAILED (%s)\r\n", err.Error()) + continue + } + break + } + + fmt.Printf("CONNECTED\r\n") + + // Automatically reconnect to Wi-Fi after power on + if err := d.execute("AT+WAUTOCONN=1", 1000); err != nil { + return err + } + + // Get/save IP/gateway addresses + if err := d.execute("AT+WJAP?", 1000); err != nil { + return err + } + d.saveIP() + + fmt.Printf("\r\n") + fmt.Printf("DHCP-assigned IP : %s\r\n", d.ip) + fmt.Printf("DHCP-assigned gateway : %s\r\n", d.gateway) + fmt.Printf("\r\n") + + // Set socket receiving mode to active + if err := d.execute("AT+SOCKETRECVCFG=1", 1000); err != nil { + return err + } + + return nil +} + +func (d *device) NetDisconnect() { + d.Lock() + defer d.Unlock() + // Disconnect from WiFi AP + d.execute("AT+WDISCONNECT", 1000) +} + +func (d *device) NetNotify(cb func(netlink.Event)) { + fmt.Printf("\r\n%s\r\n", netlink.ErrNotSupported) +} + +func (d *device) GetHardwareAddr() (net.HardwareAddr, error) { + return d.mac, nil +} + +func (d *device) _getHostByName(name string) (ip netip.Addr, err error) { + if err = d.execute("AT+WDOMAIN="+name, 1000); err != nil { + return + } + ipStr := split(d.last, 1, ":", "host by name") + return netip.ParseAddr(ipStr) +} + +func (d *device) GetHostByName(name string) (ip netip.Addr, err error) { + + // If it's already a dotted-network address, and not a host name, + // return it + ip, err = netip.ParseAddr(name) + if err == nil { + return + } + + d.Lock() + defer d.Unlock() + + return d._getHostByName(name) +} + +func (d *device) Addr() (netip.Addr, error) { + return d.ip, nil +} + +func (d *device) Socket(domain, stype, protocol int) (int, error) { + + switch domain { + case netdev.AF_INET: + default: + return -1, netdev.ErrFamilyNotSupported + } + + switch { + case protocol == netdev.IPPROTO_TCP && stype == netdev.SOCK_STREAM: + case protocol == netdev.IPPROTO_TLS && stype == netdev.SOCK_STREAM: + case protocol == netdev.IPPROTO_UDP && stype == netdev.SOCK_DGRAM: + default: + return -1, netdev.ErrProtocolNotSupported + } + + d.Lock() + defer d.Unlock() + + // Search for empty slot in sockets array + for fd, s := range d.sockets { + if s == nil { + // Found one + d.sockets[fd] = &socket{ + protocol: protocol, + rx: make(chan []byte, 10), + } + return fd, nil + } + } + + return -1, netdev.ErrNoMoreSockets +} + +func (d *device) Bind(sockfd int, ip netip.AddrPort) error { + + d.Lock() + defer d.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return err + } + + s.laddr = ip + return nil +} + +func (d *device) Connect(sockfd int, host string, ip netip.AddrPort) error { + + var addr string + var cmd string + + d.Lock() + defer d.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return err + } + + if host == "" { + addr = ip.Addr().String() + } else { + ip, err := d._getHostByName(host) + if err != nil { + return err + } + addr = ip.String() + } + port := strconv.Itoa(int(ip.Port())) + + switch s.protocol { + case netdev.IPPROTO_UDP: + cmd = "AT+SOCKET=2," + addr + "," + port + case netdev.IPPROTO_TCP: + cmd = "AT+SOCKET=4," + addr + "," + port + case netdev.IPPROTO_TLS: + cmd = "AT+SOCKET=7," + addr + "," + port + } + + if cmd == "" { + return netdev.ErrProtocolNotSupported + } + + if err := d.execute(cmd, 20000); err != nil { + return err + } + + s.id = split(d.last, 1, "=", "connection ID") + + return nil +} + +func (d *device) Listen(sockfd, backlog int) error { + + // TODO Creating a TCP server socket isn't working when in STA mode, + // TODO returning error "Socket bind error". + // TODO The reference example shows a TCP server example in AP mode. + + /* + var cmd string + + d.Lock() + defer d.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return err + } + + port := strconv.Itoa(int(s.laddr.Port())) + + switch s.protocol { + case netdev.IPPROTO_UDP: + cmd = "AT+SOCKET=1," + port + case netdev.IPPROTO_TCP: + cmd = "AT+SOCKET=3," + port + } + + if cmd == "" { + return netdev.ErrProtocolNotSupported + } + + if err := d.execute(cmd, 20000); err != nil { + return err + } + + s.id = split(d.last, 1, "=", "connection ID") + */ + + return netdev.ErrNotSupported +} + +func (d *device) Accept(sockfd int) (int, netip.AddrPort, error) { + return 0, netip.AddrPort{}, netdev.ErrNotSupported +} + +func (d *device) Send(sockfd int, buf []byte, flags int, deadline time.Time) (int, error) { + + d.Lock() + defer d.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return 0, err + } + + cmd := fmt.Sprintf("AT+SOCKETSEND=%s,%d", s.id, len(buf)) + + if err := d.send(cmd, 1000); err != nil { + return 0, err + } + + // AT+SOCKETSEND will sub-packet send data into 1024-byte chunks, + // automatically, so send the full buffer in one shot, even if it's + // bigger than 1024 bytes. + + d.uartMu.Lock() + n, err := d.uart.Write(buf) + d.uartMu.Unlock() + + if err != nil { + return 0, err + } + + // Expecting "OK" after good send, or "ERROR" + + t := time.NewTicker(time.Duration(1000) * time.Millisecond) + defer t.Stop() + + select { + case <-t.C: + return 0, errors.New("Timed out") + case <-d.ok: + return n, nil + case err = <-d.err: + return 0, err + } +} + +func (d *device) Recv(sockfd int, buf []byte, flags int, deadline time.Time) (int, error) { + + d.Lock() + defer d.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return 0, err + } + + // 1. Use leftover data first + if len(s.remainder) > 0 { + n := copy(buf, s.remainder) + s.remainder = s.remainder[n:] + return n, nil + } + + // 2. Get new data from the channel + data, ok := <-s.rx + if !ok { + // Socket closed, return EOF + return 0, io.EOF + } + + // 3. Copy data, handle leftovers + n := copy(buf, data) + if n < len(data) { + s.remainder = data[n:] + } + + return n, nil +} + +func (d *device) Close(sockfd int) error { + + d.Lock() + defer d.Unlock() + + s, err := d.getSocket(sockfd) + if err != nil { + return err + } + + // Delete socket only if connection was successful (s.id is set) + if s.id != "" { + cmd := fmt.Sprintf("AT+SOCKETDEL=%s", s.id) + if err = d.execute(cmd, 1000); err != nil { + return err + } + } + + d.sockets[sockfd] = nil + + return nil +} + +func (d *device) SetSockOpt(sockfd, level, opt int, value interface{}) error { + return netdev.ErrNotSupported +} diff --git a/comboat/errors.go b/comboat/errors.go new file mode 100644 index 0000000..5b1bf6d --- /dev/null +++ b/comboat/errors.go @@ -0,0 +1,86 @@ +package comboat + +import ( + "bytes" + "strconv" +) + +var errStrings = map[int]string{ + + // System framework related error codes + + 0: "success", + 1: "The command is not supported (the combo framework contains the command but the current platform has not transplanted or adapted to support it)", + 2: "The command parameters contain unsupported operations (the current platform only supports some operations for this command)", + 3: "The instruction format is incorrect (this refers to the wrong number of parameters, for example, two parameters are required, but only one parameter is entered)", + 4: "Parameter error (the content of the parameter is wrong, for example, a number between 0 and 9 is required, but 10 or xyz is passed in, which is a parameter error)", + 5: "Parameter length error (command length exceeds the maximum supported length)", + 31: "The current command has not ended and needs to report the status asynchronously. This value is used by the state machine to determine the use of the command and no message is returned.", + 32: "Unknown error (or unhandled error type)", + + // Common error codes + + 33: "malloc error", + 34: "Failed to read buf", + 35: "Failed to write buf", + 36: "Configuration error (configuration error loaded from memory, for example, we set port -1 for OTA upgrade, and check port error when executing AT+OTA, then configuration error will be reported)", + 37: "Failed to create task", + 38: "Flash read and write failure", + 39: "Serial port configuration error, unsupported baud rate", + 40: "Serial port configuration error, unsupported data bits", + 41: "Serial port configuration error, unsupported stop bit", + 42: "Serial port configuration error, unsupported parity bit", + 43: "Serial port configuration error, unsupported flow control", + 44: "Serial port configuration failed", + 45: "Wrong username/password", + 46: "Low power mode error or unsupported low power mode", + 47: "Uninitialized configuration data error (including io mapping data)", + 63: "General error code (without other information)", + + // Wi-Fi related error codes + + 64: "Wi-Fi not initialized or initialization failed", + 65: "Wi-Fi mode error (unable to connect to Wi-Fi in single AP mode)", + 66: "Wi-Fi connection failed", + 67: "Wi-Fi connection successful, error in obtaining IP (DHCP)", + 68: "Failed to obtain encryption method", + 69: "The specified AP was not found.", + 70: "Wi-Fi scan start failed", + 71: "Wi-Fi scan timeout", + 72: "Failed to enable AP hotspot", + 73: "Failed to obtain the Wi-Fi information of the router or the AP information that you enabled yourself", + 74: "The network card (STA/AP) is not running", + 75: "Wi-Fi country code error (unsupported Wi-Fi country code)", + 76: "The current network configuration mode is wrong.", + 95: "Wi-Fi connection unknown error", + + // Socket related error codes + + 96: "Failed to create socket", + 97: "Socket connection failed", + 98: "DNS Failure", + 99: "The socket status is wrong (for example, TCP is not connected yet)", + 100: "Socket type error", + 101: "Socket send failed", + 102: "Socket receive failed", + 103: "Socket monitoring thread creation failed", + 104: "Socket bind error", + 105: "The current connection cannot be transparently linked (wrong socket type or number)", + 106: "PING test failed (all packets lost)", + 107: "Wi-Fi country code error (unsupported Wi-Fi country code)", + 108: "SSL Config Error", + 109: "SSL verification error (usually caused by unsupported SSL encryption type or certificate error)", + 127: "Unknown socket error", +} + +func getErrStr(errLine []byte) (errStr string) { + errStr = "Can't parse ERROR response" + tokens := bytes.Split(errLine, []byte(":")) + if len(tokens) > 1 { + errCode, err := strconv.Atoi(string(tokens[1])) + if err == nil { + errStr = errStrings[errCode] + } + } + return +} diff --git a/ds18b20/ds18b20.go b/ds18b20/ds18b20.go index 3553661..ccbb9fd 100644 --- a/ds18b20/ds18b20.go +++ b/ds18b20/ds18b20.go @@ -19,7 +19,7 @@ type OneWireDevice interface { Write(uint8) Read() uint8 Select([]uint8) error - Сrc8([]uint8, int) uint8 + Сrc8([]uint8) uint8 } // Device wraps a connection to an 1-Wire devices. @@ -69,7 +69,7 @@ func (d Device) ReadTemperatureRaw(romid []uint8) ([]uint8, error) { for i := 0; i < 9; i++ { spb[i] = d.owd.Read() } - if d.owd.Сrc8(spb, 8) != spb[8] { + if d.owd.Сrc8(spb) != 0 { return nil, errReadTemperature } return spb[:2:2], nil diff --git a/ds3231/ds3231.go b/ds3231/ds3231.go index 4c6f119..9679a9d 100644 --- a/ds3231/ds3231.go +++ b/ds3231/ds3231.go @@ -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< 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< 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 +} diff --git a/ds3231/ds3231_test.go b/ds3231/ds3231_test.go index 91ba3c7..9b174db 100644 --- a/ds3231/ds3231_test.go +++ b/ds3231/ds3231_test.go @@ -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) } diff --git a/ds3231/registers.go b/ds3231/registers.go index 05f1760..f38b701 100644 --- a/ds3231/registers.go +++ b/ds3231/registers.go @@ -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 +) diff --git a/encoders/quadrature_device.go b/encoders/quadrature_device.go new file mode 100644 index 0000000..0482a92 --- /dev/null +++ b/encoders/quadrature_device.go @@ -0,0 +1,34 @@ +package encoders + +type QuadratureDevice struct { + cfg QuadratureConfig + impl quadratureImpl +} + +type QuadratureConfig struct { + Precision int +} + +type quadratureImpl interface { + configure(cfg QuadratureConfig) error + readValue() int + writeValue(int) +} + +func (enc *QuadratureDevice) Configure(cfg QuadratureConfig) error { + if cfg.Precision < 1 { + cfg.Precision = 4 + } + enc.cfg = cfg + return enc.impl.configure(cfg) +} + +// Position returns the stored int value for the encoder +func (enc *QuadratureDevice) Position() int { + return enc.impl.readValue() / enc.cfg.Precision +} + +// SetPosition overwrites the currently stored value with the specified int value +func (enc *QuadratureDevice) SetPosition(v int) { + enc.impl.writeValue(v * enc.cfg.Precision) +} diff --git a/encoders/quadrature_interrupt.go b/encoders/quadrature_interrupt.go new file mode 100644 index 0000000..8a3b959 --- /dev/null +++ b/encoders/quadrature_interrupt.go @@ -0,0 +1,69 @@ +//go:build tinygo && (rp2040 || rp2350 || stm32 || k210 || esp32c3 || nrf || sam || (avr && (atmega328p || atmega328pb))) + +// Implementation based on: +// https://gist.github.com/aykevl/3fc1683ed77bb0a9c07559dfe857304a + +// Note: build constraints in this file list targets that define machine.PinToggle. +// If this is supported for additional targets in the future, they can be added above. + +package encoders + +import ( + "machine" + "runtime/volatile" +) + +var ( + states = []int8{0, -1, 1, 0, 1, 0, 0, -1, -1, 0, 0, 1, 0, 1, -1, 0} +) + +// NewQuadratureViaInterrupt returns a rotary encoder device that uses GPIO +// interrupts and a lookup table to keep track of quadrature state changes. +// +// This constructur is only available for TinyGo targets for which machine.PinToggle +// is defined as a valid interrupt type. +func NewQuadratureViaInterrupt(pinA, pinB machine.Pin) *QuadratureDevice { + return &QuadratureDevice{impl: &quadInterruptImpl{pinA: pinA, pinB: pinB, oldAB: 0b00000011}} +} + +type quadInterruptImpl struct { + pinA machine.Pin + pinB machine.Pin + + // precision int + + oldAB int + value volatile.Register32 +} + +func (enc *quadInterruptImpl) configure(cfg QuadratureConfig) error { + enc.pinA.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + enc.pinA.SetInterrupt(machine.PinToggle, enc.interrupt) + + enc.pinB.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) + enc.pinB.SetInterrupt(machine.PinToggle, enc.interrupt) + + return nil +} + +func (enc *quadInterruptImpl) interrupt(pin machine.Pin) { + aHigh, bHigh := enc.pinA.Get(), enc.pinB.Get() + enc.oldAB <<= 2 + if aHigh { + enc.oldAB |= 1 << 1 + } + if bHigh { + enc.oldAB |= 1 + } + enc.writeValue(enc.readValue() + int(states[enc.oldAB&0x0f])) +} + +// readValue gets the value using volatile operations and returns it as an int +func (enc *quadInterruptImpl) readValue() int { + return int(enc.value.Get()) +} + +// writeValue set the value to the specified int using volatile operations +func (enc *quadInterruptImpl) writeValue(v int) { + enc.value.Set(uint32(v)) +} diff --git a/ens160/ens160.go b/ens160/ens160.go new file mode 100644 index 0000000..04f5a33 --- /dev/null +++ b/ens160/ens160.go @@ -0,0 +1,225 @@ +// Package ens160 provides a driver for the ScioSense ENS160 digital gas sensor. +// +// Datasheet: https://www.sciosense.com/wp-content/uploads/2023/12/ENS160-Datasheet.pdf +package ens160 + +import ( + "encoding/binary" + "errors" + "time" + + "tinygo.org/x/drivers" +) + +const ( + defaultTimeout = 30 * time.Millisecond + shortTimeout = 1 * time.Millisecond +) + +// Conversion constants for environment data compensation. +const ( + kelvinOffsetMilli = 273150 // 273.15 K in milli-units + tempRawFactor = 64 // As per datasheet for TEMP_IN + humRawFactor = 512 // As per datasheet for RH_IN + milliFactor = 1000 // For converting from milli-units + roundingTerm = milliFactor / 2 // For rounding before integer division +) + +// validityStrings provides human-readable descriptions for validity flags. +var validityStrings = [...]string{ + ValidityNormalOperation: "normal operation", + ValidityWarmUpPhase: "warm-up phase, wait ~3 minutes for valid data", + ValidityInitialStartUpPhase: "initial start-up phase, wait ~1 hour for valid data", + ValidityInvalidOutput: "invalid output", +} + +// Device wraps an I2C connection to an ENS160 device. +type Device struct { + bus drivers.I2C // I²C implementation + addr uint16 // 7‑bit bus address, promoted to uint16 per drivers.I2C + + // shadow registers / last measurements + lastTvocPPB uint16 + lastEco2PPM uint16 + lastAqiUBA uint8 + lastValidity uint8 // Store the latest validity status + + // pre‑allocated buffers + wbuf [5]byte // longest write: reg + 4 bytes (TEMP+RH) + rbuf [5]byte // longest read: DATA burst (5 bytes) +} + +// New returns a new ENS160 driver. +func New(bus drivers.I2C, addr uint16) *Device { + if addr == 0 { + addr = DefaultAddress + } + return &Device{ + bus: bus, + addr: addr, + lastValidity: ValidityInvalidOutput, + } +} + +// Connected returns whether a ENS160 has been found. +func (d *Device) Connected() bool { + d.wbuf[0] = regPartID + err := d.bus.Tx(d.addr, d.wbuf[:1], d.rbuf[:2]) + return err == nil && d.rbuf[0] == LowPartID && d.rbuf[1] == HighPartID +} + +// Configure sets up the device for reading. +func (d *Device) Configure() error { + // 1. Soft-reset. The device will automatically enter IDLE mode. + if err := d.write1(regOpMode, ModeReset); err != nil { + return err + } + time.Sleep(defaultTimeout) + + // 2. Clear GPR registers, then go to STANDARD mode. + if err := d.write1(regCommand, cmdClrGPR); err != nil { + return err + } + time.Sleep(defaultTimeout) + + if err := d.write1(regOpMode, ModeStandard); err != nil { + return err + } + time.Sleep(defaultTimeout) + + return nil +} + +// calculateTempRaw converts temperature from milli-degrees Celsius to the sensor's raw format. +func calculateTempRaw(tempMilliC int32) uint16 { + // Clip temperature + const ( + minC = -40 * 1000 + maxC = 85 * 1000 + ) + if tempMilliC < minC { + tempMilliC = minC + } else if tempMilliC > maxC { + tempMilliC = maxC + } + + // Integer fixed-point conversion to format required by the sensor. + // Formula from datasheet: T_IN = (T_ambient_C + 273.15) * 64 + return uint16((((tempMilliC + kelvinOffsetMilli) * tempRawFactor) + roundingTerm) / milliFactor) +} + +// calculateHumRaw converts relative humidity from milli-percent to the sensor's raw format. +func calculateHumRaw(rhMilliPct int32) uint16 { + // Clip humidity + if rhMilliPct < 0 { + rhMilliPct = 0 + } else if rhMilliPct > 100*1000 { + rhMilliPct = 100 * 1000 + } + + // Integer fixed-point conversion to format required by the sensor. + // Formula from datasheet: RH_IN = (RH_ambient_% * 512) + return uint16(((rhMilliPct * humRawFactor) + roundingTerm) / milliFactor) +} + +// SetEnvDataMilli sets the ambient temperature and humidity for compensation. +// +// tempMilliC is the temperature in milli-degrees Celsius. +// rhMilliPct is the relative humidity in milli-percent. +func (d *Device) SetEnvDataMilli(tempMilliC, rhMilliPct int32) error { + tempRaw := calculateTempRaw(tempMilliC) + humRaw := calculateHumRaw(rhMilliPct) + + d.wbuf[0] = regTempIn // start address (auto‑increment) + binary.LittleEndian.PutUint16(d.wbuf[1:3], tempRaw) + binary.LittleEndian.PutUint16(d.wbuf[3:5], humRaw) + + return d.bus.Tx(d.addr, d.wbuf[:5], nil) +} + +// Update refreshes the concentration measurements. +func (d *Device) Update(which drivers.Measurement) error { + if which&drivers.Concentration == 0 { + return nil // nothing requested + } + + const maxTries = 1000 + var ( + status uint8 + validity uint8 + ) + var gotData bool + + // Poll DEVICE_STATUS until NEWDAT or timeout + for range maxTries { + var err error + status, err = d.read1(regStatus) + if err != nil { + return err + } + if status&statusSTATER != 0 { + return errors.New("ENS160: error (STATER set)") + } + validity = (status & statusValidityMask) >> statusValidityShift + + if status&statusNEWDAT != 0 { + gotData = true + break // Always break when data available + } + time.Sleep(shortTimeout) + } + if !gotData { + return errors.New("ENS160: timeout waiting for NEWDAT") + } + + // Burst-read data regardless of validity state + d.wbuf[0] = regAQI + if err := d.bus.Tx(d.addr, d.wbuf[:1], d.rbuf[:5]); err != nil { + return errors.New("ENS160: burst read failed") + } + + d.lastAqiUBA = d.rbuf[0] + d.lastTvocPPB = binary.LittleEndian.Uint16(d.rbuf[1:3]) + d.lastEco2PPM = binary.LittleEndian.Uint16(d.rbuf[3:5]) + d.lastValidity = validity // Store the validity status + + return nil +} + +// TVOC returns the last total‑VOC concentration in parts‑per‑billion. +func (d *Device) TVOC() uint16 { return d.lastTvocPPB } + +// ECO2 returns the last equivalent CO₂ concentration in parts‑per‑million. +func (d *Device) ECO2() uint16 { return d.lastEco2PPM } + +// AQI returns the last Air‑Quality Index according to UBA (1–5). +func (d *Device) AQI() uint8 { return d.lastAqiUBA } + +// Validity returns the current operating state of the sensor. +func (d *Device) Validity() uint8 { + return d.lastValidity +} + +// ValidityString returns a human-readable string describing the current validity status. +func (d *Device) ValidityString() string { + if int(d.lastValidity) < len(validityStrings) { + return validityStrings[d.lastValidity] + } + return "unknown" +} + +// write1 writes a single byte to a register. +func (d *Device) write1(reg, val uint8) error { + d.wbuf[0] = reg + d.wbuf[1] = val + return d.bus.Tx(d.addr, d.wbuf[:2], nil) +} + +// read1 reads a single byte from a register. +func (d *Device) read1(reg uint8) (uint8, error) { + d.wbuf[0] = reg + if err := d.bus.Tx(d.addr, d.wbuf[:1], d.rbuf[:1]); err != nil { + return 0, err + } + return d.rbuf[0], nil +} diff --git a/ens160/ens160_test.go b/ens160/ens160_test.go new file mode 100644 index 0000000..022de16 --- /dev/null +++ b/ens160/ens160_test.go @@ -0,0 +1,54 @@ +package ens160 + +import ( + "testing" +) + +func TestCalculateTempRaw(t *testing.T) { + testCases := []struct { + name string + tempMilliC int32 + expectedRaw uint16 + }{ + {"25°C", 25000, 19082}, + {"-10.5°C", -10500, 16810}, + {"Min temp", -40000, 14922}, + {"Below min", -50000, 14922}, + {"Max temp", 85000, 22922}, + {"Above max", 90000, 22922}, + {"Zero", 0, 17482}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + raw := calculateTempRaw(tc.tempMilliC) + if raw != tc.expectedRaw { + t.Errorf("expected %d, got %d", tc.expectedRaw, raw) + } + }) + } +} + +func TestCalculateHumRaw(t *testing.T) { + testCases := []struct { + name string + rhMilliPct int32 + expectedRaw uint16 + }{ + {"50%", 50000, 25600}, + {"0%", 0, 0}, + {"100%", 100000, 51200}, + {"Below 0%", -10000, 0}, + {"Above 100%", 110000, 51200}, + {"33.3%", 33300, 17050}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + raw := calculateHumRaw(tc.rhMilliPct) + if raw != tc.expectedRaw { + t.Errorf("expected %d, got %d", tc.expectedRaw, raw) + } + }) + } +} diff --git a/ens160/registers.go b/ens160/registers.go new file mode 100644 index 0000000..0650c91 --- /dev/null +++ b/ens160/registers.go @@ -0,0 +1,65 @@ +package ens160 + +// DefaultAddress is the default I2C address for the ENS160 when the ADDR pin is +// connected to high (3.3V). When connected to low (GND), the address is 0x52. +const DefaultAddress = 0x53 + +// Registers +const ( + regPartID = 0x00 + regOpMode = 0x10 + regConfig = 0x11 + regCommand = 0x12 + regTempIn = 0x13 + regRhIn = 0x15 + regStatus = 0x20 + regAQI = 0x21 + regTVOC = 0x22 + regECO2 = 0x24 + regDataT = 0x30 + regDataRH = 0x32 + regMISR = 0x38 + regGPRWrite = 0x40 + regGPRRead = 0x48 +) + +// Operating modes +const ( + ModeDeepSleep = 0x00 + ModeIdle = 0x01 + ModeStandard = 0x02 + ModeReset = 0xF0 +) + +// Status register bits +const ( + statusSTATAS = 1 << 7 + statusSTATER = 1 << 6 + + statusValidityMask = 0x0C + statusValidityShift = 2 + + statusNEWDAT = 1 << 1 + statusNEWGPR = 1 << 0 +) + +// Validity flags +const ( + ValidityNormalOperation = 0x00 + ValidityWarmUpPhase = 0x01 // need ~3 minutes until valid data + ValidityInitialStartUpPhase = 0x02 // need ~1 hour until valid data + ValidityInvalidOutput = 0x03 +) + +// Commands +const ( + cmdNOP = 0x00 + cmdGetAppVer = 0x0E + cmdClrGPR = 0xCC +) + +// Part IDs +const ( + LowPartID = 0x60 + HighPartID = 0x01 +) diff --git a/espat/espat.go b/espat/espat.go index eed674c..7696e34 100644 --- a/espat/espat.go +++ b/espat/espat.go @@ -122,7 +122,7 @@ func (d *Device) NetDisconnect() { } func (d *Device) NetNotify(cb func(netlink.Event)) { - // Not supported + fmt.Printf("\r\n%s\r\n", netlink.ErrNotSupported) } func (d *Device) GetHostByName(name string) (netip.Addr, error) { @@ -473,7 +473,6 @@ func (d *Device) parseIPD(end int) error { } // load up the socket data - //d.data = append(d.data, d.response[e+1:end]...) d.data = append(d.data, d.response[e+1:e+1+v]...) return nil } diff --git a/examples/bno08x/i2c/main.go b/examples/bno08x/i2c/main.go new file mode 100644 index 0000000..8575434 --- /dev/null +++ b/examples/bno08x/i2c/main.go @@ -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) + } +} diff --git a/examples/ds3231/alarms/main.go b/examples/ds3231/alarms/main.go new file mode 100644 index 0000000..7ef9e87 --- /dev/null +++ b/examples/ds3231/alarms/main.go @@ -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) + } +} diff --git a/examples/ds3231/main.go b/examples/ds3231/basic/main.go similarity index 68% rename from examples/ds3231/main.go rename to examples/ds3231/basic/main.go index 08ec468..e406176 100644 --- a/examples/ds3231/main.go +++ b/examples/ds3231/basic/main.go @@ -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) } diff --git a/examples/encoders/quadrature-interrupt/main.go b/examples/encoders/quadrature-interrupt/main.go new file mode 100644 index 0000000..8ded393 --- /dev/null +++ b/examples/encoders/quadrature-interrupt/main.go @@ -0,0 +1,28 @@ +//go:build macropad_rp2040 + +package main + +import ( + "machine" + + "tinygo.org/x/drivers/encoders" +) + +var ( + enc = encoders.NewQuadratureViaInterrupt(machine.ROT_A, machine.ROT_B) +) + +func main() { + + enc.Configure(encoders.QuadratureConfig{ + Precision: 4, + }) + + for oldValue := 0; ; { + if newValue := enc.Position(); newValue != oldValue { + println("value: ", newValue) + oldValue = newValue + } + } + +} diff --git a/examples/ens160/main.go b/examples/ens160/main.go new file mode 100644 index 0000000..d357160 --- /dev/null +++ b/examples/ens160/main.go @@ -0,0 +1,56 @@ +// This example demonstrates ENS160 usage. +// +// Wiring: +// - VCC to 3.3V, GND to ground +// - SDA to board SDA, SCL to board SCL + +package main + +import ( + "time" + + "machine" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/ens160" +) + +func main() { + err := machine.I2C0.Configure(machine.I2CConfig{ + Frequency: 400 * machine.KHz, + }) + if err != nil { + println("Failed to configure I2C:", err) + } + + dev := ens160.New(machine.I2C0, ens160.DefaultAddress) + + connected := dev.Connected() + if !connected { + println("ENS160 not detected") + return + } + println("ENS160 detected") + + if err := dev.Configure(); err != nil { + println("Failed to configure ENS160:", err) + } + + for { + err := dev.Update(drivers.Concentration) + if err != nil { + println("Error reading ENS160: %v\n", err) + time.Sleep(5 * time.Second) + continue + } + + println( + "AQI:", dev.AQI(), + "TVOC:", dev.TVOC(), + "eCO2:", dev.ECO2(), + "Validity:", dev.ValidityString(), + ) + + time.Sleep(2 * time.Second) + } +} diff --git a/examples/flash/console/spi/main.go b/examples/flash/console/spi/main.go index 21e088d..5d28fa7 100644 --- a/examples/flash/console/spi/main.go +++ b/examples/flash/console/spi/main.go @@ -10,7 +10,7 @@ import ( func main() { console_example.RunFor( flash.NewSPI( - &machine.SPI1, + machine.SPI1, machine.SPI1_SDO_PIN, machine.SPI1_SDI_PIN, machine.SPI1_SCK_PIN, diff --git a/examples/gps/i2c/main.go b/examples/gps/i2c/main.go index 8f7873c..123cc26 100644 --- a/examples/gps/i2c/main.go +++ b/examples/gps/i2c/main.go @@ -10,7 +10,7 @@ import ( func main() { println("GPS I2C Example") machine.I2C0.Configure(machine.I2CConfig{}) - ublox := gps.NewI2C(machine.I2C0) + ublox := gps.NewI2CWithAddress(machine.I2C0, gps.UBLOX_I2C_ADDRESS) parser := gps.NewParser() var fix gps.Fix for { diff --git a/examples/gps/uart/main.go b/examples/gps/uart/main.go index 7e6a2c4..228aa1a 100644 --- a/examples/gps/uart/main.go +++ b/examples/gps/uart/main.go @@ -8,7 +8,6 @@ import ( ) func main() { - println("GPS UART Example") machine.UART1.Configure(machine.UARTConfig{BaudRate: 9600}) ublox := gps.NewUART(machine.UART1) parser := gps.NewParser() @@ -16,14 +15,24 @@ func main() { for { s, err := ublox.NextSentence() if err != nil { - println(err) - continue + switch err { + case gps.ErrUnknownNMEASentence, gps.ErrInvalidNMEASentence, gps.ErrInvalidNMEASentenceLength: + continue + default: + println("sentence error:", err) + continue + } } fix, err = parser.Parse(s) if err != nil { - println(err) - continue + switch err { + case gps.ErrUnknownNMEASentence, gps.ErrInvalidNMEASentence, gps.ErrInvalidNMEASentenceLength: + continue + default: + println("parse error:", err) + continue + } } if fix.Valid { print(fix.Time.Format("15:04:05")) @@ -43,7 +52,10 @@ func main() { } println() } else { - println("No fix") + if fix.Type == gps.GSV { + // GSV sentence provides satellite count even if no fix yet + println(fix.Satellites, "satellites visible") + } } time.Sleep(200 * time.Millisecond) } diff --git a/examples/honeyhsc/example.go b/examples/honeyhsc/example.go new file mode 100644 index 0000000..1f34d21 --- /dev/null +++ b/examples/honeyhsc/example.go @@ -0,0 +1,49 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/honeyhsc" +) + +// Data taken from https://github.com/rodan/honeywell_hsc_ssc_i2c/blob/master/hsc_ssc_i2c.cpp +// these defaults are valid for the HSCMRNN030PA2A3 chip +const ( + i2cAddress = 0x28 + // 10% + outputMinimum = 0x666 + // 90% of 2^14 - 1 + outputMax = 0x399A + // min is 0 for sensors that give absolute values + pressureMin = 0 + // 30psi (and we want results in millipascals) + // pressureMax = 206842.7 + pressureMax = 206843 * 1000 +) + +func main() { + bus := machine.I2C0 + err := bus.Configure(machine.I2CConfig{ + Frequency: 400_000, // 100kHz minimum and 400kHz I2C maximum clock. 50 to 800 for SPI. + SDA: machine.I2C0_SDA_PIN, + SCL: machine.I2C0_SCL_PIN, + }) + if err != nil { + panic(err.Error()) + } + sensor := honeyhsc.NewDevI2C(bus, i2cAddress, outputMinimum, outputMax, pressureMin, pressureMax) + for { + time.Sleep(time.Second) + const measuremask = drivers.Pressure | drivers.Temperature + err := sensor.Update(measuremask) + if err != nil { + println("error updating measurements:", err.Error()) + continue + } + P := sensor.Pressure() + T := sensor.Temperature() + println("pressure:", P, "temperature:", T) + } +} diff --git a/examples/ina219/main.go b/examples/ina219/main.go new file mode 100644 index 0000000..50fcba2 --- /dev/null +++ b/examples/ina219/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/ina219" +) + +func main() { + machine.I2C0.Configure(machine.I2CConfig{}) + + dev := ina219.New(machine.I2C0) + dev.Configure() + + for { + busVoltage, shuntVoltage, current, power, err := dev.Measurements() + if err != nil { + println("Error reading measurements", err) + } + + println("Bus Voltage:", busVoltage, "V") + println("Shunt Voltage:", shuntVoltage/100, "mV") + println("Current:", current, "mA") + println("Power:", power, "mW") + + time.Sleep(10 * time.Millisecond) + } +} diff --git a/examples/lis3dh/main.go b/examples/lis3dh/main.go index cab970e..6ca11c9 100644 --- a/examples/lis3dh/main.go +++ b/examples/lis3dh/main.go @@ -14,9 +14,18 @@ func main() { i2c.Configure(machine.I2CConfig{SCL: machine.SCL1_PIN, SDA: machine.SDA1_PIN}) accel := lis3dh.New(i2c) - accel.Address = lis3dh.Address1 // address on the Circuit Playground Express - accel.Configure() - accel.SetRange(lis3dh.RANGE_2_G) + err := accel.Configure(lis3dh.Config{ + Address: lis3dh.Address1, // address on the Circuit Playground Express + }) + for err != nil { + println("could not configure LIS3DH:", err) + time.Sleep(time.Second) + } + err = accel.SetRange(lis3dh.RANGE_2_G) + for err != nil { + println("could not set acceleration range:", err) + time.Sleep(time.Second) + } println(accel.Connected()) diff --git a/examples/lsm303dlhc/main.go b/examples/lsm303dlhc/main.go new file mode 100644 index 0000000..9e27eaf --- /dev/null +++ b/examples/lsm303dlhc/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/lsm303dlhc" +) + +func main() { + + // LSM303DLHC is connected to the I2C0 bus on Adafruit Feather M4 via pins: 20(SDA) and 21(SCL). + machine.I2C0.Configure(machine.I2CConfig{}) + + sensor := lsm303dlhc.New(machine.I2C0) + //default settings + err := sensor.Configure(lsm303dlhc.Configuration{ + AccelPowerMode: lsm303dlhc.ACCEL_POWER_NORMAL, + AccelRange: lsm303dlhc.ACCEL_RANGE_2G, + AccelDataRate: lsm303dlhc.ACCEL_DATARATE_100HZ, + MagPowerMode: lsm303dlhc.MAG_POWER_NORMAL, + MagSystemMode: lsm303dlhc.MAG_SYSTEM_CONTINUOUS, + MagDataRate: lsm303dlhc.MAG_DATARATE_10HZ, + }) + if err != nil { + for { + println("Failed to configure", err.Error()) + time.Sleep(time.Second) + } + } + + for { + accel_x, accel_y, accel_z, err := sensor.ReadAcceleration() + if err != nil { + println("Failed to read accel", err.Error()) + } + println("ACCEL_X:", accel_x, " ACCEL_Y:", accel_y, " ACCEL_Z:", accel_z) + + mag_x, mag_y, mag_z, err := sensor.ReadMagneticField() + if err != nil { + println("Failed to read mag", err.Error()) + } + println("MAG_X:", mag_x, " MAG_Y:", mag_y, " MAG_Z:", mag_z) + + pitch, roll, _ := sensor.ReadPitchRoll() + println("Pitch:", float32(pitch), " Roll:", float32(roll)) + + heading, _ := sensor.ReadCompass() + println("Heading:", float32(heading), "degrees") + + temp, _ := sensor.ReadTemperature() + println("Temperature:", float32(temp)/1000, "*C") + + println("\n") + time.Sleep(time.Millisecond * 250) + } + +} diff --git a/examples/max6675/main.go b/examples/max6675/main.go new file mode 100644 index 0000000..b583088 --- /dev/null +++ b/examples/max6675/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "machine" + "time" + + "tinygo.org/x/drivers/max6675" +) + +// example for reading temperature from a thermocouple +func main() { + // Pins are for an Adafruit Feather nRF52840 Express + machine.D5.Configure(machine.PinConfig{Mode: machine.PinOutput}) + machine.D5.High() + + machine.SPI0.Configure(machine.SPIConfig{ + Frequency: 1_000_000, + SCK: machine.SPI0_SCK_PIN, + SDI: machine.SPI0_SDI_PIN, + }) + + thermocouple := max6675.NewDevice(machine.SPI0, machine.D5) + + for { + temp, err := thermocouple.Read() + if err != nil { + println(err) + return + } + fmt.Printf("%0.02f C : %0.02f F\n", temp, (temp*9/5)+32) + time.Sleep(time.Second) + } +} diff --git a/examples/mcp2515/main.go b/examples/mcp2515/main.go index 6b7a84f..739ecf2 100644 --- a/examples/mcp2515/main.go +++ b/examples/mcp2515/main.go @@ -21,7 +21,7 @@ func main() { SDI: machine.SPI0_SDI_PIN, Mode: 0}) can := mcp2515.New(spi, csPin) - can.Configure() + can.Configure(mcp2515.Configuration{}) err := can.Begin(mcp2515.CAN500kBps, mcp2515.Clock8MHz) if err != nil { failMessage(err.Error()) diff --git a/examples/mcp9808/main.go b/examples/mcp9808/main.go new file mode 100644 index 0000000..7b2c5ea --- /dev/null +++ b/examples/mcp9808/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + "machine" + "time" + + "tinygo.org/x/drivers/mcp9808" +) + +func main() { + + //tinygo monitor + time.Sleep(time.Millisecond * 5000) + + //Configure I2C (in this case, I2C0 on RPI Pico), and wire the module accordingly + machine.I2C0.Configure(machine.I2CConfig{ + SCL: machine.GP1, + SDA: machine.GP0, + }) + + //Create sensor + sensor := mcp9808.New(machine.I2C0) + if !sensor.Connected() { + println("MCP9808 not found") + return + } else { + println("MCP9808 found") + } + + time.Sleep(time.Millisecond * 1000) + + //Set resolution + sensor.SetResolution(mcp9808.Maximum) + + time.Sleep(time.Millisecond * 1000) + + //Read temp. + temp, err := sensor.ReadTemperature() + if err != nil { + println("MCP9808 error reading temperature") + println(err.Error()) + return + } else { + fmt.Printf("Temperature: %.2f \n", temp) + } + return +} diff --git a/examples/net/http-get/main.go b/examples/net/http-get/main.go index 7f8de67..aa51acc 100644 --- a/examples/net/http-get/main.go +++ b/examples/net/http-get/main.go @@ -9,7 +9,7 @@ // examples/net/webclient (for HTTP) // examples/net/tlsclient (for HTTPS) -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/examples/net/http-head/main.go b/examples/net/http-head/main.go index 07c1f45..fe90397 100644 --- a/examples/net/http-head/main.go +++ b/examples/net/http-head/main.go @@ -9,7 +9,7 @@ // examples/net/webclient (for HTTP) // examples/net/tlsclient (for HTTPS) -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/examples/net/http-post/main.go b/examples/net/http-post/main.go index d28f62d..b5f55b1 100644 --- a/examples/net/http-post/main.go +++ b/examples/net/http-post/main.go @@ -9,7 +9,7 @@ // examples/net/webclient (for HTTP) // examples/net/tlsclient (for HTTPS) -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/examples/net/http-postform/main.go b/examples/net/http-postform/main.go index e72aac8..c5757fa 100644 --- a/examples/net/http-postform/main.go +++ b/examples/net/http-postform/main.go @@ -9,7 +9,7 @@ // examples/net/webclient (for HTTP) // examples/net/tlsclient (for HTTPS) -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/examples/net/mqttclient/natiu/main.go b/examples/net/mqttclient/natiu/main.go index 8d82fe7..84111f5 100644 --- a/examples/net/mqttclient/natiu/main.go +++ b/examples/net/mqttclient/natiu/main.go @@ -4,7 +4,7 @@ // Note: It may be necessary to increase the stack size when using // paho.mqtt.golang. Use the -stack-size=4KB command line option. -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal || challenger_rp2040 +//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw package main diff --git a/examples/net/mqttclient/paho/main.go b/examples/net/mqttclient/paho/main.go index 293e24d..a080b0d 100644 --- a/examples/net/mqttclient/paho/main.go +++ b/examples/net/mqttclient/paho/main.go @@ -4,7 +4,7 @@ // Note: It may be necessary to increase the stack size when using // paho.mqtt.golang. Use the -stack-size=4KB command line option. -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal || challenger_rp2040 +//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw package main diff --git a/examples/net/ntpclient/main.go b/examples/net/ntpclient/main.go index 6c84193..9ea079d 100644 --- a/examples/net/ntpclient/main.go +++ b/examples/net/ntpclient/main.go @@ -3,7 +3,7 @@ // It creates a UDP connection to request the current time and parse the // response from a NTP server. The system time is set to NTP time. -//go:build pyportal || arduino_nano33 || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal || challenger_rp2040 +//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw package main diff --git a/examples/net/snake/init.go b/examples/net/snake/init.go new file mode 100644 index 0000000..641ecd5 --- /dev/null +++ b/examples/net/snake/init.go @@ -0,0 +1,30 @@ +//go:build ninafw || wioterminal || comboat_fw + +package main + +import ( + "log" + "time" + + "tinygo.org/x/drivers/netlink" + "tinygo.org/x/drivers/netlink/probe" +) + +var ( + ssid string + pass string +) + +func init() { + time.Sleep(2 * time.Second) + + link, _ := probe.Probe() + + err := link.NetConnect(&netlink.ConnectParams{ + Ssid: ssid, + Passphrase: pass, + }) + if err != nil { + log.Fatal(err) + } +} diff --git a/examples/net/snake/main.go b/examples/net/snake/main.go new file mode 100644 index 0000000..a261fc1 --- /dev/null +++ b/examples/net/snake/main.go @@ -0,0 +1,120 @@ +// This example is the classic snake network test. The snake is feed a steady +// diet of pkts and the pkts work themselves thru the snake segments and exit +// the tail. Each snake segment is a TCP socket connection to a server. The +// server echos pkts received back to the snake, and serves each segment on a +// different port. (See server/main.go for server). +// +// snake | server +// | +// head ----->---|-->--+ +// seg a | | +// +---<-|--<--+ +// | | +// +-->--|-->--+ +// seg b | | +// +---<-|--<--+ +// | | +// +-->--|-->--+ +// seg c | | +// +---<-|--<--+ +// | | +// +-->--|-->--+ +// ... | | +// +---<-|--<--+ +// | | +// +-->--|-->--+ +// seg n | | +// tail -------<-|--<--+ +// | + +// The snake segments are linked by channels and each segment is run as a go +// func. This forces segments to connect and run concurrently, which is a good +// test of the underlying driver's ability to handle concurrent connections. + +//go:build ninafw || wioterminal || comboat_fw + +package main + +import ( + _ "embed" + "fmt" + "log" + "net" + "strings" + "time" +) + +//go:embed main.go +var code string + +var ( + server string = "10.0.0.100:8080" +) + +func segment(in chan []byte, out chan []byte) { + var buf [512]byte + for { + c, err := net.Dial("tcp", server) + for ; err != nil; c, err = net.Dial("tcp", server) { + println(err.Error()) + time.Sleep(5 * time.Second) + } + for { + select { + case msg := <-in: + _, err := c.Write(msg) + if err != nil { + log.Fatal(err.Error()) + } + time.Sleep(100 * time.Millisecond) + n, err := c.Read(buf[:]) + if err != nil { + log.Fatal(err.Error()) + } + out <- buf[:n] + } + } + } +} + +func feedit(head chan []byte) { + for i := 0; i < 100; i++ { + head <- []byte(fmt.Sprintf("\n---%d---\n", i)) + for _, line := range strings.Split(code, "\n") { + if len(line) == 0 { + line = " " + } + head <- []byte(line) + } + } +} + +var head = make(chan []byte) +var a = make(chan []byte) +var b = make(chan []byte) +var c = make(chan []byte) +var d = make(chan []byte) +var e = make(chan []byte) +var f = make(chan []byte) +var tail = make(chan []byte) + +func main() { + + // The snake + go segment(head, a) + go segment(a, b) + go segment(b, c) + go segment(c, d) + go segment(d, e) + go segment(e, f) + go segment(f, tail) + + go feedit(head) + + for { + select { + case msg := <-tail: + println(string(msg)) + } + } +} diff --git a/examples/net/snake/server/main.go b/examples/net/snake/server/main.go new file mode 100644 index 0000000..e67b3eb --- /dev/null +++ b/examples/net/snake/server/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "io" + "log" + "net" +) + +func main() { + // Listen for connections + l, err := net.Listen("tcp", ":8080") + if err != nil { + log.Fatal(err.Error()) + } + defer l.Close() + println("Listening on port", ":8080") + for { + // Wait for a connection + conn, err := l.Accept() + if err != nil { + log.Fatal(err) + } + println("Accepted connection from", conn.RemoteAddr().String()) + // Service the new connection in a goroutine. + // The loop then returns to accepting, so that + // multiple connections may be served concurrently + go func(c net.Conn) { + // Echo all incoming data + io.Copy(c, c) + // Shut down the connection + c.Close() + }(conn) + } +} diff --git a/examples/net/socket/main.go b/examples/net/socket/main.go index cf8bd93..2e867fd 100644 --- a/examples/net/socket/main.go +++ b/examples/net/socket/main.go @@ -4,7 +4,7 @@ // // nc -lk 8080 -//go:build pyportal || arduino_nano33 || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal || challenger_rp2040 +//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw package main diff --git a/examples/net/tcpclient/main.go b/examples/net/tcpclient/main.go index 10d2935..24f0775 100644 --- a/examples/net/tcpclient/main.go +++ b/examples/net/tcpclient/main.go @@ -5,7 +5,7 @@ // // nc -lk 8080 -//go:build pyportal || arduino_nano33 || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal || challenger_rp2040 || pico +//go:build ninafw || wioterminal || challenger_rp2040 || comboat_fw package main diff --git a/examples/net/tcpecho/main.go b/examples/net/tcpecho/main.go index 27faf8f..274dc04 100644 --- a/examples/net/tcpecho/main.go +++ b/examples/net/tcpecho/main.go @@ -7,7 +7,7 @@ // // $ nc 10.0.0.2 8080 copy ; cmp file copy -//go:build pyportal || arduino_nano33 || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal package main diff --git a/examples/net/tlsclient/main.go b/examples/net/tlsclient/main.go index 186fdd5..adef220 100644 --- a/examples/net/tlsclient/main.go +++ b/examples/net/tlsclient/main.go @@ -5,7 +5,7 @@ // // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/examples/net/webclient/main.go b/examples/net/webclient/main.go index bb2fe9d..6b28783 100644 --- a/examples/net/webclient/main.go +++ b/examples/net/webclient/main.go @@ -17,7 +17,7 @@ // } // --------------------------------------------------------------------------- -//go:build pyportal || arduino_nano33 || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/examples/net/webserver/main.go b/examples/net/webserver/main.go index 84f0fb4..4b7860c 100644 --- a/examples/net/webserver/main.go +++ b/examples/net/webserver/main.go @@ -6,7 +6,7 @@ // Note: It may be necessary to increase the stack size when using "net/http". // Use the -stack-size=4KB command line option. -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal package main diff --git a/examples/net/websocket/dial/main.go b/examples/net/websocket/dial/main.go index 5a7288d..02e17da 100644 --- a/examples/net/websocket/dial/main.go +++ b/examples/net/websocket/dial/main.go @@ -6,7 +6,7 @@ // Note: It may be necessary to increase the stack size when using // "golang.org/x/net/websocket". Use the -stack-size=4KB command line option. -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal || comboat_fw package main diff --git a/examples/net/websocket/handler/main.go b/examples/net/websocket/handler/main.go index 709f14c..f03e32c 100644 --- a/examples/net/websocket/handler/main.go +++ b/examples/net/websocket/handler/main.go @@ -6,7 +6,7 @@ // Note: It may be necessary to increase the stack size when using // "golang.org/x/net/websocket". Use the -stack-size=4KB command line option. -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal package main diff --git a/examples/net/webstatic/main.go b/examples/net/webstatic/main.go index 9130822..b2d8bb9 100644 --- a/examples/net/webstatic/main.go +++ b/examples/net/webstatic/main.go @@ -3,7 +3,7 @@ // Note: It may be necessary to increase the stack size when using "net/http". // Use the -stack-size=4KB command line option. -//go:build pyportal || nano_rp2040 || metro_m4_airlift || arduino_mkrwifi1010 || matrixportal_m4 || wioterminal +//go:build ninafw || wioterminal package main diff --git a/examples/pcf8591/main.go b/examples/pcf8591/main.go new file mode 100644 index 0000000..3994224 --- /dev/null +++ b/examples/pcf8591/main.go @@ -0,0 +1,28 @@ +// Connects to a pcf8591 ADC via I2C. +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/pcf8591" +) + +var ( + i2c = machine.I2C0 +) + +func main() { + i2c.Configure(machine.I2CConfig{}) + adc := pcf8591.New(i2c) + adc.Configure() + + // get "CH0" aka "machine.ADC" interface to channel 0 from ADC. + p := adc.CH0 + + for { + val := p.Get() + println(val) + time.Sleep(50 * time.Millisecond) + } +} diff --git a/examples/sdcard/console/feather-m4.go b/examples/sdcard/console/feather-m4.go index 258aac5..dae1fb0 100644 --- a/examples/sdcard/console/feather-m4.go +++ b/examples/sdcard/console/feather-m4.go @@ -7,7 +7,7 @@ import ( ) func init() { - spi = &machine.SPI0 + spi = machine.SPI0 sckPin = machine.SPI0_SCK_PIN sdoPin = machine.SPI0_SDO_PIN sdiPin = machine.SPI0_SDI_PIN diff --git a/examples/sdcard/console/grandcentral-m4.go b/examples/sdcard/console/grandcentral-m4.go index 51eb5c2..ed13803 100644 --- a/examples/sdcard/console/grandcentral-m4.go +++ b/examples/sdcard/console/grandcentral-m4.go @@ -7,7 +7,7 @@ import ( ) func init() { - spi = &machine.SPI1 + spi = machine.SPI1 sckPin = machine.SDCARD_SCK_PIN sdoPin = machine.SDCARD_SDO_PIN sdiPin = machine.SDCARD_SDI_PIN diff --git a/examples/sdcard/console/m0.go b/examples/sdcard/console/m0.go index 5ba5391..fcb0b03 100644 --- a/examples/sdcard/console/m0.go +++ b/examples/sdcard/console/m0.go @@ -7,7 +7,7 @@ import ( ) func init() { - spi = &machine.SPI0 + spi = machine.SPI0 sckPin = machine.SPI0_SCK_PIN sdoPin = machine.SPI0_SDO_PIN sdiPin = machine.SPI0_SDI_PIN diff --git a/examples/sdcard/console/p1am-100.go b/examples/sdcard/console/p1am-100.go index b1acb62..b003042 100644 --- a/examples/sdcard/console/p1am-100.go +++ b/examples/sdcard/console/p1am-100.go @@ -7,7 +7,7 @@ import ( ) func init() { - spi = &machine.SDCARD_SPI + spi = machine.SDCARD_SPI sckPin = machine.SDCARD_SCK_PIN sdoPin = machine.SDCARD_SDO_PIN sdiPin = machine.SDCARD_SDI_PIN diff --git a/examples/sdcard/console/pygamer.go b/examples/sdcard/console/pygamer.go index 3518244..cc1187c 100644 --- a/examples/sdcard/console/pygamer.go +++ b/examples/sdcard/console/pygamer.go @@ -7,7 +7,7 @@ import ( ) func init() { - spi = &machine.SPI0 + spi = machine.SPI0 sckPin = machine.SPI0_SCK_PIN sdoPin = machine.SPI0_SDO_PIN sdiPin = machine.SPI0_SDI_PIN diff --git a/examples/sdcard/console/pyportal.go b/examples/sdcard/console/pyportal.go index 9a8accb..3599cdf 100644 --- a/examples/sdcard/console/pyportal.go +++ b/examples/sdcard/console/pyportal.go @@ -7,7 +7,7 @@ import ( ) func init() { - spi = &machine.SPI0 + spi = machine.SPI0 sckPin = machine.SPI0_SCK_PIN sdoPin = machine.SPI0_SDO_PIN sdiPin = machine.SPI0_SDI_PIN diff --git a/examples/sdcard/console/wioterminal.go b/examples/sdcard/console/wioterminal.go index 96019e3..934b87a 100644 --- a/examples/sdcard/console/wioterminal.go +++ b/examples/sdcard/console/wioterminal.go @@ -7,7 +7,7 @@ import ( ) func init() { - spi = &machine.SPI2 + spi = machine.SPI2 sckPin = machine.SCK2 sdoPin = machine.SDO2 sdiPin = machine.SDI2 diff --git a/examples/seesaw/rotary-encoder/main.go b/examples/seesaw/rotary-encoder/main.go new file mode 100644 index 0000000..fcd4571 --- /dev/null +++ b/examples/seesaw/rotary-encoder/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/seesaw" +) + +// example reading the position of a rotary encoder (4991) powered by a seesaw +// https://learn.adafruit.com/adafruit-i2c-qt-rotary-encoder/arduino +func main() { + // This assumes you are using an Adafruit QT Py RP2040 for its Stemma QT connector + // https://www.adafruit.com/product/4900 + i2c := machine.I2C1 + i2c.Configure(machine.I2CConfig{ + SCL: machine.I2C1_QT_SCL_PIN, + SDA: machine.I2C1_QT_SDA_PIN, + }) + + dev := seesaw.New(i2c) + dev.Address = 0x36 + + for { + time.Sleep(time.Second) + + pos, err := dev.GetEncoderPosition(0, false) + if err != nil { + println(err) + continue + } + + println(pos) + } +} diff --git a/examples/seesaw/main.go b/examples/seesaw/soil-sensor/main.go similarity index 100% rename from examples/seesaw/main.go rename to examples/seesaw/soil-sensor/main.go diff --git a/examples/servo/servo.go b/examples/servo/servo.go index c986d84..a38d25d 100644 --- a/examples/servo/servo.go +++ b/examples/servo/servo.go @@ -25,19 +25,25 @@ func main() { return } - println("setting to 0°") - s.SetMicroseconds(1000) - time.Sleep(3 * time.Second) - - println("setting to 45°") - s.SetMicroseconds(1500) - time.Sleep(3 * time.Second) - - println("setting to 90°") - s.SetMicroseconds(2000) - time.Sleep(3 * time.Second) - for { - time.Sleep(time.Second) + println("setting to 0°") + s.SetAngle(0) + time.Sleep(3 * time.Second) + + println("setting to 45°") + s.SetAngle(45) + time.Sleep(3 * time.Second) + + println("setting to 90°") + s.SetAngle(90) + time.Sleep(3 * time.Second) + + println("setting to 135°") + s.SetAngle(135) + time.Sleep(3 * time.Second) + + println("setting to 180°") + s.SetAngle(180) + time.Sleep(3 * time.Second) } } diff --git a/examples/sharpmem/main.go b/examples/sharpmem/main.go new file mode 100644 index 0000000..2a3f2d7 --- /dev/null +++ b/examples/sharpmem/main.go @@ -0,0 +1,90 @@ +package main + +import ( + "image/color" + "machine" + "math/rand/v2" + "time" + + "tinygo.org/x/drivers/sharpmem" +) + +var ( + // example wiring using a nice!view and nice!nano: + // (view) (nano) + // MOSI --> P0.24 + // SCK ---> P0.22 + // GND ---> GND + // VCC ---> 3.3V + // CS ----> P0.06 + + spi = machine.SPI0 + + sckPin = machine.SPI0_SCK_PIN // SCK + sdoPin = machine.SPI0_SDO_PIN // MOSI + sdiPin = machine.SPI0_SDI_PIN // (any pin) + + csPin = machine.P0_06 // CS +) + +func main() { + time.Sleep(time.Second) + + err := spi.Configure(machine.SPIConfig{ + Frequency: 2000000, + SCK: sckPin, + SDO: sdoPin, + SDI: sdiPin, + Mode: 0, + LSBFirst: true, + }) + if err != nil { + println("spi.Configure() failed, error:", err.Error()) + return + } + + csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + display := sharpmem.New(spi, csPin) + + cfg := sharpmem.ConfigLS011B7DH03 + display.Configure(cfg) + + // clear the display before first use + err = display.Clear() + if err != nil { + println("display.Clear() failed, error:", err.Error()) + return + } + + // random boxes pop into and out of existence + for { + x0 := int16(rand.IntN(int(cfg.Width - 7))) + y0 := int16(rand.IntN(int(cfg.Height - 7))) + + for x2 := int16(0); x2 < 16; x2++ { + x2 := x2 + c := color.RGBA{R: 255, G: 255, B: 255, A: 255} + + if x2 >= 8 { + // effectively erases the box after it showed up + x2 = x2 - 8 + c = color.RGBA{R: 0, G: 0, B: 0, A: 255} + } + + for x := int16(0); x < x2; x++ { + for y := int16(0); y < 8; y++ { + display.SetPixel(x0+x, y0+y, c) + } + } + + err = display.Display() + if err != nil { + println("display.Display() failed, error:", err.Error()) + continue + } + + time.Sleep(33 * time.Millisecond) + } + } +} diff --git a/examples/si5351/main.go b/examples/si5351/main.go new file mode 100644 index 0000000..4a47754 --- /dev/null +++ b/examples/si5351/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/si5351" +) + +// Simple demo of the SI5351 clock generator. +// This is like the Arduino library example: +// https://github.com/adafruit/Adafruit_Si5351_Library/blob/master/examples/si5351/si5351.ino +// Which will configure the chip with: +// - PLL A at 900mhz +// - PLL B at 616.66667mhz +// - Clock 0 at 112.5mhz, using PLL A as a source divided by 8 +// - Clock 1 at 13.5531mhz, using PLL B as a source divided by 45.5 +// - Clock 2 at 10.76khz, using PLL B as a source divided by 900 and further divided with an R divider of 64. + +func main() { + time.Sleep(5 * time.Second) + + println("Si5351 Clockgen Test") + println() + + // Configure I2C bus + machine.I2C0.Configure(machine.I2CConfig{}) + + // Create driver instance + clockgen := si5351.New(machine.I2C0) + + // Initialize device + cnf := si5351.Config{ + Capacitance: si5351.CrystalLoad10PF, + } + + if err := clockgen.Configure(cnf); err != nil { + println("Failed to configure Si5351:", err.Error()) + return + } + println("Si5351 configured") + + // Now configure the clock outputs. + clockgen.SetFrequency(si5351.Clock0, 112_500_000) + println("Clock 0: 112.5mhz") + + // Next configure clock 1 for 13.5531mhz (616.6667mhz / 45.5). + // This uses fractional division. + clockgen.SetFrequency(si5351.Clock1, 13_553_125) + println("Clock 1: 13.5531mhz") + + // Finally configure clock 2 to output of 10.706khz. + clockgen.SetFrequency(si5351.Clock2, 10_706) + println("Clock 2: 10.706khz") + + // After configuring the clocks enable the outputs. + clockgen.EnableOutput(si5351.Clock0, true) + clockgen.EnableOutput(si5351.Clock1, true) + clockgen.EnableOutput(si5351.Clock2, true) + println("All outputs enabled") + + time.Sleep(time.Second) + + clockgen.EnableOutput(si5351.Clock0, false) + clockgen.EnableOutput(si5351.Clock1, false) + clockgen.EnableOutput(si5351.Clock2, false) + println("All outputs disabled for 5 seconds") + time.Sleep(5 * time.Second) + + // Now turn clock outputs on and off repeatedly + on := false + for { + if on { + println("Setting clock outputs off") + clockgen.EnableOutput(si5351.Clock0, false) + clockgen.EnableOutput(si5351.Clock1, false) + clockgen.EnableOutput(si5351.Clock2, false) + on = false + } else { + println("Setting clock outputs on") + clockgen.EnableOutput(si5351.Clock0, true) + clockgen.EnableOutput(si5351.Clock1, true) + clockgen.EnableOutput(si5351.Clock2, true) + on = true + } + time.Sleep(1 * time.Second) + } +} diff --git a/examples/ssd1289/main.go b/examples/ssd1289/main.go index c68f328..96a2176 100644 --- a/examples/ssd1289/main.go +++ b/examples/ssd1289/main.go @@ -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 diff --git a/examples/ssd1306/i2c_128x32/main.go b/examples/ssd1306/i2c_128x32/main.go deleted file mode 100644 index 8baf37d..0000000 --- a/examples/ssd1306/i2c_128x32/main.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "machine" - - "image/color" - "time" - - "tinygo.org/x/drivers/ssd1306" -) - -func main() { - machine.I2C0.Configure(machine.I2CConfig{ - Frequency: machine.TWI_FREQ_400KHZ, - }) - - display := ssd1306.NewI2C(machine.I2C0) - display.Configure(ssd1306.Config{ - Address: ssd1306.Address_128_32, - Width: 128, - Height: 32, - }) - - display.ClearDisplay() - - x := int16(0) - y := int16(0) - deltaX := int16(1) - deltaY := int16(1) - for { - pixel := display.GetPixel(x, y) - c := color.RGBA{255, 255, 255, 255} - if pixel { - c = color.RGBA{0, 0, 0, 255} - } - display.SetPixel(x, y, c) - display.Display() - - x += deltaX - y += deltaY - - if x == 0 || x == 127 { - deltaX = -deltaX - } - - if y == 0 || y == 31 { - deltaY = -deltaY - } - time.Sleep(1 * time.Millisecond) - } -} diff --git a/examples/ssd1306/i2c_128x64/main.go b/examples/ssd1306/i2c_128x64/main.go deleted file mode 100644 index a17010d..0000000 --- a/examples/ssd1306/i2c_128x64/main.go +++ /dev/null @@ -1,60 +0,0 @@ -// This example shows how to use 128x64 display over I2C -// Tested on Seeeduino XIAO Expansion Board https://wiki.seeedstudio.com/Seeeduino-XIAO-Expansion-Board/ -// -// According to manual, I2C address of the display is 0x78, but that's 8-bit address. -// TinyGo operates on 7-bit addresses and respective 7-bit address would be 0x3C, which we use below. -// -// To learn more about different types of I2C addresses, please see following page -// https://www.totalphase.com/support/articles/200349176-7-bit-8-bit-and-10-bit-I2C-Slave-Addressing - -package main - -import ( - "machine" - - "image/color" - "time" - - "tinygo.org/x/drivers/ssd1306" -) - -func main() { - machine.I2C0.Configure(machine.I2CConfig{ - Frequency: machine.TWI_FREQ_400KHZ, - }) - - display := ssd1306.NewI2C(machine.I2C0) - display.Configure(ssd1306.Config{ - Address: 0x3C, - Width: 128, - Height: 64, - }) - - display.ClearDisplay() - - x := int16(0) - y := int16(0) - deltaX := int16(1) - deltaY := int16(1) - for { - pixel := display.GetPixel(x, y) - c := color.RGBA{255, 255, 255, 255} - if pixel { - c = color.RGBA{0, 0, 0, 255} - } - display.SetPixel(x, y, c) - display.Display() - - x += deltaX - y += deltaY - - if x == 0 || x == 127 { - deltaX = -deltaX - } - - if y == 0 || y == 63 { - deltaY = -deltaY - } - time.Sleep(1 * time.Millisecond) - } -} diff --git a/examples/ssd1306/main.go b/examples/ssd1306/main.go new file mode 100644 index 0000000..a7f412d --- /dev/null +++ b/examples/ssd1306/main.go @@ -0,0 +1,59 @@ +package main + +// This example shows how to use SSD1306 OLED display driver over I2C and SPI. +// +// Check the `newSSD1306Display()` functions for I2C and SPI initializations. + +import ( + "runtime" + + "image/color" + "time" +) + +func main() { + + display := newSSD1306Display() + display.ClearDisplay() + + w, h := display.Size() + x := int16(0) + y := int16(0) + deltaX := int16(1) + deltaY := int16(1) + + traceTime := time.Now().UnixMilli() + 1000 + frames := 0 + ms := runtime.MemStats{} + + for { + pixel := display.GetPixel(x, y) + c := color.RGBA{255, 255, 255, 255} + if pixel { + c = color.RGBA{0, 0, 0, 255} + } + display.SetPixel(x, y, c) + display.Display() + + x += deltaX + y += deltaY + + if x == 0 || x == w-1 { + deltaX = -deltaX + } + + if y == 0 || y == h-1 { + deltaY = -deltaY + } + + frames++ + now := time.Now().UnixMilli() + if now >= traceTime { + runtime.ReadMemStats(&ms) + println("TS", now, "| FPS", frames, "| HeapInuse", ms.HeapInuse) + traceTime = now + 1000 + frames = 0 + } + } + +} diff --git a/examples/ssd1306/main_i2c_xiao-ble.go b/examples/ssd1306/main_i2c_xiao-ble.go new file mode 100644 index 0000000..c074df2 --- /dev/null +++ b/examples/ssd1306/main_i2c_xiao-ble.go @@ -0,0 +1,38 @@ +//go:build xiao_ble + +// This initializes SSD1306 OLED display driver over I2C. +// +// Seeed XIAO BLE board + SSD1306 128x32 I2C OLED display. +// +// Wiring: +// - XIAO GND -> OLED GND +// - XIAO 3v3 -> OLED VCC +// - XIAO D4 (SDA) -> OLED SDA +// - XIAO D5 (SCL) -> OLED SCK +// +// For your case: +// - Connect the display to I2C pins on your board. +// - Adjust I2C address and display size as needed. + +package main + +import ( + "machine" + + "tinygo.org/x/drivers/ssd1306" +) + +func newSSD1306Display() *ssd1306.Device { + machine.I2C0.Configure(machine.I2CConfig{ + Frequency: 400 * machine.KHz, + SDA: machine.SDA0_PIN, + SCL: machine.SCL0_PIN, + }) + display := ssd1306.NewI2C(machine.I2C0) + display.Configure(ssd1306.Config{ + Address: ssd1306.Address_128_32, // or ssd1306.Address + Width: 128, + Height: 32, // or 64 + }) + return display +} diff --git a/examples/ssd1306/main_spi_thumby.go b/examples/ssd1306/main_spi_thumby.go new file mode 100644 index 0000000..f68164b --- /dev/null +++ b/examples/ssd1306/main_spi_thumby.go @@ -0,0 +1,27 @@ +//go:build thumby + +// This initializes SSD1306 OLED display driver over SPI. +// +// Thumby board has a tiny built-in 72x40 display. +// +// As the display is built-in, no wiring is needed. + +package main + +import ( + "machine" + + "tinygo.org/x/drivers/ssd1306" +) + +func newSSD1306Display() *ssd1306.Device { + machine.SPI0.Configure(machine.SPIConfig{}) + display := ssd1306.NewSPI(machine.SPI0, machine.THUMBY_DC_PIN, machine.THUMBY_RESET_PIN, machine.THUMBY_CS_PIN) + display.Configure(ssd1306.Config{ + Width: 72, + Height: 40, + ResetCol: ssd1306.ResetValue{28, 99}, + ResetPage: ssd1306.ResetValue{0, 5}, + }) + return display +} diff --git a/examples/ssd1306/main_spi_xiao-rp2040.go b/examples/ssd1306/main_spi_xiao-rp2040.go new file mode 100644 index 0000000..fd50bd4 --- /dev/null +++ b/examples/ssd1306/main_spi_xiao-rp2040.go @@ -0,0 +1,40 @@ +//go:build xiao_rp2040 + +// This initializes SSD1306 OLED display driver over SPI. +// +// Seeed XIAO RP2040 board + SSD1306 128x64 SPI OLED display. +// +// Wiring: +// - XIAO GND -> OLED GND +// - XIAO 3v3 -> OLED VCC +// - XIAO D8 (SCK) -> OLED D0 +// - XIAO D10 (SDO) -> OLED D1 +// - XIAO D4 -> OLED RES +// - XIAO D5 -> OLED DC +// - XIAO D6 -> OLED CS +// +// For your case: +// - Connect the display to SPI pins on your board. +// - Adjust RES, DC and CS pins as needed. +// - Adjust SPI frequency as needed. +// - Adjust display size as needed. + +package main + +import ( + "machine" + + "tinygo.org/x/drivers/ssd1306" +) + +func newSSD1306Display() *ssd1306.Device { + machine.SPI0.Configure(machine.SPIConfig{ + Frequency: 50 * machine.MHz, + }) + display := ssd1306.NewSPI(machine.SPI0, machine.D5, machine.D4, machine.D6) + display.Configure(ssd1306.Config{ + Width: 128, + Height: 64, + }) + return display +} diff --git a/examples/ssd1306/spi_128x64/main.go b/examples/ssd1306/spi_128x64/main.go deleted file mode 100644 index 094f5ca..0000000 --- a/examples/ssd1306/spi_128x64/main.go +++ /dev/null @@ -1,48 +0,0 @@ -package main - -import ( - "image/color" - "machine" - "time" - - "tinygo.org/x/drivers/ssd1306" -) - -func main() { - machine.SPI0.Configure(machine.SPIConfig{ - Frequency: 8000000, - }) - display := ssd1306.NewSPI(machine.SPI0, machine.P8, machine.P7, machine.P9) - display.Configure(ssd1306.Config{ - Width: 128, - Height: 64, - }) - - display.ClearDisplay() - - x := int16(64) - y := int16(32) - deltaX := int16(1) - deltaY := int16(1) - for { - pixel := display.GetPixel(x, y) - c := color.RGBA{255, 255, 255, 255} - if pixel { - c = color.RGBA{0, 0, 0, 255} - } - display.SetPixel(x, y, c) - display.Display() - - x += deltaX - y += deltaY - - if x == 0 || x == 127 { - deltaX = -deltaX - } - - if y == 0 || y == 63 { - deltaY = -deltaY - } - time.Sleep(1 * time.Millisecond) - } -} diff --git a/examples/sx128x/lora_rx/main.go b/examples/sx128x/lora_rx/main.go new file mode 100644 index 0000000..33ea60d --- /dev/null +++ b/examples/sx128x/lora_rx/main.go @@ -0,0 +1,106 @@ +package main + +import ( + "errors" + "machine" + "runtime" + "time" + + "tinygo.org/x/drivers/sx128x" +) + +var ( + // pin mapping specific to the lilygo t3s3, change as needed for your board + sdoPin = machine.GPIO6 + sdiPin = machine.GPIO3 + sckPin = machine.GPIO5 + nssPin = machine.GPIO7 + busyPin = machine.GPIO36 + resetPin = machine.GPIO8 + dio1Pin = machine.GPIO9 +) + +func setupPins() { + nssPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + nssPin.Set(true) + + resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + resetPin.Set(true) + + busyPin.Configure(machine.PinConfig{Mode: machine.PinInput}) + dio1Pin.Configure(machine.PinConfig{Mode: machine.PinInput}) +} + +func main() { + setupPins() + + spi := machine.SPI0 + spi.Configure(machine.SPIConfig{ + Mode: 0, + Frequency: 8 * 1e6, + SDO: sdoPin, + SDI: sdiPin, + SCK: sckPin, + }) + + radio := sx128x.New( + spi, + nssPin, + resetPin, + busyPin, + ) + + radio.WaitWhileBusy(time.Second) + SetupLora(radio) + + for { + data, err := Rx(radio) + if err != nil { + println("failed to receive:", err) + } else { + println("received:", string(data)) + } + } +} + +func SetupLora(radio *sx128x.Device) { + radio.SetStandby(sx128x.STANDBY_RC) + radio.SetPacketType(sx128x.PACKET_TYPE_LORA) + radio.SetRegulatorMode(sx128x.REGULATOR_DC_DC) + + radio.SetRfFrequency(2400000000) // 2.4Ghz + radio.SetModulationParamsLoRa(sx128x.LORA_SF_9, sx128x.LORA_BW_1600, sx128x.LORA_CR_4_7) + + // section 14.4.1 shows required register setting for setting up LoRa operations. These depend on the chosen spreading factor. + radio.WriteRegister(0x925, []byte{0x32}) + radio.WriteRegister(0x93C, []byte{0x01}) + + radio.SetTxParams(13, sx128x.RADIO_RAMP_02_US) + radio.SetPacketParamsLoRa(12, sx128x.LORA_HEADER_EXPLICIT, 0xFF, sx128x.LORA_CRC_DISABLE, sx128x.LORA_IQ_STD) + radio.WriteRegister(sx128x.REG_LORA_SYNC_WORD_MSB, []byte{0x14, 0x24}) // full sync word is 0x1424 + +} + +func Rx(radio *sx128x.Device) ([]byte, error) { + radio.SetStandby(sx128x.STANDBY_RC) + radio.SetDioIrqParams(sx128x.IRQ_RX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, sx128x.IRQ_RX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, 0x00, 0x00) + radio.SetBufferBaseAddress(0, 0) + radio.ClearIrqStatus(sx128x.IRQ_ALL_MASK) + radio.SetRx(sx128x.PERIOD_BASE_4_MS, 250) // 4ms * 250 = 1s + // busy wait for IRQ indication + for dio1Pin.Get() == false { + runtime.Gosched() + } + irqStatus, _ := radio.GetIrqStatus() + if irqStatus&sx128x.IRQ_RX_DONE_MASK != 0 { + payloadLength, bufferOffset, err := radio.GetRxBufferStatus() + if err != nil { + return nil, err + } + data, err := radio.ReadBuffer(bufferOffset, payloadLength) + return data, nil + } else if irqStatus&sx128x.IRQ_RX_TX_TIMEOUT_MASK != 0 { + return nil, errors.New("rx timeout") + } + return nil, errors.New("unexpected IRQ status") +} diff --git a/examples/sx128x/lora_tx/main.go b/examples/sx128x/lora_tx/main.go new file mode 100644 index 0000000..248d3a7 --- /dev/null +++ b/examples/sx128x/lora_tx/main.go @@ -0,0 +1,96 @@ +package main + +import ( + "errors" + "machine" + "runtime" + "time" + + "tinygo.org/x/drivers/sx128x" +) + +var ( + // pin mapping specific to the lilygo t3s3, change as needed for your board + sdoPin = machine.GPIO6 + sdiPin = machine.GPIO3 + sckPin = machine.GPIO5 + nssPin = machine.GPIO7 + busyPin = machine.GPIO36 + resetPin = machine.GPIO8 + dio1Pin = machine.GPIO9 +) + +func setupPins() { + nssPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + nssPin.Set(true) + + resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + resetPin.Set(true) + + busyPin.Configure(machine.PinConfig{Mode: machine.PinInput}) + dio1Pin.Configure(machine.PinConfig{Mode: machine.PinInput}) +} + +func main() { + setupPins() + + spi := machine.SPI0 + spi.Configure(machine.SPIConfig{ + Mode: 0, + Frequency: 8 * 1e6, + SDO: sdoPin, + SDI: sdiPin, + SCK: sckPin, + }) + + radio := sx128x.New( + spi, + nssPin, + resetPin, + busyPin, + ) + + radio.WaitWhileBusy(time.Second) + SetupLora(radio) + + for { + Tx(radio, []byte("Hello, world!")) + time.Sleep(1 * time.Second) + } +} + +func SetupLora(radio *sx128x.Device) { + radio.SetStandby(sx128x.STANDBY_RC) + radio.SetPacketType(sx128x.PACKET_TYPE_LORA) + radio.SetRegulatorMode(sx128x.REGULATOR_DC_DC) + + radio.SetRfFrequency(2400000000) // 2.4Ghz + radio.SetModulationParamsLoRa(sx128x.LORA_SF_9, sx128x.LORA_BW_1600, sx128x.LORA_CR_4_7) + + // section 14.4.1 shows required register setting for setting up LoRa operations. These depend on the chosen spreading factor. + radio.WriteRegister(0x925, []byte{0x32}) + radio.WriteRegister(0x93C, []byte{0x01}) + + radio.SetTxParams(13, sx128x.RADIO_RAMP_02_US) + radio.SetPacketParamsLoRa(12, sx128x.LORA_HEADER_EXPLICIT, 0xFF, sx128x.LORA_CRC_DISABLE, sx128x.LORA_IQ_STD) + radio.WriteRegister(sx128x.REG_LORA_SYNC_WORD_MSB, []byte{0x14, 0x24}) // full sync word is 0x1424 + +} + +func Tx(radio *sx128x.Device, data []byte) error { + if len(data) > 255 { + return errors.New("data length exceeds maximum of 255 bytes") + } + radio.SetStandby(sx128x.STANDBY_RC) + radio.SetPacketParamsLoRa(12, sx128x.LORA_HEADER_EXPLICIT, uint8(len(data)&0xFF), sx128x.LORA_CRC_DISABLE, sx128x.LORA_IQ_STD) + radio.SetBufferBaseAddress(0, 0) + radio.WriteBuffer(0, data) + radio.SetDioIrqParams(sx128x.IRQ_TX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, sx128x.IRQ_TX_DONE_MASK|sx128x.IRQ_RX_TX_TIMEOUT_MASK, 0x00, 0x00) + radio.ClearIrqStatus(sx128x.IRQ_ALL_MASK) + radio.SetTx(sx128x.PERIOD_BASE_4_MS, 250) // 4ms * 250 = 1s + // busy wait for IRQ indication + for dio1Pin.Get() == false { + runtime.Gosched() + } + return nil +} diff --git a/examples/tmc2209/main.go b/examples/tmc2209/main.go new file mode 100644 index 0000000..fdb74ba --- /dev/null +++ b/examples/tmc2209/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "machine" + + "tinygo.org/x/drivers/tmc2209" +) + +func main() { + uart := machine.UART0 + comm := tmc2209.NewUARTComm(*uart, 0) + // Create an instance of the TMC2209 with UART communication + tmc := tmc2209.NewTMC2209(comm, 0x00) // Replace 0x00 with the appropriate address + + // Set up the TMC2209 driver + err := tmc.Setup() + if err != nil { + println("Failed to set up TMC2209: ", err) + } + + // Write to a register (example: setting a register value) + err = tmc.WriteRegister(0x10, 0x12345678) // Replace 0x10 with the register address and 0x12345678 with the value + if err != nil { + println("Failed to write register:", err) + } + + // Read from a register (example: reading a register value) + value, err := tmc.ReadRegister(0x10) + if err != nil { + println("Failed to read register: ", err) + } + + // Output the read value + println("Register value: ", value) +} diff --git a/examples/tmc5160/main.go b/examples/tmc5160/main.go new file mode 100644 index 0000000..3b60ccf --- /dev/null +++ b/examples/tmc5160/main.go @@ -0,0 +1,61 @@ +// Connects to SPI1 on a RP2040 (Pico) +package main + +import ( + "machine" + + "tinygo.org/x/drivers/tmc5160" +) + +func main() { + // Step 1. Setup your protocol. SPI setup shown below + spi := machine.SPI1 + spi.Configure(machine.SPIConfig{ + Frequency: 12000000, // Upto 12 MHZ is pretty stable. Reduce to 5 or 6 Mhz if you are experiencing issues + Mode: 3, + LSBFirst: false, + }) + // Step 2. Set up all associated Pins + csPin0 := machine.GPIO13 + csPin0.Configure(machine.PinConfig{Mode: machine.PinOutput}) + enn0 := machine.GPIO18 + enn0.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + // csPins is a map of all chip select pins in a multi driver setup. + //Only one pin csPin0 mapped to "0"is shown in this example, but add more mappings as required + csPins := map[uint8]machine.Pin{0: csPin0} + //bind csPin to driverAdddress + driverAddress := uint8(0) // Let's assume we are working with driver at address 0x01 + // Step 3. Bind the communication interface to the protocol + comm := tmc5160.NewSPIComm(spi, csPins) + // Step 4. Define your stepper like this below + //stepper := tmc5160.NewStepper(angle , gearRatio vSupply rCoil , lCoil , iPeak , rSense , mSteps, fclk ) + stepper := tmc5160.NewDefaultStepper() // Default Stepper should be used only for testing. + // Step 5. Instantiate your driver + driver := tmc5160.NewDriver( + comm, + driverAddress, + enn0, + stepper) + + // Setting and getting mode + rampMode := tmc5160.NewRAMPMODE(comm, driverAddress) + err := rampMode.SetMode(tmc5160.PositioningMode) + if err != nil { + return + } + mode, err := rampMode.GetMode() + if err != nil { + println("Error getting mode:", err) + } else { + println("Current Mode:", mode) + } + + // Read GCONF register + GCONF := tmc5160.NewGCONF() + gconfVal, err := driver.ReadRegister(tmc5160.GCONF) + // Uppack the register to get all the bits and bytes of the register + GCONF.Unpack(gconfVal) + //E.g. MultiStepFlit is retrieved from the GCONF register + println("GCONF:MultiStepFlit:", GCONF.MultistepFilt) +} diff --git a/examples/touch/capacitive/main.go b/examples/touch/capacitive/main.go new file mode 100644 index 0000000..fc3f926 --- /dev/null +++ b/examples/touch/capacitive/main.go @@ -0,0 +1,69 @@ +// Capacitive touch sensing example. +// +// This capacitive touch sensor works by charging a normal GPIO pin, then slowly +// discharging it through a 1MΩ resistor and seeing how long it takes to go from +// high to low. +// +// Use as follows: +// - Change touchPin below as needed. +// - Connect this pin to some metal surface, like a piece of aluminimum foil. +// Make sure this surface is covered (using paper, Scotch tape, etc). +// - Also connect this same pin to ground through a 1MΩ resistor. +// +// This sensor is very sensitive to noise on the power source, so you should +// probably try to limit it by running from a battery for example. Especially +// phone chargers can produce a lot of noise. +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/touch/capacitive" +) + +const touchPin = machine.GP16 // Raspberry Pi Pico + +func main() { + time.Sleep(time.Second * 2) + println("start") + + led := machine.LED + led.Configure(machine.PinConfig{Mode: machine.PinOutput}) + led.Low() + + // Configure the array of GPIO pins used for capacitive touch sensing. + // We're using only one pin. + array := capacitive.NewArray([]machine.Pin{touchPin}) + + // Use a dynamic threshold, meaning the GPIO pin is automatically calibrated + // and re-calibrated to adjust for varying environments (e.g. changing + // humidity). + array.SetDynamicThreshold(100) + + wasTouching := false + for i := uint32(0); ; i++ { + // Update the GPIO pin. This must be called very often. + array.Update() + touching := array.Touching(0) + + // Indicate whether the pin is touched via the LED. + led.Set(touching) + + // Print something when the touch state changed. + if wasTouching != touching { + wasTouching = touching + if touching { + println(" touch!") + } else { + println(" release!") + } + } + + // Print the current value, as a debugging aid. It's not really meant to + // be used directly. + if i%128 == 32 { + println("touch value:", array.RawValue(0)) + } + } +} diff --git a/examples/uc8151/main.go b/examples/uc8151/main.go index e55acb2..1ee778b 100644 --- a/examples/uc8151/main.go +++ b/examples/uc8151/main.go @@ -3,7 +3,9 @@ package main import ( "image/color" "machine" + "time" + "tinygo.org/x/drivers" "tinygo.org/x/drivers/uc8151" ) @@ -14,30 +16,56 @@ func main() { led = machine.LED led.Configure(machine.PinConfig{Mode: machine.PinOutput}) machine.SPI0.Configure(machine.SPIConfig{ - Frequency: 12000000, + Frequency: 12 * machine.MHz, SCK: machine.EPD_SCK_PIN, SDO: machine.EPD_SDO_PIN, }) display = uc8151.New(machine.SPI0, machine.EPD_CS_PIN, machine.EPD_DC_PIN, machine.EPD_RESET_PIN, machine.EPD_BUSY_PIN) display.Configure(uc8151.Config{ - Rotation: uc8151.ROTATION_270, - Speed: uc8151.MEDIUM, - Blocking: true, + Rotation: drivers.Rotation270, + Speed: uc8151.TURBO, + FlickerFree: true, + Blocking: false, }) black := color.RGBA{1, 1, 1, 255} - display.ClearBuffer() - display.Display() - for i := int16(0); i < 37; i++ { - for j := int16(0); j < 16; j++ { - if (i+j)%2 == 0 { - showRect(i*8, j*8, 8, 8, black) + display.ClearDisplay() + + mod := int16(1) + for { + // checkerboard + for i := int16(0); i < 11; i++ { + if mod == 1 { + mod = 0 + } else { + mod = 1 + } + + display.ClearBuffer() + for i := int16(0); i < 37; i++ { + for j := int16(0); j < 16; j++ { + if (i+j)%2 == mod { + showRect(i*8, j*8, 8, 8, black) + } + } + } + display.Display() + time.Sleep(500 * time.Millisecond) + } + + // moving line + for i := int16(16); i < 21; i++ { + display.ClearBuffer() + for j := int16(0); j < 16; j++ { + if (i+j)%2 == 0 { + showRect(i*8, j*8, 8, 8, black) + } + display.Display() + time.Sleep(250 * time.Millisecond) } } } - - display.Display() } func showRect(x int16, y int16, w int16, h int16, c color.RGBA) { diff --git a/examples/unoqmatrix/main.go b/examples/unoqmatrix/main.go new file mode 100644 index 0000000..89695f8 --- /dev/null +++ b/examples/unoqmatrix/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "machine" + + "image/color" + "math/rand" + + "tinygo.org/x/drivers/unoqmatrix" +) + +var on = color.RGBA{255, 255, 255, 255} + +func main() { + display := unoqmatrix.NewFromBasePin(machine.PF0) + display.ClearDisplay() + + w, h := display.Size() + x := int16(0) + y := int16(0) + deltaX := int16(1) + deltaY := int16(1) + + for { + pixel := display.GetPixel(x, y) + if pixel.R != 0 || pixel.G != 0 || pixel.B != 0 { + display.ClearDisplay() + x = 1 + int16(rand.Int31n(3)) + y = 1 + int16(rand.Int31n(3)) + deltaX = 1 + deltaY = 1 + if rand.Int31n(2) == 0 { + deltaX = -1 + } + if rand.Int31n(2) == 0 { + deltaY = -1 + } + } + display.SetPixel(x, y, on) + + x += deltaX + y += deltaY + + if x == 0 || x == w-1 { + deltaX = -deltaX + } + + if y == 0 || y == h-1 { + deltaY = -deltaY + } + + display.Display() + } +} diff --git a/examples/w5500/main.go b/examples/w5500/main.go new file mode 100644 index 0000000..ea0a5b6 --- /dev/null +++ b/examples/w5500/main.go @@ -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 + } +} diff --git a/examples/waveshare-epd/epd1in54/main.go b/examples/waveshare-epd/epd1in54/main.go new file mode 100644 index 0000000..11b34cf --- /dev/null +++ b/examples/waveshare-epd/epd1in54/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "image/color" + "machine" + + "tinygo.org/x/drivers/waveshare-epd/epd1in54" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/gophers" +) + +var ( + spi0 = machine.SPI0 + cs = machine.D10 + dc = machine.D9 + rst = machine.D6 + busy = machine.D5 + + black = color.RGBA{R: 1, G: 1, B: 1, A: 255} +) + +func main() { + display := epd1in54.New(spi0, cs, dc, rst, busy) + + display.LDirInit(epd1in54.Config{}) + display.Clear() + display.ClearBuffer() + + tinyfont.WriteLineRotated(&display, &gophers.Regular58pt, 150, 0, "A B C", black, tinyfont.ROTATION_90) + tinyfont.WriteLineRotated(&display, &gophers.Regular58pt, 100, 0, "D E F", black, tinyfont.ROTATION_90) + tinyfont.WriteLineRotated(&display, &gophers.Regular58pt, 50, 0, "G H I", black, tinyfont.ROTATION_90) + tinyfont.WriteLineRotated(&display, &gophers.Regular58pt, 0, 0, "J K L", black, tinyfont.ROTATION_90) + + display.Display() + display.Sleep() +} diff --git a/examples/waveshare-epd/epd2in66b/main.go b/examples/waveshare-epd/epd2in66b/main.go new file mode 100644 index 0000000..a9c75b6 --- /dev/null +++ b/examples/waveshare-epd/epd2in66b/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/waveshare-epd/epd2in66b" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/freemono" +) + +var ( + black = color.RGBA{0, 0, 0, 0xff} + white = color.RGBA{0xff, 0xff, 0xff, 0xff} + red = color.RGBA{0xff, 0, 0, 0xff} +) + +func main() { + machine.Serial.Configure(machine.UARTConfig{}) + time.Sleep(2 * time.Second) + + machine.SPI1.Configure(machine.SPIConfig{ + Frequency: epd2in66b.Baudrate, + }) + + println("started") + + // in case you have a Pico module, you can directly use + // dev, err := epd2in66b.NewPicoModule() + + display := epd2in66b.New(machine.SPI1) + + cfg := epd2in66b.Config{ + DataPin: machine.GP8, + ChipSelectPin: machine.GP9, + ResetPin: machine.GP12, + BusyPin: machine.GP13, + } + err := display.Configure(cfg) + if err != nil { + panic(err) + } + + err = display.Reset() + if err != nil { + panic(err) + } + + println("draw checkerboard") + drawCheckerBoard(&display) + + println("draw 'hello'") + tinyfont.WriteLineRotated(&display, &freemono.Bold24pt7b, 40, 10, "Hello!", white, tinyfont.ROTATION_90) + tinyfont.WriteLineRotated(&display, &freemono.Bold12pt7b, 10, 10, "tinygo rocks", white, tinyfont.ROTATION_90) + err = display.Display() + if err != nil { + panic(err) + } +} + +func drawCheckerBoard(display drivers.Displayer) { + s := 8 + width, height := display.Size() + for x := 0; x <= int(width)-s; x += s { + for y := 0; y <= int(height)-s; y += s { + c := red + if (x/s)%2 == (y/s)%2 { + c = black + } + + showRect(display, x, y, s, s, c) + } + } +} + +func showRect(display drivers.Displayer, x int, y int, w int, h int, c color.RGBA) { + for i := x; i < x+w; i++ { + for j := y; j < y+h; j++ { + display.SetPixel(int16(i), int16(j), c) + } + } +} diff --git a/examples/waveshare-epd/epd2in9v2/main.go b/examples/waveshare-epd/epd2in9v2/main.go new file mode 100644 index 0000000..3bbbd57 --- /dev/null +++ b/examples/waveshare-epd/epd2in9v2/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers/waveshare-epd/epd2in9v2" +) + +var display epd2in9v2.Device + +func main() { + machine.SPI0.Configure(machine.SPIConfig{ + Frequency: 12000000, + SCK: machine.EPD_SCK_PIN, + SDO: machine.EPD_SDO_PIN, + }) + + display = epd2in9v2.New( + machine.SPI0, + machine.EPD_CS_PIN, + machine.EPD_DC_PIN, + machine.EPD_RESET_PIN, + machine.EPD_BUSY_PIN, + ) + display.Configure(epd2in9v2.Config{ + Rotation: epd2in9v2.ROTATION_270, + Speed: epd2in9v2.SPEED_DEFAULT, + Blocking: true, + }) + + black := color.RGBA{0, 0, 0, 255} + white := color.RGBA{255, 255, 255, 255} + + // --- Step 1: clear to white --- + println("epd2in9v2: clearing display") + display.ClearBuffer() + display.Display() + time.Sleep(2 * time.Second) + + // --- Step 2: full refresh checkerboard --- + println("epd2in9v2: drawing checkerboard (full refresh)") + w, h := display.Size() + for i := int16(0); i < w/8; i++ { + for j := int16(0); j < h/8; j++ { + if (i+j)%2 == 0 { + fillRect(i*8, j*8, 8, 8, black) + } + } + } + display.Display() + time.Sleep(2 * time.Second) + + // --- Step 3: fast refresh - draw border and diagonal cross --- + println("epd2in9v2: switching to fast refresh") + display.SetSpeed(epd2in9v2.SPEED_FAST) + display.ClearBuffer() + + for x := int16(0); x < w; x++ { + display.SetPixel(x, 0, black) + display.SetPixel(x, h-1, black) + } + for y := int16(0); y < h; y++ { + display.SetPixel(0, y, black) + display.SetPixel(w-1, y, black) + } + for i := int16(0); i < w && i < h; i++ { + display.SetPixel(i, i*h/w, black) + display.SetPixel(w-1-i, i*h/w, black) + } + + display.Display() + time.Sleep(2 * time.Second) + + // --- Step 4: partial refresh counter --- + println("epd2in9v2: partial refresh demo") + display.SetSpeed(epd2in9v2.SPEED_DEFAULT) + display.ClearBuffer() + + println("epd2in9v2: setting base image") + display.DisplayWithBase() + + for count := 0; count < 10; count++ { + cx := int16(120) + cy := int16(50) + fillRect(cx, cy, 60, 20, white) + + digit := int16(count % 10) + drawDigit(cx+22, cy+2, digit, black) + + display.DisplayPartial() + time.Sleep(500 * time.Millisecond) + } + time.Sleep(2 * time.Second) + + // --- Step 5: sleep --- + println("epd2in9v2: entering deep sleep") + display.ClearBuffer() + display.Display() + display.Sleep() + println("epd2in9v2: done, you can remove power") +} + +func fillRect(x, y, w, h int16, c color.RGBA) { + for i := x; i < x+w; i++ { + for j := y; j < y+h; j++ { + display.SetPixel(i, j, c) + } + } +} + +// drawDigit draws a simple 3x5-pixel-block digit (each block 4x3 px) at position (x,y). +func drawDigit(x, y, digit int16, c color.RGBA) { + segments := [10][5]uint8{ + {0x7, 0x5, 0x5, 0x5, 0x7}, // 0 + {0x2, 0x2, 0x2, 0x2, 0x2}, // 1 + {0x7, 0x1, 0x7, 0x4, 0x7}, // 2 + {0x7, 0x1, 0x7, 0x1, 0x7}, // 3 + {0x5, 0x5, 0x7, 0x1, 0x1}, // 4 + {0x7, 0x4, 0x7, 0x1, 0x7}, // 5 + {0x7, 0x4, 0x7, 0x5, 0x7}, // 6 + {0x7, 0x1, 0x1, 0x1, 0x1}, // 7 + {0x7, 0x5, 0x7, 0x5, 0x7}, // 8 + {0x7, 0x5, 0x7, 0x1, 0x7}, // 9 + } + if digit < 0 || digit > 9 { + return + } + for row := int16(0); row < 5; row++ { + for col := int16(0); col < 3; col++ { + if segments[digit][row]&(0x4>>uint(col)) != 0 { + fillRect(x+col*4, y+row*3, 4, 3, c) + } + } + } +} diff --git a/examples/ws2812/arduino.go b/examples/ws2812/arduino.go index 3f8c58e..3712226 100644 --- a/examples/ws2812/arduino.go +++ b/examples/ws2812/arduino.go @@ -1,10 +1,11 @@ -//go:build arduino +//go:build arduino || arduino_uno package main import "machine" -// Replace neo and led in the code below to match the pin -// that you are using if different. -var neo = machine.D2 -var led = machine.LED +func init() { + // Replace neo in the code below to match the pin + // that you are using if different. + neo = machine.D2 +} diff --git a/examples/ws2812/digispark.go b/examples/ws2812/digispark.go index fa8c574..e7d7526 100644 --- a/examples/ws2812/digispark.go +++ b/examples/ws2812/digispark.go @@ -4,8 +4,9 @@ package main import "machine" -// This is the pin assignment for the Digispark only. -// Replace neo and led in the code below to match the pin -// that you are using if different. -var neo machine.Pin = 0 -var led = machine.LED +func init() { + // This is the pin assignment for the Digispark only. + // Replace neo and led in the code below to match the pin + // that you are using if different. + neo = machine.Pin(0) +} diff --git a/examples/ws2812/main.go b/examples/ws2812/main.go index 02699d6..8ade21f 100644 --- a/examples/ws2812/main.go +++ b/examples/ws2812/main.go @@ -12,11 +12,12 @@ import ( "tinygo.org/x/drivers/ws2812" ) -var leds [10]color.RGBA +var ( + neo machine.Pin + leds [10]color.RGBA +) func main() { - led.Configure(machine.PinConfig{Mode: machine.PinOutput}) - neo.Configure(machine.PinConfig{Mode: machine.PinOutput}) ws := ws2812.NewWS2812(neo) @@ -35,7 +36,6 @@ func main() { } ws.WriteColors(leds[:]) - led.Set(rg) time.Sleep(100 * time.Millisecond) } } diff --git a/examples/ws2812/others.go b/examples/ws2812/others.go index 3c90f05..d68d57e 100644 --- a/examples/ws2812/others.go +++ b/examples/ws2812/others.go @@ -1,10 +1,11 @@ -//go:build !digispark && !arduino && !qtpy && !m5stamp_c3 && !thingplus_rp2040 +//go:build !digispark && !arduino && !arduino_uno && !xiao_esp32c3 package main import "machine" -// Replace neo and led in the code below to match the pin -// that you are using if different. -var neo machine.Pin = machine.WS2812 -var led = machine.LED +func init() { + // Replace neo in the code below to match the pin + // that you are using if different. + neo = machine.WS2812 +} diff --git a/examples/ws2812/others_wo_led.go b/examples/ws2812/others_wo_led.go deleted file mode 100644 index 988d79a..0000000 --- a/examples/ws2812/others_wo_led.go +++ /dev/null @@ -1,10 +0,0 @@ -//go:build qtpy || m5stamp_c3 - -package main - -import "machine" - -// Replace neo and led in the code below to match the pin -// that you are using if different. -var neo machine.Pin = machine.WS2812 -var led = machine.NoPin diff --git a/examples/ws2812/thingplus-rp2040.go b/examples/ws2812/thingplus-rp2040.go deleted file mode 100644 index a194bda..0000000 --- a/examples/ws2812/thingplus-rp2040.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build thingplus_rp2040 - -package main - -import "machine" - -// This is the pin assignment for the internal neopixel of the -// Sparkfun thingplus rp2040. -// Replace neo and led in the code below to match the pin -// that you are using if different. -var neo machine.Pin = machine.GPIO8 -var led = machine.LED diff --git a/examples/ws2812/xiao-esp32c3.go b/examples/ws2812/xiao-esp32c3.go new file mode 100644 index 0000000..ad8f5a6 --- /dev/null +++ b/examples/ws2812/xiao-esp32c3.go @@ -0,0 +1,11 @@ +//go:build xiao_esp32c3 + +package main + +import "machine" + +func init() { + // Replace neo in the code below to match the pin + // that you are using if different. + neo = machine.D6 +} diff --git a/flash/devices.go b/flash/devices.go index 69239a6..7140e92 100644 --- a/flash/devices.go +++ b/flash/devices.go @@ -46,6 +46,10 @@ var DefaultDeviceIdentifier = DeviceIdentifierFunc(func(id JedecID) Attrs { return GD25Q16C() case 0xC84017: return GD25Q64C() + case 0x856015: + return P25Q16H() + case 0xEF4014: + return W25Q80DV() case 0xEF4015: return W25Q16JVIQ() case 0xEF4016: @@ -239,6 +243,24 @@ func GD25Q64C() Attrs { } } +// Settings for the Puya P25Q16H 2MiB SPI flash. +// Datasheet: https://files.seeedstudio.com/wiki/github_weiruanexample/Flash_P25Q16H-UXH-IR_Datasheet.pdf +func P25Q16H() Attrs { + return Attrs{ + TotalSize: 1 << 21, // 2 MiB + StartUp: 5000 * time.Microsecond, + JedecID: JedecID{0x85, 0x60, 0x15}, + MaxClockSpeedMHz: 55, + QuadEnableBitMask: 0x02, + HasSectorProtection: true, + SupportsFastRead: true, + SupportsQSPI: true, + SupportsQSPIWrites: true, + WriteStatusSplit: true, + SingleStatusByte: false, + } +} + // Settings for the Winbond W25Q16JV-IQ 2MiB SPI flash. Note that JV-IM has a // different .memory_type (0x70) Datasheet: // https://www.winbond.com/resource-files/w25q16jv%20spi%20revf%2005092017.pdf @@ -380,6 +402,25 @@ func W25Q80DL() Attrs { TotalSize: 1 << 20, // 1 MiB StartUp: 5000 * time.Microsecond, JedecID: JedecID{0xEF, 0x60, 0x14}, + MaxClockSpeedMHz: 80, + QuadEnableBitMask: 0x02, + HasSectorProtection: false, + SupportsFastRead: true, + SupportsQSPI: true, + SupportsQSPIWrites: false, + WriteStatusSplit: false, + SingleStatusByte: false, + } +} + +// Settings for the Winbond W25Q80DV 2MiB SPI flash. +// Datasheet: +// https://www.winbond.com/resource-files/w25q80dv%20dl_revh_10022015.pdf +func W25Q80DV() Attrs { + return Attrs{ + TotalSize: 1 << 21, // 2 MiB + StartUp: 5000 * time.Microsecond, + JedecID: JedecID{0xEF, 0x40, 0x14}, MaxClockSpeedMHz: 104, QuadEnableBitMask: 0x02, HasSectorProtection: false, diff --git a/ft6336/ft6336.go b/ft6336/ft6336.go index e6eb2c6..b16dc27 100644 --- a/ft6336/ft6336.go +++ b/ft6336/ft6336.go @@ -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. // @@ -80,7 +83,8 @@ func (d *Device) Read() []byte { func (d *Device) ReadTouchPoint() touch.Point { d.Read() z := 0xFFFFF - if d.buf[0] == 0 { + switch d.buf[0] { + case 0, 255: z = 0 } diff --git a/go.mod b/go.mod index 0fa3076..432c77a 100644 --- a/go.mod +++ b/go.mod @@ -1,20 +1,24 @@ module tinygo.org/x/drivers -go 1.18 +go 1.22.1 + +toolchain go1.23.1 require ( github.com/eclipse/paho.mqtt.golang v1.2.0 github.com/frankban/quicktest v1.10.2 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/orsinium-labs/tinymath v1.1.0 github.com/soypat/natiu-mqtt v0.5.1 - golang.org/x/net v0.7.0 + github.com/tinygo-org/pio v0.3.0 + golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d + golang.org/x/net v0.33.0 tinygo.org/x/tinyfont v0.3.0 tinygo.org/x/tinyterm v0.1.0 ) require ( - github.com/google/go-cmp v0.5.2 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/kr/pretty v0.2.1 // indirect github.com/kr/text v0.1.0 // indirect - golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect ) diff --git a/go.sum b/go.sum index 7f866dd..673d984 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,9 @@ github.com/eclipse/paho.mqtt.golang v1.2.0 h1:1F8mhG9+aO5/xpdtFkW4SxOJB67ukuDC3t github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= github.com/frankban/quicktest v1.10.2 h1:19ARM85nVi4xH7xPXuc5eM/udya5ieh7b/Sv+d844Tk= github.com/frankban/quicktest v1.10.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= @@ -12,12 +13,17 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/orsinium-labs/tinymath v1.1.0 h1:KomdsyLHB7vE3f1nRAJF2dyf1m/gnM2HxfTeV1vS5UA= +github.com/orsinium-labs/tinymath v1.1.0/go.mod h1:WPXX6ei3KSXG7JfA03a+ekCYaY9SWN4I+JRl2p6ck+A= github.com/soypat/natiu-mqtt v0.5.1 h1:rwaDmlvjzD2+3MCOjMZc4QEkDkNwDzbct2TJbpz+TPc= github.com/soypat/natiu-mqtt v0.5.1/go.mod h1:xEta+cwop9izVCW7xOx2W+ct9PRMqr0gNVkvBPnQTc4= +github.com/tinygo-org/pio v0.3.0 h1:opEnOtw58KGB4RJD3/n/Rd0/djYGX3DeJiXLI6y/yDI= +github.com/tinygo-org/pio v0.3.0/go.mod h1:wf6c6lKZp+pQOzKKcpzchmRuhiMc27ABRuo7KVnaMFU= github.com/valyala/fastjson v1.6.3/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0= +golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= tinygo.org/x/drivers v0.14.0/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= tinygo.org/x/drivers v0.15.1/go.mod h1:uT2svMq3EpBZpKkGO+NQHjxjGf1f42ra4OnMMwQL2aI= diff --git a/gps/gps.go b/gps/gps.go index 86bfd6a..778c692 100644 --- a/gps/gps.go +++ b/gps/gps.go @@ -11,37 +11,18 @@ import ( ) var ( - errInvalidNMEASentenceLength = errors.New("invalid NMEA sentence length") - errInvalidNMEAChecksum = errors.New("invalid NMEA sentence checksum") - errEmptyNMEASentence = errors.New("cannot parse empty NMEA sentence") - errUnknownNMEASentence = errors.New("unsupported NMEA sentence type") + ErrInvalidNMEASentenceLength = errors.New("invalid NMEA sentence length") + ErrInvalidNMEASentence = errors.New("invalid NMEA sentence format") + ErrEmptyNMEASentence = errors.New("cannot parse empty NMEA sentence") + ErrUnknownNMEASentence = errors.New("unsupported NMEA sentence type") + errInvalidGSVSentence = errors.New("invalid GSV NMEA sentence") errInvalidGGASentence = errors.New("invalid GGA NMEA sentence") errInvalidRMCSentence = errors.New("invalid RMC NMEA sentence") errInvalidGLLSentence = errors.New("invalid GLL NMEA sentence") + errGPSCommandRejected = errors.New("GPS command rejected (NAK)") + errNoACKToGPSCommand = errors.New("no ACK to GPS command") ) -type GPSError struct { - Err error - Info string - Sentence string -} - -func newGPSError(err error, sentence string, info string) GPSError { - return GPSError{ - Info: info, - Err: err, - Sentence: sentence, - } -} - -func (ge GPSError) Error() string { - return ge.Err.Error() + " " + ge.Info + " " + ge.Sentence -} - -func (ge GPSError) Unwrap() error { - return ge.Err -} - const ( minimumNMEALength = 7 startingDelimiter = '$' @@ -50,30 +31,34 @@ const ( // Device wraps a connection to a GPS device. type Device struct { - buffer []byte bufIdx int sentence strings.Builder uart drivers.UART bus drivers.I2C address uint16 + buffer [bufferSize]byte } // NewUART creates a new UART GPS connection. The UART must already be configured. func NewUART(uart drivers.UART) Device { return Device{ uart: uart, - buffer: make([]byte, bufferSize), bufIdx: bufferSize, sentence: strings.Builder{}, } } // NewI2C creates a new I2C GPS connection. +// Uses the default i2c address (0x42) for backward compatibility reasons. func NewI2C(bus drivers.I2C) Device { + return NewI2CWithAddress(bus, I2C_ADDRESS) +} + +// NewI2CWithAddress creates a new I2C GPS connection on the provided address +func NewI2CWithAddress(bus drivers.I2C, i2cAddress uint16) Device { return Device{ bus: bus, - address: I2C_ADDRESS, - buffer: make([]byte, bufferSize), + address: i2cAddress, bufIdx: bufferSize, sentence: strings.Builder{}, } @@ -166,17 +151,15 @@ func (gps *Device) WriteBytes(bytes []byte) { // It has to end with a '*' character following by a checksum. func validSentence(sentence string) error { if len(sentence) < minimumNMEALength || sentence[0] != startingDelimiter || sentence[len(sentence)-3] != checksumDelimiter { - return errInvalidNMEASentenceLength + return ErrInvalidNMEASentenceLength } var cs byte = 0 for i := 1; i < len(sentence)-3; i++ { cs ^= sentence[i] } checksum := strings.ToUpper(hex.EncodeToString([]byte{cs})) - if checksum != sentence[len(sentence)-2:len(sentence)] { - return newGPSError(errInvalidNMEAChecksum, sentence, - "expected "+sentence[len(sentence)-2:len(sentence)]+ - " got "+checksum) + if checksum != sentence[len(sentence)-2:] { + return ErrInvalidNMEASentence } return nil diff --git a/gps/gpsparser.go b/gps/gpsparser.go index 0cae7d2..cae739e 100644 --- a/gps/gpsparser.go +++ b/gps/gpsparser.go @@ -6,12 +6,28 @@ import ( "time" ) +type NMEASentenceType string + +const ( + GSA NMEASentenceType = "GSA" + GGA NMEASentenceType = "GGA" + GLL NMEASentenceType = "GLL" + GSV NMEASentenceType = "GSV" + RMC NMEASentenceType = "RMC" + VTG NMEASentenceType = "VTG" + ZDA NMEASentenceType = "ZDA" + TXT NMEASentenceType = "TXT" +) + // Parser for GPS NMEA sentences. type Parser struct { } // Fix is a GPS location fix type Fix struct { + // Type is the NMEA sentence type that provided this fix. + Type NMEASentenceType + // Valid if the fix was valid. Valid bool @@ -46,13 +62,30 @@ func NewParser() Parser { func (parser *Parser) Parse(sentence string) (Fix, error) { var fix Fix if sentence == "" { - return fix, errEmptyNMEASentence + return fix, ErrEmptyNMEASentence } if len(sentence) < 6 { - return fix, errInvalidNMEASentenceLength + return fix, ErrInvalidNMEASentenceLength } typ := sentence[3:6] switch typ { + case "GSV": + // https://docs.novatel.com/OEM7/Content/Logs/GPGSV.htm + fields := strings.Split(sentence, ",") + // GSV sentences have at least 4 fields, but typically 8, 12, 16, or 20 depending on satellites in view + if len(fields) < 4 { + return fix, errInvalidGSVSentence + } + + fix.Type = GSV + + // Number of satellites in view is always field 3 + fix.Satellites = findSatellites(fields[3]) + + // GSV does not provide position, time, or fix validity + fix.Valid = false + + return fix, nil case "GGA": // https://docs.novatel.com/OEM7/Content/Logs/GPGGA.htm fields := strings.Split(sentence, ",") @@ -60,6 +93,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) { return fix, errInvalidGGASentence } + fix.Type = GGA fix.Time = findTime(fields[1]) fix.Latitude = findLatitude(fields[2], fields[3]) fix.Longitude = findLongitude(fields[4], fields[5]) @@ -75,6 +109,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) { return fix, errInvalidGLLSentence } + fix.Type = GLL fix.Latitude = findLatitude(fields[1], fields[2]) fix.Longitude = findLongitude(fields[3], fields[4]) fix.Time = findTime(fields[5]) @@ -89,6 +124,7 @@ func (parser *Parser) Parse(sentence string) (Fix, error) { return fix, errInvalidRMCSentence } + fix.Type = RMC fix.Time = findTime(fields[1]) fix.Valid = (fields[2] == "A") fix.Latitude = findLatitude(fields[3], fields[4]) @@ -96,12 +132,15 @@ func (parser *Parser) Parse(sentence string) (Fix, error) { fix.Speed = findSpeed(fields[7]) fix.Heading = findHeading(fields[8]) date := findDate(fields[9]) - fix.Time = fix.Time.AddDate(date.Year(), int(date.Month()), date.Day()) + fix.Time = date.Add(time.Duration(fix.Time.Hour())*time.Hour + + time.Duration(fix.Time.Minute())*time.Minute + + time.Duration(fix.Time.Second())*time.Second + + time.Duration(fix.Time.Nanosecond())*time.Nanosecond) return fix, nil } - return fix, newGPSError(errUnknownNMEASentence, sentence, typ) + return fix, ErrUnknownNMEASentence } // findTime returns the time from an NMEA sentence: diff --git a/gps/gpsparser_test.go b/gps/gpsparser_test.go index 17c5dd9..0c1b56a 100644 --- a/gps/gpsparser_test.go +++ b/gps/gpsparser_test.go @@ -8,13 +8,29 @@ import ( ) func TestParseUnknownSentence(t *testing.T) { + p := NewParser() + + val := "$GPVTG,89.68,T,,M,0.00,N,0.0,K*5F" + _, err := p.Parse(val) + if err == nil { + t.Error("should have unknown sentence err") + } +} + +func TestParseGSV(t *testing.T) { c := qt.New(t) p := NewParser() val := "$GPGSV,3,1,09,07,14,317,22,08,31,284,25,10,32,133,39,16,85,232,29*7F" - _, err := p.Parse(val) - c.Assert(err.Error(), qt.Contains, "unsupported NMEA sentence type") + fix, err := p.Parse(val) + if err != nil { + t.Error("should have parsed") + } + + c.Assert(fix.Type, qt.Equals, GSV) + c.Assert(fix.Satellites, qt.Equals, int16(9)) + c.Assert(fix.Valid, qt.Equals, false) } func TestParseGGA(t *testing.T) { @@ -70,15 +86,15 @@ func TestParseRMC(t *testing.T) { t.Error("should have errInvalidRMCSentence error") } - val = "$GPRMC,203522.00,A,5109.0262308,N,11401.8407342,W,0.004,133.4,130522,0.0,E,D*2B" + val = "$GPRMC,203522.00,A,5109.0262308,N,11401.8407342,W,0.004,133.4,010622,0.0,E,D*2B" fix, err := p.Parse(val) if err != nil { t.Error("should have parsed") } c.Assert(fix.Time.Year(), qt.Equals, 2022) - c.Assert(fix.Time.Month(), qt.Equals, time.May) - c.Assert(fix.Time.Day(), qt.Equals, 13) + c.Assert(fix.Time.Month(), qt.Equals, time.June) + c.Assert(fix.Time.Day(), qt.Equals, 1) c.Assert(fix.Time.Hour(), qt.Equals, 20) c.Assert(fix.Time.Minute(), qt.Equals, 35) c.Assert(fix.Time.Second(), qt.Equals, 22) diff --git a/gps/registers.go b/gps/registers.go index 5ef9918..6d74d87 100644 --- a/gps/registers.go +++ b/gps/registers.go @@ -4,7 +4,11 @@ package gps // The I2C address which this device listens to. const ( - I2C_ADDRESS = 0x42 + // To ensure backward compatibility + I2C_ADDRESS = UBLOX_I2C_ADDRESS + + UBLOX_I2C_ADDRESS = 0x42 + PA1010D_I2C_ADDRESS = 0x10 ) const ( diff --git a/gps/ublox.go b/gps/ublox.go index 7d2e00a..4167715 100644 --- a/gps/ublox.go +++ b/gps/ublox.go @@ -1,53 +1,254 @@ package gps import ( - "errors" "time" ) -// flight mode disables the GPS COCOM limits -var flight_mode_cmd = [...]byte{ - 0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0xFF, 0xFF, 0x06, 0x03, 0x00, 0x00, 0x00, - 0x00, 0x10, 0x27, 0x00, 0x00, 0x05, 0x00, 0xFA, 0x00, 0xFA, 0x00, 0x64, 0x00, - 0x2C, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x16, 0xDC} - -// Sets CFG-GNSS to disable everything other than GPS GNSS -// solution. Failure to do this means GPS power saving -// doesn't work. Not needed for MAX7, needed for MAX8's -var cfg_gnss_cmd = [...]byte{ - 0xB5, 0x62, 0x06, 0x3E, 0x2C, 0x00, 0x00, 0x00, - 0x20, 0x05, 0x00, 0x08, 0x10, 0x00, 0x01, 0x00, - 0x01, 0x01, 0x01, 0x01, 0x03, 0x00, 0x00, 0x00, - 0x01, 0x01, 0x03, 0x08, 0x10, 0x00, 0x00, 0x00, - 0x01, 0x01, 0x05, 0x00, 0x03, 0x00, 0x00, 0x00, - 0x01, 0x01, 0x06, 0x08, 0x0E, 0x00, 0x00, 0x00, - 0x01, 0x01, 0xFC, 0x11} - -func FlightMode(d Device) (err error) { - err = sendCommand(d, flight_mode_cmd[:]) - return err +// FlightModeCmd is a UBX-CFG-NAV5 command +var nav5Cmd = CfgNav5{ + Mask: CfgNav5Dyn | CfgNav5MinEl | CfgNav5PosFixMode, + DynModel: DynModeAirborne1g, // Airborne with <1g acceleration + FixMode: FixModeAuto, // Auto 2D/3D + MinElev_deg: 5, // Minimum elevation 5 degrees + FixedAlt_me2: 0, // Not used + FixedAltVar_m2e4: 0, // Not used + PDop: 100, // 10.0 + TDop: 100, // 10.0 + PAcc_m: 5000, // 5 meters + TAcc_m: 5000, // 5 meters + StaticHoldThresh_cm_s: 0, // Not used + DgnssTimeout_s: 0, // Not used + CnoThreshNumSVs: 0, // Not used + CnoThresh_dbhz: 0, // Not used + StaticHoldMaxDist_m: 0, // Not used + UtcStandard: 0, // Automatic + Reserved1: [2]byte{}, + Reserved2: [5]byte{}, } -func SetCfgGNSS(d Device) (err error) { - err = sendCommand(d, cfg_gnss_cmd[:]) - return err +// SetFlightMode sends UBX-CFG-NAV5 command to set GPS into flight mode +func (d *Device) SetFlightMode() (err error) { + nav5Cmd.DynModel = DynModeAirborne1g + nav5Cmd.FixMode = FixModeAuto + nav5Cmd.Put42Bytes(d.buffer[:]) + + return d.SendCommand(d.buffer[:42]) } -func sendCommand(d Device, command []byte) (err error) { - d.WriteBytes(command) +// SetPedestrianMode sends UBX-CFG-NAV5 command to set GPS into pedestrian mode +func (d *Device) SetPedestrianMode() (err error) { + nav5Cmd.DynModel = DynModePedestrian + nav5Cmd.FixMode = FixModeAuto + nav5Cmd.Put42Bytes(d.buffer[:]) + + return d.SendCommand(d.buffer[:42]) +} + +// SetAutomotiveMode sends UBX-CFG-NAV5 command to set GPS into automotive mode +func (d *Device) SetAutomotiveMode() (err error) { + nav5Cmd.DynModel = DynModeAutomotive + nav5Cmd.FixMode = FixModeAuto + nav5Cmd.Put42Bytes(d.buffer[:]) + + return d.SendCommand(d.buffer[:42]) +} + +// SetBikeMode sends UBX-CFG-NAV5 command to set GPS into bike mode +func (d *Device) SetBikeMode() (err error) { + nav5Cmd.DynModel = DynModeBike + nav5Cmd.FixMode = FixModeAuto + nav5Cmd.Put42Bytes(d.buffer[:]) + + return d.SendCommand(d.buffer[:42]) +} + +var ( + // GGA (time, lat/lng, altitude) + messageRateGGACmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x00, + Rate: 1, // Every position fix + } + // GLL (time, lat/lng) + messageRateGLLCmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x01, + Rate: 1, // Every position fix + } + // GSA (satellite id list) + messageRateGSACmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x02, + Rate: 0, // Disabled + } + // GSV (satellite locations) + messageRateGSVCmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x03, + Rate: 0, // Every position fix + } + // RMC (time, lat/lng, speed, course) + messageRateRMCCmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x04, + Rate: 1, // Every position fix + } + // VTG (speed, course) + messageRateVTGCmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x05, + Rate: 0, // Disabled + } + // ZDA (time, timezone) + messageRateZDACmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x08, + Rate: 0, // Disabled + } + // TXT (text transmission) + messageRateTXTCmd = CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x41, + Rate: 0, // Disabled + } +) + +// SetMessageRatesMinimal configures the GPS to output a minimal set of NMEA sentences: +// GSV, GGA, GLL, and RMC only. +func SetMessageRatesMinimal(d *Device) (err error) { + commands := []CfgMsg1{ + messageRateGSACmd, + messageRateGLLCmd, + messageRateVTGCmd, + messageRateZDACmd, + messageRateTXTCmd, + } + for i := range commands { + commands[i].Rate = 0 // Disable + } + return setCfg1s(d, commands) +} + +// SetMessageRatesAllEnabled configures the GPS to output all NMEA sentences +func SetMessageRatesAllEnabled(d *Device) (err error) { + commands := []CfgMsg1{ + messageRateGSACmd, + messageRateGGACmd, + messageRateGLLCmd, + messageRateGSVCmd, + messageRateRMCCmd, + messageRateVTGCmd, + messageRateZDACmd, + messageRateTXTCmd, + } + for i := range commands { + commands[i].Rate = 1 // Enable + } + return setCfg1s(d, commands) +} + +func setCfg1s(d *Device, commands []CfgMsg1) (err error) { + var buf [9]byte + for _, cmd := range commands { + cmd.Put9Bytes(buf[:]) + // TODO handle errors differently here? + // This implementation just saves the last error and continues. + // Due to the GPS modules sending updates asynchronously + // the response is interleaved along with regular ASCII + // NMEA messages. + err = d.SendCommand(buf[:]) + time.Sleep(100 * time.Millisecond) + } + + return +} + +// gnssDisableCmd is a UBX-CFG-GNSS command to disable all GNSS but GPS +// Needed for MAX8's, not needed for MAX7 +var gnssDisableCmd = CfgGnss{ + MsgVer: 0x00, + NumTrkChHw: 0x20, // 32 channels + NumTrkChUse: 0x20, + ConfigBlocks: []CfgGnssConfigBlocksType{ + {GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000}, // GPS enabled + {GnssId: 1, ResTrkCh: 1, MaxTrkCh: 3, Flags: 0x010000}, // SBAS disabled + {GnssId: 3, ResTrkCh: 8, MaxTrkCh: 16, Flags: 0x010000}, // BeiDou disabled + {GnssId: 5, ResTrkCh: 0, MaxTrkCh: 3, Flags: 0x010000}, // QZSS disabled + {GnssId: 6, ResTrkCh: 8, MaxTrkCh: 14, Flags: 0x010000}, // GLONASS disabled + }, +} + +// SetGNSSDisable sends UBX-CFG-GNSS command to disable all GNSS but GPS +func (d *Device) SetGNSSDisable() (err error) { + err = gnssDisableCmd.Put(d.buffer[:]) + if err != nil { + return err + } + + return d.SendCommand(d.buffer[:]) +} + +// SendCommand sends a UBX command and waits for ACK/NAK response +func (d *Device) SendCommand(command []byte) error { + // Calculate and append checksum + checksummed := appendChecksum(command) + d.WriteBytes(checksummed) + start := time.Now() - for time.Now().Sub(start) < 1000 { - if d.readNextByte() == '\n' { - if d.readNextByte() == 0xB5 { - d.readNextByte() - if d.readNextByte() == 0x05 { - if d.readNextByte() == 0x01 { - return - } - } - } + for time.Since(start) < time.Second { + // Look for UBX sync sequence + if d.readNextByte() != ubxSyncChar1 { + continue + } + if d.readNextByte() != ubxSyncChar2 { + continue + } + + // Read message class and ID + msgClass := d.readNextByte() + msgID := d.readNextByte() + + // Check if it's an ACK class message + if msgClass != ubxClassACK { + continue + } + + // Read length (2 bytes, little-endian) - ACK is always 2 bytes payload + lenLo := d.readNextByte() + lenHi := d.readNextByte() + length := uint16(lenLo) | uint16(lenHi)<<8 + + if length != 2 { + continue + } + + // Read ACK payload: class and ID of acknowledged message + ackClass := d.readNextByte() + ackID := d.readNextByte() + + // Verify ACK is for our command (command[2] = class, command[3] = ID) + if ackClass != command[2] || ackID != command[3] { + continue + } + + if msgID == ubxACK_ACK { + return nil + } + if msgID == ubxACK_NAK { + return errGPSCommandRejected } } - return errors.New("no ACK to GPS command") + + return errNoACKToGPSCommand +} + +// appendChecksum calculates UBX checksum and appends it to the message +func appendChecksum(msg []byte) []byte { + var ckA, ckB byte + // Checksum covers class, ID, length, and payload (skip sync chars) + for i := 2; i < len(msg); i++ { + ckA += msg[i] + ckB += ckA + } + return append(msg, ckA, ckB) } diff --git a/gps/ublox_test.go b/gps/ublox_test.go new file mode 100644 index 0000000..4bad0c2 --- /dev/null +++ b/gps/ublox_test.go @@ -0,0 +1,353 @@ +package gps + +import ( + "testing" +) + +func TestAppendChecksum(t *testing.T) { + testCases := []struct { + name string + input []byte + expected []byte + }{ + { + name: "simple message", + input: []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00}, + expected: []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00, 0x2A, 0x84}, + }, + { + name: "CFG-NAV5 header only", + input: []byte{0xB5, 0x62, 0x06, 0x24, 0x24, 0x00}, + expected: []byte{0xB5, 0x62, 0x06, 0x24, 0x24, 0x00, 0x4E, 0xCC}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := appendChecksum(tc.input) + + if len(result) != len(tc.expected) { + t.Errorf("expected length %d, got %d", len(tc.expected), len(result)) + return + } + + // Check checksum bytes (last two bytes) + ckA := result[len(result)-2] + ckB := result[len(result)-1] + expectedCkA := tc.expected[len(tc.expected)-2] + expectedCkB := tc.expected[len(tc.expected)-1] + + if ckA != expectedCkA || ckB != expectedCkB { + t.Errorf("expected checksum 0x%02X 0x%02X, got 0x%02X 0x%02X", + expectedCkA, expectedCkB, ckA, ckB) + } + }) + } +} + +func TestAppendChecksumPreservesOriginal(t *testing.T) { + input := []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00} + original := make([]byte, len(input)) + copy(original, input) + + result := appendChecksum(input) + + // Verify original bytes are preserved + for i := range input { + if result[i] != original[i] { + t.Errorf("byte %d changed: expected 0x%02X, got 0x%02X", i, original[i], result[i]) + } + } + + // Verify two bytes were appended + if len(result) != len(input)+2 { + t.Errorf("expected length %d, got %d", len(input)+2, len(result)) + } +} + +func TestNav5CmdConfig(t *testing.T) { + // Verify nav5Cmd has expected values + if nav5Cmd.DynModel != 6 { + t.Errorf("expected DynModel 6 (airborne <1g), got %d", nav5Cmd.DynModel) + } + + if nav5Cmd.FixMode != 3 { + t.Errorf("expected FixMode 3 (auto 2D/3D), got %d", nav5Cmd.FixMode) + } + + expectedMask := CfgNav5Dyn | CfgNav5MinEl | CfgNav5PosFixMode + if nav5Cmd.Mask != expectedMask { + t.Errorf("expected Mask 0x%04X, got 0x%04X", expectedMask, nav5Cmd.Mask) + } + + if nav5Cmd.MinElev_deg != 5 { + t.Errorf("expected MinElev_deg 5, got %d", nav5Cmd.MinElev_deg) + } +} + +func TestGNSSDisableCmdConfig(t *testing.T) { + // Verify GNSSDisableCmd has expected structure + if gnssDisableCmd.MsgVer != 0 { + t.Errorf("expected MsgVer 0, got %d", gnssDisableCmd.MsgVer) + } + + if gnssDisableCmd.NumTrkChHw != 0x20 { + t.Errorf("expected NumTrkChHw 0x20, got 0x%02X", gnssDisableCmd.NumTrkChHw) + } + + if len(gnssDisableCmd.ConfigBlocks) != 5 { + t.Errorf("expected 5 config blocks, got %d", len(gnssDisableCmd.ConfigBlocks)) + return + } + + // Verify GPS is enabled + gpsBlock := gnssDisableCmd.ConfigBlocks[0] + if gpsBlock.GnssId != 0 { + t.Errorf("expected first block GnssId 0 (GPS), got %d", gpsBlock.GnssId) + } + if gpsBlock.Flags&CfgGnssEnable == 0 { + t.Error("expected GPS to be enabled") + } + + // Verify other GNSS are disabled + for i := 1; i < len(gnssDisableCmd.ConfigBlocks); i++ { + block := gnssDisableCmd.ConfigBlocks[i] + if block.Flags&CfgGnssEnable != 0 { + t.Errorf("expected block %d (GnssId %d) to be disabled", i, block.GnssId) + } + } +} + +func TestNav5CmdWrite(t *testing.T) { + buf := make([]byte, 64) + nav5Cmd.Put42Bytes(buf) + + // Verify sync chars + if buf[0] != 0xB5 || buf[1] != 0x62 { + t.Errorf("expected sync 0xB5 0x62, got 0x%02X 0x%02X", buf[0], buf[1]) + } + + // Verify class/id + if buf[2] != 0x06 || buf[3] != 0x24 { + t.Errorf("expected class/id 0x06 0x24, got 0x%02X 0x%02X", buf[2], buf[3]) + } + + // Verify DynModel at offset 8 + if buf[8] != 6 { + t.Errorf("expected DynModel 6, got %d", buf[8]) + } +} + +func TestGNSSDisableCmdWrite(t *testing.T) { + buf := make([]byte, 64) + err := gnssDisableCmd.Put(buf) + if err != nil { + t.Errorf("unexpected error, likely buffer too short for data: %v", err) + } + + // 6 header + 4 payload header + 5*8 blocks = 50 bytes + const expectedLen = 6 + 4 + 5*8 + sz := gnssDisableCmd.Size() + if sz != expectedLen { + t.Errorf("expected %d bytes, got %d", expectedLen, sz) + } + + // Verify sync chars + if buf[0] != 0xB5 || buf[1] != 0x62 { + t.Errorf("expected sync 0xB5 0x62, got 0x%02X 0x%02X", buf[0], buf[1]) + } + + // Verify class/id + if buf[2] != 0x06 || buf[3] != 0x3E { + t.Errorf("expected class/id 0x06 0x3E, got 0x%02X 0x%02X", buf[2], buf[3]) + } + + // Verify number of blocks + if buf[9] != 5 { + t.Errorf("expected 5 blocks, got %d", buf[9]) + } +} + +func TestChecksumCalculation(t *testing.T) { + // Test with known UBX message and expected checksum + // This is a minimal CFG-NAV5 poll message + msg := []byte{0xB5, 0x62, 0x06, 0x24, 0x00, 0x00} + + result := appendChecksum(msg) + + // Verify checksum by recalculating + var ckA, ckB byte + for i := 2; i < len(msg); i++ { + ckA += msg[i] + ckB += ckA + } + + if result[6] != ckA || result[7] != ckB { + t.Errorf("checksum mismatch: expected 0x%02X 0x%02X, got 0x%02X 0x%02X", + ckA, ckB, result[6], result[7]) + } +} + +func TestMessageRateCmdConfigs(t *testing.T) { + testCases := []struct { + name string + cmd CfgMsg1 + msgClass byte + msgID byte + rate byte + }{ + {"GGA", messageRateGGACmd, 0xF0, 0x00, 1}, + {"GLL", messageRateGLLCmd, 0xF0, 0x01, 1}, + {"GSA", messageRateGSACmd, 0xF0, 0x02, 0}, + {"GSV", messageRateGSVCmd, 0xF0, 0x03, 0}, + {"RMC", messageRateRMCCmd, 0xF0, 0x04, 1}, + {"VTG", messageRateVTGCmd, 0xF0, 0x05, 0}, + {"ZDA", messageRateZDACmd, 0xF0, 0x08, 0}, + {"TXT", messageRateTXTCmd, 0xF0, 0x41, 0}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if tc.cmd.MsgClass != tc.msgClass { + t.Errorf("expected MsgClass 0x%02X, got 0x%02X", tc.msgClass, tc.cmd.MsgClass) + } + if tc.cmd.MsgID != tc.msgID { + t.Errorf("expected MsgID 0x%02X, got 0x%02X", tc.msgID, tc.cmd.MsgID) + } + if tc.cmd.Rate != tc.rate { + t.Errorf("expected Rate %d, got %d", tc.rate, tc.cmd.Rate) + } + }) + } +} + +func TestCfgMsg1Write(t *testing.T) { + cmd := CfgMsg1{ + MsgClass: 0xF0, + MsgID: 0x00, + Rate: 1, + } + + buf := make([]byte, 16) + cmd.Put9Bytes(buf) + // Verify sync chars + if buf[0] != 0xB5 || buf[1] != 0x62 { + t.Errorf("expected sync 0xB5 0x62, got 0x%02X 0x%02X", buf[0], buf[1]) + } + + // Verify class/id (0x06 0x01 for CFG-MSG) + if buf[2] != 0x06 || buf[3] != 0x01 { + t.Errorf("expected class/id 0x06 0x01, got 0x%02X 0x%02X", buf[2], buf[3]) + } + + // Verify length (3 bytes payload) + if buf[4] != 3 || buf[5] != 0 { + t.Errorf("expected length 3, got %d", uint16(buf[4])|uint16(buf[5])<<8) + } + + // Verify payload + if buf[6] != 0xF0 { + t.Errorf("expected MsgClass 0xF0, got 0x%02X", buf[6]) + } + if buf[7] != 0x00 { + t.Errorf("expected MsgID 0x00, got 0x%02X", buf[7]) + } + if buf[8] != 1 { + t.Errorf("expected Rate 1, got %d", buf[8]) + } +} + +func TestCfgMsg1ClassID(t *testing.T) { + cmd := CfgMsg1{} + if got := cmd.classID(); got != 0x0106 { + t.Errorf("expected 0x0106, got 0x%04x", got) + } +} + +func TestMinimalMessageRatesConfig(t *testing.T) { + // Verify the minimal config has correct rates set + // GGA and RMC should be enabled (rate=1), others disabled (rate=0) + expectedRates := map[byte]byte{ + 0x00: 1, // GGA - enabled + 0x01: 1, // GLL - enabled + 0x02: 0, // GSA - disabled + 0x03: 0, // GSV - disabled + 0x04: 1, // RMC - enabled + 0x05: 0, // VTG - disabled + 0x08: 0, // ZDA - disabled + 0x41: 0, // TXT - disabled + } + + commands := []CfgMsg1{ + messageRateGGACmd, + messageRateGLLCmd, + messageRateGSACmd, + messageRateGSVCmd, + messageRateRMCCmd, + messageRateVTGCmd, + messageRateZDACmd, + messageRateTXTCmd, + } + + for _, cmd := range commands { + expectedRate, ok := expectedRates[cmd.MsgID] + if !ok { + t.Errorf("unexpected MsgID 0x%02X", cmd.MsgID) + continue + } + if cmd.Rate != expectedRate { + t.Errorf("MsgID 0x%02X: expected rate %d, got %d", cmd.MsgID, expectedRate, cmd.Rate) + } + } +} + +func TestAllMessageRatesWriteCorrectBytes(t *testing.T) { + // Test that each message rate command writes the correct bytes + commands := []CfgMsg1{ + messageRateGGACmd, + messageRateGLLCmd, + messageRateGSACmd, + messageRateGSVCmd, + messageRateRMCCmd, + messageRateVTGCmd, + messageRateZDACmd, + messageRateTXTCmd, + } + + for _, cmd := range commands { + buf := make([]byte, 16) + cmd.Put9Bytes(buf) + + // Verify MsgClass in payload + if buf[6] != 0xF0 { + t.Errorf("MsgID 0x%02X: expected MsgClass 0xF0, got 0x%02X", cmd.MsgID, buf[6]) + } + + // Verify MsgID in payload + if buf[7] != cmd.MsgID { + t.Errorf("expected MsgID 0x%02X in payload, got 0x%02X", cmd.MsgID, buf[7]) + } + + // Verify Rate in payload + if buf[8] != cmd.Rate { + t.Errorf("MsgID 0x%02X: expected Rate %d, got %d", cmd.MsgID, cmd.Rate, buf[8]) + } + } +} + +func TestSetMessageRatesAllEnabledModifiesRate(t *testing.T) { + // Verify that when we copy a command and set Rate=1, it works correctly + cmd := messageRateGSACmd // This one is disabled by default + if cmd.Rate != 0 { + t.Errorf("expected GSA default rate 0, got %d", cmd.Rate) + } + + // Simulate what SetMessageRatesAllEnabled does + cmd.Rate = 1 + + buf := make([]byte, 16) + cmd.Put9Bytes(buf) + if buf[8] != 1 { + t.Errorf("expected Rate 1 in buffer, got %d", buf[8]) + } +} diff --git a/gps/ubx.go b/gps/ubx.go new file mode 100644 index 0000000..e044d62 --- /dev/null +++ b/gps/ubx.go @@ -0,0 +1,220 @@ +package gps + +import "io" + +// UBX message classes +const ( + ubxClassACK = 0x05 +) + +// UBX ACK message IDs +const ( + ubxACK_NAK = 0x00 // Message not acknowledged + ubxACK_ACK = 0x01 // Message acknowledged +) + +// UBX sync characters +const ( + ubxSyncChar1 = 0xB5 + ubxSyncChar2 = 0x62 +) + +const ( + DynModePortable = 0 + DynModeStationary = 2 + DynModePedestrian = 3 + DynModeAutomotive = 4 + DynModeSea = 5 + DynModeAirborne1g = 6 + DynModeAirborne2g = 7 + DynModeAirborne4g = 8 + DynModeWristWatch = 9 + DynModeBike = 10 +) + +const ( + FixMode2D = 1 + FixMode3D = 2 + FixModeAuto = 3 +) + +// from https://github.com/daedaleanai/ublox/blob/main/ubx/messages.go + +// Message ubx-cfg-nav5 + +// CfgNav5 (Get/set) Navigation engine settings +// Class/Id 0x06 0x24 (36 bytes) +// See the Navigation Configuration Settings Description for a detailed description of how these settings affect receiver operation. +type CfgNav5 struct { + Mask CfgNav5Mask // Parameters bitmask. Only the masked parameters will be applied. + DynModel byte // Dynamic platform model: 0: portable 2: stationary 3: pedestrian 4: automotive 5: sea 6: airborne with <1g acceleration 7: airborne with <2g acceleration 8: airborne with <4g acceleration 9: wrist-worn watch (not supported in protocol versions less than 18) 10: bike (supported in protocol versions 19. 2) + FixMode byte // Position fixing mode: 1: 2D only 2: 3D only 3: auto 2D/3D + FixedAlt_me2 int32 // [1e-2 m] Fixed altitude (mean sea level) for 2D fix mode + FixedAltVar_m2e4 uint32 // [1e-4 m^2] Fixed altitude variance for 2D mode + MinElev_deg int8 // [deg] Minimum elevation for a GNSS satellite to be used in NAV + DrLimit_s byte // [s] Reserved + PDop uint16 // Position DOP mask to use + TDop uint16 // Time DOP mask to use + PAcc_m uint16 // [m] Position accuracy mask + TAcc_m uint16 // [m] Time accuracy mask + StaticHoldThresh_cm_s byte // [cm/s] Static hold threshold + DgnssTimeout_s byte // [s] DGNSS timeout + CnoThreshNumSVs byte // Number of satellites required to have C/N0 above cnoThresh for a fix to be attempted + CnoThresh_dbhz byte // [dBHz] C/N0 threshold for deciding whether to attempt a fix + Reserved1 [2]byte // Reserved + StaticHoldMaxDist_m uint16 // [m] Static hold distance threshold (before quitting static hold) + UtcStandard byte // UTC standard to be used: 0: Automatic; receiver selects based on GNSS configuration (see GNSS time bases) 3: UTC as operated by the U.S. Naval Observatory (USNO); derived from GPS time 5: UTC as combined from multiple European laboratories; derived from Galileo time 6: UTC as operated by the former Soviet Union (SU); derived from GLONASS time 7: UTC as operated by the National Time Service Center (NTSC), China; derived from BeiDou time (not supported in protocol versions less than 16). + Reserved2 [5]byte // Reserved +} + +func (CfgNav5) classID() uint16 { return 0x2406 } + +type CfgNav5Mask uint16 + +var _ io.WriterTo = CfgNav5{} // compile time guarantee of interface implementation. + +const ( + CfgNav5Dyn CfgNav5Mask = 0x1 // Apply dynamic model settings + CfgNav5MinEl CfgNav5Mask = 0x2 // Apply minimum elevation settings + CfgNav5PosFixMode CfgNav5Mask = 0x4 // Apply fix mode settings + CfgNav5DrLim CfgNav5Mask = 0x8 // Reserved + CfgNav5PosMask CfgNav5Mask = 0x10 // Apply position mask settings + CfgNav5TimeMask CfgNav5Mask = 0x20 // Apply time mask settings + CfgNav5StaticHoldMask CfgNav5Mask = 0x40 // Apply static hold settings + CfgNav5DgpsMask CfgNav5Mask = 0x80 // Apply DGPS settings + CfgNav5CnoThreshold CfgNav5Mask = 0x100 // Apply CNO threshold settings (cnoThresh, cnoThreshNumSVs) + CfgNav5Utc CfgNav5Mask = 0x400 // Apply UTC settings (not supported in protocol versions less than 16). +) + +func (cfg CfgNav5) Append(dst []byte) []byte { + var buf [42]byte + cfg.Put42Bytes(buf[:]) + dst = append(dst, buf[:]...) + return dst +} + +func (cfg CfgNav5) WriteTo(w io.Writer) (int64, error) { + var buf [42]byte + cfg.Put42Bytes(buf[:]) + n, err := w.Write(buf[:]) + return int64(n), err +} + +// Write CfgNav5 message to buffer +func (cfg CfgNav5) Put42Bytes(buf []byte) { + _ = buf[41] + copy(buf, []byte{0xb5, 0x62, byte(cfg.classID()), byte(cfg.classID() >> 8), 36, 0}) + + buf[6] = byte(cfg.Mask) + buf[7] = byte(cfg.Mask >> 8) + buf[8] = cfg.DynModel + buf[9] = cfg.FixMode + buf[10] = byte(cfg.FixedAlt_me2) + buf[11] = byte(cfg.FixedAlt_me2 >> 8) + buf[12] = byte(cfg.FixedAlt_me2 >> 16) + buf[13] = byte(cfg.FixedAlt_me2 >> 24) + buf[14] = byte(cfg.FixedAltVar_m2e4) + buf[15] = byte(cfg.FixedAltVar_m2e4 >> 8) + buf[16] = byte(cfg.FixedAltVar_m2e4 >> 16) + buf[17] = byte(cfg.FixedAltVar_m2e4 >> 24) + buf[18] = byte(cfg.MinElev_deg) + buf[19] = cfg.DrLimit_s + buf[20] = byte(cfg.PDop) + buf[21] = byte(cfg.PDop >> 8) + buf[22] = byte(cfg.TDop) + buf[23] = byte(cfg.TDop >> 8) + buf[24] = byte(cfg.PAcc_m) + buf[25] = byte(cfg.PAcc_m >> 8) + buf[26] = byte(cfg.TAcc_m) + buf[27] = byte(cfg.TAcc_m >> 8) + buf[28] = cfg.StaticHoldThresh_cm_s + buf[29] = cfg.DgnssTimeout_s + buf[30] = cfg.CnoThreshNumSVs + buf[31] = cfg.CnoThresh_dbhz + copy(buf[32:34], cfg.Reserved1[:]) + buf[34] = byte(cfg.StaticHoldMaxDist_m) + buf[35] = byte(cfg.StaticHoldMaxDist_m >> 8) + buf[36] = cfg.UtcStandard + copy(buf[37:42], cfg.Reserved2[:]) +} + +// Message ubx-cfg-msg + +// CfgMsg1 (Get/set) Set message rate +// Class/Id 0x06 0x01 (3 bytes) +// Set message rate configuration for the current port. See also section How to change between protocols. +type CfgMsg1 struct { + MsgClass byte // Message class + MsgID byte // Message identifier + Rate byte // Send rate on current port +} + +func (CfgMsg1) classID() uint16 { return 0x0106 } + +func (cfg CfgMsg1) Put9Bytes(buf []byte) { + copy(buf, []byte{0xb5, 0x62, byte(cfg.classID()), byte(cfg.classID() >> 8), 3, 0}) + buf[6] = cfg.MsgClass + buf[7] = cfg.MsgID + buf[8] = cfg.Rate +} + +// Message ubx-cfg-gnss + +// CfgGnss (Get/set) GNSS system configuration +// Class/Id 0x06 0x3e (4 + N*8 bytes) +// Gets or sets the GNSS system channel sharing configuration. If the receiver is sent a valid new configuration, it will respond with a UBX-ACK- ACK message and immediately change to the new configuration. Otherwise the receiver will reject the request, by issuing a UBX-ACK-NAK and continuing operation with the previous configuration. Configuration requirements: It is necessary for at least one major GNSS to be enabled, after applying the new configuration to the current one. It is also required that at least 4 tracking channels are available to each enabled major GNSS, i.e. maxTrkCh must have a minimum value of 4 for each enabled major GNSS. The number of tracking channels in use must not exceed the number of tracking channels available in hardware, and the sum of all reserved tracking channels needs to be less than or equal to the number of tracking channels in use. Notes: To avoid cross-correlation issues, it is recommended that GPS and QZSS are always both enabled or both disabled. Polling this message returns the configuration of all supported GNSS, whether enabled or not; it may also include GNSS unsupported by the particular product, but in such cases the enable flag will always be unset. See section GNSS Configuration for a discussion of the use of this message. See section Satellite Numbering for a description of the GNSS IDs available. Configuration specific to the GNSS system can be done via other messages (e. g. UBX-CFG-SBAS). +type CfgGnss struct { + MsgVer byte // Message version (0x00 for this version) + NumTrkChHw byte // Number of tracking channels available in hardware (read only) + NumTrkChUse byte // (Read only in protocol versions greater than 23) Number of tracking channels to use. Must be > 0, <= numTrkChHw. If 0xFF, then number of tracking channels to use will be set to numTrkChHw. + NumConfigBlocks byte `len:"ConfigBlocks"` // Number of configuration blocks following + ConfigBlocks []CfgGnssConfigBlocksType // len: NumConfigBlocks +} + +func (CfgGnss) classID() uint16 { return 0x3e06 } + +type CfgGnssConfigBlocksType struct { + GnssId byte // System identifier (see Satellite Numbering ) + ResTrkCh byte // (Read only in protocol versions greater than 23) Number of reserved (minimum) tracking channels for this system. + MaxTrkCh byte // (Read only in protocol versions greater than 23) Maximum number of tracking channels used for this system. Must be > 0, >= resTrkChn, <= numTrkChUse and <= maximum number of tracking channels supported for this system. + Reserved1 byte // Reserved + Flags CfgGnssFlags // Bitfield of flags. At least one signal must be configured in every enabled system. +} + +type CfgGnssFlags uint32 + +const ( + CfgGnssEnable CfgGnssFlags = 0x1 // Enable this system + CfgGnssSigCfgMask CfgGnssFlags = 0xff0000 // Signal configuration mask When gnssId is 0 (GPS) 0x01 = GPS L1C/A 0x10 = GPS L2C 0x20 = GPS L5 When gnssId is 1 (SBAS) 0x01 = SBAS L1C/A When gnssId is 2 (Galileo) 0x01 = Galileo E1 (not supported in protocol versions less than 18) 0x10 = Galileo E5a 0x20 = Galileo E5b When gnssId is 3 (BeiDou) 0x01 = BeiDou B1I 0x10 = BeiDou B2I 0x80 = BeiDou B2A When gnssId is 4 (IMES) 0x01 = IMES L1 When gnssId is 5 (QZSS) 0x01 = QZSS L1C/A 0x04 = QZSS L1S 0x10 = QZSS L2C 0x20 = QZSS L5 When gnssId is 6 (GLONASS) 0x01 = GLONASS L1 0x10 = GLONASS L2 +) + +// Write CfgGnss message to buffer +func (cfg CfgGnss) Put(buf []byte) error { + sz := cfg.Size() + if sz > len(buf) { + return io.ErrShortBuffer + } + copy(buf, []byte{0xb5, 0x62, byte(cfg.classID()), byte(cfg.classID() >> 8), 4 + byte(len(cfg.ConfigBlocks))*8, 0}) + buf[6] = cfg.MsgVer + buf[7] = cfg.NumTrkChHw + buf[8] = cfg.NumTrkChUse + buf[9] = byte(len(cfg.ConfigBlocks)) + offset := 10 + for _, block := range cfg.ConfigBlocks { + buf[offset] = block.GnssId + buf[offset+1] = block.ResTrkCh + buf[offset+2] = block.MaxTrkCh + buf[offset+3] = block.Reserved1 + buf[offset+4] = byte(block.Flags) + buf[offset+5] = byte(block.Flags >> 8) + buf[offset+6] = byte(block.Flags >> 16) + buf[offset+7] = byte(block.Flags >> 24) + offset += 8 + } + return nil +} + +// Size returns length of CfgGnss in bytes when sent over the wire. +func (cfg CfgGnss) Size() int { + return 10 + 8*len(cfg.ConfigBlocks) +} diff --git a/gps/ubx_test.go b/gps/ubx_test.go new file mode 100644 index 0000000..ac8c61f --- /dev/null +++ b/gps/ubx_test.go @@ -0,0 +1,189 @@ +package gps + +import ( + "testing" +) + +func TestCfgNav5ClassID(t *testing.T) { + cfg := CfgNav5{} + if got := cfg.classID(); got != 0x2406 { + t.Errorf("expected 0x2406, got 0x%04x", got) + } +} + +func TestCfgNav5Write(t *testing.T) { + cfg := CfgNav5{ + Mask: CfgNav5Dyn | CfgNav5MinEl, + DynModel: 4, + FixMode: 3, + FixedAlt_me2: 10000, + FixedAltVar_m2e4: 10000, + MinElev_deg: 5, + DrLimit_s: 0, + PDop: 250, + TDop: 250, + PAcc_m: 100, + TAcc_m: 300, + StaticHoldThresh_cm_s: 50, + DgnssTimeout_s: 60, + CnoThreshNumSVs: 3, + CnoThresh_dbhz: 35, + Reserved1: [2]byte{0, 0}, + StaticHoldMaxDist_m: 200, + UtcStandard: 0, + Reserved2: [5]byte{0, 0, 0, 0, 0}, + } + + buf := make([]byte, 64) + cfg.Put42Bytes(buf) + + // Check sync chars + if buf[0] != 0xb5 || buf[1] != 0x62 { + t.Errorf("expected sync chars 0xb5 0x62, got 0x%02x 0x%02x", buf[0], buf[1]) + } + + // Check class/id (little-endian) + if buf[2] != 0x06 || buf[3] != 0x24 { + t.Errorf("expected class/id 0x06 0x24, got 0x%02x 0x%02x", buf[2], buf[3]) + } + + // Check length + if buf[4] != 36 || buf[5] != 0 { + t.Errorf("expected length 36, got %d", uint16(buf[4])|uint16(buf[5])<<8) + } + + // Check Mask (little-endian) + mask := uint16(buf[6]) | uint16(buf[7])<<8 + if mask != uint16(CfgNav5Dyn|CfgNav5MinEl) { + t.Errorf("expected mask 0x03, got 0x%04x", mask) + } + + // Check DynModel + if buf[8] != 4 { + t.Errorf("expected DynModel 4, got %d", buf[8]) + } + + // Check FixMode + if buf[9] != 3 { + t.Errorf("expected FixMode 3, got %d", buf[9]) + } + + // Check FixedAlt_me2 (little-endian int32) + fixedAlt := int32(buf[10]) | int32(buf[11])<<8 | int32(buf[12])<<16 | int32(buf[13])<<24 + if fixedAlt != 10000 { + t.Errorf("expected FixedAlt_me2 10000, got %d", fixedAlt) + } +} + +func TestCfgGnssClassID(t *testing.T) { + cfg := CfgGnss{} + if got := cfg.classID(); got != 0x3e06 { + t.Errorf("expected 0x3e06, got 0x%04x", got) + } +} + +func TestCfgGnssWrite(t *testing.T) { + testCases := []struct { + name string + cfg CfgGnss + expectedLen int + expectedBlocks byte + }{ + { + name: "no config blocks", + cfg: CfgGnss{ + MsgVer: 0, + NumTrkChHw: 32, + NumTrkChUse: 32, + ConfigBlocks: nil, + }, + expectedLen: 10, + expectedBlocks: 0, + }, + { + name: "one config block", + cfg: CfgGnss{ + MsgVer: 0, + NumTrkChHw: 32, + NumTrkChUse: 32, + ConfigBlocks: []CfgGnssConfigBlocksType{ + {GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000}, + }, + }, + expectedLen: 18, + expectedBlocks: 1, + }, + { + name: "two config blocks", + cfg: CfgGnss{ + MsgVer: 0, + NumTrkChHw: 32, + NumTrkChUse: 32, + ConfigBlocks: []CfgGnssConfigBlocksType{ + {GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Flags: CfgGnssEnable | 0x010000}, + {GnssId: 6, ResTrkCh: 8, MaxTrkCh: 14, Flags: CfgGnssEnable | 0x010000}, + }, + }, + expectedLen: 26, + expectedBlocks: 2, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + buf := make([]byte, 64) + err := tc.cfg.Put(buf) + if err != nil { + t.Errorf("unexpected error, data too long?: %v", err) + } + // Check sync chars + if buf[0] != 0xb5 || buf[1] != 0x62 { + t.Errorf("expected sync chars 0xb5 0x62, got 0x%02x 0x%02x", buf[0], buf[1]) + } + + // Check class/id (little-endian) + if buf[2] != 0x06 || buf[3] != 0x3e { + t.Errorf("expected class/id 0x06 0x3e, got 0x%02x 0x%02x", buf[2], buf[3]) + } + + // Check number of config blocks + if buf[9] != tc.expectedBlocks { + t.Errorf("expected %d config blocks, got %d", tc.expectedBlocks, buf[9]) + } + }) + } +} + +func TestCfgGnssWriteBlockContent(t *testing.T) { + cfg := CfgGnss{ + MsgVer: 0, + NumTrkChHw: 32, + NumTrkChUse: 32, + ConfigBlocks: []CfgGnssConfigBlocksType{ + {GnssId: 0, ResTrkCh: 8, MaxTrkCh: 16, Reserved1: 0, Flags: CfgGnssEnable | 0x010000}, + }, + } + + buf := make([]byte, 64) + err := cfg.Put(buf) + if err != nil { + t.Fatal(err) + } + // Check first block at offset 10 + if buf[10] != 0 { + t.Errorf("expected GnssId 0, got %d", buf[10]) + } + if buf[11] != 8 { + t.Errorf("expected ResTrkCh 8, got %d", buf[11]) + } + if buf[12] != 16 { + t.Errorf("expected MaxTrkCh 16, got %d", buf[12]) + } + + // Check flags (little-endian uint32) + flags := uint32(buf[14]) | uint32(buf[15])<<8 | uint32(buf[16])<<16 | uint32(buf[17])<<24 + expectedFlags := uint32(CfgGnssEnable | 0x010000) + if flags != expectedFlags { + t.Errorf("expected flags 0x%08x, got 0x%08x", expectedFlags, flags) + } +} diff --git a/hd44780/hd44780.go b/hd44780/hd44780.go index 111f70d..2cf2c0a 100644 --- a/hd44780/hd44780.go +++ b/hd44780/hd44780.go @@ -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. diff --git a/honeyhsc/hsc.go b/honeyhsc/hsc.go new file mode 100644 index 0000000..efc58a5 --- /dev/null +++ b/honeyhsc/hsc.go @@ -0,0 +1,191 @@ +package honeyhsc + +import ( + "errors" + "math" + + "tinygo.org/x/drivers" +) + +var ( + errSensorMissing = errors.New("hsc: not connected") + errDiagnostic = errors.New("hsc: diagnostic error") +) + +const ( + measuremask = drivers.Pressure | drivers.Temperature + statusMask = 0b1100_0000 + statusOffset = 6 +) + +// DevI2C is the TruStability® High Accuracy Silicon Ceramic (HSC) Series is a piezoresistive silicon pressure sensor offering a ratiometric +// analog or digital output for reading pressure over the specified full scale pressure span and temperature range. +type DevI2C struct { + bus drivers.I2C + dev + addr uint8 + buf [6]byte +} + +// NewDevI2C creates and returns a new DevI2C that communicates with an HSC device over the provided I2C bus. +// Parameters: +// - bus: the I2C bus to use. +// - addr: the 7-bit I2C address of the sensor. +// - outMin, outMax: raw output code range (counts) corresponding to the pressure span. Depends on sensor model. +// - pMin, pMax: pressure range endpoints in millipascals (mPa). Depends on sensor model. +// +// The returned DevI2C will use these calibration parameters to convert raw bridge counts to pressure. +func NewDevI2C(bus drivers.I2C, addr, outMin, outMax uint16, pMin, pMax int32) *DevI2C { + h := &DevI2C{ + bus: bus, + addr: uint8(addr), + dev: dev{ + cmin: outMin, + cmax: outMax, + pmin: pMin, + pmax: pMax, + }, + } + return h +} + +// ReadTemperature reads and returns the temperature in milliKelvin (mC) from the I2C-attached HSC device. +// It performs an Update internally to get the latest temperature value. +func (h *DevI2C) ReadTemperature() (int32, error) { + err := h.Update(drivers.Temperature) + if err != nil { + return 0, err + } + return h.Temperature(), nil +} + +// Update reads both temperature and pressure data from the I2C-attached HSC device when +// the requested measurement mask includes pressure or temperature. +// If neither pressure nor temperature is requested, Update is a no-op. +func (d *DevI2C) Update(which drivers.Measurement) error { + // Update performs an I2C transaction to read 4 bytes, parses the status bits, 14-bit bridge data and + // temperature bits, and forwards them to the internal update routine. Any I2C transport error is returned, + // as well as errors produced by the internal update (e.g. errSensorMissing, errDiagnostic). + if which&measuremask == 0 { + return nil + } + rbuf := d.buf[:4] + wbuf := d.buf[4:6] + const reg = 0 + value := (d.addr << 1) | 1 + wbuf[0] = reg + wbuf[1] = value + err := d.bus.Tx(uint16(d.addr), wbuf, rbuf) + if err != nil { + return err + } + status := (rbuf[0] & statusMask) >> statusOffset + bridgeData := (uint16(rbuf[0]&^statusMask) << 8) | uint16(rbuf[1]) + tempData := uint16(rbuf[2])<<8 | uint16(rbuf[3]&0xe0)>>5 + return d.dev.update(status, bridgeData, tempData) +} + +type pinout func(level bool) + +// DevI2C is the TruStability® High Accuracy Silicon Ceramic (HSC) Series is a piezoresistive silicon pressure sensor offering a ratiometric +// analog or digital output for reading pressure over the specified full scale pressure span and temperature range. +type DevSPI struct { + spi drivers.SPI + cs pinout + dev + buf [4]byte +} + +// NewDevSPI creates and returns a new DevSPI that communicates with an HSC device over SPI. +// Parameters: +// - conn: the SPI connection to use. +// - cs: a chip-select function that drives the device select line low/high. +// - outMin, outMax: raw output code range (counts) corresponding to the pressure span. Depends on sensor model. +// - pMin, pMax: pressure range endpoints in millipascals (mPa). Depends on sensor model. +// +// The function returns the constructed DevSPI and an error value (currently always nil). +func NewDevSPI(conn drivers.SPI, cs pinout, outMin, outMax uint16, pMin, pMax int32) (*DevSPI, error) { + h := &DevSPI{ + spi: conn, + cs: cs, + dev: dev{ + cmin: outMin, + cmax: outMax, + pmin: pMin, + pmax: pMax, + }, + } + return h, nil +} + +// ReadTemperature reads and returns the temperature in milliKelvin (mC) from the SPI-attached HSC device. +// It performs an Update internally to get the latest temperature value. +func (h *DevSPI) ReadTemperature() (int32, error) { + err := h.Update(drivers.Temperature) + if err != nil { + return 0, err + } + return h.Temperature(), nil +} + +// Update reads pressure and temperature data from the SPI-attached HSC device when the requested measurement mask includes +// pressure or temperature. If neither pressure nor temperature is requested, Update is a no-op. +func (h *DevSPI) Update(which drivers.Measurement) error { + // It toggles the provided chip-select, performs an SPI transfer to read 4 bytes, parses the status bits, + // 14-bit bridge data and temperature bits, and forwards them to the internal update routine. Any SPI + // transport error is returned, as well as errors produced by the internal update (e.g. errSensorMissing, errDiagnostic). + if which&measuremask == 0 { + return nil + } + buf := &h.buf + h.cs(false) + err := h.spi.Tx(nil, buf[:4]) + h.cs(true) + if err != nil { + return err + } + // First two bits are status bits. + status := (buf[0] & statusMask) >> statusOffset + bridgeData := (uint16(buf[0]&^statusMask) << 8) | uint16(buf[1]) + + tempData := uint16(buf[2])<<8 | uint16(buf[3]&0xe0)>>5 + return h.dev.update(status, bridgeData, tempData) +} + +type dev struct { + pressure int32 + temp int32 + cmin, cmax uint16 + pmin, pmax int32 +} + +// Pressure returns the most recently computed pressure value in millipascals (mPa). +// The value is taken from the last successful Update. +func (d *dev) Pressure() int32 { + return d.pressure +} + +// Temperature returns the most recently read temperature value in milliKelvin (mC). +// The value is taken from the last successful Update. +func (d *dev) Temperature() int32 { + return d.temp + 273_150 +} + +// update interprets raw sensor fields (status, bridgeData, tempData) and updates the dev's stored +// pressure and temperature. It returns errSensorMissing when the temperature raw value indicates no sensor +// (tempData == math.MaxUint16), errDiagnostic when the status indicates a device diagnostic condition +// (status == 3), or nil on success. Pressure is computed with integer arithmetic using the configured +// cmin/cmax -> pmin/pmax linear mapping in order to avoid overflows. +func (d *dev) update(status uint8, bridgeData, tempData uint16) error { + if tempData == math.MaxUint16 { + return errSensorMissing + } else if status == 3 { + return errDiagnostic + } + + // Take care not to overflow here. + p := (int32(bridgeData)-int32(d.cmin))*(d.pmax-d.pmin)/int32(d.cmax-d.cmin) + d.pmin + d.temp = int32(tempData) + d.pressure = p + return nil +} diff --git a/hub75/hub75.go b/hub75/hub75.go index 8205a53..427d849 100644 --- a/hub75/hub75.go +++ b/hub75/hub75.go @@ -167,7 +167,7 @@ func (d *Device) fillMatrixBuffer(x int16, y int16, r uint8, g uint8, b uint8) { if r > colorTresh { d.buffer[c][offsetR] |= 1 << bitSelect } else { - d.buffer[c][offsetR] = d.buffer[c][offsetR] &^ 1 << bitSelect + d.buffer[c][offsetR] &^= 1 << bitSelect } if g > colorTresh { d.buffer[(c+d.colorThirdStep)%d.colorDepth][offsetG] |= 1 << bitSelect diff --git a/ili9341/spi_atsamd21.go b/ili9341/spi_atsamd21.go index 5aff3a9..c9582e4 100644 --- a/ili9341/spi_atsamd21.go +++ b/ili9341/spi_atsamd21.go @@ -8,10 +8,10 @@ import ( ) type spiDriver struct { - bus machine.SPI + bus *machine.SPI } -func NewSPI(bus machine.SPI, dc, cs, rst machine.Pin) *Device { +func NewSPI(bus *machine.SPI, dc, cs, rst machine.Pin) *Device { return &Device{ dc: dc, cs: cs, diff --git a/ili9341/spi_atsamd51.go b/ili9341/spi_atsamd51.go index 40b9995..a5cee85 100644 --- a/ili9341/spi_atsamd51.go +++ b/ili9341/spi_atsamd51.go @@ -8,10 +8,10 @@ import ( ) type spiDriver struct { - bus machine.SPI + bus *machine.SPI } -func NewSPI(bus machine.SPI, dc, cs, rst machine.Pin) *Device { +func NewSPI(bus *machine.SPI, dc, cs, rst machine.Pin) *Device { return &Device{ dc: dc, cs: cs, diff --git a/ina219/config.go b/ina219/config.go new file mode 100644 index 0000000..4c02179 --- /dev/null +++ b/ina219/config.go @@ -0,0 +1,183 @@ +package ina219 + +type Config struct { + // BusVoltageRange sets the bus voltage range. + BusVoltageRange BusVoltageRange + + // PGA sets the programmable gain amplifier. + PGA PGA + + // BusADC sets the bus ADC resolution. + BusADC BusADC + + // ShuntADC sets the shunt ADC resolution. + ShuntADC ShuntADC + + // Mode sets the operating mode. + Mode Mode + + // Calibration sets the calibration value for the expected + // voltage and current values. + Calibration Calibration + + // 1000 / uA per bit + CurrentDivider float32 + + // 1mW per bit + PowerMultiplier float32 +} + +// RegisterValue returns the register value of the configuration. +func (c *Config) RegisterValue() uint16 { + return c.BusVoltageRange.RegisterValue() | + c.PGA.RegisterValue() | + c.BusADC.RegisterValue() | + c.ShuntADC.RegisterValue() | + c.Mode.RegisterValue() +} + +// Generate a new configuration from a register value. +func NewConfig(config int16, calibration int16) Config { + return Config{ + BusVoltageRange: BusVoltageRange(config >> 13 & 0x1), + PGA: PGA(config >> 11 & 0x3), + BusADC: BusADC(config >> 7 & 0xF), + ShuntADC: ShuntADC(config >> 3 & 0xF), + Mode: Mode(config & 0x7), + Calibration: Calibration(calibration), + } +} + +// Configurations from +// https://github.com/adafruit/Adafruit_INA219/blob/master/Adafruit_INA219.cpp +var ( + // Config32V2A is a configuration for a 32V 2A range. + Config32V2A = Config{ + BusVoltageRange: Range32V, + PGA: PGA8, + BusADC: ADC12, + ShuntADC: SADC12, + Mode: ModeContShuntBus, + Calibration: Calibration16V400mA, + CurrentDivider: 10.0, + PowerMultiplier: 2.0, + } + + // Config32V1A is a configuration for a 32V 1A range. + Config32V1A = Config{ + BusVoltageRange: Range32V, + PGA: PGA8, + BusADC: ADC12, + ShuntADC: SADC12, + Mode: ModeContShuntBus, + Calibration: Calibration32V1A, + CurrentDivider: 25.0, + PowerMultiplier: 0.8, + } + + // Config16V400mA is a configuration for a 16V 400mA range. + Config16V400mA = Config{ + BusVoltageRange: Range16V, + PGA: PGA1, + BusADC: ADC12, + ShuntADC: SADC12, + Mode: ModeContShuntBus, + Calibration: Calibration16V400mA, + CurrentDivider: 20.0, + PowerMultiplier: 1.0, + } +) + +// BusVoltageRange is the bus voltage range. +type BusVoltageRange int8 + +const ( + Range16V BusVoltageRange = 0 // 0-16V + Range32V BusVoltageRange = 1 // 0-32V +) + +func (r BusVoltageRange) RegisterValue() uint16 { + return uint16(r) << 13 +} + +// PGA is the programmable gain amplifier. +type PGA int8 + +const ( + PGA1 PGA = 0 // 40mV + PGA2 PGA = 1 // 80mV + PGA4 PGA = 2 // 160mV + PGA8 PGA = 3 // 320mV +) + +func (p PGA) RegisterValue() uint16 { + return uint16(p) << 11 +} + +// BusADC is the bus ADC resolution. +type BusADC int8 + +const ( + ADC9 BusADC = 0 // 9-bit + ADC10 BusADC = 1 // 10-bit + ADC11 BusADC = 2 // 11-bit + ADC12 BusADC = 3 // 12-bit +) + +func (b BusADC) RegisterValue() uint16 { + return uint16(b) << 7 +} + +// ShuntADC is the shunt ADC resolution. +type ShuntADC int8 + +const ( + SADC9 ShuntADC = 0 // 9-bit + SADC10 ShuntADC = 1 // 10-bit + SADC11 ShuntADC = 2 // 11-bit + SADC12 ShuntADC = 3 // 12-bit +) + +func (s ShuntADC) RegisterValue() uint16 { + return uint16(s) << 3 +} + +// Mode is the operating mode. +type Mode int8 + +const ( + ModePowerDown Mode = 0 // power-down + ModeTrigShunt Mode = 1 // triggered shunt voltage + ModeTrigBus Mode = 2 // triggered bus voltage + ModeTrigShuntBus Mode = 3 // triggered shunt and bus voltage + ModeADCOff Mode = 4 // ADC off + ModeContShunt Mode = 5 // continuous shunt voltage + ModeContBus Mode = 6 // continuous bus voltage + ModeContShuntBus Mode = 7 // continuous shunt and bus voltage +) + +// ModeTriggered is a mask for triggered modes. +const ModeTriggeredMask Mode = 0x4 + +// ModeTriggered returns true if the mode is a triggered mode. +func ModeTriggered(m Mode) bool { + return m != ModePowerDown && m&ModeTriggeredMask == 0 +} + +func (m Mode) RegisterValue() uint16 { + return uint16(m) +} + +// Calibration is the calibration register for the INA219. Values from: +// https://github.com/adafruit/Adafruit_INA219/blob/master/Adafruit_INA219.cpp +type Calibration uint16 + +const ( + Calibration32V2A Calibration = 4096 + Calibration32V1A Calibration = 10240 + Calibration16V400mA Calibration = 8192 +) + +func (c Calibration) RegisterValue() uint16 { + return uint16(c) +} diff --git a/ina219/errors.go b/ina219/errors.go new file mode 100644 index 0000000..fe078fc --- /dev/null +++ b/ina219/errors.go @@ -0,0 +1,13 @@ +package ina219 + +type ErrOverflow struct{} + +func (e ErrOverflow) Error() string { return "overflow" } + +type ErrNotReady struct{} + +func (e ErrNotReady) Error() string { return "not ready" } + +type ErrConfigMismatch struct{} + +func (e ErrConfigMismatch) Error() string { return "config mismatch" } diff --git a/ina219/ina219.go b/ina219/ina219.go new file mode 100644 index 0000000..0f7c299 --- /dev/null +++ b/ina219/ina219.go @@ -0,0 +1,202 @@ +package ina219 + +import ( + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" +) + +// An INA219 device. +type Device struct { + bus drivers.I2C + Address uint16 + config Config +} + +// Create a new INA219 device with the default configuration +// and the given I2C bus at the default address. +// +// Set Address after New to change the address. +// +// Call Configure after New to write the configuration to the +// device. If you don't call Configure, the device may have a +// different configuration and the power divider and current +// multiplier are probably wrong. +func New(bus drivers.I2C) Device { + return Device{ + bus: bus, + Address: Address, + config: Config32V2A, + } +} + +// Set the configuration for the device. This only changes the +// configuration in memory, not on the device. Call Configure +// to write the configuration to the device. +func (d *Device) SetConfig(config Config) { + d.config = config +} + +// Write the current configuration to the device. +func (d *Device) Configure() (err error) { + if err = d.WriteRegister( + RegConfig, + d.config.RegisterValue(), + ); err != nil { + return + } + + if err = d.WriteRegister( + RegCalibration, + d.config.Calibration.RegisterValue(), + ); err != nil { + return + } + + var readConfig Config + // make sure the configuration is read back correctly + if readConfig, err = d.ReadConfig(); err != nil { + return + } else if readConfig.RegisterValue() != d.config.RegisterValue() { + err = ErrConfigMismatch{} + } else if readConfig.Calibration.RegisterValue() != d.config.Calibration.RegisterValue() { + err = ErrConfigMismatch{} + } + + return +} + +// Trigger a conversion. This is only necessary if the device is in +// trigger mode. In continuous mode (the default), the device will +// automatically trigger conversions and this has no effect. See +// config.go. +// +// Triggering a conversion or reading the "power" register resets +// the conversion ready bit. +func (d *Device) Trigger() (err error) { + // Only trigger if the mode is one of the triggered modes. + if ModeTriggered(d.config.Mode) { + err = d.WriteRegister(RegConfig, d.config.RegisterValue()) + } + return +} + +// Measurements reads the bus voltage, shunt voltage, current, and power +// from the device. +func (d *Device) Measurements() ( + busVoltage int16, + shuntVoltage int16, + current float32, + power float32, + err error, +) { + // Attempt to read bus voltage first, so we can check for overflow + // or conversion not ready. + if busVoltage, err = d.BusVoltage(); err != nil { + return + } + + // Read the rest of the values, reading Power last, which resets + // the conversion ready bit (relevant for triggered modes). + if shuntVoltage, err = d.ShuntVoltage(); err != nil { + return + } + + if current, err = d.Current(); err != nil { + return + } + + if power, err = d.Power(); err != nil { + return + } + + return +} + +// BusVoltage reads the "bus" voltage in millivolts. +// +// It returns an error if the value is invalid due to overflow +// or if the conversion is not ready yet. In a continuous mode +// there should always be a measurement available after the +// device is ready. See above notes on Trigger. +func (d *Device) BusVoltage() (voltage int16, err error) { + val, err := d.ReadRegister(RegBusVoltage) + if err != nil { + return + } + + // The overflow bit is set, so the values are invalid. + if val&(1<<0) != 0 { + err = ErrOverflow{} + return + } + + // The conversion is not ready yet. + if ModeTriggered(d.config.Mode) && val&(1<<1) != 0 { + err = ErrNotReady{} + return + } + + voltage = (int16(val) >> 3) * 4 + return +} + +// ShuntVoltage reads the "shunt" voltage in 100ths of a millivolt. +func (d *Device) ShuntVoltage() (voltage int16, err error) { + return d.ReadRegister(RegShuntVoltage) +} + +// Current reads the current in milliamps. +func (d *Device) Current() (current float32, err error) { + val, err := d.ReadRegister(RegCurrent) + if err != nil { + return + } + + current = float32(val) / d.config.CurrentDivider + return +} + +// Power reads the power in milliwatts. +func (d *Device) Power() (power float32, err error) { + val, err := d.ReadRegister(RegPower) + if err != nil { + return + } + + power = float32(val) * d.config.PowerMultiplier + return +} + +// Read the configuration from the device. +func (d *Device) ReadConfig() (config Config, err error) { + var cfg, cal int16 + if cfg, err = d.ReadRegister(RegConfig); err != nil { + return + } + + if cal, err = d.ReadRegister(RegCalibration); err != nil { + return + } + + config = NewConfig(cfg, cal) + return +} + +// Read a register from the device. +func (d *Device) ReadRegister(reg uint8) (val int16, err error) { + buf := make([]byte, 2) + + err = legacy.ReadRegister(d.bus, uint8(d.Address), reg, buf) + if err != nil { + return + } + + val = int16(buf[0])<<8 | int16(buf[1]&0xff) + return +} + +// Write to a register on the device. +func (d *Device) WriteRegister(reg uint8, val uint16) error { + buf := []byte{byte(val >> 8), byte(val & 0xff)} + return legacy.WriteRegister(d.bus, uint8(d.Address), reg, buf) +} diff --git a/ina219/ina219_test.go b/ina219/ina219_test.go new file mode 100644 index 0000000..f5a12ab --- /dev/null +++ b/ina219/ina219_test.go @@ -0,0 +1,232 @@ +package ina219 + +import ( + "fmt" + "testing" + + qt "github.com/frankban/quicktest" + "tinygo.org/x/drivers/tester" +) + +func TestDefaultAddress(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + dev := New(bus) + c.Assert(dev.Address, qt.Equals, uint16(Address)) +} + +func TestBusVoltage(t *testing.T) { + t.Run("valid", func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegBusVoltage: (4200 << 3) / 4, // 4.2V + } + bus.AddDevice(fake) + + dev := New(bus) + voltage, err := dev.BusVoltage() + c.Assert(err, qt.IsNil) + c.Assert(voltage, qt.Equals, int16(4200)) + }) + + t.Run("overflow", func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegBusVoltage: (1 >> 0), // overflow + } + bus.AddDevice(fake) + + dev := New(bus) + _, err := dev.BusVoltage() + c.Assert(err, qt.Not(qt.IsNil)) + c.Assert(err, qt.ErrorMatches, ErrOverflow{}.Error()) + }) + + t.Run("not ready", func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegBusVoltage: ((4200 << 3) / 4) | (1 << 1), // not ready + } + bus.AddDevice(fake) + + dev := New(bus) + dev.config.Mode = ModeTrigBus + _, err := dev.BusVoltage() + c.Assert(err, qt.Not(qt.IsNil)) + c.Assert(err, qt.ErrorMatches, ErrNotReady{}.Error()) + }) +} + +func TestShuntVoltage(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegShuntVoltage: 0x1234, + } + bus.AddDevice(fake) + + dev := New(bus) + voltage, err := dev.ShuntVoltage() + c.Assert(err, qt.IsNil) + c.Assert(voltage, qt.Equals, int16(0x1234)) +} + +func TestCurrent(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegCurrent: 420 * 6.9, // 420mA + } + bus.AddDevice(fake) + + dev := New(bus) + dev.config.CurrentDivider = 6.9 + current, err := dev.Current() + c.Assert(err, qt.IsNil) + c.Assert(current, qt.Equals, float32(420)) +} + +func TestPower(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegPower: 420 / 0.8, // 420mW + } + bus.AddDevice(fake) + + dev := New(bus) + dev.config.PowerMultiplier = 0.8 + power, err := dev.Power() + c.Assert(err, qt.IsNil) + c.Assert(power, qt.Equals, float32(420)) +} + +func TestReadConfig(t *testing.T) { + // use the default configurations + for _, tc := range []Config{ + Config16V400mA, + Config32V2A, + Config32V1A, + } { + n := fmt.Sprintf("%x/%x", tc.RegisterValue(), tc.Calibration.RegisterValue()) + t.Run(n, func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + fake.Registers = map[uint8]uint16{ + RegConfig: tc.RegisterValue(), + RegCalibration: tc.Calibration.RegisterValue(), + } + bus.AddDevice(fake) + + dev := New(bus) + config, err := dev.ReadConfig() + c.Assert(err, qt.IsNil) + c.Assert(config.BusADC, qt.Equals, tc.BusADC) + c.Assert(config.BusVoltageRange, qt.Equals, tc.BusVoltageRange) + c.Assert(config.Calibration, qt.Equals, tc.Calibration) + c.Assert(config.Mode, qt.Equals, tc.Mode) + c.Assert(config.PGA, qt.Equals, tc.PGA) + c.Assert(config.ShuntADC, qt.Equals, tc.ShuntADC) + }) + } +} + +func TestWriteConfig(t *testing.T) { + // use the default configurations + for _, tc := range []Config{ + Config16V400mA, + Config32V2A, + Config32V1A, + } { + n := fmt.Sprintf("%x/%x", tc.RegisterValue(), tc.Calibration.RegisterValue()) + t.Run(n, func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + bus.AddDevice(fake) + fake.Registers = map[uint8]uint16{ + RegConfig: 0, + RegCalibration: 0, + } + + dev := New(bus) + dev.config = tc + err := dev.Configure() + c.Assert(err, qt.IsNil) + c.Assert(fake.Registers[RegConfig], qt.Equals, tc.RegisterValue()) + c.Assert(fake.Registers[RegCalibration], qt.Equals, tc.Calibration.RegisterValue()) + }) + } +} + +func TestSetConfig(t *testing.T) { + for _, tc := range []Config{ + Config16V400mA, + Config32V2A, + Config32V1A, + } { + n := fmt.Sprintf("%x/%x", tc.RegisterValue(), tc.Calibration.RegisterValue()) + t.Run(n, func(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + dev := New(bus) + dev.SetConfig(tc) + c.Assert(dev.config, qt.Equals, tc) + }) + } +} + +func TestTrigger(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + bus.AddDevice(fake) + fake.Registers = map[uint8]uint16{ + RegConfig: Config32V2A.RegisterValue(), + } + + dev := New(bus) + dev.config = Config32V2A + dev.config.Mode = ModeTrigBus + err := dev.Trigger() + c.Assert(err, qt.IsNil) + c.Assert(fake.Registers[RegConfig], qt.Equals, dev.config.RegisterValue()) +} + +func TestMeasurements(t *testing.T) { + bvVal := int16(4200) + svVal := int16(1234) + iVal := float32(420) + pVal := float32(420) + + c := qt.New(t) + bus := tester.NewI2CBus(c) + fake := tester.NewI2CDevice16(c, Address) + bus.AddDevice(fake) + fake.Registers = map[uint8]uint16{ + RegBusVoltage: uint16(((4200 << 3) / 4) | (1 << 1)), + RegShuntVoltage: uint16(svVal), + RegCurrent: uint16(iVal * Config16V400mA.CurrentDivider), + RegPower: uint16(pVal / Config16V400mA.PowerMultiplier), + } + + dev := New(bus) + dev.config = Config16V400mA + bv, sv, i, p, err := dev.Measurements() + c.Assert(err, qt.IsNil) + c.Assert(bv, qt.Equals, bvVal) + c.Assert(sv, qt.Equals, svVal) + c.Assert(i, qt.Equals, iVal) + c.Assert(p, qt.Equals, pVal) + +} diff --git a/ina219/registers.go b/ina219/registers.go new file mode 100644 index 0000000..f1bae3a --- /dev/null +++ b/ina219/registers.go @@ -0,0 +1,13 @@ +package ina219 + +// The default I2C address for this device. +const Address = 0x40 + +const ( + RegConfig uint8 = 0x0 + RegShuntVoltage uint8 = 0x1 + RegBusVoltage uint8 = 0x2 + RegPower uint8 = 0x3 + RegCurrent uint8 = 0x4 + RegCalibration uint8 = 0x5 +) diff --git a/internal/legacy/pinconfig.go b/internal/legacy/pinconfig.go new file mode 100644 index 0000000..d9d805f --- /dev/null +++ b/internal/legacy/pinconfig.go @@ -0,0 +1,62 @@ +package legacy + +import ( + "errors" + + "tinygo.org/x/drivers/internal/pin" +) + +// The pingconfig group of files serve to abstract away +// pin configuration calls on the machine.Pin type. +// It was observed this way of developing drivers was +// non-portable and unusable on "big" Go projects so +// future projects should NOT configure pins in driver code. +// Users must configure pins before passing them as arguments +// to drivers. + +// ConfigurePinOut is a legacy function used to configure pins as outputs. +// +// Deprecated: Do not configure pins in drivers. +// This is a legacy feature and should only be used by drivers that +// previously configured pins in initialization to avoid breaking users. +func ConfigurePinOut(po pin.Output) { + configurePinOut(po) +} + +// ConfigurePinInput is a legacy function used to configure pins as inputs. +// +// Deprecated: Do not configure pins in drivers. +// This is a legacy feature and should only be used by drivers that +// previously configured pins in initialization to avoid breaking users. +func ConfigurePinInputPulldown(pi pin.Input) { + configurePinInputPulldown(pi) +} + +// ConfigurePinInput is a legacy function used to configure pins as inputs. +// +// Deprecated: Do not configure pins in drivers. +// This is a legacy feature and should only be used by drivers that +// previously configured pins in initialization to avoid breaking users. +func ConfigurePinInput(pi pin.Input) { + configurePinInput(pi) +} + +// ConfigurePinInput is a legacy function used to configure pins as inputs. +// +// Deprecated: Do not configure pins in drivers. +// This is a legacy feature and should only be used by drivers that +// previously configured pins in initialization to avoid breaking users. +func ConfigurePinInputPullup(pi pin.Input) { + configurePinInputPullup(pi) +} + +// PinIsNoPin returns true if the argument is a machine.Pin type and is the machine.NoPin predeclared type. +// +// Deprecated: Drivers do not require pin knowledge from now on. +func PinIsNoPin(pin any) bool { + return pinIsNoPin(pin) +} + +var ( + ErrConfigBeforeInstantiated = errors.New("device must be instantiated with New before calling Configure method") +) diff --git a/internal/legacy/pinconfig_go.go b/internal/legacy/pinconfig_go.go new file mode 100644 index 0000000..3a7096b --- /dev/null +++ b/internal/legacy/pinconfig_go.go @@ -0,0 +1,15 @@ +//go:build !tinygo + +package legacy + +import "tinygo.org/x/drivers/internal/pin" + +// This file compiles for non-tinygo builds +// for use with "big" or "upstream" Go where +// there is no machine package. + +func configurePinOut(p pin.Output) {} +func configurePinInput(p pin.Input) {} +func configurePinInputPulldown(p pin.Input) {} +func configurePinInputPullup(p pin.Input) {} +func pinIsNoPin(a any) bool { return false } diff --git a/internal/legacy/pinconfig_nopulls.go b/internal/legacy/pinconfig_nopulls.go new file mode 100644 index 0000000..1fc6138 --- /dev/null +++ b/internal/legacy/pinconfig_nopulls.go @@ -0,0 +1,10 @@ +//go:build baremetal && fe310 + +package legacy + +import "machine" + +const ( + pulldown = machine.PinInput + pullup = machine.PinInput +) diff --git a/internal/legacy/pinconfig_pulls.go b/internal/legacy/pinconfig_pulls.go new file mode 100644 index 0000000..d7e88c6 --- /dev/null +++ b/internal/legacy/pinconfig_pulls.go @@ -0,0 +1,13 @@ +//go:build baremetal && !fe310 + +package legacy + +import "machine" + +// If you are getting a build error here you then we missed adding +// your CPU build tag to the list of CPUs that do not have pulldown/pullups. +// Add it above and in pinhal_nopulls! You should also add a smoketest for it :) +const ( + pulldown = machine.PinInputPulldown + pullup = machine.PinInputPullup +) diff --git a/internal/legacy/pinconfig_tinygo.go b/internal/legacy/pinconfig_tinygo.go new file mode 100644 index 0000000..d0068b2 --- /dev/null +++ b/internal/legacy/pinconfig_tinygo.go @@ -0,0 +1,37 @@ +//go:build baremetal + +package legacy + +import ( + "machine" + + "tinygo.org/x/drivers/internal/pin" +) + +func configurePinOut(po pin.Output) { + configurePin(po, machine.PinOutput) +} + +func configurePinInputPulldown(pi pin.Input) { + configurePin(pi, pulldown) // some chips do not have pull down, in which case pulldown==machine.PinInput. +} + +func configurePinInput(pi pin.Input) { + configurePin(pi, machine.PinInput) +} + +func configurePinInputPullup(pi pin.Input) { + configurePin(pi, pullup) // some chips do not have pull up, in which case pullup==machine.PinInput. +} + +func pinIsNoPin(a any) bool { + p, ok := a.(machine.Pin) + return ok && p == machine.NoPin +} + +func configurePin(p any, mode machine.PinMode) { + machinePin, ok := p.(machine.Pin) + if ok { + machinePin.Configure(machine.PinConfig{Mode: mode}) + } +} diff --git a/internal/pin/internalpin.go b/internal/pin/internalpin.go new file mode 100644 index 0000000..6879f34 --- /dev/null +++ b/internal/pin/internalpin.go @@ -0,0 +1,72 @@ +// package pin implements a TinyGo Pin HAL. +// It serves to eliminate machine.Pin from driver constructors +// so that drivers can be used in "big" Go projects where +// there is no machine package. +// This file contains both function and interface-style Pin HAL definitions. +package pin + +// OutputFunc is hardware abstraction for a pin which outputs a +// digital signal (high or low level). +// +// // Code conversion demo: from machine.Pin to pin.OutputFunc +// led := machine.LED +// led.Configure(machine.PinConfig{Mode: machine.Output}) +// var pin pin.OutputFunc = led.Set // Going from a machine.Pin to a pin.OutputFunc +// +// This is an alternative to [Output] which is an interface type. +type OutputFunc func(level bool) + +// High sets the underlying pin's level to high. This is equivalent to calling PinOutput(true). +func (setPin OutputFunc) High() { + setPin(true) +} + +// Low sets the underlying pin's level to low. This is equivalent to calling PinOutput(false). +func (setPin OutputFunc) Low() { + setPin(false) +} + +// InputFunc is hardware abstraction for a pin which receives a +// digital signal and reads it (high or low level). +// +// // Code conversion demo: from machine.Pin to pin.InputFunc +// input := machine.LED +// input.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) // or use machine.PinInputPullup or machine.Input +// var pin pin.InputFunc = input.Get // Going from a machine.Pin to a pin.InputFunc +// +// This is an alternative to [Input] which is an interface type. +type InputFunc func() (level bool) + +// // Below is an example on how to define a input/output pin HAL for a +// // pin that must switch between input and output mode: +// +// var pinIsOutput bool +// var po PinOutputFunc = func(b bool) { +// if !pinIsOutput { +// pin.Configure(outputMode) +// pinIsOutput = true +// } +// pin.Set(b) +// } +// +// var pi PinInputFunc = func() bool { +// if pinIsOutput { +// pin.Configure(inputMode) +// pinIsOutput = false +// } +// return pin.Get() +// } + +// Output interface represents a pin hardware abstraction layer for a pin that can output a digital signal. +// +// This is an alternative to [OutputFunc] abstraction which is a function type. +type Output interface { + Set(level bool) +} + +// Input interface represents a pin hardware abstraction layer for a pin that can read a digital signal. +// +// This is an alternative to [InputFunc] abstraction which is a function type. +type Input interface { + Get() (level bool) +} diff --git a/internal/regmap/device8.go b/internal/regmap/device8.go new file mode 100644 index 0000000..b62aec1 --- /dev/null +++ b/internal/regmap/device8.go @@ -0,0 +1,143 @@ +package regmap + +import ( + "encoding/binary" + "io" + + "tinygo.org/x/drivers" +) + +// Device8 implements common logic to most 8-bit peripherals with an I2C or SPI bus. +// All methods expect the target to support conventional register read and write operations +// where the first byte sent is the register address being accessed. +// +// All methods use an internal buffer and perform no dynamic memory allocation. +type Device8 struct { + buf [10]byte +} + +// clear zeroes Device8's buffers. +func (d *Device8) clear() { + d.buf = [10]byte{} +} + +// I2C methods. + +// Read8I2C reads a single byte from register addr of the device at i2cAddr using the provided I2C bus. +func (d *Device8) Read8I2C(bus drivers.I2C, i2cAddr uint16, addr uint8) (byte, error) { + d.buf[0] = addr + err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:2]) + return d.buf[1], err +} + +// Read16I2C reads a 16-bit value from register addr of the device at i2cAddr using the provided I2C bus. +// The byte order is specified by order. +func (d *Device8) Read16I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, order binary.ByteOrder) (uint16, error) { + d.buf[0] = addr + err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:3]) + return order.Uint16(d.buf[1:3]), err +} + +// Read32I2C reads a 32-bit value from register addr of the device at i2cAddr using the provided I2C bus. +// The byte order is specified by order. +func (d *Device8) Read32I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, order binary.ByteOrder) (uint32, error) { + d.buf[0] = addr + err := bus.Tx(i2cAddr, d.buf[0:1], d.buf[1:5]) + return order.Uint32(d.buf[1:5]), err +} + +// ReadDataI2C reads dataLength bytes from register addr of the device at i2cAddr using the provided I2C bus. +// The data is stored in dataDestination. +func (d *Device8) ReadDataI2C(bus drivers.I2C, i2cAddr uint16, addr uint8, dataDestination []byte) error { + d.buf[0] = addr + return bus.Tx(i2cAddr, d.buf[:1], dataDestination) +} + +// Write8I2C writes a single byte value to register addr of the device at i2cAddr using the provided I2C bus. +func (d *Device8) Write8I2C(bus drivers.I2C, i2cAddr uint16, addr, value uint8) error { + d.buf[0] = addr + d.buf[1] = value + return bus.Tx(i2cAddr, d.buf[:2], nil) +} + +// Write16I2C writes a 16-bit value to register addr of the device at i2cAddr using the provided I2C bus. +// The byte order is specified by order. +func (d *Device8) Write16I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, value uint16, order binary.ByteOrder) error { + d.buf[0] = addr + order.PutUint16(d.buf[1:3], value) + return bus.Tx(i2cAddr, d.buf[0:3], nil) +} + +// Write32I2C writes a 32-bit value to register addr of the device at i2cAddr using the provided I2C bus. +// The byte order is specified by order. +func (d *Device8) Write32I2C(bus drivers.I2C, i2cAddr uint16, addr uint8, value uint32, order binary.ByteOrder) error { + d.buf[0] = addr + order.PutUint32(d.buf[1:5], value) + return bus.Tx(i2cAddr, d.buf[0:5], nil) +} + +// SPI methods. + +// Read8SPI reads a single byte from register addr using the provided SPI bus. +func (d *Device8) Read8SPI(bus drivers.SPI, addr uint8) (byte, error) { + d.clear() + d.buf[0] = addr + err := bus.Tx(d.buf[0:1], d.buf[1:2]) // We suppose data is returned after first byte in SPI. + return d.buf[1], err +} + +// Read16SPI reads a 16-bit value from register addr using the provided SPI bus. The byte order is specified by order. +func (d *Device8) Read16SPI(bus drivers.SPI, addr uint8, order binary.ByteOrder) (uint16, error) { + d.clear() + d.buf[0] = addr + err := bus.Tx(d.buf[0:3], d.buf[3:6]) // We suppose data is returned after first byte in SPI. + return order.Uint16(d.buf[4:6]), err +} + +// Read32SPI reads a 32-bit value from register addr using the provided SPI bus. The byte order is specified by order. +func (d *Device8) Read32SPI(bus drivers.SPI, addr uint8, order binary.ByteOrder) (uint32, error) { + d.clear() + d.buf[0] = addr + err := bus.Tx(d.buf[0:5], d.buf[5:10]) // We suppose data is returned after first byte in SPI. + return order.Uint32(d.buf[6:10]), err +} + +// ReadDataSPI reads data from a 8bit device address. It assumes data at register address is sent back +// from device after first byte is written as address. +// It needs the auxiliary buffer length to be large enough to contain both the write and read portions of buffer, +// so 2*(dataLength+1) < len(auxiliaryBuf) must hold. +func (d *Device8) ReadDataSPI(bus drivers.SPI, addr uint8, dataLength int, auxiliaryBuf []byte) ([]byte, error) { + split := len(auxiliaryBuf) / 2 + if split < dataLength+1 { + return nil, io.ErrShortBuffer + } + + wbuf, rbuf := auxiliaryBuf[:split], auxiliaryBuf[split:] + wbuf[0] = addr + err := bus.Tx(wbuf, rbuf) + return rbuf[1:], err +} + +// Write8SPI writes a single byte value to register addr using the provided SPI bus. +func (d *Device8) Write8SPI(bus drivers.SPI, addr, value uint8) error { + d.clear() + d.buf[0] = addr + d.buf[1] = value + return bus.Tx(d.buf[:2], nil) +} + +// Write16SPI writes a 16-bit value to register addr using the provided SPI bus. The byte order is specified by order. +func (d *Device8) Write16SPI(bus drivers.SPI, addr uint8, value uint16, order binary.ByteOrder) error { + d.clear() + d.buf[0] = addr + order.PutUint16(d.buf[1:3], value) + return bus.Tx(d.buf[:3], nil) +} + +// Write32SPI writes a 32-bit value to register addr using the provided SPI bus. The byte order is specified by order. +func (d *Device8) Write32SPI(bus drivers.SPI, addr uint8, value uint32, order binary.ByteOrder) error { + d.clear() + d.buf[0] = addr + order.PutUint32(d.buf[1:5], value) + return bus.Tx(d.buf[:5], nil) +} diff --git a/internal/regmap/i2c-spi.go b/internal/regmap/i2c-spi.go new file mode 100644 index 0000000..0e0abe7 --- /dev/null +++ b/internal/regmap/i2c-spi.go @@ -0,0 +1,123 @@ +package regmap + +import ( + "encoding/binary" + + "tinygo.org/x/drivers" +) + +// Device8SPI implements common logic to most 8-bit peripherals with an SPI bus. +// All methods expect the target to support conventional register read and write operations +// where the first byte sent is the register address being accessed. +// +// All methods use an internal buffer and perform no dynamic memory allocation. +type Device8SPI struct { + bus drivers.SPI + order binary.ByteOrder + d Device8 +} + +// SetBus sets the SPI bus and byte order for the Device8SPI. +// +// As a hint, most SPI devices use big-endian (MSB) byte order. +// - Big endian: A value of 0x1234 is transmitted as 0x12 followed by 0x34. +// - Little endian: A value of 0x1234 is transmitted as 0x34 followed by 0x12. +func (d *Device8SPI) SetBus(bus drivers.SPI, order binary.ByteOrder) { + d.bus = bus + d.order = order +} + +// Read8 reads a single byte from register addr. +func (d *Device8SPI) Read8(addr uint8) (byte, error) { + return d.d.Read8SPI(d.bus, addr) +} + +// Read16 reads a 16-bit value from register addr. +func (d *Device8SPI) Read16(addr uint8) (uint16, error) { + return d.d.Read16SPI(d.bus, addr, d.order) +} + +// Read32 reads a 32-bit value from register addr. +func (d *Device8SPI) Read32(addr uint8) (uint32, error) { + return d.d.Read32SPI(d.bus, addr, d.order) +} + +// ReadData reads dataLength bytes from register addr. Due to the internal functioning of +// SPI, an auxiliary buffer must be provided to perform the operation and avoid memory allocation. +// The returned slice is a subslice of auxBuffer containing the read data. +func (d *Device8SPI) ReadData(addr uint8, datalength int, auxBuffer []byte) ([]byte, error) { + return d.d.ReadDataSPI(d.bus, addr, datalength, auxBuffer) +} + +// Write8 writes a single byte value to register addr. +func (d *Device8SPI) Write8(addr, value uint8) error { + return d.d.Write8SPI(d.bus, addr, value) +} + +// Write16 writes a 16-bit value to register addr. +func (d *Device8SPI) Write16(addr uint8, value uint16) error { + return d.d.Write16SPI(d.bus, addr, value, d.order) +} + +// Write32 writes a 32-bit value to register addr. +func (d *Device8SPI) Write32(addr uint8, value uint32) error { + return d.d.Write32SPI(d.bus, addr, value, d.order) +} + +// Device8I2C implements common logic to most 8-bit peripherals with an I2C bus. +// All methods expect the target to support conventional register read and write operations +// where the first byte sent is the register address being accessed. +// +// All methods use an internal buffer and perform no dynamic memory allocation. +type Device8I2C struct { + bus drivers.I2C + i2cAddr uint16 + order binary.ByteOrder + d Device8 +} + +// SetBus sets the I2C bus, device address, and byte order for the Device8I2C. +// +// As a hint, most I2C devices use big-endian (MSB) byte order. +// - Big endian: A value of 0x1234 is transmitted as 0x12 followed by 0x34. +// - Little endian: A value of 0x1234 is transmitted as 0x34 followed by 0x12. +func (d *Device8I2C) SetBus(bus drivers.I2C, i2cAddr uint16, order binary.ByteOrder) { + d.bus = bus + d.i2cAddr = i2cAddr + d.order = order +} + +// Read8 reads a single byte from register addr. +func (d *Device8I2C) Read8(addr uint8) (byte, error) { + return d.d.Read8I2C(d.bus, d.i2cAddr, addr) +} + +// Read16 reads a 16-bit value from register addr. +func (d *Device8I2C) Read16(addr uint8) (uint16, error) { + return d.d.Read16I2C(d.bus, d.i2cAddr, addr, d.order) +} + +// Read32 reads a 32-bit value from register addr. +func (d *Device8I2C) Read32(addr uint8) (uint32, error) { + return d.d.Read32I2C(d.bus, d.i2cAddr, addr, d.order) +} + +// ReadData reads dataLength bytes from register addr. +func (d *Device8I2C) ReadData(addr uint8, dataDestination []byte) error { + return d.d.ReadDataI2C(d.bus, d.i2cAddr, addr, dataDestination) +} + +// Write8 writes a single byte value to register addr. +func (d *Device8I2C) Write8(addr, value uint8) error { + return d.d.Write8I2C(d.bus, d.i2cAddr, addr, value) +} + +// Write16 writes a 16-bit value to register addr. +func (d *Device8I2C) Write16(addr uint8, value uint16) error { + return d.d.Write16I2C(d.bus, d.i2cAddr, addr, value, d.order) +} + +// Write32 writes a 32-bit value to register addr. +func (d *Device8I2C) Write32(addr uint8, value uint32) error { + return d.d.Write32I2C(d.bus, d.i2cAddr, addr, value, d.order) +} diff --git a/lis3dh/lis3dh.go b/lis3dh/lis3dh.go index 67f3200..0151e08 100644 --- a/lis3dh/lis3dh.go +++ b/lis3dh/lis3dh.go @@ -11,37 +11,57 @@ import ( // Device wraps an I2C connection to a LIS3DH device. type Device struct { bus drivers.I2C - Address uint16 + address uint16 r Range + accel [6]byte // stored acceleration data (from the Update call) +} + +// Driver configuration, used for the Configure call. All fields are optional. +type Config struct { + Address uint16 } // New creates a new LIS3DH connection. The I2C bus must already be configured. // // This function only creates the Device object, it does not touch the device. func New(bus drivers.I2C) Device { - return Device{bus: bus, Address: Address0} + return Device{bus: bus, address: Address0} } // Configure sets up the device for communication -func (d *Device) Configure() { +func (d *Device) Configure(config Config) error { + if config.Address != 0 { + d.address = config.Address + } + // enable all axes, normal mode - legacy.WriteRegister(d.bus, uint8(d.Address), REG_CTRL1, []byte{0x07}) + err := legacy.WriteRegister(d.bus, uint8(d.address), REG_CTRL1, []byte{0x07}) + if err != nil { + return err + } // 400Hz rate - d.SetDataRate(DATARATE_400_HZ) + err = d.SetDataRate(DATARATE_400_HZ) + if err != nil { + return err + } // High res & BDU enabled - legacy.WriteRegister(d.bus, uint8(d.Address), REG_CTRL4, []byte{0x88}) + err = legacy.WriteRegister(d.bus, uint8(d.address), REG_CTRL4, []byte{0x88}) + if err != nil { + return err + } // get current range - d.r = d.ReadRange() + d.r, err = d.ReadRange() + return err } // Connected returns whether a LIS3DH has been found. // It does a "who am I" request and checks the response. func (d *Device) Connected() bool { data := []byte{0} - err := legacy.ReadRegister(d.bus, uint8(d.Address), WHO_AM_I, data) + err := legacy.ReadRegister(d.bus, uint8(d.address), WHO_AM_I, data) if err != nil { return false } @@ -49,46 +69,51 @@ func (d *Device) Connected() bool { } // SetDataRate sets the speed of data collected by the LIS3DH. -func (d *Device) SetDataRate(rate DataRate) { +func (d *Device) SetDataRate(rate DataRate) error { ctl1 := []byte{0} - err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CTRL1, ctl1) + err := legacy.ReadRegister(d.bus, uint8(d.address), REG_CTRL1, ctl1) if err != nil { - println(err.Error()) + return err } // mask off bits ctl1[0] &^= 0xf0 ctl1[0] |= (byte(rate) << 4) - legacy.WriteRegister(d.bus, uint8(d.Address), REG_CTRL1, ctl1) + return legacy.WriteRegister(d.bus, uint8(d.address), REG_CTRL1, ctl1) } // SetRange sets the G range for LIS3DH. -func (d *Device) SetRange(r Range) { +func (d *Device) SetRange(r Range) error { ctl := []byte{0} - err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CTRL4, ctl) + err := legacy.ReadRegister(d.bus, uint8(d.address), REG_CTRL4, ctl) if err != nil { - println(err.Error()) + return err } // mask off bits ctl[0] &^= 0x30 ctl[0] |= (byte(r) << 4) - legacy.WriteRegister(d.bus, uint8(d.Address), REG_CTRL4, ctl) + err = legacy.WriteRegister(d.bus, uint8(d.address), REG_CTRL4, ctl) + if err != nil { + return err + } // store the new range d.r = r + + return nil } // ReadRange returns the current G range for LIS3DH. -func (d *Device) ReadRange() (r Range) { +func (d *Device) ReadRange() (r Range, err error) { ctl := []byte{0} - err := legacy.ReadRegister(d.bus, uint8(d.Address), REG_CTRL4, ctl) + err = legacy.ReadRegister(d.bus, uint8(d.address), REG_CTRL4, ctl) if err != nil { - println(err.Error()) + return 0, err } // mask off bits r = Range(ctl[0] >> 4) r &= 0x03 - return r + return r, nil } // ReadAcceleration reads the current acceleration from the device and returns @@ -96,28 +121,17 @@ func (d *Device) ReadRange() (r Range) { // and the sensor is not moving the returned value will be around 1000000 or // -1000000. func (d *Device) ReadAcceleration() (int32, int32, int32, error) { - x, y, z := d.ReadRawAcceleration() - divider := float32(1) - switch d.r { - case RANGE_16_G: - divider = 1365 - case RANGE_8_G: - divider = 4096 - case RANGE_4_G: - divider = 8190 - case RANGE_2_G: - divider = 16380 - } - - return int32(float32(x) / divider * 1000000), int32(float32(y) / divider * 1000000), int32(float32(z) / divider * 1000000), nil + rawX, rawY, rawZ := d.ReadRawAcceleration() + x, y, z := normalizeRange(rawX, rawY, rawZ, d.r) + return x, y, z, nil } // ReadRawAcceleration returns the raw x, y and z axis from the LIS3DH func (d *Device) ReadRawAcceleration() (x int16, y int16, z int16) { - legacy.WriteRegister(d.bus, uint8(d.Address), REG_OUT_X_L|0x80, nil) + legacy.WriteRegister(d.bus, uint8(d.address), REG_OUT_X_L|0x80, nil) data := []byte{0, 0, 0, 0, 0, 0} - d.bus.Tx(d.Address, nil, data) + d.bus.Tx(d.address, nil, data) x = int16((uint16(data[1]) << 8) | uint16(data[0])) y = int16((uint16(data[3]) << 8) | uint16(data[2])) @@ -125,3 +139,68 @@ func (d *Device) ReadRawAcceleration() (x int16, y int16, z int16) { return } + +// Update the sensor values of the 'which' parameter. Only acceleration is +// supported at the moment. +func (d *Device) Update(which drivers.Measurement) error { + if which&drivers.Acceleration != 0 { + // Read raw acceleration values and store them in the driver. + err := legacy.WriteRegister(d.bus, uint8(d.address), REG_OUT_X_L|0x80, nil) + if err != nil { + return err + } + err = d.bus.Tx(d.address, nil, d.accel[:]) + if err != nil { + return err + } + } + return nil +} + +// Acceleration returns the last read acceleration in µg (micro-gravity). +// When one of the axes is pointing straight to Earth and the sensor is not +// moving the returned value will be around 1000000 or -1000000. +func (d *Device) Acceleration() (x, y, z int32) { + // Extract the raw 16-bit values. + rawX := int16((uint16(d.accel[1]) << 8) | uint16(d.accel[0])) + rawY := int16((uint16(d.accel[3]) << 8) | uint16(d.accel[2])) + rawZ := int16((uint16(d.accel[5]) << 8) | uint16(d.accel[4])) + + // Normalize these values, to be in µg (micro-gravity). + return normalizeRange(rawX, rawY, rawZ, d.r) +} + +// Convert raw 16-bit values to normalized 32-bit values while avoiding floats +// and divisions. +func normalizeRange(rawX, rawY, rawZ int16, r Range) (x, y, z int32) { + // We're going to convert the 16-bit raw values to values in the range + // -1000_000..1000_000. For now we're going to assume a range of 16G, we'll + // adjust that range later. + // The formula is derived as follows, and carefully selected to avoid + // overflow and integer divisions (the division will be optimized to a + // bitshift): + // x = x * 1000_000 / 2048 + // x = x * (1000_000/64) / (2048/64) + // x = x * 15625 / 32 + x = int32(rawX) * 15625 / 32 + y = int32(rawY) * 15625 / 32 + z = int32(rawZ) * 15625 / 32 + + // Now we need to normalize the three values, since we assumed 16G before. + shift := uint32(0) + switch r { + case RANGE_16_G: + shift = 0 + case RANGE_8_G: + shift = 1 + case RANGE_4_G: + shift = 2 + case RANGE_2_G: + shift = 3 + } + x >>= shift + y >>= shift + z >>= shift + + return +} diff --git a/lora/config.go b/lora/config.go index dd61dc1..21cbe29 100644 --- a/lora/config.go +++ b/lora/config.go @@ -60,16 +60,20 @@ const ( ) const ( - Bandwidth_7_8 = iota // 7.8 kHz - Bandwidth_10_4 // 10.4 kHz - Bandwidth_15_6 // 15.6 kHz - Bandwidth_20_8 // 20.8 kHz - Bandwidth_31_25 // 31.25 kHz - Bandwidth_41_7 // 41.7 kHz - Bandwidth_62_5 // 62.5 kHz - Bandwidth_125_0 // 125.0 kHz - Bandwidth_250_0 // 250.0 kHz - Bandwidth_500_0 // 500.0 kHz + Bandwidth_7_8 = iota // 7.8 kHz + Bandwidth_10_4 // 10.4 kHz + Bandwidth_15_6 // 15.6 kHz + Bandwidth_20_8 // 20.8 kHz + Bandwidth_31_25 // 31.25 kHz + Bandwidth_41_7 // 41.7 kHz + Bandwidth_62_5 // 62.5 kHz + Bandwidth_125_0 // 125.0 kHz + Bandwidth_203_125 // 203.125 kHz + Bandwidth_250_0 // 250.0 kHz + Bandwidth_406_25 // 406.25 kHz + Bandwidth_500_0 // 500.0 kHz + Bandwidth_812_5 // 812.5 kHz + Bandwidth_1625_0 // 1625 kHz ) const ( diff --git a/lora/lorawan/region/eu868.go b/lora/lorawan/region/eu868.go index 7eed1f4..c314d38 100644 --- a/lora/lorawan/region/eu868.go +++ b/lora/lorawan/region/eu868.go @@ -3,8 +3,8 @@ package region import "tinygo.org/x/drivers/lora" const ( - EU868_DEFAULT_PREAMBLE_LEN = 8 - EU868_DEFAULT_TX_POWER_DBM = 20 + EU868_DEFAULT_PREAMBLE_LEN = 8 // page 103 RP002-1.0.5 + EU868_DEFAULT_TX_POWER_DBM = 16 // page 36 RP002-1.0.5, 16 is the max ) type ChannelEU struct { diff --git a/lora/radio_event.go b/lora/radio_event.go index 7cb6426..ece8270 100644 --- a/lora/radio_event.go +++ b/lora/radio_event.go @@ -6,6 +6,9 @@ const ( RadioEventTimeout RadioEventWatchdog RadioEventCrcError + RadioEventValidHeader + RadioEventCadDone + RadioEventCadDetected RadioEventUnhandled ) diff --git a/lsm303agr/lsm303agr.go b/lsm303agr/lsm303agr.go index 35a4c92..f6848eb 100644 --- a/lsm303agr/lsm303agr.go +++ b/lsm303agr/lsm303agr.go @@ -36,7 +36,7 @@ type Configuration struct { MagDataRate uint8 } -var errNotConnected = errors.New("lsm303agr: failed to communicate with either acel or magnet sensor") +var errNotConnected = errors.New("lsm303agr: failed to communicate with either accel or magnet sensor") // New creates a new LSM303AGR connection. The I2C bus must already be configured. // diff --git a/lsm303dlhc/lsm303dlhc.go b/lsm303dlhc/lsm303dlhc.go new file mode 100644 index 0000000..6e0c2fa --- /dev/null +++ b/lsm303dlhc/lsm303dlhc.go @@ -0,0 +1,214 @@ +// Package lsm303dlhc implements a driver for the LSM303dlhc, +// a 3 axis accelerometer/magnetic sensor typically available on breakout boards. +// +// Datasheet: https://www.st.com/resource/en/datasheet/lsm303dlhc.pdf + +package lsm303dlhc // import "tinygo.org/x/drivers/lsm303dlhc" + +import ( + "math" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" +) + +// Device wraps an I2C connection to a LSM303dlhc device. +type Device struct { + bus drivers.I2C + AccelAddress uint8 + MagAddress uint8 + AccelPowerMode uint8 + AccelRange uint8 + AccelDataRate uint8 + MagPowerMode uint8 + MagSystemMode uint8 + MagDataRate uint8 + buf [6]uint8 +} + +// Configuration for LSM303dlhc device. +type Configuration struct { + AccelPowerMode uint8 + AccelRange uint8 + AccelDataRate uint8 + MagPowerMode uint8 + MagSystemMode uint8 + MagDataRate uint8 +} + +// New creates a new LSM303DLHC connection. The I2C bus must already be configured. +// This function only creates the Device object, it does not touch the device. +func New(bus drivers.I2C) *Device { + return &Device{ + bus: bus, + AccelAddress: ACCEL_ADDRESS, + MagAddress: MAG_ADDRESS, + } +} + +// Configure sets up the LSM303dlhc device for communication. +func (d *Device) Configure(cfg Configuration) (err error) { + + if cfg.AccelDataRate != 0 { + d.AccelDataRate = cfg.AccelDataRate + } else { + d.AccelDataRate = ACCEL_DATARATE_100HZ + } + + if cfg.AccelPowerMode != 0 { + d.AccelPowerMode = cfg.AccelPowerMode + } else { + d.AccelPowerMode = ACCEL_POWER_NORMAL + } + + if cfg.AccelRange != 0 { + d.AccelRange = cfg.AccelRange + } else { + d.AccelRange = ACCEL_RANGE_2G + } + + if cfg.MagPowerMode != 0 { + d.MagPowerMode = cfg.MagPowerMode + } else { + d.MagPowerMode = MAG_POWER_NORMAL + } + + if cfg.MagDataRate != 0 { + d.MagDataRate = cfg.MagDataRate + } else { + d.MagDataRate = MAG_DATARATE_10HZ + } + + if cfg.MagSystemMode != 0 { + d.MagSystemMode = cfg.MagSystemMode + } else { + d.MagSystemMode = MAG_SYSTEM_CONTINUOUS + } + + data := d.buf[:1] + + data[0] = byte(d.AccelDataRate<<4 | d.AccelPowerMode | 0x07) + err = legacy.WriteRegister(d.bus, uint8(d.AccelAddress), ACCEL_CTRL_REG1_A, data) + if err != nil { + return + } + + data[0] = byte(0x80 | d.AccelRange<<4) + err = legacy.WriteRegister(d.bus, uint8(d.AccelAddress), ACCEL_CTRL_REG4_A, data) + if err != nil { + return + } + + data[0] = byte(0xC0) + err = legacy.WriteRegister(d.bus, uint8(d.AccelAddress), CRA_REG_M, data) + if err != nil { + return + } + + // Temperature compensation is on for magnetic sensor + data[0] = byte(0x80 | d.MagPowerMode<<4 | d.MagDataRate<<2 | d.MagSystemMode) + err = legacy.WriteRegister(d.bus, uint8(d.MagAddress), MAG_MR_REG_M, data) + if err != nil { + return + } + + return nil +} + +// ReadAcceleration reads the current acceleration from the device and returns +// it in µg (micro-gravity). When one of the axes is pointing straight to Earth +// and the sensor is not moving the returned value will be around 1000000 or +// -1000000. +func (d *Device) ReadAcceleration() (x, y, z int32, err error) { + data := d.buf[:6] + err = legacy.ReadRegister(d.bus, uint8(d.AccelAddress), ACCEL_OUT_AUTO_INC, data) + if err != nil { + return + } + + rangeFactor := int16(0) + switch d.AccelRange { + case ACCEL_RANGE_2G: + rangeFactor = 1 + case ACCEL_RANGE_4G: + rangeFactor = 2 + case ACCEL_RANGE_8G: + rangeFactor = 4 + case ACCEL_RANGE_16G: + rangeFactor = 12 // the readings in 16G are a bit lower + } + + x = int32(int32(int16((uint16(data[1])<<8|uint16(data[0])))>>4*rangeFactor) * 1000000 / 1024) + y = int32(int32(int16((uint16(data[3])<<8|uint16(data[2])))>>4*rangeFactor) * 1000000 / 1024) + z = int32(int32(int16((uint16(data[5])<<8|uint16(data[4])))>>4*rangeFactor) * 1000000 / 1024) + return +} + +// ReadPitchRoll reads the current pitch and roll angles from the device and +// returns it in micro-degrees. When the z axis is pointing straight to Earth +// the returned values of pitch and roll would be zero. +func (d *Device) ReadPitchRoll() (pitch, roll int32, err error) { + + x, y, z, err := d.ReadAcceleration() + if err != nil { + return + } + xf, yf, zf := float64(x), float64(y), float64(z) + pitch = int32((math.Round(math.Atan2(yf, math.Sqrt(math.Pow(xf, 2)+math.Pow(zf, 2)))*(180/math.Pi)*100) / 100) * 1000000) + roll = int32((math.Round(math.Atan2(xf, math.Sqrt(math.Pow(yf, 2)+math.Pow(zf, 2)))*(180/math.Pi)*100) / 100) * 1000000) + return + +} + +// ReadMagneticField reads the current magnetic field from the device and returns +// it in mG (milligauss). 1 mG = 0.1 µT (microtesla). +func (d *Device) ReadMagneticField() (x, y, z int32, err error) { + + if d.MagSystemMode == MAG_SYSTEM_SINGLE { + cmd := d.buf[:1] + cmd[0] = byte(0x80 | d.MagPowerMode<<4 | d.MagDataRate<<2 | d.MagSystemMode) + err = legacy.WriteRegister(d.bus, uint8(d.MagAddress), MAG_MR_REG_M, cmd) + if err != nil { + return + } + } + + data := d.buf[0:6] + legacy.ReadRegister(d.bus, uint8(d.MagAddress), MAG_OUT_AUTO_INC, data) + + x = int32(int16((uint16(data[1])<<8 | uint16(data[0])))) + y = int32(int16((uint16(data[3])<<8 | uint16(data[2])))) + z = int32(int16((uint16(data[5])<<8 | uint16(data[4])))) + return +} + +// ReadCompass reads the current compass heading from the device and returns +// it in micro-degrees. When the z axis is pointing straight to Earth and +// the y axis is pointing to North, the heading would be zero. +// +// However, the heading may be off due to electronic compasses would be effected +// by strong magnetic fields and require constant calibration. +func (d *Device) ReadCompass() (h int32, err error) { + + x, y, _, err := d.ReadMagneticField() + if err != nil { + return + } + xf, yf := float64(x), float64(y) + h = int32(float32((180/math.Pi)*math.Atan2(yf, xf)) * 1000000) + return +} + +// ReadTemperature returns the temperature in Celsius milli degrees (°C/1000) +func (d *Device) ReadTemperature() (t int32, err error) { + + data := d.buf[:2] + err = legacy.ReadRegister(d.bus, uint8(d.MagAddress), TEMP_OUT_AUTO_INC, data) + if err != nil { + return + } + + r := int16((uint16(data[1])<<8 | uint16(data[0]))) >> 4 // temperature offset from 25 °C + t = 25000 + int32((float32(r)/8)*1000) + return +} diff --git a/lsm303dlhc/registers.go b/lsm303dlhc/registers.go new file mode 100644 index 0000000..09206f3 --- /dev/null +++ b/lsm303dlhc/registers.go @@ -0,0 +1,75 @@ +package lsm303dlhc + +const ( + + // Constants/addresses used for I2C. + ACCEL_ADDRESS = 0x19 + MAG_ADDRESS = 0x1E + + // i2C 8-bit subaddress (SUB): the 7 LSb represent the actual register address + // while the MSB enables address auto increment. + // If the MSb of the SUB field is 1, the SUB (register address) is + // automatically increased to allow multiple data read/writes. + ADDR_AUTO_INC_MASK = 0x80 + + // accelerometer registers. + ACCEL_CTRL_REG1_A = 0x20 + ACCEL_CTRL_REG4_A = 0x23 + ACCEL_OUT_X_L_A = 0x28 + ACCEL_OUT_X_H_A = 0x29 + ACCEL_OUT_Y_L_A = 0x2A + ACCEL_OUT_Y_H_A = 0x2B + ACCEL_OUT_Z_L_A = 0x2C + ACCEL_OUT_Z_H_A = 0x2D + ACCEL_OUT_AUTO_INC = ACCEL_OUT_X_L_A | ADDR_AUTO_INC_MASK + + // magnetic sensor registers. + MAG_MR_REG_M = 0x02 + MAG_OUT_X_L_M = 0x68 + MAG_OUT_X_H_M = 0x69 + MAG_OUT_Y_L_M = 0x6A + MAG_OUT_Y_H_M = 0x6B + MAG_OUT_Z_L_M = 0x6C + MAG_OUT_Z_H_M = 0x6D + MAG_OUT_AUTO_INC = MAG_OUT_X_L_M | ADDR_AUTO_INC_MASK + + // temperature sensor registers. + CRA_REG_M = 0x80 + TEMP_OUT_L_M = 0x32 + TEMP_OUT_H_M = 0x31 + TEMP_OUT_AUTO_INC = TEMP_OUT_L_M | ADDR_AUTO_INC_MASK + + // accelerometer power mode. + ACCEL_POWER_NORMAL = 0x00 // default + ACCEL_POWER_LOW = 0x08 + + // accelerometer range. + ACCEL_RANGE_2G = 0x00 // default + ACCEL_RANGE_4G = 0x01 + ACCEL_RANGE_8G = 0x02 + ACCEL_RANGE_16G = 0x03 + + // accelerometer data rate. + ACCEL_DATARATE_1HZ = 0x01 + ACCEL_DATARATE_10HZ = 0x02 + ACCEL_DATARATE_25HZ = 0x03 + ACCEL_DATARATE_50HZ = 0x04 + ACCEL_DATARATE_100HZ = 0x05 // default + ACCEL_DATARATE_200HZ = 0x06 + ACCEL_DATARATE_400HZ = 0x07 + ACCEL_DATARATE_1344HZ = 0x09 // 5376Hz in low-power mode + + // magnetic sensor power mode. + MAG_POWER_NORMAL = 0x00 // default + MAG_POWER_LOW = 0x01 + + // magnetic sensor operate mode. + MAG_SYSTEM_CONTINUOUS = 0x00 // default + MAG_SYSTEM_SINGLE = 0x01 + + // magnetic sensor data rate + MAG_DATARATE_10HZ = 0x00 // default + MAG_DATARATE_20HZ = 0x01 + MAG_DATARATE_50HZ = 0x02 + MAG_DATARATE_100HZ = 0x03 +) diff --git a/lsm6ds3tr/lsm6ds3tr.go b/lsm6ds3tr/lsm6ds3tr.go index b8be8d8..76b91d1 100644 --- a/lsm6ds3tr/lsm6ds3tr.go +++ b/lsm6ds3tr/lsm6ds3tr.go @@ -8,7 +8,6 @@ import ( "errors" "tinygo.org/x/drivers" - "tinygo.org/x/drivers/internal/legacy" ) type AccelRange uint8 @@ -26,7 +25,7 @@ type Device struct { accelSampleRate AccelSampleRate gyroRange GyroRange gyroSampleRate GyroSampleRate - buf [6]uint8 + buf [7]uint8 // up to 6 bytes for read + 1 byte for the register address } // Configuration for LSM6DS3TR device. @@ -84,30 +83,20 @@ func (d *Device) doConfigure(cfg Configuration) (err error) { d.gyroSampleRate = GYRO_SR_104 } - data := d.buf[:1] - // Configure accelerometer - data[0] = uint8(d.accelRange) | uint8(d.accelSampleRate) - err = legacy.WriteRegister(d.bus, uint8(d.Address), CTRL1_XL, data) + err = d.writeByte(CTRL1_XL, uint8(d.accelRange)|uint8(d.accelSampleRate)) if err != nil { return } - // Set ODR bit - err = legacy.ReadRegister(d.bus, uint8(d.Address), CTRL4_C, data) - if err != nil { - return - } - data[0] = data[0] &^ BW_SCAL_ODR_ENABLED - data[0] |= BW_SCAL_ODR_ENABLED - err = legacy.WriteRegister(d.bus, uint8(d.Address), CTRL4_C, data) + // Enable ODR scaling + err = d.setBits(CTRL4_C, BW_SCAL_ODR_ENABLED) if err != nil { return } // Configure gyroscope - data[0] = uint8(d.gyroRange) | uint8(d.gyroSampleRate) - err = legacy.WriteRegister(d.bus, uint8(d.Address), CTRL2_G, data) + err = d.writeByte(CTRL2_G, uint8(d.gyroRange)|uint8(d.gyroSampleRate)) if err != nil { return } @@ -118,8 +107,10 @@ func (d *Device) doConfigure(cfg Configuration) (err error) { // Connected returns whether a LSM6DS3TR has been found. // It does a "who am I" request and checks the response. func (d *Device) Connected() bool { - data := d.buf[:1] - legacy.ReadRegister(d.bus, uint8(d.Address), WHO_AM_I, data) + data, err := d.readBytes(WHO_AM_I, 1) + if err != nil { + return false + } return data[0] == 0x6A } @@ -128,8 +119,7 @@ func (d *Device) Connected() bool { // and the sensor is not moving the returned value will be around 1000000 or // -1000000. func (d *Device) ReadAcceleration() (x, y, z int32, err error) { - data := d.buf[:6] - err = legacy.ReadRegister(d.bus, uint8(d.Address), OUTX_L_XL, data) + data, err := d.readBytes(OUTX_L_XL, 6) if err != nil { return } @@ -153,8 +143,7 @@ func (d *Device) ReadAcceleration() (x, y, z int32, err error) { // rotation along one axis and while doing so integrate all values over time, // you would get a value close to 360000000. func (d *Device) ReadRotation() (x, y, z int32, err error) { - data := d.buf[:6] - err = legacy.ReadRegister(d.bus, uint8(d.Address), OUTX_L_G, data) + data, err := d.readBytes(OUTX_L_G, 6) if err != nil { return } @@ -177,8 +166,7 @@ func (d *Device) ReadRotation() (x, y, z int32, err error) { // ReadTemperature returns the temperature in celsius milli degrees (°C/1000) func (d *Device) ReadTemperature() (t int32, err error) { - data := d.buf[:2] - err = legacy.ReadRegister(d.bus, uint8(d.Address), OUT_TEMP_L, data) + data, err := d.readBytes(OUT_TEMP_L, 2) if err != nil { return } @@ -187,3 +175,26 @@ func (d *Device) ReadTemperature() (t int32, err error) { t = 25000 + (int32(int16((int16(data[1])<<8)|int16(data[0])))*125)/32 return } + +func (d *Device) readBytes(reg, size uint8) ([]byte, error) { + d.buf[0] = reg + err := d.bus.Tx(d.Address, d.buf[0:1], d.buf[1:size+1]) + if err != nil { + return nil, err + } + return d.buf[1 : size+1], nil +} + +func (d *Device) writeByte(reg, value uint8) error { + d.buf[0] = reg + d.buf[1] = value + return d.bus.Tx(d.Address, d.buf[0:2], nil) +} + +func (d *Device) setBits(reg, bits uint8) error { + data, err := d.readBytes(reg, 1) + if err != nil { + return err + } + return d.writeByte(reg, (data[0]&^bits)|bits) +} diff --git a/lsm9ds1/lsm9ds1.go b/lsm9ds1/lsm9ds1.go index e77029e..cae3bcf 100644 --- a/lsm9ds1/lsm9ds1.go +++ b/lsm9ds1/lsm9ds1.go @@ -7,7 +7,6 @@ import ( "errors" "tinygo.org/x/drivers" - "tinygo.org/x/drivers/internal/legacy" ) type AccelRange uint8 @@ -28,7 +27,7 @@ type Device struct { accelMultiplier int32 gyroMultiplier int32 magMultiplier int32 - buf [6]uint8 + buf [7]uint8 // up to 6 bytes for read + 1 byte for the register address } // Configuration for LSM9DS1 device. @@ -61,10 +60,15 @@ func New(bus drivers.I2C) *Device { // Case of boolean false and error nil means I2C is up, // but "who am I" responses have unexpected values. func (d *Device) Connected() bool { - data1, data2 := d.buf[:1], d.buf[1:2] - legacy.ReadRegister(d.bus, d.AccelAddress, WHO_AM_I, data1) - legacy.ReadRegister(d.bus, d.MagAddress, WHO_AM_I_M, data2) - return data1[0] == 0x68 && data2[0] == 0x3D + data, err := d.readBytes(d.AccelAddress, WHO_AM_I, 1) + if err != nil || data[0] != 0x68 { + return false + } + data, err = d.readBytes(d.MagAddress, WHO_AM_I_M, 1) + if err != nil || data[0] != 0x3D { + return false + } + return true } // ReadAcceleration reads the current acceleration from the device and returns @@ -72,8 +76,7 @@ func (d *Device) Connected() bool { // and the sensor is not moving the returned value will be around 1000000 or // -1000000. func (d *Device) ReadAcceleration() (x, y, z int32, err error) { - data := d.buf[:6] - err = legacy.ReadRegister(d.bus, uint8(d.AccelAddress), OUT_X_L_XL, data) + data, err := d.readBytes(d.AccelAddress, OUT_X_L_XL, 6) if err != nil { return } @@ -88,8 +91,7 @@ func (d *Device) ReadAcceleration() (x, y, z int32, err error) { // rotation along one axis and while doing so integrate all values over time, // you would get a value close to 360000000. func (d *Device) ReadRotation() (x, y, z int32, err error) { - data := d.buf[:6] - err = legacy.ReadRegister(d.bus, uint8(d.AccelAddress), OUT_X_L_G, data) + data, err := d.readBytes(d.AccelAddress, OUT_X_L_G, 6) if err != nil { return } @@ -102,8 +104,7 @@ func (d *Device) ReadRotation() (x, y, z int32, err error) { // ReadMagneticField reads the current magnetic field from the device and returns // it in nT (nanotesla). 1 G (gauss) = 100_000 nT (nanotesla). func (d *Device) ReadMagneticField() (x, y, z int32, err error) { - data := d.buf[:6] - err = legacy.ReadRegister(d.bus, uint8(d.MagAddress), OUT_X_L_M, data) + data, err := d.readBytes(d.MagAddress, OUT_X_L_M, 6) if err != nil { return } @@ -115,8 +116,7 @@ func (d *Device) ReadMagneticField() (x, y, z int32, err error) { // ReadTemperature returns the temperature in Celsius milli degrees (°C/1000) func (d *Device) ReadTemperature() (t int32, err error) { - data := d.buf[:2] - err = legacy.ReadRegister(d.bus, uint8(d.AccelAddress), OUT_TEMP_L, data) + data, err := d.readBytes(d.AccelAddress, OUT_TEMP_L, 2) if err != nil { return } @@ -167,20 +167,16 @@ func (d *Device) doConfigure(cfg Configuration) (err error) { d.magMultiplier = 58 } - data := d.buf[:1] - // Configure accelerometer // Sample rate & measurement range - data[0] = uint8(cfg.AccelSampleRate)<<5 | uint8(cfg.AccelRange)<<3 - err = legacy.WriteRegister(d.bus, d.AccelAddress, CTRL_REG6_XL, data) + err = d.writeByte(d.AccelAddress, CTRL_REG6_XL, uint8(cfg.AccelSampleRate)<<5|uint8(cfg.AccelRange)<<3) if err != nil { return } // Configure gyroscope // Sample rate & measurement range - data[0] = uint8(cfg.GyroSampleRate)<<5 | uint8(cfg.GyroRange)<<3 - err = legacy.WriteRegister(d.bus, d.AccelAddress, CTRL_REG1_G, data) + err = d.writeByte(d.AccelAddress, CTRL_REG1_G, uint8(cfg.GyroSampleRate)<<5|uint8(cfg.GyroRange)<<3) if err != nil { return } @@ -190,33 +186,44 @@ func (d *Device) doConfigure(cfg Configuration) (err error) { // Temperature compensation enabled // High-performance mode XY axis // Sample rate - data[0] = 0b10000000 | 0b01000000 | uint8(cfg.MagSampleRate)<<2 - err = legacy.WriteRegister(d.bus, d.MagAddress, CTRL_REG1_M, data) + err = d.writeByte(d.MagAddress, CTRL_REG1_M, 0b10000000|0b01000000|uint8(cfg.MagSampleRate)<<2) if err != nil { return } // Measurement range - data[0] = uint8(cfg.MagRange) << 5 - err = legacy.WriteRegister(d.bus, d.MagAddress, CTRL_REG2_M, data) + err = d.writeByte(d.MagAddress, CTRL_REG2_M, uint8(cfg.MagRange)<<5) if err != nil { return } // Continuous-conversion mode // https://electronics.stackexchange.com/questions/237397/continuous-conversion-vs-single-conversion-mode - data[0] = 0b00000000 - err = legacy.WriteRegister(d.bus, d.MagAddress, CTRL_REG3_M, data) + err = d.writeByte(d.MagAddress, CTRL_REG3_M, 0b00000000) if err != nil { return } // High-performance mode Z axis - data[0] = 0b00001000 - err = legacy.WriteRegister(d.bus, d.MagAddress, CTRL_REG4_M, data) + err = d.writeByte(d.MagAddress, CTRL_REG4_M, 0b00001000) if err != nil { return } return nil } + +func (d *Device) readBytes(addr, reg, size uint8) ([]byte, error) { + d.buf[0] = reg + err := d.bus.Tx(uint16(addr), d.buf[0:1], d.buf[1:size+1]) + if err != nil { + return nil, err + } + return d.buf[1 : size+1], nil +} + +func (d *Device) writeByte(addr, reg, value uint8) error { + d.buf[0] = reg + d.buf[1] = value + return d.bus.Tx(uint16(addr), d.buf[0:2], nil) +} diff --git a/max6675/max6675.go b/max6675/max6675.go new file mode 100644 index 0000000..15ecf7c --- /dev/null +++ b/max6675/max6675.go @@ -0,0 +1,53 @@ +// Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/max6675.pdf +package max6675 + +import ( + "errors" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/pin" +) + +// ErrThermocoupleOpen is returned when the thermocouple input is open. +// i.e. not attached or faulty +var ErrThermocoupleOpen = errors.New("thermocouple input open") + +type Device struct { + bus drivers.SPI + 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 pin.Output) *Device { + return &Device{ + bus: bus, + cs: cs.Set, + } +} + +// Read and return the temperature in celsius +func (d *Device) Read() (float32, error) { + var ( + read []byte = []byte{0, 0} + value uint16 + ) + + d.cs.Low() + if err := d.bus.Tx([]byte{0, 0}, read); err != nil { + return 0, err + } + d.cs.High() + + // datasheet: Bit D2 is normally low and goes high if the thermocouple input is open. + if read[1]&0x04 == 0x04 { + return 0, ErrThermocoupleOpen + } + + // data is 12 bits, split across the two bytes + // -XXXXXXX XXXXX--- + value = (uint16(read[0]) << 5) | (uint16(read[1]) >> 3) + + return float32(value) * 0.25, nil +} diff --git a/max72xx/max72xx.go b/max72xx/max72xx.go index 27de1b5..82cceac 100644 --- a/max72xx/max72xx.go +++ b/max72xx/max72xx.go @@ -3,29 +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 machine.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 machine.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. diff --git a/mcp2515/mcp2515.go b/mcp2515/mcp2515.go index 2b636b1..2b03096 100644 --- a/mcp2515/mcp2515.go +++ b/mcp2515/mcp2515.go @@ -6,20 +6,38 @@ package mcp2515 // import "tinygo.org/x/drivers/mcp2515" import ( + "encoding/binary" "errors" "fmt" - "machine" "time" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" +) + +var ( + ErrNothingIsReceived = errors.New("readMsg: nothing is received") + ErrRequestNewModeMaxTimeEx = errors.New("requestNewMode max time expired") + ErrLengthIsLongerThanCapacity = errors.New("length is longer than capacity") + ErrTxTimeout = errors.New("Tx: Tx timeout") + ErrInvalidDirection = errors.New("invalid direction") + ErrInvalidParameter = errors.New("invalid parameter") + ErrCannotExpandBuffer = errors.New("cannot expand buffer (to avoid memory allocation)") ) // Device wraps MCP2515 SPI CAN Module. type Device struct { - spi SPI - cs machine.Pin - msg *CANMsg - mcpMode byte + spi SPI + cs pin.OutputFunc + msg *CANMsg + extended bool + mcpMode byte + configurePins func() +} + +type Configuration struct { + Extended bool } // CANMsg stores CAN message fields. @@ -36,23 +54,30 @@ 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 } // Configure sets up the device for communication. -func (d *Device) Configure() { - d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput}) +func (d *Device) Configure(cfg Configuration) { + if d.configurePins == nil { + panic(legacy.ErrConfigBeforeInstantiated) + } + d.extended = cfg.Extended + d.configurePins() } const beginTimeoutValue int = 10 @@ -109,9 +134,13 @@ func (d *Device) Tx(canid uint32, dlc uint8, data []byte) error { timeoutCount++ } if timeoutCount == timeoutvalue { - return fmt.Errorf("Tx: Tx timeout") + return ErrTxTimeout } - err = d.writeCANMsg(bufNum, canid, 0, 0, dlc, data) + ext := byte(0) + if d.extended { + ext = 1 + } + err = d.writeCANMsg(bufNum, canid, ext, 0, dlc, data) if err != nil { return err } @@ -381,7 +410,7 @@ func (d *Device) configRate(speed, clock byte) error { set = false } if !set { - return errors.New("invalid parameter") + return ErrInvalidParameter } if err := d.setRegister(mcpCNF1, cfg1); err != nil { return err @@ -441,7 +470,7 @@ func (d *Device) readMsg() error { return err } } else { - return fmt.Errorf("readMsg: nothing is received") + return ErrNothingIsReceived } return nil @@ -548,39 +577,22 @@ func (d *Device) writeCANMsg(bufNum uint8, canid uint32, ext, rtrBit, dlc uint8, } func (s *SPI) setTxBufData(canid uint32, ext, rtrBit, dlc uint8, data []byte) error { - canid = canid & 0x0FFFF + var id [4]byte if ext == 1 { - // TODO: add Extended ID - err := s.setTxData(0) - if err != nil { - return err - } - err = s.setTxData(0) - if err != nil { - return err - } - err = s.setTxData(0) - if err != nil { - return err - } - err = s.setTxData(0) - if err != nil { - return err - } + canid = canid & extidBottom29Mask + extended_id := canid + high_11 := extended_id & extidTop11WriteMask + low_18 := extended_id & extidBottom18Mask + high_11 <<= 3 + extended_id_shifted := high_11 | low_18 + canid = extended_id_shifted | extidFlagMask } else { - err := s.setTxData(byte(canid >> 3)) - if err != nil { - return err - } - err = s.setTxData(byte((canid & 0x07) << 5)) - if err != nil { - return err - } - err = s.setTxData(0) - if err != nil { - return err - } - err = s.setTxData(0) + canid = canid & stdidBottom11Mask + canid <<= 16 + 5 + } + binary.BigEndian.PutUint32(id[:], canid) + for _, b := range id { + err := s.setTxData(b) if err != nil { return err } @@ -777,7 +789,7 @@ func (d *Device) requestNewMode(newMode byte) error { if r&modeMask == newMode { return nil } else if e := time.Now(); e.Sub(s) > 200*time.Millisecond { - return errors.New("requestNewMode max time expired") + return ErrRequestNewModeMaxTimeEx } } } @@ -853,23 +865,23 @@ func (s *SPI) clearBuffer(dir int) error { return s.setBufferLength(0, dir) } func (s *SPI) setBufferLength(length int, dir int) error { if dir == tx { if length > cap(s.tx) { - return fmt.Errorf("length is longer than capacity") + return ErrLengthIsLongerThanCapacity } s.tx = s.tx[:length] } else if dir == rx { if length > cap(s.rx) { - return fmt.Errorf("length is longer than capacity") + return ErrLengthIsLongerThanCapacity } s.rx = s.rx[:length] } else { - return fmt.Errorf("invalid direction") + return ErrInvalidDirection } return nil } func (s *SPI) setTxData(data byte) error { if len(s.tx) >= bufferSize { - return fmt.Errorf("cannot expand buffer (to avoid memory allocation)") + return ErrCannotExpandBuffer } s.tx = append(s.tx, data) diff --git a/mcp2515/registers.go b/mcp2515/registers.go index 6b7f28a..9ca6661 100644 --- a/mcp2515/registers.go +++ b/mcp2515/registers.go @@ -417,4 +417,11 @@ const ( canFail = 0xff canMaxCharInMessage = 8 + + // for extended id + extidTop11WriteMask = 0x1FFC0000 + extidBottom29Mask = (1 << 29) - 1 // extended id bits + extidBottom18Mask = (1 << 18) - 1 // bottom 18 bits + stdidBottom11Mask = 0x7FF + extidFlagMask = 1 << 19 ) diff --git a/mcp9808/mcp9808.go b/mcp9808/mcp9808.go new file mode 100644 index 0000000..a4ec998 --- /dev/null +++ b/mcp9808/mcp9808.go @@ -0,0 +1,98 @@ +// Package mcp9808 implements a driver for the MCP9808 High Accuracy I2C Temperature Sensor +// +// Datasheet: https://cdn-shop.adafruit.com/datasheets/MCP9808.pdf +// Module: https://www.adafruit.com/product/1782 +// Only implemented: temperature reading, resolution read & set +package mcp9808 + +import ( + "encoding/binary" + "errors" + + "tinygo.org/x/drivers" +) + +type Device struct { + bus drivers.I2C + Address uint16 +} + +func New(bus drivers.I2C) Device { + return Device{bus, MCP9808_I2CADDR_DEFAULT} +} + +func (d *Device) Connected() bool { + data := make([]byte, 2) + d.Read(MCP9808_REG_DEVICE_ID, &data) + return binary.BigEndian.Uint16(data) == MCP9808_DEVICE_ID +} + +func (d *Device) ReadTemperature() (float64, error) { + data := make([]byte, 2) + var temp float64 + if err := d.Read(MCP9808_REG_AMBIENT_TEMP, &data); err != nil { + return 0, err + } + + data[0] = data[0] & 0x1F + if data[0]&0x10 == 0x10 { + data[0] = data[0] & 0x0F + temp = float64(data[0])*16 + float64(data[1])/16.0 - 256 + } + temp = float64(data[0])*16 + float64(data[1])/16.0 + return temp, nil +} + +func (d *Device) ReadResolution() (resolution, error) { + data := make([]byte, 2) + err := d.Read(MCP9808_REG_RESOLUTION, &data) + if err != nil { + return 0, err + } + switch data[0] { + case 0: + return Low, nil + case 1: + return Medium, nil + case 2: + return High, nil + case 3: + return Maximum, nil + + default: + return 0, errors.New("unknown resolution") + } +} + +func (d *Device) SetResolution(r resolution) error { + switch r { + case Low: + if err := d.Write(MCP9808_REG_RESOLUTION, []byte{0x00}); err != nil { + return err + } + case Medium: + if err := d.Write(MCP9808_REG_RESOLUTION, []byte{0x01}); err != nil { + return err + } + case High: + if err := d.Write(MCP9808_REG_RESOLUTION, []byte{0x02}); err != nil { + return err + } + case Maximum: + if err := d.Write(MCP9808_REG_RESOLUTION, []byte{0x03}); err != nil { + return err + } + default: + return nil + } + return nil +} + +func (d *Device) Write(register byte, data []byte) error { + buf := append([]byte{register}, data...) + return d.bus.Tx(d.Address, buf, nil) +} + +func (d *Device) Read(register byte, data *[]byte) error { + return d.bus.Tx(d.Address, []byte{register}, *data) +} diff --git a/mcp9808/registers.go b/mcp9808/registers.go new file mode 100644 index 0000000..a66d094 --- /dev/null +++ b/mcp9808/registers.go @@ -0,0 +1,53 @@ +// Package mcp9808 implements a driver for the MCP9808 High Accuracy I2C Temperature Sensor +// +// Datasheet: https://cdn-shop.adafruit.com/datasheets/MCP9808.pdf +// Module: https://www.adafruit.com/product/1782 +package mcp9808 + +// Constants/addresses used for I2C. +const ( + MCP9808_DEVICE_ID = 0x0400 + MCP9808_MANUF_ID = 0x0054 + + MCP9808_I2CADDR_DEFAULT = 0x18 //default I2C address + MCP9808_REG_CONFIG = 0x01 //MCP9808 config register + + MCP9808_REG_CONFIG_SHUTDOWN = 0x0100 //shutdown config + MCP9808_REG_CONFIG_CRITLOCKED = 0x0080 //critical trip lock + MCP9808_REG_CONFIG_WINLOCKED = 0x0040 //alarm window lock + MCP9808_REG_CONFIG_INTCLR = 0x0020 //interrupt clear + MCP9808_REG_CONFIG_ALERTSTAT = 0x0010 //alert output status + MCP9808_REG_CONFIG_ALERTCTRL = 0x0008 //alert output control + MCP9808_REG_CONFIG_ALERTSEL = 0x0004 //alert output select + MCP9808_REG_CONFIG_ALERTPOL = 0x0002 //alert output polarity + MCP9808_REG_CONFIG_ALERTMODE = 0x0001 //alert output mode + + MCP9808_REG_UPPER_TEMP = 0x02 //upper alert boundary + MCP9808_REG_LOWER_TEMP = 0x03 //lower alert boundery + MCP9808_REG_CRIT_TEMP = 0x04 //critical temperature + MCP9808_REG_AMBIENT_TEMP = 0x05 //ambient temperature + MCP9808_REG_MANUF_ID = 0x06 //manufacturer ID + MCP9808_REG_DEVICE_ID = 0x07 //device ID + MCP9808_REG_RESOLUTION = 0x08 //resolution +) + +/* +======= ============ ============== +value resolution reading Time +======= ============ ============== + + 0 0.5°C 30 ms + 1 0.25°C 65 ms + 2 0.125°C 130 ms + 3 0.0625°C 250 ms + +======= ============ ============== +*/ +type resolution uint8 + +const ( + Low resolution = iota + Medium + High + Maximum +) diff --git a/microphone/microphone.go b/microphone/microphone.go index 03d82b3..60a0e3f 100644 --- a/microphone/microphone.go +++ b/microphone/microphone.go @@ -64,7 +64,7 @@ func (d *Device) Read(r []int32) (int, error) { count := len(r) // get the next group of samples - machine.I2S0.Read(d.buf) + machine.I2S0.ReadStereo(d.buf) if len(r) > len(d.buf) { count = len(d.buf) @@ -83,7 +83,7 @@ func (d *Device) ReadWithFilter(r []int32) (int, error) { for i := 0; i < len(r); i++ { // get the next group of samples - machine.I2S0.Read(d.buf) + machine.I2S0.ReadStereo(d.buf) // filter sum = applySincFilter(d.buf) diff --git a/netdev/netdev.go b/netdev/netdev.go index 229b50a..8090d02 100644 --- a/netdev/netdev.go +++ b/netdev/netdev.go @@ -35,10 +35,12 @@ var ( var ( ErrFamilyNotSupported = errors.New("Address family not supported") ErrProtocolNotSupported = errors.New("Socket protocol/type not supported") - ErrStartingDHCPClient = errors.New("Error starting DHPC client") + ErrStartingDHCPClient = errors.New("Error starting DHCP client") + ErrStartingDHCPServer = errors.New("Error starting DHCP server") ErrNoMoreSockets = errors.New("No more sockets") ErrClosingSocket = errors.New("Error closing socket") ErrNotSupported = errors.New("Not supported") + ErrInvalidSocketFd = errors.New("Invalid socket fd") ) // Duplicate of non-exported net.errTimeout diff --git a/netlink/netlink.go b/netlink/netlink.go index aea60b4..4cff779 100644 --- a/netlink/netlink.go +++ b/netlink/netlink.go @@ -1,5 +1,4 @@ -// L2 data link layer - +// package netlink provides an interface for L2 data link layer operations. package netlink import ( @@ -20,6 +19,7 @@ var ( ErrNotSupported = errors.New("Not supported") ) +// Event is a network event type passed to the callback registered with NetNotify. type Event int // Network events @@ -38,6 +38,7 @@ const ( ConnectModeAP // Connect as Wifi Access Point ) +// AuthType is the type of WiFi authorization to use when connecting to an access point. type AuthType int // Wifi authorization types. Used when setting up an access point, or @@ -49,10 +50,11 @@ const ( AuthTypeWPA2Mixed // WPA2/WPA mixed authorization ) +// DefaultConnectTimeout is the default timeout for connection attempts. This is used when ConnectParams.ConnectTimeout is zero. const DefaultConnectTimeout = 10 * time.Second +// ConnectParams is the set of parameters used to connect a Netlinker device to a network. type ConnectParams struct { - // Connect mode ConnectMode @@ -81,22 +83,23 @@ type ConnectParams struct { // downed connection or hardware fault and try to recover the // connection. Set to zero to disable watchodog. WatchdogTimeout time.Duration + + // Hostname to use for this device. + Hostname string } // Netlinker is TinyGo's OSI L2 data link layer interface. Network device // drivers implement Netlinker to expose the device's L2 functionality. - type Netlinker interface { - - // Connect device to network + // NetConnect connects the device to a network NetConnect(params *ConnectParams) error - // Disconnect device from network + // NetDisconnect disconnects the device from the network NetDisconnect() - // Notify to register callback for network events + // NetNotify registers a callback for network events NetNotify(cb func(Event)) - // GetHardwareAddr returns device MAC address + // GetHardwareAddr returns the device's MAC address GetHardwareAddr() (net.HardwareAddr, error) } diff --git a/netlink/probe/comboat.go b/netlink/probe/comboat.go new file mode 100644 index 0000000..c1ed007 --- /dev/null +++ b/netlink/probe/comboat.go @@ -0,0 +1,26 @@ +//go:build comboat_fw + +package probe + +import ( + "machine" + + "tinygo.org/x/drivers/comboat" + "tinygo.org/x/drivers/netdev" + "tinygo.org/x/drivers/netlink" +) + +func Probe() (netlink.Netlinker, netdev.Netdever) { + + cfg := comboat.Config{ + BaudRate: 115200, + Uart: machine.UART1, + Tx: machine.UART1_TX_PIN, + Rx: machine.UART1_RX_PIN, + } + + combo := comboat.NewDevice(&cfg) + netdev.UseNetdev(combo) + + return combo, combo +} diff --git a/netlink/probe/wifinina.go b/netlink/probe/wifinina.go index bae9647..0668eee 100644 --- a/netlink/probe/wifinina.go +++ b/netlink/probe/wifinina.go @@ -1,4 +1,4 @@ -//go:build pyportal || arduino_nano33 || nano_rp2040 || metro_m4_airlift || matrixportal_m4 +//go:build ninafw && !arduino_mkrwifi1010 package probe diff --git a/onewire/onewire.go b/onewire/onewire.go index 5a51cf7..ee18283 100644 --- a/onewire/onewire.go +++ b/onewire/onewire.go @@ -27,8 +27,9 @@ type Config struct{} // Errors list var ( - errNoPresence = errors.New("Error: OneWire. No devices on the bus.") - errReadAddress = errors.New("Error: OneWire. Read address error: CRC mismatch.") + errNoPresence = errors.New("Error: OneWire. No devices on the bus.") + errTooManyDevices = errors.New("Error: OneWire. Too many devices on the bus.") + errReadAddress = errors.New("Error: OneWire. Read address error: CRC mismatch.") ) // New creates a new GPIO 1-Wire connection. @@ -46,7 +47,7 @@ func (d *Device) Configure(config Config) {} func (d Device) Reset() error { d.p.Configure(machine.PinConfig{Mode: machine.PinOutput}) time.Sleep(480 * time.Microsecond) - d.p.Configure(machine.PinConfig{Mode: machine.PinInput}) + d.p.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) time.Sleep(70 * time.Microsecond) precence := d.p.Get() time.Sleep(410 * time.Microsecond) @@ -61,11 +62,11 @@ func (d Device) WriteBit(data uint8) { d.p.Configure(machine.PinConfig{Mode: machine.PinOutput}) if data&1 == 1 { // Send '1' time.Sleep(5 * time.Microsecond) - d.p.Configure(machine.PinConfig{Mode: machine.PinInput}) + d.p.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) time.Sleep(60 * time.Microsecond) } else { // Send '0' time.Sleep(60 * time.Microsecond) - d.p.Configure(machine.PinConfig{Mode: machine.PinInput}) + d.p.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) time.Sleep(5 * time.Microsecond) } } @@ -82,7 +83,7 @@ func (d Device) Write(data uint8) { func (d Device) ReadBit() (data uint8) { d.p.Configure(machine.PinConfig{Mode: machine.PinOutput}) time.Sleep(3 * time.Microsecond) - d.p.Configure(machine.PinConfig{Mode: machine.PinInput}) + d.p.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) time.Sleep(8 * time.Microsecond) if d.p.Get() { data = 1 @@ -111,7 +112,7 @@ func (d Device) ReadAddress() ([]uint8, error) { for i := 0; i < 8; i++ { romid[i] = d.Read() } - if d.Сrc8(romid, 7) != romid[7] { + if d.Сrc8(romid) != 0 { return nil, errReadAddress } return romid, nil @@ -187,15 +188,22 @@ func (d Device) Search(cmd uint8) ([][]uint8, error) { } d.WriteBit(bit) } + if d.Сrc8(lastAddress) != 0 { + continue + } + lastFork = lastZero copy(romIDs[romIndex], lastAddress) romIndex++ + if romIndex >= 32 { + return romIDs, errTooManyDevices + } } return romIDs[:romIndex:romIndex], nil } // Crc8 compute a Dallas Semiconductor 8 bit CRC. -func (d Device) Сrc8(buffer []uint8, size int) (crc uint8) { +func (_ Device) Сrc8(buffer []uint8) (crc uint8) { // Dow-CRC using polynomial X^8 + X^5 + X^4 + X^0 // Tiny 2x16 entry CRC table created by Arjen Lentz // See http://lentz.com.au/blog/calculating-crc-with-a-tiny-32-entry-lookup-table @@ -205,7 +213,7 @@ func (d Device) Сrc8(buffer []uint8, size int) (crc uint8) { 0x00, 0x9D, 0x23, 0xBE, 0x46, 0xDB, 0x65, 0xF8, 0x8C, 0x11, 0xAF, 0x32, 0xCA, 0x57, 0xE9, 0x74, } - for i := 0; i < size; i++ { + for i := 0; i < len(buffer); i++ { crc = buffer[i] ^ crc // just re-using crc as intermediate crc = crc8_table[crc&0x0f] ^ crc8_table[16+((crc>>4)&0x0f)] } diff --git a/p1am/p1am.go b/p1am/p1am.go index d61817d..a46cd71 100644 --- a/p1am/p1am.go +++ b/p1am/p1am.go @@ -14,7 +14,8 @@ import ( ) type P1AM struct { - bus machine.SPI + bus *machine.SPI + slaveSelectPin, slaveAckPin, baseEnablePin machine.Pin // SkipAutoConfig will skip loading a default configuration into each module. diff --git a/pcd8544/pcd8544.go b/pcd8544/pcd8544.go index 3a843cb..447492b 100644 --- a/pcd8544/pcd8544.go +++ b/pcd8544/pcd8544.go @@ -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, } } diff --git a/pcf8591/pcf8591.go b/pcf8591/pcf8591.go new file mode 100644 index 0000000..3bb39ba --- /dev/null +++ b/pcf8591/pcf8591.go @@ -0,0 +1,84 @@ +// Package pcf8591 implements a driver for the PCF8591 Analog to Digital/Digital to Analog Converter. +// +// Datasheet: https://www.nxp.com/docs/en/data-sheet/PCF8591.pdf +package pcf8591 // import "tinygo.org/x/drivers/pcf8591" + +import ( + "machine" + + "errors" + + "tinygo.org/x/drivers" +) + +// Device wraps PCF8591 ADC functions. +type Device struct { + bus drivers.I2C + Address uint16 + CH0 ADCPin + CH1 ADCPin + CH2 ADCPin + CH3 ADCPin +} + +// ADCPin is the implementation of the ADConverter interface. +type ADCPin struct { + machine.Pin + d *Device +} + +// New returns a new PCF8591 driver. Pass in a fully configured I2C bus. +func New(b drivers.I2C) *Device { + d := &Device{ + bus: b, + Address: defaultAddress, + } + + // setup all channels + d.CH0 = d.GetADC(0) + d.CH1 = d.GetADC(1) + d.CH2 = d.GetADC(2) + d.CH3 = d.GetADC(3) + + return d +} + +// Configure here just for interface compatibility. +func (d *Device) Configure() { +} + +// Read analog data from channel +func (d *Device) Read(ch int) (uint16, error) { + if ch < 0 || ch > 3 { + return 0, errors.New("invalid channel for pcf8591 Read") + } + + return d.GetADC(ch).Get(), nil +} + +// GetADC returns an ADC for a specific channel. +func (d *Device) GetADC(ch int) ADCPin { + return ADCPin{machine.Pin(ch), d} +} + +// Get the current reading for a specific ADCPin. +func (p ADCPin) Get() uint16 { + // TODO: also implement DAC + tx := make([]byte, 2) + tx[0] = byte(p.Pin) + + rx := make([]byte, 2) + + // The result from the measurement triggered by the first write, + // however, the second write is required to get the result. + // See section 8.4 "A/D Conversion" in the datasheet for more info + p.d.bus.Tx(p.d.Address, tx, rx) + p.d.bus.Tx(p.d.Address, tx, rx) + + // scale result to 16bit value like other ADCs + return uint16(rx[1]) << 8 +} + +// Configure here just for interface compatibility. +func (p ADCPin) Configure() { +} diff --git a/pcf8591/registers.go b/pcf8591/registers.go new file mode 100644 index 0000000..0a660cd --- /dev/null +++ b/pcf8591/registers.go @@ -0,0 +1,7 @@ +package pcf8591 + +// PCF8591 Default Address +const defaultAddress = 0x48 + +// control bit for DAC +const PCF8591_ENABLE_DAC = 0x40 diff --git a/pixel/image.go b/pixel/image.go index 54633e3..77b2f90 100644 --- a/pixel/image.go +++ b/pixel/image.go @@ -22,12 +22,13 @@ func NewImage[T Color](width, height int) Image[T] { } var zeroColor T var data unsafe.Pointer - if zeroColor.BitsPerPixel()%8 == 0 { + switch { + case zeroColor.BitsPerPixel()%8 == 0: // Typical formats like RGB888 and RGB565. // Each color starts at a whole byte offset from the start. buf := make([]T, width*height) data = unsafe.Pointer(&buf[0]) - } else { + default: // Formats like RGB444 that have 12 bits per pixel. // We access these as bytes, so allocate the buffer as a byte slice. bufBits := width * height * zeroColor.BitsPerPixel() @@ -42,6 +43,40 @@ func NewImage[T Color](width, height int) Image[T] { } } +// NewImageFromBytes creates a new image of the given size using an existing data slice of bytes. +func NewImageFromBytes[T Color](width, height int, buf []byte) Image[T] { + if width < 0 || height < 0 || int(int16(width)) != width || int(int16(height)) != height { + // The width/height are stored as 16-bit integers and should never be + // negative. + panic("NewImageFromBytes: width/height out of bounds") + } + var zeroColor T + var data unsafe.Pointer + switch { + case zeroColor.BitsPerPixel()%8 == 0: + // Typical formats like RGB888 and RGB565. + // Each color starts at a whole byte offset from the start. + if len(buf) != width*height*int(unsafe.Sizeof(zeroColor)) { + panic("NewImageFromBytes: data slice size mismatch") + } + data = unsafe.Pointer(&buf[0]) + default: + // Formats like RGB444 that have 12 bits per pixel. + // We access these as bytes, so allocate the buffer as a byte slice. + bufBits := width * height * zeroColor.BitsPerPixel() + bufBytes := (bufBits + 7) / 8 + if len(buf) != bufBytes { + panic("NewImageFromBytes: data slice size mismatch") + } + data = unsafe.Pointer(&buf[0]) + } + return Image[T]{ + width: int16(width), + height: int16(height), + data: data, + } +} + // Rescale returns a new Image buffer based on the img buffer. // The contents is undefined after the Rescale operation, and any modification // to the returned image will overwrite the underlying image buffer in undefined @@ -80,10 +115,11 @@ func (img Image[T]) Len() int { func (img Image[T]) RawBuffer() []uint8 { var zeroColor T var numBytes int - if zeroColor.BitsPerPixel()%8 == 0 { + switch { + case zeroColor.BitsPerPixel()%8 == 0: // Each color starts at a whole byte offset. numBytes = int(unsafe.Sizeof(zeroColor)) * int(img.width) * int(img.height) - } else { + default: // Formats like RGB444 that aren't a whole number of bytes. numBits := zeroColor.BitsPerPixel() * int(img.width) * int(img.height) numBytes = (numBits + 7) / 8 // round up (see NewImage) @@ -99,7 +135,35 @@ func (img Image[T]) Size() (int, int) { func (img Image[T]) setPixel(index int, c T) { var zeroColor T - if zeroColor.BitsPerPixel()%8 == 0 { + switch { + case zeroColor.BitsPerPixel() == 1: + // Monochrome. + offset := index / 8 + bits := index % 8 + + ptr := (*byte)(unsafe.Add(img.data, offset)) + if c != zeroColor { + *((*byte)(ptr)) |= (1 << (7 - uint8(bits))) + } else { + *((*byte)(ptr)) &^= (1 << (7 - uint8(bits))) + } + + 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. offset := index * int(unsafe.Sizeof(zeroColor)) @@ -147,7 +211,23 @@ func (img Image[T]) Get(x, y int) T { var zeroColor T index := y*int(img.width) + x // index into img.data - if zeroColor.BitsPerPixel()%8 == 0 { + switch { + case zeroColor.BitsPerPixel() == 1: + // Monochrome. + var c Monochrome + offset := index / 8 + bits := index % 8 + 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)) ptr := unsafe.Add(img.data, offset) @@ -181,8 +261,26 @@ func (img Image[T]) Get(x, y int) T { func (img Image[T]) FillSolidColor(color T) { var zeroColor T - // Fast pass for colors of 8, 16, 24, etc bytes in size. - if zeroColor.BitsPerPixel()%8 == 0 { + switch { + case zeroColor.BitsPerPixel() == 1: + // Monochrome. + var colorByte uint8 + if color != zeroColor { + colorByte = 0xff + } + numBytes := int(img.width) * int(img.height) / 8 + for i := 0; i < numBytes; i++ { + // TODO: this can be optimized a lot. + // - The store can be done as a 32-bit integer, after checking for + // alignment. + // - Perhaps the loop can be unrolled to improve copy performance. + ptr := (*byte)(unsafe.Add(img.data, i)) + *((*byte)(ptr)) = colorByte + } + return + + case zeroColor.BitsPerPixel()%8 == 0: + // Fast pass for colors of 8, 16, 24, etc bytes in size. ptr := img.data for i := 0; i < img.Len(); i++ { // TODO: this can be optimized a lot. diff --git a/pixel/image_test.go b/pixel/image_test.go index 42f292c..342f81b 100644 --- a/pixel/image_test.go +++ b/pixel/image_test.go @@ -1,15 +1,38 @@ package pixel_test import ( + goimage "image" "image/color" + "math/rand" "testing" "tinygo.org/x/drivers/pixel" ) +func TestImageRGB888(t *testing.T) { + image := pixel.NewImage[pixel.RGB888](5, 3) + if width, height := image.Size(); width != 5 || height != 3 { + t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) + } + for _, c := range []color.RGBA{ + {R: 0xff, A: 0xff}, + {G: 0xff, A: 0xff}, + {B: 0xff, A: 0xff}, + {R: 0x10, A: 0xff}, + {G: 0x10, A: 0xff}, + {B: 0x10, A: 0xff}, + } { + image.Set(4, 2, pixel.NewColor[pixel.RGB888](c.R, c.G, c.B)) + c2 := image.Get(4, 2).RGBA() + if c2 != c { + t.Errorf("failed to roundtrip color: expected %v but got %v", c, c2) + } + } +} + func TestImageRGB565BE(t *testing.T) { image := pixel.NewImage[pixel.RGB565BE](5, 3) - if width, height := image.Size(); width != 5 && height != 3 { + if width, height := image.Size(); width != 5 || height != 3 { t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) } for _, c := range []color.RGBA{ @@ -28,9 +51,30 @@ func TestImageRGB565BE(t *testing.T) { } } +func TestImageRGB555(t *testing.T) { + image := pixel.NewImage[pixel.RGB555](5, 3) + if width, height := image.Size(); width != 5 || height != 3 { + t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) + } + for _, c := range []color.RGBA{ + {R: 0xff, A: 0xff}, + {G: 0xff, A: 0xff}, + {B: 0xff, A: 0xff}, + {R: 0x10, A: 0xff}, + {G: 0x10, A: 0xff}, + {B: 0x10, A: 0xff}, + } { + image.Set(4, 2, pixel.NewColor[pixel.RGB555](c.R, c.G, c.B)) + c2 := image.Get(4, 2).RGBA() + if c2 != c { + t.Errorf("failed to roundtrip color: expected %v but got %v", c, c2) + } + } +} + func TestImageRGB444BE(t *testing.T) { image := pixel.NewImage[pixel.RGB444BE](5, 3) - if width, height := image.Size(); width != 5 && height != 3 { + if width, height := image.Size(); width != 5 || height != 3 { t.Errorf("image.Size(): expected 5, 3 but got %d, %d", width, height) } for _, c := range []color.RGBA{ @@ -62,3 +106,254 @@ 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 { + t.Errorf("image.Size(): expected 128, 64 but got %d, %d", width, height) + } + for _, expected := range []color.RGBA{ + {R: 0xff, G: 0xff, B: 0xff}, + {G: 0xff}, + {R: 0xff, G: 0xff}, + {G: 0xff, B: 0xff}, + {R: 0x00}, + {G: 0x00, A: 0xff}, + {B: 0x00, A: 0xff}, + } { + encoded := pixel.NewColor[pixel.Monochrome](expected.R, expected.G, expected.B) + image.Set(5, 3, encoded) + actual := image.Get(5, 3).RGBA() + switch { + case int(expected.R)+int(expected.G)+int(expected.B) > 128*3: + // should be true eg white + if actual.R == 0 || actual.G == 0 || actual.B == 0 { + t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual) + } + default: + // should be false eg black + if actual.R != 0 || actual.G != 0 || actual.B != 0 { + t.Errorf("failed to roundtrip color: expected %v but got %v", expected, actual) + } + } + } +} + +// 128x128 +var rprofile = []byte{ + 0x00, 0x00, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, + 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, 0x00, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x00, + 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE8, 0x17, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xC0, + 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC, 0x3F, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x00, 0x00, 0x00, 0x00, 0xBF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x01, 0xF8, 0x00, 0x5F, 0xFF, 0xFF, 0xFC, 0x00, 0x02, 0x80, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x03, 0xFE, 0x03, 0xFF, 0xFD, 0xBF, 0xFF, 0x80, 0x1F, 0xE0, 0x3F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x03, 0xFE, 0x01, 0xFF, 0xF7, 0x6B, 0xFF, 0x80, 0x1F, 0xC0, 0x1F, 0xFF, 0xFE, 0x02, 0xFF, 0xFC, 0x03, 0xDF, 0x17, 0xFA, 0x00, 0x00, 0x37, 0xF0, 0x3F, 0xE0, 0x1F, 0xFF, 0xD0, + 0x00, 0x07, 0xFC, 0x07, 0x07, 0xBF, 0x00, 0x00, 0x00, 0x01, 0xFE, 0xF8, 0x78, 0x3F, 0xF8, 0x00, 0x00, 0x01, 0xFC, 0x06, 0x1B, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xEA, 0x78, 0x1F, 0x80, 0x00, + 0x00, 0x01, 0xFC, 0x03, 0x1B, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xE2, 0x68, 0x1F, 0xC0, 0x00, 0x00, 0x01, 0xFC, 0x07, 0x5B, 0xE8, 0x00, 0x00, 0x00, 0x00, 0x07, 0xC4, 0x38, 0x1F, 0x80, 0x00, + 0x00, 0x01, 0xF8, 0x03, 0x3F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7, 0x78, 0x3F, 0xC0, 0x00, 0x00, 0x01, 0xFC, 0x03, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x68, 0x1F, 0x80, 0x00, + 0x00, 0x01, 0xFC, 0x07, 0x9F, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFC, 0x70, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x03, 0x9E, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x3E, 0xE8, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x01, 0xFF, 0xFF, 0xE0, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xE0, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x01, 0xFF, 0x55, 0xF8, 0x00, 0x00, 0x03, 0xEA, 0xFF, 0xC0, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x01, 0xFE, 0xAF, 0xF0, 0x00, 0x00, 0x03, 0xFD, 0xBF, 0xE0, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x01, 0xF8, 0x00, 0x7C, 0x00, 0x00, 0x07, 0x80, 0x03, 0xE0, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x03, 0xC1, 0xE0, 0x3E, 0x00, 0x00, 0x1F, 0x03, 0xC0, 0xF0, 0x1F, 0x80, 0x00, 0x00, 0x00, 0xF8, 0x07, 0x82, 0xF8, 0x0F, 0x00, 0x00, 0x3E, 0x05, 0xE0, 0x7C, 0x1F, 0x80, 0x00, + 0x00, 0x01, 0xFC, 0x07, 0x01, 0xF8, 0x17, 0x00, 0x00, 0x3C, 0x09, 0xE0, 0x78, 0x1F, 0xC0, 0x00, 0x00, 0x03, 0xFC, 0x0F, 0x06, 0xFC, 0x07, 0x80, 0x00, 0x7C, 0x1D, 0xE0, 0x3C, 0x1F, 0xC0, 0x00, + 0x00, 0xFF, 0xFC, 0x1E, 0x03, 0xFC, 0x03, 0x80, 0x00, 0x78, 0x1F, 0xE0, 0x1E, 0x1F, 0xFE, 0x80, 0x2F, 0xFF, 0xFC, 0x1C, 0x07, 0xF8, 0x01, 0x80, 0x00, 0x60, 0x1F, 0xE0, 0x16, 0x1F, 0xFF, 0xF8, + 0x1F, 0xFF, 0xF8, 0x3E, 0x07, 0xFC, 0x01, 0xC0, 0x00, 0xE8, 0x1F, 0xE0, 0x1E, 0x1F, 0xFF, 0xEC, 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0xF8, 0x01, 0xC0, 0x00, 0xE0, 0x17, 0xE0, 0x07, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x38, 0x03, 0xE8, 0x00, 0xC0, 0x00, 0xC0, 0x07, 0xC0, 0x03, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x38, 0x00, 0xC0, 0x00, 0xE0, 0x00, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x78, 0x01, 0xE0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x00, 0x03, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x38, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xF8, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x68, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x70, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x01, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x78, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x68, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xF8, 0x70, 0x00, 0x00, 0x00, 0xC0, 0x01, 0xC0, 0x00, 0x00, 0x01, 0x9F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x68, 0x00, 0x00, 0x00, 0xE0, 0x01, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x38, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0x00, 0x03, 0x9F, 0xFF, 0xFE, + 0x07, 0xFF, 0xFC, 0x78, 0x00, 0x00, 0x00, 0xC0, 0x80, 0xC0, 0x00, 0x00, 0x03, 0x1F, 0xFF, 0xF8, 0x00, 0x37, 0xF8, 0x38, 0x00, 0x00, 0x01, 0xC7, 0xF0, 0xE0, 0x00, 0x00, 0x03, 0x9F, 0xFE, 0x00, + 0x00, 0x5F, 0xFC, 0x38, 0x00, 0x00, 0x01, 0xC3, 0xE8, 0xE0, 0x00, 0x00, 0x03, 0x1F, 0xFB, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x01, 0xC7, 0xF8, 0xE0, 0x00, 0x00, 0x07, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x03, 0x9F, 0xFC, 0x70, 0x00, 0x00, 0x07, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x1E, 0x00, 0x00, 0x03, 0xBF, 0xFE, 0x78, 0x00, 0x00, 0x1E, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x1E, 0x00, 0x00, 0x07, 0x9F, 0xFF, 0x78, 0x00, 0x00, 0x16, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x1E, 0x00, 0x00, 0x07, 0x73, 0xC3, 0x3C, 0x00, 0x00, 0x1E, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x1F, 0x00, 0x00, 0x1E, 0x60, 0x01, 0x9E, 0x00, 0x00, 0x3C, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x1F, 0x80, 0x00, 0x7E, 0x40, 0x01, 0x17, 0x80, 0x00, 0x7E, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x1F, 0x80, 0x00, 0x3C, 0x40, 0x01, 0x9F, 0x00, 0x00, 0x7C, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x1F, 0xC0, 0x00, 0xFC, 0x40, 0x01, 0x07, 0xC0, 0x00, 0xFC, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x1F, 0xF8, 0x0B, 0xE8, 0x7B, 0xFF, 0x81, 0xF8, 0x03, 0xFC, 0x1F, 0x80, 0x00, 0x00, 0x03, 0xF8, 0x1C, 0xFF, 0xFF, 0xC0, 0x3F, 0xFE, 0x00, 0xFF, 0xFF, 0xDE, 0x1F, 0xC0, 0x00, + 0x00, 0x05, 0xFC, 0x1C, 0xFF, 0xFF, 0x80, 0x7F, 0xDE, 0x00, 0xFF, 0x7F, 0xDC, 0x1F, 0x80, 0x00, 0x00, 0xFF, 0xFC, 0x1C, 0x3F, 0xFE, 0x00, 0x0E, 0xB8, 0x00, 0x3F, 0xFF, 0x1C, 0x1F, 0xFC, 0x00, + 0x2F, 0xFF, 0xF8, 0x1C, 0x00, 0x00, 0x00, 0x04, 0x98, 0x00, 0x01, 0x00, 0x1E, 0x1F, 0xFF, 0xF4, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x06, 0x98, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x1C, 0x00, 0x00, 0x00, 0x06, 0x98, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x02, 0xB8, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x1C, 0x00, 0x00, 0x00, 0x03, 0xE8, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, 0x7F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, + 0x7F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFE, + 0x7F, 0xFF, 0xFC, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x1F, 0xFF, 0xFE, 0x0B, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xF8, + 0x00, 0x7F, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFE, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xC0, 0x00, + 0x00, 0x03, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x38, 0x15, 0xB7, 0x80, 0x00, 0x00, 0x3E, 0x00, 0x00, 0x1C, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xF8, 0x3C, 0x16, 0xAB, 0x00, 0x00, 0x00, 0x52, 0x00, 0x00, 0x16, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x3F, 0xFB, 0x80, 0x00, 0x00, 0xFF, 0x80, 0x00, 0x1C, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x3C, 0x1F, 0xE9, 0x00, 0x00, 0x01, 0xF7, 0x80, 0x00, 0x1E, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x3C, 0x03, 0x82, 0x1D, 0xC6, 0x39, 0xC0, 0x07, 0x00, 0x14, 0x1F, 0xC0, 0x00, + 0x00, 0x01, 0xFC, 0x3C, 0x03, 0x87, 0x14, 0x85, 0x15, 0xC1, 0x03, 0x80, 0x1E, 0x1F, 0x80, 0x00, 0x00, 0x01, 0xFC, 0x3C, 0x03, 0x87, 0x9F, 0xE6, 0x3D, 0xC0, 0x1F, 0xC0, 0x1C, 0x1F, 0xC0, 0x00, + 0x00, 0xBF, 0xF8, 0x3C, 0x03, 0x83, 0x9F, 0xE7, 0x3F, 0x8D, 0x1E, 0xC0, 0x16, 0x1F, 0xD4, 0x00, 0x17, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9E, 0xE7, 0x79, 0xD7, 0xBC, 0xE0, 0x1C, 0x1F, 0xFF, 0xE8, + 0x1F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9C, 0xE3, 0x3F, 0x9F, 0xBC, 0xE0, 0x1E, 0x1F, 0xFF, 0xD0, 0x3F, 0xFF, 0xF8, 0x3C, 0x03, 0x83, 0x9E, 0xE7, 0x79, 0xC7, 0xBC, 0xE0, 0x16, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0xBC, 0xE3, 0xF9, 0xC3, 0xBC, 0xE0, 0x1C, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9E, 0xEB, 0xE9, 0xE3, 0xBD, 0xE0, 0x1E, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xF8, 0x3C, 0x03, 0x83, 0x9C, 0xE3, 0xF1, 0xC3, 0x9C, 0xC0, 0x1C, 0x1F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0x9E, 0xE9, 0xE0, 0xFF, 0x9F, 0xC0, 0x16, 0x1F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x03, 0x83, 0xBC, 0x61, 0xE0, 0xFF, 0x07, 0xC0, 0x1E, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x1C, 0x3F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xF8, 0x3C, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x00, 0x00, 0x00, 0x1E, 0x1F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x07, 0xC0, 0x00, 0x00, 0x00, 0x1C, 0x1F, 0xFF, 0xFC, + 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x17, 0xC0, 0x00, 0x00, 0x00, 0x16, 0x3F, 0xFF, 0xFE, 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x3F, 0xFF, 0xFE, + 0x3F, 0xFF, 0xFC, 0x3C, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x3F, 0xFF, 0xFC, 0x3F, 0xFF, 0xFE, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x7F, 0xFF, 0xFE, + 0x2F, 0xFF, 0xFF, 0xBC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x88, 0x80, 0x5F, 0xFF, 0xFF, 0xF8, 0x00, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, + 0x01, 0x3F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xA0, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, + 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, + 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +} + +func TestImageFromBytesMonochrome(t *testing.T) { + image := pixel.NewImageFromBytes[pixel.Monochrome](128, 128, rprofile) + if width, height := image.Size(); width != 128 && height != 128 { + t.Errorf("image.Size(): expected 128, 128 but got %d, %d", width, height) + } + + raw := image.RawBuffer() + for i, b := range raw { + if b != rprofile[i] { + t.Fatalf("failed to roundtrip image. expected %v but got %v", rprofile[i], b) + } + } +} + +// Test pixel formats by filling them with noise and checking whether they +// contain the same data afterwards. +func TestImageNoise(t *testing.T) { + t.Run("RGB888", func(t *testing.T) { + testImageNoiseN[pixel.RGB888](t) + }) + t.Run("RGB565BE", func(t *testing.T) { + testImageNoiseN[pixel.RGB565BE](t) + }) + t.Run("RGB555", func(t *testing.T) { + testImageNoiseN[pixel.RGB555](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) + }) +} + +// Run the testImageNoise multiple times, because a single test might not catch +// all bugs (since the test uses random data). +func testImageNoiseN[T pixel.Color](t *testing.T) { + for i := 0; i < 10; i++ { + testImageNoise[T](t) + } +} + +func testImageNoise[T pixel.Color](t *testing.T) { + // Create an image of a random width/height for extra testing. + width := rand.Int()%500 + 10 + height := rand.Int()%500 + 10 + t.Log("image size:", width, height) + + // Create two images: the to-be-tested image object and a reference image. + img := pixel.NewImage[T](width, height) + ref := goimage.NewRGBA(goimage.Rect(0, 0, width, height)) + + // Fill the two images with noise. + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + // Set a random color in both images. + c := pixel.NewColor[T](uint8(rand.Uint32()), uint8(rand.Uint32()), uint8(rand.Uint32())) + img.Set(x, y, c) + ref.Set(x, y, c.RGBA()) + } + } + + // Compare the two images. They should match. + mismatch := 0 + firstX := 0 + firstY := 0 + var firstExpected, firstActual color.RGBA + for y := 0; y < height; y++ { + for x := 0; x < width; x++ { + c := img.Get(x, y).RGBA() + r2, g2, b2, _ := ref.At(x, y).RGBA() + c2 := color.RGBA{R: uint8(r2 >> 8), G: uint8(g2 >> 8), B: uint8(b2 >> 8), A: 255} + if c != c2 { + mismatch++ + if mismatch == 1 { + firstX = x + firstY = y + firstExpected = c + firstActual = c2 + } + } + } + } + if mismatch != 0 { + t.Errorf("mismatch found: %d pixels are different (first diff at (%d, %d), expected %v, actual %v)", mismatch, firstX, firstY, firstExpected, firstActual) + } +} diff --git a/pixel/pixel.go b/pixel/pixel.go index 17db4f3..de069d3 100644 --- a/pixel/pixel.go +++ b/pixel/pixel.go @@ -14,9 +14,9 @@ import ( // Pixel with a particular color, matching the underlying hardware of a // particular display. Each pixel is at least 1 byte in size. -// The color format is sRGB (or close to it) in all cases. +// The color format is sRGB (or close to it) in all cases except for 1-bit. type Color interface { - RGB888 | RGB565BE | RGB555 | RGB444BE + RGB888 | RGB565BE | RGB555 | RGB444BE | Grayscale2bit | Monochrome BaseColor } @@ -50,6 +50,10 @@ 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: panic("unknown color format") } @@ -159,9 +163,9 @@ func (c RGB555) BitsPerPixel() int { func (c RGB555) RGBA() color.RGBA { color := color.RGBA{ - R: uint8(c>>10) << 3, - G: uint8(c>>5) << 3, - B: uint8(c) << 3, + R: (uint8(c) & 0x1F) << 3, + G: (uint8(c>>5) & 0x1F) << 3, + B: (uint8(c>>10) & 0x1F) << 3, A: 255, } // Correct color rounding, so that 0xff roundtrips back to 0xff. @@ -202,6 +206,63 @@ 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: 0–63 => 0, 64–127 => 1, 128–191 => 2, 192–255 => 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 (0–255) 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 { + // Very simple black/white split. + // This isn't very accurate (especially for sRGB colors) but is close enough. + if int(r)+int(g)+int(b) > 128*3 { // light, convert to white + return Monochrome(true) + } + // dark, convert to black + return Monochrome(false) +} + +func (c Monochrome) BitsPerPixel() int { + return 1 +} + +func (c Monochrome) RGBA() color.RGBA { + value := uint8(0) + if c { + value = 255 + } + return color.RGBA{ + R: value, + G: value, + B: value, + A: 255, + } +} + // Gamma brightness lookup table: // https://victornpb.github.io/gamma-table-generator // gamma = 0.45 steps = 256 range = 0-255 diff --git a/rtl8720dn/rtl8720dn.go b/rtl8720dn/rtl8720dn.go index be60f2a..6493f06 100644 --- a/rtl8720dn/rtl8720dn.go +++ b/rtl8720dn/rtl8720dn.go @@ -17,6 +17,7 @@ import ( "sync" "time" + "tinygo.org/x/drivers" "tinygo.org/x/drivers/netdev" "tinygo.org/x/drivers/netlink" ) @@ -27,13 +28,13 @@ var _debug debug = debugBasic //var _debug debug = debugBasic | debugNetdev | debugRpc var ( - version = "0.0.1" driverName = "Realtek rtl8720dn Wifi network device driver (rtl8720dn)" ) const ( - O_NONBLOCK = 1 // note: different value than syscall.O_NONBLOCK (0x800) - RTW_MODE_STA = 0x00000001 + O_NONBLOCK = 1 // note: different value than syscall.O_NONBLOCK (0x800) + RTW_MODE_STA = 0x00000001 + defaultChannel = 6 ) type sock int32 @@ -135,6 +136,51 @@ func (r *rtl8720dn) connectToAP() error { return r.startDhcpc() } +func (r *rtl8720dn) startDhcps() error { + if result := r.rpc_tcpip_adapter_dhcps_start(0); result == -1 { + return netdev.ErrStartingDHCPServer + } + return nil +} + +func (r *rtl8720dn) startAP() error { + if len(r.params.Ssid) == 0 { + return netlink.ErrMissingSSID + } + + if len(r.params.Passphrase) != 0 && len(r.params.Passphrase) < 8 { + return netlink.ErrShortPassphrase + } + + if debugging(debugBasic) { + fmt.Printf("Starting Wifi AP as SSID '%s'...", r.params.Ssid) + } + + // Start the connection process + securityType := uint32(0) // RTW_SECURITY_OPEN + if len(r.params.Passphrase) != 0 { + securityType = 0x00400004 // RTW_SECURITY_WPA2_AES_PSK + } + + result := r.rpc_wifi_start_ap(r.params.Ssid, r.params.Passphrase, securityType, defaultChannel) + if result != 0 { + if debugging(debugBasic) { + fmt.Printf("FAILED\r\n") + } + return netlink.ErrConnectFailed + } + + if debugging(debugBasic) { + fmt.Printf("LISTENING\r\n") + } + + if r.notifyCb != nil { + r.notifyCb(netlink.EventNetUp) + } + + return r.startDhcps() +} + func (r *rtl8720dn) showDriver() { if r.driverShown { return @@ -142,7 +188,7 @@ func (r *rtl8720dn) showDriver() { if debugging(debugBasic) { fmt.Printf("\r\n") fmt.Printf("%s\r\n\r\n", driverName) - fmt.Printf("Driver version : %s\r\n", version) + fmt.Printf("Driver version : %s\r\n", drivers.Version) } r.driverShown = true } @@ -246,14 +292,27 @@ func (r *rtl8720dn) netConnect(reset bool) error { } r.showDevice() +retry: for i := 0; r.params.Retries == 0 || i < r.params.Retries; i++ { - if err := r.connectToAP(); err != nil { - if err == netlink.ErrConnectFailed { - continue + switch r.params.ConnectMode { + case netlink.ConnectModeAP: + if err := r.startAP(); err != nil { + if err == netlink.ErrConnectFailed { + continue + } + return err } - return err + break retry + + default: + if err := r.connectToAP(); err != nil { + if err == netlink.ErrConnectFailed { + continue + } + return err + } + break retry } - break } if r.networkDown() { diff --git a/scd4x/registers.go b/scd4x/registers.go index af057b4..fb25864 100644 --- a/scd4x/registers.go +++ b/scd4x/registers.go @@ -22,4 +22,5 @@ const ( CmdStartLowPowerPeriodicMeasurement = 0x21AC CmdStartPeriodicMeasurement = 0x21B1 CmdStopPeriodicMeasurement = 0x3F86 + CmdMeasureSingleShot = 0x219D ) diff --git a/scd4x/scd4x.go b/scd4x/scd4x.go index e605254..a53bb9d 100644 --- a/scd4x/scd4x.go +++ b/scd4x/scd4x.go @@ -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 { diff --git a/seesaw/encoder.go b/seesaw/encoder.go new file mode 100644 index 0000000..dbb7222 --- /dev/null +++ b/seesaw/encoder.go @@ -0,0 +1,49 @@ +package seesaw + +import ( + "errors" +) + +var errInvalidEncoderNumber = errors.New("invalid encoder choice, 0-15 are supported") + +// GetEncoderPosition returns the absolute position (or delta since the previous call) of the specified rotary encoder. +func (d *Device) GetEncoderPosition(encoder uint, asDelta bool) (int32, error) { + if encoder >= 16 { + return 0, errInvalidEncoderNumber + } + + // The function address' upper nibble is the function, the lower nibble selects which encoder to communicate with + fnAddr := FunctionAddress(encoder) + if asDelta { + fnAddr |= FunctionEncoderDelta + } else { + fnAddr |= FunctionEncoderPosition + } + + var buf [4]byte + err := d.Read(ModuleEncoderBase, fnAddr, buf[:]) + if err != nil { + return 0, err + } + + return int32(buf[0])<<24 | int32(buf[1])<<16 | int32(buf[2])<<8 | int32(buf[3]), nil +} + +// SetEncoderPosition calibrate's the encoder's current absolute position to be whatever the provided position is. +func (d *Device) SetEncoderPosition(encoder uint, position int32) error { + if encoder >= 16 { + return errInvalidEncoderNumber + } + + // The function address' upper nibble is the function, the lower nibble selects which encoder to communicate with + fnAddr := FunctionEncoderPosition | FunctionAddress(encoder) + + buf := [4]byte{ + byte(position >> 24), + byte(position >> 16), + byte(position >> 8), + byte(position), + } + + return d.Write(ModuleEncoderBase, fnAddr, buf[:]) +} diff --git a/seesaw/registers.go b/seesaw/registers.go index 3d1ebe7..abebdf1 100644 --- a/seesaw/registers.go +++ b/seesaw/registers.go @@ -98,3 +98,13 @@ const ( FunctionKeypadCount FunctionAddress = 0x04 FunctionKeypadFifo FunctionAddress = 0x10 ) + +// encoder module function address registers +// these are the defaults for encoder 0, change the lower nibble to address other encoders +// see the Device.GetEncoderPosition and SetEncoderPosition methods for examples. +const ( + FunctionEncoderIntenset FunctionAddress = 0x10 + FunctionEncoderIntenclr FunctionAddress = 0x20 + FunctionEncoderPosition FunctionAddress = 0x30 + FunctionEncoderDelta FunctionAddress = 0x40 +) diff --git a/servo/servo.go b/servo/servo.go index 5be92ac..e073aa1 100644 --- a/servo/servo.go +++ b/servo/servo.go @@ -1,6 +1,12 @@ package servo -import "machine" +import ( + "machine" + + "errors" +) + +var ErrInvalidAngle = errors.New("servo: invalid angle") // PWM is the interface necessary for controlling typical servo motors. type PWM interface { @@ -80,3 +86,32 @@ func (s Servo) SetMicroseconds(microseconds int16) { value := uint64(s.pwm.Top()) * uint64(microseconds) / (pwmPeriod / 1000) s.pwm.Set(s.channel, uint32(value)) } + +// SetAngle sets the angle of the servo in degrees. The angle should be between +// 0 and 180, where 0 is the minimum angle and 180 is the maximum angle. +// This function should work for most servos, but if it doesn't work for yours +// you can use SetMicroseconds directly instead. +func (s Servo) SetAngle(angle int) error { + if angle < 0 || angle > 180 { + return ErrInvalidAngle + } + + // 0° is 1000µs, 180° is 2000µs. See explanation in SetMicroseconds. + microseconds := angle*1000/180 + 1000 + s.SetMicroseconds(int16(microseconds)) + + return nil +} + +// SetAngleWithMicroseconds sets the angle of the servo in degrees. The angle should be between +// 0 and 180, where 0 is the minimum angle and 180 is the maximum angle. +// The high duration can be customized +// 0° is lowMicroseconds(us), 180° is highMicroseconds(us) +func (s Servo) SetAngleWithMicroseconds(angle int, lowMicroseconds, highMicroseconds int) error { + if angle < 0 || angle > 180 { + return ErrInvalidAngle + } + microseconds := lowMicroseconds + (highMicroseconds-lowMicroseconds)*angle/180 + s.SetMicroseconds(int16(microseconds)) + return nil +} diff --git a/sharpmem/sharpmem.go b/sharpmem/sharpmem.go new file mode 100644 index 0000000..45f965b --- /dev/null +++ b/sharpmem/sharpmem.go @@ -0,0 +1,374 @@ +package sharpmem + +import ( + "errors" + "image/color" + + "tinygo.org/x/drivers" +) + +const ( + bitWriteCmd uint8 = 0b00000001 + bitVcom uint8 = 0b00000010 + bitClear uint8 = 0b00000100 +) + +var ( + ConfigLS010B7DH04 = Config{Width: 128, Height: 128} + ConfigLS011B7DH03 = Config{Width: 160, Height: 68} + ConfigLS012B7DD01 = Config{Width: 184, Height: 38} + ConfigLS013B7DH03 = ConfigLS010B7DH04 + ConfigLS013B7DH05 = Config{Width: 144, Height: 168} + ConfigLS018B7DH02 = Config{Width: 230, Height: 303} + ConfigLS027B7DH01 = Config{Width: 400, Height: 240} + ConfigLS027B7DH01A = ConfigLS027B7DH01 + ConfigLS032B7DD02 = Config{Width: 336, Height: 536} + ConfigLS044Q7DH01 = Config{Width: 320, Height: 240} +) + +type Pin interface { + High() + Low() +} + +// Device represents a Sharp Memory Display device. This driver implementation +// concerns the 1-bit color versions only (black and white memory displays). +// +// Supported SKUs include: +// LS010B7DH04, LS011B7DH03, LS012B7DD01, LS013B7DH03, LS013B7DH05, +// LS018B7DH02, LS027B7DH01, LS027B7DH01A, LS032B7DD02, LS044Q7DH01 +// +// Note: Only SKU LS011B7DH03 (160x68) has been tested as of writing. +// +// The driver includes optimizations (frame and per-line invalidation) that +// only transmit the changed lines to the display. These optimizations are on +// by default, and they can be disabled with the respective config option. +type Device struct { + bus drivers.SPI + csPin Pin + buffer []byte + txBuf []byte + lineDiff []byte + width int16 + height int16 + bufferSize int16 + bytesPerLine int16 + vcom uint8 + diffing bool +} + +type Config struct { + Width int16 + Height int16 + + // DisableOptimizations disables frame and line invalidation optimizations. + // Useful if constant frame times are desired. + DisableOptimizations bool +} + +// New creates a new device connection. +// The SPI bus must have already been configured. +func New(bus drivers.SPI, csPin Pin) Device { + d := Device{ + bus: bus, + csPin: csPin, + } + return d +} + +// Configure initializes the display with specified configuration. It can be +// called multiple times on the same display, resetting its internal state. +func (d *Device) Configure(cfg Config) { + if cfg.Width == 0 { + cfg.Width = 160 + } + if cfg.Height == 0 { + cfg.Height = 68 + } + + d.width = cfg.Width + d.height = cfg.Height + d.diffing = !cfg.DisableOptimizations + + d.initialize() +} + +// initialize properly initializes the display and the in-memory image buffers. +func (d *Device) initialize() { + d.csPin.Low() + + // initialize VCOM as high + d.vcom = bitVcom + + // bytesPerLine has to be 16-bit aligned, as some resolutions require + // padding to the nearest 2nd byte. + d.bytesPerLine = ceilDiv(d.width, 16) * 2 + + // preallocate a contiguous byte buffer for all lines, including + // protocol-required padding for each line apriori (easier to transfer). + d.bufferSize = d.bytesPerLine * d.height + d.buffer = make([]byte, d.bufferSize) + // A bit being 1 is white (reflective), 0 is black (less reflective). + for i := range d.buffer { + d.buffer[i] = 0xff + } + + // auxiliary buffer for SPI transfers to avoid dynamic allocations + d.txBuf = make([]byte, 2) + + if d.diffing { + // buffer to store the changed lines. First bit is whether any line has + // changed at all (i.e. the frame is invalid), followed by N bits, + // one for each line. + d.lineDiff = make([]byte, bitfieldBufLen(1+d.height)) + } +} + +// SetPixel enables or disables a pixel in the buffer. +// color.RGBA{0, 0, 0, 255} is considered transparent (reflective, white), +// anything else will enable a pixel on the screen (make it appear less +// reflective, black). +func (d *Device) SetPixel(x, y int16, c color.RGBA) { + if d.width == 0 { + return + } + + // bounds check + if x < 0 || x >= d.width || y < 0 || y >= d.height { + return + } + + offset := y * d.bytesPerLine + + div := offset + x/8 + mod := uint8(x % 8) + + prev := hasBit(d.buffer[div], mod) + curr := c.R == 0 && c.G == 0 && c.B == 0 && c.A == 255 + + if prev == curr { + return + } + + if curr { + d.buffer[div] = setBit(d.buffer[div], mod) + } else { + d.buffer[div] = unsetBit(d.buffer[div], mod) + } + + if d.diffing { + d.invalidateLine(y) + } +} + +// Size returns the current size of the display. +func (d *Device) Size() (x, y int16) { + return d.width, d.height +} + +// Display renders the buffer to the screen. It only transmits changed lines if +// optimizations are enabled. It should be called at >=1hz, even if the +// buffer hasn't been modified. +func (d *Device) Display() error { + if d.width == 0 { + return errors.New("display not configured") + } + + if d.diffing { + if !hasBit(d.lineDiff[0], 0) { + // no pixels have been modified, simply toggle VCOM + return d.holdDisplay() + } + + defer func() { + for i := 0; i < len(d.lineDiff); i++ { + d.lineDiff[i] = 0x00 + } + }() + } + + cmd := bitWriteCmd | d.vcom + + d.toggleVcom() + + // Padding to use for high bits of line numbers that overflow 8 bits. + var hiPad = uint8(0) + if d.height >= 512 { + hiPad = 3 + 3 // 3 mode bits + 3 low bits + } else if d.height >= 256 { + hiPad = 3 + 4 // 3 mode bits + 4 low bits + } + + // start transfer + d.csPin.High() + + for i := int16(0); i < d.height; i++ { + if d.diffing { + // Skip rendering lines that haven't changed. + linediv := (i + 1) / 8 + linemod := uint8((i + 1) % 8) + if !hasBit(d.lineDiff[linediv], linemod) { + continue + } + } + + // The first 5 bits are either dummy or part of the current line + // (1-indexed) if it overflows 8-bits. + // The last 3 bits are the command for the first line and dummy bits + // for subsequent lines (set as command for simplicity) + hi := uint8((i + 1) >> 8) + hi = hi << hiPad + d.txBuf[0] = cmd | hi + + // The second byte is the low bits of the current line (1-indexed). + // for <8 bits cases, the high bits are dummy, so we leave them as 0. + d.txBuf[1] = uint8(i + 1) + + // send the first two bytes + err := d.bus.Tx(d.txBuf, nil) + if err != nil { + return err + } + + // send the line data + err = d.bus.Tx(d.buffer[i*d.bytesPerLine:(i+1)*d.bytesPerLine], nil) + if err != nil { + return err + } + } + + // Trailer 16 bits (low) + d.txBuf[0] = 0x00 + d.txBuf[1] = 0x00 + err := d.bus.Tx(d.txBuf, nil) + if err != nil { + return err + } + + // end transfer + d.csPin.Low() + + return nil +} + +// holdDisplay simply toggles VCOM without updating any lines. +func (d *Device) holdDisplay() error { + d.txBuf[0] = d.vcom + d.txBuf[1] = 0x00 + + d.toggleVcom() + + // begin transaction + d.csPin.High() + + err := d.bus.Tx(d.txBuf, nil) + if err != nil { + return err + } + + // end transaction + d.csPin.Low() + + return nil +} + +// Clear clears both the in-memory buffer and the display. +func (d *Device) Clear() error { + if d.width == 0 { + return errors.New("display not configured") + } + + d.ClearBuffer() + return d.ClearDisplay() +} + +// ClearBuffer clears the in-memory buffer. The display is not updated. +func (d *Device) ClearBuffer() { + if d.width == 0 { + return + } + + if d.diffing { + // detect what rows need to be reset on the next render + d.invalidateModifiedLines() + } + + // reset the in-memory buffer + for i := 0; i < len(d.buffer); i++ { + d.buffer[i] = 0xff + } +} + +// invalidateModifiedLines marks any line that has at least a single black pixel +// as invalidated. Padding bits, if any, are always 1. +func (d *Device) invalidateModifiedLines() { + for y := int16(0); y < d.height; y++ { + offset := y * d.bytesPerLine + + updateLine := false + for x := int16(0); x < d.width; x++ { + div := offset + x/8 + mod := uint8(x % 8) + + if !hasBit(d.buffer[div], mod) { + updateLine = true + break + } + } + + if updateLine { + d.invalidateLine(y) + } + } +} + +// ClearDisplay clears the display. The in-memory buffer is not updated. A +// subsequent call to Display() will re-render the content as it was before +// clearing. +func (d *Device) ClearDisplay() error { + if d.width == 0 { + return errors.New("display not configured") + } + + d.txBuf[0] = d.vcom | bitClear + d.txBuf[1] = 0x00 + + d.toggleVcom() + + // begin transaction + d.csPin.High() + + err := d.bus.Tx(d.txBuf, nil) + if err != nil { + return err + } + + // end transaction + d.csPin.Low() + + return nil +} + +// invalidateLine marks a line and the frame itself as invalidated. +func (d *Device) invalidateLine(line int16) { + // mark the frame as invalidated + d.lineDiff[0] = setBit(d.lineDiff[0], 0) + + // mark the line as invalidated + linediv := (line + 1) / 8 + linemod := uint8((line + 1) % 8) + d.lineDiff[linediv] = setBit(d.lineDiff[linediv], linemod) +} + +// toggleVcom toggles the VCOM, as is instructed by the datasheet. +// Toggling VCOM can help maintain the display's longevity. It should ideally +// be called at least once per second, preferably at 4-100 Hz. +// Toggling VCOM causes a tiny bit of flicker, but without it the pixels can +// be permanently damaged by the DC bias accumulating over time. +func (d *Device) toggleVcom() { + if d.vcom != 0 { + d.vcom = 0x00 + } else { + d.vcom = bitVcom + } +} diff --git a/sharpmem/sharpmem_test.go b/sharpmem/sharpmem_test.go new file mode 100644 index 0000000..43a9645 --- /dev/null +++ b/sharpmem/sharpmem_test.go @@ -0,0 +1,225 @@ +package sharpmem + +import ( + "image/color" + "math/rand/v2" + "testing" + + qt "github.com/frankban/quicktest" +) + +func Test_setBit(t *testing.T) { + c := qt.New(t) + + for i := uint8(0); i < 8; i++ { + v := uint8(1) << i + + c.Assert(setBit(0x00, i), qt.Equals, v) + c.Assert(setBit(0x00, (i+1)%8), qt.Not(qt.Equals), v) + } +} + +func Test_unsetBit(t *testing.T) { + c := qt.New(t) + + for i := uint8(0); i < 8; i++ { + v := uint8(1) << i + + c.Assert(unsetBit(v, i), qt.Equals, uint8(0x00)) + c.Assert(unsetBit(v, (i+1)%8), qt.Not(qt.Equals), uint8(0x00)) + } +} + +func Test_hasBit(t *testing.T) { + c := qt.New(t) + + for i := uint8(0); i < 8; i++ { + v := uint8(1) << i + + c.Assert(hasBit(v, i), qt.Equals, true) + c.Assert(hasBit(v, (i+1)%8), qt.Equals, false) + } +} + +func Test_bitfieldBufLen(t *testing.T) { + c := qt.New(t) + + for i := int16(1); i < 536; i++ { + requiredBufferSize := i / 8 + wouldOverflow := i % 8 + + if wouldOverflow > 0 { + requiredBufferSize += 1 + } + + c.Assert(bitfieldBufLen(i), qt.Equals, requiredBufferSize) + } +} + +type mockBus struct { + b []byte +} + +func (m *mockBus) Tx(w, _ []byte) error { + m.b = append(m.b, w...) + return nil +} + +func (m *mockBus) Transfer(b byte) (byte, error) { + m.b = append(m.b, b) + return 0x00, nil +} + +type mockPin struct{} + +func (m mockPin) High() { +} + +func (m mockPin) Low() { +} + +func Test_Device(t *testing.T) { + c := qt.New(t) + + cfgs := []Config{ + ConfigLS010B7DH04, + ConfigLS011B7DH03, + ConfigLS012B7DD01, + ConfigLS013B7DH03, + ConfigLS013B7DH05, + ConfigLS018B7DH02, + ConfigLS027B7DH01, + ConfigLS027B7DH01A, + ConfigLS032B7DD02, + ConfigLS044Q7DH01, + } + + cfgLen := len(cfgs) + for i := 0; i < cfgLen; i++ { + cfgs = append(cfgs, Config{ + Width: cfgs[i].Width, + Height: cfgs[i].Height, + DisableOptimizations: true, + }) + } + + spi := &mockBus{} + pin := mockPin{} + display := New(spi, pin) + + for _, cfg := range cfgs { + display.Configure(cfg) + + x, y := display.Size() + c.Assert(x, qt.Equals, cfg.Width) + c.Assert(y, qt.Equals, cfg.Height) + + for i := 0; i < 10; i++ { + x := int16(rand.IntN(int(cfg.Width))) + y := int16(rand.IntN(int(cfg.Height))) + display.SetPixel(x, y, color.RGBA{R: 255, G: 255, B: 255, A: 255}) + } + + for i := 0; i < 10; i++ { + x := int16(rand.IntN(int(cfg.Width))) + y := int16(rand.IntN(int(cfg.Height))) + display.SetPixel(x, y, color.RGBA{R: 0, G: 0, B: 0, A: 255}) + } + + err := display.Display() + c.Assert(err, qt.Equals, nil) + + err = display.ClearDisplay() + c.Assert(err, qt.Equals, nil) + + display.ClearBuffer() + } +} + +func Test_HiPad(t *testing.T) { + c := qt.New(t) + + spi := &mockBus{} + pin := mockPin{} + display := New(spi, pin) + + t.Run("LS011B7DH03, 8-bit address", func(t *testing.T) { + t.Cleanup(func() { + spi.b = nil + }) + + display.Configure(ConfigLS011B7DH03) + + display.SetPixel(0, display.height-1, color.RGBA{R: 255, G: 255, B: 255, A: 255}) + err := display.Display() + c.Assert(err, qt.Equals, nil) + + // 160 perfectly divisible by 16, so 20 bytes of pixel data + c.Assert(spi.b, qt.HasLen, 2+20+2) + + // line is 1-indexed on the wire (67+1) + // 68 in binary + // 0b01000100 + + // DDDDDMMM + c.Assert(spi.b[0], qt.Equals, uint8(0b00000011)) // mode 1, vcom is high on first run + + c.Assert(spi.b[1], qt.Equals, uint8(0b01000100)) // the actual address + }) + + t.Run("LS018B7DH02, 9-bit address", func(t *testing.T) { + t.Cleanup(func() { + spi.b = nil + }) + + display.Configure(ConfigLS018B7DH02) + + display.SetPixel(0, display.height-1, color.RGBA{R: 255, G: 255, B: 255, A: 255}) + err := display.Display() + c.Assert(err, qt.Equals, nil) + + // 2 first bytes command+address + // 230 bits are not divisible by 16, 240 is (15*16), so 30 bytes for line data + // 2 trailing bytes + c.Assert(spi.b, qt.HasLen, 2+30+2) + + // line is 1-indexed on the wire (302+1) + // 303 in binary (split in 2 bytes) + // R + // 0b00000001 0b00101111 + // ^ + + // RDDDDMMM + c.Assert(spi.b[0], qt.Equals, uint8(0b10000011)) // mode 1, vcom is high on first run + // ^ + + c.Assert(spi.b[1], qt.Equals, uint8(0b00101111)) // rest of the address (low 8 bits) + }) + + t.Run("LS032B7DD02, 10-bit address", func(t *testing.T) { + t.Cleanup(func() { + spi.b = nil + }) + + display.Configure(ConfigLS032B7DD02) + + display.SetPixel(0, display.height-1, color.RGBA{R: 255, G: 255, B: 255, A: 255}) + err := display.Display() + c.Assert(err, qt.Equals, nil) + + c.Assert(spi.b, qt.HasLen, 2+336/8+2) // 2 command+address, width / 2, 2 trailing bytes + + // line is 1-indexed on the wire (535+1) + // 536 in binary (split in 2 bytes) + // RR + // 0b00000010 0b00011000 + // ^^ + + // RRDDDMMM + c.Assert(spi.b[0], qt.Equals, uint8(0b10000011)) // mode 1, vcom is high on first run + // ^^ + + c.Assert(spi.b[1], qt.Equals, uint8(0b00011000)) // rest of the address (low 8 bits) + }) + +} diff --git a/sharpmem/util.go b/sharpmem/util.go new file mode 100644 index 0000000..8c637da --- /dev/null +++ b/sharpmem/util.go @@ -0,0 +1,30 @@ +package sharpmem + +// setBit sets the bit at pos in n to 1 and returns the updated number. +func setBit(n uint8, pos uint8) uint8 { + n |= 1 << pos + return n +} + +// unsetBit sets the bit at pos in n to 0 and returns the updated number. +func unsetBit(n uint8, pos uint8) uint8 { + n &^= 1 << pos + return n +} + +// hasBit returns whether the bit at pos in n is 1. +func hasBit(n uint8, pos uint8) bool { + n = n & (1 << pos) + return n > 0 +} + +// bitfieldBufLen returns the required buffer size for keeping track of +// changed lines. +func bitfieldBufLen(bits int16) int16 { + return 1 + (bits-1)/8 +} + +// ceilDiv divides a with b, but it uses the ceiling if modulo is not 0. +func ceilDiv(a, b int16) int16 { + return 1 + (a-1)/b +} diff --git a/si5351/registers.go b/si5351/registers.go new file mode 100644 index 0000000..8241202 --- /dev/null +++ b/si5351/registers.go @@ -0,0 +1,158 @@ +package si5351 + +// The I2C address which this device listens to. +const AddressDefault = 0x60 // Assumes ADDR pin is low +const AddressAlternative = 0x61 // Assumes ADDR pin is high + +const ( + XTAL_FREQ = 25000000 + PLL_FIXED = 80000000000 + FREQ_MULT = 100 + DEFAULT_CLK = 1000000000 + + PLL_VCO_MIN = 600000000 + PLL_VCO_MAX = 900000000 + MULTISYNTH_MIN_FREQ = 500000 + MULTISYNTH_DIVBY4_FREQ = 150000000 + MULTISYNTH_MAX_FREQ = 225000000 + MULTISYNTH_SHARE_MAX = 100000000 + MULTISYNTH_SHARE_MIN = 1024000 + MULTISYNTH67_MAX_FREQ = MULTISYNTH_DIVBY4_FREQ + CLKOUT_MIN_FREQ = 4000 + CLKOUT_MAX_FREQ = MULTISYNTH_MAX_FREQ + CLKOUT67_MS_MIN = PLL_VCO_MIN / MULTISYNTH67_A_MAX + CLKOUT67_MIN_FREQ = CLKOUT67_MS_MIN / 128 + CLKOUT67_MAX_FREQ = MULTISYNTH67_MAX_FREQ + + PLL_A_MIN = 15 + PLL_A_MAX = 90 + PLL_B_MAX = PLL_C_MAX - 1 + PLL_C_MAX = 1048575 + MULTISYNTH_A_MIN = 6 + MULTISYNTH_A_MAX = 1800 + MULTISYNTH67_A_MAX = 254 + MULTISYNTH_B_MAX = MULTISYNTH_C_MAX - 1 + MULTISYNTH_C_MAX = 1048575 + MULTISYNTH_P1_MAX = (1 << 18) - 1 + MULTISYNTH_P2_MAX = (1 << 20) - 1 + MULTISYNTH_P3_MAX = (1 << 20) - 1 + VCXO_PULL_MIN = 30 + VCXO_PULL_MAX = 240 + VCXO_MARGIN = 103 + + DEVICE_STATUS = 0 + INTERRUPT_STATUS = 1 + INTERRUPT_MASK = 2 + STATUS_SYS_INIT = 1 << 7 + STATUS_LOL_B = 1 << 6 + STATUS_LOL_A = 1 << 5 + STATUS_LOS = 1 << 4 + OUTPUT_ENABLE_CTRL = 3 + OEB_PIN_ENABLE_CTRL = 9 + PLL_INPUT_SOURCE = 15 + CLKIN_DIV_MASK = 3 << 6 + CLKIN_DIV_1 = 0 << 6 + CLKIN_DIV_2 = 1 << 6 + CLKIN_DIV_4 = 2 << 6 + CLKIN_DIV_8 = 3 << 6 + PLLB_SOURCE = 1 << 3 + PLLA_SOURCE = 1 << 2 + + CLK0_CTRL = 16 + CLK1_CTRL = 17 + CLK2_CTRL = 18 + CLK3_CTRL = 19 + CLK4_CTRL = 20 + CLK5_CTRL = 21 + CLK6_CTRL = 22 + CLK7_CTRL = 23 + CLK_POWERDOWN = 1 << 7 + CLK_INTEGER_MODE = 1 << 6 + CLK_PLL_SELECT = 1 << 5 + CLK_INVERT = 1 << 4 + CLK_INPUT_MASK = 3 << 2 + CLK_INPUT_XTAL = 0 << 2 + CLK_INPUT_CLKIN = 1 << 2 + CLK_INPUT_MULTISYNTH_0_4 = 2 << 2 + CLK_INPUT_MULTISYNTH_N = 3 << 2 + CLK_DRIVE_STRENGTH_MASK = 3 << 0 + CLK_DRIVE_STRENGTH_2MA = 0 << 0 + CLK_DRIVE_STRENGTH_4MA = 1 << 0 + CLK_DRIVE_STRENGTH_6MA = 2 << 0 + CLK_DRIVE_STRENGTH_8MA = 3 << 0 + + CLK3_0_DISABLE_STATE = 24 + CLK7_4_DISABLE_STATE = 25 + CLK_DISABLE_STATE_MASK = 3 + CLK_DISABLE_STATE_LOW = 0 + CLK_DISABLE_STATE_HIGH = 1 + CLK_DISABLE_STATE_FLOAT = 2 + CLK_DISABLE_STATE_NEVER = 3 + + PARAMETERS_LENGTH = 8 + PLLA_PARAMETERS = 26 + PLLB_PARAMETERS = 34 + CLK0_PARAMETERS = 42 + CLK1_PARAMETERS = 50 + CLK2_PARAMETERS = 58 + CLK3_PARAMETERS = 66 + CLK4_PARAMETERS = 74 + CLK5_PARAMETERS = 82 + CLK6_PARAMETERS = 90 + CLK7_PARAMETERS = 91 + CLK6_7_OUTPUT_DIVIDER = 92 + OUTPUT_CLK_DIV_MASK = 7 << 4 + OUTPUT_CLK6_DIV_MASK = 7 << 0 + OUTPUT_CLK_DIV_SHIFT = 4 + OUTPUT_CLK_DIV6_SHIFT = 0 + OUTPUT_CLK_DIV_1 = 0 + OUTPUT_CLK_DIV_2 = 1 + OUTPUT_CLK_DIV_4 = 2 + OUTPUT_CLK_DIV_8 = 3 + OUTPUT_CLK_DIV_16 = 4 + OUTPUT_CLK_DIV_32 = 5 + OUTPUT_CLK_DIV_64 = 6 + OUTPUT_CLK_DIV_128 = 7 + OUTPUT_CLK_DIVBY4 = 3 << 2 + + SSC_PARAM0 = 149 + SSC_PARAM1 = 150 + SSC_PARAM2 = 151 + SSC_PARAM3 = 152 + SSC_PARAM4 = 153 + SSC_PARAM5 = 154 + SSC_PARAM6 = 155 + SSC_PARAM7 = 156 + SSC_PARAM8 = 157 + SSC_PARAM9 = 158 + SSC_PARAM10 = 159 + SSC_PARAM11 = 160 + SSC_PARAM12 = 161 + + VXCO_PARAMETERS_LOW = 162 + VXCO_PARAMETERS_MID = 163 + VXCO_PARAMETERS_HIGH = 164 + + CLK0_PHASE_OFFSET = 165 + CLK1_PHASE_OFFSET = 166 + CLK2_PHASE_OFFSET = 167 + CLK3_PHASE_OFFSET = 168 + CLK4_PHASE_OFFSET = 169 + CLK5_PHASE_OFFSET = 170 + + PLL_RESET = 177 + PLL_RESET_B = 1 << 7 + PLL_RESET_A = 1 << 5 + + CRYSTAL_LOAD = 183 + CRYSTAL_LOAD_MASK = 3 << 6 + CRYSTAL_LOAD_0PF = 0 << 6 + CRYSTAL_LOAD_6PF = 1 << 6 + CRYSTAL_LOAD_8PF = 2 << 6 + CRYSTAL_LOAD_10PF = 3 << 6 + + FANOUT_ENABLE = 187 + CLKIN_ENABLE = 1 << 7 + XTAL_ENABLE = 1 << 6 + MULTISYNTH_ENABLE = 1 << 4 +) diff --git a/si5351/si5351.go b/si5351/si5351.go new file mode 100644 index 0000000..3edecd7 --- /dev/null +++ b/si5351/si5351.go @@ -0,0 +1,1069 @@ +package si5351 + +import ( + "encoding/binary" + "errors" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/regmap" +) + +// Device wraps an I2C connection to a SI5351 device. +type Device struct { + bus drivers.I2C + Address uint8 + + rw regmap.Device8I2C + initialized bool + crystalFreq [2]CrystalFrequency + pllaRefOsc PLLReferenceOscillator + pllbRefOsc PLLReferenceOscillator + clkinDiv uint8 + pllaFreq Frequency + pllbFreq Frequency + pllAssignment [8]PLLType + clkFreq [8]Frequency + clkFirstSet [8]bool + refCorrection [2]int32 +} + +var ( + ErrInitTimeout = errors.New("si5351: init timeout") + ErrNotInitialized = errors.New("si5351: not initialized") + ErrInvalidParameter = errors.New("si5351: invalid parameter") + ErrDeviceNotFound = errors.New("si5351: device not found") + ErrInvalidPLLClockSetting = errors.New("si5351: cannot set >100MHz with other >100MHz on same PLL") + ErrInvalidPLLDivision = errors.New("si5351: CLK6/7 requires integer division ratio") +) + +// Frequency in Hz +type Frequency uint64 + +// CrystalFrequency in Hz +type CrystalFrequency uint32 + +// CrystalLoad options +type CrystalLoad uint8 + +const ( + CrystalLoad0PF CrystalLoad = iota + CrystalLoad6PF + CrystalLoad8PF + CrystalLoad10PF +) + +// PLL identifiers +type PLLType uint8 + +const ( + PLL_A PLLType = iota + PLL_B +) + +// Reference oscillator identifiers +type PLLReferenceOscillator uint8 + +const ( + PLLInputXO PLLReferenceOscillator = iota + PLLInputClockIn +) + +// Clock output identifiers +type Clock uint8 + +const ( + Clock0 Clock = iota + Clock1 + Clock2 + Clock3 + Clock4 + Clock5 + Clock6 + Clock7 +) + +const rfracDenominator = Frequency(PLL_C_MAX) + +// RegisterSet holds PLL/multisynth register values +type RegisterSet struct { + p1 uint32 + p2 uint32 + p3 uint32 +} + +// New creates a new SI5351 connection. The I2C bus must already be configured. +func New(bus drivers.I2C) *Device { + rw := regmap.Device8I2C{} + rw.SetBus(bus, AddressDefault, binary.BigEndian) + + d := Device{ + bus: bus, + rw: rw, + Address: AddressDefault, + pllaRefOsc: PLLInputXO, + pllbRefOsc: PLLInputXO, + clkinDiv: CLKIN_DIV_1, + } + d.crystalFreq[0] = XTAL_FREQ + + return &d +} + +// Config holds configuration parameters for the SI5351. +type Config struct { + Capacitance CrystalLoad + CrystalOutput CrystalFrequency + Correction int32 +} + +// Configure initializes the SI5351 with the specified crystal load capacitance, +// reference oscillator frequency, and frequency correction. +func (d *Device) Configure(cfg Config) error { + // Check for device on bus + if err := d.bus.Tx(uint16(d.Address), []byte{}, []byte{0}); err != nil { + return ErrDeviceNotFound + } + + // Wait for SYS_INIT flag to clear + timeout := time.Now().Add(100 * time.Millisecond) + for { + status, err := d.rw.Read8(DEVICE_STATUS) + if err != nil { + return err + } + if (status >> 7) == 0 { + break + } + if time.Now().After(timeout) { + return ErrInitTimeout + } + time.Sleep(time.Millisecond) + } + + // Set crystal load capacitance + var xtalLoadC uint8 + switch cfg.Capacitance { + case CrystalLoad0PF: + xtalLoadC = CRYSTAL_LOAD_0PF + case CrystalLoad6PF: + xtalLoadC = CRYSTAL_LOAD_6PF + case CrystalLoad8PF: + xtalLoadC = CRYSTAL_LOAD_8PF + case CrystalLoad10PF: + xtalLoadC = CRYSTAL_LOAD_10PF + default: + xtalLoadC = CRYSTAL_LOAD_10PF + } + if err := d.rw.Write8(CRYSTAL_LOAD, uint8(xtalLoadC&CRYSTAL_LOAD_MASK)|0x12); err != nil { + return err + } + + // Set up the XO reference frequency + if cfg.CrystalOutput == 0 { + cfg.CrystalOutput = XTAL_FREQ + } + d.SetReferenceFrequency(PLLInputXO, cfg.CrystalOutput) + + // Set frequency calibration for XO + if err := d.SetCorrection(PLLInputXO, cfg.Correction); err != nil { + return err + } + + // Reset device + if err := d.Reset(); err != nil { + return err + } + + d.initialized = true + return nil +} + +// Reset resets the Si5351. +func (d *Device) Reset() error { + // Power down all outputs + for i := range uint8(8) { + if err := d.rw.Write8(CLK0_CTRL+i, 0x80); err != nil { + return err + } + } + + time.Sleep(100 * time.Millisecond) + + // Turn clocks back on with default settings + for i := range uint8(8) { + if err := d.rw.Write8(CLK0_CTRL+i, 0x0C); err != nil { + return err + } + } + + time.Sleep(100 * time.Millisecond) + + // Set PLLA and PLLB to 800 MHz + if err := d.SetPLL(PLL_A, PLL_FIXED); err != nil { + return err + } + if err := d.SetPLL(PLL_B, PLL_FIXED); err != nil { + return err + } + + // Make PLL to CLK assignments + for i := range 6 { + d.pllAssignment[i] = PLL_A + d.SetMultisynthSource(Clock(i), PLL_A) + } + d.pllAssignment[6] = PLL_B + d.pllAssignment[7] = PLL_B + d.SetMultisynthSource(Clock(6), PLL_B) + d.SetMultisynthSource(Clock(7), PLL_B) + + // Reset VCXO parameters + d.rw.Write8(VXCO_PARAMETERS_LOW, 0) + d.rw.Write8(VXCO_PARAMETERS_MID, 0) + d.rw.Write8(VXCO_PARAMETERS_HIGH, 0) + + // Reset PLLs + d.PLLReset(PLL_A) + d.PLLReset(PLL_B) + + // Initialize clock state + for i := range 8 { + d.clkFreq[i] = 0 + d.EnableOutput(Clock(i), false) + d.clkFirstSet[i] = false + } + + return nil +} + +// SetPLL programs the specified PLL with the given frequency. +func (d *Device) SetPLL(pll PLLType, pllFreq Frequency) error { + var refOsc PLLReferenceOscillator + var baseAddr uint8 + + switch pll { + case PLL_A: + refOsc = d.pllaRefOsc + baseAddr = PLLA_PARAMETERS + d.pllaFreq = pllFreq + case PLL_B: + refOsc = d.pllbRefOsc + baseAddr = PLLB_PARAMETERS + d.pllbFreq = pllFreq + default: + return ErrInvalidParameter + } + + _, reg := d.CalculatePLL(pll, pllFreq, d.refCorrection[refOsc], false) + + params := make([]byte, 8) + params[0] = byte((reg.p3 >> 8) & 0xFF) + params[1] = byte(reg.p3 & 0xFF) + params[2] = byte((reg.p1 >> 16) & 0x03) + params[3] = byte((reg.p1 >> 8) & 0xFF) + params[4] = byte(reg.p1 & 0xFF) + params[5] = byte(((reg.p3 >> 12) & 0xF0) | ((reg.p2 >> 16) & 0x0F)) + params[6] = byte((reg.p2 >> 8) & 0xFF) + params[7] = byte(reg.p2 & 0xFF) + + for i := range params { + if err := d.rw.Write8(baseAddr+uint8(i), params[i]); err != nil { + return err + } + } + + return nil +} + +// SetFrequency sets the clock frequency of the specified CLK output. +// Frequency range is 8 kHz to 150 MHz for CLK0-5, up to 150 MHz for CLK6-7. +func (d *Device) SetFrequency(clk Clock, freq Frequency) error { + if !d.initialized { + return ErrNotInitialized + } + + freqMult := freq * FREQ_MULT + + switch { + case clk <= 5: + return d.setFreqCLK0to5(clk, freqMult) + case clk <= 7: + return d.setFreqCLK6to7(clk, freqMult) + default: + return ErrInvalidParameter + } +} + +// SetRawFrequency sets the clock frequency of the specified CLK output without +// applying the frequency multiplier. +// Frequency range is 8 kHz to 150 MHz for CLK0-5, up to 150 MHz for CLK6-7. +func (d *Device) SetRawFrequency(clk Clock, freq Frequency) error { + if !d.initialized { + return ErrNotInitialized + } + + switch { + case clk <= 5: + return d.setFreqCLK0to5(clk, freq) + case clk <= 7: + return d.setFreqCLK6to7(clk, freq) + default: + return ErrInvalidParameter + } +} + +// SetMultisynthSource sets the PLL source for a multisynth. +func (d *Device) SetMultisynthSource(clk Clock, pll PLLType) error { + regVal, err := d.rw.Read8(CLK0_CTRL + uint8(clk)) + if err != nil { + return err + } + + switch pll { + case PLL_A: + regVal &^= CLK_PLL_SELECT + case PLL_B: + regVal |= CLK_PLL_SELECT + default: + return ErrInvalidParameter + } + + if err := d.rw.Write8(CLK0_CTRL+uint8(clk), regVal); err != nil { + return err + } + + d.pllAssignment[clk] = pll + return nil +} + +// SetCorrection sets the oscillator correction factor in parts-per-billion. +func (d *Device) SetCorrection(refOsc PLLReferenceOscillator, corr int32) error { + d.refCorrection[refOsc] = corr + + if err := d.SetPLL(PLL_A, d.pllaFreq); err != nil { + return err + } + if err := d.SetPLL(PLL_B, d.pllbFreq); err != nil { + return err + } + return nil +} + +// GetCorrection returns the oscillator correction factor in parts-per-billion. +func (d *Device) GetCorrection(refOsc PLLReferenceOscillator) int32 { + return d.refCorrection[refOsc] +} + +// PLLReset applies a reset to the indicated PLL. +func (d *Device) PLLReset(pll PLLType) error { + switch pll { + case PLL_A: + return d.rw.Write8(PLL_RESET, PLL_RESET_A) + case PLL_B: + return d.rw.Write8(PLL_RESET, PLL_RESET_B) + } + return ErrInvalidParameter +} + +// SetReferenceFrequency sets the reference frequency for the specified reference oscillator. +func (d *Device) SetReferenceFrequency(refOsc PLLReferenceOscillator, refFreq CrystalFrequency) { + switch { + case refFreq <= 30_000_000: + d.crystalFreq[refOsc] = refFreq + if refOsc == PLLInputClockIn { + d.clkinDiv = CLKIN_DIV_1 + } + case refFreq <= 60_000_000: + d.crystalFreq[refOsc] = refFreq / 2 + if refOsc == PLLInputClockIn { + d.clkinDiv = CLKIN_DIV_2 + } + case refFreq <= 100_000_000: + d.crystalFreq[refOsc] = refFreq / 4 + if refOsc == PLLInputClockIn { + d.clkinDiv = CLKIN_DIV_4 + } + } +} + +// EnableOutput enables or disables a clock output. +func (d *Device) EnableOutput(clk Clock, enable bool) error { + if clk > Clock7 { + return ErrInvalidParameter + } + + regVal, err := d.rw.Read8(OUTPUT_ENABLE_CTRL) + if err != nil { + return err + } + + if enable { + regVal &^= (1 << clk) + } else { + regVal |= (1 << clk) + } + + return d.rw.Write8(OUTPUT_ENABLE_CTRL, regVal) +} + +type DriveStrength uint8 + +const ( + DriveStrength2MA DriveStrength = iota + DriveStrength4MA + DriveStrength6MA + DriveStrength8MA +) + +// SetDriveStrength sets the drive strength of the specified clock output. +func (d *Device) SetDriveStrength(clk Clock, drive DriveStrength) error { + if clk > Clock7 { + return ErrInvalidParameter + } + + regVal, err := d.rw.Read8(CLK0_CTRL + uint8(clk)) + if err != nil { + return err + } + + regVal &^= 0x03 + + switch drive { + case DriveStrength2MA: // 2mA + regVal |= CLK_DRIVE_STRENGTH_2MA + case DriveStrength4MA: // 4mA + regVal |= CLK_DRIVE_STRENGTH_4MA + case DriveStrength6MA: // 6mA + regVal |= CLK_DRIVE_STRENGTH_6MA + case DriveStrength8MA: // 8mA + regVal |= CLK_DRIVE_STRENGTH_8MA + default: + return ErrInvalidParameter + } + + return d.rw.Write8(CLK0_CTRL+uint8(clk), regVal) +} + +// SetPhase sets the 7-bit phase register for the specified clock. +func (d *Device) SetPhase(clk Clock, phase uint8) error { + phase &= 0x7F // Mask upper bit + return d.rw.Write8(CLK0_PHASE_OFFSET+uint8(clk), phase) +} + +// Fanout options for clock signals +type Fanout uint8 + +const ( + FanoutClockIn Fanout = iota + FanoutXO + FanoutMultisynth +) + +// SetClockFanout enables or disables the clock fanout options for individual clock outputs. +// If you intend to output the XO or CLKIN on the clock outputs, enable this first. +// By default, only the Multisynth fanout is enabled at startup. +func (d *Device) SetClockFanout(fanout Fanout, enable bool) error { + regVal, err := d.rw.Read8(FANOUT_ENABLE) + if err != nil { + return err + } + + switch fanout { + case FanoutClockIn: + if enable { + regVal |= CLKIN_ENABLE + } else { + regVal &^= CLKIN_ENABLE + } + case FanoutXO: + if enable { + regVal |= XTAL_ENABLE + } else { + regVal &^= XTAL_ENABLE + } + case FanoutMultisynth: + if enable { + regVal |= MULTISYNTH_ENABLE + } else { + regVal &^= MULTISYNTH_ENABLE + } + default: + return ErrInvalidParameter + } + + return d.rw.Write8(FANOUT_ENABLE, regVal) +} + +// Clock source options +type ClockSource uint8 + +const ( + ClockSourceXTAL ClockSource = iota + ClockSourceClockIn + ClockSourceMS0 + ClockSourceMS +) + +// SetClockSource sets the clock source for a multisynth (based on the options +// presented for Registers 16-23 in the Silicon Labs AN619 document). +// Choices are XTAL, CLKIN, MS0, or the multisynth associated with the clock output. +func (d *Device) SetClockSource(clk Clock, src ClockSource) error { + if clk > Clock7 { + return ErrInvalidParameter + } + + regVal, err := d.rw.Read8(CLK0_CTRL + uint8(clk)) + if err != nil { + return err + } + + // Clear the input mask bits first + regVal &^= CLK_INPUT_MASK + + switch src { + case ClockSourceXTAL: + regVal |= CLK_INPUT_XTAL + case ClockSourceClockIn: + regVal |= CLK_INPUT_CLKIN + case ClockSourceMS0: + if clk == Clock0 { + return ErrInvalidParameter + } + regVal |= CLK_INPUT_MULTISYNTH_0_4 + case ClockSourceMS: + regVal |= CLK_INPUT_MULTISYNTH_N + default: + return ErrInvalidParameter + } + + return d.rw.Write8(CLK0_CTRL+uint8(clk), regVal) +} + +// SetClockPower enables or disables power to a clock output (a power saving feature). +func (d *Device) SetClockPower(clk Clock, enable bool) error { + if clk > Clock7 { + return ErrInvalidParameter + } + + regVal, err := d.rw.Read8(CLK0_CTRL + uint8(clk)) + if err != nil { + return err + } + + if enable { + regVal &= 0x7F // Clear bit 7 (power on) + } else { + regVal |= 0x80 // Set bit 7 (power off) + } + + return d.rw.Write8(CLK0_CTRL+uint8(clk), regVal) +} + +// SetClockInvert inverts the clock output waveform. +func (d *Device) SetClockInvert(clk Clock, invert bool) error { + if clk > Clock7 { + return ErrInvalidParameter + } + + regVal, err := d.rw.Read8(CLK0_CTRL + uint8(clk)) + if err != nil { + return err + } + + if invert { + regVal |= CLK_INVERT + } else { + regVal &^= CLK_INVERT + } + + return d.rw.Write8(CLK0_CTRL+uint8(clk), regVal) +} + +// CalculatePLL calculates the PLL register values for the specified frequency +func (d *Device) CalculatePLL(pll PLLType, freq Frequency, correction int32, vcxo bool) (Frequency, RegisterSet) { + var refFreq Frequency + if pll == PLL_A { + refFreq = Frequency(d.crystalFreq[d.pllaRefOsc]) * FREQ_MULT + } else { + refFreq = Frequency(d.crystalFreq[d.pllbRefOsc]) * FREQ_MULT + } + + // Apply correction + refFreq = refFreq + Frequency(((int64(correction)<<31)/1000000000)*int64(refFreq)>>31) + + // Bounds checking + switch { + case freq < PLL_VCO_MIN*FREQ_MULT: + freq = PLL_VCO_MIN * FREQ_MULT + case freq > PLL_VCO_MAX*FREQ_MULT: + freq = PLL_VCO_MAX * FREQ_MULT + } + + a := uint32(freq / refFreq) + + switch { + case a < PLL_A_MIN: + freq = refFreq * PLL_A_MIN + case a > PLL_A_MAX: + freq = refFreq * PLL_A_MAX + } + + var b, c uint32 + if vcxo { + b = uint32(((freq % refFreq) * 1000000) / refFreq) + c = 1000000 + } else { + b = uint32(((freq % refFreq) * rfracDenominator) / refFreq) + if b != 0 { + c = uint32(rfracDenominator) + } else { + c = 1 + } + } + + p1 := 128*a + ((128 * b) / c) - 512 + p2 := 128*b - c*((128*b)/c) + p3 := c + + lltmp := (refFreq * Frequency(b)) / Frequency(c) + freqOut := lltmp + refFreq*Frequency(a) + + reg := RegisterSet{p1: p1, p2: p2, p3: p3} + + if vcxo { + return Frequency(128*a*1000000 + b), reg + } + return freqOut, reg +} + +// CalculateMultisynth calculates the multisynth register values for the specified frequency +func (d *Device) CalculateMultisynth(freq, pllFreq Frequency) (Frequency, RegisterSet) { + divby4 := false + retVal := uint8(0) + + // Bounds checking + switch { + case freq > MULTISYNTH_MAX_FREQ*FREQ_MULT: + freq = MULTISYNTH_MAX_FREQ * FREQ_MULT + case freq < MULTISYNTH_MIN_FREQ*FREQ_MULT: + freq = MULTISYNTH_MIN_FREQ * FREQ_MULT + } + + if freq >= MULTISYNTH_DIVBY4_FREQ*FREQ_MULT { + divby4 = true + } + + var a, b, c uint32 + + if pllFreq == 0 { + if !divby4 { + lltmp := Frequency(PLL_VCO_MAX * FREQ_MULT) + lltmp = lltmp / freq + switch lltmp { + case 5: + lltmp = 4 + case 7: + lltmp = 6 + } + a = uint32(lltmp) + } else { + a = 4 + } + b = 0 + c = 1 + pllFreq = Frequency(a) * freq + } else { + retVal = 1 + a = uint32(pllFreq / freq) + + switch { + case a < MULTISYNTH_A_MIN: + freq = pllFreq / MULTISYNTH_A_MIN + a = MULTISYNTH_A_MIN + case a > MULTISYNTH_A_MAX: + freq = pllFreq / MULTISYNTH_A_MAX + a = MULTISYNTH_A_MAX + } + + b = uint32(((pllFreq % freq) * rfracDenominator) / freq) + if b != 0 { + c = uint32(rfracDenominator) + } else { + c = 1 + } + } + + var p1, p2, p3 uint32 + if divby4 { + p3 = 1 + p2 = 0 + p1 = 0 + } else { + p1 = 128*a + ((128 * b) / c) - 512 + p2 = 128*b - c*((128*b)/c) + p3 = c + } + + reg := RegisterSet{p1: p1, p2: p2, p3: p3} + + if retVal == 0 { + return pllFreq, reg + } + return freq, reg +} + +// SetMultisynth programs the multisynth registers for the specified clock. +// For CLK0-5, reg contains p1, p2, p3 values. For CLK6/7, only p1 is used. +func (d *Device) SetMultisynth(clk Clock, reg RegisterSet, intMode, rDiv, divBy4 uint8) error { + switch { + case clk <= 5: + params := make([]byte, 8) + params[0] = byte((reg.p3 >> 8) & 0xFF) + params[1] = byte(reg.p3 & 0xFF) + + regVal, err := d.rw.Read8(CLK0_PARAMETERS + 2 + uint8(clk)*8) + if err != nil { + return err + } + regVal &^= 0x03 + params[2] = regVal | byte((reg.p1>>16)&0x03) + + params[3] = byte((reg.p1 >> 8) & 0xFF) + params[4] = byte(reg.p1 & 0xFF) + params[5] = byte(((reg.p3 >> 12) & 0xF0) | ((reg.p2 >> 16) & 0x0F)) + params[6] = byte((reg.p2 >> 8) & 0xFF) + params[7] = byte(reg.p2 & 0xFF) + + baseAddr := CLK0_PARAMETERS + uint8(clk)*8 + for i := range params { + if err := d.rw.Write8(baseAddr+uint8(i), params[i]); err != nil { + return err + } + } + + d.setInt(clk, intMode) + return d.msDiv(clk, rDiv, divBy4) + case clk <= 7: + // CLK6/7 + baseAddr := CLK6_PARAMETERS + if clk == 7 { + baseAddr = CLK7_PARAMETERS + } + if err := d.rw.Write8(uint8(baseAddr), byte(reg.p1)); err != nil { + return err + } + return d.msDiv(clk, rDiv, divBy4) + default: + return ErrInvalidParameter + } +} + +func (d *Device) setFreqCLK0to5(clk Clock, freq Frequency) error { + var rDiv uint8 + var divBy4 uint8 + var intMode uint8 + + // Bounds checking + switch { + case freq < CLKOUT_MIN_FREQ*FREQ_MULT: + freq = CLKOUT_MIN_FREQ * FREQ_MULT + case freq > MULTISYNTH_MAX_FREQ*FREQ_MULT: + freq = MULTISYNTH_MAX_FREQ * FREQ_MULT + } + + // Check if frequency requires PLL recalculation + if freq > MULTISYNTH_SHARE_MAX*FREQ_MULT { + // Check other clocks on same PLL + for i := range Clock(6) { + if d.clkFreq[i] > MULTISYNTH_SHARE_MAX*FREQ_MULT { + if i != clk && d.pllAssignment[i] == d.pllAssignment[clk] { + return ErrInvalidPLLClockSetting + } + } + } + + // Enable output on first set + if !d.clkFirstSet[clk] { + d.EnableOutput(clk, true) + d.clkFirstSet[clk] = true + } + + d.clkFreq[clk] = freq + + // Calculate PLL frequency + pllFreq, _ := d.CalculateMultisynth(freq, 0) + d.SetPLL(d.pllAssignment[clk], pllFreq) + + // Recalculate other synths on same PLL + for i := range Clock(6) { + if d.clkFreq[i] != 0 && d.pllAssignment[i] == d.pllAssignment[clk] { + tempFreq := d.clkFreq[i] + tempFreq, rDiv = d.selectRDiv(tempFreq) + + _, tempReg := d.CalculateMultisynth(tempFreq, pllFreq) + + if tempFreq >= MULTISYNTH_DIVBY4_FREQ*FREQ_MULT { + divBy4 = 1 + intMode = 1 + } else { + divBy4 = 0 + intMode = 0 + } + + d.SetMultisynth(i, tempReg, intMode, rDiv, divBy4) + } + } + + d.PLLReset(d.pllAssignment[clk]) + } else { + d.clkFreq[clk] = freq + + if !d.clkFirstSet[clk] { + d.EnableOutput(clk, true) + d.clkFirstSet[clk] = true + } + + freq, rDiv = d.selectRDiv(freq) + + var pllFreq Frequency + if d.pllAssignment[clk] == PLL_A { + pllFreq = d.pllaFreq + } else { + pllFreq = d.pllbFreq + } + + _, msReg := d.CalculateMultisynth(freq, pllFreq) + d.SetMultisynth(clk, msReg, intMode, rDiv, divBy4) + } + + return nil +} + +func (d *Device) setFreqCLK6to7(clk Clock, freq Frequency) error { + var rDiv uint8 + var divBy4 uint8 + var intMode uint8 + + // Bounds checking for CLK6/7 + if freq > 0 && freq < CLKOUT67_MIN_FREQ*FREQ_MULT { + freq = CLKOUT_MIN_FREQ * FREQ_MULT + } + if freq >= MULTISYNTH_DIVBY4_FREQ*FREQ_MULT { + freq = MULTISYNTH_DIVBY4_FREQ*FREQ_MULT - 1 + } + + var msReg RegisterSet + var pllFreq Frequency + + otherClk := uint8(7) + if clk == 7 { + otherClk = 6 + } + + if d.clkFreq[otherClk] != 0 { + // Other CLK6/7 already set, must use integer division + if d.pllbFreq%freq != 0 || (d.pllbFreq/freq)%2 != 0 { + return ErrInvalidPLLDivision + } + + d.clkFreq[clk] = freq + freq, rDiv = d.selectRDivMS67(freq) + _, msReg = d.multisynth67Calc(freq, d.pllbFreq) + } else { + // Set PLLB based on this clock + d.clkFreq[clk] = freq + freq, rDiv = d.selectRDivMS67(freq) + pllFreq, msReg = d.multisynth67Calc(freq, 0) + + d.SetPLL(d.pllAssignment[clk], pllFreq) + } + + divBy4 = 0 + intMode = 0 + + return d.SetMultisynth(clk, msReg, intMode, rDiv, divBy4) +} + +func (d *Device) setInt(clk Clock, enable uint8) error { + regVal, err := d.rw.Read8(CLK0_CTRL + uint8(clk)) + if err != nil { + return err + } + + if enable == 1 { + regVal |= CLK_INTEGER_MODE + } else { + regVal &^= CLK_INTEGER_MODE + } + + return d.rw.Write8(CLK0_CTRL+uint8(clk), regVal) +} + +func (d *Device) msDiv(clk Clock, rDiv, divBy4 uint8) error { + var regAddr uint8 + + switch clk { + case 0: + regAddr = CLK0_PARAMETERS + 2 + case 1: + regAddr = CLK1_PARAMETERS + 2 + case 2: + regAddr = CLK2_PARAMETERS + 2 + case 3: + regAddr = CLK3_PARAMETERS + 2 + case 4: + regAddr = CLK4_PARAMETERS + 2 + case 5: + regAddr = CLK5_PARAMETERS + 2 + case 6, 7: + regAddr = CLK6_7_OUTPUT_DIVIDER + default: + return ErrInvalidParameter + } + + regVal, err := d.rw.Read8(regAddr) + if err != nil { + return err + } + + switch { + case clk <= 5: + regVal &^= 0x7C + + if divBy4 == 0 { + regVal &^= OUTPUT_CLK_DIVBY4 + } else { + regVal |= OUTPUT_CLK_DIVBY4 + } + + regVal |= (rDiv << OUTPUT_CLK_DIV_SHIFT) + case clk == 6: + regVal &^= 0x07 + regVal |= rDiv + case clk == 7: + regVal &^= 0x70 + regVal |= (rDiv << OUTPUT_CLK_DIV_SHIFT) + } + + return d.rw.Write8(regAddr, regVal) +} + +func (d *Device) selectRDiv(freq Frequency) (Frequency, uint8) { + rDiv := OUTPUT_CLK_DIV_1 + + switch { + case freq >= CLKOUT_MIN_FREQ*FREQ_MULT && freq < CLKOUT_MIN_FREQ*FREQ_MULT*2: + rDiv = OUTPUT_CLK_DIV_128 + freq *= 128 + case freq >= CLKOUT_MIN_FREQ*FREQ_MULT*2 && freq < CLKOUT_MIN_FREQ*FREQ_MULT*4: + rDiv = OUTPUT_CLK_DIV_64 + freq *= 64 + case freq >= CLKOUT_MIN_FREQ*FREQ_MULT*4 && freq < CLKOUT_MIN_FREQ*FREQ_MULT*8: + rDiv = OUTPUT_CLK_DIV_32 + freq *= 32 + case freq >= CLKOUT_MIN_FREQ*FREQ_MULT*8 && freq < CLKOUT_MIN_FREQ*FREQ_MULT*16: + rDiv = OUTPUT_CLK_DIV_16 + freq *= 16 + case freq >= CLKOUT_MIN_FREQ*FREQ_MULT*16 && freq < CLKOUT_MIN_FREQ*FREQ_MULT*32: + rDiv = OUTPUT_CLK_DIV_8 + freq *= 8 + case freq >= CLKOUT_MIN_FREQ*FREQ_MULT*32 && freq < CLKOUT_MIN_FREQ*FREQ_MULT*64: + rDiv = OUTPUT_CLK_DIV_4 + freq *= 4 + case freq >= CLKOUT_MIN_FREQ*FREQ_MULT*64 && freq < CLKOUT_MIN_FREQ*FREQ_MULT*128: + rDiv = OUTPUT_CLK_DIV_2 + freq *= 2 + } + + return freq, uint8(rDiv) +} + +func (d *Device) selectRDivMS67(freq Frequency) (Frequency, uint8) { + rDiv := OUTPUT_CLK_DIV_1 + + // The minimum frequency for MS67 with max divider is lower than the calculated constant + // We use the same ranges as selectRDiv for consistency + minFreq := Frequency(CLKOUT_MIN_FREQ * FREQ_MULT) + + switch { + case freq >= minFreq && freq < minFreq*2: + rDiv = OUTPUT_CLK_DIV_128 + freq *= 128 + case freq >= minFreq*2 && freq < minFreq*4: + rDiv = OUTPUT_CLK_DIV_64 + freq *= 64 + case freq >= minFreq*4 && freq < minFreq*8: + rDiv = OUTPUT_CLK_DIV_32 + freq *= 32 + case freq >= minFreq*8 && freq < minFreq*16: + rDiv = OUTPUT_CLK_DIV_16 + freq *= 16 + case freq >= minFreq*16 && freq < minFreq*32: + rDiv = OUTPUT_CLK_DIV_8 + freq *= 8 + case freq >= minFreq*32 && freq < minFreq*64: + rDiv = OUTPUT_CLK_DIV_4 + freq *= 4 + case freq >= minFreq*64 && freq < minFreq*128: + rDiv = OUTPUT_CLK_DIV_2 + freq *= 2 + } + + return freq, uint8(rDiv) +} + +func (d *Device) multisynth67Calc(freq, pllFreq Frequency) (Frequency, RegisterSet) { + // Bounds checking + if freq > MULTISYNTH67_MAX_FREQ*FREQ_MULT { + freq = MULTISYNTH67_MAX_FREQ * FREQ_MULT + } + if freq < MULTISYNTH_MIN_FREQ*FREQ_MULT { + freq = MULTISYNTH_MIN_FREQ * FREQ_MULT + } + + var a uint32 + + if pllFreq == 0 { + lltmp := Frequency(PLL_VCO_MAX*FREQ_MULT - MULTISYNTH_SHARE_MAX) + lltmp = lltmp / freq + a = uint32(lltmp) + + // Must be even + if a%2 != 0 { + a++ + } + + // Bounds check + if a < MULTISYNTH_A_MIN { + a = MULTISYNTH_A_MIN + } + if a > MULTISYNTH67_A_MAX { + a = MULTISYNTH67_A_MAX + } + + pllFreq = Frequency(a) * freq + + // PLL bounds + if pllFreq > PLL_VCO_MAX*FREQ_MULT { + a -= 2 + pllFreq = Frequency(a) * freq + } else if pllFreq < PLL_VCO_MIN*FREQ_MULT { + a += 2 + pllFreq = Frequency(a) * freq + } + + return pllFreq, RegisterSet{p1: a, p2: 0, p3: 0} + } else { + if pllFreq%freq != 0 { + return 0, RegisterSet{} + } + + a = uint32(pllFreq / freq) + + if a < MULTISYNTH_A_MIN || a > MULTISYNTH67_A_MAX { + return 0, RegisterSet{} + } + + return 1, RegisterSet{p1: a, p2: 0, p3: 0} + } +} diff --git a/si5351/si5351_test.go b/si5351/si5351_test.go new file mode 100644 index 0000000..392556f --- /dev/null +++ b/si5351/si5351_test.go @@ -0,0 +1,214 @@ +package si5351 + +import ( + "testing" +) + +func TestSelectRDiv(t *testing.T) { + d := &Device{} + + tests := []struct { + name string + freq Frequency + wantDiv uint8 + wantFreq Frequency + }{ + {"4kHz", 4000 * FREQ_MULT, OUTPUT_CLK_DIV_128, 4000 * FREQ_MULT * 128}, + {"8kHz", 8000 * FREQ_MULT, OUTPUT_CLK_DIV_64, 8000 * FREQ_MULT * 64}, + {"16kHz", 16000 * FREQ_MULT, OUTPUT_CLK_DIV_32, 16000 * FREQ_MULT * 32}, + {"32kHz", 32000 * FREQ_MULT, OUTPUT_CLK_DIV_16, 32000 * FREQ_MULT * 16}, + {"64kHz", 64000 * FREQ_MULT, OUTPUT_CLK_DIV_8, 64000 * FREQ_MULT * 8}, + {"128kHz", 128000 * FREQ_MULT, OUTPUT_CLK_DIV_4, 128000 * FREQ_MULT * 4}, + {"256kHz", 256000 * FREQ_MULT, OUTPUT_CLK_DIV_2, 256000 * FREQ_MULT * 2}, + {"512kHz", 512000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 512000 * FREQ_MULT}, + {"1MHz", 1000000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 1000000 * FREQ_MULT}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + freq := tt.freq + freq, gotDiv := d.selectRDiv(freq) + if gotDiv != tt.wantDiv { + t.Errorf("selectRDiv() div = %v, want %v", gotDiv, tt.wantDiv) + } + if freq != tt.wantFreq { + t.Errorf("selectRDiv() freq = %v, want %v", freq, tt.wantFreq) + } + }) + } +} + +func TestSelectRDivMS67(t *testing.T) { + d := &Device{} + + tests := []struct { + name string + freq Frequency + wantDiv uint8 + wantFreq Frequency + }{ + {"4kHz", 4000 * FREQ_MULT, OUTPUT_CLK_DIV_128, 4000 * FREQ_MULT * 128}, + {"8kHz", 8000 * FREQ_MULT, OUTPUT_CLK_DIV_64, 8000 * FREQ_MULT * 64}, + {"16kHz", 16000 * FREQ_MULT, OUTPUT_CLK_DIV_32, 16000 * FREQ_MULT * 32}, + {"64kHz", 64000 * FREQ_MULT, OUTPUT_CLK_DIV_8, 64000 * FREQ_MULT * 8}, + {"256kHz", 256000 * FREQ_MULT, OUTPUT_CLK_DIV_2, 256000 * FREQ_MULT * 2}, + {"512kHz", 512000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 512000 * FREQ_MULT}, + {"1MHz", 1000000 * FREQ_MULT, OUTPUT_CLK_DIV_1, 1000000 * FREQ_MULT}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + freq := tt.freq + freq, gotDiv := d.selectRDivMS67(freq) + if gotDiv != tt.wantDiv { + t.Errorf("selectRDivMS67() div = %v, want %v", gotDiv, tt.wantDiv) + } + if freq != tt.wantFreq { + t.Errorf("selectRDivMS67() freq = %v, want %v", freq, tt.wantFreq) + } + }) + } +} + +func TestCalculatePLL(t *testing.T) { + d := &Device{} + d.crystalFreq[0] = 25000000 + + tests := []struct { + name string + freq Frequency + wantMin Frequency + wantMax Frequency + }{ + {"600MHz", 600000000 * FREQ_MULT, 599000000 * FREQ_MULT, 601000000 * FREQ_MULT}, + {"750MHz", 750000000 * FREQ_MULT, 749000000 * FREQ_MULT, 751000000 * FREQ_MULT}, + {"900MHz", 900000000 * FREQ_MULT, 899000000 * FREQ_MULT, 901000000 * FREQ_MULT}, + {"BelowMin", 500000000 * FREQ_MULT, 600000000 * FREQ_MULT, 600000000 * FREQ_MULT}, + {"AboveMax", 1000000000 * FREQ_MULT, 900000000 * FREQ_MULT, 900000000 * FREQ_MULT}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, reg := d.CalculatePLL(PLL_A, tt.freq, 0, false) + if got < tt.wantMin || got > tt.wantMax { + t.Errorf("CalculatePLL() = %v, want between %v and %v", got, tt.wantMin, tt.wantMax) + } + if reg.p1 == 0 || reg.p3 == 0 { + t.Errorf("CalculatePLL() invalid register values: p1=%v, p2=%v, p3=%v", reg.p1, reg.p2, reg.p3) + } + }) + } +} + +func TestCalculateMultisynth(t *testing.T) { + d := &Device{} + + tests := []struct { + name string + freq Frequency + pllFreq Frequency + wantDiv bool + }{ + {"10MHz from 800MHz", 10000000 * FREQ_MULT, 800000000 * FREQ_MULT, false}, + {"1MHz from 800MHz", 1000000 * FREQ_MULT, 800000000 * FREQ_MULT, false}, + {"Auto PLL 10MHz", 10000000 * FREQ_MULT, 0, false}, + {"150MHz DivBy4", 150000000 * FREQ_MULT, 600000000 * FREQ_MULT, true}, + {"BelowMin", 100000 * FREQ_MULT, 800000000 * FREQ_MULT, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, reg := d.CalculateMultisynth(tt.freq, tt.pllFreq) + if tt.pllFreq == 0 { + // Auto mode should return a valid PLL frequency + if got < PLL_VCO_MIN*FREQ_MULT || got > PLL_VCO_MAX*FREQ_MULT { + t.Errorf("CalculateMultisynth() returned invalid PLL freq %v", got) + } + } + if reg.p3 == 0 { + t.Errorf("CalculateMultisynth() p3 should not be 0") + } + }) + } +} + +func TestMultisynth67Calc(t *testing.T) { + d := &Device{} + + tests := []struct { + name string + freq Frequency + pllFreq Frequency + wantErr bool + }{ + {"10MHz Auto", 10000000 * FREQ_MULT, 0, false}, + {"100MHz Auto", 100000000 * FREQ_MULT, 0, false}, + {"100MHz from 800MHz", 100000000 * FREQ_MULT, 800000000 * FREQ_MULT, false}, + {"Invalid Division", 10000000 * FREQ_MULT, 777000000 * FREQ_MULT, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, reg := d.multisynth67Calc(tt.freq, tt.pllFreq) + if tt.pllFreq == 0 { + if got < PLL_VCO_MIN*FREQ_MULT || got > PLL_VCO_MAX*FREQ_MULT { + t.Errorf("multisynth67Calc() returned invalid PLL freq %v", got) + } + } else if tt.wantErr { + if got != 0 { + t.Errorf("multisynth67Calc() should return 0 for invalid division, got %v", got) + } + } + if reg.p1 == 0 && !tt.wantErr { + t.Errorf("multisynth67Calc() p1 should not be 0") + } + }) + } +} + +func TestSetCorrection(t *testing.T) { + // Skip this test as it requires a mock I2C bus + t.Skip("Requires mock I2C bus implementation") +} + +func TestSetRefFreq(t *testing.T) { + d := &Device{} + + tests := []struct { + name string + freq CrystalFrequency + wantFreq CrystalFrequency + wantDiv uint8 + }{ + {"25MHz", 25000000, 25000000, CLKIN_DIV_1}, + {"50MHz", 50000000, 25000000, CLKIN_DIV_2}, + {"100MHz", 100000000, 25000000, CLKIN_DIV_4}, + {"30MHz", 30000000, 30000000, CLKIN_DIV_1}, + {"60MHz", 60000000, 30000000, CLKIN_DIV_2}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d.SetReferenceFrequency(PLLInputClockIn, tt.freq) + if d.crystalFreq[PLLInputClockIn] != tt.wantFreq { + t.Errorf("SetReferenceFrequency() freq = %v, want %v", d.crystalFreq[PLLInputClockIn], tt.wantFreq) + } + if d.clkinDiv != tt.wantDiv { + t.Errorf("SetReferenceFrequency() clkinDiv = %v, want %v", d.clkinDiv, tt.wantDiv) + } + }) + } +} + +func TestGetCorrection(t *testing.T) { + d := &Device{} + d.refCorrection[PLLInputXO] = 5000 + d.refCorrection[PLLInputClockIn] = -3000 + + if got := d.GetCorrection(PLLInputXO); got != 5000 { + t.Errorf("GetCorrection(PLLInputXO) = %v, want 5000", got) + } + if got := d.GetCorrection(PLLInputClockIn); got != -3000 { + t.Errorf("GetCorrection(PLLInputClockIn) = %v, want -3000", got) + } +} diff --git a/smoketest.sh b/smoketest.sh index 5ccd7e8..924fcac 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -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 @@ -44,6 +46,7 @@ tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/ili9341 tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/lis3dh/main.go tinygo build -size short -o ./build/test.hex -target=nano-33-ble ./examples/lps22hb/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/lsm303agr/main.go +tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/lsm303dlhc/main.go tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/lsm6ds3/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mag3110/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/mcp23017/main.go @@ -58,33 +61,39 @@ tinygo build -size short -o ./build/test.hex -target=p1am-100 ./examples/p1am/ma tinygo build -size short -o ./build/test.hex -target=pico ./examples/pca9685/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/pcd8544/setbuffer/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/pcd8544/setpixel/main.go -tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/seesaw +tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/seesaw/soil-sensor +tinygo build -size short -o ./build/test.hex -target=qtpy-rp2040 ./examples/seesaw/rotary-encoder tinygo build -size short -o ./build/test.hex -target=arduino ./examples/servo tinygo build -size short -o ./build/test.hex -target=pico ./examples/sgp30 tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/shifter/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/sht3x/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/sht4x/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/shtc3/main.go -tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ssd1306/i2c_128x32/main.go -tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ssd1306/spi_128x64/main.go +tinygo build -size short -o ./build/test.hex -target=xiao-ble ./examples/ssd1306/ +tinygo build -size short -o ./build/test.hex -target=xiao-rp2040 ./examples/ssd1306/ +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=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 +tinygo build -size short -o ./build/test.hex -target=pico ./examples/touch/capacitive tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/touch/resistive/fourwire/main.go tinygo build -size short -o ./build/test.hex -target=pyportal ./examples/touch/resistive/pyportal_touchpaint/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/vl53l1x/main.go tinygo build -size short -o ./build/test.hex -target=itsybitsy-m0 ./examples/vl6180x/main.go +tinygo build -size short -o ./build/test.hex -target=feather-nrf52840-sense ./examples/waveshare-epd/epd1in54/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd2in13/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd2in13x/main.go tinygo build -size short -o ./build/test.hex -target=microbit ./examples/waveshare-epd/epd4in2/main.go +tinygo build -size short -o ./build/test.hex -target=pico ./examples/waveshare-epd/epd2in66b/main.go tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/ws2812 tinygo build -size short -o ./build/test.bin -target=m5stamp-c3 ./examples/ws2812 tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/is31fl3731/main.go tinygo build -size short -o ./build/test.hex -target=arduino ./examples/ws2812 tinygo build -size short -o ./build/test.hex -target=digispark ./examples/ws2812 +tinygo build -size short -o ./build/test.bin -target=xiao-esp32c3 ./examples/ws2812 tinygo build -size short -o ./build/test.hex -target=trinket-m0 ./examples/bme280/main.go tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/microphone/main.go tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/buzzer/main.go @@ -105,7 +114,9 @@ tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/clk tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/time/ tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/timer/ tinygo build -size short -o ./build/test.hex -target=pico ./examples/qmi8658c/main.go +tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/pcf8591/ tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/ina260/main.go +tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/ina219/main.go tinygo build -size short -o ./build/test.hex -target=nucleo-l432kc ./examples/aht20/main.go tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/sdcard/console/ tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/i2csoft/adt7410/ @@ -114,9 +125,11 @@ tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/xpt2046/mai tinygo build -size short -o ./build/test.elf -target=m5stack-core2 ./examples/ft6336/basic/ tinygo build -size short -o ./build/test.elf -target=m5stack-core2 ./examples/ft6336/touchpaint/ tinygo build -size short -o ./build/test.hex -target=nucleo-wl55jc ./examples/sx126x/lora_rxtx/ +tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/sx127x/lora_rxtx/ tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/ssd1289/main.go tinygo build -size short -o ./build/test.hex -target=pico ./examples/irremote/main.go tinygo build -size short -o ./build/test.hex -target=badger2040 ./examples/uc8151/main.go +tinygo build -size short -o ./build/test.hex -target=badger2040 ./examples/waveshare-epd/epd2in9v2/main.go tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/scd4x/main.go tinygo build -size short -o ./build/test.uf2 -target=circuitplay-express ./examples/makeybutton/main.go tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/ds18b20/main.go @@ -129,6 +142,15 @@ tinygo build -size short -o ./build/test.hex -target=microbit ./examples/ndir/ma tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/ndir/main_ndir.go tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mpu9150/main.go tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/sh1106/macropad_spi +tinygo build -size short -o ./build/test.hex -target=macropad-rp2040 ./examples/encoders/quadrature-interrupt +tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/mcp9808/main.go +tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/tmc2209/main.go +tinygo build -size short -o ./build/test.hex -target=pico ./examples/tmc5160/main.go +tinygo build -size short -o ./build/test.uf2 -target=nicenano ./examples/sharpmem/main.go +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 tinygo build -size short -o ./build/test.hex -target=pico ./examples/sd # network examples (espat) tinygo build -size short -o ./build/test.hex -target=challenger-rp2040 ./examples/net/ntpclient/ @@ -144,3 +166,6 @@ tinygo build -size short -o ./build/test.hex -target=nano-rp2040 -stack-size 8kb tinygo build -size short -o ./build/test.hex -target=wioterminal -stack-size 8kb ./examples/net/webclient/ tinygo build -size short -o ./build/test.hex -target=wioterminal -stack-size 8kb ./examples/net/webserver/ tinygo build -size short -o ./build/test.hex -target=wioterminal -stack-size 8kb ./examples/net/mqttclient/paho/ +# network examples (comboat) +tinygo build -size short -o ./build/test.hex -target=elecrow-rp2040 -stack-size 8kb ./examples/net/tlsclient/ +tinygo build -size short -o ./build/test.hex -target=elecrow-rp2350 -stack-size 8kb ./examples/net/ntpclient/ diff --git a/ssd1289/pinbus.go b/ssd1289/pinbus.go index aacdf79..7dde7b6 100644 --- a/ssd1289/pinbus.go +++ b/ssd1289/pinbus.go @@ -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) + } } diff --git a/ssd1289/ssd1289.go b/ssd1289/ssd1289.go index f5cc22a..8b1e55f 100644 --- a/ssd1289/ssd1289.go +++ b/ssd1289/ssd1289.go @@ -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 } diff --git a/ssd1306/registers.go b/ssd1306/registers.go index 63590b8..974ecee 100644 --- a/ssd1306/registers.go +++ b/ssd1306/registers.go @@ -1,5 +1,7 @@ package ssd1306 +import "tinygo.org/x/drivers" + // Registers const ( Address = 0x3D @@ -38,4 +40,7 @@ const ( EXTERNALVCC VccMode = 0x1 SWITCHCAPVCC VccMode = 0x2 + + NO_ROTATION = drivers.Rotation0 + ROTATION_180 = drivers.Rotation180 ) diff --git a/ssd1306/ssd1306.go b/ssd1306/ssd1306.go index 2f27096..2e65a3a 100644 --- a/ssd1306/ssd1306.go +++ b/ssd1306/ssd1306.go @@ -6,22 +6,30 @@ package ssd1306 // import "tinygo.org/x/drivers/ssd1306" import ( "errors" "image/color" - "machine" "time" "tinygo.org/x/drivers" - "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/pixel" ) +var ( + errBufferSize = errors.New("invalid size buffer") + errOutOfRange = errors.New("out of screen range") +) + +type ResetValue [2]byte + // Device wraps I2C or SPI connection. type Device struct { - bus Buser - buffer []byte - width int16 - height int16 - bufferSize int16 - vccState VccMode - canReset bool + bus Buser + buffer []byte + width int16 + height int16 + vccState VccMode + canReset bool + resetCol ResetValue + resetPage ResetValue + rotation drivers.Rotation } // Config is the configuration for the display @@ -30,55 +38,28 @@ type Config struct { Height int16 VccState VccMode Address uint16 -} - -type I2CBus struct { - wire drivers.I2C - Address uint16 -} - -type SPIBus struct { - wire drivers.SPI - dcPin machine.Pin - resetPin machine.Pin - csPin machine.Pin + // ResetCol and ResetPage are used to reset the screen to 0x0 + // This is useful for some screens that have a different size than 128x64 + // For example, the Thumby's screen is 72x40 + // The default values are normally set automatically based on the size. + // If you're using a different size, you might need to set these values manually. + ResetCol ResetValue + ResetPage ResetValue + Rotation drivers.Rotation } type Buser interface { - configure() error - tx(data []byte, isCommand bool) error - setAddress(address uint16) error + configure(address uint16, size int16) []byte // configure the bus and return the image buffer to use + command(cmd uint8) error // send a command to the display + flush() error // send the image to the display, faster than "tx()" in i2c case since avoids slice copy + tx(data []byte, isCommand bool) error // generic transmit function } type VccMode uint8 -// NewI2C creates a new SSD1306 connection. The I2C wire must already be configured. -func NewI2C(bus drivers.I2C) Device { - return Device{ - bus: &I2CBus{ - wire: bus, - Address: Address, - }, - } -} - -// 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}) - return Device{ - bus: &SPIBus{ - wire: bus, - dcPin: dcPin, - resetPin: resetPin, - csPin: csPin, - }, - } -} - // Configure initializes the display with default configuration func (d *Device) Configure(cfg Config) { + var zeroReset ResetValue if cfg.Width != 0 { d.width = cfg.Width } else { @@ -89,19 +70,24 @@ func (d *Device) Configure(cfg Config) { } else { d.height = 64 } - if cfg.Address != 0 { - d.bus.setAddress(cfg.Address) - } if cfg.VccState != 0 { d.vccState = cfg.VccState } else { d.vccState = SWITCHCAPVCC } - d.bufferSize = d.width * d.height / 8 - d.buffer = make([]byte, d.bufferSize) + if cfg.ResetCol != zeroReset { + d.resetCol = cfg.ResetCol + } else { + d.resetCol = ResetValue{0, uint8(d.width - 1)} + } + if cfg.ResetPage != zeroReset { + d.resetPage = cfg.ResetPage + } else { + d.resetPage = ResetValue{0, uint8(d.height/8) - 1} + } d.canReset = cfg.Address != 0 || d.width != 128 || d.height != 64 // I2C or not 128x64 - d.bus.configure() + d.buffer = d.bus.configure(cfg.Address, d.width*d.height/8) time.Sleep(100 * time.Nanosecond) d.Command(DISPLAYOFF) @@ -120,8 +106,8 @@ func (d *Device) Configure(cfg Config) { } d.Command(MEMORYMODE) d.Command(0x00) - d.Command(SEGREMAP | 0x1) - d.Command(COMSCANDEC) + + d.SetRotation(cfg.Rotation) if (d.width == 128 && d.height == 64) || (d.width == 64 && d.height == 48) { // 128x64 or 64x48 d.Command(SETCOMPINS) @@ -163,11 +149,22 @@ func (d *Device) Configure(cfg Config) { d.Command(NORMALDISPLAY) d.Command(DEACTIVATE_SCROLL) d.Command(DISPLAYON) + +} + +// Command sends a command to the display +func (d *Device) Command(command uint8) { + d.bus.command(command) +} + +// Tx sends data to the display; if isCommand is false, this also updates the image buffer. +func (d *Device) Tx(data []byte, isCommand bool) error { + return d.bus.tx(data, isCommand) } // ClearBuffer clears the image buffer func (d *Device) ClearBuffer() { - for i := int16(0); i < d.bufferSize; i++ { + for i := 0; i < len(d.buffer); i++ { d.buffer[i] = 0 } } @@ -186,14 +183,14 @@ func (d *Device) Display() error { // Since we're printing the whole buffer, avoid resetting it in this case if d.canReset { d.Command(COLUMNADDR) - d.Command(0) - d.Command(uint8(d.width - 1)) + d.Command(d.resetCol[0]) + d.Command(d.resetCol[1]) d.Command(PAGEADDR) - d.Command(0) - d.Command(uint8(d.height/8) - 1) + d.Command(d.resetPage[0]) + d.Command(d.resetPage[1]) } - return d.Tx(d.buffer, false) + return d.bus.flush() } // SetPixel enables or disables a pixel in the buffer @@ -222,13 +219,10 @@ func (d *Device) GetPixel(x int16, y int16) bool { // SetBuffer changes the whole buffer at once func (d *Device) SetBuffer(buffer []byte) error { - if int16(len(buffer)) != d.bufferSize { - //return ErrBuffer - return errors.New("wrong size buffer") - } - for i := int16(0); i < d.bufferSize; i++ { - d.buffer[i] = buffer[i] + if len(buffer) != len(d.buffer) { + return errBufferSize } + copy(d.buffer, buffer) return nil } @@ -237,82 +231,86 @@ func (d *Device) GetBuffer() []byte { return d.buffer } -// Command sends a command to the display -func (d *Device) Command(command uint8) { - d.bus.tx([]byte{command}, true) -} - -// setAddress sets the address to the I2C bus -func (b *I2CBus) setAddress(address uint16) error { - b.Address = address - return nil -} - -// setAddress does nothing, but it's required to avoid reflection -func (b *SPIBus) setAddress(address uint16) error { - // do nothing - println("trying to Configure an address on a SPI device") - return nil -} - -// configure does nothing, but it's required to avoid reflection -func (b *I2CBus) configure() error { return nil } - -// configure configures some pins with the SPI bus -func (b *SPIBus) configure() error { - b.csPin.Low() - b.dcPin.Low() - b.resetPin.Low() - - b.resetPin.High() - time.Sleep(1 * time.Millisecond) - b.resetPin.Low() - time.Sleep(10 * time.Millisecond) - b.resetPin.High() - - return nil -} - -// Tx sends data to the display -func (d *Device) Tx(data []byte, isCommand bool) error { - return d.bus.tx(data, isCommand) -} - -// tx sends data to the display (I2CBus implementation) -func (b *I2CBus) tx(data []byte, isCommand bool) error { - if isCommand { - return legacy.WriteRegister(b.wire, uint8(b.Address), 0x00, data) - } else { - return legacy.WriteRegister(b.wire, uint8(b.Address), 0x40, data) - } -} - -// tx sends data to the display (SPIBus implementation) -func (b *SPIBus) tx(data []byte, isCommand bool) error { - var err error - - if isCommand { - b.csPin.High() - time.Sleep(1 * time.Millisecond) - b.dcPin.Low() - b.csPin.Low() - - err = b.wire.Tx(data, nil) - b.csPin.High() - } else { - b.csPin.High() - time.Sleep(1 * time.Millisecond) - b.dcPin.High() - b.csPin.Low() - - err = b.wire.Tx(data, nil) - b.csPin.High() - } - - return err -} - // Size returns the current size of the display. func (d *Device) Size() (w, h int16) { return d.width, d.height } + +// DrawBitmap copies the bitmap to the screen at the given coordinates. +func (d *Device) DrawBitmap(x, y int16, bitmap pixel.Image[pixel.Monochrome]) error { + width, height := bitmap.Size() + if x < 0 || x+int16(width) > d.width || y < 0 || y+int16(height) > d.height { + return errOutOfRange + } + + for i := 0; i < width; i++ { + for j := 0; j < height; j++ { + d.SetPixel(x+int16(i), y+int16(j), bitmap.Get(i, j).RGBA()) + } + } + + return nil +} + +// Rotation returns the currently configured rotation. +func (d *Device) Rotation() drivers.Rotation { + return d.rotation +} + +// SetRotation changes the rotation of the device (clock-wise). +func (d *Device) SetRotation(rotation drivers.Rotation) error { + d.rotation = rotation + switch d.rotation { + case drivers.Rotation0: + d.Command(SEGREMAP | 0x1) // Reverse horizontal mapping + d.Command(COMSCANDEC) // Reverse vertical mapping + case drivers.Rotation180: + d.Command(SEGREMAP) // Normal horizontal mapping + d.Command(COMSCANINC) // Normal vertical mapping + // nothing to do + default: + d.Command(SEGREMAP | 0x1) // Reverse horizontal mapping + d.Command(COMSCANDEC) // Reverse vertical mapping + } + return nil +} + +// Set the sleep mode for this display. When sleeping, the panel uses a lot +// less power. The display won't show an image anymore, but the memory contents +// should be kept. +func (d *Device) Sleep(sleepEnabled bool) error { + if sleepEnabled { + d.Command(DISPLAYOFF) + } else { + d.Command(DISPLAYON) + } + return nil +} + +// FillRectangle fills a rectangle at a given coordinates with a color +func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error { + dw, dh := d.Size() + + if x < 0 || y < 0 || width <= 0 || height <= 0 || + x >= d.width || (x+width) > dw || y >= dh || (y+height) > dh { + return errOutOfRange + } + + if x+width == dw && y+height == dh && c.R == 0 && c.G == 0 && c.B == 0 { + d.ClearDisplay() + return nil + } + + for i := x; i < x+width; i++ { + for j := y; j < y+height; j++ { + d.SetPixel(i, j, c) + } + } + + return nil +} + +// SetScroll sets the vertical scrolling for the display, which is a NOP for this display. +func (d *Device) SetScroll(line int16) { + return +} diff --git a/ssd1306/ssd1306_i2c.go b/ssd1306/ssd1306_i2c.go new file mode 100644 index 0000000..19f3a1c --- /dev/null +++ b/ssd1306/ssd1306_i2c.go @@ -0,0 +1,52 @@ +package ssd1306 + +import ( + "tinygo.org/x/drivers" +) + +type I2CBus struct { + wire drivers.I2C + address uint16 + buffer []byte // buffer to avoid heap allocations +} + +// NewI2C creates a new SSD1306 connection. The I2C wire must already be configured. +func NewI2C(bus drivers.I2C) *Device { + return &Device{ + bus: &I2CBus{ + wire: bus, + address: Address, + }, + } +} + +// configure address for the I2C bus and allocate the buffer +func (b *I2CBus) configure(address uint16, size int16) []byte { + if address != 0 { + b.address = address + } + b.buffer = make([]byte, size+2) // +1 for the mode and +1 for a command + return b.buffer[2:] // return the image buffer +} + +// command sends a command to the display +func (b *I2CBus) command(cmd uint8) error { + b.buffer[0] = 0x00 // Command mode + b.buffer[1] = cmd + return b.wire.Tx(b.address, b.buffer[:2], nil) +} + +// flush sends the image to the display +func (b *I2CBus) flush() error { + b.buffer[1] = 0x40 // Data mode + return b.wire.Tx(b.address, b.buffer[1:], nil) +} + +// tx sends data to the display +func (b *I2CBus) tx(data []byte, isCommand bool) error { + if isCommand { + return b.command(data[0]) + } + copy(b.buffer[2:], data) + return b.flush() +} diff --git a/ssd1306/ssd1306_spi.go b/ssd1306/ssd1306_spi.go new file mode 100644 index 0000000..938abce --- /dev/null +++ b/ssd1306/ssd1306_spi.go @@ -0,0 +1,70 @@ +package ssd1306 + +import ( + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" +) + +type SPIBus struct { + wire drivers.SPI + 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 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.Set, + resetPin: resetPin.Set, + csPin: csPin.Set, + }, + } +} + +// configure pins with the SPI bus and allocate the buffer +func (b *SPIBus) configure(address uint16, size int16) []byte { + b.csPin.Low() + b.dcPin.Low() + b.resetPin.Low() + + b.resetPin.High() + time.Sleep(1 * time.Millisecond) + b.resetPin.Low() + time.Sleep(10 * time.Millisecond) + b.resetPin.High() + + b.buffer = make([]byte, size+1) // +1 for a command + return b.buffer[1:] // return the image buffer +} + +// command sends a command to the display +func (b *SPIBus) command(cmd uint8) error { + b.buffer[0] = cmd + return b.tx(b.buffer[:1], true) +} + +// flush sends the image to the display +func (b *SPIBus) flush() error { + return b.tx(b.buffer[1:], false) +} + +// tx sends data to the display +func (b *SPIBus) tx(data []byte, isCommand bool) error { + b.csPin.High() + b.dcPin(!isCommand) + b.csPin.Low() + err := b.wire.Tx(data, nil) + b.csPin.High() + return err +} diff --git a/ssd1331/ssd1331.go b/ssd1331/ssd1331.go index 0717c48..426850f 100644 --- a/ssd1331/ssd1331.go +++ b/ssd1331/ssd1331.go @@ -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) } diff --git a/ssd1351/ssd1351.go b/ssd1351/ssd1351.go index 3561dd6..dd737f1 100644 --- a/ssd1351/ssd1351.go +++ b/ssd1351/ssd1351.go @@ -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() diff --git a/st7735/st7735.go b/st7735/st7735.go index 6f15781..a763215 100644 --- a/st7735/st7735.go +++ b/st7735/st7735.go @@ -5,12 +5,13 @@ package st7735 // import "tinygo.org/x/drivers/st7735" import ( "image/color" - "machine" "time" "errors" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" "tinygo.org/x/drivers/pixel" ) @@ -39,10 +40,10 @@ type Device = DeviceOf[pixel.RGB565BE] // formats. type DeviceOf[T Color] struct { bus drivers.SPI - dcPin machine.Pin - resetPin machine.Pin - csPin machine.Pin - blPin machine.Pin + dcPin pin.OutputFunc + resetPin pin.OutputFunc + csPin pin.OutputFunc + blPin pin.OutputFunc width int16 height int16 columnOffset int16 @@ -65,23 +66,25 @@ type Config struct { } // New creates a new ST7735 connection. The SPI wire must already be configured. -func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device { +func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin pin.Output) Device { return NewOf[pixel.RGB565BE](bus, resetPin, dcPin, csPin, blPin) } // NewOf creates a new ST7735 connection with a particular pixel format. The SPI // wire must already be configured. -func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) DeviceOf[T] { - dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - blPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) +func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin pin.Output) DeviceOf[T] { + // IMPORTANT: pin configuration should really be done outside of this driver, + // but for backwards compatibility with existing code, we do it here. + legacy.ConfigurePinOut(dcPin) + legacy.ConfigurePinOut(resetPin) + legacy.ConfigurePinOut(csPin) + legacy.ConfigurePinOut(blPin) return DeviceOf[T]{ bus: bus, - dcPin: dcPin, - resetPin: resetPin, - csPin: csPin, - blPin: blPin, + dcPin: dcPin.Set, + resetPin: resetPin.Set, + csPin: csPin.Set, + blPin: blPin.Set, } } @@ -423,7 +426,7 @@ func (d *DeviceOf[T]) Data(data uint8) { // Tx sends data to the display func (d *DeviceOf[T]) Tx(data []byte, isCommand bool) { - d.dcPin.Set(!isCommand) + d.dcPin(!isCommand) d.bus.Tx(data, nil) } diff --git a/st7789/st7789.go b/st7789/st7789.go index 5db2402..b7e30d1 100644 --- a/st7789/st7789.go +++ b/st7789/st7789.go @@ -513,8 +513,8 @@ func (d *DeviceOf[T]) setRotation(rotation Rotation) error { d.columnOffset = 0 case drivers.Rotation90: madctl = MADCTL_MX | MADCTL_MV - d.rowOffset = 0 - d.columnOffset = 0 + d.rowOffset = d.columnOffsetCfg + d.columnOffset = d.rowOffsetCfg case drivers.Rotation180: madctl = MADCTL_MX | MADCTL_MY d.rowOffset = d.rowOffsetCfg @@ -593,8 +593,18 @@ func (d *DeviceOf[T]) SetScrollArea(topFixedArea, bottomFixedArea int16) { // The screen doesn't use the full 320 pixel height. // Enlarge the bottom fixed area to fill the 320 pixel height, so that // bottomFixedArea starts from the visible bottom of the screen. - topFixedArea += d.rowOffset - bottomFixedArea += (320 - d.height) - d.rowOffset + // + // VSCRDEF/VSCRSADD always operate on physical frame memory rows (0-319), + // regardless of MADCTL. For rotations with MV set (90°/270°), CASET + // addresses physical rows due to row/column exchange, so the physical row + // offset is d.columnOffset (= rowOffsetCfg). For other rotations, + // d.rowOffset is the physical row offset. + physRowOffset := d.rowOffset + if d.rotation == drivers.Rotation90 || d.rotation == drivers.Rotation270 { + physRowOffset = d.columnOffset + } + topFixedArea += physRowOffset + bottomFixedArea += (320 - d.height) - physRowOffset } if d.rotation == drivers.Rotation180 { // The screen is rotated by 180°, so we have to switch the top and @@ -613,10 +623,20 @@ func (d *DeviceOf[T]) SetScrollArea(topFixedArea, bottomFixedArea int16) { // SetScroll sets the vertical scroll address of the display. func (d *DeviceOf[T]) SetScroll(line int16) { - if d.rotation == drivers.Rotation180 { + switch d.rotation { + case drivers.Rotation90: + // With MV set, hardware scroll operates on physical rows, which map to the + // visual X axis. Add the physical row offset (d.columnOffset = rowOffsetCfg) + // so that line=0 addresses the first visible physical row. + line = line + d.columnOffset + case drivers.Rotation180: // The screen is rotated by 180°, so we have to invert the scroll line // (taking care of the RowOffset). line = (319 - d.rowOffset) - line + case drivers.Rotation270: + // With MV+MY, physical rows map to the visual X axis in reverse direction. + // line=0 addresses the last physical row of the visible area. + line = (d.columnOffset + d.height - 1) - line } d.buf[0] = uint8(line >> 8) d.buf[1] = uint8(line) diff --git a/sx127x/registers.go b/sx127x/registers.go index 903eb51..9dd7379 100644 --- a/sx127x/registers.go +++ b/sx127x/registers.go @@ -96,6 +96,12 @@ const ( SX127X_OPMODE_RX_SINGLE = uint8(0x06) SX127X_OPMODE_CAD = uint8(0x07) + SX127X_OPMODE_LOW_FREQUENCY = uint8(0x4) + + SX127X_OPMODE_MODULATION_MASK = uint8(0x60) + SX127X_OPMODE_MODULATION_FSK = uint8(0x0) + SX127X_OPMODE_MODULATION_OOK = uint8(0x20) + SX127X_LORA_MAC_PUBLIC_SYNCWORD = 0x34 SX127X_LORA_MAC_PRIVATE_SYNCWORD = 0x14 ) diff --git a/sx127x/sx127x.go b/sx127x/sx127x.go index bced11a..8b23929 100644 --- a/sx127x/sx127x.go +++ b/sx127x/sx127x.go @@ -25,9 +25,9 @@ type Device struct { rstPin machine.Pin // GPIO for reset radioEventChan chan lora.RadioEvent // Channel for Receiving events loraConf lora.Config // Current Lora configuration - controller RadioController // to manage interactions with the radio + controller RadioController // to manage interrupts with the radio deepSleep bool // Internal Sleep state - deviceType int // sx1261,sx1262,sx1268 (defaults sx1261) + deviceType int // sx1272, sx1273, sx1276, sx1279 (defaults sx1276) spiTxBuf []byte // global Tx buffer to avoid heap allocations in interrupt spiRxBuf []byte // global Rx buffer to avoid heap allocations in interrupt } @@ -43,7 +43,7 @@ func (d *Device) GetRadioEventChan() chan lora.RadioEvent { } // New creates a new SX127x connection. The SPI bus must already be configured. -func New(spi machine.SPI, rstPin machine.Pin) *Device { +func New(spi drivers.SPI, rstPin machine.Pin) *Device { k := Device{ spi: spi, rstPin: rstPin, @@ -65,6 +65,11 @@ func (d *Device) SetRadioController(rc RadioController) error { return nil } +// Specify device type (sx1272, sx1273, sx1276, sx1279) +func (d *Device) SetDeviceType(devType int) { + d.deviceType = devType +} + // Reset re-initialize the sx127x device func (d *Device) Reset() { d.rstPin.Low() @@ -81,9 +86,11 @@ func (d *Device) DetectDevice() bool { // ReadRegister reads register value func (d *Device) ReadRegister(reg uint8) uint8 { - d.controller.SetNss(false) + if d.controller != nil { + d.controller.SetNss(false) + } + // Send register - //d.spiTxBuf = []byte{reg & 0x7f} d.spiTxBuf = d.spiTxBuf[:0] d.spiTxBuf = append(d.spiTxBuf, byte(reg&0x7f)) d.spi.Tx(d.spiTxBuf, nil) @@ -91,13 +98,19 @@ func (d *Device) ReadRegister(reg uint8) uint8 { d.spiRxBuf = d.spiRxBuf[:0] d.spiRxBuf = append(d.spiRxBuf, 0) d.spi.Tx(nil, d.spiRxBuf) - d.controller.SetNss(true) + if d.controller != nil { + d.controller.SetNss(true) + } + return d.spiRxBuf[0] } // WriteRegister writes value to register func (d *Device) WriteRegister(reg uint8, value uint8) uint8 { - d.controller.SetNss(false) + if d.controller != nil { + d.controller.SetNss(false) + } + // Send register d.spiTxBuf = d.spiTxBuf[:0] d.spiTxBuf = append(d.spiTxBuf, byte(reg|0x80)) @@ -108,7 +121,10 @@ func (d *Device) WriteRegister(reg uint8, value uint8) uint8 { d.spiRxBuf = d.spiRxBuf[:0] d.spiRxBuf = append(d.spiRxBuf, 0) d.spi.Tx(d.spiTxBuf, d.spiRxBuf) - d.controller.SetNss(true) + if d.controller != nil { + d.controller.SetNss(true) + } + return d.spiRxBuf[0] } @@ -119,9 +135,20 @@ func (d *Device) SetOpMode(mode uint8) { d.WriteRegister(SX127X_REG_OP_MODE, new) } -// SetOpMode changes the sx1276 mode +// SetOpModeLora changes the sx1276 mode to lora. func (d *Device) SetOpModeLora() { - d.WriteRegister(SX127X_REG_OP_MODE, SX127X_OPMODE_LORA) + d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)|SX127X_OPMODE_LORA) +} + +// SetOpModeFsk changes the sx1276 mode to fsk/ook. +func (d *Device) SetOpModeFsk() { + d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)&^SX127X_OPMODE_LORA) +} + +// SetModulationType changes the modulation type (SX127X_OPMODE_MODULATION_FSK, SX127X_OPMODE_MODULATION_OOK) +func (d *Device) SetModulationType(typ uint8) { + cleared := d.ReadRegister(SX127X_REG_OP_MODE) &^ SX127X_OPMODE_MODULATION_MASK + d.WriteRegister(SX127X_REG_OP_MODE, cleared|typ) } // GetVersion returns hardware version of sx1276 chipset @@ -244,9 +271,9 @@ func (d *Device) SetLowDataRateOptim(val uint8) { // SetLowFrequencyModeOn enables Low Data Rate Optimization func (d *Device) SetLowFrequencyModeOn(val bool) { if val { - d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)|0x04) + d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)|SX127X_OPMODE_LOW_FREQUENCY) } else { - d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)&0xfb) + d.WriteRegister(SX127X_REG_OP_MODE, d.ReadRegister(SX127X_REG_OP_MODE)&^SX127X_OPMODE_LOW_FREQUENCY) } } diff --git a/sx128x/README.md b/sx128x/README.md new file mode 100644 index 0000000..1dd0198 --- /dev/null +++ b/sx128x/README.md @@ -0,0 +1,7 @@ +# SX128x Radio +Radio from Semtech in the 2.4 GHz band. This driver uses SPI to communicate with the radio instead of the alternative UART interface. + +## Supported Chips +- [SX1280](https://www.semtech.com/products/wireless-rf/lora-connect/sx1280) +- [SX1281](https://www.semtech.com/products/wireless-rf/lora-connect/sx1281) + diff --git a/sx128x/commands.go b/sx128x/commands.go new file mode 100644 index 0000000..fc294cd --- /dev/null +++ b/sx128x/commands.go @@ -0,0 +1,52 @@ +package sx128x + +const ( + // SX128X SPI commands + cmdGetStatus = uint8(0xC0) + + // Register Access Operations + cmdWriteRegister = uint8(0x18) + cmdReadRegister = uint8(0x19) + + // Data Buffer Operations + cmdWriteBuffer = uint8(0x1A) + cmdReadBuffer = uint8(0x1B) + + // Radio Operation Modes + cmdSetSleep = uint8(0x84) + cmdSetStandby = uint8(0x80) + cmdSetFS = uint8(0xC1) + cmdSetTx = uint8(0x83) + cmdSetRx = uint8(0x82) + cmdSetRxDutyCycle = uint8(0x94) + cmdSetLongPreamble = uint8(0x9B) + cmdSetCAD = uint8(0xC5) + cmdSetTxContinuousWave = uint8(0xD1) + cmdSetContinuousPreamble = uint8(0xD2) + cmdSetAutoTx = uint8(0x98) + cmdSetAutoFS = uint8(0x9E) + + // Radio Configuration + cmdSetPacketType = uint8(0x8A) + cmdGetPacketType = uint8(0x03) + cmdSetRFFrequency = uint8(0x86) + cmdSetTxParams = uint8(0x8E) + cmdSetCADParams = uint8(0x88) + cmdSetBufferBaseAddress = uint8(0x8F) + cmdSetModulationParams = uint8(0x8B) + cmdSetPacketParams = uint8(0x8C) + + // Communication Status Information + cmdGetRxBufferStatus = uint8(0x17) + cmdGetPacketStatus = uint8(0x1D) + cmdGetRSSIInst = uint8(0x1F) + + // IRQ Handling + cmdSetDIOIRQParams = uint8(0x8D) + cmdGetIRQStatus = uint8(0x15) + cmdClearIRQStatus = uint8(0x97) + + // Miscellaneous + cmdSetRegulatorMode = uint8(0x96) + cmdSetSaveContext = uint8(0xD5) +) diff --git a/sx128x/constants.go b/sx128x/constants.go new file mode 100644 index 0000000..aa0e17d --- /dev/null +++ b/sx128x/constants.go @@ -0,0 +1,356 @@ +package sx128x + +type SleepConfig uint8 +type StandbyConfig uint8 +type PeriodBase uint8 +type PacketType uint8 +type RadioRampTime uint8 +type CadSymbolNum uint8 + +// GFSK Modulation Params +type GFSKBLEBitrateBandwidth uint8 +type ModulationIndex uint8 +type ModulationShaping uint8 + +// GFSK Packet Params +type GFSKPreambleLength uint8 +type GFSKSyncWordLength uint8 +type GFSKSyncWordMatch uint8 +type GFSKHeaderType uint8 +type GFSKCrcType uint8 + +// BLE Packet Params +type BLEConnectionState uint8 +type BLECrcType uint8 +type BLETestPayload uint8 + +// FLRC Modulation Params +type FLRCBitrateBandwidth uint8 +type FLRCCodingRate uint8 + +// FLRC Packet Params +type FLRCPreambleLength uint8 +type FLRCSyncWordLength uint8 +type FLRCSyncWordMatch uint8 +type FLRCHeaderType uint8 +type FLRCCrcType uint8 + +// LoRa Modulation Params +type LoRaSpreadingFactor uint8 +type LoRaBandwidth uint8 +type LoRaCodingRate uint8 + +// LoRa Packet Params +type LoRaHeaderType uint8 +type LoRaCrcType uint8 +type LoRaIqType uint8 + +// Misc +type RegulatorMode uint8 +type IRQMask = uint16 +type CircuitMode uint8 +type CommandStatus uint8 + +// Packet Status +type GFSKPacketInfo uint8 +type BLEPacketInfo uint8 +type FLRCPacketInfo uint8 + +const ( + whiteningDisable = 0x00 + whiteningEnable = 0x08 + + // Circuit Mode + circuitModeMask = uint8(0b11100000) + CIRCUIT_MODE_STDBY_RC = CircuitMode(0x2) + CIRCUIT_MODE_STDBY_XOSC = CircuitMode(0x3) + CIRCUIT_MODE_FS = CircuitMode(0x4) + CIRCUIT_MODE_RX = CircuitMode(0x5) + CIRCUIT_MODE_TX = CircuitMode(0x6) + + // Command Status + commandStatusMask = uint8(0b00011100) + COMMAND_STATUS_SUCCESS = CommandStatus(0x1) + COMMAND_STATUS_DATA_AVAILABLE = CommandStatus(0x2) + COMMAND_STATUS_TIMEOUT = CommandStatus(0x3) + COMMAND_STATUS_PROCESSING_ERROR = CommandStatus(0x4) + COMMAND_STATUS_EXECUTION_ERROR = CommandStatus(0x5) + COMMAND_STATUS_TX_DONE = CommandStatus(0x6) + + // SleepConfig + SLEEP_DATA_BUFFER_RETAIN = SleepConfig(2) + SLEEP_DATA_RAM_RETAIN = SleepConfig(1) + + // StandbyConfig + STANDBY_RC = StandbyConfig(0) + STANDBY_XOSC = StandbyConfig(1) + + // PeriodBase + PERIOD_BASE_15_625_US = PeriodBase(0) + PERIOD_BASE_62_5_US = PeriodBase(1) + PERIOD_BASE_1_MS = PeriodBase(2) + PERIOD_BASE_4_MS = PeriodBase(3) + + // PacketType + PACKET_TYPE_GFSK = PacketType(0x00) // default + PACKET_TYPE_LORA = PacketType(0x01) + PACKET_TYPE_RANGING = PacketType(0x02) + PACKET_TYPE_FLRC = PacketType(0x03) + PACKET_TYPE_BLE = PacketType(0x04) + + // RampTime + RADIO_RAMP_02_US = RadioRampTime(0x00) + RADIO_RAMP_04_US = RadioRampTime(0x20) + RADIO_RAMP_06_US = RadioRampTime(0x40) + RADIO_RAMP_08_US = RadioRampTime(0x60) + RADIO_RAMP_10_US = RadioRampTime(0x80) + RADIO_RAMP_12_US = RadioRampTime(0xA0) + RADIO_RAMP_16_US = RadioRampTime(0xC0) + RADIO_RAMP_20_US = RadioRampTime(0xE0) + + // CadSymbolNum + LORA_CAD_01_SYMBOL = CadSymbolNum(0x00) + LORA_CAD_02_SYMBOLS = CadSymbolNum(0x20) + LORA_CAD_04_SYMBOLS = CadSymbolNum(0x40) + LORA_CAD_08_SYMBOLS = CadSymbolNum(0x60) + LORA_CAD_16_SYMBOLS = CadSymbolNum(0x80) + + // GFSK Modulation Params + // Bitrate + Bandwidth - same for BLE + GFSK_BLE_BR_2_000_BW_2_4 = GFSKBLEBitrateBandwidth(0x04) + GFSK_BLE_BR_1_600_BW_2_4 = GFSKBLEBitrateBandwidth(0x28) + GFSK_BLE_BR_1_000_BW_2_4 = GFSKBLEBitrateBandwidth(0x4C) + GFSK_BLE_BR_1_000_BW_1_2 = GFSKBLEBitrateBandwidth(0x45) + GFSK_BLE_BR_0_800_BW_2_4 = GFSKBLEBitrateBandwidth(0x70) + GFSK_BLE_BR_0_800_BW_1_2 = GFSKBLEBitrateBandwidth(0x69) + GFSK_BLE_BR_0_500_BW_1_2 = GFSKBLEBitrateBandwidth(0x8D) + GFSK_BLE_BR_0_500_BW_0_6 = GFSKBLEBitrateBandwidth(0x86) + GFSK_BLE_BR_0_400_BW_1_2 = GFSKBLEBitrateBandwidth(0xB1) + GFSK_BLE_BR_0_400_BW_0_6 = GFSKBLEBitrateBandwidth(0xAA) + GFSK_BLE_BR_0_250_BW_0_6 = GFSKBLEBitrateBandwidth(0xCE) + GFSK_BLE_BR_0_250_BW_0_3 = GFSKBLEBitrateBandwidth(0xC7) + GFSK_BLE_BR_0_125_BW_0_3 = GFSKBLEBitrateBandwidth(0xEF) + + // Modulation Index - same for BLE + MOD_IND_0_35 = ModulationIndex(0x00) + MOD_IND_0_5 = ModulationIndex(0x01) + MOD_IND_0_75 = ModulationIndex(0x02) + MOD_IND_1_00 = ModulationIndex(0x03) + MOD_IND_1_25 = ModulationIndex(0x04) + MOD_IND_1_50 = ModulationIndex(0x05) + MOD_IND_1_75 = ModulationIndex(0x06) + MOD_IND_2_00 = ModulationIndex(0x07) + MOD_IND_2_25 = ModulationIndex(0x08) + MOD_IND_2_50 = ModulationIndex(0x09) + MOD_IND_2_75 = ModulationIndex(0x0A) + MOD_IND_3_00 = ModulationIndex(0x0B) + MOD_IND_3_25 = ModulationIndex(0x0C) + MOD_IND_3_50 = ModulationIndex(0x0D) + MOD_IND_3_75 = ModulationIndex(0x0E) + MOD_IND_4_00 = ModulationIndex(0x0F) + + // Modulation Shaping - same for BLE and FLRC + MOD_SHAPING_OFF = ModulationShaping(0x00) + MOD_SHAPING_1_0 = ModulationShaping(0x10) + MOD_SHAPING_0_5 = ModulationShaping(0x20) + + // GFSK Packet Params + // Preamble Length + GFSK_PREAMBLE_LENGTH_04_BITS = GFSKPreambleLength(0x00) + GFSK_PREAMBLE_LENGTH_08_BITS = GFSKPreambleLength(0x10) + GFSK_PREAMBLE_LENGTH_12_BITS = GFSKPreambleLength(0x20) + GFSK_PREAMBLE_LENGTH_16_BITS = GFSKPreambleLength(0x30) + GFSK_PREAMBLE_LENGTH_20_BITS = GFSKPreambleLength(0x40) + GFSK_PREAMBLE_LENGTH_24_BITS = GFSKPreambleLength(0x50) + GFSK_PREAMBLE_LENGTH_28_BITS = GFSKPreambleLength(0x60) + GFSK_PREAMBLE_LENGTH_32_BITS = GFSKPreambleLength(0x70) + + // Sync Word Length + GFSK_SYNC_WORD_LEN_1_B = GFSKSyncWordLength(0x00) + GFSK_SYNC_WORD_LEN_2_B = GFSKSyncWordLength(0x02) + GFSK_SYNC_WORD_LEN_3_B = GFSKSyncWordLength(0x04) + GFSK_SYNC_WORD_LEN_4_B = GFSKSyncWordLength(0x06) + GFSK_SYNC_WORD_LEN_5_B = GFSKSyncWordLength(0x08) + + // Sync Word Match + GFSK_SYNCWORD_MATCH_OFF = GFSKSyncWordMatch(0x00) + GFSK_SYNCWORD_MATCH_1 = GFSKSyncWordMatch(0x10) + GFSK_SYNCWORD_MATCH_2 = GFSKSyncWordMatch(0x20) + GFSK_SYNCWORD_MATCH_1_2 = GFSKSyncWordMatch(0x30) + GFSK_SYNCWORD_MATCH_3 = GFSKSyncWordMatch(0x40) + GFSK_SYNCWORD_MATCH_1_3 = GFSKSyncWordMatch(0x50) + GFSK_SYNCWORD_MATCH_2_3 = GFSKSyncWordMatch(0x60) + GFSK_SYNCWORD_MATCH_1_2_3 = GFSKSyncWordMatch(0x70) + + // GFSK Header Type + GFSK_HEADER_FIXED_LENGTH = GFSKHeaderType(0x00) + GFSK_HEADER_VARIABLE_LENGTH = GFSKHeaderType(0x20) + + // GFSK CRC Type + GFSK_CRC_OFF = GFSKCrcType(0x00) + GFSK_CRC_1_BYTE = GFSKCrcType(0x10) + GFSK_CRC_2_BYTES = GFSKCrcType(0x20) + + // BLE Packet Params + // Connection State + BLE_MASTER_SLAVE = BLEConnectionState(0x00) + BLE_ADVERTISER = BLEConnectionState(0x02) + BLE_TX_TEST_MODE = BLEConnectionState(0x04) + BLE_RX_TEST_MODE = BLEConnectionState(0x06) + BLE_RXTX_TEST_MODE = BLEConnectionState(0x08) + + // CRC Type + BLE_CRC_OFF = BLECrcType(0x00) + BLE_CRC_3_BYTES = BLECrcType(0x10) + + // BLE Test Payload + BLE_PAYLOAD_PRBS_9 = BLETestPayload(0x00) + BLE_PAYLOAD_EYELONG_1_0 = BLETestPayload(0x04) + BLE_PAYLOAD_EYESHORT_1_0 = BLETestPayload(0x08) + BLE_PAYLOAD_PRBS_15 = BLETestPayload(0x0C) + BLE_PAYLOAD_ALL_1 = BLETestPayload(0x10) + BLE_PAYLOAD_ALL_0 = BLETestPayload(0x14) + BLE_PAYLOAD_EYELONG_0_1 = BLETestPayload(0x18) + BLE_PAYLOAD_EYESHORT_0_1 = BLETestPayload(0x1C) + + // FLRC Modulation Params + // Bitrate + Bandwidth + FLRC_BR_1_300_BW_1_2 = FLRCBitrateBandwidth(0x45) + FLRC_BR_1_000_BW_1_2 = FLRCBitrateBandwidth(0x69) + FLRC_BR_0_650_BW_0_6 = FLRCBitrateBandwidth(0x86) + FLRC_BR_0_520_BW_0_6 = FLRCBitrateBandwidth(0xAA) + FLRC_BR_0_325_BW_0_3 = FLRCBitrateBandwidth(0xC7) + FLRC_BR_0_260_BW_0_3 = FLRCBitrateBandwidth(0xEB) + + // Coding Rate + FLRC_CR_1_2 = FLRCCodingRate(0x00) // 1/2 + FLRC_CR_3_4 = FLRCCodingRate(0x02) // 3/4 + FLRC_CR_1_0 = FLRCCodingRate(0x04) // 1 + + // FLRC Packet Params + // Preamble Length + FLRC_PREAMBLE_LENGTH_4_BITS = FLRCPreambleLength(0x00) + FLRC_PREAMBLE_LENGTH_8_BITS = FLRCPreambleLength(0x10) + FLRC_PREAMBLE_LENGTH_12_BITS = FLRCPreambleLength(0x20) + FLRC_PREAMBLE_LENGTH_16_BITS = FLRCPreambleLength(0x30) + FLRC_PREAMBLE_LENGTH_20_BITS = FLRCPreambleLength(0x40) + FLRC_PREAMBLE_LENGTH_24_BITS = FLRCPreambleLength(0x50) + FLRC_PREAMBLE_LENGTH_28_BITS = FLRCPreambleLength(0x60) + FLRC_PREAMBLE_LENGTH_32_BITS = FLRCPreambleLength(0x70) + + // Sync Word Length + FLRC_SYNC_WORD_LEN_0 = FLRCSyncWordLength(0x00) + FLRC_SYNC_WORD_LEN_32_BITS = FLRCSyncWordLength(0x04) + + // Sync Word Match + FLRC_SYNC_WORD_MATCH_DISABLE = FLRCSyncWordMatch(0x00) // Disable Sync Word + FLRC_SYNC_WORD_MATCH_1 = FLRCSyncWordMatch(0x10) // Sync Word 1 + FLRC_SYNC_WORD_MATCH_2 = FLRCSyncWordMatch(0x20) // Sync Word 2 + FLRC_SYNC_WORD_MATCH_1_2 = FLRCSyncWordMatch(0x30) // Sync Word 1 or Sync Word 2 + FLRC_SYNC_WORD_MATCH_3 = FLRCSyncWordMatch(0x40) // Sync Word 3 + FLRC_SYNC_WORD_MATCH_1_3 = FLRCSyncWordMatch(0x50) // Sync Word 1 or Sync Word 3 + FLRC_SYNC_WORD_MATCH_2_3 = FLRCSyncWordMatch(0x60) // Sync Word 2 or Sync Word 3 + FLRC_SYNC_WORD_MATCH_1_2_3 = FLRCSyncWordMatch(0x70) // Sync Word 1 or Sync Word 2 or Sync Word 3 + + // Header Type + FLRC_HEADER_FIXED_LENGTH = FLRCHeaderType(0x00) + FLRC_HEADER_VARIABLE_LENGTH = FLRCHeaderType(0x20) + + // CRC Type + FLRC_CRC_OFF = FLRCCrcType(0x00) + FLRC_CRC_1_BYTE = FLRCCrcType(0x10) + FLRC_CRC_2_BYTES = FLRCCrcType(0x20) + FLRC_CRC_3_BYTES = FLRCCrcType(0x30) + + // LoRa Modulation Params + + // SpreadingFactor + LORA_SF_5 = LoRaSpreadingFactor(0x50) + LORA_SF_6 = LoRaSpreadingFactor(0x60) + LORA_SF_7 = LoRaSpreadingFactor(0x70) + LORA_SF_8 = LoRaSpreadingFactor(0x80) + LORA_SF_9 = LoRaSpreadingFactor(0x90) + LORA_SF_10 = LoRaSpreadingFactor(0xA0) + LORA_SF_11 = LoRaSpreadingFactor(0xB0) + LORA_SF_12 = LoRaSpreadingFactor(0xC0) + + // Bandwidth + LORA_BW_1600 = LoRaBandwidth(0x0A) + LORA_BW_800 = LoRaBandwidth(0x18) + LORA_BW_400 = LoRaBandwidth(0x26) + LORA_BW_200 = LoRaBandwidth(0x34) + + // CodingRate + LORA_CR_4_5 = LoRaCodingRate(0x01) + LORA_CR_4_6 = LoRaCodingRate(0x02) + LORA_CR_4_7 = LoRaCodingRate(0x03) + LORA_CR_4_8 = LoRaCodingRate(0x04) + LORA_CR_LI_4_5 = LoRaCodingRate(0x05) + LORA_CR_LI_4_6 = LoRaCodingRate(0x06) + LORA_CR_LI_4_8 = LoRaCodingRate(0x07) + + // LoraPacketParams + // HeaderType + LORA_HEADER_EXPLICIT = LoRaHeaderType(0x00) + LORA_HEADER_IMPLICIT = LoRaHeaderType(0x80) + + // CRC Type + LORA_CRC_ENABLE = LoRaCrcType(0x20) + LORA_CRC_DISABLE = LoRaCrcType(0x00) + + // IQ Type + LORA_IQ_INVERTED = LoRaIqType(0x00) + LORA_IQ_STD = LoRaIqType(0x40) + + // RegulatorMode + REGULATOR_LDO = RegulatorMode(0) + REGULATOR_DC_DC = RegulatorMode(1) + + // IRQ masks + IRQ_ALL_MASK = IRQMask(0xFFFF) + IRQ_NONE_MASK = IRQMask(0x0000) + IRQ_TX_DONE_MASK = IRQMask(0b0000000000000001) + IRQ_RX_DONE_MASK = IRQMask(0b0000000000000010) + IRQ_SYNC_WORD_VALID_MASK = IRQMask(0b0000000000000100) + IRQ_SYNC_WORD_ERROR_MASK = IRQMask(0b0000000000001000) + IRQ_HEADER_VALID_MASK = IRQMask(0b0000000000010000) + IRQ_HEADER_ERROR_MASK = IRQMask(0b0000000000100000) + IRQ_CRC_ERROR_MASK = IRQMask(0b0000000001000000) + IRQ_RANGING_SLAVE_RESPONSE_DONE_MASK = IRQMask(0b0000000010000000) + IRQ_RANGING_SLAVE_RESPONSE_DISCARD_MASK = IRQMask(0b0000000100000000) + IRQ_RANGING_MASTER_RESULT_VALID_MASK = IRQMask(0b0000001000000000) + IRQ_RANGING_MASTER_TIMEOUT_MASK = IRQMask(0b0000010000000000) + IRQ_RANGING_SLAVE_REQUEST_VALID_MASK = IRQMask(0b0000100000000000) + IRQ_CAD_DONE_MASK = IRQMask(0b0001000000000000) + IRQ_CAD_DETECTED_MASK = IRQMask(0b0010000000000000) + IRQ_RX_TX_TIMEOUT_MASK = IRQMask(0b0100000000000000) + IRQ_PREAMBLE_DETECTED_MASK = IRQMask(0b1000000000000000) + IRQ_ADVANCED_RANGING_DONE_MASK = IRQMask(0b1000000000000000) + + // GFSK Packet Info + GFSK_SYNC_ERROR = GFSKPacketInfo(0b1000000) + GFSK_LENGTH_ERROR = GFSKPacketInfo(0b0100000) + GFSK_CRC_ERROR = GFSKPacketInfo(0b0010000) + GFSK_ABORT_ERROR = GFSKPacketInfo(0b0001000) + GFSK_HEADER_RECEIVED = GFSKPacketInfo(0b0000100) + GFSK_PACKET_RECEIVED = GFSKPacketInfo(0b0000010) + GFSK_PACKET_CRTL_BUSY = GFSKPacketInfo(0b0000001) + + // BLE Packet Info + BLE_SYNC_ERROR = BLEPacketInfo(0b1000000) + BLE_LENGTH_ERROR = BLEPacketInfo(0b0100000) + BLE_CRC_ERROR = BLEPacketInfo(0b0010000) + BLE_ABORT_ERROR = BLEPacketInfo(0b0001000) + BLE_HEADER_RECEIVED = BLEPacketInfo(0b0000100) + BLE_PACKET_RECEIVED = BLEPacketInfo(0b0000010) + BLE_PACKET_CRTL_BUSY = BLEPacketInfo(0b0000001) + + // FLRC Packet Info + FLRC_SYNC_ERROR = FLRCPacketInfo(0b1000000) + FLRC_LENGTH_ERROR = FLRCPacketInfo(0b0100000) + FLRC_CRC_ERROR = FLRCPacketInfo(0b0010000) + FLRC_ABORT_ERROR = FLRCPacketInfo(0b0001000) + FLRC_HEADER_RECEIVED = FLRCPacketInfo(0b0000100) + FLRC_PACKET_RECEIVED = FLRCPacketInfo(0b0000010) + FLRC_PACKET_CRTL_BUSY = FLRCPacketInfo(0b0000001) +) diff --git a/sx128x/errors.go b/sx128x/errors.go new file mode 100644 index 0000000..e81b812 --- /dev/null +++ b/sx128x/errors.go @@ -0,0 +1,19 @@ +package sx128x + +import "errors" + +var ( + ErrBusyPinTimeout = errors.New("busy pin timeout") + errDataTooLong = errors.New("data over 256 bytes") + errInvalidSleepConfig = errors.New("invalid sleep config") + errInvalidStandbyConfig = errors.New("invalid standby config") + errFrequencyTooLow = errors.New("frequency below 2.4Ghz") + errFrequencyTooHigh = errors.New("frequency above 2.5Ghz") + errPowerTooLow = errors.New("power level below -18dBm") + errPowerTooHigh = errors.New("power level above 13dBm") + errInvalidPeriodBase = errors.New("invalid period base") + errInvalidPacketType = errors.New("invalid packet type") + errInvalidRegulatorMode = errors.New("invalid regulator mode") + errPayloadLengthTooShort = errors.New("payload length too short") + errPayloadLengthTooLong = errors.New("payload length too long") +) diff --git a/sx128x/registers.go b/sx128x/registers.go new file mode 100644 index 0000000..83cd9db --- /dev/null +++ b/sx128x/registers.go @@ -0,0 +1,69 @@ +package sx128x + +const ( + + // SX128X register map + REG_FIRMWARE_VERSIONS = uint16(0x153) + REG_RX_GAIN = uint16(0x891) + REG_MANUAL_GAIN_SETTING = uint16(0x895) + REG_LNA_GAIN_VALUE = uint16(0x89E) + REG_LNA_GAIN_CONTROL = uint16(0x89F) + REG_SYNCH_PEAK_ATTENUATION = uint16(0x8C2) + REG_PAYLOAD_LENGTH = uint16(0x901) + REG_LORA_HEADER_MODE = uint16(0x903) + REG_RANGING_REQUEST_ADDRESS_BYTE_3 = uint16(0x912) + REG_RANGING_REQUEST_ADDRESS_BYTE_2 = uint16(0x913) + REG_RANGING_REQUEST_ADDRESS_BYTE_1 = uint16(0x914) + REG_RANGING_REQUEST_ADDRESS_BYTE_0 = uint16(0x915) + REG_RANGING_DEVICE_ADDRESS_BYTE_3 = uint16(0x916) + REG_RANGING_DEVICE_ADDRESS_BYTE_2 = uint16(0x917) + REG_RANGING_DEVICE_ADDRESS_BYTE_1 = uint16(0x918) + REG_RANGING_DEVICE_ADDRESS_BYTE_0 = uint16(0x919) + REG_RANGING_FILTER_WINDOW_SIZE = uint16(0x91E) + REG_RESET_RANGING_FILTER = uint16(0x923) + REG_RANGING_RESULT_MUX = uint16(0x924) + REG_SF_ADDITIONAL_CONFIGURATION = uint16(0x925) + REG_RANGING_CALIBRATION_BYTE_2 = uint16(0x92B) + REG_RANGING_CALIBRATION_BYTE_1 = uint16(0x92C) + REG_RANGING_CALIBRATION_BYTE_0 = uint16(0x92D) + REG_RANGING_ID_CHECK_LENGTH = uint16(0x931) + REG_FREQUENCY_ERROR_CORRECTION = uint16(0x93C) + REG_CAD_DETECT_PEAK = uint16(0x942) + REG_LORA_SYNC_WORD_MSB = uint16(0x944) + REG_LORA_SYNC_WORD_LSB = uint16(0x945) + REG_HEADER_CRC = uint16(0x954) + REG_CODING_RATE = uint16(0x950) + REG_FEI_BYTE_2 = uint16(0x954) + REG_FEI_BYTE_1 = uint16(0x955) + REG_FEI_BYTE_0 = uint16(0x956) + REG_RANGING_RESULT_BYTE_2 = uint16(0x961) + REG_RANGING_RESULT_BYTE_1 = uint16(0x962) + REG_RANGING_RESULT_BYTE_0 = uint16(0x963) + REG_RANGING_RSSI = uint16(0x964) + REG_FREEZE_RANGING_RESULT = uint16(0x97F) + REG_PACKET_PREAMBLE_SETTINGS = uint16(0x9C1) + REG_WHITENING_INITIAL_VALUE = uint16(0x9C5) + REG_CRC_POLYNOMIAL_DEFINITION_MSB = uint16(0x9C6) + REG_CRC_POLYNOMIAL_DEFINITION_LSB = uint16(0x9C7) + REG_CRC_POLYNOMIAL_SEED_BYTE_2 = uint16(0x9C7) + REG_CRC_POLYNOMIAL_SEED_BYTE_1 = uint16(0x9C8) + REG_CRC_POLYNOMIAL_SEED_BYTE_0 = uint16(0x9C9) + REG_CRC_MSB_INITIAL_VALUE = uint16(0x9C8) + REG_CRC_LSB_INITIAL_VALUE = uint16(0x9C9) + REG_SYNC_ADDRESS_CONTROL = uint16(0x9CD) + REG_SYNC_ADDRESS_1_BYTE_4 = uint16(0x9CE) + REG_SYNC_ADDRESS_1_BYTE_3 = uint16(0x9CF) + REG_SYNC_ADDRESS_1_BYTE_2 = uint16(0x9D0) + REG_SYNC_ADDRESS_1_BYTE_1 = uint16(0x9D1) + REG_SYNC_ADDRESS_1_BYTE_0 = uint16(0x9D2) + REG_SYNC_ADDRESS_2_BYTE_4 = uint16(0x9D3) + REG_SYNC_ADDRESS_2_BYTE_3 = uint16(0x9D4) + REG_SYNC_ADDRESS_2_BYTE_2 = uint16(0x9D5) + REG_SYNC_ADDRESS_2_BYTE_1 = uint16(0x9D6) + REG_SYNC_ADDRESS_2_BYTE_0 = uint16(0x9D7) + REG_SYNC_ADDRESS_3_BYTE_4 = uint16(0x9D8) + REG_SYNC_ADDRESS_3_BYTE_3 = uint16(0x9D9) + REG_SYNC_ADDRESS_3_BYTE_2 = uint16(0x9DA) + REG_SYNC_ADDRESS_3_BYTE_1 = uint16(0x9DB) + REG_SYNC_ADDRESS_3_BYTE_0 = uint16(0x9DC) +) diff --git a/sx128x/sx128x.go b/sx128x/sx128x.go new file mode 100644 index 0000000..a477fc6 --- /dev/null +++ b/sx128x/sx128x.go @@ -0,0 +1,768 @@ +package sx128x + +import ( + "runtime" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/pin" +) + +type Device struct { + spi drivers.SPI + nssPin pin.Output + resetPin pin.Output + busyPin pin.Input + spiTxBuf []byte + spiRxBuf []byte +} + +func New(spi drivers.SPI, nssPin pin.Output, resetPin pin.Output, busyPin pin.Input) *Device { + return &Device{ + spi: spi, + nssPin: nssPin, + resetPin: resetPin, + busyPin: busyPin, + spiTxBuf: make([]byte, 256), // TODO: optimize buffer size + spiRxBuf: make([]byte, 256), + } +} + +func (d *Device) Reset() { + d.resetPin.Set(false) + time.Sleep(10 * time.Millisecond) + d.resetPin.Set(true) + time.Sleep(10 * time.Millisecond) +} + +func (d *Device) WaitWhileBusy(timeout time.Duration) error { + // largest busy period is on boot with around ~400ish this should be more than enough + now := time.Now() + for d.busyPin.Get() { + if time.Since(now) > timeout { + return ErrBusyPinTimeout + } + runtime.Gosched() + } + return nil +} + +// Get tranceiver status, returns circuit mode and command status +func (d *Device) GetStatus() (CircuitMode, CommandStatus, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return 0, 0, err + } + d.nssPin.Set(false) + status, err := d.spi.Transfer(cmdGetStatus) + d.nssPin.Set(true) + + if err != nil { + return 0, 0, err + } + + circuitMode := (status & circuitModeMask) >> 5 + commandStatus := (status & commandStatusMask) >> 2 + return CircuitMode(circuitMode), CommandStatus(commandStatus), nil +} + +func (d *Device) WriteRegister(addr uint16, data []byte) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdWriteRegister, uint8((addr>>8)&0xFF), uint8(addr&0xFF)) + d.spiTxBuf = append(d.spiTxBuf, data...) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +func (d *Device) ReadRegister(addr uint16) (uint8, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return 0, err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdReadRegister, uint8((addr&0xFF00)>>8), uint8(addr&0x00FF), 0x00, 0x00) + d.spiRxBuf = d.spiRxBuf[:5] + err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf) + d.nssPin.Set(true) + if err != nil { + return 0, err + } + return d.spiRxBuf[4], nil +} + +func (d *Device) WriteBuffer(offset uint8, data []byte) error { + if len(data) > 256 { + return errDataTooLong + } + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdWriteBuffer, offset) + d.spiTxBuf = append(d.spiTxBuf, data...) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Read data from the payload buffer starting at the given offset with the given length +func (d *Device) ReadBuffer(offset uint8, length uint8) ([]byte, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return nil, err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdReadBuffer, offset, 0x00) + for i := uint8(0); i < length; i++ { + d.spiTxBuf = append(d.spiTxBuf, 0x00) + } + d.spiRxBuf = d.spiRxBuf[:len(d.spiTxBuf)] + err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf) + d.nssPin.Set(true) + if err != nil { + return nil, err + } + return d.spiRxBuf[3:], nil +} + +// Set the device into sleep mode with the given configuration: 0 (no retention), 1 (ram retentation), 2 (buffer retention) or 3 (ram and buffer retention) +func (d *Device) SetSleep(sleepConfig SleepConfig) error { + if sleepConfig > (SLEEP_DATA_BUFFER_RETAIN | SLEEP_DATA_RAM_RETAIN) { + return errInvalidSleepConfig + } + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetSleep, uint8(sleepConfig)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Put device into standby mode, 0 (RC) or 1 (XOSC) +func (d *Device) SetStandby(standbyConfig StandbyConfig) error { + if standbyConfig > STANDBY_XOSC { // XOSC is the highest standby config anything higher is invalid + return errInvalidStandbyConfig + } + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetStandby, uint8(standbyConfig)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Set the device into Frequency Synthesizer mode +func (d *Device) SetFs() error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetFS) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +func checkPeriodBase(periodBase PeriodBase) error { + if periodBase > PERIOD_BASE_4_MS { // 4ms is the highest period base anything higher is invalid + return errInvalidPeriodBase + } + return nil +} + +// Sets the device in transmit mode, the IRQ status should be cleared before using this command +// timout is determined by periodBase * periodBaseCount +func (d *Device) SetTx(periodBase PeriodBase, periodBaseCount uint16) error { + err := checkPeriodBase(periodBase) + if err != nil { + return err + } + err = d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetTx, uint8(periodBase), uint8((periodBaseCount>>8)&0xFF), uint8(periodBaseCount&0xFF)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Sets the device in receive mode, the IRQ status should be cleared before using this command +// timeout is determined by periodBase * periodBaseCount +func (d *Device) SetRx(periodBase PeriodBase, periodBaseCount uint16) error { + err := checkPeriodBase(periodBase) + if err != nil { + return err + } + err = d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetRx, uint8(periodBase), uint8((periodBaseCount>>8)&0xFF), uint8(periodBaseCount&0xFF)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Sets the device in a continuous receive mode, it enters receive mode with a timeout of periodBase * rxPeriodBaseCount. +// If no packet is received it will enter sleep mode for periodBase * sleepPeriodBaseCount before re-entering receive mode. +// The loop is exited when a packet is received or the device is put into standby mode. +func (d *Device) SetRxDutyCycle(periodBase PeriodBase, rxPeriodBaseCount uint16, sleepPeriodBaseCount uint16) error { + err := checkPeriodBase(periodBase) + if err != nil { + return err + } + err = d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetRxDutyCycle, uint8(periodBase), uint8((rxPeriodBaseCount&0xFF00)>>8), uint8(rxPeriodBaseCount&0x00FF)) + d.spiTxBuf = append(d.spiTxBuf, uint8((sleepPeriodBaseCount&0xFF00)>>8), uint8(sleepPeriodBaseCount&0x00FF)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Sets the transceiver into Long Preamble mode, and can only be used with either the LoRa mode and GFSK mode +func (d *Device) SetLongPreamble(enable bool) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetLongPreamble) + if enable { + d.spiTxBuf = append(d.spiTxBuf, 1) + } else { + d.spiTxBuf = append(d.spiTxBuf, 0) + } + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Channel activity detection (CAD) is a LoRa specific mode of operation where the device searches for a LoRa signal. +// After search has completed, the device returns to STDBY_RC mode. The length of the search is configured via the SetCadParams() command. +func (d *Device) SetCAD() error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetCAD) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Test command to generate a Continuous Wave (RF tone) at a selected frequency and output power +// The device remains in Tx Continuous Wave until the host sends a mode configuration command. +func (d *Device) SetTxContinuousWave() error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetTxContinuousWave) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Test command to generate an infinite sequence of alternating ‘0’s and ‘1’s in +// GFSK modulation and symbol 0 in LoRa. The device remains in transmit until the host sends a mode configuration command. +func (d *Device) SetTxContinuousPreamble() error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetContinuousPreamble) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// This command allows the transceiver to send a packet at a user programmable time after the end of a packet reception. +// This is useful for Bluetooth Low Energy (BLE) compatibility which requires the transceiver to be able to send back a response 150µs after a packet reception. +func (d *Device) SetAutoTx(timeUs uint16) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetAutoTx, uint8((timeUs&0xFF00)>>8), uint8(timeUs&0x00FF)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Modifies the chip behavior so that the state following a Rx or Tx operation is FS and not standby. +// This allows for faster transitions between Rx and/or Tx. +func (d *Device) SetAutoFs(enable bool) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetAutoFS) + if enable { + d.spiTxBuf = append(d.spiTxBuf, 1) + } else { + d.spiTxBuf = append(d.spiTxBuf, 0) + } + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Choose between GFSK, LoRa, Ranging, FLRC or BLE packet types, this will affect the available configuration parameters and the structure of the packet +func (d *Device) SetPacketType(packetType PacketType) error { + if packetType > PACKET_TYPE_BLE { // BLE is the highest packet type anything higher is invalid. + return errInvalidPacketType + } + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetPacketType, uint8(packetType)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Get the currently configured packet type, this will be 0 (GFSK), 1 (LoRa), 2 (Ranging), 3 (FLRC) or 4 (BLE) +func (d *Device) GetPacketType() (PacketType, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return 0, err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdGetPacketType, 0x00, 0x00) + d.spiRxBuf = d.spiRxBuf[:3] + err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf) + d.nssPin.Set(true) + if err != nil { + return 0, err + } + return PacketType(d.spiRxBuf[2]), nil +} + +// Set the RF frequency in Hz, must be between 2.4 GHz and 2.5 GHz +func (d *Device) SetRfFrequency(frequencyHz uint32) error { + if frequencyHz < 2400000000 { + return errFrequencyTooLow + } + if frequencyHz > 2500000000 { + return errFrequencyTooHigh + } + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + rfFrequency := uint32((uint64(frequencyHz) << 18) / 52000000) + d.spiTxBuf = append(d.spiTxBuf, cmdSetRFFrequency, uint8((rfFrequency>>16)&0xFF), uint8((rfFrequency>>8)&0xFF), uint8(rfFrequency&0xFF)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Set the output power in dBm, must be between -18 and 13 dBm, and the ramp time +func (d *Device) SetTxParams(powerdBm int8, rampTime RadioRampTime) error { + if powerdBm < -18 { + return errPowerTooLow + } + if powerdBm > 13 { + return errPowerTooHigh + } + + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + adjustedPower := uint8(powerdBm + 18) + d.spiTxBuf = append(d.spiTxBuf, cmdSetTxParams, adjustedPower, uint8(rampTime)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Set the number of symbols used for channel activity detection which determines the sensitivity of the detection. +// This is only applicable in LoRa mode. +func (d *Device) SetCadParams(cadSymbolNum uint8) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetCADParams, cadSymbolNum) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Set the base address for the internal buffer for Tx and Rx operations. +// When transmitting or receiving data is read from or written to the buffer starting at the given offset. +func (d *Device) SetBufferBaseAddress(txBase uint8, rxBase uint8) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetBufferBaseAddress, txBase, rxBase) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// The arguments to this function depend on the packet type. It is recommended to use the mode specific functions for a better experience. +// BLE & GFSK: BitrateBandwidth, ModulationIndex, ModulationShaping +// FLRC: BitrateBandwidth, CodingRate, ModulationShaping +// LoRa & Ranging: SpreadingFactor, Bandwidth, CodingRate +func (d *Device) SetModulationParams(modParam1, modParam2, modParam3 uint8) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetModulationParams, modParam1, modParam2, modParam3) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +func (d *Device) SetModulationParamsBLE(bitrateBandwidth GFSKBLEBitrateBandwidth, modulationIndex ModulationIndex, modulationShaping ModulationShaping) error { + return d.SetModulationParams(uint8(bitrateBandwidth), uint8(modulationIndex), uint8(modulationShaping)) +} + +func (d *Device) SetModulationParamsGFSK(bitrateBandwidth GFSKBLEBitrateBandwidth, modulationIndex ModulationIndex, modulationShaping ModulationShaping) error { + return d.SetModulationParams(uint8(bitrateBandwidth), uint8(modulationIndex), uint8(modulationShaping)) +} + +func (d *Device) SetModulationParamsFLRC(bitrateBandwidth FLRCBitrateBandwidth, codingRate FLRCCodingRate, modulationShaping ModulationShaping) error { + return d.SetModulationParams(uint8(bitrateBandwidth), uint8(codingRate), uint8(modulationShaping)) +} + +func (d *Device) SetModulationParamsLoRa(spreadingFactor LoRaSpreadingFactor, bandwidth LoRaBandwidth, codingRate LoRaCodingRate) error { + return d.SetModulationParams(uint8(spreadingFactor), uint8(bandwidth), uint8(codingRate)) +} + +// The arguments to this function depend on the packet type. It is recommended to use the mode specific functions for a better experience. +// GFSK & FLRC: PreambleLength, SyncWordLength, SyncWordMatch, HeaderType, PayloadLength, CrcLength, Whitening +// BLE: ConnectionState, CrcLength, BleTestPayload, Whitening +// LoRa & Ranging: PreambleLength, HeaderType, PayloadLength, CRC, InvertIQ/chirp invert +func (d *Device) SetPacketParams(param1, param2, param3, param4, param5, param6, param7 uint8) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetPacketParams, param1, param2, param3, param4, param5, param6, param7) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Set GFSK related packet parameters, this assumes the packet type is already set to GFSK. +// - payloadLength: range of 0-255 +func (d *Device) SetPacketParamsGFSK(preambleLength GFSKPreambleLength, syncWordLength GFSKSyncWordLength, syncWordMatch GFSKSyncWordMatch, headerType GFSKHeaderType, payloadLength uint8, crcLength GFSKCrcType, whitening bool) error { + var whiteningVal uint8 + if whitening { + whiteningVal = whiteningEnable + } else { + whiteningVal = whiteningDisable + } + return d.SetPacketParams(uint8(preambleLength), uint8(syncWordLength), uint8(syncWordMatch), uint8(headerType), payloadLength, uint8(crcLength), whiteningVal) +} + +// Set FLRC related packet parameters, this assumes the packet type is already set to FLRC. +// - payloadLength: range of 6-127 +func (d *Device) SetPacketParamsFLRC(preambleLength FLRCPreambleLength, syncWordLength FLRCSyncWordLength, syncWordMatch FLRCSyncWordMatch, headerType FLRCHeaderType, payloadLength uint8, crcLength FLRCCrcType) error { + if payloadLength < 6 { + return errPayloadLengthTooShort + } + if payloadLength > 127 { + return errPayloadLengthTooLong + } + return d.SetPacketParams(uint8(preambleLength), uint8(syncWordLength), uint8(syncWordMatch), uint8(headerType), payloadLength, uint8(crcLength), whiteningDisable) +} + +// Set BLE related packet parameters, this assumes the packet type is already set to BLE. +func (d *Device) SetPacketParamsBLE(connectionState BLEConnectionState, crcLength BLECrcType, bleTestPayload BLETestPayload, whitening bool) error { + var whiteningVal uint8 + if whitening { + whiteningVal = whiteningEnable + } else { + whiteningVal = whiteningDisable + } + return d.SetPacketParams(uint8(connectionState), uint8(crcLength), uint8(bleTestPayload), whiteningVal, 0, 0, 0) +} + +// Set LoRa related packet parameters, this assumes the packet type is already set to LoRa. +// - payloadLength: range of 1-255 +func (d *Device) SetPacketParamsLoRa(preambleLength uint32, headerType LoRaHeaderType, payloadLength uint8, crcType LoRaCrcType, iqType LoRaIqType) error { + if payloadLength == 0 { + return errPayloadLengthTooShort + } + exponent, mantissa := getExponentAndMantissa(preambleLength) + return d.SetPacketParams(uint8(exponent<<4)|mantissa, uint8(headerType), payloadLength, uint8(crcType), uint8(iqType), 0, 0) +} + +func getExponentAndMantissa(value uint32) (uint8, uint8) { + // pulled from RadioLib https://github.com/jgromes/RadioLib/blob/master/src/modules/SX128x/SX128x.cpp + e := uint8(1) + m := uint8(1) + len := uint32(0) + for e = uint8(1); e <= 15; e++ { + for m = uint8(1); m <= 15; m++ { + len = uint32(m) * (uint32(1 << e)) + if len >= value { + break + } + } + if len >= value { + break + } + } + + return e, m +} + +// Get information about the most recent packet received. +// Return the payload length, the offset in the buffer where the payload starts. +func (d *Device) GetRxBufferStatus() (uint8, uint8, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return 0, 0, err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdGetRxBufferStatus, 0x00, 0x00, 0x00) + d.spiRxBuf = d.spiRxBuf[:4] + err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf) + d.nssPin.Set(true) + if err != nil { + return 0, 0, err + } + return d.spiRxBuf[2], d.spiRxBuf[3], nil +} + +// The return type of this function depends on the packet type. Use mode specific function for typed returns. +// BLE, GFSK & FLRC: unused, rssiSync, errors, status, sync +// LoRa & Ranging: rssiSync, SNR +func (d *Device) GetPacketStatus() (uint8, uint8, uint8, uint8, uint8, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return 0, 0, 0, 0, 0, err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdGetPacketStatus, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00) + d.spiRxBuf = d.spiRxBuf[:7] + err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf) + d.nssPin.Set(true) + if err != nil { + return 0, 0, 0, 0, 0, err + } + return d.spiRxBuf[2], d.spiRxBuf[3], d.spiRxBuf[4], d.spiRxBuf[5], d.spiRxBuf[6], nil +} + +// Get information about the most recent GFSK packet received or transmitted: +// - RSSI of last received packet +// - packet information (each bit represents a different error or status flag) +// - whether the last packet transmission has ended +// - the sync word that was used for the last packet reception (0-3) +func (d *Device) GetPacketStatusGFSK() (float32, GFSKPacketInfo, bool, uint8, error) { + _, rssiSync, packetInfo, status, sync, err := d.GetPacketStatus() + if err != nil { + return 0, 0, false, 0, err + } + return float32(int8(rssiSync)) / 2 * -1, GFSKPacketInfo(packetInfo), status != 0, sync, nil +} + +// Get information about the most recent BLE packet received or transmitted: +// - RSSI of last received packet +// - packet information (each bit represents a different error or status flag) +// - whether the last packet transmission has ended +// - the sync word that was used for the last packet reception (0-1) +func (d *Device) GetPacketStatusBLE() (float32, BLEPacketInfo, bool, uint8, error) { + _, rssiSync, packetInfo, status, sync, err := d.GetPacketStatus() + if err != nil { + return 0, 0, false, 0, err + } + return float32(int8(rssiSync)) / 2 * -1, BLEPacketInfo(packetInfo), status != 0, sync, nil +} + +// Get information about the most recent BLE packet received or transmitted: +// - RSSI of last received packet +// - packet information (each bit represents a different error or status flag) +// - PID field of the received packet +// - NO_ACK field of the received packet +// - PID check status of the current packet +// - whether the last packet transmission has ended +// - the sync word that was used for the last packet reception (0-1) +func (d *Device) GetPacketStatusFLRC() (float32, FLRCPacketInfo, uint8, bool, bool, bool, uint8, error) { + _, rawRSSI, packetInfo, rxTxInfo, sync, err := d.GetPacketStatus() + + rxPid := (rxTxInfo & 0b11000000) >> 6 + noAck := (rxTxInfo & 0b00100000) != 0 + pidCheck := (rxTxInfo & 0b00010000) != 0 + txDone := (rxTxInfo & 0b00000001) != 0 + + if err != nil { + return 0, 0, 0, false, false, false, 0, err + } + return float32(int8(rawRSSI)) / 2 * -1, FLRCPacketInfo(packetInfo), rxPid, noAck, pidCheck, txDone, sync, nil +} + +// Get information about the most recent LoRa packet received: +// - RSSI of last received packet +// - signal-to-noise ratio (SNR) of last received packet +func (d *Device) GetPacketStatusLoRa() (float32, float32, error) { + rawRSSI, rawSnr, _, _, _, err := d.GetPacketStatus() + if err != nil { + return 0, 0, err + } + return float32(int8(rawRSSI)) / 2 * -1, float32(int8(rawSnr)) / 4, nil +} + +// Get the instantaneous RSSI value during reception of the packet +func (d *Device) GetRssiInst() (float32, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return 0, err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdGetRSSIInst, 0x00, 0x00) + d.spiRxBuf = d.spiRxBuf[:3] + err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf) + d.nssPin.Set(true) + if err != nil { + return 0, err + } + return float32(int8(d.spiRxBuf[2])) / 2 * -1, nil +} + +// Configure the overall IRQ mask and the mapping of individual IRQs to the DIO1, DIO2 and DIO3 pins +func (d *Device) SetDioIrqParams(irqMask IRQMask, dio1Mask IRQMask, dio2Mask IRQMask, dio3Mask IRQMask) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetDIOIRQParams, uint8((irqMask&0xFF00)>>8), uint8(irqMask&0x00FF)) + d.spiTxBuf = append(d.spiTxBuf, uint8((dio1Mask&0xFF00)>>8), uint8(dio1Mask&0x00FF)) + d.spiTxBuf = append(d.spiTxBuf, uint8((dio2Mask&0xFF00)>>8), uint8(dio2Mask&0x00FF)) + d.spiTxBuf = append(d.spiTxBuf, uint8((dio3Mask&0xFF00)>>8), uint8(dio3Mask&0x00FF)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Get the current IRQ status. +func (d *Device) GetIrqStatus() (IRQMask, error) { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return 0, err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdGetIRQStatus, 0x00, 0x00, 0x00) + d.spiRxBuf = d.spiRxBuf[:4] + err = d.spi.Tx(d.spiTxBuf, d.spiRxBuf) + d.nssPin.Set(true) + if err != nil { + return 0, err + } + return uint16(d.spiRxBuf[2])<<8 | uint16(d.spiRxBuf[3]), err +} + +// Clear the IRQ bits specified in the irqMask. +func (d *Device) ClearIrqStatus(irqMask IRQMask) error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdClearIRQStatus, uint8((irqMask&0xFF00)>>8), uint8(irqMask&0x00FF)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Switch between the low-dropout regulator (LDO) and the DC-DC converter for internal power regulation. +func (d *Device) SetRegulatorMode(mode RegulatorMode) error { + if mode > REGULATOR_DC_DC { // DC-DC is the highest regulator mode anything higher is invalid + return errInvalidRegulatorMode + } + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetRegulatorMode, uint8(mode)) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} + +// Stores the present context of the radio register values to the Data RAM which will be restored when the device wakes up from sleep mode. +func (d *Device) SetSaveContext() error { + err := d.WaitWhileBusy(time.Second) + if err != nil { + return err + } + d.nssPin.Set(false) + d.spiTxBuf = d.spiTxBuf[:0] + d.spiTxBuf = append(d.spiTxBuf, cmdSetSaveContext) + err = d.spi.Tx(d.spiTxBuf, nil) + d.nssPin.Set(true) + return err +} diff --git a/tmc2209/README.md b/tmc2209/README.md new file mode 100644 index 0000000..7e67ede --- /dev/null +++ b/tmc2209/README.md @@ -0,0 +1,163 @@ +# TMC2209 Go Package + +This package provides a lightweight interface to communicate with the TMC2209 stepper motor driver via UART. It is designed to be used with microcontrollers and supports both default UART communication and custom interface implementations for flexibility. +Currently this package only implements the communications with TMC2209. Functions to handle specific operations such as EnableStealthChop() etc. will need to be implemented by the user. +## Features +- **UART Communication:** Standard communication through UART for controlling the TMC2209 driver. +- **Custom Communication Interfaces:** Allows custom implementations of communication interfaces (e.g., USB, SPI, etc.) via the `RegisterComm` interface. The usecase for this is if you have host system that can "talk" to the microcontroller connected to the TMC2209 +- **Error Handling:** Lightweight error handling for TinyGo compatibility. +- **Register Access:** Provides methods to read and write registers on the TMC2209. + +## Setup + +### Prerequisites +- **TinyGo**: This package is optimized for TinyGo, which is suitable for running Go on microcontrollers and embedded systems. +- **UART Communication**: For microcontrollers with UART support, such as ESP32, STM32, and Raspberry Pi Pico. + + +## Usage +### Using with Microcontrollers (Default UART) + +To use the package with a microcontroller, you need to initialize the UART communication and configure the TMC2209 driver. +Example: + +```aiignore +package main + +import ( + "fmt" + "log" + "github.com/yourusername/tmc2209" + "machine" +) + +func main() { + uart := machine.UART0 + + // Create an instance of the TMC2209 with UART communication + tmc := tmc2209.NewTMC2209(uart, 0x00) // Replace 0x00 with the appropriate address + + // Set up the TMC2209 driver + err := tmc.Setup() + if err != nil { + log.Fatalf("Failed to set up TMC2209: %v", err) + } + + // Write to a register (example: setting a register value) + err = tmc.WriteRegister(0x10, 0x12345678) // Replace 0x10 with the register address and 0x12345678 with the value + if err != nil { + log.Fatalf("Failed to write register: %v", err) + } + + // Read from a register (example: reading a register value) + value, err := tmc.ReadRegister(0x10) + if err != nil { + log.Fatalf("Failed to read register: %v", err) + } + + // Output the read value + fmt.Printf("Register value: 0x%X\n", value) +} + +``` +## Microcontroller Notes + +- **TinyGo Support:** This code is optimized for use with TinyGo on supported microcontrollers. +- **UART Configuration:** Ensure the UART instance is configured correctly for your microcontroller. +- The machine.UART0 in the example is for the default UART on TinyGo-compatible devices like the Raspberry Pi Pico. Check your microcontroller's documentation for the correct UART instance and pin configuration. + +## 2. Custom Interface Implementation + +If you want to use a custom communication interface (e.g., USB, SPI, etc.), you can implement the RegisterComm interface. +Custom Interface Example (e.g., USB Communication): + +``` +package main + +import ( + "fmt" + "log" + "github.com/yourusername/tmc2209" + "yourcustompackage" // Custom package for communication +) + +type CustomComm struct { + // Implement your custom communication method here +} + +func (c *CustomComm) ReadRegister(register uint8, driverIndex uint8) (uint32, error) { + // Implement the register read logic using your custom interface (USB, SPI, etc.) + // For example, send the register read command over USB and read the response + return 0, nil +} + +func (c *CustomComm) WriteRegister(register uint8, value uint32, driverIndex uint8) error { + // Implement the register write logic using your custom interface (USB, SPI, etc.) + // For example, send the register write command over USB + return nil +} + +func main() { + // Create an instance of the TMC2209 with your custom communication interface + customComm := &CustomComm{} + tmc := tmc2209.NewTMC2209(customComm, 0x00) // Replace 0x00 with the appropriate address + + // Set up the TMC2209 driver + err := tmc.Setup() + if err != nil { + log.Fatalf("Failed to set up TMC2209: %v", err) + } + + // Write to a register (example: setting a register value) + err = tmc.WriteRegister(0x10, 0x12345678) // Replace 0x10 with the register address and 0x12345678 with the value + if err != nil { + log.Fatalf("Failed to write register: %v", err) + } + + // Read from a register (example: reading a register value) + value, err := tmc.ReadRegister(0x10) + if err != nil { + log.Fatalf("Failed to read register: %v", err) + } + + // Output the read value + fmt.Printf("Register value: 0x%X\n", value) +} + +``` + +### Custom Interface Notes: + +- RegisterComm Interface: Your custom communication interface (e.g., USB, SPI) should implement the RegisterComm interface. This ensures that the TMC2209 driver can interact with your custom interface for reading and writing registers. +- Error Handling: Implement proper error handling in your custom interface methods (ReadRegister and WriteRegister). + +#### Functions +``` +NewTMC2209(comm RegisterComm, address uint8) *TMC2209 +``` +Creates a new TMC2209 instance with the provided communication interface (comm) and driver address (address). + +```Setup() error``` + +Initializes the communication interface. This is required before interacting with the TMC2209 driver. The method checks if the communication interface is UART and sets it up accordingly. + +```WriteRegister(register uint8, value uint32) error``` + +Writes a value to a specific register on the TMC2209 driver. + +```ReadRegister(register uint8) (uint32, error)``` + +Reads a value from a specific register on the TMC2209 driver. + +### Error Handling + +This package uses a lightweight custom error type (CustomError) to ensure compatibility with TinyGo and reduce external dependencies. +Example Error: + +```CustomError("communication interface not set")``` + + +Created by Amken3d +info@amken3d.us + + diff --git a/tmc2209/address.go b/tmc2209/address.go new file mode 100644 index 0000000..d637f45 --- /dev/null +++ b/tmc2209/address.go @@ -0,0 +1,1409 @@ +package tmc2209 + +import "log" + +// TMC2209 Register addresses +const ( + GCONF = 0x00 + GSTAT = 0x01 + IFCNT = 0x02 + IOIN = 0x06 + IHOLD_IRUN = 0x10 + TPOWERDOWN = 0x11 + TSTEP = 0x12 + TPWMTHRS = 0x13 + TCOOLTHRS = 0x14 + VACTUAL = 0x22 + SGTHRS = 0x40 + SG_RESULT = 0x41 + COOLCONF = 0x42 + MSCNT = 0x6A + MSCURACT = 0x6B + CHOPCONF = 0x6C + DRV_STATUS = 0x6F + PWMCONF = 0x70 + PWM_SCALE = 0x71 + PWM_AUTO = 0x72 + expectedVersion = 0x03 +) + +type RegisterComm interface { + ReadRegister(register uint8, driverIndex uint8) (uint32, error) + WriteRegister(register uint8, value uint32, driverIndex uint8) error +} + +// Register is an interface that all register structs will implement for generic access +type Register interface { + Pack() uint32 + Unpack(value uint32) + GetAddress() uint8 +} + +// NewRegister is a generic function to initialize any register with a given address +func NewRegister(registerAddr uint8) Register { + switch registerAddr { + case IOIN: + return NewIoin() + case PWMCONF: + return NewPWMConf() + case GCONF: + return NewGconf() + case GSTAT: + return NewGstat() + case IFCNT: + return NewIfcnt() + case IHOLD_IRUN: + return NewIholdIrun() + case TPOWERDOWN: + return NewTpowerdown() + case TSTEP: + return NewTstep() + case TPWMTHRS: + return NewTpwmthrs() + case TCOOLTHRS: + return NewTcoolthrs() + case VACTUAL: + return NewVactual() + case SGTHRS: + return NewSgthrs() + case SG_RESULT: + return NewSgResult() + case COOLCONF: + return NewCoolConf() + case MSCNT: + return NewMscnt() + case MSCURACT: + return NewMscuract() + case CHOPCONF: + return NewChopconf() + case DRV_STATUS: + return NewDrvStatus() + case PWM_SCALE: + return NewPwmScale() + case PWM_AUTO: + return NewPwmAuto() + default: + return nil + } +} + +// ReadRegister function using the register constants +func ReadRegister(comm RegisterComm, driverIndex uint8, register uint8) (uint32, error) { + // Read the register value using the comm interface + + value, err := comm.ReadRegister(register, driverIndex) + log.Printf("Request read ", register, driverIndex, value) + if err != nil { + return 0, err + } + return value, nil +} + +// WriteRegister function using the register constants +func WriteRegister(comm RegisterComm, register uint8, driverIndex uint8, value uint32) error { + // Write the value to the register using the comm interface + return comm.WriteRegister(register, value, driverIndex) +} + +// Ioin represents the fields of the IOIN register (0x06) in the TMC2209 +// +// The IOIN register provides access to various inputs and control signals +// that the driver uses to determine its current state. This register contains +// bits related to motor driver status, step input, direction, diagnostics, +// and other control signals. +// +// The structure represents each field in the IOIN register and provides +// methods to pack and unpack these fields into the `Bytes` field (a 32-bit +// packed representation of all fields) for easier access and manipulation. +// +// Fields: +// - Enn: Enables the driver. 1 = Driver enabled, 0 = Driver disabled. +// - Reserved0: Reserved bit, must be set to 0. +// - Ms1: Microstep setting, first bit of the microstep configuration. +// - Ms2: Microstep setting, second bit of the microstep configuration. +// - Diag: Diagnostics flag. Used for fault detection and error reporting. +// - Reserved1: Reserved bit, must be set to 0. +// - PdnSerial: Power-down (sleep) state for the UART interface. 1 = Power down, 0 = Active. +// - Step: Step input signal. 1 = Step signal active, 0 = No step signal. +// - SpreadEn: SpreadCycle enable. 1 = Enable SpreadCycle, 0 = Disable SpreadCycle. +// - Dir: Direction input. 1 = Reverse direction, 0 = Forward direction. +// - Reserved2: Reserved bits, must be set to 0. +// - Version: Driver version information. The version of the IOIN register in the TMC2209 chip. +// +// `Bytes`: A 32-bit value that stores the packed representation of the IOIN register. +// The `Bytes` field is used to manipulate the register's value as a single 32-bit value, +// allowing you to read and write it more efficiently. +type Ioin struct { + Enn uint32 // 1-bit field: Driver enable status (1 = enabled, 0 = disabled) + Reserved0 uint32 // 1-bit field: Reserved, should always be 0 + Ms1 uint32 // 1-bit field: Microstep setting (first bit) + Ms2 uint32 // 1-bit field: Microstep setting (second bit) + Diag uint32 // 1-bit field: Diagnostics flag (error reporting) + Reserved1 uint32 // 1-bit field: Reserved, should always be 0 + PdnSerial uint32 // 1-bit field: Power-down state for the UART interface (1 = power down, 0 = active) + Step uint32 // 1-bit field: Step signal input (1 = active, 0 = inactive) + SpreadEn uint32 // 1-bit field: SpreadCycle enable (1 = enabled, 0 = disabled) + Dir uint32 // 1-bit field: Direction input (1 = reverse, 0 = forward) + Reserved2 uint32 // 14-bit field: Reserved bits, should always be 0 + Version uint32 // 8-bit field: Version information for the driver + Bytes uint32 // 32-bit field: Packed representation of the IOIN register (all fields packed into a single 32-bit value) + RegisterAddr uint8 // The address of the register, in this case, IOIN (0x06) +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +// This method combines all the individual fields (like Enn, Ms1, etc.) +// into a packed 32-bit value that can be written to the register. +func (ioin *Ioin) Pack() uint32 { + ioin.Bytes = (ioin.Enn & 0x01) | // Enn field (1 bit) + ((ioin.Reserved0 & 0x01) << 1) | // Reserved0 field (1 bit) + ((ioin.Ms1 & 0x01) << 2) | // Ms1 field (1 bit) + ((ioin.Ms2 & 0x01) << 3) | // Ms2 field (1 bit) + ((ioin.Diag & 0x01) << 4) | // Diag field (1 bit) + ((ioin.Reserved1 & 0x01) << 5) | // Reserved1 field (1 bit) + ((ioin.PdnSerial & 0x01) << 6) | // PdnSerial field (1 bit) + ((ioin.Step & 0x01) << 7) | // Step field (1 bit) + ((ioin.SpreadEn & 0x01) << 8) | // SpreadEn field (1 bit) + ((ioin.Dir & 0x01) << 9) | // Dir field (1 bit) + ((ioin.Reserved2 & 0x3FFF) << 10) | // Reserved2 field (14 bits) + ((ioin.Version & 0xFF) << 24) // Version field (8 bits) + return ioin.Bytes +} + +// Unpack the Bytes field into the individual fields. +// This method takes the packed 32-bit value from the Bytes field and extracts +// the individual register fields into their corresponding variables. +func (ioin *Ioin) Unpack(uint32) { + ioin.Enn = ioin.Bytes & 0x01 + ioin.Reserved0 = (ioin.Bytes >> 1) & 0x01 + ioin.Ms1 = (ioin.Bytes >> 2) & 0x01 + ioin.Ms2 = (ioin.Bytes >> 3) & 0x01 + ioin.Diag = (ioin.Bytes >> 4) & 0x01 + ioin.Reserved1 = (ioin.Bytes >> 5) & 0x01 + ioin.PdnSerial = (ioin.Bytes >> 6) & 0x01 + ioin.Step = (ioin.Bytes >> 7) & 0x01 + ioin.SpreadEn = (ioin.Bytes >> 8) & 0x01 + ioin.Dir = (ioin.Bytes >> 9) & 0x01 + ioin.Reserved2 = (ioin.Bytes >> 10) & 0x3FFF + ioin.Version = (ioin.Bytes >> 24) & 0xFF +} +func (ioin *Ioin) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, ioin.RegisterAddr) +} +func (ioin *Ioin) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, ioin.RegisterAddr, driverIndex, value) +} +func (ioin *Ioin) GetAddress() uint8 { + return ioin.RegisterAddr +} +func NewIoin() *Ioin { + return &Ioin{ + RegisterAddr: IOIN, + } +} + +// PWMConf represents the fields in the TMC2209 PWMCONF register. +// +// This register controls the Pulse Width Modulation (PWM) configuration for the stepper motor. +// It defines parameters related to the motor's driving mechanism, including PWM offset, +// gradient, frequency, and scaling. These parameters allow fine control over the motor's +// behavior, particularly in the context of energy saving modes like StealthChop. +// +// The fields are as follows: +// - **PwmOfs** (8 bits): The offset for PWM, affecting the motor's drive current. +// - **PwmGrad** (8 bits): The gradient applied to the PWM waveform. It determines the +// rise and fall of the PWM signal, influencing motor smoothness and efficiency. +// - **PwmFreq** (2 bits): PWM frequency control, affecting the switching rate of the motor. +// This value influences the motor's operating frequency and noise characteristics. +// - **PwmAutoscale** (1 bit): This flag enables automatic scaling of the PWM amplitude +// based on the motor’s load. When enabled, the motor adjusts its drive based on current +// requirements, improving energy efficiency and reducing heat generation. +// - **PwmAutograd** (1 bit): Similar to `PwmAutoscale`, but focuses on the gradient +// of the PWM signal, adjusting for the motor's behavior dynamically to maintain efficient +// operation under varying load conditions. +// - **Freewheel** (2 bits): Determines the state of the motor's freewheel functionality, +// which allows the motor to coast when not actively driven, thereby reducing power consumption. +// - **PwmReg** (4 bits): Register for fine-tuning the PWM signal’s behavior. It is typically used +// for customizing the PWM waveform for optimal performance in different conditions. +// - **PwmLim** (4 bits): PWM limit for the motor’s current control. It provides an upper +// bound for the current drive to prevent overheating or excessive current draw. +// +// The `PWMCONF` register allows for precise control over the motor's electrical characteristics, +// contributing to better performance and energy efficiency, particularly in StealthChop mode. +type PWMConf struct { + PwmOfs uint32 // 8 bits + PwmGrad uint32 // 8 bits + PwmFreq uint32 // 2 bits + PwmAutoscale uint32 // 1 bit + PwmAutograd uint32 // 1 bit + Freewheel uint32 // 2 bits + PwmReg uint32 // 4 bits + PwmLim uint32 // 4 bits + Bytes uint32 // 32-bit packed representation of all fields + RegisterAddr uint8 +} + +func (pwm *PWMConf) GetAddress() uint8 { + return pwm.RegisterAddr +} + +// NewPWMConf Initialize PWMConf with register address +func NewPWMConf() *PWMConf { + return &PWMConf{ + RegisterAddr: PWMCONF, + } +} + +// Pack Method to pack the fields into the Bytes field +func (pwm *PWMConf) Pack() uint32 { + // Pack the individual fields into the Bytes field + pwm.Bytes = (pwm.PwmOfs & 0xFF) | + ((pwm.PwmGrad & 0xFF) << 8) | + ((pwm.PwmFreq & 0x03) << 16) | + ((pwm.PwmAutoscale & 0x01) << 18) | + ((pwm.PwmAutograd & 0x01) << 19) | + ((pwm.Freewheel & 0x03) << 20) | + ((pwm.PwmReg & 0x0F) << 24) | + ((pwm.PwmLim & 0x0F) << 28) // PWM_LIM is 4 bits and goes in the last 4 bits + return pwm.Bytes +} + +// Unpack Method to unpack the Bytes field into individual fields +func (pwm *PWMConf) Unpack(uint32) { + // Unpack the Bytes field into individual fields + pwm.PwmOfs = pwm.Bytes & 0xFF + pwm.PwmGrad = (pwm.Bytes >> 8) & 0xFF + pwm.PwmFreq = (pwm.Bytes >> 16) & 0x03 + pwm.PwmAutoscale = (pwm.Bytes >> 18) & 0x01 + pwm.PwmAutograd = (pwm.Bytes >> 19) & 0x01 + pwm.Freewheel = (pwm.Bytes >> 20) & 0x03 + pwm.PwmReg = (pwm.Bytes >> 24) & 0x0F + pwm.PwmLim = (pwm.Bytes >> 28) & 0x0F +} +func (pwm *PWMConf) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, pwm.RegisterAddr) +} +func (pwm *PWMConf) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, driverIndex, pwm.RegisterAddr, value) +} + +// Chopconf represents the fields in the TMC2209 CHOPCONF register. +// +// The CHOPCONF register configures the chopping control for the stepper motor. +// It defines parameters related to the current waveform, step behavior, and +// microstepping resolution. These parameters directly affect the motor's efficiency, +// torque output, and smoothness, especially when operating in StealthChop mode. +// +// The fields are as follows: +// - **Toff** (4 bits): The Off-time parameter for the chopping waveform. +// It sets the duration of the 'off' period of the waveform, which controls how long +// the motor driver remains idle between each pulse. Adjusting this value affects motor efficiency +// and noise characteristics. Lower values allow faster switching and more responsive motors, +// while higher values can improve efficiency at the cost of motor speed. +// - **Hstrt** (3 bits): The Hysteresis start value. This parameter controls the start +// of the hysteresis in the chopping process. It determines when the current will be limited +// based on the hysteresis function. The higher the value, the more gradual the transition +// between phases. +// - **Hend** (4 bits): The Hysteresis end value. This value defines the end of the hysteresis +// region, where the current is stabilized. Like `Hstrt`, higher values result in a smoother +// transition in current regulation and lower torque ripple. +// - **Tbl** (2 bits): The Table selection for the internal chopping table, affecting +// the switching behavior and optimization of the motor’s current profile. +// This setting is typically tuned for different load conditions or step modes. +// - **Vsense** (1 bit): This field selects the sense resistor mode. If enabled, the driver +// uses the internal sense resistors for current sensing, which enables more precise current +// regulation and monitoring, enhancing the motor’s performance and efficiency. +// - **Mres** (4 bits): The microstepping resolution. This value controls how many steps the motor +// takes per full rotation. Higher values allow for finer stepping, leading to smoother motion +// but at the cost of reduced torque at higher microstep resolutions. +// - **Intpol** (1 bit): This field enables interpolation in the motor driver, which improves +// smoothness by interpolating intermediate microsteps between full steps. It helps reduce +// torque ripple and improve overall motor performance. +// - **Dedge** (1 bit): This parameter enables the detection of a specific edge (rising or falling) +// in the step pulse, which is used to control the timing of motor transitions. +// It is generally used for fine-tuning performance or reducing noise during operation. +// - **Diss2g** (1 bit): This field, when enabled, disables the second MOSFET during idle phases +// in the motor operation, further improving efficiency and reducing heat generation. +// - **Diss2vs** (1 bit): Similar to `Diss2g`, this parameter disables the second MOSFET during +// specific phases of operation to reduce losses, increase efficiency, and improve motor control +// in certain scenarios, particularly during low-speed or low-power operation. +// +// The `CHOPCONF` register plays a crucial role in fine-tuning motor operation, ensuring +// smooth motion, efficient power consumption, and minimizing torque ripple in the system. +// These settings are particularly useful when transitioning between operating modes like StealthChop. +type Chopconf struct { + Toff uint32 + Hstrt uint32 + Hend uint32 + Tbl uint32 + Vsense uint32 + Mres uint32 + Intpol uint32 + Dedge uint32 + Diss2g uint32 + Diss2vs uint32 + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 +} + +func (chopconf *Chopconf) GetAddress() uint8 { + return chopconf.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (chopconf *Chopconf) Pack() uint32 { + chopconf.Bytes = (chopconf.Toff & 0x0F) | + ((chopconf.Hstrt & 0x07) << 4) | + ((chopconf.Hend & 0x0F) << 7) | + ((chopconf.Tbl & 0x03) << 15) | + ((chopconf.Vsense & 0x01) << 17) | + ((chopconf.Mres & 0x0F) << 24) | + ((chopconf.Intpol & 0x01) << 28) | + ((chopconf.Dedge & 0x01) << 29) | + ((chopconf.Diss2g & 0x01) << 30) | + ((chopconf.Diss2vs & 0x01) << 31) + return chopconf.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (chopconf *Chopconf) Unpack(uint32) { + chopconf.Toff = chopconf.Bytes & 0x0F + chopconf.Hstrt = (chopconf.Bytes >> 4) & 0x07 + chopconf.Hend = (chopconf.Bytes >> 7) & 0x0F + chopconf.Tbl = (chopconf.Bytes >> 15) & 0x03 + chopconf.Vsense = (chopconf.Bytes >> 17) & 0x01 + chopconf.Mres = (chopconf.Bytes >> 24) & 0x0F + chopconf.Intpol = (chopconf.Bytes >> 28) & 0x01 + chopconf.Dedge = (chopconf.Bytes >> 29) & 0x01 + chopconf.Diss2g = (chopconf.Bytes >> 30) & 0x01 + chopconf.Diss2vs = (chopconf.Bytes >> 31) & 0x01 +} +func NewChopconf() *Chopconf { + return &Chopconf{ + RegisterAddr: CHOPCONF, + } +} +func (chopconf *Chopconf) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, chopconf.RegisterAddr) +} +func (chopconf *Chopconf) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, chopconf.RegisterAddr, driverIndex, value) +} + +// Gstat represents the fields in the TMC2209 GSTAT register. +type Gstat struct { + Reset uint32 + DrvErr uint32 + UvCp uint32 + Reserved uint32 + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 +} + +func (gstat *Gstat) GetAddress() uint8 { + return gstat.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (gstat *Gstat) Pack() uint32 { + gstat.Bytes = (gstat.Reset & 0x01) | + ((gstat.DrvErr & 0x01) << 1) | + ((gstat.UvCp & 0x01) << 2) | + ((gstat.Reserved & 0x1FFFFF) << 3) // 21 bits reserved + return gstat.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (gstat *Gstat) Unpack(uint32) { + gstat.Reset = gstat.Bytes & 0x01 + gstat.DrvErr = (gstat.Bytes >> 1) & 0x01 + gstat.UvCp = (gstat.Bytes >> 2) & 0x01 + gstat.Reserved = (gstat.Bytes >> 3) & 0x1FFFFF +} +func NewGstat() *Gstat { + return &Gstat{ + RegisterAddr: GSTAT, + } +} +func (gstat *Gstat) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, gstat.RegisterAddr) +} +func (gstat *Gstat) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, gstat.RegisterAddr, driverIndex, value) +} + +// Gconf represents the fields in the TMC2209 GCONF register. +// +// The GCONF register configures global settings for the motor driver. +// It controls various aspects of the operation, including stepper driver modes, +// fault detection, and other motor control behaviors that influence motor performance, +// efficiency, and system diagnostics. The register also enables or disables certain features +// based on specific application requirements. +// +// The fields are as follows: +// - **IScaleAnalog** (1 bit): This field controls the scaling of the analog current sense. +// If enabled, it adjusts the scaling of the motor current detection circuitry, improving +// the precision of current regulation. This is important for accurate motor control and +// better torque handling, especially at low speeds. +// - **InternalRsense** (1 bit): When enabled, this field indicates that the internal sense +// resistors are used for current sensing. This allows for improved accuracy in current +// measurement and feedback, resulting in better overall motor performance and energy efficiency. +// - **EnSpreadcycle** (1 bit): This field enables SpreadCycle operation mode. When enabled, +// it switches the motor driver to operate in SpreadCycle mode, which is the traditional +// chopper operation mode. This mode typically results in higher efficiency and more precise +// current control, but it may produce more noise compared to StealthChop mode. +// - **Shaft** (1 bit): This field enables the detection of a shaft (step) in the system, which +// can be used to detect whether the motor is moving or has completed a cycle. It is used +// to improve system control and diagnostics in motor-driven applications. +// - **IndexOtpw** (1 bit): This field, when set, allows detection of the over-temperature +// warning flag (otpw), indicating that the motor or the driver has exceeded safe operating +// temperatures. If enabled, the system can take actions like reducing speed or stopping the motor +// to avoid damage due to overheating. +// - **IndexStep** (1 bit): This field enables indexing of the motor steps. When enabled, it +// allows for step detection, which is helpful in stepper motors where exact step counts are critical. +// This setting ensures accurate positioning of the motor in precise step operations. +// - **PdnDisable** (1 bit): This field enables or disables the power-down functionality for +// the driver. When enabled, the driver will power down the motor and enter a low-power state. +// This is typically used to reduce energy consumption when the motor is not active or when the +// system is in idle mode. This helps save power during periods of non-operation. +// - **MstepRegSelect** (1 bit): This field allows the selection of the multistep regulation +// mode. When enabled, the driver uses the multistep algorithm to control current regulation, +// which can help reduce torque ripple and improve motor smoothness, particularly in applications +// requiring high precision or smooth motion. +// - **MultistepFilt** (1 bit): When enabled, this field activates the multistep filtering. +// This feature is used to reduce the effect of noise and fluctuations in the current waveform. +// It smooths out the power supply and motor performance, particularly at higher speeds or during +// high-load conditions. +// - **Reserved** (21 bits): These reserved bits are unused and should always be set to zero. +// They ensure backward compatibility and alignment with future versions of the register. +// They have no effect on the system's operation. +type Gconf struct { + IScaleAnalog uint32 + InternalRsense uint32 + EnSpreadcycle uint32 + Shaft uint32 + IndexOtpw uint32 + IndexStep uint32 + PdnDisable uint32 + MstepRegSelect uint32 + MultistepFilt uint32 + Reserved uint32 + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 +} + +func (gconf *Gconf) GetAddress() uint8 { + return gconf.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (gconf *Gconf) Pack() uint32 { + gconf.Bytes = (gconf.IScaleAnalog & 0x01) | + ((gconf.InternalRsense & 0x01) << 1) | + ((gconf.EnSpreadcycle & 0x01) << 2) | + ((gconf.Shaft & 0x01) << 3) | + ((gconf.IndexOtpw & 0x01) << 4) | + ((gconf.IndexStep & 0x01) << 5) | + ((gconf.PdnDisable & 0x01) << 6) | + ((gconf.MstepRegSelect & 0x01) << 7) | + ((gconf.MultistepFilt & 0x01) << 8) | + ((gconf.Reserved & 0x1FFFFF) << 9) // 21 bits reserved + return gconf.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (gconf *Gconf) Unpack(uint32) { + gconf.IScaleAnalog = gconf.Bytes & 0x01 + gconf.InternalRsense = (gconf.Bytes >> 1) & 0x01 + gconf.EnSpreadcycle = (gconf.Bytes >> 2) & 0x01 + gconf.Shaft = (gconf.Bytes >> 3) & 0x01 + gconf.IndexOtpw = (gconf.Bytes >> 4) & 0x01 + gconf.IndexStep = (gconf.Bytes >> 5) & 0x01 + gconf.PdnDisable = (gconf.Bytes >> 6) & 0x01 + gconf.MstepRegSelect = (gconf.Bytes >> 7) & 0x01 + gconf.MultistepFilt = (gconf.Bytes >> 8) & 0x01 + gconf.Reserved = (gconf.Bytes >> 9) & 0x1FFFFF +} +func NewGconf() *Gconf { + return &Gconf{ + RegisterAddr: GCONF, + } +} +func (gconf *Gconf) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, gconf.RegisterAddr, driverIndex) +} +func (gconf *Gconf) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, gconf.RegisterAddr, driverIndex, value) +} + +// Ifcnt represents the fields in the TMC2209 IFCNT register. +// +// The IFCNT register is used to monitor the input frequency of the step signal. It holds the +// count of the number of steps that have been processed by the driver and provides +// valuable information about the frequency of step pulses being received. This can be useful +// for diagnostic purposes, to measure the actual step rate, or to check the system’s input +// signal behavior. It helps in monitoring the stepper signal's integrity and rate of pulses +// generated by the control system. +// +// The field is as follows: +// - **Ifcnt** (8 bits): This 8-bit field holds the count of the received step pulses. The count +// is incremented on each step pulse the driver receives and represents the number of steps +// processed by the motor driver. The counter resets at the start of each pulse cycle. This +// field can be used to verify the input signal's frequency and detect potential issues +// such as missed or delayed pulses. Monitoring this register can help optimize the stepper +// signal for better accuracy and performance. +type Ifcnt struct { + Ifcnt uint32 // 8-bit interface counter + Reserved uint32 // Reserved bits, here represented as uint32 for simplicity + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 +} + +func (ifcnt *Ifcnt) GetAddress() uint8 { + return ifcnt.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (ifcnt *Ifcnt) Pack() uint32 { + ifcnt.Bytes = (ifcnt.Ifcnt & 0xFF) | // 8 bits for the interface counter + ((ifcnt.Reserved & 0xFFFFFF) << 8) // Remaining bits for reserved (24 bits) + return ifcnt.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (ifcnt *Ifcnt) Unpack(uint32) { + ifcnt.Ifcnt = ifcnt.Bytes & 0xFF + ifcnt.Reserved = (ifcnt.Bytes >> 8) & 0xFFFFFF +} + +// Initialize IFCNT with register address +func NewIfcnt() *Ifcnt { + return &Ifcnt{ + RegisterAddr: IFCNT, // IFCNT register address + } +} +func (ifcnt *Ifcnt) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, ifcnt.RegisterAddr) +} +func (ifcnt *Ifcnt) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, ifcnt.RegisterAddr, driverIndex, value) +} + +// IholdIrun represents the fields in the TMC2209 IHOLD_IRUN register. +// +// The IHOLD_IRUN register is used to configure the current settings for the motor during +// different phases of operation. It specifically defines two current settings: the hold current +// and the run current. +// +// - **Ihold** (5 bits): This field defines the current value for holding the motor in place when +// it is idle. The value can be set between 0 and 31, with 0 representing the lowest possible current, +// and 31 representing the maximum hold current. The hold current is typically lower than the run current +// to minimize energy consumption when the motor is not actively moving. +// +// - **Iruns** (5 bits): This field sets the current for the motor when it is running (i.e., moving). +// The value can range from 0 to 31, and higher values represent stronger currents for increased torque +// when the motor is under load. The run current is used when the motor is actively engaged in motion. +// +// - **Iholddelay** (4 bits): This field defines the delay in microsteps before transitioning between +// the hold current and the run current. This delay allows for smoother transitions when starting or stopping +// the motor, preventing sudden jumps in current that could cause issues with the stepper system. +// +// This register is critical for efficient motor control, allowing dynamic management of motor currents to optimize +// energy consumption and torque output. Tuning the `Ihold` and `Iruns` settings can significantly impact motor +// performance and efficiency, especially in applications where energy efficiency is important. +type IholdIrun struct { + Ihold uint32 // 5 bits for hold current + Irun uint32 // 5 bits for run current + Iholddelay uint32 // 4 bits for hold delay + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 // Register address +} + +func (iholdIrun *IholdIrun) GetAddress() uint8 { + return iholdIrun.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (iholdIrun *IholdIrun) Pack() uint32 { + iholdIrun.Bytes = (iholdIrun.Ihold & 0x1F) | // 5 bits for IHOLD + ((iholdIrun.Irun & 0x1F) << 5) | // 5 bits for IRUN + ((iholdIrun.Iholddelay & 0x0F) << 10) // 4 bits for IHOLDD_DELAY + return iholdIrun.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (iholdIrun *IholdIrun) Unpack(uint32) { + iholdIrun.Ihold = iholdIrun.Bytes & 0x1F + iholdIrun.Irun = (iholdIrun.Bytes >> 5) & 0x1F + iholdIrun.Iholddelay = (iholdIrun.Bytes >> 10) & 0x0F +} + +// Initialize IHOLD_IRUN with register address +func NewIholdIrun() *IholdIrun { + return &IholdIrun{ + RegisterAddr: IHOLD_IRUN, // IHOLD_IRUN register address + } +} +func (iholdIrun *IholdIrun) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, iholdIrun.RegisterAddr) +} +func (iholdIrun *IholdIrun) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, iholdIrun.RegisterAddr, driverIndex, value) +} + +// Tpwmthrs represents the fields in the TMC2209 TPWMTHRS register. +// +// The TPWMTHRS register is used to set the threshold for transitioning between +// traditional stepper driving (full stepping) and the use of PWM (Pulse Width Modulation) +// for controlling the stepper motor. This register allows for better current control +// and smoother operation, especially at higher speeds. +// +// - **Tpwmthrs** (20 bits): This field defines the threshold value for switching +// from traditional stepper driving to PWM control. The motor will operate with +// standard stepping until the velocity exceeds the value set in this register, +// at which point it will switch to PWM mode. This allows for improved efficiency +// and smoother operation at higher speeds. +// +// The value for the `Tpwmthrs` field is typically set based on the specific motor and +// the desired speed range for the application. A higher threshold means the motor +// will operate in traditional stepping mode at lower speeds, while a lower threshold +// will enable PWM control at lower speeds for better motor control and efficiency. +type Tpwmthrs struct { + Threshold uint32 // 32-bit threshold value + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 // Register address +} + +func (tpwmthrs *Tpwmthrs) GetAddress() uint8 { + return tpwmthrs.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (tpwmthrs *Tpwmthrs) Pack() uint32 { + tpwmthrs.Bytes = tpwmthrs.Threshold & 0xFFFFFFFF // 32-bit threshold value + return tpwmthrs.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (tpwmthrs *Tpwmthrs) Unpack(uint32) { + tpwmthrs.Threshold = tpwmthrs.Bytes & 0xFFFFFFFF +} + +// NewTpwmthrs Initialize TPWMTHRS with register address +func NewTpwmthrs() *Tpwmthrs { + return &Tpwmthrs{ + RegisterAddr: TPWMTHRS, // TPWMTHRS register address + } +} +func (tpwmthrs *Tpwmthrs) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, tpwmthrs.RegisterAddr) +} +func (tpwmthrs *Tpwmthrs) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, tpwmthrs.RegisterAddr, driverIndex, value) +} + +// Vactual represents the fields in the TMC2209 VACTUAL register. +// +// The VACTUAL register provides the actual velocity of the motor in terms of the +// stepper's step clock. It is used to monitor the real-time motor speed and is +// an important part of controlling the motor's behavior during operation. +// +// - **Vactual** (20 bits): This field holds the current actual velocity of the motor. +// It represents the stepper's velocity in terms of the step clock, providing feedback +// on how fast the motor is turning. The value in this register is updated regularly +// based on the microstepping and the actual motor speed. +// +// The `VACTUAL` register is particularly useful for closed-loop control systems, +// where the actual motor speed needs to be compared against the desired speed +// (set in other registers) to make adjustments. This can help in fine-tuning the +// motor's behavior for smoother operation and more accurate performance. +type Vactual struct { + Velocity uint32 // 32-bit velocity value + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 // Register address +} + +func (vactual *Vactual) GetAddress() uint8 { + return vactual.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (vactual *Vactual) Pack() uint32 { + vactual.Bytes = vactual.Velocity & 0xFFFFFFFF // 32-bit velocity value + return vactual.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (vactual *Vactual) Unpack(uint32) { + vactual.Velocity = vactual.Bytes & 0xFFFFFFFF +} + +// Initialize VACTUAL with register address +func NewVactual() *Vactual { + return &Vactual{ + RegisterAddr: VACTUAL, // VACTUAL register address + } +} +func (vactual *Vactual) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, vactual.RegisterAddr) +} +func (vactual *Vactual) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, vactual.RegisterAddr, driverIndex, value) +} + +// Tcoolthrs represents the fields in the TMC2209 TCOOLTHRS register. +// +// The TCOOLTHRS register sets the lower threshold velocity for switching on +// smart energy CoolStep and StallGuard output to the DIAG pin. This helps +// manage the motor's energy usage by enabling CoolStep at higher velocities +// and StallGuard at lower velocities. +// +// - **TCOOLTHRS** (20 bits): The threshold velocity (in units of steps per +// microstep) used to switch between CoolStep and StallGuard modes. The motor +// operates in CoolStep mode if the velocity exceeds this threshold and uses +// StallGuard if the velocity falls below it. +// +// This register is important for optimizing motor performance, especially +// in applications that require energy efficiency. It ensures that the motor +// does not use unnecessary energy at low speeds while still being able to detect stalls +// and adjust torque at higher speeds. CoolStep allows the driver to dynamically adjust +// current levels based on the motor's actual load, reducing power consumption. +type Tcoolthrs struct { + Velocity uint32 // 20 bits for velocity + Bytes uint32 // Packed 32-bit value + RegisterAddr uint8 +} + +func (tcoolthrs *Tcoolthrs) GetAddress() uint8 { + return tcoolthrs.RegisterAddr +} + +// Initialize TCOOLTHRS with register address +func NewTcoolthrs() *Tcoolthrs { + return &Tcoolthrs{ + RegisterAddr: TCOOLTHRS, + } +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (tcoolthrs *Tcoolthrs) Pack() uint32 { + tcoolthrs.Bytes = tcoolthrs.Velocity & 0xFFFFF // Keep only the lower 20 bits + return tcoolthrs.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (tcoolthrs *Tcoolthrs) Unpack(uint32) { + tcoolthrs.Velocity = tcoolthrs.Bytes & 0xFFFFF +} +func (tcoolthrs *Tcoolthrs) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, tcoolthrs.RegisterAddr) +} +func (tcoolthrs *Tcoolthrs) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, tcoolthrs.RegisterAddr, driverIndex, value) +} + +// Sgthrs represents the fields in the TMC2209 SGTHRS register. +// +// The SGTHRS register sets the detection threshold for stall detection in the motor. +// It compares the StallGuard result (SG_RESULT) to twice the value set in this register. +// If the SG_RESULT value falls below the threshold (SG_RESULT < SGTHRS * 2), a stall +// is detected and can trigger a response such as a warning or motor shutdown. +// +// - **SGTHRS** (8 bits): This value sets the detection threshold for the StallGuard feature. +// The result of the StallGuard measurement (SG_RESULT) is compared to this threshold value +// multiplied by 2. When the SG_RESULT value is less than this threshold, a stall is detected. +// +// The `SGTHRS` register is part of the StallGuard feature, which allows the driver to +// detect and respond to motor stalls. StallGuard provides real-time feedback on the motor's +// status by measuring the motor’s back EMF to detect any irregularities that might indicate +// a stall condition. This can help in preventing mechanical damage by responding to stalls early. +type Sgthrs struct { + Threshold uint32 // 8 bits for threshold value + Bytes uint32 // Packed 32-bit value + RegisterAddr uint8 +} + +func (sgthrs *Sgthrs) GetAddress() uint8 { + return sgthrs.RegisterAddr +} + +// Initialize SGTHRS with register address +func NewSgthrs() *Sgthrs { + return &Sgthrs{ + RegisterAddr: SGTHRS, + } +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (sgthrs *Sgthrs) Pack() uint32 { + sgthrs.Bytes = sgthrs.Threshold & 0xFF // Keep only the lower 8 bits + return sgthrs.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (sgthrs *Sgthrs) Unpack(uint32) { + sgthrs.Threshold = sgthrs.Bytes & 0xFF +} +func (sgthrs *Sgthrs) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, sgthrs.RegisterAddr) +} +func (sgthrs *Sgthrs) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, sgthrs.RegisterAddr, driverIndex, value) +} + +// SgResult represents the fields in the TMC2209 SG_RESULT register. +// +// The SG_RESULT register stores the result of the StallGuard measurement, which is used +// to detect motor stalls. The StallGuard feature measures the motor's back electromotive force +// (back EMF) to assess the motor's load and detect irregularities that could indicate a stall. +// +// - **SG_RESULT** (10 bits): The register holds the result of the StallGuard measurement, +// which provides information about the motor's load and stall condition. A higher value in +// SG_RESULT generally means the motor is operating normally, while a lower value indicates +// the motor might be experiencing a stall or a mechanical blockage. +// +// The value in the SG_RESULT register is used in conjunction with the `SGTHRS` register +// (StallGuard Threshold) to determine if a stall condition has occurred. If the SG_RESULT +// value is below twice the value set in the `SGTHRS` register, a stall is considered to have occurred. +// +// The SG_RESULT register is particularly useful for motor stall detection, enabling the driver +// to protect the motor from damage caused by excessive load or mechanical binding by halting the motor's operation. +type SgResult struct { + Result uint32 // 10 bits for the result + Bytes uint32 // Packed 32-bit value + RegisterAddr uint8 +} + +func (sgResult *SgResult) GetAddress() uint8 { + return sgResult.RegisterAddr +} + +// NewSgResult Initialize SG_RESULT with register address +func NewSgResult() *SgResult { + return &SgResult{ + RegisterAddr: SG_RESULT, + } +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (sgResult *SgResult) Pack() uint32 { + sgResult.Bytes = sgResult.Result & 0x3FF // Keep only the lower 10 bits + return sgResult.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (sgResult *SgResult) Unpack(uint32) { + sgResult.Result = sgResult.Bytes & 0x3FF +} +func (sgResult *SgResult) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, sgResult.RegisterAddr) +} +func (sgResult *SgResult) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, sgResult.RegisterAddr, driverIndex, value) +} + +// CoolConf represents the fields in the TMC2209 COOLCONF register. +// +// The COOLCONF register controls various settings related to the CoolStep feature, +// which automatically adjusts the motor's current based on the load to optimize power consumption +// and minimize heat generation. +// +// - **SEMIN** (5 bits): The minimum current value for the CoolStep algorithm to be enabled. If the +// motor current falls below this value, CoolStep will reduce the current. The SEMIN field helps to +// control the minimum threshold for current scaling. +// +// - **SEUP** (2 bits): The step-up value for the current when CoolStep detects an increase in load. +// It defines the amount by which the motor current is increased when the load increases and the motor +// is at risk of stalling. It helps to balance current efficiency and motor performance. +// +// - **SEMAX** (5 bits): The maximum current value for CoolStep. This value sets the upper threshold +// for the current when the motor is under heavy load. It ensures that the motor can handle the load +// by increasing the current when necessary, while still maintaining efficiency. +// +// - **SEDN** (2 bits): The step-down value for the current when CoolStep detects a decrease in load. +// It helps to lower the current consumption when the motor is no longer under heavy load, optimizing +// energy efficiency. +// +// - **SEIMIN** (1 bit): Enables or disables the current scaling for low loads. If enabled, the motor +// current will be reduced under low load conditions, improving energy efficiency. If disabled, the +// motor current remains constant at the preset level, regardless of the load. +// +// The COOLCONF register allows for fine-tuning of the motor current scaling behavior based on the load, +// helping to optimize motor efficiency and reduce power consumption and heat generation. +type CoolConf struct { + Semin uint32 // 1 bit + Sedn uint32 // 2 bits (sedn0, sedn1) + Semax uint32 // 4 bits (semax0 to semax3) + Seup uint32 // 3 bits (seup0, seup1, seup2) + Semin2 uint32 // 6 bits (semin0 to semin5) + CoolStepEnable uint32 // 1 bit + Reserved uint32 // Reserved 10 bits + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 // The register address (COOLCONF) +} + +func (coolConf *CoolConf) GetAddress() uint8 { + //TODO implement me + panic("implement me") +} + +// Initialize COOLCONF with register address +func NewCoolConf() *CoolConf { + return &CoolConf{ + RegisterAddr: COOLCONF, + } +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (coolConf *CoolConf) Pack() uint32 { + coolConf.Bytes = (coolConf.Semin & 0x01) | + ((coolConf.Sedn & 0x03) << 1) | + ((coolConf.Semax & 0x0F) << 3) | + ((coolConf.Seup & 0x07) << 7) | + ((coolConf.Semin2 & 0x3F) << 10) | + ((coolConf.CoolStepEnable & 0x01) << 16) | + ((coolConf.Reserved & 0x3FF) << 17) // Reserve 10 bits for reserved fields + return coolConf.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (coolConf *CoolConf) Unpack(uint32) { + coolConf.Semin = coolConf.Bytes & 0x01 + coolConf.Sedn = (coolConf.Bytes >> 1) & 0x03 + coolConf.Semax = (coolConf.Bytes >> 3) & 0x0F + coolConf.Seup = (coolConf.Bytes >> 7) & 0x07 + coolConf.Semin2 = (coolConf.Bytes >> 10) & 0x3F + coolConf.CoolStepEnable = (coolConf.Bytes >> 16) & 0x01 + coolConf.Reserved = (coolConf.Bytes >> 17) & 0x3FF +} +func (coolConf *CoolConf) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, coolConf.RegisterAddr) +} +func (coolConf *CoolConf) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, coolConf.RegisterAddr, driverIndex, value) +} + +// DrvStatus represents the fields in the TMC2209 DRV_STATUS register. +// +// The DRV_STATUS register provides information about the driver’s status, +// including over-temperature warnings, short-circuit detection, and current status. +// +// - **OTPW** (1 bit): Overtemperature warning. If this bit is set to 1, it indicates that the +// temperature of the driver has exceeded the safe operating threshold. This can be used to +// trigger protective actions, such as reducing the current or stopping the motor to prevent damage. +// +// - **OT** (1 bit): Overtemperature fault. If this bit is set to 1, it indicates that the driver +// has overheated and is currently in a fault condition due to excessive temperature. It typically +// results in disabling the motor or reducing motor power until the temperature drops to a safe level. +// +// - **S2GA** (1 bit): Short-to-Ground on A phase (phase A of the motor). This bit is set to 1 +// if a short-circuit is detected between the motor's phase A and ground. This protection feature +// helps to safeguard the driver and motor from short circuits that could lead to damage. +// +// - **S2GB** (1 bit): Short-to-Ground on B phase (phase B of the motor). This bit is set to 1 +// if a short-circuit is detected between the motor's phase B and ground. Similar to the previous +// bit, this provides protection against short-circuits for the B phase. +// +// - **S2VSA** (1 bit): Short-to-VCC on A phase (phase A of the motor). If this bit is set to 1, +// it indicates a short-circuit between phase A of the motor and the power supply voltage (VCC). +// This condition could be hazardous and requires corrective actions. +// +// - **S2VSB** (1 bit): Short-to-VCC on B phase (phase B of the motor). Similar to S2VSA, this +// bit is set when a short-circuit is detected between phase B of the motor and VCC. +// +// - **OLA** (1 bit): Overcurrent fault on A phase. This bit is set to 1 if the current through +// phase A exceeds the configured threshold, indicating an overcurrent condition. It may indicate +// a problem with the motor or wiring that requires attention. +// +// - **OLB** (1 bit): Overcurrent fault on B phase. Similar to OLA, this bit indicates an overcurrent +// fault in phase B, signaling an issue that needs to be addressed to prevent damage to the system. +// +// - **T120** (1 bit): Timeout fault for phase A (motor A). If this bit is set, it indicates that +// the motor did not receive any signal or is stuck for too long, possibly due to a failure in the +// motor or wiring. It indicates that the system has detected an abnormal motor condition. +// +// - **T143** (1 bit): Timeout fault for phase B (motor B). Similar to T120, this bit indicates +// an issue with phase B, such as the motor not receiving a signal within the expected timeframe. +// +// - **T150** (1 bit): Timeout fault for both phases A and B. This bit is set if both phases are +// experiencing a timeout condition, which could be due to an issue with the motor or control signals. +// +// - **T157** (1 bit): Timeout fault due to thermal shutdown. If this bit is set, it indicates that +// the driver has shut down due to an overtemperature condition, preventing further damage to the system. +// +// - **CS_ACTUAL** (5 bits): This field provides the actual current setting value for the motor. +// It is used to monitor the current being supplied to the motor in real-time, helping to detect any +// discrepancies or performance issues during operation. +// +// - **STEALTH** (1 bit): StealthChop status. When set to 1, this bit indicates that the StealthChop +// mode is active, which is a feature used to reduce motor noise and improve efficiency at low speeds. +// If set to 0, StealthChop is not active, and the driver may be using a more standard operating mode. +// +// - **STST** (1 bit): Step status. If set to 1, this bit indicates that the motor is currently +// moving or stepping. It can be used to detect if the motor is active or idle at any given time. +type DrvStatus struct { + Stst uint32 // Standstill indicator + Stealth uint32 // StealthChop indicator + CsActual uint32 // Actual motor current / smart energy current + T157 uint32 // 157°C comparator + T150 uint32 // 150°C comparator + T143 uint32 // 143°C comparator + T120 uint32 // 120°C comparator + Olb uint32 // Open load indicator phase B + Ola uint32 // Open load indicator phase A + S2vsb uint32 // Low-side short indicator phase B + S2vsa uint32 // Low-side short indicator phase A + S2gb uint32 // Short to ground indicator phase B + S2ga uint32 // Short to ground indicator phase A + Ot uint32 // Overtemperature flag + Otpw uint32 // Overtemperature pre-warning flag + Reserved uint32 // Reserved bits + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 // Register address +} + +func (drvStatus *DrvStatus) GetAddress() uint8 { + return drvStatus.RegisterAddr +} + +// Initialize DRV_STATUS with register address +func NewDrvStatus() *DrvStatus { + return &DrvStatus{ + RegisterAddr: DRV_STATUS, + } +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (drvStatus *DrvStatus) Pack() uint32 { + drvStatus.Bytes = (drvStatus.Stst & 0x01) | + ((drvStatus.Stealth & 0x01) << 1) | + ((drvStatus.CsActual & 0xFFFF) << 2) | // Actual current in bits 16-31 + ((drvStatus.T157 & 0x01) << 18) | + ((drvStatus.T150 & 0x01) << 19) | + ((drvStatus.T143 & 0x01) << 20) | + ((drvStatus.T120 & 0x01) << 21) | + ((drvStatus.Olb & 0x01) << 22) | + ((drvStatus.Ola & 0x01) << 23) | + ((drvStatus.S2vsb & 0x01) << 24) | + ((drvStatus.S2vsa & 0x01) << 25) | + ((drvStatus.S2gb & 0x01) << 26) | + ((drvStatus.S2ga & 0x01) << 27) | + ((drvStatus.Ot & 0x01) << 28) | + ((drvStatus.Otpw & 0x01) << 29) | + ((drvStatus.Reserved & 0x7FF) << 30) // Reserved bits + return drvStatus.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (drvStatus *DrvStatus) Unpack(uint32) { + drvStatus.Stst = drvStatus.Bytes & 0x01 + drvStatus.Stealth = (drvStatus.Bytes >> 1) & 0x01 + drvStatus.CsActual = (drvStatus.Bytes >> 2) & 0xFFFF + drvStatus.T157 = (drvStatus.Bytes >> 18) & 0x01 + drvStatus.T150 = (drvStatus.Bytes >> 19) & 0x01 + drvStatus.T143 = (drvStatus.Bytes >> 20) & 0x01 + drvStatus.T120 = (drvStatus.Bytes >> 21) & 0x01 + drvStatus.Olb = (drvStatus.Bytes >> 22) & 0x01 + drvStatus.Ola = (drvStatus.Bytes >> 23) & 0x01 + drvStatus.S2vsb = (drvStatus.Bytes >> 24) & 0x01 + drvStatus.S2vsa = (drvStatus.Bytes >> 25) & 0x01 + drvStatus.S2gb = (drvStatus.Bytes >> 26) & 0x01 + drvStatus.S2ga = (drvStatus.Bytes >> 27) & 0x01 + drvStatus.Ot = (drvStatus.Bytes >> 28) & 0x01 + drvStatus.Otpw = (drvStatus.Bytes >> 29) & 0x01 + drvStatus.Reserved = (drvStatus.Bytes >> 30) & 0x7FF +} +func (drvStatus *DrvStatus) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, drvStatus.RegisterAddr) +} +func (drvStatus *DrvStatus) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, drvStatus.RegisterAddr, driverIndex, value) +} + +// PwmScale represents the fields in the TMC2209 PWM_SCALE register. +// +// The PWM_SCALE register provides information related to the current PWM scaling +// values for the motor driver. It is used to determine the effective current +// supplied to the motor and the PWM (Pulse Width Modulation) duty cycle. +// +// - **PwmScaleSum** (8 bits): This field provides the total PWM scaling value used +// for the motor driver. It represents the summed scaling value of both motor phases. +// A higher value typically means more power is delivered to the motor. +// +// - **PwmScaleAuto** (9 bits): This field contains the automatically calculated +// PWM scaling value. It adjusts the power delivery dynamically based on motor +// load and thermal conditions to optimize performance and efficiency. +// The value in this field provides an indication of the current level of scaling +// that the driver is using in the automatic mode, based on the real-time motor conditions. +type PwmScale struct { + PwmScaleSum uint32 // 8-bit PWM duty cycle + PwmScaleAuto int32 // 9-bit signed offset (-255 to +255) + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 // Register address +} + +func (pwm *PwmScale) GetAddress() uint8 { + return pwm.RegisterAddr +} + +// NewPwmScale Initialize PwmScale with register address +func NewPwmScale() *PwmScale { + return &PwmScale{ + RegisterAddr: PWM_SCALE, // PWM_SCALE register address + } +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (pwm *PwmScale) Pack() uint32 { + pwm.Bytes = (pwm.PwmScaleSum & 0xFF) | + ((uint32(pwm.PwmScaleAuto) & 0x1FF) << 8) // 9 bits for PWM_SCALE_AUTO + return pwm.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (pwm *PwmScale) Unpack(uint32) { + pwm.PwmScaleSum = pwm.Bytes & 0xFF + pwm.PwmScaleAuto = int32((pwm.Bytes >> 8) & 0x1FF) // 9-bit signed value +} +func (pwm *PwmScale) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, pwm.RegisterAddr) +} +func (pwm *PwmScale) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, pwm.RegisterAddr, driverIndex, value) +} + +// PwmAuto represents the fields in the TMC2209 PWM_AUTO register. +// +// The PWM_AUTO register is used to configure and monitor the automatic scaling and +// adjustment of PWM settings in the TMC2209 motor driver. This register helps to +// fine-tune motor control behavior based on real-time operating conditions. +// +// - **PwmOfsAuto** (8 bits): This field contains the automatically adjusted PWM +// offset value. The PWM offset is used to set the initial duty cycle for the PWM +// signal. Adjusting this field allows for fine-tuning of motor behavior in +// specific scenarios, such as optimizing torque or minimizing motor heating. +// +// - **PwmGradAuto** (8 bits): This field holds the automatically adjusted PWM +// gradient value. The gradient determines the rate at which the PWM duty cycle +// increases or decreases over time. A higher gradient value can lead to faster +// transitions in power delivery, which is useful for certain motor acceleration +// profiles or applications requiring smooth power changes. +type PwmAuto struct { + PwmOfsAuto int32 // 8-bit signed offset value (-255 to +255) + PwmGradAuto int32 // 8-bit automatically determined gradient value (-255 to +255) + Bytes uint32 // The packed 32-bit value + RegisterAddr uint8 // Register address +} + +func (pwm *PwmAuto) GetAddress() uint8 { + return pwm.RegisterAddr +} + +// Initialize PwmAuto with register address +func NewPwmAuto() *PwmAuto { + return &PwmAuto{ + RegisterAddr: PWM_AUTO, // PWM_AUTO register address + } +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (pwm *PwmAuto) Pack() uint32 { + pwm.Bytes = (uint32(pwm.PwmOfsAuto) & 0xFF) | + ((uint32(pwm.PwmGradAuto) & 0xFF) << 8) // 8 bits for each value + return pwm.Bytes +} + +// Unpack the Bytes field into the individual fields. +func (pwm *PwmAuto) Unpack(uint32) { + pwm.PwmOfsAuto = int32(pwm.Bytes & 0xFF) + pwm.PwmGradAuto = int32((pwm.Bytes >> 8) & 0xFF) +} +func (pwm *PwmAuto) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, pwm.RegisterAddr) +} +func (pwm *PwmAuto) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, pwm.RegisterAddr, driverIndex, value) +} + +// Tpowerdown represents the fields in the TMC2209 TPOWERDOWN register. +// +// The TPOWERDOWN register is used to configure the time delay before the driver +// enters the power-down state after the motor has been idle. This is useful for +// energy-saving purposes and to reduce the overall power consumption when the +// motor is not being actively driven. +// +// - **Tpowerdown** (8 bits): This field specifies the time delay (in microseconds) +// before the TMC2209 enters the power-down mode. The power-down mode reduces the +// current drawn by the motor and motor driver, which helps save energy when the +// motor is not in use. The delay is adjustable to balance between responsiveness +// and power consumption. +type Tpowerdown struct { + DelayTime uint32 // Delay time from standstill detection to motor current power-down (8 bits) + RegisterAddr uint8 // Register address + Bytes uint32 // The packed 32-bit value +} + +func (tpd *Tpowerdown) GetAddress() uint8 { + return tpd.RegisterAddr +} + +// Initialize Tpowerdown with register address +func NewTpowerdown() *Tpowerdown { + return &Tpowerdown{ + RegisterAddr: TPOWERDOWN, // TPOWERDOWN register address + } +} + +// Pack the DelayTime field into the Bytes field (a single 8-bit value). +func (tpd *Tpowerdown) Pack() uint32 { + tpd.Bytes = tpd.DelayTime & 0xFF + return tpd.Bytes +} + +// Unpack the Bytes field into the DelayTime field. +func (tpd *Tpowerdown) Unpack(uint32) { + tpd.DelayTime = tpd.Bytes & 0xFF +} +func (tpd *Tpowerdown) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, tpd.RegisterAddr) +} +func (tpd *Tpowerdown) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, tpd.RegisterAddr, driverIndex, value) +} + +// Tstep represents the fields in the TMC2209 TSTEP register. +// +// The TSTEP register configures the inter-step duration, which is the time between +// the steps sent to the motor driver. This is important for controlling the speed +// of the motor, and is typically used to set the stepping frequency. +// +// - **Tstep** (24 bits): This field specifies the duration of time between each step +// signal, measured in microseconds. The value determines how quickly the motor steps, +// effectively controlling the motor's speed. Smaller values result in faster stepping +// (higher speed), while larger values reduce the stepping frequency (lower speed). +// +// The `TSTEP` register is critical for applications that require precise motor control, +// especially for variable-speed applications. The duration set in `TSTEP` should be +// chosen based on the desired motor speed and the capabilities of the motor driver. +type Tstep struct { + StepTime uint32 // Time between 1/256 microsteps (20 bits) + RegisterAddr uint8 // Register address + Bytes uint32 // The packed 32-bit value +} + +func (tstep *Tstep) GetAddress() uint8 { + return tstep.RegisterAddr +} + +// Initialize Tstep with register address +func NewTstep() *Tstep { + return &Tstep{ + RegisterAddr: TSTEP, // TSTEP register address + } +} + +// Pack the StepTime field into the Bytes field (a single 20-bit value). +func (tstep *Tstep) Pack() uint32 { + tstep.Bytes = tstep.StepTime & 0xFFFFF // 20 bits for TSTEP + return tstep.Bytes +} + +// Unpack the Bytes field into the StepTime field. +func (tstep *Tstep) Unpack(uint32) { + tstep.StepTime = tstep.Bytes & 0xFFFFF +} +func (tstep *Tstep) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, tstep.RegisterAddr) +} +func (tstep *Tstep) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, tstep.RegisterAddr, driverIndex, value) +} + +// Mscnt represents the Microstep Counter Register (0x6A) in the TMC2209 +// +// This register provides the actual microstep position within the microstep table. +// The value of MSCNT allows determination of the motor position within the electrical wave, +// which is essential for accurately controlling the motor's position during operation. +// +// Fields: +// - The register contains a 10-bit value indicating the actual position in the microstep table. +// - Range: 0 to 1023 (0x000 to 0x3FF) +type Mscnt struct { + Position uint32 + RegisterAddr uint8 +} + +func (mscnt *Mscnt) GetAddress() uint8 { + return mscnt.RegisterAddr +} + +// Pack the Position value (10-bit) into the Bytes field +func (mscnt *Mscnt) Pack() uint32 { + mscnt.Position = mscnt.Position & 0x03FF // Limit to 10 bits (0x3FF) + return mscnt.Position +} + +// Unpack the Bytes field into the Position field. +func (mscnt *Mscnt) Unpack(uint32) { + mscnt.Position = mscnt.Position & 0x03FF +} + +// NewMscnt initializes a new Mscnt struct with the correct register address. +func NewMscnt() *Mscnt { + return &Mscnt{ + RegisterAddr: MSCNT, // MSCNT register address + } +} +func (mscnt *Mscnt) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, mscnt.RegisterAddr) +} +func (mscnt *Mscnt) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, mscnt.RegisterAddr, driverIndex, value) +} + +// Mscuract MSCURACT represents the Microstep Current Register (0x6B) in the TMC2209 +// +// This register provides the actual current values for motor phases A and B as read from +// the internal sine wave table. The values are not scaled by the current setting but provide +// a raw value for the motor's electrical waves. +// +// Fields: +// - CUR_B (bits 0-7): The actual microstep current for motor phase B (signed value in the range +/-255). +// - CUR_A (bits 24-16): The actual microstep current for motor phase A (signed value in the range +/-255). +type Mscuract struct { + // Microstep current for phase B (sine wave) + CurB uint32 + // Microstep current for phase A (cosine wave) + CurA uint32 + // Register address + RegisterAddr uint8 +} + +func (mscuract *Mscuract) GetAddress() uint8 { + return mscuract.RegisterAddr +} + +// Pack the individual fields into the Bytes field (a single 32-bit value). +func (mscuract *Mscuract) Pack() uint32 { + mscuract.CurB = mscuract.CurB & 0xFF // Limit to 8 bits for CUR_B + mscuract.CurA = mscuract.CurA & 0xFF // Limit to 8 bits for CUR_A + return mscuract.CurA +} + +// Unpack the Bytes field into the individual fields. +func (mscuract *Mscuract) Unpack(uint32) { + mscuract.CurB = mscuract.CurB & 0xFF // Extract CUR_B (8 bits) + mscuract.CurA = (mscuract.CurA >> 16) & 0xFF // Extract CUR_A (8 bits) +} + +// NewMscuract initializes a new Mscuract struct with the correct register address. +func NewMscuract() *Mscuract { + return &Mscuract{ + RegisterAddr: MSCURACT, + } +} +func (mscuract *Mscuract) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, mscuract.RegisterAddr) +} +func (mscuract *Mscuract) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, mscuract.RegisterAddr, driverIndex, value) +} diff --git a/tmc2209/current.go b/tmc2209/current.go new file mode 100644 index 0000000..1224e6f --- /dev/null +++ b/tmc2209/current.go @@ -0,0 +1,42 @@ +package tmc2209 + +func Constrain(value, low, high uint32) uint32 { + if value < low { + return low + } + if value > high { + return high + } + return value +} + +func SetRunCurrent(percent uint8) { + _ = PercentToCurrentSetting(percent) + + // Set the run current register to runCurrent value +} + +func SetHoldCurrent(percent uint8) { + _ = PercentToCurrentSetting(percent) + // Set the hold current register to holdCurrent value +} +func PercentToCurrentSetting(percent uint8) uint8 { + constrainedPercent := Constrain(uint32(percent), 0, 100) + return uint8(Map(constrainedPercent, 0, 100, 0, 255)) +} + +func CurrentSettingToPercent(currentSetting uint8) uint8 { + return uint8(Map(uint32(currentSetting), 0, 255, 0, 100)) +} + +func PercentToHoldDelaySetting(percent uint8) uint8 { + constrainedPercent := Constrain(uint32(percent), 0, 100) + return uint8(Map(constrainedPercent, 0, 100, 0, 255)) +} + +func HoldDelaySettingToPercent(holdDelaySetting uint8) uint8 { + return uint8(Map(uint32(holdDelaySetting), 0, 255, 0, 100)) +} +func Map(value, fromLow, fromHigh, toLow, toHigh uint32) uint32 { + return (value-fromLow)*(toHigh-toLow)/(fromHigh-fromLow) + toLow +} diff --git a/tmc2209/motor_config.go b/tmc2209/motor_config.go new file mode 100644 index 0000000..c828e77 --- /dev/null +++ b/tmc2209/motor_config.go @@ -0,0 +1,37 @@ +package tmc2209 + +func SetMicrostepsPerStep(microsteps uint16) uint8 { + exponent := uint8(0) + microstepsShifted := microsteps >> 1 + + for microstepsShifted > 0 { + microstepsShifted = microstepsShifted >> 1 + exponent++ + } + + SetMicrostepsPerStepPowerOfTwo(exponent) + return exponent +} + +func SetMicrostepsPerStepPowerOfTwo(exponent uint8) { + switch exponent { + case 0: + // Set MRES_001 + case 1: + // Set MRES_002 + case 2: + // Set MRES_004 + case 3: + // Set MRES_008 + case 4: + // Set MRES_016 + case 5: + // Set MRES_032 + case 6: + // Set MRES_064 + case 7: + // Set MRES_128 + default: + // Set MRES_256 + } +} diff --git a/tmc2209/stealthchop.go b/tmc2209/stealthchop.go new file mode 100644 index 0000000..b9e1fe3 --- /dev/null +++ b/tmc2209/stealthchop.go @@ -0,0 +1,25 @@ +package tmc2209 + +func EnableStealthChop() { + // Set StealthChop enabled in the global config register +} + +func DisableStealthChop() { + // Set StealthChop disabled in the global config register +} + +func EnableCoolStep(lowerThreshold, upperThreshold uint8) { + // Enable CoolStep with specified thresholds +} + +func DisableCoolStep() { + // Disable CoolStep feature +} + +func EnableAutomaticCurrentScaling() { + // Enable Automatic Current Scaling +} + +func DisableAutomaticCurrentScaling() { + // Disable Automatic Current Scaling +} diff --git a/tmc2209/tmc2209.go b/tmc2209/tmc2209.go new file mode 100644 index 0000000..c00ec26 --- /dev/null +++ b/tmc2209/tmc2209.go @@ -0,0 +1,56 @@ +//go:build tinygo + +package tmc2209 + +import ( + "log" +) + +// TMC2209 represents a single TMC2209 stepper motor driver on the communication line. +type TMC2209 struct { + comm RegisterComm + address uint8 +} + +// NewTMC2209 creates a new instance of the TMC2209 driver for a specific address. +func NewTMC2209(comm RegisterComm, address uint8) *TMC2209 { + return &TMC2209{ + comm: comm, + address: address, + } +} + +// Setup initializes the communication interface with the TMC2209. +func (driver *TMC2209) Setup() error { + // Check if comm is of type *UARTComm + if uartComm, ok := driver.comm.(*UARTComm); ok { + // Call Setup only if comm is a *UARTComm + err := uartComm.Setup() + if err != nil { + return CustomError("Failed to setup UART communication: " + err.Error()) + } + } else { + // If it's not a UARTComm, log that it's using a different communication method + log.Println("Using a non-UART communication method") + } + + return nil +} + +// WriteRegister sends a register write command to the TMC2209. +func (driver *TMC2209) WriteRegister(reg uint8, value uint32) error { + if driver.comm == nil { + return CustomError("communication interface not set") + } + // Use the communication interface (RegisterComm) to write the register + return driver.comm.WriteRegister(reg, value, driver.address) +} + +// ReadRegister sends a register read command to the TMC2209 and returns the read value. +func (driver *TMC2209) ReadRegister(reg uint8) (uint32, error) { + if driver.comm == nil { + return 0, CustomError("communication interface not set") + } + // Use the communication interface (RegisterComm) to read the register + return driver.comm.ReadRegister(reg, driver.address) +} diff --git a/tmc2209/uartcomm.go b/tmc2209/uartcomm.go new file mode 100644 index 0000000..f1afb86 --- /dev/null +++ b/tmc2209/uartcomm.go @@ -0,0 +1,112 @@ +//go:build tinygo + +package tmc2209 + +import ( + "machine" + "time" +) + +// CustomError is a lightweight error type used for TinyGo compatibility. +type CustomError string + +func (e CustomError) Error() string { + return string(e) +} + +// UARTComm implements RegisterComm for UART-based communication +type UARTComm struct { + uart machine.UART + address uint8 +} + +// NewUARTComm creates a new UARTComm instance. +func NewUARTComm(uart machine.UART, address uint8) *UARTComm { + return &UARTComm{ + uart: uart, + address: address, + } +} + +// Setup initializes the UART communication with the TMC2209. +func (comm *UARTComm) Setup() error { + // Check if UART is initialized + if comm.uart == (machine.UART{}) { + return CustomError("UART not initialized") + } + + // Configure the UART interface with the desired baud rate and settings + err := comm.uart.Configure(machine.UARTConfig{ + BaudRate: 115200, + }) + if err != nil { + return CustomError("Failed to configure UART") + } + + // No built-in timeout in TinyGo, so timeout will be handled in the read/write methods + return nil +} + +// WriteRegister sends a register write command to the TMC2209 with a timeout. +func (comm *UARTComm) WriteRegister(register uint8, value uint32, driverIndex uint8) error { + buffer := []byte{ + 0x05, // Sync byte + comm.address, // Slave address + register | 0x80, // Write command (set MSB to 1 for write) + byte((value >> 24) & 0xFF), // MSB of value + byte((value >> 16) & 0xFF), // Middle byte + byte((value >> 8) & 0xFF), // Next byte + byte(value & 0xFF), // LSB of value + 0, // CRC + } + + buffer[7] = CalculateCRC(buffer[:7]) + + // Write the data to the TMC2209 + done := make(chan error, 1) + + go func() { + comm.uart.Write(buffer) + done <- nil + }() + + // Implementing timeout using a 100ms timer + select { + case err := <-done: + return err + case <-time.After(100 * time.Millisecond): // Timeout after 100ms + return CustomError("write timeout") + } +} + +// ReadRegister sends a register read command to the TMC2209 with a timeout. +func (comm *UARTComm) ReadRegister(register uint8, driverIndex uint8) (uint32, error) { + var writeBuffer [4]byte + writeBuffer[0] = 0x05 // Sync byte + writeBuffer[1] = 0x00 // Slave address + writeBuffer[2] = register & 0x7F // Read command (MSB clear for read) + writeBuffer[3] = CalculateCRC(writeBuffer[:3]) + + // Send the read command + done := make(chan []byte, 1) + go func() { + comm.uart.Write(writeBuffer[:]) + readBuffer := make([]byte, 8) + comm.uart.Read(readBuffer) + done <- readBuffer + }() + + // Implementing timeout using a 100ms timer + select { + case readBuffer := <-done: + checksum := CalculateCRC(readBuffer[:7]) + if checksum != readBuffer[7] { + return 0, CustomError("checksum error") + } + + // Return the value from the register + return uint32(readBuffer[3])<<24 | uint32(readBuffer[4])<<16 | uint32(readBuffer[5])<<8 | uint32(readBuffer[6]), nil + case <-time.After(100 * time.Millisecond): // Timeout after 100ms + return 0, CustomError("read timeout") + } +} diff --git a/tmc2209/utils.go b/tmc2209/utils.go new file mode 100644 index 0000000..4aca832 --- /dev/null +++ b/tmc2209/utils.go @@ -0,0 +1,73 @@ +package tmc2209 + +import "log" + +func CalculateCRC(data []byte) uint8 { + crc := uint8(0) + for _, byte := range data { + for i := 0; i < 8; i++ { + if (crc>>7)^(byte&0x01) == 1 { + crc = (crc << 1) ^ 0x07 + } else { + crc = crc << 1 + } + byte >>= 1 + } + } + return crc +} + +// VerifyCommunication checks the communication with the TMC2209 by reading the version register (IOIN). +// It returns true if the communication is successful (i.e., the version matches the expected version). +// VerifyCommunication verifies the communication with the TMC2209 by reading the version register (IOIN). +// It explicitly resets the struct and de-references it after the check to ensure memory is managed manually. +func VerifyCommunication(comm RegisterComm, driverIndex uint8) bool { + var io *Ioin + if io == nil { + io = NewIoin() // Initialize the struct if not already initialized + } else { + *io = Ioin{} + } + _, err := ReadRegister(comm, driverIndex, io.GetAddress()) + if err != nil { + return false + } + if io.Version == expectedVersion { + io = nil + return true + } + io = nil + return false +} + +// CheckErrorStatus verifies the communication and checks for error flags in the TMC2209 driver status. +// It explicitly resets the struct and de-references it when done to ensure memory is managed manually. +func CheckErrorStatus(comm RegisterComm, driverIndex uint8) bool { + var d *DrvStatus + if d == nil { + d = NewDrvStatus() + } else { + *d = DrvStatus{} + } + _, err := d.Read(comm, driverIndex) + if err != nil { + return false + } + errorFlags := d.Ola | d.S2vsa | d.S2vsb | d.Ot | d.S2ga | d.S2gb | d.Olb + if errorFlags != 0 { + log.Printf("TMC2209 Error Detected: %X", errorFlags) + return false + } + d = nil + return true +} + +// GetInterfaceTransmissionCount reads the IFCNT register to check for UART transmission status +func GetInterfaceTransmissionCount(comm RegisterComm, driverIndex uint8) (uint32, error) { + ifcnt := NewIfcnt() + _, err := ReadRegister(comm, driverIndex, ifcnt.GetAddress()) + if err != nil { + return 0, err + } + return ifcnt.Bytes, nil +} diff --git a/tmc2209/velocity.go b/tmc2209/velocity.go new file mode 100644 index 0000000..d3c1bb6 --- /dev/null +++ b/tmc2209/velocity.go @@ -0,0 +1,12 @@ +package tmc2209 + +func MoveAtVelocity(microstepsPerPeriod int32) { + // Write velocity to the relevant register +} +func MoveUsingStepDirInterface() { + // Set the interface to use step/direction interface +} +func GetVelocity() uint32 { + // Read the actual velocity register + return 0 // Placeholder return value +} diff --git a/tmc5160/README.MD b/tmc5160/README.MD new file mode 100644 index 0000000..4c4e11c --- /dev/null +++ b/tmc5160/README.MD @@ -0,0 +1,211 @@ +# TMC5160 Driver for Go (TinyGo) + +This repository provides a Go-based driver for the **TMC5160** stepper motor driver, implemented for both **SPI** and **UART** communication modes. The driver allows you to easily interface with the TMC5160 to configure and control stepper motors. + +## Table of Contents + +- [Installation](#installation) +- [Communication Modes](#communication-modes) + - [SPI Mode](#spi-mode) + - [UART Mode](#uart-mode) +- [Usage Example](#usage-example) + - [Setting and Getting Modes](#setting-and-getting-modes) + - [Reading and Writing Registers](#reading-and-writing-registers) +- [API Reference](#api-reference) +- [License](#license) + +## Installation + +To use the TMC5160 driver, you'll need to have **TinyGo** installed. You can install TinyGo by following the [official installation guide](https://tinygo.org/getting-started/). + +### Dependencies + +- **machine**: To interface with hardware on platforms like Raspberry Pi, STM32, etc. +- **TinyGo**: A Go compiler for embedded systems. + +Add the module + +```bash +import "tinygo.org/x/drivers/tmc5160" +``` + +### Communication Modes + +The TMC5160 supports two communication modes for controlling the motor: + +**SPI Mode** + +To communicate with the TMC5160 in SPI mode, you'll need to configure the SPI bus and the chip-select (CS) pin. This allows full-speed communication between your microcontroller and the TMC5160. +SPI Setup + +In SPI mode, you must configure the SPI interface on your microcontroller. Here's how to set up SPI communication for the TMC5160. + +```go +spi := machine.SPI1 +csPin := machine.GPIO13 +spi.Configure(machine.SPIConfig{ +SCK: machine.GPIO10, +SDI: machine.GPIO11, +SDO: machine.GPIO12, +Frequency: 5000000, +Mode: 3, +LSBFirst: false, +}) + +csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) +``` +**Sending Commands via SPI** + +The driver supports reading and writing registers using the SPIComm interface, which is initialized with the configured SPI bus and CS pins + +```go +comm := tmc5160.NewSPIComm(*spi, csPins) +driver := tmc5160.NewTMC5160(comm, driverIndex) +driver.WriteRegister(tmc5160.GCONF, value) + +``` + +**UART Mode** + +Alternatively, you can use UART mode to communicate with the TMC5160. UART mode is useful for cases where SPI is not available or when the TMC5160 is used in multi-driver configurations with limited SPI pins. +UART Setup + +In UART mode, you will need to configure the UART interface with the appropriate baud rate and settings: + +```go +uart := machine.UART0 +uart.Configure(machine.UARTConfig{ + BaudRate: 115200, +}) +``` +#### Sending Commands via UART + +The UART communication is handled through the UARTComm struct, which wraps the UART interface. + +```go +comm := tmc5160.NewUARTComm(uart, 0x01) +driver := tmc5160.NewTMC5160(comm, 0) +driver.WriteRegister(tmc5160.GCONF, 0x01) +``` + +## Usage Example + +Here’s a simple example of how to use the TMC5160 driver with SPI and UART modes: + +```aiignore +// Connects to SPI1 on a RP2040 (Pico) +package main + +import ( + "machine" + + "tinygo.org/x/drivers/tmc5160" +) + +func main() { + // Step 1. Setup your protocol. SPI setup shown below + spi := machine.SPI1 + spi.Configure(machine.SPIConfig{ + SCK: machine.GPIO10, + SDI: machine.GPIO11, + SDO: machine.GPIO12, + Frequency: 12000000, // Upto 12 MHZ is pretty stable. Reduce to 5 or 6 Mhz if you are experiencing issues + Mode: 3, + LSBFirst: false, + }) + // Step 2. Set up all associated Pins + csPin0 := machine.GPIO13 + csPin0.Configure(machine.PinConfig{Mode: machine.PinOutput}) + enn0 := machine.GPIO18 + enn0.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + // csPins is a map of all chip select pins in a multi driver setup. + //Only one pin csPin0 mapped to "0"is shown in this example, but add more mappings as required + csPins := map[uint8]machine.Pin{0: csPin0} + //bind csPin to driverAdddress + driverAddress := uint8(0) // Let's assume we are working with driver at address 0x01 + // Step 3. Bind the communication interface to the protocol + comm := tmc5160.NewSPIComm(*spi, csPins) + // Step 4. Define your stepper like this below + //stepper := tmc5160.NewStepper(angle , gearRatio vSupply rCoil , lCoil , iPeak , rSense , mSteps, fclk ) + stepper := tmc5160.NewDefaultStepper() // Default Stepper should be used only for testing. + // Step 5. Instantiate your driver + driver := tmc5160.NewDriver( + comm, + driverAddress, + enn0, + stepper) + + // Setting and getting mode + rampMode := tmc5160.NewRAMPMODE(comm, driverAddress) + err := rampMode.SetMode(tmc5160.PositioningMode) + if err != nil { + return + } + mode, err := rampMode.GetMode() + if err != nil { + println("Error getting mode:", err) + } else { + println("Current Mode:", mode) + } + + // Read GCONF register + GCONF := tmc5160.NewGCONF() + gconfVal, err := driver.ReadRegister(tmc5160.GCONF) + // Uppack the register to get all the bits and bytes of the register + GCONF.Unpack(gconfVal) + //E.g. MultiStepFlit is retrieved from the GCONF register + println("GCONF:MultiStepFlit:", GCONF.MultistepFilt) +} + + +``` +## Reading and Writing Registers + +You can easily read and write registers using the WriteRegister and ReadRegister methods: + +```aiignore +// Write a value to a register +err := driver.WriteRegister(tmc5160.GCONF, 0x01) +if err != nil { + fmt.Println("Error writing register:", err) +} + +// Read a register +value, err := driver.ReadRegister(tmc5160.GCONF) +if err != nil { + fmt.Println("Error reading register:", err) +} else { + fmt.Println("Read value from GCONF:", value) +} + +``` + +## API Reference + + NewSPIComm(spi *machine.SPI, csPins map[uint8]machine.Pin) *SPIComm + +Creates a new SPI communication interface for the TMC5160. + + NewUARTComm(uart machine.UART, address uint8) *UARTComm + +Creates a new UART communication interface for the TMC5160. + + NewTMC5160(comm RegisterComm, address uint8) *TMC5160 + +Creates a new instance of the TMC5160 driver. + + WriteRegister(register uint8, value uint32) error + +Writes a value to the specified register. + + ReadRegister(register uint8) (uint32, error) + +Reads a value from the specified register. + +## License + +This project is licensed under the MIT License + + + diff --git a/tmc5160/address.go b/tmc5160/address.go new file mode 100644 index 0000000..473626c --- /dev/null +++ b/tmc5160/address.go @@ -0,0 +1,82 @@ +package tmc5160 + +// Driver Register addresses +const ( + GCONF uint8 = 0x00 // Global configuration flags + GSTAT uint8 = 0x01 // Global status flags + IFCNT = 0x02 // UART transmission counter + SLAVECONF = 0x03 // UART slave configuration + IOIN = 0x04 // Read input / write output pins + X_COMPARE = 0x05 // Position comparison register + OTP_PROG = 0x06 // OTP programming register + OTP_READ = 0x07 // OTP read register + FACTORY_CONF = 0x08 // Factory configuration (clock trim) + SHORT_CONF = 0x09 // Short detector configuration + DRV_CONF = 0x0A // Driver configuration + GLOBAL_SCALER = 0x0B // Global scaling of motor current + OFFSET_READ = 0x0C // Offset calibration results + + /* Velocity dependent driver feature control registers */ + IHOLD_IRUN = 0x10 // Driver current control + TPOWERDOWN = 0x11 // Delay before power down + TSTEP = 0x12 // Actual time between microsteps + TPWMTHRS = 0x13 // Upper velocity for stealthChop voltage PWM mode + TCOOLTHRS = 0x14 // Lower threshold velocity for switching on smart energy coolStep and stallGuard feature + THIGH = 0x15 // Velocity threshold for switching into a different chopper mode and fullstepping + + /* Ramp generator motion control registers */ + RAMPMODE = 0x20 // Driving mode (Velocity, Positioning, Hold) + XACTUAL = 0x21 // Actual motor position + VACTUAL = 0x22 // Actual motor velocity from ramp generator + VSTART = 0x23 // Motor start velocity + A_1 = 0x24 // First acceleration between VSTART and V1 + V_1 = 0x25 // First acceleration/deceleration phase target velocity + AMAX = 0x26 // Second acceleration between V1 and VMAX + VMAX = 0x27 // Target velocity in velocity mode + DMAX = 0x28 // Deceleration between VMAX and V1 + D_1 = 0x2A // Deceleration between V1 and VSTOP + //Attention: Do not set 0 in positioning mode, even if V1=0! + VSTOP = 0x2B // Motor stop velocity + //Attention: Set VSTOP > VSTART! + //Attention: Do not set 0 in positioning mode, minimum 10 recommend! + TZEROWAIT = 0x2C // Waiting time after ramping down to zero velocity before next movement or direction inversion can start. + XTARGET = 0x2D // Target position for ramp mode + + /* Ramp generator driver feature control registers */ + VDCMIN = 0x33 // Velocity threshold for enabling automatic commutation dcStep + SW_MODE = 0x34 // Switch mode configuration + RAMP_STAT = 0x35 // Ramp status and switch event status + XLATCH = 0x36 // Ramp generator latch position upon programmable switch event + + /* Encoder registers */ + ENCMODE = 0x38 // Encoder configuration and use of N channel + X_ENC = 0x39 // Actual encoder position + ENC_CONST = 0x3A // Accumulation constant + ENC_STATUS = 0x3B // Encoder status information + ENC_LATCH = 0x3C // Encoder position latched on N event + ENC_DEVIATION = 0x3D // Maximum number of steps deviation between encoder counter and XACTUAL for deviation warning + + /* Motor driver registers */ + MSLUT0 uint8 = 0x60 // 32 bits + MSLUT1 uint8 = 0x61 // 32 bits + MSLUT2 uint8 = 0x62 // 32 bits + MSLUT3 uint8 = 0x63 // 32 bits + MSLUT4 uint8 = 0x64 // 32 bits + MSLUT5 uint8 = 0x65 // 32 bits + MSLUT6 uint8 = 0x66 // 32 bits + MSLUT7 uint8 = 0x67 // 32 bits + MSLUTSEL = 0x68 // Look up table segmentation definition + MSLUTSTART = 0x69 // Absolute current at microstep table entries 0 and 256 + MSCNT = 0x6A // Actual position in the microstep table + MSCURACT = 0x6B // Actual microstep current + CHOPCONF = 0x6C // Chopper and driver configuration + COOLCONF = 0x6D // coolStep smart current control register and stallGuard2 configuration + DCCTRL = 0x6E // dcStep automatic commutation configuration register + DRV_STATUS = 0x6F // stallGuard2 value and driver error flags + PWMCONF = 0x70 // stealthChop voltage PWM mode chopper configuration + PWM_SCALE = 0x71 // Results of stealthChop amplitude regulator. + PWM_AUTO = 0x72 // Automatically determined PWM config values + LOST_STEPS = 0x73 // Number of input steps skipped due to dcStep. only with SD_MODE = 1 + expectedVersion = 0x03 + DEFAULT_F_CLK = 12000000 +) diff --git a/tmc5160/helpers.go b/tmc5160/helpers.go new file mode 100644 index 0000000..3ffa1da --- /dev/null +++ b/tmc5160/helpers.go @@ -0,0 +1,56 @@ +package tmc5160 + +import ( + "github.com/orsinium-labs/tinymath" + "golang.org/x/exp/constraints" +) + +// VelocityToVMAX calculates the VMAX register value from the current stepper velocity which is in microsteps per tRef (i.e 1/clock speed) +func (stepper *Stepper) CurrentVelocityToVMAX() uint32 { + tref := float32(16777216) / (float32(stepper.Fclk) * 1000000) + r := stepper.VelocitySPS * stepper.GearRatio * float32(tref) + return constrain(uint32(r), 0, maxVMAX) // VMAX register value cannot exceed maxVMAX +} +func (stepper *Stepper) DesiredVelocityToVMAX(v float32) uint32 { + tref := 16777216 / (float32(stepper.Fclk) * 1000000) + r := tinymath.Round(v * stepper.GearRatio * tref) + return constrain(uint32(r), 0, maxVMAX) // VMAX register value cannot exceed maxVMAX +} + +func (stepper *Stepper) DesiredAccelToAMAX(dacc float32, dVel float32) uint32 { + dVelToVMAX := stepper.DesiredVelocityToVMAX(dVel) + _a := uint64(dVelToVMAX) * 131072 + _b := float32(_a) / dacc + _c := _b / float32(uint32(stepper.Fclk)*1000000) + return uint32(_c) + +} + +// Convert threshold speed (Hz) to internal TSTEP value +func (stepper *Stepper) DesiredSpeedToTSTEP(thrsSpeed uint32) uint32 { + if thrsSpeed < 0 { + return 0 + } + _a := stepper.DesiredVelocityToVMAX(float32(thrsSpeed)) + _b := float32(16777216 / _a) + _c := float32(stepper.MSteps) / float32(256) + _d := uint32(_b * _c) + return constrain(_d, 0, 1048575) +} + +func (stepper *Stepper) VMAXToTSTEP(vmax uint32) uint32 { + _b := float32(16777216 / vmax) + _c := float32(stepper.MSteps) / float32(256) + _d := tinymath.Round(_b * _c) + return constrain(uint32(_d), 0, 1048575) +} + +// Constrain function to limit values to a specific range (supports multiple types). +func constrain[T constraints.Ordered](value, min, max T) T { + if value < min { + return min + } else if value > max { + return max + } + return value +} diff --git a/tmc5160/registers.go b/tmc5160/registers.go new file mode 100644 index 0000000..d4d13a1 --- /dev/null +++ b/tmc5160/registers.go @@ -0,0 +1,2080 @@ +package tmc5160 + +import ( + math "github.com/orsinium-labs/tinymath" +) + +// RegisterComm defines an interface for reading from and writing to hardware registers. +type RegisterComm interface { + ReadRegister(register uint8, driverIndex uint8) (uint32, error) + WriteRegister(register uint8, value uint32, driverIndex uint8) error +} + +// ReadRegister function using the register constants +func ReadRegister(comm RegisterComm, driverIndex uint8, register uint8) (uint32, error) { + // Read the register value using the comm interface + + value, err := comm.ReadRegister(register, driverIndex) + if err != nil { + return 0, err + } + return value, nil +} + +// WriteRegister function using the register constants +func WriteRegister(comm RegisterComm, register uint8, driverIndex uint8, value uint32) error { + // Write the value to the register using the comm interface + return comm.WriteRegister(register, value, driverIndex) +} + +// Register and methods to pack and unpack +// Base Register struct +type Register struct { + RegisterAddr uint8 + Bytes uint32 +} + +// Common New function for creating a new register instance +func NewRegister(addr uint8) *Register { + return &Register{ + RegisterAddr: addr, + } +} + +// Common Pack method: for subclasses to implement their packing logic +func (r *Register) Pack() uint32 { + return r.Bytes // Default, should be overridden in register-specific structs +} + +// Common Unpack method: for subclasses to implement their unpacking logic +func (r *Register) Unpack(registerValue uint32) { + r.Bytes = registerValue // Default, should be overridden in register-specific structs +} + +// Common GetAddress method +func (r *Register) GetAddress() uint8 { + return r.RegisterAddr +} + +// Common Read method (assuming the communication interface is implemented) +func (r *Register) Read(comm RegisterComm, driverIndex uint8) (uint32, error) { + return ReadRegister(comm, driverIndex, r.RegisterAddr) +} + +// Common Write method +func (r *Register) Write(comm RegisterComm, driverIndex uint8, value uint32) error { + return WriteRegister(comm, r.RegisterAddr, driverIndex, value) +} + +// GCONF Register bit fields' masks and shifts +const ( + // Recalibrate: Zero crossing recalibration during driver disable + GCONF_Recalibrate_Mask = 1 << 0 + // Faststandstill: Timeout for step execution until standstill detection + GCONF_Faststandstill_Mask = 1 << 1 + // Enable PWM mode for StealthChop + GCONF_EnPwmMode_Mask = 1 << 2 + // Enable step input filtering for StealthChop optimization + GCONF_MultistepFilt_Mask = 1 << 3 + // Motor direction + GCONF_Shaft_Mask = 1 << 4 + // Error flags on DIAG0 pin + GCONF_Diag0Error_Mask = 1 << 5 + // Enable DIAG0 for Over temperature warning + GCONF_Diag0Otpw_Mask = 1 << 6 + // Enable DIAG0 for stall step detection + GCONF_Diag0StallStep_Mask = 1 << 7 + // Enable DIAG1 for stall direction + GCONF_Diag1StallDir_Mask = 1 << 8 + // Enable DIAG1 for index position + GCONF_Diag1Index_Mask = 1 << 9 + // Enable DIAG1 for chopper on state + GCONF_Diag1Onstate_Mask = 1 << 10 + // Enable DIAG1 for skipped steps + GCONF_Diag1StepsSkipped_Mask = 1 << 11 + // Enable DIAG0 push-pull output + GCONF_Diag0IntPushPull_Mask = 1 << 12 + // Enable DIAG1 push-pull output + GCONF_Diag1PosCompPushPull_Mask = 1 << 13 + // Small hysteresis for step frequency comparison + GCONF_SmallHysteresis_Mask = 1 << 14 + // Enable emergency stop + GCONF_StopEnable_Mask = 1 << 15 + // Direct motor coil current and polarity control + GCONF_DirectMode_Mask = 1 << 16 + // Test mode (not for normal use) + GCONF_TestMode_Mask = 1 << 17 +) + +// GCONF Register structure +type GCONF_Register struct { + Register + // Fields corresponding to individual settings in GCONF register + Recalibrate bool + Faststandstill bool + EnPwmMode bool + MultistepFilt bool + Shaft bool + Diag0Error bool + Diag0Otpw bool + Diag0StallStep bool + Diag1StallDir bool + Diag1Index bool + Diag1Onstate bool + Diag1StepsSkipped bool + Diag0IntPushPull bool + Diag1PosCompPushPull bool + SmallHysteresis bool + StopEnable bool + DirectMode bool + TestMode bool +} + +// NewGCONF initializes a new GCONF register instance +func NewGCONF() *GCONF_Register { + return &GCONF_Register{ + Register: Register{ + RegisterAddr: GCONF, // GSTAT register address + }, + } +} + +// Pack the fields into a single 32-bit register value +func (g *GCONF_Register) Pack() uint32 { + var registerValue uint32 + + // Use bitwise OR to set individual bits based on the field values + if g.Recalibrate { + registerValue |= GCONF_Recalibrate_Mask + } + if g.Faststandstill { + registerValue |= GCONF_Faststandstill_Mask + } + if g.EnPwmMode { + registerValue |= GCONF_EnPwmMode_Mask + } + if g.MultistepFilt { + registerValue |= GCONF_MultistepFilt_Mask + } + if g.Shaft { + registerValue |= GCONF_Shaft_Mask + } + if g.Diag0Error { + registerValue |= GCONF_Diag0Error_Mask + } + if g.Diag0Otpw { + registerValue |= GCONF_Diag0Otpw_Mask + } + if g.Diag0StallStep { + registerValue |= GCONF_Diag0StallStep_Mask + } + if g.Diag1StallDir { + registerValue |= GCONF_Diag1StallDir_Mask + } + if g.Diag1Index { + registerValue |= GCONF_Diag1Index_Mask + } + if g.Diag1Onstate { + registerValue |= GCONF_Diag1Onstate_Mask + } + if g.Diag1StepsSkipped { + registerValue |= GCONF_Diag1StepsSkipped_Mask + } + if g.Diag0IntPushPull { + registerValue |= GCONF_Diag0IntPushPull_Mask + } + if g.Diag1PosCompPushPull { + registerValue |= GCONF_Diag1PosCompPushPull_Mask + } + if g.SmallHysteresis { + registerValue |= GCONF_SmallHysteresis_Mask + } + if g.StopEnable { + registerValue |= GCONF_StopEnable_Mask + } + if g.DirectMode { + registerValue |= GCONF_DirectMode_Mask + } + if g.TestMode { + registerValue |= GCONF_TestMode_Mask + } + return registerValue +} + +// Unpack a 32-bit register value into individual fields +func (g *GCONF_Register) Unpack(registerValue uint32) { + g.Recalibrate = (registerValue & GCONF_Recalibrate_Mask) != 0 + g.Faststandstill = (registerValue & GCONF_Faststandstill_Mask) != 0 + g.EnPwmMode = (registerValue & GCONF_EnPwmMode_Mask) != 0 + g.MultistepFilt = (registerValue & GCONF_MultistepFilt_Mask) != 0 + g.Shaft = (registerValue & GCONF_Shaft_Mask) != 0 + g.Diag0Error = (registerValue & GCONF_Diag0Error_Mask) != 0 + g.Diag0Otpw = (registerValue & GCONF_Diag0Otpw_Mask) != 0 + g.Diag0StallStep = (registerValue & GCONF_Diag0StallStep_Mask) != 0 + g.Diag1StallDir = (registerValue & GCONF_Diag1StallDir_Mask) != 0 + g.Diag1Index = (registerValue & GCONF_Diag1Index_Mask) != 0 + g.Diag1Onstate = (registerValue & GCONF_Diag1Onstate_Mask) != 0 + g.Diag1StepsSkipped = (registerValue & GCONF_Diag1StepsSkipped_Mask) != 0 + g.Diag0IntPushPull = (registerValue & GCONF_Diag0IntPushPull_Mask) != 0 + g.Diag1PosCompPushPull = (registerValue & GCONF_Diag1PosCompPushPull_Mask) != 0 + g.SmallHysteresis = (registerValue & GCONF_SmallHysteresis_Mask) != 0 + g.StopEnable = (registerValue & GCONF_StopEnable_Mask) != 0 + g.DirectMode = (registerValue & GCONF_DirectMode_Mask) != 0 + g.TestMode = (registerValue & GCONF_TestMode_Mask) != 0 +} + +// Example Register: GSTAT +type GSTAT_Register struct { + Register + Reset bool + DrvErr bool + UvCp bool +} + +// NewGSTAT creates a new GSTAT register instance +func NewGSTAT() *GSTAT_Register { + return &GSTAT_Register{ + Register: Register{ + RegisterAddr: GSTAT, // GSTAT register address + }, + } +} + +// Pack method for GSTAT: overrides the base Pack +func (g *GSTAT_Register) Pack() uint32 { + var registerValue uint32 + if g.Reset { + registerValue |= 1 << 0 + } + if g.DrvErr { + registerValue |= 1 << 1 + } + if g.UvCp { + registerValue |= 1 << 2 + } + return registerValue +} + +// Unpack method for GSTAT: overrides the base Unpack +func (g *GSTAT_Register) Unpack(registerValue uint32) { + g.Reset = (registerValue & (1 << 0)) != 0 + g.DrvErr = (registerValue & (1 << 1)) != 0 + g.UvCp = (registerValue & (1 << 2)) != 0 +} + +// IOIN_Register struct to represent the IOIN register +type IOIN_Register struct { + Register + ReflStep bool + RefrDir bool + EncbDcenCfg4 bool + EncaDcinCfg5 bool + DrvEnn bool + EncNDcoCfg6 bool + SdMode bool + SwcompIn bool + Version uint8 +} + +// NewIOIN creates a new IOIN register instance +func NewIOIN() *IOIN_Register { + return &IOIN_Register{ + Register: Register{ + RegisterAddr: IOIN, + }, + } +} + +// Pack method for IOIN: overrides the base Pack +func (i *IOIN_Register) Pack() uint32 { + var registerValue uint32 + + // Set individual bits based on the field values + if i.ReflStep { + registerValue |= 1 << 0 + } + if i.RefrDir { + registerValue |= 1 << 1 + } + if i.EncbDcenCfg4 { + registerValue |= 1 << 2 + } + if i.EncaDcinCfg5 { + registerValue |= 1 << 3 + } + if i.DrvEnn { + registerValue |= 1 << 4 + } + if i.EncNDcoCfg6 { + registerValue |= 1 << 5 + } + if i.SdMode { + registerValue |= 1 << 6 + } + if i.SwcompIn { + registerValue |= 1 << 7 + } + // Handle the version field (8 bits, starting at bit 24) + registerValue |= uint32(i.Version) << 24 + + return registerValue +} + +// Unpack method for IOIN: overrides the base Unpack +func (i *IOIN_Register) Unpack(registerValue uint32) { + i.ReflStep = (registerValue & (1 << 0)) != 0 + i.RefrDir = (registerValue & (1 << 1)) != 0 + i.EncbDcenCfg4 = (registerValue & (1 << 2)) != 0 + i.EncaDcinCfg5 = (registerValue & (1 << 3)) != 0 + i.DrvEnn = (registerValue & (1 << 4)) != 0 + i.EncNDcoCfg6 = (registerValue & (1 << 5)) != 0 + i.SdMode = (registerValue & (1 << 6)) != 0 + i.SwcompIn = (registerValue & (1 << 7)) != 0 + // Extract the version field (8 bits, starting at bit 24) + i.Version = uint8((registerValue >> 24) & 0xFF) +} + +// SHORT_CONF_Register struct to represent the SHORT_CONF register +type SHORT_CONF_Register struct { + Register + S2vsLevel uint8 // Short to VS detector sensitivity (4 bits) + S2gLevel uint8 // Short to GND detector sensitivity (4 bits) + ShortFilter uint8 // Spike filtering bandwidth for short detection (2 bits) + ShortDelay bool // Short detection delay (1 bit) +} + +// NewSHORT_CONF creates a new SHORT_CONF register instance +func NewSHORT_CONF() *SHORT_CONF_Register { + return &SHORT_CONF_Register{ + Register: Register{ + RegisterAddr: SHORT_CONF, + }, + } +} + +// Pack method for SHORT_CONF: overrides the base Pack +func (s *SHORT_CONF_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(s.S2vsLevel&0xF) << 0 // S2vsLevel: 4 bits + registerValue |= uint32(s.S2gLevel&0xF) << 8 // S2gLevel: 4 bits + registerValue |= uint32(s.ShortFilter&0x3) << 16 // ShortFilter: 2 bits + if s.ShortDelay { + registerValue |= 1 << 18 // ShortDelay: 1 bit + } + return registerValue +} + +// Unpack method for SHORT_CONF: overrides the base Unpack +func (s *SHORT_CONF_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + s.S2vsLevel = uint8((registerValue >> 0) & 0xF) // Extract 4 bits for S2vsLevel + s.S2gLevel = uint8((registerValue >> 8) & 0xF) // Extract 4 bits for S2gLevel + s.ShortFilter = uint8((registerValue >> 16) & 0x3) // Extract 2 bits for ShortFilter + s.ShortDelay = (registerValue & (1 << 18)) != 0 // Extract 1 bit for ShortDelay +} + +// DRV_CONF_Register struct to represent the DRV_CONF register +type DRV_CONF_Register struct { + Register + BBMTime uint8 // Break before make delay (5 bits) + BBMClks uint8 // Digital BBM Time in clock cycles (4 bits) + OTSelect uint8 // Over temperature level selection for bridge disable (2 bits) + DrvStrength uint8 // Gate drivers current selection (2 bits) + FiltIsense uint8 // Filter time constant of sense amplifier (2 bits) +} + +// NewDRV_CONF creates a new DRV_CONF register instance +func NewDRV_CONF() *DRV_CONF_Register { + return &DRV_CONF_Register{ + Register: Register{ + RegisterAddr: DRV_CONF, + }, + } +} + +// Pack method for DRV_CONF: overrides the base Pack +func (d *DRV_CONF_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(d.BBMTime&0x1F) << 0 // BBMTime: 5 bits + registerValue |= uint32(d.BBMClks&0xF) << 8 // BBMClks: 4 bits + registerValue |= uint32(d.OTSelect&0x3) << 16 // OTSelect: 2 bits + registerValue |= uint32(d.DrvStrength&0x3) << 18 // DrvStrength: 2 bits + registerValue |= uint32(d.FiltIsense&0x3) << 20 // FiltIsense: 2 bits + + return registerValue +} + +// Unpack method for DRV_CONF: overrides the base Unpack +func (d *DRV_CONF_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + d.BBMTime = uint8((registerValue >> 0) & 0x1F) // Extract 5 bits for BBMTime + d.BBMClks = uint8((registerValue >> 8) & 0xF) // Extract 4 bits for BBMClks + d.OTSelect = uint8((registerValue >> 16) & 0x3) // Extract 2 bits for OTSelect + d.DrvStrength = uint8((registerValue >> 18) & 0x3) // Extract 2 bits for DrvStrength + d.FiltIsense = uint8((registerValue >> 20) & 0x3) // Extract 2 bits for FiltIsense +} + +// OFFSET_READ_Register struct to represent the OFFSET_READ register +type OFFSET_READ_Register struct { + Register + PhaseB uint8 // Phase B offset (8 bits) + PhaseA uint8 // Phase A offset (8 bits) +} + +// NewOFFSET_READ creates a new OFFSET_READ register instance +func NewOFFSET_READ() *OFFSET_READ_Register { + return &OFFSET_READ_Register{ + Register: Register{ + RegisterAddr: OFFSET_READ, + }, + } +} + +// Pack method for OFFSET_READ: overrides the base Pack +func (o *OFFSET_READ_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(o.PhaseB&0xFF) << 0 // PhaseB: 8 bits + registerValue |= uint32(o.PhaseA&0xFF) << 8 // PhaseA: 8 bits + + return registerValue +} + +// Unpack method for OFFSET_READ: overrides the base Unpack +func (o *OFFSET_READ_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + o.PhaseB = uint8((registerValue >> 0) & 0xFF) // Extract 8 bits for PhaseB + o.PhaseA = uint8((registerValue >> 8) & 0xFF) // Extract 8 bits for PhaseA +} + +// IHOLD_IRUN_Register struct to represent the IHOLD_IRUN register +type IHOLD_IRUN_Register struct { + Register + Ihold uint8 // Standstill current (5 bits) + Irun uint8 // Motor run current (5 bits) + IholdDelay uint8 // Motor power down delay (4 bits) +} + +// NewIHOLD_IRUN creates a new IHOLD_IRUN register instance +func NewIHOLD_IRUN() *IHOLD_IRUN_Register { + return &IHOLD_IRUN_Register{ + Register: Register{ + RegisterAddr: IHOLD_IRUN, + }, + } +} + +// Pack method for IHOLD_IRUN: overrides the base Pack +func (i *IHOLD_IRUN_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(i.Ihold&0x1F) << 0 // Ihold: 5 bits + registerValue |= uint32(i.Irun&0x1F) << 8 // Irun: 5 bits + registerValue |= uint32(i.IholdDelay&0xF) << 16 // IholdDelay: 4 bits + + return registerValue +} + +// Unpack method for IHOLD_IRUN: overrides the base Unpack +func (i *IHOLD_IRUN_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + i.Ihold = uint8((registerValue >> 0) & 0x1F) // Extract 5 bits for Ihold + i.Irun = uint8((registerValue >> 8) & 0x1F) // Extract 5 bits for Irun + i.IholdDelay = uint8((registerValue >> 16) & 0xF) // Extract 4 bits for IholdDelay +} + +// SW_MODE_Register struct to represent the SW_MODE register +type SW_MODE_Register struct { + Register + StopLEnable bool // Enable automatic motor stop during active left reference switch input + StopREnable bool // Enable automatic motor stop during active right reference switch input + PolStopL bool // Sets the active polarity of the left reference switch input + PolStopR bool // Sets the active polarity of the right reference switch input + SwapLR bool // Swap the left and right reference switch inputs + LatchLActive bool // Activate latching of the position to XLATCH upon an active going edge on REFL + LatchLInactive bool // Activate latching of the position to XLATCH upon an inactive going edge on REFL + LatchRActive bool // Activate latching of the position to XLATCH upon an active going edge on REFR + LatchRInactive bool // Activate latching of the position to XLATCH upon an inactive going edge on REFR + EnLatchEncoder bool // Latch encoder position to ENC_LATCH upon reference switch event + SgStop bool // Enable stop by stallGuard2 + EnSoftStop bool // Enable soft stop upon a stop event +} + +// NewSW_MODE creates a new SW_MODE register instance +func NewSW_MODE() *SW_MODE_Register { + return &SW_MODE_Register{ + Register: Register{ + RegisterAddr: SW_MODE, + }, + } +} + +// Pack method for SW_MODE: overrides the base Pack +func (s *SW_MODE_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + if s.StopLEnable { + registerValue |= 1 << 0 + } + if s.StopREnable { + registerValue |= 1 << 1 + } + if s.PolStopL { + registerValue |= 1 << 2 + } + if s.PolStopR { + registerValue |= 1 << 3 + } + if s.SwapLR { + registerValue |= 1 << 4 + } + if s.LatchLActive { + registerValue |= 1 << 5 + } + if s.LatchLInactive { + registerValue |= 1 << 6 + } + if s.LatchRActive { + registerValue |= 1 << 7 + } + if s.LatchRInactive { + registerValue |= 1 << 8 + } + if s.EnLatchEncoder { + registerValue |= 1 << 9 + } + if s.SgStop { + registerValue |= 1 << 10 + } + if s.EnSoftStop { + registerValue |= 1 << 11 + } + + return registerValue +} + +// Unpack method for SW_MODE: overrides the base Unpack +func (s *SW_MODE_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + s.StopLEnable = (registerValue & (1 << 0)) != 0 + s.StopREnable = (registerValue & (1 << 1)) != 0 + s.PolStopL = (registerValue & (1 << 2)) != 0 + s.PolStopR = (registerValue & (1 << 3)) != 0 + s.SwapLR = (registerValue & (1 << 4)) != 0 + s.LatchLActive = (registerValue & (1 << 5)) != 0 + s.LatchLInactive = (registerValue & (1 << 6)) != 0 + s.LatchRActive = (registerValue & (1 << 7)) != 0 + s.LatchRInactive = (registerValue & (1 << 8)) != 0 + s.EnLatchEncoder = (registerValue & (1 << 9)) != 0 + s.SgStop = (registerValue & (1 << 10)) != 0 + s.EnSoftStop = (registerValue & (1 << 11)) != 0 +} + +// RAMP_STAT_Register struct to represent the RAMP_STAT register +type RAMP_STAT_Register struct { + Register + StatusStopL bool // Reference switch left status (1=active) + StatusStopR bool // Reference switch right status (1=active) + StatusLatchL bool // Latch left ready (enable position latching) + StatusLatchR bool // Latch right ready (enable position latching) + EventStopL bool // Active stop left condition due to stop switch + EventStopR bool // Active stop right condition due to stop switch + EventStopSG bool // Active StallGuard2 stop event + EventPosReached bool // Target position reached + VelocityReached bool // Target velocity reached + PositionReached bool // Target position reached + VZero bool // Actual velocity is 0 + TZeroWaitActive bool // TZEROWAIT is active after motor stop + SecondMove bool // Automatic ramp required moving back in opposite direction + StatusSG bool // Active stallGuard2 input +} + +// NewRAMP_STAT creates a new RAMP_STAT register instance +func NewRAMP_STAT() *RAMP_STAT_Register { + return &RAMP_STAT_Register{ + Register: Register{ + RegisterAddr: RAMP_STAT, + }, + } +} + +// Pack method for RAMP_STAT: overrides the base Pack +func (r *RAMP_STAT_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + if r.StatusStopL { + registerValue |= 1 << 0 + } + if r.StatusStopR { + registerValue |= 1 << 1 + } + if r.StatusLatchL { + registerValue |= 1 << 2 + } + if r.StatusLatchR { + registerValue |= 1 << 3 + } + if r.EventStopL { + registerValue |= 1 << 4 + } + if r.EventStopR { + registerValue |= 1 << 5 + } + if r.EventStopSG { + registerValue |= 1 << 6 + } + if r.EventPosReached { + registerValue |= 1 << 7 + } + if r.VelocityReached { + registerValue |= 1 << 8 + } + if r.PositionReached { + registerValue |= 1 << 9 + } + if r.VZero { + registerValue |= 1 << 10 + } + if r.TZeroWaitActive { + registerValue |= 1 << 11 + } + if r.SecondMove { + registerValue |= 1 << 12 + } + if r.StatusSG { + registerValue |= 1 << 13 + } + + return registerValue +} + +// Unpack method for RAMP_STAT: overrides the base Unpack +func (r *RAMP_STAT_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + r.StatusStopL = (registerValue & (1 << 0)) != 0 + r.StatusStopR = (registerValue & (1 << 1)) != 0 + r.StatusLatchL = (registerValue & (1 << 2)) != 0 + r.StatusLatchR = (registerValue & (1 << 3)) != 0 + r.EventStopL = (registerValue & (1 << 4)) != 0 + r.EventStopR = (registerValue & (1 << 5)) != 0 + r.EventStopSG = (registerValue & (1 << 6)) != 0 + r.EventPosReached = (registerValue & (1 << 7)) != 0 + r.VelocityReached = (registerValue & (1 << 8)) != 0 + r.PositionReached = (registerValue & (1 << 9)) != 0 + r.VZero = (registerValue & (1 << 10)) != 0 + r.TZeroWaitActive = (registerValue & (1 << 11)) != 0 + r.SecondMove = (registerValue & (1 << 12)) != 0 + r.StatusSG = (registerValue & (1 << 13)) != 0 +} + +// ENCMODE_Register struct to represent the ENCMODE register +type ENCMODE_Register struct { + Register + PolA bool // Required A polarity for an N channel event + PolB bool // Required B polarity for an N channel event + PolN bool // Defines active polarity of N (0=low active, 1=high active) + IgnoreAB bool // Ignore A and B polarity for N channel event + ClrCont bool // Always latch or latch and clear X_ENC upon an N event + ClrOnce bool // Latch or latch and clear X_ENC on the next N event + Sensitivity uint8 // N channel event sensitivity (2 bits) + ClrEncX bool // Clear encoder counter X_ENC upon N-event + LatchXAct bool // Also latch XACTUAL position together with X_ENC + EncSelDecimal bool // Encoder prescaler divisor binary mode (0) / decimal mode (1) +} + +// NewENCMODE creates a new ENCMODE register instance +func NewENCMODE() *ENCMODE_Register { + return &ENCMODE_Register{ + Register: Register{ + RegisterAddr: ENCMODE, + }, + } +} + +// Pack method for ENCMODE: overrides the base Pack +func (e *ENCMODE_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + if e.PolA { + registerValue |= 1 << 0 + } + if e.PolB { + registerValue |= 1 << 1 + } + if e.PolN { + registerValue |= 1 << 2 + } + if e.IgnoreAB { + registerValue |= 1 << 3 + } + if e.ClrCont { + registerValue |= 1 << 4 + } + if e.ClrOnce { + registerValue |= 1 << 5 + } + registerValue |= uint32(e.Sensitivity&0x3) << 6 // Sensitivity: 2 bits + if e.ClrEncX { + registerValue |= 1 << 8 + } + if e.LatchXAct { + registerValue |= 1 << 9 + } + if e.EncSelDecimal { + registerValue |= 1 << 10 + } + + return registerValue +} + +// Unpack method for ENCMODE: overrides the base Unpack +func (e *ENCMODE_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + e.PolA = (registerValue & (1 << 0)) != 0 + e.PolB = (registerValue & (1 << 1)) != 0 + e.PolN = (registerValue & (1 << 2)) != 0 + e.IgnoreAB = (registerValue & (1 << 3)) != 0 + e.ClrCont = (registerValue & (1 << 4)) != 0 + e.ClrOnce = (registerValue & (1 << 5)) != 0 + e.Sensitivity = uint8((registerValue >> 6) & 0x3) // Extract 2 bits for Sensitivity + e.ClrEncX = (registerValue & (1 << 8)) != 0 + e.LatchXAct = (registerValue & (1 << 9)) != 0 + e.EncSelDecimal = (registerValue & (1 << 10)) != 0 +} + +// ENC_STATUS_Register struct to represent the ENC_STATUS register +type ENC_STATUS_Register struct { + Register + NEvent bool // N event detected + DeviationWarn bool // Deviation between X_ACTUAL and X_ENC detected +} + +// NewENC_STATUS creates a new ENC_STATUS register instance +func NewENC_STATUS() *ENC_STATUS_Register { + return &ENC_STATUS_Register{ + Register: Register{ + RegisterAddr: ENC_STATUS, + }, + } +} + +// Pack method for ENC_STATUS: overrides the base Pack +func (e *ENC_STATUS_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + if e.NEvent { + registerValue |= 1 << 0 + } + if e.DeviationWarn { + registerValue |= 1 << 1 + } + + return registerValue +} + +// Unpack method for ENC_STATUS: overrides the base Unpack +func (e *ENC_STATUS_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + e.NEvent = (registerValue & (1 << 0)) != 0 + e.DeviationWarn = (registerValue & (1 << 1)) != 0 +} + +// CHOPCONF_Register struct to represent the CHOPCONF register +type CHOPCONF_Register struct { + Register + Toff uint8 // Off time setting (4 bits) + HstrtTfd uint8 // Hysteresis start value or fast decay time setting (3 bits) + HendOffset uint8 // Hysteresis low value or sine wave offset (4 bits) + Tfd3 bool // Fast decay time setting bit 3 + Disfdcc bool // Disable current comparator usage for fast decay termination + Rndtf bool // Enable random modulation of chopper TOFF time + Chm bool // Chopper mode (0=standard, 1=constant off time with fast decay) + Tbl uint8 // Comparator blank time select (2 bits) + Vsense bool // Select resistor voltage sensitivity (low or high) + Vhighfs bool // Enable fullstep switching when VHIGH is exceeded + Vhighchm bool // Enable switching to chm=1 and fd=0 when VHIGH is exceeded + Tpfd uint8 // Passive fast decay time (4 bits) + Mres uint8 // Microstep resolution (4 bits) + Intpol bool // Enable interpolation to 256 microsteps + Dedge bool // Enable double edge step pulses + Diss2g bool // Disable short to GND protection + Diss2vs bool // Disable short to supply protection +} + +// NewCHOPCONF creates a new CHOPCONF register instance +func NewCHOPCONF() *CHOPCONF_Register { + return &CHOPCONF_Register{ + Register: Register{ + RegisterAddr: CHOPCONF, + }, + } +} + +// Pack method for CHOPCONF: overrides the base Pack +func (c *CHOPCONF_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(c.Toff&0xF) << 0 // Toff: 4 bits + registerValue |= uint32(c.HstrtTfd&0x7) << 4 // HstrtTfd: 3 bits + registerValue |= uint32(c.HendOffset&0xF) << 7 // HendOffset: 4 bits + if c.Tfd3 { + registerValue |= 1 << 11 // Tfd3: 1 bit + } + if c.Disfdcc { + registerValue |= 1 << 12 // Disfdcc: 1 bit + } + if c.Rndtf { + registerValue |= 1 << 13 // Rndtf: 1 bit + } + if c.Chm { + registerValue |= 1 << 14 // Chm: 1 bit + } + registerValue |= uint32(c.Tbl&0x3) << 15 // Tbl: 2 bits + if c.Vsense { + registerValue |= 1 << 17 // Vsense: 1 bit + } + if c.Vhighfs { + registerValue |= 1 << 18 // Vhighfs: 1 bit + } + if c.Vhighchm { + registerValue |= 1 << 19 // Vhighchm: 1 bit + } + registerValue |= uint32(c.Tpfd&0xF) << 20 // Tpfd: 4 bits + registerValue |= uint32(c.Mres&0xF) << 24 // Mres: 4 bits + if c.Intpol { + registerValue |= 1 << 28 // Intpol: 1 bit + } + if c.Dedge { + registerValue |= 1 << 29 // Dedge: 1 bit + } + if c.Diss2g { + registerValue |= 1 << 30 // Diss2g: 1 bit + } + if c.Diss2vs { + registerValue |= 1 << 31 // Diss2vs: 1 bit + } + + return registerValue +} + +// Unpack method for CHOPCONF: overrides the base Unpack +func (c *CHOPCONF_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + c.Toff = uint8((registerValue >> 0) & 0xF) // Extract 4 bits for Toff + c.HstrtTfd = uint8((registerValue >> 4) & 0x7) // Extract 3 bits for HstrtTfd + c.HendOffset = uint8((registerValue >> 7) & 0xF) // Extract 4 bits for HendOffset + c.Tfd3 = (registerValue & (1 << 11)) != 0 // Extract 1 bit for Tfd3 + c.Disfdcc = (registerValue & (1 << 12)) != 0 // Extract 1 bit for Disfdcc + c.Rndtf = (registerValue & (1 << 13)) != 0 // Extract 1 bit for Rndtf + c.Chm = (registerValue & (1 << 14)) != 0 // Extract 1 bit for Chm + c.Tbl = uint8((registerValue >> 15) & 0x3) // Extract 2 bits for Tbl + c.Vsense = (registerValue & (1 << 17)) != 0 // Extract 1 bit for Vsense + c.Vhighfs = (registerValue & (1 << 18)) != 0 // Extract 1 bit for Vhighfs + c.Vhighchm = (registerValue & (1 << 19)) != 0 // Extract 1 bit for Vhighchm + c.Tpfd = uint8((registerValue >> 20) & 0xF) // Extract 4 bits for Tpfd + c.Mres = uint8((registerValue >> 24) & 0xF) // Extract 4 bits for Mres + c.Intpol = (registerValue & (1 << 28)) != 0 // Extract 1 bit for Intpol + c.Dedge = (registerValue & (1 << 29)) != 0 // Extract 1 bit for Dedge + c.Diss2g = (registerValue & (1 << 30)) != 0 // Extract 1 bit for Diss2g + c.Diss2vs = (registerValue & (1 << 31)) != 0 // Extract 1 bit for Diss2vs +} + +// COOLCONF_Register struct to represent the COOLCONF register +type COOLCONF_Register struct { + Register + Semin uint8 // Minimum stallGuard2 value for smart current control (4 bits) + Seup uint8 // Current increment step width (2 bits) + Semax uint8 // stallGuard2 hysteresis value for smart current control (4 bits) + Sedn uint8 // Current decrement step speed (2 bits) + Seimin bool // Minimum current for smart current control (1 bit) + Sgt uint8 // stallGuard2 threshold value (7 bits) + Sfilt bool // Enable stallGuard2 filter (1 bit) +} + +// NewCOOLCONF creates a new COOLCONF register instance +func NewCOOLCONF() *COOLCONF_Register { + return &COOLCONF_Register{ + Register: Register{ + RegisterAddr: COOLCONF, + }, + } +} + +// Pack method for COOLCONF: overrides the base Pack +func (c *COOLCONF_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(c.Semin&0xF) << 0 // Semin: 4 bits + registerValue |= uint32(c.Seup&0x3) << 5 // Seup: 2 bits + registerValue |= uint32(c.Semax&0xF) << 8 // Semax: 4 bits + registerValue |= uint32(c.Sedn&0x3) << 13 // Sedn: 2 bits + if c.Seimin { + registerValue |= 1 << 15 // Seimin: 1 bit + } + registerValue |= uint32(c.Sgt&0x7F) << 16 // Sgt: 7 bits + if c.Sfilt { + registerValue |= 1 << 24 // Sfilt: 1 bit + } + + return registerValue +} + +// Unpack method for COOLCONF: overrides the base Unpack +func (c *COOLCONF_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + c.Semin = uint8((registerValue >> 0) & 0xF) // Extract 4 bits for Semin + c.Seup = uint8((registerValue >> 5) & 0x3) // Extract 2 bits for Seup + c.Semax = uint8((registerValue >> 8) & 0xF) // Extract 4 bits for Semax + c.Sedn = uint8((registerValue >> 13) & 0x3) // Extract 2 bits for Sedn + c.Seimin = (registerValue & (1 << 15)) != 0 // Extract 1 bit for Seimin + c.Sgt = uint8((registerValue >> 16) & 0x7F) // Extract 7 bits for Sgt + c.Sfilt = (registerValue & (1 << 24)) != 0 // Extract 1 bit for Sfilt +} + +// DCCTRL_Register struct to represent the DCCTRL register +type DCCTRL_Register struct { + Register + DcTime uint16 // Upper PWM on time limit for commutation (10 bits) + DcSg uint8 // Max. PWM on time for step loss detection using dcStep (8 bits) +} + +// NewDCCTRL creates a new DCCTRL register instance +func NewDCCTRL() *DCCTRL_Register { + return &DCCTRL_Register{ + Register: Register{ + RegisterAddr: DCCTRL, + }, + } +} + +// Pack method for DCCTRL: overrides the base Pack +func (d *DCCTRL_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(d.DcTime&0x3FF) << 0 // DcTime: 10 bits + registerValue |= uint32(d.DcSg&0xFF) << 16 // DcSg: 8 bits + + return registerValue +} + +// Unpack method for DCCTRL: overrides the base Unpack +func (d *DCCTRL_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + d.DcTime = uint16((registerValue >> 0) & 0x3FF) // Extract 10 bits for DcTime + d.DcSg = uint8((registerValue >> 16) & 0xFF) // Extract 8 bits for DcSg +} + +// DRV_STATUS_Register struct to represent the DRV_STATUS register +type DRV_STATUS_Register struct { + Register + SgResult uint16 // stallGuard2 result or motor temperature estimation in standstill (9 bits) + S2vsa bool // Short to supply indicator phase A + S2vsb bool // Short to supply indicator phase B + Stealth bool // stealthChop indicator + FsActive bool // Full step active indicator + CsActual uint8 // Actual motor current / smart energy current (5 bits) + StallGuard bool // stallGuard2 status + Ot bool // Overtemperature flag + Otpw bool // Overtemperature pre-warning flag + S2ga bool // Short to ground indicator phase A + S2gb bool // Short to ground indicator phase B + Ola bool // Open load indicator phase A + Olb bool // Open load indicator phase B + Stst bool // Standstill indicator +} + +// NewDRV_STATUS creates a new DRV_STATUS register instance +func NewDRV_STATUS() *DRV_STATUS_Register { + return &DRV_STATUS_Register{ + Register: Register{ + RegisterAddr: DRV_STATUS, + }, + } +} + +// Pack method for DRV_STATUS: overrides the base Pack +func (d *DRV_STATUS_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(d.SgResult&0x1FF) << 0 // SgResult: 9 bits + if d.S2vsa { + registerValue |= 1 << 12 // S2vsa: 1 bit + } + if d.S2vsb { + registerValue |= 1 << 13 // S2vsb: 1 bit + } + if d.Stealth { + registerValue |= 1 << 14 // Stealth: 1 bit + } + if d.FsActive { + registerValue |= 1 << 15 // FsActive: 1 bit + } + registerValue |= uint32(d.CsActual&0x1F) << 16 // CsActual: 5 bits + if d.StallGuard { + registerValue |= 1 << 24 // StallGuard: 1 bit + } + if d.Ot { + registerValue |= 1 << 25 // Ot: 1 bit + } + if d.Otpw { + registerValue |= 1 << 26 // Otpw: 1 bit + } + if d.S2ga { + registerValue |= 1 << 27 // S2ga: 1 bit + } + if d.S2gb { + registerValue |= 1 << 28 // S2gb: 1 bit + } + if d.Ola { + registerValue |= 1 << 29 // Ola: 1 bit + } + if d.Olb { + registerValue |= 1 << 30 // Olb: 1 bit + } + if d.Stst { + registerValue |= 1 << 31 // Stst: 1 bit + } + + return registerValue +} + +// Unpack method for DRV_STATUS: overrides the base Unpack +func (d *DRV_STATUS_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + d.SgResult = uint16((registerValue >> 0) & 0x1FF) // Extract 9 bits for SgResult + d.S2vsa = (registerValue & (1 << 12)) != 0 // Extract 1 bit for S2vsa + d.S2vsb = (registerValue & (1 << 13)) != 0 // Extract 1 bit for S2vsb + d.Stealth = (registerValue & (1 << 14)) != 0 // Extract 1 bit for Stealth + d.FsActive = (registerValue & (1 << 15)) != 0 // Extract 1 bit for FsActive + d.CsActual = uint8((registerValue >> 16) & 0x1F) // Extract 5 bits for CsActual + d.StallGuard = (registerValue & (1 << 24)) != 0 // Extract 1 bit for StallGuard + d.Ot = (registerValue & (1 << 25)) != 0 // Extract 1 bit for Ot + d.Otpw = (registerValue & (1 << 26)) != 0 // Extract 1 bit for Otpw + d.S2ga = (registerValue & (1 << 27)) != 0 // Extract 1 bit for S2ga + d.S2gb = (registerValue & (1 << 28)) != 0 // Extract 1 bit for S2gb + d.Ola = (registerValue & (1 << 29)) != 0 // Extract 1 bit for Ola + d.Olb = (registerValue & (1 << 30)) != 0 // Extract 1 bit for Olb + d.Stst = (registerValue & (1 << 31)) != 0 // Extract 1 bit for Stst +} + +// PWMCONF_Register struct to represent the PWMCONF register +type PWMCONF_Register struct { + Register + PwmOfs uint8 // User defined PWM amplitude offset (8 bits) + PwmGrad uint8 // User defined PWM amplitude gradient (8 bits) + PwmFreq uint8 // PWM frequency selection (2 bits) + PwmAutoscale bool // Enable PWM automatic amplitude scaling (1 bit) + PwmAutograd bool // PWM automatic gradient adaptation (1 bit) + Freewheel uint8 // Standstill option when motor current setting is zero (2 bits) + PwmReg uint8 // Regulation loop gradient (4 bits) + PwmLim uint8 // PWM automatic scale amplitude limit when switching on (4 bits) +} + +// NewPWMCONF creates a new PWMCONF register instance +func NewPWMCONF() *PWMCONF_Register { + return &PWMCONF_Register{ + Register: Register{ + RegisterAddr: PWMCONF, + }, + } +} + +// Pack method for PWMCONF: overrides the base Pack +func (p *PWMCONF_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(p.PwmOfs&0xFF) << 0 // PwmOfs: 8 bits + registerValue |= uint32(p.PwmGrad&0xFF) << 8 // PwmGrad: 8 bits + registerValue |= uint32(p.PwmFreq&0x3) << 16 // PwmFreq: 2 bits + if p.PwmAutoscale { + registerValue |= 1 << 18 // PwmAutoscale: 1 bit + } + if p.PwmAutograd { + registerValue |= 1 << 19 // PwmAutograd: 1 bit + } + registerValue |= uint32(p.Freewheel&0x3) << 20 // Freewheel: 2 bits + registerValue |= uint32(p.PwmReg&0xF) << 24 // PwmReg: 4 bits + registerValue |= uint32(p.PwmLim&0xF) << 28 // PwmLim: 4 bits + + return registerValue +} + +// Unpack method for PWMCONF: overrides the base Unpack +func (p *PWMCONF_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + p.PwmOfs = uint8((registerValue >> 0) & 0xFF) // Extract 8 bits for PwmOfs + p.PwmGrad = uint8((registerValue >> 8) & 0xFF) // Extract 8 bits for PwmGrad + p.PwmFreq = uint8((registerValue >> 16) & 0x3) // Extract 2 bits for PwmFreq + p.PwmAutoscale = (registerValue & (1 << 18)) != 0 // Extract 1 bit for PwmAutoscale + p.PwmAutograd = (registerValue & (1 << 19)) != 0 // Extract 1 bit for PwmAutograd + p.Freewheel = uint8((registerValue >> 20) & 0x3) // Extract 2 bits for Freewheel + p.PwmReg = uint8((registerValue >> 24) & 0xF) // Extract 4 bits for PwmReg + p.PwmLim = uint8((registerValue >> 28) & 0xF) // Extract 4 bits for PwmLim +} + +// PWM_SCALE_Register struct to represent the PWM_SCALE register +type PWM_SCALE_Register struct { + Register + PwmScaleSum uint8 // Actual PWM duty cycle (8 bits) + PwmScaleAuto uint16 // Result of the automatic amplitude regulation based on current measurement (9 bits) +} + +// NewPWM_SCALE creates a new PWM_SCALE register instance +func NewPWM_SCALE() *PWM_SCALE_Register { + return &PWM_SCALE_Register{ + Register: Register{ + RegisterAddr: PWM_SCALE, + }, + } +} + +// Pack method for PWM_SCALE: overrides the base Pack +func (p *PWM_SCALE_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(p.PwmScaleSum&0xFF) << 0 // PwmScaleSum: 8 bits + registerValue |= uint32(p.PwmScaleAuto&0x1FF) << 16 // PwmScaleAuto: 9 bits + + return registerValue +} + +// Unpack method for PWM_SCALE: overrides the base Unpack +func (p *PWM_SCALE_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + p.PwmScaleSum = uint8((registerValue >> 0) & 0xFF) // Extract 8 bits for PwmScaleSum + p.PwmScaleAuto = uint16((registerValue >> 16) & 0x1FF) // Extract 9 bits for PwmScaleAuto +} + +// PWM_AUTO_Register struct to represent the PWM_AUTO register +type PWM_AUTO_Register struct { + Register + PwmOfsAuto uint8 // Automatically determined offset value (8 bits) + PwmGradAuto uint8 // Automatically determined gradient value (8 bits) +} + +// NewPWM_AUTO creates a new PWM_AUTO register instance +func NewPWM_AUTO() *PWM_AUTO_Register { + return &PWM_AUTO_Register{ + Register: Register{ + RegisterAddr: PWM_AUTO, + }, + } +} + +// Pack method for PWM_AUTO: overrides the base Pack +func (p *PWM_AUTO_Register) Pack() uint32 { + var registerValue uint32 + + // Pack each field using bitwise operations + registerValue |= uint32(p.PwmOfsAuto&0xFF) << 0 // PwmOfsAuto: 8 bits + registerValue |= uint32(p.PwmGradAuto&0xFF) << 16 // PwmGradAuto: 8 bits + + return registerValue +} + +// Unpack method for PWM_AUTO: overrides the base Unpack +func (p *PWM_AUTO_Register) Unpack(registerValue uint32) { + // Unpack each field using bitwise operations + p.PwmOfsAuto = uint8((registerValue >> 0) & 0xFF) // Extract 8 bits for PwmOfsAuto + p.PwmGradAuto = uint8((registerValue >> 16) & 0xFF) // Extract 8 bits for PwmGradAuto +} + +// MSCNT_Register struct to represent the MSCNT register (10-bit value) +type MSCNT_Register struct { + Register + Value uint16 // Microstep counter value (10 bits) +} + +// NewMSCNT creates a new MSCNT register instance +func NewMSCNT() *MSCNT_Register { + return &MSCNT_Register{ + Register: Register{ + RegisterAddr: MSCNT, + }, + } +} + +// Pack method for MSCNT: combines the 10-bit value into a 16-bit value +func (m *MSCNT_Register) Pack() uint16 { + return m.Value & 0x3FF // Mask the value to ensure it is within the 10-bit range (0-1023) +} + +// Unpack method for MSCNT: extracts the 10-bit value from a 16-bit value +func (m *MSCNT_Register) Unpack(registerValue uint16) { + m.Value = registerValue & 0x3FF // Mask to extract the 10-bit value (0-1023) +} + +// VDCMIN_Register struct for VDCMIN register (23 bits) +type VDCMIN_Register struct { + Register + Value uint32 // 23-bit value +} + +// NewVDCMIN creates a new VDCMIN register instance +func NewVDCMIN() *VDCMIN_Register { + return &VDCMIN_Register{ + Register: Register{ + RegisterAddr: VDCMIN, + }, + } +} + +// Pack method for VDCMIN: packs the 23-bit value into a 32-bit value +func (v *VDCMIN_Register) Pack() uint32 { + return v.Value & 0x7FFFFF // Mask to 23 bits +} + +// Unpack method for VDCMIN: unpacks the 23-bit value from a 32-bit value +func (v *VDCMIN_Register) Unpack(registerValue uint32) { + v.Value = registerValue & 0x7FFFFF // Mask to 23 bits +} + +// XLATCH_Register struct for XLATCH register (32 bits) +type XLATCH_Register struct { + Register + Value uint32 // 32-bit value +} + +// NewXLATCH creates a new XLATCH register instance +func NewXLATCH() *XLATCH_Register { + return &XLATCH_Register{ + Register: Register{ + RegisterAddr: XLATCH, + }, + } +} + +// Pack method for XLATCH: directly returns the 32-bit value +func (x *XLATCH_Register) Pack() uint32 { + return x.Value // No mask needed, since it's 32 bits +} + +// Unpack method for XLATCH: unpacks the 32-bit value from a 32-bit register +func (x *XLATCH_Register) Unpack(registerValue uint32) { + x.Value = registerValue // Direct assignment since it's 32 bits +} + +// RAMPMODE_Register struct for RAMPMODE register (2 bits) +type RAMPMODE_Register struct { + Register + mode RampMode // Mode is now an enum-like type + comm RegisterComm + driverIndex uint8 +} +type RampMode uint8 + +const ( + PositioningMode RampMode = iota // 0 + VelocityPositiveMode // 1 + VelocityNegativeMode // 2 + HoldMode // 3 +) + +func NewRAMPMODE(comm RegisterComm, driverIndex uint8) *RAMPMODE_Register { + return &RAMPMODE_Register{ + Register: Register{ + RegisterAddr: RAMPMODE, + }, + driverIndex: driverIndex, + comm: comm, + mode: PositioningMode, // Default to Positioning Mode + } +} + +// SetMode sets the mode of the RAMPMODE register +func (r *RAMPMODE_Register) SetMode(mode RampMode) error { + r.mode = mode + registerValue := r.Pack() + return r.comm.WriteRegister(r.RegisterAddr, uint32(registerValue), r.driverIndex) +} + +// GetMode returns the current mode of the RAMPMODE register +func (r *RAMPMODE_Register) GetMode() (RampMode, error) { + registerValue, err := r.comm.ReadRegister(r.RegisterAddr, r.driverIndex) + if err != nil { + return 0, err //Defaults to Postioning Mode + } + + // Unpack the register value to get the mode + r.Unpack(uint8(registerValue)) + return r.mode, nil + +} + +// Pack method for RAMPMODE: packs the mode value into a single byte (now using enums) +func (r *RAMPMODE_Register) Pack() uint8 { + return uint8(r.mode) // Simply cast the mode to uint8 +} + +// Unpack method for RAMPMODE: unpacks the mode value from a byte +func (r *RAMPMODE_Register) Unpack(registerValue uint8) { + r.mode = RampMode(registerValue & 0x03) // Mask to 2 bits +} + +// String method to display the mode as a string (useful for logging or debugging) +func (r RampMode) String() string { + switch r { + case PositioningMode: + return "Positioning Mode" + case VelocityPositiveMode: + return "Velocity Mode (Positive VMAX)" + case VelocityNegativeMode: + return "Velocity Mode (Negative VMAX)" + case HoldMode: + return "Hold Mode" + default: + return "Unknown Mode" + } +} + +// XACTUAL_Register struct for XACTUAL register (32 bits) +type XACTUAL_Register struct { + Register + Value uint32 // 32-bit value +} + +// NewXACTUAL creates a new XACTUAL register instance +func NewXACTUAL() *XACTUAL_Register { + return &XACTUAL_Register{ + Register: Register{ + RegisterAddr: XACTUAL, + }, + } +} + +// Pack method for XACTUAL: returns the 32-bit value +func (x *XACTUAL_Register) Pack() uint32 { + return x.Value // 32 bits, no masking needed +} + +// Unpack method for XACTUAL: unpacks the 32-bit value +func (x *XACTUAL_Register) Unpack(registerValue uint32) { + x.Value = registerValue // Direct assignment since it's 32 bits +} + +// VACTUAL_Register struct for VACTUAL register (24 bits) +type VACTUAL_Register struct { + Register + Value uint32 // 24-bit value (stored in a 32-bit field) +} + +// NewVACTUAL creates a new VACTUAL register instance +func NewVACTUAL() *VACTUAL_Register { + return &VACTUAL_Register{ + Register: Register{ + RegisterAddr: VACTUAL, + }, + } +} + +// Pack method for VACTUAL: packs the 24-bit value into a 32-bit value +func (v *VACTUAL_Register) Pack() uint32 { + return v.Value & 0xFFFFFF // Mask to 24 bits +} + +// Unpack method for VACTUAL: unpacks the 24-bit value from a 32-bit value +func (v *VACTUAL_Register) Unpack(registerValue uint32) { + v.Value = registerValue & 0xFFFFFF // Mask to 24 bits +} + +// VSTART_Register struct for VSTART register (18 bits) +type VSTART_Register struct { + Register + Value uint32 // 18-bit value +} + +// NewVSTART creates a new VSTART register instance +func NewVSTART() *VSTART_Register { + return &VSTART_Register{ + Register: Register{ + RegisterAddr: VSTART, + }, + } +} + +// Pack method for VSTART: packs the 18-bit value into a 16-bit value +func (v *VSTART_Register) Pack() uint32 { + return v.Value & 0x3FFFF // Mask to 18 bits +} + +// Unpack method for VSTART: unpacks the 18-bit value from a 16-bit value +func (v *VSTART_Register) Unpack(registerValue uint32) { + v.Value = registerValue & 0x3FFFF // Mask to 18 bits +} + +// A1_Register struct for A1 register (16 bits) +type A1_Register struct { + Register + Value uint16 // 16-bit value +} + +// NewA1 creates a new A1 register instance +func NewA1() *A1_Register { + return &A1_Register{ + Register: Register{ + RegisterAddr: A_1, + }, + } +} + +// Pack method for A1: returns the 16-bit value +func (a *A1_Register) Pack() uint16 { + return a.Value // 16 bits, no masking needed +} + +// Unpack method for A1: unpacks the 16-bit value +func (a *A1_Register) Unpack(registerValue uint16) { + a.Value = registerValue // Direct assignment since it's 16 bits +} + +// V1_Register struct for V1 register (20 bits) +type V1_Register struct { + Register + Value uint32 // 20-bit value (stored in a 32-bit field) +} + +// NewV1 creates a new V1 register instance +func NewV1() *V1_Register { + return &V1_Register{ + Register: Register{ + RegisterAddr: V_1, + }, + } +} + +// Pack method for V1: packs the 20-bit value into a 32-bit value +func (v *V1_Register) Pack() uint32 { + return v.Value & 0xFFFFF // Mask to 20 bits +} + +// Unpack method for V1: unpacks the 20-bit value from a 32-bit value +func (v *V1_Register) Unpack(registerValue uint32) { + v.Value = registerValue & 0xFFFFF // Mask to 20 bits +} + +// AMAX_Register struct for AMAX register (16 bits) +type AMAX_Register struct { + Register + Value uint16 // 16-bit value +} + +// NewAMAX creates a new AMAX register instance +func NewAMAX() *AMAX_Register { + return &AMAX_Register{ + Register: Register{ + RegisterAddr: AMAX, + }, + } +} + +// Pack method for AMAX: returns the 16-bit value +func (a *AMAX_Register) Pack() uint16 { + return a.Value // 16 bits, no masking needed +} + +// Unpack method for AMAX: unpacks the 16-bit value +func (a *AMAX_Register) Unpack(registerValue uint16) { + a.Value = registerValue // Direct assignment since it's 16 bits +} + +// VMAX_Register struct for VMAX register (23 bits) +type VMAX_Register struct { + Register + Value uint32 // 23-bit value (stored in a 32-bit field) +} + +// NewVMAX creates a new VMAX register instance +func NewVMAX() *VMAX_Register { + return &VMAX_Register{ + Register: Register{ + RegisterAddr: VMAX, + }, + } +} + +// Pack method for VMAX: packs the 23-bit value into a 32-bit value +func (v *VMAX_Register) Pack() uint32 { + return v.Value & 0x7FFFFF // Mask to 23 bits +} + +// Unpack method for VMAX: unpacks the 23-bit value from a 32-bit value +func (v *VMAX_Register) Unpack(registerValue uint32) { + v.Value = registerValue & 0x7FFFFF // Mask to 23 bits +} + +// D1_Register struct for D1 register (16 bits) +type D1_Register struct { + Register + Value uint16 // 16-bit value +} + +// NewD1 creates a new D1 register instance +func NewD1() *D1_Register { + return &D1_Register{ + Register: Register{ + RegisterAddr: D_1, + }, + } +} + +// Pack method for D1: returns the 16-bit value +func (d *D1_Register) Pack() uint16 { + return d.Value // 16 bits, no masking needed +} + +// Unpack method for D1: unpacks the 16-bit value +func (d *D1_Register) Unpack(registerValue uint16) { + d.Value = registerValue // Direct assignment since it's 16 bits +} + +// VSTOP_Register struct for VSTOP register (18 bits) +type VSTOP_Register struct { + Register + Value uint32 // 18-bit value (stored in a 32-bit field) +} + +// NewVSTOP creates a new VSTOP register instance +func NewVSTOP() *VSTOP_Register { + return &VSTOP_Register{ + Register: Register{ + RegisterAddr: VSTOP, + }, + } +} + +// Pack method for VSTOP: packs the 18-bit value into a 32-bit value +func (v *VSTOP_Register) Pack() uint32 { + return v.Value & 0x3FFFF // Mask to 18 bits +} + +// Unpack method for VSTOP: unpacks the 18-bit value from a 32-bit value +func (v *VSTOP_Register) Unpack(registerValue uint32) { + v.Value = registerValue & 0x3FFFF // Mask to 18 bits +} + +// TZEROWAIT_Register struct for TZEROWAIT register (16 bits) +type TZEROWAIT_Register struct { + Register + Value uint16 // 16-bit value +} + +// NewTZEROWAIT creates a new TZEROWAIT register instance +func NewTZEROWAIT() *TZEROWAIT_Register { + return &TZEROWAIT_Register{ + Register: Register{ + RegisterAddr: TZEROWAIT, + }, + } +} + +// Pack method for TZEROWAIT: returns the 16-bit value +func (t *TZEROWAIT_Register) Pack() uint16 { + return t.Value // 16 bits, no masking needed +} + +// Unpack method for TZEROWAIT: unpacks the 16-bit value +func (t *TZEROWAIT_Register) Unpack(registerValue uint16) { + t.Value = registerValue // Direct assignment since it's 16 bits +} + +// XTARGET_Register struct for XTARGET register (32 bits) +type XTARGET_Register struct { + Register + Value uint32 // 32-bit value +} + +// NewXTARGET creates a new XTARGET register instance +func NewXTARGET() *XTARGET_Register { + return &XTARGET_Register{ + Register: Register{ + RegisterAddr: XTARGET, + }, + } +} + +// Pack method for XTARGET: returns the 32-bit value +func (x *XTARGET_Register) Pack() uint32 { + return x.Value // 32 bits, no masking needed +} + +// Unpack method for XTARGET: unpacks the 32-bit value +func (x *XTARGET_Register) Unpack(registerValue uint32) { + x.Value = registerValue // Direct assignment since it's 32 bits +} + +// X_COMPARE_Register struct for X_COMPARE register (32 bits) +type X_COMPARE_Register struct { + Register + Value uint32 // 32-bit value for position comparison +} + +// NewX_COMPARE creates a new X_COMPARE register instance +func NewX_COMPARE() *X_COMPARE_Register { + return &X_COMPARE_Register{ + Register: Register{ + RegisterAddr: X_COMPARE, + }, + } +} + +// Pack method for X_COMPARE: returns the 32-bit value +func (x *X_COMPARE_Register) Pack() uint32 { + return x.Value // 32 bits, no masking needed +} + +// Unpack method for X_COMPARE: unpacks the 32-bit value +func (x *X_COMPARE_Register) Unpack(registerValue uint32) { + x.Value = registerValue // Direct assignment since it's 32 bits +} + +// GLOBAL_SCALER_Register struct for GLOBAL SCALER register (8 bits) +type GLOBAL_SCALER_Register struct { + Register + Value uint8 // 8-bit value for global motor current scaling +} + +// NewGLOBAL_SCALER creates a new GLOBAL_SCALER register instance +func NewGLOBAL_SCALER() *GLOBAL_SCALER_Register { + return &GLOBAL_SCALER_Register{ + Register: Register{ + RegisterAddr: GLOBAL_SCALER, + }, + } +} + +// Pack method for GLOBAL_SCALER: returns the 8-bit value +func (g *GLOBAL_SCALER_Register) Pack() uint8 { + return g.Value // 8 bits, no masking needed +} + +// Unpack method for GLOBAL_SCALER: unpacks the 8-bit value +func (g *GLOBAL_SCALER_Register) Unpack(registerValue uint8) { + g.Value = registerValue // Direct assignment since it's 8 bits +} + +// TPOWERDOWN_Register struct for TPOWERDOWN register (8 bits) +type TPOWERDOWN_Register struct { + Register + Value uint8 // 8-bit value for time delay after standstill +} + +// NewTPOWERDOWN creates a new TPOWERDOWN register instance +func NewTPOWERDOWN() *TPOWERDOWN_Register { + return &TPOWERDOWN_Register{ + Register: Register{ + RegisterAddr: TPOWERDOWN, + }, + } +} + +// Pack method for TPOWERDOWN: returns the 8-bit value +func (t *TPOWERDOWN_Register) Pack() uint8 { + return t.Value // 8 bits, no masking needed +} + +// Unpack method for TPOWERDOWN: unpacks the 8-bit value +func (t *TPOWERDOWN_Register) Unpack(registerValue uint8) { + t.Value = registerValue // Direct assignment since it's 8 bits +} + +// PWMTHRS_Register struct for PWMTHRS register (20 bits) +type PWMTHRS_Register struct { + Register + Value uint32 // 20-bit value (stored in a 32-bit field) +} + +// NewPWMTHRS creates a new PWMTHRS register instance +func NewPWMTHRS() *PWMTHRS_Register { + return &PWMTHRS_Register{ + Register: Register{ + RegisterAddr: TPWMTHRS, + }, + } +} + +// Pack method for PWMTHRS: packs the 20-bit value into a 32-bit value +func (p *PWMTHRS_Register) Pack() uint32 { + return p.Value & 0xFFFFF // Mask to 20 bits +} + +// Unpack method for PWMTHRS: unpacks the 20-bit value from a 32-bit value +func (p *PWMTHRS_Register) Unpack(registerValue uint32) { + p.Value = registerValue & 0xFFFFF // Mask to 20 bits +} + +// TCOOLTHRS_Register struct for TCOOLTHRS register (20 bits) +type TCOOLTHRS_Register struct { + Register + Value uint32 // 20-bit value (stored in a 32-bit field) +} + +// NewTCOOLTHRS creates a new TCOOLTHRS register instance +func NewTCOOLTHRS() *TCOOLTHRS_Register { + return &TCOOLTHRS_Register{ + Register: Register{ + RegisterAddr: TCOOLTHRS, + }, + } +} + +// Pack method for TCOOLTHRS: packs the 20-bit value into a 32-bit value +func (t *TCOOLTHRS_Register) Pack() uint32 { + return t.Value & 0xFFFFF // Mask to 20 bits +} + +// Unpack method for TCOOLTHRS: unpacks the 20-bit value from a 32-bit value +func (t *TCOOLTHRS_Register) Unpack(registerValue uint32) { + t.Value = registerValue & 0xFFFFF // Mask to 20 bits +} + +// THIGH_Register struct for THIGH register (16 bits) +type THIGH_Register struct { + Register + Value uint16 // 16-bit value +} + +// NewTHIGH creates a new THIGH register instance +func NewTHIGH() *THIGH_Register { + return &THIGH_Register{ + Register: Register{ + RegisterAddr: THIGH, + }, + } +} + +// Pack method for THIGH: returns the 16-bit value +func (t *THIGH_Register) Pack() uint16 { + return t.Value // 16 bits, no masking needed +} + +// Unpack method for THIGH: unpacks the 16-bit value +func (t *THIGH_Register) Unpack(registerValue uint16) { + t.Value = registerValue // Direct assignment since it's 16 bits +} + +// DMAX_Register struct for DMAX register (16 bits) +type DMAX_Register struct { + Register + Value uint16 // 16-bit value for deceleration between VMAX and VSTOP +} + +// NewDMAX creates a new DMAX register instance +func NewDMAX() *DMAX_Register { + return &DMAX_Register{ + Register: Register{ + RegisterAddr: DMAX, + }, + } +} + +// Pack method for DMAX: returns the 16-bit value +func (d *DMAX_Register) Pack() uint16 { + return d.Value // 16 bits, no masking needed +} + +// Unpack method for DMAX: unpacks the 16-bit value +func (d *DMAX_Register) Unpack(registerValue uint16) { + d.Value = registerValue // Direct assignment since it's 16 bits +} + +// TSTEP_Register struct for TSTEP register (20 bits) +type TSTEP_Register struct { + Register + Value uint32 // 20-bit value (stored in a 32-bit field) +} + +// NewTSTEP creates a new TSTEP register instance +func NewTSTEP() *TSTEP_Register { + return &TSTEP_Register{ + Register: Register{ + RegisterAddr: TSTEP, + }, + } +} + +// Pack method for TSTEP: packs the 20-bit value into a 32-bit value +func (t *TSTEP_Register) Pack() uint32 { + return t.Value & 0xFFFFF // Mask to 20 bits +} + +// Unpack method for TSTEP: unpacks the 20-bit value from a 32-bit value +func (t *TSTEP_Register) Unpack(registerValue uint32) { + t.Value = registerValue & 0xFFFFF // Mask to 20 bits +} + +// X_ENC_Register struct for X_ENC register (32 bits) +type X_ENC_Register struct { + Register + Value int32 // 32-bit signed value for actual encoder position +} + +// NewX_ENC creates a new X_ENC register instance +func NewX_ENC() *X_ENC_Register { + return &X_ENC_Register{ + Register: Register{ + RegisterAddr: X_ENC, + }, + } +} + +// Pack method for X_ENC: returns the 32-bit signed value +func (x *X_ENC_Register) Pack() int32 { + return x.Value // 32 bits, no masking needed for signed integer +} + +// Unpack method for X_ENC: unpacks the 32-bit signed value +func (x *X_ENC_Register) Unpack(registerValue int32) { + x.Value = registerValue // Direct assignment since it's 32 bits signed integer +} + +// ENC_CONST_Register struct for ENC_CONST register (32 bits) +type ENC_CONST_Register struct { + Register + Value int32 // 32-bit signed accumulation constant +} + +// NewENC_CONST creates a new ENC_CONST register instance +func NewENC_CONST() *ENC_CONST_Register { + return &ENC_CONST_Register{ + Register: Register{ + RegisterAddr: ENC_CONST, + }, + } +} + +// Pack method for ENC_CONST: returns the 32-bit signed accumulation constant +func (e *ENC_CONST_Register) Pack() int32 { + return e.Value // 32 bits, no masking needed for signed integer +} + +// Unpack method for ENC_CONST: unpacks the 32-bit signed accumulation constant +func (e *ENC_CONST_Register) Unpack(registerValue int32) { + e.Value = registerValue // Direct assignment since it's 32 bits signed integer +} + +// ENC_LATCH_Register struct for ENC_LATCH register (32 bits) +type ENC_LATCH_Register struct { + Register + Value int32 // 32-bit signed value for encoder position latched on N event +} + +// NewENC_LATCH creates a new ENC_LATCH register instance +func NewENC_LATCH() *ENC_LATCH_Register { + return &ENC_LATCH_Register{ + Register: Register{ + RegisterAddr: ENC_LATCH, + }, + } +} + +// Pack method for ENC_LATCH: returns the 32-bit signed value +func (e *ENC_LATCH_Register) Pack() int32 { + return e.Value // 32 bits, no masking needed for signed integer +} + +// Unpack method for ENC_LATCH: unpacks the 32-bit signed value +func (e *ENC_LATCH_Register) Unpack(registerValue int32) { + e.Value = registerValue // Direct assignment since it's 32 bits signed integer +} + +// ENC_DEVIATION_Register struct for ENC_DEVIATION register (20 bits) +type ENC_DEVIATION_Register struct { + Register + Value uint32 // 20-bit unsigned value for maximum deviation +} + +// NewENC_DEVIATION creates a new ENC_DEVIATION register instance +func NewENC_DEVIATION() *ENC_DEVIATION_Register { + return &ENC_DEVIATION_Register{ + Register: Register{ + RegisterAddr: ENC_DEVIATION, + }, + } +} + +// Pack method for ENC_DEVIATION: packs the 20-bit value into a 32-bit value +func (e *ENC_DEVIATION_Register) Pack() uint32 { + return e.Value & 0xFFFFF // Mask to 20 bits +} + +// Unpack method for ENC_DEVIATION: unpacks the 20-bit value from a 32-bit value +func (e *ENC_DEVIATION_Register) Unpack(registerValue uint32) { + e.Value = registerValue & 0xFFFFF // Mask to 20 bits +} + +// MSCURACT_Register struct for MSCURACT register (18 bits) +type MSCURACT_Register struct { + Register + CUR_B int16 // 9-bit signed value for motor phase B (sine wave) + CUR_A int16 // 9-bit signed value for motor phase A (cosine wave) +} + +// NewMSCURACT creates a new MSCURACT register instance +func NewMSCURACT() *MSCURACT_Register { + return &MSCURACT_Register{ + Register: Register{ + RegisterAddr: MSCURACT, + }, + } +} + +// Pack method for MSCURACT: packs the 9-bit signed values for CUR_B and CUR_A into a 32-bit value +func (m *MSCURACT_Register) Pack() uint32 { + return uint32(m.CUR_A<<16 | m.CUR_B) // Combine CUR_A and CUR_B into a 32-bit value +} + +// Unpack method for MSCURACT: unpacks the 32-bit value into CUR_B and CUR_A +func (m *MSCURACT_Register) Unpack(registerValue uint32) { + m.CUR_B = int16(registerValue & 0x1FF) // Mask to get the lower 9 bits for CUR_B + m.CUR_A = int16((registerValue >> 16) & 0x1FF) // Mask to get the next 9 bits for CUR_A +} + +// LOST_STEPS_Register struct for LOST_STEPS register (20 bits) +type LOST_STEPS_Register struct { + Register + Value uint32 // 20-bit unsigned value for lost steps count +} + +// NewLOST_STEPS creates a new LOST_STEPS register instance +func NewLOST_STEPS() *LOST_STEPS_Register { + return &LOST_STEPS_Register{ + Register: Register{ + RegisterAddr: LOST_STEPS, + }, + } +} + +// Pack method for LOST_STEPS: returns the 20-bit value +func (l *LOST_STEPS_Register) Pack() uint32 { + return l.Value & 0xFFFFF // Mask to 20 bits +} + +// Unpack method for LOST_STEPS: unpacks the 20-bit value from a 32-bit value +func (l *LOST_STEPS_Register) Unpack(registerValue uint32) { + l.Value = registerValue & 0xFFFFF // Mask to 20 bits +} + +// MSLUTSEL_Register struct for MSLUTSEL register (32 bits) +type MSLUTSEL_Register struct { + Register + X3 uint8 // 3-bit value for LUT segment 3 start + X2 uint8 // 3-bit value for LUT segment 2 start + X1 uint8 // 3-bit value for LUT segment 1 start + W3 uint8 // 2-bit value for LUT width control W3 + W2 uint8 // 2-bit value for LUT width control W2 + W1 uint8 // 2-bit value for LUT width control W1 + W0 uint8 // 2-bit value for LUT width control W0 +} + +// NewMSLUTSEL creates a new MSLUTSEL register instance +func NewMSLUTSEL() *MSLUTSEL_Register { + return &MSLUTSEL_Register{ + Register: Register{ + RegisterAddr: MSLUTSEL, + }, + } +} + +// Pack method for MSLUTSEL: combines all the fields into a 32-bit value +func (m *MSLUTSEL_Register) Pack() uint32 { + return uint32(m.X3<<27 | m.X2<<24 | m.X1<<21 | m.W3<<18 | m.W2<<16 | m.W1<<14 | m.W0<<12) // Combine fields into a 32-bit value +} + +// Unpack method for MSLUTSEL: unpacks the 32-bit value into individual fields +func (m *MSLUTSEL_Register) Unpack(registerValue uint32) { + m.X3 = uint8((registerValue >> 27) & 0x07) // Extract the 3 bits for X3 + m.X2 = uint8((registerValue >> 24) & 0x07) // Extract the 3 bits for X2 + m.X1 = uint8((registerValue >> 21) & 0x07) // Extract the 3 bits for X1 + m.W3 = uint8((registerValue >> 18) & 0x03) // Extract the 2 bits for W3 + m.W2 = uint8((registerValue >> 16) & 0x03) // Extract the 2 bits for W2 + m.W1 = uint8((registerValue >> 14) & 0x03) // Extract the 2 bits for W1 + m.W0 = uint8((registerValue >> 12) & 0x03) // Extract the 2 bits for W0 +} + +// MSLUT_Register struct for MSLUT register (32 bits) +type MSLUT_Register struct { + Register + Value uint32 // 32-bit value for microstep table entry +} + +// NewMSLUT creates a new MSLUT register instance +func NewMSLUT() *MSLUT_Register { + return &MSLUT_Register{ + Register: Register{ + RegisterAddr: MSLUT0, + }, + } +} + +// Pack method for MSLUT: returns the 32-bit value for the microstep entry +func (m *MSLUT_Register) Pack() uint32 { + return m.Value // 32 bits, no masking needed +} + +// Unpack method for MSLUT: unpacks the 32-bit value into the microstep entry +func (m *MSLUT_Register) Unpack(registerValue uint32) { + m.Value = registerValue // Direct assignment since it's 32 bits +} + +// MSLUTSTART_Register struct for MSLUTSTART register (16 bits) +type MSLUTSTART_Register struct { + Register + START_SIN int8 // 8-bit signed value for the absolute current at microstep entry 0 + START_SIN90 int8 // 8-bit signed value for the absolute current at microstep entry 256 +} + +// NewMSLUTSTART creates a new MSLUTSTART register instance +func NewMSLUTSTART() *MSLUTSTART_Register { + return &MSLUTSTART_Register{ + Register: Register{ + RegisterAddr: MSLUTSTART, + }, + } +} + +// Pack method for MSLUTSTART: combines START_SIN and START_SIN90 into a 16-bit value +func (m *MSLUTSTART_Register) Pack() uint16 { + return uint16(m.START_SIN) | (uint16(m.START_SIN90) << 8) // Combine the 8-bit values into a 16-bit value +} + +// Unpack method for MSLUTSTART: unpacks the 16-bit value into START_SIN and START_SIN90 +func (m *MSLUTSTART_Register) Unpack(registerValue uint16) { + m.START_SIN = int8(registerValue & 0xFF) // Extract the lower 8 bits for START_SIN + m.START_SIN90 = int8((registerValue >> 8) & 0xFF) // Extract the upper 8 bits for START_SIN90 +} + +// Function to calculate the sine wave values for the microstep table +func calculateSineWaveTable() []int { + // Create a slice to store the sine wave table + table := make([]int, 256) + + // Loop through each table index (i) + for i := 0; i < 256; i++ { + // Calculate the sine value and scale it by 248 + sineValue := 248 * math.Sin(2*math.Pi*float32(i)/1024) + + // Round the result and subtract 1 + roundedValue := int(math.Round(sineValue)) - 1 + + // Store the value in the table + table[i] = roundedValue + } + + return table +} diff --git a/tmc5160/spicomm.go b/tmc5160/spicomm.go new file mode 100644 index 0000000..fb3a46f --- /dev/null +++ b/tmc5160/spicomm.go @@ -0,0 +1,134 @@ +//go:build tinygo + +package tmc5160 + +import ( + "machine" + "time" +) + +// CustomError is a lightweight error type used for TinyGo compatibility. +type CustomError string + +func (e CustomError) Error() string { + return string(e) +} + +// SPIComm implements RegisterComm for SPI-based communication +type SPIComm struct { + spi *machine.SPI + CsPins map[uint8]machine.Pin // Map to store CS pin for each Driver by its address +} + +// NewSPIComm creates a new SPIComm instance. +func NewSPIComm(spi *machine.SPI, csPins map[uint8]machine.Pin) *SPIComm { + return &SPIComm{ + spi: spi, + CsPins: csPins, + } +} + +// Setup initializes the SPI communication with the Driver and configures all CS pins. +func (comm *SPIComm) Setup() error { + // Check if SPI is initialized + if comm.spi == nil { + return CustomError("SPI not initialized") + } + + // Configure all CS pins (make them output and set them high) + for _, csPin := range comm.CsPins { + csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + csPin.High() // Set all CS pins high initially + } + + // Configure the SPI interface with the desired settings + err := comm.spi.Configure(machine.SPIConfig{ + LSBFirst: false, + Mode: 3, + }) + if err != nil { + return CustomError("Failed to configure SPI") + } + + return nil +} + +// WriteRegister sends a register write command to the TMC5160. +func (comm *SPIComm) WriteRegister(register uint8, value uint32, driverAddress uint8) error { + // Assert the chip select pin (set CS low to start communication) + csPin, exists := comm.CsPins[driverAddress] + if !exists { + return CustomError("Invalid driver address") + } + csPin.Low() + + // Set the register address with WRITE_ACCESS (0x80) + addressWithWriteAccess := register | 0x80 + + // Send the address and the data to write (split into 4 bytes) + _, err := spiTransfer40(comm.spi, addressWithWriteAccess, value) + if err != nil { + csPin.High() + return CustomError("Failed to write register") + } + + // Deassert the chip select pin (set CS high to end communication) + csPin.High() + + return nil +} + +// ReadRegister sends a register read command to the TMC5160. +func (comm *SPIComm) ReadRegister(register uint8, driverAddress uint8) (uint32, error) { + // Assert the chip select pin (set CS low to start communication) + csPin, exists := comm.CsPins[driverAddress] + if !exists { + return 0, CustomError("Invalid driver address") + } + csPin.Low() + + // Step 1: Send a dummy write operation to begin the read sequence + _, err := spiTransfer40(comm.spi, register, 0x00) // Send dummy data + if err != nil { + csPin.High() + return 0, CustomError("Failed to send dummy write") + } + csPin.High() + time.Sleep(176 * time.Nanosecond) + csPin.Low() + // Step 2: Send the register read request again to get the actual value + response, err := spiTransfer40(comm.spi, register, 0x00) // Send again to get actual register data + if err != nil { + csPin.High() + return 0, CustomError("Failed to read register") + } + + // Deassert the chip select pin (set CS high to end communication) + csPin.High() + + return response, nil +} + +func spiTransfer40(spi *machine.SPI, register uint8, txData uint32) (uint32, error) { + // Prepare the 5-byte buffer for transmission (1 byte address + 4 bytes data) + tx := []byte{ + register, // Address byte + byte(txData >> 24), // Upper 8 bits of data + byte(txData >> 16), // Middle 8 bits of data + byte(txData >> 8), // Next 8 bits of data + byte(txData), // Lower 8 bits of data + } + + rx := make([]byte, 5) + + // Perform the SPI transaction + err := spi.Tx(tx, rx) + if err != nil { + return 0, err + } + + // Combine the received bytes into a 32-bit response, ignore the address byte + rxData := uint32(rx[1])<<24 | uint32(rx[2])<<16 | uint32(rx[3])<<8 | uint32(rx[4]) + + return rxData, nil +} diff --git a/tmc5160/stepper.go b/tmc5160/stepper.go new file mode 100644 index 0000000..62e1bde --- /dev/null +++ b/tmc5160/stepper.go @@ -0,0 +1,105 @@ +package tmc5160 + +const maxVMAX = 8388096 + +// PowerStageParameters represents the power stage parameters +type PowerStageParameters struct { + drvStrength uint8 + bbmTime uint8 + bbmClks uint8 +} + +// MotorParameters represents the motor parameters +type MotorParameters struct { + globalScaler uint16 + ihold uint8 + irun uint8 + iholddelay uint8 + pwmGradInitial uint16 + pwmOfsInitial uint16 + freewheeling uint8 +} + +// MotorDirection defines motor direction constants +type MotorDirection uint8 + +const ( + Clockwise MotorDirection = iota + CounterClockwise +) + +const ( + // Common stepper motor angles + StepAngle_1_8 = 1.8 + StepAngle_0_9 = 0.9 + StepAngle_0_72 = 0.72 + StepAngle_1_2 = 1.2 + StepAngle_0_48 = 0.48 + + // Common microstepping options + Step_1 uint8 = 1 + Step_2 uint8 = 2 + Step_4 uint8 = 4 + Step_8 uint8 = 8 + Step_16 uint8 = 16 + Step_32 uint8 = 32 + Step_64 uint8 = 64 + Step_128 uint8 = 128 +) + +const ( + DefaultAngle float32 = StepAngle_1_8 + DefaultGearRatio float32 = 1.0 + DefaultVSupply float32 = 12.0 + DefaultRCoil float32 = 1.2 + DefaultLCoil float32 = 0.005 + DefaultIPeak float32 = 2.0 + DefaultRSense float32 = 0.1 + DefaultFclk uint8 = 12 + DefaultStep_256 = 256 +) + +type Stepper struct { + Angle float32 + GearRatio float32 + VelocitySPS float32 // Velocity in Steps per sec + VSupply float32 + RCoil float32 + LCoil float32 + IPeak float32 + RSense float32 + MSteps uint8 + Fclk uint8 //Clock in Mhz + +} + +// NewStepper function initializes a Stepper with default values used for testing +func NewDefaultStepper() Stepper { + return Stepper{ + Angle: StepAngle_1_8, // Default to 1.8 degrees + GearRatio: 1.0, // Default to no reduction (1:1) + VSupply: 12.0, // Default 12V supply + RCoil: 1.2, // Default coil resistance (1.2 ohms) + LCoil: 0.005, // Default coil inductance (5 mH) + IPeak: 2.0, // Default peak current (2A) + RSense: 0.1, // Default sense resistance (0.1 ohms) + MSteps: Step_16, // Default 16 Microsteps + Fclk: DefaultFclk, + } +} + +// NewStepper initializes a Stepper with user-defined values +func NewStepper(angle float32, gearRatio, vSupply, rCoil, lCoil, iPeak, rSense float32, mSteps uint8, fclk uint8) Stepper { + return Stepper{ + Angle: angle, // User-defined stepper angle (e.g., StepAngle_1_8) + GearRatio: gearRatio, // User-defined gear ratio + VSupply: vSupply, // User-defined supply voltage + RCoil: rCoil, // User-defined coil resistance + LCoil: lCoil, // User-defined coil inductance + IPeak: iPeak, // User-defined peak current + RSense: rSense, // User-defined sense resistance + MSteps: mSteps, // User-defined microstepping setting + Fclk: fclk, // User-defined clock frequency in MHz + + } +} diff --git a/tmc5160/tmc5160.go b/tmc5160/tmc5160.go new file mode 100644 index 0000000..0bf2b10 --- /dev/null +++ b/tmc5160/tmc5160.go @@ -0,0 +1,194 @@ +//go:build tinygo + +package tmc5160 + +import ( + "github.com/orsinium-labs/tinymath" + "machine" +) + +type Driver struct { + comm RegisterComm + address uint8 + enablePin machine.Pin + stepper Stepper +} + +func NewDriver(comm RegisterComm, address uint8, enablePin machine.Pin, stepper Stepper) *Driver { + return &Driver{ + comm: comm, + address: address, + enablePin: enablePin, + stepper: stepper, + } +} + +// WriteRegister sends a register write command to the Driver. +func (driver *Driver) WriteRegister(reg uint8, value uint32) error { + if driver.comm == nil { + return CustomError("communication interface not set") + } + // Use the communication interface (RegisterComm) to write the register + return driver.comm.WriteRegister(reg, value, driver.address) +} + +// ReadRegister sends a register read command to the Driver and returns the read value. +func (driver *Driver) ReadRegister(reg uint8) (uint32, error) { + if driver.comm == nil { + return 0, CustomError("communication interface not set") + } + // Use the communication interface (RegisterComm) to read the register + return driver.comm.ReadRegister(reg, driver.address) +} + +// Begin initializes the Driver driver with power and motor parameters +func (driver *Driver) Begin(powerParams PowerStageParameters, motorParams MotorParameters, stepperDirection MotorDirection) bool { + // Clear the reset and charge pump undervoltage flags + gstat := NewGSTAT() + gstat.Reset = true + gstat.UvCp = true + err := driver.WriteRegister(GSTAT, gstat.Pack()) + if err != nil { + return false + } + + // Configure driver settings + drvConf := NewDRV_CONF() + drvConf.DrvStrength = constrain(powerParams.drvStrength, 0, 3) + drvConf.BBMTime = constrain(powerParams.bbmTime, 0, 24) + drvConf.BBMClks = constrain(powerParams.bbmClks, 0, 15) + err = driver.WriteRegister(DRV_CONF, drvConf.Pack()) + if err != nil { + return false + } + + // Set global scaler + err = driver.WriteRegister(GLOBAL_SCALER, uint32(constrain(motorParams.globalScaler, 32, 256))) + if err != nil { + return false + } + + // Set initial currents and delay + iholdrun := NewIHOLD_IRUN() + iholdrun.Ihold = constrain(motorParams.ihold, 0, 31) + iholdrun.Ihold = constrain(motorParams.irun, 0, 31) + iholdrun.IholdDelay = 7 + err = driver.WriteRegister(IHOLD_IRUN, iholdrun.Pack()) + if err != nil { + return false + } + + // Set PWM configuration values + pwmconf := NewPWMCONF() + err = driver.WriteRegister(PWMCONF, 0xC40C001E) + if err != nil { + return false + } // Reset default value pwm_ofs = 196,pwm_grad = 12,pwm_freq = 0, pwm_autoscale = false, pwm_autograd = false,freewheel = 3 + pwmconf.PwmAutoscale = false // Temporarily set to false for setting OFS and GRAD values + _fclk := int(driver.stepper.Fclk) * 1000000 + if _fclk > DEFAULT_F_CLK { + pwmconf.PwmFreq = 0 + } else { + pwmconf.PwmFreq = 0b01 // Recommended: 35kHz with internal 12MHz clock + } + pwmconf.PwmGrad = uint8(motorParams.pwmGradInitial) + pwmconf.PwmOfs = uint8(motorParams.pwmOfsInitial) + pwmconf.Freewheel = motorParams.freewheeling + err = driver.WriteRegister(PWMCONF, pwmconf.Pack()) + if err != nil { + return false + } + + // Enable PWM auto-scaling and gradient adjustment + pwmconf.PwmAutoscale = true + pwmconf.PwmAutograd = true + err = driver.WriteRegister(PWMCONF, pwmconf.Pack()) + if err != nil { + return false + } + + // Recommended chop configuration settings + _chopConf := NewCHOPCONF() + _chopConf.Toff = 5 + _chopConf.Tbl = 2 + _chopConf.HstrtTfd = 4 + _chopConf.HendOffset = 0 + err = driver.WriteRegister(CHOPCONF, _chopConf.Pack()) + if err != nil { + return false + } + rampMode := NewRAMPMODE(driver.comm, driver.address) + rampMode.SetMode(PositioningMode) + gconf := NewGCONF() + gconf.EnPwmMode = true // Enable stealthChop PWM mode + gconf.Shaft = stepperDirection == Clockwise + err = driver.WriteRegister(GCONF, gconf.Pack()) + if err != nil { + return false + } + + // Set default start, stop, threshold speeds + driver.setRampSpeeds(0.0, 0.1, 0.0) // Start, stop, threshold speeds + + // Set default D1 (must not be = 0 in positioning mode even with V1=0) + err = driver.WriteRegister(D_1, 100) + if err != nil { + return false + } + + return false +} +func (driver *Driver) setRampSpeeds(startSpeed float32, stopSpeed float32, transitionSpeed float32) { + str := driver.stepper.DesiredSpeedToTSTEP(uint32(startSpeed)) + stp := driver.stepper.DesiredSpeedToTSTEP(uint32(stopSpeed)) + ts := driver.stepper.DesiredSpeedToTSTEP(uint32(transitionSpeed)) + driver.WriteRegister(VSTART, uint32(tinymath.Min(0x3FFFF, float32(str)))) + driver.WriteRegister(VSTOP, uint32(tinymath.Min(0x3FFFF, float32(stp)))) + driver.WriteRegister(V_1, uint32(tinymath.Min(0xFFFFF, float32(ts)))) + println("Ramp set to: startSpeed:", startSpeed, "stopSpeed:", stopSpeed, "transitionSpeed:", transitionSpeed) +} + +// setMaxSpeed sets the maximum speed to 0 (placeholder function) +func setMaxSpeed(speed uint32) { + // This is a placeholder function that sets the speed + // Implement the actual logic to set the maximum speed register value + println("Max Speed set to:", speed) +} + +// Dump_TMC reads multiple registers from the Driver and logs their values with their names. +func (driver *Driver) Dump_TMC() error { + registers := []uint8{ + GCONF, CHOPCONF, GSTAT, DRV_STATUS, FACTORY_CONF, IOIN, LOST_STEPS, MSCNT, + MSCURACT, OTP_READ, PWM_SCALE, PWM_AUTO, TSTEP, + } + registerNames := map[uint8]string{ + GCONF: "GCONF", + CHOPCONF: "CHOPCONF", + GSTAT: "GSTAT", + DRV_STATUS: "DRV_STATUS", + FACTORY_CONF: "FACTORY_CONF", + IOIN: "IOIN", + LOST_STEPS: "LOST_STEPS", + MSCNT: "MSCNT", + MSCURACT: "MSCURACT", + OTP_READ: "OTP_READ", + PWM_SCALE: "PWM_SCALE", + PWM_AUTO: "PWM_AUTO", + TSTEP: "TSTEP", + } + for _, reg := range registers { + // Fetch the register name from the map + regName, exists := registerNames[reg] + if !exists { + regName = "Unknown Register" + } + val, err := driver.ReadRegister(reg) + if err != nil { + println("Error reading register", regName, err) + return err + } + println("Register", regName, "Value:", val) + } + + return nil +} diff --git a/tmc5160/uartcomm.go b/tmc5160/uartcomm.go new file mode 100644 index 0000000..37321a5 --- /dev/null +++ b/tmc5160/uartcomm.go @@ -0,0 +1,101 @@ +//go:build uart + +package tmc5160 + +import ( + "machine" + "time" +) + +// UARTComm implements RegisterComm for UART-based communication with Driver. +type UARTComm struct { + uart machine.UART + address uint8 +} + +// NewUARTComm creates a new UARTComm instance. +func NewUARTComm(uart machine.UART, address uint8) *UARTComm { + return &UARTComm{ + uart: uart, + address: address, + } +} + +// Setup initializes the UART communication with the Driver. +func (comm *UARTComm) Setup() error { + if comm.uart == (machine.UART{}) { + return CustomError("UART not initialized") + } + err := comm.uart.Configure(machine.UARTConfig{ + BaudRate: 115200, + }) + if err != nil { + return CustomError("Failed to configure UART") + } + return nil +} + +// WriteRegister sends a register write command to the Driver. +// Prepare the data packet (sync byte + address + register + data + checksum) +func (comm *UARTComm) WriteRegister(register uint8, value uint32, driverIndex uint8) error { + + buffer := []byte{ + 0x05, // Sync byte + comm.address, // Slave address + register | 0x80, // Write command (MSB set to 1 for write) + byte((value >> 24) & 0xFF), // MSB of value + byte((value >> 16) & 0xFF), // Middle byte + byte((value >> 8) & 0xFF), // Next byte + byte(value & 0xFF), // LSB of value + } + checksum := byte(0) + for _, b := range buffer[:7] { + checksum ^= b + } + buffer[7] = checksum // Set checksum byte + + // Write the data to the Driver + done := make(chan error, 1) + + go func() { + comm.uart.Write(buffer) + done <- nil + }() + + select { + case err := <-done: + return err + case <-time.After(100 * time.Millisecond): // Timeout after 100ms + return CustomError("write timeout") + } +} + +// ReadRegister sends a register read command to the Driver. +func (comm *UARTComm) ReadRegister(register uint8, driverIndex uint8) (uint32, error) { + // Prepare the read command (sync byte + address + register + checksum) + var writeBuffer [4]byte + writeBuffer[0] = 0x05 // Sync byte + writeBuffer[1] = comm.address // Slave address + writeBuffer[2] = register & 0x7F // Read command (MSB clear for read) + writeBuffer[3] = writeBuffer[0] ^ writeBuffer[1] ^ writeBuffer[2] // Checksum + done := make(chan []byte, 1) + go func() { + comm.uart.Write(writeBuffer[:]) + readBuffer := make([]byte, 8) // Prepare the buffer to read 8 bytes + comm.uart.Read(readBuffer) + done <- readBuffer + }() + select { + case readBuffer := <-done: + checksum := byte(0) + for i := 0; i < 7; i++ { + checksum ^= readBuffer[i] + } + if checksum != readBuffer[7] { + return 0, CustomError("checksum error") + } + return uint32(readBuffer[3])<<24 | uint32(readBuffer[4])<<16 | uint32(readBuffer[5])<<8 | uint32(readBuffer[6]), nil + case <-time.After(100 * time.Millisecond): // Timeout after 100ms + return 0, CustomError("read timeout") + } +} diff --git a/tmc5160/utils.go b/tmc5160/utils.go new file mode 100644 index 0000000..ce2dade --- /dev/null +++ b/tmc5160/utils.go @@ -0,0 +1,13 @@ +package tmc5160 + +func ToHex(value uint32) string { + hexChars := "0123456789ABCDEF" + result := "" + + for i := 0; i < 8; i++ { // 8 nibbles for a 32-bit number + nibble := (value >> (28 - i*4)) & 0xF + result += string(hexChars[nibble]) + } + + return "0x" + result +} diff --git a/touch/capacitive/gpio.go b/touch/capacitive/gpio.go new file mode 100644 index 0000000..c98d353 --- /dev/null +++ b/touch/capacitive/gpio.go @@ -0,0 +1,313 @@ +package capacitive + +import ( + "machine" + "runtime/interrupt" + "time" +) + +const ( + // How often to measure. + // The Update function will wait until this amount of time has passed. + measurementFrequency = 200 + minTimeBetweenMeasurements = time.Second / measurementFrequency + + // How much to multiply values before averaging. A value higher than 1 will + // help to avoid integer rounding errors and may improve accuracy slightly. + oversampling = 8 + + // How many samples to use for the moving average. + movingAverageWindow = 16 + + // After how many samples should the touch sensor be recalibrated? + // This should be a power of two (for efficient division) and be a multiple + // of movingAverageWindow. Ideally it should cause a recalibration every 5s + // or so. + recalibrationSamples = 1024 +) + +type Array struct { + // Time when the last update finished. This is used to make sure we call + // Update() the expected number of times per second. + lastUpdate time.Time + + // List of pins to measure each time. + pins []machine.Pin + + // Raw values (non-smoothed) from the last read. + values []uint16 + + hasFirstMeasurement bool + + // Static threshold. Zero if using a dynamic threshold. + staticThreshold uint16 + + // How long to measure. + measureCycles uint16 + + // Sensitivity (in promille) for the dynamic threshold. + sensitivity uint16 + + // Capacitance trackers for dynamic capacitance measurement. + trackers []capacitanceTracker +} + +// Create a new array of pins to be used as touch sensors. +// The pins do not need to be initialized. The array is immediately ready to +// use. +// +// By default, NewArray configures a static threshold that is not very +// sensitive. If you want the touch inputs to be more sensitive, use +// SetDynamicThreshold. +func NewArray(pins []machine.Pin) *Array { + for _, pin := range pins { + pin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + pin.High() + } + array := &Array{ + pins: pins, + values: make([]uint16, len(pins)), + measureCycles: uint16(machine.CPUFrequency() / 125000), // 1000 on the RP2040 (which is 125MHz) + lastUpdate: time.Now(), + } + + // A threshold of 500 works well on the RP2040. Scale this number to + // something similar on other chips. + array.SetStaticThreshold(int(machine.CPUFrequency() / 250000)) + + return array +} + +// Use a static threshold. This works well on simple touch surfaces where you'll +// directly touch the metal. +func (a *Array) SetStaticThreshold(threshold int) { + if threshold > 0xffff { + threshold = 0xffff + } + a.staticThreshold = uint16(threshold) + a.trackers = nil +} + +// Use a dynamic threshold (as promille), that will calibrate automatically. +// This is needed when you want to be able to detect touches through a +// non-conducting surface for example. Something like 100‰ (10%) will probably +// work in many cases, though you may need to try different value to reliably +// detect touches. +func (a *Array) SetDynamicThreshold(sensitivity int) { + a.sensitivity = uint16(sensitivity) + a.staticThreshold = 0 + a.trackers = make([]capacitanceTracker, len(a.pins)) +} + +// Measure all GPIO pins. This function must be called very often, ideally about +// 100-200 times per second (it will delay a bit when called more than 200 times +// per second). +func (a *Array) Update() { + // Wait until enough time has passed to charge all pins. + now := time.Now() + timeSinceLastUpdate := now.Sub(a.lastUpdate) + sleepTime := minTimeBetweenMeasurements - timeSinceLastUpdate + time.Sleep(sleepTime) + a.lastUpdate = now.Add(sleepTime) // should be ~equivalent to time.Now() + + // Measure each pin in turn. + for i, pin := range a.pins { + // Interrupts must be disabled during measuring for accurate results. + mask := interrupt.Disable() + + // Switch to input. This will stop the charging, and let it discharge + // through the resistor. + pin.Configure(machine.PinConfig{Mode: machine.PinInput}) + + // Wait for the pin to go low again. + // A longer duration means more capacitance, which means something is + // touching it (finger, banana, etc). + count := uint32(i) + for i := 0; i < int(a.measureCycles); i++ { + if !pin.Get() { + break + } + count++ + } + + interrupt.Restore(mask) + + a.values[i] = uint16(count) + + // Set the pin to high, to charge it for the next measurement. + pin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + pin.High() + } + + // The first measurement tends to be slightly off (too low value) so ignore + // that one. + if !a.hasFirstMeasurement { + a.hasFirstMeasurement = true + return + } + + for i := 0; i < len(a.trackers); i++ { + a.trackers[i].addValue(int(a.values[i]), int(a.sensitivity)) + } +} + +// Return the raw value of the given pin index of the most recent call to +// Update. This value is not smoothed in any way. +func (a *Array) RawValue(index int) int { + return int(a.values[index]) +} + +// Return the value from the moving average. This value is only available when a +// dynamic threshold has been set, it will panic otherwise. +func (a *Array) SmoothedValue(index int) int { + return int(a.trackers[index].avg) / oversampling +} + +// Return whether the given pin index is currently being touched. +func (a *Array) Touching(index int) bool { + if a.staticThreshold != 0 { + // Using a static threshold. + return a.values[index] > a.staticThreshold + } + + return a.trackers[index].touching +} + +// Separate object to store calibration data and track capacitance over time. +type capacitanceTracker struct { + recentValues [movingAverageWindow]uint16 + sum uint32 + avg uint16 + + baseline uint16 + noise uint16 + valueCount uint8 + touching bool + + recalibrationCount uint8 + recalibrationPrevAvg uint16 + recalibrationNoiseSum int32 + recalibrationSum uint32 +} + +func (ct *capacitanceTracker) addValue(value int, sensitivity int) { + // Maybe increase the resolution slightly by oversampling. This should + // increase the resolution a little bit after averaging and should reduce + // rounding errors. + // Typical input values on the RP2040 are 100-200 (or up to 1000 or so when + // touching the metal) so multiplying by 4-8 should be fine. Other chips + // generally have much lower values. + value *= oversampling + if value > 0xffff { + value = 0xffff // unlikely, but make sure we don't overflow + } + + // This does a number of things at the same time: + // * Add the new value to the recentValues array. + // * Calculate the moving sum (and average) of recentValues using a + // recursive moving average algorithm: + // https://www.dspguide.com/ch15/5.htm + ptr := &ct.recentValues[ct.valueCount%movingAverageWindow] + ct.sum -= uint32(*ptr) + ct.sum += uint32(value) + ct.avg = uint16(ct.sum / movingAverageWindow) + *ptr = uint16(value) + ct.valueCount++ + + // Do an initial calibration once the first values have been read. + if ct.baseline == 0 && ct.valueCount == movingAverageWindow { + ct.baseline = ct.avg + + // Calculate initial noise as an average absolute deviation: + // https://en.wikipedia.org/wiki/Average_absolute_deviation + // This is a quick and imprecise way to find the noise, better noise + // detection happens during recalibration. + var diffSum uint32 + for _, sample := range ct.recentValues { + diff := int(ct.avg) - int(sample) + if diff < 0 { + diff = -diff + } + diffSum += uint32(diff) + } + ct.noise = uint16(diffSum / (movingAverageWindow / 2)) + } + + // Now determine whether the touch pad is being touched. + + if ct.baseline == 0 { + // Not yet calibrated. + ct.touching = false + return + } + + // Calculate the threshold. + // Divide by 65536 (instead of 65500) to avoid a potentially expensive + // division while still being close enough. + threshold := (uint32(ct.baseline) * uint32(sensitivity+1000) * 65) / 65536 + + // Add noise to the threshold, to avoid toggling quickly. This mainly + // filters out mains noise. + threshold += uint32(ct.noise) + + // Implement some hysteresis: if the touch pad was previously touched, lower + // the threshold a little to avoid bouncing effects. + // TODO: let this hysteresis depend on the amount of noise. + if ct.touching { + threshold = (threshold*3 + uint32(ct.baseline)) / 4 // lower the threshold by 25% + } + + // Is the pad being touched? + ct.touching = uint32(ct.avg) > threshold + + // Do a recalibration after the sensor hasn't been touched for ~5s, to + // account for drift over time (humidity etc). + if ct.touching { + // Reset calibration (start from zero). + ct.recalibrationCount = 0 + ct.recalibrationSum = 0 + ct.recalibrationNoiseSum = 0 + } else { + // Add the last batch of samples to the sum. + if ct.valueCount%movingAverageWindow == 0 { + ct.recalibrationCount++ + + // Wait a few cycles before starting data collection for + // calibration. + cycle := int(ct.recalibrationCount) - 3 + + if cycle < 0 { + // Store the previous average, to calculate the noise value. + ct.recalibrationPrevAvg = ct.avg + + } else if cycle >= 0 { + // Collect data for recalibration. + ct.recalibrationSum += ct.sum + + // Add difference between two (averaged) samples as a measure of + // the noise. + diff := int32(ct.recalibrationPrevAvg) - int32(ct.avg) + if diff < 0 { + diff = -diff + } + ct.recalibrationNoiseSum += diff + ct.recalibrationPrevAvg = ct.avg + + } + + // Do the recalibration after enough samples have been collected. + // Note: the noise is basically the average of absolute differences + // between two averaging windows. I don't know whether this + // algorithm has a name, but it seems to work here to detect the + // amount of noise. + const totalRecalibrationCount = recalibrationSamples / movingAverageWindow + if cycle == totalRecalibrationCount { + ct.baseline = uint16(ct.recalibrationSum / recalibrationSamples) + ct.noise = uint16(ct.recalibrationNoiseSum / (totalRecalibrationCount / 2)) + ct.recalibrationCount = 0 + ct.recalibrationSum = 0 + ct.recalibrationNoiseSum = 0 + } + } + } +} diff --git a/uc8151/lut.go b/uc8151/lut.go new file mode 100644 index 0000000..392488b --- /dev/null +++ b/uc8151/lut.go @@ -0,0 +1,30 @@ +package uc8151 + +// LUTType is the look-up table for the display +type LUTType [42]uint8 + +type LUTSet struct { + VCOM LUTType + WW LUTType + BW LUTType + WB LUTType + BB LUTType +} + +func (lut *LUTType) Clear() { + for i := range lut { + lut[i] = 0 + } +} + +func (lut *LUTType) SetRow(row int, pat uint8, dur [4]uint8, rep uint8) error { + index := row * 6 + lut[index] = pat + lut[index+1] = dur[0] + lut[index+2] = dur[1] + lut[index+3] = dur[2] + lut[index+4] = dur[3] + lut[index+5] = rep + + return nil +} diff --git a/uc8151/registers.go b/uc8151/registers.go index 386d2c8..407d7a4 100644 --- a/uc8151/registers.go +++ b/uc8151/registers.go @@ -1,5 +1,7 @@ package uc8151 +import "tinygo.org/x/drivers" + // Registers const ( // Display resolution @@ -141,13 +143,16 @@ const ( HZ_100 = 0b00111010 HZ_200 = 0b00111001 - NO_ROTATION Rotation = 0 - ROTATION_90 Rotation = 1 // 90 degrees clock-wise rotation - ROTATION_180 Rotation = 2 - ROTATION_270 Rotation = 3 + // deprecated constants, just here for backward compatibility. + NO_ROTATION = drivers.Rotation0 + ROTATION_90 = drivers.Rotation90 + ROTATION_180 = drivers.Rotation180 + ROTATION_270 = drivers.Rotation270 DEFAULT Speed = 0 - MEDIUM Speed = 1 - FAST Speed = 2 - TURBO Speed = 3 + SLOW Speed = 1 + MEDIUM Speed = 2 + FAST Speed = 3 + FASTER Speed = 4 + TURBO Speed = 5 ) diff --git a/uc8151/uc8151.go b/uc8151/uc8151.go index 866b321..4367568 100644 --- a/uc8151/uc8151.go +++ b/uc8151/uc8151.go @@ -1,56 +1,71 @@ // Package uc8151 implements a driver for e-ink displays controlled by UC8151 // // Inspired by https://github.com/pimoroni/pimoroni-pico/blob/main/drivers/uc8151/uc8151.cpp +// Additional inspiration from https://github.com/antirez/uc8151_micropython // Datasheet: https://www.buydisplay.com/download/ic/UC8151C.pdf package uc8151 // import "tinygo.org/x/drivers/uc8151" import ( "errors" "image/color" - "machine" "time" "tinygo.org/x/drivers" + "tinygo.org/x/drivers/internal/legacy" + "tinygo.org/x/drivers/internal/pin" + "tinygo.org/x/drivers/pixel" +) + +var ( + errOutOfRange = errors.New("out of screen range") ) type Config struct { - Width int16 - Height int16 - Rotation Rotation // Rotation is clock-wise - Speed Speed // Value from DEFAULT, MEDIUM, FAST, TURBO - Blocking bool + Width int16 + Height int16 + Rotation drivers.Rotation // Rotation is clock-wise + Speed Speed // Value from DEFAULT, SLOW, MEDIUM, FAST, FASTER, TURBO + Blocking bool // block on calls to display or return immediately + FlickerFree bool // if we should avoid flickering + UpdateAfter int // if we are using flicker-free mode, how often we should update the screen } type Device struct { - bus drivers.SPI - cs machine.Pin - dc machine.Pin - rst machine.Pin - busy machine.Pin - width int16 - height int16 - buffer []uint8 - bufferLength uint32 - rotation Rotation - speed Speed - blocking bool + bus drivers.SPI + cs pin.OutputFunc + dc pin.OutputFunc + rst pin.OutputFunc + isBusy pin.InputFunc + width int16 + height int16 + buffer []uint8 + bufferLength uint32 + rotation drivers.Rotation + speed Speed + blocking bool + flickerFree bool + updateCount, updateAfter int } -type Rotation uint8 type Speed uint8 -// New returns a new epd2in13x driver. Pass in a fully configured SPI bus. -func New(bus drivers.SPI, csPin, dcPin, rstPin, busyPin machine.Pin) Device { - csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - rstPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) - busyPin.Configure(machine.PinConfig{Mode: machine.PinInput}) +// New returns a new uc8151 driver. Pass in a fully configured SPI bus. +// Pins passed in must be configured beforehand. +func New(bus drivers.SPI, csPin, dcPin, rstPin pin.Output, busyPin pin.Input) Device { + // For backwards compatibility. + // This driver used to configure pins, + // so leave in to not break users. + // May be removed in future so try not to depend on it! + legacy.ConfigurePinOut(csPin) + legacy.ConfigurePinOut(dcPin) + legacy.ConfigurePinOut(rstPin) + legacy.ConfigurePinInput(busyPin) return Device{ - bus: bus, - cs: csPin, - dc: dcPin, - rst: rstPin, - busy: busyPin, + bus: bus, + cs: csPin.Set, + dc: dcPin.Set, + rst: rstPin.Set, + isBusy: busyPin.Get, } } @@ -69,6 +84,8 @@ func (d *Device) Configure(cfg Config) { d.rotation = cfg.Rotation d.speed = cfg.Speed d.blocking = cfg.Blocking + d.flickerFree = cfg.FlickerFree + d.updateAfter = cfg.UpdateAfter d.bufferLength = (uint32(d.width) * uint32(d.height)) / 8 d.buffer = make([]uint8, d.bufferLength) for i := uint32(0); i < d.bufferLength; i++ { @@ -84,14 +101,14 @@ func (d *Device) Configure(cfg Config) { d.SendData(RES_128x296 | LUT_REG | FORMAT_BW | SHIFT_RIGHT | BOOSTER_ON | RESET_NONE | SCAN_UP) } - d.SetLUT(d.speed) + d.SetLUT(d.speed, d.flickerFree) d.SendCommand(PWR) d.SendData(VDS_INTERNAL | VDG_INTERNAL) - d.SendData(VCOM_VG | VGHL_16V) - d.SendData(0x2B) - d.SendData(0x2B) - d.SendData(0x2B) + d.SendData(VCOM_VD | VGHL_16V) + d.SendData(0b100110) // +10v VDH + d.SendData(0b100110) // -10v VDL + d.SendData(0b000011) // VDHR default (For red pixels, not used here) d.SendCommand(PON) d.WaitUntilIdle() @@ -102,7 +119,7 @@ func (d *Device) Configure(cfg Config) { d.SendData(START_10MS | STRENGTH_3 | OFF_6_58US) d.SendCommand(PFS) - d.SendData(FRAMES_1) + d.SendData(FRAMES_4) d.SendCommand(TSE) d.SendData(TEMP_INTERNAL | OFFSET_0) @@ -111,14 +128,13 @@ func (d *Device) Configure(cfg Config) { d.SendData(0x22) d.SendCommand(CDI) - d.SendData(0x4C) // 4C //5C + d.SendData(0b11_00_1100) d.SendCommand(PLL) d.SendData(HZ_100) d.SendCommand(POF) d.WaitUntilIdle() - } // Reset resets the device @@ -135,31 +151,30 @@ func (d *Device) PowerOff() { d.SendCommand(POF) } +// PowerOn power on the device +func (d *Device) PowerOn() { + d.SendCommand(PON) +} + // SendCommand sends a command to the display func (d *Device) SendCommand(command uint8) { - d.sendDataCommand(true, command) + d.dc.Low() + d.cs.Low() + d.bus.Transfer(command) + d.cs.High() } // SendData sends a data byte to the display -func (d *Device) SendData(data uint8) { - d.sendDataCommand(false, data) -} - -// sendDataCommand sends image data or a command to the screen -func (d *Device) sendDataCommand(isCommand bool, data uint8) { - if isCommand { - d.dc.Low() - } else { - d.dc.High() - } +func (d *Device) SendData(data ...uint8) { + d.dc.High() d.cs.Low() - d.bus.Transfer(data) + d.bus.Tx(data, nil) d.cs.High() } // SetPixel modifies the internal buffer in a single pixel. // The display have 2 colors: black and white -// We use RGBA(0,0,0, 255) as white (transparent) +// We use RGBA(0, 0, 0) as white (transparent) // Anything else as black func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { x, y = d.xy(x, y) @@ -168,31 +183,60 @@ func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { return } byteIndex := x/8 + y*(d.width/8) - if c.R == 0 && c.G == 0 && c.B == 0 { // TRANSPARENT / WHITE - d.buffer[byteIndex] &^= 0x80 >> uint8(x%8) - } else { // WHITE / EMPTY + if c.R != 0 || c.G != 0 || c.B != 0 { d.buffer[byteIndex] |= 0x80 >> uint8(x%8) + } else { + d.buffer[byteIndex] &^= 0x80 >> uint8(x%8) } } +// DrawBitmap copies the bitmap to the screen at the given coordinates. +func (d *Device) DrawBitmap(x, y int16, bitmap pixel.Image[pixel.Monochrome]) error { + dw, dh := d.Size() + bw, bh := bitmap.Size() + if x < 0 || x+int16(bw) > dw || y < 0 || y+int16(bh) > dh { + return errOutOfRange + } + + for i := 0; i < bw; i++ { + for j := 0; j < bh; j++ { + d.SetPixel(x+int16(i), y+int16(j), bitmap.Get(i, j).RGBA()) + } + } + + return nil +} + // Display sends the buffer to the screen. func (d *Device) Display() error { if d.blocking { d.WaitUntilIdle() } - d.SendCommand(PON) + + if d.flickerFree && d.updateAfter != 0 && d.updateCount%d.updateAfter == 0 { + // we need full refresh here + d.SetLUT(MEDIUM, false) + } else { + d.SetLUT(d.speed, d.flickerFree) + } + d.updateCount++ + + d.PowerOn() + d.SendCommand(PTOU) d.SendCommand(DTM2) - for i := uint32(0); i < d.bufferLength; i++ { - d.SendData(d.buffer[i]) - } + d.SendData(d.buffer...) d.SendCommand(DSP) d.SendCommand(DRF) + + d.SetLUT(d.speed, d.flickerFree) + if d.blocking { d.WaitUntilIdle() d.PowerOff() } + return nil } @@ -208,13 +252,14 @@ func (d *Device) DisplayRect(x int16, y int16, width int16, height int16) error if x < 0 || y < 0 || x >= d.width || y >= d.height || width < 0 || height < 0 { return errors.New("wrong rectangle") } - if d.rotation == ROTATION_90 { + switch d.rotation { + case drivers.Rotation0: width, height = height, width x -= width - } else if d.rotation == ROTATION_180 { + case drivers.Rotation90: x -= width - 1 y -= height - 1 - } else if d.rotation == ROTATION_270 { + case drivers.Rotation180: width, height = height, width y -= height } @@ -262,20 +307,26 @@ func (d *Device) DisplayRect(x int16, y int16, width int16, height int16) error // ClearDisplay erases the device SRAM func (d *Device) ClearDisplay() { + ff := d.flickerFree + d.flickerFree = false + defer func() { + d.flickerFree = ff + }() + d.ClearBuffer() d.Display() } // WaitUntilIdle waits until the display is ready func (d *Device) WaitUntilIdle() { - for !d.busy.Get() { - time.Sleep(100 * time.Millisecond) + for !d.isBusy() { + time.Sleep(10 * time.Millisecond) } } // IsBusy returns the busy status of the display func (d *Device) IsBusy() bool { - return d.busy.Get() + return d.isBusy() } // ClearBuffer sets the buffer to 0xFF (white) @@ -287,15 +338,32 @@ func (d *Device) ClearBuffer() { // Size returns the current size of the display. func (d *Device) Size() (w, h int16) { - if d.rotation == ROTATION_90 || d.rotation == ROTATION_270 { + if d.rotation == drivers.Rotation90 || d.rotation == drivers.Rotation270 { return d.height, d.width } return d.width, d.height } +// Rotation returns the currently configured rotation. +func (d *Device) Rotation() drivers.Rotation { + return d.rotation +} + // SetRotation changes the rotation (clock-wise) of the device -func (d *Device) SetRotation(rotation Rotation) { +func (d *Device) SetRotation(rotation drivers.Rotation) error { d.rotation = rotation + return nil +} + +// Set the sleep mode for this display. +func (d *Device) Sleep(sleepEnabled bool) error { + if sleepEnabled { + d.PowerOff() + return nil + } + + d.PowerOn() + return nil } // SetBlocking changes the blocking flag of the device @@ -303,16 +371,16 @@ func (d *Device) SetBlocking(blocking bool) { d.blocking = blocking } -// xy chages the coordinates according to the rotation +// xy changes the coordinates according to the rotation func (d *Device) xy(x, y int16) (int16, int16) { switch d.rotation { - case NO_ROTATION: + case drivers.Rotation0: return x, y - case ROTATION_90: + case drivers.Rotation90: return d.width - y - 1, x - case ROTATION_180: + case drivers.Rotation180: return d.width - x - 1, d.height - y - 1 - case ROTATION_270: + case drivers.Rotation270: return y, d.height - x - 1 } return x, y @@ -338,296 +406,109 @@ func (d *Device) Invert(invert bool) { } } -// SetLUT sets the look up tables for full or partial updates -func (d *Device) SetLUT(speed Speed) { - switch speed { - case MEDIUM: - var lut = [44]uint8{ - 0x00, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x00, 0x23, 0x23, 0x00, 0x00, 0x02, - 0x00, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - } - d.SendCommand(LUT_VCOM) - for i := 0; i < 44; i++ { - d.SendData(lut[i]) - } - lut = [44]uint8{ - 0x54, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x60, 0x23, 0x23, 0x00, 0x00, 0x02, - 0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } +// SetLUT sets the look up tables for full or partial updates based on +// the speed and flicker-free mode. +// Based on code from https://github.com/antirez/uc8151_micropython +func (d *Device) SetLUT(speed Speed, flickerFree bool) error { + var lut LUTSet - lut = [44]uint8{ - 0x54, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x60, 0x23, 0x23, 0x00, 0x00, 0x02, - 0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x60, 0x23, 0x23, 0x00, 0x00, 0x02, - 0x54, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x60, 0x23, 0x23, 0x00, 0x00, 0x02, - 0x54, 0x16, 0x16, 0x0d, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - break - case FAST: - var lut = [44]uint8{ - 0x00, 0x04, 0x04, 0x07, 0x00, 0x01, - 0x00, 0x0c, 0x0c, 0x00, 0x00, 0x02, - 0x00, 0x04, 0x04, 0x07, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - } - d.SendCommand(LUT_VCOM) - for i := 0; i < 44; i++ { - d.SendData(lut[i]) - } - lut = [44]uint8{ - 0x54, 0x04, 0x04, 0x07, 0x00, 0x01, - 0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02, - 0xa8, 0x04, 0x04, 0x07, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0x54, 0x04, 0x04, 0x07, 0x00, 0x01, - 0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02, - 0xa8, 0x04, 0x04, 0x07, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x04, 0x04, 0x07, 0x00, 0x01, - 0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02, - 0x54, 0x04, 0x04, 0x07, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x04, 0x04, 0x07, 0x00, 0x01, - 0x60, 0x0c, 0x0c, 0x00, 0x00, 0x02, - 0x54, 0x04, 0x04, 0x07, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - break - case TURBO: - var lut = [44]uint8{ - 0x00, 0x01, 0x01, 0x02, 0x00, 0x01, - 0x00, 0x02, 0x02, 0x00, 0x00, 0x02, - 0x00, 0x02, 0x02, 0x03, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - } - d.SendCommand(LUT_VCOM) - for i := 0; i < 44; i++ { - d.SendData(lut[i]) - } - lut = [44]uint8{ - 0x54, 0x01, 0x01, 0x02, 0x00, 0x01, - 0x60, 0x02, 0x02, 0x00, 0x00, 0x02, - 0xa8, 0x02, 0x02, 0x03, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0x54, 0x01, 0x01, 0x02, 0x00, 0x01, - 0x60, 0x02, 0x02, 0x00, 0x00, 0x02, - 0xa8, 0x02, 0x02, 0x03, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x01, 0x01, 0x02, 0x00, 0x01, - 0x60, 0x02, 0x02, 0x00, 0x00, 0x02, - 0x54, 0x02, 0x02, 0x03, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x01, 0x01, 0x02, 0x00, 0x01, - 0x60, 0x02, 0x02, 0x00, 0x00, 0x02, - 0x54, 0x02, 0x02, 0x03, 0x00, 0x02, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - break - default: - var lut = [44]uint8{ - 0x00, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x00, 0x8c, 0x8c, 0x00, 0x00, 0x04, - 0x00, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - } - d.SendCommand(LUT_VCOM) - for i := 0; i < 44; i++ { - d.SendData(lut[i]) - } - lut = [44]uint8{ - 0x54, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04, - 0xa8, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0x54, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04, - 0xa8, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BW) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04, - 0x54, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_WB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - lut = [44]uint8{ - 0xa8, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x60, 0x8c, 0x8c, 0x00, 0x00, 0x04, - 0x54, 0x64, 0x64, 0x37, 0x00, 0x01, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} - d.SendCommand(LUT_BB) - // do not send last two bytes - for i := 0; i < 42; i++ { - d.SendData(lut[i]) - } - - break + // Num. of frames for single direction change. + period := 64 + p := uint8(period / (2 ^ (int(speed) - 1))) + if p < 1 { + p = 1 } + + // Num. of frames for back-and-forth change. + hperiod := period % 2 + hp := uint8(hperiod / (2 ^ (int(speed) - 1))) + if hp < 1 { + hp = 1 + } + + if speed < FAST && !flickerFree { + // For low speed everything is charge-neutral, even WB/BW. + + // Phase 1: long go-inverted-color. + lut.VCOM.SetRow(0, 0x00, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02) + lut.BW.SetRow(0, 0b01_000000, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02) + lut.WB.SetRow(0, 0b10_000000, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02) + + // Phase 2: short ping/pong. + lut.VCOM.SetRow(1, 0x00, [4]uint8{hp, hp, 0x00, 0x00}, 0x02) + lut.BW.SetRow(1, 0b10_01_0000, [4]uint8{hp, hp, 0x00, 0x00}, 0x01) + lut.WB.SetRow(1, 0b01_10_0000, [4]uint8{hp, hp, 0x00, 0x00}, 0x01) + + // Phase 3: long go-target-color. + lut.VCOM.SetRow(2, 0x00, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02) + lut.BW.SetRow(2, 0b10_000000, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02) + lut.WB.SetRow(2, 0b01_000000, [4]uint8{p, 0x00, 0x00, 0x00}, 0x02) + + // For this speed, we use the same LUTs for WW/BB as well. + copy(lut.WW[:], lut.BW[:]) + copy(lut.BB[:], lut.WB[:]) + } else { + // Speed >= FAST + // For greater than 3 we use non charge-neutral LUTs for WB/BW + // since the inpulse is short and it gets reversed when the + // pixel changes color, so that's not a problem for the display, + // however we still need to use charge-neutral LUTs for WW/BB. + lut.VCOM.SetRow(0, 0x00, [4]uint8{p, p, p, p}, 0x01) + lut.BW.SetRow(0, 0b10_00_00_00, [4]uint8{p * 4, 0x00, 0x00, 0x00}, 0x01) + lut.WB.SetRow(0, 0b01_00_00_00, [4]uint8{p * 4, 0x00, 0x00, 0x00}, 0x01) + lut.WW.SetRow(0, 0b01_10_00_00, [4]uint8{p * 2, p * 2, 0x00, 0x00}, 0x01) + lut.BB.SetRow(0, 0b10_01_00_00, [4]uint8{p * 2, p * 2, 0x00, 0x00}, 0x01) + } + + if flickerFree { + // If no flickering mode is enabled, we use an empty + // waveform BB and WW. The screen will need to be periodically fully refreshed. + lut.WW.Clear() + lut.BB.Clear() + } + + d.SendCommand(LUT_VCOM) + d.SendData(append(lut.VCOM[:], []uint8{0, 0}...)...) + + d.SendCommand(LUT_BW) + d.SendData(lut.BW[:]...) + + d.SendCommand(LUT_WB) + d.SendData(lut.WB[:]...) + + d.SendCommand(LUT_WW) + d.SendData(lut.WW[:]...) + + d.SendCommand(LUT_BB) + d.SendData(lut.BB[:]...) + + return nil +} + +// FillRectangle fills a rectangle at a given coordinates with a color +func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error { + dw, dh := d.Size() + + if x < 0 || y < 0 || width <= 0 || height <= 0 || + x >= d.width || (x+width) > dw || y >= dh || (y+height) > dh { + return errOutOfRange + } + + if x+width == dw && y+height == dh && c.R == 0 && c.G == 0 && c.B == 0 { + d.ClearDisplay() + return nil + } + + for i := x; i < x+width; i++ { + for j := y; j < y+height; j++ { + d.SetPixel(i, j, c) + } + } + + return nil +} + +// SetScroll sets the vertical scrolling for the display, which is a NOP for this display. +func (d *Device) SetScroll(line int16) { + return } diff --git a/unoqmatrix/matrix.go b/unoqmatrix/matrix.go new file mode 100644 index 0000000..7430c08 --- /dev/null +++ b/unoqmatrix/matrix.go @@ -0,0 +1,290 @@ +// Package unoqmatrix provides a driver for the UnoQMatrix LED matrix display. +// +// The UnoQMatrix is an 8x13 LED matrix display that can be controlled using a single pin. +// It uses a multiplexing technique to control the LEDs, which allows for a large number of LEDs to be controlled with fewer pins. +// +// This driver provides basic functionality to set individual pixels, clear the display, and refresh the display. +// +// Note: The UnoQMatrix does not support brightness control or color depth. Each pixel can only be turned on or off. +// Could it suppport brightness control by using PWM on the pin? To be investigated. +package unoqmatrix + +import ( + "image/color" + "time" + + pin "tinygo.org/x/drivers/internal/pin" +) + +type Config struct { + // Rotation of the LED matrix. + Rotation uint8 +} + +// Valid values: +// +// 0: regular orientation, (0 degree rotation) +// 1: 90 degree rotation clockwise +// 2: 180 degree rotation clockwise +// 3: 270 degree rotation clockwise +const ( + RotationNormal = 0 + Rotation90 = 1 + Rotation180 = 2 + Rotation270 = 3 +) + +const ( + ledRows = 8 + ledCols = 13 + + pixelRefreshDelay = 10 * time.Microsecond +) + +// CharlieplexPin represents a pin used for charlieplexing. +// It must be able to drive high/low (output mode) and float (high-impedance/input mode). +// +// Example construction from a machine.Pin using the pin HAL pattern: +// +// var isOutput bool +// cp := unoqmatrix.CharlieplexPin{ +// Set: pin.OutputFunc(func(level bool) { +// if !isOutput { +// p.Configure(machine.PinConfig{Mode: machine.PinOutput}) +// isOutput = true +// } +// p.Set(level) +// }), +// Float: func() { +// if isOutput { +// p.Configure(machine.PinConfig{Mode: machine.PinInput}) +// isOutput = false +// } +// }, +// } +type CharlieplexPin struct { + Set pin.OutputFunc // Drive pin high (true) or low (false); auto-configures to output mode. + Float func() // Put pin into high-impedance (input) mode. +} + +const numPins = 11 + +// Device represents the UnoQMatrix LED matrix display. +type Device struct { + pins [numPins]CharlieplexPin + buffer [ledRows][ledCols]color.RGBA + rotation uint8 +} + +// New returns a new unoqmatrix driver. +// The provided pins are the 11 charlieplex pins used to control the LED matrix. +func New(pins [numPins]CharlieplexPin) Device { + return Device{pins: pins} +} + +// Configure sets up the device. +func (d *Device) Configure(cfg Config) { + d.SetRotation(cfg.Rotation) +} + +// SetRotation changes the rotation of the LED matrix. +// +// Valid values for rotation: +// +// 0: regular orientation, (0 degree rotation) +// 1: 90 degree rotation clockwise +// 2: 180 degree rotation clockwise +// 3: 270 degree rotation clockwise +func (d *Device) SetRotation(rotation uint8) { + d.rotation = rotation % 4 +} + +// SetPixel sets the color of a specific pixel. +func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { + d.buffer[y][x] = c +} + +// GetPixel returns the color of a specific pixel. +func (d *Device) GetPixel(x int16, y int16) color.RGBA { + return d.buffer[y][x] +} + +// Display sends the buffer (if any) to the screen. +// Only lights active (non-black) pixels, and resets only the 2 previously +// driven pins between LEDs instead of all 11, making each refresh cycle +// proportional to the number of lit LEDs. +func (d *Device) Display() error { + d.clearDisplay() + + var lastIdx0, lastIdx1 uint8 + hasLast := false + + for row := 0; row < ledRows; row++ { + for col := 0; col < ledCols; col++ { + c := d.buffer[row][col] + if c.R == 0 && c.G == 0 && c.B == 0 { + continue + } + + idx := row*ledCols + col + if idx < 0 || idx >= len(pinMapping) { + continue + } + + // Float only the two pins that were driving the previous LED. + if hasLast { + d.pins[lastIdx0].Float() + d.pins[lastIdx1].Float() + } + hasLast = true + + idx0 := pinMapping[idx][0] + idx1 := pinMapping[idx][1] + d.pins[idx0].Set.High() + d.pins[idx1].Set.Low() + lastIdx0 = idx0 + lastIdx1 = idx1 + + time.Sleep(pixelRefreshDelay) + } + } + + // Float the last driven LED. + if hasLast { + d.pins[lastIdx0].Float() + d.pins[lastIdx1].Float() + } + + return nil +} + +// ClearDisplay turns off all the LEDs on the display. +func (d *Device) ClearDisplay() { + for row := 0; row < ledRows; row++ { + for col := 0; col < ledCols; col++ { + d.buffer[row][col] = color.RGBA{0, 0, 0, 255} + } + } +} + +// Size returns the current size of the display. +func (d *Device) Size() (w, h int16) { + return ledCols, ledRows +} + +// pinMapping defines the mapping of LED indices to pin pairs. Each entry corresponds +// to an LED index (0-104) and contains the two pin numbers that need to be set to turn on that LED. +// based on https://github.com/arduino/ArduinoCore-zephyr/blob/main/loader/matrix.inc#L13 +var pinMapping = [][2]uint8{ + {0, 1}, // 0 + {1, 0}, + {0, 2}, + {2, 0}, + {1, 2}, + {2, 1}, + {0, 3}, + {3, 0}, + {1, 3}, + {3, 1}, + {2, 3}, // 10 + {3, 2}, + {0, 4}, + {4, 0}, + {1, 4}, + {4, 1}, + {2, 4}, + {4, 2}, + {3, 4}, + {4, 3}, + {0, 5}, // 20 + {5, 0}, + {1, 5}, + {5, 1}, + {2, 5}, + {5, 2}, + {3, 5}, + {5, 3}, + {4, 5}, + {5, 4}, + {0, 6}, // 30 + {6, 0}, + {1, 6}, + {6, 1}, + {2, 6}, + {6, 2}, + {3, 6}, + {6, 3}, + {4, 6}, + {6, 4}, + {5, 6}, // 40 + {6, 5}, + {0, 7}, + {7, 0}, + {1, 7}, + {7, 1}, + {2, 7}, + {7, 2}, + {3, 7}, + {7, 3}, + {4, 7}, // 50 + {7, 4}, + {5, 7}, + {7, 5}, + {6, 7}, + {7, 6}, + {0, 8}, + {8, 0}, + {1, 8}, + {8, 1}, + {2, 8}, // 60 + {8, 2}, + {3, 8}, + {8, 3}, + {4, 8}, + {8, 4}, + {5, 8}, + {8, 5}, + {6, 8}, + {8, 6}, + {7, 8}, // 70 + {8, 7}, + {0, 9}, + {9, 0}, + {1, 9}, + {9, 1}, + {2, 9}, + {9, 2}, + {3, 9}, + {9, 3}, + {4, 9}, // 80 + {9, 4}, + {5, 9}, + {9, 5}, + {6, 9}, + {9, 6}, + {7, 9}, + {9, 7}, + {8, 9}, + {9, 8}, + {0, 10}, // 90 + {10, 0}, + {1, 10}, + {10, 1}, + {2, 10}, + {10, 2}, + {3, 10}, + {10, 3}, + {4, 10}, + {10, 4}, + {5, 10}, // 100 + {10, 5}, + {6, 10}, + {10, 6}, +} + +// clearDisplay turns off all the LEDs on the display by floating all pins. +func (d *Device) clearDisplay() { + for i := range d.pins { + d.pins[i].Float() + } +} diff --git a/unoqmatrix/matrix_test.go b/unoqmatrix/matrix_test.go new file mode 100644 index 0000000..4e29958 --- /dev/null +++ b/unoqmatrix/matrix_test.go @@ -0,0 +1,325 @@ +package unoqmatrix + +import ( + "image/color" + "testing" + + pin "tinygo.org/x/drivers/internal/pin" +) + +// pinState tracks the state of a mock charlieplex pin. +type pinState struct { + level bool // true=high, false=low + isOutput bool // true=output mode, false=floating (high-Z) +} + +// mockPins creates 11 mock CharlieplexPins and returns them along with their observable state. +func mockPins() ([numPins]CharlieplexPin, *[numPins]pinState) { + var pins [numPins]CharlieplexPin + var states [numPins]pinState + for i := range pins { + idx := i // capture + pins[i] = CharlieplexPin{ + Set: pin.OutputFunc(func(level bool) { + states[idx].isOutput = true + states[idx].level = level + }), + Float: func() { + states[idx].isOutput = false + states[idx].level = false + }, + } + } + return pins, &states +} + +func newTestDevice() (Device, *[numPins]pinState) { + pins, states := mockPins() + d := New(pins) + return d, states +} + +func TestNew(t *testing.T) { + d, _ := newTestDevice() + w, h := d.Size() + if w != ledCols || h != ledRows { + t.Errorf("Size() = (%d, %d), want (%d, %d)", w, h, ledCols, ledRows) + } +} + +func TestSize(t *testing.T) { + d, _ := newTestDevice() + w, h := d.Size() + if w != 13 { + t.Errorf("width = %d, want 13", w) + } + if h != 8 { + t.Errorf("height = %d, want 8", h) + } +} + +func TestSetGetPixel(t *testing.T) { + d, _ := newTestDevice() + c := color.RGBA{R: 255, G: 128, B: 64, A: 255} + + d.SetPixel(3, 2, c) + got := d.GetPixel(3, 2) + if got != c { + t.Errorf("GetPixel(3,2) = %v, want %v", got, c) + } + + // Unset pixel should be zero-value. + got = d.GetPixel(0, 0) + if got != (color.RGBA{}) { + t.Errorf("GetPixel(0,0) = %v, want zero", got) + } +} + +func TestClearDisplay(t *testing.T) { + d, _ := newTestDevice() + on := color.RGBA{R: 255, G: 255, B: 255, A: 255} + off := color.RGBA{A: 255} + + d.SetPixel(0, 0, on) + d.SetPixel(5, 3, on) + d.ClearDisplay() + + for y := int16(0); y < ledRows; y++ { + for x := int16(0); x < ledCols; x++ { + got := d.GetPixel(x, y) + if got != off { + t.Errorf("after ClearDisplay, GetPixel(%d,%d) = %v, want %v", x, y, got, off) + } + } + } +} + +func TestSetRotation(t *testing.T) { + d, _ := newTestDevice() + + tests := []struct { + input uint8 + want uint8 + }{ + {0, 0}, + {1, 1}, + {2, 2}, + {3, 3}, + {4, 0}, // wraps + {7, 3}, // wraps + } + for _, tt := range tests { + d.SetRotation(tt.input) + if d.rotation != tt.want { + t.Errorf("SetRotation(%d): rotation = %d, want %d", tt.input, d.rotation, tt.want) + } + } +} + +func TestConfigure(t *testing.T) { + d, _ := newTestDevice() + d.Configure(Config{Rotation: 2}) + if d.rotation != 2 { + t.Errorf("Configure(Rotation:2): rotation = %d, want 2", d.rotation) + } +} + +func TestDisplayEmptyBuffer(t *testing.T) { + d, states := newTestDevice() + + err := d.Display() + if err != nil { + t.Fatalf("Display() error: %v", err) + } + + // All pins should be floating after displaying an empty buffer. + for i, s := range states { + if s.isOutput { + t.Errorf("pin %d still in output mode after empty Display()", i) + } + } +} + +func TestDisplaySinglePixel(t *testing.T) { + d, states := newTestDevice() + on := color.RGBA{R: 255, G: 255, B: 255, A: 255} + + // LED index 0 -> pinMapping[0] = {0, 1}: pin 0 high, pin 1 low. + d.SetPixel(0, 0, on) + err := d.Display() + if err != nil { + t.Fatalf("Display() error: %v", err) + } + + // After Display completes, all pins should be floating (last LED turned off). + for i, s := range states { + if s.isOutput { + t.Errorf("pin %d still in output mode after Display()", i) + } + } +} + +func TestDisplayMultiplePixels(t *testing.T) { + d, states := newTestDevice() + on := color.RGBA{R: 255, G: 255, B: 255, A: 255} + + d.SetPixel(0, 0, on) // idx 0 -> pins {0,1} + d.SetPixel(1, 0, on) // idx 1 -> pins {1,0} + d.SetPixel(2, 0, on) // idx 2 -> pins {0,2} + + err := d.Display() + if err != nil { + t.Fatalf("Display() error: %v", err) + } + + // All pins floating after display completes. + for i, s := range states { + if s.isOutput { + t.Errorf("pin %d still in output mode after Display()", i) + } + } +} + +// pinEvent records a single pin action during Display(). +type pinEvent struct { + pinIdx int + action string // "high", "low", or "float" +} + +// traceDevice creates a device that records every pin event for verification. +func traceDevice() (Device, *[]pinEvent) { + var pins [numPins]CharlieplexPin + events := &[]pinEvent{} + for i := range pins { + idx := i + pins[i] = CharlieplexPin{ + Set: pin.OutputFunc(func(level bool) { + action := "low" + if level { + action = "high" + } + *events = append(*events, pinEvent{pinIdx: idx, action: action}) + }), + Float: func() { + *events = append(*events, pinEvent{pinIdx: idx, action: "float"}) + }, + } + } + d := New(pins) + return d, events +} + +func TestDisplayDrivesCorrectPins(t *testing.T) { + d, events := traceDevice() + on := color.RGBA{R: 255, A: 255} + + // Set pixel at (0,0) -> LED index 0 -> pinMapping[0] = {0, 1}. + d.SetPixel(0, 0, on) + d.Display() + + // Expected sequence: + // 1. clearDisplay: float pins 0..10 + // 2. Drive LED 0: pin 0 high, pin 1 low + // 3. Cleanup: float pin 0, float pin 1 + + // Find the high/low events (skip initial floats from clearDisplay). + var driveEvents []pinEvent + for _, e := range *events { + if e.action == "high" || e.action == "low" { + driveEvents = append(driveEvents, e) + } + } + + if len(driveEvents) != 2 { + t.Fatalf("expected 2 drive events, got %d: %v", len(driveEvents), driveEvents) + } + if driveEvents[0].pinIdx != 0 || driveEvents[0].action != "high" { + t.Errorf("first drive event = %v, want pin 0 high", driveEvents[0]) + } + if driveEvents[1].pinIdx != 1 || driveEvents[1].action != "low" { + t.Errorf("second drive event = %v, want pin 1 low", driveEvents[1]) + } +} + +func TestDisplaySkipsBlackPixels(t *testing.T) { + d, events := traceDevice() + on := color.RGBA{R: 255, A: 255} + + // Only set one pixel in the middle of the matrix. + d.SetPixel(4, 1, on) // idx = 1*13+4 = 17 -> pinMapping[17] = {4,2} + d.Display() + + var driveEvents []pinEvent + for _, e := range *events { + if e.action == "high" || e.action == "low" { + driveEvents = append(driveEvents, e) + } + } + + // Should only drive one LED's worth of pin events. + if len(driveEvents) != 2 { + t.Fatalf("expected 2 drive events for 1 lit pixel, got %d", len(driveEvents)) + } + if driveEvents[0].pinIdx != 4 || driveEvents[0].action != "high" { + t.Errorf("expected pin 4 high, got %v", driveEvents[0]) + } + if driveEvents[1].pinIdx != 2 || driveEvents[1].action != "low" { + t.Errorf("expected pin 2 low, got %v", driveEvents[1]) + } +} + +func TestDisplayFloatsBetweenLEDs(t *testing.T) { + d, events := traceDevice() + on := color.RGBA{R: 255, A: 255} + + d.SetPixel(0, 0, on) // idx 0 -> {0,1} + d.SetPixel(1, 0, on) // idx 1 -> {1,0} + d.Display() + + // After the initial clearDisplay floats, the sequence for two LEDs should be: + // drive LED0 (pin0 high, pin1 low) + // float pin0, float pin1 (between LEDs) + // drive LED1 (pin1 high, pin0 low) + // float pin1, float pin0 (cleanup) + + // Skip the initial 11 float events from clearDisplay. + postClear := (*events)[numPins:] + + // Verify pin 0 and 1 are floated between the two LEDs. + foundFloatBetween := false + driveCount := 0 + for _, e := range postClear { + if e.action == "high" || e.action == "low" { + driveCount++ + } + // After the first pair of drive events, we should see floats before the next pair. + if driveCount == 2 && e.action == "float" { + foundFloatBetween = true + break + } + } + if !foundFloatBetween { + t.Error("expected float events between LED drives, found none") + } +} + +func TestPinMappingLength(t *testing.T) { + expected := 104 // 8x13 matrix = 104 LEDs + if len(pinMapping) != expected { + t.Errorf("pinMapping has %d entries, want %d", len(pinMapping), expected) + } +} + +func TestPinMappingIndicesInRange(t *testing.T) { + for i, pair := range pinMapping { + if pair[0] >= numPins { + t.Errorf("pinMapping[%d][0] = %d, exceeds numPins (%d)", i, pair[0], numPins) + } + if pair[1] >= numPins { + t.Errorf("pinMapping[%d][1] = %d, exceeds numPins (%d)", i, pair[1], numPins) + } + if pair[0] == pair[1] { + t.Errorf("pinMapping[%d] has same pin for both: %d", i, pair[0]) + } + } +} diff --git a/unoqmatrix/matrix_tinygo.go b/unoqmatrix/matrix_tinygo.go new file mode 100644 index 0000000..148e6a5 --- /dev/null +++ b/unoqmatrix/matrix_tinygo.go @@ -0,0 +1,36 @@ +//go:build baremetal + +package unoqmatrix + +import ( + "machine" + + pin "tinygo.org/x/drivers/internal/pin" +) + +// NewFromBasePin creates a Device from a base machine.Pin. +// It constructs 11 CharlieplexPin values from consecutive pins starting at basePin. +// Each pin lazily switches between output and input mode as needed. +func NewFromBasePin(basePin machine.Pin) Device { + var pins [numPins]CharlieplexPin + for i := range pins { + p := basePin + machine.Pin(i) + var isOutput bool + pins[i] = CharlieplexPin{ + Set: pin.OutputFunc(func(level bool) { + if !isOutput { + p.Configure(machine.PinConfig{Mode: machine.PinOutput}) + isOutput = true + } + p.Set(level) + }), + Float: func() { + if isOutput { + p.Configure(machine.PinConfig{Mode: machine.PinInput}) + isOutput = false + } + }, + } + } + return New(pins) +} diff --git a/version.go b/version.go index 2f0b54b..0b44de9 100644 --- a/version.go +++ b/version.go @@ -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.26.0" +const Version = "0.35.0" diff --git a/w5500/io.go b/w5500/io.go new file mode 100644 index 0000000..479c06c --- /dev/null +++ b/w5500/io.go @@ -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) +} diff --git a/w5500/netdev.go b/w5500/netdev.go new file mode 100644 index 0000000..d64a16a --- /dev/null +++ b/w5500/netdev.go @@ -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 +} diff --git a/w5500/registers.go b/w5500/registers.go new file mode 100644 index 0000000..0ff1b09 --- /dev/null +++ b/w5500/registers.go @@ -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 +) diff --git a/w5500/w5500.go b/w5500/w5500.go new file mode 100644 index 0000000..d63c812 --- /dev/null +++ b/w5500/w5500.go @@ -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 +} diff --git a/waveshare-epd/epd1in54/epd1in54.go b/waveshare-epd/epd1in54/epd1in54.go new file mode 100644 index 0000000..120b356 --- /dev/null +++ b/waveshare-epd/epd1in54/epd1in54.go @@ -0,0 +1,424 @@ +// Package epd1in54 implements a driver for Waveshare 1.54in black and white e-paper device. +// +// Derived from: +// +// https://github.com/tinygo-org/drivers/tree/master/waveshare-epd +// https://github.com/waveshare/e-Paper/blob/master/Arduino/epd1in54_V2/epd1in54_V2.cpp +// +// Datasheet: https://www.waveshare.com/w/upload/e/e5/1.54inch_e-paper_V2_Datasheet.pdf +package epd1in54 + +import ( + "image/color" + "machine" + "time" +) + +type Config struct { + Width int16 + Height int16 + LogicalWidth int16 + Rotation Rotation +} + +type Device struct { + bus *machine.SPI + cs machine.Pin + dc machine.Pin + rst machine.Pin + busy machine.Pin + + buffer []uint8 + rotation Rotation +} + +type Rotation uint8 + +var fullRefresh = [159]uint8{ + 0x80, 0x48, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x48, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x80, 0x48, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x48, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x8, 0x1, 0x0, 0x8, 0x1, 0x0, 0x2, + 0xA, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, + 0x22, 0x17, 0x41, 0x0, 0x32, 0x20, +} + +var partialRefresh = [159]uint8{ + 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, + 0x02, 0x17, 0x41, 0xB0, 0x32, 0x28, +} + +// New returns a new epd1in54 driver. Pass in a fully configured SPI bus. +func New(bus *machine.SPI, csPin, dcPin, rstPin, busyPin machine.Pin) Device { + return Device{ + buffer: make([]uint8, (uint32(Width)*uint32(Height))/8), + bus: bus, + cs: csPin, + dc: dcPin, + rst: rstPin, + busy: busyPin, + } +} + +func (d *Device) LDirInit(cfg Config) { + d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.rst.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.dc.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.busy.Configure(machine.PinConfig{Mode: machine.PinInput}) + + d.bus.Configure(machine.SPIConfig{ + Frequency: 2000000, + Mode: 0, + LSBFirst: false, + }) + + d.Reset() + d.WaitUntilIdle() + + d.SendCommand(0x12) + d.WaitUntilIdle() + + d.SendCommand(0x01) + d.SendData(0xC7) + d.SendData(0x00) + d.SendData(0x00) + + d.SendCommand(0x11) + d.SendData(0x03) + + d.SendCommand(0x44) + /* x point must be the multiple of 8 or the last 3 bits will be ignored */ + d.SendData((0 >> 3) & 0xFF) + d.SendData((199 >> 3) & 0xFF) + + d.SendCommand(0x45) + d.SendData(0 & 0xFF) + d.SendData((0 >> 8) & 0xFF) + d.SendData(199 & 0xFF) + d.SendData((199 >> 8) & 0xFF) + + d.SendCommand(0x3C) + d.SendData(0x01) + + d.SendCommand(0x18) + d.SendData(0x80) + + d.SendCommand(0x22) + d.SendData(0xB1) + + d.SendCommand(0x20) + + d.SendCommand(0x4E) + d.SendData(0x00) + + d.SendCommand(0x4F) + d.SendData(0xC7) + d.SendData(0x00) + + d.WaitUntilIdle() + d.setLUT(fullRefresh) +} + +func (d *Device) HDirInit(cfg Config) { + d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.rst.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.dc.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.busy.Configure(machine.PinConfig{Mode: machine.PinInput}) + + d.bus.Configure(machine.SPIConfig{ + Frequency: 2000000, + Mode: 0, + LSBFirst: false, + }) + + d.Reset() + d.WaitUntilIdle() + + d.SendCommand(0x12) + d.WaitUntilIdle() + + d.SendCommand(0x01) + d.SendData(0xC7) + d.SendData(0x00) + d.SendData(0x01) + + d.SendCommand(0x11) + d.SendData(0x01) + + d.SendCommand(0x44) + d.SendData(0x00) + d.SendData(0x18) + + d.SendCommand(0x45) + d.SendData(0xC7) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + + d.SendCommand(0x3C) + d.SendData(0x01) + + d.SendCommand(0x18) + d.SendData(0x80) + + d.SendCommand(0x22) + d.SendData(0xB1) + + d.SendCommand(0x20) + + d.SendCommand(0x4E) + d.SendData(0x00) + + d.SendCommand(0x4F) + d.SendData(0xC7) + d.SendData(0x00) + + d.WaitUntilIdle() + d.setLUT(fullRefresh) +} + +func (d *Device) setLUT(lut [159]uint8) { + d.SendCommand(0x32) + for i := 0; i < 153; i++ { + d.SendData(lut[i]) + } + d.WaitUntilIdle() + + d.SendCommand(0x3F) + d.SendData(lut[153]) + + d.SendCommand(0x03) + d.SendData(lut[154]) + + d.SendCommand(0x04) + d.SendData(lut[155]) + d.SendData(lut[156]) + d.SendData(lut[157]) + + d.SendCommand(0x2C) + d.SendData(lut[158]) +} + +// Reset resets the display. +func (d *Device) Reset() { + d.rst.High() + time.Sleep(20 * time.Millisecond) + d.rst.Low() + time.Sleep(5 * time.Millisecond) + d.rst.High() + time.Sleep(20 * time.Millisecond) +} + +// SendCommand sends a command to the display +func (d *Device) SendCommand(command uint8) { + d.sendDataCommand(true, command) +} + +// SendData sends a data byte to the display +func (d *Device) SendData(data uint8) { + d.sendDataCommand(false, data) +} + +// sendDataCommand sends image data or a command to the screen +func (d *Device) sendDataCommand(isCommand bool, data uint8) { + if isCommand { + d.dc.Low() + } else { + d.dc.High() + } + d.cs.Low() + d.bus.Transfer(data) + d.cs.High() +} + +// SetPixel modifies the internal buffer in a single pixel. +// The display have 2 colors: black and white +// We use RGBA(0,0,0, 255) as white (transparent) +// Anything else as black +func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { + x, y = d.xy(x, y) + if x < 0 || x >= Width || y < 0 || y >= Height { + return + } + byteIndex := (uint32(x) + uint32(y)*uint32(Width)) / 8 + if c.R == 0 && c.G == 0 && c.B == 0 { // TRANSPARENT / WHITE + d.buffer[byteIndex] |= 0x80 >> uint8(x%8) + } else { // WHITE / EMPTY + d.buffer[byteIndex] &^= 0x80 >> uint8(x%8) + } +} + +func (d *Device) DisplayImage(image []uint8) { + var w, h int + if Width%8 == 0 { + w = int(Width / 8) + } else { + w = int(Width/8 + 1) + } + h = int(Height) + + d.SendCommand(0x24) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + d.SendData(image[i+j*w]) + } + } + + d.SendCommand(0x26) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + d.SendData(image[i+j*w]) + } + } + + d.displayFrame() +} + +func (d *Device) Display() error { + var w, h int + if Width%8 == 0 { + w = int(Width / 8) + } else { + w = int(Width/8 + 1) + } + h = int(Height) + + d.SendCommand(0x24) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + x := i + j*w + d.SendData(d.buffer[x]) + } + } + + d.SendCommand(0x26) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + x := i + j*w + d.SendData(d.buffer[x]) + } + } + + d.displayFrame() + + return nil +} + +func (d *Device) displayFrame() { + d.SendCommand(0x22) + d.SendData(0xC7) + d.SendCommand(0x20) + d.WaitUntilIdle() +} + +func (d *Device) Clear() { + var w, h int + if Width%8 == 0 { + w = int(Width / 8) + } else { + w = int(Width/8 + 1) + } + h = int(Height) + + d.SendCommand(0x24) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + d.SendData(0xff) + } + } + + d.SendCommand(0x26) + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + d.SendData(0xff) + } + } + + d.displayFrame() +} + +// WaitUntilIdle waits until the display is ready +func (d *Device) WaitUntilIdle() { + for d.busy.Get() { + time.Sleep(100 * time.Millisecond) + } + time.Sleep(200 * time.Millisecond) +} + +// IsBusy returns the busy status of the display +func (d *Device) IsBusy() bool { + return d.busy.Get() +} + +// ClearBuffer sets the buffer to 0xFF (white) +func (d *Device) ClearBuffer() { + for i := 0; i < len(d.buffer); i++ { + d.buffer[i] = 0xFF + } +} + +// Size returns the current size of the display. +func (d *Device) Size() (w, h int16) { + if d.rotation == ROTATION_90 || d.rotation == ROTATION_270 { + return Height, Width + } + return Width, Height +} + +// SetRotation changes the rotation (clock-wise) of the device +func (d *Device) SetRotation(rotation Rotation) { + d.rotation = rotation +} + +// xy chages the coordinates according to the rotation +func (d *Device) xy(x, y int16) (int16, int16) { + switch d.rotation { + case NO_ROTATION: + return x, y + case ROTATION_90: + return Width - y - 1, x + case ROTATION_180: + return Width - x - 1, Height - y - 1 + case ROTATION_270: + return y, Height - x - 1 + } + return x, y +} + +func (d *Device) Sleep() { + d.SendCommand(0x10) + d.SendData(0x01) + time.Sleep(200 * time.Millisecond) + + d.rst.Low() +} diff --git a/waveshare-epd/epd1in54/registers.go b/waveshare-epd/epd1in54/registers.go new file mode 100644 index 0000000..ba09c8d --- /dev/null +++ b/waveshare-epd/epd1in54/registers.go @@ -0,0 +1,52 @@ +package epd1in54 + +// Derived from https://github.com/waveshare/e-Paper/blob/master/Arduino/epd4in2/epd4in2.h + +const ( + Width = 200 + Height = 200 + + PANEL_SETTING = 0x00 + POWER_SETTING = 0x01 + POWER_OFF = 0x02 + POWER_OFF_SEQUENCE_SETTING = 0x03 + POWER_ON = 0x04 + POWER_ON_MEASURE = 0x05 + BOOSTER_SOFT_START = 0x06 + DEEP_SLEEP = 0x07 + DATA_START_TRANSMISSION_1 = 0x10 + DATA_STOP = 0x11 + DISPLAY_REFRESH = 0x12 + DATA_START_TRANSMISSION_2 = 0x13 + LUT_FOR_VCOM = 0x20 + LUT_WHITE_TO_WHITE = 0x21 + LUT_BLACK_TO_WHITE = 0x22 + LUT_WHITE_TO_BLACK = 0x23 + LUT_BLACK_TO_BLACK = 0x24 + PLL_CONTROL = 0x30 + TEMPERATURE_SENSOR_COMMAND = 0x40 + TEMPERATURE_SENSOR_SELECTION = 0x41 + TEMPERATURE_SENSOR_WRITE = 0x42 + TEMPERATURE_SENSOR_READ = 0x43 + VCOM_AND_DATA_INTERVAL_SETTING = 0x50 + LOW_POWER_DETECTION = 0x51 + TCON_SETTING = 0x60 + RESOLUTION_SETTING = 0x61 + GSST_SETTING = 0x65 + GET_STATUS = 0x71 + AUTO_MEASUREMENT_VCOM = 0x80 + READ_VCOM_VALUE = 0x81 + VCM_DC_SETTING = 0x82 + PARTIAL_WINDOW = 0x90 + PARTIAL_IN = 0x91 + PARTIAL_OUT = 0x92 + PROGRAM_MODE = 0xA0 + ACTIVE_PROGRAMMING = 0xA1 + READ_OTP = 0xA2 + POWER_SAVING = 0xE3 + + NO_ROTATION Rotation = 0 + ROTATION_90 Rotation = 1 + ROTATION_180 Rotation = 2 + ROTATION_270 Rotation = 3 +) diff --git a/waveshare-epd/epd2in66b/dev.go b/waveshare-epd/epd2in66b/dev.go new file mode 100644 index 0000000..cadff9c --- /dev/null +++ b/waveshare-epd/epd2in66b/dev.go @@ -0,0 +1,287 @@ +// Package epd2in66b implements a driver for the Waveshare 2.66inch E-Paper E-Ink Display Module (B) +// for Raspberry Pi Pico, 296×152, Red / Black / White +// Datasheet: https://files.waveshare.com/upload/e/ec/2.66inch-e-paper-b-specification.pdf +package epd2in66b + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers" +) + +const ( + displayWidth = 152 + displayHeight = 296 +) + +const Baudrate = 4_000_000 // 4 MHz + +type Config struct { + ResetPin machine.Pin + DataPin machine.Pin + ChipSelectPin machine.Pin + BusyPin machine.Pin +} + +type Device struct { + bus drivers.SPI + cs machine.Pin + dc machine.Pin + rst machine.Pin + busy machine.Pin + + blackBuffer []byte + redBuffer []byte +} + +// New allocates a new device. +// The bus is expected to be configured and ready for use. +func New(bus drivers.SPI) Device { + pixelCount := displayWidth * displayHeight + + bufLen := pixelCount / 8 + + return Device{ + bus: bus, + blackBuffer: make([]byte, bufLen), + redBuffer: make([]byte, bufLen), + } +} + +// Configure configures the device and its pins. +func (d *Device) Configure(c Config) error { + d.cs = c.ChipSelectPin + d.dc = c.DataPin + d.rst = c.ResetPin + d.busy = c.BusyPin + + d.cs.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.dc.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.rst.Configure(machine.PinConfig{Mode: machine.PinOutput}) + d.busy.Configure(machine.PinConfig{Mode: machine.PinInput}) + + return nil +} + +func (d *Device) Size() (x, y int16) { + return displayWidth, displayHeight +} + +// SetPixel modifies the internal buffer in a single pixel. +// The display has 3 colors: red, black and white +// +// - white = RGBA(255,255,255, 1-255) +// - red = RGBA(1-255,0,0,1-255) +// - Anything else as black +func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { + if x < 0 || x >= displayWidth || y < 0 || y >= displayHeight { + return + } + + bytePos, bitPos := pos(x, y, displayWidth) + + if c.R == 0xff && c.G == 0xff && c.B == 0xff && c.A > 0 { // white + set(d.blackBuffer, bytePos, bitPos) + unset(d.redBuffer, bytePos, bitPos) + } else if c.R != 0 && c.G == 0 && c.B == 0 && c.A > 0 { // red-ish + set(d.blackBuffer, bytePos, bitPos) + set(d.redBuffer, bytePos, bitPos) + } else { // black or other + unset(d.blackBuffer, bytePos, bitPos) + unset(d.redBuffer, bytePos, bitPos) + } +} + +func set(buf []byte, bytePos, bitPos int) { + buf[bytePos] |= 0x1 << bitPos +} + +func unset(buf []byte, bytePos, bitPos int) { + buf[bytePos] &^= 0x1 << bitPos +} + +func pos(x, y, stride int16) (bytePos int, bitPos int) { + p := int(x) + int(y)*int(stride) + bytePos = p / 8 + + // reverse bit position as it is reversed on the device's buffer + bitPos = 7 - p%8 + + return bytePos, bitPos +} + +func (d *Device) Display() error { + // Write RAM (Black White) / RAM 0x24 + // 1 == white, 0 == black + if err := d.sendCommandByte(0x24); err != nil { + return err + } + + if err := d.sendData(d.blackBuffer); err != nil { + return err + } + + // Write RAM (RED) / RAM 0x26) + // 0 == blank, 1 == red + if err := d.sendCommandByte(0x26); err != nil { + return err + } + + if err := d.sendData(d.redBuffer); err != nil { + return err + } + + return d.turnOnDisplay() +} + +func (d *Device) ClearBuffer() { + fill(d.redBuffer, 0x00) + fill(d.blackBuffer, 0xff) +} + +func (d *Device) turnOnDisplay() error { + // also documented as 'Master Activation' + if err := d.sendCommandByte(0x20); err != nil { + return err + } + d.WaitUntilIdle() + return nil +} + +func (d *Device) Reset() error { + d.hwReset() + d.WaitUntilIdle() + + // soft reset & set defaults + if err := d.sendCommandByte(0x12); err != nil { + return err + } + d.WaitUntilIdle() + + // data entry mode setting + if err := d.sendCommandSequence([]byte{0x11, 0x03}); err != nil { + return err + } + + if err := d.setWindow(0, displayWidth-1, 0, displayHeight-1); err != nil { + return err + } + + // display update control 1 - resolution setting + if err := d.sendCommandSequence([]byte{0x21, 0x00, 0x80}); err != nil { + return err + } + + if err := d.setCursor(0, 0); err != nil { + return err + } + d.WaitUntilIdle() + + return nil +} + +func (d *Device) setCursor(x, y uint16) error { + // Set RAM X address counter + if err := d.sendCommandSequence([]byte{0x4e, byte(x & 0x1f)}); err != nil { + return err + } + + // Set RAM Y address counter + yLo := byte(y) + yHi := byte(y>>8) & 0x1 + if err := d.sendCommandSequence([]byte{0x4f, yLo, yHi}); err != nil { + return err + } + + return nil +} + +func (d *Device) hwReset() { + d.rst.High() + time.Sleep(50 * time.Millisecond) + d.rst.Low() + time.Sleep(2 * time.Millisecond) + d.rst.High() + time.Sleep(50 * time.Millisecond) +} + +func (d *Device) setWindow(xstart, xend, ystart, yend int16) error { + // set RAM X-address start / end position + d1 := byte((xstart >> 3) & 0x1f) + d2 := byte((xend >> 3) & 0x1f) + if err := d.sendCommandSequence([]byte{0x44, d1, d2}); err != nil { + return err + } + + // set RAM Y-address start / end position + ystartLo := byte(ystart) + ystartHi := byte(ystart>>8) & 0x1 + + yendLo := byte(yend) + yendHi := byte(yend>>8) & 0x1 + + return d.sendCommandSequence([]byte{0x45, ystartLo, ystartHi, yendLo, yendHi}) +} + +func (d *Device) WaitUntilIdle() { + // give it some time to get busy + time.Sleep(50 * time.Millisecond) + + for d.busy.Get() { // high = busy + time.Sleep(10 * time.Millisecond) + } + + // give it some extra time + time.Sleep(50 * time.Millisecond) +} + +// sendCommandSequence sends the first byte in the buffer as a 'command' and all following bytes as data +func (d *Device) sendCommandSequence(seq []byte) error { + err := d.sendCommandByte(seq[0]) + if err != nil { + return err + } + + for i := 1; i < len(seq); i++ { + err = d.sendDataByte(seq[i]) + if err != nil { + return err + } + } + + return nil +} + +func (d *Device) sendCommandByte(b byte) error { + d.dc.Low() + d.cs.Low() + _, err := d.bus.Transfer(b) + d.cs.High() + return err +} + +func (d *Device) sendDataByte(b byte) error { + d.dc.High() + d.cs.Low() + _, err := d.bus.Transfer(b) + d.cs.High() + return err +} + +func (d *Device) sendData(b []byte) error { + d.dc.High() + d.cs.Low() + err := d.bus.Tx(b, nil) + d.cs.High() + return err +} + +// fill quickly fills a slice with a given value +func fill(s []byte, b byte) { + s[0] = b + for j := 1; j < len(s); j *= 2 { + copy(s[j:], s[:j]) + } +} diff --git a/waveshare-epd/epd2in66b/dev_pico.go b/waveshare-epd/epd2in66b/dev_pico.go new file mode 100644 index 0000000..eb18d1e --- /dev/null +++ b/waveshare-epd/epd2in66b/dev_pico.go @@ -0,0 +1,34 @@ +//go:build pico + +package epd2in66b + +import ( + "machine" +) + +// DefaultConfig contains the default config for the https://www.waveshare.com/wiki/Pico-ePaper-2.66 module +var DefaultConfig = Config{ + DataPin: machine.GP8, + ChipSelectPin: machine.GP9, + ResetPin: machine.GP12, + BusyPin: machine.GP13, +} + +// NewPicoModule allocates a new device backed by the https://www.waveshare.com/wiki/Pico-ePaper-2.66 module +// This will also configure the SPI1 bus and configure the device with the DefaultConfig +func NewPicoModule() (Device, error) { + spi := machine.SPI1 + + if err := spi.Configure(machine.SPIConfig{ + Frequency: Baudrate, + }); err != nil { + return Device{}, err + } + + dev := New(spi) + if err := dev.Configure(DefaultConfig); err != nil { + return dev, err + } + + return dev, nil +} diff --git a/waveshare-epd/epd2in66b/dev_test.go b/waveshare-epd/epd2in66b/dev_test.go new file mode 100644 index 0000000..e970d9a --- /dev/null +++ b/waveshare-epd/epd2in66b/dev_test.go @@ -0,0 +1,92 @@ +package epd2in66b + +import ( + _ "embed" + "fmt" + "image" + "image/color" + "image/draw" + "image/png" + "os" + "testing" + "time" + + "tinygo.org/x/drivers" + "tinygo.org/x/tinyfont" + "tinygo.org/x/tinyfont/freemono" +) + +type mockBus struct{} + +func (m *mockBus) Tx(w, r []byte) error { + return nil +} + +func (m *mockBus) Transfer(b byte) (byte, error) { + return 0, nil +} + +func TestBufferDrawing(t *testing.T) { + dev := New(&mockBus{}) + + tinyfont.WriteLine(&dev, &freemono.Bold9pt7b, 10, 40, "Hello World!", color.RGBA{0xff, 0xff, 0xff, 0xff}) + + red := color.RGBA{0xff, 0, 0, 0xff} + black := color.RGBA{0xff, 0xff, 0xff, 0xff} + showRect(&dev, 10, 10, 10, 10, black) + showRect(&dev, 10, 20, 10, 10, red) + + img := toImage(&dev) + writeImage(img) +} + +func toImage(dev *Device) *image.RGBA { + red := color.RGBA{0xff, 0, 0, 0xff} + + xMax, yMax := dev.Size() + r := image.Rect(0, 0, int(xMax), int(yMax)) + container := image.NewRGBA(r) + draw.Draw(container, container.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Over) + + for x := 0; x < int(xMax); x++ { + for y := 0; y < int(yMax); y++ { + + bytePos, bitPos := pos(int16(x), int16(y), displayWidth) + + if isSet(dev.redBuffer, bytePos, bitPos) { + container.Set(x, y, red) + } else if isSet(dev.blackBuffer, bytePos, bitPos) { + container.Set(x, y, color.Black) + } + } + } + + return container +} + +func isSet(buf []byte, bytePos, bitPos int) bool { + return (buf[bytePos])&(0x1<> 8)) + d.SendData(0x00) + + d.SendCommand(DATA_ENTRY_MODE) + d.SendData(0x03) + + d.setWindow(0, 0, d.width-1, d.height-1) + + if cfg.Speed == SPEED_FAST { + d.SendCommand(BORDER_WAVEFORM_CONTROL) + d.SendData(0x05) + } + + d.SendCommand(DISPLAY_UPDATE_CONTROL_1) + d.SendData(0x00) + d.SendData(0x80) + + d.setCursor(0, 0) + d.WaitUntilIdle() + + switch cfg.Speed { + case SPEED_FAST: + d.setLUTByHost(&lutFast) + default: + d.setLUTByHost(&lutDefault) + } +} + +// HardwareReset resets the device via the RST pin. +func (d *Device) Reset() { + d.rst.High() + time.Sleep(10 * time.Millisecond) + d.rst.Low() + time.Sleep(2 * time.Millisecond) + d.rst.High() + time.Sleep(10 * time.Millisecond) +} + +// SendCommand sends a command byte to the display. +func (d *Device) SendCommand(command uint8) { + d.dc.Low() + d.cs.Low() + d.bus.Transfer(command) + d.cs.High() +} + +// SendData sends a data byte to the display. +func (d *Device) SendData(data uint8) { + d.dc.High() + d.cs.Low() + d.bus.Transfer(data) + d.cs.High() +} + +// WaitUntilIdle waits until the display is ready. +// On SSD1680, BUSY pin is HIGH when busy, LOW when idle. +func (d *Device) WaitUntilIdle() { + for d.busy.Get() { + time.Sleep(50 * time.Millisecond) + } + time.Sleep(50 * time.Millisecond) +} + +// IsBusy returns the busy status of the display. +func (d *Device) IsBusy() bool { + return d.busy.Get() +} + +// SetPixel modifies the internal buffer in a single pixel. +// Uses color.RGBA where black (0,0,0) = black pixel, anything else = white pixel. +func (d *Device) SetPixel(x int16, y int16, c color.RGBA) { + x, y = d.xy(x, y) + + if x < 0 || x >= d.width || y < 0 || y >= d.height { + return + } + byteIndex := (y * (d.width / 8)) + (x / 8) + if c.R == 0 && c.G == 0 && c.B == 0 { + d.buffer[byteIndex] &^= 0x80 >> uint8(x%8) + } else { + d.buffer[byteIndex] |= 0x80 >> uint8(x%8) + } +} + +// Display sends the buffer to the screen. +func (d *Device) Display() error { + if d.blocking { + d.WaitUntilIdle() + } + + d.setCursor(0, 0) + d.SendCommand(WRITE_RAM_BW) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(d.buffer[i]) + } + + d.turnOnDisplay() + + if d.blocking { + d.WaitUntilIdle() + } + return nil +} + +// DisplayWithBase writes the buffer to both BW and RED RAM then refreshes. +// This is useful before partial updates to set the base image. +func (d *Device) DisplayWithBase() error { + if d.blocking { + d.WaitUntilIdle() + } + + d.setCursor(0, 0) + d.SendCommand(WRITE_RAM_BW) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(d.buffer[i]) + } + + d.setCursor(0, 0) + d.SendCommand(WRITE_RAM_RED) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(d.buffer[i]) + } + + d.turnOnDisplay() + + if d.blocking { + d.WaitUntilIdle() + } + return nil +} + +// DisplayPartial performs a partial refresh of the display. +// Call DisplayWithBase first to set the base image before using partial updates. +func (d *Device) DisplayPartial() error { + d.rst.Low() + time.Sleep(1 * time.Millisecond) + d.rst.High() + time.Sleep(2 * time.Millisecond) + + d.setLUT(&lutPartial) + + d.SendCommand(OTP_SELECTION_CONTROL) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x40) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + d.SendData(0x00) + + d.SendCommand(BORDER_WAVEFORM_CONTROL) + d.SendData(0x80) + + d.SendCommand(DISPLAY_UPDATE_CONTROL_2) + d.SendData(0xC0) + d.SendCommand(MASTER_ACTIVATION) + d.WaitUntilIdle() + + d.setWindow(0, 0, d.width-1, d.height-1) + d.setCursor(0, 0) + + d.SendCommand(WRITE_RAM_BW) + for i := uint32(0); i < d.bufferLength; i++ { + d.SendData(d.buffer[i]) + } + + d.turnOnDisplayPartial() + d.WaitUntilIdle() + return nil +} + +// ClearDisplay erases the display. +func (d *Device) ClearDisplay() { + d.ClearBuffer() + d.Display() +} + +// ClearBuffer sets the buffer to 0xFF (white). +func (d *Device) ClearBuffer() { + for i := uint32(0); i < d.bufferLength; i++ { + d.buffer[i] = 0xFF + } +} + +// Size returns the current size of the display. +func (d *Device) Size() (w, h int16) { + if d.rotation == ROTATION_90 || d.rotation == ROTATION_270 { + return d.height, d.width + } + return d.width, d.height +} + +// SetRotation changes the rotation (clock-wise) of the device. +func (d *Device) SetRotation(rotation Rotation) { + d.rotation = rotation +} + +// SetBlocking changes the blocking flag of the device. +func (d *Device) SetBlocking(blocking bool) { + d.blocking = blocking +} + +// SetSpeed changes the refresh speed and reconfigures the device. +func (d *Device) SetSpeed(speed Speed) { + d.Configure(Config{ + Width: d.width, + Height: d.height, + Rotation: d.rotation, + Speed: speed, + Blocking: d.blocking, + }) +} + +// Sleep puts the display into deep sleep mode. A hardware reset is needed to wake it. +func (d *Device) Sleep() { + d.SendCommand(DEEP_SLEEP_MODE) + d.SendData(0x01) + time.Sleep(100 * time.Millisecond) +} + +// PowerOff disables the display analog/clock. Lighter than Sleep. +func (d *Device) PowerOff() { + d.SendCommand(DISPLAY_UPDATE_CONTROL_2) + d.SendData(0x03) + d.SendCommand(MASTER_ACTIVATION) + d.WaitUntilIdle() +} + +func (d *Device) xy(x, y int16) (int16, int16) { + switch d.rotation { + case NO_ROTATION: + return x, y + case ROTATION_90: + return d.width - y - 1, x + case ROTATION_180: + return d.width - x - 1, d.height - y - 1 + case ROTATION_270: + return y, d.height - x - 1 + } + return x, y +} + +func (d *Device) setWindow(xStart, yStart, xEnd, yEnd int16) { + d.SendCommand(SET_RAM_X_ADDRESS) + d.SendData(uint8((xStart >> 3) & 0xFF)) + d.SendData(uint8((xEnd >> 3) & 0xFF)) + + d.SendCommand(SET_RAM_Y_ADDRESS) + d.SendData(uint8(yStart & 0xFF)) + d.SendData(uint8((yStart >> 8) & 0xFF)) + d.SendData(uint8(yEnd & 0xFF)) + d.SendData(uint8((yEnd >> 8) & 0xFF)) +} + +func (d *Device) setCursor(x, y int16) { + d.SendCommand(SET_RAM_X_COUNTER) + d.SendData(uint8(x & 0xFF)) + + d.SendCommand(SET_RAM_Y_COUNTER) + d.SendData(uint8(y & 0xFF)) + d.SendData(uint8((y >> 8) & 0xFF)) +} + +func (d *Device) turnOnDisplay() { + d.SendCommand(DISPLAY_UPDATE_CONTROL_2) + d.SendData(0xC7) + d.SendCommand(MASTER_ACTIVATION) + d.WaitUntilIdle() +} + +func (d *Device) turnOnDisplayPartial() { + d.SendCommand(DISPLAY_UPDATE_CONTROL_2) + d.SendData(0x0F) + d.SendCommand(MASTER_ACTIVATION) + d.WaitUntilIdle() +} + +func (d *Device) setLUT(lut *[159]uint8) { + d.SendCommand(WRITE_LUT_REGISTER) + for i := 0; i < 153; i++ { + d.SendData(lut[i]) + } + d.WaitUntilIdle() +} + +func (d *Device) setLUTByHost(lut *[159]uint8) { + d.setLUT(lut) + + d.SendCommand(END_OPTION) + d.SendData(lut[153]) + + d.SendCommand(GATE_DRIVING_VOLTAGE) + d.SendData(lut[154]) + + d.SendCommand(SOURCE_DRIVING_VOLTAGE) + d.SendData(lut[155]) + d.SendData(lut[156]) + d.SendData(lut[157]) + + d.SendCommand(WRITE_VCOM_REGISTER) + d.SendData(lut[158]) +} diff --git a/waveshare-epd/epd2in9v2/registers.go b/waveshare-epd/epd2in9v2/registers.go new file mode 100644 index 0000000..cfed5be --- /dev/null +++ b/waveshare-epd/epd2in9v2/registers.go @@ -0,0 +1,50 @@ +package epd2in9v2 + +// Commands from SSD1680 datasheet +const ( + EPD_WIDTH = 128 + EPD_HEIGHT = 296 + + DRIVER_OUTPUT_CONTROL = 0x01 + GATE_DRIVING_VOLTAGE = 0x03 + SOURCE_DRIVING_VOLTAGE = 0x04 + DEEP_SLEEP_MODE = 0x10 + DATA_ENTRY_MODE = 0x11 + SW_RESET = 0x12 + MASTER_ACTIVATION = 0x20 + DISPLAY_UPDATE_CONTROL_1 = 0x21 + DISPLAY_UPDATE_CONTROL_2 = 0x22 + WRITE_RAM_BW = 0x24 + WRITE_RAM_RED = 0x26 + VCOM_SENSE = 0x28 + VCOM_SENSE_DURATION = 0x29 + PROGRAM_VCOM_OTP = 0x2A + WRITE_VCOM_CONTROL = 0x2B + WRITE_VCOM_REGISTER = 0x2C + OTP_READ_DISPLAY_OPTION = 0x2D + USER_ID_READ = 0x2E + PROGRAM_WS_OTP = 0x30 + LOAD_WS_OTP = 0x31 + WRITE_LUT_REGISTER = 0x32 + PROGRAM_OTP_SELECTION = 0x36 + OTP_SELECTION_CONTROL = 0x37 + WRITE_USER_ID = 0x38 + OTP_PROGRAM_MODE = 0x39 + BORDER_WAVEFORM_CONTROL = 0x3C + END_OPTION = 0x3F + SET_RAM_X_ADDRESS = 0x44 + SET_RAM_Y_ADDRESS = 0x45 + SET_RAM_X_COUNTER = 0x4E + SET_RAM_Y_COUNTER = 0x4F + SET_ANALOG_BLOCK_CONTROL = 0x74 + SET_DIGITAL_BLOCK_CONTROL = 0x7E + + NO_ROTATION Rotation = 0 + ROTATION_90 Rotation = 1 + ROTATION_180 Rotation = 2 + ROTATION_270 Rotation = 3 + + SPEED_DEFAULT Speed = 0 + SPEED_FAST Speed = 1 + SPEED_PARTIAL Speed = 2 +) diff --git a/wifinina/wifinina.go b/wifinina/wifinina.go index c0c3a66..a70c825 100644 --- a/wifinina/wifinina.go +++ b/wifinina/wifinina.go @@ -33,7 +33,6 @@ var _debug debug = debugBasic //var _debug debug = debugBasic | debugNetdev | debugCmd | debugDetail var ( - version = "0.0.1" driverName = "Tinygo ESP32 Wifi network device driver (WiFiNINA)" ) @@ -48,6 +47,9 @@ const ( statusConnectFailed connectionStatus = 4 statusConnectionLost connectionStatus = 5 statusDisconnected connectionStatus = 6 + statusAPListening connectionStatus = 7 + statusAPConnected connectionStatus = 8 + statusAPFailed connectionStatus = 9 encTypeTKIP encryptionType = 2 encTypeCCMP encryptionType = 4 @@ -163,11 +165,12 @@ type encryptionType uint8 type sock uint8 type hwerr uint8 -type socket struct { - protocol int - laddr netip.AddrPort // Set in Bind() - raddr netip.AddrPort // Set in Connect() - inuse bool +type Socket struct { + protocol int + clientConnected bool + laddr netip.AddrPort // Set in Bind() + raddr netip.AddrPort // Set in Connect() + sock // Device socket, as returned from w.getSocket() } type Config struct { @@ -214,17 +217,13 @@ type wifinina struct { killWatchdog chan bool fault error - sockets map[sock]*socket // keyed by sock as returned by getSocket() -} - -func newSocket(protocol int) *socket { - return &socket{protocol: protocol, inuse: true} + sockets map[int]*Socket // keyed by sockfd } func New(cfg *Config) *wifinina { w := wifinina{ cfg: cfg, - sockets: make(map[sock]*socket), + sockets: make(map[int]*Socket), killWatchdog: make(chan bool), cs: cfg.Cs, ack: cfg.Ack, @@ -303,6 +302,61 @@ func (w *wifinina) connectToAP() error { return netlink.ErrConnectTimeout } +func (w *wifinina) startAP() error { + timeout := w.params.ConnectTimeout + if timeout == 0 { + timeout = netlink.DefaultConnectTimeout + } + + if len(w.params.Ssid) == 0 { + return netlink.ErrMissingSSID + } + + if debugging(debugBasic) { + fmt.Printf("Starting Wifi AP as SSID '%s'...", w.params.Ssid) + } + + start := time.Now() + + // Start the connection process + switch { + case w.params.Passphrase != "": + w.setPassphraseForAP(w.params.Ssid, w.params.Passphrase) + default: + w.setNetworkForAP(w.params.Ssid) + } + + // Check if we are listening + for { + status := w.getConnectionStatus() + switch status { + case statusAPListening: + if debugging(debugBasic) { + fmt.Printf("LISTENING\r\n") + } + if w.notifyCb != nil { + w.notifyCb(netlink.EventNetUp) + } + return nil + case statusAPFailed: + if debugging(debugBasic) { + fmt.Printf("FAILED (%s)\r\n", w.reason()) + } + return netlink.ErrConnectFailed + } + if time.Since(start) > timeout { + break + } + time.Sleep(1 * time.Second) + } + + if debugging(debugBasic) { + fmt.Printf("FAILED (timed out)\r\n") + } + + return netlink.ErrConnectTimeout +} + func (w *wifinina) netDisconnect() { w.disconnect() } @@ -314,7 +368,7 @@ func (w *wifinina) showDriver() { if debugging(debugBasic) { fmt.Printf("\r\n") fmt.Printf("%s\r\n\r\n", driverName) - fmt.Printf("Driver version : %s\r\n", version) + fmt.Printf("Driver version : %s\r\n", drivers.Version) } w.driverShown = true } @@ -384,7 +438,12 @@ func (w *wifinina) showIP() { } func (w *wifinina) networkDown() bool { - return w.getConnectionStatus() != statusConnected + switch w.getConnectionStatus() { + case statusConnected, statusAPListening, statusAPConnected: + return false + default: + return true + } } func (w *wifinina) watchdog() { @@ -422,15 +481,28 @@ func (w *wifinina) netConnect(reset bool) error { } w.showDevice() +retry: for i := 0; w.params.Retries == 0 || i < w.params.Retries; i++ { - if err := w.connectToAP(); err != nil { - switch err { - case netlink.ErrConnectTimeout, netlink.ErrConnectFailed: - continue + switch w.params.ConnectMode { + case netlink.ConnectModeAP: + if err := w.startAP(); err != nil { + switch err { + case netlink.ErrConnectTimeout, netlink.ErrConnectFailed: + continue + } + return err } - return err + break retry + default: + if err := w.connectToAP(); err != nil { + switch err { + case netlink.ErrConnectTimeout, netlink.ErrConnectFailed: + continue + } + return err + } + break retry } - break } if w.networkDown() { @@ -505,6 +577,12 @@ func (w *wifinina) GetHostByName(name string) (netip.Addr, error) { fmt.Printf("[GetHostByName] name: %s\r\n", name) } + // If it's already in dotted-decimal notation, return a copy + // per gethostbyname(3). + if ip, err := netip.ParseAddr(name); err == nil { + return ip, nil + } + w.mu.Lock() defer w.mu.Unlock() @@ -547,6 +625,20 @@ func (w *wifinina) Addr() (netip.Addr, error) { return ip, nil } +// newSockfd returns the next available sockfd, or -1 if none available +func (w *wifinina) newSockfd() int { + if len(w.sockets) >= maxNetworks { + return -1 + } + // Search for the next available sockfd starting at 0 + for sockfd := 0; ; sockfd++ { + if _, ok := w.sockets[sockfd]; !ok { + return sockfd + } + } + return -1 +} + // See man socket(2) for standard Berkely sockets for Socket, Bind, etc. // The driver strives to meet the function and semantics of socket(2). @@ -574,34 +666,46 @@ func (w *wifinina) Socket(domain int, stype int, protocol int) (int, error) { w.mu.Lock() defer w.mu.Unlock() - sock := w.getSocket() - if sock == noSocketAvail { + sockfd := w.newSockfd() + if sockfd == -1 { return -1, netdev.ErrNoMoreSockets } - socket := newSocket(protocol) - w.sockets[sock] = socket + w.sockets[sockfd] = &Socket{ + protocol: protocol, + sock: noSocketAvail, + } - return int(sock), nil + if debugging(debugNetdev) { + fmt.Printf("[Socket] <-- sockfd %d\r\n", sockfd) + } + + return sockfd, nil } func (w *wifinina) Bind(sockfd int, ip netip.AddrPort) error { if debugging(debugNetdev) { - fmt.Printf("[Bind] sockfd: %d, addr: %s\r\n", sockfd, ip) + fmt.Printf("[Bind] sockfd: %d, addr: %s:%d\r\n", sockfd, ip.Addr(), ip.Port()) } w.mu.Lock() defer w.mu.Unlock() - var sock = sock(sockfd) - var socket = w.sockets[sock] + socket, ok := w.sockets[sockfd] + if !ok { + return netdev.ErrInvalidSocketFd + } switch socket.protocol { case netdev.IPPROTO_TCP: case netdev.IPPROTO_TLS: case netdev.IPPROTO_UDP: - w.startServer(sock, ip.Port(), protoModeUDP) + socket.sock = w.getSocket() + if socket.sock == noSocketAvail { + return netdev.ErrNoMoreSockets + } + w.startServer(socket.sock, ip.Port(), protoModeUDP) } socket.laddr = ip @@ -629,22 +733,40 @@ func (w *wifinina) Connect(sockfd int, host string, ip netip.AddrPort) error { w.mu.Lock() defer w.mu.Unlock() - var sock = sock(sockfd) - var socket = w.sockets[sock] + socket, ok := w.sockets[sockfd] + if !ok { + return netdev.ErrInvalidSocketFd + } // Start the connection switch socket.protocol { + case netdev.IPPROTO_TCP: - w.startClient(sock, "", toUint32(ip.Addr().As4()), ip.Port(), protoModeTCP) + socket.sock = w.getSocket() + if socket.sock == noSocketAvail { + return netdev.ErrNoMoreSockets + } + w.startClient(socket.sock, "", toUint32(ip.Addr().As4()), ip.Port(), protoModeTCP) + case netdev.IPPROTO_TLS: - w.startClient(sock, host, 0, ip.Port(), protoModeTLS) + socket.sock = w.getSocket() + if socket.sock == noSocketAvail { + return netdev.ErrNoMoreSockets + } + w.startClient(socket.sock, host, 0, ip.Port(), protoModeTLS) + case netdev.IPPROTO_UDP: + if socket.sock == noSocketAvail { + return fmt.Errorf("Must Bind before Connecting") + } // See start in sendUDP() socket.raddr = ip + socket.clientConnected = true return nil } - if w.getClientState(sock) == tcpStateEstablished { + if w.getClientState(socket.sock) == tcpStateEstablished { + socket.clientConnected = true return nil } @@ -664,12 +786,18 @@ func (w *wifinina) Listen(sockfd int, backlog int) error { w.mu.Lock() defer w.mu.Unlock() - var sock = sock(sockfd) - var socket = w.sockets[sock] + socket, ok := w.sockets[sockfd] + if !ok { + return netdev.ErrInvalidSocketFd + } switch socket.protocol { case netdev.IPPROTO_TCP: - w.startServer(sock, socket.laddr.Port(), protoModeTCP) + socket.sock = w.getSocket() + if socket.sock == noSocketAvail { + return netdev.ErrNoMoreSockets + } + w.startServer(socket.sock, socket.laddr.Port(), protoModeTCP) case netdev.IPPROTO_UDP: default: return netdev.ErrProtocolNotSupported @@ -687,9 +815,10 @@ func (w *wifinina) Accept(sockfd int) (int, netip.AddrPort, error) { w.mu.Lock() defer w.mu.Unlock() - var client sock - var sock = sock(sockfd) - var socket = w.sockets[sock] + socket, ok := w.sockets[sockfd] + if !ok { + return -1, netip.AddrPort{}, netdev.ErrInvalidSocketFd + } switch socket.protocol { case netdev.IPPROTO_TCP: @@ -697,8 +826,9 @@ func (w *wifinina) Accept(sockfd int) (int, netip.AddrPort, error) { return -1, netip.AddrPort{}, netdev.ErrProtocolNotSupported } +skip: for { - // Accept() will be sleeping most of the time, checking for a + // Accept() will be sleeping most of the time, checking for // new clients every 1/10 sec. w.mu.Unlock() time.Sleep(100 * time.Millisecond) @@ -709,48 +839,43 @@ func (w *wifinina) Accept(sockfd int) (int, netip.AddrPort, error) { return -1, netip.AddrPort{}, w.fault } - // TODO: BUG: Currently, a sock that is 100% busy will always be - // TODO: returned by w.accept(sock), starving other socks - // TODO: from begin serviced. Need to figure out how to - // TODO: service socks fairly (round-robin?) so no one sock - // TODO: can dominate. - // Check if a client has data - client = w.accept(sock) + var client sock = w.accept(socket.sock) if client == noSocketAvail { // None ready continue } - raddr := w.getRemoteData(client) - - // If we've already seen this socket, we can reuse - // the socket and return it. But, only if the socket - // is closed. If it's not closed, we'll just come back - // later to reuse it. - - clientSocket, ok := w.sockets[client] - if ok { - // Wait for client to Close - if clientSocket.inuse { - continue + // If we already have a socket for the client, skip + for _, s := range w.sockets { + if s.sock == client { + continue skip } - // Reuse client socket - return int(client), raddr, nil } - // Create new socket for client and return fd - w.sockets[client] = newSocket(socket.protocol) - return int(client), raddr, nil + // Otherwise, create a new socket + clientfd := w.newSockfd() + if clientfd == -1 { + return -1, netip.AddrPort{}, netdev.ErrNoMoreSockets + } + + w.sockets[clientfd] = &Socket{ + protocol: netdev.IPPROTO_TCP, + sock: client, + clientConnected: true, + } + + raddr := w.getRemoteData(client) + + return clientfd, raddr, nil } } -func (w *wifinina) sockDown(sock sock) bool { - var socket = w.sockets[sock] +func (w *wifinina) sockDown(socket *Socket) bool { if socket.protocol == netdev.IPPROTO_UDP { return false } - return w.getClientState(sock) != tcpStateEstablished + return w.getClientState(socket.sock) != tcpStateEstablished } func (w *wifinina) sendTCP(sock sock, buf []byte, deadline time.Time) (int, error) { @@ -778,7 +903,7 @@ func (w *wifinina) sendTCP(sock sock, buf []byte, deadline time.Time) (int, erro } // Check if socket went down - if w.sockDown(sock) { + if w.getClientState(sock) != tcpStateEstablished { return -1, io.EOF } @@ -817,8 +942,10 @@ func (w *wifinina) sendUDP(sock sock, raddr netip.AddrPort, buf []byte, deadline } func (w *wifinina) sendChunk(sockfd int, buf []byte, deadline time.Time) (int, error) { - var sock = sock(sockfd) - var socket = w.sockets[sock] + socket, ok := w.sockets[sockfd] + if !ok { + return -1, netdev.ErrInvalidSocketFd + } // Check if we've timed out if !deadline.IsZero() { @@ -829,9 +956,9 @@ func (w *wifinina) sendChunk(sockfd int, buf []byte, deadline time.Time) (int, e switch socket.protocol { case netdev.IPPROTO_TCP, netdev.IPPROTO_TLS: - return w.sendTCP(sock, buf, deadline) + return w.sendTCP(socket.sock, buf, deadline) case netdev.IPPROTO_UDP: - return w.sendUDP(sock, socket.raddr, buf, deadline) + return w.sendUDP(socket.sock, socket.raddr, buf, deadline) } return -1, netdev.ErrProtocolNotSupported @@ -876,7 +1003,10 @@ func (w *wifinina) Recv(sockfd int, buf []byte, flags int, w.mu.Lock() defer w.mu.Unlock() - var sock = sock(sockfd) + socket, ok := w.sockets[sockfd] + if !ok { + return -1, netdev.ErrInvalidSocketFd + } // Limit max read size to chunk large read requests var max = len(buf) @@ -897,22 +1027,22 @@ func (w *wifinina) Recv(sockfd int, buf []byte, flags int, // doesn't return unless there is data, even a single byte, or // on error such as timeout or EOF. - n := int(w.getDataBuf(sock, buf[:max])) + n := int(w.getDataBuf(socket.sock, buf[:max])) if n > 0 { if debugging(debugNetdev) { fmt.Printf("[<--Recv] sockfd: %d, n: %d\r\n", - sock, n) + sockfd, n) } return n, nil } // Check if socket went down - if w.sockDown(sock) { + if w.sockDown(socket) { // Get any last bytes - n = int(w.getDataBuf(sock, buf[:max])) + n = int(w.getDataBuf(socket.sock, buf[:max])) if debugging(debugNetdev) { fmt.Printf("[<--Recv] sockfd: %d, n: %d, EOF\r\n", - sock, n) + sockfd, n) } if n > 0 { return n, io.EOF @@ -941,34 +1071,18 @@ func (w *wifinina) Close(sockfd int) error { w.mu.Lock() defer w.mu.Unlock() - var sock = sock(sockfd) - var socket = w.sockets[sock] - - if !socket.inuse { - return nil + socket, ok := w.sockets[sockfd] + if !ok { + return netdev.ErrInvalidSocketFd } - w.stopClient(sock) - - if socket.protocol == netdev.IPPROTO_UDP { - socket.inuse = false - return nil + if socket.clientConnected { + w.stopClient(socket.sock) } - start := time.Now() - for time.Since(start) < 5*time.Second { + delete(w.sockets, sockfd) - if w.getClientState(sock) == tcpStateClosed { - socket.inuse = false - return nil - } - - w.mu.Unlock() - time.Sleep(100 * time.Millisecond) - w.mu.Lock() - } - - return netdev.ErrClosingSocket + return nil } func (w *wifinina) SetSockOpt(sockfd int, level int, opt int, value interface{}) error { diff --git a/ws2812/gen-ws2812.go b/ws2812/gen-ws2812.go index 54dc6b8..c1f2634 100644 --- a/ws2812/gen-ws2812.go +++ b/ws2812/gen-ws2812.go @@ -17,7 +17,8 @@ import ( // the new assembly implementation - no fiddly timings to calculate and no nops // to count! // -// Right now this is specific to Cortex-M chips and assume the following things: +// Right now this is specific to specific chips: +// On Cortex-M chips it assume the following things: // - Arithmetic operations (shift, add, sub) take up 1 clock cycle. // - The nop instruction also takes up 1 clock cycle. // - Store instructions (to the GPIO pins) take up 2 clock cycles. @@ -25,8 +26,15 @@ import ( // depends on whether the branch is taken or not. On the M4, the documentation // is less clear but it appears the instruction is still 1 to 3 cycles // (possibly including some branch prediction). -// It is certainly possible to extend this to other architectures, such as AVR -// and RISC-V if needed. +// On RISC-V chips it assumes the following things: +// - Arithmetic operations (shift, add, sub) take up 1 clock cycle. +// - The nop instruction also takes up 1 clock cycle. +// - Store instructions (to the GPIO pins) take up 1 clock cycle. +// - Branch instructions can take up 1 or 3 clock cycles, depending on branch +// prediction. This is based on the SiFive FE310 CPU, but hopefully it +// generalizes to other RISC-V chips as well. + +// It is certainly possible to extend this to other architectures, such as AVR as needed. // // Here are two important resources. For the timings: // https://wp.josh.com/2014/05/13/ws2812-neopixels-are-not-so-finicky-once-you-get-to-know-them/ @@ -45,6 +53,7 @@ type architectureImpl struct { maxBaseCyclesT1H int minBaseCyclesTLD int valueTemplate string // template for how to pass the 'c' byte to assembly + funcAttr string // C function attribute (default: always_inline) template string // assembly template } @@ -83,7 +92,7 @@ var architectures = map[string]architectureImpl{ // - branches are 1 or 3 cycles, depending on branch prediction // - ALU operations are 1 cycle (as on most CPUs) // Hopefully this generalizes to other chips. - buildTag: "tinygo.riscv32", + buildTag: "tinygo.riscv32 && !esp32c3", minBaseCyclesT0H: 1 + 1 + 1, // shift + branch (not taken) + store maxBaseCyclesT0H: 1 + 3 + 1, // shift + branch (not taken) + store minBaseCyclesT1H: 1 + 1 + 1, // shift + branch (taken) + store @@ -103,6 +112,37 @@ var architectures = map[string]architectureImpl{ @DELAY3 addi %[i], %[i], -1 // [1] bnez %[i], 1b // [1/3] send_bit +`, + }, + "esp32c3": { + // ESP32-C3 RISC-V core: + // - stores are 1 cycle + // - branches are 1 or 3 cycles + // - ALU operations are 1 cycle + // Uses the same instruction timing as the SiFive FE310, but the + // function is placed in IRAM instead of flash to avoid instruction + // cache miss stalls that would destroy WS2812 timing. + buildTag: "esp32c3", + minBaseCyclesT0H: 1 + 1 + 1, // shift + branch (not taken) + store + maxBaseCyclesT0H: 1 + 3 + 1, // shift + branch (not taken) + store + minBaseCyclesT1H: 1 + 1 + 1, // shift + branch (taken) + store + maxBaseCyclesT1H: 1 + 3 + 1, // shift + branch (taken) + store + minBaseCyclesTLD: 1 + 1 + 1, // subtraction + branch + store (in next cycle) + valueTemplate: "(uint32_t)c << 23", + funcAttr: `__attribute__((section(".iram1"), noinline))`, + template: ` +1: // send_bit + sw %[maskSet], %[portSet] // [1] T0H and T0L start here + @DELAY1 + slli %[value], %[value], 1 // [1] shift value left by 1 + bltz %[value], 2f // [1/3] skip_store + sw %[maskClear], %[portClear] // [1] T0H -> T0L transition +2: // skip_store + @DELAY2 + sw %[maskClear], %[portClear] // [1] T1H -> T1L transition + @DELAY3 + addi %[i], %[i], -1 // [1] + bnez %[i], 1b // [1/3] send_bit `, }, } @@ -208,7 +248,11 @@ func writeCAssembly(f *os.File, arch string, megahertz int) error { // ignore I/O errors. buf := &bytes.Buffer{} fmt.Fprintf(buf, "\n") - fmt.Fprintf(buf, "__attribute__((always_inline))\nvoid ws2812_writeByte%d(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {\n", megahertz) + funcAttr := archImpl.funcAttr + if funcAttr == "" { + funcAttr = "__attribute__((always_inline))" + } + fmt.Fprintf(buf, "%s\nvoid ws2812_writeByte%d(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) {\n", funcAttr, megahertz) fmt.Fprintf(buf, " // Timings:\n") fmt.Fprintf(buf, " // T0H: %2d - %2d cycles or %.1fns - %.1fns\n", actualMinCyclesT0H, actualMaxCyclesT0H, actualMinNanosecondsT0H, actualMaxNanosecondsT0H) fmt.Fprintf(buf, " // T1H: %2d - %2d cycles or %.1fns - %.1fns\n", actualMinCyclesT1H, actualMaxCyclesT1H, actualMinNanosecondsT1H, actualMaxNanosecondsT1H) diff --git a/ws2812/ws2812-asm_cortexm.go b/ws2812/ws2812-asm_cortexm.go index 0cdece1..a1722f6 100644 --- a/ws2812/ws2812-asm_cortexm.go +++ b/ws2812/ws2812-asm_cortexm.go @@ -931,6 +931,727 @@ void ws2812_writeByte125(char c, uint32_t *portSet, uint32_t *portClear, uint32_ [portClear]"m"(*portClear)); } +__attribute__((always_inline)) +void ws2812_writeByte150(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { + // Timings: + // T0H: 53 - 55 cycles or 353.3ns - 366.7ns + // T1H: 158 - 160 cycles or 1053.3ns - 1066.7ns + // TLD: 173 - cycles or 1153.3ns - + uint32_t value = (uint32_t)c << 24; + char i = 8; + __asm__ __volatile__( + "1: @ send_bit\n" + "\t str %[maskSet], %[portSet] @ [2] T0H and T0L start here\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t lsls %[value], #1 @ [1]\n" + "\t bcs.n 2f @ [1/3] skip_store\n" + "\t str %[maskClear], %[portClear] @ [2] T0H -> T0L transition\n" + "\t2: @ skip_store\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t str %[maskClear], %[portClear] @ [2] T1H -> T1L transition\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t subs %[i], #1 @ [1]\n" + "\t beq.n 3f @ [1/3] end\n" + "\t b 1b @ [1/3] send_bit\n" + "\t3: @ end\n" + : [value]"+r"(value), + [i]"+r"(i) + : [maskSet]"r"(maskSet), + [portSet]"m"(*portSet), + [maskClear]"r"(maskClear), + [portClear]"m"(*portClear)); +} + +__attribute__((always_inline)) +void ws2812_writeByte160(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { + // Timings: + // T0H: 56 - 58 cycles or 350.0ns - 362.5ns + // T1H: 168 - 170 cycles or 1050.0ns - 1062.5ns + // TLD: 184 - cycles or 1150.0ns - + uint32_t value = (uint32_t)c << 24; + char i = 8; + __asm__ __volatile__( + "1: @ send_bit\n" + "\t str %[maskSet], %[portSet] @ [2] T0H and T0L start here\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t lsls %[value], #1 @ [1]\n" + "\t bcs.n 2f @ [1/3] skip_store\n" + "\t str %[maskClear], %[portClear] @ [2] T0H -> T0L transition\n" + "\t2: @ skip_store\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t str %[maskClear], %[portClear] @ [2] T1H -> T1L transition\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t subs %[i], #1 @ [1]\n" + "\t beq.n 3f @ [1/3] end\n" + "\t b 1b @ [1/3] send_bit\n" + "\t3: @ end\n" + : [value]"+r"(value), + [i]"+r"(i) + : [maskSet]"r"(maskSet), + [portSet]"m"(*portSet), + [maskClear]"r"(maskClear), + [portClear]"m"(*portClear)); +} + __attribute__((always_inline)) void ws2812_writeByte168(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { // Timings: @@ -1320,6 +2041,465 @@ void ws2812_writeByte168(char c, uint32_t *portSet, uint32_t *portClear, uint32_ [maskClear]"r"(maskClear), [portClear]"m"(*portClear)); } + +__attribute__((always_inline)) +void ws2812_writeByte200(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { + // Timings: + // T0H: 70 - 72 cycles or 350.0ns - 360.0ns + // T1H: 210 - 212 cycles or 1050.0ns - 1060.0ns + // TLD: 230 - cycles or 1150.0ns - + uint32_t value = (uint32_t)c << 24; + char i = 8; + __asm__ __volatile__( + "1: @ send_bit\n" + "\t str %[maskSet], %[portSet] @ [2] T0H and T0L start here\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t lsls %[value], #1 @ [1]\n" + "\t bcs.n 2f @ [1/3] skip_store\n" + "\t str %[maskClear], %[portClear] @ [2] T0H -> T0L transition\n" + "\t2: @ skip_store\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t str %[maskClear], %[portClear] @ [2] T1H -> T1L transition\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t subs %[i], #1 @ [1]\n" + "\t beq.n 3f @ [1/3] end\n" + "\t b 1b @ [1/3] send_bit\n" + "\t3: @ end\n" + : [value]"+r"(value), + [i]"+r"(i) + : [maskSet]"r"(maskSet), + [portSet]"m"(*portSet), + [maskClear]"r"(maskClear), + [portClear]"m"(*portClear)); +} */ import "C" @@ -1373,6 +2553,26 @@ func (d Device) writeByte125(c byte) { interrupt.Restore(mask) } +func (d Device) writeByte150(c byte) { + portSet, maskSet := d.Pin.PortMaskSet() + portClear, maskClear := d.Pin.PortMaskClear() + + mask := interrupt.Disable() + C.ws2812_writeByte150(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear)) + + interrupt.Restore(mask) +} + +func (d Device) writeByte160(c byte) { + portSet, maskSet := d.Pin.PortMaskSet() + portClear, maskClear := d.Pin.PortMaskClear() + + mask := interrupt.Disable() + C.ws2812_writeByte160(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear)) + + interrupt.Restore(mask) +} + func (d Device) writeByte168(c byte) { portSet, maskSet := d.Pin.PortMaskSet() portClear, maskClear := d.Pin.PortMaskClear() @@ -1382,3 +2582,13 @@ func (d Device) writeByte168(c byte) { interrupt.Restore(mask) } + +func (d Device) writeByte200(c byte) { + portSet, maskSet := d.Pin.PortMaskSet() + portClear, maskClear := d.Pin.PortMaskClear() + + mask := interrupt.Disable() + C.ws2812_writeByte200(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear)) + + interrupt.Restore(mask) +} diff --git a/ws2812/ws2812-asm_esp32c3.go b/ws2812/ws2812-asm_esp32c3.go new file mode 100644 index 0000000..21e3659 --- /dev/null +++ b/ws2812/ws2812-asm_esp32c3.go @@ -0,0 +1,396 @@ +//go:build esp32c3 + +package ws2812 + +// Warning: autogenerated file. Instead of modifying this file, change +// gen-ws2812.go and run "go generate". + +import "runtime/interrupt" +import "unsafe" + +/* +#include + +__attribute__((section(".iram1"), noinline)) +void ws2812_writeByte160(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { + // Timings: + // T0H: 56 - 58 cycles or 350.0ns - 362.5ns + // T1H: 168 - 170 cycles or 1050.0ns - 1062.5ns + // TLD: 184 - cycles or 1150.0ns - + uint32_t value = (uint32_t)c << 23; + char i = 8; + __asm__ __volatile__( + "1: // send_bit\n" + "\t sw %[maskSet], %[portSet] // [1] T0H and T0L start here\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t slli %[value], %[value], 1 // [1] shift value left by 1\n" + "\t bltz %[value], 2f // [1/3] skip_store\n" + "\t sw %[maskClear], %[portClear] // [1] T0H -> T0L transition\n" + "\t2: // skip_store\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t sw %[maskClear], %[portClear] // [1] T1H -> T1L transition\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t nop\n" + "\t addi %[i], %[i], -1 // [1]\n" + "\t bnez %[i], 1b // [1/3] send_bit\n" + : [value]"+r"(value), + [i]"+r"(i) + : [maskSet]"r"(maskSet), + [portSet]"m"(*portSet), + [maskClear]"r"(maskClear), + [portClear]"m"(*portClear)); +} +*/ +import "C" + +func (d Device) writeByte160(c byte) { + portSet, maskSet := d.Pin.PortMaskSet() + portClear, maskClear := d.Pin.PortMaskClear() + + mask := interrupt.Disable() + C.ws2812_writeByte160(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear)) + + interrupt.Restore(mask) +} diff --git a/ws2812/ws2812-asm_tinygoriscv.go b/ws2812/ws2812-asm_tinygoriscv.go index 05e9653..046df7a 100644 --- a/ws2812/ws2812-asm_tinygoriscv.go +++ b/ws2812/ws2812-asm_tinygoriscv.go @@ -1,4 +1,4 @@ -//go:build tinygo.riscv32 +//go:build tinygo.riscv32 && !esp32c3 package ws2812 @@ -11,378 +11,6 @@ import "unsafe" /* #include -__attribute__((always_inline)) -void ws2812_writeByte160(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { - // Timings: - // T0H: 56 - 58 cycles or 350.0ns - 362.5ns - // T1H: 168 - 170 cycles or 1050.0ns - 1062.5ns - // TLD: 184 - cycles or 1150.0ns - - uint32_t value = (uint32_t)c << 23; - char i = 8; - __asm__ __volatile__( - "1: // send_bit\n" - "\t sw %[maskSet], %[portSet] // [1] T0H and T0L start here\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t slli %[value], %[value], 1 // [1] shift value left by 1\n" - "\t bltz %[value], 2f // [1/3] skip_store\n" - "\t sw %[maskClear], %[portClear] // [1] T0H -> T0L transition\n" - "\t2: // skip_store\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t sw %[maskClear], %[portClear] // [1] T1H -> T1L transition\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t nop\n" - "\t addi %[i], %[i], -1 // [1]\n" - "\t bnez %[i], 1b // [1/3] send_bit\n" - : [value]"+r"(value), - [i]"+r"(i) - : [maskSet]"r"(maskSet), - [portSet]"m"(*portSet), - [maskClear]"r"(maskClear), - [portClear]"m"(*portClear)); -} - __attribute__((always_inline)) void ws2812_writeByte320(char c, uint32_t *portSet, uint32_t *portClear, uint32_t maskSet, uint32_t maskClear) { // Timings: @@ -1109,16 +737,6 @@ void ws2812_writeByte320(char c, uint32_t *portSet, uint32_t *portClear, uint32_ */ import "C" -func (d Device) writeByte160(c byte) { - portSet, maskSet := d.Pin.PortMaskSet() - portClear, maskClear := d.Pin.PortMaskClear() - - mask := interrupt.Disable() - C.ws2812_writeByte160(C.char(c), (*C.uint32_t)(unsafe.Pointer(portSet)), (*C.uint32_t)(unsafe.Pointer(portClear)), C.uint32_t(maskSet), C.uint32_t(maskClear)) - - interrupt.Restore(mask) -} - func (d Device) writeByte320(c byte) { portSet, maskSet := d.Pin.PortMaskSet() portClear, maskClear := d.Pin.PortMaskClear() diff --git a/ws2812/ws2812.go b/ws2812/ws2812.go index a0e0791..eaa26ab 100644 --- a/ws2812/ws2812.go +++ b/ws2812/ws2812.go @@ -1,8 +1,12 @@ // Package ws2812 implements a driver for WS2812 and SK6812 RGB LED strips. +// +// On most platforms NewWS2812 uses bit-banging. +// On RP2040/RP2350 it uses PIO for hardware-timed control. package ws2812 // import "tinygo.org/x/drivers/ws2812" -//go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 168 -//go:generate go run gen-ws2812.go -arch=tinygoriscv 160 320 +//go:generate go run gen-ws2812.go -arch=cortexm 16 48 64 120 125 150 160 168 200 +//go:generate go run gen-ws2812.go -arch=tinygoriscv 320 +//go:generate go run gen-ws2812.go -arch=esp32c3 160 import ( "errors" @@ -15,7 +19,8 @@ var errUnknownClockSpeed = errors.New("ws2812: unknown CPU clock speed") // Device wraps a pin object for an easy driver interface. type Device struct { Pin machine.Pin - writeColorFunc func(Device, []color.RGBA) error + brightness uint8 + writeColorFunc func(Device, []color.RGBA, uint8) error } // deprecated, use NewWS2812 or NewSK6812 depending on which device you want. @@ -24,26 +29,30 @@ func New(pin machine.Pin) Device { return NewWS2812(pin) } -// New returns a new WS2812(RGB) driver. -// It does not touch the pin object: you have -// to configure it as an output pin before calling New. +// NewWS2812 returns a new WS2812(RGB) driver. +// On RP2040/RP2350, it uses PIO for hardware-timed control. +// On other platforms, you must configure the pin as output before calling this. func NewWS2812(pin machine.Pin) Device { - return Device{ - Pin: pin, - writeColorFunc: writeColorsRGB, - } + return newWS2812Device(pin) } -// New returns a new SK6812(RGBA) driver. -// It does not touch the pin object: you have -// to configure it as an output pin before calling New. +// NewSK6812 returns a new SK6812W/RGBW driver (4 channels, GRBW order, 32-bit protocol), for the 3 channels version use NewWS2812 +// Use this for SK6812W strips that have a dedicated white channel controlled via color.A. +// It does not touch the pin object: you have to configure it as an output pin before +// calling this. func NewSK6812(pin machine.Pin) Device { return Device{ Pin: pin, + brightness: 255, writeColorFunc: writeColorsRGBA, } } +// SetBrightness sets the global brightness (0-255). +func (d *Device) SetBrightness(b uint8) { + d.brightness = b +} + // Write the raw bitstring out using the WS2812 protocol. func (d Device) Write(buf []byte) (n int, err error) { for _, c := range buf { @@ -55,24 +64,35 @@ func (d Device) Write(buf []byte) (n int, err error) { // Write the given color slice out using the WS2812 protocol. // Colors are sent out in the usual GRB(A) format. func (d Device) WriteColors(buf []color.RGBA) (err error) { - return d.writeColorFunc(d, buf) + return d.writeColorFunc(d, buf, d.brightness) } -func writeColorsRGB(d Device, buf []color.RGBA) (err error) { +func writeColorsRGB(d Device, buf []color.RGBA, brightness uint8) (err error) { for _, color := range buf { - d.WriteByte(color.G) // green - d.WriteByte(color.R) // red - err = d.WriteByte(color.B) // blue + r, g, b := applyBrightness(color, brightness) + d.WriteByte(g) // green + d.WriteByte(r) // red + err = d.WriteByte(b) // blue } return } -func writeColorsRGBA(d Device, buf []color.RGBA) (err error) { +func writeColorsRGBA(d Device, buf []color.RGBA, brightness uint8) (err error) { for _, color := range buf { - d.WriteByte(color.G) // green - d.WriteByte(color.R) // red - d.WriteByte(color.B) // blue + r, g, b := applyBrightness(color, brightness) + + d.WriteByte(g) // green + d.WriteByte(r) // red + d.WriteByte(b) // blue err = d.WriteByte(color.A) // alpha } return } + +// applyBrightness scales a color by the brightness value. +func applyBrightness(c color.RGBA, brightness uint8) (r, g, b uint8) { + r = uint8((uint16(c.R) * uint16(brightness)) >> 8) + g = uint8((uint16(c.G) * uint16(brightness)) >> 8) + b = uint8((uint16(c.B) * uint16(brightness)) >> 8) + return +} diff --git a/ws2812/ws2812_cortexm.go b/ws2812/ws2812_cortexm.go index dafdb49..44aa8de 100644 --- a/ws2812/ws2812_cortexm.go +++ b/ws2812/ws2812_cortexm.go @@ -28,12 +28,21 @@ func (d Device) WriteByte(c byte) error { case 120_000_000: // 120MHz d.writeByte120(c) return nil - case 125_000_000: // 125 MHz e.g. rp2040 + case 125_000_000: // 125 MHz e.g. rp2040 originally d.writeByte125(c) return nil + case 150_000_000: // 150MHz, e.g. rp2350 + d.writeByte150(c) + return nil + case 160_000_000: // 160MHz, e.g. stm32u585 + d.writeByte160(c) + return nil case 168_000_000: // 168MHz, e.g. stm32f405 d.writeByte168(c) return nil + case 200_000_000: // 200MHz, e.g. rp2040 starting with TinyGo v0.37 + d.writeByte200(c) + return nil default: return errUnknownClockSpeed } diff --git a/ws2812/ws2812_esp32c3.go b/ws2812/ws2812_esp32c3.go new file mode 100644 index 0000000..d67c761 --- /dev/null +++ b/ws2812/ws2812_esp32c3.go @@ -0,0 +1,16 @@ +//go:build esp32c3 + +package ws2812 + +import "machine" + +// Send a single byte using the WS2812 protocol. +func (d Device) WriteByte(c byte) error { + switch machine.CPUFrequency() { + case 160_000_000: // 160MHz + d.writeByte160(c) + return nil + default: + return errUnknownClockSpeed + } +} diff --git a/ws2812/ws2812_init_other.go b/ws2812/ws2812_init_other.go new file mode 100644 index 0000000..4ba363a --- /dev/null +++ b/ws2812/ws2812_init_other.go @@ -0,0 +1,10 @@ +//go:build !rp2040 && !rp2350 + +package ws2812 + +import "machine" + +// newWS2812Device creates a WS2812 device using the bit-bang driver. +func newWS2812Device(pin machine.Pin) Device { + return Device{Pin: pin, brightness: 255, writeColorFunc: writeColorsRGB} +} diff --git a/ws2812/ws2812_rp2_pio.go b/ws2812/ws2812_rp2_pio.go new file mode 100644 index 0000000..ac142bd --- /dev/null +++ b/ws2812/ws2812_rp2_pio.go @@ -0,0 +1,39 @@ +//go:build rp2040 || rp2350 + +package ws2812 + +import ( + "image/color" + "machine" + "runtime" + + pio "github.com/tinygo-org/pio/rp2-pio" + "github.com/tinygo-org/pio/rp2-pio/piolib" +) + +// newWS2812Device creates a WS2812 device using PIO for hardware-timed control. +// If PIO initialization fails, it falls back to the bit-bang driver. +func newWS2812Device(pin machine.Pin) Device { + sm, err := pio.PIO0.ClaimStateMachine() + if err != nil { + return Device{Pin: pin, brightness: 255, writeColorFunc: writeColorsRGB} + } + ws, err := piolib.NewWS2812B(sm, pin) + if err != nil { + return Device{Pin: pin, brightness: 255, writeColorFunc: writeColorsRGB} + } + return Device{ + Pin: pin, + brightness: 255, + writeColorFunc: func(_ Device, buf []color.RGBA, brightness uint8) error { + for _, c := range buf { + r, g, b := applyBrightness(c, brightness) + for ws.IsQueueFull() { + runtime.Gosched() + } + ws.PutRGB(r, g, b) + } + return nil + }, + } +} diff --git a/ws2812/ws2812_tinygoriscv.go b/ws2812/ws2812_tinygoriscv.go index 7212310..735cdac 100644 --- a/ws2812/ws2812_tinygoriscv.go +++ b/ws2812/ws2812_tinygoriscv.go @@ -1,4 +1,4 @@ -//go:build tinygo.riscv32 +//go:build tinygo.riscv32 && !esp32c3 package ws2812 @@ -7,9 +7,6 @@ import "machine" // Send a single byte using the WS2812 protocol. func (d Device) WriteByte(c byte) error { switch machine.CPUFrequency() { - case 160_000_000: // 160MHz, e.g. esp32c3 - d.writeByte160(c) - return nil case 320_000_000: // 320MHz, e.g. fe310 d.writeByte320(c) return nil