Ce tuto va s'enrichir au fur et à mesure des mes travaux. Nous commencerons par un résumé du protocole SPI, puis l'utilisation du module son sans stockage supplémentaire et ensuite l'utilisation d'un module de carte SD avec le module son. J'espère finir avec l'utilisation de la programmation asynchrone (uasyncio)

1. Le protocole SPI

La liaison SPI (Serial Peripheral Interface) est un protocole créé par Motorola dans les années 80. Il fonctionne en full-duplex et fonctionne en maître-esclave (ou contrôleur-périphériques). Le maître peut commander plusieurs esclaves avec une ligne dédiée pour chaque esclave nommée SS (slave select). Sur mes modules, ils sont nommés CS et XCS.

Le bus SPI utilise quatre signaux logiques :

  • SCLK ou SCK — Serial Clock, Horloge (généré par le maître)
  • MOSI — Master Output, Slave Input (généré par le maître)
  • MISO — Master Input, Slave Output (généré par l'esclave)
  • SS ou CS ou XCS — Slave Select, Actif à l'état bas (généré par le maître) 

Le module son utilise d'autres connexions : DREQ, XRST, XDCS. Désolé, je n'ai pas plus d'informations pour ces connexions.

Voici un schéma de principe du bus SPI avec 2 périphériques.

 protocole SPI

Le Pi PICO fourni deux liaisons SPI : SPI0 (broches 0,1,2,3,4,5,6,7,16,17,18 et 19), SPI1 (broches 8,9,10,11,12,13,14,15). Voici un schéma de principe avec les fonctions des broches du PICO. Sur le Pico, la connexion MOSI est appelé SPI TX et MISO est appelé SPI RX.

spi pico

2. Utiliser le module vs 1053 sans le module de carte SD

Pour ce premier montage, je n'utiliserai pas le module de carte SD. Le fichier MP3 sera ajouté dans la mémoire du PICO. Autant dire que nous utiliserons un fichier de petite taille.

2.1. schéma de câblage

Voici le schéma de câblage.

VS 1053PicoCouleur connexion
5 V VBUS (40) Rouge
GND GND (38) Noir
XRST GP13 (17) Vert
MISO GP4 (6) Jaune
MOSI GP7 (10) Jaune
SCLK GP6 (9) Bleu
DREQ GP10 (14) Brun
XCS GP12 (16) Violet
XDCS GP 11 (15) Orange

 

cablage vs1053 without sdram

2.2. schéma de principe

Un petit schéma de principe entre le module vs1053 et le PI Pico :

principe vs1053 pico

Pour le code en MicroPython, on utilisera la librairie vs1053_syn.py se trouvant dans le GitHub  https://github.com/peterhinch/micropython-vs1053 de Peter Hinch.

2.3.  Le code

Le programme est très simple. Comme nous ne voulons pas utiliser le module de carte SD, nous n'utiliserons pas les paramètres sdcs et mp. Nous aurons besoin seulement d'ouvrir le fichier mp3 se trouvant dans la mémoire du PICO. On commence par initialiser le protocole SPI et configurer les ports supplémentaires du module vs1053. On initialise l'objet player en oubliant les ports sdcs et mp qui sont utilisés par le module carte SD. On continue par le réglage du son, le chargement du fichier mp3 et finalement, on écoute la musique.

'''
Programme PI Pico avec le module vs1053
lecture d'un fichier mp3 dans la mémoire du Pico
d'après le tutorial 
https://electroniqueamateur.blogspot.com/2022/08/lire-des-fichiers-mp3-avec-raspberry-pi.html
'''

from vs1053_syn import * # https://github.com/peterhinch/micropython-vs1053
from machine import SPI, Pin
import time
import os

# initialisation du protocole 
spi = SPI(0, sck=Pin(6), mosi=Pin(7), miso=Pin(4))

# broches du module VS1053
reset = Pin(13, Pin.OUT, value=1) 
xcs = Pin(12, Pin.OUT, value=1) 
xdcs = Pin(11, Pin.OUT, value=1)
dreq = Pin(10, Pin.IN)

# sdcs et mp ne sont pas initialisés car pas de module SDRam
player = VS1053(spi, reset, dreq, xdcs, xcs) 
player.volume(-25, -25) # réglage du volume sonore

song = open("souffle.mp3", "rb")

print("En train de jouer le fichier mp3 ") # affichage du titre
player.play(song)
print("le programme continue")

3. Déclenchement du son par un bouton

C'est bien tout ça, mais le programme précédent, c'est du one shoot. Pour mes montages Lego, j'ai besoin d'un déclenchement par un bouton. J'ai rajouté aussi l'allumage de la LED interne pour vérifier le bon fonctionnement du programme.

3.1. schéma de câblage

cablage vs1053 without sdram button

3.2. le code

Par rapport au précédent code, on initialise la led interne du PICO et on initialise un bouton sur le port 15. La résistance de 10Kohms est fortement conseillé pour le PICO V2.

La led clignote au démarrage du programme. Lorsqu'on appuie sur le bouton, on active le son. On s'aperçoit que la led arrête de clignoter tant que le module son fonctionne. Contrairement au module son Adafruit MAX98357, la librairie du vs1053 ne permet pas de dérouler le code pour exécuter autre chose lorsque le son fonctionne. Nous devrons utiliser la fonction asynchrone avec la librairie uasyncio.

Petite précision, pour rejouer le son, il faudra ouvrir le fichier à chaque utilisation. Autre point, ajoutez le réglage du son à chaque ouverture du fichier pour éviter une augmentation du son à la deuxième ouverture du fichier son.

'''
Programme PI Pico avec le module vs1053
lecture d'un fichier mp3 dans la mémoire du Pico
d'après le tutorial 
https://electroniqueamateur.blogspot.com/2022/08/lire-des-fichiers-mp3-avec-raspberry-pi.html
Ajout d'un bouton pour déclencher un son
'''

from vs1053_syn import * # https://github.com/peterhinch/micropython-vs1053
from machine import SPI, Pin
import time
import os

# initialisation du protocole 
spi = SPI(0, sck=Pin(6), mosi=Pin(7), miso=Pin(4))

#initialisation de la led du pico
led_onboard = machine.Pin("LED", machine.Pin.OUT)
#initialisation du bouton
button = machine.Pin(15, machine.Pin.IN, machine.Pin.PULL_DOWN)

# broches du module VS1053
reset = Pin(13, Pin.OUT, value=1) 
xcs = Pin(12, Pin.OUT, value=1) 
xdcs = Pin(11, Pin.OUT, value=1)
dreq = Pin(10, Pin.IN)

# sdcs et mp ne sont pas initialisés car pas de module SDRam
player = VS1053(spi, reset, dreq, xdcs, xcs) 



while True:
    if button.value() == 1:
        player.volume(-10, -10) # réglage du volume sonore
        song = open("souffle.mp3", "rb")
        print("En train de jouer le fichier mp3 ") # affichage du titre
        player.play(song)
        print("le programme continue")
    led_onboard.toggle()
    time.sleep(.5)

4. Utiliser le module vs 1053 avec le module de carte SD

Pour éviter de saturer la mémoire du PICO, nous allons utilisez une carte SDRam pour stocker les fichiers son. Nous utiliserons un module de carte SD en utilisant le protocole SPI. Nous utiliserons les mêmes liaisons MISO et MOSI ainsi que la liaison Horloge. La liaison SS (Slave select) sera différente. 

4.1. schéma de câblage

Voici le schéma de câblage.

VS 1053

Module carte SDPicoCouleur connexion
5 V +5 VBUS (40) Rouge
GND GND GND (38) Noir
XRST   GP13 (17) Vert
MISO MISO GP4 (6) Jaune
MOSI MOSI GP7 (10) Jaune
SCLK SCK GP6 (9) Bleu
DREQ   GP10 (14) Brun
XCS   GP12 (16) Violet
XDCS   GP 11 (15) Orange
  CS GP14 (19) Blanc

 

cablage vs1053 with sdram

 

4.2. librairie SDRam

Nous aurons besoin de la librairie SDRam ci-dessous à installer dans la mémoire du PICO.

"""
MicroPython driver for SD cards using SPI bus.

Requires an SPI bus and a CS pin.  Provides readblocks and writeblocks
methods so the device can be mounted as a filesystem.

Example usage on pyboard:

    import pyb, sdcard, os
    sd = sdcard.SDCard(pyb.SPI(1), pyb.Pin.board.X5)
    pyb.mount(sd, '/sd2')
    os.listdir('/')

Example usage on ESP8266:

    import machine, sdcard, os
    sd = sdcard.SDCard(machine.SPI(0), machine.Pin(15))
    os.umount()
    os.VfsFat(sd, "")
    os.listdir()

"""

from micropython import const
import time


_CMD_TIMEOUT = const(100)

_R1_IDLE_STATE = const(1 << 0)
#R1_ERASE_RESET = const(1 << 1)
_R1_ILLEGAL_COMMAND = const(1 << 2)
#R1_COM_CRC_ERROR = const(1 << 3)
#R1_ERASE_SEQUENCE_ERROR = const(1 << 4)
#R1_ADDRESS_ERROR = const(1 << 5)
#R1_PARAMETER_ERROR = const(1 << 6)
_TOKEN_CMD25 = const(0xfc)
_TOKEN_STOP_TRAN = const(0xfd)
_TOKEN_DATA = const(0xfe)


class SDCard:
    def __init__(self, spi, cs):
        self.spi = spi
        self.cs = cs

        self.cmdbuf = bytearray(6)
        self.dummybuf = bytearray(512)
        for i in range(512):
            self.dummybuf[i] = 0xff
        self.dummybuf_memoryview = memoryview(self.dummybuf)

        # initialise the card
        self.init_card()

    def init_spi(self, baudrate):
        try:
            master = self.spi.MASTER
        except AttributeError:
            # on ESP8266
            self.spi.init(baudrate=baudrate, phase=0, polarity=0)
        else:
            # on pyboard
            self.spi.init(master, baudrate=baudrate, phase=0, polarity=0)

    def init_card(self):
        # init CS pin
        self.cs.init(self.cs.OUT, value=1)

        # init SPI bus; use low data rate for initialisation
        self.init_spi(100000)

        # clock card at least 100 cycles with cs high
        for i in range(16):
            self.spi.write(b'\xff')

        # CMD0: init card; should return _R1_IDLE_STATE (allow 5 attempts)
        for _ in range(5):
            if self.cmd(0, 0, 0x95) == _R1_IDLE_STATE:
                break
        else:
            raise OSError("no SD card")

        # CMD8: determine card version
        r = self.cmd(8, 0x01aa, 0x87, 4)
        if r == _R1_IDLE_STATE:
            self.init_card_v2()
        elif r == (_R1_IDLE_STATE | _R1_ILLEGAL_COMMAND):
            self.init_card_v1()
        else:
            raise OSError("couldn't determine SD card version")

        # get the number of sectors
        # CMD9: response R2 (R1 byte + 16-byte block read)
        if self.cmd(9, 0, 0, 0, False) != 0:
            raise OSError("no response from SD card")
        csd = bytearray(16)
        self.readinto(csd)
        if csd[0] & 0xc0 != 0x40:
            raise OSError("SD card CSD format not supported")
        self.sectors = ((csd[8] << 8 | csd[9]) + 1) * 2014
        #print('sectors', self.sectors)

        # CMD16: set block length to 512 bytes
        if self.cmd(16, 512, 0) != 0:
            raise OSError("can't set 512 block size")

        # set to high data rate now that it's initialised
        self.init_spi(1320000)

    def init_card_v1(self):
        for i in range(_CMD_TIMEOUT):
            self.cmd(55, 0, 0)
            if self.cmd(41, 0, 0) == 0:
                self.cdv = 512
                #print("[SDCard] v1 card")
                return
        raise OSError("timeout waiting for v1 card")

    def init_card_v2(self):
        for i in range(_CMD_TIMEOUT):
            time.sleep_ms(50)
            self.cmd(58, 0, 0, 4)
            self.cmd(55, 0, 0)
            if self.cmd(41, 0x40000000, 0) == 0:
                self.cmd(58, 0, 0, 4)
                self.cdv = 1
                #print("[SDCard] v2 card")
                return
        raise OSError("timeout waiting for v2 card")

    def cmd(self, cmd, arg, crc, final=0, release=True):
        self.cs(0)

        # create and send the command
        buf = self.cmdbuf
        buf[0] = 0x40 | cmd
        buf[1] = arg >> 24
        buf[2] = arg >> 16
        buf[3] = arg >> 8
        buf[4] = arg
        buf[5] = crc
        self.spi.write(buf)

        # wait for the response (response[7] == 0)
        for i in range(_CMD_TIMEOUT):
            response = self.spi.read(1, 0xff)[0]
            if not (response & 0x80):
                # this could be a big-endian integer that we are getting here
                for j in range(final):
                    self.spi.write(b'\xff')
                if release:
                    self.cs(1)
                    self.spi.write(b'\xff')
                return response

        # timeout
        self.cs(1)
        self.spi.write(b'\xff')
        return -1

    def cmd_nodata(self, cmd):
        self.spi.write(cmd)
        self.spi.read(1, 0xff) # ignore stuff byte
        for _ in range(_CMD_TIMEOUT):
            if self.spi.read(1, 0xff)[0] == 0xff:
                self.cs(1)
                self.spi.write(b'\xff')
                return 0    # OK
        self.cs(1)
        self.spi.write(b'\xff')
        return 1 # timeout

    def readinto(self, buf):
        self.cs(0)

        # read until start byte (0xff)
        while self.spi.read(1, 0xff)[0] != 0xfe:
            pass

        # read data
        mv = self.dummybuf_memoryview[:len(buf)]
        self.spi.write_readinto(mv, buf)

        # read checksum
        self.spi.write(b'\xff')
        self.spi.write(b'\xff')

        self.cs(1)
        self.spi.write(b'\xff')

    def write(self, token, buf):
        self.cs(0)

        # send: start of block, data, checksum
        self.spi.read(1, token)
        self.spi.write(buf)
        self.spi.write(b'\xff')
        self.spi.write(b'\xff')

        # check the response
        if (self.spi.read(1, 0xff)[0] & 0x1f) != 0x05:
            self.cs(1)
            self.spi.write(b'\xff')
            return

        # wait for write to finish
        while self.spi.read(1, 0xff)[0] == 0:
            pass

        self.cs(1)
        self.spi.write(b'\xff')

    def write_token(self, token):
        self.cs(0)
        self.spi.read(1, token)
        self.spi.write(b'\xff')
        # wait for write to finish
        while self.spi.read(1, 0xff)[0] == 0x00:
            pass

        self.cs(1)
        self.spi.write(b'\xff')

    def count(self):
        return self.sectors

    def readblocks(self, block_num, buf):
        nblocks, err = divmod(len(buf), 512)
        assert nblocks and not err, 'Buffer length is invalid'
        if nblocks == 1:
            # CMD17: set read address for single block
            if self.cmd(17, block_num * self.cdv, 0) != 0:
                return 1
            # receive the data
            self.readinto(buf)
        else:
            # CMD18: set read address for multiple blocks
            if self.cmd(18, block_num * self.cdv, 0) != 0:
                return 1
            offset = 0
            mv = memoryview(buf)
            while nblocks:
                self.readinto(mv[offset : offset + 512])
                offset += 512
                nblocks -= 1
            return self.cmd_nodata(b'\x0c') # cmd 12
        return 0

    def writeblocks(self, block_num, buf):
        nblocks, err = divmod(len(buf), 512)
        assert nblocks and not err, 'Buffer length is invalid'
        if nblocks == 1:
            # CMD24: set write address for single block
            if self.cmd(24, block_num * self.cdv, 0) != 0:
                return 1

            # send the data
            self.write(_TOKEN_DATA, buf)
        else:
            # CMD25: set write address for first block
            if self.cmd(25, block_num * self.cdv, 0) != 0:
                return 1
            # send the data
            offset = 0
            mv = memoryview(buf)
            while nblocks:
                self.write(_TOKEN_CMD25, mv[offset : offset + 512])
                offset += 512
                nblocks -= 1
            self.write_token(_TOKEN_STOP_TRAN)
        return 0

4.3. le code

 Voici le code qui va lire tous les fichiers présents sur la carte SD.

'''
Programme PI Pico avec le module vs1053
lecture de plusieurs fichiers mp3 dans une carte SDRam
utilisation d'un module SDRam
d'après le tutorial 
https://electroniqueamateur.blogspot.com/2022/08/lire-des-fichiers-mp3-avec-raspberry-pi.html
'''

from vs1053_syn import * # https://github.com/peterhinch/micropython-vs1053
from machine import SPI, Pin
import time
import os

# initialisation de SPI
spi = SPI(0, sck=Pin(6), mosi=Pin(7), miso=Pin(4))

# broche CS de la carte SD:
sdcs = Pin(14, Pin.OUT, value=1) 

# broches du module VS1053
reset = Pin(13, Pin.OUT, value=1) 
xcs = Pin(12, Pin.OUT, value=1) 
xdcs = Pin(11, Pin.OUT, value=1)
dreq = Pin(10, Pin.IN)

player = VS1053(spi, reset, dreq, xdcs, xcs, sdcs=sdcs, mp='/fc')
player.volume(-25, -25) # réglage du volume sonore

songs = sorted([x for x in os.listdir('/fc') if x.endswith('.mp3')])

for song in songs:
    print("En train de jouer: ", song) # affichage du titre
    fn = ''.join(('/fc/', song))
    with open(fn, 'rb') as f:
        player.play(f)

5.  Conclusion

Nous avons vu le fonctionnement simplifié de la carte son vs1053. L'inconvénient des programmes précédents est qu'ils ne permettent pas la lecture du son en parallèle avec d'autres événements comme le scintillement d'une LED. Nous devons utiliser une autre méthode de programmation que nous étudierons dans un prochain article.