Configuring Arduino 25 kHz PWM Fan Control in LabVIEW

Brian Holt9 min read
Other ManufacturerOther TopicTechnical Reference
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

The fan wants a PWM input near 25 kHz, and the Arduino's stock PWM pins put out 490 Hz, so the fan never sees a command it recognizes. A second fault sits on top of it: the LabVIEW diagram re-sends the LINX frequency and duty setup inside an outer While loop. Work the checks below in order. Each one names a reading and tells you which check comes next.

Measure the frequency at the fan input pin first

Put a scope or frequency counter on the PWM signal pin, with the ground clip on the Arduino ground. Do not guess from fan behavior. A fan that idles, surges, or ignores duty changes tells you nothing about frequency.

Reading Meaning Next check
About 490 Hz, stable Default analogWrite-style PWM. The LINX PWM write does not change timer frequency. Check 4 (decide if you need exactly 25 kHz), then check 5
Unstable, drops out, or restarts every few hundred ms Setup commands are being re-sent continuously Check 2 (loop)
Flat 0 V or flat 5 V Nothing configured, wrong pin, or setup command failing Check 3 (block settings)
Near 25 kHz with correct duty, fan still dead Firmware and diagram are fine. The problem is the fan side. Check the fan wiring section below

Delete the While loop around the LINX setup calls

The quick fixes people try first are a longer loop wait, a different duty value, or a different pin. None of them work, because the fault is structural. A While loop around open, set frequency, set duty, close makes LabVIEW hammer the Arduino with setup commands. The board spends its time servicing configuration and never gets to run the output it was asked for.

That pattern comes from the motor control portion, where a loop is correct: a servo or motor position must be updated continuously. A fan PWM output is different. You configure it once and the hardware keeps producing it.

  1. Delete the outer While loop.
  2. Wire the diagram to run once: open the connection, configure frequency and duty, close.
  3. Rescan the pin. If the signal is now stable, the loop was the fault.

This is the temporary restore. It gets the fan turning at some duty. It does not by itself get you 25 kHz if the LINX block cannot produce it, which is what checks 3 to 5 decide.

Check the square wave write block: frequency, duration, and pin

The diagram uses the LINX square wave write block to set frequency, with a duration of 0 intended to mean run indefinitely. Read the block's context help in your LINX version to confirm what duration 0 means for your target. Then read the pin.

  • Frequency input: confirm the number wired is in the unit the block expects. A unit mismatch produces a wrong-but-steady frequency on the scope.
  • Duty for the 10% test case: confirm it is wired as a percentage or fraction as the block defines it, not as a 0-255 count.
  • Output frequency ceiling: if the scope shows a steady frequency well below what you commanded, the block cannot generate that rate on your board. Stop tuning the block and go to check 5.

If the firmware side prints debug text to serial, remove it. Debug print lines steal time from the loop, and they matter most when you want the fastest output rate. Turn them off for any full-speed run.

Decide whether you need exactly 25 kHz

25 kHz is the fan's recommended PWM frequency, not a hard requirement. Read the fan datasheet for the accepted PWM input frequency range, the input logic level, and whether the input expects an open-collector or pulled-up drive. If a range is listed and it includes a frequency your timer can produce cleanly, you can accept a value slightly off 25 kHz. You still have to change timer registers, because 490 Hz is outside any 25 kHz-class fan input range.

Option Reaches about 25 kHz? Cost and risk
Timer register change (prescaler and top value) Yes. Certain Arduino PWM pins reach about 62 kHz, so 25 kHz is within reach. Low-level register work. Can collide with other timer users (see pitfalls).
Bit-banged GPIO: high for x µs, low for 40-x µs Nominally, since 40 µs is a 25 kHz period Quickest and dirtiest. Jitter from serial handling. Ties up the CPU.
I2C-controlled PWM or fan controller chip Per the chip datasheet Extra hardware. Offloads timing from the Arduino. Best for a long-term project.
Move to a Mega Same timer method, more pins and timers Buy it if the project keeps growing. Read the timer table in the Mega datasheet for pin-to-timer mapping.

Reconfigure the hardware timer for 25 kHz

A timer register change is the permanent repair for a bench wind tunnel. The hardware generates the waveform with no CPU time, so serial traffic and LabVIEW timing cannot disturb it. The mechanism: output frequency equals the timer clock divided by the prescaler times the counter top value. Pick a mode where you control the top value, and 25 kHz falls out of the arithmetic.

Assumptions for the numbers below: an ATmega328P-class board with a 16 MHz clock, fan PWM input on pin 9 (OC1A, Timer1). If your board differs, read the timer section of its datasheet and recompute.

f_out = 16,000,000 / (prescaler * (ICR1 + 1))
prescaler = 1, ICR1 = 639  ->  16,000,000 / 640 = 25,000 Hz
duty      = OCR1A / (ICR1 + 1)
OCR1A = 64  ->  64 / 640 = 10 %
void setup() {
  pinMode(9, OUTPUT);
  TCCR1A = _BV(COM1A1) | _BV(WGM11);              // fast PWM, mode 14, OC1A non-inverting
  TCCR1B = _BV(WGM13) | _BV(WGM12) | _BV(CS10);   // prescaler 1
  ICR1  = 639;                                    // 25 kHz
  OCR1A = 0;
  Serial.begin(115200);                           // match the VISA serial setting
}

void loop() {
  if (Serial.available()) {
    int c = Serial.read();
    if (c >= '0' && c <= ':') {                // '0'..':' = 0..10 (ASCII minus 48)
      OCR1A = (c - '0') * 64;                     // 0..640, 10 steps of 10 %
    }
  }
}

Do not change Timer0. It normally drives the Arduino time base, and altering it breaks delay() and millis(). If you want a range near 62 kHz instead of exactly 25 kHz, an 8-bit fast PWM at prescaler 1 on 16 MHz gives 16,000,000 / 256 = 62,500 Hz, which matches the roughly 62 kHz ceiling.

Send duty from LabVIEW over VISA serial

This replaces the LINX PWM block for the fan. The Arduino owns the timing, and LabVIEW only sends a duty command.

  1. Flash the sketch above with the Arduino IDE. Do not run LINX firmware on the same board for this fan unless you merge the two.
  2. In LabVIEW, configure VISA Configure Serial Port with the same baud rate as Serial.begin.
  3. Put a VISA Write inside an event structure triggered by a value change on a numeric control. The event loop is the correct use of a loop here: it waits on the control and writes only on change.
  4. Convert the control value to a single character by adding 48, for example 0 sends 0 and 10 sends :.
  5. Open the port once before the event loop and close it after the loop ends.

If you keep the bit-bang approach instead, the sequence is GPIO high, wait x µs, GPIO low, wait 40-x µs, with x set from the serial terminal by the same subtract-48 character trick. Expect visible jitter whenever the loop reads serial data, and expect it to worsen if debug prints are left on.

Avoid the pitfalls that reset or cap the output

Symptom Cause Fix
Fan stops when LabVIEW opens or reopens the port Many Uno-class boards reset when the serial port opens, which returns timers to default Open the port once. Send the first duty command after the board finishes resetting. A board that resets on port open will start with the fan input low.
Duty tops out near 40% after timer change An analogWrite call writes an 8-bit value (0-255) into OCR1A, and top is now 639, so 255/640 is about 40% Write OCR1A directly as in the sketch. Do not use LINX PWM write on this pin.
Airfoil motor misbehaves after timer change If the airfoil actuator is a servo, the Servo library uses Timer1 on Uno-class boards. That collides with the register configuration above. Move the fan to a pin on a different timer, or move to a Mega and use a free timer. Confirm against the datasheet pin table.
Scope shows correct 25 kHz, fan does not respond Wrong logic level, no common ground, or fan power drawn from the Arduino Power the fan from its own supply. Tie grounds together. The Arduino pin only drives the PWM input.

Confirm 25 kHz, duty, and persistence before you leave the bench

  1. With the fan disconnected, scope pin 9. Confirm about 25 kHz, 5 V logic level, and duty at the value you commanded. In the sketch above, command 1 for 10%.
  2. Step the duty from 0 to 10. Confirm duty changes in even increments and the frequency does not move.
  3. Reconnect the fan. Confirm speed rises monotonically with duty.
  4. Close the LabVIEW VI and confirm the output holds. If it drops, revisit the port-open reset behavior in the pitfalls table.
  5. Run at full duty for several minutes and confirm no dropout with debug prints removed.

FAQ

Can I get 25 kHz PWM from the Arduino's default PWM pins?

Not with the default setting, which is 490 Hz. Change the timer prescaler and top value: with a 16 MHz clock, prescaler 1, and ICR1 = 639 on Timer1, you get 25 kHz. Certain pins can reach about 62 kHz.

Does the LabVIEW square wave write block need to sit in a While loop?

No. Run open, configure frequency and duty, and close once. A While loop re-sends setup commands continuously and prevents the Arduino from producing the output you asked for.

Can I run the fan at a frequency slightly off 25 kHz?

Yes, if the fan datasheet input frequency range allows it. 25 kHz is the recommended value, not a strict requirement, but you still need to change timer registers because 490 Hz is far outside that range.

Can I keep troubleshooting if the scope shows 25 kHz and the fan still does not spin?

Stop after you confirm fan power, common ground, and input logic level against the fan datasheet. Contact the fan manufacturer's official support with the datasheet PWM input specification and your scope capture. For LINX behavior, use the official NI or Digilent support channels for that library.

Back to blog