Here in Germany, the mains alternating current is supposed to run at exactly 50 Hz, and it wobbles around that value all the time depending on how much power is being generated versus consumed. The Bundesnetzagentur (Germany's grid regulator) uses that wobble as its own feedback signal: if consumption suddenly outpaces generation, the frequency dips below 50 Hz, and if generation runs ahead, it creeps above. I wanted to see how precisely I could track that wobble myself with nothing more than an Arduino, so I built a small mains-frequency counter.
Turning a Frequency Into Something Countable
An Arduino has no direct way to read out "50 Hz" as a number; all it can do is notice when a digital pin changes state and count. The trick is to convert the measurement into two things a microcontroller is genuinely good at: counting edges and counting ticks of a fast, known clock.
Say we count exactly \(m\) mains cycles and, over that same stretch of time, count \(n\) cycles of a reference clock running at a known frequency \(f_\text{ref}\). Both counts describe the same elapsed time \(T\), so
\[T = \frac{m}{f} = \frac{n}{f_\text{ref}}\]
where \(f\) is the mains frequency we are after. Solving for \(f\) gives
\[f = f_\text{ref}\cdot\frac{m}{n}.\]
For our setup, \(f_\text{ref}=32768\text{ Hz}\), the standard "watch crystal" frequency also used as the tick source inside a real-time clock (RTC) chip, so
\[f = \frac{32768\, m}{n}\text{ Hz}.\]
Choosing a Reference Clock
The precision of this whole scheme depends entirely on how trustworthy \(f_\text{ref}\) is, since any drift in the reference clock leaks directly into the computed frequency. The Arduino's own on-board oscillator is not a good candidate here: an ATmega328P is normally driven by a ceramic resonator specified only to about \(\pm 0.5\%\), which alone would already allow a \(\pm 0.25\text{ Hz}\) error on a 50 Hz signal, far too coarse for spotting the kind of small grid fluctuations we're interested in, and it drifts noticeably with temperature on top of that.
A far better and still cheap reference is a DS3231 real-time clock module. Its whole purpose is to keep accurate time, so its 32.768 kHz crystal is temperature-compensated (a TCXO) and specified at typically around \(\pm 2\text{ ppm}\), roughly two hundred times tighter than the ceramic resonator, and it conveniently exposes that same 32.768 kHz clock on a pin (or, depending on the module, a 1 Hz square wave derived from it), which is exactly what we need to count against as \(n\).
Hardware Parts
- Arduino UNO
- VOH1016AB optocoupler
- DS3231 RTC
- \(120\text{ k}\Omega\) resistor (2 Watt)
- \(10\text{ k}\Omega\) resistor
- 1N4007 diode
The VOH1016AB optocoupler was chosen specifically because it has a Schmitt trigger built into its output stage. Arduinos already have Schmitt triggers on their own digital inputs, which is normally enough to clean up a slow optocoupler's output and would let you get away with something cheaper like a PC815, but once you move to a faster microcontroller such as an ESP32 you have to compensate for the missing input hysteresis yourself, which the VOH1016AB avoids entirely by doing it on the sensor side.
The emitter side of the VOH1016AB is tied to GND, and its collector delivers the resulting digital signal to interrupt pin 2, which is held high through a 10 kΩ pull-up resistor whenever the optocoupler isn't conducting.
On the mains side, the 1N4007 diode chops off the negative half of the AC cycle, since the VOH1016AB's maximum reverse voltage is only 6 V, and a 2 W, \(120\text{ k}\Omega\) resistor limits the current flowing through the optocoupler's input LED. With a 220 V AC source, the peak forward current (ignoring the small diode forward-voltage drop) works out to
\[\frac{220\text{V}\cdot\sqrt{2}}{120\text{ k}\Omega} = 2.59\text{ mA},\]
and, since the diode only lets the positive half-wave through, the RMS current over a full cycle is half that peak value again:
\[\frac{2.59\text{ mA}}{2} = 1.3\text{ mA}.\]
This circuit is connected directly to mains voltage. Working with wall-outlet electricity is genuinely dangerous and can be lethal if done incorrectly — build this only if you know what you are doing, and at your own risk.
How Precise Is This, Actually?
Counting edges is exact, but there's still one unavoidable source of error: the mains signal and the 32.768 kHz reference clock run completely independently of each other, so the moment we start and stop counting \(n\) can land anywhere within one reference-clock tick. That is a classic \(\pm 1\) gating error, the same kind of one-count uncertainty that shows up in any digital frequency counter.
Differentiating \(f=f_\text{ref}\,m/n\) with respect to \(n\) tells us how strongly a one-tick error propagates into the computed frequency:
\[\frac{\partial f}{\partial n} = -\frac{f_\text{ref}\cdot m}{n^2} = -\frac{f^2}{f_\text{ref}\cdot m},\]
where the second form comes from substituting \(n=f_\text{ref}\,m/f\) back in. So for a small counting error \(\Delta n\), the resulting absolute error in the measured frequency is approximately
\[|\Delta f| \approx \frac{f^2}{f_\text{ref}\cdot m}\,|\Delta n|.\]
Plugging in \(f=50\text{ Hz}\), \(f_\text{ref}=32768\text{ Hz}\), our chosen \(m=50\) mains cycles, and the worst-case single-tick error \(\Delta n=1\) gives
\[|\Delta f| \approx \frac{50^2}{32768\cdot 50}\text{ Hz} = \frac{50}{32768}\text{ Hz} \approx 1.53\text{ mHz}.\]
Compare that against the DS3231's own \(\pm 2\text{ ppm}\) crystal tolerance, which contributes an absolute error of only \(2\times 10^{-6}\cdot 50\text{ Hz} = 10^{-4}\text{ mHz}\) — more than four orders of magnitude smaller. In other words, the counting resolution, not the reference clock's own accuracy, is what actually limits this setup, which also tells us exactly how to improve it: counting more mains cycles per measurement (increasing \(m\)) or using a faster reference clock both shrink \(|\Delta f|\) directly, at the cost of a slower update rate.
Source Code
volatile uint32_t cnt50Hz = 0;
volatile uint32_t cnt32kHz = 0;
uint32_t cnt = 0;
void counterFunc1() {
cnt50Hz++;
if (cnt50Hz == 50) {
cnt = cnt32kHz;
cnt32kHz = 0;
cnt50Hz = 0;
}
}
void counterFunc2() {
cnt32kHz++;
}
void setup(void) {
Serial.begin(9600);
attachInterrupt(0, counterFunc1, FALLING);
attachInterrupt(1, counterFunc2, FALLING);
}
void loop() {
delay(500);
Serial.println(32768. * 50. / cnt, 4);
} The first interrupt routine counts mains half-cycles coming in through the optocoupler and, every \(m=50\) of them, latches the current 32.768 kHz tick count into cnt and resets both counters for the next window. The second interrupt routine simply tallies reference-clock ticks in the background. loop() then only has to plug the latest latched count into \(f=32768\cdot 50/n\) and print it — all the actual timing precision comes from the two interrupt service routines running independently of whatever loop() happens to be doing.