Using PWM

If you’ve gone through the blinky example and want to take it to the next level with PWM, or Pulse-Width Modulation, this is the tutorial for you. PWM allows for control of most servos and LED intensity. We’ll be trying out the latter here.

Requirements

Let’s create a directory anywhere on your computer and navigate to it with the terminal. Run the following command in the directory:

go mod init pwm-blinky

Next, create a new file named main.go with the following contents:

package main

import (
	"machine"
	"time"
)

func main() {
    pin := machine.LED
    // This program is specific to the Raspberry Pi Pico.
	pin.Configure(machine.PinConfig{Mode: machine.PinPWM})
	pwm, err := machine.NewPWM(pin)
	if err != nil {
		println(err.Error())
	}
	var period uint64 = 1e9 / 500
	err = pwm.Configure(machine.PWMConfig{Period: period})
	if err != nil {
		println(err.Error())
	}
	ch, err := pwm.Channel(pin)
	if err != nil {
		println(err.Error())
	}
	for { 
		for i := 1; i < 255; i++ {
            // This performs a stylish fade-out blink
			pwm.Set(ch, pwm.Top()/uint32(i))
			time.Sleep(time.Millisecond * 5)
		}
	}
}

There’s quite a bit to unpack in the program above and there are some notable differences between the blinky example. The main take-away here is the two parts to the program: first we setup the PWM peripheral which corresponds to the LED pin, lastly we enter an infinite for loop which blinks the LED with variable intensity given by pwm.Set method.

To transfer this program to the board, we’ll use the flash subcommand. Flashing is the term used for writing a program to a microcontroller. You can do it by cd‘ing to the directory and running this command:

tinygo flash -target=pico

Wait a few seconds and the LED should start to blink with a fade-out effect. This effect can be changed by changing the second argument to pwm.Set.

Explanation

So let’s take a deeper look, how does PWM work behind the curtains? We’ll go through the program line by line.

The first ~10 or so lines are the package declaration and imports, those are explained in finer detail in the blinky example.

Our logic for the program lies entirely within the main function. This is where the magic happens.

led := machine.LED

This declares a new variable named led. If you are not familiar with Go, the := operator is somewhat like auto in C++: it declares a new variable while inferencing the type from the right hand size. In this case, this is the machine.LED constant which has type machine.Pin. Boards which have an on-board LED will have this constant available for use.

pin.Configure(machine.PinConfig{Mode: machine.PinPWM})

This code configures the GPIO pin. Like most peripherals in the machine package, GPIO pins need initialization before they can be used. Maybe the following is clearer if you’re not used to this syntax:

config := machine.PinConfig{
    Mode: machine.PinPWM
}
pin.Configure(config)

The first three lines define a new machine.PinConfig object and sets the Mode field to machine.PinPWM. This means that config.Mode will now equal machine.PinPWM.

pwm, err := machine.NewPWM(pin)

Now things start to look quite different to the blinky example. This code creates a variable which references the PWM peripheral. A peripheral refers to any on-board integrated circuit which interfaces directly with the microprocessor and has I/O capabilities. Some other tasks which different peripherals may manage are communications (I2C, SPI, UART, etc.) and ADC/DAC.

if err != nil {
		println(err.Error())
}

This code checks if there was an error and prints it. It is quite common in Go to come across this pattern though usually the error is handled and not simply printed.

var period uint64 = 1e9 / 500

We declare a new variable to calculate and store the PWM period of 2 milliseconds. By convention periods are unsigned integers which represent a duration in nanoseconds, also written as ns. The formula used above is period = 1 / frequency, where 500 is the desired frequency in hertz (periods per second) and 1e9 is a conversion constant to convert a period in seconds to nanoseconds.

The period of a PWM signal is the time between rising edges.

err = pwm.Configure(machine.PWMConfig{Period: period})

This code configures the PWM peripheral with the desired period. Keep in mind the output period of the PWM is usually not exact and may differ between microcontrollers.

ch, err := pwm.Channel(pin)

This code obtains the channel of the peripheral corresponding to the pin we want output on.

for { 
    // ... 
}

This code defines an endless for loop. All of it’s contents are run on repeat forever. This can usually be found as “while true” in other languages.

for i := 1; i < 255; i++ {
    // ...
}

This defines a loop which:

  • declares and initializes the variable i to 1 (i := 1)
  • Loops until i is equal to 255 or greater (i < 255)
  • Adds 1 to i after each loop (i++)
pwm.Set(ch, pwm.Top()/uint32(i))

The Set method sets the duty cycle of the PWM. We must pass in the channel ch we obtained earlier which sets the PWM on the desired pin.

The second argument to Set is the threshold, which is expressed as a part of the PWM’s top value. There are various details to how PWM works at the lowest detail, to put it simply:

PWM works by having a counter (like the variable i mentioned above) which decides if the pin is “off” or “on”. If this counter is above the threshold then the pin is “off”, if it is below the threshold it is “on”. The counter is incremented automatically continuously until it reaches the top value, when the counter reaches top, it is reset to 0. The fraction of time the pin is “on” vs. “off” is called the duty cycle.

pwm.Top()/uint32(i)

By using the above expression we are setting the threshold at a value between top and top/255, where threshold=top is a 100% dutycycle (always on) and threshold=top/255 is 1/255% dutycycle, or 0.4% (practically off).

time.Sleep(time.Millisecond * 5)

This code stops the program for 5 milliseconds and does nothing for that duration. It is used to slow down the rate at which the LED blinks. If it were not present then we would not notice the fade out effect because it would happen too fast to be perceived!

Final thoughts

Although PWM may have it’s ins and outs which make it a complex topic to explain thoroughly, the program shown has everything one needs to begin harnessing the power of PWM control.

I hope this tutorial was helpful. If you have any questions, feel free to join the #tinygo channel on the Gophers Slack.

Last modified August 8, 2021: tutorials: add PWM guide (746578c)