Full text
2
ESCUELA TÉCNICA SUPERIOR DE INGENIERÍA INFORMÁTICA GRADO EN INGENIERÍA DEL SOFTWARE CONFIGURACIÓN Y USO DE PARALLELLA-16 CONFIGURATION AND USE OF PARALLELLA-16 Realizado por Eliecer García Rey Tutorizado por Dr. D. Sergio Gálvez Roja Departamento Lenguajes y Ciencias de la Computación UNIVERSIDAD DE MÁLAGA MÁLAGA, enero de 2015 Fecha defensa: El Secretario del Tribunal 3
4
A mis padres 5
6
RESUMEN Y PALABRAS CLAVE Parallella-16 es un ordenador de tipo SBC (del inglés Single Board Computer, traducido al español como Ordenador de Placa Reducida) con 16 núcleos fabricado por Adapteva. El precio de este ordenador, poco menos de 110€, y la capacidad de conectar varios de estos para trabajar como uno solo, lleva a sus responsables a utilizar como eslogan “Supercomputing For Everyone” (Supercomputación para todo el mundo). Este trabajo fin de grado pretende ser un punto de inicio para todo aquel que quiera trabajar con Parallella-16, ya que su documentación está desactualizada, la descripción de sus librerías es pobre y carece de ejemplos sencillos y didácticos. Esto es evidente en su foro oficial, donde mucha gente pregunta cosas que podrían calificarse de básicas. En este documento se explica la forma de configurar y trabajar con Parallella-16, incluyendo comentarios y opiniones de quien ha empezado a trabajar con ella sin ningún conocimiento. Tiene un capítulo de introducción a la programación en esta arquitectura utilizando el ejemplo hello-world que acompaña a su kit de desarrollo, la traducción de sus librerías y ejemplos sencillos que permiten conocer cómo utilizar las funciones de estas librerías. Palabras clave: Informática, Supercomputación, Programación Paralela, Adapteva, Epiphany, Parallella-16, SBC (Single Board Computer), Multi-núcleo. 7
ABSTRACT AND KEYWORDS Parallella-16 is a SBC (Single Board Computer) with 16 cores manufactured by Adapteva. The price of this computer, just under $120, and the ability to connect several of these to work as one, takes its responsibility to use the slogan “Supercomputing for Everyone”. This work aims to be a starting point for anyone who wants to work with Parallella-16. The documentation of this project (Parallella-16) is outdated, the description of its libraries are poor and hasn’t simple and didactic examples. This is evident in his official forum where many people question things that could be described as basic. This document describes how to configure and work with Parallella-16, including comments and opinions of those who have begun to work with it without any knowledge. It has a chapter of introduction to programming in this architecture using the hello-world example that came with your software kit development, translation of its libraries to Spanish and simple examples to learn how to use the functions of these libraries. Keywords: Computing, Supercomputing, Parallel computing, Adapteva, Epiphany, Parallella-16, SBC (Single Board Computer), Multicore. 8
ÍNDICE 1 INTRODUCCIÓN......................................................................................................11 2 PARALLELLA-16....................................................................................................13 2.1 Adapteva.........................................................................................................13 2.2 Parallella.........................................................................................................13 2.3 Epiphany.........................................................................................................14 2.4 Epiphany III.....................................................................................................14 2.5 Parallella-16....................................................................................................15 3 PRIMEROS PASOS.................................................................................................19 3.1 Tarjeta microSD..............................................................................................19 3.2 ESDK..............................................................................................................21 3.3 Manuales.........................................................................................................22 3.4 Ejemplos.........................................................................................................23 3.5 Buscar ayuda..................................................................................................23 4 ARQUITECTURA.....................................................................................................25 4.1 Núcleos...........................................................................................................25 4.2 Malla 2D..........................................................................................................25 4.3 Memoria..........................................................................................................26 4.4 Modelos de programación..............................................................................29 5 HELLO WORLD.......................................................................................................31 5.1 ¿Qué hace este ejemplo?...............................................................................31 5.2 ¿Cómo vamos a estudiar este ejemplo?........................................................32 5.3 Iniciar el sistema (en el host)..........................................................................32 5.4 Reserva de la memoria compartida (en el host).............................................33 5.5 Establecimiento de un grupo de trabajo (en el host)......................................33 5.6 Carga de los ejecutables y ejecución (en el host)..........................................34 5.7 Reserva de la memoria compartida (en un eCore)........................................34 5.8 Escritura en la memoria compartida (en un eCore)........................................35 5.9 Lectura de la memoria compartida (en el host)..............................................35 5.10 Visualización por pantalla (en el host)..........................................................35 5.11 Tareas de finalización (en el host)................................................................36 5.12 Utilizar este ejemplo como plantilla..............................................................36 6 ECLIPSE..................................................................................................................37 6.1 Abandono de Eclipse como IDE.....................................................................37 6.2 Incompatibilidad con ARM..............................................................................37 6.3 Esquema de desarrollo...................................................................................37 6.4 Instalar el eSDK..............................................................................................38 6.5 Hello World con Eclipse..................................................................................39 9
Ilustración 2-2. Parallella-16 sobre la palma de una mano Ilustración 2-3. Componentes de Parallella-16 16
Ilustración 2-4. Parte inferior de Parallella-16 Ilustración 2-5. Conexión entre componentes de Parallella-16 17
18
3 PRIMEROS PASOS Antes de empezar con los detalles técnicos, vamos a ver en este capítulo los primeros pasos que debemos dar con una Parallella-16, el software que necesitamos, los manuales y ejemplos disponibles, y muy importante, dónde acudir si necesitamos ayudas. 3.1 Tarjeta microSD Parallella-16 utiliza una tarjeta microSD como disco duro, pero esta no viene incluida. Por tanto, uno de los primeros pasos es obtener una tarjeta microSD y añadir en ella el sistema operativo junto a las librerías necesarias. La página web oficial hay una sección dedicada a ello: https://www.parallella.org/create-sdcard. Por tanto, vamos a esta página y descargamos la versión de Linux que Adapteva ha preparado para esta plataforma. En la actualidad la distribución utilizada es Ubuntu. Hay que descargar además 2 archivos: Núcleo de Linux para Parallella con o sin soporte para HDMI. Archivos para la FPGA de Parallella. Para seleccionar bien qué versión de estos archivos se debe descargar solo hay que tener claro cuántos núcleos tiene (16 o 64), el modelo del procesador Zynq y si tiene HDMI o no. Una vez descargado todo, el siguiente paso es instalarlo en la tarjeta. Los pasos en Windows son: 1. Insertar la tarjeta en un ordenador con Windows. 2. Descomprimir la imagen de Ubuntu. Adapteva recomienda utilizar 7zip. 3. Grabar la imagen de Ubuntu en la tarjeta microSD como haríamos con un CD. Adapteva recomienda utilizar Win32 Disk Imager. 4. Copiar los archivos uImage y devicetree.dtb (están comprimidos en el archivo que hemos descargado con el núcleo de Linux) y el archivo “parallella-*.bin”. 5. Renombrar el fichero “parallella-*.bin” como “parallella.bit.bin”. IMPORTANTE: Por alguna razón que desconocemos, cuando seguimos estos pasos en el laboratorio la microSD no arrancaba. En cambio sí lo hizo cuando hicimos los pasos en Linux. Si intenta crear una microSD y no le funciona le recomiendo que intente seguir los pasos para Linux. 19
Los pasos en Linux son: 1. Insertar la tarjeta en un ordenador con Linux. 2. Descomprimir la imagen de Ubuntu. $ gunzip -d <releasename>.img.gz 3. Verifique la ruta de la tarjeta microSD. $ sudo fdisk -l | grep Disk Como el siguiente paso sobrescribirá los datos del disco seleccionado. Asegúrese de que la ruta es correcta. En el siguiente ejemplo la tarjeta microSD tiene la ruta "/dev/mmcblk0/". El resultado debe ser similar al siguiente. Disk /dev/sda: 500.1 GB, 500107862016 bytes Disk identifier: 0xa26fb586 Disk /dev/mmcblk0: 15.9 GB, 15931539456 bytes Disk identifier: 0x000a07b6 4. Grabe la imagen de Ubuntu en la tarjeta microSD. $ sudo dd bs=64k if=<release-name>.img of=<sd-path> Deberá tener paciencia, ya que dependiendo del ordenador este proceso puede llegar a tardar más de 30 minutos. 5. Copie el núcleo de Linux para Parallella y los archivos del FPGA en la tarjeta microSD. $ tar -zxvf <kernel-*>.tgz -C <sd-path>/BOOT $ cp <parallella-*>.bin <sd-path>/BOOT/parallella.bit.bin $ sync Compruebe que puede ver las particiones 'rootfs' y 'BOOT' en la tarjeta microSD. Dependiendo de su configuración, usted puede o no necesitarlo para montar la partición. En Ubuntu, la ruta debería sería: “/media/$USER/BOOT” 20
Los pasos en Mac son: 1. Insertar la tarjeta en un ordenador con Mac: 2. Descomprimir la imagen de Ubuntu. 3. Verifique la ruta de la tarjeta microSD. $ diskutil list 4. Desmontar la tarjeta microSD. $ diskutil unmountDisk <sd-path> 5. Copia la imagen de Ubuntu en la tarjeta microSD. $ sudo dd if=<release-name>.img of=<sd-path> bs=1m Deberá tener paciencia, puede llevar bastante tiempo. 6. Copie el núcleo de Linux para Parallella y los archivos del FPGA en la tarjeta microSD. $ cp <parallella-*> <sd-path>/BOOT/parallella.bit.bin $ cp uImage <sd-path>/BOOT $ cp devicetree.dtb <sd-path>/BOOT 3.2 ESDK En la documentación oficial, se denomina eSDK al SDK (el Kit de Desarrollo de Software) de Epiphany. Este kit está compuesto por: Compilador optimizado ANSI-C basado en gcc. Depurador multi-núcleo basado en gdb. Entorno de desarrollo Eclipse (solo para x86-64). Simulador funcional. Librerías para la comunicación entre núcleos. Manuales. Ejemplos. Este kit viene instalado en la imagen de Ubuntu utilizada en la tarjeta microSD. En este documento hay un capítulo dedicado al entorno de desarrollo Eclipse, pero hay que advertir que este ya no tiene soporte de Adapteva, ya no se incluye en las nuevas versiones del eSDK. En el foro oficial algunos usuarios han optado por utilizar Code::Block como entorno alternativo. 21
3.3 Manuales Adapteva pone a disposición de todo el mundo en su página web oficial 4 manuales y una serie de documentos que van desde presentaciones a informes, algunos realizados por entidades como Australia National University, Computer Journal o Ericsson. Parallella Reference Manual (Septiembre 2014) – Es un manual en el que podemos encontrar los detalles de Parallella-16, desde un mapa de la placa hasta los detalles eléctricos. Epiphany III (E16G301) Datasheet (Febrero 2013) – Es una ficha de datos que ha acabado siendo un manual. En él se pueden encontrar los datos más relevantes acerca del procesador Epiphany III, el modelo con 16 núcleos. Epiphany-IV (E64G401) Datasheet (Junio 2013) – Es la ficha de datos del procesador Epiphany-IV, el modelo con 64 núcleos. Parallella Schematic (Diciembre 2013) – Son los planos de la circuitería eléctrica de Parallella-16. Epiphany Architecture Reference Manual (Marzo 2014) – Aquí podemos encontrar todo lo referente a aspectos técnicos relacionados con la plataforma Epiphany. En el siguiente capítulo, dedicado a su arquitectura, puede encontrar un resumen de este manual. Epiphany SDK Reference Manual (Septiembre 2013) – Este manual habla del eSDK y en él podemos encontrar desde cómo compilar hasta las funciones de las librerías para el manejo de los núcleos. Uno de los problemas de utilizar Parallella-16 es son sus manuales. La fecha que acompaña al título de los manuales es la fecha de la versión que actualmente (14/01/2015) están colgados en su página web. Un ejemplo del estado de estos manuales lo podemos ver en Parallella Reference Manual. La página del capítulo 6, dedicado al arranque de Parallella-16 está, salvo por el título, en blanco. Otro ejemplo, que puede hacer que una persona invierta mucho tiempo en buscar dónde está el error en el código de hello-world, es que si nos vamos al documento Epiphany SDK Reference Manual podemos encontrar la descripción de la función e_reset_core(), la función utilizada en el ejemplo hello-world, cuando esta función ya no se encuentra en el eSDK y ha sido sustituida por e_reset_group(). 22
3.4 Ejemplos Aunque el eSDK viene con algunos ejemplos, Adapteva tiene en GitHub un repositorio de ejemplos entre los que cabe destacar: john – Una versión para Parallella del programa “John the Ripper” utilizado para descifrar contraseñas por fuerza bruta. kinect_test – Una demostración de Kinect que utiliza Epiphany para colorear, escalar y renderizar. El problema de los ejemplos que aporta Adapteva es que no son didácticos. El ejemplo hello-world que viene con el eSDK no utiliza los 16 núcleos a la vez, sino uno por uno, y el siguiente ejemplo, una multiplicación de matrices, es tan extenso que es difícil sacar de él algo en claro. En el capítulo dedicado a las librerías de programación, a medida que se describen las funciones se ofrecen varios ejemplos que se pueden encontrar en el CD que acompaña a este trabajo fin de grado. El propósito de estos ejemplos es mostrar cómo utilizar la función, no utilizarla en una aplicación útil. De manera que una persona con conocimientos de C pueda comprender para qué sirve y cómo utilizarla, por ejemplo, una barrera. 3.5 Buscar ayuda La forma ideal de buscar ayuda es entrar en el foro (https://parallella.org/forums) que Adapteva ha creado para interactuar con la gente. Dentro del foro, existen varias categorías dedicadas en exclusiva a hardware, software, proyectos, etc. Cada una de ellas divididas en subcategorías. En Freenode, una red de servidores IRC, se puede encontrar un canal #parallella en la que se pueden resolver problemas en tiempo real, pero la presencia de usuarios no siempre es síntoma de presencia de personas. Por lo que aconsejo utilizar mejor el foro, donde quizás alguien haya buscado solución al mismo problema. 23
24
4 ARQUITECTURA En este capítulo se abordan temas relacionados con la arquitectura Parallella-16. No pretende sustituir al manual oficial sobre su arquitectura, sino simplemente un resumen de lo más relevante antes de ponerse a programar en ella. 4.1 Núcleos Cada uno de los núcleos es de tipo RISC a 1 GHz. Cuenta con una memoria local de 32 kB que a su vez forma parte de la memoria externa (compartida) del sistema. Una FPU (Floating Point Unit) permite operaciones en coma flotante de 32 bits con precisión simple. Estas operaciones son: Suma, resta, multiplicación más suma, multiplicación más resta, conversiones entre fijo y flotante, valor absoluto. Por cada ciclo de reloj es capaz de realizar 1 operación de enteros o 2 en coma flotante. 4.2 Malla 2D En la arquitectura Epiphany los núcleos de sus procesadores forman una malla 2D, de forma que, los núcleos de Parallella-16 una malla de 4x4. La arquitectura permite conectar múltiples Epiphany III, de manera que conectamos nuestra Parallella-16 a otras 3, la malla sería de 64x641. Cada núcleo tiene una interface de red que utiliza para comunicarse con un router que lo conecta a la malla. Como puede observarse en la ilustración 4-1, estos routers, uno por núcleo, conectan entre sí a todos los núcleos. Su latencia baja les permite comunicarse entre sí a una velocidad muy alta, 512 GB/s de ancho de banda con la memoria local según la documentación. Ilustración 4-6. Arquitectura multi-núcleo Epiphany 1 La documentación no deja claro si el número de chips conectados debe ser potencia de 2, es decir, no dice si es posible conectarla a otra o si es necesario al menos tres. 25
5.2 ¿Cómo vamos a estudiar este ejemplo? Para simplificar el análisis de este ejemplo vamos a dividirlo en fases, y vamos a ver qué funciones están implicadas en cada fase. Estas fases, y las funciones utilizadas en este ejemplo, son prácticamente las mismas que vamos a poder encontrar en cualquier aplicación diseñada para esta arquitectura. Este ejemplo consta de dos ejecutables distintos, uno que ejecuta el procesador ARM y el que ejecutan los núcleos. El código fuente de ambos ejecutables vamos a dividirlo en las siguientes fases para su estudio por separado: #Ejecutable principal (en el host) Ejecutable secundario (en un eCore) 1 Inicialización del sistema. 2 Reserva de la memoria externa. 3 Establecimiento de un grupo de trabajo 4 Carga de los ejecutables y ejecución. 5 Reserva de la memoria externa. 6 Escritura en la memoria externa. 7 Lectura de la memoria externa. 8 Visualización por pantalla. 9 Tareas de finalización NOTA: En gris las fases que se repiten en cada bucle. 5.3 Iniciar el sistema (en el host) En el siguiente código, la primera función se encarga de establecer la conexión con Epiphany III. En el capítulo dedicado a la librería e-hal.h se explicará por qué se pasa NULL. La segunda línea se encarga de poner a punto el sistema. Y finalmente, la tercera línea obtiene información sobre el procesador Epiphany, de esta forma se puede conocer el número de núcleos que tiene y las coordenadas del primero. // initialize system, read platform params from // default HDF. Then, reset the platform and // get the actual system parameters. e_init(NULL); e_reset_system(); e_get_platform_info(&platform); 32
5.4 Reserva de la memoria compartida (en el host) En el siguiente código, la función e_alloc() sirve para reservar memoria dentro de la memoria externa (compartida). La variable emem no se utilizará como buffer para comunicarse con el procesador principal (host), sino que apunta a una parte de la memoria externa. En la declaración de las variables podemos ver que el tamaño es de 128 (bytes) y el desplazamiento sobre la memoria (offset) tiene un valor de en hexadecimal de 0x01000000. ¿Por qué ese desplazamiento? El sistema reserva por defecto 32 MB para código y datos, siendo los primeros 16 MB para código y los siguientes 16 MB para datos. Un salto de 0x01000000 (16777216 = 16 x 220) equivale a saltarse esos 16 MB. Cuando el núcleo cree el buffer en la memoria externa (compartida), el buffer estará dentro de la memoria apuntada por emem. // Allocate a buffer in shared external memory // for message passing from eCore to host. e_alloc(&emem, _BufOffset, _BufSize); 5.5 Establecimiento de un grupo de trabajo (en el host) Antes de cargar un ejecutable en los núcleos de Epiphany III hay que crear un grupo de trabajo, es decir, indicar qué núcleos vamos a utilizar. En el siguiente código, la primera función sirve para establecer un grupo de trabajo. En el capítulo dedicado a la arquitectura del sistema vimos que sus núcleos forman una matriz 4x4. Un grupo de trabajo es un subconjunto de esa matriz y se establece indicando las coordenadas del primer núcleo, en el ejemplo (row, col), y el número de filas y columnas que tendrá, en este caso, el grupo de trabajo es tan solo de 1x1. La siguiente función sirve para poner a punto el núcleo (0, 0) dentro de ese grupo de trabajo. // Open the single-core workgroup and reset the core, in // case a previous process is running. Note that we used // core coordinates relative to the workgroup. e_open(&dev, row, col, 1, 1); e_reset_core(&dev, 0, 0); IMPORTANTE: Con la actualización de la API, esta función ya no existe. En su lugar se utiliza la función e_reset_group(), que pone a punto todos los núcleos del grupo de trabajo, de esa forma no es necesario ir uno a uno. 33
5.6 Carga de los ejecutables y ejecución (en el host) En el siguiente código, la primera función carga el ejecutable “e_hello_world.srec” en el núcleo que ocupa la posición (0, 0) dentro del grupo de trabajo dev. Otra función permite cargar el mismo ejecutable en todos los núcleos del grupo de trabajo. El parámetro E_TRUE le indica al núcleo que en cuanto tenga el ejecutable lo ejecute. En el capítulo dedicado a la librería e-hal.h veremos que en algunas ocasiones necesitaremos retardar el inicio de esa ejecución, utilizando las funciones e_start() y e_start_group() Para indicar el comienzo de la ejecución. Como vemos, tras cargar el ejecutable ejecuta la función usleep() para detener la ejecución del programa 10.000 microsegundos (10 milisegundos), permitiendo al núcleo finalizar su programa. La lectura que hace a continuación la veremos más tarde. // Load the device program onto the selected eCore // and launch after loading. e_load("e_hello_world.srec", &dev, 0, 0, E_TRUE); // Wait for core program execution to finish, then // read message from shared buffer. usleep(10000); e_read(&emem, 0, 0, 0x0, emsg, _BufSize); 5.7 Reserva de la memoria compartida (en un eCore) En la documentación oficial hace referencia a los núcleos de Epiphany como eCore. Ahora que el núcleo ha cargado el ejecutable en un núcleo y ha detenido su ejecución, vamos a ver qué ocurre en el núcleo. Para comunicarse con el host, el procesador principal, el núcleo va a crear un array de caracteres, que utilizará como buffer, en la memoria externa (compartida). Este array está situado dentro de la memoria apuntada por la variable emem del host, al principio de la memoria externa (compartida). El núcleo, para situar el buffer al inicio de la memoria externa (compartida) utiliza la palabra reservada SECTION indicando dónde quiere situarlo. En el capítulo 5 del manual del SDK aparece una lista de secciones definidas: “data_bank1”, “code_dram”, etc. char outbuf[128] SECTION("shared_dram"); 34
5.8 Escritura en la memoria compartida (en un eCore) Para pasarle los datos al host el núcleo solo debe escribir en el buffer. Para hacerlo, utiliza la función sprintf() de la librería stdio.h, que permite escribir sobre cadena los datos utilizando un determinado formato // The PRINTF family of functions do not fit // in the internal memory, so we link against // the FAST.LDF linker script, where these // functions are placed in external memory. sprintf(outbuf, "Hello World from core 0x%03x!", coreid); 5.9 Lectura de la memoria compartida (en el host) Una vez finalizada la ejecución del núcleo, el host lee la memoria externa (compartida) utilizando la variable emem, que apuntaba al inicio de esta. La función e_read() permite leer la memoria externa (compartida) o la memoria local de un núcleo. En este caso, como el primer parámetro es emem, se lee de la memoria externa (compartida). Los siguientes parámetros representan las coordenadas (0, 0), pero como no se va a leer la memoria local de un núcleo, estos dos parámetros son ignorados. El siguiente parámetro 0x0 indica el desplazamiento (offset) sobre la memoria. En este caso no hay desplazamiento, se lee al principio de la memoria externa (compartida). Por último se le pasa un array de caracteres y la cantidad de bytes a leer, que coincide con la cantidad de caracteres (1 carácter ocupa 1 byte en esta arquitectura). // Wait for core program execution to finish, then // read message from shared buffer. usleep(10000); e_read(&emem, 0, 0, 0x0, emsg, _BufSize); 5.10 Visualización por pantalla (en el host) Como ya se ha dicho, los núcleos no pueden imprimir por pantalla. En este caso, el host imprime por pantalla el mensaje recogido en la memoria externa (compartida). Por alguna razón, el autor del código ha preferido utilizar la función fprintf(), pero no porque no se pueda utilizar printf(). // Print the message and close the workgroup. fprintf(stderr, "\"%s\"\n", emsg); e_close(&dev); 35
5.11 Tareas de finalización (en el host) Como ya se ha dicho anteriormente, al inicio del bucle este programa crea un grupo de trabajo con un solo núcleo. Al final del bucle cierra o elimina el grupo de trabajo para crear otro nuevo. Para ello utiliza e_close(). Cuando el bucle finaliza, antes de finalizar la ejecución del programa, se realizan dos operaciones. La primera es liberar la memoria reservada utilizando e_free(), y después utilizar e_finalize() para finalizar la conexión con Epiphany III. // Print the message and close the workgroup. fprintf(stderr, "\"%s\"\n", emsg); e_close(&dev); } // Release the allocated buffer and finalize the // e-platform connection. e_free(&emem); e_finalize(); return 0; 5.12Utilizar este ejemplo como plantilla A la hora de crear nuestras propias aplicaciones, el ejemplo hello-world puede utilizarse como plantilla. Si cambiamos el nombre de los archivos, o si añadimos más, por ejemplo porque vayamos a utilizar diferentes ejecutables en diferentes núcleos, habrá que realizar los cambios apropiados en los archivos build.sh y run.sh. En el archivo build.sh podemos distinguir 3 líneas: La primera línea que utiliza los archivos fuentes utiliza gcc para compilar el archivo que ejecutará el procesador ARM. Si su archivo cambia el nombre habrá que modificar esta línea. El ejecutable tiene la extensión ELF. Otra línea que utiliza los ficheros fuente utiliza e-gcc, el compilador basado en gcc para compilar los ejecutables para los núcleos. La línea que utiliza e-objcopy realmente no utiliza ningún archivo fuente del programa, pero utiliza el archivo compilado con e-gcc. Si se cambia la línea anterior probablemente haya que modificar también esta línea. El archivo run.sh básicamente lanza la ejecución del archivo ELF del ARM. Si su nombre cambió habrá que modificar también este archivo. 36
6 ECLIPSE Este capítulo es, en parte, una traducción del que se puede encontrar en el manual Epiphany SDK Reference. En él se muestra cómo configurar la versión de Eclipse que Adapteva proporciona para utilizarlo como entorno de desarrollo integrado para desarrollar programas que utilicen Parallella-16. Aunque Adapteva ya no proporciona Eclipse en sus últimas actualizaciones del eSDK, este venía con la versión utilizada en este proyecto fin de grado, y parece lógico añadir este capítulo. 6.1 Abandono de Eclipse como IDE Hasta llegar a Parallella-16, Adapteva ha tenido algunos prototipos como EMEK3 y EMEK4. Con estos prototipos, la versión de Eclipse ha funcionado correctamente. Con la llegada de Parallella-16, el producto final, Adapteva advertía en su manual de referencia del eSDK que Eclipse estaba desactualizado y que tenía problemas con la sincronización con Parallella-16, y con el lanzamiento y depuración de aplicaciones. Ya hay en el FTP del proyecto Parallella versiones del eSDK más actualizadas que la utilizada para este proyecto fin de grado, y en estas versiones ya no se puede encontrar este entorno de desarrollo. 6.2 Incompatibilidad con ARM La versión modificada de Eclipse no funciona con procesadores ARM, como es el caso de Parallella-16. Está pensada para ejecutarse en un sistema Linux de 64 bits. Hoy día, crear aplicaciones sin un entorno de desarrollo parece algo del pasado, pero existen pocos o ningún entorno de desarrollo a la altura de lo que cualquiera esperaría encontrar en una arquitectura como i386 o amd64. Por eso Adapteva ha apostado por ofrecer Eclipse, aunque no se ejecute en Parallella-16. 6.3 Esquema de desarrollo El esquema de desarrollo con Eclipse y Parallella-16 es el siguiente. Si conectamos a una misma red un ordenador y Parallella-16, podemos crear y compilar las aplicaciones en el ordenador con Eclipse y ejecutarlas en Parallella-16. El eSDK cuenta con una aplicación llamada e-server que arrancada en Parallella-16 permite ejecutar aplicaciones a través de TCP/IP. Eclipse, al que previamente se le ha indicado la IP de Parallella-16, hace las funciones de cliente y lanza la aplicación, mostrando en su consola el resultado de dicha ejecución. 37
6.4 Instalar el eSDK Para instalar el eSDK, que incluye Eclipse, en un ordenador aparte de Parallella-16 se recomienda utilizar la versión Ubuntu 12.4 de 64 bits. Esta es la versión más cercana en el tiempo a cuando se escribió el manual, y cuenta con paquetes necesarios que en versiones posteriores de Ubuntu han sido sustituidos por otros. Para instalar el eSDK basta con seguir los pasos que vienen en el manual. Utilizar un terminal para ejecutar estas instrucciones: 1. Instalar los siguientes paquetes: sudo apt-get install libgmp3-dev libexpat1-dev openjdk-6-jre libmpfr-dev libmpc-dev tcsh csh g++ 2. Crear un directorio para el eSDK: sudo mkdir -p /opt/adapteva/ 3. Descargar el eSDK (* será un número que indica la versión) y descomprimir en la carpeta anterior: sudo tar xzf esdk.*.linux_x86_64.tar.gz -C /opt/adapteva/ 4. Crear un enlace simbólico al eSDK: sudo ln -sTf /opt/adapteva/esdk.* /opt/adapteva/esdk 5. Copiar y pegar estas líneas: echo 'EPIPHANY_HOME=/opt/adapteva/esdk' >> ${HOME}/.bashrc echo '. ${EPIPHANY_HOME}/setup.sh' >> ${HOME}/.bashrc Para comprobar si todo ha ido bien, hay que cerrar el terminal y volver a abrirlo, para que las modificaciones del archivo .bashrc tengan efecto. Una vez hecho esto, ejecutamos la instrucción e-gcc –version. $ e-gcc --version e-gcc (Epiphany toolchain (built 20130910)) 4.8.2 20130729 Copyright (C) 2013 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 38
6.5 Hello World con Eclipse Si hemos seguido los pasos anteriores, Eclipse se encontrará en la carpeta /opt/adapteva/esdk/tools/eclipse. Es bueno comprobarlo, hay versiones del eSDK que pueden haberlo puesto en una carpeta distinta. Es importante no hay que arrancarlo haciendo doble clic sobre él, hay que hacerlo a través del terminal, ya que de esa forma, tienen en cuenta los parámetros que se han introducido en el archivo .bashrc. Podemos arrancar Eclipse ejecutando la siguiente línea: ${EPIPHANY_HOME}/tools/eclipse/eclipse & Crear el proyecto Para crear el ejemplo vamos a File > New > C Project. Como muestra la ilustración 6-1, escribimos el título de la aplicación, eligiendo como prototipo Hello World C Project, que está dentro de Epiphany Executable (Multi Core), y pulsamos Next. Ilustración 6-9. Creación de un proyecto en Eclipse 39
En la siguiente ventana podemos seleccionar cuantos núcleos queremos utilizar, y las coordenadas del primero. Como ya se dijo en el capítulo relacionado con la arquitectura, el núcleo tiene unas coordenadas que no son las que esperamos. Hay que comprobar cuáles tiene nuestra Parallella-16. En el caso del utilizado para este trabajo fin de grado es (32, 8). En este ejemplo, como se muestra en la ilustración 6-2, se utilizarán 16 (4x4) indicando que las coordenadas del primer núcleo es (32, 8). Pulsar Next, y en la siguiente ventana Finish. Ilustración 6-10. Indicando cuantos núcleos utilizar en Eclipse El resultado es que Eclipse crea 16 proyectos (uno por cada núcleo), y otros dos. Uno con el nombre que hemos indicado, otro al que se le ha concatenado _commonlib, y los 16 restantes con el núcleo al que va dirigido (.core.row_col). 40
Configuración del proyecto A continuación hay que hacer una serie de cambios, ya que viene configurado por defecto para utilizar los prototipos EMEK3 y EMEK4, y no para Parallella-16. Haciendo clic con el botón secundario del ratón sobre el proyecto dirigido a un núcleo, hacemos clic en Properties. En la ventana desplegamos C/C++ Builder y hacemos clic en Settings. Hay que buscar Linker Description File, y veremos como el archivo utilizado por defecto es para EMEK3. La ilustración 6-3 muestra dónde se encuentra esta opción dentro de la ventana de propiedades del proyecto. Ilustración 6-11. Propiedades del proyecto en Eclipse Si se está utilizando, como en este trabajo fin de grado, Parallella-16, hay que indicarle el archivo adecuado. Dentro de la carpeta /bsps se puede encontrar carpetas que hacen referencia a otros prototipos y modelos. No es difícil encontrar el archivo legacy.ldf correcto. Hay que hacer varios pasos más si queremos que la compilación del proyecto no de errores. El primero es incluir, desde esta misma ventana de propiedades en Setting, en Libraries, la librería “e-lib”. 41
7.4 Programando en Parallella-16 Al principio, para programar accedía a Parallella-16 de forma visual y no en modo texto con PuTTY. Para ello arrancaba un servidor VNC en Parallella-16. A continuación, desde el ordenador del laboratorio que controlaba su fuente de alimentación, utilizando UltraVNC Viewer accedía de forma visual. Para cargar y descargar ficheros en Parallella-16 utilizaba WinSCP, también desde el ordenador del laboratorio. Como Parallella-16 no contaba con ningún entorno de desarrollo ni editor de texto visual que resaltara la sintaxis, salvo Emacs, la única ventaja de conectar de forma visual era el uso del ratón y la navegación mediante ventanas. Con el tiempo empecé a utilizar Emacs sin tener conocimientos por la única razón de que resaltaba la sintaxis. En cuanto aprendí a manejar ficheros, cortar y pegar, dividir la ventana y acceder al terminal sin salir de Emacs, pasé a utilizar Emacs en modo texto. Al final acabé accediendo a Parallellla-16 únicamente con PuTTY y trabajando con Emacs en modo texto, como muestra la ilustración 7-4. Ilustración 7-17. Utilizando Emacs (modo texto) desde PuTTY 48
7.5 Software utilizado El ordenador del laboratorio es un Core 2 Quad a 2.4 GHz con una RAM de 4GB. Utiliza un sistema operativo Windows 7 de 32 bits. Para conectar con Parallella-16 se ha utilizado PuTTY 0.62 y WinSCP 5.5.5. El sistema operativo Linux de Parallella-16 ha sido Linaro Nano 3.12, que venía con el eSDK 5.13.09. El servidor VNC utilizado ha sido vnc4server 4.1.1 y como editor de texto Emacs 24.3.1. Las ocasiones en las que he tenido que estar presente en el laboratorio han sido escasas. He trabajado en remoto desde casa, unas veces desde un PC y otras desde el portátil. La conexión mediante Conexión a Escritorio remoto me ha permitido trabajar igual en Windows que en Linux. La ilustración 7-5 se puede ver la conexión desde Windows 7, y la ilustración 7-6 desde Ubuntu (Linux). Utilizar en casa Linux en lugar de Windows me ha permitido utilizar Remmina, una aplicación para conexión remota que, además de permitir conectar por escritorio remoto al ordenador del laboratorio, permite conectar a Parallella-16 utilizando el ordenador del laboratorio como túnel SSH, lo cual evita la situación incómoda que se puede ver en la siguiente ilustración. Ilustración 7-18. Parallella-16 desde Windows 7 con Conexión a Escritorio remoto 49
Ilustración 7-19. Parallella-16 desde Ubuntu con Remmina 50
8 EPIPHANY HOST LIBRARY (EHAL) En este capítulo se presentan las funciones y las estructuras de datos aportadas por la librería e-hal.h (Epiphany Hardware Abstraction Layer). Esta librería se utiliza para programar los ejecutables que correrán en el host, el procesador ARM. La librería utilizada para programar los ejecutables de los núcleos es e_lib.h. Se trata de una traducción y ampliación del mismo capítulo que podemos encontrar en el manual Epiphany SDK Reference que acompaña al eSDK. 8.1 Introducción Además de utilizar la librería para programar, el compilador y el enlazador deben configurarse con las rutas a los archivos de cabecera y las librerías binarias. $ gcc -I${EPIPHANY_HOME}/tools/host/include \ -L${EPIPHANY_HOME}/tools/host/lib -le-hal ... Modo básico de operación El modo estándar de operación es trabajar con grupos de trabajo. Estos grupos son mallas rectangulares de núcleos que se establecen para realizar una tarea. Es posible cargar todos los núcleos del grupo de trabajo con la misma copia del ejecutable (estilo SPMD), o cargar subgrupos, o incluso cargar cada núcleo diferentes con ejecutables diferentes. Es responsabilidad del usuario asegurarse de que las tareas no se asignan a núcleos que ya están siendo utilizados. Arquitectura de la memoria externa (compartida) La aplicación principal en el host puede comunicarse con los núcleos accediendo a la memoria local del núcleo o mediante un buffer a la memoria externa (compartida) del dispositivo. En el capítulo dedicado a la arquitectura de la plataforma, se ha explicado cómo la memoria a la que acceden es la misma (la memoria DRAM del host), pero lo hacen utilizando vías distintas (el host con un bus del sistema y los núcleos por eLinks), y por esta razón, aunque tengan cada uno un puntero a la misma zona de memoria, sus direcciones pueden ser diferentes al estar mapeadas. Las direcciones de base (real y alias) de la memoria externa (compartida) se encuentran en el archivo HDF (Hardware Description File). La dirección base alias también se define en el archivo LDF (Linker Description File). 51
Para trabajar con valores lógicos, ya que C no cuenta con uno, se ha definido el siguiente tipo: typedef enum { E_FALSE, E_TRUE, } e_bool_t; Y para indicar el resultado de la ejecución de una función, algunas devuelven el siguiente tipo: typedef enum { E_OK, E_ERR, E_WARN, } e_return_stat_t; Los registros con los que cuenta el sistema son: // General Purpose Registers // (see Epiphany Architecture Manual for details) typedef enum { E_REG_R0, E_REG_R8, E_REG_R16, E_REG_R24, E_REG_R1, E_REG_R9, E_REG_R17, E_REG_R25, E_REG_R2, E_REG_R10, E_REG_R18, E_REG_R26, E_REG_R3, E_REG_R11, E_REG_R19, E_REG_R27, E_REG_R4, E_REG_R12, E_REG_R20, E_REG_R28, E_REG_R5, E_REG_R13, E_REG_R21, E_REG_R29, E_REG_R6, E_REG_R14, E_REG_R22, E_REG_R30, E_REG_R7, E_REG_R15, E_REG_R23, E_REG_R31, E_REG_R32, E_REG_R40, E_REG_R48, E_REG_R56, E_REG_R33, E_REG_R41, E_REG_R49, E_REG_R57, E_REG_R34, E_REG_R42, E_REG_R50, E_REG_R58, E_REG_R35, E_REG_R43, E_REG_R51, E_REG_R59, E_REG_R36, E_REG_R44, E_REG_R52, E_REG_R60, E_REG_R37, E_REG_R45, E_REG_R53, E_REG_R61, E_REG_R38, E_REG_R46, E_REG_R54, E_REG_R62, E_REG_R39, E_REG_R47, E_REG_R55, E_REG_R63, } e_gp_reg_id_t; 52
// eCore Special Registers typedef enum { // DMA registers E_REG_DMA0CONFIG, E_REG_DMA1CONFIG, E_REG_DMA0STRIDE, E_REG_DMA1STRIDE, E_REG_DMA0COUNT, E_REG_DMA1COUNT, E_REG_DMA0SRCADDR, E_REG_DMA1SRCADDR, E_REG_DMA0DSTADDR, E_REG_DMA1DSTADDR, E_REG_DMA0AUTODMA0, E_REG_DMA1AUTODMA0, E_REG_DMA0AUTODMA1, E_REG_DMA1AUTODMA1, E_REG_DMA0STATUS, E_REG_DMA1STATUS, // Event Timer Registers E_REG_CTIMER0, E_REG_CTIMER1, // Control Registers E_REG_CONFIG, E_REG_IRET, E_REG_LC, E_REG_STATUS, E_REG_IMASK, E_REG_LS, E_REG_FSTATUS, E_REG_ILAT, E_REG_LE, E_REG_PC, E_REG_ILATST, E_REG_DEBUGSTATUS, E_REG_ILATCL, E_REG_DEBUGCMD, E_REG_IPEND, // Processor Control Registers E_REG_MEMPROTECT, E_REG_MESH_CONFIG, E_REG_COREID, E_REG_CORE_RESET, } e_core_reg_id_t; 53
// Chip Registers // (see Epiphany Chip Datasheets for details) typedef enum { E_REG_IO_LINK_MODE_CFG, E_REG_IO_LINK_TX_CFG, E_REG_IO_LINK_RX_CFG, E_REG_IO_LINK_DEBUG, E_REG_IO_GPIO_CFG, E_REG_IO_FLAG_CFG, E_REG_IO_SYNC_CFG, E_REG_IO_HALT_CFG, E_REG_IO_RESET, } e_chip_reg_id_t; // Epiphany system registers // (see Board manual for details) typedef enum { E_SYS_CONFIG, E_SYS_RESET, E_SYS_VERSION, E_SYS_FILTERL, E_SYS_FILTERH, E_SYS_FILTERC, } e_sys_reg_id_t 54
8.2 Funciones de Configuración de la Plataforma Estas funciones son utilizadas para inicializar y preparar el sistema Epiphany para trabajar con él desde el host. También se utilizan para consultar y recuperar información de la plataforma: e_init() e_get_platform_info() e_finalize() 55
8.2.1 e_init() Definición int e_init( char *hdf ); Descripción Esta función inicializa las estructuras de datos de HAL (Hardware Abstraction Layer), y establece la conexión con la plataforma Epiphany. Los parámetros de la plataforma se leen desde un archivo HDF (Hardware Description File), cuya ruta se da como argumento de la función. Si se pasa como parámetro NULL, la función toma como ruta del archivo HDF la devuelta por la variable de entorno EPIPHANY_HDF. Esta variable está normalmente en el archivo de arranque (~/.bashrc), y refleja la estructura de datos de la plataforma Epiphany. Valor devuelto Si la inicialización tiene éxito devuelve E_OK. En caso contrario E_ERR. Ejemplos Todos los ejemplos hacen uso de esta función. Nota: En el momento del lanzamiento, el analizador XML no estaba todavía plenamente integrado en el controlador. En lugar de un archivo de descripción XML, la biblioteca utiliza un simple archivo de texto para listar los componentes de la plataforma. Por favor, use los archivos proporcionados o cree el suyo propio: EPIPHANY_HDF = "${EPIPHANY_HOME}/bsps/parallella/parallella.hdf" Podemos consultar el valor de esta variable de entorno escribiendo: Linaro:~> echo $EPIPHANY_HDF /opt/adapteva/esdk/bsps/current/plataform.hdf 56
8.2.2 e_get_platform_info() Definición int e_get_platform_info( e_platform_t *platform ); Descripción La información de la plataforma Epiphany está grabada internamente en un objeto de tipo e_platform_t. Contiene los datos sobre los diferentes chips, los segmentos de memoria externos y la geometría del sistema. Algunos de estos datos pueden ser recuperados a través de esta función. Valor devuelto Si la inicialización tiene éxito devuelve E_OK. En caso contrario E_ERR. Ejemplos /info – En este ejemplo se muestra cómo consultar los datos de la plataforma. VERSION : PARALLELLA1601 ROW : 32 COL : 8 ROWS : 4 COLS : 4 NUM_CHIPS : 1 NUM_EMEMS : 1 57
8.4 Funciones de Transferencia de Datos Estas funciones son utilizadas para leer y escribir con los buffer sobre la memoria externa (compartida) y sobre los núcleos de un grupo de trabajo: e_read() e_write() 64
8.4.1 e_read() Definición ssize_t e_read( void *dev, unsigned row, unsigned col, off_t from_addr, void *buf, size_t size ); Descripción Esta función lee un dato de tamaño size y lo almacena en el buffer local buf. El argumento from_addr especifica el offset. Cuando se quiera leer la memoria o los registros de un núcleo, el argumento dev será del tipo e_epiphany_t, utilizado para definir un grupo de trabajo. El núcleo será aquél con coordenadas (row, col) dentro del grupo de trabajo. Cuando se quiera leer de la memoria externa (compartida) el argumento dev será del tipo e_mem_t. Los argumentos row y col serán ignorados. Para acceder a los registros del sistema el argumento from_addr deberá ser del tipo e_gp_reg_id_t, e_core_reg_id_t, e_chip_reg_id_t, o e_sys_reg_id_t. Valor devuelto La función devolverá el número de bytes leídos. En otro caso E_ERR. Ejemplos /whoiam_emem – En este ejemplo utiliza la memoria externa (compartida) para que el núcleo comunique sus datos al host. /whoiam_bank – Es el mismo ejemplo que el anterior, pero se utiliza el banco de memoria del núcleo para comunicar los datos al host. /whoiam_reg – Es el mismo ejemplo que los anteriores, pero utiliza los registros R32 a R39 para que el núcleo comunique sus datos al host. SIEMPRE QUE SE PUEDA HAY QUE EVITAR UTILIZAR LOS REGISTROS PARA COMUNICAR DATOS, YA QUE SON UTILIZADAS INTERNAMENTE POR LA 65
PLATAFORMA Y SU MANIPULACIÓN INCORRECTA PUEDE HACER QUE SU FUNCIONAMIENTO SEA INCORRECTO. 8.4.2 e_write() Definición ssize_t e_write( void *dev, unsigned row, unsigned col, off_t to_addr, const void *buf, size_t size ); Descripción Esta función escribe un dato de tamaño size y almacenado en el buffer local buf. El argumento from_addr especifica el offset. Cuando se quiera escribir la memoria o los registros de un núcleo, el argumento dev será del tipo e_epiphany_t, utilizado para definir un grupo de trabajo. El núcleo será aquél cuyas coordenadas dentro del grupo de trabajo sean (row, col). Cuando se quiera escribir en la memoria externa el argumento dev será del tipo e_mem_t. En este caso los argumentos row y col son ignorados. Para acceder a los registros del sistema el argumento from_addr deberá ser del tipo e_gp_reg_id_t, e_core_reg_id_t, e_chip_reg_id_t, o e_sys_reg_id_t. Valor devuelto Si la escritura tiene éxito, el número de bytes escritos. En otro caso E_ERR. Ejemplos /inc_emem_buff – En este ejemplo el host coloca un número en la memoria externa (compartida) y un núcleo accede a esta memoria para incrementarlo. /inc_emem_func – Este ejemplo hace exactamente igual que el anterior, pero el núcleo, en lugar de declarar una variable en la memoria compartida, utiliza una función para leer la memoria compartida. /inc_bank – Es el mismo ejemplo que los anteriores, pero utiliza uno de los bancos de memoria para recibir e incrementar el número pasado por el host. 66
SIEMPRE QUE SE PUEDA HAY QUE EVITAR UTILIZAR LOS REGISTROS PARA COMUNICAR DATOS, YA QUE SON UTILIZADAS INTERNAMENTE POR LA PLATAFORMA Y SU MANIPULACIÓN INCORRECTA PUEDE HACER QUE SU FUNCIONAMIENTO SEA INCORRECTO. 8.5 Funciones de Control del System Estas funciones permiten controlar diferentes aspectos del sistema y la ejecución de un programa: e_reset_system() e_reset_core() e_start() e_start_group() e_signal() e_halt() e_resume() 67
8.5.1 e_reset_system() Definición int e_reset_system(); Descripción Esta función sirve para poner a punto todo el hardware de la plataforma, incluyendo los chips de la Epifanía y la lógica FPGA. Se debe tener especial cuidado cuando se utiliza esta función en un entorno de multiprocesamiento para no interrumpir la ejecución de tareas, posiblemente lanzadas por otras aplicaciones. Valor devuelto Si la función tiene éxito devuelve E_OK. En caso contrario devuelve E_ERR. Ejemplos Todos los ejemplos hacen uso de esta función. 68
8.5.2 e_start() Definición int e_start( e_epiphany_t *dev, unsigned row, unsigned col ); Descripción Esta función sirve para poner en marcha los núcleos cuando estos no han arrancado o han sido detenidos. Los parámetros row y col especifican las coordenadas del núcleo relativas al grupo de trabajo dev. Desde un punto de vista más técnico, esta función escribe la señal SYNC en el registro ILAT de un núcleo. Normalmente, esto se utilizará después de cargar un archivo ejecutable en el núcleo. Esto causa que el núcleo salte a la entrada IVT número 0. Valor devuelto Si la función tiene éxito devuelve E_OK. En caso contrario devuelve E_ERR. Ejemplos /inc_emem_buff – En este ejemplo se reserva memoria compartida para que el host pase un número a un núcleo y que este lo incremente. Se utiliza esta función para iniciar la ejecución del programa en el núcleo una vez que el host ha escrito en la memoria compartida. Si lo hiciera antes, la carga del ejecutable borraría la memoria y el dato no llegaría al núcleo. /inc_emem_func – Este ejemplo hace exactamente igual que el anterior, pero el núcleo, en lugar de declarar una variable en la memoria compartida, utiliza una función para leer la memoria compartida. 69
8.5.3 e_start_group() Definición int e_start_group( e_epiphany_t *dev ); Descripción Esta función arranca todos los núcleos del grupo de trabajo dev. Un motivo por el que la ejecución de los programas en los núcleos se retrase (indicando explícitamente que no se ejecuten) puede ser porque se quiere pasar datos por la memoria externa (compartida), ya que esta memoria parece ser borrada al cargar los ejecutables. Valor devuelto Si la función tiene éxito devuelve E_OK. En caso contrario devuelve E_ERR. Ejemplos /mutex – En este ejemplo varios núcleos intentan incrementar un número almacenado en la memoria externa (compartida). Para que los núcleos puedan leer los datos en la memoria externa debe escribirlos una vez que los núcleos han sido cargados. Esta función se utiliza en este ejemplo para arrancar la ejecución de todos los núcleos del grupo de trabajo. 70
8.5.4 e_signal() Definición int e_signal( e_epiphany_t *dev, unsigned row, unsigned col ); Descripción Esta función escribe la señal USER_INT en el registro ILAT de un núcleo del grupo de trabajo. Esto causa que el núcleo modifique el bit 9 de su IVT (Interrupt Vector Table). Los parámetros row y col especifican las coordenadas del núcleo relativas al grupo de trabajo dev. Valor devuelto Si la función tiene éxito devuelve E_OK. En caso contrario devuelve E_ERR. Ejemplos Ningún ejemplo hace uso de esta función. El registro ILAT anota todas las interrupciones de eventos. Cada bit del registro ILAT (excepto el del bit 9) está ligado a un evento específico del hardware. Se puede acceder a este registro directamente o a través de sus dos alias ILATST y ILATCL. 71
8.5.5 e_halt() Definición int e_halt( e_epiphany_t *dev, unsigned row, unsigned col ); Descripción Esta función detiene la ejecución de un núcleo. Esto puede ser útil a la hora de depurar la aplicación. Para reanudar su ejecución se utiliza e_resume(). Los parámetros row y col especifican las coordenadas del núcleo relativas al grupo de trabajo dev. Valor devuelto Si la función tiene éxito devuelve E_OK. En caso contrario devuelve E_ERR. Ejemplos /halt – En este ejemplo un núcleo incremente indefinidamente un número almacenado en su banco de memoria. Se utiliza esta función para detener el núcleo antes de leer su valor. 72
8.5.6 e_resume() Definición int e_resume( e_epiphany_t *dev, unsigned row, unsigned col ); Descripción Esta función reanuda la ejecución de un núcleo que ha sido detenido previamente con la función e_halt(). Los parámetros row y col especifican las coordenadas del núcleo relativas al grupo de trabajo dev. Valor devuelto Si la función tiene éxito devuelve E_OK. En caso contrario devuelve E_ERR. Ejemplos /halt – En este ejemplo un núcleo incremente indefinidamente un número almacenado en su banco de memoria. Se utiliza esta función para reanudar la ejecución del programa en el núcleo, detenido antes de leer el valor del número. 73
8.7.2 e_get_coords_from_num() Definición void e_get_coords_from_num( e_epiphany_t *dev, unsigned corenum, unsigned *row, unsigned *col ); Descripción Calcula las coordenadas relativas row y col del núcleo que en el grupo de trabajo dev tiene el asignado el número corenum. Valor devuelto Ninguno. Ejemplos /core_coords – En este ejemplo se listan en orden, desde 0, todos los núcleos del grupo de trabajo con sus coordenadas. Esta función se utiliza para obtener las coordenadas que ocupa cada uno de estos núcleos. 1 (0, 1) 0 (0, 0) 1 (0, 1) 2 (0, 2) 3 (1, 0) 4 (1, 1) 5 (1, 2) 6 (2, 0) 7 (2, 1) 8 (2, 2) 0 (0, 0) 1 (0, 1) 2 (0, 2) 3 (1, 0) 4 (1, 1) 5 (1, 2) 0 (0, 0) 1 (0, 1) 2 (0, 2) 3 (1, 0) 0 (0, 0) 2 (0, 2) 80
8.7.3 e_is_addr_on_chip() Definición e_bool_t e_is_addr_on_chip( void *addr ); Descripción Esta función comprueba si una dirección global de 32 bits, dada como argumento addr, está dentro del espacio de memoria del chip Epifanía. Valor devuelto Esta función devuelve E_TRUE si la dirección está dentro del chip. En caso contrario devuelve E_FALSE. Ejemplos /address – En este ejemplo se crea un puntero a una zona de memoria y un grupo de trabajo y se indica si el puntero apunta al espacio de memoria del chip y al del grupo de trabajo. Esta función se utiliza para comprobar si apunta al espacio de memoria del chip. En el ejemplo la dirección 0x80800000 apunta al inicio del núcleo 0x808 en hexadecimal, o 100000001000 en binario. Como los 6 dígitos más altos (100000) corresponden a la fila y los 6 más bajos (001000) a la columna, podemos decir que esta dirección apunta es al inicio del núcleo (32,8). En coordenadas relativas al chip, este núcleo corresponde al (0,0). Si al crear el grupo de trabajo cambiamos este núcleo y ponemos otro, veremos como el resultado cambia. En el manual Epiphany Architecture Reference se encontrar más información acerca de la distribución de la memoria. 81
8.7.4 e_is_addr_on_group() Definición e_bool_t e_is_addr_on_group( e_epiphany_t *dev, void *addr ); Descripción Esta función comprueba si una dirección global de 32 bits, dada como argumento addr, está dentro del espacio de memoria de un grupo de trabajo. El grupo de trabajo viene especificado por dev. Valor devuelto Esta función devuelve E_TRUE si la dirección está dentro del grupo de trabajo. En caso contrario devuelve E_FALSE. Ejemplos /address – En este ejemplo se crea un puntero a una zona de memoria y un grupo de trabajo y se indica si el puntero apunta al espacio de memoria del chip y al del grupo de trabajo. Esta función se utiliza para comprobar si apunta al espacio de memoria del grupo de trabajo. En el ejemplo la dirección 0x80800000 apunta al inicio del núcleo 0x808 en hexadecimal, o 100000001000 en binario. Como los 6 dígitos más altos (100000) corresponden a la fila y los 6 más bajos (001000) a la columna, podemos decir que esta dirección apunta es al inicio del núcleo (32,8). En coordenadas relativas al chip, este núcleo corresponde al (0,0). En el manual Epiphany Architecture Reference se encontrar más información acerca de la distribución de la memoria. 82
8.7.5 e_set_host_verbosity() Definición e_hal_diag_t e_set_host_verbosity( e_hal_diag_t verbose ); Descripción Esta función ajusta el nivel de detalle de funciones en el ejecutable del host. Los niveles definidos van desde H_D0 a H_D4. El nivel H_D0 no conlleva ningún tipo de diagnóstico. A partir del nivel H_D1 los diagnósticos son más detallados. Esta función está destinada al diagnóstico y a fines de depuración. Valor devuelto Esta función devuelve el nivel de diagnóstico antiguo. Ejemplos Ningún ejemplo hace uso de esta función. 83
8.7.6 e_set_loader_verbosity() Definición e_loader_diag_t e_set_loader_verbosity( e_loader_diag_t verbose ); Descripción Esta función ajusta el nivel de detalle de las funciones del ejecutable cargado en los núcleos, por encima de las funciones del ejecutable del host. Los niveles definidos van desde H_D0 a H_D4. El nivel H_D0 no conlleva ningún tipo de diagnóstico. A partir del nivel H_D1 los diagnósticos son más detallados. Esta función está destinada al diagnóstico y a fines de depuración. Valor devuelto Esta función devuelve el nivel de diagnóstico antiguo. Ejemplos Ningún ejemplo hace uso de esta función. 84
9 EPIPHANY HARDWARE UTILITY LIBRARY (ELIB) En este capítulo se presentan las funciones y las estructuras de datos aportadas por la librería e_lib.h. Esta librería se utiliza para programar los ejecutables que correrán en los núcleos. La librería utilizada para programar los ejecutables del host (el procesador ARM) es e-hal.h. Se trata de una traducción ampliada del mismo capítulo que podemos encontrar en el manual Epiphany SDK Reference que acompaña al eSDK. 9.1 Introducción Esta librería está compuesta de funciones que permiten configurar y consultar el hadrware de la arquitectura Epiphany. Estas funciones permiten automatizar muchas de las tareas de programación comunes que no son proporcionadas por los lenguajes C/C++ al tratarse de tareas específicas de la arquitectura Epiphany. En las siguientes secciones se describen las funciones de esta librería, denominada eLib en la documentación oficial. Estas funciones están divididas por familias, según su cometido. El archivo de cabecera de esta librería es “e_lib.h”, y contiene otros archivos de cabecera correspondientes a cada una de las familias en las que está dividida la API. El código de las aplicaciones diseñadas para ejecutarse en los núcleos deberán añadir la siguiente línea al inicio del código. #include "e_lib.h" También será necesario utilizar con el compilador e-gcc la opción -le-lib, con el fin de que este utilice la librería al construir el ejecutable. Como ya se ha dicho en el capítulo relacionado con la arquitectura, para utilizar los núcleos hay que crear un grupo de trabajo, un subconjunto de la malla 2D. Para que el núcleo conozca ciertos datos relacionados con la memoria y qué posición ocupa dentro del grupo de trabajo al que pertenece, cada núcleo tiene acceso a dos objetos globales. Uno llamado e_emem_config que contiene información sobre la dirección base de la memoria externa. e_emem_config.base Dirección absoluta de la memoria. 85
El otro llamado e_group_config que contiene la información sobre el tipo de chip, la posición y el tamaño de su grupo de trabajo y la posición que ocupa dentro de él. e_group_config.chiptype Tipo de chip e_group_config.group_id ID del primer núcleo e_group_config.group_row Fila del primer núcleo e_group_config.group_col Columna del primer núcleo e_group_config.group_rows Número de filas del grupo e_group_config.group_cols Número de columnas del grupo e_group_config.core_row Fila que ocupa el núcleo e_group_config.core_col Columna que ocupa el núcleo Suponiendo que definimos un grupo de trabajo como el marcado en gris, y que consultamos en el núcleo en negro estos datos, esta sería la información obtenida. (32, 8) (32, 9) (32, 10) (32, 11) (33, 8) (33, 9) (33, 10) (33, 11) (34, 8) (34, 9) (34, 10) (34, 11) (35, 8) (35, 9) (35, 10) (35, 11) e_group_config.chiptype 0 e_group_config.group_id 2121 e_group_config.group_row 33 e_group_config.group_col 11 e_group_config.group_rows 3 e_group_config.group_cols 3 e_group_config.core_row 0 e_group_config.core_col 2 Dos de estos valores necesitan una explicación: El chiptype puede tener dos valores, 0 cuando tiene 16 núcleos (E16G301) y 1 cuando tiene 64 (E64G401). El ID de los núcleos es un número binario de 12 dígitos. Los 6 bits más bajos corresponden a la fila y los otros 6 a la columna. El entero 2121 es el binario 100001001001, donde 001001 (en decimal 9) representan la columna y 100001 (en decimal 33) representan la fila. 86
Para simplificar la definición de atributos variables en C se han definido: #define ALIGN(x) __attribute__ ((aligned (x))) #define PACKED __attribute__ ((packed)) #define SECTION(x) __attribute__ ((section (x))) Se puede encontrar documentación relacionada con la creación de atributos variables en esta página: https://gcc.gnu.org/onlinedocs/gcc/Variable-Attributes.html Para trabajar con valores lógicos, ya que C no cuenta con uno, se ha definido el siguiente tipo: typedef enum { E_FALSE, E_TRUE, } e_bool_t; Y para indicar el resultado de la ejecución de una función, algunas devuelven el siguiente tipo: typedef enum { E_OK, E_ERR, E_WARN, } e_return_stat_t; 87
9.2 Funciones de acceso al sistema de registros Las funciones de acceso al sistema de registros y que permiten leer y escribir los registros de forma sencilla son: e_reg_read() e_reg_write() Los registros con los que cuenta el sistema son: // General Purpose Registers typedef enum { E_REG_R0, E_REG_R8, E_REG_R16, E_REG_R24, E_REG_R1, E_REG_R9, E_REG_R17, E_REG_R25, E_REG_R2, E_REG_R10, E_REG_R18, E_REG_R26, E_REG_R3, E_REG_R11, E_REG_R19, E_REG_R27, E_REG_R4, E_REG_R12, E_REG_R20, E_REG_R28, E_REG_R5, E_REG_R13, E_REG_R21, E_REG_R29, E_REG_R6, E_REG_R14, E_REG_R22, E_REG_R30, E_REG_R7, E_REG_R15, E_REG_R23, E_REG_R31, E_REG_R32, E_REG_R40, E_REG_R48, E_REG_R56, E_REG_R33, E_REG_R41, E_REG_R49, E_REG_R57, E_REG_R34, E_REG_R42, E_REG_R50, E_REG_R58, E_REG_R35, E_REG_R43, E_REG_R51, E_REG_R59, E_REG_R36, E_REG_R44, E_REG_R52, E_REG_R60, E_REG_R37, E_REG_R45, E_REG_R53, E_REG_R61, E_REG_R38, E_REG_R46, E_REG_R54, E_REG_R62, E_REG_R39, E_REG_R47, E_REG_R55, E_REG_R63, } e_gp_reg_id_t; 88
// eCore Special Registers typedef enum { // DMA registers E_REG_DMA0CONFIG, E_REG_DMA1CONFIG, E_REG_DMA0STRIDE, E_REG_DMA1STRIDE, E_REG_DMA0COUNT, E_REG_DMA1COUNT, E_REG_DMA0SRCADDR, E_REG_DMA1SRCADDR, E_REG_DMA0DSTADDR, E_REG_DMA1DSTADDR, E_REG_DMA0AUTODMA0, E_REG_DMA1AUTODMA0, E_REG_DMA0AUTODMA1, E_REG_DMA1AUTODMA1, E_REG_DMA0STATUS, E_REG_DMA1STATUS, // Event Timer Registers E_REG_CTIMER0, E_REG_CTIMER1, // Control Registers E_REG_CONFIG, E_REG_IRET, E_REG_LC, E_REG_STATUS, E_REG_IMASK, E_REG_LS, E_REG_FSTATUS, E_REG_ILAT, E_REG_LE, E_REG_PC, E_REG_ILATST, E_REG_DEBUGSTATUS, E_REG_ILATCL, E_REG_DEBUGCMD, E_REG_IPEND, // Processor Control Registers E_REG_MEMPROTECT, E_REG_MESH_CONFIG, E_REG_COREID, E_REG_CORE_RESET, } e_core_reg_id_t; 89
9.3.3 e_irq_mask() Definición void e_irq_mask( e_irq_type_t irq, e_bool_t state ); Descripción Esta función permite activar o desactivar un tipo de evento de interrupción, especificado por irq, modificando su respectivo bit en el registro IMASK del núcleo. Si el state es E_TRUE, los eventos de interrupción de este tipo son enmascarados. Si el state es E_FALSE, los eventos de interrupción de este tipo no son enmascarados. Valor devuelto Ninguno. 96
9.3.4 e_irq_set() Definición void e_irq_set( unsigned row, unsigned col, e_irq_type_t irq ); Descripción Esta función genera un evento de interrupción activando el bit especificado por irq de su registro ILAT. El evento es generado en el núcleo cuyas coordenadas (row, col) son relativas a su grupo de trabajo. Valor devuelto Ninguno. 97
9.3.5 e_irq_clear() Definición void e_irq_clear( unsigned row, unsigned col, e_irq_type_t irq ); Descripción Esta función limpia (elimina) las peticiones de interrupción pendiente, de tipo irq, limpiando su bit del registro ILAT. Esta función actuará en el núcleo cuyas coordenadas (row, col) son relativas a su grupo de trabajo. Valor devuelto Ninguno. 98
9.4 Funciones Timer Cada núcleo tiene dos contadores Timer. Estas funciones de la interfaz Timer permiten leer, escribir y manipular estos contadores: e_ctimer_get() e_ctimer_set() e_ctimer_start() e_ctimer_stop() e_wait() Tipos y constantes definidas relacionados con estas funciones. typedef enum { E_CTIMER_0, E_CTIMER_1, } e_ctimer_id_t; //Para ver el significado de estos valores //consultar el manual “Epiphany Architecture Reference” typedef enum { E_CTIMER_OFF, E_CTIMER_CLK, E_CTIMER_IDLE, E_CTIMER_IALU_INST, E_CTIMER_FPU_INST, E_CTIMER_DUAL_INST, E_CTIMER_E1_STALLS, E_CTIMER_RA_STALLS, E_CTIMER_EXT_FETCH_STALLS, E_CTIMER_EXT_LOAD_STALLS, } e_ctimer_config_t; 99
#define E_CTIMER_MAX 9.4.1 e_ctimer_get() Definición unsigned e_ctimer_get( e_ctimer_id_t timerid ); Descripción Lee el valor del contador timerid del núcleo que hace la llamada. Tenga en cuenta que los contadores decrecen hasta llegar a 0. Valor devuelto Devuelve el valor del contador timerid. Ejemplos /timers – En este ejemplo el núcleo utiliza un contador para calcular el tiempo transcurrido en una operación. En este ejemplo se utiliza esta función para obtener el valor del contador utilizado. 100
9.4.2 e_ctimer_set() Definición unsigned e_ctimer_set( e_ctimer_id_t timerid, unsigned val ); Descripción Establece el valor del contador timerid a val. Tenga en cuenta que los contadores decrecen hasta llegar a 0. Use la constante E_CTIMER_MAX para establecer el máximo valor permitido. Valor devuelto Devuelve el nuevo valor del contador timerid. Ejemplos /timers – En este ejemplo el núcleo utiliza un contador para calcular el tiempo transcurrido en una operación. En este ejemplo se utiliza esta función para establecer el valor del contador utilizado. 101
9.4.3 e_ctimer_start() Definición unsigned e_ctimer_start( e_ctimer_id_t timerid, e_ctimer_config_t config ); Descripción Hace que el contador timerid empiece a contar, hacia atrás, los eventos especificados por la variable config. La función establece en el registro de configuración CTIMERxCFG (la x puede ser 0 o 1, dependiendo del contador) el valor de config. Para más detalles consultar el manual Epiphany Architecture Reference. Valor devuelto Devuelve el valor actual del contador timerid. Ejemplos /timers – En este ejemplo el núcleo utiliza un contador para calcular el tiempo transcurrido en una operación. En este ejemplo se utiliza esta función para iniciar el contador utilizado. 102
9.4.4 e_ctimer_stop() Definición unsigned e_ctimer_stop( unsigned timerid ); Descripción Hace que el contador timerid se detenga. La función establece en el registro de configuración CTIMERxCFG (la x puede ser 0 o 1, dependiendo del contador) al valor E_CTIMER_OFF. Para más detalles consultar el manual Epiphany Architecture Reference. Valor devuelto Devuelve el valor actual del contador. Ejemplos /timers – En este ejemplo el núcleo utiliza un contador para calcular el tiempo transcurrido en una operación. En este ejemplo se utiliza esta función para detener el contador utilizado. 103
9.4.5 e_wait() Definición void e_wait( e_ctimer_id_t timerid, unsigned clicks ); Descripción Esta función detiene el programa el número de ciclos de reloj especificadas por clicks. Para ello, utiliza el contador timerid. Como consecuencia, se anulará cualquier conteo que se esté realizando sobre el contador timerid. Asegúrese de guardar el valor de este contador antes de llamar e_wait() si lo necesita posteriormente. Tenga en cuenta que el tiempo real (el de un reloj de pared) depende de la velocidad de reloj del chip Epifanía. Valor devuelto Ninguno. Ejemplos El ejemplo que hace uso de esta función es complejo y está relacionado con las funciones DMA. No es recomendable utilizarlo para ver cómo funciona esta función. Para ver su funcionalidad, podría utilizarse en algunos ejemplos dentro de los bucles de espera, de esa forma, en lugar de consumir ciclos de reloj mientras se cumple una condición, hace una pausa antes de comprobar si debe seguir a la espera. 104
9.5 Funciones de Movimiento de Datos y DMA Las funciones DMA controlan dos canales DMA incluidos en cada núcleo. Se utilizan para consultar el estado, la configuración y la copia de memoria usando el motor DMA. e_read() e_write() e_dma_copy() e_dma_start() e_dma_busy() e_dma_wait() e_dma_set_desc() Tipos definidos relacionadas con estas funciones: typedef enum { E_DMA_0, E_DMA_1 } e_dma_id_t; typedef struct { unsigned config; unsigned inner_stride; unsigned count; unsigned outer_stride; void *src_addr; void *dst_addr; } e_dma_desc_t; 105
9.5.6 e_dma_wait() Definición void e_dma_wait( e_dma_id_t chan ); Descripción Detiene la ejecución del programa y espera mientras canal de DMA chan está ocupado. Se puede utilizar esta función tras iniciar una transacción para asegurarse de que no se Valor devuelto Ninguno. Ejemplos Esta función se utiliza en los ejemplo que utilizan DMA después de iniciar la trasferencia con la función e_dma_start(). De esta forma, el programa no continúa hasta que la transferencia ha terminado. 112
9.5.7 e_dma_set_desc() Definición void e_dma_set_desc( e_dma_id_t chan, unsigned config, e_dma_desc_t *next_desc, unsigned stride_i_src, unsigned stride_i_dst, unsigned count_i, unsigned count_o, unsigned stride_o_src, unsigned stride_o_dst, void *addr_src, void *addr_dst, e_dma_desc_t *descriptor ); Descripción La descripción de esta función es la más compleja que se puede encontrar en este manual, debido a su extensión y a que el tema no es trivial. Como podrá comprobar, no se trata de una traducción del manual oficial. Esta función establece en descriptor los valores que se utilizarán en una transacción DMA. El canal DMA viene sera chan. Si se indica que al acabar esta transacción debe ejecutarse otra transacción, de forma encadenada, el descriptor de dicha transacción será next_desc. Podemos imaginar una transacción DMA como una copia desde el origen al destino utilizando un bucle parecido a este: for(i=0; i<count_i; i++){ addr_dst[i * stride_i_dst] = addr_src[i * stride_i_src]; } En este bucle se especifican el destino (addr_dst) y origen (addr_src) de los datos, cuántos datos deben copiarse (count_i), y cuántos bytes debe saltarse (stride_i_dst y stride_i_src) hasta el siguiente dato, ya que no ocupa lo mismo un carácter (1 byte) que un número entero (4 bytes) Si lo dicho anteriormente queda claro, es fácil entender la mayor parte de los ejemplos /dma_start. En estos ejemplos el host coloca 10 números en la 113
memoria externa (compartida) y el núcleo los copia a la memoria local mediante una transacción DMA. En los ejemplos siempre se utiliza el canal E_DMA_0. El valor de config es el resultado de hacer un OR entre todas las constantes de configuración que necesitamos para especificar qué tipo de transacción DMA es esta. Las constantes y su significado son los siguientes: E_DMA_ENABLE – Enciende el canal DMA chan. E_DMA_MASTER - Indica que el canal trabaje en modo maestro. No utilizar este modo equivale a indicar que trabaje en modo esclavo. E_DMA_CHAIN – Indica quede debe encadenar esta transacción con otra utilizando para ello el descriptor next_desc. E_DMA_STARTUP – Indica que se inicie la transferencia inmediatamente, sin necesidad de utilizar a la función e_dma_start(). E_DMA_IRQEN - Permite que se produzca una interrupción al finalizar una transferencia DMA. En el caso de interrupciones encadenadas, la interrupción se establece antes que se cargue el siguiente descriptor. E_DMA_BYTE – Indica que el tamaño de los datos es 1 byte. E_DMA_HWORD – El tamaño de los datos es half word (2 byte). E_DMA_WORD – El tamaño de los datos es word (4 byte). E_DMA_DWORD – El tamaño de los datos es double word (8 byte). De esta lista se han quitado los modos experimentales, pero se ha utilizado uno en los ejemplos y es lógico comentar su significado. E_DMA_MSGMODE – Cuando una transición acaba, se modifica un registro DMA para indicarlo. Por lo tanto, para saber si una transacción ha acabado es necesario chequear constantemente este registro. Este modo experimental permite controlar todo esto a través de la función e_dma_wait(), de manera que no es necesario chequear ningún registro. Existe un ejemplo /dma_start_master_v2 que no utiliza este modo y donde se puede ver cómo chequear la finalización de una transacción. 114
Como no queremos encadenar ninguna otra transacción, el valor para next_desc es 0x0000, que es como no pasarle nada. Como son 10 números utilizamos para el contador 0x000A (10 en hexadecimal). El segundo contador debe ser positivo, por eso se le da 0x0001. El paso utilizado, tanto en el origen como en el destino es 0x0004, ya que los números enteros ocupan 4 bytes. Por último, como el buffer en la memoria externa (compartida) apunta a dicha memoria y el array local de números apuntando a la memoria local, ya tenemos las direcciones de origen y destino para la transacción. Valor devuelto Ninguno. Ejemplos Todos los ejemplos DMA hacen uso de esta función para construir su descriptor de transacciones. 115
9.6 Funciones para Mutex y Barrera Los mutex (abreviatura de mutual exclusion) son objetos que permiten bloquear un recurso compartido, garantizando el acceso al mismo de forma exclusiva. Cuando se requiere el acceso a un recurso compartido, primero se comprueba el mutex asociado al mismo. Si el mutex está libre, el recurso también lo está. El proceso que adquiera el control del mutex tiene garantiza su acceso al recurso compartido mientras no libere el mutex. Las barrerras se utilizan para sincronizar hilos de ejecución paralelos. Si se carga un programa con una barrera en varios núcleos, cuando un núcleo llega a una barrera se detiene y espera a que el resto de núcleos lleguen a la misma barrera. Entonces todos los núcleos continúan con su ejecución. Para los mutex y barreras se han definido los siguientes tipos: typedef int e_mutex_t; typedef int e_mutexattr_t; typedef char e_barrier_t; Las funciones para mutex son: e_mutex_init() e_mutex_lock() e_mutex_trylock() e_mutex_unlock() Las funciones para barreras son: e_barrier_init() e_barrier() 116
9.6.1 e_mutex_init() Definición void e_mutex_init( unsigned row, unsigned col, e_mutex_t *mutex, e_mutexattr_t *attr ); Descripción Esta función inicializa mutex en el núcleo (row, col) del grupo de trabajo. Una vez inicializado el estado de mutex es libre. El atributo de inicialización, especificado por attr, está reservado para uso futuro. Cuando se use esta función, utilice NULL. Valor devuelto Ninguno. Ejemplos /mutex – En este ejemplo varios núcleos se incrementan un valor almacenado en la memoria externa (compartida). Esta función se utiliza para inicializar mutex en el propio núcleo. 117
9.6.2 e_mutex_lock() Definición void e_mutex_lock( unsigned row, unsigned col, e_mutex_t *mutex ); Descripción Esta función intenta adquirir el control de mutex en el núcleo (row, col) del grupo de trabajo. Si el control del mutex ha sido adquirido por otro núcleo, detiene su ejecución hasta adquirir el control de mutex. Valor devuelto Ninguno. Ejemplos /mutex – En este ejemplo varios núcleos se incrementan un valor almacenado en la memoria externa (compartida). Esta función se utiliza para inicializar adquirir o bloquear mutex, de forma que se tenga acceso exclusivo al valor que se quiere incrementar. Si eliminamos esta función, todos los núcleos intentan acceder a la vez al mismo recurso. Esto provoca que varios núcleos puedan leer el mismo valor, intentando posteriormente escribir el mismo valor incrementado. Como consecuencia, el valor final es menor al esperado. Pruebe a comentar esta función en el ejemplo para ver sus consecuencias. 118
9.6.3 e_mutex_trylock() Definición unsigned e_mutex_trylock( unsigned row, unsigned col, e_mutex_t *mutex ); Descripción Esta función intenta adquirir el control mutex en el núcleo (row, col) del grupo de trabajo. Si el control de mutex ha sido adquirido por otro núcleo, no se queda a la espera de adquirir el control de mutex, continua su ejecución. Valor devuelto Si tiene éxito al adquirir el control de mutex, la función devuelve 0. En otro caso, devuelve el ID del núcleo que lo controla. Ejemplos Ningún ejemplo hace uso de esta función. Es posible modificar el ejemplo /mutex para incrementar el valor sólo cuando esta función no devuelva 0, es decir, cuando no se consiga obtener el control del mutex. De esa forma, contabilizaríamos cuántas de veces se intenta sin éxito obtener el control del mutex. Pero cuidado, estaríamos contabilizándolo utilizando un recurso compartido sin control, por lo que el resultado real será probablemente mayor. 119
9.6.4 e_mutex_unlock() Definición void e_mutex_unlock( unsigned row, unsigned col, e_mutex_t *mutex ); Descripción La función liberar el control de mutex en el núcleo (row, col) del grupo de trabajo. Valor devuelto Si tiene éxito, la función devuelve 0. En otro caso, devuelve un valor distinto. Ejemplos /mutex – En este ejemplo varios núcleos se incrementan un valor almacenado en la memoria externa (compartida). Esta función se utiliza para liberar el mutex, de forma que otros núcleos tengan acceso exclusivo al valor que se quiere incrementar. Si eliminamos esta función, los demás núcleos quedan bloqueados a la espera de que mutex sea liberado, y lo que es más curioso, el propio núcleo queda bloqueado en la siguiente iteración del bucle, ya que intentará adquirir un recurso bloqueado por el mismo. 120
9.6.5 e_barrier_init() Definición void e_barrier_init( volatile e_barrier_t bar_array[], e_barrier_t *tgt_bar_array[] ); Descripción Esta función inicializa una barrera en el grupo de trabajo. Los parámetros bar_array y tgt_bar_array son arrays de un tamaño igual al número de núcleos en el grupo de trabajo. La barrera es la misma para todos los núcleos en el grupo de trabajo, por lo que se debe tener cuidado al colocar la llamada e_barrier(), para evitar las condiciones de estancamiento. Valor devuelto Ninguno. Ejemplos /barrier – En este ejemplo se van iniciando uno a uno todos los núcleos. El programa en el núcleo utiliza una barrera, de forma que, aunque cada núcleo empieza en un momento diferente, hasta que todos no llegan a la barrera, los núcleos no pueden terminar con la ejecución del programa. Esta función se utiliza para inicializar la barrera. 121
9.7.4 e_coords_from_coreid() Definición void e_coords_from_coreid( e_coreid_t coreid, unsigned *row, unsigned *col ); Descripción Calcula las coordenadas (row, col) del núcleo cuyo Core ID es coreid dentro del grupo de trabajo. Tenga en cuenta que la función no comprueba que el Core ID pertenece a un núcleo del grupo de trabajo. Si el núcleo no pertenece al grupo de trabajo el valor devuelto puede ser negativo o superior al tamaño del grupo. Valor devuelto Ninguno. Ejemplos /mutex – En este ejemplo el host carga un programa en varios núcleos que incrementan el valor de una variable en la memoria externa (compartida). En este ejemplo se usa esta función para obtener las coordenadas del núcleo, necesarias para iniciar el mutex. /barrier – En este ejemplo se van iniciando uno a uno todos los núcleos. El programa del núcleo utiliza una barrera, de forma que, aunque cada uno empieza en un momento diferente, hasta que todos no llegan a la barrera no terminan. En este ejemplo se usa esta función para obtener las coordenadas, necesarias para calcular su número dentro del grupo de trabajo. 128
9.7.5 e_is_on_core() Definición e_bool_t e_is_on_core( const void *ptr ); Descripción Esta función comprueba si la dirección apuntada por ptr está dentro del espacio de memoria correspondiente al núcleo que hace la llamada. Valor devuelto Devuelve E_TRUE si la dirección está dentro del espacio de memoria correspondiente al núcleo que hace la llamada y E_FALSE en caso contrario. Ejemplos Ningún ejemplo hace uso de esta función. Puede probar esta función en el ejemplo /cajero, donde se calcula a partir de un puntero que apunta a la memoria local, la dirección a la memoria local de otro núcleo. 129
9.7.6 e_neighbor_id() Definición void e_neighbor_id( e_coreid_wrap_t dir, e_coreid_wrap_t wrap, unsigned *row, unsigned *col ); Descripción Esta función solo se puede utilizar cuando el número de núcleos del grupo de trabajo es potencias de 2, es decir, 2n: 1, 2, 4, 8, 16… Esta función calcula las coordenadas (row, col) del núcleo vecino. El parámetro dir (E_NEXT_CORE, E_PREV_CORE) indica si se quieren el vecino siguiente o el anterior. Los vecinos dependen del valor de wrap, que indica el tipo de encadenamiento de los núcleos, que puede ser: E_COL_WRAP – Recorre las columnas de arriba a abajo. E_ROW_WRAP – Recorre las filas de izquierda a derecha. E_GROUP_WRAP – Recorre las filas de izquierda a derechas, y llegado al final de la fila, pasa a la fila siguiente. Las ilustración 9-1 muestra el sentido en el que avanzan los núcleos utilizando E_COL_WRAP y E_ROW_WARP, y la ilustración 9-2 al utilizar E_GROUP_WRAP. Ilustración 9-20. E_COL_WRAP y E_ROW_WRAP 130
Ilustración 9-21. E_GROUP_WRAP Valor devuelto Ninguno. Ejemplos /cajero – En este ejemplo se simula un cajero automático del que se quiere sacar un importe determinado. Esta función la utilizan los núcleos para determinar las coordenadas del siguiente núcleo. En el ejemplo se utiliza una configuración E_GROUP_WRAP. 131
132
10 CONCLUSIONES En este capítulo expreso las conclusiones de este trabajo fin de grado, esto es, experiencia personal, líneas de investigación futuras y mi opinión acerca del proyecto Parallella. 10.1Experiencia personal Lo primero que destacaría sería la incomodidad de no contar con un entorno de desarrollo para programar. En el foro oficial se puede ver que hay gente pidiendo ayuda sobre cómo configurar Eclipse. Hay quien al ver las limitaciones de Eclipse respecto a Parallella-16, que solo es compatible con los prototipos y que Adapteva ha dejado de proporcionar Eclipse, pasan directamente a programar, compilar y ejecutar desde la línea de comandos. En mi caso, como se puede ver en la ilustración 10-1, opté por utilizar Emacs en Parallella-16, el único editor de texto en modo visual que venía instalado y resaltaba la sintaxis. Con el tiempo pasé a utilizarlo en modo texto. Aunque he llegado a apreciar sus muchas virtudes, hay que ser consciente de que hoy en día, los programadores esperan contar con algo como Eclipse o NetBeans. Ilustración 10-22. Emacs (modo visual) como entorno de programación 133
Ha sido difícil investigar, averiguar y probar cómo utilizar las funciones de las librerías, y eso que están documentadas. Ofrecer un ejemplo simple es fundamental para entender la función y el contexto en el que se utiliza. Es lo que he intentado con el conjunto de ejemplos que he creado. El hecho de que el programador de Parallella-16 no pueda ignorar su arquitectura, y que deba conocer por ejemplo los detalles de su memoria, son incomodidades que se resuelven acudiendo a sus manuales, pero deducir de ellos que para reservar memoria compartida con una función se deben saltar 16777216 bytes, es algo que solo te resuelve alguien con muchos conocimientos en el foro oficial. Yo lo explico cuando describo la función y en el capítulo Hello World, que es como una introducción a la programación en esta arquitectura. Es algo que todo el mundo debería conocer si piensa programar en Parallella-16. Por último, me alegro de haber realizado este trabajo fin de grado, me ha puesto a prueba. En demasiadas ocasiones he pensado que estaba a punto de terminar, y me encontraba con un nuevo problema. Para investigar hay que tener tiempo, paciencia, perseverar y no dar por hecho nada. Ojalá tanto esfuerzo haya merecido la pena. 10.2Líneas de investigación futuras Son muchas las cosas que no he tocado en este trabajo fin de grado: Depuración. No he probado ni documentado la depuración en Parallella-16. Sin la depuración, los núcleos son auténticas cajas negras donde uno no sabe que está ocurriendo. Por otro lado, la depuración de un programa ejecutado en 16 núcleos requiere de un enfoque distinto. Alguien podría completar este trabajo añadiendo algo al respecto, el siguiente paso de Adapteva es sacar Parallella-16, con 64 núcleos. Su enfoque de la depuración no será el tradicional. Varias Parallella-16. En la fecha en la que se escribieron están líneas no era posible adquirir nuevas Parallella-16 por estar agotadas. Yo solo he contado con una. El potencial que tiene poder acoplar varios de estos ordenadores es digno de estudio y de un trabajo fin de grado. Librerías adicionales. Como forma de facilitar la programación se podría aportar nuevas librerías. En el foro hay quien propone una que permita compartir datos de forma más sencilla entre el host (procesador ARM) y los núcleos. Video-tutoriales. A veces resulta más fácil explicar algo mostrándolo. No existen videos relacionados con la programación de Parallella-16. Los vídeos relacionados con este ordenador son tan escasos que resulta difícil imaginar que esto no se le haya ocurrido a los responsables de Adapteva. 134
Comparativas. Empiezan a verse Raspberry Pi ejecutándose en paralelo. Podría realizarse una comparativa a cerca del rendimiento y la potencia de ambos como alternativa a los superordenadores tradicionales. Parallella-16 como base de un proyecto. El tamaño, el peso, la potencia consumida y su precio lo hacen apto para un montón de proyectos donde estas variables dificultan su desarrollo. Pienso en cosas como la robótica, los drones, el espacio, etc. 10.3Mi opinión sobre el proyecto Parallella Creo que Adapteva con Parallella-16 ha dado un gran paso, cualquier laboratorio puede contar con un ordenador con 64 núcleos por el precio de un ordenador de sobre mesa. Pero Adapteva se ha olvidado de una cosa, poner las cosas fáciles. Parallella-16 es como un filón de oro perdido en la montaña, me da la sensación que la comunidad de investigadores desconoce que existe, o ha sido incapaz de hacer algo con él. Por alguna de estas razones, o ambas, no se ha visto aún a nadie hacer nada novedoso o basado en este ordenador. Si yo fuera el director de Adapteva lo primero que haría sería intentar vender a cada laboratorio de investigación de las universidades de Estados Unidos 4 Parallella-16, su precio es ridículo, y los beneficios serían altos. Estoy seguro que con este dinero podría financiar los nuevos modelos y pagar a gente que documentara adecuadamente su uso y creara un conjunto de videos tutoriales y promocionales que colgados en Internet darían sus frutos. Con la documentación actual, que está desactualizada, algo que ellos mismos reconocen, con 4 videos en YouTube, con un foro en el que se ve que la gente está muy perdida, y un canal de IRC que ha sido declarado ya en alguna ocasión congelado por no haber actividad en él durante casi 2 semanas, por muy buena que sea Parallella-16, el proyecto Parallella está abocado al fracaso. 135
136
11 ÍNDICE DE ILUSTRACIONES Ilustración 2-1. Evolución prevista de la arquitectura de ordenadores.......................14 Ilustración 2-2. Parallella-16 sobre la palma de una mano.........................................16 Ilustración 2-3. Componentes de Parallella-16...........................................................16 Ilustración 2-4. Parte inferior de Parallella-16.............................................................17 Ilustración 2-5. Conexión entre componentes de Parallella-16..................................17 Ilustración 4-6. Arquitectura multi-núcleo Epiphany....................................................25 Ilustración 4-7. Memoria de la arquitectura Epiphany.................................................26 Ilustración 4-8. Malla 2D de cuatro Parallellas-16......................................................27 Ilustración 6-9. Creación de un proyecto en Eclipse...................................................39 Ilustración 6-10. Indicando cuantos núcleos utilizar en Eclipse..................................40 Ilustración 6-11. Propiedades del proyecto en Eclipse...............................................41 Ilustración 6-12. Ejecución del e-server......................................................................42 Ilustración 6-13. Eclipse en modo depuración ejecutando hello-world.......................43 Ilustración 7-14. Configuración PC y Parallella-16.....................................................46 Ilustración 7-15. Relé USB con los cables de alimentación.......................................47 Ilustración 7-16. Parallella-16 con un disipador de calor............................................47 Ilustración 7-17. Utilizando Emacs (modo texto) desde PuTTY.................................48 Ilustración 7-18. Parallella-16 desde Windows 7 con Conexión a Escritorio remoto. 49 Ilustración 7-19. Parallella-16 desde Ubuntu con Remmina.......................................50 Ilustración 9-20. E_COL_WRAP y E_ROW_WRAP.................................................130 Ilustración 9-21. E_GROUP_WRAP.........................................................................131 Ilustración 10-22. Emacs (modo visual) como entorno de programación................133 137
/barrier - Este ejemplo utiliza las funciones relacionadas con las barreras. En este ejemplo se van iniciando uno a uno todos los núcleos. El programa del núcleo utiliza una barrera, de manera que, aunque cada uno empieza en un momento diferente, hasta que todos no llegan a la barrera no terminan. /cajero - Este ejemplo simula un cajero automático, en el sentido de que calcula cuantos billetes y de qué valor debe devolver si se le pide un importe determinado. Este ejemplo hace uso de las funciones que permiten conocer los núcleos vecinos. /core_coords - En este ejemplo se listan todos los núcleos de un grupo de trabajo junto a sus coordenadas. Este ejemplo demuestra cómo obtener en el host, las coordenadas de un núcleo conociendo su número. /core_number - En este ejemplo se crea un grupo de trabajo y se saca por pantalla una representación visual del chip (16 núcleos dispuestos 4x4). Este ejemplo demuestra cómo obtener en el host, el número de un núcleo conociendo sus coordenadas. Hay 4 ejemplos donde los núcleos utilizan las funciones DMA para obtener una lista de 10 números para sumarlos. o/dma_copy - Esta versión utiliza la función e_dma_copy(), esta función no necesita declarar un descriptor de transacción. o/dma_start_master - Esta versión utiliza un descriptor con los modos MASTER y MSGMODE. o/dma_start_master_v2 - Esta versión utiliza un descriptor solo con el modo MASTER. o/dma_start_slave - Esta versión utiliza un descriptor sin el modo MASTER, lo que significa implícitamente que trabaja en modo SLAVE. /halt – La función e_halt() sirve para detener un núcleo. En este ejemplo un núcleo incremente indefinidamente un número almacenado en su banco de memoria. Se utiliza esta función para detener el núcleo antes de leer su valor. Hay 3 ejemplos que demuestran cómo pasar/recibir datos entre host y núcleo: Utilizando los bancos de memoria de los núcleos o la memoria compartida. El ejemplo pasa un número y un núcleo lo incrementan. o/inc_bank – Utiliza el banco de memoria del núcleo. o/inc_emem_buff – Accede a la memoria compartida con un buffer. o/inc_emem_func – Accede a la memoria compartida con una función. 144
/info – Muestra como consultar los datos de la plataforma en la que se ejecuta el programa: Versión del chip, cuántos chips tiene, número de filas y columnas, coordenadas del primer núcleo. /mutex – En este ejemplo se muestra cómo los núcleos pueden declarar y utilizar mutex. En el ejemplo varios núcleos intentan incrementar a la vez un número. Para evitar problemas de concurrencia se utiliza el mutex. /timers – En este ejemplo se utilizan las funciones relacionadas con los contadores internos de los núcleos. El ejemplo calcula el tiempo transcurrido por una operación que realiza el núcleo. Hay 3 ejemplos que demuestran cómo recibir datos en el host desde los núcleos: Utilizando los bancos de memoria y los registros de los núcleos, o la memoria compartida. El ejemplo devuelve información sobre un núcleo. o/whoiam_bank – Utilizando el banco de memoria del núcleo. o/whoiam_emem – Utilizando la memoria externa (compartida). o/whoiam_reg – Utilizando los registros del núcleo. 145
146
147