This 10th article in the series of “Do It Yourself: Electronics”, demonstrates a digital to analog conversion (DAC) approximation using pulse width modulation (PWM) of a micro-controller.
Once the fundamental setup for programming the micro-controller ATmega16 was up, the club members were on a spree to try out all kind of micro-controller programming based electronics experiments. If you have not yet got the setup, check out the previous article.
“With Diwali approaching and LED blinking just done, can we do LED dimming using a micro-controller”, queried Rohit.
“Should be possible. Basically we need to have a varying (analog) voltage between 0V & 5V, instead of just 0V & 5V as in the blinking of LED”, replied Pugs.
“But a micro-controller is all about digital electronics, right? How do we get analog voltage output?”
“We may not get perfect analog voltages but we definitely can do approximations, which would be practically good enough for cases like LED dimming.”
“What kind of approximations & how?”
“ATmega16 have pulse width modulation (PWM) support. We can use that for the purpose.”
“What is PWM?”
“It is a kind of timer mechanism, using which we can keep on generating square wave of desired frequency with desired duty cycle.”
“Meaning we can change the width of the square pulse on the fly as desired.”
“Yes. And that’s were the name pulse width modulation.”
“But, isn’t it still a digital waveform?”
“Yes. But, if the frequency is high enough, the voltage value averages out and the net voltage appears to be the average of the 0V & 5V, weighted by the duty cycle.”
“You mean to say, if I measure it as a DC voltage on multimeter, it would show me a stable averaged voltage.”
“Yes. And for many other practical purposes like LED dimming this is good enough an analog voltage.”
“That’s a great technique. So, I guess details of programming it would be in the datasheet.”
“Correct. Look into the timer section, say Timer0.”
Rohit and Pugs figure out the following about Timer0 from the ATmega16 datasheet pg 71-88:
- Output of the PWM waveform would be on pin OC0/PB3, i.e Pin 4
- For high frequency wave generation, there is a fast PWM mode, enabled by setting each of the 2 bits of WGM0 to 1 in TCCR0 register
- Timer0 has a 8-bit counter TCNT0, which in fast PWM mode increments continuously from 0 to 255 and back to 0
- The counter incrementing frequency is to be set by the 3-bit clock select CS0 in TCCR0 register
- During the increment, it compares TCNT0 with OCR0, and updates the waveform output as per the settings of the 2-bit compare output mode COM0 in TCCR0 register
- If COM0 is set to 2, pin OC0 is set to 0 on match with OCR0 and back to 1 on counter TCNT0 wrapping around to 0, generating a pulse during counting from 0 to OCR0
- Thus values of 0 to 255 for OCR0 controls the duty cycle of the square wave between 0% to 100%, thus giving 0% to 100% of 5V averaged output
In a way, it can also be viewed as an 8-bit DAC converting the digital value 0 to 255 (in OCR0) to analog voltage 0V to 5V (on the pin PB3).
As the output waveform is pre-determined on PB3, Pugs connected the LED with a resistor (as in the previous article) but with the pin PB3 instead of PB0.
Rohit got the following dac.c program coded in C:
/* * DAC is basically averaging out PWM to analog voltages. * This example uses Fast PWM mode of 8-bit Timer/Counter0 for DAC output on * OC0/PB3/AIN1 (Pin 4) */ #include <avr/io.h> #include <util/delay.h> void set_digital(uint8_t digital) { OCR0 = digital; } void init_pwm(uint8_t digital) { set_digital(digital); /* * Setting the Timer/Counter0 in Fast PWM for averaging DAC with highest * possible frequency, i.e. clock select (CS0) as 1. * Getting pulse during up-counting till it matches OCR0. * Output would come on OC0/PB3 (Pin 4). */ TCCR0 = (1 << WGM01) | (1 << WGM00) | (0b10 << COM00); TCCR0 |= (0b001 << CS00); DDRB |= (1 << PB3); // OC0 is PB3. Setting it as output } int main(void) { uint8_t digital = 0xFF; // Full output (5V) init_pwm(digital); while (1) { _delay_ms(10); if (--digital == 0x00) // Decrementing upto almost 0V { digital = 0xFF; } set_digital(digital); } return 0; }
Then, Rohit compiled the program as follows (using the AVR toolchain already installed as in the previous article):
$ avr-gcc -mmcu=atmega16 -DF_CPU=1000000 -Os dac.c -o dac.elf $ avr-objcopy -O ihex dac.elf dac.hex
And finally, downloaded the dac.hex into the ATmega16 with J1 shorted (same as in the previous article), using the following command:
$ avrdude -c ponyser -P /dev/ttyUSB0 -p m16 -U flash:w:dac.hex:i
And … removing the jumper J1, wow! they see the LED dimming and then bright again, dimming and bright again, …