rocketnumbernine

Andrew's Project Blog: Hardware, Software, Stuff I find Interesting

The following describes a quick project to create 'music' (ok 'sound') with a piezo-electric transducer driven directly from an AVR microcontroller and as few other components as possible.

Driving the Piezo-electric transducer.

Just about all AVR microcontrollers have one or more counters/timers that can be driven from the main clock or an external signal. A prescaler can be used to reduce the frequency of the clock (16MHz on the Arduino) by 8, 64, 256, or 1024 when this scaled clock ticks it increments a timer and when that timer reaches a specified MAX value it can create an interrupt to execute a piece of code or toggle a register bit. That register bit can be connected to an IO pin and used to drive an external circuit.

The brilliance of timers is that once setup, they can run unattended with no code. They're very useful for generating accurate fixed width pulses to control servo motors for example, (the MAX value is set so that the output pin is high for the right amount of time to specify the position of the servo arm). The controller will continue to generate the pulses at regular intervals allowing the code to do other things - all it has to do is set the register holding the MAX value of the counter to the desired servo position.

This example uses the AVR counter/timer 0 mechanism in CTC mode (Clear Timer on Compare). As the counter is incremented its value is compared with the MAX value specified in a register, when these values are equal the timer is cleared back to zero and starts counting up again. When this happens the microcontroller can be configured to toggle the value of the OC0A output (which is connected to an output port - D6 on the atmega68/132, B7 on the AT90USB162).

As the output pin is toggled each time the counter reaches its max value we'll get a square wave of frequency:

frequency = clock/(2 * PRESCALE * (1+MAX))

As Timer0 is an 8 bit counter (max value 255), with a 16Mz clock we can generate frequencies of between 8MHz (no prescale, max=0) and 30Hz (1024 prescale, max=255). The human ear can hear between 15-20KHz although age and the abilities of the piezo transducer will limit this.

The test program below sets a fixed prescale of 256 and cycles the max value of the counter between 0 and 255 to test that things are working (will output between 110-12KHz). Note, the arduino IDE doesn't provide helper functionality for controlling the timer in this mode (the analogueWrite() method uses a similar technique for generating PWM). The registers are available as variables/constants (MAX counter value = OCR0A, Timer/Counter Control Register = TCCR0A) and individual bits have to be set to configure the timer accordingly (see BitMath tutorial and the full details of the ports used in Section 12 of the Atmega320 Datasheet. Arduino also uses timer0 for timing so the delay() method won't work - the following uses the delayms() method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <util/delay.h>

void setup()
{
  // Toggle OC0A on compare match
  TCCR0A = (0<<COM0A1) | (1<<COM0A0)
  // Mode 2 - CTC mode
    | (0<<WGM02) |  (1<<WGM01) | (0<<WGM00); 
  // OC0A port is D6 on atmega328 - set to output
  DDRD |= (1<<6); 
  // prescale (0=Off,1=clk,2=8,3=64,4=256,5=1024) 
  TCCR0B = (1<<CS02) | (0<<CS01) | (0<<CS00);
}

void loop()
{
  for (int i=255; i>0; i--) {
    OCR0A = i;
    _delay_ms(50);
  }
}

The piezo transducer positive wire was placed into pin D6 and common to ground.

<video?>

Making Music.

The next stage was to provide a layer ontop of the frequency generation to produce frequencies corresponding to musical notes.

The trick is to find values of MAX counter and prescale that create a frequency as close as possible to a note - the resolution of the timer means this is not always straightforward. The following Spreadsheet lists the notes (C to B) that make up the Midi pitch scale (based on formula on WikiPedia) and shows the values of MAX counter value for different prescales together with how close it is to the original frequency (accuracy % column). The cells marked in yellow show the values that I used. Thanks to the fact that the frequency doubles every octave and the prescale values quadruple - the MAX values are repeated every 2 octaves - so we only have to store a third of the values in the program.

The following program configures 12 input pins to use as a 1 octave keyboard, with 3 push button switches used to select which octave to play (pressing all three will make the system go into demo mode).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#include <util/delay.h>

// the digital pins used in the keyboard
int keyboard[] = {1,2,3,4,5,7,8,9,10,11,12,13};
// max counts for notes
int oc[] = {
  239, 225, 213, 201, 190, 179, 169, 159, 150, 142, 134, 127,
  119, 113, 106, 100, 95, 89, 84, 80, 75, 71, 67, 63};
// prescale for octaves
int pre[] = {5, 5, 4, 4, 3, 3};

void setup()
{
  // Toggle OC0A on compare match
  TCCR0A = (0<<COM0A1) | (1<<COM0A0)
    // Mode 2 - CTC mode
    | (0<<WGM02) |  (1<<WGM01) | (0<<WGM00); 
  // OC0A port is D6 on atmega328 - set to output
  DDRD |= (1<<6); 
  // prescale (0=Off,1=clk,2=8,3=64,4=256,5=1024) 
  TCCR0B = (1<<CS02) | (0<<CS01) | (0<<CS00);

  // set the keyboard pins to inputs and set pull up resistors
  for (int i=0; i<12; i++) {
    pinMode(keyboard[i], INPUT);
    digitalWrite(keyboard[i], HIGH);
  }
  // set the 'analog' pins as digital inputs
  for (int i=14; i<17; i++) {
    pinMode(i, INPUT);
    digitalWrite(i, HIGH);
  }
}

// play the note (-1 turns off)
void playMidiNote(int octave, int note)
{
  if (note == -1) {
    TCCR0B = 0;
  } else {
    TCCR0B = pre[octave];
    OCR0A = oc[(octave%2)*12 + note];
  }
}

// play the scales
void demo()
{
  for (int i=0; i<6; i++) {
    for (int j=0; j<12; j++) {
      playMidiNote(i, j);
      _delay_ms(200);
    }
    playMidiNote(0, -1);
    _delay_ms(2000);
  }
}

void loop()
{
  // read the push buttons treating as binary input
  int octave = ((digitalRead(14) == LOW) ? 4 : 0)
    + ((digitalRead(15) == LOW) ? 2 : 0)
      + ((digitalRead(16) == LOW) ? 1 : 0);

  if (octave == 7) { // play demo if all pressed
    demo();
  }
  // see which keyboard 'button' is selected
  int i;
  for (i=11; i>-1; i--) {
    if (digitalRead(keyboard[i]) == LOW) {
      break;
    }
  }
  // play note (if none selected -1 will stop sound)
  playMidiNote(octave, i);
  _delay_ms(25); // debounce switch
}

The 'stylus' is connected to ground via a small resistor (to limit current incase of accidental connection to +V). A flickr annotated photo of the circuit is below:

Piezo Keyboard Prototype

Possible improvements:

  • PCB keyboard for input.
  • A square wave generates a fairly boring tone, its possible to use an interrupt handler to intercept signal generation and create a more colourful signal. The higher resolution of the 16 bit timer1 may also help here.
  • Midi file parsing and output?
  • It looks like your browser doesn't support javascript so comments won't work

    Tags/Categories: projects, arduino, piezo