How many elapsedMicros can you use on the teensy 4?

Status
Not open for further replies.

frankzappa

Well-known member
I'm new at programming, I'm still half way through a book on C programming and I'm learning so bare with me.

Is there a limit on how many timers you can run simultaneously with the teensy 4?

I'm measuring the width of peaks on a AC signals so I check when the value crosses above zero and when it comes back down. I have 10 sensors to measure. There are two ways I can think of doing this.

Either let one timer run indefinitely and just subtract the time when the peak went over zero from the time it went back down. This would give me the width of the peak.

Or set a timer to 0 when the value is rising over zero and store the elapsed time when it comes back down.

The first one would produce some insanely huge numbers quickly (because I'm measuring microseconds) and the second method would need 10 timers running because the peaks occur at the same time slightly shifted from each other.

I would prefer the second. Is there a limit to the amount of timers I can have running and are there any drawbacks?

Any better suggestions on how to do this?

One other method would be just counting the amount of ADC samples elapsed when the value is above zero, they come in at regular intervals.
 
Is there a limit on how many timers you can run simultaneously with the teensy 4?

Short answer: There's no limit. You can create as many elapsedMicros instances as you like.

elapsedMicros only uses CPU when your code checks actually uses it, so adding hundreds or thousands of them won't slow the rest of your program while you're not actually checking them.

But checking elapsedMicros does use a small amount of CPU time. So does the rest of your program, which puts a limit on how rapidly you can check each elapsedMicros and actually do something. This is the main drawback. But it can also be quite an advantage, that you are in control of when things get checked, which is much simpler and less error-prone than things like IntervalTimer (which is limited to only 4 instances).
 
Short answer: There's no limit. You can create as many elapsedMicros instances as you like.

elapsedMicros only uses CPU when your code checks actually uses it, so adding hundreds or thousands of them won't slow the rest of your program while you're not actually checking them.

But checking elapsedMicros does use a small amount of CPU time. So does the rest of your program, which puts a limit on how rapidly you can check each elapsedMicros and actually do something. This is the main drawback. But it can also be quite an advantage, that you are in control of when things get checked, which is much simpler and less error-prone than things like IntervalTimer (which is limited to only 4 instances).

That's great. How much CPU time are we talking?

I would only need to set a timer to 0 when a rising value passes zero and then check the elapsed time when it goes back down below zero. So the resets and checks would only need to be done once every millisecond or two which is the duration of the half wave of the peak. No need to check every time in the loop.
 
Not much.

But why don't you tell me? On your other thread, I and others went to lengths to explain how you can use the cycle counter to actually measure these sorts of things. Please, go forth and use it to benchmark the actual CPU usage, then reply with what you've learned.

I will check. I usually use elapsed micros to check these things. I assumed it was pretty instantaneous until you said it above.

I didn't get around to using the cycle counter. I will check the thread.
 
When working on performance critical code, you should use the ARM_DWT_CYCCNT cycle counter to benchmark your code. It's a 32 bit integer which increments every clock cycle. It's inside the ARM core, so reading it is very fast, but keeping a copy throughout your performance-critical code does either consume one of the precious registers or require a write to memory. Still, the cost is low and the insight you gain from benchmarking as you experiment is worthwhile. Just read it before and after and subtract the 2 to get the number of cycles you code too to execute. If running inside an interrupt or other timing critical place, maybe store it to a global variable so you can print the number to the serial monitor at a less critical moment.

How would I actually read it?

Code:
uint32_t g_cycleCounter = ARM_DWT_CYCCNT;

void setup() {
}
void loop(){
uint32_ t start = g_cycleCounter;
....
....
program 
....
....
uint32_t end = cycleCounter;

Serial.print(end - start);
Serial.println("");
}
 
No, like this:
Code:
void setup() {
}
void loop(){
uint32_t start = ARM_DWT_CYCCNT;
....
....
program 
....
....
uint32_t end = ARM_DWT_CYCCNT;

Serial.print(end - start);
Serial.println("");
}

Pete
 
Also one of the great things with playing with things like a Teensy (and in most Arduinos) is you can often simply look at the sources and see exactly what is happening.

There is nothing magical about it, although the class does have a lot of C++ gunk, for most cases...

But if you look in the cores of which teensy in this case 4 (???\cores\teemsy4\elapsedMillis.h), you will see the definition of this whole class:
Code:
class elapsedMicros
{
private:
	unsigned long us;
public:
	elapsedMicros(void) { us = micros(); }
	elapsedMicros(unsigned long val) { us = micros() - val; }
	elapsedMicros(const elapsedMicros &orig) { us = orig.us; }
	operator unsigned long () const { return micros() - us; }
	elapsedMicros & operator = (const elapsedMicros &rhs) { us = rhs.us; return *this; }
	elapsedMicros & operator = (unsigned long val) { us = micros() - val; return *this; }
	elapsedMicros & operator -= (unsigned long val)      { us += val ; return *this; }
	elapsedMicros & operator += (unsigned long val)      { us -= val ; return *this; }
	elapsedMicros operator - (int val) const           { elapsedMicros r(*this); r.us += val; return r; }
	elapsedMicros operator - (unsigned int val) const  { elapsedMicros r(*this); r.us += val; return r; }
	elapsedMicros operator - (long val) const          { elapsedMicros r(*this); r.us += val; return r; }
	elapsedMicros operator - (unsigned long val) const { elapsedMicros r(*this); r.us += val; return r; }
	elapsedMicros operator + (int val) const           { elapsedMicros r(*this); r.us -= val; return r; }
	elapsedMicros operator + (unsigned int val) const  { elapsedMicros r(*this); r.us -= val; return r; }
	elapsedMicros operator + (long val) const          { elapsedMicros r(*this); r.us -= val; return r; }
	elapsedMicros operator + (unsigned long val) const { elapsedMicros r(*this); r.us -= val; return r; }
};

So if you have code like:
Code:
elapsedMicros em;
function_to_time();
Serial.println(em, DEC);

The code and data usage is more or less identical to simply doing:
Code:
uint32_t start_time = micros();
function_to_time();
Serial.println((uint32_t)(micros()-start_time), DEC);
But using the class makes it a little easier to read!
 
Also one of the great things with playing with things like a Teensy (and in most Arduinos) is you can often simply look at the sources and see exactly what is happening.

There is nothing magical about it, although the class does have a lot of C++ gunk, for most cases...

But if you look in the cores of which teensy in this case 4 (???\cores\teemsy4\elapsedMillis.h), you will see the definition of this whole class:
Code:
class elapsedMicros
{
private:
	unsigned long us;
public:
	elapsedMicros(void) { us = micros(); }
	elapsedMicros(unsigned long val) { us = micros() - val; }
	elapsedMicros(const elapsedMicros &orig) { us = orig.us; }
	operator unsigned long () const { return micros() - us; }
	elapsedMicros & operator = (const elapsedMicros &rhs) { us = rhs.us; return *this; }
	elapsedMicros & operator = (unsigned long val) { us = micros() - val; return *this; }
	elapsedMicros & operator -= (unsigned long val)      { us += val ; return *this; }
	elapsedMicros & operator += (unsigned long val)      { us -= val ; return *this; }
	elapsedMicros operator - (int val) const           { elapsedMicros r(*this); r.us += val; return r; }
	elapsedMicros operator - (unsigned int val) const  { elapsedMicros r(*this); r.us += val; return r; }
	elapsedMicros operator - (long val) const          { elapsedMicros r(*this); r.us += val; return r; }
	elapsedMicros operator - (unsigned long val) const { elapsedMicros r(*this); r.us += val; return r; }
	elapsedMicros operator + (int val) const           { elapsedMicros r(*this); r.us -= val; return r; }
	elapsedMicros operator + (unsigned int val) const  { elapsedMicros r(*this); r.us -= val; return r; }
	elapsedMicros operator + (long val) const          { elapsedMicros r(*this); r.us -= val; return r; }
	elapsedMicros operator + (unsigned long val) const { elapsedMicros r(*this); r.us -= val; return r; }
};

So if you have code like:
Code:
elapsedMicros em;
function_to_time();
Serial.println(em, DEC);

The code and data usage is more or less identical to simply doing:
Code:
uint32_t start_time = micros();
function_to_time();
Serial.println((uint32_t)(micros()-start_time), DEC);
But using the class makes it a little easier to read!

That is very convenient indeed, I haven't read about classes yet. I've mostly read about C so far because it was a bit simpler to learn but i will get there eventually because I've noticed they are used in libraries a lot. Structs in C seem similar but I haven't played around with them either. So right now I don't understand much of that code.
 
Back on that other thread Paul linked >> the Cycle Timer was already shown in use to see how long the read of 5 pairs of ADC's took when the looping code was unrolled.

This was used to accumulate it per second to see how long each of the 50K loops took on average.
>> uint32_t r10c = ARM_DWT_CYCCNT;

Given all points read in each loop() pass the micros() could be tracked - perhaps with ARM_DWT_CYCCNT offset - just recorded at the top of the loop() code.

Or each of the ten adc points could have a dedicated elapsedMicros var for peak HIGH and even LOW min depending on the needs. They are just 32 bit unsigned values tracking micros() - so a four byte number.
 
In stead of making more noob threads I'll just ask here:

I've run into a little trouble with unsigned integers. As I understand you can't mix unsigned and signed together.

For example: If I have an array of signed int, can I use an unsigned int for the "i" in the for loop and vice versa?

Also if I add two unsigned ints together and put the sum in a signed int, will there be any unexpected conversions going on?

Basically I need to do some calculations that may result in negative values, I'm wondering if it's ok to convert the unsigned into signed somewhere before? Like during a previous calculation that doesn't risk negative values?
 
It is a @frankzappa thread - so piggybacking by @frankzappa won't offend @frankzappa - but if info develops it will be harder for others to find.

For a given number of bytes of storage a 'uint' can always hold the same positive value of an 'int'. There is only a problem if a negative int is assigned to a uint.

For positive values of both there is no problem mixing - but the compiler will issue a warning. You can ignore that warning - or (cast) it away as needed and the only issue would be when the int holds a value <0. A quick test could be written to see what happens.
 
It is a @frankzappa thread - so piggybacking by @frankzappa won't offend @frankzappa - but if info develops it will be harder for others to find.

For a given number of bytes of storage a 'uint' can always hold the same positive value of an 'int'. There is only a problem if a negative int is assigned to a uint.

For positive values of both there is no problem mixing - but the compiler will issue a warning. You can ignore that warning - or (cast) it away as needed and the only issue would be when the int holds a value <0. A quick test could be written to see what happens.

That is true, maybe not that great for the forum.

I don't seem to have problems but maybe it only shows in a certain situation that I'm not anticipating.

Is there any situation where a 0 will be intepreted as for example -2,147,483,648 when mixing uint and int?
 
Was suggesting it may be worthy of it's own thread - rahter than some posts on this unique thread.

Zero is 0 in both cases. Not sure what a negative compare or assign to a unsigned does - simple test and prints would show.

An 8 bit byte has 256 possible values - unsigned is 0-255 and signed is -128 to 127 - but 0 is zero in both cases. The same applies to two and four byte integer numbers.
 
To quickly answer your questions...


If I have an array of signed int, can I use an unsigned int for the "i" in the for loop and vice versa?

The array index is independent of the type of array's actual data. You can have an array of float or an array of a struct. Regarding the index, the compiler doesn't care what the array's data type is.

You can use either signed or unsigned for array index. But if signed, of course negative numbers shouldn't be used. Neither way should positive numbers beyond the array size.


Also if I add two unsigned ints together and put the sum in a signed int, will there be any unexpected conversions going on?

That really depends on what "unexpected" means! If your understanding of the binary math is perfect, then of course the results will always be exactly what you expected. Otherwise, generally no, you'll get something that's hard to anticipate when you assign a value from one variable type into another where it doesn't fit.

Signed integers have a positive range that's only half the range of unsigned. So when you add 2 unsigned together, if the sum is within the positive range of the signed integer, you will get the correct result when you store it in a signed integer. If the sum is greater than the allowed range, you will get something else. Since the sum can be up to 4 times the allowed range, you can get 3 distinct types of wrong results, depending on whether it's more than double or triple the allowed range. But you can be certain all will be wrong, since the sum was larger than the maximum positive number.


I'm wondering if it's ok to convert the unsigned into signed somewhere before?

Again, it really depends on whether the unsigned number is within the smaller unsigned range of the signed integer. If it is, you'll get the correct result by just setting it equal to the unsigned integer. But if the unsigned integer is too larger to fit into the positive portion of the signed integer, you'll get a wrong result. The bits are just copied as-is without any check to make sure the result will fit. If the number was too large, you'll get whatever signed number corresponds to that particular bit pattern, because the bits are just copied without as they are.

If you want to have some sort of check, you need to add code to perform it. For example, this might make sense if your unsigned integer could be larger than the signed integer range.

if (x <= 2147483647) n = x;
else n = 2147483647;

The important point to remember is the compiler doesn't not do this automatically if you just assign the number. An assignment without special code will just copy the binary bits. If those bits represented a number outside the allowed range of the destination variable, they will still be copied anyway. You will end up with whatever number that type of variable represents when it has that specific combination of bits stored.

For example, if the unsigned is a uint16_t holding the number 65534, when you copy that into a int16_t which has a maximum positive value of 32767, the bit pattern that was 65534 just happens to be the same bit pattern int16_t uses of -2.

So if you have a perfect understanding of what bit patterns represent which numbers, then the result will always be what you expected. But if you don't intuitively expect 65534 will become -2, then you'll get the unexpected result of -2.

Hopefully that answers the question?
 
Last edited:
To quickly answer your 3 specific questions...




The array index is independent of the type of array's actual data. You can have an array of float or an array of a struct. Regarding the index, the compiler doesn't care what the array's data type is.

You can use either signed or unsigned for array index. But if signed, of course negative numbers shouldn't be used. Neither way should positive numbers beyond the array size.




That really depends on what "unexpected" means! If your understanding of the binary math is perfect, then of course the results will always be exactly what you expected.

Signed integers have a positive range that's only half the range of unsigned. So when you add 2 unsigned together, if the sum is within the positive range of the signed integer, you will get the correct result. If the sum is greater than the allowed range, you will get something else.




Again, it really depends on whether the unsigned number is within the smaller unsigned range of the signed integer. If it is, you'll get the correct result by just setting it equal to the unsigned integer. But if the unsigned integer is too larger to fit into the positive portion of the signed integer, you'll get a wrong result. The bits are just copied as-is without any check to make sure the result will fit. If the number was too large, you'll get whatever signed number corresponds to that particular bit pattern, because the bits are just copied without as they are.

If you want to have some sort of check, you need to add code to perform it. For example, this might make sense if your unsigned integer could be larger than the signed integer range.

if (x <= 2147483647) n = x;
else n = 2147483647;

Thanks, I think this is very clear now.

So if we have 4 bits and want to convert an unsigned value of 1111 which is 15 to a signed 4 bit value, then that would convert into -7. However as long as the value is smaller than the maximum allowed, say 7 in this case it will convert into positive 7 because it is the same in binary (0111).
 
Status
Not open for further replies.
Back
Top