Using PWM to dirve Servos

1. Introduction

This article will describe how to drive two servos (e.g. for a pan-tilt unit) through a PWM signal on an ARM Conrtex-M3. The example firmware described here uses “libopencm3” as a basis, but could be ported easily to any other STM32 library.

2. Prerequisites

  • Toolchain for building binaries for the STM32 is in place
  • Toolchain for flashing binaries to the STM32 is in place (e.g. for the STM32-Discovery, or the IFLAT-32 with OpenOCD or USART)
  • Basic understanding of the STM32 (GPIOs)
  • libopencm3 compiled for your target board
  • Basic understanding of how to program the STM32 in C

3. Parts Needed

4. Drive a Servo

There are many good articles which describe how servos are driven (e.g. on Wikipedia, or at srs). Therefor I am not going into detail here, and will just sum up what we need later on.

Servos are driven by a PWM signal with a period of 20ms. By putting a high pulse at the beginning of this period, the position of the servo (normally between -90 and 90 deg.) could be set. The high pulse has a length of max. 2ms and min 1ms. Thus, the middle position of an servo is given through a pulse with a length of 1.5ms. To turn the serve to the max. left position a 1ms pulse is needed, and to drive it to the max. right position 2ms are given.

It has to be noted, that the numbers given above may vary slightly for different types of servos, you have to try what is adequate for your servo.

For the PWM signal we need to generate later, we keep in mind the following:

  • PWM period must be 20ms
  • Pulse with must be adjustable between 1 and 2 ms
Example PWM Signal to drive the servo

Example PWM signal to drive a servo

5. Generte a PWM

The STM32 comes with hardware support for generating PWMs. This is by far the most efficient and accurate way to generate a PWM. Generation of the PWM is done through one of the build in timers. Thus I will try to describe how a timer in PWM mode works basically, and how they must be configured for PWM.

6. PWM Timer

The STM32 has six timers in total. Timers 6 and 7 are mainly used for DAC, the timers 1 and 8 are advanced timers also providing hal-sensor and quadrature support. The timers 2, 3, 4, and 5 are general purpose timers. For PWM generation, the timers 2 to 5 are good enough.

Each of the timers 2 to 5 has four output channels. The output channels for timer 2 are mapped for example as follows:

PA0 TIM2_CH1
PA1 TIM2_CH2
PA2 TIM2_CH3
PA3 TIM2_CH4

Now how does a timer work in PWM mode (there are various other modes too)? Each timer is tight to a clock frequency. If you did not fiddle around with your clock setup, they are all supplied with 72MHz (this is also true for the timers 2 to 5 which are connected to APB1 because of the multiplier in the clock tree).

Clock frequency for the timers 2 to 5 is by default 72Mhz.

Each timer also has a counter register CNT. This register is increased by one with each clock tick it receives. So a timer at 72MHz gets 72 000 000 ticks per second, and thus the CNT register will be 72 000 000 after one second. One can lower the CNT update frequency by applying a prescale factor to the PSC register of the timer. E.g. by setting PSC to 2, the CNT register will updated at a frequency of 36MHz and so one.

Count frequency in CNT could be modified by a prescaling favtoc in the PSC register.

Next there is the auto-reload-register ARR. The value in the ARR register is compared to the CNT register. As soon as CNT has reached ARR, CNT is reset to zero and the period of the PWM is over.

The period for the PWM is defined by the value in the ARR register.

One thing to note about the previously said is, that all the the registers CNT, PSC and ARR apply to the whole timer and thus are used on all four output channels.

To state how log the duty cycle should be, there is the CCRx register for each output channel. Until CNT is below the value in CCRx, the channels output (e.g. PA0-PA3 for timer 2) is kept high. As soon as CNT has reached the value in CCRx, the output is driven low for the rest of the period. At the end of the period CNT is reset, and the next period is started (beginning with the duty cycle).

By varaing the value in the CCRx register, the duty cycle of the output channel is modified at runtime.

7. Calculating the Timer

As stated before, a period of 20ms and a duty cycle between 1ms and 2ms is needed.
First thing to do is to determine what 20ms is in Hz. This is calculated by the formula:

1000 ms / p1 ms = p2 Hz

For our 20ms period, it is:

1000 ms/ 20 ms = 50 Hz

To be able to adjust the duty cycle more fine grained we are going to use us instead of ms. So 20ms become 20000us, 1ms is 1000us and 2ms is then 2000ms.

First we prescale the 72Mhz frequency by 72:

72 000 000 Hz / 72 = 1 000 000 Hz (or 1 Mhz)

This increases out count register CNT by 1 000 000 each second. To calculate what is the correct value for the ARR register (which defines the period), we divide the result of the prescaled frequency by the intended frequency for our period:

1 000 000 Hz / 50 Hz = 20 000

Now every time the CNT register is increased by one, this is the equivalent to 1us. The duty cycle now could by set easily. E.g. for a 1.5ms duty cycle we set the CCRx register to 1500.

8. Configuring the Timer

This chapter will briefly describe how to setup one of the STM32 timers for PWM. It uses "libopencm32" as a basis. For PWM the following from "libopenstm32" has to be included:

  #include <libopencm3/stm32/f1/rcc.h>
  #include <libopencm3/stm32/f1/gpio.h>
  #include <libopencm3/stm32/timer.h>

The first (rcc.h) is needed for enabling the peripherals clock. The second one (gpio.h) to configure the output channels of the timer to be outputs, and the third one (timer.h) is needed to setup the timer itself.

First thing to do, is to setup the global timer settings (those which apply to the whole timer) like prescaling, period and mode. In the firmware example included with this article, this is done within the file "pwm.c" in the function "pwm_init_timer":

void pwm_init_timer(volatile uint32_t *reg, uint32_t en, uint32_t timer_peripheral, uint32_t prescaler, uint32_t period)
{
      rcc_peripheral_enable_clock(reg, en);

      timer_reset(timer_peripheral);

      timer_set_mode(timer_peripheral,
                     TIM_CR1_CKD_CK_INT,
                     TIM_CR1_CMS_EDGE,
                     TIM_CR1_DIR_UP);

       timer_set_prescaler(timer_peripheral, prescaler);
       timer_set_repetition_counter(timer_peripheral, 0);
       timer_enable_preload(timer_peripheral);
       timer_continuous_mode(timer_peripheral);
       timer_set_period(timer_peripheral, period);
}

I will go through this code step by step. We will assume that "pwm_init_timer" was called like so:

pwm_init_timer(&RCC_APB1ENR, RCC_APB1ENR_TIM2EN, TIM2, 72, 20000);

This then enables the clock for the timer peripheral first:

      rcc_peripheral_enable_clock(reg, en);

Reset the timer:

      timer_reset(timer_peripheral);

This sets the mode of the time. It writes to the timers configuration register to do no prescaling but use the internal clock, edge aligned, and and make the counter direction upward:

      timer_set_mode(timer_peripheral,
                     TIM_CR1_CKD_CK_INT,
                     TIM_CR1_CMS_EDGE,
                     TIM_CR1_DIR_UP);

Set the value for the prescale register PSC:

       timer_set_prescaler(timer_peripheral, prescaler);

Set repetition counter to 0 in register RCR to 0. This counter could be used to influence update event frequency. Only if set to 0 update events occur:

       timer_set_repetition_counter(timer_peripheral, 0);

Enable preload registers. According to the manual, preload has to be enabled for PWM to work:

       timer_enable_preload(timer_peripheral);

We like to have a continuous PWM signal, not only one pulse. Thus we enable continuous mode:

       timer_continuous_mode(timer_peripheral);

Set the timers period in the ARR register:

       timer_set_period(timer_peripheral, period);

Then the timers output channels have to be configured, as well as the GPIOs corresponding to the channels. In the firmware example included with this article, this is done within the file "pwm.c" in the function "pwm_init_output_channel":

void pwm_init_output_channel(uint32_t timer_peripheral, enum tim_oc_id oc_id,
     volatile uint32_t *gpio_reg, uint32_t gpio_en, uint32_t gpio_port, uint16_t gpio_pin)
{
       rcc_peripheral_enable_clock(gpio_reg, gpio_en);

       gpio_set_mode(gpio_port, GPIO_MODE_OUTPUT_50_MHZ,
                     GPIO_CNF_OUTPUT_ALTFN_PUSHPULL,
                     gpio_pin);

       timer_disable_oc_output(timer_peripheral, oc_id);
       timer_set_oc_mode(timer_peripheral, oc_id, TIM_OCM_PWM1);
       timer_set_oc_value(timer_peripheral, oc_id, 0);
       timer_enable_oc_output(timer_peripheral, oc_id);
}

We will assume that "pwm_init_output_channel" for OC1 was called like so:

pwm_init_output_channel(TIM2, TIM_OC2, &RCC_APB2ENR, RCC_APB2ENR_IOPAEN, GPIOA, GPIO_TIM2_CH2);

First thing done in the example above is enabling the peripherals clock for the GPIOs corresponding to the output channel:

      rcc_peripheral_enable_clock(gpio_reg, gpio_en);

Then we set the GPIOs mode to output:

       gpio_set_mode(gpio_port, GPIO_MODE_OUTPUT_50_MHZ,
                     GPIO_CNF_OUTPUT_ALTFN_PUSHPULL,
                     gpio_pin);

Then we disable the OCs output for the time we configure it:

       timer_disable_oc_output(timer_peripheral, oc_id);

Configure the mode fore the output. In our example we like to use edge aligned PWM (PWM1):

       timer_set_oc_mode(timer_peripheral, oc_id, TIM_OCM_PWM1);

Start with OC set to low by applying a duty cycle of zero:

       timer_set_oc_value(timer_peripheral, oc_id, 0);

At the point where we are done with the OCs configuration, we re enable it:

       timer_enable_oc_output(timer_peripheral, oc_id);

9. Use the PWM to Move the Servo

Not that the timer is configured, we can start it by enabling counting:

	timer_enable_counter(timer_peripheral);

To position one of the servos to its center position, we have to set a PWM of 1.5ms (or 1500us). This is done by setting the value to the OCs CCRx register with the function "timer_set_oc_value):

       timer_set_oc_value(TIM2, TIM_OC2, 1500);

To move the servo left/right:

       timer_set_oc_value(TIM2, TIM_OC2, 1000);
       timer_set_oc_value(TIM2, TIM_OC2, 2000);

In the example firmware, the initialization of the timers for the servo, and the positioning function are encapsulated within "servo.c".

10. Example Firmware

The example firmware I wrote and tested on an IFLAT-32 (which uses an STM32F103 MCU). But it should also run on the STM32-Discovery board (double check the linker script in "src/firmware.ld").  As you can see in the picture below and in the schematics, only two pins are needed to drive the servo (for my servos it is the orange one). Also you can see, that the servos use a separate power supply (connected to the red (+) and black (-) cables on my servos), and that the ground of the board has to be connected to the ground of the servos. The first servos signal goes to PA1 (TIM_OC2), the second goes to PA2 (TIM_OC3).

  • Clone example sources from github
  • Download the example firmware sources here.
  • Download the example firmware binaries here.
The Setup with two servos and the IFLAT-32

The Setup with two servos and the IFLAT-32

The schematic for the setup when using an IFLAT-32

The schematic for the setup when using the IFLAT-32

The schematic for the setup when using the STM32-Discovery

The schematic for the setup when using the STM32-Discovery

2 Comments to Using PWM to dirve Servos

  1. 7. Juli 2012 at 01:41 | Permalink

    Great example, thanks so much, saved me a lot of time.

    I had a bit of trouble, the timer would take a while to start, and figured that it was waiting for some event. Looks like I needed the following:

    timer_generate_event(TIM2, TIM_EGR_UG);

    As per the datasheet:

    “As the preload registers are transferred to the shadow registers only when an update event occurs, before starting the counter, you have to initialize all the registers by setting the UG bit in the TIMx_EGR register.”

    So I needed to have done:

    timer_generate_event(TIM2, TIM_EGR_UG);
    timer_enable_counter(TIM2);

    Thanks again!

Leave a Reply

You must be logged in to post a comment.