Como você usa o SPI em um Arduino?


Respostas:


81

Introdução ao SPI

A interface SPI ( Serial Peripheral Interface Bus ) é usada para comunicação entre vários dispositivos em curtas distâncias e em alta velocidade.

Normalmente, existe um único dispositivo "mestre", que inicia as comunicações e fornece o relógio que controla a taxa de transferência de dados. Pode haver um ou mais escravos. Para mais de um escravo, cada um tem seu próprio sinal de "seleção de escravo", descrito mais adiante.


Sinais SPI

Em um sistema SPI completo, você terá quatro linhas de sinal:

  • Master Out, Slave In ( MOSI ) - que são os dados que vão do mestre para o escravo
  • Master In, Slave Out ( MISO ) - que são os dados que vão do escravo para o mestre
  • Relógio serial ( SCK ) - quando alterna a amostra principal e a escrava do próximo bit
  • Seleção de Escravo ( SS ) - diz a um escravo em particular para ficar "ativo"

Quando vários escravos são conectados ao sinal MISO, espera-se que eles tri-state (mantenha a alta impedância) que a linha MISO até que eles sejam selecionados pelo Slave Select sendo afirmados. Normalmente, a Seleção de Escravo (SS) fica baixa para afirmá-lo. Ou seja, é ativo baixo. Uma vez que um escravo específico é selecionado, ele deve configurar a linha MISO como uma saída, para que possa enviar dados ao mestre.

Esta imagem mostra a maneira como os dados são trocados quando um byte é enviado:

Protocolo SPI mostrando 4 sinais

Observe que três sinais são saídas do mestre (MOSI, SCK, SS) e um é uma entrada (MISO).


Cronometragem

A sequência de eventos é:

  • SS vai baixo para afirmar e ativar o escravo
  • A SCKlinha alterna para indicar quando as linhas de dados devem ser amostradas
  • Os dados são amostrados pelo mestre e pelo escravo na borda principal de SCK(usando a fase do relógio padrão)
  • Ambos mestre e escravo preparar para o próximo bit no arrasto borda de SCK(usando a fase do relógio padrão), alterando MISO/ MOSIse necessário
  • Depois que a transmissão terminar (possivelmente após o envio de vários bytes), SSaumente a velocidade para desativá-la

Observe que:

  • O bit mais significativo é enviado primeiro (por padrão)
  • Os dados são enviados e recebidos no mesmo instante (full duplex)

Como os dados são enviados e recebidos no mesmo pulso do relógio, não é possível que o escravo responda imediatamente ao mestre. Os protocolos SPI geralmente esperam que o mestre solicite dados em uma transmissão e obtenha uma resposta em uma transmissão subsequente.

Usando a biblioteca SPI no Arduino, fazer uma única transferência se parece com isso no código:

 byte outgoing = 0xAB;
 byte incoming = SPI.transfer (outgoing);

Código de amostra

Exemplo de envio apenas (ignorando quaisquer dados recebidos):

#include <SPI.h>

void setup (void)
  {
  digitalWrite(SS, HIGH);  // ensure SS stays high
  SPI.begin ();
  } // end of setup

void loop (void)
  {
  byte c;

  // enable Slave Select
  digitalWrite(SS, LOW);    // SS is pin 10

  // send test string
  for (const char * p = "Fab" ; c = *p; p++)
    SPI.transfer (c);

  // disable Slave Select
  digitalWrite(SS, HIGH);

  delay (100);
  } // end of loop

Fiação para SPI somente de saída

O código acima (que envia apenas) pode ser usado para conduzir um registro de deslocamento serial de saída. Como são dispositivos apenas de saída, não precisamos nos preocupar com nenhum dado recebido. No caso deles, o pino SS pode ser chamado de pino "armazenar" ou "trava".

Protocolo SPI mostrando 3 sinais

Exemplos disso são o registrador de turno de série 74HC595 e várias faixas de LED, apenas para mencionar alguns. Por exemplo, este display LED de 64 pixels acionado por um chip MAX7219:

Telão LED de 64 pixels

Nesse caso, você pode ver que o fabricante da placa usou nomes de sinal ligeiramente diferentes:

  • DIN (entrada de dados) é MOSI (saída principal, entrada secundária)
  • CS (Chip Select) é SS (Slave Select)
  • CLK (relógio) é SCK (relógio serial)

A maioria das placas segue um padrão semelhante. Às vezes, DIN é apenas DI (entrada de dados).

Aqui está outro exemplo, desta vez uma placa de LED de 7 segmentos (também baseada no chip MAX7219):

Tela LED de 7 segmentos

Isso usa exatamente os mesmos nomes de sinal que a outra placa. Nos dois casos, você pode ver que a placa precisa apenas de 5 fios, os três para SPI, além de potência e terra.


Fase e polaridade do relógio

Existem quatro maneiras de você experimentar o relógio SPI.

O protocolo SPI permite variações na polaridade dos pulsos do relógio. CPOL é a polaridade do relógio e CPHA é a fase do relógio.

  • Modo 0 (o padrão) - o relógio normalmente é baixo (CPOL = 0) e os dados são amostrados na transição de baixa para alta (borda superior) (CPHA = 0)
  • Modo 1 - o relógio é normalmente baixo (CPOL = 0) e os dados são amostrados na transição de alta para baixa (borda de fuga) (CPHA = 1)
  • Modo 2 - o relógio normalmente é alto (CPOL = 1) e os dados são amostrados na transição de alto para baixo (borda superior) (CPHA = 0)
  • Modo 3 - o relógio é normalmente alto (CPOL = 1) e os dados são amostrados na transição de baixa para alta (borda de fuga) (CPHA = 1)

Estes são ilustrados neste gráfico:

Fase e polaridade do relógio SPI

Você deve consultar a folha de dados do seu dispositivo para obter a fase e a polaridade corretas. Geralmente, haverá um diagrama que mostra como amostrar o relógio. Por exemplo, na folha de dados do chip 74HC595:

74HC595 clock

Como você pode ver, o relógio normalmente é baixo (CPOL = 0) e é amostrado na borda anterior (CPHA = 0), portanto, esse é o modo SPI 0.

Você pode alterar a polaridade do relógio e colocar o código da fase como este (escolha apenas um, é claro):

SPI.setDataMode (SPI_MODE0);
SPI.setDataMode (SPI_MODE1);
SPI.setDataMode (SPI_MODE2);
SPI.setDataMode (SPI_MODE3);

Este método foi descontinuado nas versões 1.6.0 em diante do IDE do Arduino. Para versões recentes, você altera o modo do relógio na SPI.beginTransactionchamada, assim:

SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0));  // 2 MHz clock, MSB first, mode 0

Ordem de dados

O padrão é o bit mais significativo primeiro, no entanto, você pode dizer ao hardware para processar o bit menos significativo primeiro assim:

SPI.setBitOrder (LSBFIRST);   // least significant bit first
SPI.setBitOrder (MSBFIRST);   // most significant bit first

Novamente, isso foi preterido nas versões 1.6.0 em diante do IDE do Arduino. Para versões recentes, você altera a ordem dos bits na SPI.beginTransactionchamada, assim:

SPI.beginTransaction (SPISettings (1000000, LSBFIRST, SPI_MODE2));  // 1 MHz clock, LSB first, mode 2

Rapidez

A configuração padrão para o SPI é usar a velocidade do clock do sistema dividida por quatro, ou seja, um pulso de clock do SPI a cada 250 ns, assumindo um clock da CPU de 16 MHz. Você pode alterar o divisor do relógio usando o setClockDividerseguinte:

SPI.setClockDivider (divider);

Onde "divisor" é um dos seguintes:

  • SPI_CLOCK_DIV2
  • SPI_CLOCK_DIV4
  • SPI_CLOCK_DIV8
  • SPI_CLOCK_DIV16
  • SPI_CLOCK_DIV32
  • SPI_CLOCK_DIV64
  • SPI_CLOCK_DIV128

A taxa mais rápida é "dividir por 2" ou um pulso de clock SPI a cada 125 ns, assumindo um clock de CPU de 16 MHz. Portanto, isso levaria 8 * 125 ns ou 1 µs para transmitir um byte.

Este método foi descontinuado nas versões 1.6.0 em diante do IDE do Arduino. Para versões recentes, você altera a velocidade de transferência na SPI.beginTransactionchamada, assim:

SPI.beginTransaction (SPISettings (4000000, MSBFIRST, SPI_MODE0));  // 4 MHz clock, MSB first, mode 0

No entanto, os testes empíricos mostram que é necessário ter dois pulsos de clock entre os bytes, portanto a taxa máxima na qual os bytes podem atingir o tempo limite é de 1,125 µs cada (com um divisor de clock de 2).

Para resumir, cada byte pode ser enviado a uma taxa máxima de 1 por 1.125 µs (com um relógio de 16 MHz), fornecendo uma taxa de transferência máxima teórica de 1 / 1.125 µs ou 888.888 bytes por segundo (excluindo despesas gerais, como definir SS baixo e assim em).


Conectando ao Arduino

Arduino Uno

Conexão via pinos digitais 10 a 13:

Pinos do Arduino Uno SPI

Conectando através do cabeçalho ICSP:

Pinagem do ICSP - Uno

Cabeçalho ICSP

Arduino Atmega2560

Conexão via pinos digitais 50 a 52:

Pinos Arduino Mega2560 SPI

Você também pode usar o cabeçalho ICSP, semelhante ao Uno acima.

Arduino Leonardo

O Leonardo e o Micro não expõem os pinos SPI nos pinos digitais, ao contrário do Uno e Mega. Sua única opção é usar os pinos do cabeçalho ICSP, conforme ilustrado acima para o Uno.


Escravos múltiplos

Um mestre pode se comunicar com vários escravos (no entanto, apenas um de cada vez). Isso é feito afirmando SS para um escravo e declarando-o para todos os outros. O escravo que o SS afirmou (geralmente significa LOW) configura seu pino MISO como uma saída para que o escravo, e esse escravo sozinho, possam responder ao mestre. Os outros escravos ignoram todos os pulsos de clock de entrada se SS não for afirmado. Portanto, você precisa de um sinal adicional para cada escravo, assim:

Vários escravos SPI

Neste gráfico, você pode ver que MISO, MOSI, SCK são compartilhados entre os dois escravos, mas cada escravo tem seu próprio sinal SS (seleção de escravo).


Protocolos

A especificação SPI não especifica protocolos como tal, portanto, cabe aos pares mestre / escravo individuais concordar com o que os dados significam. Embora você possa enviar e receber bytes simultaneamente, o byte recebido não pode ser uma resposta direta ao byte enviado (pois eles estão sendo montados simultaneamente).

Portanto, seria mais lógico para uma extremidade enviar uma solicitação (por exemplo, 4 pode significar "listar o diretório do disco") e depois fazer transferências (talvez apenas enviando zeros para fora) até receber uma resposta completa. A resposta pode terminar com uma nova linha ou caractere 0x00.

Leia a folha de dados do seu dispositivo escravo para ver quais seqüências de protocolos ele espera.


Como fazer um escravo SPI

O exemplo anterior mostra o Arduino como mestre, enviando dados para um dispositivo escravo. Este exemplo mostra como o Arduino pode ser um escravo.

Configuração de hardware

Conecte dois Arduino Unos juntamente com os seguintes pinos conectados um ao outro:

  • 10 (SS)
  • 11 (MOSI)
  • 12 (MISO)
  • 13 (SCK)

  • + 5v (se necessário)

  • GND (para retorno do sinal)

No Arduino Mega, os pinos são 50 (MISO), 51 (MOSI), 52 (SCK) e 53 (SS).

Em qualquer caso, o MOSI em uma extremidade está conectado ao MOSI na outra, você não os troca (ou seja, você não possui o MOSI <-> MISO). O software configura uma extremidade do MOSI (extremidade mestre) como uma saída e a outra extremidade (extremidade escrava) como uma entrada.

Exemplo mestre

#include <SPI.h>

void setup (void)
{

  digitalWrite(SS, HIGH);  // ensure SS stays high for now

  // Put SCK, MOSI, SS pins into output mode
  // also put SCK, MOSI into LOW state, and SS into HIGH state.
  // Then put SPI hardware into Master mode and turn SPI on
  SPI.begin ();

  // Slow down the master a bit
  SPI.setClockDivider(SPI_CLOCK_DIV8);

}  // end of setup


void loop (void)
{

  char c;

  // enable Slave Select
  digitalWrite(SS, LOW);    // SS is pin 10

  // send test string
  for (const char * p = "Hello, world!\n" ; c = *p; p++)
    SPI.transfer (c);

  // disable Slave Select
  digitalWrite(SS, HIGH);

  delay (1000);  // 1 seconds delay
}  // end of loop

Exemplo de escravo

#include <SPI.h>

char buf [100];
volatile byte pos;
volatile bool process_it;

void setup (void)
{
  Serial.begin (115200);   // debugging

  // turn on SPI in slave mode
  SPCR |= bit (SPE);

  // have to send on master in, *slave out*
  pinMode (MISO, OUTPUT);

  // get ready for an interrupt
  pos = 0;   // buffer empty
  process_it = false;

  // now turn on interrupts
  SPI.attachInterrupt();

}  // end of setup


// SPI interrupt routine
ISR (SPI_STC_vect)
{
byte c = SPDR;  // grab byte from SPI Data Register

  // add to buffer if room
  if (pos < sizeof buf)
    {
    buf [pos++] = c;

    // example: newline means time to process buffer
    if (c == '\n')
      process_it = true;

    }  // end of room available
}  // end of interrupt routine SPI_STC_vect

// main loop - wait for flag set in interrupt routine
void loop (void)
{
  if (process_it)
    {
    buf [pos] = 0;
    Serial.println (buf);
    pos = 0;
    process_it = false;
    }  // end of flag set

}  // end of loop

O escravo é totalmente orientado a interrupções, portanto pode fazer outras coisas. Os dados SPI recebidos são coletados em um buffer e um sinalizador é definido quando um "byte significativo" (nesse caso, uma nova linha) chega. Isso diz ao escravo para continuar e começar a processar os dados.

Exemplo de conexão do mestre ao escravo usando SPI

Mestre e escravo do Arduino SPI


Como obter uma resposta de um escravo

Seguindo o código acima, que envia dados de um mestre SPI para um escravo, o exemplo abaixo mostra o envio de dados a um escravo, fazendo com que ele faça algo com ele e retorne uma resposta.

O mestre é semelhante ao exemplo acima. No entanto, um ponto importante é que precisamos adicionar um pequeno atraso (algo como 20 microssegundos). Caso contrário, o escravo não tem chance de reagir aos dados recebidos e fazer algo com eles.

O exemplo mostra o envio de um "comando". Nesse caso, "a" (adicione algo) ou "s" (subtraia algo). Isso é para mostrar que o escravo está realmente fazendo algo com os dados.

Depois de afirmar a seleção do escravo (SS) para iniciar a transação, o mestre envia o comando, seguido por qualquer número de bytes e, em seguida, gera o SS para finalizar a transação.

Um ponto muito importante é que o escravo não pode responder a um byte recebido no mesmo momento. A resposta deve estar no próximo byte. Isso ocorre porque os bits que estão sendo enviados e os que estão sendo recebidos estão sendo enviados simultaneamente. Assim, para adicionar algo a quatro números, precisamos de cinco transferências, assim:

transferAndWait ('a');  // add command
transferAndWait (10);
a = transferAndWait (17);
b = transferAndWait (33);
c = transferAndWait (42);
d = transferAndWait (0);

Primeiro, solicitamos ação no número 10. Mas não obtemos resposta até a próxima transferência (a de 17). No entanto, "a" será definido como a resposta como 10. Finalmente, acabamos enviando um número "fictício" 0, para obter a resposta para 42.

Mestre (exemplo)

  #include <SPI.h>

  void setup (void)
    {
    Serial.begin (115200);
    Serial.println ();

    digitalWrite(SS, HIGH);  // ensure SS stays high for now
    SPI.begin ();

    // Slow down the master a bit
    SPI.setClockDivider(SPI_CLOCK_DIV8);
    }  // end of setup

  byte transferAndWait (const byte what)
    {
    byte a = SPI.transfer (what);
    delayMicroseconds (20);
    return a;
    } // end of transferAndWait

  void loop (void)
    {

    byte a, b, c, d;

    // enable Slave Select
    digitalWrite(SS, LOW);

    transferAndWait ('a');  // add command
    transferAndWait (10);
    a = transferAndWait (17);
    b = transferAndWait (33);
    c = transferAndWait (42);
    d = transferAndWait (0);

    // disable Slave Select
    digitalWrite(SS, HIGH);

    Serial.println ("Adding results:");
    Serial.println (a, DEC);
    Serial.println (b, DEC);
    Serial.println (c, DEC);
    Serial.println (d, DEC);

    // enable Slave Select
    digitalWrite(SS, LOW);

    transferAndWait ('s');  // subtract command
    transferAndWait (10);
    a = transferAndWait (17);
    b = transferAndWait (33);
    c = transferAndWait (42);
    d = transferAndWait (0);

    // disable Slave Select
    digitalWrite(SS, HIGH);

    Serial.println ("Subtracting results:");
    Serial.println (a, DEC);
    Serial.println (b, DEC);
    Serial.println (c, DEC);
    Serial.println (d, DEC);

    delay (1000);  // 1 second delay
    }  // end of loop

O código para o escravo basicamente faz quase tudo na rotina de interrupção (chamada quando os dados SPI recebidos chegam). Ele pega o byte recebido e adiciona ou subtrai conforme o "byte de comando" lembrado. Observe que a resposta será "coletada" da próxima vez no loop. É por isso que o mestre precisa enviar uma transferência final "fictícia" para obter a resposta final.

No meu exemplo, estou usando o loop principal para simplesmente detectar quando o SS fica alto e limpar o comando salvo. Dessa forma, quando o SS é reduzido novamente para a próxima transação, o primeiro byte é considerado o byte de comando.

Mais confiável, isso seria feito com uma interrupção. Ou seja, você conectaria fisicamente o SS a uma das entradas de interrupção (por exemplo, no Uno, conectaria o pino 10 (SS) ao pino 2 (uma entrada de interrupção) ou usaria uma interrupção de troca de pino no pino 10.

A interrupção pode ser usada para perceber quando o SS está sendo puxado para baixo ou alto.

Escravo (exemplo)

// what to do with incoming data
volatile byte command = 0;

void setup (void)
  {

  // have to send on master in, *slave out*
  pinMode(MISO, OUTPUT);

  // turn on SPI in slave mode
  SPCR |= _BV(SPE);

  // turn on interrupts
  SPCR |= _BV(SPIE);

  }  // end of setup


// SPI interrupt routine
ISR (SPI_STC_vect)
  {
  byte c = SPDR;

  switch (command)
    {
    // no command? then this is the command
    case 0:
      command = c;
      SPDR = 0;
      break;

    // add to incoming byte, return result
    case 'a':
      SPDR = c + 15;  // add 15
      break;

    // subtract from incoming byte, return result
    case 's':
      SPDR = c - 8;  // subtract 8
      break;

    } // end of switch

  }  // end of interrupt service routine (ISR) SPI_STC_vect

void loop (void)
  {

  // if SPI not active, clear current command
  if (digitalRead (SS) == HIGH)
    command = 0;
  }  // end of loop

Saída de exemplo

Adding results:
25
32
48
57
Subtracting results:
2
9
25
34
Adding results:
25
32
48
57
Subtracting results:
2
9
25
34

Saída do analisador lógico

Isso mostra o tempo entre o envio e o recebimento no código acima:

SPI mestre e escravo


Nova funcionalidade no IDE 1.6.0 em diante

A versão 1.6.0 do IDE mudou a maneira como o SPI funciona, até certo ponto. Você ainda precisa fazer SPI.begin() antes de usar o SPI. Isso configura o hardware SPI. No entanto, agora, quando você está prestes a começar a se comunicar com um escravo, também faz SPI.beginTransaction()para configurar o SPI (para este escravo) com o correto:

  • Velocidade do relógio
  • Ordem de bits
  • Fase e polaridade do relógio

Quando você terminar de se comunicar com o escravo, você liga SPI.endTransaction(). Por exemplo:

SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0));
digitalWrite (SS, LOW);        // assert Slave Select
byte foo = SPI.transfer (42);  // do a transfer
digitalWrite (SS, HIGH);       // de-assert Slave Select
SPI.endTransaction ();         // transaction over

Por que usar o SPI?

Eu acrescentaria uma pergunta preliminar: quando / por que você usaria o SPI? A necessidade de configuração multimestre ou um número muito grande de escravos inclinaria a escala em direção ao I2C.

Esta é uma excelente pergunta. Minhas respostas são:

  • Alguns dispositivos (alguns) suportam apenas o método de transferência SPI. Por exemplo, o registro de deslocamento de saída 74HC595, o registro de deslocamento de entrada 74HC165, o driver de LED MAX7219 e algumas faixas de LED que eu já vi. Portanto, você pode usá-lo porque o dispositivo de destino é compatível apenas com ele.
  • O SPI é realmente o método mais rápido disponível nos chips Atmega328 (e similares). A taxa mais rápida citada acima é de 888.888 bytes por segundo. Usando I 2 C, você pode obter apenas cerca de 40.000 bytes por segundo. A sobrecarga do I 2 C é bastante substancial e, se você estiver tentando fazer uma interface muito rapidamente, o SPI é a escolha preferida. Muitas famílias de chips (por exemplo, MCP23017 e MCP23S17) realmente suportam I 2 C e SPI, para que você possa escolher entre a velocidade e a capacidade de ter vários dispositivos em um único barramento.
  • Os dispositivos SPI e I 2 C são compatíveis com hardware no Atmega328, portanto é possível fazer uma transferência via SPI simultaneamente com o I 2 C, o que aumentaria a velocidade.

Ambos os métodos têm seu lugar. O I 2 C permite conectar muitos dispositivos a um único barramento (dois fios, mais terra), portanto, seria a escolha preferida se você precisasse interrogar um número substancial de dispositivos, talvez com pouca frequência. No entanto, a velocidade do SPI pode ser mais relevante para situações em que você precisa produzir rapidamente (por exemplo, uma faixa de LED) ou inserir rapidamente (por exemplo, um conversor ADC).


Referências


Você vai cobrir a estranheza que é o SPI do Due? Onde a configuração da porta SPI está vinculada ao pino SS usado e existem 4 pinos SS de hardware (IIRC) atribuídos à porta SPI?
Majenko

Outro ponto sobre a seleção: às vezes você realmente não tem escolha, porque o sensor que deseja / precisa usar está disponível apenas como I2C.
Igor Stoppa 26/09

Are you going to cover the weirdness that is the Due's SPI?- Não sei nada sobre o SPI do Due (além de presumir que o protocolo geral seja o mesmo). Você pode adicionar uma resposta que cubra esse aspecto.
Nick Gammon

Quando é o audiobook desta resposta que sai, e você vai estar lendo isso mesmo;)
AMADANON Inc.

1
@AMADANONInc. Talvez um videoclipe? Ou uma animação? Não sei se meu sotaque australiano seria compreensível. : P
Nick Gammon
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.