3. Desarrollo del Proyecto - alejandromz/AudioPlayer-ESP32 GitHub Wiki

Prueba de los módulos individuales

A continuación, se presenta los códigos y métodos usados para las pruebas de funcionamiento individuales de cada uno de los módulos antes del montaje completo del proyecto y así garantizar el correcto funcionamiento de cada uno de ellos.

Amplificador MAX98357A (I2S)

Debatiblemente, el módulo más importante para el desarrollo del proyecto es el módulo de audio. Como se estableció previamente, esta tarea va a ser desarrollada por el Amplificador Max98357a mediante el protocolo I2S. A continuación se explica detalladamente la estructura del código desarrollado para implementar este módulo. El código completo se puede encontrar en este mismo repositorio, con el nombre de i2s_audio.py.

Para que se pueda hacer funcionar este modulo se requieren instalar los paquetes I2S y Pin. Adicionalmente para este ejemplo se importa una sección del paquete time.

from machine import I2S
from machine import Pin
from time import sleep_ms

A continuación se deben definir los pines de la ESP32 a los cuales se va a conectar el modulo. Inicialmente esta conexión de pines se puede realizar en cualquier pin GPIO de la tarjeta. En los comentarios del código se hacen explícitos los nombres de los pines que se tienen impresos en el modulo.

bck_pin = Pin(22)   # Pin BCLK
ws_pin = Pin(21)    # Pin LRC
sdout_pin = Pin(23) # Pin Din

Es importante aclarar que el módulo tiene otros cuarto pines: dos de alimentación y dos llamados SD y Gain. En este ejemplo, los pines SD se dejan libres ya que no se requiere un valor particular de ganancia y se quiere manejar un sonido mono. Habiendo aclarado esto, el siguiente paso es crear una instancia de la clase I2S llamada audio_out. Para crear esta instancia se requiere definir los Pines, el modo (entrada o salida), el tamaño de la muestra en bits, el formato del audio (estéreo o mono), la frecuencia de muestreo en Hertz, y la longitud interna del buffer (bytes).

audio_out = I2S(1,                                 
                sck=bck_pin, ws=ws_pin, sd=sdout_pin,
                mode=I2S.TX, 
                bits=16,
                format=I2S.MONO,
                rate=16000,
                ibuf=2000)

Por último, simplemente se define una cadena de bits arbitraria que se manda repetidamente a la salida I2S mediante el método write. El resultado de esto es un tono intermitente en el parlante conectado al amplificador.

samples = bytearray([1,0,1,0,0,0,1,1,1,1,0,1,1,0]*100)

while True:
    num_bytes_written = audio_out.write(samples)
    sleep_ms(100)

Lector de tarjeta MicroSD

A continuación, se desarrolló el código para poder manejar el módulo micro SD, es decir, para poder tener acceso a sus archivos y que la ESP32 lo reconociera como parte de su almacenamiento interno. El código completo se puede encontrar en este mismo repositorio, con el nombre de mainSD.py

Para el desarrollo de este ejemplo y para garantizar el correcto funcionamiento de este modulo es indispensable tener acceso a la clase SDCard. Esta no se encuentra actualmente en los paquetes importados, sino que se obtuvo directamente del Github Oficial de Micropython y se puede encontrar allí directamente o en este mismo repositorio con el nombre sdcard.py.

A continuación se va a realizar una explicación detallada del código que se desarrolló. En términos de paquetes, para manejar el módulo micro SD se requiere importar los paquetes os, SoftSPI y Pin.

import os
from machine import Pin, SoftSPI
from sdcard import SDCard

A continuación se deben definir los pines de la ESP32 a los cuales se va a conectar el modulo y se crea la instancia de la clase SDCard. Inicialmente esta conexión de pines se puede realizar en cualquier pin GPIO de la tarjeta. Los nombres que se tienen en el programa son los mismos que se tienen impresos en el modulo al lado de cada pin, aclarando que el Pin(27) es el llamado CS en el modulo.

spisd = SoftSPI(-1,
                miso=Pin(13),
                mosi=Pin(12),
                sck=Pin(14))
sd = SDCard(spisd, Pin(27))

Teniendo esta instancia asociada al modulo físico ya se puede asociar este almacenamiento con el almacenamiento interno de la ESP32. Esto se logra mediante el método VfsFat y se asocia a un directorio llamado /sd mediante el método mount. Ejecutando el código se va a visualiza el almacenamiento interno de la ESP32 antes y despúes de asociar la tarjeta SD y además se muestran los archivos dentro der la misma.

print('Directory: {}'.format(os.listdir()))
vfs=os.VfsFat(sd)
os.mount(vfs, '/sd')
print('Directory: {}'.format(os.listdir()))
os.chdir('sd')
print('Directory SD: {}'.format(os.listdir()))

Joystick

Las conexiones correspondientes se pueden visualizar en el propio código hacia la ESP32 para realizar la respectiva prueba, el código usado es el siguiente:

from machine import Pin, ADC
from time import sleep_ms

x = ADC(Pin(34))
y = ADC(Pin(35))

x.atten(ADC.ATTN_11DB)
y.atten(ADC.ATTN_11DB)

sw = Pin(26, Pin.IN, Pin.PULL_UP)

while True:
    x_val = x.read()
    y_val = y.read()
    print("Current_position:{},{}".format(x_val,y_val))
    print('Pin_Value=',sw.value())
    sleep_ms(300)
    

Esto con el fin de visualizar en el panel de ejecución los valores arrojados de cada pin en el eje X y el eje Y, a su vez del valor correspondiente a el pin del botón sw, el cual tendrá un valor de 1 (uno) al no oprimirlo y un valor de 0 (cero) al oprimirlo. Con respecto a los valores de los ejes X y Y se espera observar que valores sean los que se ven nombrados como PinDig en la siguiente imagen al mover el Joystick hacia las direcciones indicadas.

pindig

Display LCD (I2C)

El código usado para la realización del test de la pantalla LCD se muestra en el siguiente código donde también se puede observar los pines usados para la conexión de este módulo, nuevamente considere que se hace la importación de los archivos lcd_api.py y i2c_lcd.py anexos también en la carpeta de test en los códigos.

from time import sleep_ms, ticks_ms
from machine import I2C, Pin
from i2c_lcd import I2cLcd

DEFAULT_I2C_ADDR = 0X27

i2c=I2C(scl=Pin(22), sda=Pin(21), freq=400000)
lcd = I2cLcd(i2c, DEFAULT_I2C_ADDR,2,16)

Donde puede realizar las pruebas con el siguiente comando:

lcd.putstr('hello world')

Se espera a que el texto (hello world) sea mostrado en la pantalla del display LCD.

Integración del Software

Menú a partir de joystick y LCD.

Inicialmente se realiza la integración de los módulos Joystick, LCD y Lector micro SD para la construcción de una interfaz sencilla e intuitiva para el usuario, con la estructura de un menú listado, donde se pretende mostrar en la pantalla LCD el nombre de los archivos, carpetas y subcarpetas que se encuentran almacenados en la memoria SD, y así mismo se pretende navegar por medio de la palanca del módulo Joystick, si se detecta que la palanca baja o sube, se señalara con una flecha el archivo que esta sobre seleccionado subiendo y bajando esa flecha, para un mejor entendimiento de la navegabilidad.

Para esto se realiza el siguiente código donde se observa la integración de estos módulos:

from time import sleep_ms, ticks_ms
from machine import I2C, Pin
from i2c_lcd import I2cLcd
from machine import Pin, ADC, SoftSPI
from time import sleep_ms
from sdcard import SDCard

x = ADC(Pin(34))
y = ADC(Pin(35))

x.atten(ADC.ATTN_11DB)
y.atten(ADC.ATTN_11DB)

sw = Pin(26, Pin.IN, Pin.PULL_UP)

DEFAULT_I2C_ADDR = 0X27

i2c=I2C(scl=Pin(22), sda=Pin(21), freq=400000)
lcd = I2cLcd(i2c, DEFAULT_I2C_ADDR,2,16)

threshold = 500
directorio = ['uno', 'dos', 'tres', 'cuatro', 'cinco']
pos = 0

lcd.clear()
lcd.putstr('{}\n{}'.format(directorio[pos],directorio[pos+1]))

while True:
    x_val = x.read()
    y_val = y.read()
    if y_val < threshold:
        pos = pos-1
        if pos<0:
            pos=0
    elif y_val > 4095-threshold:
        pos = pos+1
        if pos>len(directorio)-1:
            pos = len(directorio)-1

    lcd.clear()
    if pos == len(directorio)-1:
        lcd.putstr('{}\n>{}'.format(directorio[pos-1],directorio[pos]))
    else:
        lcd.putstr('>{}\n{}'.format(directorio[pos],directorio[pos+1]))
    sleep_ms(300)
    

Logrando el siguiente resultado:

Menú a partir de joystick, LCD y tarjeta micro SD.

from time import sleep_ms, ticks_ms
from machine import I2C, Pin
from i2c_lcd import I2cLcd
from machine import Pin, ADC, SoftSPI
from time import sleep_ms
from sdcard import SDCard
import os

x = ADC(Pin(34))
y = ADC(Pin(35))

x.atten(ADC.ATTN_11DB)
y.atten(ADC.ATTN_11DB)

sw = Pin(26, Pin.IN, Pin.PULL_UP)

DEFAULT_I2C_ADDR = 0X27

i2c=I2C(scl=Pin(22), sda=Pin(21), freq=400000)
lcd = I2cLcd(i2c, DEFAULT_I2C_ADDR,2,16)

spisd = SoftSPI(-1,
                miso=Pin(13),
                mosi=Pin(12),
                sck=Pin(14))
sd = SDCard(spisd, Pin(27))

vfs=os.VfsFat(sd)
os.mount(vfs, '/sd')
owd = os.getcwd()
old_dir=owd
## os.chdir('sd')

threshold = 500
directorio = os.listdir()
## directorio.append('Return')
directorio_dsp = [st[0:12] for st in directorio]
pos = 0

lcd.clear()
lcd.putstr('{}\n{}'.format(directorio_dsp[pos],directorio_dsp[pos+1]))
print(owd)

while True:
    x_val = x.read()
    y_val = y.read()
    
    if y_val < threshold:
        pos = pos-1
        if pos<0:
            pos=0
    elif y_val > 4095-threshold:
        pos = pos+1
        if pos>len(directorio)-1:
            pos = len(directorio)-1
    
    if sw.value()==0:
        if directorio[pos] == 'Return':
            try:
                lst = old_dir.split('/')
                print(lst[0:-2])
                old_dir=''
                
                if len(lst) > 2:
                    for item in lst[0:-2]:
                        ## print(item)
                        if item != '':
                            old_dir=old_dir+'/'+item
                else:
                    old_dir=owd
                print(old_dir)
            except:
                print('Error')
                
            os.chdir(old_dir)
            directorio = os.listdir()
            directorio.append('Return')
            directorio_dsp = [st[0:12] for st in directorio]
            pos = 0
        else:
            try:
                os.chdir(directorio[pos])
                old_dir=old_dir+directorio[pos]+'/'
                print(old_dir)
                directorio = os.listdir()
                directorio.append('Return')
                directorio_dsp = [st[0:12] for st in directorio]
                pos = 0
            except:
                print('No es un directorio.')
    
    lcd.clear()
    if pos == len(directorio)-1:
        lcd.putstr('{}\n>{}'.format(directorio_dsp[pos-1],directorio_dsp[pos]))
    else:
        lcd.putstr('>{}\n{}'.format(directorio_dsp[pos],directorio_dsp[pos+1]))
    sleep_ms(300)

Reproducir archivo de audio de tarjeta micro SD

El propósito de integrar el código del modulo micro SD y el amplificador de audio es poder reproducir algo más que tonos; se quiere reproducir archivos de audio previamente subidos al almacenamiento de la tarjeta. Las explicaciones de las partes del código que fueron reutilizadas de las pruebas individuales se van a obviar y el código completo se puede encontrar en int_SD_Aud.py.

from machine import I2S
from machine import Pin
from time import sleep_ms
import os
from machine import Pin, SoftSPI
from sdcard import SDCard

spisd = SoftSPI(-1,
                miso=Pin(13),
                mosi=Pin(12),
                sck=Pin(14))
sd = SDCard(spisd, Pin(27))

vfs=os.VfsFat(sd)
os.mount(vfs, '/sd')

bck_pin = Pin(22)   # Pin BCLK
ws_pin = Pin(21)    # Pin LRC
sdout_pin = Pin(23) # Pin Din

audio_out = I2S(1,
                sck=bck_pin, ws=ws_pin, sd=sdout_pin,
                mode=I2S.TX,
                bits=16,
                format=I2S.MONO,
                rate=16000,
                ibuf=2000)

Es importante aclarar que en este código todavía no es posible elegir el archivo de audio mediante ninguna interfaz, por lo que el nombre del archivo que se quiere reproducir y su ubicación dentro de la memoria deben ser determinadas previamente. A continuación se puede ver como se almacena el nombre del archivo en la variable WAV_FILE, se determina la ubicación del mismo y se extraen sus datos a la variable wav.

WAV_FILE = 'taunt-16k-16bits-mono-12db.wav'
wav_file = '/sd/{}'.format(WAV_FILE)
wav = open(wav_file,'rb')

A continuación, se determina el tamaño (en bytes) del archivo de audio mediante el método seek y se establece la posición inicial de lectura del archivo en el byte 44, ya que los bytes previos a este son el header del archivo de audio.

wav_len = wav.seek(0, 2)
print(wav_len)
pos = wav.seek(44)

A continuación se crea un arreglo de 1024 bytes, que son los que se van a enviar como buffer a la interfaz I2S en cada iteración del loop. Sin embargo, para poder mejorar la eficiencia del manejo de memoria, no se va a editar directamente este arreglo de bytes sino que se va a editar mediante un memoryview. Esta implementación permite tener la opción de pausar el audio en futuras mejoras del código.

wav_samples = bytearray(1024)
wav_samples_mv = memoryview(wav_samples)

Por último, se hace un loop que se va a repetir mientras que la posición en la que estamos leyendo el archivo de audio sea menor a la longitud del mismo. Dentro de este loop simplemente vamos a pasar el buffer mencionado previamente a la interfaz I2S y actualizar la posición dentro del archivo para que los próximos 1024 bytes sean cargados en el arreglo (mediante el memoryview).

while  pos < wav_len:
    num_written = audio_out.write(wav_samples_mv)
    pos += num_written
    wav.seek(pos)
    wav.readinto(wav_samples_mv)
    
print("Fin del audio.")

Por último, se imprime en la terminal un mensaje para confirmar que se llega al final del archivo de audio.