Los controladores clásicos: 555, PIC16F877a y Basic Stamp
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ística | 555 | PIC16F877a | Basic Stamp 2 |
|---|---|---|---|
| Categoría | Temporizador analógico-digital | Microcontrolador de 8 bits | Módulo con microcontrolador + intérprete |
| Año de aparición | 1971 (Signetics, SE555/NE555) | 2001 (Microchip, familia PIC16F87x) | 1990s, BS2 desde 1995; línea educativa consolidada hacia 1997 |
| Se programa | No, se configura con R y C | Sí, en C o ensamblador | Sí, en PBASIC |
| Memoria de programa | Ninguna | 8K palabras de 14 bits (14 KB) | 2 KB EEPROM de tokens |
| Memoria de datos | Ninguna | 368 bytes RAM + 256 bytes EEPROM | 32 bytes RAM (26 para variables) |
| Reloj típico | Definido por el circuito RC | Hasta 20 MHz | 20 MHz internos del intérprete |
| Velocidad efectiva | Continua, analógica | 5 MIPS (200 ns por instrucción) | ~4.000 instrucciones PBASIC por segundo |
| Pines de E/S | 8 pines totales, 1 salida | 33 pines de E/S | 16 pines de E/S |
| Periféricos | Ninguno | ADC 10 bits, 3 timers, CCP, USART, MSSP | Los que provee el intérprete por software |
| Alimentación | 4,5 a 16 V (bipolar) | 2,0 a 5,5 V | 5 a 12 V en el módulo (regulador incluido) |
| Herramienta | Calculadora y protoboard | MPLAB X / MPLAB Xpress + PICkit | Basic Stamp Editor + cable serial/USB |
| Costo relativo | Muy bajo | Bajo | Medio-alto |
| Uso hoy | Osciladores, retardos, PWM simple | Control industrial, kits, docencia | Docencia 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
| Pin | Nombre | Función |
|---|---|---|
| 1 | GND | Referencia de masa |
| 2 | TRIGGER | Entrada del comparador inferior; por debajo de 1/3 Vcc activa la salida |
| 3 | OUTPUT | Salida push-pull; en el NE555 bipolar entrega o absorbe hasta unos 200 mA |
| 4 | RESET | Activo en bajo; forzarlo a masa apaga la salida. Se conecta a Vcc si no se usa |
| 5 | CONTROL VOLTAGE | Acceso directo al umbral 2/3 Vcc. Se desacopla con 10 nF si no se usa |
| 6 | THRESHOLD | Entrada del comparador superior; por encima de 2/3 Vcc desactiva la salida |
| 7 | DISCHARGE | Colector abierto del transistor interno; descarga el condensador de tiempo |
| 8 | VCC | Alimentació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:
| Componente | Valor | Función |
|---|---|---|
| IC | NE555 | Generador astable |
| C1 | 10 µF electrolítico | Condensador de tiempo |
| R1 | 47 kΩ | Camino de carga |
| R2 | 47 kΩ | Camino de carga y descarga |
| R3, R4 | 470 Ω | Limitadoras de corriente de cada LED |
| LED1, LED2 | Rojos de 5 mm | Señalización |
| C2 | 100 nF cerámico | Desacople de alimentación |
| C3 | 10 nF cerámico | Desacople 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.
| Componente | Valor | Función |
|---|---|---|
| IC | NE555 | Astable audible |
| R1 | 1 kΩ | Camino de carga fijo |
| P1 | Potenciómetro de 250 kΩ | Ajuste del tempo |
| C1 | 22 µF electrolítico | Condensador de tiempo |
| C2 | 100 nF | Acoplo al altavoz |
| SP1 | Altavoz 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ística | PIC16F877a | PIC18F4550 |
|---|---|---|
| Familia | PIC16, gama media | PIC18, gama media-alta |
| Ancho de instrucción | 14 bits | 16 bits |
| Repertorio | 35 instrucciones | 75+ instrucciones, con multiplicación por hardware |
| Memoria de programa | 8K palabras (14 KB) | 16K palabras (32 KB) |
| RAM | 368 bytes | 2048 bytes |
| EEPROM | 256 bytes | 256 bytes |
| Frecuencia | 20 MHz | 48 MHz (con PLL interno) |
| ADC | 8 canales de 10 bits | 13 canales de 10 bits |
| USB | No | Sí, USB 2.0 Full Speed integrado |
| Pines | 40 / 44 | 40 / 44 |
| Optimización para C | Limitada, pila de hardware de 8 niveles | Diseñ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ódulo | Velocidad aproximada | RAM de variables | EEPROM de programa | Pines de E/S |
|---|---|---|---|---|
| BS1 | ~2.000 instrucciones/s | 14 bytes | 256 bytes | 8 |
| BS2 | ~4.000 instrucciones/s | 26 bytes | 2 KB | 16 |
| BS2sx | ~10.000 instrucciones/s | 26 bytes | 8 x 2 KB (bancos) | 16 |
| BS2p | ~12.000 instrucciones/s | 38 bytes | 8 x 2 KB | 16 |
| BS2px | ~19.000 instrucciones/s | 38 bytes | 8 x 2 KB | 16 |
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ón | Que hace |
|---|---|
HIGH pin / LOW pin | Configura el pin como salida y lo pone en alto o bajo |
TOGGLE pin | Invierte el estado del pin |
INPUT pin / OUTPUT pin | Fija la dirección del pin sin cambiar su estado |
PAUSE ms | Detiene la ejecución la cantidad de milisegundos indicada |
DEBUG "texto" | Envia texto al terminal del PC por el puerto de programación |
DEBUGIN var | Lee un valor tecleado en el terminal del PC |
PULSOUT pin, duracion | Genera un pulso en unidades de 2 µs en el BS2 |
PULSIN pin, estado, var | Mide el ancho de un pulso entrante |
FREQOUT pin, ms, frecuencia | Genera un tono en el pin |
SERIN pin, baudmode, [ ... ] | Recibe datos serie por software |
SEROUT pin, baudmode, [ ... ] | Transmite datos serie por software |
RCTIME pin, estado, var | Mide el tiempo de descarga de un circuito RC, para sensores resistivos |
COUNT pin, ms, var | Cuenta transiciones en un pin durante un intervalo |
GOSUB / RETURN | Llamada a subrutina |
DO ... LOOP, FOR ... NEXT | Estructuras de repetición |
EEPROM / READ / WRITE | Acceso 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
| Necesidad | Herramienta adecuada | Razón |
|---|---|---|
| Retardo fijo de milisegundos a minutos, sin firmware | 555 monoestable | No requiere programación ni mantenimiento |
| Oscilador estable de audio o baja frecuencia | 555 astable o TLC555 | Costo mínimo, independiente del software |
| Lazo de control con ADC, PWM y lógica de decisión | PIC16F877a o equivalente moderno | Periféricos dedicados, tiempo determinista |
| Enseñar programación de hardware sin cadena de herramientas | Basic Stamp con PBASIC | Curva de entrada mínima, retroalimentación inmediata |
| Conectividad de red, JSON, TLS, base de datos | Elixir con Nerves | La BEAM aporta concurrencia y tolerancia a fallos |
| Wi-Fi con código Elixir sobre el propio microcontrolador | AtomVM en ESP32 | Elixir corriendo en el chip, sin Linux |
| Interfaz web en vivo sobre datos del hardware | Phoenix LiveView | Actualización por WebSocket sin escribir JavaScript |
Errores comunes
| Síntoma | Causa | Solución |
|---|---|---|
| El 555 astable oscila a una frecuencia distinta de la calculada | Tolerancia 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 555 | Pico de corriente en cada transición de la etapa de salida | Condensador 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 ruido | Pin 5 (Control Voltage) flotando y captando interferencia | Condensador cerámico de 10 nF entre el pin 5 y masa |
| El 555 no arranca nunca | Pin 4 (Reset) sin conectar | Unir 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,5 | Agregar un diodo en paralelo con R2 para separar carga y descarga |
| El monoestable no reinicia la cuenta con nuevos disparos | El 555 monoestable no es reactivable por diseño | Descargar el condensador antes del nuevo disparo, o usar un temporizador reactivable |
| El PIC compila sin errores pero no hace nada | Bits de configuración incorrectos, típicamente FOSC distinto del oscilador real | Revisar los #pragma config: FOSC = HS para cristal de 20 MHz, XT para 4 MHz |
| Los pines de PORTA del PIC no responden como digitales | Arrancan configurados como entradas analógicas del ADC | Escribir ADCON1 = 0x06; en la inicialización |
| El pin RB3 del PIC se comporta de forma errática | Programación en baja tensión activa reserva ese pin | Poner #pragma config LVP = OFF |
Los __delay_ms del PIC dan tiempos escalados | _XTAL_FREQ no coincide con el cristal instalado | Definir _XTAL_FREQ con la frecuencia real antes de incluir los retardos |
| El PWM del PIC sale con frecuencia equivocada | Prescaler de Timer2 o PR2 mal calculados | Verificar con T_pwm = (PR2+1) x 4 x Tosc x prescaler |
Un pin del PIC leido con PORTx da valores raros al escribir bits sueltos | Efecto lectura-modificación-escritura sobre pines con carga capacitiva | Escribir el puerto completo desde una variable espejo en RAM |
| El Basic Stamp se queda sin memoria de variables | Solo hay 26 bytes disponibles en el BS2 | Reutilizar variables, usar Nib y Bit en vez de Byte, o guardar datos con WRITE en EEPROM |
| El programa PBASIC no descarga al módulo | Falta la directiva {$STAMP BS2} o el puerto serie está ocupado | Agregar la directiva en la primera línea y cerrar otras terminales que usen el puerto |
| El servo del Boe-Bot se mueve a tirones | El bucle de PULSOUT no mantiene el periodo de 20 ms | Incluir PAUSE 20 dentro del bucle de refresco y evitar operaciones lentas entre pulsos |
| El robot gira cuando debería avanzar recto | Servos no calibrados: el punto neutro no está exactamente en 750 | Ajustar 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 PC | Baudmode incorrecto: falta o sobra la inversión de niveles | Usar 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 esperado | Configurar 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 real | Rebotes del 555 o del cableado disparando interrupciones espurias | Filtrar por tiempo mínimo entre flancos en el handle_info, o agregar un filtro RC en la entrada |
Ejercicios propuestos
Nivel introductorio
- Ejecutar
ic555.exsy modificarlo para que acepte los valores por línea de comandos usandoSystem.argv/0. Debe imprimir un mensaje de uso si faltan argumentos. - 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.
- 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
- Extender el módulo
IC555con una funcióndisenar_monoestable/2que, dada una duración objetivo y un condensador, devuelva la resistencia necesaria y su valor E12 más cercano. - 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
ADCON0yADCON1. - Modificar
contador_pulsaciones.bs2para que use la EEPROM del Basic Stamp: la cuenta debe sobrevivir a un corte de alimentación. UsarREADyWRITE. - Implementar en
PuenteSerialun mecanismo de reintento: si tres sondeos consecutivos fallan, el GenServer debe cerrar el puerto, esperar un segundo, reabrirlo y volver a intentar.
Nivel avanzado
- Construir el Atari Punk Console y medir su salida con el módulo
Contador555conectado 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. - 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. - 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 conProcess.exit/2. - 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/3en 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.