8. Pulse width modulation


Let’s say we want to provide a component with a constant voltage of, say, 2.5 volts. It would be convenient if we could just tell the Arduino Uno to deliver analog 2.5 volts out of a specified pin. This process, in which you send a digital number that results in an analog voltage coming out is called digital-to-analog conversion, abbreviated as DAC. Arduino Due and Ardunio Zero offer 12-bit and 10-bit DAC, respectively, but there are no built-in DAC capabilities in Arduino Uno. The Uno board can only write digital values. That is, it can only send values that are HIGH (5 V) and LOW (0 V). To deliver voltages out of pins between zero and five volts, it uses pulse width modulation (PWM) as a replacement for DAC.

The idea behind PWM

While we cannot deliver 2.5 volts at any given time using Arduino Uno, we can deliver an average of 2.5 volts over time. Think of a light switch. If you flip it on for a second, then off for a second, them back on for a second, etc., over the course of a minute, you would have, on average, a half-dimmed light.

Now, say we turn digital pin 3 on (HIGH, 5 V) for one millisecond. Then we turn it off (LOW, 0 V) for one millisecond. We keep doing this, and, on average, we deliver 2.5 V.

A signal given by PWM is characterized by three parameters.

  1. The amplitude. For Arduino Uno, this is 5 Volts.

  2. The frequency. This is how rapidly the voltage oscillates.

  3. The duty ratio. This is the fraction of time in one period of the oscillation that the voltage is high.

To see how these parameters affect the voltage levels over time, you can manipulate the interactive plot below. I fixed the amplitude to 5 V, and you can vary the frequency and duty ratio. I intentionally left the Python/JavaScript code to generate the plot in case you are interested in how to build these things using Bokeh. Soon, we will be building interactive dashboards for controlling devices using Bokeh. (But don’t worry, you won’t have to hack and JavaScript; I used JavaScript so you could interact with the plot in the static HTML document.)

[1]:
import numpy as np

import bokeh.plotting
import bokeh.io

bokeh.io.output_notebook()

tmax = 0.025


def pwm_signal(duty_ratio, freq):
    T = 1 / freq
    d = duty_ratio * T
    n_periods = int(tmax / T) + 1
    x = np.concatenate([np.array([0, d, d, T]) + i * T for i in range(n_periods)])
    y = np.concatenate([np.array([5, 5, 0, 0])] * n_periods)

    return x, y


x, y = pwm_signal(0.5, 490)
source = bokeh.models.ColumnDataSource(dict(x=x, y=y))

duty_ratio_slider = bokeh.models.Slider(
    start=0.01, end=0.99, value=0.5, step=0.01, title="duty ratio"
)
freq_slider = bokeh.models.Slider(
    start=100, end=1000, value=490, step=1, title="frequency (Hz)"
)

js_code = """
// Exract data from sliders
let dutyRatio = duty_ratio_slider.value;
let freq = freq_slider.value;
let tmax = 0.025;

// Compute period, duty ratio in units of seconds and number of periods
let T = 1.0 / freq;
let d = dutyRatio * T;
let nPeriods = Math.ceil(tmax / T)

// Update the data in the plot
let x = [];
let y = []
for (let i = 0; i < nPeriods; i++) {
    let iT = i * T;
    x = x.concat([0 + iT, d + iT, d + iT, T + iT]);
    y = y.concat([5, 5, 0, 0]);
}

source.data['x'] = x;
source.data['y'] = y;

source.change.emit();
"""

callback = bokeh.models.CustomJS(
    args=dict(
        source=source, duty_ratio_slider=duty_ratio_slider, freq_slider=freq_slider
    ),
    code=js_code,
)

duty_ratio_slider.js_on_change("value", callback)
freq_slider.js_on_change("value", callback)

p = bokeh.plotting.figure(
    x_axis_label="time (s)",
    y_axis_label="voltage(V)",
    frame_height=150,
    frame_width=500,
    x_range=[0, tmax],
    y_range=[-0.25, 5.25],
    tools="save",
)

p.line(source=source, x="x", y="y", line_width=2)

bokeh.io.show(bokeh.layouts.column(duty_ratio_slider, freq_slider, p))
Loading BokehJS ...

PWM on the Arduino Uno with analogWrite()

Given that the Arduino Uno does not have DAC, why does it have a function analogWrite()? The perhaps ill-named function actually invokes PWM from a digital pin. The analogWrite() function takes two arguments: the pin you are using for PWM and the duty ratio. The duty ratio is not given as a fraction, but rather an int. Although this argument must be given as an int, its minimum value is zero, corresponding to a duty ratio of zero, and its maximum value is 255, corresponding to a duty ratio of one. So, if you wanted a duty ratio of \(d\), you can compute the number you want to pass to analogWrite() as \(255 d\). Here is an example of telling pin 3 to perform PWM with a duty ratio of approximately 0.5.

analogWrite(3, 127);

Note that not all digital pins can do PWM. This is because PWM requires use of the various timers available on Arduino, and a maximum of six pins can be performing PWM at a time. Those that can do PWM are marked with a tilde next to them on the Arduino Uno. Because they use different timers, they have different frequencies.

Pin

Frequency (Hz)

3

490

5

980

6

980

9

490

10

490

11

490

Note that when PWM is invoked on a pin, its use of the time could affect other functions. Specifically, pins 3 and 11 use Timer2, which is also used for tone(). When tone() is being used, it may interfere with PWM on pins 3 and 11.


Follow-along exercise 4: PWM demo

To demonstrate how the Arduino Uno does PWM in lieu of an oscilloscope, you can watch the voltage sent out of a digital pin by reading its output via ADC. The set-up couldn’t be simpler. Connect digital pin 3 to analog input A0.

PWM demo setup

We will do PWM on pin 3 and then immediately read in the signal on pin A0. We will send the reading along the serial connection as fast as we can (which is 2,000,000 baud).

const int sensorPin = A0;
const int outPin = 3;

void setup() {
  pinMode(outPin, OUTPUT);

  Serial.begin(2000000);
  analogWrite(outPin, 127);
}

void loop() {
  Serial.println(analogRead(sensorPin));
}

To visualize the signal, you can either use serial-dashboard or the Serial Plotter of the Arduino IDE. In either case, be sure to set the baud rate to 2,000,000. If you do use serial-dashboard, note that you may fill up RAM as you watch the signal, since serial-dashboard aims to store whatever you record, while the Serial Plotter will dump values not displayed.

Go ahead and upload this sketch and watch the plot to see the actual voltage signals coming out of pin 3.


Many devices, especially mechanical devices like motors, do not react as quickly as the rate at which the voltage switches on and off during PWM. This means that PWM can be an effective means to throttle power to a device.

Even when some devices can react as fast as the PWM, when perception of the device is more important, the device would be perceived as having throttled voltage. This is the case for LEDs. By flipping the input voltage on and off via PWM, an LED can appear brighter or dimmer, depending on the duty ratio. Let’s put that idea to work.


Do-it-yourself exercise 3: Brightness control by PWM

Connect a red LED to a pin capable of PWM. Don’t forget to use a 220 Ω resistor so you do not fry your LED. Write a sketch that gradually brings the brightness of the LED up and down, up and down, up and down, ad infinitum.


Computing environment

[2]:
%load_ext watermark
%watermark -v -p numpy,bokeh,jupyterlab
CPython 3.8.8
IPython 7.21.0

numpy 1.19.2
bokeh 2.3.0
jupyterlab 3.0.11