From 5eaaac75e1314501d2f14a5c7dddf4bf84753a63 Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Sat, 2 May 2026 13:03:22 -0400 Subject: [PATCH 1/2] Adding table to doc --- README.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/README.md b/README.md index bb2935d..cade0ea 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,72 @@ Curious? Look at [supported devices](https://periph.io/device/) for more examples! +## Supported Devices + +| Package | Description | +|---------|-------------| +| [ads1x15](ads1x15) | ADS1015/ADS1115 analog-to-digital converters via I²C | +| [adxl345](adxl345) | ADXL345 3-axis accelerometer over SPI | +| [aht20](aht20) | AHT20 temperature/humidity sensor over I²C | +| [aip31068](aip31068) | AIP31068 HD44780-compatible I²C LCD driver | +| [am2320](am2320) | AOSONG AM2320 temperature/humidity sensor | +| [apa102](apa102) | APA102 LED strip over SPI | +| [as7262](as7262) | AMS AS7262 6-channel visible spectral sensor via I²C | +| [bh1750](bh1750) | ROHM BH1750 ambient light sensor over I²C | +| [bitbang](bitbang) | Software bit-banging protocols (I²C, SPI, UART) via GPIO pins | +| [bmxx80](bmxx80) | Bosch BMP180/BME280/BMP280 temperature/pressure/humidity sensor over I²C or SPI | +| [cap1xxx](cap1xxx) | Microchip CAP1xxx capacitive touch sensors over I²C | +| [ccs811](ccs811) | CCS811 volatile organic compound (VOC) sensor via I²C | +| [common](common) | Shared utility functions used across multiple packages | +| [dht22](dht22) | DHT22/AM2302 temperature/humidity sensor | +| [ds18b20](ds18b20) | DS18B20/DS18S20/MAX31820 1-wire temperature sensors | +| [ds248x](ds248x) | Maxim DS2483/DS2482-100 1-wire interface chip over I²C | +| [ep0099](ep0099) | EP-0099 Raspberry Pi HAT with 4 relays via I²C | +| [epd](epd) | Waveshare e-paper display series | +| [hd44780](hd44780) | Hitachi HD44780 LCD display chipset | +| [hdc302x](hdc302x) | Texas Instruments HDC3021/3022 temperature/humidity sensor over I²C | +| [ht16k33](ht16k33) | Holtek HT16K33 16×8 LED driver | +| [hx711](hx711) | HX711 24-bit analog-to-digital converter (load cells) | +| [ina219](ina219) | Texas Instruments INA219 current/voltage/power monitor over I²C | +| [inky](inky) | Pimoroni Inky pHAT/wHAT e-ink displays | +| [lepton](lepton) | FLIR Lepton infrared (IR) camera | +| [lirc](lirc) | Infrared receiver support via Linux LIRC | +| [matrixorbital](matrixorbital) | MatrixOrbital character LCD displays | +| [max7219](max7219) | MAX7219 7-segment and LED matrix displays | +| [mcp23xxx](mcp23xxx) | Microchip MCP23xxx GPIO expanders | +| [mcp472x](mcp472x) | Microchip MCP472x digital-to-analog converters | +| [mcp9808](mcp9808) | Microchip MCP9808 temperature sensor | +| [mfrc522](mfrc522) | MFRC522 Mifare RFID card reader | +| [mpu9250](mpu9250) | MPU-9250 9-axis IMU (gyroscope, accelerometer, magnetometer) | +| [nrzled](nrzled) | WS2811/WS2812/WS2812B and compatible NRZ-encoded LEDs (SK6812, UCS1903) | +| [nxp74hc595](nxp74hc595) | 74HC595 serial-in parallel-out shift register | +| [pca9548](pca9548) | PCA9548 8-port I²C multiplexer | +| [pca9633](pca9633) | PCA9633 4-channel LED PWM controller | +| [pca9685](pca9685) | PCA9685 16-channel PWM controller (servos, LEDs) | +| [pcf857x](pcf857x) | TI/NXP PCF857x I²C I/O expander | +| [piblaster](piblaster) | PWM via the pi-blaster daemon on Raspberry Pi | +| [rainbowhat](rainbowhat) | Pimoroni Rainbow HAT | +| [scd4x](scd4x) | Sensirion SCD4x CO₂ sensors | +| [screen1d](screen1d) | 1D display output to terminal via ANSI color codes | +| [serlcd](serlcd) | SparkFun SerLCD intelligent LCD display | +| [sgp30](sgp30) | Sensirion SGP30 multi-gas sensor (TVOC and CO₂eq) | +| [sht4x](sht4x) | Sensirion SHT-40/SHT-41/SHT-45 temperature/humidity sensors | +| [sn3218](sn3218) | SN3218 18-channel LED driver over I²C | +| [ssd1306](ssd1306) | SSD1306/SH1106/SH1107 monochrome OLED displays | +| [st7567](st7567) | ST7567 single-chip dot matrix LCD | +| [tca95xx](tca95xx) | Texas Instruments TCA95xx 8-bit I²C GPIO expanders | +| [tic](tic) | Tic stepper motor controllers via I²C | +| [tlv493d](tlv493d) | Infineon TLV493D 3D magnetic (Hall effect) sensor | +| [tm1637](tm1637) | TM1637 LED display driver over GPIO | +| [tmp102](tmp102) | Texas Instruments TMP102 temperature sensor over I²C | +| [unicornhd](unicornhd) | Pimoroni Unicorn HD HAT (16×16 RGB LED matrix) | +| [videosink](videosink) | Display driver serving frames over HTTP | +| [waveshare1602](waveshare1602) | Waveshare 1602 LCD display | +| [waveshare2in13v2](waveshare2in13v2) | Waveshare 2.13" v2 e-paper display | +| [waveshare2in13v3](waveshare2in13v3) | Waveshare 2.13" v3 e-paper display | +| [waveshare2in13v4](waveshare2in13v4) | Waveshare 2.13" v4 e-paper display | + + ## Authors `periph` was initiated with ❤️️ and passion by [Marc-Antoine From 421e8d88e72ad52ac26b79d9a0d6ed33cbdfc0d8 Mon Sep 17 00:00:00 2001 From: Drew Bednar Date: Sat, 2 May 2026 15:07:46 -0400 Subject: [PATCH 2/2] adding device table and gc9101 LCD display --- README.md | 1 + gc9a01/doc.go | 18 ++ gc9a01/example_test.go | 119 +++++++++++ gc9a01/gc9a01.go | 465 +++++++++++++++++++++++++++++++++++++++++ gc9a01/gc9a01_test.go | 318 ++++++++++++++++++++++++++++ 5 files changed, 921 insertions(+) create mode 100644 gc9a01/doc.go create mode 100644 gc9a01/example_test.go create mode 100644 gc9a01/gc9a01.go create mode 100644 gc9a01/gc9a01_test.go diff --git a/README.md b/README.md index cade0ea..39cfa38 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ examples! | [ds248x](ds248x) | Maxim DS2483/DS2482-100 1-wire interface chip over I²C | | [ep0099](ep0099) | EP-0099 Raspberry Pi HAT with 4 relays via I²C | | [epd](epd) | Waveshare e-paper display series | +| [gc9a01](gc9a01) | GC9A01 240x240 round RGB LCD display over SPI | | [hd44780](hd44780) | Hitachi HD44780 LCD display chipset | | [hdc302x](hdc302x) | Texas Instruments HDC3021/3022 temperature/humidity sensor over I²C | | [ht16k33](ht16k33) | Holtek HT16K33 16×8 LED driver | diff --git a/gc9a01/doc.go b/gc9a01/doc.go new file mode 100644 index 0000000..b56d3af --- /dev/null +++ b/gc9a01/doc.go @@ -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 diff --git a/gc9a01/example_test.go b/gc9a01/example_test.go new file mode 100644 index 0000000..f451d8b --- /dev/null +++ b/gc9a01/example_test.go @@ -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() +} diff --git a/gc9a01/gc9a01.go b/gc9a01/gc9a01.go new file mode 100644 index 0000000..bd0769b --- /dev/null +++ b/gc9a01/gc9a01.go @@ -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{} diff --git a/gc9a01/gc9a01_test.go b/gc9a01/gc9a01_test.go new file mode 100644 index 0000000..f342458 --- /dev/null +++ b/gc9a01/gc9a01_test.go @@ -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 +}