Scope
Contents |
Osciloscopio Básico
En esta sección realizaremos un ejemplo básico haciendo uso del conversor Análogo-Digital y el LCD del que dispone la plataforma para mostrar señales. Comenzaremos por describir el funcionamiento del ADC.
Usando el ADC
SIE utiliza el conversor Análogo-Digital TLV1548; La siguiente Figura muestra el diagrama de tiempos de este dispositivo, en el que basaremos el diseño de nuestro periférico.
La interface en modo "microprocesador" con el ADC funciona de la siguiente manera: en cada flanco de bajada de CS se resetean todos los contadores e inicializa la máquina de estados. Luego durante los primeros 4 flancos de I/O CLK el ADC recibe los datos en DI comenzando por el bit más significativo, esto quiere decir que durante los seis flancos restantes no recibe información. En el décimo flanco de subida de I/O CLK la señal EOC se pone en bajo durante el tiempo de conversión (10us o 40us) y se pone en alto cuando esta halla terminado. El MSB del valor de la última conversión es puesto en DO en el flanco de bajada de CS, así recibiremos los diez bits de cada conversión durante los primeros diez flancos de subida del reloj. En resumen, mientras estamos enviando un comando al ADC también estamos recibiendo los valores de muestreo.
Para poder comenzar a utilizar el conversor debemos inicializarlo, para ello le enviamos el comando 0x09, que lo configura en modo de conversión rápida. A continuación mostramos la lista completa de comandos que recibe el ADC:
| Función | Valor Hexadecimal | Comentario |
|---|---|---|
| Canal A0 seleccionado | 0h | |
| Canal A1 seleccionado | 1h | |
| Canal A2 seleccionado | 2h | |
| Canal A3 seleccionado | 3h | |
| Canal A4 seleccionado | 4h | |
| Canal A5 seleccionado | 5h | |
| Canal A6 seleccionado | 6h | |
| Canal A7 seleccionado | 7h | |
| Modo de bajo consumo | 8h | No hay resultados de conversión |
| Modo de conversión rápida | 9h | (~10us) No hay resultados de conversión |
| Modo de conversión lenta | Ah | (~40us) No hay resultados de conversión |
| Modo de auto-prueba 1 | Bh | (Vref) – Vref–)/2 >> Retorna 200h |
| Modo de auto-prueba 2 | Ch | Vref– >> Retorna 000h |
| Modo de auto-prueba 2 | Dh | Vref >> Retorna 3FFh |
ADC como periférico en la FPGA
Basados en lo desarrollado en Xburst-FPGA Interface y en Xburst-FPGA communication example hemos descrito el hardware necesario para usar el ADC, los archivos fuente se pueden encontran en el siguiente enlace.
El archivo ADC.v es el que se encarga de realizar la sincronización de las señales y de decodificar el bus de direcciónes para generar las señales de Chip Select correspondientes y controlar el bus de datos.
Con respecto al ejemplo de comunicación con la FPGA, este archivo no tiene la RAM y en lugar de ello tenemos:
// Peripherals controlwire [3:0] csN;
wire [7:0] rdBus0, rdBus1, rdBus2, rdBus3;
assign csN = buffer_addr[12]? (buffer_addr[11]? 4'b1000:
4'b0100)
: (buffer_addr[11]? 4'b0010:
4'b0001);
assign rdBus = buffer_addr[12]? (buffer_addr[11]? rdBus3:
rdBus2): (buffer_addr[11]? rdBus1:
rdBus0);
Este ejemplo está diseñado para que eventualmente podamos incluir más periféricos adicionales al ADC, por ello se muestran en gris los componentes que podrían existir.
Como vemos, de las trece líneas de dirección que llegan a la FPGA utilizamos las primeras dos como selector de periférico, por lo tanto para este ejemplo podremos conectar hasta cuatro periféricos, cada uno capaz de direccionar hasta 2048 bytes.
El siguiente diagrama muestra el diseño del periférico
Basados en este diagrama describimos el hardware necesario en el archivo ADC_peripheral.v. Ahora explicaremos el funcionamiento de este periférico:
En el diagrama, las señales o buses de color gris son las entradas o salidas del módulo, en ADC.v lo encontraremos instanciado de la siguiente forma
// Peripheral instantiationADC_peripheral P1(.clk(clk),
.reset(reset),
.cs(csN[0]),
.ADC_EOC(ADC_EOC),
.ADC_CS(ADC_CS),
.ADC_CSTART(ADC_CSTART),
.ADC_SCLK(ADC_SCLK),
.ADC_SDIN(ADC_SDIN),
.ADC_SDOUT(ADC_SDOUT),
.addr(buffer_addr[10:0]),
.rdBus(rdBus0),
.wrBus(wrBus),
.we(we));
Mientras que las señales o buses de color negro son señales internas del módulo ADC_peripheral.
Registros de configuración (Memory Map)
Para configurar el periférico tenemos disponibles seis registros:
CLKDIV: Utilizado para generar el reloj para la comunicación con el ADC.
SIZEB: Determina el tamaño del buffer, o más bien, la cantidad de muestras que se tomaran.
CMD_SW: Se utiliza para realizar el cambio de canal análogo de maneta automática en cada lectura del ADC.
CMD_START: Cuando este en alto le indica al control cuando debe iniciar un proceso de lectura o muestreo, cuando el periférico termine de leer la cantidad de datos especificados por SIZEB esta señal se pondrá en bajo automáticamente.
CMD_TYP: Este define el tipo de comando que se realizará, tiene un valor de '1' para tipo SET y '0' para tipo READ, este registro existe debido a que algunos comando no necesariamente implican una lectura de datos.
CMD_ADC: este es el comando que se le envía al ADC, corresponde a los descritos en la tabla mostrada anteriormente.
Esto es de acuerdo con el memory map dispuesto. Además de los registros de configuración tenemos el RX_BUFFER que es donde se almacenarán los datos recibidos desde el ADC, esté consta de una RAM dual-port para que pueda ser escrita y leída desde el microprocesador e internamente.
Bloque REGISTER BANK
El bloque REGISTER BANK se encarga específicamente de controlar la escritura y lectura de los registros de configuración y del buffer de datos en base al mapeo de memoria que se muestra en el diagrama. Éste bloque toma las señales fullB y rstStart que resetean el valor del registro CMD_START cuando el buffer este lleno o cuando el bloque CONTROL termine de enviar un comando al ADC. En el archivo fuente se encuentra definido en dos partes, el control de escritura y el control de lectura, en el primero se realiza la escritura de los registros de configuración y se controla la escritura sobre la RAM dual-port (RX_BUFFER), además de incluir un reset del registro CMD_START, en el segundo se leen los registros de configuración y además se controla la lectura de la RAM, esto para cuando vamos a leer los datos recibidos desde el ADC. El código que describe este bloque es el siguiente:
// REGISTER BANK: Write controlalways @(negedge clk)
beginif(reset)
{CMD_START, CMD_TYP,CMD_ADC,SIZEB,we1} <= 0;
else if(we & cs) begin
case (addr)
0: begin CLKDIV[7:0] <= wrBus; end
1: begin SIZEB[7:0] <= wrBus; end
2: begin SIZEB[9:8] <= wrBus[1:0]; end
3: begin CMD_SW[1:0] <= wrBus[7:6];
CMD_START <= wrBus[5];
CMD_TYP <= wrBus[4];
CMD_ADC[3:0] <= wrBus[3:0]; end
default: begin we1 <= 1; end
endcaseendelse begin
we1 <= 0; end
if(fullB | rstStart) CMD_START <= 0;
end// REGISTER BANK: Read controlalways @(posedge clk)
if(reset)
{rdBus} <= 0;
else begin
case (addr)
0: begin rdBus <= CLKDIV; end
1: begin rdBus <= SIZEB[7:0]; end
2: begin rdBus <= SIZEB[9:8]; end
3: begin rdBus <= {CMD_SW,CMD_START,CMD_TYP,CMD_ADC};end
default: begin rdBus <= rdBus1; end
endcaseend
Bloque CONTROL
Para establecer el estado de la señal ADC_CS, realizar el control de la comunicación SPI y la recepción de datos tenemos el bloque CONTROL. Éste iniciará su proceso cuando se ponga en alto CMD_START, luego pone en alto a SPI_wr para comenzar una nueva transmisión de datos, cuando la transmición finaliza (determinado por los valores que tomen busy y ADC_EOC) la máquina de estados indicará que se debe guardar un dato del buffer de recepción colocando en alto las señales initB y loadB en la secuencia adecuada para cuando el comando transmitido es de tipo READ, en caso contrario, si se trataba de un comando de tipo SET simplemente no indica el almacenamiento de datos y en lugar de ello pone en alto la señal rstStart durante un ciclo de reloj para que CMD_START se ponga en bajo. El comportamiento puede quedar más claro si analizamos el siguiente diagrama de flujo.
Luego la descripción de hardware del bloque CONTROL es:
// CONTROLalways @(posedge clk)
if(reset) begin
{w_st0, SPI_wr, loadB, initB} <= 0;
ADC_CS <=1;
endelse begin
case (w_st0)
0: begin
rstStart <= 0;
if(CMD_START) begin
ADC_CS <=0;
SPI_wr <= 1;
w_st0 <=1;
endend1: begin SPI_wr <= 0; w_st0 <=2; end
2: begin
if(!busy & ADC_EOC) begin
ADC_CS <=1;
if(CMD_TYP) begin
rstStart <= 1;
w_st0<= 0;
endelse begin
initB<=1;
w_st0<= 3;
endendend3: begin loadB <= 1; w_st0<= 4; end
4: begin loadB <= 0; initB<=0; w_st0<= 0; end
endcaseend
Bloques de comunicación SPI
Cuando la señal SPI_wr se pone en alto el bloque SPI CTRL comienza a funcionar activando la señal load_in para cargar el registro CMD_ADCt como dato de transmisión en TRANSMITTER, luego pone en alto la señal clkdiv_en para poner en funcionamiento el bloque CLOCK GENERATOR que genera el reloj para el ADC y los pulsos para los registros de corrimiento RECEPTOR y TRANSMITER, estos registros se activan dependiendo del valor de la señal fallingSCLK, los datos de transmisión se ponen en ADC_SDOUT en los flancos de subida (fallinSCLK='0') y los datos de recepción puestos por el ADC en ADC_SDIN se almacenan en los flancos de bajada (fallingSCLK='1'). La señal fallingSCLK es generada con un contador de pulsos en el SPI CTRL, quien detiene la transmisión cuando el contador alcanza un valor determinado. El diagrama de flujo es el sigueinte.
Además la descripción de hardware de este bloque es:
// SPI Controlalways @(posedge clk)
if(reset) begin
{w_st1, pulsecount, clkdiv_en, busy} <= 0;
end else begin
case (w_st1)
0: begin
if(SPI_wr) begin
clkdiv_en <= 1;
load_in <= 1;
w_st1 <= 1; busy <= 1;
endend1: begin
load_in <= 0;
if(pulse)
pulsecount <= pulsecount + 1;
else if (pulsecount > 55) begin
clkdiv_en <= 0; busy <= 0;
w_st1 <= 0; pulsecount <= 0;
endendendcaseend
Como deciamos anteriormente el bloque CLOCK GENERATOR se activa con la señal clkdiv_en y es quien genera el reloj para el ADC (ADC_SCLK), es decir, el reloj de transmisión y su velocidad depende del valor del registro CLKDIV. La descripción de hardware es sencilla:
// SPI Clock Generatoralways@(posedge clk)
if (clkdiv_en) begin
if(clkcount < CLKDIV) begin
clkcount <= clkcount + 1; pulse <=0;
end else begin
clkcount <= 0; pulse <=1;
if((pulsecount>0) && (pulsecount < 21))
ADC_SCLK_buffer <= ~ADC_SCLK_buffer;
endend else begin
ADC_SCLK_buffer <= 0; pulse <=0;
end
Los otros dos componentes relacionados a la comunicación con el ADC son el RECEPTOR y el TRANSMITTER, que son dos registros de corrimiento hacia la izquierda activados por la señal fallingSCLK y la señal pulse. La descripción también es sencilla:
// SPI Receptoralways@(posedge clk)
beginif((fallingSCLK & pulse) && (pulsecount < 21)) begin
out_buffer <= out_buffer << 1;
out_buffer[0] <= ADC_SDOUT;
endend// SPI Transmitteralways@(posedge clk)
beginif(load_in) in_buffer <= CMD_ADCt[3:0];
if(!fallingSCLK & pulse) begin
ADC_SDIN_buffer <= in_buffer[3];
in_buffer <= in_buffer << 1;
endend
Bloque RECEPTION BUFFER
El bloque RECEPTION BUFFER es el encargado de almacenar los datos recibidos del ADC (salida out_buffer de registro de corrimiento RECEPTOR) en la memoria RAM para que puedan ser leidos desde el procesador accediendo al periférico en la FPGA. Cuando el buffer de datos esta lleno se pone en lato la señal fullB que puede ser utilizada como IRQ y que en este caso también resetea el valor de CMD_START. El buffer tiene un tamaño máximo de 1024 datos, usando dos bytes contiguos para almacenar un valor de muestra recibido desde el ADC en una RAM dual-port de 2K 8bit, aunque los primeros dos datos (cuatro primeras posiciones de la RAM) son inaccesibles, aunque esto no implica problema debido a que al usar el ADC a altas frecuencias debemos desprecias una cierta cantidad de datos iniciales inválidos. El diagrama de flujo para el RECEPTION BUFFER se muestra a continuación.
Y la descripción de hardware es la siguiente:
// Reception Bufferalways @(posedge clk)
if(reset)
{we2, w_st2, fullB, SIZEB1, SIZEB2} <= 0;
else begin
case (w_st2)
0: begin
fullB <= 0;
if(initB) begin
w_st2 <= 1;
SIZEB1<=SIZEB;
SIZEB2<=SIZEB;
endend1: begin
if(loadB) begin
// If buffer full set fullB flag by a clock cicleif(SIZEB2>0) begin
w_st2 <= 2; end
else begin
fullB <= 1;
w_st2 <= 0;
endendend2: begin
//Write data on BRAM (LOW)wrBus2[7:0] <= out_buffer[7:0];
addr2 <= {subSIZEB,1'b0};
we2 <= 1; w_st2 <= 3;
end3: begin we2 <= 0; w_st2 <= 4; end
4: begin
//Write data on BRAM (HI)wrBus2[7:0] <= out_buffer[9:8];
addr2 <= {subSIZEB,1'b1};
we2 <= 1; w_st2 <= 5;
end5: begin
we2 <= 0; w_st2 <= 1; SIZEB2 <= SIZEB2-1;
endendcaseendassign subSIZEB = SIZEB1-SIZEB2;
Modo muestreo alternado de canales
En la mayoría de los casos necesitamos realizar muestreos de varios canales de forma simultánea, aunque esto no sea posible, si podremos realizar un cambio de canal de forma rápida después de cada conversión del ADC. Para implementar esta función hemos dispuesto un contador MOD8 aumenta cada vez que almacenamos un dato en el buffer y se resetea cada vez que el buffer se llena, el valor de este contador es sumado al comando que se envia al ADC (CMD_ADC), de esta forma después de cada conversión del ADC estamos leyendo una muestra de un canal diferente, comenzando por el canal A0 hasta el A7. Pero no siempre querremos tomar muestras de los ocho canales de forma alterna además de que con esto reducimos drásticamente la velocidad de muestreo por canal. Para ello disponemos de la señal CMD_SW que utilizamos para multiplexar el valor que se le suma al comando ADc. Cuando CMD_SW='00' sólo se toman muestras del canal determinado por CMD_ADC, cuando CMD_SW='01' se alternan dos canales tomando como base el canal determinado por CMD_ADC, es decir, si CMD_ADC determina la selección del canal A6 (CMD_ADC=6h) los datos que se guarden el buffer corresponderan a los canales A6 y A7 alternados. Cuando CMD_SW='10' se alternan cuatro canales y cuando CMD_SW='11' se alternan ocho canales. Esta función debe ser usada con cuidado, debido a que al alternar no se discrimina entre un comando para establecer un canal o un comando para configurar el ADC, es decir, se debe asegurar que CMD_ADCt es menor a 7h cuando el modo de canales alternados esta activo para evitar un comportamiento indeseable. La descripción de hardware para implementear esta función es la siguiente.
// ADC channel offset, MOD8 counteralways @(posedge clk)
if(fullB | reset)
CMD_OFFSET <= 0;
else if(loadB) begin
CMD_OFFSET <= CMD_OFFSET + 1;
end// MUX to select the channel offsetassign CMD_OFFSETt = CMD_SW[1]? (CMD_SW[0]? CMD_OFFSET[2:0] :
CMD_OFFSET[1:0] )
: (CMD_SW[0]? CMD_OFFSET[0] :
3'b0 );
// Add ADC command and offsetassign CMD_ADCt = CMD_ADC + CMD_OFFSETt;
Usando el periférico ADC y QT
Para utilizar el periférico desarrollado decidimos realizar un pequeño ejemplo usando QT para mostrar las señales desde dos de los ocho canales analógicos del ADC. El código fuente completo de éste ejemplo se encuentra en el siguiente enlace.
La GUI de la aplicación es muy básica, se compone de un Main Window y dentro de él un widget llamado Signal Display que simplemente dibuja un vector de datos en el área especificada haciendo uso de la clase QPainter.
La funciones básicas del widget Signal Display:
- Agregar valores a los vetores, addPoint1() y addPoint2(), para cambia la señal que se esta mostrando. En este caso los vectores se comportan como registros de corrimiento.
- Establecer los segundos por división horizontal, setSecsPerDiv().
- Establecer los voltios por división vertical, setVoltsPerDiv().
- Establecer la longitud de los vectores, setPointsPerPlot().
- Establecer el color de los trazos, setColorTrace1() y setColorTrace2().
Primero realizamos una pequeña prueba de la aplicación en el PC colocando en los vectores señales fijas, en este caso dos señales sinusiodales de frecuencia diferente.
Luego escribimos una clase sencilla llamada ADCw que se encarga de controlar el periférico en la FPGA haciendo uso de los códigos escritos para Xbusrst-FPGA comunication example. El constructor de esta clase se encarga de realizar la configuración adecuada del GPIO correspondiente al CS2 y de realizar el mapeo de la memoria física para poder direccionar la región de memoria correspondiente a CS2, además inicializa el ADC en modo de conversión rápida. Las funciones públicas que hemos dispuesto para esta clase son las siguientes:
- Realizar prueba del ADC, testADC(), que configura al periférico para enviar los comandos correspondientes al ADC para colocarlo en modo auto-selft (ver tabla de comados) y luego leer los datos para corroborar que son correctos y que el ADC está realizando su trabajo.
- Colocar el ADC en modo de bajo consumo, powerDownADC(), que configura al periférico para enviar el comando correspondiente para colocarlo en modo Power Down.
- Tomar muestras de determinado canal del ADC, takeSamples(int CHANNEL), que configura el periférico para que tome muestras del canal especificado o canal base si se esta utilizando el modo de canales alternados.
- Establecer el valor del divisor de reloj, setClockDiv(uchar value), que es la variable que se le envía al periférico para el registro de configuración CLKDIV.
- Establecer la longitud del buffer, setBufferLen(int value), que es la variable que se le envía al periférico para el registro de configuración SIZEB.
- Establecer el modo de canales alternados, setMuxChannels(uchar value), que es la variable que se le envía al periférico para el registro de configuración CMD_SW.
Ahora hacemos uso de estas clases para generar nuestra aplicación, el diagrama de flujo general del inicio es muy sencillo y se muestra a continuación.
Luego el hilo principal queda a la espera de que la clase del timer indique un time out. El diagrama de flujo también es sencillo como se muestra a continuación.
Para dejar más claro el funcionamiento del proyecto en QT mostramos el siguiente diagrama de secuencia que relaciona las clases creadas.
en el diagrama trazamos 120 puntos por cada señal, pero configuramos el periférico ADC de tal manera que se tomen 240 muestras alternando dos canales siendo el canal base A0, es decir, el periférico llenará el buffer con 240 muestras alternadas del canal A0 y el canal A1, esto se realizará cada 50ms.
Compilando aplicaciones en QT para Openwrt-xburst
Primero debemos asegurarnos de que tenemos compilado QT, para ello podemos seguir las instrucciones Building Software Image, específicamente debemos obtener los "feeds", luego ir al menú de configuración de openwrt-xburst y en las opciones Xorg seleccionar el framework de QT, y entonces compilar de nuevo con make.
En el directorio /DIRECTORIO/HASTA/OPENWRT/openwrt-xburst/build_dir/target-mipsel_uClibc-0.9.30.1/qt-everywhere-opensource-src-4.6.2/ podremos encontrar todas las librerías y archivos necesarios para compilar nuestras aplicaciones en QT.
Para compilar aplicaciones lo primero que debemos hacer es editar el archivo /DIRECTORIO/HASTA/OPENWRT/openwrt-xburst/build_dir/target-mipsel_uClibc-0.9.30.1/qt-everywhere-opensource-src-4.6.2/mkspecs/qws/linux-openwrt-g++/qmake.conf, agregando las siguientes líneas:
QMAKE_INCDIR_QT = /DIRECTORIO/HASTA/QT/qt-everywhere-opensource-src-4.6.2/include QMAKE_LIBDIR_QT = /DIRECTORIO/HASTA/QT/qt-everywhere-opensource-src-4.6.2/lib
QMAKE_MOC = /DIRECTORIO/HASTA/QT/qt-everywhere-opensource-src-4.6.2/bin/moc QMAKE_UIC = /DIRECTORIO/HASTA/QT/qt-everywhere-opensource-src-4.6.2/bin/uic
Luego podremos ir a nuestro directorio de trabajo y generamos el archivo "Makefile" necesario de la siguiente forma:
$ make clean $ /DIRECTORIO/HASTA/QT/qt-everywhere-opensource-src-4.6.2/bin/qmake -spec \ /DIRECTORIO/HASTA/QT/qt-everywhere-opensource-src-4.6.2/mkspecs/qws/linux-openwrt-g++ -unix -o Makefile
Finalmente obtendremos nuestra aplicación compilada para nuestra plataforma ejecutando en consola:
$ make



