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.

Target Board Variant: The code and pin mappings in this guide specifically target the Arduino Nano V3 (ATmega328P, 16MHz crystal). If you are using an older Nano with the ATmega168, or a 3.3V/8MHz Pro Mini clone, the timer architecture is identical, but your absolute clock frequencies and voltage thresholds will differ.

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.

Difficulty: Intermediate | Time: 45 Minutes | Cost: ~$18 USD

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

  1. 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.05V under load.
  2. Power Supply Voltage Sag: A 12V wall-wart might output 14V open-circuit but drop to 7V when the motor draws stall current. Measure VM on 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.
  3. 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 the TCCR0B register. Never change Timer0's prescaler unless you are writing a custom RTOS and understand the math required to compensate micros().

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:

  1. 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.
  2. Cause: Attempting to use a third-party library like PWM.h that expects a different core. Fix: Stick to the native analogWrite() 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.