-
-
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 |
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.
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:
Check this to fix it