h1

Generando una señal de vídeo compuesto con el RX62N

8 \08+00:00 septiembre \08+00:00 2013

ImageMadre mía, cuanto tiempo hace que no actualizo. Pero la ocasión bien merece la pena.

Estos últimos años he profundizado en algunos de mis hobbies. Las matemáticas, la física, la dichosa radio, que mis colegas tienen que estar hasta las narices de verme hacer el loco en el monte con ella y sobre todo la electrónica. De eso va la entrada de hoy, de quizá el primer invento puramente electrónico que he hecho funcionar exactamente como esperaba. Si todo va bien… quién sabe, quizá este sea el comienzo de una larga saga de artículos sobre lo que voy aprendiendo sobre el tema (probablemente no). Pero bueno, que no se diga que al menos no lo he intentado.

El nacimiento de la ocurrencia: una Play Station que no funcionaba
Hace unas semanas tuvo lugar en el pueblo costero de San Ciprián cierta fiesta conocida como La Maruxaina que básicamente consiste en beber queimada. Y una amiga nos cedió su casita para dormir el fin de semana.

La cosa es que la pleisteishon no les funcionaba, se conectaba mediante un cable RCA de vídeo compuesto y dos de audio directamente a la tele, pero no se veía nada. Dudábamos del cable, de la tele, o de la propia play.

La cosa es que se me ocurrió hacer el siguiente experimento: en mi mochila suelo llevar siempre cables y adaptadores si me hiciesen falta para conectar mi portátil a una minicadena y poner música (por ejemplo). Entre esos adaptadores, había un adaptador de minijack a RCA el cual se me ocurrió conectar a la tele de la señorita. Si veía rayas, es que el chisme funciona.

¿Puedo hacer esto, preguntaréis? Pues sí por las siguientes razones: la primera, es que la impedancia de entrada del conector de vídeo compuesto es de 75 Ω, mientras que la impedancia de entrada de mis cascos es de 8 Ω o menos. Por otro lado, la tensión de entrada debe ser del orden de 1 voltio de pico a pico, mientras que la que da mi salida de audio es (como mucho) del orden de 3V de pico a pico (esto medido con mi polímetro). Que será un poco menos al enchufarlo a la entrada de audio debido a la propia impedancia de salida del ordenador. Total: que siempre y cuando empiece con el volumen bajito y subiéndolo poco a poco, aseguro que no me cargaré la tele. Todo correcto, todo en orden.

El programa perfecto para probar esto es siggen. Siggen me permite generar distintos tipos de ondas (sinusoidal, cuadrada, triangular, diente de sierra) a frecuencias de como mucho la mitad de la de muestreo (normal). La propia salida de audio me limita a 20 KHz, pero el sacar un conjunto de líneas horizontales en la tele a 15 KHz más o menos me ha servido para saber que el problema estaba en otro lado.

Fue en ese momento cuando dije… hey, ¿y si usase alguno de mis cacharros para sacar imágenes por la tele?

La señal de vídeo compuesto y el sincronismo horizontal
Antes de profundizar en el asunto, debemos saber qué clase de entrada acepta nuestro receptor de televisión. La mayor parte de televisores modernos disponen de un conjunto de tres entradas RCA hembra de colores rojo, blanco y amarillo. Las dos primeras están destinadas al audio, mientras que la última es lo que se conoce como entrada de vídeo compuesto y es la que utilizamos para jugar a la consola en el canal de AV.

Esta señal es básicamente una señal de televisión en banda base y depende de la norma de televisión de cada país. En este lado del mundo funcionamos con el sistema PAL, que es una pequeña variación del NTSC americano que, a pesar de ser incompatible con este, su filosofía es básicamente la misma.

El sistema PAL funciona muestreando la imagen en 625 líneas horizontales (de las cuales sólo se usan 576 ya que en las otras se envía información de teletexto y demás), enviando 25 imágenes por segundo, cada una de las cuales dividida en dos «campos», uno conteniendo las líneas pares y otro las impares. Es decir, primero enviamos la mitad de la imagen (tomando las líneas impares) y luego se envían las pares entrelazándose con la anterior, obteniendo la imagen completa. Esto lleva a que enviamos 50 campos por segundo. Compárese esto con la frecuencia de la red eléctrica en Europa (de hecho, esto es intencional, nos permite aprovechar la propia frecuencia de la red y ahorrarnos un oscilador). En EEUU el sistema NTSC funciona con una frecuencia de campo de 60Hz, que precisamente coincide con la frecuencia de red.

Interlacing

Entrelazamiento: primero se envían las líneas impares (campo impar) y luego las pares (campo par). La imagen original se obtiene entrelazando ambos campos para obtener la imagen completa. Hablamos entonces de 576i (576 lines – interlaced)

En cada línea se codifican tres tipos de información, de ahí el nombre de vídeo compuesto: la luminancia (la información de brillo, básicamente), la crominancia (la de color) y los sincronismos (que indican dónde empieza y acaba cada línea). Dejaremos de momento de lado la información de color (simplemente porque los componentes de los que disponemos no son capaces de generarla debido a que son demasiado lentos, que si no le dedicaba también su párrafo).

La información de luminancia se codifica mediante niveles de voltaje: un voltaje bajo implica tono oscuro y un voltaje alto un tono claro. Esta información debe introducirse en los 52 μs que dura la información de luminancia de los 64 μs que dura una línea PAL. Esto se llama «parte activa de la línea». En esos 52 μs se recorre una línea de la imagen de izquierda a derecha. Esto quiere decir que los detalles más pequeños durarán «menos» que los grandes. Para que nos hagamos una idea, una imagen que divida horizontalmente la pantalla en tres rectángulos «negro, blanco y negro» implicaría que cada línea tendría un período de 17.33  μs a voltaje bajo, después subiría y se mantendría en nivel de voltaje alto otros 17.33 μs y volvería a bajar para mantenerse en voltaje bajo durante otros 17.33 μs. Esto parece de perogrullo, pero tiene profundas implicaciones en en las características de los componentes necesarios para generarla: a detalles más finos necesitaremos circuitos capaces de conmutar distintos valores de voltaje muy rápidamente, y por tanto trabajar con frecuencias más altas.

Pero dejemos esto un momento de lado. Cuando encendemos la tele puede suceder (y de hecho, sucede) que la señal de vídeo se coja «a medias», es decir, que la tele empiece a leer una línea por la mitad. Tiene que haber una forma de determinar dónde empieza y acaba una línea para centrarla correctamente en la pantalla. De esto se encarga la parte inactiva de la línea: después de cada línea y antes de la siguiente se introduce una señal de 12 μs que reconoce el televisor para este fin.

En estos 12 μs se suceden una serie de impulsos de diversa duración. En esos impulsos que atañen sólo a la luminancia encontramos, por orden de aparición:

Image

Señal de una línea típica monocroma de PAL. Nótense los niveles de blanco, negro y sincronismo. Podemos ver que a ambos lados de los dos sincronismos (los pulsos hacia abajo) tenemos un breve pórtico anterior (un nivel constante de 1.5 microsegundos de duración) y un largo pórtico posterior (un nivel constante de 5.8 microsegundos). Imagen obtenida de la página de Martin Hinner.

Pórtico anterior (front porch): Es una bajada al nivel de negro de 1.5 μs. En las teles de tubo provocaba el apagado del haz de electrones y el retorno del mismo a la izquierda de la pantalla. Si esto no existiese, la imagen se «doblaría» superponiéndose en el margen derecho.

Sincronismo horizontal (sync pulse): es una bajada del 30% del rango dinámico del voltaje de la señal por debajo del nivel de negro durante un período de 4.7 μs. Este pulso negativo permite sincronizar el barrido del televisor con el de la señal y así comenzar la imagen en el lugar correcto.

Pórtico posterior (back porch): es una subida al nivel de negro durate 5.8 μs que en las teles de tubo permitía apagar el haz durante el tiempo que dura el ajuste de las bobinas deflectoras para su retorno al margen izquierdo.

Con estos tres elementos estamos podemos empezar a encajar la imagen en los márgenes laterales de la pantalla. El problema de cómo encaja la imagen en los márgenes superior e inferior todavía no está solucionado, pero con todo esto podemos empezar a experimentar con nuestra televisión.

Los juguetes
Para comenzar a generar esta señal he utilizado la placa del Renesas Demonstration Kit (RDK) RX62N, también conocida como YRDKRX62N. Esta placa está muy bien por la inmensa cantidad de periféricos que tiene (un acelerómetro, un sensor de temperatura, micrófono, salida de jack, serie, LCD, etc, etc), por haber un conjunto de herramientas bestial (bajo GNU/Linux) para jugar con ella y sobre todo porque hay muestras gratuitas. O casi (a mí me calcaron 26€ en gastos de envío). Bueno, al menos las había. En cualquier caso, estoy contentísimo con ella, oiga.

Esta placa dispone de un DAC escondido en la patilla 23 del socket J8. El fallo es que este DAC está unido a una resistencia pull-up (R55) que reduce bastante el rango dinámico de la señal. He necesitado DESTRUÍRLA para aumentar el voltaje de la salida pico a pico máximo medio voltio, porque sino nunca alcanzará el cero. Me costó mucho tomar esta decisión y no es realmente necesario si dispones de un buen amplificador. Sin embargo yo lo advierto: hacer esto puede limitar muchísimo las potenciales aplicaciones de tu placa. Hazlo bajo tu propia responsabilidad.

También me he valido de un osciloscopio ATTEN 1102CAL (una ganga) para ajustar correctamente los tiempos de la señal que generaré.

También me hice con una protoboard y algunos componentes que mencionaré más adelante. Pero para los primeros experimentos no necesitaremos más.

Preparando nuestro entorno de desarrollo
Antes de empezar a programar sobre nuestra placa RX62N necesitaremos un cable USB que la conecte con nuestro PC, las herramientas para compilar y las herramientas para enviar nuestro programa a la placa.

El cable USB debería estar incluido con el propio kit. Sobre las herramientas para compilar, yo me he decantado por las KPIT GNU Tools. El problema es que te piden que te registres. No es un gran problema, es gratuito. El fallo es que pueden pasar días antes de que te respondan con el usuario y contraseña.

Una vez descargado e instalado el compilador, toca bajarse los programas para flashear la placa. Tenemos que bajarnos el JLink commander y el GDB server desde aquí. Esa página nos pide el número de serie del depurador USB, el cual debería estar incluido con la documentación de la placa. Yo he descubierto que para enterarme del número de serie tengo que ejecutar el propio JLink commander, lo cual es un poco absurdo. Así me enteré que el mío era 270100723 (lo dejo aquí por si a alguien le vale). La versión del JLink commander que me bajé es la 4.74. Esa sé que al menos funciona.

Y por último, como biblioteca de apoyo para comenzar a programar con esta placa, echaremos mano de la última versión de projectman (la cual se puede descargar haciendo clic en «snapshot»). Esta herramienta, además de darnos todo el soporte base en C, nos generará los Makefiles y scripts necesarios para simplificar la tarea de flashear nuestro programa. Con esto nos ahorramos toda la parte sucia de programar el arranque en ensamblador y pasamos directamente a escribir cosas en C.

Poniendo nuestro entorno a punto
Vamos a comenzar generando nuestro proyecto. Una vez compilado e instalado projectman, creamos un proyecto llamado videogen con el siguiente comando:

% projectman create videogen renesas

Después de generar el proyecto se abrirá nuestro editor favorito en el punto de entrada (main) del programa en el fichero videogen/src/main.c

Para probar que todo funciona, escribiremos un pequeño programa de prueba que encenderá cuatro LEDs en la placa. Reescribimos el main como sigue:

int
main (int argc, char *argv[], char *envp[])
{
  led_on (4);
  led_on (7);
  led_on (10);
  led_on (13);

  for (;;);
}

Nos dirigimos a la raíz del proyecto (la carpeta videogen) y ejecutamos:

% make

No debería, pero aquí pueden fallar varias cosas. La más típica es que no se encuentre rx-elf-gcc. Eso se debe a que no hemos instalado correctamente las KPIT GNU Tools. En caso de un error de compilación en el propio código, no dudes en contactar conmigo. El código de projectman está todavía muy verde y no he tenido oportunidad de probarlo en muchos sistemas. Cualquier correo sobre bug encontrado es bienvenido.

Si todo ha ido bien, nos aseguramos de haber conectado la placa a nuestro PC por el puerto USB de la parte superior izquierda, pegado a un recuadro blanco impreso en la placa que dice «SEGGER». El LED llamado «DEBUGGER USB CONN» debe estar encendido en verde. También debemos asegurarnos de que el jumper llamado «J-LINK DISABLE» está abierto, y que el conjunto de interruptores SW5 está en modo DEBUG (OFF, OFF, ON, OFF).

Y por último, queda la parte más crítica. Ejecutamos el siguiente comando como root:

# cd src; rxflash.sh videogen

O bien como sudo:

% cd src; sudo rxflash.sh videogen

Es importante ejecutar el comando rxflash.sh directamente desde src, sino fallará. ¿Por qué? Pues porque lo programé rápido y mal. Es un hack al fin y al cabo, quizá lo mejor en una versión futura, todo se andará.

Si TODO ha ido bien, ¡enhorabuena! has superado la parte más crítica de todo el proceso. Ahora mismo tu placa debería tener cuatro LEDs encendidos, dos verdes y dos rojos. Y por fin podremos pasarnos a cosas más complicadas.

Generando una onda cuadrada
El primer paso es configurar nuestra placa para que sea capaz de ejecutar un pequeño pedacito de código un número concreto de veces por segundo. Esto nos permitirá ajustar fácilmente los tiempos en los que debe ser sacada la señal por el DAC. Esto es muy importante para que la televisión pueda sincronizarse correctamente a nuestro circuito.

Comenzaremos configurando el temporizador cero (TMR0) . Para esto escribiremos una pequeña rutina llamada timer_init donde encapsularemos todo el código relativo al arranque del temporizador:

void
timer_init (void)
{
/* Ponemos el divisor de frecuencia de los relojes del sistema a 1, así alcanzaremos la máxima frecuencia disponible en la placa */
  SYSTEM.SCKCR.LONG = 0x00;
  MSTP (TMR0) = 0; /* Activamos el temporizador 0 */
  TMR0.TCORA = 1;  /* Registro del comparador a 1 */
  TMR0.TCR.BIT.CCLR  = 1; /* Usar registro comparador */
  TMR0.TCR.BIT.CMIEA = 1; /* Generar interrupciones */
  TMR0.TCCR.BIT.CSS  = 1; /* Fuente de reloj: PCLK */
  TMR0.TCCR.BIT.CKS  = 3; /* Divisor de frecuencia: PCLK / 64 = 96 MHz / 64 = 1.5 MHz */

  IEN(TMR0, CMIA0) = 1; /* Activar interrupción */
  IPR(TMR0, CMIA0) = 2; /* Prioridad */
  IR(TMR0, CMIA0) = 0;  /* Limpiar flag de interrupción */
}

Yo aconsejo añadir esta función a un fichero llamado timer.c que incluya el fichero videogen.h e incluirlo en el Makefile.am del directorio src para poder estructurar mejor nuestro código. También es buena idea colocar un prototipo de esta función en el propio videogen.h.

Ahora podemos llamar a esta función desde main directamente:

int
main (int argc, char *argv[], char *envp[])
{
  timer_init ();

  for (;;);
}

Sin embargo, este código no hará nada ya que todavía no hemos escrito la rutina a ejecutarse por cada interrupción del temporizador.

Afortunadamente, esta rutina se haya escrita en el archivo renesas/inthandler.c. Sin embargo, por claridad y para tener su código en el mismo directorio, vamos a buscar la línea que dice:

void INT_Excep_TMR0_CMI0A(void){ }

Y la comentamos. Posteriormente, en el archivo timer.c que habíamos creado dentro de src y la añadimos como sigue:

void INT_Excep_TMR0_CMI0A (void) __attribute__ ((interrupt));

void
INT_Excep_TMR0_CMI0A (void)
{
}

Nótese el prototipo con el atributo interrupt. Esto es muy importante, ya que le dice al compilador que esa función es llamada como una rutina de servicio de interrupción (ISR), la cual tiene un prólogo y epílogo especial en ensamblador. Si no la declaramos con este prototipo y se trata como una función normal, a la hora de devolver el control nuestro código se colgará.

Ahora vamos a hacer algo útil con esa función: en el main antes de llamar a timer_init hacemos una llamada a dac_init (sin argumentos) para inicializar la salida del DAC.

Ahora, fuera de la función INT_Excep_TMR0_CMI0A definimos una variable llamada volatile int value (nótese el atributo volatile, esto impedirá que el compilador la optimice y la trate siempre como una variable global normal). Dentro del manejador INT_Excep_TMR0_CMI0A escribimos el siguiente código:

void
INT_Excep_TMR0_CMI0A (void)
{
  value = !value;

  DAC_SET (1023 * value);
}

La macro DAC_SET está definida dentro de la dependencia renesas de projectman y abstrae el acceso al registro del DAC. Esta macro debe ser usada sólo si hemos llamado previamente dac_init(). En esta rutina provocará la variación de la salida del DAC de 0 (voltaje mínimo respecto a masa) a 1023 (voltaje máximo respecto a masa). El DAC proporcionado por la placa es de 10 bits, de ahí estos límites. La variación de la tensión a la salida es (teóricamente) lineal respecto al registro del propio DAC. Asumiremos esto como cierto.

Compilamos, comprobamos que todo va bien y flasheamos con rxflash.sh. Si conectamos ahora los pins 2 (masa) y 23 (señal) del socket J8 a un osciloscopio podremos ver algo parecido a esto:

ADS00001

La señal no es totalmente cuadrada (de hecho, no lo es en absoluto) debido a que las sondas de mi osciloscopio APESTAN (comprobado) y hay alguna reactancia que me fulmina la señal. En cualquier caso, esta reactancia desaparecerá en el televisor.

Lo importante en este oscilograma es la frecuencia de la señal: casi 750 KHz (la mitad de 1.5 MHz). La función se ejecuta aproximadamente cada 666 nanosegundos. Debido a que cada ciclo de CPU nos dura cerca de 10 ns, debemos optimizar muchísimo el contenido de esa función porque de lo contrario sencillamente no nos dará tiempo a generar la señal a tiempo.

El código de esta primera iteración se puede descargar directamente de http://actinid.org/videogen-iter-0.tar.gz Ahora intentaremos sincronizar esta salida y mostrar estos pulsos en la pantalla.

Escribiendo los pórticos y el sincronismo horizontal
El problema, llegados a este punto, es optimizar al máximo el contenido de la función INT_Excep_TMR0_CMI0A de forma que se ejecute en el menor tiempo posible. Mi solución ha sido construirla como una máquina de estados: tenemos una serie de estados (línea activa, sincronismo horizontal, vertical, etc) cada uno de los cuales con un número de ciclos de duración.

Sabiendo que la rutina de la interrupción se ejecuta cada 666 nanosegundos, podemos hacer un poco de matemáticas:

Cada línea (de sincronismo a sincronismo) debe durar 64 μs: 64 μs / 0.666 μs = 96.096 interrupciones por línea (redondeamos a 96). 

Cada sincronismo dura 4.7 μs: 7.057 interrupciones por sincronismo (lo redondeamos a siete).

El pórtico anterior dura 1.5 μs: 2.25 interrupciones por pórtico anterior (lo redondeamos por arriba a 3 y nos queda una fracción del margen del derecho de la pantalla en negro: no es tan grave)

El pórtico posterior dura 5.8 μs: 8.7087 interrupciones por pórtico posterior (lo redondeamos a 9  y nos queda una finísima fracción del margen izquierdo de la pantalla en negro: tampoco es tan grave)

Duración total (en interrupciones) de la parte inactiva de la línea: 3 + 7 + 9 = 19 interrupciones por parte inactiva.

Duración total (en interrupciones) de la parte activa de la línea: 96 – 19 = 77 interrupciones por parte activa.

Estos datos nos adelantan un error del orden de 64 nanosegundos respecto a la duración real de la línea. Esto es si tomamos realmente 0.666 como duración de cada interrupción. En la teoría el error debería ser cero, ya que este 0.666 (dos tercios) es periódico y 96 x 2 / 3 = 64 microsegundos exactos. Además, de la duración de la parte activa hemos deducido 77 píxeles de ancho. Una resolución un poco grosera, pero suficiente para poner unas letras.

Para enviar la información por el DAC cuanto antes lo mejor es tenerla precalculada en un array global.

Antes de escribir todo esto, es buena idea definir una serie de macros en videogen.h que codifiquen algunas constantes útiles. Podemos empezar por definir los niveles de la señal:

#define SYNC_LEVEL 0 /* El sincronismo es el mínimo voltaje posible */
#define BLANK_LEVEL 307 /* El nivel de negro de los pórticos es está 30% del rango dinámico de la señal por encima del sincronismo */
#define BLACK_LEVEL BLANK_LEVEL /* Lo que nosotros llamamos "negro" es el mismo "negro" que el nivel de los pórticos */
#define WHITE_LEVEL 1023 /* El blanco es el mayor valor de voltaje que el DAC es capaz de sacar */

Y añadir también el número de interrupciones (ticks) que dura cada parte de la señal:

#define LINE_TICKS 77 /* Duración de la parte activa de la línea */
#define HSYNC_TICKS 19 /* Duración de la parte inactiva de la línea */

Con todo esto, vamos a poder generar nuestra primera señal y enchufársela directamente a la tele. Crearemos un nuevo archivo llamado pal.c en el directorio src (no nos olvidemos de añadirlo al Makefile.am) donde moveremos la rutina de servicio del temporizador INT_Excep_TMR0_CMI0A con su prototipo y definiremos el array en espacio global:

uint16_t hsync[HSYNC_TICKS] =
{
  /* Pórtico anterior */
  BLANK_LEVEL, BLANK_LEVEL, BLANK_LEVEL,

  /* Sincronismo */
  SYNC_LEVEL, SYNC_LEVEL, SYNC_LEVEL,
  SYNC_LEVEL, SYNC_LEVEL, SYNC_LEVEL,
  SYNC_LEVEL,

  /* Pórtico posterior */
  BLANK_LEVEL, BLANK_LEVEL, BLANK_LEVEL,
  BLANK_LEVEL, BLANK_LEVEL, BLANK_LEVEL,
  BLANK_LEVEL, BLANK_LEVEL, BLANK_LEVEL
};

Definimos del mismo modo dos variables que codificarán el estado de nuestra máquina de estados. Inicialmente sólo conmutará entre parte activa (píxeles por pantalla) y parte inactiva (sincronismos, pórticos, etc):

int state = STATE_LINE; /* Estado incial: línea */
int ticks = 0; /* Cuenta de ticks dentro de cada estado */
int line = 0; /* Cuenta de número de líneas enviadas */

La variable ticks contará el número de veces que ha sido llamada la rutina de servicio de interrupción del temporizador dentro de cada estado. Una vez superado el número de «ticks» por estado, se reinicia y se conmuta al siguiente estado:

Además, por cada conmutación al estado STATE_HSYNC incrementamos el número de líneas en line.

Para producir una señal un poco interesante, intentaremos generar un patrón en damero en la pantalla de la tele. La mejor forma de hacer esto es computando la operación XOR del bit 2 del número de línea con el bit cero del número de columna (LSB primero). Para que los píxeles sean más o menos cuadrados, meteremos 8 líneas horizontales por píxel, lo que nos lleva a 4 líneas horizontales por campo. La expresión que determina entonces si enviamos un punto blanco o negro es:

((line & 4) == 0) ^ (ticks & 1)

Por tanto, el código de la rutina de servicio del temporizador queda ahora como:

void
INT_Excep_TMR0_CMI0A (void)
{
  uint16_t value;

  switch (state)
  {
    case STATE_LINE:
      value = (((lines & 4) == 0) ^ (ticks & 1)) ? WHITE_LEVEL : BLACK_LEVEL;

      /* Determinamos si conmutamos el estado */
      if (++ticks >= LINE_TICKS)
      {
        ++lines;
        ticks = 0;
        state = STATE_HSYNC;
      }

      break;

    case STATE_HSYNC:
      value = hsync[ticks];

      if (++ticks >= HSYNC_TICKS)
      {
        ticks = 0;
        state = STATE_LINE;
      }

      break;
  }

  DAC_SET (value);
}

El código hasta ahora se puede descargar aquí. Compilamos y flasheamos. Si todo ha ido bien, la salida en el osciloscopio debe ser algo muy similar a esto:

ADS00003

Podemos ver que el cursor de tiempo (las líneas verticales) marca exactamente 64 microsegundos. Y aquí cabe destacar algunos aspectos interesantes sobre el sistema PAL: la señal se muestrea horizontalmente en intervalos de 0.666 microsegundos, y con el patrón en ajedrez estamos subiendo y bajando el valor de la señal a cada tick de temporizador. Esto va a generar un pico en la frecuencia de los 750 KHz correspondientes a la velocidad a la que enviamos píxeles a la pantalla (con esta imagen que hemos generado hemos alcanzado la máxima frecuencia horizontal posible):

ADS00004

Además hay otra cosa importante. Vemos que además del pico principal en los 750 KHz hay multitud de picos más pequeñitos. Estos picos son consecuencia del barrido, de los sincronismos horizontales que enviamos al final de cada línea. Al enviar un pulso negativo cada 64 microsegundos, se produce una respuesta en frecuencia también con forma de pulsos, equiespaciados esta vez a 1/64 microsegundos = 15625 KHz que se corresponde, precisamente, con la cantidad de líneas por segundo que se envían a la pantalla:

ADS00005

Este tipo de respuesta en frecuencia es muy típica y nos puede ayudar a identificar una señal de televisión analógica tan sólo echando un vistazo al espectro de la misma.

Para poder ver esto en la tele, debemos conectar el pin 2 de J8 en la toma a masa del cable RCA de vídeo (el cilindro de metal exterior) y el pin 23 al polo central. El resultado debe ser parecido a esto:

OLYMPUS DIGITAL CAMERA

Aquí vemos dos cosas. La primera es que el patrón de tablero de ajedrez no se queda fijo en la pantalla. Si bien está encajado en los bordes laterales de la pantalla, la imagen se desplaza verticalmente (de ahí que se vea borroso, en el mundo real eso se está moviendo muy rápido). Esto se debe al problema del sincronismo vertical, que trataremos más adelante. Pero además tenemos otro problema: a pesar de estar poniendo el máximo voltaje admitido por el DAC (codificado con el valor 1023 en su registro), ¡la imagen se ve anormalmente oscura!

El problema es básicamente el siguiente: la salida del DAC debería poder variar entre 0 y 3.3V. Sin embargo, esta salida está conectada a una resistencia pull-up de 5.1 kΩ, y además la salida del DAC tiene una impedancia interna de 3.6 kΩ. Esto es básicmente un divisor de tensión que reducirá la tensión de salida considerablemente. Pero aún destruyendo la resistencia pull-up nos encontramos con un escenario bastante miserable: una impedancia de salida muy grande (3.6 kΩ) contra una impedancia de entrada en la televisión muy pequeña (75 Ω). Estamos hablando de un divisor de tensión en el cual sólo el 2% de la tensión total cae dentro de la televisión. Para que nos hagamos una idea, con el DAC a 1023 dentro de la televisión sólo caen 67 mV.

Esto es una configuración verdaderamente lamentable. Sin embargo, mi televisión se las apaña para al menos darme un gris. No está tan mal, y esto es algo que podemos arreglar.

Amplificando un poco la señal
Esto más que un problema de amplificación es un problema de impedancias. Lo cierto es que los resultados distaban mucho de ser aceptables, así que rebuscando en mi caja de cacharros inútiles descubrí una bolsita con componentes de un montaje que nunca realicé. Entre ellos:

  • Un amplificador operacional 741
  • Una resistencia de 2.2 kΩ
  • Una resistencia de 3.9 kΩ
ua741

El 741, un clásico de los operacionales de baja frecuencia.

El amplificador operacional es un poco patatero. Dista de tener un ancho de banda grande (1 MHz de ancho de banda máximo, frente a los 15 MHz de otros más sofisticados como el LM318 y similares). Además, la ganancia del amplificador cae con la frecuencia, pasando de 105 a prácticamente la unidad cuando nos acercamos al MHz. Nuestra señal se acerca mucho al MHz, por lo que tenemos que contar con distorsiones en los bordes laterales de los píxeles.

Lo ideal aquí sería utilizar un circuito seguidor de voltaje, y esa fue mi primera idea. Para hacer mis pruebas, hice unos cambios para, en lugar de mostrar un patrón de tablero de ajedrez, mostrase simplemente rayas verticales. Sin embargo mi televisor parecía confundirse. Por supuesto que se incrementaba el brillo, pero a veces se descentraban, o incluso salían con la mitad de brillo. No conseguí explicar del todo este fenómeno, pero lo más probable es que estuviese saturando el operacional por el lado de los sincronismos. Esto tiene fácil solución, sólo habría que invertir los valores del DAC y darle la vuelta a la conexión.

Sin embargo desistí, no quería seguir buscando unos buenos valores a la salida del DAC por prueba/error e intenté un montaje amplificador-inversor de manual con esas dos resistencias. Para esto, sobre mi protoboard, coloqué la resistencia de 3.9 kΩ entre los pines 2 y 6 del amplificador (este será nuestro bucle de realimentación). La resistencia de 2.2 kΩ la coloqué en el pin 2 (entrada inversora) del operacional, cortocircuité los pines 3 y 4 (entrada no inversora a masa) y uní los pines 4 al terminal de masa del espacio dedicado al módulo de LCD y 7 a la salida de cinco voltios del mismo espacio, sujetado con dos agujas para mantenerlos fijos en el agujero.

La señal del terminal 23 del J8 entraría por el terminal libre de la resistencia de 2.2 kΩ y la señal (invertida y amplificada) la tendría en el pin 6.

800px-Generic_741_pinout_top

Finalmente, saqué dos cables del terminal 24 (tensión constante a 3.3V) del J8 y del pin 6 del operacional, usando la salida del pin 6 como masa y la del J8 como señal. Esto es importante hacerlo así, ya que me he dado cuenta que con los valores que he usado en el DAC una componente de continua demasiado grande confunde la tele. Con la salida de 3.3V del J8 centro muy bien la señal. El problema es que claro, está invertida, es por eso por lo que invertimos la polaridad en el RCA.

El resultado fue de nuevo un poco patata, no había gran diferencia entre blancos y grises, pero era realmente culpa de la saturación del operacional.

Dividiendo todos los valores del DAC aproximadamente por tres (de forma que el negro pasaba a 90 y el blanco a 300) los resultados adquirían una calidad aceptable dentro de las limitaciones del 741. Comparemos la imagen original, sin amplificación:

La señal en forma de rayas verticales a máximo brillo, sin amplificación.

La señal en forma de rayas verticales a máximo brillo, sin amplificación.

A la imagen a la salida del operacional:

Salida amplificada. Nótense los bordes borrosos.

Salida amplificada. Nótense los bordes borrosos.

¿Y cómo es que el negro sale tan estrecho, comparado con la imagen sin amplificar, preguntaréis? Pues esto es culpa precisamente del limitado ancho de banda del 741. En la foto no se ve bien, pero esas líneas blancas en realidad tienen unos bordes muy difuminados. Además, la saturación del operacional corta el blanco en un tono de gris extraño, por lo que la sensación de anchura de las barras blancas se acentúa. Más que hacer una señal cuadrada hace una especie de patrón en forma de olas que suben y bajan.

Más adelante veremos que esto es suficiente para mostrar un pequeño texto por pantalla. Ahora solucionemos el último detalle que nos falta.

Un poco más complicado: el sincronismo vertical
Hasta aquí vimos como encajar la imagen en los bordes laterales y aumentar la intensidad del brillo. Sin embargo, nos queda una última cosa por solucionar para poder apreciar algo que podamos llamar «dibujo» en la pantalla: el sincronismo vertical.

El sincronismo vertical tiene una complicación extra porque a) es largo y complejo (mucho más largo que el sincronismo de línea) y b) empieza antes o después en función del campo.

El campo impar comienza al principio de la línea 6 completa, comenzando con un sincronismo horizontal. La información real de la imagen empieza en la mitad  de la línea 23 (empezando a contar desde uno), aunque esto no se marca de ninguna forma especial: la línea se entrega completa y dura los 64 microsegundos de rigor. El campo acaba en la línea 310 completa, después de la cual comienza el sincronismo vertical.

Por contra, el campo par comienza en la mitad de la línea 318, sin ningún sincronismo que marque tal inicio y acaba en la mitad de la línea 623. Esto quiere decir que antes de que la línea 623 acabe comienza el sincronismo vertical. Sin embargo, una vez más, no es hasta la línea 336 completa donde empezaremos a ver datos de imagen.

Pulsos de igualación y sincronismo vertical en PAL, tomado de la página web de Martin Hinner.

Pulsos de igualación y sincronismo vertical en PAL, tomado de la página web de Martin Hinner.

Nótese que en el campo impar tenemos 18 líneas completas y en el par tenemos 17 y media en la que no va ninguna información de imagen. Es aquí donde las emisoras colocan la información de teletexto y similares. Cuando en algunas televisiones viejas el sincronismo vertical fallaba se podían ver algunos datos de teletexto superpuestos a la imagen real en el borde superior de la pantalla (que es donde se escondían estos datos), apareciendo algo muy parecido a esto.

Empiece a mitad de la línea o no, después de la última línea se suceden una serie de 5 impulsos de igualación a nivel de sincronismo cada 32 µs, exactamente a la mitad de la duración de una línea. Estos impulsos de igualación se componen de:

  1. Un intervalo en el que se alcanza el nivel de negro de 3.2 µs, similar al pórtico anterior.
  2. El pulso propiamente dicho, mucho más corto que el sincronismo horizontal. Se hace bajando al nivel de sincronismo y dura 2.35 µs
  3. El resto de los 32 µs, a modo de pórtico posterior, de nuevo a nivel de negro.

Siguiendo nuestro esquema de 0.666 µs por tick, tenemos que cada una de las partes deben durar 4.8 ticks (que redondeamos a 5 para el intervalo de negro), 3.5 ticks (que redondearemos a 4 para el impulso de igualación) y 96 / 2 – (4 + 5) = 39 ticks para el resto)

Después de los impulsos de igualación vienen 5 impulsos de sincronización de campo que también duran 32 µs cada uno y que se componen de:

  1. El pulso en sí mismo, que dura 27.3 µs y se hace a nivel de sincronismo.
  2. Un intervalo de 4.7 µs donde se alcanza el nivel de negro.

Nótese que el intervalo de separación dura lo mismo que un sincronismo horizontal, sólo que esta vez no hay pórticos. Como hemos visto que el sincronismo horizontal se codificaba en 7 ticks, esto nos da 96 / 2 – 7 = 41 ticks para el pulso de sincronización.

De nuevo se suceden otros 5 pulsos de igualación de las mismas características que los anteriores y, en función del campo que estemos comenzando, empezaremos a mitad de una línea o no.

En cualquier caso, mi solución es la siguiente: debemos modificar la máquina de estados para decidir si se toman las líneas completas o no. Yo opto por añadir ahora un flag que identifique el campo par, y tres estados más que identifiquen el primer tren de impulsos de igualación, los de sincronismo y el último tren (STATE_PREEQ, STATE_VSYNC, STATE_POSTEQ). Además, podemos simplificar los cálculos utilizando dos arrays con los pulsos ya precocinados.

El código ha sido especialmente complicado de optimizar. De hecho, en estado STATE_LINE no hay ningún cálculo más complicado que una suma. El comienzo y fin de cada imagen se precalcula en variables globales en vez de ejecutar las sentencias condicionales pertinentes a cada tick. La imagen esta vez se precalcula en un array llamado buffer en pal.c y dicho array ni siquiera es bidimensional: los arrays bidimensionales son costosos ya que su acceso implica siempre una multiplicación (y esto ha sido determinante a la hora de mantener la rutina del temporizador dentro de los 0.666 microsegundos).

El precálculo de estos arrays ha llevado a la escritura de la función pal_init() la cual debe ser llamada desde el main antes de configurar el temporizador.

Los resultados han tenido que ser ajustados con ayuda del osciloscopio, midiendo manualmente cada retardo entre pulsos, líneas, sincronismos etcétera. La transición de campo impar a campo par se puede ver aquí:

ADS00007

Podemos apreciar que la línea 310 acaba entera, y que la 318 empieza justo por la mitad, dando paso al campo par. Y del mismo modo:

ADS00006

La línea 623 acaba por la mitad, pero la línea 6 del campo impar comienza completa. Además, podemos ver que el retardo entre dos pulsos del sincronismo vertical es siempre 64 microsegundos como estaba previsto.

Las modificaciones han sido numerosas, pero todas están en la misma línea de lo que se ha hecho hasta ahora. El código listo y funcional, mostrando un patrón en damero a máxima frecuencia se puede descargar aquí.

La guinda
Los problemas técnicos más importantes han sido superados. El avance más significativo con estas mejoras se encuentra en haber separado el proceso de construcción de la imagen con el de su envío a la pantalla.

Bueno, ¿y qué podemos enviar a la pantalla? Pues virtualmente cualquier cosa con una resolución de 77×78 píxeles. Yo por mi parte he escrito una rutina de procesamiento de archivos BMP de Windows que me permite incrustar un BMP en escala de grises y enviarlo por pantalla. El código lo metí en bmp.c y la imagen la pasé a escala de grises y luego a un array de bytes en C en pic.h. Sólo fue necesario sustituir el bucle que generaba el patrón en damero por la llamada esta rutina y listo.

El código de esta última iteración se puede descargar aquí.

No es nada del otro mundo, sin embargo simplificó muchísimo la parte de enviar imágenes cualesquiera a la pantalla. En blanco y negro, eso sí. Los resultados se pueden apreciar aquí:

OLYMPUS DIGITAL CAMERA

Sí, puse una foto mía, realmente no se me ocurrió nada mejor que poner 😛

Posibles mejoras
Esto es algo bastante extremo en términos de tiempo de ejecución. Realmente apenas da tiempo a hacer las cosas en 666 nanosegundos, he ahí el núcleo de la complejidad técnica de esta ocurrencia. Sin embargo, hay posibles mejoras que se podrían hacer. Esta placa dispone de una cosa llamada DTC (Data Transfer Controller) que permite programar desplazamientos de datos enteros paralelamente al flujo de ejecución de la MCU, de una forma muy similar a como lo hace DMA. El asunto es que hay algún ejemplo que utiliza el DTC para enviar muestras de audio por el DAC, por lo que a mi entender la velocidad del DTC se puede configurar. El único problema es que no sé si hay una velocidad máxima, y hasta qué punto daría tiempo a reconfigurar el buffer de pantalla entre sincronismo y sincronismo (y si este cabe en memoria, vaya).

De todos modos, ya veremos. Si todo fuese genial, quizá el DTC pudiese correr a una velocidad suficiente como para colocar datos de teletexto en las primeras líneas del marco, y hacer una especie de biblioteca secreta de contenido absurdo en ese ridículo formato alfanumérico de aquellos felices años de la era pre-internet.

Anuncio publicitario

3 comentarios

  1. Fijo que no funciona, que es todo mentira 😀

    Pd. Fenómeno.


  2. que barbaro, te felicito, gracias por la claridad de la expli.


  3. Me he quedado asombrado, practicamente no entendia nada pero no he podido evitar seguir leyendo.



Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

A %d blogueros les gusta esto: