The Arduino Uno R3 has exactly six hardware PWM pins: 3, 5, 6, 9, 10, and 11. You can identify them by the tilde (~) printed on the board's silkscreen next to the digital pin numbers. However, treating all six pins as identical is a common mistake that leads to broken timing functions, motor whine, and serial communication dropouts. Under the hood of the ATmega328P microcontroller, these pins are tied to three distinct hardware timers, each with different default frequencies, maximum resolutions, and system-level conflicts.

This guide maps every Arduino PWM pin to its underlying hardware timer, provides a complete DC motor control build using a modern MOSFET-based driver, and details the exact debugging steps to take when your PWM output fails.

Arduino Uno R3 PWM Pin and Timer Mapping

Before wiring your project, you must select the correct pin based on your frequency requirements and potential hardware conflicts. The analogWrite() function relies on the ATmega328P's internal timers. Altering the prescaler of a timer to change the PWM frequency will affect every pin attached to that timer, as well as core Arduino functions that rely on it.

PWM Pin Silkscreen Hardware Timer Default Frequency Fast PWM Max System Conflicts & Warnings
3 ~ Timer 2B 490.20 Hz 31.37 kHz Alters tone() library if used simultaneously.
5 ~ Timer 0B 976.56 Hz 62.50 kHz CRITICAL: Changing Timer 0 prescaler breaks millis(), delay(), and micros().
6 ~ Timer 0A 976.56 Hz 62.50 kHz CRITICAL: Shares Timer 0 with Pin 5. Do not modify prescaler.
9 ~ Timer 1A 490.20 Hz 31.37 kHz Safe for high-frequency modification. Used by Servo library.
10 ~ Timer 1B 490.20 Hz 31.37 kHz Safe for high-frequency modification. Shares Timer 1 with Pin 9.
11 ~ Timer 2A 490.20 Hz 31.37 kHz Shares Timer 2 with Pin 3. Conflicts with tone().
Callout Tip: If your project requires ultrasonic PWM frequencies (above 20 kHz) to eliminate audible motor whine, always use Pin 9 or 10. These are tied to the 16-bit Timer 1, which can be safely reconfigured without destroying the Arduino core timing functions tied to Timer 0.

Parts List and Wiring for DC Motor Speed Control

For this build, we are targeting the Arduino Uno R3 (ATmega328P). We will use a TB6612FNG motor driver instead of the older L298N. The L298N uses a BJT H-bridge design that drops roughly 2V to 3V across the motor at high currents. The TB6612FNG uses MOSFETs, dropping only about 0.5V, which delivers significantly more torque to the motor and runs much cooler.

Difficulty Rating: Intermediate (Requires understanding of H-bridge logic and ADC filtering)

Estimated Time: 45 minutes

Required Components

  • Microcontroller: Arduino Uno R3 (ATmega328P variant)
  • Motor Driver: TB6612FNG Dual Motor Driver Breakout (e.g., Pololu #713 or SparkFun ROB-14451)
  • Motor: 12V Brushed DC Motor (e.g., Pololu 100:1 Micro Metal Gearmotor)
  • Input: 10kΩ Linear Taper Potentiometer (B10K)
  • Power: 12V 2A DC Power Supply (Barrel jack to terminal adapter)
  • Misc: 10kΩ pull-down resistor, breadboard, 22 AWG solid jumper wires

Pin Mapping Table

Arduino Uno R3 Pin TB6612FNG Pin Component Function
9 (PWM ~) PWMA Motor Driver PWM Speed Control (Timer 1A)
8 (Digital) AIN2 Motor Driver Direction Logic Bit 2
7 (Digital) AIN1 Motor Driver Direction Logic Bit 1
A0 (Analog) - Potentiometer (Wiper) Speed Command Input
5V VCC Motor Driver Logic Power (5V)
GND GND Motor Driver / Pot Common Ground Reference
- VMOT 12V Power Supply Motor High-Voltage Rail
- STBY Jumper to VCC Takes driver out of standby

Complete PWM Motor Control Code

The following code reads the potentiometer, applies a software low-pass filter to eliminate ADC noise, and maps the value to a 0-255 PWM duty cycle. It includes explicit error handling to detect a disconnected or floating potentiometer, a common hardware fault on breadboards.

// Target Board: Arduino Uno R3 (ATmega328P)
// Library Dependencies: None (Core Arduino)

#define PWM_PIN     9    // Timer 1A - Safe for frequency modification
#define DIR_PIN_A   7    // Motor Direction A
#define DIR_PIN_B   8    // Motor Direction B
#define POT_PIN     A0   // Analog Input for Speed Command

// Error thresholds
#define NOISE_THRESHOLD 150  // Max allowed ADC jump between reads
#define FLOAT_THRESHOLD 1000 // ADC value indicating pulled-high floating pin

int lastReadVal = 0;
int filteredVal = 0;

void setup() {
  Serial.begin(115200);
  pinMode(PWM_PIN, OUTPUT);
  pinMode(DIR_PIN_A, OUTPUT);
  pinMode(DIR_PIN_B, OUTPUT);
  
  // Set motor to forward direction permanently for this single-direction test
  digitalWrite(DIR_PIN_A, HIGH);
  digitalWrite(DIR_PIN_B, LOW);
  
  // Initialize ADC baseline
  lastReadVal = analogRead(POT_PIN);
  filteredVal = lastReadVal;
  
  Serial.println("System Initialized. Timer 1A PWM active on Pin 9.");
}

void loop() {
  int rawRead = analogRead(POT_PIN);
  
  // Error Handling: Check for disconnected/floating potentiometer
  // If the internal pullup or stray noise drives a disconnected pin high, rawRead approaches 1023
  // Or if the delta between reads is physically impossible for a human turning a knob
  int delta = abs(rawRead - lastReadVal);
  
  if (rawRead > FLOAT_THRESHOLD && delta > NOISE_THRESHOLD) {
    Serial.println("ERR: POT_FLOAT_DETECTED. Check A0 wiring and ground.");
    analogWrite(PWM_PIN, 0); // Fail-safe: Kill motor
    delay(1000);
    return;
  }
  
  // Software Low-Pass Filter (Alpha = 0.2)
  filteredVal = (0.8 * filteredVal) + (0.2 * rawRead);
  lastReadVal = rawRead;
  
  // Map 10-bit ADC (0-1023) to 8-bit PWM (0-255)
  // Implementing a 5% deadband at the bottom to prevent motor stalling/whining
  int pwmDuty = map(filteredVal, 50, 1023, 0, 255);
  
  if (pwmDuty < 0) pwmDuty = 0;
  if (pwmDuty > 255) pwmDuty = 255;
  
  analogWrite(PWM_PIN, pwmDuty);
  
  // Debug output every 250ms
  static unsigned long lastPrint = 0;
  if (millis() - lastPrint > 250) {
    Serial.print("ADC: ");
    Serial.print(rawRead);
    Serial.print(" | Filtered: ");
    Serial.print(filteredVal);
    Serial.print(" | PWM Duty: ");
    Serial.println(pwmDuty);
    lastPrint = millis();
  }
  
  delay(10); // 100Hz control loop rate
}

Debugging PWM Failures: The First Three Things to Check

When your motor refuses to spin, emits a high-pitched whine without moving, or your serial monitor spits out garbage, do not immediately rewrite your code. Hardware and timer conflicts are the culprit 90% of the time. Here are the first three things to check, ranked by likelihood.

1. Timer 0 Prescaler Corruption (The millis() Trap)

Symptom: The motor works, but your delay() functions take 64 times longer than expected, or your serial print timestamps drift wildly.

Cause: You searched online for "how to change Arduino PWM frequency" and copied a snippet that modifies the TCCR0B register. Pins 5 and 6 share Timer 0 with the system clock. Altering this prescaler breaks the millis() overflow interrupt.

Fix: Move your PWM output to Pin 9 or 10 (Timer 1) and modify TCCR1B instead. Never touch TCCR0B unless you are writing bare-metal firmware without Arduino core dependencies.

2. Power Supply Brownout and VBUS Backfeeding

Symptom: The Arduino resets randomly when the motor starts, or the onboard 'L' LED dims. Serial connection drops with ERR: POT_FLOAT_DETECTED or garbage characters.

Cause: A 12V DC motor under load can draw 1A to 3A. If your motor driver's VMOT and Logic VCC share the same weak 5V USB rail, or if the motor's back-EMF is collapsing into the logic rail due to missing flyback diodes (the TB6612FNG has them built-in, but wiring inductance can still cause spikes), the ATmega328P browns out at ~4.2V.

Fix: Ensure VMOT is fed by a dedicated 12V supply capable of at least 2A. Verify that the GND of the 12V supply is tied directly to the Arduino GND to establish a common equipotential reference.

3. Logic-Level Voltage Mismatch

Symptom: The motor driver gets warm but the motor doesn't spin, or it only spins at full speed (ignoring PWM).

Cause: If you are using a 3.3V microcontroller (like an ESP32 or Arduino Nano 33 IoT) to drive a 5V-logic motor driver, the 3.3V HIGH signal may not cross the driver's V_IH (Input High Voltage) threshold, leaving the H-bridge in an undefined or high-impedance state.

Fix: Use a bidirectional logic level converter (e.g., TXB0104) between the MCU and the driver, or select a motor driver explicitly rated for 3.3V logic inputs.

Extending and Simplifying the Build

Depending on your end goal, you can either push the hardware to its limits or strip it down to the bare essentials.

How to Extend: Pushing to 31.25 kHz Ultrasonic PWM

If you are driving a motor in a quiet environment (like a camera gimbal or a medical device), the default 490 Hz PWM frequency will produce an audible, irritating whine. You can push Timer 1 (Pins 9 and 10) to 31.25 kHz, moving the switching noise above human hearing.

Add this to your setup() function before your first analogWrite():

// Set Timer 1 to Fast PWM, 8-bit, Prescaler = 1 (31.25 kHz on 16MHz clock)
TCCR1A = _BV(COM1A1) | _BV(WGM10);
TCCR1B = _BV(WGM12) | _BV(CS10);

Note: When changing Timer 1 modes, verify the ATmega328P datasheet to ensure you aren't conflicting with the Servo library, which strictly requires Timer 1 to remain at its default 50Hz configuration.

How to Simplify: The Logic-Level MOSFET Route

If your project only requires single-direction speed control (e.g., a fan, a water pump, or a conveyor belt), an H-bridge like the TB6612FNG is overkill and adds unnecessary cost and wiring complexity.

Strip the driver board out entirely. Replace it with a single IRLZ44N logic-level N-channel MOSFET. Connect the Arduino PWM pin to the Gate via a 220Ω resistor, the Source to GND, and the Drain to the motor's negative terminal. Add a 10kΩ pull-down resistor between Gate and Source to prevent the motor from spinning up while the Arduino is booting and the GPIO pins are floating. This reduces your BOM cost by roughly $6 and cuts your wiring time in half.