Interrupciones usando funciones del kernel

El programa presentado a continuación es una implementación básica, un ejemplo funcional para manejar tareas, semáforos, eventos, e interrupciones utilizando las funciones del kernel del sistema operativo.


kernel_cfg.c actúa como un "mapa" para el kernel, indicando qué recursos están disponibles y cómo están organizados.


main.c contiene la lógica principal del sistema y define las tareas:

init_task: Inicializa el sistema (temporizadores, UART, reloj, LEDs).

main_task: Controla el flujo general del programa. Arranca las tareas adicionales (test1_task y test2_task).

test1_task y test2_task: Ejemplos de tareas con funcionalidades específicas (contadores, temporizadores).

idle_task: Tarea de reposo que mantiene el sistema en un estado estable cuando no hay tareas activas.


Interrupciones y manejo de hardware: se configuran interrupciones de hardware que activan ciertas tareas (test1_task y test2_task) o realizan acciones específicas.


Conceptos Clave

Las tareas son funciones que se ejecutan de manera concurrente bajo la gestión del kernel. Cada tarea tiene una prioridad y un contexto independiente. Por ejemplo, main_task es la tarea principal del programa y controla el flujo general del sistema.

Los semáforos son mecanismos de sincronización que controlan el acceso a recursos compartidos. Si varias tareas intentan acceder al mismo recurso, un semáforo puede garantizar que solo una lo haga a la vez, evitando conflictos o errores.

Las flags de eventos permiten que una tarea se "despierte" o ejecute una acción en respuesta a un evento específico, como la llegada de datos o la finalización de una operación.

Los mutexes son similares a los semáforos, pero están más enfocados en garantizar que solo una tarea pueda acceder a un recurso crítico en un momento dado, asegurando exclusión mutua y evitando condiciones de carrera.

Los temporizadores generan eventos en intervalos definidos. Se usan para crear retardos, ejecutar acciones periódicas o medir el tiempo transcurrido entre eventos. En este programa, los temporizadores se configuran mediante canales.

Un canal, dentro del contexto de los temporizadores, se puede imaginar como una alarma dentro de un reloj. Cada canal puede configurarse para sonar en momentos diferentes y realizar tareas específicas. El sistema, en este caso, tiene dos canales. El Canal 1 puede sonar a una hora para activar una interrupción y despertar una tarea, mientras que el Canal 2 puede hacerlo a otra hora distinta. Cuando una alarma suena, despierta la tarea correspondiente para que continúe trabajando.

Las interrupciones responden a eventos externos generados por el hardware, como la expiración de un temporizador o la activación de una señal en un pin. Estas interrupciones suelen estar vinculadas a tareas específicas para manejar eventos en tiempo real.


Flujo Básico del Programa

  1. Inicio del sistema:

init_task configura el hardware (LEDs, UART, temporizadores) y el kernel para manejar tareas.

  1. Ejecución de la tarea principal (main_task):

Muestra información básica en la consola (modo de arranque y versión del compilador).

Arranca las tareas test1_task y test2_task.

  1. Tareas en paralelo:

test1_task y test2_task realizan sus funciones específicas (incrementar contadores, manejar temporizadores) mientras el sistema opera.

  1. Tarea de inactividad (idle_task):

Siempre está activa, pero no realiza acciones importantes. Sirve como "tarea de respaldo".

  1. Interrupciones y sincronización:

Las interrupciones del temporizador (TAUJ2I1 y TAUJ2I2) despiertan las tareas test1_task y test2_task, asegurando que respondan a eventos en tiempo real.


Propósito del Código

Este ejemplo sirve como base para comprender cómo estructurar un sistema embebido utilizando μITRON. Las funcionalidades básicas que implementa son:

Manejo de múltiples tareas con diferentes prioridades.

Sincronización mediante semáforos y eventos.

Control de hardware (temporizadores, LEDs, UART).

Respuesta a interrupciones externas.

kernel_cfg.c

1. Inclusión de bibliotecas:

#include "kernel.h"

#include "kernel_id.h"

#include "RIN32M3.h"

Estas son las bibliotecas principales del kernel del sistema y del hardware específico (R-IN32M3).

kernel.h y kernel_id.h son para la configuración y control del sistema operativo.

RIN32M3.h contiene definiciones de hardware específicas del microcontrolador.



2. Definición de tareas:

const TSK_TBL static_task_table[] = {

    {ID_TASK_INIT, {TA_HLNG | TA_ACT, 0, (FP)init_task, 1, 0x400, NULL}},

    {ID_TASK_MAIN, {TA_HLNG | TA_ACT, 0, (FP)main_task, 3, 0x400, NULL}},

    {ID_TASK_TEST1, {TA_HLNG, 0, (FP)test1_task, 5, 0x400, NULL}},

    {ID_TASK_TEST2, {TA_HLNG, 0, (FP)test2_task, 6, 0x400, NULL}},

    {ID_TASK_IDLE, {TA_HLNG | TA_ACT, 0, (FP)idle_task, 15, 0x100, NULL}},

    {TASK_TBL_END, {0, 0, (FP)NULL, 0, 0, NULL}}

};

Aquí se definen las tareas del sistema. Cada entrada tiene:

tskid: Identificador único de la tarea.

tskatr: Atributos de la tarea (TA_HLNG para alto nivel y TA_ACT para activar automáticamente).

exinf: Información extendida, generalmente 0.

task: Puntero a la función de la tarea.

itskpri: Prioridad inicial (1 es la más alta, 15 es la más baja).

stksz: Tamaño de la pila de la tarea.

stk: Puntero a la pila (NULL si el kernel la asigna automáticamente).


Ejemplo práctico:

ID_TASK_INIT llama a init_task con prioridad 1 y un tamaño de pila de 0x400 bytes.



3. Definición de semáforos, flags, buzones y mutexes:

const SEM_TBL static_semaphore_table[] = {

    {ID_APL_SEM1, {TA_TFIFO, 0, 1}},

    {ID_APL_SEM2, {TA_TFIFO, 0, 1}},

    {SEMAPHORE_TBL_END, {0, 0, 0}}

};

Tabla de semáforos: controla el acceso concurrente a recursos compartidos.

isemcnt indica el valor inicial del semáforo.

maxsem define el valor máximo que puede alcanzar.



4. Definición de manejadores de interrupción:

Esta tabla (static_hwisr_table) se utiliza para decirle al sistema qué hacer cuando ocurre una interrupción de hardware específica.

const HWISR_TBL static_hwisr_table[] = {

    {TAUJ2I1_IRQn, HWISR_WUP_TSK, ID_TASK_TEST1, 0x0000},

    {TAUJ2I2_IRQn, HWISR_WUP_TSK, ID_TASK_TEST2, 0x0000},

    {HWISR_TBL_END, 0, 0, 0}

};

TAUJ2I1_IRQn y TAUJ2I2_IRQn: Son un número entero. Se usan como identificadores únicos que representan interrupciones generadas por el temporizador TAUJ2. Cada canal del temporizador (como "alarmas" independientes) puede generar una interrupción.

HWISR_WUP_TSK: Es lo que hará el sistema cuando ocurra esta interrupción, debe despertar una tarea específica que esté dormida o en espera.

ID_TASK_TEST1 e ID_TASK_TEST2: Estas son las tareas que el sistema debe despertar. Cada interrupción está vinculada a una tarea específica:

Si ocurre la interrupción en el Canal 1 (TAUJ2I1_IRQn), se despierta la tarea ID_TASK_TEST1.

Si ocurre en el Canal 2 (TAUJ2I2_IRQn), se despierta la tarea ID_TASK_TEST2.

0x0000: Este es un campo que puede usarse para parámetros adicionales o configuraciones específicas de la interrupción. Aquí no se utiliza, así que se deja en 0x0000.


main.c

1. Inicialización:

Macros útiles:

#define LED_INIT() { /* Código para configurar LEDs como salida */ }

#define LED_INVERT(d) { /* Código para invertir el estado de los LEDs */ }

Estas macros controlan el hardware del LED conectado al microcontrolador.


Función init_task:

void init_task(int exinf) {

    hwos_init(); // ①

    LED_INIT(); //

    timer_interval_init(TIMER_CHANNEL_1, 1); //

    timer_start(TIMER_CHANNEL_1); //

    uart_init(SYS_UART_CH); //

    clock_init(); //

    ext_tsk(); //

}

  1. Inicializa el sistema operativo en R-IN32M3.

  2.  Configura los LEDs.

  3.  Configura un temporizador para 1ms. No se está usando.

  4. Inicia el temporizador.

  5. Inicializa el UART para comunicación serie.

  6. Configura el reloj del sistema.

  7. Finaliza la tarea. No se ejecuta debido al bucle infinito, está presente como precaución o plantilla, en caso de que el diseño cambie en el futuro.


2. Función principal:

void main_task(int exinf) {

    uint32_t boot_mode;

    boot_mode = ((RIN_SYS->MDMNT >> 7) & 0x3); // Lee el modo de arranque desde un registro.

    printf("hello world\n");

    printf("- compiler  =%s\n", COMPILER_VERSION);

    printf("- boot mode =%s\n", BOOT_MODE(boot_mode));

    sta_tsk(ID_TASK_TEST1, 0); // Inicia la tarea 1.

    sta_tsk(ID_TASK_TEST2, 0); // Inicia la tarea 2.

    while (1) {

        tslp_tsk(100); // Duerme la tarea actual (la que sea que se esté ejecutando en el momento) por 100 ms.

    }

    ext_tsk(); // Finaliza la tarea.

}



3. Tareas adicionales:

test1_task:

void test1_task(int exinf) {

    while (1) {

        slp_tsk(); // Espera a ser despertada.

        test1_task_count++; // Incrementa un contador.

    }

}

La tarea comienza dormida gracias a la función slp_tsk(), que la suspende para que no consuma recursos mientras espera un evento. Cuando ocurre dicho evento, el kernel despierta la tarea y esta continúa su ejecución. Una vez activa, realiza su trabajo, que en este caso es incrementar un contador. Al finalizar, vuelve a entrar en el estado de espera, repitiendo este ciclo de dormir, ser despertada, trabajar y regresar a dormir.


test2_task:

void test2_task(int exinf) {

    while (1) {

        tslp_tsk(1000); // ① 

        timer_onecount_swtrg_init(TIMER_CHANNEL_2, 500); // ② 

        timer_start(TIMER_CHANNEL_2); // ③ 

        test2_task_count++; // ④ 

        if (!(test2_task_count % (1000 * 1))) {

            printf("."); // Imprime un punto cada cierto tiempo.

        }

    } // ⑥ 

}

① La tarea se suspende durante 1000 milisegundos (1 segundo). Durante este tiempo, el kernel puede dar CPU a otras tareas o realizar otras operaciones del sistema.

② Configura el canal 2 del temporizador para disparar un evento (generar una interrupción) después de 500 milisegundos. Es un temporizador de "un solo conteo", lo que significa que se ejecuta una vez y luego se detiene automáticamente.

③ Inicia el temporizador. Comienza a contar el tiempo configurado (500 ms en este caso).

④ Cada vez que la tarea completa un ciclo de trabajo, incrementa el contador global test2_task_count. Esto podría ser usado para monitorear cuántas veces se ejecuta esta tarea.

⑤ Comprueba si test2_task_count es un múltiplo de 1000. Si es así, imprime un punto en la consola (1 vez cada 1000 ciclos de ejecución).

⑥ Repite el ciclo. Duerme 1000 milisegundos, configura y dispara el temporizador, incrementa el contador, imprime el punto si corresponde.


4. Tarea de inactividad:

void idle_task(int exinf) {

    while (1) {

        // No hace nada, conserva la CPU en estado de reposo.

    }

}


Este programa en concreto:

Ejecuta task1 (se duerme a sí misma).

Ejecuta task2: En un bucle infinito, duerme, configura un temporizador que provoca una interrupción, incrementa un contador que se usa solo para imprimir un punto, repite el ciclo. La interrupción no hace mas que despertar a la misma tarea que como ya está en un bucle infinito no tiene Ningún efecto. 

Claves para entender las interrupciones en un programa.

Ver la tabla de interrupciones:
Identifica qué interrupciones están declaradas y qué hacen (en este caso, despertar tareas).

Identificar qué genera la interrupción:
Encuentra qué evento provoca la interrupción (por ejemplo, la expiración de un temporizador).

Revisar la configuración del periférico:
Observa cómo está configurado el temporizador u otro hardware que genera la interrupción.

Entender cómo responde el programa:
Revisa si la interrupción despierta tareas, ejecuta código directamente o hace algo más.

Validar el comportamiento:
Comprueba que todo funcione como se espera, ya sea imprimiendo mensajes o con herramientas de depuración.