Triggered interval timer, i.e. start clocking on trigger

DrM

Well-known member
Hi,

Running on Teensy4, I would like to use a logical level input as a trigger to start clocking a function. The latency between the trigger input and the first call to the function should be as short as feasible, and thereafter the function needs to be called ar precise intervals. For example, imagine you want to clock 50 frames from a camera with the start triggered by a logic level input.

So far, I looked at connecting an ISR to a digital pin, and have the ISR invoke something along the lines of IntervalTimer.begin() or PeriodicTimer.trigger(). But each seems to have issues.

For the first, the call to mytimer.begin( myfunc, someinterval) takes about 560 nsecs to complete. I am not sure how long it takes before myfunc is called.

For the second, again I don't know what the latency is, and that call seems good for one frame. I need to start clocking.


So, hjow do I do this?


Thank you
 
Well, I think I found it. Using the TeensyTimerTool, begin a PeriodicTimer in stopped mode and then start it from an ISR launched on a transiton on the digital i/o pin. The start call seems to be a one-linear in the TeensyTimerTool source code. So, maybe that will do the trick.
 
Okay, TeensyTimerTool doesn;t really solve it. The first frame is acquired by TeensyTimerTool, at about 1 clock period after calling begin(). That won't do for a triggered set of frames. We need the first frame immediately and the next and all subsequent frames spaced at the specified clock period.

Back to the original question, how do we trigger a periodic set of frames with the first frame starting at the trigger?
 
Okay, TeensyTimerTool doesn;t really solve it. The first frame is acquired by TeensyTimerTool, at about 1 clock period after calling begin(). That won't do for a triggered set of frames. We need the first frame immediately and the next and all subsequent frames spaced at the specified clock period.

Back to the original question, how do we trigger a periodic set of frames with the first frame starting at the trigger?

I assumed you would set your periodic timer to trigger at 2x the frequency of your ouput, and then toggle a digital output on each interrupt so that you get a square wave at the desired frequency. Another way to do what you're asking on T4 is to use two channels of a QuadTimer. One channel is configured for input capture and a second channel for output compare. When an edge arrives on the input channel, the output channel is enabled, with an interrupt on each output rising edge. The ISR keeps a count of output edges and disables the output when the desired count is reached. The cycle repeats on the next rising edge of input. The program below is a working example. The input frequency is 2 kHZ (period = 500 us), and the output frequency is 125 kHz (period = 8 us). 22 output pulses are generated for each input pulse.

Code:
//==============================================================================
// T4.x QTIMER TEST PROGRAM (input capture on CH2, output compare on CH1)
//==============================================================================
// Joe Pasquariello  09/09/22  adapted from program by @TelephoneBill
//
// Pin11 = input for TMR1 CH2 (input capture)
// Pin12 = output for TMR1 CH1 (output compare)
// Pin13 = built-in LED
// Pin23 = PWM for test (jumper to pin 11)

volatile uint32_t ISRTotalCount, ISRTotalPrev;
volatile uint32_t ISRComp1Count, ISRComp1Prev;
volatile uint32_t ISRCapt2Count, ISRCapt2Prev;
volatile uint16_t CH2_IC_Value, CH2_IC_Delta, CH2_IC_Prev;
volatile uint32_t CH1_OC_Count;
float CPUTemp;
elapsedMillis looptime;
uint32_t sec = 0;

// QTMR1 as alias for IMXRT_TMR1
#define QTMR1 (IMXRT_TMR1)

void setup()
{
  Serial.begin(9600);                   // USB serial
  pinMode(13, OUTPUT);                  // pin 13 dig out (LED)

  // turn on clocks for QTMR1 (CG13)
  CCM_CCGR6 |= CCM_CCGR6_QTIMER1(CCM_CCGR_ON);

  // Disable all 4 channels of QTMR1
  QTMR1.ENBL = 0;
  
  //===================================================================
  // configure TMR1 CH1 for OUTPUT COMPARE w/ interrupt on COMP1 match
  //===================================================================
  QTMR1.CH[1].CTRL = 0;			// stop channel
  
  // set SCTRL status and control register
  QTMR1.CH[1].SCTRL = TMR_SCTRL_OEN;	// output enable
  
  QTMR1.CH[1].LOAD = 0;			// counter starts counting from zero
  QTMR1.CH[1].COMP1 = 1200-1;		// 8 uS - count up to this value
  QTMR1.CH[1].CMPLD1 = 1200-1;		// load COMP reg with value from this reg
  
  // set CSCTRL comparator status and control register
  // enable Timer Compare Flag 1 interrupt, load from CMPLD1  
  QTMR1.CH[1].CSCTRL = TMR_CSCTRL_TCF1EN | TMR_CSCTRL_CL1(1);
  
  // set pin 12 as TMR1 CH1 external pin (see R.M. page 309).
  IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_01 = 1; 
					
  //===================================================================
  // configure TMR1 CH2 for INPUT CAPTURE w/ interrupt on RISING EDGE
  //===================================================================
  QTMR1.CH[2].CTRL = 0;			// stop channel

  // set SCTRL status and control register
  // enable Input Edge Flag interrupt, capture mode rising edges
  QTMR1.CH[2].SCTRL = TMR_SCTRL_IEFIE | TMR_SCTRL_CAPTURE_MODE(1);
  
  QTMR1.CH[2].LOAD = 0;			// counter starts counting from zero
  QTMR1.CH[2].COMP1 = 0xFFFF;		// 16.7uS - count up to this value
  QTMR1.CH[2].CMPLD1 = 0xFFFF;		// load COMP reg with value from this reg

  // set CSCTRL comparator status and control register
  QTMR1.CH[2].CSCTRL = 0;		// no compare function
  
  // set pin 11 as TMR1 CH2 external pin (see R.M. page 309).
  IOMUXC_SW_MUX_CTL_PAD_GPIO_B0_02 = 1;

  //===================================================================
  // set CTRL control registers
  //===================================================================  					
  QTMR1.CH[1].CTRL = 0			// CH1 output compare
             | TMR_CTRL_CM(0b001)	// count rising edges of primary source
             | TMR_CTRL_PCS(0b1000)	// primary source = IP Bus Clock / 1
             | TMR_CTRL_SCS(0b01)	// secondary source = CH1 input pin
           //| TMR_CTRL_ONCE		// 0 -> count repeatedly
             | TMR_CTRL_LENGTH		// 1 -> re-init counter on compare match
           //| TMR_CTRL_DIR          	// 0 -> count up
           //| TMR_CTRL_COINIT		// 0 -> no co-channel init
             | TMR_CTRL_OUTMODE(0b011);	// toggle OFLAG on compare match

  QTMR1.CH[2].CTRL = 0			// CH2 input capture
             | TMR_CTRL_CM(0b001)	// count rising edges of primary source
             | TMR_CTRL_PCS(0b1001)	// primary source = IP Bus Clock / 2
             | TMR_CTRL_SCS(0b10)	// secondary source = CH2 input pin
           //| TMR_CTRL_ONCE		// 0 -> count repeatedly
           //| TMR_CTRL_LENGTH		// 0 -> free-running counter
           //| TMR_CTRL_DIR          	// 0 -> count up
           //| TMR_CTRL_COINIT		// 0 -> no co-channel init
             | TMR_CTRL_OUTMODE(0b011);	// toggle OFLAG on compare match
					
  // enable CH2 for input capture (CH1 gets enabled/disabled in ISR)
  QTMR1.ENBL = 0b0100;
  
  // attach interrupt vector and enable interrupt
  attachInterruptVector(IRQ_QTIMER1, QTMR1_isr);
  NVIC_ENABLE_IRQ(IRQ_QTIMER1);

  // PWM on pin 23 for test
  analogWriteFrequency( 23, 2000 );	// 2-kHz PWM	
  analogWrite( 23, 128 );		// 50% duty cycle
  looptime = 0;				// init elapsedMillis timer
}
  

void QTMR1_isr()
{
  if (QTMR1.CH[2].SCTRL & TMR_SCTRL_IEF) {	// if CH2 IEF (input edge flag)
    QTMR1.CH[2].SCTRL &= ~(TMR_SCTRL_IEF);	//   clear IEF
    ISRCapt2Count++;				//   increment Capture count
    CH2_IC_Value = QTMR1.CH[2].CAPT;		//   read CH2 capture value
    CH2_IC_Delta = CH2_IC_Value - CH2_IC_Prev;	//   compute delta since prev
    CH2_IC_Prev = CH2_IC_Value;			//   save value for next capture
    CH1_OC_Count = 0;				//   init OC edge count = 0   
    QTMR1.CH[1].CNTR = 0;			//   init CH1 counter = 0
    QTMR1.CH[1].COMP1 = 150*47;			//   1st compare match in 47 us
    QTMR1.CH[1].CMPLD1 = 150*8;			//   next compare match in 8 us
    QTMR1.ENBL |= 0b0010;			//   enable CH1
  }

  if (QTMR1.CH[1].CSCTRL & TMR_CSCTRL_TCF1) {	// if CH1 TCFG flag set
    QTMR1.CH[1].CSCTRL &= ~(TMR_CSCTRL_TCF1);	//   clear TCF1
    ISRComp1Count++;				//   increment Compare count
    if (++CH1_OC_Count >= 22)			//   if OC edge count >= 22
      QTMR1.ENBL &= ~0b0010;			//     disable CH1
  }
  
  ISRTotalCount++;				// increment total ISR count

  asm volatile ("dsb");				// wait for clear memory barrier
}


void loop()
{
  if (looptime >= 1000) {
    looptime -= 1000;
    sec++;
    CPUTemp = tempmonGetTemp();
    Serial.printf( "sec=%6lu  ISR=%8lu  OC(1)=%8lu  IC(2)=%5lu  DELTA=%5hu  CPU=%1.1f(degC)\n", 
      sec,
      ISRTotalCount-ISRTotalPrev,
      ISRComp1Count-ISRComp1Prev,
      ISRCapt2Count-ISRCapt2Prev,
      CH2_IC_Delta,
      CPUTemp );
    ISRTotalPrev = ISRTotalCount;
    ISRComp1Prev = ISRComp1Count;
    ISRCapt2Prev = ISRCapt2Count;
    digitalToggleFast( 13 );
  }
}
 
I'm sorry, maybe I'm missing it, but I don't see how that addresses the problem.

I need to start a timer and produce an interrupt at the start as well as each clock interval.

At present, both of the periodic timer libraries produce the first interrupt at the end of the first clock interval. So again, what is lacking is an interrupt at the beginning of the first interval.

That makes sense for pit, since it is count-down timer. Still, it is an obvious use case, there should be some way to do it.

The kluge, manually calling the isr and then starting the timer to clock the isr, leaves a an extra 600 nanoseconds in the first interval.
 
You haven't told us any specific requirements, and I'm still not sure what you're trying to do except to produce a series of output edges in response to an input trigger. If you're going to use a periodic timer, you can start the periodic timer and generate the first output edge in the trigger ISR. Don't use the timer for the first output edge, but rather use it for as many additional edges as necessary. With a periodic timer, that's the best you can do. If you want more precision, use the method in the program I sent. The first output can occur at whatever delay you want relative to the input trigger, down to a small fraction of a microsecond.
 
As joeqasquariello mentioned it is difficult to advice without any timing requirements. Here a quick test sketch (calling the first ISR manually) and some measurments. The code generates a 2kHz trigger signal on pin13 (connected to pin0) and 10 pulses (1us duration, 20us spacing) after each trigger.

Code:
#include "Arduino.h"
#include "TeensyTimerTool.h"
using namespace TeensyTimerTool;

PeriodicTimer timer(TMR1);

constexpr int triggerPin = 0;
constexpr int pulsePin   = 1;
unsigned pulseCount = 0;

void onTimer()
{
    digitalWriteFast(pulsePin, HIGH); // generate a 1us pulse
    delayMicroseconds(1);
    digitalWriteFast(pulsePin, LOW);

    if (++pulseCount >= 10) // stop pulsing after 10 pulses
    {
        timer.stop();
        pulseCount = 0;
    }
}

void onTrigger()
{
    timer.start();
    onTimer();
}
void setup()
{
    timer.begin(onTimer, 20us, false);  // setup timer in stopped mode, pulse spacing 20us
    attachInterrupt(triggerPin, onTrigger, RISING); //

    // test signal generation on pin 13 whichis connectedt to the trigger pin
    pinMode(triggerPin, INPUT_PULLDOWN);
    pinMode(pulsePin, OUTPUT);
    analogWriteFrequency(LED_BUILTIN, 2000); // generate 2kHz pulses on pin 13
    analogWrite(LED_BUILTIN, 10);          // start triggering
}

void loop()
{
}

Screenshot 2023-01-06 073334.jpg
Screenshot 2023-01-06 073502.jpg

The measured delay from the trigger to the first pulse is some 300us. Parts of it (~100us) are from the time needed to detect/invoke the pin ISR, part of it are from the time needed to start the timer. Don't see any possibiltiy to speed that up using the TimerTool.

Having an option to invoke the first timer ISR directly after starting it in the TimerTool shouldn't be too difficult, I'll see if I can add that to the next version.
 
Last edited:
If you want to try you can change the code in /src/TimerModules/TMR/TMRChannel.h (line 52ff) from this:
Code:
errorCode TMRChannel::start()
{
    regs->CNTR = 0x0000;

    regs->CSCTRL &= ~TMR_CSCTRL_TCF1;
    regs->CSCTRL |= TMR_CSCTRL_TCF1EN;
    return errorCode::OK;
}

to this
Code:
errorCode TMRChannel::start()
{
    regs->CSCTRL = TMR_CSCTRL_CL1(1) | TMR_CSCTRL_TCF1 | TMR_CSCTRL_TCF1EN;
    return errorCode::OK;
}
This will set the overflow flag in start() so that the timer ISR will be called immediately.

To get more precision you should also set the timer prescaler to PSC_1 instead the default PSC_128 in defaultConfig.h. (line 46). Additionally it is a good idea to lift the timer priority above that of the pin interrupt, so that the first timer callback call can interrupt the pin ISR.

Code:
timer.begin(onTimer, 20us, false);  // setup timer in stopped mode, pulse spacing 20us
NVIC_SET_PRIORITY(IRQ_QTIMER1, 0);

I also added a pin toggle (pin 2) in the pin ISR to measure how long the pin ISR takes to detect the pin change. This leads to the following results:

Screenshot 2023-01-06 083750.jpg

I.e., it takes some 80ns to call the pin ISR, and some 170ns to invoke the callback, in total some 250ns from the input edge to the timer ISR.

Here the adapted test sketch:
Code:
#include "Arduino.h"
#include "TeensyTimerTool.h"
using namespace TeensyTimerTool;

PeriodicTimer timer(TMR1);

constexpr int triggerPin = 0;
constexpr int pulsePin   = 1;
unsigned pulseCount = 0;

void onTimer()
{
    digitalWriteFast(pulsePin, HIGH); // generate a 1us pulse
    delayMicroseconds(1);
    digitalWriteFast(pulsePin, LOW);

    if (++pulseCount >= 10) // stop pulsing after 10 pulses
    {
        timer.stop();
        pulseCount = 0;
    }
}

void onTrigger()
{
    digitalToggleFast(2);
    timer.start();
    // onTimer();
}
void setup()
{
    timer.begin(onTimer, 20us, false);  // setup timer in stopped mode, pulse spacing 20us
    NVIC_SET_PRIORITY(IRQ_QTIMER1, 0);
    attachInterrupt(triggerPin, onTrigger, RISING); //

    // test signal generation on pin 13 whichis connectedt to the trigger pin
    pinMode(triggerPin, INPUT_PULLDOWN);
    pinMode(pulsePin, OUTPUT);
    pinMode(2, OUTPUT);
    analogWriteFrequency(LED_BUILTIN, 2000); // generate 20Hz pulses on pin 13
    analogWrite(LED_BUILTIN, 10);          // start triggering
}

void loop()
{
}
 
@luni, thank you. That's what I was looking for. A fixed latency of 250ns is perfectly okay for clocking the isr. Nice work. Thank you again.
 
Back
Top