目录
一、GPIO的操控
base文件
label文件
ngpio文件
gpio引脚编号计算
export文件
导出测试
direction文件
value文件
active_low文件
edge文件
unexport文件
二、代码编写之GPIO输出
sprintf()函数
access()函数
测试
代码如下
三、代码编写之GPIO输入
测试
代码如下
四、代码编写之GPIO中断
poll()函数与struct pollfd 结构体
代码如下
测试
一、GPIO的操控
与 LED 设备一样, GPIO 同样也是通过 sysfs 方式进行操控,进入到/sys/class/gpio 目录下,如下所示:
gpiochip( 0、 32、 64、 96、 128):当前 SoC 所包含的 GPIO 控制器,I.MX6ULL 一共包含了 5 个 GPIO控制器,分别为 GPIO1、 GPIO2、 GPIO3、 GPIO4、 GPIO5,在这里分别对应 gpiochip0、 gpiochip32、gpiochip64、 gpiochip96、 gpiochip128 这 5 个文件夹
下面以gpiochip0目录为例,如下图
base文件
base:表示该控制器所管理的这组 GPIO 引脚中最小的编号。每一个 GPIO引脚都会有一个对应的编号, Linux 下通过这个编号来操控对应的 GPIO 引脚,这是属性文件,只读、不可写
label文件
label: 该组 GPIO 对应的标签,也就是名字,这是属性文件,只读、不可写
ngpio文件
ngpio: 该控制器所管理的 GPIO 引脚的数量(引脚编号范围是: base ~ base+ngpio-1),这是属性文件,只读、不可写
gpio引脚编号计算
譬如给定一个 GPIO引脚为 GPIO4_IO16,那它对应的编号是多少呢?
首先我们要确定 GPIO4 对应于 gpiochip96,该组 GPIO 引脚的最小编号是 96(对应于 GPIO4_IO0),所以 GPIO4_IO16 对应的编号自然是 96 + 16 = 112;
同理GPIO3_IO20 对应的编号是 64 + 20 = 84
export文件
用于将指定编号的 GPIO 引脚导出。 在使用 GPIO 引脚之前,需要将其导出,导出成功之后才能使用它。 注意 export 文件是只写文件,不能读取,将一个指定的编号写入到件 export 文中即可将对应的 GPIO 引脚导出,导出成功之后会发现在/sys/class/gpio 目录下生成了一个导出来的 GPIO 引脚对应的文件夹,用于管理、控制该 GPIO 引脚
导出测试
导出编号为 0 的 GPIO 引脚,导出成功之后在/sys/class/gpio 目录下生成了一个名为 gpio0 的文件夹,进入查看
direction文件
配置 GPIO 引脚为输入或输出模式。该文件可读、可写,读表示查看 GPIO 当前是输入还是输出模式,写表示将 GPIO 配置为输入或输出模式;读取或写入操作值为"out"(输出模式)和"in"(输入模式)
value文件
在 GPIO 配置为输出模式下,向 value 文件写入"0"控制 GPIO 引脚输出低电平,写入"1"则控制 GPIO 引脚输出高电平。在输入模式下,读取 value 文件获取 GPIO 引脚当前的输入电平状态
active_low文件
这个属性文件用于控制极性, 可读可写,默认情况下为 0,这时没有影响,如果为1,在输出模式下,输出1就变为输出低电平,输出0就变为输出高电平
edge文件
控制中断的触发模式,该文件可读可写。 在配置 GPIO 引脚的中断触发模式之前,需将其设置为输入模式
unexport文件
将导出的 GPIO 引脚删除。当使用完 GPIO 引脚之后,我们需要将导出的引脚删除,同样该文件也是只写文件、不可读,删除成功之后,之前生成的对应文件夹也会消失,如下
需要注意的是,并不是所有 GPIO 引脚都可以成功导出, 如果对应的 GPIO 已经在内核中被使用了, 那便无法成功导出,会提示“error:Device o resource busy”,也就是意味着该引脚已经被内核使用了,譬如某个驱动使用了该引脚,那么将无法导出成功
二、代码编写之GPIO输出
控制开发板上的某一个 GPIO 输出高、低不同的电平状态,需要连接一个 LED 小灯进行检验
首先就是需要将指定编号的 GPIO 引脚导出,那么这就需要先操作一下export文件,在main函数里面,一进入36行就先判断传参是否等于三个,不等则提示并退出运行,接着41行利用的sprintf函数把需要操作的gpio路径转化成为字符串类型保存在缓冲区之中,然后42行就用access函数判断缓冲中的文件是否存在,不存在就执行44-56行中的内容,45行用只写的权限打开export文件,接着50行写入传参进来的字符串并比较写入量是否等于传参字符的数量,写入成功之后,export文件就会把写入的数对应GPIO引脚文件导出
58行,调用gpio_config函数,direction参数就是用于配置gpio输出\输入模式,out参数说明配置为输出模式,此函数在17行开始,接收到两个参数后,17行就把direction参数并在gpio_path后面,此时的路径就是direction文件的绝对路径,18行就用只写的权限打开该文件,24行把第二个参数写到direction文件中并判断写入量是否等于参数字符数,写入成功之后就已经配置成输出模式了
60和62行同理58行,分别把“active_low(控制极性),0”和“value(状态值文件),命令行传参2 ”这两对参数传到gpio_config函数,于是在17行就分别合并出active_low文件和value文件的绝对路径,在18行就分别用只写方式打开,在24行就分别把第二个参数写到各自的文件之中
这用到的函数原型如下
sprintf()函数
int sprintf(char *buf, const char *format, ...);
一般会使用这个函数进行格式化转换,并将转换后的字符串存放在缓冲区中,比如上面的代码中,缓冲区是字符数组gpio_path和file_path,要转换的字符串就是路径或传递的参数
access()函数
int access(const char *pathname, int mode);
pathname: 需要进行权限检查的文件路径;
mode: 该参数可以取以下值(可以单独使用之外,还可以通过按位或运算符" | "组合在一起):
⚫ F_OK:检查文件是否存在 ⚫ R_OK:检查是否拥有读权限
⚫ W_OK:检查是否拥有写权限 ⚫ X_OK:检查是否拥有执行权限
返回值: 检查项通过则返回 0,表示拥有相应的权限并且文件存在;否则返回-1,如果多个检查项组合在一起,只要其中任何一项不通过都会返回-1
测试
第一条命令就是把gpio1_io01引脚设置为低电平,第二天就是高电平,这都需要自己用杜邦线接LED灯之类的测试,第三条就是故意输出的提示
代码如下
#include <stdio.h>
#include <unistd.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
static char gpio_path[100] = {0};
static int gpio_config(const char *attr, const char *val)
{
char file_path[100];
int fd, len = strlen(val);
sprintf(file_path, "%s/%s", gpio_path, attr);
if ((fd = open(file_path, O_WRONLY)) < 0)
{
perror("open error");
return fd;
}
if (len != write(fd, val, len))
{
perror("write error");
close(fd);
return -1;
}
close(fd);
return 0;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
fprintf(stderr, "usage:%s <gpio> <value>\n", argv[0]);
exit(-1);
}
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
if (access(gpio_path, F_OK))
{
int fd, len = strlen(argv[1]);
if ((fd = open("/sys/class/gpio/export", O_WRONLY)) < 0)
{
perror("open export error");
exit(-1);
}
if (len != write(fd, argv[1], len))
{
perror("write error");
close(fd);
exit(-1);
}
close(fd);
}
if (gpio_config("direction", "out"))
exit(-1);
if (gpio_config("active_low", "0"))
exit(-1);
if (gpio_config("value", argv[2]))
exit(-1);
exit(0);
}
三、代码编写之GPIO输入
前半段代码和上面输出差不多,gpio_config函数不变,进入main函数之后先判断命令行传参是否相等,然后根据命令行传参来决定GPIO所属组的路径,接着判断GPIO 引脚是否导出,不同点如下
在导出引脚之后,设置direction,参数in就是用于配置gpio输入模式;63行的极性不变,默认为0,这里添加一个非中断模式(也就是不启用),配置完之后67行用sprintf函数格式化value文件的路径放在file_path中,value文件在输入模式下,读取 value 文件获取 GPIO 引脚当前的输入电平状态,所以我们要读取这个文件的数据,接在68行打开该文件,73行读取该文件存在val变量,80行把读取到的val打印出来
测试
通过杜邦线将 GPIO1_IO01 引脚连接到板子上的 3.3V 电源引脚上,接着执行命令读取 GPIO 电平状态
代码如下
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
static char gpio_path[100];
static int gpio_config(const char *attr, const char *val)
{
char file_path[100];
int len;
int fd;
sprintf(file_path, "%s/%s", gpio_path, attr);
if (0 > (fd = open(file_path, O_WRONLY))) {
perror("open error");
return fd;
}
len = strlen(val);
if (len != write(fd, val, len)) {
perror("write error");
close(fd);
return -1;
}
close(fd); //关闭文件
return 0;
}
int main(int argc, char *argv[])
{
char file_path[100];
char val;
int fd;
/* 校验传参 */
if (2 != argc) {
fprintf(stderr, "usage: %s <gpio>\n", argv[0]);
exit(-1);
}
/* 判断指定编号的GPIO是否导出 */
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
if (access(gpio_path, F_OK)) {//如果目录不存在 则需要导出
int len;
if (0 > (fd = open("/sys/class/gpio/export", O_WRONLY))) {
perror("open error");
exit(-1);
}
len = strlen(argv[1]);
if (len != write(fd, argv[1], len)) {//导出gpio
perror("write error");
close(fd);
exit(-1);
}
close(fd); //关闭文件
}
/* 配置为输入模式 */
if (gpio_config("direction", "in"))
exit(-1);
/* 极性设置 */
if (gpio_config("active_low", "0"))
exit(-1);
/* 配置为非中断方式 */
if (gpio_config("edge", "none"))
exit(-1);
/* 读取GPIO电平状态 */
sprintf(file_path, "%s/%s", gpio_path, "value");
if (0 > (fd = open(file_path, O_RDONLY))) {
perror("open error");
exit(-1);
}
if (0 > read(fd, &val, 1)) {
perror("read error");
close(fd);
exit(-1);
}
printf("value: %c\n", val);
/* 退出程序 */
close(fd);
exit(0);
}
四、代码编写之GPIO中断
代码是在上面gpio输入的基础上增加的,如下
65行,在配置完极性之后,配置中断,中断触发方式为上升沿和下降沿
67行就把value文件路径格式化在缓冲区
68行把打开value文件
73行设置标志为POLLPRI,关心高优先级数据可读,也就是中断,中断就是一种高优先级事件,当中断触发时表示有高优先级数据可被读取
74行I/O操作一次用于清除状态
77行,poll函数对value 的文件描述符进行监视,value文件达到就绪态或者捕获到一个信号时返回,否则一直阻塞
89行,检查高优先级的数据是否可读
91行,把光标移到文件开头
96行,读取val值
101行,打印val值
poll()函数与struct pollfd 结构体
在 poll()函数中,则需要构造一个 struct pollfd 类型的数组,每个数组元素指定一个文件描述符以及我们对该文件描述符所关心的条件(数据可读、可写或异常情况)。 poll()函数原型如下所示
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fds: 指向一个 struct pollfd 类型的数组,数组中的每个元素都会指定一个文件描述符以及我们对该文件描述符所关心的条件, struct pollfd 结构体如下所示:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents ; /* returned events */
};
结构体参数:fd 是一个文件描述符, struct pollfd 结构体中的 events 和 revents 都是位掩码,调用者初始化 events 来指定需要为文件描述符 fd 做检查的事件。当 poll()函数返回时, revents 变量由 poll()函数内部进行设置,用于说明文件描述符 fd 发生了哪些事件( poll()没有更改 events 变量) ,我们可以对 revents 进行检查,判断文件描述符 fd 发生了什么事件。events 成员有很多标志,常用的是POLLIN(有数据可以读取) 、 POLLOUT(可写入数据)和POLLPRI(可读取高优先级数据),使用"revents & POLLIN"按位与的方式来检查是否发生了相应的事件
nfds: 参数 nfds 指定了 fds 数组中的元素个数,数据类型 nfds_t 实际为无符号整形。
timeout:用于决定 poll()函数的阻塞行为,具体用法如下:
如果 timeout 等于-1,则poll()会一直阻塞,直到 fds数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号时返回。
如果 timeout 等于 0, poll()不会阻塞,只是执行一次检查看看哪个文件描述符处于就绪态。
如果timeout大于0,则表示设置 poll()函数阻塞时间的上限值,意味着 poll()函数最多阻塞 timeout毫秒,直到 fds 数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号为止
返回值:
返回-1 表示有错误发生,并且会设置 errno。
返回 0 表示该调用在任意一个文件描述符成为就绪态之前就超时了。
返回一个正整数表示有一个或多个文件描述符处于就绪态了, 返回值表示 fds 数组中返回的 revents变量不为 0 的 struct pollfd 对象的数量。
要注意的是,当监测到某一个或多个文件描述符成为就绪态(可以读或写)时,需要执行相应的 I/O 操作,以清除该状态,否则该状态将会一直存在;
代码如下
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <poll.h>
static char gpio_path[100];
static int gpio_config(const char *attr, const char *val)
{
char file_path[100];
int len;
int fd;
sprintf(file_path, "%s/%s", gpio_path, attr);
if (0 > (fd = open(file_path, O_WRONLY))) {
perror("open error");
return fd;
}
len = strlen(val);
if (len != write(fd, val, len)) {
perror("write error");
return -1;
}
close(fd); //关闭文件
return 0;
}
int main(int argc, char *argv[])
{
struct pollfd pfd;
char file_path[100];
int ret;
char val;
/* 校验传参 */
if (2 != argc) {
fprintf(stderr, "usage: %s <gpio>\n", argv[0]);
exit(-1);
}
/* 判断指定编号的GPIO是否导出 */
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]);
if (access(gpio_path, F_OK)) {//如果目录不存在 则需要导出
int len;
int fd;
if (0 > (fd = open("/sys/class/gpio/export", O_WRONLY))) {
perror("open error");
exit(-1);
}
len = strlen(argv[1]);
if (len != write(fd, argv[1], len)) {//导出gpio
perror("write error");
exit(-1);
}
close(fd); //关闭文件
}
/* 配置为输入模式 */
if (gpio_config("direction", "in"))
exit(-1);
/* 极性设置 */
if (gpio_config("active_low", "0"))
exit(-1);
/* 配置中断触发方式: 上升沿和下降沿 */
if (gpio_config("edge", "both"))
exit(-1);
/* 打开value属性文件 */
sprintf(file_path, "%s/%s", gpio_path, "value");
if (0 > (pfd.fd = open(file_path, O_RDONLY))) {
perror("open error");
exit(-1);
}
/* 调用poll */
pfd.events = POLLPRI; //只关心高优先级数据可读(中断)
read(pfd.fd, &val, 1);//先读取一次清除状态
for ( ; ; ) {
ret = poll(&pfd, 1, -1); //调用poll
if (0 > ret) {
perror("poll error");
exit(-1);
}
else if (0 == ret) {
fprintf(stderr, "poll timeout.\n");
continue;
}
/* 校验高优先级数据是否可读 */
if(pfd.revents & POLLPRI) {
if (0 > lseek(pfd.fd, 0, SEEK_SET)) {//将读位置移动到头部
perror("lseek error");
exit(-1);
}
if (0 > read(pfd.fd, &val, 1)) {
perror("read error");
exit(-1);
}
printf("GPIO中断触发<value=%c>\n", val);
}
}
/* 退出程序 */
exit(0);
}
测试
可以使用杜邦线将 GPIO1_IO01 引脚连接到 GND 或 3.3V 电源引脚上,来回切 换,使得GPIO1_IO01 引脚的电平状态发生由高到低或由低到高的状态变化,以验证 GPIO 中断的边沿触发 情况;当发生中断时,终端将会打印相应的信息
因为程序设置死循环,只能使用ctrl+c组合键强制退出该程序