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:
Postar um comentário