Capítulo 1 GUÍA DE MANEJO DEL DRIVER DE LA TCS 1 Introducción El presente documento tiene como objetivo describir cómo se maneja el driver que permite realizar programas en una PDA para controlar a la TCS. Un driver no es más que un objeto que tiene un conjunto de funciones que permiten manejar un dispositivo y un conjunto de variables que permiten monitorizar el estado del mismo, sin necesidad de conocer cómo está hecho. Lo único que se necesita conocer es el conjunto de funciones del driver que son interfaz con el usuario. 2 Nociones básicas del driver La clase que maneja la TCS se llama Comm y se encuentra declarada en el fichero Comm.h y definida en el fichero Comm.cpp. Esta clase necesita el driver del puerto serie para poder funcionar, que es el medio de comunicación que el PC o la PDA tiene con la TCS. Este driver se encuentra en los ficheros Serial.h y Serial.cpp. Para entender el funcionamiento del driver es necesario entender qué es una comunicación entre el PC/PDA con la TCS. Una comunicación consiste de una petición de ejecución de comandos a la TCS y de una respuesta por parte de la TCS a esa petición; es decir, una comunicación implica el trasiego de dos flujos de datos: uno del PC/PDA a la TCS y otro en sentido contrario. Por ejemplo, una comunicación podría consistir en la petición a la TCS de la ejecución de dos comandos: que mueva los motores a una determinada velocidad y que le devuelva todas las medidas analógicas que tenga en ese momento; como contrapartida la TCS respondería con los valores de las medidas analógicas pedidas. Se pueden realizar tantas comunicaciones con la TCS como se quieran, de hecho el ejemplo anterior podría haberse llevado a cabo con dos comunicaciones en vez de con una, simplemente enviando un sólo comando en cada una de las comunicaciones. En cualquier caso, en este esquema de funcionamiento es la PDA la que actúa como maestro; es decir, toma la 1 iniciativa en las comunicaciones, y la TCS actúa como esclavo; es decir, responde a la PDA cuando esta se lo pide únicamente. 3 Variables y funciones del driver A continuación se describen las variables del driver que puede utilizar el programador cuando maneja el driver: • interrupcion: vector de cuatro elementos que informa de los datos no esperados que se reciben en una comunicación con la TCS; es decir, es información que no se ha pedido a la TCS y que en cambio la TCS decide mandar para informar al robot de eventos que ocurren y que debe conocer. Estos eventos se llaman interrupciones, aunque no lo son propiamente, porque surgen sin esperarse de la misma forma que las interrupciones, lo que lleva a tener que programar líneas de código que atiendan estos eventos. Cada uno de los elementos del vector tiene dos posibles estados: 1 si hay interrupción y 0 si no la hay. Las posibles fuentes de interrupción son: o interrupcion[1]: información recibida por radiofrecuencia. El PC/PDA no sabe cuándo puede recibir información por radio. Por ello cuando la TCS recibe información por radio, se lo comunica a la PDA. Para la recepción de mensajes por radio se usa el elemento 1 del vector interrupcion. o interrupcion[2]: medidas de los ultrasonidos. Los sensores de ultrasonidos no pueden estar midiendo de continuo ya que el transductor se desgasta con el uso. Esto lleva a realizar medidas de forma no discriminada y bajo petición. Dado que un sensor de ultrasonido puede tardar hasta 20ms en hacer una medida (por lo tanto 8 sensores pueden tardar hasta 160 ms), no se puede permitir que cada vez que la PDA pida medidas de ultrasonido tenga que esperar a que se obtenga el dato ya que estaría perdiendo un tiempo en el cual podría estar haciendo otra cosa y le podría llevar a que el robot se chocara. Por ello, se ha optado por gestionar los ultrasonidos como una interrupción; es decir, en una comunicación con la TCS, la PDA pide que se hagan ciertas medidas de ultrasonido, pero no espera a que se hagan, sino que sigue realizando 2 comunicaciones para pedir otro tipo de medidas a la TCS o para actuar. Cuando la TCS ha terminado de hacer las medidas, introduce los resultados en una comunicación con la PDA sin que ésta sepa a priori en qué comunicación la TCS le va a enviar las medidas. Para los sensores de ultrasonido se usa el elemento 2 del vector interrupción. o interrupcion[3]: medidas de sensores digitales. Este tipo de sensores miden 0 o 1. Como sólo existen dos posibles estados este tipo de sensores ofrecen medidas muy estáticas ya que cambian poco en el tiempo. Para evitar trasiego de información innecesaria en la comunicación entre el PC/PDA y la TCS, la TCS enviará a la PDA las medidas de los sensores digitales cuando alguno de estos cambie de estado. Por ejemplo, este tipo de sensores se suele usar para saber si el robot se ha chocado; en ese caso la TCS informará al robot sólo cuando se acabe de chocar o cuando deje de hacerlo. Para los sensores digitales se usa el elemento 3 del vector interrupcion. • compruebaPing: variable de dos estados que indica si al mandar el comando “ping” (comando que se usa comúnmente en comunicaciones para determinar si un dispositivo está vivo: ¿estas ahí?) a la TCS, éste ha respondido. A veces se usa para establecer una comunicación con la TCS, únicamente para saber si existe algún evento que deba ser recibido; ya que como se comentó en la sección 2 la TCS sólo puede mandar eventos si la PDA a tomado la iniciativa de abrir una comunicación. • RFrespuesta: variable de dos estados que indica si el último mensaje de radiofrecuencia espera una respuesta (RFrespuesta==1) o no (RFrespuesta==0). A continuación se describen las funciones del driver que permiten controlar y establecer una comunicación entre la PDA y la TCS: 1. inicio(): función que se debe llamar al principio de cualquier comunicación entre la PDA y la TCS. 2. comunica(): establece la comunicación de la PDA con la TCS, enviando todas las ordenes a la TCS y recibiendo la respuesta de la misma. En caso de que surja algún problema durante la comunicación devolverá un 0 y sino un 1. 3 A continuación se describen las funciones interfaz del driver que permiten enviar ordenes para manejar la TCS: 1. configuracion(número de ADs, número de USs): función que indica a la TCS cómo se configuran los pines de conexión con el exterior en función del número de sensores de cada tipo que se le manden en esta función. 2. leerAD(sensor): función que pide la medida de un sensor analógico que se manda como parámetro. El parámetro sensor va de 1 hasta el número de sensores analógicos que se hayan configurado. 3. leerADs(): función que pide todas las medidas de los sensores analógicos que se hayan configurado. 4. pedirUS(sensor): función que pide la medida de un sensor de ultrasonido que se manda como parámetro. El parámetro sensor va de 1 hasta el número de sensores de ultrasonidos que se hayan configurado. 5. pedirUSs(): función que pide todas las medidas de los sensores de ultrasonido que se hayan configurado. 6. activarMotores(Byte de configuración): activa los motores. Si el byte de configuración es 1 se activa el motor 1, si es 2 se activa el motor 0 y si es 3 se activan los dos motores. Para que un motor se ponga en funcionamiento es necesario haberlo activado. 7. periodoMotores(Periodo): indica a la TCS el periodo de la señal PWM que va a utilizar para controlar los motores. El parámetro tiene que ser un número que esté comprendido entre 70 y 255. Cuanto mayor sea el valor, mayor será el periodo. 8. periodoServos(Periodo): indica a la TCS el periodo de la señal PWM que va a utilizar para controlar los servos. Como los servos se controlan siempre a 50Hz, el valor del parámetro afecta poco al comportamiento del servo, ya que es un ajuste fino de la frecuencia entorno a 50Hz. Tiene que ser un número que esté comprendido entre 0 y 255; por ejemplo 200. 9. DcMotores(Dc motor1, Dc motor0): fija el valor de los anchos del pulso de la señales PWM que controlan los motores 0 y 1. Variando el ancho del pulso del PWM se puede variar la velocidad de los motores. El parámetro tiene que ser un número que esté comprendido entre 0 y el valor del periodo de 4 control de los motores. En concreto el valor en el que se encuentra parado el motor es cuando coincide con la mitad del periodo de control de los motores (por ejemplo 100 en caso de que el periodo sea 200); el valor en que el motor gira a la máxima velocidad en sentido negativo es 0; el valor en el que el motor gira a la máxima velocidad en sentido positivo es el que coincide con el valor del periodo de control; y valores intermedios hacen que el motor se mueva a una velocidad que se puede obtener interpolando entre los valores anteriormente citados. 10. DcServos(Dc servo1, Dc servo0): fija el valor de los anchos del pulso de la señales PWM que controlan los servos 0 y 1. Variando el ancho del pulso se puede variar el ángulo de giro de cada servo. El valor del ancho del pulso puede variar entre 50 y 240. Un valor de 50 hace que el servo se encuentre posicionado a -90º, en cambio un valor de 240 hace que el servo se encuentre posicionado a 90º. Valores intermedios hacen que el servo se posicione en un ángulo que se puede obtener interpolando entre los valores anteriormente citados. 11. Ping(): función que sirve para determinar si la TCS se encuentra en funcionamiento y que existe comunicación entre la PDA y la TCS. 12. escribeRadio(longitud_mensaje, mensaje, espera_respuesta): función que indica a la TCS que tiene que enviar un mensaje por radio. El parámetro longitud_mensaje indica el número de bytes que se quieren transmitir, el parámetro mensaje representa el mensaje a transmitir y el parámetro espera_respuesta indica si la PDA espera una respuesta de aquel a quien envía el mensaje. Funciones para leer las medidas de los sensores: 1. valorADs(): devuelve un vector de 10 elementos (10 bytes) con todas las medida analógicas que ha devuelto la TCS. 2. valorEncoders(): devuelve un vector de 4 elementos (4 bytes) con las medidas de los encoders. El primer byte es la parte menos significativa de la medida del encoder 0, el segundo byte es la parte más significativa del encoder 0, el tercer byte es la medida menos significativa del encoder 1 y el cuarto byte es la medida más significativa del encoder 1. 5 3. valorUSs(): devuelve un vector de 8 elementos (8 bytes) con todas las medidas de ultrasonido que se han pedido a la TCS. 4. valorDig(): devuelve un vector de 1 elemento (1 byte) con la medida de los ocho sensores digitales. Cada bit del elemento se corresponde con la medida de un sensor. 5. valorRF(nElementos): devuelve un vector de nElementos elementos con el mensaje que la TCS ha recibido por radio. 4 Instrucciones de manejo del objeto Comm A continuación se detallan las instrucciones para el manejo del driver desde una aplicación programada en C++, que será la que se ejecute en un PC o en una PDA. 4.1 Creación de objeto El primer paso es añadir los siguientes archivos al proyecto donde se va a realizar la aplicación deseada: • Comm.h: Definición de la clase Comm, encargada de la comunicación. • Comm.cpp: Código de la clase Comm. • Serial.h: Definición de la clase Serial, encargada del manejo del puerto serie. • Serial.cpp: Código de la clase Serial. Una vez añadidos, en el archivo del proyecto donde se vaya a crear el objeto de la clase Comm se introduce el código correspondiente, habiendo incluido anteriormente la cabecera Comm.h: #include “Comm.h” … Comm comunicacion; … De esta manera se ha creado el objeto comunicacion con el que se podrán establecer comunicaciones con la TCS. 4.2 Inicialización de la comunicación Antes de establecer una nueva comunicación lo primero que hay que hacer es llamar a al método inicio() del objeto Comm: comunicacion.inicio(); 6 Este método tiene como función vaciar el buffer de datos de envío. En caso de que no se ejecutara, al introducir las órdenes para la TCS, éstas se irían colocando tras las órdenes solicitadas en la comunicación anterior. 4.3 Introducción de las órdenes a enviar Una vez que se ha inicializado la comunicación, es necesario introducir las órdenes que se quieren mandar al microcontrolador. Para ello, tan sólo se tienen que ejecutar las funciones o métodos de la sección 3 que se encargan de mandar actuaciones o de pedir medidas. A la hora de ejecutar dichos métodos se debe tener en cuenta los siguientes aspectos: • En la primera comunicación con la TCS se debe configurar, enviando la orden configuracion que le indica el número de sensores de cada tipo que se van a utilizar. Si por ejemplo, se quiere usar sólo el sensor de ultrasonidos 3, se debe configurar el número de ultrasonidos al menos a 3, aunque el 2 y el 1 no se usen. Lo mismo sucede para los sensores analógicos. • En caso de que en la misma comunicación se envíe la orden de configuracion y además alguna otra orden para pedir la medida de sensores analógicos o ultrasonidos, siempre se deberá introducir primero la orden de configuracion, ya que si no, cuando en la TCS vaya a realizar las medidas no estará configurada de la forma adecuada. • En las funciones leerAD(sensor) y pedirUS(sensor), se debe tener en cuenta que el primer sensor se pide con un 1 y el último con el número de sensores totales configurados (no como los vectores en C++, que usan los índices que van de 0 al número de elementos – 1). • Si se solicita una medida de ultrasonidos antes de haber recibido la respuesta con medidas de una petición anterior, ésta se sobrescribe y no se sigue esperando. • Si no se han activado nunca los motores, es aconsejable que no se envíe solamente la orden activarMotores en una comunicación, sino que se acompañe con las órdenes que configuran el periodo de control y los la velocidad (duty cycle) de los mismos. De esta manera se evita que al activar los motores, se pongan en marcha de forma no controlada. 7 • Cuando se envíen los parámetros de actuación a los servos (fundamentalmente el ángulo de giro, duty cycle), se deben tener en cuenta los límites de estos para no dañarlos: dc mínimo 50 (-90º) y dc máximo 240 (90º). • Al enviar un mensaje para la radiofrecuencia, se debe tener en cuenta que mensajes de excesiva longitud (mayor de 30 bytes) pueden presentar problemas a la hora de ser enviados. Un ejemplo a la hora de introducir las órdenes: ... comunicacion.inicio(); //se reinicia la comunicación comunicacion.configuracion(5,4); //se introduce una nueva configuración en los sensores, 5 ADs y 4 USs comunicacion.leerAD(4); // se solicita el 4º sensor analógico comunicacion.pedirUSs(); // se pide un barrido de todos los US comunicacion.periodoMotores(200); // el periodo de los motores a 200 comunicacion.DcMotores(100,100); // ambos Dc a 100 (motores parados) comunicacion.activarMotores(3); // los dos motores activados ... 4.4 Envío y Recepción Para enviar el vector con las órdenes para el microcontrolador, se llama al método comunica. Este método envía las órdenes y no devuelve el control hasta que no recibe la respuesta y la procesa. En caso de que se produjeran errores durante la comunicación, esta función devolvería un 0. Por ello, sería recomendable que al método se le llamase desde dentro de un if, y si fallase se pusiera de manifiesto al usuario: if(!comunicacion.comunica()) //si devuelve 0 en caso de error MessageBox(_T("Error en la comunicación")); Sólo se produce una comunicación si hay órdenes a ejecutar; es decir, que: comunicacion.inicio(); comunicacion.comunica(); no realiza ninguna comunicación. 4.5 Lectura de las medidas Existen tres tipos de medidas: las que se solicitan en una comunicación y se reciben en la propia respuesta (analógicas y encoders); las que se solicitan en una comunicación y, debido al tiempo que se tarda en realizarlas, no se reciben en la propia respuesta sino en 8 la respuesta de una o varias comunicaciones posteriores (ultrasonidos); y las que se reciben sin ni siquiera haber sido solicitadas con anterioridad (digitales y radiofrecuencia). En el caso de las primeras, no existe problema, pues la aplicación principal tratará de leerlas sólo cuando las haya solicitado primero. Para los otros dos tipos se deberá consultar el vector interrupcion de forma periódica en el que se indica, con un 1 (sí) o un 0 (no), si en la última comunicación se ha recibido alguna de estas medidas no esperadas. Cada elemento del vector se refiere a: • interrupcion[1]: si se ha recibido o no un mensaje procedente de la radio. • interrupcion[2]: si se ha recibido la medida o medidas de ultrasonidos solicitadas anteriormente. • interrupcion[3]: si se ha producido un cambio en el estado de los sensores digitales. Lo lógico sería que cada vez que se realice una comunicación se comprobara si se ha recibido un nuevo mensaje de la radiofrecuencia o si ha habido cambios en el estado de los sensores digitales. Y, en caso de que se hubiese pedido anteriormente los ultrasonidos, comprobar también si estos se han recibido. Hay que tener en cuenta que el vector interrupcion pondrá a 1 el elemento correspondiente solamente en el momento que se recibe dicha respuesta no esperada, volviendo a ponerse a 0 en la siguiente comunicación. Una vez que se sabe qué medidas se deben leer, la forma de hacerlo es la siguiente. Se crea un puntero constante de tipo BYTE, y éste se iguala a la función de petición de medidas correspondiente (ver sección 3). Un ejemplo de lectura de medidas, suponiendo que anteriormente se ha pedido un barrido de los sensores de ultrasonidos: int elemRadio; const BYTE // indica el número de elementos que devuelve la radio *vectorADs, *vectorEncoders, *vectorRadio, *vectorUSs, *digit; // definición de variables para el manejo de valores entregados por el driver vectorADs = comunicacion.valorADs(); //el valor de los ADs se guarda vectorADs, que será un const BYTE* vectorEncoders = comunicacion.valorEncoders(); //similar a la función anterior if(comunicacion.interrupcion[1]) //si 1, se ha recibido mensaje de RF 9 en vectorRadio = comunicacion.valorRF(&elemRadio); //en el caso de la radio se pasa un puntero donde dejará el número de elementos que tiene el mensaje if(comunicacion.interrupcion[2]) //si 1, se han recibido los USs vectorUSs = comunicacion.valorUSs(); //se pasa las medidas a vectorUSs if(comunicacion.interrupcion[3]) //si 1, ha habido cambios en digitales digit = comunicacion.valorDig(); //se pasa el estado a digit ... Además, en el caso de la radio se debe tener otro factor en cuenta. Los envíos recibidos desde la radiofrecuencia pueden esperar una respuesta de la PDA hacia el que envió el mensaje. La forma de saberlo es a través de la variable RFrespuesta, que tendrá valor 1 si espera respuesta y 0 en caso contrario. if(interrupcion[1]) { //si 1, se ha recibido mensaje de RF vectorRadio = comunicacion.valorRF(&elemRadio); if (RFrespuesta == 1) // aquí el código para responder } Por otra parte, para saber si la TCS está funcionando y existe comunicación entre la PDA y la TCS se puede usar la función Ping. En caso de que exista comunicación la variable compruebaPing se pone a 1 y en caso contrario a 0. comunicacion.inicio(); //se reinicia la comunicación comunicacion.Ping(); if (comunicacion.comunica()){ if (comunicacion.compruebaPing) MessageBox(_T("Comunicación OK. La TCS está viva"),_T("Aviso"),MB_OK); else MessageBox(_T("Error de comunicación"),_T("Error"),MB_OK); } 4.6 Problemas que pueden surgir • Al pedir ultrasonidos y esperar la respuesta el programa deja de responder. En ocasiones pudiera ocurrir que alguna comunicación fallase, o que la TCS no entendiese la orden de realizar la lectura de los ultrasonidos. Si esto ocurre, teniendo la aplicación programada para no continuar hasta recibir la medida, 10 podría quedarse colgado el programa ya que la TCS nunca responderá. La solución de este problema consiste en reprogramar la aplicación indicando que si en diez comunicaciones (número orientativo) no se ha recibido la medida se volviese a pedir. • Al crear el objeto Comm salta el mensaje “No se puede abrir el puerto”. Si no se puede abrir el puerto serie es debido a que hay otro programa usándolo. Cerrando dicho programa se solucionaría el problema. Si no se puede cerrar o no hay ningún programa abierto la solución será resetear la PDA (botón de reset situado en la parte inferior o trasera). • No se puede establecer la comunicación. Normalmente esto es debido a que la última vez que se utilizó la comunicación serie, el puerto se cerró mal, o a que hay algún problema en la TCS. En tal caso se debe resetear la PDA y la TCS. 4.7 Ejemplo de uso Suponemos que se tiene un robot con las siguientes características: • El robot cuenta con dos motores situados en sus ruedas traseras. • La navegación se quiere controlar por medio de tres sensores de ultrasonidos. • También tiene el robot dos sensores de infrarrojos (analógicos) situados en un costado, para poder controlar la distancia a la pared. • Además el robot cuenta con tres sensores digitales (lógica negada) que le advierten en caso de contacto. • También se quiere llevar un control de la posición, y esto se realiza por medio de dos encoders situados en cada una de las ruedas motoras. En una primera comunicación se deberían pasar los parámetros de configuración de la plataforma: el número de sensores, tanto de ultrasonidos como analógicos; y la configuración inicial de los motores, es decir, activarlos pero que se encuentren parados. Ésta primera comunicación quedaría: // definición de variables globales BYTE Dc1=0, Dc2=0; // Primera comunicación con el robot, donde se activan los motores, se configuran el número de sensores analógicos y de ultrasonidos. comunicacion.inicio(); // se reinicia las colas Rx y Tx comunicacion.configuracion(2,3); // 3 sensores US y 2 sensores AD comunicacion.periodoMotores(200); // se pone un periodo de 200 11 comunicacion.DcMotores(100,100); // inicialmente parados comunicacion.activarMotores(3); // se activan los dos motores comunicacion.comunica(); // se pasan los parámetros al PIC En el funcionamiento normal del robot, la función periódica a ejecutar cuando el robot esté activo podría ser: // definición de variables locales const BYTE *Digitales, *USs, *ADs, *Encoders; comunicacion.inicio(); // se reinician las colas Tx y Rx comunicacion.pedirUSs(); // se pide el barrido de los tres USs comunicacion.comunica(); // se comunica, sabiendo que los USs no pueden llegar en la propia comunicación // Se inicia un bucle hasta que se reciben las medidas de los ultrasonidos, o bien se recibe un cambio en los digitales. En el bucle se está continuamente pidiendo los encoders y los ADs, para que cuando se salga del mismo estos no estén desfasados respecto a los USs. int i = 0; // se establece un contador para que no se quedara infinitamente en el bucle si hubiera algún problema en la comunicación y no se recibiesen nunca los USs while(comunicacion.interrupcion[3]!=1 && comunicacion.interrupcion[2]!=1 && i < 10) { i++; comunicacion.inicio(); // se reinician las colas comunicacion.leerEncoders(); // petición de los Encoders comunicacion.leerADs(); // petición de los ADs comunicacion.comunica(); } if(comunicacion.interrupcion[3] == 1) // si hubo cambio en digitales { Digitales = comunicacion.valorDig(); // se toma el valor de los sensores digitales if (Digitales[0] < 15) { // ha habido choque y paro el robot por ejemplo Dc1 = 100; Dc2 = 100; } } else if(comunicacion.interrupcion[2] == 1) // si hubo medida de los US { USs = comunicacion.valorUSs(); // se guardan las medidas de los USs ADs = comunicacion.valorADs(); // se guardan las medidas de los ADs Encoders = comunicacion.valorEncoders();// Encoders 12 se guardan las medidas de // lógica de control en función de las medidas. Por ejemplo una sencilla if (USs[0] < 20 || USs[1] < 20) { // gira en un sentido Dc1 = 100; Dc2 = 0; } else if (USs[1] < 20) { // gira en otro Dc1 = 0; Dc2 = 100; } } comunicacion.inicio(); comunicacion.DcMotores(Dc1,Dc2); comunicacion.comunica(); // se volvería a enlazar con el principio, bien siendo esto una función que la volverían a llamar, o dentro de un bucle, etc. Este es un modelo de comunicación, pero se podrán idear multitud de ellos. Es necesario darse cuenta de la ineficiencia de este código, ya que precisa de tres comunicaciones al menos por cada ciclo de control: una para pedir los ultrasonidos, otra para recoger las medidas y otra para actuar. Si en el control no intervinieran los sensores de ultrasonidos, el modelo sería muy distinto. 13