Tinkster Logo
expertLinux & Homelab

DIY NVMe Cooling for the HPE ProLiant Gen 10 Plus Microserver

Author
Savva
Tashkent, UZ
3 days
245
18
Cover
Hello friends! Today we will talk about how to upgrade the HPE Proliant Gen 10 Plus microserver by installing a server NVMe drive from Samsung (model PM1725B, 1600 GB) . NVMe SSDs are very fast compared to SATA SSDs (10-12 times faster), and I recently learned that it is possible to install a Samsung HHHL (half-height, half-length) NVMe SSD into the PCIe slot of my server and subsequently install Linux on it (this will be the topic of the next guide).
However, the problem with NVMe SSDs is that they get very hot (this drive was already at 57 degrees Celsius in idle mode) . It was decided to build a custom cooling system with a PWM fan because the server motherboard has no free PWM headers.

Steps

1

Installing the Server NVMe Drive

Metal bracket
Install the Samsung PM1725B server NVMe drive into the PCIe4 x16 slot, having first removed the iron bracket from the Riser board, as it will interfere with the installation of the PWM fan .
Turn on the server and check the UEFI to see if the drive is detected.
If everything is fine, boot into Linux and check the NVMe drive temperature with the command (path to NVMe /dev/nvme0n1, yours may differ):
1
watch -n 2 'nvme smart-log /dev/nvme0n1 | grep -i temperature'
In my case, it was already 57 degrees in idle mode, which is quite high. Turn off the server.
2

Flashing the Seeeduino Microcontroller

Seeeduino microcontroller
We will install the Seeeduino microcontroller with the adapter into the free USB 3.2 Gen 1 port inside the Proliant Gen 10 Plus microserver, originally intended for a boot flash drive .
Flash the controller with the following sketch in the Arduino IDE:
1
#define FAN_PWM 4 // PWM on GPIO4
2
#define FAN_TACH 3 // Tachometer on GPIO3
3
4
const int pwmFreq = 25000; // PWM Frequency (Hz)
5
const int pwmResolution = 8; // PWM Resolution (bits)
6
volatile unsigned long rpmPulses = 0; // Number of pulses from tachometer
7
unsigned long lastDataTime = 0; // Time of last temperature receipt
8
bool emergencyMode = false; // Emergency mode (no data)
9
const int pulsesPerRev = 2; // Tachometer pulses per revolution
10
11
int currentPwm = 25; // Current PWM
12
int targetPwm = 25; // Target PWM
13
14
// Curve defining relationship between temperature and fan PWM
15
struct FanCurvePoint {
16
int temp;
17
int pwm;
18
} fanCurve[] = {
19
{30, 25},
20
{45, 40},
21
{55, 120},
22
{65, 200},
23
{75, 255}
24
};
25
26
const int curvePoints = sizeof(fanCurve) / sizeof(fanCurve[0]);
27
28
// Interrupt handler for counting tachometer pulses
29
void IRAM_ATTR countRPM() {
30
rpmPulses++;
31
}
32
// End: tachometer pulse counting function
33
34
void setup() {
35
Serial.begin(115200);
36
delay(500);
37
38
// PWM setup: using ledcAttach() according to Espressif Arduino 3.x
39
ledcAttach(FAN_PWM, pwmFreq, pwmResolution);
40
41
// Set initial PWM value
42
ledcWrite(FAN_PWM, currentPwm);
43
44
// Tachometer input setup with pull-up resistor
45
pinMode(FAN_TACH, INPUT_PULLUP);
46
attachInterrupt(digitalPinToInterrupt(FAN_TACH), countRPM, FALLING);
47
48
lastDataTime = millis();
49
Serial.println("=== FAN Controller started ===");
50
}
51
// End: initialization and setup
52
53
void loop() {
54
// Read data from Serial to get temperature
55
if (Serial.available()) {
56
String input = Serial.readStringUntil('\n');
57
input.trim();
58
if (input.startsWith("temperature")) {
59
int temp = input.substring(input.indexOf(':') + 1).toInt();
60
if (temp > 0 && temp < 120) {
61
targetPwm = mapTemperatureToPwm(temp);
62
lastDataTime = millis();
63
emergencyMode = false;
64
} else {
65
Serial.printf("Invalid temp: %d\n", temp);
66
}
67
}
68
}
69
70
// If no data received for over 8 seconds — enable emergency mode with max PWM
71
if (millis() - lastDataTime > 8000) {
72
emergencyMode = true;
73
targetPwm = 255;
74
Serial.println("EMERGENCY: No data from NVMe!");
75
}
76
77
smoothFanSpeed();
78
79
// Output RPM to Serial every 2 seconds
80
static unsigned long lastRpmTime = 0;
81
if (millis() - lastRpmTime > 2000) {
82
detachInterrupt(digitalPinToInterrupt(FAN_TACH));
83
unsigned long rpm = (rpmPulses * (60000 / 2000)) / pulsesPerRev;
84
Serial.printf("RPM: %lu (pulses: %lu)\n", rpm, rpmPulses);
85
rpmPulses = 0;
86
attachInterrupt(digitalPinToInterrupt(FAN_TACH), countRPM, FALLING);
87
lastRpmTime = millis();
88
}
89
}
90
// End: main processing loop
91
92
// Function that returns PWM based on temperature according to the curve,
93
// turns off the fan at temperatures below 30,
94
// and prevents PWM from dropping below 24 when the fan is on.
95
int mapTemperatureToPwm(int temp) {
96
if (temp < 30) return 0; // Fan off when temp < 30°C
97
for (int i = 0; i < curvePoints - 1; i++) {
98
if (temp <= fanCurve[i + 1].temp) {
99
int pwm = map(temp, fanCurve[i].temp, fanCurve[i + 1].temp,
100
fanCurve[i].pwm, fanCurve[i + 1].pwm);
101
if (pwm < 24) pwm = 24; // Minimum PWM for stable fan startup
102
return pwm;
103
}
104
}
105
return fanCurve[curvePoints - 1].pwm;
106
}
107
// End: temperature to PWM mapping function
108
109
// Function to smoothly change fan speed to target value
110
// Minimum PWM 24 if targetPwm is not 0, otherwise full stop (0)
111
void smoothFanSpeed() {
112
if (targetPwm < 24) {
113
// If target is below startup threshold — turn fan off immediately
114
currentPwm = 0;
115
} else {
116
// Smoothly approach targetPwm
117
if (currentPwm < targetPwm) {
118
currentPwm += 5;
119
if (currentPwm > targetPwm) currentPwm = targetPwm;
120
} else if (currentPwm > targetPwm) {
121
currentPwm -= 2;
122
if (currentPwm < targetPwm) currentPwm = targetPwm;
123
}
124
// Minimum threshold for startup and maintenance
125
if (currentPwm < 24) currentPwm = 24;
126
}
127
ledcWrite(FAN_PWM, currentPwm);
128
Serial.printf("PWM set to: %d\n", currentPwm);
129
}
130
// End: smooth fan speed change
This sketch controls the speed of a 4-wire fan with PWM control and tachometer feedback. It regulates fan rotation speed depending on the temperature received via the serial port from the NVMe drive, using a preset temperature curve. The sketch includes an emergency mode with maximum RPM if data is missing for more than 8 seconds, provides smooth speed changes, and periodically outputs current RPM readings to the port monitor .
Next, turn on the server.
In Linux, create the file nvme_temp_sender.sh:
1
# nano /usr/local/bin/nvme_temp_sender.sh
nvme_temp_sender.sh
1
#!/bin/bash
2
3
DEVICE="/dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_XX:XX:XX:XX:XX:XX-if00"
4
NVME_DEVICE="/dev/nvme0n1"
5
6
while true; do
7
# Wait for ESP32 device to appear
8
while [ ! -e "$DEVICE" ]; do
9
echo "[WARN] ESP32 not found, waiting for connection..."
10
sleep 2
11
done
12
13
echo "[INFO] Connecting to $DEVICE"
14
15
# Open device for writing via descriptor 3
16
exec 3> "$DEVICE"
17
18
while true; do
19
# If device is disconnected — exit inner loop to reconnect
20
if [ ! -e "$DEVICE" ]; then
21
echo "[WARN] ESP32 disconnected, restarting..."
22
exec 3>&-
23
break
24
fi
25
26
# Read NVMe temperature
27
TEMP=$(nvme smart-log "$NVME_DEVICE" 2>/dev/null | awk '/^temperature/ {print $3; exit}')
28
29
if [[ -z "$TEMP" ]]; then
30
echo "[ERROR] Failed to get NVMe temperature!"
31
TEMP=0
32
fi
33
34
# Send string to ESP32
35
echo "temperature: $TEMP" >&3
36
sleep 2
37
done
38
done
The path to the Seeeduino (DEVICE="/dev/serial/by-id/usb-Espressif_USB_JTAG_serial_debug_unit_XX:XX:XX:XX:XX:XX-if00") will be unique to you; find it in /dev/serial/by-id/. Also, the path to the NVMe (NVME_DEVICE="/dev/nvme0n1") might be different for you .
Save the file and make it executable:
1
# chmod +x /usr/local/bin/nvme_temp_sender.sh
Next, create the service nvme_temp_monitor.service:
nvme_temp_monitor.service
1
# nano /etc/systemd/system/nvme_temp_monitor.service
And paste the following into the file:
nvme_temp_monitor.service
1
[Unit]
2
Description=NVMe Temperature Monitor for ESP32 over USB Serial
3
After=network.target
4
5
[Service]
6
Type=simple
7
User=root
8
ExecStart=/usr/local/bin/nvme_temp_sender.sh
9
Restart=always
10
RestartSec=5
11
StandardOutput=syslog
12
StandardError=syslog
13
SyslogIdentifier=nvme_temp_monitor
14
15
[Install]
16
WantedBy=multi-user.target
Save the file and reload all systemd unit files without restarting systemd itself:
1
# systemctl daemon-reload
Enable autorun for the nvme_temp_monitor service in the current session:
1
# systemctl enable --now nvme_temp_monitor.service
Next, start the nvme_temp_monitor service:
1
# systemctl start nvme_temp_monitor
Briefly, the nvme_temp_monitor service runs a script (nvme_temp_sender.sh) that reads the NVMe drive temperature every 2 seconds and sends it via the USB serial port to the Seeeduino. If the Seeeduino is unplugged/plugged back in, the script reconnects automatically.
3

Connecting the Seeeduino Microcontroller to the PWM Fan

Connecting the Seeeduino microcontroller to the PWM fan and 12-volt power.
How it looks in the microserver
Insert the microcontroller into the USB 3.2 Gen 1 port. Take a 2-pin female connector and solder the black wire to D1 (GPIO 3) and the red wire to D2 (GPIO 4) on the microcontroller. Then we need to cut the connecting wires from the 4-Pin Molex that go to the white 4 PWM connector (yellow and black wires). From the black 4 PWM connector, physically pull out the black and red wires with their locking pins (you will have to ruin one black 4 PWM connector and cut the wires from the 4-Pin Molex to do this) and insert the locking pins into the free holes of the white 4 PWM connector (black on the left, as this is PWM).
Next, solder the black and red wires of the white 4 PWM connector to the corresponding colored wires of a 2-pin male connector and connect it to the 2-pin female on the microcontroller. Use heat shrink tubing on all wire solder joints.
We will take 12-volt power for the PWM fan from the SATA 7+15pin connector on the server from Bay 4. Take the data cable (Cablexpert CC-SATAMF-715-50CM) and cut off the yellow (12 volts) and two black wires (Ground) from the female connector. Insulate the cut areas of the wires from the female connector and all contacts of the female connector using hot glue. Insert the male connector of the cable into the SATA 7+15pin female in Bay 4.
I had to sacrifice Bay 4 to get power from SATA, but in my disk configuration, it was always empty, so this did not affect anything. Solder the yellow and black wires from the connector to the wires of the corresponding color on the white 4 PWM connector. Solder the second black wire to Ground on the microcontroller, also via a 2-pin connector (snipping off the second red wire from it, as it is not needed). This is not mandatory, as Ground on SATA and Ground on the microcontroller (via USB-A) should be common. I tried to do everything via connectors so that it would be convenient to connect/disconnect everything later.
Place the PWM fan itself on the NVMe drive heatsink in "blow" mode (i.e., blowing onto the heatsink). Also, remove the blanking plate from the rear wall of the server intended for iLO. Air intake will be carried out from this cutout on the rear wall.
4

Testing the PWM Fan

Turn on the server and check the RPM and PWM signal with the command:
1
cat /dev/ttyACM0 | awk '/RPM:/ {rpm=$2} /PWM set to:/ {pwm=$4} rpm && pwm {print "RPM=" rpm, "PWM=" pwm}'
If everything is fine, you should see something like this:
1
RPM=1410 PWM=154
2
RPM=1410 PWM=154
3
RPM=1410 PWM=159
4
RPM=1800 PWM=159
5
RPM=1800 PWM=159
6
RPM=1800 PWM=159
You should see changes in RPM and PWM.
Also, check the temperature with the command:
1
watch -n 2 'nvme smart-log /dev/nvme0n1 | grep -i temperature'
You should see something similar to:
1
temperature : 44 C
2
Warning Temperature Time : 0
3
Critical Composite Temperature Time : 0
4
Temperature Sensor 1 : 44 C
5
Temperature Sensor 2 : 43 C
6
Temperature Sensor 3 : 43 C
In my case, the temperature dropped by 13 degrees in idle mode, which is an excellent result. But we will see later how the temperature behaves under load.
5

Air Duct Design

STL model of the air duct
Finished part
Air intake occurs through the cutout from the HHHL blanking plate
How it looks in the microserver
To make the airflow even more effective, I designed an air duct for my fan in Solidworks. After printing the air duct, I installed a long M3 nut into the hexagonal recess (having previously coated its surface with Poxipol two-component glue). When the glue dried after 10 minutes, I secured the air duct in place of the iLO blanking plate using an M3 screw and fastened the air duct to the fan using connecting screws

Conclusion

I also recommend installing the HPE Proliant Gen 10 Plus microserver in a vertical position (rubber anti-slip pads are included in the kit for this) so that the air intake for the NVMe is at the bottom, and the exhaust of the main cooler fan is at the top.

Discussion (0)

No comments yet. Be the first!

Maker

Avatar
Savva
Tashkent, UZ

Anton is the Managing Partner of Tinkster. He studied oil and gas engineering in the United States and also holds two honors degrees from Tomsk Polytechnic University.