diff --git a/Makefile b/Makefile index 623c9db..5e016ca 100644 --- a/Makefile +++ b/Makefile @@ -157,6 +157,8 @@ smoke-test: @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/ws2812 @md5sum ./build/test.hex + tinygo build -size short -o ./build/test.hex -target=feather-nrf52840 ./examples/is31fl3731/main.go + @md5sum ./build/test.hex ifneq ($(AVR), 0) tinygo build -size short -o ./build/test.hex -target=arduino ./examples/ws2812 @md5sum ./build/test.hex diff --git a/README.md b/README.md index 48b5390..8fc3696 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ The following 78 devices are supported. | [software I2C driver](https://www.ti.com/lit/an/slva704/slva704.pdf) | GPIO | | [ILI9341 TFT color display](https://cdn-shop.adafruit.com/datasheets/ILI9341.pdf) | SPI | | [INA260 Volt/Amp/Power meter](https://www.ti.com/lit/ds/symlink/ina260.pdf) | I2C | +| [IS31FL3731 matrix LED driver](https://www.lumissil.com/assets/pdf/core/IS31FL3731_DS.pdf) | I2C | | [4x4 Membrane Keypad](https://cdn.sparkfun.com/assets/f/f/a/5/0/DS-16038.pdf) | GPIO | | [L293x motor driver](https://www.ti.com/lit/ds/symlink/l293d.pdf) | GPIO/PWM | | [L9110x motor driver](https://www.elecrow.com/download/datasheet-l9110.pdf) | GPIO/PWM | diff --git a/examples/is31fl3731/main.go b/examples/is31fl3731/main.go new file mode 100644 index 0000000..7525c8e --- /dev/null +++ b/examples/is31fl3731/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "time" + + "machine" + + "tinygo.org/x/drivers/is31fl3731" +) + +// I2CAddress -- address of led matrix +var I2CAddress uint8 = is31fl3731.I2C_ADDRESS_74 + +func main() { + bus := machine.I2C0 + err := bus.Configure(machine.I2CConfig{}) + if err != nil { + println("could not configure I2C:", err) + return + } + + // Create driver for Adafruit 15x7 CharliePlex LED Matrix FeatherWing + // (CharlieWing): https://www.adafruit.com/product/3163 + ledMatrix := is31fl3731.NewAdafruitCharlieWing15x7(bus, I2CAddress) + + err = ledMatrix.Configure() + if err != nil { + println("could not configure is31fl3731 driver:", err) + return + } + + // Fill the whole matrix on the frame #0 (visible by default) + ledMatrix.Fill(is31fl3731.FRAME_0, uint8(3)) + + // Draw couple pixels on the frame #1 (not visible yet) + ledMatrix.DrawPixelXY(is31fl3731.FRAME_1, uint8(0), uint8(0), uint8(10)) + ledMatrix.DrawPixelXY(is31fl3731.FRAME_1, uint8(14), uint8(6), uint8(10)) + + // There are 8 frames available, it's a good idea to draw on an invisible + // frame and then switch to that frame to reduce flickering. Switch between + // frame #0 and #1 in a loop to show animation: + for { + println("show frame #0...") + ledMatrix.SetActiveFrame(is31fl3731.FRAME_0) + time.Sleep(time.Second * 3) + + println("show frame #1...") + ledMatrix.SetActiveFrame(is31fl3731.FRAME_1) + time.Sleep(time.Second * 3) + } +} diff --git a/is31fl3731/is31fl3731.go b/is31fl3731/is31fl3731.go new file mode 100644 index 0000000..0d0ad8b --- /dev/null +++ b/is31fl3731/is31fl3731.go @@ -0,0 +1,209 @@ +// Package is31fl3731 provides a driver for the Lumissil IS31FL3731 matrix LED +// driver. +// +// Driver supports following layouts: +// - any custom LED matrix layout +// - Adafruit 15x7 CharliePlex LED Matrix FeatherWing (CharlieWing) +// https://www.adafruit.com/product/3163 +// +// Datasheet: +// https://www.lumissil.com/assets/pdf/core/IS31FL3731_DS.pdf +// +// This driver inspired by Adafruit Python driver: +// https://github.com/adafruit/Adafruit_CircuitPython_IS31FL3731 +// +package is31fl3731 + +import ( + "fmt" + "time" + + "tinygo.org/x/drivers" +) + +// Device implements TinyGo driver for Lumissil IS31FL3731 matrix LED driver +type Device struct { + Address uint8 + bus drivers.I2C + + // Currently selected command register (one of the frame registers or the + // function register) + selectedCommand uint8 +} + +// Configure chip for operating as a LED matrix display +func (d *Device) Configure() (err error) { + // Shutdown software + err = d.writeFunctionRegister(SET_SHUTDOWN, []byte{SOFTWARE_OFF}) + if err != nil { + return fmt.Errorf("failed to shutdown: %w", err) + } + + time.Sleep(time.Millisecond * 10) + + // Wake up software + err = d.writeFunctionRegister(SET_SHUTDOWN, []byte{SOFTWARE_ON}) + if err != nil { + return fmt.Errorf("failed to wake up: %w", err) + } + + // Set display to a picture mode ("auto frame play mode" and "audio frame play + // mode" are not supported in this version of the driver) + err = d.writeFunctionRegister(SET_DISPLAY_MODE, []byte{DISPLAY_MODE_PICTURE}) + if err != nil { + return fmt.Errorf("failed to switch to a picture move: %w", err) + } + + // Enable LEDs that are present (soldered) on the board. From the datasheet: + // LEDs which are no connected must be off by LED Control Register (Frame + // Registers) or it will affect other LEDs + err = d.enableLEDs() + if err != nil { + return fmt.Errorf("failed to enable LEDs: %w", err) + } + + // Disable audiosync + err = d.writeFunctionRegister(SET_AUDIOSYNC, []byte{AUDIOSYNC_OFF}) + if err != nil { + return fmt.Errorf("failed to disable audiosync: %w", err) + } + + // Clear all frames + for frame := FRAME_0; frame <= FRAME_7; frame++ { + err = d.Clear(frame) + if err != nil { + return fmt.Errorf("failed to clear frame %d: %w", frame, err) + } + } + + // 1st frame is displayed by default + err = d.SetActiveFrame(FRAME_0) + if err != nil { + return fmt.Errorf("failed to set active frame: %w", err) + } + + return nil +} + +// selectCommand selects command register, can be: +// - frame registers 0-7 +// - function register +func (d *Device) selectCommand(command uint8) (err error) { + if command != d.selectedCommand { + d.selectedCommand = command + return d.bus.WriteRegister(d.Address, COMMAND, []byte{command}) + } + + return nil +} + +// writeFunctionRegister selects the function register and writes data into it +func (d *Device) writeFunctionRegister(operation uint8, data []byte) (err error) { + err = d.selectCommand(FUNCTION) + if err != nil { + return err + } + + return d.bus.WriteRegister(d.Address, operation, data) +} + +// enableLEDs enables only LEDs that are soldered on the set board. Enabled +// all 16x9 LEDs by default +func (d *Device) enableLEDs() (err error) { + for frame := FRAME_0; frame <= FRAME_7; frame++ { + err = d.selectCommand(frame) + if err != nil { + return err + } + + // Enable every LED (16 columns x 9 rows) + for i := uint8(0); i < 16; i++ { + err = d.bus.WriteRegister(d.Address, i, []byte{0xFF}) + if err != nil { + return err + } + } + } + + return nil +} + +// setPixelPWD sets individual pixel's PWM value [0-255] on the selected frame +func (d *Device) setPixelPWD(frame, n, value uint8) (err error) { + err = d.selectCommand(frame) + if err != nil { + return err + } + + return d.bus.WriteRegister(d.Address, LED_PWM_OFFSET+n, []byte{value}) +} + +// SetActiveFrame sets frame to display with LEDs +func (d *Device) SetActiveFrame(frame uint8) (err error) { + if frame > FRAME_7 { + return fmt.Errorf("frame %d is out of valid range [0-7]", frame) + } + + return d.writeFunctionRegister(SET_ACTIVE_FRAME, []byte{frame}) +} + +// Fill the whole frame with provided PWM value [0-255] +func (d *Device) Fill(frame, value uint8) (err error) { + if frame > FRAME_7 { + return fmt.Errorf("frame %d is out of valid range [0-7]", frame) + } + + err = d.selectCommand(frame) + if err != nil { + return err + } + + data := make([]byte, 24) + for i := range data { + data[i] = value + } + + for i := uint8(0); i < 6; i++ { + err = d.bus.WriteRegister(d.Address, LED_PWM_OFFSET+i*24, data) + if err != nil { + return err + } + } + + return nil +} + +// Clear the whole frame +func (d *Device) Clear(frame uint8) (err error) { + return d.Fill(frame, 0x00) +} + +// DrawPixelIndex draws a single pixel on the selected frame by its index with +// provided PWM value [0-255] +func (d *Device) DrawPixelIndex(frame, index, value uint8) (err error) { + if frame > FRAME_7 { + return fmt.Errorf("frame %d is out of valid range [0-7]", frame) + } + + return d.setPixelPWD(frame, index, value) +} + +// DrawPixelXY draws a single pixel on the selected frame by its XY coordinates +// with provided PWM value [0-255]. Raw LEDs layout assumed to be a 16x9 matrix, +// and can be used with any custom board that has IS31FL3731 driver. +func (d *Device) DrawPixelXY(frame, x, y, value uint8) (err error) { + return d.setPixelPWD(frame, 16*x+y, value) +} + +// New creates a raw driver w/o any preset board layout. +// Addresses: +// - 0x74 (AD pin connected to GND) +// - 0x75 (AD pin connected to SCL) +// - 0x76 (AD pin connected to SDA) +// - 0x77 (AD pin connected to VCC) +func New(bus drivers.I2C, address uint8) Device { + return Device{ + Address: address, + bus: bus, + } +} diff --git a/is31fl3731/is31fl3731_acw15x7.go b/is31fl3731/is31fl3731_acw15x7.go new file mode 100644 index 0000000..57d5dfb --- /dev/null +++ b/is31fl3731/is31fl3731_acw15x7.go @@ -0,0 +1,106 @@ +package is31fl3731 + +import ( + "fmt" + + "tinygo.org/x/drivers" +) + +// DeviceAdafruitCharlieWing15x7 implements TinyGo driver for Lumissil +// IS31FL3731 matrix LED driver on Adafruit 15x7 CharliePlex LED Matrix +// FeatherWing (CharlieWing) board: https://www.adafruit.com/product/3163 +type DeviceAdafruitCharlieWing15x7 struct { + Device +} + +// enableLEDs enables only LEDs that are soldered on the Adafruit CharlieWing +// board. The board has following LEDs matrix layout: +// +// "o" - connected (soldered) LEDs +// "x" - not connected LEDs +// +// + - - - - - - - - - - - - - - + +// | + - - - - - - - - - - - - + | +// | | | | +// | | v v +// +---------------------------------+ +// | o o o o o o o o o o o o o o o x | +// | o o o o o o o o o o o o o o o x | +// | o o o o o o o o o o o o o o o x | +// | o o o o o o o o o o o o o o o x | +// | o o o o o o o o o o o o o o o x | +// | o o o o o o o o o o o o o o o x | +// | o o o o o o o o o o o o o o o x | +// | x x x x x x x x x x x x x x x x | +// +---------------------------------+ +// ^ ^ | | +// | | ... - - + | +// | + - - - - - - - - - - - - - + +// | +// start (address 0x00) +// +func (d *DeviceAdafruitCharlieWing15x7) enableLEDs() (err error) { + for frame := FRAME_0; frame <= FRAME_7; frame++ { + err = d.selectCommand(frame) + if err != nil { + return err + } + + // Enable left half + for i := uint8(0); i < 16; i += 2 { + err = d.bus.WriteRegister(d.Address, i, []byte{0b11111110}) + if err != nil { + return err + } + } + // Enable right half + for i := uint8(3); i < 16; i += 2 { + err = d.bus.WriteRegister(d.Address, i, []byte{0b01111111}) + if err != nil { + return err + } + } + // Disable invisible column on the right side + err = d.bus.WriteRegister(d.Address, 1, []byte{0b00000000}) + if err != nil { + return err + } + } + + return nil +} + +// DrawPixelXY draws a single pixel on the selected frame by its XY coordinates +// with provided PWM value [0-255] +func (d *DeviceAdafruitCharlieWing15x7) DrawPixelXY(frame, x, y, value uint8) (err error) { + var index uint8 + + if x >= 15 { + return fmt.Errorf("invalid value: X is out of range [0, 15]") + } else if y >= 7 { + return fmt.Errorf("invalid value: Y is out of range [0, 7]") + } + + // Board is one pixel shorter (7 vs 8 supported pixels) + if x < 8 { + index = 16*x + y + 1 + } else { + index = 16*(16-x) - y - 1 - 1 + } + + return d.setPixelPWD(frame, index, value) +} + +// NewAdafruitCharlieWing15x7 creates a new driver with Adafruit 15x7 +// CharliePlex LED Matrix FeatherWing (CharlieWing) layout. +// Available addresses: +// - 0x74 (default) +// - 0x77 (when the address jumper soldered) +func NewAdafruitCharlieWing15x7(bus drivers.I2C, address uint8) DeviceAdafruitCharlieWing15x7 { + return DeviceAdafruitCharlieWing15x7{ + Device: Device{ + Address: address, + bus: bus, + }, + } +} diff --git a/is31fl3731/registers.go b/is31fl3731/registers.go new file mode 100644 index 0000000..9411d7f --- /dev/null +++ b/is31fl3731/registers.go @@ -0,0 +1,52 @@ +package is31fl3731 + +// Registers. Names taken from the datasheet: +// https://www.lumissil.com/assets/pdf/core/IS31FL3731_DS.pdf +const ( + // AD pin connected to GND + I2C_ADDRESS_74 uint8 = 0x74 + // AD pin connected to SCL + I2C_ADDRESS_75 uint8 = 0x75 + // AD pin connected to SDA + I2C_ADDRESS_76 uint8 = 0x76 + // AD pin connected to VCC + I2C_ADDRESS_77 uint8 = 0x77 + + // Main command register + COMMAND uint8 = 0xFD + + // Commands for each of 8 frames + FRAME_0 uint8 = 0x00 + FRAME_1 uint8 = 0x01 + FRAME_2 uint8 = 0x02 + FRAME_3 uint8 = 0x03 + FRAME_4 uint8 = 0x04 + FRAME_5 uint8 = 0x05 + FRAME_6 uint8 = 0x06 + FRAME_7 uint8 = 0x07 + + // Command to set configuration + FUNCTION uint8 = 0x0B + + // Configuration: + SET_DISPLAY_MODE uint8 = 0x00 + SET_ACTIVE_FRAME uint8 = 0x01 + SET_AUDIOSYNC uint8 = 0x06 + SET_SHUTDOWN uint8 = 0x0A + + // Configuration: display mode + DISPLAY_MODE_PICTURE uint8 = 0x00 + + // Configuration: audiosync (enable audio signal to modulate the intensity of + // the matrix) + AUDIOSYNC_OFF uint8 = 0x00 + AUDIOSYNC_ON uint8 = 0x01 + + // Configuration: software shutdown + SOFTWARE_OFF uint8 = 0x00 + SOFTWARE_ON uint8 = 0x01 + + // Frame LEDs + LED_CONTROL_OFFSET uint8 = 0x00 // to on/off each LED + LED_PWM_OFFSET uint8 = 0x24 // to set PWM (0-255) for each LED +)