Cooling Fan Upgrade for Lattepanda Delta

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.

 

MATERIALS REQUIRED

These 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

 

COOLING FAN ISSUE ON LATTEPANDA 3 DELTA

 

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.

https://www.dfrobot.com/product-2594.html?srsltid=AfmBOoobGQkg4_Jv1uCnve57xWTspKUq6-fqdAoaJktiOmAyebzgmHpN

FAN TEARDOWN

We 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 SETUP

 

For 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:

  1. Red – VCC (12 V)
  2. Black – Ground
  3. Yellow – Tachometer output for RPM feedback
  4. 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.

CODE
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;
  }
}

ESP32 S3 DEV BOARD

For 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

SWITCH PCB

For 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.

SWITCH PCB ASSEMBLY

For 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

STEP 1
We then moved on to the wiring stage by following the prepared wiring diagram and connecting all components together.
STEP 2
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.
STEP 3
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.
STEP 4
On the input side of the buck converter, a barrel DC jack was added to supply the main input voltage.
STEP 5
In addition, a CON4 connector was connected in parallel with the barrel jack’s VCC and GND.
STEP 6
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.

TEST RUN

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.

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.

CODE
#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;
}

3D MODEL

The 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.

FINAL ASSEMBLY

  1. For the final assembly, we first placed the ESP32 display board into its designated position inside the LattePanda holder.
  2. 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.
  3. 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.
  4. The LattePanda Delta was then placed onto its mounting position and secured using four M2 screws.
  5. Finally, the CON4 connector was connected to the LattePanda’s power input, allowing the board to be powered directly from the DC jack.

RESULT

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.

 

The 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.

License
All Rights
Reserved
licensBg
0