Comment configurer une communication point à point de type RS232 ?

NIVEAU 2

Objectifs

  • Mettre en œuvre une liaison RS232 bidirectionnelle entre le microcontroleur et l’ordinateur

Pré-requis

Protocole RS232

Utilisation et définition

Le protocole RS232 a longtemps été utilisé pour piloter des périphériques d’ordinateur (Modem, GBF, Oscilloscope…).

Il est encore utilisé pour sa simplicité de mise en oeuvre par la plupart des systèmes embarqués pour discuter avec un PC. La console Série d’Arduino par exemple passe par ce protocole, tout comme la discussion entre le PC et la carte Nucléo (voir tutoriel Déboguer son programme et utiliser l’affichage série).

C’est un protocole asynchrone de transfert de données point à point, c’est à dire pour que deux noeuds puissent discuter. Les échanges se font en Full-Duplex grâce à deux signaux distincts RX (réception) et TX (transmission).

Les débits maximaux admissibles sont d’environ 100 kbits/s pour des distances de l’ordre de 10 m maximum. Le taux de transfert doit être paramétré de manière identique entre l’émetteur et le récepteur, puisque la liaison est asynchrone (i.e. le signal d’horloge n’est pas transmis). La vitesse de transfert est souvent donné en baud (ou symbole par secondes). Dans le cas d’une liaison RS232, 1 baud = 1 bit/s.

Trame de données

Voici une trame RS232 :

Oscillogramme de la transmission du caractère K (01001011), avec un bit de départ et un bit d’arrêt – Wikipedia / RS232

Lorsqu’aucun message n’est transmis, le bus est à l’état de repos (ou IDLE STATE en anglais). Le signal TX est à l’état ‘1’.

Lors de l’envoi d’un message, un START bit (‘0’) débute la transmission. De la même façon, un STOP bit la termine. La donnée utile est composée d’un octet. Pour transmettre plusieurs données, il faut reproduire ce motif autant de fois que nécessaire.

Dans la transmission des données, le protocole impose de transmettre du bit de poids faible (LSB) jusqu’au bit de poids fort (MSB).

Mise en oeuvre avec une carte Nucléo

La carte Nucléo L476RG est équipée de 3 liaisons Série différentes, nommées Serial1, Serial2 et Serial3. Chacune de ces liaisons est équipée des signaux TX – transmission – et RX – réception.

La liaison Serial2 semble toutefois être la liaison principale, utilisée notamment par le cable USB (par l’intermédiaire d’un convertisseur vers RS232 simulé). Ainsi les broches Serial2_TX et Serial2_RX correspondent respectivement à USB_TX et USB_RX. Elles se trouvent sur les broches D1 et D0 (qui ne sont alors pas utilisables comme sorties numériques standard).

Connexion RS232 sur la carte Nucleo L476RG

Chacune des liaisons série proposées précédemment possède 2 fils :

  • TX : Broche de transmission – sortie
  • RX : Broche de réception – entrée

Pour pouvoir relier les deux noeuds ensemble via le protocole RS232, on peut réaliser le cablage suivant (quelque soit le bus série utilisé) :

On peut noter que la sortie TX d’un noeud est branchée à l’entrée RX de l’autre noeud. Les deux canaux de communication sont (presque) indépendants. Les données peuvent donc aller dans les deux sens en même temps (Full-Duplex).

RS232-LOGIC vs RS232 standard

La représentation précédente ne tient pas compte de la vraie norme RS232, qui impose également des niveaux de tensions particulier sur les lignes RX et TX.

Lorsqu’un ‘0’ est traduit par une tension de 0V et qu’un ‘1’ est traduit par une tension de 5V (par exemple, ou 3.3V dans le cas de la carte Nucléo), on parle d’une liaison logique. Vous trouverez aussi par exemple RS232-TTL (TTL étant une norme également de circuit logique dont la tension d’alimentation est de 5V).

Pour gagner en immunité aux bruits et par le fait en qualité de transmission des données, la norme RS232 impose des tensions positives et négatives comprises entre 3V et 25V (en valeur absolue). Un niveau logique ‘0’ est représenté par une tension de +3V à +25V et un niveau logique ‘1’ par une tension de -3V à -25V. Souvent, des niveaux de +12V et -12V sont utilisés.

Oscillogramme de la transmission du caractère K (01001011), avec un bit de départ et un bit d’arrêt – Wikipedia / RS232

Il est donc souvent nécessaire pour discuter avec un élément de type RS232 d’un composant permettant l’adaptation de tension entre la partie logique (celle qui sort réellement d’un microcontroleur) et le bus. On trouve des composants déjà intégrés de type MAX232.

Configuration des broches

L’ensemble des classes et des fonctions permettant de réaliser des opérations avec les modules du microcontroleur se trouvent dans la bibliothèque “mbed.h (ou “mbed-os.h”). Il est donc indispensable d’avoir au préalable inclut cette bibliothèque :

#include "mbed.h"

Déclaration des entrées/sorties

La première étape est la déclaration des broches utilisées pour la liaison RS232. Il faut la placer après la déclaration des ressources externes (bibliothèques) et avant toute définition de fonction (voir tutoriel Tester mon premier programme sur Nucléo – Code d’exemple /* Déclaration des entrées/sorties */).

UnbufferedSerial my_pc(USBTX, USBRX);

Pour cela, on déclare, à l’aide du constructeur de la classe UnbufferedSerial, le nom de la liaison dans le reste du programme (ici la variable my_pc) et les broches TX et RX à utiliser sur la carte.

N.B. Dans l’exemple précédent, on utilise les broches TX et RX associées à la liaison USB (ce n’est en fait qu’une version simulée d’une liaison RS232 via le protocole USB). Il s’agit en fait des broches Serial2_TX et Serial2_RX (qui ne sont alors plus utilisables pour d’autres signaux).

Pour pouvoir visualiser les signaux, il faut se connecter sur les broches indiquées ci-dessous :

Il existe d’autres broches qui permettent la communication RS232 sur la carte Nucléo :

  • Serial 1 : A4 (TX) / A5 (RX)
  • Serial 4 : A0 (TX) / A1 (RX)

Configuration de la vitesse de transmission

Avec le compilateur MBED, il est possible de changer la vitesse de transmission, à l’aide de la fonction baud de la classe UnbufferedSerial.

my_pc.baud(115200);

Dans l’exemple ci-dessus, on paramètre la liaison pour qu’elle émette à 115200 bauds. Il faut penser à adapter cette valeur en fonction de la vitesse de transmission qu’est capable d’utiliser le noeud associé.

Remarque Il existe également une fonction format de la classe UnbufferedSerial qui permet de configurer d’autres paramètres (nombre de bits transmis, utilisation d’un bit de parité…).

Envoi d’un message depuis la carte Nucléo

On va pouvoir transmettre un message sous forme d’un ensemble d’octets stockés dans un tableau de données grâce à la fonction write de la classe UnbufferedSerial.

Dans les deux exemples ci-après, un message est transmis au PC par l’intermédiaire du cable USB. Sur le PC, il faut penser à ouvrir un terminal série (type Teraterm) et le paramétrer pour écouter la liaison correspondante à la bonne vitesse de transmission.

Envoi d’un ensemble d’octets

Les données à transmettre doivent être placées dans un tableau de type char.

char data_to_send[5] = {0x10, 0x20, 0x35, 0x65, 0x50};

La fonction write permet alors l’envoi séquentiel de chacun de ces octets à travers la liaison série, à la vitesse de transmission paramétrée.

my_pc.write(data_to_send, 5);

Dans cet exemple, les données du tableau data_to_send sont transmises. Le second paramètre de cette fonction est le nombre d’octets à transmettre.

Envoi d’une chaîne de caractères formatée

La fonction printf n’a pas été réimplémentée dans la classe BufferedSerial de MBED. Pour pouvoir envoyer une chaine de caractères formatée, il faut alors passer par une chaine de caractères intermédiaire, dont la taille dépend de la taille maximale de la chaine de caractères que votre application devra traiter :

char message[128];

Pour formater une chaine de caractères, on peut alors utiliser la fonction sprintf (bibliothèque mbed.h) :

sprintf(message, "Valeur de a = %d \r\n", a);   // ou a est une variable de type int

Chacun des caractères étant codé sur un octet (code ASCII), il est alors possible d’utiliser la fonction write vue précédemment pour transmettre cette chaîne de caractères :

my_pc.write(message, sizeof(message));

On peut remarquer dans l’exemple précédent l’utilisation de la fonction sizeof pour spécifier la taille de la chaine de caractères à transmettre. Cette fonction retourne le nombre d’octets d’une variable passée en paramètre.

Exemple complet

#include "mbed.h"

UnbufferedSerial my_pc(USBTX, USBRX);
int a;
char message[128];

int main() {
    a = 0;
    my_pc.baud(115200);
    sprintf(message, "Bienvenue a l'IOGS\r\n");  
    my_pc.write(message, sizeof(message));

    while(1){
        a++;
        sprintf(message, "Valeur de a = %d\r\n", a);  
        my_pc.write(message, sizeof(message));
        thread_sleep_for(500);   // break de 500ms
    }
}

Dans cet exemple, on affiche la valeur d’un compteur a qui s’incrémente toutes les 500 ms.

Réception d’un message depuis la carte Nucléo

De la même façon que pour émettre une donnée sur la liaison série, il existe la fonction read permettant de récupérer des données (dans la classe BufferedSerial).

Il existe également deux façons de récupérer une information sur une liaison série :

  • par scrutation, par le biais de la fonction readable de la classe BufferedSerial qui permet de savoir quand une donnée est prête (liaison asynchrone)
  • par interruption, par le biais de la fonction attach de la classe BufferedSerial, qui permet d’associer à un évènement de réception une fonction spécifique de récupération des données

Réception par scrutation

Dans l’exemple suivant, on va récupérer une donnée à la fois (sous forme d’un octet stocké dans une variable de type char).

#include "mbed.h"

UnbufferedSerial my_pc(USBTX, USBRX);
char data;
char message[128];

int main() {
    double rc;
    int rx_receive;
    my_pc.baud(115200);
    
    while(1){
        if (my_pc.readable()) {
            my_pc.read(&data, 1);
            sprintf(message, "Valeur recue = %c\r\n", data);  
            my_pc.write(message, sizeof(message));
        }
    }
}

La fonction readable de la classe BufferedSerial permet de savoir si une donnée est prête (reçue). La donnée alors lue est la première reçue dans la mémoire tampon de la liaison série.

Réception par interruption

Une meilleure façon de procéder est d’utiliser des interruptions. Les registres de réception et d’émission des liaisons séries de la carte Nucléo sont associés à des interruptions. Il est alors possible de lier (via la fonction attach) une fonction particulière pour le traitement des caractères arrivant à la liaison série.

Dans l’exemple suivant, une routine d’interruption va venir récupérer un caractère (read) et le renvoyer sur la même liaison.

#include "mbed.h"
DigitalOut led1(LED1);

UnbufferedSerial      my_pc(USBTX, USBRX);
char    message[32];
char    data;

void ISR_my_pc_reception(void){
    led1 = !led1;
    my_pc.read(&data, 1);           
    sprintf(message, "Valeur recue = %c\r\n", data);  
    my_pc.write(message, sizeof(message));
}

int main() {
    my_pc.baud(115200);
    my_pc.attach(&ISR_my_pc_reception);
    
    while(1){

    }
}

Il est possible de préciser si l’on souhaite une interruption sur une donnée entrante ou sortante en ajoutant l’option suivante à la fonction attach :

my_pc.attach(&ISR_my_pc_reception, Serial::RxIrq);  // reception
my_pc.attach(&ISR_my_pc_reception, Serial::TxIrq);  // transmission

Tutoriel lié

MInE Prototyper Prototyper avec Nucleo et MBED

Nucléo – Configurer une communication point à point de type RS232