En la última entrada hablé del gestor de tareas. Sin embargo, desde entonces la cosa ha evolucionado mucho y el código actualmente es algo diferente. En concreto, es este:
old_stack: DEFW 0 task_pointer: DEFW 0 task_run: ld (old_stack), SP ld iy, task_list task_loop: ld a, (iy+0) and a jr z, task_end ld l, (iy+1) ld h, (iy+2) ld sp, hl ld (task_pointer), iy ret task_end: ld sp, (old_stack) ret task_yield: ld iy, (task_pointer) ld hl, 0 add hl, sp ; get the current stack ld (iy+1), l ld (iy+2), h ld e, (iy+0) ld d, 0 add iy, de ; jump to next entry jp task_loop task_list: TASK_ENTRY 36, task1 TASK_ENTRY 10, task2 TASK_ENTRY 14, task3 DEFB 0 ; fin de la lista de tareas
El primer cambio es que ahora guardo el valor de IY, de manera que puede modificarse dentro de la tarea sin problemas. El segundo es que ahora, cada tarea puede tener un tamaño de pila diferente, lo que permite ahorrar mucha memoria, pues hay tareas que casi no hacen llamadas anidadas, mientras que otras sí.
El segundo cambio fue crear una macro que simplifica crear la tabla de tareas. Al final, en la etiqueta task_list, tengo una lista de tareas de ejemplo con un total de tres tasks, donde el código de la primera empieza en task1, el de la segunda en task2, y el de la tercera en task3, y sus stacks respectivos tienen un tamaño de 36, 10 y 14 bytes. TASK_ENTRY es una macro con el siguiente formato:
MACRO TASK_ENTRY, STACK_SIZE, TASK_FUNCTION DEFB STACK_SIZE+3 DEFW $+STACK_SIZE DEFS STACK_SIZE-2 DEFW TASK_FUNCTION ENDM
El primer byte indica el tamaño completo de la entrada, que será de tres bytes más que el tamaño de pila que queremos asignar a esta task. Los dos siguientes bytes contienen el valor del registro SP de esta tarea, o sea, el actual puntero de pila. Se inicializa de manera que apunte al final del bloque reservado para la pila. Luego vienen tantos bytes como tamaño de pila queramos menos 2, de manera que reservamos los dos últimos bytes para poner la dirección de inicio de la tarea. De esta manera, cuando se crea la entrada para una tarea, la pila sólo contiene la dirección de inicio de dicha tarea, y el puntero de pila apunta justo a ella.
Por último, añadí una función optativa para depurar las tareas. Es ésta:
ld hl, (task_pointer) inc hl inc hl inc hl ; jump over the size and the stored SP pointer ld d, 0 task_check_size: ld a, (hl) and a jr nz, task_end_check_size inc d inc hl jr task_check_size task_end_check_size: ld a, d ld SP, (old_stack) call do_debug
Este bloque se puede ejecutar justo después de que una tarea haya vuelto (llamando a task_yield), y lo que hace es comprobar cuanto espacio se ha llegado a ocupar en la pila. Para ello se basa en que el bloque de la pila se inicializó a cero, y que cuando se saca un valor de la pila sólo se incrementa SP, pero el valor sigue estando ahí. De esta manera, si vemos cuantos bytes seguidos cuyo valor sea cero hay desde el principio, sabremos hasta donde ha llegado a crecer la pila. Si este tamaño es cero significa que esa pila se ha llenado del todo en algún momento (y hasta es posible que haya sobreescrito alguna posición extra), por lo que en ese caso hay que aumentar el tamaño.
La llamada a CALL do_debug se supone que muestra, de alguna manera, el contenido del registro A.
Qué pinta tiene
Pues el juego ya lo he terminado y ahora mismo estoy puliendo los textos y la traducción al inglés, y los petatesters están buscando los bugs y pifias varias. Pero tiene esta pinta: