Creating PWM Output

a timing diagram of a single PWM signal, that indicates the parts of the PWM signal.  those parts being the Total Period, the Pulse itself, and the duty cycle.

What exactly is PWM?

In this installment of How to Program a Raspberry Pi Pico with Rust I’ll give a description of what PWM, when and how to use it, and some code examples in Rust that are tested and usable on the Pico.

What is PWM?

PWM stands for Pulse Width Modulation. The name gives away a bit of what is happening, but let’s break it down and get some deep understanding.

Pulse & Pulse Width

The pulse represents the period of time that a pin of the microcontroller has a logically high state. This typically means that voltage is being provided by the controller for an interval of time. Pulse Width describes the percentage of a given Total Period where the logically high state is true. The bigger the percentage, the wider the pulse width.

Width Modulation

This describes the process of increasing or decreasing the percentage of a period where a pulse is in a logically high state. The act of defining this width is the defining trait of PWM. The term modulation is describing the effect that the ‘pulsing of voltage’ is having on the target device.

Total Period

This describes the entire ‘waveform’ of the pulse. The high point, the low point, and the moment in time when the signal would again go high. This period is what determines the functionality of the applied PWM. Total periods of longer than 15 seconds can be used for controlling a heating element. Extremely short total periods happening tens of thousands of times in a second can be used for very precise voltage regulation.

How to Use PWM with Raspberry Pi Pico?

PWM has a wide variety of use cases, but in almost all of those cases, it’s used to express a magnitude in some form or fashion. So technically it has only one use case, which is to represent some voltage or frequency as an adjustable value. Some examples:

  • A dimmer functionality controls the magnitude of light emitting from a light source.

  • A motor controller uses PWM to control torque or speed magnitude.

  • A heating element is implemented via PWM expressing heat magnitude.

PWM is different from communications protocols because of this. PWM signals essentially represent a single number in any given slice of time, and that number is what the sender / receiver is logically concerned with, not combining it with future or past values.

Communications protocols are patterns that are used to exchange rich sets of data between devices. Temporally speaking with comms-protocols values that come before and after are critical to the protocols working. PWM on its own is not practical for the transmission of complex data structures.

Let’s See A Code Example

This code example is pulled directly from the rp-hal project for increasing and decreasing the intensity of an LED using PWM. This is not the full example code, only what’s relevant for the PWM functionality. You cannot run the code below without the HAL crate. To get a blank project with this HAL crate already included check out my super-blank-raspberry-pico-project.

 

// Init PWMs
let mut pwm_slices = hal::pwm::Slices::new(pac.PWM, &mut pac.RESETS);

// Configure PWM4
let pwm = &mut pwm_slices.pwm4;
pwm.set_ph_correct();
pwm.enable();

// Output channel B on PWM4 to GPIO 25
let channel = &mut pwm.channel_b;
channel.output_to(pins.gpio25);

// Infinite loop, fading LED up and down
loop {
// Ramp brightness up
for i in (LOW..=HIGH).skip(25) {
delay.delay_us(32);
channel.set_duty(i);
}

// Ramp brightness down
for i in (LOW..=HIGH).rev().skip(25) {
delay.delay_us(32);
channel.set_duty(i);
}
delay.delay_ms(2000);
}

 

Now Let’s Understand The Code

Let’s go bit by bit to make sense of this, as there is a huge amount of work happening here, in only a few lines of code.

hal::pwm::Slices::new(…);

This is a module provided by the HAL crate from the rp-rs team.

It allows the user to create a set of references that are vital to being able to interface from the code to the GPIO pins that will generate the PWM signal.

let mut pwm_slices = …new(…)

The pwm_slices variable gives us the ability to select from a set of PWM configurations that are contained within the HAL.

let pwm = &mut pwm_slices.pwm4;

Here we are using the variable from above to select the slice called pwm4 from the HAL. Interestingly there are 8 different PWM slices available, and this one happens to contain GPIO25, which is the IO that has control over the onboard green LED for the regular Raspberry Pi Pico.

Here is what pwm4 looks like:

Pwm4: (4, [Gpio8, Gpio9, Gpio24, Gpio25], 4),

I won’t pretend to understand why 8,9,24, and 25 are grouped together. I also do not understand consecutive pairs would be grouped together either.

I do however know that there is no GP24 on the Raspberry PI, and after a bit of digging learned that GPIO24 is not even accessible on the Pico, and would only be useful if the developer was working with the RP2040 directly. Interesting stuff!

pwm.set_ph_correct();

pwm.enable();

These two commands are telling the PWM slice that it should utilize phase correction. I won’t go into that too much other than to say it’s just to ensure that voltage levels are produced and interpreted correctly.

enable sets these slices up to enable the registers they are referencing to be writable. The registers are likely the memory locations in the RP2040 silicon that drive PWM Output Functionalities, but I can’t say for certain if that’s true.

let channel = &mut pwm.channel_b;
channel.output_to(pins.gpio25);

Apparently, the RP2040 has an A-Side Channel and a B-Side Channel. I’m not sure what’s accomplished by having the two channels, nor am I expert enough to know why the need for different channels would exist.

I can definitely say though, this is where the HAL is assigning output responsibility, and it’s giving the output responsibility to GP25, which is the onboard LED light.

for i in (LOW..=HIGH).skip(25) {
delay.delay_us(32);
channel.set_duty(i);
}
This loop ramps up the intensity of the light. We can see that in the loop we are delaying 32 Micro Seconds before setting the duty cycle of the PWM channel to be an increment of 25 higher. Also, in the loop condition we see (LOW..=HIGH), the HAL crate establishes that they are:
Low = 0
High = 25000

I don’t know why 25K? These posts are making me so aware of how little I know.

Quick Functionality Description


In skipping 25 values each iteration, this loop makes 1000 iterations.

Take 32 microseconds, multiply it times 1000, and you get 32000 microseconds, AKA 32 milliseconds, AKA ~3% of a second.

The loop does the same thing backwards immediately after ramping up. So the LED will ramp up in brightness for ~3 percent of a second, then ramp down for ~3 percent of a second. then there is a 2 second delay prior to carrying out this op again. So for 6 Percent of one second, we see the light brightening then dimming, then nothing for ~1.94 seconds. Cool.

Using Example Code for Practical Applications

What would be a practical application?

It’s likely that the code example above does the common user almost no good for any practical application, as a slowly flashing light doesn’t serve much purpose other than a test.

But that’s ok! We still learned what we needed which was, “How am I able to tell the Raspberry Pi Pico (RP2040) to produce custom PWM signals on a specific pin?”

In order to change the pin producing the PWM signal there are two variables to change:

  • pwm slice assignment must include the target GPIO
  • channel.output_to(pins.gpio25) must have the target GPIO in the parentheses instead of gpio25.


To change the PWM signal itself:

  • channel.set_duty(your_desired_duty_cycle);

And that’s it! It can programmatically determine the duty cycle, link to the voltage output from a potentiometer, or even listen to button press events, increasing and decreasing on each press.

Conclusion

PWM is a great tool for setting the magnitude of some output for a specific device. It’s possible to create control logic that changes the PWM signal during process execution, and even accept feedback from user or system inputs and update the signal based on those factors.

Previous
Previous

What do I Mean By Context?

Next
Next

Understanding Iterators in Rust