-
-
Save alwynallan/1c13096c4cd675f38405702e89e0c536 to your computer and use it in GitHub Desktop.
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, [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); | |
} |
[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 |
Thank you so much for this! Worked perfectly for a 64-bit Ubuntu server Raspberry Pi 8GB. As for the Prometheus integration, should I first get Prometheus running via its docker image and target the port you specify here (8764) or is this prometheus-client-c a standalone version?
EDIT: It seems I can't build the Prometheus C client, it gave me a Make error on the step GOPATH in the Dockerfile:
+ GOPATH=/gopath /usr/local/go/bin/go get github.com/prometheus/prom2json
/bin/sh: 1: /usr/local/go/bin/go: Exec format error
The command '/bin/sh -c set -x && apt-get update && ... && chmod +x /entrypoint && GOPATH=/gopath /usr/local/go/bin/go get github.com/prometheus/prom2json && ... && rm -rf /var/lib/apt/lists/*' returned a non-zero code: 2
make: *** [Makefile:2: docker] Error 2
Check this to fix it
hi,
I am using RPI 4 8GB model headless with Raspberry Pi OS Lite 64bit installed. I have this fan installed and connected to GPIO 18 pin.
My system is up to date.
I can follow your guide up to this point
$ sudo apt install -y build-essential git stress-ng
$ cd
$ wget http://www.airspayce.com/mikem/bcm2835/bcm2835-1.68.tar.gz
$ tar zxvf bcm2835-1.68.tar.gz
$ cd bcm2835-1.68
$ ./configure
$ make
$ sudo make install
$ cd
$ git clone https://gist.github.com/1c13096c4cd675f38405702e89e0c536.git
$ cd 1c13096c4cd675f38405702e89e0c536
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 have tried a few times but I am kind of lost here on what to do next.
Is there an issue with the file/format or do you think I don't have the right hardware to follow this through?
Please let me know.
Best,
V
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.
@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.
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.
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