Un poco sobre la multitarea en microcontroladores

Un poco sobre multitarea


Todos los que día tras día, o de un caso a otro, se dedican a programar microcontroladores, tarde o temprano se enfrentarán a la pregunta: ¿debo usar un sistema operativo multitarea? Hay bastantes de ellos en la red, y muchos de ellos son gratuitos (o casi gratuitos). Solo elige.


Surgen dudas similares cuando se encuentra con un proyecto en el que el microcontrolador debe realizar simultáneamente varias acciones diferentes. Algunos de ellos no están conectados con otros, mientras que el resto, por el contrario, no pueden sin el otro. Además, puede haber demasiados de ambos. Lo que es "demasiado" depende de quién evaluará o quién llevará a cabo el desarrollo. Bueno, si es la misma persona.


Más bien, no se trata de una cantidad, sino de una diferencia cualitativa en las tareas en relación con la velocidad de ejecución, o algunos otros requisitos. Tales pensamientos pueden surgir, por ejemplo, cuando el proyecto necesita monitorear regularmente el voltaje de suministro (¿falta?), Lea y guarde con frecuencia los valores de las cantidades de entrada (no dan descanso), controle ocasionalmente la temperatura y controle el ventilador (no hay nada que respirar), verifique su mira con alguien de confianza (es bueno que lo ordene), manténgase en contacto con el operador (trate de no irritarlo), verifique la suma de verificación de la memoria permanente del programa para detectar demencia (cuando se enciende, o una vez a la semana, o en la mañana).


Dichas tareas heterogéneas pueden programarse de manera bastante significativa y exitosa, dependiendo de una sola tarea en segundo plano e interrupciones del temporizador. En el controlador de estas interrupciones, cada vez que se ejecuta una de las "piezas" de la siguiente tarea. Dependiendo de la importancia, urgencia o consideraciones similares, estos desafíos a menudo se repiten para algunas tareas, pero rara vez para otras. Y, sin embargo, debemos asegurarnos de que cada tarea realice una parte breve del trabajo, luego se prepare para la siguiente pequeña porción del trabajo, y así sucesivamente. Este enfoque, si te acostumbras, no parece demasiado complicado. La incomodidad ocurre cuando quieres construir un proyecto. O, por ejemplo, de repente transferir a otro. Cabe señalar que el segundo es a menudo más difícil y sin ninguna tarea de pseudo-muchos.


Pero, ¿qué pasa si usa un sistema operativo listo para microcontroladores? Claro, muchos lo hacen. Esta es una buena opción Pero el autor de estas líneas, hasta ahora, ha sido y sigue siendo detenido por la idea de que será necesario comprender esto, después de pasar mucho tiempo, elegir entre lo que conseguimos y usar solo lo que realmente se necesita de esto. ¡Y haz todo esto, fíjate, profundizando en el código de otra persona! Y no hay certeza de que en seis meses esto no tendrá que repetirse, ya que será olvidado.


En otras palabras, ¿por qué necesita un garaje lleno de herramientas y accesorios si una bicicleta se almacena y se usa allí?


Por lo tanto, había un deseo de hacer un simple "cambio" de tareas solo para Cortex-M4 (bueno, tal vez incluso para M3 y M7). Pero el viejo y buen deseo de no esforzarse mucho no desapareció.


Entonces, hacemos lo más simple. Un pequeño número de tareas comparte el tiempo de ejecución por igual. Como en la Figura 1 a continuación, cuatro tareas hacen esto. Deje que el principal sea cero, ya que es difícil imaginar otro.



Al trabajar de esta manera, se garantiza que obtendrán su espacio o intervalo de tiempo (tick) y no están obligados a conocer la existencia de otras tareas. Cada tarea exactamente después de 3 ticks tendrá nuevamente la oportunidad de hacer algo.


Pero, por otro lado, si se requiere cualquiera de las tareas para esperar un evento externo, por ejemplo, presionar un botón, entonces perderá estúpidamente un tiempo precioso de nuestro microcontrolador. No podemos estar de acuerdo con esto. Y nuestro sapo (conciencia) también. Algo debe hacerse.


Y deje que la tarea, si no tiene nada que hacer hasta ahora, dedique el tiempo restante de la marca a sus camaradas, quienes, muy probablemente, aran con todas sus fuerzas.


En otras palabras, compartir es necesario. Deje que la tarea 2 haga exactamente eso, como en la Figura 2.



¿Y por qué no debería permitirse que nuestra tarea en segundo plano renuncie el resto del tiempo, si todavía tiene que esperar? Permitámoslo. Como se muestra en la Figura 3.



¿Y si sabe que algunas de las tareas no requerirán que vuelva a verificar algo o simplemente que funcione? Y podía permitirse dormir un poco, y en cambio perdería el tiempo y se pondría de pie. No es un pedido, necesita ser arreglado. Deje que la tarea 3 pierda una parte de su tiempo (o mil). Como se muestra en la figura 4.



Bueno, como vemos, hemos delineado una coexistencia justa de tareas o algo así. Ahora debemos hacer que nuestras tareas individuales se comporten según lo prescrito. Y si tratamos de valorar el tiempo, vale la pena recordar un lenguaje de bajo nivel (no le tengo miedo al ensamblador de palabras) y no confiar completamente en el compilador de ningún idioma, de alto nivel o muy alto. De hecho, en el fondo de nuestros corazones, estamos decididamente en contra de toda dependencia. Además, el hecho de que no necesitamos ningún ensamblador, sino solo de Cortex-M4, simplifica nuestras vidas.


Para la pila, seleccionamos un área común de RAM que se llenará, es decir, en la dirección de disminución de las direcciones de memoria. Por qué Solo porque no funciona de manera diferente. Dividiremos mentalmente esta importante área en secciones iguales de acuerdo con la cantidad máxima declarada de nuestras tareas. La Figura 5 muestra esto para cuatro tareas.



A continuación, seleccionamos el lugar donde almacenaremos copias de los punteros de la pila para cada tarea. Ahora, al interrumpir el temporizador, que tomamos como temporizador del sistema, guardamos todos los registros de la tarea actual en su área de pila (el registro SP ahora apunta hacia allí), luego guardamos su puntero de pila en un lugar especial (guardamos su valor), obtenemos el puntero de pila de la siguiente tarea ( escriba un nuevo valor en el registro SP) desde nuestro lugar especial y restaure todos sus registros. Sus copias ahora están indicadas por el registro SP de nuestra próxima tarea. Bueno, salimos de la interrupción, por supuesto. Además, todo el contexto de la siguiente tarea en la lista aparece en los registros.


Probablemente, será superfluo decir que el siguiente después de task3 en la cola será principal. Y no es superfluo, por supuesto, recordar que el Cortex-M4 ya tiene un temporizador SysTick y una interrupción especial, y muchos fabricantes de microcontroladores lo saben. Lo usaremos y esta interrupción según lo previsto.


Para iniciar este temporizador del sistema, así como realizar todas las preparaciones y comprobaciones necesarias, debe utilizar el procedimiento previsto para esto.


U8 main_start_task_switcher(void); 

Esta rutina devuelve 0 si todas las verificaciones han pasado, o un código de error si algo salió mal. Se verifica, básicamente, si la pila está alineada correctamente y si hay suficiente espacio para ella, y también todos nuestros lugares especiales están llenos de valores iniciales. En resumen, el aburrimiento.


Si alguien quiere ver el texto del programa, al final de la narración podrá hacerlo fácilmente, por ejemplo, a través del correo personal.


Sí, olvidé por completo cuando sacamos los registros de la siguiente tarea del almacenamiento por primera vez en su vida, es necesario que obtengan valores originales significativos. Y dado que los recogerá de su sección de la pila, debe colocarlos allí con anticipación y mover el puntero de la pila para que sea conveniente tomarlos. Para esto necesitamos un procedimiento


  U8 task_run_and_return_task_number(U32 taskAddress); 

En esta subrutina, informamos la dirección de 32 bits del comienzo de nuestra tarea que queremos ejecutar. Y ella (la subrutina) nos dice el número de la tarea, que resultó en una tabla general especial, o 0 si no había espacio en la tabla. Entonces podemos ejecutar otra tarea, luego otra y así sucesivamente, a pesar de que los tres se suman a nuestra tarea principal interminable. Ella nunca le dará su número cero a nadie.


Algunas palabras sobre prioridades. La principal prioridad era y sigue siendo no sobrecargar al lector con detalles innecesarios.


Pero en serio, debemos recordar que hay interrupciones de los puertos seriales, de varias conexiones SPI, de un convertidor analógico a digital, de otro temporizador, después de todo. ¿Y qué sucederá si vamos a cambiar a otra tarea (cambiar de contexto) cuando estemos en el controlador de algún tipo de interrupción? Después de todo, esto no será una tarea legítima, sino un enturbiamiento temporal del programa. Y mantendremos este extraño contexto como algún tipo de tarea. Habrá una confusión: el collar no se abrocha, la tapa no encaja. Para, no, esto es de una historia diferente.


En nuestro caso, esto simplemente no se puede permitir. No debemos permitirnos cambiar de contexto durante el procesamiento de una interrupción no planificada. Aquí están las prioridades para esto. Solo tenemos que esperar un poco, y solo entonces, cuando termine esta audacia sin precedentes, cambie con calma a otra tarea. En resumen, la prioridad de la interrupción de nuestro cambio de tarea debe ser más débil que la prioridad de cualquiera de las otras interrupciones utilizadas. Esto, por cierto, también se realiza en nuestro procedimiento de inicio, y es allí donde se instala, la mayor prioridad posible.


No quería hablar, pero tenía que hacerlo. Nuestro procesador tiene dos modos de funcionamiento: privilegiado y no privilegiado. Y también dos registros para el puntero de la pila:
SP principal y proceso SP. Por lo tanto, no cambiaremos por pequeñeces, usaremos solo el modo privilegiado y solo el puntero de la pila principal. Además, todo esto ya se ha dado al inicio del controlador. Entonces, simplemente no complicaremos nuestras vidas.


Queda por recordar que cada tarea, sin duda, le gustaría poder tirar todo al infierno y cómo relajarse. Y esto puede suceder en cualquier momento durante la jornada laboral, es decir, durante nuestro tic. Cortex-M4 proporciona para tales casos un comando de ensamblador especial SVC, que adaptaremos a nuestra situación. Conduce a una interrupción que nos llevará a la meta. Y permitiremos que la tarea no solo salga del lugar de trabajo después del almuerzo, sino que no venga mañana. Por qué, que venga después de las vacaciones. Y si es necesario, déjelo venir cuando finalice la reparación o no llegue en absoluto. Para hacer esto, hay un procedimiento que la tarea misma puede causar.


  void release_me_and_set_sleep_period(U32 ticks); 

Esta rutina solo necesita indicar cuántas garrapatas están planeadas para descansar. Si es 0, puede descansar solo el resto de la marca actual. Si 0xFFFFFFFF, la tarea "dormirá" hasta que alguien se despierte. Todos los demás números indican la cantidad de tics durante los cuales la tarea estará en estado de suspensión.


Para que alguien más pueda despertarse de lado o hacerlo dormir, tuve que agregar tales procedimientos.


  void task_wake_up_action(U8 taskNumber); void set_task_sleep_period(U8 taskNumber, U32 ticks); 

Y, por si acaso, incluso esa subrutina.


  void task_remove_action(U8 taskNumber); 

En términos generales, tacha una tarea de la lista de empleados. Honestamente, aún no sé por qué lo escribí. ¿De repente es útil?


Es hora de mostrar cómo se ve el lugar donde una tarea se reemplaza por otra, es decir, el interruptor en sí.


Por si acaso, recordemos que algunos de los registros, al ingresar a la interrupción, se guardan en la pila sin nuestra participación, automáticamente (como es habitual en Cortex-M4). Por lo tanto, solo necesitamos guardar el resto. Esto se puede ver a continuación. No se alarme por lo que ve, estas son las instrucciones del ensamblador Cortex-M4 (M3, M7), como se describe en el IAR Embedded Workbench.


Aquellos que aún no han encontrado instrucciones de montaje, solo créanme, realmente se ven así. Estas son las moléculas que componen cualquier programa bajo el ARM Cortex-M4.


 SysTick_Handler STMDB SP!,{R4-R11} ;   LDR R0,=timersTable ;    LDR R1,=stacksTable ;    LDR R2,[R0] ;R2   ()  STR SP,[R1,R2,LSL #2] ;   SP (R2 * 4) __st_next_check ADD R2,R2,#1 ;   CMP R2,#TASKS_LIMIT ;R2-TASKS_LIMIT  BLO __st_no_border_yet ;   MOV R2,#0 ;    (main) LDR R3,[R1] ; main SP MOV SP,R3 B __st_timer_ok __st_no_border_yet ;; LDR SP,[R1,R2,LSL #2] ;    (errata Cortex M4) ;; CMP SP,#0 ; LDR R3,[R1,R2,LSL #2] ;  SP      CMP R3,#0 ; =0     BEQ __st_next_check MOV SP,R3 LDR R3,[R0,R2,LSL #2] ;  suspend timer CBZ R3,__st_timer_ok ; 0    ,   ; CMP R3,#0xFFFFFFFF ; ,   BEQ __st_next_check SUB R3,R3,#1 ;  1 STR R3,[R0,R2,LSL #2] ;  suspend timer B __st_next_check __st_timer_ok STR R2,[R0] ;     LDMIA SP!,{R4-R11} ;  R4-R11 BX LR 

Manejar la interrupción ordenada por la tarea en sí misma cuando devuelve el resto de la marca parece similar. La única diferencia es que todavía debe preocuparse por dormir un poco más tarde (o quedarse dormido a fondo). Hay una sutileza. Deben realizarse dos acciones, escribir el número deseado en el temporizador de apagado y hacer que se interrumpa el SVC. El hecho de que estas dos acciones no ocurran atómicamente (es decir, no ambas al mismo tiempo) me preocupa un poco. Imagine por un milisegundo que acabamos de activar el temporizador y en ese momento era hora de trabajar en otra tarea. La otra comenzó a gastar su tic, mientras que nuestra tarea será dormir los próximos tics, como se esperaba (porque su temporizador no es cero). Luego, cuando llegue el momento, nuestra tarea recibirá su tic e inmediatamente le dará la interrupción de SVC, debido a las dos acciones que aún queda por hacer. Nada terrible, en mi opinión, sucederá, pero el sedimento permanecerá. Por lo tanto, lo haremos. El temporizador de sueño futuro se coloca en un lugar preliminar. Es tomado de allí por la rutina de interrupción de SVC. La atomicidad, por así decirlo, se logra. Esto se muestra a continuación.


 SVC_Handler LDR R0,__sysTickAddr ; SysTick  MOV R1,#6 ;   CSR ,   STR R1,[R0] ;Stop SysTimer MOV R1,#7 ; ,   STR R1,[R0] ;Start SysTimer ; STMDB SP!,{R4-R11} ;   LDR R0,=timersTable ;    LDR R1,=stacksTable ;    LDR R2,[R0] ;R2   ()  STR SP,[R1,R2,LSL #2] ;   SP (R2 * 4) LDR R3,=tmpTimersTable ;   tmpTimers LDR R3,[R3,R2,LSL #2] ;tmpTimer    STR R3,[R0,R2,LSL #2] ; timer  __svc_next_check ADD R2,R2,#1 ;   CMP R2,#TASKS_LIMIT ;R2-TASKS_LIMIT  BLO __svc_no_border_yet ;   MOV R2,#0 ;    (main) LDR R3,[R1] ; main SP MOV SP,R3 B __svc_timer_ok __svc_no_border_yet ;; LDR SP,[R1,R2,LSL #2] ;Restore SP does not work (errata Cortex M4) ;; CMP SP,#0 ; LDR R3,[R1,R2,LSL #2] ;  SP      CMP R3,#0 ; =0     BEQ __svc_next_check MOV SP,R3 LDR R3,[R0,R2,LSL #2] ;  suspend timer CBZ R3,__svc_timer_ok ; 0    ,   B __svc_next_check __svc_timer_ok STR R2,[R0] ;     LDMIA SP!,{R4-R11} ; R4-R11 BX LR 

Debe recordarse que todas estas subrutinas y manejadores de interrupciones se refieren a un área de datos determinada, que se ve realizada por el autor como se muestra en la Figura 7.


  DATA SECTION .taskSwitcher:CODE:ROOT(2) __topStack DCD sfe(CSTACK) __botStack DCD sfb(CSTACK) __dimStack DCD sizeof(CSTACK) __sysAIRCRaddr DCD 0xE000ED0C __sysTickAddr DCD 0xE000E010 __sysSHPRaddr DCD 0xE000ED18 __sysTickReload DCD RELOAD ;******************************************************************************* ; Task table for concurrent tasks (main is number 0). ;******************************************************************************* SECTION TABLE:DATA:ROOT(2) DS32 1 ;stack shift due to FPU mainCopyCONTROL DS32 1 ;Needed to determine if FPU is used mainPSRvalue DS32 1 ;Copy from main ;******************************************************************************* 

Para asegurarse de que todo lo anterior es de sentido común, el autor tuvo que escribir un pequeño proyecto en el IAR Embedded Workbench, donde logró examinar y tocar todo en detalle. Todo fue probado en el controlador STM32F303VCT6 (ARM Cortex-M4). O más bien, usando la placa STM32F3DISCOVERY. Hay suficientes LED para que cada tarea parpadee con su propio LED por separado.


Hay algunas características más que encontré útiles. Por ejemplo, una subrutina que cuenta en cada área de la pila el número de palabras no afectadas, es decir, que permanece igual a cero. Esto puede ser útil al depurar, cuando necesita verificar si llenar la pila con una tarea u otra está demasiado cerca del nivel límite.


  U32 get_task_stack_empty_space(U8 taskNum); 

Me gustaría mencionar una función más. Esta es una oportunidad para que la tarea misma encuentre su número en la lista. Puedes contarle a alguien más tarde.


 ;******************************************************************************* ; Example: U8 get_my_number(void); ;     (). ..    . ;******************************************************************************* get_my_number LDR R0,=timersTable ;    (currentTaskNumber) LDR R0,[R0] ;  BX LR ;============================================================== 

Eso es probablemente todo por el momento.

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


All Articles