Use serial communication

UART for Universal Asynchronous Receiver Transmitter is a component used to link the microcontroller to one of its serial ports. There is also a variant of the UART, theUSART for Universal Synchronous/ Asynchronous Receiver Transmitter. In the context of MicroPython programming, the distinction between the two should not be made and we will later consider that UART designates these two components.

The UART therefore allows serial communication between two systems, that is to exchange messages formed by bits sequences, usually interpreted as text. It can be used to interact with a device and drive it (for example a GPS module, WiFi module or ultrasonic distance sensor, several tutorials illustrating this use are available in this section) or to exchange information between two boards as we will show in this example.

You have been using the NUCLEO-WB55 UART since the beginning of the exercises, perhaps without being aware of it: the one that is wired with the USB USER and allows you to exchange with the serial terminal you have chosen (PuTTY or other, see figure below).


UART USB USER


Some additions on serial communication in microcontrollers

The UART allows data exchange on two wires (Rx and Tx) between a transmitter and a receiver. The process is asynchronous; no clock signal is used to time exchanges.

Communication can be one-way (from a transmitter to a receiver), in which case the UART is said to operate in half duplex mode. If the communication is two-way (each of the two systems can be in turn transmitter or receiver), it is called full duplex mode. That’s the one we’ll use afterwards.

An exchanged message, also called frame is composed of a start bit, 5 to 9 bits of data, 1 or 2 stop bits and 1 parity bit optional. The parity bit is used to detect transmission errors. Parity is generated by the issuer and verified by the receiver. For an even parity, the number of 1 in the data plus the parity bit is even, while for an odd parity the number of 1 in the data plus that of the parity is odd.

The flow of a UART is expressed in bauds. The baud represents the frequency of (de)modulation of a signal, for example that sent or received by a modem (modulator-demodulator), that is, the number of times it changes per second. For example, 1200 bauds implies that the signal changes state every 833 microseconds.

The baud should not be confused with the bit per second (bit/s), the latter being the unit of measure of the number of information actually transmitted per second. It is often possible to transmit several bits per unit interval. The bit-per-second measurement of the transmission speed is then higher than the baud measurement.

The following datagram illustrates this difference: to transmit a character, no less than 13 bits are required. For this, the signal will pass 8 times between logical levels 0 and 1, so we will have a bit rate per second 13/8 times higher than the baud rate.


UART, bauds


These transmission speeds are standardized and the usual values are: 75, 110, 150, 300, 600, 900, 1200, 2400, 3600, 4800, 7200, 9600,19200, 38400, 76800 and 115200 bauds.

Required Hardware

For this example you will need two NUCLEO-WB55 boards and two male/male cables. Connect the RX and TX pins of the two boards by “crossing” the cables as shown in the figure below:


UART, Two connected NUCLEO-WB55


MicroPython code

The following script uses the UART of the two NUCLEO-WB55 boards which will exchange messages containing their unique identification numbers in sequence.

The following scripts are available in download area.

For each of the two NUCLEO-WB55 boards, edit the main.py script in the directory of the NUCLEO-WB55 virtual USB disk: PYBFLASH.

# Objet of the script : 
# Exchange text messages between two NUCLEO-WB55 using the connected UART 2
# on pins D0 (RX) and D1 (TX).
# Implementation: copy this script in the "PYBFLASH" folder of the two boards, 
# and connect the RX pin (TX respectively) from one to the TX pin (RX respectively) from the other.
# Run both scripts and watch the message exchanges.

from time import sleep_ms, time # Classes to pause in milliseconds and for timestamp
from machine import unique_id # Class to obtain a unique identifier of the NUCLEO-WB55
from machine import UART # Classe to manage the UART
from ubinascii import hexlify # Class to convert a hexadecimal number to its binary display

# Obtains a unique board identifier, gives a UTF8 coded text representation
id_carte = hexlify(unique_id()).decode("utf-8")
print("Board Identifier : " + id_carte)

# Delay in milliseconds for main loop
delai_while = const(500)

# UART Setting Constants
delai_timeout = const(100) # Time (in milliseconds) during which the UART waits to receive a message
debit = const(115200) # Baud rate of the serial communication
Numero_UART = const(2) # UART identifier for the NUCLEO-WB55 to be used
RX_BUFF = const(64) # Size of the receiving buffer (the received messages will be truncated to this number of characters)

# UART Initialization
uart = UART(Numero_UART, debit, timeout = delai_timeout, rxbuf = RX_BUFF) 

# First reading to "empty" UART RX receive queue
uart.read()

while True: # Loop without output clause ("infinite")

	# Timestamp
	timestamp = time()

	# Message to be sent: the unique identifier of the NUCLEO-WB55
	message_a_expedier = id_carte 

	# Message Delivery
	# Write bytes/characters in the Tx transmit queue.
	uart.write(message_a_expedier)

	# Displays the sent message on the serial port of the USB USER
	print(str(timestamp) + " Sent message : " + id_carte)

	# Receiving a possible message
	# Reading bytes/characters in the Rx receive queue.
	message_recu = uart.read() # Read the received characters to the end.

	# If there was indeed a pending message in Rx ...
	if not (message_recu is None) :

		# Interprets bytes read as a string encoded in UTF8
		message_decode = message_recu.decode("utf-8")

		# Displays the received message, preceded by the time stamp, on the serial port of the USB USER
		print(str(timestamp) + " received message : " + message_decode)

	# Timeout
	sleep_ms(delai_while) # wait for "delai" milleseconds

On each of the PuTTY terminals you can read the sequence of exchanges between the two boards. In our example below, one of the boards was connected to the COM12 of our PC on Windows 10 and the other on its COM15:


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


To go further: management by interrupts

In general, it is not possible to predict exactly when a message will be received (or will need to be sent). The solution we have programmed manages this problem in a rather subtle way: an “infinite loop” questions the UART at high frequency and tests the possible reception of characters.

A way to make reception asynchronous is given by the following script. A message is sent from one board by pressing its SW1 button and the receiving UART interrupt is used on the other board to respond to the message only when it is received.

First example of MicroPython code with interrupt UART management

The following scripts are available in download area.

For each of the two NUCLEO-WB55 boards, edit the main.py script in the directory of the NUCLEO-WB55 virtual USB disk: PYBFLASH.

# Objet of the script : Managing the UART via interrupts (version 2) 
# Exchange text messages between two NUCLEO-WB55 boards using the connected UART 2
# on pins D0 (RX) and D1 (TX).
# This time we use interrupts:
# - One attached to the SW1 button, to send a message
# - One attached to the receive channel (RX) to display a received message
# Implementation: copy this script in the "PYBFLASH" folder of the two caboardsrds, 
# and connect the RX pin (TX respectively) from one to the TX pin (RX respectively) from the other.
# Run both scripts and watch the message exchanges.

from pyb import Pin # for managing7 GPIO
from time import time # for timestamp
from machine import unique_id, UART # To obtain a unique identifier for the NUCLEO-WB55 and to manage the UART
from ubinascii import hexlify # To convert a hexadecimal number to its binary viewable representation

# Obtains a unique board identifier, gives a UTF8 coded text representation
id_carte = hexlify(unique_id()).decode("utf-8")
print("Board identifier : " + id_carte)

# Global variables modified by interrupts
message_recu = None
bouton_appuye = 0

# SW1 Button Initialization
sw1 = Pin('SW1')
sw1.init(Pin.IN, Pin.PULL_UP, af=-1)

# Interrupt service function for SW1
def Envoi(line):
	global bouton_appuye
	bouton_appuye = 1

# Activate the button interrupt
irq_bouton = ExtInt(sw1, ExtInt.IRQ_FALLING, Pin.PULL_UP, Envoi)

# UART Setting Constants
delai_timeout = const(100) # Time (in milliseconds) during which the UART waits to receive a message
debit = const(115200) # Baud rate of serial communication
Numero_UART = const(2) # UART identifier of the NUCLEO-WB55 board to be used
RX_BUFF = const(64) # Size of the receiving buffer (the received messages will be truncated to this number of characters)

# UART Initialization
uart = UART(Numero_UART, debit, timeout = delai_timeout, rxbuf = RX_BUFF) 

# First read to "empty" UART RX receive queue
uart.read() 

# Reception Interrupt Service Function for the UART
def Reception(uart_object):
	 # Reading the received characters 
	message_recu = uart_object.read()
	# If a message si received
	if not (message_recu is None):
		# Timestamp
		timestamp = time()
		# Displays the received message, preceded by the time stamp, on the serial port of the USB USER
		print(str(timestamp) + " Received message : " + message_recu.decode("utf-8"))

# Activating the UART Interrupt (Interrupt Vector)
irq_uart = uart.irq(Reception, UART.IRQ_RXIDLE, False)

while True: # Loop without output clause ("infinite")

	# If button is pressed
	if bouton_appuye == 1 :

		# Timestamp
		timestamp = time()

		# Message to be sent: the unique identifier of the board
		message_a_expedier = id_carte 

		# Message Delivery
		# Write bytes/characters in the Tx transmit queue.
		uart.write(message_a_expedier)

		# Displays the sent message on the serial port of the USB USER
		print(str(timestamp) + " Sent message : " + id_carte)

		bouton_appuye = 0

One might object that the code we have just seen always uses an infinite loop to manage the sending. It is indeed possible to also process this one in an interrupt service (ISR) function, as shown in the following script.

However, even if this new code seems clever at first glance, it is not at all easy to write because the memory management by the ISRs with MicroPython still remains constrained and limited; it sometimes gives rise to complicated bugs. We therefore advise you to keep a main loop to perform a maximum of processing after notification of an interrupt, as is done in the previous script.

Second example of MicroPython code with UART management by interrupts

The following scripts are available in download area.

For each of the two NUCLEO-WB55 boards, edit the main.py script in the directory of the NUCLEO-WB55 virtual USB disk: PYBFLASH.

# Objet of the script : Managing the UART via interrupts (version 2) 
# Exchange text messages between two NUCLEO-WB55 boards using the connected UART 2
# on pins D0 (RX) and D1 (TX).
# This time we use interrupts:
# - One attached to the SW1 button, to send a message
# - One attached to the receive channel (RX) to display a received message
# Implementation: copy this script in the "PYBFLASH" folder of the two boards, 
# and connect the RX pin (TX respectively) from one to the TX pin (RX respectively) from the other.
# Run both scripts and watch the message exchanges.

from pyb import Pin # managing GPIO
from time import time # for timestamp
from machine import unique_id # To obtain a unique identifier for the NUCLEO-WB55 and to manage the UART
from ubinascii import hexlify #To convert a hexadecimal number to its binary viewable representation

# SW1 Button Initialization
sw1 = Pin('SW1')
sw1.init(Pin.IN, Pin.PULL_UP, af=-1)

# Board identifier
# This operation requires manipulations in memory that cannot be performed
# in an interrupt service function.

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

# Interrupt service function for SW1
def Envoi(line):
	# Retrieval of the identifier, shared as a global variable
	global id_carte
	# Write bytes/characters in the Tx transmit queue.
	uart.write(id_carte)

# Activating the button interrupt
irq_bouton = ExtInt(sw1, ExtInt.IRQ_FALLING, Pin.PULL_UP, Envoi)

# UART Setting Constants
delai_timeout = const(100) # Time (in milliseconds) during which the UART waits to receive a message
debit = const(115200) # Baud rate of serial communication
Numero_UART = const(2) # UART identifier of the NUCLEO-WB55 board to be used
RX_BUFF = const(64) # Size of the receiving buffer (the received messages will be truncated to this number of characters)
#TX_BUFF = const(64) # Transmit buffer size (cannot send messages with more than one character)

# UART Initialization
uart = UART(Numero_UART, debit, timeout = delai_timeout, rxbuf = RX_BUFF) 

# First read to "empty" UART RX receive queue
uart.read()

# Reception Interruption Service Function for the UART
def Reception(uart_object):
	
	 # Reading the received characters 
	message_recu = uart_object.read()
	
	# if a message is received
	if not (message_recu is None):
		
		# timestamp
		timestamp = time()

		# Displays the received message, preceded by the timestamp, on the serial port of the USB USER
		print(str(timestamp) + " Received message : " + message_recu.decode("utf-8"))

# Enable UART Interrupt (Interrupt Vector)
irq_uart = uart.irq(Reception, UART.IRQ_RXIDLE, False)

Further information