每日一个简单的驱动,日久方长,对Linux驱动就越来越熟悉,也越来容易学会写驱动程序。今日进行设备树下的platform设备驱动。
前面一篇我们讲解了传统的、未采用设备树的 platform 设备和驱动编写方法。最新的 Linux 内核已经支持了设备树,因此在设备树下如何编写 platform驱动就显得尤为重要,本章我们就来学习一下如何在设备树下编写 platform 驱动。
一、设备树下的 platform 驱动简介
platform 驱动框架分为总线、设备和驱动,其中总线不需要我们这些驱动程序员去管理,这个是 Linux 内核提供的,我们在编写驱动的时候只要关注于设备和驱动的具体实现即可。
在没有设备树的 Linux 内核下,我们需要分别编写并注册 platform_device 和 platform_driver,分别代表设备和驱动。
在使用设备树的时候,设备的描述被放到了设备树中,因此 platform_device 就不需要我们去编写了,我们只需要实现 platform_driver 即可。
在编写基于设备树的 platform 驱动的时候我们需要注意以下几点:
- 1、在设备树中创建设备节点
毫无疑问,肯定要先在设备树中创建设备节点来描述设备信息,重点是要设置好 compatible属性的值,因为 platform 总线需要通过设备节点的 compatible 属性值来匹配驱动!这点要切记。
- 2、编写 platform 驱动的时候要注意兼容属性
在使用设备树的时候 platform 驱动会通过 of_match_table 来保存兼容性值,也就是表明此驱动兼容哪些设备。所以, of_match_table 将会尤为重要
- 3、编写 platform 驱动
基于设备树的 platform 驱动和上一章无设备树的 platform 驱动基本一样,都是当驱动和设备匹配成功以后就会执行 probe 函数。我们需要在 probe 函数里面执行字符设备驱动那一套,当注销驱动模块的时候 remove 函数就会执行,都是大同小异的。
二、修改设备树文件
修改设备树文件,加上我们需要的设备信息,本章我们就使用到一个 LED 灯。
2.1 添加 LED 设备节点
在根节点“/”下创建 LED 灯节点,节点名为“gpioled”,节点内容如下:
gpioled { #address-cells = <1>; #size-cells = <1>; compatible = "imx6ull-gpioled"; pinctrl-name = "default"; pinctrl-0 = <&pinctrl_led>; led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>; status = "okay"; };
2.2 添加 pinctrl 节点
I.MX6U-ALPHA开发板上的 LED 灯使用了 GPIO1_IO03 这个 PIN,打开 imx6ul-14x14-evk.dtsi ,在 iomuxc 节点的 imx6ul-evk 子节点下创建一个名为“pinctrl_led”的子节点,节点内容如下所示:
pinctrl_led: ledgrp { fsl,pins = < MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x10B0 /* LED0 */ >; };
2.3 检查 PIN 是否被其他外设使用
三、platform 驱动程序编写
新建名为 dtsplatform_driver.c 的驱动文件,在 dtsplatform_driver.c 中输入如下所示内容:
/*********************************************************** * Copyright © toto Co., Ltd. 1998-2029. All rights reserved. * Description: * Version: 1.0 * Autor: toto * Date: Do not edit * LastEditors: Seven * LastEditTime: Do not edit ***********************************************************/ #include <linux/types.h> #include <linux/kernel.h> #include <linux/delay.h> #include <linux/module.h> #include <linux/cdev.h> #include <linux/device.h> #include <linux/of_gpio.h> #include <linux/semaphore.h> #include <linux/irq.h> #include <linux/fcntl.h> #include <linux/fs.h> #include <linux/platform_device.h> #include <asm/mach/map.h> #include <asm/uaccess.h> #define LEDDEV_CNT 1 /* 设备号数量 */ #define LEDDEV_NAME "dts_platform_led" /* 设备名字 */ #define LED_ON 1 #define LED_OFF 0 /* led_dev 设备结构体 */ struct led_dev { dev_t devid; /* 设备号 */ struct cdev cdev; /* cdev */ struct class *class; /* 类 */ struct device *device; /* 设备 */ int major; /* 主设备号 */ struct device_node *node; /* 设备节点 */ int led_gpio; /* led gpio号 */ }; struct led_dev leddev; /* led 设备 */ /* * @Brief led 打开、关闭接口 * @Param sta:1打开,0关闭 * @Note NOne * @RetVal NOne */ void led_switch(u8 sta) { if (sta == LED_ON) { gpio_set_value(leddev.led_gpio, 0); } else if (sta == LED_OFF) { gpio_set_value(leddev.led_gpio, 1); } } /* * @Brief 打开设备 * @Param inode:传递给驱动的inode * @Param filp:设备文件 * @Note NOne * @RetVal NOne */ static int led_open(struct inode *inode, struct file *filp) { /* 设置私有数据 */ filp->private_data = &leddev; return 0; } /* * @Brief 向设备写数据 * @Param filp:设备文件 * @Param buf:要写入设备的数据 * @Param cnt:要写入的数据长度 * @Param offt:相对于文件首地址的偏移 * @Note NOne * @RetVal 写入的字节数,如果为负值,表示写入失败 */ static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) { int ret; unsigned char databuf[1]; unsigned char ledstat; ret = copy_from_user(databuf, buf, cnt); if (ret < 0) { return -EFAULT; } ledstat = databuf[0]; led_switch(ledstat); return 0; } /* 设备操作函数 */ static struct file_operations led_fops = { .owner = THIS_MODULE, .open = led_open, .write = led_write, }; /* * @Brief * @Param None * @Note NOne * @RetVal NOne */ static int led_probe(struct platform_device *dev) { printk(KERN_INFO "led driver and device has matched\n"); /* 注册字符设备驱动 */ /* 1.创建设备号 */ if (leddev.major) { leddev.devid = MKDEV(leddev.major, 0); register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME); } else { alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME); leddev.major = MAJOR(leddev.devid); } /* 2.注册设备 */ cdev_init(&leddev.cdev, &led_fops); cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT); /* 3.创建类 */ leddev.class = class_create(THIS_MODULE, LEDDEV_NAME); if (IS_ERR(leddev.class)) { return PTR_ERR(leddev.class); } /* 4.创建设备 */ leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME); if (IS_ERR(leddev.device)) { return PTR_ERR(leddev.device); } /* 5.初始化IO */ leddev.node = of_find_node_by_path("/gpioled"); if (leddev.node == NULL) { printk("gpioled node not found\n"); return -EINVAL; } leddev.led_gpio = of_get_named_gpio(leddev.node, "led-gpio", 0); if (leddev.led_gpio < 0) { printk("can't get led-gpio\n"); return -EINVAL; } gpio_request(leddev.led_gpio, "my_led"); /* 设置输出模式,默认高电平 */ gpio_direction_output(leddev.led_gpio, 1); return 0; } /* * @Brief 移除 platform 驱动函数 * @Param dev:platform设备 * @Note NOne * @RetVal NOne */ static int led_remove(struct platform_device *dev) { gpio_set_value(leddev.led_gpio, 1); cdev_del(&leddev.cdev); unregister_chrdev_region(leddev.devid, LEDDEV_CNT); device_destroy(leddev.class, leddev.devid); class_destroy(leddev.class); return 0; } /* 匹配列表 */ static const struct of_device_id led_of_match[] = { { .compatible = "imx6ull-gpioled"}, { /* sentinel */} }; /* platform 驱动结构体 */ static struct platform_driver led_driver = { .driver = { .name = "imx6ull-led", /* 驱动名字,用于和设备匹配 */ .of_match_table = led_of_match, /* 设备树匹配表 */ }, .probe = led_probe, .remove = led_remove, }; /* * @Brief 驱动模块加载函数 * @Param None * @Note NOne * @RetVal NOne */ static int __init leddriver_init(void) { return platform_driver_register(&led_driver); } /* * @Brief 驱动模块卸载函数 * @Param None * @Note NOne * @RetVal NOne */ static void __exit leddriver_exit(void) { platform_driver_unregister(&led_driver); } module_init(leddriver_init); module_exit(leddriver_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("toto");
- 第 33~112 行,传统的字符设备驱动,没什么要说的。
- 第 120~164 行, platform 驱动的 probe 函数,当设备树中的设备节点与驱动之间匹配成功以后此函数就会执行,原来在驱动加载函数里面做的工作现在全部放到 probe 函数里面完成。
- 第 171~180 行, remobe 函数,当卸载 platform 驱动的时候此函数就会执行。在此函数里面释放内存、注销字符设备等,也就是将原来驱动卸载函数里面的工作全部都放到 remove 函数中完成。
- 第 183~186 行,匹配表,描述了此驱动都和什么样的设备匹配,
- 第 184 行添加了一条值为"atkalpha-gpioled"的 compatible 属性值,当设备树中某个设备节点的 compatible 属性值也为“atkalpha-gpioled”的时候就会与此驱动匹配。
- 第 189~196 行,platform_driver 驱动结构体, 191 行设置这个 platform 驱动的名字为“imx6ulled”,因此,当驱动加载成功以后就会在/sys/bus/platform/drivers/目录下存在一个名为“imx6uled”的文件。
- 第 192 行设置 of_match_table 为上面的 led_of_match。
- 第 203~206 行,驱动模块加载函数,在此函数里面通过 platform_driver_register 向 Linux 内核注册 led_driver 驱动。
- 第 213~216 行,驱动模块卸载函数,在此函数里面通过 platform_driver_unregister 从 Linux内核卸载 led_driver 驱动。
四、测试 APP 编写
新建名为 dtsplatform_app.c 的测试程序文件,在 dtsplatform_app.c 中输入如下所示内容:
/*********************************************************** * Copyright © toto Co., Ltd. 1998-2029. All rights reserved. * Description: * Version: 1.0 * Autor: toto * Date: Do not edit * LastEditors: Seven * LastEditTime: Do not edit ***********************************************************/ #include "stdio.h" #include "unistd.h" #include "sys/types.h" #include "sys/stat.h" #include "fcntl.h" #include "stdlib.h" #include "string.h" #define LEDON 1 #define LEDOFF 0 /* * @Brief * @Param None * @Note NOne * @RetVal NOne */ int main(int argc, char *argv[]) { int fd, retval; char *filename; unsigned char databuf[1]; if (argc != 3) { printf("Error argc par cnt\n"); return -1; } filename = argv[1]; fd = open(filename, O_RDWR); if (fd < 0) { printf("file %s open failed\n", filename); return -1; } databuf[0] = atoi(argv[2]); retval = write(fd, databuf, sizeof(databuf)); if (retval < 0) { printf("led control failed\n"); close(fd); return -1; } close(fd); return 0; }
五、运行测试
5.1 编译
1.编译驱动程序 编写 Makefile 文件,Makefile 内容如下所示:
KERNELDIR := /home/toto/workspace/linux/linux-5.19 CURRENT_PATH := $(shell pwd) obj-m := dtsplatform_driver.o build: kernel_modules kernel_modules: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules clean: $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
编译命令:
make -j8
编译成功以后就会生成一个名为“dtsplatform_driver.ko”的驱动模块文件。
2. 编译测试app 编译命令:
arm-linux-gnueabihf-gcc dtsplatform_app.c -o dtsplatform_app
编译成功以后就会生成 platform_app 这个应用程序。
5.2 运行测试
开发板上电,将 dtsplatform_driver.ko 和 dtsplatform_app 这两个文件拷贝到 /lib/modules/5.19.0-g794a2f7be62d-dirty/ 目录中,输入如下命令加载 dtsplatform_driver.ko 这个驱动模块:
insmod dtsplatform_driver.ko
驱动模块加载完成以后到 /sys/bus/platform/drivers/目录下查看驱动是否存在,我们在dtsplatform_driver.c 中设置 led_driver (platform_driver 类型)的 name 字段为“imx6ull-led”,因此会在/sys/bus/platform/drivers/目录下存在名为“imx6ull-led”这个文件,结果如下图所示:
同理,在/sys/bus/platform/devices/目录下也存在 led 的设备文件,也就是设备树中 gpioled 这个节点,结果如下所示:
驱动模块和设备模块加载成功以后 platform 总线就会进行匹配,当驱动和设备匹配成功以后就会输出如下所示一行语句:
/lib/modules/5.19.0-g794a2f7be62d-dirty # insmod dtsplatform_driver.ko [ 45.657342] led driver and device has matched
驱动和设备匹配成功以后就可以测试 LED 灯驱动了,输入如下命令打开 LED 灯:
./dtsplatform_app /dev/dts_platform_led 1
在输入如下命令关闭 LED 灯:
./dtsplatform_app /dev/dts_platform_led 0
卸载驱动命令如下:
rmmod dtsplatform_driver.ko