mirror of https://github.com/periph/devices
adding device table and gc9101 LCD display
parent
5eaaac75e1
commit
421e8d88e7
@ -0,0 +1,18 @@
|
|||||||
|
// Copyright 2025 The Periph Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed under the Apache License, Version 2.0
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Package gc9a01 controls a GC9A01 240x240 round RGB LCD display over SPI.
|
||||||
|
//
|
||||||
|
// The GC9A01 is a single-chip driver for 240x240 resolution TFT LCD displays
|
||||||
|
// with 65K colors (RGB565). It communicates via 4-wire SPI.
|
||||||
|
//
|
||||||
|
// # Datasheet
|
||||||
|
//
|
||||||
|
// https://www.buydisplay.com/download/ic/GC9A01A.pdf
|
||||||
|
//
|
||||||
|
// # Wiring
|
||||||
|
//
|
||||||
|
// Connect SDA to SPI_MOSI, SCL to SPI_CLK, CS to SPI_CS, DC to a GPIO pin.
|
||||||
|
// Optionally connect RST to a GPIO pin for hardware reset.
|
||||||
|
package gc9a01
|
||||||
@ -0,0 +1,119 @@
|
|||||||
|
// Copyright 2025 The Periph Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed under the Apache License, Version 2.0
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gc9a01_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"periph.io/x/conn/v3/gpio/gpioreg"
|
||||||
|
"periph.io/x/conn/v3/spi/spireg"
|
||||||
|
"periph.io/x/devices/v3/gc9a01"
|
||||||
|
"periph.io/x/host/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example() {
|
||||||
|
// Make sure periph is initialized.
|
||||||
|
if _, err := host.Init(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use spireg SPI bus registry to find the first available SPI bus.
|
||||||
|
p, err := spireg.Open("")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer p.Close()
|
||||||
|
|
||||||
|
// Data/Command pin.
|
||||||
|
dc := gpioreg.ByName("GPIO25")
|
||||||
|
if dc == nil {
|
||||||
|
log.Fatal("failed to find DC pin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional reset pin.
|
||||||
|
rst := gpioreg.ByName("GPIO27")
|
||||||
|
|
||||||
|
dev, err := gc9a01.New(p, dc, rst, &gc9a01.DefaultOpts)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("device=%s\n", dev.String())
|
||||||
|
|
||||||
|
// Draw a colorful bullseye pattern.
|
||||||
|
img := image.NewNRGBA(dev.Bounds())
|
||||||
|
colors := []color.NRGBA{
|
||||||
|
{0xFF, 0x00, 0x00, 0xFF}, // Red
|
||||||
|
{0xFF, 0xA5, 0x00, 0xFF}, // Orange
|
||||||
|
{0xFF, 0xFF, 0x00, 0xFF}, // Yellow
|
||||||
|
{0x00, 0xFF, 0x00, 0xFF}, // Green
|
||||||
|
{0x00, 0x00, 0xFF, 0xFF}, // Blue
|
||||||
|
{0x4B, 0x00, 0x82, 0xFF}, // Indigo
|
||||||
|
{0xEE, 0x82, 0xEE, 0xFF}, // Violet
|
||||||
|
}
|
||||||
|
cx, cy := 120, 120
|
||||||
|
for y := 0; y < 240; y++ {
|
||||||
|
for x := 0; x < 240; x++ {
|
||||||
|
dx := float64(x - cx)
|
||||||
|
dy := float64(y - cy)
|
||||||
|
dist := math.Sqrt(dx*dx + dy*dy)
|
||||||
|
ring := int(dist / 18)
|
||||||
|
if ring < len(colors) {
|
||||||
|
img.SetNRGBA(x, y, colors[ring])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := dev.Draw(dev.Bounds(), img, image.Point{}); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
// Draw a gradient.
|
||||||
|
for y := 0; y < 240; y++ {
|
||||||
|
for x := 0; x < 240; x++ {
|
||||||
|
img.SetNRGBA(x, y, color.NRGBA{
|
||||||
|
R: uint8(x * 255 / 239),
|
||||||
|
G: uint8(y * 255 / 239),
|
||||||
|
B: 128,
|
||||||
|
A: 255,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := dev.Draw(dev.Bounds(), img, image.Point{}); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
// Clear to white using image.Uniform.
|
||||||
|
white := &image.Uniform{color.NRGBA{0xFF, 0xFF, 0xFF, 0xFF}}
|
||||||
|
if err := dev.Draw(dev.Bounds(), white, image.Point{}); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a filled red circle in the center using draw.Draw.
|
||||||
|
red := &image.Uniform{color.NRGBA{0xFF, 0x00, 0x00, 0xFF}}
|
||||||
|
circle := image.NewNRGBA(dev.Bounds())
|
||||||
|
draw.Draw(circle, circle.Bounds(), white, image.Point{}, draw.Src)
|
||||||
|
for y := 0; y < 240; y++ {
|
||||||
|
for x := 0; x < 240; x++ {
|
||||||
|
dx := float64(x - cx)
|
||||||
|
dy := float64(y - cy)
|
||||||
|
if dx*dx+dy*dy <= 80*80 {
|
||||||
|
circle.Set(x, y, red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := dev.Draw(dev.Bounds(), circle, image.Point{}); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
_ = dev.Halt()
|
||||||
|
}
|
||||||
@ -0,0 +1,465 @@
|
|||||||
|
// Copyright 2025 The Periph Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed under the Apache License, Version 2.0
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gc9a01
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/draw"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"periph.io/x/conn/v3"
|
||||||
|
"periph.io/x/conn/v3/display"
|
||||||
|
"periph.io/x/conn/v3/gpio"
|
||||||
|
"periph.io/x/conn/v3/physic"
|
||||||
|
"periph.io/x/conn/v3/spi"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GC9A01 command registers.
|
||||||
|
const (
|
||||||
|
_SWRESET = 0x01
|
||||||
|
_SLPOUT = 0x11
|
||||||
|
_INVOFF = 0x20
|
||||||
|
_INVON = 0x21
|
||||||
|
_DISPOFF = 0x28
|
||||||
|
_DISPON = 0x29
|
||||||
|
_CASET = 0x2A
|
||||||
|
_RASET = 0x2B
|
||||||
|
_RAMWR = 0x2C
|
||||||
|
_MADCTL = 0x36
|
||||||
|
_COLMOD = 0x3A
|
||||||
|
)
|
||||||
|
|
||||||
|
// MADCTL flags.
|
||||||
|
const (
|
||||||
|
_MADCTL_MY = 0x80
|
||||||
|
_MADCTL_MX = 0x40
|
||||||
|
_MADCTL_MV = 0x20
|
||||||
|
_MADCTL_BGR = 0x08
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
_width = 240
|
||||||
|
_height = 240
|
||||||
|
_bufSize = _width * _height * 2 // RGB565: 2 bytes per pixel
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rotation describes the display orientation.
|
||||||
|
type Rotation int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Rotation0 is the default orientation.
|
||||||
|
Rotation0 Rotation = iota
|
||||||
|
// Rotation90 rotates 90 degrees clockwise.
|
||||||
|
Rotation90
|
||||||
|
// Rotation180 rotates 180 degrees.
|
||||||
|
Rotation180
|
||||||
|
// Rotation270 rotates 270 degrees clockwise.
|
||||||
|
Rotation270
|
||||||
|
)
|
||||||
|
|
||||||
|
// madctlValues maps Rotation to MADCTL register values.
|
||||||
|
var madctlValues = [4]byte{
|
||||||
|
_MADCTL_MX | _MADCTL_BGR, // Rotation0: 0x48
|
||||||
|
_MADCTL_MV | _MADCTL_BGR, // Rotation90: 0x28
|
||||||
|
_MADCTL_MY | _MADCTL_BGR, // Rotation180: 0x88
|
||||||
|
_MADCTL_MX | _MADCTL_MY | _MADCTL_MV | _MADCTL_BGR, // Rotation270: 0xE8
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultOpts is the recommended default options.
|
||||||
|
var DefaultOpts = Opts{}
|
||||||
|
|
||||||
|
// Opts defines the options for the device.
|
||||||
|
type Opts struct {
|
||||||
|
// Rotation sets the display rotation. Default is Rotation0.
|
||||||
|
Rotation Rotation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dev is an open handle to a GC9A01 display controller.
|
||||||
|
type Dev struct {
|
||||||
|
c conn.Conn
|
||||||
|
dc gpio.PinOut
|
||||||
|
rect image.Rectangle
|
||||||
|
|
||||||
|
buffer []byte // last-sent RGB565 data
|
||||||
|
nextBuf []byte // pre-allocated RGB565 conversion target
|
||||||
|
next *image.NRGBA // lazily allocated intermediate draw target
|
||||||
|
dirty bool // forces full redraw on first Draw
|
||||||
|
halted bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a Dev object that communicates over SPI to a GC9A01 display
|
||||||
|
// controller.
|
||||||
|
//
|
||||||
|
// The dc pin is the Data/Command pin for 4-wire SPI mode. The rst pin is
|
||||||
|
// optional; pass nil if not connected.
|
||||||
|
func New(p spi.Port, dc gpio.PinOut, rst gpio.PinOut, opts *Opts) (*Dev, error) {
|
||||||
|
if dc == gpio.INVALID {
|
||||||
|
return nil, fmt.Errorf("gc9a01: invalid dc pin")
|
||||||
|
}
|
||||||
|
if err := dc.Out(gpio.Low); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c, err := p.Connect(16*physic.MegaHertz, spi.Mode0, 8)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
d := &Dev{
|
||||||
|
c: c,
|
||||||
|
dc: dc,
|
||||||
|
rect: image.Rect(0, 0, _width, _height),
|
||||||
|
buffer: make([]byte, _bufSize),
|
||||||
|
nextBuf: make([]byte, _bufSize),
|
||||||
|
dirty: true,
|
||||||
|
}
|
||||||
|
if rst != nil {
|
||||||
|
if err := rst.Out(gpio.Low); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
time.Sleep(10 * time.Millisecond)
|
||||||
|
if err := rst.Out(gpio.High); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
time.Sleep(120 * time.Millisecond)
|
||||||
|
}
|
||||||
|
if err := d.initDisplay(opts); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements display.Drawer.
|
||||||
|
func (d *Dev) String() string {
|
||||||
|
return fmt.Sprintf("GC9A01{%s, %s, %s}", d.c, d.dc, d.rect.Max)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ColorModel implements display.Drawer.
|
||||||
|
func (d *Dev) ColorModel() color.Model {
|
||||||
|
return color.NRGBAModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds implements display.Drawer. Min is guaranteed to be {0, 0}.
|
||||||
|
func (d *Dev) Bounds() image.Rectangle {
|
||||||
|
return d.rect
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw implements display.Drawer.
|
||||||
|
//
|
||||||
|
// It draws synchronously, once this function returns, the display is updated.
|
||||||
|
// Using *image.NRGBA as source with matching bounds is the fastest path.
|
||||||
|
func (d *Dev) Draw(dstRect image.Rectangle, src image.Image, sp image.Point) error {
|
||||||
|
var srcNRGBA *image.NRGBA
|
||||||
|
if img, ok := src.(*image.NRGBA); ok && dstRect == d.rect && img.Bounds() == d.rect && sp.X == 0 && sp.Y == 0 {
|
||||||
|
srcNRGBA = img
|
||||||
|
} else {
|
||||||
|
if d.next == nil {
|
||||||
|
d.next = image.NewNRGBA(d.rect)
|
||||||
|
}
|
||||||
|
draw.Src.Draw(d.next, dstRect, src, sp)
|
||||||
|
srcNRGBA = d.next
|
||||||
|
}
|
||||||
|
// Convert NRGBA to RGB565.
|
||||||
|
pix := srcNRGBA.Pix
|
||||||
|
for y := 0; y < _height; y++ {
|
||||||
|
for x := 0; x < _width; x++ {
|
||||||
|
srcOff := y*srcNRGBA.Stride + x*4
|
||||||
|
dstOff := (y*_width + x) * 2
|
||||||
|
d.nextBuf[dstOff], d.nextBuf[dstOff+1] = nrgbaToRGB565(pix[srcOff], pix[srcOff+1], pix[srcOff+2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return d.drawInternal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Halt turns off the display.
|
||||||
|
//
|
||||||
|
// Sending any other command afterward reenables the display.
|
||||||
|
func (d *Dev) Halt() error {
|
||||||
|
d.halted = false
|
||||||
|
err := d.sendCommand([]byte{_DISPOFF})
|
||||||
|
if err == nil {
|
||||||
|
d.halted = true
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invert the display colors.
|
||||||
|
func (d *Dev) Invert(on bool) error {
|
||||||
|
if on {
|
||||||
|
return d.sendCommand([]byte{_INVON})
|
||||||
|
}
|
||||||
|
return d.sendCommand([]byte{_INVOFF})
|
||||||
|
}
|
||||||
|
|
||||||
|
// nrgbaToRGB565 converts 8-bit R, G, B to RGB565 big-endian.
|
||||||
|
func nrgbaToRGB565(r, g, b byte) (byte, byte) {
|
||||||
|
r5 := r >> 3
|
||||||
|
g6 := g >> 2
|
||||||
|
b5 := b >> 3
|
||||||
|
hi := (r5 << 3) | (g6 >> 3)
|
||||||
|
lo := (g6 << 5) | b5
|
||||||
|
return hi, lo
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawInternal compares nextBuf against buffer and sends only changed pixels.
|
||||||
|
func (d *Dev) drawInternal() error {
|
||||||
|
startRow, endRow, startCol, endCol, skip := d.calculateDirtyRect()
|
||||||
|
if skip {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set address window.
|
||||||
|
if err := d.setWindow(startCol, startRow, endCol-1, endRow-1); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Send RAMWR command.
|
||||||
|
if err := d.sendCommand([]byte{_RAMWR}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the pixel data for the dirty rectangle.
|
||||||
|
w := endCol - startCol
|
||||||
|
data := make([]byte, 0, (endRow-startRow)*w*2)
|
||||||
|
for y := startRow; y < endRow; y++ {
|
||||||
|
rowStart := (y*_width + startCol) * 2
|
||||||
|
rowEnd := rowStart + w*2
|
||||||
|
data = append(data, d.nextBuf[rowStart:rowEnd]...)
|
||||||
|
}
|
||||||
|
if err := d.sendData(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update buffer with sent data.
|
||||||
|
for y := startRow; y < endRow; y++ {
|
||||||
|
rowStart := (y*_width + startCol) * 2
|
||||||
|
rowEnd := rowStart + w*2
|
||||||
|
copy(d.buffer[rowStart:rowEnd], d.nextBuf[rowStart:rowEnd])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateDirtyRect finds the minimal bounding rectangle of changed pixels.
|
||||||
|
func (d *Dev) calculateDirtyRect() (startRow, endRow, startCol, endCol int, skip bool) {
|
||||||
|
startRow = 0
|
||||||
|
endRow = _height
|
||||||
|
startCol = 0
|
||||||
|
endCol = _width
|
||||||
|
|
||||||
|
if d.dirty {
|
||||||
|
d.dirty = false
|
||||||
|
return startRow, endRow, startCol, endCol, false
|
||||||
|
}
|
||||||
|
|
||||||
|
rowBytes := _width * 2
|
||||||
|
|
||||||
|
// Scan from top.
|
||||||
|
for ; startRow < endRow; startRow++ {
|
||||||
|
off := startRow * rowBytes
|
||||||
|
if !bytes.Equal(d.buffer[off:off+rowBytes], d.nextBuf[off:off+rowBytes]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Scan from bottom.
|
||||||
|
for ; endRow > startRow; endRow-- {
|
||||||
|
off := (endRow - 1) * rowBytes
|
||||||
|
if !bytes.Equal(d.buffer[off:off+rowBytes], d.nextBuf[off:off+rowBytes]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if startRow == endRow {
|
||||||
|
return 0, 0, 0, 0, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan from left (2 bytes per pixel).
|
||||||
|
for ; startCol < endCol; startCol++ {
|
||||||
|
changed := false
|
||||||
|
for y := startRow; y < endRow; y++ {
|
||||||
|
off := (y*_width + startCol) * 2
|
||||||
|
if d.buffer[off] != d.nextBuf[off] || d.buffer[off+1] != d.nextBuf[off+1] {
|
||||||
|
changed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan from right.
|
||||||
|
for ; endCol > startCol; endCol-- {
|
||||||
|
changed := false
|
||||||
|
for y := startRow; y < endRow; y++ {
|
||||||
|
off := (y*_width + endCol - 1) * 2
|
||||||
|
if d.buffer[off] != d.nextBuf[off] || d.buffer[off+1] != d.nextBuf[off+1] {
|
||||||
|
changed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return startRow, endRow, startCol, endCol, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// setWindow sets the column and row address window for subsequent RAMWR.
|
||||||
|
func (d *Dev) setWindow(x0, y0, x1, y1 int) error {
|
||||||
|
if err := d.sendCommand([]byte{_CASET}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.sendData([]byte{byte(x0 >> 8), byte(x0), byte(x1 >> 8), byte(x1)}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.sendCommand([]byte{_RASET}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return d.sendData([]byte{byte(y0 >> 8), byte(y0), byte(y1 >> 8), byte(y1)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dev) sendCommand(c []byte) error {
|
||||||
|
if d.halted {
|
||||||
|
c = append([]byte{_DISPON}, c...)
|
||||||
|
d.halted = false
|
||||||
|
}
|
||||||
|
if err := d.dc.Out(gpio.Low); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return d.c.Tx(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Dev) sendData(data []byte) error {
|
||||||
|
if d.halted {
|
||||||
|
if err := d.sendCommand(nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := d.dc.Out(gpio.High); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Chunk large data to avoid exceeding SPI driver buffer limits.
|
||||||
|
const maxChunk = 4096
|
||||||
|
for len(data) > 0 {
|
||||||
|
chunk := data
|
||||||
|
if len(chunk) > maxChunk {
|
||||||
|
chunk = data[:maxChunk]
|
||||||
|
}
|
||||||
|
if err := d.c.Tx(chunk, nil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data = data[len(chunk):]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// initDisplay sends the initialization command sequence.
|
||||||
|
// The sequence is derived from the Adafruit GC9A01A Arduino driver.
|
||||||
|
func (d *Dev) initDisplay(opts *Opts) error {
|
||||||
|
rotation := Rotation0
|
||||||
|
if opts != nil {
|
||||||
|
rotation = opts.Rotation
|
||||||
|
}
|
||||||
|
if rotation < Rotation0 || rotation > Rotation270 {
|
||||||
|
rotation = Rotation0
|
||||||
|
}
|
||||||
|
|
||||||
|
type cmd struct {
|
||||||
|
c byte
|
||||||
|
data []byte
|
||||||
|
delay time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
cmds := []cmd{
|
||||||
|
// Software reset.
|
||||||
|
{_SWRESET, nil, 150 * time.Millisecond},
|
||||||
|
|
||||||
|
// Undocumented vendor init registers (from Adafruit reference).
|
||||||
|
{0xEF, nil, 0},
|
||||||
|
{0xEB, []byte{0x14}, 0},
|
||||||
|
{0xFE, nil, 0},
|
||||||
|
{0xEF, nil, 0},
|
||||||
|
{0xEB, []byte{0x14}, 0},
|
||||||
|
{0x84, []byte{0x40}, 0},
|
||||||
|
{0x85, []byte{0xFF}, 0},
|
||||||
|
{0x86, []byte{0xFF}, 0},
|
||||||
|
{0x87, []byte{0xFF}, 0},
|
||||||
|
{0x88, []byte{0x0A}, 0},
|
||||||
|
{0x89, []byte{0x21}, 0},
|
||||||
|
{0x8A, []byte{0x00}, 0},
|
||||||
|
{0x8B, []byte{0x80}, 0},
|
||||||
|
{0x8C, []byte{0x01}, 0},
|
||||||
|
{0x8D, []byte{0x01}, 0},
|
||||||
|
{0x8E, []byte{0xFF}, 0},
|
||||||
|
{0x8F, []byte{0xFF}, 0},
|
||||||
|
{0xB6, []byte{0x00, 0x00}, 0},
|
||||||
|
|
||||||
|
// MADCTL: memory access control (rotation + BGR).
|
||||||
|
{_MADCTL, []byte{madctlValues[rotation]}, 0},
|
||||||
|
|
||||||
|
// COLMOD: 16-bit color (RGB565).
|
||||||
|
{_COLMOD, []byte{0x05}, 0},
|
||||||
|
|
||||||
|
// More undocumented vendor registers.
|
||||||
|
{0x90, []byte{0x08, 0x08, 0x08, 0x08}, 0},
|
||||||
|
{0xBD, []byte{0x06}, 0},
|
||||||
|
{0xBC, []byte{0x00}, 0},
|
||||||
|
{0xFF, []byte{0x60, 0x01, 0x04}, 0},
|
||||||
|
{0xC3, []byte{0x13}, 0}, // Power control 2.
|
||||||
|
{0xC4, []byte{0x13}, 0}, // Power control 3.
|
||||||
|
{0xC9, []byte{0x22}, 0}, // Power control 4.
|
||||||
|
{0xBE, []byte{0x11}, 0},
|
||||||
|
{0xE1, []byte{0x10, 0x0E}, 0},
|
||||||
|
{0xDF, []byte{0x21, 0x0C, 0x02}, 0},
|
||||||
|
|
||||||
|
// Gamma correction.
|
||||||
|
{0xF0, []byte{0x45, 0x09, 0x08, 0x08, 0x26, 0x2A}, 0},
|
||||||
|
{0xF1, []byte{0x43, 0x70, 0x72, 0x36, 0x37, 0x6F}, 0},
|
||||||
|
{0xF2, []byte{0x45, 0x09, 0x08, 0x08, 0x26, 0x2A}, 0},
|
||||||
|
{0xF3, []byte{0x43, 0x70, 0x72, 0x36, 0x37, 0x6F}, 0},
|
||||||
|
|
||||||
|
{0xED, []byte{0x1B, 0x0B}, 0},
|
||||||
|
{0xAE, []byte{0x77}, 0},
|
||||||
|
{0xCD, []byte{0x63}, 0},
|
||||||
|
{0x70, []byte{0x07, 0x07, 0x04, 0x0E, 0x0F, 0x09, 0x07, 0x08, 0x03}, 0},
|
||||||
|
|
||||||
|
// Frame rate control.
|
||||||
|
{0xE8, []byte{0x34}, 0},
|
||||||
|
|
||||||
|
{0x62, []byte{0x18, 0x0D, 0x71, 0xED, 0x70, 0x70, 0x18, 0x0F, 0x71, 0xEF, 0x70, 0x70}, 0},
|
||||||
|
{0x63, []byte{0x18, 0x11, 0x71, 0xF1, 0x70, 0x70, 0x18, 0x13, 0x71, 0xF3, 0x70, 0x70}, 0},
|
||||||
|
{0x64, []byte{0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07}, 0},
|
||||||
|
{0x66, []byte{0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00}, 0},
|
||||||
|
{0x67, []byte{0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98}, 0},
|
||||||
|
{0x74, []byte{0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00}, 0},
|
||||||
|
{0x98, []byte{0x3E, 0x07}, 0},
|
||||||
|
|
||||||
|
{0x35, nil, 0}, // Tearing effect line ON.
|
||||||
|
// Display inversion ON — required for correct colors on most modules.
|
||||||
|
{_INVON, nil, 0},
|
||||||
|
|
||||||
|
// Exit sleep mode.
|
||||||
|
{_SLPOUT, nil, 150 * time.Millisecond},
|
||||||
|
// Display ON.
|
||||||
|
{_DISPON, nil, 20 * time.Millisecond},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cmds {
|
||||||
|
if err := d.sendCommand([]byte{c.c}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(c.data) > 0 {
|
||||||
|
if err := d.sendData(c.data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.delay > 0 {
|
||||||
|
time.Sleep(c.delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ display.Drawer = &Dev{}
|
||||||
@ -0,0 +1,318 @@
|
|||||||
|
// Copyright 2025 The Periph Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed under the Apache License, Version 2.0
|
||||||
|
// that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package gc9a01
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"periph.io/x/conn/v3/conntest"
|
||||||
|
"periph.io/x/conn/v3/gpio"
|
||||||
|
"periph.io/x/conn/v3/gpio/gpiotest"
|
||||||
|
"periph.io/x/conn/v3/physic"
|
||||||
|
"periph.io/x/conn/v3/spi"
|
||||||
|
"periph.io/x/conn/v3/spi/spitest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
port := getPlayback(t)
|
||||||
|
dev, err := New(port, &gpiotest.Pin{N: "dc", Num: 1}, nil, &DefaultOpts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if dev == nil {
|
||||||
|
t.Fatal("expected device")
|
||||||
|
}
|
||||||
|
if err := port.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew_fail_invalid_dc(t *testing.T) {
|
||||||
|
if d, err := New(&spitest.Playback{}, gpio.INVALID, nil, &DefaultOpts); d != nil || err == nil {
|
||||||
|
t.Fatal("expected failure with gpio.INVALID dc pin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew_fail_dc_err(t *testing.T) {
|
||||||
|
if d, err := New(&spitest.Playback{}, &failPin{fail: true}, nil, &DefaultOpts); d != nil || err == nil {
|
||||||
|
t.Fatal("expected failure when dc pin fails")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew_fail_connect(t *testing.T) {
|
||||||
|
if d, err := New(&configFail{}, &gpiotest.Pin{N: "dc", Num: 1}, nil, &DefaultOpts); d != nil || err == nil {
|
||||||
|
t.Fatal("expected failure when SPI connect fails")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColorModel(t *testing.T) {
|
||||||
|
port := getPlayback(t)
|
||||||
|
dev, err := New(port, &gpiotest.Pin{N: "dc", Num: 1}, nil, &DefaultOpts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if c := dev.ColorModel(); c != color.NRGBAModel {
|
||||||
|
t.Fatalf("expected NRGBAModel, got %v", c)
|
||||||
|
}
|
||||||
|
if err := port.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBounds(t *testing.T) {
|
||||||
|
port := getPlayback(t)
|
||||||
|
dev, err := New(port, &gpiotest.Pin{N: "dc", Num: 1}, nil, &DefaultOpts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
expected := image.Rect(0, 0, 240, 240)
|
||||||
|
if b := dev.Bounds(); b != expected {
|
||||||
|
t.Fatalf("expected %v, got %v", expected, b)
|
||||||
|
}
|
||||||
|
if err := port.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestString(t *testing.T) {
|
||||||
|
port := getPlayback(t)
|
||||||
|
dev, err := New(port, &gpiotest.Pin{N: "dc", Num: 1}, nil, &DefaultOpts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
expected := "GC9A01{playback, dc(1), (240,240)}"
|
||||||
|
if s := dev.String(); s != expected {
|
||||||
|
t.Fatalf("%q != %q", expected, s)
|
||||||
|
}
|
||||||
|
if err := port.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHalt(t *testing.T) {
|
||||||
|
port := &spitest.Playback{
|
||||||
|
Playback: conntest.Playback{
|
||||||
|
Ops: append(initOps(),
|
||||||
|
// Halt: DC low, then DISPOFF
|
||||||
|
conntest.IO{W: []byte{_DISPOFF}},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dev, err := New(port, &gpiotest.Pin{N: "dc", Num: 1}, nil, &DefaultOpts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := dev.Halt(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !dev.halted {
|
||||||
|
t.Fatal("expected halted to be true")
|
||||||
|
}
|
||||||
|
if err := port.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvert(t *testing.T) {
|
||||||
|
port := &spitest.Playback{
|
||||||
|
Playback: conntest.Playback{
|
||||||
|
Ops: append(initOps(),
|
||||||
|
conntest.IO{W: []byte{_INVOFF}},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
dev, err := New(port, &gpiotest.Pin{N: "dc", Num: 1}, nil, &DefaultOpts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// The display inversion is ON by default (from init), so Invert(false)
|
||||||
|
// should send INVOFF.
|
||||||
|
if err := dev.Invert(false); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := port.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDraw_noop(t *testing.T) {
|
||||||
|
port := getPlayback(t)
|
||||||
|
dev, err := New(port, &gpiotest.Pin{N: "dc", Num: 1}, nil, &DefaultOpts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// First draw: full frame of black (matches zero buffer). But dirty=true
|
||||||
|
// forces full send.
|
||||||
|
// Instead let's just clear dirty and test that a second identical draw
|
||||||
|
// sends nothing.
|
||||||
|
dev.dirty = false
|
||||||
|
// Draw all black (matches the zero-initialized buffer).
|
||||||
|
black := image.NewNRGBA(dev.Bounds())
|
||||||
|
if err := dev.Draw(dev.Bounds(), black, image.Point{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := port.Close(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNrgbaToRGB565(t *testing.T) {
|
||||||
|
// Pure red: R=0xFF -> 0b11111 = 31, G=0, B=0
|
||||||
|
// hi = (31 << 3) | 0 = 0xF8, lo = 0x00
|
||||||
|
hi, lo := nrgbaToRGB565(0xFF, 0x00, 0x00)
|
||||||
|
if hi != 0xF8 || lo != 0x00 {
|
||||||
|
t.Fatalf("red: got 0x%02X 0x%02X, expected 0xF8 0x00", hi, lo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pure green: R=0, G=0xFF -> 0b111111 = 63, B=0
|
||||||
|
// hi = (0 << 3) | (63 >> 3) = 0x07, lo = (63 << 5) | 0 = 0xE0
|
||||||
|
hi, lo = nrgbaToRGB565(0x00, 0xFF, 0x00)
|
||||||
|
if hi != 0x07 || lo != 0xE0 {
|
||||||
|
t.Fatalf("green: got 0x%02X 0x%02X, expected 0x07 0xE0", hi, lo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pure blue: R=0, G=0, B=0xFF -> 0b11111 = 31
|
||||||
|
// hi = 0, lo = 0 | 31 = 0x1F
|
||||||
|
hi, lo = nrgbaToRGB565(0x00, 0x00, 0xFF)
|
||||||
|
if hi != 0x00 || lo != 0x1F {
|
||||||
|
t.Fatalf("blue: got 0x%02X 0x%02X, expected 0x00 0x1F", hi, lo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// White: all 0xFF
|
||||||
|
// hi = (31 << 3) | (63 >> 3) = 0xFF, lo = (63 << 5) | 31 = 0xFF
|
||||||
|
hi, lo = nrgbaToRGB565(0xFF, 0xFF, 0xFF)
|
||||||
|
if hi != 0xFF || lo != 0xFF {
|
||||||
|
t.Fatalf("white: got 0x%02X 0x%02X, expected 0xFF 0xFF", hi, lo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Black: all 0
|
||||||
|
hi, lo = nrgbaToRGB565(0x00, 0x00, 0x00)
|
||||||
|
if hi != 0x00 || lo != 0x00 {
|
||||||
|
t.Fatalf("black: got 0x%02X 0x%02X, expected 0x00 0x00", hi, lo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDraw_gpio_fail(t *testing.T) {
|
||||||
|
port := getPlayback(t)
|
||||||
|
pin := &failPin{fail: false}
|
||||||
|
dev, err := New(port, pin, nil, &DefaultOpts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// GPIO suddenly fails.
|
||||||
|
pin.fail = true
|
||||||
|
img := image.NewNRGBA(dev.Bounds())
|
||||||
|
if err := dev.Draw(dev.Bounds(), img, image.Point{}); err == nil || err.Error() != "injected error" {
|
||||||
|
t.Fatalf("expected injected error, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initOps returns the conntest.IO operations expected during initialization.
|
||||||
|
// Each command and its data are sent as separate SPI transactions.
|
||||||
|
func initOps() []conntest.IO {
|
||||||
|
rotation := Rotation0
|
||||||
|
madctl := madctlValues[rotation]
|
||||||
|
|
||||||
|
ops := []conntest.IO{}
|
||||||
|
type initCmd struct {
|
||||||
|
c byte
|
||||||
|
data []byte
|
||||||
|
}
|
||||||
|
cmds := []initCmd{
|
||||||
|
{_SWRESET, nil},
|
||||||
|
{0xEF, nil},
|
||||||
|
{0xEB, []byte{0x14}},
|
||||||
|
{0xFE, nil},
|
||||||
|
{0xEF, nil},
|
||||||
|
{0xEB, []byte{0x14}},
|
||||||
|
{0x84, []byte{0x40}},
|
||||||
|
{0x85, []byte{0xFF}},
|
||||||
|
{0x86, []byte{0xFF}},
|
||||||
|
{0x87, []byte{0xFF}},
|
||||||
|
{0x88, []byte{0x0A}},
|
||||||
|
{0x89, []byte{0x21}},
|
||||||
|
{0x8A, []byte{0x00}},
|
||||||
|
{0x8B, []byte{0x80}},
|
||||||
|
{0x8C, []byte{0x01}},
|
||||||
|
{0x8D, []byte{0x01}},
|
||||||
|
{0x8E, []byte{0xFF}},
|
||||||
|
{0x8F, []byte{0xFF}},
|
||||||
|
{0xB6, []byte{0x00, 0x00}},
|
||||||
|
{_MADCTL, []byte{madctl}},
|
||||||
|
{_COLMOD, []byte{0x05}},
|
||||||
|
{0x90, []byte{0x08, 0x08, 0x08, 0x08}},
|
||||||
|
{0xBD, []byte{0x06}},
|
||||||
|
{0xBC, []byte{0x00}},
|
||||||
|
{0xFF, []byte{0x60, 0x01, 0x04}},
|
||||||
|
{0xC3, []byte{0x13}},
|
||||||
|
{0xC4, []byte{0x13}},
|
||||||
|
{0xC9, []byte{0x22}},
|
||||||
|
{0xBE, []byte{0x11}},
|
||||||
|
{0xE1, []byte{0x10, 0x0E}},
|
||||||
|
{0xDF, []byte{0x21, 0x0C, 0x02}},
|
||||||
|
{0xF0, []byte{0x45, 0x09, 0x08, 0x08, 0x26, 0x2A}},
|
||||||
|
{0xF1, []byte{0x43, 0x70, 0x72, 0x36, 0x37, 0x6F}},
|
||||||
|
{0xF2, []byte{0x45, 0x09, 0x08, 0x08, 0x26, 0x2A}},
|
||||||
|
{0xF3, []byte{0x43, 0x70, 0x72, 0x36, 0x37, 0x6F}},
|
||||||
|
{0xED, []byte{0x1B, 0x0B}},
|
||||||
|
{0xAE, []byte{0x77}},
|
||||||
|
{0xCD, []byte{0x63}},
|
||||||
|
{0x70, []byte{0x07, 0x07, 0x04, 0x0E, 0x0F, 0x09, 0x07, 0x08, 0x03}},
|
||||||
|
{0xE8, []byte{0x34}},
|
||||||
|
{0x62, []byte{0x18, 0x0D, 0x71, 0xED, 0x70, 0x70, 0x18, 0x0F, 0x71, 0xEF, 0x70, 0x70}},
|
||||||
|
{0x63, []byte{0x18, 0x11, 0x71, 0xF1, 0x70, 0x70, 0x18, 0x13, 0x71, 0xF3, 0x70, 0x70}},
|
||||||
|
{0x64, []byte{0x28, 0x29, 0xF1, 0x01, 0xF1, 0x00, 0x07}},
|
||||||
|
{0x66, []byte{0x3C, 0x00, 0xCD, 0x67, 0x45, 0x45, 0x10, 0x00, 0x00, 0x00}},
|
||||||
|
{0x67, []byte{0x00, 0x3C, 0x00, 0x00, 0x00, 0x01, 0x54, 0x10, 0x32, 0x98}},
|
||||||
|
{0x74, []byte{0x10, 0x85, 0x80, 0x00, 0x00, 0x4E, 0x00}},
|
||||||
|
{0x98, []byte{0x3E, 0x07}},
|
||||||
|
{0x35, nil},
|
||||||
|
{_INVON, nil},
|
||||||
|
{_SLPOUT, nil},
|
||||||
|
{_DISPON, nil},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cmds {
|
||||||
|
// Command byte (DC low).
|
||||||
|
ops = append(ops, conntest.IO{W: []byte{c.c}})
|
||||||
|
// Data bytes (DC high), if any.
|
||||||
|
if len(c.data) > 0 {
|
||||||
|
ops = append(ops, conntest.IO{W: c.data})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ops
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPlayback(t *testing.T) *spitest.Playback {
|
||||||
|
t.Helper()
|
||||||
|
return &spitest.Playback{
|
||||||
|
Playback: conntest.Playback{
|
||||||
|
Ops: initOps(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type configFail struct {
|
||||||
|
spitest.Record
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *configFail) Connect(f physic.Frequency, mode spi.Mode, bits int) (spi.Conn, error) {
|
||||||
|
return nil, errors.New("injected error")
|
||||||
|
}
|
||||||
|
|
||||||
|
type failPin struct {
|
||||||
|
gpiotest.Pin
|
||||||
|
fail bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *failPin) Out(l gpio.Level) error {
|
||||||
|
if f.fail {
|
||||||
|
return errors.New("injected error")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue