PWM
Pulse Width Modulation (PWM) generates a square wave with a fixed frequency and a variable duty cycle. It's commonly used to dim LEDs, control motor speed, drive servos, or output digital pulses for protocols like infrared. The duty cycle is the ratio of the pulse width to its period — both frequency and duty cycle are software-controllable.
Two APIs
TinyCLR offers two PWM APIs that coexist on the same hardware. Both namespaces ship in a single NuGet package — install one and you have both.
- .NET IoT API — Microsoft's standard
System.Device.Pwmnamespace. Use this for new code and for portability with desktop .NET IoT applications. - TinyCLR API — the original
GHIElectronics.TinyCLR.Devices.Pwmnamespace. Keeps v2 code working without changes.
NuGet packages:
GHIElectronics.TinyCLR.Devices.Pwm— contains both theSystem.Device.Pwm(.NET IoT) andGHIElectronics.TinyCLR.Devices.Pwm(TinyCLR) namespaces.GHIElectronics.TinyCLR.Pins— pin name constants for SITCore boards. Shared across GPIO, PWM, SPI, I²C, UART, and other peripherals.
Pick the right controller and channel — there is no Default. Most peripherals have a single controller, but PWM has multiple. Always select a specific channel on a specific controller.
Notation: "PWM2.3" means channel 3 on controller 2.
.NET IoT chip numbering: the chip argument in PwmChannel.Create(chip, channel, ...) is the controller number minus one — Controller1 → chip: 0, Controller2 → chip: 1, Controller3 → chip: 2. The pin constant tells you which controller (and channel) to use — for example, FEZBit.Timer.Pwm.Controller1.Led is on chip 0.
Energy level
PWM is perfect for dimming an LED or controlling the speed of a motor. When the duty cycle is 50%, half the energy is transferred to the attached load.
- .NET IoT API
- TinyCLR API
using System.Device.Pwm;
using GHIElectronics.TinyCLR.Pins;
using PwmChannel led = PwmChannel.Create(FEZBit.Timer.Pwm.Controller1.Chip, FEZBit.Timer.Pwm.Controller1.Led, 10000, 0.5);
double duty = 0.5, speed = 0.01;
led.Start();
while (true){
if (duty <= 0 || duty >= 1.0){
speed *= -1; // Reverse direction.
duty += speed;
}
led.DutyCycle = duty;
duty += speed;
Thread.Sleep(10); // Always give the system time to think!
}
using System.Threading;
using GHIElectronics.TinyCLR.Devices.Pwm;
using GHIElectronics.TinyCLR.Pins;
var controller = PwmController.FromName(FEZBit.Timer.Pwm.Controller1.Id);
var led = controller.OpenChannel(FEZBit.Timer.Pwm.Controller1.Led);
controller.SetDesiredFrequency(10000);
double duty = 0.5, speed = 0.01;
led.Start();
while (true){
if (duty <= 0 || duty >= 1.0){
speed *= -1; // Reverse direction.
duty += speed;
}
led.SetActiveDutyCyclePercentage(duty);
duty += speed;
Thread.Sleep(10); // Always give the system time to think!
}
Polarity and the idle level
Duty cycle is the fraction of the period the output spends active, and polarity decides which level "active" means. With the default ActiveHigh, the pin idles low and pulses high. With ActiveLow it idles high and pulses low.
That has one consequence worth stating plainly, because it surprises people: a duty cycle of 0 does not mean 0 V. It means zero active time, so the pin rests at the inactive level — low for ActiveHigh, high for ActiveLow. If you need a driven logic 0, use ActiveHigh with a duty cycle of 0.
Stopping a channel
Stopping a PWM channel releases the pin rather than driving it. The pin is reconfigured as a floating input, so its level is decided entirely by whatever is attached to it — not by PWM. Stopping does not give you a logic 0.
This matters most for anything that must fail safe: a motor driver, a heater, a FET gate. It is also true before your application runs at all — the pin floats from reset until you configure it.
There are three ways to get a defined level, and they are not equivalent:
Fit an external pull resistor. The only option that also covers reset, boot and a crashed application, because it doesn't depend on software running. Use this for anything where the wrong level is unsafe.
Leave the channel running at 0% duty. With ActiveHigh this actively drives low, which is genuinely different from stopping. Costs you a timer channel, but the level is driven rather than floating.
Dispose the channel, then drive the pin as GPIO:
pwmChannel.Stop();
pwmChannel.Dispose(); // required - see below
var gpio = GpioController.GetDefault().OpenPin(FEZBit.GpioPin.Led);
gpio.SetDriveMode(GpioPinDriveMode.Output);
gpio.Write(GpioPinValue.Low);
Stop() on its own does not free the pin — it stays reserved by the PWM controller, and opening it as a GPIO fails with a sharing violation. You must Dispose() the channel first.
Musical tones
Musical notes have specific frequencies — C is about 261 Hz. Putting frequencies and durations in arrays lets you sequence notes to play simple melodies. When playing notes by changing the frequency, keep the duty cycle at 0.5.
The example below targets the SC20100S Dev Board.
- .NET IoT API
- TinyCLR API
using System.Device.Pwm;
using GHIElectronics.TinyCLR.Pins;
const int NOTE_C = 261;
const int NOTE_D = 294;
const int NOTE_E = 330;
const int NOTE_F = 349;
const int NOTE_G = 392;
const int WHOLE_DURATION = 2000;
const int EIGHTH = WHOLE_DURATION / 8;
const int QUARTER = WHOLE_DURATION / 4;
const int QUARTERDOT = WHOLE_DURATION / 3;
const int HALF = WHOLE_DURATION / 2;
const int WHOLE = WHOLE_DURATION;
// Make sure the two arrays match in length — each duration corresponds to one note.
int[] note = { NOTE_E, NOTE_E, NOTE_F, NOTE_G, NOTE_G, NOTE_F, NOTE_E,
NOTE_D, NOTE_C, NOTE_C, NOTE_D, NOTE_E, NOTE_E, NOTE_D,
NOTE_D, NOTE_E, NOTE_E, NOTE_F, NOTE_G, NOTE_G, NOTE_F,
NOTE_E, NOTE_D, NOTE_C, NOTE_C, NOTE_D, NOTE_E, NOTE_D,
NOTE_C, NOTE_C };
int[] duration = { QUARTER, QUARTER, QUARTER, QUARTER, QUARTER, QUARTER,
QUARTER, QUARTER, QUARTER, QUARTER, QUARTER, QUARTER,
QUARTERDOT, EIGHTH, HALF, QUARTER, QUARTER, QUARTER, QUARTER,
QUARTER, QUARTER, QUARTER, QUARTER, QUARTER, QUARTER,
QUARTER, QUARTER, QUARTERDOT, EIGHTH, WHOLE };
using PwmChannel toneOut = PwmChannel.Create(FEZBit.Timer.Pwm.Controller3.Chip, FEZBit.Timer.Pwm.Controller3.Buzzer, NOTE_C, 0.5);
while (true){
toneOut.Start();
for (int i = 0; i < note.Length; i++){
toneOut.Frequency = note[i];
Thread.Sleep(duration[i]);
}
toneOut.Stop();
Thread.Sleep(1000);
}
using GHIElectronics.TinyCLR.Devices.Pwm;
using GHIElectronics.TinyCLR.Pins;
const int NOTE_C = 261;
const int NOTE_D = 294;
const int NOTE_E = 330;
const int NOTE_F = 349;
const int NOTE_G = 392;
const int WHOLE_DURATION = 2000;
const int EIGHTH = WHOLE_DURATION / 8;
const int QUARTER = WHOLE_DURATION / 4;
const int QUARTERDOT = WHOLE_DURATION / 3;
const int HALF = WHOLE_DURATION / 2;
const int WHOLE = WHOLE_DURATION;
// Make sure the two arrays match in length — each duration corresponds to one note.
int[] note = { NOTE_E, NOTE_E, NOTE_F, NOTE_G, NOTE_G, NOTE_F, NOTE_E,
NOTE_D, NOTE_C, NOTE_C, NOTE_D, NOTE_E, NOTE_E, NOTE_D,
NOTE_D, NOTE_E, NOTE_E, NOTE_F, NOTE_G, NOTE_G, NOTE_F,
NOTE_E, NOTE_D, NOTE_C, NOTE_C, NOTE_D, NOTE_E, NOTE_D,
NOTE_C, NOTE_C };
int[] duration = { QUARTER, QUARTER, QUARTER, QUARTER, QUARTER, QUARTER,
QUARTER, QUARTER, QUARTER, QUARTER, QUARTER, QUARTER,
QUARTERDOT, EIGHTH, HALF, QUARTER, QUARTER, QUARTER, QUARTER,
QUARTER, QUARTER, QUARTER, QUARTER, QUARTER, QUARTER,
QUARTER, QUARTER, QUARTERDOT, EIGHTH, WHOLE };
var controller = PwmController.FromName(FEZBit.Timer.Pwm.Controller3.Id);
var toneOut = controller.OpenChannel(FEZBit.Timer.Pwm.Controller3.Buzzer);
toneOut.SetActiveDutyCyclePercentage(0.5);
while (true){
toneOut.Start();
for (int i = 0; i < note.Length; i++){
controller.SetDesiredFrequency(note[i]);
Thread.Sleep(duration[i]);
}
toneOut.Stop();
Thread.Sleep(1000);
}
Software PWM
Hardware PWM generates signals accurately with zero CPU overhead but is limited to specific pins. TinyCLR also supports software PWM on any GPIO pin — less accurate, costs CPU time, but works well at lower frequencies suitable for dimming or servos.
On SITCore, the PWM frequency range is 15.4 Hz to 10 kHz.
Software PWM is currently exposed through the TinyCLR API only.
using System.Threading;
using GHIElectronics.TinyCLR.Devices.Pwm;
using GHIElectronics.TinyCLR.Pins;
var softwarePwmController = PwmController.FromName(FEZBit.Timer.Pwm.Software.Id);
var buzzer = softwarePwmController.OpenChannel(FEZBit.GpioPin.Buzzer);
softwarePwmController.SetDesiredFrequency(1000); // 1 kHz
buzzer.SetActiveDutyCyclePercentage(0.5);
buzzer.Start();
Thread.Sleep(10 * 1000); // 10 seconds
buzzer.Stop();
Servo motors
Servos use PWM signals to position their shaft. A driver NuGet package is provided to simplify common servo operations. The example below uses software PWM so any GPIO pin works.
Servos typically have three wires:
- Center — connect to 5V
- Lighter color (one side) — signal, connect to a PWM pin
- Darker color (third) — ground
Typical pulse width is 1–2 ms, but check your servo's datasheet.
NuGet packages needed: GHIElectronics.TinyCLR.Devices.Pwm, GHIElectronics.TinyCLR.Pins, GHIElectronics.TinyCLR.Drivers.Motor.Servo.
using System.Threading;
using GHIElectronics.TinyCLR.Devices.Pwm;
using GHIElectronics.TinyCLR.Drivers.Motor.Servo;
using GHIElectronics.TinyCLR.Pins;
var softwarePwmController = PwmController.FromName(FEZBit.Timer.Pwm.Software.Id);
var pad0 = softwarePwmController.OpenChannel(FEZBit.GpioPin.P0);
var servo = new ServoController(softwarePwmController, pad0);
while (true){
servo.Set(0); // 0 degrees
Thread.Sleep(2000);
servo.Set(45.0); // 45 degrees
Thread.Sleep(2000);
servo.Set(90.0); // 90 degrees
Thread.Sleep(2000);
servo.Set(180.0); // 180 degrees
Thread.Sleep(4000);
}
API reference
| Namespace | Description |
|---|---|
| GHIElectronics.TinyCLR.Devices.Pwm | TinyCLR PWM controller and channel classes |
| System.Device.Pwm | .NET IoT standard PWM API (included in the same NuGet) |