To use PWM on Arduino, you must select a pin marked with a tilde (~) on the silkscreen and call analogWrite(pin, duty). On the standard Arduino Uno R3, pins 5 and 6 output a 980 Hz square wave, while pins 3, 9, 10, and 11 output at 490 Hz. If your project requires custom frequencies—like 20 kHz for silent motor control or high-resolution dimming—you must bypass analogWrite() and manipulate the hardware timers directly.
millis(), delay(), and micros() functions. Changing its prescaler will break all time-based logic in your sketch.
The PWM on Arduino Decision Matrix
Choosing the right pin and timer is where most embedded projects stall. The ATmega328P has three hardware timers (Timer0, Timer1, Timer2), each mapped to specific pins. Use this decision tree to lock in your hardware configuration.
| Application | Required Frequency | Recommended Pin | Hardware Timer | Resolution |
|---|---|---|---|---|
| Simple LED Dimming | 490 Hz (Default) | Pin 3 or 11 | Timer2 (8-bit) | 0-255 |
| DC Motor / Fan Control | 20 kHz - 25 kHz (Ultrasonic) | Pin 9 or 10 | Timer1 (16-bit) | 0-65535 |
| Audio / DAC Reconstruction | 32 kHz - 62 kHz | Pin 3 or 11 | Timer2 (8-bit) | 0-255 |
| Standard Hobby Servos | 50 Hz (Fixed) | Pin 9 or 10 | Timer1 (via Servo.h) | Pulse width (µs) |
The Concrete Pick: For 90% of DIY motor, solenoid, and high-power LED projects, select Pin 9 (OC1A) on the Uno R3. It is tied to Timer1, a 16-bit timer that allows for massive frequency flexibility and high-resolution duty cycle adjustments without breaking core Arduino timing functions.
Benchmark Build: Parts List & Pin Mapping
This build drives a 12V DC fan/motor at 20 kHz (eliminating audible PWM whine) using the Arduino Uno R3. We are using a logic-level MOSFET; do not substitute a standard IRF520, as it requires 10V at the gate to fully open and will overheat when driven by the Uno's 5V logic.
Hardware Spec Sheet
| Component | Exact Part Number | Why This Specific Part? |
|---|---|---|
| Microcontroller | Arduino Uno R3 (ATmega328P) | Standard 5V logic, well-documented AVR timers. |
| MOSFET | IRLZ44N (Logic-Level) | Vgs(th) is 1-2V. Fully turns on at 5V gate drive. |
| Flyback Diode | 1N5819 (Schottky) | Fast reverse recovery for 20kHz PWM. (A standard 1N4007 is too slow and will overheat at ultrasonic frequencies). |
| Gate Pulldown Resistor | 10kΩ (1/4W) | Prevents motor spin-up during Uno boot/flash. |
| Gate Series Resistor | 220Ω (1/4W) | Limits inrush current to the gate capacitor, protecting the ATmega328P GPIO. |
Pin Mapping Table
| Arduino Uno R3 Pin | Connects To | Function |
|---|---|---|
| Pin 9 (~) | 220Ω Resistor -> MOSFET Gate | PWM Output (Timer1, OC1A) |
| GND | MOSFET Source & 10kΩ Pulldown | Common Ground Reference |
| 5V | (Optional) Fan Tachometer | Pull-up for RPM sensing |
Step-by-Step Wiring & Compilable Code
Difficulty Rating: Intermediate (Requires direct register manipulation).
Time to Build: 20 minutes.
- Wire the Gate: Connect Pin 9 to the 220Ω resistor, then to the Gate of the IRLZ44N. Connect a 10kΩ resistor between the Gate and GND.
- Wire the Load: Connect the 12V motor's positive lead to your 12V power supply. Connect the negative lead to the MOSFET's Drain.
- Wire the Source & Ground: Connect the MOSFET's Source to the GND of your 12V power supply. Crucial: You must tie the Arduino GND to the 12V Power Supply GND.
- Place the Flyback Diode: Connect the 1N5819 across the motor terminals, with the cathode (silver stripe) pointing toward the 12V positive side.
- Upload the Code: Flash the sketch below.
// Target Board: Arduino Uno R3 (ATmega328P DIP or SMD)
// Application: 20kHz Ultrasonic PWM Motor Control on Pin 9
#if !defined(__AVR_ATmega328P__)
#error "Compilation halted: This code targets the ATmega328P (Uno R3 / Nano). For Nano Every, use TCA registers. For ESP32, use the LEDC peripheral."
#endif
#include <avr/io.h>
const uint8_t PWM_PIN = 9; // OC1A pin
const uint16_t PWM_FREQ_HZ = 20000; // 20kHz target
// Error handling bounds for duty cycle
void setDutyCycle(float percentage) {
if (percentage < 0.0) percentage = 0.0;
if (percentage > 100.0) percentage = 100.0;
// Calculate OCR1A value for 16-bit timer
// ICR1 is set to 799 in setup for 20kHz. Max duty = 799.
uint16_t dutyValue = (uint16_t)((percentage / 100.0) * 799.0);
OCR1A = dutyValue;
}
void setup() {
Serial.begin(115200);
pinMode(PWM_PIN, OUTPUT);
// Configure Timer1 for Phase and Frequency Correct PWM
// Clear OC1A on compare match when up-counting, set when down-counting
TCCR1A = (1 << COM1A1) | (0 << COM1A0) | (1 << WGM11) | (0 << WGM10);
// Set prescaler to 1 (no prescaling), enable Phase/Freq Correct mode via ICR1
TCCR1B = (1 << WGM13) | (0 << WGM12) | (0 << CS12) | (0 << CS11) | (1 << CS10);
// Calculate ICR1 for 20kHz:
// Formula: ICR1 = (F_CPU / (2 * Prescaler * Target_Freq)) - 1
// ICR1 = (16,000,000 / (2 * 1 * 20000)) - 1 = 399
// Wait, Phase Correct counts up AND down, so top value is F_CPU / (2 * N * f)
// Let's use Fast PWM instead for simpler math and exact 20kHz.
// RECONFIGURING FOR FAST PWM (Mode 14, ICR1 as TOP)
TCCR1A = (1 << COM1A1) | (0 << COM1A0) | (1 << WGM11) | (0 << WGM10);
TCCR1B = (1 << WGM13) | (1 << WGM12) | (0 << CS12) | (0 << CS11) | (1 << CS10);
// Fast PWM Formula: ICR1 = (F_CPU / (Prescaler * Target_Freq)) - 1
// ICR1 = (16,000,000 / (1 * 20000)) - 1 = 799
ICR1 = 799;
setDutyCycle(50.0); // Start at 50% duty cycle
Serial.println("Timer1 configured for 20kHz Fast PWM on Pin 9.");
}
void loop() {
// Example: Ramp motor speed up and down
for (float duty = 0; duty <= 100; duty += 5) {
setDutyCycle(duty);
Serial.print("Duty: "); Serial.print(duty); Serial.println("%");
delay(500);
}
for (float duty = 100; duty >= 0; duty -= 5) {
setDutyCycle(duty);
Serial.print("Duty: "); Serial.print(duty); Serial.println("%");
delay(500);
}
}
Debugging: First Three Checks & Exact Error Strings
When your PWM output fails, don't immediately rewrite your code. Run through this physical and logical checklist.
The First Three Things to Check
- Is it actually a hardware PWM pin? If you call
analogWrite(8, 128), the Uno will not output a square wave. It will output a static 5V (if duty > 127) or 0V. Only pins with the ~ symbol support hardware PWM. - Is a library hijacking the timer? If you include
<Servo.h>, it automatically claims Timer1. This completely disables hardware PWM on Pins 9 and 10, reverting them to standard digital I/O. If you need servos and PWM simultaneously, move your PWM to Timer2 (Pins 3 or 11). - Are you measuring with a multimeter? A standard DMM in DC Voltage mode reads the average voltage of a PWM signal. A 50% duty cycle on a 5V pin will read exactly 2.5V on your meter. This is not an analog voltage; it's a square wave. You must use an oscilloscope or a logic analyzer to verify the PWM frequency and waveform.
Exact Compiler Error: error: 'TCCR1B' was not declared in this scope
If you copy-paste raw AVR timer code from an older forum post and hit compile, you will likely see this exact error string:
error: 'TCCR1B' was not declared in this scope
error: 'ICR1' was not declared in this scope
Ranked Causes & Fixes:
| Rank | Cause | Fix |
|---|---|---|
| 1 (Most Likely) | Wrong Board Selected: You are using an Arduino Nano Every (ATmega4809) or Uno R4 (Renesas RA4M1), which do not have AVR TCCR registers. |
Go to Tools > Board and select the classic Arduino Uno (ATmega328P). If you must use the Nano Every, rewrite the code using the TCA peripheral registers. |
| 2 | Missing Header in PlatformIO: The Arduino IDE includes avr/io.h implicitly, but PlatformIO requires it explicitly for register names. |
Add #include <avr/io.h> at the very top of your sketch. |
| 3 | Typo in Register Name: Confusing Timer1 Control Register B (TCCR1B) with Timer0 (TCCR0B). |
Verify the register matches the timer you are trying to manipulate. |
Scaling the Build: Simplify or Extend
Direct register manipulation is powerful, but it isn't always the right tool. Use this framework to decide how to scale your project.
When to Simplify (Use analogWrite)
If you are driving a simple indicator LED, a heating element via a solid-state relay, or a low-frequency water pump, the default 490 Hz frequency is perfectly adequate. Drop the register math and use analogWrite(9, 128). It is universally compatible across almost all Arduino-compatible boards, requires zero configuration, and keeps your code readable. For deeper reading on standard analog outputs, refer to the official Arduino analogWrite documentation.
When to Extend (Move to ESP32)
The ATmega328P is limited by its 16-bit timers and shared clock domains. If your project requires:
- Independent frequencies on multiple pins simultaneously.
- High-resolution (16-bit to 20-bit) duty cycle control for precision lab equipment.
- Dead-time insertion for H-bridge motor drivers.
The Upgrade Pick: Migrate to the ESP32-WROOM-32. The ESP32 utilizes the LEDC (LED Controller) peripheral, which allows you to assign any GPIO pin to a PWM channel, set independent frequencies per channel, and achieve up to 20-bit resolution. You can explore the architectural differences in PWM generation in the classic Secrets of Arduino PWM tutorial, which highlights the limitations of the AVR architecture that the ESP32 solves.






