Servo Pulse Width Modulation PWM and Timer Resolution for 1ms-2ms and 600us-2300us

I moved an app from an ESP8266 to a Raspberry Pi Pico processor and none of my servos worked correctly. The servos moved but not the right amount.  Then I google-found someone that had given a completely different set of microsecond ranges.  It turns out they fixed the problem but did not understand why they had the problem. 

The following type of explanation is not correct

The setServoCycle(position) function can be used to set the position of the servo by passing the position parameter from 1000 to 9000. The values for duty_u16 are in microseconds instead of degrees. The servo values 1000-9000 represent 0-180 degrees

Servo control is based on Pulse Width Modulation where the width of a pulse tells the servo its position.  The control pulse is between 1-2 msec in a 50hz cycle.  That 50hz translates into a 20msec period.  So basically the control pulse ranges from 5%-10% of the period. 

That translates to 1000usec - 2000usec.  Servos are fickle beasts and you can get a little more motion range by stretching the range  600usec - 2300usec. This is why all the articles say things like "you can set the value from 600-2300".  They really mean you can vary pulse width as specified in Microseconds.

Timer Basics

PWM is driven by the IoT device's hardware timer.  Timers are programmed with the
  • Full Frequency / Period, in our case 50hz/20msec.  Timers are counters and we program them to periodically roll over and restart.  In our case, they roll over every 20msec.  
  • The portion of the period that is on. This is the length of the pulse inside the full period.  We tell the timer how many tics long this pulse is.
The counter rate is adjusted to count from max to at a rate that matches our period.  The Pulse Width is a subset of the full length.  So the Pulse Width is a portion of a timer's range.  

Ex: I have a 100-step timer that rolls over every 20msec.  
  1. The timer rollover rate is 50hz  
  2. The internal clock rate is 50*100 or 5000 tics/second.  
  3. A 10% pulse would span 1/10 of the total counter 100 tic size or 10 timer tics. 
  4. A 5% pulse would span 1/20 of the total 100 counter tics in the period or 5 timer tics.


Reference Code

def setServoAngle(servo, angle,
pulse_min_usec = 600, pulse_max_usec=2400, timer_bits=16):
    timer_resolution=2**timer_bits - 1
    angle = max(min(angle, 180),0)
    pulse_target = (angle * pulse_range_usec // 180) + pulse_min_usec
    tics = pulse_target * timer_resolution // 20000
    print(pulse_min_usec, pulse_max_usec, timer_bits, timer_resolution,
pulse_range_usec, angle, pulse_target, tics)


The number of tics required for a specific PWM pulse width is dependent on: the pulse width in seconds, the number of bits and the derived number of steps in the timer, and the cycle frequency that is always 50 hertz for servos.
  • duty-tics = (pulse width in seconds) * (scale: number of timer steps) * (frequency in Hz)
  • duty-tics = (pulse width in seconds) * (scale: number of steps) / (Period in seconds)
Notes: 50 Hz = 20ms period. Servo pulses of  1ms to 2ms are up about 5-10% of the 20ms period.

Millisecond Math

Multiply top and bottom by 1000 to convert the formula to msec. 
  • duty-tics = (pulse width in seconds * 1000) * scale * freq) / 1,000

Change the units to milliseconds taking into account multiplication or division by 1,000

  1. duty-tics = ((pulse width in ms) * scale * 50)/1000
  2. duty-tics = (pulse width in ms) * scale / 20

Microsecond Math

Most of the APIs are presented as Microseconds.  Updating the millisecond formula from aboe
  • duty-tics = (pulse width in s seconds * 1000) * scale * freq) / 1,000
Multiply the top and bottom by 1,000,000 to convert the formula to microseconds (usec).
  • duty-tics = (pulse width in seconds* 1,000,000) * scale * freq) / 1,000,000

We change the unit to microseconds, Microsecond, because most of the APIs accept microseconds and not seconds in order to stay with integer math.

  • duty-tics = ((pulse width in usec) * scale * 50) / 1,000,000
  • duty-tics = (pulse width in usec) * scale / 20,000

Taking it to the Timer (house)

The Servo library needs to know the size of the timer in order to program the timer with the duty tick count for any given pulse width. I have two devices that I run a program that has different size timers.  This means any Servo class to be able to take into account the different rollover sizes for the period and pulse widths.

DeviceTimer SizeRollover Timer Tics
ESP826610      1024
ESP328, 10, 12, 15256, 1024, 4096, 32K
Avtel Arduino 8256

10-Bit Timer Tic Examples

Some machines have 10-bit timers or 1024 steps. We can calculate the number of timer tics required for pulses of these widths

  •     duty-tics = 1ms * 1024 * 50 / 1000 ==> 51
  •     duty-tics = 2ms * 1024 * 50 / 1000 ==> 102
Microsecond calculates give the exact same results as expected.

  • duty-tics = 1000us * 1024 / 20000 =  51
  • duty-tics = 2000us * 1024 / 20000 = 102

16-Bit Timer Tick Examples

Some machines have 16-bit timers or cascaded 8-bit timers resulting in 65535 or 65205 (255*255) steps.  We calculate the number of timer tics required for a couple pulse widths.

  • duty-tics = 1000us * 65535 // 20000 = 3276
  • duty-tics = 2000us * 65535 // 20000 = 6553

Real world Servo pulse lengths

Actual Servo behavior and pulse range vary. People tend to use an expanded range of Servos.  Most libraries use a range of 600usec -2300usec


Revision History

Created 2023 01

Updated 2023 01


Popular posts from this blog

Understanding your WSL2 RAM and swap - Changing the default 50%-25%

Installing the RNDIS driver on Windows 11 to use USB Raspberry Pi as network attached

DNS for Azure Point to Site (P2S) VPN - getting the internal IPs