fenglogo
Laboratório de Processadores
Programação do LPC2378 usando o arm-elf-gcc
Parte 8: Interface I2C: termômetro TMP100 e smartcard
puclogo

Compilação e uso do Programa

Neste exemplo vamos fazer o programa a08_tmp101 para fazer um relógio que também le a temperatura através de uma interface I2C. Para configurar e testar o programa fazemos como de costume:

Configurar para gravar na memória Flash

Colocar o kit em modo de programação

Compilar e gravar a flash com o comando:

make isp
Testar chamando o terminal ltser e iniciar a execução apertando no botão de RESET.

Configurar para gravar na memória RAM

Pode-se executar o programa na RAM. Até os vertores de interrupção podem rer remapeados para funcionar a partir da RAM a partir do endereço 0x40000000. Um registrador de configuração do LPC2378 chamado MEMMAP permite remapear os vetores de interrupção para a RAM.

O programa mon23 inclui um carregador de arquivos *.hex que pode colocar o programa na RAM usando o terminal ltser. Isto é feito automaticamente usando a diretiva make tser deste projeto. Antes de executar make tser é necessário que o monitor mon23 esteja gravado na Flash e sendo executado. Então basta fazer

make tser
O programa será compilado com a configuração para executar na RAM e carregado na RAM usando o terminal ltser. Para executa-lo usa-se o comando P do monitor mon23.

Análise do programa: Configuração da interface I2C

Agora veremos como este programa foi feito, estudando os recursos do LPC2378 que foram usados. O programa foi subdividido em módulos conforme a tabela a seguir:
uart.c Configuração do clock e da interface serial.
i2c.c Rotinas de acesso à interface I2C para a configuração e leitura do termômetro TMP100
lcd.cConfiguração e acesso ao display LCD
main.c Programa principal que faz um relógio com termômetro.

Descrição Genérica da interface I2C

A interface I2C é um padrão de comunicação criado pela Philips Semiconductors (Atualmente NXP) para possibilitar que microcontroladores acessem dispositivos periféricos usando apenas 2 pinos de E/S. A velocidade de comunicação pode ser de 0 a 100kBit/s no modo padrão ou até 400kBit/s no modo rápido (Fast mode). Como o modo rápido exige certos cuidados especiais, como a terminação dos sinais e dispositivos especiais, vamos considerar apenas o modo padrão.

A interface I2C usa dois sinais com acionamento de dreno aberto: O SDA (Serial DAta) é usado para os dados seriais; e o SCL (Serial CLock) é um sinal de relógio usado para sincronizar os dados. O acionamento em dreno aberto significa que os dispositivos ligados ao barramento I2C podem aterrar as linhas de sinal SDA ou SCL, mas não podem obriga-las a ir para nível alto. O nível alto nos sinais I2C ocorre quando nenhum dispositivo ligado ao barramento está puxando para baixo. Neste caso resistores externos pull-up fazem subir o nível de tensão das linhas de sinal.

Existem dois tipos de dispositivo conectado a um barramento I2C: o mestre e os escravos. Normalmente so tem um mestre, podendo ter vários escravos. O mestre (geralmente o microcontrolador) é que toma a iniciativa nas transações, enquanto os escravos operam de modo passivo.

A sincronização dos dados SDA com o clock é feita de modo que SDA pode mudar enquanto SCL=0 e deve permanecer estável enquanto SCL=1. Esta regra tem duas exeções: o START bit e o STOP bit.

Escrita I2C.

A escrita é feita na seguinte seqüência:

  1. Gera o START fazendo descer primeiro SDA e depois SCL
  2. Escreve os 7 bits do endereço I2C do dispositivo.
  3. Escreve o bit R/W, que para a escrita é zero.
  4. Libera SDA para ler o ACK. O ACK da escrita é feito pelo periférico (escravo).
  5. Escreve 8 bits de dados e recebe o ACK. Esta operação pode ser repetida para enviar vários bytes.
  6. Gera o STOP, fazendo subir primeiro o SCL e depois SDA.
Diagrama de tempo da escrita I2C

Leitura I2C.

A leitura é feita na seguinte seqüência:

  1. Gera o START fazendo descer primeiro SDA e depois SCL
  2. Escreve os 7 bits do endereço I2C do dispositivo.
  3. Escreve o bit R/W, que para a escrita é um.
  4. O mestre libera o SDA e le o ACK gerado pelo periférico.
  5. O mestre le os 8 bits de dados e gera o sinal ACK ou NACK. Esta operação pode ser repetida para ler vários bytes na mesma transação. O mestre deve responder NACK no último byte lido e ACK nos outros bytes.
  6. Gera o STOP, fazendo subir primeiro o SCL e depois SDA.
Diagrama de tempo da leitura I2C

Na figura acima os dados em Verde são gerados pelo microprocessador e em Vermelho são as respostas do periférico. Observe que no último dado lido o microcontrolador responde NACK.

O termômetro I2C tipo TMP100

O TMP100 da Texas Instruments é um sensor de temperatura com calibração e conversão para digital interna, fornecendo a temperatura em graus Celcius através de uma interface I2C.

O TMP100 tem pinos de endereço que permitem configura-lo para atender em endereços que vão de 0x90 a 0x9E. No nosso kit de Lab. Processadores o TMP100 está configurado para atender no endereço 0x96. Desejamos fazer com ele dois tipos de operação: Escrever no registrador de configuração e ler a temperatura.

Registrador de configuração:

Os bits deste registrador permitem configurar o TMP100 para a máxima resolução: 1/16 Grau Celcius. Os bits do Registrador de configuração são os seguintes:
76543210
OS/ALERTR1R0F1F0POLTMSD

No caso os bits R1 e R0 definem a resolução. 11 corresponde a uma resolução de 1/16 grau Celcius. Veja a função dos outros bits no manual do TMP100. No nesso programa vamos usar o valor 0x60 neste registrador. Para programa-lo temos que fazer a seguinte sequencia de operações:

Leitura da Temperatura

Antes de poder ler a temperatura é necessario escrever o valor 0 (zero) para selecionar o registrador de temperatura. A temperatura aparece em 2 bytes: o primeiro tem a parte inteira de graus Celcius. O segundo tem a parte fracionária, com os bits alinhados à esquerda. Portanto as seguintes operações são necessárias:

A interface I2C no LPC2378

Os microcontroladores da família LPC23xx possuem hardware dedicado para comunicar-se através de uma interface I2C. Podem ser configurados para atuar como mestre ou como escravo. Na leitura do TMP100 queremos que o microcontrolador seja o mestre.

O LPC2378 tem 3 interfaces I2C suportadas em hardware. No kit temos um termômetro tipo TMP100 conectado à interface número 2. Os seguintes registradores são relevantes para este programa:
I2C2SCLH
0xE0080010
Divisor que determina o tempo alto do clock SCL
I2C2SCLL
0xE0080014
Divisor que determina o tempo baixo do clock SCL
I2C2CONCLR
0xE0080018
Apaga bits do registrador de controle do I2C.
I2C2CONSET
0xE0080000
Liga bits do registrador de controle do I2C. Os bits são os seguintes:
BitValorSímboloDescrição
70x80 - Não usado
60x40I2ENHabilita o funcionamento da interface I2C
50x20STAInicia uma transação gerando um START bit
40x10STOEscrevendo 1 aqui gera um STOP bit
30x08SIIndicador de Interrupção: Liga quando mudou o estado.
20x04AADetermina que após a leitura deve gerar ACK
1 - Não usado
0 - Não usado
I2C2STAT
0xE0080004
Os bits 7 a 3 deste registrador indicam o estado de uma transação I2C em andamento. Os bits 0, 1 e 2 são sempre 0. O softare deve consultar este registrador para verificar qual ´ a próxima coisa a fazer.
I2C2DAT
0xE0080008
Byte de dados. Neste registrador pode-se ler um byte recebido ou escrever para transmitir um byte.

Configuração inicial

O manual da NXP recomenda configurar a interface I2C para gerar interupção sempre que ocorre uma mudança de estado. Para não complicar demais logo no início, vou primeiro mostrar como funciona o programa arm07/ que le o TMP100 sem usar interrupção.

O seguinte trecho de código inicializa a interface deixando-a prota para uso:

/* Inicializa o canal 2 de I2C */
void ini_i2c(void)
{
PINSEL0 &= 0xffafffff;
PINSEL0 |= 0x00a00000;	/* Seleciona pinos do SDA2 e SCL2 */
I22CONCLR = 0xff;
I22CONSET = 0x40;	/* Habilita o I2C-2 um modo mestre */
I22SCLH   = 100;	/* Tempo alto do SCL	*/
I22SCLL   = 100;	/* Tempo baixo do SCL	*/
}

Tres coisas precisam ser feitas: Selecionar os pinos dos sinais SDA2 e SCL2 no seletor de funçõs dos pinos; habilitar a interface I2C2 em modo mestre e configurar a frequência do clock de I2C. Com o valor 100 escrito em I22SCLH e I22SCLL obtém-se um clock I2C de 60kHz quando o clock principal é de 12MHz.

Escrita

A escrita inicia ligando o bit STA no I22CONSET. As operações subsequentes são feitas a medida que a leitura do I22STAT indica que as etapas ateriores foram concluídas.

/* Escreve 2 bytes na interface i2c. Se val<0 escreve so o dado. */
void escreve_i2c(int addr, int dado, int val)
{
int n;	/* Conta bytes a escrever */
int i;	/* Conta votas esperando no loop (timeout) */
/* Inicia a transacao: Envia o endereco 0x90 para escrita no TMP101 */
I22CONCLR = 0xFF;	/* Limpa bits de controle */
I22CONSET = 0x60;	/* Habilita e gera START */
k=2; i=1000;
do	{
	switch(I22STAT){
		case 0x08: /* Terminou START; envia endereco */
			I22CONCLR=0x20;
		case 0x20: /* NACK do endreco: reenvia */
			I22DAT = addr; I22CONCLR=8; i=1000; break;
		case 0x18: /* ACK do endereco: envia dado */
			I22DAT = dado; I22CONCLR=8; i=1000; break;
		case 0x28: /* ACK do dado */
			if(k < 2) I22CONSET = 0x10;	/* Gera STOP */
			else I22DAT=val;	/* Envia outro dado */
			I22CONCLR=8; 
			i=1000; k--;
			break;
		}
	} while(k && --i);
}

Acesso ao I2C com interupção

O hardware de I2C pode gerar interupção sempre que uma etapa da transação é concluída. O programa letmp100/ implementa um relógio que também le um termômetro tipo TMP100 ligado no I2C número 2, usando interrupção.

A inicialização do I2C com interrupção pode ser feita com o seguinte trecho de código:

/* Inicializa o canal 2 de I2C com interrupcao */
void ini_i2c(void)
{
PCONP |= 0x04000080;	/* Liga energia do I2C0 e I2C2 */
PINSEL0 &= 0xffafffff;
PINSEL0 |= 0x00a00000;	/* Seleciona pinos do SDA2 e SCL2 */
I22CONCLR = 0xff;
I22CONSET = 0x40;	/* Habilita o I2C-2 um modo mestre */
I22SCLH   = 100;	/* Tempo alto do SCL	*/
I22SCLL   = 100;	/* Tempo baixo do SCL	*/

/* Habilita a interrupcao do i2c2 como IRQ no VIC	*/
desabilitaIRQ();	/* Definida no #include "vic_cpsr.h"	*/
VICIntSelect &= ~0x40000000;	/* i2c2=bit 30 como IRQ	*/
VICIntEnable |= 0x40000000;	/* Habilita int do i2c2 no VIC*/
VICVectAddr30 = (int)IRQ_i2c2;	/* Vetor para atendimento do I2C2 */
i2cn=0;
habilitaIRQ();	/* Definida no #include "vic_cpsr.h"	*/
I22CONSET=0x10;
}

A operação habilitaIRQ(); é feita usando uma macro definida no arquivo de cabeçalho vic.cpsr.h para gerar uma sequência de instruções em assembly para ligar o bit de habilitação da IRQ no registrador de status CPSR:

#define habilitaIRQ() asm volatile(\
	"mrs r3, cpsr\n" \
	"bic r3, r3, # 0x80\n" \
	"msr cpsr, r3\n")

A rotina de atendimento da IRQ da interface I2C é implementada como uma máquina de estados com diretivas tipo switch-case. Dependendo da operação que foi concluída a rotina decide qual é a próxima coisa a ser feita:

/* Variaveis globais da Interrupcao i2c */
char i2cdados[32];	/* Buffer de dados lidos ou escritos */
int slvaddr;		/* Endereco do dispositivo i2c	*/
int i2cn;		/* Numero de bytes a ler/escrever */

void IRQ_i2c2(void) __attribute__ ((interrupt("IRQ")));
/*
A rotina de interrupcao do I2C implementa uma maquina de estados.
Dependendo do valor do I22STAT sabe o que foi feito e qual e' a
proxima coisa a fazer. */
void IRQ_i2c2(void)
{
static int k;		/* Indice do array de dados */
I22CONSET = 4;		/* Normalmente responde ACK */
switch(I22STAT){
	case 0:	case 0x20:	/* Erro */
	case 0x30:	/* Dado foi escrito recebendo NACK */
	case 0x48:	/* Endereco de leitura recebeu NACK */
		I22CONSET = 0x10;	/* Gera STOP */
		i2cn=-1;		/* i2cn=-1 indica erro */
		break;
	case 8:		/* Terminou de enviar o START bit */
		I22CONCLR=0x20;
		I22DAT = slvaddr;	/* Escreve endereco	*/
		k=0;
		break;
	case 0x18:	/* Enviou endereco de escrita recebendo ACK */
	case 0x28:	/* Escreveu dado recebendo ACK */
		/* Escreve mais dados */
		if(i2cn) { I22DAT = i2cdados[k++]; i2cn--; }
		else I22CONSET=0x14;	/* Fim dos dados: gera STOP */
		break;
	case 0x40:	/* Enviou endereco de leitura e recebeu ACK */
	/* Prepara para iniciar a leitura de dados */
		i2cdados[0]=I22DAT; k=0; /* Le lixo so para iniciar */
		if(i2cn==1) I22CONCLR=4; /* So 1 dado responde NACK */
		break;
	case 0x50:	/* Dado recebido enviando ACK */
		i2cdados[k++]=I22DAT;	/* Le o proximo dado */
		/* Se for o ultimo envia NACK */
		if(!(--i2cn)) I22CONCLR=4;
		break;
	case 0x58:	/* Leu enviando NACK */
		i2cdados[k++]=I22DAT;	/* Le o ultimo dado */
		i2cn=0;
		I22CONSET=0x10;	/* Gera STOP */
		break;
	}
I22CONCLR = 8;	/* Limpa o indicador de interrupcao do I2C */
VICVectAddr = 0;	/* Limpa o vetor do VIC */
}
Para fazer uma escrita ou leitura da interface I2C agora basta colocar os dados nas variaveis globais
char i2cdados[32];	/* Buffer de dados lidos ou escritos */
int slvaddr;		/* Endereco do dispositivo i2c	*/
int i2cn;		/* Numero de bytes a ler/escrever */
e iniciar a transação escrevendo 0x20 no I22CONSET. Isto faz com que um START bit seja emitido. Depois disto a rotina de interrupção faz as operações necessárias para concluir a transação. Apenas devemos cuidar para dar tempo para que isto seja feito.

Referências