Arduino millis() interrupt frequency measurement blues

A cautionary tale for those trying to measure frequency using Arduino

Rig - oscillator and CD4060 divider going into D2 on the Arduino

For the paramagnetism project, I need to measure the frequency of the oscillator accurately. I did that with a 16F628 PIC last time, but I am after an easier life, and the obvious choice here is Arduino. I get serial output, a way to use I2C OLED displays rather than bit-banging a Hitachi character display 1980’s style, and all that extra wiring, what’s not to like?

The lack of consistency, that’s what. I count the number of 16MHz Arduino UNO R3 clock cycles that have elapsed with every rising edge on D2 in an interrupt service routine – the code is at the end of the post. Once I have seen 16 rising edges pass, I copy that value over, set TCNT1 to zero and start over. The first result will be garbage, after that it should be OK. Every half a second I print a copy of the last result out. This measures the period, in 1/16MHz segments. I collect 2300 results, of which the first 10 are like so

freq,cnt
4793.92, 53398
4794.01, 53399
4795.35, 53400
4794.37, 53396
4794.01, 53400
4794.01, 53400
4794.01, 53400
4794.01, 53400
4793.92, 53401
4793.83, 53402

and you can see right from the get-go that there is trouble in Paradise. These are two crystal oscillators. One is the 16MHz in the Arduino board, the other is a SEI 2.4576MHz oscillator block, which I divided by 512 using a CD4060 to get 4.8kHz

SEI oscillator block

Okay, so the oscillator1 is 24 years old. Just to eliminate this going bad I ran a scope on the 1/512 output, in persistence mode

x divisions 50us

It was OK, even on many more cycles in persistence mode.

Say the crystals are good for 50ppm, which is a bit rubbish really. And the conspire to drift opposite directions, I’m still expecting an error of 1 in ten thousand, son on a count of roughly 50k I will take an error of +/- 5 or 6

A histogram of the resulting counts using R is rubbish – 100 off to the low side and about 70 on the high side.

Histogram of counts. Yuck

I discover Arduino uses interrupts to maintain the millis() function – not, apparently the micros(). I consider shutting that down, but it appears this is unwise, and it appears I would lose some of the other handy functions.

No interrupts:
no serial send receive
no I2C-communication,
no SPI-communication
no timing

Using nointerrupts(); will shoot myself in the foot as I want to use my interrupts. Since millis() runs every millisecond and my cycle time is 208us I am likely to take a hit every fifth time, and I average over 16 cycles for what I thought was greater resolution which ensures I take a hit. Enabling Serial probably means I set off some other ISR. I looked for a definitive guide to the Uno interrupts but all I got were many tutorials about how to use interrupts. I get the feeling I’m chasing down the wrong track here. Perplexity suggested I read the ATMega 328P datasheet 2which is all very well and I discover I have three timers, and more sleuthing suggests that millis() indirectly comes off the overflow of Timer0. So I added a line just after

 void setup() { 

to mask that overflow

  TIMSK0=0x00; // can this. THIS WILL KILL MILLIS

and took out delay(500); because that depends on millis, so the board just madly throws out measurements. I repeat the R plot histogram

the spread in the value of count with millis() disabled

It’s better/less bad than the first histogram, confirming the hypothesis that the millis(); interrupt is screwing up the result. I have disabled the timer0 overflow (TOIE0 bit in TIMSK0). The bad news is there is still something running infrequently that is giving me a big error. At a guess that’s Serial Receive, but I am going to quit while I am ahead and still have an unbricked board. After all the whole point of using Arduino is for the display libraries and if I am going to lose them, well, why keep a dog if you have to bark yourself?

Back to the 16f628A PIC – it’s primitive, but it will do my bidding and only that

Unlike the Arduino3 the 16F628 PIC isn’t clever enough to try and do more than one thing at a time, so I will go back to that and use the same sort of technique, hopefully for a more repeatable result. It’s a little bit closer to the bare metal. I am getting too soft to code in MPASM any more, I will try in in MPLABX C 😉

Arduino code

#include <Arduino.h>
// pin used
#define pulse_ip 2
// how many cycles to count
#define maxcyles 16
#define ard_clockf 16000000
volatile unsigned int isr_cntr; // number of cycles, vol because changed in ISR
double freq,period;
volatile unsigned long TCNT1_copy; // copy of TCNT!, volatile becaus changed in ISR


// put function declarations here:
void pinChange ();

void setup() {
  // put your setup code here, to run once:
  pinMode(pulse_ip,INPUT);
  Serial.begin(9600);
  Serial.println("Booted");
  TCCR1A=0;
  TCCR1B=0x01; // counter 1 running PSC 1
  attachInterrupt (digitalPinToInterrupt (2), pinChange, RISING);  // attach interrupt handler for D2
}

void loop() {
  double result=double(maxcyles*ard_clockf)/TCNT1_copy;
  Serial.print(result);
  Serial.print(", ");
  Serial.println(TCNT1_copy );
  delay(500);
}

// Interrupt Service Routine (ISR)
void pinChange ()
{
  isr_cntr++;
  if (isr_cntr >= maxcyles) {
    TCNT1_copy=TCNT1;
    TCNT1=0;
    isr_cntr=0;
  }

}  // end of pinChange

It is, of course, possible I have made a stupid mistake in the code 😉

  1. If somebody is looking for the pinout of a SEI TTL crystal oscillator then it’s the same as most of these. Imagine it were a 14 pin DIL, then pin 1=NC (sometimes an enable but not here), pin 7 in GND which is connected to the case, 8 is O/P and pin 14 is VCC, 5V in this case ↩︎
  2. Just like people on Stackexchange would, along with Use the Source, Luke. Microchip move their datasheets about every so often so the link may go to a 404 ↩︎
  3. If you programmed the ATMega328P directly without the Arduino bootloader then you would have full control over a more capable device than a 16F628A and could make it run only one interrupt at at time. I don’t know how to program that at the bare metal, but I do for the PICs. I ought to learn how to do that on the Atmel chips one day ↩︎

Leave a Reply

Your email address will not be published. Required fields are marked *