Este blog está dedicado a mis experiencias, proyectos, dificultades y demás en todo lo relacionado a la electrónica y la programación en general sobre GNU/Linux

AVR, Digital, Electrónica, Nivel: Intermedio

Algunos tips útiles para programar un micro AVR con GCC

Nivel: Intermedio

¿Qué tengo que saber para este post?

  • Estructuras de microprocesadores/microcontroladores.
  • Mapeo de memoria.
  • Programación en C/C++, programación orientada a objetos (POO), uso de punteros.

—————————————

Soy culpable, no he escrito en mucho tiempo en este pequeño espacio de la red, pero les traigo varios tips que creo podrían ser de utilidad. Como siempre, espero poder explicar cada una de estas pequeñas soluciones.

A medida que avanza este post voy a ir describiendo un pequeño programa que hice en C++ para un ATmega8 (simplemente por jugar, no tiene ningún fin práctico demasiado útil). En este programa utilizo una tabla que contiene los valores necesarios para mostrar los números del 0 (cero) al 9 (nueve) en un display 7 segmentos, incluyendo un onceavo valor que es un código de error (enciende el punto). Para esto, hice una clase «SevenSeg» para crear un objeto display al cual por medio de un método que utiliza una tabla almacenada en memoria de programa.

Veamos qué pasa:

Pasar un registro como parámetro

Para este primer tip no hace falta mucho, sólo saber que los registros de funciones especiales (SFRs) son un espacio en memoria RAM que se utiliza como interfaz entre el procesador y el hardware que conforma nuestro µC. Por ejemplo, para poder poner un pin del µC en 1 ó 0 debemos escribir en una dirección de memoria especial (en los AVR, se llama PORTx), claro que para eso hay otra dirección en memoria que sirve para configurar si ese pin va a ser entrada o salida (DDRx).

Ahora, si nos ponemos a explorar, veremos que en la librería del avr-libc para declarar un registro de los SFRs hay una serie de macros (una atrás de la otra), pero al fin y al cabo no es nada más que una dirección de memoria, manejada como un puntero a un «volatile uint8_t». Recuerden que un uint8_t es un typedef de un unsigned char y volatile es para que el compilador no la optimice, es decir, que le cambie la dirección al registro.

Por ende, si necesitamos pasar como parámetro a una función (o a este caso al constructor de mi clase), nos conviene utilizar un puntero o (cómo nos permite hacer en C++) una referencia a la dirección de un volatile uint8_t.

class SevenSeg
{
    // (...)
    public:
    SevenSeg(volatile uint8_t &ddrPort,volatile uint8_t &port,uint8_t type = COM_CAT);
    // (...)
}

Como habrán notado, no me voy a detener mucho en la implementación de la clase que cree, de todas formas se las voy a dejar en un link a mi GoogleDrive, como siempre. Observen que en C++ podemos utilizar el & en el parámetro, directamente, cosa que en C no es permitido. En C debemos declarar un puntero (volatile uint8_t *) y cuando llamamos a nuestra función agregar el &. A continuación les mostraré el main para ponerlos en tema de lo que hace el código.

#include <avr/io.h>
#include <util/delay.h>
#include "SevenSeg.h"

int main(void)
{
    // Creo el objeto display, ubicado en el Puerto D del micro
    SevenSeg display(DDRD,PORTD);

    while(1)
    {
        // Muestro los números del 0 al 9
        for(uint8_t i=0;i<10;i++)
        {
            display.put(i);
            _delay_ms(500);
        }
    }
    return 0;

Como verán, el código es bastante sencillo (es la idea), utilizo el método put() del objeto display para mostrar el número que quiero. En definitiva, no hay mucho más que pueda aportar. Recuerden que si el código está en C++, los fuentes son «.cpp» (C Plus Plus), y lo demás es igual que siempre. Pasemos al siguiente tópico.

Guardar tablas en la memoria de programa

Este es un recurso que, para los que hace rato que venimos programando microcontroladores (µC), nos es muy útil para almacenar tablas de valores constantes medianamente largas (o no tan largas). ¿Por qué almacenar datos en la memoria de programa? Bueno, muy simple, agarre la hoja da datos (datasheet) de su µC favorito y compare la memoria de programa (usualmente llamada FLASH) y la memoria de datos (RAM/SRAM). Por lo general la memoria de programa suele ser mucho mas grande que la RAM. Está bien, ya sé que en los AVR hay KBs de memoria de datos y muy pocas veces usamos tanto, sin embargo hay parte de la RAM que usamos sin saberlo, por ejemplo para el STACK. El STACK es una memoria tipo LIFO que almacena las direcciones de los saltos que pegamos (llamadas a función), para luego retornar al lugar donde estábamos. Claro que los que programamos en C/C++, ésto no nos preocupa demasiado, pero es bueno saber que existe, porque para aplicaciones grandes la optimización optimizar el uso de la memoria se va tornando cada vez más importante.

Analicemos un poco las dificultades de programar un µC en C/C++. Originalmente el lenguaje C nació como un lenguaje de programación para sistemas operativos (UNIX), y no hace falta decir que las PCs son en su mayoría de arquitectura Von Neumann. ¿Y qué tiene que ver eso? Mucho. Resulta que la mayoría de los µCs son de arquitectura Harvard o Harvard modificado y los AVR no difieren. La principal diferencia está en que en las arquitecturas Von Neumann la memoria de programa y la memoria de datos (vulgarmente ROM y RAM) comparten los buses de datos, direcciones y control. Mientras que en las arquitecturas del tipo Harvard las memorias tienen buses diferentes, es decir que no son direccionadas por el mismo bus. La ventaja de esto es que se pueden acceder a las dos memorias al mismo tiempo ganando en tiempo de proceso (lo cual es muy deseable en un sistema embebido).

Ahora pensemos que son los datos (o variables) en nuestros programas. Bueno, un profesor de la facultad nos comentaba que todo son datos y direcciones, ¿y saben qué? Es cierto. Sin ir más lejos, una variable no es más que una dirección en memoria en donde encontrar los datos. ¿Ahora ven el problema? Claro, como los buses de direcciones en una arquitectura Harvard son distintos no podemos direccionar esa memoria de la misma manera. ¿Cómo solucionan estas cosas los compiladores de C/C++ para µCs? Bueno, haciendo artilugios. En general, los programadores de compiladores nos proveen bytes adicionales para indicarle que queremos direccionar la memoria de programa y no la de datos. El avr-gcc no se salva de esto.

Basta de preámbulos, ¿cómo lo hacemos? Bien, para almacenar una variable en la memoria de programa hay un atributo especial en avr-gcc, pero para evitar al usuario final recordar difíciles palabras hay macros y defines que nos provee la librería. Éstas están en el header <avr/pgmspace.h>. Para declarar este atributo se hace con el define PROGMEM:

#include &lt;avr/pgmspace.h&gt;

/*
    Ubicación de los pines en el puerto seleccionado
    (puede variar, según la implementación)
*/
               // Pins: 7654|3210
                     // .gfe|dcba
#define CERO    0x3F // 0011|1111b
#define UNO     0x06 // 0000|0110b
#define DOS     0x5B // 0101|1011b
#define TRES    0x4F // 0100|1111b
#define CUATRO  0x66 // 0110|0110b
#define CINCO   0x6D // 0110|1101b
#define SEIS    0x7D // 0111|1101b
#define SIETE   0x07 // 0000|0111b
#define OCHO    0x7F // 0111|1111b
#define NUEVE   0x6F // 0110|1111b
#define ERROR   0x80 // 1000|0000b

static const uint8_t tabla[11] PROGMEM = {
    CERO,UNO,DOS,TRES,CUATRO,CINCO,SEIS,SIETE,OCHO,NUEVE,ERROR
};

Hagamos una pequeña aclaración, la memoria de programa de los AVR es de 16-bits, por ende, por más que pongamos uint8_t o unsigned char, vamos a estar desperdiciando 8-bits. Pero esto a modo informativo.

Bien, con eso nuestra tabla está en memoria de programa. Ahora nos queda acceder a ella. Si estabas pensando que era tan fácil como usar el array así nomás, te equivocaste. Veamos:

Acceso a una tabla almacenada en memoria

Para acceder a los datos que guardamos en memoria vamos a utilizar una macro que está en el header <avr/pgmspace.h>, esta macro es una pequeña rutina de assembler, la cual nos ocupará más de lo que esperábamos en la memoria de programa, pero aún así, se supone que debería ser mejor que sacrificar la RAM, hasta cierto punto. Lo ideal sería hacernos los machos y estudiar el assembler generado por el compilador, para entender mejor lo que está pasando y verificar que nuestro sacrificio de memoria de programa no sea muy excesivo.

Esta macro es «pgm_read_byte()» la cual recibe como parámetro la dirección del byte que queremos leer, por ende, en código se vería algo así:

variable = pgm_read_byte(&(tabla[<número>]))

Así se debería ver, estamos pasando la dirección del «número» de elemento a la macro para poder almacenarla en RAM en alguna «variable».

No se termina acá… apenas empieza

En el header hay muchas funciones para manejar strings guardados en memoria de programa, agregando un «_P» a las funciones «estándar» para, por ejemplo, comparar strings y demás conocidas. Todo lo que aquí vieron lo saqué de esta página. Es muy útil, échenle varios vistazos, guárdenla en favoritos. Les voy a dejar la declaración completa de la clase y su implementación ACÁ. También está el main.cpp para que lo puedan compilar y probar Uds. mismos.

Espero poder aportar mucho más que esto en los próximos meses, estoy con algunos proyectos en el colegio donde doy clases y espero que rindan frutos.

Sean felices y hasta la próxima.

  1. farameo

    hola. que es .gfe|dcba que no esta explicado. gracias

Dejar una respuesta