Neste post vamos conhecer um pouco sobre a utilização da I2C com a Raspberry Pi Pico. A interface I2C é uma forma popular de interligar componentes e módulos a um microcontrolador. Para testar sua funcionalidade vamos utilizá-la junto com a Raspberry Pi Pico e ver exemplos de uso em MicroPython e C.
A Interface I2C
A interface I2C é uma interface serial, os bits de dados são enviados sequencialmente um a um. Para isso são usados dois sinais, um com os dados (SDA) e um com clock (SCL, que indica onde está cada bit no sinal de dados).
O sinal de clock é sempre gerado pelo “mestre”, que é quem inicia as comunicações. O sinal de dados pode ser acionado tanto pelo “mestre” como pelo dispositivo com quem ele está se comunicando.
É possível conectar um mestre a vários dispositivos em paralelo, em uma topologia de “varal”. Para isso, cada dispositivo tem um endereço de 7 bits.
Em uma situação de “repouso” os sinais de dados e clock estão em nível alto. Normalmente o sinal de dados só é alterado quando o sinal de clock está no nível baixo. Isto é violado para o mestre indicar o início e o fim de uma comunicação (“start” e “stop”). Após o start, o mestre envia o endereço do dispositivo mais um bit que indica se será feita uma leitura (1) ou escrita (0). Os bytes trocados em seguida dependem do dispositivo. Após a transferência de cada byte o receptor deve indicar a aceitação enviando um bit (ACK ou NAK).
Esta descrição trata somente dos aspectos mais comuns do I2C, o datasheet do RP2040 (que você pode ver no datasheet da RP2040) traz uma descrição bem mais detalhada.
Suporte ao I2C na Raspberry Pi Pico
O microprocessador RP2040, usado na Raspberry Pi Pico, possui duas interfaces I2C idênticas (I2C0 e I2C1). Elas são capazes de gerar clock, start e stop (no modo master), possuem filas de dezesseis posições para entrada e saída e suportam o uso de DMA.. No modo dispositivo (“slave”) as interfaces são capazes de conferir os endereços e gerar uma interrupção quando for recebida uma comunicação do mestre.
Os pinos do RP2040 usados para comunicação I2C são configuráveis, a tabela abaixo mostra as opções disponíveis:
Material Necessário para os Exemplos
- Raspberry Pi Pico
- Protoboard
- Real Time Clock RTC DS1307 (Tiny RTC)
- Conversor de Nível Lógico I2C 3,3-5V Bidirecional
- Fios para interconexão
O módulo “Tiny RTC” possui dois dispositivos I2C: um relógio DS1307 e uma memória EEPROM 24C32. O DS1307 possui registradores onde são armazenadas data e hora, uma bateria no módulo mantém estas informações atualizadas mesmo quando o módulo não recebe alimentação externa. A memória 24C32 tem 4K bytes de memória não volátil.
Os nossos exemplos serão simples: através da serial será possível:
- Acertar o relógio, através de uma sequência “AddmmaaHHMMSS” onde ddmmaa é a data e HHMMSS é a hora
- Ler o relógio, através do comando “L”
- Gravar um valor na EEPROM, através da sequência “Geeeedd” onde eeee é o endereço e dd é o dado (ambos em hexadecimal)
- Recuperar um valor da EEPROM, através da sequência “Reeee” onde eeee é o endereço em hexadecimal
Montagem
O Tiny RTC é alimentado por 5V enquanto que o Raspberry Pi trabalha a 3,3V. Devido a forma como os sinais de I2C são acionados, a conexão direta entre dispositivos de 3,3 e 5V irá obrigar o dispositivo de 3,3 a trabalhar fora das suas especificações. Na maioria dos casos isso não causa danos (você vai achar muitos exemplos disso, inclusive meus), mas aqui vamos ser cuidadosos e usar um conversor de nível.
A figura abaixo mostra a montagem usada:
Atenção para a marcação A/B do conversor! A parte de baixo da protoboard trabalha a 5V e a parte de cima a 3,3V.
Exemplo de Uso de I2C na Raspberry Pi Pico com MicroPython
Para usar o MicroPython é necessário primeiro carregá-lo na Raspberry Pi Pico, como descrito neste artigo da Rosana Guse. Para carregar e rodar o programa MicroPython usei a IDE Thonny.
O suporte ao I2C no MicroPython é feito pela classe I2C do módulo machine. No construtor nós especificamos qual periféricos usar (0 ou 1), os pinos para SDA e SCL e a frequência do clock:
i2c = I2C(0, scl=Pin(21), sda=Pin(20), freq=100000)
Um método útil é o scan(), que procura os dispositivos ligados ao barramento I2C. Isto é feito tentando os endereços de 0x08 a 0x77 e vendo se tem um ACK em resposta.
Podemos agrupar os demais métodos da classe I2C em três grupos:
- Operações primitivas: permitem gerar um start ou stop e enviar ou receber bytes. Estes métodos são usados principalmente quando temos um dispositivo que opera de uma forma não convencional. Os métodos são start(), stop(), readinto() e write().
- Operações padrão: estes métodos enviam o start e o endereço, transferem um conjunto de bytes e depois enviam o stop. A geração do stop pode ser suprimida, para o caso de dispositivos que exigem que duas operações consecutivas sejam feitas sem stop entre elas. Com estes métodos conseguimos comunicar com a maioria dos dispositivos I2C. Os métodos são readfrom(), readfrom_into(), writeto() e writevto().
- Operações orientadas à memória: estes métodos são especializados para o caso em que o dispositivo tem uma organização de memória ou registradores. Neles é enviado primeiro o start, o endereço I2C do dispositivo (selecionando escrita), o endereço da memória (ou número do registrador). Se a operação for escrita, os bytes de dados são enviados em seguida. Se a operação for leitura, o endereço I2C do dispositivo é enviado novamente (desta vez selecionando leitura) e os bytes de dados são lidos. Nos dois casos um stop é enviado ao final. Estes métodos (quando apropriados para o dispositivo) simplificam e compactam a programação. Os métodos são readfrom_mem(), readfrom_mem_into() e writeto_mem().
Os dois dispositivos que vamos usar podem ser acessados pelas operações orientadas à memória, tomando o cuidado que o relógio utiliza um endereço de 8 bits e a EEProm de 16 bits. O código fica assim:
from machine import Pin from machine import I2C from time import sleep # Endereço dos nossos dispositivos ADDR_DS1307 = 0x68 ADDR_EEPROM = 0x50 # Iniciação i2c = I2C(0, scl=Pin(21), sda=Pin(20), freq=100000) print ('Verificando dispositivos conectados...') disp = i2c.scan() print ('Dispositivos encontrados:') for d in disp: print (' 0x%02X' % d) if not (ADDR_DS1307 in disp or ADDR_EEPROM in disp): print ('*** Confira as conexões!!! ***') print() # Converte de BCD para inteiro def bcd2num(x): return ((x >> 4)*10) + (x & 0x0F) # Converte de inteiro para BDC def num2bcd(x): return ((x // 10)<<4) + (x % 10) # Rotinas que implementam os vários comandos def leRelogio(cmd): regs = i2c.readfrom_mem(ADDR_DS1307, 0, 7) segundo = bcd2num(regs[0] & 0x7F) minuto = bcd2num(regs[1]) hora = bcd2num(regs[2] & 0x3F) dia = bcd2num(regs[4]) mes = bcd2num(regs[5]) ano = bcd2num(regs[6]) print ('Data: %02d/%02d/%02d Hora: %02d:%02d:%02d' % (dia, mes, ano, hora, minuto, segundo)) def acertaRelogio(cmd): # AddmmaaHHMMSS if not len(cmd) == 13: return dia = int(cmd[1:3]) mes = int(cmd[3:5]) ano = int(cmd[5:7]) hora = int(cmd[7:9]) minuto = int(cmd[9:11]) segundo = int(cmd[11:13]) regs = bytearray(7) regs[0] = num2bcd(segundo) regs[1] = num2bcd(minuto) regs[2] = num2bcd(hora) + 0x40 regs[3] = 1 regs[4] = num2bcd(dia) regs[5] = num2bcd(mes) regs[6] = num2bcd(ano) i2c.writeto_mem(ADDR_DS1307, 0, regs) leRelogio('') def leEEProm(cmd): # Reeee if not len(cmd) == 5: return endereco = int(cmd[1:5], 16) dado = i2c.readfrom_mem(ADDR_EEPROM, endereco, 1, addrsize=16) print ('EEProm[%04X] = %02X' % (endereco, dado[0])) return def gravaEEProm(cmd): # Geeeedd if not len(cmd) == 7: return endereco = int(cmd[1:5], 16) dado = bytearray(1) dado[0] = int(cmd[5:7], 16) i2c.writeto_mem(ADDR_EEPROM, endereco, dado, addrsize=16) sleep(0.01) # tempo para gravar leEEProm ('R'+cmd[1:5]) return # Dicionario para decodificar os comandos dictCmds = { 'A': acertaRelogio, 'L': leRelogio, 'G': gravaEEProm, 'R': leEEProm } # Laço principal try: while True: cmd = input('Cmd: ') if len(cmd) == 0: continue cmd = cmd.upper() if cmd[0] in dictCmds: dictCmds[cmd[0]](cmd) else: print ('Comando desconhecido!') except KeyboardInterrupt: print ('Fim') except Exception as e: print (e)
Exemplo de Uso de I2C na Raspberry Pi Pico com C
A programação da Raspberry Pi Pico em C é descrita no manual do SDK (que você baixa aqui); a preparação do ambiente é descrita em outro artigo da Rosana (e na documentação oficial).
As funções para comunicação i2c no SDK são bastante simples e correspondem, grosseiramente, aos métodos para operações padrão no MicroPython. O SDK não tem uma função equivalente ao scan(), mas a documentação tem um exemplo que implementa isso, não incluí para o código não ficar longo.
O processo para gerar um executável a partir do programa C usando a linha de comando é trabalhoso, abaixo as instruções para Linux (veja mais detalhes nas referências acima).
Supondo que você instalou o SDK dentro do seu ‘home’, num diretório chamado pico, crie debaixo do pico o diretório exi2c e coloque dentro dele os arquivos exi2c.c e CMakeLists.txt listados abaixo, junto com uma cópia do arquivo pico_sdk_import.cmake que está em ~/pico/pico-sdk/external.
exi2c.c #include <stdio.h> #include <string.h> #include "pico/stdlib.h" #include "hardware/gpio.h" #include "hardware/i2c.h" const int SDA_PIN = 20; const int SCL_PIN = 21; const uint8_t ADDR_DS1307 = 0x68; const uint8_t ADDR_EEPROM = 0x50; static void leRelogio(); static void acertaRelogio(char *cmd); static void leEEProm(char *cmd); static void gravaEEProm(char *cmd); static void readMem (uint8_t addr, uint8_t *buf, uint16_t pos, int tam, int word); static void poeBCD (char *buf, uint8_t val); static uint8_t num2bcd(char *num); static uint16_t pegaHex(char *num, int ndig); static void leCmd(char *buf, int tbuf); // Programa principal int main() { char cmd[20]; stdio_init_all(); i2c_init(i2c0, 100000); gpio_set_function(SDA_PIN, GPIO_FUNC_I2C); gpio_set_function(SCL_PIN, GPIO_FUNC_I2C); gpio_pull_up(SDA_PIN); gpio_pull_up(SCL_PIN); while(1) { printf ("Cmd: "); leCmd (cmd, sizeof(cmd)); switch (cmd[0]) { case 'a': case 'A': acertaRelogio(cmd); break; case 'l': case 'L': leRelogio(); break; case 'g': case 'G': gravaEEProm(cmd); break; case 'r': case 'R': leEEProm(cmd); break; } } } // Trata comando de leitura do relogio static void leRelogio() { uint8_t regs[7]; char leitura[] = "Data: xx/xx/xx Hora: xx:xx:xx\n"; readMem (ADDR_DS1307, regs, 0, 7, 1); poeBCD (leitura+6, regs[4]); poeBCD (leitura+9, regs[5]); poeBCD (leitura+12, regs[6]); poeBCD (leitura+21, regs[2] & 0x3F); poeBCD (leitura+24, regs[1]); poeBCD (leitura+27, regs[0] & 0x7F); printf (leitura); } // Trata comando de acerto do relogio static void acertaRelogio(char *cmd) { uint8_t buf[8]; if (strlen(cmd) < 13) { return; } buf[0] = 0; // endereco do primeiro registrador buf[1] = num2bcd(cmd+11); // segundo buf[2] = num2bcd(cmd+9); // minuto buf[3] = num2bcd(cmd+7) + 0x40; // hora buf[4] = 1; // dia da semana buf[5] = num2bcd(cmd+1); // dia do mes buf[6] = num2bcd(cmd+3); // mes buf[7] = num2bcd(cmd+5); // ano i2c_write_blocking (i2c0, ADDR_DS1307, buf, 8, false); leRelogio(); } // Trata comando de leitura da EEProm static void leEEProm (char *cmd) { uint16_t ender; uint8_t dado; if (strlen(cmd) < 5) { return; } ender = pegaHex (cmd+1, 4); readMem (ADDR_EEPROM, &dado, ender, 1, 2); printf ("EEProm[%04X] = %02X\n", ender, dado); } // Trata o comando de escrita na EEProm static void gravaEEProm(char *cmd) { uint16_t ender; uint8_t dado; uint8_t buf[3]; if (strlen(cmd) < 7) { return; } ender = pegaHex (cmd+1, 4); dado = pegaHex (cmd+5, 2); buf[0] = ender >> 8; buf[1] = ender & 0xFF; buf[2] = dado; i2c_write_blocking (i2c0, ADDR_EEPROM, buf, 3, false); readMem (ADDR_EEPROM, &dado, ender, 1, 2); printf ("EEProm[%04X] = %02X\n", ender, dado); } // Faz uma leitura de memoria / registrador static void readMem (uint8_t addr, uint8_t *buf, uint16_t pos, int tam, int tpos) { // Envia o endereco uint8_t aux[2]; if (tpos == 2) { aux[0] = pos >> 8; aux[1] = pos & 0xFF; i2c_write_blocking (i2c0, addr, aux, 2, true); } else { aux[0] = pos & 0xFF; i2c_write_blocking (i2c0, addr, aux, 1, true); } // Le a resposta i2c_read_blocking (i2c0, addr, buf, tam, false); } // Converte numero ASCII para BCD static uint8_t num2bcd(char *num) { return (uint8_t) (((num[0]-'0') << 4) + (num[1]-'0')); } // Converte numero BDC para ASCII static void poeBCD (char *buf, uint8_t val) { buf[0] = (char) ((val >> 4) + '0'); buf[1] = (char) ((val & 0x0F) + '0'); } // Pega um valor hexa de um texto static uint16_t pegaHex(char *num, int ndig) { uint16_t ret = 0; int i; for (i = 0; i < ndig; i++) { char c = num[i]; ret = ret << 4; if ((c >= '0') && (c <= '9')) { ret += c - '0'; } else if ((c >= 'A') && (c <= 'F')) { ret += c - 'A' + 10; } else if ((c >= 'a') && (c <= 'f')) { ret += c - 'a' + 10; } } return ret; } // Le comando finalizado por \r (Enter) static void leCmd(char *buf, int tbuf) { int c; int n = 0; tbuf--; // deixa espaco para o nul final while (1) { c = getchar(); if (c == '\r') { buf[n] = 0; putchar('\n'); return; } if ((c >= 0x20) && (c < 0x7F) && (n < tbuf)) { buf[n++] = (char) c; putchar(c); } } }
CMakeLists.txt cmake_minimum_required(VERSION 3.13) include(pico_sdk_import.cmake) project(exi2c_project) pico_sdk_init() add_executable(exi2c exi2c.c ) pico_enable_stdio_usb(exi2c 1) pico_add_extra_outputs(exi2c) target_link_libraries(exi2c pico_stdlib hardware_i2c)
Agora crie um diretório build dentro do exi2c e execute os comandos abaixo:
cd build export PICO_SDK_PATH=../../pico-sdk cmake .. make
Ao final terá sido criado, entre outros, o arquivo exi2c.uf2. Aperte o botão BOOT da Pi Pico, conecte ao micro e solte o botão, o micro vai reconhecer a placa como um pendrive. Copie o arquivo exi2c.uf2 para este drive, a placa irá reiniciar e executar o programa.
Para interagir com o programa você vai precisar de um programa de comunicação. No Windows você pode usar o Monitor da IDE do Arduino ou o puTTY. No Linux podemos usar o minicom:
minicom -b 115200 -o -D /dev/ttyACM0
Conclusão
Neste artigo aprendemos um pouco sobre a comunicação I2C e vimos como utilizá-la na Raspberry Pi Pico para conversar com um módulo de relógio + memória EEProm.
A figura abaixo mostra o funcionamento do exemplo MicroPython dentro da IDE Thonny:
Gostou do artigo? Deixe seu comentário logo abaixo dizendo o que achou. Para mais artigos e tutorias de projetos acesse nosso blog.
Gostaria de saber se há a possibilidade de utilizar o Raspberry Pi Pico como slave programando em MicroPython e se há um tutorial para isso?
Marcelo, por enquanto o MicroPython não suporta diretamente a operação I2C no modo slave : ( Em https://www.raspberrypi.org/forums/viewtopic.php?t=302978 , no meio de uma longa discussão, tem uma implementação acessando direto os registradores do RP2040.