mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-07-26 10:38:41 +00:00
af33129f41
This adds a new DrawBitmap method, which is meant to replace DrawRGBBitmap8.
438 lines
11 KiB
Go
438 lines
11 KiB
Go
package ili9341
|
|
|
|
import (
|
|
"errors"
|
|
"image/color"
|
|
"machine"
|
|
"time"
|
|
|
|
"tinygo.org/x/drivers"
|
|
"tinygo.org/x/drivers/pixel"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
// Image buffer type used in the ili9341.
|
|
type Image = pixel.Image[pixel.RGB565BE]
|
|
|
|
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
|
|
//
|
|
// Deprecated: use DrawBitmap instead.
|
|
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
|
|
//
|
|
// Deprecated: use DrawBitmap instead.
|
|
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
|
|
}
|
|
|
|
// 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 *Device) DrawBitmap(x, y int16, bitmap Image) error {
|
|
width, height := bitmap.Size()
|
|
return d.DrawRGBBitmap8(x, y, bitmap.RawBuffer(), int16(width), int16(height))
|
|
}
|
|
|
|
// 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))
|
|
}
|