TFT_eSPI: Support for Raspberry Pi Pico added

I have updated the TFT_eSPI master library (only available from Github at the moment).

The library has been tested with the Raspberry Pi Pico connected to an ILI9341 4 wire SPI 240x320 pixel display. Other displays supported by TFT_eSPI should also work but have not been tested.

The "Unoficial" Arduino core package from Earle Philhower has been used for this development. However as low level SDK functions are called it should also work with the official Arduino package when it is released. If it does not then I will update!

The examples are working and performance is quite good.

For those familiar with the "Graphicstest" example these are the results:

Benchmark,                Time (microseconds)
Screen fill (5 times),    114981  (43.49 fps)
Text,                     31986
Lines,                    128851
Horiz/Vert Lines,         18031
Rectangles (outline),     10210
Rectangles (filled),      241497
Circles (filled),         70702
Circles (outline),        58318
Triangles (outline),      33224
Triangles (filled),       109793
Rounded rects (outline),  30585
Rounded rects (filled),   255167
Total = 1103345us
Total = 1.1033s

The Jpeg decode is slower than processors that have a dedicated FPU, but it is still acceptable.

Sprites work very well and the large RAM means full frame buffers are possible for the smaller displays (e.g. 240x320).

1 Like

The TFT_eSPI library now supports the "official" Arduino IDE RP2040 board package and I have added DMA capability for the Pico/RP2040 when using SPI displays.

The ILI9341 display operates reliably at 62.5MHz so frame rates up to ~43fps is possible with DMA:

The movement of the circles is smooth and better than the video indicates!

The SPI transmit code for the TFT_eSPI library has been optimised further to make better use if the transmit FIFO. This has boosted performance slightly. As an example the graphics test now takes ~0.8s.

Pico 62.5MHz SPI rate, ILI9341, with Arduino board package
Benchmark,                Time (microseconds)
Screen fill (5 times),    107574  (46.48 fps)
Text,                     11829
Lines,                    60568
Horiz/Vert Lines,         8991
Rectangles (outline),     5843
Rectangles (filled),      223526
Circles (filled),         31207
Circles (outline),        24176
Triangles (outline),      15048
Triangles (filled),       78680
Rounded rects (outline),  11956
Rounded rects (filled),   225319
Total = 804717us
Total = 0.8047s

A comparable test with Adafruit_GFX gives poor results (~10 secs for graphicstest) due to lack of optimisation, but I have submitted a pull request update for that library to boost the performance to ~1.2 s.

1 Like

@Bodmer,

Do you have a link to the video source code?
It is very impressive. I would like to run it on my screen.

David.

1 Like

The source code will be a library example but it has not been published yet. Here it is though with minor tweaks to the colour choices:

// This sketch is for the RP2040 and ILI9341 TFT display.
// The RP2040 has sufficient RAM for a full screen buffer:
// 240 x 320 x 2 = 153,600 bytes
// In this example two sprites are used to create DMA toggle
// buffers. Each sprite is half the screen size, this allows
// graphics to be rendered in one sprite at the same time
// as the other sprite is being sent to the screen.

// Created by Bodmer 20/04/2021 as an example for:
// https://github.com/Bodmer/TFT_eSPI

// Number of circles to draw
#define CNUMBER 42

#include <TFT_eSPI.h>

// Library instance
TFT_eSPI    tft = TFT_eSPI();

// Create two sprites for a DMA toggle buffer
TFT_eSprite spr[2] = {TFT_eSprite(&tft), TFT_eSprite(&tft)};

// Pointers to start of Sprites in RAM
uint16_t* sprPtr[2];

// Used for fps measuring
uint16_t counter = 0;
int32_t startMillis = millis();
uint16_t interval = 100;
String fps = "xx.xx fps";

// Structure to hold circle plotting parameters
typedef struct circle_t {
  int16_t   cx[CNUMBER] = { 0 };
  int16_t   cy[CNUMBER] = { 0 };
  int16_t   cr[CNUMBER] = { 0 };
  uint16_t col[CNUMBER] = { 0 };
  int16_t   dx[CNUMBER] = { 0 };
  int16_t   dy[CNUMBER] = { 0 };
} circle_param;

// Create the structure and get a pointer to it
circle_t *circle = new circle_param;

// #########################################################################
// Setup
// #########################################################################
void setup() {
  Serial.begin(115200);

  tft.init();

  tft.initDMA();

  tft.fillScreen(TFT_BLACK);

  // Create the 2 sprites, each is half the size of the screen
  sprPtr[0] = (uint16_t*)spr[0].createSprite(tft.width(), tft.height() / 2);
  sprPtr[1] = (uint16_t*)spr[1].createSprite(tft.width(), tft.height() / 2);

  // Define text datum for each Sprite
  spr[0].setTextDatum(MC_DATUM);
  spr[1].setTextDatum(MC_DATUM);

  // Initialise circle parameters
  for (uint16_t i = 0; i < CNUMBER; i++) {
    circle->cr[i] = random(12, 24);
    circle->cx[i] = random(circle->cr[i], tft.width() - circle->cr[i]);
    circle->cy[i] = random(circle->cr[i], tft.height() - circle->cr[i]);
    circle->col[i] = rainbow(4 * i);
    circle->dx[i] = random(1, 5);
    if (random(2)) circle->dx[i] = -circle->dx[i];
    circle->dy[i] = random(1, 5);
    if (random(2)) circle->dx[i] = -circle->dx[i];
  }

  tft.startWrite(); // TFT chip select held low permanently

  startMillis = millis();
}

// #########################################################################
// Loop
// #########################################################################
void loop() {
  drawUpdate(0); // Update top half
  drawUpdate(1); // Update bottom half

  //delay(1000);
  counter++;
  // only calculate the fps every <interval> iterations.
  if (counter % interval == 0) {
    long millisSinceUpdate = millis() - startMillis;
    fps = String((interval * 1000.0 / (millisSinceUpdate))) + " fps";
    Serial.println(fps);
    startMillis = millis();
  }
}

// #########################################################################
// Render circles to sprite 0 or 1 and initiate DMA
// #########################################################################
void drawUpdate (bool sel) {
  spr[sel].fillSprite(TFT_BLACK);
  for (uint16_t i = 0; i < CNUMBER; i++) {
    int16_t dsy = circle->cy[i] - (sel * tft.height() / 2);
    // Draw
    spr[sel].fillCircle(circle->cx[i], dsy, circle->cr[i], circle->col[i]);
    spr[sel].drawCircle(circle->cx[i], dsy, circle->cr[i], TFT_WHITE);
    spr[sel].setTextColor(TFT_BLACK, circle->col[i]);
    spr[sel].drawNumber(i + 1, 1 + circle->cx[i], dsy, 2);

    // Update circle positions after bottom half has been drawn
    if (sel) {
      circle->cx[i] += circle->dx[i];
      circle->cy[i] += circle->dy[i];
      if (circle->cx[i] <= circle->cr[i]) {
        circle->cx[i] = circle->cr[i];
        circle->dx[i] = -circle->dx[i];
      }
      else if (circle->cx[i] + circle->cr[i] >= tft.width() - 1) {
        circle->cx[i] = tft.width() - circle->cr[i] - 1;
        circle->dx[i] = -circle->dx[i];
      }
      if (circle->cy[i] <= circle->cr[i]) {
        circle->cy[i] = circle->cr[i];
        circle->dy[i] = -circle->dy[i];
      }
      else if (circle->cy[i] + circle->cr[i] >= tft.height() - 1) {
        circle->cy[i] = tft.height() - circle->cr[i] - 1;
        circle->dy[i] = -circle->dy[i];
      }
    }
  }
  tft.pushImageDMA(0, sel * tft.height() / 2, tft.width(), tft.height() / 2, sprPtr[sel]);
}

// #########################################################################
// Return a 16 bit rainbow colour
// #########################################################################
uint16_t rainbow(byte value)
{
  // If 'value' is in the range 0-159 it is converted to a spectrum colour
  // from 0 = red through to 127 = blue to 159 = violet
  // Extending the range to 0-191 adds a further violet to red band

  value = value % 192;

  byte red   = 0; // Red is the top 5 bits of a 16 bit colour value
  byte green = 0; // Green is the middle 6 bits, but only top 5 bits used here
  byte blue  = 0; // Blue is the bottom 5 bits

  byte sector = value >> 5;
  byte amplit = value & 0x1F;

  switch (sector)
  {
    case 0:
      red   = 0x1F;
      green = amplit; // Green ramps up
      blue  = 0;
      break;
    case 1:
      red   = 0x1F - amplit; // Red ramps down
      green = 0x1F;
      blue  = 0;
      break;
    case 2:
      red   = 0;
      green = 0x1F;
      blue  = amplit; // Blue ramps up
      break;
    case 3:
      red   = 0;
      green = 0x1F - amplit; // Green ramps down
      blue  = 0x1F;
      break;
    case 4:
      red   = amplit; // Red ramps up
      green = 0;
      blue  = 0x1F;
      break;
    case 5:
      red   = 0x1F;
      green = 0;
      blue  = 0x1F - amplit; // Blue ramps down
      break;
  }

  return red << 11 | green << 6 | blue;
}

1 Like

Yes, it looks really good in real life too !!

There is a "copy and paste" typo, for the dy direction, the second:

    if (random(2)) circle->dx[i] = -circle->dx[i];

Should be dy:

    if (random(2)) circle->dy[i] = -circle->dy[i];

Also the pattern is always the same at reset due to the fixed random number sequence so best to seed the generator with something noisy like:

randomSeed(analogRead(A0));

The Raspberry Pi Pico has a two channel hardware interpolator. This is put to good effect in the sdk example here to rotate and render a bitmap image to a TFT. Unfortunately the SPI implementation is rather lame and this reduces the image performance to ~28fps for rotating the image.

By using the DMA capability this can be boosted to circa 62fps. Example attached.sdk_ST7789_dma.zip (71.8 KB)

doesnt compile.

TFT_miso and TFT_GOLD in processors cause cause compiler error. not sure if your using picoprobe or some other means to upload stuff but the ide says its spaghetti code. why is a color definition in the proccessors? idk but i doubt many will use this

Hello, I just ran into this example with an ESP32 and it's really impressive indeed, thanks!

Not strictly related to the topic, but could someone post me a link or tutorial for this kind of structure creation?

// Create the structure and get a pointer to it
circle_t *circle = new circle_param;

What is new and why circle_t and circle_param also used?

Could somebody help me out with this one? Thanks in advance.

Look through your header files. circle_t will have a class definition somewhere.
I would guess that the centre x, y location and the radius r would be essential parameters of circle_param class

No, I have not searched for myself.

new is just the regular operation to instantiate an object of a C++ class. Study your C++ textbook.

David.

Thanks for the guidance. I looked up the header files, but neither circle_t nor circle_param is defined/used/written elsewhere. I guess it was @bodmer's choice to write it this way (possible advanced reason beyond my understanding).

Anyway, it drove me to read more about typedef and struct definitions. :slight_smile: (I'm a beginner)

This also works:

// Structure to hold circle plotting parameters
typedef struct {
  int16_t   cx[CNUMBER] = { 0 }; // x coord of centre
  int16_t   cy[CNUMBER] = { 0 }; // y coord of centre
  int16_t   cr[CNUMBER] = { 0 }; // radius
  uint16_t col[CNUMBER] = { 0 }; // colour
  int16_t   dx[CNUMBER] = { 0 }; // x movment & direction
  int16_t   dy[CNUMBER] = { 0 }; // y movment & direction
} circle_t;

// Create the structure and get a pointer to it
circle_t *circle = new circle_t;

This topic was automatically closed after 82 days. New replies are no longer allowed.