Com referência às placas Arduino Uno, Mega2560, Leonardo e similares:
- Como o SPI funciona?
- Qual a velocidade do SPI?
- Como eu me conecto entre um mestre e um escravo?
- Como eu faço um escravo SPI?
Observação: isso é uma pergunta de referência.
Com referência às placas Arduino Uno, Mega2560, Leonardo e similares:
Observação: isso é uma pergunta de referência.
Respostas:
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.
Em um sistema SPI completo, você terá quatro linhas de sinal:
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:
Observe que três sinais são saídas do mestre (MOSI, SCK, SS) e um é uma entrada (MISO).
A sequência de eventos é:
SS
vai baixo para afirmar e ativar o escravoSCK
linha alterna para indicar quando as linhas de dados devem ser amostradasSCK
(usando a fase do relógio padrão)SCK
(usando a fase do relógio padrão), alterando MISO
/ MOSI
se necessárioSS
aumente a velocidade para desativá-laObserve que:
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);
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
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".
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:
Nesse caso, você pode ver que o fabricante da placa usou nomes de sinal ligeiramente diferentes:
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):
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.
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.
Estes são ilustrados neste gráfico:
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:
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.beginTransaction
chamada, assim:
SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0)); // 2 MHz clock, MSB first, mode 0
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.beginTransaction
chamada, assim:
SPI.beginTransaction (SPISettings (1000000, LSBFIRST, SPI_MODE2)); // 1 MHz clock, LSB first, mode 2
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 setClockDivider
seguinte:
SPI.setClockDivider (divider);
Onde "divisor" é um dos seguintes:
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.beginTransaction
chamada, 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).
Conexão via pinos digitais 10 a 13:
Conectando através do cabeçalho ICSP:
Conexão via pinos digitais 50 a 52:
Você também pode usar o cabeçalho ICSP, semelhante ao Uno acima.
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.
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:
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).
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.
O exemplo anterior mostra o Arduino como mestre, enviando dados para um dispositivo escravo. Este exemplo mostra como o Arduino pode ser um escravo.
Conecte dois Arduino Unos juntamente com os seguintes pinos conectados um ao outro:
13 (SCK)
+ 5v (se necessário)
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.
#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
#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.
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.
#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.
// 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
Adding results:
25
32
48
57
Subtracting results:
2
9
25
34
Adding results:
25
32
48
57
Subtracting results:
2
9
25
34
Isso mostra o tempo entre o envio e o recebimento no código acima:
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:
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
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:
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).
Minha página sobre o SPI - também possui detalhes sobre o SPI com interrupção de bits e o uso do USART para obter um segundo SPI de hardware no chip Atmega328.
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.