* notes / arduino

Arduino y el compilador tipo C

Un fin de semana con el ATmega32u4 de un Arduino Leonardo: del LDR al PWM, del PWM al MOSFET, y oír un relé chasquear porque un puñado de bytes lo ordenaron.

November 20, 20257 min read#arduino#electronica#embebidos#c

Contexto: el ATmega32u4 y el dialecto que lo alimenta

El corazón del Arduino Leonardo es el ATmega32u4, un microcontrolador de Atmel (hoy Microchip) con 32 KB de memoria flash (de los que el bootloader se queda 4), 2,5 KB de SRAM, un reloj de 16 MHz y un conjunto generoso de periféricos: cuatro temporizadores, un ADC de diez bits multiplexado entre doce canales analógicos, USART, SPI, I²C y, lo más vistoso para nuestro propósito, varias salidas capaces de PWM por hardware. Su rasgo distintivo frente al Uno es el USB nativo: el mismo chip habla USB sin conversor serie aparte, y puede presentarse ante el sistema como teclado o ratón. Todo eso se ofrece desde el entorno de Arduino mediante una capa fina que renombra PORTC |= (1 << PC7) como digitalWrite(13, HIGH). La abstracción cuesta unos microsegundos por llamada; rara vez se notan.

El compilador subyacente es avr-gcc, y el lenguaje, C++ a todos los efectos: pueden escribirse clases, plantillas y referencias. La ficción del «C de Arduino» se sostiene únicamente porque el IDE concatena los .ino en un único traduction unit y declara automáticamente los prototipos de las funciones. Conviene saberlo, porque cuando uno crece se topará tarde o temprano con volatile, con interrupciones (ISR(TIMER1_OVF_vect)) y con la necesidad de razonar sobre memoria en términos de bytes contados.

El circuito: LDR, LED y relé en una misma protoboard

ATmega32u45VGNDA0D9 (PWM)D7LDR+5V10kGNDLED220ΩGNDRelébobina + contacto230V ACD1N4007

Los elementos clave del esquema merecen detenerse en ellos:

  • El LDR (resistencia dependiente de la luz) forma un divisor de tensión con una resistencia fija de 10 kΩ. Cuando hay luz, su resistencia cae a centenares de ohmios; en oscuridad puede dispararse a megaohmios. La tensión leída en A0 cumple Vout = 5 · R_fija / (R_fija + R_LDR), que el ADC convierte en un entero entre 0 y 1023.
  • El LED se conecta a un pin con capacidad PWM (D9, gobernado por el Timer1) a través de una resistencia limitadora calculada con la ley de Ohm: R = (Vcc − Vf) / If. Para un LED rojo típico (Vf ≈ 2 V, If = 15 mA), R ≈ (5 − 2)/0.015 = 200 Ω; uso 220 Ω por comodidad.
  • El módulo de relé controla una carga de mayor voltaje. La bobina del relé es un inductor; al cortarle la corriente, la inductancia trata de mantener el flujo y genera un pico de tensión inverso que puede destruir el transistor de salida del Arduino. Por eso se coloca en antiparalelo el diodo flyback (1N4007 o similar), cuyo cátodo apunta al positivo, drenando ese pico de forma controlada.

Implementación: lectura, mapeo y umbral

El sketch hace dos cosas a la vez: traduce la luz ambiente en intensidad PWM para el LED (cuanto más oscuro, más brilla) y enciende el relé cuando la oscuridad supera un umbral, con un pequeño antirrebote temporal para evitar que oscile.

// pines
constexpr uint8_t PIN_LDR    = A0;
constexpr uint8_t PIN_LED    = 9;   // PWM (Timer1)
constexpr uint8_t PIN_RELAY  = 7;
 
// parámetros
constexpr uint16_t UMBRAL_OSCURO = 700;   // 0..1023
constexpr uint16_t HISTERESIS    = 40;
constexpr uint32_t DEBOUNCE_MS   = 250;
 
bool releActivo = false;
uint32_t ultimoCambio = 0;
 
void setup() {{
  pinMode(PIN_LED, OUTPUT);
  pinMode(PIN_RELAY, OUTPUT);
  digitalWrite(PIN_RELAY, LOW);
  Serial.begin(115200);
  // En el Leonardo, Serial es USB nativo (CDC): tarda un instante en
  // enumerarse. Sin esta espera se pierden las primeras líneas.
  while (!Serial) {{}}
}}
 
void loop() {{
  const uint16_t lectura = analogRead(PIN_LDR);
 
  // map: oscuro -> alto brillo; claro -> apagado.
  // invertimos porque queremos compensación de luz.
  const uint8_t brillo = map(constrain(lectura, 0, 1023), 0, 1023, 255, 0);
  analogWrite(PIN_LED, brillo);
 
  const uint32_t ahora = millis();
  if (ahora - ultimoCambio > DEBOUNCE_MS) {{
    if (!releActivo && lectura > UMBRAL_OSCURO + HISTERESIS) {{
      releActivo = true;
      digitalWrite(PIN_RELAY, HIGH);
      ultimoCambio = ahora;
      Serial.println(F("RELE ON"));
    }} else if (releActivo && lectura < UMBRAL_OSCURO - HISTERESIS) {{
      releActivo = false;
      digitalWrite(PIN_RELAY, LOW);
      ultimoCambio = ahora;
      Serial.println(F("RELE OFF"));
    }}
  }}
 
  delay(20);
}}

Tres detalles del fragmento merecen comentario. Primero, el uso de histéresis: si comparáramos contra un único umbral, una lectura oscilante alrededor de él produciría una traqueteo insoportable del relé. Definir una banda alta y otra baja convierte el comparador en un Schmitt trigger software. Segundo, el antirrebote temporal mediante millis(): nunca uso delay() largos para esto, porque congelan el ADC y la respuesta del PWM. Tercero, el F(...) que envuelve la cadena de texto guarda esa constante en flash y libera unos preciados bytes de SRAM, un truco casi obligatorio en sketches que crecen.

Una vuelta de tuerca: la interrupción

Cuando el evento que dispara la acción no proviene del ADC sino de un pulsador, conviene atender el cambio mediante una rutina de servicio de interrupción (ISR). El siguiente fragmento conecta un pulsador entre D2 y GND con resistencia pull-up interna, y alterna el relé en el flanco descendente:

constexpr uint8_t PIN_BTN = 2;
volatile bool toggle = false;
volatile uint32_t ultimaISR = 0;
 
void onPulsador() {{
  const uint32_t t = millis();
  if (t - ultimaISR > 50) {{  // debouncing dentro de la ISR
    toggle = !toggle;
    ultimaISR = t;
  }}
}}
 
void setup() {{
  pinMode(PIN_BTN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(PIN_BTN), onPulsador, FALLING);
  pinMode(PIN_RELAY, OUTPUT);
}}
 
void loop() {{
  digitalWrite(PIN_RELAY, toggle ? HIGH : LOW);
}}

Aquí el volatile es indispensable: avisa al compilador de que la variable puede mutar fuera del flujo principal y que no debe optimizarla manteniéndola en un registro.

Hallazgos

Volví a Arduino con la sospecha de redescubrir cosas que ya sabía, y me topé con tres lecciones que me parecieron dignas de anotar:

  1. El compilador no es mágico, sólo discreto. Saber que digitalWrite se traduce a un par de instrucciones sobre los registros DDRx y PORTx reconcilia la abstracción amable con el silicio real. Cuando uno necesita conmutar a kHz, baja un peldaño y manipula los registros directamente; el lenguaje no lo impide.
  2. La electrónica castiga al que improvisa. Olvidar el diodo flyback cuesta un MOSFET; calcular mal una resistencia limitadora cuesta un LED. La disciplina del lápiz y la calculadora, paradójicamente, se aprende mejor aquí que en muchos cursos.
  3. La física devuelve algo que el software pocas veces ofrece: presencia. Un endpoint que devuelve 200 OK es satisfactorio; un relé que hace click a tu orden es eufórico. No es una distinción menor cuando se trata de mantener viva la curiosidad.