Conecte uma EEPROM serial com o Arduino via SPI

Aprenda como fazer a interface com uma EEPROM serial da Atmel AT25HP512 mediante o protocolo Serial Peripheral Interface ou SPI. Os chips EEPROM tal como esse são muito úteis para armazenar dados, e os passos para implementar a comunicação SPI podem ser mudados de acordo com o dispositivo SPI em questão. Perceba que o chip presente na placa Arduino contém uma EEPROM interna, portanto, acompanhe este tutorial caso precise de mais espaço do que já lá existe.

Materiais necessários

  • Chip EEPROM serial, modelo AT25HP512 (ou similar)
  • Fios
  • Placa Arduino

Introdução à SPI

Serial Peripheral Interface (SPI) é um protocolo de dados seriais síncrono usado por microcontroladores para se comunicarem rapidamente, a curtas distâncias, com um ou mais dispositivos periféricos. Também pode ser usado para a comunicação entre dois microcontroladores.

Em uma conexão SPI, há sempre um dispositivo mestre (geralmente o microcontrolador), que controla os dispositivos periféricos. Normalmente há três linhas comuns entre todos os dispositivos,

  • Master In Slave Out (MISO) - Linha do escravo, para enviar dados ao mestre,
  • Master Out Slave In (MOSI) - Linha do mestre, para enviar dados aos periféricos,
  • Serial Clock (SCK) - Pulsos de clock que sincronizam a transmissão de dados gerados pelo mestre, e
  • Pino Slave Select - existente em cada periférico, que pode ser usada pelo mestre para habilitá-los ou inibi-los e evitar transmissões falsas devidas ao ruído na linha.

A parte difícil do SPI é que é uma padronização relaxada e assim cada dispositivo a implementa de forma um pouco diferente entre si. Isso significa que você deve prestar atenção especial à folha de dados quando for escrever o código de interface. Em geral, há três modos de transmissão, numerados de 0 a 3. Eles controlam se os dados são enfileirados no momento de subida OU descida do sinal de clock, e se o clock está pausado se estiver em HIGH ou LOW.

Todas as configurações de SPI no Arduino são determinadas pelo registrador Arduino SPI Control (SPCR). Um registrador é tão-somente um byte dentro do microcontrolador, que pode ser lido e gravado. Eles geralmente servem para três finalidades: controle, dados e estado.

Os registradores de controle servem para ajustes de várias funcionalidades do microcontrolador. Geralmente, cada bit de um registrador de controle provoca um efeito particular, tal como ajuste na velocidade, ou na polaridade.

Os registradores de dados apenas guardam dados. Por exemplo, o registrador SPI Data (SPDR) guarda o byte que está para ser enviado pela linha MOSI, e os dados que acabaram de chegar pela linha MISO.

Os registradores de estado mudam de valor de acordo com condições do microcontrolador. Por exemplo, o 7o. bit do registrador SPI Status (SPSR) vai para 1 quando um valor foi enviado para fora ou foi recebido pela SPI.

O registrador SPI Control (SPCR) tem 8 bits e cada um controla uma configuração particular da SPI.

SPCR
76543210
SPIESPEDORDMSTRCPOLCPHASPR1SPR0

 SPIE - Habilita interrupção SPI quando 1
 SPE - Habilita SPI quando 1
 DORD - Envia primeiro o bit menos significativo quando 1 e o mais significativo quando 0 
 MSTR - Ajusta o Arduino em modo mestre quando 1, modo escravo quando 0
 CPOL - Clock pausado em HIGH quando 1, pausado em LOW quando 0
 CPHA - Realiza amostragem dos dados na descida do clock quando 1 e na subida do clock quando 0
 SPR1 e SPR0 - Ajusta velocidade da SPI, 00 é mais rápido (4MHz) e 11, mais lento (250KHz)

Isto significa que, para escrever código para um novo dispositivo SPI, é necessário observar vários aspectos e ajustar o SPCR adequadamente:

  • Os dados são enfileirados com o bit mais significativo em primeiro, ou o bit menos significativo?
  • O clock é pausado em nível alto ou baixo?
  • As amostras estão na subida ou na descida do clock?
  • Qual é a velocidade da SPI?

Uma vez configurado corretamente o registrador SPI Control, aí apenas deve-se descobrir quanto tempo de pausa existe entre instruções. Agora que já tem uma idéia de como a SPI funciona, vamos dar uma olhada nos detalhes do chip EEPROM.

Introdução à EEPROM serial

O AT25HP512 é uma EEPROM serial de 65,536 bytes. Suporta os modos SPI 0 e 3 e roda a até 10 MHz a 5 V, podendo rodas a velocidades menores até 1,8 V. Sua memória é organizada na forma de 512 páginas de 128 bytes cada. Pode-se apenas gravar-se 128 bytes de cada vez, mas a leitura é de 1 a 128 bytes por vez. Esse dispositivo também oferece vários graus de proteção contra gravação e um pino Hold, mas não vamos falar disso neste tutorial.

O dispositivo é habilitado quando aplicamos LOW no pino Chip Select (CS). As instruções são enviadas em códigos operativos de 8 bits (opcodes) e são amostrados à subida do sinal de clock. A EEPROM demora cerca de 10 milissegundos para gravar uma página (128 bytes) de dados, de forma que é necessário incluir uma pausa de 10 ms a cada chamada à rotina de gravação à EEPROM.

Prepare a protoboard

Insira o chip AT25HP512 na protoboard. Conecte os 5 V e o terra ao protoboard e ao chip. Depois conecte os pinos 3, 7 e 8 do chip aos 5 V e o pino 4 ao terra.

Os fios de 5 V são vermelhos e os pretos são os do terra

Conecte o pino 1 da EEPROM ao pino 10 do Arduino (Slave Select - SS), o pino 2 da EEPROM ao pino 12 do Arduino (Master In Slave Out - MISO), o pino 5 do chip ao pino 11 do Arduino (Master Out Slave In - MOSI) e o pino 6 do chip ao pino 13 do Arduino (Serial Clock - SCK).

O branco é o SS, o MISO é amarelo, o MOSI é azul e o SCK é verde

Programe o Arduino

Agora escrevemos o código que permitirá a comunicação SPI entre a EEPROM e o Arduino. Na rotina setup(), este programa preenche a EEPROM com 128 bytes, ou uma página completa da EEPROM. No loop(), retiramos dela os bytes, um de cada vez, e imprimimo-nos à porta serial. Acompanhe os comentários ao longo do código.

O primeiro passo é ajustar as diretivas de pré-processamento. Elas são processadas antes que a compilação propriamente dita ocorra. Iniciam-se com "#" e não terminam com ponto-e-vírgula.

Definimos os pinos que usaremos para a conexão SPI: DATAOUT, DATAIN, SPICLOCK e SLAVESELECT. E então definimos os opcodes da EEPROM. Opcodes são comandos de controle:

#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

Agora, alocamos espaço para as variáveis globais que usaremos mais logo abaixo no programa. Observe o char buffer[128]; -- trata-se de um vetor de 128 bytes que usaremos para guardar os dados a gravar à EEPROM:

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

Primeiro inicializamos a conexão serial, ajustamos os modos dos pinos de entrada e saída e ajustamos a linha SLAVESELECT para HIGH, para começar. Assim, des-selecionamos o dispositivo e evitamos que haja mensagens transmitidas erroneamente por causa de ruído branco:

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

  pinMode(DATAOUT, OUTPUT);
  pinMode(DATAIN, INPUT);
  pinMode(SPICLOCK,OUTPUT);
  pinMode(SLAVESELECT,OUTPUT);
  digitalWrite(SLAVESELECT,HIGH); // desabilita chip

Agora dispomos o valor binário 01010000 dentro do registrador SPI Control (SPCR). No registrador de controle, cada bit faz o ajuste de uma funcionalidade diferente. O 8o. bit desabilita interrupções SPI, o 7o. bit habilita a SPI, o 6o. bit diz que bits mais significativos serão enviados primeiro, o 5o. bit põe o Arduino em modo mestre, o 4o. bit comanda que o clock é pausado em nível LOW, o 3o. bit ajusta a SPI para fazer amostragens de dados à subida do clock e os 2o. e 1o. bits ajustam a velocidade da SPI para (mesma do sistema / 4) (que é a mais rápida). Depois de ajustar o registrador de controle, inserimos o valor nos registradores SPI Status (SPSR) e SPI Data (SPDR) dentro da variável clr, para limpar qualquer informação espúria:

 // 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); 

Agora preenchemos o vetor de dados com números e enviamos à EEPROM uma instrução para permitir gravação. A EEPROM precisa ser habilitada para gravação antes que façamos qualquer gravação nela. Para isso, põe-se a linha SLAVESELECT em LOW, o que habilita o chip, e então enviamos a instrução por meio da função spi_transfer(). Observe que usamos o opcode WREN definido ao início do programa. Finalmente, põe-se de volta a linha SLAVESELECT em HIGH para liberá-la:

// preencha buffer com dados
  fill_buffer();
  // preencha a eeprom com o buffer
  digitalWrite(SLAVESELECT,LOW);
  spi_transfer(WREN); // permita gravação
  digitalWrite(SLAVESELECT,HIGH);

Agora põe-se a linha SLAVESELECT em LOW, para selecionar o dispositivo novamente, após uma pequena pausa. Enviamos uma instrução WRITE para dizer à EEPROM que enviaremos dados para gravação em memória. Enviamos os 16 bits do endereço onde se começará a gravação na forma de dois bytes, com o byte mais significativo primeiro. Em seguida, vão os 128 bytes de dados provenientes do nosso vetor, um byte após o outro, sem pausas. Finalmente põe-se a linha SLAVESELECT em HIGH para liberar o dispositivo e envia-se uma pausa para que a EEPROM processe a gravação:

  delay(10);
  digitalWrite(SLAVESELECT,LOW);
  spi_transfer(WRITE); // grave a instrução
  address = 0;
  spi_transfer((char)(address>>8));   //envie o byte mais significativo primeiro
  spi_transfer((char)(address));      //envie o outro byte do endereço
  // grave 128 bytes
  for (int I=0;I<128;I++)
  {
    spi_transfer(buffer[I]); //grave byte
  }
  digitalWrite(SLAVESELECT,HIGH); //libere o chip
  //aguarde a eeprom 'digerir' os dados
  delay(3000); 

O fim do setup() consiste em enviar a palavra "hi" mais um caracter nova linha à porta serial, para fins de depuração de erros. Dessa forma, se nossos dados estiverem estranhos mais tarde, poderemos dizer se não é culpa da porta serial:

  Serial.print('h',BYTE);
  Serial.print('i',BYTE);
  Serial.print('\n',BYTE); // depuração de erros
  delay(1000);
}

Em loop(), apenas lemos um byte por vez a partir da EEPROM e imprimimos na porta serial. Incluímos um caracter de nova linha e uma pause por questões de legibilidade. A cada iteração, incrementamos o endereço da EEPROM a ser lido. Quando chegar ao endereço 128, meia-volta ao 0 pois há apenas 128 endereços preenchidos com dados dentro da EEPROM:

void loop()
{
  eeprom_output_data = read_eeprom(address);
  Serial.print(eeprom_output_data,DEC);
  Serial.print('\n',BYTE);
  address++;
  delay(500); // pausa para questões de legibilidade
} 

A função fill_buffer() simplesmente preenche nosso vetor de dados com os números de 0 a 127 em cada índice do vetor. Mude-a de acordo com a sua necessidade:

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

A função spi_transfer() carrega os dados de saída dentro do registrador de transmissão de dados, o que causa o início da transmissão SPI. Ela vigia, com uma máscara de bits, um bit do registrador SPI Status (SPSR) para detectar se a transmissão foi completada. Uma explicação sobre máscaras de bits pode ser vista aqui. Em seguida, retorna qualquer dado que tiver chegado a partir da EEPROM:

char spi_transfer(volatile char data)
{
  SPDR = data;                    // Começa transmissão
  while (!(SPSR & (1<<SPIF)))     // Espera finalizar
  {
  };
  return SPDR;                    // Retorna o byte chegador
}

A função read_eeprom() permite ler dados vindos da EEPROM. Primeiro, põe-se a linha SLAVESELECT em LOW, para habilitar o chip. A seguir, transmitimos uma instrução READ seguida pelo endereço de 16 bits do qual queremos a informação, sendo o bit mais significativo primeiro. Depois, enviamos um byte de enchimento à EEPROM com o objetivo de deslocar a informação para fora do chip. Finalmente, põe-se a linha SLAVESELECT em HIGH novamente, para liberar o chip, e retorna-se o dado. Se quiséssemos ler múltiplos bytes, poderíamos manter a linha SLAVESELECT em LOW enquanto se repetia a parte do data = spi_transfer(0xFF); -- até o máximo de 128 vezes, que é uma página inteira de dados:

byte read_eeprom(int EEPROM_address)
{
  // lê EEPROM
  int data;
  digitalWrite(SLAVESELECT,LOW);
  spi_transfer(READ); // transmita opcode READ
  spi_transfer((char)(EEPROM_address>>8));   //envie o byte mais significativo primeiro
  spi_transfer((char)(EEPROM_address));      //envie o outro byte
  data = spi_transfer(0xFF); // receba byte de dados
  digitalWrite(SLAVESELECT,HIGH); // libere o chip, sinalize fim transmissão
  return data;
} 

Share