How to generate PWM signal using Microchip 8-bit AVR microcontrollers

How to generate PWM signal using Microchip 8-bit AVR microcontrollers
NOTE: This article explains how to generate PWM signals with newer Microchip 8-bit AVR microcontrollers released after 2016. These include microcontrollers from the tinyAVR 0/1/2-series or megaAVR 0-series.

Before diving in, it’s assumed that you already understand what PWM (Pulse Width Modulation) is and how it functions. If not, you can check out one of our earlier articles here for a brief explanation.

Although you can generate a PWM signal programmatically on any pin, in this article, we will concentrate on generating PWM signals using timer modules exclusively.

The latest 8-bit AVR microcontrollers usually come with a few 16-bit Timer/Counter modules, often referred to as Type A (TCA) and Type B (TCB). Some microcontrollers in the tinyAVR 1-series and AVR DA family may also have a Timer/Counter Type D module, but we won’t cover it in this article.

These timers offer various functions like Compare Match, frequency generation, event counting, and measurement. However, in this article, we’ll focus solely on their Waveform Generation capability.

We’ll start by explaining how to create a PWM signal using the TCA module in its two different modes: normal and split mode. After that, we’ll take a closer look at generating PWM signals using the TCB module.

Timer/Counter Type A Module (TCA)

The Timer/Counter Type A module is a 16-bit timer. It’s made up of two parts: the Base Counter block and a group of Compare Channel blocks, usually three of them. Figure 1.

Timer/Counter Type A Structural Diagram

Figure 1. Timer/Counter Type A Structural Diagram.

The Base Counter holds the Timer Logic Control part, module control registers, and two important registers: the Period (TCAn.PER) and Counter (TCAn.CNT) registers. These registers are shared with the Compare Channel blocks.

Each compare channel has a 16-bit Compare (TCAn.CMPm) register and two associated Waveform Outputs (WO). In Normal mode, you can use only one WO output, while in Split mode, both WO outputs can be used. To figure out which WO output is connected to a specific physical pin on a particular chip model, you’ll need to check the “I/O Multiplexing and Considerations” section in the datasheet.

The TCA can create PWM signals in two modes: Normal mode and Split mode. In Normal mode, you can have up to 16-bit resolution, and it supports both single-slope and dual-slope operations. However, in Split mode, the TCA behaves like two separate 8-bit timers, and it only supports single-slope operation.

The value in the TCAn.PER register sets the length of the PWM period. A smaller value means a shorter period, resulting in a higher frequency. This PER register value also determines the PWM resolution. The smallest resolution possible is 2 bits (represented by the value 0x0003), and the largest is 16 bits (represented by the value 0xFFFF).

To adjust the pulse width or duty cycle, you can change the value in the TCAn.CMPm register. A higher value results in a longer pulse duration. Next, we’ll explain each mode in detail.

Normal Mode Single-Slope

In Single-Slope operation, the Counter (CNT) register starts from 0 (also called “BOTTOM” in the datasheet) and increases with each clock tick until it reaches the value set in the PER register (referred to as “TOP” in the datasheet). When CNT reaches the PER value, it resets to 0, and this cycle repeats. See Figure 2.
It’s important to note that in this mode, the backward counting from the PER value to 0 is not supported.

TCA Normal Mode Single-Slope operation

Figure 2. TCA Normal Mode Single-Slope operation.

In the non-inverted mode, when the CNT register value is less than the CMP register value, the signal on the WOn output is high. When the CNT value becomes equal to or greater than the CMPn value, the signal on the WOn output goes low.

You can calculate the PWM signal’s frequency using this formula:
PWM frequency = Peripheral Clock / (N * (TOP + 1))

Where, N represents the prescaler divider, which you can set in the TCA.CTRLA register. It can take values like 1, 2, 4, 8, 16, 64, 256, and 1024.
The TOP is the value you set in the PER register.

Now, let’s take a look at the code example below, which configures the TCA timer to generate three PWM signals on an ATTiny202 microcontroller. Since all three compare channels use the same PER register, the frequency of all three PWM signals will be the same. However, you can customize the duty cycle for each individual signal.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <avr/io.h>

int main(void)
{
	PORTA.DIR |= PIN1_bm | PIN2_bm | PIN3_bm;
	TCA0.SINGLE.PERBUF = 0xFF;
	TCA0.SINGLE.CMP0BUF = 0x80;
	TCA0.SINGLE.CMP1BUF = 0x40;
	TCA0.SINGLE.CMP2BUF = 0x20;
	TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP0EN_bm | TCA_SINGLE_CMP1EN_bm | TCA_SINGLE_CMP2EN_bm | TCA_SINGLE_WGMODE_SINGLESLOPE_gc;
	TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc | TCA_SINGLE_ENABLE_bm;
	for(;;);
}
  • Line 5: Sets the direction as output on corresponding pins.
  • Line 6: Sets the period duration.
  • Lines 7- 9: Set the duty cycle for each PWM signal.
  • Line 10: Enables all three compare channels and configures the timer for Single-slope Waveform Generation mode.
  • Line 11: Sets the prescaler divider to 1 and enables the TCA module.

You might notice that we write values to the PER and CMPn registers through their buffer registers, PERBUF and CMPnBUF. This is done to prevent unexpected changes in the waveform. The PER and CMPn registers get updated with the values from their buffers whenever there’s a new value in the buffer, and the CNT register value is reset to 0.

The default peripheral frequency is 3.3 MHz. This is because the default microcontroller’s clock frequency is 20 MHz, and it is divided by 6 before reaching the peripherals. If you set the PER register to a value of 0x0003 (the smallest 2-bit resolution), you can generate a PWM signal with a frequency of 825 KHz. However, keep in mind that you’ll only have the ability to change the duty cycle in just 4 different values.

If you want to adjust or turn off the microcontroller’s clock prescaler, you’ll need to use the ccp_write_io() function found in the avr/cpufunc.h file. The “ccp” in the function name stands for Configuration Change Protection. For instance, if you want to disable the clock prescaler and achieve up to 5 MHz with the lowest resolution, you can do it like this:
ccp_write_io((uint8_t *)&CLKCTRL.MCLKCTRLB, 0);

A few things to keep in mind:

  • When you set the CMPn register value to 0, you’ll get a continuous low signal on the output pin. In the previous generation of AVR microcontrollers, you would usually encounter a spike in this situation.
  • If you set the CMPn value register to a value higher than the PER register value, you’ll get a continuous high signal on the output pin.
  • If you want an inverted signal on the output pin, you can set the INVEN bit in the PORTx.PINnCTRL control register.

Normal Mode Dual-Slope

In Dual-Slope operation, the CNT value starts from 0, increases to the PER register value, and then decreases back to 0, and this cycle repeats, Figure 3.

TCA Normal Mode Dual-Slope operation

Figure 3. TCA Normal Mode Dual-Slope operation.

You can calculate the PWM signal’s frequency in this mode using the formula:
PWM Frequency = Peripheral Clock / (2 * N * TOP)

Since the counter value goes up and down in two periods, we get half of the maximum frequency achievable in Single-Slope operation.

Here’s the same code as before, with one change: in line 10, we set the timer module to Dual-Slope Waveform Generation mode.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <avr/io.h>

int main(void)
{
	PORTA.DIR |= PIN1_bm | PIN2_bm | PIN3_bm;
	TCA0.SINGLE.PERBUF = 0xFF;
	TCA0.SINGLE.CMP0BUF = 0x80;
	TCA0.SINGLE.CMP1BUF = 0x40;
	TCA0.SINGLE.CMP2BUF = 0x20;
	TCA0.SINGLE.CTRLB = TCA_SINGLE_CMP0EN_bm | TCA_SINGLE_CMP1EN_bm | TCA_SINGLE_CMP2EN_bm | TCA_SINGLE_WGMODE_DSBOTH_gc;
	TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV1_gc | TCA_SINGLE_ENABLE_bm;
	for(;;);
}

Split Mode

In Split Mode, the TCA works as two separate 8-bit timers. This means the LOW and HIGH byte registers of the PER, CNT, and CMPn registers are treated as separate registers. In other words, the LCNT and HCNT registers that make up the CNT register will be updated independently.

In this mode, the minimum resolution is also 2 bits, but the maximum resolution is 8 bits. Because the CMPn registers are split into two, this allows to generate up to 6 PWM signals using a single TCA module.

The HPER and LPER registers can have different values, allowing you to have two different PWM frequencies.

In Split Mode, only Single-Slope Down-Count is supported. This means the CNT register value will decrease from PER to 0 and then reset back to the PER register value, Figure 4.

TCA Split Mode 8-bit PWM operation

Figure 4. TCA Split Mode 8-bit PWM operation.

It’s important to note that buffer registers like PERBUF and CMPnBUF cannot be used in this mode. According to the datasheet, you need to set the timer in Split Mode before configuring the PER registers.

The PWM frequency is calculated using the same formula as in the Normal Mode Single-Slope section.

The code below demonstrates how to generate four PWM signals using TCA in Split Mode on the ATTiny202 microcontroller. Why only four PWM signals?
The ATTiny202 chip has a limited number of pins, so you can’t generate all six PWM signals at once. You can generate three signals using the lower byte registers and one PWM signal using the higher byte register. However, there’s a catch: WO3 output will override WO0 output. To use WO0 as well, you’ll need to choose an alternative pin using a multiplexer. Check the “I/O Multiplexing and Considerations” section in the datasheet for details.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <avr/io.h>

int main(void)
{
	PORTA.DIR |= PIN1_bm | PIN2_bm | PIN3_bm | PIN7_bm;
	PORTMUX.CTRLC = PORTMUX_TCA00_bm;
	TCA0.SPLIT.CTRLD = TCA_SPLIT_SPLITM_bm; // <== IMPORTANT - this needs to be set first before PER and CMP registers
	TCA0.SPLIT.LPER = 0xff;
	TCA0.SPLIT.HPER = 0x80;
	TCA0.SPLIT.LCMP0 = 0x80;
	TCA0.SPLIT.HCMP0 = 0x40;
	TCA0.SPLIT.LCMP1 = 0x40;
	TCA0.SPLIT.LCMP2 = 0x20;
	TCA0.SPLIT.CTRLB = TCA_SPLIT_LCMP0EN_bm | TCA_SPLIT_LCMP1EN_bm | TCA_SPLIT_LCMP2EN_bm | TCA_SPLIT_HCMP0EN_bm;
	TCA0.SPLIT.CTRLA = TCA_SPLIT_CLKSEL_DIV1_gc | TCA_SPLIT_ENABLE_bm;
	for(;;);
}
  • Line 5: Sets the direction as output on corresponding pins.
  • Line 6: Sets an alternative pin for WO0 output since it conflicts with WO3 output.
  • Line 7: Enables the Split Mode (IMPORTANT: do this before configuring PER and CMP registers).
  • Lines 8-9: Sets the period duration.
  • Lines 10-13: Sets the duty cycles for the signals.
  • Line 14: Enables the compare channels.
  • Line 15: Sets the prescaler divider and enables the TCA module.

Here’s the code for the ATTiny824 microcontroller that generates six PWM signals on physical pins 2, 3, 7, 8, 9, and 13:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <avr/io.h>

int main(void)
{
PORTB.DIR |= PIN0_bm | PIN1_bm | PIN2_bm;
	PORTA.DIR |= PIN3_bm | PIN4_bm | PIN5_bm;
	TCA0.SPLIT.CTRLD = TCA_SPLIT_SPLITM_bm;
	TCA0.SPLIT.LPER = 0xff;
	TCA0.SPLIT.HPER = 0x80;
	TCA0.SPLIT.LCMP0 = 0x80;
	TCA0.SPLIT.HCMP0 = 0x40;
	TCA0.SPLIT.LCMP1 = 0x40;
	TCA0.SPLIT.HCMP1 = 0x20;
	TCA0.SPLIT.LCMP2 = 0x20;
	TCA0.SPLIT.HCMP2 = 0x10;
	TCA0.SPLIT.CTRLB = TCA_SPLIT_LCMP0EN_bm | TCA_SPLIT_LCMP1EN_bm | TCA_SPLIT_LCMP2EN_bm | TCA_SPLIT_HCMP0EN_bm | TCA_SPLIT_HCMP1EN_bm | TCA_SPLIT_HCMP2EN_bm;
	TCA0.SPLIT.CTRLA = TCA_SPLIT_CLKSEL_DIV1_gc | TCA_SPLIT_ENABLE_bm;
	for(;;);
}

You can notice that the changes from the previous code are minimal:

  • Lines 5 and 6: Sets the corresponding pins on PORTB and PORTA as output.
  • Lines 14 and 15: Sets the duty cycle for the remaining two outputs.
  • Line 16: Enables the additional two compare channels.

This code should work for another microcontroller like the ATMega1608 with minimal changes. Just ensure you set the directions of the ports on the appropriate pins as they may vary from model to model.

Timer/Counter Type B Module (TCB)

The TCB structure is different from TCA. It doesn’t have multiple compare channels or the Period (PER) register. Instead, it has only the Compare/Capture register (CCMP), Figure 5. The TCB module is mainly designed for measurements, such as frequency or PWM measurement, time-out checks, etc. However, it does support PWM generation, which we’ll discuss here.

Timer/Counter Type B Structural Diagram

Figure 5. Timer/Counter Type B Structural Diagram.

Even though TCB is a 16-bit timer, it can generate a PWM signal with a maximum resolution of 8 bits. In Waveform Generation mode, it only supports Single-Slope operation, and you can generate only one PWM signal per TCB module.

8-bit PWM Mode

Since the TCB module lacks the PER register, you specify the period duration using the CCMPL register. To configure the duty cycle, you use the CCMPH register.

The TCB only supports Single-Slope operation, where the CNT register value goes from 0 to the CCMPL value and then resets back to 0. When the CNT value is less than the CCMPH register value, the signal on the output pin is high, and when it’s equal to or greater than the CCMPH value, the output signal is low - Figure 6.

TCB 8-bit PWM operation

Figure 6. TCB 8-bit PWM operation.

You can calculate the PWM frequency using the same formula as in the Normal Mode Single-Slope section.

Here’s an example of how to configure the TCB0 timer on an ATTiny202 microcontroller to generate a PWM signal:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <avr/io.h>

int main(void)
{
	PORTA.DIR |= PIN6_bm;
	TCB0.CCMPL = 0x80;
	TCB0.CCMPH = 0x40;
	TCB0.CTRLB = TCB_CCMPEN_bm | TCB_CNTMODE_PWM8_gc;
	TCB0.CTRLA = TCB_ENABLE_bm;
	
	for(;;);
}
  • Line 5: Sets the appropriate pin direction as an output.
  • Line 6: Specifies the period duration.
  • Line 7: Sets the duty cycle duration.
  • Line 8: Enables the waveform output by setting the CCMPEN bit and configures the module in 8-bit PWM mode.
  • Line 9: Enables the TCB module.

The TCB module offers limited choices for the prescaler divider, only 1 and 2. Alternatively, you can use a TCA prescaled clock, but you’ll need to configure and enable the TCA module for this purpose. Here’s an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <avr/io.h>

int main(void)
{
	TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV64_gc | TCA_SINGLE_ENABLE_bm;
	PORTA.DIR |= PIN6_bm;
	TCB0.CCMPL = 0x80;
	TCB0.CCMPH = 0x40;
	TCB0.CTRLB = TCB_CCMPEN_bm | TCB_CNTMODE_PWM8_gc;
	TCB0.CTRLA = TCB_ENABLE_bm | TCB_CLKSEL_CLKTCA_gc;
	
	for(;;);
}
  • Line 5: Configures and enables the TCA with a prescaler of 64.
  • Line 10: Enables the TCB timer and sets the clock source from TCA.

For some AVR chip models, certain WO (Waveform Output) outputs from TCA (Timer/Counter Type A) can overlap with those from TCB (Timer/Counter Type B). If you plan to use both timers for PWM generation, you’ll need to set alternative pins in the multiplexer. You can find details about alternative pin locations in the “I/O Multiplexing and Considerations” section and learn how to configure these alternative output positions in the “PORTMUX - Port Multiplexer” chapter of the datasheet.

One great thing about these timer modules is that you can often reuse the same timer configuration code with minimal or no modifications when porting between different AVR chip families. The primary change you might need to make is to update the output direction configuration because the WO outputs could be connected to different physical pins on various chip models.