Usando I2C na Raspberry Pi Pico com MicroPython e C Deixe um comentário

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.

Protoboard montada com a interface I2C e a Raspberry Pi Pico.
Protoboard montada com a interface I2C e a Raspberry Pi Pico.

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.

Ilustração da distribuição de sinais entre mestre e dispositivos conectados, uma funcionalidade da I2C.
Distribuição de sinais na interface I2C.

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:

Tabela de configuração dos pinos da Raspberry Pi Pico.
Tabela de configuração dos pinos de comunicação da Pi Pico.

Material Necessário para os Exemplos

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:

Ilustração das conexões entre a I2C e a Pi Pico feita no Fritzing.
Ilustração das conexões entre a I2C e a Pi Pico feita no Fritzing.

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:

Printscreen da IDE Thonny com código MicroPython.
Tela da IDE Thonny com código MicroPython.

Gostou do artigo? Deixe seu comentário logo abaixo dizendo o que achou. Para mais artigos e tutorias de projetos acesse nosso blog.

Faça seu comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *