Archivo por meses: abril 2021

Pintando en el Spectrum (15)

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: