← Índice del curso

Microcontroladores · Tema 26

Taller: atenuar un LED y mover un servo

Cierre del bloque 6. Con los tres temporizadores trabajando a la vez construimos un proyecto completo: un LED que «respira» con brillo parejo para el ojo, un servo que se mueve con dos pulsadores, y un bucle principal que no bloquea nunca. Es la combinación de todo el bloque: PWM rápida, modo con TOP en ICR1, base de tiempo y antirrebote.

01 · Punto de partida

Qué vamos a construir

Tres comportamientos que corren a la vez, sin que ninguno espere a otro:

  • Un LED que respira: su brillo sube y baja suavemente en unos cuatro segundos, con una curva corregida para que el ojo perciba una variación pareja.
  • Un servo cuya posición se controla con dos pulsadores (izquierda y derecha); mantenerlos apretados lo mueve de forma continua y gradual.
  • Una base de tiempo de 1 ms con la que el bucle principal reparte las tareas sin usar un solo _delay_ms().

Materiales: la placa Uno, un LED con resistencia de 330 Ω, un servo pequeño (tipo SG90 o similar), dos pulsadores, una fuente de 5 V independiente para el servo (por ejemplo cuatro pilas AA) y un capacitor electrolítico de 470 µF. Retomamos los temas 17 (botones), 23 a 25 (temporizadores y PWM) y el 8 (tablas en Flash).

02 · Repartir los recursos

El diseño: un temporizador por tarea

Cada tarea de tiempo crítico va a un temporizador distinto, porque cada uno tiene su propio contador, su propia frecuencia y sus propios pines:

Asignación de recursos del proyecto.
TareaTemporizadorModoPor qué
Brillo del LEDTimer0 (8 bits), OC0A en PD6 (D6)Fast PWM, ÷8: 7,8 kHzFrecuencia alta (sin parpadeo visible) y 256 niveles bastan
Posición del servoTimer1 (16 bits), OC1A en PB1 (D9)Fast PWM modo 14, ICR1 = 39999, ÷8: 50 HzHace falta una frecuencia exacta y miles de pasos de resolución
Base de tiempo de 1 msTimer2 (8 bits)CTC, ÷64, OCR2A = 249Un período exacto que no dependa de los otros dos
Reparto de tareas y botones—Bucle principal cooperativo con millis()Sin bloqueos ni interrupciones adicionales

Una vez configurados, los dos PWM funcionan sin intervención de la CPU: el programa solo escribe OCR0A y OCR1A cuando cambia el brillo o la posición. Eso es lo que permite que todo el proyecto ocupe una fracción mínima del procesador. La única interrupción es la de la base de tiempo, una vez por milisegundo.

03 · Paso 1

El cableado

Conexiones del taller.
ElementoConexiónPin del chip
LEDD6 → resistencia de 330 Ω → ánodo; cátodo a masaPD6 (OC0A)
Señal del servo (cable naranja o amarillo)D9PB1 (OC1A)
Alimentación del servo (cable rojo)+5 V de la fuente externa, no del pin 5V de la placa—
Masa del servo (cable marrón o negro)Masa de la fuente externa unida a la masa de la placa—
Capacitor de 470 µFEntre +5 V y masa de la fuente del servo, cerca del servo (el negativo a masa)—
Pulsador izquierdaD2 a masa, con el pull-up internoPD2
Pulsador derechaD3 a masa, con el pull-up internoPD3

Los pulsadores usan los pines D2 y D3, que además son INT0 e INT1: no los usamos como interrupciones porque la lectura periódica del tema 17 basta. Los pines D6 y D9 son los canales A de los temporizadores 0 y 1 (tema 25).

04 · Paso 2

El LED que respira

La PWM del LED es Fast PWM de 8 bits en el temporizador 0 con prescaler ÷8, lo que da 16 000 000 / (8 × 256) = 7812,5 Hz, muy por encima de cualquier parpadeo visible:

static void pwm_led_init(void) {
    DDRD  |= (1 << PD6);                                    // OC0A como salida
    TCCR0A = (1 << COM0A1) | (1 << WGM01) | (1 << WGM00);  // Fast PWM, salida no inversora
    OCR0A  = 0;
    TCCR0B = (1 << CS01);                                   // ÷8: arranca
}

Para que la variación se perciba pareja se necesita una tabla de gamma (tema 25): en lugar de subir OCR0A de 1 en 1, se recorren 64 pasos cuyos valores siguen 255 · (i/63)2,2. La tabla va en la memoria Flash para no gastar SRAM (tema 8):

#include <avr/pgmspace.h>

static const uint8_t GAMMA[64] PROGMEM = {
      0,   0,   0,   0,   1,   1,   1,   2,   3,   4,   4,   5,   7,   8,   9,  11,
     13,  14,  16,  18,  20,  23,  25,  28,  31,  33,  36,  40,  43,  46,  50,  54,
     57,  61,  66,  70,  74,  79,  84,  89,  94,  99, 105, 110, 116, 122, 128, 134,
    140, 147, 153, 160, 167, 174, 182, 189, 197, 205, 213, 221, 229, 238, 246, 255,
};

/* Cada 30 ms: */
OCR0A = pgm_read_byte(&GAMMA[i]);

Para subir y bajar se usa un índice i que va de 0 a 63 y vuelve, con un paso +1 o −1 que se invierte en los extremos. Con 30 ms por paso, un ciclo completo (126 pasos) dura 3,78 s. Observá los primeros cuatro valores de la tabla: son 0, 0, 0, 0. Con solo 8 bits no hay niveles suficientes para describir la zona oscura, y los primeros pasos no se distinguen; es una limitación de la resolución, no de la tabla. Con el temporizador 1 y 16 bits se evitaría.

05 · Paso 3

El servo

El servo necesita un pulso cada 20 ms cuyo ancho, de 1 a 2 ms, fija el ángulo. Con el temporizador 1 en modo rápido con ICR1 como TOP (modo 14), prescaler ÷8 (0,5 µs por cuenta) y ICR1 = 39999, el período es exactamente 20 ms, y el ancho del pulso es OCR1A + 1 cuentas:

static void servo_init(void) {
    DDRB  |= (1 << PB1);                                   // OC1A como salida
    ICR1   = 39999;                                        // TOP: 40 000 cuentas = 20 ms
    OCR1A  = 2999;                                         // 3000 cuentas = 1,5 ms: el centro
    TCCR1A = (1 << COM1A1) | (1 << WGM11);                // Fast PWM, modo 14, no inversor
    TCCR1B = (1 << WGM13) | (1 << WGM12) | (1 << CS11);  // ÷8: arranca
}

static void servo_us(uint16_t us) {                        // ancho del pulso en microsegundos
    OCR1A = us * 2 - 1;                                    // 0,5 µs por cuenta; el pulso dura OCR1A + 1
}

Desde ahora, servo_us(1500) centra el servo, servo_us(1000) lo lleva a un extremo y servo_us(2000) al otro. La resolución es de 0,5 µs: unos 2000 pasos para todo el recorrido, de modo que el movimiento puede ser suave.

Dos precauciones. Los servos reales no son exactos: algunos solo cubren de 1,1 a 1,9 ms, y otros admiten de 0,5 a 2,5 ms. Un pulso fuera de su rango lo hace topar contra el tope mecánico, vibrar y calentarse. Probá en pasos cortos hasta conocer los límites del tuyo y limitá el rango en el programa. Y el servo se mueve a la velocidad que él quiere, no a la que le pedís: un salto grande de posición hace que consuma mucha corriente un instante.

06 · Paso 4

La base de tiempo y los botones

El temporizador 2 en CTC produce la marca de 1 ms (tema 24): prescaler ÷64 (en el temporizador 2, CS22 solo), OCR2A = 249. La rutina de interrupción solo cuenta, y millis() lee el contador con una sección crítica:

volatile uint32_t ms = 0;
ISR(TIMER2_COMPA_vect) { ms++; }

static uint32_t millis(void) {
    uint32_t copia;
    ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { copia = ms; }
    return copia;
}

static void tick_init(void) {
    TCCR2A = (1 << WGM21);              // CTC
    OCR2A  = 249;                       // 250 cuentas de 4 µs = 1 ms
    TIMSK2 |= (1 << OCIE2A);
    TCCR2B = (1 << CS22);               // ÷64: arranca
}

Con la base de tiempo, el bucle principal reparte las tareas. Cada una guarda su próxima marca y la avanza con += (tema 24), de modo que el ritmo no deriva aunque una vuelta del bucle tarde más de lo previsto. Los botones se filtran con la técnica de N lecturas iguales (tema 17): una lectura cada 5 ms, y el estado se acepta tras tres iguales. No se necesitan flancos: el servo se mueve mientras el botón siga apretado.

Y el servo no salta a la posición pedida: cada 20 ms el destino avanza como máximo 10 µs. Esa limitación de velocidad (slew rate) recorre el rango completo de 1000 µs en 2 segundos y evita picos de corriente.

07 · Paso 5

El programa completo

#define F_CPU 16000000UL
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/pgmspace.h>
#include <util/atomic.h>

/* GAMMA[64] PROGMEM: la tabla del paso 2 */
/* pwm_led_init(), servo_init(), servo_us(), tick_init(), millis() y la ISR: pasos 2 a 4 */

int main(void) {
    DDRD  &= ~((1 << PD2) | (1 << PD3));        // botones: entradas
    PORTD |=  (1 << PD2) | (1 << PD3);          // con pull-up

    tick_init();
    pwm_led_init();
    servo_init();
    sei();

    uint32_t t_boton = 0, t_servo = 0, t_led = 0;
    uint16_t destino = 1500;                       // µs
    uint8_t  candidato = 0, estable = 0, cuenta = 0;
    uint8_t  i = 0;
    int8_t   paso = 1;

    while (1) {
        uint32_t ahora = millis();

        if ((int32_t)(ahora - t_boton) >= 0) {     // cada 5 ms: leer y filtrar los botones
            t_boton += 5;
            uint8_t lectura = (~PIND >> 2) & 0x03;   // bit 0: izquierda, bit 1: derecha (1 = apretado)
            if (lectura != candidato) { candidato = lectura; cuenta = 0; }
            else if (cuenta < 3 && ++cuenta == 3) { estable = candidato; }
        }

        if ((int32_t)(ahora - t_servo) >= 0) {     // cada 20 ms: mover el destino y el servo
            t_servo += 20;
            if ((estable & 0x01) && destino > 1000) destino -= 10;
            if ((estable & 0x02) && destino < 2000) destino += 10;
            servo_us(destino);
        }

        if ((int32_t)(ahora - t_led) >= 0) {       // cada 30 ms: respirar
            t_led += 30;
            OCR0A = pgm_read_byte(&GAMMA[i]);
            i += paso;
            if (i == 63 || i == 0) paso = -paso;
        }
    }
}

El conjunto ocupa una fracción mínima de la CPU: la interrupción de 1 ms cuesta unos 60 ciclos (0,4 %), las tres tareas del bucle cuestan decenas de ciclos cada vez que les toca, y el resto del tiempo el procesador está libre. Se podría dormir en esos intervalos para ahorrar energía (bloque 10).

08 · Paso 6

Verificar

  • El LED. Respira con un ciclo de unos 3,8 s, sube y baja sin saltos visibles. Con el osciloscopio en D6 se ve una onda cuadrada de unos 7,8 kHz cuyo ancho cambia.
  • La señal del servo. En D9 debe verse un período de 20 ms y un pulso de 1,5 ms con los botones sueltos. Al mantener un botón, el pulso crece o decrece 10 µs por período.
  • Los extremos. Llevá el servo hasta cada tope: el destino se detiene en 1000 y en 2000 µs. Si el servo vibra o zumba en un extremo, reducí el rango.
  • La alimentación. Mientras el servo se mueve, la placa no debe reiniciarse ni el LED parpadear. Si lo hace, la masa no es común o el capacitor falta.
  • Los botones. Un toque rápido mueve el servo una cantidad pequeña; un toque largo lo mueve de forma continua.
Síntomas frecuentes y causas probables.
SíntomaCausa probable
El servo vibra o zumba sin moversePulso fuera de su rango, o alimentación insuficiente
La placa se reinicia al mover el servoEl servo se alimenta de la placa, o falta el capacitor
El servo no respondeFalta la masa común, DDRB sin configurar, o ICR1 no escrito (período erróneo)
El servo se mueve al revésLos pulsadores están intercambiados en el código o en el cableado
El LED no respira, queda fijoFalta el DDRD, o COM0A1 está en 0, o la tarea del LED no se ejecuta
El LED parpadea de forma visiblePrescaler equivocado: con ÷1024 la PWM es de unos 61 Hz

09 · Analogía con programación

La tabla, el servo y el reparto de tareas

Este programa genera la tabla de gamma en formato C (la misma que está en el código), convierte anchos de pulso en valores de OCR1A, calcula el tiempo de recorrido del servo con la limitación de velocidad y simula el bucle cooperativo durante 10 s para comparar dos formas de avanzar la marca de cada tarea.

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

import random

random.seed(8)

# 1) Tabla de gamma de 64 pasos: OCR para brillos percibidos parejos (listo para pegar en C).
GAMMA = [round(255 * (i / 63) ** 2.2) for i in range(64)]
print("static const uint8_t GAMMA[64] PROGMEM = {")
for fila in range(0, 64, 16):
    print("    " + ", ".join(f"{v:>3}" for v in GAMMA[fila:fila + 16]) + ",")
print("};")

# 2) El servo: ancho de pulso -> OCR1A (÷8: 0,5 µs por cuenta) y ángulo supuesto de 1000 a 2000 µs.
print()
for us in (1000, 1250, 1500, 1750, 2000):
    print(f"{us} µs -> OCR1A = {us * 2 - 1:>4}   ángulo ≈ {(us - 1000) * 180 / 1000:5.1f}°   ciclo de trabajo {us / 20_000 * 100:4.1f} %")
pasos = (2000 - 1500) // 10
print(f"Recorrido de 1500 a 2000 µs a 10 µs cada 20 ms: {pasos} pasos = {pasos * 20} ms")


# 3) El bucle cooperativo: tres tareas con periodos de 5, 20 y 30 ms durante 1 s.
def simular(avanzar_desde_la_marca, segundos=10):
    tareas = {"botones": 5, "servo": 20, "LED": 30}
    proximas = {nombre: 0 for nombre in tareas}
    ejecuciones = {nombre: 0 for nombre in tareas}
    t = 0.0
    while t < segundos * 1000:
        ahora = int(t)                                   # millis()
        for nombre, periodo in tareas.items():
            if ahora - proximas[nombre] >= 0:
                ejecuciones[nombre] += 1
                proximas[nombre] += periodo if avanzar_desde_la_marca else (ahora - proximas[nombre]) + periodo
        t += random.uniform(0.1, 3.0)                    # lo que tarda cada vuelta del bucle
    return ejecuciones


print()
for etiqueta, modo in (("marca += periodo", True), ("marca = ahora + periodo", False)):
    e = simular(modo)
    esperadas = {"botones": 2000, "servo": 500, "LED": 333}
    print(f"{etiqueta:<24} " + "  ".join(f"{n}: {c} (ideal {esperadas[n]})" for n, c in e.items()))

Resultado: la tabla coincide con la del paso 2. Los pulsos de 1000, 1500 y 2000 µs dan OCR1A = 1999, 2999 y 3999 (ciclos de trabajo de 5, 7,5 y 10 %), y recorrer 500 µs a 10 µs cada 20 ms toma 1 s. En la simulación, avanzar con += período ejecuta cada tarea el número exacto de veces (2000, 500 y 334 en 10 s, aun con vueltas del bucle de hasta 3 ms), mientras que reiniciar con ahora + período pierde ejecuciones (por ejemplo, unas 1800 de las 2000 de los botones). Probá cambiar la duración de la vuelta del bucle.

El modelo ignora el costo de las propias tareas y de la interrupción de 1 ms; sirve para ver la diferencia entre los dos esquemas de planificación. En la práctica, si una vuelta del bucle tarda más que el período de una tarea, ésta se ejecuta varias veces seguidas para «ponerse al día».

10 · Exploración interactiva

Servo y tabla de gamma

La primera herramienta mueve un servo virtual con el ancho del pulso y muestra el valor de OCR1A, el ciclo de trabajo y el ángulo. La segunda genera tablas de gamma con el exponente y el número de pasos que elijas, y dibuja el brillo percibido.

Banco del taller

1. Servo virtual

0°90°180°
Registro—
Ciclo de trabajo—
Ángulo—

2. Generador de tablas de gamma

Pasos repetidos en 0—
Mayor salto percibido entre pasos—

El simulador del servo supone que el rango elegido se corresponde con 0° a 180°; cada servo real tiene sus propios límites. En la tabla de gamma, el brillo percibido se calcula como el valor elevado a 1/2,2 (una aproximación estándar de la respuesta del ojo), y el mayor salto que se ve en la tabla ideal viene del primer paso distinto de cero: con 8 bits es imposible suavizar la zona más oscura.

Recorrido guiado

  1. En el servo virtual, 1500 µs: OCR1A = 2999, 7,5 % y 90°. Movelo a 1000 y 2000 µs: los extremos.
  2. Bajá a 800 µs: aparece la advertencia de que el pulso queda fuera del rango. Pasá al rango amplio (500 a 2500): ahora 800 µs es un ángulo válido.
  3. En la tabla de gamma, dejá 2,2 y 64 pasos: es la tabla del taller (con 4 pasos repetidos en 0). Mirá la curva percibida: casi recta.
  4. Cambiá a gamma 1,0 (PWM lineal): la curva percibida se arquea por encima de la recta y el mayor salto sube a más del 15 %. Con gamma 3,0, queda por debajo.
  5. Probá 16 pasos con gamma 2,2: pasos más espaciados, con un salto percibido de unos 7 a 8 % (el mayor es siempre el primer paso distinto de cero). Es una tabla más corta para una animación más rápida o con menos memoria.
Desafío: dos servos

¿Cómo se agregaría un segundo servo en D10 (OC1B)? El temporizador 1 ya genera los 50 Hz con ICR1 = 39999: hay que poner DDRB |= (1 << PB2), activar COM1B1 en TCCR1A y escribir el ancho en OCR1B. Los dos servos comparten la frecuencia (que es lo que necesitan) y tienen posiciones independientes. Con el temporizador 1 se pueden manejar dos; para más, hay que multiplexar con interrupciones.

11 · Cierre del bloque 6

Lo aprendido en el bloque

  1. Un temporizador es un contador con prescaler, comparadores y modos; la resolución y el alcance se compensan.
  2. La comparación produce y la captura mide: las dos unidades trabajan con el instante exacto del hardware, no con la latencia de la CPU.
  3. La PWM regula una carga con tres números ligados (ciclo, frecuencia y resolución); el modo rápido y el de fase correcta resuelven el compromiso de manera distinta.
  4. Un temporizador por tarea mantiene los tiempos independientes y libera a la CPU.
  5. Una base de tiempo con millis() y marcas que avanzan con += permiten un programa cooperativo sin bloqueos.
  6. Una carga de potencia (un servo) tiene su propia alimentación, con la masa en común.

Con este taller termina el bloque 6. Hasta ahora todas las señales eran digitales (dos niveles). El bloque 7 abre la puerta al mundo analógico: cómo el microcontrolador mide tensiones continuas, con el muestreo, el conversor analógico-digital y su contraparte, el DAC.

12 · Comprensión

Confusiones frecuentes

«Con analogWrite ya tengo todo resuelto»

Sirve para un LED, pero fija la frecuencia y la resolución por defecto, no permite elegir el modo ni ajustar el período de un servo. Con los registros se controla todo y no se compite con la biblioteca.

«El servo se alimenta del pin 5V de la placa, como cualquier sensor»

Sus picos de corriente superan lo que ofrece la placa (sobre todo por USB) y reinician el microcontrolador. Necesita una fuente propia con la masa en común.

«Para que el LED respire basta con subir y bajar OCR de a uno»

El ojo percibe el brillo de forma no lineal: con pasos lineales, casi todo el cambio visible ocurre en la parte baja del rango. La tabla de gamma reparte los pasos de forma pareja para la vista.

«Con _delay_ms entre pasos hago lo mismo, y es más simple»

Funciona con una sola tarea. Con tres (LED, servo y botones) las esperas se suman y las tareas se estorban. La base de tiempo con marcas permite que todas avancen a su ritmo.

«Dos servos necesitan dos temporizadores»

Los dos canales del temporizador 1 comparten el período de 20 ms y tienen anchos independientes: dos servos con un solo temporizador.

13 · Práctica guiada

Predecí antes de comprobar

1. Un pulso de 1,2 ms

¿Qué valor lleva OCR1A para un pulso de 1,2 ms con ÷8 a 16 MHz?

Ver solución

1200 µs / 0,5 µs = 2400 cuentas, y OCR1A = 2400 − 1 = 2399 (el pulso dura OCR1A + 1 cuentas). Es lo que calcula servo_us(1200).

2. El período de la respiración

El índice recorre 0 a 63 y vuelve a 0, con una escritura de la tabla cada 30 ms. ¿Cuánto dura un ciclo completo?

Ver solución

Las escrituras van G0 a G63 (64) y G62 a G1 (62): 126 escrituras por ciclo × 30 ms = 3,78 s.

3. La velocidad del servo

El destino cambia 10 µs cada 20 ms mientras se mantiene un botón. ¿Cuánto tarda en recorrer 1000 µs y a cuántos grados por segundo equivale (180° por cada 1000 µs)?

Ver solución

1000 µs / 10 µs = 100 pasos × 20 ms = 2 s. Son 180° / 2 s = 90° por segundo.

4. ¿Por qué no del pin 5V?

Un servo bloqueado pide 650 mA. La placa, alimentada por USB, admite unos 500 mA. ¿Qué ocurre si el servo se alimenta de la placa?

Ver solución razonada

La demanda supera lo que entrega la fuente: la tensión de 5 V cae, el microcontrolador (que se alimenta del mismo riel) se reinicia por caída de tensión (tema 13) y el comportamiento es errático. Se evita con una fuente propia para el servo, y masa común.

5. Un LED más

Querés agregar un segundo LED con respiración distinta en D5 (OC0B). ¿Qué cambia respecto del primero y qué limitación tiene?

Ver solución razonada

Se usa el canal B del temporizador 0: DDRD |= (1 << PD5), COM0B1 en TCCR0A y OCR0B. Comparte con D6 la frecuencia (7,8 kHz) y el modo (Fast PWM de 8 bits), solo cambia el ciclo de trabajo, que es lo que se quiere.

14 · Síntesis

Ideas para recordar

  • Un temporizador por tarea: Timer0 para el brillo del LED, Timer1 para el servo y Timer2 para la base de tiempo.
  • El brillo se corrige con una tabla de gamma (255·(i/63)2,2) guardada en Flash, para que el ojo perciba pasos parejos.
  • El servo usa Fast PWM modo 14: ICR1 = 39999 con ÷8 da 50 Hz, y OCR1A = us·2 − 1 fija el ancho del pulso.
  • El servo se alimenta de una fuente propia, con masa común y un capacitor, nunca del pin 5V de la placa.
  • El bucle cooperativo usa millis() y marcas que avanzan con +=, sin bloqueos ni deriva.
  • La limitación de velocidad del destino (10 µs cada 20 ms) suaviza el movimiento y evita picos de corriente.
  • Los dos canales de un temporizador comparten la frecuencia: dos servos pueden usar el Timer1.

¿Listo para el bloque 7?

Antes de avanzar, calculá el OCR1A para un pulso dado, explicá por qué se usa una tabla de gamma y por qué el servo lleva su propia fuente. Si podés justificar las tres respuestas, cerraste el bloque de temporizadores y PWM.

En el próximo tema empieza el bloque de conversión analógica y digital con señales analógicas, muestreo y cuantización: cómo se convierte una tensión continua en números.