mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-08-10 01:43:40 +00:00
f6d399ec08
The existing code was broken in a few ways:
- It didn't use the correct operator precedence for the VSCRDEF VSA
variable: it needed some extra parentheses to be correct.
- It used the configured height instead of the actual display height
for calculating VSA, which is incorrect. TFA+VSA+BFA must always be
exactly 320, even if a lower value is configured.
- If a lower than 320 pixel height is configured, the bottomFixedArea
parameter applied to the whole 320 pixel screen height. Because this
seems counter intuitive (and relies on properties of any given
screen), I've changed it to work from the actual visible bottom of
the screen (which may be smaller than 320 pixels).
TODO: this doesn't take RowOffset into account, while it probably
should.
I haven't fixed the st7735 implementation, because I didn't have example
code on hand that would easily work on a st7735 screen. This is left as
a TODO for the future.
423 lines
11 KiB
Go
423 lines
11 KiB
Go
package ili9341
|
|
|
|
import (
|
|
"errors"
|
|
"image/color"
|
|
"machine"
|
|
"time"
|
|
|
|
"tinygo.org/x/drivers"
|
|
)
|
|
|
|
type Config struct {
|
|
Width int16
|
|
Height int16
|
|
Rotation drivers.Rotation
|
|
DisplayInversion bool
|
|
}
|
|
|
|
type Device struct {
|
|
width int16
|
|
height int16
|
|
rotation drivers.Rotation
|
|
driver driver
|
|
|
|
x0, x1 int16 // cached address window; prevents useless/expensive
|
|
y0, y1 int16 // syscalls to PASET and CASET
|
|
|
|
dc machine.Pin
|
|
cs machine.Pin
|
|
rst machine.Pin
|
|
rd machine.Pin
|
|
}
|
|
|
|
var cmdBuf [6]byte
|
|
|
|
var initCmd = []byte{
|
|
0xEF, 3, 0x03, 0x80, 0x02,
|
|
0xCF, 3, 0x00, 0xC1, 0x30,
|
|
0xED, 4, 0x64, 0x03, 0x12, 0x81,
|
|
0xE8, 3, 0x85, 0x00, 0x78,
|
|
0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02,
|
|
0xF7, 1, 0x20,
|
|
0xEA, 2, 0x00, 0x00,
|
|
PWCTR1, 1, 0x23, // Power control VRH[5:0]
|
|
PWCTR2, 1, 0x10, // Power control SAP[2:0];BT[3:0]
|
|
VMCTR1, 2, 0x3e, 0x28, // VCM control
|
|
VMCTR2, 1, 0x86, // VCM control2
|
|
MADCTL, 1, 0x48, // Memory Access Control
|
|
VSCRSADD, 1, 0x00, // Vertical scroll zero
|
|
PIXFMT, 1, 0x55,
|
|
FRMCTR1, 2, 0x00, 0x18,
|
|
DFUNCTR, 3, 0x08, 0x82, 0x27, // Display Function Control
|
|
0xF2, 1, 0x00, // 3Gamma Function Disable
|
|
GAMMASET, 1, 0x01, // Gamma curve selected
|
|
GMCTRP1, 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma
|
|
0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00,
|
|
GMCTRN1, 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma
|
|
0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F,
|
|
}
|
|
|
|
// Configure prepares display for use
|
|
func (d *Device) Configure(config Config) {
|
|
|
|
if config.Width == 0 {
|
|
config.Width = TFTWIDTH
|
|
}
|
|
if config.Height == 0 {
|
|
config.Height = TFTHEIGHT
|
|
}
|
|
d.width = config.Width
|
|
d.height = config.Height
|
|
d.rotation = config.Rotation
|
|
|
|
// try to pick an initial cache miss for one of the points
|
|
d.x0, d.x1 = -(d.width + 1), d.x0
|
|
d.y0, d.y1 = -(d.height + 1), d.y0
|
|
|
|
output := machine.PinConfig{machine.PinOutput}
|
|
|
|
// configure chip select if there is one
|
|
if d.cs != machine.NoPin {
|
|
d.cs.Configure(output)
|
|
d.cs.High() // deselect
|
|
}
|
|
|
|
d.dc.Configure(output)
|
|
d.dc.High() // data mode
|
|
|
|
// driver-specific configuration
|
|
d.driver.configure(&config)
|
|
|
|
if d.rd != machine.NoPin {
|
|
d.rd.Configure(output)
|
|
d.rd.High()
|
|
}
|
|
|
|
// reset the display
|
|
if d.rst != machine.NoPin {
|
|
// configure hardware reset if there is one
|
|
d.rst.Configure(output)
|
|
d.rst.High()
|
|
delay(100)
|
|
d.rst.Low()
|
|
delay(100)
|
|
d.rst.High()
|
|
delay(200)
|
|
} else {
|
|
// if no hardware reset, send software reset
|
|
d.sendCommand(SWRESET, nil)
|
|
delay(150)
|
|
}
|
|
|
|
if config.DisplayInversion {
|
|
initCmd = append(initCmd, INVON, 0x80)
|
|
}
|
|
|
|
initCmd = append(initCmd,
|
|
SLPOUT, 0x80, // Exit Sleep
|
|
DISPON, 0x80, // Display on
|
|
0x00, // End of list
|
|
)
|
|
for i, c := 0, len(initCmd); i < c; {
|
|
cmd := initCmd[i]
|
|
if cmd == 0x00 {
|
|
break
|
|
}
|
|
x := initCmd[i+1]
|
|
numArgs := int(x & 0x7F)
|
|
d.sendCommand(cmd, initCmd[i+2:i+2+numArgs])
|
|
if x&0x80 > 0 {
|
|
delay(150)
|
|
}
|
|
i += numArgs + 2
|
|
}
|
|
|
|
d.SetRotation(d.rotation)
|
|
}
|
|
|
|
// Size returns the current size of the display.
|
|
func (d *Device) Size() (x, y int16) {
|
|
switch d.rotation {
|
|
case Rotation90, Rotation270, Rotation90Mirror, Rotation270Mirror:
|
|
return d.height, d.width
|
|
default: // Rotation0, Rotation180, etc
|
|
return d.width, d.height
|
|
}
|
|
}
|
|
|
|
// SetPixel modifies the internal buffer.
|
|
func (d *Device) SetPixel(x, y int16, c color.RGBA) {
|
|
d.setWindow(x, y, 1, 1)
|
|
c565 := RGBATo565(c)
|
|
d.startWrite()
|
|
d.driver.write16(c565)
|
|
d.endWrite()
|
|
}
|
|
|
|
// Display sends the buffer (if any) to the screen.
|
|
func (d *Device) Display() error {
|
|
return nil
|
|
}
|
|
|
|
// EnableTEOutput enables the TE ("tearing effect") line.
|
|
// The TE line goes high when the screen is not currently being updated and can
|
|
// be used to start drawing. When used correctly, it can avoid tearing entirely.
|
|
func (d *Device) EnableTEOutput(on bool) {
|
|
if on {
|
|
cmdBuf[0] = 0
|
|
d.sendCommand(TEON, cmdBuf[:1]) // M=0 (V-blanking only, no H-blanking)
|
|
} else {
|
|
d.sendCommand(TEOFF, nil) // TEOFF
|
|
}
|
|
}
|
|
|
|
// DrawRGBBitmap copies an RGB bitmap to the internal buffer at given coordinates
|
|
func (d *Device) DrawRGBBitmap(x, y int16, data []uint16, 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 errors.New("rectangle coordinates outside display area")
|
|
}
|
|
d.setWindow(x, y, w, h)
|
|
d.startWrite()
|
|
d.driver.write16sl(data)
|
|
d.endWrite()
|
|
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 errors.New("rectangle coordinates outside display area")
|
|
}
|
|
d.setWindow(x, y, w, h)
|
|
d.startWrite()
|
|
d.driver.write8sl(data)
|
|
d.endWrite()
|
|
return nil
|
|
}
|
|
|
|
// FillRectangle fills a rectangle at 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)
|
|
d.startWrite()
|
|
d.driver.write16n(c565, int(width)*int(height))
|
|
d.endWrite()
|
|
return nil
|
|
}
|
|
|
|
// DrawRectangle draws a rectangle at given coordinates with a color
|
|
func (d *Device) DrawRectangle(x, y, w, h int16, c color.RGBA) error {
|
|
if err := d.DrawFastHLine(x, x+w-1, y, c); err != nil {
|
|
return err
|
|
}
|
|
if err := d.DrawFastHLine(x, x+w-1, y+h-1, c); err != nil {
|
|
return err
|
|
}
|
|
if err := d.DrawFastVLine(x, y, y+h-1, c); err != nil {
|
|
return err
|
|
}
|
|
if err := d.DrawFastVLine(x+w-1, y, y+h-1, c); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DrawFastVLine draws a vertical line faster than using SetPixel
|
|
func (d *Device) DrawFastVLine(x, y0, y1 int16, c color.RGBA) error {
|
|
if y0 > y1 {
|
|
y0, y1 = y1, y0
|
|
}
|
|
return 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) error {
|
|
if x0 > x1 {
|
|
x0, x1 = x1, x0
|
|
}
|
|
return 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 == Rotation0 || d.rotation == Rotation180 {
|
|
d.FillRectangle(0, 0, d.width, d.height, c)
|
|
} else {
|
|
d.FillRectangle(0, 0, d.height, d.width, c)
|
|
}
|
|
}
|
|
|
|
// 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 *Device) Sleep(sleepEnabled bool) error {
|
|
if sleepEnabled {
|
|
// Shut down LCD panel.
|
|
d.sendCommand(SLPIN, nil)
|
|
time.Sleep(5 * time.Millisecond) // 5ms required by the datasheet
|
|
} else {
|
|
// Turn the LCD panel back on.
|
|
d.sendCommand(SLPOUT, nil)
|
|
// Note: the ili9341 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
|
|
}
|
|
|
|
// Rotation returns the current rotation of the device.
|
|
func (d *Device) Rotation() drivers.Rotation {
|
|
return d.rotation
|
|
}
|
|
|
|
// GetRotation returns the current rotation of the device.
|
|
//
|
|
// Deprecated: use Rotation instead.
|
|
func (d *Device) GetRotation() drivers.Rotation {
|
|
return d.rotation
|
|
}
|
|
|
|
// SetRotation changes the rotation of the device (clock-wise).
|
|
func (d *Device) SetRotation(rotation drivers.Rotation) error {
|
|
madctl := uint8(0)
|
|
switch rotation % 8 {
|
|
case Rotation0:
|
|
madctl = MADCTL_MX | MADCTL_BGR
|
|
case Rotation90:
|
|
madctl = MADCTL_MV | MADCTL_BGR
|
|
case Rotation180:
|
|
madctl = MADCTL_MY | MADCTL_BGR | MADCTL_ML
|
|
case Rotation270:
|
|
madctl = MADCTL_MX | MADCTL_MY | MADCTL_MV | MADCTL_BGR | MADCTL_ML
|
|
case Rotation0Mirror:
|
|
madctl = MADCTL_BGR
|
|
case Rotation90Mirror:
|
|
madctl = MADCTL_MY | MADCTL_MV | MADCTL_BGR | MADCTL_ML
|
|
case Rotation180Mirror:
|
|
madctl = MADCTL_MX | MADCTL_MY | MADCTL_BGR | MADCTL_ML
|
|
case Rotation270Mirror:
|
|
madctl = MADCTL_MX | MADCTL_MY | MADCTL_MV | MADCTL_BGR | MADCTL_ML
|
|
}
|
|
cmdBuf[0] = madctl
|
|
d.sendCommand(MADCTL, cmdBuf[:1])
|
|
d.rotation = rotation
|
|
return nil
|
|
}
|
|
|
|
// SetScrollArea sets an area to scroll with fixed top/bottom or left/right parts of the display
|
|
// Rotation affects scroll direction
|
|
func (d *Device) 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.
|
|
bottomFixedArea += 320 - d.height
|
|
}
|
|
cmdBuf[0] = uint8(topFixedArea >> 8)
|
|
cmdBuf[1] = uint8(topFixedArea)
|
|
cmdBuf[2] = uint8((320 - topFixedArea - bottomFixedArea) >> 8)
|
|
cmdBuf[3] = uint8(320 - topFixedArea - bottomFixedArea)
|
|
cmdBuf[4] = uint8(bottomFixedArea >> 8)
|
|
cmdBuf[5] = uint8(bottomFixedArea)
|
|
d.sendCommand(VSCRDEF, cmdBuf[:6])
|
|
}
|
|
|
|
// SetScroll sets the vertical scroll address of the display.
|
|
func (d *Device) SetScroll(line int16) {
|
|
cmdBuf[0] = uint8(line >> 8)
|
|
cmdBuf[1] = uint8(line)
|
|
d.sendCommand(VSCRSADD, cmdBuf[:2])
|
|
}
|
|
|
|
// StopScroll returns the display to its normal state
|
|
func (d *Device) StopScroll() {
|
|
d.sendCommand(NORON, nil)
|
|
}
|
|
|
|
// 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
|
|
x1 := x + w - 1
|
|
if x != d.x0 || x1 != d.x1 {
|
|
cmdBuf[0] = uint8(x >> 8)
|
|
cmdBuf[1] = uint8(x)
|
|
cmdBuf[2] = uint8(x1 >> 8)
|
|
cmdBuf[3] = uint8(x1)
|
|
d.sendCommand(CASET, cmdBuf[:4])
|
|
d.x0, d.x1 = x, x1
|
|
}
|
|
y1 := y + h - 1
|
|
if y != d.y0 || y1 != d.y1 {
|
|
cmdBuf[0] = uint8(y >> 8)
|
|
cmdBuf[1] = uint8(y)
|
|
cmdBuf[2] = uint8(y1 >> 8)
|
|
cmdBuf[3] = uint8(y1)
|
|
d.sendCommand(PASET, cmdBuf[:4])
|
|
d.y0, d.y1 = y, y1
|
|
}
|
|
d.sendCommand(RAMWR, nil)
|
|
}
|
|
|
|
//go:inline
|
|
func (d *Device) startWrite() {
|
|
if d.cs != machine.NoPin {
|
|
d.cs.Low()
|
|
}
|
|
}
|
|
|
|
//go:inline
|
|
func (d *Device) endWrite() {
|
|
if d.cs != machine.NoPin {
|
|
d.cs.High()
|
|
}
|
|
}
|
|
|
|
func (d *Device) sendCommand(cmd byte, data []byte) {
|
|
d.startWrite()
|
|
d.dc.Low()
|
|
d.driver.write8(cmd)
|
|
d.dc.High()
|
|
if data != nil {
|
|
d.driver.write8sl(data)
|
|
}
|
|
d.endWrite()
|
|
}
|
|
|
|
type driver interface {
|
|
configure(config *Config)
|
|
write8(b byte)
|
|
write8n(b byte, n int)
|
|
write8sl(b []byte)
|
|
write16(data uint16)
|
|
write16n(data uint16, n int)
|
|
write16sl(data []uint16)
|
|
}
|
|
|
|
func delay(m int) {
|
|
t := time.Now().UnixNano() + int64(time.Duration(m*1000)*time.Microsecond)
|
|
for time.Now().UnixNano() < t {
|
|
}
|
|
}
|
|
|
|
// 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))
|
|
}
|