Learning   Examples | Foundations | Hacking | Links

Interactuando con una memoria EEPROM Serie usando SPI.

En este tutorial aprenderás como interactuar con una memoria EEPROM Serie AT25HP512 de la marca Atmel utilizando el protocolo de Interfaz Periférico Serie (siglas SPI en ingles). Los chips de memoria EEPROM son ampliamente utilizados para almacenar datos. Los pasos que veremos para implementar una comunicación SPI pueden ser fácilmente modificados para su uso con cualquier otro dispositivo SPI. Ten en cuenta que el chip de la placa Arduino contiene una memoria EEPROM interna, sigue este tutorial sólo si necesitas una cantidad mayor.

Materiales que necesitamos:

  • Chip AT25HP512 EEPROM Serie (o similar).
  • Cables de unión.
  • Microcontrolador Arduino.

Introducción al Interfaz Periférico Serie.

El Interfaz Periférico Serie (SPI) es un protocolo de datos serie síncrono utilizado por microcontroladores para comunicar con uno o varios dispositivos periféricos de una forma rápida y a través de una distancia corta. También puede comunicar dos microcontroladores.

En la comunicación SPI hay un dispositivo maestro (generalmente el microcontrolador) que controla varios dispositivos esclavos. Típicamente se usan tres lineas comunes para todos los dispositivos:

  • Master In Slave Out (MISO) - La linea del esclavo para enviar datos al maestro.

  • Master Out Slave In (MOSI) - La linea del maestro para enviar datos a los periféricos.

  • Serial Clock (SCK) - Los pulsos de reloj generados por el maestro para sincronizar la transmisión de los datos.

Se utiliza una cuarta linea que es diferente para cada uno de los periféricos:

  • Slave Select pin (SS) - Asignada a cada uno de los dispositivos el maestro la utiliza para habilitar o inhabilitar la comunicación. Esto protege de falsas transmisiones por culpa de posibles ruidos en los cableados.

La parte difícil del SPI es que se ha perdido el standard y cada dispositivo implementa un protocolo ligeramente diferente. Esto quiere decir que debes prestar especial atención a las hojas de características (datasheet) del dispositivo cuando escribas el código para la comunicación. Generalmente hablando hay cuatro tipos de transmisiones numeradas del 0 al 3. Estos tipos de transmisión establecen si los datos se desplaza en un sentido u otro en el flanco de subida o de bajada de la señal de reloj y cuando el reloj entra en reposo, si en estado alto o bajo.

Todos los valores del SPI están determinados por el Registro de Control SPI (siglas en ingles SPCR) de Arduino. El registro es un byte en la memoria del microcontrolador que puede ser leído o escrito. Los registros normalmente sirven para tres propósitos: control, datos y estado.

Los registros de control establecen varias funcionalidades de los microcontroladores. Usualmente cada bit en un registro de control afecta a una función particular, como velocidad o polaridad.

Los registros de datos solo almacenan bytes. Por ejemplo, el registro de datos del SPI almacena el byte que está a punto de ser lanzado por la linea MOSI, y el byte que acaba de ser recibido por la linea MISO.

Los registros de estado cambian su estado basándose en varias condiciones del microcontrolador. Por ejemplo, el séptimo bit del registro de estado del SPI se pone en 1 cuando un valor es desplazado dentro o fuera del SPI.

El registro de control SPI (SPCR) tiene 8 bits, cada uno de ellos controla un parametro particular del SPI.

SPCR
| 7    | 6    | 5    | 4    | 3    | 2    | 1    | 0    |
| SPIE | SPE  | DORD | MSTR | CPOL | CPHA | SPR1 | SPR0 |

SPIE - Permite la interrupción del SPI cuando es 1.
SPE - Permite el SPI cuando es 1.
DORD - Envía el bit menos significativo primero cuando el 1, el mas significativo bit primero cuando es 0.
MSTR - Establece la Arduino como maestro cuando es 1, esclava cuando es 0.
CPOL - Ajusta la polaridad del reloj en alto cuando es 1 y en bajo cuando es 0.
CPHA - Envía los datos en el flanco de bajada del reloj cuando es 1, en el de subida cuando es 0.
SPR1 and SPR0 - Establece la velocidad del SPI: 00 es la mas rápida (4MHz) y 11 es la mas lenta (250KHz).

Esto significa que para escribir código para un periférico SPI nuevo debes tener en cuenta sus especificaciones y establecer el SPCR adecuadamente:

  • ¿Son los datos desplazados en MSB o en LSB primero?.
  • ¿Está el reloj inactivo en alto o en bajo?.
  • ¿Se hace el muestreo en el flanco de subida o en el de bajada?.
  • ¿A que velocidad correrá el SPI?.

Un vez que tengas el Registro de Control del SPI correctamente establecido solo necesitas calcula como de larga debe ser la pausa entre instrucciones y estarás listo para continuar. Ahora que sabes como funciona el SPI es el momento de ver los detalles del chip EEPROM.

Introducción a la EEPROM Serie.

El AT25HP512 es una memoria EEPROM Serie de 65.536 bytes. Soporta los modos SPI 0 y 3, funciona a 10MHZ y 5v y puede funcionar a velocidades inferiores por debajo de los 1.8v. Esta memoria está organizada en 512 páginas de 128 bytes cada una. Solo se pueden escribir 128 bytes cada vez, pero pueden ser leídos de 1 a 128 bytes cada vez. El dispositivo también ofrece varios grados de protección de escritura y un pin de espera, pero no veremos eso en este tutorial.

El dispositivo esta habilitado cuando su pin Chip Select (CS) esta en nivel bajo. Las instrucciones se envían como códigos operacionales (opcodes) de 8 bits y son desplazados en el borde de subida del pulso de reloj. Esto le lleva a la EEPROM unos 10 milisegundos para escribir una página de datos (128 bytes) por lo que hay que introducir en la rutina de escritura una pausa de 10ms por cada ciclo de escritura.

Preparando la placa entrenadora.

Insertamos el chip AT25HP512 en la placa entrenadora. Conectamos 5v al pin de alimentación y el pin GND lo conectamos a la linea negativa del microcontrolador. Conectamos los pines 3, 7 y 8 de la EEPROM a 5v y el pin 4 a GND.

los cables +5v en rofo, los GND en negro

Conectamos el pin 1 de la EEPROM al pin 10 de la Arduino (Slave Select - SS), el pin 2 de la EEPROM al pin 12 de la Arduino (Master In Slave Out - MISO), el pin 5 de la EEPROM al pin 11 de la Arduino (Master Out Slave In - MOSI),y por último, el pin 6 de la EEPROM al pin 13 de la Arduino (Serial Clock - SCK).

El cable SS es blanco, el MISO es amarillo, MOSI azul y SCK en color verde.

Programando la Arduino.

Ahora escribiremos el código para permitir la comunicación SPI entre la EEPROM y nuestra Arduino. En la rutina setup() este programa grabará 128 bytes, o una página, de datos en la EEPROM. En la rutina loop() leeremos los datos, un byte cada vez y enviaremos esta byte por el puerto serie. Iremos comentando el código por pequeñas secciones.

El primer paso es establecer nuestra directivas de pre-procesamiento. Estas directivas se procesan antes de que la compilación comience, cada linea empieza con el carácter # y no necesita terminar con ;.

Definimos los pines que vamos a utilizar para nuestra conexión SPI: DATAOUT, DATAIN, SPICLOCK y SLAVESELECT. Después definiremos nuestros "opcodes" para la EEPROM. Los "opcodes" son comandos de control:

#define DATAOUT 11//MOSI
#define DATAIN  12//MISO 
#define SPICLOCK  13//sck
#define SLAVESELECT 10//ss

//opcodes
#define WREN  6
#define WRDI  4
#define RDSR  5
#define WRSR  1
#define READ  3
#define WRITE 2 

Aquí ubicamos las variables globales que mas adelante usaremos en el programa. Nota: char buffer [128]; es una matriz de 128 bytes que usaremos para almacenar datos y escribirlos en la EEPROM.

byte eeprom_output_data;
byte eeprom_input_data=0;
byte clr;
int address=0;
//data buffer
char buffer [128]; 

Primero inicializamos la comunicación serie, establecemos nuestros pines de entrada o salida y fijamos SLAVESELECT en nivel alto para comenzar. Esto inhabilita el dispositivo e impide cualquier transmisión falsa debida a ruido en los cables:

void setup()
{
  Serial.begin(9600);

  pinMode(DATAOUT, OUTPUT);
  pinMode(DATAIN, INPUT);
  pinMode(SPICLOCK,OUTPUT);
  pinMode(SLAVESELECT,OUTPUT);
  digitalWrite(SLAVESELECT,HIGH); //disable device 

Ahora establecemos el registro de control SPI (SPCR) con el valor binario 01010000. En el registro de control cada bit tiene una función diferente. El octavo bit deshabilita las interrupciones del SPI, el séptimo habilita el SPI, el sexto bit elige la transmisión del bit más significativo primero, el quinto bit establece la Arduino como maestro, el cuarto señala que el ciclo bajo del reloj es el de reposo, el tercer bit establece el SPI para que muestree datos en el borde de subida de cada ciclo de reloj y el segundo y primer bits fijan la velocidad en 4, el modo más rápido.

Después de definir nuestro registro de control pasamos a leer el registro de estado (SPSR) y el registro de datos (SPDR) con una variable clr que limpiará cualquier dato falso contenido en esos registros.

 
  // SPCR = 01010000
  //interrupt disabled,spi enabled,msb 1st,master,clk low when idle,
  //sample on leading edge of clk,system clock/4 rate (fastest)
  SPCR = (1<<SPE)|(1<<MSTR);
  clr=SPSR;
  clr=SPDR;
  delay(10); 

Aquí llenaremos nuestra matriz con números y enviaremos una instrucción que permita la escritura en la EEPROM. La EEPROM debe tener la escritura habilitada antes de enviar ninguna instrucción de escritura. Para enviar la instrucción ponemos la linea SLAVESELECT (SS) en estado bajo, seleccionando el dispositivo, y después enviamos la instrucción utilizando la función spi_transfer(). Al utilizar el opcode WREN estamos definiendo el comienzo del programa. Finalmente devolvemos la linea SLAVESELECT a estado alto para liberarlo.

  //fill buffer with data
  fill_buffer();
  //fill eeprom w/ buffer
  digitalWrite(SLAVESELECT,LOW);
  spi_transfer(WREN); //write enable
  digitalWrite(SLAVESELECT,HIGH);

Ahora volvemos a poner la línea SLAVESELECT en estado bajo para volver a seleccionar el dispositivo tras un pequeño retardo. Enviamos una instrucción WRITE para comunicarle a la EEPROM que le vamos a enviar datos que debe guardar en la memoria. Enviamos una dirección de 16 bits en 2 bytes para comenzar la escritura, el bit más significativo primero (Most Significant Bit first). Luego enviaremos los 128 bytes de datos almacenados en nuestra matriz, un byte tras otro sin pausas. Finalmente volveremos a poner el pin SLAVESELECT en estado alto para liberar el dispositivo y haremos una pausa para darle tiempo a la EEPROM a escribir los datos.

  delay(10);
  digitalWrite(SLAVESELECT,LOW);
  spi_transfer(WRITE); //write instruction
  address=0;
  spi_transfer((char)(address>>8));   //send MSByte address first
  spi_transfer((char)(address));      //send LSByte address
  //write 128 bytes
  for (int I=0;I<128;I++)
  {
    spi_transfer(buffer[I]); //write data byte
  }
  digitalWrite(SLAVESELECT,HIGH); //release chip
  //wait for eeprom to finish writing
  delay(3000); 

Terminamos la función de setup() enviando la palabra "hi" más un retorno de carro por el puerto serie para corrección de errores.

  Serial.print('h',BYTE);
  Serial.print('i',BYTE);
  Serial.print('\n',BYTE);//debug
  delay(1000);
}

En nuestro bloque loop() tan solo leeremos uno por uno los bytes de la EEPROM y los escribiremos a través del puerto serie. Podemos añadir un retorno de carro y una pausa por motivos de legibilidad. Cada vez que se ejecute el bucle incrementaremos la dirección a leer. Cuando la dirección llegue al valor 128 la pondremos a 0 por que solo hemos llenado 128 direcciones en la EEPROM con datos.

void loop()
{
  eeprom_output_data = read_eeprom(address);
  Serial.print(eeprom_output_data,DEC);
  Serial.print('\n',BYTE);
  address++;
  delay(500); //pause for readability
} 

La función fill_buffer simplemente llena nuestra matriz de datos con números del 0 al 127, uno para cada índice de la matriz. Esta función puede ser fácilmente modificada para llenar la matriz con datos que sean relevantes a tus aplicaciones.

void fill_buffer()
{
  for (int I=0;I<128;I++)
  {
    buffer[I]=I;
  }
} 

La función spi_transfer carga los datos de salida en el registro de transmisión de datos, así empieza la transmisión SPI. Sondeando un bit en el registro de estado del SPI (SPSR) para detectar cuando la transmisión se ha completado usando una máscara de bit, SPIF. Una explicación más profunda de lo que son las máscaras de bits se puede encontrar aquí. Esta retorna los datos que han sido desplazados en el registro de datos por la EEPROM.

char spi_transfer(volatile char data)
{
  SPDR = data;                    // Start the transmission
  while (!(SPSR & (1<<SPIF)))     // Wait for the end of the transmission
  {
  };
  return SPDR;                    // return the received byte
}

La función read_eeprom nos permite leer los datos devueltos por la EEPROM. Primero debemos poner en estado bajo la línea SLAVESELECT para seleccionar el dispositivo. Luego transmitimos una instrucción READ, seguida por la dirección de 16 bits (Most Significant Bit first) que queremos leer. Lo siguiente es enviar un byte falso a la EEPROM con el propósito de desplazar el dato fuera. Finalmente ponemos la línea SLAVESELECT en estado alto para liberar el dispositivo después de leer un byte y retornar el dato. Si queremos leer múltiples bytes al mismo tiempo podemos mantener la línea SLAVESELECT en estado bajo mientras repetimos la data = spi_transfer(0xFF); hasta 128 veces por cada página completa de datos.

byte read_eeprom(int EEPROM_address)
{
  //READ EEPROM
  int data;
  digitalWrite(SLAVESELECT,LOW);
  spi_transfer(READ); //transmit read opcode
  spi_transfer((char)(EEPROM_address>>8));   //send MSByte address first
  spi_transfer((char)(EEPROM_address));      //send LSByte address
  data = spi_transfer(0xFF); //get data byte
  digitalWrite(SLAVESELECT,HIGH); //release chip, signal end transfer
  return data;
} 

Para un más fácil copiado y pegado aquí viene el sketch completo:

#define DATAOUT 11//MOSI
#define DATAIN  12//MISO 
#define SPICLOCK  13//sck
#define SLAVESELECT 10//ss

//opcodes
#define WREN  6
#define WRDI  4
#define RDSR  5
#define WRSR  1
#define READ  3
#define WRITE 2

byte eeprom_output_data;
byte eeprom_input_data=0;
byte clr;
int address=0;
//data buffer
char buffer [128];

void fill_buffer()
{
  for (int I=0;I<128;I++)
  {
    buffer[I]=I;
  }
}

char spi_transfer(volatile char data)
{
  SPDR = data;                    // Start the transmission
  while (!(SPSR & (1<<SPIF)))     // Wait the end of the transmission
  {
  };
  return SPDR;                    // return the received byte
}

void setup()
{
  Serial.begin(9600);

  pinMode(DATAOUT, OUTPUT);
  pinMode(DATAIN, INPUT);
  pinMode(SPICLOCK,OUTPUT);
  pinMode(SLAVESELECT,OUTPUT);
  digitalWrite(SLAVESELECT,HIGH); //disable device
  // SPCR = 01010000
  //interrupt disabled,spi enabled,msb 1st,master,clk low when idle,
  //sample on leading edge of clk,system clock/4 rate (fastest)
  SPCR = (1<<SPE)|(1<<MSTR);
  clr=SPSR;
  clr=SPDR;
  delay(10);
  //fill buffer with data
  fill_buffer();
  //fill eeprom w/ buffer
  digitalWrite(SLAVESELECT,LOW);
  spi_transfer(WREN); //write enable
  digitalWrite(SLAVESELECT,HIGH);
  delay(10);
  digitalWrite(SLAVESELECT,LOW);
  spi_transfer(WRITE); //write instruction
  address=0;
  spi_transfer((char)(address>>8));   //send MSByte address first
  spi_transfer((char)(address));      //send LSByte address
  //write 128 bytes
  for (int I=0;I<128;I++)
  {
    spi_transfer(buffer[I]); //write data byte
  }
  digitalWrite(SLAVESELECT,HIGH); //release chip
  //wait for eeprom to finish writing
  delay(3000);
  Serial.print('h',BYTE);
  Serial.print('i',BYTE);
  Serial.print('\n',BYTE);//debug
  delay(1000);
}

byte read_eeprom(int EEPROM_address)
{
  //READ EEPROM
  int data;
  digitalWrite(SLAVESELECT,LOW);
  spi_transfer(READ); //transmit read opcode
  spi_transfer((char)(EEPROM_address>>8));   //send MSByte address first
  spi_transfer((char)(EEPROM_address));      //send LSByte address
  data = spi_transfer(0xFF); //get data byte
  digitalWrite(SLAVESELECT,HIGH); //release chip, signal end transfer
  return data;
}

void loop()
{
  eeprom_output_data = read_eeprom(address);
  Serial.print(eeprom_output_data,DEC);
  Serial.print('\n',BYTE);
  address++;
  if (address == 128)
    address = 0;
  delay(500); //pause for readability
}


código y tutorial por Dewey-Hagborg, fotos por Thomas Dexter

Share