树莓派添加温控风扇(二)

前言

上回说到,对于arm64的系统来说,目前并没有一个库可以支持对GPIO的调用。那么接下来摆在我面前的就三条路:

  1. 修改setup.c,让cpuinfo中包含相应的Hardware信息,随后重新编译内核
  2. 修改wiringPi源码
  3. 放弃使用库

可行的方法

上面三个选项中,1和2显然是极其不现实的,因此我决定采用第三条路。在之前那个issue中,我看到了这样一种用法:

At least as root GPIO works in bash on low level:
echo “23” > /sys/class/gpio/export
echo “out” > /sys/class/gpio/gpio23/direction
echo “1” > /sys/class/gpio/gpio23/value

and can be verified by LED.

根据网上的资料,/sys/class/gpio是linux通用的GPIO控制方法,看样子是好好地贯彻了Unix“一切皆文件”的思想。那么接下来简单地测试一下:

1
2
3
4
5
6
# root @ rasp in /sys/class/gpio [19:07:22]
$ echo 18>export

# root @ rasp in /sys/class/gpio [19:07:33]
$ ls
export gpio18 gpiochip0 gpiochip100 gpiochip128 unexport

可以看到系统自动生成了相关的GPIO目录,进入之后可以看到相关的文件

1
2
3
# root @ rasp in /sys/class/gpio/gpio18 [19:10:36]
$ ls
active_low device direction edge power subsystem uevent value

相关的命名还是非常简单直观的,向direction中写入信息控制GPIO的输入输出方向,value则控制的是输出值,那么就拿我们先前选定的18号针脚来测试一下吧:

1
2
3
4
5
# root @ rasp in /sys/class/gpio/gpio18 [19:11:31]
$ echo out>direction

# root @ rasp in /sys/class/gpio/gpio18 [19:14:50]
$ echo "1">value

接下来用万用表检测
万用表检测电压
3.28v,确实是1,取消输出不知道为什么向value写入0并不管用,因此只有直接向unexport写入端口号:

1
2
3
4
5
6
# root @ rasp in /sys/class/gpio [19:29:18]
$ echo 18>unexport

# root @ rasp in /sys/class/gpio [19:29:22]
$ ls
export gpiochip0 gpiochip100 gpiochip128 unexport

可以看到相关的目录自动被移除了,电压也变成了0

代码编写

实际上逻辑并不复杂,但是要考虑到程序退出之后风扇依旧会旋转,因此要做好信号的捕捉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#include <cstdio>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <malloc.h>
#include <time.h>
#include <sys/types.h>
#include <iostream>
#include <string>
#include <cstring>
#include <fcntl.h>
#include <signal.h>

using namespace std;

#define TEMP_PATH "/sys/class/thermal/thermal_zone0/temp"
#define MAX_SIZE 20
#define GPIO_PIN 18

float getTemp();
bool status = false;
char *timenow;

char *getTime()
{
free(timenow);
timenow = (char *)calloc(40, sizeof(char));
time_t rawtime;
struct tm *timeinfo;
time(&rawtime);
timeinfo = localtime(&rawtime);
*timenow = '[';
strcat(timenow, asctime(timeinfo));
timenow[strlen(timenow) - 1] = '\0';
strcat(timenow, "] ");
return timenow;
}
void setHigh(int gpio_num)
{
string str1 = "echo ";
string str2 = "> /sys/class/gpio";
system((str1 + to_string(gpio_num) + str2 + "/export").c_str());
system((str1 + "high" + str2 + "/gpio" + to_string(gpio_num) + "/direction").c_str());
printf(" Fan started.\n");
}
void removeGPIO(int gpio_num)
{
string str1 = "echo ";
string str2 = "> /sys/class/gpio";
system((str1 + to_string(gpio_num) + str2 + "/unexport").c_str());
printf(" Fan stopped.\n");
}
void sigroutine(int sig)
{
if (sig == SIGINT)
printf("%sGet SIGINT, quiting...", getTime());
else if (sig == SIGTERM)
printf("%sGet SIGTERM, quiting...", getTime());
if (status)
removeGPIO(GPIO_PIN);
exit(-1);
}
int main(int argc, char *argv[])
{
signal(SIGINT, sigroutine);
signal(SIGTERM, sigroutine);
if (argc != 1)
{
if (strcmp(argv[1], "stop") == 0)
{
removeGPIO(GPIO_PIN);
return 0;
}
}
float temp;
while (true)
{
temp = getTemp();
printf("%sTemperature is %.2f,", getTime(), temp);
if (temp > 50 && status == false)
{
setHigh(GPIO_PIN);
status = true;
}
else if (temp < 45 && status)
{
removeGPIO(GPIO_PIN);
status = false;
}
else if (!status)
printf(" nothing to do...\n");
else if (status)
printf(" keep fan working...\n");
sleep(5);
}
return 0;
}

float getTemp(void)
{
int fd;
float temp = 0;
char buf[MAX_SIZE];
fd = open(TEMP_PATH, O_RDONLY);
if (fd < 0)
{
fprintf(stderr, "failed to open thermal_zone0/temp\n");
return -1;
}
if (read(fd, buf, MAX_SIZE) < 0)
{
fprintf(stderr, "failed to read temp\n");
return -1;
}
temp = atoi(buf) / 1000.0;
close(fd);
return temp;
}

请原谅我极其不优雅不简洁的实现方式和混乱的代码风格,毕竟有太久没碰过了。但是我要说的还是那句话:

又不是不能用

再次碰壁

我按照上面的方法接入了风扇,随后echo……嗯,看起来一切正常,也没有报错什么的,除了风扇纹丝不动以外。
纹丝不动……
纹丝不动……
不动……
动……
……
Why?为什么会变成这样呢……第一次有了能调用GPIO的方法。有了能控制风扇开关的代码。两件快乐事情重合在一起。而这两份快乐,又给我带来更多的快乐。得到的,本该是像梦境一般幸福的时间……但是,为什么,会变成这样呢……

原因分析及解决方法

咳咳,请不要打我。其实仔细想想原因不难想到,GPIO是一种数字电路,而数字电路的电阻通常大得惊人,电流则是mA级别的,所能做的也仅仅就是点亮LED而已,想要让它驱动风扇实在是强人所难。因此不可避免地,我们就需要对电流进行放大。目前手头只有A42 A331的三极管,β值大约在320左右,勉强可以使用,就就地使用了。电路图大致如下(手残请忽略):
电路图
基极连接GPIO并调至高电平之后,测得发射极和和集电极之间电压为4.66v,带动风扇应该没问题
发射极和集电极间电压
接入风扇之后,风扇正常工作,至此,温控风扇连接完毕。

自启动

写了一个程序,当然要做成自启动服务了,得益于systemd,编写系统服务变得非常简单,在/etc/systemd/system目录下新建一个autofan.service文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
[Unit]
Description=Temperature controled fan daemon

[Service]
Type=simple
PIDFile=/var/run/autofan.pid
User=root
ExecStart=/usr/bin/autoFan

[Install]
WantedBy=multi-user.target

随后运行systemctl daemon-reload重新加载服务,systemctl stop autofan.service启动服务即可。

后文

虽然成功地实现了风扇的温度控制,但是文中的操控GPIO的方法实在太不优雅了,仅仅是个临时之策,无法大规模运用。当然,也不是没有将之封装成一个库的想法,但时目前并没有太多的需要以及动力,就先这样吧。又不是不能用

不要重复造轮子
——忘了谁说的了

では、諸君は。