From 698e691c51e186b318ed7df4c383d334af1c22a4 Mon Sep 17 00:00:00 2001 From: Yurii Soldak Date: Thu, 19 Jun 2025 16:05:31 +0200 Subject: [PATCH 1/6] ssd1306: avoid unnecessary heap allocations --- ssd1306/ssd1306.go | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/ssd1306/ssd1306.go b/ssd1306/ssd1306.go index dd8ebeb62..4a4ff0f1a 100644 --- a/ssd1306/ssd1306.go +++ b/ssd1306/ssd1306.go @@ -10,7 +10,6 @@ import ( "time" "tinygo.org/x/drivers" - "tinygo.org/x/drivers/internal/legacy" "tinygo.org/x/drivers/pixel" ) @@ -128,7 +127,7 @@ func (d *Device) Configure(cfg Config) { d.resetPage = ResetValue{0, uint8(d.height/8) - 1} } d.bufferSize = d.width * d.height / 8 - d.buffer = make([]byte, d.bufferSize) + d.buffer = make([]byte, d.bufferSize+1) // +1 for the I2C command byte d.canReset = cfg.Address != 0 || d.width != 128 || d.height != 64 // I2C or not 128x64 d.bus.configure() @@ -268,7 +267,8 @@ func (d *Device) GetBuffer() []byte { // Command sends a command to the display func (d *Device) Command(command uint8) { - d.bus.tx([]byte{command}, true) + d.buffer[1] = command // The second byte is the actual command + d.bus.tx(d.buffer[0:2], true) } // setAddress sets the address to the I2C bus @@ -310,32 +310,24 @@ func (d *Device) Tx(data []byte, isCommand bool) error { // 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) + data[0] = 0x00 // Command mode } else { - return legacy.WriteRegister(b.wire, uint8(b.Address), 0x40, data) + data[0] = 0x40 // Data mode } + return b.wire.Tx(uint16(b.Address), data, nil) } // tx sends data to the display (SPIBus implementation) func (b *SPIBus) tx(data []byte, isCommand bool) error { - var err error - + b.csPin.High() 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() } - + b.csPin.Low() + err := b.wire.Tx(data[1:], nil) // The first byte is reserved for I2C communcation, strip it + b.csPin.High() return err } From a9e5944d83928b073c39fe679dd50518954d175c Mon Sep 17 00:00:00 2001 From: Yurii Soldak Date: Mon, 23 Jun 2025 18:27:14 +0200 Subject: [PATCH 2/6] ssd1306: extract i2c and spi bus implementations --- ssd1306/ssd1306.go | 246 +++++++++++------------------------------ ssd1306/ssd1306_i2c.go | 41 +++++++ ssd1306/ssd1306_spi.go | 68 ++++++++++++ 3 files changed, 176 insertions(+), 179 deletions(-) create mode 100644 ssd1306/ssd1306_i2c.go create mode 100644 ssd1306/ssd1306_spi.go diff --git a/ssd1306/ssd1306.go b/ssd1306/ssd1306.go index 4a4ff0f1a..1241ad252 100644 --- a/ssd1306/ssd1306.go +++ b/ssd1306/ssd1306.go @@ -6,7 +6,6 @@ package ssd1306 // import "tinygo.org/x/drivers/ssd1306" import ( "errors" "image/color" - "machine" "time" "tinygo.org/x/drivers" @@ -22,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 @@ -50,51 +48,14 @@ 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 with the given configuration and return the buffer to use + command(cmd uint8) error // send a command to the display + flush() error // send the data in the buffer to the display } 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 @@ -108,9 +69,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 { @@ -126,77 +84,75 @@ 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+1) // +1 for the I2C command byte 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) - d.Command(SETDISPLAYCLOCKDIV) - d.Command(0x80) - d.Command(SETMULTIPLEX) - d.Command(uint8(d.height - 1)) - d.Command(SETDISPLAYOFFSET) - d.Command(0x0) - d.Command(SETSTARTLINE | 0x0) - d.Command(CHARGEPUMP) + d.bus.command(DISPLAYOFF) + d.bus.command(SETDISPLAYCLOCKDIV) + d.bus.command(0x80) + d.bus.command(SETMULTIPLEX) + d.bus.command(uint8(d.height - 1)) + d.bus.command(SETDISPLAYOFFSET) + d.bus.command(0x0) + d.bus.command(SETSTARTLINE | 0x0) + d.bus.command(CHARGEPUMP) if d.vccState == EXTERNALVCC { - d.Command(0x10) + d.bus.command(0x10) } else { - d.Command(0x14) + d.bus.command(0x14) } - d.Command(MEMORYMODE) - d.Command(0x00) + d.bus.command(MEMORYMODE) + d.bus.command(0x00) d.SetRotation(cfg.Rotation) if (d.width == 128 && d.height == 64) || (d.width == 64 && d.height == 48) { // 128x64 or 64x48 - d.Command(SETCOMPINS) - d.Command(0x12) - d.Command(SETCONTRAST) + d.bus.command(SETCOMPINS) + d.bus.command(0x12) + d.bus.command(SETCONTRAST) if d.vccState == EXTERNALVCC { - d.Command(0x9F) + d.bus.command(0x9F) } else { - d.Command(0xCF) + d.bus.command(0xCF) } } else if d.width == 128 && d.height == 32 { // 128x32 - d.Command(SETCOMPINS) - d.Command(0x02) - d.Command(SETCONTRAST) - d.Command(0x8F) + d.bus.command(SETCOMPINS) + d.bus.command(0x02) + d.bus.command(SETCONTRAST) + d.bus.command(0x8F) } else if d.width == 96 && d.height == 16 { // 96x16 - d.Command(SETCOMPINS) - d.Command(0x2) - d.Command(SETCONTRAST) + d.bus.command(SETCOMPINS) + d.bus.command(0x2) + d.bus.command(SETCONTRAST) if d.vccState == EXTERNALVCC { - d.Command(0x10) + d.bus.command(0x10) } else { - d.Command(0xAF) + d.bus.command(0xAF) } } else { // fail silently, it might work println("there's no configuration for this display's size") } - d.Command(SETPRECHARGE) + d.bus.command(SETPRECHARGE) if d.vccState == EXTERNALVCC { - d.Command(0x22) + d.bus.command(0x22) } else { - d.Command(0xF1) + d.bus.command(0xF1) } - d.Command(SETVCOMDETECT) - d.Command(0x40) - d.Command(DISPLAYALLON_RESUME) - d.Command(NORMALDISPLAY) - d.Command(DEACTIVATE_SCROLL) - d.Command(DISPLAYON) + d.bus.command(SETVCOMDETECT) + d.bus.command(0x40) + d.bus.command(DISPLAYALLON_RESUME) + d.bus.command(NORMALDISPLAY) + d.bus.command(DEACTIVATE_SCROLL) + d.bus.command(DISPLAYON) } // 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 } } @@ -214,15 +170,15 @@ func (d *Device) Display() error { // In the 128x64 (SPI) screen resetting to 0x0 after 128 times corrupt the buffer // Since we're printing the whole buffer, avoid resetting it in this case if d.canReset { - d.Command(COLUMNADDR) - d.Command(d.resetCol[0]) - d.Command(d.resetCol[1]) - d.Command(PAGEADDR) - d.Command(d.resetPage[0]) - d.Command(d.resetPage[1]) + d.bus.command(COLUMNADDR) + d.bus.command(d.resetCol[0]) + d.bus.command(d.resetCol[1]) + d.bus.command(PAGEADDR) + d.bus.command(d.resetPage[0]) + d.bus.command(d.resetPage[1]) } - return d.Tx(d.buffer, false) + return d.bus.flush() } // SetPixel enables or disables a pixel in the buffer @@ -251,12 +207,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 } @@ -265,72 +219,6 @@ func (d *Device) GetBuffer() []byte { return d.buffer } -// Command sends a command to the display -func (d *Device) Command(command uint8) { - d.buffer[1] = command // The second byte is the actual command - d.bus.tx(d.buffer[0:2], 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 { - data[0] = 0x00 // Command mode - } else { - data[0] = 0x40 // Data mode - } - return b.wire.Tx(uint16(b.Address), data, nil) -} - -// tx sends data to the display (SPIBus implementation) -func (b *SPIBus) tx(data []byte, isCommand bool) error { - b.csPin.High() - if isCommand { - b.dcPin.Low() - } else { - b.dcPin.High() - } - b.csPin.Low() - err := b.wire.Tx(data[1:], nil) // The first byte is reserved for I2C communcation, strip it - 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 @@ -362,15 +250,15 @@ 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 + d.bus.command(SEGREMAP | 0x1) // Reverse horizontal mapping + d.bus.command(COMSCANDEC) // Reverse vertical mapping case drivers.Rotation180: - d.Command(SEGREMAP) // Normal horizontal mapping - d.Command(COMSCANINC) // Normal vertical mapping + d.bus.command(SEGREMAP) // Normal horizontal mapping + d.bus.command(COMSCANINC) // Normal vertical mapping // nothing to do default: - d.Command(SEGREMAP | 0x1) // Reverse horizontal mapping - d.Command(COMSCANDEC) // Reverse vertical mapping + d.bus.command(SEGREMAP | 0x1) // Reverse horizontal mapping + d.bus.command(COMSCANDEC) // Reverse vertical mapping } return nil } @@ -380,9 +268,9 @@ func (d *Device) SetRotation(rotation drivers.Rotation) error { // should be kept. func (d *Device) Sleep(sleepEnabled bool) error { if sleepEnabled { - d.Command(DISPLAYOFF) + d.bus.command(DISPLAYOFF) } else { - d.Command(DISPLAYON) + d.bus.command(DISPLAYON) } return nil } diff --git a/ssd1306/ssd1306_i2c.go b/ssd1306/ssd1306_i2c.go new file mode 100644 index 000000000..8d48cb206 --- /dev/null +++ b/ssd1306/ssd1306_i2c.go @@ -0,0 +1,41 @@ +package ssd1306 + +import ( + "tinygo.org/x/drivers" +) + +type I2CBus struct { + wire drivers.I2C + address uint16 + buffer []byte +} + +// 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 { + b.address = address + b.buffer = make([]byte, size+2) // +2 for the commands + return b.buffer[2:] // return the buffer without the command part +} + +// flush sends the data part of the buffer to the display +func (b *I2CBus) flush() error { + b.buffer[1] = 0x40 // Data mode + return b.wire.Tx(b.address, b.buffer[1:], nil) +} + +// 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) +} diff --git a/ssd1306/ssd1306_spi.go b/ssd1306/ssd1306_spi.go new file mode 100644 index 000000000..0b985eb15 --- /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 +} + +// 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 the command + return b.buffer[1:] // return the buffer without the command part +} + +// flush sends the data part of the buffer to the display +func (b *SPIBus) flush() error { + b.csPin.High() + b.dcPin.High() + b.csPin.Low() + err := b.wire.Tx(b.buffer[1:], nil) + b.csPin.High() + return err +} + +// command sends a command to the display +func (b *SPIBus) command(cmd uint8) error { + b.buffer[0] = cmd + b.csPin.High() + b.dcPin.Low() + b.csPin.Low() + err := b.wire.Tx(b.buffer[:1], nil) + b.csPin.High() + return err +} From 02def04ae943b2df826896c99d0aae019acac7d2 Mon Sep 17 00:00:00 2001 From: Yurii Soldak Date: Mon, 23 Jun 2025 18:50:18 +0200 Subject: [PATCH 3/6] ssd1306: refactor tests -- show fps and heap usage --- examples/ssd1306/common/common.go | 52 +++++++++++++++++++++++++++++ examples/ssd1306/i2c_128x32/main.go | 34 ++----------------- examples/ssd1306/i2c_128x64/main.go | 33 ++---------------- examples/ssd1306/spi_128x64/main.go | 31 ++--------------- examples/ssd1306/spi_thumby/main.go | 31 ++--------------- 5 files changed, 62 insertions(+), 119 deletions(-) create mode 100644 examples/ssd1306/common/common.go diff --git a/examples/ssd1306/common/common.go b/examples/ssd1306/common/common.go new file mode 100644 index 000000000..aa4dd9cb5 --- /dev/null +++ b/examples/ssd1306/common/common.go @@ -0,0 +1,52 @@ +package common + +import ( + "runtime" + + "image/color" + "time" + + "tinygo.org/x/drivers/ssd1306" +) + +var ms = runtime.MemStats{} + +func Loop(display ssd1306.Device) { + display.ClearDisplay() + w, h := display.Size() + x := int16(0) + y := int16(0) + deltaX := int16(1) + deltaY := int16(1) + trace := time.Now().UnixMilli() + 1000 + frames := 0 + 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 >= trace { + runtime.ReadMemStats(&ms) + println("TS", now, "| FPS", frames, "| HeapInuse", ms.HeapInuse) + trace = now + 1000 + frames = 0 + } + } +} diff --git a/examples/ssd1306/i2c_128x32/main.go b/examples/ssd1306/i2c_128x32/main.go index 8baf37d75..ce842851b 100644 --- a/examples/ssd1306/i2c_128x32/main.go +++ b/examples/ssd1306/i2c_128x32/main.go @@ -3,15 +3,13 @@ package main import ( "machine" - "image/color" - "time" - + "tinygo.org/x/drivers/examples/ssd1306/common" "tinygo.org/x/drivers/ssd1306" ) func main() { machine.I2C0.Configure(machine.I2CConfig{ - Frequency: machine.TWI_FREQ_400KHZ, + Frequency: 400 * machine.KHz, }) display := ssd1306.NewI2C(machine.I2C0) @@ -21,31 +19,5 @@ func main() { 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) - } + common.Loop(display) } diff --git a/examples/ssd1306/i2c_128x64/main.go b/examples/ssd1306/i2c_128x64/main.go index a17010def..f988dcbb7 100644 --- a/examples/ssd1306/i2c_128x64/main.go +++ b/examples/ssd1306/i2c_128x64/main.go @@ -12,15 +12,14 @@ package main import ( "machine" - "image/color" - "time" + "tinygo.org/x/drivers/examples/ssd1306/common" "tinygo.org/x/drivers/ssd1306" ) func main() { machine.I2C0.Configure(machine.I2CConfig{ - Frequency: machine.TWI_FREQ_400KHZ, + Frequency: 400 * machine.KHz, }) display := ssd1306.NewI2C(machine.I2C0) @@ -30,31 +29,5 @@ func main() { 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) - } + common.Loop(display) } diff --git a/examples/ssd1306/spi_128x64/main.go b/examples/ssd1306/spi_128x64/main.go index 094f5cab6..06aa5f77a 100644 --- a/examples/ssd1306/spi_128x64/main.go +++ b/examples/ssd1306/spi_128x64/main.go @@ -1,10 +1,9 @@ package main import ( - "image/color" "machine" - "time" + "tinygo.org/x/drivers/examples/ssd1306/common" "tinygo.org/x/drivers/ssd1306" ) @@ -18,31 +17,5 @@ func main() { 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) - } + common.Loop(display) } diff --git a/examples/ssd1306/spi_thumby/main.go b/examples/ssd1306/spi_thumby/main.go index b2a41e503..e8d95e6fb 100644 --- a/examples/ssd1306/spi_thumby/main.go +++ b/examples/ssd1306/spi_thumby/main.go @@ -3,10 +3,9 @@ package main import ( - "image/color" "machine" - "time" + "tinygo.org/x/drivers/examples/ssd1306/common" "tinygo.org/x/drivers/ssd1306" ) @@ -20,31 +19,5 @@ func main() { 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) - } + common.Loop(display) } From 26f940326fcf66e6c1b5a8a8ca5f3c77ab67f985 Mon Sep 17 00:00:00 2001 From: Yurii Soldak Date: Wed, 25 Jun 2025 01:54:09 +0200 Subject: [PATCH 4/6] ssd1306: bring back the lost exported methods --- ssd1306/ssd1306.go | 116 +++++++++++++++++++++++------------------ ssd1306/ssd1306_i2c.go | 31 +++++++---- ssd1306/ssd1306_spi.go | 30 +++++------ 3 files changed, 100 insertions(+), 77 deletions(-) diff --git a/ssd1306/ssd1306.go b/ssd1306/ssd1306.go index 1241ad252..2e65a3a2f 100644 --- a/ssd1306/ssd1306.go +++ b/ssd1306/ssd1306.go @@ -49,9 +49,10 @@ type Config struct { } type Buser interface { - configure(address uint16, size int16) []byte // configure the bus with the given configuration and return the buffer to use + 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 data in the buffer 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 @@ -89,65 +90,76 @@ func (d *Device) Configure(cfg Config) { d.buffer = d.bus.configure(cfg.Address, d.width*d.height/8) time.Sleep(100 * time.Nanosecond) - d.bus.command(DISPLAYOFF) - d.bus.command(SETDISPLAYCLOCKDIV) - d.bus.command(0x80) - d.bus.command(SETMULTIPLEX) - d.bus.command(uint8(d.height - 1)) - d.bus.command(SETDISPLAYOFFSET) - d.bus.command(0x0) - d.bus.command(SETSTARTLINE | 0x0) - d.bus.command(CHARGEPUMP) + d.Command(DISPLAYOFF) + d.Command(SETDISPLAYCLOCKDIV) + d.Command(0x80) + d.Command(SETMULTIPLEX) + d.Command(uint8(d.height - 1)) + d.Command(SETDISPLAYOFFSET) + d.Command(0x0) + d.Command(SETSTARTLINE | 0x0) + d.Command(CHARGEPUMP) if d.vccState == EXTERNALVCC { - d.bus.command(0x10) + d.Command(0x10) } else { - d.bus.command(0x14) + d.Command(0x14) } - d.bus.command(MEMORYMODE) - d.bus.command(0x00) + d.Command(MEMORYMODE) + d.Command(0x00) d.SetRotation(cfg.Rotation) if (d.width == 128 && d.height == 64) || (d.width == 64 && d.height == 48) { // 128x64 or 64x48 - d.bus.command(SETCOMPINS) - d.bus.command(0x12) - d.bus.command(SETCONTRAST) + d.Command(SETCOMPINS) + d.Command(0x12) + d.Command(SETCONTRAST) if d.vccState == EXTERNALVCC { - d.bus.command(0x9F) + d.Command(0x9F) } else { - d.bus.command(0xCF) + d.Command(0xCF) } } else if d.width == 128 && d.height == 32 { // 128x32 - d.bus.command(SETCOMPINS) - d.bus.command(0x02) - d.bus.command(SETCONTRAST) - d.bus.command(0x8F) + d.Command(SETCOMPINS) + d.Command(0x02) + d.Command(SETCONTRAST) + d.Command(0x8F) } else if d.width == 96 && d.height == 16 { // 96x16 - d.bus.command(SETCOMPINS) - d.bus.command(0x2) - d.bus.command(SETCONTRAST) + d.Command(SETCOMPINS) + d.Command(0x2) + d.Command(SETCONTRAST) if d.vccState == EXTERNALVCC { - d.bus.command(0x10) + d.Command(0x10) } else { - d.bus.command(0xAF) + d.Command(0xAF) } } else { // fail silently, it might work println("there's no configuration for this display's size") } - d.bus.command(SETPRECHARGE) + d.Command(SETPRECHARGE) if d.vccState == EXTERNALVCC { - d.bus.command(0x22) + d.Command(0x22) } else { - d.bus.command(0xF1) + d.Command(0xF1) } - d.bus.command(SETVCOMDETECT) - d.bus.command(0x40) - d.bus.command(DISPLAYALLON_RESUME) - d.bus.command(NORMALDISPLAY) - d.bus.command(DEACTIVATE_SCROLL) - d.bus.command(DISPLAYON) + d.Command(SETVCOMDETECT) + d.Command(0x40) + d.Command(DISPLAYALLON_RESUME) + 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 @@ -170,12 +182,12 @@ func (d *Device) Display() error { // In the 128x64 (SPI) screen resetting to 0x0 after 128 times corrupt the buffer // Since we're printing the whole buffer, avoid resetting it in this case if d.canReset { - d.bus.command(COLUMNADDR) - d.bus.command(d.resetCol[0]) - d.bus.command(d.resetCol[1]) - d.bus.command(PAGEADDR) - d.bus.command(d.resetPage[0]) - d.bus.command(d.resetPage[1]) + d.Command(COLUMNADDR) + d.Command(d.resetCol[0]) + d.Command(d.resetCol[1]) + d.Command(PAGEADDR) + d.Command(d.resetPage[0]) + d.Command(d.resetPage[1]) } return d.bus.flush() @@ -250,15 +262,15 @@ func (d *Device) SetRotation(rotation drivers.Rotation) error { d.rotation = rotation switch d.rotation { case drivers.Rotation0: - d.bus.command(SEGREMAP | 0x1) // Reverse horizontal mapping - d.bus.command(COMSCANDEC) // Reverse vertical mapping + d.Command(SEGREMAP | 0x1) // Reverse horizontal mapping + d.Command(COMSCANDEC) // Reverse vertical mapping case drivers.Rotation180: - d.bus.command(SEGREMAP) // Normal horizontal mapping - d.bus.command(COMSCANINC) // Normal vertical mapping + d.Command(SEGREMAP) // Normal horizontal mapping + d.Command(COMSCANINC) // Normal vertical mapping // nothing to do default: - d.bus.command(SEGREMAP | 0x1) // Reverse horizontal mapping - d.bus.command(COMSCANDEC) // Reverse vertical mapping + d.Command(SEGREMAP | 0x1) // Reverse horizontal mapping + d.Command(COMSCANDEC) // Reverse vertical mapping } return nil } @@ -268,9 +280,9 @@ func (d *Device) SetRotation(rotation drivers.Rotation) error { // should be kept. func (d *Device) Sleep(sleepEnabled bool) error { if sleepEnabled { - d.bus.command(DISPLAYOFF) + d.Command(DISPLAYOFF) } else { - d.bus.command(DISPLAYON) + d.Command(DISPLAYON) } return nil } diff --git a/ssd1306/ssd1306_i2c.go b/ssd1306/ssd1306_i2c.go index 8d48cb206..90ed721b3 100644 --- a/ssd1306/ssd1306_i2c.go +++ b/ssd1306/ssd1306_i2c.go @@ -7,7 +7,7 @@ import ( type I2CBus struct { wire drivers.I2C address uint16 - buffer []byte + buffer []byte // buffer to avoid heap allocations } // NewI2C creates a new SSD1306 connection. The I2C wire must already be configured. @@ -22,15 +22,11 @@ func NewI2C(bus drivers.I2C) Device { // configure address for the I2C bus and allocate the buffer func (b *I2CBus) configure(address uint16, size int16) []byte { - b.address = address - b.buffer = make([]byte, size+2) // +2 for the commands - return b.buffer[2:] // return the buffer without the command part -} - -// flush sends the data part of the buffer to the display -func (b *I2CBus) flush() error { - b.buffer[1] = 0x40 // Data mode - return b.wire.Tx(b.address, b.buffer[1:], nil) + 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 @@ -39,3 +35,18 @@ func (b *I2CBus) command(cmd uint8) error { 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 index 0b985eb15..c45cfd4cb 100644 --- a/ssd1306/ssd1306_spi.go +++ b/ssd1306/ssd1306_spi.go @@ -12,7 +12,7 @@ type SPIBus struct { dcPin machine.Pin resetPin machine.Pin csPin machine.Pin - buffer []byte + buffer []byte // buffer to avoid heap allocations } // NewSPI creates a new SSD1306 connection. The SPI wire must already be configured. @@ -42,27 +42,27 @@ func (b *SPIBus) configure(address uint16, size int16) []byte { time.Sleep(10 * time.Millisecond) b.resetPin.High() - b.buffer = make([]byte, size+1) // +1 for the command - return b.buffer[1:] // return the buffer without the command part -} - -// flush sends the data part of the buffer to the display -func (b *SPIBus) flush() error { - b.csPin.High() - b.dcPin.High() - b.csPin.Low() - err := b.wire.Tx(b.buffer[1:], nil) - b.csPin.High() - return err + 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.Low() + b.dcPin.Set(!isCommand) b.csPin.Low() - err := b.wire.Tx(b.buffer[:1], nil) + err := b.wire.Tx(data, nil) b.csPin.High() return err } From 02f4996312dd33fb4fa41027a8e7c45fba46570a Mon Sep 17 00:00:00 2001 From: Yurii Soldak Date: Fri, 4 Jul 2025 11:41:11 +0200 Subject: [PATCH 5/6] Adjust examples --- examples/ssd1306/i2c_128x32/main.go | 23 ----------- examples/ssd1306/i2c_128x64/main.go | 33 --------------- .../ssd1306/{common/common.go => main.go} | 23 +++++++---- examples/ssd1306/main_i2c_xiao-ble.go | 38 ++++++++++++++++++ .../main.go => main_spi_thumby.go} | 16 +++++--- examples/ssd1306/main_spi_xiao-rp2040.go | 40 +++++++++++++++++++ examples/ssd1306/spi_128x64/main.go | 21 ---------- ssd1306/ssd1306_i2c.go | 4 +- ssd1306/ssd1306_spi.go | 4 +- 9 files changed, 107 insertions(+), 95 deletions(-) delete mode 100644 examples/ssd1306/i2c_128x32/main.go delete mode 100644 examples/ssd1306/i2c_128x64/main.go rename examples/ssd1306/{common/common.go => main.go} (65%) create mode 100644 examples/ssd1306/main_i2c_xiao-ble.go rename examples/ssd1306/{spi_thumby/main.go => main_spi_thumby.go} (61%) create mode 100644 examples/ssd1306/main_spi_xiao-rp2040.go delete mode 100644 examples/ssd1306/spi_128x64/main.go diff --git a/examples/ssd1306/i2c_128x32/main.go b/examples/ssd1306/i2c_128x32/main.go deleted file mode 100644 index ce842851b..000000000 --- a/examples/ssd1306/i2c_128x32/main.go +++ /dev/null @@ -1,23 +0,0 @@ -package main - -import ( - "machine" - - "tinygo.org/x/drivers/examples/ssd1306/common" - "tinygo.org/x/drivers/ssd1306" -) - -func main() { - machine.I2C0.Configure(machine.I2CConfig{ - Frequency: 400 * machine.KHz, - }) - - display := ssd1306.NewI2C(machine.I2C0) - display.Configure(ssd1306.Config{ - Address: ssd1306.Address_128_32, - Width: 128, - Height: 32, - }) - - common.Loop(display) -} diff --git a/examples/ssd1306/i2c_128x64/main.go b/examples/ssd1306/i2c_128x64/main.go deleted file mode 100644 index f988dcbb7..000000000 --- a/examples/ssd1306/i2c_128x64/main.go +++ /dev/null @@ -1,33 +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" - - "tinygo.org/x/drivers/examples/ssd1306/common" - - "tinygo.org/x/drivers/ssd1306" -) - -func main() { - machine.I2C0.Configure(machine.I2CConfig{ - Frequency: 400 * machine.KHz, - }) - - display := ssd1306.NewI2C(machine.I2C0) - display.Configure(ssd1306.Config{ - Address: 0x3C, - Width: 128, - Height: 64, - }) - - common.Loop(display) -} diff --git a/examples/ssd1306/common/common.go b/examples/ssd1306/main.go similarity index 65% rename from examples/ssd1306/common/common.go rename to examples/ssd1306/main.go index aa4dd9cb5..a7f412d48 100644 --- a/examples/ssd1306/common/common.go +++ b/examples/ssd1306/main.go @@ -1,25 +1,31 @@ -package common +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" - - "tinygo.org/x/drivers/ssd1306" ) -var ms = runtime.MemStats{} +func main() { -func Loop(display ssd1306.Device) { + display := newSSD1306Display() display.ClearDisplay() + w, h := display.Size() x := int16(0) y := int16(0) deltaX := int16(1) deltaY := int16(1) - trace := time.Now().UnixMilli() + 1000 + + traceTime := time.Now().UnixMilli() + 1000 frames := 0 + ms := runtime.MemStats{} + for { pixel := display.GetPixel(x, y) c := color.RGBA{255, 255, 255, 255} @@ -42,11 +48,12 @@ func Loop(display ssd1306.Device) { frames++ now := time.Now().UnixMilli() - if now >= trace { + if now >= traceTime { runtime.ReadMemStats(&ms) println("TS", now, "| FPS", frames, "| HeapInuse", ms.HeapInuse) - trace = now + 1000 + 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/spi_thumby/main.go b/examples/ssd1306/main_spi_thumby.go similarity index 61% rename from examples/ssd1306/spi_thumby/main.go rename to examples/ssd1306/main_spi_thumby.go index e8d95e6fb..f68164b18 100644 --- a/examples/ssd1306/spi_thumby/main.go +++ b/examples/ssd1306/main_spi_thumby.go @@ -1,15 +1,20 @@ -// This example using the SSD1306 OLED display over SPI on the Thumby board -// A very tiny 72x40 display. +//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/examples/ssd1306/common" "tinygo.org/x/drivers/ssd1306" ) -func main() { +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{ @@ -18,6 +23,5 @@ func main() { ResetCol: ssd1306.ResetValue{28, 99}, ResetPage: ssd1306.ResetValue{0, 5}, }) - - common.Loop(display) + 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 06aa5f77a..000000000 --- a/examples/ssd1306/spi_128x64/main.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "machine" - - "tinygo.org/x/drivers/examples/ssd1306/common" - "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, - }) - - common.Loop(display) -} diff --git a/ssd1306/ssd1306_i2c.go b/ssd1306/ssd1306_i2c.go index 90ed721b3..19f3a1cc9 100644 --- a/ssd1306/ssd1306_i2c.go +++ b/ssd1306/ssd1306_i2c.go @@ -11,8 +11,8 @@ type I2CBus struct { } // NewI2C creates a new SSD1306 connection. The I2C wire must already be configured. -func NewI2C(bus drivers.I2C) Device { - return Device{ +func NewI2C(bus drivers.I2C) *Device { + return &Device{ bus: &I2CBus{ wire: bus, address: Address, diff --git a/ssd1306/ssd1306_spi.go b/ssd1306/ssd1306_spi.go index c45cfd4cb..d96299de5 100644 --- a/ssd1306/ssd1306_spi.go +++ b/ssd1306/ssd1306_spi.go @@ -16,11 +16,11 @@ type SPIBus struct { } // NewSPI creates a new SSD1306 connection. The SPI wire must already be configured. -func NewSPI(bus drivers.SPI, dcPin, resetPin, csPin machine.Pin) Device { +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{ + return &Device{ bus: &SPIBus{ wire: bus, dcPin: dcPin, From e12783f339fd1c3a537bf4a3419a7e2bb93e2268 Mon Sep 17 00:00:00 2001 From: Yurii Soldak Date: Fri, 4 Jul 2025 11:49:37 +0200 Subject: [PATCH 6/6] Fix smoketests for ssd1306 --- smoketest.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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