¡Descarga UART con el stm32 con el HAL y más Apuntes en PDF de Microcontroladores solo en Docsity!
UART Communication in Polling Mode
Los microcontroladores STM32, y por lo tanto el CubeHAL, ofrecen tres maneras de intercambiar
datos entre pares a través de una comunicación UART: sondeo, interrupción y modo DMA. Es
importante destacar desde ahora que estos modos no son sólo tres sabores diferentes para
manejar las comunicaciones UART. Ellos
son tres enfoques de programación diferentes para la misma tarea, que introducen varios
beneficios tanto desde el punto de vista del diseño y la actuación. Presentémoslos brevemente.
- En el modo polling, también llamado modo de bloqueo, la aplicación principal, o uno de sus hilos,
espera sincrónicamente la transmisión y recepción de datos. Esta es la forma más simple de la
comunicación de datos utilizando este periférico, y puede utilizarse cuando la tasa de transmisión
no es demasiado bajo y cuando la UART no se utiliza como periférico crítico en nuestra aplicación
(el ejemplo clásico es el uso de la UART como consola de salida para actividades de depuración).
En el modo de interrupción, también llamado modo no bloqueante, la aplicación principal se libera
de la espera para completar la transmisión y recepción de datos. Las rutinas de transferencia de
datos terminan tan pronto como completen la configuración del periférico. Cuando la transmisión
de datos termina. La subsiguiente interrupción señalará el código principal sobre esto. Este modo
es más adecuado cuando la velocidad de comunicación es baja (por debajo de 38400 Bps) o
cuando ocurre "raramente", en comparación con otros actividades realizadas por la MCU, y no
queremos atascarla esperando la transmisión de datos.
- El modo DMA ofrece el mejor rendimiento en la transmisión de datos, gracias al acceso directo
del UART periférica a la memoria RAM interna de la MCU. Este modo es el mejor para las
comunicaciones de alta velocidad y cuando queremos liberar totalmente a la MCU de la
sobrecarga de la transmisión de datos. Sin el modo DMA, es casi imposible alcanzar las tasas de
transferencia más rápidas que el USART periférico es capaz de manejar. En este capítulo no
veremos esta comunicación USART
dejando para el siguiente capítulo dedicado a la gestión de la DMA.
Para transmitir una secuencia de bytes a través de la USART en modo de sondeo, la HAL
proporciona la función
**HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef huart, uint8_t pData, uint16_t Size, uint32_t Timeout);
donde:
- huart: es el puntero a una instancia de la estructura UART_HandleTypeDef vista anteriormente,
que identifica y configura el periférico de la UART;
- pData: es el puntero de un array, con una longitud igual al parámetro Size, que contiene la
secuencia de bytes que vamos a transmitir;
- Tiempo de espera: es el tiempo máximo, expresado en milisegundos, que vamos a esperar para
la transmisión ... a la finalización. Si la transmisión no se completa en el tiempo de espera
especificado, la función aborta y devuelve el valor HAL_TIMEOUT; de lo contrario devuelve el valor
HAL_OK si no hay otro se producen errores. Además, podemos pasar un tiempo de espera igual a
HAL_MAX_DELAY (0xFFFF FFFF) para esperar indefinidamente para la finalización de la
transmisión.
Por el contrario, para recibir una secuencia de bytes a través de la USART en modo de sondeo, el
HAL proporciona la función
**HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef huart, uint8_t pData, uint16_t Size, uint32_t Timeout);
donde:
- huart: es el puntero a una instancia de la estructura UART_HandleTypeDef vista anteriormente,
que identifica y configura el periférico de la UART;
pData: es el puntero de un array, con una longitud como máximo igual al parámetro Size, que
contiene la secuencia de bytes que vamos a recibir. La función se bloqueará hasta que todos los
bytes especificados por el parámetro de tamaño se reciben.
- Tiempo de espera: es el tiempo máximo, expresado en milisegundos, que vamos a esperar para
recibir ...la finalización. Si la transmisión no se completa en el tiempo de espera especificado, la
función aborta y devuelve el valor HAL_TIMEOUT; de lo contrario devuelve el valor HAL_OK si no
hay otro se producen errores. Además, podemos pasar un tiempo de espera igual a
HAL_MAX_DELAY (0xFFFF FFFF) para esperar indefinidamente para la finalización de la recepción.
LEA CUIDADOSAMENTE
Es importante señalar que el mecanismo de tiempo de espera que ofrecen las dos funciones
funciona sólo si la rutina HAL_IncTick() es llamada cada 1ms, como lo hace el código generado por
CubeMX (la función que incrementa el contador de garrapatas HAL se llama dentro del SysTick
temporizador ISR).
int main( void ) { 22 uint8_t opt = 0; 23 24 /* Reset of all peripherals, Initializes the Flash interface and the SysTick. */
79 break ; 80 case 3: 81 return 2 ; 82 }; 83 84 return 1 ; 85 }
El ejemplo es una especie de consola de gestión de huesos desnudos. La aplicación comienza a
imprimir una bienvenida (líneas 36) y luego entrar en un bucle esperando la elección del usuario.
La primera opción permite conmutar el LED LD2, mientras que la segunda permite leer el estado
del botón USER. Finalmente, la opción 3 hace que la pantalla de bienvenida se imprima de nuevo.
Las dos cadenas "\033[0;0H" y "\033[2J" son secuencias de escape. Son estándar secuencias de
caracteres utilizadas para manipular la consola terminal. El primero coloca el cursor en la parte
superior izquierda de la pantalla de la consola disponible, y el segundo borra la pantalla
8.4 UART Communication in Interrupt Mode
Consideremos de nuevo el primer ejemplo de este capítulo. ¿Qué tiene de malo? Ya que nuestro
firmware está comprometido con esta simple tarea, no hay nada malo en usar la UART en modo
pulling. El MCU está esencialmente bloqueado esperando la entrada del usuario (el valor de
timeout HAL_MAX_DELAY bloquea el HAL_UART_Receive() hasta que se envíe un char sobre la
UART). ¿Pero qué pasa si nuestro firmware tiene que realizar otras actividades intensivas en cpu
en tiempo real?
Supongamos que reordenamos el main() del primer ejemplo de la siguiente manera:
38 while ( 1 ) { 39 opt = readUserInput(); 40 processUserInput(opt); 41 if (opt == 3) 42 goto printMessage; 43 44 performCriticalTasks(); 45 }
En este caso no podemos bloquear la ejecución de la función processUserInput() que espera al
usuario pero tenemos que especificar un valor de tiempo de espera mucho más corto para el
HAL_UART_Receive() de lo contrario, performCriticalTasks() nunca se ejecuta. Sin embargo, esto
podría causar la pérdida de datos importantes que vienen del periférico de la UART (recuerde que
la interfaz de la UART tiene un byte de ancho de buffer).
Para abordar esta cuestión el HAL ofrece otra forma de intercambiar datos a través de un
periférico UART: el modo de interrupción. Para usar este modo, tenemos que realizar las
siguientes tareas:
Para habilitar la interrupción USARTx_IRQn y para implementar el correspondiente
USARTx_IRQHandler() ISR.
- Para llamar a HAL_UART_IRQHandler() dentro del USARTx_IRQHandler(): esto realizará todas las
actividades relacionadas con la gestión de las interrupciones generadas por el periférico de la
UART¹².
- Para utilizar las funciones HAL_UART_Transmit_IT() y HAL_UART_Receive_IT() para intercambiar
datos sobre la UART. Estas funciones también permiten el modo de interrupción del periférico de
la UART: de esta manera el periférico afirmará la línea correspondiente en el controlador NVIC
para que el ISR se eleva cuando ocurre un evento.
- Para reorganizar nuestro código de aplicación para tratar los eventos asíncronos.
Antes de reordenar el código del primer ejemplo, es mejor echar un vistazo a la UART disponible
y a la forma en que se diseñan las rutinas de HAL.
8.4.1 UART Related Interrupts
- Las IRQs generadas durante la transmisión: Transmisión completa, despejada para enviar o
transmitir datos Registro de interrupción vacía.
- IRQs generadas mientras se reciben: Detección de línea ociosa, error de desbordamiento,
registro de datos de recepción no
vacío, error de paridad, detección de rotura de LIN, Bandera de Ruido (sólo en comunicación
multi-búfer) y error de encuadre (sólo en la comunicación multibúfer).
Tabla 6: La lista de interrupciones relacionadas con la USART
Interrupt Event Event Flag Enable Control
Bit
Registro de datos de transmisión TXE TXEIE
Clear To Send (CTS) CTS CTSIE
Transmisión completa TC TCIE
Datos recibidos listos para ser leídos RXNE RXNEIE
Se ha detectado un error de desbordamiento ORE RXNEIE
Se detecta la línea de inactividad IDLE IDLEIE
Error de paridad PE PEIE
Bandera de ruptura LBD LBDIE
Bandera de ruido, error de desbordamiento y enmarcado NF or ORE or FE EIE
Error en la comunicación multi-buffer
Estos eventos generan una interrupción si se activa el correspondiente bit de control de activación
(tercera columna de Tabla 6). Sin embargo, las MCU de STM32 están diseñadas para que todas
estas IRQs estén unidas a una sola ISR para cada periférico USART (ver Figura 11¹³). Por ejemplo, la
USART2 define sólo la USART2_-IRQn como IRQ para todas las interrupciones generadas por este
periférico. Depende del código de usuario analizar la correspondiente indicador de eventos para
inferir qué interrupción ha generado la solicitud.
Por el contrario, para recibir una secuencia de bytes sobre la USART en modo de interrupción, el HAL proporciona la función: **HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef huart, uint8_t pData, uint16_t Size);
donde:
- huart: es el puntero a una instancia de la estructura UART_HandleTypeDef vista anteriormente,
que identifica y configura el periférico de la UART;
- pData: es el puntero de un array, con una longitud como máximo igual al parámetro Size, que
contiene la secuencia de bytes que vamos a recibir. La función no bloqueará la espera de los datos
recepción, y pasará el control al flujo principal tan pronto como termine de configurar la UART.
Ahora podemos proceder a reordenar el primer ejemplo.
/* Enable USART2 interrupt */ 38 HAL_NVIC_SetPriority(USART2_IRQn, 0 , 0 ); 39 HAL_NVIC_EnableIRQ(USART2_IRQn); 40 41 printMessage: 42 printWelcomeMessage(); 43 44 while ( 1 ) { 45 opt = readUserInput(); 46 if (opt > 0) { 47 processUserInput(opt); 48 if (opt == 3) 49 goto printMessage; 50 } 51 performCriticalTasks(); 52 } 53 } 54 55 int8_t readUserInput( void ) { 56 int8_t retVal = -1; 57 58 if (UartReady == SET) { 59 UartReady = RESET; 60 HAL_UART_Receive_IT(&huart2, ( uint8_t *)readBuf, 1 ); 61 retVal = atoi(readBuf); 62 } 63 return retVal; 64 } 65 66 67 uint8_t processUserInput( int8_t opt) { 68 char msg[ 30 ]; 69 70 if (!(opt >=1 && opt <= 3)) 71 return 0 ; 72 73 sprintf(msg, "%d", opt); 74 HAL_UART_Transmit(&huart2, ( uint8_t *)msg, strlen(msg), HAL_MAX_DELAY); 75 HAL_UART_Transmit(&huart2, ( uint8_t *)PROMPT, strlen(PROMPT), HAL_MAX_DELAY); 76 77 switch (opt) { 78 case 1:
79 HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); 80 break ; 81 case 2: 82 sprintf(msg, " \r\n USER BUTTON status: %s", 83 HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET? "PRESSED" : "RELEASED"); 84 HAL_UART_Transmit(&huart2, ( uint8_t )msg, strlen(msg), HAL_MAX_DELAY); 85 break ; 86 case 3: 87 return 2 ; 88 }; 89 90 return 1 ; 91 } 92 93 void HAL_UART_RxCpltCallback(UART_HandleTypeDef UartHandle) { 94 _/ Set transmission flag: transfer complete/_ 95 UartReady = SET; 96 }
Como pueden ver en el código anterior, el primer paso es habilitar el USART2_IRQn y asignarle un
priority¹⁵. A continuación, definimos la correspondiente ISR dentro del archivo stm32xxxx_it.c (no
se muestra aquí) y añadimos la llamada a la función HAL_UART_IRQHandler() dentro de ella. La
parte restante de la El archivo de ejemplo se trata de reestructurar las funciones readUserInput() y
processUserInput() para se ocupan de los eventos asincrónicos.
La función readUserInput() ahora comprueba el valor de la variable global UartReady. Si es igual a
SET, significa que el usuario ha enviado un char a la consola de gestión. Este carácter es contenida
dentro de la matriz global readBuf. La función entonces llama al HAL_UART_Receive_IT() a recibir
otro personaje en modo de interrupción. Cuando readUserInput() devuelve un valor mayor que 0,
se llama la función processUserInput(). Finalmente, la función HAL_UART_RxCpltCallback(), que es
llamado automáticamente por el HAL cuando se recibe un byte, se define: simplemente establece
la variable global UartReady, que a su vez es utilizada por el readUserInput() como se ha visto
antes.
Es importante aclarar que la función HAL_UART_RxCpltCallback() es llamada sólo cuando se
reciben todos los bytes especificados con el parámetro Size, pasados a la función
HAL_UART_Receive_IT().
¿Qué hay de la función HAL_UART_Transmit_IT()? Funciona de forma similar a la HAL_UART_-
Receive_IT(): transfiere el siguiente byte de la matriz cada vez que la interrupción del Registro de
Datos de Transmisión Vacío (TXE) es
generada. Sin embargo, se debe tener especial cuidado al llamarla múltiple
veces. Como la función devuelve el control a la persona que llama tan pronto como termina de
configurar la UART, un La llamada posterior de la misma función fallará y devolverá el valor
HAL_BUSY.
Supongamos que reordenamos la función printWelcomeMessage() del ejemplo anterior en el ...de
la siguiente manera:
void printWelcomeMessage( void ) { HAL_UART_Transmit_IT(&huart2, ( uint8_t *)" \033 [0;0H", strlen(" \033 [0;0H"));
buffer circular, la primera y la última posición de la matriz se ven "contiguas" (véase la figura 12).
Esta es la razón por la que estos datos La estructura se llama circular. Los buffers circulares tienen
una característica importante también: a menos que nuestra aplicación tiene hasta dos flujos de
ejecución simultáneos (en nuestro caso, el flujo principal que coloca los caracteres dentro de la y
la rutina ISR que envía estos caracteres por la UART), son intrínsecamente seguros, ya que el hilo
"consumidor" (el ISR en nuestro caso) actualizará sólo el puntero de la cola y el productor (el flujo
principal) actualizará sólo el de la cabeza. Los buffers circulares pueden ser implementados de
varias maneras. Algunos de ellos son más rápidos, otros son más seguros (es decir, añaden una
sobrecarga adicional para asegurar que manejemos el contenido del buffer correctamente). Usted
encuentran una implementación simple y bastante rápida en los ejemplos del libro. Explicar cómo
está codificado esta fuera del alcance de este libro. Usando un buffer circular, podemos definir una
nueva función de transmisión UART de la siguiente manera:
uint8_t UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t len) { if (HAL_UART_Transmit_IT(huart, pData, len) != HAL_OK) { if (RingBuffer_Write(&txBuf, pData, len) != RING_BUFFER_OK) return 0 ; } return 1 ; } La función hace sólo dos cosas: intenta enviar el búfer sobre la UART en modo de interrupción; si la función HAL_UART_Transmit_IT() falla (lo que significa que la UART ya está transmitiendo otro mensaje), entonces la secuencia de bytes se coloca dentro de un búfer circular. Corresponde a la función HAL_UART_TxCpltCallback() comprobar si hay bytes pendientes dentro del búfer circular: void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (RingBuffer_GetDataLength(&txBuf) > 0) { RingBuffer_Read(&txBuf, &txData, 1 ); HAL_UART_Transmit_IT(huart, &txData, 1 ); } }
El RingBuffer_Read() no es tan rápido como podría ser con una implementación más eficiente.
Para algunas situaciones del mundo real, toda la sobrecarga de la rutina HAL_UART_-
TxCpltCallback() (que se llama desde la rutina ISR) podría ser demasiado alta. Si este es tu caso,
puedes considerar crear una función como la siguiente:
void processPendingTXTransfers(UART_HandleTypeDef *huart) { if (RingBuffer_GetDataLength(&txBuf) > 0) { RingBuffer_Read(&txBuf, &txData, 1 ); HAL_UART_Transmit_IT(huart, &txData, 1 ); } }
Entonces, podrías llamar a esta función desde el código principal de la aplicación o en una tarea de
menor privilegio si estás usando un RTOS.
8.5 Error Management
Cuando se trata de comunicaciones externas, el manejo de errores es un aspecto que debemos
tomar fuertemente en consideración. Un periférico UART STM32 ofrece algunos indicadores de
error relacionados con errores de comunicación. Además, es posible permitir que se note una
interrupción correspondiente cuando se produce el error.
El CubeHAL está diseñado para detectar automáticamente las condiciones de error y advertirnos
de ellas. Nosotros sólo necesitamos implementar la función HAL_UART_ErrorCallback() dentro de
nuestro código de aplicación. El HAL_UART_IRQHandler() lo invocará automáticamente en caso de
que ocurra un error. Para entender qué se ha producido un error, podemos comprobar el valor del
campo UART_HandleTypeDef->ErrorCode. El La lista de códigos de error se indica en el cuadro 7.
El HAL_UART_IRQHandler() está diseñado para que no nos preocupemos por los detalles de
implementación de la gestión de errores de la UART. El código HAL realizará automáticamente
todos los pasos necesarios para manejar el error (como borrar las banderas de eventos, los bits
pendientes y así sucesivamente), dejándonos la responsabilidad de manejar el error a nivel de la
aplicación (por ejemplo, podemos pedir al otro par que reenvíe un marco corrupto).
8.6 I/O Retargeting
En el capítulo 5 hemos aprendido a utilizar la función de semi alojamiento para enviar mensajes de
depuración a la Abrir la consola de OCD usando la función C printf(). Si ya has usado esta función,
sabes que
que hay dos fuertes limitaciones:
- el semiacoplamiento realmente ralentiza la ejecución del firmware;
- también impide que el firmware funcione si se ejecuta sin una sesión de depuración (debido a el
hecho de que el semianfitrión se implementa utilizando puntos de ruptura de software).
Ahora que estamos familiarizados con la gestión de la UART, podemos redefinir las llamadas de
sistema necesarias (_write(), _read() y así sucesivamente) para redireccionar los flujos estándar
STDIN, STDOUT y STDERR a la Núcleo USART2. Esto se puede hacer fácilmente de la siguiente
manera:
14 #if !defined(OS_USE_SEMIHOSTING) 15 16 #define STDIN_FILENO 0 17 #define STDOUT_FILENO 1 18 #define STDERR_FILENO 2
79 errno = EBADF; 80 return -1; 81 } 82 83 int _fstat( int fd, struct stat* st) { 84 if (fd >= STDIN_FILENO && fd <= STDERR_FILENO) { 85 st->st_mode = S_IFCHR; 86 return 0 ; 87 } 88 89 errno = EBADF; 90 return 0 ; 91 } 92 93 #endif //#if !defined(OS_USE_SEMIHOSTING)
Para reiniciar las secuencias estándar en su firmware, tiene que eliminar la macro
OS_USE_SEMIHOSTING a nivel de proyecto, e inicializar la biblioteca que llama a RetargetInit()
pasando el puntero a la instancia UART_HandleTypeDef de la UART2. Por ejemplo, el siguiente
código muestra cómo usar las funciones printf()/scanf() en su firmware:
int main( void ) { char buf[ 20 ]; HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART2_UART_Init(); RetargetInit(&huart2); printf("Write your name: "); scanf("%s", buf); printf(" \r\n Hello %s! \r\n ", buf); while ( 1 ); }
Si va a utilizar las funciones printf()/scanf() para imprimir/leer tipos de datos de flotación en la
consola serial (pero también si va a utilizar sprintf() y rutinas similares), necesita habilitar
explícitamente el soporte de flotación en newlib-nano, que es la versión más compacta de la
biblioteca de tiempo de ejecución C para sistemas empotrados. Para ello, vaya al menú Proyecto-
>Propiedades..., luego vaya a C/C++ Construcción->Configuración->Abrir ARM C++ Enlazador-
>Miscelánea y marque Usar float con nano printf/scanf según la característica que necesite, como
se muestra en la figura 13. Esto aumentará el tamaño del binario del firmware.