Linux按键驱动开发

发布于:2025-09-02 ⋅ 阅读:(14) ⋅ 点赞:(0)

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操作需要经过以下步骤:

  1. 使能相应的时钟
  2. 配置引脚复用功能(IOMUX)
  3. 配置电气特性(如上下拉、驱动能力)
  4. 配置GPIO方向(输入/输出)
  5. 读取或写入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);
}

读取按键状态的关键函数:

  1. 读取GPIO值
  2. 如果为低电平(按键按下),等待按键释放(去抖动)
  3. 设置按键值
  4. 将按键值复制到用户空间

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初始化流程:

  1. 通过设备树路径查找设备节点
  2. 从设备节点获取GPIO编号
  3. 申请GPIO资源
  4. 设置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;
}

模块初始化流程:

  1. 设置初始按键值
  2. 申请设备号(动态分配)
  3. 初始化并添加字符设备
  4. 创建设备类
  5. 创建设备文件
  6. 初始化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. 检查命令行参数
  2. 打开设备文件
  3. 循环读取按键状态
  4. 判断按键是否按下
  5. 打印结果
  6. 延时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_tatomic_set()/atomic_read()保证多线程安全
  • 避免使用锁带来的性能开销

8.5 去抖动处理

  • 在检测到按键按下后,等待按键释放
  • 简单有效的软件去抖动方法

9. 参考资料

  • 《i.MX6ULL参考手册》
  • 《Linux设备驱动程序》
  • 《设备树规范》