← Índice del curso

Microcontroladores · Tema 14

Taller: medir ciclos y tiempos de ejecución

Cierre del bloque 3. Optimizar sin medir es adivinar. En un microcontrolador, cada ciclo se puede contar y cada microsegundo se puede ver en un osciloscopio. En este taller combinamos las tres técnicas básicas y aprendemos a no engañarnos con ellas.

01 · Punto de partida

Medir antes de optimizar

Los programadores conocen la regla: no optimices sin medir, porque la intuición sobre dónde se pierde el tiempo suele fallar. Con los microcontroladores la regla pesa más, porque el tiempo no es solo velocidad: es un requisito de corrección. Si una rutina tiene que terminar antes del próximo byte que llega por la UART, tardar un poco más no es lento, es incorrecto.

La ventaja es que un AVR es un sistema determinista: no tiene memoria caché, ni predicción de saltos, ni sistema operativo que lo interrumpa. Si no hay interrupciones de por medio, ejecutar el mismo código siempre tarda exactamente los mismos ciclos. Un tiempo medido hoy es el tiempo de mañana. Eso permite predecir los tiempos con una precisión que en un procesador de escritorio es imposible.

Retomamos la tabla de ciclos por instrucción de los temas 6 y 7, el temporizador del tema 11 y el reloj del tema 12 (62,5 ns por ciclo a 16 MHz).

02 · El panorama

Tres formas de medir

Técnicas para medir el tiempo de un fragmento de código.
TécnicaCómo funcionaResoluciónInstrumental
Contar a manoSumar los ciclos de cada instrucción del ensambladorUn ciclo, en teoríaNinguno; hace falta el listado en ensamblador
Marca en un pinSubir un pin antes del código y bajarlo después; medir el pulsoDepende del instrumento: decenas de ns (osciloscopio) o ~40 ns (analizador de 24 MHz)Osciloscopio o analizador lógico
Temporizador internoLeer un contador antes y después y restarUn ciclo (sin prescaler)Ninguno; el resultado se envía por la UART
Funciones de bibliotecamicros() o millis() de Arduino4 µs y 1 ms, respectivamenteNinguno

Las tres primeras se complementan: contar a mano predice, el pin y el osciloscopio verifican sin alterar el programa, y el temporizador mide dentro del chip cuando no hay instrumental. Si coinciden, estás seguro. Si no, hay algo que no entendiste del código o de la medición, y eso es lo más valioso que puede enseñarte este taller.

03 · Predecir

Contar ciclos a mano

Con el ensamblador y la tabla de ciclos del tema 7 se puede calcular el tiempo sin ejecutar nada. Tomemos un bucle que suma 100 bytes de un arreglo, con el puntero X apuntando ya al arreglo:

        ldi r16, 100      ; 1 ciclo: 100 elementos
bucle:  ld  r17, X+       ; 2 ciclos: leer un byte y avanzar el puntero
        add r18, r17      ; 1 ciclo: acumular
        dec r16           ; 1 ciclo
        brne bucle        ; 2 ciclos si salta, 1 si no

Cada vuelta cuesta 2 + 1 + 1 + 2 = 6 ciclos, salvo la última, donde BRNE no salta y cuesta uno menos (5 ciclos). Total:

1 (LDI) + 100 · 6 − 1 = 600 ciclos = 37,5 µs a 16 MHz

Es la misma técnica que usamos para el retardo de 3·N ciclos del tema 6. Dos fuentes de error al contar: olvidar que el último BRNE no salta, y contar el código que se cree que generó el compilador en lugar del real. Para esto último, el listado se obtiene con avr-objdump -d o con avr-gcc -S (tema 7).

04 · Verificar sin alterar

Una marca en un pin

La técnica clásica: encerrar el código entre dos escrituras a un pin que se pueda observar. Con SBI y CBI (tema 7) cada marca cuesta 2 ciclos:

DDRB |= (1 << PB0);              // PB0 como salida (una sola vez)

PORTB |=  (1 << PB0);            // sbi: el pin sube
/* … código a medir … */
PORTB &= ~(1 << PB0);           // cbi: el pin baja

En el osciloscopio se ve un pulso cuyo ancho es los ciclos del código más 2: el flanco de subida ocurre al final del SBI y el de bajada al final del CBI, de modo que el segundo contribuye con sus 2 ciclos. Si el código medido es un bucle de 600 ciclos, el pulso dura 602 ciclos = 37,625 µs a 16 MHz.

Calibración con un pulso vacío

Antes de medir nada, dejá las dos marcas una al lado de la otra, sin código en medio. El pulso debe medir exactamente 2 ciclos (125 ns a 16 MHz). Si tu instrumento marca otra cosa, aprendés su incertidumbre de medición. Es la prueba de que la técnica funciona.

Configuración del instrumento

  • Disparo (trigger) por flanco ascendente del pin PB0, para que el pulso aparezca estable en pantalla.
  • Base de tiempo tal que el pulso ocupe varias divisiones, y cursores para leer el ancho con precisión.
  • Ancho de banda: los pulsos de unos cientos de nanosegundos exigen un osciloscopio de decenas de MHz como mínimo. Un analizador lógico de 24 MHz de muestreo resuelve unos 42 ns, y por tanto sirve para pulsos de varios microsegundos, no para los de 125 ns.
  • Sonda con masa corta: el cable de masa largo de la sonda añade oscilaciones en los flancos.

05 · Medir desde adentro

El temporizador como cronómetro

Si no hay osciloscopio a mano, el propio chip puede medir. El temporizador 1 tiene 16 bits, y sin prescaler cuenta un ciclo de reloj por cuenta:

TCCR1A = 0;                       // modo normal
TCCR1B = (1 << CS10);             // sin prescaler: una cuenta = un ciclo

TCNT1 = 0;
uint16_t t0 = TCNT1;              // leer el inicio
/* … código a medir … */
uint16_t t1 = TCNT1;              // leer el final
uint16_t ciclos = t1 - t0 - SOBRECOSTO;   // la resta funciona aunque el contador desborde

Dos detalles. El sobrecosto es el tiempo de las propias lecturas (cada acceso a TCNT1 son dos instrucciones, tema 9) y se mide con una prueba vacía: dejando el código a medir en blanco, lo que se lee es el sobrecosto. Y el alcance: 65 536 cuentas, es decir, 4,096 ms a 16 MHz sin prescaler. Para tareas más largas se usa un prescaler a costa de resolución:

Resolución y alcance del temporizador 1 a 16 MHz.
PrescalerResoluciónAlcance máximo
÷162,5 ns4,096 ms
÷80,5 µs32,768 ms
÷644 µs262,1 ms
÷25616 µs1,049 s
÷102464 µs4,194 s

Con prescaler aparece otro fenómeno: la cuantización. Si el contador avanza cada 8 ciclos, una medición de 604 ciclos puede leerse como 75 o como 76 cuentas, según en qué fase del prescaler caiga el inicio. Una sola lectura tiene un error de hasta una cuenta (8 ciclos); repitiendo la medición muchas veces y promediando se recupera la precisión.

06 · Trampas

Cuidados para no engañarse

  • Medí el programa que va a correr. El mismo código compilado con -O0 y con -Os tarda cosas muy distintas. Medí con la optimización final.
  • Que el compilador no borre lo que querés medir. Un cálculo cuyo resultado no se usa es eliminado y el pulso medirá casi cero. Usá el resultado (guardalo en una variable volatile) para que se conserve.
  • Las interrupciones alargan las mediciones. Una interrupción que ocurre dentro del bloque suma su duración. Podés deshabilitarlas durante la medición, o medir muchas veces y quedarte con el mínimo (el tiempo sin interrupciones) y el máximo (el peor caso).
  • El tiempo depende de los datos. Un bucle que termina cuando encuentra un valor, una división por software o un salto condicional tardan distinto según la entrada. Medí el mejor y el peor caso, y quedate con el peor para el presupuesto.
  • No mezcles unidades: los ciclos son lo primario; el tiempo depende del reloj. Una medición en ciclos sigue valiendo si cambia la frecuencia; una en microsegundos, no.

07 · Para qué se mide

El presupuesto de ciclos

Medir tiene sentido contra un objetivo: los ciclos disponibles entre un evento y el siguiente. Si llega un evento cada T segundos y el reloj es de f hercios, hay f · T ciclos para atenderlo. La tarea debe caber con margen.

Ciclos disponibles entre eventos con un reloj de 16 MHz.
EventoPeríodoCiclos disponibles
Marca de tiempo de 1 kHz1 ms16 000
Byte por UART a 9600 baudios (10 bits)1,04 ms16 667
Muestreo de audio a 8 kHz125 µs2000
Byte por UART a 115 200 baudios86,8 µs1389
Período de un PWM de 20 kHz50 µs800
Un bit de I2C a 100 kHz10 µs160

Un byte a 115 200 baudios deja 1389 ciclos entre uno y el siguiente. Una rutina que tarde 200 ciclos ocupa el 14 % de la CPU. Si tarda 1500, se pierden bytes. A esos números hay que sumar el costo fijo de atender una interrupción: unos 5 a 7 ciclos de respuesta más los del RETI y los que el compilador agrega para guardar y restaurar registros (se detalla en el tema 19).

Esta es la razón de ser de todo el bloque de tiempos: elegir el reloj (tema 12), medir el costo (este tema) y comprobar que el costo cabe en el presupuesto. Cuando no cabe hay tres salidas: simplificar el trabajo, subir el reloj o repartirlo (por ejemplo, delegarlo en un periférico).

08 · Analogía con programación

Calibrar, restar el sobrecosto y promediar

Este programa en Python modela una medición con el temporizador: el contador solo avanza de a una cuenta cada prescaler ciclos, el inicio cae en una fase al azar, y la medición incluye 4 ciclos de sobrecosto. Mide un código de 600 ciclos con tres prescalers y muestra cómo promediar recupera la precisión. Al final convierte anchos de pulso del osciloscopio a ciclos.

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

import random

random.seed(42)
SOBRECOSTO = 4          # ciclos que cuesta leer el temporizador dos veces
REAL = 600              # ciclos reales del código que queremos medir


def medir(ciclos_reales, prescaler):
    """Cuentas que lee el temporizador: el inicio puede caer en cualquier fase del prescaler."""
    fase = random.randrange(prescaler)
    total = ciclos_reales + SOBRECOSTO
    return (fase + total) // prescaler - fase // prescaler


print(f"Código real: {REAL} ciclos (sobrecosto de la medición: {SOBRECOSTO})")
for prescaler in (1, 8, 64):
    muestras = [medir(REAL, prescaler) for _ in range(1000)]
    una_vez = muestras[0] * prescaler - SOBRECOSTO
    promedio = sum(muestras) / len(muestras) * prescaler - SOBRECOSTO
    print(f"÷{prescaler:<3} cuentas leídas: {min(muestras)} a {max(muestras)}"
          f"  una medición -> {una_vez:>4} ciclos   promedio de 1000 -> {promedio:6.1f} ciclos")

print()
# Del ancho de pulso en el osciloscopio a los ciclos del código (marcas con SBI/CBI).
F_CPU = 16_000_000
for ancho_us in (37.625, 0.375, 1.0):
    ciclos_pulso = ancho_us * 1e-6 * F_CPU
    print(f"pulso de {ancho_us:>7} µs -> {ciclos_pulso:6.1f} ciclos en el pulso -> {ciclos_pulso - 2:6.1f} ciclos de código")

Resultado: con ÷1 cada medición da exactamente 600 ciclos; con ÷8, una sola lectura puede dar 596 pero el promedio de 1000 da 600,2; con ÷64, una lectura da 572 y el promedio 599,9. Cambiá REAL a otro valor, o el sobrecosto a 2, y comprobá que la resta lo corrige.

El modelo es determinista salvo por la fase del prescaler. En un chip real también influyen las interrupciones que ocurran durante la medición, que el modelo no incluye.

09 · Exploración interactiva

Contador de ciclos, pulsos y presupuesto

La primera herramienta ejecuta un fragmento de ensamblador AVR y cuenta los ciclos que consume, con la tabla de duración de cada instrucción. La segunda traduce lo que ves en el osciloscopio a ciclos. La tercera calcula cuántos ciclos tenés por evento y qué parte de la CPU consume tu tarea.

Banco de tiempos

1. Contador de ciclos

Ciclos totales—
Instrucciones—
Tiempo—
Pulso con marcas SBI/CBI—

2. Del osciloscopio a los ciclos

Ciclos en el pulso—
Ciclos de código (− 2 de la marca de fin)—

3. ¿Cuántos ciclos tengo por evento?

Ciclos disponibles por evento—
Uso de la CPU—

4. Temporizador 1 como cronómetro

Resolución y alcance según el prescaler (65 536 cuentas).
PrescalerResoluciónAlcance

El contador de ciclos es un modelo del conjunto de instrucciones, no un emulador del chip: ignora las interrupciones y los periféricos, y trata las lecturas con LD como accesos de 2 ciclos a una memoria ideal. Para el tiempo real de un programa con interrupciones, medí en el hardware. En el valor «Pulso con marcas», el resultado supone que se mide todo el fragmento entre una marca de inicio y una de fin.

Recorrido guiado

  1. Con «Suma de 100 bytes» comprobá los 600 ciclos y los 37,5 µs que calculamos a mano. Cambiá ldi r16, 100 por 50: ahora son 300 ciclos. Y el último brne sin saltar explica el «− 1».
  2. Cargá «Retardo de 100 vueltas»: 300 ciclos, la fórmula 3·N del tema 6. Cargá «Retardo anidado»: 3030 ciclos, casi 190 µs. Calculá a mano por qué es 3030 y no 3000.
  3. En la herramienta 2, el valor de partida (37,625 µs) corresponde al pulso de la suma de 100 bytes: 602 ciclos en el pulso y 600 de código. Probá con 125 ns: son 2 ciclos de pulso y 0 de código, el pulso vacío de la calibración.
  4. En la herramienta 3, con 11 520 eventos por segundo (bytes a 115 200 baudios) y 200 ciclos de tarea, la CPU ocupa el 14 %. Subí la tarea a 1500: supera el 100 %.
  5. En la herramienta 4, elegí 16 MHz y comprobá la tabla: ÷1 llega a 4,096 ms. ¿Qué prescaler necesitás para medir una tarea de 50 ms? (÷64: alcance de 262 ms.)
Desafío: predecir antes de medir

Escribí un bucle que haga ldi r16, 200 / dec r16 / brne y calculá a mano los ciclos antes de ejecutarlo: 3 × 200 = 600. Ejecutalo en el contador y compará. Después hacelo con nop adicionales dentro del bucle y repetí la predicción: cada nop agrega 200 ciclos.

10 · Método de trabajo

Procedimiento del taller

  1. Definí qué medís y contra qué límite: ¿cuántos ciclos hay disponibles entre eventos?
  2. Predecí: obtené el listado en ensamblador y contá los ciclos a mano o con la herramienta.
  3. Calibrá: medí un bloque vacío entre dos marcas (2 ciclos) o con el temporizador para conocer el sobrecosto.
  4. Medí el código real, con la optimización final y con las interrupciones que tendrá en uso.
  5. Comparé la medición con la predicción. Si difieren, buscá la causa antes de seguir: una instrucción de más, una interrupción, un salto que no previste.
  6. Medí el peor caso con datos que fuerzan el camino más lento, y calculá el porcentaje de CPU que consume.
  7. Documentá el resultado (ciclos, reloj y condiciones) junto al código, para que no se pierda cuando algo cambie.

Con este taller se cierra el bloque 3. Ya sabés de dónde sale el reloj, cómo arranca y se protege el chip y cuánto cuesta cada pedazo de código. El bloque 4 aborda la primera gran familia de periféricos: las entradas y salidas digitales.

11 · Comprensión

Confusiones frecuentes

«El pulso del osciloscopio mide exactamente el código»

Incluye el costo de la marca de fin (2 ciclos con CBI), y a veces la resolución del instrumento. Por eso se calibra con un pulso vacío y se resta lo que se mide en él.

«Si lo mido una vez, ya lo sé»

Una medición puede incluir una interrupción, la cuantización del prescaler o un camino de datos poco habitual. Se mide varias veces, y se registran mínimo y máximo.

«Más optimización del compilador siempre da más velocidad»

Casi siempre, pero -Os prioriza tamaño y -O2/-O3 pueden agrandar el código. Lo único seguro es medir. Además, una optimización mayor puede eliminar el código que querías medir.

«micros() de Arduino tiene precisión de microsegundo»

Tiene resolución de 4 µs a 16 MHz (el temporizador cuenta de a 4 µs) y su lectura cuesta decenas de ciclos. Sirve para medir milisegundos, no unas pocas decenas de ciclos.

«Un reloj más rápido arregla cualquier problema de tiempo»

Aumenta los ciclos disponibles por evento, pero el consumo sube y hay un límite de tensión. A veces conviene reducir el trabajo (un cálculo más simple, una tabla precalculada) antes de subir el reloj.

12 · Práctica guiada

Predecí antes de comprobar

1. Predecir un pulso

Un bucle de suma de 100 bytes cuesta 600 ciclos. Si se encierra entre SBI y CBI con un reloj de 16 MHz, ¿cuánto mide el pulso?

Ver solución

600 + 2 = 602 ciclos. 602 × 62,5 ns = 37,625 µs.

2. Leer el osciloscopio

El osciloscopio muestra un pulso de 37,75 µs con un reloj de 16 MHz. ¿Cuántos ciclos de código son y qué diferencia hay con la predicción de 600?

Ver solución

37,75 µs × 16 MHz = 604 ciclos en el pulso, es decir, 602 de código. Hay 2 ciclos más de lo previsto: típicamente una instrucción de dos ciclos que no se tuvo en cuenta (por ejemplo, un acceso a memoria o un RJMP). Conviene revisar el listado.

3. Elegir el prescaler

Querés medir con el temporizador 1 una tarea que dura unos 50 ms con un reloj de 16 MHz. ¿Qué prescaler usás y qué resolución obtenés?

Ver solución

Con ÷8, el alcance es de 32,8 ms: no alcanza. Con ÷64, el alcance es de 262 ms y la resolución de 4 µs: es el menor prescaler que sirve.

4. Presupuesto de una UART

Se recibe un byte cada 86,8 µs (115 200 baudios) con un reloj de 16 MHz. Una rutina de atención consume 250 ciclos. ¿Qué porcentaje de la CPU ocupa y qué margen queda?

Ver solución

Ciclos disponibles: 16 MHz × 86,8 µs ≈ 1389. 250 / 1389 ≈ 18 %. Quedan unos 1139 ciclos por byte para el resto del programa.

5. Un pulso que mide casi cero

Medís for (i = 0; i < 1000; i++) suma += i; y el pulso dura 2 ciclos. ¿Qué pasó y cómo se corrige?

Ver solución razonada

Si suma no se usa después, el compilador con optimización elimina todo el bucle, porque no tiene efecto observable. Se corrige haciendo suma volatile, o usando su valor (por ejemplo, escribirlo en un puerto o enviarlo por la UART) para que el cálculo se conserve.

13 · Síntesis

Ideas para recordar

  • Un AVR es determinista: sin caché ni sistema operativo, un fragmento tarda siempre los mismos ciclos (salvo interrupciones y datos).
  • Se mide de tres modos que se complementan: contar a mano, marca en un pin y temporizador interno.
  • Con marcas SBI/CBI el pulso dura los ciclos del código más 2; se calibra con un pulso vacío.
  • El temporizador 1 sin prescaler da una cuenta por ciclo y un alcance de 4,096 ms a 16 MHz; con prescaler aparece la cuantización.
  • Se mide el programa final, se usa el resultado para que el compilador no lo borre, y se registran mínimo y máximo.
  • El presupuesto de ciclos es f / (eventos por segundo); la tarea tiene que caber con margen.
  • Si lo medido no coincide con lo predicho, hay algo que entender antes de seguir.

¿Listo para el bloque 4?

Antes de avanzar, contá a mano los ciclos de un bucle de tu invención, explicá por qué el pulso mide 2 ciclos de más y decí cuántos ciclos disponés para atender un byte a 9600 baudios. Si podés justificar las tres respuestas, sabés poner números al tiempo.

En el próximo tema empieza el bloque de entradas y salidas digitales con GPIO: configurar y escribir pines: la dirección de cada pin, los registros de datos y las salidas digitales.