Tutorial_Handel-C_MicroBlaze

Anuncio
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);
Descargar