ARM Linux 驱动开发篇--- Linux 按键输入实验--- Ubuntu20.04互斥体实验

🎬 渡水无言个人主页渡水无言

专栏传送门 : 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》

专栏传送门 : 《freertos专栏》 《STM32 HAL库专栏》《linux裸机开发专栏

⭐️流水不争先,争的是滔滔不绝

📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生

| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生

在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连

目录

前言

一、Linux下按键驱动原理

二、硬件原理图分析

三、实验程序编写

3.1、修改设备树文件

[3.1.1、添加 pinctrl 节点(引脚复用配置)](#3.1.1、添加 pinctrl 节点(引脚复用配置))

[3.1.2、添加 KEY 设备节点](#3.1.2、添加 KEY 设备节点)

[3.1.3、检查 PIN 是否被其他外设使用](#3.1.3、检查 PIN 是否被其他外设使用)

3.1.4、编译设备树

3.2、按键驱动程序编写(key.c)

[3.3 驱动代码逐段解析](#3.3 驱动代码逐段解析)

3.3.1、设备结构体定义(30-43行)

[3.3.2、GPIO 初始化函数(51-69 行:keyio_init)](#3.3.2、GPIO 初始化函数(51-69 行:keyio_init))

[3.3.3、文件操作接口(78-138 行)](#3.3.3、文件操作接口(78-138 行))

[1. open 函数(78-89 行:key_open)](#1. open 函数(78-89 行:key_open))

[2. read 函数(99-115 行:key_read)](#2. read 函数(99-115 行:key_read))

[3. write/release 函数(125-138 行)](#3. write/release 函数(125-138 行))

[3.3.4、设备操作集(140-147 行)](#3.3.4、设备操作集(140-147 行))

[3.3.5、驱动入口函数(154-190 行:mykey_init)](#3.3.5、驱动入口函数(154-190 行:mykey_init))

[3.3.6、驱动出口函数(197-206 行:mykey_exit)](#3.3.6、驱动出口函数(197-206 行:mykey_exit))

[3.4、编写测试 APP](#3.4、编写测试 APP)

3.5、应用层与驱动层的交互流程

四、运行测试

[4.1、编译驱动程序和测试 APP](#4.1、编译驱动程序和测试 APP)

4.2、运行测试

总结


前言

前面几期博客我们使用的 基本都是使用GPIO 输出功能,还没有用过 GPIO 输入功能,本期博客我们就来学习一下如何在 Linux 下编写 GPIO 输入驱动程序,我们就使用此按键来完成功能。同时使用原子操作来对按价值进行保护。


一、Linux下按键驱动原理

按键驱动和 LED 驱动本质都是操作 GPIO,区别仅在于:

LED:GPIO 输出高低电平;

按键:GPIO 读取高低电平.

在 Linux 驱动中实现按键输入功能:

驱动层读取 GPIO 电平判断按键状态;

应用层通过read函数获取按键值;

用原子操作保护按键值这个 "共享资源"(驱动写、应用读)

二、硬件原理图分析

1) LED 灯 LED0。

2)1 个按键 KEY0

按键 KEY0 的原理图如下:

图中可以看出,按键 KEY0 是连接到 I.MX6U 的 UART1_CTS 这个 IO 上的,KEY0接了一个 10K 的上拉电阻,因此 KEY0 没有按下的时候 UART1_CTS 应该是高电平,当 KEY0按下以后 UART1_CTS 就是低电平。

三、实验程序编写

3.1、修改设备树文件

3.1.1、添加pinctrl节点(引脚复用配置)

iomuxc节点的imx6ul-evk子节点下,添加按键的 pinctrl 配置:

复制代码
pinctrl_key: keygrp {
    fsl,pins = <
        MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080 /* KEY0 */
    >;
};

第 3 行,将 GPIO_IO18 这个 PIN 复用为 GPIO1_IO18。

3.1.2、添加KEY设备节点

在根节点/下创建key节点:

复制代码
key {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "atkalpha-key";  // 驱动匹配标识
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_key>;   // 关联上面的pinctrl节点
    key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0:GPIO1_IO18,低电平有效 */
    status = "okay";
};

第 6 行, pinctrl-0 属性设置 KEY 所使用的 PIN 对应的 pinctrl 节点。
第 7 行, key-gpio 属性指定了 KEY 所使用的 GPIO 。

3.1.3、检查PIN是否被其他外设使用

在本章实验中蜂鸣器使用的 PIN 为 UART1_CTS_B,因此先检查 PIN 为 UART1_CTS_B 这

个 PIN 有没有被其他的 pinctrl 节点使用,如果有使用的话就要屏蔽掉,然后再检查 GPIO1_IO18

这个 GPIO 有没有被其他外设使用,如果有的话也要屏蔽掉。

3.1.4、编译设备树

设备树编写完成以后使用如下命令重新编译设备树:

复制代码
make dtbs  # 重新编译设备树

将新生成的imx6ull-alientek-emmc.dtb替换开发板的设备树文件,重启动成功以后进入"/proc/device-tree"目录中。使用如下命令:

复制代码
cd /proc/device-tree
ls | grep key  # 能看到key节点则说明配置成功

查看"key"节点是否存在,如果存在的话就说明设备树基本修改成功(具体还要驱动验证)。

3.2、按键驱动程序编写(key.c)

复制代码
  <linux/types.h>
  2<linux/kernel.h>
  3<linux/delay.h>
  4<linux/ide.h>
  5<linux/init.h>
  6<linux/module.h>
  7<linux/errno.h>
  8<linux/gpio.h>
  9<linux/cdev.h>
 10<linux/device.h>
 11<linux/of.h>
 12<linux/of_address.h>
 1<linux/of_gpio.h>
 14 #include <linux/semaphore.h>
 15 #include<asm/mach/map.h>
 16<asm/uaccess.h>
 17<asm/io.h>
 18 /***************************************************************
 19 文件名	: key.c
 20 版本	  	: V1.0
 21 描述	  	: Linux按键输入驱动实验
 22 ***************************************************************/
 23 #define KEY_CNT		1	/* 设备号个数 	*/
 24 #define KEY_NAME	"key"	/* 名字 		*/
 25 
 26 /* 定义按键值 */
 27 #define KEY0VALUE	0XF0	/* 按键值 		*/
 28 #define INVAKEY		0X00	/* 无效的按键值  */
 29 
 30 /* key设备结构体 */
 31 struct key_dev{
 32 	dev_t devid;			/* 设备号 	*/
 33 	struct cdev cdev;		/* cdev 	*/
 34 	struct class *class;	/* 类 		*/
 35 	struct device *device;	/* 设备 	*/
 36 	int major;				/* 主设备号	*/
 37 	int minor;				/* 次设备号   */
 38 	struct device_node	*nd; /* 设备节点 */
 39 	int key_gpio;			/* key所使用的GPIO编号	*/
 40 	atomic_t keyvalue;		/* 按键值 	*/	
 41 };
 42 
 43 struct key_dev keydev;		/* key设备 */
 44 
 45 /*
 46  * @description	: 初始化按键IO,open函数打开驱动的时候
 47  * 				  初始化按键所使用的GPIO引脚。
 48  * @param 		: 无
 49  * @return 		: 无
 50  */
 51 static int keyio_init(void)
 52 {
 53 	keydev.nd = of_find_node_by_path("/key");
 54 	if (keydev.nd== NULL) {
 55 		return -EINVAL;
 56 	}
 57 
 58 	keydev.key_gpio = of_get_named_gpio(keydev.nd ,"key-gpio", 0);
 59 	if (keydev.key_g< 0) {
 60 		printk("can't get key0\r\n");
 61 		return -EINVAL;
 62 	}
 63 	printk("key_gpio=%d\r\n", keydev.key_gpio);
 64 	
 65 	/* 初始化key所使用的IO */
 66 	gpio_request(keydev.key_gpio, "key0");	/* 请求IO */
 67 	gpio_direction_input(keydev.key_gpio);	/* 设置为输入 */
 68 	return 0;
 69 }
 70 
 71 /*
 72  * @description	: 打开设备
 73  * @param - inode : 传递给驱动的inode
 74  * @param - filp 	: 设备文件,file结构体有个叫做private_data的成员变量
 75  * 				  一般在open的时候将private_data指向设备结构体。
 76  * @return 		: 0 成功;其他 失败
 77  */
 78 static int key_open(struct inode *inode, struct file *filp)
 79 {
 80 	int ret = 0;
 81 	filp->private_data = &keydev; /* 设置私有数据 */
 82 
 83 	ret = keyio_init();			/* 初始化按键IO */
 84 	if (ret< 0) {
 85 		return ret;
 86 	}
 87 
 88 	return 0;
 89 }
 90 
 91 /*
 92  * @description	: 从设备读取数据 
 93  * @param - filp 	: 要打开的设备文件(文件描述符)
 94  * @param - buf 	: 返回给用户空间的数据缓冲区
 95  * @param - cnt 	: 要读取的数据长度
 96  * @param - offt 	: 相对于文件首地址的偏移
 97  * @return 		: 读取的字节数,如果为负值,表示读取失败
 98  */
 99 static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
100 {
101 	int ret = 0;
102 	int value;
103 	struct key_dev *dev = filp->private_data;
104 
105 	if (gpio_get_value(dev->key_gpio) == 0) { 		/* key0按下 */
106 		while(!gpio_get_value(dev->key_gpio));		/* 等待按键释放 */
107 		atomic_set(&dev->keyvalue, KEY0VALUE);	
108 	} else {	
109 		atomic_set(&dev->keyvalue, INVAKEY);		/* 无效的按键值 */
110 	}
111 
112 	value = atomic_read(&dev->keyvalue);
113 	ret = copy_to_user(buf, &value, sizeof(value));
114 	return ret;
115 }
116 
117 /*
118  * @description	: 向设备写数据 
119  * @param - filp 	: 设备文件,表示打开的文件描述符
120  * @param - buf 	: 要写给设备写入的数据
121  * @param - cnt 	: 要写入的数据长度
122  * @param - offt 	: 相对于文件首地址的偏移
123  * @return 		: 写入的字节数,如果为负值,表示写入失败
124  */
125 static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
126 {
127 	return 0;
128 }
129 
130 /*
131  * @description	: 关闭/释放设备
132  * @param - filp 	: 要关闭的设备文件(文件描述符)
133  * @return 		: 0 成功;其他 失败
134  */
135 static int key_release(struct inode *inode, struct file *filp)
136 {
137 	return 0;
138 }
139 
140 /* 设备操作函数 */
141 static struct file_operations key_fops = {
142 	.owner = THIS_MODULE,
143 	.open = key_open,
144 	.read = key_read,
145 	.write = key_write,
146 	.release = 	key_release,
147 };
148 
149 /*
150  * @description	: 驱动入口函数
151  * @param 		: 无
152  * @return 		: 无
153  */
154 static int __init mykey_init(void)
155 {
156 	/* 初始化原子变量 */
157 	atomic_set(&keydev.keyvalue, INVAKEY);
158 
159 	/* 注册字符设备驱动 */
160 	/* 1、创建设备号 */
161 	if (keydev.major) {		/*  定义了设备号 */
162 		keydev.devid = MKDEV(keydev.major, 0);
163 		register_chrdev_region(keydev.devid, KEY_CNT, KEY_NAME);
164 	} else {						/* 没有定义设备号 */
165 		alloc_chrdev_region(&keydev.devid, 0, KEY_CNT, KEY_NAME);	/* 申请设备号 */
166 		keydev.major = MAJOR(keydev.devid);	/* 获取分配号的主设备号 */
167 		keydev.minor = MINOR(keydev.devid);	/* 获取分配号的次设备号 */
168 	}
169 	
170 	/* 2、初始化cdev */
171 	keydev.cdev.owner = THIS_MODULE;
172 	cdev_init(&keydev.cdev, &key_fops);
173 	
174 	/* 3、添加一个cdev */
175 	cdev_add(&keydev.cdev, keydev.devid, KEY_CNT);
176 
177 	/* 4、创建类 */
178 	keydev.class = class_create(THIS_MODULE, KEY_NAME);
179 	if (IS_ERR(keydev.class)) {
180 		return PTR_ERR(keydev.class);
181 	}
182 
183 	/* 5、创建设备 */
184 	keydev.device = device_create(keydev.class, NULL, keydev.devid, NULL, KEY_NAME);
185 	if (IS_ERR(keydev.device)) {
186 		return PTR_ERR(keydev.device);
187 	}
188 	
189 	return 0;
190 }
191 
192 /*
193  * @description	: 驱动出口函数
194  * @param 		: 无
195  * @return 		: 无
196  */
197 static void __exit mykey_exit(void)
198 {
199 	/* 注销字符设备驱动 */
200 	gpio_free(keydev.key_gpio);
201 	cdev_del(&keydev.cdev);/*  删除cdev */
202 	unregister_chrdev_region(keydev.devid, KEY_CNT); /* 注销设备号 */
203 
204 	device_destroy(keydev.class, keydev.devid);
205 	class_destroy(keydev.class);
206 }
207 
208 module_init(mykey_init);
209 module_exit(mykey_exit);
210 MODULE_LICENSE("GPL");
211 MODULE_AUTHOR("duan");

3.3 驱动代码逐段解析

3.3.1、设备结构体定义(30-43行)

将驱动所有相关资源(设备号、GPIO、原子变量等)封装到结构体,便于管理和传递,代码解析如下:

复制代码
struct key_dev{
	dev_t devid;			// 设备号(主+次,内核唯一标识)
	struct cdev cdev;		// 字符设备核心对象(关联文件操作集)
	struct class *class;	// 驱动类(自动创建设备节点用)
	struct device *device;	// 设备节点(最终生成/dev/key)
	int major;				// 主设备号(手动指定或动态分配)
	int minor;				// 次设备号(配合主设备号使用)
	struct device_node	*nd; // 设备树节点(解析key节点的硬件信息)
	int key_gpio;			// 按键对应的GPIO编号(如GPIO1_IO18)
	atomic_t keyvalue;		// 按键值(原子变量,保护共享资源)	
};
struct key_dev keydev;		// 定义全局设备实例(简化版,实际可动态分配)

3.3.2、GPIO 初始化函数(51-69 行:keyio_init)

该函数负责从设备树解析 GPIO 信息,并初始化 GPIO 为输入模式,是按键驱动的硬件初始化核心,逐行解析:

复制代码
 static int keyio_init(void)
 52 {
 53 	keydev.nd = of_find_node_by_path("/key");
 54 	if (keydev.nd== NULL) {
 55 		return -EINVAL;
 56 	}
 57 
 58 	keydev.key_gpio = of_get_named_gpio(keydev.nd ,"key-gpio", 0);
 59 	if (keydev.key_g< 0) {
 60 		printk("can't get key0\r\n");
 61 		return -EINVAL;
 62 	}
 63 	printk("key_gpio=%d\r\n", keydev.key_gpio);
 64 	
 65 	/* 初始化key所使用的IO */
 66 	gpio_request(keydev.key_gpio, "key0");	/* 请求IO */
 67 	gpio_direction_input(keydev.key_gpio);	/* 设置为输入 */
 68 	return 0;
 69 }
行号 代码逻辑 关键说明
53-56 of_find_node_by_path("/key") 从设备树中查找路径为/key的节点,失败返回-EINVAL(无效参数)。
58-62 of_get_named_gpio(nd, "key-gpio", 0) key节点中读取key-gpio属性,获取 GPIO 编号(如返回 18 表示 GPIO1_IO18);返回负数表示失败,打印错误信息。
63 printk("key_gpio=%d",keydev.key_gpio) 打印 GPIO 编号,调试用(确认设备树解析成功,方便排查问题)。
66 gpio_request(keydev.key_gpio, "key0") 向内核申请 GPIO 资源,避免引脚冲突;"key0" 是 GPIO 名称(调试用,可自定义)。
67 gpio_direction_input(keydev.key_gpio) 将 GPIO 配置为输入模式(按键核心:读取电平判断状态)

3.3.3、文件操作接口(78-138 行)

复制代码
 static int key_open(struct inode *inode, struct file *filp)
 79 {
 80 	int ret = 0;
 81 	filp->private_data = &keydev; /* 设置私有数据 */
 82 
 83 	ret = keyio_init();			/* 初始化按键IO */
 84 	if (ret< 0) {
 85 		return ret;
 86 	}
 87 
 88 	return 0;
 89 }
 90 
 91 /*
 92  * @description	: 从设备读取数据 
 93  * @param - filp 	: 要打开的设备文件(文件描述符)
 94  * @param - buf 	: 返回给用户空间的数据缓冲区
 95  * @param - cnt 	: 要读取的数据长度
 96  * @param - offt 	: 相对于文件首地址的偏移
 97  * @return 		: 读取的字节数,如果为负值,表示读取失败
 98  */
 99 static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
100 {
101 	int ret = 0;
102 	int value;
103 	struct key_dev *dev = filp->private_data;
104 
105 	if (gpio_get_value(dev->key_gpio) == 0) { 		/* key0按下 */
106 		while(!gpio_get_value(dev->key_gpio));		/* 等待按键释放 */
107 		atomic_set(&dev->keyvalue, KEY0VALUE);	
108 	} else {	
109 		atomic_set(&dev->keyvalue, INVAKEY);		/* 无效的按键值 */
110 	}
111 
112 	value = atomic_read(&dev->keyvalue);
113 	ret = copy_to_user(buf, &value, sizeof(value));
114 	return ret;
115 }
116 
117 /*
118  * @description	: 向设备写数据 
119  * @param - filp 	: 设备文件,表示打开的文件描述符
120  * @param - buf 	: 要写给设备写入的数据
121  * @param - cnt 	: 要写入的数据长度
122  * @param - offt 	: 相对于文件首地址的偏移
123  * @return 		: 写入的字节数,如果为负值,表示写入失败
124  */
125 static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
126 {
127 	return 0;
128 }
129 
130 /*
131  * @description	: 关闭/释放设备
132  * @param - filp 	: 要关闭的设备文件(文件描述符)
133  * @return 		: 0 成功;其他 失败
134  */
135 static int key_release(struct inode *inode, struct file *filp)
136 {
137 	return 0;
138 }
1. open 函数(78-89 行:key_open)
复制代码
 static int key_open(struct inode *inode, struct file *filp)
 79 {
 80 	int ret = 0;
 81 	filp->private_data = &keydev; /* 设置私有数据 */
 82 
 83 	ret = keyio_init();			/* 初始化按键IO */
 84 	if (ret< 0) {
 85 		return ret;
 86 	}
 87 
 88 	return 0;
 89 }
行号 核心逻辑 关键说明
81 filp->private_data = &keydev 将设备结构体赋值给file的私有数据,后续read/write可直接通过私有数据获取设备信息,是驱动层通用写法。
83-86 调用keyio_init() 打开驱动时初始化 GPIO。
2. read 函数(99-115 行:key_read)

应用层调用read函数时,该函数执行,负责读取按键状态、设置按键值,并将值拷贝到用户空间,逐行解析:

复制代码
 91 /*
 92  * @description	: 从设备读取数据 
 93  * @param - filp 	: 要打开的设备文件(文件描述符)
 94  * @param - buf 	: 返回给用户空间的数据缓冲区
 95  * @param - cnt 	: 要读取的数据长度
 96  * @param - offt 	: 相对于文件首地址的偏移
 97  * @return 		: 读取的字节数,如果为负值,表示读取失败
 98  */
 99 static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
100 {
101 	int ret = 0;
102 	int value;
103 	struct key_dev *dev = filp->private_data;
104 
105 	if (gpio_get_value(dev->key_gpio) == 0) { 		/* key0按下 */
106 		while(!gpio_get_value(dev->key_gpio));		/* 等待按键释放 */
107 		atomic_set(&dev->keyvalue, KEY0VALUE);	
108 	} else {	
109 		atomic_set(&dev->keyvalue, INVAKEY);		/* 无效的按键值 */
110 	}
111 
112 	value = atomic_read(&dev->keyvalue);
113 	ret = copy_to_user(buf, &value, sizeof(value));
114 	return ret;
115 }
行号 代码逻辑 关键说明
103 struct key_dev*dev=filp>private_data 从私有数据中获取设备结构体,避免全局变量的滥用,规范写法。
105 gpio_get_value(dev->key_gpio) == 0 读取 GPIO 电平 0 = 按键按下(低电平有效) 1 = 未按下,与硬件电平逻辑对应。
106 while(!gpio_get_value(dev->key_gpio)) 等待按键释放(阻塞式,直到电平变为 1); 缺点:无消抖,按键机械抖动可能导致多次触发。
107 atomic_set(&dev->keyvalue, KEY0VALUE) 原子操作设置按键值为 0XF0(按下),保证赋值原子性,避免多线程访问冲突。
109 atomic_set(&dev->keyvalue, INVAKEY) 未按下时,原子操作设置为无效值 0X00,区分 "按下" 和 "未按下" 状态。
112 value = atomic_read(&dev->keyvalue) 原子操作读取按键值(避免读写冲突),确保读取到的是完整、正确的值。
113 copy_to_user(buf, &value, sizeof(value)) 将按键值从内核空间拷贝到用户空间(应用层 read 的核心); 返回 0 表示成功,负数表示失败(如拷贝失败)。
3. write/release 函数(125-138 行)
复制代码
/*
118  * @description	: 向设备写数据 
119  * @param - filp 	: 设备文件,表示打开的文件描述符
120  * @param - buf 	: 要写给设备写入的数据
121  * @param - cnt 	: 要写入的数据长度
122  * @param - offt 	: 相对于文件首地址的偏移
123  * @return 		: 写入的字节数,如果为负值,表示写入失败
124  */
125 static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
126 {
127 	return 0;
128 }
129 
130 /*
131  * @description	: 关闭/释放设备
132  * @param - filp 	: 要关闭的设备文件(文件描述符)
133  * @return 		: 0 成功;其他 失败
134  */
135 static int key_release(struct inode *inode, struct file *filp)
136 {
137 	return 0;
138 }

key_write(125-128 行):返回 0,无实际功能(按键是输入设备,无需写操作,预留接口可后续扩展);

key_release(135-138 行):返回 0,无资源释放(GPIO 释放移到驱动出口,避免重复释放)。

3.3.4、设备操作集(140-147 行)

Linux 字符设备驱动的 "接口映射表",内核通过该结构体调用驱动的具体函数,代码解析:

复制代码
static struct file_operations key_fops = {
	.owner = THIS_MODULE,	// 归属本模块(内核安全机制,防止模块被意外卸载)
	.open = key_open,		// 关联open函数(应用层open时调用)
	.read = key_read,		// 关联read函数(应用层read时调用)
	.write = key_write,		// 关联write函数(应用层write时调用)
	.release = 	key_release,	// 关联release函数(应用层close时调用)
};

3.3.5、驱动入口函数(154-190 行:mykey_init)

驱动加载时执行(insmod/modprobe),完成字符设备驱动的注册流程,是驱动的 "启动入口",步骤清晰:

复制代码
150  * @description	: 驱动入口函数
151  * @param 		: 无
152  * @return 		: 无
153  */
154 static int __init mykey_init(void)
155 {
156 	/* 初始化原子变量 */
157 	atomic_set(&keydev.keyvalue, INVAKEY);
158 
159 	/* 注册字符设备驱动 */
160 	/* 1、创建设备号 */
161 	if (keydev.major) {		/*  定义了设备号 */
162 		keydev.devid = MKDEV(keydev.major, 0);
163 		register_chrdev_region(keydev.devid, KEY_CNT, KEY_NAME);
164 	} else {						/* 没有定义设备号 */
165 		alloc_chrdev_region(&keydev.devid, 0, KEY_CNT, KEY_NAME);	/* 申请设备号 */
166 		keydev.major = MAJOR(keydev.devid);	/* 获取分配号的主设备号 */
167 		keydev.minor = MINOR(keydev.devid);	/* 获取分配号的次设备号 */
168 	}
169 	
170 	/* 2、初始化cdev */
171 	keydev.cdev.owner = THIS_MODULE;
172 	cdev_init(&keydev.cdev, &key_fops);
173 	
174 	/* 3、添加一个cdev */
175 	cdev_add(&keydev.cdev, keydev.devid, KEY_CNT);
176 
177 	/* 4、创建类 */
178 	keydev.class = class_create(THIS_MODULE, KEY_NAME);
179 	if (IS_ERR(keydev.class)) {
180 		return PTR_ERR(keydev.class);
181 	}
182 
183 	/* 5、创建设备 */
184 	keydev.device = device_create(keydev.class, NULL, keydev.devid, NULL, KEY_NAME);
185 	if (IS_ERR(keydev.device)) {
186 		return PTR_ERR(keydev.device);
187 	}
188 	
189 	return 0;
190 }
步骤 行号 代码逻辑 关键说明
初始化原子变量 157 atomic_set(&keydev.keyvalue, INVAKEY) 初始化为无效按键值(未按下),避免初始值异常。
1. 创建设备号 161-168 ① 静态注册: 若指定了major,用register_chrdev_region; ② 动态注册: 未指定则用alloc_chrdev_regionMAJOR/MINOR:从devid中解析主 / 次设备号。 字符设备驱动第一步:申请设备号(内核唯一标识,应用层通过设备号访问驱动)。
2. 初始化 cdev 171-172 cdev_init(&keydev.cdev,&key_fops) cdevfile_operations绑定,告诉内核该字符设备的操作接口。
3. 添加 cdev 175 cdev_add(&keydev.cdev,keydev.devid, KEY_CNT) 将 cdev 添加到内核,完成字符设备注册,内核开始识别该设备。
4. 创建类 178-181 class_create(THIS_MODULE, KEY_NAME) 创建驱动类(/sys/class/key),失败则返回错误码,类用于自动创建设备节点。
5. 创建设备 184-187 device_create(keydev.class, NULL,devid, NULL, KEY_NAME) 基于类自动创建/dev/key设备节点(无需手动 mknod,应用层通过该节点访问驱动)。

3.3.6、驱动出口函数(197-206 行:mykey_exit)

驱动卸载时执行(rmmod),释放所有资源(反向流程),避免内存泄漏,逐行解析:

行号 代码逻辑 关键说明
200 gpio_free(keydev.key_gpio) 释放申请的 GPIO 资源(避免内存泄漏,必须与 gpio_request 对应)。
201 cdev_del(&keydev.cdev) 从内核删除 cdev 对象,注销字符设备操作接口。
202 unregister_chrdev_region 注销设备号(归还内核,避免设备号占用)。
204 device_destroy 删除/dev/key设备节点(与 device_create 对应)。
205 class_destroy 删除驱动类(/sys/class/key),与 class_create 对应。

3.4、编写测试****APP

新建名为 keyApp.c 的文件,然后输入如下所示内容:

复制代码
#include <stdio.h>          // 标准输入输出(printf)
#include <unistd.h>         // 系统调用(read/write/close)
#include <sys/types.h>      // 类型定义(如pid_t)
#include <sys/stat.h>       // 文件状态(open函数参数)
#include <fcntl.h>          // 文件控制(O_RDWR等宏)
#include <stdlib.h>         // 标准库(exit/atoi等)
#include <string.h>         // 字符串操作(本文未用到,预留)

/* 按键值定义(和驱动一致!必须与驱动中KEY0VALUE/INVAKEY宏匹配) */
#define KEY0VALUE 0XF0      // 按键按下的标识值(对应驱动22行)
#define INVAKEY   0X00      // 无效按键值(对应驱动23行)

/*
 * @brief  主函数:应用层核心逻辑,与驱动交互读取按键值
 * @param  argc: 参数个数
 * @param  argv: 参数列表(需传入/dev/key设备节点路径)
 * @return 0:成功; 其他:失败
 */
int main(int argc, char *argv[])
{
    int fd, ret;                     // fd:文件描述符;ret:函数返回值
    char *filename;                  // 存储设备节点路径(如/dev/key)
    unsigned char keyvalue;          // 存储从驱动读取的按键值

    /* 步骤1:校验命令行参数 */
    if(argc != 2){                   // 必须传入1个参数(设备节点路径)
        printf("Usage: %s /dev/key\r\n", argv[0]);  // 提示正确用法
        return -1;                   // 参数错误,返回-1退出
    }
    filename = argv[1];              // 保存传入的设备节点路径(如/dev/key)

    /* 步骤2:打开驱动设备节点 */
    fd = open(filename, O_RDWR);     // 以读写模式打开设备节点
    if(fd < 0){                      // 打开失败(如驱动未加载、节点不存在)
        printf("open %s failed!\r\n", filename);
        return -1;                   // 打开失败,返回-1退出
    }

    /* 步骤3:循环读取按键值(核心逻辑) */
    printf("Start read key value...\r\n");  // 提示开始读取
    while(1) {                       // 死循环,持续检测按键
        // 从驱动读取1字节数据(按键值)到keyvalue变量
        read(fd, &keyvalue, sizeof(keyvalue));  
        
        // 判断是否读取到有效按键值(对应驱动68行设置的KEY0VALUE)
        if (keyvalue == KEY0VALUE) {  
            printf("KEY0 Press, value = 0x%X\r\n", keyvalue);  // 打印按键按下信息
        }
        // 若为INVAKEY(未按键),无输出,继续循环
    }

    /* 步骤4:关闭设备(实际不会执行到,因为上面是死循环) */
    ret = close(fd);                 // 关闭文件描述符
    if(ret < 0){                     // 关闭失败
        printf("close %s failed!\r\n", filename);
        return -1;
    }
    return 0;                        // 程序正常退出(实际不会执行)
}
行号 核心代码 关键说明
18-22 if(argc != 2) 校验命令行参数: ✅ 运行命令必须是./keyApp /dev/key(1 个参数); ✅ 若直接运行./keyApp,会提示用法并退出,避免程序崩溃;
25-30 fd = open(filename, O_RDWR) 打开设备节点的核心注意点: ✅ fd是文件描述符(非负整数),后续 read/close 都依赖它;
行号 核心代码 关键说明
37 read(fd,&keyvalue,sizeof(keyvalue)) 应用层与驱动层交互的核心 ✅ 调用read时,会触发驱动层的key_read函数(驱动 90 行) ✅ 参数说明: fd:打开设备的文件描述符; &keyvalue:接收数据的缓冲区(用户空间); sizeof(keyvalue):读取 1 字节(unsigned char); ✅ 驱动层通过copy_to_user(驱动 74 行)将按键值拷贝到该缓冲区;
40 if (keyvalue == KEY0VALUE) 按键检测逻辑: ✅ 仅当读取到驱动设置的0XF0时,判定为按键按下; ✅ 若为0X00(未按键),无输出,继续循环; ✅ 死循环while(1):持续检测按键,符合嵌入式实时性需求;

**3.5、**应用层与驱动层的交互流程

四、运行测试

4.1、编译驱动程序和测试 APP

编写 Makefile 文件,本次实验的 Makefile 文件和之前的led实验基本一样,只是将 obj-m 变量的值改为key.o,Makefile 内容如下所示:

复制代码
KERNELDIR := /home/duan/linux/linux-imx-rel_imx_4.1.15_2.1.1_ga_alientek_v2.2
CURRENT_PATH := $(shell pwd)

obj-m :=key.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

第 4 行,设置 obj-m 变量的值为key.o。

输入如下命令编译出驱动模块文件:

复制代码
make -j32

编译成功以后就会生成一个名为"key.ko"的驱动模块文件。

编译测试 APP

输入如下命令编译测试试keyApp.c这个测试程序:

复制代码
arm-linux-gnueabihf-gcc keyApp.c -o  keyApp

编译成功以后就会生成 keyApp 这个应用程序。

4.2、运行测试

将上一小节编译出来的key.ko 和 keyApp这两个文件拷贝到 rootfs/lib/modules/4.1.15 目录中。

复制代码
sudo cp key.ko /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f

sudo cp  keyApp /home/duan/linux/nfs/rootfs/lib/modules/4.1.15/ -f

进入到目录 lib/modules/4.1.15 中,输入如下命令加载key.ko驱动模块:

复制代码
depmod //第一次加载驱动的时候需要运行此命令
modprobe key.ko//加载驱动

驱动加载成功以后如下命令来测试:

复制代码
./keyApp /dev/key

如下图所示:

按下开发板上的 KEY0 按键, keyApp 就会获取并且输出按键信息,如下图所示:

从上图可以看出,当我们按下 KEY0 以后就会打印出"KEY0 Press, value = 0XF0",

表示按键按下。但是大家可能会发现,有时候按下一次 KEY0 但是会输出好几行"KEY0 Press,

value = 0XF0",这是因为我们的代码没有做按键消抖处理。

如果要卸载驱动的话输入如下命令即可:

复制代码
rmmod key.ko

总结

本期博客完成了如何在 Linux 下编写 GPIO 输入驱动程序,我们就使用此按键来完成功能。同时使用原子操作来对按价值进行保护。

相关推荐
亦复何言??2 小时前
Ubuntu内核更新导致显卡驱动掉线 (nvidia-smi报错) 的“最小化改动”修复方案
linux·运维·ubuntu
_OP_CHEN2 小时前
【MySQL数据库基础】(三)MySQL 库的核心操作全解析:创建、修改、备份一条龙搞定
linux·数据库·sql·mysql·c/c++·mysql操作·企业级组件
_OP_CHEN2 小时前
【Linux系统编程】(四十六)线程池原理与实现:从固定线程池到线程安全单例模式
linux·单例模式·操作系统·线程池·进程·线程安全·c/c++
爱吃生蚝的于勒2 小时前
【Linux】网络基础(一)
linux·运维·服务器·网络·后端·算法·架构
ken22322 小时前
双硬盘、分别独立安装 linux & win 双系统。BIOS 设置问题导致无法启动: UEFI 与 Legacy
linux·运维·服务器
程序员敲代码吗2 小时前
深入解析ZooKeeper在分布式系统中的角色与挑战
linux·分布式·zookeeper
HalvmånEver2 小时前
Linux:初始网络(下)
linux·运维·网络·学习
欧耶~~2 小时前
tomcat
java·linux·tomcat
炽天使3282 小时前
龙虾尝鲜记(2)——装ubuntu(续)
linux·运维·ubuntu