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
This commit is contained in:
Jason Striegel
2020-08-13 00:48:27 -04:00
committed by Ron Evans
parent db02cbb8a4
commit ce9c93f228
3 changed files with 204 additions and 38 deletions
+28 -3
View File
@@ -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()
+38
View File
@@ -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
)
+138 -35
View File
@@ -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.