01 · Punto de partida
Qué vamos a construir
Contar eventos es una de las tareas más comunes de un microcontrolador: pulsos de un caudalímetro, vueltas de una rueda, personas que cruzan un haz, oscilaciones de una señal. El proyecto del taller es un contador de eventos con ventana de un segundo: cuenta los flancos ascendentes que llegan al pin PD2 durante exactamente un segundo y muestra el resultado, que es la frecuencia en hercios, en ocho LED.
Para probarlo sin instrumentos, el propio chip genera una señal de frecuencia conocida en otro pin; con un cable se la lleva a PD2 y el LED debe mostrar esa frecuencia. Es una forma de cerrar el círculo: el microcontrolador se mide a sí mismo.
Retomamos el modo CTC del tema 11, INT0 del tema 20, y la sección crítica y los datos compartidos del tema 21. Los temporizadores 1 y 2 se parecen al temporizador 0 que ya conocés; los estudiamos a fondo en el bloque 6.
02 · El diseño
La arquitectura
Tres actores y tres variables compartidas. pulsos la modifica solo INT0 y la lee y borra la ISR de la compuerta; como una ISR no interrumpe a la otra (el hardware deshabilita las interrupciones al entrar), esa compartición es segura sin ninguna precaución. resultado y nuevo los escribe la ISR de la compuerta y los lee el programa principal: ahí sí hace falta una sección crítica.
Es una arquitectura típica: la rutina de interrupción hace lo mínimo (contar, copiar) y el programa principal hace el resto. La compuerta de un segundo la da un temporizador, no un retardo de software, de modo que su exactitud depende del cristal y no del programa.
03 · Paso 1
La señal de prueba
Antes de medir hay que tener algo que medir. El temporizador 2 puede invertir el pin PB3 (salida OC2A) cada vez que coincide con OCR2A, lo que genera una onda cuadrada sin que la CPU intervenga. Es el mismo principio del modo CTC del tema 11, ahora con la salida conectada al pin:
static void senal_de_prueba(void) {
DDRB |= (1 << PB3); // PB3 (D11) como salida
TCCR2A = (1 << COM2A0) | (1 << WGM21); // CTC; invertir OC2A en cada coincidencia
OCR2A = 249; // 250 cuentas por mitad de ciclo
TCCR2B = (1 << CS22) | (1 << CS21); // prescaler 256 (CS22:0 = 110)
}
La frecuencia de la salida es f = f_CPU / (2 · N · (1 + OCR2A)). Con 16 MHz, prescaler 256 y OCR2A = 249: 16 000 000 / (2 · 256 · 250) = 125 Hz, exactos. La elegimos así para que el resultado entre en los 8 bits de los LED (el máximo es 255).
| Función | Pin del chip | Pin de la placa | Configuración |
|---|---|---|---|
| Entrada de pulsos (INT0) | PD2 | D2 | Entrada con pull-up |
| Señal de prueba (OC2A) | PB3 | D11 | Salida del temporizador 2 |
| Cable de prueba | Unir D11 con D2 con un cable corto | ||
| LED, bits 0 a 5 | PC0 a PC5 | A0 a A5 | Salidas, cada una con 330 Ω |
| LED, bits 6 y 7 | PB0 y PB1 | D8 y D9 | Salidas, cada una con 330 Ω |
Los ocho LED se reparten en dos puertos porque los pines de un puerto completo están ocupados: PD2 es la entrada y PB3 la señal. Es una situación habitual, y se resuelve con las máscaras del tema 10.
04 · Paso 2
Contar los flancos
INT0 se configura para el flanco ascendente (ISC01:0 = 11) y su rutina hace lo mínimo:
volatile uint16_t pulsos = 0; // la incrementa INT0
ISR(INT0_vect) {
pulsos++; // 16 bits: lds, lds, adiw, sts, sts
}
/* En la inicialización: */
DDRD &= ~(1 << PD2);
PORTD |= (1 << PD2); // entrada con pull-up
EICRA |= (1 << ISC01) | (1 << ISC00); // flanco ascendente
EIFR = (1 << INTF0); // borrar un evento viejo
EIMSK |= (1 << INT0);
El contador es de 16 bits: cuenta hasta 65 535 pulsos por segundo. Con frecuencias mayores desbordaría; en ese caso se pasa a 32 bits (cuatro bytes, con la misma estructura). Esta rutina cuesta unos 44 ciclos (tema 19): 10 del cuerpo, 8 por dos registros guardados y 26 fijos.
Como la señal que viene del temporizador 2 es limpia, no hace falta antirrebote. Para un sensor mecánico o ruidoso, habría que aplicar el tiempo muerto del tema 20.
05 · Paso 3
La compuerta de un segundo
Contar pulsos no dice nada hasta que se sabe durante cuánto tiempo. El temporizador 1, de 16 bits, en modo CTC, provee una interrupción cada segundo exacto: con prescaler 256, 16 000 000 / 256 = 62 500 cuentas por segundo, así que OCR1A = 62 499 (recordá el «+1» del tema 11).
volatile uint16_t resultado = 0; // la escribe la ISR de la compuerta
volatile uint8_t nuevo = 0; // bandera: hay un resultado sin leer
ISR(TIMER1_COMPA_vect) { // exactamente cada 1 s
resultado = pulsos; // copiar el recuento de la ventana…
pulsos = 0; // …y empezar otra
nuevo = 1; // avisar al programa principal
}
/* En la inicialización: */
TCCR1A = 0;
TCCR1B = (1 << WGM12) | (1 << CS12); // CTC, prescaler 256
OCR1A = 62499; // 62 500 cuentas = 1 s
TIMSK1 |= (1 << OCIE1A); // interrupción en cada coincidencia
Este diseño es la solución a una pregunta que no tiene respuesta buena por sondeo: «¿durante cuánto tiempo conté?». La compuerta la define el hardware, y la rutina de la compuerta copia y reinicia el contador en pocos ciclos. Entre la copia y el reinicio puede llegar un pulso de INT0: no lo atiende en ese momento (no se anidan), pero su bandera queda registrada y lo contará en la ventana siguiente, de modo que ningún pulso se pierde, solo se desplaza de ventana.
El programa principal recibe el resultado con una sección crítica (tema 21), para leer los dos bytes de resultado y borrar nuevo sin que la ISR se cuele:
uint16_t f;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
f = resultado; // los dos bytes, juntos
nuevo = 0; // y borrar la bandera sin perder un aviso nuevo
}
06 · Paso 4
Mostrar el resultado y el programa completo
#define F_CPU 16000000UL
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/atomic.h>
volatile uint16_t pulsos = 0;
volatile uint16_t resultado = 0;
volatile uint8_t nuevo = 0;
ISR(INT0_vect) { pulsos++; }
ISR(TIMER1_COMPA_vect) { resultado = pulsos; pulsos = 0; nuevo = 1; }
static void senal_de_prueba(void) {
DDRB |= (1 << PB3);
TCCR2A = (1 << COM2A0) | (1 << WGM21);
OCR2A = 249; // 125 Hz
TCCR2B = (1 << CS22) | (1 << CS21); // prescaler 256
}
int main(void) {
DDRC |= 0x3F; DDRB |= 0x03; // LED: bits 0 a 5 en PC0..PC5, bits 6 y 7 en PB0..PB1
DDRD &= ~(1 << PD2); PORTD |= (1 << PD2);
senal_de_prueba();
EICRA |= (1 << ISC01) | (1 << ISC00); // INT0: flanco ascendente
EIFR = (1 << INTF0);
EIMSK |= (1 << INT0);
TCCR1A = 0; // compuerta de 1 s
TCCR1B = (1 << WGM12) | (1 << CS12);
OCR1A = 62499;
TIMSK1 |= (1 << OCIE1A);
sei();
while (1) {
if (nuevo) {
uint16_t f;
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { f = resultado; nuevo = 0; }
uint8_t v = (f > 255) ? 255 : (uint8_t)f; // saturar a lo que entra en 8 LED
PORTC = (PORTC & ~0x3F) | (v & 0x3F); // bits 0 a 5
PORTB = (PORTB & ~0x03) | (v >> 6); // bits 6 y 7
}
}
}
El valor se reparte entre dos puertos con el patrón de campo del tema 10: se borra el campo con su máscara y se escribe el valor desplazado. Con la señal de 125 Hz, los LED deben mostrar 01111101 cada segundo. Con una frecuencia mayor que 255 Hz, el valor se satura en 255 y los ocho LED quedan encendidos; para ver cifras mayores el siguiente paso natural es la UART (tema 32).
Aprovechamos que el programa principal está casi ocioso: no hace nada salvo esperar. Podría dormir hasta la próxima interrupción para ahorrar energía, tema que veremos en el bloque 10.
07 · Paso 5
Verificar
- Sin cable. Antes de unir D11 con D2, los LED deben mostrar 0 (pull-up en PD2, sin flancos).
- Con la señal de prueba. Unís D11 con D2: tras un segundo, los LED muestran 125 (
01111101) y lo mantienen. La primera lectura puede salir distinta, porque la primera ventana empieza con el chip ya funcionando. - Con otros valores. Cambiá
OCR2Aa 124: la frecuencia pasa a 250 Hz (16 000 000 / (2 · 256 · 125)). Con 62 obtenés unos 500 Hz y los LED se saturan. - Con el osciloscopio en D11 comprobás la frecuencia de la señal de prueba; con una marca en un pin dentro de la ISR de la compuerta (tema 14) comprobás que se ejecuta cada segundo.
- Con un pulsador en lugar del cable, el contador cuenta rebotes: es una buena demostración de por qué hace falta el tiempo muerto.
| Síntoma | Causa probable |
|---|---|
| Los LED siempre en 0 | El cable no está conectado, falta sei() o la máscara de INT0, o el flanco está mal configurado |
| El programa se reinicia solo | Una interrupción habilitada sin su rutina (BADISR_vect, tema 19) |
| El valor es la mitad o el doble | Prescaler del temporizador 2 distinto del previsto, o ISC01:0 en «cualquier cambio» (cuenta los dos flancos) |
| El valor fluctúa en ±1 | Es normal: el error de ±1 cuenta de la compuerta (ver la sección siguiente) |
| El valor salta en cualquier dirección | Una lectura de resultado sin sección crítica |
08 · Hasta dónde llega
Precisión y límites
La compuerta es exacta, el recuento tiene ±1. La ventana dura 1 s con la precisión del cristal (decenas de ppm, tema 12). Pero según en qué punto del ciclo de la señal empiece, la cuenta puede dar un pulso más o uno menos. Con una compuerta de 1 s eso es una resolución de 1 Hz: a 125 Hz, el error máximo es de 0,8 %; a 10 kHz, de 0,01 %. Con una compuerta de 100 ms la resolución cae a 10 Hz. Para frecuencias bajas conviene medir el período en lugar de contar (captura de entrada, tema 24).
La frecuencia máxima la fija la rutina. Cada pulso cuesta unos 44 ciclos, de modo que a 16 MHz la CPU se satura a unos 363 000 pulsos por segundo. Antes de llegar a ese punto, el programa principal y la ISR de la compuerta ya sufren. Una regla práctica es no pasar del 30 al 50 % de uso de la CPU: unos 100 000 a 180 000 pulsos por segundo. Para frecuencias mayores se usa el contador por hardware del temporizador (el pin T1 como entrada de reloj), que cuenta hasta la mitad de la frecuencia del reloj sin consumir CPU.
El contador de 16 bits desborda a 65 535. Eso alcanza para una ventana de 1 s y frecuencias menores que 65 kHz; con más, se necesita un contador de 32 bits.
09 · Analogía con programación
Simular el contador y el error de la compuerta
Este programa simula dos cosas. Primero, una ISR que cuesta 44 ciclos atendiendo flancos de frecuencias crecientes: hasta que la CPU se satura, cuenta todos; después, no. Después calcula qué cuentas puede dar una compuerta de 100 ms y de 1 s para señales de frecuencia no entera, y el error relativo máximo.
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
F_CPU = 16_000_000
COSTO_ISR = 44 # ciclos por evento (entrada, prólogo, cuerpo, epílogo y RETI)
def contar(frecuencia, costo=COSTO_ISR, duracion=1.0):
"""Cuenta los flancos que alcanza a atender la ISR. Mientras corre una rutina, un flanco
nuevo queda registrado en la bandera (uno solo); los demás se pierden."""
periodo = F_CPU / frecuencia # ciclos entre flancos
libre_en, pendiente, contados = 0.0, False, 0
for k in range(1, int(frecuencia * duracion) + 1):
t = k * periodo
if pendiente and libre_en <= t: # la CPU terminó y había un flanco esperando
contados += 1
libre_en += costo
pendiente = False
if t >= libre_en:
contados += 1
libre_en = t + costo
elif not pendiente:
pendiente = True # queda registrado en la bandera
# si ya había uno pendiente, este flanco se pierde
return contados + (1 if pendiente else 0)
print("frecuencia flancos reales contados uso de CPU")
for f in (125, 1_000, 10_000, 100_000, 300_000, 363_000, 500_000, 1_000_000):
c = contar(f)
uso = min(100.0, f * COSTO_ISR / F_CPU * 100)
print(f"{f:>9} Hz {f:>14} {c:>9} {uso:6.1f} %")
# La compuerta: el error de ±1 cuenta según en qué punto de la señal empieza.
random.seed(2)
print()
print(" frecuencia compuerta cuentas posibles error relativo máximo")
for f in (1.5, 12.7, 125.3, 1_000.7, 10_000.3):
for compuerta in (0.1, 1.0):
cuentas = {int(f * compuerta + random.random()) for _ in range(400)}
peor = max(abs(c - f * compuerta) for c in cuentas) / (f * compuerta) * 100
print(f"{f:>9} Hz {compuerta * 1000:>6.0f} ms {sorted(cuentas)!s:<22} {peor:8.2f} %")
Resultado: hasta 363 000 Hz se cuentan todos los flancos (con un 99,8 % de uso de CPU); a 500 kHz o 1 MHz se cuentan solo unos 363 640, el límite de f_CPU / 44. En la compuerta, 125,3 Hz con 100 ms da 12 o 13 cuentas (hasta 4,2 % de error) y con 1 s da 125 o 126 (0,56 %); 1,5 Hz con 100 ms es inservible. Probá cambiar COSTO_ISR a 100 o las frecuencias de la segunda tabla.
El modelo cuenta que un flanco que llega con la rutina corriendo queda en la bandera (uno solo). Ignora el costo de las demás interrupciones y la latencia de entrada, que reducirían el límite en un caso real.
10 · Exploración interactiva
Contador y compuerta
La primera herramienta es el contador virtual: elegí si se cuenta por interrupción o por sondeo, la frecuencia de entrada, la compuerta y el costo, y mirá cuántos pulsos se cuentan realmente. La segunda calcula la configuración del temporizador 1 para cualquier tiempo de compuerta.
Banco del contador de eventos
1. Contador virtual
2. Configurar la compuerta con el temporizador 1
| Prescaler | OCR1A | Compuerta real | Error |
|---|
El contador virtual es un modelo simplificado: para la interrupción, limita la tasa a f_CPU / costo; para el sondeo, calcula la frecuencia aparente por submuestreo (alias). El número de cuentas se da como rango porque el valor depende de la fase de la señal con respecto a la compuerta. No incluye las demás interrupciones del sistema.
Recorrido guiado
- Con los valores iniciales (interrupción, 125 Hz, 1 s): 125 pulsos, 125 cuentas y una carga de CPU insignificante. Cambiá la compuerta a 100 ms: 12 o 13 cuentas y una resolución de 10 Hz.
- Subí la frecuencia a 100 000 Hz: sigue contando bien, pero la CPU está al 27,5 %. A 400 000 Hz se satura: cuenta como máximo unos 363 640.
- Cambiá a sondeo con 20 ciclos por vuelta y 1000 Hz: bien. Subí a 600 000 Hz: el programa ve una frecuencia falsa de 200 000 Hz (alias).
- En la herramienta 2, 1 s a 16 MHz: el prescaler ÷256 con
OCR1A = 62499es exacto, y el ÷1024 también (15 624). Probá 100 ms: ÷64 da 24 999. - Probá 10 s: ningún prescaler alcanza con 16 bits. Hay que contar 10 compuertas de 1 s por software.
Desafío: medir hasta 1 MHz
Con el contador por interrupción no se pasa de unos 363 kHz. Para llegar a 1 MHz hay que contar con el hardware: configurar el temporizador 1 para que cuente flancos del pin T1 (PD5) como reloj externo, y leer TCNT1 al final de cada compuerta. Esa cuenta no consume ciclos de CPU y llega hasta la mitad de la frecuencia del reloj (8 MHz a 16 MHz). Es lo que hacen los frecuencímetros de laboratorio.
11 · Cierre del bloque 5
Lo aprendido en el bloque
- Una interrupción tiene tres permisos (bandera, máscara, habilitación global) y una tabla de vectores con prioridades fijas.
- La ISR es corta: cuenta, copia o marca una bandera; el programa principal hace el resto.
- Los pines interrumpen con INT0/INT1 (flanco o nivel) o con cambio de pin (un vector por grupo), y el rebote se filtra con tiempo muerto.
- Lo compartido es
volatiley los accesos de más de un byte se protegen con una sección crítica, o se diseñan para que no hagan falta. - El tiempo lo da un temporizador, no un retardo, para que no acumule deriva.
- El presupuesto de ciclos dice hasta qué frecuencia de eventos se puede atender.
Con este taller termina el bloque 5. Los temporizadores aparecieron aquí como cajas negras muy útiles; el bloque 6 los abre: modos, comparación, captura de entrada y PWM, para generar y medir señales con el hardware.
12 · Comprensión
Confusiones frecuentes
«Para medir un segundo basta con _delay_ms(1000)»
El retardo bloquea la CPU (que no puede atender los pulsos) y no es exacto si hay interrupciones en medio. La compuerta del temporizador funciona en paralelo y marca el segundo al ciclo.
«Se pierden pulsos entre la copia y el reinicio del contador»
No: si llega un pulso mientras corre la rutina de la compuerta, su bandera queda registrada y se cuenta en la ventana siguiente. Solo se desplaza de ventana, y a cambio no se pierde.
«La lectura de resultado en el programa principal no necesita protección porque se escribe una vez por segundo»
El riesgo es bajo pero real (por ejemplo, si el programa tarda más de un segundo en volver al bucle) y borrar la bandera sin protección puede perder un aviso. La sección crítica cuesta unos pocos ciclos y elimina la duda.
«Con una compuerta más larga el resultado es siempre mejor»
Mejora la resolución, pero la medida tarda más en actualizarse y no refleja cambios rápidos de la señal. Es un compromiso entre resolución y tiempo de respuesta.
«El contador por interrupción mide cualquier frecuencia»
Solo hasta la tasa que permite el costo de la rutina, unos pocos cientos de kilohercios. Para señales más rápidas se necesita el contador por hardware.
13 · Práctica guiada
Predecí antes de comprobar
1. Una compuerta de 100 ms
Con 16 MHz y prescaler 64, ¿qué valor lleva OCR1A para una compuerta de 100 ms?
Ver solución
16 000 000 × 0,1 / 64 = 25 000 cuentas, por tanto OCR1A = 24 999 (el contador cuenta de 0 a OCR1A inclusive).
2. La señal de prueba
Con prescaler 64 y OCR2A = 124 en el modo de inversión de OC2A, ¿qué frecuencia sale a 16 MHz?
Ver solución
f = 16 000 000 / (2 · 64 · 125) = 1000 Hz. (El prescaler de 64 del temporizador 2 corresponde a CS22:0 = 100.)
3. Resolución y error
Con una compuerta de 100 ms, se mide una señal de 125 Hz. ¿Qué cuentas pueden salir y qué frecuencias muestra el programa?
Ver solución
125 × 0,1 = 12,5 pulsos: salen 12 o 13 cuentas, que equivalen a 120 o 130 Hz (resolución de 10 Hz, error de hasta 4 %). Con 1 s saldrían 125 exactos.
4. Frecuencia máxima
La rutina de INT0 cuesta 44 ciclos. ¿Qué frecuencia consume el 50 % de la CPU y cuál la satura?
Ver solución
50 %: 0,5 × 16 000 000 / 44 ≈ 181 800 Hz. Saturación: 16 000 000 / 44 ≈ 363 600 Hz.
5. El flanco equivocado
Configurás ISC01:0 = 01 (cualquier cambio) en lugar de 11. Con la señal de 125 Hz, ¿qué marcan los LED?
Ver solución
Cada ciclo tiene dos flancos y los dos disparan la interrupción: el contador cuenta el doble, 250 (11111010). Es uno de los síntomas de la tabla de la sección 7.
14 · Síntesis
Ideas para recordar
- Un contador de eventos combina una interrupción que cuenta y otra, de un temporizador, que define la ventana de tiempo.
- La compuerta de 1 s con el temporizador 1 en modo CTC usa prescaler 256 y
OCR1A = 62499. - Una ISR no interrumpe a otra, así que compartir entre ISR es seguro; compartir con el programa principal exige una sección crítica.
- La frecuencia medida tiene una resolución de 1/compuerta y un error de ±1 cuenta; la compuerta es tan exacta como el cristal.
- El límite de frecuencia es f_CPU / costo de la ISR (unos 363 kHz con 44 ciclos); por encima se usa un contador por hardware.
- El sondeo submuestrea: por encima de la mitad de la tasa de vueltas del bucle se ven frecuencias falsas.
- El propio chip puede generar la señal de prueba con un temporizador, sin instrumentos.
¿Listo para el bloque 6?
Antes de avanzar, calculá OCR1A para una compuerta dada, estimá la frecuencia máxima que puede contar tu rutina y explicá por qué el contador por interrupción no pierde pulsos en el reinicio de la ventana. Si podés justificar las tres respuestas, cerraste el bloque de interrupciones.
En el próximo tema empieza el bloque de temporizadores y PWM con temporizadores y contadores: los registros de cuenta, el prescaler, el desbordamiento y los modos, ahora con todo el detalle.