Greetings everyone, and welcome back.
In this project, I modify a LattePanda Delta by replacing its original cooling system with a custom, ducted fan solution. The build uses a high-speed 12 V server-grade fan mounted above the existing heatsink, along with a custom enclosure designed to direct airflow efficiently through the heatsink fins.
For the control electronics, a Waveshare ESP32-S3 1.47-inch display is used, paired with a custom switch PCB that controls the server fan and provides real-time feedback.
The cooling module is integrated into a portable stand that holds the LattePanda Delta securely and includes an onboard display for showing fan RPM and operating mode, along with physical buttons for controlling fan speed.
This article covers the complete build process, from the mechanical design and airflow considerations to the electronics, fan control system, and final assembly.
MATERIAL REQUIREDThese were the materials used in this build:
- Lattepanda MU with full evaluation board
- Custom Switch PCB (provided by PCBWAY)
- PF40281B1-000U-S99 Server BLDC FAN 12V
- ESP32 S3 1.47 Inch Waveshare Display
- DFROBOT Rainbow Link
- 3D-printed parts
- DC-DC Buck Converter
This is my very first LattePanda, which I’ve been using regularly since 2022. A couple of weeks ago, its onboard cooling fan completely stopped working. I’ve used this LattePanda in several previous projects, and apart from the cooling system, the processor and all other electronics were still functioning perfectly—the only failure was the cooling section itself.
Rather than waiting for a replacement cooling unit, I decided to investigate the issue and design my own solution.
As a first step, I tested the original BLDC fan by supplying it with 12 V, ground, and a PWM signal on the control wire, but the fan did not respond. To rule out a faulty fan, I connected a different 12 V BLDC fan with a JST connector to the onboard fan header, but that fan also failed to operate.
From this, I concluded that something had gone wrong with the onboard fan-driving circuitry itself—likely due to damage or a short—rendering the original cooling control system unusable.
At that point, the only practical solution was to build an externally powered and externally controlled cooling system, which ultimately led to this project.
FAN TEARDOWNWe began the project by tearing down the original cooling fan assembly. The process was straightforward at first but became more tedious as we progressed. The teardown started by unplugging the fan connector from the board. Next, we removed the four M2 screws securing the top plastic cover. After that, three additional M2 screws were removed from the underside, which released the heatsink from its mounting position. This allowed the entire heatsink assembly to be removed from the board.
Once the heatsink was removed, both the heatsink base and the processor surface were cleaned using isopropyl alcohol to remove the old thermal paste. Care was taken not to apply excessive pressure while cleaning the processor, as doing so could dislodge nearby SMD components and permanently damage the board.
With the heatsink cleaned, we proceeded to remove the onboard fan from it. The fan was disassembled by first removing the rotor, followed by the stator assembly. The stator was connected to a small driver PCB, which was also removed completely, along with the brass bushing mounted in the heatsink.
After stripping away all fan-related components, we were left with a bare heatsink. This heatsink would later be reused in combination with a custom-designed airflow duct and an external server fan as part of the new cooling solution.
EXTERNAL SERVER FAN SETUPFor the cooling system, we reused an older Sunon PF40281B1-000U-S99 tubeaxial DC brushless server fan. This fan operates at 12 V, has a power rating of 6.7 W (relatively low power for a server-class fan), and can reach a maximum speed of 20, 000 RPM, with an airflow rating of 24.9 CFM.
For comparison, a typical 40 mm × 40 mm PC fan delivers around 5.4 CFM, which is significantly lower. This large difference in airflow explains why server-grade fans are far more effective at cooling dense heat sinks. While this fan is noticeably louder than a regular PC fan, the substantial increase in airflow makes it a worthwhile trade-off for this application.
https://www.digikey.in/en/products/detail/sunon-fans/PF40281B1-000U-S99/4840557
The BLDC fan uses a standard four-wire interface:
- Red – VCC (12 V)
- Black – Ground
- Yellow – Tachometer output for RPM feedback
- Blue – PWM input for speed control
For fan control and testing, we used a Seeed Studio XIAO SAMD21 (XIAO M0), although any microcontroller capable of generating a PWM signal and reading a tachometer output can be used. The fan’s ground is connected to the microcontroller ground, the PWM control wire (blue) is connected to GPIO 0, and the tachometer output (yellow wire) is connected to GPIO 1.
To power the fan, we used DFRobot Rainbow Link, a protocol converter board that accepts a USB-C power adapter and provides multiple regulated outputs, including 12 V, 5 V, and 3.3 V. In this setup, the fan’s 12 V supply (red wire) is connected directly to the 12 V output of the Rainbow Link.
The ground of the XIAO is tied to the ground of the Rainbow Link to establish a common reference. The 5 V output from the Rainbow Link is also connected to the 5 V pin of the XIAO to power the microcontroller, completing the circuit.
We next uploaded the below test code in XIAO and opened the serial monitor.
const int fanPWM = 0; // Blue wire
const int fanTach = 1; // Yellow wire (interrupt)
volatile unsigned int pulseCount = 0;
unsigned long lastRPMTime = 0;
unsigned long lastModeTime = 0;
int mode = 0; // 0=LOW, 1=MED, 2=HIGH, 3=OFF
// Interrupt: count tach pulses
void countPulse() {
pulseCount++;
}
void setup() {
pinMode(fanPWM, OUTPUT);
pinMode(fanTach, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(fanTach), countPulse, FALLING);
analogWrite(fanPWM, 0);
Serial.begin(9600);
}
void loop() {
unsigned long now = millis();
/* -------- RPM CALCULATION (EVERY 1s) -------- */
if (now - lastRPMTime >= 1000) {
noInterrupts();
unsigned int pulses = pulseCount;
pulseCount = 0;
interrupts();
// 2 pulses per revolution
int rpm = (pulses / 2) * 60;
Serial.print("Mode: ");
Serial.print(mode);
Serial.print(" | RPM: ");
Serial.println(rpm);
lastRPMTime = now;
}
/* -------- FAN MODE CHANGE (EVERY 5s) -------- */
if (now - lastModeTime >= 5000) {
mode = (mode + 1) % 4;
switch (mode) {
case 0: analogWrite(fanPWM, 80); break; // LOW
case 1: analogWrite(fanPWM, 150); break; // MED
case 2: analogWrite(fanPWM, 255); break; // HIGH
case 3: analogWrite(fanPWM, 0); break; // OFF
}
lastModeTime = now;
}
}The fan’s PWM control wire is connected to GPIO0, and the XIAO cycles the fan between low, medium, high, and off states every 5 seconds. At the same time, the fan speed is measured using the tachometer output, and the calculated RPM is displayed on the serial monitor.
ESP32 S3 DEV BOARDFor the main microcontroller, we are using the ESP32-S3 LCD 1.47" Display Board by Waveshare. The name might be long, but the board itself is surprisingly compact, and it packs a lot of power into a small footprint.
At its core, the board is powered by an ESP32-S3 microcontroller, featuring a dual-core 32-bit Xtensa LX7 processor running at up to 240 MHz, along with built-in Wi-Fi (802.11 b/g/n) and Bluetooth 5 (LE) support. It comes with 8 MB of Flash and PSRAM, making it well suited for applications that require graphics, real-time data processing, and responsive user interfaces.
The board integrates a 1.47" SPI-based ST7789 LCD display with a resolution of 172 × 320 pixels and support for 262K colors. Despite its small size, the display is crisp and vibrant, making it ideal for compact UI elements such as fan RPM readouts, mode indicators, and status screens.
For interaction and expansion, the board includes an onboard RGB LED for visual feedback, a TF (microSD) card slot, and a GPIO header that allows additional peripherals to be connected directly. The ST7789 display works reliably with graphics libraries such as Adafruit GFX and LVGL, making UI development straightforward.
You can check out more about this display from its wiki page.
https://www.waveshare.com/wiki/ESP32-C6-LCD-1.47
Also, for sourcing this display, we got it from PCBWAY's gift shop.
SWITCH PCBFor this project, we reused one of our previously designed switch PCBs. The board consists of two 4×4 tactile push buttons wired in a pull-down configuration. The ground pins of both switches are connected together, and each button pulls its respective signal line low when pressed, allowing the microcontroller to reliably detect button presses.
After finalizing the schematic, we designed the PCB and added a few custom elements to the silkscreen layer to improve its overall aesthetics and make the board easier to identify during assembly.
Once the design was complete, the Gerber files were exported and shared with a PCB manufacturer for fabrication.
PCBWAY SERVICEAfter finalizing the PCB design, we exported the Gerber files and uploaded them to the quote page on PCBWay. For this project, we chose a purple solder mask with white silkscreen.
The PCBs were delivered within a week, and the quality was excellent, as always. While I usually stick to white or standard black solder masks for most of my builds, this time I decided to try PCBWay’s purple finish. This was partly inspired by their Christmas sale campaign, during which matte black, purple, and pink solder mask options were available for a limited time.
Over the past ten years, PCBWay has distinguished themselves by providing outstanding PCB manufacturing and assembly services, becoming a trusted partner for countless engineers and designers worldwide.
Also, PCBWay is organizing the PCBWay 8th Project Design Contest, a global event that invites makers, engineers, and innovators to showcase their most creative builds. With categories in Electronics, Mechanical, and AIoT, it’s a great opportunity to share your work, connect with the community, and compete for exciting prizes.
You guys can check out PCBWAY if you want great PCB service at an affordable rate.
SWITCH PCB ASSEMBLYFor the switchboard assembly process, we just need to place push buttons in their place, flip the board, and solder the pads with a soldering iron, securing things in place.
MAIN ELECTRONICS SETUP- We then moved on to the wiring stage by following the prepared wiring diagram and connecting all components together.
- The switch PCB was connected by wiring button A and button B signal lines to GPIO 2 and GPIO 3 of the ESP32-S3 board, with the switch ground connected to the common GND.
- Next, the power connections were made. The output side of the DC–DC buck converter was connected to the ESP32-S3 board, with 5 V routed to the VIN pin and GND connected to ground.
- On the input side of the buck converter, a barrel DC jack was added to supply the main input voltage.
- In addition, a CON4 connector was connected in parallel with the barrel jack’s VCC and GND.
- This connector is used to supply 12 V power to the LattePanda Delta using the same external adapter, allowing both the control electronics and the SBC to be powered from a single power source.
Using the below sketch, we tested the complete setup by powering both the server fan and the ESP32-S3 display board from a single 12 V adapter connected through the barrel jack.
#include <Arduino.h>
#include <Arduino_GFX_Library.h>
/* ================= DISPLAY ================= */
#define LCD_MOSI 45
#define LCD_SCLK 40
#define LCD_CS 42
#define LCD_DC 41
#define LCD_RST 39
#define LCD_BL 46
Arduino_DataBus *bus = new Arduino_ESP32SPI(
LCD_DC, LCD_CS, LCD_SCLK, LCD_MOSI, GFX_NOT_DEFINED
);
Arduino_GFX *gfx = new Arduino_ST7789(
bus,
LCD_RST,
3, // correct orientation for your panel
true,
172, 320,
34, 0,
34, 0
);
/* ================= SAFE AREA ================= */
#define UI_MARGIN 10
#define UI_X UI_MARGIN
#define UI_Y UI_MARGIN
#define UI_W (320 - UI_MARGIN * 2)
#define UI_H (172 - UI_MARGIN * 2)
/* ================= FAN (YOUR PINS) ================= */
#define FAN_PWM 0
#define FAN_TACH 1
#define PWM_FREQ 25000
#define PWM_RES 8
/* ================= BUTTONS (YOUR PINS) ================= */
#define BTN_PLUS 2
#define BTN_MINUS 3
/* ================= RPM ================= */
volatile uint32_t pulseCount = 0;
uint32_t lastRPMMillis = 0;
uint32_t rpm = 0;
/* ================= MODE ================= */
int mode = 0; // 0=LOW,1=MED,2=HIGH,3=OFF
#define MODE_COUNT 4
/* ================= PER-MODE RPM SCALE ================= */
const uint16_t MODE_MAX_RPM[MODE_COUNT] = {
1200, // LOW
2200, // MED
3500, // HIGH
1 // OFF (dummy, not used)
};
/* ================= BAR GRAPH ================= */
#define BAR_W 30
#define BAR_H 110
#define BAR_X (UI_X + UI_W - BAR_W - 10)
#define BAR_Y (UI_Y + 35)
/* ================= BUTTON STATE ================= */
bool lastPlusState = HIGH;
bool lastMinusState = HIGH;
uint32_t lastDebounce = 0;
/* ================= ISR ================= */
void IRAM_ATTR tachISR() {
pulseCount++;
}
/* ================= FAN MODE ================= */
void setFanMode(int m) {
mode = (m + MODE_COUNT) % MODE_COUNT;
switch (mode) {
case 0: ledcWrite(FAN_PWM, 80); break; // LOW
case 1: ledcWrite(FAN_PWM, 150); break; // MED
case 2: ledcWrite(FAN_PWM, 255); break; // HIGH
case 3: ledcWrite(FAN_PWM, 0); break; // OFF
}
}
/* ================= BAR COLOR ================= */
uint16_t barColorForMode() {
switch (mode) {
case 0: return GREEN; // LOW
case 1: return YELLOW; // MED
case 2: return RED; // HIGH
default: return BLACK; // OFF
}
}
/* ================= STATIC UI ================= */
void drawStaticUI() {
gfx->fillScreen(BLACK);
gfx->drawRect(UI_X, UI_Y, UI_W, UI_H, WHITE);
gfx->setTextColor(WHITE);
gfx->setTextSize(2);
gfx->setCursor(UI_X + 10, UI_Y + 8);
gfx->println("FAN MONITOR");
gfx->setCursor(UI_X + 10, UI_Y + 40);
gfx->println("RPM");
gfx->drawRect(UI_X + 150, UI_Y + 35, 90, 60, WHITE);
gfx->setCursor(UI_X + 160, UI_Y + 40);
gfx->println("MODE");
gfx->drawRect(BAR_X, BAR_Y, BAR_W, BAR_H, WHITE);
}
/* ================= DYNAMIC UI ================= */
void updateRPMNumber(uint32_t value) {
gfx->fillRect(UI_X + 10, UI_Y + 65, 140, 50, BLACK);
gfx->setTextSize(4);
gfx->setCursor(UI_X + 10, UI_Y + 65);
gfx->println(value);
}
void updateModeText() {
gfx->fillRect(UI_X + 160, UI_Y + 65, 70, 25, BLACK);
gfx->setTextSize(2);
gfx->setCursor(UI_X + 165, UI_Y + 65);
switch (mode) {
case 0: gfx->println("LOW"); break;
case 1: gfx->println("MED"); break;
case 2: gfx->println("HIGH"); break;
case 3: gfx->println("OFF"); break;
}
}
void updateRPMBar(uint32_t rpmValue) {
gfx->fillRect(BAR_X + 1, BAR_Y + 1, BAR_W - 2, BAR_H - 2, BLACK);
if (mode == 3) return; // OFF
uint16_t maxRPM = MODE_MAX_RPM[mode];
if (rpmValue > maxRPM) rpmValue = maxRPM;
int filled = map(rpmValue, 0, maxRPM, 0, BAR_H);
if (filled > 0) {
gfx->fillRect(
BAR_X + 2,
BAR_Y + BAR_H - filled + 2,
BAR_W - 4,
filled - 4,
barColorForMode()
);
}
}
/* ================= SETUP ================= */
void setup() {
pinMode(LCD_BL, OUTPUT);
digitalWrite(LCD_BL, HIGH);
gfx->begin();
drawStaticUI();
updateRPMNumber(0);
updateRPMBar(0);
updateModeText();
ledcAttach(FAN_PWM, PWM_FREQ, PWM_RES);
setFanMode(0);
pinMode(FAN_TACH, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(FAN_TACH), tachISR, FALLING);
pinMode(BTN_PLUS, INPUT_PULLUP);
pinMode(BTN_MINUS, INPUT_PULLUP);
}
/* ================= LOOP ================= */
void loop() {
uint32_t now = millis();
if (now - lastRPMMillis >= 1000) {
noInterrupts();
uint32_t pulses = pulseCount;
pulseCount = 0;
interrupts();
rpm = (pulses / 2) * 60;
updateRPMNumber(rpm);
updateRPMBar(rpm);
lastRPMMillis = now;
}
bool plusState = digitalRead(BTN_PLUS);
bool minusState = digitalRead(BTN_MINUS);
if (now - lastDebounce > 200) {
if (plusState == LOW && lastPlusState == HIGH) {
setFanMode(mode + 1);
updateModeText();
updateRPMBar(rpm);
lastDebounce = now;
}
if (minusState == LOW && lastMinusState == HIGH) {
setFanMode(mode - 1);
updateModeText();
updateRPMBar(rpm);
lastDebounce = now;
}
}
lastPlusState = plusState;
lastMinusState = minusState;
}This version is largely based on the earlier test sketch used with the XIAO, where PWM was used to control fan speed and an interrupt-based tachometer input was used to measure RPM.
The main difference in this version is the addition of two physical buttons that allow the fan mode to be changed in ascending or descending order. Pressing the plus (+) button increases the fan mode step by step, while pressing the minus (–) button decreases it.
In addition to fan control, this sketch also implements a custom user interface on the built-in display. The UI shows the live RPM value, the current operating mode, and a vertical bar graph that represents fan speed. The bar graph changes color based on the selected mode: green for low, yellow for medium, and red for high, and it remains empty when the fan is off.
3D MODELThe entire 3D design process was based around the idea of adding a server fan on top of the heatsink. The goal was for the fan to intake air from the front and direct it at high speed onto the heatsink, allowing the air to pass through the fins and maintain a stable operating temperature.
In the stock design, the fan is positioned horizontally inside a pocket in the heatsink and covered by a plastic cover. This cover acts as a duct, with an opening only above the fan blades, allowing the fan to pull air in and force it through the heatsink fins.
We initially tried to replicate this approach by placing our server fan over the heatsink and creating a cover based on the original heatsink file. This modification allowed us to mount the fan correctly, but it did not provide optimal airflow control. After testing several design iterations, we finalized a solution where the fan is mounted tangentially. In this configuration, the fan draws air from one side and directs it into the heatsink through a custom duct. This duct also acts as a heatsink cover, ensuring that when high-speed air enters the heatsink, the only exit path is through the fins.
All of these modifications were possible because the official 3D models of the LattePanda Delta 3 were available on the DFRobot wiki pages, allowing accurate alignment and mechanical integration.
The next step was to design a base or holder that supports the LattePanda Delta while also housing the ESP32 display, the switch PCB, and dedicated button actuators for operating the push buttons.
After finalizing the design, the duct, LattePanda holder frame, and button actuator components were exported as mesh files and 3D printed using an Anycubic Kobra S1 with white PLA.
THREADED INSERT PROCESSOur original plan was to mount the fan to the duct using M2.5 screws and nuts. However, the DC fan does not have threaded mounting holes.
To solve this, we added M2.5 threaded inserts to all four mounting holes of the fan. A soldering iron set to around 250°C was used for this process.
Each threaded insert was carefully placed over a mounting hole and then pressed in using the heated soldering iron tip. The heat softens the plastic around the hole, allowing the insert to sink in smoothly. Once the plastic cools down, the insert locks firmly in place.
By adding threaded inserts to all four mounting points, we created proper, durable threads in the fan body. These threads are then used to securely mount both the BLDC fan and the duct component, resulting in a strong and serviceable mechanical assembly.
HEATSINK ASSEMBLY- We start the heatsink assembly process by placing the duct part over the aluminum heatsink.
- The duct is positioned by aligning the mounting holes of both parts together.
- Using M2.5 bolts, we secure the duct firmly to the heatsink.
- Next, we apply a sufficient amount of silicone-based thermal grease over the processor die and spread it evenly to ensure proper heat transfer.
- The heatsink assembly is then placed over the CPU, carefully aligning the mounting holes of the LattePanda with the screw bosses on the heatsink.
- From the bottom side, we fasten the heatsink in place using the mounting screws, completing the heatsink installation.
- For the final assembly, we first placed the ESP32 display board into its designated position inside the LattePanda holder.
- Next, the button actuators were inserted into the slots provided in the holder, followed by installing the switch PCB behind them so that the actuators aligned correctly with the push buttons.
- The DC barrel jack was then mounted in its cutout and secured using the included nut. After that, the VCC and GND connections from the input side of the DC–DC buck converter were connected to the DC jack.
- The LattePanda Delta was then placed onto its mounting position and secured using four M2 screws.
- Finally, the CON4 connector was connected to the LattePanda’s power input, allowing the board to be powered directly from the DC jack.
The end result of this build is a LattePanda Delta equipped with a completely custom cooling solution. The system consists of a high-airflow server fan paired with a custom-designed airflow duct, along with a compact stand that houses a display and two control buttons while also serving as a stable holder for the board.
With this setup, the entire cooling section of the LattePanda Delta has been redesigned. Since the new fan provides significantly higher airflow than the original one, the system now has enough thermal headroom to safely handle higher CPU loads and even light overclocking, with the fan and heatsink keeping temperatures under control.
Pressing the plus (+) button moves the device into the first fan mode, and continued presses increase the fan speed at a fixed step rate. The minus (–) button reduces the fan speed in the same manner. The current fan RPM and operating mode are displayed live on the screen, providing immediate feedback.
With this cooling solution in place, the system now idles at around 26–35°C, which is a significant improvement compared to the earlier setup.
Previously, with the stock fan and control circuitry no longer functioning, idle temperatures exceeded 60°C, and the board would shut down under load. This modification restores stability and makes the LattePanda Delta usable again for sustained operation.
CONCLUSIONThe motivation behind modifying the cooling system of the LattePanda Delta came from another ongoing project. I was working on a game emulator build, with the goal of creating a small, Steam-machine-like device capable of running retro games. The LattePanda Delta 3 is more than powerful enough for this purpose, as demonstrated in my earlier Latteintosh project, so I needed the board to operate at its best for extended periods.
During this process, the stock cooling fan on the board failed. Instead of waiting for a replacement fan module, I decided to take the opportunity to design an externally controlled cooling system. This approach not only solved the immediate cooling problem but also resulted in a more flexible, higher-performance solution that turned out to be both effective—and admittedly loud.
Overall, the project was a success, and all design files, code, and resources related to this build are attached on the project page.
Thanks for reaching this far, and I will be back with a new project pretty soon.
Peace.







_t9PF3orMPd.png?auto=compress%2Cformat&w=40&h=40&fit=fillmax&bg=fff&dpr=2)







Comments