diff --git a/examples/ssd1306/i2c_128x32/main.go b/examples/ssd1306/i2c_128x32/main.go deleted file mode 100644 index 8baf37d75..000000000 --- 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 a17010def..000000000 --- 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 000000000..a7f412d48 --- /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 000000000..c074df262 --- /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 000000000..f68164b18 --- /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 000000000..fd50bd459 --- /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 094f5cab6..000000000 --- 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/ssd1306/spi_thumby/main.go b/examples/ssd1306/spi_thumby/main.go deleted file mode 100644 index b2a41e503..000000000 --- a/examples/ssd1306/spi_thumby/main.go +++ /dev/null @@ -1,50 +0,0 @@ -// This example using the SSD1306 OLED display over SPI on the Thumby board -// A very tiny 72x40 display. -package main - -import ( - "image/color" - "machine" - "time" - - "tinygo.org/x/drivers/ssd1306" -) - -func main() { - 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}, - }) - - display.ClearDisplay() - - x := int16(36) - y := int16(20) - 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 == 71 { - deltaX = -deltaX - } - - if y == 0 || y == 39 { - deltaY = -deltaY - } - time.Sleep(1 * time.Millisecond) - } -} diff --git a/smoketest.sh b/smoketest.sh index f21e2be55..01cc2a97d 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -65,8 +65,9 @@ tinygo build -size short -o ./build/test.hex -target=pybadge ./examples/shifter/ 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 diff --git a/ssd1306/ssd1306.go b/ssd1306/ssd1306.go index dd8ebeb62..2e65a3a2f 100644 --- a/ssd1306/ssd1306.go +++ b/ssd1306/ssd1306.go @@ -6,11 +6,9 @@ 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" ) @@ -23,16 +21,15 @@ 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 - resetCol ResetValue - resetPage ResetValue - rotation drivers.Rotation + 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 @@ -51,51 +48,15 @@ type Config struct { Rotation drivers.Rotation } -type I2CBus struct { - wire drivers.I2C - Address uint16 -} - -type SPIBus struct { - wire drivers.SPI - dcPin machine.Pin - resetPin machine.Pin - csPin machine.Pin -} - 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 @@ -109,9 +70,6 @@ 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 { @@ -127,11 +85,9 @@ func (d *Device) Configure(cfg Config) { } else { d.resetPage = ResetValue{0, uint8(d.height/8) - 1} } - d.bufferSize = d.width * d.height / 8 - d.buffer = make([]byte, d.bufferSize) 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) @@ -193,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 } } @@ -223,7 +190,7 @@ func (d *Device) Display() error { d.Command(d.resetPage[1]) } - return d.Tx(d.buffer, false) + return d.bus.flush() } // SetPixel enables or disables a pixel in the buffer @@ -252,12 +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 { + if len(buffer) != len(d.buffer) { return errBufferSize } - for i := int16(0); i < d.bufferSize; i++ { - d.buffer[i] = buffer[i] - } + copy(d.buffer, buffer) return nil } @@ -266,79 +231,6 @@ 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() - b.dcPin.Low() - b.csPin.Low() - - err = b.wire.Tx(data, nil) - b.csPin.High() - } else { - b.csPin.High() - 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 diff --git a/ssd1306/ssd1306_i2c.go b/ssd1306/ssd1306_i2c.go new file mode 100644 index 000000000..19f3a1cc9 --- /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 000000000..d96299de5 --- /dev/null +++ b/ssd1306/ssd1306_spi.go @@ -0,0 +1,68 @@ +package ssd1306 + +import ( + "machine" + "time" + + "tinygo.org/x/drivers" +) + +type SPIBus struct { + wire drivers.SPI + dcPin machine.Pin + resetPin machine.Pin + csPin machine.Pin + buffer []byte // buffer to avoid heap allocations +} + +// NewSPI creates a new SSD1306 connection. The SPI wire must already be configured. +func NewSPI(bus drivers.SPI, dcPin, resetPin, csPin machine.Pin) *Device { + dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + return &Device{ + bus: &SPIBus{ + wire: bus, + dcPin: dcPin, + resetPin: resetPin, + csPin: csPin, + }, + } +} + +// 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.Set(!isCommand) + b.csPin.Low() + err := b.wire.Tx(data, nil) + b.csPin.High() + return err +}