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) :

Figure made with TikZ

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 :

  1. /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.

  2. 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.

  3. 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

  1. Créez un projet tp10_dynamixel comme au TP 5 (UART5 et printf pour afficher les résultats).

  2. Dans Connectivity → UART4, choisissez Asynchronous, sur PA12 (TX) et PA11 (RX). Baud Rate : 1000000, 8 bits, sans parité, 1 bit d’arrêt.

  3. Configurez PA10 en GPIO_Output, label DXL_OE, avec GPIO output level sur High : au démarrage, la carte écoute.

  4. Générez le code.

Question

  1. À 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 :

  1. construit la trame et sa somme de contrôle ;

  2. passe en émission (DXL_OE à 0), envoie la trame avec HAL_UART_Transmit — qui ne rend la main qu’une fois le dernier octet parti — puis repasse en écoute (DXL_OE à 1) ;

  3. 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);
    
  4. lit la réponse : 6 + m octets, en un seul HAL_UART_Receive, avec un délai de quelques millisecondes ;

  5. vérifie l’en-tête FF FF, l’identifiant, la somme de contrôle et l’octet d’erreur, puis recopie les m paramètres.

Testez-la avec un PING sur l’identifiant de votre servomoteur.

Questions

  1. Calculez à la main la trame PING de l’identifiant 1, somme de contrôle comprise.

  2. 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

  1. Balayage : envoyez un PING à chaque identifiant de 1 à 253, et notez ceux qui répondent.

  2. Lisez le numéro de modèle (registre 0, 2 octets) : il doit valoir 18.

  3. 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

  1. La LED du servomoteur : écrivez 1 dans le registre 25.

  2. 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.

  3. 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

  1. En position, que lit-on au registre 36 si l’on retient le palonnier à la main ?

  2. 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.