Teensy 4.1 running 3 x ST7789 displays + on-board SD card

Status
Not open for further replies.

lrtjosh

Active member
Hi!

I have just got an Adafruit 1.14'' 240x135 TFT display working on a Teensy 4.1 using the T3/4 optimised library. Thanks team for the easy examples! It still took me 2 days to get it working, but it turned out I just had a faulty Adafruit display.

My goal is to be able to drive 3 of these ST7789 displays from the same Teensy 4.1, which I understand is doable from KurtE's example in the ST7789_t3 repo, using one display per SPI buss. The catch is I also want to use the on-board SD card reader for loading some data into memory and also loading some bitmaps for the displays here and there. Given that the SPI2 buss on Teensy 4.1 is already taken up by the on-board SD card reader, I am hoping someone can offer some advice on how I might achieve my goal? I'm new to this but my current thinking is that my options might be:

1) Share the SPI2 buss between the SD card and the third display. Can this be achieved by just using a separate CS pin for the display?

2) Drive two of the displays from SPI or SPI1, then leave the SPI2 buss alone for the SD card. If I run two instances of the ST7789_t3 object with the same MOSI/SCLK pins but differing CS/DC/RST pins, will that work or will it conflict somehow?

3) Run some kind of purely bit-banging SPI on any random digital pins for one of the devices?

Does anyone have any thoughts on whether one of these strategies might work, or reasons why one or more can be ruled out, or perhaps another even better option? I am aware there are going to be speed considerations with sharing SPI busses for two devices. The displays will generally just be holding static images for a while, not constantly updating, so I'm not too concerned. The processor will be doing almost nothing else.

Thanks in advance for your thoughts!

- Josh
 
Last edited:
You should be able to run multiple displays on the same SPI buss. The main thing that would need to be different is the CS pin.

There are of course a few caveats to this. That if you try do something like tft.updateScreenAsync(...) only one can be active at a time. That is of course true with any other device as well. The Async update will hold the CS pin of the device active until the DMA operation completes. In theory you should be able to alternate between the two with DMA, but not sure if we held onto some resource like DMAChannel on an SPI buss basis or not.

But as I said especially for normal none DMA based stuff should work.
 
It's definitely possible to run multiple displays per SPI bus, the main issue is making sure you code you program accordingly so that you don't have any slow downs. The most I've done currently is 6 160x80 ST7735 displays at 30 fps, depending on how the rest of the project goes I may have to drop it down to 24 fps though. It's important when running multiple displays to a) cap the fps and b) don't update one display after the other, without doing both you will cause a significant slow down in your program.
 
Thanks @KurtE, I'll give that a go! Very helpful.

Are pins 42,43,44 (MOSI2/MISO2/CS2) actually physically accessible anywhere on the Teensy 4.1 or are they only directly connected to the SD card holder? I don't see them on the excel document but worth asking in case they're unmarked.
 
Thanks @vjmuzik. I'm not sure I follow your second point about updating both displays at once.. can you clarify?
 
I'm fairly certain they are only on the SD card holder, but you don't really need more than one SPI bus for multiple displays.
 
Thanks @vjmuzik. I'm not sure I follow your second point about updating both displays at once.. can you clarify?

What I mean by this is don't have your code do the following:
Code:
void updateDisplays(){
  display1.update();
  display2.update();
  display3.update();
  display4.update();
  display5.update();
  display6.update();
}
Updating one after the other is really slow.
 
OK thanks. I'm still not entirely sure I understand what you're saying but it will probably make more sense to me once I get going. If you had 6 displays updating @ 30fps over the same SPI buss then I don't see a way around updating them one after the other?! Unless you mean something more specific. No stress, I'll work it out as I go.
 
T4.1-Cardlike.jpg

As mentioned SPI2, the SDIO pins (42-47) are only exported on the SDIO connector. Which of course you can make an adapter board to get to...

BUT: The SPI2 pins are also on the Back Memory chips. So you can get to them if you don't use them for RAM/ROM.

Note: The current startup code will always ping some of these pins at startup to see if there are memory chips...

And yes updating 6 displays like that will take awhile... But in theory if 1, 2, 3 are on SPI SPI1 and SPI2 and likewise 4, 5, 6.

And the code does not have issue... you can do things like:
Code:
  display1.updateScreenAsync();
  display2.updateScreenAsync();
  display3.updateScreenAsync();
  display1.waitUpdateAsyncComplete();
  display2.waitUpdateAsyncComplete();
  display3.waitUpdateAsyncComplete();
  
  display4.updateScreenAsync();
  display5.updateScreenAsync();
  display6.updateScreenAsync();
  display4.waitUpdateAsyncComplete();
  display5.waitUpdateAsyncComplete();
  display6.waitUpdateAsyncComplete();
And 3 displays at a time will be doing their SPI DMA code to update at same time. Each does add overhead for starting the SPI conversations and the like.

And of course you could change the code to start up three and return and have code that checks to see if they are done by calling asyncUpdateActive
and if they are done, then start up the other three...
 
Note: What I should have mentioned, is in cases like I just mentioned.

The code for lets two screens on same SPI using Async...

I might do something like:
< Do code to update the stuff on screen 1>
<wait for Screen 2 updates to complete>
<Start up the Async Update Screen 1>
<Do code to update the stuff on Screen 2>
<Wait for first screen async to have completed>
<start up async update Screen 2>
repeat...
 
@KurtE AMAZING. Makes total sense. Thanks! Now excuse me while I bury my head in this for a long time haha
 
Follow-up question... now that I've had a display working for a couple of days, I'm curious about performance. I have set up a very simple sketch that changes between all black and all white pixels across the whole 240x135 display, with just one display running and nothing else on the SPI buss. I can see visible tearing when the screen updates, almost like I can see the lines being drawn or a very quick glimpse of a partial frame change each time it updates. It looks the same if I alternate between two BMP files from the SD card, using code adapted from one of the examples.

As I've got no experience with SPI displays on microcontrollers before this, I'm curious if this is normal? Or should I be expecting a seamless update? Perhaps I'm not using the Framebuffer correctly on T4.1? Here is my entire sketch:

Code:
#include <Adafruit_GFX.h>    // Core graphics library
#include <ST7735_t3.h>
#include <ST7789_t3.h>
#include <SPI.h>

#define TFT_SCLK 13
#define TFT_MOSI 11
#define TFT_CS   10
#define TFT_DC    9
#define TFT_RST   8

ST7789_t3 tft = ST7789_t3(TFT_CS, TFT_DC, TFT_RST);

void setup(void) {

  tft.init(135,240); // Adafruit 1.14" 240x135 ST7789
  tft.useFrameBuffer(true);

}

void loop() {

  tft.fillScreen(ST7735_BLACK);
  tft.updateScreenAsync();
  delay(500);

  tft.fillScreen(ST7735_WHITE);
  tft.updateScreenAsync();
  delay(500);

}
 
You should try upping the SPI frequency, by default it is 24Mhz and I don't believe the Teensy modified libraries have any way to change it from the sketch that I can see. You should be able to set it to 60Mhz with no issues, 80Mhz may also work for you, I've had trouble with it in the past so I normally stick with anything under 80Mhz. This will help, but it may not completely solve the issue unless you have a display that brings out the tearing effect pin.

https://github.com/PaulStoffregen/S...06708c6fd0ce100d40bd5ccbe4326/ST7735_t3.h#L45
 
Thanks, great idea. I managed to get it to run as high as 70Mhz. It still tears just as badly, unfortunately. Changing the value of ST7735_SPICLOCK is definitely having an effect, as I can wind it right down to see an obvious slow-down in the drawing.

Interestingly, if I use setRotation() to rotate the display, the tearing rotates as well. That seems strange to me... I would have thought the tearing would be a result of something down at the SPI/hardware level but it seems to be influenced by the way the pixels are being served higher up in the process.
 
I shot a short video of the tearing. The sketch is using fillScreen(uint8_t) to write a full frame of red, then black, with a delay in between. Teensy 4.1 @ 600Mhz clock speed, SPI @ 70Mhz, Adafruit 1.14" 135x240 display.

Any further ideas welcome, otherwise maybe this is just how good it gets on this particular display.

Short video of tearing

I can confirm that the way it looks in the video is the same way it looks to the human eye.
 
If I remember correctly setRotation is built in to the display so the tearing would rotate like you are seeing. I don’t know anything about how the DMA is working, but at first glance it looks like it’s sending the SPI 16 bits at a time. If it’s not already being done it can be sent 32 bits at a time for a nice speed improvement. I’m curious how bad my displays are tearing if I switch between solid colors like you are, I’ll have to test it later. Though I’m not using the Teensy modified libraries, I’m using the mostly stock Adafruit libraries with some modifications I made for faster SPI calls so I wonder how it compares.
 

As you can see mine tears just as well, though maybe not as bad, it's really hard to tell the difference. In my setup even though I only have 3 displays hooked up it's running as if there was 6 of these connected and visually it looks pretty much simultaneous. The tearing isn't noticeable if you aren't making changes to the colors of the pixels between each frame, so if you are making a GUI and the background stays the same between each frame you won't see any tearing. The displays in my video are updating at 30FPS while the color is changing every half a second and you don't see any tearing between the frames when the color doesn't change. Like I said earlier the only way to really mitigate it is to have the TE pin brought out, but most SPI displays don't do that since most people don't use it.

Here's my code for this, some parts won't compile for you and without the changes to SPI that I did it won't be fast, but it may help you get some ideas on the order to run things.
Code:
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <SPI.h>
#include <T4_PowerButton.h>

//Change these if needed
uint8_t displayNumber = 6;  //Number of displays to update, only 6 are setup below
uint8_t targetFPS = 24;     //FPS to target

//Don't change
elapsedMillis oneSecond;
elapsedMillis internalRefresh;
uint32_t looped = 0, internal = 0, fps = 0;
elapsedMicros screenRefresh;
uint8_t displayToUpdate = 0;

uint16_t solidColor = 0;

Adafruit_ST7735 display0 = Adafruit_ST7735(&SPI, 35, 34, 33);
Adafruit_ST7735 display1 = Adafruit_ST7735(&SPI, 36, 34, -1);
Adafruit_ST7735 display2 = Adafruit_ST7735(&SPI, 37, 34, -1);
Adafruit_ST7735 display3 = Adafruit_ST7735(&SPI, 44, 34, -1);
Adafruit_ST7735 display4 = Adafruit_ST7735(&SPI, 45, 34, -1);
Adafruit_ST7735 display5 = Adafruit_ST7735(&SPI, 46, 34, -1);
Adafruit_SPITFT *displays[6] = {
  &display0,
  &display1,
  &display2,
  &display3,
  &display4,
  &display5,
};

const uint16_t canvasWidth = 80;
const uint16_t canvasHeight = 160;
GFXcanvas16 Canvas0 = GFXcanvas16(canvasHeight, canvasWidth);
GFXcanvas16 Canvas1 = GFXcanvas16(canvasHeight, canvasWidth);
GFXcanvas16 Canvas2 = GFXcanvas16(canvasHeight, canvasWidth);
GFXcanvas16 Canvas3 = GFXcanvas16(canvasHeight, canvasWidth);
GFXcanvas16 Canvas4 = GFXcanvas16(canvasHeight, canvasWidth);
GFXcanvas16 Canvas5 = GFXcanvas16(canvasHeight, canvasWidth);
GFXcanvas16 *Canvas[6] = {
  &Canvas0,
  &Canvas1,
  &Canvas2,
  &Canvas3,
  &Canvas4,
  &Canvas5,
};

void setup() {
  // put your setup code here, to run once:
  delay(1000);
  Serial.begin(115200);

  for(uint8_t i = 0; i < displayNumber; i++){
    Adafruit_ST7735* disp = (Adafruit_ST7735*)displays[i];
    disp->initS(79999999); //Modification for ST7735S based display
    Canvas[i]->setRotation(1);
    Canvas[i]->setTextWrap(0);
  }
  
  flexRamInfo();
  fps = 0;
  looped = 0;
  internal = 0;
}

void loop() {
  // put your main code here, to run repeatedly:
  while(true){
    screenUpdate();
  
    if(internalRefresh >= 500){ //Update or read peripherals here, lower refresh rate if needed
      internalRefresh -= 500;
      internal++;

      //Screen color is being changed here
      static uint8_t toggle = false;
      toggle++;
      if(toggle == 3){
        solidColor = display0.color565(0,0,0xFF);
        toggle = 0;
      }
      else if(toggle == 2){
        solidColor = display0.color565(0,0xFF,0);
      }
      else if(toggle == 1){
        solidColor = display0.color565(0xFF,0,0);
      }
    }
    
    looped++;
    if(oneSecond >= 1000){
      oneSecond -= 1000;
      Serial.print("FPS: ");
      Serial.print(fps);
      Serial.print("  Looped: ");
      Serial.print(looped);
      Serial.print("  Internal: ");
      Serial.println(internal);
      fps = 0;
      looped = 0;
      internal = 0;
    }
  }
}

void screenUpdate(){  //Displays are refreshed here
  if(Serial.available()){
    targetFPS = (uint8_t)Serial.parseInt();
    Serial.print("Setting FPS to ");
    Serial.println(targetFPS);
  }
  if(screenRefresh >= (uint32_t)round((1000000/targetFPS/displayNumber))){  //Display refresh interval based on the number of displays and desired FPS
//    screenRefresh -= (uint32_t)round((1000000/targetFPS/displayNumber));
    screenRefresh = 0;

    drawDisplay(displays[displayToUpdate], Canvas[displayToUpdate]);
    
    displayToUpdate++;
    if(displayToUpdate == displayNumber){
      displayToUpdate = 0;
      fps++;
    }
  }
}

void drawDisplay(Adafruit_SPITFT *display, GFXcanvas16* canvas){  //Displays are drawn here (only make changes to the canvas here before drawing to your display for increased efficiency)
  //Use varibles to keep track of what to draw to the canvas then make the updates here accordingly
  canvas->fillScreen(solidColor);  //Change canvas color
  display->drawRGBBitmap(0,0,canvas->getBuffer(), canvas->height(), canvas->width());  //Draw to display once canvas is ready
}
 
Status
Not open for further replies.
Back
Top