01 · Punto de partida
Sin osciloscopio, con intérprete
Hasta acá las señales se describieron con números, tablas y dibujos. Este tema las convierte en algo que se puede ejecutar: una lista de Python que representa un voltaje a lo largo del tiempo. Con ella podés generar una senoide de 50 Hz en cinco líneas, medirle el medio, el eficaz y la frecuencia en diez más, y verla en la consola en otras quince.
Una escena típica. Vas a armar un comparador que cuente los ciclos de una señal ruidosa. Antes de comprar una sola pieza, necesitás saber si la idea funciona: ¿con cuánto ruido empieza a contar de más? ¿qué umbral de histéresis hace falta? Podés tener un osciloscopio y un generador sobre la mesa, o podés escribir veinte líneas y experimentar ahora, con cualquier combinación de parámetros, cien veces, sin que se queme nada.
Es la misma idea que escribir primero la prueba con datos sintéticos (fixtures) y recién después conectar la base real.
Todo el toolkit usa solo la biblioteca estándar (math y random), sin numpy ni gráficos, para que corra en tu navegador y para que se vea cada paso. Vas a escribir cuatro herramientas que se reutilizan en el resto del curso:
- ¿Cómo genero las cuatro formas con amplitud, frecuencia, offset y duty?
- ¿Cómo estimo la frecuencia de una señal sin saberla, y cómo se rompe con ruido?
- ¿Cómo la grafico en texto para verla antes de creerle a los números?
- ¿Qué se pierde cuando se muestrea con pocas muestras por ciclo?
Python en tu navegador. Los bloques de este tema forman un toolkit: ejecutalos en orden, porque cada uno usa funciones definidas en los anteriores. La primera vez tarda unos segundos en cargar.
02 · Diseño
Una señal es una lista y una frecuencia de muestreo
Una señal continua tiene un valor en cada instante: infinitos números. Un programa solo puede guardar una cantidad finita, así que la muestrea (Tema 2): anota el voltaje cada 1/fs segundos, donde fs es la frecuencia de muestreo. El resultado es una lista de voltajes, y la lista sola no alcanza: sin fs no se sabe cuánto tiempo hay entre un elemento y el siguiente.
Tres decisiones de diseño gobiernan todo el toolkit:
- Cuántas muestras por ciclo
- Es
fs / f. Nyquist exige más de 2 (Tema 2), pero para ver bien la forma conviene muchas más. La regla práctica de este curso: unas 20 muestras por ciclo para trabajar tranquilo, no menos de 4 a 5 para ver algo. - Cuántos ciclos
- Un número entero de ciclos, para que el medio y el eficaz no se corran (Tema 6). La función generadora va a pedir cantidad de ciclos y no segundos, justamente por eso.
- Qué devuelve y qué recibe
- Cada herramienta recibe la lista (y
fscuando hace falta) y devuelve otra lista o un número. Sin estado global ni clases: son funciones que se pueden probar y combinar.
| Señal | fs / f | Muestras por ciclo | Calidad |
|---|---|---|---|
| 50 Hz | 2000 / 50 | 40 | Sobrado: se ve y se mide bien |
| 100 Hz | 2000 / 100 | 20 | Sobrado, en el límite recomendado |
| 440 Hz | 2000 / 440 | 4,5 | Justo: se reconoce la forma, se pierden picos |
| 900 Hz | 2000 / 900 | 2,2 | Al borde de Nyquist: la estimación tiembla |
| 1500 Hz | 2000 / 1500 | 1,3 | Por debajo de Nyquist: aliasing |
03 · Generar
Cuatro formas, una función
La función generar() produce senoide, cuadrada con duty, triangular y diente de sierra con los mismos parámetros que usa cualquier generador de funciones de laboratorio: amplitud, frecuencia, offset, duty y frecuencia de muestreo. Calcula la fase de cada muestra (la posición dentro del ciclo, de 0 a 1) y de ahí el valor según la forma:
import math
def generar(forma="seno", amp=5.0, f_hz=50.0, offset=0.0, duty=0.5, fs=2000.0, ciclos=4, fase0=0.0):
n = round(fs * ciclos / f_hz) # ciclos enteros: el medio no se corre (Tema 6)
v = []
for k in range(n):
fase = (k * f_hz / fs + fase0) % 1.0 # posición dentro del ciclo, de 0 a 1
if forma == "seno":
s = math.sin(2 * math.pi * fase)
elif forma == "cuadrada":
s = 1.0 if fase < duty else -1.0
elif forma == "triangular":
s = 4 * fase if fase < 0.25 else (2 - 4 * fase if fase < 0.75 else 4 * fase - 4)
else: # diente de sierra
s = 2 * fase - 1
v.append(offset + amp * s)
return v
onda = generar("seno", amp=5.0, f_hz=50.0, fs=2000.0)
print(f"{len(onda)} muestras (40 por ciclo): max={max(onda):.2f} min={min(onda):.2f}")
pwm = generar("cuadrada", amp=2.5, offset=2.5, duty=0.25, f_hz=100.0, fs=5000.0)
print(f"cuadrada 0-5 V al 25 %: max={max(pwm):.2f} min={min(pwm):.2f}, {len(pwm)} muestras")
Cuatro ciclos a 50 Hz con fs de 2 kHz son 160 muestras. Para una cuadrada de 0 a 5 V se usa amp=2.5 y offset=2.5, igual que en el Tema 3. Probá una triangular de ±3 V.
04 · Medir
Medir: medio, eficaz, pico a pico y frecuencia
Medio, eficaz y Vpp son las fórmulas de los Temas 3 y 6 aplicadas a la lista. La frecuencia hay que estimarla: se cuentan las subidas, es decir, las veces que la señal cruza hacia arriba su valor medio, y se divide por el tiempo transcurrido.
Para que esa cuenta resista el ruido, la función usa un disparador de dos umbrales (en electrónica, un trigger Schmitt o comparador con histéresis, Tema 20). La señal solo cuenta como «alta» cuando supera el medio más la histéresis, y solo vuelve a «baja» cuando cae por debajo del medio menos la histéresis. Con histéresis 0 es el cruce simple.
def frecuencia(v, fs, histeresis=0.0):
# Cuenta subidas con un disparador de dos umbrales (medio +- histéresis)
medio = sum(v) / len(v)
alto = False
subidas = []
for k, x in enumerate(v):
if not alto and x > medio + histeresis:
alto = True
subidas.append(k)
elif alto and x < medio - histeresis:
alto = False
if len(subidas) < 2:
return None
return (len(subidas) - 1) * fs / (subidas[-1] - subidas[0])
def analizar(v, fs, histeresis=0.0):
medio = sum(v) / len(v)
rms = math.sqrt(sum(x * x for x in v) / len(v))
vpp = max(v) - min(v)
f = frecuencia(v, fs, histeresis)
f_txt = "sin datos" if f is None else f"{f:.1f} Hz"
print(f"medio={medio:.2f} V rms={rms:.2f} V Vpp={vpp:.2f} V f estimada={f_txt}")
return medio, rms, vpp, f
analizar(generar("seno", f_hz=50.0, fs=2000.0), 2000.0)
analizar(generar("triangular", amp=3.0, f_hz=100.0, fs=5000.0), 5000.0)
analizar(generar("cuadrada", amp=2.5, offset=2.5, duty=0.25, f_hz=100.0, fs=5000.0), 5000.0)
Fijate en las dos últimas líneas: la triangular de ±3 V mide 5,76 V de pico a pico en lugar de 6, porque el vértice cae entre dos muestras; y la cuadrada al 25 % da medio 1,30 V en lugar de 1,25, porque 12,5 muestras no existen: son 13 de 50 (26 %). Son errores de discretización del muestreo, no bugs.
La frecuencia se calcula entre la primera y la última subida (no sobre el total de muestras): así no importa si la lista empieza o termina a mitad de un ciclo.
05 · Graficar
Graficar sin librerías: el scope de texto
Antes de creerle a un número, mirá la forma. Un gráfico de texto de unas diez líneas alcanza para ver el offset, el recorte, el duty y el ruido. La función escala los valores al rango de la señal y escribe un asterisco por columna en la fila que corresponde:
def ascii_plot(v, ancho=64, alto=11):
lo, hi = min(v), max(v)
span = (hi - lo) or 1.0
paso = max(1, len(v) // ancho)
muestras = v[::paso][:ancho]
filas = [[" "] * len(muestras) for _ in range(alto)]
for c, x in enumerate(muestras):
filas[round((hi - x) / span * (alto - 1))][c] = "*"
for i, fila in enumerate(filas):
nivel = hi - i * span / (alto - 1)
print(f"{nivel:+7.2f} |{''.join(fila)}")
ascii_plot(generar("seno", amp=5.0, f_hz=50.0, fs=2000.0, ciclos=2))
print()
ascii_plot(generar("cuadrada", amp=2.5, offset=2.5, duty=0.25, f_hz=50.0, fs=2000.0, ciclos=2))
En la cuadrada se ven los dos niveles (5 V arriba, 0 V abajo) y que el nivel alto ocupa un cuarto de cada ciclo. ¿Qué cambia si le ponés duty=0.5?
06 · Ruido
Ensuciar a propósito: ruido e histéresis
Una señal de laboratorio, sin ruido, no se parece a nada real. Para probar un algoritmo hay que ensuciarla. Con random.gauss(0, sigma) se suma ruido gaussiano de media cero y desviación sigma (en voltios) a cada muestra:
import random
random.seed(7) # misma semilla: el experimento se puede repetir
def con_ruido(v, sigma=1.0):
return [x + random.gauss(0, sigma) for x in v]
limpia = generar("seno", amp=5.0, f_hz=50.0, fs=2000.0, ciclos=10)
sucia = con_ruido(limpia, sigma=1.0)
for nombre, v, h in (("limpia", limpia, 0.0), ("sucia sin histéresis", sucia, 0.0),
("sucia con histéresis 2 V", sucia, 2.0)):
print(f"{nombre:28s}", end=" ")
analizar(v, 2000.0, histeresis=h)
Con 1 V de ruido, el medio y el eficaz casi no cambian (0,02 V y 3,71 V contra 3,54 V), pero el pico a pico crece de 10 a 14,3 V porque el ruido suma sus propios picos, y la frecuencia sin histéresis sale en 95 Hz en lugar de 50: casi el doble. Con 2 V de histéresis vuelve a 50,3 Hz.
Sin ruido
Cruces exactos
Cada subida es real. La frecuencia estimada coincide con la verdadera y el pico a pico es el de la señal.
Con ruido
Cruces falsos
El ruido cruza y vuelve a cruzar el medio cerca de cada flanco, y la cuenta se dispara. La solución es una histéresis (mayor que el ruido) o filtrar antes (Temas 20 y 23).
Fijate qué se salva y qué no. El medio y el eficaz son promedios y toleran bien el ruido (Tema 6). El pico a pico y los cruces dependen de valores puntuales y son frágiles. Es una regla útil: ante una señal ruidosa, preferí las medidas que promedian.
07 · Límites
Los límites del muestreo: aliasing y pico perdido
El toolkit también sirve para ver con tus propios ojos lo que dice la teoría del Tema 2. Dos experimentos cortos, que podés repetir con cualquier frecuencia:
print("Aliasing: senoide de 1500 Hz muestreada a 2000 Hz")
analizar(generar("seno", f_hz=1500.0, fs=2000.0, ciclos=30), 2000.0)
print("Pico perdido: senoide de 10 Vpp, 50 Hz, según las muestras por ciclo")
for por_ciclo in (2.5, 4, 8, 20, 100):
fs = por_ciclo * 50.0
v = generar("seno", amp=5.0, f_hz=50.0, fs=fs, ciclos=40, fase0=0.1)
vpp = max(v) - min(v)
print(f" {por_ciclo:5} muestras/ciclo -> Vpp medido = {vpp:5.2f} V (error {10 - vpp:4.2f} V)")
La senoide de 1500 Hz se estima como 500 Hz (2000 − 1500): una frecuencia que no existe, con total confianza. Y el pico a pico mide 9,51; 8,09; 9,88; 10,00 y 10,00 V: no es monótono, depende de en qué fase caen las muestras respecto del pico.
- Aliasing
- Si fs ≤ 2·f, una frecuencia alta se disfraza de baja (|f − fs·k|, con k el múltiplo más cercano). El análisis no puede darse cuenta: la lista de números es compatible con las dos. No se arregla después; se evita antes (filtrando lo que supere fs/2 o subiendo fs).
- Pico perdido
- Con pocas muestras por ciclo es fácil que ninguna caiga justo en el máximo. El Vpp medido subestima el real, y el error cambia con la fase de arranque. A partir de unas 20 muestras por ciclo el pico se captura casi completo. El error es más persistente cuando fs y f guardan una relación simple (como 2,5 o 4 muestras por ciclo): las muestras caen siempre en las mismas fases del ciclo y nunca «prueban» otras. Con una relación desacoplada y una captura larga, algún punto termina cayendo cerca del pico.
- Resolución en tiempo
- Todo lo que ocurra entre dos muestras (un pico de ruido, un rebote corto) es invisible. Es la contraparte en el tiempo del escalón del ADC en tensión.
08 · Simulador
Generador y analizador en vivo
Es el mismo toolkit, escrito en JavaScript para que lo muevas con controles. Elegí la forma, la frecuencia y la frecuencia de muestreo, sumale ruido y probá la histéresis. La línea clara es la señal «real» continua; los puntos turquesa son las muestras que ve tu programa; los triángulos naranjas son las subidas que cuenta el analizador.
Generador + analizador
seno · 5 V · 50 Hz · fs 2 kHz
Recorrido guiado (6 minutos)
- 1Empezá con «Bien muestreada».
Senoide de 50 Hz a 2 kHz: 40 muestras por ciclo, 4 subidas detectadas, 10 Vpp medidos y 50 Hz estimados. Es el caso de libro.
- 2Pasá a «PWM 25 %».
Cuadrada de 0–5 V a 100 Hz con fs de 5 kHz. El medio sale 1,30 V y no 1,25: la discretización del duty (13 de 50 muestras).
- 3Pasá a «Pocas muestras».
440 Hz a 1 kHz son apenas 2,3 muestras por ciclo: en el gráfico la forma casi no se reconoce. Fijate que el analizador, con una captura larga, igual estima unos 440 Hz; el dibujo es lo que miente. Subí fs a 5 kHz y la señal «aparece».
- 4Pasá a «Aliasing».
1,5 kHz muestreada a 2 kHz. El analizador informa 500 Hz (30 subidas en 90 ciclos, en lugar de 90) con total seguridad: es la sombra de 2000 − 1500. Subí fs a 5 kHz y vuelve la frecuencia correcta.
- 5Pasá a «Ruido sin histéresis».
Con 1 V de ruido los triángulos naranjas se multiplican alrededor de cada flanco y la frecuencia estimada se dispara muy por encima de 50 Hz. El medio y el eficaz casi no se mueven.
- 6Pasá a «Ruido con histéresis» y probá umbrales.
Con 2 V de histéresis el analizador ignora los cruces falsos y vuelve a 4 subidas. Si la histéresis es menor que el ruido, el problema reaparece; si es mayor que la amplitud, deja de contar. Hay un rango útil.
Preguntas para pensar
- ¿Por qué el medio y el eficaz sobreviven al ruido y los cruces no?
- Una señal de 440 Hz se muestrea a 1 kHz. ¿Qué dos cosas puede hacer el programador para medirla bien?
- ¿Cómo elegirías la histéresis para una señal de 5 V de pico y 1 V de ruido?
Ver respuestas sugeridas
- Porque son promedios: el ruido, que sube y baja sin favorecer un lado, se compensa. Los cruces dependen de valores puntuales cerca del umbral, justo donde un poco de ruido decide si cuenta o no.
- Subir fs (a 5 kHz o más, para tener 10 o más muestras por ciclo) o, si no se puede, filtrar la señal antes de muestrearla para que no tenga nada por encima de fs/2.
- Mayor que el ruido pico (unas 3σ = 3 V si σ = 1 V, aunque eso ya es la mitad de la amplitud) y menor que la amplitud de la señal. En la práctica, 2 V: deja margen a ambos lados. Con ruido más grande, hay que filtrar primero.
09 · Analogía de programador
Observabilidad de series temporales
| En software | En el toolkit de señales |
|---|---|
| Datos sintéticos (fixtures) para probar antes de producción | generar(): señales con forma conocida |
| Un test con inyección de fallos | con_ruido(): ensuciar a propósito |
| Detectar eventos en un stream (flancos, webhooks) | Contar subidas con frecuencia() |
| Un debounce o un umbral con histéresis para evitar flapping | La histéresis del disparador |
Logging por consola (printf-debugging) | ascii_plot(): ver sin GUI |
| Una semilla fija para reproducir un test | random.seed(7) |
| Muestreo (sampling) de métricas y pérdida de picos | Aliasing y pico perdido por pocas muestras por ciclo |
La más importante es la última: el muestreo de una métrica a un intervalo largo oculta los picos cortos, igual que el pico perdido de este tema. El remedio es el mismo en software y en electrónica: muestrear más rápido que lo que querés ver, o medir otra cosa (un máximo retenido, un promedio) que no dependa de pillar el instante justo.
10 · Comprensión
Confusiones frecuentes
«Más muestras siempre es mejor»
Mejor hasta sobrar (unas 20 por ciclo). Más allá solo gasta CPU y memoria sin información nueva. Y no arregla ni el ruido ni el aliasing: el aliasing se arregla con una fs suficiente desde el principio, no con más muestras de una captura mal tomada.
«Contar cruces da la frecuencia siempre»
Solo sin ruido y con fs holgada. Con ruido o con un medio mal ubicado los cruces mienten; con aliasing, mienten con seguridad. Mirá la forma primero.
«Si el programa mide 5,76 V es que el generador está mal»
Puede estar bien: el vértice de una triangular cae entre dos muestras. Con pocas muestras por ciclo, el pico a pico medido subestima el real; no es un bug, es la discretización.
«El ruido solo molesta un poco»
Depende de qué midas. Al medio y al eficaz casi no los toca; al pico a pico y a los cruces los puede arruinar. El diseño tiene que apoyarse en las medidas robustas o filtrar antes.
«Random sin semilla da igual»
Sin random.seed() cada ejecución da un ruido distinto y el experimento no se puede repetir ni comparar. Fijar la semilla hace reproducible el experimento (y el bug).
«Un gráfico de texto no sirve para nada serio»
Sirve para la mayoría de los casos: offset, recorte, duty y ruido se ven en texto. Para la última milésima hace falta un graficador real, pero el primer diagnóstico casi nunca lo necesita.
11 · Práctica guiada
Ejercicios de toolkit
Ejercicio 1: reproducir el caso de libro
Generá una senoide de ±5 V a 50 Hz con fs de 2000 y ejecutá analizar(). ¿Cuánto da el medio, el eficaz, el Vpp y la frecuencia? Compará con el simulador.
Ver solución razonada
Medio ≈ 0 V, eficaz 3,54 V, Vpp 10 V y f 50 Hz. El código y el simulador deben coincidir: es la misma matemática.
Ejercicio 2: cazar el aliasing
Generá una senoide de 900 Hz con fs de 1000 Hz (usá ciclos=40 para tener suficientes muestras) y analizala. ¿Qué frecuencia estima? ¿Qué fs mínima exige la regla de las 20 muestras por ciclo?
Ver solución razonada
La frecuencia aparente es |900 − 1000| = 100 Hz, con total confianza. Con 20 muestras por ciclo hace falta fs = 20 × 900 = 18 kHz; el mínimo teórico (Nyquist) sería algo más de 1800 Hz, pero con eso apenas se reconocería la onda.
Ejercicio 3: el duty que se discretiza
Generá una cuadrada de 0–5 V al 25 % con f_hz=100 y fs de 5000, y después con fs de 10000. ¿Cómo cambia el medio?
Ver solución razonada
Con fs = 5000: 50 muestras por ciclo; 25 % serían 12,5, y salen 13: medio 1,30 V. Con fs = 10000: 100 muestras por ciclo, 25 exactas: medio 1,25 V. Más muestras por ciclo achican el error de discretización del duty.
Ejercicio 4: elegir la histéresis
Ensuciá una senoide de ±5 V con sigma=1.0 y calculá la frecuencia con histéresis 0, 1, 2 y 3 V. ¿Qué observás? ¿Cuál elegirías?
Ver solución razonada
Con 0 V sale ≈ 95 Hz (cruces falsos); desde 1 V ya da 50 Hz. Con 2 o 3 V sigue correcto y sobra margen. Se elige 2 V: más del doble del desvío del ruido y bastante menor que los 5 V de pico. Una histéresis muy grande podría hacer que el analizador no vea nada en señales chicas.
Ejercicio 5: escribí tu propia función
Escribí ciclo_de_trabajo(v), que devuelva el porcentaje de muestras por encima del valor medio. Probala con la cuadrada al 25 % y con una senoide. ¿Qué da en cada caso?
Ver guía
Algo como 100 * sum(1 for x in v if x > sum(v)/len(v)) / len(v). En la cuadrada al 25 % da cerca de 26 % (13 de 50 muestras, por la discretización) y en la senoide, cerca de 50 %. Es una medida honesta del duty para cuadradas y una curiosidad en otras formas.
12 · Síntesis
Ideas para recordar
- Una señal en código es una lista de voltajes más una frecuencia de muestreo fs. Sin fs no hay tiempo.
- Generar con parámetros (forma, amplitud, f, offset, duty) y con un número entero de ciclos; unas 20 muestras por ciclo para trabajar tranquilo.
- Medio y eficaz son promedios y toleran el ruido; el pico a pico y los cruces son frágiles.
- La frecuencia se estima contando subidas con un disparador de dos umbrales: la histéresis evita los cruces falsos.
- Graficar en texto antes de creerle a los números revela offset, recorte, duty y ruido.
- Con pocas muestras: aliasing (una frecuencia falsa, sin aviso) y pico perdido (Vpp subestimado).
- Semilla fija en
randompara experimentos repetibles; mismas funciones para simular antes de armar.
¿Listo para el Tema 8?
Podés avanzar si sos capaz de responder esto sin mirar:
- Explicar por qué una señal en código necesita una lista y una fs.
- Calcular cuántas muestras por ciclo hay para una f y una fs dadas.
- Describir cómo estima la frecuencia el analizador y para qué sirve la histéresis.
- Predecir qué frecuencia aparente produce el aliasing de 900 Hz muestreada a 1 kHz.
- Decir qué medidas resisten al ruido y cuáles no.
- Dar un ejemplo de por qué se grafica antes de medir.
Toolkit listo para todo el curso. Ahora, a medir de verdad: en el próximo tema, medir señales con multímetro y osciloscopio.