Descripción general
El escenario de uso más habitual de Xillybus es la adquisición de datos (data acquisition). Esta página explica cómo empezar a transmitir datos desde la FPGA hasta el ordenador anfitrión.
Para una visión más detallada de la interacción con Xillybus dentro de la FPGA, consulta la Xillybus FPGA designer's guide.
Si la aplicación tiene una tasa de datos alta, también se recomienda leer las pautas al respecto en el capítulo 5 de la guía de inicio para Linux o de la guía de inicio para Microsoft Windows.
El lado del anfitrión
Para entender cómo funciona Xillybus, lo más fácil es empezar por el ordenador. Este comando puede utilizarse para realizar una adquisición de datos hacia un archivo en el disco:
$ cat /dev/xillybus_read_32 > capture-file.dat
«cat» es un comando estándar de Linux que lee todos los datos de un archivo y los escribe en la salida estándar. En este ejemplo, la entrada no es un archivo normal, sino un archivo de dispositivo (device file). Esa entrada consiste en un flujo de datos que llega desde la FPGA. Hay una redirección de la salida estándar, de modo que esos datos se escriben en un archivo del disco.
No es un ejemplo artificial: en algunos escenarios de uso, esta es la forma correcta de emplear Xillybus en la práctica para adquirir datos. Puede ser mejor usar «dd» para obtener una cantidad concreta de datos. Más a menudo se utiliza un programa informático específico que lee los datos del archivo de dispositivo. Cada aplicación tiene su forma preferida de consumir los datos.
Así pues, da igual qué lenguaje de programación prefieras, o si usas Linux o Windows. El software del ordenador que recibe los datos de la FPGA solo necesita hacer lo mismo que «cat»: abrir un archivo y leer de él. Hay una página aparte que presenta las técnicas de programación estándar para la entrada/salida de archivos. Puedes encontrar aún más información sobre las técnicas de programación con Xillybus en las guías de programación para Linux y para Windows.
La lógica de la adquisición de datos
Ahora veamos qué ocurre en la FPGA. El núcleo IP (IP core) de Xillybus y la lógica de la aplicación interactúan a través de una FIFO: la lógica de la aplicación escribe datos en la FIFO, y Xillybus se encarga de que esos datos lleguen al ordenador anfitrión. Si no estás familiarizado con este concepto, hay una página aparte que explica cómo funcionan las FIFO.
El paquete de demostración (demo bundle) de Xillybus contiene un archivo llamado xillydemo.v. Este es el código Verilog que sirve de interfaz con el núcleo IP. También hay un archivo VHDL en el paquete de demostración: xillydemo.vhd. Sin embargo, el ejemplo siguiente está en Verilog.
La instanciación (instantiation) del núcleo IP de Xillybus se realiza en xillydemo.v (o xillydemo.vhd). Estas son las partes relevantes para el ejemplo de adquisición de datos con el comando «cat» de arriba (las demás se omiten):
// Wires related to /dev/xillybus_read_32
wire user_r_read_32_rden;
wire user_r_read_32_empty;
wire [31:0] user_r_read_32_data;
wire user_r_read_32_eof;
wire user_r_read_32_open;
[ ... ]
xillybus xillybus_ins (
[ ... ]
// Ports related to /dev/xillybus_read_32
// FPGA to CPU signals:
.user_r_read_32_rden(user_r_read_32_rden),
.user_r_read_32_empty(user_r_read_32_empty),
.user_r_read_32_data(user_r_read_32_data),
.user_r_read_32_eof(user_r_read_32_eof),
.user_r_read_32_open(user_r_read_32_open),
[ ... ]
.bus_clk(bus_clk),
[ ... ]
);
El significado de los puertos del núcleo IP se describe con detalle en la guía de Xillybus para la API de lógica.
En xillydemo.v hay una instanciación de una FIFO. Esa FIFO es de reloj único, y por tanto sirve para el bucle de retorno (loopback) que se demuestra en el código Verilog original. Para una aplicación de adquisición de datos es más adecuada una FIFO de doble reloj, porque la lógica de adquisición suele depender de su propio reloj.
Así pues, puedes convertir xillydemo.v en un módulo que realice la adquisición de datos de la siguiente manera: elimina la instanciación de la FIFO de reloj único (se llama fifo_32x512) e inserta esto en su lugar:
assign user_r_read_32_eof = 0;
dualclock_fifo_32 fifo_32
(
.rd_clk(bus_clk),
.rst(!user_r_read_32_open),
.rd_en(user_r_read_32_rden),
.dout(user_r_read_32_data),
.empty(user_r_read_32_empty),
.wr_clk(capture_clk),
.wr_en(capture_en),
.din(capture_data),
.full(capture_full)
);
La FIFO de doble reloj
dualclock_fifo_32 es una FIFO de doble reloj estándar. Hay que crearla como un núcleo IP con el software de desarrollo de la FPGA. La profundidad de esta FIFO debería ser de 512 elementos o más. Debido a las diferencias entre los programas de FPGA, los nombres de los puertos pueden ser distintos de los que se muestran arriba. No obstante, debería ser fácil deducir cómo conectar los puertos de la FIFO.
Una vez más, si aún no estás familiarizado con las FIFO, hay una página que las presenta.
Varios de los puertos de la FIFO se conectan directamente al núcleo IP de Xillybus: @rd_clk, @rd_en, @dout y @empty (vacío). Observa que estas conexiones se hacen exactamente igual que en el paquete de demostración. El núcleo IP utiliza estas cuatro señales para extraer datos de la FIFO. Esta es siempre la forma correcta de conectar estos puertos con el núcleo IP.
Observa que @rd_clk está conectado a @bus_clk. Esta señal procede del núcleo IP de Xillybus. En otras palabras, el núcleo IP impone el reloj que utiliza uno de los lados de la FIFO.
En cuanto a @rst, observa que está conectado a !user_r_read_32_open. @user_r_read_32_open está alta únicamente cuando el archivo de dispositivo correspondiente está abierto en el anfitrión. Como consecuencia, la FIFO se resetea cuando el archivo no está abierto. Por eso la FIFO está vacía cuando el anfitrión abre el archivo de dispositivo: si quedaran restos en el almacenamiento de la FIFO de una sesión anterior, se eliminarían al cerrar el archivo de dispositivo.
Este comportamiento es el que solemos esperar de una fuente de datos. Pero si quieres que la FIFO conserve sus datos cuando el archivo de dispositivo está cerrado, conecta otra cosa a @rst. O simplemente mantén @rst baja.
Observa que @user_r_read_32_eof es cero en este ejemplo, igual que en el paquete de demostración. Esta señal puede utilizarse para enviar una marca de fin de archivo (EOF) al anfitrión. Más sobre esto en la guía de la API.
Interfaz con la lógica de la aplicación
En una aplicación de adquisición de datos hay siempre algún tipo de lógica de aplicación que produce los datos que se van a transmitir al anfitrión. Esta parte cambia de una aplicación a otra y, por tanto, no es relevante para esta discusión. Nos centraremos en cómo enviar esos datos al anfitrión.
Esta parte es sorprendentemente fácil: la lógica de la aplicación escribe los datos en la FIFO. Los datos que se escriben en la FIFO llegan al programa del ordenador anfitrión como un flujo de datos contiguo.
En consecuencia, la lógica de la aplicación utiliza la convención estándar para escribir en una FIFO. En el ejemplo anterior esto se muestra con @capture_clk, @capture_en, @capture_data y @capture_full. Esa lógica solo tiene que poner los datos en @capture_data y controlar @capture_en para escribir correctamente en la FIFO. Observa que la lógica de la aplicación usa su propio reloj para escribir en la FIFO.
El flujo de datos
Este es un diagrama de bloques simplificado que ilustra el flujo de datos desde la lógica de la aplicación hasta el programa de aplicación del usuario en el anfitrión.
Observa que se han omitido dos detalles técnicos de este diagrama de bloques: no se muestran el bloque PCIe ni el controlador del kernel, porque no son relevantes para la percepción que tiene el usuario del flujo de datos. La forma correcta de usar Xillybus es olvidarse de esos detalles y centrarse en la lógica de la aplicación y en el software de aplicación.
No hace falta organizar los datos en paquetes: el canal de comunicación entre la lógica de la aplicación y el ordenador es un flujo continuo. El núcleo IP y el controlador se aseguran de que el flujo de datos se comporte como otros protocolos de flujo, por ejemplo las tuberías (pipes) entre programas en Linux. Otro protocolo con comportamiento similar es TCP/IP. En otras palabras, da igual cuántos datos escribas en la FIFO: esos datos llegarán pronto al programa de usuario del anfitrión.
Es un error habitual organizar los datos en paquetes y adaptar los búferes DMA del núcleo IP al tamaño de esos paquetes. No hay ninguna ventaja en hacerlo. Aunque los datos se envíen en paquetes de tamaño constante, no hace falta adaptar el núcleo IP a ese tamaño.
¿Y si la FIFO se llena?
Una de las reglas básicas de una FIFO es: si la señal @full (lleno) está alta, @wr_en debe estar baja. En palabras sencillas: no escribas en una FIFO llena. ¿Y qué pasa si eso ocurre? Gestionar esta situación complicaría notablemente la lógica de la aplicación.
La respuesta corta es que la FIFO nunca debería llenarse: en condiciones normales de funcionamiento esto se evita activamente. El núcleo IP lee datos de la FIFO y los copia en la RAM del anfitrión. Esto ocurre con la rapidez suficiente para evitar que la FIFO se llene. Normalmente, la FIFO no necesita tener más de 512 elementos de profundidad.
Sin embargo, la FIFO puede llenarse si la lógica de la aplicación escribe en ella con demasiada rapidez. En otras palabras, si la tasa media de datos de la lógica de la aplicación supera el límite del núcleo IP (según lo anunciado para cada tipo de núcleo IP), el núcleo IP no podrá leer los datos de la FIFO con la rapidez suficiente.
Otra posibilidad es que el software de aplicación del usuario (por ejemplo, «cat» en el ejemplo anterior) no lea los datos del archivo de dispositivo con la rapidez suficiente. Como resultado, el búfer en la RAM del anfitrión se llenará, y esto también impedirá que el núcleo IP lea de la FIFO (porque no tendrá dónde escribir los datos). Por tanto, se produce un desbordamiento (overflow). Esto puede ocurrir porque el software de aplicación del usuario está mal escrito. Otra causa posible está relacionada con el sistema operativo; hablaremos de ella más abajo.
El tamaño del búfer en la RAM del anfitrión depende del núcleo IP de Xillybus. Por ejemplo, ese tamaño es de 4 MB para xillybus_read_32 y xillybus_write_32 (en el núcleo IP incluido en el paquete de demostración). La IP Core Factory permite crear núcleos IP que soliciten búferes significativamente mayores.
En conclusión: evitar un desbordamiento es cuestión de elegir los parámetros adecuados del núcleo IP. Ante todo, el núcleo IP debe ser capaz de manejar la tasa de datos. Además, el búfer en la RAM del anfitrión debe ser suficientemente grande. Esto garantiza que el flujo de datos pueda continuar incluso mientras el programa de aplicación del usuario no lee del archivo de dispositivo.
Si a pesar de todo la FIFO se llena, la causa más común es un error en el diseño del sistema. Una causa habitual de desbordamiento es sobrestimar la capacidad del ordenador para manejar la tasa de datos.
En particular, si los datos se escriben en un archivo del disco (como con el comando «cat» de arriba), la tasa máxima de datos puede ser más lenta de lo que uno piensa. La razón es que el sistema operativo suele tener una caché de disco grande (posiblemente de varios gigabytes). Si se mide la tasa de datos del disco con una cantidad de datos menor que la caché, los resultados serán demasiado optimistas: el sistema operativo fingirá haber terminado de escribir los datos en el disco cuando en realidad todavía no lo ha hecho. En realidad, los datos solo llegan a la caché, y la escritura real en el disco ocurre más tarde. Este error solo se pone de manifiesto cuando se maneja una cantidad mayor de datos.
Privación de la CPU
Desgraciadamente, existe una posibilidad inevitable de desbordamiento: al sistema operativo (Linux o Windows) se le permite privar de la CPU a cualquier proceso de espacio de usuario durante un tiempo ilimitado. En otras palabras, el programa informático que lee los datos puede dejar de funcionar de repente durante un periodo de tiempo, y luego reanudar su funcionamiento normal. No hay ningún límite para ese periodo. Cualquier sistema operativo que no sea de tiempo real puede pausar procesos aleatoriamente de este modo.
Y sin embargo, es posible hacer adquisición de datos con estos sistemas operativos. Esto se debe sobre todo a que una privación prolongada de la CPU se considera en general un defecto. Así que esas pausas suelen ser cortas.
Durante esas pausas, el núcleo IP sigue llenando el búfer en la RAM del anfitrión (mediante DMA, de modo que no se necesita la intervención del procesador). Cuando el programa recupera la CPU, puede consumir rápidamente todos los datos acumulados. Un búfer en RAM que compense una pausa de 10 ms suele ser suficiente. No obstante, es posible solicitar un búfer bastante mayor al crear un núcleo IP personalizado (custom IP core) en la IP Core Factory.
Dicho esto, sigue existiendo la posibilidad de que la pausa sea demasiado larga. Como resultado, el búfer en la RAM se llenará y, en consecuencia, la FIFO de la FPGA se llenará también. El resultado de este desbordamiento es que se perderán datos. Esto no debería ocurrir nunca, y probablemente no ocurrirá nunca. Pero, ¿y si ocurre?
Detección de un desbordamiento
La solución sugerida es poner fin al flujo de datos si la FIFO se llena: la lógica envía una marca de fin de archivo (EOF) al anfitrión inmediatamente después del último elemento de datos contiguos. Veamos, pues, qué ocurre si el anfitrión consume los datos con el comando «cat» sugerido arriba:
$ cat /dev/xillybus_read_32 > capture-file.dat
Por lo general, este comando continuará hasta que lo detengas con CTRL-C. Pero si la FIFO se ha llenado en la FPGA, el comando terminará de forma normal, igual que haría después de copiar un archivo normal. El archivo de salida contendrá todos los datos que se recogieron antes de que la FIFO se llenara.
En resumen, este método es el siguiente: se garantiza que todos los datos escritos en capture-file.dat no contienen errores y son contiguos. Si el sistema de adquisición de datos no puede mantener la continuidad debido a una privación de CPU, el resultado es un archivo de salida más corto. Pero puedes confiar en el contenido del archivo.
Para implementar esta solución, sustituye la instanciación de dualclock_fifo_32 por esto:
eof_fifo fifo_32
(
.rd_clk(bus_clk),
.rst(!user_r_read_32_open),
.rd_en(user_r_read_32_rden),
.dout(user_r_read_32_data),
.empty(user_r_read_32_empty),
.wr_clk(capture_clk),
.wr_en(capture_en),
.din(capture_data),
.full(),
.eof(user_r_read_32_eof)
);
La definición de eof_fifo se da en una página aparte.
Observa que @user_r_read_32_eof está conectado al puerto @eof de esta FIFO. Así es como la lógica envía la marca EOF al anfitrión cuando es necesario. Fíjate también en que al puerto @full (lleno) de esta FIFO no hay nada conectado: ya no hace falta vigilar esa señal. Si la FIFO se llena, no hay mucho que hacer al respecto. El mecanismo de EOF se asegura de que el anfitrión reinicie el flujo de datos después de que todos los datos válidos hayan sido consumidos.
Reproducción de datos
¿Y qué pasa con la dirección contraria? ¿Qué tal algo como esto?
$ cat playback-data.dat > /dev/xillybus_write_32
Esto funciona con el mismo principio: el comando «cat» lee de un archivo del disco y escribe los datos en un archivo de dispositivo. En la FPGA, el núcleo IP escribe esos datos en una FIFO. La lógica de la aplicación lee los datos de la FIFO. La misma idea, solo que en dirección contraria.
Este es un diagrama de bloques simplificado que ilustra el flujo de datos desde el programa de aplicación del usuario en el anfitrión hasta la lógica de la aplicación.
Al igual que en la adquisición de datos, no hay riesgo de pérdida de datos: el núcleo IP no escribe en la FIFO cuando está llena. Como resultado, el búfer en la RAM del anfitrión también puede llenarse. Cuando esto ocurre, el programa de aplicación del usuario en el anfitrión espera (durmiendo) hasta que la lógica de la aplicación lee suficientes datos de la FIFO.
Otra semejanza con la adquisición de datos es que la FIFO nunca se vaciará mientras el programa de aplicación del usuario siga escribiendo datos con la rapidez suficiente. Para garantizarlo, hay que tener en cuenta las mismas consideraciones: la especificación del núcleo IP y el programa de aplicación del usuario deben soportar la tasa de datos requerida.
Cuando se cumplen estas condiciones, la lógica de la aplicación puede consumir datos de la FIFO cuando los necesite. Nunca debería producirse un subdesbordamiento (underflow).
Flujos asíncronos frente a flujos síncronos
Este tema no está directamente relacionado, pero merece un breve comentario.
En una aplicación de adquisición de datos, el objetivo principal es mantener un flujo de datos continuo. Por eso, el núcleo IP mueve los datos desde la FIFO hasta el búfer en la RAM del anfitrión tan pronto como puede. No importa si el programa de aplicación del usuario está solicitando los datos en ese momento (mediante una llamada a read() o similar): el flujo de datos continúa mientras el archivo de dispositivo esté abierto y haya datos en la FIFO.
Esto significa que el anfitrión no tiene forma de controlar el flujo de datos desde la FPGA (salvo abriendo y cerrando el archivo de dispositivo, o recurriendo a una solución específica para la aplicación). Sin embargo, en la mayoría de las aplicaciones reales de adquisición de datos no hace falta controlar el flujo: es aceptable que el flujo comience al abrir el archivo de dispositivo. No importa en qué instante exacto se leyó cada dato de la FIFO.
Un archivo de dispositivo que se comporta así se denomina flujo asíncrono (asynchronous stream) —en la terminología de Xillybus—.
Sin embargo, en otras aplicaciones es importante saber cuándo se recogieron los datos. Por ejemplo, la lógica de la aplicación en la FPGA puede enviar el contenido de un registro de estado en lugar de datos procedentes de una FIFO. Esto se demuestra en el paquete de demostración con el archivo de dispositivo llamado xillybus_mem_8. En ese caso es muy importante controlar cuándo se recogen los datos en la FPGA: el anfitrión lee del archivo de dispositivo para obtener información sobre el estado en ese momento, y no sobre cuál era el estado en algún momento desconocido del pasado.
Xillybus dispone de flujos síncronos (synchronous streams) para aplicaciones de este tipo: el núcleo IP siempre recoge de la FPGA la menor cantidad posible de datos. En otras palabras, el núcleo IP solo recoge datos en respuesta a una llamada a read() (o similar) realizada desde el anfitrión. Por tanto, es el anfitrión quien controla cuándo se recogen los datos de la FPGA.
La desventaja de los flujos síncronos son las pausas en el flujo de datos. El principal problema de estas pausas es que la FIFO de la FPGA puede llenarse cuando el flujo de datos se detiene momentáneamente. Estas pausas también reducen la eficiencia del flujo de datos, por lo que la tasa máxima de datos se vuelve menor. Sin embargo, estas dos desventajas solo son relevantes para una aplicación de adquisición de datos. Una aplicación así debería usar de todos modos un flujo asíncrono.
En cuanto a los archivos de dispositivo en la dirección contraria, también hay una diferencia entre flujos asíncronos y flujos síncronos. En esta dirección, la diferencia está en el retorno de la llamada a write(): con los flujos asíncronos, write() retorna en cuanto los datos se han escrito en el búfer de la RAM. Así que, en la mayoría de los casos, write() no se bloquea. Con los flujos síncronos, en cambio, write() espera hasta que los datos se han entregado en la FPGA. Esto es importante cuando el canal de comunicación se utiliza para enviar comandos. Pero, una vez más, esto es malo para una aplicación de adquisición de datos.
En el paquete de demostración, solo /dev/xillybus_mem_8 es un flujo síncrono. Los otros cuatro archivos de dispositivo son flujos asíncronos.
En la IP Core Factory, la elección entre un flujo síncrono y un flujo asíncrono depende de la selección de aplicación (el menú desplegable «use»). Por ejemplo, si eliges «Data acquisition / playback», la herramienta produce un flujo asíncrono. Si eliges «Command and status», obtienes un flujo síncrono. También es posible seleccionarlo manualmente, desactivando la opción «Autoset internals».
Para más información sobre flujos asíncronos y flujos síncronos, consulta la sección 2 de la guía de programación para Linux (o la guía de programación para Windows). En cuanto a la IP Core Factory, consulta la Guide to defining a custom Xillybus IP core.
Resumen
Un sistema de adquisición de datos sencillo, y sin embargo funcional en la práctica, puede crearse de forma simple y rápida con Xillybus: el software de la aplicación se reduce a usar un comando estándar de Linux («cat»). En el lado de la FPGA, la interacción con el núcleo IP de Xillybus consiste simplemente en escribir los datos en una FIFO.
Se garantiza que los datos recogidos con Xillybus no contienen errores y son contiguos. No obstante, debido a la naturaleza del sistema operativo, no hay forma de garantizar que nunca se produzca un desbordamiento. Como esto es inevitable, el enfoque óptimo es garantizar la detección del desbordamiento si ocurre. Xillybus ofrece un mecanismo sencillo para este fin, mediante el envío de una marca EOF al anfitrión.

