Teensy 4.0 QuadTimer interrupt for TLC5940

AVSTech

Member
Hi everyone!
I am trying to write an add-on for the TLC5940 library for Teensy 4.x. I have already used this library for the Teensy 3.6 platform and am now migrating to the Teensy 4.0 platform.
I am not good at working with registers and it is still very difficult for me to understand counters, timers and interrupts.
Now I manage to light up the LEDs at the setup () stage. But none of this makes sense without interrupts.
Here is my code, which I integrated into the TLC5940 standard library:

Code:
/****************************************** LIBRARIES ******************************************/
#include <Arduino.h>
#//include <SPI.h>
#include <math.h>

/****************************************** DEFINITIONS & MACROS ******************************************/
#define NUM_TLCS          2
#define CHANNELS          16 * NUM_TLCS
#define CHNBITS           CHANNELS * 12
#define CHNBYTES          CHNBITS / 8
#define TLC_CHANNEL_TYPE  uint8_t

#define F_BUS             75000000                          // 30000000 для F_CPU 180-120MHz; 24000000 для F_CPU 96-48MHz; 18000000 для F_CPU 72MHz; 12000000 для F_CPU 24MHz
#define TLC_PORT_RES      12                                // bitness port
#define FREQ              (F_BUS/pow(2, TLC_PORT_RES))*2    // frequency port

#define GS_VAL_INIT       333                        // from 0 to 1023
#define PORT_DUTY         GS_VAL_INIT*4              // GS_VALUE/4GS_VALUE/4 - 1024 transferred to 4096 
#define GS_VALUE_TEST     10                         //Gray Scale value to test


#define SIN_BB            6             //FlexPWM2 Module2      // pin 6, 9
#define SCLK_BB           7             //FlexPWM1 Module3      // pin 7, 8, 25

#define XLAT              19            //QuadTimer3 Module0    // pin 19
#define BLANK             18            //QuadTimer3 Module1    // pin 18
#define GSCLK             5             //FlexPWM2 Module1      // pin 5

/*-------------------------------------------- Pin functions --------------------------------------------*/
#define pulse_pin(port, pin)                digitalWriteFast(pin, HIGH); digitalWriteFast(pin, LOW)
#define set_pin(port, pin)                  digitalWriteFast(pin, HIGH)
#define clear_pin(port, pin)                digitalWriteFast(pin, LOW)
#define output_pin(ddr, pin)                pinMode(pin, OUTPUT)
#define pullup_pin(ddr, port, pin)          pinMode(pin, INPUT_PULLUP)

/** GSCLK pin setup and PWM output*/
#define gsclk_pin_setup(pin, freq, res)     analogWriteFrequency(pin, freq); analogWriteResolution(res);
#define gsclk_pin_pulse(pin, duty)          analogWrite(pin, duty);


/** Enables the output of XLAT pulses */ //TODO for T4
//#define enable_XLAT_pulses()              CORE_PIN3_CONFIG = PORT_PCR_MUX(3)|PORT_PCR_DSE|PORT_PCR_SRE
//#define disable_XLAT_pulses()             CORE_PIN3_CONFIG = PORT_PCR_MUX(1)|PORT_PCR_DSE|PORT_PCR_SRE

#define enable_XLAT_pulses()                digitalWriteFast(XLAT, HIGH); delayMicroseconds(1); digitalWriteFast(XLAT, LOW); // TODO timer on millis???
#define disable_XLAT_pulses()               digitalWriteFast(XLAT, HIGH); delayMicroseconds(1); digitalWriteFast(XLAT, LOW); // TODO timer on millis???


/**Enables the QuadTimer3_0 Overflow interrupt, which will fire after an XLAT pulse */ //TODO for T4
//#define set_XLAT_interrupt()              FTM1_SC = FTM_SC_CLKS(1) | FTM_SC_CPWMS | FTM_SC_TOIE | (FTM1_SC & FTM_SC_PS(7))
//#define clear_XLAT_interrupt()            FTM1_SC = FTM_SC_CLKS(1) | FTM_SC_CPWMS | (FTM1_SC & FTM_SC_PS(7))

#define set_XLAT_interrupt()                // attachInterrupt (pin, func, mode)
#define clear_XLAT_interrupt()              // detachInterrupt(pin);



/****************************************** FUNCTIONS & VARIABLES ******************************************/

//Variables
uint8_t tlc_GSData[NUM_TLCS * 24];
uint16_t initialValue = GS_VAL_INIT;
uint16_t value = GS_VALUE_TEST;
volatile uint8_t tlc_needXLAT;
static uint8_t firstGSInput;

//BitBang SPI
/** Initializes the SPI module */
void tlc_shift8_init(void) {
  output_pin(SIN_BB, SIN_BB);   // SIN as output
  output_pin(SCLK_BB, SCLK_BB); // SCLK as output
  clear_pin(SCLK_BB, SCLK_BB);  // SCLK to LOW
}

/** Shifts a byte out, MSB first */
void tlc_shift8(uint8_t byte) {
  for (uint8_t bit = 0x80; bit; bit >>= 1) {
    if (bit & byte) {
      set_pin(SIN_BB, SIN_BB);
    } else {
      clear_pin(SIN_BB, SIN_BB);
    }
    pulse_pin(SCLK_BB, SCLK_BB);
  }
}


//Interrupts //TODO for T4
////////////////////////////////////////////
/*
  // Interrupt called after an XLAT pulse to prevent more XLAT pulses.
  static inline void Tlc5940_interrupt(void)
  {
    disable_XLAT_pulses();
    clear_XLAT_interrupt();
    tlc_needXLAT = 0;
    if (tlc_onUpdateFinished) {
        sei();
        tlc_onUpdateFinished();
    }
  }

  void ftm1_isr(void)
  {
    uint32_t sc = FTM1_SC;
    if (sc & 0x80) FTM1_SC = sc & 0x7F;
    Tlc5940_interrupt();
  }
*/
///////////////////////////////////////////

volatile void (*tlc_onUpdateFinished)(void);

/** Interrupt called after an XLAT pulse to prevent more XLAT pulses. */
static inline void Tlc5940_interrupt(void)
{
  disable_XLAT_pulses();
  clear_XLAT_interrupt();
  tlc_needXLAT = 0;
  if (tlc_onUpdateFinished) {
    sei();
    tlc_onUpdateFinished();
  }
}

void my_isr(void)
{
  FLEXPWM1_SM3STS = FLEXPWM_SMSTS_RF; //TODO QTIMER3_0
  Tlc5940_interrupt();
}


//Functions TLCs
void setAll(uint16_t value) {
  uint8_t firstByte = value >> 4;
  uint8_t secondByte = (value << 4) | (value >> 8);
  uint8_t *p = tlc_GSData;
  while (p < tlc_GSData + NUM_TLCS * 24) {
    *p++ = firstByte;
    *p++ = secondByte;
    *p++ = (uint8_t)value;
  }
}

void set(TLC_CHANNEL_TYPE channel, uint16_t value) {
  TLC_CHANNEL_TYPE index8 = (NUM_TLCS * 16 - 1) - channel;
  uint8_t *index12p = tlc_GSData + ((((uint16_t)index8) * 3) >> 1);
  if (index8 & 1) { // starts in the middle
    // first 4 bits intact | 4 top bits of value
    *index12p = (*index12p & 0xF0) | (value >> 8);
    // 8 lower bits of value
    *(++index12p) = value & 0xFF;
  } else { // starts clean
    // 8 upper bits of value
    *(index12p++) = value >> 4;
    // 4 lower bits of value | last 4 bits intact
    *index12p = ((uint8_t)(value << 4)) | (*index12p & 0xF);
  }
}

uint8_t update(void) {
    if (tlc_needXLAT) {
        return 1;
    }
    disable_XLAT_pulses();
    if (firstGSInput) {
        // adds an extra SCLK pulse unless we've just set dot-correction data
        firstGSInput = 0;
    } else {
        pulse_pin(SCLK_BB, SCLK_BB);
    uint8_t *p = tlc_GSData;
    while (p < tlc_GSData + NUM_TLCS * 24) {
        tlc_shift8(*p++);
        tlc_shift8(*p++);
        tlc_shift8(*p++);
    }
    tlc_needXLAT = 1;
    enable_XLAT_pulses();
    set_XLAT_interrupt();
    return 0;
}

}


/****************************************** SETUP ******************************************/
void setup() {

  /****************************************** INIT ******************************************/

  output_pin(BLANK, BLANK);
  output_pin(XLAT, XLAT);
  output_pin(GSCLK, GSCLK);
  //output_pin(SCLK_BB, SCLK_BB);
  //output_pin(SIN_BB, SIN_BB);

  set_pin(BLANK, BLANK); // leave blank high (until the timers start)

  tlc_shift8_init();

  setAll(initialValue);
  update();
  disable_XLAT_pulses();
  clear_XLAT_interrupt();
  tlc_needXLAT = 0;
  pulse_pin(XLAT, XLAT);

  //PWM setup
  gsclk_pin_setup (GSCLK, FREQ, TLC_PORT_RES);

  //BLANK_pulse
  set_pin(BLANK, BLANK);
  delayMicroseconds(1);
  clear_pin(BLANK, BLANK);
  delayMicroseconds(1);

  //GSCLK_output
  gsclk_pin_pulse (GSCLK, PORT_DUTY);
  delayMicroseconds(2);

  //Interrupt TODO: must be here
  set_XLAT_interrupt();

  update();

}


void loop() {

  //setAll(GS_VALUE_TEST);

  //for (int channel = 0; channel < 32; channel++) {                    // TODO 32 #define NUM_CHNLS   NUM_TLCS*16
  //  set(channel, GS_VALUE_TEST);                                      // Setting the brightness of all channels from 0 to 1023 (10 bit ADC)
  //}

  //update();

}

I ask to help me with interrupts for Teensy 4.0, and to be more specific, for the timer QuadTimer3 Module0 (see //TODO for T4 in code)
I think it will be useful for the whole community.

Best regards.
 
I didn’t wait for help here.
It turned out to be faster to write own code. But I still did not figure out how to work with registers, so I had to write the code as I can.

Code:
/*
  AVSTech lib for TLC5940 and Teensy 4.0

  TEST

  See original https://github.com/PaulStoffregen/Tlc5940
*/

#include <Arduino.h>
#include <math.h>


/*************************************** DEFINITIONS ***************************************/
#define NUM_TLCS            2
#define NUM_CHNLS           NUM_TLCS * 16
#define TLC_CHANNEL_TYPE    uint8_t

#define PORT_RES              12                                  // resolution port in bits
#define FULL_PERIOD_CNT       pow(2, PORT_RES)                    // 4096 for 12 bits port

#define ALS_RES                10
#define DUTY_RATIO             pow(2, (PORT_RES-ALS_RES))         // GS_VALUE/4GS_VALUE/4 - 1024 transferred to 4096

#define GS_VAL_INIT            0              // Initial value from 0 to 1023 
#define ALS_VALUE_TEST         1              // Gray Scale value from ALSensor (0 to 1023)


//Pinout
#define SIN_BB                  6             //FlexPWM2 Module2      // pin 6, 9
#define SCLK_BB                 7             //FlexPWM1 Module3      // pin 7, 8, 25

#define XLAT                    19            //QuadTimer3 Module0    // pin 19
#define BLANK                   18            //QuadTimer3 Module1    // pin 18
#define GSCLK                   5             //FlexPWM2 Module1      // pin 5


/***************************************** MACROSES *****************************************/
#define set_dig(pin);               pinMode (pin, OUTPUT); digitalWrite (pin, LOW);

#define output_pin(pin)                   pinMode (pin, OUTPUT);
#define set_pin(pin)                      digitalWrite (pin, HIGH);
#define clear_pin(pin)                    digitalWrite (pin, LOW);
#define pulse_pin(pin)                    digitalWrite (pin, HIGH); digitalWrite (pin, LOW);


/***************************************** VARIABLES *****************************************/
uint16_t fullPeriod = FULL_PERIOD_CNT;
uint16_t dutyRatio = DUTY_RATIO;
uint16_t numCH = NUM_CHNLS;

/** Packed grayscale data, 24 bytes (16 pin * 12 bits) per TLC.*/
uint8_t tlc_GSData[NUM_TLCS * 24];

// Counters
volatile uint16_t countGSCLK = 0;     // max value 4096
volatile uint8_t countBLANK = 0;      // max value 3
volatile uint8_t countXLAT = 0;       // max value 1

// Flags
volatile boolean flagGsclk = false;
volatile boolean flagBlank = false;
volatile boolean needXlat = false;

/** Don't add an extra SCLK pulse after switching from dot-correction mode. */
static uint8_t firstGSInput;


/***************************************** PULS FUNC *****************************************/
// Gray Scale Clock
void beginTlc () {
  // Pin Setup
  set_dig(GSCLK);
  set_dig(BLANK);
  set_dig(XLAT);
  clear_pin(GSCLK);
  clear_pin(BLANK);
  clear_pin(XLAT);

  // Flags Setup
  flagGsclk = false;
  flagBlank = true;
  needXlat = true;

  if (!flagGsclk && flagBlank) {

    while (countGSCLK < fullPeriod) {
      pulse_pin(GSCLK);
      countGSCLK = countGSCLK + 1;
    }
    countGSCLK = 0;
    flagGsclk = true;

    if (flagGsclk) {
      if (needXlat) {
        while (countBLANK < 2) {
          set_pin(BLANK);
          countBLANK = countBLANK + 1;
          if (countBLANK == 1) {
            pulse_pin(XLAT);
            countXLAT = countXLAT + 1;
          }
        }
      }
      else {
        while (countBLANK < 3) {
          set_pin(BLANK);
          countBLANK = countBLANK + 1;
        }
      }
      clear_pin(BLANK);
      countBLANK = 0;
      flagBlank = true;
    }
    needXlat = false;
  }
}


/*******************_______________LIBRARY______________*******************/
// Initial TLC5940
void initTlc (uint16_t initialValue) {

  /* Pin Setup */
  output_pin(XLAT);
  output_pin(BLANK);
  output_pin(GSCLK);
  set_pin(BLANK);                 // leave blank high (until the timers start)

  /* Sets all the bit-bang pins to output */
  tlc_shift8_init();

  /* Transfer initial data */
  setAllTlc(initialValue);
  updateTlc();

  /* Disable XLAT pulses inside BLANK*/
  needXlat = false;

  /* First pulse XLAT*/
  pulse_pin(XLAT);

  ////////////////////////////////////////////////////////////
  /* Start first GS/transfer data cycle */
  beginTlc ();
  ////////////////////////////////////////////////////////////
  updateTlc();
}


// Transfer data SIN & SCLK
uint8_t updateTlc(void) {

  /////////
  //TODO
  while (countGSCLK > 3) {
  }
  ////////

  if (needXlat) {
    return 1;
  }
  needXlat = false;
  if (firstGSInput) {
    // adds an extra SCLK pulse unless we've just set dot-correction data
    firstGSInput = 0;
  } else {
    pulse_pin(SCLK_BB);
  }
  uint8_t *p = tlc_GSData;
  while (p < tlc_GSData + NUM_TLCS * 24) {
    tlc_shift8(*p++);
    tlc_shift8(*p++);
    tlc_shift8(*p++);
  }
  needXlat = true;
  return 0;
}

// Data packing
void setAllTlc(uint16_t value) {
  //value = value * dutyRatio;
  uint8_t firstByte = value >> 4;
  uint8_t secondByte = (value << 4) | (value >> 8);
  uint8_t *p = tlc_GSData;
  while (p < tlc_GSData + NUM_TLCS * 24) {
    *p++ = firstByte;
    *p++ = secondByte;
    *p++ = (uint8_t)value;
  }
}

void setTlc(TLC_CHANNEL_TYPE channel, uint16_t value) {
  TLC_CHANNEL_TYPE index8 = (NUM_TLCS * 16 - 1) - channel;
  uint8_t *index12p = tlc_GSData + ((((uint16_t)index8) * 3) >> 1);
  if (index8 & 1) { // starts in the middle
    // first 4 bits intact | 4 top bits of value
    *index12p = (*index12p & 0xF0) | (value >> 8);
    // 8 lower bits of value
    *(++index12p) = value & 0xFF;
  } else { // starts clean
    // 8 upper bits of value
    *(index12p++) = value >> 4;
    // 4 lower bits of value | last 4 bits intact
    *index12p = ((uint8_t)(value << 4)) | (*index12p & 0xF);
  }
}

void clearTlc(void) {
  setAllTlc(0);
}

//BitBang SPI
/** Sets all the bit-bang pins to output */
void tlc_shift8_init(void) {
  output_pin(SIN_BB);   // SIN as output
  output_pin(SCLK_BB);  // SCLK as output
  clear_pin(SCLK_BB);   // SCLK no pulse
}

/** Shifts a byte out, MSB first */
void tlc_shift8(uint8_t byte) {
  for (uint8_t bit = 0x80; bit; bit >>= 1) {
    if (bit & byte) {
      set_pin(SIN_BB);
    } else {
      clear_pin(SIN_BB);
    }
    pulse_pin(SCLK_BB);
  }
}


void setup() {
  Serial.begin (9600);
  initTlc (GS_VAL_INIT);
}


void loop() {
  beginTlc ();                                            // Required TLC5940 for Teensy 4.x
  for (int channel = 0; channel < numCH; channel++) {  
    setTlc(channel, ALS_VALUE_TEST * dutyRatio);
  }

  setTlc (10, 10 * dutyRatio);                              // BT_L1
  updateTlc ();
}


Maybe someone will come in handy.
Personally, everything works for me in this form.
 
Paul, thank you so much!
This is great! Thank you for your support and help!
I'm going to try the library today! I will let you know the results!
 
It turned out to be faster to write own code. But I still did not figure out how to work with registers, so I had to write the code as I can.

Maybe someone will come in handy. Personally, everything works for me in this form.

Nice work. And now you have an official update from Paul, too.
 

Paul, hi!
Thank you for your work.
It's a big step towards promoting the Teensy platform.

However, I have one more big favor to ask of you.
How do I set up the QuadTimer for pins 19 (QuadTimer3 Module0) and 18 (QuadTimer3 Module1) for XLAT and BLANK respectively?
I understand that FlexPWM channel A and B have been selected for synchronization. But in my case all FlexPWM pins are busy with either CAN or SPI for MCP23S17 and ST7735 or other functions.
Can you help to solve this problem? Maybe you can tell me where I can see code examples?

Thank you!
 
Last edited:
How do I set up the QuadTimer for pins 19 (QuadTimer3 Module0) and 18 (QuadTimer3 Module1) for XLAT and BLANK respectively?

Realistically, you don't.


I understand that FlexPWM channel A and B have been selected for synchronization. But in my case all FlexPWM pins are busy with either CAN or SPI for MCP23S17 and ST7735 or other functions.

I'm confident you'll find a way to reassign pins. It may be painful, but nowhere near as difficult as trying to program the QuadTimer hardware.


Can you help to solve this problem?

No, absolutely not. A massive backlog of software tasks are pending. TLC5940 was on that list because it's been requested 6 times (though 2 of those 6 were yours).

My work on TLC5940 yesterday is as much time as I can dedicate to this old library.


Maybe you can tell me where I can see code examples?

As far as I know, there aren't any (of the type of waveforms Tlc5940 needs). But I haven't spent much time (really, not any) looking at NXP's SDK timer examples, so it's theoretically possible such code exists and I just don't know of it.

It's also quite likely the quad timers simply may not be up to this task. The original AVR code uses a feature called "phase correct" PWM. All the AVR timers have it. The FTM and FlexPWM timers are also well documented as offering this feature. For FlexPWM it's called "center aligned PWM" on page 3095. I ended up using a slightly different approach than page 3095 based on unsigned integers, but the point is FlexPWM offers this capability which the original AVR design uses.

As far as I can tell from the Quad Timer documentation, the hardware simply does not have this same ability. I don't want to say it's impossible. Maybe someone with a lot of time to experiment and a lot of ingenuity to configure the hardware in a way very different than any of the cases NXP documents might be able to get it working. But there's also a strong chance the end result would be concluding the Quad Timer hardware simply can't create those center aligned pulses. The only thing I can say is I have carefully read the Quad Timer chapter and I have experience using these timers with analogWrite / analogWriteFrequency, OctoWS2811, and AudioInputAnalog, and I do not see a way to use the Quad Timers to create the same waveforms.

Normally I don't like to discourage creative experimentation, but in this case my opinion is use of Quad Timers just isn't realistic.
 
Is it really impossible to set the timers so that after 4096 clock cycles GSCLK there are two signals BLANK and XLAT, the latter with interruption? For example like this:
Start GSCLK, count down 4096 clock cycles.
Turn on GSCLK interrupt
Activate the BLANK (High) signal during 3 clock cycles (4097-4099 clock cycles)
Initiate XLAT signal (4098 clock cycle) if "needXLAT" flag is set
Disable GSCLK interrupt
The cycle restarts

Anyway, thank you very much!
 
Back
Top