ST7796 Teensyduino support

KenHahn

Well-known member
I have a new Teensy 4.1 board in the works that uses the 3.5" ST7796 IPS display with FT6336 capacitive touch. A really nice upgrade over the typical ILI9341 setup. Higher resolution, better viewing angles and no need to calibrate the touch.

@KurtE I have been using your ST7796_t3 fork off Paul's ST7735_t3 library and it seems to be working fine from the somewhat limited testing I have done.

The 3.5" display does require the colors to be inverted ala tft.invertDisplay(true);. The 4.0" ST7796 display I tested worked without the colors being inverted. Might be IPS vs non-IPS related, but not a big issue either way other than you see the colors invert briefly at power up. I am also not using Reset in the constructor, the reset pin is just pulled high.

Any chance of getting your fork rolled into the upcoming Teensyduino 1.60 Teensyduino release as it seems pretty minor?
 
Any chance of getting your fork rolled into the upcoming Teensyduino 1.60 Teensyduino release as it seems pretty minor?
That is a Paul call... There is an open PR for it:
 
using the ST7796 4" display
I have one on my desk now from @KenHahn and finding the @KurtE ST7796 Branch to work. Would be great to have it incorporated in next Beta.
Viewing angle is great. Nice size and resolution. Just getting started but it is a big improvement in many ways - yet not too big - and touch is working.
 
Just trying this, and when I setRotation(2) I seem to get blue and red swapped. Anyone else see this? Like @KenHahn I have /RESET pulled high permanently. Code pulled just now from Paul's repo at SHA-1 ID f0449c6.
 
Just trying this, and when I setRotation(2) I seem to get blue and red swapped.
Can Confirm. Using @KurtE github source

Can't type much but this line:
Rotation 1,3,4 give BLUE screen background and 3 Buttons with RED background - all white text.
>> Change to Rotation(2) and the background is RED and the Buttons go BLUE

Notes in PR #36 - Fix confirmed
 
Last edited:
On the @KenHahn Mini board (with embedded PJRC Audio on PCB) modifying the Audio Tutorial to work with the ST7796_t3 and replace the ILI9341_t3 it works the display on the 320 direction!

But the SOUND is broken audio. Perfect when the bar updates removed. Modified the timing entry to redraw and going from 15 msec to 65 with great improvement.

Seems the drawing takes 21 msec - so the 15 msec timer was elapsed on next loop. Moved the msec=0 to end of drawing from start and that helped, but better with longer wait to redraw.

The math was set up for the 320 direction of the smaller ILI9341_t3 so perfect on the narrow rotation side of the ST7796_t3 320x480 unchanged.

But drawing the same number of pixels taking much longer and affecting the audio library work getting the sound to play from SD card?

Anyone with the ST display have an audio board on T_4.1 to try this? Seems it is dwell time on display updates or somehow it impacts Audio Updates?

Current code edited here - minor edits to PJRC src? Made an error?:
 
I can confirm this.

No time to do the forum search now, but I recall there's a flaw (one of many...) in AudioPlaySdWav in that it asserts it's using the SPI bus even when it isn't, e.g. when using the built-in SD card on SDIO. I've disabled this temporarily, and the audio clears right up, even with display updates running continuously.

Never mind, my buffered SD playback PR is in, so this will all become academic some day.
 
Alternate Info - as above ucing Audio Tutorial: Part_3_03_TFT_Display.ino.
Code:
Seems the diff is:
#include <ILI9341_t3.h>
#include <font_Arial.h> // from ILI9341_t3
versus:
#include <ST7796_t3.h>
#include <st7735_t3_font_Arial.h>

@KenHahn sent me a 'Beta' MINI PCB that then got some digital pin upgrades added buttons and RGB LED in addition to the display change ILI to ST above.

Otherwise the PCB is the same overbuilt testbed with the only dozen pins unused brought to a GPIO connector. (Audio onboard, CAN, rs485, USB_Host, Ethernet, 1Gb SerFlash on SPI1, UART to ESP32)

So, with only (and minor unrelated {?} wiring diffs) the change in display from PJRC provided working on ILI display the p#10 code with ST library.
The ILI runs fine from SDBUILTIN and the ST fails horrible when it comes to the audio playback.

Replacing the right Meter print value with the elapsed msecs - they both show 21 ms to update the bar graphs and update the display to that point.

The Library change seems the problem, but both seem to take the same 21ms draw time? Is there something else unique to the ST7796_t3 that is not present in the ILI9341_t3 ? Or was something missed replacing the ILI with the ST display?

@KenHahn can speak to any real diffs in the PCB - though AFAIK non would make this match the behavior it seems the @h4yn0nnym0u5e also observed?

ILI9341 as edited attached as it is complete to build unlike files for the tutorial, The edited version for ST7796 current on github p#10
 

Attachments

  • Part_3_03_TFT_Display.ino
    4.2 KB · Views: 9
There are no significant differences in the basic display wiring between the two. Both are on SPI0. CS moved from pin 40 on ILI9341 to pin 10 on the ST7796.

The display is the only thing on the SPI0 bus on either version, except that the ST7796 version does also have a SN74LVC1G125 buffer attached to pin 13/SCK to drive a remote mounted LED since the one on the Teensy 4.1 is not visible.

The touch portion which is on the SPI0 bus on the ILI9341 version moved to I2C on the ST7796 with a separate Adafruit_FT6206.h library. Touch isn't being used in this demo, but the ILI9341 touch is handled as part of the ILI9341.h display library so possibly some difference there.

One possible clue is that with the audio level bars commented out, but the amplitude numbers still updating at the same speed, the issue does not occur. Even though the overall display update speed appears to be the same, perhaps something about drawing the rectangle bars requires longer SPI transactions on the ST7796 than on the ILI9341 which exceed the SDIO buffer size?
 
One possible clue is that with the audio level bars commented out,
May be a helpful clue. That was tested and noted to Ken in email but not shown in p#10, So rewriting the text numbers at the same rate not an issue - but the larger pixel volume of the bars triggers the issue:
Code:
      tft.setFont(Arial_14);
      tft.fillRect(60, 284, 40, 16, ST7735_BLACK);
      tft.setCursor(60, 284);
      tft.print(leftNumber);

Note: That post #11 was Sunday (2 days back) - and it was attempted then on the ILI9341 but only showing white display - as they do. Plugged it in today and it was running perfectly and reprogrammed with edits and Audio heard to be good. So post #13 info was almost at hand ,,, but not working. Also note - it has been running that sketch now for hours and display working properly and audio is still loud and clear on the ILI9341 unit - both Headphones and Line Out.

Also not working right still is my right arm thing :( so that isn't helping here either.
 
Came across something interesting based on Defragster's comments above.

Using his code that illustrates the issue with the ST7796, I first changed the bar height down to 120 to reduce the amount of pixels being drawn by 50% to see if it had any effect and it did not.

I put the height back to 240 and changed the width of the bars from 40 down to 20 which also reduced the amount of pixels being drawn by 50% and it improved things significantly even at a 25mSec update rate. Going down to 10 on width sounds good even at the original 15mSec update rate. Not sure if this points to something with the ST7796 library or the display itself.

if (msecs > 15)
// draw the verticle bars
int height = leftNumber * 240;
tft.fillRect(60, 280 - height, 10, height, ST7735_GREEN);
tft.fillRect(60, 280 - 240, 10, 240 - height, ST7735_BLACK);
height = rightNumber * 240;
tft.fillRect(140, 280 - height, 10, height, ST7735_GREEN);
tft.fillRect(140, 280 - 240, 10, 240 - height, ST7735_BLACK);
 
Out of time - but have conjecture I just sent in email to Ken based on this quick test:

Yes, Narrow bars sounds good!

Showing 6ms update time now - where both were the same 21ms before.

Even without the msecs=0 closing out the IF(){}!

Maybe there is something the way the pixels pack for wider areas? Or the ;dwell' time doing 40 wide is somehow different?

Seems that is it! Drawing two 10 wide is good. Drawing one 10 and one 15 is BAD!

Drawing 3 or 4 that are 10 wide to fill that same 40 pixels is GOOD! Update msec is 22ms with 4 draws of 10, so it takes longer and with no msec=0 at the if() tail it will enter immediately on exit!


Like:
Code:
// draw the verticle bars
      int height = leftNumber * 240;
 //     tft.fillRect(60, 280 - height, 40, height, ST7735_GREEN);
 //     tft.fillRect(60, 280 - 240, 40, 240 - height, ST7735_BLACK);
 //     height = rightNumber * 240;
 //     tft.fillRect(140, 280 - height, 40, height, ST7735_GREEN);
 //     tft.fillRect(140, 280 - 240, 40, 240 - height, ST7735_BLACK);


      tft.fillRect(60, 280 - height, 10, height, ST7735_GREEN);
      tft.fillRect(60, 280 - 240, 10, 240 - height, ST7735_BLACK);
      height = rightNumber * 240;
      tft.fillRect(140, 280 - height, 10, height, ST7735_GREEN);
      tft.fillRect(140, 280 - 240, 10, 240 - height, ST7735_BLACK);
      // a smarter approach would redraw only the changed portion...


      height = leftNumber * 240;
      tft.fillRect(80, 280 - height, 10, height, ST7735_GREEN);
      tft.fillRect(80, 280 - 240, 10, 240 - height, ST7735_BLACK);
      height = rightNumber * 240;
      tft.fillRect(160, 280 - height, 10, height, ST7735_GREEN);
      tft.fillRect(160, 280 - 240, 10, 240 - height, ST7735_BLACK);


      height = leftNumber * 240;
      tft.fillRect(70, 280 - height, 10, height, ST7735_GREEN);
      tft.fillRect(70, 280 - 240, 10, 240 - height, ST7735_BLACK);
      height = rightNumber * 240;
      tft.fillRect(150, 280 - height, 10, height, ST7735_GREEN);
      tft.fillRect(150, 280 - 240, 10, 240 - height, ST7735_BLACK);


      height = leftNumber * 240;
      tft.fillRect(90, 280 - height, 10, height, ST7735_GREEN);
      tft.fillRect(90, 280 - 240, 10, 240 - height, ST7735_BLACK);
      height = rightNumber * 240;
      tft.fillRect(170, 280 - height, 10, height, ST7735_GREEN);
      tft.fillRect(170, 280 - 240, 10, 240 - height, ST7735_BLACK);
 
The fillRect function in the library has the note below.

// TODO: this can result in a very long transaction time
// should break this into multiple transactions, even though
// it'll cost more overhead, so we don't stall other SPI libs

By drawing multiple narrower rectangles, even though it totals the same area, we are by default breaking the drawing up into multiple smaller transactions.

Code:
// fill a rectangle
void ST7735_t3::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color)
{
....
    // TODO: this can result in a very long transaction time
    // should break this into multiple transactions, even though
    // it'll cost more overhead, so we don't stall other SPI libs
    beginSPITransaction();
    setAddr(x, y, x+w-1, y+h-1);
    writecommand(ST7735_RAMWR);
    for(y=h; y>0; y--) {
      for(x=w; x>1; x--) {
        writedata16(color);
      }
      writedata16_last(color);   
    }
    endSPITransaction();
  }
}

Interestingly, the ILI9341fillRect function has the same note, but does handles this routine a little differently and breaks the draw function up into multiple transactions. Seems like this could be the fix, but I don't know how to test changes to library code (above my programming pay grade)

Code:
  // fill a rectangle
void ILI9341_t3::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color)
{
....
  // TODO: this can result in a very long transaction time
  // should break this into multiple transactions, even though
  // it'll cost more overhead, so we don't stall other SPI libs
  beginSPITransaction(_clock);
  setAddr(x, y, x+w-1, y+h-1);
  writecommand_cont(ILI9341_RAMWR);
  for(y=h; y>0; y--) {
    for(x=w; x>1; x--) {
      writedata16_cont(color);
    }
    writedata16_last(color);
    if (y > 1 && (y & 1)) {
      endSPITransaction();
      beginSPITransaction(_clock);
    }
  }
  endSPITransaction();
}
 
GOOD WORK KEN!
That does it. Taking 21 & 22 msecs now with restored full BAR DRAW width.
Audio plays good and proper!

It just periodically does END and BEGIN SPI to free the bus.

I'll do a PR ... just home from MRI and needs to ICE ...
 
DONE:
1746666503003.png
 
This is fine for one method for one display, but the correct place to fix this is surely in the Audio library. You’d presumably have the same problem if you wrote a screenful of text, or drew a big filled circle, on an ST7796, or an ILI9341, or…
 
A fix in the Audio library would be ideal, but from what I have read it is unclear if that is even possible and probably not something that would be done in the near-term, if at all.

Most drawing functions like unfilled rectangles, circles and text don't seem to tie up the SPI bus long enough to cause issues from my limited testing. Running audio and creating something like filled audio level bars are a common area to run into this problem and was discovered while running Paul's original Audio Tutorials.

It would probably be nice to at least follow the ILI9341 convention since it has been heavily used over the years and also add this functionality to break-up transactions on any of the fill rectangle subroutines like fillRectHGradient or fillRoundRec.

Other fill routines like circle, triangle don't seem to bother with it. Not sure if it's not needed, or are just unlikely usage scenarios.
 
I had a look at the ILI9341 code, and reckon that there’s an opportunity to improve the API and overhead imposed by the occasional SPI transaction end/begin pair. It would involve a couple of extra SPI library methods:
  • add midTransaction(), almost but not quite the same as end+begin
  • add setMidThreshold(microseconds, doYield = false)
The latter would indicate how long it’s OK to hold the SPI for before doing an end+begin; begin in particular is quite complex so avoiding doing it unnecessarily would be good. A default value of say 2000us could be used, to suit the Audio library, but the user would be able to adjust as needed. It’s not exactly a precision tool, but probably OK.

It also adds the option (default false for backward compatibility) to put a yield() call between end+begin. This is done in other libraries (SD, Wire etc.) - I’m not a huge fan, but apparently we’re stuck with it so I’d say this is another place it “should” be. At least I’m proposing that the user can turn it off…

Display and other long-running SPI code would just use a single call to midTransaction(), as needed. No need to check for how many rectangle rows had been drawn, etc, though “obviously” don’t put it in the inner loop :D
 
Making the change pointed out there was a begin(_clock) argument in the reference code that did not have a variable for in the ST7796 lib - so it was left out, like in the begin() before the loop. Extending the transaction code would add more complexity.

Did a quick search/scan of code that uses the same end&begin and found a couple libs that had it in - one was under "#if 0" in that loop so not active.

Rect is a common thing it seemed called even within the code - though for simple/short things perhaps.

Noticed other elements didn't even seem to wrap in begin/end? But maybe those are all subroutines that are under public call that handles the transaction?

Other than Rect's most things are a smaller series of pixels - not blocks - though if they are wrapped by a transaction it could add up.

Not having the spurious SPI coded in audio would be best when needed less often given SDIO SD and other FS options - but that is a choice for PJRC to offer a better way without resulting in code bloat or breakage.
 
Back
Top