AVR programación en C – 15 Lectura y escritura en la Memoria EEPROM de los AVR

Por definición la memoria EEPROM (Electrically Erasable Programmable Read-Only Memory) es una memoria de almacenamiento no volátil que es grabada y borrada eléctricamente en la cual se pueden almacenar datos que perduren con el tiempo, inclusive si el dispositivo en el que se encuentra no está alimentado.

Los microcontroladores AVR contienen en su interior una memoria de este tipo, de reducida capacidad pero muy útil para guardar datos que no deseamos que se pierdan si el micro se queda sin energía, por ejemplo para guardar configuraciones del sistema que estén disponibles entre cada reinicio o encendido del micro.

En esta entrada se mostrará como escribir y leer dentro de la memoria EEPROM de los AVR.

Contenido

Descripción

Para mostrar la programación de la memoria EEPROM, se utiliza el microcontrolador ATmega32, el cual contiene una memoria de un tamaño de 1024 bytes (512 para el ATmega16).

La memoria EEPROM de los AVR se organiza en un espacio separado de la memoria de programa y la memoria RAM. Según el fabricante, la memoria puede resistir al menos 100,000 ciclos de escritura/borrado y el tiempo de almacenamiento de datos es de… bueno el tiempo básicamente es el tiempo de vida del microcontrolador.

La programación en lenguaje C/C++ de la memoria EEPROM es muy sencilla, se puede realizar de 2 maneras: una es escribiendo y leyendo sobre los registros del micro asociados y la otra es utilizando la biblioteca eeprom.h incluida en la colección avr-libc. Aquí se muestra como programarla de las 2 maneras, dejando al lector la decisión de cuál método utilizar.

Otra manera de grabar la memoria EEPROM es mediante el programador de hardware, es un proceso similar al de escritura del archivo .hex en la memoria flash, solo que hay que especificar que los datos o el archivo a grabar debe ser dentro de la memoria EEPROM.

Funcionamiento

Para acceder a la memoria EEPROM desde código se necesitan acceder a sus registros. Estos registros se encuentran mapeados en el espacio de dirección de E/S (I/O space) y se escribe y leen como cualquier otro registro.

Hay ciertas puntos que se deben de tomar en cuenta a la hora de programar esta memoria en los AVR, por ejemplo, la hoja de datos menciona que la memoria tiene un tiempo de acceso mínimo que se debe cumplir entre cada escritura. También menciona que los voltajes de alimentación deben ser lo más estable posibles para evitar corrupción de datos al momento de escribir.

El punto más importante mencionado en la hoja de datos, es que la escritura debe seguir un procedimiento específico o una serie de pasos que se deben seguir, esto para prevenir escrituras involuntarias dentro de la memoria. Además, la memoria EEPROM no se pude escribir mientras se lleva a cabo una escritura de la memoria FLASH. Este proceso se muesta en los ejemplos de escritura de la memoria.

Cuando la memoria es leída, el micro se congela durante 4 ciclos de reloj antes de que la siguiente instrucción sea ejecutada y cuando la memoria es escrita el micro se congela durante 2 ciclos de reloj antes de ejecutar la siguiente instrucción.

Registros asociados a la memoria EEPROM

La memoria EEPROM tiene 3 registros asociados para realizar operaciones de Escritura/Lectura: un registro de direcciones, un registro de datos y un registro de control:

EEARH y EEARL: EEPROM Address Register High y Low. Estos registros sirven para indicar la dirección de la memoria sobre la que se va a leer o escribir.

EERAH-L

EEDR: EEPROM Data Register. Este registro guarda el dato a escribir o el dato leído desde la memoria.

EEDR

EECR: EEPROM Control Register. Este registro controla las operaciones de lectura o escritura y contiene los estados de la operación actual.

EECR

  • EERIE: EEPROM Ready Interrupt Enable. Cuando este bit es 1, habilita la interrupción cada que la memoria EEPROM está lista para realizar otra operación de lectura o escritura, es decir cada que el registro EEWE se escribe con 0.
  • EEMWEEEPROM Master Write Enable. Este bit determina cuándo se debe escribir en la memoria. Cuando este bit se encuentra en 1, el escribir un 1 en el bit EEWE durante 4 ciclos de reloj se realizara una escritura del dato en el registro EEDR a la dirección EEARH/L. Si el bit EEMWE es 0, el escribir en el bit EEWE no tendrá efecto.
  • EEWE: EEPROM Write Enable. Este bit es la activación de la escritura de la memoria EEPROM. Una vez que la dirección y el dato han sido establecidos, este bit debe ser puesto a 1 para escribir dentro de la memoria, para esto el bit EEMWE deberá estar en 1.
  • EERE: EEPROM Read Enable. Este bit es la activación de la lectura de la memoria EEPROM. Cuando la dirección han sido establecidos, este bit se debe escribir en 1 para realizar la lectura de la memoria. El acceso de lectura de la memoria se ejecuta en una instrucción, y el dato solicitado estará disponible inmediatamente.
    Se debe verificar el bit EEWE antes de comenzar una operación de lectura, ya que no es posible leer ni cambiar el registro EEARH/L mientras se realiza una operación de escritura.

Nota importante: En algunos microcontroladors los bits EEMWE y EEWE están definidos como EEMPE y EEPE en las bibliotecas de C (avr-libc), es recomendable revisar el datasheet del micro que usen para verificar el nombre de los bits y registros.

Escritura y Lectura utilizando los registros

La escritura de la memoria eeprom es algo especial, como se mencionó anteriormente se deben seguir ciertos paso para lograr la escritura:

  1. Esperar hasta que el bit EEWE sea cero.
  2. Esperar hasta que el bit SPMEN en el registro SPCR sea cero.
  3. Escribir la nueva dirección de la memoria EEPROM en el registro EEAR (opcional)
  4. Escribir el nuevo dato en el registro EEDR (opcional)
  5. Escribir un 1 en el bit EEMWE mientras el bit EEWE está en 0 en el registro EECR.
  6. Esperar 4 ciclos de reloj después de escribir el bit EEMWE y a continuación escribir un 1 en el bit EEPE.

La escritura de la memoria EEPROM tarda aproximadamente 8.5ms, por lo que después de cada escritura debemos esperar como mínimo este tiempo antes de realizar otra escritura.

Nota: Para mantener la memoria EEPROM intacta entre cada grabado del programa dentro del microcontrolador, es necesario activar el fusible EESAVE (Preserve EEPROM memory through the Chip Erase cycle). Consulte la hoja de datos del micro que estén usando para determinar el valor de los fusibles o usen la página AVR Fuse Calculator.

Ejemplo

Este ejemplo muestra la escritura y verificación de un byte almacenado en la dirección 0x001 de la memoria, encendiendo un Led conectado al puerto PB0 en caso de que el dato sea guardado correctamente.

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

void EEPROM_write(uint16_t direccion, uint8_t dato);
uint8_t EEPROM_read(uint16_t direccion);

void AVRInit()
{
	DDRB = 0x01; // PB0 como salida
	PORTB = 0;
}

int main()
{
	AVRInit();

	// Escribe A en la dirección 0x051 de la eeprom.
	EEPROM_write(0x051, 'A');
	// Lee la dirección 0x051 y si es A enciende el led.
	if(EEPROM_read(0x051) == 'A')
	PORTB |= 0x01;

	while(1)
	{}

	return 0;
}

void EEPROM_write(uint16_t direccion, uint8_t dato)
{
	// Espera a que el bit EEWE en EECR sea 0.
	while(EECR & (1 << EEWE));
	// Espera a que el bit SPMEN en SPMCR sea 0.
	while( SPMCR &  (1 << SPMEN));
	// Escribe la dirección de la memoria en EEADR.
	EEAR = direccion;
	// Escribe el dato a guardar en EEDR.
	EEDR = dato;
	// Establece operación de escritura.
	EECR |= (1 << EEMWE);
	// Inicia la operación de escritura.
	EECR |= (1 << EEWE);
}

uint8_t EEPROM_read(uint16_t direccion)
{
	// Espera a que el bit EEWE en EECR sea 0.
	while(EECR &  (1 << EEWE));
	// Espera a que el bit SPMEN en SPMCR sea 0.
	while( SPMCR &  (1 << SPMEN));
	// Escribe la dirección de la memoria en EEADR.
	EEAR = direccion;
	// Inicia la operación de escritura.
	EECR |= (1 << EERE);
	// Devuelve el dato leido de la memoria.
	return EEDR;
}

Ejemplo utilizando interrupciones

Aquí se muestra el uso de las interrupciones para escribir  y leer (verificar) una cadena en la memoria EEPROM. Al igual que el programa anterior si la verificación es correcta (se lee lo mismo que se escribe) se enciende un Led conectado al puerto PB0. El vector de interrupción para la memoria EEPROM es EE_RDY_vect.

Escritura
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h> 

void EEPROM_write(uint16_t direccion, uint8_t dato);

char text[] = "Vida Embebida";
volatile int count = 13;
volatile int address = 0;
volatile uint8_t finish = 0;;
volatile uint8_t i = 0;

ISR(EE_RDY_vect)
{
    /* Verifica el fin de cadena y que la dirección no sobrepase el
    limite de la memoria EEPROM */
	if ( i < count &&  (address <= E2END) )
	{
		EEPROM_write(address++, text[i++]);
	}
	else
	{
		// Deshabilita la interrupción de la memoria EEPROM
		EECR &= ~(1<<EERIE);
		finish = 1;
	}
}

void AVRInit()
{
	DDRB = 0x01;	// PB0 como salida
	PORTB = 0;
	sei();			// Habilita interrupciones globales
}

int main()
{
	AVRInit();

    EECR |= (1<<EERIE);
	while(!finish);

	PORTB = 1;

	while(1){}

	return 0;
}

void EEPROM_write(uint16_t direccion, uint8_t dato)
{
	// Espera a que el bit EEWE en EECR sea 0.
	while(EECR &  (1 << EEWE));
	// Espera a que el bit SPMEN en SPMCR sea 0.
	while( SPMCR &  (1 << SPMEN));
	// Escribe la dirección de la memoria en EEADR.
	EEAR = direccion;
	// Escribe el dato a guardar en EEDR.
	EEDR = dato;
	// Establece operación de escritura.
	EECR |= (1 << EEMWE);
	// Inicia la operación de escritura.
	EECR |= (1 << EEWE);
}

Lectura
#include <avr/io.h>
#include <util/delay.h>
#include <avr/interrupt.h> 

void EEPROM_write(uint16_t direccion, uint8_t dato);
uint8_t EEPROM_read(uint16_t direccion);

char text[] = "Vida Embebida";
volatile char readed[20];
volatile int count = 13;
volatile int address = 0;
volatile uint8_t finish = 0;;
volatile uint8_t i = 0;

ISR(EE_RDY_vect)
{
    /* Verifica el fin de cadena y que la dirección no sobrepase el
    limite de la memoria EEPROM */
	if ( i < count &&  (address <= E2END) )
	{
		readed[i++] = EEPROM_read(address++);
	}
	else
	{
		// Deshabilita la interrupción de la memoria EEPROM
		EECR & = ~(1<<EERIE);
		finish = 1;
	}
}

void AVRInit()
{
	DDRB = 0x01;	// PB0 como salida
	PORTB = 0;
	sei();			// Habilita interrupciones globales
}

int main()
{
	AVRInit();

    EECR |= (1<<EERIE);
	while(!finish);

	for(i = 0; i< count; i++)
	{
		if(text[i] != readed[i])
			while(1);
	}
	PORTB = 1;

	while(1){}

	return 0;
}

uint8_t EEPROM_read(uint16_t direccion)
{
	// Espera a que el bit EEWE en EECR sea 0.
	while(EECR &  (1 << EEWE));
	// Espera a que el bit SPMEN en SPMCR sea 0.
	while( SPMCR &  (1 << SPMEN));
	// Escribe la dirección de la memoria en EEADR.
	EEAR = direccion;
	// Inicia la operación de escritura.
	EECR |= (1 << EERE);
	// Devuelve el dato leido de la memoria.
	return EEDR;
}

Escritura y Lectura utilizando la biblioteca eeprom.h (avrlibc)

Estos ejemplos realizan la misma tarea que los ejemplos anteriores, sólo que en lugar de utilizar las funciones propuestas para lectura y escritura, se utilizan las funciones incluidas en la biblioteca eeprom.h, la cual contiene otras funciones útiles para almacenar datos de mas de 1 byte de longitud como números enteros, números flotantes, arrays, cadenas o hasta estructuras de datos, que al final de cuentas son un array de bytes. La lista de funciones disponibles en la biblioteca eeprom.h es la siguiente:

  • uint8_t eeprom_read_byte (const uint8_t *__p) __ATTR_PURE__
  • uint8_t eeprom_read_byte (const uint8_t *__p) __ATTR_PURE__
  • uint16_t eeprom_read_word (const uint16_t *__p) __ATTR_PURE__
  • uint32_t eeprom_read_dword (const uint32_t *__p) __ATTR_PURE__
  • float eeprom_read_float (const float *__p) __ATTR_PURE__
  • void eeprom_read_block (void *__dst, const void *__src, size_t __n)
  • void eeprom_write_byte (uint8_t *__p, uint8_t __value)
  • void eeprom_write_word (uint16_t *__p, uint16_t __value)
  • void eeprom_write_dword (uint32_t *__p, uint32_t __value)
  • void eeprom_write_float (float *__p, float __value)
  • void eeprom_write_block (const void *__src, void *__dst, size_t __n)
  • void eeprom_update_byte (uint8_t *__p, uint8_t __value)
  • void eeprom_update_word (uint16_t *__p, uint16_t __value)
  • void eeprom_update_dword (uint32_t *__p, uint32_t __value)
  • void eeprom_update_float (float *__p, float __value)
  • void eeprom_update_block (const void *__src, void *__dst, size_t __n)

Además existen macros para declarar variables que se almacenarán en EEPROM y macros para saber el estado de las operaciones de escritura y lectura. Las macros son:

  • #define EEMEM   __attribute__((section(“.eeprom”)))
  • #define EEMEM   __attribute__((section(“.eeprom”)))
  • #define eeprom_is_ready()#define eeprom_busy_wait()   do {} while (!eeprom_is_ready())

Ejemplo

#include <avr/io.h>
#include <util/delay.h>
#include <avr/eeprom.h> 

void AVRInit()
{
	DDRB = 0x01; // PB0 como salida
	PORTB = 0;
}

int main()
{
	AVRInit();

	// Escribe el caracter M en la dirección 0x50 de la eeprom.
        eeprom_write_byte((uint8_t*)0x050, 'M');
        // Lee la dirección 0x50 y si es igual a M enciende el led
        if(eeprom_read_byte((uint8_t*)0x050) == 'M')
                PORTB |= 0x01;

	while(1)
	{}

	return 0;
}

Ejemplo con bloques de memoria

En este ejemplo se escribe una cadena de texto en la dirección 0x60 utilizando la función eeprom_write_block, se lee la dirección 0x60 y se compara con la cadena escrita, si las cadenas son iguales enciende el led en el puerto PB0.


#include <avr/io.h>
#include <util/delay.h>
#include <avr/eeprom.h>

#define BLOCK_SIZE 13
uint8_t text[] ="Vida Embebida";

int main(void)
{
	int i;
	uint16_t address = 0x060;
	uint8_t readed[BLOCK_SIZE];
	// Escribe en texto en la EEPROM
	eeprom_write_block((const void *)text, address, BLOCK_SIZE);

	// Lee el texto de la EEPROM
	eeprom_read_block ((void *)readed, address, BLOCK_SIZE);

	for(i = 0; i < BLOCK_SIZE; i++)
	{
		if(text[i] != readed[i])
			while(1);
	}
	PORTB = 1;

	while(1)
	{
		//do nothing
	}
}

Almacenando variables en EEPROM con el atributo EEMEM

Es posible almacenar variables directamente en la memoria EEPROM utilizando la macro EEMEM. Las variable se declaran como cualquier otra variable, sólo que se antepone la palabra EEMEM antes del nombre de la variable. Al compilar el programa, el enlazador crea una sección de datos especial para la memoria EEPROM, y mostrará cuánta de esta memoria es utilizada por el programa.

Las variables se almacenan en la memoria EEPROM en orden inverso a como han sido declaradas, es decir en la dirección 0 de la memoria EEPROM se almacena la última variable declarada y en la dirección NN la primera variable declarada. NN varia dependiendo del tamaño que ocupen las variables. La lectura de estas variables se hace mediante las funciones mencionadas anteriormente, y la dirección de lectura es la dirección de de memoria de la variable. Para entender como funciona veamos un ejemplo:

#include <avr/io.h>
#include <avr/eeprom.h>
#include <string.h> 

uint8_t EEMEM text[] ="Texto";
float EEMEM flotante = 5.55;
int EEMEM entero = 35;
uint16_t EEMEM word = 0xDEAD;

int main(void)
{
	DDRB = 0x00;
	char eeprom_text[6];

	eeprom_read_block(eeprom_text, & text, 6);

	if(eeprom_read_word(& word) == 0xDEAD &&
	   eeprom_read_float(& flotante) == 5.55 &&
	   strcmp(eeprom_text, "Texto") == 0)
		PORTB = 1;

	while(1)
	{}
}

Al compilar el código anterior con el Makefile de APOS, nos generará 2 archivos, un .hex y un .eep. El primero es el programa como tal, y se debe cargar dentro de la memoria flash. El segundo son los datos de la memoria EEPROM y deben ser cargados dentro de la misma. Para grabar los datos correspondientes a la memoria EEPROM se utiliza el comando:

avrdude -c usbtiny -p atmega32 -P usb-U eeprom:w:MY_ARCHIVO_EEPROM.eep:i

Si se desea generar el archivo .eep desde cero con el compilador avr-gcc se deben ejecutar los siguientes comandos:

Compilar:

avr-g++ -ffunction-sections -fpermissive -std=c++11 -Wl,-gc-sections -Wall -Os -DF_CPU=16000000 -mmcu=atmega32 -I . -I ./include -c main.c -o main.o

Enlazar:

avr-g++ -ffunction-sections -fpermissive -std=c++11 -Wl,-gc-sections -Wall -Os -DF_CPU=16000000 -mmcu=atmega32-I . -I ./include -o eeprom-write-manual.elf main.o

Generar archivo EEPROM:

avr-objcopy -j .eeprom –set-section-flags=.eeprom=”alloc,load” –change-section-lma .eeprom=0 -O ihex eeprom-write-manual.elf eeprom-write-manual.eep

Obviamente se deben cambiar los parámetros para el microcontrolador que utilicen, tanto el modelo como la frecuencia.

 

Conclusiones

El uso de la memoria EEPROM tiene una infinidad de aplicaciones, se puede utilizar para guardar el estado de un proceso actual y continúe donde se quedo en caso de que suceda un reset o un fallo de la alimentación; para guardar las opciones de un sistema, números de serie de un producto, versiones de firmware o lo que puedan imaginar que les sea de utilidad.

Espero este tutorial les sea de utilidad, Hasta la próxima!

2 comentarios sobre “AVR programación en C – 15 Lectura y escritura en la Memoria EEPROM de los AVR

Add yours

  1. Alfreedom, muchas gracias por todos tus tutoriales, por tomarte el tiempo de hacerlos y compartirlos..me va a ser de mucha ayuda para comenzar a entender mucho mejor los atmega..de verdad muchas gracias y saludos desde Colombia.

Dejar un comentario

Crea una web o blog en WordPress.com

Subir ↑