Timer & Interrupt, for delay-width-pulse-generator - Teensy 4.0

Status
Not open for further replies.

Croc

Member
Hello,

for my first post here, i would like to thanks everyone for the help and tips, and of course Paul for the board development.
I bought my first Teensy (4.0) a month ago, and i've tested a lot of my previous works from my Arduino experience.

One of my most useful program/device is a 6 channels Pulse Generator, which generates 6 independant pulses (width + delay) triggered from an external trigger signal. That is used for triggering camera, valve, or any device in an experimental laboratory, for example.

Now trying to re-use my old code into my new Teensy 4.0, but you know, with the high speed it provides and hoping a jitter far below the µs.
With my UNO board (16 MHz) i achieved a 1 to 2 µs resolution, coz of the minimum clock cycles needed for some operation. I was using the direct port access, and the ISR() interrupt, then compare the timer value with ISR(TIMER1_COMPA_vect), playing with ISR (TIMER1_OVF_vect), use masks/flags, etc... i'm pretty sure you know that world.


But it's impossible to use the "old" ISR() command with that Teensy...
even with this simpliest and useless code, i'm having compilation error pointing on the ISR() line. (expected constructor, destructor, or type conversion before '(' token)
Code:
#include <avr/io.h>
#include <avr/interrupt.h>

ISR(TIMER0_OVF_vect)
{
    /* Timer 0 overflow */
}

That basic example comes from here https://www.pjrc.com/teensy/interrupts.html

I've also tested the TeensyTimerTool, with that example : https://github.com/luni64/TeensyTimerTool/wiki/Callbacks#functors-as-callback-objects
Code:
(...)
OneShotTimer t1, t2;

void setup()
{
    pinMode(1, OUTPUT);
    pinMode(2, OUTPUT);

    t1.begin(PulseGenerator(1, 5));  // 5µs pulse on pin 1
    t2.begin(PulseGenerator(2, 10)); //10µs pulse on pin 2
}

void loop()
{
    t1.trigger(1'000);  // delay 1ms
    t2.trigger(500);  // delay 500 µs
    delay(10);
}
There are few problems that i would like to avoid :
- the t1/t2.trigger() are executed one after the other, and t2 has to wait the end of t1. I can't start t2 during the middle of t1. (no overlap = no multichannel)
- we can't use zero as trigger(delay). The minimum delay is 1 µs.
- resolution is 1 µs minimum, not tick from timer/clock.

That library is very useful, but probably not in my case.

I really want the accuracy way (correct me if i'm wrong) of :
- interrupt of one pin, from where the external trigger comes
- set the timer to "zero"
- compare timer (or waiting for) value until i've to LOW/HIGH one (or multiple) output pin
- compare/wait for the next LOW/HIGH/channel, etc...
- end/restart the interrupt routine, to catch the next external trig.


In advance, thanks for your help/comments !
 

That page seem to be related to the AVR Teensies. The T3x - T4x use processors with an ARM core which is a completely different thing.
-------

If I understood correctly you want to achieve the following:

  • You want n delay channels with setable pin, delay time and pulsewith
  • Triggered by a pin interrupt all channels shall simultaneously start a delay timer with its delay time
  • After this delay time the pin should be set HIGH
  • This pin should be set LOW after the channel pulse with

I've also tested the TeensyTimerTool, with that example : https://github.com/luni64/TeensyTime...llback-objects
The TeensyTimerTool is definitely able to do what you want to achieve but the example you are using was just meant to show how to use callbacks. By no means this is a real life pulse generator. As you have noticed, it is blocking while generating the pulse.

I'll add another example which is more suitable to your use case later today (if I understood it correctly)
 
thx luni for your post.
Yes you understood it correctly, and i'm waiting for your example.

let me explain in other words, how i've made it in the past :
- with a third software (by serial communication), i send the parameters of each channel (enable channel, delay, width)
- then, i build a timed todo list, which will include at max = 2*n action to do, at 2*n different moment. (each of the n channel goes HIGH, then LOW)
basically, it's a 2D array, one column for the ISR timer comparator value to reach (= when to do something ?). The second column is the direct port access code (= what to do ?), which allow to set one or all port pins at the very same time. In a concrete case, there is always multiple action to do at the same time (e.g. multiple channel to go HIGH at the same time, for example)
- the pin interrupt (from external trigger) happens, reset the timer, and start all successive ISR(TIMER1_COMPA_vect) from that 1st column, then execute the associated code for port manipulation.

That was illegible code (thanks to direct port access !), but a quite logical and cycle-efficient methodology.
 
First, you most probably do not need to do complicated stuff like direct port manipulation and combining actions to happen at the same time etc. Those Teensy boards are so much faster than the ancient AVRs that you probably will not notice any difference if you do this straight forward.

Below a simple example showing how I would solve the problem. I did a small class to do the actual pulsing. It encapsulates all the required timers and callbacks. In the begin function of the class you set the pin, pulse delay (µs) and pulse duration (µs). If you want to trigger a pulse you simply call trigger(). If needed you can change the settings by calling begin(...) again later.

Here the main sketch:

Code:
#include "TeensyTimerTool.h"
#include "PulseGen.h"

constexpr unsigned genCnt = 5;                 // array of 5 pulse generators
PulseGen generators[genCnt];

void trigger()                                 // trigger all
{
  digitalWriteFast(12, HIGH);                  // pulse on pin 12 during trigger to mark triggering for the LA

  for (unsigned i = 0; i < genCnt; i++)
  {
    generators[i].trigger();
  }

  digitalWriteFast(12, LOW);
}


void setup()
{
  pinMode(LED_BUILTIN,OUTPUT);
  pinMode(12, OUTPUT);         // use pin12 to mark the trigger events for the LA

  // some code to define settings at runtime via your serial connection or whatever
  //....

  generators[0].begin(0, 100'000, 50);      // pin 0, delay 100ms, duration 50µs
  generators[1].begin(1, 120'000, 150);     // pin 1, delay 120ms, duration 150µs
  generators[2].begin(2, 80'000,  15'000);  // pin 2, delay 80ms,  duration 15ms
  generators[3].begin(3, 80'000,  20'000);  // pin 3, delay 80ms,  duration 20ms
  generators[4].begin(4, 80'000,  100'000); // pin 4, delay 80ms,  duration 100ms

  //attachInterrupt(14, trigger, RISING);   // attach trigger function to pin 14
}

void loop()
{
  trigger(); // for the sake of simplicity we trigger manually (500ms) and don't use the pin which would require debouncing...

  digitalWriteFast(LED_BUILTIN,!digitalReadFast(LED_BUILTIN));
  delay(500);
}

It defines an array of 5 pulse generators and a simple trigger function which can be called from your pin interrupt. The trigger function simply triggers all generators. The setting of pin 12 is to have a signal for the logic analyser to measure the delay times. I set the same delay (80ms) for channel 2,3 and 4 to demonstrate that there is no need to do tricks, the timing difference between channels is below 100ns. (If this is not fast enough you can of course do tricks in the callback functions).

Please note: for the sake of a quick test I commented the pin interrupt assignment (didn't want to handle the debouncing stuff) and called the trigger function each 500ms from loop.

Here the pulseGen class (this goes into pulseGen.h)
Code:
#include "Arduino.h"
#include "TeensyTimerTool.h"

using namespace TeensyTimerTool;

class PulseGen
{
public:
    PulseGen() : delayTimer(TCK), durationTimer(TCK) // use the tick timers for this task, we have 20 per default, can be increased in config
    {
    }

    void begin(unsigned pin, unsigned delay, unsigned duration)  
    {
        this->pin = pin;
        this->delay = delay;
        this->duration = duration;

        pinMode(pin, OUTPUT);
        delayTimer.begin([this] { digitalWriteFast(this->pin,HIGH); this->durationTimer.trigger(this->duration) ; });
        durationTimer.begin([this] { digitalWriteFast(this->pin, LOW); });
    }

    void trigger() { delayTimer.trigger(delay); }

protected:
    unsigned pin, delay, duration;
    OneShotTimer delayTimer, durationTimer;
};

I used the tick timers since you have 20 of them per default, you can of course use other timers if you prefer.
  • The trigger function simply starts the one shot delay timer.
  • In the callback function of this timer the pin is set HIGH and the duration timer is triggered.
  • The callback of the duration timer sets the pin back to LOW

I used lambdas to define the two callback functions to get some compact code. If you prefer a less condensed version you can of course define a startPulse() and endPulse() function to handle the pins and use those as callback.

Here the output of the LA:

Anmerkung 2020-05-21 210007.jpg
 
nice example, i'll have to check that tomorrow on my scope.

As i'm absolutely not familiar with class/pointer/callback/lambda and other trick, let's try to explain what i've understood :

- begin() is used to "prepare" the function to be executed (= the callback). That is not executed yet.
- in the PulseGen class, you write a "special" begin() function, with my "special" parameters (pin, delay, duration)
- you are using pointer. Why ? In my mind, it could be faster to manipulate address (= pointer) instead of variable. I dont really know.
- then, the successive step comes :
Code:
pinMode(pin, OUTPUT);
delayTimer.begin([this] { digitalWriteFast(this->pin,HIGH); this->durationTimer.trigger(this->duration) ; });
durationTimer.begin([this] { digitalWriteFast(this->pin, LOW); });
the pinMode is useless, if output are already defined in the setup. But why not a pointer of pin ?

Then, go back to the main sketch loop : the trigger() now start the process.
in the Class : that trigger() is "special" too. He have to first wait the delay (as the natural use of this parameter, as i understood), before executing the delayTimer.
delayTimer is then executed, himself launching the trigger() of durationTimer.
i just dont understand the notation [this] in the begin(), and why there is pointer here : this->durationTimer

i'm on it for hours, but i think i understand it a little better now !
thx and waiting for comments ;)
 
As i'm absolutely not familiar with class/pointer/callback/lambda and other trick, let's try to explain what i've understood :
Sorry if I confused you. Since you where talking about 2dim arrays and direct port manipulation I deduced that you are very familiar with this stuff :). Anyway, let me answer your questions:

begin() is used to "prepare" the function to be executed (= the callback). That is not executed yet.
Right, it just stores the values of the passed in parameters, sets up the pin mode and attaches the callbacks to the timers. It does not invoke them right now.

- in the PulseGen class, you write a "special" begin() function, with my "special" parameters (pin, delay, duration)
- you are using pointer. Why ? In my mind, it could be faster to manipulate address (= pointer) instead of variable. I dont really know.

I think you refer to this:

Code:
void begin(unsigned pin, unsigned delay, unsigned duration)
{
        this->pin = pin;
        this->delay = delay;
        this->duration = duration;
//....

This has nothing to do with speed I just prefer to use the same names for the member variables as the parameters to the begin function. Since 'pin = pin' would't make too much sense you need to tell the compiler which one you mean. "this->pin = pin" just tells the compiler to assign the value of the parameter 'pin' to the class member 'pin'. I.e. it does the same as

Code:
void begin(unsigned pinParameter, unsigned passedDelay, unsigned _duration)
{
        pin = pinParameter;
        delay = passedDelay;
       duration = _duration;
//...
So, this is just a matter of taste, you can use both variants.

- then, the successive step comes :
Code:
pinMode(pin, OUTPUT);
delayTimer.begin([this] { digitalWriteFast(this->pin,HIGH); this->durationTimer.trigger(this->duration) ; });
durationTimer.begin([this] { digitalWriteFast(this->pin, LOW); });
the pinMode is useless, if output are already defined in the setup. But why not a pointer of pin ?

As you state, if you set the pinMode in setup, it is not necessary to repeat in the begin function of the class.
But, in object orientated programming you usually try to hide away as much implementation details as possible. The pulseGen class just needs information about the pin and the delays to perform its task. The fact that it has to setup the pin as output can be regarded as implementation detail. So, let the class do that low level stuff. The user of the class doesn't need to even know about that. Think of it like e.g. the Hardware Serial objects. You just tell them the required baud rate, all the low level stuff like output mode, setting up the registers, buffers etc is handled by the class and you don't have to bother with it.

Regarding the attachment of the callback functions:
Sorry for the terse example, I'm so used to lambdas that I sometimes forget that the syntax looks weird to people not used to it.

Below a more traditional way to do the same (I also changed the parameter saving in the begin() to not use the this pointer)
Code:
class PulseGen
{
public:
    PulseGen() : delayTimer(TCK), durationTimer(TCK) // use the tick timers for this task, we have 20 per default, can be increased in config
    {
    }

    void begin(unsigned _pin, unsigned _delay, unsigned _duration)
    {
        pin = _pin;
        delay = _delay;
        duration = _duration;

        pinMode(pin, OUTPUT);
        delayTimer.begin([this] { startPulse(); });
        durationTimer.begin([this] { endPulse(); });
    }

    void trigger() { delayTimer.trigger(delay); }

protected:
    void startPulse()
    {
        digitalWriteFast(pin,HIGH);
        durationTimer.trigger(duration) ;
    }

    void endPulse()
    {
        digitalWriteFast(pin,LOW);
    }

    unsigned pin, delay, duration;
    OneShotTimer delayTimer, durationTimer;
};

The two lines

Code:
  delayTimer.begin([this] { startPulse(); });
  durationTimer.begin([this] { endPulse(); });

attach the member functions 'startPulse' and 'endPulse' to the one shot timers. Since they are not free functions but member functions you need to use the shown syntax. Don't think too much about it but if you are interested in details google for std::function and lambda expressions.

Hope that helps a bit to understand whats going on.
 
Last edited:
Ok that's clear now !
That is just notation/syntax that i've never learned before. :eek:

now i'll test and adapt it for my purpose.
thx for your help ;)
 
ok, i'm back with some tests.

here is the best optimization i can do with my config :
5.png
(15 min capture persistence) bottomtrace = 1 µs delay / 1 µs duration from rising pulse (toptrace)

- my setup is a 7 channels
- nothing in the loop(), but a serial.print "hello world" every delay(1)
- interrupt attached to my pin 0 (= input trigger from an external generator)
- NVIC_SET_PRIORITY(IRQ_GPIO6789, 0); //irqnum,priority. lower num = higher priority
- usb.c modified as explained here https://forum.pjrc.com/threads/60831-Teensy4-0-and-_disable_irq-in-the-core-code, and the serial monitor is running (i've also jitter with the linked bench sketch ~30 ns)
- yield() to YIELD_OPTIMIZED in TeensyTimerTool config file.
- offset of ~140 (instead of 67 if i remember), in the TckChannel.h, line 128.
- (last update 0.1.10 of your Tool)

so, for this scope screenshot :
- i've set a begin(pin, delay, duration) to all the 7 channels.
- only the channel on screen is trigger() (the others are only configured, never trigger() )
- the top pulse is from the generator with 500 ns duration.
- the bottom trace, with infinite persistence too, is configured to be 1 µs delay and 1 µs duration, at the rising of trig.

> if ALL channels are trigger(), the jitter is worse, adding up to 500 ns more than that, for delay and for duration.
> if the unused channel are stop(), the jitter is better
> as we can see (or presume) in this traces, most of pulse (the dark ones = the last ones) are in a ~400 ns jitter.
> but a lot of pulses have delay as long as 1.5 µs ! (we can distinguish the min/max of rise/fall time with the overshots)
> some traces have a correct delay, but a duration up to 2 µs.
> it make me think about a round() in a microsecond integer scale, but i didnt find anything to fix, and your code already seems to use the clock tick count...
> last point that i've mentionned in a previous post : we can't set a delay = 0 with your code. The minimum is 1 µs. But i can bypass this with a digitalWriteFast(HIGH) before calling the trigger(), and let the delay to 1 µs.

:confused:
 
Status
Not open for further replies.
Back
Top