Está en la página 1de 11

Lección 10: Almacenamiento en búfer de recepción

UART
En la última lección, creamos un controlador UART muy simple que sondea el periférico en busca de datos
recibidos. Como aprendimos con el botón en la lección 6, esta no es la solución óptima para la mayoría de los
conductores. Una vez que comenzamos a agregar más funcionalidad al bucle principal, es posible que los caracteres
se pierdan porque la CPU está ocupada haciendo otras cosas. Podemos simular fácilmente este escenario agregando
un gran retraso en el bucle principal, digamos un segundo, utilizando la función __delay_cyles.

1 watchdog_pet();
2 menu_run();
3 __delay_cycles(1000000);

La función menu_run lee la entrada UART y luego se retrasa un segundo antes de buscar el siguiente carácter. Este
retraso es exagerado, pero demuestra un punto importante. Compile el código con este retraso y, a continuación,
ejecútelo. Intente escribir '1234' rápidamente en el indicador del menú. Notará que los caracteres se eliminan, solo
uno o dos de los caracteres se repiten. Lo que sucede aquí es que cada carácter recibido por el periférico se coloca
en el registro UCA0RXBUF. Si el software no lee los datos del registro antes de recibir el siguiente carácter, se
sobrescribirá el valor del registro.

La solución es doble: detectar los datos entrantes mediante interrupciones en lugar de sondeo y, a continuación,
almacenar cada carácter recibido en un búfer FIFO (primero en entrar, primero en salir). Un FIFO es un tipo de
búfer (o cola) en el que los datos entran y salen en el mismo orden. Si el FIFO está lleno y hay otro dato para
ingresar, se elimina (se pierden los datos más recientes) o los datos más antiguos en el FIFO se envían y descartan.
Hay diferentes tipos de FIFO, así que no cubriré todos los diseños posibles, pero veremos uno en detalle en breve.
El uso de un FIFO para poner en cola los datos recibidos es muy común. De hecho, el registro UCA0RXBUF puede
considerarse un FIFO de profundidad 1 (profundidad de 'n' significa 'n' elementos encaja en el FIFO) que elimina
los datos más antiguos una vez llenos. El campo UCA0STAT[UCOE] se establecerá si se produce esta condición,
denominada error de desbordamiento.

Algunos MCU de gama alta proporcionan un FIFO UART en hardware. Sin embargo, incluso con colas de
hardware, puede ser óptimo implementar una cola de software junto con para proporcionar más flexibilidad. En este
tutorial implementaremos en el tipo FIFO que se puede utilizar para poner en cola todo tipo de datos.

Conceptos básicos del búfer de anillo

El tipo de FIFO que implementaremos se denomina búfer de anillo, también conocido como búfer circular. Se llama
búfer de anillo porque los datos pueden volver al principio, siempre que haya espacio. Realmente solo se
implementa como una matriz, pero el comienzo de la cola no tiene que comenzar en el primer elemento de la
matriz, y el final no necesariamente termina en el último elemento de la matriz. El inicio de la cola podría comenzar
en algún lugar en el medio de la matriz, envolver el último elemento hasta el principio y terminar allí. El inicio de la
cola es donde se escribirán los nuevos datos. El final de la cola contiene los datos más antiguos y es donde leerá la
persona que llama. Estos se refieren comúnmente a la cabeza y la cola respectivamente. Tenga en cuenta que estas
son solo convenciones de nomenclatura por el bien de la teoría: su significado exacto es específico de la
implementación, como verá más adelante.

Para ayudar a aclarar cómo funciona el búfer de anillo, echemos un vistazo a algunos diagramas. Digamos que
nuestro búfer de anillo puede contener 4 elementos. Cuando se inicializa, la cabeza y la cola están en el primer
elemento.
No hay datos en el búfer de anillo. En la siguiente imagen, se agrega un elemento como lo indica el cuadro azul
claro.

Los datos se insertan en la cabeza actual y la cabeza se incrementa al siguiente elemento. Se presiona otra tecla y se
ingresa otro carácter.

Y otro...
Y otro...

Y otro... ¡Oh, espera, el búfer del anillo está lleno! La cabeza se ha envuelto alrededor de la posición de la cola. Si
se produce una escritura más, se perderían los datos más antiguos. Por lo tanto, la siguiente escritura fallaría.
Entonces, ¿qué pasa si la aplicación ahora lee un carácter del búfer de anillo?

La cola se incrementa y hay un elemento libre en el búfer del anillo. Ahora se agrega un carácter más y llena el
búfer nuevamente, pero ahora el anillo se envuelve alrededor de la matriz.
Y alrededor y alrededor de los datos va. Pero hay una trampa. ¿Ve un posible desafío de implementación con estos
diagramas? La cabeza y la cola están en el mismo elemento en dos instancias: cuando el búfer está vacío y cuando
el búfer está lleno. Entonces, ¿cómo puedes diferenciar entre los dos? Hay varias maneras de manejar este
problema. Una implementación común para determinar si el búfer de anillo está lleno es realizar un seguimiento del
recuento de datos. Esto significa que para cada escritura el contador se incrementa y para cada lectura se disminuye
el contador. Es muy fácil de implementar, sin embargo, este enfoque tiene un defecto importante. La escritura se
invocará desde una interrupción y la lectura se invocará desde la aplicación. Tener una sola variable para rastrear el
conteo significaría que DEBEMOS tener una sección crítica en ambas funciones. Volviendo a la lección sobre
temporizadores, aprendimos que una sección crítica es necesaria cuando se accede a una variable por más de un
contexto. Esto significa que al leer datos fuera del búfer de anillo, las interrupciones tendrían que deshabilitarse
temporalmente. Aunque a veces es inevitable, es mejor intentar escribir código que no requiera el uso de secciones
críticas. En la siguiente sección implementaremos un búfer de anillo que aborda ambas preocupaciones.

Implementación de un búfer de anillo sin bloqueo

Nuestra implementación del búfer de anillo será lo suficientemente genérica como para que podamos usarlo para
cualquier tipo de datos, no solo caracteres. Esto significa que tenemos que saber no solo el número de elementos en
el búfer de anillo, sino también el tamaño de los elementos. Para empezar, echemos un vistazo a la estructura
rb_attr_t en el archivo de encabezado include/ring_buffer.h.

1 typedef struct {
2     size_t s_elem;
3     size_t n_elem;
4     void *buffer;
5 } rb_attr_t;

Esta estructura contiene los atributos definidos por el usuario del búfer de anillo que se pasarán a la rutina de
inicialización. La estructura contiene las variables miembro s_elem – el tamaño de cada elemento, n_elem – el
número de elementos y buffer – un puntero al buffer que contendrá los datos. El diseño de esta estructura significa
que el usuario debe proporcionar la memoria utilizada por el búfer de anillo para almacenar los datos. Esto es
necesario porque no tenemos funciones de asignación de memoria disponibles. Incluso si lo hiciéramos,
comúnmente se considera una mala práctica utilizar la asignación dinámica de memoria en sistemas embebidos (es
decir, malloc, realloc, calloc, etc.).

En el archivo de cabecera, hay typedef del descriptor de búfer de anillo rbd_t.

1 typedef unsigned int rbd_t;

Este descriptor será utilizado por la persona que llama para acceder al búfer de timbre que ha inicializado. Es un
tipo entero sin signo porque se utilizará como índice en una matriz de la estructura de búfer de anillo interna
ubicada en src/ring_buffer.c. Aparte de los atributos que discutimos en el párrafo anterior, la cabeza y la cola son
todo lo que se requiere para esta estructura. Observe cómo la cabeza y la cola se declaran como volátiles. Esto se
debe a que se accederá a ellos tanto desde el contexto de la aplicación como desde el contexto de interrupción.
1 struct ring_buffer
2 {
3     size_t s_elem;
4     size_t n_elem;
5     uint8_t *buf;
6     volatile size_t head;
7     volatile size_t tail;
8 };

Esta estructura se asigna como una matriz privada a este archivo. El número máximo de búferes de anillo
disponibles en el sistema se determina en tiempo de compilación mediante el hash definido RING_BUFFER MAX,
que por ahora tiene un valor de 1. La asignación de la estructura de búfer de anillo se ve así.

1 static struct ring_buffer _rb[RING_BUFFER_MAX];

La inicialización del búfer de anillo es sencilla.

1 int ring_buffer_init(rbd_t *rbd, rb_attr_t *attr)


2 {
3     static int idx = 0;
4     int err = -1;
5  
6     if ((idx < RING_BUFFER_MAX) && (rbd != NULL) && (attr != NULL)) {
7         if ((attr->buffer != NULL) && (attr->s_elem > 0)) {
8             /* Check that the size of the ring buffer is a power of 2 */
9             if (((attr->n_elem - 1) & attr->n_elem) == 0) {
10                 /* Initialize the ring buffer internal variables */
11
                _rb[idx].head = 0;
12
                _rb[idx].tail = 0;
13
                _rb[idx].buf = attr->buffer;
14
                _rb[idx].s_elem = attr->s_elem;
15
                _rb[idx].n_elem = attr->n_elem;
16
17  
                *rbd = idx++;
18
19                 err= 0;

20             }

21         }
22     }
23  
24     return err;
}

Primero comprobamos que hay un búfer de anillo libre y que los punteros rbd y attr no son NULL. La variable
estática 'idx' cuenta el número de búferes de anillo utilizados. La segunda instrucción condicional comprueba que el
tamaño del elemento y el puntero de búfer son válidos. La comprobación final se realiza para comprobar que el
número de elementos es una potencia par de dos. Hacer cumplir esto nos permitirá hacer optimizaciones en el
código que discutiremos en breve. Para verificar n_elem es una potencia de dos, hay un truco que aprovecha el
sistema numérico binario. Cualquier valor que sea una potencia de dos tendrá sólo un '1' en su representación
binaria. Por ejemplo:

(Utilizo el guión bajo solo para mayor claridad)

Tenga en cuenta que el 1 se desplaza a la izquierda por el número en el exponente. Si se resta uno de cualquier
potencia de dos, el resultado será una serie consecutiva de 1s desde el bit cero hasta el bit 'exponente – 1'.

Si el valor original es lógico AND'ed con esta cadena de unos, el resultado siempre será un cero para una potencia
de dos.

Si el valor inicial no era una potencia de dos, el resultado siempre será distinto de cero.

Se utilizará una técnica similar para envolver los índices de cabeza y cola que veremos en breve.

Ahora que se validan todos los argumentos, se copian en la estructura local y el índice se devuelve al autor de la
llamada como descriptor del búfer de timbre. La variable idx también se incrementa para indicar que se utiliza el
búfer de anillo. El valor ahora será RING_BUFFER_MAX por lo que si se vuelve a llamar a la función de
inicialización, se producirá un error.

Antes de pasar al resto de las API públicas, echemos un vistazo a las dos funciones auxiliares estáticas:
_ring_buffer_full y _ring_buffer_empty.

1 static int _ring_buffer_full(struct ring_buffer *rb)


2 {
3     return ((rb->head - rb->tail) == rb->n_elem) ? 1 : 0;
4 }
5  
6 static int _ring_buffer_empty(struct ring_buffer *rb)
7 {
8     return ((rb->head - rb->tail) == 0U) ? 1 : 0;
9 }

Ambos calculan la diferencia entre la cabeza y la cola y luego comparan el resultado con el número de elementos o
cero respectivamente. Notará que en las funciones posteriores, la cabeza y la cola no están envueltas dentro de los
límites del búfer de anillo como cabría esperar de los diagramas anteriores. En su lugar, se incrementan y se
envuelven automáticamente cuando se desbordan. Esta es una 'característica' de C (tenga en cuenta que esto solo se
aplica a enteros sin signo) y nos ahorra realizar un cálculo adicional cada vez que se llama a la función. También
nos permite calcular el número de elementos actualmente en el búfer de anillo sin ninguna variable adicional (leer
sin contador = sin sección crítica). Cuando la diferencia entre los dos es cero, el búfer de anillo está vacío. Sin
embargo, dado que la cabeza y la cola no están envueltas alrededor de n_elem, siempre que haya datos en el búfer
del anillo, la cabeza y la cola nunca tendrán el mismo valor. El búfer de anillo solo está lleno cuando la diferencia
entre los dos es igual a n_elem.

Cuando el puntero de cabeza y cola alcanzan sus límites (para un entero de 16 bits esto estará en 65535) y
desbordan entran en juego algunos trucos binarios. La cabeza se desborda primero, pero la cola sigue siendo un
valor grande, por lo que la diferencia entre los dos será negativa. Sin embargo, esto funciona a nuestro favor porque
estamos usando enteros sin signo. La resta da como resultado un valor positivo muy grande que se puede utilizar
para obtener la diferencia real entre los dos valores sin costo adicional. Para demostrar cómo funciona esto,
digamos por ejemplo que tenemos dos valores de 8 bits sin signo: 5 y 250, la cabeza y la cola respectivamente. Para
determinar si el amortiguador del anillo está lleno o vacío, debemos restar la cola de la cabeza:

Bueno, ese resultado es definitivamente más de 8 bits, entonces, ¿qué sucede con el byte más significativo?
Siempre que el resultado también se almacene como un valor de 8 bits sin signo, el byte superior (MSB) se
descartará o se truncará. Por lo tanto, al resultado solo se le asigna el byte inferior

¡Esta es la diferencia absoluta entre la cabeza y la cola! En el caso de nuestro software, estamos usando size_t, que
es de 16 bits, pero el principio es el mismo.

La siguiente función es ring_buffer_put que agrega un elemento al búfer de anillo.

1 int ring_buffer_put(rbd_t rbd, const void *data)


2 {
3     int err = 0;
4  
5     if ((rbd < RING_BUFFER_MAX) && (_ring_buffer_full(&_rb[rbd]) == 0)) {
6         const size_t offset = (_rb[rbd].head & (_rb[rbd].n_elem - 1)) * _rb[rbd].s_elem;
7         memcpy(&(_rb[rbd].buf[offset]), data, _rb[rbd].s_elem);
8         _rb[rbd].head++;
9     } else {
10         err = -1;
11
    }
12
 
13
    return err;
14
}

Dado que el tamaño de cada elemento ya se conoce, no es necesario pasar el tamaño de los datos. Después de
validar el argumento y comprobar que el búfer de anillo no está lleno, los datos deben copiarse en el búfer de anillo.
El desplazamiento en el búfer está determinado por algunas matemáticas más complicadas. El búfer es solo una
matriz de bytes, por lo que necesitamos saber dónde comienza cada elemento para copiar los datos en la ubicación
correcta. El índice de encabezado debe ajustarse alrededor del número de elementos en el búfer de anillo para
obtener en qué elemento queremos escribir. Normalmente, una operación de ajuste se realiza utilizando la operación
de módulo. Por ejemplo, el desplazamiento podría calcularse así:

1 const size_t offset = (_rb[rbd].head % _rb[rbd].n_elem) * _rb[rbd].s_elem;

Si modificamos cualquier valor con el número de elementos, el resultado será un elemento válido dentro del rango
del número de elementos. Por ejemplo, si la cabeza es 100, y el número de elementos es 4, el módulo es 0, por lo
tanto, estamos insertando en el elemento cero. Si el número de elementos fuera 8, entonces el resultado sería 4 y,
por lo tanto, estamos copiando los datos al elemento 4.

head % n_elem = elemento en el búfer de anillo

El problema con el módulo es que la división es costosa. Requiere muchas operaciones y en realidad se implementa
en software. Por lo tanto, es ideal encontrar una manera de reducir esta sobrecarga innecesaria. Es por esta razón
que el número de elementos está restringido a una potencia de dos. Esto nos permite aprovechar esas reglas que
aprendimos anteriormente para realizar una operación de módulo usando solo el operador lógico AND una resta
simple. Restar uno de cualquier potencia de dos da como resultado una cadena binaria de unos. Lógico ANDing el
resultado con cualquier valor obtendrá el módulo. Tomando el último ejemplo de nuevo, con un búfer de anillo que
tiene ocho elementos y la cabeza es 100:

head & (n_elem -1) = elemento en el búfer de anillo

El resultado es el mismo que el anterior. La resta y la operación lógica AND se implementan en una sola
instrucción cada una en casi todas las CPU, mientras que el módulo requiere muchas instrucciones para hacer lo
mismo. Por lo tanto, el uso de este truco optimiza el rendimiento del búfer de anillo.

Volviendo al cálculo del desplazamiento, solo hemos encontrado el elemento en el que queremos insertar datos. Sin
embargo, dado que el tamaño de los datos es definido por el autor de la llamada, el desplazamiento real de bytes en
la matriz de memoria se puede calcular tomando el elemento y multiplicándolo por el tamaño de cada elemento en
bytes. Una vez que los datos se copian en la memoria del búfer de anillo, el cabezal se incrementa.

La última función de este módulo es ring_buffer_get.

1 int ring_buffer_get(rbd_t rbd, void *data)


2 {
3     int err = 0;
4  
5     if ((rbd < RING_BUFFER_MAX) && (_ring_buffer_empty(&_rb[rbd]) == 0)) {
6         const size_t offset = (_rb[rbd].tail & (_rb[rbd].n_elem - 1)) * _rb[rbd].s_elem;
7         memcpy(data, &(_rb[rbd].buf[offset]), _rb[rbd].s_elem);
8         _rb[rbd].tail++;
9     } else {
10         err = -1;
11
    }
12
13  
14     return err;
}

Es esencialmente lo mismo que ring_buffer_put, pero en lugar de copiar los datos, se copian del búfer de timbre a la
persona que llama. Aquí, sin embargo, el punto en el que se incrementa la cola es clave. En cada una de las dos
funciones anteriores, solo se modifica la cabeza o la cola, nunca ambas. Sin embargo, ambos valores se leen para
determinar el número de elementos en el búfer de anillo. Para evitar tener que usar una sección crítica, la
modificación de la cabeza debe ocurrir después de leer la cola, y viceversa. Es posible que una interrupción se
dispare justo antes o durante el memcpy. Si la cola se incrementa antes de que los datos se copien fuera del búfer y
el búfer esté lleno, ring_buffer_put vería que hay espacio en el búfer de anillo y escribiría los nuevos datos. Cuando
la interrupción regresa y la aplicación recupera el contexto, los datos sobrescritos se perderían y, en su lugar, la
persona que llama obtendría los datos más recientes o los datos dañados. Al incrementar el índice solo al final,
incluso si se dispara una interrupción en el medio del memcpy, ring_buffer_put llamado desde el ISR vería la cola
actual como todavía se usa y no escribiría en ella.

Uso del búfer de anillo en el controlador UART

Ahora que entendemos cómo funciona el búfer de anillo, debe integrarse en el controlador UART. Primero como
global en el archivo, se debe declarar el descriptor_rbd de búfer de anillo y el _rbmem de memoria del búfer de
anillo.

1 static rbd_t _rbd;


2 static char _rbmem[8];

Dado que se trata de un controlador UART donde se espera que cada carácter sea de 8 bits, la creación de una
matriz de caracteres es válida. Si se usaba el modo de 9 o 10 bits, entonces cada elemento debería ser un uint16_t.
El búfer de anillo debe tener el tamaño para evitar la pérdida de datos, por lo que, dadas las limitaciones de
memoria y el rendimiento del sistema, debería poder contener el número de elementos del peor de los casos.
Determinar el peor de los casos puede ser una combinación de conjeturas educadas y prueba y error. A menudo, los
módulos de cola contienen información estadística para que se pueda supervisar el uso máximo. Esto es algo que
podemos explorar en una lección posterior. Aquí la cola tiene un tamaño de 8 elementos. Creo que es un número
altamente improbable de caracteres que cualquiera podría escribir 8 caracteres coherentemente en un segundo.
También es un poder de dos. Cuatro caracteres probablemente serían suficientes, pero planeamos para el peor de los
casos y cuatro bytes adicionales no romperán el banco (por ahora).

En la función de inicialización uart_init, el búfer de anillo debe inicializarse llamando a ring_buffer_init y pasando
la estructura de atributos del búfer de anillo con cada miembro asignado los valores discutidos. Si el búfer de anillo
se inicializa correctamente, el módulo UART se puede sacar del restablecimiento y la interrupción de recepción se
habilita en IFG2.

1 ...
2 if (i < ARRAY_SIZE(_baud_tbl)) {
3     rb_attr_t attr = {sizeof(_rbmem[0]), ARRAY_SIZE(_rbmem), _rbmem};
4  
5     /* Set the baud rate */
6     UCA0BR0 = _baud_tbl[i].UCAxBR0;
7     UCA0BR1 = _baud_tbl[i].UCAxBR1;
8     UCA0MCTL = _baud_tbl[i].UCAxMCTL;
9  
10     /* Initialize the ring buffer */
11     if (ring_buffer_init(&_rbd, &attr) == 0) {
12
        /* Enable the USCI peripheral (take it out of reset) */
13
        UCA0CTL1 &= ~UCSWRST;
14  
15         /* Enable rx interrupts */
16         IE2 |= UCA0RXIE;
17  
18         status = 0;
19     }
20 }
21 ...

La segunda función que debe modificarse es uart_getchar. La lectura del carácter recibido fuera del periférico
UART se reemplaza por la lectura de la cola. Si la cola está vacía, la función debería devolver -1 como lo hacía
antes.

1 int uart_getchar(void)
2 {
3     char c = -1;
4  
5     ring_buffer_get(_rbd, &c);
6  
7     return c;
8 }

Finalmente, necesitamos implementar el ISR de recepción de UART. Abra el archivo de encabezado


msp430g2553.h y desplácese hacia abajo hasta la sección de vectores de interrupción donde encontrará el vector
llamado USCIAB0RX. La nomenclatura implica que esta interrupción es utilizada por los módulos USCI A0 y B0.
Esto solo significa que tenemos que ser muy cuidadosos en nuestro ISR para responder solo cuando se establece la
bandera de estado apropiada. El estado de interrupción de recepción USCI A0 se puede leer desde IFG2. Si se
establece, el indicador debe borrarse y los datos del búfer de recepción deben insertarse en el búfer de anillo
mediante ring_buffer_put.

1 __attribute__((interrupt(USCIAB0RX_VECTOR))) void rx_isr(void)


2 {
3     if (IFG2 & UCA0RXIFG) {
4         const char c = UCA0RXBUF;
5  
6         /* Clear the interrupt flag */
7         IFG2 &= ~UCA0RXIFG;
8  
9         ring_buffer_put(_rbd, &c);
10     }
11 }

Si la cola está llena, los datos se perderán ya que en la interrupción debe volver lo más rápido posible. Nunca debe
realizar una espera ocupada aquí, es decir, un bucle hasta que el envío de los datos a la cola finalmente tenga éxito.
Esto sólo sería aceptable en el contexto de la solicitud.

Una cosa más que tenemos que modificar es el makefile. Cuando comencé a ejecutar este código la primera vez que
no funcionaba. Por un tiempo estuve perplejo. Lo copié y compilé para mi PC y funcionó bien. Después de algunas
depuraciones, descubrí que todas las multiplicaciones devolvían un valor incorrecto. Investigaciones posteriores
mostraron que el compilador estaba, por alguna razón, tratando de usar el multiplicador de hardware que existe en
dispositivos MSP430 de gama alta, pero no en el MSP430G2553. Afortunadamente, hay un indicador de
compilador 'mhwmult' que se puede usar para decirle al compilador que no use el multiplicador de hardware
estableciendo el indicador en 'none'. Ahora la variable CFLAGS debe tener la siguiente definición:

1 CFLAGS:= -mmcu=msp430g2553 -mhwmult=none -c -Wall -Werror -Wextra -Wshadow -std=gnu90 -Wpedantic


-MMD -I$(INC_DIR)

Estamos ejecutando una versión bastante antigua del compilador (realmente tengo que hacer una actualización
sobre la construcción de la cadena de herramientas), así que tal vez lo hayan arreglado en una versión más nueva,
pero ese fue un error bastante desagradable para rastrear. Sin embargo, el uso de esta bandera es válido y explícito,
por lo que podemos dejarlo de cualquier manera.

Prueba del búfer de anillo

Ahora que hemos hecho todas las modificaciones necesarias, programamos la placa y ejecutamos el nuevo código
dejando en el retraso de un segundo. Intente escribir '1234' nuevamente como lo hicimos al principio del tutorial.
Debes ver que aunque los caracteres se retrasan, todos se reciben y en el orden correcto. Ahora nuestro controlador
UART tiene cierta protección contra la caída de caracteres una vez que la aplicación se vuelve más ocupada.

También podría gustarte