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_modules
kernel_modules:
$(MAKE) -C $(KERNERDIR) M=$(CURRENTDIR) modules
clean:
$(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设备驱动程序》
- 《设备树规范》