Linux按键驱动开发
Linux按键驱动开发
1. 概述
本笔记详细分析基于i.MX6ULL开发板的按键驱动程序实现。该驱动采用设备树(Device Tree)方式获取硬件信息,通过GPIO子系统读取按键状态,实现了标准的字符设备驱动框架。
2. 硬件原理
2.1 按键电路
按键通常连接在GPIO引脚上,通过上拉/下拉电阻形成稳定的高/低电平。当按键未按下时,GPIO保持高电平(或低电平,取决于电路设计);当按键按下时,GPIO电平发生变化。
2.2 i.MX6ULL GPIO架构
i.MX6ULL具有多个GPIO控制器(GPIO1-GPIO7),每个控制器管理32个GPIO引脚。GPIO操作需要经过以下步骤:
- 使能相应的时钟
- 配置引脚复用功能(IOMUX)
- 配置电气特性(如上下拉、驱动能力)
- 配置GPIO方向(输入/输出)
- 读取或写入GPIO值
3. 设备树配置
3.1 设备节点定义
在imx6ull-alientek-emmc.dts
中定义了按键设备节点:
key{compatible = "alientek,key";pinctrl-names = "default";pinctrl-0 = <&pinctrl_key>;states = "okay";key-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;interrupt-parent = <&gpio1>;interrupts = <18 IRQ_TYPE_EDGE_BOTH>;
};
3.2 关键属性说明
compatible
: 匹配驱动的标识符,驱动程序通过此值找到对应的设备pinctrl-0
: 引用引脚控制组,配置GPIO的复用和电气特性key-gpios
: 定义使用的GPIO,格式为<&gpio_controller pin_number active_level>
interrupts
: 定义中断信息,此处配置为边沿触发
3.3 引脚控制配置
在iomuxc
节点中定义了按键引脚的复用和电气特性:
pinctrl_key: keygrp {fsl,pins = <MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0xF080>;
}
其中0xF080
是PUE/PULL设置值,具体含义需参考i.MX6ULL参考手册。
4. 驱动程序分析
4.1 头文件包含
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>
关键头文件说明:
<linux/module.h>
: 模块相关定义<linux/fs.h>
: 文件系统相关定义<linux/cdev.h>
: 字符设备相关定义<linux/device.h>
: 设备模型相关定义<linux/of.h>
: 设备树相关定义<linux/gpio.h>
: GPIO子系统相关定义<linux/of_gpio.h>
: 设备树GPIO相关定义
4.2 宏定义
#define GPIOKEY_CNT 1
#define GPIOKEY_NAME "key"
#define KEYVALUE 11
#define KEYINVA 10
GPIOKEY_CNT
: 设备数量GPIOKEY_NAME
: 设备名称KEYVALUE
: 按键按下时返回的值KEYINVA
: 按键未按下时返回的值
4.3 设备结构体
struct key_dev {dev_t devid; /* 设备号 */int major; /* 主设备号 */int minor; /* 次设备号 */struct cdev cdev; /* cdev结构体 */struct class *class; /* 类 */struct device *device; /* 设备 */struct device_node *nd; /* 设备节点 */int key_gpio; /* 按键GPIO编号 */atomic_t keyvalue; /* 按键值,使用原子变量保证线程安全 */
};
4.4 文件操作函数
4.4.1 open函数
static int key_open(struct inode *inode, struct file *filp)
{filp->private_data = &key;return 0;
}
将设备结构体指针保存到文件私有数据中,便于其他操作函数访问。
4.4.2 release函数
static int key_release(struct inode *inode, struct file *filp)
{return 0;
}
释放资源,此处无需特殊处理。
4.4.3 write函数
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{return 0;
}
按键为输入设备,不支持写操作。
4.4.4 read函数
static ssize_t key_read(struct file *filp, char __user *buf, size_t cnt, loff_t *ppos)
{int value;struct key_dev *dev = filp->private_data;u8 ret = 0;if (gpio_get_value(dev->key_gpio) == 0) {while (gpio_get_value(dev->key_gpio) == 0);atomic_set(&dev->keyvalue, KEYVALUE);} else {atomic_set(&dev->keyvalue, KEYINVA);}value = atomic_read(&dev->keyvalue);if (copy_to_user(buf, &value, sizeof(value))) {return -EFAULT;}return sizeof(value);
}
读取按键状态的关键函数:
- 读取GPIO值
- 如果为低电平(按键按下),等待按键释放(去抖动)
- 设置按键值
- 将按键值复制到用户空间
4.5 文件操作结构体
static const struct file_operations key_fops = {.owner = THIS_MODULE,.open = key_open,.release = key_release,.write = key_write,.read = key_read,
};
定义了驱动支持的文件操作函数。
4.6 GPIO初始化函数
static int keyio_init(struct key_dev *dev)
{u8 ret = 0;/* 查找设备节点 */dev->nd = of_find_node_by_path("/key");if (!dev->nd) {ret = -EFAULT;goto fail_nd;}/* 获取GPIO编号 */dev->key_gpio = of_get_named_gpio(dev->nd, "key-gpios", 0);if (dev->key_gpio < 0) {ret = -EFAULT;goto fail_prop_read;}/* 申请GPIO */ret = gpio_request(dev->key_gpio, "key");if (ret < 0) {goto fail_gpio_req;}/* 设置GPIO为输入 */ret = gpio_direction_input(dev->key_gpio);if (ret) {ret = -EFAULT;goto fail_gpio_dir;}return 0;fail_gpio_dir:gpio_free(dev->key_gpio);
fail_gpio_req:
fail_prop_read:
fail_nd:return ret;
}
GPIO初始化流程:
- 通过设备树路径查找设备节点
- 从设备节点获取GPIO编号
- 申请GPIO资源
- 设置GPIO为输入模式
4.7 模块初始化函数
static int __init key_init(void)
{u8 ret = 0;/* 初始化按键值 */atomic_set(&key.keyvalue, KEYINVA);/* 申请设备号 */key.major = 0;if (key.major) {key.devid = MKDEV(key.major, 0);ret = register_chrdev_region(key.devid, GPIOKEY_CNT, GPIOKEY_NAME);} else {ret = alloc_chrdev_region(&key.devid, 0, GPIOKEY_CNT, GPIOKEY_NAME);key.major = MAJOR(key.devid);key.minor = MINOR(key.devid);}if (ret < 0) {goto fail_devid;}/* 初始化cdev */key.cdev.owner = THIS_MODULE;cdev_init(&key.cdev, &key_fops);ret = cdev_add(&key.cdev, key.devid, GPIOKEY_CNT);if (ret < 0) {goto fail_cedv_add;}/* 创建设备类 */key.class = class_create(key.cdev.owner, GPIOKEY_NAME);if (IS_ERR(key.class)) {ret = PTR_RET(key.class);goto fail_class;}/* 创建设备 */key.device = device_create(key.class, NULL, key.devid, NULL, GPIOKEY_NAME);if (IS_ERR(key.device)) {ret = PTR_RET(key.device);goto fail_device;}/* 初始化GPIO */ret = keyio_init(&key);if (ret < 0) {goto fail_init;}return 0;fail_init:printk("GPIO INIT ERROR!!\r\n");
fail_device:class_destroy(key.class);
fail_class:cdev_del(&key.cdev);
fail_cedv_add:unregister_chrdev(key.major, GPIOKEY_NAME);
fail_devid:return ret;
}
模块初始化流程:
- 设置初始按键值
- 申请设备号(动态分配)
- 初始化并添加字符设备
- 创建设备类
- 创建设备文件
- 初始化GPIO
4.8 模块退出函数
static void __exit key_exit(void)
{gpio_set_value(key.key_gpio, 1);gpio_free(key.key_gpio);device_destroy(key.class, key.devid);class_destroy(key.class);cdev_del(&key.cdev);unregister_chrdev(key.major, GPIOKEY_NAME);
}
清理资源,释放所有申请的资源。
5. Makefile分析
KERNERDIR := /home/ubuntu2004/linux/IMX6ULL/linux/linux-imx-rel_imx_4.1.15_2.1.0_ga
CURRENTDIR := $(shell pwd)obj-m := key.o
build : kernel_moduleskernel_modules:$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) modulesclean:$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) clean
5.1 关键变量
KERNERDIR
: 内核源码目录CURRENTDIR
: 当前目录obj-m
: 指定生成的模块文件
5.2 编译命令
make -C $(KERNERDIR) M=$(CURRENTDIR) modules
-C
: 切换到内核源码目录M=$(CURRENTDIR)
: 指定模块源码目录modules
: 编译模块目标
6. 应用程序分析
6.1 头文件包含
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
包含系统调用和标准库函数所需头文件。
6.2 main函数
int main(int argc, char *argv[])
{int cnt = 0;int value;if (argc != 2) {fprintf(stderr, "Usage: %s <device>\n", argv[0]);return -1;}char *fileanme = argv[1];int fd = 0;fd = open(fileanme, O_RDWR);if (fd < 0) {perror("open device error\r\n");return -1;}while (1) {int ret = read(fd, &value, sizeof(value));if (ret <= 0) {printf("User: read error\r\n");} else {if (value == KEYVALUE) {printf("KEY0 press, value: %d\r\n", value);}}sleep(1);}close(fd);return 0;
}
6.3 程序流程
- 检查命令行参数
- 打开设备文件
- 循环读取按键状态
- 判断按键是否按下
- 打印结果
- 延时1秒
7. 编译与测试
7.1 编译驱动
make
生成key.ko
文件。
7.2 加载驱动
insmod key.ko
7.3 运行应用程序
./keyAPP /dev/key
7.4 卸载驱动
rmmod key
8. 关键技术点总结
8.1 设备树使用
- 使用
of_find_node_by_path()
查找设备节点 - 使用
of_get_named_gpio()
获取GPIO编号 - 驱动与设备信息分离,提高代码可移植性
8.2 GPIO子系统
- 使用
gpio_request()
申请GPIO - 使用
gpio_direction_input()
设置输入模式 - 使用
gpio_get_value()
读取GPIO值 - 使用
gpio_free()
释放GPIO
8.3 字符设备驱动框架
- 使用
alloc_chrdev_region()
动态分配设备号 - 使用
cdev_init()
和cdev_add()
注册字符设备 - 使用
class_create()
和device_create()
自动创建设备文件
8.4 原子变量
- 使用
atomic_t
和atomic_set()
/atomic_read()
保证多线程安全 - 避免使用锁带来的性能开销
8.5 去抖动处理
- 在检测到按键按下后,等待按键释放
- 简单有效的软件去抖动方法
9. 参考资料
- 《i.MX6ULL参考手册》
- 《Linux设备驱动程序》
- 《设备树规范》