mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-02 22:17:49 +00:00
ae9e8f915e
* 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
317 lines
8.1 KiB
Go
317 lines
8.1 KiB
Go
// Package ssd1306 implements a driver for the SSD1306 led matrix controller, it comes in various colors and screen sizes.
|
|
//
|
|
// Datasheet: https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf
|
|
package ssd1306 // import "tinygo.org/x/drivers/ssd1306"
|
|
|
|
import (
|
|
"errors"
|
|
"image/color"
|
|
"time"
|
|
|
|
"tinygo.org/x/drivers"
|
|
"tinygo.org/x/drivers/pixel"
|
|
)
|
|
|
|
var (
|
|
errBufferSize = errors.New("invalid size buffer")
|
|
errOutOfRange = errors.New("out of screen range")
|
|
)
|
|
|
|
type ResetValue [2]byte
|
|
|
|
// Device wraps I2C or SPI connection.
|
|
type Device struct {
|
|
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
|
|
type Config struct {
|
|
Width int16
|
|
Height int16
|
|
VccState VccMode
|
|
Address uint16
|
|
// ResetCol and ResetPage are used to reset the screen to 0x0
|
|
// This is useful for some screens that have a different size than 128x64
|
|
// For example, the Thumby's screen is 72x40
|
|
// The default values are normally set automatically based on the size.
|
|
// If you're using a different size, you might need to set these values manually.
|
|
ResetCol ResetValue
|
|
ResetPage ResetValue
|
|
Rotation drivers.Rotation
|
|
}
|
|
|
|
type Buser interface {
|
|
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
|
|
|
|
// Configure initializes the display with default configuration
|
|
func (d *Device) Configure(cfg Config) {
|
|
var zeroReset ResetValue
|
|
if cfg.Width != 0 {
|
|
d.width = cfg.Width
|
|
} else {
|
|
d.width = 128
|
|
}
|
|
if cfg.Height != 0 {
|
|
d.height = cfg.Height
|
|
} else {
|
|
d.height = 64
|
|
}
|
|
if cfg.VccState != 0 {
|
|
d.vccState = cfg.VccState
|
|
} else {
|
|
d.vccState = SWITCHCAPVCC
|
|
}
|
|
if cfg.ResetCol != zeroReset {
|
|
d.resetCol = cfg.ResetCol
|
|
} else {
|
|
d.resetCol = ResetValue{0, uint8(d.width - 1)}
|
|
}
|
|
if cfg.ResetPage != zeroReset {
|
|
d.resetPage = cfg.ResetPage
|
|
} else {
|
|
d.resetPage = ResetValue{0, uint8(d.height/8) - 1}
|
|
}
|
|
d.canReset = cfg.Address != 0 || d.width != 128 || d.height != 64 // I2C or not 128x64
|
|
|
|
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)
|
|
if d.vccState == EXTERNALVCC {
|
|
d.Command(0x10)
|
|
} else {
|
|
d.Command(0x14)
|
|
}
|
|
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.Command(SETCOMPINS)
|
|
d.Command(0x12)
|
|
d.Command(SETCONTRAST)
|
|
if d.vccState == EXTERNALVCC {
|
|
d.Command(0x9F)
|
|
} else {
|
|
d.Command(0xCF)
|
|
}
|
|
} else if d.width == 128 && d.height == 32 { // 128x32
|
|
d.Command(SETCOMPINS)
|
|
d.Command(0x02)
|
|
d.Command(SETCONTRAST)
|
|
d.Command(0x8F)
|
|
} else if d.width == 96 && d.height == 16 { // 96x16
|
|
d.Command(SETCOMPINS)
|
|
d.Command(0x2)
|
|
d.Command(SETCONTRAST)
|
|
if d.vccState == EXTERNALVCC {
|
|
d.Command(0x10)
|
|
} else {
|
|
d.Command(0xAF)
|
|
}
|
|
} else {
|
|
// fail silently, it might work
|
|
println("there's no configuration for this display's size")
|
|
}
|
|
|
|
d.Command(SETPRECHARGE)
|
|
if d.vccState == EXTERNALVCC {
|
|
d.Command(0x22)
|
|
} else {
|
|
d.Command(0xF1)
|
|
}
|
|
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
|
|
func (d *Device) ClearBuffer() {
|
|
for i := 0; i < len(d.buffer); i++ {
|
|
d.buffer[i] = 0
|
|
}
|
|
}
|
|
|
|
// ClearDisplay clears the image buffer and clear the display
|
|
func (d *Device) ClearDisplay() {
|
|
d.ClearBuffer()
|
|
d.Display()
|
|
}
|
|
|
|
// Display sends the whole buffer to the screen
|
|
func (d *Device) Display() error {
|
|
// Reset the screen to 0x0
|
|
// This works fine with I2C
|
|
// 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])
|
|
}
|
|
|
|
return d.bus.flush()
|
|
}
|
|
|
|
// SetPixel enables or disables a pixel in the buffer
|
|
// color.RGBA{0, 0, 0, 255} is consider transparent, anything else
|
|
// with enable a pixel on the screen
|
|
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
|
|
if x < 0 || x >= d.width || y < 0 || y >= d.height {
|
|
return
|
|
}
|
|
byteIndex := x + (y/8)*d.width
|
|
if c.R != 0 || c.G != 0 || c.B != 0 {
|
|
d.buffer[byteIndex] |= 1 << uint8(y%8)
|
|
} else {
|
|
d.buffer[byteIndex] &^= 1 << uint8(y%8)
|
|
}
|
|
}
|
|
|
|
// GetPixel returns if the specified pixel is on (true) or off (false)
|
|
func (d *Device) GetPixel(x int16, y int16) bool {
|
|
if x < 0 || x >= d.width || y < 0 || y >= d.height {
|
|
return false
|
|
}
|
|
byteIndex := x + (y/8)*d.width
|
|
return (d.buffer[byteIndex] >> uint8(y%8) & 0x1) == 1
|
|
}
|
|
|
|
// SetBuffer changes the whole buffer at once
|
|
func (d *Device) SetBuffer(buffer []byte) error {
|
|
if len(buffer) != len(d.buffer) {
|
|
return errBufferSize
|
|
}
|
|
copy(d.buffer, buffer)
|
|
return nil
|
|
}
|
|
|
|
// GetBuffer returns the whole buffer
|
|
func (d *Device) GetBuffer() []byte {
|
|
return d.buffer
|
|
}
|
|
|
|
// Size returns the current size of the display.
|
|
func (d *Device) Size() (w, h int16) {
|
|
return d.width, d.height
|
|
}
|
|
|
|
// DrawBitmap copies the bitmap to the screen at the given coordinates.
|
|
func (d *Device) DrawBitmap(x, y int16, bitmap pixel.Image[pixel.Monochrome]) error {
|
|
width, height := bitmap.Size()
|
|
if x < 0 || x+int16(width) > d.width || y < 0 || y+int16(height) > d.height {
|
|
return errOutOfRange
|
|
}
|
|
|
|
for i := 0; i < width; i++ {
|
|
for j := 0; j < height; j++ {
|
|
d.SetPixel(x+int16(i), y+int16(j), bitmap.Get(i, j).RGBA())
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Rotation returns the currently configured rotation.
|
|
func (d *Device) Rotation() drivers.Rotation {
|
|
return d.rotation
|
|
}
|
|
|
|
// SetRotation changes the rotation of the device (clock-wise).
|
|
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
|
|
case drivers.Rotation180:
|
|
d.Command(SEGREMAP) // Normal horizontal mapping
|
|
d.Command(COMSCANINC) // Normal vertical mapping
|
|
// nothing to do
|
|
default:
|
|
d.Command(SEGREMAP | 0x1) // Reverse horizontal mapping
|
|
d.Command(COMSCANDEC) // Reverse vertical mapping
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Set the sleep mode for this display. When sleeping, the panel uses a lot
|
|
// less power. The display won't show an image anymore, but the memory contents
|
|
// should be kept.
|
|
func (d *Device) Sleep(sleepEnabled bool) error {
|
|
if sleepEnabled {
|
|
d.Command(DISPLAYOFF)
|
|
} else {
|
|
d.Command(DISPLAYON)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FillRectangle fills a rectangle at a given coordinates with a color
|
|
func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error {
|
|
dw, dh := d.Size()
|
|
|
|
if x < 0 || y < 0 || width <= 0 || height <= 0 ||
|
|
x >= d.width || (x+width) > dw || y >= dh || (y+height) > dh {
|
|
return errOutOfRange
|
|
}
|
|
|
|
if x+width == dw && y+height == dh && c.R == 0 && c.G == 0 && c.B == 0 {
|
|
d.ClearDisplay()
|
|
return nil
|
|
}
|
|
|
|
for i := x; i < x+width; i++ {
|
|
for j := y; j < y+height; j++ {
|
|
d.SetPixel(i, j, c)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetScroll sets the vertical scrolling for the display, which is a NOP for this display.
|
|
func (d *Device) SetScroll(line int16) {
|
|
return
|
|
}
|