Teensy 4: TFT doesn't work together with encoder when in DMA mode

Status
Not open for further replies.

Codepoet

Member
Hello everybody,

I've been working on a project which is based on a Teensy 4.0 and that uses a linear encoder (AMS AS5311) with a magnet strip together with a TFT to show the actual value (ST7789). The Adafruit 1.14" TFT is used in Hardware SPI mode.
The setup is as follows:
The linear encoder chip moves along the magnet strip and every 20ms the current position value is shown on the display. Besides that a button is attached to set the zero position.
For better testing I've build a jig with a digital caliper where the linear encoder chip is mounted on the moving jaw.
I use the hardware quadrature library from Mike https://github.com/mjs513/Teensy-4.x-Quad-Encoder-Library and the TFT library ST7735_t3
https://github.com/PaulStoffregen/ST7735_t3. This lib offers the possibility to work with Frame buffering/DMA.
As you may see in the screenshot the code works pretty good. But now comes the problem. When is use Framebuffering/DMA for the display the encoder totally freaks out. The counter becomes wrong and after 2-3 seconds evrything freezes.
If framebuffering/DMA isn't used, everything works fine except that the display flickers. I also tried to use Pauls encoder library https://www.pjrc.com/teensy/td_libs_Encoder.html instead of the hardware encoder lib, but the problem remains the same.

Teensy_Encoder_Proj.jpg

Does anyone know what I'm doing wrong?

Thanks for your help.
Michael

This is my code so far:
Main.cpp

Code:
#include <Arduino.h>
#include "Button.h"
#include "Screen.h"
#include <EEPROM.h>
#include "QuadEncoder.h"

void ButtonPressed(); // Called when the ZERO button was pressed.

// == Variables & Constants ===========
QuadEncoder posEnc(1, 1, 0); // PinA and PinB is swapped so the value becomes positiv when moving to the right

Button btnZero(3, ButtonPressed); // Use Pin 3 as Interrupt and handle button press in ButtonPressed
Screen scr;                       // TFT Display
elapsedMillis valueChangeTime;    // update position in certain intervals only

int32_t posCounter = 0; // Encoder-Position-Counter
float position = 0.0;   // Position in mm

static const uint8_t VALUE_UPDATE_TIME = 20;    // Milliseconds
static const uint8_t MAGNET_WIDTH = 2;          // Magnet pitch 2mm
static const uint16_t MAGNET_RESOLUTION = 1024; // 1024 pulses

/*  ===============================
/   Configuration of all needed  
/   components
/   ===============================*/
void setup()
{
  Serial.begin(115200);

  while (!Serial)
    ; // Wait until serial is ready

  Serial.println("Ready..");

  scr.Begin(); // Start TFT display

  posEnc.setInitConfig(); //Loads default configuration for the encoder channel
  posEnc.init();          //Initializes the encoder channel
}

/*  ===============================
/   Endless loop 
/   
/   ===============================*/
void loop()
{
  if (valueChangeTime >= VALUE_UPDATE_TIME)
  {
    posCounter = posEnc.read();

    // Calc position in millimeters
    position = MAGNET_WIDTH / (float)MAGNET_RESOLUTION * posCounter;

    Serial.println(posCounter);
    // Show value on display
    scr.ShowValue(position);

    valueChangeTime = 0; // Reset interval
  }
}

/*  ===============================
/   Handle button press. 
/   Sets counter back to zero
/   ===============================*/
void ButtonPressed()
{
  // Zero position
  posEnc.write(0);
}

Screen.h

Code:
#ifndef _SCREEN_h
#define _SCREEN_h

#if defined(ARDUINO) && ARDUINO >= 100
#include "arduino.h"
#else
#include "WProgram.h"
#endif

class Screen
{
public:
    void Begin();
    void ShowValue(float distance);

private:
    void DrawUnit();
    void CalcMeasValueTextSize();
    void SetMeasValueFont();
   
};
#endif

Screen.cpp

Code:
#include "Screen.h"
#include "ST7789_t3.h"
#include "st7735_t3_font_Arial.h"
#include <SPI.h>

// Use Hardware SPI on Teensy 4.0
#define TFT_T4_DC 6
#define TFT_T4_CS 10
#define TFT_T4_RST 8

// -- Colors
#define COLOR_BACKGROUND 0x0841 // Grey
#define COLOR_SEPARATOR 0x73AE  // Light Blue
#define COLOR_UNIT 0x73AE
#define COLOR_MEAS_VALUE 0xFFFF // White

// ---
const String UNIT = "mm";
const uint16_t DISPLAY_HEIGHT = 135; // 1.14" Display
const uint16_t DISPLAY_WIDTH = 240;

bool isMeasValFont = false;
uint16_t measValTextWidth;
uint16_t measValTextHeight;
String lastMeasValue = "";

ST7789_t3 tft = ST7789_t3(TFT_T4_CS, TFT_T4_DC, TFT_T4_RST);

/**
 * Setup TFT display
 */
void Screen::Begin()
{
    tft.init(DISPLAY_HEIGHT, DISPLAY_WIDTH); // Set dimensions
    //tft.useFrameBuffer(true);                // use buffering with DMA for speed reasons
    tft.fillScreen(COLOR_BACKGROUND); // Set background to black
    tft.setRotation(3);               // Use Landscape orientation

    tft.setTextWrap(false);
    tft.setTextDatum(TR_DATUM); // set Reference point for text drawings to TOP RIGHT (default is TOP LEFT)

    // draw horizontal separator
    tft.drawFastHLine(0, DISPLAY_HEIGHT * 2 / 3, DISPLAY_WIDTH, COLOR_SEPARATOR);

    // Show initial values
    DrawUnit();
    ShowValue(0.0); // Must be done after DrawUnit, cause then we do not need to set Font and color each time (faster)
}

/**
 * Show new measurement value on the TFT
 */
void Screen::ShowValue(float distance)
{
    // Buffer for distance as string. max. distance = 6 chars + NULL at the end
    char strBuf[7] = "";
    // convert float to string with 2 decimal digits, e.g. "12.34"
    dtostrf(distance, 6, 2, strBuf);

    if (!isMeasValFont)
    {
        // Set correct font
        SetMeasValueFont();
        CalcMeasValueTextSize();
    }

    // Only draw if the given value differs from new value
    if (lastMeasValue != strBuf)
    {
        // "erase" last drawing
        tft.fillRect(0, 15, DISPLAY_WIDTH, DISPLAY_HEIGHT * 2 / 3 - 30, COLOR_BACKGROUND);
        // Show new value
        tft.drawString(strBuf, DISPLAY_WIDTH - 40, (DISPLAY_HEIGHT * 2 / 3 - measValTextHeight) / 2);
        //tft.updateScreenAsync();

        lastMeasValue = strBuf;
    }
}

/**
 * Draw unit indicator, e.g. mm
 */
void Screen::DrawUnit()
{
    tft.setTextColor(COLOR_UNIT, COLOR_BACKGROUND);
    tft.setFont(Arial_24);

    isMeasValFont = false;

    // calculate position after setting font
    int16_t x, y;
    uint16_t txtHeight, txtWidth;

    tft.getTextBounds(UNIT, 0, 0, &x, &y, &txtWidth, &txtHeight);

    tft.drawString(UNIT, (DISPLAY_WIDTH + txtWidth) / 2, DISPLAY_HEIGHT * 5 / 6 - txtHeight / 2);

    //tft.updateScreenAsync();
}

/**
 * Calculate text metrics for msx. possible measurement value
 */
void Screen::CalcMeasValueTextSize()
{
    if (!isMeasValFont)
    {
        SetMeasValueFont();
    }

    // calculate position after setting correct font
    int16_t x, y;
    char tmp[7] = "999.99"; // for ease of use we stay with the metrics of the max. (widest) possible number

    tft.getTextBounds(tmp, 0, 0, &x, &y, &measValTextWidth, &measValTextHeight);
}

/**
 * Set color and font for measurement value
 */
void Screen::SetMeasValueFont()
{
    tft.setFont(Arial_40);
    tft.setTextColor(COLOR_MEAS_VALUE, COLOR_BACKGROUND);

    isMeasValFont = true;
}

Button.h
Code:
#ifndef _BUTTON_h
#define _BUTTON_h

#if defined(ARDUINO) && ARDUINO >= 100
#include "arduino.h"
#else
#include "WProgram.h"
#endif

class Button
{

public:
	Button(); // Constructor (If no interrupt is used, PIN must be a constant value, see NO_INTERRUPT_BUTTON_PIN)
	Button(uint8_t interruptPin, void (*btnPressedCallback)());
	bool IsPressed(); // Indicates if button was pressed

	void IsrButtonPressed(); // ONLY FOR INTERNAL USE.
private:
	static const uint8_t NO_INTERRUPT_BUTTON_PIN = 3; // Must be const otherwise digitalReadFast isn't fast
	uint8_t _interruptButtonPin;					  // Pin where button is connected to when interrupt is used
	uint8_t _buttonState = HIGH;					  // the current reading from the input pin. At beginning HIGH when pin is pulled up
	uint8_t _lastButtonState = HIGH;				  // the previous reading from the input pin. At beginning HIGH when pin is pulled up

	uint32_t _lastDebounceTime = 0; // the last time the output pin was toggled
	uint32_t _debounceDelay = 20;	// the debounce time
	uint32_t _lastIsrCall;			// time when ISR was lastly called

	void (*BtnPressed)(); // Callback to handle button pressed
};

#endif

Button.cpp

Code:
Button *ptrButton;

/*
/	Global interrupt handler (must be global and outside of the class)
*/
void GlobalInterruptHandler()
{
	// reroute to function inside of the class
	ptrButton->IsrButtonPressed();
}

#include "Button.h"

/* --------------------------------------------------------------
Constructor for use without interrupts
		The pin for the button must be const, otherwise digitalReadFast
		isn't faster as normal digitalRead
-------------------------------------------------------------- */
Button::Button()
{
	pinMode(NO_INTERRUPT_BUTTON_PIN, INPUT_PULLUP);
}

/* --------------------------------------------------------------
Constructor for use with interrupts
-------------------------------------------------------------- */
Button::Button(uint8_t interruptPin, void (*btnPressedCallback)())
{
	ptrButton = this;

	_lastIsrCall = 0;
	_interruptButtonPin = interruptPin;

	pinMode(_interruptButtonPin, INPUT_PULLUP);
	BtnPressed = btnPressedCallback;

	//Pull pin high and use it as input
	pinMode(interruptPin, INPUT_PULLUP);
	//Attach global interupt callback
	attachInterrupt(interruptPin, GlobalInterruptHandler, FALLING);
}

/* --------------------------------------------------------------
Indicates if the button was pressed
		Button is debounced via software
-------------------------------------------------------------- */
bool Button::IsPressed()
{
	bool isPressed = false;

	uint8_t reading = digitalReadFast(NO_INTERRUPT_BUTTON_PIN);

	// check to see if you just pressed the button
	// (i.e. the input went from HIGH to LOW),  and you've waited
	// long enough since the last press to ignore any noise:

	// If the switch changed, due to noise or pressing:
	if (reading != _lastButtonState)
	{
		// reset the debouncing timer
		_lastDebounceTime = millis();
	}

	if ((millis() - _lastDebounceTime) > _debounceDelay)
	{
		// whatever the reading is, it's been there for longer
		// than the debounce delay, so take it as the actual current state

		// if the button state has changed:
		if (reading != _buttonState)
		{
			_buttonState = reading;

			// Pin is pulled up, so LOW means pressed
			if (_buttonState == LOW)
			{
				isPressed = true;
			}
		}
	}

	// save the reading.
	_lastButtonState = reading;

	return isPressed;
}

/*
/	Handle button pressed and call thr user defined callback function
*/
void Button::IsrButtonPressed()
{
	// Debounce button --> when button is pressed signal goes low
	// debouncing : React directly to the first falling edge and again only after a waiting period of
	if (millis() - _lastIsrCall < 20)
	{
		return;
	}

	_lastIsrCall = millis();
	BtnPressed(); // call user defined callback
}
 
Hi codepoet,

i am not that good in programming and this kind of c++ Style
So bare with me.

So technically it’s an encoder program that only should work if the encoder is turned or moved nd should be resetted is a pushbutton is pressed and restarts from zero.

did the encoder work freely without the display? Did you test that?
Did you run an up and down counter on the screen let the gauge spin like a clock, just to test the display.
Why don’t you use millis to measure the elapsed time and then reset it if a pushbutton is pressed, maybe the program is to tight for other processes to occur, and freezes over.

I have build menus with encoder and pushbuttons that are skipped through back and forth with single and double click.
Parallel I have a program that uses the values from the previous menu function to print on the screen where I am.

Use SPI.BeginTransaction(,,);
EndTransaction(); ( I didn’t found it directly in your code, maybe I missed it)

which libraries are you using from whom?
stick with the encoder from Paul or google other linear encoder examples arduino/teensy
I use libraries from mostly teensy. My total code is ( pjrc search Max31855;Bastiaan

use Serial flush As I will start with next week in debugging my code
It will slow down the program and allows you to search more precise where the problem lies.

Best of luck!
 
I never used the TFT library and can be completely wrong but doesn't the API suggests to call waitUpdateAsyncComplete(void) before you do a new update? https://github.com/PaulStoffregen/ST7735_t3#asynchronous-update-support-frame-buffer[

You can wait for the update to be completed, but you‘re not forced to. Nevertheless even when I use „updateScreen“ instead of the Async variant it doesn’t work. It is all about the use of frame buffer (useFrameBuffer(True)).
 
Note: the library only does DMA if you call the async to update.

And yes you should not start up a new update until the previous one failed. There is code in there to try to fail the update if an update is already active. BUT I am not sure how much we tested to make sure it caught things quick enough.

That is the code tries to initialize the DMA settings and the like before it checks to see it is already running. Some of this should be rearranged to test early and bail.

The other issue with updating screen while it is already updating, is that you are updating the same memory with the TFT operations that is being used to write the state of data to the screen. So I usually catch that state earlier on. Like if I am wanting to update the screen, I wait for previous one to complete right at the start of the code that wants to update the new state... Like maybe right at the start of: Screen::DrawUnit()

Note: updateScreen - Does not use DMA and it completes all of its stuff before it returns. Logically it is more or less the same as doing writeRect of the whole screen from a memory bitmap.

There is some screwyness with the Async (DMA) support in that this still does interrupts and copy memory from the DMAMEM to normal memory to take care of issues where you might do continuous updates and issues of cache not equal to memory and then the actual memory... With some of the libraries I have updated to remove this copy of memory, which helps performance especially if other things are going on. Again not sure if that would help here or not.

As for the Quad library? I am not sure of any resources that are common between the two? I know it uses XBar stuff, I don't think we use it in the ST... library.

Again updateScreen without the Async does nothing of this nature. It just does:
Code:
void ST7735_t3::updateScreen(void)					// call to say update the screen now.
{
	// Not sure if better here to check flag or check existence of buffer.
	// Will go by buffer as maybe can do interesting things?
	if (_use_fbtft) {
		beginSPITransaction();
		// Doing full window. 
		setAddr(0, 0, _width-1, _height-1);
		writecommand(ST7735_RAMWR);

		// BUGBUG doing as one shot.  Not sure if should or not or do like
		// main code and break up into transactions...
		uint16_t *pfbtft_end = &_pfbtft[(_count_pixels)-1];	// setup 
		uint16_t *pftbft = _pfbtft;

		// Quick write out the data;
		while (pftbft < pfbtft_end) {
			writedata16(*pftbft++);
		}
		writedata16_last(*pftbft);

		endSPITransaction();
	}
}
It just does a steady stream of output over SPI. @mjs513 maybe able to describe more about the Quad code.

I will take a look to see how hard to convert the Async code of update to what is now done in ili9341_t3n...
 
Morning All
I spent the morning looking over the 2 libraries and there shouldn't be any conflicts that I can see. So while I don't have the same encoder and screen that you have I hooked up a Adafruit ST7735 128x128 display with a simple rotary encoder on a T4.0:
IMG-0259 (1).jpg
I loaded up you sketch and except for changing the configuration of the display from a ST7789 to a ST7735 I used your same pins. NOTE: If you are using a ST7735 you probably should change it as well. Ran the sketch with and without asynchScreenUpdates/useFrameBuffer enabled and could duplicate your issue where the encoder went crazy.

So not sure what is going on with your setup.
 
Thanks to all of you trying to help me. I highly appreciate that.

@KurtE: I reenabled frame buffering, async update and added waitUpdateAsyncComplete() to all functions updating the TFT, namely Screen::ShowValue and Screen::DrawUnit. Right at the beginning of those functions. At first glance I thought that would do the trick. It now doesn't happen every time as before but it still happens every now and then. It's not predictable when it is going to happen, moving the sensor fast or slow doesn't make any difference.

@mjs513: Wow, thanks for diving so deep into it ! Did I get you right, that you ran into the same problem? I really use a ST7789, so no need to change to ST7735.

Hmm, still confused with this problem. I can live with the flickering when used without frame buffering, but it of course would be a lot nicer without it.
Michael
 
@mjs513: Wow, thanks for diving so deep into it ! Did I get you right, that you ran into the same problem? I really use a ST7789, so no need to change to ST7735.
I couldn't duplicate the problem you are seeing with my setup at all. Screen updated no problem. No problem with the config then - thought you were using a ST7735. Have to see if I can find my ST7789 and re-run the sketch. May take a while.

EDIT: Ok - hooked up the ST7789 240x320 display. Couldn't duplicate your issue. You could try just printing to screen every x-ms to see what happens?
 
Last edited:
Frame buffer can make it easier to work work without flicker... Of course you again do this without using DMA. just use tft.updateScreen() instead.

Likewise you can probably write all of this without using frame buffer and not have it flicker as well.

Sorry I am sort of lazy this morning and as such will just outline the code.

I think you are centering the new value... The steps I would take if I did not want to use the frame buffer would be:

You all ready get the text extent of the text you are going to output.

So, the steps I would do include:
a) Set text color to do Opaque output which I think you are already doing:
b) If the new text x extent is longer than previous text output, you can just output it and be done.
c) else, need to do a couple fill rects, one at the start and one at the end...
That is if the previous text was 160 long and the new is 120, and screen width is 240
fillRect(40, y start, 20, height, BACKGROUND COLOR) // start of previous output to start of new output
set cursor (60, y...)
tft.write(...)
fillRect(180, ..., 20, ... ) // fill from end of new text to end of previous text...

Hope that makes sense.

Edit: Forgot to mention, that one issue that you might run into with Opaque text is that it wants to write the background color down to what would be the start of the next line of text. When you are doing it within fields and the like, this can overwrite things you don't want to overwrite, like maybe the horizontal line between the number and units...

An easy way to handle this is by setting a clip Rectangle. Could be simply setup for top of screen full width to just above the horizontal line and then your update will only update this region.
tft.setClipRect(x, y, w, h);

I believe to clear is just tft.setClipRect();
 
Last edited:
@KurtE: I incorporated your suggestions, some of them where already done, and it now is much more smoothly even without DMA/ASYNC.

Thanks to all of you. Both libs are absolutely great!
Michael
 
Status
Not open for further replies.
Back
Top