Teensy LC help needed, capturing multiple signals with interrupts.

Status
Not open for further replies.

ODSYViper

Member
Hi,

I'm working on a project using a Teensy LC, where I want to control and monitor 4x 4-wire PWM fans.

Currently I have two fans connected, using a separate 12v supply (with common ground to the teensy). I am sending PWM signals from the Teensy which seems to work fine for controlling fan RPM. The tach signals from the fans are open drain and are connected directly to Teensy pins with internal pull up resistors enabled.

I am having trouble timing the fan tach pulse width interrupts detecting the edge of the pulse, the first interrupt seems to work and gives an acceptable output, but the second one does not seem to work.

Part of the problem is that I can't find any documentation on what interrupts are available on what pins, or how many interrupts are available to use. Also tbh, I don't fully understand how attachinterrupt(digitalPinToInterrupt(fan1_tach), -, -) resolves.

Any help would be greatly appreciated.

Code:
const int fan1_tach = 10;
const int fan2_tach = 6;
const int fan1_enabled = 1;
const int fan2_enabled = 3;
const int fan1_pwm = 4;
const int fan2_pwm = 9;
const int led = 13;

volatile unsigned long tach1_pulseWidth[2];
volatile unsigned long tach2_pulseWidth[2];

void setup() {
  pinMode(fan1_tach, INPUT_PULLUP);
  pinMode(fan2_tach, INPUT_PULLUP);
  pinMode(fan1_enabled, OUTPUT);
  pinMode(fan2_enabled, OUTPUT);
  pinMode(fan1_pwm, OUTPUT);
  pinMode(fan2_pwm, OUTPUT);
  pinMode(led, OUTPUT);

  analogWriteFrequency(fan1_pwm, 25000);
  analogWriteFrequency(fan2_pwm, 25000);

  digitalWrite(fan1_enabled, LOW);
  digitalWrite(fan2_enabled, LOW);
  digitalWrite(led, HIGH);
  analogWrite(fan1_pwm, 128);
  analogWrite(fan2_pwm, 128);

  attachInterrupt(digitalPinToInterrupt(fan1_tach), fan1_ISR, FALLING);
  attachInterrupt(digitalPinToInterrupt(fan2_tach), fan2_ISR, FALLING);

  Serial.begin(9600);
}

void loop() {
  // put your main code here, to run repeatedly:
  delay(1000);
  Serial.print(tach1_pulseWidth[0] - tach1_pulseWidth[1]);
  Serial.print(", ");
  Serial.println(tach2_pulseWidth[0] - tach2_pulseWidth[1]);
}

void fan1_ISR() {
  tach1_pulseWidth[1] = tach1_pulseWidth[0];
  tach1_pulseWidth[0] = micros();
}

void fan2_ISR() {
  tach2_pulseWidth[1] = tach2_pulseWidth[0];
  tach2_pulseWidth[0] = micros();
}

output.PNG
 
With the Teensy card in hand - or the pinout image on PJRC.com - look at the card back side. Pins with interrupts are indicated as INT. Short list is :: NOT FOR PINS :: 0,1 and 16-19. Other pins in 2-23 exclusive of 16-19 will each get tagged with a unique function to call - the "what pin fired how" is handled before that call is made.

What is the repeat nature of the interrupt from those pins ? - like 1,000 to 3,000 ?

Exact nature of the T_LC interrupts shares a great deal of function from the other T_3.x and T_4.x units - with fewer levels of priority to customize that doesn't seem critical to deal with - but it may have some limitation - certainly slower response time and execution based on CPU clock - but some few thousand per second should work and a second interrupt at the same time should trigger on leaving the first - but will cause delay.

If speed in response with two competing pins is a problem they could be enabled in succession perhaps for measurable periods as needed - enough to get the true reading needed to make the needed adjustment. Enabling each in turn would allow perhaps a simple counter to be set zero before enabling and inc++ on each _isr() - ignoring it for some few ms to count - while calculating and adjusting the alternate fan. That would eliminate reading data from an active _isr as well, and dealing with atomic read/write or volatile variables. Measuring each in turn for 50ms would allow 10 updates to each per second for instance. A loop() watching an elapsedMicros or elapsedMicros variable for desired time then recording the time and detachInterrupt() and zero the other _isr() and attachInterrupt to that function while attending to the first one to update ... repeat keeping one of the two active.

code prototypes needed - just take pin# and digitalPinToInterrupt() is not needed:
Code:
attachInterrupt(uint8_t pin, void (*function)(void), int mode)

detachInterrupt(uint8_t pin)

Paul this page is apparently still Pre-ARM Teensy? :: pjrc.com/teensy/interrupts.html - confusing as not marked as such and not at all helpful for ARM Teensy - and not including names or example use of the above func()'s?
 
Appreciate the info defragster. I hadn't noticed the INT labels on the pinout, I think that might be the only information available regards to the interrupts on LC. So theoretically you could have all interrupts on all those pins at once? Regardless of the capture mode?

The max RPM of the fans is 2000, and they send two pulses per revolution, which I think means a 66Hz signal.

After removing the digitalPinToInterrupt() calls (no other changes) the values from both tachs are pretty similar now. Both of them now give low values for the pulse width at random times, so its not completely stable. This might be related to each ISR being delayed before the next one like you suggested.

I will try to implement your solution where each signal is sampled in isolation and let you know how it goes.
 
Don't be silly - every thing is fully documented ... in the source code :) - for T_LC when "defined(KINETISL)" ... much better than reading the manual.

Indeed the web pages need to have some updates as noted in p#2. But Bing or g00gle can search the forum if forum.pjrc search fails and there may be relevant details there. Except for code like below - the T_LC's M0 is very much like the non-LowCost M4 and M7 ARM Teensy if you find examples they should work well where Paul has worked to get functional equivalence.

This below from "...\hardware\teensy\avr\cores\teensy3\pins_teensy.c" Suggests indeed that each valid pin can have its own function selectively called it seems. { same file has detachinterrupt() and attachInterrupt()

It also shows using pins #3 and #4 will have less overhead in getting there if they can be used. There is even a comment noting why it differs ... M0 has no CLZ instruction that makes the KINETISK T_3.x's faster to process interrupts.
Code:
#elif defined(KINETISL)
// Kinetis L (Teensy LC) is based on Cortex M0 and doesn't have hardware
// support for CLZ.

#define DISPATCH_PIN_ISR(pin_nr) { voidFuncPtr pin_isr = intFunc[pin_nr]; \
                                   if(isfr & CORE_PIN ## pin_nr ## _BITMASK) pin_isr(); }

static void porta_interrupt(void)
{
	uint32_t isfr = PORTA_ISFR;
	PORTA_ISFR = isfr;
	DISPATCH_PIN_ISR(3);
	DISPATCH_PIN_ISR(4);
}

static void portcd_interrupt(void)
{
	uint32_t isfr = PORTC_ISFR;
	PORTC_ISFR = isfr;
	DISPATCH_PIN_ISR(9);
	DISPATCH_PIN_ISR(10);
	DISPATCH_PIN_ISR(11);
	DISPATCH_PIN_ISR(12);
	DISPATCH_PIN_ISR(13);
	DISPATCH_PIN_ISR(15);
	DISPATCH_PIN_ISR(22);
	DISPATCH_PIN_ISR(23);
	isfr = PORTD_ISFR;
	PORTD_ISFR = isfr;
	DISPATCH_PIN_ISR(2);
	DISPATCH_PIN_ISR(5);
	DISPATCH_PIN_ISR(6);
	DISPATCH_PIN_ISR(7);
	DISPATCH_PIN_ISR(8);
	DISPATCH_PIN_ISR(14);
	DISPATCH_PIN_ISR(20);
	DISPATCH_PIN_ISR(21);
}
#undef DISPATCH_PIN_ISR

#endif
 
So theoretically you could have all interrupts on all those pins at once? Regardless of the capture mode?

Yes, you can indeed use all those pins for interrupts.

However, in practice running each interrupt function takes time. Teensy LC runs at only 48 MHz and uses the Cortex-M0 processor, which gives you better speed than old 8 bit AVR boards (like Teensy 2.0 and Arduino Uno & Mega), but the performance is nowhere near what you get from the higher end Teensy models.

How long each interrupt takes depends largely on your code inside that interrupt function. But there is also some overhead just to run the function, almost 1 microsecond on Teensy LC. Let's imagine you put some substantial but well optimized code into those functions which makes the total time 10 us.

If you use 20 interrupts and the stars line up just right so they all want to run at the same instant, of course they will be run in sequence. The M0 (and other ARM processors) does do a "tail chain" optimization to more efficiently transition between them, so let's imagine the first takes 10 us and all the others take 9.7 us. My calculator says the time for the first 19 adds up to 184.6 us. So whatever that 20th interrupt will do gets delayed at least that long. If you're reading a 66 Hz signal, the good news is the period is 15151 us, so this bad-case interrupt latency is only 1.2% of the waveform's period.

But as you try to do more inside the interrupt which lengthens the time is runs, and as you try to use it for faster signals or other uses which can't tolerate as much latency, you can quickly get into a situation where the worst case latency is too long.

This isn't unique to Teensy LC. Interrupt latency is a general issue affecting all systems. Sadly, most of the tutorials you'll find online are written for beginners and they rarely cover these more advanced concepts. But hopefully this message at least helps give you a picture that you can indeed use 20 interrupts and how the practical limitations come into play if you do.
 
Yes, you can indeed use all those pins for interrupts.

However, in practice running each interrupt function takes time. Teensy LC runs at only 48 MHz and uses the Cortex-M0 processor, which gives you better speed than old 8 bit AVR boards (like Teensy 2.0 and Arduino Uno & Mega), but the performance is nowhere near what you get from the higher end Teensy models.

How long each interrupt takes depends largely on your code inside that interrupt function. But there is also some overhead just to run the function, almost 1 microsecond on Teensy LC. Let's imagine you put some substantial but well optimized code into those functions which makes the total time 10 us.

If you use 20 interrupts and the stars line up just right so they all want to run at the same instant, of course they will be run in sequence. The M0 (and other ARM processors) does do a "tail chain" optimization to more efficiently transition between them, so let's imagine the first takes 10 us and all the others take 9.7 us. My calculator says the time for the first 19 adds up to 184.6 us. So whatever that 20th interrupt will do gets delayed at least that long. If you're reading a 66 Hz signal, the good news is the period is 15151 us, so this bad-case interrupt latency is only 1.2% of the waveform's period.

But as you try to do more inside the interrupt which lengthens the time is runs, and as you try to use it for faster signals or other uses which can't tolerate as much latency, you can quickly get into a situation where the worst case latency is too long.

This isn't unique to Teensy LC. Interrupt latency is a general issue affecting all systems. Sadly, most of the tutorials you'll find online are written for beginners and they rarely cover these more advanced concepts. But hopefully this message at least helps give you a picture that you can indeed use 20 interrupts and how the practical limitations come into play if you do.

Thankyou for the detailed response, that makes the operation of the LC interrupts a bit clearer. You are definitely right about it being difficult to find solutions for more intermediate topics.

Unfortunately I'm still having the same issue i was originally, I have tried a solution where each interrupt was allowed to run in isolation without any other interrupts enabled but the results were similar.

This is my thought process at the moment. If I only have two signals at 66Hz and these are the only interrupts enabled then the error from the possible interrupt delay should be very minimal, so there should not be any need to manage them while still getting accurate results. The first interrupt seems to give pretty consistent results with a measurement of ~26000 us, or 1150 RPM after adjusting for two pulses per revolution. This seems about right but i have no way of verifying the RPM. The second interrupt produces odd readings.

I'm using the same code as above, just with the digitalPinToInterrupt() calls removed as defragster suggested. This is the type of input I am getting.

SerialOutput.PNG

Maybe I need an oscilloscope so that I can verify that the fan tach signals are as they should be?
 
Is there any chance you could create a program which demonstrates the problem using analogWriteFrequency() and analogWrite() to create the test waveforms? That would allow any of us to run it on our Teensy LC using only some wires to connect pins together.
 
Is there any chance you could create a program which demonstrates the problem using analogWriteFrequency() and analogWrite() to create the test waveforms? That would allow any of us to run it on our Teensy LC using only some wires to connect pins together.

Good and bad news. Good, when timing the signal generated with analogWriteFrequency() the program works well, values generated for the pulse width are very accurate. Bad news, now I have to figure out why the tach waveforms from the fans aren't correct :(

Code:
const int tach_signal_1 = 17;
const int tach_signal_2 = 16;
const int tach_input_1 = 15;
const int tach_input_2 = 14;
const int led = 13;

volatile unsigned long tach1_pulseWidth[2];
volatile unsigned long tach2_pulseWidth[2];

void setup() {
  pinMode(tach_signal_1, OUTPUT);
  pinMode(tach_signal_2, OUTPUT);
  pinMode(tach_input_1, INPUT_PULLUP);
  pinMode(tach_input_2, INPUT_PULLUP);
  pinMode(led, OUTPUT);

  analogWriteFrequency(tach_signal_1, 38);
  analogWriteFrequency(tach_signal_2, 38);
  analogWrite(tach_signal_1, 128);
  analogWrite(tach_signal_2, 128);

  digitalWrite(led, HIGH);

  attachInterrupt(tach_input_1, tach1_ISR, RISING);
  attachInterrupt(tach_input_2, tach2_ISR, RISING);

  Serial.begin(9600);
}

void loop() {
  // put your main code here, to run repeatedly:
  delay(1000);
  Serial.print(tach1_pulseWidth[0] - tach1_pulseWidth[1]);
  Serial.print(", ");
  Serial.println(tach2_pulseWidth[0] - tach2_pulseWidth[1]);
}

void tach1_ISR() {
  tach1_pulseWidth[1] = tach1_pulseWidth[0];
  tach1_pulseWidth[0] = micros();
}

void tach2_ISR() {
  tach2_pulseWidth[1] = tach2_pulseWidth[0];
  tach2_pulseWidth[0] = micros();
}

Here's the output:
SerialOutput2.PNG
 
Maybe for a quick sanity check, try using the FreqCount and FreqMeasure libraries? They only support specific pins, so you'll probably need to move the signal to another pin. But they both have ready-to-go examples you can just run and see numbers printed in the serial monitor.
 
Status
Not open for further replies.
Back
Top