The Arduino PWM output function, accessed via analogWrite(pin, value), generates a square wave with a variable duty cycle (0-255) to simulate an analog voltage. On the standard ATmega328P-based Arduino Uno R3, this output defaults to 490 Hz on most pins and 980 Hz on pins 5 and 6. While toggling a pin HIGH and LOW is trivial, driving real-world loads like DC motors, LED arrays, or heating elements requires understanding timer prescalers, logic-level gate thresholds, and inductive kickback protection.
This guide provides the exact hardware specifications, a robust high-power motor control build, and the debugging frameworks needed when your PWM signal fails to drive the load.
Hardware Spec Sheet: MCU PWM Capabilities & Timer Defaults
Before writing a single line of code, you must know which timer controls your target pin. The analogWrite() function abstracts the hardware, but if you need to change the PWM frequency to avoid audible motor whine or to sync with a camera shutter, you must manipulate the underlying timer registers. Below is the data-dense reference for the most common Arduino board variants.
| Board Variant (MCU) | PWM Capable Pins | Default Frequency | Resolution | Underlying Timers |
|---|---|---|---|---|
| Uno R3 (ATmega328P) | 3, 5, 6, 9, 10, 11 | 490 Hz (980 Hz on 5, 6) | 8-bit (0-255) | Timer0 (8-bit), Timer1 (16-bit), Timer2 (8-bit) |
| Uno R4 Minima (Renesas RA4M1) | 0, 1, 2, 3, 5, 6, 9, 10, 11 | 490 Hz | 8-bit (default), up to 12-bit | GPT (General PWM Timer) modules |
| Nano Every (ATmega4809) | 3, 5, 6, 9, 10, 11 | 490 Hz | 8-bit (0-255) | TCA0, TCB0-TCB3 |
| Mega 2560 (ATmega2560) | 2-13, 44-46 | 490 Hz (980 Hz on 4, 13) | 8-bit (0-255) | Timer0 through Timer5 |
millis() and delay() functions. Changing Timer0's prescaler to alter PWM frequency will break your timing functions. Always use Timer1 (pins 9 and 10) or Timer2 (pins 3 and 11) for frequency modifications.
Project Build: High-Power 12V Motor Speed Controller
The Arduino's GPIO pins can only source about 20mA safely. To drive a 12V DC motor drawing 2A, we use a logic-level N-channel MOSFET. Do not use the IRF520. The IRF520 requires a Gate-Source voltage (Vgs) of 10V to fully turn on; at the Uno's 5V logic, it operates in its linear region, acting as a resistor and overheating violently. Instead, we use the IRLZ44N, which is fully enhanced at Vgs = 4.0V.
Parts List & Pricing (2026 Estimates)
- Microcontroller: Arduino Uno R3 (ATmega328P) - $27.50
- MOSFET: IRLZ44N (Logic-Level, TO-220 package) - $1.20
- Gate Resistor: 220Ω (limits inrush current to the gate capacitor) - $0.05
- Pull-down Resistor: 10kΩ (prevents floating gate shoot-through on boot) - $0.05
- Flyback Diode: 1N4007 (protects MOSFET from inductive kickback) - $0.10
- Power Supply: 12V 5A DC switching supply - $14.00
Pin Mapping & Wiring Steps
| Component Pin | Connects To | Notes / Constraints |
|---|---|---|
| Arduino Pin 9 (PWM) | 220Ω Resistor -> MOSFET Gate | Timer1 output (OC1A). Capable of high-frequency modification. |
| MOSFET Gate | 10kΩ Resistor -> GND | Pulls gate LOW during Uno boot sequence to prevent motor spin-up. |
| MOSFET Source | Arduino GND & 12V PSU GND | Common ground is mandatory for the 5V logic reference. |
| MOSFET Drain | Motor Negative Terminal | Switches the low side of the load. |
| Motor Positive | 12V PSU Positive | Direct connection to power rail. |
| 1N4007 Cathode (Stripe) | Motor Positive | Must point towards the positive rail to clamp reverse voltage. |
| 1N4007 Anode | Motor Negative (MOSFET Drain) | Provides a recirculation path for inductive energy. |
Compilable Code with Bounds Checking & Error Handling
The following sketch targets the Arduino Uno R3 (ATmega328P). It reads a potentiometer on A0, maps the 10-bit ADC reading to an 8-bit PWM duty cycle, and includes bounds checking and serial error reporting to prevent out-of-range writes.
/*
* Target Board: Arduino Uno R3 (ATmega328P)
* Project: High-Power PWM Motor Speed Controller
* Pin 9 uses Timer1 (16-bit), default 490Hz.
*/
// Hardware Pin Definitions
#define PWM_OUTPUT_PIN 9
#define POT_INPUT_PIN A0
#define SERIAL_BAUD 115200
// System Constants
const int ADC_MAX = 1023;
const int PWM_MAX = 255;
const int PWM_MIN = 0;
const int DEAD_ZONE_THRESHOLD = 15; // Prevents motor stutter at very low voltages
void setup() {
Serial.begin(SERIAL_BAUD);
// Validate pin capability at runtime (software check)
if (!digitalPinHasPWM(PWM_OUTPUT_PIN)) {
Serial.println("FATAL ERROR: Pin 9 does not support PWM on this board variant.");
while(1); // Halt execution
}
pinMode(PWM_OUTPUT_PIN, OUTPUT);
pinMode(POT_INPUT_PIN, INPUT);
// Ensure motor is off at boot
analogWrite(PWM_OUTPUT_PIN, 0);
Serial.println("System Initialized. PWM Output Ready on Pin 9.");
}
void loop() {
int rawAdc = analogRead(POT_INPUT_PIN);
// Map 10-bit ADC (0-1023) to 8-bit PWM (0-255)
int targetPwm = map(rawAdc, 0, ADC_MAX, PWM_MIN, PWM_MAX);
// Apply dead-zone to prevent MOSFET linear-region heating at micro-duty cycles
if (targetPwm > 0 && targetPwm < DEAD_ZONE_THRESHOLD) {
targetPwm = 0;
}
// Bounds checking and error handling
if (targetPwm < PWM_MIN || targetPwm > PWM_MAX) {
Serial.print("ERROR: Calculated PWM out of bounds: ");
Serial.println(targetPwm);
targetPwm = constrain(targetPwm, PWM_MIN, PWM_MAX);
}
analogWrite(PWM_OUTPUT_PIN, targetPwm);
// Telemetry (throttled to prevent serial buffer flooding)
static unsigned long lastPrint = 0;
if (millis() - lastPrint > 250) {
Serial.print("ADC: "); Serial.print(rawAdc);
Serial.print(" | Duty: "); Serial.print((targetPwm * 100) / 255);
Serial.println("%");
lastPrint = millis();
}
delay(10); // Small delay for ADC settling
}
Debugging: When Your PWM Output Fails
When the motor doesn't spin, the LED flickers, or the compiler throws an error, follow this decision path. Here are the first three things to check when it fails:
- Verify the Pin is PWM-Capable: Look for the tilde (
~) symbol next to the pin on the Arduino silkscreen. If you wired to Pin 8,analogWrite()will simply output HIGH (5V) for any value > 127, and LOW (0V) for <= 127. - Measure Gate-Source Voltage (Vgs): Put your multimeter in DC voltage mode. Probe the MOSFET Gate and Source while the Arduino outputs 255. If you read < 4.5V, you have a voltage drop issue, or you are using a standard-level MOSFET (like the IRF520) that requires 10V to fully enhance.
- Check for Floating Gate Shoot-Through: If the motor spins out of control the moment you plug the Arduino into USB (before the sketch finishes booting), your 10kΩ pull-down resistor is missing or wired incorrectly. The GPIO pin floats during the bootloader sequence, partially turning on the MOSFET.
Common Error Strings and Ranked Causes
Error 1: Compiler Error
fatal error: avr/interrupt.h: No such file or directory or error: 'TCCR1B' was not declared in this scope
- Cause A (Most Likely): You are trying to compile code written for the ATmega328P (AVR architecture) but have the Arduino Uno R4 Minima (Renesas ARM architecture) selected in the IDE board manager. ARM cores do not have AVR timer registers like
TCCR1B. - Fix: Switch the board back to Uno R3 in the IDE, or rewrite the timer manipulation using the Renesas FSP (Flexible Software Package) PWM API if you must use the R4.
Error 2: Hardware Symptom
Motor emits a loud, high-pitched whine that changes pitch with the potentiometer.
- Cause A: The default 490 Hz PWM frequency falls squarely in the human audible range. The motor windings are physically vibrating at 490 times per second.
- Fix: You must push the frequency above 20 kHz (ultrasonic). See the extension section below for Timer1 prescaler adjustments.
Extending and Simplifying the Build
How to Extend: Pushing Frequency to 20 kHz (Silent Operation)
To eliminate audible motor whine, we can manipulate Timer1's prescaler and mode registers on the ATmega328P. By setting Timer1 to Phase Correct PWM mode with a prescaler of 1, we achieve a frequency of roughly 31.3 kHz. Add this block to the end of your setup() function (this only works on AVR boards like the Uno R3):
// Configure Timer1 for ~31.3 kHz PWM on Pin 9
TCCR1A = 0; // Clear control register A
TCCR1B = 0; // Clear control register B
// Set to Phase Correct PWM, 8-bit resolution
TCCR1A |= (1 << COM1A1) | (1 << WGM10);
// Set prescaler to 1 (no prescaling), starts the timer
// 16MHz / (2 * 1 * 255) = ~31,372 Hz
TCCR1B |= (1 << CS10);
Note: Changing Timer1 affects the Servo library, which relies on Timer1's default 50Hz configuration. If you use this high-frequency PWM, you cannot use standard hobby servos on pins 9 or 10 simultaneously.
How to Simplify: Direct LED Dimming
If you are only driving a low-power LED strip (under 20mA total) or a single indicator LED and do not need high-power switching, strip away the MOSFET, the pull-down resistor, and the flyback diode. Connect the LED anode to Pin 9 via a 330Ω current-limiting resistor, and the cathode directly to Arduino GND. The standard analogWrite() function handles the rest without any external power supplies or gate-drive circuitry.






