0

I need to get interrupts at one of three specific rates: 1920, 2000, and 2400 per sec, at a fairly high precision (~10 ppm). I've asked about this on Electronics but I think here might be more appropriate.

I can't use the standard 16MHz with internal timers, since 16MHz / 2400 (for example) gives 66666.666... counts.

I would use an external standard, but I don't seem to be able to find one that fits the need. Something small and precise at 48KHz / 96KHz etc would be fine.

Alternatively, I could swap the crystal / resonator for one running at 18.432MHz, which divides evenly by those freqs and shouldn't tax a 328p. Can I even do that on a Nano, or some clone / comparable, with the required 10ppm?

-- extra removed --

5
  • Sure you can swap the crystal. The maximum is 20MHz, so 18.whatever is well within the capabilities. You'll have to adjust the board settings to match of course (F_CPU).
    – Majenko
    Nov 11, 2021 at 11:32
  • Whoo, 10 ppm, quite a challenge! Did you consider the initial precision, the influence of temperature and voltage/current, drift, ageing? You might need something better than a simple crystal, for example an oven-controlled crystal oscillator. Nov 11, 2021 at 14:31
  • Oh, and of course the jitter due to different numbers of clocks for instructions, leading to different interrupt latency. Nov 11, 2021 at 14:34
  • @thebusybee - I know 10ppm is asking a lot. If I could find a reasonably priced TXCO at 48 or 96 KHz in a small package I'd do that for sure. As for jitter, a small amount is OK in exchange for longer-term (~30 msec) accuracy.
    – Jim Mack
    Nov 11, 2021 at 17:29
  • You could use a TXCO of higher frequency and divide it. -- All this seems to call for a more hardware based solution, perhaps a CPLD or FPGA. Why do you need such an accuracy? Nov 11, 2021 at 17:36

1 Answer 1

3

You could use an internal timer.

You are using interrupts, and you are probably aware that interrupt handling inevitably incurs some jitter. Thus, if you do not mind an extra single CPU cycle of jitter, you could reconfigure the timer on each interrupt in order to control the average interrupt period.

Here is how I would do it, with Timer 1 running in normal mode (CTC mode would require an extra operation):

// Interrupt period in units of 1/2^16 CPU cycles.
// For 2.4 kHz, that is 16000/2.4*65536 = 436906666.666...
uint32_t interrupt_period = 436906667;

ISR(TIMER1_COMPA_vect) {
    // Update the timer for the next interrupt.
    static uint32_t accumulator;
    accumulator += interrupt_period;
    OCR1A = accumulator >> 16;

    // The periodic task goes here...
}

void setup() {
    // Configure Timer 1.
    TCCR1A = 0;            // normal mode
    TCCR1B = _BV(CS10);    // clock @ F_CPU
    TIMSK1 = _BV(OCIE1A);  // enable COMPA interrupt
}

void loop(){}

Here, accumulator is a 32-bit extension of the timer's output compare register. The upper 16 bits are copied into the actual timer register, whereas the lower 16 bits ensure that the accumulated error never exceeds one cycle.

This way of updating the timer should only take about 2 µs per interrupt. It is a constant time operation, so it doesn't add extra jitter to your task, other than the single cycle of jitter induced by rounding to the next edge of the CPU clock.

There are a couple of caveats though:

  1. This can only work on an Arduino clocked off a crystal (e.g., a Micro). Those clocked off a ceramic resonator (Uno...) would be too unstable in frequency to meet your 10 ppm requirement.
  2. Even though a crystal is more than stable enough to meet your requirement, it is unlikely to be accurate enough. You will then need to calibrate it, e.g. by outputting a PWM signal (analogWrite()) into a frequency meter. Then adjust the variable interrupt_period according to the measured clock frequency.

Edit: I added the timer configuration to the code, and here comes an explanation of how this code works.

Think of the timer as a digital alarm clock. It measures time in units of CPU cycles instead of hours and minutes. It rolls over every 216 cycles instead of every 1440 minutes. But it is otherwise a regular alarm clock. It has two alarms called “COMPA” and “COMPB”, but you only need one. In order to perform a periodic task, you turn the alarm on and, every time it rings, you advance the alarm time by the required period. That is, you set:

new alarm time = previous alarm time + task period

Note that the addition has to be computed modulo the roll-over period of your clock : 1440 minutes for a regular alarm clock, 216 cycles for the timer. Thankfully, the arithmetics on unsigned numbers do this automatically.

Now, lets say the task period is 43 minutes and 35 seconds, but the alarm can only be set with a resolution of one minute. What you can do is compute the “ideal” alarm time on paper (this is the accumulator variable), and truncate it to the resolution of the clock when setting the alarm. If you start at 00:00:00, you will compute the next ideal time to be 00:43:35 and set the alarm to 00:43. When it rings, you add the period to the previously computed time, you get 01:27:10, and you set the alarm to 01:27. Etc. Here is the list of the alarm times as computed and as actually set on the clock:

computed  set
───────────────
00:00:00  00:00
00:43:35  00:43
01:27:10  01:27
02:10:45  02:10
...

Note that, even though the limited resolution of the alarm setting creates some jitter, the average task period is still exactly 43 minutes and 35 seconds.

The code I wrote above does exactly the same. The “ideal” alarm time is computed as a fractional number of CPU cycles, written down in binary fixed point, with 16-bits on each side of the radix point. In hex, it is 1A0A.AAAB CPU cycles. The computed alarm times are multiples of this period, whereas the times actually written to OCR1A are the same truncated to an integer:

computed   set
───────────────
1A0A.AAAB  1A0A
3415.5556  3415
4E20.0001  4E20
682A.AAAC  682A
...
3
  • Thanks much for this, but, sorry, I don't get how it works. I gather there's some initial setting up of the counter that you don't show? It seems that we're outputting wildly variable values to OCR1A: 1A0A, 3415, 4E20, 682A etc. What's going on?
    – Jim Mack
    Nov 12, 2021 at 19:03
  • @JimMack: See expanded answer. Nov 12, 2021 at 21:03
  • Very nice. I knew that somehow I should implement Bresenham's algorithm to average away the error, but I couldn't see a way to do it. I'll ask another question based on this, to try to solve another issue.
    – Jim Mack
    Nov 12, 2021 at 21:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.