Controlar un servomotor con el PC

Supongo que muchos estaréis familiarizados con los servos. No es mi caso. Por unas cosas u otras nunca me he dedicado al modelismo ni a la robótica así que para mí estos motores como si no existieran. Sin embargo hace unas semanas vi uno barato en DealExtreme y pensé que algún día puedo necesitarlo. Y para entonces mejor saber cómo se usa. Así que esta primera prueba no va a ser nada elaborado, solamente un servo, un PIC, y un PC para dar las órdenes.

He utilizado el modelo TowerPro MG995. Según dicen, y he podido comprobar, tiene sus ventajas y sus inconvenientes.

Ventajas
  • Es barato. O relativamente barato comparado con otros modelos.
  • Potente. Tiene bastante torque.
  • Engranajes metálicos.
Inconvenientes
  • Tiene una zona muerta muy estrecha, y le cuesta quedarse quieto.
  • Los engranajes metálicos lo hacen lento y con inercia, por lo que a veces se pasa y tiene que retroceder.
  • Con la inercia que tiene consume mucho al arrancar (más de 1A) y mete mucho ruido en la alimentación. Así que si no desacopláis bien el micro se os va a reiniciar.
  • Es muy sensible a las caídas de tensión aunque sean breves. Cuando lo alimentamos con 5V hay ocasiones que la electrónica interna se reinicia y el motor se mueve sólo.

En esta página de la Wikipedia se explica cómo se controla y en esta otra explican cómo funcionan interiormente. Hay cientos de páginas por toda Internet, de aficionados a la robótica sobre todo, que tienen mucha información.


Como ya os he dicho nunca había manejado un servo y no tenía muy claro cómo son los pulsos que hay que enviar. Según la Wikipedia es así:


Pero los valores de 1ms y 2ms son orientativos y dependen del modelo de servo. Y como yo no he podido encontrar el datasheet del MG995 pues me he preparado un PIC para probar con varios.

Programa del PIC

Como de costumbre usaremos un 12F683 y un programita en C que podéis compilar con el compilador de CCS. Y por si no lo tenéis también os dejo al final del artículo un enlace al código fuente y al fichero compilado.

Utilizaremos el puerto serie para la transmisión, si vuestro ordenador ya no tiene puerto serie necesitaréis un conversor USB-RS232. Como este.

Tras presentar una cabecera se queda esperando comandos. Los comandos constan de una letra y un número. La letra puede ser p si queremos cambiar el periodo u o si queremos cambiar la duración del pulso. El número que sigue son los microsegundos de duración.

Si por ejemplo queremos cambiar el tiempo del pulso a 1ms, que son 1000us pondremos
o1000

Tal que así:

Prueba de servo.
Escribe oXXXX para variar el tiempo On
o pXXXX para variar el periodo

>o1000
Ton = 1000
Periodo = 200000
>p15000
Ton = 1000
Periodo = 200000
>

Variar el periodo no debería infuir en la posición del servo, pero es útil para hacer pruebas.

Varias cosas en cuanto a este código:

Utilizamos un Timer para gestionar el pulso. Habría sido más fácil con un bucle y un par de delays, pero no podríamos hacer nada más mientras tanto. Usando un timer nos quitamos el problema del bucle principal y lo delegamos a la interrupción. Y si luego más adelante quisiéramos manejar más de un servo con el mismo PIC ya tenemos un paso avanzado.

He rescrito la función atol. Mi versión es menos potente que la que trae el compilador en sus librerías pero también ocupa menos espacio en memoria. No necesito convertir números hexadecimales ni negativos en este programa.

También he pasado de gets por dos motivos. El primero es que no tiene límite de caracteres, y se puede cargar otras cosas de la memoria si escribo una cadena demasiado larga. Tampoco me preocupa mucho porque sólo son pruebas lo que estoy haciendo. Pero el motivo principal es que get_string tiene eco remoto. Eso quiere decir que en el PC verás en el terminal lo que tecleas mientras escribes. Cosa que gets no hace.


/*****************************************************************************/
/*   Primera prueba de servos.
/*   Recibe los parámetros desde el PC para ver cuales son los que
/*   mejor se ajustan al motor.
/*
/*   14/12/2010
/*****************************************************************************/

#include "prueba1.h"
// Prefiero get_string() en input.c a gets() de stdlib.h
// porque se le puede poner un límite de caracteres
// y además tiene echo remoto (se ve lo que envías mientras escribes)
#include <input.c>


short activo =     0;   // indica si el pin está activo
                        // si lo hiciera con una instruccion bit
                        // no podría cambiarlo en el #define fácilmente
long Ton     =  1000;   // tiempo de duración del pulso (us)
long Periodo = 20000;   // duracion del periodo (us)

long atol(char *);

#int_TIMER1
void  TIMER1_isr(void) 
{
 /* Lo activamos y plantamos la interrupción
    para que se desactive al rato 
    Los valores de offset estan calculados con el simulador */
 if (!activo) {
  activo = 1;
  output_high(PIN_Servo);
  set_timer1(65535 - Ton + 60);
 }
 else {
  activo = 0;
  output_low(PIN_Servo);
  set_timer1(65535 - Periodo + Ton + 65);
 }
}



void main()
{
 setup_oscillator(OSC_4MHZ);
 
    setup_adc_ports(NO_ANALOGS|VSS_VDD);
 setup_adc(ADC_OFF);
 
 /* Timer1 con resolución de 1us,
    desbordamiento en 65.535ms a 4MHz */
 setup_timer_1(T1_INTERNAL|T1_DIV_BY_1);

 printf("Prueba de servo.\r\n");
 printf("  Escribe oXXXX para variar el tiempo On\r\n");
 printf("  o pXXXX para variar el periodo\r\n");

 enable_interrupts(INT_TIMER1);
 enable_interrupts(GLOBAL);


 for(;;) {
  char string[10];
  char var;
  long valor;

  get_string(string, sizeof(string));
  //strcpy(string,"o1234"); (DEBUG)
  var   = string[0];
  valor = atol(string+1);
  
  if (valor <= 0) {
   puts("Valor no valido.");
  }
  else { 
   if (var == 'o') {
    if (valor > Periodo) {
     puts("Valor no permitido pata Ton.");
    } 
    else { 
     Ton = valor;
    }
   }
   else if (var == 'p') {
    Periodo = valor;
   }
   else {
    puts("Comando no reconocido."); 
   }
   
   printf("\nTon = %Lu\r\n", Ton);
   printf("Periodo = %Lu\r\n", Periodo);
  }
 } 
}


/* Versión reducida de atol:
   Esta versión sólo trabaja con positivos y en decimal.
   Y para cadenas de hasta 256 caracteres. */
long atol(char *s)
{
 long result;
 char indice;
 char c;

 indice = 0;
 result = 0;
 
 c = s[indice];
 while (c >= '0' && c <= '9') {
   result = 10*result + (c - '0');
   indice++;
   c = s[indice];
 }

 return(result);
}

Por si teneis curiosidad, el fichero prueba1.h es así

#include <12F683.h>

#device adc=16

#FUSES NOWDT                  //No Watch Dog Timer
#FUSES INTRC_IO               //Internal RC Osc, no CLKOUT
#FUSES NOCPD                  //No EE protection
#FUSES NOPROTECT              //Code not protected from reading
#FUSES NOMCLR                 //Master Clear pin used for I/O
#FUSES NOPUT                  //No Power Up Timer
#FUSES NOBROWNOUT             //No brownout reset
#FUSES IESO                   //Internal External Switch Over mode enabled
#FUSES FCMEN                  //Fail-safe clock monitor enabled

#define PIN_Servo   PIN_A2 // Salida para el servo
#define PIN_SrTX    PIN_A0 // TX serie para conectar al PC
#define PIN_SrRX    PIN_A1 // RX serie para conectar al PC

#use delay(clock=4000000)
#use rs232(baud=9600,INVERT,DISABLE_INTS,parity=E,xmit=PIN_SrTX,rcv=PIN_SrRX,bits=8)


Salida del PIC

Vamos a usar la tarjeta de sonido que nos curramos en otra entrada (Medir valores lógicos con una tarjeta de sonido) para ver cómo son los pulsos que genera el programa y cómo cambian cuando tecleamos los comandos.


La imagen es una captura de pantalla del Audacity. En la primera pista vemos pulsos de 500us con un periodo de 30ms. En la segunda el periodo es el mismo pero los pulsos son de 2.5ms (los hemos cambiado enviando el comando o2500) y en la tercera mantenemos la duración del pulso pero el periodo cambia a 20ms (comando p20000).

Fijaos que aquí se aprecia muy bien el efecto de Gibbs del que ya habíamos hablado en otro artículo debido al filtrado de las altas frecuencias. Se ve mucho porque hay una resistencia de 1k en serie con la entrada de la tarjeta y porque la frecuencia de muestreo de esta última es baja. A mayor corriente menos se nota la oscilación. Pero la corriente de salida del PIC es finita.

Resultado

En este servo, el cable marrón va a masa, el rojo a positivo y el naranja es la señal de posición.

Los valores del periodo válidos van desde los 5ms a más de 35ms (que es el tope que se puede probar con el programa anterior, porque Timer1 se desborda).

En cuanto la de duración del pulso he visto que va entre 0.5ms y 2.5ms. Por debajo de 0.5ms el servo se coloca en posiciones aleatorias. Por encima de 2.5ms permanece en la posición de 180º sin moverse.

<0.5ms ->   ?   (indeterminado)
 0.5ms ->   0º
 1.0ms ->  45º
 1.5ms ->  90º (centro)
 2.0ms -> 135º
 2.5ms -> 180º
>2.5ms -> 180º (inmóvil)

A partir de ahí basta extrapolar para saber cuánto tiene que durar el pulso para situar el motor en la posición que queramos. Luego habrá que tener en cuenta la velocidad de giro.


Esto es todo por ahora. Os dejo el enlace a los archivos aquí.
Artículo completo >>

Dimmer controlado por mando a distancia: el hardware

A este proyecto le he dedicado otras entradas en el blog. Se trata de diseñar y construir un circuito para regular la intensidad de luz de una lámpara utilizando un mando a distancia.

Hoy voy a presentaros el hardware. Para que luego sea más fácil entender el software. He aquí el esquema:


No difiere mucho de los esquemas que encontraréis en otras páginas. Como cabe esperar el corazón del circuito es el microcontrolador que se ve a la derecha. Yo he usado un 12F683 porque es el que tengo para las pruebas pero vosotros podeis usar otros modelos si lo preferís. Para examinar el esquema lo dividimos en cinco partes.

Microcontrolador
No es necesario escribir mucho sobre esta parte.

Se trata del PIC IC1, es el cerebro y a él van conectadas el resto de elementos. Sí es interesante que tenga bajo consumo para no sobrecargar la fuente de alimentación. También conviene contar con un par de temporizadores para hacer más sencilla la rutina de recepción NEC.


Fuente de alimentación
Si queremos que el circuito sea lo más sencillo posible no podemos contar con una fuente de alimentación completa, ni con transformador ni conmunatada. Además se supone que el consumo va a ser muy bajo así que nos decantamos por una fuente de alimentación sin transformador. En inglés se conoce como Transformerless Power Supply y encontraréis abundante información y esquemas en Google. Por mi parte os recomiendo esta Nota de Aplicación de Microchip: Transformerless Power Supplies: Resistive and Capacitive.

Hablamos de R1, R4, R5, R7, D1, D2, C1, C2 y C3. Vamos a entender brevemente cuál es la función de cada componente.

La tensión de red (220V 50Hz en España) se aplica a los terminales X1-1 y X1-2. R5 es un fusible que protege al resto por si hubiera un cortocircuito interno.

C1 es el corazón de esta parte. Se trata de un condensador de tipo X2. Eso significa dos cosas, por un lado que está dimensionado para la tensión de red entre 150V y 250V AC. Y por otro que ante un fallo el condensador queda abierto. Otros modelos pueden fallar y quedar los terminales en corto, aplicando la tensión de red a todo el circuito. Cuando trabajamos con doce voltios nos da igual, pero con 220V hay que cuidar estos detalles o nos podemos llevar una buena hostia. Como dicen en la nota anterior: "si el condensador queda en corto puede reventar algo... literalmente".

R1 limita la intensidad que pasa por C1 cuando este se encuentra descargado al suministrar tensión. Y otro elemento de protección es R4 que garantiza que el condensador no se quede cargado cuando desenchufemos el circuito. Un condensador cargado a 220V da sustos muy desagradables. Es habitual cuando cargamos condensadores a esas tensiones no dejarlos cargados, ya lo vimos también en esta entrada.

Para la tensión alterna C1 representa una resistencia, más baja cuanto mayor su capacidad, sólo que a diferencia de una resistencia C1 no disipa calor (ni consume). El diodo zener D1 estabiliza la tensión en 5V que nos viene estupendamente para alimentar el PIC. D2, C2 y C3 forman un rectificador de media onda, que es apropiado sólo para bajos consumos.

Mientras que C2 es un condensador electrolítico que elimina el rizado de alterna, C3 es un condensador de poliester, de menor capacidad pero de reacción mucho más rápida. Nos suministrará los picos de corriente necesarios por ejemplo para disparar el optotriac sin afectar al voltaje de alimentación del resto del circuito.

R7 es un varistor que deriva los picos de la red que excedan cierto voltaje para que no lleguen al circuito. Es opcional aunque muy recomendable.

Sensor de paso por cero
Una vez tenemos el PIC alimentado necesitamos saber cuando la tensión de red pasa por cero para disparar el triac con un retardo acorde con la energía que queramos.

Mirad el datasheet de un PIC como el 12F683 y fijaos cómo son por dentro las entradas de tipo IO entrada/salida:


La imagen no está sacada del datasheet, sino de esta Nota de Aplicación donde viene más simplificado: Interfacing to AC Power Lines. El caso es que tienen dos diodos limitadores. Lo que quiere decir que aunque apliquemos 220V directamente al pin del PIC no se dañará siempre y cuando limitemos la intensidad. R2 es una resistencia de un valor muy elevado, suficiente para elevar la tensión hasta nivel alto, pero con una intensidad muy baja para no destruir los diodos. La tensión en el puerto GP2 oscilará entre 0 cuando la tensión de red pase por el semiciclo negativo hasta 5V como máximo en el semiciclo positivo. En cualquiera de las dos transiciones sabemos que la tensión acaba de pasar o va a pasar inmediatamente por cero.

Conviene utilizar una entrada de tipo Schmitt trigger (ST) para que la transición sea limpia. De lo contrario los transitorios producidos por el encendido de motores y electrodomésticos pueden causar que el circuito conmute varias veces antes de tiempo y por tanto que no funcione bien.

Disparador
Aunque veréis muchos esquemas que utilizan un triac de baja corriente de disparo (Logic level and sensitive gate triacs) aquí he usado un circuito clásico de triac-optotriac. Es cuestión de gustos, a mi personalmente no me gusta excitar directamente el triac con el micro, y menos aún cuando la fuente no está aislada eléctricamente como sí lo estaría si usásemos una fuente con transformador. Creo en el aislamiento galvánico y los optoacopladores me parecen la mejor opción; pero esto, repito, es solamente mi opinión personal y no quiero decir que de la otra forma no sea igual de válido.

En cuanto al optotriac, hay dos tipos: con detección de paso por cero y sin ella.

En lo que tienen detección de cruce por cero los más comunes son los modelos MOC30XY, donde X indica la máxima tensión nominal:
MOC 3041 -> 400V
MOC 3061 -> 600V
MOC 3081 -> 800V

y la Y indica la corriente que necesita el LED para garantizar el disparo:
MOC 3040 -> 30mA (no existen todos los modelos)
MOC 3041 -> 15mA
MOC 3042 -> 10mA
MOC 3043 ->  5mA

Aunque en el esquema está puesto como tal, no nos interesa un optotriac con detección de paso por cero. Pues no nos permitiría hacer el disparo cuando nosotros queramos. Así que será mejor emplear otros integrados como el MOC3020 o el MOC3030 que no incorporan esa característica. Los primeros son muy importantes para hacer una intermitencia o en general un control de encendido/apagado (por tiempo, por temperatura, etc) pero no precisamente un dimmer.

Además como el disparo lo vamos a hacer con un microcontrolador no es preciso que sea del modelo sensible (los acabados en 3) porque lo que haremos será enviar un pulso muy breve, que será suficiente para encender el triac pero al durar muy poco tiempo no agotará el condensador de la fuente de alimentación. La cosa sería muy distinta si en lugar de un PIC hiciésemos una intermitencia utilizando un timer como por ejemplo un 555 que deja la salida a nivel alto durante todo el periodo que dure la luz encendida.

El TRIAC puede ser cualquiera siempre y cuando soporte la tensión de la red y la carga que vayáis a usar. En este caso al ser una lámpara la carga es resistiva pura, pero si queréis controlar un motor tened cuidado que la cosa se vuelve compleja.

Conectores auxiliares
Son dos, el primero, indicado como SL1 es a donde va conectado el módulo receptor de infrarrojos (hemos hablado de él en otras entradas), y será el que reciba la señal del mando.

Para terminar he incorporado un conector externo que no he usado pero podría venir bien por ejemplo para conectar un LED y que el dimmer se regule automáticamente con la luz ambiente. Mirad esta entrada para ver como se hace un sensor utilizando un simple LED: Sensor óptico sencillo con amplio rango dinámico. En su momento yo usaba esta patilla para depuración conectándola al puerto serie.

Lo bueno de usar microcontroladores es que el circuito es muy simple y siempre se puede ampliar por software. Así que si os sobra un puerto no lo dejéis sin conectar porque el día de mañana se os puede ocurrir una utilidad para él. Si ya tenéis el conector sólo os hace falta reprogramar el chip y no tenéis que hacer otra placa.


Montaje

Esta es la PCB con las pistas vistas por el lado de cobre:




Y así es como quedaría el circuito ya montado:


Observad que el condensador C1 no es de 47nF sino de 100nF. En realidad con un consumo tan bajo no es crítico, eso sí cuanta mayor capacidad más margen de maniobra tendremos.


Conexión del circuito

La tensión de red va conectada a la clema gris de la izquierda. El terminal del centro es común, el de abajo es la entrada de la tensión y el de arriba es la salida hacia la bombilla.

A la derecha está el conector de tres pines a donde va conectado el módulo IR. Se hace así para que el circuito pueda estar escondido y tan sólo quede expuesto el receptor.

Se trata de un circuito lo suficientemente pequeño como para que quepa dentro del plafón o de la caja de registro más cercana.



Por último os dejo los esquemas (para Eagle), las imágenes y un PDF con las pistas en este enlace.
El software está publicado en esta entrada.
Artículo completo >>

Decodificar Aiken Biphase con Perl

Antes de nada quería mandar un saludo a Explorer de perlenespanol.com. Porque, sin conocernos previamente, se ve que le gustan mis artículos. Pues casi desde que empecé con el blog todo lo que escribo que tenga que ver con Perl acaba reseñado en su foro.

Ya hemos codificado y decodificado una señal digital otras veces para extraer información. Hemos decodificado señales NRZ, Manchester, etc. Hoy vamos a hablar de un tipo de señal llamado FM1, Biphase Mark Code (BMC) o también Aiken Biphase. Es un tipo de FSK ampliamente utilizado. Vamos a ver uno de los sitios donde se usa y lo usaremos como ejemplo para construir un programita que lo decodifique.

El soporte de datos que vamos a usar como ejemplo nos exigirá hacer uso de la característica de auto sincronía (self clocking) de la señal. Si seguís leyendo ya veréis el motivo.


Self Clocking

En la mayoría de circuitos digitales hay un mecanismo de coordinación de todo el esquema. Un reloj. No es más que oscilador de onda cuadrada a una frecuencia fija, pero hace que los componentes actúen como un todo. Y lo que es más importante, puedan interactuar con otros sistemas digitales.

Pero los osciladores no son tan exactos como nos gustaría. Los relojes se atrasan y se adelantan, los sistemas se descoordinan y la transmisión de información falla. Pero hay una forma fácil de evitarlo: hacer que en cada bit, ya sea uno o cero, la señal cambie de estado. Así el receptor reconoce fácilmente cuando empieza y cuando acaba cada bit, y si el transmisor tiene una deriva temporal puede adaptarse a ella.

Vamos a ver un ejemplo de esto que os digo. En una codificación Aiken Biphase la frecuencia del cero es la mitad que la del uno. En el gráfico siguiente hay un pico de señal en cada transición. Es decir, dos picos juntos es un uno, dos picos separados es un cero. Veremos esto más despacio pues por ahora lo que me interesa que veáis es cómo hacia el final se van juntando los impulsos.


Pinchad para ampliar la imagen. Fijaos que lo que hay al comienzo y al final son ceros. Y entre medias hay algunos unos. Pero los ceros del final están más juntos que los del principio. Es decir que la trnasmisión se ha ido acelerando en el tiempo. Y si el receptor no se adapta, e insiste en interpretar la señal del final con el mismo patrón que la del principio acabará detectando los ceros más juntos como unos.

Como ya lo sabemos, nuestro programa tiene que irse adaptando a la velocidad de la señal en tiempo real, tanto si se acelera como si desacelera, dentro de unos márgenes, porque si no lo más probable es que falle.

¿Pero por qué tanto insistir en este aspecto? Un oscilador puede tener cierta deriva temporal, pero no tanto. Pasará desapercibido a menos que sean velocidades muy altas. El problema viene cuando mezclamos lo digital con lo analógico.

La señal del gráfico anterior es la lectura de una banda magnética de una tarjeta de identificación. Obtenida al recorrer la banda con un cabezal lector de un casete (un fonocaptor magnético para ser pedantes). La aceleración viene por el hecho de que es muy difícil mover el brazo a una velocidad constante; aunque con práctica se consigue, sería de vagos no aprovechar el self clocking de la señal que precisamente está para eso.


Bandas magnéticas

Por si os ha sorprendido: sí, se puede. Con un cabezal de cinta se pueden leer y hasta grabar tarjetas de identificación o de crédito, billetes de aparcamiento y cualquier otro soporte que use banda magnética para almacenar datos. Hay mucha información en Internet y experimentos. Si os interesa el tema os recomiendo especialmente una página que de haberla descubierto antes me habría ahorrado un montón de trabajo: http://www.gae.ucm.es/~padilla/extrawork/stripe.html.

Casi todas las tarjetas se adhieren a un formato concreto. Por la sencilla razón de que es más fácil implementar una norma que ya está hecha y se usa, que diseñar un sistema desde cero. No obstante también los hay que usan sus propios formatos no estándar. Por lo general casi siempre se usa Aiken Biphase pero no es el único sistema.

Doy brevemente unas pinceladas sobre cómo se graba la información en una banda magnética para que entendáis de dónde viene la forma de la señal que vamos a tratar. El siguiente dibujo está sacado de http://www.gae.ucm.es/~padilla/extrawork/card-o-rama.txt. La cabeza grabadora/lectora es simplemente un anillo de un metal ferromagnético pero no cerrado, sino con un hueco diminuto en la pate que se expone a la cinta. Hay un cable que está enrollado al anillo, con muchas muchas espiras, y es con el que captaremos la señal, o la grabaremos.

                        | |  <----cables al amplificador      
                        | |       (se enrollan al anillo)
                      /-|-|-\
                     /       \
                     |       | <----solenoide (acaba de cambiar la polaridad)                           
                     \       /
                      \ N S / <---hueco en el anillo
N----------------------SS-N-------------------------S
                       ^^  
             <<<<<-la banda se mueve en esta dirección

Imaginaos la banda como millones de micro-imanes todos seguidos. Si no hay nada grabado todos los imanes están orientados en la misma dirección. N es el polo norte del imán y S el polo sur. Así:

N---------------------------------------------------S

Para grabar información digital lo que hacemos recorrer la banda con el solenoide e ir cambiando la polaridad cuando nos interese invertir el flujo.

N---------S S-------N N----------S S---------N N----S

Cuando recorremos la banda con la cabeza de lectura los imanes cierran el anillo y fijan una dirección del flujo magnético, que se establece de acuerdo al dominio magnético con el que esté en contacto el cabezal en ese momento.

            /-|-|-\
           /       \
           |       | <----solenoide 
           \       /
            \ N S / 
N--------------------S S--------------N N-----------S

Ahora avanzamos por la cinta y nos encontramos con un cambio en la polaridad magnética. Como el flujo que atraviesa el anillo lo fija la banda magnética, se invierte, y pasa de ser N-S a S-N.

                           /-|-|-\
                          /       \
                          |       | <----solenoide 
                          \       /
                           \ S N /  (ha cambiado la dirección)
N--------------------S S--------------N N-----------S

Y ya sabíamos que al cambiar el flujo magnético en un solenoide se induce una corriente eléctrica. En este caso va a ser muy débil, pero suficiente para detectarla. Cuando avanzamos más vuelve a cambiar el flujo y se induce una corriente en el otro sentido.

Ya os habéis dado cuenta de que sólo se induce cuando cambia el flujo, y ese cambio va a ser casi instantáneo. Así que esperamos ver picos puntuales. Pero ya sabéis que en la naturaleza la línea recta no existe, y en la electrónica menos aún. Luego lo que vamos a ver es esto:


Ya veis la que la regla del Aiken Biphase es sencilla, os pego esta imagen de la Wikipedia.




Preamplificador de entrada

Hemos dicho que no se necesita electrónica ninguna para conectar el lector a la tarjeta de sonido. Salvo un condensador, porque si recordáis como es la entrada de una tarjeta de sonido sabréis que si lo conectáis tal cual hay una corriente continua atravesando el lector. Con tan mala suerte que es bastante para borrar el contenido de la banda magnética. Así que no sólo no leeréis nada sino que borraréis lo que hubiera escrito.

Aunque no es necesario un amplificador, yo sí voy a utilizarlo porque facilita captar correctamente la señal. Este es el circuito, que como veis no es más que un amplificador inversor que habíamos explicado ya. Este tiene una ganancia de 7.5 veces aproximadamente. Los componentes no son críticos, sirve casi cualquier operacional y las resistencias que tengáis por el taller.


El condensador de entrada merece mención aparte. Si usamos capacidades muy bajas, por debajo de 47nF vamos a hacer que las frecuencias bajas estén muy atenuadas. Y el valle que hay entre los picos se deformará apareciendo un pico secundario a modo de rebote. Y el lector se va a confundir con esos picos. En cambio si usamos una capacidad alta por encima de 10uF la forma de la onda no se va a deformar apenas, y los picos aparecerán claros y contundentes. Pero también se van a colar ruidos de baja frecuencia que hacen que el tren de pulsos suba y baje como en una montaña rusa. Aunque el algoritmo adaptativo que os presento tolera esas variaciones, si podemos es mejor que nos las quitemos. Yo he hecho las pruebas con una capacidad de 470nF y funciona aceptablemente.


Tratamiento digital previo

Conectando la cabeza lectora a la entrada de micro de la tarjeta ya llega con suficiente señal para detectarla bien. Aunque para las pruebas he utilizado un pequeño amplificador operacional a la entrada.

Os cuento ahora algunos pasos que damos en la detección para que entendáis mejor el programa que sigue. Lo primero que vamos a hacer es tomar el valor absoluto de la señal recibida. Porque sólo nos interesa saber cuando hay un pico. Si es positivo o negativo no nos importa. Lo segundo es pensar si filtramos la señal. Como siempre que nos interesa detectar picos, una solución socorrida es elevar la señal a alguna potencia para aumentar la relación señal-ruido. Esta es la señal en valor absoluto:


La imagen que sigue es al cuadrado. Vemos que el ruido ha disminuido y los picos están más claros. Observad el cambio de escala porque estamos elevando valores menores que uno.


Pero hay que tener cuidado con esta técnica. Primero porque en un ordenador es muy fácil elevar a una potencia, pero si usamos un integrado tipo PIC o DSP es bastante más chungo. Y segundo porque tiene sus desventajas. Esta es la misma señal a la cuarta potencia:


Mirad como se amplifica la diferencia entre los picos. Esta técnica viene muy bien si lo que hacemos es fijar un umbral y todo lo que hay por encima decimos que es un pico.

Una de las decisiones más difíciles es dónde colocar el umbral del ruido. Cuando tenemos el archivo completo delante de nuestras narices lo vemos clarísimo, pero de alguna manera tiene que saber el programa que empieza una lectura y no es ruido.

En el programa de debajo vamos a usar un método adaptativo que tiene en cuenta dos cosas. Por un lado hay un umbral de ruido que tiene un cierto valor que nosotros le damos. Más o menos en función de lo ruidoso que sea el ambiente y de la ganancia del preamplificador. Y por el otro lado hay una variable que llega hasta 0.6 veces el valor del pico anterior. Fijamos el umbral en el más alto de esos valores. Así cuando hay una señal fuerte el umbral se sube automáticamente y evitamos captar nada que no sean picos.

Nuestra mala suerte es que ese algoritmo de ajuste no funciona bien si hacemos lo de elevar al cuadrado. Así que no alteraremos la señal de entrada.


El algoritmo

Ya hemos nombrado antes dos parámetros que varían: uno es la distancia entre picos (o la frecuencia de reloj, o la velocidad, o como queráis llamarlo); y otro la amplitud de la señal. Francamente nos interesa mucho más el primero porque el segundo es cuestión de fijar un umbral a mano y ya está. El tiempo como tal no nos interesa, lo que vamos a mirar es la distancia en muestras. Si la frecuencia de de muestreo es de 44100Hz quiere decir que cada 44100 muestras habrá transcurrido un segundo, pero eso también nos da igual.

Para controlar el tiempo no podemos (o no debemos) fiarnos de la última medida, porque pudiéramos estar en un error y alterar los bits siguientes. Así que lo que hacemos es usar una media móvil exponencial de los últimos bits leídos. Es una especie de filtro IIR paso bajos para que una mala lectura no cause estragos. En cuanto leemos un bit y determinamos si es un CERO o un UNO realimentamos la variable. Como las aceleraciones o deceleraciones no son bruscas sino que son graduales, a la media móvil le da tiempo de adaptarse bit a bit. Aunque los bits finales estén mucho más juntos o mucho más separados que los del principio, como el cambio se ha hecho poco a poco el tiempo entre UNOS (que es como llamamos a la variable) se ha ido adaptando progresivamente.

Ya hemos visto antes cómo decodificarlo. Pero yo lo voy a hacer de otra manera aprovechando que tengo un ordenador con un lenguaje de alto nivel y no un integrado. Llamadme vago. Voy a considerar que es un UNO cuando el impulso siguiente venga separado una distancia (en muestras) parecida a Tuno y que es un cero cuando venga a una distancia parecida al doble de Tuno. La desventaja es que los UNOS me salen duplicados, porque un UNO son dos picos juntos, que yo detectaré como dos unos separados. Pero no os preocupéis que lo arreglamos después. Lo he hecho así porque viene muy bien para leer bandas con formato desconocido, que pueden no ser Aiken Biphase.

Hemos hablado antes de una duración parecida a Tuno o al doble de Tuno ¿Pero cuánto es parecida? Bueno pues vamos a tomar un criterio sencillo. Tomamos tres valores:
el valor es        Tuno
su mitad es    1/2*Tuno
y su doble es    2*Tuno

Al principio Tuno puede ser la duración del CERO o del UNO (hablamos de esto en el párrafo siguiente). Si recibimos una duración entre pulsos equivalente a Tuno diremos que es el mismo carácter que tiene Tuno. Si sabemos que eran UNOS pues diremos que llega un UNO. Si llega una duración equivalente al doble, diremos que hemos recibido un CERO. Pero si llega una duración equivalente a la mitad de Tuno pueden pasar dos cosas: durante la inicialización servirá para discriminar que Tuno era en realidad el tiempo del CERO y no del UNO. Pero pasada la etapa de inicialización se tratará de un error. Lo mismo que si la medida supera el doble. Así dado un tiempo t tenemos 5 intervalos:
error si            t < 1/4*Tuno
mitad si 1/4*Tuno < t <= 3/4*Tuno
igual si 3/4*Tuno < t <= 3/2*Tuno
doble si 3/2*Tuno < t <= 5/2*Tuno
error si 5/2*Tuno < t


Pero ¡un momento! Habrá que inicializar Tuno de alguna manera. Generalmente al principio de la lectura se repite mucho uno de los dos bits, que suele ser CERO para que el receptor se entere de la velocidad de transmisión. Vamos a intentar dar una vuelca de tuerca y a hacer que nuestro decodificador sea inteligente y sepa cuándo los caracteres iniciales sean CEROS y cuando UNOS. Pero eso no lo puede saber hasta que no encuentre un bit diferente. Si este dura la mitad es que lo de antes eran CEROS. Pero si dura el doble es que lo de antes eran UNOS. Por eso al empezar a leer estamos leyendo caracteres "T", que no sabemos si son UNOS o CEROS hasta leer otro diferente para poder comparar.

Cuando ya tenemos una cadena de unos y ceros se la pasamos a las rutinas que decodifican los formatos conocidos. No voy a entrar en cómo calcular la paridad ni el bit de LRC. Si os interesa hay mucha información. Lo que me gustaría es que os fijarais en que se usa paridad IMPAR. Y es por una razón muy sencilla si lo pensáis. Con paridad PAR un byte que sea 0, o sea todo CEROS, su bit de paridad también es CERO. Con lo que si tenemos muchos bytes 0 seguidos nos encontramos con una cadena de bit 0 todos iguales. En cambio usando paridad IMPAR el bit de paridad es 1, así que se obliga a que por lo menos uno de los bits del grupo sea distinto. Y así se favorece la sincronía. Un ejemplo con grupos de 5 bits + 1 de paridad.


Paridad par (siempre el mismo bit):
000000 000000 000000 000000 000000

Paridad impar (se obliga a un bit distinto):
000001 000001 000001 000001 000001


El programa

Pego el programa, parece largo pero quiero dejarlo íntegro porque está muy comentado. Os doy unas pinceladas breves y si decidís que os interesa ampliáis información leyendo en los comentarios en el código.

Así por encima, distinguimos cuatro partes:
  • Inicio, hasta la linea 67. Donde abrimos el dispositivo del que leeremos las muestras, ya sea un archivo de disco o la tarjeta de sonido.
  • Bucle principal. Hay una rutina nada más empezar el bucle que se para hasta detectar un pico. Es decir que el bucle sólo reacciona a los picos de señal. Fijaos cómo los primeros picos sólo se usan para inicializar variables y a medida que llegan más picos vamos avanzando y llegando más adentro en el bucle. Hasta que del tercer pico en adelante ya pasamos a la parte donde se discrimina la duración para ver si es un UNO o un CERO.
  • Rutinas de la señal. Aproximadamente entre las líneas 199 y 324. Tratan diversos aspectos de la señal que aún es sonido.
  • Rutinas de decodificación. Cuando la señal ya no es sonido sino una hilera de bits, entonces pasamos a decodificarla.

Tenemos una variable que activa o desactiva el modo depuración. Cuando el script está en modo depuración escribe por pantalla abundante información sobre cuándo ha detectado un pico, de qué duración, en qué muestra, etc.

Como ejercicio si te interesa, intenta buscar la respuesta a estas preguntas (casi todo está en los comentarios):
  • ¿Cual es el criterio de intervalos para Tuno y por qué se ha hecho así?
  • ¿Donde se hace la realimentación de las medias móviles y por qué sólo ahí?
  • ¿Donde se aplica la histéresis al Umbral de ruido?

#!/usr/bin/perl 
#===============================================================================
#
#         FILE:  decodifica.pl
#
#        USAGE:  ./decodifica.pl [fichero.wav]
#                Si no se da fichero.wav se lee del dispositivo de grabación hw:0,0.
#
#  DESCRIPTION:  Recibe un archivo de sonido e intenta decodificar la información que 
#                contiene la banda magnética.
#
#                Esta utiliza los picos y medias móviles para adaptarse a la velocidad
#                de lectura variable.
#
#      OPTIONS:  ---
# REQUIREMENTS:  sox
#         BUGS:  ---
#        NOTES:  ---
#       AUTHOR:  Reinoso Guzman
#      VERSION:  1.0
#      CREATED:  14/11/10 12:16:09
#===============================================================================

use strict;
use warnings;
use List::Util qw(max);

my $alphaVpico = 0.33;    # Para la media móvil del nivel de pico.
                          # Si la sigue mucho se va hasta el nivel de ruido
           # y no corta la lectura.        
my $alphaTuno  = 0.33;    # Para la media móvil del intervalo del UNO.
my $umbralInicial = 0.4;

my $debug = 0;


my $file = $ARGV[0];
if ($file and ! -e $file) {
 die "El fichero $file no existe o no se puede leer.\n";
}

my $data;
if ($file) {
 open $data, "sox $file -t dat - |" or die "Error: $!\n";
}
else {
    $ENV{AUDIODEV} = "hw:0,0";
 open $data, "rec -q -r 48000 -t alsa hw:0,0  -t dat - |" or die "Error: $!\n";
}


my $muestra;     # No usamos el tiempo sino cuantas muestras,
                 # así no depende de la velocidad de lectura.
                 # Esta variable cuenta por qué muestra vamos.

my $nbits;       # Para contar cuantos picos van.
my $Tuno;        # Tiempo del 1, el del 0 será el doble (media movil).
my $Vpico;       # Valor de pico (media movil).

my $last_pico;   # Para calcular el tiempo entre transiciones
my $string;      # Cadena leída, por ahora vacía.
my $trailChar;   # Caracter inicial a priori no sabemos si es 0 o 1
my $umbral;      # Lo inicializamos más adelante
my $leyendo;     # Indica si estamos en mitad de una lectura

inicializar();

while (1) { # sale cuando get_sample se quede sin datos
 my $intervalo; # duración entre el pico anterior y el proximo que encontremos.

 espera_senal() or next;
 my ($pos_pico, $valPico) = procesa_pico($data);

 # Estamos en el primer pico, aún no tenemos la mitad de las variables
 # definidas. Definimos los valores iniciales y no hacemos nada más.
 if (not defined $last_pico) {
  $last_pico = $pos_pico;
  $Vpico     = $valPico;
  print "\n------------- Comienza nueva lectura en la muestra $muestra.\n";
  $leyendo = 1;

  # Bajamos el umbral en cuanto llega el primer impulso para
  # intentar captar los demás.
  $umbral = 0.70*$umbralInicial;
  # No seguimos.
  next;
 }

 # Está definido last_pico, luego ya ha habido un pico anterior y
 # este puede ser del segundo en adelante. Ya podemos hablar de intervalo
 # entre dos picos.
 $intervalo = $pos_pico - $last_pico;
 print "Pico en $pos_pico.   Duración: $intervalo.   Valor: $valPico.\n" if $debug;
 
 # Si el pico dura muy poco (menos que 3 muestras) es sospechoso de ser debido
 # al ruido. Si además el valor es muy próximo al umbral lo ignoramos.
 # Consideramos corta duración si es menor que 1/4 de Tuno, pero puede no estar
 # definido Tuno. En ese caso que dure menos de 1/4*24 = 6 muestras.
 if ($intervalo < 1/4*($Tuno||24) and $valPico <= 1.1 * $umbral) {
  print "Pico ingnorado\n" if $debug;
  next;
 }

 # Si hemos llegado aquí se trata de un pico válido. Actualizamos. 
 $last_pico = $pos_pico;

 # Si aún no está definido $Tuno Estamos en el segundo pico: 
 # lo inicializamos al primer intervalo que pillemos. Y luego veremos si eran
 # cero o eran unos.
 if (not defined $Tuno) {
  $Tuno = $intervalo;
  $string .= $trailChar; # no sabemos cual es el caracter inicial.
  next;
 }


 # Este es a partir del tercer pico, ya tenemos una referencia con la que comparar.
 # Juzgamos si es de la misma duración que el anterior del doble o de la mitad.

 # La duración t puede ser:
 #   1/2T   si  1/4T < t <= 3/4T
 #      T   si  3/4T < t <= 3/2T
 #     2T   si  3/2T < t <= 5/2T
 #   indefinida en otros casos.
 
 # Es de la misma duración que Tuno
 if ($intervalo > 3/4*$Tuno and $intervalo <= 3/2*$Tuno) {
  # Si aún no hemos recibido nada diferente para comparar no sabemos
  # si Tuno es la duración del UNO o del CERO.
  if ($trailChar eq "T") {
   $string .= $trailChar;
  }
  # Si ya sabemos que Tuno es del uno pues lo ponemos
  else {
   $string .= "1";
  }
  
  $Tuno  = $alphaTuno  * $intervalo + (1-$alphaTuno)  * $Tuno;
  $Vpico = $alphaVpico * $valPico   + (1-$alphaVpico) * $Vpico;
 }

 # Recibimos un símbolo de duración el doble: un CERO
 elsif ($intervalo > 3/2*$Tuno and $intervalo <= 5/2*$Tuno) {
  # Confirmamos, si no lo sabíamos, que Tuno es el tiempo del UNO
  # porque acabamos de recibir un CERO.
  if ($trailChar eq "T") {
   $trailChar = "1";
   $string =~ s/T/$trailChar/g;
  }
  # Y añadimos el CERO recién recibido a la vez que realimentamos
  # la media móvil que lleva el tiempo del UNO.
  $string .= "0";
  $Tuno  = $alphaTuno  * $intervalo/2 + (1-$alphaTuno)  * $Tuno;
  $Vpico = $alphaVpico * $valPico     + (1-$alphaVpico) * $Vpico;
 }

 # Recibimos un símbolo de duración la mitad: un UNO
 elsif ($intervalo > 1/4*$Tuno and $intervalo <= 3/4*$Tuno) {
  # Si no sabíamos cual era el caracter inicial eso quiere decir que son 
  # CEROS y no UNOS. Porque acabamos de recibir el primer UNO.
  # Así que rectificamos la cadena y la duración del UNO.
  if ($trailChar eq "T") {
   $trailChar = 0;
   $string =~ s/T/$trailChar/g;
   $Tuno = $Tuno / 2; # Rectificamos la duración
  
   $string .= "1";
   $Tuno  = $alphaTuno  * $intervalo + (1-$alphaTuno)  * $Tuno;
   $Vpico = $alphaVpico * $valPico   + (1-$alphaVpico) * $Vpico;
  }

  # Pero si ya habíamos determinado la duración del UNO y nos llega un
  # pulso que dura la mitad, es que hay algo que está mal. 
  # Se tratará de un error.
  else {
   $string .= "M";
  }
 }

 # Dura más o menos de lo esperado, se trata de un error, hemos perdido algo.
 # Evitamos alimentar $Tuno con una medida errónea y con suerte el resto de
 # bits los recibiremos correctamente.
 elsif ($intervalo < 1/4*$Tuno) {
  $string .= "_";
 }

 elsif ($intervalo > 5/2*$Tuno) {
  $string .= "^";
 }

 print "Tuno = $Tuno      Vpico = $Vpico      Umbral = ". max($umbral, 0.7*($Vpico||0))."\n" if $debug;
}


print "\n";



# Sale en cuanto haya una señal.
# Mientras Vpico está si definir sólo cuenta el umbral
# Si pasa mucho tiempo con ruido decimos que es otra lectura diferente
sub espera_senal {
 my $muestras_ruido = 0;
 my $valor;
 while (defined ($valor = get_sample($data)) and $valor < max($umbral, 0.6*($Vpico||0))) {
  $muestras_ruido++;
  # El tiempo del cero tiene que ser como mucho el doble que el del uno
  # pero si pasa el triple asumimos que se ha terminado la lectura.
  if ($leyendo and defined $Tuno and $muestras_ruido > 3*$Tuno) {
   print "Ruido durante $muestras_ruido > 3*$Tuno\n" if $debug;
   fin_lectura(); # des-define Tuno y saldría del bucle
   return undef;
  }
  elsif ($leyendo and not defined $Tuno and $muestras_ruido > 100*44100/1000) {
   print "Ruido durante $muestras_ruido\n" if $debug;   
   fin_lectura();
   return undef;
  }
 }
 return $valor;
}


# Procesa lo que es un pico, devuelve la posición y el valor máximo
sub procesa_pico {
 my $maximo   = $umbral;  # Valor de pico
 my $posicion = $muestra; # Posición del pico
 my $valor    = 0; # Valor de la muestra actual
 print "Entra pico: $muestra " if $debug;
 while (($valor = get_sample($data)) > max($umbral, 0.6*($Vpico||0))) {
  if ($valor >= $maximo) {
   $posicion = $muestra;
   $maximo   = $valor;
  }
 }
 print "Sale pico: $muestra.    Valor: $maximo en $posicion\n" if $debug;
 return ($posicion, $maximo);
}


# Inicializa las variables para una nueva lectura
sub inicializar {
 print "Variables reseteadas.\n" if $debug;
 $last_pico = undef;
 $Tuno      = undef;
 $string    = "";
 $Vpico     = undef;
 $trailChar = "T";
 $leyendo   = 0;

 # El umbral tiene histéresis:
 #   Cuando no estamos leyendo es un 10% superior al fijado.
 #   Pero en cuanto se detecte la primera señal bajamos al 75%
 #   por si luego llegan señales débiles.
 $umbral    = $umbralInicial * 1.1;

 printf "Umbral de señal fijado en %3.4f\n", $umbral if $debug;
}

# Obtiene una muestra a partir del descriptor abierto
# Lee la línea y extrae la información arpopiada.
# Incrementa la posición contando una muestra más.
sub get_sample {
 my $data = shift;

 my $linea;
 while ($linea = <$data>) {
  next unless $linea;

  my (undef, $time, $valor) = split /\s+/, $linea;
  next unless $time =~ /\d+/; # Saltar las lineas no numéricas.
  
  # Preprocesamos la señal para hacer más evidentes lo picos.
  $valor = abs($valor);
  #$valor = 0.90*$valor + (1-0.90)*$valor_old;
  #$valor = $valor*$valor*$valor;

  $muestra++;
  return $valor if defined $valor;
 }

 
 print "Fin de los datos en la muestra $muestra\n";
 fin_lectura();
 exit; # Se acabaron los datos.
}


sub fin_lectura {
 # No hace nada si no estábamos leyendo.
 return undef unless $leyendo;

 # Si estábamos leyendo termina.
 print "Terminamos en la muestra $muestra\n";
 if ($string and length $string > 10) {
  #print "String: $string\n";
  print_data_stream($string);
  print "-------------------------------------------------------\n\n";
 }
 else {
  print "Lectura vacía.\n" if $debug;
 }

 inicializar();
}



sub print_data_stream {
 my $string = shift;

 $string =~ s/11/1/g; # apaño porque los 1 salen en parejas
 my $bits = length($string);
 print "Crudo: $string\n";
 print "Bits: $bits\n\n" if $bits;


 # Intentamos decodificarla con todo lo que podría ser.
 # Y comprobamos los bit de paridad, lrc, etc
 my ($decoded, $chars, $perrors, $LRCerror);
 
 ($decoded, $chars, $perrors, $LRCerror) = decode_ALPHA($string);
 if ($decoded) {
  printf "Formato:            ALPHA\n";
  printf "Caracteres:         $chars\n";
  printf "Errores de paridad: $perrors\n";
  printf "Comprobación LRC:   %s\n", $LRCerror ? "No válido." : "¡Correcto!";
  printf "Contenido:          %s\n", $decoded;

#  if ($chars and $chars > 10 and $perrors < 2) {
#   print "Anotada.\n";
#   open my $fh, ">> log";
#   print $fh "$string : $decoded\n";
#   close $fh;
#  }
 
  return;
 }

 ($decoded, $chars, $perrors, $LRCerror) = decode_BCD($string);
 if ($decoded) {
  printf "Formato:            BCD\n";
  printf "Caracteres:         $chars\n";
  printf "Errores de paridad: $perrors\n";
  printf "Comprobación LRC:   %s\n", $LRCerror ? "No válido." : "¡Correcto!";
  printf "Contenido:          %s\n", $decoded;

#  if ($chars and $chars > 10 and $perrors < 2) {
#   print "Anotada.\n";
#   open my $fh, ">> log";
#   print $fh "$string : $decoded\n";
#   close $fh;
#  }
  return;
 }
}


# Sustituye los grupos por su correspondiencia
sub decode_BCD {
 # http://www.gae.ucm.es/~padilla/extrawork/card-o-rama.txt
 my %BCD_table = (
  '00001' => '0',
  '10000' => '1',
  '01000' => '2',
  '11001' => '3',
  '00100' => '4',
  '10101' => '5',
  '01101' => '6',
  '11100' => '7',
  '00010' => '8',
  '10011' => '9',
  '01011' => ':',
  '11010' => ';', # Start Sentinel
  '00111' => '<',
  '10110' => '=', # Field Separator
  '01110' => '>',
  '11111' => '?', # End Sentinel
 );
 my $string  = shift;
 my $decoded = "";
 my $errores = 0;
 my $chars   = 0;
 my @LRC;
 
 ($string) = $string =~ /(11010([01\?]{5})+11111[01\?]{5})/;
 if (not $string) {
  print "DecodeBCD: Error de formato.\n" if $debug;
  return (undef);
 }

 for (my $i = 0; $i < length($string); $i+= 5) {
  my $grupo = substr ($string, $i, 5);
  if (exists $BCD_table{$grupo}) {
   $decoded .= $BCD_table{$grupo};
   $LRC[$_] = ($LRC[$_]||0) ^ substr($grupo, $_, 1) for (0..3);
  }
  else {
   $decoded .= "_";
   $errores++;
  }
  $chars ++;
 }

 return ($decoded, $chars, $errores, any(@LRC));
}

# Sustituye los grupos por su correspondiencia
sub decode_ALPHA {
 # http://www.gae.ucm.es/~padilla/extrawork/card-o-rama.txt
 my %ALPHA_table = (
  "0000001" => ' ', # (0H)   Special
  "1000000" => '!', # (1H)      "
  "0100000" => '"', # (2H)      "
  "1100001" => '#', # (3H)      "
  "0010000" => '$', # (4H)      "
  "1010001" => '%', # (5H)   Start Sentinel
  "0110001" => '&', # (6H)   Special
  "1110000" => "'", # (7H)      "
  "0001000" => '(', # (8H)      "
  "1001001" => ')', # (9H)      "
  "0101001" => '*', # (AH)      "
  "1101000" => '+', # (BH)      "
  "0011001" => ',', # (CH)      "
  "1011000" => '-', # (DH)      "
  "0111000" => '.', # (EH)      "
  "1111001" => '/', # (FH)      "

  "0000100" => '0', # (10H)    Data (numeric)
  "1000101" => '1', # (11H)     "
  "0100101" => '2', # (12H)     "
  "1100100" => '3', # (13H)     "
  "0010101" => '4', # (14H)     "
  "1010100" => '5', # (15H)     "
  "0110100" => '6', # (16H)     "
  "1110101" => '7', # (17H)     "
  "0001101" => '8', # (18H)     "
  "1001100" => '9', # (19H)     "

  "0101100" => ':', # (1AH)   Special
  "1101101" => ';', # (1BH)      "
  "0011100" => '<', # (1CH)      "
  "1011101" => '=', # (1DH)      "
  "0111101" => '>', # (1EH)      "
  "1111100" => '?', # (1FH)   End Sentinel
  "0000010" => '@', # (20H)   Special

  "1000011" => 'A', # (21H)   Data (alpha) 
  "0100011" => 'B', # (22H)     "
  "1100010" => 'C', # (23H)     "
  "0010011" => 'D', # (24H)     "
  "1010010" => 'E', # (25H)     "
  "0110010" => 'F', # (26H)     "
  "1110011" => 'G', # (27H)     "
  "0001011" => 'H', # (28H)     "
  "1001010" => 'I', # (29H)     "
  "0101010" => 'J', # (2AH)     "
  "1101011" => 'K', # (2BH)     "
  "0011010" => 'L', # (2CH)     "
  "1011011" => 'M', # (2DH)     "
  "0111011" => 'N', # (2EH)     "
  "1111010" => 'O', # (2FH)     "
  "0000111" => 'P', # (30H)     "
  "1000110" => 'Q', # (31H)     "
  "0100110" => 'R', # (32H)     "
  "1100111" => 'S', # (33H)     "
  "0010110" => 'T', # (34H)     "
  "1010111" => 'U', # (35H)     "
  "0110111" => 'V', # (36H)     "
  "1110110" => 'W', # (37H)     "
  "0001110" => 'X', # (38H)     "
  "1001111" => 'Y', # (39H)     "
  "0101111" => 'Z', # (3AH)     "

  "1101110" => '[', # (3BH)    Special
  "0011111" => '\\', # (3DH)   Special
  "1011110" => ']', # (3EH)    Special
  "0111110" => '^', # (3FH)    Field Separator
  "1111111" => '_', # (40H)    Special
 );
 my $string  = shift;
 my $decoded = "";
 my $errores = 0;
 my $chars   = 0;
 my @LRC;
 
 ($string) = $string =~ /(1010001([01\?]{7})+1111100[01\?]{7})/;
 if (not $string) {
  print "Decode ALPHA: Error de formato.\n" if $debug;
  return;
 }

 for (my $i = 0; $i < length($string); $i+= 7) {
  my $grupo = substr ($string, $i, 7);
  if (exists $ALPHA_table{$grupo}) {
   $decoded .= $ALPHA_table{$grupo};
   $LRC[$_] = ($LRC[$_]||0) ^ substr($grupo, $_, 1) for (0..5); # paridad no entra en LRC
  }
  else {
   $decoded .= "_";
   $errores++;
  }
  $chars++;
 }

 return ($decoded, $chars, $errores, any(@LRC));
}


# One argument is true
sub any { $_ && return 1 for @_; 0 }


Conclusiones

Y para terminar os acompaño una captura para que veais cómo una secuencia de dominios magnéticos apropiadamente colocados se traduce en unos y ceros, y esa información binaria se decodifica en información útil. La salida no es del programa de arriba sino de una versión anterior.


A veces cuesta un poco leer las pistas de alta densidad porque el lector que usamos es más estrecho para el ancho de la banda. Pero no quiere decir que no vayamos a poder leer, sino que cuesta más obtener una señal válida.

Ni que decir tiene que si has leído hasta aquí es porque estás interesado en la parte didáctica del artículo. Porque, si fueras un delincuente no estarías perdiendo el tiempo leyendo sobre electrónica digital y lectores de cintas de cassete sino que estarías clonando ya tarjetas con un grabador comercial comprado por Internet.
Artículo completo >>

Contraseña dinámica para acceder al PC de casa

En ocasiones necesitamos acceder a nuestro ordenador desde fuera de casa. No hay problema, instalamos un servidor SSH y desde cualquier ordenador con Linux, o con PuTTY o SecureCRT instalado nos podemos conectar y ejecutar comandos o ver el correo como si estuviéramos delante mismo de la consola en casita.

El problema viene cuando nos conectamos desde ordenadores no seguros. Qué se yo, un cyber-café, un puesto de acceso libre en alguna party, o el ordenador de un amigo o no tan amigo. Estos sitos no seguros pueden tener instalados algún tipo de troyano o programa semejante para capturar las contraseñas que la gente mete. Puesto que la contraseña de acceso siempre es la misma (salvo que la cambiemos) con que alguien nos la robe en un descuido ya puede andar por nuestro PC de casa sin problemas. Y si tenemos alguna de estas distribuciones con el sudo abierto pues puede organizar un desaguisado de mucho cuidado.

La solución pasa entonces por tener una clave que cambie cada día, o mejor aún que cada vez que accedamos sea una distinta. Así al intruso no le bastará con sólo capturar la clave una vez, porque no servirá para la próxima vez que intente entrar.

Pero la pregunta es, si cada vez que entre la clave es distinta ¿cómo sé yo con qué clave tengo que entrar? Pues una solución sencilla podría ser que el ordenador nos mande un desafío y nosotros tengamos que responder siguiendo un algoritmo que hayamos programado previamente. Esa es la dinámica del programa que os quiero presentar hoy.



El algoritmo

En este caso la clave es el algoritmo. Al espía no se bastará capturar una clave o veinte, necesitará descubrir con qué algoritmo respondemos. Porque si intenta entrar y su respuesta es errónea, el programa nos alertará.

Hay algoritmos que aunque el intruso sepa el algoritmo sería incapaz de generar una clave válida a partir de una clave usada. Es el caso del intercambio de claves Diffie-Hellman basado en el problema del logaritmo discreto. Pero como no es cuestión de teclear varias decenas de cifras cada vez que nos conectemos, vamos a usar algoritmos un poco menos seguros.

Como cualquier móvil hoy en día tiene una calculadora básica tenemos muchas operaciones para elegir. Por ejemplo podríamos elegir las cuatro últimas cifras del cuadrado del número. Si el ordenador nos pasa el número 3465 nosotros responderíamos con 6225. Hay infinitos algoritmos para elegir. Algunos os habréis dado cuenta de que precisamente este no es muy inteligente.

Con las modernas calculadoras para móviles no es difícil hacer cualquier operación, a mi me gusta mucho esta: http://midp-calc.sourceforge.net/Calc.html. Además como es programable sólo tengo que pasarle los números y me devuelve el resultado.

Conviene escoger funciones que no sean lineales, o por lo menos no lo parezcan. Por ejemplo el módulo (he dicho no lo parezcan) va oscilando entre 0 y un valor máximo, igual que el seno o el coseno. Esta propiedad es estupenda para despistar.

El programa

Veamos el programa que permite esto. Se trata de un pequeño script en Perl que se coloca en lugar de la shell de nuestro usuario. Así cuando alguien meta la contraseña correcta se le presenta el desafío.

Desde el momento que se presenta el desafío el que entra tiene dos opciones:
  • Responder correctamente, con lo que se le dejará entrar.
  • Cualquier otra acción, ya sea cortar la conexión o responder equivocadamente disparará la rutina de error.

#!/usr/bin/perl 
#===============================================================================
#
#         FILE:  escudo.pl
#
#        USAGE:  ./escudo.pl  
#
#  DESCRIPTION:  Se pone como shell de usuario para proporcionar un nivel de
#                seguridad extra. Puede ser una contraseña dinámica que cambie
#                cada vez a modo de token. O que cambie según día.
#
#                Cuando un usuario se logee con nuetra cuenta y no sepa qué algoritmo
#                hemos puesto en el escudo fallará. Y nos llegará un correo avisando.
#                De esa forma sabremos que nuestra contraseña ha sido comprometida.
#
#                Como acciones posteriores puede cambiar la contraseña y enviar la nueva
#                a un correo seguro. O bloquear la IP origen.
#
#        NOTES:  Hay que modificar las funciones &parametros y &calcular para crear
#                el algoritmo que nosotros diseñemos.
#
#       AUTHOR:  Reinoso Guzmán
#      VERSION:  1.0
#      CREATED:  13/11/10 12:48:18
#     REVISION:  ---
#===============================================================================

use strict;
use warnings;
use Sys::Syslog;
use Net::SMTP;

openlog('escudo', 'cons,pid', 'user');

$| = 1;
# Si el usuario hace cualquier otra cosa que no sea meter la clave correcta,
# damos la alarma.
$SIG{TERM} = \&tomar_medidas;
$SIG{HUP}  = \&tomar_medidas;
$SIG{INT}  = \&tomar_medidas;
$SIG{CHLD} = \&tomar_medidas;


# Variable global con el nombre del usuario
my $usuario = $ENV{LOGNAME} || $ENV{USER} || "Perfecto desconocido";
$usuario = ucfirst($usuario);

# Actuamos si es shell remota por SSH.
# Los comandos no interactivos fallarán, pero es de lo que se trata.
if (not exists $ENV{SSH_CLIENT}) {
 do_shell();
}


# Le hacemos una pregunta al usuario con los parámetros
my @params = parametros();
print "\nHola $usuario.\nSi yo te digo @params, ¿tú que me contestas?\n";

# Esperar respuesta
my $respuesta = <>;
chomp $respuesta;

# Comprobamos la respuesta
if (calcular(@params) eq $respuesta) {
 do_shell();
}


# Tomar medidas en caso de que algo no funcione bien.
# La shell se lanza con exec, que no retorna. 
# Luego si de cualquier forma llegamos a esta función (ya sea por un fallo
# en exec, o por algún truco del intruso, tomamos medidas):
tomar_medidas();
exit(1);


##############################################################################


# Proporciona un array con los parámetros que se le dan al usuario.
sub parametros {
 my $param1 = 13 + int (rand(10000));
 my $param2 = 13 + int (rand(100));
 #$param1 = 7521;
 #$param2 = 77;
 return($param1, $param2);
}


# Devuelve 0 si la respuesta coincide con el número que se esperaría.
# Se le pasan los parámetros de &parametros.
sub calcular {
 my ($a, $b) = @_;
 my $dia = (localtime(time))[3];
 my $respuesta;

 # Inventar aquí el algoritmo: respuesta = f(a, b, c, ...)
 # Otra opción sería usar tokens:
 # (números aparentemente aleatorios pero con una estructura interna desconocida
 # para el atacante. Calculados de manera automática y de un sólo uso.
 # --------------------------
 $respuesta = abs (int (log($a + $b * $dia) * 10000));
 # --------------------------

 return $respuesta;
}


# Hemos comprobado que el usuario es legítimo y ejecutamos la shell.
sub do_shell {
 syslog('notice', 'Respuesta correcta, entra %s.', $usuario);

 # Reemplazando SHELL y llamando a exec de esa manera es como si el escudo nunca
 # hubiera existido por medio.
 $ENV{SHELL} = '/bin/bash';
 exec {"/bin/bash"} "-bash";
}

sub interr {
 tomar_medidas();
}


# Esta función toma las medidas que se prevean. Generalmente enviar un correo
# o bloquear la IP atacante al cabo de algunos intentos.
sub tomar_medidas {
# print "Password comprometida. ¡Fuera!\n¡Avisaré a $usuario!\n";
 my $conn = $ENV{SSH_CLIENT} || "localhost";
 syslog('notice',
     'Respuesta incorrecta al desafio para %s: acceso denegado.',
     $usuario);
 
 my $smtp = Net::SMTP->new('localhost');
    $smtp->mail($usuario.'@localhost');
    $smtp->to($usuario.'@localhost');
    $smtp->data();
    $smtp->datasend("To: $usuario\n");
    $smtp->datasend("From: root\n");
    $smtp->datasend("Subject: Clave de $usuario comprometida.\n");
    $smtp->datasend("\n");
    $smtp->datasend("Alguien intento entrar desde $conn con la clave de $usuario.\n");
    $smtp->dataend();
    $smtp->quit;

 closelog();

 # La última medida que se toma, es por supuesto, terminar la shell para tirar la sesión.
 exit(1);
}


El funcionamiento es sencillo. Lo primero que hacemos es capturar todas la interrupciones que pueden ocurrir. Para conseguir que una vez llamado, el programa sea una trampa: o se contesta bien, o se tomarán medidas.

Por otro lado no nos interesa que el escudo moleste cuando iniciemos sesión nosotros localmente. Así que justo después hay una condición para que si la conexión no es desde un terminal remoto por SSH nos presente la shell sin mediar palabra.

Una primera rutina nos genera unos parámetros dentro del rango adecuado a nuestro algoritmo. Estos números son los que nos presentará el escudo cuando intentemos entrar. Una segunda rutina se encarga de generar la respuesta que corresponde a esos parámetros. Como la calculadora en el móvil debe seguir el mismo que hay programado en el escudo las respuestas deben ser idénticas.

Si la respuesta era la esperada por el escudo, llamamos a la shell. Fijaos la forma de llamarla con exec, y cómo se reemplaza la variable de entorno SHELL. Así es como si el escudo nunca hubiera estado en medio.

La rutina tomar_medidas actúa según hayamos programado.
  • Evidentemente sale del programa. Terminando la shell y desconectando al posible intruso.
  • Puede enviar un correo a alguna dirección que se ponga para que nos avise de que alguien ha conseguido entrar con nuestra contraseña.
  • Podría cambiar la contraseña a otra que tengamos programado. Para el caso de que comprometan la primera podamos seguir entrando con la de respaldo.
  • Podría bloquear la IP al cabo de unos cuantos intentos infructuosos. Aunque haría falta un poco de maña, porque el escudo corre con los privilegios del usuario.


Observaciones

Es de cajón, pero hay que advertir no teclear en la calculadora de Windows o en la de Google la operación. Porque si realmente hay instalado un programa para capturar claves también descubrirá las operaciones que hacemos.

Yo he optado por sustituir a la shell del usuario. Pero otra opción es sustituir al programa loginpara que actúe sobre todos los usuarios. Este paso es peliagudo, porque login hace algunas cosas más a parte de autenticar al usuario y lo que es peor: se ejecuta con privilegios de root. Así que cualquier fallo es peligroso.

Si quisiéramos usarlo para varios usuarios lo mejor sería un fichero de configuración en el home del usuario. Bien con el código del algoritmo para ejecutarlo con eval, o bien con algunos números que sirvan de parámetros y poder personalizar el algoritmo para cada usuario.

Huelga decir que el código que presento hay que modificarlo si lo quieres usar. No dejes el algoritmo que pongo de ejemplo.
Artículo completo >>

Mando tipo ultraligero para simulador de vuelo

Hoy os quiero enseñar un invento que hice hace unos años para jugar al FlightGear Flight Simulator. Es muy sencillo y una forma de aprovechar un viejo ratón de bola.

Se trata de que controlar el avión por teclado es muy insípido. Hacerlo con un ratón pues también. Y hacerlo con un joystick tamaño normal deja mucho que desear. Así que vamos a construir un pseudo-joystick de talla XXL que asemeja bastante a las palancas de los ultraligeros. Un ejemplo en esta imagen.

No hay mucho que contar y yo no tengo mucho tiempo para escribir. Así que os explico mientras veis las fotos. La pinta hay que admitir que no es nada profesional. Digamos que si funciona, es suficiente.


Como otros buenos inventos españoles, como el chupachups o la fregona, este consiste tomar un ratón y ponerle un palo. Únicamente hay que fijar el ratón a la base y utilizar unos hilos para que el movimiento de la palanca se transmita a los rodillos.

En la foto de abajo vemos cómo hemos fijado los hilos con argollas. Es un detalle importante para que el chisme sea desmontable.

Observad que las argollas de los laterales están más arriba que las del frente y atrás. Mientras más arriba está el hilo, más se nota cuando movemos la palanca. Lo que conseguimos así es que el alabeo (movimiento lateral) del avión sea más sensible que el cabeceo. O dicho en otras palabras, tenemos más precisión para girar el avión que para levantar o bajar el morro.


Esos hilos los pasamos por unos ejes para guiarlo, en nuestro caso hechos con clavos doblados.


Y después rodeamos los rodillos del ratón de forma que el movimiento del hilo se transmita a ellos. Es preciso que el hilo quede tenso, aunque no demasiado. Si fuera necesario podríamos también ponerle un pequeño muelle en algún punto intermedio del hilo para que no se afloje.


Luego hay que sacar los interruptores del ratón para llevarlos a un lugar más accesible, cerca de la mano, por si tenemos que pulsarlos. Utilizaremos una clavija para poder conectar y desconectar el cable cuando los desmontemos.



Para que el eje pivote libremente pero no se salga de su posición podemos utilizar la base de un remache como vemos aquí:


Y finalmente un buen toque sería que la longitud de los hilos sea tal que además de engancharlos a las argollas, sirva igualmente para dejarlos enganchados en los ejes que habíamos dicho antes. Y que cuando lo desmontemos no queden hilos por ahí colgando.


Si el montaje está suficientemente tenso el eje se mantiene vertical por el rozamiento y no hace falta sujetarlo.

Como algunos os habréis dado cuenta, si inclinamos la palanca demasiado los hilos se destensan y se pueden salir de su sitio. Pero esto lo hemos planteado para ser el mando de un avión, y no es probable que necesitemos tanto ángulo para manejarlo.
Artículo completo >>

Transmitir información usando el mando de un coche teledirigido

Hoy vamos a ver una introducción a la transmisión digital de señales. Veremos los conceptos básicos y haremos una pequeña práctica con los materiales que tenemos a mano. Caracterizaremos el sistema y programaremos un par de PICs para que hagan las funciones de transmisor y receptor.

Lo primero es un transmisor y un receptor. Lo tenemos, vamos a usar el transmisor y el receptor de un coche teledirigido. El circuito receptor no está pensado para esto y no trabaja demasiado bien, lo veremos. El consumo del transmisor ronda los 12mA, mientras que el del receptor es de 9.5mA. La velocidad que conseguiremos es de 14 baudios, poco más de un byte por segundo. Muy lenta para algunas cosas, pero por ejemplo se podría utilizar en un sensor inalámbrico de lluvia, de temperatura o una alarma.


Transmisión de la señal

Para hacer llegar información (datos) de un punto a otro necesitamos usar varios elementos. Si os acordáis del modelo OSI, vamos a trabajar en la primera capa, la más baja y cruel de todas: la capa física. Resumiendo, esta capa transmite información de un lado a otro, dándole igual los errores con que llegue. De eso ya se encargarán otras capas superiores.

Y dentro de esta, vamos a construir los cimientos desde el sótano describiendo nuestro medio, canal, modulación, codificación y alguna idea relativa al protocolo.

  • Medio: Es el medio que se usa para mover la información, puede ser por ejemplo un cable eléctrico o uno de fibra optica. En nuestro caso es transmisión inalámbrica, así que nuestro medio son las ondas radioeléctricas.

  • Portadora: Pero el espectro radioeléctrico es muy amplio. Y habrá que decir qué parte de él usamos. 27.145MHz es la frecuencia a la que trabajan la mayoría de los coches teledirigidos de gama baja. No siempre se indica pero por las características concretas de nuestro proyecto conviene.

  • Modulación: Los bits se transmiten haciendo variar algún parámetro de la portadora que el receptor pueda detectar. Podemos variar la amplitud (ASK) por ejemplo, o la frecuencia de la portadora (FSK). Nuestro transmisor funciona en modulación de amplitud, y tiene dos frecuencias distintas con las que modula la portadora en función del botón que pulsemos. Una es 250Hz y la otra 1kHz.

    Este tipo de modulación se llama AFSK. Significa que variamos la frecuencia de modulación, pero no la de la portadora en sí, que seguirá siendo 27.145MHz. No es lo mismo que FSK aunque muchas veces se habla de FSK por simplificar.

  • Codificación: Ya sabemos el parámetro que varía. Habrá que determinar cuando significa un bit 0 y qué forma toma un bit 1. Eso es la codificación. Hay varias formas de hacerlo. Ya vimos cómo funciona la codificación Manchester o la Codificación por Separación de Pulsos hablando de mandos a distancia RC5 y NEC respectivamente.
    No me voy a enrollar contando las bondades e inconvenientes de las codificaciones que se usan hoy en día. Aquí voy a usar una que se llama No Retorno a Cero porque es la más sencilla de implementar y de entender para la prueba de concepto.

  • Protocolo: Como nuestra transmisión es unidireccional de emisor a receptor no hay respuesta. Así que el protocolo que implementemos tiene que ser lo más robusto posible, porque el receptor no puede pedir que le vuelvan a enviar un paquete que ha llegado mal. Pero eso lo dejamos para más adelante. De momento no nos interesa. Si acaso podríamos incorporar un bit de paridad.

    Decidimos enviar los datos en paquetes de 8 bits (byte) más un bit de inicio, para probar. Si luego encontramos una aplicación concreta podemos dedicar 4 de esos 8 bits a una cosa y otros 4 a otra, o mandar paquetes de 32 en vez de 8 bits, lo que necesitemos.


Caracterizar el sistema

Bueno. Ya que tenemos un poco de idea de cómo transmitir información digital vamos a coger nuestros circuitos transmisor y receptor del coche teledirigido y obtendremos algunas características clave. Como por ejemplo el alcance o la demora entre que pulsamos un botón y en el transmisor y el receptor se entera. Este retardo es el que va a limitar la velocidad de transmisión, porque si cambiamos de tono para el 0 y para el 1 demasiado rápido el receptor no se va a enterar y no nos servirá de nada. Así que lo primero que tenemos que medir es cuánto tarda en recibir la señal.

Una característica típica de los detectores de tono (que es lo que lleva el integrado RX-3 para saber qué botón pulsamos), es que funcionan más deprisa cuanto más alta es la frecuencia que tienen que detectar. Digamos que el tiempo que tarda en identificar un tono es proporcional a su periodo. Como aquí los tonos que tenemos son de 250 y 1000Hz ya os digo yo que la velocidad de transmisión va a ser muy baja.

Como tenemos dos botones en el mando, tenemos tres estados:
  • A: Parado. Ningún botón activo.
  • B: Grave. El botón del tono grave está pulsado.
  • C: Agudo. Pulsamos el botón del tono agudo.

Habrá que medir cuánto tarda en pasar del estado A al B, del B al C y del C al A, y viceversa. Son pues, 6 transiciones que se pueden hacer en bucle:
Estad: A -> B -> C -> A -> C -> B -> A
Trans:   AB   BC   CA   AC   CB   BA

Lo hacemos con este montaje, en el que el receptor y el transmisor van conectados al mismo PIC.


Para abreviar no pondré los programas. Podéis encontrarlos si os interesa en el enlace que hay al final de la entrada, dentro del directorio transiciones. El algoritmo es muy sencillo y consiste en lo siguiente:
  1. Fijar el estado A (ningún botón pulsado).
  2. Esperar a que se desactive la señal en el receptor.
  3. Enviar por puerto serie el nuevo estado.
  4. Fijar el estado B (botón de tono grave).
  5. Esperar a que en el receptor se refleje el cambio.
  6. Enviar por puerto serie el nuevo estado.
  7. ...
  8. Repetir en el orden que habíamos dicho antes.

Si pusiéramos la oreja en la antena esto es lo que oiríamos:


Aquí vemos un sonograma de Baudline. El tiempo transcurre hacia abajo y las frecuencias aumentan hacia la derecha. Como es una onda cuadrada, las lineas verticales son los armónicos. En el estado A no hay ninguna señal. En el B las líneas están muy juntas porque se trata de los armónicos de una onda de 250Hz (250Hz, 500Hz, 750Hz, 1000Hz, 1250Hz, etc). En cambio la frecuencia moduladora para el estado C es de 1000Hz. Y los armónicos son múltiplos de 1000 (1000Hz, 2000Hz, 3000Hz, etc), por eso salen más separados.

Otra cosa interesante es que está más tiempo parado en el estado A que en los demás. Y que el estado C es el que menos dura. En efecto, el receptor se da cuenta muy rápidamente del tono de 1000Hz, para detectar el de 250Hz le cuesta un poco más. Pero una vez cesamos de enviar señal, la inercia que tiene es muy grande. Es normal, es un circuito pensado para un coche teledirigido, no para hacer de modem.

Luego en el puerto serie tenemos un recolector de información que es el que lleva el registro del tiempo entre un estado y otro. Similar al que habíamos usado en nuestro sensor LED.

Los tiempos mínimos y máximos que se obtienen son (en milisegundos):

AC:    8 - 17
BC: 17.5 - 17.7

AB: 64.4 - 65.6
CB: 65.5 - 67.5

CA: 146 - 149
BA: 175 - 194

Pensad que estos tiempos son los tiempos durante los que tenemos que mantener un mismo estado para que el receptor se entere. Luego para nuestra codificación está claro que nos tenemos que centrar en B y C que son los dos estados más "rápidos", porque en cuanto queramos hacer algo con el estado A tendríamos que esperar hasta 200ms. La velocidad va a caer en picado. Y dentro del juego B y C lo que nos dice esto es que tenemos que darle a cada tono unos 70ms de margen (el máximo entre los dos).

Lo ideal sería quedarnos sólo con el C, pero como para transmitir algo tenemos que tener dos estados por fuerza hay que elegir B y C.

En realidad no siempre tardan lo mismo. Va a haber un valor mínimo del tiempo de detección, por debajo del cual no se detecta nada. Pero habrá veces que tarde más o tarde menos dependiendo por ejemplo de la calidad de la recepción. Hay una distribución estadística, que se llama de Weibull que más o menos modela este comportamiento. No vamos a hacer el test porque creo que ya queda fuera del ámbito del artículo. Para las tres transiciones podemos dibujar tres histogramas que nos darán una idea de cual es el margen que tenemos que dejar si queremos que el receptor no se confunda. Esto es importante, porque la tasa de error va en funciona de este tiempo. Si dejamos un tiempo demasiado corto podremos transmitir más rápido pero a costa de tener más errores, habrá símbolos que se reciban bien (1 o 0) y otras veces se recibirán mal (un 0 por un 1 o al revés).



En la imagen superior vemos que la transición hacia A (apagado) desde cualquiera de los estados activos (lo que tarda el receptor en enterarse de que hemos soltado el botón) ronda los 182ms. Puede ocurrir que se detecta antes, por diversas causas, pero es raro que se detecte antes de 178ms. Y hay un tiempo mínimo donde la probabilidad de que se detecte es cero. Por ejemplo en t=0 no vamos a detectar nada, porque es evidente que el integrado necesita un tiempo para procesar la señal y conmutar.


La transición hacia C es mucho más rápida, de hecho en la imagen superior se notan los pasos discretos de tiempo. Lo mismo que la anterior, lo normal es que se detecte en 15ms. Puede darse en menos tiempo pero también puede tardar más.


Esta es la transición hacia B. El factor limitante de la velocidad. Al igual que las otras tiene un tiempo característico, que es de unos 64.6ms. Pero si ajustamos ahí nuestro retardo puede pasar que no se detecte algunas veces, porque como ya vemos en la distribución, hay muchas medidas que han ocurrido en tiempos superiores. Cuando diseñamos un sistema de transmisión tenemos que alcanzar un compromiso entre velocidad y tasa se fallo. En este caso con tomar 66ms parece que cubriríamos todo el margen. Pero eso no nos asegura una tasa de error cero. Porque siempre habrá ruido e interferencias.

Resumiendo, con 70ms de duración de cada simbolo tenemos una velocidad máxima de 14 baudios. Como decíamos al principio es poca cosa pero para según qué proyectos puede ser muy útil.


Enviar la información

Ya tenemos todo listo. Vamos a usar el método de No Retorno a Cero, con una velocidad de 14 baudios. Si transmitimos paquetes de 8 bits, y teniendo en cuenta los tiempos de parada (de 200ms) es algo más de 1 byte por segundo. Basta programar un poco para tener dos PICs, uno actuando sobre el transmisor con un texto preprogramado y otro con el receptor.


He usado un pequeño truco para sincronizar las dos partes. Porque al haber tanta inercia es fácil que el receptor se pierda. Consiste en que al inicio siempre transmito un 0. Como es el que más tarda, el retardo también sirve para el 1. Si no lo hiciera, el receptor perdería el hilo en los signos que empezaran por 1. Ese truco se llama Look At Me y se usa en casi todos los dispositivos remotos. Tened en cuenta que aunque yo conmute siempre a un tiempo fijo en el transmisor, al receptor le llega la señal cada vez con un retardo distinto. Por eso tengo que ver qué estado hay al final de los 70ms. Si lo viera a la mitad unas veces se recibiría bien (para los 1, que tardan menos por ser el tono más agudo) y otras mal.

Este es el programa transmisor:

void one (void) {
 output_high(PIN_LED);
 input(PIN_TXGrave);
 output_low(PIN_TXAgudo);
 delay_ms(symbol_time);
}

void zero (void) {
 input(PIN_TXAgudo);
 output_low(PIN_TXGrave);
 delay_ms(symbol_time);
}

void off (void) {
 input(PIN_LED);
 input(PIN_TXAgudo);
 input(PIN_TXGrave);
} 

/* Transmitiremos primero el bít más significativo  */
/* Se podría hacer al revés también, va al gusto.   */
/* Antes de empezar la transmision mandamos un bit  */
/* de start.                                        */
void transmit_byte (char value) {
 #bit msb = value.7
 char i;
 
 // Bit de start a modo de LAM (Look At Me)
 // Uso el cero porque tarda más en recibirse que el uno
 // entonces el retardo es válido para los dos.
 zero();

 // Transmisión del bit
 for (i=0; i <= 7; i++) {
  if (msb) {
   one(); 
  }
  else {
   zero();
  }
  value <<= 1;
 }
 off();
}

void main()
{
 unsigned char i;
 unsigned char test_seq[] = {
                             0b00000000, // 0x00
                             0b00010001, // 0x11
                             0b01010101, // 0x55
                             0b10101010, // 0xAA
                             0b11110000, // 0xF0
                             0b00001111, // 0x0F
                             0b11111111, // 0xFF -> FIN
                               };

 setup_adc(ADC_OFF);
 setup_timer_0(RTCC_INTERNAL|RTCC_DIV_1);
 setup_oscillator(OSC_4MHZ);
 port_a_pullups(FALSE);

 off();

 i = 0;
 for(;;) {
  char value;
//  value = read_eeprom (i);
  value = test_seq[i];
  if (value == 0xFF) {
   i = 0;
   delay_ms(repetition_time);
  }
  else {
   output_high(PIN_LED);
   transmit_byte(value);
   output_low(PIN_LED);
   delay_ms(off_time);
   i++;
  }
 }

}

El algoritmo es muy simple. Descomponer el símbolo en sus bits y transmitirlos en el orden que queramos (ya sea primero el más significativo o el menos). También me gustaría que os fijarais en los códigos de prueba que se transmiten.

A la hora de recibir es cuando nos servimos del truco que os había dicho antes. Recibimos el primer bit y sabemos que va a ser un cero. Así que una vez lo recibamos vamos muestreando los estados a intervalos regulares. Cuando juntamos 8 bits transmitimos el byte correspondiente por el puerto serie hasta el PC.

void main()
{
 setup_adc(ADC_OFF);
 setup_timer_0(RTCC_INTERNAL|RTCC_DIV_1);
 setup_oscillator(OSC_4MHZ);
 port_a_pullups(FALSE);

 
 for (;;) {
  char value;
  char num;
  
  // Esperamos el estado A (a que todo se apague)
  while(input(PIN_RXAgudo) || input(PIN_RXGrave));
  printf("A");
  // Esperamos un B que significa el comienzo
  while(!input(PIN_RXGrave));
  printf("C ");

  // Esperamos un pequeño lapso porque la transición 
  // A-B tiene un error de más-menos unos pocos ms.
  delay_ms(5);
  
  // Y muestreamos cada symbol_time ms
  // Terminamos si se apagan los dos (se vuelve a A)
  // o si se llega a los 8 bits
  value = 0;
  for (num=0; num<=7; num++) {
   #bit lsb = value.0
   short grave, agudo;
   
   delay_ms(symbol_time);
   
   grave = input(PIN_RXGrave);
   agudo = input(PIN_RXAgudo);
   
   if (grave) {
    // Es un 0, rotamos y añadimos el valor
    value <<= 1;
    lsb = 0;
    printf("0");  
   }
   else if (agudo) {
    // Es un 1, rotamos y añadimos el valor
    value <<= 1;
    lsb = 1;
    printf("1");
   }
   else {
    // No hay más datos
    printf("A");
    break;
   }
  }
  
  output_high(PIN_LED);
  printf(" %X\r\n", value);
  delay_ms(100);
  output_low(PIN_LED);
 
 }

}

Lo mismo que antes, cuando ponemos la oreja en la antena vemos algo como esto. La captura es de una prueba anterior, en la que habíamos usado un retardo de 68ms y la señal de Look At Me era un 1 seguido de un 0. Así es como se vería la transmisión de 3 bytes: 0x0F, 0x55 y 0x55:




 Después de hacer todas las pruebas tenemos un sistema de transmisión inalámbrico, lento, muy lento pero muy fácil de hacer. Y hasta aquí podemos llegar con estos circuitos tal como vienen de fábrica sin modificaciones importantes.



Evolución del sistema

¿Qué modificaciones podríamos hacer en el sistema para convertirlo en algo más útil? Pues algunas, por ejemplo:
  • Más velocidad. Con el RX-3 que viene de fábrica es imposible. Así que tal vez podríamos capturar la señal cuando sale del demodulador (ver este esquema) y procesarla para detectar nosotros el tono.
  • También podemos obviar la parte del oscilador en el transmisor (ver esquema del transmisor) y utilizar otro sistema para modular la portadora. Así podríamos usar frecuencias más altas que sean más rápidas de detectar.
  • Más potencia. Incorporando un transistor para amplificar la potencia al final y una buena antena tendríamos un poco más de alcance.
En definitiva, aprovechar los osciladores de RF y currarnos el resto, que cada uno juzgue si le parece práctico o no. Encontraréis los archivos usados en las pruebas aqui. Están los logs de las transiciones, los programas y los hex de los PICs.
Artículo completo >>