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)
}
+23 -16
View File
@@ -21,19 +21,19 @@ const (
RAMWR = 0x2C RAMWR = 0x2C
RAMRD = 0x2E RAMRD = 0x2E
PTLAR = 0x30 PTLAR = 0x30
VSCRDEF = 0x33
TEOFF = 0x34
TEON = 0x35
VSCRSADD = 0x37
COLMOD = 0x3A COLMOD = 0x3A
MADCTL = 0x36 MADCTL = 0x36
MADCTL_MY = 0x80 GSCAN = 0x45
MADCTL_MX = 0x40 PWCTRL1 = 0xD0
MADCTL_MV = 0x20
MADCTL_ML = 0x10
MADCTL_RGB = 0x00
MADCTL_BGR = 0x08
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
@@ -41,19 +41,26 @@ const (
FRMCTR3 = 0xB3 FRMCTR3 = 0xB3
INVCTR = 0xB4 INVCTR = 0xB4
DISSET5 = 0xB6 DISSET5 = 0xB6
PWCTR1 = 0xC0 GCTRL = 0xB7
VCOMS = 0xBB
LCMCTRL = 0xC0
PWCTR2 = 0xC1 PWCTR2 = 0xC1
PWCTR3 = 0xC2 VDVVRHEN = 0xC2
PWCTR4 = 0xC3 VRHS = 0xC3
PWCTR5 = 0xC4 VDVS = 0xC4
VMCTR1 = 0xC5 VMCTR1 = 0xC5
FRCTRL2 = 0xC6 FRCTRL2 = 0xC6
PVGAMCTRL = 0xE0
NVGAMCTRL = 0xE1
PWCTR6 = 0xFC PWCTR6 = 0xFC
GMCTRP1 = 0xE0
GMCTRN1 = 0xE1 MADCTL_RGB = 0x00
GSCAN = 0x45 MADCTL_ROWORDER = 0x80
VSCRDEF = 0x33 MADCTL_COLORDER = 0x40
VSCRSADD = 0x37 MADCTL_SWAPXY = 0x20
MADCTL_SCANORDER = 0x10
MADCTL_BGR = 0x08
MADCTL_MH = 0x04
ColorRGB444 ColorFormat = 0b011 ColorRGB444 ColorFormat = 0b011
ColorRGB565 ColorFormat = 0b101 ColorRGB565 ColorFormat = 0b101
+127 -54
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
@@ -75,6 +76,8 @@ type Config struct {
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})
// command(reg::VCOMS, 1, "\x37");
// 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.sendCommand(DISPON, nil) // Screen ON
time.Sleep(10 * time.Millisecond) //
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
} }