Principio de multitarea. Desterrando a los delays.

Dr. Zoidberg

Well-known-Papá Pitufo
Particularmente, de lo mostrado hasta ahora, no estoy de acuerdo con lo de los bucles infinitos (#use timer también pueden funcionar en modo ISR), pero es porque siempre intento que el chip consuma lo menos posible.
Finalmente, todo depende de la aplicacion destino.
Los lazos infinitos son imprescindibles en cualquier app con microcontroladores ya que su ejecucion nunca finaliza, por que no hay un S.O. que tome el control cuando el micro no hace nada (y en este caso el loop lo haria el S.O.).
Si usas el modo ISR de la #use timer, entonces vas a tener que hacer un loop infinito esperando que ocurra la interrupcion y quedamos igual que antes. YO prefiero usar un loop que haga algo y dejar la isr lo mas reducida posible, y si tengo que despachar tareas temporizadas y no-criticas, entonces lo hago fuera de la isr.

De todas formas, a mi no me termina de gustar esta forma de trabajo por que me "queda incompleta". YO haria polling sobre una cola de eventos y despacharia las tareas basado en los eventos presentes en la cola. Esto permitiria ampliar las carateristicas del despacho de tareas, incluyendo otras que nada tienen que ver con el tiempo. Pero claro, los costos de memoria y de otros recursos son mayores que en este simple caso.
 
Es que no estas llevando varios temporizadores, sino uno solo sobre el cual se despachan todas las tareas.
Esto no es, ni por cerca, una implementacion hard-real-time pero tampoco lo son los multiples temporizadores, donde el polling de los bits de estado es un equivalente a get_ticks.
Entiendo que este tema pretende proporcionar una metodologia mas o menos simple y estandarizada para sincronizar tareas sobre un timer y asi eliminar los delays....pero puedo estar equivocado.

No me expresé bien, me refería mas o menos a eso. get_ticks solo incrementa un contador y eso obliga a verificar el evento haciendo cuentas con cada uno de los temporizadores, cuando lo sensato es que en cada tic se actualicen los contadores y en el bucle se chequee un bit de estado.

De multitarea coincido en que esto no tiene nada. Un multitarea consiste en ir saltando de un proceso a otro por interrupción junto a funciones como lanzar nuevos procesos, interrumpirlos, delays consistentes mientras en ejecutar los demas procesos en lugar clavarse ahí, y obviamente mas pero que en microcontroladores solo tienen sentido implementar algunas.

-----------------------------------
Joaquin Ferrero:
Particularmente, de lo mostrado hasta ahora, no estoy de acuerdo con lo de los bucles infinitos (#use timer también pueden funcionar en modo ISR), pero es porque siempre intento que el chip consuma lo menos posible.

No estoy seguro si referís a hacer el salto a las tareas dentro de la interrupción.
Si es así no lo veo conveniente porque si la tarea por detrás es consumidora de tiempo (como ser evaluar expresiones con funciones trigonométricas) te puede hacer perder la llamada a procesos de alta frecuencia.
 
M

Miembro eliminado 356005

Los lazos infinitos son imprescindibles en cualquier app con microcontroladores ya que su ejecucion nunca finaliza, por que no hay un S.O. que tome el control cuando el micro no hace nada (y en este caso el loop lo haria el S.O.).
Si usas el modo ISR de la #use timer, entonces vas a tener que hacer un loop infinito esperando que ocurra la interrupcion y quedamos igual que antes.
Bueno, sí, son imprescindibles porque la CPU corre continuamente, pero... de lo que estoy hablando es que la CPU se pare cuando no tenga nada que hacer.

Casi todos los microcontroladores disponen de un modo de bajo consumo, con una o varias instrucciones SLEEP, que pone el microcontrolador en modo bajo consumo, o directamente para el reloj interno, dejando solo en funcionamiento las interrupciones que llegan del exterior o las interrupciones propias de los temporizadores.

En uno de mis últimos proyectos con un ATtiny85, el bucle principal era un

while(1) {
sleep();
}


O sea, no hace nada más que dormir. La gestión de una entrada, el reloj interno, generación de un breve pitido de aviso, iluminación momentánea de un LED... todo se hace con interrupciones.

Esto no es nada raro incluso en monstruos como Linux: cuando no tienen nada que hacer, el procesador ejecuta un proceso para dormir los procesadores o para reducir su velocidad.

Eduardo dijo:
Si es así no lo veo conveniente porque si la tarea por detrás es consumidora de tiempo (como ser evaluar expresiones con funciones trigonométricas) te puede hacer perder la llamada a procesos de alta frecuencia.
Entonces hablamos de tareas apropiativas (se apropian de la CPU y no la sueltan hasta que terminan). Al final, depende de cómo sean las tareas y la precisión que necesitemos en cuanto al inicio de las tareas. A lo que me refería era a lo que le he respondido a Zoidberg antes, sobre lo de intentar reducir el bucle principal a la nada, y si es posible, solo dormir.

Esto parece exagerado, pero hay muchas aplicaciones para correr con baterías, en sitios aislados de una alimentación general.
 
........
Entonces hablamos de tareas apropiativas (se apropian de la CPU y no la sueltan hasta que terminan). Al final, depende de cómo sean las tareas y la precisión que necesitemos en cuanto al inicio de las tareas. A lo que me refería era a lo que le he respondido a Zoidberg antes, sobre lo de intentar reducir el bucle principal a la nada, y si es posible, solo dormir.
Esto parece exagerado, pero hay muchas aplicaciones para correr con baterías, en sitios aislados de una alimentación general.

Pienso igual, solo que no al extremo de vaciar completamente el bucle principal porque la administración de las tareas igual se tienen que hacer en algún lado.
Prefiero hacer:

while(1) {
...........
chequeo de estados, pines y llamado a tareas
...........
sleep();
}
 

Dr. Zoidberg

Well-known-Papá Pitufo
Ahhh...bueno....el contexto lo es todo.
Si necesitas reducir el consumo por el motivo que sea, no vale el esquema de saint tal como esta, pero nada impide cambiar el ritmo de los ticks a unp que sea comun denominador de todas las tareas y que despierte a la cpu cada mas tiempo...si no hay nada que hacer, dormimos de nuevo.
Esto ya es muy dependiente de la aplicacion y se puede, dentro de ciertos limites, utilizar la metodologia de saint. Fuera de esos limites habra que usar otras tecnicas.

Ooopppssss...ya contesto Eduardo.
 

D@rkbytes

Moderador
RTOS va muy bien para microcontroladores PIC de gran memoria, consume demasiada RAM (Flash) y ROM.
Es inimaginable llevar un proyecto grande con RTOS para un PIC promedio de 1024 o 2048 bytes.
Con un simple proceso de hacer destellar LEDS's, ya uno se puede ir dando cuenta del gran consumo de RAM.

Con esto no quiero decir que no sirva, pues es obvio que es una alternativa para crear un sistema pseudo multitarea.
De hecho, lo uso bastante en algunos proyectos y siempre trato de elegir qué microcontrolador PIC se ajusta a lo que necesito en cuanto a memoria y pines.
Yo pienso que si no se ha difundido mucho este tema, es precisamente por el hecho del consumo de RAM.
Aparte porque las personas que recién inician en este mundo de los microcontroladores, ya lo hacen sin ningún tipo de preparación académica.
Internet les ha abierto las puertas a muchas personas sin conocimientos, ni de electrónica básica, ni de electrónica digital.
Así que ahora cualquiera puede programar un microcontrolador siguiendo instrucciones de otra persona que posiblemente tampoco sabe nada pero que también por ahí leyó algún "tutorial".
Entonces, este tipo de programación ya les parecerá complejo y simplemente se dedicarán a copiar y por consecuencia a seguir creando programas con un Copy/Paste.
Por eso existen los temas de: ¿Cómo elevar un voltaje de 5 V. a 12 V.?
¿Qué pasa si mi fuente de poder es de 10 A. y mi circuito apenas consume 100 mA? O preguntas similares.
Y esto ya es tan común que realmente alarma. (Al menos a mi, sí me preocupa)

Ahora, retomando el tema que va de la mano sobre lo citado, veo que cada quien opina diferente, cuando la realidad es llegar al objetivo.
Yo aprendí sobre este proceso leyendo la ayuda del compilador y viendo sus ejemplos, que por cierto, son varios.
Si uno, como programador del entorno, los ve, encontrará que la historia ya está escrita.
 
En el programa y agregando tareas.c al proyecto se protege el mecanismo de despacho de modificaciones involuntarias..o nó.
Aunque no estaba pensado ser implementar como una librería me parece buena idea.
Entiendo que este tema pretende proporcionar una metodologia mas o menos simple y estandarizada para sincronizar tareas sobre un timer y asi eliminar los delays....pero puedo estar equivocado.
Estas en lo cierto, el objetivo es mostrar que mucho esfuerzo se puede hacer que el microcontrolador (en este caso PIC gama 16) puede efectuar varias tareas.
YO haria polling sobre una cola de eventos y despacharia las tareas basado en los eventos presentes en la cola. Esto permitiria ampliar las carateristicas del despacho de tareas, incluyendo otras que nada tienen que ver con el tiempo. Pero claro, los costos de memoria y de otros recursos son mayores que en este simple caso.
De todos modos sigue siendo una metodología que sea accesible y relativamente fácil de implementar. De otro modo el título del post seria “implementando un RTOS”, CCS ya tiene uno que es cooperativo así que tampoco lo haría.
De multitarea coincido en que esto no tiene nada. Un multitarea consiste en ir saltando de un proceso a otro por interrupción junto a funciones como lanzar nuevos procesos, interrumpirlos, delays consistentes mientras en ejecutar los demas procesos en lugar clavarse ahí, y obviamente mas pero que en microcontroladores solo tienen sentido implementar algunas.
En el caso de un multitarea real tendríamos que usar un uC de varios núcleos como el propiller o de plano hacer trabajar a varios uC de manera sincronizada, pero en este segundo caso para que usar varios uC cuando uno solo podría realizar todo.

Sobre los modos de bajo consumo y su uso dependerá del programador.
 

Dr. Zoidberg

Well-known-Papá Pitufo
En el caso de un multitarea real tendríamos que usar un uC de varios núcleos como el propiller o de plano hacer trabajar a varios uC de manera sincronizada, pero en este segundo caso para que usar varios uC cuando uno solo podría realizar todo.
Lo que normalmente se refiere como multitarea en un solo nucleo es lo que se denomina "time slicing", que consiste en un despachador preemptivo que asigna pequeños tiempos de ejecucion a cada tarea y conmuta entre ellas cuando se les cumple el plazo o cuando si inicia una operacion I/O bloqueante.
Claro que esto implica un rediseño cpmpleto de la base de software disponible para estos micros y es algo que casi no vale la pena en un dispositivo de pocos recursos de CPU y memoria.
 

Dr. Zoidberg

Well-known-Papá Pitufo
Ambos son multitarea, solo que en el cooperativo las decisiones las toma el "programador" y la eficiencia queda librada a sus decisiones, mientras que en el preemptivo las decisiones las toma el despachador basado en alguna política dependiente del objetivo del multitarea (no es lo mismo el despachador de Linux estándar que el despachador de Real Time Linux, por ejemplo).
Otra diferencia mas simple de analizar es que en el cooperativo el despachador corre en el mismo espacio de prioridades que las tareas de usuario, mientras que en el preemptivo el despachador corre con prioridad "suprema"... superior a cualquier tarea de usuario. Esto ultimo no es muy simple de lograr en un microcontrolador tipo PIC por que no tiene idea de niveles de prioridad de ejecución (por ejemplo, lo que sería el Ring 0 y el Ring 3 en los chips X86) y cualquier programador puede desahbilitar las interrupciones impunemente y decir adios al despachador.

Digamos... ambos son multitarea, pero para objetivos diferentes.
 
....
En el caso de un multitarea real tendríamos que usar un uC de varios núcleos como el propiller o de plano hacer trabajar a varios uC de manera sincronizada, pero en este segundo caso para que usar varios uC cuando uno solo podría realizar todo.

Eso no sería multitarea sino procesamiento paralelo. Parece lo mismo, pero no lo es.

En una versión mínima de multitarea, se debe guardar el estado y los registros internos del proceso interrumpido para luego cargar los del entrante --> esto obliga a necesitar stacks independientes para cada proceso mas un buffer LIFO con punteros a los stacks.
El problema de implementarlo en un PIC comunardo no es tanto la memoria sino que el stack es interno y de pocos niveles. Si se cortó un proceso en medio de una subrutina y los otros llaman o retornan de subrutinas van a saltar a donde no quieren.

Acá la discusión apunta a lo inapropiado de "multitarea" en el título no a las ventajas de reemplazar el delay() por algo basado en interrupciones. Con lo que estoy plenamente de acuerdo.

Sobre los modos de bajo consumo y su uso dependerá del programador.

Depende de la aplicación. El programador puede elegir como, pero como formas eficientes no tiene tantas para elegir...
 

Dr. Zoidberg

Well-known-Papá Pitufo
Si uno, como programador del entorno, los ve, encontrará que la historia ya está escrita.
Estoy de acuerdo que la historia ya está escrita, al menos para CCS, pero desconozco los otro entornos, de los que hay varios para desarrollo en PICs. Lo propuesto en este tema - creo yo - podría ser de aplicacion no solo a los PICs sino a cualquier micro de "nivel" parecido, tales como los ATMega de los Arduino...o los MSP de Texas. En todos los casos, si no se tiene la directiva #use timer, pues es muy simple configurar un timer para interrumpir cada cierto tiempo, colgar una ISR que incremente un contador y armar una función que devuelva el estado de la cuenta....y lo demás sigue igual.
Es mas, con un par de archivos como los anteriores, con directivas de compilación condicional, se puede incluir el código para todas las plataformas que se quiera...
 
M

Miembro eliminado 356005

Multitarea
«La multitarea es la característica de los sistemas operativos modernos de permitir que varios procesos o aplicaciones se ejecuten aparentemente al mismo tiempo, compartiendo uno o más procesadores».

Tipos de multitarea
  • Cooperativa: El sistema operativo da el control a un proceso, y es este el que cede de nuevo el control pasando a estar en espera cuando decide voluntariamente que no puede seguir su ejecución.
  • Apropiativa o preferente: El sistema operativo es el encargado de administrar el/los procesador(es) repartiendo el tiempo de uso entre los procesos que estén esperando para utilizarlo.
  • Real: Solo se da en sistemas con multiprocesador; varios procesos se ejecutan realmente al mismo tiempo en distintos microprocesadores.
 
Digamos... ambos son multitarea, pero para objetivos diferentes.
Eso mismo pensé
En una versión mínima de multitarea, se debe guardar el estado y los registros internos del proceso interrumpido para luego cargar los del entrante --> esto obliga a necesitar stacks independientes para cada proceso mas un buffer LIFO con punteros a los stacks.

Esto significa que si se tiene un programa donde se tiene varias tareas ejecutándose no es multitarea y que solamente es multitarea sí obligatoriamente hay que guardar estados y pilas y punteros a pilas, etc.

Cuando en los ejemplos que subí una tarea se apropia de la CPU, termina su pequeña operación cambiando de estado un led y luego retornar al despachador de tareas para que este pueda lanzar a las otras tareas…aparentando simultaneidad ¿no es multitarea cooperativo?

Acá la discusión apunta a lo inapropiado de "multitarea"
Si esto es cierto y está fundamentado les pido a los moderadores que cambien el título y con eso se acaba la discusión.

Por otro lado.
Estoy de acuerdo que la historia ya está escrita, al menos para CCS, pero desconozco los otro entornos, de los que hay varios para desarrollo en PICs. Lo propuesto en este tema - creo yo - podría ser de aplicacion no solo a los PICs sino a cualquier micro de "nivel" parecido, tales como los ATMega de los Arduino...o los MSP de Texas. En todos los casos, si no se tiene la directiva #use timer, pues es muy simple configurar un timer para interrumpir cada cierto tiempo, colgar una ISR que incremente un contador y armar una función que devuelva el estado de la cuenta....y lo demás sigue igual.
Es mas, con un par de archivos como los anteriores, con directivas de compilación condicional, se puede incluir el código para todas las plataformas que se quiera...
...Le diste en el clavo

Mientras voy trabajando en la siguiente entrega
 
Última edición:

Dr. Zoidberg

Well-known-Papá Pitufo
Si esto es cierto y está fundamentado les pido a los moderadores que cambien el título y con eso se acaba la discusión.
Es que no sé si es taaaan inapropiado el nombre "multitarea", ya que podés pensar que están todas las tareas "esperando simultáneamente" que se cumpla su tiempo de ejecución y entonces - la que le toca - se apropia de la CPU hasta que termina...mientras la otras "siguen esperando".
No es muy diferente a lo que sucedería con un despachador preemptivo, solo que este - además - corta la ejecución de la tarea que tiene la CPU si se le acaba el plazo asignado.
 
M

Miembro eliminado 356005

Sí que es multitarea porque, de cara al usuario, da la sensación de que se realizan varias tareas de forma simultánea. En concreto, cooperativa, ya que las tareas ceden el control al despachador una vez que terminan.

En mi mensaje #33 he puesto las definiciones, y ahí no se especifica si hay cambios de contexto, registros, varias CPU... El concepto es más general que su implantación física.
 
Para aclara un poco la idea de los ejemplos que subí.

En los ejemplos anteriores una tarea se apropia de la CPU, termina sus acciones y retorna al despachador de tareas. Esto puede generar la impresión de que si una tarea hace varias cosas entre esas tener que esperar a que por ejemplo llegue un dato del puerto serie y solo cuando haya terminado todas sus acciones recién retorna al despachador de tareas haciendo que el resto de tareas esperen “las ganas” de la que está ocupando la CPU.

Bueno esa no es la idea. La idea es dividir las tareas de modo que se tengan estados que permitan retornar al despachador de tareas aunque no hayan terminado todas sus acciones y luego de un tiempo (cuando le vuelva a tocar el turno) retornar a donde se quedó para continuar lo que le queda pendiente… pero esto será recién planteado en las siguientes entregas.
 

Dr. Zoidberg

Well-known-Papá Pitufo
La idea es dividir las tareas de modo que se tengan estados que permitan retornar al despachador de tareas aunque no hayan terminado todas sus acciones y luego de un tiempo (cuando le vuelva a tocar el turno) retornar a donde se quedó para continuar lo que le queda pendiente
A eso me referia en mis primeros mensajes cuando hablaba de una pequeña API para liberar la CPU cuando fuera necesario.
Para eso existe una primitiva denominada yield que cumple precisamente esa tarea: devuelve el control al despachador hasta que este decida retornarselo. Ojo que aca la politica de retorno a la tarea no nececesariamente tiene que ver con el tiempo, y eso exige modificar un poco el despachador.
 
En aplicaciones sencillas basta con no poner bucles que esperen a que pasen cosas y hacer estas funciones verificando flags, activar interrupciones siempre que se pueda, no poner delays etc.

Hay que programar de forma que se quede todo a medias y en la siguiente pasada se siga por dónde se quedó. Haciendo eso sencillamente puedes poner una tarea tras otra en el bucle principal.
Si alguna tiene más prioridad la llamas más veces:
Loop{
Tarea1
Tarea2
Tarea1
Tarea3
}
Aquí la tarea 1 se ejecuta el doble de veces que las otras
Y se ejecutan de forma "asíncrona" osea siempre que se pueda, no sé si cada ms o cada cuando sea.
Si hay una tarea que tenga que ser "síncrona" esa se cuelga de la interrupcion del timer.
Para saber lo que tardan las tareas puedes añadir unos pulsos por un pin y medirlos en un osciloscopio
Loop{
Tarea1
Pulso
Tarea2
Pulso
Tarea1
Pulso
Tarea3
Pulso
}
La pega es que programar las rutinas así obliga a pensar de otro modo, usar flags para indicar si está activo o no algo, contadores etc.

Por ejemplo, en un sistema de control de tráfico, entre otras cosas se verificaba que las salidas a triacs estuviesen en el estado en el que se había programado leyendo por el lado de los 230V que había tensión o no y en caso de no coincidir marcar una avería o apagar el cruce. Además se verificaba la corriente en las luces rojas. Como en los pasos por cero etc no coincide lo programado con lo leído y eso no es avería, la programación clásica haría un bucle en una salida verificando que lo escrito coincida con lo leído y daría un tiempo para que coincidiese.
Yo verificaba las treinta señales una sola vez por pasada y aumentaba o disminuía un contador por cada luz según si estaba "bien" o "mal". Al cabo de X fallos consecutivos se disparaba la alarma, este valor de sensibilidad era programable. La rutina no tenía ningún bucle, comprobaba en cada pasada las treinta señales se le llamaba siempre que se podía digamos que siempre que se volvía al main estabas verificando sin parar. Era ensamblador, no era C.
El resto de rutinas era algo semejante, cada vez que se pulsaba una tecla se metía su valor en un buffer, se actualizaba el display y se seguía. Solo al pulsar intro se analizaba el contenido del buffer y se ejecutaba la acción, y se seguía...
La parte de los tiempos sí que pendía de la interrupción del timer. El resto se ejecutaba sin parar en un bucle, activando en watchdog de paso.

Otra opción que se me ha ocurrido es editar la función delay dentro poner código que lea el teclado o lo que sea necesario, así cada vez que esperas mantienes el sistema en lugar de fundir ciclos.
 
Última edición:

Dr. Zoidberg

Well-known-Papá Pitufo
@Scooter
Lo "malo" de tu propuesta es el lio de codigo que se produce y la dificultad de mantenimiento del programa... Y ni hablar si ademas va en assembler.
De esta otra forma es posible aislar las tareas entre si usando funciones independientes para cada una de ellas. Incluso es muy simple modificar el despachador para que ejecute una tarea determinada cuando no hay otra cosa por hacer, de forma tal que se ejecute asincronicamente con el timer...atada a ciertas restricciones, claro.

Si basaramos el despachador en una cola de mensajes la potencia de la solucion escala de manera importante, por que ahora las interrupciones de los perifericos pueden realizar la tarea necesaria y mandar un mensaje que advierta al despachador que ocurrio la interrupcion, que fue procesada y que los datos estan disponibles para que la tarea destino los utilice, de manera tal que pueda despachar la tarea asociada que hasta ese instante estaba "esperando".
 
Última edición:
Arriba