tinygo.org/x/drivers@v0.27.1-0.20240509133757-7dbca2a54349/st7789/st7789.go (about) 1 // Package st7789 implements a driver for the ST7789 TFT displays, it comes in various screen sizes. 2 // 3 // Datasheets: https://cdn-shop.adafruit.com/product-files/3787/3787_tft_QT154H2201__________20190228182902.pdf 4 // 5 // http://www.newhavendisplay.com/appnotes/datasheets/LCDs/ST7789V.pdf 6 package st7789 // import "tinygo.org/x/drivers/st7789" 7 8 import ( 9 "image/color" 10 "machine" 11 "math" 12 "time" 13 14 "errors" 15 16 "tinygo.org/x/drivers" 17 "tinygo.org/x/drivers/pixel" 18 ) 19 20 // Rotation controls the rotation used by the display. 21 // 22 // Deprecated: use drivers.Rotation instead. 23 type Rotation = drivers.Rotation 24 25 // The color format used on the display, like RGB565, RGB666, and RGB444. 26 type ColorFormat uint8 27 28 // Pixel formats supported by the st7789 driver. 29 type Color interface { 30 pixel.RGB444BE | pixel.RGB565BE 31 32 pixel.BaseColor 33 } 34 35 // FrameRate controls the frame rate used by the display. 36 type FrameRate uint8 37 38 var ( 39 errOutOfBounds = errors.New("rectangle coordinates outside display area") 40 ) 41 42 // Device wraps an SPI connection. 43 type Device = DeviceOf[pixel.RGB565BE] 44 45 // DeviceOf is a generic version of Device. It supports multiple different pixel 46 // formats. 47 type DeviceOf[T Color] struct { 48 bus drivers.SPI 49 dcPin machine.Pin 50 resetPin machine.Pin 51 csPin machine.Pin 52 blPin machine.Pin 53 width int16 54 height int16 55 columnOffsetCfg int16 56 rowOffsetCfg int16 57 columnOffset int16 58 rowOffset int16 59 rotation drivers.Rotation 60 frameRate FrameRate 61 batchLength int32 62 batchData pixel.Image[T] // "image" with (width, height) of (batchLength, 1) 63 isBGR bool 64 vSyncLines int16 65 cmdBuf [1]byte 66 buf [6]byte 67 } 68 69 // Config is the configuration for the display 70 type Config struct { 71 Width int16 72 Height int16 73 Rotation drivers.Rotation 74 RowOffset int16 75 ColumnOffset int16 76 FrameRate FrameRate 77 VSyncLines int16 78 79 // Gamma control. Look in the LCD panel datasheet or provided example code 80 // to find these values. If not set, the defaults will be used. 81 PVGAMCTRL []uint8 // Positive voltage gamma control (14 bytes) 82 NVGAMCTRL []uint8 // Negative voltage gamma control (14 bytes) 83 } 84 85 // New creates a new ST7789 connection. The SPI wire must already be configured. 86 func New(bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) Device { 87 return NewOf[pixel.RGB565BE](bus, resetPin, dcPin, csPin, blPin) 88 } 89 90 // NewOf creates a new ST7789 connection with a particular pixel format. The SPI 91 // wire must already be configured. 92 func NewOf[T Color](bus drivers.SPI, resetPin, dcPin, csPin, blPin machine.Pin) DeviceOf[T] { 93 dcPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) 94 resetPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) 95 csPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) 96 blPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) 97 return DeviceOf[T]{ 98 bus: bus, 99 dcPin: dcPin, 100 resetPin: resetPin, 101 csPin: csPin, 102 blPin: blPin, 103 } 104 } 105 106 // Configure initializes the display with default configuration 107 func (d *DeviceOf[T]) Configure(cfg Config) { 108 if cfg.Width != 0 { 109 d.width = cfg.Width 110 } else { 111 d.width = 240 112 } 113 if cfg.Height != 0 { 114 d.height = cfg.Height 115 } else { 116 d.height = 240 117 } 118 119 d.rotation = cfg.Rotation 120 d.rowOffsetCfg = cfg.RowOffset 121 d.columnOffsetCfg = cfg.ColumnOffset 122 123 if cfg.FrameRate != 0 { 124 d.frameRate = cfg.FrameRate 125 } else { 126 d.frameRate = FRAMERATE_60 127 } 128 129 if cfg.VSyncLines >= 2 && cfg.VSyncLines <= 254 { 130 d.vSyncLines = cfg.VSyncLines 131 } else { 132 d.vSyncLines = 16 133 } 134 135 d.batchLength = int32(d.width) 136 if d.height > d.width { 137 d.batchLength = int32(d.height) 138 } 139 d.batchLength += d.batchLength & 1 140 141 // Reset the device 142 d.resetPin.High() 143 time.Sleep(50 * time.Millisecond) 144 d.resetPin.Low() 145 time.Sleep(50 * time.Millisecond) 146 d.resetPin.High() 147 time.Sleep(50 * time.Millisecond) 148 149 // Common initialization 150 d.startWrite() 151 d.sendCommand(SWRESET, nil) // Soft reset 152 d.endWrite() 153 time.Sleep(150 * time.Millisecond) // 154 d.startWrite() 155 156 d.sendCommand(SLPOUT, nil) // Exit sleep mode 157 158 // Memory initialization 159 var zeroColor T 160 switch any(zeroColor).(type) { 161 case pixel.RGB444BE: 162 d.setColorFormat(ColorRGB444) // 12 bits per pixel 163 default: 164 // Use default RGB565 color format. 165 d.setColorFormat(ColorRGB565) // 16 bits per pixel 166 } 167 time.Sleep(10 * time.Millisecond) 168 169 d.setRotation(d.rotation) // Memory orientation 170 171 d.setWindow(0, 0, d.width, d.height) // Full draw window 172 d.fillScreen(color.RGBA{0, 0, 0, 255}) // Clear screen 173 174 // Framerate 175 d.sendCommand(FRCTRL2, []byte{byte(d.frameRate)}) // Frame rate for normal mode (default 60Hz) 176 177 // Frame vertical sync and "porch" 178 // 179 // Front and back porch controls vertical scanline sync time before and after 180 // a frame, where memory can be safely written without tearing. 181 // 182 fp := uint8(d.vSyncLines / 2) // Split the desired pause half and half 183 bp := uint8(d.vSyncLines - int16(fp)) // between front and back porch. 184 185 d.sendCommand(PORCTRL, []byte{ 186 bp, // Back porch 5bit (0x7F max 0x08 default) 187 fp, // Front porch 5bit (0x7F max 0x08 default) 188 0x00, // Seprarate porch (TODO: what is this?) 189 0x22, // Idle mode porch (4bit-back 4bit-front 0x22 default) 190 0x22, // Partial mode porch (4bit-back 4bit-front 0x22 default) 191 }) 192 193 // Ready to display 194 d.sendCommand(INVON, nil) // Inversion ON 195 time.Sleep(10 * time.Millisecond) // 196 197 // Set gamma tables, if configured. 198 if len(cfg.PVGAMCTRL) == 14 { 199 d.sendCommand(GMCTRP1, cfg.PVGAMCTRL) // PVGAMCTRL: Positive Voltage Gamma Control 200 } 201 if len(cfg.NVGAMCTRL) == 14 { 202 d.sendCommand(GMCTRN1, cfg.NVGAMCTRL) // NVGAMCTRL: Negative Voltage Gamma Control 203 } 204 205 d.sendCommand(NORON, nil) // Normal mode ON 206 time.Sleep(10 * time.Millisecond) // 207 208 d.sendCommand(DISPON, nil) // Screen ON 209 time.Sleep(10 * time.Millisecond) // 210 211 d.endWrite() 212 d.blPin.High() // Backlight ON 213 } 214 215 // Send a command with data to the display. It does not change the chip select 216 // pin (it must be low when calling). The DC pin is left high after return, 217 // meaning that data can be sent right away. 218 func (d *DeviceOf[T]) sendCommand(command uint8, data []byte) error { 219 d.cmdBuf[0] = command 220 d.dcPin.Low() 221 err := d.bus.Tx(d.cmdBuf[:1], nil) 222 d.dcPin.High() 223 if len(data) != 0 { 224 err = d.bus.Tx(data, nil) 225 } 226 return err 227 } 228 229 // startWrite must be called at the beginning of all exported methods to set the 230 // chip select pin low. 231 func (d *DeviceOf[T]) startWrite() { 232 if d.csPin != machine.NoPin { 233 d.csPin.Low() 234 } 235 } 236 237 // endWrite must be called at the end of all exported methods to set the chip 238 // select pin high. 239 func (d *DeviceOf[T]) endWrite() { 240 if d.csPin != machine.NoPin { 241 d.csPin.High() 242 } 243 } 244 245 // getBuffer returns the image buffer, that's always d.batchLength wide and 1 246 // pixel high. It can be used as a temporary buffer to transmit image data. 247 func (d *DeviceOf[T]) getBuffer() pixel.Image[T] { 248 if d.batchData.Len() == 0 { 249 d.batchData = pixel.NewImage[T](int(d.batchLength), 1) 250 } 251 return d.batchData 252 } 253 254 // Sync waits for the display to hit the next VSYNC pause 255 func (d *DeviceOf[T]) Sync() { 256 d.SyncToScanLine(0) 257 } 258 259 // SyncToScanLine waits for the display to hit a specific scanline 260 // 261 // A scanline value of 0 will forward to the beginning of the next VSYNC, 262 // even if the display is currently in a VSYNC pause. 263 // 264 // Syncline values appear to increment once for every two vertical 265 // lines on the display. 266 // 267 // NOTE: Use GetHighestScanLine and GetLowestScanLine to obtain the highest 268 // and lowest useful values. Values are affected by front and back porch 269 // vsync settings (derived from VSyncLines configuration option). 270 func (d *DeviceOf[T]) SyncToScanLine(scanline uint16) { 271 scan := d.GetScanLine() 272 273 // Sometimes GetScanLine returns erroneous 0 on first call after draw, so double check 274 if scan == 0 { 275 scan = d.GetScanLine() 276 } 277 278 if scanline == 0 { 279 // we dont know where we are in an ongoing vsync so go around 280 for scan < 1 { 281 time.Sleep(1 * time.Millisecond) 282 scan = d.GetScanLine() 283 } 284 for scan > 0 { 285 scan = d.GetScanLine() 286 } 287 } else { 288 // go around unless we're very close to the target 289 for scan > scanline+4 { 290 time.Sleep(1 * time.Millisecond) 291 scan = d.GetScanLine() 292 } 293 for scan < scanline { 294 scan = d.GetScanLine() 295 } 296 } 297 } 298 299 // GetScanLine reads the current scanline value from the display 300 func (d *DeviceOf[T]) GetScanLine() uint16 { 301 d.startWrite() 302 data := []uint8{0x00, 0x00} 303 d.dcPin.Low() 304 d.bus.Transfer(GSCAN) 305 d.dcPin.High() 306 for i := range data { 307 data[i], _ = d.bus.Transfer(0xFF) 308 } 309 scanline := uint16(data[0])<<8 + uint16(data[1]) 310 d.endWrite() 311 return scanline 312 } 313 314 // GetHighestScanLine calculates the last scanline id in the frame before VSYNC pause 315 func (d *DeviceOf[T]) GetHighestScanLine() uint16 { 316 // Last scanline id appears to be backporch/2 + 320/2 317 return uint16(math.Ceil(float64(d.vSyncLines)/2)/2) + 160 318 } 319 320 // GetLowestScanLine calculate the first scanline id to appear after VSYNC pause 321 func (d *DeviceOf[T]) GetLowestScanLine() uint16 { 322 // First scanline id appears to be backporch/2 + 1 323 return uint16(math.Ceil(float64(d.vSyncLines)/2)/2) + 1 324 } 325 326 // Display does nothing, there's no buffer as it might be too big for some boards 327 func (d *DeviceOf[T]) Display() error { 328 return nil 329 } 330 331 // SetPixel sets a pixel in the screen 332 func (d *DeviceOf[T]) SetPixel(x int16, y int16, c color.RGBA) { 333 if x < 0 || y < 0 || 334 (((d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180) && (x >= d.width || y >= d.height)) || 335 ((d.rotation == drivers.Rotation90 || d.rotation == drivers.Rotation270) && (x >= d.height || y >= d.width))) { 336 return 337 } 338 d.FillRectangle(x, y, 1, 1, c) 339 } 340 341 // setWindow prepares the screen to be modified at a given rectangle 342 func (d *DeviceOf[T]) setWindow(x, y, w, h int16) { 343 x += d.columnOffset 344 y += d.rowOffset 345 copy(d.buf[:4], []uint8{uint8(x >> 8), uint8(x), uint8((x + w - 1) >> 8), uint8(x + w - 1)}) 346 d.sendCommand(CASET, d.buf[:4]) 347 copy(d.buf[:4], []uint8{uint8(y >> 8), uint8(y), uint8((y + h - 1) >> 8), uint8(y + h - 1)}) 348 d.sendCommand(RASET, d.buf[:4]) 349 d.sendCommand(RAMWR, nil) 350 } 351 352 // FillRectangle fills a rectangle at a given coordinates with a color 353 func (d *DeviceOf[T]) FillRectangle(x, y, width, height int16, c color.RGBA) error { 354 d.startWrite() 355 err := d.fillRectangle(x, y, width, height, c) 356 d.endWrite() 357 return err 358 } 359 360 func (d *DeviceOf[T]) fillRectangle(x, y, width, height int16, c color.RGBA) error { 361 k, i := d.Size() 362 if x < 0 || y < 0 || width <= 0 || height <= 0 || 363 x >= k || (x+width) > k || y >= i || (y+height) > i { 364 return errors.New("rectangle coordinates outside display area") 365 } 366 d.setWindow(x, y, width, height) 367 368 image := d.getBuffer() 369 image.FillSolidColor(pixel.NewColor[T](c.R, c.G, c.B)) 370 j := int(width) * int(height) 371 for j > 0 { 372 // The DC pin is already set to data in the setWindow call, so we can 373 // just write bytes on the SPI bus. 374 if j >= image.Len() { 375 d.bus.Tx(image.RawBuffer(), nil) 376 } else { 377 d.bus.Tx(image.Rescale(j, 1).RawBuffer(), nil) 378 } 379 j -= image.Len() 380 } 381 return nil 382 } 383 384 // DrawRGBBitmap8 copies an RGB bitmap to the internal buffer at given coordinates 385 // 386 // Deprecated: use DrawBitmap instead. 387 func (d *DeviceOf[T]) DrawRGBBitmap8(x, y int16, data []uint8, w, h int16) error { 388 k, i := d.Size() 389 if x < 0 || y < 0 || w <= 0 || h <= 0 || 390 x >= k || (x+w) > k || y >= i || (y+h) > i { 391 return errOutOfBounds 392 } 393 d.startWrite() 394 d.setWindow(x, y, w, h) 395 d.bus.Tx(data, nil) 396 d.endWrite() 397 return nil 398 } 399 400 // DrawBitmap copies the bitmap to the internal buffer on the screen at the 401 // given coordinates. It returns once the image data has been sent completely. 402 func (d *DeviceOf[T]) DrawBitmap(x, y int16, bitmap pixel.Image[T]) error { 403 width, height := bitmap.Size() 404 return d.DrawRGBBitmap8(x, y, bitmap.RawBuffer(), int16(width), int16(height)) 405 } 406 407 // FillRectangleWithBuffer fills buffer with a rectangle at a given coordinates. 408 func (d *DeviceOf[T]) FillRectangleWithBuffer(x, y, width, height int16, buffer []color.RGBA) error { 409 i, j := d.Size() 410 if x < 0 || y < 0 || width <= 0 || height <= 0 || 411 x >= i || (x+width) > i || y >= j || (y+height) > j { 412 return errors.New("rectangle coordinates outside display area") 413 } 414 if int32(width)*int32(height) != int32(len(buffer)) { 415 return errors.New("buffer length does not match with rectangle size") 416 } 417 d.startWrite() 418 d.setWindow(x, y, width, height) 419 420 k := int(width) * int(height) 421 image := d.getBuffer() 422 offset := 0 423 for k > 0 { 424 for i := 0; i < image.Len(); i++ { 425 if offset+i < len(buffer) { 426 c := buffer[offset+i] 427 image.Set(i, 0, pixel.NewColor[T](c.R, c.G, c.B)) 428 } 429 } 430 // The DC pin is already set to data in the setWindow call, so we don't 431 // have to set it here. 432 if k >= image.Len() { 433 d.bus.Tx(image.RawBuffer(), nil) 434 } else { 435 d.bus.Tx(image.Rescale(k, 1).RawBuffer(), nil) 436 } 437 k -= image.Len() 438 offset += image.Len() 439 } 440 d.endWrite() 441 return nil 442 } 443 444 // DrawFastVLine draws a vertical line faster than using SetPixel 445 func (d *DeviceOf[T]) DrawFastVLine(x, y0, y1 int16, c color.RGBA) { 446 if y0 > y1 { 447 y0, y1 = y1, y0 448 } 449 d.FillRectangle(x, y0, 1, y1-y0+1, c) 450 } 451 452 // DrawFastHLine draws a horizontal line faster than using SetPixel 453 func (d *DeviceOf[T]) DrawFastHLine(x0, x1, y int16, c color.RGBA) { 454 if x0 > x1 { 455 x0, x1 = x1, x0 456 } 457 d.FillRectangle(x0, y, x1-x0+1, 1, c) 458 } 459 460 // FillScreen fills the screen with a given color 461 func (d *DeviceOf[T]) FillScreen(c color.RGBA) { 462 d.startWrite() 463 d.fillScreen(c) 464 d.endWrite() 465 } 466 467 func (d *DeviceOf[T]) fillScreen(c color.RGBA) { 468 if d.rotation == NO_ROTATION || d.rotation == ROTATION_180 { 469 d.fillRectangle(0, 0, d.width, d.height, c) 470 } else { 471 d.fillRectangle(0, 0, d.height, d.width, c) 472 } 473 } 474 475 // Control the color format that is used when writing to the screen. 476 // The default is RGB565, setting it to any other value will break functions 477 // like SetPixel, FillRectangle, etc. Instead, you can write color data in the 478 // specified color format using DrawRGBBitmap8. 479 func (d *DeviceOf[T]) SetColorFormat(format ColorFormat) { 480 d.startWrite() 481 d.setColorFormat(format) 482 d.endWrite() 483 } 484 485 func (d *DeviceOf[T]) setColorFormat(format ColorFormat) { 486 // Lower 4 bits set the color format used in SPI. 487 // Upper 4 bits set the color format used in the direct RGB interface. 488 // The RGB interface is not currently supported, so it is left at a 489 // reasonable default. Also, the RGB interface doesn't support RGB444. 490 colmod := byte(format) | 0x50 491 d.sendCommand(COLMOD, []byte{colmod}) 492 } 493 494 // Rotation returns the current rotation of the device. 495 func (d *DeviceOf[T]) Rotation() drivers.Rotation { 496 return d.rotation 497 } 498 499 // SetRotation changes the rotation of the device (clock-wise) 500 func (d *DeviceOf[T]) SetRotation(rotation Rotation) error { 501 d.rotation = rotation 502 d.startWrite() 503 err := d.setRotation(rotation) 504 d.endWrite() 505 return err 506 } 507 508 func (d *DeviceOf[T]) setRotation(rotation Rotation) error { 509 madctl := uint8(0) 510 switch rotation % 4 { 511 case drivers.Rotation0: 512 d.rowOffset = 0 513 d.columnOffset = 0 514 case drivers.Rotation90: 515 madctl = MADCTL_MX | MADCTL_MV 516 d.rowOffset = 0 517 d.columnOffset = 0 518 case drivers.Rotation180: 519 madctl = MADCTL_MX | MADCTL_MY 520 d.rowOffset = d.rowOffsetCfg 521 d.columnOffset = d.columnOffsetCfg 522 case drivers.Rotation270: 523 madctl = MADCTL_MY | MADCTL_MV 524 d.rowOffset = d.columnOffsetCfg 525 d.columnOffset = d.rowOffsetCfg 526 } 527 if d.isBGR { 528 madctl |= MADCTL_BGR 529 } 530 return d.sendCommand(MADCTL, []byte{madctl}) 531 } 532 533 // Size returns the current size of the display. 534 func (d *DeviceOf[T]) Size() (w, h int16) { 535 if d.rotation == drivers.Rotation0 || d.rotation == drivers.Rotation180 { 536 return d.width, d.height 537 } 538 return d.height, d.width 539 } 540 541 // EnableBacklight enables or disables the backlight 542 func (d *DeviceOf[T]) EnableBacklight(enable bool) { 543 if enable { 544 d.blPin.High() 545 } else { 546 d.blPin.Low() 547 } 548 } 549 550 // Set the sleep mode for this LCD panel. When sleeping, the panel uses a lot 551 // less power. The LCD won't display an image anymore, but the memory contents 552 // will be kept. 553 func (d *DeviceOf[T]) Sleep(sleepEnabled bool) error { 554 if sleepEnabled { 555 d.startWrite() 556 d.sendCommand(SLPIN, nil) 557 d.endWrite() 558 time.Sleep(5 * time.Millisecond) // 5ms required by the datasheet 559 } else { 560 // Turn the LCD panel back on. 561 d.startWrite() 562 d.sendCommand(SLPOUT, nil) 563 d.endWrite() 564 // Note: the st7789 documentation says that it is needed to wait at 565 // least 120ms before going to sleep again. Sleeping here would not be 566 // practical (delays turning on the screen too much), so just hope the 567 // screen won't need to sleep again for at least 120ms. 568 // In practice, it's unlikely the user will set the display to sleep 569 // again within 120ms. 570 } 571 return nil 572 } 573 574 // InvertColors inverts the colors of the screen 575 func (d *DeviceOf[T]) InvertColors(invert bool) { 576 d.startWrite() 577 if invert { 578 d.sendCommand(INVON, nil) 579 } else { 580 d.sendCommand(INVOFF, nil) 581 } 582 d.endWrite() 583 } 584 585 // IsBGR changes the color mode (RGB/BGR) 586 func (d *DeviceOf[T]) IsBGR(bgr bool) { 587 d.isBGR = bgr 588 } 589 590 // SetScrollArea sets an area to scroll with fixed top and bottom parts of the display. 591 func (d *DeviceOf[T]) SetScrollArea(topFixedArea, bottomFixedArea int16) { 592 if d.height < 320 { 593 // The screen doesn't use the full 320 pixel height. 594 // Enlarge the bottom fixed area to fill the 320 pixel height, so that 595 // bottomFixedArea starts from the visible bottom of the screen. 596 topFixedArea += d.rowOffset 597 bottomFixedArea += (320 - d.height) - d.rowOffset 598 } 599 if d.rotation == drivers.Rotation180 { 600 // The screen is rotated by 180°, so we have to switch the top and 601 // bottom fixed area. 602 topFixedArea, bottomFixedArea = bottomFixedArea, topFixedArea 603 } 604 verticalScrollArea := 320 - topFixedArea - bottomFixedArea 605 copy(d.buf[:6], []uint8{ 606 uint8(topFixedArea >> 8), uint8(topFixedArea), 607 uint8(verticalScrollArea >> 8), uint8(verticalScrollArea), 608 uint8(bottomFixedArea >> 8), uint8(bottomFixedArea)}) 609 d.startWrite() 610 d.sendCommand(VSCRDEF, d.buf[:6]) 611 d.endWrite() 612 } 613 614 // SetScroll sets the vertical scroll address of the display. 615 func (d *DeviceOf[T]) SetScroll(line int16) { 616 if d.rotation == drivers.Rotation180 { 617 // The screen is rotated by 180°, so we have to invert the scroll line 618 // (taking care of the RowOffset). 619 line = (319 - d.rowOffset) - line 620 } 621 d.buf[0] = uint8(line >> 8) 622 d.buf[1] = uint8(line) 623 d.startWrite() 624 d.sendCommand(VSCRSADD, d.buf[:2]) 625 d.endWrite() 626 } 627 628 // StopScroll returns the display to its normal state. 629 func (d *DeviceOf[T]) StopScroll() { 630 d.startWrite() 631 d.sendCommand(NORON, nil) 632 d.endWrite() 633 }