El PWM es una técnica para simular niveles analógicos alternando una señal digital muy rápido.
Hasta ahora, nuestro mundo digital era blanco o negro. Un pin GPIO solo podía estar en High (3.3V) o en Low (0V). Pero, ¿qué pasa si queremos poner un LED al 50% de brillo? ¿O si queremos mover un brazo robótico a una posición exacta de 90 grados?
Para eso usamos el PWM (Pulse Width Modulation).
En lugar de generar una tensión intermedia, encendemos y apagamos el pin muy rápido. El LED integra los pulsos y percibimos un brillo medio; un servo, en cambio, interpreta su duración como una consigna.
En esta entrada aprenderemos a usar la clase PwmChannel para generar y ajustar esa señal.
Para este tutorial necesitas instalar el paquete NuGet: nanoFramework.System.Device.Pwm
¿Qué es el PWM?
El PWM se basa en dos parámetros clave:
- Frecuencia (Hz): Cuántas veces por segundo se repite el ciclo. (Ej: 1000Hz = 1000 veces por segundo).
- Ciclo de trabajo (duty cycle): qué porcentaje del tiempo la señal está en
High.
- 0%: Siempre apagado (0V).
- 50%: Mitad encendido y mitad apagado (valor medio ideal de 1,65 V con una señal de 3,3 V).
- 100%: Siempre encendido (3.3V).
La clase PwmChannel
En nanoFramework, el control se realiza a través de PwmChannel. A diferencia de Arduino (analogWrite), aquí tenemos control total sobre la frecuencia.
using System.Device.Pwm;
// Crear el canal
// Chip 1, Canal 2 (Esto depende del mapeo de cada placa, en ESP32 suele ser más flexible)
PwmChannel pwm = PwmChannel.CreateFromPin(pin: 2, frequency: 1000, dutyCyclePercentage: 0.5);
pwm.Start();
pwm.DutyCycle = 0.8; // Cambiar al 80%Nota sobre el Hardware (ESP32): El ESP32 tiene un periférico llamado LEDC para generar PWM. nanoFramework lo gestiona automáticamente, pero recuerda que no todos los pines soportan PWM (aunque la gran mayoría sí).
Regular el brillo de un LED
Vamos a hacer que el LED integrado (GPIO 2) suba y baje de intensidad suavemente.
Usaremos una frecuencia de 1000 Hz (1 KHz), suficiente para que no se note parpadeo en un LED.
using System;
using System.Device.Pwm;
using System.Threading;
using System.Diagnostics;
namespace PwmEjemplo
{
public class Program
{
public static void Main()
{
// GPIO 2, Frecuencia 1000Hz, Empezamos apagados (0.0)
PwmChannel ledPwm = PwmChannel.CreateFromPin(2, 1000, 0.0);
ledPwm.Start();
Debug.WriteLine("Iniciando efecto Fading...");
while (true)
{
// Subir brillo
for (double i = 0; i <= 1.0; i += 0.05)
{
ledPwm.DutyCycle = i;
Thread.Sleep(20);
}
// Bajar brillo
for (double i = 1.0; i >= 0; i -= 0.05)
{
ledPwm.DutyCycle = i;
Thread.Sleep(20);
}
}
}
}
}Ya tienes un LED que cambia de brillo suavemente al variar DutyCycle entre 0.0 y 1.0.
Controlar un servomotor
Controlar un Servo (SG90 / MG995) es un poco más delicado. Los servos no funcionan simplemente con “más voltaje = más rápido”. Esperan una señal PWM muy específica:
- Frecuencia: habitualmente 50 Hz (periodo de 20 ms; revisa el modelo concreto).
- Ancho de Pulso: Determina el ángulo.
- ~1.0 ms = 0 grados.
- ~1.5 ms = 90 grados (Centro).
- ~2.0 ms = 180 grados
Estos valores varían ligeramente según el fabricante del servo.
Calcular el ciclo de trabajo
Dado que nanoFramework trabaja con DutyCycle (0.0 a 1.0) y no con microsegundos directamente en esta clase, tenemos que hacer una regla de tres.
Si el periodo es 20ms (50Hz):
- 1ms (0º) = 1ms / 20ms = 0.05 (5% Duty)
- 2ms (180º) = 2ms / 20ms = 0.10 (10% Duty)
El rango de movimiento de nuestro servo estará entre el 5% y el 10% del ciclo de trabajo.
Código del servo
Conecta el pin de señal del servo al GPIO 15. Aliméntalo con una fuente adecuada y une su GND con el del ESP32; no alimentes el motor desde un GPIO.
using System;
using System.Device.Pwm;
using System.Threading;
namespace ServoControl
{
public class Program
{
public static void Main()
{
// Configuración para Servo: 50Hz es MANDATORIO
PwmChannel servo = PwmChannel.CreateFromPin(15, 50, 0.05); // Empezamos en 0º
servo.Start();
// Calibración típica (ajustar según tu modelo)
double dutyMin = 0.025; // ~0.5ms (0 grados) - Ajustado para SG90
double dutyMax = 0.125; // ~2.5ms (180 grados)
while (true)
{
// Mover a 0 grados
SetServoAngle(servo, 0, dutyMin, dutyMax);
Thread.Sleep(1000);
// Mover a 90 grados
SetServoAngle(servo, 90, dutyMin, dutyMax);
Thread.Sleep(1000);
// Mover a 180 grados
SetServoAngle(servo, 180, dutyMin, dutyMax);
Thread.Sleep(1000);
}
}
static void SetServoAngle(PwmChannel pwm, double angle, double minDuty, double maxDuty)
{
// Mapear ángulo (0-180) al rango de Duty Cycle
double duty = minDuty + ((angle / 180.0) * (maxDuty - minDuty));
// Protección de límites
if (duty > maxDuty) duty = maxDuty;
if (duty < minDuty) duty = minDuty;
pwm.DutyCycle = duty;
Console.WriteLine($"Angulo: {angle} -> Duty: {duty:F3}");
}
}
}Calibración: Los servos baratos (SG90 azules) son famosos por no respetar los tiempos estándar. Si ves que el servo hace tope y zumba, ajusta dutyMin y dutyMax poco a poco.
Con PwmChannel podemos regular un LED o controlar la consigna de un servo. Para cargas como motores DC necesitamos además una etapa de potencia adecuada.