| |

Reproductor de MP3 de bricolaje. Cómo armar y programar un gadget en casa

Contenido del artículo

  • Componentes
  • Diseño
  • Plantilla de programa futuro
  • Controlador de tarjeta sd
  • Algunas palabras sobre depuración
  • Fatfs
  • Códec de audio VS1011E
  • Implementación del jugador
  • Interfaz
  • Secuencia de seguimiento
  • Dispositivo completo
  • Lo que vale la pena agregar

Diseñe sus dispositivos electrónicos; tal vez no sea muy práctico, pero emocionante e informativo. En este artículo te contaré cómo creé mi propio reproductor de música. El resultado, por supuesto, no es el iPod nano (y ni siquiera el mini), pero luego veremos cómo trabajar con diferentes hardware en C: una tarjeta SD, un códec, una pantalla y un teclado.

INFORMACIÓN

En un  artículo anterior,  hablé sobre cómo ensamblar un teléfono móvil, y tomar prestados algunos trucos de este proyecto.

Recuerdo cómo en 2004 tenía un reproductor de MP3 y estaba completamente encantado. Es cierto que solo tenía 128 MB de memoria, que en ese momento ya se consideraba modesta. Además, el reproductor tenía una característica muy mala de distorsionar los archivos grabados en él. Como se explica en las instrucciones, esto no es un error, sino una «característica», es decir, protección contra copia.

Ahora, por supuesto, los reproductores de MP3 ya no disfrutan de su antigua popularidad y todos están escuchando música desde los teléfonos, pero como objetivo para su proyecto, esta es una buena opción, no trivial, pero bastante factible.

Entonces, desde mi proyecto, quería:

  • el dispositivo (obviamente) estaba reproduciendo MP3;
  • Tarjetas SD modernas compatibles
  • FAT se considera como sistema de archivos;
  • la calidad del sonido fue aceptable;
  • siempre que fue posible hubo un bajo consumo de energía.

Componentes

Para la base del dispositivo, tomé el económico códec MP3 VS1011E. De hecho, sería más inteligente elegir el VS1053 o VS1063 más avanzado o la versión actualizada de VS1011 – VS1003 (tiene una mayor velocidad de reloj), todos cuestan casi lo mismo.

Sin embargo, no profundicé en estas sutilezas y decidí por el primer microcircuito que apareció. Como controlador, tomé STM32F103C8T6 para poder hacer un modelo usando la placa Blue Pill terminada, y solo entonces ensamblar todo de manera seria. La pantalla elegí TFT, resolución – 128 por 160 (ST7735). Ya tengo bibliotecas escritas para él.

El código, como en el caso del teléfono, lo escribiremos en C usando las bibliotecas libopencm3 y FatFs.

El dispositivo funciona simplemente: lea los datos de un archivo en una unidad flash USB y alimente el códec, y el códec hará el resto.

Diseño

Antes de pasar al código, tiene sentido construir una maqueta de dispositivo (generalmente soy fanático de depurar programas en hardware real). Tomamos la placa Blue Pill y soldamos el módulo de pantalla con el portatarjetas. La soldadura nos permite no enfrentar el problema del ruido de las juntas, lo que puede causar muchos solmas en la etapa de depuración.

Ensamble el módulo de prueba para VS1011 en la placa de pruebas, utilizando el adaptador de QNF48 a DIP, cuyo circuito busqué en la hoja de datos. De hecho, no es necesario molestarse así: puede tomar un módulo listo para usar. Pero no lo tenía, pero no quería esperar.

Como resultado, recopilar todo esto en unas pocas horas y estaba listo para pasar al código.

Plantilla de programa futuro

Antes de escribir las funciones básicas, es útil inicializar la pantalla y el teclado. Ya hablé sobre la pantalla de arriba, y el teclado de cuatro por cuatro botones restantes del diseño del teléfono.

En la fuente a continuación: archivos de encabezado estándar, funciones de inicialización periférica, funciones de inicialización de pantalla y teclado, y al final, la salida de la línea Hello world.

sd.c

#include <libopencm3 / stm32 / rcc.h>

También en el archivo HAGA debe agregar el directorio con las bibliotecas de origen y las bibliotecas mismas. A continuación hay un fragmento  Makefile:

...

Controlador de tarjeta sd

Sin un controlador, trabajar con tarjetas SD no funcionará, así que comencemos con él. La lectura y escritura de discos SDHC se realiza en bloques de 512 bytes. Nuestro controlador debería ser capaz de: escribir un bloque en el disco, leer un bloque del disco e inicializar el disco.

WWW

Encontrar documentación sobre cómo trabajar con tarjetas SD mediante SPI no es un problema, también está disponible en ruso.

Sin embargo, hay varios puntos importantes y no muy obvios, nuestro conocimiento acelerará en gran medida la escritura y la depuración del controlador. En primer lugar, si hay otros dispositivos en el bus SPI junto con SD, primero debe inicializar SD, de lo contrario no se iniciará.

En segundo lugar, la inicialización debe tener una frecuencia de bus bastante baja (aproximadamente 500 kHz), de lo contrario, SD no responde. Luego puede torcer la frecuencia al máximo (tengo 36 MHz, es de aproximadamente 4 Mbit / s).

Tercer lugar, hay varios tipos de tarjetas SD, y cada tipo tiene su propia inicialización. Me guiaron las tarjetas SDHC más modernas y extendidas, y mi versión de la función de inicialización fue escrita solo para ellas.

En los  ejemplos  en el sitio web de Elma Chan, puede encontrar una función de inicialización universal. En realidad, intente escribir el controlador mínimo necesario, por lo que solo admite un tipo de tarjeta, además de escribir y leer en un sector. Sin embargo, durante la depuración, quedaron claros que la lectura y escritura multisectorial no eran necesarios.

Tenga en cuenta que antes de enviar comandos de inicialización, se deben transmitir 80 pulsos de reloj en el bus a un nivel alto en el contacto de la tarjeta CS. Esto es necesario para cambiar SD al modo SPI (el modo de tarjeta normal es SDIO). Después de eso, CS se baja y comienza la inicialización, que no me detendré.

sdcard.c

uint8_t sd_init () {

Las tarjetas SD tienen una tendencia desagradable de mantener el MISO alto durante unos pocos ciclos CLK más después de aplicar un nivel bajo a CS. Esto se trata pasando el byte 0xFF en el bus a un nivel alto en el CS. Sin embargo, en mi caso esto no es crítico.

A continuación,  read y  write del archivo  sdcard.c.

uint8_t sd_read_block (uint8_t * buf, uint32_t lba) {

Después de cavar un par de días con un analizador lógico, esto comenzó a inicializarse, leer y escribir.

Ahora debe agregar la biblioteca  sdcard.c y su archivo de encabezado al proyecto ya la función  main() : inicialice la tarjeta SD. Y aquí recordamos que SPI1 está configurado a baja velocidad para una oficialización exitosa (FCPU / 128 ~ 500 kHz), y es inconveniente trabajar con la pantalla a esta velocidad. Por lo tanto, agregamos una función  spi1_forsage(void)que, de hecho, reinicializa SPI1, pero con una frecuencia aumentada (FCPU / 2 36 MHz).

...

Ahora tenemos una tarjeta SD, pero también necesitamos un sistema de archivos para funcionar.

Algunas palabras sobre depuración

Anteriormente, solía usar la salida UART para la depuración, pero cuando el dispositivo tiene su propia pantalla y la salida estándar se dirige a él, entonces ni siquiera necesita conectar UART, solo usar la función  stprintf(). Fue con su ayuda que analicé los desafíos  discio.c.

A continuación se muestra un ejemplo de mensajes de  discio.h depuración (los comandos de depuración están comentados).

DRESULT disk_write (BYTE pdrv, const BYTE * buff, LBA_t sector, UINT count) {

Como resultado, cada vez que se llama a la función, los  disk_write() argumentos que se le pasan y el valor de retorno se muestran en la pantalla  sd_write_block(). Durante mucho tiempo no pude entender por qué falla la escritura en el disco a través de FatFs, aunque el analizador lógico dice que todo va como debería: tanto una llamada de función directa  sd_write_block()como una llamada posterior  sd_read_block() que registra la escritura.

Resultó que la función  sd_write_block() escrita con éxito, pero no devolvió 0, y FatFs lo consideró un error de escritura. Solucioné el error y los mensajes de depuración están comentados.

También es útil en la depuración el analizador lógico Saleae Logic (más específico, su clon chino) y  el programa del mismo nombre , que funciona muy bien en Linux y ayuda mucho al depurar protocolos.

Fatfs

Para leer un archivo de una tarjeta, primero debe escribirlo allí de alguna manera. Y es más conveniente hacer esto cuando hay un sistema de archivos en el mapa. Entonces lo conectamos a la computadora, formateamos y copiamos los archivos necesarios.

Escribir el controlador del sistema de archivos para el reproductor sigue siendo demasiado, incluso para mí, pero hay un controlador de FatFs escrito en C y fácilmente transportado a cualquier cosa.

WWW

Descargar el Código de PUEDE fuente de FATFS Y  Obtener  Una Descripción específica del Sitio web de Elma Chan.

Para agregar FatFs al proyecto, debe hacer algunas cosas. El primero es realizar cambios en el archivo  ffconf.h.

#define FF_CODE_PAGE 866 // 866 - Página de códigos cirílicos

Eso es suficiente sin soporte cirílico, estaremos tristes, pero elegí la codificación UTF-8, ya que el uso en el escritorio y simplifica enormemente las operaciones de archivo.

Ahora necesitas editar el archivo  diskio.c. Las funciones en él asocian FatFs con el controlador de la tarjeta SD, que discutimos previamente. Hacemos los cambios necesarios.

...

Esto es solo un trozo, ya que inicializamos manualmente el disco en otro lugar. Todo este desastre está relacionado con el hecho de que la tarjeta SD debe inicializarse primero en el autobús.

DRESULT disk_read (BYTE pdrv, BYTE * buff, LBA_t sector, UINT count) {

Aquí sería posible agregar una lectura multisectorial, esto aumentará la velocidad, pero para este proyecto no es tan importante. Lo mismo puede decirse de la grabación.

DRESULT disk_write (

Y la última función que necesita ser ajustada también es un trozo.

DRESULT disk_ioctl (

Ahora agregue los archivos de encabezado ( ff.h) al proyecto y el código fuente ( ff.cdiskio.c y  ffunicode.c) al Makefile. Hecho ahora tenemos soporte para los sistemas de archivos FAT12, 16 y 32.

WWW

En el blog «Notas del programador» hay un buen  artículo sobre cómo trabajar con la biblioteca FatFs .

Códec de audio VS1011E

El códec de audio es bastante fácil de usar. Su interfaz (SPI) tiene dos modos: modo de comando (bajo en CCS) y modo de datos (bajo en DCS). Es decir, desde afuera parece dos dispositivos SPI independientes en el bus.

Además, se utilizan dos pines DREQ y RST más. Con RST, todo está claro: un nivel bajo provocando un reinicio del chip. DREQ muestra la disponibilidad del chip para aceptar 32 bytes de datos en el bus.

Esta es una conexión de siete cables del chip, que le permite colocarlo en el mismo bus SPI con otros dispositivos. Sin embargo, al ensamblar y ajustar el diseño, resultó que es inconveniente mantener la pantalla, la tarjeta SD y el VS1011E en el mismo bus. Esto se debe principalmente al límite de velocidad del bus VS1011. La hoja de datos indica que la frecuencia máxima del bus es FCPU / 6, es decir, en mi caso 12 * 2/6 = 4 MHz. Para la pantalla y la tarjeta de memoria, esto es demasiado lento y, como resultado, el sonido se retrasará, lo cual es inaceptable.

Por supuesto, podría cambiar dinámicamente la velocidad del autobús, pero decidir cambiar el código al segundo SPI, ya que mi STM tiene dos de ellos.

INFORMACIÓN

En la conexión y el protocolo para el intercambio de datos con el VS1011E, hay un apéndice separado sobre VS1011e SPI AN, incluso hay ejemplos de funciones de intercambio de datos para diferentes opciones de conexión. Y al escribir el controlador VS1011, el VS1011E Jugar AN apnout nos controladores.

Entonces, para nuestro jugador juegue, debe enviar datos en lotes de 32 bytes al códec y tener en cuenta la disponibilidad del chip para recibir datos. Convenientemente, omitirá los encabezados MP3, por lo que se puede transferir todo el archivo, lo que simplifica la tarea.

Así comenzamos las funciones de lectura y escritura para controlar los registros.

#define VS_CCS_DOWN () gpio_clear (VS_PORT, VS_CCS)

Ahora necesitamos una función para enviar datos, los datos se transmiten en matrices de hasta 32 bytes. Todo aquí es similar a la función anterior, pero el chip cambia el modo de datos, y no es necesario enviar un comando especial, puede enviar datos de inmediato.

#define VS_DCS_DOWN () gpio_clear (VS_PORT, VS_DCS)

Ahora podemos inicializar el chip. Para ello, tiene que soltar brevemente RST, a continuación, establezca los bits  SM_SDINEW y  SM_RESET el registro  SCI_MODE. Finalmente, debe establecer el valor correcto de la frecuencia de cuarzo en el registro  SCI_CLOCKF, para lo cual se utiliza una macro conveniente  HZ_TO_SCI_CLOCKF(hz). Esto es importante para la velocidad de reproducción correcta.

// Esta macro para VS1011 se instalará automáticamente

Ahora puedes ir directamente a reproducir archivos.

Implementación del jugador

En el apnout anterior VS1011 AN Jugar hay un ejemplo de implementación del jugador: me orienté sobre ello.

Considera la función  play_file(char *name). Abrimos el archivo MP3 con funciones FatFs, leemos 512 bytes en el búfer desde allí y comenzamos a enviar datos desde el búfer al códec en grupos de 32 bytes cuando el chip está listo para recibirlos. Sin embargo, la expectativa de preparación ya está en la función  vs_write_sdi(), por lo que aquí no podemos pensar en ello.

Después de enviar varios de estos paquetes, puede sondear el teclado y la interfaz (para agregar una barra de progreso, por ejemplo). Cuando el búfer esté vacío, lea otros 512 bytes y repita nuevamente. Si el archivo termina antes de que el búfer esté lleno, está bien, le daremos 32 bytes, el mayor tiempo posible, y el último paquete estará más corto que 32 bytes. Para determinar cuentos casos, utilizamos la función macro  min(a,b).

#define FILE_BUFFER_SIZE 512

Esta es simplemente una función simple, si elimina de ella ryushechki y todo lo relacionado con la interfaz, pero de esta forma la función es inconveniente, por lo que implementamos la interfaz.

Interfaz

Sobre la salida a la pantalla de 128 por 160 en la placa ST7735, ya escribió en el artículo sobre el teléfono. Sin embargo, para este proyecto tuve que implementar soporte para UTF-8, aunque de forma simplificada. Los caracteres admitidos son latín y cirílico (sin la letra e). Esto simplificó el retrabajo con CP866: simplemente reorganice los caracteres en las tablas, corrija la búsqueda del personaje y agregue códigos de ignoración con los caracteres 0xD0 y 0xD1, prefijos de la página circular.

st7735_128x160.c

oid st7735_drawchar (unsigned char x, unsigned char y, char chr,

Por lo tanto, los codigos de hasta 0x7F se interpretan como ASCII y el resto como símbolos de la página cirílica. La solución, por supuesto, no es universal, y cuando nos encontramos con la letra é veremos artefactos, pero esto es más fácil para la compatibilidad con la configuración regional en el escritorio.

Para dibujar una barra de progreso, también seremos símbolos de texto para simplificar.

nulo st7735_progress_bar (uint8_t y, valor de uint8_t,

Además de esto, stprintf.c agregue una función de salida con formato a una línea dada en la biblioteca, es más fácil dibujar una interfaz.

int stprintf_at (uint8_t x, uint8_t y, uint16_t color,

La pantalla está dividida en tres partes. La primera parte, las 14 líneas de texto superiores, se utiliza para mostrar mensajes (nombre de la pista actual, errores, etc.). La segunda parte es la línea 15, donde se encuentra la barra de progreso, y la última línea 16 con información sobre la pista actual.

Los siguientes datos se muestran en la línea inferior: «Kbyte leído / Kbyte total, tiempo desde el comienzo de la pista, modo, número de pista, pistas totales». En código, se ve así:

// Variables globales

Después de renderizar la interfaz, el controlador del teclado continúa, el teclado se ensambla en el registro de desplazamiento 74HC165D y funciona de manera similar al teclado del teléfono del artículo anterior. El sondeo de registro se realiza mediante la emulación de software del protocolo SPI. No hay sutilezas aquí.

uint8_t read_key (void) {uint8_t data, cnt = 0; gpio_clear (HC165_PORT, HC165_CS); // Habilitar la sincronización gpio_clear (HC165_PORT, HC165_PL); // Escribe el valor en el registro de desplazamiento gpio_set (HC165_PORT, HC165_PL); para (uint8_t i = 0; i <8; i ++) {data << = 1; if (gpio_get (HC165_PORT, HC165_Q7)) datos | = 1; gpio_set (HC165_PORT, HC165_CLK); gpio_clear (HC165_PORT, HC165_CLK); } gpio_set (HC165_PORT, HC165_CS); datos = ~ datos; devolver datos; }

El procesador de pulsación de teclado del teclado lee el estado del teclado del registro y, según el valor recibido, realiza la acción necesaria. Actualmente, se implementan las siguientes funciones: más silencioso / más alto, siguiente pista / pista anterior, pausa, reproducción aleatoria / reproducción secuencial, reproducción la pista actual ( zanuda_mode  ).

Como el controlador del teclado está dentro de la función  play_file(), y la pista selecciona dentro del bucle de funciones  main(), hace necesario cambiar el comando al bucle de funciones  main(). Esto se puede hacer usando los play_file() valores devueltos por la función:

  • 0 – la siguiente pista o la siguiente pista aleatoria;
  • 2 – siguiente pista;
  • 1 – pista anterior.

Secuencia de seguimiento

La función descrita anteriormente  play_file() requiere una ruta completa al archivo. No es muy conveniente operar con nombres de archivos, además, esto puede requerir una cantidad significativa de memoria. Por lo tanto, es razonable asignarles algunos números.

Obtener nombres de archivo en el directorio permite la función de  f_readdir(&dir, &fileInfo) la biblioteca FatFs. Esta función lee un directorio y escribe fileInfo información de archivo en la estructura. Su campo  fname es el nombre del archivo. Utilizándolo, podemos, por ejemplo, enumerar los archivos y subdirectorios en un directorio.

uint8_t ls (ruta char *) {

Es necesario más bien para la depuración. Pero para nuestro propósito, necesitamos una función  is_mp3()que determine si el archivo realmente tiene una extensión  MP3. Si tiene éxito, devuelve cero.

Ahora podemos contar fácilmente los archivos MP3 en un directorio y obtener el número de nombre de archivo N (funciones  cnt_mp3_in_dir() y  get_name_mp3()).

uint8_t ismp3 (char * name) {

No es razonable escribir una función separada para reproducir un archivo por número; es mejor envolverlo sobre una función  play_file().

uint8_t play_mp3_n (char * ruta, uint16_t n) {

Mención especial merece la reproducción aleatoria. Obtener números pseudoaleatorios generalmente es un  tema especial , pero no tiene nada que ver con cosas como un reproductor de MP3. Simplemente utilizamos la función  rand() de la biblioteca  stdlib.h, pero para obtener una secuencia de números aleatorios, podemos pasarle un número aleatorio. Para semillas idénticas, la secuencia siempre será la misma.

¿Dónde obtener un número aleatorio en el microcontrolador? Puede tomar el valor del contador de reloj en tiempo real, o puede leer la señal del ADC. La primera opción, en mi opinión, es mejor, pero el reloj en este proyecto aún no se ha implementado. Por lo tanto, queda por leer la señal del ADC.

La secuencia de acciones es la siguiente: encienda el ADC en el modo más rápido e impreciso y mida el potencial en un tramo de controlador no utilizado. Es mejor conectar un conductor de longitud pequeña para que funcione como antena y atrape las pastillas. Pero esto no es necesario, porque solo mezclamos las pistas en el reproductor.

Luego apagamos el ADC como innecesario y transferimos el valor resultante a una función  srand()que configurará el PRNG.

estático uint16_t get_random (nulo) {

Dispositivo completo

Cuando todo o casi todo lo que quería se probó en el diseño, puede ensamblar un prototipo. Para esto, se fabrica dos placas de circuito impreso.

En principio, todos los detalles necesarios caber en un tablero de doble cara, pero yo era demasiado vago.

A continuación, el módulo de visualización se ensambló y probó en el diseño.

Luego, la placa de códec se solda y se conecta a la placa de visualización.

Y en conclusión, todo se colocó en una caja de plexiglás, que resultó ser demasiado grande.

El reproductor suena bastante decente, pero, desafortunadamente, consume demasiado (aproximadamente 60 mA). Sin embargo, esto no es tan aterrador.

Lo que vale la pena agregar

En el futuro plano agregar soporte para etiquetas ID3, una búsqueda recursiva en el sistema de archivos y soporte para listas de reproducción. Bueno, ahora puedes armar tu propio jugador y adjuntarlo a lo que tu corazón quieras.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *