
🎬 渡水无言 :个人主页渡水无言
❄专栏传送门 : 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》
❄专栏传送门 : 《freertos专栏》 《STM32 HAL库专栏》《linux裸机开发专栏》
⭐️流水不争先,争的是滔滔不绝
📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生
| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生
在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连
目录
[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.3 驱动代码逐段解析](#3.3 驱动代码逐段解析)
[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)
[4.1、编译驱动程序和测试 APP](#4.1、编译驱动程序和测试 APP)
前言
前面几期博客我们使用的 基本都是使用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_region ③ MAJOR/MINOR:从devid中解析主 / 次设备号。 |
字符设备驱动第一步:申请设备号(内核唯一标识,应用层通过设备号访问驱动)。 |
| 2. 初始化 cdev | 171-172 | cdev_init(&keydev.cdev,&key_fops) |
将cdev与file_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 输入驱动程序,我们就使用此按键来完成功能。同时使用原子操作来对按价值进行保护。