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; }
Hola
Existe algún archivo equivalente a “platform.h” para Vivado Design Suite v2018.3?, debido a que para esta versión no existe.
Hola,
El platform.h lo genera solo. Me parece raro que 2018 no lo genere. Has creado un proyecto vacio con la plantilla Hello World?
De todas formas si no lo genera copia el contenido de las funciones dentro del esqueleto que te genera y deberia funcionar
Hola, He seguido el tutorial del microblaze y este a continuación y todo perfecto hasta la implementación de la memoria… En la fase de place me dice:
[Place 30-58] IO placement is infeasible. Number of unplaced terminals (45) is greater than number of available sites (18).
The following are banks with available pins:
IO Group: 0 with : SioStd: LVCMOS18 VCCO = 1.8 Termination: 0 TermDir: Out RangeId: 1 Drv: 12 has only 18 sites available on device, but needs 45 sites.
Term: emc_rtl_addr[0]
Term: emc_rtl_addr[1]
Term: emc_rtl_addr[2]
Term: emc_rtl_addr[3]
Parece relacionado con el fichero de constraints, pero según vuestro ejemplo yo solo he indicado los pines del ADC. Saben cómo puedo solucionar este tema??
Saludos y gracias!
Hola,
Estas usando la misma placa? Esos pines son los del controlador de memoria externa. Parece como que tu BSP no tiene añadidos esos pines. Si usas la misma placa deberia pillarlos.
De todas maneras se puede hacer lo mismo sin la memoria externa. Quitala y debería de funcionar todo igual, solo cambia la direccion de memoria donde almacenas los datos del ADC a una que este en la memoria local del Microblaze
Hola que tal! Muchas gracias por su tutorial, disculpe yo estoy trabajando con una PYNQ Z2, sin embargo tengo la duda de cómo podría reflejar el dato leído por el XADC directo a salidas digitales, por ejemplo un conjunto de LED’s que representen los valores binarios.
La otra pregunta es sobre en cuál terminal del IP Block del XADC es donde el valor convertido?
De antemano muchas gracias por su ayuda!
Hola,
Gracias por tus comentarios.
Lo mas facil es meter el microprocesador, Zynq o Microblaze, leer el dato y escribirlo.
Si quieres hacerlo en VHDL tienes que escribir una maquina de estados que lea el dato del ADC y lo escriba en un registro de salida conectado a los leds. No es complicado.
Gracias por su atenta respuesta, precisamente el día de ayer con los bloques logré leer datos con el bloque XADC, utilicé la interfaz DRP y de ahí tomé el vector “do_out[15:0]”, investigando encontré que los primeros 4 bits son de “relleno” mientras que los 12 bits restantes son los de la resolución del ADC. Y precisamente los mandé a salidas digitales para observar el conteo binario. Sin embargo, al momento de variar la entrada analógica en 0V están encendidos los 5 primeros bits; posteriormente cuando comienzo a cambiar la entrada, los LED’s comienzan a hacer el conteo (los 5 primeros bits siguen encendidos).
¿De casualidad usted sabrá a qué se deba éste comportamiento?
De antemano muchas gracias!
Saludos!!
Seguramente los leds se enciendan con 0 y se apaguen con 1. Es bastante habitual.
Getting error at bitstream generation for EMC (external memory) for IO placement. Kindly help to solve the problem.
Hello,
The reason for that is the configuration of the external memory. Have you applied the board setup to it?
If you still have problems you can remove it and save the samples in the processor RAM
Regards
Hola ! Una consulta. Tengo una PYNQ Z1 y quiero leer el ADC y despues recoger los resultados desde el Python. Para eso, es construir el bitstream como lo mencionan aca y despues leer directamente del Python??? O se necesita algo adicional.
Gracias !!
Hola,
No he usado nunca la Pynq con Python, pero por lo que he leido entiendo que puedes conectar el ADC de manera similar y crear el overlay siguiendo el flujo de Xilinx para ello. Deberia funcionar.
Oki. Entonces, creare esto y ver si puedo leer el overlay desde Python :D. gracias!!