(June 2009)

I am a software developer by vocation, but I recently discovered that hardware tinkering can be just as fun... So I went ahead and spent the amazing amount of 6 Euros (!) to buy the necessary parts for tinkering with an ATmega168...

Many thanks to Denilson Figueiredo for the excellent tutorial that allowed me to program the microcontroller with just a parallel port circuit.

The 5.1 High Fidelity Audio in action

After being programmed by my code, the ATmega168 started singing the "Happy Birthday" song... The videos were taken with an old mobile phone (hence the superb quality):
 

(An overall view,
zooming-in on the buzzer)

(Close-up recording,
with sound)

And here are some snaps:
Don't you dare laugh, this is science at its best! :‑) There's no "beep" function in the microcontroller (obviously :‑), so I am driving the electrical signal to the buzzer, oscillating it the appropriate times per second (min:587, max:1175Hz) to play the notes...

Schematic and code

The schematic is... well... one buzzer added between ground and the controller's pin 24 :‑)

You can download the code I wrote (and its Makefile) from here. Make sure you have avr-gcc installed (if you use Debian or Ubuntu, simply apt-get install gcc-avr). Here's how I drive the microcontroller:


/*  Plays the birthday music on a buzzer attached to ATmega168 pin 24      */
/*  Written by Thanassis Tsiodras, June 2009                               */

/*  I've dropped the CKDIV8 in lfuse, so my ATmega168 runs at 8MHz         */
#define F_CPU 8000000

#include <avr/io.h>          /*  this contains all the IO port definitions */
#include <util/delay.h>      /*  for _delay_us and _delay_ms               */

#define UPPER_LIMIT_IN_DELAY_US (768000000/F_CPU)

/*  The song, in pairs of frequency and duration (in milliseconds)         */
struct _Note {
    int frequency;
    int durationMS;
} g_Notes[] = {
    {587,500}, {659,500}, {587,500}, {784,500}, {740,700}, {0,200},
    {587,500}, {659,500}, {587,500}, {880,500}, {784,700}, {0,200},
    {587,500}, {1175,500}, {988,500}, {784,500}, {740,500}, {659,500},
    {1046,500}, {988,500}, {784,500}, {880,500}, {784,700},
};

/*  The calculation "cache" that helps us avoid floating point in the loop */
/*  (read further below)                                                   */
unsigned g_remainderTicks[ sizeof(g_Notes)/sizeof(struct _Note) ];

/*  Create a specific frequency for a specified amount of ms.              */
void buzz(int idx)
{
    /*  From the desired frequency, we want the half-period, since we will */
    /*  set the buzzer on, sleep for half a period, set it off, sleep for  */
    /*  half a period, and then repeat...                                  */

    /*  <--Period-->                                                       */
    /*   ____       _____       _                                          */
    /*  |    |     |     |     |                                           */
    /*  |    |_____|     |_____|                                           */

    /*  <-H->                                                              */

    /*  H=half period */

    /*  We want to use _delay_us to sleep for half a period, but...        */
    /*  _delay_us has a limitation: the largest value it can sleep for is  */

    /*     Upper limit in microSeconds = 768 / FrequencyInMHz              */

    /*  If using a 1MHz clock, this value is 768us... The lowest note in   */
    /*  the "Happy birthday" tune is however 587Hz, so it requires a half  */
    /*  period of 1000000/(2*587) = 851us, larger than the limit...        */

    /*  Not to mention that I have already removed the CKDIV8 fuse, so my  */
    /*  ATmega168 runs at 8MHz: the limit there is 96us !                  */

    /*  We therefore sleep the half periods in steps of "upper limit",     */
    /*  until we have a remainder less than UPPER_LIMIT_IN_DELAY_US.       */

    int freq = g_Notes[idx].frequency, ms = g_Notes[idx].durationMS;

    /*  We first calculate the total micro-seconds for this note.          */
    int32_t total_us = (((int32_t) ms)*1000LL);

    register unsigned desiredHalfPeriod_us = 500000/freq;

    /* How many loops will we run, delaying one Period each time?          */
    int loops = total_us/(2*desiredHalfPeriod_us);

    /*  Originally, at this point the following code existed: */

    /*  while(loops--) {                                                   */
    /*    PORTC ^= 0x02;                 // Buzzer on                      */
    /*    _delay_us(desiredHalfPeriod);  // sleep for...                   */
    /*    PORTC ^= 0x02                  // Buzzer off;                    */
    /*    _delay_us(desiredHalfPeriod);  // sleep for...                   */
    /*  } */

    /*  It turned out however, that this code did not work...              */
    /*  The implementation of _delay_us does floating point arithmetic     */
    /*  to calculate the number of ticks to wait, and since _delay_us is   */
    /*  called all the time, we end up doing lots of floating point work   */
    /*  which takes too much time and  messes up our timing...             */

    /*  Two things, after reading the code in util/delay.h :               */

    /*  First, we know that the UPPER_LIMIT_IN_DELAY_US is actually the    */
    /*  same as a _delay_loop_1(255), so we will be "eating up" the        */
    /*  desiredHalfPeriod calling this directly - and knowing that we      */
    /*  "eat" UPPER_LIMIT_IN_DELAY_US each time we do it.                  */

    /*  Second, this will eventually get us to a stage where the remaining */
    /*  microseconds will be less than UPPER_LIMIT_IN_DELAY_US; We can't   */
    /*  use _delay_us for them (floating point is slow...), so we...       */
    /*  pre-calculate the "remainder" for each note, at the beginning of   */
    /*  the main() function below, and store it inside g_remainderTicks... */

    /*  We therefore prepare a "calculation" cache, that we use per loop...*/

    /*  And here, in all its glory, the main oscillator loop, without any  */
    /*  slow floating point anywhere...                                    */

    while(loops--) {
        register unsigned counter = desiredHalfPeriod_us;
        
        PORTC ^= 0x02;                  /*  Buzzer on                      */
                                        /*  Now sleep for half a period    */
        while(counter) {
            if (counter >= UPPER_LIMIT_IN_DELAY_US) {
                _delay_loop_1(255);
                counter -= UPPER_LIMIT_IN_DELAY_US;
            } else {
                _delay_loop_1(g_remainderTicks[idx]);
                break;
            }
        }

        PORTC ^= 0x02;                  /*  Buzzer off                     */
                                        /*  Now sleep for half a period    */
        counter = desiredHalfPeriod_us;
        while(counter) {
            if (counter >= UPPER_LIMIT_IN_DELAY_US) {
                _delay_loop_1(255);
                counter -= UPPER_LIMIT_IN_DELAY_US;
            } else {
                _delay_loop_1(g_remainderTicks[idx]);
                break;
            }
        }
    }
}

int main()
{
    int idx = 0;

    DDRC  = 0x0F;  /*  PC0..PC3 as output                                  */
    PORTC = 0x00;  /*  all PORTC output pins Off                           */

    /* First, update the "remainder" table for each note (a "cache",       */
    /* read the comments inside the function "buzz" above)                 */

    while(idx<sizeof(g_Notes)/sizeof(struct _Note)) {
        unsigned desiredHalfPeriod_us = 500000/g_Notes[idx].frequency;
        uint8_t __ticks;
        double __tmp = ((F_CPU) / 3e6) * 
            (desiredHalfPeriod_us % UPPER_LIMIT_IN_DELAY_US);
        if (__tmp < 1.0)
            __ticks = 1;
        else if (__tmp > 255) {
            __ticks = 255;
        } else
            __ticks = (uint8_t)__tmp;
        g_remainderTicks[idx++] = __ticks;
    }

    /* Then, play the notes (and the silences), over and over again...     */

    while(1) {
        idx = 0;
        while(idx<sizeof(g_Notes)/sizeof(struct _Note)) {
            PORTC ^= 0x01;
            if (g_Notes[idx].frequency == 0)
                _delay_ms(g_Notes[idx].durationMS);
            else
                buzz(idx);
            idx ++;
        }
    }
    return 0;
}


profile for ttsiodras at Stack Overflow, Q&A for professional and enthusiast programmers
GitHub member ttsiodras
 
Index
 
 
CV
 
 
Updated: Sat Oct 8 12:33:59 2022