- 1. Le protocole SPI
- 2. Utiliser le module vs 1053 sans le module de carte SD
- 3. Déclenchement du son par un bouton
- 4. Utiliser le module vs 1053 avec le module de carte SD
- 5. Conclusion
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.
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.
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 1053 | Pico | Couleur 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 |
2.2. schéma de principe
Un petit schéma de principe entre le module vs1053 et le PI 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
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 SD | Pico | Couleur 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 |
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.