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.
| Herramienta | Qué hace | Entrada → 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-objcopy | Extrae las secciones que van a Flash y las convierte al formato Intel HEX | .elf → .hex |
avr-size | Informa cuánta Flash y cuánta SRAM ocupa el programa | .elf → texto |
avrdude | Se 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.
| Entorno | Cómo funciona | Cuándo conviene |
|---|---|---|
| Arduino IDE | Editor simple y botón de cargar; usa avr-gcc y avrdude por debajo, con bibliotecas propias | Primeros pasos y prototipos rápidos |
| PlatformIO | Extensión de editor con gestión de proyectos, bibliotecas y varias plataformas | Proyectos medianos y trabajo con varias placas |
| Microchip Studio o MPLAB X | Entornos del fabricante con simulador y depuración integrada | Trabajo profesional con depuración por hardware |
| Línea de comandos con Makefile | Se invoca cada herramienta explícitamente | Para 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.
04 · El «Hola, mundo» de la electrónica
Primer programa: parpadear un LED
Con la placa Uno, el LED «L» está conectado al pin PB5. El programa lo configura como salida y lo invierte cada medio segundo:
#define F_CPU 16000000UL // frecuencia del reloj, necesaria para los retardos
#include <avr/io.h> // nombres de registros: DDRB, PORTB, PB5...
#include <util/delay.h> // _delay_ms()
int main(void) {
DDRB |= (1 << PB5); // PB5 como salida
while (1) {
PORTB ^= (1 << PB5); // invertir el nivel de PB5
_delay_ms(500); // esperar 500 ms
}
}
Cada elemento cumple una función:
F_CPUinforma al compilador la frecuencia del reloj. Los retardos deutil/delay.hse calculan en tiempo de compilación como una cantidad de ciclos; si declarás una frecuencia equivocada, los tiempos salen equivocados.DDRB |= (1 << PB5)pone en 1 el bit 5 del registro de dirección: ese pin es salida. Sin esta línea, el pin sigue siendo entrada y el LED nunca enciende.PORTB ^= (1 << PB5)aplica un XOR: invierte solo ese bit y deja los demás como están.while (1)es el bucle infinito del tema 1. Un programa para microcontrolador nunca termina: simain()retornara, no habría a dónde volver.
Para compilarlo y cargarlo desde la línea de comandos (en Windows el puerto será algo como COM3; en Linux, /dev/ttyACM0):
avr-gcc -mmcu=atmega328p -Os -Wall -o blink.elf blink.c
avr-objcopy -O ihex -R .eeprom blink.elf blink.hex
avr-size --format=avr --mcu=atmega328p blink.elf
avrdude -c arduino -p m328p -P COM3 -b 115200 -U flash:w:blink.hex:i
-mmcu=atmega328pselecciona el chip: define el conjunto de instrucciones, las direcciones de los registros y el código de arranque.-Osoptimiza por tamaño._delay_ms()necesita optimización activada para calcular bien los ciclos.-c arduino -b 115200indica aavrdudeque hable el protocolo del cargador de la Uno a 115 200 baudios.-U flash:w:blink.hex:isignifica: escribir (w) en la Flash el archivo en formato Intel HEX (i).
El mismo programa en el estilo de Arduino tiene la misma función, con las funciones de la biblioteca:
void setup() { pinMode(13, OUTPUT); }
void loop() {
digitalWrite(13, !digitalRead(13));
delay(500);
}
Es más corto, pero oculta qué registros se tocan y cuánto cuesta cada llamada. En este curso preferimos ver los registros.
05 · Qué sale del compilador
Los archivos que se generan
| Archivo | Contenido | Para qué sirve |
|---|---|---|
.c | Código fuente | Lo escribís vos |
.o | Código máquina de un archivo, con referencias sin resolver | Entrada del enlazador |
.elf | Programa completo con nombres de funciones y variables, y datos para depuración | Depurar y medir tamaño |
.hex | Solo los bytes que van a Flash, en texto, con direcciones y verificación | Cargar en el chip |
.map (opcional) | Dónde quedó cada símbolo en memoria | Investigar el uso de memoria |
El formato Intel HEX es texto plano. Cada línea es un registro con esta forma:
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:
| Señal ISP | Pin del chip | Pin de la placa Uno |
|---|---|---|
| MOSI | PB3 (pin 17) | D11 |
| MISO | PB4 (pin 18) | D12 |
| SCK | PB5 (pin 19) | D13 |
| RESET | PC6 (pin 1) | RESET |
| VCC y GND | Pines 7, 8 y 22 | 5V 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écnica | Qué muestra | Costo o intrusión |
|---|---|---|
| LED de estado | Si el programa llegó a cierto punto o en qué estado está | Casi ninguno |
| Mensajes por la UART | Valores de variables y secuencia de eventos | Consume tiempo y memoria; altera los tiempos |
| Multímetro | Niveles de tensión estables (alimentación, reset, salidas) | Ninguna; no sirve para señales rápidas |
| Osciloscopio o analizador lógico | Forma, ancho y orden de las señales en los pines | Ninguna sobre el programa; requiere instrumental |
| Simulador | Ejecución del código en la computadora, con acceso a registros | No reproduce todos los detalles eléctricos |
| Depurador por hardware | Puntos de parada, ejecución paso a paso, registros y memoria reales | Necesita 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
2. ¿Entra el programa en la Uno?
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
- Con los valores iniciales presioná «Compilar y cargar». Los registros muestran cómo
PORTBse invierte y el LED PB5 alterna. - Desmarcá «Incluir la línea de DDRB» y volvé a cargar.
PORTBcambia igual, peroDDRBqueda en cero y el LED nunca enciende: el pin es entrada. - 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.
- En el presupuesto de memoria, subí Data a 1600 bytes: aparece la advertencia de SRAM escasa aunque todavía «entra».
- 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
- Leé el mensaje de error completo. El compilador suele indicar archivo y línea;
avrdudeseñala si no encuentra la placa o si falla la sincronización. - 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).
- 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.
- Si carga pero no hace nada, comprobá la alimentación, el reset y el reloj, y que la frecuencia declarada en
F_CPUcoincida con la real. - 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.
- 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-gcccompila y enlaza,avr-objcopygenera el HEX yavrdudegraba la Flash. - El enlazador asigna direcciones y agrega el código de arranque que se ejecuta antes de
main(). - El archivo
.elfsirve para depurar y medir; el.hexsirve para cargar. F_CPUdebe coincidir con el reloj real, o todos los retardos salen mal.- Un pin se usa como salida solo si el bit correspondiente de
DDRxvale 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.