Les communications série, souvent désignées par le terme « port série », englobent diverses interfaces telles que UART, SPI et I2C. Dans le contexte des microcontrôleurs STM32, le UART (Universal Asynchronous Receiver/Transmitter) est l'interface la plus couramment utilisée pour la communication asynchrone. Le contrôleur UART intégré permet une configuration simple, mais pour les transmissions longue distance ou dans des environnements bruyants, une conversion de niveau électrique est nécessaire via des transceivers standards comme RS-232 ou RS-485.
La procédure d'initialisation d'un port UART sur STM32 suit ces étapes :
- Activation des horloges pour le périphérique UART et les broches GPIO via les fonctions RCC.
- Réinitialisation du périphérique UART avec la fonction de déinitialisation.
- Configuration des GPIO en mode fonction alternative et liaison aux broches UART appropriées.
- Paramétrage des caractéristiques du port UART : vitesse en bauds, longueur de mot, bits d'arrêt, etc.
- Configuration du contrôleur d'interruption NVIC et activation des interruptions UART spécifiques, comme la réception de données.
- Activation du périphérique UART.
- Implémentation du gestionnaire d'interruption pour traiter les événements de réception.
- Utilisation des fonctions d'envoi et de réception pour transférer des données.
Voici un exemple de structure de gestion pour un port UART, avec des noms modifiés pour plus de clarté :
typedef struct PortHandler {
uint8_t state;
uint8_t transmitBuffer[256];
uint16_t txCounter;
uint16_t txLength;
uint8_t receiveBuffer[256];
uint16_t rxCounter;
uint16_t timeoutCount;
} PortHandler;
La fonction d'niitialisation pour UART4, adaptée et renommée :
void ConfigureUart4(uint32_t baudRate) {
GPIO_InitTypeDef gpioConfig;
USART_InitTypeDef uartConfig;
NVIC_InitTypeDef nvicConfig;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOC, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_UART4, ENABLE);
GPIO_PinAFConfig(GPIOC, GPIO_PinSource10, GPIO_AF_UART4);
GPIO_PinAFConfig(GPIOC, GPIO_PinSource11, GPIO_AF_UART4);
gpioConfig.GPIO_Pin = GPIO_Pin_10;
gpioConfig.GPIO_Mode = GPIO_Mode_AF;
gpioConfig.GPIO_OType = GPIO_OType_PP;
gpioConfig.GPIO_PuPd = GPIO_PuPd_NOPULL;
gpioConfig.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &gpioConfig);
gpioConfig.GPIO_Pin = GPIO_Pin_11;
gpioConfig.GPIO_Mode = GPIO_Mode_AF;
gpioConfig.GPIO_OType = GPIO_OType_OD;
GPIO_Init(GPIOC, &gpioConfig);
gpioConfig.GPIO_Pin = GPIO_Pin_3;
gpioConfig.GPIO_Mode = GPIO_Mode_OUT;
GPIO_Init(GPIOE, &gpioConfig);
USART_DeInit(UART4);
uartConfig.USART_BaudRate = baudRate;
uartConfig.USART_WordLength = USART_WordLength_8b;
uartConfig.USART_StopBits = USART_StopBits_1;
uartConfig.USART_Parity = USART_Parity_No;
uartConfig.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;
USART_Init(UART4, &uartConfig);
nvicConfig.NVIC_IRQChannel = UART4_IRQn;
nvicConfig.NVIC_IRQChannelCmd = ENABLE;
nvicConfig.NVIC_IRQChannelPreemptionPriority = 1;
nvicConfig.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&nvicConfig);
USART_ITConfig(UART4, USART_IT_RXNE, ENABLE);
USART_Cmd(UART4, ENABLE);
}
Le gestionnaire d'interruption pour la réception, avec une logique ajustée :
void UART4_IRQHandler(void) {
if (USART_GetITStatus(UART4, USART_IT_RXNE) && ((portHandlers[4].state & 0x06) == 0x00)) {
portHandlers[4].state |= 0x01;
portHandlers[4].receiveBuffer[portHandlers[4].rxCounter] = USART_ReceiveData(UART4);
portHandlers[4].rxCounter++;
portHandlers[4].timeoutCount = 0;
if (portHandlers[4].rxCounter >= 256) {
portHandlers[4].rxCounter = 0;
portHandlers[4].state = 0x04;
}
}
}
Les fonctions d'envoi et de réception asynchrones, réécrites avec des noms de variables différents :
uint8_t SendDataAsync(PortHandler *port, uint8_t *data, uint16_t length) {
if (length < 256 && ((port->state & 0x03) == 0x00)) {
port->state |= 0x02;
port->txLength = length;
port->txCounter = 0;
memcpy(port->transmitBuffer, data, length);
return 1;
}
return 0;
}
uint16_t ReceiveDataAsync(PortHandler *port, uint8_t *buffer) {
uint16_t count;
if ((port->state & 0x08) == 0x08) {
if (port->rxCounter > 0) {
count = port->rxCounter;
memcpy(buffer, port->receiveBuffer, port->rxCounter);
port->rxCounter = 0;
port->timeoutCount = 0;
port->state &= ~0x08;
return count;
}
}
return 0;
}
Pour gérer les transmissions en mode semi-duplex comme RS-485, une fonction de traitement périodique est utilisée, avec des intervalles de 50 microsecondes. Cette fonction vérifie les tâches d'envoi en cours et gère les timeouts de réception à 500 microsecondes pour détecter la fin d'une trame.
void ProcessUartTask(void) {
if ((portHandlers[2].state & 0x02) == 0x02) {
if (USART_GetFlagStatus(USART2, USART_FLAG_TC) != RESET) {
USART_SendData(USART2, portHandlers[2].transmitBuffer[portHandlers[2].txCounter++]);
if (portHandlers[2].txCounter >= portHandlers[2].txLength) {
portHandlers[2].state &= ~0x02;
portHandlers[2].txCounter = 0;
portHandlers[2].txLength = 0;
}
}
} else if ((portHandlers[2].state & 0x01) == 0x01) {
portHandlers[2].timeoutCount++;
if (portHandlers[2].timeoutCount >= 10) {
portHandlers[2].state |= 0x08;
portHandlers[2].state &= ~0x01;
}
}
}
Cette approche utilise un mécanisme d'état pour assurer le fonctionnement correct en mode semi-duplex, où la réception et l'émission sont mutuellement exclusives. Cependant, un risque de blocage survient si les données reçues ne sont pas traitées rapidement, ce qui pourrait être amélioré en utilisant une pile tampon pour stocker temporairement les données entrantes.