Codiseño Hardware/Software Ingeniería Superior en Informática Tutorial Handel-C y MicroBlaze Pablo Huerta Pellitero ÍNDICE 1- Estructura de directorios para el periférico 2- Diseño del periférico en Handel-C 3- Creación de un proyecto en DK para el periférico 4- Añadiendo el periférico al sistema MicroBlaze 5- Aplicación de prueba 6- Un periférico más complejo 1- Estructura de directorios para el periférico Para crear un nuevo periférico en Handel-C y poder integrarlo en un diseño con el procesador MicroBlaze son necesarios una serie de archivos y una estructura de directorios dentro del proyecto de Xilinx Platform Studio, como la que se muestra en la figura. El directorio pcores es un directorio que existe en todo proyecto de Xilinx Platform Studio. Dentro de ese directorio, se debe crear un directorio por cada periférico que se vaya a crear. En este ejemplo se va a crear un periférico para controlar los dos leds de la placa RC200/RC203. Se llama por tanto al directorio que va a contener el periférico con el nombre del periférico más un número de versión, en este ejemplo led_driver_v1_00_a. Dentro de este directorio, deben existir dos directorios: data y netlist: El directorio data debe contener dos archivos, con extensiones .bbd y .mpd. El nombre de los archivos debe ser nombre_periférico_v2_1_0.extension: en este ejemplo serían led_driver_v2_0_0.bbd y led_driver_v2_0_0.mpd. Estos archivos no se deben completar hasta llegar al final del punto 3 de este tutorial. El archivo .bbd contiene simplemente la ruta al archivo EDF que implementa la funcionalidad del periférico, relativa al directorio netlist. En el apéndice A se incluye una plantilla para este tipo de archivo. El archivo .mpd contiene información sobre el nombre del periférico, parámetros, señales que se conectarán al bus OPB, y puertos de entrada o salida al exterior que tiene el periférico. En el apéndice B se incluye una plantilla genérica para este archivo, donde sólo se deben introducir los valores de nombre, direcciones base y final donde se mapeará el periférico y en caso de tener el periférico puertos de entrada o salida, el nombre, tamaño y dirección de estos siguiendo la siguiente nomenclatura: o PORT nombre = “” , DIR = dirección , VEC = [num1:num2] nombre: nombre del puerto. Debe coincidir con el nombre que se le de al puerto en el diseño en Handel-C que se explicará más adelante. dirección: puede valer IN, OUT o INOUT según el puerto sea de entrada, salida o entrada-salida. num1:num2: este parámetro es opcional, y sólo se usa en el caso de que el puerto sea de varios bits de anchura. Por ejemplo, para un puerto de 8 bits, se tendría: VEC = [0:7] El directorio netlist contendrá los archivos del periférico que se generarán desde Handel-C. Una vez creada esta estructura de directorios, ya se puede pasar a diseñar el periférico en Handel-C. 2- Diseño del periférico en Handel-C Handel-C proporciona una librería para poder diseñar periféricos conectables al bus OPB de forma rápida y sencilla. El primer ejemplo que se va a mostrar en este tutorial es un sencillo periférico que permite controlar los dos leds que hay en las placas de prototipado RC200/RC203. El periférico consistirá en un registro de 32 bits accesible desde el procesador MicroBlaze. Los dos últimos bits del registro serán los que enciendan o apaguen los leds de la placa. En el apéndice C, se incluye el código de este periférico, que se explica a continuación: Lo primero que aparece en el código, es la inclusión de un archivo de cabecera, “opb_bus_slave.hch” que es el que proporciona las funciones necesarias para comunicarse con el bus OPB. A continuación aparecen las siguientes líneas: DECL_OPB_BUS (opb_bus); set clock = OPB_BUS_CLOCK (opb_bus); Estas sentencias son comunes a cualquier periférico en Handel-C que vaya a ser conectado a un bus OPB. Simplemente declaran un bus OPB, y conectan el reloj del periférico al reloj del bus. A continuación se declaran dos macro procesos WriteProc y ReadProc: macro proc WriteProc (Address, DataIn, ByteEnable, Ready, myData); macro proc ReadProc (Address, DataOut, Ready, myData); Estos dos procesos son los que describirán más adelante las acciones que debe realizar el periférico cuando se reciba un petición de escritura o lectura en el periférico por parte del bus. A continuación, se define un nuevo tipo de datos de tipo struct y llamado Datos_Periferico. Esta estructura incluirá todos los elementos necesarios del periférico, como registros direccionables del periférico, registros auxiliares, etc. En este ejemplo el periférico sólo tendrá un registro direccionable: typedef struct { unsigned int 32 Registro; } Datos_Periferico; A continuación, se describe el funcionamiento del proceso de escritura. Este proceso, es el que se ejecuta cuando el bus intenta escribir en el periférico. Recibe varios parámetros del bus: dirección, dato de entrada, byte enable, y myData que es una referencia a la estructura de datos del periférico descrita en el punto anterior. El parámetro Ready es un parámetro de escritura que se debe poner a ‘1’ cuando se terminen de hacer las actividades relacionadas con la escritura. macro proc WriteProc (Address, DataIn, ByteEnable, Ready, myData) { par { myData.Registro = DataIn; Ready = 1; } } En este ejmplo, el proceso de escritura simplemente copia el valor de DataIn del bus en el único registro direccionable del bus y se activa la señal de Ready, independientemente del valor de Address. Si hubiese varios registros direccionables, se tendría que escribir en uno u otro dependiendo del valor de Address. El siguiente código es el del proceso de lectura, que es el que se ejecuta cuando el bus realiza una operación de lectura sobre el periférico. Como solo hay un registro direccionalbe, simplemente se escribe el valor de ese registro en DataOut y se activa la señal de Ready. macro proc ReadProc (Address, DataOut, Ready, myData) { par { DataOut = myData.Registro; Ready = 1; } } Una vez descritos estos procesos, se pasa al método main() del periférico. En éste, lo primero que se hace es declarar una variable del tipo de datos Datos_Periferico descrito anteriormente. Datos_Periferico myDataStructure; Luego se declara un interfaz de salida, que tendrá dos bits de ancho y de nombre OutLeds. En este puerto de salida se escriben los dos últimos bits del registro del periférico: interface port_out () MyLedDriver (unsigned 2 OutLeds = myDataStructure.Registro<-2) with { busformat="B(N:0)" }; Por último, se lanzan en paralelo dos procesos que proporciona la librería: RunOPBBus, y RunOPBSlave. Éste último recibe como parámetros el bus opb creado al principio del archivo, las macros de escritura y lectura, la variable myDataStructure creada previamente, y las direcciones base y alta donde irá mapeado el periférico: par { RunOPBBus (opb_bus); RunOPBSlave (opb_bus, WriteProc, ReadProc, myDataStructure, 0x70000000, 0x700000FF); } Si el periférico tuviese algún funcionamiento extra, este se describiría con un macro proceso que también se lanzaría en paralelo con estos dos últimos. Esto se mostrará en el segundo ejemplo. En el siguiente punto, se explicará como crear un proyecto en Handel-C en el que crear este periférico. 3- Creación de un proyecto en DK para el periférico Para poder implementar el periférico que se ha explicado en el punto anterior, es necesario crear un proyecto en Handel-C desde la herramienta DK de Celoxica, desde el que poder compilar el código del periférico: El proyecto se creará para un chip génerico, y más adelante se cambiará en las opciones para que coincida con la FPGA que vamos a utilizar. Se añade al proyecto un archivo de código Handel-C, en el que se copiará el código que se incluye en el Apéndice Una vez añadido el archivo, se procederá a cambiar las opciones del proyecto. Lo primero que se debe cambiar es el modelo de FPGA que se va a utilizar. En la siguiente imagen se muestran las opciones que se deben introducir si se está utilizando la placa de prototipado RC203 de Celoxica: Si se está utilizando la placa RC200, los valores que se deben introducir son: En la pestaña Linker se debe indicar que librerías se quieren enlazar, y la ruta de estas librerías, que es: “C:\archivos de programa\celoxica\pdk\hardware\lib”: Una vez cambiadas las opciones del proyecto, ya se puede contruir para EDIF el diseño Una vez construido el diseño, se habrá creado un directorio de nombre EDIF. Dentro de este directorio hay 3 archivos con extensiones .edf .hco y .ncf. Estos 3 archivos se deben copiar al directorio pcores/led_driver_v1_00_a/netlist del proyecto de EDK, explicado en el primer punto. Ahora que ya se conoce el nombre del periférico, que puertos tiene, direcciones donde se mapeará, etc ya se pueden completar los archivos .bbd y .mpd mencionados en el primer punto. La estructura final del directorio pcores del proyecto EDK debería ser: -pcores -led_driver_v1_00_a -data led_driver_v2_1_0.bbd led_driver_v2_1_0.mpd -netlist led_driver.edf led_driver.hco led_driver.ncf Una vez hechos estos pasos, se puede pasar a incluir el nuevo periférico en el diseño con MicroBlaze. 4- Añadiendo el periférico al sistema MicroBlaze Una vez arrancada la herramienta Xilinx Platform Studio, se puede abrir el proyecto usado en el tutorial anterior sobre MicroBlaze, o crear un proyecto nuevo. En este ejemplo se va a continuar con el sistema que ya se había utilizado en el tutorial previo. Para que el nuevo periférico aparezca en la lista de periféricos disponibles para incluir en el diseño, la estructura del directorio pcores , mencionada en el punto anterior, debe estar preparada y con todos los archivos necesarios ya creados. Seleccionando Project>Rescan User Repositories la herramienta buscará en ese directorio los nuevos periféricos que se han añadido. Una vez hecho esto, en la lista de periféricos del Ip Catalog ya debe aparecer el nuevo periférico que se ha desarrollado: Como se ha hecho ya en el tutorial anterior sobre MicroBlaze, añadimos el periférico, y lo conectamos al bus OPB. En el filtro Ports, se conecta el puerto de reset del periférico al reset del sistema, y se hace externo el puerto OutLeds. Y por último, en el filtro Addresses se asignan las direcciones al periférico que se habían utilizado en el diseño en Handel-C, que eran 0x70000000 y 0x700000FF. Por último, solo queda añadir al archivo de restricciones system.ucf los pines del periférico, mediante las dos siguientes líneas: NET led_driver_0_OutLeds_pin<0> LOC = L8; #Para RC200 LOC = J6 NET led_driver_0_OutLeds_pin<1> LOC = M8; #Para RC200 LOC = K6 Una vez realizados estos pasos, se puede generar el bitstream desde Hardware>Generate Bitstream. En unos minutos, si no ha habido errores, el diseño estará ya sintetizado, y se podrá pasar a escribir una aplicación de prueba para el periférico. 5- Aplicación de prueba Para probar el correcto funcionamiento del nuevo periférico, se incluye en el apéndice D el código C de una sencilla aplicación que enciende y apaga los leds alternativamente primero, y simultáneamente después. Basta con crear una nueva aplicación software con este código, cambiando las opciones de compilación para que se compile sin optimización, y posteriormente inicializar la memoria de bloque con el ejecutable generado y descargar el fichero de programación a la placa, tal como se explicaba en el tutorial anterior sobre MicroBlaze. 6- Un periférico más complejo En este punto se va a presentar un periférico un poco más complejo que permita entender mejor el concepto de registros direccionables del periférico, así como que realice algún tipo de operación. El primer periférico visto en el tutorial, solamente tenía un registro direccionable, y no realizaba ninguna acción: simplemente sacaba a una interfaz externa los dos últimos bits del registro. El nuevo periférico que se va a estudiar ahora, es un sencillo sumador/restador, que tendrá los siguientes registros internos: Reg_1: registro del primer operando. Reg_2: registro del segundo operando Reg_result: registro del resultado Reg_opmode: registro que indica que operación se debe realizar Los registros estarán mapeados en la memoria en las direcciones: Reg_1 => 0x10000000 Reg_2 => 0x10000004 Reg_result => 0x10000008 Reg_opmode => 0x1000000C En el apéndice E se incluye el código Handel-C de este periférico. Se explicarán a continuación las partes más relevantes del código. Estructura de datos para almacenar los registros internos del periférico: typedef struct { unsigned int unsigned int unsigned int unsigned int } Datos_Periferico; 32 32 32 32 Reg_1; Reg_2; Reg_result; Reg_opmode; Como el periférico va a tener 4 registros, se declaran dentro de la estructura de datos Datos_Periferico. Proceso de escritura: macro proc WriteProc (Address, DataIn, ByteEnable, Ready, myData) { par { if(Address == 0x10000000) myData.Reg_1 = DataIn; else if (Address == 0x10000004) myData.Reg_2 = DataIn; else if (Address == 0x10000008) delay; //no permitimos escribir en el registro de resultado else if (Address == 0x1000000C) myData.Reg_opmode = DataIn; else delay; //para que todos los caminos duren un ciclo Ready = 1; } } Como el periférico tiene varios registros direccionables, escribiremos en uno u otro dependiendo del valor de la señal Address. Proceso de lectura: macro proc ReadProc (Address, DataOut, Ready, myData) { par { if(Address == 0x10000000) DataOut = myData.Reg_1; else if (Address == 0x10000004) DataOut = myData.Reg_2; else if (Address == 0x10000008) DataOut = myData.Reg_result; else if (Address == 0x1000000C) DataOut = myData.Reg_opmode; else delay; //para que todos los caminos duren un ciclo Ready = 1; } } Al igual que en escritura, como el periférico tiene varios registros direccionables, se escribirá en DataOut el valor de uno u otro dependiendo del valor de la señal Address. Proceso que realiza la operación: macro proc Operacion(myData) { while(1) { if(myData.Reg_opmode == 0x1) myData.Reg_result = myData.Reg_1 + myData.Reg_2; else if(myData.Reg_opmode == 0x2) myData.Reg_result = myData.Reg_1 - myData.Reg_2; else delay; // todos los caminos duran un ciclo } } Este proceso realiza la operación de suma o resta, según lo que valga el código de operación. El proceso correrá indefinidamente, y escribirá el resultado en el registro de resultado. Proceso main: void main (void) { Datos_Periferico myDataStructure; par { RunOPBBus (opb_bus); RunOPBSlave (opb_bus, WriteProc, ReadProc, myDataStructure, 0x10000000, 0x100000FF); Operacion(myDataStructure); } } Como este periférico no tiene ninguna interfaz externa a la FPGA, no hay declaraciones de ese tipo. Al igual que en el ejemplo anterior, se crea una variable del tipo de datos del periférico, myDataStructure. Luego se lanzan en paralelo los dos procesos que controlan la comunicación con el bus OPB: RunOPBBus y RunOPBSlave. En paralelo con estos dos procesos, se lanza tambien el proceso Operación que es el que implementa la funcionalidad del periférico. Una vez explicado el funcionamiento del periférico, queda como trabajo para el alumno crear el proyecto Handel-C para implementar el periférico, preparar la estructura de directorios para integrar el periférico en XilinxPlatformStudio y añadirlo al sistema con MicroBlaze tal como se ha explicado en el ejemplo anterior. En el apéndice F, se encuentra el código de una aplicación en C para probar el funcionamiento del nuevo periférico. Queda también como trabajo para el alumno, el crear un nuevo periférico. El enunciado de este trabajo se encuentra en la página web de la asignatura como Prácticas EDK 2. Apéndice A Plantilla para el archivo .bbd FILES #Path relativo al directorio netlist del archivo EDF del periférico. #ejemplo: mi_periférico.edf Apéndice B Plantilla para el archivo .mpd #Introducir nombre del periférico BEGIN nombre_preiférico OPTION IPTYPE=PERIPHERAL OPTION STYLE=BLACKBOX OPTION EDIF=TRUE # Define bus interface BUS_INTERFACE BUS=SOPB, BUS_STD=OPB, BUS_TYPE=SLAVE # NO CAMBIAR. Generics for vhdl or parameters for verilog. parameter C_OPB_DWIDTH = 32, DT=integer, BUS=SOPB parameter C_OPB_AWIDTH = 32, DT=integer, BUS=SOPB #Introducir direcciones Base y Límite donde se mapeará el periférico parameter C_BASEADDR = 0xfff00000, BUS=SOPB parameter C_HIGHADDR = 0xfff000ff, BUS=SOPB # NO CAMBIAR. Señales PORT OPB_Clk PORT OPB_Rst PORT OPB_ABus BUS=SOPB PORT OPB_BE 1], BUS=SOPB PORT OPB_DBus BUS=SOPB PORT OPB_RNW BUS=SOPB PORT OPB_Select BUS=SOPB PORT OPB_SeqAddr BUS=SOPB PORT Slave_DBus BUS=SOPB PORT Slave_xferAck BUS=SOPB PORT Slave_errAck BUS=SOPB PORT Slave_retry BUS=SOPB PORT Slave_toutSup BUS=SOPB del bus OPB. NO CAMBIAR = "", DIR=IN, BUS=SOPB, SIGIS=CLK = "", DIR=IN = OPB_ABus, DIR=IN, VEC=[0:C_OPB_AWIDTH-1], = OPB_BE, DIR=IN, VEC=[0:C_OPB_DWIDTH/8- = OPB_DBus, DIR=IN, VEC=[0:C_OPB_DWIDTH-1], = OPB_RNW, DIR=IN, = OPB_select, DIR=IN, = OPB_seqAddr, DIR=IN, = Sl_DBus, DIR=OUT, VEC=[0:C_OPB_DWIDTH-1], = Sl_xferAck, DIR=OUT, = Sl_errAck, DIR=OUT, = Sl_retry, DIR=OUT, = Sl_toutSup, DIR=OUT, #Añadir puertos de entrada o salida aquí #Ejemplo de puerto de salida #PORT Puerto_Salida = "", DIR=OUT, VEC=[0:7] #Ejemplo de puerto de entrada #PORT Puerto_Entrada = "", DIR=IN, VEC=[0:3] END Apéndice C Código Handel-C del controlador de los leds. #include "opb_bus_slave.hch" DECL_OPB_BUS (opb_bus); set clock = OPB_BUS_CLOCK (opb_bus); macro proc WriteProc (Address, DataIn, ByteEnable, Ready, myData); macro proc ReadProc (Address, DataOut, Ready, myData); typedef struct { unsigned int 32 Registro; } Datos_Periferico; macro proc WriteProc (Address, DataIn, ByteEnable, Ready, myData) { par { myData.Registro = DataIn; Ready = 1; } } macro proc ReadProc (Address, DataOut, Ready, myData) { par { DataOut = myData.Registro; Ready = 1; } } void main (void) { Datos_Periferico myDataStructure; interface port_out () MyLedDriver (unsigned 2 OutLeds = myDataStructure.Registro<-2) with { busformat="B(N:0)" }; par { RunOPBBus (opb_bus); RunOPBSlave (opb_bus, WriteProc, ReadProc, myDataStructure, 0x70000000, 0x700000FF); } } Apéndice D Código C para la aplicación de prueba del periférico. #include "xparameters.h" #include "xutil.h" //==================================================== int main (void) { volatile unsigned int *ptr; unsigned int i; unsigned int j; ptr = (volatile unsigned int *)0x70000000; print("\033[H\033[J"); //Clear Screen print("Probando el funcionamiento de los leds. . .\r\n"); print("Encendiendo alternativamente.\r\n"); for(j = 0; j < 16; j++) { *ptr = 0x2; //izquierdo apagado, derecho encendido for(i = 0; i < 500000; i++); *ptr = 0x1; //izquierdo encendido, derecho apagado for(i = 0; i < 500000; i++); } print("Encendiendo/apagando simultaneamente.\r\n"); for(j = 0; j < 16; j++) { *ptr = 0x3; //ambos encendidos for(i = 0; i < 500000; i++); *ptr = 0x0; //ambos apagados for(i = 0; i < 500000; i++); } } Apéndice E Código Handel-C del sumador restador. #include "opb_bus_slave.hch" DECL_OPB_BUS (opb_bus); set clock = OPB_BUS_CLOCK (opb_bus); macro proc WriteProc (Address, DataIn, ByteEnable, Ready, myData); macro proc ReadProc (Address, DataOut, Ready, myData); typedef struct { unsigned int unsigned int unsigned int unsigned int } Datos_Periferico; 32 32 32 32 Reg_1; Reg_2; Reg_result; Reg_opmode; macro proc WriteProc (Address, DataIn, ByteEnable, Ready, myData) { par { if(Address == 0x10000000) myData.Reg_1 = DataIn; else if (Address == 0x10000004) myData.Reg_2 = DataIn; else if (Address == 0x10000008) delay; //no permitimos escribir en el registro de resultado else if (Address == 0x1000000C) myData.Reg_opmode = DataIn; else delay; //para que todos los caminos duren un ciclo Ready = 1; } } macro proc ReadProc (Address, DataOut, Ready, myData) { par { if(Address == 0x10000000) DataOut = myData.Reg_1; else if (Address == 0x10000004) DataOut = myData.Reg_2; else if (Address == 0x10000008) DataOut = myData.Reg_result; else if (Address == 0x1000000C) DataOut = myData.Reg_opmode; else delay; //para que todos los caminos duren un ciclo Ready = 1; } } macro proc Operacion(myData) { while(1) { if(myData.Reg_opmode == 0x1) myData.Reg_result = myData.Reg_1 + myData.Reg_2; else if(myData.Reg_opmode == 0x2) myData.Reg_result = myData.Reg_1 - myData.Reg_2; else delay; // todos los caminos duran un ciclo } } void main (void) { Datos_Periferico myDataStructure; par { RunOPBBus (opb_bus); RunOPBSlave (opb_bus, WriteProc, ReadProc, myDataStructure, 0x10000000, 0x100000FF); Operacion(myDataStructure); } } Apéndice F Código C para probar el funcionamiento del sumador-restador #include "xparameters.h" #include "xutil.h" //==================================================== int main (void) { volatile unsigned int *reg_1, *reg_2, *reg_opmode; volatile unsigned int *reg_result; reg_1 = (volatile unsigned int *)0x10000000; reg_2 = (volatile unsigned int *)0x10000004; reg_opmode = (volatile unsigned int *)0x1000000C; reg_result = (volatile unsigned int *)0x10000008; print("\033[H\033[J"); //Clear Screen *reg_2 = 100; *reg_1 = 250; *reg_opmode = 0x1; xil_printf("%d + %d *reg_opmode = 0x2; xil_printf("%d - %d *reg_1 = 1235; *reg_2 = 5; *reg_opmode = 0x1; xil_printf("%d + %d *reg_opmode = 0x2; xil_printf("%d - %d } = %d\r\n", *reg_1, *reg_2, *reg_result); = %d\r\n", *reg_1, *reg_2, *reg_result); = %d\r\n", *reg_1, *reg_2, *reg_result); = %d\r\n", *reg_1, *reg_2, *reg_result);