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