Los controladores clásicos: 555, PIC16F877a y Basic Stamp

Por: Artiko
elixirroboticaiotelectronica555pic16f877abasic-stampmicrocontroladorespbasic

Los controladores clásicos: 555, PIC16F877a y Basic Stamp

En el capítulo 5 separamos el mundo de los sistemas incrustados en capas: el silicio que ejecuta instrucciones, el lenguaje que las produce y el runtime que las organiza. Vimos que Elixir no baja al metal por sí solo, sino que se apoya en una máquina virtual (BEAM sobre Linux con Nerves, o AtomVM sobre un microcontrolador) y que por debajo siempre hay piezas que responden en microsegundos y no tienen ni scheduler ni recolección de basura.

Este capítulo baja a esa capa. Vamos a estudiar tres controladores que definieron la electrónica práctica durante décadas y que aún hoy aparecen en tableros, laboratorios y aulas: el circuito integrado 555, el microcontrolador PIC16F877a y el módulo Parallax Basic Stamp. No son piezas de museo. Cada uno resuelve un problema concreto que un sistema Elixir moderno resuelve peor, y entender por qué es la mejor forma de aprender dónde termina el software y dónde empieza el hardware.

El recorrido es el siguiente: qué problema resuelve cada controlador y en qué década apareció; cómo funciona por dentro el 555, con sus fórmulas de diseño y tres proyectos completos; cómo se programa un PIC16F877a, desde los registros hasta el ciclo de grabación; qué hace especial al Basic Stamp y por qué PBASIC sigue vivo en la enseñanza; y cómo los tres conviven con un sistema Elixir actual.

Por qué estudiar hardware de hace cincuenta años

Hay una razón técnica y una razón pedagógica. La razón técnica es el determinismo: un temporizador 555 que genera un pulso de 10 milisegundos lo genera siempre, con la misma precisión, sin importar cuánta carga tenga el resto del sistema, mientras que un proceso Elixir que hace Process.sleep(10) promete “al menos 10 milisegundos” y el scheduler decide el resto. Cuando el requisito es “el pulso dura 10 ms y no 10,4 ms”, ese requisito baja al hardware.

La razón pedagógica es la transparencia. El 555 tiene ocho pines y un diagrama interno que cabe en media página. El PIC16F877a tiene 35 instrucciones y una hoja de datos que puede leerse completa. El Basic Stamp expone un intérprete cuyo comportamiento se puede predecir contando tokens. Ninguno de los tres esconde capas. Cuando algo falla, la causa está a la vista, y esa disciplina se transfiere directamente al trabajo con hardware moderno.

flowchart LR
    subgraph Analogico["Dominio analogico y de tiempo duro"]
        IC555["555<br/>temporizador<br/>sin firmware"]
    end
    subgraph Digital["Dominio digital de baja escala"]
        PIC["PIC16F877a<br/>microcontrolador 8 bits<br/>firmware en C o ASM"]
        BS["Basic Stamp<br/>interprete PBASIC<br/>firmware tokenizado"]
    end
    subgraph Moderno["Dominio de aplicacion"]
        NERVES["Elixir + Nerves<br/>Linux + BEAM"]
        ATOM["Elixir + AtomVM<br/>ESP32"]
    end

    IC555 -->|"pulso, reloj, PWM crudo"| PIC
    IC555 -->|"senal a GPIO"| ATOM
    PIC -->|"UART, I2C, SPI"| NERVES
    BS -->|"UART serial"| NERVES
    ATOM -->|"MQTT, HTTP"| NERVES

El diagrama muestra la idea central del capítulo: estos integrados no compiten con Elixir, se ubican debajo de el.

Panorama comparativo

Antes de entrar en detalle, conviene fijar las diferencias de fondo. Los tres se llaman coloquialmente “controladores”, pero pertenecen a categorías distintas.

Característica555PIC16F877aBasic Stamp 2
CategoríaTemporizador analógico-digitalMicrocontrolador de 8 bitsMódulo con microcontrolador + intérprete
Año de aparición1971 (Signetics, SE555/NE555)2001 (Microchip, familia PIC16F87x)1990s, BS2 desde 1995; línea educativa consolidada hacia 1997
Se programaNo, se configura con R y CSí, en C o ensambladorSí, en PBASIC
Memoria de programaNinguna8K palabras de 14 bits (14 KB)2 KB EEPROM de tokens
Memoria de datosNinguna368 bytes RAM + 256 bytes EEPROM32 bytes RAM (26 para variables)
Reloj típicoDefinido por el circuito RCHasta 20 MHz20 MHz internos del intérprete
Velocidad efectivaContinua, analógica5 MIPS (200 ns por instrucción)~4.000 instrucciones PBASIC por segundo
Pines de E/S8 pines totales, 1 salida33 pines de E/S16 pines de E/S
PeriféricosNingunoADC 10 bits, 3 timers, CCP, USART, MSSPLos que provee el intérprete por software
Alimentación4,5 a 16 V (bipolar)2,0 a 5,5 V5 a 12 V en el módulo (regulador incluido)
HerramientaCalculadora y protoboardMPLAB X / MPLAB Xpress + PICkitBasic Stamp Editor + cable serial/USB
Costo relativoMuy bajoBajoMedio-alto
Uso hoyOsciladores, retardos, PWM simpleControl industrial, kits, docenciaDocencia STEM, robótica escolar

Una lectura de esta tabla: el 555 no ejecuta nada, el PIC ejecuta código máquina y el Basic Stamp ejecuta un lenguaje interpretado. Son tres niveles de abstracción distintos sobre el mismo problema, “controlar pines en el tiempo”.

El 555: un temporizador sin firmware

El 555 fue desarrollado por Hans Camenzind para la firma Signetics y salió al mercado en 1971 con las denominaciones SE555 (rango militar) y NE555 (rango comercial). Se le suele llamar “el integrado máquina del tiempo” porque su única función es convertir la carga de un condensador en eventos temporales. Cincuenta años después sigue fabricándose por varias empresas, lo que lo convierte en uno de los integrados con mayor volumen de producción acumulada de la historia.

Su atractivo es simple: es un temporizador flexible, barato y fácil de conseguir, con una salida capaz de manejar directamente una carga pequeña, tanto entregando corriente como absorbiendola.

Qué hay dentro del encapsulado

El 555 no es magia analógica: es un divisor de tensión, dos comparadores, un biestable y un transistor de descarga. Entender ese conjunto explica todos los modos de operación.

flowchart TD
    VCC(["Pin 8 - VCC"]) --> R1["R interna 5k"]
    R1 --> N23(("Nodo 2/3 Vcc"))
    N23 --> R2["R interna 5k"]
    R2 --> N13(("Nodo 1/3 Vcc"))
    N13 --> R3["R interna 5k"]
    R3 --> GND(["Pin 1 - GND"])

    N23 --> CTRL(["Pin 5 - Control Voltage"])
    N23 --> CMP1["Comparador 1<br/>umbral superior"]
    N13 --> CMP2["Comparador 2<br/>umbral inferior"]

    THR(["Pin 6 - Threshold"]) --> CMP1
    TRG(["Pin 2 - Trigger"]) --> CMP2

    CMP1 -->|"Vthr > 2/3 Vcc<br/>=> RESET"| FF["Biestable RS"]
    CMP2 -->|"Vtrg < 1/3 Vcc<br/>=> SET"| FF
    RST(["Pin 4 - Reset"]) -->|"nivel bajo fuerza RESET"| FF

    FF --> OUTDRV["Etapa de salida<br/>push-pull"]
    FF --> QDIS["Transistor de descarga"]
    OUTDRV --> OUT(["Pin 3 - Output"])
    QDIS --> DIS(["Pin 7 - Discharge"])

Los tres puntos que hay que retener. El divisor fija los umbrales: tres resistencias iguales de 5 kΩ (de ahí el nombre del integrado, según una de las versiones de su origen) generan referencias en 1/3 y 2/3 de la tensión de alimentación, y como son fracciones de Vcc, los tiempos del 555 no dependen de la tensión de alimentación —si Vcc sube, el condensador se carga más rápido pero los umbrales suben en la misma proporción—. Los comparadores mandan sobre un biestable: cuando la tensión en THRESHOLD supera 2/3 Vcc el biestable se resetea, la salida baja y el transistor de descarga conecta el pin 7 a masa; cuando la tensión en TRIGGER baja de 1/3 Vcc el biestable se pone a uno, la salida sube y el transistor de descarga se abre. El pin 5 permite trucar el juego: inyectar una tensión externa en CONTROL VOLTAGE mueve el umbral superior y con él el ancho del pulso, lo que abre la puerta a la modulación de ancho de pulso analógica y es el corazón del proyecto Atari Punk Console.

Asignación de pines

PinNombreFunción
1GNDReferencia de masa
2TRIGGEREntrada del comparador inferior; por debajo de 1/3 Vcc activa la salida
3OUTPUTSalida push-pull; en el NE555 bipolar entrega o absorbe hasta unos 200 mA
4RESETActivo en bajo; forzarlo a masa apaga la salida. Se conecta a Vcc si no se usa
5CONTROL VOLTAGEAcceso directo al umbral 2/3 Vcc. Se desacopla con 10 nF si no se usa
6THRESHOLDEntrada del comparador superior; por encima de 2/3 Vcc desactiva la salida
7DISCHARGEColector abierto del transistor interno; descarga el condensador de tiempo
8VCCAlimentación positiva

Modo monoestable: un pulso a pedido

En modo monoestable el 555 está en reposo con la salida en bajo. Un flanco de bajada en TRIGGER dispara un pulso alto de duración fija y luego vuelve al reposo. Es el modo de los retardos: encender una luz durante 30 segundos, mantener un relé activo mientras se procesa un evento, generar un pulso limpio a partir de un pulsador sucio.

stateDiagram-v2
    [*] --> Reposo
    Reposo: Salida en BAJO<br/>Transistor de descarga cerrado<br/>Condensador descargado
    Temporizando: Salida en ALTO<br/>Transistor abierto<br/>Condensador cargandose por R
    Reposo --> Temporizando: Trigger baja de 1/3 Vcc
    Temporizando --> Reposo: Threshold supera 2/3 Vcc<br/>tras t = 1,1 x R x C
    Temporizando --> Reposo: Reset a masa
    Temporizando --> Temporizando: Nuevos pulsos en Trigger<br/>se ignoran

La fórmula del ancho de pulso es:

t = 1,1 x R x C

t en segundos, R en ohmios, C en faradios

El factor 1,1 sale de resolver la carga exponencial del condensador hasta 2/3 de Vcc: t = R·C·ln(3) ≈ 1,0986·R·C.

Un detalle que se olvida con frecuencia: el monoestable no es reactivable. Mientras el pulso está en curso, los disparos adicionales en el pin 2 no lo extienden. Si el requisito es “cada vez que llegue un evento, reiniciar la cuenta”, hay que descargar el condensador manualmente o usar otro circuito.

Modo astable: un oscilador continuo

En modo astable no hay reposo. El condensador oscila entre 1/3 y 2/3 de Vcc, cargándose a través de R1 + R2 y descargándose a través de R2 hacia el pin 7. El resultado es una onda cuadrada continua.

stateDiagram-v2
    [*] --> Cargando
    Cargando: Salida ALTA<br/>C se carga por R1+R2<br/>de 1/3 Vcc a 2/3 Vcc
    Descargando: Salida BAJA<br/>C se descarga por R2 al pin 7<br/>de 2/3 Vcc a 1/3 Vcc
    Cargando --> Descargando: Threshold > 2/3 Vcc<br/>t_alto = 0,693 (R1+R2) C
    Descargando --> Cargando: Trigger < 1/3 Vcc<br/>t_bajo = 0,693 R2 C

Las fórmulas de diseño:

t_alto = 0,693 x (R1 + R2) x C
t_bajo = 0,693 x R2 x C
T      = t_alto + t_bajo = 0,693 x (R1 + 2 R2) x C
f      = 1,44 / ((R1 + 2 R2) x C)
ciclo de trabajo = (R1 + R2) / (R1 + 2 R2)

De la última expresión sale la limitación clásica del astable básico: como R1 y R2 son positivas, el ciclo de trabajo siempre es mayor al 50 %. Para bajar de ahí se agrega un diodo en paralelo con R2 que separa el camino de carga del de descarga, o se usa una configuración con dos diodos que hace el ciclo de trabajo totalmente independiente de la frecuencia.

Calcular componentes con Elixir

Los cálculos anteriores son un buen primer ejercicio para escribir código Elixir que sirva de herramienta de diseño. El siguiente script se ejecuta con elixir ic555.exs y no depende de ninguna librería externa.

# ic555.exs
# Calculadora de componentes para el temporizador 555.
# Ejecutar con: elixir ic555.exs

defmodule IC555 do
  @ln3 :math.log(3)

  @doc """
  Ancho del pulso monoestable en segundos.
  R en ohmios, C en faradios.
  """
  def monoestable(r, c) when r > 0 and c > 0 do
    @ln3 * r * c
  end

  @doc """
  Devuelve un mapa con los tiempos y la frecuencia del modo astable.
  """
  def astable(r1, r2, c) when r1 > 0 and r2 > 0 and c > 0 do
    t_alto = 0.693 * (r1 + r2) * c
    t_bajo = 0.693 * r2 * c
    periodo = t_alto + t_bajo

    %{
      t_alto_s: t_alto,
      t_bajo_s: t_bajo,
      periodo_s: periodo,
      frecuencia_hz: 1.0 / periodo,
      ciclo_trabajo_pct: t_alto / periodo * 100.0
    }
  end

  @doc """
  Dada una frecuencia objetivo, un ciclo de trabajo y un condensador,
  despeja R1 y R2 del modo astable.
  El ciclo de trabajo debe ser mayor a 50 y menor a 100.
  """
  def disenar_astable(frecuencia_hz, ciclo_pct, c)
      when frecuencia_hz > 0 and ciclo_pct > 50 and ciclo_pct < 100 and c > 0 do
    periodo = 1.0 / frecuencia_hz
    t_alto = periodo * ciclo_pct / 100.0
    t_bajo = periodo - t_alto

    r2 = t_bajo / (0.693 * c)
    r1 = t_alto / (0.693 * c) - r2

    %{r1_ohm: r1, r2_ohm: r2, c_farad: c}
  end

  @doc """
  Ajusta un valor de resistencia al valor comercial mas cercano
  de la serie E12 (tolerancia 10 %).
  """
  def valor_e12(r) when r > 0 do
    base = [10, 12, 15, 18, 22, 27, 33, 39, 47, 56, 68, 82]
    decada = :math.pow(10, Float.floor(:math.log10(r)) - 1)

    base
    |> Enum.map(&(&1 * decada))
    |> Enum.flat_map(&[&1, &1 * 10])
    |> Enum.min_by(&abs(&1 - r))
  end

  def formatear_ohm(r) when r >= 1_000_000, do: "#{Float.round(r / 1_000_000, 2)} MOhm"
  def formatear_ohm(r) when r >= 1_000, do: "#{Float.round(r / 1_000, 2)} kOhm"
  def formatear_ohm(r), do: "#{Float.round(r * 1.0, 1)} Ohm"
end

# --- Demostracion ---

IO.puts("== Monoestable: R = 47 kOhm, C = 10 uF ==")
t = IC555.monoestable(47_000, 10.0e-6)
IO.puts("Pulso de #{Float.round(t, 4)} s\n")

IO.puts("== Astable de las luces de ferrocarril ==")
IO.puts("R1 = 47 kOhm, R2 = 47 kOhm, C = 10 uF")

IC555.astable(47_000, 47_000, 10.0e-6)
|> Enum.each(fn {clave, valor} ->
  IO.puts("  #{clave}: #{Float.round(valor, 4)}")
end)

IO.puts("\n== Diseno inverso: 440 Hz al 60 % con C = 22 nF ==")
diseno = IC555.disenar_astable(440.0, 60.0, 22.0e-9)

IO.puts("  R1 exacta: #{IC555.formatear_ohm(diseno.r1_ohm)}")
IO.puts("  R2 exacta: #{IC555.formatear_ohm(diseno.r2_ohm)}")

r1_e12 = IC555.valor_e12(diseno.r1_ohm)
r2_e12 = IC555.valor_e12(diseno.r2_ohm)

IO.puts("  R1 comercial E12: #{IC555.formatear_ohm(r1_e12)}")
IO.puts("  R2 comercial E12: #{IC555.formatear_ohm(r2_e12)}")

IO.puts("\n== Verificacion con valores comerciales ==")
real = IC555.astable(r1_e12, r2_e12, 22.0e-9)
IO.puts("  Frecuencia real: #{Float.round(real.frecuencia_hz, 2)} Hz")
IO.puts("  Ciclo de trabajo: #{Float.round(real.ciclo_trabajo_pct, 2)} %")

La función disenar_astable/3 es la que se usa en la práctica: casi nunca se parte de las resistencias, se parte de la frecuencia deseada. La función valor_e12/1 traduce el resultado teórico a componentes que existen en una tienda, y la verificación final muestra cuánto se desvía el circuito real del cálculo, que es la parte que se omite en la mayoría de los tutoriales.

Proyecto 1: luces de ferrocarril

El primer proyecto clásico es un par de LEDs que se alternan imitando la señal de un cruce ferroviario. Se construye con un astable de baja frecuencia y dos LEDs conectados en sentidos opuestos. Lista de materiales:

ComponenteValorFunción
ICNE555Generador astable
C110 µF electrolíticoCondensador de tiempo
R147 kΩCamino de carga
R247 kΩCamino de carga y descarga
R3, R4470 ΩLimitadoras de corriente de cada LED
LED1, LED2Rojos de 5 mmSeñalización
C2100 nF cerámicoDesacople de alimentación
C310 nF cerámicoDesacople del pin 5

El montaje es el astable estándar: R1 entre Vcc y el pin 7, R2 entre el pin 7 y los pines 6-2 unidos, C1 entre los pines 6-2 y masa. Un LED va del pin 3 a masa con su resistencia y el otro va de Vcc al pin 3 con la suya. Cuando la salida está alta, el primer LED conduce; cuando está baja, el pin 3 absorbe corriente y conduce el segundo.

Con los valores de la tabla, f = 1,44 / ((47000 + 94000) × 10e-6) ≈ 1,02 Hz, es decir una alternancia por segundo. Para acelerar el parpadeo se baja C1 a 4,7 µF; para ralentizarlo se sube a 22 µF.

Proyecto 2: metrónomo

El mismo astable, movido a la banda audible y conectado a un altavoz, se convierte en un metrónomo. La diferencia está en el condensador, mucho menor, y en que R2 pasa a ser un potenciómetro para variar el tempo.

ComponenteValorFunción
ICNE555Astable audible
R11 kΩCamino de carga fijo
P1Potenciómetro de 250 kΩAjuste del tempo
C122 µF electrolíticoCondensador de tiempo
C2100 nFAcoplo al altavoz
SP1Altavoz de 8 ΩSalida audible

Con R1 = 1 kΩ, C1 = 22 µF y el potenciómetro girado a 100 kΩ el periodo es T = 0,693 × (1000 + 200000) × 22e-6 ≈ 3,06 s, unos 20 golpes por minuto. Girado a 10 kΩ el periodo cae a unos 0,32 s, cerca de 190 golpes por minuto. Ese recorrido cubre prácticamente todo el rango musical útil.

Dos consideraciones de montaje: el altavoz no se conecta directo al pin 3, sino a través de un condensador de acoplo que bloquea la componente continua, con una resistencia en serie de 100 Ω para no exigirle al integrado su corriente máxima de forma continua; y el pulso resultante es una onda cuadrada con ciclo de trabajo alto, un “clic” con mucho contenido armónico, que se vuelve más seco al reducir el ciclo de trabajo con el diodo en paralelo a R2.

Proyecto 3: Atari Punk Console

El tercer proyecto es el más interesante desde el punto de vista conceptual: dos 555 encadenados donde el primero, en modo astable, dispara al segundo, en modo monoestable. El resultado es un sintetizador de sonido con dos controles que interactúan de forma no lineal.

flowchart LR
    subgraph Etapa1["555 numero 1 - Astable"]
        A1["Oscila entre 1/3 y 2/3 Vcc"] --> A2["Pin 3: tren de pulsos<br/>frecuencia = P1"]
    end
    subgraph Etapa2["555 numero 2 - Monoestable"]
        B1["Pin 2 recibe cada flanco"] --> B2["Genera pulso de ancho fijo<br/>ancho = P2 x C"]
    end
    A2 -->|"acoplo capacitivo 10 nF"| B1
    B2 --> SAL["Pin 3: senal audible"]
    SAL --> SP(["Altavoz 8 Ohm"])

    P1(["P1 100k<br/>frecuencia"]) -.-> A1
    P2(["P2 100k<br/>ancho de pulso"]) -.-> B2
``` Lista de materiales:

| Componente | Valor | Función |
|---|---|---|
| IC1, IC2 | NE555 (o un solo 556) | Astable + monoestable |
| P1 | Potenciómetro 100 kΩ | Frecuencia del astable |
| P2 | Potenciómetro 100 kΩ | Ancho del pulso del monoestable |
| R1 | 1 kΩ | Resistencia de carga del astable |
| C1 | 10 nF | Condensador de tiempo del astable |
| C2 | 10 nF | Acoplo entre etapas |
| C3 | 100 nF | Condensador de tiempo del monoestable |
| C4 | 10 µF | Acoplo de salida al altavoz |
| SP1 | Altavoz 8 Ω | Salida |

La razón de su sonido característico es que el segundo 555 **ignora los disparos que llegan mientras está temporizando**. Si el ancho del pulso del monoestable supera el periodo del astable, la salida empieza a saltar pulsos de forma irregular, generando subarmónicos y patrones que no se corresponden con ninguna de las dos frecuencias de entrada. Es un ejemplo directo de comportamiento emergente construido con dos bloques triviales.

### Variantes CMOS y límites prácticos

El NE555 original es bipolar. Existe una familia CMOS con la misma asignación de pines y distintas características:

| Versión | Tecnología | Vcc | Consumo en reposo | Corriente de salida | Frecuencia máxima aproximada |
|---|---|---|---|---|---|
| NE555 / LM555 | Bipolar | 4,5 a 16 V | ~3 a 6 mA | ±200 mA | ~500 kHz |
| TLC555 | CMOS | 2 a 15 V | ~170 µA | 100 mA absorbiendo / 10 mA entregando | ~2 MHz |
| LMC555 | CMOS | 1,5 a 15 V | ~100 µA | ~100 mA | ~3 MHz |
| ICM7555 | CMOS | 2 a 18 V | ~60 µA | ~100 mA | ~1 MHz |

El punto crítico del NE555 bipolar es que en cada transición de salida genera un pico de corriente en la línea de alimentación que puede llegar a decenas o cientos de miliamperios durante unos pocos nanosegundos. Sin un condensador de desacople cerca del pin 8 ese pico se propaga por toda la fuente y provoca reinicios aleatorios en cualquier microcontrolador que comparta la alimentación. Es la causa número uno de "mi ESP32 se reinicia cuando conecto el 555".

## El PIC16F877a: el microcontrolador de las aulas

Si el 555 configura tiempo con resistencias, el PIC16F877a lo hace con código. Es un microcontrolador de 8 bits de Microchip, de la familia PIC16, que se convirtió en estándar de facto en universidades y laboratorios de electrónica durante casi dos décadas. Su encapsulado DIP de 40 pines, su tolerancia a errores de cableado y la abundancia de material didáctico explican esa longevidad.

### Microcontrolador contra microprocesador

Es la distinción que hay que fijar antes de cualquier otra cosa.

| Aspecto | Microprocesador | Microcontrolador |
|---|---|---|
| Que integra | Solo la CPU | CPU + memoria + periféricos + E/S |
| Memoria | Externa, en chips aparte | Interna, en el mismo silicio |
| Circuito mínimo | CPU + RAM + ROM + chipset + buses | El chip, un cristal y desacoples |
| Consumo | Alto | Bajo, con modos de reposo |
| Objetivo de diseño | Máximo rendimiento general | Control determinista de E/S |
| Ejemplo | Intel Core, ARM Cortex-A | PIC16F877a, ATmega328P, ESP32 |

Un microprocesador es un cerebro que necesita un cuerpo. Un microcontrolador ya viene con cuerpo: memoria de programa, memoria de datos, conversores analógicos, temporizadores y puertos, todo en el mismo encapsulado.

### Especificaciones del PIC16F877a

| Parámetro | Valor |
|---|---|
| Arquitectura | Harvard, RISC, 8 bits |
| Repertorio de instrucciones | 35 instrucciones de una sola palabra |
| Frecuencia máxima | 20 MHz |
| Tiempo por instrucción | 200 ns (una instrucción cada 4 ciclos de reloj) |
| Memoria de programa | 8K palabras de 14 bits (equivale a 14 KB) |
| Memoria RAM | 368 bytes |
| Memoria EEPROM de datos | 256 bytes |
| Pines del encapsulado | 40 (PDIP) o 44 (PLCC / TQFP) |
| Pines de E/S | 33, repartidos en cinco puertos |
| Tensión de operación | 2,0 a 5,5 V |
| Conversor analógico-digital | 8 canales, 10 bits |
| Temporizadores | Timer0 (8 bits), Timer1 (16 bits), Timer2 (8 bits) |
| Módulos CCP | 2, con captura de 12,5 ns, comparación de 200 ns y PWM de hasta 10 bits |
| Comunicaciones | USART, MSSP con SPI e I2C (maestro y esclavo) |
| Protección | Brown-out Reset (BOR), Watchdog Timer (WDT), Power-up Timer |
| Ciclos de escritura Flash | ~100.000 |

Los cinco puertos se reparten así: PORTA con 6 pines, PORTB con 8, PORTC con 8, PORTD con 8 y PORTE con 3. Suman los 33 pines de E/S.

### La arquitectura Harvard y el ciclo de instrucción

El PIC usa arquitectura Harvard: el bus de programa y el bus de datos son físicamente separados. El bus de programa tiene 14 bits de ancho (una instrucción completa por acceso) y el de datos tiene 8 bits. Eso permite buscar la siguiente instrucción mientras se ejecuta la actual.

```mermaid
sequenceDiagram
    participant PC as Contador de programa
    participant FLASH as Memoria Flash 14 bits
    participant DEC as Decodificador
    participant ALU as ALU 8 bits
    participant RAM as RAM 368 bytes

    Note over PC,RAM: Ciclo N
    PC->>FLASH: Solicita instruccion N
    FLASH-->>DEC: Palabra de 14 bits
    DEC->>ALU: Ejecuta instruccion N-1
    ALU->>RAM: Lee o escribe registro

    Note over PC,RAM: Ciclo N+1 (solapado)
    PC->>FLASH: Solicita instruccion N+1
    FLASH-->>DEC: Palabra de 14 bits
    DEC->>ALU: Ejecuta instruccion N
    ALU->>RAM: Lee o escribe registro

    Note over PC,ALU: Un salto rompe el solapamiento<br/>y consume dos ciclos de instruccion

De aquí sale el número de 200 ns: a 20 MHz cada ciclo de reloj dura 50 ns, y el PIC consume cuatro ciclos de reloj por instrucción, lo que da 5 millones de instrucciones por segundo. Las instrucciones de salto ocupan el doble porque hay que descartar la instrucción ya buscada.

El modelo de registros: TRIS y PORT

Toda la E/S del PIC gira alrededor de dos familias de registros. TRISx define la dirección de cada pin del puerto x: un bit en 1 lo configura como entrada (la letra I de input) y un bit en 0 como salida (la letra O de output), con la regla mnemotécnica habitual de “1 parece una I, 0 parece una O”. PORTx lee o escribe el estado lógico de los pines.

Hay una trampa específica del PIC16F877a: los pines de PORTA y PORTE arrancan configurados como entradas analógicas. Si se quiere usarlos como digitales hay que escribir el registro ADCON1. El valor 0x06 deja todos los canales del ADC como E/S digitales.

Un programa completo en C con XC8

El siguiente programa lee un pulsador en RB0 y enciende un LED en RD0 mientras el pulsador esté presionado, con antirrebote por software. Compila con el compilador XC8 de Microchip, disponible en MPLAB X y en la versión en línea MPLAB Xpress IDE.

/*
 * led_pulsador.c
 * PIC16F877a con cristal de 20 MHz.
 * Pulsador en RB0 con resistencia pull-up interna.
 * LED en RD0 con resistencia de 330 ohmios a masa.
 * Compilar con XC8.
 */

#include <xc.h>

// Bits de configuracion
#pragma config FOSC  = HS     // Oscilador de alta velocidad (cristal 20 MHz)
#pragma config WDTE  = OFF    // Watchdog desactivado
#pragma config PWRTE = ON     // Temporizador de arranque activo
#pragma config BOREN = ON     // Brown-out Reset activo
#pragma config LVP   = OFF    // Programacion en baja tension desactivada
#pragma config CPD   = OFF    // Proteccion de EEPROM desactivada
#pragma config WRT   = OFF    // Escritura de Flash desactivada
#pragma config CP    = OFF    // Proteccion de codigo desactivada

#define _XTAL_FREQ 20000000UL

#define LED     PORTDbits.RD0
#define BOTON   PORTBbits.RB0

void inicializar(void)
{
    ADCON1 = 0x06;      // Todos los pines de PORTA y PORTE como digitales
    TRISD  = 0x00;      // PORTD completo como salida
    TRISB  = 0x01;      // RB0 como entrada, el resto salida
    PORTD  = 0x00;      // Apagar todas las salidas de PORTD
    OPTION_REGbits.nRBPU = 0;   // Habilitar pull-ups internas de PORTB
}

// Devuelve 1 si el boton sigue presionado tras el antirrebote
unsigned char boton_presionado(void)
{
    if (BOTON == 0) {           // Con pull-up, presionado es nivel bajo
        __delay_ms(20);         // Espera de antirrebote
        if (BOTON == 0) {
            return 1;
        }
    }
    return 0;
}

void main(void)
{
    inicializar();

    while (1) {
        if (boton_presionado()) {
            LED = 1;
            while (BOTON == 0) {
                ;               // Mantener encendido mientras se presione
            }
            __delay_ms(20);     // Antirrebote de la liberacion
            LED = 0;
        }
    }
}

Tres cosas que este programa enseña y que no son obvias: los bits de configuración viajan en el archivo .hex, no son opciones de compilación, y un FOSC mal elegido deja el chip mudo aunque el código sea perfecto porque el microcontrolador arranca esperando otro tipo de oscilador; LVP = OFF es obligatorio si se usa RB3 como E/S normal, porque con LVP activo ese pin queda reservado para programación en baja tensión y el chip puede entrar en modo programación de forma inesperada; y __delay_ms es una macro que se expande en bucles, no una llamada a un temporizador, así que necesita que _XTAL_FREQ coincida con el cristal real o todos los tiempos salen escalados.

Generar PWM con el módulo CCP

El ejemplo anterior no aprovecha ningún periférico. El siguiente genera una señal PWM de 1250 Hz en el pin RC2 (CCP1) y hace un barrido de ciclo de trabajo, que es la base del control de velocidad de un motor de corriente continua o del brillo de un LED.

/*
 * pwm_ccp1.c
 * PWM por hardware en RC2 (CCP1) del PIC16F877a.
 * Cristal de 20 MHz, PR2 = 249, prescaler de Timer2 = 16.
 * Frecuencia resultante: 20e6 / (4 * 16 * 250) = 1250 Hz.
 */

#include <xc.h>

#pragma config FOSC  = HS
#pragma config WDTE  = OFF
#pragma config PWRTE = ON
#pragma config BOREN = ON
#pragma config LVP   = OFF
#pragma config CPD   = OFF
#pragma config WRT   = OFF
#pragma config CP    = OFF

#define _XTAL_FREQ 20000000UL

void pwm_inicializar(void)
{
    TRISCbits.TRISC2 = 1;   // CCP1 como entrada mientras se configura

    PR2    = 249;           // Define el periodo
    T2CON  = 0b00000111;    // Timer2 encendido, prescaler 1:16
    CCP1CON = 0b00001100;   // Modo PWM

    TRISCbits.TRISC2 = 0;   // Ahora si, CCP1 como salida
}

/*
 * El ciclo de trabajo del PIC16F877a tiene 10 bits:
 * los 8 mas significativos van a CCPR1L y los 2 menos
 * significativos a los bits DC1B1 y DC1B0 de CCP1CON.
 * Con PR2 = 249 el valor maximo util es (249 + 1) * 4 = 1000.
 */
void pwm_ciclo(unsigned int valor_10_bits)
{
    if (valor_10_bits > 1000) {
        valor_10_bits = 1000;
    }

    CCPR1L = (unsigned char)(valor_10_bits >> 2);
    CCP1CONbits.CCP1Y = (valor_10_bits & 0x02) ? 1 : 0;
    CCP1CONbits.CCP1X = (valor_10_bits & 0x01) ? 1 : 0;
}

void main(void)
{
    unsigned int nivel;

    ADCON1 = 0x06;
    pwm_inicializar();

    while (1) {
        for (nivel = 0; nivel <= 1000; nivel += 10) {
            pwm_ciclo(nivel);
            __delay_ms(10);
        }
        for (nivel = 1000; nivel > 0; nivel -= 10) {
            pwm_ciclo(nivel);
            __delay_ms(10);
        }
    }
}

La fórmula del periodo PWM en esta familia es:

T_pwm = (PR2 + 1) x 4 x Tosc x prescaler_timer2

Con Tosc = 1/20 MHz = 50 ns, PR2 = 249 y prescaler 16:
T_pwm = 250 x 4 x 50 ns x 16 = 800 us  =>  f = 1250 Hz

Notar la diferencia con el 555: aquí el PWM es de 10 bits, se cambia por software en tiempo de ejecución y no consume ciclos de CPU una vez configurado. El módulo CCP trabaja solo con Timer2 en paralelo al programa principal.

El flujo de trabajo completo

flowchart TD
    A["Codigo fuente<br/>main.c o main.asm"] --> B["MPLAB X IDE<br/>o MPLAB Xpress en el navegador"]
    B --> C["Compilador XC8<br/>o ensamblador MPASM"]
    C --> D{"Errores?"}
    D -->|"si"| A
    D -->|"no"| E["Archivo main.hex<br/>bits de configuracion incluidos"]
    E --> F["Programador PICkit 3 / PICkit 4<br/>o placa Xpress con bootloader"]
    F --> G["Grabacion por ICSP<br/>pines MCLR, PGD, PGC, VDD, VSS"]
    G --> H["PIC16F877a con el firmware"]
    H --> I{"Se comporta<br/>como se espera?"}
    I -->|"no"| J["Depurar: bits de config,<br/>cristal, TRIS, ADCON1"]
    J --> A
    I -->|"si"| K["Circuito terminado"]

    L["Proteus / SimulIDE"] -.->|"simular el .hex<br/>antes de grabar"| E

MPLAB Xpress IDE merece una mención: es la versión en navegador del entorno de Microchip. No requiere instalación, incluye el compilador XC8 y guarda los proyectos en la nube, lo que resuelve el problema clásico de laboratorios con máquinas compartidas.

PIC16F877a frente a PIC18F4550

Es la comparación natural cuando un proyecto crece.

CaracterísticaPIC16F877aPIC18F4550
FamiliaPIC16, gama mediaPIC18, gama media-alta
Ancho de instrucción14 bits16 bits
Repertorio35 instrucciones75+ instrucciones, con multiplicación por hardware
Memoria de programa8K palabras (14 KB)16K palabras (32 KB)
RAM368 bytes2048 bytes
EEPROM256 bytes256 bytes
Frecuencia20 MHz48 MHz (con PLL interno)
ADC8 canales de 10 bits13 canales de 10 bits
USBNoSí, USB 2.0 Full Speed integrado
Pines40 / 4440 / 44
Optimización para CLimitada, pila de hardware de 8 nivelesDiseñado para compiladores C, pila más profunda

El criterio práctico: si el proyecto necesita conectarse a un PC por USB sin conversor externo, o si el firmware en C empieza a chocar con los 368 bytes de RAM, corresponde subir al PIC18. Si el proyecto es control de E/S con lógica acotada, el PIC16F877a lo cubre con margen.

Dónde se sigue usando

Aparece en control industrial de bajo nivel (dosificadores, controles de temperatura, secuenciadores de relé) en equipos diseñados hace quince años que siguen en producción; en kits educativos, porque la mayoría de las asignaturas de microcontroladores en universidades hispanohablantes se armaron alrededor de este chip; y como periférico dedicado, con un PIC actuando de esclavo I2C que lee encoders o genera pulsos para liberar de esa tarea al procesador principal. Su encapsulado DIP de 40 pines a 5 V también lo hace común en entornos con ruido eléctrico.

El Parallax Basic Stamp: el microcontrolador que se programa en BASIC

El tercer controlador de este capítulo pertenece a otra categoría: no es un chip, es un módulo. Parallax tomó un microcontrolador, le agrego un regulador de tensión, una EEPROM externa, un resonador y una interfaz serial, y lo montó todo en un encapsulado del tamaño de una estampilla de correo. De ahí el nombre, stamp.

La línea se popularizó a partir de la segunda mitad de los noventa. El BS2 se introdujo en 1995 y hacia 1997 Parallax lo acompañó con la Board of Education, una placa de desarrollo con protoboard integrada, conectores para servos y alimentación regulada. Poco después llegó el Boe-Bot, un robot educativo de dos ruedas con servos de rotación continua que se volvió el vehículo de entrada a la robótica para miles de estudiantes.

Qué hay dentro de un Basic Stamp 2

flowchart TD
    subgraph Modulo["Modulo BS2 - encapsulado de 24 pines"]
        UC["Microcontrolador PIC16C57<br/>reloj de 20 MHz<br/>contiene el interprete PBASIC en ROM"]
        EE["EEPROM serie 24LC16B<br/>2 KB de tokens PBASIC<br/>~1 millon de reescrituras"]
        RES["Resonador ceramico 20 MHz"]
        REG["Regulador 5 V<br/>entrada de 5,5 a 15 V"]
        UC <-->|"bus I2C interno"| EE
        RES --> UC
        REG --> UC
    end

    PC(["PC con Basic Stamp Editor"]) -->|"cable serie o USB<br/>tokeniza y descarga"| EE
    UC --> IO["16 pines de E/S<br/>P0 a P15"]
    IO --> MUNDO(["LEDs, servos, sensores, LCD"])

    UC -->|"pin 16 SOUT / SIN"| PC

El punto que hay que entender es el modelo de dos etapas: en el PC, el Basic Stamp Editor tokeniza el programa PBASIC, es decir que no lo compila a código máquina del PIC sino que lo convierte en una representación compacta de tokens; esos tokens se descargan a la EEPROM externa por el puerto serie; y al arrancar, el intérprete grabado de fábrica en la ROM del microcontrolador lee los tokens de la EEPROM uno por uno y los ejecuta.

Ese modelo explica las características del módulo. El programa nunca se pierde, porque vive en EEPROM no volátil. No hay ciclo de compilación largo: tokenizar es casi instantáneo y la descarga tarda un par de segundos. A cambio, la velocidad es baja, ya que cada instrucción PBASIC implica leer tokens por I2C, decodificarlos y ejecutar rutinas del intérprete: el BS2 alcanza aproximadamente 4.000 instrucciones PBASIC por segundo, tres órdenes de magnitud por debajo de los 5 MIPS del PIC16F877a programado en C. Y la memoria de variables es diminuta: 32 bytes de RAM, de los cuales 6 están reservados para el control de los pines de E/S, dejando 26 bytes para el usuario.

La familia de módulos

MóduloVelocidad aproximadaRAM de variablesEEPROM de programaPines de E/S
BS1~2.000 instrucciones/s14 bytes256 bytes8
BS2~4.000 instrucciones/s26 bytes2 KB16
BS2sx~10.000 instrucciones/s26 bytes8 x 2 KB (bancos)16
BS2p~12.000 instrucciones/s38 bytes8 x 2 KB16
BS2px~19.000 instrucciones/s38 bytes8 x 2 KB16

Los módulos con bancos de memoria dividen el programa en slots de 2 KB entre los que se salta con la instrucción RUN, lo que da más espacio a costa de que las variables no se comparten automáticamente entre slots.

PBASIC: el lenguaje

PBASIC es un dialecto de BASIC con instrucciones pensadas para hardware. La lista corta de lo que hay que conocer:

InstrucciónQue hace
HIGH pin / LOW pinConfigura el pin como salida y lo pone en alto o bajo
TOGGLE pinInvierte el estado del pin
INPUT pin / OUTPUT pinFija la dirección del pin sin cambiar su estado
PAUSE msDetiene la ejecución la cantidad de milisegundos indicada
DEBUG "texto"Envia texto al terminal del PC por el puerto de programación
DEBUGIN varLee un valor tecleado en el terminal del PC
PULSOUT pin, duracionGenera un pulso en unidades de 2 µs en el BS2
PULSIN pin, estado, varMide el ancho de un pulso entrante
FREQOUT pin, ms, frecuenciaGenera un tono en el pin
SERIN pin, baudmode, [ ... ]Recibe datos serie por software
SEROUT pin, baudmode, [ ... ]Transmite datos serie por software
RCTIME pin, estado, varMide el tiempo de descarga de un circuito RC, para sensores resistivos
COUNT pin, ms, varCuenta transiciones en un pin durante un intervalo
GOSUB / RETURNLlamada a subrutina
DO ... LOOP, FOR ... NEXTEstructuras de repetición
EEPROM / READ / WRITEAcceso a la EEPROM no usada por el programa

Las variables se declaran con tipos explícitos de tamaño: Bit, Nib (4 bits), Byte y Word (16 bits). No hay tipos de punto flotante ni estructuras.

Un programa PBASIC completo

El siguiente programa hace parpadear un LED en P0, lee un pulsador en P1, cuenta cuántas veces se presionó y emite un tono distinto cada cinco pulsaciones. Está escrito para BS2 y funciona sin cambios en la Board of Education.

' {$STAMP BS2}
' {$PBASIC 2.5}

' -----------------------------------------------------------
' contador_pulsaciones.bs2
' LED en P0 con resistencia de 470 ohmios a masa.
' Pulsador en P1 con resistencia de 10 kOhm a masa
' y el otro extremo del pulsador a 5 V.
' Zumbador piezoelectrico en P2.
' -----------------------------------------------------------

LedPin      PIN     0
BotonPin    PIN     1
BuzzerPin   PIN     2

pulsaciones VAR     Byte
estado      VAR     Bit
anterior    VAR     Bit

pulsaciones = 0
anterior = 0

DEBUG "Contador de pulsaciones iniciado", CR

Principal:
  DO
    estado = IN1

    ' Deteccion de flanco de subida con antirrebote
    IF (estado = 1) AND (anterior = 0) THEN
      PAUSE 20
      IF IN1 = 1 THEN
        pulsaciones = pulsaciones + 1
        GOSUB Parpadear
        DEBUG "Pulsaciones: ", DEC pulsaciones, CR

        IF (pulsaciones // 5) = 0 THEN
          GOSUB Tono
        ENDIF
      ENDIF
    ENDIF

    anterior = estado
    PAUSE 10
  LOOP

Parpadear:
  HIGH LedPin
  PAUSE 100
  LOW LedPin
  RETURN

Tono:
  FREQOUT BuzzerPin, 200, 1200
  PAUSE 100
  FREQOUT BuzzerPin, 200, 1800
  RETURN

Detalles a observar: las directivas {$STAMP BS2} y {$PBASIC 2.5} no son comentarios decorativos, le indican al editor a que módulo dirigir la descarga y que versión del lenguaje aceptar; IN1 lee el pin 1 como entrada sin cambiar su dirección, y la familia INx para lectura y OUTx para escritura convive con HIGH/LOW; el operador // es el módulo en PBASIC, no un comentario; y no hay funciones con parámetros ni valores de retorno, porque los 26 bytes de RAM no permiten una pila de llamadas, así que todo se comunica por variables globales.

Control de un Boe-Bot

El robot Boe-Bot usa dos servos de rotación continua. En un servo estándar el ancho del pulso fija la posición; en uno de rotación continua fija la velocidad y el sentido. El punto neutro está en 1,5 ms.

' {$STAMP BS2}
' {$PBASIC 2.5}

' -----------------------------------------------------------
' boebot_cuadrado.bs2
' Servo izquierdo en P13, servo derecho en P12.
' PULSOUT usa unidades de 2 microsegundos en el BS2:
'   750 -> 1500 us -> detenido
'   850 -> 1700 us -> giro maximo en un sentido
'   650 -> 1300 us -> giro maximo en el sentido opuesto
' Los servos estan montados enfrentados, por eso avanzar
' requiere valores opuestos en cada uno.
' -----------------------------------------------------------

ServoIzq    PIN     13
ServoDer    PIN     12

pulsos      VAR     Word
lado        VAR     Nib

DEBUG "Boe-Bot: recorrido cuadrado", CR
PAUSE 1000

FOR lado = 1 TO 4
  DEBUG "Lado ", DEC lado, CR
  GOSUB Avanzar
  GOSUB GirarDerecha
NEXT

GOSUB Detener
DEBUG "Recorrido terminado", CR
END

Avanzar:
  ' 100 pulsos x 20 ms = aproximadamente 2 segundos
  FOR pulsos = 1 TO 100
    PULSOUT ServoIzq, 850
    PULSOUT ServoDer, 650
    PAUSE 20
  NEXT
  RETURN

GirarDerecha:
  ' Ambos servos en el mismo sentido de pulso: el robot rota
  FOR pulsos = 1 TO 24
    PULSOUT ServoIzq, 850
    PULSOUT ServoDer, 850
    PAUSE 20
  NEXT
  RETURN

Detener:
  FOR pulsos = 1 TO 10
    PULSOUT ServoIzq, 750
    PULSOUT ServoDer, 750
    PAUSE 20
  NEXT
  RETURN

Este programa expone una característica del Basic Stamp que conviene entender: el refresco de los servos se hace por software. El PAUSE 20 dentro del bucle reproduce el periodo de 20 ms que los servos esperan. Mientras el programa está en ese bucle no puede hacer nada más. Un PIC16F877a resolvería lo mismo con dos módulos CCP funcionando en paralelo, dejando la CPU libre. Es la diferencia entre generar tiempo con el procesador y generarlo con periféricos.

Por qué sigue vivo

PBASIC continúa siendo uno de los lenguajes preferidos por educadores y aulas de secundaria, universidades introductorias y clubes de robótica. Las razones son concretas: cero cadena de herramientas, porque se instala un editor, se conecta un cable y se escribe, sin compiladores ni gestores de paquetes ni configuración de placas; retroalimentación inmediata, porque DEBUG imprime en el terminal del PC sin configurar nada; material didáctico estructurado, con los textos What’s a Microcontroller? y Robotics with the Boe-Bot de Andy Lindsay, que cubren desde encender un LED hasta navegación con sensores infrarrojos alineados a objetivos STEM y mecatrónica; y robustez física, ya que el módulo tolera errores de cableado que destruirían una placa más moderna.

En Chile los módulos y kits se consiguen a través de distribuidores especializados como Rambal.

Cómo conviven con Elixir hoy

El error habitual al mirar estos tres integrados desde un stack moderno es pensarlos como alternativas. No lo son: son capas.

flowchart TB
    subgraph Capa3["Capa de aplicacion - Elixir / BEAM"]
        SUP["Supervisor OTP"]
        GEN["GenServer de protocolo"]
        PHX["Phoenix LiveView<br/>panel de operacion"]
        SUP --> GEN
        SUP --> PHX
    end

    subgraph Capa2["Capa de control - microcontroladores"]
        PICN["PIC16F877a<br/>lazo de control, ADC, PWM"]
        BSN["Basic Stamp<br/>maqueta educativa"]
    end

    subgraph Capa1["Capa de tiempo duro - analogico"]
        T555["555<br/>reloj, retardo, watchdog externo"]
    end

    GEN <-->|"UART 9600 8N1<br/>tramas ASCII"| PICN
    GEN <-->|"UART 9600 8N1<br/>SERIN / SEROUT"| BSN
    T555 -->|"pulso de reloj"| PICN
    T555 -->|"senal a GPIO"| Capa3
    PHX -->|"WebSocket"| NAV(["Navegador del operador"])

Elixir hablando con un PIC o un Basic Stamp por serial

La biblioteca circuits_uart permite a un sistema Elixir, ya sea en un Raspberry Pi con Nerves o en un PC de escritorio, conversar con cualquiera de los dos microcontroladores. El siguiente módulo es un GenServer completo que envía comandos y espera respuestas terminadas en salto de línea.

# lib/puente_serial.ex
#
# Dependencia en mix.exs:
#   {:circuits_uart, "~> 1.5"}
#
# Del lado del microcontrolador se espera un protocolo de texto:
#   El host envia  "PING\n"    y recibe "PONG\n"
#   El host envia  "LED:1\n"   y recibe "OK\n"
#   El host envia  "READ:0\n"  y recibe "ADC0=512\n"

defmodule PuenteSerial do
  use GenServer
  require Logger

  @intervalo_lectura 1_000

  # ---------- API publica ----------

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @doc "Envia un comando y espera la respuesta del microcontrolador."
  def comando(texto, timeout \\ 2_000) do
    GenServer.call(__MODULE__, {:comando, texto}, timeout + 500)
  end

  def encender_led, do: comando("LED:1")
  def apagar_led, do: comando("LED:0")
  def leer_adc(canal) when canal in 0..7, do: comando("READ:#{canal}")

  # ---------- Callbacks ----------

  @impl true
  def init(opts) do
    puerto = Keyword.get(opts, :puerto, "ttyUSB0")
    velocidad = Keyword.get(opts, :velocidad, 9600)

    {:ok, uart} = Circuits.UART.start_link()

    case Circuits.UART.open(uart, puerto,
           speed: velocidad,
           data_bits: 8,
           stop_bits: 1,
           parity: :none,
           active: false,
           framing: {Circuits.UART.Framing.Line, separator: "\n"}
         ) do
      :ok ->
        Logger.info("Puerto #{puerto} abierto a #{velocidad} baudios")
        Process.send_after(self(), :sondear, @intervalo_lectura)
        {:ok, %{uart: uart, puerto: puerto, ultimo_adc: nil, fallos: 0}}

      {:error, razon} ->
        {:stop, {:no_se_pudo_abrir, puerto, razon}}
    end
  end

  @impl true
  def handle_call({:comando, texto}, _from, estado) do
    with :ok <- Circuits.UART.write(estado.uart, texto),
         {:ok, respuesta} <- Circuits.UART.read(estado.uart, 2_000) do
      {:reply, {:ok, String.trim(respuesta)}, %{estado | fallos: 0}}
    else
      {:error, razon} ->
        {:reply, {:error, razon}, %{estado | fallos: estado.fallos + 1}}
    end
  end

  @impl true
  def handle_info(:sondear, estado) do
    nuevo_estado =
      case sondear_adc(estado) do
        {:ok, valor} ->
          Logger.debug("ADC0 = #{valor}")
          %{estado | ultimo_adc: valor, fallos: 0}

        {:error, razon} ->
          Logger.warning("Fallo de sondeo: #{inspect(razon)}")
          %{estado | fallos: estado.fallos + 1}
      end

    Process.send_after(self(), :sondear, @intervalo_lectura)
    {:noreply, nuevo_estado}
  end

  @impl true
  def terminate(_razon, estado) do
    Circuits.UART.close(estado.uart)
    :ok
  end

  # ---------- Privadas ----------

  defp sondear_adc(estado) do
    with :ok <- Circuits.UART.write(estado.uart, "READ:0\n"),
         {:ok, linea} <- Circuits.UART.read(estado.uart, 2_000),
         [_, valor] <- String.split(String.trim(linea), "="),
         {entero, ""} <- Integer.parse(valor) do
      {:ok, entero}
    else
      {:error, razon} -> {:error, razon}
      otro -> {:error, {:respuesta_inesperada, otro}}
    end
  end
end

La contraparte en PBASIC, corriendo en un Basic Stamp 2, sería esta:

' {$STAMP BS2}
' {$PBASIC 2.5}

' -----------------------------------------------------------
' puente_serial.bs2
' Recibe comandos de texto por P15 (RX) y responde por P14 (TX).
' Baudmode 84 = 9600 baudios, 8N1, sin inversion (nivel TTL).
' Para conectarse a un PC con niveles RS-232 se necesita
' un conversor de niveles o el baudmode invertido 16468.
' -----------------------------------------------------------

RxPin       PIN     15
TxPin       PIN     14
LedPin      PIN     0
SensorPin   PIN     7

Baudio      CON     84

comando     VAR     Byte(8)
valor       VAR     Word

LOW LedPin

Principal:
  DO
    SERIN RxPin, Baudio, [STR comando\8\CR]

    IF comando(0) = "P" THEN
      SEROUT TxPin, Baudio, ["PONG", CR]

    ELSEIF comando(0) = "L" THEN
      IF comando(4) = "1" THEN
        HIGH LedPin
      ELSE
        LOW LedPin
      ENDIF
      SEROUT TxPin, Baudio, ["OK", CR]

    ELSEIF comando(0) = "R" THEN
      GOSUB LeerSensor
      SEROUT TxPin, Baudio, ["ADC0=", DEC valor, CR]

    ELSE
      SEROUT TxPin, Baudio, ["ERR", CR]
    ENDIF
  LOOP

LeerSensor:
  ' El BS2 no tiene ADC. Se mide una resistencia variable
  ' con RCTIME: se carga el condensador y se mide su descarga.
  HIGH SensorPin
  PAUSE 1
  RCTIME SensorPin, 1, valor
  RETURN

Ese último detalle es importante: el BS2 no tiene conversor analógico-digital. La instrucción RCTIME mide el tiempo de descarga de un circuito RC y con eso infiere el valor de una resistencia variable, como una fotorresistencia o un termistor. Es una solución de software para un problema que el PIC16F877a resuelve con hardware dedicado.

Un 555 como periférico de un sistema Elixir

El caso más útil del 555 en un proyecto moderno es como generador de señal independiente del software: un astable de 1 Hz que alimenta un contador externo y funciona como reloj de referencia que no se detiene aunque la aplicación se reinicie; un monoestable que estira un pulso de 5 µs, demasiado corto para que un GPIO muestreado por software lo capture, hasta 10 ms; o un temporizador de vigilancia externo que corta la alimentación de la carga si el sistema principal deja de refrescar el pin TRIGGER.

El siguiente módulo Elixir con circuits_gpio cuenta los pulsos de un 555 astable usando interrupciones, sin bloquear ningún proceso.

# lib/contador_555.ex
#
# Dependencia en mix.exs:
#   {:circuits_gpio, "~> 2.0"}
#
# Conexion: salida del 555 (pin 3) a la GPIO indicada,
# con divisor resistivo si el 555 trabaja a 5 V y la placa a 3,3 V.

defmodule Contador555 do
  use GenServer
  require Logger

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @doc "Devuelve la cantidad de pulsos y la frecuencia medida."
  def estado, do: GenServer.call(__MODULE__, :estado)

  @impl true
  def init(opts) do
    linea = Keyword.get(opts, :linea, "GPIO18")

    {:ok, gpio} = Circuits.GPIO.open(linea, :input, pull_mode: :pulldown)
    :ok = Circuits.GPIO.set_interrupts(gpio, :rising)

    Logger.info("Contando pulsos en #{linea}")

    {:ok,
     %{
       gpio: gpio,
       pulsos: 0,
       inicio_us: System.monotonic_time(:microsecond),
       ultimo_us: nil,
       periodo_us: nil
     }}
  end

  @impl true
  def handle_info({:circuits_gpio, _linea, timestamp_ns, 1}, estado) do
    ahora_us = div(timestamp_ns, 1_000)

    periodo =
      case estado.ultimo_us do
        nil -> nil
        anterior -> ahora_us - anterior
      end

    {:noreply,
     %{estado | pulsos: estado.pulsos + 1, ultimo_us: ahora_us, periodo_us: periodo}}
  end

  def handle_info({:circuits_gpio, _linea, _timestamp, 0}, estado) do
    {:noreply, estado}
  end

  @impl true
  def handle_call(:estado, _from, estado) do
    frecuencia =
      case estado.periodo_us do
        nil -> nil
        0 -> nil
        p -> Float.round(1_000_000 / p, 3)
      end

    respuesta = %{
      pulsos: estado.pulsos,
      periodo_us: estado.periodo_us,
      frecuencia_hz: frecuencia
    }

    {:reply, respuesta, estado}
  end
end

Con este módulo se puede verificar en la práctica lo que la fórmula predice: montar el astable de las luces de ferrocarril, conectarlo a un GPIO y comparar la frecuencia medida con el 1,02 Hz calculado. La diferencia entre ambos números es la tolerancia real de los componentes, que en el caso de un condensador electrolítico puede llegar al 20 %.

Criterio de elección

NecesidadHerramienta adecuadaRazón
Retardo fijo de milisegundos a minutos, sin firmware555 monoestableNo requiere programación ni mantenimiento
Oscilador estable de audio o baja frecuencia555 astable o TLC555Costo mínimo, independiente del software
Lazo de control con ADC, PWM y lógica de decisiónPIC16F877a o equivalente modernoPeriféricos dedicados, tiempo determinista
Enseñar programación de hardware sin cadena de herramientasBasic Stamp con PBASICCurva de entrada mínima, retroalimentación inmediata
Conectividad de red, JSON, TLS, base de datosElixir con NervesLa BEAM aporta concurrencia y tolerancia a fallos
Wi-Fi con código Elixir sobre el propio microcontroladorAtomVM en ESP32Elixir corriendo en el chip, sin Linux
Interfaz web en vivo sobre datos del hardwarePhoenix LiveViewActualización por WebSocket sin escribir JavaScript

Errores comunes

SíntomaCausaSolución
El 555 astable oscila a una frecuencia distinta de la calculadaTolerancia del condensador electrolítico, que puede ser de ±20 %Usar condensadores de poliéster o cerámicos para tiempos críticos, o ajustar con potenciómetro
El microcontrolador cercano se reinicia al arrancar el 555Pico de corriente en cada transición de la etapa de salidaCondensador de 100 nF entre pines 8 y 1 del 555, lo más cerca posible del encapsulado
La salida del 555 tiene pulsos falsos o ruidoPin 5 (Control Voltage) flotando y captando interferenciaCondensador cerámico de 10 nF entre el pin 5 y masa
El 555 no arranca nuncaPin 4 (Reset) sin conectarUnir el pin 4 a Vcc, o a una señal de habilitación con resistencia pull-up
El ciclo de trabajo del astable no baja del 50 %La fórmula (R1+R2)/(R1+2R2) siempre es mayor a 0,5Agregar un diodo en paralelo con R2 para separar carga y descarga
El monoestable no reinicia la cuenta con nuevos disparosEl 555 monoestable no es reactivable por diseñoDescargar el condensador antes del nuevo disparo, o usar un temporizador reactivable
El PIC compila sin errores pero no hace nadaBits de configuración incorrectos, típicamente FOSC distinto del oscilador realRevisar los #pragma config: FOSC = HS para cristal de 20 MHz, XT para 4 MHz
Los pines de PORTA del PIC no responden como digitalesArrancan configurados como entradas analógicas del ADCEscribir ADCON1 = 0x06; en la inicialización
El pin RB3 del PIC se comporta de forma erráticaProgramación en baja tensión activa reserva ese pinPoner #pragma config LVP = OFF
Los __delay_ms del PIC dan tiempos escalados_XTAL_FREQ no coincide con el cristal instaladoDefinir _XTAL_FREQ con la frecuencia real antes de incluir los retardos
El PWM del PIC sale con frecuencia equivocadaPrescaler de Timer2 o PR2 mal calculadosVerificar con T_pwm = (PR2+1) x 4 x Tosc x prescaler
Un pin del PIC leido con PORTx da valores raros al escribir bits sueltosEfecto lectura-modificación-escritura sobre pines con carga capacitivaEscribir el puerto completo desde una variable espejo en RAM
El Basic Stamp se queda sin memoria de variablesSolo hay 26 bytes disponibles en el BS2Reutilizar variables, usar Nib y Bit en vez de Byte, o guardar datos con WRITE en EEPROM
El programa PBASIC no descarga al móduloFalta la directiva {$STAMP BS2} o el puerto serie está ocupadoAgregar la directiva en la primera línea y cerrar otras terminales que usen el puerto
El servo del Boe-Bot se mueve a tironesEl bucle de PULSOUT no mantiene el periodo de 20 msIncluir PAUSE 20 dentro del bucle de refresco y evitar operaciones lentas entre pulsos
El robot gira cuando debería avanzar rectoServos no calibrados: el punto neutro no está exactamente en 750Ajustar el tornillo de calibración de cada servo con PULSOUT pin, 750 en bucle hasta que se detengan
SERIN en el Basic Stamp no recibe nada del PCBaudmode incorrecto: falta o sobra la inversión de nivelesUsar 16468 para el puerto de programación con niveles RS-232 y 84 para niveles TTL directos
Circuits.UART.read/2 en Elixir devuelve {:ok, ""}El microcontrolador no terminó la trama con el separador esperadoConfigurar framing: {Circuits.UART.Framing.Line, separator: "\n"} y asegurarse de que el firmware envíe ese carácter
La cuenta de pulsos en Elixir es mayor que la realRebotes del 555 o del cableado disparando interrupciones espuriasFiltrar por tiempo mínimo entre flancos en el handle_info, o agregar un filtro RC en la entrada

Ejercicios propuestos

Nivel introductorio

  1. Ejecutar ic555.exs y modificarlo para que acepte los valores por línea de comandos usando System.argv/0. Debe imprimir un mensaje de uso si faltan argumentos.
  2. Calcular a mano y verificar con el script que valores de R1, R2 y C generan un astable de exactamente 1 kHz con ciclo de trabajo del 60 %. Comparar el resultado teórico con el que sale al ajustar a la serie E12.
  3. Dibujar el diagrama de tiempos del modo monoestable indicando la tensión en el condensador, la señal en TRIGGER y la salida del pin 3, todo en la misma base de tiempo.

Nivel intermedio

  1. Extender el módulo IC555 con una función disenar_monoestable/2 que, dada una duración objetivo y un condensador, devuelva la resistencia necesaria y su valor E12 más cercano.
  2. Escribir en C para PIC16F877a un programa que lea el canal AN0 del ADC y ajuste el ciclo de trabajo del PWM en CCP1 de forma proporcional a la lectura. Documentar la configuración de ADCON0 y ADCON1.
  3. Modificar contador_pulsaciones.bs2 para que use la EEPROM del Basic Stamp: la cuenta debe sobrevivir a un corte de alimentación. Usar READ y WRITE.
  4. Implementar en PuenteSerial un mecanismo de reintento: si tres sondeos consecutivos fallan, el GenServer debe cerrar el puerto, esperar un segundo, reabrirlo y volver a intentar.

Nivel avanzado

  1. Construir el Atari Punk Console y medir su salida con el módulo Contador555 conectado a un GPIO. Graficar la relación entre la posición de P2 y la frecuencia de salida efectiva, e identificar la región donde aparecen los saltos de pulso.
  2. Escribir un firmware para PIC16F877a que actúe como esclavo I2C usando el módulo MSSP, exponiendo dos registros: uno de lectura con el valor del ADC y otro de escritura con el ciclo de trabajo del PWM. Implementar el maestro en Elixir con circuits_i2c.
  3. Diseñar un temporizador de vigilancia con un 555 en modo monoestable de 5 segundos: un proceso Elixir debe refrescar el pin TRIGGER cada 2 segundos con circuits_gpio; si el proceso muere, el 555 debe accionar un relé. Escribir el supervisor OTP y verificar el comportamiento matando el proceso con Process.exit/2.
  4. Comparar en el osciloscopio la estabilidad temporal de tres fuentes de pulso de 1 kHz: un astable con NE555, el módulo CCP de un PIC16F877a y un Process.send_after/3 en Elixir. Cuantificar el jitter de cada una y explicar el origen de la diferencia.

Lo que viene

En este capítulo bajamos al nivel donde el tiempo se construye con resistencias, condensadores y registros de 8 bits. Vimos que el 555 resuelve el problema del tiempo sin ejecutar una sola instrucción, que el PIC16F877a agrega periféricos deterministas bajo control de un programa compilado, y que el Basic Stamp cambia velocidad por accesibilidad con un intérprete que cabe en una estampilla. Los tres siguen siendo útiles porque resuelven exactamente lo que un sistema con sistema operativo y máquina virtual resuelve peor: responder siempre en el mismo intervalo, sin importar qué esté pasando en el resto del sistema.

El capítulo 7 da el salto a las dos plataformas que reemplazaron a estos clásicos en la mayoría de los proyectos actuales: Arduino y Raspberry Pi. Vamos a ver por qué Arduino ganó la batalla del prototipado que el Basic Stamp había empezado, en qué se diferencia un microcontrolador AVR de un PIC, y por qué la Raspberry Pi pertenece a otra categoría por completo, la de las computadoras de placa única capaces de ejecutar Linux y, con él, la máquina virtual de Elixir. Ese capítulo es también la puerta de entrada a Nerves, el marco de trabajo que convierte una Raspberry Pi en un dispositivo embebido con firmware inmutable.

El índice completo del curso reúne todos los capítulos.