Sensor de ultrasonidos I2C.



Unos de los sensores del hexápodo va a ser un sensor para medir distancias por ultrasonidos que se comunica con el microcontrolador mediante el protocolo I2C, llamado TWI en los AVR.

Bus I2C.
Se trata de un protocolo serie desarrollado por Philips Semiconductors usado por muchos integrados para comunicarse entre ellos, para su funcionamiento requiere sólo dos líneas, una de reloj (SCL) y otra de datos (SDA) junto a dos resitencias de pull-up con cada una de estas líneas. Es un protocolo maestro esclavo en el que  el maestro inicia/termina la comunicación y debe generar una señal de reloj (SCL), la línea de datos (SDA) es bidireccional (el maestro puede mandar o recibir), por lo general sólo suele haber un maestro (el microcontrolador) aunque el protocolo soporta más de uno. Todos los circuitos integrados conectados a este bus tienen una dirección física distinta de la de  los demás. Tansmisiones de 8 bits en serie pueden ser realizadas a 100 kbit/s en el modo standar, 400 kbit/s en el modo rápido y 3.4 Mbits/s en High/Speed mode.

Existen todo tipo de circuitos integrados con un bus I2C, termómetros, memorias, relojes de tiempo real, drivers de displays, etc.. y en nuestro caso el sensor de ultrasonidos que vamos a utilizar entrega sus mediciones a través de este bus. Por lo que conocer como funciona el bus y aprender a usarlo es interesante en esto de la robótica.

En los AVR en protocolo I2C lo encontramos con el nombre de TWI, nos permite conectar hasta 128 integrados al bus (límite capacitancia del bus 400 pF) usando sólo dos líneas y añadiendo unas resistencias de pull-up.



Las salidas de todos los dispositivos conectados al bus son en colector-abierto, por lo que cualquier integrado puede mantener las líneas a nivel bajo activando sus salidas.


Cada bit que se manda por la línea SDA va acompañado de un pulso de reloj en la línea SCL, el valor del bit se toma en la parte alta del reloj.

La condición de inicio de transmisión tiene lugar cuando un integrado pone la línea de datos a cero estando la línea de reloj a uno, el final de transmisión se indica poniendo la línea de datos a uno con el reloj a uno. El maestro se encarga de generar estas señales que detectaran los integrados diseñados para I2C concetados al bus. Si el maestro genera una señal de Start en lugar del Stop correspondiente significa que va a seguir utilizando el bus e inicia otra transmisión con otro esclavo.



Para seleccionar el esclavo con el que el maestro quiere hablar después de la condición de Start, éste manda un byte con los 7 primeros bits indicando la dirección del esclavo (MSB el primero) y el último byte indica si va a mandar información o a leer del esclavo, es un bit de escritura/lectura. Después de cada byte siempre el receptor debe generar un bit de reconocimiento, esta señal de reconocimiento consiste en que el receptor pone la línea SDA a nivel bajo después de recibir el último bit del byte enviado, si el esclavo está ocupado o no puede recibir otro byte mantiene la línea SDA a nivel alto y el master envia una señal de Stop o una nueva señal de Start para iniciar otra transmisión.

La dirección 00000000 está reservada para una llamada a todos los esclavos, ésto se hace cuando el maestro quiere transmitir el mismo mensaje a varios esclavos en el bus, evidentemente una llamada a varios esclavos debe de indicar una operación de escritura en el octavo bit.





El esclavo en una transmisión puede poner la línea de reloj a cero si el tiempo de reloj generado por el master es demasiado rápido para el esclavo, lo que hará disminuir la velocidad de la transmisión.



El bus puede tener varios maestros pero en cada transmisión sólo puede haber uno, cuando dos o mas maestros intentan iniciar una comunicación al mismo tiempo hay un proceso de selección para que sólo uno de ellos pueda coger el control del bus, el resto de maestros deben cambiar a modo esclavo para ver si el master que tiene el control quiere hablar con ellos como esclavos. Los períodos de reloj que genera cada maestro deben de estar sincronizados para facilitar el proceso de selección, de esta forma la combinación de los distintos períodos de los relojes tendra la duración del período de reloj del maestro más lento en la parte baja del reloj (al ser las salidas de cada dispositivo a las líneas en colector abierto el cero de un dispositivo se impone al uno del otro) y la duración del reloj más rápido en la parte alta. Para seleccionar que maestro coge el control del bus estos miran la línea SDA, si el valor de la línea SDA en cada ciclo alto de reloj no coincide con el valor que ellos han puesto pierden el proceso de selección (es decir cuando ellos quieran poner un 1 en SDA y lean un 0).



En sistemas con varios maestros todas las transmisiones deben tener el mismo número de paquetes (paquete = byte + bit ack) siendo el primero el byte de dirección.

Módulo TWI en el AVR.

Está compuesto de varios submodulos:

SCL y SDA són los pines donde tenemos las líneas del bus, se pueden activar las resistencias de pull-up internas del puerto evitando en algunas ocasiones tener que colocar las dos resistencias de pull-up externas.

El "Bit rate generator" lleva el período del reloj cuando el micro está funcionando como maestro, la frecuencia de reloj de la CPU del esclavo debe ser de almenos 16 veces mayor que la frecuencia de reloj que generamos en la línea SCL, el esclavo siempre pude prolongar el tiempo de la parte baja del reloj si éste es demasiado rápido.

La frecuencia de reloj en este módulo se genera atendiendo a la siguien ecuación:


Las resistencias del bus se deben elegir en función de esta frecuencia y de la capacitancia del bus:



Bus Interface Unit:  contiene los datos a ser transmitidos o recibidos, se encarga del bit de reconocimiento, el controlador de las señales de Start y Stop y el hard para la selección de un maestro cuando requieren el control del bus varios al mismo tiempo.
Address Match Unit: comprueba si los bits de dirección que se reciben son los nuestros.
Control: Unit:  se encarga del control del bus.

El funcionamiento del módulo TWI está basado en interrupciones que debe atender el micro, una ejemplo transmisión sería algo así:, siendo TWINT el flag de interrupción

1. El primer paso en toda transmisión es generar la señal de Start, para ello se escribe un valor especifico en el registro TWCR que indica al hardware generar la condición de Start, el bit TWINT del registro TWCR se limpia y el hard comienza a generar la condición de Start.
2. Cuando la señal de Start se ha transmitido el bit TWINT se pone a uno y el registro TWSR es actualizado con un código/valor que indica que se ha transmitido una condición de Start.
3. La aplicación comprueba el registro TWSR para ver que se ha mandado el Start y carga la dirección del esclavo + el bit de leer/escribir en TWDR y escribe un código en TWCR que indica al hard que mande el valor contenido en TWDR. Se limpia el bit TWINT y se inicia la transmisión.
4. Cuando se ha transmitido el paquete el bit TWINT se pone a uno y TWSR es actualizado con un valor que indica que el byte se ha transmitido de forma correcta reflejando el valor del bit de reconocimiento.
5. El micro comprueba el registro TWSR y el valor del bit ack para ver lo que ha sucedido en el paso anterior, si todo ha sucedido según lo esperado se carga el byte de datos en TWDR, se escribe en TWCR el código para que el hard lo mande y se limpia el bit TWINT para iniciar la transmisión.
6. Cuando se manda el paquete de datos se pone a 1 el bit TWINT y se actualiza el valor de TWSR con el resultado de la transmisión.
7. La aplicación comprueba TWSR para ver lo que ha pasado y carga en TWRC el código para indicarle al hardware que mande la condición de Stop una vez que la aplicación limpie el bit TWINT. TWINT no es puesto a uno como resultado de mandar la condición de Stop.

Resumiendo:
Cuando el módulo TWI ha finalizado una operación y espera una respuesta del micro pone a 1 el bit TWINT, el reloj SCL está a nivel bajo hasta que este bit es limpiado, se limpia escribiendo un 1 lógico en él.
Cuando TWINT se ha activado el micro debe actualizar los registros del módulo con los valores necesarios para la siguiente operación en el próximo ciclo del bus. 
Una vez que se limpia TWINT el hardware ejecuta la operación establecida en los registros anteriores.

El módulo TWI tiene 4 modos de operación: tranmitr siendo maestro (MT), recibir siendo maestro (MR), transmitir siendo esclavo (ST) y recibir siendo esclavo (SR).

Master Transmitter Mode (MT).
En este modo se tranmiten un número de bytes a un esclavo, dependiendo del octavo bit del primer byte R/W se determina si se entra en este modo (MT) o en el modo MR

Los códigos a escribir en TWRC son los siguientes:

Cuando el bus esté libre se genera la señal de Start.

Para enviar un byte debemos escribir lo siguiente en el registro TWRC, una vez que hayamos modificado el registro se limpia TWINT escribiendo un uno, no antes.

El byte se pone en el registro TWDR y el resultado de la operación se guarda en TWSR.

Para generar la condición de Stop hay que escribir el siguiente código/valor en el registro TECR:


Cada vez que se realiza una operación cargada en TWCR se guarda el resultado de la operación en TWSR, pudiendo obtener los siguientes resultados:


Master Receiver Mode (MR).
En  este modo el microcontrolador se encarga de generar la señal SCL y leer los datos que un esclavo pone en SDA, los códigos a escribir en TWCR para este modo son los siguientes:


Transmitir el byte de dirección con el bit de escritura/lectura (en este caso debe ser lectura si no entramos en el modo anterior) y leer sucesivos bytes:


Señal de Stop:


Los códigos son los mismos que en el caso anterior.

Los posibles códigos de TWSR:


Slave Receiver Mode (SR)
En este modo el micro no genera el reloj, se comporta como esclavo y lee datos mandados por un maestro.

Para poder entrar en este modo los registros TWAR y TWCR deben contener los siguientes valores:

En los 7 bits más altos tenemos la dirección que identifica al microcontrolador en el bus I2C, si el bit TWGCE está a 1 el micro responde a la dirección general 0x00, si no ignorara una señal de Start en el bus seguida de esta dirección.


Una vez que hemos escrito estos valores el módulo espera a una señal de Start seguida del byte con su dirección, si el bit de R/W es 0 el módulo funciona en este modo, si es 1 en modo ST (transmite). Después de recibir cada byte se pone un código de operación en el registro TWSR , si se resetea el bit TWEA durante la transmisión el bit de reconocimiento que debe generar el micro será un 0, indicando que no puede recibir más bytes, mientras este bit esté a cero el módulo no responde al bus I2C.

Los códigos según el resultado de la operación para el registro TWSR son los siguientes:


Slave Transmitter Mode (ST).

En este modo como en el anterior no se genera la señal de reloj en SCL, los registros se deben configurar como en el caso anterior siendo el bit de R/W del byte de dirección el que selecciona entre un modo u otro. Si el bit TWEA es puesto a cero durante la transmisión el módulo envia el último byte y se desconecta del bus mientras el bit permanezaca en este estado, si el master sigue leyendo de él siempre encontrará un uno en la línea SDA.

Lós códigos que toma el registro TWSR según el resultado de cada acción son los siguientes:



Otros estados.


El estado 0x00 indica que ha ocurrido un error en el bus provocado por porque ha tenido lugar donde no debe una señal de Stop o Start. El bit TWINT se activa, para salir de este estado se debe limpiar este bit junto a TWSTO.


Registros del microcontrolador.

Los registros del micro necesarios para realizar una comunicación mediante el bus I2C/TWI son los siguientes:


Estos bits son el valor del divisor de frecuencia utilizado para generar la señal de reloj en modo maestro.



Bit 7, TWINT (TWI Interrupt Flag): se activa por hardware para indicar que el módulo TWI ha finalizado la tarea encomendada y está a la espera de instrucciones del programa saltando a la interrupción si está habilitada. Mientras este bit esté a 1 el reloj SCL está a nivel bajo. Cuando se ejecuta la interrupción TWINT no se limpia por lo que hay que hacerlo por soft escribiendo un 1 en el bit, cuando lo limpiamos el módulo TWI comienza la siguiente operación, determinada por los registros TWAR, TWSR y TWDR que deden de ser modificados antes de limpiar el bit.
Bit 6, TWEA: TWI (Enable Acknowledge Bit): si está a 1 el bit de "reconocimiento" es generado si la dirección en el bus I2C del micro o la 0x00 (si está habilitada) ha sido recibida o si un byte de datos ha sido recibido en el modo MR o SR. Escribiendo un 0 en este bit se desconecta el módulo TWI del micro del bus.
Bit 5, TWSTA: TWI (START Condition Bit):  se pone a 1 cuando se quiere ser maestro y generar la señal de Start cuando el bus esté libre, se debe limpiar por software una vez que se ha transmitido la señal de Start.
Bit 4, TWSTO: TWI (STOP Condition Bit): si se pone un 1 estando en modo maestro se genera la señal de Stop, el bit se limpia por hard cuando ésta ocurre. En modo esclavo se utiliza para salir de un estado de error en el bus (0x00).
Bit 3, TWWC: TWI (Write Collision Flag):  se pone a 1 si se intenta escribir en TWDR estando TWINT a cero, este bit se limpia cuando se escrie en el registro TWDR estnado TWINT a 1.
Bit 2, TWEN: TWI (Enable Bit): enciende o apaga el módulo TWI del microcontrolador.
Bit 0, TWIE: TWI (Interrupt Enable):  habilita la interrupción del módulo TWI, cuando TWINT se ponga a 1 se lanza la ISR si ésta está habilitada.



Bits 7:3, TWS: TWI Status: indican el estado del módulo TWI según los distintos códigos anteriores.
Bits 1:0, TWPS: (TWI Prescaler Bits):  controlar el prescaler.




En el modo de transmisión contiene el byte que se va a transmitir y en el modo de recepción contiene el byte que se ha recibido.



Bits 7:1, TWAM: (TWI Address Mask): indican la dirección del micro en el bus I2C.
Bit 0, TWGCE: si está a 1 el micro responde a la dirección "global" 0x00.


Bits 7:1 habilitan o deshabilitan los bits anteriores uno a uno.





Sensor SRF10.

El SRF10 es un sensor de ultrasonidos para medir distancias entre 3 cm y 6 m, trae conexión a un bus I2C para comunicarse con él, consumo entre 3 y 275 mA y una alimetación de 5 V. Su coste anda sobre los $60 por lo que para el robot hexápodo es caro ya que habrá sensores de menor coste que se adapten mejor a este tipo de robot (no necesitamos un rango de distancias de 6 m) pero lo tenía por aquí y viene bien para probar el bus I2C del Arduino Mega.

Todo dispositivo conectado a un bus I2C deben tener una dirección única, este sensor por defecto trae la dirección 0xE0 (1000 0000 observar que el bit más bajo en el byte de dirección es el de R/W, al asignar una dirección del dispositivo ese bit siempre lo tomamos como cero, no podemos tener una dirección como 0xE1 por ejemplo), nos permite asignar al sensor una de las siguientes direcciones: E0, E2, E4, E6, E8, EA, EC, EE, F0, F2, F4, F6, F8, FA, FC o FE, por lo que podríamos conectar hasta 16 sensores de este modelo a un bus I2C.

El sensor y sus conexiones en la siguiente imagen:


El sensor funciona siempre como un esclavo, nunca hace de maestro y se recomienda añadir un par de resistencias de pull-up de 1.8K para su funcionamiento en las líneas del bus I2C.

El sensor tiene su propio microcontrolador, un pic16F, con el que estableceremos la comunicación I2C. Tiene 4 registros:



El primer registro se utiliza para comenzar con las medidas obteniendo el resultado en los registros 2 y 3, su significado depende del comando utilizado para iniciar las medidas. Podemos escribir los siguientes comandos en el primer registro:



Si escribimos el valor 80 (0101 0000) en el "Command Register" el valor escrito en los registros 2 y 3 se corresponden con un valor en pulgadas, de la misma forma podemos obtener los resultados en cm o uS escribiendo los valores 81 y 82 en el command register.

Los otros tres valores que se pueden enviar al micro esclavo son para seleccionar una dirección I2C del sensor entre las 16 posible, para realizar el cambio de dirección sólo se puede tener conectado un sensor la bus (supongo que una vez cambiado se puede conectar otro nuevo al bus y asignar su dirección) y se le hace mandando los comandos anteriores en orden, por ejemplo si queremos cambiar la dirección a la 0xF2 debemos escribir los siguientes bytes en orden sobre el esclavo en 4 operaciones de escritura (es decir con su señal de start y stop): 0xA0, 0xAA, 0xA5, 0xF2, la dirección del sensor antes de cambiarla es la que viene por defecto 0xE0.

Para iniciar una medición se ha de escribir uno de los 3 primeros valores en el registro de comandos y transurrido un tiempo se podrá leer el resultado del esclavo, el tiempo por defecto es de 65 ms que se puede modificar escribiendo en el registro 2 (range register) antes de iniciar la medición, si, el sensor no responde a ningún comando mientras está realizando la medición.

El tiempo de 65 ms se corresponde con un rango a medir de hasta 11 metros, para reducir la distancia máxima  (no es capaz de detectar objetos a mas de 6  metros) y reducir el tiempo que tarda en hacerse una medición debemos escribir en el registro 2, un incremento o decremento en este registro representa un paso de 43 mm, es decir el rango máximo que se va a medir es el valor del registro dos (range register)*43mm + 43mm, si escribimos un valor de 0x18 podemos detectar objetos a una distancia de un metro, la única ventaja de disminuir el rango es que realizamos las mediciones en menos tiempo. Cada vez que se enciende el sensor el rango de medida se establece a 11 m y 65 ms.

El registro 1 (Max Gain Register) sirve para fijar el máximo valor de la ganancia que amplifica la señal que se recibe o algo así explica la hoja de datos que he encontrado del sensor que deja mucho que desear..  Durante cada medición la ganancia de cada etapa analógica (es decir el valor por el que multiplicamos la diferencia de potencial (supongo) que crea el eco recibido para que se entere el micro que ha llegado) empieza en su valor mínimo 40, y cada 96 uS va aumentando al siguiente valor de la tabla para detectar ecos producidos por objetos más lejanos y por tanto más débiles, aumenta hasta el máximo valor fijado en este registro. El propósito de todo ésto es poder utilizar el sensor con tiempos menores de 65 uS, es decir si no queremos detectar objetos más lejanos de 1 m de distancia y queremos realizar la medición lo más rápido posible seleccionaremos un intervalo de tiempo menor escribiendo en el registro 2, de esta manera detectamos por ejemplo un objeto a 80 cm y nada más leer el valor de distancia iniciamos otra medición, la onda generada sigue viajando y rebota en un objeto a 6 m lo que puede hacer que nos afecte a la segunda medición si recibimos el eco del objeto lejano perteneciente a la primera medición antes que el eco del objeto cercano perteneciente a la segunda medición. El eco del objeto lejano es mucho más débil que el del objeto cercano, por lo que si disminuimos el valos máximo de la ganancia, es decir la sensibilidad a los ecos del sensor, podemos ignorar el débil eco lejano evitando obtener lecturas falsas que indicaría un objeto que no existe antes de los 80 cm.. La relación entre la distancia máxima que queramos medir y la ganancia indicada para esta distancia no es lineal, por lo que no hay una fórmula que nos de una ganancia para un rango de distancias, el eco depende de la geometría, orientación del objeto que tengamos delante. Si no queremos preocuparnos de falsas medidas lo mejor es dejar los valores de ganancia y rango como se quedan cuando se enciende el sensor y no modificarlos haciendo mediciones cada 65 ms que para la mayoría de los casos es suficiente.

Valores que establecen la ganancia máxima en el registro 1.


El led de la placa nos incida la dirección en el bus I2C del sensor cuando encendemos éste y no recibe comandos I2C, según las veces que se encienda y se apague tenemos las siguientes direcciones:



Por últimos los objetos que detectará el sensor siempre que estén colocados de maneta perpendicular al eco (si les pilla de lado el ángulo de sálida es el de incidencia por lo que podemos no detectar un objeto que tengamos delante) deben de estar en este cono:



Por lo que para evitar ecos producidos por rebotes una opción es disminuir la ganancia máxima, si queremos detectar objetos a 1 m de distancia pues probablemente no tenga sentido usar la máxima, a no ser que éstos sean muy pequeños.

Resumiendo la hoja de datos que he encontrado.. para escribir un comando mandamos la señal de Start, la dirección del sensor en el bus indicando escritura, un byte con el número de registro en el que vamos a escribir para seleccionar el registro interno, el comando a escribir y la señal de Stop para finalizar.
Para leer mandamos la señal de Start, un byte con la dirección i2c del sensor en el bus indicando que vamos a escribir, un byte con el número de registro en el que vamos a comenzar a leer y una señal de Stop. A continuación mandamos una nueva señal de Start, un byte con la dirección i2c del sensor indicando que vamos a leer y comenzamos la lectura en el bus, una vez leído el primer registro se pasa de manera automática a leer el segundo finalizando con una señal de Stop, no mandando antes del Stop el byte de ack en el último byte recibido para indicarle al esclavo que vamos a finalizar.

Ejemplo de uso de todo lo anterior.

Un programa simple para comprobar el funcionamiento del bus I2C y del sensor SRF10, se va a leer el sensor y sacar la medición por el LCD de manera continua. Para ello primero debemos activar el módulo TWI en el micro para poder configurar el sensor, iniciar la medición y leer el resultado.

Nuestro microcontrolador va a funcionar como maestro y el sensor como esclavo por lo que debe generar la señal de reloj, lo primero es determinar la frecuencia de la señal y debemos atender a la siguiente tabla para ver los requisitos del bus I2C:




Vamos a seleccionar los requisitos de una frecuencia de reloj menor de 100 KHz, para ver cuál es la velocidad máxima a la que podemos intercambiar datos habría que mirar el datasheet del pic16f en el sensor para ver sus requisitos ya que información detallada sobre el sensor hay poca por no decir ninguna, o almenos yo no la he encontrado. En la tabla vemos un tiempo mínimo para la parte alta de SCL de 4 us y para la baja de 4.7 us (el esclavo puede hacer que sea más largo si es muy rápido para él poniendo la línea de reloj a cero), además están los tiempos para las señales de Start, Stop, el tiempo del bit, etc..

La frecuencia a la que va a funcionar nuestro reloj la determinamos según la siguiente fórmula:


El reloj externo del micro (CPU Clock frequency) es de 16 MHz por lo que si dejamos los registros TWBR y TWPS a 0 tendríamos una frecuencia de 1MHz, es decir la señal de reloj tiene un período de 1 us por lo que no nos vale, como mínimo necesitamos un período de 10 us (5us en la parte alta + 5 us en la parte baja), vamos a generar un período de reloj de 20 us, para ello debemos escribir en TWPS el valor 0 (son dos bits) y en TWBR el valor 152, por lo que la frecuencia según la fórmula es la siguiente, SCLf = (16000000)/(16+2*(152)*1) = 50KHz, es decir el período de 20 us.

Luego en el registro TWBR escribimos el valor 10011000 y en los bits TWSP1 y TWSP0 del registro TWSR el valor 0, de esta formas configuramos una señal de reloj en la línea SCL de 20 us que el micro genera cuando funcione como maestro. En el registro TWCR activamos los bits TWEA y TWEN para conectar el módulo al bus y habilitar a la línea. TWAR y TWAMR sirven para asignar la dirección cuando podemos ser esclavo, en este caso solo vamos a ser maestros, con TWIE de TWCR y el bit GIE de SREG del mismo registro habilitamos la interrupción que usaremos durante las transmisiones. Cada vez que se ejecute una operación del módulo se activa el flag de interrupción TWINT y se salta a la ISR, ISR(TWI_vect); en este caso. La transmisión la podemos hacer mediante instrucciones o sin ellas haciendo al micro esperar hasta que se active el flag anterior, lo más práctico es usar interrupciones y dejar al micro ejecutar otro código en el tiempo de la transmisión, para probar el sensor se va a usar el ejemplo más simple que es sin interrupción.

Para que el micro hable con el sensor se van a crear dos funciones, una para escribir y otra para leer:

Funciones I2C SRF10:


void i2c_leer(unsigned char direccion, unsigned char direccion_leer);
void i2c_escribir(unsigned char direccion, unsigned char *byte, unsigned char numero_bytes);


void i2c_inicializar()
{
    TWBR = 0b10011000;    //Período SCL 20 us
}

Establece el período del reloj de la señal 20 us en este caso.

Para escribir n bytes en un bus I2C como maestro:

void i2c_escribir(unsigned char direccion, unsigned char *byte, unsigned char numero_bytes)
{
    unsigned char *dato = byte;
    unsigned char i=0;
   
    TWCR |= (1<<TWEA);                                                           //Se enciende el módulo
    TWCR &= ~(1<<TWSTO);                                                    //enviar señal de start.
    TWCR |= ((1<<TWINT)|(1<<TWSTA)|(1<<TWEN));

    while (!(TWCR & (1<<TWINT)))                                           //Esperar a TWINT
    {
    }
                                                                                                  //comprobar resultado de la operación anterior.
    if ((TWSR & 0xF8) != 0x08)                                                 //(xxxxx000) != (0x08 Start)
    i2c_error();

                                        //enviar byte.
    TWDR = ((direccion)&(0b11111110));                                //Cargar byte a enviar
    TWCR &= ~((1<<TWSTO)|(1<<TWSTA));
    TWCR |= ((1<<TWINT)|(1<<TWEN));

    while (!(TWCR & (1<<TWINT)))                                        //Esperar a TWINT
    {
    }
                                                                                                //comprobar condición.
    if ((TWSR & 0xF8) != 0x18)                                                //(xxxxx000) != (0x18 SLA +W)
        i2c_error();

    for(i=0;i<numero_bytes;i++)                                                //Enviar bytes
    {
        TWDR = dato[i];                                                            //Cargar byte a enviar
        TWCR &= ~((1<<TWSTO)|(1<<TWSTA));
        TWCR |= ((1<<TWINT)|(1<<TWEN));

        while (!(TWCR & (1<<TWINT)))    //Esperar a TWINT
        {
        }
                                                                                            //comprobar condición.
        if ((TWSR & 0xF8) != 0x28)                                       //(xxxxx000) != (0x28 dato+ack)
            i2c_error();   
    }                                       
    TWCR &= ~(1<<TWSTA);                                            //enviar señal de stop.
    TWCR |= ((1<<TWINT)|(1<<TWEN)|(1<<TWSTO));
    _delay_us(20);
}


Conectando un analizador lógico a la línea I2C se observa la siguiente salida cuando se ejecuta está función, los parámetros que se le pasan son 0xE0 y dos bytes a escribir 0x00 y 0x51:




La primera parte del código es la señal de Start y el primer byte de direccióny  se corresponde con el comienzo de la imagen en el bus i2c, el período de la señal es de 20 us como se configuro en TWBR, los otros bits del prescaler  TWPS1:0 están a cero por defecto:



   TWCR |= (1<<TWEA);                                                           //Se enciende el módulo
    TWCR &= ~(1<<TWSTO);                                                    //enviar señal de start.
    TWCR |= ((1<<TWINT)|(1<<TWSTA)|(1<<TWEN));
    while (!(TWCR & (1<<TWINT)))                                           //Esperar a TWINT
    {
    }
                                                                                                  //comprobar resultado de la operación anterior.
    if ((TWSR & 0xF8) != 0x08)                                                 //(xxxxx000) != (0x08 Start)
    i2c_error();


Lo primero es generar la señal de Start, ésta tiene lugar cuando estando la línea de SCL a 1 la línea de SDA(la de arriba) pasa de 1 a 0, para ello ponemos los bits del registro TWCR con sus valores correspondientes para indicar el START y limpiamos TWINT escribiendo un uno lógico en él, ésto indica al módulo TWI del micro que tiene que ejecutar una acción determinada por el resto de bits del registro. Con  
while (!(TWCR & (1<<TWINT)))   esperamos a que el bit TWINT del registro TWCR se ponga a 1 para indicarnos que ya se ha enviado la señal de Start, cada acción que realiza el módulo TWI indica su resultado en los 5 bits más altos del registro TWSR, por lo que enmascaramos estos bits y leemos su estado comparandolo con el valor 0x08 que nos pone el módulo TWI en estos bits para decirnos que una señal de Start se ha transmitido. Ésto es un ejemplo sencillo y se usa un while para esperar al bit, el micro se queda ahí parado sin hacer nada, en el programa completo habrá que usar la interrupción en lugar del búcle de espera. Si no recibimos el valor 0x08 salimos de la función generando una señal de Stop en la función i2c_error();

Lo siguiente es mandar el byte de dirección con el bit de escritura y lectura:

                                                                                                  //enviar byte.
    TWDR = ((direccion)&(0b11111110));                                //Cargar byte a enviar
    TWCR &= ~((1<<TWSTO)|(1<<TWSTA));
    TWCR |= ((1<<TWINT)|(1<<TWEN));
    while (!(TWCR & (1<<TWINT)))                                        //Esperar a TWINT
    {
    }
                                                                                                //comprobar condición.
    if ((TWSR & 0xF8) != 0x18)                                                //(xxxxx000) != (0x18 SLA +W)
        i2c_error();


En TWDR cargamos la dirección de nuestro esclavo 0xE0 y la pasamos por una AND para que el bit más bajo el de R/W valga siempre 0 indicando escritura. El bit se lee en la parte alta del reloj, por lo que la imagen se puede leer el valor: 11100000 es los 8 pulsos de reloj, es decir 0xE0 que hemos mandando, sin embargo el analizador nos indica que hemos mandado la dirección 0x70 y ésto es asi porque cuenta sólo 7 bits de dirección 1110000 (0x70) y el bit más bajo de lectura/escritura independiente de la dirección, por lo que la dirección del bus no se corresponde con la dirección que indica los sensores, en los sensores se han añadido este bit de R/W de más al nombrar las direcciones.

En el pulso 9 de reloj el esclavo debe poner la línea SDA a 0 para mandar el bit de ack, se puede observar como nada más finalizar la parte alta del pulso 8 se produce un pequeño pulso en SDA, el master pone a 1 la línea (la libera en su open collector) e inmediatamente el esclavo la pone a cero para indicar que ha recibido el dato y que se le pueden mandar más, en la parte alta del 9 pulso se leer el bit de reconocimiento. El otro pulso pequeño que se ve seguido al anterior es porque se manda otra byte a continuación y su primer bit vale 0 (no tiene importancia).

Como en el caso anterior esperamos a que el bit TWINT se ponga a 1 indicando que el módulo TWI ha realizado la operación que le hemos indicado y comprobamos el estado de ésta leyendo los bits de TWSR, el valor 0x18 lo ha puesto el módulo TWI indicando que ha mandado la dirección y que ha recibido un bit de ack por parte del esclavo.

Siguiendo los mismos pasos se mandan dos bytes más de datos del maestro al esclavo y se genera una señal de Stop (SDA pasa de 0 a 1 con SCL a 1) para finalizar la transmisión y dejar libre el bus I2C, el módulo TWI nos devuelve el código 0x28 indicando que el dato se ha mandado y el esclavo ha generado el bit de reconocimiento.

   for(i=0;i<numero_bytes;i++)                                                //Enviar bytes
    {
        TWDR = dato[i];                                                            //Cargar byte a enviar
        TWCR &= ~((1<<TWSTO)|(1<<TWSTA));
        TWCR |= ((1<<TWINT)|(1<<TWEN));

        while (!(TWCR & (1<<TWINT)))    //Esperar a TWINT
        {
        }
                                                                                            //comprobar condición.
        if ((TWSR & 0xF8) != 0x28)                                       //(xxxxx000) != (0x28 dato+ack)
            i2c_error();   
    }                                       
    TWCR &= ~(1<<TWSTA);                                            //enviar señal de stop.
    TWCR |= ((1<<TWINT)|(1<<TWEN)|(1<<TWSTO));
    _delay_us(20);

Después de la señal de Stop hay que generar un pequeño delay (4us) para que ésta sea detectada, ya que el bit TWINT del módulo no se pone a 1 después de generar un STOP para indicar que se ha realizado.





La siguiente función se utiliza para leer en el sensor, para ello primero hay que escribir un valor en el esclavo para seleccionar el registro que vamos a leer y a continuación comenzar a leer bytes. Se podía haber usado la función anterior para la primera parte de la escritura.


void i2c_leer(unsigned char direccion, unsigned char direccion_leer)
{
    TWCR |= (1<<TWEA);
    TWCR &= ~(1<<TWSTO);                //enviar señal de start.
    TWCR |= ((1<<TWINT)|(1<<TWSTA)|(1<<TWEN));

    while (!(TWCR & (1<<TWINT)))        //Esperar a TWINT
    {
    }
                                        //comprobar resultado de la operación anterior.
    if ((TWSR & 0xF8) != 0x08)             //(xxxxx000) != (0x08 Start)
    i2c_error();

    TWDR = ((direccion)&(0b11111110));    //Cargar byte a enviar, escritura
    TWCR &= ~((1<<TWSTO)|(1<<TWSTA));
    TWCR |= ((1<<TWINT)|(1<<TWEN));

    while (!(TWCR & (1<<TWINT)))        //Esperar a TWINT
    {
    }
                                        //comprobar condición.
    if ((TWSR & 0xF8) != 0x18)             //(xxxxx000) != (0x18 SLA +W)
        i2c_error();

    TWDR = direccion_leer;                //Cargar byte a enviar
    TWCR &= ~((1<<TWSTO)|(1<<TWSTA));
    TWCR |= ((1<<TWINT)|(1<<TWEN));

    while (!(TWCR & (1<<TWINT)))        //Esperar a TWINT
    {
    }
                                        //comprobar condición.
    if ((TWSR & 0xF8) != 0x28)            //(xxxxx000) != (0x28 dato+ack)
        i2c_error();   
       
    TWCR &= ~(1<<TWSTA);                //enviar señal de stop.
    TWCR |= ((1<<TWINT)|(1<<TWEN)|(1<<TWSTO));   
   
    _delay_us(40);

    /********************* Leemos **************************************/

    TWCR |= (1<<TWEA);   
    TWCR &= ~(1<<TWSTO);                //enviar señal de start.
    TWCR |= ((1<<TWINT)|(1<<TWSTA)|(1<<TWEN));

    while (!(TWCR & (1<<TWINT)))        //Esperar a TWINT
    {
    }
                                        //comprobar resultado de la operación anterior.
    if ((TWSR & 0xF8) != 0x08)             //(xxxxx000) != (0x08 Start)
    i2c_error();

                                        //enviar byte.
    TWDR = ((direccion)|(0b00000001));    //Cargar byte de dirección
    TWCR &= ~((1<<TWSTO)|(1<<TWSTA));
    TWCR |= ((1<<TWINT)|(1<<TWEN));

    while (!(TWCR & (1<<TWINT)))        //Esperar a TWINT
    {
    }
                                        //comprobar condición.
    if ((TWSR & 0xF8) != 0x40)             //(xxxxx000) != (0x40 SLA +R)
        i2c_error();

    TWCR &= ~((1<<TWSTO)|(1<<TWSTA));    //Leer primer byte
    TWCR |= ((1<<TWINT)|(1<<TWEN));

    while (!(TWCR & (1<<TWINT)))        //Esperar a TWINT
    {
    }
   
    if ((TWSR & 0xF8) != 0x50)             //(xxxxx000) != (0x50 dato+ack)
        i2c_error();

    bytes_i2c[3]=TWDR;   

    TWCR &= ~((1<<TWSTO)|(1<<TWSTA)|(1<<TWEA));    //Leer segundo byte NO ACK
    TWCR |= ((1<<TWINT)|(1<<TWEN));

    while (!(TWCR & (1<<TWINT)))        //Esperar a TWINT
    {
    }
   
    if ((TWSR & 0xF8) != 0x58)             //(xxxxx000) != (0x58 dato+ no ack)
        i2c_error();

    bytes_i2c[2]=TWDR;

    TWCR &= ~(1<<TWSTA);                //enviar señal de stop.
    TWCR |= ((1<<TWEA)|(1<<TWINT)|(1<<TWEN)|(1<<TWSTO));
}

La anterior función produce la siguiente salida en el bus:



Entre la primera señal de start y stop tenemos el mismo código que la función anterior, nuestro micro es master y escribe en el bus, seleccionamos la dirección del esclavo y escribimos el valor del registro del sensor (tiene 4) sobre el que vamos a realizar la operación de lectura/escritura, una vez que realizamos una operación el "contador" de registro del sensor se incrementa permitiendonos escribir o leer directamente en el siguiente registro sin tener que seleccionarlo. En este caso seleccionamos el registro 2 que según indica la documentación guarda la parte alta del resultado y lo leemos, a continuación hacemos otra operación de lectura directamente ya sobre el registro 3 del sensor sin tener que seleccionarlo, si volvemos a hacer otra operación a continuación al no haber más regitros lo haremos sobre el 3 también.

Entre el Stop y el Start hay que poner un delay para que establezcan las señales y puedan ser detectadas en el bus.

En cuanto a la parte del micro como maestro leyendo de un esclavo, se genera la señal de Start como en los casos anteriores, y se procede a mandar la dirección pero esta vez con el bit de R/W a 1:

   TWDR = ((direccion)|(0b00000001));    //Cargar byte de dirección
    TWCR &= ~((1<<TWSTO)|(1<<TWSTA));
    TWCR |= ((1<<TWINT)|(1<<TWEN));

    while (!(TWCR & (1<<TWINT)))        //Esperar a TWINT
    {
    }
                                        //comprobar condición.
    if ((TWSR & 0xF8) != 0x40)             //(xxxxx000) != (0x40 SLA +R)
        i2c_error();


El módulo nos devuelve el código 0x40 indicando que todo se ha realizado según los esperado.

Pra leer un byte y alamcenarlo en un registro de nuestro micro escribimos el siguiente código:

    TWCR &= ~((1<<TWSTO)|(1<<TWSTA));    //Leer primer byte
    TWCR |= ((1<<TWINT)|(1<<TWEN));

    while (!(TWCR & (1<<TWINT)))        //Esperar a TWINT
    {
    }
   
    if ((TWSR & 0xF8) != 0x50)             //(xxxxx000) != (0x50 dato+ack)
        i2c_error();

    bytes_i2c[3]=TWDR;

El byte que recibimos lo tenemos en TWDR y obtenemos el valor 0x50 que indica que todo ha sucedido según lo esperado. Para leer el segundo y último byte lo hacemos de la misma forma, pero esta vez tenemos que indicar al módulo TWI que no genere el bit de reconocimiento cuando reciba el byte para que el esclavo libere la línea y podamos generar la señal de Stop, para ello ponemos a cero el bit TWEA de TWCR.
    TWCR &= ~((1<<TWSTO)|(1<<TWSTA)|(1<<TWEA));    //Leer segundo byte NO ACK
    TWCR |= ((1<<TWINT)|(1<<TWEN));

    while (!(TWCR & (1<<TWINT)))        //Esperar a TWINT
    {
    }
   
    if ((TWSR & 0xF8) != 0x58)             //(xxxxx000) != (0x58 dato+ no ack)
        i2c_error();

    bytes_i2c[2]=TWDR;


Recibimos el valor 0x58 que indica que hemos recibido el byte y no se ha generado el bit ack, guardamos el byte y generamos la condición de Stop para dejar libre el bus.

No adjunto el código completo porque no sé si mi sensor está bien, en teoría y según la documentación que he sido capaz de encontrar del SRF10 el resultado de la medida se guarda en el registro 2 y 3 pero en el registro 2 siempre tengo el valor 0x00, no sé el porque. La verdad que la única documentación que he encontrado es la que hay en la página de superrobótica y el datasheet en inglés en otras páginas de robótica, que es de donde se ha traducido lo de superrobótica. No sé si habrá más info, ta haré una sesión de google.

Mi while es el siguiente:
    while(1)
    {
        bytes_i2c[0]=0x00;                   
        bytes_i2c[1]=0x51;
        i2c_escribir(0xE0, bytes_i2c, 2);
           
        _delay_ms(100);

        i2c_leer(0xE0,0x02);
           
        posicionar_cursor(2,9);
        escribir_registro(bytes_i2c[3]);
        posicionar_cursor(3,9);
        escribir_registro(bytes_i2c[2]);
        posicionar_cursor(4,12);
        valor_real_srf10(bytes_i2c[2]);

    }


En el registro 3 los valores se incrementan y decrementan correctamente con la distancia, pero en el 2 0x00 siempre, y si aplico los 4.3 cm por bit sobre los 255 valores del registro 3 me salen distnacias que no se corresponden, por lo que no sé si tengo el sensor roto. Pero bueno para el ejemplo del funcionamiento del bus I2C en el Arduino me vale ya que responde a las funciones, ya volveré a mirar más adelante el tema del sensor bien y hacer las funciones mediante la ISR que de momento me urge el tema del AVR que tengo un 3pi para destripar y aplicar lo aprendido a un miniz con AVR =).

Para cualquier correción, comentario:

blog comments powered by Disqus