Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Hardware PWM Controller for the Raspberry Pi 4 Case Fan
CC = gcc
RM = rm -f
INSTRUMENT_FOR_PROMETHEUS := false
ifeq ($(INSTRUMENT_FOR_PROMETHEUS),true)
CFLAGS = -Wall -DINSTRUMENT_FOR_PROMETHEUS
LIBS = -lbcm2835 -lprom -lpromhttp -lmicrohttpd
else
CFLAGS = -Wall
LIBS = -lbcm2835
endif
TARGET = pi_fan_hwpwm
all: $(TARGET)
$(TARGET): $(TARGET).c Makefile
$(CC) $(CFLAGS) -o $(TARGET) $(TARGET).c $(LIBS)
install: $(TARGET)
install $(TARGET) /usr/local/sbin
cp $(TARGET).service /etc/systemd/system/
systemctl enable $(TARGET)
! systemctl is-active --quiet $(TARGET) || systemctl stop $(TARGET)
systemctl start $(TARGET)
uninstall: clean
systemctl stop $(TARGET)
systemctl disable $(TARGET)
$(RM) /usr/local/sbin/$(TARGET)
$(RM) /etc/systemd/system/$(TARGET).service
$(RM) /run/$(TARGET).*
@echo
@echo "To remove the source directory"
@echo " $$ cd && rm -rf ${CURDIR}"
@echo
clean:
$(RM) $(TARGET)
/*
/
/ pi_fan_hwpwm.c, alwynallan@gmail.com 12/2020, no license
/ latest version: https://gist.github.com/alwynallan/1c13096c4cd675f38405702e89e0c536
/
/ Need http://www.airspayce.com/mikem/bcm2835/index.html
/
/ Compile $ gcc -Wall pi_fan_hwpwm.c -lbcm2835 -o pi_fan_hwpwm
/
/ Disable $ sudo nano /boot/config.txt [Raspbian, or use GUI]
/ $ sudo nano /boot/firmware/usercfg.txt [Ubuntu]
/ # dtoverlay=gpio-fan,gpiopin=14,temp=80000 <- commented out, reboot
/ enable_uart=0 <- needed? not Ubuntu
/ dtparam=audio=off <- needed? not Ubuntu
/ dtparam=i2c_arm=off <- needed? not Ubuntu
/ dtparam=spi=off <- needed? not Ubuntu
/
/ Run $ sudo ./pi_fan_hwpwm -v
/
/ Forget $ sudo ./pi_fan_hwpwm &
/ $ disown -a
/
*/
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdarg.h>
#include <bcm2835.h>
//#define INSTRUMENT_FOR_PROMETHEUS do this in the Makefile
#ifdef INSTRUMENT_FOR_PROMETHEUS
// https://github.com/digitalocean/prometheus-client-c
#define PROM_PORT 8764
#include <signal.h>
#include "microhttpd.h"
#include "prom.h"
#include "promhttp.h"
prom_counter_t *pi_fan_hwpwm_loops;
prom_gauge_t *pi_fan_hwpwm_temp;
prom_gauge_t *pi_fan_hwpwm_pwm;
#endif //INSTRUMENT_FOR_PROMETHEUS
#define PWM_PIN 0 // default, uses both GPIO 13 and GPIO 18
#define HIGH_TEMP 80.
#define ON_TEMP 65.
#define OFF_TEMP 60.
#define MIN_FAN 150
#define KICK_FAN 200
#define MAX_FAN 480
unsigned pin = PWM_PIN;
int verbose = 0;
int fan_state = 0;
double temp = 25.0;
pid_t global_pid;
int pwm_level = -555;
void usage()
{
fprintf
(stderr,
"\n" \
"Usage: sudo ./pi_fan_hwpwm [OPTION]...\n" \
"\n" \
" -g <n> Use GPIO n for fan's PWM input, default 0 (both).\n" \
" Only hardware PWM capable GPIO 18 and GPIO 13 are present on\n" \
" the RasPi 4B pin header, and only GPIO 18 can be used with\n" \
" the unmodified case fan.\n" \
" -v Verbose output\n" \
"\n"
);
}
void fatal(int show_usage, char *fmt, ...) {
char buf[128];
va_list ap;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
fprintf(stderr, "%s\n", buf);
if (show_usage) usage();
fflush(stderr);
exit(EXIT_FAILURE);
}
void run_write(const char *fname, const char *data) {
// https://opensource.com/article/19/4/interprocess-communication-linux-storage
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_whence = SEEK_SET;
lock.l_start = 0;
lock.l_len = 0;
lock.l_pid = global_pid;
int fd;
if ((fd = open(fname, O_RDWR | O_CREAT, 0666)) < 0)
fatal(0, "failed to open %s for writing", fname);
if (fcntl(fd, F_SETLK, &lock) < 0)
fatal(0, "fcntl failed to get lock on %s", fname);
if (ftruncate(fd, 0) < 0)
fatal(0, "truncate failed to on %s", fname);
write(fd, data, strlen(data));
close(fd);
}
void PWM_out(int level) {
if(level > pwm_level && (level - pwm_level) < 5) return;
if(level < pwm_level && (pwm_level - level) < 10) return;
if(level != pwm_level) {
if(pin == 0 || pin == 13) bcm2835_pwm_set_data(1, level);
if(pin == 0 || pin == 18) bcm2835_pwm_set_data(0, level);
pwm_level = level;
}
}
void fan_loop(void) {
if(!fan_state && (temp > ON_TEMP)) {
PWM_out(KICK_FAN);
fan_state = 1;
return;
}
if(fan_state && (temp < OFF_TEMP)) {
PWM_out(0);
fan_state = 0;
return;
}
if(fan_state) {
unsigned out = (double) MIN_FAN + (temp - OFF_TEMP) / (HIGH_TEMP - OFF_TEMP) * (double)(MAX_FAN - MIN_FAN);
if(out > MAX_FAN) out = MAX_FAN;
PWM_out(out);
}
}
#ifdef INSTRUMENT_FOR_PROMETHEUS
void ae1() {
prom_collector_registry_destroy(PROM_COLLECTOR_REGISTRY_DEFAULT);
}
struct MHD_Daemon *mhdDaemon;
void ae2() {
MHD_stop_daemon(mhdDaemon);
}
#endif //INSTRUMENT_FOR_PROMETHEUS
int main(int argc, char *argv[]) {
int opt;
unsigned loop = 0;
int t;
FILE *ft;
char buf[100];
while ((opt = getopt(argc, argv, "g:v")) != -1) {
switch (opt) {
case 'g':
pin = atoi(optarg);
if(pin != 0 && pin != 13 && pin != 18) fatal(0, "Invalid GPIO");
break;
case 'v':
verbose = 1;
break;
default:
usage();
exit(EXIT_FAILURE);
}
}
if(optind != argc) fatal(1, "optind=%d argc=%d Unrecognized parameter %s", optind, argc, argv[optind]);
global_pid = getpid();
sprintf(buf, "%d\n", global_pid);
run_write("/run/pi_fan_hwpwm.pid", buf);
if(!bcm2835_init()) fatal(0, "bcm2835_init() failed");
if(pin==0 || pin==13) bcm2835_gpio_fsel(13, BCM2835_GPIO_FSEL_ALT0);
if(pin==0 || pin==18) bcm2835_gpio_fsel(18, BCM2835_GPIO_FSEL_ALT5);
bcm2835_pwm_set_clock(2); // 19.2 / 2 MHz
if(pin==0 || pin==13) bcm2835_pwm_set_mode(1, 1, 1);
if(pin==0 || pin==13) bcm2835_pwm_set_range(1, 480);
if(pin==0 || pin==18) bcm2835_pwm_set_mode(0, 1, 1);
if(pin==0 || pin==18) bcm2835_pwm_set_range(0, 480);
PWM_out(0);
#ifdef INSTRUMENT_FOR_PROMETHEUS
prom_collector_registry_default_init();
pi_fan_hwpwm_loops = prom_collector_registry_must_register_metric(
prom_counter_new("pi_fan_hwpwm_loops", "Control loop counter.", 0, NULL));
pi_fan_hwpwm_temp = prom_collector_registry_must_register_metric(
prom_gauge_new("pi_fan_hwpwm_temp", "Core temperature in Celsius.", 0, NULL));
pi_fan_hwpwm_pwm = prom_collector_registry_must_register_metric(
prom_gauge_new("pi_fan_hwpwm_pwm", "Fan speed PWM in percent.", 0, NULL));
promhttp_set_active_collector_registry(NULL);
atexit(ae1);
mhdDaemon = promhttp_start_daemon(MHD_USE_SELECT_INTERNALLY, PROM_PORT, NULL, NULL);
if (mhdDaemon == NULL) exit(EXIT_FAILURE);
else atexit(ae2);
#endif //INSTRUMENT_FOR_PROMETHEUS
while(1) {
loop++;
ft = fopen("/sys/class/thermal/thermal_zone0/temp", "r");
fscanf(ft, "%d", &t);
fclose(ft);
temp = 0.0001 * (double)t + 0.9 * temp;
if((loop%4) == 0) { // every second
fan_loop();
sprintf(buf, "%u, %.2f, %.1f\n", loop/4, temp, (float)pwm_level/(float)MAX_FAN*100.);
run_write("/run/pi_fan_hwpwm.state", buf);
if(verbose) fputs(buf, stdout);
#ifdef INSTRUMENT_FOR_PROMETHEUS
prom_counter_inc(pi_fan_hwpwm_loops, NULL);
prom_gauge_set(pi_fan_hwpwm_temp, temp, NULL);
prom_gauge_set(pi_fan_hwpwm_pwm, (double)pwm_level/(double)MAX_FAN*100., NULL);
#endif //INSTRUMENT_FOR_PROMETHEUS
}
usleep(250000);
}
exit(EXIT_SUCCESS);
}
[Unit]
Description=Hardware PWM control for Raspberry Pi 4 Case Fan
After=syslog.target
[Service]
Type=simple
User=root
WorkingDirectory=/run
PIDFile=/run/pi_fan_hwpwm.pid
ExecStart=/usr/local/sbin/pi_fan_hwpwm
Restart=on-failure
[Install]
WantedBy=multi-user.target
@bdlabitt
Copy link

bdlabitt commented Mar 17, 2021

Just tested pwm.c - the GPIO pin#18 is fine. For some reason the PWM frequency doesn't match what is stated. pwm.c claims that it will generate a 1.1 KHz waveform. Instead it runs at 3.3 KHz, with CLOCK_DIVIDER_16. When the duty cycle is long enough, the fan starts up. Now need to dig into your main routine - there is some sort of bug that my setup has found.

@bdlabitt
Copy link

bdlabitt commented Mar 17, 2021

FYI, I am running on an RPI4B-2GB with Raspberry Pi OS 32bit.

@bdlabitt
Copy link

bdlabitt commented Mar 17, 2021

No bug. The service didn't restart correctly. Killed that and all of the above works, save for changing the line in the code to lower the pwm clock rate. bcm2835_pwm_set_clock(BCM2835_PWM_CLOCK_DIVIDER_512);

A very nice piece of work! There are some subtleties in the choice of duty factor and temperatures that I didn't initially appreciate.

@bdlabitt
Copy link

bdlabitt commented Mar 17, 2021

But the service is not starting correctly after a reboot :(
If I stop and start it using sudo systemctl stop pi_fan_hwpwm followed by a start, I get no pwm control
If I then try running pwm, the pwm pin works. Then if I stop and start the service, it finally works (controls the pwm pin).

Can't figure out why the service isn't coming up correctly after a reboot. I'd like to put this on a RPI4 based NAS, but so far this setup hasn't earned my trust. I've built two HW modules - they both work the same. The SW sometimes fails to deliver a pwm signal to the pin - even though cat /run/pi_fan_hwpwm.state is reporting a PWM signal!

@bdlabitt
Copy link

bdlabitt commented Mar 18, 2021

After a cold start #systemctl shows pi_fan_hwpwm.service is loaded active and running. However there is no physical pwm pin active.
pwm state is 89.6, but no activity at the pin, it is 0 volts. Not good.

If the service is running what would interfere with the pwm? Could it be the audio defaulting to analog which might take over the pin? At the moment I am testing headless.

The following are services that might be audio related.
alsa-restore.service is loaded active exited Save/Restore Sound card state
also-state.service is load active running Manage Sound Card State (restore and store)
sys-devices-platform-soc-fe00b840.mailbox-bcm2835_audio-sound-card0.device is loaded active and plugged (what does that mean?)

pwm does not survive reboot, nor shutdown on my system.

@bdlabitt
Copy link

bdlabitt commented Mar 20, 2021

Found the problem. bcm2835 is silently failing after a restart. If one deletes all references to pwm1 in the source and recompiles only using pwm0 (GPIO#18), the service and functionality do survive reboot. With both GPIO#18 and GPIO#13 enabled (as in the source code above) the fan does not come on after a reboot (& hot condition).

Oblique reference to silent failure in bcm2835.h
If the library runs with any other effective UID (ie not root), then
bcm2835_init() will attempt to open /dev/gpiomem, and, if
successful, will only permit GPIO operations. In particular,
bcm2835_spi_begin() and bcm2835_i2c_begin() will return false and all
other non-gpio operations may fail silently or crash.

To me, this indicates that bcm2835 has been known to silently fail...

@hahieuhass
Copy link

hahieuhass commented May 16, 2021

I want to change
#define ON_TEMP 65.
#define OFF_TEMP 60. pls

@DanielVolz
Copy link

DanielVolz commented Jun 24, 2021

@alwynallan Thank you so much for your program. I was so annoyed by the "official" configuration of the fan. The high pitched fan nosie hunted me in my dreams. Though I still hear the fan, when the CPU is under moderate load it's still much better than before.

I followed your instruction and everything worked.

Here are my sys Info:
Linux rbpi 5.10.17-v8+ #1421 SMP PREEMPT Thu May 27 14:01:37 BST 2021 aarch64 GNU/Linux

One freature request would be a config file for the temperatures and rpms. So that you can change them without recompiling the program. Because I think the pi can run up to 80 degrees Celsius without throttling the CPU.

It would be great if your solution could find its way into the official raspi-config. But I don't know if your solution fits their idea of what "should" be in the program.

@timrowledge
Copy link

timrowledge commented Jun 24, 2021

@Alexeykib
Copy link

Alexeykib commented Oct 5, 2021

Thanks a lot! Worked for me!

@chengkuangan
Copy link

chengkuangan commented Nov 24, 2021

Thank you, works perfectly on Ubuntu 20.04 on RPI4

@alwynallan
Copy link
Author

alwynallan commented Feb 24, 2022

I added code to instrument this tiny service for monitoring with Prometheus. Unless you're already using Prometheus/Grafana it's probably best to ignore this, and the default is to not use the code in this this update. To use it, get it running using my previous recipe, then

  $ sudo apt-get update
  $ sudo apt-get install ca-certificates curl gnupg lsb-release
  $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
  $ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
  $ sudo apt-get update
  $ sudo apt-get install docker-ce docker-ce-cli containerd.io
  $ sudo apt install cmake libmicrohttpd-dev
  $ cd
  $ git clone https://github.com/digitalocean/prometheus-client-c.git
  $ cd prometheus-client-c
  $ ./auto build
  $ sudo make
  $ sudo make install
  $ cd ~/1c13096c4cd675f38405702e89e0c536
  $ nano Makefile
    INSTRUMENT_FOR_PROMETHEUS := true
  $ make
  $ sudo make install
  $ curl localhost:8764/metrics

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment