In a past life, we needed to get a high accuracy repeating signal over an hour period. In our application, we needed to generate a pulse between a specified value of 90 to 130 Hz with high accuracy, which is in the same ballpark as what you are attempting.
We ended up using a microcontroller board with a TCXO 32.768kHz watch crystal, something like this one. It ended up being better than trying to use more expensive TCVCXO crystals. The reason was that the accuracy of the crystals at high precision is not only dependent on the voltage and temperature, but also dependent on the capacitance of the lines between the microcontroller and the crystal, which can vary with each microcontroller and the way it is soldered to the board.
Accuracy is also dependent on the supply voltage. We had the best success with 470 uF electrolytic capacitors on each VCC pin of the microcontroller to GND in parallel with a 4.7µF tantalum to smooth out the voltage spikes as much as possible.
Trying to get 100% repeatability with each board was an impossible nightmare as each crystal is slightly different. So we took the fully contructed microcontroller board and programmed it with a program to calibrate the crystal against an NTP time source on a PC. The calibration values were then stored on the microcontroller flash/eeprom (later and earlier storage mechanisms), along with the temperature profile for that crystal batch.
Even though these were temperature compensated devices, they were still not accurate enough for our application. These values were painstakingly derived from hours of work with an oven to develop a temperature compensation profile. Fortunately, it turned out the temperature profile was almost universally accurate for all of the individual crystals in a batch, so we didn’t need to do this with each board, but rather with a board from each batch of crystals we used. We did not stick to the ppm paradigm in the datasheets and our calibration values were calculated on the number of ticks (positive or negative) per 512 seconds exactly, the reason for which will become clear below.
The next headache was getting an exact frequency emitted. For our 32.768 kHz crystals, we ended up interrupting on each timer tick by making the clock pin act as an external interrupt pin as well as a 16-bit timer source pin. We were running the microcontroller at 8MHz, and it wasn’t doing anything else, so executing an ISR 32k per second wasn’t an issue as we had 244 CPU cycles per interrupt to play with and could do most of our processing in the main loop and not in the ISR itself anyway.
We set up a combined 24-bit register that incremented each tick and triggered adjustments to the timer counter register when those registers matched our calculated adjustments we needed to make. A full 24-bit count was exactly 512 seconds, which represented nearly 16.8 million ticks of the oscillator. Our RAM was constrained, so we ended up taking the most significant byte (MSB) of the 24-bit register and looking up a tick adjustment from a 256-byte signed character RAM array. The lookup was only triggered if the MSB value changed to avoid duplications. The timer counter was increased or decreased by the signed byte value in the array.
We only wanted a pulse output, so we set an ISR on timer match and set a pin high. The tick ISR set the pin low, so the pin stayed high for 30.5µs.
If we needed to add compensations into the array, we spread them over the full array to minimise big compensatory corrections. So, for a -4 tick adjustment, 1 was subtracted from the values in the array at locations 0, 64, 128 and 192. For a +6 tick adjustment, we added 1 into locations 0,42,85,128 and 170 and 213. We also checked that the timer counter would not go beyond its set top value or underflow due to an adjustment. If it would be problematic, the adjustment was saved and attempted again the next tick until it was successful.
An array lookup also triggered an ADC reading of the temperature too (every 2 seconds). If a temperature compensation was required, the old temperature profile was subtracted from the array and the new compensation was added into the array by spreading the adjustment(s) over the full array (as above).
I won’t bore you with the tortuous way we calculated compensation adjustments to the timer match values as you are in the fortunate situation of having pre-selected specific timings, something we did not. Here are the tables for the values you will need for 24, 25 and 30 fps:
cumulative ticks for 24fps |
mod |
timer match |
mod carry |
timer top |
1365.333 |
1365.333 |
1365 |
0 |
|
2730.667 |
2730.667 |
2731 |
0 |
|
4096 |
4096 |
4096 |
0 |
|
5461.333 |
5461.333 |
5461 |
0 |
|
6826.667 |
6826.667 |
6827 |
0 |
|
8192 |
8192 |
8192 |
0 |
|
9557.333 |
9557.333 |
9557 |
0 |
|
10922.67 |
10922.67 |
10923 |
0 |
|
12288 |
12288 |
12288 |
0 |
|
13653.33 |
13653.33 |
13653 |
0 |
|
15018.67 |
15018.67 |
15019 |
0 |
|
16384 |
16384 |
16384 |
0 |
|
17749.33 |
17749.33 |
17749 |
0 |
|
19114.67 |
19114.67 |
19115 |
0 |
|
20480 |
20480 |
20480 |
0 |
|
21845.33 |
21845.33 |
21845 |
0 |
|
23210.67 |
23210.67 |
23211 |
0 |
|
24576 |
24576 |
24576 |
0 |
|
25941.33 |
25941.33 |
25941 |
0 |
|
27306.67 |
27306.67 |
27307 |
0 |
|
28672 |
28672 |
28672 |
0 |
|
30037.33 |
30037.33 |
30037 |
0 |
|
31402.67 |
31402.67 |
31403 |
0 |
|
32768 |
32768 |
32768 |
0 |
|
34133.33 |
34133.33 |
34133 |
0 |
|
35498.67 |
35498.67 |
35499 |
0 |
|
36864 |
36864 |
36864 |
0 |
|
38229.33 |
38229.33 |
38229 |
0 |
|
39594.67 |
39594.67 |
39595 |
0 |
|
40960 |
40960 |
40960 |
0 |
|
42325.33 |
42325.33 |
42325 |
0 |
|
43690.67 |
43690.67 |
43691 |
0 |
|
45056 |
45056 |
45056 |
0 |
|
46421.33 |
46421.33 |
46421 |
0 |
|
47786.67 |
47786.67 |
47787 |
0 |
|
49152 |
49152 |
49152 |
0 |
|
50517.33 |
50517.33 |
50517 |
0 |
|
51882.67 |
51882.67 |
51883 |
0 |
|
53248 |
53248 |
53248 |
0 |
|
54613.33 |
54613.33 |
54613 |
0 |
|
55978.67 |
55978.67 |
55979 |
0 |
|
57344 |
57344 |
57344 |
0 |
|
58709.33 |
58709.33 |
58709 |
0 |
|
60074.67 |
60074.67 |
60075 |
0 |
|
61440 |
61440 |
61440 |
0 |
|
62805.33 |
62805.33 |
62805 |
0 |
|
64170.67 |
64170.67 |
64171 |
0 |
|
65536 |
0 |
0 |
0 |
|
cumulative ticks for 25fps |
mod |
timer match |
mod carry |
timer top |
1310.72 |
1310.72 |
1311 |
0 |
|
2676.053 |
2676.053 |
2676 |
0 |
|
4041.387 |
4041.387 |
4041 |
0 |
|
5406.72 |
5406.72 |
5407 |
0 |
|
6772.053 |
6772.053 |
6772 |
0 |
|
8137.387 |
8137.387 |
8137 |
0 |
|
9502.72 |
9502.72 |
9503 |
0 |
|
10868.05 |
10868.05 |
10868 |
0 |
|
12233.39 |
12233.39 |
12233 |
0 |
|
13598.72 |
13598.72 |
13599 |
0 |
|
14964.05 |
14964.05 |
14964 |
0 |
|
16329.39 |
16329.39 |
16329 |
0 |
|
17694.72 |
17694.72 |
17695 |
0 |
|
19060.05 |
19060.05 |
19060 |
0 |
|
20425.39 |
20425.39 |
20425 |
0 |
|
21790.72 |
21790.72 |
21791 |
0 |
|
23156.05 |
23156.05 |
23156 |
0 |
|
24521.39 |
24521.39 |
24521 |
0 |
|
25886.72 |
25886.72 |
25887 |
0 |
|
27252.05 |
27252.05 |
27252 |
0 |
|
28617.39 |
28617.39 |
28617 |
0 |
|
29982.72 |
29982.72 |
29983 |
0 |
|
31348.05 |
31348.05 |
31348 |
0 |
|
32713.39 |
32713.39 |
32713 |
0 |
|
34078.72 |
34078.72 |
34079 |
0 |
|
35444.05 |
35444.05 |
35444 |
0 |
|
36809.39 |
36809.39 |
36809 |
0 |
|
38174.72 |
38174.72 |
38175 |
0 |
|
39540.05 |
39540.05 |
39540 |
0 |
|
40905.39 |
40905.39 |
40905 |
0 |
|
42270.72 |
42270.72 |
42271 |
0 |
|
43636.05 |
43636.05 |
43636 |
0 |
|
45001.39 |
45001.39 |
45001 |
0 |
|
46366.72 |
46366.72 |
46367 |
0 |
|
47732.05 |
47732.05 |
47732 |
0 |
|
49097.39 |
49097.39 |
49097 |
0 |
|
50462.72 |
50462.72 |
50463 |
0 |
|
51828.05 |
51828.05 |
51828 |
0 |
|
53193.39 |
53193.39 |
53193 |
0 |
|
54558.72 |
54558.72 |
54559 |
0 |
|
55924.05 |
55924.05 |
55924 |
0 |
|
57289.39 |
57289.39 |
57289 |
0 |
|
58654.72 |
58654.72 |
58655 |
0 |
|
60020.05 |
60020.05 |
60020 |
0 |
|
61385.39 |
61385.39 |
61385 |
0 |
|
62750.72 |
62750.72 |
62751 |
0 |
|
64116.05 |
64116.05 |
64116 |
0 |
|
65481.39 |
65481.39 |
65481 |
0 |
65481 |
66846.72 |
1311.107 |
1311 |
0.386667 |
|
68212.05 |
2676.44 |
2676 |
0.386667 |
|
69577.39 |
4041.773 |
4042 |
0.386667 |
|
70942.72 |
5407.107 |
5407 |
0.386667 |
|
72308.05 |
6772.44 |
6772 |
0.386667 |
|
73673.39 |
8137.773 |
8138 |
0.386667 |
|
75038.72 |
9503.107 |
9503 |
0.386667 |
|
76404.05 |
10868.44 |
10868 |
0.386667 |
|
77769.39 |
12233.77 |
12234 |
0.386667 |
|
79134.72 |
13599.11 |
13599 |
0.386667 |
|
80500.05 |
14964.44 |
14964 |
0.386667 |
|
81865.39 |
16329.77 |
16330 |
0.386667 |
|
83230.72 |
17695.11 |
17695 |
0.386667 |
|
84596.05 |
19060.44 |
19060 |
0.386667 |
|
85961.39 |
20425.77 |
20426 |
0.386667 |
|
87326.72 |
21791.11 |
21791 |
0.386667 |
|
88692.05 |
23156.44 |
23156 |
0.386667 |
|
90057.39 |
24521.77 |
24522 |
0.386667 |
|
91422.72 |
25887.11 |
25887 |
0.386667 |
|
92788.05 |
27252.44 |
27252 |
0.386667 |
|
94153.39 |
28617.77 |
28618 |
0.386667 |
|
95518.72 |
29983.11 |
29983 |
0.386667 |
|
96884.05 |
31348.44 |
31348 |
0.386667 |
|
98249.39 |
32713.77 |
32714 |
0.386667 |
|
99614.72 |
34079.11 |
34079 |
0.386667 |
|
100980.1 |
35444.44 |
35444 |
0.386667 |
|
102345.4 |
36809.77 |
36810 |
0.386667 |
|
103710.7 |
38175.11 |
38175 |
0.386667 |
|
105076.1 |
39540.44 |
39540 |
0.386667 |
|
106441.4 |
40905.77 |
40906 |
0.386667 |
|
107806.7 |
42271.11 |
42271 |
0.386667 |
|
109172.1 |
43636.44 |
43636 |
0.386667 |
|
110537.4 |
45001.77 |
45002 |
0.386667 |
|
111902.7 |
46367.11 |
46367 |
0.386667 |
|
113268.1 |
47732.44 |
47732 |
0.386667 |
|
114633.4 |
49097.77 |
49098 |
0.386667 |
|
115998.7 |
50463.11 |
50463 |
0.386667 |
|
117364.1 |
51828.44 |
51828 |
0.386667 |
|
118729.4 |
53193.77 |
53194 |
0.386667 |
|
120094.7 |
54559.11 |
54559 |
0.386667 |
|
121460.1 |
55924.44 |
55924 |
0.386667 |
|
122825.4 |
57289.77 |
57290 |
0.386667 |
|
124190.7 |
58655.11 |
58655 |
0.386667 |
|
125556.1 |
60020.44 |
60020 |
0.386667 |
|
126921.4 |
61385.77 |
61386 |
0.386667 |
|
128286.7 |
62751.11 |
62751 |
0.386667 |
|
129652.1 |
64116.44 |
64116 |
0.386667 |
|
131017.4 |
65481.77 |
65482 |
0.386667 |
65482 |
cumulative ticks for 30fps |
mod |
timer match |
mod carry |
timer top |
1092.267 |
1092.267 |
1092 |
0 |
|
2457.6 |
2457.6 |
2458 |
0 |
|
3822.933 |
3822.933 |
3823 |
0 |
|
5188.267 |
5188.267 |
5188 |
0 |
|
6553.6 |
6553.6 |
6554 |
0 |
|
7918.933 |
7918.933 |
7919 |
0 |
|
9284.267 |
9284.267 |
9284 |
0 |
|
10649.6 |
10649.6 |
10650 |
0 |
|
12014.93 |
12014.93 |
12015 |
0 |
|
13380.27 |
13380.27 |
13380 |
0 |
|
14745.6 |
14745.6 |
14746 |
0 |
|
16110.93 |
16110.93 |
16111 |
0 |
|
17476.27 |
17476.27 |
17476 |
0 |
|
18841.6 |
18841.6 |
18842 |
0 |
|
20206.93 |
20206.93 |
20207 |
0 |
|
21572.27 |
21572.27 |
21572 |
0 |
|
22937.6 |
22937.6 |
22938 |
0 |
|
24302.93 |
24302.93 |
24303 |
0 |
|
25668.27 |
25668.27 |
25668 |
0 |
|
27033.6 |
27033.6 |
27034 |
0 |
|
28398.93 |
28398.93 |
28399 |
0 |
|
29764.27 |
29764.27 |
29764 |
0 |
|
31129.6 |
31129.6 |
31130 |
0 |
|
32494.93 |
32494.93 |
32495 |
0 |
|
33860.27 |
33860.27 |
33860 |
0 |
|
35225.6 |
35225.6 |
35226 |
0 |
|
36590.93 |
36590.93 |
36591 |
0 |
|
37956.27 |
37956.27 |
37956 |
0 |
|
39321.6 |
39321.6 |
39322 |
0 |
|
40686.93 |
40686.93 |
40687 |
0 |
|
42052.27 |
42052.27 |
42052 |
0 |
|
43417.6 |
43417.6 |
43418 |
0 |
|
44782.93 |
44782.93 |
44783 |
0 |
|
46148.27 |
46148.27 |
46148 |
0 |
|
47513.6 |
47513.6 |
47514 |
0 |
|
48878.93 |
48878.93 |
48879 |
0 |
|
50244.27 |
50244.27 |
50244 |
0 |
|
51609.6 |
51609.6 |
51610 |
0 |
|
52974.93 |
52974.93 |
52975 |
0 |
|
54340.27 |
54340.27 |
54340 |
0 |
|
55705.6 |
55705.6 |
55706 |
0 |
|
57070.93 |
57070.93 |
57071 |
0 |
|
58436.27 |
58436.27 |
58436 |
0 |
|
59801.6 |
59801.6 |
59802 |
0 |
|
61166.93 |
61166.93 |
61167 |
0 |
|
62532.27 |
62532.27 |
62532 |
0 |
|
63897.6 |
63897.6 |
63898 |
0 |
|
65262.93 |
65262.93 |
65263 |
0 |
65263 |
Then we need to add in an adjustment for rounding errors. 32768 divides by 24 exactly, so that’s not a problem. For 25 and 30, there will be 0.04033 missing and 0.03347 ticks excess per second, respectively. To compensate for this, we need to spread 20 ticks and -17 ticks per 512 seconds into the tick adjustment array. So for 25 fps, we add 1 to each value at indexes: 12, 24, 36, 48, 60, 72, 84, 96, 108, 120, 132, 144, 156, 168, 180, 192, 204, 216, 228, 240. For the 30 fps, we subtract 1 from each value at indexes: 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240, 255. This will leave 0.497 and -0.138 ticks unadjusted per each 512 seconds.
In theory, this gives an error rate of +0.002 and -0.00089 frames per hour, respectively. These are ideal results and, depending on your device, your mileage may vary! We ultimately achieved results between 30 and 150 RTC ticks between each of our devices for the frequency we were aiming to achieve over an hour window, so it wasn’t as good as ideal, but it was sufficiently close to get away with.
Unfortunately, I don’t have the actual code we used and I’ve recreated the algorithm from memory. Good luck!