st7735: make the display generic over RGB565 and RGB444

Using RGB444 instead of RGB565 can speed up graphics operations by up to
25%, especially on slow screens. But for full support, all parts of the
driver need to be aware of the color format.

It's possible to do this using a regular configuration variable, but
it's unlikely to be very efficient. Hence the usage of generics.
This commit is contained in:
Ayke van Laethem
2023-11-09 18:35:28 +01:00
committed by Ron Evans
parent f96a70915e
commit 4a9667ffef
+61 -52
View File
@@ -11,6 +11,7 @@ import (
"errors"
"tinygo.org/x/drivers"
"tinygo.org/x/drivers/pixel"
)
type Model uint8
@@ -20,12 +21,23 @@ type Model uint8
// Deprecated: use drivers.Rotation instead.
type Rotation = drivers.Rotation
// Pixel formats supported by the st7735 driver.
type Color interface {
pixel.RGB444BE | pixel.RGB565BE
pixel.BaseColor
}
var (
errOutOfBounds = errors.New("rectangle coordinates outside display area")
)
// Device wraps an SPI connection.
type Device struct {
type Device = DeviceOf[pixel.RGB565BE]
// DeviceOf is a generic version of Device, which supports different pixel
// formats.
type DeviceOf[T Color] struct {
bus drivers.SPI
dcPin machine.Pin
resetPin machine.Pin
@@ -39,7 +51,7 @@ type Device struct {
batchLength int16
model Model
isBGR bool
batchData []uint8
batchData pixel.Image[T] // "image" with width, height of (batchLength, 1)
}
// Config is the configuration for the display
@@ -54,11 +66,17 @@ type Config struct {
// New creates a new ST7735 connection. The SPI wire must already be configured.
func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device {
return NewOf[pixel.RGB565BE](bus, resetPin, dcPin, csPin, blPin)
}
// NewOf creates a new ST7735 connection with a particular pixel format. The SPI
// wire must already be configured.
func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) DeviceOf[T] {
dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
blPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
return Device{
return DeviceOf[T]{
bus: bus,
dcPin: dcPin,
resetPin: resetPin,
@@ -68,7 +86,7 @@ func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device {
}
// Configure initializes the display with default configuration
func (d *Device) Configure(cfg Config) {
func (d *DeviceOf[T]) Configure(cfg Config) {
d.model = cfg.Model
if cfg.Width != 0 {
d.width = cfg.Width
@@ -93,7 +111,7 @@ func (d *Device) Configure(cfg Config) {
d.batchLength = d.height
}
d.batchLength += d.batchLength & 1
d.batchData = make([]uint8, d.batchLength*2)
d.batchData = pixel.NewImage[T](int(d.batchLength), 1)
// reset the device
d.resetPin.High()
@@ -142,8 +160,16 @@ func (d *Device) Configure(cfg Config) {
d.Data(0xEE)
d.Command(VMCTR1)
d.Data(0x0E)
// Set the color format depending on the generic type.
d.Command(COLMOD)
d.Data(0x05)
var zeroColor T
switch any(zeroColor).(type) {
case pixel.RGB444BE:
d.Data(0x03) // 12 bits per pixel
default:
d.Data(0x05) // 16 bits per pixel
}
if d.model == GREENTAB {
d.InvertColors(false)
@@ -204,12 +230,12 @@ func (d *Device) Configure(cfg Config) {
}
// Display does nothing, there's no buffer as it might be too big for some boards
func (d *Device) Display() error {
func (d *DeviceOf[T]) Display() error {
return nil
}
// SetPixel sets a pixel in the screen
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
func (d *DeviceOf[T]) SetPixel(x int16, y int16, c color.RGBA) {
w, h := d.Size()
if x < 0 || y < 0 || x >= w || y >= h {
return
@@ -218,7 +244,7 @@ func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
}
// setWindow prepares the screen to be modified at a given rectangle
func (d *Device) setWindow(x, y, w, h int16) {
func (d *DeviceOf[T]) setWindow(x, y, w, h int16) {
if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 {
x += d.columnOffset
y += d.rowOffset
@@ -234,7 +260,7 @@ func (d *Device) setWindow(x, y, w, h int16) {
}
// SetScrollWindow sets an area to scroll with fixed top and bottom parts of the display
func (d *Device) SetScrollArea(topFixedArea, bottomFixedArea int16) {
func (d *DeviceOf[T]) SetScrollArea(topFixedArea, bottomFixedArea int16) {
// TODO: this code is broken, see the st7789 and ili9341 implementations for
// how to do this correctly.
d.Command(VSCRDEF)
@@ -246,38 +272,32 @@ func (d *Device) SetScrollArea(topFixedArea, bottomFixedArea int16) {
}
// SetScroll sets the vertical scroll address of the display.
func (d *Device) SetScroll(line int16) {
func (d *DeviceOf[T]) SetScroll(line int16) {
d.Command(VSCRSADD)
d.Tx([]uint8{uint8(line >> 8), uint8(line)}, false)
}
// SpotScroll returns the display to its normal state
func (d *Device) StopScroll() {
func (d *DeviceOf[T]) StopScroll() {
d.Command(NORON)
}
// FillRectangle fills a rectangle at a given coordinates with a color
func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error {
func (d *DeviceOf[T]) FillRectangle(x, y, width, height int16, c color.RGBA) error {
k, i := d.Size()
if x < 0 || y < 0 || width <= 0 || height <= 0 ||
x >= k || (x+width) > k || y >= i || (y+height) > i {
return errors.New("rectangle coordinates outside display area")
}
d.setWindow(x, y, width, height)
c565 := RGBATo565(c)
c1 := uint8(c565 >> 8)
c2 := uint8(c565)
for i = 0; i < d.batchLength; i++ {
d.batchData[i*2] = c1
d.batchData[i*2+1] = c2
}
d.batchData.FillSolidColor(pixel.NewColor[T](c.R, c.G, c.B))
i = width * height
for i > 0 {
if i >= d.batchLength {
d.Tx(d.batchData, false)
d.Tx(d.batchData.RawBuffer(), false)
} else {
d.Tx(d.batchData[:i*2], false)
d.Tx(d.batchData.Rescale(int(i), 1).RawBuffer(), false)
}
i -= d.batchLength
}
@@ -285,7 +305,7 @@ func (d *Device) FillRectangle(x, y, width, height int16, c color.RGBA) error {
}
// DrawRGBBitmap8 copies an RGB bitmap to the internal buffer at given coordinates
func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error {
func (d *DeviceOf[T]) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error {
k, i := d.Size()
if x < 0 || y < 0 || w <= 0 || h <= 0 ||
x >= k || (x+w) > k || y >= i || (y+h) > i {
@@ -297,7 +317,7 @@ func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error {
}
// FillRectangle fills a rectangle at a given coordinates with a buffer
func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error {
func (d *DeviceOf[T]) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error {
k, l := d.Size()
if x < 0 || y < 0 || width <= 0 || height <= 0 ||
x >= k || (x+width) > k || y >= l || (y+height) > l {
@@ -315,17 +335,14 @@ func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []col
for k > 0 {
for i := int16(0); i < d.batchLength; i++ {
if offset+i < l {
c565 := RGBATo565(buffer[offset+i])
c1 := uint8(c565 >> 8)
c2 := uint8(c565)
d.batchData[i*2] = c1
d.batchData[i*2+1] = c2
c := buffer[offset+i]
d.batchData.Set(int(i), 0, pixel.NewColor[T](c.R, c.G, c.B))
}
}
if k >= d.batchLength {
d.Tx(d.batchData, false)
d.Tx(d.batchData.RawBuffer(), false)
} else {
d.Tx(d.batchData[:k*2], false)
d.Tx(d.batchData.Rescale(int(k), 1).RawBuffer(), false)
}
k -= d.batchLength
offset += d.batchLength
@@ -334,7 +351,7 @@ func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []col
}
// DrawFastVLine draws a vertical line faster than using SetPixel
func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) {
func (d *DeviceOf[T]) DrawFastVLine(x, y0, y1 int16, c color.RGBA) {
if y0 > y1 {
y0, y1 = y1, y0
}
@@ -342,7 +359,7 @@ func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) {
}
// DrawFastHLine draws a horizontal line faster than using SetPixel
func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) {
func (d *DeviceOf[T]) DrawFastHLine(x0, x1, y int16, c color.RGBA) {
if x0 > x1 {
x0, x1 = x1, x0
}
@@ -350,7 +367,7 @@ func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) {
}
// FillScreen fills the screen with a given color
func (d *Device) FillScreen(c color.RGBA) {
func (d *DeviceOf[T]) FillScreen(c color.RGBA) {
if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 {
d.FillRectangle(0, 0, d.width, d.height, c)
} else {
@@ -359,12 +376,12 @@ func (d *Device) FillScreen(c color.RGBA) {
}
// Rotation returns the currently configured rotation.
func (d *Device) Rotation() drivers.Rotation {
func (d *DeviceOf[T]) Rotation() drivers.Rotation {
return d.rotation
}
// SetRotation changes the rotation of the device (clock-wise)
func (d *Device) SetRotation(rotation drivers.Rotation) error {
func (d *DeviceOf[T]) SetRotation(rotation drivers.Rotation) error {
d.rotation = rotation
madctl := uint8(0)
switch rotation % 4 {
@@ -386,23 +403,23 @@ func (d *Device) SetRotation(rotation drivers.Rotation) error {
}
// Command sends a command to the display
func (d *Device) Command(command uint8) {
func (d *DeviceOf[T]) Command(command uint8) {
d.Tx([]byte{command}, true)
}
// Command sends a data to the display
func (d *Device) Data(data uint8) {
func (d *DeviceOf[T]) Data(data uint8) {
d.Tx([]byte{data}, false)
}
// Tx sends data to the display
func (d *Device) Tx(data []byte, isCommand bool) {
func (d *DeviceOf[T]) Tx(data []byte, isCommand bool) {
d.dcPin.Set(!isCommand)
d.bus.Tx(data, nil)
}
// Size returns the current size of the display.
func (d *Device) Size() (w, h int16) {
func (d *DeviceOf[T]) Size() (w, h int16) {
if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 {
return d.width, d.height
}
@@ -410,7 +427,7 @@ func (d *Device) Size() (w, h int16) {
}
// EnableBacklight enables or disables the backlight
func (d *Device) EnableBacklight(enable bool) {
func (d *DeviceOf[T]) EnableBacklight(enable bool) {
if enable {
d.blPin.High()
} else {
@@ -421,7 +438,7 @@ func (d *Device) EnableBacklight(enable bool) {
// Set the sleep mode for this LCD panel. When sleeping, the panel uses a lot
// less power. The LCD won't display an image anymore, but the memory contents
// will be kept.
func (d *Device) Sleep(sleepEnabled bool) error {
func (d *DeviceOf[T]) Sleep(sleepEnabled bool) error {
if sleepEnabled {
// Shut down LCD panel.
d.Command(SLPIN)
@@ -437,7 +454,7 @@ func (d *Device) Sleep(sleepEnabled bool) error {
}
// InverColors inverts the colors of the screen
func (d *Device) InvertColors(invert bool) {
func (d *DeviceOf[T]) InvertColors(invert bool) {
if invert {
d.Command(INVON)
} else {
@@ -446,14 +463,6 @@ func (d *Device) InvertColors(invert bool) {
}
// IsBGR changes the color mode (RGB/BGR)
func (d *Device) IsBGR(bgr bool) {
func (d *DeviceOf[T]) IsBGR(bgr bool) {
d.isBGR = bgr
}
// RGBATo565 converts a color.RGBA to uint16 used in the display
func RGBATo565(c color.RGBA) uint16 {
r, g, b, _ := c.RGBA()
return uint16((r & 0xF800) +
((g & 0xFC00) >> 5) +
((b & 0xF800) >> 11))
}