Last active
February 22, 2025 16:37
-
-
Save alwynallan/1c13096c4cd675f38405702e89e0c536 to your computer and use it in GitHub Desktop.
Hardware PWM Controller for the Raspberry Pi 4 Case Fan
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
/ | |
/ pi_fan_hwpwm.c, [email protected] 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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[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 |
Would it be possible for you to share your makefile with me otherwise I'll have to figure out a way of doing this without spending hours.
…-------- Original message --------
From: Jose Sarmento ***@***.***>
Date: 19/07/2022 15:41 (GMT+00:00)
To: alwynallan ***@***.***>
Cc: vincentkenny01 ***@***.***>, Manual ***@***.***>
Subject: Re: alwynallan/Makefile
@josejsarmento commented on this gist.
________________________________
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."
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.
—
Reply to this email directly, view it on GitHub<https://gist.github.com/1c13096c4cd675f38405702e89e0c536#gistcomment-4237630>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/ACGKX2VT6RIZQLQYU6ZYCVLVU25A7ANCNFSM4UTLG5OA>.
You are receiving this because you are subscribed to this thread.Message ID: ***@***.***>
@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.
Thanks for this. Much appreciated, I will have a go tomorrow at compiling this.
On the same note, did you turn off the option of enabling the fan at a certain temperature from the config.txt file (raspi-condig>perofrmance>fan) or did you leave it on?
I don't know if it will cause an issue having two programs managing the fan.
I've been playing with the docker version of pwm but much prefer the hardware version due to low cpu cycling.
…-------- Original message --------
From: Jose Sarmento ***@***.***>
Date: 19/07/2022 15:53 (GMT+00:00)
To: alwynallan ***@***.***>
Cc: vincentkenny01 ***@***.***>, Mention ***@***.***>
Subject: Re: alwynallan/Makefile
@josejsarmento commented on this gist.
________________________________
@vincentkenny010 You can check my fork of this gist<https://gist.github.com/josejsarmento/41dc1fba134b31e0b429c67747e5a4cd>, 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<https://github.com/alwynallan> ? It's highly requested on the internet, and there's so few working and efficient implementations of this. A repo would allow for pull requests for issues such as these.
—
Reply to this email directly, view it on GitHub<https://gist.github.com/1c13096c4cd675f38405702e89e0c536#gistcomment-4237644>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/ACGKX2XQ3MRWJODPLL2CANDVU26PRANCNFSM4UTLG5OA>.
You are receiving this because you were mentioned.Message ID: ***@***.***>
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
@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.