If you need a precise, jitter-free square wave to trigger an oscilloscope, drive a stepper motor, or test a logic circuit, bypass delay() and use hardware timers. To generate a strict 10kHz pulse on an Arduino Uno R3 (ATmega328P), configure Timer1 in CTC (Clear Timer on Compare) mode with a prescaler of 1, and set the OCR1A register to 1599. This yields a hardware-toggled output on Pin 9 with zero software interrupt latency.
Below is the complete decision framework, bare-metal C++ code, and bench-level debugging guide to get your signal clean on the first try.
The Quick Decision: Which Arduino Pulse Generator Method to Use?
Not all pulses are created equal. The method you choose dictates your frequency ceiling and jitter profile. Use this decision tree to lock in your approach before writing a single line of code.
| Target Frequency | Jitter Tolerance | Recommended Method | Concrete Implementation |
|---|---|---|---|
| < 500 Hz | < 50 µs | Software Delay | delayMicroseconds() in loop() |
| 500 Hz - 65 kHz | < 1 µs (Strict) | Hardware Timer CTC | Timer1 CTC with OC1A hardware toggle (Use this build) |
| > 65 kHz | < 100 ns | Direct Port + Timer or MCU Swap | Switch to ESP32 MCPWM or ATmega Timer1 Fast PWM |
Why Software Toggling Fails (And Why Hardware CTC Wins)
A common mistake is using a timer interrupt to fire an ISR (Interrupt Service Routine) that calls digitalWrite(pin, !state). While this works for blinking LEDs, it fails on the oscilloscope bench. Calling digitalWrite() inside an ISR takes roughly 3-5 µs. If your main loop triggers a higher-priority interrupt (like UART serial reception), your pulse edge gets delayed, resulting in visible jitter on the scope.
By using the COM1A0 bit in the TCCR1A register, we instruct the ATmega328P silicon to physically toggle the output pin the exact nanosecond the timer matches the compare register. The CPU isn't involved. The jitter drops to literally zero (bounded only by the 16MHz crystal's stability, typically ±50ppm).
Parts List and Pin Mapping for Hardware Timer Pulses
This build targets the standard 5V logic architecture. If you are probing high-speed edges, your physical probing technique matters as much as the code.
| Component | Exact Variant / Model | Role in Build |
|---|---|---|
| Microcontroller | Arduino Uno R3 (ATmega328P, 16MHz) | Pulse generation via Timer1 |
| Oscilloscope | Rigol DS1054Z or Siglent SDS1104X-E | Signal verification (min 50MHz bandwidth) |
| Probe Accessory | Ground Spring (replace alligator clip) | Eliminates ground-loop ringing on fast edges |
| Logic Buffer (Optional) | 74HC14 Hex Inverter (Schmitt Trigger) | Signal conditioning if driving long coax cables |
Pin Mapping
| ATmega328P Pin | Arduino Uno Header | Function | Connection |
|---|---|---|---|
| OC1A (Port B1) | Pin 9 | Primary Pulse Output | Scope Probe Tip |
| OC1B (Port B2) | Pin 10 | Secondary/Inverse Pulse | Optional (Leave floating) |
| GND | GND | Logic Ground | Scope Ground Spring |
Bare-Metal Timer1 Code: Generating a Precision 10kHz Pulse
The following code is written strictly for the Arduino Uno R3 (ATmega328P) and Arduino Nano (ATmega328P). It will not compile for the ESP32 or Arduino Uno R4 Minima, as their timer architectures are entirely different. No external libraries are required; this writes directly to the hardware registers.
// Target Board: Arduino Uno R3 / Nano (ATmega328P @ 16MHz)
// Objective: 10kHz Square Wave, 50% Duty Cycle, Zero-Jitter
const uint16_t TARGET_FREQ_HZ = 10000;
const uint32_t CPU_CLOCK_HZ = 16000000UL;
const uint16_t PRESCALER = 1;
// Calculate Compare Match Register value
// Formula: (Clock / (Prescaler * Freq)) - 1
const uint16_t OCR1A_VALUE = (CPU_CLOCK_HZ / (PRESCALER * TARGET_FREQ_HZ)) - 1;
void setup() {
// Safety check: Halt if the math resulted in an overflow for the 16-bit register
if (OCR1A_VALUE > 65535) {
pinMode(LED_BUILTIN, OUTPUT);
while(1) {
digitalWrite(LED_BUILTIN, HIGH); delay(100);
digitalWrite(LED_BUILTIN, LOW); delay(100);
} // Blink LED to indicate config error
}
// 1. Configure Pin 9 (OC1A) as an output
DDRB |= (1 << DDB1); // Direct port manipulation for Pin 9
// 2. Reset Timer1 Control Registers to default
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
// 3. Set the Compare Match value
OCR1A = OCR1A_VALUE; // 1599 for 10kHz
// 4. Configure Timer1 behavior
// COM1A0 = 1: Toggle OC1A pin on Compare Match (Hardware toggle, no ISR needed)
// WGM12 = 1: CTC (Clear Timer on Compare) mode
// CS10 = 1: Prescaler = 1 (No prescaling, raw 16MHz clock)
TCCR1A |= (1 << COM1A0);
TCCR1B |= (1 << WGM12) | (1 << CS10);
}
void loop() {
// The main loop is completely free.
// The hardware timer is generating the pulse independently.
// You can run Serial, read sensors, or sleep here without affecting the 10kHz output.
}
Debugging: First Three Checks and Common Compiler Errors
When your oscilloscope shows a flatline, a noisy mess, or the code refuses to compile, follow this strict diagnostic path.
The First Three Physical Checks
- Probe Grounding (The Ringing Illusion): If your 10kHz square wave looks like it has massive 50MHz spikes on the rising edges, your code is fine; your probing is wrong. The standard 6-inch alligator ground clip acts as an antenna. Remove it and use the ground spring attachment directly on the probe tip barrel. Reference SparkFun's oscilloscope guide for proper high-frequency probing techniques.
- Pin Mode Verification: Did you accidentally use
pinMode(9, INPUT)elsewhere in your sketch? The hardware toggle requires the DDR (Data Direction Register) bit to be set to OUTPUT. The code above handles this viaDDRB |= (1 << DDB1);, but ensure no other library overrides it. - Trigger Threshold: Set your oscilloscope trigger level to exactly 2.5V (midpoint of the 5V logic). If the trigger is set to 4.5V, minor voltage drops under load will cause the scope to miss triggers, making the pulse look unstable.
Compiler Error: Multiple Definition of Vector 13
If you add this code to an existing project and hit compile, you might see this exact error string:
multiple definition of `__vector_13'
collect2.exe: error: ld returned 1 exit status
Ranked Causes and Fixes:
- Cause 1 (Most Likely): You have the
<Servo.h>or<IRremote.h>library included in your sketch. Both of these libraries hijack Timer1 and define their own Interrupt Service Routine (ISR) for Vector 13 (Timer1 Compare Match A).
Fix: Remove the conflicting library, or move your pulse generation to Timer2 (which limits your max frequency due to its 8-bit size). - Cause 2: You copied an old tutorial that includes an empty
ISR(TIMER1_COMPA_vect) {}at the bottom of the sketch. Because we are using the hardware toggle bit (COM1A0), we do not need an ISR.
Fix: Delete theISR()block entirely.
Extending the Build: Variable Frequency and Multi-Channel Sync
Once you have a stable 10kHz baseline, you will likely need to adapt the generator for real-world testing. Here is how to extend or simplify the build without breaking the zero-jitter architecture.
Extension: Adding a Variable Frequency Potentiometer
To turn this into a variable frequency generator (e.g., 1kHz to 50kHz), wire a 10kΩ linear potentiometer to 5V, GND, and Analog Pin A0. Add this block to your loop():
void loop() {
// Read ADC (0-1023), map to desired OCR1A range
// 1599 = 10kHz, 319 = 50kHz
int adc_val = analogRead(A0);
uint16_t new_ocr = map(adc_val, 0, 1023, 1599, 319);
// Update the compare register on the fly
OCR1A = new_ocr;
delay(50); // Debounce the physical potentiometer wiper
}
Simplification: When to Use the TimerOne Library
If bare-metal register manipulation feels brittle for your project, or you need to attach a software callback to the pulse event, switch to the TimerOne library. It abstracts the math. However, be aware that the library uses an ISR to toggle the pin, which reintroduces roughly 2µs of jitter. For driving a stepper motor driver (like a TB6600), the library is perfectly adequate. For triggering a high-speed ADC or time-of-flight sensor, stick to the bare-metal CTC hardware toggle provided above.






