st7789: make the display generic over RGB565 and RGB444

Same as for st7735 in the previous commit.

In addition, this avoids allocating a big chunk of memory on _every_
draw operation (even SetPixel) and instead reuses it across draw
operations. This makes the driver a whole lot more efficient.
This commit is contained in:
Ayke van Laethem
2023-11-09 18:44:17 +01:00
committed by Ron Evans
parent 4a9667ffef
commit 100aadf585
+90 -72
View File
@@ -14,6 +14,7 @@ import (
"errors" "errors"
"tinygo.org/x/drivers" "tinygo.org/x/drivers"
"tinygo.org/x/drivers/pixel"
) )
// Rotation controls the rotation used by the display. // Rotation controls the rotation used by the display.
@@ -24,6 +25,13 @@ type Rotation = drivers.Rotation
// The color format used on the display, like RGB565, RGB666, and RGB444. // The color format used on the display, like RGB565, RGB666, and RGB444.
type ColorFormat uint8 type ColorFormat uint8
// Pixel formats supported by the st7789 driver.
type Color interface {
pixel.RGB444BE | pixel.RGB565BE
pixel.BaseColor
}
// FrameRate controls the frame rate used by the display. // FrameRate controls the frame rate used by the display.
type FrameRate uint8 type FrameRate uint8
@@ -32,7 +40,11 @@ var (
) )
// Device wraps an SPI connection. // Device wraps an SPI connection.
type Device struct { type Device = DeviceOf[pixel.RGB565BE]
// DeviceOf is a generic version of Device. It supports multiple different pixel
// formats.
type DeviceOf[T Color] struct {
bus drivers.SPI bus drivers.SPI
dcPin machine.Pin dcPin machine.Pin
resetPin machine.Pin resetPin machine.Pin
@@ -47,6 +59,7 @@ type Device struct {
rotation drivers.Rotation rotation drivers.Rotation
frameRate FrameRate frameRate FrameRate
batchLength int32 batchLength int32
batchData pixel.Image[T] // "image" with (width, height) of (batchLength, 1)
isBGR bool isBGR bool
vSyncLines int16 vSyncLines int16
cmdBuf [1]byte cmdBuf [1]byte
@@ -71,11 +84,17 @@ 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 drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device {
return NewOf[pixel.RGB565BE](bus, resetPin, dcPin, csPin, blPin)
}
// NewOf creates a new ST7789 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}) dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) csPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
blPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) blPin.Configure(machine.PinConfig{Mode: machine.PinOutput})
return Device{ return DeviceOf[T]{
bus: bus, bus: bus,
dcPin: dcPin, dcPin: dcPin,
resetPin: resetPin, resetPin: resetPin,
@@ -85,7 +104,7 @@ func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device {
} }
// Configure initializes the display with default configuration // Configure initializes the display with default configuration
func (d *Device) Configure(cfg Config) { func (d *DeviceOf[T]) Configure(cfg Config) {
if cfg.Width != 0 { if cfg.Width != 0 {
d.width = cfg.Width d.width = cfg.Width
} else { } else {
@@ -137,7 +156,14 @@ func (d *Device) Configure(cfg Config) {
d.sendCommand(SLPOUT, nil) // Exit sleep mode d.sendCommand(SLPOUT, nil) // Exit sleep mode
// Memory initialization // Memory initialization
d.setColorFormat(ColorRGB565) // Set color mode to 16-bit color var zeroColor T
switch any(zeroColor).(type) {
case pixel.RGB444BE:
d.setColorFormat(ColorRGB444) // 12 bits per pixel
default:
// Use default RGB565 color format.
d.setColorFormat(ColorRGB565) // 16 bits per pixel
}
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
d.setRotation(d.rotation) // Memory orientation d.setRotation(d.rotation) // Memory orientation
@@ -189,7 +215,7 @@ func (d *Device) Configure(cfg Config) {
// 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.
func (d *Device) sendCommand(command uint8, data []byte) error { func (d *DeviceOf[T]) sendCommand(command uint8, data []byte) error {
d.cmdBuf[0] = command d.cmdBuf[0] = command
d.dcPin.Low() d.dcPin.Low()
err := d.bus.Tx(d.cmdBuf[:1], nil) err := d.bus.Tx(d.cmdBuf[:1], nil)
@@ -202,7 +228,7 @@ func (d *Device) 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 *Device) startWrite() { func (d *DeviceOf[T]) startWrite() {
if d.csPin != machine.NoPin { if d.csPin != machine.NoPin {
d.csPin.Low() d.csPin.Low()
} }
@@ -210,14 +236,23 @@ func (d *Device) 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 *Device) endWrite() { func (d *DeviceOf[T]) endWrite() {
if d.csPin != machine.NoPin { if d.csPin != machine.NoPin {
d.csPin.High() d.csPin.High()
} }
} }
// getBuffer returns the image buffer, that's always d.batchLength wide and 1
// pixel high. It can be used as a temporary buffer to transmit image data.
func (d *DeviceOf[T]) getBuffer() pixel.Image[T] {
if d.batchData.Len() == 0 {
d.batchData = pixel.NewImage[T](int(d.batchLength), 1)
}
return d.batchData
}
// Sync waits for the display to hit the next VSYNC pause // Sync waits for the display to hit the next VSYNC pause
func (d *Device) Sync() { func (d *DeviceOf[T]) Sync() {
d.SyncToScanLine(0) d.SyncToScanLine(0)
} }
@@ -232,7 +267,7 @@ func (d *Device) Sync() {
// NOTE: Use GetHighestScanLine and GetLowestScanLine to obtain the highest // NOTE: Use GetHighestScanLine and GetLowestScanLine to obtain the highest
// and lowest useful values. Values are affected by front and back porch // and lowest useful values. Values are affected by front and back porch
// vsync settings (derived from VSyncLines configuration option). // vsync settings (derived from VSyncLines configuration option).
func (d *Device) SyncToScanLine(scanline uint16) { func (d *DeviceOf[T]) SyncToScanLine(scanline uint16) {
scan := d.GetScanLine() scan := d.GetScanLine()
// Sometimes GetScanLine returns erroneous 0 on first call after draw, so double check // Sometimes GetScanLine returns erroneous 0 on first call after draw, so double check
@@ -262,7 +297,7 @@ func (d *Device) SyncToScanLine(scanline uint16) {
} }
// GetScanLine reads the current scanline value from the display // GetScanLine reads the current scanline value from the display
func (d *Device) GetScanLine() uint16 { func (d *DeviceOf[T]) GetScanLine() uint16 {
d.startWrite() d.startWrite()
data := []uint8{0x00, 0x00} data := []uint8{0x00, 0x00}
d.dcPin.Low() d.dcPin.Low()
@@ -277,24 +312,24 @@ func (d *Device) GetScanLine() uint16 {
} }
// GetHighestScanLine calculates the last scanline id in the frame before VSYNC pause // GetHighestScanLine calculates the last scanline id in the frame before VSYNC pause
func (d *Device) GetHighestScanLine() uint16 { func (d *DeviceOf[T]) GetHighestScanLine() uint16 {
// Last scanline id appears to be backporch/2 + 320/2 // Last scanline id appears to be backporch/2 + 320/2
return uint16(math.Ceil(float64(d.vSyncLines)/2)/2) + 160 return uint16(math.Ceil(float64(d.vSyncLines)/2)/2) + 160
} }
// GetLowestScanLine calculate the first scanline id to appear after VSYNC pause // GetLowestScanLine calculate the first scanline id to appear after VSYNC pause
func (d *Device) GetLowestScanLine() uint16 { func (d *DeviceOf[T]) GetLowestScanLine() uint16 {
// First scanline id appears to be backporch/2 + 1 // First scanline id appears to be backporch/2 + 1
return uint16(math.Ceil(float64(d.vSyncLines)/2)/2) + 1 return uint16(math.Ceil(float64(d.vSyncLines)/2)/2) + 1
} }
// Display does nothing, there's no buffer as it might be too big for some boards // 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 return nil
} }
// SetPixel sets a pixel in the screen // 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) {
if x < 0 || y < 0 || if x < 0 || y < 0 ||
(((d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180) && (x >= d.width || y >= d.height)) || (((d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180) && (x >= d.width || y >= d.height)) ||
((d.rotation == drivers.Rotation90 || d.rotation == drivers.Rotation270) && (x >= d.height || y >= d.width))) { ((d.rotation == drivers.Rotation90 || d.rotation == drivers.Rotation270) && (x >= d.height || y >= d.width))) {
@@ -304,7 +339,7 @@ func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
} }
// setWindow prepares the screen to be modified at a given rectangle // 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) {
x += d.columnOffset x += d.columnOffset
y += d.rowOffset y += d.rowOffset
copy(d.buf[:4], []uint8{uint8(x >> 8), uint8(x), uint8((x + w - 1) >> 8), uint8(x + w - 1)}) copy(d.buf[:4], []uint8{uint8(x >> 8), uint8(x), uint8((x + w - 1) >> 8), uint8(x + w - 1)})
@@ -315,45 +350,39 @@ func (d *Device) setWindow(x, y, w, h int16) {
} }
// FillRectangle fills a rectangle at a given coordinates with a color // 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 {
d.startWrite() d.startWrite()
err := d.fillRectangle(x, y, width, height, c) err := d.fillRectangle(x, y, width, height, c)
d.endWrite() d.endWrite()
return err return err
} }
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() k, i := d.Size()
if x < 0 || y < 0 || width <= 0 || height <= 0 || if x < 0 || y < 0 || width <= 0 || height <= 0 ||
x >= k || (x+width) > k || y >= i || (y+height) > i { x >= k || (x+width) > k || y >= i || (y+height) > i {
return errors.New("rectangle coordinates outside display area") return errors.New("rectangle coordinates outside display area")
} }
d.setWindow(x, y, width, height) d.setWindow(x, y, width, height)
c565 := RGBATo565(c)
c1 := uint8(c565 >> 8)
c2 := uint8(c565)
data := make([]uint8, d.batchLength*2) image := d.getBuffer()
for i := int32(0); i < d.batchLength; i++ { image.FillSolidColor(pixel.NewColor[T](c.R, c.G, c.B))
data[i*2] = c1 j := int(width) * int(height)
data[i*2+1] = c2
}
j := int32(width) * int32(height)
for j > 0 { for j > 0 {
// The DC pin is already set to data in the setWindow call, so we can // The DC pin is already set to data in the setWindow call, so we can
// just write bytes on the SPI bus. // just write bytes on the SPI bus.
if j >= d.batchLength { if j >= image.Len() {
d.bus.Tx(data, nil) d.bus.Tx(image.RawBuffer(), nil)
} else { } else {
d.bus.Tx(data[:j*2], nil) d.bus.Tx(image.Rescale(j, 1).RawBuffer(), nil)
} }
j -= d.batchLength j -= image.Len()
} }
return nil return nil
} }
// DrawRGBBitmap8 copies an RGB bitmap to the internal buffer at given coordinates // 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() k, i := d.Size()
if x < 0 || y < 0 || w <= 0 || h <= 0 || if x < 0 || y < 0 || w <= 0 || h <= 0 ||
x >= k || (x+w) > k || y >= i || (y+h) > i { x >= k || (x+w) > k || y >= i || (y+h) > i {
@@ -367,7 +396,7 @@ func (d *Device) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error {
} }
// FillRectangleWithBuffer fills buffer with a rectangle at a given coordinates. // FillRectangleWithBuffer fills buffer with a rectangle at a given coordinates.
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 {
i, j := d.Size() i, j := d.Size()
if x < 0 || y < 0 || width <= 0 || height <= 0 || if x < 0 || y < 0 || width <= 0 || height <= 0 ||
x >= i || (x+width) > i || y >= j || (y+height) > j { x >= i || (x+width) > i || y >= j || (y+height) > j {
@@ -379,35 +408,32 @@ func (d *Device) FillRectangleWithBuffer(x, y, width, height int16, buffer []col
d.startWrite() d.startWrite()
d.setWindow(x, y, width, height) d.setWindow(x, y, width, height)
k := int32(width) * int32(height) k := int(width) * int(height)
data := make([]uint8, d.batchLength*2) image := d.getBuffer()
offset := int32(0) offset := 0
for k > 0 { for k > 0 {
for i := int32(0); i < d.batchLength; i++ { for i := 0; i < image.Len(); i++ {
if offset+i < int32(len(buffer)) { if offset+i < len(buffer) {
c565 := RGBATo565(buffer[offset+i]) c := buffer[offset+i]
c1 := uint8(c565 >> 8) image.Set(i, 0, pixel.NewColor[T](c.R, c.G, c.B))
c2 := uint8(c565)
data[i*2] = c1
data[i*2+1] = c2
} }
} }
// The DC pin is already set to data in the setWindow call, so we don't // The DC pin is already set to data in the setWindow call, so we don't
// have to set it here. // have to set it here.
if k >= d.batchLength { if k >= image.Len() {
d.bus.Tx(data, nil) d.bus.Tx(image.RawBuffer(), nil)
} else { } else {
d.bus.Tx(data[:k*2], nil) d.bus.Tx(image.Rescale(k, 1).RawBuffer(), nil)
} }
k -= d.batchLength k -= image.Len()
offset += d.batchLength offset += image.Len()
} }
d.endWrite() d.endWrite()
return nil return nil
} }
// DrawFastVLine draws a vertical line faster than using SetPixel // 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 { if y0 > y1 {
y0, y1 = y1, y0 y0, y1 = y1, y0
} }
@@ -415,7 +441,7 @@ func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) {
} }
// DrawFastHLine draws a horizontal line faster than using SetPixel // 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 { if x0 > x1 {
x0, x1 = x1, x0 x0, x1 = x1, x0
} }
@@ -423,13 +449,13 @@ func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) {
} }
// FillScreen fills the screen with a given color // FillScreen fills the screen with a given color
func (d *Device) FillScreen(c color.RGBA) { func (d *DeviceOf[T]) FillScreen(c color.RGBA) {
d.startWrite() d.startWrite()
d.fillScreen(c) d.fillScreen(c)
d.endWrite() d.endWrite()
} }
func (d *Device) fillScreen(c color.RGBA) { func (d *DeviceOf[T]) fillScreen(c color.RGBA) {
if d.rotation == NO_ROTATION || d.rotation == ROTATION_180 { if d.rotation == NO_ROTATION || d.rotation == ROTATION_180 {
d.fillRectangle(0, 0, d.width, d.height, c) d.fillRectangle(0, 0, d.width, d.height, c)
} else { } else {
@@ -441,13 +467,13 @@ func (d *Device) fillScreen(c color.RGBA) {
// The default is RGB565, setting it to any other value will break functions // The default is RGB565, setting it to any other value will break functions
// like SetPixel, FillRectangle, etc. Instead, you can write color data in the // like SetPixel, FillRectangle, etc. Instead, you can write color data in the
// specified color format using DrawRGBBitmap8. // specified color format using DrawRGBBitmap8.
func (d *Device) SetColorFormat(format ColorFormat) { func (d *DeviceOf[T]) SetColorFormat(format ColorFormat) {
d.startWrite() d.startWrite()
d.setColorFormat(format) d.setColorFormat(format)
d.endWrite() d.endWrite()
} }
func (d *Device) setColorFormat(format ColorFormat) { func (d *DeviceOf[T]) setColorFormat(format ColorFormat) {
// Lower 4 bits set the color format used in SPI. // Lower 4 bits set the color format used in SPI.
// Upper 4 bits set the color format used in the direct RGB interface. // Upper 4 bits set the color format used in the direct RGB interface.
// The RGB interface is not currently supported, so it is left at a // The RGB interface is not currently supported, so it is left at a
@@ -457,12 +483,12 @@ func (d *Device) setColorFormat(format ColorFormat) {
} }
// Rotation returns the current rotation of the device. // Rotation returns the current rotation of the device.
func (d *Device) Rotation() drivers.Rotation { func (d *DeviceOf[T]) Rotation() drivers.Rotation {
return d.rotation return d.rotation
} }
// SetRotation changes the rotation of the device (clock-wise) // SetRotation changes the rotation of the device (clock-wise)
func (d *Device) SetRotation(rotation Rotation) error { func (d *DeviceOf[T]) SetRotation(rotation Rotation) error {
d.rotation = rotation d.rotation = rotation
d.startWrite() d.startWrite()
err := d.setRotation(rotation) err := d.setRotation(rotation)
@@ -470,7 +496,7 @@ func (d *Device) SetRotation(rotation Rotation) error {
return err return err
} }
func (d *Device) setRotation(rotation Rotation) error { 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:
@@ -496,7 +522,7 @@ func (d *Device) setRotation(rotation Rotation) error {
} }
// 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 *DeviceOf[T]) Size() (w, h int16) {
if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 { if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 {
return d.width, d.height return d.width, d.height
} }
@@ -504,7 +530,7 @@ func (d *Device) Size() (w, h int16) {
} }
// EnableBacklight enables or disables the backlight // EnableBacklight enables or disables the backlight
func (d *Device) EnableBacklight(enable bool) { func (d *DeviceOf[T]) EnableBacklight(enable bool) {
if enable { if enable {
d.blPin.High() d.blPin.High()
} else { } else {
@@ -515,7 +541,7 @@ func (d *Device) EnableBacklight(enable bool) {
// Set the sleep mode for this LCD panel. When sleeping, the panel uses a lot // 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 // less power. The LCD won't display an image anymore, but the memory contents
// will be kept. // will be kept.
func (d *Device) Sleep(sleepEnabled bool) error { func (d *DeviceOf[T]) Sleep(sleepEnabled bool) error {
if sleepEnabled { if sleepEnabled {
d.startWrite() d.startWrite()
d.sendCommand(SLPIN, nil) d.sendCommand(SLPIN, nil)
@@ -537,7 +563,7 @@ func (d *Device) Sleep(sleepEnabled bool) error {
} }
// InvertColors inverts the colors of the screen // InvertColors inverts the colors of the screen
func (d *Device) InvertColors(invert bool) { func (d *DeviceOf[T]) InvertColors(invert bool) {
d.startWrite() d.startWrite()
if invert { if invert {
d.sendCommand(INVON, nil) d.sendCommand(INVON, nil)
@@ -548,12 +574,12 @@ func (d *Device) InvertColors(invert bool) {
} }
// IsBGR changes the color mode (RGB/BGR) // IsBGR changes the color mode (RGB/BGR)
func (d *Device) IsBGR(bgr bool) { func (d *DeviceOf[T]) IsBGR(bgr bool) {
d.isBGR = bgr d.isBGR = bgr
} }
// SetScrollArea sets an area to scroll with fixed top and bottom parts of the display. // SetScrollArea 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) {
if d.height < 320 { if d.height < 320 {
// The screen doesn't use the full 320 pixel height. // The screen doesn't use the full 320 pixel height.
// Enlarge the bottom fixed area to fill the 320 pixel height, so that // Enlarge the bottom fixed area to fill the 320 pixel height, so that
@@ -577,7 +603,7 @@ func (d *Device) SetScrollArea(topFixedArea, bottomFixedArea int16) {
} }
// SetScroll sets the vertical scroll address of the display. // SetScroll sets the vertical scroll address of the display.
func (d *Device) SetScroll(line int16) { func (d *DeviceOf[T]) SetScroll(line int16) {
if d.rotation == drivers.Rotation180 { if d.rotation == drivers.Rotation180 {
// The screen is rotated by 180°, so we have to invert the scroll line // The screen is rotated by 180°, so we have to invert the scroll line
// (taking care of the RowOffset). // (taking care of the RowOffset).
@@ -591,16 +617,8 @@ func (d *Device) SetScroll(line int16) {
} }
// StopScroll returns the display to its normal state. // StopScroll returns the display to its normal state.
func (d *Device) StopScroll() { func (d *DeviceOf[T]) StopScroll() {
d.startWrite() d.startWrite()
d.sendCommand(NORON, nil) d.sendCommand(NORON, nil)
d.endWrite() d.endWrite()
} }
// 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))
}