Frequency counting using the Teensy 4.1 GPT1 and GPT2 timers

Status
Not open for further replies.

SteveW

Active member
Just wonder if the experts here could clarify some details for me regarding use of the Teensy 4.1 GPT1/2 timers for a frequency counter I’m making. I was inspired to play with the timers by the recent Pendulum Clock timing thread here:

https://forum.pjrc.com/threads/65156-Pendulum-clock-accuracy-GPSDO-10Mhz-1Hz-pulse

Following links from there to the helpful manitou48 GitHub page gave me some further useful timer code examples:

https://github.com/manitou48/teensy4

Anyway I wrote some test code to run on a VFD display test board I’d just made. Forgive the less than optimised hack version. The code was initially written to count a 10MHz test signal using GPT2 via pin 14 as described in the Pendulum Clock thread. The divided count is output to the VFD as hours, mins and seconds elapsed time. In the long term there will be an input from a frequency standard, rubidium or GPSDO. I was pleased the example code ran well. I then added a second counter, feeding GPT1 the same clock via pin 25. A second count from GPT1 was added to my data display to show both timers were working as expected, they both count 10MHz simultaneously no problems. I’ve pasted in the two different approaches to timer to pin mapping from the manitou48 examples for each GPTn timer, see my two separate start functions.


Code:
// ---------------------------------------------------------------------------------------------------------------------------------------------
// Counter/VFD display test code 18/03/21
//
// For Hitachi interfaced VFD display
// Uses modified version of Arduino LiquidCrystal library to resolve display cold reset problems if standard library used
//
// Hardware:
// Teensy 4.1
// Noritake CU20029_UW1J VFD display 2 links made to enable European character set(JP5) plus JP3, connects reset to pin 3 of CN2
//
// Teensy display pins: 0 RS, 1 RW/WR (optional use),2 E/RD, 3..6 D4-D7, 7 Reset
// Teensy counter i/o:  14 GPT2 clock in, 25 GPT1 clock in, 15 test signal 10 MHz out
//
// -----------------------------------------------------------------------------------------------------------------------------------------------

#include "Arduino.h"
#include <VFD_Display.h>                            // Modified LiquidCrystal lib allows for hardware reset of VFD before inititalisation, adds 500ms wait also
                                                   // Hardware initialisation by constructor removed, all display intialisation is via begin() method
                                                   // Needs a hardware connection to reset pin. Additional reset method added to modified library: reset(resetPin).
#define LED 13                                      // Teensy LED on 13
#define WR_DISPLAY 1                                // Display read/write, normally ground when only writes used
#define RESET_PIN 7                                 // Pin 7 connected to VFD display reset (display pin 3 with JP3 shorted), low resets the VFD


VFD_Display vfd(0, 2, 3, 4, 5, 6);                  // Constructor parameters for VFD library version same as for standard LiquidCrystal library
                                                   // 6 wire display interface used: 0=RS,(pin 1 RW/WR not used here,set low),2=E/RD,3..6=D4-D7
elapsedMillis readTimer = 0;


void setup()
{
 pinMode(LED, OUTPUT);
 pinMode(15,OUTPUT);
 pinMode(WR_DISPLAY, OUTPUT);
 digitalWrite(WR_DISPLAY,LOW);
 vfd.reset(RESET_PIN);                                 // Additional library method provides full hardware reset of display with 500 ms delay afterwards
 vfd.begin(20, 2);                                     // begin() method does all hardware initialsation, should be called following a reset of VFD
 vfd.setCursor(0, 0);

 //analogWriteFrequency(11, 10000000);                  // for separate freq source GPT1 can test jumper 10MHz to its clock pin 25
 //analogWrite(11, 128);

 analogWriteFrequency(15, 10'000'000);                 // generate 10Mhz on pin15, jumper pin 15 to pin14 for GPT2 on pin 14 testing
 analogWrite(15, 128);

 vfd.print("Elapsed time:");

 startGPT2Counter();                                   // Sets up GPT2 with clock input on pin 14
 startGPT1Counter();                                   // Sets up GPT1 with clock input on pin 25
 readTimer = 501;
}

void loop()
{

 if (readTimer > 300)
   {
       uint64_t count1 = readGPT1Counter();
       double secondsGPT1 = count1 / 10E6;
       uint64_t count2 = readGPT2Counter();
       double timeInSeconds = count2 / 10E6;
       readTimer = 0;
                                                             // set the cursor to column 0, line 1
       uint8_t mins = timeInSeconds/60;
       float seconds=timeInSeconds-(mins*60);
       uint8_t hrs = mins/60;

       displayCounters(hrs,mins,seconds,secondsGPT1);
       flashLED();
   }

}   //loop

void displayCounters(uint8_t hrs,uint8_t mins,float seconds,double secondsGPT1)
{
 vfd.setCursor(0, 1);                                 // print the elapsed time since reset:
 if (hrs<10)  vfd.print(0);
 vfd.print(hrs);
 vfd.print(":");
 if (mins<10)  vfd.print(0);
 vfd.print(mins);
 vfd.print(":");
 if (seconds<10)  vfd.print(0);
 vfd.print(seconds,1);
 vfd.print("  1:");
 vfd.print(secondsGPT1,0);
       //Serial.printf("timestamp: %fs  |  %ull cts \n", timeInSeconds, count);
}

void flashLED(void)
{
 static bool statusLED = false;
 statusLED=(statusLED?false:true);
 digitalWrite(LED,statusLED);                  // turn the LED on or off alternately
}

void startGPT2Counter(void)
{
   CCM_CCGR0 |= CCM_CCGR0_GPT2_BUS(CCM_CCGR_ON);

   GPT2_CR = 0;
   GPT2_SR = 0x3F;                              // clear all prior status
   GPT2_CR = GPT_CR_CLKSRC(3);                  // 3 external clock

   IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_02 = 8;     // select input pin (14)
   IOMUXC_GPT2_IPP_IND_CLKIN_SELECT_INPUT = 1;

   GPT2_CR |= GPT_CR_EN;                        // Enable GPT2
}

void startGPT1Counter(void)
 {
   CCM_CCGR1 |= CCM_CCGR1_GPT(CCM_CCGR_ON) ;        // enable GPT1 module
   GPT1_CR = 0;
   GPT1_SR = 0x3F; // clear all prior status
   GPT1_CR =  GPT_CR_CLKSRC(3);                     // | GPT_CR_FRR ;    // 3 external clock
   *(portConfigRegister(25)) = 1;                   // ALT 1  Clock pin is 25
   GPT1_CR |= GPT_CR_EN;                            // enable GPT1
 }

uint64_t readGPT2Counter(void)
{
   static uint64_t curTime = 0;
   static uint32_t lastCnt = GPT2_CNT; // initialize on first call to current counter value

   uint32_t now = GPT2_CNT;            // extend 32bit counter to 64 bit (need to call readCounter at least once per 7min)
   curTime += (now - lastCnt);
   lastCnt = now;

   return curTime;
}

uint64_t readGPT1Counter(void)
{
   static uint64_t curTime = 0;
   static uint32_t lastCnt = GPT1_CNT;    // initialize on first call to current counter value

   uint32_t now = GPT1_CNT;               // extend 32bit counter to 64 bit (need to call readCounter at least once per 7min)
   curTime += (now - lastCnt);
   lastCnt = now;

   return curTime;
}

Output:

7A175E08-AD55-4278-8FCA-72BD12C744AC.jpg


Anyway, I’m not very knowledgeable about low level ARM hardware, wonder if anyone can help with these questions arising from the interesting exercise to date.

The setup routine for GPT2 includes:

IOMUXC_SW_MUX_CTL_PAD_GPIO_AD_B1_02 = 8; // select input pin (14)
IOMUXC_GPT2_IPP_IND_CLKIN_SELECT_INPUT = 1;

Whereas the example code for GPT1 uses:

*(portConfigRegister(25)) = 1; // ALT 1 Clock pin is 25

I’m thinking setting the two IOMUXC... library derived variables does the same job as the one line of GPT1 setup code but I’m not totally clear that I can edit either to select a different input pin . GPIO is clearly being mapped to the ARM pin that connects to Teensy 14, but how do we know to use pad? 8 of the ARM? How would I change this line to map other pins, that might help me understand the code better! The second method is succinct, I don’t quite understand the portConfigRegister() function though, I’m wondering if I can substitute any digital pin for 25? Any disadvantage to this later approach?

My next step will be to set up a high resolution period based low frequency counter, I’m interested in being able to measure a 32kHz xtal output accurately. The plan is to use one counter to count the low frequency input so I can average things over multiple cycles, minutes total, using the input signal counter to trigger reads of the 10MHz frequency standard count. I have no idea how to set up one of the counters with a preload so that I can time the input over a set period. Also I’m not sure how to then make the GPT counter generate an interrupt when it hits overflow, any guidance welcomed.

Finally - if I start the sampling counter using say: GPT2_CR |= GPT_CR_EN; immediately after reading the standard counter, what sort of latency error am I likely to see, I’d have thought coding this for a 600 MHz ARM processor renders the potential error pretty tiny?

Thanks to anyone who can clarify some of the above for me, it will help my understanding of Teensy timing code considerably I’m sure,

Steve
 
The pins that T4 GPT uses are defined in table 52-2 of the reference manual. Look at the T4.0 and T4.1 schematics to determine which pins are actually available for the teensy PCBs. You'll need IOMUXC_GPTx_IPP_IND_CLKIN_SELECT_INPUT to select alternate pins from table 52-2. GPT interrupts are described in section 52.5.2.3. Interrupts are useful only up to about 5 MHz on the T4.
 
The pins that T4 GPT uses are defined in table 52-2 of the reference manual. Look at the T4.0 and T4.1 schematics to determine which pins are actually available for the teensy PCBs. You'll need IOMUXC_GPTx_IPP_IND_CLKIN_SELECT_INPUT to select alternate pins from table 52-2. GPT interrupts are described in section 52.5.2.3. Interrupts are useful only up to about 5 MHz on the T4.


Thanks so much. I’ve realised that the way forwards here was to gain a better understanding of the counters and yesterday I downloaded the IMXRT1060 manual from nxp, read a lot since. I was impressed by its clarity, it’s a way more complex processor than I’ve met before yet section 52 is very understandable. I’ve also learned more now about pin mapping on the processor, including the ALT options. Plus realising that unlike PICs which is where I came in that you don’t preload a counter to get a defined interval , you count upwards and compare.

I’ve today written some code based on your ISR setup method on GitHub, tho I’m setting up a rollover interrupt instead of capture compare type as in your code there. Impressed by the number of timer interrupt choices for a simple timer, this processor outclasses anything I’ve met before. Anyway that’s where I am now, I think we’ve probably got an interrupt as the Teensy crashes exactly on cue at the 7 min rollover time now :). Just need to work on why it’s disappearing into a black hole now. Anyway this code below is as far as I’ve got, it locks the Teensy on rollover tho repowering regains control. Guess something bad is happening in the interrupt vectoring

Code:
void startGPT1Counter(void)
 {
   CCM_CCGR1 |= CCM_CCGR1_GPT(CCM_CCGR_ON) ;         // enable GPT1 module
   GPT1_CR = 0;
   GPT1_SR = 0x3F; // clear all prior status
   GPT1_CR =  GPT_CR_CLKSRC(3);                      // | GPT_CR_FRR ;    // 3 = external clock
   *(portConfigRegister(25)) = 1;                    // ALT 1 Clock pin is 25
   startGPTinterrupt(GPT1);                          // Enables overflow interrupt on GPTn (GPT1 #define = 1)
   GPT1_CR |= GPT_CR_EN;                             // enable GPT1
 }

void startGPTinterrupt(uint8_t GPTn)                  // Sets up GPT 1/2 rollover interrupt, call before enabling timer
{
   if (GPTn == 1)
     {
       GPT1_IR |= GPT_IR_ROVIE;                      // Set the GPT1 timer rollover interrupt enable bit in register GPT1_IR
       attachInterruptVector(IRQ_GPT1, GPT1TimerISR);    // GPT1 interrupt service routine is also named in: #define GPT1_ISR
       NVIC_ENABLE_IRQ(IRQ_GPT1);
     }
   else if (GPTn == 2)
     {
       GPT2_IR |= GPT_IR_ROVIE;                      // Set the GPT2 timer rollover interrupt enable
       attachInterruptVector(IRQ_GPT2, GPT2_ISR);    // GPT2_ISR named in #define: #define GPT2_ISR
       NVIC_ENABLE_IRQ(IRQ_GPT2);
     }
}

void GPT1TimerISR()
{
  GPT1IntFlag = true;                                // Interrupt flag variable is global volatile bool type
}

void GPT2TimerISR()
{
 GPT2IntFlag = true;
}
 
you need to clear the interrupt flag in the ISR, otherwise the interrupt will fire again as soon as you exit the ISR ... the black hole. you also need asm volatile ("dsb"); in the isr to keep ISR from firing twice.. see https://github.com/manitou48/teensy4/blob/master/gpt_isr.ino

Thanks so much for the suggestions! Thought that was going to fix things but however I play with those lines I still get a consistent freeze at 07min:09. I've switched the code to fully zero the SR register to no avail and moved the statements around a bit. Ive enabled only the GPT1 interrupt in the code. Strange that your code hasnt worked here.

Code:
void startGPT1Counter(void)
  {
    CCM_CCGR1 |= CCM_CCGR1_GPT(CCM_CCGR_ON) ;         // enable GPT1 module
    GPT1_CR = 0;
    GPT1_SR = 0x3F; // clear all prior status
    GPT1_CR =  GPT_CR_CLKSRC(2);          // | GPT_CR_FRR ;    // 3 = external clock  **** NOTE SET TO INTERNAL
    *(portConfigRegister(25)) = 1;                    // ALT 1  Clock pin is 25  
    startGPTinterrupt(GPT1);                          // Enables overflow interrupt on GPTn (GPT1 #define = 1)
    GPT1_CR |= GPT_CR_EN;                             // enable GPT1     //|GPT_CR_FRR.. free run set
  }

void startGPTinterrupt(uint8_t GPTn)                  // Sets up GPT 1/2 rollover interrupt, call before enabling timer
{
    if (GPTn == 1)
      {
        GPT1_IR |= GPT_IR_ROVIE;                      // Set the GPT1 timer rollover interrupt enable bit in register GPT1_IR
        attachInterruptVector(IRQ_GPT1, GPT1TimerISR);    // GPT1 interrupt service routine is also named in: #define GPT1_ISR
        NVIC_ENABLE_IRQ(IRQ_GPT1);
      }
    else if (GPTn == 2)
      {
        GPT2_IR |= GPT_IR_ROVIE;                      // Set the GPT2 timer rollover interrupt enable
        attachInterruptVector(IRQ_GPT2, GPT2_ISR);    // GPT2_ISR named in #define: #define GPT2_ISR
        NVIC_ENABLE_IRQ(IRQ_GPT2);
      }    
}

void GPT1TimerISR()
 { 
   GPT1_SR =0;    //&= ~GPT_SR_ROV;                   // Clear SR reg rollover flag
   GPT1IntFlag = true;                                // Interrupt flag variable is global volatile bool type
   while (GPT1_SR & GPT_SR_ROV);
   asm volatile ("dsb");                              // Prevents ISR firing twice
 }

void GPT2TimerISR()
{
  GPT2_SR = 0;  //&= ~GPT_SR_ROV;                             // Clear SR reg rollover flag
  GPT2IntFlag = true;
  while (GPT2_SR & GPT_SR_ROV);
  asm volatile ("dsb");                               // Prevents ISR firing twice
}
 
you need to clear the interrupt flag in the ISR, otherwise the interrupt will fire again as soon as you exit the ISR ... the black hole. you also need asm volatile ("dsb"); in the isr to keep ISR from firing twice.. see https://github.com/manitou48/teensy4/blob/master/gpt_isr.ino

Please CANCEL my last comment, your code works beautifully now! I am an idiot, I just re read the GPT SR register info again, the rollover flag is essentially *active low* ie 0 after rollover. I had expected the reverse and missed that detail. zeroing the ROV flag was somewhat counterproductive :). Anyway setting it works beautifully to stop repeat interrupts, the code interrupts and carries on, I'm getting a nice /7 minute interrupt count now! Next step will be getting count/compare working. Im thinking that may be less error prone now I have more knowledge. Thanks so much for helping to make this work. Working ISR test code posted in case it helps others.

Code:
void GPT1TimerISR()
 { 
   GPT1_SR |= GPT_SR_ROV;                       // SET the SR reg rollover flag to clear, goes LOW on interrupt
   GPT1IntFlag = true;                                // Interrupt flag variable is global volatile bool type
   while (GPT1_SR & GPT_SR_ROV);
   asm volatile ("dsb");                               // Prevents ISR firing twice
 }

void GPT2TimerISR()
{
  GPT2_SR |= GPT_SR_ROV;                       // Clear SR reg rollover flag, need to SET bit
  GPT2IntFlag = true;
  while (GPT2_SR & GPT_SR_ROV);
  asm volatile ("dsb");                               // Prevents ISR firing twice
}
 
as my example shows (and the reference manual dictates), you must "set" the status bit to clear it.
 
The convention makes extending the hardware with more units backwards compatible I think - '1's are
only ever written to flags the code knows about, and writting '0's to unimplemented parts of the register
is benign should future processors extend the number of flags in the register.
 
Thanks for the thoughts this I’m still slightly working to get my head round the reverse logic. It’s certainly a trap for the unwary. So bit 6..31 for the GPTx_SR registers are currently unimplemented. I wrongly wrote code that set the whole register to potentially flag 32 future interrupts by writing GPT1_SR=0; as a quick and dirty interrupt clearance, or so I thought at the time. It’s possible any code unnecessarily writing zeros to more than one flag bit could remain benign until new hardware that implemented bits 6.. as interrupt flags was added in a revision, a function perhaps unknown to the programmer could be inadvertently setting flags for the new hardware? Still trying to understand why this is more benign? I’ve just found out the hard way that flags in the set state at the wrong time such as in an ISR are a bad idea

Steve
 
Status
Not open for further replies.
Back
Top