Skip to content
Eren SOYLU

Your PWM driver is lying to you

Why a PCA9685 running "50 Hz" is really running 52, and what that does to a servo.

· Eren SOYLU

I spent an evening convinced I had a bug in my servo easing code. The motion was smooth on the bench and jittery on the rig, and the difference tracked with nothing I could name. The code was fine. The clock was not.

The internal oscillator is nominal, not accurate

The PCA9685 datasheet gives its internal oscillator as 25 MHz. That number is a nominal figure with a tolerance, and individual parts land a few percent either side of it. The prescaler you write is computed against that assumed 25 MHz:

prescale = round(25_000_000 / (4096 * target_hz)) - 1

If the real oscillator runs at 26 MHz, every pulse you emit is about 4% shorter than you asked for. Ask for a 1500 µs neutral pulse and the servo sees roughly 1440 µs — which it reads as "rotate a bit". Across a chain of servos with slightly different parts, they disagree with each other too.

Measure the part in front of you

Put a scope or a logic analyser on one channel, ask for a known frequency, and measure what actually comes out. Then solve for the oscillator:

# Ask for 50 Hz, measure 52.1 Hz on the pin.
measured_hz = 52.1
requested_hz = 50.0
actual_osc = 25_000_000 * (measured_hz / requested_hz)  # ≈ 26.05 MHz

Store actual_osc per board and use it in the prescaler calculation instead of the constant. That is the whole fix:

def set_frequency(self, target_hz):
    prescale = round(self.osc_hz / (4096 * target_hz)) - 1
    self._write(MODE1, SLEEP)
    self._write(PRESCALE, max(3, min(255, prescale)))
    self._write(MODE1, RESTART)

Leave the knob in

The instinct is to hide the calibration behind a constant once you have measured one board. Don't. The next board will be different, and a osc_hz field in your config costs nothing:

[driver.pca9685]
address = 0x40
osc_hz = 26_050_000  # measured 2026-08-12, board rev C

A note with the date and the board revision turns a magic number into a measurement. Six months later, that comment is the difference between trusting the value and re-deriving it from scratch.

The general lesson is older than this chip: anything derived from a nominal oscillator, a nominal resistor, or a nominal supply voltage is an estimate. If your control loop cares, measure it once and write it down.