Files
drivers/ili9341/ili9341.go
T
Ayke van Laethem af33129f41 ili9341: st7735: st7789: add DrawBitmap method
This adds a new DrawBitmap method, which is meant to replace
DrawRGBBitmap8.
2023-11-23 17:27:00 +01:00

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))
}