Anton Shagaev
Published

Active NVMe Cooling Mod for HPE MicroServer Gen10+

A completely modular, internal 12V PWM cooling solution using Seeeduino and SATA power to eliminate NVMe thermal throttling.

AdvancedFull instructions provided22
Active NVMe Cooling Mod for HPE MicroServer Gen10+

Things used in this project

Hardware components

1.6TB Enterprise NVMe Samsung PM1725B [MZPLL1T6HAJQ-00005]
×1
Seeeduino Seeed Studio XIAO ESP32-C3 Microcontroller
×1
USB-A to USB-C Adapter
×1
Noctua NF-A6X15 Fan
×1
4-Pin Molex to 4 PWM Connecting Cable
×1
2-pin x 2-pin Jumper Wires
×1
Cablexpert CC-SATAMF-715-50CM Data Cable
×1
M3 Standoff/Nut Set (7mm length)
×1
3D-printed Air Duct
×1

Story

Read more

Custom parts and enclosures

Air duct STL

Code

Seeeduino sketch

Arduino
#define FAN_PWM   4  // PWM on GPIO4
#define FAN_TACH  3  // Tachometer on GPIO3

const int pwmFreq = 25000;     // PWM Frequency (Hz)
const int pwmResolution = 8;   // PWM Resolution (bits)
volatile unsigned long rpmPulses = 0;  // Number of pulses from tachometer
unsigned long lastDataTime = 0;        // Time of last temperature receipt
bool emergencyMode = false;             // Emergency mode (no data)
const int pulsesPerRev = 2;             // Tachometer pulses per revolution

int currentPwm = 25;  // Current PWM
int targetPwm = 25;   // Target PWM

// Curve defining relationship between temperature and fan PWM
struct FanCurvePoint {
  int temp;
  int pwm;
} fanCurve[] = {
  {30, 25},
  {45, 40},
  {55, 120},
  {65, 200},
  {75, 255}
};

const int curvePoints = sizeof(fanCurve) / sizeof(fanCurve[0]);

// Interrupt handler for counting tachometer pulses
void IRAM_ATTR countRPM() {
  rpmPulses++;
}
// End: tachometer pulse counting function

void setup() {
  Serial.begin(115200);
  delay(500);

  // PWM setup: using ledcAttach() according to Espressif Arduino 3.x
  ledcAttach(FAN_PWM, pwmFreq, pwmResolution);

  // Set initial PWM value
  ledcWrite(FAN_PWM, currentPwm);

  // Tachometer input setup with pull-up resistor
  pinMode(FAN_TACH, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(FAN_TACH), countRPM, FALLING);

  lastDataTime = millis();
  Serial.println("=== FAN Controller started ===");
}
// End: initialization and setup

void loop() {
  // Read data from Serial to get temperature
  if (Serial.available()) {
    String input = Serial.readStringUntil('\n');
    input.trim();
    if (input.startsWith("temperature")) {
      int temp = input.substring(input.indexOf(':') + 1).toInt();
      if (temp > 0 && temp < 120) {
        targetPwm = mapTemperatureToPwm(temp);
        lastDataTime = millis();
        emergencyMode = false;
      } else {
        Serial.printf("Invalid temp: %d\n", temp);
      }
    }
  }

  // If no data received for over 8 seconds β€” enable emergency mode with max PWM
  if (millis() - lastDataTime > 8000) {
    emergencyMode = true;
    targetPwm = 255;
    Serial.println("EMERGENCY: No data from NVMe!");
  }

  smoothFanSpeed();

  // Output RPM to Serial every 2 seconds
  static unsigned long lastRpmTime = 0;
  if (millis() - lastRpmTime > 2000) {
    detachInterrupt(digitalPinToInterrupt(FAN_TACH));
    unsigned long rpm = (rpmPulses * (60000 / 2000)) / pulsesPerRev;
    Serial.printf("RPM: %lu (pulses: %lu)\n", rpm, rpmPulses);
    rpmPulses = 0;
    attachInterrupt(digitalPinToInterrupt(FAN_TACH), countRPM, FALLING);
    lastRpmTime = millis();
  }
}
// End: main processing loop

// Function that returns PWM based on temperature according to the curve,
// turns off the fan at temperatures below 30,
// and prevents PWM from dropping below 24 when the fan is on.
int mapTemperatureToPwm(int temp) {
  if (temp < 30) return 0;  // Fan off when temp < 30Β°C
  for (int i = 0; i < curvePoints - 1; i++) {
    if (temp <= fanCurve[i + 1].temp) {
      int pwm = map(temp, fanCurve[i].temp, fanCurve[i + 1].temp,
                    fanCurve[i].pwm, fanCurve[i + 1].pwm);
      if (pwm < 24) pwm = 24;  // Minimum PWM for stable fan startup
      return pwm;
    }
  }
  return fanCurve[curvePoints - 1].pwm;
}
// End: temperature to PWM mapping function

// Function to smoothly change fan speed to target value
// Minimum PWM 24 if targetPwm is not 0, otherwise full stop (0)
void smoothFanSpeed() {
  if (targetPwm < 24) {
    // If target is below startup threshold β€” turn fan off immediately
    currentPwm = 0;
  } else {
    // Smoothly approach targetPwm
    if (currentPwm < targetPwm) {
      currentPwm += 5;
      if (currentPwm > targetPwm) currentPwm = targetPwm;
    } else if (currentPwm > targetPwm) {
      currentPwm -= 2;
      if (currentPwm < targetPwm) currentPwm = targetPwm;
    }
    // Minimum threshold for startup and maintenance
    if (currentPwm < 24) currentPwm = 24;
  }
  ledcWrite(FAN_PWM, currentPwm);
  Serial.printf("PWM set to: %d\n", currentPwm);
}
// End: smooth fan speed change

Temperature sender

SH
#!/bin/bash

DEVICE="/dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_XX:XX:XX:XX:XX:XX-if00"
NVME_DEVICE="/dev/nvme0n1"

while true; do
    # Wait for ESP32 device to appear
    while [ ! -e "$DEVICE" ]; do
        echo "[WARN] ESP32 not found, waiting for connection..."
        sleep 2
    done

    echo "[INFO] Connecting to $DEVICE"

    # Open device for writing via descriptor 3
    exec 3> "$DEVICE"

    while true; do
        # If device is disconnected β€” exit inner loop to reconnect
        if [ ! -e "$DEVICE" ]; then
            echo "[WARN] ESP32 disconnected, restarting..."
            exec 3>&-
            break
        fi

        # Read NVMe temperature
        TEMP=$(nvme smart-log "$NVME_DEVICE" 2>/dev/null | awk '/^temperature/ {print $3; exit}')

        if [[ -z "$TEMP" ]]; then
            echo "[ERROR] Failed to get NVMe temperature!"
            TEMP=0
        fi

        # Send string to ESP32
        echo "temperature: $TEMP" >&3
        sleep 2
    done
done

Credits

Anton Shagaev
1 project β€’ 0 followers

Comments