gpiod API对platform-led进行驱动开发
修改设备树源码如何在驱动中获取设备树节点信息计算设备子节点数量给私有属性分配内存对子节点进行遍历gpiod的获取根据设备树字节给的默认状态给gpio设置初始值把私有数据保存起来
probe函数remove函数fops的操作函数open函数ioctl函数
platform设备init和exit模块init和exit应用代码
本人使用i.MX6ul开发板,不是市面主流开发板,不过和主流正点原子等厂家的cpu是一样的,设备树修改方式也很类似,知道设备树基本概念就知道第一部分增加的地方。
修改设备树源码
在官方提供的设备树源码,arch\arm\boot\dts\imx6ul-14x14-evk.dtsi 中的根节点下增加 led的节点内容,如果使用自己创建的设备树文件,那就在自己文件的根节点下添加
my_gpio_led {
compatible = "my_gpio_led";
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_my_gpio_led>;
led0{
gpios = <&gpio5 9 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
led1{
gpios = <&gpio4 16 GPIO_ACTIVE_HIGH>;
default-state = "off";
};
};
在iomuxc中添加pinctrl对应的参数
pinctrl_my_gpio_led: my_gpio_led{
fdl,pins = <
MX6UL_PAD_SNVS_TAMPER9__GPIO5_IO09 0x17059 /* my_gpio_led */
MX6UL_PAD_NAND_DQS__GPIO4_IO16 0x17059 /*board led*/
>;
};
配置管脚为普通gpio管脚,如何去配置pinctrl第二个参数,如何计算,我单独开一个博客讲。 对于gpio配置理解较为容易,<> 中的三个参数,前两个决定哪一个管脚,第三个决定该gpio的激活状态,是高电平有效还是低电平有效,根据原理图与实际应用得出。
如何在驱动中获取设备树节点信息
本人没有使用单节点,很多教程使用单节点,通过常用的of_函数可以直接获取节点信息,多节点的设计也使得代码更加通用,在增加led时候,只需要在my_gpio_led节点下创建新的led子节点。
计算设备子节点数量
linux设备驱动提供了计算设备子节点数量的API,子节点数量即设备led数量
num_leds = device_get_child_node_count(dev);
if(num_leds <= 0)
{
dev_err(dev, "No leds gpio assigned.\n");
return -EINVAL;
}
dev即设备指针,类型struct device *的
给私有属性分配内存
常用教程使用全局变量保存私有属性,其实好处在于可以随处使用该变量,但坏处很多,并不建议这么做,官方代码也是不建议这么做的,坏处所体现的地方与临界资源相关,容易导致程序出问题,后面单独深究。
priv = devm_kzalloc(dev, sizeof_gpio_leds_priv(num_leds), GFP_KERNEL);//最后一个参数查看linux/gfp.h
if(!priv)
{
return -ENOMEM;//错误码,errno-base.h中,内存不足
}
函数devm_kzalloc和kzalloc一样都是内核内存分配函数,但是devm_kzalloc是跟设备(装置)有关的,当设备(装置)被拆卸或者驱动(驱动程序)卸载(空载)时,内存会被自动释放。另外,当内存不再使用时,可以使用函数devm_kfree()释放。而kzalloc没有自动释放的功能,用的时候需要小心使用,如果忘记释放,会造成内存泄漏。
对子节点进行遍历
无需手动去一个一个获取,kernel提供了遍历字节点的一个宏进行遍历
device_for_each_child_node(dev, child) { //一个宏,遍历每个子节点
}
child 是固件子节点指针 即 struct fwnode_handle *
gpiod的获取
本文最重要的地方,将会查看kernel文档,看到最新的gpio文档建议放弃使用原本的gpio的API,建议使用gpiod库
既然官方都这么说,那有何道理不使用呢,尽管原本的gpio子系统提供的接口方便简单,但是有他的弊端,pin管脚都是int型的返回,管理起来会出现问题,并且无法自动回收。至于gpiod,那个d就是描述(describe)的英文单词第一个字母,这个新接口就是使用描述符的方式进行封装,更加安全,只比原本的接口麻烦一丢丢。 代码如下
gpiod = devm_fwnode_get_gpiod_from_child(dev, NULL, child, //获取子节点的gpio描述符,并且获取gpio
GPIOD_ASIS, //不初始化gpio方向状态
NULL);
该函数是结合了上文的子节点进行获取gpiod的方法,主要关心第四个参数,是一个enum gpiod_flags类型的枚举,用于选择性地指定 GPIO 的方向和初始值。可以是
GPIOD_ASIS 或 0 根本不初始化 GPIO。必须稍后使用专用功能之一设置方向。GPIOD_IN 将 GPIO 初始化为输入。GPIOD_OUT_LOW 将 GPIO 初始化为值为 0 的输出。GPIOD_OUT_HIGH 将 GPIO 初始化为值为 1 的输出。GPIOD_OUT_LOW_OPEN_DRAIN 与 GPIOD_OUT_LOW 相同,但也强制线路在电气上使用开漏。GPIOD_OUT_HIGH_OPEN_DRAIN 与 GPIOD_OUT_HIGH 相同,但也强制线路在电气上使用开漏。
根据设备树字节给的默认状态给gpio设置初始值
设备树默认状态属性总算用上了,并且通过代码方式获取到,通过代码自动配置它
if(!fwnode_property_read_string(child, "default-state", &state))
{
if(!strncmp(state, "off", 3))
{
level = OFF; //默认状态为off时候
}
else
{
level = ON;
}
}
if((rv = gpiod_direction_output(priv->leds[i].led_gpiod, level)) < 0) //设置gpio的输出初始电平
{
dev_err(dev, "Can't set gpio output for %s\n", priv->leds[i].name);
break;
}
第一个函数是读取固件节点中"default-state"属性的字符值,字符串给到state第二个函数是gpiod设置gpio为输出方向和配置初始电平的接口函数
其实gpiod没有想象那么难,很多接口只是比原本的gpio接口多了一个d而已,原本int gpio参数更改为gpiod描述符的指针
把私有数据保存起来
我们在自定义的函数里面把数据提取出来了,去放哪儿呢?
通过函数返回值方式。保存至设备区中的私有属性区。
当然第一种方式很多人会,使用分配的堆区进行保存私有数据,去使用它不过是一个地址而已,可以直接通过函数返回出去,让外部函数获取,外部函数即probe函数,但是有个问题,probe中可以用,那remove接口函数中咋用呢?没有了全局变量,感觉干啥都变得困难了。
其实linux内核都已经安排好了,给设备结构体分配了一块私有数据的保存的地方。
本人使用platform总线开发,kernel提供了保存的接口
platform_set_drvdata(pdev, priv);
函数原型,其实调用的是dev_set_drvdata函数
static inline void platform_set_drvdata(struct platform_device *pdev, void *data)
{
dev_set_drvdata(&pdev->dev, data);
}
继续追下去,找到数据的流向,确实很友好,数据位置清晰可见。 有set函数当然就有get函数,没办法,它俩天生一对。
static inline void *platform_get_drvdata(const struct platform_device *pdev)
{
return dev_get_drvdata(&pdev->dev);
}
使用起来也是很简单,只需要platform设备指针即可。
probe函数
解析完设备树,gpiod也配好了,那probe中只需要做一些常规事情,我直接贴代码,代码有注释,应该很好看懂。
//1.初始化io状态
rv = paser_dt_init_led(pdev);
if(rv < 0)
{
return rv;
}
priv = platform_get_drvdata(pdev); //获取私有数据
//2. 创建设备号
if (0 != dev_major)
{
devno = MKDEV(dev_major, 0);
rv = register_chrdev_region(devno, 1, DEV_NAME);//静态申请字符设备号
}
else
{
rv = alloc_chrdev_region(&devno, 0, 1, DEV_NAME);//动态申请字符设备号
dev_major = MAJOR(devno);//获取其主设备编号
}
if (rv < 0)
{
dev_err(&pdev->dev, "%s driver can't get major %d\n", DEV_NAME, dev_major);
return rv;
}
//3.注册字符设备,将注册设备好和cdev绑定,交给内核
cdev_init(&priv->cdev, &led_fops); //初始化cdev,把fops添加进去
priv->cdev.owner = THIS_MODULE;
rv = cdev_add(&priv->cdev, devno, priv->num_leds);//注册给内核,设备数量1个
if (0 != rv)
{
dev_err(&pdev->dev, "error %d add %s device failure.\n", rv, DEV_NAME);
goto undo_major; //撤销设备号
}
//4.创建类,驱动中进行节点创建
priv->dev_class = class_create(THIS_MODULE, DEV_NAME);
if(IS_ERR(priv->dev_class))
{
dev_err(&pdev->dev,"%s driver create class failure\n", DEV_NAME);
rv = -ENOMEM;
goto undo_cdev;
}
//5.创建设备
for(i=0; i
{
devno = MKDEV(dev_major, i); //给每一个led设置设备号
dev = device_create(priv->dev_class, NULL, devno, NULL, DEV_NAME"%d", i);
if(IS_ERR(dev))
{
rv = -ENOMEM; //返回错误码
goto undo_class;
}
}
remove函数
对设备进行注销,记得释放gpio,虽然它可以自己释放,但这个好习惯,就像应用空间文件IO,有open就要记得不用时候close掉,系统给你擦屁股可不是好习惯。
for (i=0; i
{
devno = MKDEV(dev_major, i);
device_destroy(priv->dev_class, devno);//注销设备
}
class_destroy(priv->dev_class); //注销类
cdev_del(&priv->cdev);//删除cdev
unregister_chrdev_region(MKDEV(dev_major, 0), priv->num_leds); //释放设备号
for(i=0; i
{
gpiod_set_value(priv->leds[i].led_gpiod, OFF);
devm_gpiod_put(&pdev->dev, priv->leds[i].led_gpiod); //释放gpiod
}
fops的操作函数
这之中我没有进行太复杂的操作,对io的控制没有使用读写操作,使用较为简单的ioctl。
open函数
通过inode的cdev属性找到私有数据的地址
priv = container_of(inode->i_cdev, struct gpio_leds_priv, cdev);//找到私有数据的地址
filp->private_data = priv;
利用强大的container_of函数找到一个结构体的首地址,然后把该地址放到file的私有数据区中
函数宏 container_of() 的解释 给定结构体中某个成员的地址、该结构体类型和该成员的名字 获取这个成员所在的结构体变量的首地址。
/**
* container_of - cast a member of a structure out to the containing structure
*
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
ioctl函数
使用魔术字,保证命令的唯一性。
#define PLATDRV_MAGIC 0x60 //魔术字
#define LED_OFF _IO (PLATDRV_MAGIC, 0x18)
#define LED_ON _IO (PLATDRV_MAGIC, 0x19)
然后利用ioctl的第三个参数,指定led设备
switch (cmd) {
case LED_OFF:/* variable case */
if(priv->num_leds <= arg)
{
printk("led%ld doesn't exist\n", arg);
return -ENOTTY;
}
gpiod_set_value(priv->leds[arg].led_gpiod, OFF);
break;
case LED_ON:
if(priv->num_leds <= arg)
{
printk("led%ld doesn't exist\n", arg);
return -ENOTTY;
}
gpiod_set_value(priv->leds[arg].led_gpiod, ON);
break;
default:
printk("%s driver don't support ioctl command=%d\n", DEV_NAME, cmd);
print_led_help();
}
使用gpiod的设置电平的api,进行电平的控制
platform设备init和exit
使用platform设备的注册函数和注销函数即可。
static int __init platdrv_led_init(void)
{
int rv = 0;
rv = platform_driver_register(&gpio_led_driver); //注册platform的led驱动
if(rv)
{
printk(KERN_ERR "%s:%d: Can't register platform driver %d\n", __FUNCTION__, __LINE__, rv);
return rv;
}
printk("Regist imx LED Platform Driver successfully!\n");
return 0;
}
static void __exit platdrv_led_exit(void)
{
printk("%s():%d remove LED platform driver\n", __FUNCTION__, __LINE__);
platform_driver_unregister(&gpio_led_driver); //卸载驱动
}
模块init和exit
module_init(platdrv_led_init);
module_exit(platdrv_led_exit);
应用代码
简易的测试led闪烁,只需要open设备,利用ioctl系统调用即可,第二个参数为魔术字。
ioctl(fd0, LED_ON, 0);
ioctl(fd1, LED_OFF, 1);
sleep(1);
ioctl(fd0, LED_OFF, 0);
ioctl(fd1, LED_ON, 1);
sleep(1);
便可以看到led的交替闪烁。
精彩链接
发表评论