Linux驱动学习day8(按键驱动读取方式、GPIO、pinctrl子系统)
按键驱动程序的几种读取方式
非阻塞
阻塞
select/poll
异步通知
GPIO和Pinctrl子系统
Pinctrl 子系统主要用于配置 多功能引脚的功能复用 和 电气属性(如上拉/下拉、驱动能力),这是很多 SoC 芯片(如 Rockchip、Allwinner)必需的步骤。
一个引脚可能有多个功能(如 UART TX/RX、SPI、GPIO)
Pinctrl 子系统负责告诉内核“当前要把这个引脚用作 GPIO(或 SPI/UART)
RK板子的pins为rockchip,pins = <bank号 引脚号 功能编号 引脚配置宏>下面代码的意思是第一组GPIO中第15个引脚,引脚复用为GPIO模式,没有上拉或者下拉电阻。
led {compatible = "my,led";pinctrl-names = "default";pinctrl-0 = <&led_pins>;gpio-names = "led";led-gpios = <&gpio1 15 GPIO_ACTIVE_HIGH>;
};pinctrl {led_pins: led-pins {rockchip,pins = <1 15 RK_FUNC_GPIO &pcfg_pull_none>;};
};
GPIO 子系统提供了一个统一的接口,用于在 Linux 驱动中 控制 GPIO 引脚的输入输出电平,支持配置方向、读写状态等。 设备树中GPIO写法示例:
led {compatible = "my,led";gpio-names = "led";led-gpios = <&gpio1 15 GPIO_ACTIVE_HIGH>;
};
上机实验
按道理来讲,将一个 PIN 用作 GPIO 功能的时候也需要创建对应的 pinctrl 节点,并
且将所用的 PIN 复用为 GPIO 功能,但是!对于 RK3568 而言,如果一个 PIN 用作 GPIO 功能
的时候不需要创建对应的 pinctrl 节点!
所以在LED点灯的实验中,只需要向设备树中添加LED灯的结点即可。
myled{compatible = "my,myled";gpio-names = "led";led-gpios = <&gpio0 RK_PC0 GPIO_ACTIVE_HIGH>;status = "okey";
};
这里的gpio-names是因为我在驱动代码中使用了gpiod-get函数,所以需要加上这个属性,led-gpio中的属性意思是 使用gpio0 0C这个引脚,GPIO_ACTIVE_HIGH表示该 GPIO 引脚的逻辑电平有效方式:高电平有效(1 表示开启,0 表示关闭)。这里之所以使用这个宏,是因为我们在驱动代码中使用了gpiod_set_value(led_gpio , status),status系统默认会是一个逻辑值。驱动只处理逻辑信号,硬件差异通过设备树隐藏起来,换板子只改设备树,不用动驱动,极大方便了维护和移植。
程序代码
代码流程分析:
1、首先还是最主要的驱动部分file_operations结构体的构造,先写出最一般的file_operations结构体,写好驱动程序对应的open,read,write,release函数。
2、由于要利用设备树来操作硬件,所以需要创建platform_driver结构体,写出基本的probe函数和remove函数,并且在.driver中设置.name 属性和.of_match_table(创建of_device_id 结构体数组,设置属性{.compatible = "和设备树中的结点compatible属性相同"}) 。
3、在入口函数注册platform_driver,出口函数释放unregister。并且使用module_init() , module_exit()修饰入口出口函数。
4、当设备树中结点和驱动程序相匹配时,会调用驱动程序的probe函数,在probe函数中,我们需要注册file_operation结构体和加载设备节点(class_create , device_create),并且获得struct gpio_desc *led_gpio。
5、当APP调用open函数的时候,进入驱动程序的open函数。在open函数中使用gpiod_direction_output函数设置其为输出引脚,默认高电平。
6、当APP调用write函数的时候,进入到驱动程序的write函数,在函数中调用gpiod_set_value,确定引脚输出高低电平。
7、在remove中注销之前创建的class,device,led_gpio , file_operation结构体。
#include <linux/module.h> // 最基本模块宏
#include <linux/kernel.h> // printk
#include <linux/init.h> // __init/__exit
#include <linux/fs.h> // register_chrdev 等
#include <linux/uaccess.h> // copy_to_user, copy_from_user
#include <linux/types.h> // dev_t, bool 等类型
#include <linux/device.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/of_device.h>static int major = 0;static struct class *myled_class;
static struct gpio_desc *led_gpio;/* funciton */static ssize_t led_drv_read (struct file *file, char __user *buf , size_t size, loff_t * offset)
{printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);printk("read from drivers:%s %s\n" , __FILE__ , __FUNCTION__ );return 0;
}
/* write(fd , &val , 1) */
static ssize_t led_drv_write (struct file *file, const char __user *buf , size_t size, loff_t * offset)
{unsigned char status;unsigned int ret;printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);ret = copy_from_user(&status, buf, 1);/* minior and status */gpiod_set_value(led_gpio, status); /* logic value */return 1;
}static int led_drv_open (struct inode *node, struct file *file)
{printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);/* init LED gpio output*/gpiod_direction_output(led_gpio, 0);return 0;
}static int led_drv_close (struct inode *node, struct file *file)
{// printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);return 0;
}/* create struct file_operations */
static struct file_operations myled_drv =
{.owner = THIS_MODULE,.open = led_drv_open,.read = led_drv_read,.write = led_drv_write,.release = led_drv_close,
};static int chip_demo_gpio_probe(struct platform_device *pdev)
{printk("%s %s %d \n", __FILE__ , __FUNCTION__ , __LINE__);led_gpio = gpiod_get(&pdev->dev, "led", 0);if(IS_ERR(led_gpio)){printk("%s %s line %d\n" , __FILE__ , __FUNCTION__ , __LINE__);dev_err(&pdev->dev , "failed to get GPIO for led\n");return PTR_ERR(led_gpio);}major = register_chrdev(0 , "led_drv", &myled_drv);/* do not need mknod */if(!myled_class){myled_class = class_create(THIS_MODULE, "led_class");if (IS_ERR(myled_class)) {printk("failed to allocate class\n");unregister_chrdev(major, "led_class");return PTR_ERR(myled_class);}}device_create(myled_class , NULL , MKDEV(major, 0) , NULL , "myled%d" , 0);return 0;
}static int chip_demo_gpio_remove(struct platform_device *pdev)
{device_destroy(myled_class , MKDEV(major, 0));class_destroy(myled_class);unregister_chrdev(major, "led_drv");gpiod_put(led_gpio);return 0;
}static const struct of_device_id myleds[] = {{ .compatible = "my,myled" },{},
};static struct platform_driver chip_demo_gpio_driver={.probe = chip_demo_gpio_probe,.remove = chip_demo_gpio_remove,.driver = {.name = "myled",.of_match_table = myleds,}
};/* register_chrdev */
static int __init led_init(void)
{/* struct device *device_create(struct class *class, struct device *parent,dev_t devt, void *drvdata, const char *fmt, ...) *///p_led_opr = get_board_led_opr();int err;err = platform_driver_register(&chip_demo_gpio_driver);return 0;
}/* entry function */
static void __exit led_exit(void)
{/* distroy void device_destroy(struct class *class, dev_t devt)*/platform_driver_unregister(&chip_demo_gpio_driver);return;
}module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");//#warning "MODULE_LICENSE included in led_template_drv.c"
上述代码中使用到了几个GPIO的API,记录一下:
int gpiod_direction_output(struct gpio_desc *desc, int value);/* 设置GPIO为输出,默认低电平 */
void gpiod_set_value(struct gpio_desc *desc, int value);/* 设置GPIO的值,value使用status 为逻辑值 */
struct gpio_desc *__must_check gpiod_get(struct device *dev, const char *con_id,enum gpiod_flags flags)
/* dev设备对象(一般是 pdev->dev),用于查找该设备节点绑定的 GPIO con_id和设备树中的 gpio-names 对应,用于匹配指定获取 GPIO 的意图__must_check:必须检查返回值
*/
void gpiod_put(struct gpio_desc *desc);/* 释放指针 */
这里要记住,我之前习惯将{ .compatible = "my,myled" }写成{ .compatible = "my , myled" },每个符号之后都有一个空格,并且将设备树中的compatible 也写成 "my , myled",但是这样写就会出现问题,加载驱动之后不会调用.probe函数,也就是说之后的都无法执行,只需要将空格去掉即可,一开始还以为逻辑出现了错误,一直找不到问题所在。
测试程序
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>/* ./ledtest /dev/led_drv on ./ledtest /dev/led_drv off
*/
int main(int argc , char *argv[])
{int fd;char status;if(argc != 3){printf("Usage: %s <dev> < off | on >\n" , argv[0]);printf("eg: %s /dev/led_drv on\n" , argv[0]);printf("eg: %s /dev/led_drv off\n" , argv[0]);return -1;}/* open */fd = open(argv[1] , O_RDWR);if(fd < 0){printf("can not open %s\n" , argv[1]);return -1;}/* write */if(strcmp(argv[2] , "on") == 0){status = 1;write(fd , &status , 1);}else if(strcmp(argv[2] , "off") == 0){status = 0;write(fd , &status , 1);}close(fd);return 0;
}
测试程序和Makefile都和之前一样,不用修改。
还有一点就是,需要查看这个引脚是否有作为其他功能使用,如果有作为其他功能使用,我们加载这个驱动的时候就会出现错误,因此需要在设备树中查看一下这个引脚的其他作用,并将其禁止。RK3568上的LED灯被作为心跳灯使用,我们要禁止它,打开k3568-evb.dtsi设备树文件,搜索gpio0 RK_PC0,我们可以看到下图,这里添加status = "disabled"。