Recently, I have been developing a PID controller class for my Qt projects. During implementation, I encountered a very interesting problem from the field of signal theory that I believe is worth sharing.

A classic PID controller works in fixed time steps dt. At each step it compares the current process value with the setpoint, considers the previous value and computes the output. And right here the first serious problem shows up - the inertia inherent in real-world measurement system.

input Value in Practice

Consider a typical measurement circuit designed to convert a raw sensor signal into a usable process value for the PID algorithm:

measuring circuit with non-inverting op-amp

a typical, non-inverting op-amp input circuit

Here the op-amp is configured as a non-inverting amplifier with gain:

$$ A = 1 + \frac{R_{134}}{R_{133}} $$

Suppose we use this circuit to measure pH. The typical characteristic of a pH electrode looks like this:

ph probe characteristic

ph probe characteristic

A change of 1 pH unit corresponds to ~57 mV at 25°C - it’s a very small signal. Additionally, a pH probe has extremely high output impedance (in the GΩ range). It is essentially a very thin glass tube with two reference electrodes, extremely sensitive to electromagnetic interference, electrostatic charges, and even movement of ions in the measured liquid.

The most common hardware solution is to place a capacitor C_IN at the input to filter out noise (in effect performing analog integration of the input voltage over time).

the fundamental design dilemma

pros cons
larger C_IN smoother, more stable process value significantly slower response to real changes in pH
smaller C_IN fat response to sudden changes very noisy, scattered readings

Moreover - we are amplifying a tiny signal (~dozens of mV), so we inevitably amplify noise as well. The faster the op-amp (higher slew rate), the more aggressively it will try to follow every noise spike, which makes the problem even worse.

the software way - collect samples and process them intelligently

In modern systems, the ADC usually samples much faster than the PID loop time dt.
Example: PID cycle = 1 second → ADC can easily give us 20, 50, 100 or even more samples per cycle.

Instead of relying on an analog capacitor introducing significant additional inertia - why not collect all samples digitally within one dt period and then decide what to do with them? After all, the capacitor is just implementing mathematical integration using physics.

Possible approaches:

  • Arithmetic mean (moving average, simplest, good for Gaussian noise)
  • Median filter excellent against impulse/spike noise (common in industrial environments)
  • Digital low-pass filters (FIR or IIR):
  • FIR low-pass → predictable phase, but needs more CPU
  • IIR low-pass → very efficient, but watch out for phase distortion and stability

In my implementation haven’t yet decided which filter to choose, but I went with a dynamic buffer in a Qt slot that collects samples:

void regulator::nextSample (SAMPLE* sample)
{
    if (this->state==STATE_IDLE) { return; }
/* you don't want race conditions between the filter code so use a semaphore. Unless you hook up the filtering routine once all samples are collected */
    this->sample_sem->acquire(1);   
    this->sample[this->sample_ctr].sample=sample->sample;
    this->sample_ctr++;
    // If you want to go ultra-fast: realloc in a loop is not ideal for performance.
    this->sample=(SAMPLE*)realloc(this->sample,(this->sample_ctr+1)*sizeof(SAMPLE));
    if (!this->sample)
    {
        QString msg; msg.append(this->name); msg.append(" error relocating sample memory for sample no.:");
        msg.append(QString::number(this->sample_ctr));
       this->log(msg); exit(1);
    }
    this->sample_sem->release(1);
    //here you could hook up your filter processing:
    //this->filter();
    //and remember to free the buffer memory once completed!
}

That’s the final effect that class can produce:

the final effect

PID in Qt: up & running

After dt elapses, the buffer is processed (filtered), the resulting value is used by the PID algorithm, and the buffer is cleared for the next cycle.

aftermath

Today we end up with a bright, filtered sky. Did I describe something you already use in your projects, or is this new to you? I’d love to hear your thoughts, experiences or even better solutions!