本文主要是介绍Linux嵌入式驱动开发12——pinctl和gpio子系统实践操作,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 全系列传送门
- 什么是pinctrl和gpio子系统
- pinctel子系统
- pinctel子系统功能
- 查看属性表达
- 查看pinctrl
- gpio子系统
- gpio子系统功能
- 常用gpio子系统提供的api函数
- gpio_request函数
- gpio_free函数
- gpio_direction_input函数
- gpio_dierction_output函数
- gpio_get_value函数
- gpio_set_value函数
- 设备树节点
- 添加节点信息
- pinfunc.h文件查找宏定义
- 测试程序源码
- app.c
- driver.c
- 结果验证
- 查看设备树节点
- 安装ko模块生成驱动
- 执行app
全系列传送门
Linux嵌入式驱动开发01——第一个驱动Hello World(附源码)
Linux嵌入式驱动开发02——驱动编译到内核
Linux嵌入式驱动开发03——杂项设备驱动(附源码)
Linux嵌入式驱动开发04——应用层和内核层数据传输
Linux嵌入式驱动开发05——物理地址到虚拟地址映射
Linux嵌入式驱动开发06——第一个相对完整的驱动实践编写
Linux嵌入式驱动开发07——GPIO驱动过程记录(飞凌开发板)
Linux嵌入式驱动开发08——字符设备(步步为营)
Linux嵌入式驱动开发09——平台总线详解及实战
Linux嵌入式驱动开发10——设备树开发详解
Linux嵌入式驱动开发11——平台总线模型修改为设备树实例
Linux嵌入式驱动开发12——pinctl和gpio子系统实践操作
Linux嵌入式驱动开发13——ioctl接口(gpio控制使用)
Linux嵌入式驱动开发14——中断的原理以及按键中断的实现(tasklet中断下文)
Linux嵌入式驱动开发15——等待队列和工作队列
Linux嵌入式驱动开发16——按键消抖实验(内核定时器)
Linux嵌入式驱动开发17——输入子系统
Linux嵌入式驱动开发18——I2C通信
什么是pinctrl和gpio子系统
在学习单片机(比如51单片机和STM32)的时候,我们可以直接对单片机的寄存器进行操作,进而达到控制pin脚的目的。
而Linux系统相较于一个单片机系统,要庞大而复杂得多,因此在Linux系统中我们不能直接对pin脚进行操作。
Linux系统讲究驱动分层,pinctrl子系统和GPIO子系统就是驱动分层的产物。如果我们要操作pin脚,就必须要借助pinctrl子系统和GPIO子系统。
pinctrl子系统的作用是pin config(引脚配置)和pin mux(引脚复用),而如果pin脚被复用为了GPIO(注意:GPIO功能只是pin脚功能的一种),就需要再借助GPIO子系统对pin脚进行控制了,GPIO子系统提供了一系列关于GPIO的API函数,供我们调用。
pinctel子系统
pinctel子系统功能
- 管理系统中所有的可以控制的pin,在系统初始化的时候,枚举所有可以控制的pin,并标识这些pin
- 管理这些pin的复用,对于SOC而言,其引脚除了配置成普通的GPIO之外,若干个引脚还可以组成一个pin group,形成特定的功能
- 配置这些pin的特性,例如使能关闭引脚上的pull-up、pull-down电阻,配置引脚的driver strength
不同SOC厂家的pin controller的节点
这些节点里面都是把某些引脚复用成某些功能
查看属性表达
不同的SOC厂家,他们的pin controller的节点里面的属性表达的含义都是不同的,所以,要想查看这些属性的含义,去到Linux源码目录
/Documentation/devicetree/bindings
/Documentation/devicetree/bindings/pinctrl
进入到pinctrl文件中,可以看到txt文件,我们的型号是im6q,找到对应的文件就可以打开查看信息
查看pinctrl
打开我们板子对应的dts文件,这里的我是imx6q-c-sabresd.dts
打开之后发现没有我们想要看到的pinctrl相关的代码,都是一些引用添加节点信息
根据头文件,我们继续打开imx6qdl-sabresd.dtsi文件
在imx6qdl-sabresd.dtsi文件里就可以看到详细的pinctrl的信息了
gpio子系统
gpio子系统功能
也就是,我们的pinctrl子系统配置完成后,我们接下来就是要交给gpio子系统来进行控制
在设备树文件中代码示例如下
gpio_user: gpios{pinctrl-names = "default";pinctrl-0 = <&pinctrl_user>; // default的状态就是对应pinctrl_user的节点compatible = "gpio-user";gpio0{label = "D01";gpios = <&gpio3 15 1>;default-direction = "out";};gpio1{label = "D02";gpios = <&gpio2 21 1>;default-direction = "out";};};
针对gpios = <&gpio3 15 1>;
代表的含义就是,gpio3组中的第15个IO口,给配置为1
那么这里使用了pinctrl和gpio子系统之后,就可以替代之前的这种写法
在之前,我们控制引脚需要对寄存器进行控制,是reg = <0x20ac000 0x0000004>;
这样的形式进行描述,而现在使用了gpio子系统之后,就可以更方便规范的使用。
常用gpio子系统提供的api函数
gpio_request函数
在这里我们看到了一个of函数来获取gpio的编号,就是of_get_named_gpio
函数,这个函数的详情如下
gpio_free函数
gpio_direction_input函数
gpio_dierction_output函数
gpio_get_value函数
gpio_set_value函数
设备树节点
添加节点信息
我们从上次的代码,设备树的驱动代码进行修改,本次使用了pinctrl和gpio子系统,本质上是替代了之前的寄存器操作,使用函数来进行电平的控制。
现在首先还是在设备树文件中添加我们的节点信息。
pinctrl_user: usergrp {fsl,pins = <MX6QDL_PAD_EIM_DA15__GPIO3_IO15 0x1b0b0MX6QDL_PAD_EIM_A17__GPIO2_IO21 0x1b0b0>;};
我们要控制的gpio引脚是EIM_A17
那么,我们是如何找到EIM_A17对应的宏定义MX6QDL_PAD_EIM_A17__GPIO2_IO21呢?
pinfunc.h文件查找宏定义
在我们的dts文件夹下,有一个对应imx6q-pinfunc.h文件,里面定义着所需要的宏定义
这样,我们只需要按照需要查找就可以了
通过搜索EIM_A17.这样就找到了对应的宏定义,通过后面的后缀名称,可以看到有很多的功能
我们也可以通过查找芯片手册,看到EIM_A17对应的引脚的说明IOMUXC_SW_MUX_CTL_PAD_EIM_ADDR17
回到我们刚才说的,根据名称,我们需要一个gpio的功能,所以选择的宏定义是
#define MX6QDL_PAD_EIM_A17__GPIO2_IO21 0x0f0 0x404 0x000 0x5 0x0
MX6QDL_PAD_EIM_A17__GPIO2_IO21
所代表的数 0x0f0 0x404 0x000 0x5 0x0
实际上就是电器属性,都已经配置好了,这样就不用我们一个一个的去手动的查找芯片手册然后进行配置。
0x0f0 0x404 0x000 0x5 0x0
这 5个值的含义如下所示:
<mux_reg conf_reg input_reg mux_mode input_val>
0x0f0 :mux_reg寄存器偏移地址,设备树中的 iomuxc节点就是 IOMUXC外设对应的节点,根据其 reg属性可知 IOMUXC外设寄存器起始地址为 0x020e0000。因此20E_0000h base + F0h offset = 20E_00F0h, IOMUXC_SW_MUX_CTL_PAD_EIM_ADDR17寄存器地址正好是 20E_00F0h,如图所示,所以这里的mux_reg = 0x0f0
0x404 conf_reg寄存器偏移地址,和 mux_reg一样, 0x020e0000+0x404=0x020e0404这个就 是寄存器IOMUXC_SW_MUX_CTL_PAD_EIM_ADDR17的地址。
0x000 input_reg寄存器偏移地址,有些外设有 input_reg寄存器,有 input_reg寄存器的外设需要配置 input_reg寄存器。没有的话就不需要设置
0x5 mux_reg寄存器值,在这里就相当于设置IOMUXC_SW_MUX_CTL_PAD_EIM_ADDR17寄存器为 0x5,也即是设置这个 PIN复用为 GPIO2_IO21。
0x0 input_reg寄存器值,在这里无效。
这就是宏 IOMUXC_SW_MUX_CTL_PAD_EIM_ADDR17的含义,看的比较仔细的同学应该会发现并没有 conf_reg寄存器的值, config_reg寄存器是设置一个 PIN的电气特性的,这么重要的寄存器怎么没有值呢?
MX6QDL_PAD_EIM_A17__GPIO2_IO21 0x1b0b0
MX6QDL_PAD_EIM_A17__GPIO2_IO21 我们上面已经分析了,就剩下了一个 0x0b0b1反应快的同学应该已经猜出来了, 0x0b0b1就是 conf_reg寄存器值!
此值由用户自行设置,通过此值来设置一个 IO的上 /下拉、驱动能力和速度等。在这里就相当于设置寄存器IOMUXC_SW_MUX_CTL_PAD_EIM_ADDR17的值为 0x0b0b1。
如果不知道要设置成多少,这里我是当作普通gpio来进行操作,所以只需要参考led灯和按键的配置信息就可以了。
这里还有一个点需要注意,就是一个io引脚只能定义一个功能,就例如我们的EIM_A17,有这么多
#define MX6QDL_PAD_EIM_A17__EIM_ADDR17 0x0f0 0x404 0x000 0x0 0x0
#define MX6QDL_PAD_EIM_A17__IPU1_DISP1_DATA12 0x0f0 0x404 0x000 0x1 0x0
#define MX6QDL_PAD_EIM_A17__IPU2_CSI1_DATA12 0x0f0 0x404 0x8b8 0x2 0x1
#define MX6QDL_PAD_EIM_A17__GPIO2_IO21 0x0f0 0x404 0x000 0x5 0x0
#define MX6QDL_PAD_EIM_A17__SRC_BOOT_CFG17 0x0f0 0x404 0x000 0x7 0x0
所以我们就要在设备树文件中,搜索MX6QDL_PAD_EIM_A17_
,来确定io口没有被使用过,不然就注释掉。
然后对这些信息继续进行配置,在我们的test节点中,添加如下代码,test-gpios = <&gpio2 21 1>;
对应的就是MX6QDL_PAD_EIM_A17__GPIO2_IO21
,gpio2组中的第21个io口,默认置高。
下面就该是driver驱动程序的代码了,依然是在probe函数中进行添加修改
测试程序源码
app.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>int main(int argc, char *argv[])
{int fd;char buff[64] = {0};fd = open("/dev/hello_misc", O_RDWR); // 打开节点if(fd < 0){perror("open error\n"); // perror在应用中打印return fd;}buff[0] = atoi(argv[1]); //字符串转化成整形 // read(fd, buff, sizeof(buff));write(fd, buff,sizeof(buff)); //在write中就传数据到底层,这样可以调用驱动的mis_write的操作了,直接操作 ./app 1或者./app 0// printf("buf is:%s\n", buff);close(fd);return 0;
}
driver.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/of_address.h>#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>#include <linux/gpio.h>
#include <linux/of_gpio.h>struct device_node *test_device_node;
u32 out_values[2] = {0};
int beep_gpio = 0; // 返回到的gpio编号struct resource *beep_mem;
struct resource *beep_mem_tmp;int misc_open (struct inode *inode, struct file *file){printk("hello misc_open!!!\n");return 0;
}int misc_release(struct inode *inode, struct file *file){printk("bye bye misc_release!!!\n");return 0;}ssize_t misc_read(struct file *file, const char __user *ubuf, size_t size, loff_t *loff_t){char kbuf[64] = "copy to user!!!\n";if( copy_to_user(ubuf, kbuf, size) != 0 ){printk("copy_to_user error!!!\n");return -1;}printk("hello misc_read!!!\n");return 0;
}ssize_t misc_write(struct file *file, const char __user *ubuf, size_t size, loff_t *loff_t){char kbuf[64] = {0};printk("hello misc_write!!!\n");if( copy_from_user(kbuf, ubuf, size) != 0 ){printk("copy_from_user error!!!\n");return -1;}printk("buf is:%s\n", kbuf);/*********实现IO的逻辑功能*********************************************************************/if(kbuf[0] == 1){ //对蜂鸣器的控制,如果是1,控制gpio口// *vir_gpio5_dr |= (1 << 1); //因为是gpio5的01,所以左移一位就可以,给一个高电平,使蜂鸣器工作gpio_set_value(beep_gpio, 1); //Linux体现出来了通用性,不需要寄存器地址了printk("gpio_set_value is high!!!\n");}else if(kbuf[0] == 0){// *vir_gpio5_dr &= ~(1 << 1); gpio_set_value(beep_gpio, 0); printk("gpio_set_value is low!!!\n"); }return 0;
}struct file_operations misc_fops = {.owner = THIS_MODULE,.open = misc_open,.release = misc_release,.read = misc_read,.write = misc_write,
};struct miscdevice misc_dev = {.minor = MISC_DYNAMIC_MINOR, // 次设备号.name = "hello_misc", // 生成一个名字叫做hello_misc的设备节点.fops = &misc_fops // 完善file_operations
};int beep_probe(struct platform_device *pdev){int ret;printk("beep_probe ok!!!\n");/*直接获取设备节点信息*/// printk("node name is %s\n", pdev->dev.of_node->name); //pdev->dev.of_node获取节点,和下面的test_device_node功能相同/*间接获取设备节点信息*//********查找指定路径的节点***********/test_device_node = of_find_node_by_path("/test");if(test_device_node == NULL) {printk("of_find_node_by_path error!!!\n");return -1;}else {printk("of_find_node_by_path ok!!!\n");printk("test_device_node name is %s\n", test_device_node->name);}beep_gpio = of_get_named_gpio(test_device_node, "test-gpios", 0); // test-gpios是设备树文件中包含我们定义的gpio信息属性名test-gpios = <&gpio2 21 1>;if(beep_gpio < 0) {printk("of_get_named_gpio error!!!\n");}else{printk("of_get_named_gpio is %d\n", beep_gpio);}ret = gpio_request(beep_gpio, "beep"); // beep是我们现在起的名字if(ret != 0) {printk("gpio_request error!!!\n");}else{printk("gpio_request ok!!!\n");}/*gpio子系统,控制输出为高电平*/gpio_direction_output(beep_gpio, 1); // 使用gpio_direction直接对编号beep_gpio对应的io口拉高/*注册杂项设备*/ret = misc_register(&misc_dev);if(ret < 0){printk("misc_register failed!!!\n");return -1;}else{printk("misc_register succeed!!!\n"); }return 0;
}int beep_remove(struct platform_device *pdev){printk("beep_remove ok!!!\n");return 0;
}const struct platform_device_id beep_id_table = {.name = "beep_device_test",
};const struct of_device_id of_match_table_test[] = {{.compatible = "test12345"},{} // 不写会提示警告
};struct platform_driver beep_device = {.probe = beep_probe, // 这个probe函数其实和 device_driver中的是一样的功能,但是一般是使用device_driver中的那个.remove = beep_remove, // 卸载平台设备驱动的时候会调用这个函数,但是device_driver下面也有,具体调用的是谁这个就得分析了.driver = {.owner = THIS_MODULE, .name = "beep_device_test", // 有了id_table,这个driver->name可有可无,优先级低.of_match_table = of_match_table_test, // 最优先匹配of_match_table其次是id_table最后是name}, // 内置的device_driver 结构体.id_table = &beep_id_table, // 该设备驱动支持的设备的列表 他是通过这个指针去指向 platform_device_id 类型的数组
};static int beep_driver_init(void)
{int ret = 0;printk("beep_driver_init ok!!!\n"); // 在内核中无法使用c语言库,所以不用printfret = platform_driver_register(&beep_device);if(ret < 0){printk("platform_driver_register error!!!\n");return ret;}else{printk("platform_driver_register ok!!!\n");}return 0;
}static void beep_driver_exit(void)
{printk("beep_driver_exit bye!!!\n");gpio_free(beep_gpio); // 与gpio_request对应起来,把获取到的gpio编号释放掉misc_deregister(&misc_dev);platform_driver_unregister(&beep_device);
}module_init(beep_driver_init);
module_exit(beep_driver_exit);MODULE_LICENSE("GPL"); //声明模块拥有开源许可
结果验证
查看设备树节点
看看申请的gpio是不是在设备树中
安装ko模块生成驱动
在dev文件中查看驱动生成的节点
执行app
我们可以看到对应的信息打印了出来,说明程序都得到了顺利的执行,接下来就是测试EIM_A17引脚实际的高低电平,通过万用表的测量,与打印信息一致。
这篇关于Linux嵌入式驱动开发12——pinctl和gpio子系统实践操作的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!