在嵌入式Linux开发中,GPIO驱动是最基础也是最重要的外设驱动之一。相比传统的裸机开发方式,Linux内核提供了完整的GPIO子系统框架,配合设备树机制,可以大幅提升驱动的可移植性和可维护性。本文将基于正点原子IMX6ULL开发板,详细介绍如何利用GPIO子系统+misc框架实现按键输入驱动开发。
在裸机开发中,我们通常直接操作寄存器来控制GPIO引脚。这种方式虽然直接高效,但存在几个明显问题:
GPIO子系统通过统一的API接口,屏蔽了底层硬件差异,提供了标准化的GPIO操作方法。开发者不再需要关心具体的寄存器操作,只需调用内核提供的标准API即可完成GPIO配置和操作。
传统的字符设备驱动开发需要手动完成以下步骤:
这些步骤不仅繁琐,而且容易出错。misc(杂项)设备框架对这些操作进行了封装,主设备号固定为10,内核自动分配次设备号,大大简化了简单字符设备的开发流程。
本次开发使用的是正点原子IMX6ULL开发板,具体硬件连接如下:
开发需要以下软件环境:
提示:建议使用与开发板系统完全匹配的内核源码版本,避免因版本差异导致的兼容性问题。
设备树(Device Tree)是Linux 3.x版本引入的硬件描述机制,它将硬件配置信息从内核代码中分离出来,以.dts文件的形式单独描述。这种设计带来了几个显著优势:
我们需要在设备树文件imx6ull-alientek-emmc.dts中添加按键相关配置:
首先在iomuxc节点中添加引脚的复用配置:
dts复制pinctrl_key: keygrp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 0x10B0 /* 按键引脚,上拉模式 */
>;
};
MX6UL_PAD_GPIO1_IO09__GPIO1_IO09:将GPIO1_IO09引脚复用为GPIO功能0x10B0:引脚电气特性配置,这里配置为上拉模式在设备树根节点下添加自定义按键节点:
dts复制putekey {
#address-cells = <1>;
#size-cells = <1>;
compatible = "pute-key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
gpio-key = <&gpio1 9 GPIO_ACTIVE_LOW>;
status = "okay";
};
关键属性说明:
compatible:驱动匹配字符串,驱动中通过该属性识别设备pinctrl-0:引用前面定义的引脚复用配置gpio-key:指定使用的GPIO组和引脚号,以及有效电平status:设置为"okay"表示启用该节点修改完成后,在内核源码目录执行以下命令:
bash复制make dtbs
生成的imx6ull-alientek-emmc.dtb文件需要通过tftp或SD卡等方式更新到开发板,并重启使配置生效。
注意事项:设备树修改后必须确保开发板使用的是新的dtb文件,否则修改不会生效。可以通过查看/proc/device-tree/目录确认节点是否被正确解析。
按键驱动采用misc框架实现,主要包含以下部分:
c复制static struct miscdevice key_misc = {
.minor = MISC_DYNAMIC_MINOR, // 内核自动分配次设备号
.name = "key_misc", // 设备节点名
.fops = &key_fops, // 文件操作集
};
minor:设置为MISC_DYNAMIC_MINOR让内核自动分配次设备号name:设备节点名称,对应/dev/key_miscfops:绑定的文件操作集c复制static struct file_operations key_fops = {
.owner = THIS_MODULE,
.read = key_read,
};
这里我们只实现了read接口,用于读取按键状态。如果需要实现写操作(如控制LED),可以添加.write成员。
驱动入口函数key_drv_init主要完成以下工作:
c复制static int __init key_drv_init(void)
{
int ret;
struct device_node *node;
// 1. 注册misc设备
ret = misc_register(&key_misc);
if (ret) {
pr_err("misc register failed\n");
return ret;
}
// 2. 查找设备树节点
node = of_find_node_by_path("/putekey");
if (!node) {
pr_err("can't find key node\n");
ret = -ENODEV;
goto deregister;
}
// 3. 解析GPIO编号
gpiokeynum = of_get_named_gpio(node, "gpio-key", 0);
if (gpiokeynum < 0) {
pr_err("get gpio number failed\n");
ret = -EINVAL;
goto deregister;
}
// 4. 申请GPIO资源
ret = devm_gpio_request(key_misc.this_device, gpiokeynum, "key_drv");
if (ret) {
pr_err("gpio request failed\n");
goto deregister;
}
// 5. 配置为输入模式
gpio_direction_input(gpiokeynum);
pr_info("key driver init success\n");
return 0;
deregister:
misc_deregister(&key_misc);
return ret;
}
关键点说明:
misc_register简化字符设备注册流程of_find_node_by_path查找设备树节点of_get_named_gpio解析GPIO编号devm_gpio_request自动管理GPIO资源gpio_direction_input配置为输入模式read接口是驱动与应用程序交互的关键:
c复制static ssize_t key_read(struct file *filp, char __user *buf, size_t count, loff_t *ppos)
{
int value;
unsigned long ret;
// 读取GPIO电平
value = gpio_get_value(gpiokeynum);
if (value < 0) {
pr_err("get gpio value failed\n");
return -EINVAL;
}
// 电平转换:低电平(按下)→1,高电平(松开)→0
value = (value == KEY_ON) ? KEY_OFF : KEY_ON;
// 拷贝到用户空间
ret = copy_to_user(buf, &value, sizeof(value));
if (ret) {
pr_err("copy to user failed\n");
return -EFAULT;
}
return sizeof(value);
}
gpio_get_value:通过GPIO子系统API读取引脚电平copy_to_user:内核空间到用户空间的安全拷贝c复制static void __exit key_drv_exit(void)
{
misc_deregister(&key_misc);
pr_info("key driver exit\n");
}
由于使用了devm_系列函数申请资源,这里只需要注销misc设备即可,GPIO资源会自动释放。
c复制module_init(key_drv_init);
module_exit(key_drv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
应用程序通过设备文件与驱动交互,主要功能:
c复制#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#define KEY_ON 1
#define KEY_OFF 0
#define LED_ON 1
#define LED_OFF 0
void delay_ms(int ms) {
usleep(ms * 1000);
}
int main() {
int fd_key, fd_led;
int prev_state = KEY_OFF, curr_state;
int led_state = LED_OFF;
// 打开设备文件
fd_key = open("/dev/key_misc", O_RDWR);
fd_led = open("/dev/led_misc", O_RDWR);
if (fd_key < 0 || fd_led < 0) {
perror("open device failed");
return -1;
}
while (1) {
// 读取按键状态
read(fd_key, &curr_state, sizeof(curr_state));
// 状态变化检测
if (curr_state != prev_state) {
// 按键按下事件
if (curr_state == KEY_ON) {
led_state = !led_state;
write(fd_led, &led_state, sizeof(led_state));
printf("Key pressed, LED %s\n", led_state ? "ON" : "OFF");
}
// 更新状态
prev_state = curr_state;
}
// 10ms延时消抖
delay_ms(10);
}
close(fd_key);
close(fd_led);
return 0;
}
机械按键在按下和松开时会产生触点抖动,通常持续5-10ms。软件消抖的基本原理:
编写Makefile进行驱动编译:
makefile复制KERNELDIR := /path/to/kernel
CURRENT_PATH := $(shell pwd)
CROSS_COMPILE := arm-linux-gnueabihf-
ARCH := arm
obj-m := key_drv.o
build:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) clean
执行make命令生成key_drv.ko驱动模块。
使用交叉编译工具链编译应用程序:
bash复制arm-linux-gnueabihf-gcc key_app.c -o key_app
bash复制insmod key_drv.ko
bash复制ls /dev/key_misc
bash复制./key_app
问题现象:insmod时提示"can't find key node"
可能原因:
解决方案:
ls /proc/device-tree/查看节点是否存在问题现象:insmod时提示"gpio request failed"
可能原因:
解决方案:
bash复制cat /sys/kernel/debug/gpio
问题现象:按键状态读取不正确
可能原因:
解决方案:
bash复制echo 9 > /sys/class/gpio/export
echo in > /sys/class/gpio/gpio9/direction
cat /sys/class/gpio/gpio9/value
当前驱动采用轮询方式读取按键状态,效率较低。可以通过GPIO中断实现事件驱动:
dts复制interrupts-extended = <&gpio1 9 IRQ_TYPE_EDGE_BOTH>;
c复制devm_request_irq(dev, irq, key_irq_handler, IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING, "key_irq", NULL);
可以通过设备树描述多个按键,驱动中使用platform_device机制实现多设备支持:
除了设备文件接口,还可以通过sysfs提供按键状态信息:
c复制static DEVICE_ATTR(status, S_IRUGO, key_status_show, NULL);
c复制static ssize_t key_status_show(struct device *dev, struct device_attribute *attr, char *buf)
{
int value = gpio_get_value(gpiokeynum);
return sprintf(buf, "%d\n", value);
}
在实际项目开发中,这种基于标准框架的驱动开发方式可以大大提高代码的可维护性和可移植性。当硬件平台变更时,通常只需要调整设备树配置,而无需修改驱动代码。