Una cosa que he encontrado útil en varias máquinas es un simple conmutador de pila. En realidad no he escrito uno para el PIC, pero supongo que el enfoque funcionaría bien en el PIC18 si ambos/todos los hilos utilizan un total de 31 o menos niveles de pila. En el 8051, la rutina principal es:
\_taskswitch:
xch a,SP
xch a,\_altSP
xch a,SP
ret
En el PIC, olvido el nombre del puntero de la pila, pero la rutina sería algo así:
\_taskswitch:
movlb \_altSP >> 8
movf \_altSP ,w,b
movff \_STKPTR,altSP
movwf \_STKPTR,c
return
Al comienzo de tu programa, llama a una rutina task2() que carga altSP con la dirección de la pila alternativa (16 probablemente funcionaría bien para un PIC18Fxx) y ejecuta el bucle task2; esta rutina no debe regresar nunca o de lo contrario las cosas morirán de forma dolorosa. En su lugar, debería llamar a _taskswitch cada vez que quiera ceder el control a la tarea primaria; la tarea primaria debería entonces llamar a _taskswitch cada vez que quiera ceder el control a la tarea secundaria. A menudo, uno tendrá pequeñas rutinas como:
void delay\_t1(unsigned short val)
{
do
taskswitch();
while((unsigned short)(millisecond\_clock - val) > 0xFF00);
}
Tenga en cuenta que el conmutador de tareas no tiene ningún medio para hacer ninguna "condición de espera"; todo lo que soporta es un spinwait. Por otro lado, el conmutador de tareas es tan rápido que un intento de taskswitch() mientras la otra tarea está esperando a que expire un temporizador cambiará a la otra tarea, comprobará el temporizador y volverá a cambiar más rápido de lo que un conmutador de tareas típico determinaría que no necesita cambiar de tarea.
Hay que tener en cuenta que la multitarea cooperativa tiene algunas limitaciones, pero evita la necesidad de un montón de bloqueos y otro código relacionado con los mutex en los casos en los que las invariantes que se alteran temporalmente pueden restablecerse rápidamente.
(Editar): Un par de advertencias respecto a las variables automáticas y demás:
- si una rutina que utiliza la conmutación de tareas es llamada desde ambos hilos, generalmente será necesario compilar dos copias de la rutina (posiblemente #incluyendo el mismo archivo fuente dos veces, con diferentes declaraciones #define). Cualquier archivo fuente contendrá código para un solo subproceso, o bien contendrá código que se compilará dos veces -una para cada subproceso- por lo que puedo utilizar macros como "#define delay(x) delay_t1(x)" o #define delay(x) delay_tx(x)" dependiendo del subproceso que esté utilizando.
- Creo que los compiladores de PIC que no pueden "ver" una función que está siendo llamada asumirán que dicha función puede destrozar todos y cada uno de los registros de la CPU, evitando así la necesidad de guardar cualquier registro en la rutina de cambio de tarea [un buen beneficio comparado con la multitarea preventiva]. Cualquiera que considere un conmutador de tareas similar para cualquier otra CPU necesita ser consciente de las convenciones de registro en uso. Empujar los registros antes de un cambio de tarea y abrirlos después es una manera fácil de ocuparse de las cosas, asumiendo que existe un espacio adecuado en la pila.
La multitarea cooperativa no permite escapar completamente de los problemas de bloqueo y demás, pero realmente simplifica mucho las cosas. En un RTOS preventivo con un recolector de basura compactado, por ejemplo, es necesario permitir que los objetos se fijen. Cuando se utiliza un conmutador cooperativo, esto no es necesario siempre que el código asuma que los objetos del GC pueden moverse en cualquier momento que se llame a taskswitch(). Un recolector de compactación que no tenga que preocuparse por los objetos anclados puede ser mucho más sencillo que uno que sí lo haga.