Skip to main content

Lab 3: Timers, Pulse Width Modulation (PWM)

Helpful chapters from the ATmega324P Datasheet

    1. Pin Configurations – section 1.1, page 15
    1. 8-bit Timer/Counter0 with PWM – section 16.9, page 140
    1. 16-bit Timer/Counter1 with PWM – section 17.14, page 173
    1. 8-bit Timer/Counter2 with PWM and asynchronous operation – section 19.11, page 202

1. Pulse Width Modulation

PWM (Pulse Width Modulation) is a technique used to control the voltage applied to an electronic device by quickly switching it between ON and OFF states.

This rapid switching results in an average voltage determined by the time the signal stays ON versus the total cycle time — this ratio is called the duty cycle.

PWM Principle

PWM allows digital control over analog signals — for instance:

  • Dimming an LED
  • Changing the color of RGB LEDs
  • Setting the pitch of a buzzer
  • Adjusting the speed of a motor

PWM LED Control


1.1. Operating Principle

The duty cycle is expressed as a percentage of the ON time relative to the total cycle:

D[%]=tonton+toff100=pulse_widthperiod100D[\%] = \frac{t_{on}}{t_{on} + t_{off}} \cdot 100 = \frac{pulse\_width}{period} \cdot 100

The average voltage received by a device is:

Vˉ=DVcc\bar{V} = D \cdot V_{cc}

PWM waveforms with different duty cycles

PWM signals are typically generated by digital circuits and microcontrollers. The ATmega324 uses a counter that resets periodically and compares its value with a reference (OCRn). When the counter exceeds the reference value, the PWM output toggles its state.

🛠️ PWM via software?

PWM can also be implemented in software using methods like:

  • Bit-banging
  • Timers with ISR

But for high-frequency PWM (e.g., kHz range for motors), hardware PWM is far more efficient, avoiding CPU overhead from frequent interrupts.


2. PWM on AVR Microcontrollers

In the previous lab, we saw that the ATmega324 has three timers:

  • Timer0 (8-bit)
  • Timer1 (16-bit)
  • Timer2 (8-bit)

Each timer can be configured via the control registers TCCRnA and TCCRnB, using the WGMnx bits to select the mode:

  • Normal
  • CTC (Clear Timer on Compare Match)
  • Fast PWM (used today!)
  • Phase Correct PWM, etc.

Each timer has two output compare channels: OCnA and OCnB. These are tied to specific physical pins. Check the Pin Configurations chapter of the datasheet for exact mappings:

PWM Output Pins on ATmega324P

Today we’ll focus on Fast PWM, a common mode suitable for general applications like LED dimming and motor speed control.

Fast PWM

In Fast PWM mode, the timer counts only on the rising edge of the clock signal. The duty cycle changes take effect immediately, but the waveform is not centered — a shift or glitch may appear when changing the duty cycle significantly.

Several Fast PWM modes are available for different timers. For Timer1, you have:

  • Fast PWM, 8-bit: TOP = 0x00FF
  • Fast PWM, 9-bit: TOP = 0x01FF
  • Fast PWM, 10-bit: TOP = 0x03FF
  • Fast PWM with TOP in ICR
  • Fast PWM with TOP in OCRnA
💡

In this lab, we’ll use only Fast PWM 8-bit mode. Refer to the datasheet for other timer-specific modes and features.


Suppose Timer1 is configured in Fast PWM mode. This mode has a fixed frequency and allows the duty cycle (threshold) to be modified during execution.

  • With mode 10 for COM1A1:COM1A0, the signal on pin OC1A stays HIGH during counting up to the threshold, and LOW afterwards until the end of the cycle.
  • To get a duty cycle of x%, set:
    OCR1A = x * TOP / 100
    where TOP is 255 (8-bit), 511 (9-bit), or 1023 (10-bit), depending on configuration.

Example: Timer1 in Fast PWM 8-bit, non-inverting mode with 1024 prescaler

PWM frequency ≈ 12 MHz / 1024 / 255 ≈ 45 Hz

/* OC1A is PD5, must be configured as OUTPUT */
DDRB |= (1 << PD5);

/* Select Fast PWM 8-bit mode: WGM[3:0] = 0b0101 */
/* WGM10 and WGM11 -> TCCR1A; WGM12 and WGM13 -> TCCR1B */
TCCR1A = (1 << WGM10);
TCCR1B = (1 << WGM12);

/* Set non-inverting mode on OC1A: COM1A[1:0] = 0b10 */
TCCR1A |= (1 << COM1A1);

/* Set prescaler to 1024: CS1[2:0] = 0b101 */
TCCR1B |= (1 << CS12) | (1 << CS10);

/* Set 50% duty cycle (TOP = 255, so OCR1A = 127) */
OCR1A = 127;

Phase Correct PWM

Although we won’t use this in the lab, it’s useful to understand how Phase Correct PWM differs from Fast PWM — it offers better accuracy, often required for BLDC motors or audio.

Key difference: the counter counts up from BOTTOM to TOP, then back down to BOTTOM.

  • Output is set based on comparison with OCRnx, just like Fast PWM.
  • However, OCRnx remains constant during the full cycle, resulting in stable ON durations and cleaner signals.
  • This avoids frequency shifts (jitter) — important for motors, which can lose efficiency due to inconsistent PWM.

Phase Correct PWM Timing


Comparison: Fast PWM vs Phase Correct PWM

Fast PWM (left) vs Phase Correct PWM (right)

3. Exercises

The goal of these exercises is to control the color of an RGB LED using PWM. You can produce any color by adjusting the brightness of each diode (Red, Green, Blue) independently!

The RGB LED pins are connected as follows:

  • Red: pin PD5, function OC1A (linked to Timer1)
  • Green: pin PD7, function OC2A (linked to Timer2)
  • Blue: pin PB3, function OC0A (linked to Timer0)
Reminder

The RGB LED is wired with common anode / "active-low" configuration — the LED turns ON when the pin is set to LOW, and OFF when set to HIGH.


Task 0

  1. Download and run the starter project ZIP.
    • What do you observe on the board?
    • Check the serial monitor — do you remember how timing was handled in the previous lab?
    • How does the LED behave? Which timer is used and in what mode?
    • Where is the intensity updated? What input does the formula use, and where is the output applied?

Task 1

  1. Let’s make the blue LED behave similarly.
    • Use Timer0 (OC0A on PB3) in Fast PWM mode.
    • Be sure to check Section 12.9 in the ATmega324P Datasheet for proper register usage — Timer0's registers differ from Timer1's!
    • Make the blue LED pulse faster than red, and in parallel.
    • What do you notice?

Task 2

  1. Time to turn on the green LED!
    • Challenge: PD7 (OC2A) is used by Timer2, which is already used for millis() system ticks...
    • Solution: We’ll generate PWM manually using interrupts:
      • Timer2 counts from 0 to 188 and triggers an interrupt via COMPA (to increment systicks).
      • Timer2’s second comparator COMPB triggers an interrupt at a value between 0–188.
      • In COMPA ISR: turn ON green LED → set GPIO LOW.
      • In COMPB ISR: turn OFF green LED → set GPIO HIGH.
      • Duty cycle is given by OCR2B.
warning

Keep OCR2A = 188 to retain proper millis() timing.


Task 3

  1. Cycle through all colors using HSV color space:
    • Convert HSV → RGB using provided convert_HSV_to_RGB() function.
    • Let Hue animate over time, keep Saturation = 1, Value = 1 (for full brightness).
    • Use system ticks to animate the color smoothly, just like LED pulsing.
    • Make sure to disable any conflicting code that modifies LED intensity.

HSV Color Wheel


Task 4 (Bonus)

  1. Use the speaker connected to PD4 (also OC1B) to play a melody from the sound.c module.
    • surprise_notes[] contains frequencies.
    • durations[] stores note durations.
    • In update_notes(), reconfigure Timer1 each time a new note is played.
      • Use CTC mode (OCR1A sets the TOP → controls frequency).
      • Set OCR1B = OCR1A / 2 for 50% duty cycle (square wave).
      • Call update_notes() every 25ms using systicks.
      • Reset TCNT1 = 0 after changing OCR1A to avoid timing delays.

Bonus 2

Combine:

  • RGB animation (HSV cycling) and
  • Sound playback (melody via speaker)

— both running simultaneously and smoothly.