ST7796 Teensyduino support

You really are leaving no stone unturned!
I’ll probably get bored soon :rolleyes:

A lot of the motivation for that rig was getting fed up with disconnecting and reconnecting in order to try different displays, and the associated risk of damage to the kit.

That looks like a nice display, I’ll probably add one to the growing collection at some point. Part of the recent hiatus was caused by the ST7789 displays being shipped from China in anti-static bags, along with others in plastic boxes, all stuffed into a single padded envelope. Oddly enough, the unboxed ones were all broken on arrival…
 
The latest commits to the big-screen-t4 branch now have the ability to coexist with ILI9341 and GC9A01A displays on a single SPI bus, provided you also grab the linked branches for those displays. You can also have multiple ST77xx displays on one bus, of course. Each display needs its own /CS, and you need to deal with a shared /RST yourself.

I haven't ported the non-interference changes into the ILI9341 and GC9A01A drivers, mainly because it's likely to become a maintenance nightmare in very short order. (Also, the repos I've based my changes on are not actually part of Teensyduino, they're developed from it with optimisations.) It's not clear to me what the best approach might be towards trying to unify the many disparate drivers, and indeed whether it's even worth trying to continue Teensy 3.x support, given they're legacy products.
 
@h4yn0nnym0u5e - installed 1.60b5 and lost the DMA code edits.
Are they needed? Did those files change - I pulled latest display github - but didn't put in libs folders ... but just building Ken's demo failed and forced recall ...
 
@defragster - yes, they are still needed, and Paul's "DMAChannel default channel preemption settings" commit has managed to break my detection of the fully-working DMA code changes :mad:

You'll need to reinstate the edits, and add #define DMA_PREEMPTION_AVAILABLE into DMAChannel.h, and pull the latest changes from my development branch. If that's all done correctly, audio will still work; if not, it goes back to being broken.
 
still needed
Figured - not sure about hacking - will see. Hope the work might get incorporated in a usable fashion.

@KenHan noted even the simple PR I did wasn't included either to prevent Always breaking the transaction - even when tiny that was enough to allow clear audio in the simple cases for fonts - but just allowing the same Transaction breaks copied from another display
 
I've just pushed another change, so the check is now against #define DMACHANNEL_HAS_PREEMPTION, which is more consistent with existing macro names.

Agree, it's all a bit of a pain to have to hack on many files to make stuff work :(
 
...and again - there's now no need to #define DMACHANNEL_HAS_PREEMPTION. This is at the expense of the user sketch having no way to tell whether DMA preemption is available. If it's not, audio playback won't work.
 
FONT QUESTION:
Is it by design that there are font refs back to ILI9341 and not all changed over to the ST7735 base family? Struct and .h included?

Few are included in the beta release - there are some in a ZIP in the folder? Working on changing a touch UI from ili9341 to st7796.

Sketch at hand wants Arial__BOLD font found in the zip. It is working - though a WIP needing cleanup.

Having some grief on other than DEV machine that works - but sharing it tough and it ends up failing.
 
FONT QUESTION:
Is it by design that there are font refs back to ILI9341 and not all changed over to the ST7735 base family? Struct and .h included?

Few are included in the beta release - there are some in a ZIP in the folder? Working on changing a touch UI from ili9341 to st7796.

Sketch at hand wants Arial__BOLD font found in the zip. It is working - though a WIP needing cleanup.

Having some grief on other than DEV machine that works - but sharing it tough and it ends up failing.
Morning Tim

All our graphics libraries using the ILI9341 font and font structure as the common denominator. You mentioned the ST7735 base family - if you look at the defined font family it references back to the ILI9341 font:
```
#ifndef _ST7735_t3_font_ComicSansMS_
#define _ST7735_t3_font_ComicSansMS_

#include "ILI9341_fonts.h"

#ifdef __cplusplus
extern "C" {
#endif

extern const ILI9341_t3_font_t ComicSansMS_8;
extern const ILI9341_t3_font_t ComicSansMS_9;
extern const ILI9341_t3_font_t ComicSansMS_10;
extern const ILI9341_t3_font_t ComicSansMS_11;
extern const ILI9341_t3_font_t ComicSansMS_12;
extern const ILI9341_t3_font_t ComicSansMS_13;
extern const ILI9341_t3_font_t ComicSansMS_14;
....
```
Doing it this way also allows for using the ILI9341_font library (https://github.com/mjs513/ILI9341_fonts).

Hope that explains it.

Mike
 
Morning all,

Might also mention, if that is not enough fonts, than most of our drivers also support using the
Adafruit GFX Fonts. (Note: ili9341_t3 does not, but ili9341_t3n does).

One nice thing of Adafruit fonts is that they are one size of character per file, so they take less room.
But a little tricky code wise as font characters can overlap...
 
ILI9341 font and font structure as the common denominator.
Thanks @mjs513 and @KurtE
I thought that was the right answer - but was chasing a build problem where it was grabbing something and then left hanging and broke the build ... except on my dev machine.

As soon as I can get back to it - I'll try to clean up the mess I made chasing a phantom and post something.
 
Hallo
I used Teensy 4.1 and ST7796S 3.5inch ISP TouchDisplay from WaveShare. For the TFT Display i used ST7735_t3-dev-big-screen-t4 Library and bitbank2/JPEGDEC@^1.8.4 with DMA. CPU speed is 720Mhz and SPI speed 60MHz. Display cable length is 9cm.


Screenshot 2026-03-09 185742.png



 
Last edited:
I've made another branch as a result of a separate project which is going to require 9 displays. Yes, really...

While the multiple screen requirement is unlikely to be super-useful for most people, in the process I had to fix some issues I didn't know the code had, so could I encourage anyone who needed the dev/big-screen-t4 branch to try the dev/big-screen-t4-muxedCS one. It ought to appear no different to you, please report back if it gives issues.

For those interested in the changes, here's a demo I made and included in the examples.
C++:
/*
 Example of multiplexed chip selects for multiple displays

 The test hardware uses a 74LVC138 with Y0 connected
 to a 2.25" 76x284 display, and Y1-3 to 3 1.54" 240x240
 displays. It should be evident from the code how to
 change this to suit your setup...
 */
#include <ST7789_t3.h> // Hardware-specific library

#define MUX_CS 0
#define MUX_A  1
#define MUX_B  2

#define GBL_RST  22
#define TFT_RST  -1 // shared - do reset in sketch code
#define TFT_DC   10
#define TFT_BLKL  4 // active low backlight (240x240 displays)
#define TFT_BLKH  3 // active high backlight (76x284 display)

/*
 * Use 74LVC138 decoder to provide /CS signal to one of
 * 4 displays, using only 3 Teensy outputs (we could
 * address up to 8).
 */
static void CSfn(int which, bool negate)
{
  if (negate)
    digitalWriteFast(MUX_CS,1);
  else
  {
    digitalWriteFast(MUX_A,(which&1)!=0);
    digitalWriteFast(MUX_B,(which&2)!=0);
    // digitalWriteFast(MUX_C,(which&4)!=0); // use for >4 displays
    digitalWriteFast(MUX_CS,0);
  }
}

// Provide lambda functions to assert /CS for each display
ST7789_t3   ST7789a  = ST7789_t3( [](bool negate) { CSfn(3, negate); } , TFT_DC, TFT_RST);
ST7789_t3   ST7789b  = ST7789_t3( [](bool negate) { CSfn(2, negate); } , TFT_DC, TFT_RST);
ST7789_t3   ST7789c  = ST7789_t3( [](bool negate) { CSfn(1, negate); } , TFT_DC, TFT_RST);
ST7789_t3   ST7789d  = ST7789_t3( [](bool negate) { CSfn(0, negate); } , TFT_DC, TFT_RST);

#define SCREENS 4
ST7789_t3* screens[]{&ST7789a, &ST7789b, &ST7789c, &ST7789d}; // easier to deal with an array
uint16_t bkgnds[]{CL(24,0,0), CL(0,24,0), CL(0,0,24), CL(128,96,32)};


// Show we can address each screen individually
void identify(ST7789_t3& tft, int n, uint16_t bkgnd, int inv)
{
  char buf[20];
  int16_t  x,y;
  uint16_t w,h;

  // Basic settings:
  tft.setRotation(1);
  tft.invertDisplay(inv);
  tft.fillScreen(bkgnd);

  // Show screen number in the middle:
  sprintf(buf,"Screen #%d",n);
  tft.setTextColor(ST77XX_WHITE);   
  tft.setTextSize(3);
  tft.getTextBounds(buf,0,0,&x,&y,&w,&h);
  tft.setCursor((tft.width() - w)/2,(tft.height() - h)/2);
  tft.print(buf);

  // Show screen resolution at bottom right:
  sprintf(buf,"%d x %d",tft.width(), tft.height());
  tft.setTextSize(1);
  tft.getTextBounds(buf,0,0,&x,&y,&w,&h);
  tft.setCursor(tft.width() - w - 2,tft.height() - h - 2);
  tft.print(buf);
}

void setup()
{
  // Initialise /CS multiplexer
#define SAFE_MUX(p,v) pinMode(p, OUTPUT); digitalWrite(p,v)
  SAFE_MUX(MUX_A,0);
  SAFE_MUX(MUX_B,0);
  SAFE_MUX(MUX_CS,1);

  // Backlights on:
  pinMode(TFT_BLKL, OUTPUT);
  pinMode(TFT_BLKH, OUTPUT);
  digitalWriteFast(TFT_BLKL,0);
  digitalWriteFast(TFT_BLKH,1);

  // Reset screens:
  pinMode(GBL_RST, OUTPUT);
  digitalWriteFast(GBL_RST,1); delay(1);
  digitalWriteFast(GBL_RST,0); delay(1);
  digitalWriteFast(GBL_RST,1); delay(1);

  // Initialise:
  for (int i=0;i<SCREENS-1;i++)
    screens[i]->init();
  screens[SCREENS-1]->init(76,284); // different, non-default size

  // Identify:
  for (int i=0;i<SCREENS;i++)
    identify(*screens[i], i, bkgnds[i], i!=3);
}

void loop()
{
}

Here's the result - the 74LVC138 multiplexer I used is just visible, indicated by the yellow arrow.
IMG_2933.jpg
 
Here's a simple demo of asynchronous updates of a small screen area.
C++:
/*
 * Demo of asynchronous updates of clipped screen areas
 */
#include <ST7796_t3.h>

#define ST7796_CS 29
#define TFT_DC    10
#define TFT_RST   34
#define TFT_BLK   33

ST7796_t3 tft = ST7796_t3(ST7796_CS, TFT_DC, TFT_RST);


void setup()
{
  pinMode(TFT_BLK,OUTPUT);
  pinMode(TFT_RST,OUTPUT);

  // reset display
  digitalWriteFast(TFT_RST, HIGH);
  delay(1);
  digitalWriteFast(TFT_RST, LOW);
  delay(1);
  digitalWriteFast(TFT_RST, HIGH);
  delay(1);

  // turn backlight on
  digitalWriteFast(TFT_BLK, HIGH);

  // standard setup
  tft.begin();
  tft.fillScreen(0);

  // this will depend on your hardware!
  tft.setRotation(1);
  // tft.setSPISpeed(60'000'000);

  // two-line (480x2) intermediate buffer,
  // i.e. 960 pixels, or 30x32 pixels, etc.
  tft.useIntermediateBuffer(tft.width() * 2 * sizeof(uint16_t)); // intermediate buffer
  tft.useFrameBuffer(true); // whole frame buffer

  // keep track of changed area
  tft.updateChangedAreasOnly(true);
}

elapsedMicros em;
int count, pixels;
void loop()
{
  if (!tft.asyncUpdateActive())
  {
    int took = (int) em;
    Serial.printf("%5dpx in %4dus (%.3fMpx/s); counted to %5d\n", pixels, took, (float) pixels/took, count);

    int w = 60+random(140), h = 40+random(80);
    int x = random(tft.width()), y = random(tft.height());
    uint16_t colour = random(65536);

    // ensure rectangle shows and timings are accurate
    if (x+w > tft.width())  x = tft.width()  - w;
    if (y+h > tft.height()) y = tft.height() - h;

    tft.clearChangedArea();       // reset area boundaries - now invalid!
    tft.fillRect(x,y,w,h,colour);
    tft.updateScreenAsync(false,true,true); // not continuous; interrupts on; update only changed area
    em = 0;
    count = 0;
    pixels = w*h;
  }
  count++; // show that loop() executes while screen is updating
}

The prompt for this came from this post from @Rolfdegen
Hello

I used the Library https://github.com/h4yn0nnym0u5e/ST7735_t3.git

I suspect a bug in the framebuffer library with ST7796S and DMA. When I draw a small filled rectangle of 100x100 pixels with
updateScreenAsyncT4(true) and DMA, it takes 45ms. When I draw the same rectangle without a framebuffer, the Teensy 4.1 only needs 2.7ms.
A fillscreen(color) takes 45msec with and without DMA :unsure:
but since it doesn't appear to relate to that thread's title (LVGL) I've posted here.

Note that updateScreenAsyncT4(true) is doing a continuous background whole-screen update, hence the quoted timings. In contrast, this example's updateScreenAsync(false,true,true) does a one-shot background update of a subset of the frame buffer. Other options are available - see this section of the documentation.
 
Hallo h4yn0nnym0u5e
Thank you very much. Your suggestion works. But I'm still getting jumps in the encoder readings. I reduce the time it takes to display the image, I tried skipping the redraw of the black background. I draw the old envelope line in black to delete it, and then draw the new line in green. The grid lines are draw in light gray. Unfortunately, that didn't help much. The encoders are still jumping.

The only solution for interference-free encoder polling was to disable the framebuffer and send the character commands directly to the display. Unfortunately, this causes slight flickering in the display output.

Envelope Line
20260330_205803.jpg



TFT MOSi Line with enabled Framebuffer
updateChangedAreasOnly(true)
tft.updateScreenAsync(false,true,false)
read change Encoder with asyncUpdateActive() Loop

RigolDS1.png

300 milliseconds to draw the screen. That's not enough time to read the encoder.
The encoders are stuttering.


TFT MOSI Line and disabled Framenuffer
read change Encoder in normal Loop

RigolDS0.png

There is enough time here to query the encoder. The encoders are running smoothly

Video Encoder query (in the video is ST7798S but is an ST7796S. Sorry)
 
Last edited:
tft.updateScreenAsync(false,true,false)
This will do a whole screen update, because the last parameter is false. You also need to call clearChangedArea() after each async update, because otherwise the "dirty" area just gets bigger and bigger.

You do have to be super-careful how much of the framebuffer is changed when you are only updating a partial area, because you are quite likely to be re-sending at least some pixels you haven't actually changed; the dirty area is always rectangular. The time taken for an async update is never going to be shorter than a synchronous one, except in cases where many pixels in the framebuffer have been repeatedly written. The advantage should be that the framebuffer writes are very fast, and that the CPU can then be doing other stuff while the async update is going on.

A small digression on how I implemented the partial update via an intermediate buffer. useIntermediateBuffer() must have previously been called to set up the buffer. As many partial lines as possible are copied to the buffer: in my example above there's room for 2 screen-width lines of 480 pixels, so if your partial area is 240 wide, then 4 lines can be transferred in one transaction. DMA is then started, and on completion it interrupts out, the next 4 lines are copied, and the process repeats - the last transaction would be between 1 and 4 lines, depending on the height of your partial area. Framebuffer to intermediate buffer copies are done by DMA - it's not possible to set up a DMA channel to copy arbitrary length partial lines straight to the display, unfortunately. However, the ISR can be very fast, because all it needs to do is prepare the next set of DMA transactions, unless it's finished. I set my example buffer to 960 pixels, because at the default SPI bus speed of 16MHz that takes about 1ms to transfer; it's probably sensible to make it bigger if you're running the bus faster.

So ... it's hard to see why this would interfere with your encoder readings. I can see that you're doing those inside your ISR, and being I²C that won't be very fast - if it blocks the screen updates you might see a slight effect, I suppose, but then your readings should be fine. The screen ISR should be so short that even if it does pre-empt the I²C ISR briefly, there ought to be no bad effects. You can alter the async update priority using setAsyncInterruptPriority(), remembering that a high value is a low priority!

It might be worth using a few more channels on your scope to look at how long the encoder and display ISRs are taking, and how those relate to the encoder edges (could encoder contact bounce be causing issues if the I²C ISR latency is slightly increased?).
 
Sorry.. I mean tft.updateScreenAsync(false,true,true)

Thank you for the detailed description. You mentioned that the buffer area is always rectangular. That's a very large area for the envelope line. It also doesn't matter whether I draw a filled rectangle or a curve of the same size. The redraw time is the same for both. This also explains the long wait before the encoder can be polled again to draw a new envelope line.

Perhaps a small buffer to store the encoder data would help. Once the redraw is complete, I would read the new encoder values from the buffer and redraw the area.
 
Last edited:
You mentioned that the buffer area is always rectangular. That's a very large area for the envelope line. It also doesn't matter whether I draw a filled rectangle or a curve of the same size. The redraw time is the same for both.
Yes indeed - it really doesn't help for a large area with few changes!
This also explains the long wait before the encoder can be polled again to draw a new envelope line.
But ... you don't have to wait for async to complete before you poll the encoder. That's the whole point of the async update. You do of course have to wait before you draw a new line ... which is painful, as you noted.

I've added an untested int getChangedArea(void) method (commit here) which returns the integer number of pixels changed since the last async update or call to clearChangedArea(). Depending on how you draw your envelope (ideally individual pixels or short lines) you can interrogate that as you draw, and trigger an asynchronous update when the number of pixels reaches a limit you choose. Say you allocated a 4kPixel buffer, you could update when you've changed a >2kPixel area. That would more or less guarantee a single pair of DMA transactions (memory-to-memory, then memory-to-display), with just one interrupt at the end.
 
I can't draw a drawFastHLine() and drawFastVLine() with updateScreenAsync(false, true, true). Is that correct ?


C:
// Draw envelope grid ------------------------------------------
void draw_env_grid()
{
    uint8_t count = 4;
    for (size_t i = 0; i < count; i++)
    {
        tft.drawFastHLine(10, 75 + (i * 45), 456, light_grey);
    }

    count = 6;
    for (size_t i = 0; i < count; i++)
    {
        tft.drawFastVLine(10 + (i * 91), 75, 136, light_grey);
    }
}

// if change attack the delete old line  and draw new
    if (AtkLevel != pre_env1_attack)
    {
        tft.clearChangedArea();

        int temp_var = AtkLevel; // save current atk value

        for (int i_ = 0; i_ < 2; i_++)
        {
            L = 0.0f;
            k = 0.0f;
            i = 0.0f;
            xpos = 10;
            result = 0;
            value2 = 0;
            hight = 0;
            x1 = 0;
            y1 = 0;

            if (i_ == 0)
            {
                AtkLevel = pre_env1_attack;
                color = ST7735_BLACK;  // delete color
            }
            else
            {
                AtkLevel = temp_var;
                color = ST7735_GREEN; // draw color
            }

            int AtkLevel_ = AtkLevel * 0.72f;
            L = AtkLevel_;

            if (L <= 1)
            {
                L = 1;
            }

            hight = drawHight - 1;
            k = -4.0f;
            x1 += xpos + DelLevel;
            y1 = 208;

            if (k == 0)
            {
                k = 0.01f;
            }
            if (L <= 1)
            {
                k = 0.0000001;
            }

           // draw line
            for (i = 0; i <= L; i++)
            {
                result = (exp(k * i / L) - exp(0)) / (exp(k) - exp(0));
                uint8_t value = float(result * hight);
                tft.drawLine(x1 + i, y1 - value2, x1 + i, y1 - value, color);
                value2 = value;
            }
        }

        // draw grid line
        void draw_env_grid();

        pre_env1_attack = AtkLevel;  // old atk level = new atk level

        tft.updateScreenAsync(false, true, true);
 
    }
 
Last edited:
As far as I’m aware you should be able to. Those methods are used internally, e.g. to draw rectangles, so it’d be fairly obvious if they didn’t work…
 
Back
Top