The Arduino Nano PWM Hardware Reality
Pulse Width Modulation (PWM) on the Arduino Nano is not a software illusion; it is a direct manipulation of hardware timers inside the ATmega328P microcontroller. When you call analogWrite(pin, value), you are not outputting a variable voltage. You are outputting a 5V square wave where the ratio of ON time to OFF time (the duty cycle) dictates the average power delivered to the load.
The most common mistake hobbyists make with Arduino Nano PWM is treating all PWM-capable pins as identical. They are not. The Nano's six PWM pins are hardwired to three distinct hardware timers (Timer0, Timer1, and Timer2). If you attach a servo to Pin 9 and a motor to Pin 10, both are fighting for Timer1 resources. If you alter Timer0 to change your motor's PWM frequency, you will instantly break the millis() and delay() functions.
ATmega328P Timer-to-Pin Mapping Table
This data-dense reference table must be your first stop before wiring any PWM project. Notice the frequency discrepancy on Pins 5 and 6—this is a deliberate factory configuration by the Arduino core to ensure millis() resolves cleanly.
| Nano Pin | Hardware Timer | Bit-Width | Default PWM Freq. | Shared Functions & Conflicts |
|---|---|---|---|---|
| D3 | Timer2 (Channel B) | 8-bit | ~490.20 Hz | Used by tone() library. Conflicts with IRremote. |
| D5 | Timer0 (Channel B) | 8-bit | ~980.39 Hz | CRITICAL: Controls millis(), delay(). Do not alter prescaler. |
| D6 | Timer0 (Channel A) | 8-bit | ~980.39 Hz | Same Timer0 constraints as Pin 5. |
| D9 | Timer1 (Channel A) | 16-bit | ~490.20 Hz | Used by Servo.h. Best pin for high-res motor control. |
| D10 | Timer1 (Channel B) | 16-bit | ~490.20 Hz | Shares Timer1 with Pin 9 and Servo.h. |
| D11 | Timer2 (Channel A) | 8-bit | ~490.20 Hz | Shares Timer2 with Pin 3 and tone(). |
Source: Microchip ATmega328P Datasheet and Arduino Nano Official Documentation.
Project Build: Precision DC Motor Speed Control
For this build, we are abandoning the outdated, heat-generating L298N motor driver. Instead, we are using the TB6612FSG dual motor driver. It utilizes MOSFETs instead of BJTs, resulting in a voltage drop of only ~0.5V at 1A (compared to the L298N's massive 2V+ drop), meaning more of your battery's power actually reaches the motor.
Parts List & Exact Variants
- Microcontroller: Arduino Nano V3 (ATmega328P, 16MHz, CH340 or FT232RL USB chip)
- Motor Driver: TB6612FSG Dual Motor Driver Breakout (Pololu #713 or Adafruit 2927)
- Motor: 12V N20 Metal Gearmotor (e.g., Pololu #2272, 100:1 metal gear ratio)
- Power Supply: 12V 2A DC switching power supply or 3S LiPo battery (11.1V nominal)
- Wiring: 22 AWG solid core for breadboard, 18 AWG stranded for motor and power terminals
Pin Mapping & Wiring Table
| TB6612FSG Pin | Arduino Nano Pin | Function & Notes |
|---|---|---|
| VCC | 5V | Logic power for the TB6612FSG. |
| VM | 12V Power Supply (+) | Motor power input. Do NOT connect to Nano 5V. |
| GND | GND (Nano) & Power Supply (-) | Crucial: All grounds must be tied together (equipotential bonding). |
| PWMA | D9 (PWM) | Speed control for Motor A. Uses Timer1 (16-bit). |
| AIN1 | D7 | Direction control bit 1. |
| AIN2 | D8 | Direction control bit 2. |
| STBY | 5V (or D6) | Standby pin. Tie to 5V to keep the chip always active. |
Complete Compilable Code with Error Handling
The following code implements a non-blocking acceleration and deceleration ramp for the motor. It avoids delay() to keep the main loop free for sensor polling or serial communication. It also includes bounds-checking to prevent out-of-range PWM values, which can cause erratic behavior on AVR chips if cast improperly.
// Target: Arduino Nano V3 (ATmega328P)
// Project: TB6612FSG Non-Blocking PWM Motor Ramp
// --- PIN DEFINITIONS ---
#define MOTOR_PWM_PIN 9 // Timer1 (16-bit), ~490Hz
#define MOTOR_IN1_PIN 7 // Direction A
#define MOTOR_IN2_PIN 8 // Direction B
// --- TIMING & STATE VARIABLES ---
unsigned long previousMillis = 0;
const long rampInterval = 50; // Update PWM every 50ms
int currentPWM = 0;
int pwmStep = 5; // Increment/decrement step
const int MAX_PWM = 255;
const int MIN_PWM = 0;
void setup() {
// Initialize Serial for debugging
Serial.begin(115200);
while (!Serial) { ; } // Wait for serial port (Nano native USB behavior)
// Configure pins
pinMode(MOTOR_PWM_PIN, OUTPUT);
pinMode(MOTOR_IN1_PIN, OUTPUT);
pinMode(MOTOR_IN2_PIN, OUTPUT);
// Set initial direction: Forward
// IN1 = HIGH, IN2 = LOW
digitalWrite(MOTOR_IN1_PIN, HIGH);
digitalWrite(MOTOR_IN2_PIN, LOW);
Serial.println("Motor Ramp Initialized on Pin 9 (Timer1)");
}
void loop() {
unsigned long currentMillis = millis();
// Non-blocking timer check
if (currentMillis - previousMillis >= rampInterval) {
previousMillis = currentMillis;
// Calculate next PWM value
int nextPWM = currentPWM + pwmStep;
// Bounds checking and direction reversal logic
if (nextPWM > MAX_PWM) {
nextPWM = MAX_PWM;
pwmStep = -pwmStep; // Reverse ramp direction
Serial.println("Reached MAX_PWM, decelerating...");
} else if (nextPWM < MIN_PWM) {
nextPWM = MIN_PWM;
pwmStep = -pwmStep; // Reverse ramp direction
Serial.println("Reached MIN_PWM, accelerating...");
}
currentPWM = nextPWM;
// Constrain is a safety net against variable overflow
int safePWM = constrain(currentPWM, 0, 255);
// Write to hardware timer
analogWrite(MOTOR_PWM_PIN, safePWM);
// Debug output
Serial.print("PWM Duty Cycle: ");
Serial.println(safePWM);
}
// Add other non-blocking tasks here (e.g., reading encoders)
}
Debugging: When Arduino Nano PWM Fails
PWM issues on the Nano rarely stem from the analogWrite() function itself; they stem from hardware limitations, power physics, or library conflicts. If your motor is whining, stuttering, or your code won't compile, follow this decision path.
The First 3 Things to Check When It Fails
- Ground Reference Mismatch: If the motor twitches or the Nano resets when the motor starts, your grounds are not bonded. The TB6612FSG logic ground and the Nano ground must share a common return path to the power supply. Measure the voltage between the Nano GND pin and the TB6612FSG GND pin with your multimeter; it should read
< 0.05Vunder load. - Power Supply Voltage Sag: A 12V wall-wart might output 14V open-circuit but drop to 7V when the motor draws stall current. Measure
VMon the motor driver while the motor is attempting to start. If it drops below the motor's rated threshold, the PWM signal is irrelevant because the driver lacks the headroom to drive the H-bridge MOSFETs. - Timer0 Prescaler Alteration: If your motor runs fine but your
millis()function is returning values that drift or run at half-speed, you (or a library you included) have modified theTCCR0Bregister. Never change Timer0's prescaler unless you are writing a custom RTOS and understand the math required to compensatemicros().
Exact Error String: The Resolution Trap
A frequent error occurs when makers copy code from 32-bit ARM boards (like the Arduino Zero or Due) to the Nano. The ARM boards support changing the PWM bit-depth. The AVR-based Nano does not.
If your code includes analogWriteResolution(10);, the AVR-GCC compiler will halt and throw this exact error:
error: 'analogWriteResolution' was not declared in this scope
Ranked Causes & Fixes:
- Cause: Code ported from a SAMD/ARM architecture. Fix: Delete the line. The Nano's
analogWrite()is hardcoded to 8-bit (0-255) resolution via its 8-bit and 16-bit timer registers. - Cause: Attempting to use a third-party library like
PWM.hthat expects a different core. Fix: Stick to the nativeanalogWrite()or use direct register manipulation (OCR1A = value;) if you need 16-bit resolution on Pin 9.
Extending and Simplifying the Build
The beauty of the ATmega328P's timer architecture is its modularity. Once you understand the hardware mapping, you can scale this project up or down without rewriting your core logic.
How to Simplify (Visual Feedback Only)
If you are just prototyping and want to strip this down to a simple LED breathing circuit, remove the TB6612FSG and motor. Connect a standard 5mm LED with a 220Ω current-limiting resistor directly to Pin 9. The exact same analogWrite() code will work flawlessly. Warning: Never connect an LED directly to a Nano pin without a resistor; the ATmega328P absolute maximum DC current per I/O pin is 40mA, and an unbuffered LED will pull 100mA+ and fry the silicon trace.
How to Extend (Closed-Loop PID Control)
To turn this open-loop ramp into a closed-loop speed controller, add a quadrature encoder to the N20 motor's rear shaft. Wire the encoder A/B channels to Pins 2 and 3 (hardware interrupts INT0 and INT1). Use the Pololu TB6612FSG documentation to verify your driver's switching speed, and implement a PID library (like Arduino's PID_v1) to dynamically adjust the safePWM variable based on the actual RPM read from the encoder. Because we kept the main loop non-blocking, your PID Compute() function will execute precisely every 50ms without being stalled by delay() calls.
By respecting the hardware timers and matching your PWM pin choice to your peripheral requirements, the Arduino Nano remains one of the most capable and cost-effective embedded controllers for precision motor and lighting applications in 2026.






