
En un
artículo anterior, analizamos los diversos tipos de planificación compatibles con RTOS y las capacidades relacionadas en Nucleus SE. En este artículo, veremos opciones de planificación adicionales en Nucleus SE y el proceso de guardar y restaurar el contexto.
Artículos anteriores de la serie:
Artículo # 9. Programador: implementaciónArtículo # 8. Nucleus SE: diseño interno y despliegueArtículo # 7. Núcleo SE: IntroducciónArtículo # 6. Otros servicios RTOSArtículo # 5. Interacción de tareas y sincronizaciónArtículo # 4. Tareas, cambio de contexto e interrupcionesArtículo # 3. Tareas y planificaciónArtículo # 2. RTOS: estructura y modo en tiempo real
Artículo # 1. RTOS: introducción.
Características opcionales
Durante el desarrollo de Nucleus SE, hice que el número máximo de funciones sea opcional, lo que ahorra memoria y / o tiempo.
Suspender tareas
Como se mencionó anteriormente en
el artículo Planificador: Implementación , Nucleus SE admite varias opciones para pausar tareas, pero esta característica es opcional y está incluida en el símbolo
NUSE_SUSPEND_ENABLE en
nuse_config.h . Si se establece en
TRUE , la estructura de datos se define como
NUSE_Task_Status [] . Este tipo de suspensión se aplica a todas las tareas. La matriz es del tipo
U8 , donde se usan 2 nibbles por separado. Los 4 bits inferiores contienen el estado de la tarea:
NUSE_READY, NUSE_PURE_SUSPEND ,
NUSE_SLEEP_SUSPEND ,
NUSE_MAILBOX_SUSPEND , etc. Si una llamada API suspende una tarea (por ejemplo,
NUSE_MAILBOX_SUSPEND ), los 4 bits altos contienen el índice del objeto en el que se suspende la tarea. Esta información se usa cuando el recurso está disponible y para llamar a la API necesita saber qué tareas suspendidas deben reanudarse.
Para realizar la suspensión de la tarea, se utilizan un par de funciones del planificador:
NUSE_Suspend_Task () y
NUSE_Wake_Task () .
El código
NUSE_Suspend_Task () es el siguiente:

La función guarda el nuevo estado de la tarea (los 8 bits), obtenida como el parámetro suspend_code. Cuando habilita el bloqueo (consulte "Bloquear llamadas API" a continuación), se
guarda el código de retorno
NUSE_SUCCESS . A continuación, se llama a
NUSE_Reschedule () para transferir el control a la siguiente tarea.
El código
NUSE_Wake_Task () es bastante simple:

El estado de la tarea se establece en
NUSE_READY . Si no se utiliza el planificador prioritario, la tarea actual continúa ocupando el procesador hasta que llegue el momento de liberar el recurso. Si se utiliza el planificador prioritario, se llama a
NUSE_Reschedule () con el índice de tarea como una indicación de finalización, ya que la tarea puede tener una prioridad más alta y debe ejecutarse de inmediato.
Bloquear llamadas API
Nucleus RTOS admite varias llamadas API con las que un desarrollador puede pausar (bloquear) una tarea si no hay recursos disponibles. La tarea se reanudará cuando los recursos estén disponibles nuevamente. Este mecanismo también se implementa en Nucleus SE y es aplicable a varios objetos del núcleo: una tarea puede bloquearse en una sección de memoria, en un grupo de eventos, buzón, cola, canal o semáforo. Pero, como la mayoría de las herramientas en Nucleus SE, es opcional y se define mediante el símbolo
NUSE_BLOCKING_ENABLE en
nuse_config.h . Si se establece en
TRUE , se define la matriz
NUSE_Task_Blocking_Return [] , que contiene el código de retorno para cada tarea; podría ser
NUSE_SUCCESS o el código
NUSE_MAILBOX_WAS_RESET , que indica que el objeto se restableció cuando se bloqueó la tarea. Cuando el bloqueo está activado, el código correspondiente se incluye en las funciones de la API mediante compilación condicional.
Contador de horario
Nucleus RTOS calcula cuántas veces se ha programado una tarea desde que se creó y se restableció por última vez. Esta característica también se implementa en Nucleus SE, pero es opcional y se define mediante el símbolo
NUSE_SCHEDULE_COUNT_SUPPORT en
nuse_config.h . Si se establece en
TRUE , se
crea una matriz de
NUSE_Task_Schedule_Count [] de tipo
U16 , que almacena el contador de cada tarea en la aplicación.
Estado inicial de la tarea.
Cuando se crea una tarea en Nucleus RTOS, puede seleccionar su estado: listo o en pausa. En Nucleus SE, de forma predeterminada, todas las tareas están listas al inicio. La opción seleccionada con el símbolo
NUSE_INITIAL_TASK_STATE_SUPPORT en
nuse_config.h le permite seleccionar el estado de inicio. La matriz
NUSE_Task_Initial_State [] se define en
nuse_config.c y requiere la inicialización de
NUSE_READY o
NUSE_PURE_SUSPEND para cada tarea en la aplicación.
Guardar contexto
La idea de mantener el contexto de una tarea con cualquier tipo de planificador, excepto RTC (Ejecutar hasta su finalización), se presentó en el artículo 3 "Tareas y programación". Como ya se mencionó, hay varias formas de mantener el contexto. Dado que Nucleus SE no está diseñado para procesadores de 32 bits, elegí usar tablas, no pilas, para mantener el contexto.
Se
utiliza una matriz bidimensional del tipo
ADDR NUSE_Task_Context [] [] para guardar el contexto para todas las tareas en la aplicación. Las filas son
NUSE_TASK_NUMBER (el número de tareas en la aplicación), las columnas son
NUSE_REGISTERS (el número de registros que deben guardarse; depende del procesador y está configurado en
nuse_types.h) .
Por supuesto, mantener el contexto y restaurar el código depende del procesador. Y este es el único código de Nucleus SE vinculado a un dispositivo específico (y entorno de desarrollo). Daré un ejemplo del código de guardar / restaurar para el procesador ColdFire. Aunque esta elección puede parecer extraña debido a un procesador desactualizado, su ensamblador es más fácil de leer que los ensambladores de la mayoría de los procesadores modernos. El código es lo suficientemente simple como para usarlo como base para crear un cambio de contexto para otros procesadores:

Cuando se requiere el cambio de contexto, este código se llama en NUSE_Context_Swap. Se utilizan dos variables:
NUSE_Task_Active , el índice de la tarea actual, cuyo contexto debe preservarse;
NUSE_Task_Next , el índice de la tarea cuyo contexto desea cargar (consulte la sección Datos globales).
El proceso de preservación del contexto funciona de la siguiente manera:
- Los registros A0 y D0 se almacenan temporalmente en la pila;
- A0 está configurado para apuntar a una matriz de bloques de contexto NUSE_Task_Context [] [] ;
- D0 se carga usando NUSE_Task_Active y se multiplica por 72 (ColdFire tiene 18 registros, que requieren 72 bytes para el almacenamiento);
- entonces D0 se agrega a A0 , que ahora apunta a un bloque de contexto para la tarea actual;
- entonces los registros se almacenan en el bloque de contexto; primero A0 y D0 (de la pila), luego D1-D7 y A1-A6 , luego SR y PC (de la pila, veremos el cambio de contexto iniciado rápidamente), y al final se guarda el puntero de la pila.
El proceso de carga de contexto es la misma secuencia de acciones en el orden inverso:
- A0 está configurado para apuntar a una matriz de bloques de contexto NUSE_Task_Context [] [] ;
- D0 se carga usando NUSE_Task_Active , se incrementa y multiplica por 72;
- luego D0 se agrega a A0 , que ahora apunta al bloque de contexto para la nueva tarea (dado que la carga de contexto debe realizarse en el proceso inverso de guardar la secuencia, primero se requiere el puntero de la pila);
- entonces los registros se restauran desde el bloque de contexto; primero, el puntero de la pila, luego PC y SR se insertan en la pila, luego se cargan D1-D7 y A1-A6 , y al final de D0 y A0 .
La dificultad para implementar el cambio de contexto es que el acceso al registro de estado es difícil para muchos procesadores (para ColdFire, esto es
SR ). Una solución común es la interrupción, es decir, la interrupción del programa o la interrupción de ramificación condicional, lo que hace que el
SR se cargue en la pila junto con la
PC . Así es como funciona Nucleus SE en ColdFire. La macro
NUSE_CONTEXT_SWAP () se establece en
nuse_types.h , que se extiende a:
asm ("trampa # 0");El siguiente es el código de inicialización (
NUSE_Init_Task () en
nuse_init.c ) para bloques de contexto:

Así es como se produce la inicialización del puntero de la pila, la
PC y el
SR . Los dos primeros tienen valores establecidos por el usuario en
nuse_config.c . El valor de
SR se define como el carácter
NUSE_STATUS_REGISTER en
nuse_types.h . Para ColdFire, este valor es
0x40002000 .
Datos globales
El planificador de Nucleus SE requiere muy poca memoria para almacenar datos, pero, por supuesto, utiliza estructuras de datos asociadas con tareas, que se analizarán en detalle en los siguientes artículos.
Datos de RAM
El planificador no usa los datos ubicados en la ROM, y de 2 a 5 variables globales se colocan en la RAM (todas se configuran en
nuse_globals.c ), dependiendo del planificador que se use:
- NUSE_Task_Active : una variable de tipo U8 que contiene el índice de la tarea actual;
- NUSE_Task_State : una variable de tipo U8 que contiene un valor que indica el estado del código que se está ejecutando actualmente, que puede ser una tarea, un controlador de interrupciones o un código de inicio; los valores posibles son: NUSE_TASK_CONTEXT , NUSE_STARTUP_CONTEXT , NUSE_NISR_CONTEXT y NUSE_MISR_CONTEXT ;
- NUSE_Task_Saved_State : una variable de tipo U8 utilizada para proteger el valor de NUSE_Task_State en una interrupción administrada;
- NUSE_Task_Next : una variable de tipo U8 que contiene el índice de la siguiente tarea, que debe programarse para todos los planificadores, excepto RTC;
- NUSE_Time_Slice_Ticks : una variable de tipo U16 que contiene un contador de segmentos de tiempo; solo se usa con el planificador de TS.
Huella de datos del planificador
El programador de Nucleus SE no utiliza datos de ROM. La cantidad exacta de datos de RAM varía según el planificador utilizado:
- para RTC - 2 bytes ( NUSE_Task_Active y NUSE_Task_State );
- para RR y prioridad: 4 bytes ( NUSE_Task_Active , NUSE_Task_State , NUSE_Task_Saved_State y NUSE_Task_Next );
- para TS: 6 bytes ( NUSE_Task_Active , NUSE_Task_State , NUSE_Task_Saved_State , NUSE_Task_Next y NUSE_Time_Slice_Ticks ).
Implementación de otros mecanismos de planificación.
A pesar de que Nucleus SE ofrece una opción de 4 programadores, que cubren la mayoría de los casos, la arquitectura abierta le permite implementar oportunidades para otros casos.
Tiempo dividido con tarea en segundo plano
Como ya se describió en el
artículo n. ° 3, "Tareas y programación", el simple Programador de tiempo de intervalo de tiempo tiene limitaciones porque limita el tiempo máximo que un procesador puede realizar una tarea. Una opción más sofisticada sería agregar soporte para la tarea en segundo plano. Dicha tarea podría programarse en cualquier ranura asignada para tareas en pausa y ejecutarse cuando la ranura esté parcialmente liberada. Este enfoque le permite programar tareas a intervalos regulares y con un porcentaje predicho del tiempo central del procesador para completar.
Prioridad y Round Robin (RR)
En la mayoría de los núcleos en tiempo real, el planificador prioritario admite varias tareas en cada nivel de prioridad, a diferencia de Nucleus SE, donde cada tarea tiene un nivel único. Le di preferencia a la última opción, ya que simplifica enormemente las estructuras de datos y, por lo tanto, el código del planificador. Para soportar arquitecturas más complejas, se necesitarían numerosas tablas ROM y RAM.
Sobre el autor: Colin Walls ha trabajado en la industria electrónica durante más de treinta años, dedicando la mayor parte de su tiempo al firmware. Ahora es ingeniero de firmware en Mentor Embedded (una división de Mentor Graphics). Colin Walls a menudo habla en conferencias y seminarios, autor de numerosos artículos técnicos y dos libros sobre firmware. Vive en el Reino Unido.
Blog profesional
de Colin , correo electrónico: colin_walls@mentor.com.
Acerca de la traducción: esta serie de artículos parecía interesante porque, a pesar de los enfoques desactualizados descritos en algunos lugares, el autor, en un lenguaje muy comprensible, presenta al lector poco capacitado las características del sistema operativo en tiempo real. Yo mismo pertenezco al equipo de creadores del
RTOS ruso , que
pretendemos liberar , y espero que el ciclo sea útil para los desarrolladores novatos.