Tonos y melodías

Vamos a explicar desde cero cómo reproducir melodías usando tonos generados por un timer en un AVR128DA28. Los tonos serán reproducidos por un buzzer o altavoz piezoeléctrico y en específico usé este de TDK PS1240P02BT. Cómo ejemlo de estudio usaremos una melodía muy famosa que la usa Arduino.

Note

Te aconsejo que uses un analizador lógico para no ir a ciegas.

Vamos a ir descomponiendo la melodía en notas y luego en tonos y así hasta llegar al código fuente que los genera. Es un ejemplo maravilloso donde se aplica ingeniería inversa, matemática y programación.

¿Qué es una melodía? Es una sucesión organizada de sonidos de diferentes alturas y duraciones, percibida como una sola entidad musical que desarrolla una idea. Los sonidos son representados por tonos.

Comencemos, para eso conectamos un analizador lógico (logic analyzer) a la salida del microcontrolador que reproduce dicha melodía para obtener una muestra de pulsos que se parecen a la siguiente imagen:

Estas son las notas y la duración de cada de la melodía. La primera línea son las notas, y la segunda los tiempos.

NOTE_C4, NOTE_G3, NOTE_G3, NOTE_A3, NOTE_G3, 0, NOTE_B3, NOTE_C4
4,       8,       8,       4,       4,       4, 4,       4

Si buscamos en la librería de tonos musicales que suele estar en un fichero llamado pitches.h obtenemos solo estas cuatro notas que conforman la melodía:

#define NOTE_G3  196
#define NOTE_A3  220
#define NOTE_B3  247
#define NOTE_C4  262

Tenemos a la mano toda la información para empezar a realizar los cálculos.

Note

Los valores decimales que se obtienen de los cálculos y los que se aprecian en las imágenes obtenidas por el analizador lógico no son exactos, hay un margen de error que se debe a múltiples factores.

Por ejemplo, la nota G3 tiene el valor 196, que corresponde a la frecuencia de 196 Hz.

Si observamos la imagen anterior, notamos que hay una serie de pulsos que los llamaremos toggle. Un toggle es un valor alto o bajo, la cantidad de estos son 25 como se muestra en la imagen, y este valor se puede calcular usando la nota y el tiempo.

Volvamos a analizar la nota G3 que está acompañada de un tiempo con el valor 8 que equivale a una corchea (jerga de la música), esto quiere decir, una duración de 1/8s es equivalente a 125ms, en la imagen anterior está como 124,60.

$$ t_{ms}=\frac{1000}{8}=125_{ms} $$
$$ toggles=\frac{2 \cdot freq \cdot t_{ms}}{1000}=\frac{2 \cdot 196Hz \cdot 125_{ms}}{1000}=49 $$

Como resultado tenemos 49 toggles que es equivalente a 24.5 períodos, si redondeamos son 25 períodos. Todo empieza a encajar según lo que nos dice la imagen anterior.

Entre nota y nota se define un tiempo llamado pausa a partir de la siguiente formula:

$$ t_{pause}=t_{ms} \cdot 0,30=125ms \cdot 0,30=37,5ms $$

El tiempo de la nota G3 es 8 y aplicando la formula, obtenemos 37,5ms como se muestra en la siguiente imagen:

Se han realizado los cálculos para la nota G3, estos pasos se debe repetir por cada nota hasta terminar la melodía. Hasta ahora se han identificado tres variables que son necesarias para poder programar; tms, tpause y toggles.

Para crear los pulsos de la forma deseada para cada tono vamos a usar uno de los timer/counter de varios que tiene el microcontrolador junto a las interrupciones, en específico el timer/counter A (TCA) que tiene una resolución de 16-bit, quiere decir que el contador va desde el 0 hasta 65535 (216 − 1). Para entender su funcionamiento, vamos a estudiar el modo NORMAL que es el que viene por defecto y es el más simple de comprender. Es importante saber que su funcionamiento y configuración es muy abstracto, por lo que voy a hacer el mejor esfuerzo al explicarlo.

Important

El reloj interno debe estar bien configurado a 24MHz (24.000.000 Hz). Ver la función clk_init().

Hay una serie de términos básicos que hay que dominar para entender cómo funciona el timer/counter y poder configurar el Microcontrolador. Vamos a conocerlos antes de profundizar aún más en el funcionamiento:

  • Counter: Es un contador que incrementa o decrementa de forma automática en cada ciclo del reloj o evento. Es el registro interno del contador TCA0.SINGLE.CNT. Al ser un contador de 16-bit, quiere decir que el contador va desde el 0 hasta 65535 (216 − 1). Entonces el counter acumula tick.
  • Tick: Cada vez que el Counter se incrementa 1.
  • Prescaled: Es un divisor que se le pone a la velocidad del reloj y es usado para indicar cada cuanto hace tick. Son valores que van desde 1 hasta 64 y suelen ser estos: 1, 2, 4, 8, 16, 32, 64.
  • PERiod: Es el valor máximo del contador (TOP) antes de volver a cero 0. No es el período en segundos.
  • Overflow: Es el desbordamiento, y ocurre cuando el contador llega a su valor máximo (TOP) y vuelve a empezar. Cuando esto ocurre, se dispara la interrupción TCA0_OVF_vect.
  • Interrupción: Es un mecanismo que detiene temporalmente la ejecución normal de un programa para atender un evento, ejecutando una rutina especial llamada ISR (Interrupt Service Routine) y al terminar regresa al punto donde estaba el programa.

Vamos a ubicar la mayoría de los términos en dos gráficas y así poder visualizarlo mejor:

El timer/counter A es un acumulador de 16 bits que llega a 65535, ajustando el prescale a 16 para la frecuencia a 24MHz (24.000.000 Hz) nos permite definir un valor PER que indica hasta donde debe llegar el contador, entonces el timer cuenta de 0 hasta 3826 ticks y luego se reinicia overflow. En ese momento que se reinicia se produce una interrupción llamada TCA0_OVF_vect que se usa para togglear el pin en este caso.

$$ tick=\frac{f_{CPU}}{ 2 \cdot prescaler \cdot freq}=\frac{24000000}{ 2 \cdot 16 \cdot 196}=3826 $$

El tick ocurre cada cierto tiempo y está definido por:

$$ T_{tick}=\frac{prescaler}{f_{CPU}}=\frac{16}{24000000}=0.667_{µs} $$

Para terminar, cada incremento del contador (tick) ocurre cada 0.667µs y el overflow a los 2.55ms:

$$ T_{PER}=tick \cdot T_{tick}=3826 \cdot 0.667_{µs}=2.55_{ms} $$

Ahora deberás relacionar todo lo que te conté con el código fuente, será mucho más fácil sabiendo de donde proviene cada fórmula y que hace cada variable que configura el timer/counter y su relación con la interrupción.

 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
80
81
82
83
84
85
86
#include <avr/cpufunc.h>
#include <avr/interrupt.h>
#include <avr/io.h>
#include <stdlib.h>
#include <util/delay.h>

#define NOTE_NA  0
#define NOTE_G3  196
#define NOTE_A3  220
#define NOTE_B3  247
#define NOTE_C4  262

static volatile uint32_t toggles = 0;

int melody[] = {
    NOTE_C4, 4,
    NOTE_G3, 8,
    NOTE_G3, 8,
    NOTE_A3, 4,
    NOTE_G3, 4,
    NOTE_NA, 4,
    NOTE_B3, 4,
    NOTE_C4, 4
};

void clk_init(void) {
    _PROTECTED_WRITE(CLKCTRL.OSCHFCTRLA, CLKCTRL_FRQSEL_24M_gc);
    _PROTECTED_WRITE(CLKCTRL.MCLKCTRLA, CLKCTRL_CLKSEL_OSCHF_gc);
    _PROTECTED_WRITE(CLKCTRL.MCLKCTRLB, 0);
}

void noTone(void) {
    TCA0.SINGLE.CTRLA = 0;
    PORTF.OUTCLR = PIN0_bm;
    toggles = 0;
}

void tone(uint32_t freq, uint32_t dur) {
    if (freq == 0 || dur == 0) return;

    cli();

    PORTF.DIRSET = PIN0_bm;
    VPORTF.OUT &= ~PIN0_bm;

    TCA0.SINGLE.INTCTRL  = TCA_SINGLE_OVF_bm;
    TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_NORMAL_gc;
    TCA0.SINGLE.EVCTRL &= ~(TCA_SINGLE_CNTAEI_bm);
    TCA0.SINGLE.PER = F_CPU / (16 * 2 * freq) - 1;
    TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV16_gc | TCA_SINGLE_ENABLE_bm;

    toggles = (2 * freq * dur) / 1000;

    sei();
}

void pause(uint32_t ms) {
    while (ms--) _delay_ms(1);
}

int main(void) {
    clk_init();

    while (1) {
        int notes=sizeof(melody)/sizeof(melody[0])/2;

        for (int thisNote = 0; thisNote < notes * 2; thisNote = thisNote + 2) {
            int duration = 1000 / melody[thisNote + 1];
            tone(melody[thisNote], duration);

            int pauseBetweenNotes = duration * 1.30;
            pause(pauseBetweenNotes);
        }
        _delay_ms(2000);
    }
}

ISR(TCA0_OVF_vect) {
    if (toggles-- > 0) {
        PORTF.OUT ^= PIN0_bm;
    } else {
        noTone();
    }

    TCA0.SINGLE.INTFLAGS = TCA_SINGLE_OVF_bm;
}

El ejemplo 2.9 muestra el código de configuración del timer/counter A (TCA) en modo NORMAL de la guía de migración que use para la función tone(uint32_t freq, uint32_t dur).

Para terminar, este es el esquema del circuito: