Compare commits

...

4 Commits

Author SHA1 Message Date
deadprogram 85e123eeb2 st7789: work in progress on display refactor
Signed-off-by: deadprogram <ron@hybridgroup.com>
2025-12-20 10:29:55 +01:00
deadprogram 51561d4eab st7789: add Bus interface to accomodate both SPI and parallel connections to display
Signed-off-by: deadprogram <ron@hybridgroup.com>
2025-12-20 10:29:55 +01:00
deadprogram b561dc36f2 examples: switch st7789 example to use the Adafruit Clue since that device actually comes with one.
Signed-off-by: deadprogram <ron@hybridgroup.com>
2025-12-20 10:29:55 +01:00
deadprogram 85b24b3418 st7789: remove dependency on the machine package by using internal/pin instead
Signed-off-by: deadprogram <ron@hybridgroup.com>
2025-12-20 10:29:55 +01:00
5 changed files with 206 additions and 118 deletions
+4 -4
View File
@@ -29,10 +29,10 @@ func main() {
Mode: 0, Mode: 0,
}) })
display := st7789.New(machine.SPI0, display := st7789.New(machine.SPI0,
machine.P6, // TFT_RESET machine.TFT_RESET, // TFT_RESET
machine.P7, // TFT_DC machine.TFT_DC, // TFT_DC
machine.P8, // TFT_CS machine.TFT_CS, // TFT_CS
machine.P9) // TFT_LITE machine.TFT_LITE) // TFT_LITE
display.Configure(st7789.Config{ display.Configure(st7789.Config{
Rotation: st7789.NO_ROTATION, Rotation: st7789.NO_ROTATION,
+1 -1
View File
@@ -74,7 +74,7 @@ tinygo build -size short -o ./build/test.hex -target=xiao-rp2040 ./examples/ssd1
tinygo build -size short -o ./build/test.hex -target=thumby ./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=clue ./examples/st7789/main.go
tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/thermistor/main.go tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examples/thermistor/main.go
tinygo build -size short -o ./build/test.hex -target=circuitplay-bluefruit ./examples/tone tinygo build -size short -o ./build/test.hex -target=circuitplay-bluefruit ./examples/tone
tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/tm1637/main.go tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/tm1637/main.go
+8
View File
@@ -0,0 +1,8 @@
package st7789
// Bus is the interface that wraps the basic Tx and Transfer methods
// for communication buses like SPI or Parallel.
type Bus interface {
Tx(w, r []byte) error
Transfer(w byte) (byte, error)
}
+57 -50
View File
@@ -4,56 +4,63 @@ import "tinygo.org/x/drivers"
// Registers // Registers
const ( const (
NOP = 0x00 NOP = 0x00
SWRESET = 0x01 SWRESET = 0x01
RDDID = 0x04 RDDID = 0x04
RDDST = 0x09 RDDST = 0x09
SLPIN = 0x10 SLPIN = 0x10
SLPOUT = 0x11 SLPOUT = 0x11
PTLON = 0x12 PTLON = 0x12
NORON = 0x13 NORON = 0x13
INVOFF = 0x20 INVOFF = 0x20
INVON = 0x21 INVON = 0x21
DISPOFF = 0x28 DISPOFF = 0x28
DISPON = 0x29 DISPON = 0x29
CASET = 0x2A CASET = 0x2A
RASET = 0x2B RASET = 0x2B
RAMWR = 0x2C RAMWR = 0x2C
RAMRD = 0x2E RAMRD = 0x2E
PTLAR = 0x30 PTLAR = 0x30
COLMOD = 0x3A VSCRDEF = 0x33
MADCTL = 0x36 TEOFF = 0x34
MADCTL_MY = 0x80 TEON = 0x35
MADCTL_MX = 0x40 VSCRSADD = 0x37
MADCTL_MV = 0x20 COLMOD = 0x3A
MADCTL_ML = 0x10 MADCTL = 0x36
MADCTL_RGB = 0x00 GSCAN = 0x45
MADCTL_BGR = 0x08 PWCTRL1 = 0xD0
MADCTL_MH = 0x04 RDID1 = 0xDA
RDID1 = 0xDA RDID2 = 0xDB
RDID2 = 0xDB RDID3 = 0xDC
RDID3 = 0xDC RDID4 = 0xDD
RDID4 = 0xDD RAMCTRL = 0xB0
FRMCTR1 = 0xB1 FRMCTR1 = 0xB1
RGBCTRL = 0xB1 RGBCTRL = 0xB1
FRMCTR2 = 0xB2 FRMCTR2 = 0xB2
PORCTRL = 0xB2 PORCTRL = 0xB2
FRMCTR3 = 0xB3 FRMCTR3 = 0xB3
INVCTR = 0xB4 INVCTR = 0xB4
DISSET5 = 0xB6 DISSET5 = 0xB6
PWCTR1 = 0xC0 GCTRL = 0xB7
PWCTR2 = 0xC1 VCOMS = 0xBB
PWCTR3 = 0xC2 LCMCTRL = 0xC0
PWCTR4 = 0xC3 PWCTR2 = 0xC1
PWCTR5 = 0xC4 VDVVRHEN = 0xC2
VMCTR1 = 0xC5 VRHS = 0xC3
FRCTRL2 = 0xC6 VDVS = 0xC4
PWCTR6 = 0xFC VMCTR1 = 0xC5
GMCTRP1 = 0xE0 FRCTRL2 = 0xC6
GMCTRN1 = 0xE1 PVGAMCTRL = 0xE0
GSCAN = 0x45 NVGAMCTRL = 0xE1
VSCRDEF = 0x33 PWCTR6 = 0xFC
VSCRSADD = 0x37
MADCTL_RGB = 0x00
MADCTL_ROWORDER = 0x80
MADCTL_COLORDER = 0x40
MADCTL_SWAPXY = 0x20
MADCTL_SCANORDER = 0x10
MADCTL_BGR = 0x08
MADCTL_MH = 0x04
ColorRGB444 ColorFormat = 0b011 ColorRGB444 ColorFormat = 0b011
ColorRGB565 ColorFormat = 0b101 ColorRGB565 ColorFormat = 0b101
+136 -63
View File
@@ -7,13 +7,14 @@ package st7789 // import "tinygo.org/x/drivers/st7789"
import ( import (
"image/color" "image/color"
"machine"
"math" "math"
"time" "time"
"errors" "errors"
"tinygo.org/x/drivers" "tinygo.org/x/drivers"
"tinygo.org/x/drivers/internal/legacy"
"tinygo.org/x/drivers/internal/pin"
"tinygo.org/x/drivers/pixel" "tinygo.org/x/drivers/pixel"
) )
@@ -45,11 +46,11 @@ type Device = DeviceOf[pixel.RGB565BE]
// DeviceOf is a generic version of Device. It supports multiple different pixel // DeviceOf is a generic version of Device. It supports multiple different pixel
// formats. // formats.
type DeviceOf[T Color] struct { type DeviceOf[T Color] struct {
bus drivers.SPI bus Bus
dcPin machine.Pin dcPin pin.OutputFunc
resetPin machine.Pin resetPin pin.OutputFunc
csPin machine.Pin csPin pin.OutputFunc
blPin machine.Pin blPin pin.OutputFunc
width int16 width int16
height int16 height int16
columnOffsetCfg int16 columnOffsetCfg int16
@@ -68,13 +69,15 @@ type DeviceOf[T Color] struct {
// Config is the configuration for the display // Config is the configuration for the display
type Config struct { type Config struct {
Width int16 Width int16
Height int16 Height int16
Rotation drivers.Rotation Rotation drivers.Rotation
RowOffset int16 RowOffset int16
ColumnOffset int16 ColumnOffset int16
FrameRate FrameRate FrameRate FrameRate
VSyncLines int16 VSyncLines int16
IdleModePorch byte
PartialModePorch byte
// Gamma control. Look in the LCD panel datasheet or provided example code // Gamma control. Look in the LCD panel datasheet or provided example code
// to find these values. If not set, the defaults will be used. // to find these values. If not set, the defaults will be used.
@@ -83,23 +86,27 @@ type Config struct {
} }
// New creates a new ST7789 connection. The SPI wire must already be configured. // New creates a new ST7789 connection. The SPI wire must already be configured.
func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device { func New(bus Bus, resetPin, dcPin, csPin, blPin pin.Output) Device {
return NewOf[pixel.RGB565BE](bus, resetPin, dcPin, csPin, blPin) return NewOf[pixel.RGB565BE](bus, resetPin, dcPin, csPin, blPin)
} }
// NewOf creates a new ST7789 connection with a particular pixel format. The SPI // NewOf creates a new ST7789 connection with a particular pixel format. The SPI
// wire must already be configured. // wire must already be configured.
func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) DeviceOf[T] { func NewOf[T Color](bus Bus, resetPin, dcPin, csPin, blPin pin.Output) DeviceOf[T] {
dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) // IMPORTANT: pin configuration should really be done outside of this
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) // driver, but for backwards compatibility with existing code, we do it
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) // here.
blPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) legacy.ConfigurePinOut(dcPin)
legacy.ConfigurePinOut(resetPin)
legacy.ConfigurePinOut(csPin)
legacy.ConfigurePinOut(blPin)
return DeviceOf[T]{ return DeviceOf[T]{
bus: bus, bus: bus,
dcPin: dcPin, dcPin: dcPin.Set,
resetPin: resetPin, resetPin: resetPin.Set,
csPin: csPin, csPin: csPin.Set,
blPin: blPin, blPin: blPin.Set,
} }
} }
@@ -129,7 +136,7 @@ func (d *DeviceOf[T]) Configure(cfg Config) {
if cfg.VSyncLines >= 2 && cfg.VSyncLines <= 254 { if cfg.VSyncLines >= 2 && cfg.VSyncLines <= 254 {
d.vSyncLines = cfg.VSyncLines d.vSyncLines = cfg.VSyncLines
} else { } else {
d.vSyncLines = 16 d.vSyncLines = 0 // Default: no VSYNC pause
} }
d.batchLength = int32(d.width) d.batchLength = int32(d.width)
@@ -139,21 +146,18 @@ func (d *DeviceOf[T]) Configure(cfg Config) {
d.batchLength += d.batchLength & 1 d.batchLength += d.batchLength & 1
// Reset the device // Reset the device
d.resetPin.High() d.Reset()
time.Sleep(50 * time.Millisecond)
d.resetPin.Low()
time.Sleep(50 * time.Millisecond)
d.resetPin.High()
time.Sleep(50 * time.Millisecond)
// Common initialization // Common initialization
d.startWrite() d.startWrite()
d.sendCommand(SWRESET, nil) // Soft reset d.sendCommand(SWRESET, nil) // Soft reset
d.endWrite() d.endWrite()
time.Sleep(150 * time.Millisecond) //
d.startWrite()
d.sendCommand(SLPOUT, nil) // Exit sleep mode time.Sleep(150 * time.Millisecond) //
d.startWrite()
// enable frame sync signal if used
d.sendCommand(TEON, nil)
// Memory initialization // Memory initialization
var zeroColor T var zeroColor T
@@ -164,54 +168,121 @@ func (d *DeviceOf[T]) Configure(cfg Config) {
// Use default RGB565 color format. // Use default RGB565 color format.
d.setColorFormat(ColorRGB565) // 16 bits per pixel d.setColorFormat(ColorRGB565) // 16 bits per pixel
} }
time.Sleep(10 * time.Millisecond)
d.setRotation(d.rotation) // Memory orientation
d.setWindow(0, 0, d.width, d.height) // Full draw window
d.fillScreen(color.RGBA{0, 0, 0, 255}) // Clear screen
// Framerate
d.sendCommand(FRCTRL2, []byte{byte(d.frameRate)}) // Frame rate for normal mode (default 60Hz)
// Frame vertical sync and "porch" // Frame vertical sync and "porch"
// //
// Front and back porch controls vertical scanline sync time before and after // Front and back porch controls vertical scanline sync time before and after
// a frame, where memory can be safely written without tearing. // a frame, where memory can be safely written without tearing.
// //
fp := uint8(d.vSyncLines / 2) // Split the desired pause half and half // TODO: is this correct? fp := uint8(d.vSyncLines / 2) // Split the desired pause half and half
bp := uint8(d.vSyncLines - int16(fp)) // between front and back porch. fp := byte(0x0c)
// TODO: is this correct? bp := uint8(d.vSyncLines - int16(fp)) // between front and back porch.
bp := byte(0x0c)
if cfg.IdleModePorch == 0 {
cfg.IdleModePorch = 0x22 // Default value
}
if cfg.PartialModePorch == 0 {
cfg.PartialModePorch = 0x22 // Default value
}
d.sendCommand(PORCTRL, []byte{ d.sendCommand(PORCTRL, []byte{
bp, // Back porch 5bit (0x7F max 0x08 default) bp, // Back porch 5bit (0x7F max 0x08 default)
fp, // Front porch 5bit (0x7F max 0x08 default) fp, // Front porch 5bit (0x7F max 0x08 default)
0x00, // Seprarate porch (TODO: what is this?) 0x00, // Separate porch (TODO: what is this?)
0x22, // Idle mode porch (4bit-back 4bit-front 0x22 default) cfg.IdleModePorch, // Idle mode porch (4bit-back 4bit-front 0x22 default)
0x22, // Partial mode porch (4bit-back 4bit-front 0x22 default) cfg.PartialModePorch, // Partial mode porch (4bit-back 4bit-front 0x22 default)
}) })
// Ready to display // LCM control, power on sequence
d.sendCommand(INVON, nil) // Inversion ON d.sendCommand(LCMCTRL, []byte{0x2C})
time.Sleep(10 * time.Millisecond) //
// VDV and VRH Command Enable - 0x01 power on sequence
d.sendCommand(VDVVRHEN, []byte{0x01})
// VRH Set - 0x12 5.3+( vcom+vcom offset+vdv)
d.sendCommand(VRHS, []byte{0x12})
// VDV Set - 0x20 0V
d.sendCommand(VDVS, []byte{0x20})
// PWCTRL1 Power control 1 - power on sequence
d.sendCommand(PWCTRL1, []byte{0xA4, 0xA1})
// Framerate
d.sendCommand(FRCTRL2, []byte{byte(d.frameRate)}) // Frame rate for normal mode (default 60Hz)
// As noted in https://github.com/pimoroni/pimoroni-pico/issues/1040
// this is required to avoid a weird light grey banding issue with low brightness green.
// The banding is not visible without tweaking gamma settings (GMCTRP1 & GMCTRN1) but
// it makes sense to fix it anyway.
d.sendCommand(RAMCTRL, []byte{0x00, 0xC0})
// Set gamma tables, if configured. // Set gamma tables, if configured.
if len(cfg.PVGAMCTRL) == 14 { if len(cfg.PVGAMCTRL) == 14 {
d.sendCommand(GMCTRP1, cfg.PVGAMCTRL) // PVGAMCTRL: Positive Voltage Gamma Control d.sendCommand(PVGAMCTRL, cfg.PVGAMCTRL) // PVGAMCTRL: Positive Voltage Gamma Control
} }
if len(cfg.NVGAMCTRL) == 14 { if len(cfg.NVGAMCTRL) == 14 {
d.sendCommand(GMCTRN1, cfg.NVGAMCTRL) // NVGAMCTRL: Negative Voltage Gamma Control d.sendCommand(NVGAMCTRL, cfg.NVGAMCTRL) // NVGAMCTRL: Negative Voltage Gamma Control
} }
d.sendCommand(NORON, nil) // Normal mode ON switch {
time.Sleep(10 * time.Millisecond) // case d.width == 240 && d.height == 240:
// command(reg::GCTRL, 1, "\x14");
// Gate Control - power on sequence for 240x240
d.sendCommand(GCTRL, []byte{0x35})
d.sendCommand(DISPON, nil) // Screen ON // command(reg::VCOMS, 1, "\x37");
time.Sleep(10 * time.Millisecond) // // VCOM Setting
d.sendCommand(VCOMS, []byte{0x37})
// command(reg::GMCTRP1, 14, "\xD0\x04\x0D\x11\x13\x2B\x3F\x54\x4C\x18\x0D\x0B\x1F\x23");
d.sendCommand(PVGAMCTRL, []byte{0xD0, 0x04, 0x0D, 0x11, 0x13, 0x2B, 0x3F, 0x54, 0x4C, 0x18, 0x0D, 0x0B, 0x1F, 0x23})
// command(reg::GMCTRN1, 14, "\xD0\x04\x0C\x11\x13\x2C\x3F\x44\x51\x2F\x1F\x1F\x20\x23");
d.sendCommand(NVGAMCTRL, []byte{0xD0, 0x04, 0x0C, 0x11, 0x13, 0x2C, 0x3F, 0x44, 0x51, 0x2F, 0x1F, 0x1F, 0x20, 0x23})
case d.width == 320 && d.height == 240:
// Gate Control - power on sequence for 320x240
d.sendCommand(GCTRL, []byte{0x35})
// VCOM Setting - 0.875V
d.sendCommand(VCOMS, []byte{0x1f})
// command(reg::GMCTRP1, 14, "\xD0\x08\x11\x08\x0C\x15\x39\x33\x50\x36\x13\x14\x29\x2D");
d.sendCommand(PVGAMCTRL, []byte{0xD0, 0x08, 0x11, 0x08, 0x0C, 0x15, 0x39, 0x33, 0x50, 0x36, 0x13, 0x14, 0x29, 0x2D})
// command(reg::GMCTRN1, 14, "\xD0\x08\x10\x08\x06\x06\x39\x44\x51\x0B\x16\x14\x2F\x31");
d.sendCommand(NVGAMCTRL, []byte{0xD0, 0x08, 0x10, 0x08, 0x06, 0x06, 0x39, 0x44, 0x51, 0x0B, 0x16, 0x14, 0x2F, 0x31})
}
// Ready to display
d.sendCommand(INVON, nil) // Inversion ON
d.sendCommand(SLPOUT, nil) // Exit sleep mode
d.sendCommand(DISPON, nil) // Screen ON
d.endWrite() d.endWrite()
time.Sleep(100 * time.Millisecond)
d.startWrite()
d.setRotation(d.rotation) // Memory orientation
d.setWindow(0, 0, d.width, d.height) // Full draw window
d.fillScreen(color.RGBA{0, 0, 0, 255}) // Clear screen
d.endWrite()
time.Sleep(50 * time.Millisecond)
d.blPin.High() // Backlight ON d.blPin.High() // Backlight ON
} }
// Reset performs a hardware reset of the display.
func (d *DeviceOf[T]) Reset() {
d.resetPin.High()
time.Sleep(50 * time.Millisecond)
d.resetPin.Low()
time.Sleep(50 * time.Millisecond)
d.resetPin.High()
time.Sleep(50 * time.Millisecond)
}
// Send a command with data to the display. It does not change the chip select // Send a command with data to the display. It does not change the chip select
// pin (it must be low when calling). The DC pin is left high after return, // pin (it must be low when calling). The DC pin is left high after return,
// meaning that data can be sent right away. // meaning that data can be sent right away.
@@ -229,7 +300,7 @@ func (d *DeviceOf[T]) sendCommand(command uint8, data []byte) error {
// startWrite must be called at the beginning of all exported methods to set the // startWrite must be called at the beginning of all exported methods to set the
// chip select pin low. // chip select pin low.
func (d *DeviceOf[T]) startWrite() { func (d *DeviceOf[T]) startWrite() {
if d.csPin != machine.NoPin { if d.csPin != nil {
d.csPin.Low() d.csPin.Low()
} }
} }
@@ -237,7 +308,7 @@ func (d *DeviceOf[T]) startWrite() {
// endWrite must be called at the end of all exported methods to set the chip // endWrite must be called at the end of all exported methods to set the chip
// select pin high. // select pin high.
func (d *DeviceOf[T]) endWrite() { func (d *DeviceOf[T]) endWrite() {
if d.csPin != machine.NoPin { if d.csPin != nil {
d.csPin.High() d.csPin.High()
} }
} }
@@ -509,18 +580,20 @@ func (d *DeviceOf[T]) setRotation(rotation Rotation) error {
madctl := uint8(0) madctl := uint8(0)
switch rotation % 4 { switch rotation % 4 {
case drivers.Rotation0: case drivers.Rotation0:
madctl = MADCTL_COLORDER
madctl |= MADCTL_SWAPXY | MADCTL_SCANORDER
d.rowOffset = 0 d.rowOffset = 0
d.columnOffset = 0 d.columnOffset = 0
case drivers.Rotation90: case drivers.Rotation90:
madctl = MADCTL_MX | MADCTL_MV madctl = MADCTL_COLORDER | MADCTL_SWAPXY
d.rowOffset = 0 d.rowOffset = 0
d.columnOffset = 0 d.columnOffset = 0
case drivers.Rotation180: case drivers.Rotation180:
madctl = MADCTL_MX | MADCTL_MY madctl = MADCTL_COLORDER | MADCTL_ROWORDER
d.rowOffset = d.rowOffsetCfg d.rowOffset = d.rowOffsetCfg
d.columnOffset = d.columnOffsetCfg d.columnOffset = d.columnOffsetCfg
case drivers.Rotation270: case drivers.Rotation270:
madctl = MADCTL_MY | MADCTL_MV madctl = MADCTL_ROWORDER | MADCTL_SWAPXY
d.rowOffset = d.columnOffsetCfg d.rowOffset = d.columnOffsetCfg
d.columnOffset = d.rowOffsetCfg d.columnOffset = d.rowOffsetCfg
} }