Creando tu propio diseño con PolarFire SoC
Los fabricantes de FPGA estan convencidos de enseñarnos como sus dispositivos pueden hacer cosas realmente complicadas como procesar vide, detectar en vivo quien lleva mascarilla y quien no, detectar si la gente es feliz o no… todo ello mientras hacen parpadear un LED. Esto hace que la información que podemos encontrar esté siempre relacionada con este tipo de proyectos. Hoy en dia es difícil es dificil encontrar proyectos distribuidos por los fabricantes en los que se muestre como poner en marcha su dispositivo por primera vez. me temo incluso que es más fácil hoy encontrar como procesar video, que un tutorial completo de como hacer parpadear un simple LED. Incluso los proyectos más básicos que comparten están llenos de código que, a priori, es innecesario para un proyecto sencillo y en general, siempre se dejan algunos pasos que, por casualidad, son los que harán que tu pequeño proyecto no funcione… Durante mis años de estudiante en la universidad, cuando empecé a programar en C, mi profesor siempre decía que la parte más complicada de cualquier programa, es enfrentarse a la hoja en blanco. En este blog intento mostrarte como, desde una hoja en blanco, podemos crear algunos proyectos interesantes o, al menos, como crear proyectos que hagan que el dispositivo haga algo. Esto es exactamente lo que vamos a hacer en este artículo, arrancar una PolarFire SoC de Microchip, y el Icicle Kit desde una hoja en blanco.
la primera vez que eché un vistazo a POlarFire SoC, con sus cinco núcleos RISCV, pensé que sería bastante complicado hacer eso funcionar. Además, el diseño de referencia que ellos te proporcionan es un diseño gigante donde es complicado encontrar como se conectan los diferentes periférico, literalmente no cabe todo en la pantalla. Además de eso, los ejemplos que te proporcionan, donde solo se añade un componente al fabric, y se conecta con el MSS, utiliza el diseño entero cuando solo el MSS y un interfaz FIC se utiliza… Como he dicho, en este artículo quiero mostrarte como crear un diseño muy sencillo que funcione en el Icicle Kit de Microchip, y verás como es más fácil de lo que parece. El proyecto utilizará dos de los cinco nñucleos de los que dispone PolarFire SoC, y veremos como entre ellos pueden interactuar. En la siguiente imagen pudes ver el esquema de PolarFire SoC. Disponemos de cinco cores RISCV en total, uno de ellos, el E51 está pensado como núcleo de control, mientras que los otros cuatro U54 están pensados para correr la aplicación. Todos los núcleos pueden funcionar hasta 667 MHz, y todos ellos tienen acceso a todos los periféricos.
Primero, vamos a crear un proyecto nuevo en Libero SOC para el dispositivo MPFS250T.
A continuación tenemos que seleccionar la tensión del núcleo y la tensión de los bancos de entrada/salida.
Una vez el proyecto está creado, como el proyecto está basado en el procesador, necesitamos crear un diseño en SmartDesign.
En este punto, te habrás dado cuenta de que en la pestaña catalog de Libero SOC, en la parte de procesadores, solo tenemos procesadores software (soft-cores) como los Mi-V de Microchip, aunque el dispositivo que hemos seleccionado dispone de un procesador integrado, este no aparece en el catálogo. Esto e sporque Libero SOC utiliza una aplicación externa para crear y configurar el MSS de PolarFire SoC. luego, este componente deberá ser añadido al SmartDesign. la aplicación se llama PolarFire SoC MSS Configurator,y se instala junto a Libero SOC 2022. Para abrir la aplicación en Linux basta con ejecutar el siguiente comando.
pablo@friday:~$ /media/pablo/ext_ssd0/Libero_SoC_v2022.2/Libero/bin64/pfsoc_mss
Una vez la aplicación se abre, necesitamos crear un proyecto nuevo, elegir un nombre para la configuración, y, para hacer la confgiuración algo más sencilla, vamos a utilizar una configuración predefinida, en este caso la configuración para el Icicle Kit Bare Metal.
Ahora podemos navegar por las diferentes pestañas para habilitar o deshabilitar los diferentes periféricos o partes de PolarFire SoC. Como el objetivo es crear un proyecto sencillo, he configurado como no utilizado la mayoría d elos periféricos, dejando solo habilitadas las UART de la cero a la tres, ya que están todas conectadas al mismo chip externo, además d elos pines cero al tres del GPIO2, ya que es el que está conectado al fabric, de esta forma podremos conectar los 4 LED que dispone el Icicle Kit.
Las dos siguientes pestañas contienen la configuración de la memoria, que vamos a dejar por defecto ya que depende del hardware. Los interfaces FIC vamos a deshabilitarlos también ya que no los vamos a utilizar.
por último, en la última pestaña, deshabilitamos los puertos de interrupciones.
Ahora, hacemos click sobre el cilindro en la parte de arriba de la ventana para generar los archivos. Nos preguntará en qué directorio queremos guardar los archivos ya que nos va a generar varios archivos, cada unos de ellos con una extensión diferente.
pablo@friday:~/workspace/microchip/06_basic_icicle/mss$ tree .
.
├── custom_icicle_kit.cfg
├── custom_icicle_kit.cxz
├── custom_icicle_kit_mss_cfg.xml
└── custom_icicle_kit_Report.html
.cfg
: Almacena la configuración del MSS. Este archivo nos servirá para poder modificar la configuración..cxz
: Este archivo será el que importemos desde Libero SOC para generar el componente..xml
: Este archivo se utiliza en SoftConsole para generar las cabeceras de la configuración hardware..html
: Este archivo contiene información sobre el MSS generado Será util para añadirlo a la documentación del proyecto.
Volviendo a Libero SOC, en la pestaña Design Flow, vamos a importar la configuración del MSS, el archivo .cxz
. Cuando lo hayamos hecho, veremos un nuevo componente en el proyecto. Lo siguiente será instanciar este nuevo componente en el SmartDesign.
Cuando el componente esté instanciado en el SmartDesign, solo tenemos que conectar externamente los GPIO, haciendo click derecho y pulsando sobre Promote to the top level así como los puertos de reset y la UART. Para utilizar los pines del Icicle Kit, podemos descargar las constraints desde el repositorio de Github de PolarFire SoC. Las constraints se encuentran en la carpeta script_support
. El resto de salidas del MSS las tenemos que configurar como Unused para que no salgan warnings.
En este punto ya tenemos el diseño hardware completo, solo nos queda generar el bitstream y programar el dispositivo.
Una vez tenemos configurado el PolarFire SoC, podemos cerrar Libero SOC y abrir SoftCosole. para hacer este proceso más sencillo, he importado un proyecto de ejemplo desde el repositorio PolarFire SoC Bare Metal Examples. En este repositorio hay muchos proyectos, d enetre todos, el que yo he importado es /driver-examples/mss/mss-mmuart/mpfs-mmuart-interrupt/
. Este proyecto itiliza el núcleo E51, y dos nñucleos U54. Tienes toda la información en el readme, pero vamos a modificar el proyecto para hacerlo más sencillo así que, para seguir estos pasos, esta información no es relevante. Una vez tenemos el proyecto copiado localmente, podemos impolrtarlo en SoftConsole de forma normal.
Cuando el importamos proyecto, tendremos la siguiente estructura.
pablo@friday:/media/pablo/ext_ssd0/sc_workspace/mpfs-mmuart-interrupt$ tree -L 2 .
.
├── mpfs-mmuart-interrupt hw all-harts attach.launch
├── mpfs-mmuart-interrupt hw all-harts debug.launch
├── mpfs-mmuart-interrupt renode all-harts debug.launch
├── mpfs-mmuart-interrupt renode all-harts start-platform-and-debug.launch
├── README.md
└── src
├── application
├── boards
└── platform
Además de estas carpetas, puedes tener otra llamada LIM-debug
con los archivos generados de una compilación previa. Esta carpeta se generará y se rellenará cuando compilemos el proyecto. En el proyecto descargado, tendremos varios archivos .launch
, con diferentes configuraciones de depuración, y una carpeta src
. Dentro de la carpeta src
tendremos la siguiente estructura.
pablo@friday:/media/pablo/ext_ssd0/sc_workspace/mpfs-mmuart-interrupt$ tree -L 2 ./src
./src
├── application
│ ├── hart0
│ ├── hart1
│ ├── hart2
│ ├── hart3
│ ├── hart4
│ └── inc
├── boards
│ └── icicle-kit-es
└── platform
├── drivers
├── hal
├── mpfs_hal
├── platform_config_reference
└── soc_config_generator
En la carpeta application
tendremos diferentes carpetas con los programas que van a correr en los diferentes nñucleos. Además de esta, tendremos la carpeta platform
con los diferentes controladores para el hardware abstraction layer (HAL), los controladores de los periféricos… y por último, tenemos la carpeta boards
con la inforación sobre el hardware que vamos a utilizar. Dentro de esta carpeta tenemos una estructura de tres carpetas, fpga_design
, fpga_design_config
and platform_config
.
pablo@friday:/media/pablo/ext_ssd0/sc_workspace/mpfs-mmuart-interrupt$ tree -L 3 ./src/boards/
./src/boards/
└── icicle-kit-es
├── fpga_design
│ └── design_description
├── fpga_design_config
│ ├── clocks
│ ├── ddr
│ ├── fpga_design_config.h
│ ├── general
│ ├── io
│ ├── memory_map
│ └── sgmii
└── platform_config
└── mpfs_hal_config
Dentro de fpga_design/design_description
es donde tenemos que copiar el archivo xml
generado por PolarFire SoC MSS Configurator. En el proyecto de ejemplo en esta carpeta tenemos el xml
correspondiente a la configuración por defecto, así que tenemos que reemplazar este archivo por el nuevo que hemos generado con la configuración simple. Si quieres, o necesitas, mantener la configuración por defecto para recuperarla más tarde, puedes crear otra carpeta diferente bajo boards
. En este caso, tendrás que ir a las propiedades del proyecto y modificar los includes
en la configuración C/C++ Build > Settings > Includes
. Si simplemente reemplazas el archivo viejo por el nuevo, no tienes nada más.
Ok, ya tenemos copiada nuestra nueva configuración, ¿pero como hacemos para que SoftConsole coja la nueva configuración y la utilice?, bien, la verdad es que SoftConsole no hace nada con el archivo xml
, es una aplicación externa programada en Python la que lee el xml
, y genera las cabeceras necesarias dependiendo de la configuración hardware. El escript es mpfs_configuration_generator.py
, y lo puedes encontrar aquí pero, como estamos utilizando un proyecto de ejemplo, el script está ya copiado en la carèta correspondiente, así que no hay que hacer nada. Entonces, ¿tenemos que ejecutar el script?, tampoco, d enuevo, al utilizar un proyecto de ejemplo, el proyecto está confgiurado para ejecutar el script justo antes de empezar la compilación.
Si has creado una nueva carpeta bajo boards
, tendrás que modificar la llamada al script reemplazando la carpeta donde se cuentra el nuevo xml
.
En este punto ya tenemos nuestro proyecto listo para ser compilado y ejecutado pero antes, vamos a hacer unas pequeñas modificaciones en el código. primero, como nuestro proyecto solo va a utilizar el nñucleo E51, y uno de los U54, pdmeos modificar el archivo mss_sw_config.h
que se encuentra en boards/icicle-kit-es/platform_config/mpfs_hal_config
para utilizar solo los núcleos del cero al uno.
#ifndef MPFS_HAL_FIRST_HART
#define MPFS_HAL_FIRST_HART 0
#endif
#ifndef MPFS_HAL_LAST_HART
#define MPFS_HAL_LAST_HART 1
#endif
Después vamos a borrar todo el código de ejemplo del núcleo U54_2, y vamos a copiar el código del núcleo E54_3 en el E54_2. De esta forma tenemos el nñucleo dos deshabilitado. Además he cmabiado algunas referencias hacia el hart 3 que se encontraban en el código que hemos copiado de forma que quede más limpio. El codigo del hart 2 quedará así.
#include <stdio.h>
#include <string.h>
#include "mpfs_hal/mss_hal.h"
#include "drivers/mss/mss_mmuart/mss_uart.h"
volatile uint32_t count_sw_ints_h2 = 0U;
/* Main function for the hart3(U54_2 processor).
* Application code running on hart2 is placed here
*
* The hart3 goes into WFI. hart0 brings it out of WFI when it raises the first
* Software interrupt to this hart.
*/
void u54_2(void)
{
uint64_t hartid = read_csr(mhartid);
volatile uint32_t icount = 0U;
/* Clear pending software interrupt in case there was any.
Enable only the software interrupt so that the E51 core can bring this
core out of WFI by raising a software interrupt. */
clear_soft_interrupt();
set_csr(mie, MIP_MSIP);
/* Put this hart in WFI. */
do
{
__asm("wfi");
}while(0 == (read_csr(mip) & MIP_MSIP));
/* The hart is now out of WFI, clear the SW interrupt. Here onwards the
* application can enable and use any interrupts as required */
clear_soft_interrupt();
__enable_irq();
while(1U)
{
icount++;
if(0x100000U == icount)
{
icount = 0U;
}
}
/* Never return */
}
/* hart2 software interrupt handler */
void Software_h2_IRQHandler(void)
{
uint64_t hart_id = read_csr(mhartid);
count_sw_ints_h2++;
}
Ahora vamos a modificar el código. EL código que he implementado corre en el nñucleo E51 y envía por la UART una cadena de caracteres pidiendo al usuario que pulse la tecla ‘1’ para activar el núcleo U54_1. Cuando la tecla ‘1’ se pulsa, entonces el núcleo U54_1 envía otra cadens para incidar que ha recivido la orden de activación. El núcleo E51 utiliza el MMUART0, y el núcleo U54_1 utiliza el MMUART1. El código para el E51 es el siguiente.
#include <stdio.h>
#include <string.h>
#include "mpfs_hal/mss_hal.h"
#include "drivers/mss/mss_mmuart/mss_uart.h"
volatile uint32_t count_sw_ints_h0 = 0U;
#define RX_BUFF_SIZE 16U
uint8_t g_rx_buff0[RX_BUFF_SIZE] = {0};
uint8_t rx_size0 = 0U;
const uint8_t e51_message[] = "Hello world from E51! \n\rPress '1' to wake up the U54_1\n\r";
const uint8_t e51_message_good[] = "EI U54_1! Wake Up\n\r";
const uint8_t e51_message_wrong[] = "I said '1', not any key!\n\r";
void e51(void)
{
uint64_t hartid = read_csr(mhartid);
/* enable MMUART 0 */
(void) mss_config_clk_rst(MSS_PERIPH_MMUART0, (uint8_t) 1, PERIPHERAL_ON);
/* Init MMUART 0 */
MSS_UART_init(&g_mss_uart0_lo,
MSS_UART_115200_BAUD,
MSS_UART_DATA_8_BITS | MSS_UART_NO_PARITY | MSS_UART_ONE_STOP_BIT);
/* Send Message on uart0 */
MSS_UART_polled_tx(&g_mss_uart0_lo, e51_message,
sizeof(e51_message));
/* Clear pending software interrupt in case there was any. */
clear_soft_interrupt();
set_csr(mie, MIP_MSIP);
while (1U)
{
/* Check for any message in MMUART0 */
rx_size0 = MSS_UART_get_rx(&g_mss_uart0_lo, g_rx_buff0, sizeof(g_rx_buff0));
if(rx_size0 > 0)
{
switch (g_rx_buff0[0u])
{
case '1':
/* Raise software interrupt to wake up hart 1 */
raise_soft_interrupt(1U);
MSS_UART_polled_tx(&g_mss_uart0_lo, e51_message_good,
sizeof(e51_message_good));
__enable_irq();
break;
default:
MSS_UART_polled_tx(&g_mss_uart0_lo, e51_message_wrong,
sizeof(e51_message_wrong));
break;
}
}
}
}
/* hart0 software interrupt handler */
void Software_h0_IRQHandler(void)
{
uint64_t hart_id = read_csr(mhartid);
count_sw_ints_h0++;
}
Y el código para el E54_1 el siguiente.
#include <stdio.h>
#include <string.h>
#include "mpfs_hal/mss_hal.h"
#include "drivers/mss/mss_mmuart/mss_uart.h"
#include "inc/common.h"
volatile uint32_t count_sw_ints_h1 = 0U;
const uint8_t wake_message1[] = "Here U54_1! I am awake!\n\r";
#define RX_BUFF_SIZE 16U
uint8_t g_rx_buff1[RX_BUFF_SIZE] = { 0 };
void u54_1(void)
{
uint64_t mcycle_start = 0U;
uint64_t mcycle_end = 0U;
uint64_t delta_mcycle = 0U;
uint64_t hartid = read_csr(mhartid);
clear_soft_interrupt();
set_csr(mie, MIP_MSIP);
/* Put this hart in WFI. */
do
{
__asm("wfi");
}while(0 == (read_csr(mip) & MIP_MSIP));
/* The hart is now out of WFI, clear the SW interrupt. Here onwards the
* application can enable and use any interrupts as required */
clear_soft_interrupt();
/* interrupt received */
__enable_irq();
/* enable MMUART 1 */
(void) mss_config_clk_rst(MSS_PERIPH_MMUART1, (uint8_t) 1, PERIPHERAL_ON);
/* Init MMUART 1 */
MSS_UART_init(&g_mss_uart1_lo,
MSS_UART_115200_BAUD,
MSS_UART_DATA_8_BITS | MSS_UART_NO_PARITY | MSS_UART_ONE_STOP_BIT);
/* Send wake message */
MSS_UART_polled_tx(&g_mss_uart1_lo, wake_message1,
strlen(wake_message1));
/* infinite loop */
while(1);
}
/* hart1 Software interrupt handler */
void Software_h1_IRQHandler(void)
{
uint64_t hart_id = read_csr(mhartid);
count_sw_ints_h1++;
}
Se puede ver que el U54_1 espera hasta que el E51 activa su intgerrupción hardware, entonces el E54_1 configura la UART, y ejecuta su código. El resultado es el siguiente.
Genial! parece sencillo no? bien, hasta que he conseguido estos resultados en Putty, he tenido varios problemas. Yo utilizo Ubuntu 20.04, y no se si utilizando Windows, u otra distribución de Linux, esto pasará. El primero fue que no conseguía que SoftConsole detectara el programador FlashPro6 que lleva el Icicle Kit, a pesar de que Libero SOC lo detectó sin problemas. Tuve que ejecutar SoftConsole con sudo
para poder hacerlo funcionar. Además, perdí algo de tiempo porque el depurador no podia detener el nñucleo E51, por lo que no funcionaba la depuración, para solucionarlo tuve que pulsar el boton de reset (SW4), antes de intentar porgramar. Una vez hice esto, todo funcionó sin problemas.
Espero que hayas podido seguir todos mis pasos, y tener tu icicle kit funcionando con tu propio diseño. Añadir un interfaz FIC abre muchas posibilidades ya que nos permite comunicar módulo del fabric con nuestro procesador, por lo que podemos añadir periféricos o funcionalidades nuevas a la tarjeta.