Skip to content

Instantly share code, notes, and snippets.

@alwynallan
Last active April 4, 2024 04:42
Show Gist options
  • Star 39 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
  • Save alwynallan/1c13096c4cd675f38405702e89e0c536 to your computer and use it in GitHub Desktop.
Save alwynallan/1c13096c4cd675f38405702e89e0c536 to your computer and use it in GitHub Desktop.
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
@josejsarmento
Copy link

josejsarmento commented Jul 19, 2022

But when I follow the next step of make, I get the following error - " Makefile:19: *** missing separator (did you mean TAB instead of 8 spaces?). Stop."

@vincentkenny01 I've had the same error, the Makefile here is erroneously indented with spaces instead of a tab. You must replace manually those spaces with tabs on the Makefile.

@vincentkenny01
Copy link

vincentkenny01 commented Jul 19, 2022 via email

@josejsarmento
Copy link

josejsarmento commented Jul 19, 2022

@vincentkenny010 You can check my fork of this gist, I have indented correctly the Makefile there.

I think a project such as this deserves its own repo on Github, don't you think @alwynallan ? It's a feature highly requested on the internet, and there's so few working and efficient implementations of this. A repo would allow for pull requests and for issues discussions such as these.

@vincentkenny01
Copy link

vincentkenny01 commented Jul 19, 2022 via email

@timrowledge
Copy link

timrowledge commented Oct 18, 2023

I've just updated a Pi4 to Bookworm and can't compile this because bcm2835.h is not found. Searching with
find / -mount -name bcm28*.h -print
says it isn't there for reals.
WTF?
Did I forget to install some prerequisite?

D'OH! Yes. Missed the entire wget http://www.airspayce.com/mikem/bcm2835/bcm2835-1.68.tar.gz thing etc. All done.

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