Hola a todos,

Hoy os traemos un proyecto para la Nexys4 de Digilent. Esta placa ya la hemos utilizado en otros tutoriales. En este tutorial vamos a aprender a manejar el conversor analógico ADC digital incluido en todas las FPGA de la Series 7 de Xilinx, llamado XADC (la X es de Xilinx, muy creativo). Este ejemplo puede servir de base para realizar un osciloscopio con la FPGA, con solo conectarle una salida VGA.

El otro día tenia que capturar unas muestras a la salida de un sensor y mostrarlas, así que me puse manos a la obra. Lo primero fue mirar el ejemplo que da Digilent para la Nexys 4, os dejo el enlace. Pero ya os digo que no me sirvió para nada. Así que vamos a montar el ejemplo completo, desde cero.

Lo que buscamos es leer una señal con una tasa de muestreo de 100.000 samples/s y exportarla para trabajar con ella en el ordenador, o utilizarla dentro de la FPGA para otras cosas.

Por tanto lo que vamos a hacer es crear un sistema con un Microblaze que va a tener el XADC conectado, lo va a configurar, a leer las muestras y guardarlas en una memoria. Luego desde memoria podéis procesarlas o enviarlas al PC por el puerto serie para sacar gráficas o lo que queráis.

Lo primero por tanto es crear un sistema básico con Microblaze. Para eso tenemos un tutorial sobre Hola mundo con Microblaze.

Una vez creado vamos a conectar nuestro XADC al Microblaze. Para ello vamos a insertarlo en el diagrama de bloques con el botón + y buscaremos el XADC. Una vez insertado vamos a configurarlo.

En la configuración no es necesario cambiar mucho, ya que luego la cambiaremos desde el Microblaze.

Lo mas imporante es asignarle el interface AXI4lite y utilizar la salida auxiliar 3. ¿Por qué la 3?. Pues porque en la Nexys4 el conector PMOD donde están conectadas, tiene la salida Aux3 del XADC conectada a los pines 1 y 2. Eso me llevo un rato descubrirlo.

Cosas que tendremos que tocar después, sera ponerlo en modo continuo, para que capture sin parar. También el Single Channel porque solo vamos a leer de una entrada, si queréis leer de varias tenéis que manejar el Channel Sequencer. Y sobre todo el reloj. El wizard calcula automáticamente el divisor a partir del reloj de entrada y la frecuencia de muestreo que le pongáis. En este caso para 100KS/s es de 39.

Ya tenemos el conversor Analogico Digital. Los pines V_n y V_p que en la placa están conectados a la entrada de audio los ponemos a tierra con una constante. Eso viene indicado en el datasheet del ADC, para reducir el ruido si no se usan. Y haremos click derecho en los pines que vamos a usar (Vaux3) y le daremos a Make external para que los enchufe al conector.

Ahora añadiremos también una la memoria externa de la placa para almacenar muchas muestras en ella. Al conectarla a mí me la asigna a partir de la posición 0x6000000. Acordaros de ese dato.

Le dais al wizard de conexión para que lo enchufe todo y el sistema os tiene que haber quedado así:

Ya podemos crear nuestro fichero de restricciones para asignar los pines de salida. Este es el que he usado yo. En mi caso los pines externos se llaman Vaux1_0_p y Vaux1_0_n. Si los vuestros se llaman distinto cambiadles el nombre.

## Clock signal
##Bank = 35, Pin name = IO_L12P_T1_MRCC_35,					Sch name = CLK100MHZ
set_property PACKAGE_PIN E3 [get_ports sys_clock]							
	set_property IOSTANDARD LVCMOS33 [get_ports sys_clock]
	create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports sys_clock]

##Buttons
##Bank = 15, Pin name = IO_L3P_T0_DQS_AD1P_15,				Sch name = CPU_RESET
set_property PACKAGE_PIN C12 [get_ports reset]				
	set_property IOSTANDARD LVCMOS33 [get_ports reset]


#Pmod Header JXADC
#Bank = 15, Pin name = IO_L9P_T1_DQS_AD3P_15,				Sch name = XADC1_P -> XA1_P
set_property PACKAGE_PIN A13 [get_ports {Vaux1_0_p}]				
	set_property IOSTANDARD LVCMOS33 [get_ports {Vaux1_0_v_p}]
#Bank = 15, Pin name = IO_L8P_T1_AD10P_15,					Sch name = XADC2_P -> XA2_P
#set_property PACKAGE_PIN A15 [get_ports {vauxp10}]				
#	set_property IOSTANDARD LVCMOS33 [get_ports {vauxp10}]
#Bank = 15, Pin name = IO_L7P_T1_AD2P_15,					Sch name = XADC3_P -> XA3_P
#set_property PACKAGE_PIN B16 [get_ports {vauxp2}]				
#	set_property IOSTANDARD LVCMOS33 [get_ports {vauxp2}]
#Bank = 15, Pin name = IO_L10P_T1_AD11P_15,					Sch name = XADC4_P -> XA4_P
#set_property PACKAGE_PIN B18 [get_ports {vauxp11}]				
#	set_property IOSTANDARD LVCMOS33 [get_ports {vauxp11}]
#Bank = 15, Pin name = IO_L9N_T1_DQS_AD3N_15,				Sch name = XADC1_N -> XA1_N
set_property PACKAGE_PIN A14 [get_ports {Vaux1_0_v_n}]				
	set_property IOSTANDARD LVCMOS33 [get_ports {Vaux1_0_v_n}]
#Bank = 15, Pin name = IO_L8N_T1_AD10N_15,					Sch name = XADC2_N -> XA2_N
#set_property PACKAGE_PIN A16 [get_ports {vauxn10}]				
#	set_property IOSTANDARD LVCMOS33 [get_ports {vauxn10}]
#Bank = 15, Pin name = IO_L7N_T1_AD2N_15,					Sch name = XADC3_N -> XA3_N 
#set_property PACKAGE_PIN B17 [get_ports {vauxn2}]				
#	set_property IOSTANDARD LVCMOS33 [get_ports {vauxn2}]
#Bank = 15, Pin name = IO_L10N_T1_AD11N_15,					Sch name = XADC4_N -> XA4_N
#set_property PACKAGE_PIN A18 [get_ports {vauxn11}]				
#	set_property IOSTANDARD LVCMOS33 [get_ports {vauxn11}]

Ahora generamos el bitstream, exportamos el hardware incluyendo el bitstream y lanzamos SDK. Si no sabéis hacerlo os recuerdo que en el tutorial de Microblaze esta explicado en video.

Una vez en SDK creamos el proyecto y este es el código para leer del ADC y mandar las muestras a través del puerto serie. Fijaos que primero las almaceno en memoria y luego las mando. Esto es porque el puerto serie es muy lento y las muestras llegan mas rápido que lo que puedo sacarlas por el puerto serie.

El programa lo que hace es configurar el XADC en single channel, con el divisor a 39 y eliminar todas las alarmas internas que tiene. Las alarmas son para monitorizar la temperatura interna de la FPGA y el estado de la alimentación, pero nosotros no las vamos a usar.

Después espera a que pulséis una tecla y se pone a leer muestras por polling. Cada vez que encuentra una la guarda en la posición 0x60000000 (os acordáis de la memoria externa).

Al terminar vuelca todo al puerto serie para recibirlo en el PC y poder cargarlo en Matlab o Python o donde queráis y hacer gráficas o procesarlo. Si quisierais también tenéis las muestras en la memoria para enviarlas a otro hardware dentro de la FPGA como una FFT o cualquier otra cosa que se os ocurra.

Esto es todo por hoy. Espero que os resulte util. Creo que es un ejemplo claro de cómo hacer una herramienta de captura de datos de forma sencilla con la FPGA y su conversor interno. Esto puede servir de base para realizar un osciloscopio con la FPGA o cualquier otra aplicación.

#include <stdio.h>
#include "xparameters.h"
#include "platform.h"
#include "xsysmon.h"
#include "xil_printf.h"
#include "xstatus.h"
#include "xuartlite.h"


#define UARTLITE_DEVICE_ID	XPAR_UARTLITE_0_DEVICE_ID
#define SYSMON_DEVICE_ID 	XPAR_SYSMON_0_DEVICE_ID
#define UART_BUFFER_SIZE 16

#define NUMBER_OF_SAMPLES 4500

int main()
{
	static XSysMon SysMonInst;      /* System Monitor driver instance */
	unsigned int ReceivedCount = 0;
	unsigned char RecvBuffer[UART_BUFFER_SIZE];
	XUartLite UartLite;

	int Status;
	XSysMon_Config *ConfigPtr;
	XSysMon *SysMonInstPtr = &SysMonInst;
	int *sample;

	//External memory address to store samples
	sample=(int *)0x60000000;

	init_platform();

	Status = XUartLite_Initialize(&UartLite, UARTLITE_DEVICE_ID);

    while(1){
		print("Press any key to begin...\n\r");*/

		ReceivedCount = 0;
		while(ReceivedCount==0){
			ReceivedCount = XUartLite_Recv(&UartLite, (unsigned char *) RecvBuffer, 1);
		}
		//Now we must wait for trigger
		//print("Waiting for trigger...\n\r");

		ConfigPtr = XSysMon_LookupConfig(SYSMON_DEVICE_ID);
		if (ConfigPtr == NULL) {
			return XST_FAILURE;
		}

		XSysMon_CfgInitialize(SysMonInstPtr, ConfigPtr, ConfigPtr->BaseAddress);

		XSysMon_SetAvg(SysMonInstPtr, XSM_AVG_16_SAMPLES);

		XSysMon_SetAdcClkDivisor(SysMonInstPtr, 39);

		XSysMon_SetSequencerMode(SysMonInstPtr, XSM_SEQ_MODE_SINGCHAN);

		XSysMon_SetCalibEnables(SysMonInstPtr,
				    XSM_CFR1_CAL_PS_GAIN_OFFSET_MASK |
					XSM_CFR1_CAL_ADC_GAIN_OFFSET_MASK);

		Status=  XSysMon_SetSingleChParams(SysMonInstPtr, XSM_CH_AUX_MIN+3,
								FALSE, FALSE, TRUE);

		
		/*
		 * Disable all the alarms in the Configuration Register 1.
		 */
		XSysMon_SetAlarmEnables(SysMonInstPtr, 0x0);


		/*
		 * Wait till the End of conversion
		 */
		//print("Capturing\n\r");
		for(int i=0;i<NUMBER_OF_SAMPLES;i++){
			XSysMon_GetStatus(SysMonInstPtr); /* Clear the old status */
			while ((XSysMon_GetStatus(SysMonInstPtr) & XSM_SR_EOC_MASK) !=
				XSM_SR_EOC_MASK);

			*(sample+i) = XSysMon_GetAdcData(SysMonInstPtr, XSM_CH_AUX_MIN+3)

		}

		for(int i=0;i<NUMBER_OF_SAMPLES-1;i++){
			xil_printf("%d,",*(sample+i));
		}
		xil_printf("%d\r\n",*(sample+NUMBER_OF_SAMPLES-1));

		xil_printf("end\n");

    }

    cleanup_platform();
    return 0;
}