I've been playing with Arduino on and off for years, today have been trying to learn how to use the timer interrupts by using them to emulate the output of a flow meter I'm expecting to use for a project in the new year. I don't have the meter but have a Nano clone with some code on i'd like to test before it arrives. To test it, i'm attempting to use another arduino to emulate the expected open-collector output from the meter.
The intention of the below code is to use Timer1 (on the chip, not the lib) channel A to output an inverted fast PWM signal between 0.28hz to 112hz. The duty cycle doesn't matter - ultimately the falling edge (of the inverted signal) just has to be in the right place.
Seems to work quite well except for the fact that the interrupts just don't get called for seconds at a time, every few seconds. It's a bit strange and I haven't had any luck figuring it out. I get the same issue on a Nano and a Mega 2560.
Can anybody see anything obviously wrong? All my vars in loop() get updated as I expect, and ICR1 gets updated as I expect, it's just that going by the increments I've placed in the ISRs they are just not getting called - sometimes - and I can't figure out why..
I'm a rather occasional programmer so I'm expecting PEBCAK...
Many thanks in advance!
Cheers
uint8_t NPulsesPerLitre = 168;
double qFlowMax = 40; // lpm
double qFlowMin = 5; // lpm
double q2freq(double flow)
{
double freqOut = flow/60 * NPulsesPerLitre;
return freqOut;
}
double fMax = q2freq(qFlowMax); //
double fMin = q2freq(qFlowMin); //
uint16_t f2ICR1(double freq)
{
uint16_t NewICR1 = 1/freq*16e6/1024; //number of cycles of counter to give this frequency
return NewICR1;
}
int NOvfCounts = 0;
void setup() {
// put your setup code here, to run once:
Serial.begin(9600);
noInterrupts();
// We use timer1 (TCNT1) generate a fixed width pulse with a variable freqency
//
// FREQUENCY
// simulating 168 pulses per litre
// simulating min 0.1 litre/min = 0.28 hz
// 16Mhz on timer1 (16bit/65535 max) without prescaler is 16,000,000/65535=244hz min.
// with 1024 prescaler it is ~0.24hz min with clearing on compare against 65535
// simulating max 40 litre/min = 112 hz
// with 1024 prescaler we have 15625hz fundamental which gives 112hz requires clearing at compare against 139
//
// PULSEWIDTH
// use a fixed PWM pulewidth of 138 cycles (8.832ms) gives max duty of 99.3% and min duty of 0.2%
// need to invert output as is an open collector NPN on the flowmeter
pinMode(11,OUTPUT); //11 for Mega2560, 9 for Nano clone
// TCCR1 is the Timer Counter Control Register and sets up the behaviour.
// TCCR1A
TCCR1A = 0; // clear default
TCCR1A |= (1<<COM1A1) | (1<<COM1A0); // inverted fast PWM mode on output A
TCCR1A |= (1<<WGM11); // one of the three bits required for mode 14 "Fast PWM" with TOP from ICR1 (other two are in TCCR1B)
//TCCR1B
TCCR1B = 0; // clear default
TCCR1B |= (1<<WGM13) | (1<<WGM12); // the other two of the three bits required for mode 14 "Fast PWM" with TOP from ICR1
TCCR1B |= (1<<CS12) | (1<<CS10); //gives 1024 prescaler
ICR1 = f2ICR1(fMin); // setting TOP to max freq value for start (~4 sec period)
OCR1A = 138; // fixed pulsewidth (inverted)
TIMSK1 = 0;
TIMSK1 |= (1<<OCIE1A) | (1<<TOIE1); //enable overflow and match interrupts
interrupts();
}
void loop() {
// put your main code here, to run repeatedly:
Serial.println(NOvfCounts);
unsigned long tNow = millis();
//Serial.println(tNow);
double sinRef = sin(double(tNow)/1000*2*PI/10); //10 sec sin wave
//Serial.println(sinRef);
double fNow = q2freq(sinRef*(qFlowMax-qFlowMin)/2 + (qFlowMax+qFlowMin)/2); // scale to flow rate min/max and convert to frequency
//Serial.println(fNow);
ICR1 = f2ICR1(fNow);
//Serial.println(ICR1);
delay(100); // just to slow down Serial scroll rate...
}
ISR(TIMER1_OVF_vect)
{
//NOvfCounts++;
}
ISR(TIMER1_COMPA_vect)
{
NOvfCounts++;
}
I think that in the mode you are using, settings are not buffered and if at the time ICR1 is reset the TCNT value is greater than the new value, the timer will carry on to the maximum 0xFFFF.
You may want to look at changing the new top value in an overflow interrupt or some other controlled manner.
OK I understand that yes...I did assume that it did a >= comparison and not ==...However when it gets stuck it gets stuck for what must easily be thousands of cycles so I don't think it's this, but will take a look regardles.. I've also added noInterrupts() and interrupts() before and after for good measure in case it was only getting half changed when the interrupt happened...that didn't help..
Consider changing the value in ICR1 in the COMPA ISR, not randomly outside. This gives a measure of synchronization of updates with the timer itself.
uint8_t NPulsesPerLitre = 168;
double qFlowMax = 40; // lpm
double qFlowMin = 5; // lpm
volatile uint16_t x;
double q2freq(double flow)
{
double freqOut = flow/60 * NPulsesPerLitre;
return freqOut;
}
double fMax = q2freq(qFlowMax); //
double fMin = q2freq(qFlowMin); //
uint16_t f2ICR1(double freq)
{
uint16_t NewICR1 = 1/freq*16e6/1024; //number of cycles of counter to give this frequency
return NewICR1;
}
int NOvfCounts = 0;
void setup()
{
// put your setup code here, to run once:
Serial.begin(19200);
noInterrupts();
// We use timer1 (TCNT1) generate a fixed width pulse with a variable freqency
//
// FREQUENCY
// simulating 168 pulses per litre
// simulating min 0.1 litre/min = 0.28 hz
// 16Mhz on timer1 (16bit/65535 max) without prescaler is 16,000,000/65535=244hz min.
// with 1024 prescaler it is ~0.24hz min with clearing on compare against 65535
// simulating max 40 litre/min = 112 hz
// with 1024 prescaler we have 15625hz fundamental which gives 112hz requires clearing at compare against 139
//
// PULSEWIDTH
// use a fixed PWM pulewidth of 138 cycles (8.832ms) gives max duty of 99.3% and min duty of 0.2%
// need to invert output as is an open collector NPN on the flowmeter
pinMode(11,OUTPUT); //11 for Mega2560, 9 for Nano clone
// TCCR1 is the Timer Counter Control Register and sets up the behaviour.
// TCCR1A
TCCR1A = 0; // clear default
TCCR1A |= (1<<COM1A1) | (1<<COM1A0); // inverted fast PWM mode on output A
TCCR1A |= (1<<WGM11); // one of the three bits required for mode 14 "Fast PWM" with TOP from ICR1 (other two are in TCCR1B)
//TCCR1B
TCCR1B = 0; // clear default
TCCR1B |= (1<<WGM13) | (1<<WGM12); // the other two of the three bits required for mode 14 "Fast PWM" with TOP from ICR1
TCCR1B |= (1<<CS12) | (1<<CS10); //gives 1024 prescaler
ICR1 = f2ICR1(fMin); // setting TOP to max freq value for start (~4 sec period)
OCR1A = 138; // fixed pulsewidth (inverted)
TIMSK1 = 0;
TIMSK1 |= (1<<OCIE1A) | (1<<TOIE1); //enable overflow and match interrupts
interrupts();
}
void loop()
{
// put your main code here, to run repeatedly:
Serial.println(NOvfCounts);
unsigned long tNow = millis();
//Serial.println(tNow);
double sinRef = sin(double(tNow)/1000*2*PI/10); //10 sec sin wave
//Serial.println(sinRef);
double fNow = q2freq(sinRef*(qFlowMax-qFlowMin)/2 + (qFlowMax+qFlowMin)/2); // scale to flow rate min/max and convert to frequency
//Serial.println(fNow);
x = f2ICR1(fNow);
//ICR1 = x;
Serial.print(NOvfCounts); Serial.write( '\t' ); Serial.println(x, HEX);
delay(100); // just to slow down Serial scroll rate...
}
ISR(TIMER1_OVF_vect)
{
//NOvfCounts++;
}
ISR(TIMER1_COMPA_vect)
{
ICR1 = x;
NOvfCounts++;
}
Instead of mode 14, You may want to consider using Fast PWM Mode 15 with OCR1A as TOP. OCR1B will be the output. The data sheet states
The procedure for updating ICR1 differs from updating OCR1A when used for defining the TOP value.
The ICR1 Register is not double buffered. This means that if ICR1 is changed to a low value when the
counter is running with none or a low prescaler value, there is a risk that the new ICR1 value written is
lower than the current value of TCNT1. As result, the counter will miss the compare match at the TOP
value. The counter will then have to count to the MAX value (0xFFFF) and wrap around starting at
0x0000 before the compare match can occur. The OCR1A Register however, is double buffered. This
feature allows the OCR1A I/O location to be written anytime. When the OCR1A I/O location is written the
value written will be put into the OCR1A Buffer Register. The OCR1A Compare Register will then be
updated with the value in the Buffer Register at the next timer clock cycle the TCNT1 matches TOP. The
update is done at the same timer clock cycle as the TCNT1 is cleared and the TOV1 Flag is set.
Using the ICR1 Register for defining TOP works well when using fixed TOP values. By using ICR1, the
OCR1A Register is free to be used for generating a PWM output on OC1A. However, if the base PWM
frequency is actively changed (by changing the TOP value), using the OCR1A as TOP is clearly a better
choice due to its double buffer feature.
This got it - thanks! I must admit I don't fully understand why it was going wrong (since I was delaying 100ms in the loop I would have expected any errors to only crop up as a 10hz blip and not an unchanging output for seconds) however I can see this is just a nicer way of doing it
Thanks but main reason for doing it this way was learning about the timers - I get that your suggestion would have been functionally fine, and simpler, though!
Yes good point this could have solved it too in this case. The advantage I suppose of mode 14 means I still preserve two different duty outputs at the variable frequency, which wasn't a requirement here but your suggestion hadn't even occurred to me so thanks for pointing it out