What is the best assembler documentation for AVR-GCC?

Ensamblador Puro AVR: Desbloquea tu Arduino

22/09/2023

Valoración: 4.21 (6038 votos)

Has dado el salto al mundo de los microcontroladores AVR con tu Arduino Uno, y si bien el IDE de Arduino y avr-gcc son herramientas fantásticas para empezar, tu curiosidad te ha llevado un paso más allá: la programación en ensamblador puro. Es una senda que, aunque desafiante por la aparente falta de documentación clara, promete un control y una comprensión sin precedentes del hardware. La frustración es comprensible; el código ensamblador generado por GCC, optimizado y a menudo ofuscado, dista mucho de ser un manual de aprendizaje.

What is the best assembler documentation for AVR-GCC?
The best documentation I could find is the avr-gcc assembler tutorial.This explains that: One can use the interrupt vectors using the avr-libc.In order to do it without external libraries, one can reference the source, as Brett Hale suggested. It mentions the avr-specific directives. It also comes with a simple example.

Este artículo es tu guía para desentrañar los misterios del ensamblador AVR, utilizando las herramientas de la suite avr-gcc, específicamente el ensamblador `avr-as`. Olvídate del ensamblador en línea en C; aquí nos centraremos en escribir código puramente en ensamblador, con el objetivo de dotarte del conocimiento para interactuar directamente con el ATmega328P.

Índice de Contenido

¿Por Qué Ensamblador Puro en AVR?

Antes de sumergirnos en los detalles técnicos, es importante entender por qué alguien querría programar en ensamblador puro, especialmente cuando lenguajes de alto nivel como C ofrecen mayor abstracción y portabilidad:

  • Control Absoluto: El ensamblador te da un control granular sobre cada ciclo de reloj, cada bit en los registros y cada byte en la memoria. Esto es crucial para tareas de temporización precisas o para interactuar con hardware específico.
  • Optimización Extrema: Puedes escribir código que sea increíblemente compacto y rápido, superando a menudo lo que un compilador automático puede lograr. Esto es vital para sistemas embebidos con recursos muy limitados.
  • Comprensión Profunda: Aprender ensamblador te obliga a entender la arquitectura del microcontrolador a un nivel fundamental: cómo funcionan los registros, la pila, la memoria y las instrucciones. Este conocimiento es invaluable para la depuración y la optimización de cualquier código, incluso en C.
  • Acceso a Características Específicas: Algunas características muy específicas del hardware pueden ser más fáciles o eficientes de manipular directamente con instrucciones de ensamblador que a través de las abstracciones de un lenguaje de alto nivel.

Por supuesto, el ensamblador tiene sus desventajas: es más lento de escribir, más propenso a errores, y mucho menos portable entre diferentes arquitecturas de microcontroladores.

La Anatomía de un Programa Ensamblador AVR con `avr-as`

Un programa ensamblador para AVR consta de varias partes, que el ensamblador `avr-as` y el enlazador `avr-ld` procesarán para crear el archivo ejecutable final. A diferencia del C, donde el compilador se encarga de gran parte de la estructura, en ensamblador tú eres el arquitecto.

Estructura Básica y Directivas Fundamentales

Los archivos ensamblador (.S o .asm) contienen instrucciones y directivas. Las instrucciones son los comandos que el microcontrolador ejecutará (ej. `ldi`, `out`, `rjmp`). Las directivas son instrucciones para el ensamblador mismo, que controlan cómo se organiza el código y los datos.

Aquí están algunas de las directivas más importantes que encontrarás y utilizarás:

  • `.device `: Aunque a menudo se especifica el MCU (`-mmcu=atmega328p`) en la línea de comandos de `avr-gcc` o `avr-as`, esta directiva puede usarse para especificar el tipo de microcontrolador. Esto permite que el ensamblador conozca los registros de E/S y las características específicas del dispositivo.
  • `.org
    `:

    Esta es una directiva crucial. Le dice al ensamblador que el siguiente código o dato debe colocarse en la dirección de memoria especificada. Es fundamental para los vectores de interrupción y para el código de inicio.

  • `.global `: Declara un símbolo (una etiqueta) como global, haciéndolo visible para el enlazador. Esto es necesario si quieres que una función o variable definida en este archivo sea accesible desde otros archivos ensamblador o C.
  • `.section `: Define una nueva sección o cambia a una existente. Las secciones más comunes son `.text` (para código ejecutable), `.data` (para datos inicializados), `.bss` (para datos no inicializados) y `.rodata` (para datos de solo lectura).
  • `.byte`, `.word`, `.long`, `.ascii`, `.asciz`: Directivas para definir datos.
    • `.byte`: Define uno o más bytes.
    • `.word`: Define una o más palabras (2 bytes).
    • `.long`: Define uno o más largos (4 bytes).
    • `.ascii`: Define una cadena de caracteres sin terminador nulo.
    • `.asciz`: Define una cadena de caracteres terminada en nulo (Z de 'zero').
  • `.equ , `: Define un símbolo con un valor constante. Similar a `#define` en C. Útil para dar nombres significativos a direcciones de puertos o bits.
  • `.set , `: Similar a `.equ`, pero el valor de `symbol` puede ser redefinido más adelante.
  • `.include `: Incluye el contenido de otro archivo en el punto actual. Útil para organizar código o para incluir definiciones de registros de E/S.
  • `.macro `, `.endmacro`: Permite definir macros, que son fragmentos de código que pueden ser reutilizados con diferentes parámetros.

Además, AVR-GCC a menudo utiliza secciones específicas para AVR, como `.dseg` (sección de datos), `.cseg` (sección de código) y `.eseg` (sección de EEPROM), que son equivalentes a `.section .data`, `.section .text` y `.section .eeprom` respectivamente.

Vectores de Interrupción en Ensamblador AVR

Los vectores de interrupción son una tabla de punteros (direcciones de memoria) que el microcontrolador consulta cuando ocurre un evento específico (como un reinicio, un desbordamiento de temporizador o una interrupción externa). En el ATmega328P, estos vectores se encuentran al inicio de la memoria Flash.

Para instruir a `avr-as` a colocar el código en los vectores de interrupción, se utiliza la directiva `.org`. El primer vector es siempre el vector de reinicio (Reset Vector), que es la dirección donde el programa comienza a ejecutarse después de un reinicio. Para el ATmega328P, el vector de reinicio está en la dirección `0x0000` (o `0x0000` para dispositivos pequeños con menos de 8KB de Flash y `0x0000` o `0x0000` para dispositivos con más de 8KB de Flash, pero para el Uno y el ATmega328P, es `0x0000`).

La tabla de vectores para el ATmega328P está definida en la hoja de datos del microcontrolador. Cada vector ocupa 2 bytes y contiene una instrucción de salto (`jmp` o `rjmp`) a la rutina de servicio de interrupción (ISR) correspondiente.

Un ejemplo de cómo estructurar los vectores:

.org 0x0000 ; Vector de Reset rjmp RESET_HANDLER ; Salta a la rutina de inicio .org 0x0002 ; Vector de Interrupción Externa 0 (INT0) rjmp EXT_INT0_HANDLER .org 0x0004 ; Vector de Interrupción Externa 1 (INT1) rjmp EXT_INT1_HANDLER ; ... y así sucesivamente para todos los vectores de interrupción ; Luego, define tus rutinas de servicio (handlers) RESET_HANDLER: ; Código de inicialización ; ... jmp MAIN_LOOP ; Salta al bucle principal EXT_INT0_HANDLER: ; Código para la interrupción INT0 reti ; Retorna de la interrupción ; ... otras rutinas de servicio 

La instrucción `rjmp` (relative jump) es un salto de 2 bytes, útil para saltos dentro de un rango limitado. Si tu ISR es muy grande o está lejos, podrías necesitar `jmp` (jump), que es un salto absoluto de 4 bytes.

Es crucial que cada rutina de servicio de interrupción termine con la instrucción `reti` (return from interrupt), que no solo salta de vuelta al programa principal, sino que también habilita globalmente las interrupciones (restablece el bit I en SREG).

Secciones de Datos: `.data`, `.bss` y `.rodata`

Comprender cómo se manejan las diferentes secciones de datos es fundamental para la gestión de memoria en ensamblador. El enlazador (`avr-ld`) es el que finalmente decide dónde se colocarán estas secciones en la memoria del microcontrolador.

  • `.data` (Datos Inicializados): Esta sección contiene variables que tienen un valor inicial predefinido en el código. En los microcontroladores AVR, estos datos se almacenan en la memoria Flash (junto con el código del programa) como parte del archivo .hex. Durante el proceso de inicio del microcontrolador (generalmente manejado por el código de reinicio o un pequeño fragmento de C), estos datos se copian de la Flash a la memoria RAM. Esto es necesario porque la Flash es de solo lectura durante la ejecución normal, y las variables deben ser modificables.
  • `.bss` (Block Started by Symbol - Datos No Inicializados): Esta sección contiene variables que no tienen un valor inicial explícito en el código. El sistema de inicio del microcontrolador (o el propio ensamblador/enlazador, dependiendo de la configuración) se encarga de inicializar estas variables a cero en la memoria RAM antes de que comience la ejecución del programa principal. Las variables `.bss` no ocupan espacio en el archivo .hex de Flash; solo reservan espacio en la RAM. Esto ahorra memoria Flash si tienes muchas variables que no necesitan un valor inicial específico.
  • `.rodata` (Read-Only Data - Datos de Solo Lectura): Esta sección se utiliza para datos que no cambiarán durante la ejecución del programa, como cadenas de texto constantes o tablas de búsqueda. Estos datos residen exclusivamente en la memoria Flash y no se copian a la RAM, lo que ahorra valiosa memoria RAM. Acceder a estos datos requiere el uso de instrucciones especiales como `lpm` (Load Program Memory) o `elpm` (Extended Load Program Memory), ya que están en un espacio de memoria diferente al de la RAM.

La ubicación física de estas secciones en la memoria RAM (para `.data` y `.bss`) y Flash (para `.data`, `.rodata` y `.text`) se define en el script del enlazador (`.ld` file), que `avr-ld` utiliza. Por defecto, avr-gcc ya tiene scripts de enlazador para los diferentes MCU que organizan estas secciones de manera apropiada.

Ejemplo de Programa: Parpadeo de un LED (ATmega328P)

Vamos a crear un ejemplo simple: un programa que haga parpadear un LED conectado al Pin 13 del Arduino Uno (que corresponde al PB5 en el ATmega328P). Este ejemplo demostrará cómo configurar un puerto, manipular bits y crear un retardo.

; archivo: blink.S ; Un programa simple en ensamblador para hacer parpadear un LED en PB5 (Pin 13 Arduino) ; para el ATmega328P .device ATmega328P ; Especifica el microcontrolador ; Definiciones de registros de E/S y bits importantes ; (Normalmente se incluiría un archivo .inc proporcionado por Microchip/Atmel) ; Para simplicidad, definimos algunos aquí: .equ DDRB, 0x04 ; Dirección del registro de dirección de datos del Puerto B .equ PORTB, 0x05 ; Dirección del registro de datos del Puerto B .equ PB5, 5 ; Bit 5 del Puerto B (Pin 13 Arduino) ; Registros de propósito general .def r16 = r16 .def r17 = r17 .def r18 = r18 .def r19 = r19 .def r20 = r20 ; ==================================================================== ; VECTORES DE INTERRUPCIÓN ; La tabla de vectores de interrupción debe estar al principio de la Flash. ; Para el ATmega328P, el vector de reset es 0x0000. ; ==================================================================== .org 0x0000 rjmp RESET_HANDLER ; Salta a la rutina de inicio tras el reset ; Puedes listar otros vectores aquí si los usas, por ejemplo: ; .org 0x0002 ; rjmp EXT_INT0_HANDLER ; ==================================================================== ; RUTINA DE INICIO (RESET_HANDLER) ; Se ejecuta al encender o reiniciar el microcontrolador. ; ==================================================================== RESET_HANDLER: ; Configurar la pila (Stack Pointer) ; El SP debe apuntar al final de la RAM. ; Para el ATmega328P, RAMEND está en 0x08FF. ldi r16, low(RAMEND) ; Cargar el byte bajo de RAMEND out SPH, r16 ; Ponerlo en SPH (byte alto) ldi r16, high(RAMEND) ; Cargar el byte alto de RAMEND out SPL, r16 ; Ponerlo en SPL (byte bajo) ; Nota: SPH/SPL son las direcciones 0x3E y 0x3D respectivamente en el espacio de E/S. ; Configurar PB5 como salida ; DDRB (Data Direction Register B) controla la dirección (entrada/salida) de los pines del Puerto B. ; Poner un '1' en el bit PB5 de DDRB lo configura como salida. sbi DDRB, PB5 ; Set Bit in I/O Register: Pone a '1' el bit PB5 del registro DDRB ; Saltar al bucle principal jmp MAIN_LOOP ; ==================================================================== ; BUCLE PRINCIPAL (MAIN_LOOP) ; El código que se ejecuta continuamente. ; ==================================================================== MAIN_LOOP: ; Encender el LED (PB5) ; PORTB controla el estado de los pines (alto/bajo) cuando son salidas. ; Poner un '1' en el bit PB5 de PORTB enciende el LED. sbi PORTB, PB5 ; Set Bit in I/O Register: Pone a '1' el bit PB5 del registro PORTB ; Esperar un tiempo (retardo) rcall DELAY_MS_250 ; Llamar a la subrutina de retardo (aproximadamente 250 ms) ; Apagar el LED (PB5) cbi PORTB, PB5 ; Clear Bit in I/O Register: Pone a '0' el bit PB5 del registro PORTB ; Esperar otro tiempo (retardo) rcall DELAY_MS_250 ; Llamar a la subrutina de retardo (aproximadamente 250 ms) ; Volver al inicio del bucle rjmp MAIN_LOOP ; ==================================================================== ; SUBRUTINA DE RETARDO (DELAY_MS_250) ; Retardo aproximado de 250 ms (para CPU a 16MHz). ; Este es un retardo de software rudimentario, bloqueante y no preciso. ; Utiliza bucles anidados. ; ==================================================================== DELAY_MS_250: ; Cargar valores para los contadores de bucle ldi r16, 250 ; Contador externo (aproximadamente milisegundos) DELAY_MS_LOOP: ldi r17, high(2000) ; Contador interno alto (ajuste para 16MHz) ldi r18, low(2000) ; Contador interno bajo DELAY_INNER_LOOP: ; Cada instrucción toma 1 ciclo de reloj (la mayoría de AVR) ; d_r17:d_r18 es un contador de 16 bits sbiw r17:r18, 1 ; Resta 1 a r17:r18 (2 ciclos) brne DELAY_INNER_LOOP ; Salta si no es cero (1/2 ciclos) dec r16 ; Decrementa el contador externo (1 ciclo) brne DELAY_MS_LOOP ; Salta si no es cero (1/2 ciclos) ret ; Retorna de la subrutina (4 ciclos) ; ==================================================================== ; SECCIONES DE DATOS ; Aquí se definirían las secciones .data, .bss, .rodata si fueran necesarias. ; Para este ejemplo simple, no tenemos variables globales inicializadas o no inicializadas, ; ni datos de solo lectura explícitos. ; ==================================================================== .data ; Ejemplo: my_variable: .byte 0xAA .bss ; Ejemplo: my_uninitialized_var: .byte 0x00 .rodata ; Ejemplo: my_string: .asciz "Hola Mundo" 

Compilación y Enlace del Ejemplo

Para compilar este código, necesitarás la suite avr-gcc. Asumiendo que tu archivo se llama `blink.S`:

  1. Ensamblar: Convierte el código ensamblador en un archivo objeto. `avr-as -mmcu=atmega328p -o blink.o blink.S`
  2. Enlazar: Convierte el archivo objeto en un archivo ejecutable ELF. `avr-ld -mmcu=atmega328p -o blink.elf blink.o`
  3. Crear archivo .hex: El archivo .hex es el que se carga al microcontrolador. `avr-objcopy -O ihex -R .eeprom blink.elf blink.hex`

Luego, puedes usar `avrdude` para cargar `blink.hex` a tu Arduino Uno.

Tabla Comparativa: Ensamblador Puro vs. C para AVR

CaracterísticaEnsamblador PuroLenguaje C (con avr-gcc)
Nivel de AbstracciónMuy bajo (directo al hardware)Alto (más portable, menos detalles)
Control de HardwareMáximo (cada bit, cada ciclo)Alto (a través de registros, librerías)
Rendimiento/VelocidadPotencialmente el más rápido y compactoMuy bueno, pero depende del optimizador
Tamaño del CódigoPotencialmente el más pequeñoGeneralmente mayor, salvo optimización extrema
Curva de AprendizajeEmpinada (requiere entender la arquitectura)Moderada (más fácil de empezar)
Tiempo de DesarrolloMás lento y propenso a erroresMás rápido, menos errores lógicos
DepuraciónMás compleja (registros, memoria)Más fácil (depuradores simbólicos)
PortabilidadMuy baja (específico del MCU)Alta (entre arquitecturas similares)
Uso TípicoControl crítico, rutinas de bajo nivel, bootloadersAplicaciones generales, la mayoría de proyectos

Preguntas Frecuentes (FAQ)

¿Cómo accedo a los registros de E/S (SFRs) en ensamblador?

Los Registros de Funciones Especiales (SFRs) como `DDRB` o `PORTB` se acceden mediante las instrucciones `in` y `out` para los registros que están en el espacio de E/S. Para manipular bits individuales, las instrucciones `sbi` (Set Bit in I/O Register) y `cbi` (Clear Bit in I/O Register) son increíblemente útiles, ya que operan directamente sobre un bit específico de un registro de E/S sin tener que leer, modificar y escribir el byte completo.

¿Cómo gestiono la pila (Stack) en ensamblador?

La pila se utiliza para almacenar temporalmente datos y direcciones de retorno de subrutinas. El puntero de pila (SP) es un registro de 16 bits (`SPH:SPL`) que apunta a la parte superior de la pila en la RAM. Las instrucciones `push` (empujar un registro a la pila) y `pop` (sacar un valor de la pila a un registro) se usan para guardar y restaurar registros. Las instrucciones `call` y `rcall` guardan la dirección de retorno en la pila antes de saltar a una subrutina, y `ret` y `reti` recuperan esa dirección para volver.

¿Puedo mezclar código C y ensamblador puro?

Sí, es posible. Puedes escribir funciones específicas en ensamblador y llamarlas desde tu código C, o viceversa. Para que esto funcione, necesitas asegurarte de que las convenciones de llamada (cómo se pasan los argumentos y se devuelven los valores) sean consistentes entre el C y el ensamblador. avr-gcc tiene una convención de llamada bien definida que puedes consultar si decides ir por este camino, aunque tu objetivo es el ensamblador puro.

¿Dónde encuentro la hoja de datos del ATmega328P?

La hoja de datos (datasheet) es tu biblia. Es un documento técnico extenso (cientos de páginas) proporcionado por Microchip (anteriormente Atmel) que detalla cada aspecto del microcontrolador: arquitectura, registros, temporizadores, interrupciones, modos de operación, etc. Buscar "ATmega328P datasheet" en Google te llevará directamente a ella. Es esencial para entender las direcciones de los registros, la función de cada bit y el comportamiento del hardware.

¿Cómo depuro código ensamblador en AVR?

Depurar ensamblador puede ser un desafío. Puedes usar simuladores de AVR (como el que viene con Atmel Studio, si lo usas en Windows) para ejecutar tu código paso a paso y examinar el estado de los registros y la memoria. Para depuración en hardware real, necesitarás un programador/depurador compatible con AVR (como un AVR Dragon, Atmel ICE, o incluso un Arduino como depurador si lo configuras para debugWIRE) y una herramienta como `avarice` (parte de avr-gdb) para interactuar con él.

Dominar el ensamblador puro para AVR es un viaje que te transformará en un verdadero conocedor del hardware. Si bien es un camino con sus desafíos, la recompensa en términos de comprensión, control y optimización es inmensa. Con la hoja de datos del ATmega328P a tu lado y las herramientas de avr-gcc, tienes todo lo necesario para empezar a escribir código que respira y vive directamente en el corazón de tu microcontrolador.

Si quieres conocer otros artículos parecidos a Ensamblador Puro AVR: Desbloquea tu Arduino puedes visitar la categoría Librerías.

Subir