This year for the holidays I created a little dynamic ornament with five channels of independent LED light. The prototype, which had to be finished in a matter of days, used an Ardweeny board from Solarbotics. The Ardweeny is based on the ATmega328, a great chip with more than five hardware channels for PWM (pulse-width modulation). While I love the ATmega chips, they get a bit pricey at $5 apiece just to drive some LEDs.
I was shopping on Digi-Key for alternatives and decided to try a couple of ATtiny13 microcontrollers. The ATtiny13 is a small, low-power AVR with the standard 8-bit core size, and conveniently comes with a 9.6-MHz internal RC oscillator. Because the ATtiny has an internal oscillator, I knew I could keep my hardware very minimal and thus very small, a requirement for the small paper ornament I was hoping to build.
The problem is that the ATtiny13 has only one PWM output. If I used only its hardware functionality, I would get one channel of control, where I needed five. To solution is to implement a simple software PWM. In this post I describe my process of getting software PWM (and embedded C programming in general) working on the ATtiny13.
The first step with programming any part is reading the datasheet [link]. The Attiny13 comes with 1KB of internal flash memory for programs. When I ordered it this sounded plenty large for my purposes, but I forgot to account for the heavy Arduino libraries I’m used to using, whose needed functionality I would have to implement myself in the code. 1KB turns out to feel like a tiny amount of space.
1KB is too small to hold the floating-point math libraries of AVR-LibC, the C library I use for AVR programs. It’s also too small to hold the math.h library, which includes the trigonometry functions. In my ‘328-based prototype I used a sin() function to calculate the brightness levels of the light channels, to give a soothing light pulsation. That won’t be an option on the tiny13.
I had a lot of trouble getting started with C on the ATtiny. It’s not too complicated once you get going, though.
Software PWM uses an inbuilt functionality of the microcontroller called a timer. The timer counts off clock cycles, and when it reaches a certain point, in this case its maximum value of 255 (the max value of a single unsigned byte), the timer triggers an interrupt.
The interrupt is important precisely because it interrupts the running code. Specifically, it tells the microcontroller to jump to a predefined location in program memory, execute the instructions there, and then return to the main routine. For software-based PWM, the timer interrupt is perfect. It frees up the main program logic by periodically and consistently executing the same function, called an Interrupt Service Routine.
So the desired program flow looks like this:
The main function in my case was supposed to set the output level of each of five LEDs such that one LED would “lead” the others in a gentle up-down pulsation. The ISR would be responsible for dealing with the actual on-off settings of the LEDs.
For my project I chose a PWM resolution of one byte, or eight bits. A byte allows for 256 possible output levels, plenty more than the eye can see in brightness gradation. A resolution of 256 means that there are 256 “time slots” wherein an LED can turn on or off.
For example, if the brightness of the LED is supposed to be 255, it is on from time slot “0” all the way to time slot “255”—in other words, all the time. If the brightness is 0, the LED is off for every cycle. If the brightness is 100, the LED is on for the first hundred cycles, then off for the remaining 155.
The resulting output is not smooth, but a series of full-on and full-off positions that, to the relatively slow human eye, average together to some intermediate brightness. So, now we know roughly what the ISR should do:
- Read the desired brightness level of an LED, in the form of a global variable.
- Read the current progress through the 256 cycles
Compare the brightness level to the current progress
If the progress is lower than the desired on-time, turn the LED on.
- If the progress is past the desired on-time, turn the LED off.4. Increment the progress by one. AVR-LibC says that the ISR for the timer interrupt on the tiny13 has the signature
The TIM0_OVF_vect parameter indicates that this ISR (potentially one of several) is specifically for interrupts triggered by a TIMER0 overflow.
Given the pin number of each LED channel, and the brightness level desired, the ISR body looks like:
ch1, ch2, ..., ch5 are the single-byte output levels of each channel, and
CH1_PIN, CH2_PIN, ..., CH5_PIN are the pin numbers for each channel.
The ISR is actually very simple:
ISRcounter holds the current count of ISR runs (0 to 255). For each channel, if
the ISRcounter is less than the brightness level desired for that channel, write
the LED HIGH (
PORTB |= (1 << pinNumber)). Once the counter exceeds the desired
brightness level, keep writing the LED LOW (
PORTB &= ~(1 << pinNumber)). So,
in effect, the brightness level byte is simply the ON-time of the LED, out of
The rest of the code can now fall into place:
Like all AVR-LibC programs, the main() function is divided into a setup phase, which runs once at the time of power-up, and a loop phase, which runs after setup as long as power is supplied. The setup function here sets the five LED channels to a starting brightness with an interval determined by the macro TIME_OFFSET, so-named because it simulates the amount of time by which each LED leads the one “behind” it in the circular brightness pattern.
Then the loop logic uses a one-bit “direction” flag to decide whether to increment or decrement the brightness of each channel, and finally a comparison checks whether the direction needs to reverse (in case the LED is at minimum or maximum brightness). The loop delays by time DELAY_MS, in milliseconds, and starts again.
That’s it! Beyond setting up the timer details with the lines
TCCR0B |= (1 << CS00); // disable timer prescale (=clock rate)
as given in the ATtiny13 datasheet, the main() function and the ISR together handle everything the program has to do to make pretty blinky lights.
There is one more step, actually. By default, the ATtiny13 ships with a 1/8th clock prescaler enabled, a fuse called “CKDIV8” or “clock divide 8.” To make the timer count quickly enough for the PWM to be invisible to the human eye, we need to disable this clock divisor. To do so, we must run the command
"avrdude -c usbtiny -p t13 -U lfuse:w:0x7A:m"
instructing avrdude to write 0x7A to the low fuse. This is the same as the default low fuse value in every way but the CKDIV8 bit, which has been disabled.
The complete code:
Feel free to use the code any way you want. Have fun!