
Cualquier sistema operativo tiene un mecanismo de inicio específico. El principio de funcionamiento de este mecanismo para cada sistema es diferente. Por lo general, dicen que el sistema arranca (Eng. Boot), esta es una abreviatura de "bootstrap", que se refiere a la expresión "tirarse de una cerca por los bootstraps" cómo el sistema se mueve independientemente de un estado en el que la memoria está llena de vacío (
aprox. traductor: si es absolutamente preciso, luego basura ) a una ejecución estable de programas. Tradicionalmente, una pequeña parte del programa se carga en la memoria; se puede almacenar en la ROM. En el pasado, se podía ingresar usando los interruptores en la parte frontal de la computadora. Este gestor de arranque lee un programa de arranque más complejo que ya cargó el sistema operativo. Hoy, la computadora de escritorio arranca de la siguiente manera: el código del BIOS busca dispositivos (discos duros, CD-ROM, memorias USB) desde los cuales arrancar, y luego arranca el sistema operativo.
El sistema operativo para sistemas integrados también se puede inicializar de manera similar. Y, de hecho, se cargan los sistemas operativos integrados desarrollados sobre la base de los sistemas operativos de escritorio. Pero en la mayoría de los RTOS "clásicos", se usa un método mucho más simple (y por lo tanto más rápido).
El sistema operativo es parte del software. Si este software ya está en la memoria (por ejemplo, en una forma u otra de ROM), solo debe asegurarse de que la secuencia de comandos de la CPU después del reinicio finalice con la ejecución del código de inicialización del sistema operativo. Así es como funciona la mayoría de RTOS, incluido Nucleus SE (
nota del traductor: esto también se aplica a nuestro RTOS MAX ).
La mayoría de las herramientas de desarrollo de software integradas incluyen el código de inicio necesario para manejar el reinicio de la CPU y el control de transferencia a la función Punto de entrada en la función
main () . El código redistribuible de Nucleus SE no se ocupa de este proceso, ya que debe ser lo más portátil posible. En cambio, contiene la función
main () , que toma el control de la CPU e inicializa e inicia el sistema operativo. Esta característica se discutirá en detalle a continuación.
Artículos anteriores de la serie: Inicialización de la memoria
Las declaraciones de todas las variables estáticas en el código de Nucleus SE comienzan con el prefijo
ROM o
RAM para indicar dónde deben ubicarse. Estas dos directivas
#define están definidas en el archivo
nuse_types.h y deben configurarse teniendo en cuenta los detalles del conjunto de herramientas de desarrollo utilizado (compilador y enlazador). Por lo general, la
ROM debe ser de tipo
const (
nota del traductor: desde mi experiencia, const no siempre es suficiente, la estática es mejor ) y la
RAM es un valor vacío.
Todas
las variables
ROM se inicializan estáticamente, lo cual es lógico.
Las variables de
RAM no se inicializan estáticamente (ya que esto solo funciona con ciertas cajas de herramientas que están configuradas para copiar automáticamente de ROM a RAM); Se incluye un código de inicialización explícito en la aplicación y se describirá en detalle a continuación.
Nucleus SE no almacena datos "constantes" en la RAM, que generalmente es escasa en sistemas pequeños. En lugar de utilizar estructuras de datos complejas para describir objetos del núcleo, se utilizan conjuntos de tablas (matrices), que se colocan fácilmente en ROM o RAM, según la necesidad.
Función principal ()
El siguiente es el código completo para la función
main () de Nucleus SE:
void main(void) { NUSE_Init(); /* initialize kernel data */ /* user initialization code here */ NUSE_Scheduler(); /* start tasks */ }
La secuencia de operaciones es bastante simple:
- Primero, se llama a la función NUSE_Init () . Inicializa todas las estructuras de datos de Nucleus SE y se describirá en detalle a continuación.
- Luego, el usuario puede insertar cualquier código de inicialización de la aplicación que se ejecutará antes de que comience el programador de tareas. Para obtener más información sobre lo que se puede lograr con este código, consulte más adelante en este artículo.
- Finalmente, se inicia el planificador de Nucleus SE ( NUSE_Scheduler () ). Esto también se discutirá en detalle más adelante en este artículo.
Función NUSE_Init ()
Esta función inicializa todas las variables de núcleo y estructuras de datos de Nucleus SE.
A continuación se muestra el código de función completo: void NUSE_Init(void) { U8 index; /* global data */ NUSE_Task_Active = 0; NUSE_Task_State = NUSE_STARTUP_CONTEXT; #if NUSE_SYSTEM_TIME_SUPPORT NUSE_Tick_Clock = 0; #endif #if NUSE_SCHEDULER_TYPE == NUSE_TIME_SLICE_SCHEDULER NUSE_Time_Slice_Ticks = NUSE_TIME_SLICE_TICKS; #endif /* tasks */ #if ((NUSE_SCHEDULER_TYPE != NUSE_RUN_TO_COMPLETION_SCHEDULER) || NUSE_SIGNAL_SUPPORT || NUSE_TASK_SLEEP || NUSE_SUSPEND_ENABLE || NUSE_SCHEDULE_COUNT_SUPPORT) for (index=0; index<NUSE_TASK_NUMBER; index++) { NUSE_Init_Task(index); } #endif /* partition pools */ #if NUSE_PARTITION_POOL_NUMBER != 0 for (index=0; index<NUSE_PARTITION_POOL_NUMBER; index++) { NUSE_Init_Partition_Pool(index); } #endif /* mailboxes */ #if NUSE_MAILBOX_NUMBER != 0 for (index=0; index<NUSE_MAILBOX_NUMBER; index++) { NUSE_Init_Mailbox(index); } #endif /* queues */ #if NUSE_QUEUE_NUMBER != 0 for (index=0; index<NUSE_QUEUE_NUMBER; index++) { NUSE_Init_Queue(index); } #endif /* pipes */ #if NUSE_PIPE_NUMBER != 0 for (index=0; index<NUSE_PIPE_NUMBER; index++) { NUSE_Init_Pipe(index); } #endif /* semaphores */ #if NUSE_SEMAPHORE_NUMBER != 0 for (index=0; index<NUSE_SEMAPHORE_NUMBER; index++) { NUSE_Init_Semaphore(index); } #endif /* event groups */ #if NUSE_EVENT_GROUP_NUMBER != 0 for (index=0; index<NUSE_EVENT_GROUP_NUMBER; index++) { NUSE_Init_Event_Group(index); } #endif /* timers */ #if NUSE_TIMER_NUMBER != 0 for (index=0; index<NUSE_TIMER_NUMBER; index++) { NUSE_Init_Timer(index); } #endif }
Primero, se inicializan las variables globales:
- NUSE_Task_Active : índice de la tarea activa, inicializado a cero; luego esto puede cambiar el programador.
- NUSE_Task_State : inicializado con el valor NUSE_STARTUP_CONTEXT , que limita la funcionalidad de la API para cualquier código de inicialización de aplicación posterior.
- Si el soporte de hora del sistema está activado, NUSE_Tick_Clock se establece en cero.
- Si el planificador Time Slice está activado, a NUSE_Time_Slice_Ticks se le asigna el valor configurado NUSE_TIME_SLICE_TICKS .
Luego se llaman las funciones para inicializar los objetos del núcleo:
- NUSE_Init_Task () se llama para inicializar las estructuras de datos de cada tarea. Esta llamada se omite solo si se usa el planificador Run to Completion, y las señales, la pausa de la tarea y el contador de programación no están configurados (dado que esta combinación de funciones dará como resultado la ausencia de estas estructuras de tareas en la RAM, por lo tanto, no se realizará la inicialización).
- Se llama a NUSE_Init_Partition_Pool () para inicializar cada objeto de grupo de particiones. Estas llamadas se omiten si no se configuran grupos de particiones.
- Se llama a NUSE_Init_Mailbox () para inicializar cada objeto de buzón. Estas llamadas se omiten si no hay buzones configurados.
- NUSE_Init_Queue () se llama para inicializar cada objeto de cola. Estas llamadas se omiten si no hay colas configuradas.
- Se llama a NUSE_Init_Pipe () para inicializar cada objeto de canal. Estas llamadas se omiten si no hay canales configurados.
- Se llama a NUSE_Init_Semaphore () para inicializar cada objeto de semáforo. Estas llamadas se omiten si no hay semáforos configurados.
- Se llama a NUSE_Init_Event_Group () para inicializar cada objeto de grupo de eventos. Estas llamadas se omiten si no hay grupos de eventos configurados.
- Se llama a NUSE_Init_Timer () para inicializar cada objeto del temporizador. Estas llamadas se omiten si no hay temporizadores configurados.
Inicialización de tareas
El siguiente es el código completo para la función NUSE_Init_Task (): void NUSE_Init_Task(NUSE_TASK task) { #if NUSE_SCHEDULER_TYPE != NUSE_RUN_TO_COMPLETION_SCHEDULER NUSE_Task_Context[task][15] = /* SR */ NUSE_STATUS_REGISTER; NUSE_Task_Context[task][16] = /* PC */ NUSE_Task_Start_Address[task]; NUSE_Task_Context[task][17] = /* SP */ (U32 *)NUSE_Task_Stack_Base[task] + NUSE_Task_Stack_Size[task]; #endif #if NUSE_SIGNAL_SUPPORT || NUSE_INCLUDE_EVERYTHING NUSE_Task_Signal_Flags[task] = 0; #endif #if NUSE_TASK_SLEEP || NUSE_INCLUDE_EVERYTHING NUSE_Task_Timeout_Counter[task] = 0; #endif #if NUSE_SUSPEND_ENABLE || NUSE_INCLUDE_EVERYTHING #if NUSE_INITIAL_TASK_STATE_SUPPORT || NUSE_INCLUDE_EVERYTHING NUSE_Task_Status[task] = NUSE_Task_Initial_State[task]; #else NUSE_Task_Status[task] = NUSE_READY; #endif #endif #if NUSE_SCHEDULE_COUNT_SUPPORT || NUSE_INCLUDE_EVERYTHING NUSE_Task_Schedule_Count[task] = 0; #endif }
Si el planificador Ejecutar para completar no se ha configurado,
se inicializa el bloque de contexto para la
tarea NUSE_Task_Context [tarea] [] . La mayoría de los elementos no son valores asignados, ya que representan registros de máquina comunes, que deben tener un valor intermedio al iniciar una tarea. En el ejemplo (Freescale ColdFire) de la implementación de Nucleus SE (pero para otros procesadores el mecanismo será similar), las últimas tres entradas se establecen explícitamente:
- NUSE_Task_Context [tarea] [15] contiene el registro de estado ( SR , registro de estado) y tiene el valor de la directiva #define NUSE_STATUS_REGISTER .
- NUSE_Task_Context [tarea] [16] contiene el contador de programa ( PC , contador de programa) y tiene el valor de dirección del punto de entrada del código de tarea: NUSE_Task_Start_Address [tarea] .
- NUSE_Task_Context [tarea] [17] contiene el puntero de pila ( SP , puntero de pila) y se inicializa con el valor calculado como la suma de la dirección de la pila de tareas ( NUSE_Task_Stack_Base [tarea] ) y el tamaño de la pila de tareas ( NUSE_Task_Stack_Size [tarea] ).
Si el soporte de señal está activado, los indicadores de señal de tarea (
NUSE_Task_Signal_Flags [tarea] ) se establecen en cero.
Si se activa la suspensión de la tarea (es decir, la llamada de servicio API
NUSE_Task_Sleep () ), el contador de tiempo de espera de la tarea (
NUSE_Task_Timeout_Counter [tarea] ) se establece en cero.
Si el estado de suspensión de la tarea está activado, el estado de la tarea (
NUSE_Task_Status [tarea] ) se inicializa. El usuario establece este valor inicial (en
NUSE_Task_Initial_State [tarea] ) si el soporte para el estado inicial de la tarea está activado. De lo contrario, el estado se asigna a
NUSE_READY .
Si el contador de planificación está activado, el contador de tareas (
NUSE_Task_Schedule_Count [tarea] ) se establece en cero.
Inicializar agrupaciones de particiones
El siguiente es el código completo para la función
NUSE_Init_Partition_Pool () :
void NUSE_Init_Partition_Pool(NUSE_PARTITION_POOL pool) { NUSE_Partition_Pool_Partition_Used[pool] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Partition_Pool_Blocking_Count[pool] = 0; #endif }
El contador del grupo de particiones "usado" (
NUSE_Partition_Pool__Partition_Used [pool] ) se establece en cero.
Si el bloqueo de tareas está activado, el contador de tareas bloqueadas de los grupos de particiones (
NUSE_Partition_Pool_Blocking_Count [grupo]) se establece en cero.
Inicializando buzones
A continuación se muestra el
código completo
NUSE_Init_Mailbox () :
void NUSE_Init_Mailbox(NUSE_MAILBOX mailbox) { NUSE_Mailbox_Data[mailbox] = 0; NUSE_Mailbox_Status[mailbox] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Mailbox_Blocking_Count[mailbox] = 0; #endif }
El almacén de datos del buzón (
NUSE_Mailbox_Data [buzón] ) se establece en cero, y el estado (
NUSE_Mailbox_Status [buzón] ) se vuelve "no utilizado" (es decir, cero).
Si el bloqueo de tareas está activado, el contador de tareas de buzón bloqueado (
NUSE_Mailbox_Blocking_Count [buzón] ) se establece en cero.
Inicialización de cola
El siguiente es el código completo para la función
NUSE_Init_Queue () :
void NUSE_Init_Queue(NUSE_QUEUE queue) { NUSE_Queue_Head[queue] = 0; NUSE_Queue_Tail[queue] = 0; NUSE_Queue_Items[queue] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Queue_Blocking_Count[queue] = 0; #endif }
Los punteros al principio y al final de la cola (de hecho, estos son los índices
NUSE_Queue_Head [queue ] y
NUSE_Queue_Tail [queue] ) son valores asignados que indican el comienzo del área de datos de las colas (es decir, toman un valor cero). El contador en la cola (
NUSE_Queue_Items [queue] ) también se establece en cero.
Si el bloqueo de tareas está activado, el contador de tareas de cola bloqueadas (
NUSE_Queue_Blocking_Count [cola] ) se establece en cero.
Inicialización de canal
El siguiente es el código completo para la función
NUSE_Init_Pipe () :
void NUSE_Init_Pipe(NUSE_PIPE pipe) { NUSE_Pipe_Head[pipe] = 0; NUSE_Pipe_Tail[pipe] = 0; NUSE_Pipe_Items[pipe] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Pipe_Blocking_Count[pipe] = 0; #endif }
Los punteros al principio y al final del canal (de hecho, estos son los índices:
NUSE_Pipe_Head [pipe] y
NUSE_Pipe_Tail [pipe] ) tienen asignado un valor que indica el comienzo del área de datos del canal (es decir, toman un valor nulo). El
contador de canal (
NUSE_Pipe_Items [pipe] ) también se establece en cero.
Si el bloqueo de tareas está activado, el contador de tareas bloqueadas del canal (
NUSE_Pipe_Blocking_Count [pipe] ) se establece en cero.
Inicialización de semáforo
El siguiente es el código completo para la función
NUSE_Init_Semaphore () :
void NUSE_Init_Semaphore(NUSE_SEMAPHORE semaphore) { NUSE_Semaphore_Counter[semaphore] = NUSE_Semaphore_Initial_Value[semaphore]; #if NUSE_BLOCKING_ENABLE NUSE_Semaphore_Blocking_Count[semaphore] = 0; #endif }
El contador de semáforos (
NUSE_Semaphore_Counter [semaphore] ) se inicializa con el valor establecido por el usuario (
NUSE_Semaphore_Initial_Value [semaphore] ).
Si el bloqueo de tareas está activado, el contador de tareas de semáforo bloqueado (
NUSE_Semaphore_Blocking_Count [semaphore] ) se establece en cero.
Inicializando grupos de eventos
El siguiente es el código completo para la función
NUSE_Init_Event_Group () :
void NUSE_Init_Event_Group(NUSE_EVENT_GROUP group) { NUSE_Event_Group_Data[group] = 0; #if NUSE_BLOCKING_ENABLE NUSE_Event_Group_Blocking_Count[group] = 0; #endif }
Los indicadores del grupo de eventos se restablecen, es decir
NUSE_Event_Group_Data [grupo] tiene asignado un valor nulo.
Si el bloqueo de tareas está activado, el contador de tareas bloqueadas del grupo de indicadores de eventos (
NUSE_Event_Group_Blocking_Count [grupo] ) se establece en cero.
Inicialización del temporizador
A continuación se muestra el código completo de
NUSE_Init_Timer () ;
void NUSE_Init_Timer(NUSE_TIMER timer) { NUSE_Timer_Status[timer] = FALSE; NUSE_Timer_Value[timer] = NUSE_Timer_Initial_Time[timer]; NUSE_Timer_Expirations_Counter[timer] = 0; }
El estado del temporizador (
NUSE_Timer_Status [timer] ) se establece en "no utilizado", es decir
FALSOEl valor de la cuenta regresiva (
NUSE_Timer_Value [timer ]) se inicializa mediante el valor establecido por el usuario (
NUSE_Timer_Initial_Time [timer] ).
El contador de finalización (
NUSE_Timer_Expirations_Counter [timer] ) se establece en cero.
Inicializando el código de aplicación
Una vez que se han inicializado las estructuras de datos de Nucleus SE, es posible ejecutar el código responsable de inicializar la aplicación antes de comenzar la tarea. Esta característica puede ser útil para las siguientes tareas:
- Inicialización de estructuras de datos de aplicación. El llenado explícito de estructuras de datos es más fácil de entender y depurar en comparación con la inicialización automática de variables estáticas.
- Asignación de objetos del núcleo. Dado que todos los objetos del núcleo se crean estáticamente en la etapa de construcción y se identifican utilizando valores de índice, puede ser útil asignar un "propietario" o determinar el uso de estos objetos. Esto se puede hacer usando la directiva #define, sin embargo, si hay varias instancias de tareas, es mejor asignar índices de objeto a través de matrices globales (indexadas por ID de tarea).
- Inicialización del dispositivo. Esto puede ser útil para la instalación inicial de periféricos.
Obviamente, muchos de estos objetivos se pueden lograr antes de la inicialización de Nucleus SE, pero la ventaja en la ubicación del código de la aplicación aquí es que ahora puede usar los servicios del núcleo (llamadas API). Por ejemplo, una cola o un buzón de correo puede rellenarse previamente con datos que deberán procesarse cuando comience la tarea.
Las llamadas a la API tienen una restricción: todas las acciones que generalmente conducen a la activación del programador están prohibidas (por ejemplo, pausar / bloquear tareas). La variable global
NUSE_Task_State se ha establecido en
NUSE_STARTUP_CONTEXT para
indicar esta limitación.
Planificador de lanzamiento
Una vez completada la inicialización, solo queda ejecutar el programador para comenzar a ejecutar el código de la aplicación: tareas. La configuración del programador y el trabajo de varios tipos de programadores se describieron en detalle en uno de los artículos anteriores (
# 9 ), por lo que aquí solo se requiere un breve resumen.
La secuencia de pasos clave es la siguiente:
- Establecer la variable global NUSE_Task_State en NUSE_TASK_CONTEXT .
- Seleccione el índice de la primera tarea a ejecutar. Si se activa el soporte para la tarea inicial, se realiza la búsqueda de la primera tarea finalizada; de lo contrario, se utiliza un valor cero.
- El planificador se llama - NUSE_Scheduler () .
Lo que sucede exactamente en el último paso depende del planificador seleccionado. Cuando se usa el programador Ejecutar para completar, se inicia un ciclo de planificación y las tareas se llaman secuencialmente. Cuando se usan otros planificadores, el contexto de la primera tarea se carga y el control se transfiere a la tarea.
El siguiente artículo discutirá el diagnóstico y la verificación de errores.
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.