Toda la verdad sobre RTOS. Artículo # 33. Uso del sistema operativo Nucleus SE en tiempo real

Hasta ahora en esta serie de artículos, hemos estado analizando qué características ofrece Nucleus SE. Ahora es el momento de ver cómo se puede usar en una aplicación de firmware real.



Artículos anteriores de la serie:
Artículo # 32. Nucleus SE Migration: características no realizadas y compatibilidad
Artículo # 31. Diagnóstico y comprobación de errores RTOS
Artículo # 30. Inicialización de Nucleus SE y procedimientos de inicio
Artículo # 29. Interrupciones en el núcleo SE
Artículo # 28. Temporizadores de software
Artículo # 27. Hora del sistema
Artículo # 26. Canales: servicios auxiliares y estructuras de datos.
Artículo # 25. Canales de datos: introducción y servicios básicos
Artículo # 24. Colas: servicios auxiliares y estructuras de datos.
Artículo 23. Colas: introducción y servicios básicos.
Artículo # 22. Buzones: servicios auxiliares y estructuras de datos
Artículo # 21. Buzones: Introducción y servicios básicos
Artículo # 20. Semáforos: servicios auxiliares y estructuras de datos
Artículo # 19. Semáforos: introducción y servicios básicos.
Artículo # 18. Grupos de banderas de eventos: servicios auxiliares y estructuras de datos
Artículo # 17. Grupos de banderas de eventos: Introducción y servicios básicos
Artículo # 16. Señales
Artículo # 15. Particiones de memoria: servicios y estructuras de datos
Artículo # 14. Secciones de memoria: introducción y servicios básicos.
Artículo 13. Estructuras de datos de tareas y llamadas de API no compatibles
Artículo # 12. Servicios para trabajar con tareas.
Artículo # 11. Tareas: configuración e introducción a la API
Artículo # 10. Programador: funciones avanzadas y preservación del contexto
Artículo # 9. Programador: implementación
Artículo # 8. Nucleus SE: diseño interno y despliegue
Artículo # 7. Núcleo SE: Introducción
Artículo # 6. Otros servicios RTOS
Artículo # 5. Interacción de tareas y sincronización
Artículo # 4. Tareas, cambio de contexto e interrupciones
Artículo # 3. Tareas y planificación
Artículo # 2. RTOS: estructura y modo en tiempo real
Artículo # 1. RTOS: introducción.

¿Qué es el núcleo SE?


Sabemos que Nucleus SE es el núcleo de un sistema operativo en tiempo real, pero debe comprender cómo encaja en el resto de la aplicación. Y simplemente encaja, porque a diferencia de un sistema operativo de escritorio (por ejemplo, Windows), la aplicación no se inicia en Nucleus SE; el kernel es simplemente parte de un programa que se ejecuta en un dispositivo integrado. Este es el caso de uso más común para RTOS.

Desde un punto de vista de alto nivel, una aplicación integrada es algún tipo de código que se inicia cuando se inicia la CPU. En este caso, se inicializa el entorno de hardware y software, y luego se llama a la función main () , que inicia el código de la aplicación principal.

Cuando se usa Nucleus SE (y muchos otros núcleos similares), la diferencia es que la función main () es parte del código del kernel. Esta función simplemente inicializa las estructuras de datos del núcleo y luego llama al planificador, lo que conduce al lanzamiento del código de la aplicación (tareas). El usuario puede agregar cualquier código de inicialización nativo a la función main () .

Nucleus SE también incluye un conjunto de funciones: una interfaz de programación de aplicaciones (API) que proporciona un conjunto de funciones como comunicación y sincronización de tareas, trabajar con temporizadores, asignación de memoria, etc. Todas las funciones API se describieron anteriormente en los artículos de esta serie.

Todo el software Nucleus SE se proporciona como código fuente (principalmente en C). Para configurar el código de acuerdo con los requisitos de una aplicación en particular, se utiliza la compilación condicional. Esto se describe en detalle en este artículo en la sección Configuración.

Después de compilar el código, los módulos de objeto Nucleus SE resultantes se asocian con los módulos de código de la aplicación, lo que da como resultado una sola imagen binaria, que generalmente se coloca en la memoria flash del dispositivo incorporado. El resultado de este enlace estático es que toda la información simbólica permanece disponible tanto del código de la aplicación como del código del núcleo. Esto es útil para la depuración; sin embargo, se requiere precaución para evitar el mal uso de los datos de Nucleus SE.

CPU y soporte de herramientas


Dado que Nucleus SE viene como código fuente, debe ser portátil. Sin embargo, el código que se ejecuta a un nivel tan bajo (cuando se usan planificadores donde se requiere el cambio de contexto, es decir, cualquier cosa que no sea Ejecutar para completar), no puede ser completamente independiente del lenguaje ensamblador. Minimicé esta dependencia, y para portar a una nueva CPU, la programación de bajo nivel casi no es necesaria. El uso de un nuevo conjunto de herramientas de desarrollo (compilador, ensamblador, enlazador, etc.) también puede provocar problemas de portabilidad.

Configurar la aplicación Nucleus SE


La clave para el uso eficiente de Nucleus SE es la configuración adecuada. Puede parecer complicado, pero de hecho, todo es bastante lógico y solo requiere un enfoque sistemático. Casi toda la configuración se realiza editando dos archivos: nuse_config.h y nuse_config.c .

Configuración de Nuse_config.h


Este archivo es solo un conjunto de caracteres de la directiva #define , a los que se les asignan los valores apropiados para obtener la configuración de kernel necesaria. En el archivo nuse_config.h, de forma predeterminada, todos los caracteres están presentes, pero se les asigna la configuración mínima.

Contadores de objetos
El número de objetos del núcleo de cada tipo se establece mediante valores de símbolo del formulario NUSE_SEMAPHORE_NUMBER . Para la mayoría de los objetos, este valor puede variar de 0 a 15. Las tareas son una excepción, debe haber al menos una. Las señales, de hecho, no son objetos independientes, ya que están asociadas con tareas y se activan estableciendo NUSE_SIGNAL_SUPPOR T en TRUE .

Activadores de funciones API
Cada función de la API de Nucleus SE se puede activar por separado asignando un símbolo cuyo nombre coincida con el nombre de la función (por ejemplo, NUSE_PIPE_JAM ) a TRUE . Esto lleva a la inclusión del código de función en la aplicación.

Selección de programador y configuraciones
Nucleus SE admite cuatro tipos de programadores, como se describe en un artículo anterior. El planificador utilizado se establece asignando NUSE_SCHEDULER_TYPE a uno de los siguientes valores: NUSE_RUN_TO_COMPLETION_SCHEDULER , NUSE_TIME_SLICE_SCHEDULER , NUSE_ROUND_ROBIN_SCHEDULER o NUSE_PRIORITY_SCHEDULER .

Puede configurar otros parámetros del planificador:
NUSE_TIME_SLICE_TICKS indica el número de ticks por ranura para el planificador Time Slice. Si se utiliza otro planificador, este parámetro debe establecerse en 0.
NUSE_SCHEDULE_COUNT_SUPPORT se puede establecer en TRUE o FALSE para habilitar / deshabilitar el mecanismo del contador del planificador.
NUSE_SUSPEND_ENABLE habilita el bloqueo de tareas (suspensión) para muchas funciones API. Esto significa que una llamada a dicha función puede llevar a la suspensión de la tarea de llamada hasta que se libere el recurso. Para seleccionar esta opción, NUSE_SUSPEND_ENABLE también debe establecerse en TRUE .

Otras opciones
A otros parámetros también se les pueden asignar valores VERDADERO o FALSO para activar / desactivar otras funciones del núcleo:
NUSE_API_PARAMETER_CHECKING agrega un código de verificación de parámetro de llamada de función API. Comúnmente utilizado para la depuración.
NUSE_INITIAL_TASK_STATE_SUPPORT establece el estado inicial de todas las tareas como NUSE_READY o NUSE_PURE_SUSPEND . Si este parámetro está deshabilitado, todas las tareas tendrán el estado inicial NUSE_READY .
NUSE_SYSTEM_TIME_SUPPORT : soporte para la hora del sistema.
NUSE_INCLUDE_EVERYTHING : un parámetro que agrega el número máximo de funciones a la configuración de Nucleus SE. Conduce a la activación de todas las funciones opcionales y cada función API de los objetos configurados. Se utiliza para crear rápidamente una configuración de Nucleus SE para verificar un nuevo puerto del código del núcleo.

Configurando nuse_config.c


Después de especificar la configuración del núcleo en nuse_config.h, es necesario inicializar las diversas estructuras de datos almacenadas en la ROM. Esto se hace en el archivo nuse_config.c . La definición de las estructuras de datos se controla mediante la compilación condicional, por lo que todas las estructuras están contenidas en una copia del archivo predeterminado nuse_config.c .

Datos de la tarea
La matriz NUSE_Task_Start_Address [] debe inicializarse con el valor de las direcciones iniciales de cada tarea. Esto suele ser solo una lista de nombres de funciones, sin paréntesis. Los prototipos de las funciones de entrada de tareas también deben ser visibles. En el archivo predeterminado, la tarea se configura con el nombre NUSE_Idle_Task () , esto se puede cambiar a la tarea de la aplicación.

Si usa cualquier programador, excepto Ejecutar hasta completar, cada tarea requiere su propia pila. Para cada pila de tareas, debe crear una matriz en RAM. Estas matrices deben ser del tipo ADDR , y la dirección de cada una de ellas debe almacenarse en NUSE_Task_Stack_Base [] . Es difícil predecir el tamaño de la matriz, por lo que es mejor usar medidas (consulte la sección "Depuración" más adelante en este artículo). El tamaño de cada matriz (es decir, el número de palabras en la pila) debe almacenarse en NUSE_Task_Stack_Size [] .

Si se ha habilitado una función para indicar el estado inicial de la tarea (utilizando el parámetro NUSE_INITIAL_TASK_STATE_SUPPORT ), la matriz NUSE_Task_Initial_State [] debe inicializarse con el estado NUSE_READY o NUSE_PURE_SUSPEND .

Partition Pool Data
Si se configura al menos un grupo de particiones, se debe crear una matriz (de tipo U8 ) para cada uno de ellos en la ROM. El tamaño de estas matrices se calcula de la siguiente manera: (número de particiones * (tamaño de partición + 1)). Las direcciones de estas secciones (es decir, sus nombres) deben asignarse a los elementos NUSE_Partition_Pool_Data_Address [] correspondientes. Para cada grupo, el número de particiones y su tamaño deben colocarse en NUSE_Partition_Pool_Partition_Number [] y NUSE_Partition_Message_Size [] , respectivamente.

Datos de cola
Si se configura al menos una cola, se debe crear una matriz (del tipo ADDR ) para cada uno de ellos en la RAM. El tamaño de estas matrices es el número de elementos en cada cola. Las direcciones de estas matrices (es decir, sus nombres) deben asignarse a los elementos NUSE_Queue_Data [] correspondientes. El tamaño de cada cola debe asignarse al elemento NUSE_Queue_Size [] correspondiente.

Datos de enlace de datos
Si se configura al menos un canal de datos, se debe crear una matriz (de tipo U8 ) en RAM para él (o para cada uno de ellos). El tamaño de estas matrices se calcula de la siguiente manera: (tamaño del canal * tamaño del mensaje en el canal). Las direcciones de estas matrices (es decir, sus nombres) deben asignarse a los elementos NUSE_Pipe_Data [] correspondientes. Para cada canal, su tamaño y tamaño de mensaje deben asignarse a los elementos NUSE_Pipe_Size [] y NUSE_Pipe_Message_Size [] correspondientes, respectivamente.

Datos del semáforo
Si se configura al menos un semáforo, la matriz NUSE_Semaphore_Initial_Value [] debe inicializarse con los valores iniciales de la cuenta regresiva.

Datos del temporizador de aplicación
Si se configura al menos un temporizador, la matriz NUSE_Timer_Initial_Time [] debe inicializarse con los valores iniciales de los contadores. Además, NUSE_Timer_Reschedule_Time [] debe tener asignados valores de reinicio. Estos valores de temporizador se utilizarán después de que finalice el primer ciclo de temporizador. Si los valores de reinicio se establecen en 0, el contador se detendrá después de un ciclo.

Si se configura la compatibilidad con los mecanismos de finalización de cuenta (estableciendo el parámetro NUSE_TIMER_EXPIRATION_ROUTINE_SUPPORT en TRUE ), se deben crear dos matrices más. Las direcciones de los mecanismos de finalización (solo una lista de nombres de funciones, sin paréntesis) deben colocarse en NUSE_Timer_Expiration_Routine_Address [] . La matriz NUSE_Timer_Expiration_Routine_Parameter [] debe inicializarse con los valores de los parámetros de finalización.

Cual API?


Todos los sistemas operativos de una forma u otra tienen una API (interfaz de programación de aplicaciones). Nucleus SE no es una excepción, y las funciones que componen la API se han descrito en detalle en esta serie de artículos.

Puede parecer obvio que al escribir una aplicación con Nucleus SE, debe usar la API como se describe en artículos anteriores. Sin embargo, este no es siempre el caso.

Para la mayoría de los usuarios, la API de Nucleus SE será algo nuevo, tal vez incluso su primera experiencia con la API del sistema operativo. Y como es bastante simple, puede servir como una buena introducción al tema. En este caso, el procedimiento es claro.

Para algunos usuarios, una API alternativa puede ser una opción más atractiva. Hay tres situaciones obvias en las que esto es posible.
  1. Nucleus SE es solo una parte de un sistema que utiliza otros sistemas operativos para otros componentes. Por lo tanto, la portabilidad del código y, lo que es más importante, la experiencia de usar varios sistemas operativos parecen muy tentadores.
  2. El usuario tiene una amplia experiencia en el uso de la API de otro sistema operativo. Usar esta experiencia también es muy recomendable.
  3. El usuario desea reutilizar el código escrito para la API de otro sistema operativo. Es posible cambiar las llamadas a la API, pero lleva mucho tiempo.


Dado que el código fuente completo de Nucleus SE está disponible para todos, no hay nada que le impida editar cada función API para que parezca su equivalente desde otro sistema operativo. Sin embargo, llevará mucho tiempo y será muy improductivo. Un enfoque más correcto sería escribir un "contenedor". Hay varias formas de hacer esto, pero la forma más fácil es crear un archivo de encabezado ( #include ) que contenga un conjunto de macros #define que asignen funciones API de terceros a funciones API de Nucleus SE.

Un contenedor que transfiere las funciones de la API Nucleus RTOS (parcialmente) a Nucleus SE se distribuye con Nucleus SE. Puede ser útil para desarrolladores con experiencia en el uso de Nucleus RTOS, o en el futuro si es posible cambiar a este RTOS. Este contenedor también puede servir de ejemplo cuando se desarrollan cosas similares.

Depuración de aplicaciones de Nucleus SE


Escribir una aplicación incrustada usando un núcleo multitarea es una tarea compleja. Asegurarse de que el código funciona y detectar errores puede ser una tarea desalentadora. A pesar de que este es solo un código que se ejecuta en un procesador, la ejecución simultánea de varias tareas hace que sea bastante difícil concentrarse en un hilo específico de ejecución. Esto se complica aún más cuando varias tareas comparten un código común. Lo peor de todo, cuando dos tareas tienen exactamente el mismo código (pero funcionan con datos diferentes). También es complicado desentrañar las estructuras de datos que se utilizan para implementar objetos del núcleo con el fin de ver información significativa.

Para depurar aplicaciones creadas con Nucleus SE, no se requieren bibliotecas adicionales u otros servicios. El depurador puede leer todo el código del núcleo. Por lo tanto, toda la información simbólica está disponible para su estudio. Cuando se trabaja con aplicaciones Nucleus SE, se puede usar cualquier herramienta de depuración moderna.

Usando un depurador


Las herramientas de depuración diseñadas específicamente para sistemas integrados se han vuelto muy poderosas en los 30 años que han existido. La característica principal de una aplicación integrada, en comparación con un programa de escritorio, es que todos los sistemas integrados son diferentes (y todas las computadoras personales son bastante similares entre sí). Un buen depurador integrado debe ser flexible y tener suficientes configuraciones para que coincida con la variedad de sistemas integrados y los requisitos del usuario. La personalización del depurador se expresa en varias formas, pero generalmente existe la posibilidad de crear scripts. Es esta característica la que permite que el depurador funcione bien con una aplicación a nivel de kernel. A continuación analizaré algunos casos de uso del depurador.

Vale la pena señalar que generalmente un depurador es una familia de herramientas, no solo un programa. El depurador puede tener varios modos de operación, a través de los cuales ayuda cuando se desarrolla código en un sistema virtual o en hardware real.

Puntos de interrupción sensibles a tareas


Si el programa tiene un código común a varias tareas, el uso de puntos de interrupción convencionales durante la depuración es complicado. Lo más probable es que necesite que el código se detenga solo cuando se alcance un punto de interrupción en el contexto de una tarea específica que está depurando actualmente. Para hacer esto, necesita un punto de interrupción que tendrá en cuenta la tarea.

Afortunadamente, la capacidad de crear scripts en depuradores modernos y la disponibilidad de datos de caracteres de Nucleus SE hacen que la implementación de puntos de interrupción específicos de tareas sea algo bastante simple. Todo lo que se necesita es escribir un script simple que se asociará con un punto de interrupción que desea enseñar para distinguir entre tareas. Este script tomará el parámetro: índice (ID) de la tarea que le interesa. El script simplemente comparará este valor con el índice de la tarea actual ( NUSE_Task_Active ). Si los valores coinciden, el programa se detiene. Si son diferentes, la ejecución continúa. Vale la pena señalar que la ejecución de este script afectará la ejecución de la aplicación en tiempo real ( nota del traductor: significa que la ejecución del programa se ralentizará en relación con su funcionamiento normal ). Sin embargo, si el script no está en un bucle que se ejecutará con mucha frecuencia, este efecto será mínimo.

Información del objeto del núcleo


La necesidad obvia de depurar la aplicación Nucleus SE es la capacidad de obtener información sobre los objetos del núcleo: cuáles son sus características y cuál es su estado actual. Esto le permite obtener respuestas a preguntas como: "¿Qué tan grande es esta cola y cuántos mensajes hay ahora?"

Esto se puede usar agregando código de depuración adicional a su aplicación, que utilizará las llamadas API "informativas" (como NUSE_Queue_Information ). Por supuesto, esto significará que su aplicación ahora contiene código adicional, que no será necesario después de la implementación de la aplicación. Usar #define para encender y apagar este código usando compilación condicional sería una decisión lógica.

Algunos depuradores pueden realizar una llamada de función específica, es decir, llamar directamente a una función API para recuperar información.Esto elimina la necesidad de código adicional, pero esta función API debe configurarse para que el depurador la use.

Un enfoque alternativo, más flexible, pero menos "no antiguo" es el acceso directo a las estructuras de datos de los objetos del núcleo. Lo más probable es que sea mejor hacer esto usando scripts de depurador. En nuestro ejemplo, el tamaño de la cola se puede obtener de NUSE_Queue_Size [] , y su uso actual de NUSE_Queue_Data [] . Además, los mensajes en la cola se pueden mostrar utilizando la dirección del área de datos de la cola (de NUSE_Queue_Data [] ).

Valores de retorno de llamadas de API


Muchas funciones API devuelven un valor de estado que indica con qué éxito se completó la llamada. Sería útil realizar un seguimiento de estos valores y marcar casos en los que no son iguales a NUSE_SUCCESS (es decir, tienen un valor de cero). Dado que este seguimiento es solo para depuración, la compilación condicional es bastante apropiada. La definición de una variable global (por ejemplo, NUSE_API_Call_Status ) puede compilarse condicionalmente (bajo el control del símbolo de directiva #define). Luego, parte de la definición de llamadas API, a saber, NUSE_API_Call_Status = , también se puede compilar condicionalmente. Por ejemplo, para fines de depuración, una llamada que generalmente se ve así:

NUSE_Mailbox_Send (mbox, msg, NUSE_SUSPSEND);

tomará la siguiente forma:

NUSE_API_Call_Status = NUSE_Mailbox_Send (mbox, msg, NUSE_SUSPEND);

Si se activa el bloqueo de tareas, muchas llamadas a funciones API solo pueden devolver información sobre una finalización exitosa de la llamada o que el objeto se ha restablecido. Sin embargo, si la comprobación de parámetros de API está activada, las llamadas a API podrán devolver muchos otros valores.

Establecer el tamaño de la pila de tareas y el desbordamiento de la pila


El tema de la protección de desbordamiento de pila se discutió en un artículo anterior (# 31). Hay varias otras posibilidades durante la depuración.

El área de memoria de la pila puede llenarse con un valor característico: algo más que todos o todos los ceros. Después de eso, el depurador se puede usar para monitorear las áreas de memoria y cuánto se cambiarán los valores, lo que nos permitirá comprender el grado de plenitud de la pila. Si se han cambiado todas las áreas de memoria, esto no significa que la pila esté llena, pero puede significar que su tamaño es apenas suficiente, lo cual es peligroso. Se debe aumentar y continuar las pruebas.

Como se describe en el artículo # 31, al implementar diagnósticos, se pueden ubicar áreas adicionales, "palabras protectoras", en cualquiera de los bordes del área de memoria de la pila. El depurador se puede usar para rastrear el acceso a estas palabras, ya que cualquier intento de escribirles significa un desbordamiento o un agotamiento de la pila.

Lista de verificación de configuración de Nucleus SE


Dado que Nucleus SE fue diseñado como un sistema altamente flexible y personalizable para cumplir con los requisitos de la aplicación, requiere un número significativo de parámetros personalizables. Es por eso que todo este artículo, de hecho, está dedicado a la configuración de Nucleus SE. Para asegurarse de que no nos perdamos nada, la siguiente es una lista de verificación de todos los pasos clave que debe seguir para crear la aplicación integrada Nucleus SE.
  1. Nucleus SE. , Nucleus SE , Nucleus SE .
  2. CPU/. .
  3. . , , .
  4. . . . , . 16 .
  5. . - main() ?
  6. . 4 , .
  7. , .
  8. .
  9. . , .
  10. . , .
  11. . , . . — 16 .
  12. . , .
  13. . , (, ).
  14. API. API, .


El siguiente artículo (el último de esta serie) resumirá toda la historia con Nucleus SE, y también proporcionará información para ayudarlo a crear y usar implementaciones de Nucleus SE.

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.

Source: https://habr.com/ru/post/467763/


All Articles