当前位置: 首页 > java >正文

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

  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_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. 检查命令行参数
  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设备驱动程序》
  • 《设备树规范》
http://www.xdnf.cn/news/19134.html

相关文章:

  • 明远智睿 RK3568 核心板:以硬核性能解锁多领域应用新可能
  • 手写一个Spring框架
  • 【活动回顾】“智驱未来,智领安全” AI+汽车质量与安全论坛
  • Labview邪修01:贪吃蛇
  • 数据结构:归并排序 (Iterative Merge Sort)
  • 非支配排序遗传算法进化多目标优化算法
  • 【混合开发】Android+webview模拟crash崩溃补充说明
  • 【LeetCode每日一题】141. 环形链表 142.环形链表 II
  • Rspack
  • Kafka入门指南:从安装到集群部署
  • Mock 在 API 研发中的痛点、价值与进化及Apipost解决方案最佳实践
  • 【Docker/Redis】服务端高并发分布式结构演进之路
  • RS485、RS232、RS422协议
  • 若依微服务一键部署(RuoYi-Cloud):Nacos/Redis/MySQL + Gateway + Robot 接入(踩坑与修复全记录)
  • 云手机的安全性如何?
  • LeetCode Hot 100 第8天
  • 群组分析 (Cohort Analysis)——哪批用户最优质?
  • 【Spring底层分析】Spring AOP补充以及@Transactional注解的底层原理分析
  • 12大主流本地文档管理系统功能与价格对比分析
  • 如何设置阿里云轻量应用服务器镜像?
  • v-model与v-bind区别
  • LG P5386 [Cnoi2019] 数字游戏 Solution
  • CesiumJS 介绍以及基础使用
  • 【完整源码+数据集+部署教程】硬币分类与识别系统源码和数据集:改进yolo11-SWC
  • GoogLeNet:深度学习中的“卷积网络变形金刚“
  • 从“安全诉讼”说起:奖励模型(Reward Model)是LLM对齐的总阀门(全视角分析)
  • 如何在实际应用中选择Blaze或Apache Gluten?
  • 【拍摄学习记录】06-构图、取景
  • 表复制某些字段的操作sql
  • LeetCode - 283. 移动零