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

ARM Linux 驱动开发篇--- Linux 按键输入实验--- Ubuntu20.04互斥体实验
🎬 渡水无言个人主页渡水无言

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

专栏传送门: 《freertos专栏》《STM32 HAL库专栏》《linux裸机开发专栏
⭐️流水不争先,争的是滔滔不绝

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

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

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

目录

前言

 一、Linux下按键驱动原理

二、硬件原理图分析

三、实验程序编写

3.1、修改设备树文件

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

3.1.2、添加 KEY 设备节点

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

3.1.4、编译设备树

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

3.3 驱动代码逐段解析

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

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

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

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

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

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

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

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

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

3.4、编写测试 APP

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

四、运行测试

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 }
行号核心逻辑关键说明
81filp->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 }
行号代码逻辑关键说明
103struct key_dev*dev=filp>private_data从私有数据中获取设备结构体,避免全局变量的滥用,规范写法。
105gpio_get_value(dev->key_gpio) == 0

读取 GPIO 电平

0 = 按键按下(低电平有效)

1 = 未按下,与硬件电平逻辑对应。

106while(!gpio_get_value(dev->key_gpio))

等待按键释放(阻塞式,直到电平变为 1);

缺点:无消抖,按键机械抖动可能导致多次触发。

107atomic_set(&dev->keyvalue, KEY0VALUE)原子操作设置按键值为 0XF0(按下),保证赋值原子性,避免多线程访问冲突。
109atomic_set(&dev->keyvalue, INVAKEY)未按下时,原子操作设置为无效值 0X00,区分 “按下” 和 “未按下” 状态。
112value = atomic_read(&dev->keyvalue)原子操作读取按键值(避免读写冲突),确保读取到的是完整、正确的值。
113copy_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)

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),释放所有资源(反向流程),避免内存泄漏,逐行解析:

行号代码逻辑关键说明
200gpio_free(keydev.key_gpio)释放申请的 GPIO 资源(避免内存泄漏,必须与 gpio_request 对应)。
201cdev_del(&keydev.cdev)从内核删除 cdev 对象,注销字符设备操作接口。
202unregister_chrdev_region注销设备号(归还内核,避免设备号占用)。
204device_destroy删除/dev/key设备节点(与 device_create 对应)。
205class_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-22if(argc != 2)

校验命令行参数:

✅ 运行命令必须是./keyApp /dev/key(1 个参数);

✅ 若直接运行./keyApp,会提示用法并退出,避免程序崩溃;

25-30fd = open(filename, O_RDWR)

打开设备节点的核心注意点:

fd是文件描述符(非负整数),后续 read/close 都依赖它;

行号核心代码关键说明
37read(fd,&keyvalue,sizeof(keyvalue))

应用层与驱动层交互的核心

✅ 调用read时,会触发驱动层的key_read函数(驱动 90 行)

✅ 参数说明:

 fd:打开设备的文件描述符;

&keyvalue:接收数据的缓冲区(用户空间);

sizeof(keyvalue):读取 1 字节(unsigned char);

✅ 驱动层通过copy_to_user(驱动 74 行)将按键值拷贝到该缓冲区;

40if (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 输入驱动程序,我们就使用此按键来完成功能。同时使用原子操作来对按价值进行保护。

Read more

xxxwww在电商爬虫中的实际应用案例

快速体验 1. 打开 InsCode(快马)平台 https://www.inscode.net 2. 点击'项目生成'按钮,等待项目生成完整后预览效果 输入框内输入如下内容: 构建一个基于xxxwww的电商爬虫系统,能够自动抓取指定电商平台的商品信息(名称、价格、评价等),并将数据清洗后存储到MySQL数据库。要求实现定时任务和反爬虫策略,输出可视化报表。 电商数据爬虫的需求背景 在电商运营和市场竞争分析中,及时获取竞品价格、用户评价等数据至关重要。传统人工收集效率低下,而爬虫技术可以自动化这一过程。最近我用xxxwww技术实现了一个电商爬虫系统,能够定时抓取多个平台商品数据并生成可视化报表,大幅提升了团队的数据获取效率。 系统核心功能设计 整个系统主要分为四个模块,每个模块都针对电商数据特点做了优化: 1. 爬虫调度模块:负责管理爬取任务队列,协调多个平台的爬取节奏 2. 数据抓取模块:使用xxxwww技术实现商品详情页的精准定位和数据提取 3. 数据处理模块:对原始数据进行清洗、去重和格式标准化 4.

By Ne0inhk
MySQL 性能优化:索引优化与查询优化

MySQL 性能优化:索引优化与查询优化

MySQL 性能优化:索引优化与查询优化 在实际生产环境中,数据库性能对业务响应速度和系统稳定性至关重要。MySQL 提供了多种手段来提升查询性能,而索引优化与查询优化是其中最常见也是最有效的方法。本文将详细探讨如何通过合理设计索引和优化查询语句来改善 MySQL 的性能。 1. 索引优化 1.1 索引的作用 索引类似于书籍的目录,能够大幅减少查询时的数据扫描量,加快数据定位。通过为查询条件和排序字段建立索引,可以提高 SELECT、JOIN 和 WHERE 子句的执行效率。 1.2 常见索引类型 * B-Tree 索引:MySQL 默认的索引类型,适用于大部分场景(如范围查询、精确匹配)。 * 哈希索引:主要应用于 MEMORY 存储引擎,对于等值查询有较高性能,但不支持范围查询。 * 全文索引:专为文本搜索设计,适用于 MyISAM 和 InnoDB(从 5.

By Ne0inhk
【SpringAI】第五弹:基于 Spring AI ToolCallback Function 实现文件操作、联网搜索、网页抓取、终端操作、资源下载、PDF生成等工具的开发与调用、核心特性解析

【SpringAI】第五弹:基于 Spring AI ToolCallback Function 实现文件操作、联网搜索、网页抓取、终端操作、资源下载、PDF生成等工具的开发与调用、核心特性解析

本节重点 以 Sprin‏g AI 框架为例,学习 A‏I 应用开发的核心特性 ——‏ 工具调用,大幅增强 AI ‏的能力,并实战主流工具的开发‌,熟悉工具的原理和高级特性。 具体内容包括: * 工具调用介绍 * Spring AI 工具开发 * 主流工具开发 * 文件操作 * 联网搜索 * 网页抓取 * 终端操作 * 资源下载 * PDF 生成 * 工具进阶知识(原理和高级特性) 重点理解 1. 使用 @Tool和 @ToolParam注解标记类方法 Spring AI 实现工具调用的流程; Tool Calling 的工作原理 一、需求分析 之前我们通过 RAG 技术让 AI 应用具备了根据外部知识库来获取信息并回答的能力,但是直到目前为止,

By Ne0inhk
YARN架构解析:深入理解Hadoop资源管理核心

YARN架构解析:深入理解Hadoop资源管理核心

YARN架构解析:深入理解Hadoop资源管理核心 🌟 你好,我是 励志成为糕手 ! 🌌 在代码的宇宙中,我是那个追逐优雅与性能的星际旅人。 ✨ 每一行代码都是我种下的星光,在逻辑的土壤里生长成璀璨的银河; 🛠️ 每一个算法都是我绘制的星图,指引着数据流动的最短路径; 🔍 每一次调试都是星际对话,用耐心和智慧解开宇宙的谜题。 🚀 准备好开始我们的星际编码之旅了吗? 目录 * YARN架构解析:深入理解Hadoop资源管理核心 * 摘要 * 1. YARN架构概述 * 1.1 设计理念与核心价值 * 1.2 核心组件架构 * 2. 核心组件深度解析 * 2.1 ResourceManager:集群资源的统一调度者 * 2.2 NodeManager:节点资源的守护者 * 2.3 ApplicationMaster:应用程序的智能管家 * 3. YARN调度策略深度分析 * 3.1 调度器对比分析 * 3.2 Capacity Scheduler配置实战 * 3.

By Ne0inhk