← Índice del curso

Microcontroladores · Tema 5

Entorno de trabajo: compilar, cargar y depurar

Un programa para microcontrolador se escribe en la computadora pero vive en otra máquina, sin pantalla ni teclado. Entre ambas hay una cadena de herramientas que traduce, enlaza y transporta el código hasta la Flash. Conocerla evita horas de búsqueda cuando algo no funciona.

01 · Punto de partida

Programar en una máquina y ejecutar en otra

Si desarrollaste software para servidores, móviles o contenedores, conocés la idea de compilación cruzada: el código se construye en una máquina (el host) para ejecutarse en otra (el target). Con un microcontrolador esa separación es total: el ATmega328P no tiene sistema operativo, ni consola, ni sistema de archivos. No se puede compilar en él ni abrir una terminal para ver qué pasa.

Todo el flujo de trabajo se resume en un ciclo: escribir, compilar, cargar, observar. Cuanto más rápido y fiable sea ese ciclo, más rápido aprendés. Este tema arma el ciclo completo, de la primera línea de C al LED que parpadea.

Vas a necesitar la placa y la mesa de trabajo del tema 4, y haber visto en el tema 3 que DDRB y PORTB son registros mapeados en memoria.

02 · De texto a binario

La cadena de herramientas

Una toolchain es el conjunto de programas que convierte tu código fuente en el archivo que se graba en el chip. Para AVR, la cadena habitual es avr-gcc (el compilador GCC configurado para generar código AVR) con la biblioteca estándar avr-libc.

Cadena de herramientas para un microcontrolador AVR El archivo blink.c se compila con avr-gcc y produce blink.o. El enlazador produce blink.elf. avr-objcopy produce blink.hex. avrdude lo graba en la memoria Flash del chip. blink.cblink.oblink.elfblink.hexFlash avr-gcc -cenlazadoravr-objcopyavrdude código fuentecódigo objetoprograma enlazadoformato de cargamemoria del chip
Las herramientas, en el orden en que intervienen.
HerramientaQué haceEntrada → salida
Preprocesador y compilador (avr-gcc)Expande #include y macros; traduce C a instrucciones AVR.c → .o
Enlazador (avr-ld, llamado por avr-gcc)Une los archivos objeto y las bibliotecas, y asigna direcciones en Flash y SRAM.o → .elf
avr-objcopyExtrae las secciones que van a Flash y las convierte al formato Intel HEX.elf → .hex
avr-sizeInforma cuánta Flash y cuánta SRAM ocupa el programa.elf → texto
avrdudeSe comunica con el cargador o el programador y graba la Flash.hex → chip

El enlazador cumple un papel clave en un microcontrolador: decide en qué dirección queda cada función y cada variable, usando como mapa la memoria del chip. Además agrega el código de arranque (crt) que se ejecuta antes de main(): coloca la tabla de vectores de interrupción, inicializa el puntero de pila y copia a la SRAM los valores iniciales de las variables.

03 · Dónde trabajar

Dónde escribir y compilar

La cadena de herramientas es la misma debajo de casi todos los entornos; cambia cuánto te esconden.

Opciones habituales para programar un ATmega328P.
EntornoCómo funcionaCuándo conviene
Arduino IDEEditor simple y botón de cargar; usa avr-gcc y avrdude por debajo, con bibliotecas propiasPrimeros pasos y prototipos rápidos
PlatformIOExtensión de editor con gestión de proyectos, bibliotecas y varias plataformasProyectos medianos y trabajo con varias placas
Microchip Studio o MPLAB XEntornos del fabricante con simulador y depuración integradaTrabajo profesional con depuración por hardware
Línea de comandos con MakefileSe invoca cada herramienta explícitamentePara entender el proceso y automatizar compilaciones

En este curso programamos en C con registros (avr-libc), no con las funciones de alto nivel de Arduino. Podés escribir ese C desde cualquiera de los entornos. Aprendés mucho más si, al menos una vez, ejecutás cada herramienta a mano para ver qué produce.

05 · Qué sale del compilador

Los archivos que se generan

Cada archivo cumple un papel distinto.
ArchivoContenidoPara qué sirve
.cCódigo fuenteLo escribís vos
.oCódigo máquina de un archivo, con referencias sin resolverEntrada del enlazador
.elfPrograma completo con nombres de funciones y variables, y datos para depuraciónDepurar y medir tamaño
.hexSolo los bytes que van a Flash, en texto, con direcciones y verificaciónCargar en el chip
.map (opcional)Dónde quedó cada símbolo en memoriaInvestigar el uso de memoria

El formato Intel HEX es texto plano. Cada línea es un registro con esta forma:

:LL AAAA TT DD…DD CC

donde LL es la cantidad de bytes de datos, AAAA la dirección de carga, TT el tipo (00 datos, 01 fin de archivo) y CC la suma de verificación: el complemento a dos de la suma de todos los bytes anteriores, de modo que al sumar todo el registro el resultado es cero. Así avrdude detecta un archivo dañado.

Con avr-size aparecen dos números que vas a mirar siempre. Program es lo que ocupa en Flash (código y constantes). Data es la SRAM que consumen las variables globales (.data más .bss). A esa cifra hay que sumarle la pila, que crece mientras el programa se ejecuta, así que no conviene llenar la SRAM hasta el borde. Un parpadeo mínimo con -Os ocupa unos pocos cientos de bytes de Flash y casi nada de SRAM.

06 · Hasta la Flash

Cargar el programa en el chip

Ya conocés las dos vías del tema 4. Veamos cómo funcionan.

Por cargador de arranque (bootloader)

El cargador de la Uno, Optiboot, ocupa unos 0,5 KB al final de la Flash (por eso a un programa le quedan unos 32 256 de los 32 768 bytes). Después de cada reset se ejecuta primero: espera unos instantes a que avrdude abra la conversación por la UART y, si no llega nada, salta al programa. El reset automático lo produce la señal DTR del puerto USB-serie.

Por programador ISP

Con in-system programming la computadora (o un programador) habla directamente con el chip por el bus SPI, manteniendo el reset en bajo:

Seis conexiones para programar un ATmega328P por ISP.
Señal ISPPin del chipPin de la placa Uno
MOSIPB3 (pin 17)D11
MISOPB4 (pin 18)D12
SCKPB5 (pin 19)D13
RESETPC6 (pin 1)RESET
VCC y GNDPines 7, 8 y 225V y GND

Una placa Arduino puede actuar como programador: con el programa de ejemplo ArduinoISP cargado, usa D10 como reset del chip destino y D11 a D13 para el bus SPI. Es una forma barata de programar chips sueltos.

07 · Ver lo invisible

Depurar sin pantalla

En un microcontrolador no hay print gratuito ni depurador en la primera línea. Hay que elegir entre varias técnicas, de la más simple a la más potente:

Técnicas de depuración y sus compromisos.
TécnicaQué muestraCosto o intrusión
LED de estadoSi el programa llegó a cierto punto o en qué estado estáCasi ninguno
Mensajes por la UARTValores de variables y secuencia de eventosConsume tiempo y memoria; altera los tiempos
MultímetroNiveles de tensión estables (alimentación, reset, salidas)Ninguna; no sirve para señales rápidas
Osciloscopio o analizador lógicoForma, ancho y orden de las señales en los pinesNinguna sobre el programa; requiere instrumental
SimuladorEjecución del código en la computadora, con acceso a registrosNo reproduce todos los detalles eléctricos
Depurador por hardwarePuntos de parada, ejecución paso a paso, registros y memoria realesNecesita una herramienta específica; el ATmega328P usa debugWIRE, que emplea el pin de reset

Una buena regla es la depuración por capas: primero confirmá la alimentación y el reloj, después que el programa se ejecuta (un LED que parpadea es una prueba de vida), después que cada entrada y cada salida se comporta como esperás, y recién entonces la lógica de la aplicación.

Atención: algunas técnicas modifican el comportamiento que querés observar. Imprimir por la UART a 9600 baudios tarda alrededor de un milisegundo por carácter; en una rutina que debe responder en microsegundos, esa demora puede ocultar o crear el error.

08 · Analogía con programación

Leer un archivo HEX como un programador

El formato HEX es lo bastante simple como para entenderlo en pocas líneas de Python. Este código verifica la suma de comprobación de cada registro y decodifica las instrucciones del comienzo de la Flash. En un ATmega328P, las primeras posiciones son la tabla de vectores de interrupción: cada entrada es una instrucción JMP de cuatro bytes. La primera es el vector de reset: lo que ejecuta el chip al arrancar.

Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.

registros = [
    ":100000000C9434000C943E000C943E000C943E0082",
    ":100010000C943E000C943E000C943E000C943E0068",
    ":00000001FF",
]

for linea in registros:
    datos = bytes.fromhex(linea[1:])
    largo, direccion, tipo = datos[0], (datos[1] << 8) | datos[2], datos[3]
    carga = datos[4:4 + largo]
    estado = "correcta" if sum(datos) & 0xFF == 0 else "ERROR"
    print(f"dirección {direccion:#06x}  tipo {tipo:02d}  bytes {largo:2d}  suma {estado}")
    # El AVR guarda las palabras de 16 bits con el byte menos significativo primero.
    for i in range(0, len(carga), 4):
        op = carga[i] | (carga[i + 1] << 8)
        destino = carga[i + 2] | (carga[i + 3] << 8)
        if op == 0x940C:
            print(f"    JMP a la palabra {destino:#06x} (byte {destino * 2:#06x})")

Resultado: tres registros con suma correcta. El primer JMP salta a la palabra 0x0034 (byte 0x0068), justo después de la tabla de vectores; los demás apuntan a otra posición. Cambiá un dígito de una línea y mirá cómo falla la verificación.

Los registros son un fragmento didáctico: un programa real tiene cientos de líneas. La analogía con la programación es directa: avrdude hace de cliente que transfiere un archivo con control de integridad, y el chip, de servidor que lo almacena. La tabla de vectores aparecerá de nuevo en el tema de interrupciones.

09 · Exploración interactiva

Placa virtual y presupuesto de memoria

La primera herramienta te deja armar el programa de parpadeo, elegir el pin y el retardo, y «cargarlo» en una placa virtual con seis LED (PB0 a PB5). Podés quitar la línea de DDRB para ver qué pasa. La segunda convierte las cifras de avr-size en porcentajes de uso.

Banco de pruebas

1. Parpadeo en la placa virtual

DDRB (1 = salida)00000000
PORTB00000000
Inversiones realizadas0
PB0 · D8
PB1 · D9
PB2 · D10
PB3 · D11
PB4 · D12
PB5 · D13

2. ¿Entra el programa en la Uno?

Flash usada—
SRAM usada (sin contar la pila)—

El umbral de aviso de SRAM (75 %) es una regla práctica, no un valor de la hoja de datos: el espacio que necesita la pila depende de la profundidad de las llamadas y de las interrupciones de cada programa. Las cifras iniciales del segundo ejemplo son ilustrativas.

Recorrido guiado

  1. Con los valores iniciales presioná «Compilar y cargar». Los registros muestran cómo PORTB se invierte y el LED PB5 alterna.
  2. Desmarcá «Incluir la línea de DDRB» y volvé a cargar. PORTB cambia igual, pero DDRB queda en cero y el LED nunca enciende: el pin es entrada.
  3. Elegí PB2 y un retardo de 200 ms: el código se reescribe con los valores nuevos. Observá que el programa se detiene cuando lo modificás: hay que cargarlo otra vez.
  4. En el presupuesto de memoria, subí Data a 1600 bytes: aparece la advertencia de SRAM escasa aunque todavía «entra».
  5. Probá 32 700 bytes de Program con y sin Optiboot: con el cargador deja de entrar.
Desafío: el parpadeo que no se ve

Con el retardo mínimo de 100 ms el LED parpadea a 5 veces por segundo, perfectamente visible. En un programa real, si quitás el retardo, el LED se invierte cientos de miles de veces por segundo: el ojo solo ve un brillo tenue (el valor medio), aunque con un osciloscopio verías una señal cuadrada rápida. Por eso en un programa hay que calcular los tiempos y no confiar en la vista.

10 · Método de trabajo

Cuando algo no funciona

  1. Leé el mensaje de error completo. El compilador suele indicar archivo y línea; avrdude señala si no encuentra la placa o si falla la sincronización.
  2. Identificá en qué etapa falló. Compilación (errores de código), enlace (falta una función, no entra en memoria), carga (puerto, cable, cargador) o ejecución (el programa carga pero se comporta mal).
  3. Si falla la carga, verificá el puerto elegido, el cable USB (algunos solo cargan), que D0 y D1 estén libres y que la placa seleccionada sea la correcta.
  4. Si carga pero no hace nada, comprobá la alimentación, el reset y el reloj, y que la frecuencia declarada en F_CPU coincida con la real.
  5. Simplificá. Volvé al parpadeo mínimo: si funciona, el hardware y la cadena de herramientas están bien, y el problema está en tu código.
  6. Cambiá una cosa por vez y anotá el resultado.

En la mesa de trabajo, el multímetro verifica las tensiones de alimentación; con el osciloscopio, la señal de un pin que debería parpadear permite distinguir un programa que no corre de uno que corre con los tiempos equivocados.

11 · Comprensión

Confusiones frecuentes

«Si el programa compila, ya está bien»

Compilar solo garantiza que el código es válido para el lenguaje. No dice si el pin está configurado, si el reloj es el esperado ni si la lógica es correcta. Hay que probar en el hardware.

«El bootloader es el sistema operativo del chip»

Es un programa pequeño cuyo único trabajo es recibir un programa nuevo y grabarlo en la Flash. No administra recursos ni ejecuta tareas.

«El archivo HEX contiene mi código fuente»

Contiene solo el código máquina, sin nombres ni comentarios. El fuente se pierde al compilar; por eso conviene guardarlo siempre con control de versiones.

«Desactivar la optimización facilita todo»

Ayuda a depurar porque el código se parece más al fuente, pero algunas funciones, como _delay_ms(), requieren optimización para calcular bien los tiempos, y un programa sin optimizar puede no entrar en la Flash.

«Después de un error de carga hay que cambiar de placa»

Casi siempre el problema es de configuración: puerto equivocado, cable que solo carga, pines D0 y D1 ocupados o placa mal seleccionada. Conviene descartar eso antes de sospechar del hardware.

12 · Práctica guiada

Predecí antes de comprobar

1. Ordenar la cadena

Ordená estos pasos: grabar la Flash, enlazar, compilar, convertir a HEX. ¿Qué archivo contiene los nombres de las funciones para depurar?

Ver solución

Compilar (.c → .o), enlazar (.o → .elf), convertir a HEX (.elf → .hex) y grabar la Flash con avrdude. El archivo con los nombres y los datos de depuración es el .elf; el HEX no los incluye.

2. Un parpadeo con la mitad del tiempo

Compilás el parpadeo con F_CPU 8000000UL en una placa que corre a 16 MHz y pedís _delay_ms(1000). ¿Cuánto dura realmente el retardo?

Ver solución

El compilador calcula la cantidad de ciclos para 1 s a 8 MHz, es decir 8 000 000. A 16 MHz esos ciclos duran la mitad: 500 ms. Una frecuencia declarada incorrectamente escala todos los retardos.

3. Tiempo de carga

Un programa de 10 KB (10 240 bytes) se carga a 115 200 baudios por la UART, con 10 bits por byte (inicio, 8 de datos y parada). ¿Cuál es el tiempo mínimo teórico de transferencia?

Ver solución

10 240 × 10 / 115 200 ≈ 0,89 s. En la práctica es algo mayor, por el protocolo del cargador, la verificación y los tiempos de escritura de la Flash.

4. Capacidad

avr-size informa Program = 30 000 bytes y Data = 1 900 bytes. ¿Entra en una Uno con Optiboot? ¿Es una situación segura?

Ver solución razonada

La Flash disponible es 32 768 − 512 = 32 256 bytes, de modo que 30 000 entra, con 2 256 de margen. La SRAM es otra historia: 1 900 de 2 048 bytes son el 93 %, y quedan 148 bytes para la pila y la memoria dinámica. Es una situación de riesgo, aunque compile sin errores.

5. Elegir el método de carga

¿Qué método usarías para (a) una Uno con el cargador original, (b) un ATmega328P nuevo sin programar, (c) una producción de 500 placas?

Ver solución razonada

(a) Bootloader por USB: es cómodo. (b) ISP, porque el chip virgen no tiene cargador. (c) ISP, que es rápido y automatizable, y que no gasta Flash en un cargador que el producto final no necesita.

13 · Síntesis

Ideas para recordar

  • Se programa en una computadora (host) y se ejecuta en el microcontrolador (target): es compilación cruzada.
  • La toolchain de AVR: avr-gcc compila y enlaza, avr-objcopy genera el HEX y avrdude graba la Flash.
  • El enlazador asigna direcciones y agrega el código de arranque que se ejecuta antes de main().
  • El archivo .elf sirve para depurar y medir; el .hex sirve para cargar.
  • F_CPU debe coincidir con el reloj real, o todos los retardos salen mal.
  • Un pin se usa como salida solo si el bit correspondiente de DDRx vale 1.
  • El bootloader permite cargar por USB a costa de algo de Flash; el ISP escribe directamente y exige seis conexiones.
  • Depurar es trabajar por capas: alimentación, reloj, programa vivo, entradas y salidas, lógica.

¿Listo para el tema 6?

Antes de avanzar, explicá qué hace cada herramienta de la cadena, qué pasa si falta DDRB |= (1 << PB5) en el parpadeo, cómo se carga un chip virgen y qué leés en la salida de avr-size. Si podés justificar las cuatro respuestas, cerraste el primer bloque del curso.

En el próximo tema empieza el bloque de CPU, memoria y registros con la CPU: cómo busca, decodifica y ejecuta cada instrucción, y qué hace el contador de programa.