前言:
本篇内容我们学习W3开发板40pin上的GPIO ,我们使用子系统框架来辅助学习GPIO。
一.什么是GPIO?
GPIO是通用输入输出(General Purpose Input/Output)的缩写,它是一种通用的硬件接口标准,常见于嵌入式系统和单片机中。GPIO允许处理器或控制器与外部设备(如传感器、执行器或其他电子组件)进行通信和交互。
具体来说,GPIO引脚可以通过软件配置为输入模式或输出模式。在输入模式下,GPIO引脚可以接收来自外部设备的电信号或数字信号,以便处理器读取该信号进行相应的操作。在输出模式下,处理器可以控制GPIO引脚的电平,以向外部设备发送信号或操控其他电子组件。
GPIO接口通常使用数字逻辑电平(通常是0V和3.3V或5V),用于表示逻辑上的“低”(0)或“高”,通过适当的编程和配置,可以利用GPIO接口实现各种控制和通信功能,使得嵌入式系统具有更大的灵活性和可扩展性。
二.GPIO的操作方法
1.使用命令通过sysfs文件系统控制GPIO
1.1GPIO编程方式有很多种:
- 可以写驱动程序调用GPIO函数操作GPIO
- 也可以直接通过直接操作寄存器的方式操作GPIO(不通用,不同开发板的寄存器是不一样的)
- 还可以通过sysfs的方式实现对GPIO的控制
使用sysfs方式控制GPIO,首先需要底层驱动的支持,连接好开发板之后,在内核目录下输入export ARCH=arm64去设置平台为arm64,然后在make menuconfig图形化配置界面中加入配置Device Drivers>GPIO Support>sys/class /gpio/...(sysfs interface),就是把这个选项勾选上,然后重新编译好,烧录进开发板。
1.2sysfs控制接口
sys/class/gpio/export 将gpio控制从内核空间导出到用户空间,只写sys/class/gpio/unexport 取消GPIO控制从内核空间到用户空间的导出,只写Gpiochipx(x=0,1,2...)代表控制器。
1.3gpio控制接口
在sys/class/gpio/gpioN(N=0,1,2...)下有多种属性,可以通过这些属性(一些属性可以把内核空间和用户空间进行数据交换)进行控制,从而实现对GPIO的控制:
- direction属性:值为in或者out,可以通过对该属性写入in或者out设置该gpio为输入或者输出。
- value属性:用汉语读取输入电平或者看控制输出电平,写入或者读取到1表示高电平,写入或者读取到0表示低电平。
- edge属性:设置触发电平,再将GPIO设置为中断时候才会出现和使用这个属性。
1.4GPIO编号确定
计算公式:
GPIOpin脚计算公式:pin=bank*32 + number //bank为组号,number为小组编号。
GPIO小组编号计算公式:number = group *8 + X //group(A=0,B=1,C=2,D=3)、
比如:gpio5 RK_PA3:5 * 32 +(0*8 + 3) = 163
1.5详细步骤
在内核目录下使用以下命令设置平台是arm64
export ARCH=arm64
然后再使用make menuconfig命令打开图形化配置界面 ,按照下面的步骤依次打开
make menuconfig
配置好之后我们使用串口在mobaxterm连接好开发板。
找到这个目录sys/class/gpio
接下来我们使用以下命令来控制打开LED灯,一步一步操作就可以完成实验。
1. echo 96 > /sys/class/gpio/export //gpio_request 申请导出相应的gpio
2. echo out > /sys/class/gpio/gpio96/direction //gpio_direction_output 设置相应gpio为输出方向
3. echo 1 > /sys/class/gpio/gpio96/value // gpio_set_value 设置输出高电平
4. cat /sys/class/gpio/gpio96/value // gpio_get_value 获取gpio当前状态值
5. echo 96 > /sys/class/gpio/unexport // gpio_free 释放申请的gpio
2.使用C语言通过sysfs文件系统控制GPIO
#include <stdio.h> // 引入 printf 和 sprintf 所需的头文件
#include <string.h> // 引入 strlen 和 sprintf 所需的头文件
#include <fcntl.h> // 引入 open、O_WRONLY 等所需的头文件
#include <unistd.h> // 引入 close 所需的头文件
#include <sys/stat.h>
#include <sys/stat.h> // 引入文件操作所需的头文件
int fd; // 文件描述符
int len; // 字符串长度
int ret; // 写操作的返回值
char gpio_path[100]; // 用于保存 GPIO 文件路径的缓冲区
// 导出 GPIO 引脚的函数
int gpio_export(char *argv) {
fd = open("/sys/class/gpio/export", O_WRONLY);
if (fd < 0) {
printf("打开 /sys/class/gpio/export 失败\n");
return -1; // 如果打开失败,则返回错误代码
}
len = strlen(argv); // 获取 GPIO 引脚名称的长度
ret = write(fd, argv, len); // 将引脚名称写入 export 文件
if (ret < 0) {
printf("写入 /sys/class/gpio/export 失败\n");
close(fd); // 关闭文件描述符
return -2; // 返回错误代码
}
close(fd); // 关闭文件描述符
return 0; // 成功返回 0
}
// 取消导出 GPIO 引脚的函数
int gpio_unexport(char *argv) {
fd = open("/sys/class/gpio/unexport", O_WRONLY);
if (fd < 0) {
printf("打开 /sys/class/gpio/unexport 失败\n");
return -1; // 如果打开失败,则返回错误代码
}
len = strlen(argv); // 获取 GPIO 引脚名称的长度
ret = write(fd, argv, len); // 将引脚名称写入 unexport 文件
if (ret < 0) {
printf("写入 /sys/class/gpio/unexport 失败\n");
close(fd); // 关闭文件描述符
return -2; // 返回错误代码
}
close(fd); // 关闭文件描述符
return 0; // 成功返回 0
}
// 控制 GPIO 引脚的值和方向的函数
int gpio_ctrl(char *arg, char *val) {
char file_path[100];
sprintf(file_path, "%s/%s", gpio_path, arg); // 生成完整的 GPIO 文件路径
fd = open(file_path, O_WRONLY);
if (fd < 0) {
printf("打开文件 %s 失败\n", file_path);
return -1; // 如果打开失败,则返回错误代码
}
len = strlen(val); // 获取值的长度
ret = write(fd, val, len); // 将值写入对应文件
if (ret < 0) {
printf("写入文件 %s 失败\n", file_path);
close(fd); // 关闭文件描述符
return -2; // 返回错误代码
}
close(fd); // 关闭文件描述符
return 0; // 成功返回 0
}
int gpio_read_value(char *arg ) {
sprintf(file_path, "%s/%s", gpio_path, arg); // 生成完整的 GPIO 文件路径
fd = open(file_path, O_WRONLY);
if (fd < 0) {
printf("打开文件 %s 失败\n", file_path);
return -1; // 如果打开失败,则返回错误代码
}
ret = write(fd, buf,1); // 将值写入对应文件
if (!strcmp(buf , "1")){
prinft ("the value is high\n");
return 1;
}else if (!strcmp(buf , "0")){
printf("the value is low\n");
return 0;
}
return -1;
close(fd);
}
// 主函数
int main(int argc, char *argv[]) {
if (argc < 3) {
printf("用法: ./program <GPIO编号> <值>\n");
return -1; // 参数不足时返回错误
}
sprintf(gpio_path, "/sys/class/gpio/gpio%s", argv[1]); // 生成 GPIO 路径
// 检查 GPIO 是否已经导出,若未导出则调用 gpio_export
if (access(gpio_path, F_OK)) {
gpio_export(argv[1]);
} else {
gpio_unexport(argv[1]);
}
// 设置 GPIO 为输出方向
gpio_ctrl("direction", "out");
// 设置 GPIO 的值
gpio_ctrl("value", argv[2]);
// 取消导出 GPIO
gpio_unexport(argv[1]);
return 0; // 正常结束
}
3.通过IO命令操作操作寄存器GPIO
IO命令可以在内核阶段读写寄存器
export:export文件用于通知系统需要导出控制的GPIO引脚
unexport:unexport文件用于通知取消导出
gpiochipX:gpiochipX目录保存系统中GPIO寄存器的信息,包括每个寄存器引脚的起始编号base,寄存器名称,引脚总数导出一个引脚的操作步骤。
3.1GPIO引脚的基地址
去数据手册中找到gpio寄存器的地址(gpio寄存器地址=基地址+偏移地址)
计算出地址后,连接开发板输入IO命令,和第一个是类似的。
4.通过内部映射(通过mem设备)的方式操作GPIO
4.1打开dev/mem设备
直接访问内核空间,是比较危险的操作,所以只有root用户才可以访问dev/men设备,另外有些系统也需要单独开启dev/mem。
开启方法:使用make menuconfig命令打开图形化配置
- Device Drivers ---->
- Character devices ----->
- [*] dev/mem virtual device support
编译好之后,烧录到开发板上,查看dev目录下是否有mem文件。
4.2dev/mem设备的使用方法
使用open函数将访问的物理地址与“dev/mem”文件描述符建,访问权限可以为只读(O_RDONLY),只写(O_WRONLY)的阻塞或者非阻塞方式
int fd = 0;
fd = open("dev/mem",O_RDWR | O_NDELAY) //读写权限,非阻塞
通过mmap函数将需要访问的物理地址与"/dev/mem"文件 描述符建立映射
char *mmap addr = NULL;
mmap addr = (char *) mmap (NULL,MMAP_SIZE,PROT_READ | PROT_WRITE,MAP_SHARED,fd ,MMAP_ADDR);
对地址进行访问,也就是对寄存器进行访问
int a = 0;
*(int*)mmap addr = 0xff; //写地址
a = *(int) mmap addr; //读地址
5.GPIO的调试方法
方法一:可以通过debugfs查看gpio的使用情况,如果没有debugfs,可以使用以下命令进行挂载:mount-t debugfs none sys/kernel/debug ,如果sys/kernel/debug没有文件,则需要打开CONFIG_DEBUGS_FS ,打开make menuconfig图形化配置界面
kernel hacking ----->
Compile-time checks and compiler options ----->
-*- Debug Filesystem
打开之后使用命令:cat /sys/kernel/debug/gpio,就可以查看gpio的使用情况
方法二:
- 使用sys/kernel/debug/pinctrl命令,打开目录
- 然后使用cd pinctrl-rockchip-pinctrl命令,打开目录
- 使用 cat pins查看gpio的引脚编号
- 使用 cat pinmux-pins 查看引脚的复用
- 使用 cat pingroups 查看引脚组
- 使用 cat pinconf-pins 查看电器属性
三.GPIO的子系统API函数
1.引入API函数
新的gpio子系统接口定义在inlcude/linux/gpio/consumer.h中,在新的接口涉及到了一个struct gpio_desc结构体,这个结构体定义在drivers/gpio/gpiolib.h头文件中。新的gpio子系统接口是和设备树结合使用的,所以没有设备树就不能使用新的接口。
2.获取API函数(旧API函数)
1.
gpio_request()函数
请求使用一个GPIO引脚,确保这个引脚没有被其他模块占用
使用:int gpio_request(unsigned gpio,const char *label);
gpio:想要使用的gpio引脚号 label:描述这个gpio的标签
2.
gpio_free函数()
释放之前请求的GPIO引脚,使其可供其他模块使用
使用:void gpio_free(unsigned gpio);
3.
gpio_direction_input()
设置GPIO引脚为输入模式
使用:int gpio_direction_input(unsigned gpio);
4.gpio_direction_output()
设置GPio引脚为输出模式,并设定初始输出值(高或低)
使用:int gpio_get_value(unsigned gpio)
5.
gpio_get_value()
读取GPIO引脚当前值(高或低)
使用:int gpio_get_value(unsigned gpio);
6.
gpio_set_value()
设置GPIO引脚的输出值(高或低)
使用:void gpio_set_value(unsined gpio,int value);
7.
gpio_to_irq()
将GPIO引脚映射到IRQ(中断请求),用于配置GPIO中断
使用:int gpio_to_irq(unsinged gpio)
8.
irq_to_gpio()
将IRO映射回GPIO引脚
使用:int gpio_to_irq(unsinged irq)
以下是一个LED连接到GPIO引脚控制的示例
int led_gpio = 25; // 假设LED连接到GPIO 25
// 请求GPIO
if (gpio_request(led_gpio, "LED") < 0) {
printk("GPIO request failed\n");
return -1;
}
// 设置GPIO为输出模式,并将LED设为关闭状态
gpio_direction_output(led_gpio, 0);
// 打开LED
gpio_set_value(led_gpio, 1);
// 关闭LED
gpio_set_value(led_gpio, 0);
// 释放GPIO
gpio_free(led_gpio);
3.API操作GPIO函数(新API函数--新函数一般是gpiod)
1.int gpiod_get_direction(struct gpio_desc *desc);
函数作用:获取GPIO的方向
2.int gpiod_firection_input(struct gpio_desc *desc);
函数作用:配置GPIO的方向为输入
3.int gpiod_dorecyion_output(struct gpio_desc*desc,int value);
函数作用:配置GPIO的方向为输出
4.void gpiod_get_value(const struct gpio_desc *desc);
函数作用:读取GPIO的电平状态
5.void gpiod_set_value(const gpio_desc*desc,int value);
函数作用:设置GPIO的电平状态
6.int gpiod_set_irq(const struct gpio_desc*desc);
函数作用:将GPIO描述转变为中断编号
四.实践操作
1.GPIO子系统和pinctrl子系统结合
再驱动中使用pinctrl的步骤:
再驱动中加载或者与运行时,获取设设备树对应的pinctrl结构体的指针
通过pinctrl结构体指针找到需要的 pin conctrl state结构体指针
找到pin conctrl state结构体指针后,将里面的信息设置到硬件中
在驱动写在的时候,释放pinctrl结构体的指针
1.1获取设备树对应的pinctrl结构体指针函数
函数原型:struct pinctrl * pinctrl_get(struct device * dev);
函数作用:获取设备对用的pinctrl指针函数
函数参数:dev:设备指针
返回值:成功返回对应的pinctrl的指针
1.2释放pinctrl指针函数
函数原型:void pinctrl put (struct pinctrl*p);
函数作用:释放在申请struct pinctrl时候申请的资源
函数参数:要释放的struct pinctrl*p 指针
1.3查找状态函数
函数原型:void pinctrl_state* pinctrl_lookup_state(struct pinctrl *p,const char * name);
函数作用: 根据状态在pinctr中查到对应的状态
函数参数:p:struct pinctrl*p指针 name:状态的名字,即在pinctrl-names属性中的名字
返回值:对应状态的struct pinctrl stste*指针
2.实现动态引脚复用的功能
动态实现引脚复用指的是:可以变换的使用引脚,比如我一会使用的是i2c功能,一会我使用的是gpio功能,一会我又使用串口功能。
分为两个部分:
1.在设备树中,将我们动态切换的引脚对应的复用是,在设备树节点中使用gpio子系统描述出来。
2.在驱动中解析这个信息,从而实现动态切换。