In the previous post I described a PID controller tested with randomly generated data in a controlled range. The simulation phase is now complete and the results look promising.
Time to move to real hardware: connecting the algorithm to an actual pH probe.
critical requirement: galvanic isolation
Due to very low signal levels and extremely high input impedance of typical pH electrodes, full galvanic isolation between the probe circuitry and the rest of the system is mandatory. Any ground loop or leakage current destroys measurement accuracy.
(How did I learn this the hard way? Adding a second probe without isolation resulted in spectacular instability, as iones were transferring charges across the probes)
I chose a classic approach: voltage-to-frequency converter (V/F) driving an optocoupler. The output is a pulse train where frequency is proportional to the measured pH value.
pulse measurement approach
To achieve good resolution I wanted to measure pulse period/duration as precisely as possible. Ideally this would be done using hardware timers with input capture functionality, triggered directly by signal edges (interrupt-driven period measurement), preferably with DMA support for minimal CPU load.
hardware interrupt vs software polling
On most microcontrollers a hardware interrupt allows the peripheral to directly notify the CPU on an event (e.g. GPIO edge), capturing timing information with very low and deterministic latency.Raspberry Pi 4 lacks a dedicated per-pin interrupt controller for GPIO. All GPIO events are funneled through one or two shared IRQ lines. The kernel must then dispatch the event to user-space, introducing variable latency (typically 10–150 µs depending on system load).
The practical options are therefore software polling – either busy-wait loops or libraries like pigpio (which uses DMA-based sampling for much better timing resolution than standard polling).
The key question: at what frequency does the measurement start to degrade significantly?
real-world performance test
I characterized pigpio’s frequency measurement example on a Raspberry Pi 4 Model B.
Test setup:
- Signal source: precise DDS generator with TTL output
- Conditioning: Schmitt-trigger inverter + voltage divider to 3.3 V
- Measurement: pigpio running in hardware-timed sampling mode
Collected >5,000 pairs of (set frequency vs measured frequency).
Raspberry Pi hardware limits curve for correct frequency reading
Results summary:
- Up to ~200 kHz → excellent agreement, typical error <1%
- At 300 kHz → significant dropouts, error reaching ~13% (≈40,000 missed cycles in longer runs)
- Pattern: first ~20–30 samples usually accurate; later readings show increasing deviation due to Linux scheduler preemption and context switching
Fortunately my target V/F converter operates around 20 kHz – well within the reliable range. With moderate system load, pigpio should provide sufficient stability for closed-loop pH regulation.
trade-offs and alternatives
For significantly higher precision I would consider:
- hardware interrupt controller
- dedicated input capture peripherals on MCU
- DMA + hardware timestamping
- programmable logic on FPGA or hybrid SoC (Zynq)
For this project the Raspberry Pi + pigpio combination offers the best balance of development speed, ecosystem maturity and cost.
aftermath
Testing boundary conditions before committing to a design path can reveal critical limitations early and save the time. How many times have projects failed silently - and at what cost - because assumptions were not validated from the very beginning?
Project work begins with understanding the physics of the process, its real boundaries, and a mathematical check of whether the chosen approach has any realistic chance of working at all.
Blind prototyping without validating key assumptions often leads to painful (and expensive) surprises down the line. I always try to characterize critical interfaces / possible pitfalls before committing a prototype assembly.
What is your approach – do you measure and calculate boundaries first, or assemble the prototype quickly and debug as you go?