terça-feira, 26 de agosto de 2008

[Renesas] I2C BitBang para 24XX256

Faz tempo que não consigo parar e escrever um bom POST. Pois bem, chego aqui com algo que havia prometido para muitos. Um código para barramento I2C para o 24C256 que usei num projeto meu.
Após ter alguns problemas em relação a configuração do R8C/25 com I2C via hardware, que já foi sanado, me sobrou este código desenvolvido, que aliás eu gostei mais que o I2C via device. Não pela versatilidade ou velocidade mas pela portabilidade. Acredito que este código que eu vou apresentar funciona com poucas altrações nos PICs e nos AVRs da vida.
Mas antes de falarmos do código vamos falar um pouco mais de I2C.
O I2C é um barramento serial, onde temos um canal de dados bidirecional e um canal de clock, regido pelo device Master, no caso o nosso R8C/25.

A ligação é feita de forma serial, vide esquema abaixo:


Notem que temos dois resitores de Pull-Up que devem ser instalados um para cada linha, SDA e SCL. Na prática com PIC geralmente uso resitores de 10k e com o R8C/25 uso resitores de 15k, ambos em 5V, não sei explicar o porquê mas com os Renesas só consegui estabilidade no Bus usando 50% a mais de carga no resitor de Pull-Up, vale como dica para não perderem tempo, como eu perdi tentando buscar outros motivos de "não funcionamento".

Os dados são transferidos, setando o estado do bit a ser transferido no SDA e chaveando o SCL. O dado é transferido na transição UP->Down. Acompanhe no diagrama abaixo:



Existem alguns poréns entre os estados de comunicação, dentre eles posso citar o Start Condition e o Stop Condition, no qual colocam.
O Start Condition serve para iniciar uma comunicação e o Stop Condition analogamente serve para encerrar a mesma.


De acordo com o diagrama acima, notamos que o Start Condition é gerado partindo dos sinais do SCL e do SDA em nível 1, chaveamos o SDA para nível baixo e no próximo tempo do Clock baixamos o nível do SCL.
Analogamente mais uma vez, o Stop Condition é gerado partindo dos sinals SCL e SDA em nível 0, chaveamos o SCL para nível 1 e em seguida, mais uma vez no tempo do Clock, elevamos o nível do SDA para 1.

Vide o seguinte código abaixo:

void startCondition(void) {
setSCLHigh();
setSDAHigh();
setSDALow();
waitFactor(2);
setSCLLow();
waitFactor(2);
}

Mais uma vez conforme o diagrama, antes elevamos os níveis para 1, em seguida baixamos o nível do SDA para 0, aguardamos 2 tempos do Clock, e em seguida baixamos o nível do SCL para 0. E mais uma vez aguardamos 2 tempos do Clock. Com este código geramos um Start Condition.

E agora o StopCondition:

void stopCondition(void) {
setSDALow();
waitFactor(1);
setSCLLow();
waitFactor(1);
setSCLHigh();
waitFactor(2);
setSDAHigh();
waitFactor(2);
}

Nesta função, note que chaveamos o SDA para nível 0 e aguardamos um tempo do Clock para em seguida descer o nível do Clock para 0. Fazemo isso para evitar que geremos um Erro no bus I2C no caso de um término de transação. Em seguida conforme diagrama do Stop Condition subimos o nível do SCL para nivel 1 e aguardamos dois tempos do Clock, em seguida subimos o nível do SDA para 1 e aguardamos mais uma vez dois tempos do Clock. Assim geramos um Stop Condition.

Um outro status importante na transação I2C é o Acknowlodge ou simplesmente Ack. Ele é a resposta do periférico que está sendo comunicado de que foi aceito o último byte.
O Ack é reconhecido após a escrita, colocando o pino do SDA em modo Input após o envio do oitavo bit. O pino SDA deverá ir para nível 0, forçado pelo dispositivo que se está comunicando. Ao gerar um novo pulso do Clock, é sinalizado ao dispositivo slave que foi lido o Ack, então o controle de SDA volta para o dispositivo Master, no nosso caso o R8C/25, onde o pino do SDA deve novamente ser setado como Output.
Acompanhe o código abaixo:

char getSDAState(void) {
_24C256_sda_d = 0;
asm("NOP");
return(_24C256_sda);
}

char readAck(void) {
char state=0;
setSCLHigh();
waitFactor(1);
state=getSDAState();
setSCLLow();
setSDAHigh();
waitFactor(3);
return(state);
}

Neste trecho setamos o SCL como nível 1 para finalizar o processo do último bit enviado, esperamos um ciclo de Clock, e lemos o estado do SDA. Em seguida setamos o SCL como nível 0 para sinalizar a leitura do Ack e ao mesmo tempo setamos o pino SDA como 1 para podermos iniciar uma nova comunicação ou enviar um Start/Stop Condition.
No caso do recebimento de bytes, nós não setamos o SDA como nível 1, somente baixamos o nível do SCL. Conforme código abaixo:

char readAckRcv(void) {
char state=0;
setSCLHigh();
waitFactor(1);
state=getSDAState();
setSCLLow();
waitFactor(1);
return(state);
}

Agora vamos abordar a comunicação I2C propriamente dita.
Os dois tipos de comunicação que podemos fazer são leitura randomica e escrita randomica. Na verdade há mais dois tipos que são leitura sequencial e escrita sequencial, no entanto não vamos tratar destes dois tipos aqui neste artigo.

Vamos falar inicialmente da gravação de 1 byte num endereço qualquer do 24C256.

O 24C256 é uma memória do tipo EEPROM com 256kBits ou 32kBytes. Potanto temos um endereçamento de 15Bits (2^15 = 32768 bytes).

Um outro aspecto que devemos considerar é que ao nos comunicarmos com periféricos I2C devemos saber seu "endereço" no BUS. No caso das memórias em geral elas iniciam com 0b1010 e em seguida temos os sinais dos pinos A2, A1 e A0 na sequencia, desta forma podemos usar mais de uma memória no Bus. 0b1010000[r/w], o último bit é 0 para escrita e 1 para leitura.

A comunicação na gravação é feita enviando uma sequencia de 4 bytes. Como descritos abaixo:

1.Byte = 7 bits do endereçamento do periférico mais 1 bit sinalizando leitura/gravação, neste caso gravação (0b10100000);
2.Byte = MSB do endereço de gravação na memória EEPROM;
3.Byte = LSB do endereço de gravação na memória EPPROM;
4.Byte = Byte a ser escrito na posição de memória.

Acompanhe o código abaixo para ver os quatro bytes sendo enviados, e a leitura do Ack em cada intervalo.

void Write_24C256(unsigned char deviceAddress, unsigned int menAddress, unsigned char byte) {
unsigned char myAck=0;
unsigned int tmsb,tlsb;
unsigned char msb, lsb;
tlsb = tmsb = menAddress;
tmsb = tmsb >> 8;
tlsb = (tlsb <<>> 8;
msb = (unsigned char) tmsb;
lsb = (unsigned char) tlsb;
startCondition();
if(!(myAck=writeByte(deviceAddress,0))) {
if(!(myAck=writeByte(msb,0))) {
if(!(myAck=writeByte(lsb,0))) {
if(!(myAck=writeByte(byte,0))) {
stopCondition();
waitFactor(100);
}
}
}
}
if(myAck) stopCondition();
}

Primeiro calculamos os MSB e LSB rotacionando os bits necessários, e guardando isso em variáveis de 8Bits para poderem ser usadas posteriormente no processo de gravação.
Em seguida geramos um Start Condition para iniciar transação, e enviamos nosso primeiro Byte, no Caso o conteúdo da variável deviceAddress, em seguida lemos o Ack para podermos continuar o processo, em seguida enviamos o MSB, e lemos o Ack, e enviamos o LSB na sequencia. Após tudo isso perfeitamente e o Ack afirmativo, enviamos o byte a ser gravado.
Se tudo correr bem enviamos um Stop Condition e agurdamos 100 ciclos do processador. É tempo suficiente (algo por volta de 5Ms) para que a EEPROM possa gravar o byte na memória indicada. Por fim, se houver algum problema e o Ack retornar positivo (status negativo ou "não aceito") é enviada um Stop Condition colocando Bus em stand by novamente.
Notem que no processo de gravação, usamos a função writeByte passando como parâmetro o byte a ser escrito e o indicador zero(0) sinalizando que deverá ser utilizada leitura de Ack para escrita e não leitura.

A comunicação na leitura é feita enviando uma sequencia de escrita de 3 bytes, um Start Condition, uma escrita de 1 byte e em seguida realizada a leitura. Como descrito abaixo:

1.Byte = 7 bits do endereçamento do periférico mais 1 bit sinalizando leitura/gravação, neste caso gravação (0b10100000);
2.Byte = MSB do endereço de gravação na memória EEPROM;
3.Byte = LSB do endereço de gravação na memória EPPROM;
4. **** Gerar Start Condition
5.byte = 7 bits do endereçamento do periférico mais 1 bit sinalizando leitura/gravação, neste caso leitura(0b10100001) * compara com passo 1;
Realizar a leitura do Byte (Colocar o SDA em modo Input e o Chavear o clock entre os Bits)

Eis o código:

unsigned char Read_24C256(unsigned char deviceAddress, unsigned int menAddress) {
unsigned char myAck=0;
unsigned int tmsb,tlsb;
unsigned char msb, lsb;
tlsb = tmsb = menAddress;
tmsb = tmsb >> 8;
tlsb = (tlsb <<>> 8;
msb = (unsigned char) tmsb;
lsb = (unsigned char) tlsb;
startCondition();
if(!(myAck=writeByte(deviceAddress,0))) {
if(!(myAck=writeByte(msb,0))) {
if(!(myAck=writeByte(lsb,0))) {
startCondition();
if(!(myAck=writeByte((deviceAddress|0x01),1))) {
myAck=getByte();
}
}
}
}
stopCondition();
return(myAck);
}

Basicamente esta função é identica ao processo de leitura, com exceção que no lugar do quarto byte, temos um Start Condition, e a escrita do deviceAddress com o bit 0 marcado como 1 (leitura). E obviamente após o Ack, a leitura do bit em si.

Eis o código completo dos dois arquivos... 24C256.h e 24C256.c ...

-------------- Inicio 24C256.h --------------
#ifndef _24C256_H
#define _24C256_H

#define _24C256_sda_d pd3_4
#define _24C256_sda p3_4
#define _24C256_scl_d pd3_5
#define _24C256_scl p3_5
#define waitUnit 100

// ---------------------- High Level Functions ---------------

void Init_24C256(void);
void Write_24C256(unsigned char deviceAddress, unsigned int menAddress, unsigned char byte);
unsigned char Read_24C256(unsigned char deviceAddress, unsigned int menAddress);

#endif
-------------- Fim 24C256.h --------------

-------------- Inicio 24C256.c --------------
#include "sfr_r825.h"
#include "24C256.h"

// ---------------------- Low Level Functions ---------------

void waitFactor(unsigned int factor) {
unsigned int fc=0;
factor*=waitUnit;
for(fc=0; fc}

char getSDAState(void) {
_24C256_sda_d = 0;
asm("NOP");
return(_24C256_sda);
}

char getSCLState(void) {
_24C256_scl_d = 0;
asm("NOP");
return(_24C256_scl);
}

void setSDAHigh(void) {
_24C256_sda_d = 1;
_24C256_sda = 1;
}

void setSCLHigh(void) {
_24C256_scl_d = 1;
_24C256_scl = 1;
}

void setSDALow(void) {
_24C256_sda_d = 1;
_24C256_sda = 0;
}

void setSCLLow(void) {
_24C256_scl_d = 1;
_24C256_scl = 0;
}

void startCondition(void) {
setSCLHigh();
setSDAHigh();
setSDALow();
waitFactor(2);
setSCLLow();
waitFactor(2);
}

void sendHighBit(void) {
setSDAHigh();
setSCLHigh();
waitFactor(1);
setSCLLow();
waitFactor(1);
}

void sendLowBit(void) {
setSDALow();
setSCLHigh();
waitFactor(1);
setSCLLow();
waitFactor(1);
}

char readAck(void) {
char state=0;
setSCLHigh();
waitFactor(1);
state=getSDAState();
setSCLLow();
setSDAHigh();
waitFactor(3);
return(state);
}

char readAckRcv(void) {
char state=0;
setSCLHigh();
waitFactor(1);
state=getSDAState();
setSCLLow();
waitFactor(1);
return(state);
}

void stopCondition(void) {
setSDALow();
waitFactor(1);
setSCLLow();
waitFactor(1);
setSCLHigh();
waitFactor(2);
setSDAHigh();
waitFactor(2);
}

unsigned char getByte(void) {
unsigned char byte=0;
unsigned char pos=0;
_24C256_sda_d=0;
while(pos<8) {
byte|=getSDAState();
if(pos<7) byte<<=1;
pos++;
setSCLHigh();
waitFactor(1);
setSCLLow();
waitFactor(1);
}
setSDALow();
waitFactor(1);
setSCLHigh();
waitFactor(1);
setSCLLow();
waitFactor(1);
return(byte);
}

unsigned char writeByte(unsigned char byte,unsigned char forRead) {
unsigned char rotateLeft;
unsigned char tmp;
rotateLeft = 0;
while(rotateLeft<8) {
tmp=byte;
tmp = (tmp <<>> 7;
rotateLeft++;
if(tmp) {
sendHighBit();
} else {
sendLowBit();
}
}
if(forRead) return(readAckRcv());
return(readAck());
}

// ---------------------- High Level Functions ---------------

void Init_24C256(void) {
_24C256_sda=1;
_24C256_scl=1;
_24C256_sda_d = 1;
_24C256_scl_d = 1;
_24C256_sda=1;
_24C256_scl=1;
}

void Write_24C256(unsigned char deviceAddress, unsigned int menAddress, unsigned char byte) {
unsigned char myAck=0;
unsigned int tmsb,tlsb;
unsigned char msb, lsb;
tlsb = tmsb = menAddress;
tmsb = tmsb >> 8;
tlsb = (tlsb <<>> 8;
msb = (unsigned char) tmsb;
lsb = (unsigned char) tlsb;
startCondition();
if(!(myAck=writeByte(deviceAddress,0))) {
if(!(myAck=writeByte(msb,0))) {
if(!(myAck=writeByte(lsb,0))) {
if(!(myAck=writeByte(byte,0))) {
stopCondition();
waitFactor(100);
}
}
}
}
if(myAck) stopCondition();
}

unsigned char Read_24C256(unsigned char deviceAddress, unsigned int menAddress) {
unsigned char myAck=0;
unsigned int tmsb,tlsb;
unsigned char msb, lsb;
tlsb = tmsb = menAddress;
tmsb = tmsb >> 8;
tlsb = (tlsb <<>> 8;
msb = (unsigned char) tmsb;
lsb = (unsigned char) tlsb;
startCondition();
if(!(myAck=writeByte(deviceAddress,0))) {
if(!(myAck=writeByte(msb,0))) {
if(!(myAck=writeByte(lsb,0))) {
startCondition();
if(!(myAck=writeByte((deviceAddress|0x01),1))) {
myAck=getByte();
}
}
}
}
stopCondition();
return(myAck);
}
-------------- Fim 24C256.c --------------

Aqui eis um link para a especificação do Barramento I2C no site da NXP (Philips).
http://www.nxp.com/acrobat_download/literature/9398/39340011.pdf

Enjoy!!!

Um comentário:

  1. muito bom
    foi de grande valia, afinal e dificil alguem ter a iniciativa de compartilhar conhecimento

    ResponderExcluir