LV532-gpio控制
在 linux 下,如何对 gpio 进行控制?怎么调试?若笔记中有错误或者不合适的地方,欢迎批评指正 😃。
一、准备工作
这里既然是做 GPIO 的调试,那肯定要先知道几个 GPIO,我这里用的是整点原子 alpha 开发板,我们准备三种 GPIO 用于测试。我们选择连接了外设的 GPIO,例如连接了 I2C 设备的 I2C1,LED 灯以及按键这些,用来测试的话现象比较直观。
1. LED 灯
同 《07-设备驱动子系统/05-pinctrl 与 gpio/LV530-gpio 调试.md》的一、准备工作。
2. KEY 按键
同 《07-设备驱动子系统/05-pinctrl 与 gpio/LV530-gpio 调试.md》的一、准备工作。
3. I2C1——ap3216c
同 《07-设备驱动子系统/05-pinctrl 与 gpio/LV530-gpio 调试.md》的一、准备工作。
4. 参考文档
linux 内核源码中有一些参考文档可以看:sysfs.txt - Documentation/gpio/sysfs.txt
二、GPIO 的控制
GPIO 软件编程方式有多种, 可以写驱动程序调用 GPIO 函数操作 GPIO, 也可以直接通过操作寄存器的方式操作 GPIO, 还可以通过 sysfs 方式实现对 GPIO 的控制。 本章节我们来学习使用 sysfs 方式实现对 GPIO 的控制。
1. 使用命令通过 sysfs 文件系统控制 GPIO
1.1 开始之前要知道的
1.1.1 内核配置
使用 sysfs 方式控制 gpio, 首先需要底层驱动的支持, 执行以下命令打开图形化配置界面
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v6_v7_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig按以下路径找到配置菜单:
Device Drivers --->
-*- GPIO Support --->
[*] /sys/class/gpio/... (sysfs interface)
1.1.2 GPIO 编号计算
i.MX6ULL 的 GPIO 引脚被组织成多个 Bank,每个 Bank 包含 32 个 GPIO 引脚。这个我们可以看数据手册过着参考手册,我这里看的数据手册:

GPIO 引脚的编号通常以 GPIOx_y 的形式表示,其中 x 表示 Bank 编号,y 表示该 Bank 中的引脚编号。例如:
GPIO1_0表示 Bank 1 的第 0 个引脚。GPIO2_15表示 Bank 2 的第 15 个引脚。
那么,i.MX6ULL 的 GPIO 编号可以通过以下公式计算:
GPIO编号 = (Bank编号 - 1) * 32 + 引脚编号像正点原子 alpha 开发板上的 led 接在 GPIO1_IO03 上,这里 GPIO 编号就是 3.
1.2 GPIO 控制器相关
1.2.1 怎么查看有哪些 GPIO 控制器
/sys/bus/gpio/devices 目录下,列出了所有的 GPIO 控制器,如下表示有 5 个 GPIO 控制器:
ls -alh /sys/bus/gpio/devices
1.2.2 每个 GPIO 控制器的详细信息
GPIO 控制器的详细信息在 /sys/class/gpio/gpiochipXXX 下,有 gpio 这些信息:
ls -alh /sys/class/gpio/gpiochipX
base # 这个 GPIO 控制器的 GPIO 编号
device
label # 名字
ngpio # 引脚个数
power
subsystem
uevent1.3 gpio 导出与取消
我们可以通过 sysfs 文件系统,将 GPIO 导出到用户空间,这样我们就可以通过 sysfs 直接控制 gpio。sysfs 控制接口为 /sys/class/gpio/export 和/sys/class/gpio/unexport。 如下图所示:

export 和 unexport 这两个文件 都是只写的。 这里的 gpiochipX 代表 GPIO 控制器。
1.3.1 gpio 导出
/sys/class/gpio/export 用于将 GPIO 控制从内核空间导出到用户空间 。
export: 用于将指定编号的 GPIO 引脚导出。 在使用 GPIO 引脚之前, 需要将其导出,导出成功之后才能使用它。 注意 export 文件是只写文件, 不能读取, 将一个指定的编号写入到 export 文件中即可将对应的 GPIO 引脚导出, 以 GPIO0_PB7 为例(pin 计算值为 15) 使用 export 文件进行导出(如果这个 gpio 被内核或者用户程序占用,那么就会导出不成功), 导出成功如下图所示:
echo 3 > /sys/class/gpio/export
会发现在/sys/class/gpio 目录下生成了一个名为 gpio3 的文件夹(gpioX, X 表示对应的编 号) , 该文件夹就是导出来的 GPIO 引脚对应的文件夹, 用于管理、 控制该 GPIO 引脚。导出成功之后进入 gpio3 文件夹如下图所示:

会发现里面生成了很多属性文件,通过在这些文件就可以控制 gpio,这些文件我们后面再来了解。
1.3.2 gpio 取消导出
/sys/class/gpio/unexport 用于取消 GPIO 控制从内核空间到用户空间的导出。
unexport: 将导出的 GPIO 引脚删除。 当使用完 GPIO 引脚之后, 需要将导出的引脚删除, 同样该文件也是只写文件、 不可读, 使用 unexport 文件进行删除 GPIO0_PB7, 删除成功如下图所示:
echo 3 > /sys/class/gpio/unexport
可以看到之前生成的 gpio3 文件夹消失了!
1.3.3 导出失败?
需要注意的是, 并不是所有 GPIO 引脚都可以成功导出, 如果对应的 GPIO 已经被导出或者在内核中被使用了, 那便无法成功导出, 例如我用的整点原子 alpha 开发板中,led 灯是接在 GPIO1_IO03,对应的 gpio 编号为 3,这里导出就会失败,导出失败如下图所示:

这个是因为设备树中用到了这个 gpio:我们可以看一下 sys/kernel/debug/gpio:
cat sys/kernel/debug/gpio
如会发现,这里 gpio-3 是被用掉了的,这个时候我们就需要找到设备树中对应的地方,就找相关的关键词,可以找到 imx6ul-14x14-evk.dtsi 这个文件中有:
&tsc {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_tsc>;
xnur-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>;
measure-delay-time = <0xffff>;
pre-charge-time = <0xfff>;
status = "okay";
};这里我们就可以把它屏蔽掉,然后更新设备树:

这个时候重新导出就没问题啦。
echo 3 > export这个时候就正常了。
1.4 gpio 属性修改
导出 gpio 成功之后进入对应的文件夹,会发现生成了一些属性文件:

可以看到 gpio3 文件夹下分别有 active_low、 device、 direction、 edge、 power、 subsystem、uevent、 value 八个文件, 需要关心的文件是 active_low、 direction、 edge 以及 value 这四个属性文件, 接下来分别介绍这四个属性文件的作用。
1.4.1 direction
direction:配置 GPIO 引脚为输入或输出模式。 该文件可读、 可写, 读表示查看 GPIO 当前是输入还是输出模式, 写表示将 GPIO 配置为输入或输出模式; 读取或写入操作可取的值为 "out"(输出模式) 和 "in"(输入模式) 。
在“/sys/class/gpio/gpio15”目录下使用 cat 命令查看 direction 输入输出模式, 如下图所示:
cat direction
默认状态下的输入输出状态为“in”, 由于 direction 为可读可写, 可以使用以下命令将模式配置为输出, 配置完成如下图所示
echo out > direction
cat direction
1.4.2 active_low
active_low: 用于控制 极性的属性文件, 可读可写, 默认情况下为 0, 使用 cat 命令进行文件内容的查看, 如下图所示 :
cat active_low当 active_low 等于 0 时, value 值若为 1 则引脚输出高电平, value 值若为 0 则引脚输出低电平。 当 active_low 等于 1 时 , value 值若为 0 则引脚输出高电平, value 值若为 1 则引脚输出低电平。
1.4.3 edge
edge:控制中断的触发模式, 该文件可读可写。 在配置 GPIO 引脚的中断触发模式之前,需将其设置为输入模式, 四种触发模式的设置如下所示:
echo "none" > edge # 非中断引脚
echo "rising" > edge # 上升沿触发
echo "falling" > edge # 下降沿触发
echo "both" > edge # 边沿触发1.4.4 value
value: 设置高低电平, 如果我们要把这个管脚设置成高电平, 我们只需要给 value 设置成 1 ,即可, 反之, 则设置成 0。 使用命令 :
# cat active_low # 通过它看极性
echo 1 > value # 把 GPIO 设置成高电平
echo 0 > value # 把 GPIO 设置成低电平
这个引脚是连接了 LED 灯,根据原理图可知,LED 的一端接 GPIO,另一端接 3.3v,这里输出 1 会熄灭 LED,输出 0 会点亮 LED。
2. 使用 C 程序通过 sysfs 文件系统控制 GPIO
上面我们已经实现了用 cat、echo 通过 sysfs 文件系统控制 gpio,其实就会发现,我们做的都是去操作一些属性文件,那么既然如此,我们肯定是可以通过 C 语言程序来操作这些文件的。
2.1 关键函数实现
2.1.1 导出 GPIO 引脚
导出的时候,我们需要访问 /sys/class/gpio/export,通过 open 和 write 函数就可以实现:
/**
* @brief gpio_export()
* @note 导出 GPIO 引脚, 相当于 echo X > /sys/class/gpio/export
* 导出成功会生成 /sys/class/gpio/gpioX 目录
* @param [in] pins : gpio 编号,传进来的是一个字符串
* @param [out]
* @retval
*/
int gpio_export(const char *pins)
{
int fd = -1;
size_t len = 0; // size_t --> 32bit 下通常为 unsigned int
ssize_t ret = 0;// ssize_t --> 32bit 下通常为 int
char attr_file_path[32] = {0};
if(pins == NULL)
{
PRTE("pins is NULL\n");
return -1;
}
sprintf(attr_file_path, "%s", "/sys/class/gpio/export"); // 构建文件路径
fd = open(attr_file_path, O_WRONLY); // 打开 export 文件
if (fd < 0)
{
PRTE("open %s error!\n", attr_file_path); // 打开文件失败
return -2;
}
//PRT("open %s success!\n", attr_file_path);
len = strlen(pins); // 获取参数字符串的长度
ret = write(fd, pins, len); // 将参数字符串写入文件, 导出 GPIO 引脚
if (ret < 0)
{
PRTE("write %s error!\n", attr_file_path); // 写入文件失败
return -3;
}
close(fd); // 关闭文件
return 0;
}2.1.2 取消导出 GPIO 引脚
/**
* @brief gpio_unexport()
* @note 取消导出 GPIO 引脚, 相当于 echo X > /sys/class/gpio/unexport
* 取消导出成功会删除 /sys/class/gpio/gpioX 目录
* @param [in] pins : gpio 编号,传进来的是一个字符串
* @param [out]
* @retval
*/
int gpio_unexport(const char *pins)
{
int fd = -1;
size_t len = 0;
ssize_t ret = 0;
char attr_file_path[32] = {0};
if(pins == NULL)
{
PRTE("pins is NULL\n");
return -1;
}
sprintf(attr_file_path, "%s", "/sys/class/gpio/unexport"); // 构建文件路径
fd = open(attr_file_path, O_WRONLY); // 打开 unexport 文件
if (fd < 0)
{
PRTE("open %s error!\n", attr_file_path); // 打开文件失败
return -2;
}
//PRT("open %s success!\n", attr_file_path);
len = strlen(pins); // 获取参数字符串的长度
ret = write(fd, pins, len); // 将参数字符串写入文件, 取消导出 GPIO 引脚
if (ret < 0)
{
PRTE("write %s error!\n", attr_file_path); // 写入文件失败
return -3;
}
close(fd); // 关闭文件
return 0;
}2.1.3 控制 GPIO 属性
/**
* @brief gpio_ctrl()
* @note 控制 GPIO 引脚的属性
* /sys/class/gpio/gpioX/direction : 配置 GPIO 引脚为输入或输出模式。
* /sys/class/gpio/gpioX/active_low : 配置 GPIO 极性
* /sys/class/gpio/gpioX/edge : 控制中断的触发模式
* /sys/class/gpio/gpioX/value : 设置高低电平/读取引脚高低电平状态
* @param [in] p_gpio_path : gpio 路径,/sys/class/gpio/gpioX
* @param [in] p_attr_name : 属性文件名称,direction,active_low,edge,value
* @param [in] val : 要写入的值 direction - in/out ;
* active_low - 1/0 ;
* edge - none/rising/falling/both ;
* value - 1/0 ;
* @param [out]
* @retval
*/
int gpio_ctrl(char *p_gpio_path, const char *p_attr_name, const char *val)
{
int fd = -1;
size_t len = 0;
ssize_t ret = 0;
char attr_file_path[32] = {0}; // 文件路径
if(p_gpio_path == NULL || p_attr_name == NULL || val == NULL)
{
PRTE("p_gpio_path, p_attr_name or val is NULL\n");
return -1;
}
sprintf(attr_file_path, "%s/%s", p_gpio_path, p_attr_name); // 构建文件路径, 格式为 p_gpio_path/p_attr_name
fd = open(attr_file_path, O_WRONLY); // 打开文件
if (fd < 0)
{
PRTE("open %s error!\n", attr_file_path); // 打开文件失败
return -2;
}
//PRT("open %s success!\n", attr_file_path);
len = strlen(val); // 获取参数字符串的长度
ret = write(fd, val, len); // 将参数字符串写入文件, 控制 GPIO 引脚的属性
if (ret < 0)
{
PRTE("write %s error!\n", attr_file_path); // 写入文件失败
return -3;
}
close(fd); // 关闭文件
return 0;
}2.1.4 读取 GPIO 数据
/**
* @brief gpio_read_value()
* @note 获取 GPIO 引脚的电平状态
* /sys/class/gpio/gpioX/value : 设置高低电平/读取引脚高低电平状态
* @param [in] p_gpio_path : gpio 路径,/sys/class/gpio/gpioX
* @param [in] p_attr_name : 属性文件名称,value
* @param [out]
* @retval
*/
int gpio_read_value(char *p_gpio_path, const char *p_attr_name)
{
int fd = -1;
ssize_t ret = 0;
char attr_file_path[32] = {0}; // 文件路径
char buf[2] = {0}; // 缓冲区
if(p_gpio_path == NULL || p_attr_name == NULL)
{
PRTE("p_gpio_path, p_attr_name is NULL\n");
return -1;
}
sprintf(attr_file_path, "%s/%s", p_gpio_path, p_attr_name); // 构建文件路径, 格式为 "p_gpio_path/p_attr_name"
fd = open(attr_file_path, O_RDONLY); // 打开文件
if (fd < 0)
{
PRTE("open %s error!\n", attr_file_path); // 打开文件失败
return -1;
}
//PRT("open %s success!\n", attr_file_path);
ret = read(fd, buf, 1); // 读取文件内容到缓冲区
if(ret < 0)
{
PRTE("read from %s fail!\n", attr_file_path);
}
if (!strcmp(buf, "1"))
{
PRT("The value is high!\n"); // GPIO 引脚值为高电平
return 1;
}
else if (!strcmp(buf, "0"))
{
PRT("The value is low!\n"); // GPIO 引脚值为低电平
return 0;
}
close(fd); // 关闭文件
return -1;
}2.1.5 监听中断
/**
* @brief gpio_interrupt()
* @note 监听 GPIO 引脚的中断事件, 中断发生时读取 gpio 的值
* /sys/class/gpio/gpioX/value : 设置高低电平/读取引脚高低电平状态
* @param [in] p_gpio_path : gpio 路径,/sys/class/gpio/gpioX
* @param [in] p_attr_name : 属性文件名称,value
* @param [out]
* @retval
*/
int gpio_interrupt(char *p_gpio_path, const char *p_attr_name)
{
int fd = -1;
ssize_t ret = 0;
char attr_file_path[32] = {0}; // 文件路径
struct pollfd fds[1]; // poll 结构体数组
char buf[2] = {0}; // 缓冲区
sprintf(attr_file_path, "%s/%s", p_gpio_path, p_attr_name); // 构建文件路径
fd = open(attr_file_path, O_WRONLY); // 打开文件
if (fd < 0)
{
PRTE("open %s error!\n", attr_file_path); // 打开文件失败
return -1;
}
//PRT("open %s success!\n", attr_file_path);
memset((void *)fds, 0, sizeof(fds)); // 清空 poll 结构体数组
fds[0].fd = fd; // 设置 poll 结构体的文件描述符
fds[0].events = POLLPRI; // 设置 poll 结构体的事件类型为 POLLPRI, 表示有紧急数据可读
read(fd, buf, 2); // 读取文件内容, 清除中断事件
while (1)
{
ret = poll(fds, 1, -1); // 调用 poll 函数等待中断事件发生, 阻塞直到事件发生
if (ret <= 0)
{
PRTE("poll error \n"); // 调用 poll 失败或超时
return -1;
}
PRT("poll return!ret=%d,fds[0].revents=0x%x,fds[0].events=0x%x\n", ret, fds[0].revents, fds[0].events); // 输出中断事件的值
if (fds[0].revents & POLLPRI)
{
lseek(fd, 0, SEEK_SET); // 重新定位文件指针到文件开头
read(fd, buf, 2); // 读取文件内容, 获取中断事件的值
PRT("value from %s : buf[0]=%x buf[1]=%x\n", attr_file_path, buf[0], buf[1]); // 输出中断事件的值
}
sleep(1);
}
return 0;
}2.2 控制 GPIO 输出 demo
int gpio_out_demo(int argc, const char *argv[]) // 主函数
{
char gpio_path[32] = {0}; // 文件路径
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]); // 构建 GPIO 路径, 格式为“/sys/class/gpio/gpio 引脚号”
if (access(gpio_path, F_OK)) // 检查 GPIO 路径是否存在
{
gpio_export(argv[1]); // 不存在则导出 GPIO 引脚
}
PRT("gpio_path:%s\n", gpio_path);
gpio_ctrl(gpio_path, "direction", "out"); // 配置 GPIO 为输出模式
gpio_ctrl(gpio_path, "value", argv[2]); // 控制 GPIO 输出高低电平
gpio_unexport(argv[1]); // 最后取消导出 GPIO 引脚
return 0; // 返回 0 表示程序正常退出
}这个就主要是看 led 灯的亮灭了:

2.3 控制 GPIO 输入 demo
int gpio_in_demo(int argc, const char *argv[]) // 主函数
{
int value = -1;
char gpio_path[32] = {0}; // 文件路径
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]); // 构建 GPIO 路径, 格式为 "/sys/class/gpio/gpio 引脚号"
if (access(gpio_path, F_OK)) // 检查 GPIO 路径是否存在
{
gpio_export(argv[1]); // 不存在则导出 GPIO 引脚
}
PRT("gpio_path:%s\n", gpio_path);
gpio_ctrl(gpio_path, "direction", "in"); // 配置 GPIO 为输入模式
value = gpio_read_value(gpio_path, "value"); // 读取 GPIO 引脚的值
PRT("The value is %d\n", value); // 打印读取的 GPIO 引脚的值
gpio_unexport(argv[1]); // 最后取消导出 GPIO 引脚
return 0; // 返回 0 表示程序正常退出
}我们编译后,拷贝到开发板,抬起按键的时候执行一次,按下按键不松,再执行一次:

2.4 监听 GPIO 中断 demo
int gpio_interrupt_demo(int argc, const char *argv[]) // 主函数
{
char gpio_path[32] = {0}; // 文件路径
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]); // 构建 GPIO 路径
if (access(gpio_path, F_OK)) // 检查 GPIO 路径是否存在
{
gpio_export(argv[1]); // 不存在则导出 GPIO 引脚
}
PRT("gpio_path:%s\n", gpio_path);
gpio_ctrl(gpio_path, "direction", "in"); // 设置 GPIO 引脚为输入模式
gpio_ctrl(gpio_path, "edge", "rising"); // 设置 GPIO 引脚的中断触发方式为上升沿
gpio_interrupt(gpio_path, "value"); // 监听 GPIO 引脚的中断事件
gpio_unexport(argv[1]); // 最后取消导出 GPIO 引脚
return 0; // 返回 0 表示程序正常退出
}