quinta-feira, julho 16, 2020

IOExpander: Usando um ATtiny24 como um Expansor de I/O I2C

Em uma etapa do meu projeto do "ATmega Detonator" achei que o PCF8574 era o culpado por problemas misteriosos. Antes de descobrir ser um simples mau contato, eu investi na linha de substituir o PCF8574 por um ATtiny24. Inicialmente parecia muito simples: controlar os inos de I/Os a partir de uma leitura ou escrita via I2C. O que eu não contava era com as dificuldades no uso da USI ("Universal" Serial Inteface) no modo escravo (já vimos anos atrás o uso no modo mestre).

Do ponto de vista de hardware o projeto é muito simples. Como vou usar o clock interno de 8MHz (precisão de clock é desnecessária), o ATtiny necessita apenas de alimentação e um resistor no pino de reset. Dois pinos são usados para o I2C e oito pinos para o I/O. Sobra um pino, no qual eu liguei um LED e foi fundamental para o debug.


A minha especificação do firmware também é simples. Ao receber uma escrita via I2C, os pinos de I/O são configurados para saída e o valor recebido indica o nível de cada pino. Ao receber uma leitura, os pinos são configurados como leitura e o valor lido é retornado para o "mestre". O endereço I2C é fixo (0x42).

O firmware foi escrito usando o avr-gcc toolchain e acessando direto os registradores do ATtiny, nada de biblioteca Arduino! Como não temos bootloader, o código sai executando assim que o microcontrolador é energizado.

Na parte do I/O um pequeno inconveniente é que os pinos estão mapeados em duas portas. Uma escolha cuidadosa dos pinos a usar e sua associação aos bits na comunicação permitem fazer o mapeamento com poucas operações lógicas:


Então vamos ao problema principal, a USI (Universal Serial Interface). A complicação é que ela faz apenas parte do trabalho de comunicação, uma parte precisa ser feita acessando os pinos como se fossem pinos de I/O digital. A Atmel disponibiliza um artigo e um exemplo de código. Eu parti deles, mas são um pouco confusos.

Como se isso não bastasse, o exemplo da Atmel trabalha por interrupções e é genérico no que diz respeito à quantidade de bits a transmitir e receber. Como o meu firmware não tem mais o que fazer além de tratar a comunicação e sempre vamos transferir um único byte, achei melhor trabalhar sem interrupções, usando loops para aguardar a mudança de bits no registrador de status da USI.

Foi uma luta árdua para fazer o código funcionar, No final saí dolorido mas vitorioso. Segue abaixo o código final:

//
// Expansor de I/O I2C
// 
// (C) 2020, Daniel Quadros
//

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
#include <avr/io.h>
#include <avr/interrupt.h>

#define FALSE 0
#define TRUE 1

// Conexões do hardware
//
//    Vcc 1    14 GND
//     D4 2    13 D0
//     D5 3    12 D1
//  RESET 4    11 D2
//     D6 5    10 D3
//     D7 6    9  SCL
//    SDA 7    8  LED 
//

// Pinos de I/O
// Port A   7 - - - 3 2 1 0
// Port B   - - - - - 6 5 4

// Nosso endereço I2C
#define I2C_ADDR   0x42

// Direção atual da porta
volatile uint8_t portaEntrada;

// Operação desejada
volatile uint8_t leitura;

#define DDR_USI             DDRA
#define PORT_USI            PORTA
#define PIN_USI             PINA
#define PORT_USI_SDA        PORTA6
#define PORT_USI_SCL        PORTA4
#define PIN_USI_SDA         PINA6
#define PIN_USI_SCL         PINA4
#define PIN_LED             PINA5

// Funções implementadas como macro
// Adaptadas do Application Note AVR32 da Atmel/Microchip
#define SET_USI_TO_SEND_ACK()                                                                                 \
{                                                                                                             \
        USIDR    =  0;                                              /* Prepare ACK                         */ \
        DDR_USI |=  (1<<PORT_USI_SDA);                              /* Set SDA as output                   */ \
        USISR    =  (0<<USISIF)|(1<<USIOIF)|(1<<USIPF)|(1<<USIDC)|  /* Clear all flags, except Start Cond  */ \
                    (0x0E<<USICNT0);                                /* set USI counter to shift 1 bit. */ \
}

#define SET_USI_TO_READ_ACK()                                                                                 \
{                                                                                                             \
        DDR_USI &=  ~(1<<PORT_USI_SDA);                             /* Set SDA as intput */                   \
        USIDR    =  0;                                              /* Prepare ACK        */                  \
        USISR    =  (0<<USISIF)|(1<<USIOIF)|(1<<USIPF)|(1<<USIDC)|  /* Clear all flags, except Start Cond  */ \
                    (0x0E<<USICNT0);                                /* set USI counter to shift 1 bit. */ \
}

#define SET_USI_TO_TWI_START_CONDITION_MODE()                                                                                     \
{                                                                                                                                 \
  DDR_USI &= ~(1<<PORT_USI_SDA);                              /* Set SDA as input                                             */  \
  USICR    =  (0<<USISIE)|(0<<USIOIE)|                        /* No interrupts.                                               */  \
              (1<<USIWM1)|(0<<USIWM0)|                        /* Set USI in Two-wire mode. No USI Counter overflow hold.      */  \
              (1<<USICS1)|(0<<USICS0)|(0<<USICLK)|            /* Shift Register Clock Source = External, positive edge        */  \
              (0<<USITC);                                                                                                         \
  USISR    =  (0<<USISIF)|(1<<USIOIF)|(1<<USIPF)|(1<<USIDC)|  /* Clear all flags, except Start Cond                            */ \
              (0x0<<USICNT0);                                                                                                     \
}

#define SET_USI_TO_SEND_DATA()                                                                               \
{                                                                                                            \
    DDR_USI |=  (1<<PORT_USI_SDA);                                  /* Set SDA as output                  */ \
    USISR    =  (0<<USISIF)|(1<<USIOIF)|(1<<USIPF)|(1<<USIDC)|      /* Clear all flags, except Start Cond */ \
                (0x0<<USICNT0);                                     /* set USI to shift out 8 bits        */ \
}

#define SET_USI_TO_READ_DATA()                                                                               \
{                                                                                                            \
    DDR_USI &= ~(1<<PORT_USI_SDA);                                  /* Set SDA as input                   */ \
    USISR    =  (0<<USISIF)|(1<<USIOIF)|(1<<USIPF)|(1<<USIDC)|      /* Clear all flags, except Start Cond */ \
                (0x0<<USICNT0);                                     /* set USI to shift out 8 bits        */ \
}

// Rotinas
void init (void);
void setPortInput (void);
void setPortOutput (void);
void escreve (uint8_t val);
uint8_t le(void);
void initUSI (void);
uint8_t selecionouUSI (void);
void enviaUSI (uint8_t val);
uint8_t recebeUSI (void);

// Programa principal
int main(void) {
    init();
    for (;;) {
        if (selecionouUSI()) {
            if (leitura) {
                if (!portaEntrada) {
                    setPortInput();
                }
                enviaUSI(le());
            } else {
                if (portaEntrada) {
                    setPortOutput();
                }
                escreve(recebeUSI());
            }
        }
    }
}

// Iniciação
void init() {
    DDRA |= PIN_LED;  // LED é saída
    setPortInput();
    initUSI();
}

// Configura os pinos de I/O como inputs sem pullup
void setPortInput(void) {
    DDRA &= ~0x8F;
    PORTA &= ~0x8F;
    DDRB &= ~0x07;
    PORTB &= ~0x07;
    portaEntrada = TRUE;
    PORTA &= ~PIN_LED;  // Apaga o LED
}

// Configura os pinos de I/O como saída
void setPortOutput(void) {
    DDRA |= 0x8F;
    DDRB |= 0x07;
    portaEntrada = FALSE;
    PORTA |= PIN_LED;  // Acende o LED
}

// Escreve um valor nos pinos
void escreve(uint8_t val) {
    uint8_t aux;
    aux = PORTA & ~0x8F;
    aux |= (val & 0x8F);
    PORTA = aux;
    aux = PORTB & ~0x07;
    aux |= (val & 0x70) >> 4;
    PORTB = aux;
}    

// Lê o valor nos pinos
uint8_t le(void) {
    uint8_t aux;
    aux = PINA & 0x8F;
    aux |= (PINB & 0x07) << 4;
    return aux;
}

// Inicia a USI para operar como I2C escravo
void initUSI(void) {
  PORT_USI |=  (1<<PORT_USI_SCL);                       // Set SCL high
  PORT_USI |=  (1<<PORT_USI_SDA);                       // Set SDA high
  DDR_USI  |=  (1<<PORT_USI_SCL);                       // Set SCL as output
  DDR_USI  &= ~(1<<PORT_USI_SDA);                       // Set SDA as input
  USICR    =  (0<<USISIE)|(0<<USIOIE)|                  // No interrupts
              (1<<USIWM1)|(0<<USIWM0)|                  // Set USI in Two-wire mode. No USI Counter overflow prior
                                                        // to first Start Condition (potentail failure)
              (1<<USICS1)|(0<<USICS0)|(0<<USICLK)|      // Shift Register Clock Source = External, positive edge
              (0<<USITC);
  USISR    = 0xF0;                                      // Clear all flags and reset overflow counter
}

// Verifica se chegou um start e o nosso endereço
// Se sim, atualiza "leitura"
uint8_t selecionouUSI (void) {
    if ((USISR & (1 << USISIF)) == 0) {
        return FALSE;
    }

    // Recebemos um start, vamos receber o endereço e o flag de direção
    //uint8_t tmpUSISR;                                               // Temporary variable to store volatile
    //tmpUSISR = USISR;                                               // Not necessary, but prevents warnings
    DDR_USI  &= ~(1<<PORT_USI_SDA);                                 // Set SDA as input
    //while ( (PIN_USI & (1<<PIN_USI_SCL)) & !(tmpUSISR & (1<<USIPF)) )    // Wait for SCL to go low to ensure the "Start Condition" has completed.
    //    ;                                                                // If a Stop condition arises then leave to prevent waiting forever.
    while (PIN_USI & (1<<PIN_USI_SCL))
        ;
    
    USICR   =   (0<<USISIE)|(0<<USIOIE)|                            // No interrupts
                (1<<USIWM1)|(1<<USIWM0)|                            // Set USI in Two-wire mode.
                (1<<USICS1)|(0<<USICS0)|(0<<USICLK)|                // Shift Register Clock Source = External, positive edge
                (0<<USITC);
    USISR  =    (1<<USISIF)|(1<<USIOIF)|(1<<USIPF)|(1<<USIDC)|      // Clear flags
                (0x0<<USICNT0);                                     // Set USI to sample 8 bits i.e. count 16 external pin toggles.

    while ((USISR & ((1 << USIOIF) | (1 << USIPF))) == 0)           // Espera receber um byte ou detectar stop
        ;
    
    if ((USISR & (1 << USIPF)) != 0) {
        SET_USI_TO_TWI_START_CONDITION_MODE();
        return FALSE;
    }
    
    // Verifica o endereço
    if (( USIDR>>1 ) == I2C_ADDR)
    {
        leitura = USIDR & 0x01;
        SET_USI_TO_SEND_ACK();
        return TRUE;
    }
    else
    {
        SET_USI_TO_TWI_START_CONDITION_MODE();
        return FALSE;
    }
}

// Envia val pela USI e encerra a transação
void enviaUSI (uint8_t val) {
    // Espera acabar de enviar o ACK
    while ((USISR & (1 << USIOIF)) == 0)
        ;
    
    // Dispara o envio
    USIDR = val;
    SET_USI_TO_SEND_DATA();
    
    // Espera acabar de enviar o byte
    while ((USISR & (1 << USIOIF)) == 0)
        ;

    // Aguarda o ACK
    SET_USI_TO_READ_ACK();
    while ((USISR & (1 << USIOIF)) == 0)
        ;

    // Aguardar stop condition
    //while ((USISR & (1 << USIPF)) == 0)
    //    ;
    
    // Aguardar a próxima transação
    SET_USI_TO_TWI_START_CONDITION_MODE();    
}

// Recebe um byte pela USI e encerra a transação
uint8_t recebeUSI (void) {
    // Espera acabar de enviar o ACK
    while ((USISR & (1 << USIOIF)) == 0)
        ;
    
    // aguarda o dado
    SET_USI_TO_READ_DATA();
    while ((USISR & (1 << USIOIF)) == 0)
        ;
    
    // Lê o dado
    uint8_t dado = USIDR;
    
    // Envia o ACK
    SET_USI_TO_SEND_ACK();
    while ((USISR & (1 << USIOIF)) == 0)
        ;

    // Aguardar a próxima transação
    SET_USI_TO_TWI_START_CONDITION_MODE();
    
    return dado;
}

Neste código o LED indica a direção atual da porta. Durante o desenvolvimento o LED foi usado para indicar várias outras situações.

O código e makefile estão no meu github.

Nenhum comentário: