← Índice del curso

Microcontroladores · Tema 43

Proyecto: estación de medición de bajo consumo

Un dispositivo del tamaño de una caja de fósforos, con dos pilas AA, que mide la temperatura cada diez minutos, la guarda en su propia memoria durante días y la entrega por el puerto serie cuando se aprieta un botón. Es el primer proyecto completo del curso: junta el sensor, el registro de datos, la comunicación serie y el sueño con la estructura de los temas 41 y 42. La dificultad no está en ninguna parte por separado, sino en que todas tienen que convivir con un presupuesto de microamperios.

01 · Punto de partida

Qué vamos a construir

Una estación autónoma que registra la temperatura ambiente y la tensión de su propia batería. Funciona sola durante años; cada tanto, una persona la conecta a una computadora, descarga los datos y la vuelve a dejar. Se maneja desde el monitor serie con comandos de una letra, después de apretar un botón que abre una «sesión» de treinta segundos:

Comandos de la sesión serie (9600 baudios, 8N1).
ComandoEfecto
DVuelca el registro, del dato más viejo al más nuevo, como líneas índice,cuenta,milivoltios
SInforma el estado: registros guardados, período de medida, tensión actual de la batería
P 600Fija el período de medida en segundos (se guarda en la EEPROM y se redondea a múltiplos de 8 s)
CBorra el registro (con una confirmación: hay que repetir C antes de 5 s)
?Ayuda

El proyecto reúne casi todo el curso: el ADC y la referencia interna (tema 28), el termistor y su linealización (temas 30 y 36), la EEPROM (tema 8), la UART (tema 32), las interrupciones y el watchdog (temas 13, 19 y 20), el consumo y el sueño (tema 40) y la estructura de módulos con un bucle que no bloquea (temas 41 y 42). Las decisiones difíciles tienen que ver con la energía: ¿cuándo se enciende cada cosa, cuánto cuesta y cómo se comprueba?

02 · Antes de escribir código

El diseño: dormir casi siempre

Ciclo de funcionamiento de la estación de medición La estación duerme en Power-down. Cada ocho segundos el watchdog la despierta; si ya pasó el período, mide la temperatura y la batería, guarda un registro de dos bytes en la EEPROM y vuelve a dormir. Un botón conectado a INT0 la despierta en cualquier momento y abre una sesión de consola serie de treinta segundos, tras la cual vuelve a dormir. DormirPower-down, ~5 µA Despertar (WDT)cada 8 s; ¿ya toca medir?si no, vuelve a dormir MedirNTC y Vcc: ~5 ms de ADCdivisor encendido 3 ms Guardar2 bytes en el anillo EEPROM~7 ms de escritura Botón (INT0)despierta al instante Sesión serie de 30 sD, S, P, C · luego a dormir una vuelta completa: unos 13 ms despierto cada 600 s

El requisito que ordena todo es la vida de la batería. Con dos pilas AA de 2500 mAh, el objetivo es de al menos diez años, que equivale a una corriente media total de unos 25 µA. Eso fija el método: el microcontrolador pasa dormido más del 99,9 % del tiempo, cada cosa que consume (el divisor del sensor, el ADC, la UART) se enciende solo cuando se usa, y el único reloj que corre mientras duerme es el watchdog.

El programa sigue la estructura del tema 41: una inicialización, un bucle con dos fuentes de despertar y módulos con interfaz pequeña. La lista de módulos:

Módulos de la estación.
MóduloResponsabilidadInterfaz
energiaSueño, watchdog, apagado de periféricosenergia_init(), dormir()
sensorDivisor del NTC, ADC, medida de Vccmedir(&cuenta, &mV)
registroAnillo en la EEPROM, recuperación de la posiciónregistro_iniciar(), registro_agregar(), registro_leer(i)
consolaUART, intérprete de comandos, sesión con tiempo límiteconsola_sesion()
mainCuenta despertares, decide cuándo medir, abre la sesión—

Las decisiones de diseño que más importan se resumen en cuatro. Reloj de 1 MHz (el oscilador interno de 8 MHz dividido por 8, la configuración de fábrica): funciona con cualquier tensión de la batería entre 1,8 y 5,5 V; a 8 MHz el chip exigiría 2,7 V y las pilas se «acabarían» antes de gastar la mitad de su carga. Datos crudos: el registro guarda la cuenta del ADC y no la temperatura, de modo que el microcontrolador evita el logaritmo y la conversión se hace en la computadora. Anillo sin puntero: la posición de escritura no se guarda en la EEPROM (se agotaría su celda en meses); se recupera al arrancar. Despertar solo por intervalos de 8 s y por el botón, sin ninguna espera activa.

03 · Paso 1

El presupuesto de energía

Se empieza por la cuenta (tema 40), con las fases de una medida y sus corrientes estimadas a 1 MHz y 3 V:

Fases de una medida (valores de ejemplo; hay que medirlos en el prototipo).
FaseDuraciónCorrienteCarga
Despertar y arranque del oscilador0,5 ms0,50 mA0,25 µA·s
Alimentar el divisor y esperar que se estabilice3,0 ms0,32 mA0,96 µA·s
16 conversiones del ADC (8 del NTC y 8 de Vcc)1,7 ms0,80 mA1,36 µA·s
Empaquetar el registro0,8 ms0,50 mA0,40 µA·s
Escribir 2 bytes en la EEPROM (3,4 ms por byte)6,8 ms0,60 mA4,08 µA·s
Total por medida12,8 ms—7,05 µA·s

(Una corriente en mA durante un tiempo en ms da directamente microcoulombs, o sea µA·s.) Con una medida cada 600 s, esos 7,05 µA·s aportan 0,012 µA a la corriente media: nada. El resto del ciclo lo domina el sueño (5 µA) y, sobre todo, la autodescarga de la pila (unos 8,6 µA). El presupuesto da dos enseñanzas que parecen contradictorias: medir cuesta casi nada, así que se puede medir más seguido sin remordimientos; y cualquier consumo continuo de apenas decenas de microamperios arruina el diseño. El caso extremo es el divisor del termistor: con 3 V y 20 kΩ gasta 150 µA si queda siempre alimentado, treinta veces el consumo del sueño. Por eso se alimenta desde un pin del microcontrolador, solo durante la medida.

El presupuesto incluye además los despertares intermedios. El watchdog solo llega a 8 s, de modo que un período de 600 s son 75 despertares, de los cuales 74 no hacen más que contar y volver a dormir. Cada uno cuesta unos 0,06 µA·s (decenas de microsegundos de arranque a 0,4 mA), entre todos suman unos 4,4 µA·s por período, más de la mitad de lo que cuesta la medida entera (7,05 µA·s). La primera herramienta del laboratorio suma todo.

04 · Paso 2

El circuito

El circuito es mínimo, y esa es la idea: cada componente extra es un consumo posible.

Conexiones del ATmega328P.
PinConexiónMotivo
VCC, AVCC, GNDPilas (3 V), con 100 nF entre VCC y GND cerca del chipSin regulador: el chip funciona de 1,8 a 5,5 V
PB0Extremo superior del divisor: PB0 → R fija 10 kΩ → PC0 → NTC 10 kΩ → GNDAlimentar el divisor solo durante la medida (~150 µA durante 3 ms)
PC0 (ADC0)Punto medio del divisorLa impedancia de la fuente (5 kΩ) está por debajo de los 10 kΩ recomendados
PD2 (INT0)Pulsador a GND; pull-up internaDespierta desde Power-down por nivel bajo
PD0 (RXD), PD1 (TXD)Adaptador USB-serie de 3,3 V (RX ↔ TXD, TX ↔ RXD, GND común)Solo se conecta durante la sesión; el adaptador toma su energía de la computadora
RESETPull-up de 10 kΩUn reset espurio vacía la sesión y gasta energía
Pines sin usarSalida en bajo o entrada con pull-up (tema 40)Un pin flotante puede consumir decenas de µA

Dos detalles eléctricos merecen una explicación. Primero, el divisor es proporcional: el termistor está entre el punto medio y masa, y el divisor se alimenta desde el mismo Vcc que sirve de referencia al ADC. La cuenta es 1024 · RNTC / (Rfija + RNTC), que no depende de Vcc: aunque la batería caiga de 3,2 V a 2 V, la cuenta que corresponde a una temperatura no se mueve. Esa es la ventaja de la medida ratiométrica y evita medir la tensión para corregir la temperatura (tema 36). Segundo, la resistencia fija se elige igual al valor del NTC en el centro del rango (10 kΩ a 25 °C): ahí la pendiente es máxima y la resolución, mejor. Con un NTC de 10 kΩ y β = 3950 queda entre 0,09 y 0,17 °C por cuenta entre −10 y 60 °C.

Los fusibles (tema 13) completan el circuito: oscilador interno de 8 MHz con el divisor por 8 activo (1 MHz), el arranque más rápido que tolere la fuente, y el detector de brown-out apagado (se suma 20 a 25 µA si queda encendido; se lo desactiva también por software con sleep_bod_disable() antes de dormir). Sin el BOD, la estación no se protege de tensiones bajas mientras escribe en la EEPROM, así que el programa decide por sí mismo no escribir si la batería baja de 2,0 V.

05 · Paso 3

Medir temperatura y batería

La medida tiene dos partes, y las dos usan el mismo ADC con la referencia de Vcc. Para el termistor: se alimenta el divisor, se espera que la tensión se estabilice, se promedian 8 muestras y se apaga. Para la batería: se mide la referencia interna de 1,1 V (el canal 14 del multiplexor del ADC) usando Vcc como referencia. Como la cuenta es 1,1 V · 1024 / Vcc, se despeja Vcc = 1,1 V · 1024 / cuenta. Es el truco clásico para saber cuánta batería queda sin ningún componente extra.

#define NTC_PWR   PB0                         // alimenta la parte alta del divisor
#define MUX_NTC   0                           // ADC0 (PC0)
#define MUX_BG    0x0E                        // referencia interna de 1,1 V
#define VBG_MV    1100UL                      // reemplazar por el valor medido de cada chip (ver paso 8)

static uint16_t adc_leer(uint8_t mux) {
    ADMUX = (1 << REFS0) | mux;                // referencia AVCC
    ADCSRA |= (1 << ADSC);
    while (ADCSRA & (1 << ADSC)) { }
    return ADC;
}

static uint16_t adc_promedio(uint8_t mux) {
    uint16_t suma = 0;
    adc_leer(mux);                            // se descarta la primera: cambió el canal o la referencia
    for (uint8_t i = 0; i < 8; i++) suma += adc_leer(mux);
    return suma >> 3;
}

void medir(uint16_t *cuenta, uint16_t *vcc_mV) {
    PRR &= ~(1 << PRADC);
    ADCSRA = (1 << ADEN) | (1 << ADPS1) | (1 << ADPS0);    // 1 MHz / 8 = 125 kHz: el ADC pide 50 a 200 kHz
    PORTB |= (1 << NTC_PWR);                  // enciende el divisor
    _delay_ms(3);                             // constante de tiempo ≈ 5 kΩ · 100 nF = 0,5 ms: sobra
    *cuenta = adc_promedio(MUX_NTC);
    PORTB &= ~(1 << NTC_PWR);                 // apagado enseguida
    uint16_t bg = adc_promedio(MUX_BG);       // incluye ~70 µs de arranque de la referencia, cubiertos por la muestra descartada
    ADCSRA = 0;
    PRR |= (1 << PRADC);                      // ADC sin reloj
    *vcc_mV = (uint16_t)(VBG_MV * 1024UL / bg);
}

Observá varias decisiones. El _delay_ms(3) es una espera activa, pero corta y acotada, y el tema 40 advierte justamente sobre eso: a 1 MHz y 0,3 mA son 1 µA·s por medida (cien veces menos que el sueño), de modo que no vale la pena complicarlo con un temporizador. La primera conversión tras cambiar de canal se descarta (la capacidad del muestreo conserva restos del canal anterior). Se promedian 8 muestras para reducir el ruido del ADC (tema 36); la suma de 8 valores de 10 bits cabe en 13 bits y el desplazamiento recupera el valor medio.

La conversión a grados no se hace en la estación. La ecuación Beta, 1/T = 1/T₀ + ln(R/R₀)/β, necesita un logaritmo: miles de ciclos de punto flotante (unos milisegundos a 1 MHz, más que todo el resto de la medida) y casi 2 KB de Flash. Se guarda la cuenta cruda y la computadora hace la cuenta. Ganan tres cosas: menos energía y menos código, ninguna pérdida de información útil (la cuenta es lo que el ADC realmente midió) y la posibilidad de recalibrar datos antiguos: si más adelante se mide el β real del termistor o se cambia una resistencia, se vuelven a convertir los datos viejos sin tocar la estación. La tercera herramienta del laboratorio muestra la conversión en la computadora.

La precisión absoluta depende de las tolerancias: el NTC (±1 % en la resistencia a 25 °C, β con ±1 %), la resistencia fija (±1 %), y un ADC con ±2 cuentas de error. Todo eso suma ±0,5 °C aproximadamente, y se mejora calibrando con dos puntos conocidos (hielo y agua tibia medida con un termómetro de referencia, tema 36). El mismo criterio vale para VBG_MV: la referencia de 1,1 V tiene una tolerancia de ±10 % en el datasheet, y con ella el Vcc calculado puede errar hasta ±0,3 V. Para este proyecto basta con saber si la batería está «bien», «a la mitad» o «casi agotada»; si importa más, se mide el Vcc con un multímetro una vez, se despeja la referencia real de ese chip y se la guarda.

06 · Paso 4

El registro en la EEPROM

La EEPROM del ATmega328P tiene 1024 bytes y soporta unas 100 000 escrituras por celda (valor mínimo garantizado). Dos restricciones moldean el diseño: el espacio es poco, y las escrituras son lo que se gasta. Cada escritura tarda 3,4 ms y consume energía, y cada celda se desgasta con el uso.

El formato del registro es de dos bytes, empaquetados en una palabra de 16 bits:

Registro de 16 bits.
BitsCampoSignificado
15VueltaAlterna 0 y 1 en cada pasada completa por el anillo
14 a 10TensiónCódigo de 5 bits: Vcc = 1,80 V + código · 0,05 V (de 1,80 a 3,35 V)
9 a 0Cuenta del ADCLa cuenta del termistor, de 1 a 1022

La cuenta nunca vale 1023 porque se la recorta, y por eso el valor 0xFFFF (el estado de una celda borrada de fábrica) no es un registro válido: indica «vacío». Esa elección evita un campo de validez y un byte más.

El almacenamiento es un anillo: se escribe en la posición siguiente y, al llegar al final, se vuelve al principio y se sobreescribe lo más viejo. Con 1008 bytes disponibles (se reservan 16 para la configuración) caben 504 registros: a un registro cada 10 minutos son 3,5 días de historia. El desgaste queda repartido: cada celda se escribe una vez por vuelta. Con 100 000 escrituras y una vuelta cada 3,5 días, las celdas durarían casi mil años, mucho más que la batería. La clave está en lo que no se hace:

¿Cómo se recupera la posición después de un reinicio? Con el bit de vuelta. En el anillo, los registros nuevos tienen la vuelta actual y los viejos, la anterior; en algún punto hay un salto. La posición de escritura es el primer registro cuya vuelta difiere de la del registro 0 (o 0, si no hay diferencia). Si el anillo está entero en la misma vuelta (o aún borrado, donde 0xFFFF tiene vuelta 1), la posición es 0. El bit de la próxima escritura se deduce de lo mismo:

#include <avr/eeprom.h>

#define EE_BASE  16                           // los primeros bytes quedan para la configuración
#define REG_N    504                          // (1024 - 16) / 2

static uint16_t cabeza;                       // dónde se escribe el próximo registro
static uint8_t  vuelta;                       // bit de vuelta del próximo registro
static uint16_t cantidad;                     // registros válidos

static uint16_t reg_leer(uint16_t i) { return eeprom_read_word((const uint16_t *)(EE_BASE + 2 * i)); }

void registro_iniciar(void) {
    uint8_t v0 = reg_leer(0) >> 15;
    cabeza = 0;
    for (uint16_t i = 1; i < REG_N; i++) {
        if ((reg_leer(i) >> 15) != v0) { cabeza = i; break; }     // el salto de vuelta marca el límite
    }
    vuelta = cabeza ? v0 : (v0 ^ 1);          // el anillo borrado tiene v0 = 1, de modo que la primera vuelta es 0
    cantidad = (reg_leer(REG_N - 1) == 0xFFFF) ? cabeza : REG_N;   // si la última celda sigue borrada, el anillo no dio la vuelta
}

void registro_agregar(uint16_t cuenta, uint16_t vcc_mV) {
    if (vcc_mV < 2000) return;                // sin brown-out: no escribir con la batería baja
    if (cuenta > 1022) cuenta = 1022;
    int16_t nv = ((int16_t)vcc_mV - 1800 + 25) / 50;
    if (nv < 0) nv = 0;
    if (nv > 31) nv = 31;
    uint16_t w = ((uint16_t)vuelta << 15) | ((uint16_t)nv << 10) | cuenta;
    eeprom_update_word((uint16_t *)(EE_BASE + 2 * cabeza), w);     // update: no escribe los bytes que ya valen lo mismo
    if (++cabeza == REG_N) { cabeza = 0; vuelta ^= 1; }
    if (cantidad < REG_N) cantidad++;
}

uint16_t registro_leer(uint16_t k) {          // k = 0 es el más viejo
    uint16_t base = (cantidad == REG_N) ? cabeza : 0;
    return reg_leer((base + k) % REG_N);
}

Otras tres cuestiones de la EEPROM. eeprom_update_word lee antes de escribir y omite los bytes iguales, lo que ahorra desgaste y energía. Una escritura interrumpida por una caída de tensión puede dejar un registro corrupto, pero como el anillo no tiene estructuras enlazadas ni índices, el daño se limita a un dato. Y la configuración (el período) se escribe solo cuando el usuario la cambia con el comando P, de modo que su desgaste es nulo.

Para reconstruir la hora de cada registro, la computadora usa el momento del volcado y el período: el registro más nuevo es de «ahora» y cada anterior, un período antes. El error viene de la deriva del watchdog (±10 % en el período): para un registro cada 10 minutos, el instante de un dato de hace tres días puede errar varias horas. Es aceptable para una temperatura ambiente. Cuando la hora importa, se reemplaza el watchdog por el Timer2 con un cristal de 32 768 Hz (tema 40), que agrega un microamperio y da precisión de cristal.

07 · Paso 5

Dormir y despertar

La estación tiene dos fuentes de despertar: el watchdog (por interrupción, cada 8 s) y el botón (por nivel en INT0, único tipo de interrupción externa que funciona en Power-down). Las dos rutinas son mínimas, como pide el tema 41: una cuenta y la otra deja una bandera.

#include <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/power.h>

static volatile uint8_t despertares;          // cuántas veces despertó el watchdog desde la última medida
static volatile uint8_t boton_flag;

ISR(WDT_vect)  { despertares++; }
ISR(INT0_vect) { EIMSK &= ~(1 << INT0); boton_flag = 1; }    // se desactiva: el nivel bajo seguiría disparándola

void energia_init(void) {
    power_all_disable();                      // nada de reloj para periféricos (se encienden cuando se usan)
    ACSR  |= (1 << ACD);                      // comparador analógico apagado
    DIDR0  = 0x3F;                            // sin buffers digitales en los pines del ADC
    DDRB = 0xFF; PORTB = 0x00;                // pines sin usar: salida en bajo (PB0 alimenta el divisor)
    DDRC = 0x3E; PORTC = 0x00;                // PC0 queda como entrada del ADC; PC1 a PC5 en bajo
    DDRD = 0xFA; PORTD = 0x05;                // PD2 (botón) y PD0 (RX) con pull-up; el resto en bajo
    cli();
    wdt_reset();
    MCUSR &= ~(1 << WDRF);
    WDTCSR |= (1 << WDCE) | (1 << WDE);
    WDTCSR = (1 << WDIE) | (1 << WDP3) | (1 << WDP0);     // interrupción cada 8 s, sin reinicio
    EICRA &= ~((1 << ISC01) | (1 << ISC00));    // INT0 por nivel bajo
    EIMSK |= (1 << INT0);
    sei();
}

void dormir(void) {
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    cli();
    if (!boton_flag) {                        // evita dormir si el botón ya se apretó (condición de carrera, tema 40)
        sleep_enable();
        sleep_bod_disable();                  // justo antes de dormir: la secuencia temporizada es de pocos ciclos
        sei();
        sleep_cpu();
        sleep_disable();
    }
    sei();
}

Un detalle sobre el botón: mientras se mantiene apretado, INT0 por nivel bajo se dispararía sin parar, por eso la propia rutina la desactiva. Se la vuelve a habilitar al terminar la sesión, después de comprobar que el botón se soltó. Otro detalle: despertar desde Power-down lleva un tiempo para que el oscilador se estabilice (según los fusibles, de microsegundos a milisegundos). La primera medida debe esperar a ese arranque, y los fusibles de arranque rápido ahorran energía en cada uno de los 75 despertares por período.

El tema 40 explicó que el Timer0 se detiene en Power-down. En esta estación no hace falta: el tiempo se cuenta en despertares del watchdog (8 s cada uno). El Timer0 solo se enciende durante la sesión serie para medir los 30 s sin actividad.

08 · Paso 6

La consola serie

La UART es el consumidor más costoso de toda la estación, así que se enciende únicamente durante la sesión, y la sesión termina sola a los 30 s sin actividad. A 1 MHz con 9600 baudios hay una sorpresa del tema 32: con el modo normal, UBRR = 1 000 000 / (16 · 9600) − 1 = 5,5, se redondea a 6 y la velocidad real queda en 8929 baudios, un 7 % de error: demasiado. Con la velocidad doble (U2X0), UBRR = 1 000 000 / (8 · 9600) − 1 = 12,02, se usa 12 y la velocidad real es 9615 baudios, un error de 0,16 %.

static void uart_on(void) {
    power_usart0_enable();
    UCSR0A = (1 << U2X0);
    UBRR0  = 12;                              // 9615 baudios a 1 MHz (error 0,16 %)
    UCSR0B = (1 << RXEN0) | (1 << TXEN0);
    UCSR0C = (1 << UCSZ01) | (1 << UCSZ00);   // 8N1
}
static void uart_off(void) { UCSR0B = 0; power_usart0_disable(); }

static void tx(char c)  { while (!(UCSR0A & (1 << UDRE0))) { } UDR0 = c; }
static void txs(const char *s) { while (*s) tx(*s++); }

static void tx_num(uint16_t n) {              // sin printf: ahorra 1 KB de Flash
    char b[6]; uint8_t i = 0;
    do { b[i++] = '0' + n % 10; n /= 10; } while (n);
    while (i) tx(b[--i]);
}

static void comando_volcar(void) {
    for (uint16_t k = 0; k < cantidad; k++) {
        uint16_t w = registro_leer(k);
        tx_num(k); tx(',');
        tx_num(w & 0x3FF); tx(',');
        tx_num(1800 + ((w >> 10) & 0x1F) * 50); tx('\n');
    }
}

void consola_sesion(void) {
    uart_on();
    power_timer0_enable(); tick_init();       // Timer0 encendido solo durante la sesión (tema 41)
    txs("Estacion lista. D S P C ?\n");
    uint32_t ultimo = tick_ms();
    while (tick_ms() - ultimo < 30000UL) {
        if (UCSR0A & (1 << RXC0)) {
            char c = UDR0;
            ultimo = tick_ms();               // la actividad extiende la sesión
            switch (c) {
            case 'D': comando_volcar(); break;
            case 'S': /* registros, período y tensión actual */ break;
            case 'P': /* leer el número, guardarlo con eeprom_update_word */ break;
            case 'C': /* pedir una segunda C en 5 s antes de borrar */ break;
            default:  txs("D S P C\n"); break;
            }
        }
        wdt_reset();
    }
    while (!(UCSR0A & (1 << TXC0))) { }     // que termine de salir el último byte
    uart_off();
    power_timer0_disable();
}

El volcado de 504 registros de unos 13 caracteres cada uno son 6,5 kilobytes, que a 9600 baudios (960 caracteres por segundo) llevan unos 7 segundos. Como la actividad extiende la sesión, la ventana de 30 s alcanza. La consola trabaja por sondeo porque la sesión es corta y la CPU está sin hacer otra cosa; no vale la pena una interrupción ni un búfer circular (tema 41) cuando el flujo es de un comando por vez.

La salida usa cuenta,milivoltios y no grados: la computadora (una hoja de cálculo o un script de Python como el de esta página) calcula la temperatura con la ecuación Beta y los parámetros que quiera. Y el borrado del registro pide confirmación porque es el único comando destructivo: un byte equivocado en el terminal no debe destruir una semana de datos.

09 · Paso 7

El programa completo

Con los módulos del diseño, main queda corto, porque casi todo vive en los módulos y la lógica se reduce a dos preguntas por despertar: «¿me apretaron el botón?» y «¿ya toca medir?».

#define PERIODO_S_DEFECTO  600

static uint16_t despertares_por_medida;

int main(void) {
    uint8_t causa = MCUSR;                    // guardar la causa del reset antes de borrarla
    MCUSR = 0;
    wdt_disable();
    (void)causa;                              // opcional: registrarla en la EEPROM la primera vez que algo falle

    energia_init();                           // pines seguros, periféricos apagados, watchdog y botón
    registro_iniciar();                       // recupera la posición del anillo sin puntero
    config_cargar(&despertares_por_medida);   // período (múltiplo de 8 s) desde la EEPROM, o el valor por defecto

    for (;;) {
        dormir();                             // Power-down hasta el watchdog o el botón

        if (boton_flag) {
            consola_sesion();                 // hasta 30 s sin actividad
            while (!(PIND & (1 << PD2))) { }  // esperar a que se suelte el botón
            boton_flag = 0;
            EIFR  = (1 << INTF0);             // borrar una interrupción pendiente
            EIMSK |= (1 << INT0);
        }

        uint8_t n;
        cli(); n = despertares; sei();
        if (n >= despertares_por_medida) {
            cli(); despertares = 0; sei();
            uint16_t cuenta, mv;
            medir(&cuenta, &mv);
            registro_agregar(cuenta, mv);
        }
    }
}

El orden evita fallas sutiles: se atiende la sesión antes de contar para la medida, de modo que una sesión larga no «recupere» medidas atrasadas a ráfagas; el botón se vuelve a habilitar con la bandera pendiente borrada, para que no despierte dos veces; y despertares se lee dentro de una sección crítica, porque la modifica una ISR (tema 21; con 8 bits sería atómico, pero el hábito vale). La misma estructura funciona en Arduino con la biblioteca LowPower, y en un ARM Cortex-M con WFI y un temporizador de baja potencia (RTC): cambia la capa de hardware, la forma no.

Si se agregaran más tareas (un LED de estado, un segundo sensor), el planificador cooperativo del tema 42 encaja con un cambio: en lugar de contar despertares, se ajusta el tick al período más corto necesario. Mientras sea una sola tarea periódica, un contador es suficiente y el planificador sobra.

10 · Paso 8

Verificar y medir

Un proyecto de bajo consumo no se da por terminado hasta que se midió. Una secuencia de verificación, de menos a más:

  1. El sueño aislado. Con el multímetro en serie en la escala de µA (tema 40), con el adaptador USB desconectado y el botón suelto, se espera del orden de 5 a 6 µA (Power-down con watchdog, BOD apagado). Si el valor es de cientos de µA o de mA, se retiran componentes de a uno: el adaptador serie (su línea RX puede alimentar al chip), el pull-up del botón, el LED; y se revisa que cada pin sin uso esté en un estado definido. Se espera a que el valor se estabilice (los capacitores tardan en cargarse).
  2. Los despertares. Con una resistencia de 100 Ω en la alimentación y un osciloscopio, se observan pulsos cada 8 s de decenas de microsegundos (despertares sin medida) y, cada período, un pulso de unos 13 ms con sus fases: el escalón del divisor, el ADC y las dos escrituras de la EEPROM. Se compara la duración con el presupuesto y se ajustan los números.
  3. La medida de temperatura. Con agua con hielo (0 °C) y un termómetro de referencia a 40 °C, se comprueba la cuenta contra lo esperado y se calibra (offset y β). Con la batería real a distintos estados de carga (3,2 V y 2,2 V) se verifica que la cuenta del termistor no cambia: es la prueba de la ratiometría.
  4. La referencia de Vcc. Con la fuente de laboratorio en 3,0 V, se lee la cuenta del bandgap, se despeja la referencia real (1,1 V = Vcc · cuenta / 1024) y se ajusta VBG_MV. Con 2,5 V y 3,3 V se comprueba que el valor calculado sigue la fuente.
  5. El registro. Se prueba con un período de 8 s hasta llenar y dar la vuelta (504 registros, algo más de una hora), se interrumpe la alimentación en el medio y se vuelve a arrancar: el volcado debe seguir ordenado del más viejo al más nuevo y sin huecos. Se repite la prueba cortando la alimentación justo durante una escritura (el peor caso: pierde, a lo sumo, un registro).
  6. La sesión. Se aprieta el botón, se vuelca, se cambia el período, se espera a que pasen 30 s y se comprueba que la corriente vuelve a bajar a los 5 µA. Una sesión que no termina es un consumo parásito de miliamperios.
  7. El reloj. Se mide el período real contando despertares del watchdog contra un cronómetro: en un watchdog típico, ±10 % de desvío. Se anota el factor de corrección que usa la computadora para reconstruir la hora.

La verificación más importante es la de la batería: la estimación de años depende de cada consumo medido, y los fallos se detectan tarde (a los meses). Por eso conviene dejar la estación funcionando en un estante unas semanas y comparar la tensión de la batería al principio y al final: una pila alcalina nueva marca 3,2 V y baja de forma lenta, de modo que una caída de 0,1 V en un mes indica que algo consume mucho más de lo calculado.

11 · Analogía con programación

Presupuesto, anillo y sensor con datos

La estación es un sistema que se diseña con números antes que con código, así que conviene simularla. Este programa suma el presupuesto de energía de una medida y calcula la vida de la batería para seis configuraciones (incluidos los dos errores típicos), simula el anillo de la EEPROM con un tamaño de 8 posiciones para mostrar cómo se recupera la cabeza sin puntero, calcula el desgaste con y sin puntero y arma la tabla de conversión del termistor con su resolución.

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

import math

# 1) presupuesto de energia de una medida (reloj de 1 MHz, 3 V)
FASES = (   # nombre, duracion en ms, corriente en mA
    ("despertar y arranque del oscilador", 0.5, 0.50),
    ("alimentar el divisor y esperar", 3.0, 0.32),
    ("16 conversiones del ADC (8 NTC + 8 VCC)", 1.7, 0.80),
    ("empaquetar el registro", 0.8, 0.50),
    ("escribir 2 bytes en la EEPROM", 6.8, 0.60),
)
Q_MEDIDA = sum(ms * ma for _, ms, ma in FASES)            # mA * ms = uA * s
T_ACT = sum(ms for _, ms, _ in FASES) / 1000
COM_UA = 0.5 * 1000 * 2 * 60 / 604800                     # 2 sesiones por semana de 60 s a 0,5 mA
SD_UA = 2500 * 1000 * 0.03 / 8760                         # autodescarga de 2500 mAh al 3 % anual

for nombre, ms, ma in FASES:
    print(f"{nombre:42s} {ms:4.1f} ms  {ma:4.2f} mA  {ms * ma:5.2f} uA*s")
print(f"total por medida: {Q_MEDIDA:.2f} uA*s en {T_ACT * 1000:.1f} ms")

def promedio(periodo_s, i_sueno=5.0, extra=0.0):
    despertares = math.ceil(periodo_s / 8) - 1            # despertares del watchdog sin medida
    carga = Q_MEDIDA + despertares * 0.06
    return (carga + i_sueno * (periodo_s - T_ACT)) / periodo_s + extra + COM_UA

def anios(i_circuito):
    return 2500 * 0.9 * 1000 / (i_circuito + SD_UA) / 8760

print()
print("configuracion                                 circuito     vida")
for nombre, periodo, extra in (
    ("cada 10 minutos", 600, 0),
    ("cada minuto", 60, 0),
    ("cada 10 segundos", 10, 0),
    ("BOD activo durante el sueno (+20 uA)", 600, 20),
    ("divisor del NTC siempre alimentado (+150 uA)", 600, 150),
):
    i = promedio(periodo, extra=extra)
    print(f"{nombre:44s} {i:7.2f} uA  {anios(i):5.1f} anios")
dias = 2500 * 0.9 * 1000 / (500 + SD_UA) / 24
print(f"{'sin dormir (activo a 1 MHz, 0,5 mA)':44s} {500.0:7.2f} uA  {dias:5.0f} dias")

i = promedio(600)
partes = (("medida", Q_MEDIDA / 600), ("despertares", (math.ceil(600 / 8) - 1) * 0.06 / 600),
          ("sueno", 5 * (600 - T_ACT) / 600), ("sesiones", COM_UA), ("autodescarga", SD_UA))
total = i + SD_UA
print("reparto a 600 s:", ", ".join(f"{n} {v / total * 100:.1f} %" for n, v in partes))

# 2) registro de 16 bits en un anillo de 8 posiciones, con recuperacion de la cabeza sin puntero
BETA, R0, RF = 3950, 10000, 10000
def cuenta(t_c):
    r = R0 * math.exp(BETA * (1 / (t_c + 273.15) - 1 / 298.15))
    return round(1024 * r / (RF + r))
def temperatura(c):
    return 1 / (1 / 298.15 + math.log(RF * c / (1024 - c) / R0) / BETA) - 273.15
def empaquetar(c, vcc, vuelta):
    n_v = max(0, min(31, round((vcc - 1.8) / 0.05)))
    return (vuelta << 15) | (n_v << 10) | c
def desempaquetar(w):
    return w & 0x3FF, 1.8 + ((w >> 10) & 0x1F) * 0.05, w >> 15

N = 8
mem = [0xFFFF] * N                                         # EEPROM borrada
cabeza, vuelta = 0, 0
for i, t in enumerate((20.0, 20.1, 20.3, 20.6, 21.0, 21.5, 22.1, 22.8, 23.6, 24.5, 25.5)):
    mem[cabeza] = empaquetar(cuenta(t), 3.0 - i * 0.01, vuelta)
    cabeza += 1
    if cabeza == N:
        cabeza, vuelta = 0, vuelta ^ 1
print()
print("memoria tras 11 medidas:", " ".join(f"{w:04x}" for w in mem))
v0 = mem[0] >> 15
hallada = next((i for i in range(1, N) if mem[i] >> 15 != v0), 0)
print("cabeza hallada al arrancar:", hallada, "(real:", cabeza, ")")
orden = [(hallada + k) % N for k in range(N)]
print("del mas viejo al mas nuevo:", [round(temperatura(desempaquetar(mem[i])[0]), 1) for i in orden])

# 3) desgaste de la EEPROM: puntero guardado en cada medida contra anillo de 504 registros
print()
for periodo in (600, 60, 10):
    puntero = 100000 * periodo / 86400
    anillo = 100000 * 504 * periodo / 86400 / 365
    print(f"cada {periodo:4d} s: puntero en EEPROM {puntero:5.0f} dias; anillo de 504 registros {anillo:5.0f} anios")

# 4) el termistor: cuenta, temperatura leida y resolucion
print()
print("T (C)   R (ohm)   cuenta   T leida   C por cuenta")
for t in (-10, 0, 25, 40, 60):
    r = R0 * math.exp(BETA * (1 / (t + 273.15) - 1 / 298.15))
    c = cuenta(t)
    print(f"{t:5d} {r:9.0f} {c:8d} {temperatura(c):9.2f} {abs(temperatura(c + 1) - temperatura(c)):12.3f}")

Resultado: una medida completa dura 12,8 ms y gasta 7,05 µA·s. Con una medida cada 10 minutos, el circuito promedia 5,12 µA y, con los 8,56 µA de autodescarga, la batería de 2500 mAh dura 18,8 años; cada minuto, 18,6 años; cada 10 s, 17,9 años: medir más seguido casi no cambia nada. Con el BOD activo (+20 µA) baja a 7,6 años; con el divisor siempre alimentado (+150 µA), a 1,6 años; sin dormir, a 184 días. A 600 s el reparto es 0,1 % la medida, 0,1 % los despertares del watchdog, 36,5 % el sueño, 0,7 % las sesiones y 62,6 % la autodescarga de la pila. En el anillo de 8 posiciones, tras 11 medidas la memoria queda da10 da06 d9fa 5e33 5e2e 5e28 5e21 5e19; el arranque halla la cabeza en la posición 3 sin ningún puntero y el orden recuperado va de 20,6 a 25,5 °C. Si la posición se guardara en la EEPROM en cada medida, esa celda duraría 694 días a un registro cada 10 minutos, 69 a uno por minuto y 12 a uno cada 10 s; el anillo dura 959, 96 y 16 años. En el termistor, la resolución es de 0,137 °C por cuenta a −10 °C, 0,088 a 25 °C y 0,172 a 60 °C.

Las corrientes y los tiempos de cada fase son valores de ejemplo; en el prototipo se miden (paso 8).

12 · Exploración interactiva

Energía, registro y sensor

La primera herramienta suma el presupuesto de energía y estima la vida de la batería. La segunda dimensiona el anillo de la EEPROM, su desgaste y el tiempo de volcado. La tercera convierte la cuenta del termistor y la de la referencia interna, y arma el registro de 16 bits.

Banco de la estación

1. Presupuesto de energía y vida de la batería

Corriente media—
Vida de la batería—
Reparto del consumo—

2. Registro circular en EEPROM

Capacidad—
Desgaste—
Volcado—

3. Termistor, batería y registro de 16 bits

Resistencia del NTC—
Temperatura—
Tensión de la batería—
Registro guardado—

Recorrido guiado

  1. Con los valores iniciales: 12,8 ms despierto cada 600 s, 74 despertares del watchdog entre medidas, 5,12 µA del circuito y 13,68 µA con la autodescarga; la vida es de 18,8 años (50,2 sin autodescarga) y la autodescarga es el 62,6 % del consumo.
  2. Bajá el período a 10 s: la vida pasa a 17,9 años. Medir sesenta veces más seguido cuesta menos de un año de batería.
  3. Subí la corriente dormido a 25 µA (un detector de brown-out activo): la vida cae a 7,6 años. A 155 µA (el divisor siempre alimentado), a 1,6 años, y la nota avisa que es un consumo parásito.
  4. En el registro, 600 s y 1008 bytes dan 504 registros y 3,5 días de historia; la celda de un puntero guardado en cada medida se agotaría en 694 días, mientras el anillo dura 959 años. Con 60 s, el puntero dura 69 días y la nota lo señala.
  5. El volcado de 504 líneas de 13 caracteres a 9600 baudios son 6,8 s; a 38 400, 1,7 s.
  6. En el sensor, la cuenta 512 es 10,000 kΩ: 25,00 °C y 0,088 °C por cuenta. La cuenta 375 de la referencia da Vcc = 3,004 V y el registro queda 0x6200 (vuelta 0, código de tensión 24, cuenta 512). Probá con 300: 4,144 kΩ y 46,24 °C; y con la referencia real de 1,05 V, Vcc baja a 2,867 V: un 4,5 % de error por un dato que se creía exacto.
Desafío: ¿cuánto dura con una pila de botón?

Se quiere reemplazar las dos AA por una CR2032 de 220 mAh y 3 V, con una autodescarga del 1 % anual (unos 0,25 µA). Con el circuito de 5,12 µA, la vida teórica es 220 mAh · 0,9 / (5,12 + 0,25) µA ≈ 36 900 h ≈ 4,2 años. Pero la CR2032 tiene dos problemas: su resistencia interna es alta (decenas de ohmios, y más con frío), de modo que el pico de 0,8 mA al medir baja la tensión unos 50 mV y puede reiniciar el chip si no hay un capacitor de 10 a 47 µF en paralelo; y la capacidad real baja mucho si la corriente de pico es alta. Con el capacitor, la estación funciona; sin él, es un candidato a reinicios esporádicos. Además, una sola celda de 3 V cae por debajo de 2,2 V cerca del final, y el registro (que se niega a escribir bajo 2,0 V) deja de guardar justo antes del agotamiento: lo que se quiere.

13 · Cierre del proyecto

Lo aprendido en el proyecto

  1. El presupuesto de energía va primero: con la cuenta hecha se sabe qué importa (el sueño y la autodescarga) y qué no (la medida misma), y los errores caros (BOD, un divisor siempre alimentado) se evitan en el diseño.
  2. Cada periférico se enciende cuando se usa y se apaga después: el divisor del sensor, el ADC, la UART y el Timer0 viven encendidos milisegundos de cada diez minutos.
  3. Medir de forma proporcional (divisor y ADC con el mismo Vcc) hace que la lectura no dependa de la batería, y la referencia interna da la tensión sin componentes extra.
  4. Guardar datos crudos y convertirlos donde sea barato: el microcontrolador ahorra energía y Flash y los datos viejos se pueden recalibrar.
  5. La EEPROM se gasta con las escrituras: un anillo reparte el desgaste, y un bit de vuelta permite recuperar la posición sin guardar ningún puntero.
  6. La estructura del tema 41 alcanza: un bucle con dos fuentes de despertar, ISR mínimas, módulos con interfaz pequeña y plazos en toda espera.
  7. Un proyecto se termina midiendo: el sueño aislado, los pulsos de cada despertar, la calibración del sensor y el comportamiento tras un corte de alimentación.

Con este proyecto se juntó todo lo del curso en un dispositivo real. El siguiente proyecto cambia de problema: en lugar de dormir casi siempre, tiene que actuar de forma continua sobre el mundo, con un lazo de control.

14 · Comprensión

Confusiones frecuentes

«Mi Arduino Uno con sleep gasta 40 mA, así que el proyecto no funciona»

La placa tiene regulador, LED de alimentación y convertidor USB-serie que consumen por su cuenta. El proyecto se arma con el chip solo (o una Pro Mini de 3,3 V sin regulador ni LED) y se mide en µA. La Uno sirve para programar y depurar el código, no para medir el ahorro.

«Guardar el puntero en la EEPROM es lo más simple y seguro»

Esa celda se escribe en cada medida y es la primera en agotarse: con una medida por minuto, en 69 días. Es simple, pero no es seguro. El anillo con bit de vuelta recupera la posición al arrancar sin escribir nada extra.

«Hay que convertir a grados en el microcontrolador para guardar algo útil»

La cuenta del ADC es más fiel que cualquier conversión con una calibración aproximada: guardarla permite recalibrar después. Además, evita el logaritmo y su costo de energía y de Flash.

«Con el período más largo, la batería dura mucho más»

Cuando el sueño y la autodescarga dominan, pasar de medir cada 10 minutos a cada 10 segundos cambia la vida de 18,8 a 17,9 años. El período solo importa si cada medida es cara (una radio, un sensor de gas) o si el sueño es muy bajo.

«Si el watchdog marca 8 s, un período de 600 s son exactamente 75 despertares»

El oscilador del watchdog tiene una tolerancia de ±10 % o más con la temperatura y la tensión, de modo que 75 despertares son entre 540 y 660 s. Para registros sin hora absoluta alcanza; para una hora exacta hace falta un cristal de 32 kHz.

15 · Práctica guiada

Predecí antes de comprobar

1. Corriente media y vida

Una estación gasta 10 µA·s por medida, una cada 300 s, y duerme a 5 µA (se ignoran los despertares del watchdog). La batería es de 2500 mAh con un 80 % utilizable y una autodescarga del 3 % anual. ¿Cuál es la corriente media, y cuántos años dura con y sin autodescarga?

Ver solución

Imedia = (10 + 5 · 300) / 300 = 5,03 µA. La autodescarga equivale a 2500 000 µAh · 0,03 / 8760 h = 8,56 µA. Sin autodescarga: 2 000 000 µAh / 5,03 µA ≈ 397 000 h = 45,4 años. Con ella: 2 000 000 / 13,59 ≈ 147 000 h = 16,8 años. El cálculo sin autodescarga es una fantasía.

2. Capacidad del anillo

El anillo tiene 1008 bytes y cada registro ocupa 2 bytes. Se mide cada 15 minutos. ¿Cuántos registros y cuántos días de historia guarda?

Ver solución

1008 / 2 = 504 registros. 504 · 15 min = 7560 min = 126 h = 5,25 días. Pasado ese tiempo los datos más viejos se sobreescriben; hay que volcarlos antes.

3. Recuperar la cabeza

Un anillo de 6 posiciones tiene los bits de vuelta (de la posición 0 a la 5): 1 1 1 0 0 0. ¿Dónde se escribe el próximo registro, con qué bit de vuelta, y cuál es el registro más viejo? ¿Y si los 6 bits fueran 0 0 0 0 0 0?

Ver solución

El bit de la posición 0 es 1 y el primer bit distinto está en la posición 3: es la cabeza. La próxima escritura va en la posición 3 con vuelta 1 (la vuelta de las posiciones 0 a 2, que continúa). El más viejo es el de la posición 3; el orden cronológico es 3, 4, 5, 0, 1, 2. Con 0 0 0 0 0 0 no hay diferencia: la cabeza es la 0, el anillo completó la vuelta 0 y la próxima escritura lleva vuelta 1.

4. La batería con la referencia interna

Al medir la referencia de 1,1 V con Vcc como referencia, el ADC da 400. ¿Cuál es el Vcc? Si la referencia real del chip puede ir de 1,0 a 1,2 V, ¿entre qué valores está el Vcc real?

Ver solución

Vcc = 1,1 · 1024 / 400 = 2,816 V. Con la referencia entre 1,0 y 1,2 V, Vcc estaría entre 1,0 · 1024 / 400 = 2,56 V y 1,2 · 1024 / 400 = 3,07 V: una incertidumbre de ±0,26 V. Para mejorarla se mide una vez el Vcc con un multímetro y se despeja la referencia real de ese chip.

5. Temperatura desde la cuenta

Con el divisor de 10 kΩ y un NTC de 10 kΩ (β = 3950) el ADC lee 300. ¿Cuál es la resistencia del NTC y la temperatura? ¿Cambiaría la cuenta si la batería bajara de 3,2 a 2,2 V?

Ver solución

RNTC = 10 kΩ · 300 / (1024 − 300) = 4,144 kΩ. 1/T = 1/298,15 + ln(4,144 / 10) / 3950 = 0,003354 − 0,000223 = 0,003131 → T ≈ 319,4 K = 46,2 °C. La cuenta no cambia: la medida es proporcional, porque el divisor y la referencia del ADC cuelgan del mismo Vcc.

16 · Síntesis

Ideas para recordar

  • Una estación autónoma duerme más del 99,9 % del tiempo en Power-down con el watchdog; cada periférico se enciende solo cuando se usa y se apaga enseguida.
  • El presupuesto de energía se hace antes del código: la medida casi no pesa; el sueño, los consumos continuos parásitos (BOD, un divisor siempre alimentado) y la autodescarga de la pila lo deciden todo.
  • Un divisor con el NTC alimentado desde un pin y medido con el ADC referenciado a Vcc es proporcional: la cuenta no depende de la batería. La referencia interna de 1,1 V mide la propia Vcc sin componentes extra, con una incertidumbre de ±10 % que se corrige calibrando.
  • El registro guarda la cuenta cruda del ADC en 16 bits (vuelta, tensión, cuenta); la conversión a grados se hace en la computadora, lo que ahorra energía y permite recalibrar.
  • La EEPROM se gasta con las escrituras: un anillo reparte el desgaste, eeprom_update_* evita las escrituras redundantes y un bit de vuelta permite recuperar la cabeza al arrancar sin guardar un puntero que moriría primero.
  • La UART se enciende solo en una sesión con tiempo límite; a 1 MHz hay que usar la velocidad doble para que el error de baudios sea del 0,16 % y no del 7 %.
  • La estructura es la del tema 41: ISR mínimas, bucle con dos fuentes de despertar, módulos con init y plazos en toda espera; el planificador del tema 42 entra solo si aparecen más tareas.
  • Un proyecto se da por terminado al medirlo: el sueño aislado, los pulsos de cada despertar, la calibración y la prueba de cortar la alimentación en cualquier momento.

¿Listo para el tema 44?

Antes de avanzar, calculá la corriente media de un dispositivo que despierta periódicamente y la vida de su batería, explicá por qué conviene guardar el valor crudo del ADC y no la temperatura, y describí cómo se recupera la posición de escritura de un anillo en la EEPROM sin guardar un puntero. Si podés justificar las tres respuestas, dominás la parte de bajo consumo y registro del curso.

En el próximo tema armamos otro proyecto completo: un controlador de temperatura, con lectura, histéresis, PWM, actuador y pantalla, donde en lugar de dormir casi siempre el dispositivo tiene que actuar de forma continua con un lazo de control.