SPI e I2C

Numa outra página já foi apresentado um protocolo de comunicação série, o UART. Nesta página vamos apresentar dois outros protocolos muito usados na interligação de microcontroladores com outros dispositivos nomeadamente sensores, ADCs e DACs, memórias, LCDs, etc. Esses dois protocolos são os seguintes:

  • SPI – Serial Peripheral Interface
  • I2C – Inter-Integrated Circuit

Uma das novidades destes dois protocolos relativamente ao UART é serem síncronos, ou seja um dos sinais envolvidos é um sinal de relógio. Desta forma é possível usar transferências mais rápidas. Por exemplo no protocolo SPI podem-se usar baudrates de 1000000bit/s ou mesmo mais.

Estes protocolos usam uma estratégia master-slave, onde o microcontrolador toma o papel de master, que ao gerar o sinal de relógio comanda a troca de dados com os outros dispositivos (os slaves).

SPI

No protocolo SPI (Serial Peripheral Interface), quer o master quer o slave possuem internamente um registo de deslocamento que serão ligados entre si por duas linhas de dados:

  • MOSI – Master Output Slave Input
  • MISO – Master Input Slave Output

400px-SPI_8-bit_circular_transfer.svg

Os bits são deslocados ao ritmo do sinal de relógio (SCLK) gerado pelo master.

Para se poder ligar diferentes slaves, o master controla o valor presente em determinadas linhas de seleção (designadas na figura seguinte por SSx). O slave que tiver a sua linha ativa, saberá que os dados na linha MOSI são para si, e/ou que pode gerar dados na linha MISO. Deste modo não há qualquer conflito ao nível das linhas MISO pois só uma estará ativa (as restantes estarão em “alta impedância”).

350px-SPI_three_slaves.svg

No caso da placa ChipKIT UNO32, os pinos da interface necessários à implementação do protocolo SPI (SCLK, MISO, MOSI e SS) são respetivamente o 13, 12, 11 e 10. De facto este último pode ser qualquer pino configurado como saída, já que ao contrário dos primeiros 3 que são geridos pelo hardware, o SS será controlado diretamente pelo programa!

chipkituno32_pinout

De notar que os jumpers JP5 e JP7 deverão estar na posição da figura pois só assim o microcontrolador assumirá o papel de master.

I2C

O protocolo I2C (Inter-Integrated Circuit) é implementado apenas com duas linhas:

  • SDA – Serial DAta
  • SCL – Serial CLock

A existência duma só linha de dados impede que tal como ocorre no protocolo SPI a comunicação seja full-duplex, ou seja que se possa realizar em simultâneo o envio e a receção de dados.

Como a figura seguinte mostra, todos os dispositivos ficam “pendurados” nesse barramento. Não existem quaisquer linhas de seleção pelo que é agora outro o mecanismo de escolha do slave com que o master pretende comunicar. Isso é feito na forma dum endereçamento por software, i.e. cada slave terá um endereço de 7 bits a ser usado quando do estabelecimento da comunicação.

I2C

De notar que todas as ligações ao barramento são realizadas por saídas em dreno-aberto. Caso os transístores estejam ao corte as linhas são mantidas a 3.3V (+Vdd) em virtude das resistências de pull-up. Quando um dos transístores conduz, a respetiva linha é “puxada” para os 0V (GND). Dessa forma nunca existirão curto-circuitos nas linhas mesmo que hajam acessos simultâneos.

No caso da placa ChipKIT UNO32, os pinos da interface necessários à implementação do protocolo I2C (SDA e SCL) são respetivamente os pinos A4 e A5. De notar que para tal esses pinos não poderão ser usados como entradas analógicas e portanto os jumpers JP6 e JP8 deverão estar na posição contrária à indicada na seguinte imagem:

chipkituno32_pinout

Projeto digipots

Neste projeto vamos ligar o PIC32 com dois potenciómetros digitais cujos interfaces são precisamente SPI e I2C. Esses circuitos integrados (e respetivas datasheets) são os seguintes:

  • MCP42010 – Duplo potenciómetro digital de 10kΩ com interface SPI
  • AD5241 – Potenciómetro digital de 10kΩ com interface I2C

Qualquer um destes potenciómetros apresentam uma resistência de 10kΩ entre os pontos A e B sendo que a posição dos seus pontos médios W podem ser programados numa de 256 posições possíveis enviando através do barramento SPI ou I2C um valor de 8 bits (entre 0 e 255).

De notar que, para um mais fácil teste, implementou-se em cada potenciómetro um divisor de tensão (ligando os seus pontos A e B a 3V3 e GND respetivamente) sendo os seus pontos médios ligados às entradas analógicas A0 e A1 do PIC32. Desta forma os potenciómetros digitais transformam-se numa espécie de DACs (Digital to Analog Converters).

O circuito resultante é pois o seguinte (não esquecer as resistências de pull-up nas linhas I2C com por exemplo 2k2):

digipots

Consultados os “pinouts” nas respetivas datasheets, foi possível definir as seguintes ligações:

digipots

Neste programa são implementados os seguintes 4 comandos a serem enviados pelo monitor série:

  • shh – posiciona o potenciómetro 1 do MCP42010 (via SPI) na posição hexadecimal hh
  • whh – posiciona o potenciómetro do AD5241 (via I2C) na posição hexadecimal hh
  • w – lê a posição hexadecimal do potenciómetro AD5241
  • ax – lê o valor analógico (de 0 a FF) no canal Ax
  • i – localiza os endereços (de 7 bits) dos dispositivos ligados ao barramento I2C

O código fonte do programa é o seguinte:

#include "Arduino.h"

#include "SPI.h"
#define SS 10

#include <Wire.h>
#define ADDR 0b0101100

#define BUFSIZE 10
char cmd[BUFSIZE];
int pos = 0;

uint8_t hex2byte(char c) {
    return ((c < 'A') ? (c - '0') : ((c & ~0x20) - 'A' + 10));
}

void setup() {
  Serial.begin(115200);
  Serial.println("Project digipots");

  pinMode(SS, OUTPUT);
  digitalWrite(SS, HIGH);
  Wire.begin();
}

void loop() {
  if (Serial.available()) {
    char ch = Serial.read();
    Serial.write(ch);
    if (ch == '\r') {
      Serial.write('\n');
      switch (cmd[0]) {
        case 's':
          uint16_t v;
          v = (0b00010010 << 8) + (hex2byte(cmd[1]) << 4) + hex2byte(cmd[2]);
          SPI.beginTransaction (SPISettings (2000000, MSBFIRST, SPI_MODE0));
          digitalWrite (SS, LOW);
          SPI.transfer16(v);
          digitalWrite (SS, HIGH);
          SPI.endTransaction ();
          break;
        case 'w':
          if (pos == 1) {
            Wire.requestFrom(ADDR, 1);
            if (Wire.available())
              Serial.println(Wire.read(), HEX);
          }
          else {
            Wire.beginTransmission(ADDR);
            Wire.write(byte(0x00));
            Wire.write((hex2byte(cmd[1]) << 4) + hex2byte(cmd[2]));
            Wire.endTransmission();
          }
          break;
        case 'i':
          int a;
          for(a = 8; a < 120; a++) {
            Wire.beginTransmission(a);
            if (Wire.endTransmission() == 0)
              Serial.println(a, HEX);
            delay(1);
          }
          break;
        case 'a':
          Serial.println(analogRead(hex2byte(cmd[1]))>>2, HEX);
          break;
      }
      pos = 0;
    } else
      if ((pos < BUFSIZE) && (ch >= ' '))
        cmd[pos++] = ch;
  }
}

SPI

O uso do barramento SPI exige a inclusão da biblioteca SPI.h, e começa com a função beginTransaction() onde se especificam as configurações através da função SPISettings(). Essas configurações incluem:

  • baudrate – 2000000 bits por segundo neste caso
  • primeiro bit – o mais significativo (MSBFIRST) ou menos significativo (LSBFIRST)
  • modo – um de 4 modos possíveis que o sinal SCLK pode apresentar

Este modo depende, se em repouso o sinal de relógio SCLK está no valor lógico 0 ou 1, e se os dados são lidos no flanco ascendente ou descendente desse relógio. Daqui resultam os 4 modos ilustrados na seguinte figura:

spi-modes

Configurado o SPI, segue-se a ativação da linha SS (em geral ao nível lógico 0) e depois a realização da transferência de dados através das funções transfer() ou transfer16() como no presente exemplo onde em lugar de enviarmos um byte enviamos um valor de 16 bits como requerido pela datasheet do MCP42010 (ver imagem seguinte). Terminado o processo há que voltar a desativar a linha de seleção SS e terminar o acesso ao barramento SPI com a função endTransaction().

Todo este processo e respetivas funções da biblioteca Arduino, podem ser revistas na seguinte página: http://www.gammon.com.au/spi

Relativamente aos valores a enviar para definição do potenciómetro presente no MCP42010, tudo está explicado na sua datasheet, resumida na seguinte figura:

spi_waves

A seguinte imagem mostra os sinais envolvidos no barramento SPI quando dum comando s13.

spi_wr

É possível ver o envio dos referidos 16 bits, formados por um byte com o comando (0x12 – escrita no potenciómetro 1), seguido dum byte com o dado propriamente dito (0x13 neste exemplo).

No caso do MCP42010, não existe sinal MISO (o pino SO destina-se apenas a ligar vários circuitos integrados semelhantes numa cadeia tipo daisy chain), não sendo pois realizadas quaisquer leituras. Quando elas existem a função a usar é igualmente a transfer() sendo o valor retornado o byte recebido do slave.

I2C

O uso do barramento I2C exige a inclusão da biblioteca Wire.h e da sua inicialização através da função Wire.begin().

Escrita

A realização duma escrita começa com a função beginTransmission() cujo parâmtero é o endereço de 7 bits do dispositivo (no caso do AD5241 é o 0b0101100 = 0x2C). Segue-se o envio dos bytes a enviar através da função write() (neste exemplo, e tal como pode ser verificado na seguinte imagem, são enviados 2 bytes). Todo o processo de envio é desencadeado com a função endTransmission(), que retorna um eventual código de erro ou 0 se tudo correr sem problemas.

Leitura

A realização duma leitura faz-se com a função requestFrom() onde para além do endereço de 7 bits indica-se também o número de bytes a ler. Seguem-se as leituras através da função read(), isto caso tenham chegado eventuais bytes (algo que pode ser comprovado através da função available().

Todo este processo e respetivas funções da biblioteca Arduino, podem ser revistas na seguinte página: http://www.gammon.com.au/i2c

Relativamente aos valores a enviar para definição do potenciómetro presente no AD5241, tudo está explicado na sua datasheet, resumida na seguinte figura:

i2c_waves

As seguinte imagens mostram os sinais envolvidos no barramento I2C quando dum comando w13 e w, respetivamente uma escrita do valor 13h e a sua leitura:

i2c_wr

i2c_rd

Como as imagens mostram, em ambos os processos (escrita e leitura) é enviado um primeiro byte com o endereço do dispositivo. Esse byte é formado juntando ao endereço de 7 bits, um 0 ou um 1, no caso duma escrita ou de uma leitura respetivamente. Assim o endereço de 7 bits 0x2C transforma-se respetivamente nos endereços de 8 bits: 0x58 ou 0x59.

De notar que este dispositivo admite 3 outros endereços de 7 bits, num total de 4 endereços portanto. Tudo depende da forma como os pinos AD1 e AD0 estão ligados. Desta forma é possível coexistir no mesmo circuito até 4 dispositivos iguais.

ToDo 1

Adicione ao programa um novo comando (ga) capaz de gerar no ponto médio dum dos potenciómetros digitais à sua escolha, um sinal sinusoidal de 1Hz com valor médio 1V e amplitude a/10V.

Use uma frequência de amostragem de 100Hz (recorrendo por exemplo à biblioteca Timer).

ToDo 2

Pretende-se desenvolver um projeto capaz de ligar o nosso PIC32 a um RTC (Real Time Clock) DS3231 presente na placa ZS-042 da imagem e que possui um barramento I2C.

zs-042

Tendo por base o projeto butledserial, implemente os seguintes comandos:

  • chhmmss – acerta o RTC para a hora hh:mm:ss
  • c – lê a hora do RTC e imprime-a no formato hh:mm:ss