martes, 10 de abril de 2012

Ensamblador 8086 Formato BMP

Hasta el momento hemos hablado del modo 13h, la memoria de gráficos y la paleta de colores. En este post veremos cómo está compuesto el formato BMP. En particular hablaremos del formato que contiene una paleta de colores, lo cual lo hace ideal para poder desplegarlo en el modo 13h.

El formato BMP con paleta de colores tiene 3 secciones principales:

  1. Encabezado fijo de 54 bytes: donde podemos encontrar información como el ancho y el alto de la imagen, y la dirección donde empieza la información de cada pixel de la imagen (sección 3).
  2. Paleta de colores: la cual puede variar en tamaño. Cada entrada tiene 4 bytes en el siguiente orden: azul (B), verde (G), rojo (R), byte reservado. Para calcular el número de elementos de la paleta tomamos la dirección donde inicia la sección 3 (información de cada pixel) y restamos los 54 bytes del encabezado fijo. Esto nos da el tamaño en bytes de la paleta de colores, simplemente dividimos este número entre cuatro y obtenemos el número de entradas o colores de la paleta.
  3. Información de cada pixel de la imagen.

Usaremos la siguiente imagen y la abriremos con un visualizador de datos en hexadecimal (GHex) para analizar algunos puntos importantes de su contenido.

Encabezado Fijo

Los valores numéricos están guardados en formato little endian, es decir el byte menos significativo está en la dirección menos significativa. Si tenemos el número 36 9A, lo leeremos 9A36.

  • 42 4D: son los valores ASCII de BM, los archivos en formato BMP empiezan con estos 2 bytes.
  • 00 00 9A 36: la imagen mide 39,478 bytes.
  • 00 00 04 36: donde empiezan los datos de cada pixel, es decir en el byte 1078 del archivo.
  • 00 00 00 28: hasta este punto van 14 bytes del encabezado y faltan 40 bytes más (28h), que dan los 54 bytes del encabezado fijo.
  • 00 00 00 F0: ancho de la imagen (240 pixeles).
  • 00 00 00 A0: alto de la imagen (160 pixeles).
  • 00 00 96 00: número de bytes de la imagen (38,400 = 240 * 160)

Si los datos de cada pixel inician en el byte 1078 y el encabezado fijo mide 54 bytes, entonces la paleta de colores mide 1024 bytes; es decir tiene 256 entradas.

Pixeles y Paleta de Colores

Ahora realizaremos un ejercicio para mostrar que leer los colores de la imagen es prácticamente equivalente a la explicación que vimos para el modo 13h.

  1. Tomamos la información de un pixel: el primer pixel inicia en 1078 (436h) y contiene 37h. Esta es la entrada de la paleta de donde tomaremos el color.



  2. La paleta de colores inicia en 36h, por lo que la entrada 37h esta en: 36h + (37h * 4) = 112h.



  3. Si ponemos el código de color en un editor de imágenes podemos ver que corresponde al color del fondo de la imagen.

El valor 37h aparece repetidamente al inicio del archivo ya que es el valor del fondo de la imagen. Ahora, busquemos el primer valor diferente y veamos su color.

  1. El primer valor encontrado es 71h.



  2. La entrada de la paleta que buscamos esta en: 36h + (71h * 4) = 1FAh.



  3. Nuestra intuición diría que el color debe ser rojo; sin embargo, podemos ver que el color es verde, y corresponde al cuadro de la parte inferior. Esto se debe a que en el formato BMP, las líneas se guardan de abajo hacia arriba con respecto a como se deben mostrar en la pantalla. Es decir, la primer línea de datos del archivo es la línea inferior de la imagen.

Muy bien, ya tenemos todos los elementos para poder desplegar una imagen BMP en ensamblador 8086 modo 13h, en el siguiente post veremos el código.

La siguiente página tiene información adicional acerca del modo 13h y del formato BMP.

domingo, 8 de abril de 2012

Ensamblador 8086 Modo 13h Paleta de Colores

En post anterior explicamos que para desplegar pixeles en la pantalla usando el modo 13h tenemos que escribir a memoria y configurar la paleta de colores. Sin embargo, el ejemplo sólo mostró como escribir a la memoria de gráficos y usamos la paleta default de colores. En este post realizaremos un pequeño experimento para mostrar cómo configurar la paleta de colores.

El siguiente programa pinta 10 franjas horizontales de 20 pixeles de ancho cada una. Recordemos que hay 64,000 pixeles en la pantalla (320x200), por lo que cada franja de 20 pixeles de ancho ocupa un 10% de la pantalla es decir 6,400 pixeles. La primera parte del programa llena el área de memoria de gráficos (que inicia en A000:0000) con 6,400 ceros, seguidos de 6,400 unos, seguidos de 6,400 dos, y así sucesivamente hasta nueve.

Los colores que vemos en la pantalla son los colores de las primeras diez entradas de la paleta default de colores.

Después de presionar una tecla, el programa configura en tonos de azules las primeras diez entradas de la paleta de colores. Podemos observar los resultados en la siguiente imagen. Es importante recalcar que NO modificamos el área de memoria de gráficos, la cual sigue teniendo diez conjuntos de 6,400 pixeles numerados del cero al nueve.

Por último, volvemos a modificar las primeras diez entradas de la paleta de colores pero esta vez con tonos de verde.

.model small
.stack 128
.data

.code
.startup
main proc
  ; INT 10h / AH = 0 - configurar modo de video.
  ; AL = modo de video deseado.
  ;     00h - modo texto. 40x25. 16 colores. 8 paginas.
  ;     03h - modo texto. 80x25. 16 colores. 8 paginas.
  ;     13h - modo grafico. 40x25. 256 colores. 
  ;           320x200 pixeles. 1 pagina. 
  mov ax,0013h
  int 10h
  mov ax, 0A000h
  mov ds, ax  ; DS = A000h (memoria de graficos).

  ; ============== Franjas horizontales ===================
  ; Pintar 10 franjas horizontales de 20 pixeles de ancho
  ; cada una. La primer franja usara el indice 0 de la 
  ; paleta de colores, y asi sucesivamente. 
  ; Es decir, tenemos que pintar toda la pantalla 64,000
  ; pixeles, pero cada 6,400 pixeles cambiamos el color.
  mov cx,0FA00h ; todos los pixeles de la pantalla
  xor dx,dx ; color para cada franja
  xor bx,bx ; contador de pixeles por franja
  xor di,di

  ciclo_1:
  mov [di], dx ; poner color en A000:DI
  inc di
  inc bx
  cmp bx,6400
  jne sig_pix1
            ; nueva franja
  xor bx,bx ; resetear contador de pixeles por franja
  inc dx    ; cambiar color
  sig_pix1:
  loop ciclo_1

  ; esperar por tecla
  mov ah,10h
  int 16h
  
  ; ======= Cambiar paleta de colores a tonos de azul =====
  mov dx,3c8h ; Empezamos a modificar la paleta de colores
  mov al,0    ; desde la entrada 0
  out dx,al
  inc dx      ; DX = 3C9h.
  mov cx,10

  ciclo_2:   ; Modificar 10 entradas de la paleta
  mov al,0   ; Rojo.
  out dx,al
  mov al,0   ; Verde
  out dx,al
  mov al,6   ; Azul
  mul cl
  out dx,al
  loop ciclo_2

  ; esperar por tecla
  mov ah,10h
  int 16h

  ; ======= Cambiar paleta de colores a tonos de verde =====
  mov dx,3c8h ; Empezamos a modificar la paleta de colores
  mov al,0    ; desde la entrada 0
  out dx,al
  inc dx      ; DX = 3C9h.
  mov cx,10

  ciclo_3:   ; Modificar 10 entradas de la paleta
  mov al,0   ; Rojo.
  out dx,al
  mov al,6   ; Verde
  mul cl
  out dx,al
  mov al,0   ; Azul
  out dx,al
  loop ciclo_3

  ; esperar por tecla
  mov ah,10h
  int 16h

  ; regresar a modo texto
  mov ax,0003h
  int 10h
  
  ; finalizar el programa
  mov ax,4c00h
  int 21h
  ret
main endp
end

jueves, 5 de abril de 2012

Ensamblador 8086 Modo 13h Memoria para Gráficos

El programa del post anterior pinta pixeles usando la interrupción 10h servicio 0Ch; podríamos pensar que estamos mandando la instrucción de pintar un pixel directamente a la tarjeta de video. Sin embargo, lo que realmente estamos haciendo es escribir al área de memoria de gráficos que inicia en A000:0000.

Esta área consta de 320x200 bytes (64,000 bytes) y cada byte corresponde a un pixel. Para cada byte del área de memoria de gráficos, la tarjeta de video lee el byte y usa ese byte como índice en la paleta de colores para obtener el color del pixel.

En la pantalla, el pixel superior-izquierdo (fila 0, columna 0) corresponde al byte en la dirección A000:0000, el pixel superior-derecho (fila 0, columna 319) corresponde a A000:013F, el primer pixel de la segunda fila (fila 1, columna 0) está mapeado a la dirección A000:0140. En general, el pixel en la fila i, columna j corresponde a A000:(140h*i+j).

La siguiente imagen muestra cómo interactuamos con el área de memoria y la paleta de colores; la tarjeta de video usa esta información para desplegar el contenido de la pantalla.

El siguiente programa muestra la misma funcionalidad del programa del post anterior, pero esta vez escribiendo directamente al área de memoria de gráficos.

.model small
.stack 128
.data

.code
.startup
main proc
  ; INT 10h / AH = 0 - configurar modo de video.
  ; AL = modo de video deseado.
  ;     00h - modo texto. 40x25. 16 colores. 8 paginas.
  ;     03h - modo texto. 80x25. 16 colores. 8 paginas.
  ;     13h - modo grafico. 40x25. 256 colores. 
  ;           320x200 pixeles. 1 pagina. 
  mov ax,0013h
  int 10h
  mov ax, 0A000h
  mov ds, ax  ; DS = A000h (memoria de graficos).

  ; ============== Lineas verticales ======================
  ; Queremos pintar 256 colummas, cada una con un alto de 
  ; 200 pixeles. Podemos ejecutar 51,200 ciclos.
  ; Como la memoria de graficos es lineal, es mejor pintar
  ; una fila a la vez, cada fila tiene 320 columnas, pero
  ; solo pintamos 256. Al llegar a la columna 256 saltamos
  ; a la siguiente fila sumando 320-256 = 64
  ; Cada pixel cambia de color para dar el efecto de lineas
  ; verticales
  mov cx,0C800h ; # de pixeles
  xor dx,dx     ; contador de columnas y color
  xor di,di

  ciclo_1:
  mov [di], dx ; poner color en A000:DI
  inc di
  inc dx
  cmp dx,256
  jne sig_pix1

  add di,0040h ; saltar al inicio de siguiente fila
  xor dx,dx    ; reiniciar columnas y color
  sig_pix1:
  loop ciclo_1

  ; esperar por tecla
  mov ah,10h
  int 16h

  ; ============== Lineas horizontales ======================
  ; Queremos pintar 200 filas, cada una con un largo de 
  ; 320 pixeles. Podemos ejecutar 64,000 ciclos.
  ; Despues de pintar los 320 pixeles correspondientes a
  ; una fila, cambiamos el color para pintar la siguiente
  ; fila y dar el efecto de lineas horizontales.
  mov cx,0FA00h ; todos los pixeles de la pantalla
  xor dx,dx ; color para cada fila
  xor bx,bx ; contador de columnas
  xor di,di

  ciclo_2:
  mov [di], dx ; poner color en A000:DI
  inc di
  inc bx
  cmp bx,320
  jne sig_pix2
            ; nueva fila
  xor bx,bx ; resetear contador de columnas
  inc dx    ; cambiar color
  sig_pix2:
  loop ciclo_2

  ; esperar por tecla
  mov ah,10h
  int 16h

  ; regresar a modo texto
  mov ax,0003h
  int 10h
  
  ; finalizar el programa
  mov ax,4c00h
  int 21h
  ret
main endp
end

martes, 3 de abril de 2012

Ensamblador 8086 Modo Gráfico 13h

El modo gráfico 13h nos permite manejar la pantalla como una matriz de 320 píxeles de ancho por 200 píxeles de alto. Cada píxel puede tomar uno de 256 colores, estos colores están definidos en una paleta de colores la cual podemos configurar.

El siguiente programa despliega primero líneas verticales, espera por una tecla, despliega líneas horizontales y espera por otra tecla. Los colores mostrados son los de la paleta default de colores. Usamos la interrupción 10h, servicio 0Ch para modificar los píxeles; el parámetro AL más que especificar directamente el color, indica la entrada de la paleta de colores que se debe usar para el píxel ubicado en la fila DX y la columna CX.

En el siguiente post veremos con más detalle la paleta de colores y cómo podemos pintar en pantalla mediante escrituras a memoria en lugar de usar la interrupción 10h.

La siguiente entrada en Wikipedia muestra la paleta default de colores que corresponde a los colores mostrados por el programa.

.model small
.stack 128
.data

.code
.startup
main proc
  ; INT 10h / AH = 0 - configurar modo de video.
  ; AL = modo de video deseado.
  ;     00h - modo texto. 40x25. 16 colores. 8 paginas.
  ;     03h - modo texto. 80x25. 16 colores. 8 paginas.
  ;     13h - modo grafico. 40x25. 256 colores. 
  ;           320x200 pixeles. 1 pagina. 
  mov ax,0013h
  int 10h

  ; ============== Lineas verticales ======================
  ; INT 10h / AH = 0Ch - cambiar color de un pixel.
  ;   AL = color
  ;   CX = columna
  ;   DX = fila

  ; for CX=0 to 256
  ;   for DX=0 to 200
  ;     Poner pixel (DX,CX) con color CX
  mov cx,0000h
  mov dx,0000h

  ciclo_1:
  mov ah,0ch
  mov al,cl
  int 10h
  inc dx
  cmp dx,200
  jne ciclo_1

  mov dx,0000h
  inc cx
  cmp cx,256
  jne ciclo_1

  ; esperar por tecla
  mov ah,10h
  int 16h

  ; ============== Lineas horizontales ======================
  ; INT 10h / AH = 0Ch - cambiar color de un pixel.
  ;   AL = color
  ;   CX = columna
  ;   DX = fila

  ; for DX=0 to 200
  ;   for CX=0 to 320
  ;     Poner pixel (DX,CX) con color DX
  mov cx,0000h
  mov dx,0000h

  ciclo_2:
  mov ah,0ch
  mov al,dl
  int 10h
  inc cx
  cmp cx,320
  jne ciclo_2
  
  mov cx,0000h
  inc dx
  cmp dx,200
  jne ciclo_2

  ; esperar por tecla
  mov ah,10h
  int 16h

  ; regresar a modo texto
  mov ax,0003h
  int 10h
  
  ; finalizar el programa
  mov ax,4c00h
  int 21h
  ret
main endp
end

domingo, 1 de abril de 2012

Ensamblador 8086 itoa

Una vez entendiendo la función atoi del post anterior, decidí implementar la función que realiza la operación inversa. Ahora la entrada es un valor entero y lo debemos transformar en cadena. El pseudo-código se muestra a continuación:

1 temp = 0
2 indice = 0
3 while entero > 0
4   residuo = entero modulo 10
5   entero = entero div 10
6   cadena[indice] = residuo + 30h
7   indice = indice + 1
8 end while

La idea es ir extrayendo los dígitos (empezando por el menos significativo) e ir guardándolos en la cadena, noten que sumamos 30h para guardar el valor ascii que representa el dígito. Sin embargo, esta implementación genera una cadena al revés, es decir, si entero inicia con valor de 1234, la cadena final contendrá “4321”. Por lo que es necesario invertir esta cadena.

Pero es posible evitar invertir la cadena al final, para ello usaremos el stack (una estructura lifo – last in first out).

01 temp = 0
02 indice = 0
03 ndigitos = 0
04 while entero > 0
05   temp = entero modulo 10
06   entero = entero div 10
07   push temp
08   ndigitos = ndigitos + 1
09 end while
10 while ndigitos > 0
11   pop temp
12   cadena[indice] = temp + 30h
13   ndigitos = ndigitos – 1
14   indice = indice + 1
15 end while

El primer ciclo (línea 04-09) extrae los dígitos y los guarda en el stack, al final del ciclo el dígito más significativo se encuentran arriba del stack. El segundo ciclo (línea 10-15) extrae los dígitos del stack y los guarda en la cadena final.

El siguiente programa convierte dos cadenas a enteros usando atoi, los suma y convierte el resultado a cadena usando itoa para poder desplegarlo.

.model SMALL
.STACK 128
.DATA
  cadena1 db '411$'
  cadena2 db '144$'
  cadena3 db 6 DUP(?)
  op1 dw ?
  op2 dw ?

.CODE
  .STARTUP
  mov ax,@data
  mov ds,ax

main proc
  ; SI parametro
  mov si, offset cadena1
  call atoi
  mov op1,bx
   
  ; SI parametro
  mov si, offset cadena2
  call atoi
  mov op2,bx
   
  ; sumar
  mov ax, op1
  add ax, op2
  mov bx, offset cadena3
  call itoa
   
  mov dx, offset cadena3
  call desplegar
   
  ; INT 21h / AH=4Ch retorna el control al sistema operativo
  ;                  termina el programa
  salir:
  mov ax,4c00h
  int 21h
  ret
main endp

; ============ Proc: Desplegar mensaje ====================
; Parametros
; dx: offset de cadena terminada por $ con respecto a DS
desplegar proc
   ; INT 21h / AH=9 - despliega cadena apuntada por DS:DX. 
   ; la cadena debe estar terminada por '$'.
   mov ah,09h
   ;mov dx, offset cad
   int 21h
   ret
desplegar endp

; ========= Convertir cadena a numero =====================
; Parametros
; si: offset inicial de la cadena con respecto a DS
; Retorna
; bx: valor
atoi proc
  xor bx,bx   ;BX = 0

  atoi_1:
  lodsb       ;carga byte apuntado por SI en AL
              ;e incrementa si
  cmp al,'0'  ;es numero ascii? [0-9]
  jb noascii  ;no, salir
  cmp al,'9'
  ja noascii  ;no, salir

  sub al,30h  ;ascii '0'=30h, ascii '1'=31h...etc.
  cbw         ;byte a word
  push ax
  mov ax,bx   ;BX tendra el valor final
  mov cx,10
  mul cx      ;AX=AX*10
  mov bx,ax
  pop ax
  add bx,ax
  jmp atoi_1  ;seguir mientras SI apunte a un numero ascii
  noascii:
  ret         ;BX tiene el valor final
atoi endp

; =============== Convertir numero a cadena ===============
; Parametros
; ax: valor
; bx: donde guardar la cadena final
; Retorna
; cadena
itoa proc
  xor cx,cx  ;CX = 0

  itoa_1:
  cmp ax,0   ; El ciclo itoa_1 extrae los digitos del
  je itoa_2  ; menos al mas significativo de AX y los
             ; guarda en el stack. Al finalizar el 
  xor dx,dx  ; ciclo el digito mas significativo esta
  push bx    ; arriba del stack.
  mov bx,10  ; CX contiene el numero de digitos
  div bx
  pop bx
  push dx
  inc cx
  jmp itoa_1

  itoa_2:
  cmp cx,0    ; Esta seccion maneja el caso cuando
  ja itoa_3   ; el numero a convertir (AX) es 0.
  mov ax,'0'  ; En este caso, el ciclo anterior
  mov [bx],ax ; no guarda valores en el stack y
  inc bx      ; CX tiene el valor 0
  jmp itoa_4

  itoa_3:
  pop ax      ; Extraemos los numero del stack
  add ax,30h  ; lo pasamos a su valor ascii
  mov [bx],ax ; lo guardamos en la cadena final
  inc bx
  loop itoa_3

  itoa_4:
  mov ax,'$'  ; terminar cadena con '$' para 
  mov [bx],ax ; imprimirla con la INT21h/AH=9
  ret
itoa endp

end