A vueltas con los números decimales:
Un tutorial sobre el formato BCD
En este documento se da por supuesto que el lector entiende bien el funcionamiento de los números decimales (base 10), binarios (base 2) y su representación en forma hexadecimal (base 16).
Los números son siempre almacenados en formato binario dentro de la memoria del ordenador, cuyos bits sólo pueden estar en dos estados eléctricos, alto o encendido, que equivaldría a uno, y bajo o apagado, que equivaldría a cero. El problema es que la notación binaria no es la más conveniente para los seres humanos, y que su conversión a notación decimal requiere operaciones largas y complicadas desde el punto de vista de su implementación en ensamblador, como pueden ser la división o el operador módulo.
Por ejemplo, para convertir 11111111b, es decir, FFh o 255 en decimal, habría que tomar el número original, dividirlo entre 100 (decimal) y almacenar el dígito de las centenas, dividir el resto entre 10 y almacenar el cociente como dígito de las decenas, y tomar el último resto como dígito para las unidades. Esto envuelve dos divisiones, y estamos en un caso relativamente sencillo, en el que el data original sólo ocupa un byte (8 bits).
Estas dificultades hacen que resulte más conveniente emplear lo que se ha dado en llamar números en formato BCD, binary coded decimals. El concepto subyacente es bastante sencillo: representar cada dígito decimal en cuatro bits (un nibble), de forma que así, en cada byte, se pueden guardar dos dígitos decimales. Este formato en concreto, en el que se almacenan dos dígitos por byte, se llama packed BCD. Culturilla general, hay que llamar a las cosas por su nombre. La ventaja de este formato es que el valor del byte se escribe utilizando los números decimales e indicando que es un valor hexadecimal. Es decir, para almacenar el número 27 en BCD en una posición de memoria, lo que haríamos sería poner en esa posición el valor 27h (la “h”, como no, indica formato hexadecimal). Esto es así porque cada dígito en hexadecimal representa un nibble. Vamos bien: para indicar ahora un número decimal en formato BCD empaquetado basta con escribirlo en formato decimal y añadir la “h” que indica formato hexadecimal.
Ahora los más listos dirán que esto es desaprovechar espacio, y así es. Estamos gastando espacio para ganar velocidad. Como se puede averiguar muy fácilmente, con cuatro bits es posible representar valores entre 0 y 15, así que nos sobran 6 valores en total, porque sólo representaremos valores entre 0 y 9. Parece ahora que es bastante complicado manejar todo esto, pero en realidad no, porque el procesador Z80 tiene una instrucción, y sólo una, para manejar los números BCD, pero es bastante potente.
Veamos un ejemplo a mano. Así podremos identificar los problemas.
Queremos calcular 23 + 45, expresando tanto los sumandos como la suma en BCD. A ver qué sucede:
23 + 45 = 68, haciendo el cálculo en decimal no hay problemas. Ahora hagamos la operación en hexadecimal: 23h + 45h = 68h . De nuevo, en formato BCD, no hay problemas.
¿Y si los sumandos fueran, por ejemplo, 17 y 26? Ahora sí que va a haber alguna pega. 17 + 26 = 43. Por supuesto, la aritmética decimal no nos engaña. En hexadecimal, empleando la notación BCD vista, ¿qué pasa?
17h + 26h = 3Dh . Mal asunto. La D no es un dígito decimal demasiado habitual que digamos. Necesitamos corregir la operación. Esto sería algo muy tedioso para hacer a mano, pero el comando DAA del Z80 lo hace por nosotros.
Vamos a ver un poco de código:
ld a,17h
add 26h ; asi tenemos en el registro A el valor 3Dh calculado arriba.
daa ; tras ejecutar este comando, el valor en A pasa a ser 43h. Correcto!
¿Cómo funciona la instrucción DAA? Pues bueno, emplea un bit especial del registro F (flags o banderas, es decir, el registro de estado del Z80) que recoge el bit de acarreo entre los nibbles, así que puede usarlo para corregir los resultados de las operaciones.
Antes de seguir, una nota importante: la instrucción DAA funciona bien después de ejecutar instrucciones aritméticas del tipo ADD, ADC, SUB, SBC de 8 bits. Ni se os ocurra usarla después de instrucciones del tipo INC o DEC, porque entonces el comando DAA no funciona en absoluto. Cuidadin, muchachos. Tampoco sería una buena idea usar DAA después de operaciones de 16 bits, porque el resultado puede ser cualquier cosa menos correcto.
Ahora, si me habéis seguido hasta aquí, estaréis pensando, ¿y de qué me sirve poder utilizar sólo dos dígitos decimales? Pues tenéis toda la razón del mundo, pero es que el bit de acareo (carry) funciona bien. Es decir, para sumar los números 2358 y 750 haríais lo siguiente:
; Guardamos los valores en los registros de 16 bits BC y DE ld bc,2358h
ld de,0750h ; Sumamos el byte bajo
ld a,c
add e ; Corregimos con DAA y almacenamos en L
daa
ld l,a ; Sumamos el byte alto – usamos aquí ADC, que usa el acarreo anterior
ld a,b
adc d ; Corregimos y almacenamos en H
daa
ld h,a ; El valor final, que debería ser 3108h, estará almacenado en el registro HL
Bien. Ahora se puede ver que se pueden hacer sumas y operaciones con múltiples bytes, representando muchos dígitos, de una forma fácil y, lo que es más importante, exacta.
Entonces, si habéis entendido todo esto, hay que ver para qué pueden servir estos metafísicos números en formato BCD. Desde luego, no sirven para guardar las coordenadas de pantalla, ni para hacer búsquedas en tablas, ni nada parecido. Es decir, son números incómodos para operar con ellos, pero, sin embargo, muy prácticos a la hora de presentar información numérica para humanos. Así, en el ámbito de los video- juegos, los números en formato BCD se suelen emplear para puntuaciones y marcadores. A continuación voy a transcribir unas cuantas rutinas muy prácticas, aunque no especialmente optimizadas, para realizar estos menesteres.
BCD2CHR: ; Esta rutina convierte un byte en BCD a dos bytes en ASCII ; Entrada: A=byte en BCD ; HL=posición en la que almacenar el resultado ; Almacena byte
ld c,a ; Mueve el nibble alto al nibble bajo
srl a srl a srl a srl a
; Convierte en ASCII add 48
; Almacena ld [hl],a
inc hl ; Recupera byte ld a,c
; Obtiene el nibble bajo and 0Fh
add 48 ; Almacena
ld [hl],a inc hl ret
BCD2TXT: ; Rutina que complementa la anterior y convierte números BCD de extensión
variable ; Entrada: ; ; ; Bucle de @@BUCLE:
B=numero de bytes a tratar DE=dirección del último byte del número BCD HL=dirección de inicio de la cadena de texto a crear
copia
ld a,[de] dec de call BCD2CHR djnz @@BUCLE
; Termina ret
Para utilizar esta última rutina debe tenerse en cuenta que el número BCD se almacena en memoria guardando primero los bytes menos significativos. Como ejemplo, si queremos almacenar a partir de la posición 9000h el valor 245349 en BCD, el contenido de memoria quedaría así:
9000h: 49h
9001h: 53h
9002h: 24h
Con las rutinas anteriores, podríamos hacer lo siguiente:
; Convertir de BCD a ASCII ld hl,9800h ld de,9002h
ld b,3
call BCD2TXT ; Imprimir el texto
ld de,NAMTBL ld hl,9800h ld bc,6 call LDIRVM
Esto debería ser suficiente para dejar claro el funcionamiento de los números en BCD. Su uso, aunque limitado, permite representar valores decimales de una forma rápida y precisa, con el tamaño que se requiera, y sin implicar operaciones aritméticas complejas. Por último, voy a incluir una rutina de suma de un valor BCD de 8 bits a otro valor BCD de extension variable.
; SUMABCD:
; Suma un valor BCD a un número BCD de varios bytes
; Entrada:
A=valor BCD a sumar
B=número de bytes usados en el valor BCD (mínimo 2 bytes, 4 dígitos)
HL=dirección de inicio del número en BCD
; Corrige el contador
dec b
; Suma del primer byte
add [hl]
daa
ld [hl],a
inc hl
; Bucle de suma
@@BUCLE:
ld a,[hl]
adc 0
daa ld [hl],a
inc hl
djnz @@BUCLE
; Finaliza
ret
Y creo que con esto tenéis todo lo que necesitáis para crear vuestros marcadores de puntos y récords en cualquier tipo de video-juego. Otro de los trucos que conviene tener en cuenta es que en la mayoría de los juegos, la última cifra es siempre un cero, con lo que se emplean contadores de 7 cifras, en la que la última es siempre 0, por lo que basta con emplear 6 dígitos BCD, que pueden almacenarse en sólo 3 bytes. Otras aplicaciones se os pueden ocurrir, y el código presentado sólo es un ejemplo, no pretende ser óptimo ni universal. Es una base para que empecéis a trabajar e investigar por vuestra cuenta las posibilidades del formato BCD y de la versátil instrucción DAA.
© Eduardo Robsy Petrus, 2005
9 de Febrero de 2005
http://www.robsy.net
Enlace relacionado: [download id=»321″] (PDF).