Cette page web fait partie d’un ensemble de petits projets qui explorent les possibilités de la carte Smart Zynq.
Introduction
Le marché propose plusieurs modèles simples et bon marché de servomoteurs à courant continu, par exemple le SG90, le MG90S, le MG995 ou le MG996R. Ces moteurs sont destinés aux projets de loisir, en particulier à la construction de petits robots. Ces moteurs se pilotent avec un unique signal PWM.
Certains moteurs tournent dans une plage angulaire limitée, typiquement 180 degrés. Pour ces moteurs, le signal PWM commande la position angulaire. D’autres moteurs (souvent appelés « moteurs 360 degrés ») peuvent tourner en continu. Avec eux, le signal PWM commande la vitesse et le sens de rotation. Notez qu’un même modèle de moteur existe souvent en deux versions : une version à angle limité, et une version qui peut tourner en continu. Il est important d’y prêter attention lors de l’achat d’un moteur de ce type.
Ce tutoriel montre comment connecter jusqu’à huit servomoteurs à la carte Smart Zynq et les piloter à l’aide d’une simple commande Linux. Ce tutoriel illustre également comment utiliser les flux repositionnables (seekable streams) de Xillybus pour implémenter une interface de registres dans le FPGA.
Le câblage présenté dans ce tutoriel ne concerne qu’un seul moteur, afin de ne pas compliquer inutilement les explications et les photos. Ajouter d’autres moteurs ne demande que de raccorder des fils supplémentaires aux mêmes éléments que ceux montrés ici.
Interface électrique du moteur
La connexion vers les servomoteurs de ce type se fait par trois fils :
- Le fil brun, relié à la masse.
- Le fil rouge, relié à une alimentation +5V.
- Le fil orange, relié au signal de commande (PWM).
Le signal PWM doit avoir une période de 20 ms (50 Hz). La durée pendant laquelle l’impulsion est à l’état haut commande la position ou la vitesse angulaire du moteur.
Selon la plupart des fiches techniques (datasheets), la durée de l’impulsion doit être comprise entre 1 ms et 2 ms. Cette information est inexacte pour certains moteurs. Par exemple, la plage correcte pour le SG90 de Tower Pro (limité à 180 degrés de rotation) se situe approximativement entre 500 μs et 2450 μs. Cette plage correspond à une rotation de 180 degrés. Il est possible de faire tourner le moteur légèrement plus loin avec une impulsion allant jusqu’à 2560 μs. Ce moteur ignore toute impulsion plus longue que cette valeur.
Aussi, pour explorer la plage de durées d’impulsion auxquelles le moteur réagit, il est préférable d’expérimenter directement avec le moteur plutôt que de se fier à la fiche technique. Comme ces moteurs sont destinés aux projets de loisir, leurs caractéristiques annoncées ne sont pas toujours d’une grande précision.
Connexions électriques
Tout d’abord, le servomoteur a besoin d’une alimentation séparée pour fournir l’énergie au moteur mécanique. La tension de cette alimentation est +5V. Il n’est pas recommandé d’utiliser l’alimentation qui nourrit la carte Smart Zynq, car les moteurs provoquent souvent de brusques variations de tension. La carte Smart Zynq peut se comporter de manière peu fiable si sa tension d’alimentation est instable.
Le signal PWM sur le fil orange doit avoir approximativement la même tension que l’alimentation lorsque ce signal est à l’état haut. Autrement dit, la tension entre le fil orange et la masse doit être soit 0, soit environ +5V.
La tension de sortie de la carte Smart Zynq n’est toutefois que de 3,3 V à l’état haut. Si cette sortie est connectée directement au fil orange du moteur, il y a de bonnes chances que le moteur réponde correctement. En effet, les circuits numériques alimentés en 5 V considèrent généralement toute tension supérieure à 2,5 V comme un niveau logique '1'.
Cela dit, on ne peut pas être certain que le moteur fonctionnera de façon fiable avec une tension de 3,3 V sur le signal PWM. La solution recommandée consiste à utiliser un convertisseur de niveaux (level shifter) qui transforme la sortie de la carte Smart Zynq en 0 V ou 5 V. Dans ce tutoriel, on utilise une petite carte avec la puce TXS0108E. Cette carte ne fait qu’exposer les broches de la puce, ce qui permet d’y connecter des fils ordinaires.
La photo ci-dessous montre comment les connexions électriques sont organisées pour piloter un servomoteur : la carte Smart Zynq, en bas de la photo, est reliée au module TXS0108E avec des fils Dupont. L’autre côté de ce module est relié à l’entrée PWM du servomoteur ainsi qu’à l’alimentation séparée des moteurs (via un adaptateur pour prise d’alimentation standard). Les fils rouge et brun du moteur sont également soudés sur ce module, afin de fournir au moteur sa tension d’alimentation.
La photo ci-dessous offre une vue plus rapprochée du module TXS0108E et de ses connexions :
Remarquez le fil rouge entre OE et VA : il garantit que la validation de sortie (output enable, OE) de la puce est à l’état haut.
Les connexions vers le connecteur à broches de la Smart Zynq sont montrées sur la photo suivante. Dans cette configuration, le moteur est commandé par J6/1.
Le tableau ci-dessous résume les connexions entre les quatre éléments concernés :
| Broche du module TXS0108E | Broche Smart Zynq | Fil du moteur | Alimentation +5V | Commentaire |
|---|---|---|---|---|
| VA | J6/37 (3,3 V) | -- | -- | Relié aussi à OE |
| A4 | J6/1 | -- | -- | |
| OE | -- | -- | -- | Relié à VA |
| GND | J6/35 (GND) | Fil brun | GND | |
| VB | -- | Fil rouge | +5V | |
| B4 | -- | Fil orange | -- |
La puce TXS0108E effectue une conversion de niveau depuis son entrée A4 vers sa sortie B4. La puce possède sept autres paires de broches. La paire A4/B4 a été choisie arbitrairement pour ce tutoriel. Pour piloter huit moteurs, on peut utiliser les huit paires de broches de la puce. Cela n’est pas montré ici, car le nombre de fils nécessaires pour piloter huit moteurs rendrait les photos difficiles à comprendre.
Pour localiser le connecteur 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 utilisé dans ce tutoriel. J6/1 est donc la broche la plus proche du connecteur HDMI.
Notez que la carte Smart Zynq est alimentée séparément par l’un de ses ports USB. Remarquez aussi que la dernière broche du connecteur J6 est en 5 V : n’y connectez aucun fil.
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 4 ». Insérez à la place ceci :
assign user_r_mem_8_empty = 0;
assign user_r_mem_8_eof = 0;
assign user_w_mem_8_full = 0;
reg [7:0] reg0, reg1, reg2, reg3, reg4, reg5, reg6, reg7;
always @(posedge bus_clk)
if (user_w_mem_8_wren)
case (user_mem_8_addr[2:0])
0: reg0 <= user_w_mem_8_data;
1: reg1 <= user_w_mem_8_data;
2: reg2 <= user_w_mem_8_data;
3: reg3 <= user_w_mem_8_data;
4: reg4 <= user_w_mem_8_data;
5: reg5 <= user_w_mem_8_data;
6: reg6 <= user_w_mem_8_data;
7: reg7 <= user_w_mem_8_data;
endcase
reg [7:0] mem_8_data_reg;
assign user_r_mem_8_data = mem_8_data_reg;
always @(posedge bus_clk)
if (user_r_mem_8_rden)
case (user_mem_8_addr[2:0])
0: mem_8_data_reg <= reg0;
1: mem_8_data_reg <= reg1;
2: mem_8_data_reg <= reg2;
3: mem_8_data_reg <= reg3;
4: mem_8_data_reg <= reg4;
5: mem_8_data_reg <= reg5;
6: mem_8_data_reg <= reg6;
7: mem_8_data_reg <= reg7;
endcase
servo_pwm servo_pwm_i [7:0]
(
.clk(bus_clk),
.rst(quiesce),
.pwm_signal(J6[7:0]),
.pwm_width( { reg7, reg6, reg5, reg4, reg3, reg2, reg1, reg0 } )
);
Il est également possible de télécharger le xillydemo.v modifié depuis ce lien.
Créez un nouveau fichier, servo_pwm.v, dans le répertoire verilog/src/ du projet, avec le contenu suivant (utilisez le copier-coller ou téléchargez-le depuis ce lien).
module servo_pwm
(
input clk,
input rst,
output reg pwm_signal,
input [7:0] pwm_width
);
reg [9:0] enable_count;
reg enable;
reg [10:0] pwm_count;
reg [8:0] threshold;
always @(posedge clk)
begin
if (enable_count == 999)
begin
enable_count <= 0;
enable <= 1;
end
else
begin
enable_count <= enable_count + 1;
enable <= 0;
end
threshold <= pwm_width + 50; // Add 0.5 ms to PWM width
if (enable)
begin
if (pwm_count == 1999)
pwm_count <= 0;
else
pwm_count <= pwm_count + 1;
pwm_signal <= (pwm_count < threshold);
end
if (rst)
begin
enable_count <= 0;
pwm_count <= 0;
pwm_signal <= 0;
end
end
endmodule
Ajoutez ensuite le fichier au projet Vivado : cliquez sur File > Add Sources… et choisissez « Add or create design sources ». Cliquez ensuite sur Next. Cliquez sur le bouton « Add Files » et choisissez le fichier nommé « servo_pwm.v » dans le répertoire verilog/src/. Cliquez enfin sur le bouton « Finish ».
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 flux de bits (bitstream) 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).
Pilotage du servomoteur
Les étapes de cette section se déroulent dans un terminal sur la carte Smart Zynq.
Tout d’abord, placez-vous dans le répertoire des applications de démonstration de Xillybus, puis compilez-les :
# cd ~/xillybus/demoapps/ # make gcc -g -Wall -O3 memwrite.c -o memwrite gcc -g -Wall -O3 memread.c -o memread gcc -g -Wall -O3 streamread.c -o streamread gcc -g -Wall -O3 streamwrite.c -o streamwrite gcc -g -Wall -O3 -pthread fifo.c -o fifo
Le FPGA contient huit registres destinés au pilotage des servomoteurs. Au départ, leur valeur est nulle. Voici comment afficher ces valeurs au format décimal.
# hexdump -v -n 8 -e '8/1 "%u " "\n" ' /dev/xillybus_mem_8 0 0 0 0 0 0 0 0
Une commande hexdump plus simple permet d’afficher ces mêmes valeurs au format hexadécimal :
# hexdump -v -n 8 -C /dev/xillybus_mem_8 00000000 00 00 00 00 00 00 00 00 |........| 00000008
Le programme memwrite (dans le répertoire xillybus/demoapps/) peut être utilisé pour piloter le moteur. Par exemple, la commande suivante met la valeur du registre 0 à 120 :
# ./memwrite /dev/xillybus_mem_8 0 120
Cela modifie la position ou la vitesse du moteur connecté à J6/1 (c’est-à-dire comme montré sur les photos ci-dessus).
La largeur de l’impulsion PWM dépend de la valeur de ce registre selon la formule suivante :
t = 500 + (x * 10)
Dans cette formule, t est donné en microsecondes et x est la valeur du registre correspondant. La largeur d’impulsion par défaut est donc de 500 μs (c’est-à-dire pour x=0). La commande ci-dessus a mis la valeur du registre à 120. La largeur de l’impulsion est donc devenue 1700 μs.
Chaque registre tient sur un octet, sa valeur est donc comprise entre 0 et 255. En conséquence, l’impulsion de chaque moteur peut être réglée entre 500 μs et 3050 μs. Notez qu’une partie de cette plage est considérée comme invalide par la plupart des moteurs. Il est préférable d’essayer différentes valeurs avec chaque moteur et d’observer sa réaction.
Pour piloter un moteur connecté sur une autre broche, écrivez dans un autre registre. Par exemple, si le moteur est connecté à la broche J6/4 de la Smart Zynq, on peut par exemple utiliser cette commande :
# ./memwrite /dev/xillybus_mem_8 3 50
Cette commande met la largeur de l’impulsion à 1000 μs (soit 1 ms).
Après ces deux commandes, on peut constater les changements en relisant les valeurs :
# hexdump -v -n 8 -e '8/1 "%u " "\n" ' /dev/xillybus_mem_8 120 0 0 50 0 0 0 0
Ceci affiche les valeurs au format décimal. Pour le format hexadécimal :
# hexdump -v -n 8 -C /dev/xillybus_mem_8 00000000 78 00 00 32 00 00 00 00 |x..2....| 00000008
Comment memwrite fonctionne
Le code source de memwrite est le fichier memwrite.c dans le répertoire ~/xillybus/demoapps/. Ce programme montre comment accéder aux registres à l’aide des flux repositionnables (seekable streams) de Xillybus. Si vous voulez comprendre comment ce programme fonctionne, je vous suggère d’examiner son code source. Je ne vais parcourir que les deux parties importantes ci-dessous.
Le programme ouvre le fichier dont le nom est donné en premier argument. Le descripteur de fichier (file descriptor) correspondant est stocké dans la variable @fd.
Le programme appelle ensuite la fonction lseek() de la manière suivante :
if (lseek(fd, address, SEEK_SET) < 0) {
perror("Failed to seek");
exit(1);
}
La variable @address contient le deuxième argument du programme. Dans le premier exemple d’utilisation de memwrite ci-dessus, c’était 0. Dans le deuxième exemple, c’était 3. De manière générale, lseek() sert à se positionner à un endroit précis dans un fichier. Ici, la position dans le fichier correspond au numéro du registre que l’on veut atteindre.
Ensuite, le programme fait un appel à allwrite() :
allwrite(fd, &data, 1);
Ceci écrit un octet dans le fichier. allwrite() est définie dans memwrite.c et ressemble à la fonction bien connue write(). Ainsi, au lieu d’utiliser allwrite(), on pourrait presque écrire ceci :
write(fd, &data, 1);
La différence est que write() ne garantit pas que les données soient effectivement écrites. La fonction allwrite(), en revanche, garantit que les données sont bien écrites dans le fichier.
Dans notre cas, l’appel à allwrite() écrit la valeur du troisième argument du programme. Autrement dit, c’est la nouvelle valeur du registre.
Pour plus de détails sur l’API de Xillybus côté hôte, reportez-vous à la documentation sur ce sujet, en particulier la section 6.1.
Explication du code Verilog
Je vais d’abord montrer comment le code Verilog dans xillydemo.v implémente les registres dans le FPGA. Ensuite, je présenterai l’implémentation de l’impulsion PWM.
L’API de Xillybus pour implémenter des registres matériels est expliquée en détail dans le guide de conception FPGA de Xillybus.
Les fils liés aux registres étaient déjà définis dans le fichier xillydemo.v d’origine :
// Wires related to /dev/xillybus_mem_8
wire user_r_mem_8_rden;
wire user_r_mem_8_empty;
wire [7:0] user_r_mem_8_data;
wire user_r_mem_8_eof;
wire user_r_mem_8_open;
wire user_w_mem_8_wren;
wire user_w_mem_8_full;
wire [7:0] user_w_mem_8_data;
wire user_w_mem_8_open;
wire [4:0] user_mem_8_addr;
wire user_mem_8_addr_update;
Ces fils sont connectés au cœur IP (IP core) Xillybus dans le cadre de son instantiation (instantiation) :
xillybus xillybus_ins (
// Ports related to /dev/xillybus_mem_8
// FPGA to CPU signals:
.user_r_mem_8_rden(user_r_mem_8_rden),
.user_r_mem_8_empty(user_r_mem_8_empty),
.user_r_mem_8_data(user_r_mem_8_data),
.user_r_mem_8_eof(user_r_mem_8_eof),
.user_r_mem_8_open(user_r_mem_8_open),
// CPU to FPGA signals:
.user_w_mem_8_wren(user_w_mem_8_wren),
.user_w_mem_8_full(user_w_mem_8_full),
.user_w_mem_8_data(user_w_mem_8_data),
.user_w_mem_8_open(user_w_mem_8_open),
// Address signals:
.user_mem_8_addr(user_mem_8_addr),
.user_mem_8_addr_update(user_mem_8_addr_update),
[ ... ]
.quiesce(quiesce)
);
Examinons maintenant le code Verilog ajouté spécifiquement pour ce projet. Il commence par cette partie, qui garantit qu’aucun contrôle de flux (flow control) n’est appliqué aux échanges de données avec l’hôte :
assign user_r_mem_8_empty = 0;
assign user_r_mem_8_eof = 0;
assign user_w_mem_8_full = 0;
Les huit registres sont déclarés ensuite :
reg [7:0] reg0, reg1, reg2, reg3, reg4, reg5, reg6, reg7;
La partie suivante implémente les opérations d’écriture vers les registres : lorsque @user_w_mem_8_wren est à l’état haut, une nouvelle valeur est écrite dans l’un des registres. @user_mem_8_addr sélectionne le registre concerné. @user_w_mem_8_wren et @user_mem_8_addr sont tous deux des sorties du cœur IP (IP core) Xillybus.
always @(posedge bus_clk)
if (user_w_mem_8_wren)
case (user_mem_8_addr[2:0])
0: reg0 <= user_w_mem_8_data;
1: reg1 <= user_w_mem_8_data;
2: reg2 <= user_w_mem_8_data;
3: reg3 <= user_w_mem_8_data;
4: reg4 <= user_w_mem_8_data;
5: reg5 <= user_w_mem_8_data;
6: reg6 <= user_w_mem_8_data;
7: reg7 <= user_w_mem_8_data;
endcase
Ensuite, la lecture des registres est implémentée : lorsque @user_r_mem_8_rden est à l’état haut, @user_r_mem_8_data est mis à jour pour contenir la valeur de l’un des registres. @user_mem_8_addr sélectionne le registre dont on lit la valeur.
reg [7:0] mem_8_data_reg;
assign user_r_mem_8_data = mem_8_data_reg;
always @(posedge bus_clk)
if (user_r_mem_8_rden)
case (user_mem_8_addr[2:0])
0: mem_8_data_reg <= reg0;
1: mem_8_data_reg <= reg1;
2: mem_8_data_reg <= reg2;
3: mem_8_data_reg <= reg3;
4: mem_8_data_reg <= reg4;
5: mem_8_data_reg <= reg5;
6: mem_8_data_reg <= reg6;
7: mem_8_data_reg <= reg7;
endcase
Enfin, l’instanciation (instantiation) du module servo_pwm a lieu.
servo_pwm servo_pwm_i [7:0]
(
.clk(bus_clk),
.rst(quiesce),
.pwm_signal(J6[7:0]),
.pwm_width( { reg7, reg6, reg5, reg4, reg3, reg2, reg1, reg0 } )
);
Notez que cette instanciation (instantiation) crée huit copies identiques de servo_pwm : la première copie est reliée à reg0 et à J6[0], la deuxième à reg1 et à J6[1], etc.
Nous allons maintenant examiner le module servo_pwm. Il commence par la déclaration de ses ports et de ses registres :
module servo_pwm
(
input clk,
input rst,
output reg pwm_signal,
input [7:0] pwm_width
);
reg [9:0] enable_count;
reg enable;
reg [10:0] pwm_count;
reg [8:0] threshold;
Rappelons que @pwm_width reçoit la valeur du registre, mise à jour à l’aide de la commande memwrite sur l’hôte. @pwm_signal est connecté à la broche de la carte Smart Zynq : c’est donc le signal qui arrive au moteur sur le fil orange.
La partie suivante du module implémente un signal impulsionnel (strobe) : @enable est à l’état haut une fois toutes les 1000 périodes d’horloge. @clk est à 100 MHz. @enable est donc à l’état haut une fois toutes les 10 μs.
always @(posedge clk)
begin
if (enable_count == 999)
begin
enable_count <= 0;
enable <= 1;
end
else
begin
enable_count <= enable_count + 1;
enable <= 0;
end
Comme mentionné plus haut, la valeur du registre, comprise entre 0 et 255, correspond à une largeur d’impulsion PWM comprise entre 500 μs et 3050 μs (la formule donnant la largeur en microsecondes était 500 + x * 10).
D’où l’expression suivante pour @threshold :
threshold <= pwm_width + 50; // Add 0.5 ms to PWM width
Ce registre contient le nombre de fois où @enable est à l’état haut pendant l’impulsion PWM (c’est-à-dire lorsque le signal PWM est à l’état haut). Cette expression traduit le fait que la valeur du registre représente la durée de l’impulsion PWM en unités de 10 μs. L’ajout de 50 à la valeur du registre correspond à la largeur d’impulsion minimale de 500 μs.
On utilise alors @enable comme validation d’horloge (clock enable) pour cette partie :
if (enable)
begin
if (pwm_count == 1999)
pwm_count <= 0;
else
pwm_count <= pwm_count + 1;
pwm_signal <= (pwm_count < threshold);
end
@pwm_count compte de 0 à 1999, puis recommence à zéro. Ce compteur ne change qu’une fois toutes les 10 μs ; il effectue donc un cycle complet toutes les 20000 μs = 20 ms. Autrement dit, la période de répétition de l’impulsion PWM est bien de 20 ms, comme requis.
@pwm_signal reste à l’état haut tant que @pwm_count est inférieur à @threshold. C’est ainsi que la largeur de l’impulsion est commandée par le registre.
La partie finale du module consiste à réinitialiser certains registres :
if (rst)
begin
enable_count <= 0;
pwm_count <= 0;
pwm_signal <= 0;
end
end
endmodule
Lorsque @rst est à l’état haut, cette partie a priorité sur tout ce qui précède. @rst joue donc le rôle d’un signal de réinitialisation (reset).
Relation entre le code Verilog et les vraies broches
Le code Verilog ci-dessus utilise le port bidirectionnel (inout) nommé J6. Mais comment les connexions de ce port aboutissent-elles 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 port 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.
Résumé
Ce tutoriel a montré comment piloter des servomoteurs à l’aide de la carte Smart Zynq et de l’API de registres de Xillybus. L’application hôte ainsi que le code Verilog correspondant ont été présentés.
Ce tutoriel peut également servir de base à d’autres applications nécessitant la commande de matériel à l’aide de registres.


