Sistema de Gestión Académica en Ensamblador 8086
Una guía pensada para principiantes: aquí se explica cada pieza de código del proyecto, sin dar por sentado que ya sabes ensamblador. Vamos desde qué es un registro hasta cómo se escribe un archivo en disco.
Este programa es un pequeño sistema de consola que permite a un usuario registrar estudiantes con tres notas, guardarlos en un archivo, y luego generar un reporte que cifra el nombre, calcula el promedio y clasifica a cada alumno como aprobado o reprobado.
Aunque el objetivo suena sencillo, hacerlo en ensamblador obliga a resolver a mano cosas que en otros lenguajes son automáticas: leer texto del teclado, convertir dígitos en números, separar campos de una línea, o pedirle al sistema operativo que abra un archivo. Por eso este proyecto es un buen ejercicio de Organización del Computador.
Si nunca has visto ensamblador, empieza por Conceptos básicos. Si ya conoces la teoría y quieres entender el proyecto, ve directo a Infraestructura y luego recorre los archivos en orden desde main.asm.
¿Qué hace, en una frase por opción?
- Opción 1 – Registrar: pide un nombre y tres notas (0 a 20), las valida y las añade al archivo
CALIF.TXT. - Opción 2 – Reporte: lee
CALIF.TXT, cifra los nombres, calcula promedios y escribeFINAL.XLScon estadísticas. - Opción 3 – Ver reporte: muestra en pantalla el contenido de
FINAL.XLS. - Opción 4 – Salir: termina el programa y devuelve el control a DOS.
Conceptos básicos de ensamblador
El ensamblador es el lenguaje más cercano al procesador. En lugar de escribir promedio = (a+b+c)/3, le damos al procesador instrucciones diminutas: «mueve este valor», «suma», «compara», «salta». Antes de leer el código conviene conocer cinco ideas.
1. Los registros: las «gavetas» del procesador
El procesador 8086 casi no puede trabajar directamente con la memoria; primero copia los valores a unos espacios internos súper rápidos llamados registros. Son pocos y de 16 bits (pueden guardar números de 0 a 65535). Los que verás todo el tiempo:
- AX
- El «acumulador». Se usa para casi todo: cálculos, resultados, y para decirle a DOS qué función queremos. Se divide en AH (parte alta) y AL (parte baja).
- BX
- «Base». Aquí se suele guardar el manejador (identificador) de un archivo abierto.
- CX
- «Contador». Instrucciones como
looplo usan para repetir; también indica «cuántos bytes» leer/escribir. - DX
- «Datos». Muy usado para apuntar a la dirección de un texto que queremos imprimir.
- SI / DI
- Índices de origen y destino. Sirven para recorrer cadenas de caracteres, letra por letra.
- DS / ES / CS
- Registros de segmento: indican en qué «zona» de memoria estamos leyendo el dato, el destino o el código.
2. Las instrucciones más comunes
| Instrucción | Qué hace |
|---|---|
mov a, b | Copia el valor b dentro de a (mover = copiar). |
add / sub | Suma o resta. |
mul / div | Multiplica o divide (usan AX y DX como pareja para números grandes). |
cmp a, b | Compara a con b (en realidad resta sin guardar) para decidir un salto. |
jmp destino | Salto incondicional: «continúa en esta etiqueta». |
je / jne / jb / ja | Saltos condicionales: salta si igual / distinto / menor / mayor. |
call / ret | Llama a un procedimiento (subrutina) y regresa al terminar. |
push / pop | Guarda/recupera un valor en la pila, para no perder registros. |
lea dx, texto | Carga en DX la dirección (offset) de texto, no su contenido. |
int 21h | Pide un servicio al sistema operativo DOS (imprimir, leer, archivos…). |
3. Las «banderas», en especial el carry (CF)
Después de muchas operaciones, el procesador enciende o apaga unos interruptores llamados banderas. En este proyecto la estrella es la bandera de acarreo (Carry Flag, CF). Por convención, los procedimientos la usan como un «semáforo» de éxito o error:
clcapaga la bandera (CF=0) → «todo salió bien».stcla enciende (CF=1) → «hubo un error».- Quien llamó revisa con
jc(salta si hubo error) ojnc(salta si no hubo error).
4. INT 21h: hablarle a DOS
El programa no dibuja letras en la pantalla ni abre archivos por sí mismo: se lo pide a DOS. Para eso se coloca un número de función en AH y se ejecuta int 21h. Estas son las funciones que usa el proyecto:
| AH | Servicio |
|---|---|
02h | Imprime un solo carácter (el que esté en DL). |
08h | Lee una tecla sin mostrarla (para «presione una tecla»). |
09h | Imprime un texto que termina en el símbolo $. |
0Ah | Lee una línea del teclado dentro de un buffer preparado. |
3Ch | Crea un archivo nuevo (o lo vacía si existía). |
3Dh | Abre un archivo existente (lectura y/o escritura). |
3Eh | Cierra un archivo. |
3Fh | Lee bytes de un archivo abierto. |
40h | Escribe bytes en un archivo (o en la pantalla, que es el «archivo 1»). |
4202h | Mueve el puntero de un archivo al final (para anexar). |
4C00h | Termina el programa y vuelve a DOS. |
También se usa una vez int 10h con AX=0003h, que es un servicio de la BIOS para reiniciar el modo de texto 80×25 y, de paso, limpiar la pantalla.
5. Procedimientos, macros y buffers
- Un procedimiento (
PROC…ENDP) es una subrutina reutilizable, como una función. Se ejecuta concally vuelve conret. - Una macro (
MACRO…ENDM) es una abreviatura: su texto se «pega» tal cual donde la escribes, antes de compilar. - Un buffer es simplemente una zona de memoria reservada por adelantado donde se guardan datos temporales (por ejemplo, el texto que el usuario escribe).
Infraestructura del proyecto
El código no está en un solo archivo gigante: se reparte en un archivo principal (main.asm) y seis archivos .inc que se «pegan» dentro de él al compilar mediante la directiva include. Esto mantiene cada tema separado y fácil de leer.
¿Por qué un programa «.COM» y qué significa org 100h?
Un programa .COM es el formato más simple de DOS: código, datos y pila conviven en un único segmento de 64 KB de memoria. DOS reserva los primeros 256 bytes (256 = 100h en hexadecimal) para información interna, así que el programa comienza en la dirección 100h. La directiva org 100h le dice al ensamblador exactamente eso: «mi primer byte vive en 100h».
Los dos archivos que produce el sistema
CALIF.TXT — entrada
Datos crudos tal como los escribió el usuario. Una línea por alumno:
Juan,15,18,16
Ana,08,09,10Formato: Nombre,Nota1,Nota2,Nota3
FINAL.XLS — salida procesada
Reporte generado: nombre cifrado, promedio y estado. Es texto CSV que Excel puede abrir:
Mxdq,15,18,16,16,APROBADO
Dqd,08,09,10,09,REPROBADOFormato: Cifrado,N1,N2,N3,Promedio,Estado
FINAL.XLS no es un archivo real de Excel: es texto plano con la extensión .XLS. Excel lo abre por compatibilidad, pero no tiene fórmulas ni formato. Es una simplificación consciente del proyecto.
Cómo compilar y ejecutar
- Abrir
main.asmen EMU8086, manteniendo la carpetaincludea su lado. - Pulsar Compile: como usa
org 100h, se genera un.COM. - Ejecutarlo desde un directorio con permiso de escritura (ahí se crearán los archivos).
Flujo general del programa
Todo gira en torno a un bucle de menú. El usuario elige una opción, se ejecuta la acción correspondiente, se hace una pausa, y se vuelve a mostrar el menú, una y otra vez, hasta que elige salir.
Cada acción del menú se apoya en procedimientos más pequeños definidos en los archivos .inc. Ese es el patrón central: funciones pequeñas que se llaman unas a otras, cada una con una única responsabilidad.
main.asm — el corazón del programa
Es el archivo que se compila. Hace tres cosas: reúne todos los demás archivos con include, prepara la memoria, y ejecuta el bucle del menú.
Cabecera e inclusión de archivos
org 100h
jmp Inicio
include 'include\constantes.inc'
include 'include\macros.inc'
include 'include\datos.inc'
include 'include\entrada_salida.inc'
include 'include\archivos.inc'
include 'include\academico.inc'
org 100h — «este programa empieza en la dirección 100h», requisito de todo programa .COM (ver Infraestructura).
jmp Inicio — salta por encima de todos los datos y procedimientos incluidos, para que la ejecución empiece en el código real. Sin este salto, el procesador intentaría «ejecutar» los mensajes de texto como si fueran instrucciones.
Los include pegan el contenido de cada archivo aquí, en orden. El orden importa: las constantes y macros deben conocerse antes de usarlas.
Inicialización de segmentos
Inicio:
push cs
pop ds
push cs
pop es
En un .COM el código y los datos comparten segmento, pero al arrancar solo CS (segmento de código) tiene el valor correcto. Estas cuatro líneas copian CS en DS y en ES usando la pila (no existe un mov ds, cs directo). Así, cuando el programa diga «imprime el texto en tal offset», DS apuntará al lugar correcto y las variables serán legibles.
El menú y sus opciones
MenuPrincipal:
call LimpiarPantalla
IMPRIMIR texto_menu
call LeerOpcionMenu
cmp al, '1'
je OpcionRegistrar
cmp al, '2'
je OpcionReporte
cmp al, '3'
jne VerificarOpcionSalir
jmp OpcionVerReporte
VerificarOpcionSalir:
cmp al, '4'
jne OpcionPrincipalInvalida
jmp SalirPrograma
Se limpia la pantalla, se imprime el menú con la macro IMPRIMIR, y LeerOpcionMenu devuelve en AL la tecla que pulsó el usuario (o 0 si escribió algo raro).
Se compara AL con los caracteres '1', '2', '3', '4'. Ojo: se compara con el carácter '1', no con el número 1, porque el teclado entrega caracteres.
Cada coincidencia salta (je) a la etiqueta de esa opción. Si no coincide con nada válido, cae en OpcionPrincipalInvalida.
Los saltos condicionales del 8086 solo alcanzan destinos cercanos. Como las etiquetas de las opciones están lejos, el código usa un truco: comprueba la condición con un salto corto (jne) y luego usa un jmp largo para llegar al destino real.
OpcionPrincipalInvalida:
IMPRIMIR msg_opcion_invalida
call SonidoError
call PausarPantalla
jmp MenuPrincipal
OpcionRegistrar:
call RegistrarAlumno
call PausarPantalla
jmp MenuPrincipal
Todas las ramas siguen el mismo patrón: hacer la acción → pausar → volver al menú. OpcionReporte y OpcionVerReporte son idénticas cambiando solo el call. La opción inválida además emite un pitido con SonidoError.
Salir del programa
SalirPrograma:
call LimpiarPantalla
IMPRIMIR encabezado_salida
IMPRIMIR msg_salida
mov ax, 4C00h
int 21h
mov ax, 4C00h seguido de int 21h es la forma estándar de terminar: la función 4Ch cierra el programa y el 00 es el «código de salida» (0 = sin errores). El control vuelve a DOS.
constantes.inc — números con nombre
En lugar de escribir números «mágicos» repartidos por el código, se les da un nombre con EQU («equivale a»). EQU no reserva memoria: es solo una etiqueta que el ensamblador reemplaza por su valor al compilar. Cambiar el límite en un solo sitio actualiza todo el programa.
MAXIMO_NOMBRE EQU 50
MAXIMO_LINEA EQU 127
TAMANO_BUFFER_VISTA EQU 512
CR EQU 13
LF EQU 10
| Constante | Significado |
|---|---|
MAXIMO_NOMBRE | Un nombre puede tener como máximo 50 caracteres. |
MAXIMO_LINEA | Una línea leída de un archivo no puede pasar de 127 caracteres. |
TAMANO_BUFFER_VISTA | Al mostrar el reporte, se lee en bloques de 512 bytes. |
CR (13) | «Retorno de carro» (Carriage Return): mueve el cursor al inicio de la línea. |
LF (10) | «Avance de línea» (Line Feed): baja una línea. |
En DOS y Windows, terminar una línea de texto requiere dos bytes: CR (13) y luego LF (10). Por eso a lo largo del proyecto verás CR, LF al final de cada mensaje y de cada registro escrito en los archivos.
macros.inc — abreviaturas de escritura
Una macro es texto que se «expande» (se copia tal cual) donde la invocas. Estas dos evitan repetir las mismas dos líneas cientos de veces. Fíjate que la lógica real vive en procedimientos: la macro solo prepara los registros y llama.
IMPRIMIR MACRO mensaje
lea dx, mensaje
call ImprimirCadena
ENDM
NUEVA_LINEA MACRO
call ImprimirNuevaLinea
ENDM
IMPRIMIR texto_menu se convierte, al compilar, en lea dx, texto_menu + call ImprimirCadena. La macro recibe un parámetro (mensaje) que se sustituye por el nombre real.
NUEVA_LINEA no recibe parámetros: solo llama al procedimiento que imprime CR+LF.
Una macro copia su código en cada uso (más rápido, pero ocupa más espacio). Un procedimiento existe una sola vez y se salta hacia él con call (ahorra espacio). Aquí se combinan: la macro es solo la fachada corta; el trabajo pesado está en el procedimiento.
datos.inc — mensajes, buffers y variables
Aquí no hay instrucciones ejecutables: solo se reserva memoria y se le da valor inicial. La directiva db reserva bytes (define byte) y dw reserva palabras de 16 bits (define word).
Nombres de archivo terminados en cero
nombre_archivo_calif db 'CALIF.TXT', 0
nombre_archivo_final db 'FINAL.XLS', 0
Las funciones de archivo de DOS necesitan el nombre terminado en un byte 0 (así saben dónde acaba el texto). A esto se le llama «cadena ASCIIZ».
Mensajes que terminan en «$»
texto_menu db '====================================', CR, LF
db ' SISTEMA DE GESTION ACADEMICA', CR, LF
db '====================================', CR, LF
db '1. Registrar Alumnos y Notas', CR, LF
db '2. Generar Reporte y Estadisticas', CR, LF
db '3. Ver Reporte Final (.XLS)', CR, LF
db '4. Salir', CR, LF
db '------------------------------------', CR, LF
db 'Seleccione una opcion: $'
La función de impresión de DOS (AH=09h) escribe carácter por carácter hasta encontrar el símbolo $, que actúa como «fin del mensaje». Por eso cada texto imprimible termina en '$'. El menú se arma con varias líneas db consecutivas: en memoria quedan una detrás de otra formando un único bloque.
El resto del archivo define, con el mismo patrón, los encabezados de cada pantalla y todos los mensajes de error y confirmación (msg_nota_invalida, msg_guardado, msg_falta_calif, etc.).
Cadenas SIN «$» (se escriben por longitud)
coma db ','
bytes_nueva_linea db CR, LF
texto_aprobado db 'APROBADO'
LONGITUD_APROBADO EQU ($ - texto_aprobado)
texto_reprobado db 'REPROBADO'
LONGITUD_REPROBADO EQU ($ - texto_reprobado)
Estos textos van a archivos, no a la pantalla, así que no pueden llevar $ (¡ensuciaría el archivo!). En su lugar, el programa indica cuántos bytes escribir. El truco EQU ($ - texto_aprobado) calcula esa longitud automáticamente: $ significa «la posición actual», así que restarle el inicio del texto da su tamaño (8 para «APROBADO»).
Buffers de entrada
buffer_entrada db MAXIMO_NOMBRE, 0, (MAXIMO_NOMBRE + 1) dup (0)
buffer_menu db 2, 0, 3 dup (0)
buffer_linea db (MAXIMO_LINEA + 1) dup (0)
buffer_visualizacion db TAMANO_BUFFER_VISTA dup (0)
nombre_cifrado db (MAXIMO_NOMBRE + 1) dup (0)
buffer_numero db 6 dup (0)
caracter_archivo db 0
dup significa «duplica»: 3 dup (0) reserva 3 bytes con valor 0. La función de lectura de teclado de DOS (AH=0Ah) exige un buffer con un formato muy específico de tres partes:
Así, buffer_menu db 2, 0, ... acepta como mucho 2 caracteres; el programa luego revisa el byte 1 para saber cuántos se escribieron. Los demás buffers son zonas de trabajo: la línea leída de un archivo, el bloque de visualización, el nombre ya cifrado, y un pequeño espacio para convertir números a texto.
Variables de trabajo
manejador_archivo dw 0
manejador_entrada dw 0
manejador_salida dw 0
...
nota1 dw 0
nota2 dw 0
nota3 dw 0
promedio dw 0
contador_total dw 0
contador_aprobados dw 0
contador_reprobados dw 0
contador_invalidos dw 0
Las variables dw guardan números de 16 bits. Los manejadores (o «handles») son los identificadores que DOS entrega al abrir un archivo; hay que guardarlos para poder escribir, leer o cerrar después. Las notas y el promedio almacenan los valores del alumno que se está procesando, y los contadores llevan las estadísticas del reporte.
entrada_salida.inc — consola, números y sonidos
Este archivo agrupa los procedimientos de más bajo nivel: cómo imprimir, cómo leer del teclado, cómo convertir dígitos en números y viceversa, y cómo emitir un pitido. Casi todo el resto del programa se apoya en estas piezas.
ImprimirCadena e ImprimirNuevaLinea
ImprimirCadena PROC NEAR
mov ah, 09h
int 21h
ret
ImprimirCadena ENDP
El más simple de todos. Espera que DX ya apunte a un texto terminado en $ (la macro IMPRIMIR se encarga de eso). Pone 09h en AH y ejecuta int 21h: DOS imprime el texto. NEAR significa que el procedimiento vive en el mismo segmento que quien lo llama.
ImprimirNuevaLinea PROC NEAR
push ax
push dx
mov ah, 02h
mov dl, CR
int 21h
mov dl, LF
int 21h
pop dx
pop ax
ret
ImprimirNuevaLinea ENDP
Imprime un salto de línea enviando dos caracteres sueltos (CR y luego LF) con la función 02h. Fíjate en el patrón push al entrar y pop al salir: guarda AX y DX para devolverlos intactos. Así quien lo llamó no pierde lo que tenía en esos registros. Este cuidado se repite en casi todos los procedimientos.
LimpiarPantalla
LimpiarPantalla PROC NEAR
push ax
mov ax, 0003h
int 10h
pop ax
ret
LimpiarPantalla ENDP
Usa la interrupción de la BIOS int 10h (no la de DOS) con AX=0003h para reiniciar el modo de texto a 80 columnas × 25 filas en color. Como efecto secundario, esto borra la pantalla, que es justo lo que queremos entre pantalla y pantalla.
LeerEntradaBuffer
LeerEntradaBuffer PROC NEAR
push ax
push bx
mov bx, dx
mov byte ptr [bx + 1], 0
mov ah, 0Ah
int 21h
pop bx
pop ax
ret
LeerEntradaBuffer ENDP
Lee una línea del teclado dentro del buffer al que apunta DX. Antes de leer pone a cero el byte 1 (la cuenta de caracteres) para poder reutilizar el mismo buffer varias veces sin arrastrar la longitud anterior. byte ptr [bx + 1] significa «el byte que está una posición después de donde apunta BX».
LeerOpcionMenu
LeerOpcionMenu PROC NEAR
push bx
push dx
lea dx, buffer_menu
call LeerEntradaBuffer
lea bx, buffer_menu
cmp byte ptr [bx + 1], 1
jne EntradaMenuInvalida
mov al, [bx + 2]
jmp FinEntradaMenu
EntradaMenuInvalida:
xor al, al
FinEntradaMenu:
pop dx
pop bx
ret
LeerOpcionMenu ENDP
Lee la entrada en buffer_menu.
Comprueba que el usuario escribiera exactamente un carácter (byte 1 == 1). Si escribió cero o dos, no es una opción válida.
Si es válido, copia ese carácter (byte 2) en AL. Si no, xor al, al pone AL en 0 (una forma rápida y clásica de «AL = 0»). El menú principal interpretará ese 0 como «opción inválida».
LeerNumeroBuffer — convertir texto en número
Cuando el usuario teclea «15», en realidad escribe los caracteres '1' y '5', no el número quince. Este procedimiento hace la conversión y, de paso, valida que el resultado esté entre 0 y 20.
LeerNumeroBuffer PROC NEAR
push bx
push cx
push dx
push si
push di
xor ax, ax
xor cx, cx
mov cl, byte ptr buffer_entrada[1]
cmp cx, 0
jne NumeroTieneEntrada
jmp NumeroSinSignoInvalido
NumeroTieneEntrada:
lea si, buffer_entrada[2]
BucleNumeroSinSigno:
mov dl, [si]
cmp dl, '0'
jb NumeroSinSignoInvalido
cmp dl, '9'
ja NumeroSinSignoInvalido
sub dl, '0'
xor dh, dh
mov di, dx
mov bx, 10
mul bx
or dx, dx
jnz NumeroSinSignoInvalido
add ax, di
cmp ax, 20
ja NumeroSinSignoInvalido
inc si
loop BucleNumeroSinSigno
clc
jmp FinNumeroSinSigno
NumeroSinSignoInvalido:
stc
FinNumeroSinSigno:
pop di
pop si
pop dx
pop cx
pop bx
ret
LeerNumeroBuffer ENDP
La idea es el algoritmo clásico de conversión decimal: por cada dígito, se multiplica el acumulado por 10 y se le suma el nuevo dígito. Por ejemplo, para «15»: empezamos en 0 → 0×10+1 = 1 → 1×10+5 = 15.
CL recibe cuántos caracteres se escribieron. Si es 0 (línea vacía), va directo a «inválido».
SI apunta al primer dígito. En el bucle, cada carácter se compara: si es menor que '0' o mayor que '9', no es un dígito → inválido.
sub dl, '0' convierte el carácter en su valor numérico ('7' menos '0' = 7). Se guarda en DI temporalmente, porque la multiplicación va a usar DX.
mul bx (con BX=10) multiplica AX por 10. Se comprueba or dx, dx / jnz: si el resultado se desbordó a DX, el número es demasiado grande → inválido.
Se suma el dígito y se verifica que no pase de 20. loop repite mientras queden caracteres. Al final, clc marca éxito (CF=0) o stc marca error (CF=1).
LeerNota — insiste hasta que la nota sea válida
LeerNota PROC NEAR
push bx
push dx
mov bx, dx
LeerNotaNuevamente:
mov dx, bx
call ImprimirCadena
lea dx, buffer_entrada
call LeerEntradaBuffer
call LeerNumeroBuffer
jnc FinLecturaNota
IMPRIMIR msg_nota_invalida
call SonidoError
jmp LeerNotaNuevamente
FinLecturaNota:
pop dx
pop bx
ret
LeerNota ENDP
Une las piezas anteriores en un bucle de validación. Guarda en BX el mensaje de solicitud (por ejemplo «Ingrese la nota 1:») para poder reimprimirlo. Muestra el mensaje, lee, convierte. Si LeerNumeroBuffer devolvió éxito (jnc), termina. Si no, muestra el error, pita, y vuelve a empezar. No sale hasta obtener una nota válida entre 0 y 20.
NumeroATexto — el camino inverso
NumeroATexto PROC NEAR
push ax
push bx
push di
lea di, buffer_numero[6]
xor cx, cx
mov bx, 10
cmp ax, 0
jne BucleConversionNumero
dec di
mov byte ptr [di], '0'
inc cx
jmp FinConversionNumero
BucleConversionNumero:
xor dx, dx
div bx
add dl, '0'
dec di
mov [di], dl
inc cx
cmp ax, 0
jne BucleConversionNumero
FinConversionNumero:
mov dx, di
pop di
pop bx
pop ax
ret
NumeroATexto ENDP
Convierte un número (en AX) al texto de sus dígitos, necesario para imprimirlo o escribirlo en un archivo. Se dividen los dígitos de derecha a izquierda usando el resto de dividir entre 10:
Se apunta con DI al final de buffer_numero y se va retrocediendo, porque los dígitos salen del último al primero.
Caso especial: si el número es 0, se escribe directamente el carácter '0' (el bucle normal no produciría ningún dígito).
En el bucle, div bx (BX=10) deja el cociente en AX y el resto en DX. Ese resto es el último dígito; add dl, '0' lo convierte en carácter y se guarda retrocediendo.
Se repite hasta que AX llega a 0. Al final, DX apunta al primer dígito y CX dice cuántos dígitos hay: justo lo que necesitan las funciones de escritura.
ImprimirSinSigno
ImprimirSinSigno PROC NEAR
push ax
push bx
push cx
push dx
call NumeroATexto
mov bx, 1
mov ah, 40h
int 21h
pop dx
pop cx
pop bx
pop ax
ret
ImprimirSinSigno ENDP
Imprime un número en pantalla. Primero lo convierte a texto con NumeroATexto, y luego usa la función de escritura 40h con BX=1. Ese «1» es el manejador de la salida estándar: en DOS la pantalla se trata como si fuera un archivo abierto con el número 1. Así se reutiliza la misma función que escribe en archivos. Se emplea para mostrar las estadísticas del reporte.
PausarPantalla
PausarPantalla PROC NEAR
push ax
IMPRIMIR msg_pausa
mov ah, 08h
int 21h
pop ax
ret
PausarPantalla ENDP
Muestra «Presione una tecla para continuar…» y con la función 08h espera a que el usuario pulse una tecla (sin mostrarla en pantalla). Es lo que permite leer el resultado antes de volver al menú.
SonidoError y SonidoExito
SonidoError PROC NEAR
push ax
push dx
mov ah, 02h
mov dl, 07h
int 21h
pop dx
pop ax
ret
SonidoError ENDP
Ambos procedimientos son idénticos: imprimen el carácter 07h, conocido como BEL (bell/campana). Ese carácter no dibuja nada; en su lugar hace sonar un pitido en el altavoz. Se usan para dar retroalimentación audible: un pitido al equivocarse y otro al guardar con éxito.
archivos.inc — trabajar con archivos de disco
Aquí están los procedimientos que crean, abren, escriben, leen y cierran archivos usando int 21h. Es donde el programa vuelve persistentes los datos.
EscribirBytes — el ladrillo de toda escritura
EscribirBytes PROC NEAR
push ax
mov ah, 40h
int 21h
jc ErrorEscribirBytes
cmp ax, cx
jne ErrorEscribirBytes
clc
jmp FinEscribirBytes
ErrorEscribirBytes:
stc
FinEscribirBytes:
pop ax
ret
EscribirBytes ENDP
Recibe tres cosas: BX = manejador del archivo, DX = dónde están los datos, CX = cuántos bytes escribir. Llama a la función 40h de DOS y comprueba dos posibles fallos: que DOS reporte error (jc) o que haya escrito menos bytes de los pedidos (cmp ax, cx; DOS deja en AX cuántos escribió realmente). Si algo falla, enciende CF; si todo va bien, lo apaga. Todas las escrituras del proyecto pasan por aquí, lo que centraliza la detección de errores.
EscribirNumeroArchivo
EscribirNumeroArchivo PROC NEAR
push ax
push cx
push dx
call NumeroATexto
call EscribirBytes
pop dx
pop cx
pop ax
ret
EscribirNumeroArchivo ENDP
Escribe un número en un archivo como texto legible. Combina dos piezas ya vistas: NumeroATexto lo convierte a dígitos (dejando DX y CX listos) y EscribirBytes los graba. Así el CSV contiene «16» y no un byte binario ilegible.
AnexarAlumno — añadir un registro a CALIF.TXT
Este procedimiento guarda una línea nueva sin borrar las anteriores. La estrategia: intentar abrir el archivo; si existe, mover el puntero al final; si no existe, crearlo.
AnexarAlumno PROC NEAR
push ax
push bx
push cx
push dx
lea dx, nombre_archivo_calif
mov ax, 3D02h
int 21h
jnc ArchivoAnexoAbierto
xor cx, cx
lea dx, nombre_archivo_calif
mov ah, 3Ch
int 21h
jnc ArchivoAnexoCreado
jmp ErrorAbrirAnexo
ArchivoAnexoCreado:
mov manejador_archivo, ax
jmp ArchivoAnexoListo
ArchivoAnexoAbierto:
mov manejador_archivo, ax
mov bx, ax
mov ax, 4202h
xor cx, cx
xor dx, dx
int 21h
jnc ArchivoAnexoListo
jmp ErrorCerrarAnexo
mov ax, 3D02h abre el archivo en modo lectura/escritura. Si jnc (no hubo error), el archivo existía → saltamos a mover el puntero al final.
Si falló, el archivo no existe: mov ah, 3Ch con CX=0 lo crea vacío. Si tampoco se puede crear, error total.
Cuando el archivo ya existía, mov ax, 4202h mueve el puntero de escritura al final (offset 0 desde el final). Así lo nuevo se agrega sin pisar lo anterior. En todos los casos se guarda el manejador devuelto por DOS.
ArchivoAnexoListo:
mov bx, manejador_archivo
xor cx, cx
mov cl, byte ptr buffer_entrada[1]
lea dx, buffer_entrada[2]
call EscribirBytes
jnc NombreAnexoEscrito
jmp ErrorCerrarAnexo
NombreAnexoEscrito:
mov cx, 1
lea dx, coma
call EscribirBytes
...
mov ax, nota1
call EscribirNumeroArchivo
...
mov cx, 2
lea dx, bytes_nueva_linea
call EscribirBytes
...
mov bx, manejador_archivo
mov ah, 3Eh
int 21h
clc
jmp FinAnexarAlumno
Con el archivo listo, se escribe el registro campo por campo, en el orden exacto del formato Nombre,Nota1,Nota2,Nota3: el nombre (tomando su longitud del byte 1 del buffer), una coma, la nota 1, otra coma, la nota 2, etc., y al final el salto de línea (CR+LF). Tras cada escritura se comprueba el carry; si algo falla, se salta a cerrar el archivo y reportar error. Al terminar bien, se cierra con la función 3Eh y se marca éxito con clc. El bloque final de etiquetas de error (ErrorCerrarAnexo, ErrorAbrirAnexo) se asegura de cerrar el archivo si estaba abierto antes de devolver CF=1.
Las notas se validan antes de abrir el archivo (en RegistrarAlumno) y se guardan en variables. Así, cuando se llega aquí, ya se sabe que todo es correcto y nunca se escribe un registro a medias.
LeerLineaArchivo — leer un registro a la vez
Para procesar el reporte hay que leer CALIF.TXT línea por línea. Este procedimiento lee carácter por carácter hasta encontrar un fin de línea, sin cargar todo el archivo en memoria.
LeerLineaArchivo PROC NEAR
push bx
... (guarda registros)
ReiniciarLecturaLinea:
lea si, buffer_linea
xor di, di
mov linea_desbordada, 0
LeerCaracterLinea:
mov bx, manejador_entrada
mov ah, 3Fh
mov cx, 1
lea dx, caracter_archivo
int 21h
jnc CaracterLeido
jmp ErrorLeerLinea
CaracterLeido:
cmp ax, 0
jne CaracterDisponible
jmp FinArchivoEnLinea
CaracterDisponible:
mov al, caracter_archivo
cmp al, CR
je LeerCaracterLinea
cmp al, LF
je LineaLeidaCompleta
cmp di, MAXIMO_LINEA
jae MarcarDesbordeLinea
mov [si], al
inc si
inc di
jmp LeerCaracterLinea
Con la función 3Fh y CX=1 se lee un solo byte del archivo. DOS deja en AX cuántos bytes leyó: si es 0, se acabó el archivo.
Los caracteres CR se ignoran; un LF marca el fin de la línea. Cualquier otro carácter se guarda en buffer_linea y se incrementan los índices (SI = dónde escribir, DI = cuántos llevamos).
Si la línea supera MAXIMO_LINEA, se marca linea_desbordada y se siguen consumiendo caracteres (sin guardarlos) hasta el fin de línea, para que la siguiente lectura empiece limpia en el próximo registro.
El procedimiento devuelve en AX un código con tres significados posibles, que el generador de reportes sabrá interpretar:
| Valor de AX | Significado |
|---|---|
0 | Se llegó al fin del archivo. |
FFFFh | La línea era demasiado larga (registro inválido a omitir). |
| otro número | Longitud de la línea leída correctamente. |
Además, se saltan las líneas totalmente vacías (ReiniciarLecturaLinea) para no procesar renglones en blanco.
VerReporteFinal — mostrar FINAL.XLS en pantalla
VerReporteFinal PROC NEAR
call LimpiarPantalla
IMPRIMIR encabezado_visualizacion
lea dx, nombre_archivo_final
mov ax, 3D00h
int 21h
jnc ArchivoVistaAbierto
jmp FaltaArchivoVista
ArchivoVistaAbierto:
mov manejador_entrada, ax
BucleVisualizacion:
mov bx, manejador_entrada
mov ah, 3Fh
mov cx, TAMANO_BUFFER_VISTA
lea dx, buffer_visualizacion
int 21h
jc ErrorLecturaVista
cmp ax, 0
je CerrarVista
mov cx, ax
mov bx, 1
mov ah, 40h
int 21h
jc ErrorLecturaVista
jmp BucleVisualizacion
Abre FINAL.XLS solo para lectura (3D00h). Si no existe, muestra el error msg_falta_final y pita.
Lee el archivo en bloques de 512 bytes (más eficiente que byte a byte). DOS indica en AX cuántos bytes leyó realmente; si son 0, terminó.
Ese mismo bloque se escribe en pantalla con la función 40h y BX=1 (salida estándar). Leer en bloques permite mostrar reportes más grandes que la memoria reservada. Al terminar, cierra el archivo con 3Eh.
academico.inc — la lógica del negocio
Este archivo contiene lo que hace «académico» al sistema: validar nombres, registrar alumnos, separar los campos de una línea (parsing CSV), aplicar el cifrado César, calcular el promedio y generar el reporte con estadísticas.
ValidarNombre
ValidarNombre PROC NEAR
push bx
push cx
push si
xor cx, cx
mov cl, byte ptr buffer_entrada[1]
jcxz NombreValidadoVacio
lea si, buffer_entrada[2]
xor bl, bl
BucleValidarNombre:
cmp byte ptr [si], ','
je NombreValidadoConComa
cmp byte ptr [si], ' '
je SiguienteCaracterNombre
mov bl, 1
SiguienteCaracterNombre:
inc si
loop BucleValidarNombre
cmp bl, 0
je NombreValidadoVacio
clc
jmp FinValidarNombre
NombreValidadoVacio:
mov al, 1
stc
jmp FinValidarNombre
NombreValidadoConComa:
xor al, al
stc
Rechaza tres tipos de nombre inválido: vacíos, formados solo por espacios, y los que contienen comas (que romperían el formato CSV). Usa BL como bandera propia: empieza en 0 y se pone en 1 si aparece al menos un carácter que no sea espacio. Al final, si BL sigue en 0, el nombre estaba «vacío de contenido». Cuando falla, además pone en AL un código (1 = vacío, 0 = tenía coma) para que quien llama sepa qué mensaje mostrar. jcxz significa «salta si CX es cero», es decir, si no se escribió nada.
RegistrarAlumno — la opción 1 completa
RegistrarAlumno PROC NEAR
call LimpiarPantalla
IMPRIMIR encabezado_registro
LeerNombreAlumno:
IMPRIMIR solicitud_nombre
lea dx, buffer_entrada
call LeerEntradaBuffer
cmp byte ptr buffer_entrada[1], 0
jne VerificarNombreAlumno
IMPRIMIR msg_nombre_vacio
call SonidoError
jmp LeerNombreAlumno
VerificarNombreAlumno:
call ValidarNombre
jnc LeerNotasAlumno
cmp al, 1
je NombreAlumnoVacio
IMPRIMIR msg_nombre_invalido
call SonidoError
jmp LeerNombreAlumno
...
LeerNotasAlumno:
lea dx, solicitud_nota1
call LeerNota
mov nota1, ax
lea dx, solicitud_nota2
call LeerNota
mov nota2, ax
lea dx, solicitud_nota3
call LeerNota
mov nota3, ax
call AnexarAlumno
jc ErrorGuardarRegistro
IMPRIMIR msg_guardado
call SonidoExito
ret
Es el «director de orquesta» de la opción Registrar. Coordina piezas ya explicadas:
Pide el nombre y lo valida con ValidarNombre. Si es inválido, muestra el mensaje adecuado (vacío o con coma) y vuelve a pedirlo.
Pide las tres notas con LeerNota (que ya insiste hasta que sean válidas) y las guarda en nota1, nota2, nota3.
Llama a AnexarAlumno para grabar la línea. Si todo salió bien, muestra «Datos guardados» y hace sonar el pitido de éxito; si falló, muestra el error de escritura.
AnalizarNumeroCsv — leer una nota de la línea
AnalizarNumeroCsv PROC NEAR
push bx
push cx
xor bx, bx
xor cx, cx
AnalizarCaracterNumero:
cmp si, fin_analisis
jb CaracterNumeroDisponible
jmp NumeroTerminaEnLinea
CaracterNumeroDisponible:
mov al, [si]
cmp al, ','
jne CaracterNumeroNoEsComa
jmp NumeroTerminaEnComa
CaracterNumeroNoEsComa:
cmp al, '0'
jae LimiteInferiorNumeroValido
jmp NumeroCsvInvalido
...
CaracterNumeroEsDigito:
sub al, '0'
xor ah, ah
push ax
mov ax, bx
mov dx, 10
mul dx
mov bx, ax
pop ax
add bx, ax
cmp bx, 20
jbe NumeroAnalizadoEnRango
jmp NumeroCsvInvalido
Es el hermano de LeerNumeroBuffer, pero para leer números dentro de una línea de archivo. SI recorre la línea y se detiene al llegar a una coma o al final (fin_analisis). Acumula el número en BX con el mismo algoritmo «×10 + dígito» y verifica el rango 0–20. Al terminar, devuelve el valor en AX y en DL qué lo detuvo: 1 si fue una coma (hay más campos), 0 si fue el fin de línea (era el último campo). Esto permite al generador comprobar que la estructura del registro es correcta.
AnalizarRegistro — cifrado César y promedio
Este es el procedimiento más rico. Recibe una línea completa y la descompone: cifra el nombre, extrae las tres notas y calcula el promedio. Empecemos por el cifrado.
Es el cifrado más antiguo que se conoce: cada letra se reemplaza por la que está 3 posiciones más adelante en el alfabeto. Así A→D, B→E, C→F… Al llegar al final se «da la vuelta»: X→A, Y→B, Z→C. En este proyecto se aplica con desplazamiento 3 al nombre del alumno antes de guardarlo en el reporte.
AnalizarCaracterNombre:
cmp si, bp
jb CaracterNombreDisponible
jmp RegistroAnalizadoInvalido
CaracterNombreDisponible:
lodsb
cmp al, ','
jne NombreNoTerminado
jmp FinAnalizarNombre
NombreNoTerminado:
cmp bx, MAXIMO_NOMBRE
jb HayEspacioParaNombre
jmp RegistroAnalizadoInvalido
HayEspacioParaNombre:
cmp al, 'a'
jb IntentarMayuscula
cmp al, 'z'
ja IntentarMayuscula
add al, 3
cmp al, 'z'
jbe GuardarCaracterCifrado
sub al, 26
jmp GuardarCaracterCifrado
IntentarMayuscula:
cmp al, 'A'
jb GuardarCaracterCifrado
cmp al, 'Z'
ja GuardarCaracterCifrado
add al, 3
cmp al, 'Z'
jbe GuardarCaracterCifrado
sub al, 26
GuardarCaracterCifrado:
stosb
inc bx
jmp AnalizarCaracterNombre
lodsb carga en AL el carácter al que apunta SI y avanza SI automáticamente. Si es una coma, el nombre terminó.
Se comprueba si el carácter es minúscula (entre 'a' y 'z') o mayúscula (entre 'A' y 'Z'). Si no es una letra (por ejemplo un espacio), se deja igual.
Si es letra, add al, 3 la desplaza 3 posiciones. Si se pasa del final del alfabeto (mayor que 'z' o 'Z'), sub al, 26 «da la vuelta» restando el tamaño del alfabeto. Esto convierte x→a, y→b, z→c.
stosb guarda AL en la posición a la que apunta DI (el buffer nombre_cifrado) y avanza DI. BX cuenta cuántas letras llevamos, para no pasar de MAXIMO_NOMBRE.
Tras cifrar el nombre, se leen las tres notas y se calcula el promedio:
call AnalizarNumeroCsv ; nota 1 (debe terminar en coma)
...
mov nota1, ax
call AnalizarNumeroCsv ; nota 2 (debe terminar en coma)
...
mov nota2, ax
call AnalizarNumeroCsv ; nota 3 (debe terminar en fin de linea)
...
mov nota3, ax
mov ax, nota1
add ax, nota2
add ax, nota3
xor dx, dx
mov bx, 3
div bx
mov promedio, ax
clc
Se llama tres veces a AnalizarNumeroCsv, comprobando que las dos primeras notas terminen en coma (DL=1) y la tercera en fin de línea (DL=0). Si la estructura no cuadra, el registro se marca inválido con stc. Para el promedio se suman las tres notas y se dividen entre 3 con div bx. Como es división entera, la parte decimal se descarta (13.6 se convierte en 13): esto es un requisito del proyecto. La bandera CF distingue registro válido (clc) de inválido (stc), lo que permite saltar líneas mal formadas sin detener el reporte.
EscribirRegistroReporte
EscribirRegistroReporte PROC NEAR
...
mov bx, manejador_salida
mov cx, longitud_cifrado
lea dx, nombre_cifrado
call EscribirBytes ; nombre cifrado
...
mov ax, nota1
call EscribirNumeroArchivo ; ,nota1
... (nota2, nota3, promedio, todas separadas por comas)
cmp promedio, 10
jb EscribirEstadoReprobado
mov cx, LONGITUD_APROBADO
lea dx, texto_aprobado
call EscribirBytes
...
EscribirEstadoReprobado:
mov cx, LONGITUD_REPROBADO
lea dx, texto_reprobado
call EscribirBytes
...
FinEscribirEstado:
mov cx, 2
lea dx, bytes_nueva_linea
call EscribirBytes
Escribe en FINAL.XLS el registro ya procesado, en el formato Cifrado,N1,N2,N3,Promedio,Estado. Va campo por campo intercalando comas, igual que AnexarAlumno. La decisión clave está al final: cmp promedio, 10 — si el promedio es menor que 10, escribe «REPROBADO»; si es 10 o más, «APROBADO». Cada escritura verifica el carry, y una única línea termina con CR+LF.
GenerarReporte — la opción 2 completa
GenerarReporte PROC NEAR
call LimpiarPantalla
IMPRIMIR encabezado_reporte
lea dx, nombre_archivo_calif
mov ax, 3D00h
int 21h
jnc EntradaReporteAbierta
jmp FaltaEntradaReporte
EntradaReporteAbierta:
mov manejador_entrada, ax
lea dx, nombre_archivo_final
xor cx, cx
mov ah, 3Ch
int 21h
jnc SalidaReporteCreada
jmp ErrorCrearReporte
SalidaReporteCreada:
mov manejador_salida, ax
mov contador_total, 0
mov contador_aprobados, 0
mov contador_reprobados, 0
mov contador_invalidos, 0
IMPRIMIR msg_procesando
Abre CALIF.TXT para leer (3D00h). Si no existe, muestra «No existe CALIF.TXT» y termina.
Crea FINAL.XLS desde cero con 3Ch. Ojo: crear vacía el archivo si ya existía, para que el reporte refleje exactamente el contenido actual y no acumule resultados viejos.
Pone los cuatro contadores a cero (total, aprobados, reprobados, inválidos) antes de empezar.
BucleGenerarReporte:
call LeerLineaArchivo
jnc LineaReporteLeida
jmp ErrorEntradaSalidaReporte
LineaReporteLeida:
cmp ax, 0
jne ReporteTieneLinea
jmp ReporteGenerado
ReporteTieneLinea:
cmp ax, 0FFFFh
je ContarRegistroInvalido
call AnalizarRegistro
jnc RegistroReporteAnalizado
jmp ContarRegistroInvalido
RegistroReporteAnalizado:
call EscribirRegistroReporte
...
inc contador_total
cmp promedio, 10
jb ContarAlumnoReprobado
inc contador_aprobados
jmp BucleGenerarReporte
ContarAlumnoReprobado:
inc contador_reprobados
jmp BucleGenerarReporte
ContarRegistroInvalido:
inc contador_invalidos
jmp BucleGenerarReporte
Este es el bucle principal del reporte, que une todo lo anterior:
LeerLineaArchivo → ¿AX=0? Fin del archivo → salir del bucleAnalizarRegistro → si CF=1 (mal formado) → contar inválidaEscribirRegistroReporte → grabar la línea procesadaReporteGenerado:
mov bx, manejador_entrada
mov ah, 3Eh
int 21h
mov bx, manejador_salida
mov ah, 3Eh
int 21h
IMPRIMIR msg_reporte_correcto
call SonidoExito
IMPRIMIR titulo_estadisticas
IMPRIMIR msg_total
mov ax, contador_total
call ImprimirSinSigno
NUEVA_LINEA
...
cmp contador_invalidos, 0
je FinGenerarReporte
IMPRIMIR msg_registros_invalidos
mov ax, contador_invalidos
call ImprimirSinSigno
NUEVA_LINEA
Cuando el archivo se acaba, se cierran ambos archivos (entrada y salida) con la función 3Eh. Luego se muestran las estadísticas: total de alumnos, aprobados y reprobados, cada número impreso con ImprimirSinSigno. Solo si hubo registros inválidos se muestra esa cuarta línea (cmp contador_invalidos, 0 / je). Las etiquetas finales de error se encargan de cerrar los archivos abiertos si algo falla a mitad de camino, evitando dejar manejadores «colgando».
El usuario registra alumnos (opción 1) que se acumulan en CALIF.TXT. Al generar el reporte (opción 2), cada línea se lee, se valida, se cifra el nombre, se promedia y se clasifica, escribiendo el resultado en FINAL.XLS junto con estadísticas. La opción 3 muestra ese reporte. Cada bloque de código que has visto encaja en esa historia.
Glosario rápido
- Registro
- Pequeño almacén interno del procesador (AX, BX, CX, DX…).
- Buffer
- Zona de memoria reservada para guardar datos temporales.
- Manejador
- Número que DOS asigna a un archivo abierto para poder usarlo.
- Interrupción
- Petición de un servicio al sistema (DOS con int 21h, BIOS con int 10h).
- Carry (CF)
- Bandera usada como semáforo de éxito (0) / error (1).
- Procedimiento
- Subrutina reutilizable (PROC/ENDP) que se llama con call.
- Macro
- Abreviatura de código que se expande al compilar.
- ASCIIZ
- Cadena de texto terminada en un byte 0.
- db / dw
- Reservar un byte / una palabra de 16 bits en memoria.
- EQU
- Dar nombre a un valor constante (no ocupa memoria).
- lea
- Cargar la dirección (offset) de una variable en un registro.
- CR / LF
- Los dos bytes (13 y 10) que juntos forman un salto de línea en DOS.
- Cifrado César
- Sustituir cada letra por otra 3 posiciones más adelante.
- CSV
- Texto con valores separados por comas (Comma-Separated Values).
- .COM
- Formato de programa DOS con un único segmento de 64 KB.