Utiliser la communication série

L’UART pour Universal Asynchronous Receiver Transmitter (en français Récepteur-Transmetteur Asynchrone Universel) est un composant utilisé pour faire la liaison entre le microcontrôleur et l’un de ses ports série. Il existe également une variante de l’UART, l’USART pour Universal Synchronous / Asynchronous Receiver Transmitter (en français Récepteur-Transmetteur Synchrone / Asynchrone Universel). Dans le cadre de la programmation en MicroPython, la distinction entre les deux n’a pas lieu d’être et nous considèrerons par la suite que UART désigne ces deux composants.

L’UART permet donc la communication série entre deux systèmes, c’est à dire d’échanger des messages formés par des séquences de bits, en général interprétées comme du texte. Il peut être utilisé pour dialoguer avec un périphérique et le piloter (par exemple un module GPS, un module WiFi ou un capteur de distance à ultrasons, plusieurs tutoriels illustrant cet usage sont disponibles dans cette section) ou encore pour échanger des informations entre deux cartes comme nous allons le montrer par cet exemple.

Vous utilisez déjà depuis le début des exercices, peut être sans en avoir conscience, l’un des UART de la NUCLEO-WB55 : celui qui est câblé avec l’USB USER et vous permet d’échanger avec le terminal série que vous avez choisi (PuTTY ou autre, voir figure ci-dessous).


UART USB USER


Quelques compléments sur la communication série dans les microcontrôleurs

L’UART permet l’échange de données sur deux fils (Rx et Tx) entre un émetteur et un récepteur. Le processus est asynchrone ; on n’utilise pas de signal d’horloge pour cadencer les échanges.

La communication peut être à sens unique (d’un émetteur et vers un récepteur), auquel cas on dit que l’UART fonctionne en mode half duplex. Si la communication est à double sens (chacun des deux systèmes peut être tour à tour émetteur ou récepteur), on parle de mode full duplex. C’est celui que nous utiliserons par la suite.

Un message échangé, aussi appelé trame est composé d’un bit de start, de 5 à 9 bits de données, de 1 ou 2 bits de stop et 1 bit de parité optionnel. Le bit de parité sert à détecter des erreurs de transmissions. La parité est générée par l’émetteur et vérifiée par le receveur. Pour une parité paire, le nombre de 1 dans la donnée plus le bit de parité est pair, alors que pour une parité impaire le nombre de 1 de la donnée plus celui de la parité est impair.

Le débit d’un UART est exprimé en bauds. Le baud représente la fréquence de (dé)modulation d’un signal, par exemple celui envoyé ou reçu par un modem (modulateur-démodulateur), c’est-à-dire le nombre de fois où il change par seconde. Par exemple, 1200 bauds impliquent que le signal change d’état toutes les 833 microsecondes.

Il ne faut pas confondre le baud avec le bit par seconde (bit/s), ce dernier étant l’unité de mesure du nombre d’informations effectivement transmises par seconde. Il est en effet souvent possible de transmettre plusieurs bits par intervalle unitaire. La mesure en bits par seconde de la vitesse de transmission est alors supérieure à la mesure en bauds.

Le datagramme ci-après illustre cette différence : pour transmettre un caractère, pas moins de 13 bits sont nécessaires. Pour cela, le signal va passer 8 fois entre les niveaux logiques 0 et 1, donc on aura un débit en bits par secondes 13/8 fois plus élevé que le débit en bauds.


UART, bauds


Ces vitesses de transmission sont normalisées et les valeurs usuelles sont les suivantes: 75, 110, 150, 300, 600, 900, 1200, 2400, 3600, 4800, 7200, 9600,19200, 38400, 76800 et 115200 bauds.

Matériel requis

Pour cet exemple vous aurez besoin de deux cartes NUCLEO-WB55 et de deux câbles dupont mâles / mâles. Reliez les broches RX et TX des deux cartes en “croisant” les câbles, comme illustré par la figure ci-dessous :


UART, Two connected NUCLEO-WB55


Le code MicroPython

Le script qui suit utilise l’UART des deux cartes NUCLEO-WB55 qui vont s’échanger en séquence des messages contenant leurs numéros d’identification uniques.

Les scripts présentés ci-après sont disponibles dans la zone de téléchargement.

Pour chacune des deux cartes NUCLEO-WB55, éditez le script main.py contenu dans le répertoire du disque USB virtuel associé à la NUCLEO-WB55 : PYBFLASH.

# Objet du script : 
# Echanger des messages textuels entre deux NUCLEO-WB55 en utilisant l'UART 2 connecté
# sur les broches D0 (RX) et D1 (TX).
# Mise en oeuvre : copiez ce script dans le dossier "PYBFLASH" des deux cartes, 
# et reliez la broche RX (respectivement TX) de l'une à la broche TX (respectivement RX) de l'autre.
# Lancez les deux scripts et observez les échanges de messages.

from time import sleep_ms, time # Classes pour faire des pauses en millisecondes et pour l'horodatage
from machine import unique_id # Classe pour obtenir un identifiant unique de la NUCLEO-WB55
from machine import UART # Classe pour gérer l'UART
from ubinascii import hexlify # Classe pour convertir un nombre hexadécimal en sa représentation binaire affichable

# Obtient un identifiant unique de la carte, en donne une représentation texte codée UTF8
id_carte = hexlify(unique_id()).decode("utf-8")
print("Identifiant de la carte : " + id_carte)

# Temporisation en millisecondes pour la boucle principale
delai_while = const(500)

# Constantes relatives au paramétrage de l'UART
delai_timeout = const(100) # Durée (en millisecondes) pendant laquelle l'UART attend de reçevoir un message
debit = const(115200) # Débit, en bauds, de la communication série
Numero_UART = const(2) # Identifiant de l'UART de la NUCLEO-WB55 qui sera utilisé
RX_BUFF = const(64) # Taille du buffer de réception (les messages reçus seront tronqués à ce nombre de caractères)

# Initialisation de l'UART
uart = UART(Numero_UART, debit, timeout = delai_timeout, rxbuf = RX_BUFF) 

# Première lecture pour "vider" la file de réception RX de l'UART
uart.read()

while True: # Boucle sans clause de sortie ("infinie")

	# Horodatage
	timestamp = time()

	# Message à envoyer : l'identifiant unique de la NUCLEO-WB55
	message_a_expedier = id_carte 

	# Expédition du message
	# Ecriture des octets / caractères dans la file d'émission Tx.
	uart.write(message_a_expedier)

	# Affiche le message expédié sur le port série de l'USB USER
	print(str(timestamp) + " Message envoyé : " + id_carte)

	# Réception d'un éventuel message
	# Lecture des octets / caractères dans la file de réception Rx.
	message_recu = uart.read() # Lis les caractères reçus jusqu'à la fin.

	# S'il y avait effectivement un message en attente dans Rx ...
	if not (message_recu is None) :

		# Interprête les octets lus comme une chaîne de caractères encodée en UTF8
		message_decode = message_recu.decode("utf-8")

		# Affiche le message reçu, précédé de l'horodatage, sur le port série de l'USB USER
		print(str(timestamp) + " Message reçu : " + message_decode)

	# Temporisation
	sleep_ms(delai_while) # Attends delai_while millisecondes

Sur chacun des terminaux PuTTY vous pourrez lire la séquence des échanges entre les deux cartes. Dans notre exemple ci-dessous, l’une des cartes était connectée au COM12 de notre PC sous Windows 10 et l’autre sur son COM15 :


UART, feed back on Windows 10 COM12 and Windows 10 COM15


Pour aller plus loin : gestion par interruptions

En général, il n’est pas possible de prévoir à quel moment exact un message va être reçu (ou devra être envoyé). La solution que nous avons programmée gère cette problématique de façon peu subtile : une “boucle infinie” interroge l’UART à grande fréquence et teste la réception éventuelle de caractères.

Un moyen pour rendre la réception asynchrone est donné par le script qui suit. On envoie un message depuis une carte en appuyant sur son bouton SW1 et on utilise l’interruption de l’UART en réception sur l’autre carte, pour réagir au message seulement lorsqu’il est reçu.

Premier exemple de code MicroPython avec une gestion de l’UART par interruptions

Les scripts présentés ci-après sont disponibles dans la zone de téléchargement.

Pour chacune des deux cartes NUCLEO-WB55, éditez le script main.py contenu dans le répertoire du disque USB virtuel associé à la NUCLEO-WB55 : PYBFLASH.

# Objet du script : Gestion UART par interruptions (version 1)
# Echanger des messages textuels entre deux cartes NUCLEO-WB55 en utilisant l'UART 2 connecté
# sur les broches D0 (RX) et D1 (TX).
# On utilise cette fois-ci des interruptions :
# - Une attachée au bouton SW1, pour envoyer un message
# - Une attachée au canal de réception (RX) pour afficher un message reçu
# Mise en oeuvre : copiez ce script dans le dossier "PYBFLASH" des deux cartes, 
# et reliez la broche RX (respectivement TX) de l'une à la broche TX (respectivement RX) de l'autre.
# Lancez les deux scripts et observez les échanges de messages.

from pyb import Pin # Pour gérer les GPIO
from time import time # Pour l'horodatage
from machine import unique_id, UART # Pour obtenir un identifiant unique de la NUCLEO-WB55 et pour gérer l'UART
from ubinascii import hexlify # Pour convertir un nombre hexadécimal en sa représentation binaire affichable

# Obtient un identifiant unique de la carte, en donne une représentation texte codée UTF8
id_carte = hexlify(unique_id()).decode("utf-8")
print("Identifiant de la carte : " + id_carte)

# Variables globales modifiées par les interruptions
message_recu = None
bouton_appuye = 0

# Initialisation du bouton SW1
sw1 = Pin('SW1')
sw1.init(Pin.IN, Pin.PULL_UP, af=-1)

# Fonction de service de l'interruption pour SW1
def Envoi(line):
	global bouton_appuye
	bouton_appuye = 1

# On active l'interruption du bouton
irq_bouton = ExtInt(sw1, ExtInt.IRQ_FALLING, Pin.PULL_UP, Envoi)

# Constantes relatives au paramétrage de l'UART
delai_timeout = const(100) # Durée (en millisecondes) pendant laquelle l'UART attend de reçevoir un message
debit = const(115200) # Débit, en bauds, de la communication série
Numero_UART = const(2) # Identifiant de l'UART de la carte NUCLEO-WB55 qui sera utilisé
RX_BUFF = const(64) # Taille du buffer de réception (les messages reçus seront tronqués à ce nombre de caractères)

# Initialisation de l'UART
uart = UART(Numero_UART, debit, timeout = delai_timeout, rxbuf = RX_BUFF) 

# Première lecture pour "vider" la file de réception RX de l'UART
uart.read() 

# Fonction de service de l'interruption de réception de l'UART
def Reception(uart_object):
	 # Lecture des caractères reçus
	message_recu = uart_object.read()
	# Si réception d'un message
	if not (message_recu is None):
		# Horodatage
		timestamp = time()
		# Affiche le message reçu, précédé de l'horodatage, sur le port série de l'USB USER
		print(str(timestamp) + " Message reçu : " + message_recu.decode("utf-8"))

# On active l'interruption de l'UART (vecteur d'interruption)
irq_uart = uart.irq(Reception, UART.IRQ_RXIDLE, False)

while True: # Boucle sans clause de sortie ("infinie")

	# Si appui sur bouton
	if bouton_appuye == 1 :

		# Horodatage
		timestamp = time()

		# Message  à envoyer : l'identifiant unique de la carte
		message_a_expedier = id_carte 

		# Expédition du message
		# Ecriture des octets / caractères dans la file d'émission Tx.
		uart.write(message_a_expedier)

		# Affiche le message expédié sur le port série de l'USB USER
		print(str(timestamp) + " Message envoyé : " + id_carte)

		bouton_appuye = 0

On pourrait objecter que le code que nous venons de voir fait toujours appel à une boucle infinie pour gérer l’envoi. Il est effectivement possible de traiter également celui-ci dans une fonction de service d’interruption (ISR), c’est ce que montre le script qui suit.

Cependant, même si ce nouveau code paraît de prime abord astucieux, il n’est pas du tout facile à écrire car la gestion de la mémoire par les ISR avec MicroPython reste encore à cette date contraignante et limitée ; elle donne parfois lieu à des bugs compliqués. Nous vous conseillons donc de conserver une boucle principale pour y effectuer un maximum de traitements après notification d’une interruption, à l’image de ce qui est fait dans le script précédent.

Deuxième exemple de code MicroPython avec une gestion de l’UART par interruptions

Les scripts présentés ci-après sont disponibles dans la zone de téléchargement.

Pour chacune des deux cartes NUCLEO-WB55, éditez le script main.py contenu dans le répertoire du disque USB virtuel associé à la NUCLEO-WB55 : PYBFLASH.

# Objet du script : Gestion UART par interruptions (version 2) 
# Echanger des messages textuels entre deux cartes NUCLEO-WB55 en utilisant l'UART 2 connecté
# sur les broches D0 (RX) et D1 (TX).
# On utilise cette fois-ci des interruptions :
# - Une attachée au bouton SW1, pour envoyer un message
# - Une attachée au canal de réception (RX) pour afficher un message reçu
# Mise en oeuvre : copiez ce script dans le dossier "PYBFLASH" des deux cartes, 
# et reliez la broche RX (respectivement TX) de l'une à la broche TX (respectivement RX) de l'autre.
# Lancez les deux scripts et observez les échanges de messages.

from pyb import Pin # Pour gérer les GPIO
from time import time # Pour l'horodatage
from machine import unique_id # Pour obtenir un identifiant unique de la NUCLEO-WB55
from ubinascii import hexlify # Pour convertir un nombre hexadécimal en sa représentation binaire affichable

# Initialisation du bouton SW1
sw1 = Pin('SW1')
sw1.init(Pin.IN, Pin.PULL_UP, af=-1)

# Identifiant de la carte
# Cette opération nécessite des manipulations en mémoire qui ne peuvent pas être effectuées
# dans une fonction de service d'interruption.

id_carte = hexlify(unique_id()).decode("utf-8")

# Fonction de service de l'interruption du bouton SW1
def Envoi(line):
	# Récupération de l'identifiant, partagé comme une variable globale
	global id_carte
	# Ecriture des octets / caractères dans la file d'émission Tx.
	uart.write(id_carte)

# On active l'interruption du bouton
irq_bouton = ExtInt(sw1, ExtInt.IRQ_FALLING, Pin.PULL_UP, Envoi)

# Constantes relatives au paramétrage de l'UART
delai_timeout = const(100) # Durée (en millisecondes) pendant laquelle l'UART attend de reçevoir un message
debit = const(115200) # Débit, en bauds, de la communication série
Numero_UART = const(2) # Identifiant de l'UART de la carte NUCLEO-WB55 qui sera utilisé
RX_BUFF = const(64) # Taille du buffer de réception (les messages reçus seront tronqués à ce nombre de caractères)
#TX_BUFF = const(64) # Taille du buffer d'émission (on ne peut pas envoyer des messages comportant plus de caractères)

# Initialisation de l'UART
uart = UART(Numero_UART, debit, timeout = delai_timeout, rxbuf = RX_BUFF) 

# Première lecture pour "vider" la file de réception RX de l'UART
uart.read()

# Fonction de service de l'interruption de réception de l'UART
def Reception(uart_object):
	
	 # Lecture des caractères reçus
	message_recu = uart_object.read()
	
	# Si réception d'un message
	if not (message_recu is None):
		
		# Horodatage
		timestamp = time()

		# Affiche le message reçu, précédé de l'horodatage, sur le port série de l'USB USER
		print(str(timestamp) + " Message reçu : " + message_recu.decode("utf-8"))

# On active l'interruption de l'UART (vecteur d'interruption)
irq_uart = uart.irq(Reception, UART.IRQ_RXIDLE, False)

Informations complémentaires