In normal computer use we usually think of peripherals as things like printers or USB drives.
In the realm of the microcontroller, this same word has a slightly different meaning. A “peripheral” is a small section of the silicon real estate, distinct from but alongside the CPU (the part of the microcontroller that actually processes machine instructions), dedicated to handling some specific task independent from the CPU.
Some of the peripherals of the Arduino’s AVR microcontroller include an analog-to-digital converter (used by the analogRead() function), a serial UART (communicates with a host computer, as when using the Serial library and when transferring code to the chip), SPI (Serial Peripheral Interface, sometimes used for SD card interfacing among other things) and I2C (another chip-to-chip communication method, supported by the Wire library).
Of interest to us at the moment are the Timer/Counter peripherals, which precisely measure time intervals and can also be used for pulse-width modulation (PWM, sometimes used for controlling LED brightness or for sound). PWM output from a timer/counter peripheral can be used to drive a servo without interrupting the CPU. NeoPixels can coexist! It’s not all roses though…there are some serious limitations…we’ll cover that later.
Heady Stuff
Controlling peripherals directly is very different from normal Arduino programming. For this reason, most are neatly wrapped up in libraries (or the core Arduino library itself, which handles the most oft-used functions like digitalWrite() or analogRead()).
Developing code at this level, one starts with the microcontroller datasheet…a massive document detailing every last bit and quantifiable attribute of the chip. Published by (and usually freely downloadable from) the chip manufacturers, these datasheets are unique to every specific chip and variant. For example:
- ATmega 328P Datasheet (Arduino Uno, Adafruit Pro Trinket, etc.). 34.3 MB.
- ATmega 32U4 Datasheet (Arduino Leonardo & Micro, Adafruit FLORA, etc.). 7.5 MB.
- ATmega 2560 Datasheet (Arduino Mega). 8.4 MB.
- ATtiny85 Datasheet (Adafruit Trinket & Gemma). 3.8 MB.
Yes, it really is 650 pages. Fortunately you don’t need to read it all. But fascinating to skim!
Peripheral control involves accessing the chip’s special-function registers, a few dozen memory addresses that can be read, written or modified like variables, but each one…often individual bits within each one…control aspects of specific peripherals.
Like variables, the special function registers are referred to by name…these have all been defined in a header file that’s automatically included in your sketch.
The ATmega328P in the Arduino Uno and Adafruit Pro Trinket has three timer/counter units (Timer/Counter0, 1 and 2 — each has its own section in the table of contents). Using the 16 MHz CPU clock as a time base, each can count off intervals anywhere between 1 and 256 ‘ticks’ — but Timer/Counter1 is of particular interest because it’s a 16-bit counter…it can count anywhere from 1 to 65,536 ticks, providing lots of extra resolution for this task. The “Register Desription” subsection of the datasheet details each of the special function registers pertaining to Timer/Counter1…the register names and also what each bit controls…
The page shown above describes the special function register named TCCR1A and the individual control bits within. All these control bits have assigned names too…each corresponds to a single bit number…0 to 7…and one must remember when writing C code to use either the _BV(bit) macro or (1 << bit) when identifying bits in the register; multiple bits can be added (+) or OR’d (|) together. Usually several registers need to be configured to get any useful work done.
Here’s a few lines from our servo library (which we’ll download on the next page) showing what this type of code looks like:
TCCR1A = _BV(WGM11); // Mode 14 (fast PWM) TCCR1B = _BV(WGM13) | _BV(WGM12) | _BV(CS11); // 1:8 prescale ICR1 = F_CPU / 8 / 50; // ~50 Hz (~20 ms)
Barely even looks like Arduino code, does it? If you’ve ever had a sketch make you go “buh?”, chances are it’s directly accessing special function registers.
You’ll need to read the relevant parts of the datasheet to fully understand what’s going on and why, but basically: the first two lines (setting the special function registers TCCR1A and TCCR1B) configure the the Timer/Counter1’s waveform generation mode (“fast PWM” in this case) and set a prescaler — sort of a throttle on the ‘tick’ time — advancing the counter every 8 CPU clock cycles instead of every cycle. The next line (ICR1) sets the timer counter’s upper limit (after which it restarts at zero) and thus the overall PWM period. The bit of math here factors in the CPU frequency in Hz (F_CPU), prescaler (1:8) and servo pulse frequency (50 Hz) to determine this number…on a 16 MHz Arduino, it would be 16,000,000 ÷ 8 ÷ 50 = 40,000 ticks per PWM cycle.
In other parts of the code are lines like these:
TCCR1A ^= _BV(COM1A1); OCR1A = pos;
The first line toggles (^ is the XOR operator in C) bit COM1A1 in special-function register TCCR1A. This either enables or disables PWM output on the OC1A pin (which is labeled elsewhere in the datasheet…on the Uno, that’s pin 9). The second line sets the output compare register for this same pin — its PWM duty cycle — to the value contained in variable “pos.”
Obtuse stuff, isn’t it? Take it in small steps. Remember, it’s all just about setting and clearing bits. Very, very, very specific bits. Don’t beat yourself up if something doesn’t work the first time, or second, or the 23rd…a few projects I’ve just had to shelve because I never could make heads or tails of it. AVR peripherals are among the hardest Arduino things to master, maybe second only to assembly language. This is why Arduino libraries exist, for the rest of us.
Peripherals are a huge subject, much more than be covered here (remember, it’s a 650 page datasheet), but I wanted to provide a high-level explanation of this very low-level concept.
The Good
The payback for all this hard work? In the case of this library, NeoPixels and servos playing nice together. In the broader sense, much more. Byte for byte, cycle for cycle, there simply is no better optimization strategy than exploiting the microcontroller’s built-in peripherals, period. Once configured and set on their way, zero code and zero instruction cycles are spent on the task. Other code then runs while the peripherals do their job; it’s an explicit form of multitasking.
There’s a lot more than just PWM and timers…some peripherals offer features not normally exposed by the core Arduino libraries, such as ADC free-run mode used in our voice changer project.
Performance isn’t everything. It oftens comes at a cost…in this case, flexibility. Consider:
- Peripherals and special-function registers are unique to every single make and model of microcontroller. To use these is to tie yourself to very specific hardware. Code that performs magic on an Arduino Uno will not work on an Arduino Due…it won’t even compile…they’re based on altogether different architectures. Our library works on the most common 8-bit AVR microcontrollers, but cutting-edge “Arduino compatibles” aren’t supported (though some of these have entirely different ways of resolving the NeoPixel timing issue).
- Peripherals are an extremely limited resource, much more so than even RAM or code space. There is exactly ONE 16-bit timer/counter on the Arduino Uno. This can easily lead to library conflicts…for example, the WaveHC library (which plays WAV files from an SD card) also relies on Timer/Counter1. It won’t get along with NeoPixels either.
- PWM output from the Timer/Counter units is limited to a very specific set of pins. On the Arduino Uno, you can control at most two servos this way, and they must be on pins 9 or 10. On the Leonardo and Micro, at most four servos on pins 5, 9, 10 or 11. There is no workaround for this that doesn’t involve interrupts…putting us back at square one.
- The Trinket and Gemma microcontrollers don’t even have a 16-bit timer. With only an 8-bit timer to work with, these are limited to about eight distinct servo positions; smooth continuous motion is not possible.
Early versions of the “official” Arduino Servo library worked exactly as we do here…using PWM output from Timer/Counter1. This was later switched to an interrupt-based technique, with the benefit of supporting many servos on any pins. There was no obvious drawback, NeoPixels weren’t even a thing until recently!
Page last edited March 08, 2024
Text editor powered by tinymce.