How to drive a TFT faster

EL Wire/Tape/Panels, LEDs, pixels and strips, LCDs and TFTs, etc products from Adafruit

Moderators: adafruit_support_bill, adafruit

Please be positive and constructive with your questions and comments.
Locked
User avatar
GianLupo
 
Posts: 14
Joined: Thu Jun 17, 2021 9:52 am

How to drive a TFT faster

Post by GianLupo »

I bought myself a 240x320 pixel, 2.8" TFT display with a ILI9341 driver, link: https://www.adafruit.com/product/1770. I found this display to refresh a lot slower than I expected, e.g. relative to the PyGamer, and I would like to better understand what is going on, whether I'm doing something wrong, and where to look for ways to improve both my understanding and performances.

I followed the tutorial (https://learn.adafruit.com/adafruit-2-8 ... eakout-v2/, SPI version), using the Adafruit ILI9341 and GFX libraries, and ran the test sketch on Raspberry Pi Pico and Arduino Uno. I wrote simple test sketches (code below) to measure the refresh time, and found 2.2s on the Pico, over 3s on the Uno, and 13ms on the PyGamer. I also found that if I only specify the CS and DC pins in the Adafruit_ILI9341 initialiser, I would get about 300ms on the Uno.

First off, I would like to understand why specifying all pins in the Adafruit_ILI9341 initialiser makes such a huge difference. The SPI pins I used are the default ones for the Uno, so I assumed passing those values would be equivalent to letting the library use the defaults.

Second, I would like to understand why the PyGamer refreshes so much faster. Here are a few things I thought:
  1. Even if the PyGamer has a smaller screen (160x128), that should only account for a factor of 4. Am I wrong?
  2. I did not specify a frequency for the SPI clock. Could that make a tangible difference? What could be a good frequency for my two controllers? What does the library default to if I don't specify it manually?
  3. The PyGamer page says it has "fast DMA support for drawing so updates are incredibly fast". I would like to understand that better, particularly if that's what I'm missing. Unfortunately, a quick Google didn't give me a good reference on what DMA is and how to use it, so a good source for that would be great. Is DMA a physical feature, something that a particular device either has or doesn't have? Or is it a particular way to set up the SPI bus? Can I use DMA to run my TFT, and how much faster would it make it?
Test sketch for Pico (Uno was similar, with just the pin numbers changed to their defaults for that board):

Code: Select all

#include "SPI.h"
#include "Adafruit_GFX.h"
#include "Adafruit_ILI9341.h"

#define TFT_DC    20  // 9 for the Uno
#define TFT_CS    17  // 10
#define TFT_COPI  19  // 11
#define TFT_CLK   18  // 13
#define TFT_RST   22  // -1
#define TFT_CIPO  16  // 12

Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_COPI, TFT_CLK, TFT_RST, TFT_CIPO);
// Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC);  // This makes the refresh 10x faster on the Uno!
unsigned long last_refresh;

void setup() {
  tft.begin();
  Serial.begin(9600);
  Serial.println("ILI9341 Test");
}

void loop() {
  last_refresh = micros();
  tft.fillScreen(ILI9341_BLACK);
  Serial.println(micros() - last_refresh);
}
Test sketch for PyGamer:

Code: Select all

#include <Adafruit_Arcada.h>

Adafruit_Arcada arcada;
unsigned long last_refresh;

void setup() {
  Serial.begin(9600);
  arcada.arcadaBegin();
  arcada.displayBegin();
  arcada.setBacklight(250);
}

void loop() {
  last_refresh = micros();
  arcada.display->fillScreen(ARCADA_RED);
  Serial.println(micros() - last_refresh);
}

User avatar
blnkjns
 
Posts: 963
Joined: Fri Oct 02, 2020 3:33 am

Re: How to drive a TFT faster

Post by blnkjns »

Try changing Adafruit_ILI9341.cpp

Code: Select all

#if defined(ARDUINO_ARCH_ARC32) || defined(ARDUINO_MAXIM)
#define SPI_DEFAULT_FREQ 16000000
// Teensy 3.0, 3.1/3.2, 3.5, 3.6
#elif defined(__MK20DX128__) || defined(__MK20DX256__) ||                      \
    defined(__MK64FX512__) || defined(__MK66FX1M0__)
#define SPI_DEFAULT_FREQ 40000000
#elif defined(__AVR__) || defined(TEENSYDUINO)
#define SPI_DEFAULT_FREQ 8000000 // Tweak this number for AVR chips.
#elif defined(ESP8266) || defined(ESP32)
#define SPI_DEFAULT_FREQ 40000000
#elif defined(RASPI)
#define SPI_DEFAULT_FREQ 80000000
#elif defined(ARDUINO_ARCH_STM32F1)
#define SPI_DEFAULT_FREQ 36000000
#else
#define SPI_DEFAULT_FREQ 24000000 ///< Default SPI data clock frequency
#endif
The problem remains the UNO is a rather low clocked chip and I believe 8 000 000 is the max (clock speed/2). You will run into other issues as well, most problematic is the RAM not able to hold a full screen buffer. Having a RAM buffer means you can create the image fast in RAM, and then dump it as single frame streat to the display. If you drive the screen directly for each graphic instruction, it will decrease speed even further.

That's why I go M3 (DUE) or M4 (Metro M4, Microbit V2, Teensy 4) minimum for displays with a resolution like this as they have the needed RAM. The PyGamer you refer to runs at 120MHz and can be overclocked to 200MHz. Even then SPI can get glitchy above 15 000 000. On the tight wiring on the PyGamer board it will go high, but not over prototype wires and such. You could add a ATSAMD51 frequency to it to push it on the M4 boards.

In the piece of driver code above you can see the RPI board is set for 80 000 000 Hz and the default for 24 000 000 Hz, so that would explain the difference between PyGamer/Pico/Uno.

User avatar
GianLupo
 
Posts: 14
Joined: Thu Jun 17, 2021 9:52 am

Re: How to drive a TFT faster

Post by GianLupo »

Funny thing, I tried doing some testing with the Pico (which seems stronger than the PyGamer in pretty much all regards), tweaking its corresponding default frequency, and nothing changed. I even tried replacing the whole block with "#define SPI_DEFAULT_FREQ 10000" (as in 10k) and absolutely nothing changed. I tried passing low values to "tft.begin()", always the same result. I have no idea why this is happening.

I understand what you're saying about the screen buffer. Wouldn't it be possible to solve that by chopping up the screen into different regions and updating each at a time? Either way, I don't think that is what Adafruit_GFX's fillScreen() method does. If I am understanding the code correctly, it is sending the driver the information of a rectangle of pixels to change, followed by the colour to be written on each of them. Am I getting that wrong?

User avatar
blnkjns
 
Posts: 963
Joined: Fri Oct 02, 2020 3:33 am

Re: How to drive a TFT faster

Post by blnkjns »

GianLupo wrote: Mon Nov 14, 2022 12:45 am Funny thing, I tried doing some testing with the Pico (which seems stronger than the PyGamer in pretty much all regards),
Curious what kind of findings you have that lead to this opinion.
I my tests, the M4 is roughly 2-3 times as fast as the M0 in the Pico. It also has floating point math and dual DACs.

User avatar
GianLupo
 
Posts: 14
Joined: Thu Jun 17, 2021 9:52 am

Re: How to drive a TFT faster

Post by GianLupo »

Evidently there is still a lot I need to figure out. I had quickly checked a couple specs and saw that the Pico has more RAM (264kB vs 196kB) and slightly higher clock rate (133MHz vs 120MHz). I think I also got confused about flash/QSPI flash memory.

Either way, it still puzzles me that absolutely nothing I've tried has made any difference...

User avatar
blnkjns
 
Posts: 963
Joined: Fri Oct 02, 2020 3:33 am

Re: How to drive a TFT faster

Post by blnkjns »

You can run the M4 on 200MHz. As far as I know the Pico has no easy overclock.

User avatar
michaelmeissner
 
Posts: 1819
Joined: Wed Aug 29, 2012 12:40 am

Re: How to drive a TFT faster

Post by michaelmeissner »

But note, simply over-clocking the CPU speed may not be enough. Typically you want to increase the SPI bus speed. On some micro-processors, the SPI bus speed is locked to be a multiple of the CPU clock speed. On other micro-processors, you can set it independently.

You might want to check in your code, and see if you have a variable using the SPISettings constructor. With this structure, the call to SPI.beginTransaction will set the SPI bus speed, SPI mode, and endianess. On processors that can adjust the SPI bus speed independently, beginTransaction will set the speed. On the microprocessors that can't adjust the SPI bus, it will use a convenient multiplier.

On the faster Teensy processors, I found I had to adjust the SPI bus speed to avoid glitching different displays (OLED displays tended to need slower bus speeds).

User avatar
Lezardo76
 
Posts: 3
Joined: Fri Jun 03, 2022 9:17 am

Re: How to drive a TFT faster

Post by Lezardo76 »

Late answer but better than nothing!

The pygamer speed of data transfer to the screen has little to do with the SPI clock tweaks … as you discovered in your tests, the speed stays unbeatable.

The adafruit graphics library exploits the DMA Controller of the SAMD51 MCU (the one used in the pygamer). Basically, DMA is like transferring data from memory-to-memory, peripheral-to-memory, memory-to-peripheral or peripheral-to-peripheral in the BACKGROUND without affecting MCU/CPU time. How is it possible? The program sets up and informs the DMAC where data have to be copied (in our case, bytes stream of pixels from memory buffer to the SPI line). When the request is initiated, the DMAC adds it in its queue and it knows when the MCU doesn’t use the bus (the control bus can tell other devices if the address/data bus is free). Therefore the DMAC can use the main bus to perform the data transfer during those unused clock ticks. It is almost like running tasks in parallel because DMAC and MCU are sharing the bandwidth. When the transfer is finished the DMAC informs the MCU (event bus, triggers or interrupt), the software gets the callback to know what to do next.

If you dig into the Adafruit GFX library, search for the Flag USE_SPI_DMA and SAMD51, you will see that it will be compiled with extra stuff (using the zeroDMA.h lib). The main SPI writePixels method will initialize a DMA channel, set up a DMA descriptor (the source and destination of blocks to transfer, etc.) and start the job (https://github.com/adafruit/Adafruit-GF ... SPITFT.cpp line 1077)

It is quite complex and specific per MCU. You can have a look at the official documentation (p270 https://ww1.microchip.com/downloads/en/ ... 01882F.pdf)

User avatar
blnkjns
 
Posts: 963
Joined: Fri Oct 02, 2020 3:33 am

Re: How to drive a TFT faster

Post by blnkjns »

I doubt the DMA will speed things up much. As I mentioned, high speed SPI can give glitches. IMO the bottle neck is the color conversions. The buffer is 16 bit R5G6B5 where most graphics functions can run on 24-bit colour. I experimented with 16-bit sprite buffers and direct memory copy instead of bitmap copy functions from GFX, and that increased speed a lot.

User avatar
Lezardo76
 
Posts: 3
Joined: Fri Jun 03, 2022 9:17 am

Re: How to drive a TFT faster

Post by Lezardo76 »

blnkjns wrote: Wed Jan 18, 2023 6:29 pm I doubt the DMA will speed things up much. As I mentioned, high speed SPI can give glitches. IMO the bottle neck is the color conversions. The buffer is 16 bit R5G6B5 where most graphics functions can run on 24-bit colour. I experimented with 16-bit sprite buffers and direct memory copy instead of bitmap copy functions from GFX, and that increased speed a lot.
The pixel format is fine. ILI9341 like the classic ST7735 screens are both RVG565 16 bits mode (well, there is an exotic 18 bits mode but unused by Arcada library) ... so no 24 bits mode in such small devices. In the examples above, the colors used are ILI9341_BLACK and ARCADA_RED, both are 2 bytes in Rgb565 format, no conversion .. It is raw sent to SPI.

line 27: https://github.com/adafruit/Adafruit_Ar ... cada_Def.h

line 107: https://github.com/adafruit/Adafruit_IL ... _ILI9341.h

In other console like the Gamebuino (using the same pygamer screen), some advanced techniques are used in games to exploit the full screen resolution (160*128*16 bits = 40 Ko of framebuffer) with lower MCU specs … the console has only 32ko of RAM. How can they do this? They sliced the screen and use two small framebuffers per slice (2.5 ko of RAM each), when the MCU is doing the rendering in one buffer, the DMA is sending the previous one in parallel ... then buffers are swaped and it is repeated until the whole screen is refreshed, no glitching. (the MCU is 48 Mhz, SPI rate / 2).

User avatar
GianLupo
 
Posts: 14
Joined: Thu Jun 17, 2021 9:52 am

Re: How to drive a TFT faster

Post by GianLupo »

Kinda late to my own party—I kinda lost track of this project, sorry, and thanks for all the replies.

I've been learning a few things about using SPI in practice and made a little project using the ILI9341 and Pico.
michaelmeissner wrote: Sun Nov 20, 2022 2:31 pm [...] You might want to check in your code, and see if you have a variable using the SPISettings constructor. With this structure, the call to SPI.beginTransaction will set the SPI bus speed, SPI mode, and endianess. On processors that can adjust the SPI bus speed independently, beginTransaction will set the speed. On the microprocessors that can't adjust the SPI bus, it will use a convenient multiplier.[...]
This was really helpful; I had not realised how the clock frequency is set but I think I understand now.
Lezardo76 wrote: Tue Jan 17, 2023 7:11 pm The pygamer speed of data transfer to the screen has little to do with the SPI clock tweaks … as you discovered in your tests, the speed stays unbeatable.
Uhm those tests you're referring to were with the Pico, not the pygamer. I haven't quite figured out that part, but I'm assuming it's probably a combination of two things. The first has to do with what michaelmeissner said: I think I was changing the default SPI clock frequency in the wrong file, as I had not realised that Pico uses different headers than Arduino Uno for the SPI classes, and there might have been somewhere in the code that was overriding my settings. The second thing is, by specifying the clock and COPI pins I was using software SPI (which I later understood is what made the 10x difference on the Uno), and I suspect using that was so slow that increasing the clock frequency didn't really make any difference.
Lezardo76 wrote: Tue Jan 17, 2023 7:11 pm The adafruit graphics library exploits the DMA Controller of the SAMD51 MCU (the one used in the pygamer). Basically, DMA is like transferring data from memory-to-memory, peripheral-to-memory, memory-to-peripheral or peripheral-to-peripheral in the BACKGROUND without affecting MCU/CPU time. How is it possible? The program sets up and informs the DMAC where data have to be copied (in our case, bytes stream of pixels from memory buffer to the SPI line). When the request is initiated, the DMAC adds it in its queue and it knows when the MCU doesn’t use the bus (the control bus can tell other devices if the address/data bus is free). Therefore the DMAC can use the main bus to perform the data transfer during those unused clock ticks. It is almost like running tasks in parallel because DMAC and MCU are sharing the bandwidth. When the transfer is finished the DMAC informs the MCU (event bus, triggers or interrupt), the software gets the callback to know what to do next.

If you dig into the Adafruit GFX library, search for the Flag USE_SPI_DMA and SAMD51, you will see that it will be compiled with extra stuff (using the zeroDMA.h lib). The main SPI writePixels method will initialize a DMA channel, set up a DMA descriptor (the source and destination of blocks to transfer, etc.) and start the job (https://github.com/adafruit/Adafruit-GF ... SPITFT.cpp line 1077)

It is quite complex and specific per MCU. You can have a look at the official documentation (p270 https://ww1.microchip.com/downloads/en/ ... 01882F.pdf)
I see, this makes a lot of sense and I think I understand this mechanism. Basically there is some hardware running on auto-pilot that will simply shifts (or transfer, I don't know if "shift" has a specific meaning in this context) data from some specific locations in memory to the SPI registers without explicit instructions from the processor, much like hardware SPI does its thing without the code manually raising and lowering the clock pin. Is that it?

Locked
Please be positive and constructive with your questions and comments.

Return to “Glowy things (LCD, LED, TFT, EL) purchased at Adafruit”