TP 10 — Les servomoteurs Dynamixel
Objectifs
comprendre une liaison série half-duplex : un seul fil, deux sens ;
construire et décoder des trames d’un protocole réel, avec somme de contrôle ;
interroger un servomoteur AX-18A : présence, modèle, tension, température ;
le commander en position, puis en rotation continue (mode roue).
Durée indicative : deux séances de 3 h. Prérequis : TP 5 — La liaison série avec le PC.
Matériel : un servomoteur AX-18A (protocole 1.0, 1 Mbit/s), son câble à trois fils, et la batterie (les servomoteurs sont alimentés en 12 V par la carte).
Avertissement
Si le servomoteur est monté sur un robot, posez le robot sur une cale, roues dans le vide : en mode roue, il tourne sans limite.
Ce qu’il faut savoir
Un bus sur un seul fil
Les servomoteurs Dynamixel AX communiquent sur un seul fil de données, à 1 Mbit/s, en série asynchrone 8N1 comme au TP 5. Le fil sert dans les deux sens, chacun son tour : c’est le half-duplex. Plusieurs servomoteurs se branchent en chaîne sur le même fil ; chacun a un identifiant (1 à 253).
La carte relie le fil à l’UART par un tampon à trois états (SN74AHCT125) :

Le bus Dynamixel : un seul fil de données, piloté par un tampon à trois états.
Trois conséquences, qui sont les pièges de ce TP :
/OE est actif à l’état bas. Au repos, PA10 doit être à 1. Si la carte reste en émission, le servomoteur ne peut jamais répondre.
On s’entend parler. PA11 lit le fil en permanence, y compris pendant que la carte émet : chaque trame envoyée revient en écho sur RX, et doit être jetée avant de lire la réponse.
Passer en écoute au bon moment : seulement quand le dernier octet est entièrement parti, sinon il est tronqué.
Le connecteur marqué « groupe 2 » sur la carte, celui des roues du robot, est relié à UART4 (PA12, PA11, PA10). La sérigraphie est croisée par rapport aux noms des UART : c’est noté dans la documentation de la carte.
Le protocole 1.0
Une instruction envoyée au servomoteur :
FF FF ID LEN INST P1 … Pn CKS
LEN = n + 2 CKS = ~(ID + LEN + INST + P1 + … + Pn) (sur 8 bits)
Sa réponse (status packet) :
FF FF ID LEN ERR P1 … Pn CKS
Trois instructions suffisent :
Instruction |
Code |
Paramètres |
|---|---|---|
PING |
0x01 |
aucun ; le servomoteur répond par une trame vide |
READ |
0x02 |
adresse du premier registre, nombre d’octets à lire |
WRITE |
0x03 |
adresse du premier registre, puis les octets à écrire |
Les registres utiles de l’AX-18A (valeurs sur 2 octets : poids faible en premier) :
Registre |
Adresse |
Taille |
Sens |
|---|---|---|---|
Numéro de modèle |
0 |
2 |
18 pour un AX-18A |
Identifiant (ID) |
3 |
1 |
1 en sortie d’usine |
Limites d’angle CW / CCW |
6 / 8 |
2 / 2 |
toutes deux à 0 : mode roue (rotation continue) |
Couple activé |
24 |
1 |
1 : le moteur tient sa position |
LED |
25 |
1 |
1 : allumée |
Position voulue |
30 |
2 |
0 à 1023, pour 0 à 300° |
Vitesse |
32 |
2 |
en mode roue : 0 à 1023 dans un sens, 1024 à 2047 dans l’autre |
Position actuelle |
36 |
2 |
0 à 1023 |
Tension |
42 |
1 |
en dixièmes de volt |
Température |
43 |
1 |
en degrés Celsius |
Étape 1 — Configurer l’UART et le bus
Créez un projet
tp10_dynamixelcomme au TP 5 (UART5 etprintfpour afficher les résultats).Dans Connectivity → UART4, choisissez Asynchronous, sur PA12 (TX) et PA11 (RX). Baud Rate : 1000000, 8 bits, sans parité, 1 bit d’arrêt.
Configurez PA10 en GPIO_Output, label
DXL_OE, avec GPIO output level sur High : au démarrage, la carte écoute.Générez le code.
Question
À 1 Mbit/s, combien de temps dure une trame PING (6 octets) ?
Réponse à la question 1
1. La durée d’un PING. Une trame PING fait 6 octets, chacun envoyé en 10 bits (départ, 8 données, arrêt), à 1 Mbit/s, soit 1 µs par bit :
6 × 10 bits × 1 µs = 60 µs
Le servomoteur attend ensuite un court délai (return delay time, 500 µs par défaut sur l’AX, réglable) avant de répondre par 6 octets de plus. Un aller-retour complet prend donc de l’ordre d’une demi-milliseconde : c’est ce qui limite le nombre de servomoteurs qu’on peut interroger chaque seconde.
Étape 2 — Envoyer une trame et lire la réponse
Écrivez une fonction transaction(id, inst, params, n, reponse, m) qui :
construit la trame et sa somme de contrôle ;
passe en émission (
DXL_OEà 0), envoie la trame avecHAL_UART_Transmit— qui ne rend la main qu’une fois le dernier octet parti — puis repasse en écoute (DXL_OEà 1) ;jette l’écho : pendant l’émission, les octets relus se sont accumulés et ont mis l’UART en erreur de débordement. Effacez-la et videz le registre de réception :
__HAL_UART_CLEAR_FLAG(&huart4, UART_CLEAR_OREF); __HAL_UART_SEND_REQ(&huart4, UART_RXDATA_FLUSH_REQUEST);
lit la réponse :
6 + moctets, en un seulHAL_UART_Receive, avec un délai de quelques millisecondes ;vérifie l’en-tête
FF FF, l’identifiant, la somme de contrôle et l’octet d’erreur, puis recopie lesmparamètres.
Testez-la avec un PING sur l’identifiant de votre servomoteur.
Questions
Calculez à la main la trame PING de l’identifiant 1, somme de contrôle comprise.
Mettez volontairement
DXL_OEà 0 au repos. Que se passe-t-il ? Pourquoi ?
Solution de l’étape 2
/* Envoie une instruction, lit la réponse. Renvoie 1 si elle est complète,
* correcte et sans erreur ; recopie ses m paramètres dans resp. */
static int transaction(uint8_t id, uint8_t inst, const uint8_t *p, uint8_t n,
uint8_t *resp, uint8_t m)
{
uint8_t tx[32];
uint8_t rx[32];
uint8_t cks;
if ((n > 24u) || (m > 24u)) { return 0; }
/* 1. la trame et sa somme de contrôle */
tx[0] = 0xFFu; tx[1] = 0xFFu; tx[2] = id; tx[3] = (uint8_t)(n + 2u); tx[4] = inst;
cks = (uint8_t)(id + tx[3] + inst);
for (uint8_t k = 0u; k < n; k++) { tx[5u + k] = p[k]; cks = (uint8_t)(cks + p[k]); }
tx[5u + n] = (uint8_t)~cks;
/* 2. émission ; HAL_UART_Transmit ne rend la main qu'au flag TC */
HAL_GPIO_WritePin(DXL_OE_GPIO_Port, DXL_OE_Pin, GPIO_PIN_RESET);
HAL_UART_Transmit(&huart4, tx, (uint16_t)(6u + n), 5u);
HAL_GPIO_WritePin(DXL_OE_GPIO_Port, DXL_OE_Pin, GPIO_PIN_SET);
/* 3. l'écho a débordé pendant l'émission : on efface et on vide */
__HAL_UART_CLEAR_FLAG(&huart4, UART_CLEAR_OREF | UART_CLEAR_FEF | UART_CLEAR_NEF);
__HAL_UART_SEND_REQ(&huart4, UART_RXDATA_FLUSH_REQUEST);
if (id == 0xFEu) { return 1; } /* diffusion : personne ne répond */
/* 4. la réponse : 6 + m octets */
if (HAL_UART_Receive(&huart4, rx, (uint16_t)(6u + m), 5u) != HAL_OK) { return 0; }
/* 5. vérifications */
if ((rx[0] != 0xFFu) || (rx[1] != 0xFFu) || (rx[2] != id) || (rx[3] != (uint8_t)(m + 2u)))
{
return 0;
}
cks = (uint8_t)(rx[2] + rx[3] + rx[4]);
for (uint8_t k = 0u; k < m; k++) { cks = (uint8_t)(cks + rx[5u + k]); }
if ((uint8_t)~cks != rx[5u + m]) { return 0; }
if (rx[4] != 0u) { return 0; } /* octet d'erreur */
for (uint8_t k = 0u; k < m; k++) { resp[k] = rx[5u + k]; }
return 1;
}
static int dxl_ping(uint8_t id) { return transaction(id, 0x01u, NULL, 0u, NULL, 0u); }
La somme de contrôle. On additionne ID, LEN, INST et les paramètres,
on garde les 8 bits de poids faible (le (uint8_t) à chaque addition), et
on prend le complément (~). À la réception, le servomoteur refait le
calcul : une trame abîmée par un parasite donne une somme différente, et
il l’ignore. On fait la même vérification sur sa réponse.
Le moment de repasser en écoute. HAL_UART_Transmit attend le drapeau
TC (transmission complete), qui n’est levé qu’une fois le bit d’arrêt
du dernier octet sorti sur le fil. Repasser /OE à 1 avant aurait coupé
la fin de la trame ; trop tard, et le début de la réponse du servomoteur
(qui commence quelques centaines de microsecondes après) serait écrasé par
la carte qui tient encore le fil.
L’écho. Pendant l’émission, la broche RX relit les octets envoyés.
L’UART en garde un dans son registre de réception, perd les autres et lève
le drapeau de débordement ORE. Tant que ce drapeau est levé, la
réception suivante échoue : on l’efface (UART_CLEAR_OREF, avec ceux
d’erreur de trame et de bruit), et on vide le registre de réception
(UART_RXDATA_FLUSH_REQUEST). Le firmware du robot fait la même chose.
Toutes ces vérifications semblent lourdes, mais sur un bus de robot, des parasites viennent des moteurs : une réponse d’un autre servomoteur, une trame tronquée ou un octet faux doivent être rejetés, pas interprétés.
Réponses aux questions 2 et 3
2. La trame PING de l’identifiant 1.
en-tête : FF FF
ID : 01
LEN : 02 (0 paramètre + 2)
INST : 01 (PING)
somme : 01 + 02 + 01 = 04 → ~04 = FB
trame : FF FF 01 02 01 FB
La réponse attendue est FF FF 01 02 00 FC : même identifiant, LEN = 2,
octet d’erreur 00, somme ~(01 + 02 + 00) = FC.
3. /OE à 0 au repos. Le tampon reste actif : la carte impose en permanence le niveau de sa broche TX (à 1 au repos) sur le fil de données. Le servomoteur reçoit bien les trames, mais quand il veut répondre, il se heurte à une sortie qui tient le fil à 1 : sa réponse n’arrive jamais. On a l’impression que l’émission fonctionne et que le servomoteur est muet. C’est exactement la panne rencontrée à la mise en service de la carte, d’où ce TP. Le symptôme « il reçoit mais ne répond pas » doit toujours faire penser au sens du bus half-duplex.
Étape 3 — Trouver le servomoteur et le lire
Balayage : envoyez un PING à chaque identifiant de 1 à 253, et notez ceux qui répondent.
Lisez le numéro de modèle (registre 0, 2 octets) : il doit valoir 18.
Lisez la tension (registre 42) et la température (registre 43), et affichez-les au terminal une fois par seconde.
Solution de l’étape 3
Lecture et écriture de registres, au-dessus de transaction :
static int dxl_read(uint8_t id, uint8_t addr, uint8_t *out, uint8_t len)
{
uint8_t p[2] = { addr, len };
return transaction(id, 0x02u, p, 2u, out, len); /* READ */
}
static int dxl_read16(uint8_t id, uint8_t addr, uint16_t *v)
{
uint8_t b[2];
if (!dxl_read(id, addr, b, 2u)) { return 0; }
*v = (uint16_t)(b[0] | ((uint16_t)b[1] << 8)); /* poids faible d'abord */
return 1;
}
Le balayage, une fois au démarrage :
for (uint32_t id = 1u; id <= 253u; id++)
{
if (dxl_ping((uint8_t)id)) { printf("servomoteur trouve : ID %lu\r\n", (unsigned long)id); }
}
et dans la boucle :
uint16_t model = 0u;
uint8_t vt[2] = { 0u, 0u };
if (dxl_read16(DXL_ID, 0u, &model) && dxl_read(DXL_ID, 42u, vt, 2u))
{
printf("modele %u tension %u.%u V temperature %u C\r\n",
model, vt[0] / 10u, vt[0] % 10u, vt[1]);
}
HAL_Delay(1000);
Une lecture pour deux registres. Tension (42) et température (43) sont voisines : on lit 2 octets à partir de 42, en une seule transaction.
Le balayage dure 253 × 5 ms (le délai de réception, pour chaque identifiant muet), soit 1,3 s : acceptable une fois au démarrage. Le firmware du robot, lui, ne cherche que les identifiants attendus.
Étape 4 — Le commander
La LED du servomoteur : écrivez 1 dans le registre 25.
En position : écrivez une position voulue (registre 30) de 512, le milieu de la course, puis alternez entre 300 et 700 toutes les deux secondes. Lisez la position actuelle (registre 36) pendant le mouvement.
En mode roue : écrivez 0 dans les deux limites d’angle (registres 6 et 8, deux octets chacun), puis une vitesse (registre 32) : 300 dans un sens, puis 1024 + 300 dans l’autre, puis 0 pour arrêter.
Questions
En position, que lit-on au registre 36 si l’on retient le palonnier à la main ?
En mode roue, la position actuelle reste-t-elle lisible sur un tour complet ? (Indice : le capteur de l’AX-18A ne couvre que 300° sur 360.)
Solution de l’étape 4
L’écriture d’un ou deux octets :
static int dxl_write8(uint8_t id, uint8_t addr, uint8_t v)
{
uint8_t p[2] = { addr, v };
return transaction(id, 0x03u, p, 2u, NULL, 0u); /* WRITE */
}
static int dxl_write16(uint8_t id, uint8_t addr, uint16_t v)
{
uint8_t p[3] = { addr, (uint8_t)(v & 0xFFu), (uint8_t)(v >> 8) };
return transaction(id, 0x03u, p, 3u, NULL, 0u);
}
En position, alterner 300 et 700 en lisant la position pendant le mouvement :
static uint32_t k = 0u;
uint16_t pos;
dxl_write8(DXL_ID, 25u, (uint8_t)(k & 1u)); /* LED */
dxl_write16(DXL_ID, 30u, (k & 1u) ? 700u : 300u); /* position voulue */
for (uint32_t t = 0u; t < 10u; t++)
{
if (dxl_read16(DXL_ID, 36u, &pos)) { printf("position %u\r\n", pos); }
HAL_Delay(200);
}
k++;
En mode roue :
dxl_write16(DXL_ID, 6u, 0u); /* limite CW = 0 */
dxl_write16(DXL_ID, 8u, 0u); /* limite CCW = 0 : mode roue */
dxl_write8(DXL_ID, 24u, 1u); /* couple activé */
dxl_write16(DXL_ID, 32u, 300u); HAL_Delay(2000); /* un sens */
dxl_write16(DXL_ID, 32u, 1024u + 300u); HAL_Delay(2000); /* l'autre */
dxl_write16(DXL_ID, 32u, 0u); HAL_Delay(2000); /* arrêt */
Le codage de la vitesse. En mode roue, le registre 32 tient sur 11 bits : les bits 0 à 9 donnent la vitesse (0 à 1023), le bit 10 le sens. D’où 1 024 + 300 pour la même vitesse dans l’autre sens : 1 024 = 210, le bit 10 seul.
Les limites d’angle sont gardées dans la mémoire EEPROM du servomoteur : elles survivent à une coupure. Un servomoteur passé en mode roue le reste ; pensez à remettre les limites (0 et 1023) pour un autre TP.
Réponses aux questions 4 et 5
4. Le palonnier retenu. Le registre 36 donne la position réelle, mesurée par le potentiomètre de l’axe : elle reste là où on retient le palonnier, différente de la position voulue (registre 30). Le servomoteur, lui, pousse de toute sa force pour réduire l’écart : c’est un asservissement (régulateur proportionnel). Si l’on insiste, le courant reste élevé, le moteur chauffe, et la protection de surcharge finit par couper le couple (bit Overload dans l’octet d’erreur).
5. Un tour complet. Non. Le capteur de position de l’AX-18A est un potentiomètre qui ne couvre que 300° sur 360 : sur les 60° restants, le registre 36 donne une valeur sans signification. C’est pour cela que le servomoteur en mode position ne peut pas aller au-delà de 0–1023 (0–300°).
C’est un vrai problème pour le robot, dont les roues sont des AX-18A en mode roue : son firmware n’utilise la position mesurée que hors de ce secteur aveugle, et le traverse à l’estime, à partir de la vitesse. Les servomoteurs plus récents (XL430) ont un capteur magnétique sur 360°.
Tout le code de la correction
main_user_code.c
— compilé sans avertissement contre la HAL du STM32H7.
Ce n’est pas un main.c complet : ce sont les extraits à placer dans
les blocs USER CODE du main.c généré par CubeMX, chacun sous le
commentaire qui indique son bloc. La boucle while (1) contient une
variante par étape, encadrée par #if 0 / #if 1 : n’en activer
qu’une à la fois. La configuration CubeMX attendue est rappelée en tête
du fichier, et les réponses aux questions à la fin.
Exercices supplémentaires
Ces exercices n’ont pas de correction.
Changer l’identifiant d’un servomoteur (registre 3), un seul branché sur le bus, et vérifier qu’il répond à sa nouvelle adresse.
Deux servomoteurs en chaîne, d’identifiants différents, commandés ensemble : un robot à deux roues qui avance, recule et tourne.
Suivi de bouton : la position du servomoteur suit le nombre d’appuis sur SW1 et SW2 (gauche, droite).
Surveillance : afficher en permanence tension, température et charge du moteur (registre 40), et allumer le voyant RGB en rouge au-delà de 55 °C.
Après le TP
Repassez le servomoteur en mode position si un autre TP en a besoin, et rendez la carte au robot : voir Remettre la carte en état RatBot.