Tuesday, 1 October 2013

Generating Tones with Timers

Previously I presented a basic way of using timers in order to generate delays. At this time, we'll learn how to use timers and piezo buzzer to generate tones (beeps). The code for that won't be very different from what we were doing previously. Let's start with defining the requirements of our implementation. Our "beeper" function should allow us to generate a beep of given frequency and duration. Simple as that. Just to quickly summarize, when using timer for delay purposes we configured it in CTC mode and configured the prescaler and OCRXA register value so the timer "overflowed" with a 1 kHz frequency (1 ms period), the delay duration was specified by the number of "overflows". Previously, as long as counting the 1 ms cycles was good enough to determine the duration, now the duration depends on both parameters, the actual duration specified as an input parameter as well as the frequency itself. 

Let's list steps that our "beep" generation algorithm will have to take:
  1. Program the timer in CTC mode.
  2. Program the OCXA pin as output.
  3. Program the timer's compare output mode to toggle the OCXA on each compare match.
  4. Calculate the prescaler and the OCRXA values to generate the frequency of 2*f.
  5. Calculate the amount of cycles needed to "realize" the requested duration.
  6. Enable interrupts.
  7. Decrement the cycles counter (calculated in step 5) inside of the ISR.
  8. Stop generation and disable interrupts once cycles counter reaches zero.

That looks a lot more complicated than previously. Don't worry it only appears that way. The trickery is hidden in steps 4 and 5, the rest is more or less the same. In point 4, let's think a for moment; why does the frequency should be twice as high as requested ? The reason behind it is really trivial. The output will be generated on pin OCXA by the hardware, which in CTC mode and COM0A1/COM0A0 configured to 0x01 - will be toggled each time a timers TCNTX register value will be equal to the one programmed in OCRXA, after that the TCNTX register will be reloaded with zero. What that means is a single timer cycle generates only half of the square wave output. So, we need the frequency twice as high for the pin to be toggled with the one requested. 

First thing that we will have to think about is a universal function which will calculate the values of both the prescaler and the OCRXA register to program the timer to the requested frequency. The formula for the frequency for the Timer in CTC mode (as stated in the datasheet) is:

f = fclk / [ 2 * N * (1 + OCRXY) ]


fclk - system clock (16 MHz for most of the Arduinos)
N - the prescaler value (1,8,64,256,1024 - for timers 0 and 1)
OCRXY - the value in the register OCR0A/OCR0B (register for timer 0 used as an example, by the way, through out this post and in most of my code I focus only on Output compare register and output compare A, but You can use register/output B good as well - it's a matter of preferences).

Using the formula above we will have to calculate the values of N and OCRXA in runtime. 

Let's have a look bellow on the code which does that for Timer0: 

Let's analyze this code. There are some improvements introduced inside to make it a little bit faster/smaller. In fact it is completely decoupled from hardware, since it's a routine performing calculations only, so we can successfully compile it on an x86 machine and do some debugging. First of all a table of "prescalers" is declared. At first glance it contains some strange values. It stores the number by which a 0x01 should be bitshifted in order to get a valid prescaler value. It has been designed this way to save memory. The prescaler values for timer0 are 1,8,64,256,1024. To store those values we would need an uint16_t data type, consuming 5 bytes more than now. Since the prescalers are a powers of two we can only store the number of bitshifts required to get the needed value. The Atmega is a very fast core, we have a lot more computing power than memory available, that's why in most of the applications we should try to minimize the memory usage over computing resources. The calculation of prescaler and the OCR value is done in the while loop, the formula has been transformed to the following one:

OCR = [ fclk / (2 * N * f)] - 1

In every loop roll we compute the OCR value using another available prescaler. The loop brakes as soon as the OCR value is bellow 256 - which means that we have found a usable set of prescaler and OCR values in order to program the timer to generate the requested frequency. The values are returned by reference. Let's test this function offline. Consider & compile the followig code:

gcc -o pocr_test pocr_test.c

Let's do some tests:

$ ./pocr_test 1000
Frequency: [1000 Hz], Prescaler: [  64], OCR: [124]

$ ./pocr_test 13000
Frequency: [13000 Hz], Prescaler: [  8], OCR: [ 75]

$ ./pocr_test 8000
Frequency: [8000 Hz], Prescaler: [   8], OCR: [124]

$ ./pocr_test 30
Frequency: [30 Hz], Prescaler: [1024], OCR: [255]

./pocr_test 31
Frequency: [31 Hz], Prescaler: [1024], OCR: [251]

$ ./pocr_test 20
Frequency: [20 Hz], Prescaler: [1024], OCR: [255]

Let's verify one of those manually:

- f = 16MHz / [ 2 * 64 * (1 + 124) ] = 1000
- f = 16MHz / [ 2 * 8 * (1 + 75) ] = 13157
- f = 16MHz / [ 2 * 8 * (1 + 124) ] = 8000
- f = 16MHz / [ 2 * 1024 * (1 + 255) ] = 30

Looks quite promising. Of course the function does not minimize the frequency error, it only tries to find first matching pair of values in order to satisfy to request. In the second example a values of (prescaler: 8, ocr: 76) would provide a better fit (12987 Hz, 13 Hz difference instead of 157), but for most of the applications (especially tone generation using buzzer) such error is really acceptable. For more precision we would have to gather the OCR results for all prescaler values and return the one which is less different from the requested frequency. To have a real control we would probably define those values ourselves and use some sort of lookup table. At the moment we do not have to worry about that.

Second thing that we need to do is to determine how many timer overflows we need to count in order to determine the duration. Let's think about it. For a given frequency 1 cycle lasts for :

T = 1/(2*f)

The frequency is multiplied by two since, the timer is running with frequency twice as high as needed. The duration will be specified in millisecods, so we need to determine how many cycles (x) do we need do realize it. This can be calculated using proportions:

duration * (1/1000) = x * (1/2*f)

so, x:

x = (2 * f * duration) / 1000


x = (f * duration) / 500

and that's the answer. Now we have all the necessary tools to finish our implementation. Let's start:

Let's connect the buzzer to the Arduino board digital pin 7 (PORTD6) (no resistor is needed), and test our code.

Connecting piezo buzzer to Timer0 OC0A output.

This functionality is covered by the libpca as well. As previously it's very easy to use. The timer_freq_prescale() function equivalent is implemented there as well in a very similar fashion (it's called _timer_freq_prescale()). Again, it's done in way to provide flexibility and be as much universal as it's possible. Let's cover the implementation in the beeper library module. Most important functions are:

- void beeper_init(e_timer a_timer);
- void beeper_beep(e_timer a_timer, uint32_t freq, uint32_t duration);

The interface is self explanatory. Let's write a simple program which will do more or less the same as our previous example:

$ git clone pca
$ mkdir beep_libpca
$ cd beep_libpca
$ touch beep_libpca.c

We cannot forget to enable the Timer interrupt implementation in the library configuration file in order for the code to work (this will be TDELAY_IMPLEMENT_T0_INT variable which must be defined as 1). The implementation presented as well as the one in the library can be extended further on. We could use the PWM capability to provide the volume control to the beep generator. For the moment though I do not plan to cover this topic. I hope that this post has been useful. In my next posts I'm going to cover the basic usage of the UART port. After that I'll proceed to some more interesting stuff (I hope :)).

No comments:

Post a Comment