Electrónica Digital · Tema 28

Hazards y glitches

La tabla de verdad dice que la salida se queda en 1. El osciloscopio muestra un pozo de pocos nanosegundos. No hay error de lógica: hay tiempo. Hasta ahora supusimos que las puertas responden al instante, y en este tema aparece lo que pasa cuando no: caminos con distinto retardo que se vuelven a juntar y dejan, por un instante, un valor que nadie pidió.

≈ 60 minutos Sin materiales: solo el navegador Incluye simulador de formas de onda y Python

01 · Punto de partida

El multiplexor que parpadea

Un multiplexor de dos entradas (Tema 18) elige entre B y C según A: Y = A·B + ~A·C. Si B y C valen los dos 1, la salida tiene que ser 1 sin importar A. Cambiar el selector no debería afectar a Y. Y sin embargo, al pasar A de 1 a 0, la salida puede caer a 0 por un instante y volver a 1.

Una escena típica. Armás un selector de fuentes con puertas y alimentás un contador con su salida. Las dos fuentes valen 1; solo conmutás cuál se usa. Pero el contador cuenta un pulso de más: ese pozo de 10 nanosegundos en Y, invisible a ojo, es un flanco falso que el contador interpretó como un pulso de reloj.

La tabla estaba bien. Lo que falló fue el tiempo: el término A·B se apaga antes de que el término ~A·C se encienda, y hay un momento en que nadie sostiene la salida.

  • ¿Por qué un camino llega antes que otro?
  • ¿Qué tipos de glitch hay y cuándo importan?
  • ¿Cómo se ve un hazard en un mapa de Karnaugh y cómo se tapa?
  • ¿Por qué un cambio de varias entradas a la vez no tiene arreglo lógico?
  • ¿Qué hace el diseño síncrono con los glitches?
  • ¿Se pueden usar a favor?

02 · La causa

Retardos y reconvergencia

Una puerta real no responde de inmediato: tarda un retardo de propagación (en la serie 74HC, del orden de diez nanosegundos; el valor exacto depende del chip y de la tensión, confirmalo en la hoja de datos). Dos caminos desde la misma entrada hasta la misma salida casi nunca tardan lo mismo. En el multiplexor:

Los dos caminos de A hasta Y en Y = A·B + ~A·C. Un nivel por puerta.
CaminoPuertasRetardo
A → AND → ORAND, OR2 pasos
A → NOT → AND → ORNOT, AND, OR3 pasos (el NOT agrega uno)

La señal se divide (A y ~A) y después se vuelve a juntar en la OR: se llama reconvergencia. Si A cae de 1 a 0 con B = C = 1:

  1. 1
    El término A·B se apaga rápido.

    Su camino es corto: apenas la AND ve A en 0, cae.

  2. 2
    El término ~A·C todavía no se encendió.

    ~A tiene que pasar por el NOT antes de llegar a la AND. Entre medio, las dos entradas de la OR valen 0.

  3. 3
    La OR muestra 0 un instante.

    Hasta que el segundo término sube y la salida vuelve a 1. Eso es el pozo.

Si los retardos fueran iguales
El pozo desaparece: el ancho es 0. Por eso un simulador «ideal», sin retardos, nunca muestra un glitch. Hace falta una simulación temporal (con retardos) o un osciloscopio.
Con A en subida, no pasa
Al pasar A de 0 a 1, el término A·B se enciende antes de que ~A·C se apague: durante un rato los dos valen 1 y la OR queda en 1. La asimetría viene del NOT, que está solo en un camino.
El álgebra no ve el tiempo
Y = A·B + ~A·C es lógicamente igual a un multiplexor perfecto. Dos circuitos con la misma tabla de verdad pueden tener comportamiento temporal distinto. Es una propiedad del circuito, no de la función.

03 · Tipos

Estáticos, dinámicos y de función

Clasificación de los hazards.
TipoLa salida deberíaHaceDónde aparece
Estático en 1 (static-1)quedarse en 1pozo a 0Suma de productos (AND-OR) cuando una entrada cambia entre dos términos
Estático en 0 (static-0)quedarse en 0pico a 1Producto de sumas (OR-AND): el dual
Dinámicocambiar una vezcambia tres o más vecesEn general, con tres o más caminos que se vuelven a juntar

Una segunda clasificación importa más para saber si hay arreglo:

Hazard lógico: una entrada cambia
Es consecuencia de cómo se armó el circuito. Se puede evitar con lógica redundante bien elegida (sección siguiente). Es el que viste en el multiplexor.
Hazard de función: varias entradas cambian a la vez
Es consecuencia de la función y de que las entradas no cambian exactamente al mismo tiempo. Ningún circuito lo evita con lógica, porque durante un instante las entradas valen una combinación intermedia que sí tiene otra salida. Se evita con otro código de entrada (Gray) o con diseño síncrono.

04 · Mapa de Karnaugh

Detectar y tapar

En el mapa de Karnaugh (Tema 16), un hazard estático en 1 es muy fácil de ver. Para Y = A·B + ~A·C:

Mapa de Y = A·B + ~A·C. Columnas: BC en orden Gray.
A \ BC00011110
A = 00110
A = 10011

Los grupos mínimos son dos: ~A·C (los dos unos de la fila A = 0, columnas 01 y 11) y A·B (los de A = 1, columnas 11 y 10). Pero los unos de la columna 11 son vecinos (B = C = 1, A cambia) y cada uno pertenece a un grupo distinto. Cuando A cambia, la salida pasa de un grupo al otro, y ahí está el hazard.

El grupo que cubre los dos unos de la columna 11 es B·C: el término de consenso (Tema 14). Es lógicamente redundante (no cambia la función) pero físicamente sostiene la salida en 1 mientras A conmuta, porque no depende de A.

Regla general (static-1)
Un circuito de suma de productos es libre de hazards estáticos en 1 cuando todo par de unos vecinos está cubierto por un mismo implicante. Es suficiente incluir todos los implicantes primos (Tema 15), no solo los esenciales.
Qué cuesta
Un término más (una puerta AND y una entrada más en la OR). La cobertura mínima que minimizaste en el Tema 15 no es, en general, la libre de hazards.
El dual para static-0
En el producto de sumas, el pico a 1 aparece cuando dos ceros vecinos no comparten ninguna suma. Se tapa con la suma de consenso: para Y = (A + B)·(~A + C), el término (B + C).
Solo una entrada cambia
Esta garantía vale únicamente cuando una entrada cambia por vez. Con varias, entra en juego el hazard de función (sección siguiente).

05 · Varios cambios a la vez

Hazards de función: el contador y el decodificador

Un contador pasa de 0111 a 1000: cuatro bits cambian a la vez. En la práctica no cambian exactamente al mismo tiempo (caminos y cargas distintas): hay un instante en que se ve una dirección intermedia. Según el orden en que lleguen los cambios, la dirección puede pasar por 1111, por 0000, por 0101 o por cualquier combinación que no es ni la inicial ni la final.

Direcciones intermedias posibles al pasar de 0111 a 1000 (cuatro bits cambian).
Primer bit en llegarDirección que se ve un instante
bit 31111
bit 20011
bit 10101
bit 00110

Y después vienen más: con los dos primeros que llegan hay seis combinaciones de intermedias más, y con tres, cuatro más. En total, 14 de las 16 direcciones pueden aparecer como visitas efímeras. Un decodificador (Tema 20) conectado a ese bus verá, por un instante, cualquiera de sus salidas en 1: un pulso falso en una línea de selección. Es el glitch de los demux que viste en el Tema 19, ahora con nombre.

No se arregla con lógica
Las entradas del decodificador valen, durante ese instante, una dirección válida. La salida es la correcta para esa dirección. No hay término redundante que lo evite.
Código Gray (Tema 5)
En un código Gray consecutivo cambia un solo bit por paso. Sin cambios múltiples, no hay direcciones intermedias. Contando 0 a 15 con 4 bits, el binario cambia 30 bits en una vuelta completa y el Gray solo 16 (uno por paso). Por eso se usa en encoders rotativos y en punteros de colas asíncronas.
Habilitación posterior (strobe)
Se mantiene el decodificador deshabilitado mientras las direcciones cambian y se habilita (pin de enable, Tema 20) cuando ya se asentaron. Las salidas solo se ven durante el pulso, que es limpio.
Registrar la dirección
Muestrear el bus con un registro disparado por un reloj y decodificar la copia registrada: la copia cambia en un solo instante y limpia.

06 · Convivir con los glitches

Ignorarlos, taparlos o aprovecharlos

Duelen

Relojes, enables y sets asíncronos

Una entrada que reacciona al flanco o al nivel sin esperar (reloj de un contador, enable de un latch, set o reset asíncrono) convierte el pozo en un evento fantasma.

No duelen

LEDs y señales muestreadas después

Unos nanosegundos son invisibles a un LED. Y una entrada que se mira solo en un flanco de reloj, cuando ya se asentó, ignora el glitch por construcción.

Diseño síncrono
Toda la lógica combinacional alimenta registros que se disparan con el mismo reloj. Mientras el período sea mayor que el retardo del camino más largo más el tiempo de preparación del registro, los glitches mueren antes de que nadie los mire. Es la regla de oro: las salidas combinacionales no se usan como reloj ni como señal asíncrona.
Redundancia
El consenso tapa el hazard estático, pero cuesta puertas y solo vale para un cambio por vez. Se usa en señales críticas sin reloj.
Filtros
Un condensador y una resistencia (Tema 11) alisan pulsos cortos; con un disparador Schmitt a la salida, el glitch desaparece. Pero también alarga los flancos legítimos y acota la velocidad.
Consumo
Cada transición espuria carga y descarga capacidades: los glitches gastan energía dinámica. En circuitos grandes son una parte apreciable del consumo, y es una de las razones de las buenas prácticas del próximo tema.
Glitch útil: el detector de flancos
Y = A·~A vale 0 por el álgebra (Tema 8), pero si el NOT tarda, en cada subida de A hay un instante en que A ya es 1 y ~A todavía vale 1: sale un pulso de ancho igual al retardo del NOT. Con un XOR entre A y una copia retardada se obtiene un pulso en ambos flancos. Es una técnica real de generadores de pulsos (Paso 6).

07 · Simulador

Laboratorio: cazar el pozo

El simulador dibuja, paso a paso, las formas de onda de un circuito con retardos. Cada puerta tarda un paso, salvo el NOT, cuyo retardo regulás con un deslizador. En t = 2 cambia la entrada A. Podés elegir entre cinco circuitos: el multiplexor en suma de productos (static-1), el mismo con el término de consenso, el producto de sumas (static-0), ese mismo con su consenso, y el detector de flancos Y = A·~A.

  • Líneas verdes: las señales; la de abajo es la salida Y, con la zona del glitch resaltada en rojo.
  • Línea punteada: el instante en que A cambia.
  • Resultados: la secuencia de Y, cuántas transiciones tuvo contra las esperadas, el ancho del glitch (en pasos y, a modo orientativo, en ns) y el veredicto.
SIMULADOR 28

Caza-pozos con retardos

SOP · A 1→0 · pozo

Formas de onda de un circuito con retardos Filas con las señales internas y la salida Y en función del tiempo. La entrada A cambia en t igual a 2. La salida muestra un pozo, un pico o un pulso cuando los caminos tienen distinto retardo. TIEMPO → CADA PASO ES UN RETARDO DE PUERTA
Y(t), t = 0 a 14—
Transiciones de Y (reales / esperadas)—
Ancho del evento—
Estado—

Multiplexor en suma de productos con B = C = 1 y A pasando de 1 a 0.

Recorrido guiado (10 minutos)

  1. 1
    Mirá el estado inicial: SOP, A 1→0, NOT de 1 paso.

    A cae en t = 2. El término A·B baja en t = 3, pero ~A·C sube recién en t = 4. Y cae en t = 4 y vuelve en t = 5: un pozo de 1 paso, aunque la tabla diga que Y vale 1 siempre.

  2. 2
    Subí el retardo del NOT a 3.

    El pozo se ensancha a 3 pasos: su ancho es la diferencia entre los retardos de los dos caminos, o sea el retardo del NOT.

  3. 3
    Bajalo a 0.

    Sin diferencia entre los caminos, el glitch desaparece. Un simulador ideal (sin retardos) nunca lo habría mostrado.

  4. 4
    Con el NOT en 1, pasá a «A: 0→1».

    Limpio: A·B sube en t = 3 antes de que ~A·C baje, así que durante un tiempo los dos valen 1 y la OR no cae.

  5. 5
    Volvé a «A: 1→0» y elegí «SOP + B·C».

    El término de consenso vale 1 todo el tiempo (B y C no cambian) y sostiene la OR. Sin pozo. Es el implicante que el mapa de Karnaugh pedía.

  6. 6
    Elegí «POS» con A 0→1.

    El dual: un pico a 1 donde la salida debería quedar en 0 (con B = C = 0). Dos sumas valen 1 a la vez un instante. Con A 1→0 es limpio.

  7. 7
    Elegí «POS + (B+C)».

    La suma de consenso vale 0 todo el tiempo y fuerza la salida en 0: el pico se va.

  8. 8
    Elegí «A·~A» con A 0→1 y NOT de 3.

    El álgebra dice que Y = 0 siempre, pero hay un pulso de 3 pasos al subir A. Con A 1→0, nada. Es un detector de flancos hecho con un glitch.

  9. 9
    Con «A·~A», mové el deslizador.

    El ancho del pulso es el retardo del NOT. Con 0 desaparece: lo que antes era un error es ahora el funcionamiento del circuito.

  10. 10
    Mirá la tarjeta de ancho en ns.

    A 10 ns por paso (un valor orientativo para la serie 74HC), un pozo de 1 paso son unos 10 ns: imperceptible para un LED, mortal para la entrada de reloj de un contador.

Preguntas para pensar

  1. Con SOP y NOT de 1, ¿qué sentido de A produce pozo y por qué solo uno?
  2. ¿Qué pasa con el pozo si el NOT tarda 0?
  3. ¿Por qué el término B·C lo tapa?
  4. En A·~A, ¿qué sentido de A produce el pulso?
Ver respuestas sugeridas
  1. A de 1 a 0. Al revés (0 a 1) los dos términos valen 1 durante un rato y la OR no cae: la asimetría viene del NOT, que está en un solo camino.
  2. Desaparece: sin diferencia de retardos entre los caminos no hay ventana donde ambos términos valgan 0.
  3. Porque no depende de A: vale 1 durante toda la transición (con B = C = 1) y sostiene la salida sin importar qué hagan los otros dos términos.
  4. La subida (0 a 1): A ya vale 1 y ~A todavía no bajó, así que la AND ve 1 y 1 durante el retardo del NOT. En la bajada, A ya vale 0 y la AND no se activa.

08 · Glitches en código

Simular retardos, buscar hazards, contar bits y muestrear

Los bloques construyen un pequeño simulador de retardos, reproducen el pozo del multiplexor y miden su ancho contra el retardo del NOT, buscan hazards estáticos con mapas de Karnaugh de verdad (con todas las funciones de tres variables), muestran las direcciones intermedias de un contador binario frente a un Gray, calculan cuándo es seguro muestrear una salida y miden el pulso de un detector de flancos.

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

Paso 1: un simulador de retardos y el pozo del mux

T, TA = 14, 2                                   # pasos simulados; A cambia en t = 2
NOT = lambda a: 1 - a
AND = lambda *x: int(all(x))
OR = lambda *x: int(any(x))


def simular(red, entradas):
    ini, fin, onda = {}, {}, {}
    for n, (v0, v1) in entradas.items():
        ini[n], fin[n] = v0, v1
        onda[n] = [v0 if t < TA else v1 for t in range(T)]
    for nombre, f, fuentes, d in red:             # en orden: cada nodo depende de los anteriores
        ini[nombre] = f(*[ini[s] for s in fuentes])
        fin[nombre] = f(*[fin[s] for s in fuentes])
        onda[nombre] = [f(*[onda[s][t - d] if t - d >= 0 else ini[s] for s in fuentes]) for t in range(T)]
    return onda, ini, fin


def mux_sop(a0, a1, t_not=1, consenso=False):
    red = [("NA", NOT, ["A"], t_not), ("P1", AND, ["A", "B"], 1), ("P2", AND, ["NA", "C"], 1)]
    fuentes = ["P1", "P2"]
    if consenso:
        red.append(("P3", AND, ["B", "C"], 1))
        fuentes.append("P3")
    red.append(("Y", OR, fuentes, 1))
    return simular(red, {"A": (a0, a1), "B": (1, 1), "C": (1, 1)})


dibujo = lambda onda: "".join("▔" if v else "▁" for v in onda)

onda, ini, fin = mux_sop(1, 0, 1)
print("A 1->0, B = C = 1, retardo de cada puerta = 1 paso, t = 0 ... 13")
for nombre in ("A", "NA", "P1", "P2", "Y"):
    print(f"{nombre:3s} {dibujo(onda[nombre])}")
print()
for etiqueta, a0, a1, cons in (("sin consenso, A 1->0", 1, 0, False), ("sin consenso, A 0->1", 0, 1, False),
                               ("con consenso B·C, A 1->0", 1, 0, True)):
    o, _, _ = mux_sop(a0, a1, 1, cons)
    print(f"{etiqueta:<28} Y = {dibujo(o['Y'])}")

Con A de 1 a 0, P1 baja en t = 3 y P2 sube recién en t = 4: Y tiene un 0 en t = 4 (un paso de pozo). Con A de 0 a 1 no hay pozo, y con el término B·C tampoco. El simulador reproduce lo que se ve en las formas de onda del laboratorio.

Paso 2: el ancho es el retardo del NOT (y el dual)

T, TA = 14, 2
NOT = lambda a: 1 - a
AND = lambda *x: int(all(x))
OR = lambda *x: int(any(x))


def simular(red, entradas):
    ini, fin, onda = {}, {}, {}
    for n, (v0, v1) in entradas.items():
        ini[n], fin[n] = v0, v1
        onda[n] = [v0 if t < TA else v1 for t in range(T)]
    for nombre, f, fuentes, d in red:
        ini[nombre] = f(*[ini[s] for s in fuentes])
        fin[nombre] = f(*[fin[s] for s in fuentes])
        onda[nombre] = [f(*[onda[s][t - d] if t - d >= 0 else ini[s] for s in fuentes]) for t in range(T)]
    return onda, ini, fin


def circuito(tipo, a0, a1, t_not, consenso=False):
    if tipo == "sop":
        red = [("NA", NOT, ["A"], t_not), ("P1", AND, ["A", "B"], 1), ("P2", AND, ["NA", "C"], 1)]
        fuentes, final, b, c = ["P1", "P2"], OR, 1, 1
        extra = ("P3", AND, ["B", "C"], 1)
    else:
        red = [("NA", NOT, ["A"], t_not), ("S1", OR, ["A", "B"], 1), ("S2", OR, ["NA", "C"], 1)]
        fuentes, final, b, c = ["S1", "S2"], AND, 0, 0
        extra = ("S3", OR, ["B", "C"], 1)
    if consenso:
        red.append(extra)
        fuentes.append(extra[0])
    red.append(("Y", final, fuentes, 1))
    return simular(red, {"A": (a0, a1), "B": (b, b), "C": (c, c)})


def ancho(tipo, a0, a1, t_not, consenso=False):
    onda, ini, fin = circuito(tipo, a0, a1, t_not, consenso)
    y = onda["Y"]
    return sum(1 for v in y if v != ini["Y"]) if ini["Y"] == fin["Y"] else None


print("retardo   SOP baja  SOP sube  SOP+B·C baja   POS sube  POS baja  POS+(B+C) sube")
for t_not in range(5):
    print(f"{t_not:5d}   {ancho('sop', 1, 0, t_not):8d}  {ancho('sop', 0, 1, t_not):8d}  "
          f"{ancho('sop', 1, 0, t_not, True):12d}   {ancho('pos', 0, 1, t_not):8d}  {ancho('pos', 1, 0, t_not):8d}  "
          f"{ancho('pos', 0, 1, t_not, True):14d}")

print()
print("a 10 ns por paso (orientativo): con un NOT de 1 paso, el camino largo tarda 3 pasos = 30 ns "
      "y el glitch dura 1 paso = 10 ns")

El ancho del glitch es igual al retardo del NOT: 0, 1, 2, 3 y 4 pasos. El pozo (SOP) ocurre al bajar A y el pico (POS) al subir A; en el otro sentido no hay glitch. Con el término de consenso, el ancho es 0 en todos los casos.

Paso 3: buscar hazards con mapas de Karnaugh reales

from itertools import combinations

N = 3
TODOS = range(1 << N)


def cubos():
    return [(m, v) for m in range(1 << N) for v in range(1 << N) if v & ~m == 0]


def cubre(cubo):
    m, v = cubo
    return frozenset(x for x in TODOS if x & m == v)


def primos(unos):
    validos = [c for c in cubos() if cubre(c) and cubre(c) <= unos]
    return [c for c in validos if not any(cubre(c) < cubre(d) for d in validos)]


def pares_adyacentes(unos):
    return [(x, x ^ (1 << i)) for x in sorted(unos) for i in range(N)
            if x < x ^ (1 << i) and (x ^ (1 << i)) in unos]


def cubre_todo(cobertura, unos):
    return set().union(*[cubre(c) for c in cobertura]) == set(unos) if cobertura else not unos


def sin_hazard(cobertura, unos):
    return all(any({a, b} <= cubre(c) for c in cobertura) for a, b in pares_adyacentes(unos))


def texto(cubo):
    m, v = cubo
    lits = []
    for i in range(N - 1, -1, -1):
        if (m >> i) & 1:
            lits.append(("" if (v >> i) & 1 else "~") + "ABC"[N - 1 - i])
    return "·".join(lits) or "1"


def tamanos(unos):
    ps = primos(unos)
    k_min = None
    for k in range(len(ps) + 1):
        for cob in combinations(ps, k):
            if cubre_todo(cob, unos):
                if k_min is None:
                    k_min = k
                if sin_hazard(cob, unos):
                    return k_min, k
    return k_min, None


unos = frozenset({1, 3, 6, 7})                      # Y = A·B + ~A·C
print("implicantes primos:", [texto(c) for c in primos(unos)])
print("pares de unos vecinos:", pares_adyacentes(unos))
minima = [c for c in primos(unos) if texto(c) in ("A·B", "~A·C")]
sin_cubrir = [p for p in pares_adyacentes(unos) if not any(set(p) <= cubre(c) for c in minima)]
print("con la cobertura mínima A·B + ~A·C, pares sin un implicante común:", sin_cubrir, "-> B=C=1, A cambia")
print("con todos los primos, libre de hazard:", sin_hazard(primos(unos), unos))
print("tamaños (mínimo, libre de hazard) para esta función:", tamanos(unos))

print()
ok = True
extra = 0
peor = 0
for f in range(256):
    u = frozenset(m for m in TODOS if (f >> m) & 1)
    ok = ok and sin_hazard(primos(u), u) and cubre_todo(primos(u), u)
    k_min, k_hf = tamanos(u)
    if k_hf > k_min:
        extra += 1
        peor = max(peor, k_hf - k_min)
print("para las 256 funciones de 3 variables, el SOP con todos los primos nunca tiene hazard estático en 1:", ok)
print(f"funciones cuya cobertura mínima NO es libre de hazard: {extra} de 256 (hacen falta hasta {peor} término(s) extra)")

Para Y = A·B + ~A·C los primos son ~A·C, A·B y B·C; la cobertura mínima deja sin cubrir el par (3, 7), es decir, B = C = 1 con A cambiando. Con los tres primos no hay hazard. En las 256 funciones de 3 variables, el SOP con todos los primos nunca lo tiene, y el programa cuenta cuántas necesitan términos extra respecto de la cobertura mínima.

Paso 4: direcciones intermedias, binario y Gray

from itertools import permutations


def intermedias(a, orden):
    x, vistas = a, []
    for i in orden:
        x ^= 1 << i
        vistas.append(x)
    return vistas[:-1]                                # sin la dirección final


def recorridos(a, b, n=4):
    cambian = [i for i in range(n) if ((a ^ b) >> i) & 1]
    return cambian, list(permutations(cambian))


a, b = 0b0111, 0b1000
cambian, ordenes = recorridos(a, b)
visitadas = set()
for o in ordenes:
    visitadas.update(intermedias(a, o))
print(f"0111 -> 1000 cambia {len(cambian)} bits; {len(ordenes)} órdenes de llegada posibles")
print(f"direcciones intermedias que pueden verse: {len(visitadas)} de 16 (todas menos la inicial y la final)")
for x in (0b1111, 0b0000, 0b0101):
    p = sum(1 for o in ordenes if x in intermedias(a, o)) / len(ordenes)
    print(f"  un decodificador de {x:04b} da un pulso falso en {p:.0%} de los órdenes")

bits = lambda x, y: bin(x ^ y).count("1")
gray = lambda n: n ^ (n >> 1)
binario = [bits(k, (k + 1) % 16) for k in range(16)]
codigo_gray = [bits(gray(k), gray((k + 1) % 16)) for k in range(16)]
print()
print(f"una vuelta completa del contador (16 pasos): binario cambia {sum(binario)} bits, Gray cambia {sum(codigo_gray)}")
print(f"máximo de bits que cambian en un paso: binario {max(binario)}, Gray {max(codigo_gray)}")
print("direcciones intermedias posibles en el peor paso: binario", 2 ** max(binario) - 2, "- Gray", 2 ** max(codigo_gray) - 2)

De 0111 a 1000 cambian los cuatro bits y pueden verse 14 de las 16 direcciones como intermedias. Un decodificador de cualquiera de ellas puede dar un pulso falso con probabilidad de entre 1/6 y 1/4 según el orden de llegada. En una vuelta completa, el binario cambia 30 bits y el Gray 16 (uno por paso, sin intermedias).

Paso 5: cuándo es seguro muestrear

T, TA = 14, 2
NOT = lambda a: 1 - a
AND = lambda *x: int(all(x))
OR = lambda *x: int(any(x))


def simular(red, entradas):
    ini, fin, onda = {}, {}, {}
    for n, (v0, v1) in entradas.items():
        ini[n], fin[n] = v0, v1
        onda[n] = [v0 if t < TA else v1 for t in range(T)]
    for nombre, f, fuentes, d in red:
        ini[nombre] = f(*[ini[s] for s in fuentes])
        fin[nombre] = f(*[fin[s] for s in fuentes])
        onda[nombre] = [f(*[onda[s][t - d] if t - d >= 0 else ini[s] for s in fuentes]) for t in range(T)]
    return onda, ini, fin


def mux(t_not):
    red = [("NA", NOT, ["A"], t_not), ("P1", AND, ["A", "B"], 1), ("P2", AND, ["NA", "C"], 1), ("Y", OR, ["P1", "P2"], 1)]
    return simular(red, {"A": (1, 0), "B": (1, 1), "C": (1, 1)})


for t_not in (1, 3):
    onda, ini, fin = mux(t_not)
    y = onda["Y"]
    ultimo = max((t for t in range(T) if y[t] != (y[t - 1] if t else ini["Y"])), default=None)
    lecturas = [y[TA + k] for k in range(8)]
    print(f"retardo del NOT = {t_not}: último cambio de Y en t = {ultimo}, es decir, {ultimo - TA} pasos después de A")
    print(f"   lecturas a los k = 0..7 pasos de A: {lecturas}   (correcto = 1)")
    print(f"   caminos: corto 2 pasos, largo {t_not + 2} pasos -> seguro muestrear desde k = {ultimo - TA}")
print()
print("regla de diseño: período de reloj >= retardo del camino más largo + tiempo de preparación del registro")

Con NOT de 1, Y se asienta 3 pasos después de que cambia A y la lectura a los 2 pasos cae en el pozo. Con NOT de 3 se asienta a los 5 pasos y hay tres lecturas malas. Muestrear desde el retardo del camino más largo es siempre seguro: es lo que hace el diseño síncrono.

Paso 6: el glitch como detector de flancos

T, TA = 16, 2
NOT = lambda a: 1 - a
AND = lambda *x: int(all(x))
XOR = lambda a, b: a ^ b
COPIA = lambda a: a


def simular(red, entradas):
    ini, fin, onda = {}, {}, {}
    for n, (v0, v1) in entradas.items():
        ini[n], fin[n] = v0, v1
        onda[n] = [v0 if t < TA else v1 for t in range(T)]
    for nombre, f, fuentes, d in red:
        ini[nombre] = f(*[ini[s] for s in fuentes])
        fin[nombre] = f(*[fin[s] for s in fuentes])
        onda[nombre] = [f(*[onda[s][t - d] if t - d >= 0 else ini[s] for s in fuentes]) for t in range(T)]
    return onda, ini, fin


def pulso_and(a0, a1, t_ret):                         # Y = A · ~A con el NOT lento
    red = [("NA", NOT, ["A"], t_ret), ("Y", AND, ["A", "NA"], 1)]
    return simular(red, {"A": (a0, a1)})


def pulso_xor(a0, a1, t_ret):                         # Y = A xor A retardada
    red = [("AD", COPIA, ["A"], t_ret), ("Y", XOR, ["A", "AD"], 1)]
    return simular(red, {"A": (a0, a1)})


ancho = lambda r: sum(1 for v in r[0]["Y"] if v != r[1]["Y"])
dibujo = lambda onda: "".join("▔" if v else "▁" for v in onda)

print("A·~A con retardo del NOT = 3:")
print("  A sube:", dibujo(pulso_and(0, 1, 3)[0]["Y"]))
print("  A baja:", dibujo(pulso_and(1, 0, 3)[0]["Y"]))
print()
print("retardo   A·~A sube  A·~A baja   XOR sube  XOR baja")
for t_ret in range(0, 5):
    print(f"{t_ret:5d}   {ancho(pulso_and(0, 1, t_ret)):9d}  {ancho(pulso_and(1, 0, t_ret)):9d}   "
          f"{ancho(pulso_xor(0, 1, t_ret)):8d}  {ancho(pulso_xor(1, 0, t_ret)):8d}")

A·~A da un pulso solo en la subida, de ancho igual al retardo del NOT (con retardo 0 no hay pulso). El XOR entre A y una copia retardada da pulso en las dos transiciones, también de ancho igual al retardo: un detector de flancos hecho con un glitch.

09 · Analogía de programador

Una race condition en silicio

El mismo concepto, en dos mundos.
En softwareEn el circuito
Una race condition: dos hilos escriben el resultado en tiempos distintosDos caminos con distinto retardo que se vuelven a juntar
Un torn read: leer una estructura mientras otro hilo la actualizaLeer una dirección de varios bits mientras cambia: una dirección intermedia
Una actualización atómica (un solo campo cambia)Un código Gray: cambia un bit por paso
Una barrera o un commit: nadie lee hasta que todos terminaronUn registro que muestrea en el flanco de reloj
Un test flaky que falla según el timingUn glitch que depende de la temperatura, la tensión y el lote
Redundancia («cinturón y tirantes») para cubrir una ventanaEl término de consenso: un implicante redundante que sostiene la salida
Una ventana de vulnerabilidad de unos microsegundosEl ancho del pozo, igual a la diferencia de retardos
Un debounce o filtro pasa-bajos sobre una señal ruidosaUn RC con Schmitt que alisa el glitch
Una simulación con retardos de delta frente a una sin tiempoSimulación temporal frente a simulación ideal
Un temporizador usado como mutex «por suerte»Un generador de pulsos con el retardo de una puerta
  1. 1
    Un cambio no es instantáneo.

    Cuando algo se ve en dos pasos, por un momento se ve el estado a medio actualizar. La solución general es una sola actualización atómica, o no mirar hasta que termine.

  2. 2
    Redundancia contra sincronización.

    Hay dos formas de curar una carrera: cubrir la ventana (el consenso) o impedir que alguien mire dentro de ella (el reloj). La segunda escala; la primera es un parche local.

  3. 3
    Corrección frente a tiempo.

    La función es correcta y el comportamiento temporal no. Es el mismo caso de un código que pasa todos los tests unitarios y falla bajo concurrencia.

Una diferencia con el software: una race condition es no determinista y se manifiesta rara vez. Un hazard es determinista: con las mismas puertas y las mismas cargas, ocurre siempre en la misma transición, y se puede predecir con el mapa de Karnaugh. Lo que sí varía es su ancho y si llega a importar.

10 · Comprensión

Confusiones frecuentes

«El simulador lógico lo habría mostrado»

Un simulador sin retardos ejecuta todas las puertas en el mismo instante y nunca ve un glitch. Hace falta una simulación temporal, con los retardos de cada puerta, o un osciloscopio.

«Un glitch es un error de lógica»

La tabla de verdad está bien. El que falla es el tiempo, y por eso corregir la ecuación no lo saca: hay que cambiar la estructura (agregar el consenso), el código de entrada (Gray) o la forma de leer (muestreo síncrono).

«Con puertas más rápidas no hay glitches»

El pozo se vuelve más angosto, pero existe: lo que cuenta es la diferencia de retardos, no su valor. Y un contador moderno puede ser sensible a pulsos de pocos nanosegundos.

«Una X en la simulación es un glitch»

Una X es un valor desconocido o una contención (dos salidas peleando). Un glitch es un 0 o un 1 perfectamente definidos en el momento equivocado.

«El término redundante lo arregla todo»

Cubre el hazard lógico, con una sola entrada cambiando. Con varias entradas a la vez (hazard de función) no hay término que ayude: se necesita otro código o diseño síncrono.

«La cobertura mínima es la mejor»

Es la más barata, pero puede dejar sin cubrir un par de unos vecinos. Un circuito libre de hazards necesita todos los implicantes primos, no solo los esenciales.

«Un filtro RC en la salida es la solución general»

Alisa pulsos cortos, pero también alarga los flancos legítimos y puede dejar la señal en una zona indefinida si no hay un Schmitt después. Funciona en señales lentas; no sirve para un reloj rápido.

11 · Práctica guiada

Ejercicios de pozos

Ejercicio 1: dónde está el hazard

En Y = A·B + ~A·C, ¿con qué valores de B y C aparece un hazard estático, qué transición de A lo produce (suponiendo el NOT más lento que las demás puertas) y cuál es el término que lo tapa?

Ver solución razonada

Con B = C = 1 (la salida debería quedarse en 1) y A bajando de 1 a 0: A·B se apaga antes de que ~A·C se encienda. Con A subiendo no pasa nada, porque durante un rato los dos términos valen 1. Se tapa con el consenso B·C: Y = A·B + ~A·C + B·C.

Ejercicio 2: el dual en producto de sumas

Y = (A + B)·(~A + C), con B = C = 0. ¿Qué transición produce el pico, de qué tipo es y cómo se tapa?

Ver solución razonada

La salida debería quedarse en 0. Cuando A sube de 0 a 1, la suma (A + B) pasa a 1 antes de que (~A + C) pase a 0: durante un instante los dos factores valen 1 y Y da un pico (static-0). Se tapa agregando la suma de consenso: Y = (A + B)·(~A + C)·(B + C), que vale 0 todo el tiempo con B = C = 0.

Ejercicio 3: encontrarlo en un mapa

La función f = Σm(1, 3, 6, 7) (con A como bit más significativo) se implementa con ~A·C + A·B. ¿Qué par de unos vecinos queda sin un implicante común y qué se agrega?

Ver solución razonada

Los unos 3 (011) y 7 (111) son vecinos (cambia A) y pertenecen a grupos distintos. Falta el implicante B·C, que cubre los dos. Los otros pares vecinos (1 y 3, 6 y 7) sí comparten un implicante. Verificalo con el Paso 3.

Ejercicio 4: ancho y camino largo

Un NOT tarda 12 ns y cada AND y OR 8 ns. ¿Cuánto dura el pozo del multiplexor y en cuánto tiempo se asienta la salida? ¿Qué período de reloj mínimo hace falta si la salida va a un registro con 5 ns de tiempo de preparación?

Ver solución razonada

Camino corto: AND + OR = 16 ns. Camino largo: NOT + AND + OR = 28 ns. El pozo dura la diferencia, 12 ns (el retardo del NOT). La salida se asienta a los 28 ns de que cambia A. Para el registro, el período debe ser al menos 28 + 5 = 33 ns (y sumar el retardo de lo que haya antes); con esa regla el glitch nunca se muestrea.

Ejercicio 5: el decodificador de un contador

Un decodificador de 4 a 16 está conectado a un contador binario de 4 bits. Al pasar de 0111 a 1000, ¿qué puede pasar en sus salidas y cómo lo evitás?

Ver solución razonada

Cuatro bits cambian a la vez, así que durante un instante puede verse cualquiera de las 14 direcciones intermedias, y el decodificador da un pulso falso en la línea correspondiente. Se evita con: un contador Gray (un solo bit cambia por paso), con un decodificador habilitado solo cuando la dirección ya se asentó (strobe), o decodificando una copia registrada del contador.

Ejercicio 6: ¿hay que actuar?

Un glitch de 20 ns aparece en: (a) un LED, (b) el enable de un latch, (c) la entrada de reloj de un contador, (d) la entrada de datos de un registro cuyo reloj llega 100 ns después. ¿En cuáles actuarías?

Ver solución razonada

En (b) y (c): son entradas sensibles a nivel o flanco que convierten el glitch en un evento falso. (a) es invisible a un LED. En (d) no hace falta: el registro solo mira su entrada en el flanco de reloj, 100 ns después, cuando el glitch ya terminó, siempre que se respete el tiempo de preparación.

12 · Síntesis

Ideas para recordar

  • Un glitch nace de caminos con distinto retardo que se vuelven a juntar. Su ancho es la diferencia de retardos; sin diferencia, no hay glitch.
  • Estático en 1 (pozo) en suma de productos y estático en 0 (pico) en producto de sumas; dinámico, con tres o más cambios. La tabla de verdad no ve ninguno.
  • En el mapa de Karnaugh, hay hazard estático en 1 cuando dos unos vecinos no comparten un implicante. Se tapa con el término de consenso; alcanza con incluir todos los implicantes primos.
  • Con varias entradas cambiando a la vez hay hazards de función, sin arreglo lógico: se evitan con código Gray (un bit por paso), strobes o registros.
  • El diseño síncrono ignora los glitches: el período debe ser mayor que el camino más largo más el tiempo de preparación.
  • Duelen en relojes, enables y sets asíncronos; no en LEDs ni en señales muestreadas después.
  • Un glitch puede usarse como detector de flancos (A·~A o A xor A retardada), con la salvedad de que depende de temperatura y lote.
  • Los glitches gastan energía: son parte de la historia del consumo.

¿Listo para el Tema 29?

Podés avanzar si sos capaz de responder esto sin mirar:

  • Explicar por qué Y = A·B + ~A·C tiene un pozo con B = C = 1 y qué transición lo produce.
  • Calcular el ancho de un glitch a partir de los retardos de los dos caminos.
  • Encontrar un hazard estático en un mapa de Karnaugh y agregar el término que lo tapa.
  • Distinguir un hazard lógico de uno de función y decir cuál tiene solución con lógica.
  • Elegir entre Gray, strobe, registro o redundancia ante un decodificador con glitches.
  • Decidir si un glitch de unos nanosegundos importa según qué entrada alimenta.

Con el tiempo entendido, falta cerrar el costado eléctrico: cuánta energía gasta un circuito y cómo se arma con cuidado para que funcione en la práctica. En el próximo tema, consumo y buenas prácticas.