// Package st7789 implements a driver for the ST7789 TFT displays, it comes in various screen sizes. // // Datasheets: https://cdn-shop.adafruit.com/product-files/3787/3787_tft_QT154H2201__________20190228182902.pdf // // http://www.newhavendisplay.com/appnotes/datasheets/LCDs/ST7789V.pdf package st7789 // import "tinygo.org/x/drivers/st7789" import ( "image/color" "machine" "math" "time" "errors" "tinygo.org/x/drivers" "tinygo.org/x/drivers/pixel" ) // Rotation controls the rotation used by the display. // // Deprecated: use drivers.Rotation instead. type Rotation = drivers.Rotation // The color format used on the display, like RGB565, RGB666, and RGB444. 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. type FrameRate uint8 var ( errOutOfBounds = errors.New("rectangle coordinates outside display area") ) // Device wraps an SPI connection. 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 dcPin machine.Pin resetPin machine.Pin csPin machine.Pin blPin machine.Pin width int16 height int16 columnOffsetCfg int16 rowOffsetCfg int16 columnOffset int16 rowOffset int16 rotation drivers.Rotation frameRate FrameRate batchLength int32 batchData pixel.Image[T] // "image" with (width, height) of (batchLength, 1) isBGR bool vSyncLines int16 cmdBuf [1]byte buf [6]byte } // Config is the configuration for the display type Config struct { Width int16 Height int16 Rotation drivers.Rotation RowOffset int16 ColumnOffset int16 FrameRate FrameRate VSyncLines int16 // Gamma control. Look in the LCD panel datasheet or provided example code // to find these values. If not set, the defaults will be used. PVGAMCTRL []uint8 // Positive voltage gamma control (14 bytes) NVGAMCTRL []uint8 // Negative voltage gamma control (14 bytes) } // 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 { 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}) resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) blPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) return DeviceOf[T]{ bus: bus, dcPin: dcPin, resetPin: resetPin, csPin: csPin, blPin: blPin, } } // Configure initializes the display with default configuration func (d *DeviceOf[T]) 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 d.rowOffsetCfg = cfg.RowOffset d.columnOffsetCfg = cfg.ColumnOffset if cfg.FrameRate != 0 { d.frameRate = cfg.FrameRate } else { d.frameRate = FRAMERATE_60 } if cfg.VSyncLines >= 2 && cfg.VSyncLines <= 254 { d.vSyncLines = cfg.VSyncLines } else { d.vSyncLines = 16 } 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(50 * time.Millisecond) d.resetPin.Low() time.Sleep(50 * time.Millisecond) d.resetPin.High() time.Sleep(50 * time.Millisecond) // Common initialization d.startWrite() d.sendCommand(SWRESET, nil) // Soft reset d.endWrite() time.Sleep(150 * time.Millisecond) // d.startWrite() d.sendCommand(SLPOUT, nil) // Exit sleep mode // Memory initialization 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) d.setRotation(d.rotation) // Memory orientation d.setWindow(0, 0, d.width, d.height) // Full draw window d.fillScreen(color.RGBA{0, 0, 0, 255}) // Clear screen // Framerate d.sendCommand(FRCTRL2, []byte{byte(d.frameRate)}) // Frame rate for normal mode (default 60Hz) // Frame vertical sync and "porch" // // Front and back porch controls vertical scanline sync time before and after // a frame, where memory can be safely written without tearing. // fp := uint8(d.vSyncLines / 2) // Split the desired pause half and half bp := uint8(d.vSyncLines - int16(fp)) // between front and back porch. d.sendCommand(PORCTRL, []byte{ bp, // Back porch 5bit (0x7F max 0x08 default) fp, // Front porch 5bit (0x7F max 0x08 default) 0x00, // Seprarate porch (TODO: what is this?) 0x22, // Idle mode porch (4bit-back 4bit-front 0x22 default) 0x22, // Partial mode porch (4bit-back 4bit-front 0x22 default) }) // Ready to display d.sendCommand(INVON, nil) // Inversion ON time.Sleep(10 * time.Millisecond) // // Set gamma tables, if configured. if len(cfg.PVGAMCTRL) == 14 { d.sendCommand(GMCTRP1, cfg.PVGAMCTRL) // PVGAMCTRL: Positive Voltage Gamma Control } if len(cfg.NVGAMCTRL) == 14 { d.sendCommand(GMCTRN1, cfg.NVGAMCTRL) // NVGAMCTRL: Negative Voltage Gamma Control } d.sendCommand(NORON, nil) // Normal mode ON time.Sleep(10 * time.Millisecond) // d.sendCommand(DISPON, nil) // Screen ON time.Sleep(10 * time.Millisecond) // d.endWrite() d.blPin.High() // Backlight ON } // 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, // meaning that data can be sent right away. func (d *DeviceOf[T]) sendCommand(command uint8, data []byte) error { d.cmdBuf[0] = command d.dcPin.Low() err := d.bus.Tx(d.cmdBuf[:1], nil) d.dcPin.High() if len(data) != 0 { err = d.bus.Tx(data, nil) } return err } // startWrite must be called at the beginning of all exported methods to set the // chip select pin low. func (d *DeviceOf[T]) startWrite() { if d.csPin != machine.NoPin { d.csPin.Low() } } // endWrite must be called at the end of all exported methods to set the chip // select pin high. func (d *DeviceOf[T]) endWrite() { if d.csPin != machine.NoPin { 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 func (d *DeviceOf[T]) Sync() { d.SyncToScanLine(0) } // SyncToScanLine waits for the display to hit a specific scanline // // A scanline value of 0 will forward to the beginning of the next VSYNC, // even if the display is currently in a VSYNC pause. // // Syncline values appear to increment once for every two vertical // lines on the display. // // NOTE: Use GetHighestScanLine and GetLowestScanLine to obtain the highest // and lowest useful values. Values are affected by front and back porch // vsync settings (derived from VSyncLines configuration option). func (d *DeviceOf[T]) SyncToScanLine(scanline uint16) { scan := d.GetScanLine() // Sometimes GetScanLine returns erroneous 0 on first call after draw, so double check if scan == 0 { scan = d.GetScanLine() } if scanline == 0 { // we dont know where we are in an ongoing vsync so go around for scan < 1 { time.Sleep(1 * time.Millisecond) scan = d.GetScanLine() } for scan > 0 { scan = d.GetScanLine() } } else { // go around unless we're very close to the target for scan > scanline+4 { time.Sleep(1 * time.Millisecond) scan = d.GetScanLine() } for scan < scanline { scan = d.GetScanLine() } } } // GetScanLine reads the current scanline value from the display func (d *DeviceOf[T]) GetScanLine() uint16 { d.startWrite() data := []uint8{0x00, 0x00} d.dcPin.Low() d.bus.Transfer(GSCAN) d.dcPin.High() for i := range data { data[i], _ = d.bus.Transfer(0xFF) } scanline := uint16(data[0])<<8 + uint16(data[1]) d.endWrite() return scanline } // GetHighestScanLine calculates the last scanline id in the frame before VSYNC pause func (d *DeviceOf[T]) GetHighestScanLine() uint16 { // Last scanline id appears to be backporch/2 + 320/2 return uint16(math.Ceil(float64(d.vSyncLines)/2)/2) + 160 } // GetLowestScanLine calculate the first scanline id to appear after VSYNC pause func (d *DeviceOf[T]) GetLowestScanLine() uint16 { // First scanline id appears to be backporch/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 func (d *DeviceOf[T]) Display() error { return nil } // SetPixel sets a pixel in the screen func (d *DeviceOf[T]) SetPixel(x int16, y int16, c color.RGBA) { if x < 0 || y < 0 || (((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))) { return } d.FillRectangle(x, y, 1, 1, c) } // setWindow prepares the screen to be modified at a given rectangle func (d *DeviceOf[T]) setWindow(x, y, w, h int16) { x += d.columnOffset y += d.rowOffset copy(d.buf[:4], []uint8{uint8(x >> 8), uint8(x), uint8((x + w - 1) >> 8), uint8(x + w - 1)}) d.sendCommand(CASET, d.buf[:4]) copy(d.buf[:4], []uint8{uint8(y >> 8), uint8(y), uint8((y + h - 1) >> 8), uint8(y + h - 1)}) d.sendCommand(RASET, d.buf[:4]) d.sendCommand(RAMWR, nil) } // FillRectangle fills a rectangle at a given coordinates with a color func (d *DeviceOf[T]) FillRectangle(x, y, width, height int16, c color.RGBA) error { d.startWrite() err := d.fillRectangle(x, y, width, height, c) d.endWrite() return err } func (d *DeviceOf[T]) 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) image := d.getBuffer() image.FillSolidColor(pixel.NewColor[T](c.R, c.G, c.B)) j := int(width) * int(height) for j > 0 { // The DC pin is already set to data in the setWindow call, so we can // just write bytes on the SPI bus. if j >= image.Len() { d.bus.Tx(image.RawBuffer(), nil) } else { d.bus.Tx(image.Rescale(j, 1).RawBuffer(), nil) } j -= image.Len() } return nil } // DrawRGBBitmap8 copies an RGB bitmap to the internal buffer at given coordinates // // Deprecated: use DrawBitmap instead. func (d *DeviceOf[T]) 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.startWrite() d.setWindow(x, y, w, h) d.bus.Tx(data, nil) d.endWrite() return nil } // DrawBitmap copies the bitmap to the internal buffer on the screen at the // given coordinates. It returns once the image data has been sent completely. func (d *DeviceOf[T]) DrawBitmap(x, y int16, bitmap pixel.Image[T]) error { width, height := bitmap.Size() return d.DrawRGBBitmap8(x, y, bitmap.RawBuffer(), int16(width), int16(height)) } // FillRectangleWithBuffer fills buffer with a rectangle at a given coordinates. func (d *DeviceOf[T]) 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.startWrite() d.setWindow(x, y, width, height) k := int(width) * int(height) image := d.getBuffer() offset := 0 for k > 0 { for i := 0; i < image.Len(); i++ { if offset+i < len(buffer) { c := buffer[offset+i] image.Set(i, 0, pixel.NewColor[T](c.R, c.G, c.B)) } } // The DC pin is already set to data in the setWindow call, so we don't // have to set it here. if k >= image.Len() { d.bus.Tx(image.RawBuffer(), nil) } else { d.bus.Tx(image.Rescale(k, 1).RawBuffer(), nil) } k -= image.Len() offset += image.Len() } d.endWrite() return nil } // DrawFastVLine draws a vertical line faster than using SetPixel func (d *DeviceOf[T]) 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 *DeviceOf[T]) 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 *DeviceOf[T]) FillScreen(c color.RGBA) { d.startWrite() d.fillScreen(c) d.endWrite() } func (d *DeviceOf[T]) 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) } } // Control the color format that is used when writing to the screen. // 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 // specified color format using DrawRGBBitmap8. func (d *DeviceOf[T]) SetColorFormat(format ColorFormat) { d.startWrite() d.setColorFormat(format) d.endWrite() } func (d *DeviceOf[T]) setColorFormat(format ColorFormat) { // Lower 4 bits set the color format used in SPI. // 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 // reasonable default. Also, the RGB interface doesn't support RGB444. colmod := byte(format) | 0x50 d.sendCommand(COLMOD, []byte{colmod}) } // Rotation returns the current rotation of the device. func (d *DeviceOf[T]) Rotation() drivers.Rotation { return d.rotation } // SetRotation changes the rotation of the device (clock-wise) func (d *DeviceOf[T]) SetRotation(rotation Rotation) error { d.rotation = rotation d.startWrite() err := d.setRotation(rotation) d.endWrite() return err } func (d *DeviceOf[T]) setRotation(rotation Rotation) error { madctl := uint8(0) switch rotation % 4 { case drivers.Rotation0: d.rowOffset = 0 d.columnOffset = 0 case drivers.Rotation90: madctl = MADCTL_MX | MADCTL_MV d.rowOffset = d.columnOffsetCfg d.columnOffset = d.rowOffsetCfg case drivers.Rotation180: madctl = MADCTL_MX | MADCTL_MY d.rowOffset = d.rowOffsetCfg d.columnOffset = d.columnOffsetCfg case drivers.Rotation270: madctl = MADCTL_MY | MADCTL_MV d.rowOffset = d.columnOffsetCfg d.columnOffset = d.rowOffsetCfg } if d.isBGR { madctl |= MADCTL_BGR } return d.sendCommand(MADCTL, []byte{madctl}) } // Size returns the current size of the display. func (d *DeviceOf[T]) Size() (w, h int16) { if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 { return d.width, d.height } return d.height, d.width } // EnableBacklight enables or disables the backlight func (d *DeviceOf[T]) EnableBacklight(enable bool) { if enable { d.blPin.High() } else { d.blPin.Low() } } // 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 // will be kept. func (d *DeviceOf[T]) Sleep(sleepEnabled bool) error { if sleepEnabled { d.startWrite() d.sendCommand(SLPIN, nil) d.endWrite() time.Sleep(5 * time.Millisecond) // 5ms required by the datasheet } else { // Turn the LCD panel back on. d.startWrite() d.sendCommand(SLPOUT, nil) d.endWrite() // Note: the st7789 documentation says that it is needed to wait at // least 120ms before going to sleep again. Sleeping here would not be // practical (delays turning on the screen too much), so just hope the // screen won't need to sleep again for at least 120ms. // In practice, it's unlikely the user will set the display to sleep // again within 120ms. } return nil } // InvertColors inverts the colors of the screen func (d *DeviceOf[T]) InvertColors(invert bool) { d.startWrite() if invert { d.sendCommand(INVON, nil) } else { d.sendCommand(INVOFF, nil) } d.endWrite() } // IsBGR changes the color mode (RGB/BGR) func (d *DeviceOf[T]) IsBGR(bgr bool) { d.isBGR = bgr } // SetScrollArea sets an area to scroll with fixed top and bottom parts of the display. func (d *DeviceOf[T]) SetScrollArea(topFixedArea, bottomFixedArea int16) { if d.height < 320 { // The screen doesn't use the full 320 pixel height. // Enlarge the bottom fixed area to fill the 320 pixel height, so that // bottomFixedArea starts from the visible bottom of the screen. // // VSCRDEF/VSCRSADD always operate on physical frame memory rows (0-319), // regardless of MADCTL. For rotations with MV set (90°/270°), CASET // addresses physical rows due to row/column exchange, so the physical row // offset is d.columnOffset (= rowOffsetCfg). For other rotations, // d.rowOffset is the physical row offset. physRowOffset := d.rowOffset if d.rotation == drivers.Rotation90 || d.rotation == drivers.Rotation270 { physRowOffset = d.columnOffset } topFixedArea += physRowOffset bottomFixedArea += (320 - d.height) - physRowOffset } if d.rotation == drivers.Rotation180 { // The screen is rotated by 180°, so we have to switch the top and // bottom fixed area. topFixedArea, bottomFixedArea = bottomFixedArea, topFixedArea } verticalScrollArea := 320 - topFixedArea - bottomFixedArea copy(d.buf[:6], []uint8{ uint8(topFixedArea >> 8), uint8(topFixedArea), uint8(verticalScrollArea >> 8), uint8(verticalScrollArea), uint8(bottomFixedArea >> 8), uint8(bottomFixedArea)}) d.startWrite() d.sendCommand(VSCRDEF, d.buf[:6]) d.endWrite() } // SetScroll sets the vertical scroll address of the display. func (d *DeviceOf[T]) SetScroll(line int16) { switch d.rotation { case drivers.Rotation90: // With MV set, hardware scroll operates on physical rows, which map to the // visual X axis. Add the physical row offset (d.columnOffset = rowOffsetCfg) // so that line=0 addresses the first visible physical row. line = line + d.columnOffset case drivers.Rotation180: // The screen is rotated by 180°, so we have to invert the scroll line // (taking care of the RowOffset). line = (319 - d.rowOffset) - line case drivers.Rotation270: // With MV+MY, physical rows map to the visual X axis in reverse direction. // line=0 addresses the last physical row of the visible area. line = (d.columnOffset + d.height - 1) - line } d.buf[0] = uint8(line >> 8) d.buf[1] = uint8(line) d.startWrite() d.sendCommand(VSCRSADD, d.buf[:2]) d.endWrite() } // StopScroll returns the display to its normal state. func (d *DeviceOf[T]) StopScroll() { d.startWrite() d.sendCommand(NORON, nil) d.endWrite() }