Teensy 4: Measuring multiple frequencies

JoseanB

Member
I have a several sensors outputting frequencies that I want to monitor. Is it possible to modify freqcount to measure the frequency of 4 of these sensors? I understand that the counter input pin is 9 on Teensy 4, but could the other timer/counters be utilized as well or can the measurement also be coupled to other pins somehow? The frequencies are in the MHz range so a counter that operates similarly to freqcount is required for accuracy. Any guidance on this would greatly be appreciated. Right now I'm using a MUX to switch between sensors, but I'd rather try to get rid of it.
Thanks
-Jose
 
Some frequency to voltage converters might be one approach, then you can use ADC inputs.
 
I did, but it only measures up to 1 MHz. I need to go up to 14 MHz. Can the upper limit be increased??

The only reason I can think of that FreqMeasureMulti would be limited to 1 MHz is that there is an interrupt per input period, so with multiple signals over 1 MHz, the processor could become overwhelmed. If you're using T4, the FlexTimer has an option not currently supported by FreqMeasureMulti to measure the duration of more than 1 period. There is a 1-byte edge-count register, so you can measure over a period of up to 255 total edges, or I think 254/2 = 126 full periods of the input. That would reduce the interrupt rate by a factor of 126 and give you much better resolution than single-period measurements. You would still be getting over 100,000 measurements per second at 14 MHz. Would that be enough. I used this edge-count option once, and I can dig it up and post it later.
 
The only reason I can think of that FreqMeasureMulti would be limited to 1 MHz is that there is an interrupt per input period, so with multiple signals over 1 MHz, the processor could become overwhelmed. If you're using T4, the FlexTimer has an option not currently supported by FreqMeasureMulti to measure the duration of more than 1 period. There is a 1-byte edge-count register, so you can measure over a period of up to 255 total edges, or I think 254/2 = 126 full periods of the input. That would reduce the interrupt rate by a factor of 126 and give you much better resolution than single-period measurements. You would still be getting over 100,000 measurements per second at 14 MHz. Would that be enough. I used this edge-count option once, and I can dig it up and post it later.

Hi MarkT!

If you could post it, that would be very much appreciated!

Are you referring to this? I found this in the readme. I tried this and it doesn't seem to make an improvement, unless I'm doing something wrong.

As in the v0.1, an instance can be started with begin(pin). More capture modes are available through begin(pin, mode) by using the following constants for mode as follows :
* FREQMEASUREMULTI_RAISING -> A new value becomes available at each rising ramp and contains the time since the previous rising ramp (default value when mode is not given in begin(pin))
* FREQMEASUREMULTI_FALLING -> A new value becomes available at each falling ramp and contains the time since the previous falling ramp.
* FREQMEASUREMULTI_INTERLEAVE -> A new value becomes available at each rising and each falling ramp and contains the time since the previous similar (rising or falling) ramp. That virtually doubles the actualization rate and allows quicker tracking of variations in the signal frequency.
* FREQMEASUREMULTI\_SPACE\_ONLY -> A new value becomes available at each rising ramp and contains the time since the previous falling ramp. Thus, only the "low time" of the signal period is returned
* FREQMEASUREMULTI\_MARK\_ONLY -> A new value becomes available at each falling ramp and contains the time since the previous rising ramp. Thus, only the "high time" of the signal period is returned
* FREQMEASUREMULTI_ALTERNATE -> A new value becomes available at each rising and each falling ramp and contains the time since the previous ramp. Thus, the high and the low time of the signal period are returned alternating.
 
Are you referring to this?

No, I'm referring to a feature of the timer that is not exposed in the existing library. In addition to being able to choose among rising/falling/etc edges, there is also a feature to set the NUMBER of edges over which to measure. If you search the manual for EDGCNT, you will see it.

These source files and example sketch extend the T4.x FreqMeasureMulti to have an additional version of begin() with an "edgcnt" argument. When edgcnt > 0, the edge-count feature is enabled and configured in the CPP file. To use the updated version of the library, COPY the FreqMeasureMulti folder from your TeensyDuino library folder to the "libraries" folder of your sketch folder, then replace the FreqMeasureMultiIMXRT source and example files with the files attached below. You probably need to close and re-open Arduino, and then you should be able to access this updated example via the Examples menu in the Arduino IDE. You will see the code below at the end of setup(). This configures each of the three instances of FreqMeasureMulti to measure over 200 edges (100 full periods) of the input signal. You can modify this number as necessary. I've been meaning to do a pull request to ask Paul to add this feature to the driver, but I felt like I had not tested it enough. It does work, though. One detail to mention is that the "edgcnt" value is always the number total edges (rising + falling), no matter whether the "mode" argument is rising/falling/whatever. It would be nice if it was the number of edges that you choose with mode, but it doesn't seem to work that way.

Code:
  mode = 1;	// RISING EDGES
  edgcnt = 200;	// 200 rising and falling edges
  freq1.begin( 5, mode, edgcnt );
  freq2.begin( 6, mode, edgcnt );
  freq3.begin( 7, mode, edgcnt );
 

Attachments

  • FreqMeasureMultiIMXRT.h
    1.9 KB · Views: 104
  • FreqMeasureMultiIMXRT.cpp
    21.6 KB · Views: 93
  • Three_PWM_in_Serial_Output.ino
    2 KB · Views: 88
Hi Mart T!

The code worked and it can count above 15MHz, however, the resolution is very poor. I need to be able to resolve changes by 100's of MHz. Changing the edgcnt seems to change the upper limit, but not the resolution. Any thoughts on this? I'm guessing this is a fundamental limitation with freqmeasure. Freqcount, I'm able to measure 13MHz signal and resolve 10 Hz changes. I'm using a signal generator by the way.
 
Hello. This is Joe, not MarkT. With FreqCount, you are counting pulses over a specified time period. If you count for 1 second, you can get 1-Hz resolution. With FreqMeasureMulti, you are measuring the period (time) for EDGCNT edges, and because the clock frequency is quite high (150 MHz), you can get even better resolution with FreqMeasureMulti by adding together measurements. The example program computes the average of each of the three frequency inputs over all of the measurements that occur within a 500-ms period. It generates PWM on pins 10,11,12 for measurement on pins 5,6,7. The output is shown below. These are 0.5-second measurements, and the resolution is ~0.1 Hz for the 10 MHz signal, and even better for lower frequencies. The first value is the total number of measurements in 0.5 seconds. The second number is the sum of clock counts over the measurements. The third number is the average frequency in Hz. If you measure over a longer period, you get more resolution, and if you measure over a shorter period, you get less resolution.

Code:
loopcount = 3386544
      count       sum      frequency
      -----    ---------  -------------
1:   50000   75021040 10000000.00
2:   25000   75000000 5000000.00
3:   10000   75004208 2000000.00
 
Hey Joe! Sorry about that!

I ran the script just as you sent it and I get the following:


loopcount = 3552560
1: 50000 75021040 10000000.00
2: 25000 75000000 5000000.00
3: 10000 75004208 2000000.00


Similar to yours. However if I put in the code to change the frequency by 1KHz, I get the same results. Are you saying you get this to work on your end? I also confirm this with my signal generator.

analogWriteResolution(8); // (10);
analogWriteFrequency(10, 10001000);
 
However if I put in the code to change the frequency by 1KHz, I get the same results. Are you saying you get this to work on your end? I also confirm this with my signal generator.

analogWriteResolution(8); // (10);
analogWriteFrequency(10, 10001000);

When you request 10.001 MHz on the T4.1, you get the same signal as when you ask for exactly 10 MHz. This is because the T4.1 clock is 150 MHz, so a 10-MHz PWM period is only 15 clocks. From that point, you can only make very rough jumps in frequency. These are the frequencies >= 10 MHz that you can generate with F_TMR = 150 MHz (F_CPU = 600 MHz)

Code:
clocks     Hz
150/15    10,000,000
150/14    10,714,286
150/13    11,538,462
150/12    12,500,000
150/11    13,636,363
150/10    15,000,000
150/9      16,666,666
150/8      18,750,000

My function generator only goes to 3.1 MHz, and it shows the same behavior as I get close to that value. Unless you have a function generator than can go to very high frequency (~1 GHz?), then you may be seeing the same thing. You can experiment with lower frequencies to show that the resolution is very good. For example, if I set PWM frequency to 15000, I get exactly 15000, and if I request 15001, I get 15001.50, which is the frequency that is actually generated.

What will be the source of the signal in your application?
 
In my application, the source of the signals are oscillators with an output of about 10-13MHz. When the sense the target VOC, the frequency changes by 100's of Hz so having the kind of resolution is critical.

Yes, the code measures frequencies from 100 Hz all the way up to 15 MHz (my signal generator goes up to 30 MHz) and even resolves frequencies like 101 and 102 Hz. However, what I'm seeing on my end is that as I bring the frequency up to 10 MHz and change the frequency by 100 Hz on the signal generator, it can't see the change. Even at 1 MHz with the signal generator, the result is 1.006711.44 MHz. If I change it to 1.0001 MHz I still get the same 1.006711.44 MHz.

I tried changing the timeout but this didn't seem to help either. Thanks again for the help!
 
Yes, what I’m saying is that the output of your function generator is the same when you enter 10,000,100 as it is when you enter exactly 10. You have the resolution in the measurement but not in the test signal.
 
Ah sorry. I forgot to mention that. I did confirm the signal generator frequencies with the FreqCount program. I can change the signal frequency at 15 MHz by 100 Hz and the freqcount program will track the same amount of change. It will even do 1 Hz.
 
Ah sorry. I forgot to mention that. I did confirm the signal generator frequencies with the FreqCount program. I can change the signal frequency at 15 MHz by 100 Hz and the freqcount program will track the same amount of change. It will even do 1 Hz.

Okay, thanks for checking that. Something must be wrong in my calcs. I will look into it.
 
I can't try this until tomorrow, but I think you'll get a better result by changing this line

Code:
    freq = (count1==0) ? 0 : freq1.countToFrequency( sum1 / (float)(count1*edgcnt/2) );

to

Code:
    freq = count1 * (edgcnt/2.0) * freq1.countToFrequency( sum1 );

The argument to countToFrequency() is type uint32_t, so the original code was throwing away the fractional part of the average period measurement.
 
Last edited:
Ah of course! That solved the problem. It doesn't have the 1Hz resolution of freqcount, but definitely in the 10's of Hz. Thank you so much!

-Jose
 
Ah of course! That solved the problem. It doesn't have the 1Hz resolution of freqcount, but definitely in the 10's of Hz. Thank you so much


You're welcome. You should be able to get 0.1 Hz resolution with FreqMeasureMulti. Try the changes below.

The type of the "sum" variables should be uint32_t rather than float

Code:
uint32_t sum1=0, sum2=0, sum3=0;
double freq;

and the frequency calculation should benefit from using double precision. For example, I think you need double precision to resolve a value of 15,000,000.1 Hz.

Code:
    freq = count1 * (edgcnt/2.0) * ((double)F_BUS_ACTUAL / sum1);
    Serial.printf( "1: %7d %10lu %10.2lf\n", count1, sum1, freq );
 
Wow! Now its even better! Its much more stable and looks to behave exactly as FreqCount!

I also noticed that pins 2,4,& 7 don't appear to don't work for this. According to the documentation, they are supposed to. This is more of a curiosity, but any idea why that might be?

Thanks again! This has been a huge help.
 
Wow! Now its even better! Its much more stable and looks to behave exactly as FreqCount!

For a given total measurement period, it should be better than FreqCount. For lower frequencies, you can get a very accurate measurement in a short time. For high frequencies, you need to accumulate over many readings.

I also noticed that pins 2,4,& 7 don't appear to don't work for this.

They work for me. Here is the complete test sketch. Outputs on 10,11,12 with jumpers to inputs on 2,4,7.

Code:
/* FreqMeasureMulti - Example with serial output
 * http://www.pjrc.com/teensy/td_libs_FreqMeasure.html
 *
 * This example code is in the public domain.
 */
#include <FreqMeasureMulti.h>

// Measure 3 frequencies at the same time! :-)
FreqMeasureMulti freq1;
FreqMeasureMulti freq2;
FreqMeasureMulti freq3;

uint8_t mode, edgcnt;

void setup() {
  Serial.begin(57600);
  while (!Serial) ; // wait for Arduino Serial Monitor
  analogWriteResolution(8); // (10);
  analogWriteFrequency(10, 15'000'000);
  analogWrite(10, 50);
  analogWriteFrequency(11, 15'000'000);
  analogWrite(11, 50);
  analogWriteFrequency(12, 15'000'000);
  analogWrite(12, 50);
  
  delay(10);
  Serial.println("FreqMeasureMulti Begin");
  delay(10);
  
  mode = 1;	// RISING EDGES
  edgcnt = 200;	// 200 rising and falling edges
  freq1.begin( 2, mode, edgcnt ); // 7);
  freq2.begin( 4, mode, edgcnt ); // 8);
  freq3.begin( 7, mode, edgcnt ); // 0);
}
uint32_t sum1=0, sum2=0, sum3=0;
uint32_t count1=0, count2=0, count3=0, loopcount=0;
double freq;
elapsedMillis timeout;

void loop() {
  if (freq1.available()) {
    sum1 = sum1 + freq1.read();
    count1 = count1 + 1;
  }
  if (freq2.available()) {
    sum2 = sum2 + freq2.read();
    count2 = count2 + 1;
  }
  if (freq3.available()) {
    sum3 = sum3 + freq3.read();
    count3 = count3 + 1;
  }
  loopcount++;
  // print results every half second
  if (timeout >= 1000) {
    Serial.printf( "loopcount = %1d\n", loopcount );
    
    //freq = count1 * (edgcnt/2.0) * freq1.countToFrequency( sum1 );
    freq = count1 * (edgcnt/2.0) * ((double)F_BUS_ACTUAL / sum1);
    Serial.printf( "1: %7d %10lu %12.2lf\n", count1, sum1, freq );
    
    freq = count2 * (edgcnt/2.0) * ((double)F_BUS_ACTUAL / sum2);
    Serial.printf( "2: %7d %10lu %12.2lf\n", count2, sum2, freq );
    
    freq = count3 * (edgcnt/2.0) * ((double)F_BUS_ACTUAL / sum3);
    Serial.printf( "3: %7d %10lu %12.2lf\n", count3, sum3, freq );
    
    Serial.println();
    sum1 = 0;
    sum2 = 0;
    sum3 = 0;
    count1 = 0;
    count2 = 0;
    count3 = 0;
    loopcount = 0;
    timeout = 0;
  }
}
 
For a given total measurement period, it should be better than FreqCount. For lower frequencies, you can get a very accurate measurement in a short time. For high frequencies, you need to accumulate over many readings.

They work for me. Here is the complete test sketch. Outputs on 10,11,12 with jumpers to inputs on 2,4,7.

...

Very cool - impressive addition - it works here jumpers as indicated.

Works on these edited boundaries too:
Code:
  analogWriteFrequency(10, 18'750'000);
  analogWrite(10, 50);
  analogWriteFrequency(11, 12'500'000);
  analogWrite(11, 50);
  analogWriteFrequency(12, 10'000'000);
  analogWrite(12, 50);

Code:
loopcount = 5690466
1:  187500  150000000  18750000.00
2:  125000  150000000  12500000.00
3:  100000  150000000  10000000.00

And you don't even need to check .available() that often for same results! Doing each 100 ms bumps to: loopcount = 8743799. So doing other tasks won't miss - at least with fixed freq?
Code:
loopcount = 8737969
1:    2110    1688000  18750000.00
2:    1480    1776000  12500000.00
3:    1230    1845000  10000000.00
From - results same without while():
Code:
// ...
  if ( !(timeout%100) ) {
    while (freq1.available()) {
      sum1 = sum1 + freq1.read();
      count1 = count1 + 1;
    }
    while (freq2.available()) {
      sum2 = sum2 + freq2.read();
      count2 = count2 + 1;
    }
    while (freq3.available()) {
      sum3 = sum3 + freq3.read();
      count3 = count3 + 1;
    }
  }
  loopcount++;
// ...
 
Might be worth mentioning FreqCount is using QuadTimer 4 for counting and IntervalTimer for the gate interval.

Theoretically, it should be possible to craft code which would use all 4 of the QuadTimers and 1 IntervalTimer. But doing so would be involve a pretty substantial effort.
 
Here's a quick experiment to extend FreqCount to 10 simultaneous inputs.

Or at least a 16 bit counting version which accumulates groups of 16 bit counts, rather than extending the count with overflow interrupts as the FreqCount library does. Sound work the same, but requires keeping the gate interval short to count higher frequencies due to the 16 bit hardware count size.

Code:
// https://forum.pjrc.com/threads/71193-Teensy-4-Measuring-multiple-frequencies

// Timer	Pin   Pad	ALT	input mux
// QuadTimer4_1   6   B0_10	1
// QuadTimer4_2   9   B0_11	1
// QuadTimer1_0  10   B0_00	1
// QuadTimer1_2  11   B0_02	1
// QuadTimer1_1  12   B0_01	1
// QuadTimer2_0  13   B0_03	1	IOMUXC_QTIMER2_TIMER0_SELECT_INPUT=1
// QuadTimer3_2  14   AD_B1_02	1	IOMUXC_QTIMER3_TIMER2_SELECT_INPUT=1
// QuadTimer3_3  15   AD_B1_03	1	IOMUXC_QTIMER3_TIMER3_SELECT_INPUT=1
// QuadTimer3_1  18   AD_B1_01	1	IOMUXC_QTIMER3_TIMER1_SELECT_INPUT=1
// QuadTimer3_0  19   AD_B1_00	1	IOMUXC_QTIMER3_TIMER0_SELECT_INPUT=1

#define GATE_INTERVAL 2000  // microseconds for each gate interval
#define GATE_ACCUM    100   // number of intervals to accumulate
#define MULT_FACTOR   5     // multiply to get Hz output

typedef struct {
	IMXRT_TMR_t *timer;
	int timerchannel;
	int pin;
	int pinconfig;
	volatile uint32_t *inputselectreg;
	int inputselectval;
} timerinfo_t;

const timerinfo_t timerlist[] = {
	// Timer     Ch  Pin  Alt Input Select
	{&IMXRT_TMR4, 1,   6,  1, NULL, 0},
	{&IMXRT_TMR4, 2,   9,  1, NULL, 0},
	{&IMXRT_TMR1, 0,  10,  1, NULL, 0},
	{&IMXRT_TMR1, 2,  11,  1, NULL, 0},
	{&IMXRT_TMR1, 1,  12,  1, NULL, 0},
	{&IMXRT_TMR2, 0,  13,  1, &IOMUXC_QTIMER2_TIMER0_SELECT_INPUT, 1},
	{&IMXRT_TMR3, 2,  14,  1, &IOMUXC_QTIMER3_TIMER2_SELECT_INPUT, 1},
	{&IMXRT_TMR3, 3,  15,  1, &IOMUXC_QTIMER3_TIMER3_SELECT_INPUT, 1},
	{&IMXRT_TMR3, 1,  18,  1, &IOMUXC_QTIMER3_TIMER1_SELECT_INPUT, 0},
	{&IMXRT_TMR3, 0,  19,  1, &IOMUXC_QTIMER3_TIMER0_SELECT_INPUT, 1},
	// TODO: can 6 more be used with XBAR1 and GPR6 ?
};

#define NUM_TIMERS (sizeof(timerlist) / sizeof(timerinfo_t))

// gate interval interrupt deposits data here
volatile bool count_update = false;
volatile uint32_t count_output[NUM_TIMERS];

uint16_t read_count(unsigned int n) {
	static uint16_t prior[NUM_TIMERS];
	if (n >= NUM_TIMERS) return 0;
	uint16_t count = (timerlist[n].timer)->CH[timerlist[n].timerchannel].CNTR;
	uint16_t inc = count - prior[n];
	prior[n] = count;
	return inc;
}

void gate_timer() {
	static unsigned int count = 0;
	static uint32_t accum[NUM_TIMERS];

	for (unsigned int i=0; i < NUM_TIMERS; i++) {
		accum[i] += read_count(i);
	}
	if (++count >= GATE_ACCUM) {
		for (unsigned int i=0; i < NUM_TIMERS; i++) {
			count_output[i] = accum[i];
			accum[i] = 0;
		}
		count_update = true;
		count = 0;
	}
}

void setup() {
	// create some test frequencies
	analogWriteFrequency(0, 3000000);
	analogWriteFrequency(1, 15000000);
	analogWriteFrequency(2, 220000);
	analogWriteFrequency(4, 10000000);
	analogWriteFrequency(5, 455000);
	analogWrite(0, 128);
	analogWrite(1, 128);
	analogWrite(2, 128);
	analogWrite(4, 128);
	analogWrite(5, 128);

	// Welcome message
	Serial.begin(9600);
	Serial.print("FreqCountMany, maximum frequency = ");
	Serial.print(65535.0 / ((double)GATE_INTERVAL), 3);
	Serial.println(" MHz");

	// turn on clock to all quad timers
	CCM_CCGR6 |= CCM_CCGR6_QTIMER1(CCM_CCGR_ON) | CCM_CCGR6_QTIMER2(CCM_CCGR_ON)
		| CCM_CCGR6_QTIMER3(CCM_CCGR_ON) | CCM_CCGR6_QTIMER4(CCM_CCGR_ON);

	// configure all counting timers
	for (unsigned int i=0; i < NUM_TIMERS; i++) {
		IMXRT_TMR_t *timer = timerlist[i].timer;
		int ch = timerlist[i].timerchannel;
		timer->CH[ch].CTRL = 0;
		timer->CH[ch].CNTR = 0;
		timer->CH[ch].LOAD = 0;
		timer->CH[ch].COMP1 = 65535;
		timer->CH[ch].CMPLD1 = 65535;
		timer->CH[ch].SCTRL = 0;
		timer->CH[ch].CTRL = TMR_CTRL_CM(1) | TMR_CTRL_PCS(ch) | TMR_CTRL_LENGTH;
		int pin = timerlist[i].pin;
		*portConfigRegister(pin) = timerlist[i].pinconfig;
		if (timerlist[i].inputselectreg) {
			*timerlist[i].inputselectreg = timerlist[i].inputselectval;
		}
	}

	// start gate interval timer
	static IntervalTimer t;
	t.begin(gate_timer, GATE_INTERVAL);
}

void loop() {
	if (count_update) {
		for (unsigned int i=0; i < NUM_TIMERS; i++) {
			Serial.printf("%8u ", count_output[i] * MULT_FACTOR);
		}
		Serial.println();
		count_update = 0;
	}
}

Signal inputs are meant to be on pins 6, 9, 10, 11, 12, 13, 14, 15, 18, 19.
 
Here's a quick experiment to extend FreqCount to 10 simultaneous inputs.

@PaulStoffregen, FreqCountMulti?

My change to FreqMeasureMulti was to add support for the EDGCNT feature of FlexPWM, to measure period over a specified number of edges instead of a single period or half period. This extends the capability to use FreqMeasureMulti for higher frequency, and is also useful if you want to measure over a specific number of periods, such as a full revolution of a tachometer. The changes are pretty small:

- add a new private data member _edgcnt
- add a new begin() function with a third argument edgcnt
- add logic to enable and set the EDGCNT register if appropriate
- modify countToFrequency() to return double instead of float (???)

There are no changes to the interrupt processing, posting data to the queue, or anything else. Could changing countToFrequency() to return double break existing applications? If so, would it be better to add a new countToFreqDouble() function? The OP's use case requires double precision, but most uses likely don't.

There is still one thing I should try to understand, which is how the EDGCNT feature interracts with the various edge specifiers. It seems to ignore them, but I haven't seen that for sure in the manual (yet). I can figure out how to do a PR if all of this sounds okay.
 
Wow this is great! Thanks for the addition, Paul!

You guys can probably answer this question, but I've been goin through the ARM M7 manual trying to understand this a bit more.
Joe mentioned the internal clock is 150 MHz. Is this the internal 600 MHz oscillator of the the M7 with a divide by 4? Is this the default clock utilized for the quad timers and general purpose timers? From what I understand, the GPT has a 32 bit counter which would naturally yield better precision for frequency counting.
 
Back
Top