Liaison UART


DOCUMENTATION


La liaison UART (Universal Asynchronous Receiver Transceiver ) est une vieille liaison, peu performante, historiquement utilisée pour le dialogue entre un PC et un modem.
Cette liaison est malgré tout toujours présente dans les microprocesseurs en raison de sa simplicité de mise en oeuvre, dans un contexte de Debug ou pour un dialogue avec un capteur.


1 - La trame UART

Une trame UART permet d’envoyer un seul caractère, de longueur 7,8 ou 9 bits.

Pour chaque caractère, il faut ajouter :

  • Un bit de start
  • Un bit de parité (optionnel) : on réalise un OU exclusif entre tous les bits de la donnée ( 1 si nombre impair de bits, 0 sinon ). Cette opération est réalisée à nouveau à la réception et permet de détecter un éventuel problème de transmission.
  • 0,1 ou 2 bits de Stop : permet de garantir un front descendant pour chaque bit de start dans le cas d’un envoi de plusieurs caractères consécutifs.

trame_uart.svg


2 - Configuration du périphérique UART

uart.svg

2.1 - Configuration d’une trame

Le microcontrôleur STM32F411 présente 3 registres de contrôle/configuration.

Dans ces 3 registres, les bits importants pour une utilisation minimale sont :

  • UE ( CR1 - bit 13 ) : UART Enable
  • M ( CR1 - bit 12 ) : Word length ( 8 ou 9 bits )
  • PCE ( CR1 - bit 10 ) : Parity Control Enable
  • TE ( CR1 - bit 3 ) : Transmitter Enable
  • RE ( CR1 - bit 2 ) : Receiver Enable
  • STOP( CR2 - bits 13:12 ) : Nombre de bits de Stop
  • RXNEIE ( CR1 - bit 5 ) : RX Not Empty Interrupt Enable : Autorisation des Interruptions en Réception

registres_CR.svg

2.2 - Configuration du baud rate ( débit sur la ligne )

Le débit sur la ligne est plus lent que l’horloge du périphérique UART.
Il va donc falloir diviser la fréquence afin de définir le baud rate.
On a vu dans le chapitre ’liaison série’ qu’il fallait attendre T/2 pour faire l’acquisition d’un bit ( T étant la durée d’un bit ), afin d’être sûr d’avoir un état stabilisé.
Pour garantir ce positionnement par rapport au débit sur la ligne, on divise le temps en quantums de temps 16 fois plus rapides que le débit.

Il faut donc régler dans le registre BRR un ratio tel que :

USARTDIV=FCK16.baudrate USARTDIV = \frac{F_{CK}}{16.baudrate}

Le registre BRR contient une valeur au format virgule fixe 12.4

soit FCK=42MHz F_{CK} = 42MHz

Je souhaite baudrate = 9600

J’ai donc :
USARTDIV=FCKbaudrate USARTDIV= \frac{F_{CK}}{baudrate}
USARTDIV=42.106169600 USARTDIV= \frac{42.10^6}{16*9600}
USARTDIV=273.4375 USARTDIV= 273.4375

Comme on est au format 12.4, le codage de 273.4375 est le même que celui de 273.737524=4375 273.7375*2^4 = 4375

Conclusion :

BRR=FCKbaudrate \boxed{ BRR = \frac{F_{CK}}{baudrate} }

registre_BRR.svg


3 - Le périphérique UART en action

3.1 - Emission d’un octet

Pour simplifier On fait l’hypothèse que les données font toujours 8 bits, qu’il n’y a pas de bit de parité, et qu’il y a un bit de stop.

3.2 - Réception d’un octet ( avec interruption )


4 - Programmation du périphérique UART

RAPPEL : Les registres sont détaillés dans la documentation RefManual_STM32F411.pdf

4.1 - Configuration des Broches

Le STM32F411 comporte 3 périphériques UART ( USART1, USART2, USART6 ).
USART2 est relié au processeur gérant l’interface de debug de la carte ( STLINK ) , de telle sorte qu’il y ait conversion UART –> USB.
Ainsi la liaison UART2 est accessible directement par le cable USB.

Extrait de la documentation de la carte Nucleo F411 :

uart2_usb.svg

Les broches à configurer sont donc PA2 (TX) et PA3 (RX) ( Datasheet STM32F411 ) :

PA2 et PA3 doivent donc être configurées en Alternate Function 07

REMARQUE : PA3 est une entrée d’un périphérique, elle peut être également configurée en Input

stm32f4xx_hal_msp.c
void HAL_UART2_MspInit(void)
{
  GPIO_InitTypeDef  GPIO_InitStruct;

  __GPIOA_CLK_ENABLE();
  __USART2_CLK_ENABLE();

  GPIO_InitStruct.Pin       = GPIO_PIN_2 | GPIO_PIN_3;
  GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull      = GPIO_NOPULL;
  GPIO_InitStruct.Speed     = GPIO_SPEED_MEDIUM;
  GPIO_InitStruct.Alternate = GPIO_AF7_USART2;

  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}  

4.2 - Configuration du périphérique

Nous allons considérer la structure UART_HandleTypeDef suivante :

stm32f4xx_hal_uart.h
typedef struct __UART_HandleTypeDef
{
  USART_TypeDef                 *Instance;        /*!< UART registers base address        */
  UART_InitTypeDef              Init;             /*!< UART communication parameters      */
  uint8_t                       *pTxBuffPtr;      /*!< Pointer to UART Tx transfer Buffer */
  uint16_t                      TxXferSize;       /*!< UART Tx Transfer size              */
  __IO uint16_t                 TxXferCount;      /*!< UART Tx Transfer Counter           */
  uint8_t                       *pRxBuffPtr;      /*!< Pointer to UART Rx transfer Buffer */
  uint16_t                      RxXferSize;       /*!< UART Rx Transfer size              */
  __IO uint16_t                 RxXferCount;      /*!< UART Rx Transfer Counter           */
} UART_HandleTypeDef;

USART_TypeDef Permet d’accéder aux différents registres de UART :

stm32f411xe.h
typedef struct
{
  __IO uint32_t SR;         /*!< USART Status register,                   Address offset: 0x00 */
  __IO uint32_t DR;         /*!< USART Data register,                     Address offset: 0x04 */
  __IO uint32_t BRR;        /*!< USART Baud rate register,                Address offset: 0x08 */
  __IO uint32_t CR1;        /*!< USART Control register 1,                Address offset: 0x0C */
  __IO uint32_t CR2;        /*!< USART Control register 2,                Address offset: 0x10 */
  __IO uint32_t CR3;        /*!< USART Control register 3,                Address offset: 0x14 */
  __IO uint32_t GTPR;       /*!< USART Guard time and prescaler register, Address offset: 0x18 */
} USART_TypeDef;

UART_InitTypeDef Permet de définir les différents éléments de configuration :

stm32f4xx_hal_uart.h
typedef struct
{
  uint32_t BaudRate;
  uint32_t WordLength;
  uint32_t StopBits;
  uint32_t Parity;
  uint32_t Mode;
  uint32_t HwFlowCtl;
  uint32_t OverSampling;
} UART_InitTypeDef;

La fonction HAL_UART_Init() permet de configurer les registres du périphérique à partir des éléments définis dans le champ Init de la structure UART_HandleTypeDef

Appel de HAL_UART_Init() en début du main() :

main.c
	huart2.Instance          = USART2;
	huart2.Init.BaudRate     = 115200;
	huart2.Init.WordLength   = UART_WORDLENGTH_8B;
	huart2.Init.StopBits     = UART_STOPBITS_1;
	huart2.Init.Parity       = UART_PARITY_NONE;
	HAL_UART_Init(&huart2);

4.3 - Emission d’un Caractère

Dès lors que j’ai branché ma carte sur le PC, il se crée un fichier /dev/ttyACM0 auquel je ferai référence côté PC pour désigner l’émission et la réception de caractère sur l’UART.
Ouvrons sur le PC un terminal série ( gtkterm ou avec la commande minicom -D /dev/ttyACM0 -b 115200 )

Si j’écris, dans le programme de la carte :

main.c
huart2.Instance->DR = 'a';

J’écris le codage ASCII de ‘a’ dans le registre de transmission, ce caractère est sérialisé.

Si maintenant j’écris :

main.c
huart2.Instance->DR = 'a';
huart2.Instance->DR = 'b';
huart2.Instance->DR = 'c';

seul le caractère ‘c’ s’affiche, je n’ai pas attendu qu’un caractère soit sérialisé pour envoyer le suivant.
Il faut donc tester la bonne émission de chaque caractère avant d’écrire dans DR.
Le bit TXE ( registre USART_SR, bit 7 ) Transmit data register empty vaut 1 quand le registre TX est vide, autrement dit la donnée a bien été transmise au registre sérialisateur, on peut écrire une nouvelle donnée dans DR.

registre_SR.svg

Ainsi on peut écrire :

main.c
...
huart2.Instance->DR = 'a';
while ((huart2.Instance->SR & (1<<7)) == 0) {} ;
huart2.Instance->DR = 'b';
while ((huart2.Instance->SR & (1<<7)) == 0) {} ;
huart2.Instance->DR = 'c';
while ((huart2.Instance->SR & (1<<7)) == 0) {} ;
...

4.4 - Réception d’un Caractère sur Interruption

Autorisation de l’Interruption USART2 niveau NVIC :

stm32f4xx_hal_msp.c
void HAL_UART2_MspInit(void)
{
  ... 
  NVIC_SetPriority(USART2_IRQn, USART2_IRQ_PRIO);
  NVIC_EnableIRQ(USART2_IRQn);
} 

Autorisation de l’Interruption USART2 niveau Périphérique :

stm32f4xx_hal_uart.c
int HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
	huart->Instance->CR1 = (huart->Instance->CR1) | (1<<5); // Receiver Not Empty Interrupt Enable

	huart->pRxBuffPtr = pData;
	huart->RxXferSize = Size;
	huart->RxXferCount = Size;

	return 0;
}

IRQ Handler :

REMARQUE : La lecture du registre DR est dans tous les cas nécessaire afin d’acquiter la demande d’interruption.

stm32f4xx_hal_uart.c
void HAL_USART_IRQHandler(UART_HandleTypeDef *huart)
{
	uint32_t sr = huart->Instance->SR;

	if (sr & (1<<5)) // Read data register not empty interrupt
		{
			UART_Receive_IT(huart);
		}
	else if (sr & (1<<4)) // idle interrupt
		{ huart->Instance->DR;}
	else if (sr & (1<<3)) // overrun interrupt
		{ huart->Instance->DR;}
	else if (sr & (1<<0)) // parity error interrupt
		{ huart->Instance->DR;}
}

void UART_Receive_IT(UART_HandleTypeDef *huart)
{

	*huart->pRxBuffPtr++ = (uint8_t)(huart->Instance->DR);
	huart->RxXferCount--;

	 if (huart->RxXferCount == 0U)
	 {
		 huart->Instance->CR1 = (huart->Instance->CR1) & ~(1<<5); // Disable Int
		 HAL_UART_RxCpltCallback(huart);

	 }
}

Fonction de Callback liée à la réception d’un caractère :

main.c
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	uint8_t c=0;

	if(huart == &huart2)
	{
		c = huart2.Instance->DR;
		HAL_UART_Receive_IT(&huart2, buf, 1);
	}
}

5 - Travaux Pratiques

PROJET SOURCE

WORKSPACE_F411_HAL_STM32CUBE.zip

5.1 - Vérification de la configuration

Q1. Compléter le fichier stm32f4xx_hal_msp.c afin de configurer les broches PA2 et PA3 en tant que broches TX et RX du périphérique USART2.
La réception d’un caractère devant déclencher une interruption, le NVIC doit être configuré.

Q2. Véfifier à l’aide du debugger la valeurs des bits UE, M, PCE, TE, RE, STOP à l’issue de l’appel de la fonction HAL_UART_Init()

5.2 - Envoi d’une chaine de caractères : HAL_UART_Transmit()

Q3. Compléter la fonction HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size) permettant d’envoyer Size caractères situés à l’adresse pData avec le périphérique huart

Q4. Tester la fonction HAL_UART_Transmit() :

  • Déclarer une chaine de caractères
  • Appeler la fonction HAL_UART_Transmit() depuis main()
  • Vérifier à l’aide du debugger l’emplacement mémoire de la chaine de caractères
  • Vérifier que la chaine de caractères s’affiche bien dans le terminal série

Q5. Compléter les fonctions uart_putc() et uart_puts permettant d’envoyer respectivement un caractère et une chaine de caractères se terminant par le caractère nul ( code ascii 0x00 )

Q6. Réaliser l’enregistrement d’une trame UART avec un analyseur logique.

5.3 - Réception de caractères sur interruptions

Q7. Placer un point d’arrêt dans la routine d’interruption HAL_USART_IRQHandler() et vérfier qu’à chaque caractère reçu on s’y arrête bien.
Vérifier que le caractère reçu est bien celui qui a été envoyé depuis le PC avec le terminal série.

5.4 - Gestion d’un buffer Circulaire en réception

Q8. On veut faire varier la vitesse de clignotement d’une led ( R, G ou B ) depuis le PC.
Pour cela nous devons envoyer via la liaison UART des ordres au format suivant :

trame_circulaire.svg

La durée de clignotement est comprise entre 0 et 5000ms, cette donnée est envoyée sous forme de valeur ( et non en chaine de caractères ).

Le codage de cette valeur n’est donc pas possible sur 8 bits, il faut 2 octets.

Pour répartir notre valeur sur 2 octets, on fera donc :

  • poids_fort = (uint8_t) ( valeur » 8 )
  • poids_faible = (uint8_t) ( valeur & 0xFF )

A.N : valeur = 1000 = 0x03E8

  • poids_fort = 0x03
  • poids_faible = 0xE8

Comme il faut recevoir et interpréter un message de plusieurs caractères, une méthode peut consister à :

  • Enregistrer chaque caractère dans un buffer circulaire à chaque interruption ; on fait alors évoluer le pointeur p_wr.

    buffer_circulaire.svg

main.c
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	uint8_t c=0;

	if(huart == &huart2)
	{
		c = huart2.Instance->DR;

		uart2_buffer[(p_wr++)%BUF_SIZE] = c; // % : operateur modulo : reste de la division
		size++;
	}
}
  • Dans la boucle principale du programme main, on vient régulièrement consulter le buffer circulaire ; on fait alors évoluer le pointeur p_rd jusqu’à ce qu’il pointe comme p_wr.
    Si plusieurs ordres sont présents, on ne retient que le dernier.

Pour envoyer les ordres depuis le PC, nous pouvons utiliser le programme python suivant :

#! /usr/bin/python3

import serial
import time
from tkinter import *
import struct

port='/dev/ttyACM0'
baudrate=115200
serBuffer = ""

########################################################################
#                        FUNCTIONS
########################################################################

def onPushSend():
    var_led=led.get() 
    var_blink=blinkTime.get()
    var_blink=int(var_blink)
    var_blink_low=var_blink & 0xFF
    var_blink_high=(var_blink >> 8) & 0xFF
    
    ser.write(struct.pack('B',0xFF)) # Header
    ser.write(var_led.encode())
    ser.write(struct.pack('B',var_blink_high))    
    ser.write(struct.pack('B',var_blink_low))    

########################################################################
#                             MAIN
########################################################################

if  __name__ == "__main__" :

    try: 
	    ser = serial.Serial(port, baudrate, bytesize=8, parity='N', stopbits=1, timeout=None, rtscts=False, dsrdtr=False)
	    print("serial port " + ser.name + " opened")
    except Exception:
        print("error open serial port: " + port)
        exit()

    ui=Tk()

    ui.title("SERIAL SEND ORDER")
    ui.geometry("200x200")

    labelToSendMes=Label(ui, text="LED (r/g/b)", font=("Arial", 10), fg="black")
    labelToSendMes.pack()
    
    led=StringVar()
    toSendEntry=Entry(ui, textvariable=led)
    toSendEntry.pack()
    
    labelToSendMes=Label(ui, text="Blink time (ms)", font=("Arial", 10), fg="black")
    labelToSendMes.pack()

    blinkTime=StringVar()
    toSendEntry=Entry(ui, textvariable=blinkTime)
    toSendEntry.pack()


    copyButton=Button(ui, text="SEND", font=("Arial",10, "bold"), bg="seagreen3", fg="black", bd=3, relief=RAISED, command=onPushSend)
    copyButton.pack()
        
    ui.mainloop()		# MAIN LOOP

########################################################################

Pour lancer le programme, dans un terminal :

$ chmod +x sendOrder.py
$ ./sendOrder.py