Skip to content

Latest commit

 

History

17 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BKHD Fan Controller

⚠️ WORK IN PROGRESS — DO NOT REPLICATE YET ⚠️

This project is under active development and is NOT ready for replication.

Hardware, firmware, protocol, and host software are all still evolving. Pin assignments, schematics, and APIs may change without notice. Nothing here has been fully validated on the final PCB. Building this now may result in damaged hardware, non-working fans, or worse.

Please wait for a tagged release (v1.0.0 or later) before attempting to build or flash anything from this repository.

STM32G031-based 5-channel PWM fan controller with Linux host software for the BKHD-2049NP-6L mini PC.

Features

  • 5 independent PWM channels — 25 kHz, 0-100% duty, for 4-pin PC fans
  • TACH monitoring — RPM measurement per fan with stall detection
  • Optional NTC input — one analog NTC thermistor input (Semitec 104NT) on PB7, gated behind the NTC1_ENABLED build flag. Intended as a workaround for OEM 82599ES variants whose on-die thermal sensor is disabled in the vendor NVM image (see docs/hardware-notes.md for the full story). When enabled and populated, the value is surfaced to the host via the STS frame as a virtual ntc1 sensor.
  • Buzzer alarm — active buzzer alerts on fan failure or host communication loss
  • Host watchdog — all fans ramp to 100% if host goes silent for 60 seconds
  • UART protocol — simple ASCII (NMEA-style) for easy debugging with any terminal
  • Linux host daemon — reads hwmon temperatures, computes fan curves, sends set-points
  • systemd integration — runs as a service with syslog-compatible logging

Hardware

  • MCU: STM32G031K8T6 (Cortex-M0+, 64 MHz, 64 KB Flash, 8 KB RAM)
  • Fans: 5x 5V 4-pin PWM via NPN open-collector drivers
  • TACH: 5x input with 3.3V pull-up, EXTI-based pulse counting
  • NTC (optional, v1 PCB workaround): Semitec 104NT-4-R025H42G (100 kΩ, B = 4267 K) in a 100 kΩ divider to +3V3, sampled on ADC1_IN11 (PB7). The v1 PCB does not carry this divider — it has to be retrofitted with fine wires soldered from PB7 and from +3V3/GND to a leaded NTC cemented to the 82599ES heatsink. Enable in firmware via NTC1_ENABLED=1. See docs/hardware-notes.md for why this workaround exists and how to carry it out.
  • Buzzer: Active 5V buzzer via NPN
  • UART: USART2 @ 115200 8N1 with level shifting

See docs/hardware-notes.md for schematics and pin mapping.

Building the Firmware

Prerequisites

Linux (Ubuntu/Debian)
sudo apt install gcc-arm-none-eabi cmake make
Linux (Arch)
sudo pacman -S arm-none-eabi-gcc arm-none-eabi-newlib cmake
macOS

Install Homebrew if not already present, then:

brew install --cask gcc-arm-embedded
brew install cmake
Windows
  1. Download the Arm GNU Toolchain — choose the arm-none-eabi AArch32 bare-metal target for Windows:
    • .msi installer (recommended) — installs to C:\Program Files (x86)\Arm\GNU Toolchain mingw-w64-i686-arm-none-eabi\bin. After installation, add this bin directory to your system PATH manually.
    • .zip portable — extract anywhere and add the bin directory to your PATH.
  2. Install CMake (.msi installer, check "Add CMake to PATH").
  3. Install a build system — either:
    • Ninja (recommended, drop ninja.exe into a directory on your PATH), or
    • mingw32-make via MSYS2 (pacman -S mingw-w64-x86_64-make) or chocolatey (choco install make) — ensure mingw32-make.exe is on your PATH.

Verify after installation:

arm-none-eabi-gcc --version
cmake --version

Clone and init submodules

git clone https://github.com/MrDix/BKHD-FanController.git
cd BKHD-FanController
git submodule update --init --recursive

Build

Two firmware variants are supported from the same source tree, selected by the NTC1_ENABLED CMake option:

  • Default (NTC1_ENABLED=0) — unmodified v1 PCB. No NTC soldered on. The firmware leaves PB7 alone, skips ADC init entirely, and reports t1 = -32768 in every STS frame. Any fan configured against ntc1 transparently falls back to its fallback_sensor on the host.
  • Opt-in (NTC1_ENABLED=1) — v1 PCB with NTC nachrüstung, or a future PCB revision that carries the divider by default. Enables ADC1 sampling and ships real temperature readings in t1.

Linux / macOS:

cd firmware

# Default: stock v1 PCB, no NTC
cmake -B build -DCMAKE_TOOLCHAIN_FILE=arm-none-eabi.cmake
cmake --build build

# With NTC nachrüstung present (or on a future PCB that carries the divider):
cmake -B build -DCMAKE_TOOLCHAIN_FILE=arm-none-eabi.cmake -DNTC1_ENABLED=1
cmake --build build

Do not pass -DNTC1_ENABLED=1 for an unmodified v1 PCB. Without the divider, PB7 is floating and any sampled value is meaningless; a fan configured to follow ntc1 would be driven by noise.

Windows (a generator must be specified — CMake defaults to NMake which requires Visual Studio):

cd firmware
rem With Ninja (recommended):
cmake -B build -G Ninja -DCMAKE_TOOLCHAIN_FILE=arm-none-eabi.cmake
rem Or with MinGW Make (if installed via MSYS2/chocolatey):
cmake -B build -G "MinGW Makefiles" -DCMAKE_TOOLCHAIN_FILE=arm-none-eabi.cmake
rem Append -DNTC1_ENABLED=1 to either of the above if the NTC is populated.

cmake --build build

Output: build/fan_controller.bin and build/fan_controller.hex

Flash via SWD

Using an STLINK-V3MINIE

The STLINK-V3MINIE connects to the MCU via the SWD header (SWDIO, SWCLK, GND, 3.3V).

Important: The STLINK-V3MINIE does not supply power to the target board — it only measures the target voltage. Your board must be powered externally (e.g. via USB or a 5V supply) before flashing.

Flashing setup: Lab power supply (5V) connected to the PCB (right), STLINK-V3MINIE programmer connected via SWD header (left)

Install flash tools:

Linux
# Option 1: stlink open-source tools (recommended)
sudo apt install stlink-tools        # Ubuntu/Debian
sudo pacman -S stlink                # Arch

# Option 2: OpenOCD
sudo apt install openocd             # Ubuntu/Debian
sudo pacman -S openocd               # Arch

udev rules (required for non-root access):

sudo cp /usr/lib/udev/rules.d/*stlink* /etc/udev/rules.d/ 2>/dev/null || \
  sudo sh -c 'echo "SUBSYSTEM==\"usb\", ATTR{idVendor}==\"0483\", ATTR{idProduct}==\"3754\", GROUP=\"plugdev\", MODE=\"0660\", TAG+=\"uaccess\"" > /etc/udev/rules.d/49-stlink.rules'
sudo udevadm control --reload-rules && sudo udevadm trigger

# If the device is still not accessible, add your user to the plugdev group:
# sudo usermod -a -G plugdev $USER
# (log out and back in for group changes to take effect)
macOS
# Option 1: stlink open-source tools (recommended)
brew install stlink

# Option 2: OpenOCD
brew install openocd
Windows

Note: The open-source stlink-tools (st-flash) do not support the STLINK-V3 on Windows. Use OpenOCD or STM32CubeProgrammer instead.

Option 1: OpenOCD (recommended)

Download from openocd.org/releases and extract the archive. Add the bin directory to your PATH (the scripts folder must remain in the same relative location).

Option 2: STM32CubeProgrammer

Download from st.com. The installer is Java-based — the setup wizard may open behind other windows, check the taskbar if it appears to hang.

Default install path: C:\Program Files\STMicroelectronics\STM32Cube\STM32CubeProgrammer\bin

Add this directory to your PATH.

Flash the firmware:

Using stlink tools (Linux / macOS):

st-flash write build/fan_controller.bin 0x08000000

Using OpenOCD (Linux / macOS / Windows):

openocd -f interface/stlink.cfg -f target/stm32g0x.cfg -c "program build/fan_controller.bin 0x08000000 verify reset exit"

Using STM32CubeProgrammer (Linux / macOS / Windows):

STM32_Programmer_CLI -c port=SWD -w build/fan_controller.bin 0x08000000 -v --start

Verify the connection (optional):

# stlink (Linux / macOS only)
st-info --probe

# OpenOCD (Linux / macOS / Windows)
openocd -f interface/stlink.cfg -f target/stm32g0x.cfg -c "init; shutdown"

# STM32CubeProgrammer (Linux / macOS / Windows)
STM32_Programmer_CLI -c port=SWD

Host Software Setup

The host daemon is a small Python program (pyserial + pyyaml) that runs as a systemd service. Modern distributions (Debian 12+, Ubuntu 23.10+, Fedora 38+, and derivatives such as Proxmox VE 8+) enforce PEP 668 and will reject a plain pip3 install -r requirements.txt with externally-managed-environment. The instructions below use a dedicated virtual environment at /opt/bkhd-fanctrl/venv so the daemon is fully isolated from the system Python and the fan-controller.service unit can call the interpreter by absolute path without any activation step.

Install

# 1. Create the installation directory and virtual environment
sudo mkdir -p /opt/bkhd-fanctrl
sudo python3 -m venv /opt/bkhd-fanctrl/venv

# 2. Install dependencies inside the venv
sudo /opt/bkhd-fanctrl/venv/bin/pip install -r host/requirements.txt

# 3. Deploy the daemon and default config
sudo cp host/fan_controller.py /opt/bkhd-fanctrl/
sudo cp host/config.yaml /etc/fanctrl.yaml

Older distributions without PEP 668 can still use the classic sudo pip3 install -r host/requirements.txt path, but the venv approach works everywhere and is strongly recommended.

Configure

Edit /etc/fanctrl.yaml to match your sensor layout:

# Find your hwmon sensor names:
for h in /sys/class/hwmon/hwmon*; do echo "$h: $(cat $h/name)"; done

Sensor syntax in config.yaml:

Value Meaning
"acpitz", "coretemp" Match an hwmon device by its name file
"nvme:0", "nvme:1" Pick the Nth device when multiple share the same name (sorted by hwmon index)
"ntc1" Virtual sensor populated from the MCU STS frame. Requires firmware built with NTC1_ENABLED=1 and the NTC nachrüstung actually soldered on. If either is missing, the value is reported as invalid and affected fans fall back to fallback_sensor automatically — so referencing ntc1 in the config is safe even on unmodified v1 boards.

DDR5 temperature sensors (spd5118) require the kernel module to be loaded and the per-DIMM I2C devices to be instantiated. On Proxmox VE / Debian:

echo spd5118 | sudo tee /etc/modules-load.d/spd5118.conf
sudo modprobe spd5118
# Bind DIMM addresses (SMBus bus number is typically i2c-1 for the I801 SMBus):
echo "spd5118 0x50" | sudo tee /sys/bus/i2c/devices/i2c-1/new_device
echo "spd5118 0x52" | sudo tee /sys/bus/i2c/devices/i2c-1/new_device

For persistent binding across reboots, add a udev rule (adjust the adapter name match to your hardware):

sudo tee /etc/udev/rules.d/99-spd5118.rules <<'EOF'
ACTION=="add", SUBSYSTEM=="i2c-adapter", ATTR{name}=="SMBus I801 adapter*", \
  RUN+="/bin/sh -c 'echo spd5118 0x50 > /sys/bus/i2c/devices/%k/new_device; \
                    echo spd5118 0x52 > /sys/bus/i2c/devices/%k/new_device'"
EOF
sudo udevadm control --reload-rules

Run manually

sudo /opt/bkhd-fanctrl/venv/bin/python /opt/bkhd-fanctrl/fan_controller.py \
    -c /etc/fanctrl.yaml -v

Install as systemd service

sudo cp host/fan-controller.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now fan-controller.service
sudo journalctl -u fan-controller -f  # view logs

Updating

When the repo changes, redeploy the script and refresh dependencies:

git -C /usr/local/src/BKHD-FanController pull
sudo cp /usr/local/src/BKHD-FanController/host/fan_controller.py /opt/bkhd-fanctrl/
sudo /opt/bkhd-fanctrl/venv/bin/pip install -U -r \
    /usr/local/src/BKHD-FanController/host/requirements.txt
sudo systemctl restart fan-controller.service

Protocol

See docs/protocol.md for the full UART protocol specification.

Quick reference:

Host -> MCU:  $SET,80,60,50,40,30*4A     Set fan duties (%)
              $KA*35                      Keep-alive
              $ACK*24                     Acknowledge error

MCU -> Host:  $STS,1200,980,850,720,600,0,0,80,60,50,40,30,523*4C
              RPM x5, error mask, watchdog, duty x5, NTC1 (tenths of °C)

Debug with terminal

# Monitor MCU output:
screen /dev/ttyS0 115200

# Or with minicom:
minicom -D /dev/ttyS0 -b 115200

Testing

Loopback test (without hardware)

# Create virtual serial pair:
socat -d PTY,raw,echo=0 PTY,raw,echo=0
# Note the two /dev/pts/X paths, use one for the host script and one for minicom

Test plan

  1. Flash firmware, verify UART output with terminal (STS frames every 500ms, now with t1 field)
  2. Send $SET,50,50,50,50,50*XX\n manually, verify PWM with oscilloscope
  3. Block a fan, verify buzzer activates and error bit appears in STS
  4. Release the fan; verify the buzzer stops again (regression test for the stuck-on bug)
  5. Stop sending commands for 60s, verify failsafe (all fans 100%)
  6. Run host script, verify temperature-based fan curve operation
  7. Kill host script, verify failsafe kicks in after 60s
  8. Build with NTC1_ENABLED=1 on a board with NTC nachrüstung: heat the NTC with a finger or hot-air station; verify ntc1 rises in the host logs and the configured fan ramps up
  9. Build with NTC1_ENABLED=0 (default / stock v1 PCB): verify every STS frame reports t1=-32768 and any fan configured against ntc1 falls back to its fallback_sensor

Project Structure

BKHD-FanController/
├── firmware/
│   ├── CMakeLists.txt          # Build configuration (NTC1_ENABLED option)
│   ├── arm-none-eabi.cmake     # Cross-compilation toolchain
│   ├── STM32G031K8Tx.ld        # Linker script
│   ├── Inc/                    # Header files
│   └── Src/                    # Source files
│       ├── main.c              # Clock, GPIO, timer, UART, alarm edge
│       ├── fan_control.c       # PWM duty control
│       ├── tach_measure.c      # RPM measurement via EXTI
│       ├── uart_protocol.c     # NMEA-style protocol parser
│       ├── buzzer.c            # Buzzer control
│       ├── watchdog.c          # Host timeout + IWDG
│       └── ntc.c               # NTC sampling + Beta conversion (opt-in via NTC1_ENABLED)
├── host/
│   ├── fan_controller.py       # Linux daemon
│   ├── config.yaml             # Fan curve configuration
│   ├── fan-controller.service  # systemd unit file
│   └── requirements.txt        # Python dependencies
└── docs/
    ├── protocol.md             # UART protocol spec
    └── hardware-notes.md       # Schematics, pin mapping, NTC workaround rationale

License

MIT

About

STM32G031-based 5-channel PWM fan controller with Linux host software for the BKHD-2049NP-6L mini PC

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages