The Core Misconception: ADC vs. Digital PWM

When troubleshooting how to Arduino read PWM signals, the most common point of failure stems from a fundamental misunderstanding of microcontroller hardware. Many beginners assume that because Pulse Width Modulation (PWM) controls analog-like behavior (such as dimming an LED or controlling motor speed), it can be measured using the analogRead() function. This is incorrect.

The ATmega328P (found on the classic Arduino Uno R3 and Nano) utilizes a Successive Approximation Register (SAR) ADC that samples at a maximum rate of roughly 9.6 kHz. According to the Nyquist-Shannon sampling theorem, to accurately reconstruct a waveform, your sampling rate must be significantly higher than the signal frequency. If you feed a standard 1 kHz PWM signal into an analog pin, the ADC will sample random points along the square wave's HIGH (5V) and LOW (0V) states. Instead of reading a clean 50% duty cycle, your serial monitor will output erratic values jumping between 0 and 1023.

The Golden Rule: PWM is a digital time-domain signal, not an analog voltage. To measure it, you must measure time, not voltage.

Symptom 1: The Main Loop Freezes (PulseIn Blocking)

The standard software method to measure a digital pulse is the pulseIn() function. While the official Arduino pulseIn reference documents its basic usage, it often hides a critical flaw for real-time systems: it is a blocking function.

When you call pulseIn(pin, HIGH), the microcontroller halts all other operations and sits in a tight while-loop, counting CPU clock cycles until the pin transitions LOW. By default, the timeout is set to 1,000,000 microseconds (1 full second). If your PWM source—such as an RC receiver or a faulty sensor—drops the signal or fails to pull the line HIGH, your entire Arduino sketch freezes for one second. In a balancing robot or drone application, a one-second loop delay guarantees a crash.

The Fix: Implement Strict Timeouts

Never use pulseIn() without defining a custom timeout. If you are reading a standard 50Hz RC servo signal (which has a 20ms total period), set your timeout slightly above the maximum expected pulse width (usually 2.5ms).

unsigned long duration = pulseIn(pin, HIGH, 4000); // 4ms timeout

This ensures that if the signal drops, the function returns 0 within 4 milliseconds, allowing your failsafe routines to trigger immediately.

Symptom 2: Inaccurate Readings at High Frequencies

Software-based timing relies on the CPU executing instructions sequentially. At low frequencies (50Hz to 500Hz), pulseIn() is highly accurate. However, if you are attempting to read a 20 kHz PC fan PWM tachometer signal or a 25 kHz ESC telemetry line, software overhead and interrupt jitter (caused by millis() or Serial communication) will destroy your measurement accuracy.

Method Comparison Matrix

Reading Method Max Reliable Freq CPU Overhead Accuracy Best Use Case
analogRead() ~500 Hz (Filtered) High Poor Reading heavily filtered DC voltage (RC filter)
pulseIn() ~5 kHz Extreme (Blocking) Medium RC Receivers (50Hz), Ultrasonic Sensors
Pin Change Interrupts ~15 kHz Medium High Multiple low-frequency PWM inputs
Hardware Input Capture ~500 kHz Zero (Background) Perfect ESC Telemetry, High-speed motor control

The Professional Fix: Hardware Input Capture (ICP1)

To accurately read high-frequency PWM without burdening the CPU, you must use the microcontroller's hardware timers. On the ATmega328P, Timer1 features an Input Capture Pin (ICP1), which is mapped to Digital Pin D8 on the Arduino Uno.

When a signal edge (rising or falling) is detected on D8, the hardware instantly copies the current value of the Timer1 counter (TCNT1) into the Input Capture Register (ICR1) and fires an interrupt. Because this happens at the silicon level, it is completely immune to software jitter or delayed interrupt service routines (ISRs).

Configuration Steps for ATmega328P

  1. Configure Timer1: Set the prescaler to match your expected frequency. A prescaler of 8 (yielding a 2 MHz tick rate at 16MHz system clock) provides 0.5-microsecond resolution.
  2. Enable Input Capture: Set the ICES1 bit in the TCCR1B register to trigger on a rising edge.
  3. Handle the ISR: Write an Interrupt Service Routine for TIMER1_CAPT_vect. In the ISR, read ICR1, switch the edge detection polarity (to catch the falling edge), and calculate the pulse width.

For exact register mappings and timing diagrams, always refer to the official Microchip ATmega328P Datasheet, specifically Section 16 on 16-bit Timer/Counter1.

Modern Alternatives: ESP32 and the PCNT Peripheral

As of 2026, the industry has heavily shifted toward 32-bit architectures like the ESP32-S3 and the Renesas RA4M1 (found in the Arduino Uno R4 Minima). If you are using an ESP32, you should completely abandon software timers for PWM reading and utilize the dedicated Pulse Counter (PCNT) peripheral.

The PCNT module operates entirely independently of the CPU cores. You configure it to increment on rising edges and decrement on falling edges, or simply count pulses within a specific time window. According to the Espressif PCNT API Reference, this peripheral can handle signal frequencies up to 40 MHz, making it virtually un-jammable by standard PWM signals. This is the preferred method for reading industrial flow sensors and high-speed optical encoders in modern IoT deployments.

Hardware Wiring & Signal Integrity Troubleshooting

Even with perfect code, noisy hardware will result in erratic PWM readings. Microcontrollers are highly sensitive to floating voltages and electromagnetic interference (EMI).

  • The Floating Ground Problem: If your PWM source is disconnected while the Arduino is powered, the input pin floats, acting as an antenna and triggering hundreds of false interrupts per second. Fix: Always install a 10kΩ pull-down resistor between the PWM input pin and GND.
  • Logic Level Shifting: Never feed a 12V automotive or 24V industrial PWM signal directly into a 5V or 3.3V Arduino pin. You will instantly destroy the GPIO clamping diodes. Fix: Use a high-speed optocoupler (like the 6N137) or a bidirectional logic level shifter (like the TXS0108E) to isolate the domains.
  • High-Frequency Noise: When reading PWM from brushed DC motors or alternators, inductive kickback creates massive voltage spikes. Fix: Place a 100nF ceramic bypass capacitor directly across the signal and ground lines at the Arduino header, and consider a 5.1V Zener diode clamp for overvoltage protection.

Frequently Asked Questions

Can I use a multimeter to read PWM?

Standard multimeters average the voltage over time. If you measure a 5V PWM signal at a 50% duty cycle, your multimeter will display ~2.5V DC. While this confirms the signal is present, it cannot tell you the frequency or verify if the duty cycle is stable. You must use an oscilloscope or a logic analyzer to properly debug PWM waveforms.

Why does my RC receiver output 1000 to 2000 microseconds?

Standard hobby RC protocols (PPM/PWM) do not use a continuous high-frequency square wave. Instead, they send a single pulse every 20ms (50Hz). The width of that pulse (ranging from 1000µs to 2000µs) represents the throttle or steering position. This is why pulseIn() works perfectly for RC receivers but fails for PC fan tachometers.

My Input Capture ISR is missing pulses. Why?

If your ISR takes too long to execute (e.g., you are using Serial.println() inside the interrupt), the hardware will flag an Input Capture Overflow (TOV1). Keep your ISR under 5 microseconds. Read the register, save the value to a volatile variable, set a boolean flag, and exit immediately. Process the math in the main loop().