pwm-servomotores-nanoframework

PWM and Servomotors in nanoFramework

  • 5 min

PWM is a technique for simulating analog levels by alternating a digital signal very quickly.

Until now, our digital world was black or white. A GPIO pin could only be High (3.3V) or Low (0V). But what if we want to set an LED to 50% brightness? Or move a robotic arm to an exact 90-degree position?

For that, we use PWM (Pulse Width Modulation).

Instead of generating an intermediate voltage, we turn the pin on and off very fast. The LED integrates the pulses and we perceive an average brightness; a servo, on the other hand, interprets the pulse duration as a setpoint.

In this post, we will learn how to use the PwmChannel class to generate and adjust that signal.

For this tutorial, you need to install the NuGet package: nanoFramework.System.Device.Pwm

What is PWM?

PWM is based on two key parameters:

  1. Frequency (Hz): How many times per second the cycle repeats (e.g., 1000Hz = 1000 times per second).
  2. Duty cycle: what percentage of the time the signal is High.
  • 0%: Always off (0V).
  • 50%: Half on, half off (ideal average value of 1.65V with a 3.3V signal).
  • 100%: Always on (3.3V).

The PwmChannel Class

In nanoFramework, control is performed through PwmChannel. Unlike Arduino (analogWrite), here we have full control over the frequency.

using System.Device.Pwm;

// Create the channel
// Chip 1, Channel 2 (This depends on the pin mapping of each board; it's usually more flexible on ESP32)
PwmChannel pwm = PwmChannel.CreateFromPin(pin: 2, frequency: 1000, dutyCyclePercentage: 0.5);

pwm.Start();
pwm.DutyCycle = 0.8; // Change to 80%
Copied!

Note on Hardware (ESP32): The ESP32 has a peripheral called LEDC to generate PWM. nanoFramework manages it automatically, but remember that not all pins support PWM (though the vast majority do).

Adjusting LED Brightness

We will make the built-in LED (GPIO 2) smoothly increase and decrease its intensity.

We will use a frequency of 1000 Hz (1 KHz), which is sufficient to avoid noticeable flicker in an LED.

using System;
using System.Device.Pwm;
using System.Threading;
using System.Diagnostics;

namespace PwmEjemplo
{
    public class Program
    {
        public static void Main()
        {
            // GPIO 2, Frequency 1000Hz, Start off (0.0)
            PwmChannel ledPwm = PwmChannel.CreateFromPin(2, 1000, 0.0);
            
            ledPwm.Start();

            Debug.WriteLine("Starting Fading effect...");

            while (true)
            {
                // Increase brightness
                for (double i = 0; i <= 1.0; i += 0.05)
                {
                    ledPwm.DutyCycle = i;
                    Thread.Sleep(20);
                }

                // Decrease brightness
                for (double i = 1.0; i >= 0; i -= 0.05)
                {
                    ledPwm.DutyCycle = i;
                    Thread.Sleep(20);
                }
            }
        }
    }
}
Copied!

You now have an LED that changes brightness smoothly by varying DutyCycle between 0.0 and 1.0.

Controlling a Servomotor

Controlling a Servo (SG90 / MG995) is a bit more delicate. Servos don’t simply work with “more voltage = faster”. They expect a very specific PWM signal:

  • Frequency: typically 50 Hz (period of 20 ms; check your specific model).
  • Pulse Width: Determines the angle.
  • ~1.0 ms = 0 degrees.
  • ~1.5 ms = 90 degrees (Center).
  • ~2.0 ms = 180 degrees.

These values vary slightly depending on the servo manufacturer.

Calculating the Duty Cycle

Since nanoFramework works with DutyCycle (0.0 to 1.0) and not directly with microseconds in this class, we need to do a rule of three.

If the period is 20ms (50Hz):

  • 1ms (0º) = 1ms / 20ms = 0.05 (5% Duty)
  • 2ms (180º) = 2ms / 20ms = 0.10 (10% Duty)

The movement range of our servo will be between 5% and 10% of the duty cycle.

Servo Code

Connect the servo signal pin to GPIO 15. Power it from a suitable source and connect its GND to the ESP32’s GND; do not power the motor from a GPIO.

using System;
using System.Device.Pwm;
using System.Threading;

namespace ServoControl
{
    public class Program
    {
        public static void Main()
        {
            // Configuration for Servo: 50Hz is MANDATORY
            PwmChannel servo = PwmChannel.CreateFromPin(15, 50, 0.05); // Start at 0º
            servo.Start();

            // Typical calibration (adjust according to your model)
            double dutyMin = 0.025; // ~0.5ms (0 degrees) - Adjusted for SG90
            double dutyMax = 0.125; // ~2.5ms (180 degrees)

            while (true)
            {
                // Move to 0 degrees
                SetServoAngle(servo, 0, dutyMin, dutyMax);
                Thread.Sleep(1000);

                // Move to 90 degrees
                SetServoAngle(servo, 90, dutyMin, dutyMax);
                Thread.Sleep(1000);

                // Move to 180 degrees
                SetServoAngle(servo, 180, dutyMin, dutyMax);
                Thread.Sleep(1000);
            }
        }

        static void SetServoAngle(PwmChannel pwm, double angle, double minDuty, double maxDuty)
        {
            // Map angle (0-180) to the Duty Cycle range
            double duty = minDuty + ((angle / 180.0) * (maxDuty - minDuty));
            
            // Limit protection
            if (duty > maxDuty) duty = maxDuty;
            if (duty < minDuty) duty = minDuty;

            pwm.DutyCycle = duty;
            
            Console.WriteLine($"Angle: {angle} -> Duty: {duty:F3}");
        }
    }
}
Copied!

Calibration: Cheap servos (blue SG90s) are notorious for not respecting standard timings. If you see the servo hitting its stop and buzzing, adjust dutyMin and dutyMax little by little.

With PwmChannel we can control an LED or set the position of a servo. For loads like DC motors, we also need an appropriate power stage.