Archivo del sitio

Funciones útiles para la UART en un AVR ATmega8 y/o ATmega328P

Nivel: Intermiedio

¿Qué tengo que saber para este post?

  • Hexadecimal (no excluyente).
  • Conocimientos sólidos de lenguaje C de programación: punteros y operadores de bit, colas y pilas circulares.
  • Conocimientos básicos de electrónica digital: qué es un 1 y un 0, compuertas lógicas.
  • Conocimientos intermedios de microcontroladores: qué son los SFR’s, qué es el Transmisor Receptor Asincrónico Universal (UART). Qué es una máscara y operadores de bit. Mínimos conocimientos de la familia AVR de ATMEL, en especial la linea ATmega.

NOTA: Si te interesa saber como fueron pensadas las funciones y la explicación de como es su funcionamiento básico, seguí leyendo. Sino, anda directo al ejemplo, donde dice “a los bifes” al final del post.

NOTA2: Este post fue editado el 16/01/13, fue agregada la posibilidad de compilarlo con el ATmega328P. Los archivos main m8.c tienen el archivo original que se muestra en este post y el main m328p.c el mismo ejemplo pero inicializando los registros de la USART del ATmega328P, que son muy similares al del ATmega8. Saludos.

————————————–

Introducción

Buenas y santas, estimados lectores y curiosos. Este es el primer post técnico oficia en el que les voy a transmitir mis pobres conocimientos a Uds. en la espera de que les sea de utilidad y lo puedan utilizar en algún proyecto personal que tengan. Antes que nada, pido encarecidamente que si alguien nota algo mal en el post, que me avise, ya que empecé a experimentar con los AVR hace un par de semanas nada más y capaz que le pifio en algo. Mi mayor experiencia con MCUs la hice con micros basados en la familia Intel 8051, y tengo mucho código basado en ellos, pero siempre en vista de que sean lo más portable dentro de las posibilidades. Además tengo apuntes hechos que subiré en algún momento, porque es la materia que dicto en el colegio donde doy clases. Por ello, me vi interesado en usar algunas funciones que ya tenía implementadas para MCS51 en los ATmega de AVR. Busqué en las hojas de datos y los ejemplos que encontré allí no me parecieron muy útiles, en Internet tampoco encontré lo que buscaba. Claro, Uds. deben estar preguntándose ¿qué estaba buscando este tipo? Muy sencillo, buscaba un código que utilizando las interrupciones asociadas a la transmisión y recepción de la USART, enviara y recibiera información sin desperdiciar tiempo en ese proceso y leerlo solamente cuando me sea necesario de un buffer de entrada y otro de salida. Es decir, que a mí me pueden llegar muchos datos, almacenarlo en un buffer de recepción y luego en algún punto x del programa, sacar los datos. En todos los ejemplos que encontré en Internet la comunicación serie estaba asociada a un while en el que se esperaba a que el registro de datos del micro se liberara para enviar el próximo dato. Si quisiera enviar un string, cuando hay otras cosas que atender dentro de nuestro programa, esta demora involuntaria nos estaría molestando (al menos desde mi punto de visa).

¿Cómo funciona?

La idea es la siguiente, tener dos buffer circulares del tipo FIFO (First Input First Output – el primero en entrar es el primero en salir) en el que nosotros almacenemos los datos que queremos que salgan y otro para los datos que entran de la UART. En un buffer circuilar tenemos 2 punteros, uno es el puntero de extracción y el otro de inserción de datos. Cuando entra un dato por la UART este entra a la dirección de memoria a la que apunta el puntero de inserción (push) y este se aumenta en 1, y así sucesivamente con todos los datos que vayan entrando. Luego, lo único que tenemos que hacer es ir recuperando los datos del buffer (pop), cuando nosotros lo necesitemos, empezando por la dirección de memoria del puntero de extracción.

Buffer Circular

Ocurre exactamente lo contrario (y de cierta manera, lo mismo), cuando nosotros queremos transmitir por la UART. Simplemente debemos insertar (push) datos en el buffer de transmisión y luego, por medio de la interrupción se irán sacando los datos del buffer (pop) a medida que se van transmitiendo. Esto nos da la facilidad de escribir todos los datos que queremos enviar sin tener que esperar a que el registro de transmisión se vacíe ganando tiempo para ejecutar otras cosas.

En cualquiera de los dos casos, cuando el puntero de extracción alcanza al de inserción, esto indica que ya no hay datos que sacar del buffer y retorna un (-1). Pero CUIDADO, también podría pasar que estemos escribiendo datos más rápido de los que podemos transmitir y eso nos llevaría a dar toda la vuelta al buffer y a perder información. En el caso de la recepción es igual, si nosotros no leemos el buffer y la información supera la longitud del mismo, perderemos información, por lo cual deberemos aumentar el tamaño de los buffers. De todas formas, siempre podemos crear algún mecanismo o algoritmo para pedir que nos envíen la información nuevamente, como con un checksum. Pero bueno, tampoco les voy a solucionar la vida xD algo tienen que laburar Uds. también.

Implementación

  • Preparativos:

Antes que nada, debemos configurar la USART del micro, para ello, debemos atenernos a la hoja de datos del ATmega8. Claro que no les voy a hacer leerse las 400 hojas que tiene, vayan al indice del pdf y a “USART Register description”. Allí procederemos a los 3 pasos básicos para usar una UART cualquiera.

  1. Establecer un Baudrate para la transmisión y la recepción.
  2. Configurar el modo de la USART (sincrónica/asincrónica, 5/6/7/8/9 bits de datos, paridad, etc.)
  3. Y no menos importante, habilitar la transmisión y/o recepción + las interrupciones pertinentes  (si se desean utilizar).

Lo primero lo hacemos con los registros UBRRH y UBRRL y el bit-1  del registro UCSRA (U2X) que nos permite multiplicar x2 el baudrate. Gracias a él, podemos (y lo haremos) generar 9600 baudios (dentro de un error más que tolerable) con el oscilador RC interno de 1 Mhz =) Ahora les copipasteo copio y pego una tabla que está en la hoja de datos, para mostrarles la configuración que utilicé para el ejemplo que les voy a pasar:

Baudrates posibles a 1Mhz de Fosc

Lo segundo se hace con el bit-2 de UCSRB (UCSZ2) y el registro UCSRC, el cual los muy putos de los fabricantes pusieron en el mismo espacio de memoria que el registro UBRRH. Por lo tanto, para acceder a él debemos poner su bit más significativo (el bit-7), a.k.a. URSEL, en 1. y allí hay varias tablas para configurar la cantidad de datos, la paridad, y demás cosas (miren la hoja de datos).

Por último, para habilitar la recepción, transmisión e interrupciones anexadas a la USART, debemos dirigirnos al registro UCSRB, salvo los bit-0, bit-1 y bit-2. El bit-2 lo vimos en el párrafo anterior, y los otros 2 se utilizan como el bit que falta en las transmisiones de 9 bits de datos. Así que, sabiendo todas estas cosas, les paso a comentar qué necesitamos para poder utilizar las funciones que les voy a pasar:

  • Baudrate: cualquiera.
  • Cantidad de bits de datos: 8 o menos.
  • Paridad: Cualquiera.
  • Control de flujo: si quieren…
  • Habilitar recepción y/o transmisión de datos bit-4 y bit-3 de UCSRB.
  • Habilitar interrupciones de recepción y transmisión completadas bit-7 y bit-6 del registro UCSRB.
  • Las funciones que disponemos:

Las funciones son las siguientes:

int USART_PopRx(void);

void USART_PushTx(unsigned char nDato);

void USART_SendStr(const char* pszStr);

  • PopRx(): devuelve el valor leído del buffer de recepción, si no hay datos, entonces devuelve -1.
  • PushTx(): entra nDato al buffer de transmisión, si no hay datos que se estén enviando, entonces arranca la transmisión.
  • SendStr(): pone un string en el buffer de transmisión, son llamadas múltiples a la función PushTx().
  • A los bifes:

El ejemplo que les presento utiliza las 3 funciones para que quede claro como trabajar con ellas:

#include <avr/io.h>
#include <avr/interrupt.h>
#include "Serial.h"

#define ULDIFF      ('A'-'a')
#define TOUPPER(X)  (X+ULDIFF)
#define TOLOWER(X)  (X-ULDIFF)

void USART_Init(void)
{
    /* Configuración de 9600 con error del 0.2% a 1 Mhz de F_CPU */
    UBRRL=12;   // Dato sacado de la hoja de datos
    UCSRA=0x02; // Doblar el Baudrate - U2X = 1

    /* UART con 8-bits de datos y 1 de STOP sin paridad */
    UCSRC=0x86; // URSEL = 1 : acceder al registro UCSRC
                // UCSZ1 = UCSZ0 = 1 : 8-bits de dato

    /* Habilitación de la Recepcion y la Transmisión e interrupciones
        de recepción y transmisión terminada */
    UCSRB=0xD8; // RXCIE = TXCIE = RXEN = TXEN = 1

    /* Habilitación de las interrupciones globales, sino no funcionan
        las funciones de la USART */
    sei();
}

int main(void)
{
    int data;

    // Inicializo el puerto serie
    USART_Init();

    // Envio un dato para verificar que funcione la comunicación
    USART_SendStr("Hola Mundo!!!");

    while(1)
    {
        // Saco el dato del buffer y lo almaceno en data
        data=USART_PopRx();

        // Verifico si había datos en el buffer
        if(data!=-1)
        {
            if(data>='a'&&data<='z')                    // Si el dato es una minúscula
                USART_PushTx((uint8_t)TOUPPER(data));   // envio su mayúscula
            else if(data>='A'&&data<='Z')               // sino, si el dato es una mayúscula
                USART_PushTx((uint8_t)TOLOWER(data));   // envio su minúscula
            else                                        // sino,
                USART_PushTx((uint8_t)data);            // devuelvo el dato tal cual entró
        }

    }

    return 0;
}

Por último les dejo el link a una carpeta de GoogleDrive para que se bajen el header y la implementación de las funciones junto con el mismo ejemplo que ven acá, para que les echen un ojo y tal vez las mejoren o las adapten para que les sean de mayor utilidad. Ah, casi me olvido de mencionar que el código está bajo licencia GNU/GPLv3, así que estamos “tudo bem, tudo legal”.

Código fuente ACÁ

Es todo, espero que les sea útil y como dice un colega y amigo, sean felices.