Skip to content

Add tour to explain PWM #454

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions content/tour/pwm/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: "PWM"
description: "Using the PWM peripheral of common microcontrollers."
type: "docs"
weight: 4
---

PWM, or Pulse Width Modulation, is a deceptively simple but very powerful technique for controlling various things in the real world. Perhaps the most well known application is dimming LEDs, but PWM can also be used to control servo motors and producing crude (square wave) tones on speakers.
112 changes: 112 additions & 0 deletions content/tour/pwm/blink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
title: "LED: blink"
description: "Get a LED to blink using the PWM peripheral."
weight: 1
---

PWM, or pulse-width modulation, is a method of turning a GPIO pin high and low at a fixed frequency but changing the percent of time it is turned on. This has numerous applications, such as dimming LEDs. We'll see various applications of PWM throughout this tour.

To start off with, we're going to do something slightly unusual with PWM: we're going to blink an LED. Remember that PWM is a way of quickly turning an output high and low, if we do that slow enough we can see the LED blink. This should make it easier to see how PWM works.

At first we'll just define some variables. These may vary by board, so it's useful to define them in a single place.

```go
// Board specific configuration.
led := machine.LED
pwm := machine.TCC0
```

Next we're going to configure the PWM peripheral itself:

```go
// Configure the timer/PWM.
err := pwm.Configure(machine.PWMConfig{
Period: 2e9, // two seconds for a single cycle
})
if err != nil {
println("could not configure:", err.Error())
return
}
```

This configures the PWM peripheral for a cycle time of two seconds. That's much longer than PWM is commonly used for, but it allows to see us what happens.

Next up, we connect the PWM peripheral to the given output pin:

```go
// Get the channel for this PWM peripheral.
ch, err := pwm.Channel(led)
if err != nil {
println("could not obtain channel:", err.Error())
return
}
```

We get a channel number back. One PWM peripheral typically has 2-4 channels, which can be connected to certain pins. Which pins can be used depends a lot on the hardware, which we'll [get to later](../multiple/#finding-the-correct-pwm-peripheral-and-pins). For now, all you need to know is that the PWM channel is now connected to a single GPIO pin.

To actually make the LED blink, we will need to set the duty cycle. The duty cycle refers to the part of the time when the PWM is turned on. In this case, we will set the duty cycle to half the "top" value of the LED:

```go
// Blink the LED, setting it to "on" half the time.
pwm.Set(ch, pwm.Top()/2)
```

The top value is equivalent to a duty cycle of 100% - or 100% of the time on. So using the top divided by 2 results in a duty cycle of 50%, or "high" for half the time. Dividing by 3 results in a duty cycle of 33%, etc. By varying the formula you can either use a particular percentage (like we do here) or if you know the period size you can calculate a specific "on" time within the cycle (such as "500ms on, 2000ms off").

To get a feel for how it works, you can try a few things:

* You can change the period to something other than `2e9` (two seconds) and see what happens to the "top" value.
* You can change the duty cycle by changing the `pwm.Set` call.
* You can connect a different LED to the virtual board, and blink it.
* You can try using a different PWM peripheral. These boards also have `TCC1` and `TCC2`.

## More information

SparkFun has written an [excellent tutorial on PWM](https://learn.sparkfun.com/tutorials/pulse-width-modulation) that is well worth a read!



<script type="module">
import { setupTour } from '/tour.js';
let code = `
package main

import "machine"

func main() {
// Board specific configuration.
led := machine.LED
pwm := machine.TCC0

// Configure the timer/PWM.
err := pwm.Configure(machine.PWMConfig{
Period: 2e9, // two seconds for a single cycle
})
if err != nil {
println("could not configure:", err.Error())
return
}

// Get the channel for this PWM peripheral.
ch, err := pwm.Channel(led)
if err != nil {
println("could not obtain channel:", err.Error())
return
}

// Blink the LED, setting it to "on" half the time.
pwm.Set(ch, pwm.Top()/2)

println("top value: ", pwm.Top())
println("LED channel:", ch)

// Main returns, so the program exits. Yet the LED continues blinking.
}
`;
setupTour({
code: code,
boards: {
'arduino-nano33': {},
'circuitplay-express': {},
}});
</script>
93 changes: 93 additions & 0 deletions content/tour/pwm/fade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
---
title: "LED: fade"
description: "Fade a LED using the PWM peripheral."
weight: 2
---

Next up, we're going to do what PWM was actually designed for! Namely, quickly controlling an output so that the average output can be controlled precisely.

Most of it is similar to blinking an LED using the PWM peripheral, but fading needs a little bit more work:

```go
// Fade the LED in.
for percentOn := 0; percentOn <= 100; percentOn++ {
pwm.Set(ch, pwm.Top()*uint32(percentOn)/100)
time.Sleep(time.Second / 100)
}
```

Here we fade the LED in. This loop loops 101 times (from 0 to 100 inclusive), setting the output to a percentage of the input. The code `pwm.Top()*uint32(percentOn)/100` is essentially the integer variant of the following formula:

```math
\frac{T}{100} * P
```

Where \\(T\\) is the top value (`pwm.Top()`) and \\(P\\) is the percentage (`percentOn`).

Fading out is very similar. It's almost identical, except it loops from 100 to 0 (inclusive):

```go
// Fade the LED out.
for percentOn := 100; percentOn >= 0; percentOn-- {
pwm.Set(ch, pwm.Top()*uint32(percentOn)/100)
time.Sleep(time.Second / 100)
}
```


<script type="module">
import { setupTour } from '/tour.js';
let code = `
package main

import "machine"
import "time"

func main() {
// Board specific configuration.
led := machine.LED
pwm := machine.TCC0

// Configure the timer/PWM.
// Use the default period, which works well enough for LEDs.
err := pwm.Configure(machine.PWMConfig{})
if err != nil {
println("could not configure:", err.Error())
return
}

// Get the channel for this PWM peripheral.
ch, err := pwm.Channel(led)
if err != nil {
println("could not obtain channel:", err.Error())
return
}

for {
// Fade the LED in.
for percentOn := 0; percentOn <= 100; percentOn++ {
pwm.Set(ch, pwm.Top()*uint32(percentOn)/100)
time.Sleep(time.Second / 100)
}

// Fade the LED out.
for percentOn := 100; percentOn >= 0; percentOn-- {
pwm.Set(ch, pwm.Top()*uint32(percentOn)/100)
time.Sleep(time.Second / 100)
}
}
}
`;
setupTour({
code: code,
boards: {
'arduino-nano33': {},
'circuitplay-bluefruit': {
code: code.replace('machine.TCC0', 'machine.PWM0'),
},
'circuitplay-express': {},
'pico': {
code: code.replace('machine.TCC0', 'machine.PWM4'),
},
}});
</script>
Loading