Autor Tema: Diseño de un OSD  (Leído 19110 veces)

0 Usuarios y 1 Visitante están viendo este tema.

Desconectado elgarbe

  • Moderador Local
  • PIC24H
  • *****
  • Mensajes: 2178
Diseño de un OSD
« en: 09 de Febrero de 2014, 22:12:11 »
Bueno, seguimos abriendo puertas en el diseño de nuestra controladora de vuelo. En este caso empezaremos ver lentamente el diseño del sistema OSD (On Screen Display), el cual se encarga de sumar informacion a la señal de video de la cámara que usemos en nuestro UAV para el vuelo en primera persona.
Generalmente los sistemas UAV de los usados para hobby suelen tener una o más camaras a bordo, con ellas uno puede practicar FPV (First person view) y volar en tierra como si estuviese sentado en el UAV. Uno de los sistemas más importantes es el OSD, el cual puede tener o no sistema de estabilizacion, suele tener conexion a GPS para lo que se denomina RTH (ruder o return to home), navegacion por waypoint, suele tener telemetría, la cual se suma a la señal de video que nos envía la cámara para poder ver toda la telemetría en pantalla.

Por ahora estoy en la etapa de estudio, por lo que iré posteando lo que valla consiguiendo hacer funcionar. como primer medida he visto que muchos sistemas usan el LM1881 para extraer los pulsos de sincronismo de la señal de video y por lo tanto me he pedido un par de samples de TI.

Dejo algunos links muy interesantes para ir leyendo:

http://www.open-electronics.org/a-video-overlay-shield-for-arduino/ - Usando el LM1881 de un shield para arduino. Sin multiplexor para la mezcla.
http://catarina.udlap.mx/u_dl_a/tales/documentos/lem/paz_l_oj/capitulo3.pdf - Excelente trabajo y muy bien explicado. Usa el LM y multiplexor. Hay mucha teoría.
http://electronics-home-projects.tripod.com/ - Otro sistema con el LM y multiplexor. En ingles.
http://web.archive.org/web/20070111104641/www.rickard.gunee.com/projects/video/pic/howto.php como usar uC PIC para generar video compuesto y bastante teoría en ingles.

Bueno, proximamente avances!

Saludos!
-
Leonardo Garberoglio

Desconectado diegogprs

  • PIC10
  • *
  • Mensajes: 21
Re: Diseño de un OSD
« Respuesta #1 en: 10 de Febrero de 2014, 02:58:38 »
Hola , Interesante proyecto , Aqui te dejo algunos link   para sistemas de osd ,para ir ampliando tu info http://www.libstock.com/projects/view/505/osd-click-example  http://www.micro-examples.com/public/microex-navig/doc/081-pic-osd-superimposer.html  . Saludos

Desconectado elgarbe

  • Moderador Local
  • PIC24H
  • *****
  • Mensajes: 2178
Re: Diseño de un OSD
« Respuesta #2 en: 10 de Febrero de 2014, 10:07:45 »
Muy buenos links!, el primero es con el IC de maxim, es casi un OSD en sí. es caro y creo que dificil de conseguir. El segundo link es mucho más interesante y es ideal para empezar a probar. No usa LM1881 ni multiplexor para mezclar las señales... Usa el comparador interno del uC para detectar los pulsos de sincronismo. A simple vista no entiendo la forma de sacar los datos.... en teoría la señal de salida debe estar entre 0 y 1V.... habrá que estudiarlo un poco.

Muy bueno! gracias!
-
Leonardo Garberoglio

Desconectado elgarbe

  • Moderador Local
  • PIC24H
  • *****
  • Mensajes: 2178
Re: Diseño de un OSD
« Respuesta #3 en: 10 de Febrero de 2014, 13:05:36 »
Aquí hay un proyecto más que interesante para tomar como base:

http://garydion.com/projects/videoverlay/

Usa el LM1881, lo que simplifica la obtencion de los pulsos de sincronismo. Para sacar informacion usa la salida serie del uC. Tiene un preset de 1K para ajustar los niveles de señal. Otra vez mezclan la señal "a la fuerza"... eso no lo entiendo bien aún... creo que la idea es suponer que la señal de video tendrá valor de tension por debajo de 1V (1V es blanco) siempre, o sea, serán grises o negro. Entonces si pongo una señal de 1V mi señal "le gana a la de video" y cuando meto señal se pinta de blanco. Entiendo que con este método no podría poner texto en escala de grises, solo blanco...
No estoy seguro aún del uso de la conexion PB0...

Despues el código lo que hace es simple, todo se maneja con 3 interrupciones. Una la de sincronismo horizontal o INT0, lo que hace aquí es setear el timer para que interrumpa despues de XX useg, ese XX será el número de fila. aparte incrementa el contador de filas. El sincronismo vertical entra por la INT1 y simplemente resetea el contador de filas. Finalmente en la interrupcion del timer lo que hace es verificar en que línea se encuentra y en funcion de ello pone datos en el buffer del puerto serie (SPDR) y espera a que se transmita...

por lo visto acá todo el secreto está en la configuracion del puerto serie para que sincronice con la velocidad de una fila... es un buen punto de partida este código para tratar de crear uno propio...

EDITO: La señal de video no esta en PB0 como dice el esquemático, está en el AIN1 como se ve en el protoboard. Probablemente para intentar obtener sincronismo directamente desde la señal de video, con el comparador para detectar señales menor a 0.3V.
Tampo usa el puerto serie, usa el bus SPI...

Saludos!
« Última modificación: 10 de Febrero de 2014, 13:23:39 por elgarbe »
-
Leonardo Garberoglio

Desconectado elgarbe

  • Moderador Local
  • PIC24H
  • *****
  • Mensajes: 2178
Re: Diseño de un OSD
« Respuesta #4 en: 13 de Febrero de 2014, 13:09:02 »
Bueno, hoy reciví finalmente el bichito gracias a Texas:



voy a ir armando el circuito para ir testeando las señales de video...

Saludos!
-
Leonardo Garberoglio

Desconectado elgarbe

  • Moderador Local
  • PIC24H
  • *****
  • Mensajes: 2178
Re: Diseño de un OSD
« Respuesta #5 en: 16 de Febrero de 2014, 17:01:41 »
Bueno, llego la hora de empezar a probar este nuevo bichito...
Es recomendable leer este excelente trabajo de Otto Joel Paz Luna como tesis de grado en la Universidad de las americas en méxico:
http://catarina.udlap.mx/u_dl_a/tales/documentos/lem/paz_l_oj/

En particular el capítulo 2 y 3 donde se habla de las señales de video.

Veamos algunos coceptos básicos. Una IMAGEN de video se pinta o refresca a 50Hz (60Hz en brasil y EEUU), está formada 525 lineas en el sistema NTSC o 625 lineas en el sistema PAL. Para formar una imagen en pantalla son necesarios 2 barridos completos de la pantalla, ya que la imagen se forma entrelazada, primero se pintan 312.5 líneas y luego 312.5 lineas más para formar la imagen completa (por lo menos mi camarita que es PAL así lo hace). Si la frecuencia de refrezco es de 50Hz y tenemos 312.5 líneas por cada refrezco, entonces tenemos 15625 líneas por segundo. Por lo tanto el tiempo que nos lleva barrer una línea completa (tiempo de barrido horizontal) es de 64uSeg.
Cuando el haz de electrones (en los viejos televisores de TRC) llega al final de un CUADRO (termina de pintar una imagen) debe retroceder y se debe generar una señal de barrido vertical. Aparte existe una zona de blanqueo del vertical y otras cuestiones que, insisto, podrán leer con mucho detalle y en español en el capítulo 2 del trabajo que les mencioné... no me voy a poner a copiar lo que otro ya explicó y decir que lo hice yo...
Lo importante a saber es que antes, durante y despues del pulso de sincronismo vertica, hay pulsos de sincronizmo horizontál que no llevan informacion de imagen. Una parte de esa porcion puede ser utilizada para enviar informacion. En mi caso, al querer usar este OSD para un UAV, se puede aprovechar el enlace de video (el cual generalmente es comercial por la dificultad de diseñar en circuitos para 1.2 GHz) para enviar telemetría a tierra.
Tambien hay que tener en cuenta esos pulsos para contabilizarlos al momento de intentar generar nuestra señal de video en el momento justo para que se muestre en la posicion deseada en la pantalla.

Bien, demasiada teoría por ahora, vallamos a los bifes.
Como les comente la idea es utilizar el LM1881 (gracias texas!!!!) para extraer las señales de sincronismo.
Entoces, siguiendo las recomendaciones de la hoja de datos monte esto en el protoboard:



En la foto pueden ver, mi super fuente de alimentacion (dentro de la fuente de PC), el receptor de video de mi equipo FPV, el protoboard con el LM, 1 resistencia y 3 capacitores y el analizador lógico.
Bien, he muestreado la señal de sincronismo vertical y horizontal a la salida del LM y obtengo lo siguiente:



Bien, aquí pueden ver dos pulsos de sincronismo vertical, espaciados exactamente 20mseg (50Hz) y pueden ver muchos pulsos de sincronismo horizontal. Tambien pueden notar que alrededor de los pulsos de sincronismo vertical hay mas densidad de pulsos de sincr. horizontal.

Bueno, hagamos zoom a esa zona y veamos que tenemos:



en esta imagen vemos antes del pulso vertical tenemos 6 pulsos llamados pulsos de ecualizacion. Esos pulsos se generan al doble de la frecuencia de campo y en la hoja de datos los cuenta como 3 campos. Luego, vienen 6 pulsos más (3 campos) llamados serrated vertical pulse, luego bienen 6 pulsos de ecualizacion (3 campos más) y finalmente comienzan los campos de imagen. cabe destacar que de los campos 10 a 21 (o 12, no estoy seguro) no hay informacion de imagen, solo los pulsos de sincronismo horizontal.

En resumen, nosotros deberíamos entrar con el pulso vertical a un Input Capture de nuestro uC, cuando detectamos el falling edge es que estamos en el campo 4. Tambien debemos entrar con los pulsos horizontales en otra Input Capture y a partir del campo 4 contamos 18 falling edge y estaríamos en el comienzo de la primer línea del televisor, arriba a la izquierda. Como el barrido horizontal dura 64uSeg, tenemos ese tiempo para ubicar nuestro texto horizontalmente... Luego, llevando el conteo de pulsos horizontales sabremos en que linea vertical estamos. La resolucion que podamos conseguir dependerá de la velocidad del micro que usemos. Notar que si queremos, por ejemplo, 64 columnas o pixel horizontales tendremos 1useg para sacar la info de pixel....

Bien, entendido esto, viene la segunda parte, la de insercion de informacion en la señal de video.

Veremos eso en la próxima...

Saludos!
-
Leonardo Garberoglio

Desconectado elgarbe

  • Moderador Local
  • PIC24H
  • *****
  • Mensajes: 2178
Re: Diseño de un OSD
« Respuesta #6 en: 16 de Febrero de 2014, 20:22:52 »
Bueno, ya que estamos con esto continuemos un poco más.

Ya hemos visto como obtener los pulsos de sincronismo tanto vertical (comienzo de cuadro) como los del horizontal (comienzo de línea).
Antes de ver como usar esa info en el uC (cosa que estoy estudiando) veamos como se "mezcla" la señal de video original con la que queremos agregar nosotros.
La señal de video blanco y negro se consigue con una variacion del nivel de tension en la parte del cuadro donde va la informacion de la imagen. La señal de video tiene aproximadamente 1Vpp. Por lo general entre -0.3 y 0V es donde encontramos las señales de sincronismo. Entre 0V y 0.7V tenemos la señal de grises. A mayor tension mayor intensidad de blanco. La tension mínima corresponde al negro.

Dicho esto sabemos entonces que para elegir el tono de gris en el que saldrá nuestro texto dependerá de la tension aplicada a la señal de video. El tema es como mezclar la señal de video original con nuestra señal.
La primera forma que a uno se le ocurriría es la de multiplexar las dos señales. Con simples llaves analógicas (4066) podemos elegir entre nuestra señal (generada con el uC) o la señal original la que pasará de largo. Otra forma es meter nuestra señal "a la fuerza" y "ganarle" a la señal original.
El primer caso tiene como ventaja que no importa el color de la señal original, ya que en el lugar donde va nuestro texto ponemos el color que queremos. Tiene como desventaja que necesita mas hardware y un poco más de software.
El segundo caso tiene alguna limitaciones. Por ejemplo, el texto no podrá ser negro. Ya que si con el micro genero la señal con la mínima intensidad que es el negro, no le voy a poder ganar nunca a la señal de video original, si la señal original tiene 0.6V representando algo gris y yo pongo una tension de 0.2V la señal que "gana" es la del video original y no podre mostrar nada. por lo tanto, según lo entiendo yo, podré usar colores más tirando al blanco. Un ejemplo de este método es el que vemos aquí: http://garydion.com/projects/videoverlay/
Notar que el texto es blanco y con fondo casi negro. Es un poco transparente ese negro en algunas partes. Como pueden ver el transistor (activado por el bus SPI) manda señal (regulada por el preset) a la fuerza a la señal de video.
En el trabajo de Otto que les menciones en el otro post hace uso de la multiplexacion.

En mi caso, he visto que los OSD "caseros" usan la tecnica de mezcla "a la fuerza" (http://www.telecable.es/personales/label/DAKAR_OSD_27B.rar), por lo que probaré dicha técnica.

Bueno, ya veremos entonces como seguimos armando la idea completa del sistema.

Saludos!
-
Leonardo Garberoglio

Desconectado elgarbe

  • Moderador Local
  • PIC24H
  • *****
  • Mensajes: 2178
Re: Diseño de un OSD
« Respuesta #7 en: 17 de Febrero de 2014, 22:54:38 »
Sigamos con este tema.

Hoy decidí empezar a trabajar con el uC, tomando las señales de sincronismo vertical y horizontal como para ir mezclando un poco todo.
Como primer medida caí en la cuenta que el uC trabaja con 3.3V y el LM1881 trabaja con 5V... Decidí armarme entonces una plaquita para convertir los niveles de tension. Para ello utilicé unos "Bidirectional voltalge level translator" o TXB0102 de mi querido Texas Instruments. Lamentablemente hoy en el laburo no alcancé a terminar el Impreso así que les debo las fotos y el proyecto (el cual voy a compartir una vez probado). Pero como quiero avanzar si o sí algo hoy monte un simple divisor resistivo y tomé la señal de sincronismo de la division. con una R de 5.6K y otra de 3.9K obtengo unos 3V de señal activa... como esa señal es en una sola direccion puedo usar eso por ahora.

Bueno, como primer tema podría intentar medir el período de la señal de sincronismo vertical como para ir viendo que lo que voy haciendo funcione. Para ello ingresaré la señal de sinc. vertical en la INT0 de mi LPC1769 y lo voy a configurar para que interrumpa en el falling edge. Tambie deberé hacer correr un timer (TMR2 en mi caso) con el Tick más pequeño que pueda. A este micro, corriendo a 100MHz y configurando como entrada del timer el coreclock tendré un tick de 10nseg!!!!
Entonces, cuando llega la interrupcion almaceno el valor de contador del timer y se lo resto al que tenía en la interrupcion anterior para obtener el valor en decenas de nanosegundos entre interrupciones.

Veamos el código:

Código: [Seleccionar]
/*
 * Timer2.h
 *
 *  Created on: 28/01/2014
 *      Author: elgarbe
 */

#ifndef TIMER2_H_
#define TIMER2_H_

#include "LPC17xx.h"

/* functions */
void tim2_init(uint8_t irq_priority);

#endif /* Timer2 */

Código: [Seleccionar]
/*
 * Timer2.c
 *
 *  Created on: 28/01/2014
 *      Author: elgarbe
 */

#include "Timer2.h"

/****************************************************************************/
/*                             Public Functions                             */
/****************************************************************************/
// This functions initializes TIMER2.
//   Assumes system clock is 100 MHz.
void tim2_init(uint8_t irq_priority) {

    // give power to TIMER2
    LPC_SC->PCONP |= (1 << 22);

    // set peripheral clock selection for timer 2
    LPC_SC->PCLKSEL1 &= ~(3 << 12); //Borramos bits
    LPC_SC->PCLKSEL1 |=  (1 << 12); // set to "01" = Core Clock (100 MHz) 10nseg Tick

    // set the clock source for TIMER2 to PCLK
    LPC_TIM2->CTCR = 0; // this is the default

    // set the prescaler
    LPC_TIM2->PR = 0; // TC increments on every PCLK

    LPC_TIM2->MR0 = ~0; //Para que en un inicio no Interrumpa
    LPC_TIM2->MCR |= 3; //Interrupcion en Match R o y resetea el Contador

    // enable and reset the the counters
    LPC_TIM2->TCR = 3;
    LPC_TIM2->TCR = 1;


    // Registramos la interrupcion del Timer2
    NVIC_EnableIRQ(TIMER2_IRQn);

    // Fijamos la prioridad de la interrupcion
    NVIC_SetPriority(TIMER2_IRQn, irq_priority); // '0' es la mayor
}

/****************************************************************************/
/*                           Interrupt Functions                            */
/****************************************************************************/
void TIMER2_IRQHandler(void) {

    // Verificamos que la Int es por Match Register 0
    if(LPC_TIM2->IR & 0x1){

    }

    // clear the interrupt
    LPC_TIM2->IR |= 0x1;
}

Bueno, acá tenemos la inicializacion del Timer 2. Está configurado para que genere interrupcion cuando el Contador alcance el valor almacenado en MatchRegister0. Pero esa interrupcion no la utilizaremos por ahora, simplemente haremos uso del free runing timer.

Código: [Seleccionar]
#ifndef __EXTINT_H
#define __EXTINT_H

void EINT0_IRQHandler(void);
void EINT1_IRQHandler(void);
void EINTInit( void );
long getTime();
static long old_tmr=0;
static long new_tmr=0;
static long time=0;

long getTime(){
return(time);
}
/*****************************************************************************
** Function name: EINT0_Handler
**
** Descriptions: external INT handler
**
** Interrupcion en el Pulso de Sincronismo Vertical
**
*****************************************************************************/
void EINT0_IRQHandler (void) {
LPC_SC->EXTINT |= 1; // clear interrupt
old_tmr=new_tmr;
new_tmr=LPC_TIM2->TC;
time=new_tmr-old_tmr;
// line=0; //Reseteo el contador de lineas
}

/*****************************************************************************
** Function name: EINT1_Handler
**
** Descriptions: external INT handler
**
** Interrupcion en el Pulso de Sincronismo Horizontal
**
*****************************************************************************/
void EINT1_IRQHandler (void) {
LPC_SC->EXTINT |= 2; // clear interrupt
line++; //Sumo 1 al contador de líneas
//Aqui debo verificar si estoy en alguna línea que me interese
//y debo configurar el TIMER para que me interrumpa en el tiempo correspondiente
//a la columna donde quiero que valla el texto
}

/*****************************************************************************
** Function name: EINTInit
**
** Descriptions: Initialize external interrupt pin and
** install interrupt handler
**
** parameters: None
** Returned value: true or false, return false if the interrupt
** handler can't be installed to the VIC table.
**
*****************************************************************************/
void EINTInit( void )
{
LPC_PINCON->PINSEL4 &= ~(0xF << 20); // Borro los bits 20-23
LPC_PINCON->PINSEL4 |= ((1 << 20) | (1 << 22)); // Elijo 01 en P2.10 y P2.11


//  LPC_GPIOINT->IO2IntEnF = 0x200; /* Port2.10 is falling edge. */
LPC_SC->EXTMODE = 3; // INT0 e INT1 se disparan por Flanco
LPC_SC->EXTPOLAR = 0; // INT0 e INT1 se disparan por Falling Edge

NVIC_EnableIRQ(EINT0_IRQn); //Habilito la interrupcion EINT0
NVIC_EnableIRQ(EINT1_IRQn); //Habilito la interrupcion EINT1
}

/******************************************************************************
**                            End Of File
******************************************************************************/


#endif /* end __EXTINT_H */

Aquí tenemos el código de inicializacion de las interrupciones externas 0 y 1. También tenemos las ISR de dichas interrupciones.
En la de la Int0 tengo el código para medir el tiempo entre una int y la anterior. En la Int1 (sincronismo horizontal) simplemente incrementamos una variable que cuenta en qué fila de la pantalla estamos. Eso tampoco lo usaremos por ahora.

Finalmente el main.c

Código: [Seleccionar]
#ifdef __USE_CMSIS
#include "LPC17xx.h"
#endif

unsigned short line=0;

#include <cr_section_macros.h>
#include <NXP/crp.h>
#include "extint.h"
#include "Timer2.h"

/*******************************************************************************
**   Main Function  main()
*******************************************************************************/
int main (void){
long tiempo=0;
tim2_init(2); // Inicializamos el Tmr2 y le establecemos prioridad 2
EINTInit(); // initialize GPIO pins as external interrupts

while ( 1 ){
tiempo=getTime();
}
}

Bien simple, inicializamos el timer y la interrupcion y nos quedamo pidiendo el valor del tiempo entre interrupcion...

Veamos una captura del IDE midiendo dicho pulso:



como ven mido 2.000.080 decenas de nanosegundos. O lo que es lo mismo 20mseg con 800ns...

Bueno, nuestro uC ya empieza a leer la señal de video!

Saludos!
-
Leonardo Garberoglio

Desconectado elgarbe

  • Moderador Local
  • PIC24H
  • *****
  • Mensajes: 2178
Re: Diseño de un OSD
« Respuesta #8 en: 18 de Febrero de 2014, 00:14:10 »
Bueno, sigamos un poco más. Intentemos medir ahora cuantos pulsos (no es lo mismo que la cantidad de cuadros, ya veremos por que) de sincronismo horizontal entran entre dos pulsos de sincronismo vertical.

Para ello realizamos algunas modificaciones en el código:

La interrupcion externa 0 ahora queda así:

Código: [Seleccionar]
void EINT0_IRQHandler (void) {
LPC_SC->EXTINT |= 1; // clear interrupt
old_tmr=new_tmr;
new_tmr=LPC_TIM2->TC;
time=new_tmr-old_tmr;
prev_line=line;
line=0; //Reseteo el contador de lineas
}

Cuando llega el pulso de sinc vertical, mido el tiempo, luego almaceno el número actual de pulso y reseteo el contador de linea.

En el main tengo:

Código: [Seleccionar]
int main (void){
long tiempo=0;
short lineas=0;
tim2_init(2); // Inicializamos el Tmr2 y le establecemos prioridad 2
EINTInit(); // initialize GPIO pins as external interrupts

while ( 1 ){
tiempo=getTime();
lineas=getLine();
}
}

Al detener el código luego de unas pasadas obtengo la siguiente pantalla:



Como ven estoy contando 320 pulsos que corresponden a los 312.5 campos + las señales de sincronismo vertical.

Voy a intentar medir ahora el tiempo entre los diversos falling edge de la señal de sincronismo horizontal del LM.

Para ello modificamos la funcion de la IntExt 1,la cual se produce con los pulsos horizontales del LM.

Código: [Seleccionar]
void EINT1_IRQHandler (void) {
LPC_SC->EXTINT |= 2; // clear interrupt
//Aqui debo verificar si estoy en alguna línea que me interese
//y debo configurar el TIMER para que me interrumpa en el tiempo correspondiente
//a la columna donde quiero que valla el texto
old_tmrH=new_tmrH;
new_tmrH=LPC_TIM2->TC;
tiempos[line]=new_tmrH-old_tmrH;
line++; //Sumo 1 al contador de líneas
}

Bien, veamos el resultado:

tiempos[0]   long int   3204   
tiempos[1]   long int   3204   
tiempos[2]   long int   3212   
tiempos[3]   long int   3200   
tiempos[4]   long int   3204   
tiempos[5]   long int   3196   
tiempos[6]   long int   3200   
tiempos[7]   long int   3200   
tiempos[8]   long int   3200   
tiempos[9]   long int   3200   
tiempos[10]   long int   6400   
tiempos[11]   long int   6392   
tiempos[12]   long int   6396   
tiempos[13]   long int   6400   

Como se ve, al comienzo del pulso vertical tenemos 10 pulsos de 32uSeg de duracion. Luego comienzan los pulsos a frecuencia nominal con duracion de 64uSeg.
Al finalizar el cuadro tenemos los siguientes pulsos:

tiempos[312]   long int   6400   
tiempos[313]   long int   6400   
tiempos[314]   long int   3200   
tiempos[315]   long int   3200   
tiempos[316]   long int   3200   
tiempos[317]   long int   3204   
tiempos[318]   long int   3196   
tiempos[319]   long int   3200   
tiempos[320]   long int   0   

como ven tenemos 6 pulsos más con período 32uSeg. Tambien podemos obtener la cantidad de pulsos con 64uSeg. 313-10+1 = 304 pulsos o cuadros. Hay una diferencia con los 312.5 cuadros que calculamos. Estimo que algunos de los pulsos de 32useg deben ser contados como cuadros.

Bien, en principio ya tenemos el uC leyendo con presicion las señales de sincronismo de video. Resta ahora hacer un sistema de "posicionamiento"vertical y horizontal, para finalmente probar la insercion de informacion en la parte de video....

Saludos!
-
Leonardo Garberoglio

Desconectado elgarbe

  • Moderador Local
  • PIC24H
  • *****
  • Mensajes: 2178
Re: Diseño de un OSD
« Respuesta #9 en: 18 de Febrero de 2014, 20:18:32 »
Bueno, como ya hemos visto y resumiendo un poco tenemos dos señales que obtenemos del video, gracias al LM1881. El de sincronismo vertical, el cual se da una vez por CUADRO y 2 veces por IMAGEN (cada imagen se transmite en 2 partes interlazadas), nos servirá para saber cuando comienza un nuevo cuadro. Lugo tenemos los pulsos de sincronismo horizontal. El LM nos entrega 320 pulsos en un sistema PAL. Solo 312.5 corresponden a sincronismo horizontal de líneas de imagen. El resto son señales adicionales en la zona de sincronismo vertical.

Para superponer texto debemos saber en que línea nos encontramos actualmente. Para ello podemos tener una variable "linea" que almacene el pulso de sincronismo actual. Dicha variable se inicializa a 0 cuando llega el pulso de sincronismo vertical. Las primeras 10 o 20 líneas de las 320 no correspone a área de pantalla. Por ejemplo, en la imagen que se ve aquí, el texto comienza en la línea 41 y tiene un alto de 7 pixeles.

Una vez que detectamos la línea en la que queremos escrivir debemos tener en cuenta la cantidad de pixeles verticales de la fuente que usemos. En el ejemplo, como vimos tenemos 7 pixeles de altura. Para facilitar la programacion, el ancho de un caracter es de 8 pixeles. Entonces podemos codificar los carateres en una tabla (debo confesar que todo esto lo estoy sacando del código fuente de la página que puse más arriba...  :shock:):

Código: [Seleccionar]
/* ' '  A   B   C   D   E   F   G   H   I   J   K   L   M   N   O   P   Q   R   S   T   U   V   W   X   Y   Z  */
unsigned char ltrs[189] = { 255,231,131,195,135,129,129,195,189,131,129,189,191,125,125,195,131,195,131,195,131,189,189,125,125,125,  1,
255,219,189,189,187,191,191,189,189,239,247,187,191, 57, 61,189,189,189,189,189,239,189,189,125,187,187,251,
255,189,189,191,189,191,191,191,189,239,247,183,191, 85, 93,189,189,189,189,191,239,189,189,109,215,215,247,
255,189,131,191,189,131,131,177,129,239,247,143,191,109,109,189,131,189,131,195,239,189,219,109,239,239,239,
255,129,189,191,189,191,191,189,189,239,247,183,191,125,117,189,191,179,183,253,239,189,219,109,215,239,223,
255,189,189,189,187,191,191,189,189,239,183,187,129,125,121,189,191,185,187,189,239,189,219,147,187,239,191,
255,189,131,195,135,129,191,195,189,131,207,189,129,125,125,195,191,205,189,195,239,195,231,147,125,239,  1};

Entonces la A será:
Código: [Seleccionar]
11100111 -> 231
11011011 -> 219
10111101 -> 189
10111101 -> 189
10000001 -> 129
10111101 -> 189
10111101 -> 189

Pueden ver como los 0s forman la letra A.

Bien, supongamos que estamos en la línea 41, y en la posicion horizontal deseada (ya veremos esto) sacamos por el SPI (ya veremos esto también) el dato 231, en donde el 1 representa un color más bien negro y el 0 un color blanco (ya veremos eso tambien). En la línea 42 columna XX sacamos el dato 219 y así susesivamente. Automatizando esa tarea podemos sacar un caracter del micro en la línea de video deseada.

Bien, veamos como es una la señal en una línea del horizontal:



bueno, no es la mejor imagen que existem pero es la unica que encontre rapido. Esos tiempo son para una señal PAL. Como podemos ver tenemos 52 useg de área útil. En funcion de cuántos caracteres queramos mostrar en la pantalla y la cantidad de pixel por caracter es el tiempo que tendremos que usar para el que el SPI saque los datos.
Por ejemplo si queremos 40 columnas y cada columna tiene 8 pixel, tendremos 320 pixeles. Si tenemos 52 useg para los 320 pixeles, la separacion entre ellos será de 162nseg!!!! tenemos que configirar nuestro SPI a 6.154MHz mas o menos.... será cuestion de ir probando y viendo la resolucion que se obtiene...

Bueno, finalmente, para forzar la señal de video estoy viendo este esquemático:



Como ven la salida del SPI del Atmega esta conectado a un transistor y a un preset. Activando y desactivando el Tr ponemos un nivel de señal u otro en la señal de video. Aún no entiendo la conexion del C del Tr al Pin PB1, no veo en el codigo fuente que hace esto...

Bueno, esta es la teortía que estaba faltando para seguir avanzando con las pruebas. Ahora hay que empezar a implementar...

Saludos!
-
Leonardo Garberoglio

Desconectado elgarbe

  • Moderador Local
  • PIC24H
  • *****
  • Mensajes: 2178
Re: Diseño de un OSD
« Respuesta #10 en: 20 de Febrero de 2014, 13:19:50 »
Bien, la idea entonces es activar el módulo SPI del uC, configurarlo con un clock de unos 6MHz, para conseguir que cada bit tenga un tiempo de 166nSeg. Con ese tiempo podemos conseguir más de 300 pixeles horizontales. Mucho más que suficiente para una pantalla chica. Incluso creo que en la implementacion final podemos usar menos pixeles, ya veremos los resultados que dicen.

Bueno, la idea es entonces manejar todo el sistema con 3 interrupciones.
La int ext 0 será la encargada de recivir el sincronismo vertical y resetear el contador de líneas.
La int ext 1 será la encargada de recivir el sincronismo horizontal. Incrementará el contador de líneas y configurará y reseteará el timer2 para que interrumpa en el punto justo dentro de los 52uSeg de la imagen.
El TMR2, el cual irá poniendo una a una las filas del caracter a mostrar en el registro de salida del SPI.

Veamos un poco el código que voy escribiendo:

extint.h:

Código: [Seleccionar]

#ifndef __EXTINT_H
#define __EXTINT_H

void EINT0_IRQHandler(void);
void EINT1_IRQHandler(void);
void EINTInit( void );


/*****************************************************************************
** Function name: EINT0_Handler
**
** Descriptions: external INT handler
**
** Interrupcion en el Pulso de Sincronismo Vertical
**
*****************************************************************************/
void EINT0_IRQHandler (void) {
LPC_SC->EXTINT |= 1; // clear interrupt

line=0; //Reseteo el contador de lineas
}

/*****************************************************************************
** Function name: EINT1_Handler
**
** Descriptions: external INT handler
**
** Interrupcion en el Pulso de Sincronismo Horizontal
**
*****************************************************************************/
void EINT1_IRQHandler (void) {
LPC_SC->EXTINT |= 2; // clear interrupt
//Aqui debo verificar si estoy en alguna línea que me interese
//y debo configurar el TIMER para que me interrumpa en el tiempo correspondiente
//a la columna donde quiero que valla el texto
line++; //Sumo 1 al contador de líneas
    LPC_TIM2->MR0 = 3800; //Con esto va a interrumpir 38uSeg despues del pulso de sincro horiz
    LPC_TIM2->IR |= 1; // Borro cualquier Int del timer pendiente
    LPC_TIM2->TCR |= 2; // Pongo a 0 el Contador del Timer
    LPC_TIM2->MCR |= 3; // Interrupcion en Match R0 y resetea el Contador cuando hay match
    LPC_TIM2->TCR &= ~2; // Des-reseteo el Contador del Timer
}

/*****************************************************************************
** Function name: EINTInit
**
** Descriptions: Initialize external interrupt pin and
** install interrupt handler
**
** parameters: None
** Returned value: true or false, return false if the interrupt
** handler can't be installed to the VIC table.
**
*****************************************************************************/
void EINTInit( void )
{
LPC_PINCON->PINSEL4 &= ~(0xF << 20); // Borro los bits 20-23
LPC_PINCON->PINSEL4 |= ((1 << 20) | (1 << 22)); // Elijo 01 en P2.10 y P2.11


//  LPC_GPIOINT->IO2IntEnF = 0x200; /* Port2.10 is falling edge. */
LPC_SC->EXTMODE = 3; // INT0 e INT1 se disparan por Flanco
LPC_SC->EXTPOLAR = 0; // INT0 e INT1 se disparan por Falling Edge

NVIC_EnableIRQ(EINT0_IRQn); //Habilito la interrupcion EINT0
NVIC_EnableIRQ(EINT1_IRQn); //Habilito la interrupcion EINT1
}

#endif /* end __EXTINT_H */
/******************************************************************************
**                            End Of File
******************************************************************************/

Timer2.c

Código: [Seleccionar]
/*
 * Timer2.c
 *
 *  Created on: 28/01/2014
 *      Author: elgarbe
 */

#include "Timer2.h"

extern unsigned short line; // Línea (fila) actual en el monitor

/****************************************************************************/
/*                             Public Functions                             */
/****************************************************************************/
// This functions initializes TIMER2.
//   Assumes system clock is 100 MHz.
void tim2_init(uint8_t irq_priority) {

    // give power to TIMER2
    LPC_SC->PCONP |= (1 << 22);

    // set peripheral clock selection for timer 2
    LPC_SC->PCLKSEL1 &= ~(3 << 12); //Borramos bits
    LPC_SC->PCLKSEL1 |=  (1 << 12); // set to "01" = Core Clock (100 MHz) 10nseg Tick

    // set the clock source for TIMER2 to PCLK
    LPC_TIM2->CTCR = 0; // this is the default

    // set the prescaler
    LPC_TIM2->PR = 0; // TC increments on every PCLK

    LPC_TIM2->MR0 = ~0; //Para que en un inicio no Interrumpa
    LPC_TIM2->MCR |= 3; //Interrupcion en Match R 0 y resetea el Contador

    // enable and reset the the counters
    LPC_TIM2->TCR = 3;
    LPC_TIM2->TCR = 1;


    // Registramos la interrupcion del Timer2
    NVIC_EnableIRQ(TIMER2_IRQn);

//     Fijamos la prioridad de la interrupcion
    NVIC_SetPriority(TIMER2_IRQn, irq_priority); // '0' es la mayor
}

/****************************************************************************/
/*                           Interrupt Functions                            */
/****************************************************************************/
void TIMER2_IRQHandler(void) {

    if(LPC_TIM2->IR & 0x1){ // Verificamos que la Int es por Match Register 0
        LPC_TIM2->MCR &= ~3; // Deshabilito interrupcion del MR0 del timer 2

    LPC_SSP0->DR = 0b10011001; // Dato a sacar por el SPI y a mostrar en la pantalla
    while ( LPC_SSP0->SR & (1 << 4) ); //Espero a que salga el dato completo
    }
   
    LPC_TIM2->IR |= 0x1; // clear the interrupt
}

mi main.c

Código: [Seleccionar]
#ifdef __USE_CMSIS
#include "LPC17xx.h"
#endif

unsigned short line; // Línea (fila) actual en el monitor

unsigned char const head[14] = {'E','L','G','A','R','B','E'};

/* ' '  A   B   C   D   E   F   G   H   I   J   K   L   M   N   O   P   Q   R   S   T   U   V   W   X   Y   Z  */
unsigned char const ltrs[189] =
{ 255,231,131,195,135,129,129,195,189,131,129,189,191,125,125,195,131,195,131,195,131,189,189,125,125,125,  1,
255,219,189,189,187,191,191,189,189,239,247,187,191, 57, 61,189,189,189,189,189,239,189,189,125,187,187,251,
255,189,189,191,189,191,191,191,189,239,247,183,191, 85, 93,189,189,189,189,191,239,189,189,109,215,215,247,
255,189,131,191,189,131,131,177,129,239,247,143,191,109,109,189,131,189,131,195,239,189,219,109,239,239,239,
255,129,189,191,189,191,191,189,189,239,247,183,191,125,117,189,191,179,183,253,239,189,219,109,215,239,223,
255,189,189,189,187,191,191,189,189,239,183,187,129,125,121,189,191,185,187,189,239,189,219,147,187,239,191,
255,189,131,195,135,129,191,195,189,131,207,189,129,125,125,195,191,205,189,195,239,195,231,147,125,239,  1};

/* -   .   /   0   1   2   3   4   5   6   7   8   9   :  */
unsigned char const nums[98] =
{ 255,255,207,131,239,131,131,227,  1,131,  1,131,131,255,
255,255,187,125,207,125,125,219,127,127,253,125,125,239,
255,255,187,125,239,253,253,187,127,127,251,125,125,255,
131,255,207,125,239,243,227,123,131,  3,247,131,129,255,
255,255,255,125,239,207,253,  1,253,125,239,125,251,255,
255,231,255,125,239,191,125,251,125,125,239,125,247,239,
255,231,255,131,199,  1,131,251,131,131,239,131,207,255};

#include <cr_section_macros.h>
#include <NXP/crp.h>
#include "extint.h"
#include "Timer2.h"
#include "ssp.h"

/*******************************************************************************
**   Main Function  main()
*******************************************************************************/
int main (void){

tim2_init(2); // Inicializamos el Tmr2 y le establecemos prioridad 2
EINTInit(); // initialize GPIO pins as external interrupts
SSP0Init(); // Inicializamos el bus SPI


while ( 1 ){
}
}

Bien, como ven, el main lo único que hace es inicializar los periféricos, todo sucde dentro de la interrupcion.

En teoría, el resultado debería ser que en todos los campos de la imagen, 38useg despues del pulso de sincro horizontal debería aparecer nuestro dato.

Veamos que obtengo primero con el analizador lógico:



Aquí tenemos dos pulsos de sincronismo horizontal en un canal. No hay informacion de imagen porque estoy mostrando la señal de salida del LM1881. Como ven el tiempo entre el falling edge del pulso y el comienzo del dato es de 40.83useg y no 38 como tengo configrado. Entiendo que esto se debe a pequeños retrasos que se van produciendo entre el momento en que configuro el timer y llega la instruccion de sacar el dato por el SPI.

Veamos ahora que es lo que salió por el bus SPI:



Aquí tenemos el dato que parece ser 1 0 0 1 1 0 0 1 y coincide con el que tengo en el programa. El ancho de pulso es de 166nseg y la frecuencia del bus está en 6MHz. Aquí tengo una pequeña diferencia tambien con la configuracion.  Para el bus SPI tengo 3 registros que determinan la velocidad. Primero la fuente de reloj del bus, la cual tengo configurada a CoreClock (100MHz), luego un prescaler, el cual tengo en 2 (el mínimo) y un divisor, el cual lo tengo en 7 (se le suma uno internamente). Por lo que debería tener una frecuencia de 100MHz / (2 * (7+1) = 6.25MHz con un periodo de 160nseg.

Bueno, veamos ahora las mismas señales con el osciloscopio:

Primero veamos los pulsos de sincronismo horizontal con el dato en el medio.



como vemos tenemos los 64uSeg entre pulsos. No me di cuenta de medir el tiempo en el que aparece el dato del SPI, pero parece ser 40useg mas o menos.

Veamos ahora la informacion del dato del SPI:



Saqué la señal de sincronismo horizontal y puse el CLK del SPI para poder medir bien la frecuncia.... Esta perfecta, 6.25MHz!!!

La gran pregunta es esa forma "fea" de la señal de datos del SPI es normal, verdad? se debe a parásitas que se generan a esa frecuancia? no creo que sea necesario acomodar dicha señal, ya que debería entrar el video compuesto adaptando impedancias y nada más...

Bueno, ya solo queda mezclar las señales y ver que aparece en el monitor!!! espero poder hacer algo a la noche....

Saludos!
-
Leonardo Garberoglio

Desconectado elgarbe

  • Moderador Local
  • PIC24H
  • *****
  • Mensajes: 2178
Re: Diseño de un OSD
« Respuesta #11 en: 20 de Febrero de 2014, 19:20:16 »
Bueno, llegó la hora de la verdad!!!! ya mucho software mucha teoría, pero de conectar el uC con el video nada????

Bue, conectemos y veamos que pasa....

Bien, habíamos conseguido generar un "dato" en sincronismo con el barrido horizontal, aproximadamente 40useg despues del pulso de sincronismo... esto es mas o menos en el centro de la pantalla. El dato que sacamos es 1 0 0 1 1 0 0 1. Si el 1 representa el blanco y el 0 representa el negro, y si ponemos ese dato en todas las lineas deberiamos ver.... mmmm.... 4 líneas blancas verticales????

Bueno, para conectar la señal del SPI al video compuesto simplemente haremo un acople con un diodo y una resistencia.
Si la resistencia de la señal de video es 75ohm, el color blanco es 1V y nuestro micro funciona con 3.3V podemos hacer un divisor resistivo entre nuestra resistencia y la interna del video. Por ejemplo, si ponemos una R de 100ohm obtendremos:
(3.3V - 0.7V) * 75 ohm / (75ohm + 100ohm) = 0.98V lo cual es casi el color blanco.
Si ponemos una R de 150ohm obtendremos una tension de 0.76V, que sería un gris tirando a blanco. Probemos con esa R...

a ver:



muy bien!!!   :-/ :-/ nuestro micro ya "habla" !!!! o por lo menos balbucea!!!!

Como se ve tengo una linea vetical, luego 2 espacios negros, 2 lineas pegadas blancas, dos espacios negros mas y una linea blanca al final.... es lo que esperábamos!!!!

Próximo paso: escribir texto!

Saludos!
-
Leonardo Garberoglio

Desconectado rivale

  • Colaborador
  • PIC24H
  • *****
  • Mensajes: 1707
Re: Diseño de un OSD
« Respuesta #12 en: 20 de Febrero de 2014, 19:38:08 »
 ((:-)) ((:-)) ((:-))

felicidades elgarbe, estoy al tanto del hilo, pero tenía que comentar algo despues de ver que "Esta vivo!!" :mrgreen:

 (((:-))) (((:-))) (((:-)))
« Última modificación: 20 de Febrero de 2014, 19:51:27 por rivale »
"Nada es imposible, no si puedes imaginarlo"

Desconectado elgarbe

  • Moderador Local
  • PIC24H
  • *****
  • Mensajes: 2178
Re: Diseño de un OSD
« Respuesta #13 en: 20 de Febrero de 2014, 19:51:31 »
Bueno, no me aguante, aprovecho mi hijo salio con mi mujer a hacer los mandados y le meto media horita más...

Bien, para mostrar un texto nos falta 2 cosas.
Primero distinguir en que línea quiero el texto. Luego definir la forma de automatizar la extraccion de texto de alguna tabla.

Veamos las modificaciones para tomar una línea determinada para nuestro texto:

Modificamos la ISR de la int ext 1 que lee los pulsos de sincronismo horizontal como sigue:

Código: [Seleccionar]
/*****************************************************************************
** Function name: EINT1_Handler
**
** Descriptions: external INT handler
**
** Interrupcion en el Pulso de Sincronismo Horizontal
**
*****************************************************************************/
void EINT1_IRQHandler (void) {
LPC_SC->EXTINT |= 2; // clear interrupt
//Aqui debo verificar si estoy en alguna línea que me interese
//y debo configurar el TIMER para que me interrumpa en el tiempo correspondiente
//a la columna donde quiero que valla el texto
line++; //Sumo 1 al contador de líneas
if (line>40 || line <48){
   LPC_TIM2->MR0 = 2000; //Con esto va a interrumpir a los 20useg
   LPC_TIM2->IR |= 1; // Borro cualquier Int del timer pendiente
   LPC_TIM2->TCR |= 2; // Pongo a 0 el Contador del Timer
   LPC_TIM2->MCR |= 3; // Interrupcion en Match R0 y resetea el Contador cuando hay match
   LPC_TIM2->TCR &= ~2; // Des-reseteo el Contador del Timer

}
}

De esta forma, activo el timer2 y lo configuro para que me interrumpa a los 20useg, solo en las líneas 41 a 47. Recordar que los caracteres tienen 7 pixeles de alto.
Ahora veamos nuestra tabla de caracteres y mensaje:

Código: [Seleccionar]
unsigned char const head[7] = {'E','L','G','A','R','B','E'};

/* ' '  A   B   C   D   E   F   G   H   I   J   K   L   M   N   O   P   Q   R   S   T   U   V   W   X   Y   Z  */
unsigned char const ltrs[189] =
{ 255,231,131,195,135,129,129,195,189,131,129,189,191,125,125,195,131,195,131,195,131,189,189,125,125,125,  1,
255,219,189,189,187,191,191,189,189,239,247,187,191, 57, 61,189,189,189,189,189,239,189,189,125,187,187,251,
255,189,189,191,189,191,191,191,189,239,247,183,191, 85, 93,189,189,189,189,191,239,189,189,109,215,215,247,
255,189,131,191,189,131,131,177,129,239,247,143,191,109,109,189,131,189,131,195,239,189,219,109,239,239,239,
255,129,189,191,189,191,191,189,189,239,247,183,191,125,117,189,191,179,183,253,239,189,219,109,215,239,223,
255,189,189,189,187,191,191,189,189,239,183,187,129,125,121,189,191,185,187,189,239,189,219,147,187,239,191,
255,189,131,195,135,129,191,195,189,131,207,189,129,125,125,195,191,205,189,195,239,195,231,147,125,239,  1};

La tabla de caracteres ya la habíamos visto, agrego ahora la del mensaje de cabecera "ELGARBE" en el array head. Recuerden que gran parte de este código lo estoy adaptando de la siguiente página: http://garydion.com/projects/videoverlay/ acá yo no inventé nada!

veamos entonces la interrupcion del timer 2:

Código: [Seleccionar]
/****************************************************************************/
/*                           Interrupt Functions                            */
/****************************************************************************/
void TIMER2_IRQHandler(void) {

    if(LPC_TIM2->IR & 0x1){ // Verificamos que la Int es por Match Register 0
        LPC_TIM2->MCR &= ~3; // Deshabilito interrupcion del MR0 del timer 2

     if ((line > 40) && (line < 48)){
     ltemp = (line - 41) * 27 - 64;
//     DDRD  = 0x80; /* Data direction register for port D */
     LPC_SSP0->DR = ~ltrs[head[0] + ltemp]; Wait();
     LPC_SSP0->DR = ~ltrs[head[1] + ltemp]; Wait();
     LPC_SSP0->DR = ~ltrs[head[2] + ltemp]; Wait();
     LPC_SSP0->DR = ~ltrs[head[3] + ltemp]; Wait();
     LPC_SSP0->DR = ~ltrs[head[4] + ltemp]; Wait();
     LPC_SSP0->DR = ~ltrs[head[5] + ltemp]; Wait();
     LPC_SSP0->DR = ~ltrs[head[6] + ltemp]; Wait();
//     DDRD  = 0x00; /* Data direction register for port D */
     }
    }

    LPC_TIM2->IR |= 0x1; // clear the interrupt
}

Bueno, cuando reconocemos la línea 41 a 47 debemos sacar info por el SPI. Primero hay una pequeña cuenta de un indice necesario para tomar línea por línea los caracteres a sacar.
Veamos, en la línea 41 la variable ltemp tomará el valor -64. Entonces los 8 bits que enviamos primero son ltrs[head[0] - 64]. head[0] es la E, cuyo valor en char es 69, al restarle 64 nos queda como dato ltrs[5]. Vamos a la tabla de letras y tenemos que el dato es 10000001. Ese dato representa la primer fila de la letra E. En el proyecto original el 1 representa el negro y el 0 el blanco, a la inversa al mío, por lo que en mi caso debo invertir ese dato (operador ~).
Luego Wait esta definida como
Código: [Seleccionar]
#define Wait() while ( LPC_SSP0->SR & (1 << 4) ); //Espero a que salga el dato completo

Con lo cual se espera a que el dato salga completo antes de volver a cargar el Data Register del SPI. Así sacamos línea por línea cada parte de la letra... una solucion muy bien pensada, no?

Bueno, ahora si, suenen las trompetas.... el resultado:



 :-/ :-/

Bueno, esto ya funciona.... ahora a avanzar con el hardware, optimizar el código y dejarlo listo para la siguiente parte...

Saludos!
-
Leonardo Garberoglio

Desconectado elgarbe

  • Moderador Local
  • PIC24H
  • *****
  • Mensajes: 2178
Re: Diseño de un OSD
« Respuesta #14 en: 20 de Febrero de 2014, 19:53:20 »
((:-)) ((:-)) ((:-))

felicidades elgarbe, estoy al tanto del hilo, pero tenía que comentar algo despues de ver que "Esta vivo!!" :mrgreen:

 (((:-))) (((:-))) (((:-)))

Gracias!!! la verdad que es muy emocionante ver resultados despues de 15 días de puro estudio en los ratos libres!!!!

esto lo necesito medio urgente para poder mostrar datos del GPS en mi planeador FPV!!!!

Saludos!
-
Leonardo Garberoglio