# Micro-controller based Waveform Generator

This 17th article in the series of “Do It Yourself: Electronics”, demonstrates generating various waveforms, using the AVR micro-controller ATmega16.

<< Previous Article

Immediately after Pugs generated the 1KHz sine wave, he got the idea of generating other interesting waveforms like triangular and ramp. So, before trying some resistor-capacitor (RC) based experiments in his room, as per his plan, he first thought of just doing some more experiments with the waveforms, using his micro-controller ATmega16.

On little analysis of his code from the previous article, he realized the following:

1. He doesn’t really need to ride these waveforms on a centred DC voltage like the sine wave
2. And there needs to be a corresponding array containing the amplitudes for each of the waveforms

With that, he got an idea of run-time switching between the different waveforms, on say a switch press. And hence he connected a small switch to pin PB2, with its other end grounded, which he initialized to be pulled-up in the code as follows:

void init_switch(void)
{
DDRB &= ~0b00000100; // PB2 as input
PORTB |= 0b00000100; // Pull up on PB2
}

Now for dynamic switching, Pugs modified the function set_amplitude_centred() to work with all waveforms by taking an additional centred voltage parameter. Also, he defined an additional waveform array to contain the various waveform amplitude arrays, as follows(, so as to be able to switch between them, based on the switch press):

struct
{
int dc_off;
int cnt;
int *amp;
} waveform[] =
{
{DC_OFF, ARR_SIZE(sine), sine},
{0, ARR_SIZE(tri_1k), tri_1k},
{0, ARR_SIZE(tri_2k), tri_2k},
{0, ARR_SIZE(ramp_1k), ramp_1k},
{0, ARR_SIZE(ramp_2k), ramp_2k}
};


where sine, tri_1k, tri_2k, ramp_1k, ramp_2k are the various waveform amplitude arrays defined as follows (assuming the same interrupt cycle of 50us):

/* Length = l = 20. Period = l * 50us = 1000us = 1ms. f = 1KHz */
// 0, 18, 36, 54, 72, 90, 108, 126, 144, 162, 180, 198, 216, 234, 252, 270, 288, 306,
// 324, 342 degrees
int sine[] = {0, 3, 6, 8, 9, 10, 9, 8, 6, 3, 0, -3, -6, -8, -9, -10, -9, -8, -6, -3};
/* Length = l = 20. Period = l * 50us = 1000us = 1ms. f = 1KHz */
int tri_1k[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
/* Length = l = 10. Period = l * 50us = 500us = 0.5ms. f = 2KHz */
int tri_2k[] = {0, 2, 4, 6, 8, 10, 8, 6, 4, 2};
/* Length = l = 20. Period = l * 50us = 1000us = 1ms. f = 1KHz */
int ramp_1k[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19
};
/* Length = l = 10. Period = l * 50us = 500us = 0.5ms. f = 2KHz */
int ramp_2k[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

With all the infrastructure set, Pugs added the variable cur_wave_i for indexing the current waveform and globalized the variable i – the index into the individual amplitude arrays from the interrupt handler aka ISR. Here’s the modified switching logic in the interrupt handler:

int cur_wave_i = 0;
int i = 0;

...

ISR(TIMER0_COMP_vect)
{
set_amplitude_centred(waveform[cur_wave_i].dc_off,
AMP_FACTOR * waveform[cur_wave_i].amp[i]);
if (++i == waveform[cur_wave_i].cnt)
i = 0;
}

Note that, cur_wave_i needs to be global as would be updated from the main() based on the switch press but why has been i globalized? Think harder. And you’d realize that, as the amplitude arrays are of different sizes, the value of i could become out of array size, during switching of the array to be used on switch press. Hence, it also needs to be reset to 0, during the update of cur_wave_i. With all these changes in the previous article’s sine_wave.c code, here’s the final code as waveforms.c:

/*
* Generates variety of waveforms on OC2/PD7, using PWM on Timer 2.
* Variety is generated one at a time, changing based on switch press
* connected to PB2.
*
* The timing is achieved using timer interrupt handler of Timer 0 triggering
* at a 20KHz frequency, i.e. every 50us.
*/

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

#define ARR_SIZE(a) (sizeof(a) / sizeof(*a))

#define PRESCALAR_BITS 0b010
#define PRESCALAR 8 // Gives 1MHz clock

#define DC_OFF 127 // Centered
#define AMP_FACTOR 5 // Should range from 1-12, for 8-bit values between 0 & 2*12*10

/* Length = l = 20. Period = l * 50us = 1000us = 1ms. f = 1KHz */
// 0, 18, 36, 54, 72, 90, 108, 126, 144, 162, 180, 198, 216, 234, 252, 270, 288, 306,
// 324, 342 degrees
int sine[] = {0, 3, 6, 8, 9, 10, 9, 8, 6, 3, 0, -3, -6, -8, -9, -10, -9, -8, -6, -3};
/* Length = l = 20. Period = l * 50us = 1000us = 1ms. f = 1KHz */
int tri_1k[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
/* Length = l = 10. Period = l * 50us = 500us = 0.5ms. f = 2KHz */
int tri_2k[] = {0, 2, 4, 6, 8, 10, 8, 6, 4, 2};
/* Length = l = 20. Period = l * 50us = 1000us = 1ms. f = 1KHz */
int ramp_1k[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19
};
/* Length = l = 10. Period = l * 50us = 500us = 0.5ms. f = 2KHz */
int ramp_2k[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

struct
{
int dc_off;
int cnt;
int *amp;
} waveform[] =
{
{DC_OFF, ARR_SIZE(sine), sine},
{0, ARR_SIZE(tri_1k), tri_1k},
{0, ARR_SIZE(tri_2k), tri_2k},
{0, ARR_SIZE(ramp_1k), ramp_1k},
{0, ARR_SIZE(ramp_2k), ramp_2k}
};

int cur_wave_i = 0;
int i = 0;

void set_amplitude_centred(uint8_t centre, uint8_t a)
{
OCR2 = (centre + a);
}

ISR(TIMER0_COMP_vect)
{
set_amplitude_centred(waveform[cur_wave_i].dc_off,
AMP_FACTOR * waveform[cur_wave_i].amp[i]);
if (++i == waveform[cur_wave_i].cnt)
i = 0;
}

void init_pwm(void)
{
DDRD |= 0b10000000; // PD7

OCR2 = 0; // Initialize to 0V output

// Setup OC2 on PD7
TCCR2 = (1 << WGM21) | (1 << WGM20); /* Fast PWM */
TCCR2 |= (2 << COM20); /* Clear on Match */
TCCR2 |= (1 << CS20); /* No prescaling => Clock @ F_CPU */ // Starts the PWM
}
void init_timer(void) /* Setting Timer 0 for trigger every 50us */
{
sei(); // Enable global interrupts
TIMSK |= (1 << OCIE0); // Enable Compare Match interrupt
/*
* Pre-scaled clock = F_CPU / PRESCALAR
* => Each timer counter increment takes PRESCALAR / F_CPU seconds
* => Formula for timer expiry interval is:
* 		OCR0 (top timer count) * PRESCALAR / F_CPU
* Example: For F_CPU = 8MHz, PRESCALAR = 8
* Pre-scaled clock = 8MHz / 8 = 1MHz
* => Each timer counter increment takes 1/1MHz = 1us
* => Formula for timer expiry interval is OCR0 (top timer count) * 1us
* Example: For 50us = OCR0 * 1us, i.e. OCR0 = 50
*/
OCR0 = (F_CPU / PRESCALAR) / 20000; /* 1/20000 for 50us */
/*
* Setting & Starting the Timer/Counter0 in CTC (Clear Timer on Compare)
* (non-PWM) for controlling timer expiry interval, directly by the compare
* register
*/
TCCR0 = (1 << WGM01) | (0 << WGM00) | PRESCALAR_BITS;
}
void init_switch(void)
{
DDRB &= ~0b00000100; // PB2 as input
PORTB |= 0b00000100; // Pull up on PB2
}

int main(void)
{
init_pwm();
init_timer();
init_switch();

while (1)
{
if (!(PINB & (1 << PB2))) // Switch pressed
{
if (++cur_wave_i == ARR_SIZE(waveform))
{
i = 0; // Reset the index inside the waveform
cur_wave_i = 0;
}
}
_delay_ms(20);
}

return 0;
}

Then, Pugs compiled the program as follows (Note the value of F_CPU defined as 8MHz, same as in the previous article):

$avr-gcc -mmcu=atmega16 -DF_CPU=8000000 -Os waveforms.c -o waveforms.elf$ avr-objcopy -O ihex waveforms.elf waveforms.hex

And finally, downloaded the waveforms.hex into the ATmega16 with J1 shorted (same as in the previous articles), using the following command:

$avrdude -c ponyser -P /dev/ttyUSB0 -p m16 -U flash:w:waveforms.hex:i Note that Pugs have used the same ATmega16, which has been modified for 8MHz internal clock, same as in the previous article, by writing 0xE4 into the lfuse, using avrdude. Otherwise, you may have to issue the following command as well: $ avrdude -c ponyser -P /dev/ttyUSB0 -p m16 -U lfuse:w:0xE4:m

Pugs then checked the output between PD7 and GND pins of the micro-controller (after removing the short of jumper J1), on the home-made PC oscilloscope, as created in his previous PC Oscilloscope article. It showed up the sine wave exactly in the previous article. On press of the switch connected between PB2 and GND, Pugs enjoyed observing the triangular and ramp waveforms of 1 & 2 KHz, as well.

Next Article >>

Send article as PDF

# Micro-controller based Sine Wave Generator

This 16th article in the series of “Do It Yourself: Electronics”, demonstrates generating sine wave using the AVR micro-controller ATmega16.

<< Previous Article

Pugs have been thinking of trying some resistor-capacitor (RC) based experiments in his room, the way he has done in his electronics lab. But for that he needed to have some sort of sine wave signal generator. As usual of Pugs, rather than buying a function (wave) generator, he thought of starting with creating a simple sine wave generator of his own, say at a fixed frequency of 1KHz, using his micro-controller ATmega16.

For that Pugs needed some sort of analog output generator. And, interestingly he remembered already creating a PWM-based digital to analog conversion (DAC) approximation with Rohit, in one of his previous articles. There they have generated the analog output voltage on OC0/PB3 using Timer0. However, as always, Pugs is looking for learning things he has not yet tried. So, this time, he thought of doing the analog voltage generation on OC2/PD7 using Timer2.

On deeper thought, it clicked to Pugs that for a sine wave one needs different analog outputs but in a periodic fashion. And to get a perfect periodicity, it would possibly need one more timer, say Timer0.

“So, this time why not try interrupt-based timer for the periodicity. I would also get a chance to write a interrupt handler”, flashed the thought through Pugs’ mind.

Triggered by these thoughts, here is what the initialization code written by Pugs for the Timer2 as PWM and Timer0 as 50us period timer, respectively:

#include <avr/io.h>
#include <avr/interrupt.h>

#define PRESCALAR_BITS 0b010
#define PRESCALAR 8 // Gives 1MHz clock

void init_pwm(void)
{
DDRD |= 0b10000000; // PD7

OCR2 = 0; // Initialize to 0V output

// Setup OC2 on PD7
TCCR2 = (1 << WGM21) | (1 << WGM20); /* Fast PWM */
TCCR2 |= (2 << COM20); /* Clear on Match */
TCCR2 |= (1 << CS20); /* No prescaling => Clock @ F_CPU */ // Starts the PWM
}
void init_timer(void) /* Setting Timer 0 for trigger every 50us */
{
sei(); // Enable global interrupts
TIMSK |= (1 << OCIE0); // Enable Compare Match interrupt
/*
* Pre-scaled clock = F_CPU / PRESCALAR
* => Each timer counter increment takes PRESCALAR / F_CPU seconds
* => Formula for timer expiry interval is:
*		OCR0 (top timer count) * PRESCALAR / F_CPU
* Example: For F_CPU = 8MHz, PRESCALAR = 8
* Pre-scaled clock = 8MHz / 8 = 1MHz
* => Each timer counter increment takes 1/1MHz = 1us
* => Formula for timer expiry interval is OCR0 (top timer count) * 1us
* Example: For 50us = OCR0 * 1us, i.e. OCR0 = 50
*/
OCR0 = (F_CPU / PRESCALAR) / 20000; /* 1/20000s for 50us */
/*
* Setting & Starting the Timer/Counter0 in CTC (Clear Timer on Compare)
* (non-PWM) for controlling timer expiry interval, directly by the compare
* register
*/
TCCR0 = (1 << WGM01) | (0 << WGM00) | PRESCALAR_BITS;
}

Obviously, Pugs referred to the ATmega16 datasheet pg 71-86 & pg 117-134. But the earlier articles on DAC and Music Generation using micro-controller were quick starters.

## Computing the sine wave

Now, why is the Timer0 period set to 50us, meaning it will trigger every 50us. Let’s understand that by computing the sine wave. To generate a sine wave, one needs to output various analog voltages – theoretically speaking infinite different values between -1 to +1 multiplied by the amplitude. But practically, in our case we can make it only finite, say 20 different values. So after these 20 different values, we would repeat them, thus generating a periodic approximate sine wave. And to achieve a 1KHz sine wave, or in other words sine wave with a period of 1 / 1KHz = 1ms, we would need to output these 20 values spread over this 1ms, meaning outputting 1 value every 1ms / 20 = 50us. And, setting the Timer0 to 50us, now means outputting 1 value every time the Timer0 handler is triggered.

Time-wise computation for the sine wave done. But what about the amplitude-wise values to output during those Timer0 handler triggers. For that, note that the output on the GPIOs of a micro-controller could be only between 0V to 5V, which maps to programming the OCR2 of Timer2 between 0 to 255. Then, how do we generate negative voltages for sine wave. A usual trick applied in such scenarios is shift the 0V to the midpoint 2.5V and then oscillate the sine wave between 0 & 5V. That translates to oscillating the OCR2 values between 0 to 255 with 127 as the midpoint. Great, so for that let’s have this small handy function set_amplitude_centred():

#define DC_OFF 127 // Centred
#define AMP_FACTOR 5 // Should range from 1-12, for 8-bit values between 0 & 2*12*10

void set_amplitude_centred(uint8_t a)
{
OCR2 = (DC_OFF + a); // DC shift by peak to get it centred around DC_OFF
}

With this, we could have the maximum sine amplitude of 127 units in terms of OCR2. So, we shall take some default, say 10, and control it by multiplying it with the AMP_FACTOR macro defined above. This allows us to have a maximum amplification factor of 12 for an amplification of 12 * 10 = 120 (< 127). Currently, just defined it to some intermediate value of 5.

All boundaries set, but what about the actual 20 values we need. For that, we have to use the sine function at intervals of 360° / 20 = 18°, i.e. $\pi$ / 10 – the values being 0, 0.3, 0.6, 0.8, 0.9, 1.0, 0.9, 0.8, 0.6, 0.3, 0, -0.3, -0.6, -0.8, -0.9, -1.0, -0.9, -0.8, -0.6, -0.3. Multiplying by 10 (our default amplitude), we get 0, 3, 6, 8, 9, 10, 9, 8, 6, 3, 0, -3, -6, -8, -9, -10, -9, -8, -6, -3. Hence, finally we need to set the corresponding value, centred with 127, to OCR2 on every Timer2 interrupt trigger.

## Programming the sine wave

Here’s the final sine_wave.c coded with an empty “while 1” in main(), as all logic is handled in the interrupt handler specified by ISR().

/*
* Generates sine wave @ 1KHz on OC2/PD7, using PWM on Timer 2
*
* The timing is achieved using timer interrupt handler of Timer 0 triggering
* at a 20KHz frequency, i.e. every 50us.
*/

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

#define ARR_SIZE(a) (sizeof(a) / sizeof(*a))

#define PRESCALAR_BITS 0b010
#define PRESCALAR 8 // Gives 1MHz clock

#define DC_OFF 127 // Centred
#define AMP_FACTOR 5 // Should range from 1-12, for 8-bit values between 0 & 2*12*10

/* Length = l = 20. Period = l * 50us = 1000us = 1ms */
// 0, 18, 36, 54, 72, 90, 108, 126, 144, 162, 180, 198, 216, 234, 252, 270, 288, 306,
// 324, 342 degrees
int sine[] = {0, 3, 6, 8, 9, 10, 9, 8, 6, 3, 0, -3, -6, -8, -9, -10, -9, -8, -6, -3};

void set_amplitude_centred(uint8_t a)
{
OCR2 = (DC_OFF + a); // DC shift by peak to get it centred around DC_OFF
}

ISR(TIMER0_COMP_vect)
{
static int i = 0;

set_amplitude_centred(AMP_FACTOR * sine[i]);
if (++i == ARR_SIZE(sine))
i = 0;
}

void init_pwm(void)
{
DDRD |= 0b10000000; // PD7

OCR2 = 0; // Initialize to 0V output

// Setup OC2 on PD7
TCCR2 = (1 << WGM21) | (1 << WGM20); /* Fast PWM */
TCCR2 |= (2 << COM20); /* Clear on Match */
TCCR2 |= (1 << CS20); /* No prescaling => Clock @ F_CPU */ // Starts the PWM
}
void init_timer(void) /* Setting Timer 0 for trigger every 50us */
{
sei(); // Enable global interrupts
TIMSK |= (1 << OCIE0); // Enable Compare Match interrupt
/*
* Pre-scaled clock = F_CPU / PRESCALAR
* => Each timer counter increment takes PRESCALAR / F_CPU seconds
* => Formula for timer expiry interval is:
*		OCR0 (top timer count) * PRESCALAR / F_CPU
* Example: For F_CPU = 8MHz, PRESCALAR = 8
* Pre-scaled clock = 8MHz / 8 = 1MHz
* => Each timer counter increment takes 1/1MHz = 1us
* => Formula for timer expiry interval is OCR0 (top timer count) * 1us
* Example: For 50us = OCR0 * 1us, i.e. OCR0 = 50
*/
OCR0 = (F_CPU / PRESCALAR) / 20000; /* 1/20000s for 50us */
/*
* Setting & Starting the Timer/Counter0 in CTC (Clear Timer on Compare)
* (non-PWM) for controlling timer expiry interval, directly by the compare
* register
*/
TCCR0 = (1 << WGM01) | (0 << WGM00) | PRESCALAR_BITS;
}

int main(void)
{
init_pwm();
init_timer();

while (1)
{
_delay_ms(500);
}

return 0;
}

Then, Pugs compiled the program as follows (Note the value of F_CPU defined as 8MHz as in the previous article):

$avr-gcc -mmcu=atmega16 -DF_CPU=8000000 -Os sine_wave.c -o sine_wave.elf$ avr-objcopy -O ihex sine_wave.elf sine_wave.hex

And finally, downloaded the sine_wave.hex into the ATmega16 with J1 shorted (same as in the previous articles), using the following command:

$avrdude -c ponyser -P /dev/ttyUSB0 -p m16 -U flash:w:sine_wave.hex:i Note that Pugs have used the same ATmega16, which has been modified for 8MHz internal clock, as in the previous article, by writing 0xE4 into the lfuse, using avrdude. Otherwise, you may have to issue the following command as well: $ avrdude -c ponyser -P /dev/ttyUSB0 -p m16 -U lfuse:w:0xE4:m

Pugs then checked the DC & AC voltages between PD7 and GND pins of the micro-controller, after removing the short of jumper J1. They showed up as approximately 2.5V (average) and 0.7V (root-mean-square (RMS)), respectively. And it was a visual treat to view the sine wave on the home-made PC oscilloscope, as created in his previous PC Oscilloscope article.

Next Article >>

Send article as PDF

# Digital to Analog Conversion using a Micro-controller

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.

<< Previous Article

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, …

Next Article >>

Send article as PDF