Gestión de interrupciones en microcontroladores RISC-V 
La gestión de las interrupciones es un tema planteado de forma muy básica en la arquitectura original RISC-V. En controlador básico de interrupciones que se describe en los documentos oficiales de la arquitectura está más orientado a la captura de eventos entre diferentes niveles de ejecución que a la captura de eventos externos, algo que, en el caso de los microcontroladores, se echa de menos. Abordaremos el uso del este controlador básico de interrupciones y el uso del CLIC, un controlador de interrupciones más avanzado, que complementa al primero y que está disponible en casi todos los núcleos RISC-V.

Controlador básico de interrupciones

El control de interrupciones que se describe en el volumen 2 de la especificación es un control de interrupciones muy básico orientado más a la captura de eventos entre diferentes niveles de privilegio (captura desde el nivel supervisor de eventos producidos en el nivel usuario, o captura desde el nivel máquina de eventos producidos en el nivel supervisor) que a la captura de eventos externos. De hecho, en el controlador de interrupciones "oficial" sólo tenemos como "interrupciones" (tal y como las entendemos casi todos los que venimos del mundo de lo microcontroladores) dos tipos: las de timer y las etiquetadas genéricamente como "externas" (estas ultimas, dependientes de la implementación que haga cada fabricante).

Como se puede ver, se trata de un mecanismo de interrupción muy pobre y que, aunque muy bien pensado para entornos multiusuario, es insuficiente para entornos embebidos, en los que todo el código se ejecutará casi siempre con los niveles de privilegio M (de máquina) y apenas se usan otros niveles de privilegio. Sin embargo, en caso de que sólo necesitemos una interrupción de timer, sería la mejor opción, ya que está presente en todas las implementaciones de RISC-V (forma parte de la especificación estándar y siempre existirá).

Los CSR

Muchos de los parámetros de funcionamiento en los procesadores RISC-V se configuran mediante CSR (Control and Status Registers), dichos registros son registros internos del procesador que son accesibles mediantes instrucciones de esamblador especiales de transferencias de valores desde/hasta registros normales (CSRR, CSRW, etc.) o mediante valores inmediatos (CSRWI, etc.). Existen una serie de CSRs estándar y, además, cada fabricante puede incluir los suyos propios, siempre y cuando se mantengan los del estándar de RISC-V.

...
// poner a 1 el bit 3 del CSR "msi"
asm volatile (
    "csrsi mstatus, 8"
);
...
// escribir 0x08000000 en el CSR número 0x307 (cuando no podemos usar el nombre)
asm volatile (
    "csrw 0x307, %[reg]" : : [reg] "r" (0x08000000)
);
...

En este ejemplo de código el ensamblador que viene con las binutils sí reconoce el símbolo "mstatus" como CSR (al ser un CSR estándar presente en todos los núcleos RV32I), sin embargo el CSR "mtvt" es un CSR definido para el controlador de interrupciones avanzado (CLIC) y, como no está definido como símbolo en las "binutils", debemos poner su valor (0x307) si queremos acceder a él desde ensamblador.

Ejemplo 1: Blinker usando el mecanismo básico de interrupciones

Como dijimos antes, el mecanismo básico de interrupciones permite el uso de una interrupción de timer así como de una interrupción externa. En este caso usaremos este mecanismo básico de interrupciones para configurar la interrupción de timer con el objetivo de hacer parpadear un led.



Lo primero que hacemos es configurar en el CSR llamado "mtvec" el vector de interrupción, que podrá ser vectorizado o no, es decir, podremos indicar un puntero a una función o la dirección de memoria de una tabla donde estén indicadas, a su vez, las direcciones de salto de cada uno de las interrupciones que se definan (al estilo ARM Cortex-M). La forma en la que el RISC-V debe interpretar este valor (si como puntero a función o como puntero a tabla) se debe indicar en los dos bits menos significativos de este CSR. Como vamos a usar el mecanismo básico de interrupciones, la cantidad de interrupciones que se pueden generar es muy escaso y, por tanto, he optado por indicar directamente un puntero a una función (modo directo, mode = 00): Nótese que los bits 0 y 1 de la dirección de memoria de la función deben estar a 0 (puesto que estos bits en el CSR de mtvec se usan para indicar el modo), lo que obliga a que el handle de interrupción esté en una dirección de memoria múltiplo de 4 (alineada a 32 bits).

A continuación configuramos el CSR "mie" (Machine Interrupt Enable), donde decimos, para el nivel de privilegio M (el de máquina, con el que arranca el RISC-V por defecto) qué interrupciones se habilitan (activamos el bit correspondiente al timer):



En este CSR ponemos a 1 el bit 7 (MTIE = Machine Timer Interrupt Enable), mientras que en el CSR "mstatus" debemos habilitar las interrupciones de nivel de máquina poniendo a 1 el bit 3.



Con estos pasos ya tenemos configurado el controlador básico de interrupciones para que se dispare la interrupción de timer. El timer es muy parecido al SysTick de los procesadores ARM Cortex-M: un contador que se incrementa a la misma velocidad que el reloj del sistema y que, en el caso del RISC-V, se configura con dos registros mapeados en memoria:

- MTIME: Un registro de 64 bits que se va incrementando de forma indefinida y que cuando se desborda vuelve a cero (de lectura y escritura).

- MTIMECMP: Un registro de 64 bits que sólo se escribe desde software (aunque también es de lectura y escritura) y que, cuando vale igual que mtime, se produce la interrupción de timer en caso de estar ésta configurada.

Las direcciones de memoria donde están mapeados estos dos registros de 64 bits no están definidas por el estándar, son de libre elección por parte del fabricante. En el caso del GD32VF103, se alojan en 0xD1000000 y 0xD1000008 respectivamente.

#include <stdint.h>

using namespace std;

#define  RCU_APB2EN  *((volatile uint32_t *) 0x40021018)
#define  GPIOC_CTL1  *((volatile uint32_t *) 0x40011004)
#define  GPIOC_OCTL  *((volatile uint32_t *) 0x4001100C)
#define  MTIME       *((volatile uint64_t *) 0xD1000000)
#define  MTIMECMP    *((volatile uint64_t *) 0xD1000008)

#define  MTIME_INTERRUPT_PERIOD  12000000    // 24 MHz, so 12000000 generates an interrupt period of exactly half a second

void interruptHandler() __attribute__ ((interrupt, section(".interrupt_handler")));

void interruptHandler() {
    MTIME = 0;
    MTIMECMP = MTIME_INTERRUPT_PERIOD;
    GPIOC_OCTL = GPIOC_OCTL ^ (((uint32_t) 1) << 13);
}

int main() {
    // enable clock on port C
    RCU_APB2EN = RCU_APB2EN | (((uint32_t) 1) << 4);
    // PC13 pin is output, low speed, push-pull
    GPIOC_CTL1 = 0x44244444;
    // basic (non vectored) interrupt handler (to force non vectored, set 0 to lower two bits of mtvec, so force 4 byte aligned on linker script for interrupt handler)
    asm volatile (
        "csrw mtvec, %[reg]" : : [reg] "r" ((uint32_t) interruptHandler)
    );
    // machine interrupt enable
    asm volatile (
        "csrw mie, %[reg]" : : [reg] "r" ((uint32_t) 0x80)
    );
    asm volatile (
        "csrsi mstatus, 8"
    );
    // configure interrupt period
    MTIME = 0;
    MTIMECMP = MTIME_INTERRUPT_PERIOD;
    // sleep
    while (true)
        asm volatile ("wfi");
}

Paso a comentar las partes más relevantes del código:

1. Definimos la función "interruptHandler" (aunque da igual el nombre que le pongamos), le ponemos los atributos de compilador "interrupt" y "section(".interrupt_handler")" (el nombre de la sección también da igual, lo importante es que no esté en la sección ".text" que es donde se pone por defecto todo el código). En la función reiniciamos los registro MTIME y MTIMECMP y cambiamos de estado el led. No es necesario marcar ningún flag para indicar que la interrupción está atendida ya que, según la documentación del estándar del RISC-V, al escribir en la dirección de memoria de MTIMECMP el procesador ya considera atendida la interrupción de timer.

2. En la función main habilitamos el reloj para la circuitería de GPIO, configuramos el puerto PC13 como salida push-pull a baja velocidad y configuramos la interrupción de timer: Escribimos en "mtvec" la dirección de memoria del handler que hemos definido antes (como estará alineado a 32 bits, sus dos bits menos significativos ya valdrán 0, por lo que, de paso, también seleccionamos que queremos un tratamiento de interrupciones no vectorizado, sino definiendo una única función para atender todas las interrupciones), habilitamos los bits de "mie" y "mstatus" que vimos antes y configuramos los registros MTIME y MTIMECMP para que el tiempo entre interrupciones sea de medio segundo (sabiendo que el reloj del timer va a 96 MHz / 4 = 24 MHz).

3. Entramos en un bucle infinito con la instrucción "wfi" para que el procesador permanezca dormido y en modo de bajo consumo mientras no esté atendiendo interrupciones.

Para que este código funcione correctamente es necesario, además de "startup.cc", que se encarga de inicializar todo el subsistema de relojes para que el núcleo vaya a 96 MHz, que en el linker script estén correctamente indicadas las secciones, sobre todo la sección que hemos llamado ".interrupt_handler", ya que debemos forzar a que dicha sección esté alineada a 32 bits (para que su dirección de memoria sea múltiplo de 4 y así los dos bits menos significativos de dicha dirección de memoria valgan 0).

SECTIONS {
    . = 0x08000000 ;
    .text : {
        startup.o (.startup0)
    }
    . = 0x08000200 ;
    .text : {
        _linker_code = . ;
        startup.o (.startup1)
        *(.text)
        *(.text.*)
        *(.rodata*)
        *(.gnu.linkonce.t*)
        *(.gnu.linkonce.r*)
    }
    . = ALIGN(4);     /* to force lower 2 bits of address to 0 (mtvec.mode = 0 to select non vectored interrupt handler) */
    .interrupt_handler : {
        *(.interrupt_handler)
    }
...
}


Ejemplo 2: Blinker usando el CLIC (Core Local Interrupt Controller)

El CLIC (Core Local Interrupt Controller) es aún una propuesta de estándar, que se encuentra documentada en el GitHub de RISC-V International, pero que ya está siendo implementada por múltiples fabricantes, entre ellos GigaDevice (en su GD32VF103), SiFive (en todos sus procesadores) y muchos otros. Es una propuesta de controlador de interrupciones compatible con el sistema básico de interrupciones, pero que permite hasta 4096 interrupciones externas, configurables por niveles, prioridad, etc. Al ser una propuesta de la propia RISC-V International, lo cierto es que se ha convertido en estándar "de facto" y sólo cabe esperar que se acabe poniendo de forma "bonita" en un documento de especificación, en lugar de como está ahora. El CLIC define una serie de CSRs nuevos además de un conjunto de registros mapeados en memoria para el manejo "ampliado" de interrupciones.

En este caso, aunque el CLIC puede usarse con interrupciones no vectorizadas, es recomendable configurarlo para usarlas vectorizadas. En un CSR denominado "mclicbase" se encuentra la dirección de memoria base a partir de la que se localiza el resto de registros mapeados en memoria del CLIC. En el caso del GD32VF103 he optado por poner a fuego dicho valor, ya que es conocido (está indicado en la documentación del fabricante) y de esta forma también simplificamos el código generado (aunque sea menos portable). A partir de la dirección base del CLIC se obtiene el resto de registros mapeados en memoria.

#define  CLIC_BASE                    0xD2000000
#define  CLIC_IP(source)              *(volatile uint8_t *)(CLIC_BASE + 0x00001000 + ((source) * 4))
#define  CLIC_IE(source)              *(volatile uint8_t *)(CLIC_BASE + 0x00001001 + ((source) * 4))
#define  CLIC_ATTR(source)            *(volatile uint8_t *)(CLIC_BASE + 0x00001002 + ((source) * 4))
#define  CLIC_CTL(source)             *(volatile uint8_t *)(CLIC_BASE + 0x00001003 + ((source) * 4))
#define  CLIC_CFG                     *(volatile uint8_t *)(CLIC_BASE + 0x00000000)
#define  CLIC_MTH                     *(volatile uint8_t *)(CLIC_BASE + 0x0000000B)
#define  CLIC_IP_IE_ATTR_CTL(source)  *(volatile uint32_t *)(CLIC_BASE + 0x00001000 + ((source) * 4))

Para configurar las interrupciones vectorizadas con el CLIC primero preparamos la tabla de interrupciones que, en el caso del GD32VF103, debe estar a partir de la dirección 0x00000000 (limitaciones del propio fabricante) y que nosotros situaremos a partir de la dirección 0x08000000, ya que ambas zonas de memoria son alias una de la otra y es a partir de 0x08000000 donde se encuentra la memoria flash de programa. Recordemos que en el caso del GD32VF103, la dirección de memoria 0x08000000 es la dirección de reset y que en esa posición lo que tenemos es una instrucción "jump" hacia 0x08000200 que es donde empieza la ejecución del código. Es en este "hueco" entre 0x08000000 y 0x08000200 es donde se aloja la tabla de vectores de interrupción. Como sólo vamos a configurar uno de los vectores de interrupción (el del timer, que se corresponde con la posición 7), hacemos que el linker script situe ahí (0x08000000 + (7 * 4) = 0x0800001C) la dirección de memoria de una sección que llamaremos ".clic_int_tmr" (el nombre no es relevante).

SECTIONS {
    . = 0x08000000 ;
    .text : {
        startup.o (.startup0)
    }
    . = 0x0800001C ;
    .clic_int_tmr_vector : {
        LONG(CLIC_INT_TMR_ADDRESS);
    }
    . = 0x08000200 ;
    .text : {
        _linker_code = . ;
        startup.o (.startup1)
        *(.text)
        *(.text.*)
        *(.rodata*)
        *(.gnu.linkonce.t*)
        *(.gnu.linkonce.r*)
    }
    . = ALIGN(4);
    CLIC_INT_TMR_ADDRESS = . ;
    .clic_int_tmr : {
        *(.clic_int_tmr)
    }
    ...
}

A continuación, en "main.cc" definimos nuestra función de manejo de las interrupciones:

void clicIntTmr() __attribute__ ((interrupt, section(".clic_int_tmr")));

void clicIntTmr() {
    MTIME = 0;
    MTIMECMP = MTIME_1_SEC;
    GPIOC_OCTL = GPIOC_OCTL ^ (((uint32_t) 1) << 13);   // toggle led
    CLIC_IP(7) = 0;    // not pending
}

También aplicamos a esta función el atributo "interrupt" e indicamos que dicha función debe estar en la sección ".clic_int_tmr" (el nombre que elegimos en el linker script). La única diferencia notable con respecto al caso del mecanismo básico de interrupciones es que aquí nos aseguramos de borrar de forma explícita el flag "interrupt pending" (IP) antes de regresar de la interrupción.

La preparación del CLIC es ligeramente más compleja que en el caso del mecanismo básico de interrupciones:

int main() {
    // enable clock on port C
    RCU_APB2EN = RCU_APB2EN | (((uint32_t) 1) << 4);
    // PC13 pin is output, low speed, push-pull
    GPIOC_CTL1 = 0x44244444;
    // clear CLIC config register and set vectored interrupts
    CLIC_CFG = 0;
    CLIC_MTH = 0;   // specific register for GD32VF103 ECLIC
    for (uint16_t i = 0; i < 4096; i++)
        CLIC_IP_IE_ATTR_CTL(i) = 0;
    // use CLIC vectored interrupt handler (put 1 on mode (lower two) bits of mtvec)
    asm volatile (
        "csrw mtvec, %[reg]" : : [reg] "r" (0x00000003)
    );
    // machine interrupt enable
    asm volatile (
        "csrsi mstatus, 8"
    );
    // set CLIC interrupt vector table (mtvt = 0x307)
    asm volatile (
        "csrw 0x307, %[reg]" : : [reg] "r" (0x08000000)
    );
    CLIC_ATTR(7) = 0b11000001;    // machine mode, level triggered, vectored
    CLIC_IE(7) = 1;               // enable interrupt 7
    CLIC_IP(7) = 0;               // not pending
    CLIC_CTL(7) = 0;              // priority
    // configure interrupt period
    MTIME = 0;
    MTIMECMP = MTIME_1_SEC;
    while (true)
        asm volatile ("wfi");
}

Los pasos son los siguientes:

1. Inicializamos el CLIC según indica la documentación: borrando CLIC_CFG, CLIC_MTH y poniendo a cero los 4096 registros de configuración mapeados en memoria.

2. Indicamos en el CSR "mtvec" que vamos a usar el CLIC en lugar del mecanismo básico de interrupciones poniendo a 1 los dos bits menos significativos de "mtvec". Recordemos que en el caso del mecanismo básico de interrupciones, estos dos bits estaban a 0.

3. Indicamos en el CSR "mstatus" que se habilitan las interrupciones a nivel de máquina (M). Igual que como hacíamos con el mecanismo básico de interrupciones.

4. Escribimos en un CSR nuevo definido para CLIC y llamado "mtvt" la dirección de memoria de la tabla de vectores de interrupción, que debe estar alineada a 64 bytes (en nuestro caso la dirección de memoria 0x08000000 cumple con este requisito). Nótese que el CSR "mtvt" no se reconoce por parte de las binutils de GNU, por lo que en el ensamblador generado hemos tenido que usar el valor numérico que corresponde con dicho CSR (0x307).

5. Configuramos en los registros del CLIC mapeados en memoria (en la posición correspondiente a la interrupción 7 de los 4096 registros), la prioridad, el tipo de interrupción y la propia habilitación de la misma (CLIC_ATTR, CLIC_IE, CLIC_IP y CLIC_CTL).

6. El resto de operaciones son las mismas que en caso del mecanismo básico de interrupciones de timer: inicializar MTIME y MTIMECMP y entrar en el bucle infinito en el que dormimos al procesador con la instrucción "wfi" cuando no está atendiendo interrupciones.

El resultado es el mismo: un led que parpadea, pero utilizando las dos aproximaciones distintas al mecanismo de interrupciones que permite hoy día la arquitectura RISC-V.

Todo el código está disponible en la sección soft.

[ 2 comentarios ] ( 1408 visualizaciones )   |  [ 0 trackbacks ]   |  enlace permanente
  |    |    |    |   ( 3.1 / 90 )

<Anterior | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Siguiente> >>