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:
| Comando | Efecto |
|---|---|
D | Vuelca el registro, del dato más viejo al más nuevo, como líneas índice,cuenta,milivoltios |
S | Informa el estado: registros guardados, período de medida, tensión actual de la batería |
P 600 | Fija el período de medida en segundos (se guarda en la EEPROM y se redondea a múltiplos de 8 s) |
C | Borra 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
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ódulo | Responsabilidad | Interfaz |
|---|---|---|
energia | Sueño, watchdog, apagado de periféricos | energia_init(), dormir() |
sensor | Divisor del NTC, ADC, medida de Vcc | medir(&cuenta, &mV) |
registro | Anillo en la EEPROM, recuperación de la posición | registro_iniciar(), registro_agregar(), registro_leer(i) |
consola | UART, intérprete de comandos, sesión con tiempo límite | consola_sesion() |
main | Cuenta 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:
| Fase | Duración | Corriente | Carga |
|---|---|---|---|
| Despertar y arranque del oscilador | 0,5 ms | 0,50 mA | 0,25 µA·s |
| Alimentar el divisor y esperar que se estabilice | 3,0 ms | 0,32 mA | 0,96 µA·s |
| 16 conversiones del ADC (8 del NTC y 8 de Vcc) | 1,7 ms | 0,80 mA | 1,36 µA·s |
| Empaquetar el registro | 0,8 ms | 0,50 mA | 0,40 µA·s |
| Escribir 2 bytes en la EEPROM (3,4 ms por byte) | 6,8 ms | 0,60 mA | 4,08 µA·s |
| Total por medida | 12,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.
| Pin | Conexión | Motivo |
|---|---|---|
| VCC, AVCC, GND | Pilas (3 V), con 100 nF entre VCC y GND cerca del chip | Sin regulador: el chip funciona de 1,8 a 5,5 V |
| PB0 | Extremo superior del divisor: PB0 → R fija 10 kΩ → PC0 → NTC 10 kΩ → GND | Alimentar el divisor solo durante la medida (~150 µA durante 3 ms) |
| PC0 (ADC0) | Punto medio del divisor | La impedancia de la fuente (5 kΩ) está por debajo de los 10 kΩ recomendados |
| PD2 (INT0) | Pulsador a GND; pull-up interna | Despierta 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 |
| RESET | Pull-up de 10 kΩ | Un reset espurio vacía la sesión y gasta energía |
| Pines sin usar | Salida 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:
| Bits | Campo | Significado |
|---|---|---|
| 15 | Vuelta | Alterna 0 y 1 en cada pasada completa por el anillo |
| 14 a 10 | Tensión | Código de 5 bits: Vcc = 1,80 V + código · 0,05 V (de 1,80 a 3,35 V) |
| 9 a 0 | Cuenta del ADC | La 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:
- 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).
- 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.
- 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.
- 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 ajustaVBG_MV. Con 2,5 V y 3,3 V se comprueba que el valor calculado sigue la fuente. - 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).
- 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.
- 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
2. Registro circular en EEPROM
3. Termistor, batería y registro de 16 bits
Recorrido guiado
- 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.
- 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.
- 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.
- 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.
- El volcado de 504 líneas de 13 caracteres a 9600 baudios son 6,8 s; a 38 400, 1,7 s.
- 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
- 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.
- 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.
- 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.
- Guardar datos crudos y convertirlos donde sea barato: el microcontrolador ahorra energía y Flash y los datos viejos se pueden recalibrar.
- 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.
- 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.
- 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
inity 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.