Communication série en Python

Communication série en Python #

Durant ce TP, nous allons programmer une interface de communication utilisant, sur le port USB, une liaison série RS232 entre d’une part une carte à micro-contrôleur à base de MSP430 (déjà programmée), et d’autre part un PC ou une Raspberry Pi, en Python.

Pour manipuler le port série avec Python, nous utiliserons le module tiers pyserial, dont la documentation se trouve ici : https://pyserial.readthedocs.io/en/latest/

Branchement du MSP430 #

Pour que la carte MSP430 commence à émettre sur le port série, il suffit de la connecter à la Raspberry ou au PC, à l’aide d’un cable USB. Le programme qui tourne sur le MSP430 peut être relancé à tout moment, en utilisant le bouton Reset, situé sur la même face que le connecter micro-USB.

Ouverture du port série #

L’ouverture du port série nécessite de connaître certains paramèters : nom du port, vitesse de transmission, bits de parité, contrôle de flux…

Sous Windows, le nom du port peut être obtenu (après branchement de la carte) dans le gestionnaire de périphériques. Le nom ressemble à COM1, COM2… Sous Linux (Raspberry Pi), la consultation du journal après branchement permet généralement de connaître le port utilisé par le nouveau périphérique :

$ dmesg
...
[ 2702.474341] usb 2-2: new full-speed USB device number 17 using xhci_hcd
[ 2702.617486] usb 2-2: New USB device found, idVendor=2047, idProduct=0013, bcdDevice= 2.00
[ 2702.617491] usb 2-2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 2702.617493] usb 2-2: Product: MSP Tools Driver
[ 2702.617495] usb 2-2: Manufacturer: Texas Instruments
[ 2702.617496] usb 2-2: SerialNumber: AE443A5125000100
[ 2702.622308] cdc_acm 2-2:1.0: ttyACM0: USB ACM device
[ 2702.622950] cdc_acm 2-2:1.2: ttyACM1: USB ACM device

On voit ici que le MSP430 correspond à deux interfaces : ttyACM0 et ttyACM1 (ce sont des fichiers de périphérique, situés dans le répertoire /dev). Il reste à déterminer, parmi ces deux interfaces, celle qui est utilisée par le MSP430 pour communiquer avec l’extérieur.

Les autres paramètres de la communication sont :

  • vitesse : 19200 bauds
  • 8 bits de données
  • pas de bit de parité
  • un bit stop

L’ouverture du port série ressemble un peu à l’ouverture d’un fichier :

import serial

ser = serial.Serial('/dev/ttyACM??', 19200, timeout=5, 
                    bytesize=8, parity='N', stopbits=1)

# lecture / écriture

ser.close()

Lecture sur le port série #

Lorsque le port série est ouvert, on peut lire les données qui se présentent, octet par octet :

# ouverture du port 

b = ser.read() # Lecture d'un octet

# fermeture du port

L’opération de lecture est bloquante. Elle ne reste toutefois pas bloquée éternellement, et lorsque le timeout utilisé lors de l’ouverture du port série est dépassé, la méthode read se termine (et renvoie un octet vide si rien n’a été lu).

Il est habituel, en Python, d’utiliser des context managers. Ce sont des objets qui offrent certaines garanties d’initialisation et de finalisation. En ce qui concerne les fichiers, ils permettent par exemple de garantir la fermeture du fichier à la fin du code (que celui-ci se termine proprement ou plante durant l’exécution) :


import serial

with serial.Serial('/dev/ttyACM??', 19200, timeout=5, 
                    bytesize=8, parity='N', stopbits=1) as ser:
    # Dans ce bloc indenté, 
    # on peut disposer de l'objet `ser`
    # Lorsque le bloc se terminera, la liaison sera refermée.

Notez qu’il n’est pas nécessaire de fermer explicitement ser.

Réalisez un premier programme, nommé read_50_bytes.py qui se contente de lire 50 octets à la suite sur le port série et les affiche proprement à l’écran. Quel genre de séquence d’octets est envoyée par le MSP430 ?
Si vous obtenez l’erreur PermissionDenied à la lecture du port série, c’est peut être que lors d’une lecture précédente, vous n’avez pas refermé le port série, et êtes maintenant dans l’impossibilité de l’ouvrir à nouveau. Dans ce cas, redémarrez le shell Python (qui refermera tous les fichiers ouverts).

Objets de type bytes #

Le type bytes en Python permet de représenter une séquence d’octets. Les objets de type bytes se manipulent un peu comme des chaînes de caractères. Python peut parfois les afficher sous forme de caractères (lorsque l’octet correspond à un caractère ASCII imprimable par exemple) ou comme une valeur numérique.

 # Le préfixe b indique qu'il s'agit d'octets.
 # Il y en a ici 3 : 65 (correcpond à A), 16 (10 en hexadécimal),
 # et 66 (correspond à B)
>>> octets = b'A\x10B'
>>> octets
b'A\x10B' 
>>> octets[0]
65
>>> octets[1]
16
>>> octets[2]
66
>>> hex(octets[1])
'0x10'

Il existe des moyens efficaces pour passer d’une séquence d’octets à une chaîne de caractères et inversement (voir str.encode et str.decode), ou d’une séquence d’octets à un entier, codé sur plusieurs octets, en mode big ou little endian (voir int.from_bytes ou int.to_bytes). Pensez à vous documenter sur ces possibilités avant de vous lancer dans des conversions fastidieuses.

Le type bytes n’est pas mutable (les chaînes de caractères non plus). Il existe toutefois un type bytearray, mutable, et la conversion est assez facile :

>>> octets = b'\x01\x02\x03\x04' # octets est de type bytes, non mutable
>>> m_octets = bytearray(octets) # m_octets est de type bytearray, mutable
>>> m_octets[1] = 42 # <= possible car m_octets est mutable
>>> m_octets
bytearray(b'\x01*\x03\x04')

Protocole de communication #

Un protocole de communication a été implémenté sur le MSP 430, et il peut répondre à certaines commandes. Ce protocole échange des trames de plusieurs octets :

  • STX : 0x02 (début de trame)
  • LG : longueur de la trame en octets
  • CMD : octet de commande (parfois absent)
  • DATA : de 0 à plusieurs octets de données
  • CHKSUM : checksum de la trame

Le checksum, sur un octet, est la somme (masquée sur 1 octet) de tous les octets de la trame qui précèdent le checksum.

Le protocole implémente plusieurs commandes :

  • 22 : demande d’accusé de réception (ACK request)
  • 24 : demande de température (TEMP request)
  • 128 : réponse à un accusé de réception

Écriture sur le port série #

Sans surprise, on écrit sur la liaison série avec write :

...
# Écriture d'un seul octet (ici 0)
ser.write(b'\x00')
# Écriture de 4 octets (0, 255, 0, 255)
ser.write(b'\x00\xff\x00\xff')
Écrivez un programme nommé write_4_bytes.py qui écrit les octets de valeurs (ici données en décimal): 2, 3, 22 et 27 (4 octets), puis lit le port série tant que des octets y sont présents. Notez les octets que vous avez reçu.
En utilisant les détails du protocole de communication indiqués précédemment, à quoi correspondent les octets que vous avez envoyé au MSP430 ? Et à quoi correspondent les octets que vous avez reçu en réponse ? Les checksums sont-ils corrects ?

Lecture de la température #

Écrivez le programme send_frame.py qui envoie une trame de demande de température et affiche les octets reçus en réponse. Vous décoderez manuellement les octets reçus (l’octet CMD n’est pas présent dans la réponse). C’est à vous de trouver comment interprêter les octets reçus, et vous devez expliquer la démarche. Ceci peut éventuellement vous aider : https://fr.wikipedia.org/wiki/Boutisme

Dans le fichier ask_temp.py, écrivez une fonction qui envoie une demande de température, attend la réponse, la décode, et renvoie la température sous forme numérique (nombre à virgule flottante). Voici une liste de points à traiter (selon le temps dont vous disposez) :

  • vérification de la trame (est-ce bien une réponse de température ?)
  • traitement des erreurs de décodage de la trame
  • vérification du checksum