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
ContadorconCBLOCK, 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:
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.
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.
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, Wguarda 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).
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 fuenteQué 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
ENDPoco 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
ENDNuestro ú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