| It seems like a lot of people these days Arduino for a lot of microcontroller tasks, using the magic Arduino libraries to abstract away the underlying architecture. This time and energy saving platform is a beautiful thing, however, people shouldn't be afraid of "pulling back the curtain" on Arduino and, moreover, they should know roughly what's going on. This post covers the voodoo behind the Servo library and how 6 simple registers control flexible hardware PWM. |
The Timer 1 module on the atmega328 is perhaps the most capable timer of the three, being 16-bit and supporting two PWM channels which run at the same frequency, but with different pulse widths. The module has 5 different modes of operation, but we'll be using the "Fast PWM Mode" (found on Section 15.9.3 of the datasheet). In this mode, the 16-bit TCNT register increments once every p clock cycles, where p is the value of the prescaler.
When the TCNT register reaches the value in ICR1, the Timer 1 interrupt is fired (which we ignore), and TCNT is reset to zero. In this way, ICR1 determines the period of the PWM signal and hence the refresh rate. Now, to determine the pulse width, OCR1A (controlling PORTB1) and OCR1B (controlling PORTB2) are set to a value between 0 and ICR1, where 0 would cause the pin to be always off and ICR1 would cause it to be always on. If set to ICR1/2, the corresponding pin will be on the same amount it is off, or a 50% duty cycle. The figure below shows the counting timer in red and the user-configurable threshold values:

In this example, we are aiming for a standard servo configuration of a 50Hz update rate (period = 20ms). The varying pulse width should be able to go from 1ms to 2ms, however, the architecture dictates that the width can go the full range of 0 to 20ms.
The first thing we want to do is configure the module to turn on and use the fast PWM mode. We also want to tell the module to take control of PORTB1/PORTB2 (by default they are normal IO Pins). This information is set in TCCR1A and partly in TCCR1B.
TCCR1A register (section 15.11.1 in datasheet)
TCCR1A = _BV(COM1A1) | _BV(COM1B1) | _BV(WGM11); // clear on compare, fast PWM, TOP=ICR1 (WGM13/WGM12 in TCCR1B)
Our period, 20ms, is equivalent to (20ms) * (16MHz), or 320,000 clock instructions. This is obviously over the 16-bit limit imposed by the timer which has a maximum of 65,535. To solve this, we can use the module's prescaler, which divides the incoming clock speed by a set factor. The datasheet offers prescaling factors of 256 or 1024 which would make the resulting ICR1 value less than 65,535. For this example, we'll use 1024, though 256 actually offers more precision. The prescaler is set in TCCR1B.
TCCR1B register (section 15.11.2 in datasheet)
TCCR1B = _BV(WGM12) | _BV(WGM13) | _BV(CS10) | _BV(CS12); // prescaler 1024
The actual value for ICR1 should now be 20ms * 16MHz / 1024, or 312.5.
ICR1 register (section 15.11.7 in datasheet)
ICR1 = 312;
Finally, the pins should be set as output pins so the output buffers actually electrically create signals on the pins.
DDRB register
DDRB |= _BV(1) | _BV(2);
Thats it for the setup. All there is to do is set OCR1A and OCR1B to however long the pulse should be on. For servos, 1ms (full reverse) is 16, 1.5ms (centered) is 23, and 2ms (full forward) is 31. Below is the code I use in my projects:
#include <inttypes.h> #include <avr/io.h> #include <avr/pgmspace.h> #include <avr/eeprom.h> #include <avr/interrupt.h> #include <stdio.h> #include <util/delay.h> // Sets up Timer 1 for fast-PWM operation // servos are enabled on B1 and B2 // Period is given in increments of 64us up to 65535 // suggest value of 312 for 20ms period void start_servos(const int period_64us) { //initialize TMR1 (PWM) TCCR1A = _BV(COM1A1) | _BV(COM1B1) | _BV(WGM11); // clear on compare, fast PWM, TOP=ICR1 (WGM13/WGM12 in TCCR1B) TCCR1B = _BV(WGM12) | _BV(WGM13) | _BV(CS10) | _BV(CS12); // prescaler 1024 ICR1 = period_64us; OCR1A = -1;//off OCR1B = -1;//off DDRB |= _BV(1) | _BV(2); // output on B1 and B2 } // sets "high" time of B1 for pwm*64us void inline set_servo1(int pwm) { OCR1A = pwm; } // sets "high" time of B2 for pwm*64us void inline set_servo2(int pwm) { OCR1B = pwm; }