Pequeños experimentos multitarea en un microcontrolador

En una de las notas anteriores, el autor trató de argumentar que al programar el microcontrolador, un simple cambio de tarea será útil en situaciones en las que usar el sistema operativo en tiempo real es demasiado, y el bucle integral para todas las acciones requeridas es demasiado pequeño ( Él dijo, al igual que el Conde de La Fer). Más precisamente, no muy poco, pero demasiado confundido.


En una nota posterior, se planeó optimizar el acceso a los recursos compartidos por varias tareas usando colas basadas en buffers de anillo (FIFO) y una tarea separada especialmente asignada para esto. Habiendo dispersado para diferentes tareas aquellas acciones que no están relacionadas entre sí, tenemos derecho a esperar un código más visible. Y si al mismo tiempo obtenemos algo de comodidad y simplicidad, ¿por qué no probarlo?


Obviamente, el microcontrolador no está diseñado para resolver ninguna tarea concebible del usuario. Entonces, tal vez, un conmutador de tareas de este tipo será suficiente en muchas situaciones. En resumen, es poco probable que un pequeño experimento duela. Por lo tanto, para no ser infundado, su humilde servidor decidió escribir algo y probar sus garabatos.


En los microcontroladores, debo decir que el requisito de contar con el tiempo como algo importante y rígido es más común que en las computadoras de uso general. Ir más allá del marco en el primer caso es equivalente a la inoperancia, y en el segundo caso, solo conduce a un aumento en el tiempo de espera, que es bastante aceptable si los nervios están en orden. Incluso hay dos términos "tiempo real suave" y "tiempo real duro".


Permítame recordarle que estábamos hablando de controladores con el núcleo Cortex-M3,4,7. Hoy es una familia muy común. En los ejemplos a continuación, utilizamos el microcontrolador STM32F303, que forma parte de la placa STM32F3DISCOVERY.


El conmutador es un archivo de ensamblador único.
El ensamblador no le tiene miedo al autor, pero, por el contrario, inspira esperanza de que se alcance la velocidad máxima.


Inicialmente, se planificó la lógica más simple de la operación del interruptor, que se presenta en la Figura 1 para ocho tareas.



En este esquema, las tareas toman su porción de tiempo una por una y solo pueden dar el resto de su tic y, si es necesario, omitir algunas de ellas. Esta lógica demostró ser buena, porque el tamaño cuántico puede hacerse pequeño. Y esto es precisamente lo que se requiere para no plantear urgentemente una tarea para la que acaba de ocurrir una interrupción, y también para plantear, y luego reducir su prioridad. El paquete que se acaba de recibir esperará silenciosamente 200-300 microsegundos hasta que su tarea reciba su tic. Y si tenemos un Cortex-M7 operando a una frecuencia de 216 MHz, entonces 20 microsegundos para un tic es bastante razonable, ya que tomará menos de medio microsegundo para cambiar. Y cualquier tarea del ejemplo anterior nunca tardará más de 140 microsegundos.


Sin embargo, con un aumento en el número de tareas, incluso con un tamaño extremadamente pequeño del tiempo cuántico, la demora en el inicio de la actividad de la tarea requerida puede dejar de ser agradable. En base a esto, y también teniendo en cuenta que solo una pequeña parte de las tareas realmente requieren un tiempo real difícil, se decidió modificar ligeramente la lógica del interruptor. Se muestra en la Figura 2.



Ahora seleccionamos solo una parte de las tareas que reciben un cuanto completo, y seleccionamos solo una marca para el resto, en el que se turnan en el juego. En este caso, la subrutina de inicialización recibe un parámetro de entrada, a saber, el número de posición, a partir del cual todas las tareas se verán afectadas en los derechos y compartirán una marca. Al mismo tiempo, el antiguo esquema permaneció disponible, para esto es suficiente establecer el valor del parámetro en cero o el número total de tareas. Los costos de cambio aumentaron con solo unas pocas instrucciones del ensamblador.


Se utilizan dos esquemas similares para permitir el acceso a recursos compartidos. El primero, que se mencionó en una nota anterior, utiliza varios FIFO (o búferes circulares por el número de productores de mensajes) y una tarea de correspondencia separada. Está diseñado para comunicarse con el mundo exterior y no requiere expectativas de tareas que generan mensajes. Solo es necesario asegurarse de que las colas no estén llenas.


El segundo esquema también utiliza una tarea separada para permitir el acceso, pero introduce expectativas porque administra el recurso interno en ambas direcciones. Estas acciones no pueden estar vinculadas al tiempo. La figura 3 muestra los componentes del segundo circuito.



Los elementos principales que contiene son un búfer de solicitudes, de acuerdo con el número de tareas deseadas, y un indicador de acceso. El funcionamiento de este diseño es bastante simple. La tarea de la izquierda envía una solicitud de acceso a un lugar especialmente asignado para ello (por ejemplo, la tarea 2 escribe 1 en la Solicitud 2). Tarea: el despachador selecciona a quién permitir y escribe el número de la tarea seleccionada en el indicador de resolución. La tarea que recibió permiso realiza sus acciones y escribe el signo del final del acceso a la solicitud, el valor 0xFF. El planificador, al ver que la solicitud se borra, restablece el indicador de permiso, restablece la solicitud anterior y restablece la solicitud de otra tarea.


Aquí se pueden ver dos proyectos de prueba bajo IAR y una descripción de la placa STM32F3DISCOVERY utilizada. En el primer proyecto, el ATS303 simplemente verificó su rendimiento y lo depuró. Todos los LED instalados en esta placa fueron útiles. Nadie resultó herido.


El segundo borrador de BTS303 probó las dos opciones de asignación de recursos mencionadas. En él, las tareas 1 y 2 generan mensajes de prueba que son recibidos por el operador. Para comunicarme con el operador, tuve que agregar una bufanda con un puerto COM TTL, como se muestra en la foto a continuación.



El operador usa un emulador de terminal. Creo que el lector disculpará al autor por el color suave del tubo. Se ve así.



Para iniciar todo el sistema, antes de resolver las interrupciones, se requieren pasos preliminares en el cuerpo de la tarea cero main (), que se presentan a continuación.


void main_start_task_switcher(U8 border); U8 task_run_and_return_task_number((U32)t1_task); U8 task_run_and_return_task_number((U32)t2_task); U8 task_run_and_return_task_number((U32)t3_human_link); U8 task_run_and_return_task_number((U32)t4_human_answer); U8 task_run_and_return_task_number((U32)task_5); U8 task_run_and_return_task_number((U32)task_6); U8 task_run_and_return_task_number((U32)task_7); 

En estas líneas, el interruptor comienza primero y luego, a su vez, las siete tareas restantes.


Aquí está el conjunto mínimo de llamadas necesarias para el trabajo.


  void task_wake_up_action(U8 taskNumber); 

Esta llamada se utiliza en una interrupción de un temporizador de hardware del usuario. Los desafíos de las tareas mismas hablan por sí mismos.


  void release_me_and_set_sleep_steps(U32 ticks); U8 get_my_number(void); 

Todas estas funciones están en el archivo del conmutador ensamblador. Hay varias funciones más que son útiles para las pruebas, pero que no son necesarias.


En el proyecto BTS303, la tarea 3 recibe comandos del operador desde el exterior y le envía las respuestas que provienen de la tarea 4. La tarea 4 recibe los comandos del operador de la tarea 3 y los ejecuta con posibles respuestas. La tarea 3 también recibe mensajes de las tareas 1 y 2 y la envía a través de UART al emulador de terminal (por ejemplo, masilla).


La tarea 0 (principal) realiza un trabajo auxiliar, por ejemplo, verifica el número de palabras que no se ven afectadas en el área apilada de cada tarea. El operador puede solicitar esta información y tener una idea del uso de la pila. Inicialmente, para cada tarea, se asigna un área de pila de 512 bytes (128 palabras) y es necesario monitorear (al menos en la etapa de depuración) que estas áreas no se acercan al desbordamiento.


Las tareas 5 y 6 hacen cálculos en alguna variable de coma flotante común. Para hacer esto, solicitan acceso desde la tarea 7.


Hay otra característica adicional que se puede ver en los proyectos de prueba. Está diseñado para que pueda despertar la tarea no después de que haya expirado el número de tics, sino después de un tiempo específico, y se ve así.


  void wake_me_up_after_milliSeconds(U32 timeMS); 

Para su implementación, también se requiere un temporizador de hardware adicional, que también se implementa en casos de prueba.


Como puede ver, la lista de todas las llamadas necesarias cabe en una página.

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


All Articles