MKR Vidor 4000 : Programmation du FPGA (Partie 2)

MKR Vidor 4000 : Programmation du FPGA (Partie 2)

Dans cette deuxième partie du tutoriel sur la programmation du FPGA, nous allons implémenter un petit module PWM avec sa configuration via une interface SPI.

– Notez que je décline toutes responsabilités quant aux conséquences que pourraient avoir le suivi de ce tutoriel. Les composants de la carte sont sensibles et n’apprécieront pas une mauvaise tension sur leurs broches ou que vous mettiez celles-ci en court circuit suite à une mauvaise configuration chargée dans le FPGA.
– Le module SPI a été conçu spécifiquement pour ce tutoriel et de manière à être décrit de façon didactique. Il n’est pas optimisé et je ne garantie pas sa robustesse.

Les deux paramètres d’une modulation à largeur d’impulsion (PWM) sont sa fréquence de modulation et son rapport cyclique.

Rapport cyclique.

Si nous divisons la période de modulation en 256 plus petites périodes, nous pouvons en utilisant un compteur 8 bits et un comparateur faire varier le rapport cyclique (t1 / T) :

  

 

Sur un nouveau schéma Quartus « TUTO_Schematic2.bdf », insérez une instance de LPM_COUNTER et de LPM_COMPARE en suivant la procédure que vous avez vu dans la 1ère partie de ce tutoriel.

Entrez la valeur 8 pour le paramètre LPM_WIDTH des deux instances.

Reliez (voir schéma ci-dessous) :

  • la sortie q[] du compteur à l’entrée dataa[] du comparateur (outil « Bus Tool »),
  • l’entrée datab[] du comparateur à un bus d’entrée que vous nommerez iPWM_PULS[7..0] (« Pin Tool » et « Bus Tool »),
  • la sortie alb (less than) du comparateur à une sortie oPWM_OUT (« Pin Tool » et « Node Tool »).

En modifiant la valeur d’entrée iPWM_PULS, vous modifierez le rapport cyclique (0 pour 0 %, 255 pour 100%).

Fréquence de modulation.

Dans la première partie du tutoriel , je vous présentais comment générer des fréquences différentes en utilisant les différentes sorties d’un compteur.

Chacune des sorties (n) ayant une fréquence propre égale à :
Freq_bit_n = ( Freq_entrée / (2 ^ (n+1)))

Si pour le clignotement de la LED nous avions choisi de prendre n=25 (environ 1Hz), nous souhaitons maintenant pouvoir choisir la valeur de n à prendre en compte.

Vous allez donc insérez un deuxième compteur LPM_COUNTER dont l’horloge sera l’entrée iPWM_CLK (« Pin Tool »). Mettez cette fois ci 16 en valeur de paramètre LPM_WIDTH.

Insérez ensuite un multiplexeur LPM_MUX avec le paramétrage suivant :

LPM_SIZE = 16 (Nous nous limiterons à des valeurs de n comprises entre 0 et 15)

LPM_WIDTH = 1 (Nous n’avons qu’un seul bus de 16 bits en entrée)

Reliez ensuite la sortie du compteur q[] à l’entrée data[][], et une entrée iCLK_SEL[3..0] à l’entrée sel[].

Pour finir, il ne vous reste plus qu’à connecter (« Node Tool ») les deux blocs que vous venez de créer (sortie result[] du multiplexeur vers entrée d’horloge de votre premier compteur).

Vous devriez obtenir le schéma suivant :

 

Schématique vs HDL

Nous venons de voir comment en utilisant l’outil de schéma de Quartus, nous pouvons spécifier des modules assez simples.

Cette méthode présente de nombreux avantages :

  • aspect visuel
  • recopie d’un schéma électronique utilisant des fonctions logiques simples : portes AND/OR/.. , bascules, circuits logiques 74xxx,…
  • interconnections de modules plus importants.

Elle ne peut par contre raisonnablement être envisagée, si vous devez spécifier des systèmes plus complexes, voir même de petites machines d’état.

Vous devrez pour cela utiliser des « langages de description du matériel » (HDL en anglais).

C’est le langage VERILOG qui a été retenu par ARDUINO pour la programmation du FPGA.

Voici ci dessous à quoi ressemble une implémentation en VERILOG du schéma que vous venez de créer sous Quartus :

Note : Il existe différentes conventions d’écriture pour le code VERILOG. J’ai choisi de suivre celle utilisée dans le code mis à disposition par ARDUINO dans un soucis d’homogénéité.

 

module TUTO_Schematic2 (

    	input iPWM_CLK, 	// Entree d'horloge du module PWM
    	input [3:0] iCLK_SEL,	// Valeur du prediviseur
    	input [7:0] iPWM_PULS, // Longueur du creneau 
		output oPWM_OUT	// Signal de sortie PWM

);

reg [15:0] 	rPRES_COUNT;
reg [7:0] 	rPWM_COUNT;

wire wPWM_COUNT_CLK;

assign wPWM_COUNT_CLK = rPRES_COUNT[ iCLK_SEL ];
assign oPWM_OUT = (rPWM_COUNT < iPWM_PULS ) ? 1 : 0 ;

always @(posedge iPWM_CLK)
begin
	rPRES_COUNT <= rPRES_COUNT + 1 ;
end

always @(posedge wPWM_COUNT_CLK)
begin
	rPWM_COUNT <= rPWM_COUNT + 1 ;
end

endmodule

Configuration du module PWM / Liaison SPI.

Le bloc PWM que nous venons de créer serait bien inutile si nous ne pouvions le configurer (fréquence de modulation et rapport cyclique).

Le plus simple à ce stade est de transmettre cette configuration depuis le microcontrôleur SAMD21.

Une liaison SPI me semble être le meilleur choix, d’une part pour la vitesse élevée que permet ce protocole, mais également parce que celle-ci peut être partagée avec d’autres périphériques.

Pour simplifier les choses nous allons fixer CPHA = 0, CPOL = 0 avec le bit de poids fort MSB transmis en premier (voir la description du protocole ici).

Le 1er octet que nous transmettrons sera un octet de commande qui indiquera au FPGA le module destinataire, l’adresse de la donnée pour ce module, et s’il s’agit d’une lecture ou d’une écriture.

Le microcontrôleur enverra ensuite les données à écrire dans le module concerné.

Nous devrions donc recevoir du microcontrôleurs les signaux ci-dessous :

Dans un premier temps, on souhaite connaître quel bit de l’octet nous sommes en train de traiter, et s’il s’agit ou non du premier octet (la commande).

On va pour cela utiliser le bloc suivant :

 

reg r1stBYTE_n;	// 0 : octet de commande, 1 : octets de données
reg [2:0] rCPT_BIT;  // Compteur de bits (0-7)

// Bloc à exécuter sur le front descendant de l'horloge ou à la fin de la transmission
always @(negedge iSPI_CLK or posedge iSPI_SS_n)
begin
    // Si fin de transmission, on réinitialise les registres
    if (iSPI_SS_n == 1)
    begin
        rCPT_BIT <= 3'b0;
        r1stBYTE_n <= 1'b0;
    end
    else
    // Sinon transmission en cours
    begin
        rCPT_BIT <= rCPT_BIT + 1; // On incrémente le compteur de bits
        if (rCPT_BIT == 3'b111) // Test fin de transmission d'un octet
        begin
            r1stBYTE_n <= 1'b1;
        end
    end
end

Cela nous donne deux nouvelles données : rCPT_BIT[2:0] et r1stBYTE_n

 

Note : Il me paraît important d’aborder ici une particularité des langages HDL. A l’inverse des langages de programmation pour lesquels le microcontrôleur va exécuter le code de manière séquentielle, VERILOG est un langage descriptif. Cela implique que toutes les lignes seront évaluées/exécutées simultanément. Aussi l’ordre dans lequel nous avons écrit ci-dessus le test et l’incrément du registre rCPT_BIT n’a aucune importance. Avec un langage de programmation la condition (rCPT_BIT == 3’b111) aurait été évaluée APRES l’incrémentation de rCPT_BIT. Le signal r1stBYTE_n serait donc monté un coup d’horloge plus tôt.

Il nous faut maintenant mémoriser chaque bit entrant iSPI_IN sur le front montant de l’horloge iSPI_CLK:

  • soit dans un registre de commande pour les bits du 1er octet (rLATCH_CMD[7:0]). La commande devra rester valide durant le reste de la communication.
    if (r1stBYTE_n == 1'b0)
    	rLATCH_CMD[7-rCPT_BIT] <= iSPI_IN;
    
  • soit dans un registre de donnée qui devra être valide au moment de l’écriture vers le périphérique spécifié par la commande.

    if (r1stBYTE_n == 1'b1)
    
    		if (rCPT_BIT == 3'b111)
    				rBYTE_READY <= 1'b1;
    
    		else
    			begin
    				rLATCH_DATA[6-rCPT_BIT] <= iSPI_IN;
    				rBYTE_READY <= 1'b0;
    			end
    

On n’enregistre ici dans rLATCH_DATA que les 6 bits de poids fort de l’octet de donnée (Le bit de poids faible est iSPI_IN au moment ou nous positionnons rBYTE_READY à 1).

 

assign oSPI_RCV_BYTE = {rLATCH_DATA[6:0],iSPI_IN}; 

 

Puisque nous souhaitons pouvoir écrire plusieurs données successives, il faut décomposer le signal rBYTE_READY en deux signaux temporellement distincts :

  • oSPI_WRITE_SIG qui correspond à l’écriture en mémoire. Il est valide sur une première période de rBYTE_READY dans le cas d’une demande d’écriture (rLATCH_CMD[7] == 1’b1).
    assign oSPI_WRITE_SIG = (rBYTE_READY == 1'b1) && (rLATCH_CMD[7] == 1'b1) && (rCPT_BIT == 3'b111)? 1 : 0;

     

  • oSPI_INC_WRADDR qui signale au périphérique adressé qu’il faut passer à l’adresse suivante. Il est valide sur une deuxième période de rBYTE_READY dans le cas d’une demande d’écriture (rLATCH_CMD[7] == 1’b1).
    assign oSPI_INC_WRADDR = (rBYTE_READY == 1'b1) && (rLATCH_CMD[7] == 1'b1) && (rCPT_BIT == 3'b000) ? 1 : 0;

     

Cela nous donnera au final le chronogramme :

Et voici la totalité du code  :

module SPISlave (
    input iSPI_CLK,		
    input iSPI_SS_n,
    input iSPI_IN,
	 
	 input [7:0] iSPI_SEND_BYTE,
	 
    output oSPI_OUT,
	 
	 output [4:0] oSPI_PERIPH_SLCT,

	 output oSPI_WRITE_SIG,
//	 output oSPI_READ_SIG,

	 output oSPI_INC_WRADDR,
//	 output oSPI_INC_RDADDR,
	 
	 output [7:0] oSPI_RCV_BYTE,
	 output [7:0] oSPI_RCV_CMD
    );
	 
reg [7:0] rLATCH_CMD;
reg [6:0] rLATCH_DATA;
reg [2:0] rCPT_BIT;
reg r1stBYTE_n;
reg rBYTE_READY;	 

assign oSPI_RCV_CMD = rLATCH_CMD;
assign oSPI_WRITE_SIG  = (rBYTE_READY == 1'b1) && (rLATCH_CMD[7] == 1'b1) && (rCPT_BIT == 3'b111)? 1 : 0;
assign oSPI_INC_WRADDR = (rBYTE_READY == 1'b1) && (rLATCH_CMD[7] == 1'b1) && (rCPT_BIT == 3'b000) ? 1 : 0;
//assign oSPI_INC_RDADDR = (rBYTE_READY == 1'b1) && (rCPT_BIT == 3'b111) ? 1 : 0;
//assign oSPI_READ_SIG = (rCPT_BIT == 3'b000) && (iSPI_SS_n == 1'b0) ? 1 : 0;
assign oSPI_RCV_BYTE = {rLATCH_DATA[6:0],iSPI_IN};	 
assign oSPI_OUT = (iSPI_SS_n == 1) ? 'bZ : iSPI_SEND_BYTE[7-rCPT_BIT];
assign oSPI_PERIPH_SLCT = (iSPI_SS_n == 1) ? 0 : rLATCH_CMD[6:3];

initial
	begin
		rLATCH_CMD <= 7'b0;
		r1stBYTE_n <= 1'b0;
		rLATCH_DATA <= 7'b0;
		rBYTE_READY <= 1'b0;
		rCPT_BIT <= 3'b0;
	end
	 
always @(negedge iSPI_CLK or posedge iSPI_SS_n)
begin
	if (iSPI_SS_n == 1)
		begin
			rCPT_BIT <= 3'b0;
			r1stBYTE_n <= 1'b0;
		end
   else
		begin
			rCPT_BIT <= rCPT_BIT + 1;
			if (rCPT_BIT == 3'b111)
				r1stBYTE_n <= 1'b1;
		end
end
	 	 
always @(posedge iSPI_CLK or posedge iSPI_SS_n)
begin
	if (iSPI_SS_n == 1)
		begin
				rLATCH_CMD <= 7'b0;
				rLATCH_DATA <= 7'b0;
				rBYTE_READY <= 1'b0;
		end
	else
		if (r1stBYTE_n == 1'b0)
				rLATCH_CMD[7-rCPT_BIT] <= iSPI_IN;
		else
			if (rCPT_BIT == 3'b111)
					rBYTE_READY <= 1'b1;
			else
				begin
					rLATCH_DATA[6-rCPT_BIT] <= iSPI_IN;
					rBYTE_READY <= 1'b0;
				end
end
	 
endmodule

Pour l’intégrer à votre projet Quartus faites File → New

Choisissez « Verilog HDL File »

Copiez/Collez le code et enregistrez le fichier avec le nom SPISlave.v

Enfin il vous faut intégrer les modules dans le fichier MKRVIDOR4000_top.v en remplaçant l’ancienne instance TUTO_schematic vue dans la 1ère partie du tutoriel, par le code suivant.

/////PWM &amp; SPI MODULES //////////////////////////////////

wire wPWM_OUTPUT;
wire [7:0] wSPI_RCV_BYTE;
wire [7:0] wSPI_RCV_CMD;

reg [7:0] rPWM_REG1; // ON/OFF et prédiviseur
reg [7:0] rPWM_REG2; // Rapport cyclique

// N'est pas utilisé pour le moment. Peut servir à remonter des infos au micro par SPI
reg [7:0] rFPGA_STATUS;


// Sur un front montant du signal d'écriture on met a jour le registre SPI sélectionné
always @(posedge wSPI_WRITE_SIG)
begin
	if (wSPI_RCV_CMD[6:3] == 4'b0001)     // Périphérique selectionné = PWM
		if (wSPI_RCV_CMD[0] == 1'b0)       // Choix du registre à écrire en fonction de l'adresse
				rPWM_REG1 <= wSPI_RCV_BYTE;
		else
				rPWM_REG2 <= wSPI_RCV_BYTE;
		
end


// On ne valide la sortie PWM que si le bit 7 du registre 1 (PWM ON/OFF) est à 1
assign bMKR_D[6] = (rPWM_REG1[7] == 1) ? wPWM_OUTPUT : 1'bz;

TUTO_Schematic2 TUTO_Schematic2_inst
(
	.iPWM_CLK(wOSC_CLK) ,	    // L'entrée d'horloge est reliée à l'oscillateur interne 80Mhz
	.iCLK_SEL(rPWM_REG1[3:0]),  // Le prédiviseur correspond au 4 bits de poids faible du registre 1
	.iPWM_PULS(rPWM_REG2[7:0]), // Le rapport cyclique est dans le registre 2
	.oPWM_OUT(wPWM_OUTPUT) 	    
);
  
SPISlave SPISlave_inst(

    .iSPI_CLK(bMKR_D[9]),
    .iSPI_SS_n(iSAM_INT),
    .iSPI_IN(bMKR_D[8]),
    .oSPI_OUT(bMKR_D[10]),
	 
	 
	 .oSPI_WRITE_SIG(wSPI_WRITE_SIG),

//	 .oSPI_INC_WRADDR(wSPI_INC_WRADDR),
	 
	 .iSPI_SEND_BYTE(rFPGA_STATUS),
	 
	 .oSPI_RCV_BYTE(wSPI_RCV_BYTE),
	 .oSPI_RCV_CMD(wSPI_RCV_CMD)
);  
    
/////////////////////////////////////////////////////////////  

Il ne vous reste plus qu’à compiler le projet Quartus, et convertir le fichier MKRVIDOR4000.ttf généré en app.h.

Vous trouverez dans la page de téléchargement l’intégralité du projet Quartus ainsi qu’un sketch d’exemple pour l’IDE ARDUINO. Cet exemple fait varier la PWM de 0 à 75%.

 

 

2 réponses

  1. DjTGv dit :

    Bonjour Philippe,

    Merci pour ce nouveau tuto ; génial en perspective car il est rempli de nouvelles notions tels que : le principe de fonctionnement du langage HDL, la génération d’un chronogramme avec un FPGA.

    Néanmoins, j’en appréhende le principe.

    Cela peut paraitre simple comme demande mais un schéma électronique avec le dispositif à mettre en œuvre afin de vérifier le fonctionnement des paramétrages de la fréquence de modulation et son rapport cyclique serait un plus pour un néophyte.

    • philippe dit :

      Bonjour DjTGv,

      merci pour ce retour.

      Je n’ai pas sous la main de montage a base de PWM que j’aurais pu tester (ce sont souvent des contrôles de vitesse moteur)

      Une simple LED en série avec une résistance (même schéma que pour la partie 1 du tutoriel), permettra déjà de vérifier un fonctionnement basic :
      -dans le sketch ARDUINO, les valeurs basse et haute de PWMPuls vont affecter la luminosité.
      -le delay(50), la rapidité avec laquelle la LED s’allume ou s’éteint
      -le changement de fréquence de modulation, n’aura par contre pas d’effet visible

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *