-
-
Save Zorono/d41e96d624e396af7d878c9d2cda0214 to your computer and use it in GitHub Desktop.
Hardware PWM Controller for the Raspberry Pi 4 Case Fan
This file contains 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 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 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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment