// Package st7789 implements a driver for the ST7789 TFT displays, it comes in various screen sizes. // // Datasheet: https://cdn-shop.adafruit.com/product-files/3787/3787_tft_QT154H2201__________20190228182902.pdf // package st7789 // import "tinygo.org/x/drivers/st7789" import ( "image/color" "machine" "time" "errors" ) type Rotation uint8 // Device wraps an SPI connection. type Device struct { bus machine.SPI dcPin machine.Pin resetPin machine.Pin blPin machine.Pin width int16 height int16 columnOffsetCfg int16 rowOffsetCfg int16 columnOffset int16 rowOffset int16 rotation Rotation batchLength int32 isBGR bool } // Config is the configuration for the display type Config struct { Width int16 Height int16 Rotation Rotation RowOffset int16 ColumnOffset int16 } // New creates a new ST7789 connection. The SPI wire must already be configured. func New(bus machine.SPI, resetPin, dcPin, blPin machine.Pin) Device { dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) blPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) return Device{ bus: bus, dcPin: dcPin, resetPin: resetPin, blPin: blPin, } } // Configure initializes the display with default configuration func (d *Device) Configure(cfg Config) { if cfg.Width != 0 { d.width = cfg.Width } else { d.width = 240 } if cfg.Height != 0 { d.height = cfg.Height } else { d.height = 240 } d.rotation = cfg.Rotation if cfg.RowOffset != 0 { d.rowOffsetCfg = cfg.RowOffset } else { d.rowOffsetCfg = 80 } if cfg.ColumnOffset != 0 { d.columnOffsetCfg = cfg.ColumnOffset } d.batchLength = int32(d.width) if d.height > d.width { d.batchLength = int32(d.height) } d.batchLength += d.batchLength & 1 // 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(COLMOD) d.Data(0x55) time.Sleep(10 * time.Millisecond) d.SetRotation(d.rotation) d.InvertColors(true) d.Command(NORON) time.Sleep(10 * time.Millisecond) d.Command(DISPON) time.Sleep(500 * time.Millisecond) 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) { if x < 0 || y < 0 || (((d.rotation == NO_ROTATION || d.rotation == ROTATION_180) && (x >= d.width || y >= d.height)) || ((d.rotation == ROTATION_90 || d.rotation == ROTATION_270) && (x >= d.height || y >= d.width))) { 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) { x += d.columnOffset y += d.rowOffset 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) } // 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) data := make([]uint8, d.batchLength*2) for i := int32(0); i < d.batchLength; i++ { data[i*2] = c1 data[i*2+1] = c2 } j := int32(width) * int32(height) for j > 0 { if j >= d.batchLength { d.Tx(data, false) } else { d.Tx(data[:j*2], false) } j -= d.batchLength } 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 { i, j := d.Size() if x < 0 || y < 0 || width <= 0 || height <= 0 || x >= i || (x+width) > i || y >= j || (y+height) > j { return errors.New("rectangle coordinates outside display area") } if int32(width)*int32(height) != int32(len(buffer)) { return errors.New("buffer length does not match with rectangle size") } d.setWindow(x, y, width, height) k := int32(width) * int32(height) data := make([]uint8, d.batchLength*2) offset := int32(0) for k > 0 { for i := int32(0); i < d.batchLength; i++ { if offset+i < int32(len(buffer)) { c565 := RGBATo565(buffer[offset+i]) c1 := uint8(c565 >> 8) c2 := uint8(c565) data[i*2] = c1 data[i*2+1] = c2 } } if k >= d.batchLength { d.Tx(data, false) } else { d.Tx(data[: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, y, 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 d.rowOffset = d.rowOffsetCfg d.columnOffset = d.columnOffsetCfg break case 1: madctl = MADCTL_MY | MADCTL_MV d.rowOffset = d.columnOffsetCfg d.columnOffset = d.rowOffsetCfg break case 2: d.rowOffset = 0 d.columnOffset = 0 break case 3: madctl = MADCTL_MX | MADCTL_MV d.rowOffset = 0 d.columnOffset = 0 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) { if isCommand { d.dcPin.Low() d.bus.Tx(data, nil) } else { d.dcPin.High() 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)) }