01signal.com

Connecter un casque audio ordinaire à une broche de sortie numérique et écouter de la musique

Cette page web fait partie d’un ensemble de petits projets qui explorent les possibilités de la carte Smart Zynq.

Ce projet est également publié sur HelloFPGA, un site recommandé pour les lecteurs chinois.

Introduction

Ce tutoriel explique comment connecter un casque audio ordinaire à la carte Smart Zynq et écouter de la musique. Le but de ce projet est de montrer comment utiliser un flux Xillybus pour envoyer des données en continu vers le FPGA. Le code Verilog qui implémente un modulateur PWM est également présenté ici.

Le code présenté ici n’est pas un exemple de la manière d’implémenter une sortie audio. La méthode habituelle pour réaliser une sortie analogique est une technique plus compliquée, appelée Sigma-Delta. Cette technique peut être implémentée sur un FPGA, mais ses fondements théoriques sont beaucoup plus difficiles à comprendre.

Un autre inconvénient de cette implémentation est que sa fréquence d’échantillonnage (sampling rate) est imprécise (48828 Hz au lieu de 48000 Hz). Cela peut être corrigé facilement en changeant la fréquence de l’horloge utilisée par la logique. Le sujet de la manipulation des horloges à cette fin n’est pas abordé ici, car cet exemple privilégie la simplicité plutôt que la précision.

Le matériel nécessaire pour cette démonstration est le suivant :

Préparation du projet Vivado

Créez un nouveau projet Vivado à partir de l’archive du pack de démonstration (demo bundle), c’est-à-dire le kit de partition de démarrage (boot partition kit). Ouvrez verilog/src/xillydemo.v dans un éditeur de texte. Supprimez la partie du code intitulée « PART 2 ». Insérez à la place l’extrait de code suivant :

   /*
    * PART 2
    * ======
    *
    * This code demonstrates a PWM-based audio output
    */

   reg [10:0]   pwm_level, threshold_left, threshold_right;
   reg 		pwm_left, pwm_right;
   reg 		fifo_out_valid;
   wire [31:0] 	fifo_out;
   wire 	fifo_empty;

   wire 	fifo_rd_en = !fifo_out_valid && !fifo_empty;
   wire 	next_word = (pwm_level == 11'h7ff);

   assign J6 = { pwm_right, pwm_left };

   always @(posedge bus_clk)
     begin
	pwm_level <= pwm_level + 1;

	if (next_word && fifo_out_valid)
	  begin
	     // The audio samples are signed integers. Change them to
	     // unsigned by adding 1024.
	     threshold_left <= fifo_out[15:5] + 1024;
	     threshold_right <= fifo_out[31:21] + 1024;
	  end
	else if (next_word) // FIFO's output not valid, keep silent
	  begin
	     threshold_left <= 0;
	     threshold_right <= 0;
	  end

	pwm_left <= (threshold_left > pwm_level);
	pwm_right <= (threshold_right > pwm_level);

	if (fifo_rd_en)
	  fifo_out_valid <= 1;
	else if (next_word)
	  fifo_out_valid <= 0;
     end

   // 32-bit FIFO for audio samples
   fifo_32x512 fifo_32
     (
      .clk(bus_clk),
      // Interface with Xillybus IP core
      .srst(!user_w_write_32_open),
      .din(user_w_write_32_data),
      .wr_en(user_w_write_32_wren),
      .full(user_w_write_32_full),

      // Interface with application logic
      .rd_en(fifo_rd_en),
      .dout(fifo_out),
      .empty(fifo_empty)
      );

   // Send the text "PWM" to reassure that the correct bitstream is used.
   assign user_r_read_32_eof = 0;
   assign user_r_read_32_empty = 0;
   assign user_r_read_32_data = 32'h0a_4d_57_50; // "PWM" + LF

Vous pouvez aussi télécharger le fichier xillydemo.v depuis ce lien.

Générez un flux de bits (bitstream) à partir du projet modifié de la même manière que vous l’avez fait pour le pack de démonstration (demo bundle). Copiez également le fichier sur la carte TF de la même manière (écrasez l’ancien fichier xillydemo.bit avec le fichier généré par ce projet).

Connexion du casque

Connectez une résistance de 50 Ω à 200 Ω à la broche d’E/S qui porte le signal audio : J6/1 (oreille gauche) ou J6/2 (oreille droite). Pour localiser J6, cherchez l’endroit où est écrit « Bank 33 VCCIO Vadj » au dos de la carte Smart Zynq. La rangée de broches proche de cette inscription est le connecteur à broches sur lequel nous allons travailler. J6/1 est donc la broche la plus proche du connecteur HDMI.

Connectez l’autre côté de la résistance à la pointe de la fiche du casque. Une pince crocodile peut être utilisée à cette fin.

Connectez le manchon de la fiche du casque à la masse de la Smart Zynq. Les broches de masse du connecteur sont situées en J6/35 ou J6/36. Il n’est toutefois pas recommandé d’utiliser ces broches, car elles sont proches des broches d’alimentation.

À la place, on peut également utiliser n’importe quelle broche entre J6/3 et J6/34. Le FPGA les considère comme des sorties et les maintient au niveau logique '0'. On peut donc utiliser ces broches comme masse.

On peut aussi obtenir la connexion à la masse en fixant une pince crocodile sur la partie métallique extérieure de l’un des connecteurs de la carte : le connecteur Ethernet, le connecteur HDMI ou l’un des connecteurs USB.

Démarrage de la carte

Allumez la carte Smart Zynq comme d’habitude (ou effectuez un redémarrage). L’étape suivante consiste à vérifier que le bon flux de bits (bitstream) est chargé dans le FPGA (partie PL).

Tapez la commande « head /dev/xillybus_read_32 » dans le terminal. Cette commande lit les premières lignes de /dev/xillybus_read_32 et affiche le résultat :

# head /dev/xillybus_read_32
PWM
PWM
PWM
PWM
PWM
PWM
PWM
PWM
PWM
PWM

Si cette commande ne produit aucune sortie, ou si la sortie diffère de celle montrée ci-dessus, le mauvais flux de bits (bitstream) est chargé.

Lecture d’un fichier audio

Copiez un fichier audio dans le système de fichiers de Xillinux. Autrement dit, ce fichier doit être accessible aux commandes du système Linux.

Ce fichier doit être au format WAV : PCM non compressé, 2 canaux, s16le (c’est presque toujours le format des fichiers WAV). La fréquence d’échantillonnage (sampling rate) doit être de 48000 Hz, mais 44100 Hz fonctionnera aussi raisonnablement bien.

Un fichier audio adapté peut être téléchargé depuis ce lien.

Il existe plusieurs méthodes pour copier un fichier vers un système Linux. Par exemple, on peut utiliser le réseau Ethernet pour copier le fichier depuis un autre ordinateur vers le répertoire personnel de Xillinux avec cette commande :

$ scp sample.wav root@192.168.1.10:~/

Cette commande fonctionne aussi bien avec l’invite de commande Microsoft Windows qu’avec un shell Linux. Remplacez l’adresse IP (192.168.1.10 dans cet exemple) par celle de la carte.

Il existe aussi d’autres méthodes pour copier un fichier vers Xillinux, par exemple avec NFS ou CIFS.

Une fois le fichier copié dans le système de fichiers de Xillinux, utilisez cette commande pour écouter le fichier audio :

# cat sample.wav > /dev/xillybus_write_32

Remplacez « sample.wav » par le nom du fichier que vous voulez lire. La commande ci-dessus fonctionne si le fichier se trouve dans le répertoire courant.

La commande lit le fichier sur le casque jusqu’à ce qu’une nouvelle invite de commande apparaisse. Vous devriez entendre la musique dans une oreille (ou les deux, si vous avez connecté J6/1 et J6/2 à des parties distinctes de la fiche du casque).

On peut arrêter cette commande en cours d’exécution avec CTRL-C.

Voilà. La suite de cette page explique comment cela fonctionne.

Comment les données audio arrivent au FPGA

La commande « cat » copie le contenu du fichier audio (sample.wav) dans un fichier de périphérique nommé « xillybus_write_32 ». Sous Linux, c’est la méthode habituelle pour envoyer des données à un pilote matériel. Dans cet exemple, le pilote s’interface avec le cœur IP (IP core) de Xillybus. Les données sont ainsi envoyées dans une FIFO à l’intérieur de la logique du FPGA.

Regardons la partie correspondante du code Verilog présenté plus haut :

   fifo_32x512 fifo_32
     (
      .clk(bus_clk),
      // Interface with Xillybus IP core
      .srst(!user_w_write_32_open),
      .din(user_w_write_32_data),
      .wr_en(user_w_write_32_wren),
      .full(user_w_write_32_full),

      // Interface with application logic
      .rd_en(fifo_rd_en),
      .dout(fifo_out),
      .empty(fifo_empty)
      );

C’est une instanciation (instantiation) d’une FIFO standard. Pour une explication générale du fonctionnement d’une FIFO, reportez-vous à cette page.

Cette FIFO comporte trois ports destinés à l’insertion de données dans la FIFO : din, wr_en et full (pleine). Ces trois ports sont tous connectés au cœur IP (IP core) Xillybus. Autrement dit, les trois signaux (user_w_write_32_data, user_w_write_32_wren et user_w_write_32_full) sont reliés à un module nommé xillybus. Ce montage permet au cœur IP (IP core) Xillybus d’écrire des données dans la FIFO.

Xillybus utilise ce montage pour remplir la FIFO avec les données que le logiciel écrit dans /dev/xillybus_write_32. Xillybus essaie en permanence d’écrire le plus de données possible dans la FIFO, mais il ne provoque jamais de débordement (overflow). Autrement dit, il obéit au signal full (pleine) de la FIFO.

En résumé, voici ce qui se passe :

Simplified data flow diagram for data playback with Xillybus

Toutes ces opérations se déroulent simultanément et en continu.

Pour plus d’informations sur Xillybus, consultez cette série de pages, en particulier cette page.

Comment le signal audio est créé

Jusqu’ici, la description a expliqué comment les données arrivent à la logique applicative du FPGA. Nous allons maintenant voir comment ces données se transforment en son.

Tout d’abord, prêtez attention à cette ligne du code Verilog :

   assign J6 = { pwm_right, pwm_left };

D’après cette ligne, les deux sorties audio sont pwm_right et pwm_left. Ces deux registres reçoivent leurs valeurs de la manière suivante :

   always @(posedge bus_clk)
     begin
	pwm_level <= pwm_level + 1;

 [ ... ]
	pwm_left <= (threshold_left > pwm_level);
	pwm_right <= (threshold_right > pwm_level);
 [ ... ]
    end

Remarquez que pwm_level est un simple compteur. Ce registre comporte 11 bits : il compte donc de 0 à 2047, puis repart de 0.

pwm_left vaut '1' lorsque threshold_left est supérieur à pwm_level. Autrement dit, threshold_left est comparé à un compteur qui balaie en boucle tous les nombres de 0 à 2047. Plus la valeur de threshold_left est élevée, plus pwm_left reste à l’état '1' longtemps. C’est le principe de la PWM : la durée de l’impulsion est proportionnelle à la valeur du signal analogique que l’on veut générer.

pwm_right fonctionne de la même manière vis-à-vis de threshold_right.

threshold_left et threshold_right contiennent les données du fichier WAV envoyées à travers le cœur IP (IP core) Xillybus. Examinons maintenant en détail comment cela se produit.

Commençons par regarder la partie de l’instanciation (instantiation) de la FIFO qui concerne la lecture :

      // Interface with application logic
      .rd_en(fifo_rd_en),
      .dout(fifo_out),
      .empty(fifo_empty)

fifo_rd_en est défini comme suit :

wire 	fifo_rd_en = !fifo_out_valid && !fifo_empty;

La validation de lecture (read enable) de la FIFO est donc à l’état haut quand la FIFO n’est pas empty (vide) et quand fifo_out_valid est à l’état bas. Regardons donc la définition de fifo_out_valid :

   always @(posedge bus_clk)
     begin
 [ ... ]
	if (fifo_rd_en)
	  fifo_out_valid <= 1;
	else if (next_word)
	  fifo_out_valid <= 0;
     end

fifo_out_valid signifie que ce registre est à l’état haut lorsque la sortie de la FIFO est valide. Plus précisément, fifo_out_valid est à l’état haut lorsque la sortie de la FIFO n’a pas encore été consommée. C’est pourquoi ce registre passe à l’état haut un cycle d’horloge après fifo_rd_en. Ce registre repasse à l’état bas lorsque next_word est à l’état haut. Comme nous le verrons plus bas, la logique qui implémente la PWM consomme la sortie de la FIFO lorsque next_word est à l’état haut.

next_word est défini comme suit :

   wire 	next_word = (pwm_level == 11'h7ff);

Rappelons que pwm_level est un compteur qui parcourt toutes les valeurs entre 0 et 2047. L’écriture hexadécimale de 2047 est 7ff. next_word est donc à l’état haut juste avant que pwm_level ne repasse à zéro.

À quelle fréquence next_word passe-t-il à l’état haut ? La fréquence de bus_clk est de 100 MHz. next_word est à l’état haut une fois par cycle complet de 2048 périodes d’horloge. 100 MHz ÷ 2048 ≈ 48828 Hz. next_word passe donc à l’état haut environ 48 828 fois par seconde.

J’ai mentionné plus haut que next_word passe à l’état haut quand la sortie de la FIFO est consommée. Voici la partie correspondante du code Verilog :

   always @(posedge bus_clk)
     begin
 [ ... ]

	if (next_word && fifo_out_valid)
	  begin
	     // The audio samples are signed integers. Change them to
	     // unsigned by adding 1024.
	     threshold_left <= fifo_out[15:5] + 1024;
	     threshold_right <= fifo_out[31:21] + 1024;
	  end
	else if (next_word) // FIFO's output not valid, keep silent
	  begin
	     threshold_left <= 0;
	     threshold_right <= 0;
	  end
 [ ... ]
    end

Observons d’abord que, lorsque next_word est à l’état haut, de nouvelles valeurs sont affectées à threshold_left et threshold_right. Si fifo_out_valid est à l’état bas, la valeur de ces deux registres devient zéro. Cela se produit quand aucune donnée n’est envoyée à la FIFO, qui devient alors empty (vide).

Si fifo_out_valid est à l’état haut, cela signifie que la sortie dout de la FIFO contient la valeur d’un échantillon audio (audio sample). Cette valeur représente le signal analogique des deux canaux stéréo. Chaque échantillon contient deux nombres signés représentés en complément à deux sur 16 bits.

L’échantillon audio du canal gauche est fourni dans fifo_out[15:0]. C’est un nombre signé compris entre -32768 et 32767. Les cinq bits de poids faible sont retirés, si bien que fifo_out[15:5] est compris entre -1024 et 1023. L’expression « fifo_out[15:5] + 1024 » est donc un nombre non signé compris entre 0 et 2047. Cette plage de valeurs se prête bien à la comparaison avec pwm_level.

Par conséquent, lorsque fifo_out[15:0] vaut -32768, threshold_left reçoit la valeur zéro. La condition « threshold_left > pwm_level » n’est jamais vraie, et pwm_left reste donc à l’état bas en permanence. À l’inverse, lorsque fifo_out[15:0] vaut 32767, threshold_left reçoit la valeur 2047. pwm_left est alors à l’état haut presque tout le temps. C’est ainsi que fifo_out[15:0] contrôle la durée pendant laquelle pwm_left est à l’état haut dans chaque impulsion. fifo_out[31:16] pilote pwm_right de la même manière.

Pour résumer tout le mécanisme : next_word est à l’état haut une fois toutes les 2048 périodes d’horloge. Quand next_word est à l’état haut, la sortie de la FIFO est récupérée et copiée dans threshold_left et threshold_right. Cela consomme la sortie de la FIFO, si bien que fifo_out_valid repasse à l’état bas. Par conséquent, si la FIFO n’est pas empty (vide), fifo_rd_en passe à l’état haut afin de lire un nouvel échantillon audio dans la FIFO.

Rappelons que le cœur IP (IP core) Xillybus remplit cette FIFO avec le contenu de sample.wav. Il y a donc un flux d’échantillons audio depuis le contenu de sample.wav jusqu’à threshold_left et threshold_right. Comme mentionné plus haut, next_word passe à l’état haut environ 48 828 fois par seconde. C’est la fréquence d’échantillonnage (sampling rate) de ce mécanisme.

threshold_left contrôle la proportion du temps pendant laquelle pwm_left est à l’état haut. Il en va de même pour threshold_right et pwm_right. Enfin, pwm_right et pwm_left sont reliés au port de sortie nommé J6 ; ce sont donc les signaux que l’on voit sur le connecteur à broches.

Notez que lorsque next_word est à l’état haut, deux choses se produisent : un échantillon audio est consommé et pwm_level recommence à compter à partir de zéro. Une impulsion est donc générée pour chaque échantillon audio.

Affichage de « PWM »

Plus haut, je vous ai suggéré d’utiliser la commande « head /dev/xillybus_read_32 » pour vérifier que le FPGA contient le bon flux de bits (bitstream). Le résultat attendu était que « PWM » s’affiche un grand nombre de fois. Cela est implémenté par cette partie du code Verilog :

   // Send the text "PWM" to reassure that the correct bitstream is used.
   assign user_r_read_32_eof = 0;
   assign user_r_read_32_empty = 0;
   assign user_r_read_32_data = 32'h0a_4d_57_50; // "PWM" + LF

Si vous regardez xillydemo.v tel qu’il était avant les modifications, vous verrez que user_r_read_32_rden, user_r_read_32_data et user_r_read_32_empty étaient connectés à la FIFO. Le cœur IP (IP core) Xillybus utilise ces signaux pour lire les données d’une FIFO et les rendre disponibles sous la forme d’un flux de données présenté via /dev/xillybus_read_32.

Avant la modification de xillydemo.v, ces signaux étaient connectés à la même FIFO que celle dans laquelle le cœur IP (IP core) Xillybus écrit. Le résultat était un bouclage (loopback) : les données écrites dans /dev/xillybus_write_32 par le logiciel étaient d’abord insérées dans la FIFO par le cœur IP Xillybus. Le cœur IP Xillybus lisait ensuite les données de la FIFO et les présentait via /dev/xillybus_read_32. Le but de ce bouclage est de servir de point de départ pour apprendre le fonctionnement de Xillybus.

Après la modification de xillydemo.v, ces signaux sont déconnectés de la FIFO. À la place, user_r_read_32_data vaut toujours 0x0a4d5750 et user_r_read_32_empty est toujours à zéro. Par ailleurs, user_r_read_32_rden est ignoré par la logique. On obtient ainsi une FIFO imaginaire qui n’est jamais empty (vide). La sortie de cette FIFO imaginaire a toujours la même valeur : 0x0a4d5750. Le cœur IP (IP core) Xillybus se comporte comme s’il existait une FIFO constamment remplie de cette valeur constante. Par conséquent, lorsqu’on lit /dev/xillybus_read_32, le mot 0x0a4d5750 arrive en boucle. Lorsque ce mot est imprimé, il est interprété comme quatre octets : 0x50, 0x57, 0x4d et 0x0a. Autrement dit, les caractères P, W, M et un saut de ligne (line feed, utilisé pour marquer la fin d’une ligne sous Linux).

Relation entre le code Verilog et les vraies broches

Le code Verilog ci-dessus connecte les signaux PWM à J6, mais comment cela aboutit-il sur le connecteur à broches ? La réponse se trouve dans xillydemo.xdc. Ce fichier fait partie du projet Vivado qui génère le flux de bits (bitstream) (dans le répertoire « vivado-essentials »).

xillydemo.xdc contient diverses informations nécessaires au bon fonctionnement du FPGA en tant que composant électronique. Ce fichier contient notamment les lignes suivantes :

[ ... ]

## J6 on board (BANK33 VADJ)
set_property PACKAGE_PIN U22  [get_ports {J6[0]}];   #J6/1  = IO_B33_LN2
set_property PACKAGE_PIN T22  [get_ports {J6[1]}];   #J6/2  = IO_B33_LP2
set_property PACKAGE_PIN W22  [get_ports {J6[2]}];   #J6/3  = IO_B33_LN3
set_property PACKAGE_PIN V22  [get_ports {J6[3]}];   #J6/4  = IO_B33_LP3
set_property PACKAGE_PIN Y21  [get_ports {J6[4]}];   #J6/5  = IO_B33_LN9
set_property PACKAGE_PIN Y20  [get_ports {J6[5]}];   #J6/6  = IO_B33_LP9
set_property PACKAGE_PIN AB22 [get_ports {J6[6]}];   #J6/7  = IO_B33_LN7
set_property PACKAGE_PIN AA22 [get_ports {J6[7]}];   #J6/8  = IO_B33_LP7

[ ... ]

La première ligne indique que le signal J6[0] doit être connecté à U22. C’est une position sur le boîtier physique du FPGA. D’après les schémas de la Smart Zynq, cette broche du FPGA est reliée à la première broche du connecteur. Les positions des autres ports sont définies de la même manière.

J’ai mentionné plus haut que n’importe quelle broche entre J6/3 et J6/34 peut être utilisée comme masse, car ces broches de sortie ont la valeur '0'. Cela est vrai parce que J6 comporte 34 bits, selon cette ligne au début de xillydemo.v :

  inout [33:0] J6,  //BANK33 VADJ

Rappelons que l’affectation de J6 est la suivante :

 assign J6 = { pwm_right, pwm_left };

Cela signifie que J6[0] vaut pwm_left et J6[1] vaut pwm_right. Et le reste ? D’après la syntaxe de Verilog, tous les autres bits reçoivent la valeur zéro.

La composante continue

Le connecteur à broches est relié aux sorties logiques du FPGA. Chacune de ces broches présente une tension d’environ 3,3 V lorsque l’état logique est '1'. Lorsque l’état logique est '0', la tension est proche de 0 V.

Si la valeur de l’échantillon audio d’origine est nulle, threshold_left et threshold_right vaudront 1024. Autrement dit, pwm_right et pwm_left seront à l’état haut pendant la moitié du temps, en moyenne. La tension moyenne (continue, DC) est donc de 3,3 V ÷ 2 = 1,65 V. Ainsi, même si les échantillons audio du fichier WAV présentent un équilibre parfait en continu (DC), le casque est soumis à une composante continue (DC bias) de 1,65 V.

Le but de la résistance de 100 Ω n’est donc pas seulement de réduire le niveau sonore, mais aussi de limiter le courant continu (DC). Même sans cette résistance, le courant électrique serait probablement sans danger, en raison de la limite intrinsèque du FPGA et de la résistance électrique du casque. Cette résistance n’est qu’une mesure de précaution.

Résumé

Ce projet a montré comment utiliser une broche de sortie numérique pour produire un signal audio analogique pouvant être connecté directement à un casque. L’accent a été mis sur l’utilisation d’un flux Xillybus pour envoyer des données depuis un logiciel vers le FPGA. Une implémentation simple d’une PWM a également été présentée.

Cette page a été traduite de l’anglais par une machine. En cas de doute, veuillez vous reporter au texte original
Copyright © 2021-2026. All rights reserved. (dcc38493)