Implementando un SOC con Litex
Existen muchos casos en los que vamos a necesitar un procesador en nuestor diseño en FPGA. En mi caso, los soft-cores son muy útiles cuando tengo que implementar en la FPGA ciertos protocolos de alto nivel como Ethernet o Modbus. Para utilizar estos interfaces se pueden encontrar en la red muchos ejemplos escritos en C. Además. la cantidad de lógica que utlizariamos en el caso de implementarlos directamente en RTL sería mayor a la lógica requerida por un soft-core que haga la misma labor. Para los dispositivos de Xilinx, ya vimos que podemos utilizar Microblaze. Mi-V es una excelente opción si en cambio estamos utilizando una FPGA de Microchip, el cual es un procesador basado en Risc-V. EN ambos casos, el procesador es configurable para adaptarlo a nuesras necesidades, y de esta forma poder optimizar la cantidad de lógica utilizada, pero en ocasiones, necesitamos algo incluso más personalizado ya que estos procesadores, aunque se puedne uitlizar sin pagar ninguna licencia, son cerrados, por lo que no podemos ver y modificar su código. Para los casos en los que necesitamos algo más personalizado, necesitaremos recurrir a otras opciones.
En GitHub puedes encontrar infinidad de soft-cores desarrollados por desarrolladores independientes o incluso compañías, que pueden adaptarse mejor a nuestras necesidades, bien porque su configuración por defecto nos encaja más, o bien porque son más personalizables y podemos hacerlo encajar en nuestro proyecto. Además, otra vemtaja de estos soft-cores es que no dependemos el fabricante de la FPGA, por lo que es más sencillo hacer la migración de una familia de FPGA a otra.
En este punto, ya hemos visto que tenemos diferentes opciones de soft-cores en GitHub, por lo que lo “único” que debemos hacer es encontrar el repositorio del procesador que queramos incluir en nuestro diseño, descargarnos el repositorio, y crear un nuevo diseño donde instanciaremos el módulo correspondiente al procesador, y voilà, lo tenemos. Si estamos en la etapa de prototipado de nuestro proyecto, lo más probable es que necesitemos hacer diferentes pruebas, por lo que necesitaremos probar diferentes soft-cores para encontrar el que más se adapta a nuestras necesidades. Si estas acostumbrado a lidiar con código HDL, no tendrás problemas en crear un nuevo fichero top, instanciar el softcore correspondiente, configurar los periféricos, crear el archivo de restricciones, y tenerlo implementado y funcionando en unas cuantas horas, o quizás unos cuantos dias, tiempo que puede parecer excesivo en muchos casos. Además, tambien puede ser el caso de que te encuentres en una compañia que se encarga de desarrollar software, por lo que el diseño del hardware es algo que no tenéis muy controlado. En este caso, necesitariamos una herramienta que permitiera a desarrolladores software implementar diferentes soft-cores en la FPGA, y si además esta herramienta lo hace en el menor tiempo posible sería ideal. Bien, pues estos dos requisitos los cumple el proyecto Litex, una herramienta de Enjoy Digital.
Litex
En su documentación podemos encontrar
El framework Litex proporciona una infraestructura eficiente para crear FPGA Cores, explorar diferentes arquitecturas, y crear sistemas basados basados solo en FPGA.
En otras palabras, es un framework para crear sistemas en chip (SoC) de una forma rápida y fácil. La herramienta está basada en Migen, que es otra herramienta basada en Python para crear código RTL a partir de un código de Python. Además del procesador, Litex integra periféricos descritos utilizando Migen para ser integrados con el procesador mediante código Python. Para ver mejor como funciona, vamos a ver como descargarlo y Utlizarlo.
Descargando e instalando Litex.
Lo primero que debemos hacer es instalar Litex y todas sus dependencias. Para ello debemos seguir las indicaciones que encontraremos en su Guía de inicio rápido. Una vez tengamos descargado el repositorio e instaladas todas las dependencias, tendremos un árbol de directorios como el siguiente:
pablo@friday:~/litex$ tree -d -L 1
.
├── amaranth
├── build
├── fpga_101
├── litedram
├── liteeth
├── litehyperbus
├── liteiclink
├── litejesd204b
├── litepcie
├── litesata
├── litescope
├── litesdcard
├── litespi
├── litex
├── litex-boards
├── migen
├── pythondata-cpu-blackparrot
├── pythondata-cpu-cv32e40p
├── pythondata-cpu-ibex
├── pythondata-cpu-lm32
├── pythondata-cpu-microwatt
├── pythondata-cpu-minerva
├── pythondata-cpu-mor1kx
├── pythondata-cpu-picorv32
├── pythondata-cpu-rocket
├── pythondata-cpu-serv
├── pythondata-cpu-vexriscv
├── pythondata-cpu-vexriscv-smp
├── pythondata-misc-tapcfg
├── pythondata-misc-usb_ohci
├── pythondata-software-compiler_rt
├── pythondata-software-picolibc
└── riscv64-unknown-elf-gcc-8.3.0-2019.08.0-x86_64-linux-ubuntu14
Podemos verlos periférico en diferentes carpetas como litespi
, litepcie
o incluso periféricos muy especificos como litejesd204b
. Dentro de la carpeta /cores
podemos encontrar otros periféricos. En la parte de abajo podemos ver los diferentes soft-cores que podemos incluir en nuestro diseño. Los procesadores que podemos utilizar por defecto incluyen Minerva, PicoRV32, Serv, VexRiscv, u otros con arquitecturas de 64 bits como Rocket. Si naveganos dentro de cada una de estas carpetas podemos ver que los procesadores estan descritos en Python, y preparados para utilizar la herramienta Migen y obtener el RTL de cada uno de ellos. Por último, si navegamos dentro de la carpeta litex-boards/litex_boards/platforms/
podremos ver una lista de archivos Python con las definiciones necesarias para muchas tarjetas basadas en FPGAs de Gowin, AMD-Xilinx, Intel y Freescale. En estos archivos encontraremos las definiciones del hardware que podemos encontrar en estas tarjetas, su nombre y su pinout. En este artículo voy a utilizar la tarjeta Arty A35t de Digilent, que Litex soporta de forma nativa. Si navegamos a litex-boards/litex_boards/targets/
, podemos ver también archivos de Python de las diferentes tarjetas. Estos archivos son los que contienen las instrucciones necesarias para compilar nuestro soft-core en cada una de esas tarjetas. Vamos a ver como funciona.
Creando un diseño con Litex.
Para crear nuestro primer diseño con Litex, tenemos que ir a la carpeta litex-boards/litex_boards/targets
, y ejecutar el archivo de Python correspondiente a la tarjeta que tenemos. Cada uno de estos archivos tiene una ayuda a la cual podemos acceder utilizando el argumento --help
.
pablo@friday:~/litex/litex-boards/litex_boards/targets$ ./digilent_arty.py --help
usage: digilent_arty.py [-h] [--toolchain TOOLCHAIN] [--build] [--load] [--variant VARIANT]
[--sys-clk-freq SYS_CLK_FREQ] [--with-ethernet | --with-etherbone]
[--eth-ip ETH_IP] [--eth-dynamic-ip] [--with-spi-sdcard | --with-sdcard]
[--sdcard-adapter SDCARD_ADAPTER] [--no-ident-version] [--with-jtagbone]
[--with-spi-flash] [--with-pmod-gpio] [--output-dir OUTPUT_DIR]
[--gateware-dir GATEWARE_DIR] [--software-dir SOFTWARE_DIR]
[--include-dir INCLUDE_DIR] [--generated-dir GENERATED_DIR]
[--no-compile-software] [--no-compile-gateware] [--csr-csv CSR_CSV]
[--csr-json CSR_JSON] [--csr-svd CSR_SVD] [--memory-x MEMORY_X] [--doc]
[--bus-standard BUS_STANDARD] [--bus-data-width BUS_DATA_WIDTH]
[--bus-address-width BUS_ADDRESS_WIDTH] [--bus-timeout BUS_TIMEOUT]
[--cpu-type CPU_TYPE] [--cpu-variant CPU_VARIANT]
[--cpu-reset-address CPU_RESET_ADDRESS] [--cpu-cfu CPU_CFU] [--no-ctrl]
[--integrated-rom-size INTEGRATED_ROM_SIZE]
[--integrated-rom-init INTEGRATED_ROM_INIT]
[--integrated-sram-size INTEGRATED_SRAM_SIZE]
[--integrated-main-ram-size INTEGRATED_MAIN_RAM_SIZE]
[--csr-data-width CSR_DATA_WIDTH] [--csr-address-width CSR_ADDRESS_WIDTH]
[--csr-paging CSR_PAGING] [--csr-ordering CSR_ORDERING] [--ident IDENT]
[--ident-version IDENT_VERSION] [--no-uart] [--uart-name UART_NAME]
[--uart-baudrate UART_BAUDRATE] [--uart-fifo-depth UART_FIFO_DEPTH]
[--no-timer] [--timer-uptime] [--l2-size L2_SIZE]
[--synth-mode SYNTH_MODE]
El comando nos muestra todos los argumentos de personalización que admite esta tarjeta, con su correspondiente explicación. Si observamos el argumento --cpu-type
, podemos ver los procesadores que podemos utilizar en nuestro diseño.
--cpu-type CPU_TYPE Select CPU: None, external, lm32, mor1kx, microwatt, serv, femtorv,
picorv32, minerva, vexriscv, vexriscv_smp, ibex, cv32e40p, rocket,
blackparrot, zynq7000, eos-s3, gowin_emcu, (default=vexriscv).
Para probar diferentes procesadores, lo único que tenemos que hacer es cambiar este argumento por el procesador que queramos o necesitemos, y la herramienta lo añadirá a nuestro diseño. Además, FPGA que incluyen procesadores hardware, como la familia Zynq7000 o Gowin, estos procesadores también serán seleccionables, y el programa que ejecutemos, se hará en estos procesadores hardware. Por el momento, vamos a utilizar el procesador por defecto, el cual es VexRiscV.
Ahora, lo que tenemos que hacer es ejecutar los diferentes pasos, de manera conjunta o por separado, para implementar nuestro diseño. Primero necesitamos crear los archivos de Verilog a partir de los archivos de Python utilizando Migen. Para hacer, simplemente tenemos que ejecutar el archivo Python de la tarjeta correspondiente con los argumentos que queramos de tipo de procesador, periféricos que queremos añadir… Este paso generará todos los archivos RTL correspondiente, además de un archivo TCL para implementar el diseño en Vivado. Una vez tenemos generados los archivos, ejecutando el script TCL desde Vivado nuestro diseño se implementará. El primer paso lo realiza Migen, el paso de la implementación, para FPGA de Xilinx podremos elegir que lo ejecute Vivado o Yosys. Por último, necesitamos enviar el diseño a la FPGA, lo cual lo podremos hacer de nuevo utilizando Vivado, o utilizando Open OCD. Estos pasos que realizamos desde herramientas externas, también pueden ser ejecutados directamente añadiendo ciertos argumentos. Por ejemplo, para crear un diseño para la Arty A35t, con el procesador VexRiscV, e implementando en Vivado, el comando que deberemos utilizar es el siguiente.
pablo@friday:~/litex/litex-boards/litex_boards/targets$ ./digilent_arty.py --build --load
Para un diseño con VexRiscv, verás que el diseño tarda en implementarse un par de minutos, lo cual es más rápido que implementar el diseño con Microblaze. Si abrimos el proyecto creado, podemos ver que la utilización es de 1993 LUTs. Esta configuración básica incluye un interfaz Wishbone, un bus de codigo abierto para procesadores equivalente al interfaz AXI.
Si comparamos la utilización de este procesador, con un Microblaze, según su guía de inicio rápido, podemos ver que el número mínimo de celdas utilizadas es de 1900.
La diferencia es que Microblaze es un procesador cerrado, y VexRiscv es un procesador de que podemos programar utilizando diferentes herramientas de código abierto.
Si los requisitos de tu diseño basado en procesador son muy bajos, puedes utilizar otros procesadores más óptimos en cuanto a utilización, por ejemplo SeRV, un procesador creado por Olof Kindren, el cual es el procesador RiscV más pequeño, y su utilización es de solo 562 LUTs.
Recuerda que el cambio de VexRiscv a SerV lo hemos hecho solo modificando en argumento --cpu-type
.
Una vez tenemos cargado el diseño en la FPGA, podemos utilizar cualquier terminal serie para comunicarnos con la tarjeta. Litex incluye el terminal lxterm
, el cual hay que ejecutar con el puerto serie como argumento. haciendo esto, la salida que obtenemos es la siguiente.
pablo@friday:~/litex/litex-boards/litex_boards/targets/build/digilent_arty$ lxterm /dev/ttyUSB1
__ _ __ _ __
/ / (_) /____ | |/_/
/ /__/ / __/ -_)> <
/____/_/\__/\__/_/|_|
Build your hardware, easily!
(c) Copyright 2012-2021 Enjoy-Digital
(c) Copyright 2007-2015 M-Labs
BIOS built on Jan 8 2022 15:44:19
BIOS CRC passed (7b8e3195)
Migen git sha1: ac70301
LiteX git sha1: d36e1b60
--=============== SoC ==================--
CPU: SERV @ 100MHz
BUS: WISHBONE 32-bit @ 4GiB
CSR: 32-bit data
ROM: 128KiB
SRAM: 8KiB
L2: 8KiB
SDRAM: 262144KiB 16-bit @ 800MT/s (CL-7 CWL-5)
Escribiendo help
nos aparecerán diferentes opciones.
--============= Console ================--
litex> help
LiteX BIOS, available commands:
help - Print this help
ident - Identifier of the system
crc - Compute CRC32 of a part of the address space
flush_cpu_dcache - Flush CPU data cache
flush_l2_cache - Flush L2 cache
leds - Set Leds value
boot - Boot from Memory
reboot - Reboot
serialboot - Boot from Serial (SFL)
mem_list - List available memory regions
mem_read - Read address space
mem_write - Write address space
mem_copy - Copy address space
mem_test - Test memory access
mem_speed - Test memory speed
sdram_init - Initialize SDRAM (Init + Calibration)
sdram_cal - Calibrate SDRAM
sdram_test - Test SDRAM
sdram_force_rdphase - Force read phase
sdram_force_wrphase - Force write phase
sdram_mr_write - Write SDRAM Mode Register
En este punto ya tenemos nuestro soft-core ejecutando un programa por defecto en la tarjeta Arty A7 35t. Para enviar un kernel diferente, una aplicación que queramos ejecutar, lo haremos utilizando el siguiente comando, de forma que eviaremos un archivo .bin
con nuestra aplicación.
pablo@friday:~/litex/litex/litex/soc/software/demo$ lxterm /dev/ttyUSB1 --kernel=demo.bin
El diseño RTL siempre ha sido algo que, para los que no están familiarizados con él, puede parecer algo complejo. Con herramientas como Litex o Migen, acercamos el diseño RTL a muchos desarrolladores software que quieren o necesitan utilizar FPGA en sus proyectos, y es para ellos para los que estas herramientas son útiles. Si en cambio eres un diseñador de FPGA, quizás utilizando estas herramientas te puede parecer que te faltan cosas. En cualquier caso, acercar las FPGA al resto de desarrolladores siempre es algo positivo.