Programación PIC para decodificar RC5

Para decodificar el protocolo RC5 hay dos formas de hacerlo. Una es muestreando el puerto cada cierto tiempo, y comprobando si está a nivel alto o nivel bajo. En función del resultado lo interpretamos.

Esto es un ejemplo de codificación Manchester:


Hace ya tiempo conté una forma de decodificar el protocolo RC5 utilizando un PIC: Decodificacion del protocolo RC5 usando un PIC. Por algunos correos que he recibido parece que no terminó de quedar claro, y además prometí dedicarle otra entrada al código C. Sería muy conveniente que repasaras la entrada a la que me refiero, porque esta no es más que una especie de nota aclaratoria.

Para empezar supongamos que nos llega una señal como la de arriba. Las lineas verticales separan los periodos y es más fácil de ver donde empieza y acaba cada símbolo. Recordemos la dos reglas de oro:
  1. Siempre, siempre hay una transición en mitad de un periodo. Precisamente porque tiene la señal de reloj incorporada. Recordad que es para ayudarnos a sincronizar el reloj del receptor con el del transmisor. Aunque casi nunca se ajusta dinámicamente; simplemente sincronizamos al principio y nos limitamos a dar error si se desincroniza. Aunque ya vimos como se puede ajustar dinámicamente la frecuencia de reloj para decodificar señales tipo Aiken Biphase cuando leímos la banda magnética de las tarjetas de crédito.
  2. Si la transición es hacia arriba (de 0 a 1) se interpreta como un 1, si es hacia abajo, se interpreta como un 0. Lo que también podría decirse como si el pulso positivo está a la izquierda del periodo es un 0 y si está a la derecha es un 1. Mira la imagen de abajo, te ayudará.


El programa

No quiero resultar cansino, así que voy a suponer que has leído la entrada anterior, o que la recuerdas y me voy a saltar las explicaciones.

Tenemos la señal de ejemplo de arriba, voy a quitar las líneas divisorias y a numerar las transiciones.


Los números en la parte de arriba son correlativos e indican de qué transición se trata. La línea de abajo corresponde a la interpretación que s ele da a las transiciones. Las marcas de confirmación, que representan bits, están representadas por números grandes, mientras que los número pequeños indican una marcas de continuación.

Pegamos el código y a continuación os explico cómo funciona.

#include "rc5.h"

// 100 tmr1 =~ 200us =~ +- 20% (sobre 889us)
#define TMR1_TOL 100
#define IR_PIN  PIN_A0

/*
 Estados de la máquina:
 0 - Reset
  - Se resetan las variables
 1 - S1 recibido
  - Calculado t
 2 - S2 recibido
  - Calculado t en función de la media
  - Totalmente inicializada.
 3 - Marca de continuidad no recibida
 4 - Marca de continuidad recibida
*/

#define RC5_RESET 0
#define RC5_S1  1
#define RC5_S2  2
#define RC5_DONE 3
#define RC5_MARK 4



unsigned int16 rc5_COMANDO = 0; // S1 y S2.
unsigned int16 rc5_t;
unsigned char rc5_stad = 0;


#int_TIMER1
void  TIMER1_isr(void) 
{
 // Si el ultimo pulso fue hace más de X ms da timeout y resetea la máquina de estados RC5.
 // El prescaler de TMR1 es 2: 2 x 256 x 256 =~ 131 ms.
 // Teoricamente un semiperiodo deben ser (889us) tmr1 = 444.
 rc5_stad = 0; // maquina de estados RC5 reiniciada
}

#int_RA
void  RA_isr(void) 
{
 unsigned int16 int_time;
 unsigned char semiperiodos;

 int_time = get_timer1();
 
 
 semiperiodos = input(IR_PIN); // para limpiar la interrupción
 clear_interrupt(INT_RA);
 semiperiodos = 0;

 
 // Calculamos cuantos semiperiodos dura el lapso de tiempo
 // para ahorrar el cálculo después
 if (rc5_stad > RC5_S1) {
  signed int16 lapso;
  lapso = (signed int16) (rc5_t - int_time);
  
  if (lapso < 0)  lapso = - lapso;
  lapso -= TMR1_TOL;
  
  if (lapso < 0) {
   semiperiodos = 1;
  } 
  else if (lapso < rc5_t) {
   semiperiodos = 2;
  }
  else {
   rc5_stad = RC5_RESET;
   goto END; 
  } 

 } 
 
 // COMENZAR AQUI
 // Es el pulso de start1
 if (rc5_stad == 0) {
  rc5_COMANDO = 0b0000000000000011;
  rc5_stad++;  // maquina iniciada (estado 1)
 }
 
 // es el segundo pulso (marca de continuidad del start1)
 else if (rc5_stad == RC5_S1){
  rc5_t = int_time;
  rc5_stad++;  // primer pulso recibido (estado 2)
 }
 
 // es el tercer pulso (confirmación de start2)
 else if (rc5_stad == RC5_S2) {

  if (semiperiodos != 1) {
   //error("No parece RC5.");
   rc5_stad = 0;
   goto END;
  }
  rc5_t += int_time;
  rc5_t /= 2;  // media entre los dos
  rc5_stad++;  // cálculo del periodo completado (estado 3)
 }
 
 // transición sin marca de continuación
 // se ha invertido el bit
 else if ( semiperiodos == 2 ) {
  // El estado 4 es para esperar la confirmación de continuidad
  // No debería darse el caso
  #bit OLDlastBit = rc5_COMANDO.1
  #bit NEWlastBit = rc5_COMANDO.0
  
  if (rc5_stad == RC5_MARK) {
   //error("Error de protocolo.");
   rc5_stad = 0;
   goto END;
  }
  
  rc5_COMANDO <<= 1;
  NEWlastBit = ~OLDlastBit;
 }
 // se trata de una marca de continuación o una confirmación
 else if ( semiperiodos == 1 ) {
  // es una marca, espero la confirmación
  if (rc5_stad == RC5_DONE) {
   rc5_stad = RC5_MARK;
  }
  // es una confirmación
  // se continua el bit anterior
  else {
   #bit OLDlastBit = rc5_COMANDO.1
   #bit NEWlastBit = rc5_COMANDO.0
   rc5_COMANDO <<= 1;
   NEWlastBit = OLDlastBit;
   
   rc5_stad = RC5_DONE;
  }
 }
 
 else {
  rc5_stad = RC5_RESET; // algún error
 } 

END:
 set_timer1(0);
}




void main()
{

   port_a_pullups(TRUE);
   setup_timer_1(T1_INTERNAL|T1_DIV_BY_2);
   enable_interrupts(INT_TIMER1);
   enable_interrupts(INT_RA);
   enable_interrupts(GLOBAL);
   setup_oscillator(OSC_4MHZ);


 for (;;) {
  if (bit_test (rc5_COMANDO, 7)) {
   rc5_COMANDO = 0; 
  }   
 } 

}


El bucle de recepción está en la interrupción RA. Lo primero que hacemos cuando detectamos el cambio del puerto es quedarnos con el valor del timer 1. Ese valor puede ser aleatorio si es la primera transición, pero a partir de la segunda ese valor nos indica el tiempo transcurrido desde el último cambio porque precisamente lo que hacemos como última instrucción al salir de la interrupción es reiniciar timer1. Luego limpiamos la interrupción haciendo un input del puerto; he reutilizado la variable semiperíodos por no definir otra, pero ni que decir tiene que eso no son los semiperíodos.

Hay que tener en cuenta que este programa es sólo para que veais el algoritmo. En la vida real tendríamos que poner una condición para ver si lo que ha cambiado es el pin al que tenemos conectado el sensor o es otro distinto.

Todo gira alrededor de una máquina de estados. El estado inicial es el estado cero. Las variables están en sus valores por defecto y el sistema está listo para empezar a recibir un comando.


Inicialización

Llega la primera transición. Nuestro objetivo ahora es calcular el semiperíodo para cuando lleguen las transiciones de los datos poder saber qué significan. Sabemos, por definición del protocolo, que lo primero que nos va a llegar son dos bit de start y van a ser sendos unos. Mirad la imagen de arriba, son las transiciones 1, 2 y 3 y entre cada una hay un semiperíodo. Podría calcular la duración del semiperíodo simplemente basándome en la diferencia entre la 1ª y la 2ª. Pero ya que son dos marcas de start es más fiable si calculo la diferencia de tiempos entre la 1 y la 2, y también de la 2 a la 3 y luego hago una media.

Como decíamos, llega la primera transición. Id a la línea 80, donde dice "Comenzar aquí". Estamos en el estado cero, todo reseteado. A lo más que podemos aspirar aquí es a poner los dos unos de start en la variable COMANDO y poco más. Pero lo más importante que hacemos es pasar al estado uno y salir reiniciando timer 1 como habíamos dicho.

Llega otra transición, sería la 2ª. Estamos en el estado uno. Lo que nos indica que no es la primera y que tenemos otra para calcular cuanto tiempo ha pasado entre ambas. La variable int_time, calculada al principio del bucle contiene el valor de timer1, y puesto que lo habíamos reiniciado antes, contiene el tiempo desde la transición anterior, que como sabemos es un semiperíodo.

Notad que cuando hablo de tiempos, no me refiero a segundos, ni a instrucciones, sino a tics del timer1. El tiempo real en segundos dependerá de la velocidad del reloj y de cómo esté configurado el prescaler. En cualquier caso no nos interesa el tiempo real, sino una medida con la que comparar para saber si entre dos transiciones hay una medida (semiperíodo) o dos (un periodo). A cuántos microsegundos equivale es irrelevante.

Calculado el primer semiperíodo avanzamos al estado dos. Cuando llega la tercera transición, como el estado es dos, vamos a la línea 58. Ahí comparamos la duración con la variable rc5_t, que es la que dice cuanto dura un semiperíodo. Hablaremos luego de cómo funciona la comparación.

Volvemos a la parte del código que controla la máquina de estados, a partir de la línea "Comenzar aquí". En este caso como el estado es dos, aterrizamos en la línea 92. Acto seguido miramos si la duración entre las transiciones 2 y 3 es equiparable a la duración entre la 1 y la 2. Porque si no es así puede que hayamos hecho mal la medida. Si son comparables, hacemos la media aritmética y nos quedamos con el resultado. Esa será nuestra variable rc5_t para toda la ráfaga que sigue. Pasamos al estado tres: inicialización completada. El estado tres también implica Marca de continuidad no recibida, que significa que hemos terminado de recibir un bit. Y así es porque los bits de start son unos. Ya podemos empezar a recibir datos de verdad.


Comparación

Ahora sí vamos a explicar cómo hacemos la comparación, vamos a la línea 58 del código. Tenemos, por un lado la duración de un semiperíodo (recordemos, en pulsos de timer 1) en la variable rc5_t; y por el otro, en int_time el tiempo desde la última transición. Lo que quiero es comparar ambos valores, dentro de unos márgenes de tolerancia, para saber si int_time equivale a un periodo, a dos, o a ninguna de las dos cosas.

Si estuviéramos programando en un PC, para saber si int_time es equivalente a rc5_t con una tolerancia del 10% haríamos una comparación tal que así:
if (int_time > 0.90*rc5_t) &&
   (int_time < 1.10*rc5_t) ...
Pero en un microcontrolador este tipo de cosas conviene evitarlas, principalmente porque los compiladores no suelen estar tan optimizados y malgastan los limitados bits de RAM. Además implícitamente estamos obligando al compilador a:

  • Usar aritmética de coma flotante: Que implica cargar unas librerías más que pesadas y nos van a agotar la ROM si es un PIC pequeñito. Tal vez el compilador se diera cuenta de lo que queremos hacer y usara aritmética de punto fijo, pero en ese caso...
  • ... forzamos una o varias divisiones. Y las divisiones son las operaciones más lentas, a menos que se trate de dividir por una potencia de dos, que entonces es tan simple como desplazar los bit hacia la derecha. No olvidéis que todo esto se ejecuta dentro de un servicio de interrupción, donde la rapidez y la ligereza son imprescindibles. No podemos permitirnos tener al procesador ocupado atendiendo una interrupción durante mucho tiempo, porque mientras tanto no hace lo que tiene que hacer.
En resumidas cuentas, que siendo esto una rutina para reconocer comandos por mando a distancia, estamos obligados a hacerla lo más compacta y eficiente posible. Lo ideal sería hacerla en ensamblador... eso ya os lo dejo a vosotros jejeje.

Así que recurriré a una vuelta un poco menos evidente, pero que una vez compilada, en comparación es más eficaz.
  1. Para empezar sólo se ejecuta esta parte cuando hemos pasado el estado 1, o sea cuando ya tenemos un valor de rc5_t con el que comparar.
  2. Definimos una tolerancia fija, TMR1_TOL, en este caso de 100 tics de timer1 (vedlo en la linea 4).
  3. Definimos una variable temporal, llamada lapso, y le asignamos la diferencia entre int_time y rc5_t. Lapso tiende a 0 cuando ambas fueran iguales (un semiperíodo) y tendería a rc5_t si es el doble de este valor (dos semiperíodos).
  4. Nos interesa sólo el valor absoluto de lapso, ya que ahora vamos restarle la tolerancia.
  5. Si lapso era menor que la tolerancia significa que int_time está dentro de los márgenes para ser considerada igual a rc5_t, o sea, un semiperíodo.
  6. Si, por el contrario, lapso es mayor que la tolerancia, pero no llega a sobrepasar rc5_t diremos que dura dos semiperíodos.
  7. En el caso que lapso fuera mayor que rc5_t, implica que int_time es mayor que el doble de rc5_t más la tolerancia. Significa que nos hemos saltado alguna transición o que el protocolo no es RC5, así que ponemos el estado cero para que la máquina de estados se reinicie.

He tenido en cuenta la forma en la que el compilador CCS optimiza. Por ejemplo, las comparaciones con 0 o con un número fijo son más rápidas que las comparaciones con variables, por eso sólo se hace una vez. En general siempre es esí, pero puede depender del compilador. Cuando se realizan operaciones implícitas dentro de la comparación se está utilizando espacio de almacenamiento temporal y da lugar a un código más complejo. Estas son cosas que sólo se ven en el código compilado. Si estáis programando un microcontrolador para un proyecto crítico siempre es bueno repasar el código una vez compilado, sobre todo en ciertas partes "problemáticas" como las comparaciones, y las rutinas que más se ejecutan. Para proyectos profesionales hay herramientas de tipo perfiladores para micros, pero si no disponemos de ellas pues nos toca hacerlo a mano.


Recepción de los datos

Vale, ahora ya sabemos si el tiempo desde la última transición es (aproximadamente) un semiperíodo o dos. A partir de la línea 106 y de la 122 se aplica el algoritmo que habíamos descrito en la entrada que cito al principio. Y cuando se trata de una marca de confirmación rotamos todos los bit de la variable rc5_COMANDO hacia la izquierda y metemos el bit nuevo.

La máquina de estados se pone en el estado tres cuando recibimos una marca de confirmación o de cambio. En ambos casos se fija el bit. Mientras el estado cuatro es un estado temporal que indica que hemos recibido una marca de continuación, pero aún no hemos recibido la confirmación. Si en este estado la comparación nos devuelve 2 semiperiodos se trata de una sitación que no tiene sentido, así que asignamos el estado 0 y salimos.


Parada

Hay dos situaciones en que la máquina de estados deja de recibir.

La primera es cuando la comparación de la línea 162 en la función main es verdadera. Recordad que main se está ejecutando continuamente, siendo interrumpida ocasionalmente cuando cambia el pin del sensor infrarrojo para meter más bit en la variable rc5_COMANDO. Pues bien, a medida que vamos metiendo bits por la derecha, los bits de start van avanzando hacia la izquierda. Si yo sé que mi comando tiene 7 bits de largo, voy fijándome en la variable para que en cuando los bits de start alcancen la posición 7ª interpretar el comando completo.

Hay otra situación, y es que la máquina de estados se reinicia automáticamente cuando no se reciben datos por un tiempo. Recordad que la última intrucción de la rutina que examinamos antes es reinicial el contador timer1. Si un comando se corta y llega a la mitad no se reinicia más, y llegará el momento que timer 1 se desborde. Cuando eso ocurre se llega a la rutina en la línea 34, que lo único que hace es poner el estado a cero, para volver a empezar la recepción de nuevo.
Artículo completo >>

Adaptador de USB a Serie

La primera entrada de este blog (Conversor USB - RS232) la dediqué a contaros cómo hacernos un adaptador sencillo y muy cómodo para conectar un microcontrolador al PC cuando no teníamos puerto serie. Y aún cuando tuviéramos, a mi me resulta mucho más práctico este adaptador que un puerto serie de verdad.

Recordemos que nos basamos en un adaptador comercial barato (2.86 USD) y de poca calidad: http://www.dealextreme.com/details.dx/sku.24799. En esta segunda versión voy a aportaros alguna foto con más calidad, para facilitaros el montaje. También quiero explicaros por qué se hacen los puentes que se hacen. Y por último voy a añadir un cuarto cable de alimentación positiva para alimentar el dispositivo directamente desde el puerto USB.


Aquí tenemos cuatro cables:
  • Positivo de alimentación +5V (rojo)
  • Transmisión de datos hacia el PC (blanco)
  • Recepción de datos desde el PC (naranja)
  • Negativo de alimentación y masa de la señal, 0V (negro)

Aunque el conector USB también tiene cuatro hilos, la transmisión de datos es muy diferente. Y aunque yo lo he planteado como un adaptador externo, por el precio que tiene bien se podría dejar dentro de algún que otro invento que nos sea práctico. En ocasiones vale más eso que comprar un PL2303 (que es el chip en que se basa) y un cuarzo y montarlo nosotros, con las dificultades para comprarlo (generalmente online), el coste del chip más los gastos de envío, y los problemas que da soldar placas con componentes SMD. Estoy hablando de cacharros para nosotros, claro. A la hora de diseñar algo para la venta hay mejores opciones.

Decía antes que es de mala calidad, pero son detalles que bien nos benefician o que podemos solucionar fácilmente, me explico:
  • Los niveles de salida no son RS232 (+-12V), sino que son TTL (0-5V). Pues bien, eso que para algunos módems no sirve, para nosotros que queremos conectarlo a un microcontrolador, que precisamente usa niveles TTL nos viene de perlas. Porque si llevara un conversor de nivel como el MAX232 tendríamos que quitárselo.
  • A veces me he encontrado soldaduras mal hechas y pistas cortocircuitadas. Pero el circuito tiene tan pocos componentes que a poco que uno mire se da cuenta.
  • El oscilador es inestable. No termino de ver el motivo pero en todos los adaptadores de este modelo que he comprado falla el oscilador. De forma que cuando lo conectas, Windows no reconoce el dispositivo y lo da por defectuoso, y Linux no sabe indicar qué has conectado. Una forma fácil y rápida de arreglarlo es colocar una resistencia de 100kohm en paralelo con el cristal, como veréis en las fotos siguientes.

Montaje

Se trata nada más de que eliminar las clavijas USB y RS232 y conectar sendos cables. Conectar la resistencia de la que hablábamos antes y hacer unos cuantos puentes para el handshake y el conrol de flujo. De qué lineas puentear hablaremos en el apartado siguiente.






Handshake y control de flujo

Habréis notado que he puenteado tres lineas por un lado, y dos por otro. En principio no hace falta, porque las aplicaciones que vamos a usar van destinadas a un microcontrolador, no esperan encontrarse un módem. Sin embargo no viene mal un poco del culturilla sobre cómo funcionaba antaño un puerto serie.

Vamos a describir las líneas más importantes que son las que aparecen en un conector DB9, después os contaré cómo es la secuencia para conectar telefónicamente un módem con otro y entenderéis perfectamente qué es lo que el ordenador entiende cuando puenteamos juntas las lineas. Uso un módem porque el propósito original del puerto serie era conectar un módem. Aunque más adelante resultaba idóneo para conectar el ratón no se inventó para eso sino que fue una utilidad posterior.

  1. Data Carrier Detect (DCD): Indica que se ha establecido una conexión con el destino, y que estamos preparados para recibir y enviarle datos. Tenemos una portadora. Si la conexión se pierde, por ejemplo el destinatario de la llamada colgara el teléfono, nuestro módem pone inmediatamente a 0 la linea DCD y así el PC entiende que se ha perdido la conexión.
  2. Receive Data (RxD): Sirve para que el PC reciba los datos que le envía el módem.
  3. Transmit Data (TxD): Sirve para que el PC transmita datos al módem o al dispositivo que esté conectado.
  4. Data Terminal Ready (DTR): Cuando iniciamos un programa que va a hacer uso de un aparato conectado al puerto serie, el PC activa la línea DTR preguntando al dispositivo si está listo.
  5. Masa. Observad que no existe ninguna línea de alimentación positiva y los circuitos que quisieran alimentarse por el puerto serie (lo cual sólo es posible si consumían poco) debían hacerlo por la línea DTR, lo que es muy poco fiable.
  6. Data Set Ready (DSR): Esta línea sirve para que el hardware que hay conectado al puerto indique al PC que está conectado, encendido y listo para usarse.
  7. Request to Send (RTS): Como los módems antiguos eran torpes, el PC tenía que avisar antes de enviarles datos, porque si los enviaba antes de que el módem estuviera listo para recibirlos se perdían. Así cuando el PC quiere enviar datos pone a 1 esta línea y espera la confirmación por la siguiente.
  8. Clear to Send (CTS): Podríamos traducirlo por vía libre para enviar Cuando el módem tiene buffers libres para recibir nuevos datos activa esta línea. O al menos esa era la idea original. Porque con el tiempo la misión de las líneas RTS y CTS cambió ligeramente y pasaron a emplearse de otra manera.
  9. Ring Indicator (RI): Si el módem recibe una llamada entrante lo indica al PC poniendo a 1 esta línea. El PC genera una interrupción que recibe el sistema operativo y ejecuta lo que tenga que hacer para aceptar una conexión entrante.

Resumiendo:
  • Iniciamos el programa terminal en el PC para marcar un número de teléfono y conectarnos con otro PC. El PC activa la línea DTR, y el módem, que está conectado y listo reacciona activando DSR.
  • El PC quiere enviar los comandos de inicialización y conexión (por lo general comandos Hayes: descolgar y marcar un número) así que activa RTS y espera. Como no hay ningún problema para recibir los datos, el módem activa CTS y comienza la transmisión del comando.
  • El módem descuelga, marca el número de teléfono y espera respuesta. Cuando el otro extremo contesta hay una negociación de velocidades (o no) y nos envía una portadora de datos en señal de que la conexión está operativa. En ese momento se activa la línea DCD. Pudiera pasar que al otro extremo de la línea no haya otro módem sino una persona de carne y hueso. En ese caso no se activaría la línea DCD.
  • Ahora ya hay conexión, se ha completado la primera fase (lo que se llama el handshake) y lo que se usa a partir de ahora son las líneas RTS y CTS (control de flujo por hardware).
  • Supongamos que el otro extremo cuelga: la linea DCD se va a 0 lógico y nuestro programa terminal nos avisa de que se ha terminado la conexión (NO CARRIER ).
  • Y supongamos ahora que los que queremos colgar somos nosotros: damos la orden de terminar la conexión nuestro y en ese momento nuestro PC lleva a cero la línea DTR, indicando al módem un mensaje, algo como "ya no estoy listo". El módem lo entiende e interrumpe la llamada.

Entonces ¿qué conseguimos puenteando las líneas 1, 4 y 6 (DCD, DTR y DST)? Pues falsear el handshake, de forma que en cuanto el PC esté preparado, le llegue la señal de que el hardware también está preparado y conectado.

El puente entre las líneas 7 y 8 (RTS y CTS) falsea el control de flujo por hardware. De forma que en cuanto el PC envíe la señal de que quiere enviar datos, le llegue por el puente que hemos hecho la señal de que el dispositivo también está listo para recibirlos.

¿Os habéis fijado en que el módem en cualquier momento puede pedir al PC que no le mande más datos (llevando a cero CTS) pero el PC no puede decirle que haga una pausa? Esto es así porque no tendría sentido que el PC pidiera al módem que haga una pausa si el otro extremo le sigue enviando datos. Para eso está el control de flujo por software, que es una forma de decirle al sistema remoto que estamos saturados y que no envíe nada más hasta nueva orden.

Para otras aplicaciones sí tiene sentido que tanto el PC como el aparato puedan decirse mutuamente que están preparados o no para recibir datos. Para ese caso os decía arriba que la misión de RTS y CTS ha cambiado un poco. Lo que se hace es dejar CTS para lo que estaba, indicarle al PC si el dispositivo conectado puede o no recibir datos en ese momento; y hacer que RTS sea lo mismo pero para el PC, indicándole al aparato si el PC está o no listo para recibir en ese momento.
Artículo completo >>