Thursday, February 25, 2021

Playing music and tones using a piezo buzzer - library for AVR microcontrollers using timer interrupt | ATmega328

This is a library for playing monophonic music using PWM and a piezoelectric buzzer. Monophonic means it can play only one note at a time. Regardless, it can produce some nice music. Optionally a led can be made to blink with the music rhythm.

Apart from playing songs it can also be used for tone generation useful in projects where audio indicators are needed.

All you need is a 16-bit timer that can be one of the following: timer1, timer3 and timer4 (provided that the microcontroller has them) and an 1ms interval interrupt where to place the main function. This is needed because the notes must have a certain duration.

Playing music and tones using a piezo buzzer - library for AVR microcontrollers using timer interrupt | ATmega328P, ATmega88

Playing music on a piezoelectric buzzer using PWM and a microcontroller


To make a song first we need musical notes. These can be found in the file "pitches.h". Here are just two octaves from the file and look like this 

#define NOTE_C4  262
#define NOTE_CS4 277
#define NOTE_D4  294
#define NOTE_DS4 311
#define NOTE_E4  330
#define NOTE_F4  349
#define NOTE_FS4 370
#define NOTE_G4  392
#define NOTE_GS4 415
#define NOTE_A4  440
#define NOTE_AS4 466
#define NOTE_B4  494

#define NOTE_C5  523
#define NOTE_CS5 554
#define NOTE_D5  587
#define NOTE_DS5 622
#define NOTE_E5  659
#define NOTE_F5  698
#define NOTE_FS5 740
#define NOTE_G5  784
#define NOTE_GS5 831
#define NOTE_A5  880
#define NOTE_AS5 932
#define NOTE_B5  988

For example NOTE_G4 is the note G in octave 4, NOTE_CS5 is the note C sharp in octave 5. After each note is it's frequency in Hertz. The higher the frequency the higher the pitch.

These notes can be used to make songs like this

const int happy_birthday[] PROGMEM = {
	NOTE_C4,4,	NOTE_C4,8, 
	NOTE_D4,-4,	NOTE_C4,	NOTE_F4,
	NOTE_E4,-2,	NOTE_C4,4,	NOTE_C4,8, 
	NOTE_D4,-4,	NOTE_C4,	NOTE_G4,
	NOTE_F4,-2,	NOTE_C4,4,	NOTE_C4,8,

	NOTE_C5,-4,	NOTE_A4,	NOTE_F4,
	NOTE_E4,	NOTE_D4,	NOTE_AS4,4,	NOTE_AS4,8,
	NOTE_A4,-4,	NOTE_F4,	NOTE_G4,	NOTE_F4,-2,
	MUSIC_END
};

This will play the well known "Happy Bird Day" song. The array has the PROGMEM attribute which means it will only be stored in flash memory not in flash memory AND RAM memory as it will be the case without it. This is very important because without this attribute the RAM will be filled very quickly.

After the note is the note's duration. 1 is a whole note, 2 is half a note, 4 is a quarter note and so on. So a higher number means a shorter note. A negative duration means a dotted note. For example -4 means a dotted quarter note, that is, a quarter plus an eighteenth or note duration * 1.5.

Not every note has a duration after it. That means the duration hasn't changed. Only when the duration of the note changes there is a number after it. Every note can have a duration after it even if it's the same duration but this will only increase the size of the array.

MUSIC_END is a flag and represents a 0.

To put a rest (pause) between notes use REST, duration. Same as NOTE,duration. REST represents 1.

There is also these defines: __CHIP_TUNES_START_MARKER__. __CHIP_TUNES_END_MARKER__, __CHIP_TUNES_GOTO_MARKER__. In some melodies there are sections that repeats and these markers move the pointer to the section that needs repeating thus saving lots of flash memory.

To know how exactly the notes are played by the microcontroller check out the main code, but basically the 16-bit timer is set to generate a PWM frequency based on the note.

Where to get the songs

I have included down below a few songs but if you need more you can find at this link https://dragaosemchama.com/en/2019/02/songs-for-arduino/. There are many sites where you can find music sheets for different songs provided you know music theory. I personally don't. Because the songs provided at that link have repeated duration thus increasing the array size, I have made a header file "tunes.h" with some songs where I have removed the repeated duration and duplicate sections. 

 


 

Types of buzzers

Before going any further I should clarify regarding the types of buzzers for those who are not familiar with them.

There are passive buzzers (or piezoelectric transducers but buzzer is shorter) and active buzzers.

Active piezoelectric buzzers

This types of buzzers have built-in circuitry that when a DC voltage is applied to them they buzz at a fixed frequency. They cannot produce music but they are easy to drive and can be used as sound indicators. They look like passive buzzers from the outside.

Passive piezoelectric buzzers

There are two main types of passive buzzers: just the piezoelectric diaphragm and the diaphragm inside a resonant case.

Passive or active piezoelectric buzzers with resonant case
Passive or active piezoelectric buzzers with resonant case

Piezoelectric diaphragm
Piezoelectric diaphragm

The resonant case makes the buzzer sound louder while the diaphragm could be used for a postcard or hand-watch because of the very low profile. The passive buzzers needs to be controlled by an AC or pulsed DC voltage (PWM).

There are also magnetic buzzers and buzzers with a feedback wire for self resonance. Links about them can be found at the end of this page. 

Did you know: applying a DC voltage for a long time can degrade the buzzer.



 

Driving and connecting a piezoelectric buzzer with a microcontroller

There are two ways to drive the piezo buzzer with a microcontroller: 

  • having the buzzer connected directly to the microcontroller using 1 or 2 pins. With 2 pins the buzzer will be louder because of the push-pull
  • using a NPN transistor to drive the buzzer and 1 microcontroller pin

The second method is my preferred way because you can use higher voltages than the microcontroller can provide which will make the buzzer louder.

The library supports both methods and if you have the buzzer connected directly to the microcontroller then you could use two pins in push-pull mode to make the buzzer louder with a lower voltage such as 5V. For example if Timer1 is used the buzzer could be connected on pins OCR1A and OCR1B. This way the buzzer will see 10V even if the microcontroller outputs 5V.

Driving the buzzer directly with the microcontroller pins 

When using 2 pins the buzzer is connected across OCnA and OCnB n representing the selected timer number. If only one pin is used the buzzer is connected between OCnA or OCnB and ground.

Is recommended to place a resistor between 10 to 100 ohm in series because the buzzer is capacitive and could stress the microcontroller pin gate driver. I tested it with 100 ohm and is sounded good.

Wiring a buzzer to AVR microcontroller
1 pin method: the buzzer is wired to OC1A and ground

Piezoelectric buzzer connected directly to microcontroller pins
2 pins method: the piezo buzzer is connected to OC1A and OC1B because Timer1 was selected


Driving the piezo buzzer using an NPN transistor

Driving a piezo buzzer using a transistor and microcontroller

Since the piezoelectric transducer is capacitive, the resistor in parallel with the buzzer also improves the high frequency response by dissipating quicker the energy stored in the piezoelectric element while the transistor is off, and also protects the transistor from voltage spikes because there is also inductance.

Finally the code

Include the file "chipTunes.h". This file will include "pitches.h" and "tunes.h", so they must be  downloaded and placed in the same location as "chipTunes.h".

#include "chipTunes.h"

Select the timer: 1, 3 or 4. These settings are inside the chipTunes.h file in the user setup section.

#define CHIP_TUNES_TIMER 	CHIP_TUNES_TIMER4

Specify where the pin is located. For 2 pins method this pin is where OCnA is. For 1 pin method this can be either OCnA or OCnB. This is an example for pin PC4.

#define CHIP_TUNES_PIN1_DDR 	DDRC
#define CHIP_TUNES_PIN1_PORT 	PORTC
#define CHIP_TUNES_PIN1_PIN  	PC4

In case 2 pins are used, next is the second pin. If only 1 pin is used comment out this lines

#define CHIP_TUNES_PIN2_DDR 	DDRB
#define CHIP_TUNES_PIN2_PORT 	PORTB
#define CHIP_TUNES_PIN2_PIN  	PB2

For one pin method this setting specifies which channel to use: OCnA or OCnB

#define CHIP_TUNES_CHANNEL	CHANNEL_A

Optionally if you wish to blink a led on music, set this to true, false otherwise

#define CHIP_TUNES_BLINK_LED	0
 

Setup function

void chipTunes_Init(void)

This function is used to set up the timer and pins


Tone generation using PWM

void chipTunes_Tone(uint16_t tone, uint16_t duration_ms)

tone: the tone frequency in Hertz
duration_ms: tone duration in milliseconds

This function is non-blocking meaning the CPU can do other things while the tone is output. For this function to work the following code must be in the 1ms interrupt routine

if(chipTunes_IsPlaying()) chipTunes_ISR();

 

Main function

void chipTunes_ISR(void)

This function must be inside an ISR function that triggers every 1 millisecond, like so

if(chipTunes_IsPlaying()) chipTunes_ISR();

You can find here the millis.h library for AVR that can do that https://www.programming-electronics-diy.xyz/2021/01/millis-and-micros-library-for-avr.html

 

Playing a song

void chipTunes_Play(const int *melody, uint8_t tempo)

melody: the array name that includes the musical notes.
tempo:  tempo of the song. The higher the number the faster the song will be played.

Starts the timer and sets a flag indicating that a tune is currently playing. 

 

Stopping the song or tone

void chipTunes_Stop(void)

 

Setting the volume

void chipTunes_SetVolume(volume)

 volume: a number from 0 to 50. Default value is 50 which is the maximum volume.

This kinda works. Values over 10 to 50 barely make any difference but if you want a quieter buzzer start from a value of 1.


Check if a song or tone is playing

char chipTunes_IsPlaying(void)

If a song or tone is currently playing returns 1 and 0 otherwise.

 

Generating alarm tone

void chipTunes_alert_alarm(uint8_t vuvuzela)

vuvuzela: if 0 the alarm will sound like a siren, if 1 it will sound like a vuvuzela.

Check the video to hear how the sounds sound. This function is a blocking function and will exit after the alarm is done. After that a 200ms delay is added because usually you want it to play it multiple times. 

 

Creating a playlist

To be able to play a specific song together with it's tempo, you need an array for the playlist and another one for the tempo. By default they look like this. Add more or comment out the ones that are not used

const int *chipTunes_Playlist[] = {
	tetris_theme,
	fur_elise,
	cannon_in_d_pachelbel,
	greensleeves,
	happy_birthday,
	ode_to_joy
};

const uint8_t *chipTunes_Tempo[] = {
	144, 	// tetris_theme
	80, 	// fur_elise
	100,	// cannon_in_d_pachelbel
	70,	// greensleeves
	140,	// happy_birthday
	114,	// ode_to_joy
};

Code example:

#include <avr/io.h>
#include "chipTunes.h"

const int star_wars_theme[] PROGMEM = {
	NOTE_AS4,8,		NOTE_AS4,		NOTE_AS4,															//1
	NOTE_F5,2,		NOTE_C6,
	NOTE_AS5,8,		NOTE_A5,		NOTE_G5,		NOTE_F6,2,		NOTE_C6,4,
	NOTE_AS5,8,		NOTE_A5,		NOTE_G5,		NOTE_F6,2,		NOTE_C6,4,
	NOTE_AS5,8,		NOTE_A5,		NOTE_AS5,		NOTE_G5,2,		NOTE_C5,8,		NOTE_C5,	NOTE_C5,
	NOTE_F5,2,		NOTE_C6,
	NOTE_AS5,8,		NOTE_A5,		NOTE_G5,		NOTE_F6,2,		NOTE_C6,4,

	NOTE_AS5,8,		NOTE_A5,		NOTE_G5,		NOTE_F6,2,		NOTE_C6,4,							//8
	NOTE_AS5,8,		NOTE_A5,		NOTE_AS5,		NOTE_G5,2,		NOTE_C5,-8,		NOTE_C5,16,
	NOTE_D5,-4,		NOTE_D5,8,		NOTE_AS5,		NOTE_A5,		NOTE_G5,		NOTE_F5,
	NOTE_F5,		NOTE_G5,		NOTE_A5,		NOTE_G5,4,		NOTE_D5,8,		NOTE_E5,4,	NOTE_C5,-8,		NOTE_C5,16,
	NOTE_D5,-4,		NOTE_D5,8,		NOTE_AS5,		NOTE_A5,		NOTE_G5,		NOTE_F5,

	NOTE_C6,-8,		NOTE_G5,16,		NOTE_G5,2,		REST,8,		NOTE_C5,								//13
	NOTE_D5,-4,		NOTE_D5,8,		NOTE_AS5,		NOTE_A5,	NOTE_G5,			NOTE_F5,
	NOTE_F5,		NOTE_G5,		NOTE_A5,		NOTE_G5,4,	NOTE_D5,8,			NOTE_E5,4,	NOTE_C6,-8,		NOTE_C6,16,
	NOTE_F6,4,		NOTE_DS6,8,		NOTE_CS6,4,		NOTE_C6,8,	NOTE_AS5,4,			NOTE_GS5,8,	NOTE_G5,4,		NOTE_F5,8,
	NOTE_C6,1,
	MUSIC_END
};

int main(void){
	chipTunes_Init();
	chipTunes_Play(star_wars_theme, 108);

    while (1){
		
    }
}

// 1ms interrupt. It can be the millis library at this link
// https://www.programming-electronics-diy.xyz/2021/01/millis-and-micros-library-for-avr.html

ISR(){
	if(chipTunes_IsPlaying()) chipTunes_ISR();
}

Code example on how to play a playlist of tunes:

#include "chipTunes.h"

int main(void){
        chipTunes_Init();
	const uint8_t NR_OF_TUNES = sizeof(chipTunes_Tempo);
	uint8_t tune = 0;
	
	while(1){
	
		// Whait for the song to end then move to the next one
		if(chipTunes_IsPlaying() == 0 && NR_OF_TUNES){
			// Delay between songs
			_delay_ms(2000);
			
			chipTunes_Play(chipTunes_Playlist[tune], chipTunes_Tempo[tune]);
			
			tune++;
			if(tune > NR_OF_TUNES - 1) tune = 0;
		}
	}

}
 
// 1ms interrupt. It can be the millis library at this link
// https://www.programming-electronics-diy.xyz/2021/01/millis-and-micros-library-for-avr.html

ISR(){
	if(chipTunes_IsPlaying()) chipTunes_ISR();
} 


Download

v2.0

chipTunes.h

chipTunes - the main library code

pitches.h

pitches - contains the musical notes that chipTunes needs

tunes.h

Contains the following songs:

Dart Vader theme (Imperial March) - Star wars
Tetris theme - (Korobeiniki)
Mario Main Theme
Mario Underworld Melody
Fur Elise - Ludwig van Beethoven
Cannon in D - Pachelbel
Greensleeves
Ode to Joy - Beethoven's Symphony No. 9
Happy Birthday

Most of the songs inside the "tunes.h" are from this website https://dragaosemchama.com/en/2019/02/songs-for-arduino/ but with the duplicate duration removed. Leave him a nice comment. Each song is credited separately.

Other Resources

Fundamental Rhythm Explained for Beginners by Piano Lessons On The Web (12:49)

https://www.musictheory.net/lessons/11 - about note duration

https://en.wikipedia.org/wiki/Octave

https://www.murata.com/~/media/webrenewal/support/library/catalog/products/sound/p15e.ashx - many things about piezoelectric transducers

https://www.cuidevices.com/product-spotlight/piezo-and-magnetic-buzzers - and again more things about buzzers

1 comment:

  1. something is missing for me to work, not letting me put the ISR method in

    ReplyDelete