Translate

miércoles, 27 de junio de 2012

Aplicación: Audio con PWM

Una aplicación inmediata (y más interesante que los LED de colores del programa anterior) de la modulación PWM es intentar reproducir un archivo de audio digital tipo WAV usando el módulo PWM del PIC como un DAC (Digital-Analog Converter) incorporado.

Obviamente el principal problema es dónde guarda el PIC el audio a reproducir.  En la memoria de un PIC pueden caber unos 1000/2000 bytes, lo que a un ritmo de típico de 8 o 10 KHz solo da para una fracción de segundo. Lo que vamos a hacer en esta aplicación es recibir los datos a través del puerto serie. Si ajustamos la velocidad del puerto serie a 115200, teniendo en cuenta que para cada byte necesitamos mandar 10 bits (datos + start + stop) esto se traduce a un ritmo de unos 11500 bytes/segundo, muy cercano a 11025 que es una típica frecuencia de muestreo de archivos de audio. Según lleguen los datos por el puerto serie los usaremos para modificar el ciclo de trabajo del módulo PWM que previamente habremos configurado.

A la izquierda tenemos una foto con el hardware usado. A la derecha, un video donde pueden oírse el resultado,  junto con una captura de la señal PWM en el osciloscopio:

                         

Código asociado a esta entrada:  PWM_audio_serial.c
Proyecto C18 completo:  proyecto.rar


------------------------------------------------------------------------------------------------------------------------------

Hardware:

Obviamente la salida del pin PWM del micro no puede conectarse directamente a un buzzer o altavoz, por lo que será necesario usar algún tipo de driver. Para esta aplicación, un simple transistor NPN bastará.  El esquema de salida (asumiendo que usamos RC2 como salida PWM) es el siguiente:



Como "altavoz" he usado uno de esos zumbadores que vienen en los teléfonos de juguete. Dado que un buzzer no deja de ser una inductancia sería conveniente usar un diodo para reducir/suprimir el kickback de la bobina cuando el PWM caiga a cero. Como las intensidades que manejamos son pequeñas puede que no sea imprescindible y de hecho yo no lo he usado en mi montaje. La resistencia usada puede estar en el rango 1-5 K

En la foto siguiente se muestra el "montaje de audio" acoplado a la placa  EasyPic6. Negro es la masa común, rojo son 5V al colector del transistor y el cable naranja trae la señal PWM desde RC2 a la base del 2222A a través de una resistencia de 2K2.



Como vamos a usar el puerto serie, será preciso un convertidor de niveles (MAX232 o similar) entre el puerto serie del PIC y el del PC. En mi caso he usado el convertidor integrado en la placa EasyPIC.
   

Software:

El primer aspecto es decidir la frecuencia PWM a usar. Una cuestión importante es conseguir que dicha frecuencia sea lo más alta posible, para distanciarla de las frecuencias de la señal de audio a reproducir (en un rango de hasta unos 5KHz).  Sabemos que, dada la frecuencia del cristal, Fosc,  la frecuencia PWM viene dada por:
        
                        F_pwm =    F_osc / (4*PRE*(PR2+1)) 

Para un oscilador de 20 MHz sabemos que la máxima frecuencia que podemos usar manteniendo 1024 (10 bits) posibles niveles para el ciclo de trabajo es de Fosc/1024 = 19.6 KHz.

Sería interesante poder aumentar más la frecuencia. Si nos limitamos a 8 bits de resolución (muy adecuado porque trabajaremos con ficheros WAV con 8 bits/muestra = 256 niveles) podríamos multiplicar por 4 dicha frecuencia hasta llegar a 20000/256 = 78 KHz. Esta elección corresponde a un divisor previo PRE=1 y PR2=63.

Además de esta forma no tendremos que reescalar los datos de audio (entre 0 y 255). Basta asignar al ciclo de trabajo del PWM el valor recibido a través del puerto serie.

En cuanto a la recepción de datos por el puerto serie, la idea es implementarlo a través de la interrupción de recepción de forma que cada vez que se reciba un carácter se use su valor para fijar el "duty" del PWM.  De esta forma no hay que guardar los valores recibidos. La diferencia entre la frecuencia de recepción de datos (11500 bytes/sec) y la frecuencia ideal de reproducción (11025 muestras/sec) es lo suficientemente pequeña para que podamos ignorarla.

Vamos con el programa que resulta ser muy sencillo. Como siempre arrancamos con los #includes y #pragmas de siempre. Vamos a usar las funciones de C18 para inicializar el puerto serie y el módulo PWM por lo que debemos incluir los ficheros adecuados:

 #include <p18F4520.h>
#include <delays.h>
#include <timers.h>
#include <pwm.h>
#include <usart.h>
 
#pragma config OSC=HS
#pragma config PWRT = OFF, BOREN = OFF
#pragma config WDT = OFF, WDTPS = 128
#pragma config PBADEN = OFF, LVP = OFF

#include "../int_defs_C18.h"    // Defs about enabling interrupts, flags, etc.
#include "../tipos.h"


uint8 get_usart_speed(uint32 baud, uint32 Fosc, uint8* spbrg)
// baud rate 1200,2400,4800,9600,19200, ..., 115200
// Fosc = Frequency osc in KHz -> 20MHz = 20000 
// Returns 0 (USART_BRGH_LOW), 1 (USART_BRGH_HIGH), or 2 (cannot get speed requested)
// It places in spbrg the value to be used as argument spbrg in OpenUSART
 {
  uint16 FF;
  uint8 brgh;
  uint8 shift;

  Fosc=Fosc*1000; 
  baud>>=1; Fosc=Fosc/baud+1; Fosc>>=1;  // computes round(Fosc/baud)

  FF=(uint16)Fosc;
 
  if ((FF>16320)||(FF<16)) return 2;   // Baud rate too low or too high
 
  brgh = (FF>4096)? 0:1;  // Decides between BRGH=0 o BRGH=1
  shift= (brgh==1)? 3:5; 
  FF>>=shift; FF--; FF>>=1; // Computes round(FF/64 -1) or round(FF/16-1)

  *spbrg = (uint8)FF;
  return brgh;
 }

También hemos incluido la rutina get_usart_speed que usaremos para fijar la velocidad a 115200 bauds.

En cuanto a la rutina de interrupción del puerto serie es muy simple.  El inicio es simplemente un contador de bytes recibidos (cada 256 bytes incrementa PORTB). Esta parte es prescindible, ya que solo trata de dar un feedback visual de si se están recibiendo bytes a través del puerto serie.

La parte central de la ISR es simplemente coger el carácter recibido (en RCREG) y usarlo para establecer el ciclo de trabajo del PWM. Para ello colocamos sus dos bits menos significativos en CCP1CON.DC1B0 y CCP1CON.DC1B1 y los 8 más significativos (en este caso 6) en el registro CCPR1L. Ya hemos remarcado que con la configuración usada del PWM tenemos sólo 256 niveles del ciclo de trabajo, lo cual es ideal en esta aplicación porque los valores que llegan son justamente bytes:

uint8 cont=0;
// High priority interruption
#pragma interrupt high_ISR
void high_ISR (void)
{
 uint8 duty;

 if (RX_flag)
  {
   cont++; if (cont==0) PORTB++;

   duty = RCREG;
   CCP1CONbits.DC1B0=(duty& 0x01); duty>>=1;
   CCP1CONbits.DC1B1=(duty& 0x01); duty>>=1;
   CCPR1L=(duty);

   RX_flag=0;
  }
}

// Code @ 0x0008 -> Jump to ISR for high priority ints
#pragma code high_vector = 0x0008
  void high_interrupt (void){_asm goto high_ISR _endasm}
#pragma code


Como siempre que hemos pensado bien las cosas y hemos decidido que una interrupción haga el trabajo, en la rutina principal queda poco que hacer:

  • Inicializar el módulo PWM (PR2=63, PRE=1  => 78 KHz, 256 niveles de resolución).
  • Inicializamos el puerto serie a 115200 bauds.
  • Habilitamos la interrupción de recepción del puerto serie (junto con las interrupciones globales y periféricas).
  • Esperamos (while) sin hacer nada hasta que lleguen cosas por el puerto serie y la interrupción de recepción haga su trabajo.
void main()
{
 uint8 brgh,brgh_config,sp; 

 TRISB=0; PORTB=0;

 // Configure Serial POrt @ 115K
 brgh=get_usart_speed(115200,20000,&sp);
 brgh_config=(brgh)? USART_BRGH_HIGH:USART_BRGH_LOW;
 OpenUSART(USART_ASYNCH_MODE & USART_EIGHT_BIT & brgh_config, sp);

 // Configure PWM1 (RC2) @ 78 KHz with 256 levels od duty cicle
 OpenPWM1(63);   
 OpenTimer2(TIMER_INT_OFF & T2_PS_1_1); // Starts TMR2 with 1:1 prescaler

// Enable RX int, along with global and peripherical ints.
 enable_RX_int; enable_perif_ints; enable_global_ints;

 while(1);
}

Para probar el programa basta mandar (usando vuestro programa de terminal favorito) cualquier fichero WAV (configurado a 11025 Hz, 1 canal, 8 bits/sample) al puerto serie configurado a 115200 bauds.

Los resultados se muestran en el siguiente vídeo que ya vimos al principio  http://youtu.be/sRahHYadGtg

En el video vemos una captura de la pantalla del osciloscopio. El canal 1 (arriba) muestra la señal PWM a la salida del PIC. El canal 2 (abajo) muestra el voltaje en el "altavoz". El periodo de la señal es constante (unos 13 usec) y corresponde a la frecuencia PWM (78 KHz). El ciclo de trabajo cambia con los contenidos de la señal, aunque su correlación con el audio no se aprecia bien por la lenta actualización de la pantalla del osciloscopio. En el audio podemos escuchar el resultado, grabado a través de un micrófono colocado junto al altavoz.  

El resultado es bastante cutre desde el punto de vista de calidad de audio, pero ilustra bien las posibilidades de la modulación PWM como un conversor Digital/Analógico.

Se puede usar cualquier WAV a 11025, 1 canal y 8 bits (1 byte) por muestra. Al principio puede sonar un "click". Es simplemente el resultado de mandar al "altavoz" la cabecera del fichero WAV. Afortunadamente dicha cabecera es muy corta y enseguida llegan los datos de audio. Si el fichero está configurado como MONO y 8 bits/muestra entonces cada byte corresponde a una muestra y el audio se escuchará "correctamente".

Si no disponéis de un fichero WAV de las características descritas, aquí podéis descargaros un ejemplo para probar vuestro montaje: test_11025.wav.

Las posibilidades de mejora son muchas:
  • Parsear los datos del fichero WAV para saltarnos la cabecera y mandar sólo el audio.
  • Usar un verdadero DAC externo en lugar del módulo PWM como conversor digital/analógico.
  • Usar una tarjeta de memoria como fuente de datos.
Exploraremos algunas de ellas en sucesivas aplicaciones.

29 comentarios:

  1. que programa uso para mandar los datos del wav a el pic

    ResponderEliminar
    Respuestas
    1. Cualquier programa que te permita abrir una conexión al puerto serie y enviar un archivo valdría. Yo he usado hyperterm (tranfer/send text file).

      Antonio

      Eliminar
  2. El video no puedo verlo en yotube indica que es privado, como puedo visualizarlo

    ResponderEliminar
    Respuestas
    1. Por error no lo había compartido. Ya lo he cambiado y deberías poder verlo.
      Dímelo si todavía tienes problemas.

      Eliminar
  3. Hola, no hay manera de guardar los datos de a una memoria eeprom, y reproducir desde alli?

    ResponderEliminar
    Respuestas
    1. En esta entrada me he centrado en describir el uso de PWM como un "conversor DAC". Por eso he usado el "truco" de mandar la info por el puerto serie, para minimizar la parte del acceso a los datos.

      Por supuesto que podrías usar una memoria eeprom para guardar los datos y luego leerlos desde allí. Los problemas que podrías tener son:

      a) Limitación por el tamaño de la memoria. Para reproducir audio necesitarías del orden de 8000 datos (bytes) por segundo. Eso puede limitar el tamaño de tus mensajes.

      b) Necesitarías ser capaz de leer 8000 bytes por segundo de tu memoria eeprom.

      Podrías considerar la alternativa de usar una tarjeta SD.

      Antonio.

      Eliminar
  4. Hola Antonio, te hago una consulta, ¿es complicado adaptar para utilizar una memorio SD para almacenar el audio? Había leído también que se puede utilizar ICs de la serie ICDxxxx que almacenan audio por x segs. Lo que necesito es guardar audios de 1 segundo mas o menos pero son 45 a 60 audios y tener un teclado que a medida que presiono una tecla se reproduce tal sonido. Me podés guiar un poco así se por donde investigar, estoy desorientado.
    Saludos y gracias.

    ResponderEliminar
    Respuestas
    1. Con una SD se podría hacer (mira mis entradas sobre el tema SD), pero a lo mejor es matar moscas a cañonazos porque suponiendo 8000 muestras/sec tu sólo precisas unos 8000 samples/sec x 1 sec/audio x 60 audios = 1/2 Mbyte.

      No conozco los integrados que mencionas, pero cualquier memoria con 1Mbyte o así también te valdría.

      Con uno u otro enfoque lo único "complicado" sería pensarte algún sistema de buffering. Me explico: típicamente, del dispositivo externo tendrás que leer en bloques del orden de unos pocos cientos de bytes (muestras). Por ejemplo en una SD es típico leer bloques de 512 byte, lo que puede tardar del orden de unos 3-5 milisegundos. Sin embargo para que el audio suene bien tienes que estar mandando muestras de forma regular a una velocidad de 1/8000 = 1/8 msec. Si no quieres que el audio se entrecorte tendrás que mantener dos procesos, uno que vaya leyendo el bloque siguiente y otro que vaya mandando las muestras del actual al DAC.

      Si de tu dispositivo puedes leer bytes individuales y puedes hacerlo en menos de 1/8 msec lo tienes más fácil:

      Inicializar pos mem = inicio segmento audio.
      Bucle: Leer byte, mandar a DAC.

      En cualquier caso si usas una SD no tienes porque complicarte la vida con un sistema de ficheros. Como solo tu micro va a usarla puedes poner p.e el audio 1 en los sectores 1 a 15 (8192 muestras = 1 sec), el 2º audio del 16 al 31, etc.

      Otra cosa a considerar sería otro sistema DAC para no tener el audio "atroz" del PWM.

      Espero haber servido de algo,

      Antonio.

      Eliminar
  5. YO LO HICE DESDE MICROSD...NO PUEDE PASAR DE 2GIGAS http://elm-chan.org/works/sd8p/report.html

    ResponderEliminar
  6. Antonio he seguido tu blog y te agradezco por compartir con nosotros tus conocimientos. En este momento me encuentro trabajando en reproducir Audio con el PIC 18F4550 programando en lenguaje C y sobre el compilador PIC CCS.

    Mi problema es que tu explicas que el dato recibido por el puerto serial lo usas para modular el ancho de pulso, por tanto para el registro del PWM que es de 10 bit fracciones los 2 bit menos significativos del dato recibido en CCP1CON.DC1B0 y CCP1CON.DC1B1 y los 6 bit faltantes en CCPR1L, pero no he encontrado la forma de hacerlo sobre este compilador.

    Tu podrías darme una explicación mas detallada de como hacerlo para el compilador PIC CCS. Te lo agradezco inmensamente.

    ResponderEliminar
    Respuestas
    1. En esta entrada fijo el duty del PWM accediendo directamente a los registros CCP1CON y CCPR1L. El problema es que el compilador CCS tiende a desanimar sobre el acceso directo a registros. De todas formas, siempre es posible hacerlo: si te interesa busca la entrada titulada: "How do I directly read/write to internal registers?" en el manual del CCS.

      De todas formas puede que en este caso no sea necesario molestarse con los registros. Buscando en el manual del CCS he visto la función set_pwm1_duty, que a bote pronto, se encargaría de hacer precisamente lo que quieres.

      Espero haber ayudado, Antonio.

      Eliminar
    2. Hola Antonio, te agradezco por tu pronta respuesta...

      En CCS existe efectivamente la funcion set_pwm1_duty que configura el ciclo util del PWM. El problema es que si el dato recibido es de 8 bit, y si el contador fijado es de 63, cuando el byte del WAV recibido sea mayor a 63 como por ejemplo FE, no se podría ya que sobrepasa el valor máximo y habría un error. En esta parte de tu programa es donde no tengo claridad. Ayudame a entender ese fragmento de codigo. Muchas Gracias...

      Eliminar
    3. Según entiendo yo (si quieres, mirate la entrada anterior sobre PWM donde lo explico) el rango máximo posible del duty es de 4x(PR2+1). Por lo tanto con un contador PR2 puesto a 63 tendrías un máximo duty de 256, correspondiente a 8 bits.

      Espero haberte aclarado tu duda,

      Antonio

      Eliminar
  7. Antonio tu podrías subir todo el proyecto en C18, para realizar las simulaciones en Proteus. Por favor..

    ResponderEliminar
    Respuestas
    1. Añadido proyecto completo (al principio de la página, junto al código original).

      Antonio

      Eliminar
    2. Muchas gracias Antonio.

      Eliminar
  8. Antonio, he estado estudiando muy detalladamente el proyecto expuesto aquí y he seguido tus recomendaciones. En este momento estoy reproduciendo WAV muestreado a 11025Hz de 8bit Monostereo, y la información la tengo almacenada en una tarjeta SD tal como lo recomendaste, pero por algún motivo el audio reproducido se siente como con cortes, por tanto determine que no alcanzaba a leer la información de la SD y reproducirla inmediatamente. Seguí leyendo y encontré que se debe almacenar la información en 2 Buffers, por tanto mientras en uno lo estoy almacenando en el otro lo estoy reproduciendo. El audio mejoro notablemente pero aun así se sienten algunos cortes. Lo mire por el lado del filtro, pero no es el motivo. Podrías darme una asesoría, o si tal vez intentaras retomar este gran proyecto junto a la memoria SD. Muchas gracias.

    ResponderEliminar
    Respuestas
    1. ¿Estás usando interrupciones (de un timer) para sacar los datos de audio por el DAC?
      De no ser así tendrías un problema porque leer un sector de la SD tardará unos 3-4 milisegundos y en ese tiempo debes dar salida a unas 30-40 muestras de audio.

      Si estás usando interrupciones para mandar el audio al DAC el problema será más sutil y difícil de ver. Puede tener algo que ver con el manejo que haces de los buffers. Se trata de evitar que nunca se te vacíe el buffer de entrada y te encuentres sin muestras que mandar.

      Respecto a lo de retomar el tema si que tenía en mi lista documentar un proyecto de una grabadora de voz para grabar/reproducir audio usando una tarjeta SD. Espero encontrar un poco de tiempo para hacerlo.

      Espero haberte servido de algo, Antonio

      Eliminar
  9. Disculpa queria preguntar si con el mismo metodo se puede utilizar pero en ves de usar el puerto serial para recivir datos, hacer que este mande datos a una memoria sd para poder grabar un archivo, este es por medio de pcm

    ResponderEliminar
    Respuestas
    1. No entiendo muy bien lo que me dices, pero creo entender que quieres que el PIC reciba datos a través del puerto serie y los grabe en una memoria SD. Eso sería posible combinando el código de este apartado (para la recepción de datos a través del puerto serie) con el código de la entrada donde hablamos sobre cómo escribir en una memoria SD:
      http://picfernalia.blogspot.com.es/2013/04/tarjetas-sd-lecturaescritura-de-datos.html

      De todas formas ten en cuenta que los métodos que contamos en esa entrada de memorias SD son para escribir bloques de datos directamente sobre la tarjeta, sin un sistema de ficheros intermedio, por lo que los datos escritos no podrían ser leídos en un PC (por ejemplo).

      Un saludo, Antonio

      Eliminar
  10. yo vi un proyecto similar en youtube solo que la calidad no es muy buena sin embargo yo creo que todo proyecto con microcontroladores pic es algo talentoso crear un reproductor pcm wav usando el pwm a 78khz http://www.youtube.com/watch?v=dhzKCA4Sejw

    ResponderEliminar
  11. Hola, quería saber si sabes como le podría hacer para igual generar audio pero con el PIC18F46k20. Gracias

    ResponderEliminar
  12. Por lo que he podido ver con un rápido vistazo al datasheet del 18F46K20 el funcionamiento y los registros usados para el módulo del PWM son los mismos que los descritos aquí. El enfoque sería completamente similar en ambos casos (y puede que el código fuese válido en gran parte).

    Antonio

    ResponderEliminar
  13. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  14. hola amigo realize este proyecto tiempos atras, pero tengo una pregunta para realizar un proyecto nuevo , ya no quiero usar audio digital almacenado en una SD, sino quiero tener a la salida de un pic audio generado a traves de un microfono conectado a la entrada del pic , la meta es luego usar xbee y hacer una tx full duplex. La pregunta es si haz intentado hacer algo parecido ???

    ResponderEliminar
  15. Hola Antonio,

    En primer lugar me gustaría agradecerte lo que haces, me parecen muy interesantes todos los proyectos que has subido, y lo único que lamento es no haber visto tu web antes.

    En concreto estoy muy interesado en reproducir audio desde un microcontrolador, y este proyecto es exactamente lo que necesitaba. La pega es que no consigo que suene audio por mi altavoz, solo escucho un zumbido muy feo que nada tiene que ver con música. Evidentemente algo debo estar haciendo mal. Te explico lo que diferencia mi proyecto del tuyo:

    - En primer lugar no estoy usando un pic, sino el micro STM32F103CB, de tipo ARM 32-bit Cortex M3, con el que no tengo un DAC pero si varias salidas PWM.

    - El transistor que uso no es exactamente el mismo, pero creo que es muy parecido. Es el BC547C, tambien NPN.

    - El altavoz usado es comprado en una tienda de electrónica: FL 0.5W 8ohm YD50

    - Otra diferencia es la tensión usada, 3,3 V que da la conexión microUSB a la que conecto la placa de pruebas de mi microcontrolador

    El caso es que es por defecto la salida PWM del micro es de 500Hz, pero he conseguido modificarla para conseguir hasta 100kHz, con una resolución de 256 (8 bits), exactamente igual que en este proyecto.

    Mirando en el osciloscopio, parece que la señal PWM a 78kHz (Igual que en este proyecto), se genera bien. Esto lo compruebo en el osciloscopio, voy pasando caracteres uno a uno por hyperterminal, y el duty de la señal PWM parece corresponderse con el valor ASCII del carácter pasado. Con lo cual he dado por supuesto que hasta ahí va bien.

    En resumen, viendolo en el osciloscopio, midiendo ambas señales, PWM y la señal de voltaje en el altavoz son muy parecidas a las que muestras en el video. Pero por alguna razón, el sonido en el altavoz no es precisamente música

    Muchas gracias por todo antonio. Si se te ocurriera alguna idea de porqué me está pasando esto te estaría muy agradecido

    Saludos

    ResponderEliminar
  16. Chico y si ya pase el audio a la memoria EEPROM ahora como lo reproduzco puedes ayudarme?

    ResponderEliminar
  17. Buenas estoy intentando implementar una loop station (grabar pequeñas porciones de cancion 1,2 compases, y reproducirlos en bucle) introduciendo la señal de audio en un pin analogico del pic (16f883) para posteriormente reproducirlos mediante el pin CCP1 en modo PWM. Estoy trabajando con CCS y no soy capaz de realizar la onversion de datos del adc a duty cicle, no se si esty generando la señal PWM correctamente (para la comanda "set_pwm1_duty(PWMduty);" los valores PWMduty van de 0 a 100 o de 0 a 1024? podrias ayudarme. Muchas gracias

    ResponderEliminar
  18. Buenos dias, tengo una duda! espero por favor que me pueda responder, como es la decodificacion de un archivo wav a binario? es decir como es la conversion que hace el puerto serie para convertir cada muestra en un byte?? espero su respuesta gracias.

    ResponderEliminar