01signal.com

Acceso a los archivos de dispositivo de Xillybus

Introducción

Esta página se basa en la guía de programación de Xillybus para Linux. Para una visión más completa de los temas que se tratan a continuación, se recomienda consultar esa guía. También existe una guía similar para Microsoft Windows.

Si todavía no has hecho la prueba del «Hola, mundo», se recomienda hacerla primero.

La comunicación con el núcleo IP (IP core) de Xillybus se realiza mediante archivos de dispositivo (device files) en el ordenador anfitrión. Estos archivos de dispositivo se manejan como un archivo normal. Sin embargo, a diferencia de un archivo normal, un archivo de dispositivo no representa datos almacenados en un disco: leer y escribir en un archivo de dispositivo da lugar, en cambio, a operaciones de entrada/salida (I/O).

En consecuencia, se puede acceder a los archivos de dispositivo de Xillybus con prácticamente cualquier lenguaje de programación. También se pueden usar las utilidades de línea de comandos de Linux para este fin. Así que uno puede preguntarse por qué hace falta hablar de este tema: si sabes leer y escribir archivos correctamente, sabes trabajar con Xillybus.

Ciertamente, es posible escribir programas que accedan a archivos normales sin un conocimiento detallado de la API. Pero esto no basta cuando se trabaja con E/S: la interacción con el hardware crea situaciones que rara vez ocurren con un archivo normal. Como programador, es necesario saber cómo manejar esas situaciones con ayuda de la API.

Por tanto, la gran mayoría de esta página se dedica a temas que también son relevantes para acceder a archivos normales. La diferencia con los archivos de dispositivo es que estos no perdonan los errores al usar la API.

La falta de comprensión de la API puede dar lugar a dos tipos principales de confusión:

Lenguajes de programación y sistemas operativos

Con Linux, cualquier herramienta o lenguaje de programación que pueda acceder a un archivo funciona con Xillybus.

Cuando se usa Windows, no hay problema con los lenguajes de programación habituales, como C, C++, C#, Python, Perl y cualquier herramienta que venga con Cygwin. Sin embargo, algunas herramientas (por ejemplo, MATLAB) pueden negarse a trabajar con los archivos de dispositivo de Xillybus: esas herramientas detectan que el archivo de dispositivo no es un archivo normal y lo consideran un error. Hay soluciones alternativas para esta situación, que normalmente consisten en usar extensiones pensadas para E/S de bajo nivel.

La exposición que sigue se basa en el lenguaje C; sin embargo, los temas son relevantes para todos los lenguajes de programación.

Código de ejemplo

En la web de Xillybus se pueden descargar ejemplos de código. Estos ejemplos están escritos en C y muestran cómo usar la API de bajo nivel correctamente.

Para descargar los ejemplos, ve a la página web desde la que descargaste el paquete de demostración (demo bundle), es decir, la página de Xillybus o la página de XillyUSB.

Si usas Linux, descarga el controlador para Linux. El código de ejemplo está incluido en el mismo archivo .tar.gz.

Si usas Windows, descarga el paquete de Xillybus para Windows.

En cualquier caso, el código de ejemplo está dentro del subdirectorio demoapps/. Ten en cuenta que en ese código la cantidad de datos que se lee o se escribe en cada operación de E/S es pequeña, porque se asigna un búfer pequeño (128 bytes). El propósito de esto es únicamente simplificar el código. En una aplicación real se recomienda un búfer mayor (32 kBytes suele ser una buena elección).

Las diferencias entre Linux y Windows son pequeñas, pero importantes. En concreto:

Dicho esto, los principios que hay detrás del código de ejemplo son exactamente los mismos.

E/S de archivos con búfer

Los lenguajes de programación suelen ofrecer dos API separadas para acceder a archivos: una API de alto nivel y otra de bajo nivel. La API de alto nivel es la más utilizada porque es más cómoda de manejar.

Por ejemplo, en el lenguaje C, la API de alto nivel se compone de fopen(), fread(), fwrite(), fprintf(), fclose(), etc. La API de bajo nivel se compone de open(), read(), write(), close(), etc. La diferencia entre estas dos API no es solo una pequeña diferencia en los nombres de las funciones: la API de alto nivel proporciona búferes de RAM en el espacio de usuario (user-space RAM buffers). Estos búferes los implementa la biblioteca de tiempo de ejecución de C. No hay que confundirlos con los búferes DMA, que controla el controlador en el kernel.

La diferencia más importante es el comportamiento de fwrite(): el resultado de una llamada a esta función puede ser que los datos se guarden en un búfer de espacio de usuario. La transmisión hacia la FPGA puede retrasarse hasta más tarde. De hecho, los datos pueden permanecer indefinidamente en el búfer de fwrite() hasta que se cierra el archivo. Esto puede parecer un bug de Xillybus, porque los datos se han escrito en el archivo y, sin embargo, no ha pasado nada.

Por tanto, se recomienda usar la API de bajo nivel (sin búfer). Esto suele ser posible, aunque la mayoría de los lenguajes de programación fomentan el uso de la API de alto nivel. Si la herramienta o el lenguaje no soporta una API de bajo nivel, usa la API que esté disponible. En ese caso, es importante recordar que no hay control sobre cuándo se realiza la E/S. No obstante, esto es suficiente en muchas aplicaciones, por ejemplo, para la adquisición de datos (data acquisition).

Una vez más, la E/S con búfer no debe confundirse con los búferes de Xillybus. Sobre todo, los datos nunca se quedarán atascados indefinidamente por culpa de los búferes de Xillybus. Esto se explica más abajo, en el contexto de write() de longitud cero.

Lectura de un archivo de dispositivo: conceptos básicos

El código de ejemplo para leer de un archivo de dispositivo es streamread.c. Este programa se encuentra en el directorio demoapps/. Sin embargo, el código que voy a mostrar a continuación procede de otro programa: memread.c (del mismo directorio). Este programa está pensado para un propósito distinto, pero contiene una función llamada allread(). Resulta más cómodo demostrar algunos temas con esta función.

Supongamos que se ha abierto un archivo con este comando:

int fd, len;
char *buf;

fd = open("/dev/xillybus_read_32", O_RDONLY);

Ahora queremos leer @len bytes de un archivo a un búfer. Pero no se permite un resultado parcial: queremos una función que siempre lea la cantidad de datos requerida.

Esto es lo que hace allread() cuando se usa así:

allread(fd, buf, len);

Esta función se define de la siguiente manera:

void allread(int fd, unsigned char *buf, int len) {
  int received = 0;
  int rc;

  while (received < len) {
    rc = read(fd, buf + received, len - received);

    if ((rc < 0) && (errno == EINTR))
      continue;

    if (rc < 0) {
      perror("allread() failed to read");
      exit(1);
    }

    if (rc == 0) {
      fprintf(stderr, "Reached read EOF\n");
      exit(1);
    }

    received += rc;
  }
}

Observa que esta función siempre lee el número de bytes solicitado. Si no es posible, la función hace que el programa termine. Esto puede ser demasiado drástico para un escenario de uso normal. allread() debe tratarse como una demostración sencilla de cómo acceder a un archivo con la API de bajo nivel.

Expliquemos esta función. La primera parte interesante es esta:

rc = read(fd, buf + received, len - received);

@received es igual a cero durante la primera iteración. Así que esta línea es equivalente a esto:

rc = read(fd, buf, len);

read() intenta leer @len bytes del descriptor de archivo (@fd) y almacenar los datos en el búfer (@buf).

En cuanto a un archivo de dispositivo de Xillybus: read() espera hasta 10 ms si la cantidad de datos solicitada (@len bytes) no está disponible. Tras ese breve periodo, la función retorna con menos datos de los requeridos (pero al menos un byte). Si no hay datos en absoluto, read() espera indefinidamente hasta que lleguen datos de la FPGA (pero hay excepciones; más sobre esto abajo). Este comportamiento es específico del controlador de Xillybus (aunque cumple con la API estándar).

Si read() pudo leer algo, @rc es igual al número de bytes leídos. Eso significa que @rc es un número positivo y que todas las sentencias if del bucle se omiten. De modo que lo siguiente que ocurre es:

received += rc;

En consecuencia, @received contiene siempre el número total de bytes leídos hasta el momento. Ten en cuenta que es perfectamente legal y normal que @rc sea menor que @len (el número de bytes solicitados).

El bucle while continúa hasta que @received (el número total de bytes leídos) alcanza @len. Esto se refleja en la sentencia while:

while (received < len) { ... }

Así que se leen más datos si es necesario:

rc = read(fd, buf + received, len - received);

Esta vez, el punto de partida en el búfer se desplaza con @received. El número de bytes solicitados también se reduce en la misma cantidad. Estos ajustes reflejan simplemente que se trata de un intento repetido de leer datos.

Cuando read() no lee nada

Hasta ahora me he centrado en lo que ocurre cuando read() puede leer datos. Sin embargo, hay tres situaciones en las que read() no lee nada. Cada una de ellas se maneja con su propia sentencia if.

Señales POSIX

Cuando pulsas CTRL-C para detener un programa, el sistema operativo envía una señal POSIX al proceso. Ese es el mecanismo que hace que el programa termine. Lo mismo ocurre cuando usas el comando «kill» con ese fin. Pero también hay muchos otros tipos de señales que, en la mayoría de las situaciones, conviene ignorar.

¿Qué ocurre entonces si el proceso recibe una señal en mitad de una llamada a read()? De acuerdo con las convenciones de Linux, read() debe devolver el control al programa principal de inmediato. Si read() había podido leer datos antes de que esto ocurriera, no pasa nada inusual: @rc contendrá el número de bytes y no habrá ninguna indicación de que se haya recibido una señal.

Pero si no ha llegado ningún dato nuevo, @rc será un número negativo y @errno será igual a EINTR. La forma estándar de manejar esta situación es la que se muestra en el código: comportarse como si no hubiera pasado nada e intentarlo de nuevo.

if ((rc < 0) && (errno == EINTR))
  continue;

Eso no significa que la señal se ignore: por ejemplo, si el motivo de la señal fue que el usuario pulsó CTRL-C, el programa terminará como siempre. Hay otro mecanismo que se encarga de eso. El propósito de esta sentencia if es manejar las señales que no pretenden provocar nada grave. La sentencia continue garantiza que no ocurra nada raro si el proceso recibe una señal de este tipo.

Por ejemplo, si el proceso se detiene con CTRL-Z, esta sentencia if es necesaria para asegurar que el programa continúe cuando se reanude la ejecución. Además, hay varias otras señales que pueden llegar sin ninguna intervención humana.

Un error real

Es natural que algo pueda salir mal al intentar leer datos. Cuando esto ocurre, @rc será un número negativo y el valor de @errno será algo distinto de EINTR. El código de ejemplo simplemente informa del error y termina el programa:

if (rc < 0) {
  perror("allread() failed to read");
  exit(1);
}

EOF

Si read() no puede suministrar datos porque se ha alcanzado el fin de archivo (EOF), la función retorna el valor cero. Esto es, por supuesto, cierto para un archivo normal. Pero también Xillybus tiene la capacidad de declarar que el flujo de datos ha terminado. El comportamiento es el mismo.

El código relevante es este:

if (rc == 0) {
  fprintf(stderr, "Reached read EOF\n");
  exit(1);
}

Esto también se trata como un error y provoca la terminación del programa: significa que se alcanzó el EOF antes de leer la cantidad de datos solicitada (@len bytes). Esta sentencia if solo puede alcanzarse si @received es menor que @len.

Recuerda que la idea detrás de allread() era que siempre lee la cantidad de datos requerida. Si no es posible, esta función detiene el programa informático.

Relevancia para otros lenguajes de programación

El código de arriba está escrito en C, pero demuestra algunos puntos importantes que son relevantes sea cual sea el lenguaje de programación.

Escritura en un archivo de dispositivo

La API de bajo nivel para escribir en un archivo es casi la misma que para leer de un archivo. Para demostrarlo, esta es la función llamada allwrite(), que se encuentra en streamwrite.c:

void allwrite(int fd, unsigned char *buf, int len) {
  int sent = 0;
  int rc;

  while (sent < len) {
    rc = write(fd, buf + sent, len - sent);

    if ((rc < 0) && (errno == EINTR))
      continue;

    if (rc < 0) {
      perror("allwrite() failed to write");
      exit(1);
    }

    if (rc == 0) {
      fprintf(stderr, "Reached write EOF (?!)\n");
      exit(1);
    }

    sent += rc;
  }
}

Compárala con allread(), mostrada arriba: solo hay tres diferencias:

Así que, en principio, no hay diferencia entre escribir y leer.

Dicho esto, ten en cuenta que @rc nunca debería ser cero, porque no tiene sentido un EOF al escribir en un archivo. Según el estándar POSIX, @rc solo puede ser cero cuando se pide a write() que escriba cero bytes. Pero eso nunca ocurre en este bucle while.

En resumen, allwrite() siempre escribe el número de bytes que se le piden. La única alternativa es terminar el proceso. En otras palabras, supongamos que se ha abierto un archivo de dispositivo de la siguiente manera:

int fd, len;
char *buf;

fd = open("/dev/xillybus_write_32", O_WRONLY);

Escribir @len bytes desde @buf se hace así:

allwrite(fd, buf, len);

Todo lo dicho antes sobre allread() es aplicable también a allwrite(), incluida la relevancia para otros lenguajes de programación.

Escritura de longitud cero

Está permitido hacer una llamada a write() con cero bytes. La API estándar no dice qué ocurrirá en ese caso concreto. Pero, evidentemente, eso significa que no se escribirá ningún dato.

Una llamada de este tipo tiene un significado especial para un archivo de dispositivo de Xillybus: escribir cero bytes equivale a solicitar un vaciado del búfer (flush). Para entender su significado, veamos primero qué ocurre cuando se escriben datos en un archivo de dispositivo.

Supongamos que el archivo de dispositivo es un flujo asíncrono (asynchronous stream). Este término se explica brevemente en una página aparte y con más detalle en la documentación.

Cuando se escriben datos en un archivo de dispositivo (con write()), el controlador de Xillybus almacena esos datos en un búfer de RAM. Es posible que una parte o todos esos datos se envíen a la FPGA de inmediato. Pero, por lo general, puede quedar una cantidad de datos en el búfer, y el programa que realizó la llamada continúa igualmente. El propósito de este mecanismo es mejorar el rendimiento, en particular si hay muchas llamadas a write().

Entonces, ¿cuándo se envían a la FPGA los datos que están en el búfer de RAM del controlador? Hay cuatro situaciones posibles:

Así pues, los datos nunca permanecen demasiado tiempo en el búfer del controlador. Esto se debe a que los datos siempre se envían a la FPGA en un plazo de 10 ms. Pero en algunas aplicaciones ni siquiera ese retardo es aceptable. En ese caso, se puede usar una llamada a write() de longitud cero para solicitar que los datos restantes se envíen de inmediato.

Así se hace en C:

write(fd, NULL, 0);

Observa que la dirección del búfer es NULL. Eso es correcto, porque el número de bytes a escribir es cero. Pero esta llamada no garantiza que la solicitud tenga éxito. Aunque es muy probable que funcione, esta es la forma correcta de hacerlo:

while (1) {
  rc = write(fd, NULL, 0);

  if ((rc < 0) && (errno == EINTR))
    continue; // Interrupted. Try again.

  if (rc < 0) {
    perror("flushing failed");
    break;
  }

  break; // Flush successful
}

Todo esto se ha dicho para un flujo asíncrono. Si el archivo de dispositivo es un flujo síncrono (synchronous stream), los datos siempre se envían a la FPGA de inmediato como resultado de una llamada a write(). Además, write() espera hasta que los datos han llegado a la FPGA antes de retornar (una llamada a write() de longitud cero no hace eso).

Así pues, write() de longitud cero solo es relevante para flujos asíncronos. Esta funcionalidad no debería usarse si no es necesario, porque ralentiza la comunicación con la FPGA.

Resumen

Como ya se ha mencionado, casi todo lo escrito arriba es válido para acceder a cualquier archivo. Solo un par de temas eran específicos de Xillybus.

Es importante seguir estas pautas para garantizar un comportamiento coherente en la comunicación con la FPGA. Los programas que se escriban sin tener en cuenta estos temas probablemente fallarán de vez en cuando. A menudo, esos fallos parecen un problema de la FPGA o del controlador. Por tanto, unas técnicas de programación adecuadas te ahorrarán mucha confusión y esfuerzos innecesarios.

Esta página se ha traducido del inglés mediante traducción automática. En caso de duda, consulta el texto original.
Copyright © 2021-2026. All rights reserved. (dcc38493)