-
Notifications
You must be signed in to change notification settings - Fork 2
S7: Procesadores en FPGA: RISC V
- Tiempo: 2h
-
Objetivos de la sesión:
- Aprender a meter un procesador RISCV (RV32I) en la FPGA
- Entender el funcionamiento del computador FemtoRV
- Saber ensamblar programas y ejecutarlos en el procesador
- Introducción
- El computador FemtoRV
- Programando el FemtoRV
- Automatizando el flujo de trabajo
- ¡A practicar!
- Autor
- Licencia
- Créditos
- Enlaces
Los procesadores son circuitos digitales, y como tales, los podemos meter dentro de una FPGA. Frente a los procesadores construidos directamente en el Chip, están los soft-cores o procesadores blandos que se sintetizan en la FPGA. Esto permite que se puedan diseñar en comunidad, y compartirlos de forma similar al software libre
Hay muchos procesadores listos para usar en la FPGA: Z80, AVR (Arduino), 6800... nosotros vamos a utilizar un RISC-V de 32-bits (RV32I)
Los computadores están formados por un procesador, que ejecuta instrucciones, una memoria, que almacena las instrucciones y los datos y las unidades de Entrada/Salida para comunicarse con el exterior. Además necesitan componentes digitales adicionales para combinar todos esos elementos (se conoce como la glue logic, lógica pegamento)
Cuando todos estos elementos se meten dentro de un chip (o de una FPGA), lo denominamos System on Chip (Soc, Sistema en un chip)
Vamos a construir un computador MUY SIMPLE para entender su funcionamiento. Utilizaremos como procesador el FemtoRV creado por Bruno Levy, al que le añadiremos el resto de componentes. Este es el esquema:
En nuestro SOC tenemos los siguientes elementos:
- Procesador: Un RISC-V de 32 bits (RV32I). Utilizaremos el FemtoRV creado por Bruno Levy. Es un core completo, pero muy minimalista y que ocupa pocos recursos en la FPGA (Menos de 1000 bloques lógicos). Frecuencia de funcionamiento máxima: 50Mhz. Nosotros los utilizaremos a 12Mhz
- Memoria para el código: 4KiB (Espacio para 1K instrucciones aproximadamente).
- Sin memoria RAM: No usaremos variables. Haremos programas pequeños usando directamente los registros del RISC-V
-
Periféricos
- Puerto de salida de 8 bits
- Puerto de entrada de 8 bits
- Lógica de seleción. Lógica adicional para el mapeo de los recursos (memoria y puertos), y gestión del bus de entrada del procesador
La memoria de código (ROM) comienza en la dirección 0 y llega hasta la 0xFFF (4KiB). El resto bloques de 4KiB hasta llegar a las 16Kib (dirección 0x8000) están sin usar. Excepto las direcciones 0x7F00
y 0x7F04
que se corresponden con el puerto de salida y el puerto de entrada, ambos de 8 bits
🚧 TODO
🚧 TODO
🚧 TODO
🚧 TODO
Esta es la implementación base, con 4KiB de ROM y los dos puertos (de entrada y salida). El programa que está cargado en la RAM es el ejemplo del contador que se para al apretar el pulsador SW1
Este es el programa de ejemplo:
#----------------------------------------------------
#-- Ejemplo para el computador FemtoRV
#-- Contador con boton de stop
#-- El contador se incrementa y se muestra en los LEDs
#-- Al apretar el pulsador SW1 se detiene, y se reanuda
#-- al soltarlo
#-------------------------------------------------------
#-- Direccion base de los perifericos
.eqv BASE_IO 0x7F00
#-- Desplazamiento de los puertos
.eqv LEDS 0x00 #-- Puerto de salida
.eqv BTNS 0X04 #-- Puerto de entrada
#-- Mascara para el pulsador SW1
.eqv BOTON_1 0x01
.text
#-- s0: Direccion base de los puertos I/O
li s0, BASE_IO
#-- t0: Contador
li s1, 0
bucle:
#-- Mostrar el contador actual en los LEDs
sb s1, LEDS(s0)
#-- Incrementar contador
addi s1,s1,1
#-- Pausa
jal wait
#-- Comprobar estado pulsador
#-- Mientras este pulsado el contador
#-- no se incrementa
pressed:
lb t1, BTNS(s0)
andi t1,t1,BOTON_1
bne t1,zero,pressed
#-- Repetir
b bucle
#-- Stop
inf: j inf
#-- Subrutina de pausa
wait:
#-- Inicializar t0
li t0,0xFFFF
loop:
#-- Decrementar t0
addi t0,t0,-1
#-- Repetir mientras no sea 0
bne t0,zero, loop
#-- Volver
ret
Y este es el código máquina que se debe colocar en la memoria ROM:
00008437
f0040413
00000493
00940023
00148493
018000ef
00440303
00137313
fe031ce3
fe9ff06f
0000006f
000102b7
fff28293
fff28293
fe029ee3
00008067
Esta implementación base la colocamos en un bloque separado para que sea más sencilla de integrar en nuestros diseños. Para probar el ejemplo del contador conectamos los 8 LEDs al puerto de salida y los dos pulsadores al puerto de entrada
El computador FemtoRV lo vamos a programar en Ensamblador, usando el Simulador RARs. Este es el mismo entorno que usamos en la asignaura de Arquitectura de Computadores, para aprender la programación en ensamblador de los RISC-V
Para usar el RARs con el computador FemtoRV tenemos que configurar el mapa de memoria que viene por defecto. Esto lo hacemos desde la opción Settings/Memory configuration...
. Hay que elegir la opción Compact, Text at address 0
El flujo de trabajo que utilizaremos es muy básico y poco automatizado todavía, pero muy simple. Es el siguiente:
- Escribir el programa en el editor del RARs
- Ensamblar
- Simular para comprobar el funcionamiento
- Volcar el segmento de código (.text) con el código máquina a un fichero de texto (Formato Hexadecimal text)
- Copiar el código máquina en la caja de texto de la Memoria ROM en Icestudio
- Cargar el diseño en la FPGA
Empezamos haciendo el programa más sencillo posible: Encender todos los LEDs
Para encender los LEDs basta con escribir el valor 0xFF
en el puerto de salida, que se encuentra en la dirección 0x7F00
. Es decir, sóo hay que escribir 0xFF en la dirección 0x7F00. Para detener el procesador usamos un bucle infinito
# ----------------------------------------------
# -- Programa Hola mundo: Encender los LEDs
# ----------------------------------------------
#-- Direccion base de los perifericos
.eqv BASE_IO 0x7F00
#-- Desplazamiento de los puertos
.eqv LEDS 0x00 #-- Puerto de salida
#-- Valor a sacar por los LEDs
.eqv VALOR 0xFF
.text
#-- s0: Direccion base de acceso a los
#-- perifericos
li s0, BASE_IO
#-- Valor a enviar a los LEDs
li t0, VALOR
#-- Escribir el valor en los LEDs
sb t0, (s0)
#-- Stop
inf: j inf
Este es el código máquina. El programa sólo tiene 5 instrucciones
00008437
f0040413
0ff00293
00540023
0000006f
Seguiremos el flujo de tabajo indicado anteriormente
Primero abrimos con el editor del RARs el ejemplo (o lo escribimos desde cero)
El siguiente paso es ensamblar el código y solucionar los errores. Si todo está ok, veremos una pantalla como esta
En la parte izquierda vermos el código máquina, formado por 5 instrucciones. También vemos las direcciones donde están almacenadas las instrucciones: Dese la 0x00000000
hasta la 0x00000010
. Este código máquina es el que se debe situar dentro de la memoria ROM
Este código lo podemos simular para comprobar que funciona correctamente. En el ejemplo del LED, ejecutamos las 5 instrucciones paso a paso para comprobar que el valor 0xFF
se ha guardado en la dirección 0xF700
, que es donde está mapeado el puerto de salida donde están conectados los LEDs
El siguiente paso es volcar el codigo máquina a un fichero de texto. Esto lo hacemos desde la opción File/Dump Memory...
o pulsando el icono correspondiente en la parte superior del RARs. En el desplegable de la derecha seleccionamos el formato Hexadecimal Text (Texto hexadecimal) y pulsamos en Dump file...
Indicamos el fichero donde almacenar el código máquina. Si este fichero (que es de texto) lo abrimos con un editor, veremos el código máquina en hexadecimal:
00008437
f0040413
0ff00293
00540023
0000006f
Partimos del circuito base y copiamos el código en la caja de parámetros de la memoria. En el puerto de entrada no conectamos nada. Sólo conectamos los LEDs al puerto de salida
El último paso es sintetizar el diseño y cargarlo en la FPGA. Al hacerlo veremos cómo todos los LEDs se quedan encendidos
Este es un ejemplo de lectura del puerto de entrada. Se leen los pulsadores y su estado se escribe en el puerto de salida para verlos en los LEDs
# ----------------------------------------------
# -- Mostrar el estado de los pulsadores
# -- por los LEDs
# ----------------------------------------------
#-- Direccion base de los perifericos
.eqv BASE_IO 0x7F00
#-- Desplazamiento de los puertos
.eqv LEDS 0x00 #-- Puerto de salida
.eqv BTNS 0x04 #-- Puerto de entrada
.text
#-- s0: Direccion base de acceso a los
#-- perifericos
li s0, BASE_IO
#-- Bucle principal
bucle:
#-- Leer el puerto de entrada (pulsadores)
lb t0, BTNS(s0)
#-- Escribir el valor en los LEDs
sb t0, (s0)
#-- Repetir
b bucle
- Código máquina:
00008437
f0040413
00440283
00540023
ff9ff06f
Para comprobar el funcionamiento en el simulador lo ejecutamos a una velocidad baja (Por ejemplo a 17 instrucciones/segundo). En cualquier momento podemos situar manualmente un valor en la posición 0x7F04
(puerto de entrada) para simular que se han apretado los pulsadores. El programa leerá este valor y lo escribirá en el la dirección 0x7F00
(puerto de salida)
Este es el circuito de prueba:
Por los LEDs se muestra un contador que se incrementa. Al apretar el pulsador SW1 se inicializa la cuenta. Para que se pueda ver la cuenta en los LEDs es necesario realizar una pausa, o de lo contrario irá muy rápido y veremos todos los LEDs encendidos
Por eso, en este programa se ha implementado la subrutina wait que pierde tiempo decrementando un contador
# ----------------------------------------------
# -- Contador con reset
# -- El contador se incrementa automaticamente
# -- Boton SW1: Poner el contador a 0
# -- Es necesario utilizar una rutina de espera para
# -- que el contador se incremente más despacio y poder
# -- verlo en los LEDs
# -- (Si lo hacemos a la maxima velocidad se veran todos los
# -- LEDs encendidos. El ojo NO apreciara actividad)
# ----------------------------------------------
#-- Direccion base de los perifericos
.eqv BASE_IO 0x7F00
#-- Desplazamiento de los puertos
.eqv LEDS 0x00 #-- Puerto de salida
.eqv BTNS 0x04 #-- Puerto de entrada
#-- Mascaras para leer los pulsadores
.eqv BTN1 0x01 #-- Pulsador 1
.eqv BTN2 0x02 #-- Pulsador 2
.text
#-- s0: Direccion base de acceso a los
#-- perifericos
li s0, BASE_IO
#-- s1: Contador de pulsaciones
li s1, 0
#-- Bucle principal
bucle:
#-- Mostrar el contador actual por los LEDs
sb s1, (s0)
#-- Leer el puerto de entrada (pulsadores)
lb t0, BTNS(s0)
#-- Boton SW1 apretado?
andi t1,t0,BTN1
#-- Si t1 es cero, el pulsador NO está apretado
beq t1,zero, b1_no_pulsado
#-- Boton 1 apretado:
#-- Inicializar el contador a 0
li s1,0
#-- Volver al bucle principal
b bucle
#-- Pulsador NO apretado
b1_no_pulsado:
#-- Incrementar el contador
addi s1,s1,1
#-- Esperar
jal wait
#-- volver al bucle principal
b bucle
#---------------------------
#-- Funcion: wait
#-- Esperar un cierto tiempo
#-----------------------------
wait:
#-- Hacemos un bucle para perder tiempo
li t0, 0xFFFF
loop:
#-- Decrementar contador
addi t0,t0,-1
#-- Mientras no se llegue a cero se repite
bne t0,zero,loop
#-- Contador ha llegado a cero
ret
- Este es el código máquina:
00008437
f0040413
00000493
00940023
00440283
0012f313
00030663
00000493
fedff06f
00148493
008000ef
fe1ff06f
000102b7
fff28293
fff28293
fe029ee3
00008067
En este ejemplo el contador se incrementa manualmente con la pulsación del botón 1. Cuando queremos que suceda algo al apretar un pulsador, y sólo al apretarlo, debemos hacerlo al detectar un cambio de 1 a 0 (y solo en ese momento). Por ello debemos leer el estado del pulsador en un instante y compararlo con el estado anterior. Así decidimos si ha habido pulsación o no
# ----------------------------------------------
# -- Contador manual
# -- El contador se incrementa manualmente con
# -- cada pulsacion del boton 1
# -- Boton SW1: Incrementar contador
# -- Boton SW2: Inicializar contador
# ----------------------------------------------
#-- Direccion base de los perifericos
.eqv BASE_IO 0x7F00
#-- Desplazamiento de los puertos
.eqv LEDS 0x00 #-- Puerto de salida
.eqv BTNS 0x04 #-- Puerto de entrada
#-- Mascaras para leer los pulsadores
.eqv BTN1 0x01 #-- Pulsador 1
.eqv BTN2 0x02 #-- Pulsador 2
.text
#-- s0: Direccion base de acceso a los
#-- perifericos
li s0, BASE_IO
#-- s1: Contador de pulsaciones
li s1, 0
#-- t1: Valor actual de los pulsadores
#-- Inicialmente a 0
li t1, 0
#-- Bucle principal
bucle:
#-- Mostrar el contador en los LEDs
sb s1, LEDS(s0)
#-- Almacenar el valor anterior
mv t0, t1
#-- Leer nuevo valor de los pulsadores
lb t1, BTNS(s0)
#-- Comprobar si ha habido cambios con respecto a la
#-- lectura anterior
#-- Lo detectamos haciendo una operacion XOR
xor t2,t0,t1
#-- Examinando t2 sabemos si ha habido una pulsacion
#-- de una tecla o no
#-- Si t2 es cero, no hay cambios
beq t2,zero,bucle
#-- Ha habido un cambio en algun boton
#-- Comprobar si es un cambio de pulsacion de la tecla 1
andi t3,t2,BTN1
bne t3,zero,b1_cambiado
#-- No hay cambio en el boton 1
#-- Comprobar si hay cambio en el boton 2
andi t3,t2,BTN2
bne t3,zero,b2_cambiado
b bucle
#-- Cambio en el boton 1 (pulsado o liberado)
b1_cambiado:
#-- Comprobar el valor actual del boton 1. Con ese valor
#-- sabemos si se ha apretado o liberado
bne t1,zero,b1_pulsado
#-- Boton b1 no pulsado
b bucle
b1_pulsado:
#-- Incrementar el contador
addi s1,s1,1
b bucle
b2_cambiado:
#-- Boton 2 pulsado o liberado
#-- En ambos casos se pone el contador a 0
li s1,0
b bucle
- Este es el código máquina:
00008437
f0040413
00000493
00000313
00940023
006002b3
00440303
0062c3b3
fe0388e3
0013fe13
000e1863
0023fe13
000e1c63
fddff06f
00031463
fd5ff06f
00148493
fcdff06f
00000493
fc5ff06f
El contador ahora está en dos estados diferentes: contando y parado (start/stop). El estado lo determina el valor del registro S4. Con el pulsador 1 se cambia este estado, y se detiene o activa el contador. Con el botón 2 se inicializa
# ----------------------------------------------
# -- Contador start/stop con reset
# -- El contador se incrementa automaticamente
# -- Boton SW1: start/stop
# -- Boton SW2: Inicializar contador
# ----------------------------------------------
#-- Direccion base de los perifericos
.eqv BASE_IO 0x7F00
#-- Desplazamiento de los puertos
.eqv LEDS 0x00 #-- Puerto de salida
.eqv BTNS 0x04 #-- Puerto de entrada
#-- Mascaras para leer los pulsadores
.eqv BTN1 0x01 #-- Pulsador 1
.eqv BTN2 0x02 #-- Pulsador 2
.text
#-- s0: Direccion base de acceso a los
#-- perifericos
li s0, BASE_IO
#-- s1: Contador de pulsaciones
li s1, 0
#-- s2: Valor actual de los pulsadores
#-- Inicialmente a 0
li s2, 0
#-- s4: Estado del contador:
#-- s4=0: STOP
#-- s4=1: START (contando)
#-- Por defecto empieza contando
li s4,1
#-- Bucle principal
bucle:
#-- Mostrar el contador en los LEDs
sb s1, LEDS(s0)
#-- Esperar
jal wait
#-- Incrementar el contador segun el estado
beq s4,zero,no_inc
#-- Incrementar el contador
addi s1,s1,1
no_inc:
#-- Almacenar el valor anterior
mv s3, s2
#-- Leer nuevo valor de los pulsadores
lb s2, BTNS(s0)
#-- Comprobar si ha habido cambios con respecto a la
#-- lectura anterior
#-- Lo detectamos haciendo una operacion XOR
xor t0,s2,s3
#-- Examinando t0 sabemos si ha habido una pulsacion
#-- de una tecla o no
#-- Si t2 es cero, no hay cambios
beq t0,zero,bucle
#-- Ha habido un cambio en algun boton
#-- Comprobar si es un cambio de pulsacion de la tecla 1
andi t1,t0,BTN1
bne t1,zero,b1_cambiado
#-- No hay cambio en el boton 1
#-- Comprobar si hay cambio en el boton 2
andi t1,t0,BTN2
bne t1,zero,b2_cambiado
b bucle
#-- Cambio en el boton 1 (pulsado o liberado)
b1_cambiado:
#-- Comprobar el valor actual del boton 1. Con ese valor
#-- sabemos si se ha apretado o liberado
bne s2,zero,b1_pulsado
#-- Boton b1 no pulsado
b bucle
b1_pulsado:
#--Cambiar estado: start/stop
xori s4,s4,1
b bucle
b2_cambiado:
#-- Boton 2 pulsado o liberado
#-- Comprobar el valor actual del boton 2
bne s2,zero,b2_pulsado
#-- Boton b2 no pulsado
b bucle
b2_pulsado:
#-- Inicializar el contador a cero
li s1,0
b bucle
#---------------------------
#-- Funcion: wait
#-- Esperar un cierto tiempo
#-----------------------------
wait:
#-- Hacemos un bucle para perder tiempo
li t0, 0xFFFF
loop:
#-- Decrementar contador
addi t0,t0,-1
#-- Mientras no se llegue a cero se repite
bne t0,zero,loop
#-- Contador ha llegado a cero
ret
- Este es el código máquina
00008437
f0040413
00000493
00000913
00100a13
00940023
050000ef
000a0463
00148493
012009b3
00440903
013942b3
fe0282e3
0012f313
00031863
0022f313
00031c63
fd1ff06f
00091463
fc9ff06f
001a4a13
fc1ff06f
00091463
fb9ff06f
00000493
fb1ff06f
000102b7
fff28293
fff28293
fe029ee3
00008067
El flujo de trabajo que hemos usado es muy básico y simple, pero permite entender muy bien cómo funcionan las cosas a bajo nivel. Ahora mejoraremos ese flujo automantizando algunas tareas
A la memoria ROM se le puede pasar como parámetro un fichero de texto con el código máquina, en vez de tener que hacer copy & paste. Utilizaremos como plantilla el siguiente circuito:
(07-risc-v-soc-test-fichero.ice)
El código máquina ahora se debe situar el fichero firmware.list. Al sintetizar el circuito se abre primero este fichero, se usa para rellenar la ROM y se genera el bitstream. Este fichero debe estar en el mismo directorio donde se encuentre el proyecto de Icestudio abierto (fichero .ice)
El fichero con el código máquina también se puede obtener directamente desde la línea de comandos, sin necesidad de tener que abrir el entorno gráfico del RARs.
Por ejemplo, si queremos ensamblar el fichero 01-counter-stop.s
ejecutamos este comando desde el terminal. Es necesario que el fichero rars1_5.jar
se encuentre en ese directorio (o bien indicar la ruta como parámetro)
java -jar rars1_5.jar a mc CompactTextAtZero dump .text HexText firmware.list 01-counter-stop.s
Ahora desde Icestudio simplemente le damos a la opción de cargar (Ctrl-U) y automáticamente se utilizará este fichero
Para que sea más cómodo, podemos utilizar este script de la shell (asm.sh)
#!/bin/bash
echo "* Ensamblando fichero: $1"
java -jar rars1_5.jar a mc CompactTextAtZero dump .text HexText firmware.list $1
echo "* Fichero generado: Firmware.list"
Le damos permisos de ejecución y simplemente ejecutaríamos esta línea
./asm.sh 01-counter-stop.s
Así, el flujo de trabajo quedaría así:
- Editar el fichero fuente .s con un editor (o el propio rars)
- Simular para comprobar que funciona
- Ejecutar este comando desde el terminal:
./asm.sh mi_programa.s
- Realizar la carga desde Icestudio
Ahora que ya sabes cómo meter un RISC-V en la FPGA, puedes crearte tus propios perféricos (unidades PWM, temporizadores...) y controlarlas mediante programas escritos en ensamblador que corren en el procesador RISC-V
Crea un circuito con el Computador FemtoRV que tenga conectados dos controladores para los servos de rotación continua en el puerto de salida. Utiliza 2 bits para cada motor (para especificar las 3 velocidades de cada motor: parado, adelante y atrás)
Escribe un programa en ensamblador que genere una secuencia de movimiento en los motores
Si tienes construido un robot con estos servos, podrás hacer que es programa haga que tu robot se mueva
Haz un circuito que integre el computador FemtoRV, los dos controladores de servos en el puerto de salida y dos sensores de IR conectados al puerto de entrada. Realiza un programa para que el robot siga una linea negra
- Juan González-Gómez (Obijuan)
- Bruno Levy. Es el autor del FemtoRV. Muchísimas gracias por publicarlo con licencia libre y compartir el conocimiento
- L15: FPGAs Libres. Icestudio
- L16: FPGAs (II). Domadores de bits
- L17: FPGAs (III). Señales y tiempo
- L18: Control digital de motores
- S7: Procesadores en FPGA: RISC-V
- S1: Robots
- S2: Estructuras mecánicas
- S3: Estructuras mecánicas (II)
- S4: Estructuras mecánicas (III)
- S5: Sensores binarios
- S6: Comunicaciones