Blinker básico para RISC-V en C++ 
Partiendo del compilador cruzado GCC para RISC-V descrito en el post anterior, el primer objetivo será hacer una prueba de concepto con un procesador RISC-V real. En este caso he optado por un GD32VF103, un microcontrolador de GigaDevice, con 128 Kb de memoria flash de programa, 32 Kb de SRAM, procesador RISC-V con arquitectura básica RV32IMAC y periféricos muy parecidos a los del STM32F103.

Secuencia de arranque

Los procesadores RISC-V, al contrario que los ARM, no poseen, en su configuración base, un vector de interrupciones, es una extensión estándar pero no forma parte del núcleo en sí. Por tanto, el concepto que existe en los ARM de una tabla de interrupciones en la que el "reset" es una interrupción más, no existe en RISC-V: En los RISC-V existe el "RESET_VECTOR", que es la dirección de memoria desde donde arranca el procesador cuando se reinicia o se enciende. Dicha dirección suele ser 0x00000000 pero puede ser libremente definida por el fabricante, en el caso que nos ocupa, el GD32VF103 incluye un bootloader en la dirección 0x00000000, mientras que en la dirección 0x08000000 se encuentra la memoria flash de programa (los 128 Kb). Cuando el micro arranca en modo bootloader (determinado por un pin), arranca desde la dirección 0x00000000 (RESET_VECTOR = 0x00000000), ejecuta el bootloader y éste se encarga de poner el micro en modo DFU a través del USB, mientras que cuando arranca en modo normal (sin bootloader), el micro arranca desde la dirección 0x08000000 (RESET_VECTOR = 0x08000000), donde se supone que debe estar el programa principal.

Algo parecido ocurre con el puntero de pila: Al contrario que los Cortex-M de ARM, en los RISC-V no existe una tabla que indique el valor que debe tener, en el momento del arranque, el registro de pila (SP), por lo que dicho registro debe ser cargado a mano mediante una instrucción en ensamblador. Por ejemplo, una secuencia de arranque mínima podría ser:

reset_vector:
    la sp, 0x20005000   ; la SRAM acaba en 0x20005000 y la pila "crece" hacia abajo
    call main           ; llamamos a la función main

Nótese que no se está teniendo el cuenta la inicialización de variables globales (incluidos los constructores de objetos globales), pero es un punto de partida.

Linker script y arranque

Basándonos en un post anterior hacemos un linker script básico y un código de arranque e inicialización para variables globales y constructores. En este caso, al principio del linker script dividimos el código generado en dos zonas:

- Una al principio, en 0x08000000, donde sólo alojaremos una instrucción de salto.

- Otra a partir de 0x08000200 donde alojaremos la inicialización y el resto del código (incluida la función main).

Esto lo hacemos así porque en el caso del GD32VF103 sí que existe, para la extensión de las interrupciones de RISC-V, una tabla de interrupciones que se extiende desde 0x0800000C hasta 0x080001FC, por lo que, para poder en un futuro usar ese vector de interrupciones, haremos que el grueso del código de aloje a partir de 0x08000200 y en 0x08000000 lo que ponemos es un salto incondicional a 0x08000200.

SECTIONS {
. = 0x08000000 ;
.text : {
startup.o (.startup0)
}
. = 0x08000200 ;
.text : {
_linker_code = . ;
startup.o (.startup1)
*(.text)
*(.text.*)
*(.rodata*)
*(.gnu.linkonce.t*)
*(.gnu.linkonce.r*)
}
.preinit_array : {
__preinit_array_start = . ;
*(.preinit_array)
__preinit_array_end = . ;
}
.init_array : {
__init_array_start = . ;
*(.init_array)
__init_array_end = . ;
}
.fini_array : {
__fini_array_start = . ;
*(.fini_array)
__fini_array_end = . ;
}
.ctors : {
__CTOR_LIST__ = . ;
LONG((__CTOR_END__ - __CTOR_LIST__) / 4 - 2)
*(.ctors)
LONG(0)
__CTOR_END__ = . ;
}
.dtors : {
__DTOR_LIST__ = . ;
LONG((__DTOR_END__ - __DTOR_LIST__) / 4 - 2)
*(.dtors)
LONG(0)
__DTOR_END__ = . ;
}
...

Como se puede ver, se definen dos secciones al principio del linker script:

- Una a partir de la dirección 0x08000000 y en la que se alojará el código de las funciones etiquetadas con la sección ".startup0" del fichero startup.o.

- Otra a partir de la dirección 0x08000200 y en la que se alojará primero el código de las funciones que se encuentren etiquetadas con la sección ".startup1" seguido por el resto del código del programa.

A continuación se ubican las secciones estándar ".preinit_array", ".init_array", ".ctors" encargadas de alojar las llamadas a los constructores y las funciones de inicialización de variables globales, y las secciones estándar ".fini_array" y ".dtors", encargadas de alojar las llamadas a los destructores y las funciones de destrucción de variables globales (esta parte del código no se debería ejecutar jamás en un sistema embebido, se incluye por elegancia).

Variables globales en RAM y copia de datos de Flash a RAM

...
flash_sdata = . ;
. = 0x20000000 ;
ram_sdata = . ;
.data : AT (flash_sdata) {
_linker_data = . ;
*(.data)
*(.data.*)
*(.gnu.linkonce.d*)
}
ram_edata = . ;
data_size = ram_edata - ram_sdata;
ram_sbssdata = . ;
.bss : AT (LOADADDR(.data) + SIZEOF(.data)) {
_linker_bss = . ;
*(.bss)
*(.bss.*)
*(.gnu.linkonce.b.*)
*(.COMMON)
}
ram_ebssdata = . ;
bssdata_size = ram_ebssdata - ram_sbssdata;
_linker_end = . ;
end = . ;
}

En esta parte del linker script se indica que la dirección virtual de los datos es 0x20000000 (esta es la dirección que se usará desde el código de programa para acceder a los datos en RAM), pero sin embargo dichos datos están inicialmente alojados en la memoria flash (a continuación de la sección ".dtors"), por lo que es necesario un copiado de dichas variables globales inicializadas desde la memoria flash hasta la RAM antes de que se ejecute la función "main".

Código de arranque

En el fichero startup.cc se definen dos funciones _startup_0 y _startup_1 y se etiquetan como de las secciones ".startup0" y ".startup1", respectivamente. La función _startup_0 está definida con el atributo "naked" (para que el compilador no genere código de preámbulo ni postámbulo) y contiene una única instrucción de salto:

void _startup_0() __attribute__((section(".startup0"), naked));

void _startup_0() {
    asm volatile (
        "j %0" : : "i" (_startup_1)
    );
}

Esta es la función que se aloja en la dirección 0x08000000 y que, como se vio antes, es la encargada de saltar a _startup_1. La función _startup_1, que también está definida como "naked" pero etiquetada en la sección ".startup1" (por lo que se aloja a partir de la dirección 0x08000200) alberga el resto de código de inicialización:

void _startup_1() __attribute__((section(".startup1"), naked));

void _startup_1() {
    asm volatile (
        "la sp, 0x20005000"  // point SP to the end of SRAM
    );
    _initClock();
    _initDataRAM();
    _initBssRAM();
    _callConstructors();
    _callInitArray();
    main();
    _callFiniArray();
    _callDestructors();
    while (true)
        ;
}

Y realiza lo siguiente:

1. Inicializa el puntero de pila para que apunte al final de la RAM (el puntero de pila se decrementa cuando se hace "push" y se incrementa cuando se hace "pop").

2. Inicializa los PLLs del microcontrolador para configurar el reloj de cristal (en mi caso he dejado esta función vacía por lo que el microcontrolador queda funcionando con el reloj RC interno que es más lento pero suficiente para nuestro blinker).

3. Inicializa la RAM que se copia desde la flash (ver aquí los detalles de este proceso).

4. Inicializa la RAM que debe estar a cero (BSS).

5. Invoca las funciones de inicialización y los constructores.

6. Finalmente incova a la función "main".

Al final se coloca una especie de código "de cortesía" para que, en el caso de que la función "main" regrese (cosa que no debería ocurrir), se invoquen los destructores y el micro se quede "colgado" (bucle infinito).

Prueba de concepto

Se ha optado por hacer un simple blinker basado en esperas estándar de ciclos de procesador, en lugar de un timer, ya que, como se comentó antes, el núcleo básico de un RISC-V no incluye interrupciones y creo que es mejor abordarlas en una siguiente entrega:

#include 

using namespace std;

#define  RCU_APB2EN  *((uint32_t *) 0x40021018)
#define  GPIOC_CTL1  *((uint32_t *) 0x40011004)
#define  GPIOC_OCTL  *((uint32_t *) 0x4001100C)

int main() {
    // enable clock on port C
    RCU_APB2EN |= ((uint32_t) 1) << 4;
    // PC13 pin is output, low speed, push-pull
    GPIOC_CTL1 = 0x44244444;
    while (true) {
        for (uint32_t i = 0; i < 200000; i++)
            ;
        GPIOC_OCTL ^= (((uint32_t) 1) << 13);
    }
}

La placa en la que está alojada el GD32VF103 es una Longan Nano que posee un led tricolor con la componente roja conectada al pin PC13 del micro. Para hacer parpadear este led rojo debemos:

1. Habilitar el reloj interno de la circuitería GPIO del micro.

2. Configurar el pin PC13 como GPIO de salida en push-pull.

3. Modificar el bit 13 del registro asociado de forma periódica para hacer que el led parpadee.

Los registros son una copia casi exacta de los del STM32F103 por lo que si estás acostumbrado a ese micro de ST, adaptarte al GD32VF103 será muy sencillo.

Bootloader

Cuando arranca el microcontrolador GD32VF103, si el pin BOOT0 se encuentra a nivel 1, no se arranca desde la dirección 0x08000000, sino que se ejecuta un "bootloader" interno que configura el microcontrolador como un dispositivo USB con interfaz estándar DFU (Device Firmware Upgrade). Se trata de un estándar conocido y para el que existen herramientas como dfu-util (que, en el caso de Linux, sólo requiere de la librería "libusb"). Con el microcontrolador en modo DFU y el comando "dfu-util" podemos "tostar" nuestro ejecutable "main.bin" de forma muy sencilla:

dfu-util --dfuse-address 0x08000000 -D main.bin

En la sección soft está todo el código disponible.



Comentarios 
Lo sentimos. No se permiten nuevos comentarios después de 90 días.