mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-08 08:53:40 +00:00
cba17ef125
This is needed for high performance graphics. Using this API, and using partial updates to the screen (no full screen refreshes), I've been able to get well over 60fps of updates.
433 lines
9.5 KiB
Go
433 lines
9.5 KiB
Go
// Package st7735 implements a driver for the ST7735 TFT displays, it comes in various screen sizes.
|
|
//
|
|
// Datasheet: https://www.crystalfontz.com/controllers/Sitronix/ST7735R/319/
|
|
package st7735 // import "tinygo.org/x/drivers/st7735"
|
|
|
|
import (
|
|
"image/color"
|
|
"machine"
|
|
"time"
|
|
|
|
"errors"
|
|
|
|
"tinygo.org/x/drivers"
|
|
)
|
|
|
|
type Model uint8
|
|
type Rotation uint8
|
|
|
|
var (
|
|
errOutOfBounds = errors.New("rectangle coordinates outside display area")
|
|
)
|
|
|
|
// Device wraps an SPI connection.
|
|
type Device struct {
|
|
bus drivers.SPI
|
|
dcPin machine.Pin
|
|
resetPin machine.Pin
|
|
csPin machine.Pin
|
|
blPin machine.Pin
|
|
width int16
|
|
height int16
|
|
columnOffset int16
|
|
rowOffset int16
|
|
rotation Rotation
|
|
batchLength int16
|
|
model Model
|
|
isBGR bool
|
|
batchData []uint8
|
|
}
|
|
|
|
// Config is the configuration for the display
|
|
type Config struct {
|
|
Width int16
|
|
Height int16
|
|
Rotation Rotation
|
|
Model Model
|
|
RowOffset int16
|
|
ColumnOffset int16
|
|
}
|
|
|
|
// 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 {
|
|
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{
|
|
bus: bus,
|
|
dcPin: dcPin,
|
|
resetPin: resetPin,
|
|
csPin: csPin,
|
|
blPin: blPin,
|
|
}
|
|
}
|
|
|
|
// Configure initializes the display with default configuration
|
|
func (d *Device) Configure(cfg Config) {
|
|
d.model = cfg.Model
|
|
if cfg.Width != 0 {
|
|
d.width = cfg.Width
|
|
} else {
|
|
if d.model == MINI80x160 {
|
|
d.width = 80
|
|
} else {
|
|
d.width = 128
|
|
}
|
|
}
|
|
if cfg.Height != 0 {
|
|
d.height = cfg.Height
|
|
} else {
|
|
d.height = 160
|
|
}
|
|
d.rotation = cfg.Rotation
|
|
d.rowOffset = cfg.RowOffset
|
|
d.columnOffset = cfg.ColumnOffset
|
|
|
|
d.batchLength = d.width
|
|
if d.height > d.width {
|
|
d.batchLength = d.height
|
|
}
|
|
d.batchLength += d.batchLength & 1
|
|
d.batchData = make([]uint8, d.batchLength*2)
|
|
|
|
// reset the device
|
|
d.resetPin.High()
|
|
time.Sleep(5 * time.Millisecond)
|
|
d.resetPin.Low()
|
|
time.Sleep(20 * time.Millisecond)
|
|
d.resetPin.High()
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
// Common initialization
|
|
d.Command(SWRESET)
|
|
time.Sleep(150 * time.Millisecond)
|
|
d.Command(SLPOUT)
|
|
time.Sleep(500 * time.Millisecond)
|
|
d.Command(FRMCTR1)
|
|
d.Data(0x01)
|
|
d.Data(0x2C)
|
|
d.Data(0x2D)
|
|
d.Command(FRMCTR2)
|
|
d.Data(0x01)
|
|
d.Data(0x2C)
|
|
d.Data(0x2D)
|
|
d.Command(FRMCTR3)
|
|
d.Data(0x01)
|
|
d.Data(0x2C)
|
|
d.Data(0x2D)
|
|
d.Data(0x01)
|
|
d.Data(0x2C)
|
|
d.Data(0x2D)
|
|
d.Command(INVCTR)
|
|
d.Data(0x07)
|
|
d.Command(PWCTR1)
|
|
d.Data(0xA2)
|
|
d.Data(0x02)
|
|
d.Data(0x84)
|
|
d.Command(PWCTR2)
|
|
d.Data(0xC5)
|
|
d.Command(PWCTR3)
|
|
d.Data(0x0A)
|
|
d.Data(0x00)
|
|
d.Command(PWCTR4)
|
|
d.Data(0x8A)
|
|
d.Data(0x2A)
|
|
d.Command(PWCTR5)
|
|
d.Data(0x8A)
|
|
d.Data(0xEE)
|
|
d.Command(VMCTR1)
|
|
d.Data(0x0E)
|
|
d.Command(COLMOD)
|
|
d.Data(0x05)
|
|
|
|
if d.model == GREENTAB {
|
|
d.InvertColors(false)
|
|
} else if d.model == MINI80x160 {
|
|
d.isBGR = true
|
|
d.InvertColors(true)
|
|
}
|
|
|
|
// common color adjustment
|
|
d.Command(GMCTRP1)
|
|
d.Data(0x02)
|
|
d.Data(0x1C)
|
|
d.Data(0x07)
|
|
d.Data(0x12)
|
|
d.Data(0x37)
|
|
d.Data(0x32)
|
|
d.Data(0x29)
|
|
d.Data(0x2D)
|
|
d.Data(0x29)
|
|
d.Data(0x25)
|
|
d.Data(0x2B)
|
|
d.Data(0x39)
|
|
d.Data(0x00)
|
|
d.Data(0x01)
|
|
d.Data(0x03)
|
|
d.Data(0x10)
|
|
d.Command(GMCTRN1)
|
|
d.Data(0x03)
|
|
d.Data(0x1D)
|
|
d.Data(0x07)
|
|
d.Data(0x06)
|
|
d.Data(0x2E)
|
|
d.Data(0x2C)
|
|
d.Data(0x29)
|
|
d.Data(0x2D)
|
|
d.Data(0x2E)
|
|
d.Data(0x2E)
|
|
d.Data(0x37)
|
|
d.Data(0x3F)
|
|
d.Data(0x00)
|
|
d.Data(0x00)
|
|
d.Data(0x02)
|
|
d.Data(0x10)
|
|
|
|
d.Command(NORON)
|
|
time.Sleep(10 * time.Millisecond)
|
|
d.Command(DISPON)
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
if cfg.Model == MINI80x160 {
|
|
d.Command(MADCTL)
|
|
d.Data(0xC0)
|
|
}
|
|
|
|
d.SetRotation(d.rotation)
|
|
|
|
d.blPin.High()
|
|
}
|
|
|
|
// Display does nothing, there's no buffer as it might be too big for some boards
|
|
func (d *Device) Display() error {
|
|
return nil
|
|
}
|
|
|
|
// SetPixel sets a pixel in the screen
|
|
func (d *Device) SetPixel(x int16, y int16, c color.RGBA) {
|
|
w, h := d.Size()
|
|
if x < 0 || y < 0 || x >= w || y >= h {
|
|
return
|
|
}
|
|
d.FillRectangle(x, y, 1, 1, c)
|
|
}
|
|
|
|
// setWindow prepares the screen to be modified at a given rectangle
|
|
func (d *Device) setWindow(x, y, w, h int16) {
|
|
if d.rotation == NO_ROTATION || d.rotation == ROTATION_180 {
|
|
x += d.columnOffset
|
|
y += d.rowOffset
|
|
} else {
|
|
x += d.rowOffset
|
|
y += d.columnOffset
|
|
}
|
|
d.Tx([]uint8{CASET}, true)
|
|
d.Tx([]uint8{uint8(x >> 8), uint8(x), uint8((x + w - 1) >> 8), uint8(x + w - 1)}, false)
|
|
d.Tx([]uint8{RASET}, true)
|
|
d.Tx([]uint8{uint8(y >> 8), uint8(y), uint8((y + h - 1) >> 8), uint8(y + h - 1)}, false)
|
|
d.Command(RAMWR)
|
|
}
|
|
|
|
// SetScrollWindow sets an area to scroll with fixed top and bottom parts of the display
|
|
func (d *Device) SetScrollArea(topFixedArea, bottomFixedArea int16) {
|
|
d.Command(VSCRDEF)
|
|
d.Tx([]uint8{
|
|
uint8(topFixedArea >> 8), uint8(topFixedArea),
|
|
uint8(d.height - topFixedArea - bottomFixedArea>>8), uint8(d.height - topFixedArea - bottomFixedArea),
|
|
uint8(bottomFixedArea >> 8), uint8(bottomFixedArea)},
|
|
false)
|
|
}
|
|
|
|
// SetScroll sets the vertical scroll address of the display.
|
|
func (d *Device) 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() {
|
|
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 {
|
|
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
|
|
}
|
|
i = width * height
|
|
for i > 0 {
|
|
if i >= d.batchLength {
|
|
d.Tx(d.batchData, false)
|
|
} else {
|
|
d.Tx(d.batchData[:i*2], false)
|
|
}
|
|
i -= d.batchLength
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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 {
|
|
k, i := d.Size()
|
|
if x < 0 || y < 0 || w <= 0 || h <= 0 ||
|
|
x >= k || (x+w) > k || y >= i || (y+h) > i {
|
|
return errOutOfBounds
|
|
}
|
|
d.setWindow(x, y, w, h)
|
|
d.Tx(data, false)
|
|
return nil
|
|
}
|
|
|
|
// FillRectangle fills a rectangle at a given coordinates with a buffer
|
|
func (d *Device) 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 {
|
|
return errors.New("rectangle coordinates outside display area")
|
|
}
|
|
k = width * height
|
|
l = int16(len(buffer))
|
|
if k != l {
|
|
return errors.New("buffer length does not match with rectangle size")
|
|
}
|
|
|
|
d.setWindow(x, y, width, height)
|
|
|
|
offset := int16(0)
|
|
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
|
|
}
|
|
}
|
|
if k >= d.batchLength {
|
|
d.Tx(d.batchData, false)
|
|
} else {
|
|
d.Tx(d.batchData[:k*2], false)
|
|
}
|
|
k -= d.batchLength
|
|
offset += d.batchLength
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DrawFastVLine draws a vertical line faster than using SetPixel
|
|
func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) {
|
|
if y0 > y1 {
|
|
y0, y1 = y1, y0
|
|
}
|
|
d.FillRectangle(x, y0, 1, y1-y0+1, c)
|
|
}
|
|
|
|
// DrawFastHLine draws a horizontal line faster than using SetPixel
|
|
func (d *Device) DrawFastHLine(x0, x1, y int16, c color.RGBA) {
|
|
if x0 > x1 {
|
|
x0, x1 = x1, x0
|
|
}
|
|
d.FillRectangle(x0, y, x1-x0+1, 1, c)
|
|
}
|
|
|
|
// FillScreen fills the screen with a given color
|
|
func (d *Device) FillScreen(c color.RGBA) {
|
|
if d.rotation == NO_ROTATION || d.rotation == ROTATION_180 {
|
|
d.FillRectangle(0, 0, d.width, d.height, c)
|
|
} else {
|
|
d.FillRectangle(0, 0, d.height, d.width, c)
|
|
}
|
|
}
|
|
|
|
// SetRotation changes the rotation of the device (clock-wise)
|
|
func (d *Device) SetRotation(rotation Rotation) {
|
|
madctl := uint8(0)
|
|
switch rotation % 4 {
|
|
case 0:
|
|
madctl = MADCTL_MX | MADCTL_MY
|
|
break
|
|
case 1:
|
|
madctl = MADCTL_MY | MADCTL_MV
|
|
break
|
|
case 2:
|
|
break
|
|
case 3:
|
|
madctl = MADCTL_MX | MADCTL_MV
|
|
break
|
|
}
|
|
if d.isBGR {
|
|
madctl |= MADCTL_BGR
|
|
}
|
|
d.Command(MADCTL)
|
|
d.Data(madctl)
|
|
|
|
}
|
|
|
|
// Command sends a command to the display
|
|
func (d *Device) Command(command uint8) {
|
|
d.Tx([]byte{command}, true)
|
|
}
|
|
|
|
// Command sends a data to the display
|
|
func (d *Device) Data(data uint8) {
|
|
d.Tx([]byte{data}, false)
|
|
}
|
|
|
|
// Tx sends data to the display
|
|
func (d *Device) 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) {
|
|
if d.rotation == NO_ROTATION || d.rotation == ROTATION_180 {
|
|
return d.width, d.height
|
|
}
|
|
return d.height, d.width
|
|
}
|
|
|
|
// EnableBacklight enables or disables the backlight
|
|
func (d *Device) EnableBacklight(enable bool) {
|
|
if enable {
|
|
d.blPin.High()
|
|
} else {
|
|
d.blPin.Low()
|
|
}
|
|
}
|
|
|
|
// InverColors inverts the colors of the screen
|
|
func (d *Device) InvertColors(invert bool) {
|
|
if invert {
|
|
d.Command(INVON)
|
|
} else {
|
|
d.Command(INVOFF)
|
|
}
|
|
}
|
|
|
|
// IsBGR changes the color mode (RGB/BGR)
|
|
func (d *Device) 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))
|
|
}
|