ssd1306: avoid unnecessary heap allocations (#767)

* ssd1306: avoid unnecessary heap allocations

* ssd1306: extract i2c and spi bus implementations

* ssd1306: refactor tests -- show fps and heap usage

* ssd1306: bring back the lost exported methods

* Adjust examples

* Fix smoketests for ssd1306
This commit is contained in:
Yurii Soldak
2025-07-13 14:53:58 +02:00
committed by deadprogram
parent 45fad80c3e
commit ae9e8f915e
12 changed files with 316 additions and 348 deletions
-51
View File
@@ -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)
}
}
-60
View File
@@ -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)
}
}
+59
View File
@@ -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
}
}
}
+38
View File
@@ -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
}
+27
View File
@@ -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
}
+40
View File
@@ -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
}
-48
View File
@@ -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)
}
}
-50
View File
@@ -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)
}
}
+3 -2
View File
@@ -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/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/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/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=xiao-ble ./examples/ssd1306/
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-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/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/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=microbit ./examples/st7789/main.go
+29 -137
View File
@@ -6,11 +6,9 @@ package ssd1306 // import "tinygo.org/x/drivers/ssd1306"
import ( import (
"errors" "errors"
"image/color" "image/color"
"machine"
"time" "time"
"tinygo.org/x/drivers" "tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/legacy"
"tinygo.org/x/drivers/pixel" "tinygo.org/x/drivers/pixel"
) )
@@ -23,16 +21,15 @@ type ResetValue [2]byte
// Device wraps I2C or SPI connection. // Device wraps I2C or SPI connection.
type Device struct { type Device struct {
bus Buser bus Buser
buffer []byte buffer []byte
width int16 width int16
height int16 height int16
bufferSize int16 vccState VccMode
vccState VccMode canReset bool
canReset bool resetCol ResetValue
resetCol ResetValue resetPage ResetValue
resetPage ResetValue rotation drivers.Rotation
rotation drivers.Rotation
} }
// Config is the configuration for the display // Config is the configuration for the display
@@ -51,51 +48,15 @@ type Config struct {
Rotation drivers.Rotation 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 { type Buser interface {
configure() error configure(address uint16, size int16) []byte // configure the bus and return the image buffer to use
tx(data []byte, isCommand bool) error command(cmd uint8) error // send a command to the display
setAddress(address uint16) error 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 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 // Configure initializes the display with default configuration
func (d *Device) Configure(cfg Config) { func (d *Device) Configure(cfg Config) {
var zeroReset ResetValue var zeroReset ResetValue
@@ -109,9 +70,6 @@ func (d *Device) Configure(cfg Config) {
} else { } else {
d.height = 64 d.height = 64
} }
if cfg.Address != 0 {
d.bus.setAddress(cfg.Address)
}
if cfg.VccState != 0 { if cfg.VccState != 0 {
d.vccState = cfg.VccState d.vccState = cfg.VccState
} else { } else {
@@ -127,11 +85,9 @@ func (d *Device) Configure(cfg Config) {
} else { } else {
d.resetPage = ResetValue{0, uint8(d.height/8) - 1} 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.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) time.Sleep(100 * time.Nanosecond)
d.Command(DISPLAYOFF) d.Command(DISPLAYOFF)
@@ -193,11 +149,22 @@ func (d *Device) Configure(cfg Config) {
d.Command(NORMALDISPLAY) d.Command(NORMALDISPLAY)
d.Command(DEACTIVATE_SCROLL) d.Command(DEACTIVATE_SCROLL)
d.Command(DISPLAYON) 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 // ClearBuffer clears the image buffer
func (d *Device) ClearBuffer() { func (d *Device) ClearBuffer() {
for i := int16(0); i < d.bufferSize; i++ { for i := 0; i < len(d.buffer); i++ {
d.buffer[i] = 0 d.buffer[i] = 0
} }
} }
@@ -223,7 +190,7 @@ func (d *Device) Display() error {
d.Command(d.resetPage[1]) d.Command(d.resetPage[1])
} }
return d.Tx(d.buffer, false) return d.bus.flush()
} }
// SetPixel enables or disables a pixel in the buffer // 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 // SetBuffer changes the whole buffer at once
func (d *Device) SetBuffer(buffer []byte) error { func (d *Device) SetBuffer(buffer []byte) error {
if int16(len(buffer)) != d.bufferSize { if len(buffer) != len(d.buffer) {
return errBufferSize return errBufferSize
} }
for i := int16(0); i < d.bufferSize; i++ { copy(d.buffer, buffer)
d.buffer[i] = buffer[i]
}
return nil return nil
} }
@@ -266,79 +231,6 @@ func (d *Device) GetBuffer() []byte {
return d.buffer 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. // Size returns the current size of the display.
func (d *Device) Size() (w, h int16) { func (d *Device) Size() (w, h int16) {
return d.width, d.height return d.width, d.height
+52
View File
@@ -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()
}
+68
View File
@@ -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
}