diff --git a/Makefile b/Makefile index c7a502e..dc3dcfa 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ smoke-test: @mkdir -p build tinygo build -size short -o ./build/test.elf -target=itsybitsy-m0 ./examples/adxl345/main.go tinygo build -size short -o ./build/test.elf -target=itsybitsy-m0 ./examples/apa102/main.go + tinygo build -size short -o ./build/test.elf -target=itsybitsy-m0 ./examples/apa102/itsybitsy-m0/main.go tinygo build -size short -o ./build/test.elf -target=microbit ./examples/at24cx/main.go tinygo build -size short -o ./build/test.elf -target=itsybitsy-m0 ./examples/bh1750/main.go tinygo build -size short -o ./build/test.elf -target=itsybitsy-m0 ./examples/blinkm/main.go diff --git a/apa102/apa102.go b/apa102/apa102.go index e319db2..f8ade96 100644 --- a/apa102/apa102.go +++ b/apa102/apa102.go @@ -21,15 +21,28 @@ const ( // Device wraps APA102 SPI LEDs. type Device struct { - bus machine.SPI + bus SPI Order int } +// The SPI interface specifies the minimum functionality that a bus +// implementation needs to provide for use by the APA102 driver. Hardware +// SPI from the TinyGo "machine" package implements this already. +type SPI interface { + Tx(w, r []byte) error +} + // New returns a new APA102 driver. Pass in a fully configured SPI bus. -func New(b machine.SPI) Device { +func New(b SPI) Device { return Device{bus: b, Order: BGR} } +// NewSoftwareSPI returns a new APA102 driver that will use a software based +// implementation of the SPI protocol. +func NewSoftwareSPI(sckPin, mosiPin machine.Pin, delay uint32) Device { + return New(&bbSPI{SCK: sckPin, MOSI: mosiPin, Delay: delay}) +} + // WriteColors writes the given RGBA color slice out using the APA102 protocol. // The A value (Alpha channel) is used for brightness, set to 0xff (255) for maximum. func (d Device) WriteColors(cs []color.RGBA) (n int, err error) { diff --git a/apa102/softspi.go b/apa102/softspi.go new file mode 100644 index 0000000..2a0a74f --- /dev/null +++ b/apa102/softspi.go @@ -0,0 +1,68 @@ +package apa102 + +import "machine" + +// 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. +// Note: making this unexported for now because it is probable not suitable +// 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 + MOSI machine.Pin + Delay uint32 +} + +// Configure sets up the SCK and MOSI pins as outputs and sets them low +func (s *bbSPI) Configure() { + s.SCK.Configure(machine.PinConfig{Mode: machine.PinOutput}) + s.MOSI.Configure(machine.PinConfig{Mode: machine.PinOutput}) + s.SCK.Low() + s.MOSI.Low() + if s.Delay == 0 { + s.Delay = 1 + } +} + +// Tx matches signature of machine.SPI.Tx() and is used to send multiple bytes. +// The r slice is ignored and no error will ever be returned. +func (s *bbSPI) Tx(w []byte, r []byte) error { + s.Configure() + for _, b := range w { + s.Transfer(b) + } + return nil +} + +// delay represents a quarter of the clock cycle +func (s *bbSPI) delay() { + for i := uint32(0); i < s.Delay; { + i++ + } +} + +// Transfer is used to send a single byte. +func (s *bbSPI) Transfer(b byte) { + for i := uint8(0); i < 8; i++ { + + // half clock cycle high to start + s.SCK.High() + s.delay() + + // write the value to MOSI (MSB first) + if b&(1<<(7-i)) == 0 { + s.MOSI.Low() + } else { + s.MOSI.High() + } + s.delay() + + // half clock cycle low + s.SCK.Low() + s.delay() + + // for actual SPI would try to read the MISO value here + s.delay() + + } +} diff --git a/examples/apa102/itsybitsy-m0/main.go b/examples/apa102/itsybitsy-m0/main.go new file mode 100644 index 0000000..7170ca1 --- /dev/null +++ b/examples/apa102/itsybitsy-m0/main.go @@ -0,0 +1,84 @@ +// This example demostrates how to control the "Dotstar" (APA102) LED included +// on the Adafruit Itsy Bitsy M0 board. It implements a "rainbow effect" based +// on the following example: +// https://github.com/adafruit/Adafruit_Learning_System_Guides/blob/master/CircuitPython_Essentials/CircuitPython_Internal_RGB_LED_rainbow.py +package main + +import ( + "image/color" + "machine" + "time" + + "tinygo.org/x/drivers/apa102" +) + +var ( + apa apa102.Device + + led = machine.PWM{machine.LED} + leds = make([]color.RGBA, 1) + wheel = &Wheel{Brightness: 0x10} +) + +func init() { + + // APA102 on Itsy Bitsy is connected to pins that require a software-based + // SPI implementation. + apa = apa102.NewSoftwareSPI(machine.PA00, machine.PA01, 1) + + // Configure the regular on-board LED for PWM fading + machine.InitPWM() + led.Configure() + +} + +func main() { + + // We'll fade the on-board LED in a goroutine to show/ensure that the APA102 + // works fine with the scheduler enabled. Comment this out to test this code + // with the scheduler disabled. + go func() { + for i, brightening := uint8(0), false; ; i++ { + if i == 0 { + brightening = !brightening + continue + } + var brightness uint16 = uint16(i) << 8 + if !brightening { + brightness = 0xFFFF - brightness + } + led.Set(brightness) + time.Sleep(5 * time.Millisecond) + } + }() + + // Use the "wheel" function from Adafruit's example to cycle the APA102 + for { + leds[0] = wheel.Next() + apa.WriteColors(leds) + time.Sleep(25 * time.Millisecond) + } + +} + +// Wheel is a port of Adafruit's Circuit Python example referenced above. +type Wheel struct { + Brightness uint8 + pos uint8 +} + +// Next increments the internal state of the color and returns the new RGBA +func (w *Wheel) Next() (c color.RGBA) { + pos := w.pos + if w.pos < 85 { + c = color.RGBA{R: 0xFF - pos*3, G: pos * 3, B: 0x0, A: w.Brightness} + } else if w.pos < 170 { + pos -= 85 + c = color.RGBA{R: 0x0, G: 0xFF - pos*3, B: pos * 3, A: w.Brightness} + } else { + pos -= 170 + c = color.RGBA{R: pos * 3, G: 0x0, B: 0xFF - pos*3, A: w.Brightness} + } + w.pos++ + return +}