Docsity
Docsity

Prepara tus exámenes
Prepara tus exámenes

Prepara tus exámenes y mejora tus resultados gracias a la gran cantidad de recursos disponibles en Docsity


Consigue puntos base para descargar
Consigue puntos base para descargar

Gana puntos ayudando a otros estudiantes o consíguelos activando un Plan Premium


Orientación Universidad
Orientación Universidad


UART con el stm32 con el HAL, Apuntes de Microcontroladores

Manejo del UART en modo polling con el stm32 y el HAL

Tipo: Apuntes

2019/2020

Subido el 03/08/2020

cervantes-mejia-gabriel
cervantes-mejia-gabriel 🇲🇽

4.8

(11)

14 documentos

1 / 14

Toggle sidebar

Esta página no es visible en la vista previa

¡No te pierdas las partes importantes!

bg1
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:
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe

Vista previa parcial del texto

¡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.