How to generate PWM signal with Arduino board

How to generate PWM signal with Arduino board

In this article, we discussed what a PWM (Pulse Width Modulation) signal is and how to generate it using Raspberry Pi. In this article, we’ll talk about how to generate PWM signals on the Arduino Uno board.

We’ll start with a simple example of generating a PWM signal using software and proceed to use the built-in function analogWrite(). Afterward, we will delve into more advanced topics, exploring how to generate PWM signals by configuring timer registers.

Generating PWM using software

Below is a code example showing how to generate a PWM signal on pin 3 using software only. By connecting an LED to the pin (with an appropriate current-limiting resistor), you can observe the LED gradually turning on and off.

One of the advantages of software-generated PWM is the flexibility to produce the PWM signal on any digital pin of the Arduino.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#define PERIOD 10000 // period in microseconds
#define PWM_PIN 3

const int duty_unit = PERIOD / 100;

void setup() {
  pinMode(PWM_PIN, OUTPUT);
}

void generatePulse(int duty_cycle) {
  digitalWrite(PWM_PIN, HIGH);
  delayMicroseconds(duty_cycle * duty_unit);
  digitalWrite(PWM_PIN, LOW);
  delayMicroseconds(PERIOD - (duty_cycle * duty_unit));
}

void loop() {
  
  for (int i = 0; i < 100; i++) {
    generatePulse(i);
  }

  for (int i = 100; i > -1; i--) {
    generatePulse(i);
  }

}
  • Line 1: The PERIOD constant defines the duration of one complete PWM cycle. By adjusting this value, you can change the frequency of the PWM signal.
  • Line 2: The PWM_PIN constant specifies the pin on which the PWM signal should be generated.
  • Line 4: The duty_unit variable represents the duration of one duty cycle unit. The PERIOD is divided by 100 to obtain 1% of the period, which helps achieve a 1% duty cycle resolution.
  • Line 7: The setup() function sets the specified pin as an output.
  • Line 10: The generatePulse() function generates one pulse per period with a specified duty cycle. It sets the PWM pin high, delays for the duration of the duty cycle, then sets the PWM pin low, and then waits for the remaining cycle duration.
  • Line 17: Inside the loop() function, there are two loops. The first loop increases the duty cycle from 0% to 100%, while the second loop decreases it from 100% to 0%.

This approach has a couple of drawbacks. First, it consumes some processing power, as this requires the microcontroller to handle the timing and control of the pulses. Second, any interrupts or additional logic that runs alongside the PWM generation can affect the duration of the periods and duty cycles, leading to variations in the frequency and pulse width.

PWM using analogWrite() function

The Arduino built-in library provides an analogWrite() function that allows us to generate PWM signals on specific pins. The analogWrite() function takes two parameters: the pin on which to generate PWM and the duty cycle, which can range from 0 to 255.

Below is an example code that achieves the same functionality as the previous example but utilizes the analogWrite() function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#define PWM_PIN 3

void setup() {
  pinMode(PWM_PIN, OUTPUT);
}

void loop() {
  for (int i = 0; i < 255; i++) {
    analogWrite(PWM_PIN, i);
    delay(10);
  }

  for (int i = 255; i > -1; i--) {
    analogWrite(PWM_PIN, i);
    delay(10);
  }
}
  • Line 1: The PWM_PIN constant defines the pin on which PWM can be generated. On Arduino Uno with the ATMega328P microcontroller, valid pins for PWM generation are 3, 5, 6, 9, 10, and 11.
  • Line 4: The setup() function sets the specified pin as an output.
  • Line 7: Inside the loop() function, we have two for-loops.
  • Line 8: The first for loop increases the duty cycle from 0 to 255 at intervals of 10 milliseconds.
  • Line 13: The second for loop decreases the duty cycle from 255 to 0 at intervals of 10 milliseconds and the cycle repeats again from line 8.

The analogWrite() function utilizes hardware-based PWM generation through the microcontroller’s Timer modules. This means you can add more code without impacting the PWM frequency. However, the `analogWrite() function doesn’t allow changing the PWM frequency.

For Timer0 (pins 3 and 11) and Timer2 (pins 9 and 10) modules, the frequency is set to 500 Hz (measured around 487Hz).

For Timer1 (pins 5 and 6), the frequency is set to 1 kHz (measured around 970 Hz).

To achieve different PWM frequencies and resolutions, we need to configure the timer modules directly. In the following section, we will discuss how to do this in detail.

PWM Using AVR Timers

The Arduino Uno is equipped with an ATMega328P chip, which features three timers: Timer0, Timer1, and Timer2. Among these, Timer0 and Timer2 are 8-bit timers, while Timer1 is a 16-bit timer.

Each timer consists of multiple registers and supports various functionalities. In this article, we’ll focus on exploring the register configurations that enable us to generate PWM signals. The register names are similar for all three timers. However, they may differ in the number of bits they contain and how they are configured. Please refer to the datasheet for appropriate configuration for each timer.

Each timer has two Timer/Counter Control Registers, namely TCCRnA and TCCRnB (where ’n’ represents the timer number). These registers include a set of control bits that allow us to configure various aspects of the timers, such as setting the signal on output pins, selecting the waveform generation mode, and configuring the prescaler factor, see Figure 1.

Timer/Counter Control Registers

Figure 1. Timer/Counter Control Registers.
a) Bit configuration of the TCCRnA control register common for all timers.
b) Bit configuration of the TCCRnB control register common for Timer0 and Timer2.
c) Bit configuration of the TCCR1B control register of the Timer1.

The Compare Output Mode bits (COMnAx and COMnBx) are part of the TCCRnA register. These bits are used for enabling PWM generation on a specific output (OCnA and OCnB) associated with the timer.

The Waveform Generation Mode bits WGMnx, found in both the TCCRnA and TCCRnB control registers, are used to configure the PWM mode. Each timer offers various PWM modes, like Fast PWM, Phase-Correct PWM and different TOP configuration for these. Please consult the microcontroller’s datasheet to learn more about each mode.

In the TCCRnB register, there are three Clock Select bits (CSnx) responsible for specifying the prescaler factor of the clock. By selecting an appropriate prescaler value, you can adjust the timer’s counting speed and, consequently, the PWM frequency. The supported prescaler factor values common to all timers are: 1, 8, 64, 256, and 1024. However, Timer2 supports an additional prescaler value - 32.

Warning: Changing Timer0 settings will affect Arduino timing functions like delay(), delayMicroseconds(), micros, and millis(). If you modify Timer0 configuration, be aware that it may cause unexpected issues or inaccurate timekeeping in your Arduino sketch.

Each timer has two Output Compare Registers: OCRnA and OCRnB, Figure 2 a) and b). By changing the value in these registers, you can adjust the duration of the PWM pulses generated on outputs OCnA and OCnB. Additionally, there is an Input Capture Register (ICRn) that can also be used to adjust pulse width, but we won’t cover it in this article.

The timer’s count value is stored and incremented in the Timer/Counter register (TCNTn), Figure 2 c).

Timer Counter and Output Compare Registers

Figure 2. 8-bit Timer Counter and Output Compare Registers.

The microcontroller and its data bus work with 8-bit data. However, for Timer1, the registers TCNT1, ICR1, OCR1A, and OCR1B are 16 bits, Figure 3. To handle 16-bit values with an 8-bit data bus, the registers are split into two 8-bit parts. For example, TCNT1 has two 8-bit registers: TCNT1L (low byte) and TCNT1H (high byte), Figure 3 c).

16-bit Timer Counter and Output Compare Registers

Figure 3. 16-bit Timer Counter and Output Compare Registers.

Each Output Compare Register is associated with a particular timer output, which, in turn, is linked to a specific pin on the Arduino board. The table below shows the mapping between the Arduino digital pin and the respective timer output.

Timer Timer Output Output Compare Register Arduino Pin
Timer0 OC0A OCR0A 6
OC0B OCR0B 5
Timer1 OC1A OCR1A 9
OC1B OCR1B 10
Timer2 OC2A OCR2A 11
OC2B OCR2B 3

Each timer on Arduino supports two PWM modes: Fast PWM and Correct PWM. Let’s describe each mode and provide an example for better understanding.

Fast PWM mode

According to the datasheet, Fast PWM is a single slope operation. The timer’s counter value in TCNTn register increments with each clock pulse, starting from 0 and going up to the specified TOP value. Once it reaches the TOP value, the counter resets back to 0, and the cycle starts again, see Figure 4.

For 8-bit timers, the default TOP value is 255, but it can be configured using the OCRnA or ICRn registers. As for Timer1 which is 16-bit timer, it offers several TOP value options like 255 (8-bit resolution), 511 (9-bit resolution), or 1023 (10-bit resolution), or a value set in the OCR1A or ICR1 registers, which can go up to 65535 (16-bit resolution). The number of bits for the TOP value represents the resolution of the PWM signal. A higher number of bits means higher resolution. The minimum resolution allowed is 2 bits, which corresponds to a TOP value of 3.

In non-inverting mode, when the timer counter value is less than the OCRnA or OCRnB register value, the signal value on the output pin will be HIGH. Conversely, when the timer counter value is greater than the output compare register’s value, the signal value on the output pin will be LOW, see Figure 4.

Fast PWM generation histogram

Figure 4. Fast PWM generation histogram.

The PWM signal frequency for Fast PWM can be calculated using the formula:
PWM Frequency = Fckl / (N * (TOP + 1))

Where:
Fclk - is the frequency of the microcontroller’s clock, usually 16MHz.
N - is the prescaler factor set by Clock Select bits (CSnx)

Keep in mind that if you set the Output Compare Register value to 0, you’ll still get a short pulse spike on the output with a pulse width of 1/(TOP+1). The maximum frequency the microcontroller can operate at is 16MHz. If we use the prescaler factor as 1 (no prescaling), we can achieve the maximum PWM frequency of about 62.5KHz.
Frequency = Fckl / (N * (TOP + 1)) = 16000000 Hz / (1 * (255 + 1)) = 62500 Hz

You have the option to specify the TOP value in the OCRnA register, which allows you to change the frequency of the PWM signal on the OCnB output. This way you can increase the frequency further, with the trade-off of reducing the PWM resolution. For instance, if you set the OCRnA register value to 9 (4-bit resolution) and use a prescaler factor of 1, you can achieve a maximum frequency of 1.6MHz. However, in this case, you will only have 10 possible values for the duty cycle, ranging from 0 to 9.

Keep in mind that on the OCnA output, you will get a PWM signal with a fixed 50% duty cycle, which cannot be changed. Additionally, the frequency on the OCnA output is double the frequency on the OCnB output, Figure 5.

Fast PWM generation histogram with TOP defined in OCRnA

Figure 5. Fast PWM generation histogram with TOP defined in OCRnA.

The following example demonstrates how to generate a PWM signal using Timer2 in Fast PWM mode on Arduino pins 11 and 3. If you wish to use other timers, you can follow a similar approach by updating the register names and setting the relevant bits. For more detailed information, consult the microcontroller datasheet.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void setup() {
  pinMode(11, OUTPUT);
  pinMode(3, OUTPUT);
  TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
  TCCR2B = _BV(CS20);
  OCR2A = 100;
  OCR2B = 50;
}

void loop() {
}
  • Lines 2 and 3: We set Arduino pins 11 and 3 as output.
  • Line 4: We configure the TCCR2A register. We set Compare Output Mode bits (COM2A1 and COM2B1) to enable PWM output on both pins. Additionally, we set Waveform Generation Mode bits (WGM21 and WGM20) to configure Fast PWM mode with a TOP value of 255.
  • Line 5: We set the Clock Select bit (CS20) in the TCCR2B register, which sets the prescaler factor to 1, allowing the PWM signal to operate at the maximum frequency.
  • Lines 6 and 7: We set the values in the Output Compare Registers OCR2A and OCR2B, which determine the width of the PWM pulse. The greater the value, the wider the pulse.

If you wish to use the OCR2A register value as the top value, you’ll need to modify lines 4 and 5 as demonstrated below. Set the COM2A0 bit in the TCCR2A register, and the WGM22 bit in the TCCR2B register.

  TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM21) | _BV(WGM20);
  TCCR2B = _BV(WGM22) | _BV(CS20);

Phase Correct PWM mode

The Phase Correct PWM is a dual slope operation, which means that the timer counter value in the TCNTn register is incremented from 0 to TOP, and then decremented from TOP back to 0 within one PWM period. This dual slope behavior generates a PWM signal with a frequency twice lower than the Fast PWM mode, but it provides a double higher resolution, Figure 6.

The TOP value in Phase Correct PWM is configurable in the same way as for Fast PWM mode.

In non-inverting mode operation, the initial value on the output pin is set to HIGH. When the timer counter value becomes greater than the output compare register value, the output pin’s value will switch to LOW. Once the counter reaches the TOP value and starts decreasing below the output compare value, the output pin will switch back to HIGH. See Figure 6.

Phase Correct PWM generation histogram

Figure 6. Phase Correct PWM generation histogram.

The PWM signal frequency for Phase Correct PWM can be calculated using the formula:
PWM Frequency = Fclk / (N * 2 * TOP)

Where:
Fclk - is the frequency of the microcontroller’s clock, usually 16MHz.
N - is the prescaler factor set by Clock Select bits (CSnx)

The code below illustrates how to generate a PWM signal in Phase Correct PWM mode using Timer2 on Arduino pins 11 and 3.

1
2
3
4
5
6
7
8
void setup() {
  pinMode(11, OUTPUT);
  pinMode(3, OUTPUT);
  TCCR2A = _BV(COM2A1) | _BV(COM2B1) | _BV(WGM20);
  TCCR2B = _BV(CS20);
  OCR2A = 100;
  OCR2B = 50;
}
  • Lines 2 and 3: We set Arduino pins 11 and 3 as output.
  • Line 4: We configure the TCCR2A register by setting Compare Output Mode bits (COM2A1 and COM2B1) to enable PWM output on both pins. Additionally, we set the Waveform Generation Mode bit WGM20 to configure Timer2 to work in Phase Correct PWM mode with a TOP value of 255.
  • Line 5: We set the Clock Select bit CS20 in the TCCR2B register, which sets the prescaler factor to 1, enabling the PWM signal to operate at the maximum frequency.
  • Lines 6 and 7: We set the values in the Output Compare Registers OCR2A and OCR2B, which determine the width of the PWM pulse. The greater the value, the wider the pulse.

As in case of Fast PWM mode, if you want to use the OCR2A register value as a top value, you need to modify lines 4 and 5, Set the COM2A0 bit in the TCCR2A register, and the WGM22 bit in the TCCR2B register.

  TCCR2A = _BV(COM2A0) | _BV(COM2B1) | _BV(WGM20);
  TCCR2B = _BV(WGM22) | _BV(CS20);

This will allow to adjust the frequency on OCnB output, but the frequency on the OCnA output is double the frequency on the OCnB output, with a fixed 50% duty cycle, which cannot be changed. See Figure 7.

Phase Correct PWM generation histogram with TOP defined in OCRnA

Figure 7. Phase Correct PWM generation histogram with TOP defined in OCRnA.

You might have observed that the Fast PWM frequency formula includes a (TOP + 1), while the Phase Correct PWM frequency formula does not have the (+ 1) in it. If you’re curious, you can explore the reasoning behind this difference here.

In summary, PWM signals can be generated using either software or hardware (timer modules).
Software PWM can be applied to any digital pin, but it consumes CPU power and may have less accurate frequency and duty cycle when additional logic is added.
Hardware PWM can be generated using the analogWrite() function or by configuring timer modules directly. With analogWrite(), you can adjust the duty cycle, but not the frequency.
Configuring the timer register directly offers more flexibility, enabling you to change both the frequency and the PWM resolution.
Increasing the resolution lowers the frequency, while higher resolution decreases the frequency.