Conhecendo o Protocolo SPI com Arduino
Olá, tudo bem? No post de hoje iremos finalizar o nosso estudo sobre os protocolos de comunicação utilizando o Arduino. Dessa forma, hoje estaremos conhecendo o protocolo SPI com arduino. Ele, assim como o protocolo I2C já estudado, é amplamente utilizado devido o seu modo de operação Full-Duplex. Então quer saber como tudo isso funciona? Vamos nessa então!
Funcionamento do Protocolo SPI
O protocolo SPI foi desenvolvido pela Motorola e rapidamente ganhou espaço no mercado devido a sua simplicidade de utilização, precisando apenas de 3 fios + 1 para cada dispositivo interligado à ele, para que possa realizar a comunicação entre todos.
Portanto, toda essa comunicação é feita de forma serial. Analisando então, o termo SPI, do inglês Serial Peripheral Interface, conseguimos identificar isso. Dessa forma, como seu nome sugere, seu propósito é interligar periféricos a um dispositivo mestre utilizando comunicação serial. Por exemplo, podemos encontrar esse protocolo no PlayStation 2, o qual utiliza este protocolo em seus controles.
Mas como o protocolo SPI trabalha?
Acima de tudo, devemos saber que seu propósito é interligar periféricos a um dispositivo mestre. Por exemplo, essa ideia é a mesma do protocolo I2C, caso não tenha lido ainda, aconselho dar uma olhada para melhor compreensão, abaixo tem o link do post, beleza?
Então, já sabemos que ele tem seu mestre e seus escravos como no protocolo I2C. Mas sabe aqueles 3 fios + 1 para cada escravo? Como eles trabalham? Para isso, nós temos 3 principais conexões:
- MISO (Master In Slave Out) – Uso do barramento Slave para enviar dados do escravo para o mestre.
- MOSI (Master Out Slave In) – Uso do barramento Master para enviar dados do Mestre para os periféricos (escravos).
- SCLK (Serial Clock) – O relógio de pulsos que sincroniza a transmissão de dados gerada pelo mestre.
Ou seja, no MOSI temos o canal de comandos do mestre para seus escravos e só. Contudo no MISO, temos o contrário, é onde o escravo envia os dados solicitados pelo mestre. Por fim, o canal SCLK é o nosso clock, a velocidade das trocas de informações. Observe tudo isso configurado na imagem abaixo:
Em resumo, na imagem acima temos toda a ligação dos barramentos. Observe, portanto, que o MOSI do mestre é conectado no MOSI do escravo, o mesmo vale para o barramento MISO e SCLK. Contudo temos mais alguns pinos, como no exemplo acima temos 3 escravos, é necessário ter mais 3 pinos além dos 3 principais.
Analisando os Pinos SSX
Assim, temos os pinos SS1, SS2, SS3. O que eles são? Em resumo, eles são os pinos de seleção (Slave Select), o qual vai dizer qual escravo o mestre quer se comunicar naquele momento. Dessa forma, quando o pino Slave Select de um dispositivo está no nível baixo, ele se comunica com o mestre. Assim, quando ele está alto, ignora o mestre. Como resultado, isso irá permitir que você tenha vários dispositivos SPI compartilhando os mesmo barramentos de MISO, MOSI e SCLK como na imagem.
Além disso, a sua arquitetura é do tipo Full-Duplex. Ou seja, como já explicado no post do protocolo I2C, a arquitetura Full-Duplex permite que o envio/recebimento de dados sejam feitos simultaneamente. Perceba que isso é possível uma vez que os barramentos (o canal de comunicação) são independentes, ou seja, ou você envia ou recebe. Beleza?
Alguns pontos importantes
Acima de tudo, é importante saber que quando trabalhamos com o protocolo SPI, temos que analisar o dispositivo utilizado para saber alguns dados importantes dele, tais como:
- Qual é a velocidade máxima de comunicação que ele pode usar? Isso é controlado pelo primeiro parâmetro no SPISettings . Se você estiver usando um chip classificado em 15 MHz, use 15000000.
- Como é o deslocamentos de dados? Bit Mais Significativo (MSB) ou Bit Mais Significativo (LSB)? Isso é controlado pelo segundo parâmetro do SPISettings, MSBFIRST ou LSBFIRST.
- O clock de dados está inativo quando alto ou baixo? Os dados estão na borda de subida ou descida dos pulsos do relógio? Esses modos são controlados pelo terceiro parâmetro no SPISettings.
Os dois primeiros dados é fácil entender, mas vamos analisar o clock de dados e esses estados de subida e descida. Só para exemplificar, temos 4 modos de comunicação que o dispositivo pode trabalhar. O que eles fazem?
Esses modos controlam o chamado “Fase do clock” (CPHA) o qual vai definir se os dados são inseridos e/ou desativados na borda de subida ou descida do sinal do clock.
Além disso, temos também a “Polaridade do clock” (CPOL). Que raios é isso? Essa polaridade vai informar o estado que o clock vai assumir quando ele ficar ocioso. Ou seja, quando o clock estiver “a toa” ele pode assumir o estado alto ou baixo.
Os quatro modos trabalham segundo a imagem abaixo:
Modo | Polaridade do Relógio (CPOL) | Fase do Relógio (CPHA) | Borda de saída | Captura de dados |
SPI_MODE0 | 0 | 0 | Descida | Subida |
SPI_MODE1 | 0 | 1 | Subida | Descida |
SPI_MODE2 | 1 | 0 | Subida | Descida |
SPI_MODE3 | 1 | 1 | Descida | Subida |
Benefícios e Limitação do Protocolo SPI
Assim como o I2C, o protocolo SPI tem suas vantagens e desvantagens. Portanto, cabe ao usuário analisar qual está mais adequado para a sua aplicação e estar conhecendo o protocolo SPI. Dessa forma, as vantagens que temos no SPI são:
- Comunicação Full duplex;
- Flexibilidade no protocolo completo para os bits transferidos;
- Normalmente requer menor energia do que o I2C devido a menos circuitos;
- Não necessita de resistores de pull-up;
- Os escravos não precisa de um endereço único;
Assim, entrando agora nas suas desvantagens, temos alguns pontos até que interessantes:
- Requer mais pinos do CI;
- Não há reconhecimento do escravo (o mestre poder enviar a lugar nenhum e ele nem saberia disso);
- Suporta apenas um dispositivo mestre;
- Nenhum protocolo de verificação de erros é definido;
Pinagem SPI nos Arduinos
Então, igual fizemos sobre o protocolo I2C, vamos identificar quais pinos dos diferentes modelos de Arduino são utilizados para a comunicação com o protocolo SPI. Observe a tabela abaixo retirado do site do Arduino:
Placas | MOSI | MISSO | SCK | SS (escravo) | SS (mestre) | Nível |
Uno ou Duemilanove | 11 ou ICSP-4 | 12 ou ICSP-1 | 13 ou ICSP-3 | 10 | – | 5V |
Mega1280 ou Mega2560 | 51 ou ICSP-4 | 50 ou ICSP-1 | 52 ou ICSP-3 | 53 | – | 5V |
Leonardo | ICSP-4 | ICSP-1 | ICSP-3 | – | – | 5V |
Zero | ICSP-4 | ICSP-1 | ICSP-3 | – | – | 3,3V |
101 | 11 ou ICSP-4 | 12 ou ICSP-1 | 13 ou ICSP-3 | 10 | 10 | 3,3V |
MKR1000 | 8 | 10 | 9 | – | – | 3,3V |
Componentes do Projeto
Montagem do Circuito
Código Comentado
Código Mestre
<code>/* ================================================================================================== Projeto: Conhecendo o protocolo SPI com Arduino - Master Autor: Danilo Nogueira Data: 01/09/2019 Autor: Danilo Nogueira Referencias: - Debounce - https://www.arduino.cc/en/Tutorial/Debounce - SPI - https://www.arduino.cc/en/reference/SPI // ==================================================================================================*/ // --- Declaração Bibliotecas --- #include "SPI.h" int pino_Button = 4; // Pino onde conectamos o botão int pino_Slave = 10; // Pino Slave boolean estado_Button; // estado atual do botao boolean ultimo_estado = LOW; // valor da ultima leitura do botao boolean estado_led = HIGH; // valor inicial para o LED unsigned long lastDebounceTime = 0; // tempo da ultima modificacao do estado do LED unsigned long debounceDelay = 60; // tempo de debounce // ==================================================================================================*/ // --- Definindo o void setup() --- void setup() { SPI.begin(); // iniciando o barramento SPI // configura o pino do botao como entrada com resistor de pullup interno pinMode(pino_Button, INPUT_PULLUP); pinMode(pino_Slave, OUTPUT); // configura o pino seletor do slave como saida digitalWrite(pino_Slave, HIGH); // coloca o seletor como HIGH para bloquear a conexão } // ==================================================================================================*/ // --- Definindo o void loop() --- void loop() { int leitura = digitalRead(pino_Button); // Guardando o estado do botão na variável local // Quando pressionar o botão, entra na lógica do Debounce if (leitura != ultimo_estado) { // reseta o tempo do debounce lastDebounceTime = millis(); } if ((millis() - lastDebounceTime) > debounceDelay) { // se o estado do botao foi alterado: if (leitura != estado_Button) { estado_Button = leitura; // apenas altera o estado do LED se o novo estado do botao for HIGH if (estado_Button == HIGH) { estado_led = !estado_led; // coloca o pino seletor do slave em nivel baixo iniciando a transmissao digitalWrite(pino_Slave, LOW); SPI.transfer(estado_led); // envia um byte contendo o estado do LED // coloca o pino seletor do slave em nivel alto finalizando a conexão digitalWrite(pino_Slave, HIGH); } } } // salva a leitura. No proximo laço este sera o ultimo // estado do botao (ultimo_estado) ultimo_estado = leitura; }</code>
Código Slave
<code>/* ================================================================================================== Projeto: Conhecendo o protocolo SPI com Arduino - Slave Autor: Danilo Nogueira Data: 01/09/2019 Autor: Danilo Nogueira Referencias: - Debounce - https://www.arduino.cc/en/Tutorial/Debounce - SPI - https://www.arduino.cc/en/reference/SPI // ==================================================================================================*/ // --- Declaração Bibliotecas --- #include "SPI.h" int pino_led = 4; // Pino onde o led está conectado char dados; // Variável p/ armazenar os dados recebidos volatile boolean flag_recebimento; // flag de recebimento de dado // ================================================================================================== // --- Definindo o void setup() --- void setup() { // inicia a SPI no modo escravo SPCR |= bit (SPE); // configura o pino MISO como saida pinMode(MISO, OUTPUT); // prepara o flag de recebimento para interrupcao flag_recebimento = false; // liga as interrupcoes SPI.attachInterrupt(); pinMode(pino_led, OUTPUT); // configura o pino do LED como saida } // ==================================================================================================*/ // --- Definindo o void loop() --- void loop() { // Caso a flag de recebimento seja verdadeira, então if (flag_recebimento){ // se o byte recebido for igual a 0, apaga o LED if (dados == 0) { digitalWrite(pino_led, LOW); } // se o byte recebido for igual a 1 acende o LED if (dados == 1) { digitalWrite(pino_led, HIGH); } // limpa o flag de recebimento flag_recebimento = false; } } // Rotina de interrupcao do SPI ISR (SPI_STC_vect) { // le e salva o byte do Registrador de dados da SPI dados = SPDR; // seta o flag de recebimento para que o dado recebido // seja processado no proximo loop flag_recebimento = true; }</code>
Explicando o Código
Código Mestre
Iniciamos o código declarando a nossa biblioteca “SPI” e as variáveis definidas para utilizarmos. Em seguida, dentro do void setup temos a inicialização do barramento SPI e colocando o pino de Slave como “HIGH” para no começo, a comunicação esteja fechada.
<code>void setup() { SPI.begin(); // iniciando o barramento SPI // configura o pino do botao como entrada com resistor de pullup interno pinMode(pino_Button, INPUT_PULLUP); pinMode(pino_Slave, OUTPUT); // configura o pino seletor do slave como saida digitalWrite(pino_Slave, HIGH); // coloca o seletor como HIGH para bloquear a conexão }</code>
Agora, entrando no void loop(), temos a leitura do estado do sensor de som, verificando o seu estado. Ainda não conhece este sensor? Clique no botão abaixo e confere o post onde explicamos sobre ele.
Depois de alterar o estado, opino de slave é ativado (colocando ele em “LOW”), a transferência do estado do botão é realizada e por fim, o pino slave novamente é desligado.
<code>if (estado_Button == HIGH) { estado_led = !estado_led; // coloca o pino seletor do slave em nivel baixo iniciando a transmissao digitalWrite(pino_Slave, LOW); SPI.transfer(estado_led); // envia um byte contendo o estado do LED // coloca o pino seletor do slave em nivel alto finalizando a conexão digitalWrite(pino_Slave, HIGH); }</code>
Código Slave
Em seguida, iniciamos o código declarando nossa biblioteca além de criar as variáveis que utilizaremos. Contudo, criamos uma variável do tipo “volatile boolean” para trabalhar com o estado recebido do mestre e tratar ele no código.
<code>#include "SPI.h" int pino_led = 4; // Pino onde o led está conectado char dados; // Variável p/ armazenar os dados recebidos volatile boolean flag_recebimento; // flag de recebimento de dado</code>
Então, em nosso void setup(), temos a linha de comandos “SPCR |= bit (SPE)” a qual serve para inicializar nosso escravo no barramento. Ou seja, estamos dizendo “inicia a comunicação SPI mas saiba que eu sou o escravo”.
Logo depois, colocamos o pino MISO como saída para poder responder os comandos do mestre e inicializamos as interrupções da comunicação.
<code>void setup() { // inicia a SPI no modo escravo SPCR |= bit (SPE); // configura o pino MISO como saida pinMode(MISO, OUTPUT); // prepara o flag de recebimento para interrupcao flag_recebimento = false; // liga as interrupcoes SPI.attachInterrupt(); pinMode(pino_led, OUTPUT); // configura o pino do LED como saida }</code>
Por fim, no void loop(), iniciamos com uma condição a qual vai verificar o estado da nossa variável que recebe o dado enviado pelo mestre. Nisso, quando a variável é chamada, o código entra na função criada no final para armazenar os dados enviados.
Em seguida, colocamos a variável “flag_recebimento” como HIGH e entramos no if, o qual vai comparar o valor armazenado na variável “dados” e realizar a ação de apagar ou acender o led.
<code>ISR (SPI_STC_vect) { // le e salva o byte do Registrador de dados da SPI dados = SPDR; // seta o flag de recebimento para que o dado recebido // seja processado no proximo loop flag_recebimento = true; }</code>
Resultado do Projeto
Por fim, confira o resultado do projeto em nosso canal do Youtube!
Mas e ai? Gostou do post? Caso tenha ficado algo incompleto para você, comente abaixo 📝
Dicas? Dúvidas? 🤔 Críticas? 😱 Só comentar abaixo! 👇
Forte abraço e compartilha se gostou! 🤘
Olá Danilo Nogueira, parabéns pelo seu trabalho. Gostaria de tirar uma dúvida com você, se for possivel. Então a dúvida é, eu tenho 3 slaves como escolher os pinos de selecao “CS” no ARDUINO UNO uma vez que so tenho um pino no ARDUINO como “CS” e preciso de mais dois
Obrigada
CP
Jose Soares
Olá José, primeiramente fico muito feliz pelo seu feedback e sobre a sua dúvida, ela é bem importante!
Nesse caso, você pode utilizar um simples multiplexador (74HC151) por volta de R$ 5,00. A partir disso você consegue colocar até 8 slave na porta do Arduino e com os seletores você escolhe qual você quer receber informações de resposta beleza?
Qualquer outra dúvida pode mandar direto no e-mail: d169692@dac.unicamp.br que respondo mais rápido!
Grande abraço e bons projetos!
Ola, Tudor bem?
Estou com um projeto envonvendo 2 arduinos uno em que o primeiro tem que passa para o segundo dois valores de temperatura distintos via comunicação SPI, porém nao sei como tratar no arduino slave a segregacao destas informacoes para que eu possa dar tratativas diferentes. Exemplo A temperatura recebida na variavel Temp1 vou utilizar para mostrar em um display e a temperatura recebida na variavel Temp2 utilizo para mostrar em outro display.
Vi em alguns posts logica que envolve estrutura de dados, mas achei bem confuso. Consegue me ajudar ou indicado algum material de estudo ou projeto sober isso?
Olá Vitor, tudo bem?
Então vamos lá, uma dúvida:
– Você está recebendo as duas informações? Consegue identificar quais são cada uma?
Aguardo o seu retorno,
Abraço!
Olá Danilo, estou bem graças a Deus. Espero que esteja bem também!!
Exemplo resumido para entender:
…
Código do Arduíno uno Master:
#include “SPI.h” // Inclusão da bilioteca de comunicação via SPI
#define ssPin 10 // pino seletor do slave
float Temp1 = 28; // Fixei a temperatura só para exemplo, mas na real será lida do sensor 1 de temperatura
float Temp2 = 30; // Fixei a temperatura só para exemplo, mas na real será lida do sensor 2 de temperatura
void setup() {
SPI.begin(); // ingressa ao barramento SPI
pinMode(ssPin, OUTPUT); // configura o pino seletor do slave como saida
digitalWrite(ssPin, HIGH); // coloca o pino seletor do slave em nivel alto
}
void loop() {
delay(2000);
digitalWrite(ssPin, LOW); // coloca o pino seletor do slave em nivel baixo iniciando a transmissao
SPI.transfer (Temp1); // envia o contendo da variável Temp1 (28).
digitalWrite(ssPin, HIGH); // coloca o pino seletor do slave em nivel alto finalizando a transmissão
delay(2000);
digitalWrite(ssPin, LOW); // coloca o pino seletor do slave em nivel baixo iniciando a transmissao
SPI.transfer (Temp2); // envia o contendo da variável Temp1 (30).
digitalWrite(ssPin, HIGH); // coloca o pino seletor do slave em nivel alto finalizando a transmissão
}
…
Código do Arduíno Uno Slave:
LiquidCrystal_I2C lcd(0x27,20,4);
include “SPI.h” // Inclusão da bilioteca de comunicação via SPI
volatile boolean received_flag; // flag de recebimento de dado da comunicação SPI
float Temp1; // Variável para armazenar a temperatura 1 lida na arduíno master
float Temp2; // Variável para armazenar a temperatura 2 lida no arduino master
void setup() {
lcd.init(); //Inicia o módulo do LCD
lcd.backlight(); // Liga a luz de fundo do LCD
SPI.attachInterrupt(); // liga as interrupcoes da comunicação SPI
SPCR |= bit (SPE); // inicia a SPI no modo escravo
pinMode(MISO, OUTPUT); // configura o pino MISO como saida
void loop() {
if (received_flag){
lcd.setCursor(0,0); //Posiciona o cursor para imprimir o texto da temperatura 1
lcd.print(“Temperatura 1: “); // Escreve Temperatura 1 na tela do LCD
lcd.setCursor(16,0);
lcd.print(Temp1) // Imprime o valor da variável global Temp1 no LCD que será recebida do master
lcd.setCursor(0,1); //Posiciona o cursor para imprimir o texto da temperatura 1
lcd.print(“Temperatura 2: “); // Escreve Temperatura 2: na tela do LCD na linha abaixo do Temperatura1:
lcd.setCursor(16,1);
lcd.print(Temp1) // Imprime o valor da variável global Temp2 no LCD que será recebida do master
}
ISR (SPI_STC_vect) {
Temp1 = SPDR; // Atribui o valor de SPDR recebido pelo arduíno master na variável global Temp1
Temp2 = SPDR; // Atribui o valor de SPDR recebido pelo arduíno master na variável global Temp2
received_flag = true;
}
}
Quando o master envia o valor da variável Temp1 na primeira comunicação SPI, a interrupção ISR (SPI_STC_vect) do Slave faz com que as variáveis Temp1 e Temp2 fiquem com o mesmo valor (ambas com o valor da Temp1), logo no display aparecerá, por exemplo:
Temperatura 1: 28
Temperatura 2: 28
Desta mesma forma, quando o master executa a segunda comunicação SPI enviando o valor da variável Temp2, ambas também ficam com o mesmo valor, por exemplo:
Temperatura 1: 30
Temperatura 2: 30
Meu objetivo é fazer com que o Slave consiga separar o conteúdo das variáveis que são enviadas pelo master. Exemplo: O master executa a primeira comunicação SPI e envia para o Slave o valor da variável Temp1 (exemplo 28) que por sua vez armazena este valor na variável global Temp1 criada no Slave.
Após o delay de 2 segundos, o master executa a segunda comunicação SPI e enviar ao Slave o valor da variável Temp2 (exemplo 30) que grava na variável global Temp2 criada no Slave.
Desta forma a ideia é que apareça no LCD as duas temperaturas segregadas. exemplo:
Temperatura 1: 28
Temperatura 2: 30
Na prática, imagina que no Master terá dois sensores de temperatura e as variações das leituras serão enviadas para o Slave que mostrará no LCD.
Abraço e desde já obrigado pela atenção.
Olá Vitor, estou me cuidando bem também!
Bom, entendendo um pouco melhor agora, você já pensou em trabalhar esses dados como se fossem dados enviados através de uma serial?
Isso porque a comunicação aqui está sendo feita serialmente. Já tentou transformar o dado enviado em uma string e trabalhar com ela para separar as informações?
Você pode por exemplo pegar os dois dados, transformar em uma só string e enviar apenas ela. Quando o Arduino Slave receber, analisa o dado até uma (,) e o que estava antes vai para Temp1 e o restante para Temp2
Caso não tenha ficado claro a ideia, me envia um e-mail para d169692@dac.unicamp.br e podemos marcar uma call para fazermos juntos!
Abraços e se cuide bem!