Al escribir para código MK más complicado que "parpadear una luz", el desarrollador se enfrenta a las limitaciones inherentes a la programación lineal en el estilo de "superciclo más interrupciones". El procesamiento de las interrupciones requiere velocidad y concisión, lo que lleva a agregar banderas al código y hacer que el estilo del proyecto "superciclo con interrupciones y banderas".
Si la complejidad del sistema crece, el número de indicadores interdependientes crece exponencialmente, y el proyecto se convierte rápidamente en un "código de pasta" poco legible y manejable.
El uso de sistemas operativos en tiempo real ayuda a eliminar el "código de pasta" y devolver la flexibilidad y capacidad de administración a un complejo proyecto MK.
Se han desarrollado varios sistemas operativos cooperativos en tiempo real y muy populares para los microcontroladores AVR. Sin embargo, todos están escritos en C o Assembler y no son adecuados para aquellos que programan MK en el entorno BASCOM AVR, privándolos de una herramienta tan útil para escribir aplicaciones serias.
Para corregir esta deficiencia, desarrollé un RTOS simple para el entorno de programación AVR de BASCOM, que llamo la atención de los lectores.

Para muchos, el estilo de programación familiar MK es el llamado
superciclo En este caso, el código consiste en un conjunto de funciones, procedimientos y descriptores (constantes, variables), posiblemente los de la biblioteca, generalmente llamados "código de fondo", así como un gran bucle infinito encerrado en una construcción
do-loop . Al inicio, el equipo del MK y los dispositivos externos se inicializan primero, se establecen las constantes y los valores iniciales de las variables, y luego se transfiere el control a este superciclo infinito.
La simplicidad del superciclo es obvia. La mayoría de las tareas realizadas por MK, porque de una forma u otra cíclica. Las desventajas también son evidentes: si algún dispositivo o señal requiere una reacción inmediata, MK lo proporcionará tan pronto como el ciclo cambie. Si la duración de la señal es más corta que el período del ciclo, dicha señal se omitirá.
En el siguiente ejemplo, queremos verificar si se
presiona el botón:
do
Obviamente, si "algún código" funciona el tiempo suficiente, MK puede no notar una breve pulsación de un botón.
Afortunadamente, MK está equipado con un sistema de interrupción que puede resolver este problema: todas las señales críticas pueden "colgarse" en las interrupciones y se puede escribir un controlador para cada una. Entonces aparece el siguiente nivel: un
superciclo con interrupciones .
El siguiente ejemplo muestra la estructura del programa con un superciclo y una interrupción que procesa un clic de botón:
on button button_isr
Sin embargo, el uso de interrupciones plantea un nuevo problema: el código del controlador de interrupciones debe ser lo más rápido y corto posible; dentro de las interrupciones, la funcionalidad MK es limitada. Dado que los AVR MK no tienen un sistema de interrupción jerárquico, no puede ocurrir otra interrupción dentro de una interrupción; en este momento, están desactivados por hardware. Por lo tanto, la interrupción debe ejecutarse lo más rápido posible, de lo contrario, otras interrupciones (y posiblemente las más importantes) se omitirán y no se procesarán.
Interrupción de la memoriaDe hecho, al estar dentro de la interrupción, MK puede notar el hecho de otra interrupción en un registro especial, lo que permite que se procese más tarde. Sin embargo, esta interrupción no se puede procesar de inmediato.
Por lo tanto, no podemos escribir algo complicado en el controlador de interrupciones, especialmente si este código debe tener demoras, porque hasta que la demora funcione, el MK no volverá al programa principal (superciclo) y estará sordo a otras interrupciones.
Debido a esto, dentro del controlador de interrupciones, a menudo solo tiene que marcar el hecho del evento con una bandera, la suya para cada evento, y luego verificar y procesar las banderas dentro del superciclo. Esto, por supuesto, alarga el tiempo de reacción al evento, pero al menos no nos perdemos algo importante.
Por lo tanto, surge el siguiente nivel de complejidad: un
superciclo con interrupciones y banderas .
Se muestra el siguiente código:
on button button_isr
Esto limita un número considerable de programas para MK. Sin embargo, estos programas suelen ser aún más o menos simples. Si escribe algo más complicado, entonces el número de banderas comienza a crecer como una bola de nieve, y el código se vuelve cada vez más confuso e ilegible. Además, en el ejemplo anterior, el problema con los retrasos no se ha resuelto. Por supuesto, puede "colgar" una interrupción separada en el temporizador, y en él ... también controlar varias banderas. Pero esto hace que el programa sea completamente feo, el número de indicadores interdependientes está creciendo exponencialmente, e incluso el propio desarrollador difícilmente puede descubrir un "código de pasta" muy pronto. Tratar de encontrar un error o modificar el código a menudo se vuelve igual en los esfuerzos para desarrollar un nuevo proyecto.
¿Cómo resolver el problema del "código de pasta" y hacerlo más legible y manejable? El
sistema operativo (SO) viene al rescate. En él, la funcionalidad que MK debería implementar se divide en tareas cuya operación es controlada por el sistema operativo.
Tipos de sistemas operativos para MK
Los sistemas operativos para MK se pueden dividir en dos grandes clases: SO con desplazamiento y SO cooperativo. En cualquiera de estos sistemas operativos, las tareas se controlan mediante un procedimiento especial llamado
despachador . En un sistema operativo con
desplazamiento, el despachador de forma independiente en cualquier momento cambia la ejecución de una tarea a otra, asignando a cada uno un cierto número de ciclos de reloj (posiblemente diferentes, dependiendo de la prioridad de la tarea). Este enfoque en su conjunto funciona muy bien, lo que le permite no mirar en absoluto el contenido de las tareas: puede escribir al menos el código de la tarea
1: goto 1
- y aún se realizarán el resto de las tareas (incluida esta). Sin embargo, los sistemas operativos preventivos requieren muchos recursos (memoria del procesador y ciclos de reloj), ya que cada conmutador debe guardar completamente el contexto de la tarea que se deshabilitará y cargar el contexto del currículum. El contexto aquí se refiere al contenido de los registros de la máquina y la pila (BASCOM usa dos pilas: la de hardware para las direcciones de retorno de los subprogramas y la de software para pasar argumentos). Esta carga no solo requiere muchos ciclos de procesador, sino que también el contexto de cada tarea debe almacenarse en algún lugar durante un tiempo hasta que funcione. En los procesadores "grandes", inicialmente orientados a la multitarea, estas funciones a menudo son compatibles con el hardware y tienen muchos más recursos. En AVR MK no hay soporte de hardware para la multitarea (todo debe hacerse "manualmente"), y la memoria disponible es pequeña. Por lo tanto, los sistemas operativos de desplazamiento, aunque existen, no son demasiado adecuados para MK simples.
Otra cosa es el
sistema operativo cooperativo . Aquí la tarea misma controla en qué punto transferir el control al despachador, lo que le permite iniciar otras tareas. Además, las tareas aquí son necesarias para hacer esto; de lo contrario, la ejecución del código se detendrá. Por un lado, parece que este enfoque reduce la confiabilidad general: si una tarea se cuelga, nunca llamará al despachador y todo el sistema dejará de responder. Por otro lado, un código lineal o un superciclo no es mejor a este respecto, ya que pueden congelarse con exactamente el mismo riesgo.
Sin embargo, un sistema operativo cooperativo tiene una ventaja importante. Como aquí el programador establece el momento de cambiarse a sí mismo, no puede suceder repentinamente, por ejemplo, mientras la tarea está trabajando con algún recurso o en el medio del cálculo de una expresión aritmética. Por lo tanto, en un sistema operativo cooperativo, en la mayoría de los casos, puede hacerlo sin mantener el contexto. Esto ahorra significativamente tiempo y memoria del procesador y, por lo tanto, parece mucho más adecuado para la implementación en MK AVR.
Cambio de tareas en BASCOM AVR
Para implementar la conmutación de tareas en el entorno AVR de BASCOM, el código de la tarea, cada uno de los cuales se implementa como un procedimiento normal, debe en algún lugar llamar al despachador, también implementado como un procedimiento normal.
Imagine que tenemos dos tareas, cada una de las cuales en algún lugar de su código es llamada por el despachador.
sub task1() do
Supongamos que se ejecutó la tarea 1. Veamos qué sucede en la pila cuando ejecuta una "llamada de despachador":
dirección de retorno al código principal (2 bytes)
parte superior de la pila -> dirección de retorno a la Tarea 1 que llamó al despachador (2 bytes)
La parte superior de la pila apuntará a la dirección de la instrucción en la Tarea 1, que sigue a la llamada del despachador (la instrucción de
bucle en nuestro ejemplo).
El objetivo del despachador en el caso más simple es transferir el control a la Tarea 2. La pregunta es ¿cómo hacer esto? (supongamos que el despachador ya conoce la dirección de la Tarea 2).
Para hacer esto, el despachador debe extraer la dirección de retorno a la Tarea 1 de la pila (y en algún lugar para recordar), y poner la dirección de la Tarea 2 en este lugar en la pila, y luego dar el comando de devolución. El procesador extraerá la dirección colocada allí de la pila y, en lugar de regresar a la Tarea 1, procederá a la ejecución de la Tarea 2.
A su vez, cuando la Tarea 2 llama al despachador, también extraemos la pila y guardamos la dirección donde será posible regresar a la Tarea 2, y cargamos la dirección de la tarea 1 previamente guardada en la pila. Dé el comando de
retorno y estaremos en el punto de continuación de la Tarea 1 .
Como resultado, tenemos un desastre:
Tarea 1 -> Despachador -> Tarea 2 -> Despachador -> Tarea 1 ....
No esta mal! Y funciona Pero, por supuesto, esto no es suficiente para un sistema operativo que es prácticamente utilizable. Después de todo, no siempre y no todas las tareas deberían funcionar; por ejemplo, pueden
esperar algo (la expiración del tiempo de retraso, la aparición de alguna señal, etc.). Por lo tanto, las tareas deben tener un
estado (TRABAJOS, LISTO, ESPERADO, etc.). Además, sería bueno tener tareas asignadas con
prioridad . Luego, si hay más de una tarea lista para su ejecución, el despachador continuará la tarea que tenga la máxima prioridad.
AQUA RTOS
Para implementar la idea descrita, se desarrolló la cooperativa OS AQUA RTOS, que proporciona los servicios necesarios para las tareas y permite implementar la multitarea cooperativa en el entorno BASCOM AVR.
Aviso importante sobre el modo de procedimiento en BASCOM AVRAntes de comenzar la descripción de AUQA RTOS, debe tenerse en cuenta que el entorno BASCOM AVR admite dos tipos de procedimientos de direccionamiento. Esto está regulado por el submodo config = nuevo | viejo
En el caso de especificar la opción anterior, el compilador, en primer lugar, compilará todo el código linealmente, independientemente de si se usa en algún lugar o no, y en segundo lugar, los procedimientos sin argumentos diseñados en el estilo del subnombre / subnombre final se percibirán como procedimientos , con el estilo del nombre: / return. Esto nos permite pasar la dirección del procedimiento como una etiqueta como argumento a otro procedimiento utilizando el modificador bylabel. Esto también se aplica a los procedimientos diseñados en el estilo del subnombre / subestilo final (debe pasar el nombre del procedimiento como una etiqueta).
Al mismo tiempo, el modo submode = old impone algunas restricciones: los procedimientos de tareas no deben contener argumentos; el código de los archivos conectados a través de $ include se incluye linealmente en el proyecto general, por lo tanto, se debe proporcionar un bypass en los archivos conectados; vaya de principio a fin usando goto y una etiqueta.
Por lo tanto, en AQUA RTOS, el usuario debe usar solo la antigua notación de tarea en el estilo de nombre_tarea: / return para tareas, o usar el subnombre más común / end sub, agregando el modificador submode = old al comienzo de su código, y omitir los archivos incluidos Ir a etiqueta / incluir código de archivo / etiqueta:.
Estados de tareas de AQUA RTOS
Los siguientes estados se definen para tareas en AQUA RTOS:
OSTS_UNDEFINE OSTS_READY OSTS_RUN OSTS_DELAY OSTS_STOP OSTS_WAIT OSTS_PAUSE OSTS_RESTART
Si la tarea aún no se ha inicializado, se le asigna el estado
OSTS_UNDEFINE .
Después de la inicialización, la tarea tiene el estado
OSTS_STOP .
Si la tarea
está lista para ejecutarse , se le asigna el estado
OSTS_READY .
La tarea actualmente en ejecución tiene el estado
OSTS_RUN .
Desde allí, puede ir a los estados
OSTS_STOP, OSTS_READY, OSTS_DELAY, OSTS_WAIT, OSTS_PAUSE .
El estado
OSTS_DELAY tiene una tarea que cumple un
retraso .
El estado
OSTS_WAIT se asigna a las tareas que están
esperando un semáforo, evento o mensaje (más información sobre ellos a continuación).
¿Cuál es la diferencia entre los
estados OSTS_STOP y
OSTS_PAUSED ?
Si por alguna razón la tarea recibe el estado de
OSTS_STOP , la reanudación posterior de la tarea (al recibir el estado de
OSTS_READY ) se llevará a cabo desde su punto de entrada, es decir. Desde el principio. Desde el estado de
OSTS_PAUSE, la tarea continuará funcionando en el lugar donde se suspendió.
Gestión del estado de la tarea
Tanto el sistema operativo en sí puede administrar automáticamente las tareas, como el usuario, llamando a los servicios del sistema operativo. Hay varios servicios de administración de tareas (los nombres de todos los servicios del sistema operativo comienzan con el prefijo
OS_ ):
OS_InitTask(task_label, task_prio) OS_Stop() OS_StopTask(task_label) OS_Pause() OS_PauseTask(task_label) OS_Resume() OS_ResumeTask(task_label) OS_Restart()
Cada uno de ellos tiene dos opciones:
OS_service y
OS_serviceTask (excepto el servicio
OS_InitTask , que solo tiene una opción; el servicio
OS_Init inicializa el sistema operativo).
¿Cuál es la diferencia entre
OS_service y
OS_serviceTask ? El primer método actúa sobre la tarea misma que la causó; el segundo le permite establecer como argumento un puntero a otra tarea y, por lo tanto, administrar otro desde una tarea.
Sobre OS_ResumeTodos los servicios de gestión de tareas, excepto OS_Resume y OS_ResumeTask, llaman automáticamente al administrador de tareas después del procesamiento. Por el contrario, los servicios OS_Resume * solo establecen el estado de la tarea en OSTS_READY. Este estado se procesará solo cuando se llame explícitamente al despachador.
Prioridad y cola de tareas
Como se mencionó anteriormente, en un sistema real, algunas tareas pueden ser más importantes, mientras que otras pueden ser secundarias. Por lo tanto, una característica útil del sistema operativo es la capacidad de asignar tareas prioritarias. En este caso, si hay varias tareas
preparadas al mismo tiempo, el sistema operativo primero seleccionará la tarea con la prioridad más alta. Si
todas las tareas preparadas tienen la misma prioridad, el sistema operativo las pondrá en ejecución en un círculo, en un orden llamado "carrusel" o round-robin.
En AQUA RTOS, la prioridad se asigna a una tarea cuando se
inicializa a través de una llamada al servicio
OS_InitTask , que
recibe la dirección de la tarea como primer argumento y un número del 1 al 15 como segundo argumento.
Un número más bajo significa una prioridad más alta . Durante el funcionamiento del sistema operativo, no se proporciona un cambio en la prioridad asignada a la tarea.
Retrasos
En cada tarea, el retraso se procesa independientemente de otras tareas.
Por lo tanto, mientras el sistema operativo está resolviendo el retraso de una tarea, se pueden ejecutar otras.
Para la organización de los servicios prestados retrasos
OS_Delay | OS_DelayTask . El argumento es el número de milisegundos para los que se
retrasa la tarea. Dado que la dimensión del argumento es
dword , el retraso máximo es de 4294967295 ms, o aproximadamente 120 horas, lo que parece ser suficiente para la mayoría de las aplicaciones. Después de llamar al servicio de demora, se llama automáticamente al despachador, que transfiere el control a otras tareas mientras dura la demora.
Semáforos
Los semáforos en AQUA RTOS son algo así como banderas y variables disponibles para las tareas. Son de dos tipos: binarios y contables. Los primeros tienen solo dos estados: libre y cerrado. El segundo es un contador de bytes (el servicio de contar semáforos en la versión actual de AQUA RTOS no está implementado (soy un vago), por lo que todo lo que se dice a continuación se aplica solo a los semáforos binarios).
La diferencia entre un semáforo y un indicador simple es que se puede hacer que la tarea
espere la liberación del semáforo especificado. De alguna manera, el uso de semáforos realmente se asemeja a un ferrocarril: al llegar al semáforo, la composición (tarea) verificará el semáforo, y si no está abierto, esperará a que la señal de activación parezca ir más allá. En este momento, otros trenes (tareas) pueden continuar moviéndose (correr).
En este caso, todo el trabajo negro se asigna al despachador. Tan pronto como se le dice a la tarea que espere el semáforo, el control se transfiere automáticamente al despachador, y él puede comenzar otras tareas, exactamente hasta que se libere el semáforo especificado. Tan pronto como el estado del semáforo cambie a
libre , el despachador asigna a todas las tareas que estaban esperando este semáforo el estado
listo (
OSTS_READY ), y se ejecutarán en el orden de prioridad y prioridad.
En total, AQUA RTOS proporciona 16 semáforos binarios (este número puede, en principio, aumentarse cambiando la dimensión de la variable en la unidad de control de tareas, porque en su interior se implementan como banderas de bits).
Los semáforos binarios funcionan a través de los siguientes servicios:
hBSem OS_CreateBSemaphore() OS_WaitBSemaphore(hBSem) OS_WaitBSemaphoreTask(task_label, hBSem) OS_BusyBSemaphore(hBSem) OS_FreeBSemaphore(hBSem)
Antes de usar un semáforo debe ser
creado . Esto se realiza llamando al servicio
OS_CreateBSemaphore , que devuelve el identificador de byte único (identificador) del semáforo
hBSem creado, o mediante el controlador definido por el usuario genera un error
OSERR_BSEM_MAX_REACHED , lo que indica que se ha alcanzado el número máximo posible de semáforos binarios.
Puede trabajar con el identificador recibido pasándolo como argumento a otros servicios de semáforos.
Servicio
OS_WaitBSemaphore | OS_WaitBSemaphoreTask coloca la tarea (actual | especificada) en un estado para
esperar el lanzamiento del semáforo hBSem si este semáforo está ocupado, y luego transfiere el control al despachador para que pueda iniciar otras tareas. Si el semáforo es libre, la transferencia de control no se produce y la tarea simplemente continúa.
Los servicios
OS_BusyBSemaphore y
OS_FreeBSemaphore establecen el semáforo
hBSem en
ocupado o
libre, respectivamente.
No se proporciona la destrucción de semáforos para simplificar el sistema operativo y reducir la cantidad de código. Por lo tanto, todos los semáforos creados son estáticos.
Eventos
Además de los semáforos, las tareas pueden ser conducidas por eventos. Se puede indicar a
una tarea que
espere un evento determinado , y otra tarea (así como el código de fondo) puede
indicar este evento. Al mismo tiempo, todas las tareas que esperaban este evento recibirán el estado
listo para la ejecución (
OSTS_READY ) y serán establecidas por el despachador para su ejecución en el orden de prioridad y prioridad.
¿A qué eventos puede responder la tarea? Bueno, por ejemplo:
- interrupción
- ocurrencia de un error;
- liberación del recurso (a veces es más conveniente usar un semáforo para esto);
- cambiar el estado de la línea de E / S o presionar una tecla en el teclado;
- recibir o enviar un personaje a través de RS-232;
- transferencia de información de una parte de la aplicación a otra (ver también mensajes).
El sistema de eventos se implementa a través de los siguientes servicios:
hEvent OS_CreateEvent() OS_WaitEvent(hEvent) OS_WaitEventTask(task_label, hEvent) OS_WaitEventTO(hEvent, dwTimeout) OS_SignalEvent(hEvent)
Antes de usar un evento, debe
crearlo . Esto se realiza llamando a la función
OS_CreateEvent , que devuelve un identificador de byte único (identificador) para el evento
hEvent , o arroja un error
OSERR_EVENT_MAX_REACHED a través del controlador definido por el usuario, lo que indica que se ha alcanzado el límite en el número de eventos que se pueden generar en el sistema operativo (máximo 255 eventos diferentes).
Para hacer que una tarea espere un evento
hEvent , llame a
OS_WaitEvent en su código, pasando el identificador del evento como argumento. Después de llamar a este servicio, el control se transferirá automáticamente al despachador.
A diferencia del servicio de semáforo, el servicio de eventos ofrece una opción para esperar un evento con un
tiempo de espera . Para hacer esto, use el servicio
OS_WaitEventTO . El segundo argumento aquí puede especificar el número de milisegundos que la tarea puede esperar del evento. Si el tiempo especificado ha expirado, la tarea recibirá el estado
listo para la ejecución como si hubiera ocurrido el evento, y el despachador lo configurará para continuar la ejecución en el orden de prioridad y prioridad. La tarea puede conocer el hecho de que no fue un evento, sino un tiempo de espera, marcando el
indicador global
OS_TIMEOUT .
La tarea o el código de fondo pueden señalar la ocurrencia de un evento dado llamando al servicio OS_SignalEvent , que recibe el identificador del evento como un argumento. En este caso, todas las tareas que esperan este evento, el sistema operativo establecerá el estado listo para la ejecución , de modo que puedan continuar ejecutándose en orden de prioridad y prioridad.Mensajes
El sistema de mensajes funciona en general de manera similar al sistema de eventos, pero proporciona tareas con más opciones y flexibilidad: proporciona no solo la expectativa de un mensaje sobre un tema específico, sino la forma en que el mensaje se transmite de una tarea a otra: un número o una cadena.Esto se implementa a través de los siguientes servicios: hTopic OS_CreateMessage() OS_WaitMessage(hTopic) OS_WaitMessageTask(task_label, hTopic) OS_WaitMessageTO(hTopic, dwTimeout) OS_SendMessage(hTopic, wMessage) word_ptr OS_GetMessage(hTopic) word_ptr OS_PeekMessage(hTopic) string OS_GetMessageString(hTopic) string OS_PeekMessageString(hTopic)
Para usar el servicio de mensajería, primero debe crear un asunto de mensaje . Esto se realiza a través del servicio OS_CreateMessage , que devuelve el identificador de byte del tema hTopic , o mediante el controlador definido por el usuario, arroja un error OSERR_TOPIC_MAX_REACHED , lo que indica que se ha alcanzado el número máximo posible de temas de mensaje y ya no se puede crear.Para indicarle a la tarea que espere un mensaje sobre el tema hTopic , llame a OS_WaitMessage en su código , pasando el identificador del tema como argumento. Después de llamar a este servicio, el control se transferirá automáticamente al administrador de tareas. Por lo tanto, este servicio coloca la tarea actual en un estadoEspere un mensaje hTopic .El servicio de espera con el tiempo de espera OS_WaitMessageTO funciona de manera similar al servicio OS_WaitEventTO del sistema de eventos .Para enviar mensajes, se proporciona el servicio OS_SendMessage . El primer argumento es el identificador del tema al que se transmitirá el mensaje, y el segundo es el argumento de dimensión de palabra . Esto puede ser un número independiente o un puntero a una cadena , que, a su vez, ya es un mensaje.Para obtener un puntero de línea, simplemente use la función varptr integrada en BASCOM , por ejemplo, así: strMessage = "Hello, world!" OS_SendMessage hTopic, varptr (strMessage)
Reanudar el trabajo después de llamar a OS_WaitMessage , es decir, cuando se recibe el mensaje esperado, la tarea puede recibir el mensaje con su posterior destrucción automática o simplemente ver el mensaje; en este caso, no se destruirá. Para hacer esto, use los últimos cuatro servicios de la lista. Los dos primeros devuelven varias palabras de dimensión , que pueden ser un mensaje independiente o servir como puntero a la cadena que contiene el mensaje. En este caso, OS_GetMessage elimina automáticamente el mensaje y OS_PeekMessage lo deja.Si la tarea necesita inmediatamente una cadena, no un puntero, puede usar los servicios OS_GetMessageString o OS_PeekMessageString , que funcionan de manera similar a las dos anteriores, pero devuelven una cadena, no un puntero.Servicio de temporizador interno
Para trabajar con los retrasos y tiempos AQUA RTOS utiliza un temporizador de hardware integrado IC TIMER0 . Por lo tanto, el código externo (antecedentes y tareas) no debe usar este temporizador. Pero generalmente esto no es necesario, porque El sistema operativo proporciona tareas con todas las herramientas necesarias para trabajar con intervalos de tiempo. La resolución del temporizador es de 1 ms.Ejemplos de trabajo con AQUA RTOS
Configuraciones iniciales
Al comienzo del código de usuario, debe determinar si el código se ejecutará en el simulador incorporado o en hardware real. Defina la constante OS_SIM = TRUE | FALSO , que establece el modo de simulación.Además, en el código del sistema operativo, edite la constante OS_MAX_TASK , que determina el número máximo de tareas admitidas por el sistema operativo. Cuanto menor es este número, más rápido funciona el sistema operativo (menos sobrecarga) y menos memoria consume. Por lo tanto, no debe indicar más tareas de las que necesita. No olvide cambiar esta constante si el número de tareas ha cambiado.Inicialización del sistema operativo
Antes de comenzar, AQUA RTOS debe inicializarse. Para hacer esto, llame al servicio OS_Init . Este servicio configura la configuración inicial del sistema operativo. Más importante aún, tiene un argumento: la dirección de la rutina de manejo de errores definida por el usuario. Ella, a su vez, también tiene un argumento: un código de error.Este controlador debe estar en el código de usuario (al menos en forma de código auxiliar): el sistema operativo le envía códigos de error y el usuario no tiene otra forma de atraparlos y procesarlos en consecuencia. Recomiendo encarecidamente que, al menos en la etapa de desarrollo, no ponga un código auxiliar, sino que incluya cualquier salida de información de error en este procedimiento.Entonces, el primer paso para trabajar con AQUA RTOS es agregar el código de inicialización del sistema operativo y el procedimiento de manejo de errores al programa del usuario: OS_Init my_err_trap
Inicialización de tareas
El segundo paso es inicializar las tareas especificando sus nombres y prioridad: OS_InitTask task_1, 1 OS_InitTask task_2 , 1
Tareas de prueba
LED intermitente
Entonces, creemos una aplicación de prueba que se pueda descargar a una placa Arduino Nano V3 estándar. Cree una carpeta en la carpeta con el archivo del sistema operativo (por ejemplo, prueba), y cree el siguiente archivo bas:
Conecte los ánodos de los LED a los pines D4 y D5 de la placa Arduino (u otros pines cambiando las líneas de definición correspondientes en el código). Conecte los cátodos a través de resistencias de terminación de 100 ... 500 ohmios al bus GND . Compila y sube el firmware a la placa. Los LED comenzarán a cambiar de forma asincrónica con un período de 2 y 0,66 s.Miremos el código. Entonces, primero inicializamos el equipo (establecemos las opciones del compilador, el modo de operación del puerto y asignamos alias), luego el sistema operativo en sí y finalmente las tareas.Dado que las tareas recién creadas están en estado "detenido", debe darles el estado "listo para la ejecución" (quizás no todas las tareas en una aplicación real; después de todo, algunas de ellas, según la intención del desarrollador, pueden estar inicialmente en un estado detenido y ejecutarse en ejecución solo desde otras tareas, y no inmediatamente al inicio del sistema; sin embargo, en este ejemplo, ambas tareas deberían comenzar a funcionar inmediatamente). Por lo tanto, para cada tarea, llamamos al servicio OS_ResumeTask .Ahora las tareas están listas para su ejecución, pero aún no se han completado. ¿Quién los lanzará? Por supuesto, el despachador! Para hacer esto, debemos llamarlo al primer inicio del sistema. Ahora, si todo está escrito correctamente, el despachador realizará nuestras tareas una por una, y podemos finalizar la parte principal del programa con la declaración final.Veamos las tareas. Es inmediatamente evidente que cada uno de ellos está enmarcado como un do-loop sin fin . La segunda propiedad importante, dentro de dicho ciclo, debe haber al menos una llamada al despachador o al servicio del sistema operativo que llama automáticamente al despachador después de sí mismo; de lo contrario, dicha tarea nunca cederá el control y otras tareas no podrán completarse. En nuestro caso, este es el servicio de demora OS_Delay . Como argumento, le indicamos el número de milisegundos para los cuales se debe pausar cada tarea.Si establece la constante OS_SIM = TRUE al comienzo del código y ejecuta el código no en el chip real, sino en el simulador, puede rastrear cómo funciona el sistema operativo.El despachador al que llamamos verá si las tareas con el estado están "listas para la ejecución" y las alineará según la prioridad. Si la prioridad es la misma, el despachador "rodará las tareas en el carrusel", moviendo la tarea recién completada al final de la cola.Después de seleccionar la tarea a ejecutar (por ejemplo, tarea_1 ), el despachador reemplaza la dirección de retorno (inicialmente, señala la instrucción final en el código principal) con la dirección del punto de entrada de la tarea tarea_1 , que el sistema reconoce durante la inicialización de la tarea, y ejecuta el comando de retorno , lo que obliga MK para extraer la dirección de retorno de la pila e ir a ella, es decir, iniciar la ejecución de la tarea task_1 (operadorhacer en la tarea_1 código ).La tarea task_1 , al cambiar su LED, llama al servicio OS_delay , que, una vez completadas las acciones necesarias, va al despachador.El despachador guarda la dirección que estaba en la pila en la unidad de control de tarea task_1 (señala la instrucción que sigue a la llamada OS_delay , es decir, la instrucción de bucle ) y luego, "girando el carrusel", descubre que la tarea_2 ahora debe completarse . Empuja la dirección de tarea task_2 (actualmente apunta a la instrucción do en el código de tarea task_2 ) y ejecuta el comandoreturn , lo que hace que MK extraiga la dirección de retorno de la pila y vaya a ella, es decir, comience a ejecutar task_2 .La tarea task_2 , cambiando su LED, llama al servicio OS_delay , que, después de realizar las acciones necesarias, va al despachador.El despachador guarda la dirección que estaba en la pila en la unidad de control de tarea task_1 (señala la instrucción que sigue a la llamada OS_delay , es decir, la instrucción de bucle ) y luego, "girando el carrusel", descubre que task_2 ahora debe completarse . La diferencia con el estado inicial será que ahora en el bloque de control de tareas task_1no se almacena la dirección de inicio de la tarea, sino la dirección del punto desde el cual ocurrió la transición al despachador. Allí (a la instrucción de bucle en el código de tarea task_1 ), y se transferirá el control.La tarea tarea_1 ejecutará la instrucción de bucle , y luego todo el ciclo "Tarea 1 - Despachador - Tarea 2 - Despachador" se repetirá sin cesar.Enviar mensajes
Ahora intentemos enviar mensajes de una tarea a otra.
El resultado de ejecutar el programa en el simulador será el siguiente resultado en la ventana de terminal:task 1
task 2 is waiting messages…
task 1
task 1
task 1
task 1 is sending message to task 2
task 1
message recieved: Hello, task 2!
task 2 is waiting messages…
task 1
task 1
...
Preste atención al orden en que se produce el cambio de trabajo y tarea. Tan pronto como la Tarea 1 imprime la tarea 1 , el control se transfiere al despachador para que pueda comenzar la segunda tarea. La tarea 2 imprime la tarea 2 está esperando mensajes ... , luego llama al servicio de mensajes en espera sobre el tema hTopic , y el control se transfiere automáticamente al despachador, que nuevamente llama a la Tarea 1. Imprime la tarea 1 nuevamente y le da el control al despachador. Sin embargo, dado que el despachador detecta que la Tarea 2 ahora está esperando mensajes, devuelve el control a la Tarea 1 en la declaración incr después de la llamada del despachador.Cuando el contador task_1_cnten la Tarea 1, excederá el valor especificado, la tarea envía un mensaje, pero continúa ejecutándose: ejecuta la instrucción de bucle e imprime la tarea 1 nuevamente . Después de eso, llama al despachador, que ahora descubre que hay un mensaje para la Tarea 2, y le transfiere el control. A continuación, el proceso se realiza cíclicamente.Manejo de eventos
El siguiente código sondea dos botones y cambia los LED cuando presiona el botón correspondiente:
Un ejemplo de aplicación real bajo AQUA RTOS
Tratemos de imaginar cómo se vería un programa de máquinas expendedoras de café. La máquina debe indicar la presencia de opciones de café y la elección de LED en los botones; reciba señales del receptor de monedas, prepare la bebida ordenada, emita el cambio. Además, la máquina debe controlar el equipo interno: por ejemplo, mantener la temperatura del calentador de agua a 95 ... 97 ° C; transmite datos sobre el mal funcionamiento del equipo y el stock de ingredientes y recibe comandos a través de acceso remoto (por ejemplo, a través de un módem GSM), así como señal de vandalismo.Enfoque dirigido por eventos
Al principio, puede ser difícil para un desarrollador cambiar del esquema habitual de "superciclo + banderas + interrupciones" a un enfoque basado en tareas y eventos. Esto requiere resaltar las tareas básicas que debe realizar el dispositivo.Tratemos de describir tales tareas para nuestra máquina:- control y gestión de calentadores - ControlHeater ()
- indicación de disponibilidad y elección de bebidas - ShowGoods ()
- aceptación de monedas / billetes y su suma - AcceptMoney ()
- botones de sondeo - ScanKeys ()
- cambiar entrega - MakeChange ()
- beber licencia - ReleaseCoffee ()
- Protección contra vandalismo - Alarma ()
Calculemos el grado de importancia de las tareas y la frecuencia de su llamada.ControlHeater () es obviamente importante porque siempre necesitamos agua hirviendo para hacer café. Pero no debe realizarse con demasiada frecuencia, porque el calentador tiene una alta inercia y el agua se enfría lentamente. Es suficiente verificar la temperatura una vez por minuto.Dar prioridad a esta tarea 5. ShowGoods () no es demasiado importante. La oferta puede cambiar solo después de la liberación de los productos, si se agota el suministro de cualquier ingrediente. Por lo tanto, le daremos a esta tarea una prioridad de 8 y dejaremos que se ejecute cuando la máquina se inicie y cada vez que se liberen los productos.ScanKeys ()debe tener una prioridad lo suficientemente alta para que la máquina responda rápidamente a las pulsaciones de botones. Otorgue a esta tarea la prioridad 3, y la ejecutaremos cada 40 milisegundos.AcceptMoney () también es parte de la interfaz de usuario. Le daremos la misma prioridad que ScanKeys (), y lo ejecutaremos cada 20 milisegundos.MakeChange () se ejecuta solo después de que se liberan los productos. Lo asociaremos con ReleaseCoffee () y le daremos prioridad 10.ReleaseCoffee () solo es necesario cuando se ha aceptado la cantidad adecuada de dinero y se presiona el botón de selección de bebida. Para una respuesta rápida, le damos prioridad 2.Dado que la resistencia al vandalismo es una función bastante importante de la máquina, la tarea de alarma ()puede establecer la prioridad más alta: 1 y activar una vez por segundo para verificar los sensores de inclinación o manipulación.Por lo tanto, necesitaremos siete tareas con diferentes prioridades. Después del inicio, cuando el programa lee la configuración de EEPROM e inicializa el equipo, es hora de inicializar el sistema operativo y comenzar las tareas.
Para trabajar como parte de RTOS, cada tarea debe tener una estructura determinada: debe tener al menos una llamada al despachador (o un servicio del sistema operativo que transfiere automáticamente el control al despachador): esta es la única forma de garantizar la multitarea cooperativa.Por ejemplo, ReleaseCoffee () podría verse así: sub ReleaseCoffee() do OS_WaitMessage bCoffeeSelection wItem = OS_GetMessage(bCoffeeSelection) Release wItem loop end sub
La tarea ReleaseCoffee en un bucle infinito espera un mensaje sobre el tema bCoffeeSelection y no hace nada hasta que llega (el control se devuelve automáticamente al despachador para que pueda iniciar otras tareas). Tan pronto como se envía el mensaje, ReleaseCoffee () está listo para la ejecución, y cuando esto sucede, la tarea recibe el contenido del mensaje (código de la bebida seleccionada) wItem utilizando el servicio OS_GetMessage y libera los productos al cliente. Dado que ReleaseCoffee () usa el subsistema de mensajes, se debe crear un mensaje antes de comenzar la multitarea: dim bCoffeeSelection as byte bCoffeeSelection = OS_CreateMessage()
Como se mencionó anteriormente, ShowGoods () debe ejecutarse una vez al inicio y cada vez que se liberan productos. Para asociarlo con el procedimiento de lanzamiento ReleaseCoffee () , utilizamos el servicio de eventos. Para hacer esto, cree un evento antes de comenzar la multitarea: dim bGoodsReliased as byte bGoodsReliased = OS_CreateEvent()
Y en el procedimiento ReleaseCoffee () , después de la línea Release wItem agregamos una alarma sobre el evento bGoodsReliased : OS_SignalEvent bGoodsReliased
Inicialización del sistema operativo
Para preparar el sistema operativo para el trabajo, debemos inicializarlo, indicando la dirección del controlador de errores, que se encuentra en el código del usuario. Hacemos esto usando el servicio OS_Init : OS_Init Mailfuncion
En el código de usuario, debe agregar un controlador, un procedimiento cuyo argumento de byte es el código de error: sub Mailfuncion (bCoffeeErr) print "Mailfunction! Error #: "; bCoffeeErr if isErrCritical (bCoffeeErr) = 1 then CallService(bCoffeeErr) end if end sub
Este procedimiento imprime un código de error (o puede mostrarlo de alguna otra manera: en la pantalla, a través de un módem GSM, etc.), y en caso de que el error sea crítico, llama al departamento de servicio.Lanzamiento de tarea
Ya recordamos que eventos, semáforos, etc. debe inicializarse antes de ser utilizado. Además, las tareas mismas deben inicializarse con el servicio OS_InitTask antes de comenzar : OS_InitTask ControlHeater , 5 OS_InitTask ShowGoods , 8 OS_InitTask AcceptMoney , 3 OS_InitTask ScanKeys , 3 OS_InitTask MakeChange, 10 OS_InitTask ReleaseCoffee , 2 OS_InitTask Alarm , 1
Como el modo multitarea aún no ha comenzado, el orden en que comienzan las tareas es insignificante y, en cualquier caso, no depende de sus prioridades. En este punto, todas las tareas todavía están en estado detenido. Para prepararlos para la ejecución, debemos usar el servicio OS_ResumeTask para establecer el estado "listo para la ejecución": OS_ResumeTask ControlHeater OS_ResumeTask ShowGoods OS_ResumeTask AcceptMoney OS_ResumeTask ScanKeys OS_ResumeTask MakeChange OS_ResumeTask ReleaseCoffee OS_ResumeTask Alarm
Como ya se mencionó, no todas las tareas deben comenzar necesariamente cuando se inicia la multitarea; algunos de ellos pueden en cualquier momento permanecer en un estado "detenido" y recibir disponibilidad solo bajo ciertas condiciones. El servicio OS_ResumeTask se puede llamar en cualquier momento desde cualquier parte del código (fondo o tarea) cuando la multitarea ya se está ejecutando. Lo principal es que la tarea a la que se refiere está preinicializada.Inicio multitarea
Ahora todo está listo para comenzar la multitarea. Hacemos esto llamando al despachador: OS_Sheduler
Después de eso, podemos poner fin con seguridad en el código del programa : el sistema operativo ahora se encarga de la ejecución posterior del código.Veamos todo el código:
Por supuesto, un enfoque sería más correcto, no con el sondeo periódico de botones y un sensor de efectivo, sino con el uso de interrupciones. En los manejadores de estas interrupciones, podríamos usar el envío de mensajes usando el servicio OS_SendMessage () con contenido igual al número de la tecla presionada o el valor de la moneda / billete ingresado. Invito al lector a modificar el programa por su cuenta. Gracias al enfoque orientado a tareas y al servicio proporcionado por el sistema operativo, esto requerirá cambios mínimos de código.Código fuente AQUA RTOS
El código fuente de la versión 1.05 está disponible para descargar aquíPostdata
P: ¿Por qué AQUA?R: Bueno, hice el controlador del acuario, es como una "casa inteligente", no solo para las personas, sino también para los peces. Lleno de todo tipo de sensores, un reloj en tiempo real, relés y salidas de potencia analógicas, un menú en pantalla, un "programa de eventos" flexible e incluso un módulo WiFi. Los intervalos deben contarse, los botones deben sondearse, los sensores deben procesarse, el programa del evento debe leerse y ejecutarse desde la EEPROM, la pantalla debe actualizarse y el Wi-Fi debe responder. Además, el controlador debe ir a un menú de varios niveles para la configuración y la programación. Hacer en banderas e interrupciones es solo obtener el mismo "código de pasta", que no se entiende ni se modifica. Por lo tanto, decidí que necesitaba un sistema operativo. Aquí ella es AQUA.P: ¿Seguramente el código está lleno de errores lógicos y problemas técnicos?A: seguramente. Yo, como pude, ideé una variedad de pruebas y manejé el sistema operativo en una variedad de tareas, e incluso bloqueé un número notable de errores, pero esto no significa que todo esté completo. Estoy más que seguro de que todavía hay muchos de ellos en las calles secundarias del código. Por lo tanto, estaré muy agradecido si, en lugar de pincharme con los errores en la cara, los señalas educadamente y con tacto, y mejor me dices cómo crees que es mejor solucionarlos. También será genial si el proyecto se desarrolla más como producto de la creatividad colectiva. Por ejemplo, alguien agregará un servicio para contar semáforos (¿no lo olvidó? Soy un holgazán) y ofrecerá otras mejoras. En cualquier caso, estaré muy agradecido por la contribución constructiva.