From ce9c93f228c311dc3c80786183776b7eaec0bf8b Mon Sep 17 00:00:00 2001 From: Jason Striegel Date: Thu, 13 Aug 2020 00:48:27 -0400 Subject: [PATCH] Add support for setting framerate, vsync pause, and reading scanline data. - FrameRate option sets device framerates from 39-111Hz - VSyncLines option adjusts device vsync pause using st7789 "porch control" feature - Added Rx(cmd, bytes[]) to support retrieving scanline timing data over SPI - Added Sync functions to support syncronization of animation to vertical scanline timing - Minor adjustments to Configure to clear screen memory before display is visible --- examples/st7789/main.go | 31 ++++++- st7789/registers.go | 38 +++++++++ st7789/st7789.go | 173 ++++++++++++++++++++++++++++++++-------- 3 files changed, 204 insertions(+), 38 deletions(-) diff --git a/examples/st7789/main.go b/examples/st7789/main.go index 1d74bb7..48532d8 100644 --- a/examples/st7789/main.go +++ b/examples/st7789/main.go @@ -9,12 +9,37 @@ import ( ) func main() { + + // Example configuration for Adafruit Clue + // machine.SPI1.Configure(machine.SPIConfig{ + // Frequency: 8000000, + // SCK: machine.TFT_SCK, + // SDO: machine.TFT_SDO, + // SDI: machine.TFT_SDO, + // Mode: 0, + // }) + // display := st7789.New(machine.SPI1, + // machine.TFT_RESET, + // machine.TFT_DC, + // machine.TFT_CS, + // machine.TFT_LITE) + machine.SPI0.Configure(machine.SPIConfig{ Frequency: 8000000, - Mode: 3, + Mode: 0, + }) + display := st7789.New(machine.SPI0, + machine.P6, // TFT_RESET + machine.P7, // TFT_DC + machine.P8, // TFT_CS + machine.P9) // TFT_LITE + + display.Configure(st7789.Config{ + Rotation: st7789.NO_ROTATION, + RowOffset: 80, + FrameRate: st7789.FRAMERATE_111, + VSyncLines: st7789.MAX_VSYNC_SCANLINES, }) - display := st7789.New(machine.SPI0, machine.P6, machine.P7, machine.P8, machine.P9) - display.Configure(st7789.Config{Rotation: st7789.NO_ROTATION}) width, height := display.Size() diff --git a/st7789/registers.go b/st7789/registers.go index 2a905e4..cdea194 100644 --- a/st7789/registers.go +++ b/st7789/registers.go @@ -33,7 +33,9 @@ const ( RDID3 = 0xDC RDID4 = 0xDD FRMCTR1 = 0xB1 + RGBCTRL = 0xB1 FRMCTR2 = 0xB2 + PORCTRL = 0xB2 FRMCTR3 = 0xB3 INVCTR = 0xB4 DISSET5 = 0xB6 @@ -47,9 +49,45 @@ const ( PWCTR6 = 0xFC GMCTRP1 = 0xE0 GMCTRN1 = 0xE1 + GSCAN = 0x45 NO_ROTATION Rotation = 0 ROTATION_90 Rotation = 1 // 90 degrees clock-wise rotation ROTATION_180 Rotation = 2 ROTATION_270 Rotation = 3 + + // Allowable frame rate codes for FRCTRL2 (Identifier is in Hz) + FRAMERATE_111 FrameRate = 0x01 + FRAMERATE_105 FrameRate = 0x02 + FRAMERATE_99 FrameRate = 0x03 + FRAMERATE_94 FrameRate = 0x04 + FRAMERATE_90 FrameRate = 0x05 + FRAMERATE_86 FrameRate = 0x06 + FRAMERATE_82 FrameRate = 0x07 + FRAMERATE_78 FrameRate = 0x08 + FRAMERATE_75 FrameRate = 0x09 + FRAMERATE_72 FrameRate = 0x0A + FRAMERATE_69 FrameRate = 0x0B + FRAMERATE_67 FrameRate = 0x0C + FRAMERATE_64 FrameRate = 0x0D + FRAMERATE_62 FrameRate = 0x0E + FRAMERATE_60 FrameRate = 0x0F // 60 is default + FRAMERATE_58 FrameRate = 0x10 + FRAMERATE_57 FrameRate = 0x11 + FRAMERATE_55 FrameRate = 0x12 + FRAMERATE_53 FrameRate = 0x13 + FRAMERATE_52 FrameRate = 0x14 + FRAMERATE_50 FrameRate = 0x15 + FRAMERATE_49 FrameRate = 0x16 + FRAMERATE_48 FrameRate = 0x17 + FRAMERATE_46 FrameRate = 0x18 + FRAMERATE_45 FrameRate = 0x19 + FRAMERATE_44 FrameRate = 0x1A + FRAMERATE_43 FrameRate = 0x1B + FRAMERATE_42 FrameRate = 0x1C + FRAMERATE_41 FrameRate = 0x1D + FRAMERATE_40 FrameRate = 0x1E + FRAMERATE_39 FrameRate = 0x1F + + MAX_VSYNC_SCANLINES = 254 ) diff --git a/st7789/st7789.go b/st7789/st7789.go index 6a295fb..03ae024 100644 --- a/st7789/st7789.go +++ b/st7789/st7789.go @@ -1,12 +1,14 @@ // 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 +// 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" @@ -14,6 +16,8 @@ import ( type Rotation uint8 +type FrameRate uint8 + // Device wraps an SPI connection. type Device struct { bus machine.SPI @@ -28,8 +32,10 @@ type Device struct { columnOffset int16 rowOffset int16 rotation Rotation + frameRate FrameRate batchLength int32 isBGR bool + vSyncLines int16 } // Config is the configuration for the display @@ -39,6 +45,8 @@ type Config struct { Rotation Rotation RowOffset int16 ColumnOffset int16 + FrameRate FrameRate + VSyncLines int16 } // New creates a new ST7789 connection. The SPI wire must already be configured. @@ -73,13 +81,25 @@ func (d *Device) Configure(cfg Config) { 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 + // Reset the device d.resetPin.High() time.Sleep(50 * time.Millisecond) d.resetPin.Low() @@ -87,46 +107,118 @@ func (d *Device) Configure(cfg Config) { d.resetPin.High() time.Sleep(50 * time.Millisecond) - - // Common initialization - d.Command(SWRESET) // Soft reset - time.Sleep(150 * time.Millisecond) + d.Command(SWRESET) // Soft reset + time.Sleep(150 * time.Millisecond) // - d.Command(SLPOUT) // Exit sleep mode - time.Sleep(500 * time.Millisecond) + d.Command(SLPOUT) // Exit sleep mode + time.Sleep(500 * time.Millisecond) // - d.Command(COLMOD) // Set color mode - d.Data(0x55) // 16-bit color - time.Sleep(10 * time.Millisecond) + // Memory initialization + d.Command(COLMOD) // Set color mode + d.Data(0x55) // 16-bit color + time.Sleep(10 * time.Millisecond) // - d.Command(MADCTL) // Memory orientation - d.Data(0x08) // row/col bottom to top refresh + d.SetRotation(d.rotation) // Memory orientation - d.Command(CASET) // Column addr range set - d.Data(0x00) - d.Data(0x00) // xstart (0) 2 bytes - d.Data(240 >> 8) - d.Data(240 & 0xFF) // xend (240) 2 bytes + d.setWindow(0, 0, d.width, d.height) // Full draw window + d.FillScreen(color.RGBA{0, 0, 0, 255}) // Clear screen - d.Command(RASET) // Row addr range set - d.Data(0x00) - d.Data(0x00) // ystart (0) 2 bytes - d.Data(320 >> 8) - d.Data(320 & 0xFF) // yend (320) 2 bytes + // Framerate + d.Command(FRCTRL2) // Frame rate for normal mode + d.Data(uint8(d.frameRate)) // Default is 60Hz - d.Command(INVON) // Inversion ON - time.Sleep(10 * time.Millisecond) + // 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.Command(NORON) // Normal mode ON - time.Sleep(10 * time.Millisecond) + d.Command(PORCTRL) + d.Data(bp) // Back porch 5bit (0x7F max 0x08 default) + d.Data(fp) // Front porch 5bit (0x7F max 0x08 default) + d.Data(0x00) // Seprarate porch (TODO: what is this?) + d.Data(0x22) // Idle mode porch (4bit-back 4bit-front 0x22 default) + d.Data(0x22) // Partial mode porch (4bit-back 4bit-front 0x22 default) - d.Command(DISPON) // Screen ON - time.Sleep(10 * time.Millisecond) + // Ready to display + d.Command(INVON) // Inversion ON + time.Sleep(10 * time.Millisecond) // - d.SetRotation(d.rotation) + d.Command(NORON) // Normal mode ON + time.Sleep(10 * time.Millisecond) // - d.blPin.High() + d.Command(DISPON) // Screen ON + time.Sleep(10 * time.Millisecond) // + + d.blPin.High() // Backlight ON +} + +// Sync waits for the display to hit the next VSYNC pause +func (d *Device) 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 *Device) 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 *Device) GetScanLine() uint16 { + data := []uint8{0x00, 0x00} + d.Rx(GSCAN, data) + return uint16(data[0])<<8 + uint16(data[1]) +} + +// GetHighestScanLine calculates the last scanline id in the frame before VSYNC pause +func (d *Device) 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 *Device) 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 @@ -155,8 +247,6 @@ func (d *Device) setWindow(x, y, w, h int16) { 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() @@ -293,11 +383,24 @@ func (d *Device) Data(data uint8) { 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) } + d.csPin.Low() + d.bus.Tx(data, nil) + d.csPin.High() +} + +// Rx reads data from the display +func (d *Device) Rx(command uint8, read_bytes []byte) { + d.dcPin.Low() + d.csPin.Low() + d.bus.Transfer(command) + d.dcPin.High() + for i := range read_bytes { + read_bytes[i], _ = d.bus.Transfer(0xFF) + } + d.csPin.High() } // Size returns the current size of the display.