Es necesario remetirse a tres archivos del proyecto:
kernel_cfg.c
main.c
kernel_id.h
Definir las funciones de las tareas (main.c)
* Primero debemos determinar el objetivo de la tarea (lo que queremos que la tarea haga).
** Las tareas deben ejecutarse en un bucle infinito, y se debe definir un lapso de tiempo para que el sistema operativo maneje su “despacho”.
*** El parámetro “int exinf” es por defecto, debe ser declarado así.
Añadir los prototipos de las tareas (al inicio de kernel_cfg.c)
Configurar las tareas en kernel_cfg.c
TSK_TBL es una tabla donde se declaran las tareas. Las tareas que tienen el atributo TA_ACT son puestas en estado “run” por el sistema operativo al iniciarse. Si la tarea no incluye este atributo deberá ser iniciada manualmente.
No hay una determinada regla para poder definir el valor de “stksz” (el tamaño de la pila), debe el desarrollador ir probando varios valores y verificar el comportamiento del programa. Un ajuste incorrecto de la cantidad de memoria puede dar lugar a “stack overflow” causando comportamientos inesperados, errores, etc.
Añadir los IDs de las tareas en kernel_id.h
* Se puede asignar IDs libremente bajo cualquier criterio que se considere razonable desde los números disponibles aun no utilizados. En μiTron no hay que pasarse del 63.
Iniciar las tareas manualmente en main_task (dentro de main.c)
El primer argumento que recibe sta_tsk() es el id de la tarea, el segundo argumento es un valor de inicialización (con 0 está bien).
Nota a considerar: El sample program que utilicé inicialmente ya tenía una tarea (task) creada. En este caso, se puede eliminar si no es necesaria o, si decidimos conservarla, es importante asegurarnos de que tenga definido un tiempo de suspensión (por ejemplo, tslp_tsk(1000);). Sin esta instrucción, la tarea no permitirá el dispatch (cambio entre tareas en el CPU), lo que afectará la multitarea del sistema.
Debbuging:
Para hacer debbuging utilizando IAR, es necesario contar con un ICE conectado a una placa que corra nuestro programa.
El hecho de hacer Debug en dicha placa no se relaciona con el programa que ya esté flasheado actualmente en la tarjeta, ni lo afecta de ningún modo.
La función sta_tsk (que usamos anteriormente para poner nuestras tasks en modo run) tiene valores de retorno que son códigos de error (0 indica funcionamiento normal). Una técnica valiosa para verificar el buen funcionamiento de la tarea es:
Activar el panel que permite verificar el contenido de las variables locales (View >> Locals)
Crear dentro de main_task() una variable que almacene este código de error devuelto por la función ejecutora del task.
asignar las funciones sta_tsk a esta variable
de esta forma (y sin afectar el funcionamiento de las mismas) se podrá confirmar si se reporta algún error o no.
Declaración y uso de semáforos.
1. Definir el semáforo en kernel_cfg.c
const SEM_TBL static_semaphore_table[] = {
{ID_SEM_TEST, {TA_TFIFO, 0, 1}}, // Semáforo inicializado en 0
{SEMAPHORE_TBL_END, {0, 0, 0}}
};
2. Declarar el ID del semáforo en kernel_id.h
#define ID_SEM_TEST 1
3. Modificar test1_task para señalar al semáforo
void test1_task(int exinf) {
tslp_tsk(500); // Retraso inicial para sincronización
while (1) {
printf("Hello task 1\n");
tslp_tsk(1000); // Simula trabajo
sig_sem(ID_SEM_TEST); // Señalar semáforo
}
}
4. Modificar test2_task para esperar el semáforo
void test2_task(int exinf) {
while (1) {
wai_sem(ID_SEM_TEST); // Esperar señal de test1_task
printf("Hello task 2\n");
tslp_tsk(2000);
}
}
*) Aclaración
La línea tslp_tsk(2000); en test2_task no es estrictamente necesaria después de implementar el semáforo, porque el semáforo ya introduce el mecanismo de sincronización que bloquea test2_task hasta que reciba una señal de test1_task.
Antes de implementar el semáforo Después de implementar el semáforo
Mailbox
¿Qué es un Mailbox en μITRON?
Un mailbox es una estructura utilizada para comunicar tareas enviando mensajes entre ellas. Funciona como un buzón:
Una tarea (emisor) coloca un mensaje en el mailbox.
Otra tarea (receptor) recoge ese mensaje.
Es útil para sincronizar tareas y pasar datos entre ellas.
Cómo Funciona el Mailbox
Configuración:
Se declara un mailbox con un identificador único (en este caso, ID_TEST_MBX1).
El mailbox funciona como un contenedor para almacenar mensajes enviados.
Envío (snd_mbx):
Una tarea (por ejemplo, test1_task) usa snd_mbx para colocar un mensaje en el mailbox.
Si el mailbox está lleno, el envío puede fallar o bloquear la tarea hasta que haya espacio disponible.
Recepción (rcv_mbx):
Otra tarea (por ejemplo, test2_task) usa rcv_mbx para recoger mensajes.
Si el mailbox está vacío, la tarea que llama rcv_mbx se bloquea hasta que se reciba un mensaje.
Fuente: Renesas (Manual de R-IN32M3)
Ejemplo de implementación
Definir el mailbox en la tabla del kernel (kernel_cfg.c en este caso).
kernel_id.h
Implementación de las Tareas
Definir una variable que pueda ser vista tanto por Task1 y Task2 para contener el mensaje que será enviado y recibido.
!) Las funciones de mailBox como snd_mbx() y rcv_mbx() requieren para funcionar recibir como uno de sus parámetros un dato tipo T_MSG.
Este struct “T_MSG” contiene un vector (“array”) con el texto. Y, un puntero a la dirección de memoria donde comienza el mensaje.
El contenido del puntero (msgque) es administrado por μiTRON al usar una función, así que no necesitamos preocuparnos de asignarle nada. El requisito es que la variable contenga esta estructura con ambos atributos.
Fuente: Renesas
test1_task: Enviar mensajes
Esta tarea envía mensajes al mailbox usando snd_mbx. El mensaje es una estructura T_MSG.
*) El uso del semáforo no es necesario porque el mailbox ya maneja la sincronización entre las tareas.
Expliación de snd_mbx(ID_TEST_MBX1, (T_MSG *)&msg);
Se usa porque snd_mbx espera un puntero a T_MSG.
snd_mbx espera dos parámetros.
Uno el ID del mailbox que estamos utilizando, y otro un tipo T_MSG que es un puntero al inicio del mensaje que se va a enviar.
¿Qué hace (T_MSG *)&msg? Conversión de tipo (type casting): Convierte el tipo de msg a un puntero de tipo T_MSG.
Por ejemplo supongamos:
msg es de tipo CUSTOM_MSG.
&msg obtiene la dirección de msg.
(T_MSG *)&msg le indica al compilador que trate msg como si fuera un puntero a T_MSG.
test2_task: Recibir mensajes
Esta tarea espera un mensaje del mailbox con rcv_mbx y lo procesa.
*) El mailbox no solo almacena los mensajes, sino que también gestiona el bloqueo y desbloqueo de las tareas, garantizando que test2_task no intente procesar algo hasta que haya un mensaje. Por eso, agregar un semáforo sería redundante y no aporta valor adicional en este caso. Puedes eliminar la señalización del semáforo y confiar únicamente en el mecanismo del mailbox para la sincronización.
Expliación de rcv_mbx(ID_TEST_MBX1, (T_MSG **)&msg);
Se usa porque snd_mbx espera un DOBLE puntero a T_MSG para almacenar la dirección del mensaje recibido.
rcv_mbx espera dos parámetros.
Uno el ID del mailbox que estamos utilizando, y otro un puntero doble (T_MSG **) que almacena la dirección del mensaje recibido desde el buzón.
¿Qué hace (T_MSG **)&msg? Conversión de tipo doble puntero: Convierte el tipo de &msg a un puntero doble de tipo T_MSG **.
Por ejemplo supongamos:
msg es un puntero de tipo CUSTOM_MSG *.
&msg obtiene la dirección de msg (lo convierte en un puntero doble: CUSTOM_MSG **).
(T_MSG **)&msg le dice al compilador: "Trata la dirección de msg como si fuera un puntero doble de tipo T_MSG **".
¿Por qué se usa?
rcv_mbx espera un doble puntero (T_MSG **) para almacenar la dirección del mensaje recibido.
Al convertir &msg a (T_MSG **)&msg, garantizamos que el compilador lo acepte y μITRON pueda almacenar el puntero recibido en msg.
Eventflag
¿Qué son las Event Flags?
Un flag (bandera) es como un interruptor que puede estar activado (1) o desactivado (0). En los event flags, cada bit en un número representa un flag diferente.
En μITRON, los event flags usan un grupo de 16 bits. Puedes tener hasta 16 flags diferentes en un solo grupo. Debe entenderse por grupo de event flags, cada entrada en la tabla de configuración de flags, como esta:
Un grupo de flags es una estructura que administra un conjunto de hasta 16 banderas binarias (bits). Todas las tareas que interactúan con este grupo comparten el mismo espacio de flags.
En este ejemplo:
ID_TEST_FLG1 es el identificador del grupo.
TA_TFIFO | TA_CLR son los atributos del grupo:
TA_TFIFO: Procesa las tareas en orden FIFO.
TA_CLR: Limpia automáticamente los flags después de ser usados.
0: Inicializa todas las banderas en 0 (todas desactivadas).
Cada flag es un bit en el rango 0x01 (bit 0) a 0x8000 (bit 15).
Si necesitas más de 16 flags, puedes configurar otro grupo adicional.
Por ejemplo, imagina un número con 16 bits (16 interruptores):
Cada bit (de izquierda a derecha) representa un flag:
Bit 0 → FLAG_1
Bit 1 → FLAG_2
Bit 2 → FLAG_3
... hasta Bit 15 → FLAG_16
Si activas un flag, el bit correspondiente cambia a 1. Por ejemplo:
¿Por qué usamos 0x para representar los flags?
Los números en los programas suelen estar en formato hexadecimal (base 16).
0x es una convención en programación para decir: "Este número está en formato hexadecimal".
Hexadecimal es útil porque cada dígito representa 4 bits, lo que hace que sea más compacto que usar binario.
¿Por qué los flags son 0x01, 0x02, 0x04, etc.?
Cada flag es un único bit activado, y en hexadecimal se representa con potencias de 2:
Bit 0 activado → 0x01 (00000001 en binario).
Bit 1 activado → 0x02 (00000010 en binario).
Bit 2 activado → 0x04 (00000100 en binario).
Bit 3 activado → 0x08 (00001000 en binario).
Si activas varios flags al mismo tiempo, se suman en binario:
Resumen:
Cada bit representa un flag.
Bit 0 → FLAG_1 → 0x01
Bit 1 → FLAG_2 → 0x02
Bit 2 → FLAG_3 → 0x04
Y así sucesivamente.
0x significa que estás usando formato hexadecimal.
Hexadecimal es más compacto y fácil de leer que binario.
Puedes combinar flags sumando sus valores.
0x01 | 0x02 → Activa FLAG_1 y FLAG_2 al mismo tiempo (0x03 en total).
En un grupo de event flags, puedes usar hasta 16 bits (0x01 a 0x8000).
Ejemplo de implementación
Definir el flag en la tabla del kernel (kernel_cfg.c en este caso).
kernel_id.h
Implementación de las Tareas
test1_task: Establece las banderas.
Después de enviar un mensaje al mailbox, establece dos banderas:
0x01 para notificar a test2_task.
0x02 para notificar a test3_task
set_flg() tiene dos parámetros.
El ID del flag (un entero entre 1 y 64).
El patrón en bits esperado (debe estar dentro de los 16 bits o devolverá un error).
test2_task: Espera el flag 0x01 antes de recibir el mensaje del mailbox.
wei_flg() tiene cuatro parámetros.
El ID del flag (un entero entre 1 y 64).
El patrón en bits esperado (debe estar dentro de los 16 bits o devolverá un error).
El modo de espera que define cómo se deben interpretar las banderas que la tarea está esperando.
Los valores posibles son:
TWF_ANDW: Espera que todas las banderas especificadas en waiptn estén activas simultáneamente.
Por ejemplo, si waiptn es 0x03 (bits 0 y 1), la tarea se desbloqueará solo cuando FLAG_1 (0x01) y FLAG_2 (0x02) estén activas.
TWF_ORW: Espera que al menos una de las banderas especificadas en waiptn esté activa.
Por ejemplo, si waiptn es 0x03, la tarea se desbloqueará si FLAG_1 o FLAG_2 están activas (no es necesario que ambas lo estén).
*) En este ejemplo usamos TWF_ANDW para garantizar que solo se desbloquee cuando la bandera 0x01 esté activa.
Un puntero donde se puede almacenar el patrón actual de banderas activas cuando la tarea se desbloquea.
Si pasas un puntero (&variable):
La función guardará en esa variable el patrón de banderas activas en el momento en que se satisfaga la condición de espera.
Esto es útil si quieres saber qué banderas estaban activas al desbloquearse.
Si pasas NULL:
Indicas que no te interesa saber cuáles banderas estaban activas.
Es útil cuando solo necesitas que la tarea reaccione al evento y no necesitas información adicional.
En este caso, usamos NULL porque no necesitamos verificar el patrón de banderas al desbloquearse; simplemente actúa cuando se activa la bandera esperada.
test3_task: Espera el flag 0x02 antes de imprimir un mensaje diferente.
Resultados
Cuando ejecutas este programa:
test1_task envía un mensaje y establece dos flags.
test2_task reacciona al flag 0x01, recoge el mensaje y lo imprime.
test3_task reacciona al flag 0x02 y realiza su propia acción.
Interrupciones
Una interrupción es un evento que detiene temporalmente la ejecución normal de un programa para atender una tarea más urgente. Es como una llamada de emergencia: cuando ocurre, el sistema pausa lo que está haciendo, ejecuta un "manejador de interrupción" (ISR, Interrupt Service Routine) y luego regresa al flujo principal.
En sistemas embebidos, las interrupciones funcionan así: el hardware genera una señal de interrupción, por ejemplo, cuando se presiona un botón, expira un temporizador o llega un dato por UART. En ese momento, el procesador pausa la tarea actual y ejecuta el código del ISR, que se encarga de manejar la interrupción (como leer el dato recibido o reiniciar el temporizador). Una vez que el ISR termina, el procesador regresa al programa principal y continúa donde lo dejó.
En sistemas operativos como μITRON, las interrupciones son eventos críticos que interactúan directamente con el kernel para coordinar tareas. El kernel decide qué hacer después de una interrupción, como priorizar la ejecución de una tarea de mayor prioridad o desbloquear una tarea que estaba esperando un evento, como un flag o un mensaje en el mailbox.
El uso de interrupciones ofrece varias ventajas. Primero, su baja latencia permite responder rápidamente a eventos críticos. Segundo, optimizan el uso del CPU al eliminar la necesidad de que el procesador espere activamente un evento; solo responde cuando ocurre. Finalmente, ofrecen una sincronización natural al coordinarse fácilmente con tareas a través de mecanismos como event flags, mailboxes o semáforos.
Para usarlas eficientemente en μITRON, es fundamental que los ISR sean cortos y rápidos, dejando el procesamiento completo a las tareas que se desbloqueen. El kernel manejará automáticamente las prioridades, ejecutando la tarea más importante después de una interrupción. Además, se pueden usar semáforos, mailboxes o event flags para pasar información entre el ISR y las tareas de manera segura y ordenada.
Ejemplo de implementación
Declarar el ISR.
Primero, necesitamos un ISR (Interrupt Service Routine) que se ejecute cuando ocurra la interrupción. Este ISR activará el flag.
En el archivo kernel_cfg.c declara en “extern” el prototipo de la nueva función que agregaremos en main.c.
Es void y no exinf porque en el caso de un Interrupt Service Routine (ISR) en μITRON, no lleva el parámetro int exinf como las tareas. Esto se debe a que un ISR no se ejecuta como una tarea regular, sino que es llamado directamente por el hardware o el kernel del sistema operativo cuando ocurre la interrupción.
En el archivo main.c, define la función:
Qué hace:
Imprime un mensaje indicando que la interrupción ocurrió.
Usa set_flg para activar el FLAG_1 (0x01) en el grupo de flags ID_TEST_FLG2.
Registrar el ISR en la tabla de interrupciones
En el archivo kernel_cfg.c, registra la interrupción en la tabla INT_TBL. Esto vincula el temporizador con el ISR (Interrupt Service Routine).
En el archivo kernel_id.h, define el identificador TEST_TIMER_IRQn para el temporizador:
TEST_TIMER_IRQn es el número de interrupción asignado a nuestro temporizador de prueba.
Configurar el grupo de event flags
En el archivo kernel_cfg.c, asegura que el grupo de flags ID_TEST_FLG2 esté configurado. Si ya existe, no es necesario agregarlo.
En kernel_id.h, define el identificador si aún no lo tienes:
Configurar el temporizador
En la función init_task, inicializamos y configuramos el temporizador para que genere interrupciones cada 5 segundos. Agrega este código al final de init_task en main.c:
Crear la tarea que espera el flag
Agrega una nueva tarea en main.c que espera el flag 0x01 activado por el ISR:
* No olvidar agregarla en la sección “extern” de kernel_cfg.cs declarando el prototipo.
Registrar la tarea
En kernel_cfg.c, agrega la tarea en la tabla de tareas:
En kernel_id.h, define el identificador de la nueva tarea:
Flujo del Programa
Temporizador inicia → Cada 5 segundos genera una interrupción.
Interrupción ocurre → El hardware notifica al kernel.
Interrupt handler (test_timer_isr) ejecuta →
Imprime un mensaje.
Activa el flag 0x01 en el grupo ID_TEST_FLG2.
Tarea (task_wait_interrupt) desbloquea →
Detecta el flag activado.
Procesa el evento y realiza la acción asociada.
Reinicio del ciclo → El temporizador espera otros 5 segundos para repetir el proceso.
INHNO, Interrupt Handler Number
ISR, Interrupt Service Routine