Software PWM on the ATtiny13

11 January 2012

About a 4-minute read

A first go at the Stardweeny using an ATtiny13

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.

Implementing Software PWM

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:

main(){
	loop infinitely, changing the output levels
	accordingly.
}

ISR(){
	run now and then, twiddling the hardware
	outputs to correspond with the output levels
	set in main
}

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:

  1. Read the desired brightness level of an LED, in the form of a global variable.

  2. Read the current progress through the 256 cycles

  3. Compare the brightness level to the current progress

  4. If the progress is lower than the desired on-time, turn the LED on.

  5. 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

ISR(TIM0_OVF_vect){}

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:

ISR(TIM0_OVF_vect)
{
	if(ISRcounter < ch1) PORTB |= (1 << CH1_PIN); //Write the ch1 pin high
	else PORTB &= ~(1 << CH1_PIN); //Write the ch1 pin low

	if(ISRcounter < ch2) PORTB |= (1 << CH2_PIN);
	else PORTB &= ~(1 << CH2_PIN);

	if(ISRcounter < ch3) PORTB |= (1 << CH3_PIN);
	else PORTB &= ~(1 << CH3_PIN);

	if(ISRcounter < ch4) PORTB |= (1 << CH4_PIN);
	else PORTB &= ~(1 << CH4_PIN);

	if(ISRcounter < ch5) PORTB |= (1 << CH5_PIN);
	else PORTB &= ~(1 << CH5_PIN);

	ISRcounter++;
}

where 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 255.

The rest of the code can now fall into place:

#define F_CPU 9.6E6L /* CPU Freq. Must come before delay.h include. 9.6MHz */

#include  /* For data types */
#include  /* Register and port definitions */
#include  /* Busy-wait delay functions */
#include  /* Exposes timers, counters and ISR functions */

#define CH1_PIN PB0 /* Bind output channels to specific PortB pins. This depends on schematic. */
#define CH2_PIN PB1
#define CH3_PIN PB2
#define CH4_PIN PB3
#define CH5_PIN PB4

#define TIME_OFFSET 50
#define DELAY_MS 3

uint8_t ch1, ch2, ch3, ch4, ch5; /* The one-byte PWM level of each channel */
uint8_t directions = 0xFF;
/*
This one-byte flag holds five channel direction bit flags
0x [nothing] [nothing] [nothing] [ch5] [ch4] [ch3] [ch2] [ch1]
*/

volatile uint8_t ISRcounter = 0; /* Count the number of times the ISR has run */
uint8_t timeCount;

int main(void);

int main(void)
{
	/* Setup */
	ch1 = 0;
	ch2 = TIME_OFFSET;
	ch3 = 2 * TIME_OFFSET;
	ch4 = 3 * TIME_OFFSET;
	ch5 = 4 * TIME_OFFSET;

	DDRB = 0xFF; //Every PORTB pin is output
	PORTB = 0x00; //Start with every pin low

	TCCR0B |= (1 << CS00); // disable timer prescale (=clock rate)
	TIMSK0 |= (1 << TOIE0); // enable timer overflow interrupt specifically 	sei(); // enable interrupts in general 	/* Loop */ 	while(1){ 		if(directions & 1) ch1++; 		else ch1--; 		if(directions & 0B00000010) ch2++; 		else ch2--; 		if(directions & 0B00000100) ch3++; 		else ch3--; 		if(directions & 0B00001000) ch4++; 		else ch4--; 		if(directions & 0B00010000) ch5++; 		else ch5--; 		if(ch1 > 254) directions &= ~0B00000001;
		else if(ch1 < 1) directions |= 0B00000001; 		if(ch2 > 254) directions &= ~0B00000010;
		else if(ch2 < 1) directions |= 0B00000010; 		if(ch3 > 254) directions &= ~0B00000100;
		else if(ch3 < 1) directions |= 0B00000100; 		if(ch4 > 254) directions &= ~0B00001000;
		else if(ch4 < 1) directions |= 0B00001000; 		if(ch5 > 254) directions &= ~0B00010000;
		else if(ch5 < 1) directions |= 0B00010000;

		_delay_ms(DELAY_MS);
	}
	return 0;
}

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)
	TIMSK0 |= (1 << TOIE0); // enable timer overflow interrupt specifically

	sei(); // enable interrupts in general

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:

/*
* Stardweeny Source Code
* Author: Travis Geis
* Version: 1
* Date: January 2012
* URL: http://zenlogic.org/
*
* file: main.c
*
* The Stardweeny project runs five channels of software PWM to
* control the five individual LEDs in the points of a paper star.
*
* Inspired by and dedicated to Linda Geis.
*
* For this code to work correctly, lfuse = 0x7A. (Disabling CKDIV8)
* Run the command "avrdude -c usbtiny -p t13 -U lfuse:w:0x7A:m"
*/

#define F_CPU 9.6E6L /* CPU Freq. Must come before delay.h include. 9.6MHz */

#include  /* For data types */
#include  /* Register and port definitions */
#include  /* Busy-wait delay functions */
#include  /* Exposes timers, counters and ISR functions */

#define CH1_PIN PB0 /* Bind output channels to specific PortB pins. This depends on schematic. */
#define CH2_PIN PB1
#define CH3_PIN PB2
#define CH4_PIN PB3
#define CH5_PIN PB4

#define TIME_OFFSET 50
#define DELAY_MS 3

uint8_t ch1, ch2, ch3, ch4, ch5; /* The one-byte PWM level of each channel */
uint8_t directions = 0xFF;
/*
This one-byte flag holds five channel direction bit flags
0x [nothing] [nothing] [nothing] [ch5] [ch4] [ch3] [ch2] [ch1]
*/

volatile uint8_t ISRcounter = 0; /* Count the number of times the ISR has run */
uint8_t timeCount;

int main(void);

/*
* int main(void):
*
* The main function runs automatically when the AVR powers up.
* It never returns, and so dispatches all other actions for the
* microcontroller.
*
* The goal is to make a sinusoidal light intensity with time.
*
*/

int main(void)
{
	/* Setup */
	ch1 = 0;
	ch2 = TIME_OFFSET;
	ch3 = 2 * TIME_OFFSET;
	ch4 = 3 * TIME_OFFSET;
	ch5 = 4 * TIME_OFFSET;

	DDRB = 0xFF; //Every PORTB pin is output
	PORTB = 0x00; //Start with every pin low

	TCCR0B |= (1 << CS00); // disable timer prescale (=clock rate)
	TIMSK0 |= (1 << TOIE0); // enable timer overflow interrupt specifically 	 	sei(); // enable interrupts in general 	 	/* Loop */ 	while(1){ 		if(directions & 1) ch1++; 		else ch1--; 		if(directions & 0B00000010) ch2++; 		else ch2--; 		if(directions & 0B00000100) ch3++; 		else ch3--; 		if(directions & 0B00001000) ch4++; 		else ch4--; 		if(directions & 0B00010000) ch5++; 		else ch5--; 		if(ch1 > 254) directions &= ~0B00000001;
		else if(ch1 < 1) directions |= 0B00000001; 		if(ch2 > 254) directions &= ~0B00000010;
		else if(ch2 < 1) directions |= 0B00000010; 		if(ch3 > 254) directions &= ~0B00000100;
		else if(ch3 < 1) directions |= 0B00000100; 		if(ch4 > 254) directions &= ~0B00001000;
		else if(ch4 < 1) directions |= 0B00001000; 		if(ch5 > 254) directions &= ~0B00010000;
		else if(ch5 < 1) directions |= 0B00010000;

		_delay_ms(DELAY_MS);
	}
	return 0;
}
/*
* The ISR is responsible for toggling the states of the five output channels
* based on the current global variables for the desired brightness levels.
* Because the brightnesses (ch1 ... ch5) are single-byte values, the range
* of values is 0 to 255\. Thus the ISR needs to restart its counting cycle every
* 256th time it is called.
*
* To avoid undesired wiggle on the PWM, the ISR updates evey channel's pin
* every time it runs. It should take the same number of cycles every time.
* The duty period of the PWM is the first section. Thus the LED goes ON then OFF.
*/
ISR(TIM0_OVF_vect)
{
	if(ISRcounter < ch1) PORTB |= (1 << CH1_PIN); //Write the ch1 pin high
	else PORTB &= ~(1 << CH1_PIN); //Write the ch1 pin low

	if(ISRcounter < ch2) PORTB |= (1 << CH2_PIN);
	else PORTB &= ~(1 << CH2_PIN);

	if(ISRcounter < ch3) PORTB |= (1 << CH3_PIN);
	else PORTB &= ~(1 << CH3_PIN);

	if(ISRcounter < ch4) PORTB |= (1 << CH4_PIN);
	else PORTB &= ~(1 << CH4_PIN);

	if(ISRcounter < ch5) PORTB |= (1 << CH5_PIN);
	else PORTB &= ~(1 << CH5_PIN);

	ISRcounter++;
}

Feel free to use the code any way you want. Have fun!

Comments