martes, 20 de octubre de 2026

¿Ensamblador?¿Obsoleto?

Programación en ensamblador de microcontroladores PIC: por qué todavía importa



En una época dominada por Arduino, ESP32 y lenguajes de alto nivel, hablar de ensamblador puede parecer hablar de arqueología informática. Y sin embargo, en el mundo de los microcontroladores (y en particular en la familia PIC de Microchip) el ensamblador sigue teniendo un papel importante. Yo reconozco que cuando lo vi en el temario de la asignatura de la Universidad... Pensé... ¿Estamos locos? Con lo maravilloso que es Python o lo fácil que puede ser programar un Arduino. xD

En esta entrada vamos a ver por qué merece la pena aprenderlo, qué son los registros de un PIC explicados de forma sencilla, y cómo se estructura un programa en ensamblador con varios ejemplos comentados línea a línea.

¿Por qué aprender ensamblador si "está obsoleto"?

Es cierto que hoy en día casi nadie desarrolla aplicaciones completas en ensamblador. Compiladores de C, C++ e incluso Python (con MicroPython) generan código lo bastante bueno para la mayoría de usos. Pero el ensamblador conserva ventajas que ningún lenguaje de alto nivel te ofrece:

  • Entiendes realmente cómo funciona el microcontrolador. Con ensamblador no hay capas de abstracción: tú mueves los datos entre registros, tú activas los bits de configuración, tú controlas cada ciclo de instrucción. Aprendes qué hace la máquina, no solo qué hace el compilador.

  • Control absoluto del tiempo de ejecución. En un PIC cada instrucción tarda 4 ciclos de reloj (1 µs a 4 MHz, salvo saltos que tardan el doble). Eso permite temporizaciones exactísimas, algo crítico en comunicaciones como RS-232 "a mano" o generación de señales.

  • Eficiencia máxima en recursos. Los PIC más pequeños (10F, 12F) tienen muy poca memoria flash y RAM. En ensamblador el programa es tan pequeño como tú lo escribas; no hay librerías ni código de arranque innecesario.

  • Es la mejor herramienta didáctica. Para quien estudia electrónica o informática industrial, el ensamblador conecta el hardware con el software: registros, interrupciones, timers, puertos… todo queda a la vista.

  • Depuración y trabajo a bajo nivel. Cuando hay que analizar o parchear código máquina, entender ensamblador es imprescindible.

En resumen: no es el lenguaje más productivo, pero es el que más te enseña y el que más control te da.

Los registros de un PIC, explicados de forma sencilla

Puedes imaginar un microcontrolador PIC como una pequeña oficina: dentro hay cajones (los registros) donde se guardan temporalmente datos e instrucciones, y unas notitas pegadas en cada cajón (los bits de configuración) que dicen qué hace cada cosa.


Un registro no es más que una memoria muy rápida, normalmente de 8 bits, que está dentro del propio chip. Cada registro tiene una dirección y una función.

En un PIC hay dos tipos de registros:

  • SFR (Special Function Registers, registros de función especial): son los "cajones oficiales" del chip, con una misión concreta asignada por el fabricante. Cada bit tiene un significado: configurar un puerto, arrancar un timer, activar una interrupción… No los eliges tú: vienen fijos de fábrica en direcciones concretas de la RAM. Los de la tabla de abajo son todos SFR.

  • GPR (General Purpose Registers, registros de propósito general): son los "cajones libres": posiciones de RAM sin función asignada que tú usas como variables del programa. Cuando en un ejemplo reservamos Contador con CBLOCK, lo que hacemos es darle nombre a un GPR. A partir de ahí, es solo un cajón donde guardas y lees datos.

Dicho de otra forma: los SFR son el panel de control del microcontrolador (tú escribes en ellos para manejar el hardware) y los GPR son tu block de notas (tú decides qué guardar en cada uno). Físicamente ambos viven en la misma RAM, solo se distinguen por su función y dirección.

Los SFR más importantes:

Registro

Nombre

¿Para qué sirve?

W

Working register

El "cajón" de trabajo. Casi todas las operaciones pasan por él. Es como la calculadora de mano del micro.

STATUS

Estado

Guarda banderas (flags) que informan del resultado de las operaciones: si hubo acarreo (C), si el resultado fue cero (Z), etc.

PORTA / PORTB

Puertos de E/S

Reflejan el estado físico de los pines: escribir en ellos pone pines a 1 o 0; leerlos te dice si hay tensión en cada pin.

TRISA / TRISB

Dirección de los pines

Configuran cada pin como entrada (1) o salida (0). "TRIS" viene de triestado.

OPTION_REG

Opciones

Configura el temporizador TMR0, el prescaler y las interrupciones por flanco en RB0.

INTCON

Control de interrupciones

Activa/desactiva interrupciones y guarda sus flags.

TMR0

Temporizador 0

Un contador que se incrementa solo y permite medir tiempos.

FSR / INDF

Puntero indirecto

Sirven para acceder a posiciones de memoria RAM "por dirección", como un puntero.

Dos ideas clave para entenderlo todo:

  1. Todo en un PIC son registros. Leer un pin, mandar un dato por el puerto serie o cambiar la velocidad de un timer se hace siempre igual: escribiendo o leyendo bits de un registro.

  2. El registro W es el protagonista. Cargar un dato en W (MOVLW), moverlo a un registro (MOVWF), sumarlo (ADDWF)… el flujo normal de un programa en ensamblador es un ir y venir de datos entre W y el resto de registros.

En la siguiente tabla se puede ver el resumen de los registros de función especial(SFR) del PIC16F84A(extraído del datasheet)



Las directivas principales del ensamblador (MPASM)

Las directivas no son instrucciones del PIC: son órdenes que se le dan al ensamblador (MPASM, el de Microchip) para que prepare el código. Las más usadas:

  • LIST P=16F84A — Indica el modelo de PIC para el que se ensambla.

  • #INCLUDE <P16F84A.INC> — Incluye el fichero de Microchip con los nombres de todos los registros y bits, para no tener que memorizar direcciones.

  • __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF — Graba los fusibles de configuración: tipo de oscilador, watchdog desconectado, etc.

  • ORG 0x00 — Indica en qué dirección de memoria empieza el código (0x00 es el vector de reset; 0x04, el de interrupción).

  • EQU — Define constantes con nombre (por ejemplo, para dar nombre a una variable).

  • CBLOCK ... ENDC — Reserva posiciones de RAM para variables.

  • END — Marca el fin del código fuente.

Además, conviene recordar los modos de direccionamiento de las instrucciones, porque en los ejemplos aparecen constantemente:

  • W → el destino es el registro W (ej.: ADDWF REG, W guarda el resultado en W).

  • F → el destino es el propio registro (ej.: MOVWF PORTB).

  • LW → "literal en W": el dato viaja directamente (ej.: MOVLW 0xFF).

  • Etiquetas → direcciones de salto (ej.: GOTO Bucle).


En la siguiente imagen se puede ver un ejemplo de una práctica que tuve que realizar empleando MPLAB X(uso V5.35 por estabilidad en mi pc)



Las instrucciones más comunes del ensamblador PIC

Un PIC16F solo entiende 35 instrucciones, pero con menos de 20 se escribe el 90 % de los programas. Estas son las que usarás a diario, agrupadas por lo que hacen:

Mover datos:

  • MOVLW → MOVe Literal to W. Carga una constante en W. Ej.: MOVLW d'255'.

  • MOVWF → MOVe W to File. Copia W a un registro. Ej.: MOVWF PORTB.

  • MOVF → MOVe File. Copia un registro a W (MOVF Contador, W) o a sí mismo. Es la forma de leer variables.

  • SWAPF → SWAP File. Intercambia los dos nibbles (4 bits) de un registro. Su uso estrella es restaurar W sin tocar las banderas.

Aritmética y lógica:

  • ADDWF → ADD W to File. Suma W a un registro.

  • SUBLW → SUBtract W from Literal. Resta W de una constante (guardando el resultado en W).

  • ANDWF / IORWF / XORWF → Operaciones lógicas AND, OR y XOR entre W y un registro. Imprescindibles para poner, borrar o invertir bits concretos.

  • INCF / DECF → INCrement / DECrement File. Suman o restan 1 a un registro.

  • COMF → COMplement File. Invierte todos los bits de un registro.

  • RLF / RRF → Rotate Left/Right through Carry. Rotan los bits a izquierda o derecha pasando por la bandera C (multiplicar/dividir por 2, registros de desplazamiento).

Manipular bits:

  • BSF → Bit Set File. Pone a 1 un bit de un registro. Ej.: BSF PORTB, 0.

  • BCF → Bit Clear File. Pone a 0 un bit. Ej.: BCF STATUS, RP0.

  • BTFSC → Bit Test File, Skip if Clear. Si el bit vale 0, salta la instrucción siguiente (equivale a "si… entonces").

  • BTFSS → Bit Test File, Skip if Set. Lo mismo pero al revés: salta si el bit vale 1.

Saltos y control del programa:

  • GOTO → GO TO. Salto incondicional a una etiqueta. Con él se construyen los bucles.

  • CALL / RETURN → Llaman a una subrutina y vuelven de ella.

  • RETFIE → RETurn From Interrupt and Enable. Vuelve de una interrupción reactivándolas.

  • DECFSZ → DECrement File, Skip if Zero. Resta 1 y salta la siguiente instrucción si el resultado es cero. Es LA instrucción de los bucles de repetición y los retardos.

  • INCFSZ → Igual que la anterior pero sumando 1.

  • CLRW / CLRF → Ponen a 0 el registro W o un registro completo (ej.: CLRF TRISB).

  • NOP → No OPeration. No hace nada; sirve para perder un ciclo, como hueco o para temporizaciones finas.

Truco para recordarlas: el nombre casi siempre es la contracción de su significado en inglés (BSF = Bit Set File, DECFSZ = DECrement File Skip if Zero). Cuando interiorices esa lógica, el instruction set se lee solo.


_________________________________________________________________________________

Unos ejemplillos comentados línea a línea

Todos los ejemplos son para el PIC16F84A, un clásico de aprendizaje. Fíjate en cómo cada instrucción hace una sola cosa muy simple: la potencia viene de combinarlas.

Ejemplo 1: Hola mundo — encender un LED

    LIST    P=16F84A            ; Indicamos el modelo de PIC
    #INCLUDE <P16F84A.INC>      ; Nombres de registros y bits
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF
                                ; Oscilador a cristal, watchdog OFF

    ORG     0x00                ; El programa empieza en la dirección 0
    BSF     STATUS, RP0         ; Paso al banco 1 de memoria (RP0=1)
    MOVLW   b'11111110'         ; Cargo en W el valor 11111110 (RB0 salida)
    MOVWF   TRISB               ; Lo copio a TRISB: RB0 salida, resto entradas
    BCF     STATUS, RP0         ; Vuelvo al banco 0 (donde está PORTB)
    BSF     PORTB, 0            ; Pongo RB0 a "1": el LED se enciende
Bucle:
    GOTO    Bucle               ; Bucle infinito: no hacemos nada más

    END                         ; Fin del código fuente

Qué hace: configura RB0 como salida y lo pone a 1, dejando el LED encendido para siempre. Nótese que b'11111110' es binario: el bit 0 vale 0, y en TRIS un 0 significa salida.

Ejemplo 2: Parpadeo de un LED

    LIST    P=16F84A
    #INCLUDE <P16F84A.INC>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF

    ORG     0x00
    BSF     STATUS, RP0         ; Banco 1
    MOVLW   b'11111110'         ; RB0 como salida
    MOVWF   TRISB
    BCF     STATUS, RP0         ; Banco 0
Parpadeo:
    BSF     PORTB, 0            ; LED ON
    CALL    Retardo             ; Espero un tiempo (subrutina)
    BCF     PORTB, 0            ; LED OFF
    CALL    Retardo             ; Espero otro rato
    GOTO    Parpadeo            ; Repito eternamente

Retardo:                        ; --- Subrutina de retardo ---
    MOVLW   d'255'              ; W = 255 (d'255' = 255 en decimal)
    MOVWF   0x0C                ; Lo guardo en una posición de RAM (contador)
Decremento:
    DECFSZ  0x0C, F            ; Resto 1 al contador; si da 0, salto la
                                ; siguiente instrucción (SZ = skip if zero)
    GOTO    Decremento         ; Todavía no es cero: sigo restando
    RETURN                      ; Contador a cero: vuelvo al programa principal

    END

¿Qué hace este programilla? el LED parpadea. El truco del retardo es DECFSZ: resta 1 bit y salta la instrucción siguiente si el resultado es cero. Es una de las instrucciones más usadas del PIC.

Ejemplo 3: Leer un botón y actuar sobre un LED

    LIST    P=16F84A
    #INCLUDE <P16F84A.INC>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF

    ORG     0x00
    BSF     STATUS, RP0         ; Banco 1
    MOVLW   b'11111110'         ; RB0 salida (LED)...
    MOVWF   TRISB              ; ...y RB1..RB7 de entrada (ahí irá el botón)
    MOVLW   b'00010000'         ; RA4 como entrada (ahí conecto el botón)
    MOVWF   TRISA
    BCF     STATUS, RP0         ; Banco 0
Espera:
    BTFSC   PORTA, 4            ; "Bit Test File, Skip if Clear":
                                ; si RA4 es 0 (botón pulsado),
                                ; salto la siguiente instrucción
    GOTO    Espera              ; RA4 sigue a 1 (botón sin pulsar): sigo esperando
    BSF     PORTB, 0            ; Botón pulsado: enciendo el LED
Suelta:
    BTFSS   PORTA, 4            ; "Skip if Set": si RA4 vuelve a 1
                                ; (botón soltado), salto la siguiente
    GOTO    Suelta               ; Sigue pulsado: espero
    BCF     PORTB, 0            ; Botón soltado: apago el LED
    GOTO    Espera              ; Vuelvo a esperar otra pulsación

    END

¿Qué hace este nuevo programa?: espera a que pulses el botón conectado a RA4 para encender el LED, y lo apaga al soltarlo. BTFSC y BTFSS son las dos instrucciones de test de bits, las "responsables" de casi toda la toma de decisiones en ensamblador.

Ejemplo 4: Contador en el puerto B (8 LEDs en binario)

    LIST    P=16F84A
    #INCLUDE <P16F84A.INC>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF

    CBLOCK  0x0C                ; A partir de la dirección 0x0C de RAM...
    Contador                   ; ...reservo una variable llamada "Contador"
    GuardaRet                  ; ...y otra para guardar contadores de retardo
    ENDC

    ORG     0x00
    BSF     STATUS, RP0         ; Banco 1
    CLRF    TRISB               ; TRISB = 0: todo el puerto B como salida
    BCF     STATUS, RP0         ; Banco 0
    CLRF    Contador            ; Contador = 0
Bucle:
    MOVF    Contador, W         ; Copio Contador a W (MOVF mueve de registro a W)
    MOVWF   PORTB              ; Lo saco por el puerto B: 8 LEDs muestran
                                ; el valor en binario
    INCF    Contador, F        ; Contador = Contador + 1 (INCrement File)
    CALL    Retardo             ; Espero un poco para que se vea
    GOTO    Bucle               ; Siguiente valor

Retardo:
    MOVLW   d'50'               ; Bucle doble para alargar la espera
    MOVWF   GuardaRet
Bucle1:
    MOVLW   d'255'
    MOVWF   Contador            ; Reutilizo Contador como variable interna
Bucle2:
    DECFSZ  Contador, F        ; Bucle interno (255 vueltas)
    GOTO    Bucle2
    DECFSZ  GuardaRet, F       ; Bucle externo (50 vueltas)
    GOTO    Bucle1
    RETURN

    END

Poco a poco lo vamos complicando, ¿Que hace ahora nuestro código?: cuenta de 0 a 255 sin parar mostrando cada valor en binario sobre 8 LEDs conectados al puerto B. Fíjate en MOLF… perdón, en MOVF: mientras MOVLW carga una constante en W, MOVF copia el contenido de un registro (¡importante para leer variables!).

Ejemplo 5: Interrupción por cambio en RB0

    LIST    P=16F84A
    #INCLUDE <P16F84A.INC>
    __CONFIG _XT_OSC & _WDT_OFF & _PWRTE_ON & _CP_OFF

    CBLOCK  0x0C
    GuardaW                    ; Aquí guardaré W al entrar en la interrupción
    GuardaSTATUS               ; ...y aquí STATUS (¡imprescindible!)
    ENDC

    ORG     0x00                ; Vector de RESET: por aquí se empieza
    GOTO    Inicio              ; Salto sobre el vector de interrupción
    ORG     0x04                ; Vector de INTERRUPCIÓN: la CPU salta aquí
                                ; automáticamente cuando ocurre
    MOVWF   GuardaW             ; Salvo W (la interrupción puede llegar
                                ; en cualquier momento del programa)
    MOVF    STATUS, W           ; Copio STATUS a W...
    MOVWF   GuardaSTATUS        ; ...y lo salvo también
    BCF     INTCON, INTF        ; Borro el flag que avisó de la interrupción
                                ; (si no, saltaría eternamente)
    BTFSC   PORTB, 0            ; ¿RB0 está a 0?
    GOTO    Fin                 ; No: no hago nada
    BSF     PORTB, 7            ; Sí: enciendo el LED de RB7
Fin:
    MOVF    GuardaSTATUS, W     ; Restauro STATUS...
    MOVWF   STATUS
    SWAPF   GuardaW, F          ; SWAPF intercambia nibbles sin tocar las
    SWAPF   GuardaW, W          ; banderas: truco clásico para restaurar W
    RETFIE                     ; Retorno de interrupción (RETurn From
                                ; Interrupt and Enable)
Inicio:
    BSF     STATUS, RP0         ; --- Configuración ---
    MOVLW   b'00000001'         ; RB0 entrada, resto del puerto B salida
    MOVWF   TRISB
    BCF     OPTION_REG, INTEDG  ; Interrupción en flanco de bajada de RB0
    BCF     STATUS, RP0
    BSF     INTCON, INTE        ; Activo la interrupción de RB0
    BSF     INTCON, GIE         ; Activo las interrupciones en general
Bucle:
    GOTO    Bucle               ; El programa "duerme": todo lo hace la ISR

    END

Nuestro último programilla... el programa principal no hace nada; cuando el pin RB0 detecta un flanco de bajada (por ejemplo, un botón con lógica negativa), la CPU salta sola a la dirección 0x04, se ejecuta la rutina de interrupción y luego RETFIE devuelve el control. Observa el ritual de guardar W y STATUS al entrar y restaurarlos al salir: sin eso, la interrupción "corrompería" el programa principal. El truco de SWAPF dos veces se usa porque SWAPF no afecta a las banderas del registro STATUS.

Conclusiones

El ensamblador en los PIC no ha muerto: ha cambiado de papel. Ya no es la forma habitual de desarrollar productos, pero sigue siendo la mejor escuela posible para entender qué pasa de verdad dentro de un microcontrolador: registros, bancos de memoria, temporizaciones exactas e interrupciones sin capas de por medio. Si algún día programas en C un PIC (o cualquier otro micro), verás que lo que hace el compilador por debajo ya no será magia para ti… porque habrás escrito eso mismo a mano.

Tengo más códigos y ejemplos que puedo compartir, si necesitas mas ayuda no dudes en escribirme. Obviamente esto son ejemplos básicos, una vez que empezamos a utilizar bucles, interrupciones o "funciones" elaborar el código de control puede complicarse bastante.

 He utilizado MPLAB X para crear los códigos y compilar, si necesitas ayuda para utilizarlo no dudes en escribirme también.

¡Espero que te sirva! ¡Un saludo!

No hay comentarios:

Publicar un comentario