[TUTO] Programmer le télémètre laser TW10S-UART

Bonjour,

Si vous vous intéressez à la télémetrie laser, vous avez sûrement déjà vu ce télémètre, bien moins cher que ses concurrents, mais comment l'utiliser? Voici un petit tuto sympa (du moins j'espère) pour apprendre à utiliser le capteur laser TW10S-UART.

1. Le module

C'est sûrement le plus accessible pour quelqu'un qui, comme moi, veut utiliser un télémètre laser sans se ruiner!
En effet on le trouve facilement entre 40 et 45€ sur AliExpress.
Malheureusement ce laser est fourni sans aucune documentation précise, et la seule que j'ai pu trouver est très insuffisante et ne correspond pas complètement au produit :no_mouth:.

D'expérience, ce module n'est cependant pas très approprié pour la réalisation d'un LiDAR du fait de sa faible vitesse d'acquisition.

2. Le câblage

Heureusement, nous pouvons trouver sur AliExpress le nom des broches! C'est une connexion de type UART, avec 5 pin de type MX au pas de 1.25mm:

EN_PWR => 3.3V

Rx => Sur une broche digital de la carte (PIN 3 pour l'exemple)

Tx => Sur une autre broche digital (PIN 4 pour l'exemple)

GND => GND

3.3V => 3.3V

La broche Rx du laser doit recevoir du 3.3V mais les sorties de la carte Arduino envoient du 5V. On utilise donc un pont divideur de tension pour regler ce problème avec un couple de résistance 10kΩ et 20kΩ (voir schéma).
Si vous utilisez des valeur de resistance plus forte, ou un régulateur en un peu moins de 3V, il est probable que votre carte ne reçoive pas les donées à cause d'une tension trop faible.

cablage_laser_TW10S

Pour obtenir du 3.3V pour l'alimentation du laser, je recomande d'utiliser un régulateur de tension en 3.3V afin de ne pas surcharger la sortie 3.3V de la carte (on risquerait de la griller car elle ne delivre que 50mA pour une Arduino UNO). Un régulateur de 800mA comme celui ci conviens parfaitement, mais en fonction de la charge (si vous utilisez plusieurs éléments fonctionnant en 3.3V), il peut etre utile d'en acheter un plus puissant.

N.B. Le Pin EN_PWR peut aussi être branché sur un pin de la carte et être mise à HIGH pour pouvoir utiliser le laser et LOW pour le mettre en mode économie d'énergie.

N.B. Je ne sais pas si c'est une généralité, mais dans mon cas, la partie plastique fixée sur le PCB n'est pas paralèle avec ce dernier. Faites y atention si vous réalisez un boitier pour installer le laser...

3. Le code

Malheureusement, il n'y a pas (enfin pas à ma connaissance) de librairie automatisant la tâche🥲. On va donc devoir se débrouiller tout seuls!

Tout d'abord il faut initialser la communication UART grâce à la libraire SoftwareSerial (Documentation en anglais) . L'avantage de cette librairie est qu'elle s'utilise comme vous le faites déjà sûrement avec le fameux Serial.println.

Pour commencer voici comment procéder:

#include <SoftwareSerial.h> //on importe la librairie

#define TX_laser 4 //la broche reliée au TX du laser
#define RX_laser 3 //la broche reliée au RX du laser

SoftwareSerial laser(TX_laser, RX_laser); //on initialise la communication série qui sera appelée laser

void setup() {

  Serial.begin(9600); // pour afficher les valeurs retournées par le laser on démarre Serial pour pouvoir utiliser le moniteur série
  laser.begin(9600); // on démarre la communication série nomée laser avec une vitesse de 9600 bauds
  delay(200);
  Serial.print("start");

}

Une fois la connexion établie on va pouvoir l'utiliser pour demander au laser d'abord de s'allumer puis de nous retourner la distance mesurée.

Pour envoyer les commandes, il ne suffit pas de lui dire "allume toi!". Il faut lui envoyer une serie de Bytes (Octets) en Hexadécimal selon le protocole MODBUS:

Format de requête MODBUS:

(1Byte- code d'adresse) (1Byte- code de fonction) (2Bytes - adresse initiale ) (2Bytes- Register number (N) ) (2Bytes- CRC)

Format de réponse MODBUS:

Réponse normale:

(1Byte- code d'adresse) (1Byte- code de fonction) (1Bytes - nombre de Byte ) (2 * N Bytes - valeurs ) (2Bytes- CRC)

Réponse anormale:
(1Byte- code d'adresse ) (1Byte- code d'erreur) (1Bytes - code d'Exception ) (2Bytes- CRC).

Définition des codes d'Exception:

0x01: Erreur dans le code de fonction

0x02: Erreur dans l'adresse initiale

0x03: Erreur dans le Register Number

0x04: Erreur dans la valeur

0x05: Erreur dans le CRC

0x06: Equipement Occupé (Busy)

Nous verrons plus tard à quoi sert le CRC

Pour envoyer la commande "allume toi" par exemple il faudra donc envoyer les Bytes suivants :
{0x01, 0x10, 0x00, 0x03, 0x00, 0x01, 0x02, 0x00, 0x01, 0x67, 0xA3}0x signifie que c'est une valeur en Hexadécimal. C'est la même commande pour ensuite l'éteindre.

La commande pour demander une mesure unique et la commande pour une mesure continue sont les suivantes:

uint8_t commandeContinue[8] = {0x01, 0x03, 0x00, 0x01, 0x00, 0x02, 0x95, 0xCB};
uint8_t commandeUnique[8] = {0x01, 0x03, 0x00, 0x0F, 0x00, 0x02, 0xF4, 0x08};

uint8_t marche[11] = {0x01, 0x10, 0x00, 0x03, 0x00, 0x01, 0x02, 0x00, 0x01, 0x67, 0xA3}; //commande on/off

uint8_t arretContinue[8] = {0x01, 0x03, 0x00, 0x0A, 0x00, 0x02, 0xE4, 0x09} //arrête la mesure continue (non testée pour l'instant)

Ce site (avec une partie en japonais et l'autre en anglais) récapitule les bytes à envoyer pour plusieurs autres commandes. Attention! Lorsqu'on passe par le langague arduino il faut penser à lui préciser que les valeurs sont en hexadécimal en rajoutant 0xdevant la valeur comme on le fait dans le code précédent.

Pour envoyer les commande il suffit d'écrire sur le port série qu'on vient d'initialiser:

void setup() {

  Serial.begin(9600); // pour afficher les valeurs retournées par le laser on démarre Serial pour pouvoir utiliser le moniteur série
  laser.begin(9600); // on démarre la communication série nomée laser avec une vitesse de 9600 bauds
  delay(200);
  Serial.print("start");
  laser.write(marche, 11); //envoie la commande "allume toi"

}


void loop() {

  laser.write(commandUnique, 8); //on lui demande une valeur de mesure de distance 

}

Si tout se passe bien on devrait recevoir de la part du capteur une suite de Bytes de type: {0x01, 0x03, 0x04, 0xXX, 0xXX, 0xXX, 0xXX, 0xYY, 0xYY} où XX sont les Bytes de valeur de la distance et YY le code CRC

Pour lire la réponse on va utiliser laser.readBytes() (Voir documentation de la librairie)

  laser.write(commandUnique, 8); //requête
  delay(200);
  if( laser.available() >0){ //on verifie que des données sont disponibles à la lecture
    laser.readBytes(data, 9); //on lit les 9 premiers Bytes et on les stocke dans data qui est de type uint_8[9]
    //laser.overflow(); 
  }

  for(int i = 0; i < sizeof(data); i++) { //on parcourt toutes les valeurs de data
    Serial.print(data[i], HEX); //on affiche le Byte au format Hexadécimal dans le moniteur série
    Serial.print("  "); //sépare les valeurs
  }

Bon c'est bien beau tout ça mais on en fait quoi de cette liste de caractères incomprehensible ?!

On va pouvoir décoder les caractères reçus tout simplement en convertissant l'hexadécimal en décimal. Pour cela j'utilise cette fonction:

  byte3_1 = data[3] / 16; //un byte en hexadecimal a 2 lettres/chiffres donc il faut prendre chacun des deux séparément: ici on récupère la première partie (le poids le plus fort)
  byte3_2 = data[3] - byte3_1 * 16; //ici on récupère la deuxième partie (poids faible)
  byte4_1 = data[4] / 16;
  byte4_2 = data[4] - byte4_1 * 16;
  byte5_1 = data[5] / 16;
  byte5_2 = data[5] - byte5_1 * 16;
  byte6_1 = data[6] / 16;
  byte6_2 = data[6] - byte6_1 * 16;

  uint8_t bytes[8] = {byte6_1,byte6_2,byte5_1,byte5_2,byte4_1,byte4_2,byte3_1,byte3_2}; //bit de poids faible en premier
  
  float dist = 0;
  for(int i =0; i< 8;i++) { //parcourt toutes les valeurs de bytes[]
    dist+= bytes[i]*pow(16,i); //pour chaque bit en commençant par le bit de poids faible, on le multiplie par 16 élevé à la puissance de i et on l'ajoute à la valeur de dist
  }

En savoir plus sur la conversion Hex->Dec

Pour rendre le traitement des données plus facile, je concentre toute la gestion de la reception des informations transmises par le laser dans une seule fonction. Ici je crée la fonction getDist() qui effectue tous les calculs décrits précédemment et retourne la distance mesurée.


long getDist() {
  if( laser.available() >0){ //lecture des données
    laser.readBytes(data, 9);
    laser.overflow();
  }

  for(int i = 0; i < sizeof(data); i++) //affichage des données reçues 
  {
    Serial.print(data[i], HEX);
    Serial.print("  ");
  }

  //traitement des données
  delay(200);
  byte3_1 = data[3] / 16;
  byte3_2 = data[3] - byte3_1 * 16;
  byte4_1 = data[4] / 16;
  byte4_2 = data[4] - byte4_1 * 16;
  byte5_1 = data[5] / 16;
  byte5_2 = data[5] - byte5_1 * 16;
  byte6_1 = data[6] / 16;
  byte6_2 = data[6] - byte6_1 * 16;
  uint8_t bytes[8] = {byte6_2,byte6_1,byte5_2,byte5_1,byte4_2,byte4_1,byte3_2,byte3_1}; //bit de poids faible en premier
  
  float dist = 0;
  for(int i =0; i< 8;i++) {
    dist+= bytes[i]*pow(16,i);
  }

  return dist; //retourne la distance mesurée
}

4. Mais ducoup... c'est quoi le CRC

Le CRC c'est tout simplement un code de vérification qui permet d'être sûr que les données ne sont pas corrumpues (qu'on en a pas perdu en route en gros). Pour faire simple, les caractères de la chaîne de données transmise sont ajoutés d'une certaine mainière qui fait que la probabilité de trouver la même somme avec des caractères différents est extrêmement faible. On transmet à la fin du "message" la somme ainsi calculée c'est à dire le code CRC (Cyclic Redundancy Check). La personne (ou ordinateur) qui reçoit le message va faire le même calcul et comparer le résultat au code CRC reçu. Si les deux sont identiques, les données ont bien été reçues correctement. Sinon il faut demander à réenvoyer les données.

Le calcul du CRC est le résultats de calculs mathématiques complexes incluant des divisions polynomiales et tout un tas de choses avec lesquelles on n'a pas envie de s'embêter. C'est pour ça qu'on est content quand on voit qu'une librairie s'en charge à notre place!

Comment l'utiliser?

Tout d'abord il faut la télécharger via le gestionnaire de librairies (son petit nom c'est tout simplement CRC) et l'importer au début de notre sketch:


#include <CRC16.h>

//puis on l'initialise avec des constantes toutes prêtes
CRC16 crc(CRC16_MODBUS_POLYNOME,
          CRC16_MODBUS_INITIAL,
          CRC16_MODBUS_XOR_OUT,
          CRC16_MODBUS_REV_IN,
          CRC16_MODBUS_REV_OUT);

Ensuite on remet le "compteur" à zéro à chaque fois qu'on veut l'utiliser en utilisant crc.restart(); ( il ne faut pas oublier cette étape sinon tous les calculs effectués par la librairie seront faussés )

Pour calculer le CRC, il faut ajouter un par un les bytes reçus avec crc.add(byte); puis le calculer avec crc.calc();. Au final ça donne ça:

  crc.restart();
  laser.write(commandUnique, 8);
  delay(200);
  if( laser.available() >0){
    laser.readBytes(data, 9);
    //laser.overflow();
  }

  for(int i = 0; i < sizeof(data); i++)
  {
    crc.add(data[i]);
  }
  uint8_t CRC = crc.calc();

  if (CRC==0) { //le code est bon si il vaut 0 (la différence entre le code calculé et celui envoyé avec les données est égale à 0 si les 2 codes sont les mêmes)
    Serial.println("Les données ne sont pas corrompues");
  }

5. Récap

Au final après avoir fait tout ça on peut résumer l'utilisation du laser à:

  • un câblage:

cablage_laser_TW10S

  • un code:
/**
* Code pour une première utilisation simple du télémètre laser TW10S-UART
* https://fr.aliexpress.com/item/33035807395.html
* 
* Par guillaume_lrt 
* Le 24/06/2024
*/

#include <CRC16.h> //librairie pour le CRC
#include <SoftwareSerial.h> //librairie pour la communication série

#define TX_laser 4 //la broche reliée au TX du laser
#define RX_laser 3 //la broche reliée au RX du laser

SoftwareSerial laser(TX_laser, RX_laser); //on initialise la communication série qui sera appelée laser

CRC16 crc(CRC16_MODBUS_POLYNOME,
          CRC16_MODBUS_INITIAL,
          CRC16_MODBUS_XOR_OUT,
          CRC16_MODBUS_REV_IN,
          CRC16_MODBUS_REV_OUT); //on initialise la librairie qui calculera le CRC


//les codes à envoyer au laser
uint8_t commandContinue[8] = {0x01, 0x03, 0x00, 0x01, 0x00, 0x02, 0x95, 0xCB}; //mesure continue

uint8_t arretContinue[8] = {0x01, 0x03, 0x00, 0x0A, 0x00, 0x02, 0xE4, 0x09} //arrête la mesure continue (non testée pour l'instant)

uint8_t commandUnique[8] = {0x01, 0x03, 0x00, 0x0F, 0x00, 0x02, 0xF4, 0x08}; //mesure unique

uint8_t marche[11] = {0x01, 0x10, 0x00, 0x03, 0x00, 0x01, 0x02, 0x00, 0x01, 0x67, 0xA3}; //commande on/off

uint8_t data[9];


//*****************************************************************************
void setup() {

  Serial.begin(9600); // pour afficher les valeurs retournées par le laser on démarre Serial pour pouvoir utiliser le moniteur série
  laser.begin(9600); // on démarre la communication série nomée laser avec une vitesse de 9600 bauds
  delay(200);
  Serial.print("start");
  laser.write(marche, 11); //démarre le laser
}

//*****************************************************************************

void loop() {
  Serial.println(getDist()); //appelle la fonction getDist() et affiche le résultat
}

/**********************Fonction Distance**************************/
long getDist() { 
  crc.restart();
  laser.write(commandUnique, 8);
  delay(200);
  if( laser.available() >0){
    laser.readBytes(data, 9);
    //laser.overflow();
  }

  for(int i = 0; i < sizeof(data); i++)
  {
    //Serial.print(data[i], HEX);
    //Serial.print("  ");
    crc.add(data[i]);
  }
  uint8_t CRC = crc.calc();
  //Serial.println(CRC, HEX);

  uint8_t byte6_1, byte5_1, byte6_2, byte5_2, byte4_1, byte4_2, byte3_1, byte3_2;

  delay(200);
  byte3_1 = data[3] / 16; //premier caractère du byte en hex
  byte3_2 = data[3] - byte3_1 * 16; //Deuxième caractère
  byte4_1 = data[4] / 16;
  byte4_2 = data[4] - byte4_1 * 16;
  byte5_1 = data[5] / 16;
  byte5_2 = data[5] - byte5_1 * 16;
  byte6_1 = data[6] / 16;
  byte6_2 = data[6] - byte6_1 * 16;
  uint8_t bytes[8] = {byte6_2,byte6_1,byte5_2,byte5_1,byte4_2,byte4_1,byte3_2,byte3_1}; //bit de poids faible en premier
  
  float dist = 0;
  for(int i =0; i< 8;i++) {
    dist+= bytes[i]*pow(16,i);
  }


  if (CRC == 0) {
    Serial.println("Les données ne sont pas corrompues");
    return dist;
  } else return -1; //retourne -1 si la mesure n'est pas recevable (CRC pas bon)

}

En éspérant que ce tuto vous ait plu et soit utile au plus grand nombre,

bon courage dans vos projets et aventures avec Arduino!

1 Like

Merci pour le partage !

juste un point peut être sur la lecture du port série:

vous imposez une attente de 200ms ce qui peut être trop ou pas assez. Dans l'absolu il n'est jamais bon d'essayer de deviner le timing d'un protocole asynchrone ➜ vous pouvez jeter un oeil à mon petit tuto sur le sujet pour traiter cela différemment (attendre les octets 0x01, 0x03, 0x04 pour détecter un début de trame, ajouter un timeout, etc)

sinon

    laser.overflow(); //on efface les éventuelles données qui peuvent rester sur le port série

la fonction overflow() ne fait pas ce qui est décrit dans le commentaire (cf la doc)

Merci pour vos retours.

Il est vrai que remplacer le delay(200); par ce que vous dites serait plus académique, mais le but de ce tuto est de faire un programme simple à la portée de tous, ce qui n'empêche toutefois pas les développeurs plus aguerris de le remanier. J'essaierai de remplacer ce delay par ce que vous suggérez et de faire un édit.

Pour ce qui est du laser.overflow(); ce n'est en effet pas l'utilisation principale de cette commande. Il me semble qu'appeler cette fonction a aussi comme effet d'effacer les données restantes sur le buffer mais je n'en suis pas complètement sûr. Toutefois, dès que j'ai rajouté cette ligne dans le code, les données résiduelles dans le buffer n'étaient plus lues par le readBytes() à l'itération suivante.

non ça n'efface rien du tout. ça dit juste si le buffer de réception a débordé depuis la dernière lecture d'un octet ou test d'overflow de ce buffer.

le code source tient sur une ligne:

le contenu du buffer n'est pas affecté.

--

OK. ça a l'inconvénient de propager des mauvaises pratiques pour la gestion d'un port série, c'est pour cela que je me permettais la remarque vu que c'est un tuto.

au pire vous pourriez remplacer le delay() par un

while(laser.available() < sizeof data) yield(); // on attend la réponse 

mais si la réponse ne vient pas, on est coincé (faudrait aussi un timeout et gestion des erreurs)