Ufff... Costó, pero creo que lo logré. Pude grabar el Goonies-130XE de 72.670 bytes en un .CAS usando SITRE modificado en el emulador Atari800-a8cas con 128K de RAM y posteriormente lo cargué en Altirra con 320K. Va el adjunto para que lo prueben. Si alguien lo puede pasar a WAV y cargar en un Atari 130XE real o en 800XL con 256K, sería genial que compartiera la experiencia. Pero le advierto que son 18 minutos de carga!!!
Grabando:
Cargando:
Después del pito lento, se instala la presentación propia:
Forzando un error moviendo el control del .CAS un poco más adelante de donde iba:
Carga finalizada:
¿Y cómo logré esto? Vamos a lo técnico...
Hasta ahora no me había referido a los
internals de SITRE, pero llegó el momento, y voy a comenzar con el cargador (
loader), pues es lo primero que modifiqué.
El cargador de SITRE consta de 4 partes que se graban a cinta antes que el XEX:
- Boot record: Es un registro de formato estándar que no tiene código a ejecutar, sino que sólo se usa para cargar en memoria los vectores de ejecución de SITRE. Si alguien lee la cinta usando un copiador en BASIC, lo que verá es sólo un mensaje de relleno indicando que se trata de SITRE. Pude haber puesto ahí también el nombre del juego grabado, pero ya no fue.
- Registro EOF (falso): En cassette se requiere un bloque extra al final de la grabación. Normalmente ese bloque está vacío, pero en el caso de SITRE va la rutina de inicialización, la cual queda en el buffer de cassette estándar en memoria. Esta rutina carga el 3er bloque y le pasa el control.
- Primera etapa de carga turbo: Este bloque carga en página 6 y ahí está contenida la pantalla de característica de SITRE. Si el XEX que se cargue tiene su propia pantalla de presentación (usualmente son los juegos que requieren pitos lentos), la de SITRE será reemplazada, incluso si carga en la misma página 6. Este bloque tiene además la rutina que carga el 4to bloque y principal de SITRE.
- Segunda etapa turbo: Este bloque entra en página 7 y contiene el cargador de SITRE. Aquí está el código para la carga del XEX, incluyendo las rutinas de carga de bloques, de su validación y reintentos. También contiene la pantalla de reintentos en caso de error y el mensaje "quedan XXX bloques" que se muestra además en la pantalla cargada en la primera etapa; el mensaje va aquí por seguridad, pues si un XEX carga código en página 6, SITRE podría corromper código válido al decrementar el contador.
A continuación van los bloques de XEX, los cuales tienen la siguiente estructura:
- 2 bytes para ajuste de velocidad (BAUD). Son constantes y ambos tienen el valor $55 (en binario son unos y ceros alternados).
- 1 byte para indicar el número del bloque. En SITRE, los números van en orden decreciente, siendo el bloque 0 el último en cargar. De esta forma se permitía cargar hasta 256 bloques.
- 1 byte para indicar la cantidad de bytes en el bloque. Esto fue pensado para permitir la carga de bloques parciales y facilitar la funcionalidad de pitos lentos, pero finalmente no fue necesario, excepto para el último bloque que sí puede traer pocos bytes. A diferencia de otros cargadores que he visto, no se requiere de bytes de control adicionales para indicar el término del XEX.
- 256 bytes de datos. Los bytes que no se usan a la cola (indicado por el byte anterior) son considerados basura.
- 1 byte de checksum, puesto por la rutina SIO del sistema al momento de grabar y controlado también por SIO al momento de leer el bloque. Si no calzan, se activa la pantalla con efecto rainbow solicitando rebobinar para reintentar.
Finalmente, en la cinta se graba el registro EOF real, a velocidad normal (no turbo) y que fue simulado en el segundo bloque al grabar. Este bloque no es leído por SITRE, pues la cinta es detenida justo antes para pasarle el control al XEX.
Dado todo lo anterior, se puede observar que hay una restricción en el tamaño máximo del XEX: 256 bloques por 256 bytes permiten 65.536 bytes (64K). Por lo tanto, para poder cargar XEX de mayor tamaño, se requerió modificar el
loader. Pensé en incorporar 1 byte adicional para el contador de bloques en la data del XEX o en reemplazar la funcionalidad del byte del tamaño del bloque (como sólo es relevante en el último bloque, se podíra almacenar por separado junto al loader), pero como quería tocar lo menos posible el código original, me aproveché de las características del álgebra binaria en complemento a 2 y no fue necesario modificar los bloques del XEX. Simplemente comparé un registro de 8 bits con el LSB de uno de 16 bits. Este cambio agregó unos pocos bytes al 4to segmento, pero eso impactó al 3ro, pues se corrieron algunos vectores, lo que de paso afectó al 2do. El 1ro no se salvó porque ahí iba la fecha de la versión de SITRE que tenía el bug Y2K, así que la cambié por la fecha de hoy.
Pero para poder llevar el cargador a cinta, fue necesario modificar el copiador. Mi intensión era modificar lo menos posible y mantener la concepción origina de SITRE, pero creo que debo haber cambiado como un tercio del programa BASIC. Si bien no modifiqué la estructura y conserve el orden, los nombres de las variables y hasta los números de línea, varias cosas debieron ser cambiadas radicalmente.
Detalles técnicos del por qué en el spoiler...
La memoria de un 130XE o un 800XL modificado tiene más o menos la siguiente estructura:
- $0000 - $06FF: casi 2K para registros del HW, de BASIC y de DOS. Algunos bytes en página cero y la página 6 completa pueden ser usados por los programas como SITRE.
- $0700 - $1FFF: Poco más 6K utilizados por el DOS. El límite superior es variable y depende de la versión del DOS de turno. Es importante señalar que SITRE es incompatible con cualquier RAMDISK.
- $2000 - $3FFF: 8K disponibles para programas en BASIC.
- $4000 - $7FFF: Ventana de 16K para acceder a los bancos de memoria extendida. Si el programa en BASIC no va a utilizar directamente los bancos, están completamente disponibles para él.
- $8000 - $9FFF: 8K disponibles para programas en BASIC, pero que incluyen la memoria de la pantalla en la parte finaldependiendo del modo gráfico utilizado. En modo de texto se reserva poco más de 1K.
- $A000 - $BFFF: 8K correspondientes a la ROM de BASIC.
- $C000 - $FFFF: 16K con la ROM del sistema operativo y algunos registros para el control del hardware.
Si no se usan los bancos, un programa BASIC tiene disponibles aproximadamente 8K+16K+8K-1K=31K de memoria contigua.
Un programa en BASIC se estructura de la siguiente forma:
- Nombres de las variables.
- Valores de las variables numéricas y punteros a los arreglos y cadenas.
- Instrucciones del programa (en formato de token).
- Valores de las cadenas y los arreglos, dispuestos según el orden de las instrucciones DIM.
Se puede decir que las 3 primeras partes ocupan una cantidad fija de memoria, como por ejemplo cuando el programa se graba en disco, y el área de cadenas (
strings) y arreglos y matrices numéricas se asignan dinámicamente cuando el programa se ejecuta, y posteriormente se le asignan valores.
El programa original de SITRE en BASIC incluía todas las rutinas en lenguaje de máquina como asignaciones a cadenas empotradas en el código, incluyendo las 4 etapas del cargador, con la consecuencia que cuando el programa iniciaba, todas ellas quedaban duplicadas en memoria (en el área de código y en el área de valores).
Así, la cantidad de memoria total que utilizaba SITRE era muy cercano al límite de 8K. Para poder grabar más de 64K (256 bloques), era necesario hacer crecer algunos buffers, en particular la tabla para marcar los pitos lentos (64 bytes por cada banco extra), y no era posible crecer sin chocar con la zona para el acceso a los bancos de memoria.
Por lo tanto, el primer paso fue cambiar la forma en que se accedían a las rutinas USR: en lugar de asignarlas a variables de tipo cadena, se cambiaron a variables numéricas, y se les asignó como valor la dirección de la cadena directamente en el área de instrucciones. Es decir:
Código: Seleccionar todo
10 DIM RUTINA$(47)
20 RUTINA$="CARACTERES ATASCII REPRESENTANDO CODIGO BINARIO"
30 U=USR(ADR(RUTINA$),1,2,3)
se cambió por:
Código: Seleccionar todo
10 RUTINA=ADR("CARACTERES ATASCII REPRESENTANDO CODIGO BINARIO")
30 U=USR(RUTINA,1,2,3)
El segundo paso fue mover los buffers que estaban en otras variables de tipo cadena hacia el área de memoria disponible sobre la zona de acceso a los bancos. Ahí hay sobre 6K disponibles para todos ellos, y a la tabla de pitos lentos le asigné tranquilamente 2K.
Ya con memoria suficiente para trabajar en las cosas nuevas, introduje la nueva forma de operar con los bancos. Originalmente utilizaba el mecanismo de usar un valor base para asignar al registro PORTB y a ese sumarle el número del banco a seleccionar, pero ahora utiliza el esquema de valores predefinidos que propuse en otro hilo, a recordar en el siguiente spoiler.
El acceso a la memoria extendida del 130XE y del 800XL modificado se realiza a través de bancos de memoria de 16K cada uno, accesibles a través de una zona fija de memoria ($4000-$7FFF) que es reemplazada por el banco. Para activar un banco específico, se debe modificar algunos bits en el registro de hardware denominado PORTB (dirección de memoria 54017). La combinación de bits requerida para cada banco es la siguiente:
Código: Seleccionar todo
| POKE | Banco real | D=0 V=0 E=0 B=0 R=1
Banco | 54017 | 130XE 256K | 7 6 5 4 3 2 1 0
-------|-------|------------|---------------------------------
0 | 177 | RAM RAM | 1 0 1 1 0 0 0 1
1 | 161 | 0 4 | 1 0 1 0 0 0 0 1
2 | 165 | 1 5 | 1 0 1 0 0 1 0 1
3 | 169 | 2 6 | 1 0 1 0 1 0 0 1
4 | 173 | 3 7 | 1 0 1 0 1 1 0 1
5 | 193 | 8 | 1 1 0 0 0 0 0 1
6 | 197 | 9 | 1 1 0 0 0 1 0 1
7 | 201 | 10 | 1 1 0 0 1 0 0 1
8 | 205 | 11 | 1 1 0 0 1 1 0 1
9 | 225 | 12 | 1 1 1 0 0 0 0 1
10 | 229 | 13 | 1 1 1 0 0 1 0 1
11 | 233 | 14 | 1 1 1 0 1 0 0 1
12 | 237 | 15 | 1 1 1 0 1 1 0 1
-------|-------|------------|---------------------------------
13 | 129 | 0 | 1 0 0 0 0 0 0 1
14 | 133 | 1 | 1 0 0 0 0 1 0 1
15 | 137 | 2 | 1 0 0 0 1 0 0 1
16 | 141 | 3 | 1 0 0 0 1 1 0 1
En la tabla puse los bancos ordenados de forma que sean compatibles un 130XE con un 800XL con el mod de Claus Bushholz o RAMBO (XL o 320K), en que el banco 0 representa la memoria real, los bancos 1 al 4 a los bancos del 130XE, los 5 al 12 al 800XL con mod, y del 13 al 16 los de RAMBO-320K. Esto se puede extender a 16 valores más para un mod de 512K, en que se repiten los valores anteriores, pero con el bit 7 en cero (se resta 128 a los valores de los bancos 1 al 16 para habilitar del 17 al 32).
Para acceder a cada banco en forma simple, basta asignar un arreglo con los valores establecidos, y usar esos valores para modificar PORTB. En BASIC, sería de la siguiente forma:
Código: Seleccionar todo
1 M=16:DIM B(M+1):FOR I=0 TO M:READ X:B(I)=X:NEXT I
2 DATA 177,161,165,169,173,193,197,201,205,225,229,233,237,129,133,137,141
La variable M indica cuántos bancos se podrían utilizar. El arreglo B tiene los M+1 valores posibles, partiendo de 0 para memoria real hasta el máximo banco M.
Para determinar en forma dinámica cuánta memoria extra dispone el programa, es decir, cuantos bancos están disponibles dependiendo del tipo de computador o modificación, se puede usar el siguiente código que ajusta M al máximo posible:
Código: Seleccionar todo
3 FOR I=M TO 1 STEP -1:POKE 54017,B(I):POKE 16384,B(I):NEXT I
4 FOR I=1 TO M:POKE 54017,B(I):IF PEEK(16384)=B(I) THEN NEXT I
5 IF I<=M THEN M=I-1:POP
6 PRINT M
La línea 3 marca cada banco con un valor individual, y las líneas 4 y 5 determinan cuál es el mayor banco utilizable.
Como consecuencia del cambio para manipular bancos, tuve que cambiar el proceso de carga del XEX, el de análisis para descubrir dónde aplicar pitos lentos y la rutina de grabación de bloques en cinta. Sin embargo, me impresionó lo simple que quedó con el arreglo de valores predefinidos para PORTB. Pero debo hacer notar algo relevante: los valores del arreglo habilitan los respectivos bancos, pero suponen que el bit 1 del registro está encendido, es decir, que está presente la ROM del S.O., lo cual no es siempre cierto en SITRE; cuando se graba en modalidad turbo, se están usando las rutinas modificadas de SIO, por lo que la ROM debe estar desactivada, y en ese caso los bancos se activan con:
Como consecuencia del nuevo tamaño de la tabla de pitos lentos, decidí que la rutina utilizara sólo el rango asignado a los bloques de la página que se está grabando, por lo que las marcas ya no se están almacenando en orden invertido como comenté en un post anterior. Mientras acomodaba el código para trabajar con la porción correspondiente, me di cuenta que sí se estaba considerando la duración de las pausas en forma individual, es decir, no se estaba ignorando ese dato como también lo aseguré en ese post.
En la pasada se fueron a la basura:
- Una rutina USR para acceder a los bancos de memoria como si se tratara de memoria lineal y simular un PEEK sobre ella, pues estaba restringida a 64K, y no tenía sentido cambiarla porque me toparía con el ERROR 3 si intentaba pasar en un parámetro un valor mayor a 65535. Como debía separar el valor de la dirección en 2 valores de 16 bits, en realidad no costaba nada hacer el cambio de banco y leer el valor deseado en la misma subrutina. Como la rutina USR también permitía hacer POKE, la nueva rutina se clonó para habilitar esa funcionalidad.
- Una rutina USR que limpiaba los 4 bancos de memoria del 130XE antes de cargar el XEX. Como ahora podría haber más de 4 bancos, y a cinta se van los datos que se carguen del XEX, no era necesario inicializar la memoria, salvo para el último bloque, pero ya dije que el cargador ignorará eso. De todos modos es una mejora pendiente.
- La antigua rutina que verificaba que se trate de un 130XE. Fue reemplazado por una optimización del esquema para detectar los bancos disponibles, y ahora en vez de bloquearse, indica cuántos bancos hay disponibles para copiar, que puede ser sólo 1 en el caso de un 800XL estándar.
Lo que va quedando pendiente:
- Rutina en Assembler para inicializar los buffers. Actualmente la tabla de pitos lentos no se limpia si se copian varios XEX en la misma sesión y las marcas se van acumulando. Nada grave por cierto, pero usar un ciclo FOR-NEXT para 2000 iteraciones introduce una latencia innecesaria.
- Hacer algún cambio para que el último bloque no contenga basura, lo que se produciría cuando se graba un XEX más pequeño que otro grabado anteriormente en la misma sesión. Para ello hay alternativas sin tener que limpiar todos los bancos antes de cargar un XEX, por ejemplo borrar sólo los bytes siguientes al último byte cargado del XEX hasta completar la página de 256 bytes, o bien modificar la rutina de grabación para que cuando se vaya a grabar el bloque con identificador cero (el último), se rellenen los bytes mayores al largo de registro.
- Restringir el número máximo de bloques a grabar en cinta a 999 o cambiar el contador a 4 dígitos. 999 bloques son 255.744 bytes, para los cuales se necesitan algo más de 15 bancos. Lo más simple es restringir el M a 15, aunque en el MOD de Bushholz sólo hay 12+1=13. Con 15 bancos, el contador llegaría a 960, y serían 59 minutos de carga... pero no he visto un XEX de 245K, eso es casi un diskette de densidad ampliada grabado por los 2 lados!!!
- Permitir la lectura de XEX desde unidades de disco distintos de "D1:". Cuando desarrollé SITRE, pocos tenían más de una disketera.
Espero tener un poco de tiempo para estos ajustes...