【Linux驱动开发】第16天:按键中断完整实战

目录

  1. 今日学习目标
  2. 第一步:定位飞凌OK3399-C板级DTS文件
  3. 原厂按键节点深度解析
  4. GPIO中断设备树语法详解
  5. RK3399中断架构与通用规则
  6. 驱动层中断获取API
  7. 完整实战代码:设备树+平台驱动+按键中断
  8. 编译与运行步骤
  9. 常见问题排查
  10. 今日核心总结

1. 今日学习目标

  1. 学会定位飞凌OK3399-C的板级设备树文件
  2. 看懂原厂按键节点的配置,区分ADC按键与GPIO中断按键
  3. 掌握GPIO中断的设备树标准写法与等价转换
  4. 理解RK3399的二级中断架构
  5. 编写完整的按键中断驱动,包含顶半部+底半部(工作队列消抖)
  6. 掌握驱动开发中资源管理与错误处理的最佳实践

2. 第一步:定位飞凌OK3399-C板级DTS文件

解析任何板级设备树的第一步,都是先找到对应的DTS文件。最准确的方法是通过运行中的系统查询:

bash 复制代码
# 读取设备树兼容属性,输出的第一个字符串就是DTS文件名前缀
cat /proc/device-tree/compatible | tr '\0' '\n'

输出结果

复制代码
rockchip,linux
rockchip,rk3399
forlinx,OK3399-C

由此确定,飞凌OK3399-C对应的板级设备树文件是:

复制代码
arch/arm64/boot/dts/rockchip/rk3399-ok3399-c.dts

3. 原厂按键节点深度解析

rk3399-ok3399-c.dts中搜索rockchip-key,找到原厂按键节点:

dts 复制代码
rk_key: rockchip-key {
    compatible = "rockchip,key";
    status = "okay";

    io-channels = <&saradc 1>;

    // 音量+键(ADC模拟按键)
    vol-up-key {
        linux,code = <115>;
        label = "volume up";
        rockchip,adc_value = <1>;
    };

    // 音量-键(ADC模拟按键)
    vol-down-key {
        linux,code = <114>;
        label = "volume down";
        rockchip,adc_value = <170>;
    };

    // 电源键(GPIO中断按键,核心学习对象)
    power-key {
        gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
        linux,code = <116>;
        label = "power";
        gpio-key,wakeup;
    };
};

3.1 按键分类与原理

OK3399-C的板载按键分为两类,工作原理完全不同:

按键类型 代表按键 工作原理 中断来源
ADC模拟按键 音量+、音量- 通过SARADC采集电压值区分按键 SARADC控制器中断
GPIO中断按键 电源键 GPIO引脚电平变化触发中断 GPIO组中断控制器

3.2 电源键GPIO属性拆解

电源键是本次学习的核心,其GPIO配置语句:

dts 复制代码
gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
  • &gpio0:指定所属GPIO组,同时也是中断父控制器
  • 5:GPIO组内引脚号,对应硬件引脚GPIO0_B5
  • GPIO_ACTIVE_LOW:低电平有效,按键按下时引脚拉低

4. GPIO中断设备树语法详解

4.1 简写形式与标准中断写法等价转换

原厂使用的gpios简写形式,本质上是标准中断写法的封装。内核rockchip,key驱动会自动调用gpio_to_irq()将GPIO转换为中断号。

等价标准中断写法(自定义驱动必须掌握):

dts 复制代码
interrupt-parent = <&gpio0>;  // 中断父节点:GPIO0中断控制器
interrupts = <5 IRQ_TYPE_EDGE_FALLING>;  // 引脚5,下降沿触发

4.2 核心属性说明

  • interrupt-parent:指定中断信号连接到哪个中断控制器
  • interrupts:中断配置,格式由父中断控制器的#interrupt-cells决定
  • RK3399的GPIO中断控制器#interrupt-cells = <2>,因此interrupts包含两个参数:引脚号 + 触发方式

4.3 常用中断触发方式

宏定义 适用场景
IRQ_TYPE_EDGE_FALLING 2 低电平有效按键(上拉电阻)
IRQ_TYPE_EDGE_RISING 1 高电平有效按键(下拉电阻)
IRQ_TYPE_EDGE_BOTH 3 需要同时检测按下和松开
IRQ_TYPE_LEVEL_HIGH 4 高电平触发
IRQ_TYPE_LEVEL_LOW 8 低电平触发

5. RK3399中断架构与通用规则

5.1 二级中断级联架构

RK3399采用标准的ARM二级中断架构:

复制代码
硬件外设 → GPIO组中断控制器(gpio0~gpio4) → GICv3全局中断控制器 → CPU
  • 第一级:GICv3通用中断控制器,是整个系统的总中断控制器
  • 第二级:每个GPIO组都有自己的中断控制器,负责将GPIO电平变化转换为中断信号

5.2 板内其他中断节点参考

结合DTS中其他外设的中断配置,验证RK3399中断配置的通用规则:

  1. PMIC(RK808)中断

    dts 复制代码
    rk808: pmic@1b {
        interrupt-parent = <&gpio1>;
        interrupts = <21 IRQ_TYPE_LEVEL_LOW>;
    };
  2. 触摸屏中断

    dts 复制代码
    polytouch: edt-ft5x06@38{
        interrupt-parent = <&gpio1>;
        interrupts = <RK_PC6 IRQ_TYPE_EDGE_FALLING>;
    };

通用规则 :所有GPIO类外设的中断,都必须通过interrupt-parent绑定对应GPIO组,interrupts严格遵循"引脚号+触发方式"的双参数格式。


6. 驱动层中断获取API

Linux内核提供了三种主流的中断号获取方式,适配不同开发场景:

6.1 平台设备通用API(推荐)

c 复制代码
int platform_get_irq(struct platform_device *pdev, unsigned int num);
  • 最简洁、最通用的方式,适配绝大多数外设驱动
  • 内部自动解析设备树的interrupt-parentinterrupts属性
  • 参数num是中断索引,从0开始

6.2 底层OF中断API

c 复制代码
int of_irq_get(struct device_node *node, int index);
  • 直接解析设备树节点的中断属性
  • 适合需要手动解析设备树的场景

6.3 GPIO转中断API

c 复制代码
int gpio_to_irq(unsigned int gpio);
  • 将GPIO编号转换为对应的中断号
  • 是原厂rockchip,key驱动的底层实现方式

7. 完整实战代码:设备树+平台驱动+按键中断

7.1 设备树节点(添加到rk3399-ok3399-c.dts根节点下)

dts 复制代码
/ {
    // 自定义按键中断节点
    my_gpio_key: my-gpio-key@0 {
        compatible = "forlinx,ok3399-gpio-key";
        status = "okay";

        // GPIO引脚定义(复用原生电源键GPIO0_B5)
        key-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;

        // 标准中断配置
        interrupt-parent = <&gpio0>;
        interrupts = <5 IRQ_TYPE_EDGE_FALLING>;
    };
};

// pinctrl引脚配置
&pinctrl {
    my_key_pins: my-key-pins {
        rockchip,pins = 
            <0 RK_PB5 RK_FUNC_GPIO &pcfg_pull_up>;
    };
};

7.2 完整驱动代码(ok3399_gpio_key.c

c 复制代码
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/interrupt.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/workqueue.h>
#include <linux/delay.h>

// 设备私有数据结构体:保存所有资源,避免全局变量
struct ok3399_key_dev {
    int gpio;           // GPIO引脚号
    int irq;            // 中断号
    struct work_struct work;  // 底半部工作队列(用于按键消抖)
    struct device *dev; // 设备指针,用于打印日志
};

// ====================== 底半部:工作队列处理函数 ======================
// 运行在内核线程上下文,可以安全睡眠
static void ok3399_key_work_handler(struct work_struct *work)
{
    // 从work指针获取设备私有数据
    struct ok3399_key_dev *key_dev = container_of(work, struct ok3399_key_dev, work);
    int val;

    // 1. 按键消抖:睡眠10ms(必须用工作队列,tasklet不能睡眠)
    msleep(10);

    // 2. 再次读取GPIO状态,确认按键事件
    val = gpio_get_value(key_dev->gpio);

    // 3. 处理按键事件
    if (val == 0) {
        dev_info(key_dev->dev, "=== 按键按下 ===\n");
        // 这里添加你的业务逻辑:上报输入事件、控制LED等
    } else {
        dev_info(key_dev->dev, "=== 按键松开 ===\n");
    }
}

// ====================== 顶半部:中断处理函数 ======================
// 运行在中断上下文,绝对不能睡眠
static irqreturn_t ok3399_key_irq_handler(int irq, void *dev_id)
{
    struct ok3399_key_dev *key_dev = dev_id;

    dev_dbg(key_dev->dev, "顶半部:中断触发,调度底半部\n");

    // 调度工作队列底半部
    schedule_work(&key_dev->work);

    return IRQ_HANDLED;
}

// ====================== 驱动probe函数 ======================
// 设备树匹配成功后自动执行
static int ok3399_key_probe(struct platform_device *pdev)
{
    struct ok3399_key_dev *key_dev;
    int ret;

    dev_info(&pdev->dev, "按键驱动probe开始\n");

    // 1. 分配设备私有数据(devm自动管理内存,无需手动释放)
    key_dev = devm_kzalloc(&pdev->dev, sizeof(*key_dev), GFP_KERNEL);
    if (!key_dev) {
        dev_err(&pdev->dev, "分配设备私有数据失败\n");
        return -ENOMEM;
    }

    key_dev->dev = &pdev->dev;
    platform_set_drvdata(pdev, key_dev);

    // 2. 从设备树获取GPIO引脚号
    key_dev->gpio = of_get_named_gpio(pdev->dev.of_node, "key-gpios", 0);
    if (key_dev->gpio < 0) {
        dev_err(&pdev->dev, "获取GPIO失败,ret=%d\n", key_dev->gpio);
        return key_dev->gpio;
    }
    dev_info(&pdev->dev, "获取到GPIO号:%d\n", key_dev->gpio);

    // 3. 申请GPIO资源
    ret = devm_gpio_request(&pdev->dev, key_dev->gpio, "ok3399-gpio-key");
    if (ret) {
        dev_err(&pdev->dev, "申请GPIO失败,ret=%d\n", ret);
        return ret;
    }

    // 4. 设置GPIO为输入模式
    ret = gpio_direction_input(key_dev->gpio);
    if (ret) {
        dev_err(&pdev->dev, "设置GPIO为输入失败,ret=%d\n", ret);
        return ret;
    }

    // 5. 从设备树获取中断号(核心API)
    key_dev->irq = platform_get_irq(pdev, 0);
    if (key_dev->irq < 0) {
        dev_err(&pdev->dev, "获取中断号失败,ret=%d\n", key_dev->irq);
        return key_dev->irq;
    }
    dev_info(&pdev->dev, "获取到中断号:%d\n", key_dev->irq);

    // 6. 初始化工作队列
    INIT_WORK(&key_dev->work, ok3399_key_work_handler);

    // 7. 注册中断
    ret = devm_request_irq(&pdev->dev, key_dev->irq, ok3399_key_irq_handler,
                          IRQF_TRIGGER_FALLING, "ok3399-gpio-key", key_dev);
    if (ret) {
        dev_err(&pdev->dev, "注册中断失败,ret=%d\n", ret);
        return ret;
    }

    dev_info(&pdev->dev, "按键驱动加载成功!\n");
    return 0;
}

// ====================== 驱动remove函数 ======================
static int ok3399_key_remove(struct platform_device *pdev)
{
    struct ok3399_key_dev *key_dev = platform_get_drvdata(pdev);

    // 取消待执行的工作,等待正在执行的工作完成
    cancel_work_sync(&key_dev->work);

    dev_info(&pdev->dev, "按键驱动卸载成功\n");
    return 0;
}

// ====================== 设备树匹配表 ======================
static const struct of_device_id ok3399_key_of_match[] = {
    { .compatible = "forlinx,ok3399-gpio-key" },
    { /* 必须空结尾 */ }
};
MODULE_DEVICE_TABLE(of, ok3399_key_of_match);

// ====================== platform驱动结构体 ======================
static struct platform_driver ok3399_key_driver = {
    .probe = ok3399_key_probe,
    .remove = ok3399_key_remove,
    .driver = {
        .name = "ok3399-gpio-key",
        .of_match_table = ok3399_key_of_match,
    },
};

module_platform_driver(ok3399_key_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux Driver");
MODULE_DESCRIPTION("OK3399-C GPIO按键中断驱动(Linux 5.15)");
MODULE_VERSION("1.0");

7.3 Makefile

makefile 复制代码
obj-m += ok3399_gpio_key.o

# 替换为你的RK3399 Linux 5.15内核源码路径
KERNELDIR := /home/yourname/rk3399-linux-5.15
PWD := $(shell pwd)
ARCH := arm64
CROSS_COMPILE := aarch64-linux-gnu-

all:
	make ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNELDIR) M=$(PWD) modules

clean:
	make ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNELDIR) M=$(PWD) clean

8. 编译与运行步骤

8.1 编译设备树

bash 复制代码
# 进入内核源码根目录
cd /home/yourname/rk3399-linux-5.15

# 编译DTB
make ARCH=arm64 rk3399-ok3399-c.dtb

8.2 编译驱动

bash 复制代码
# 进入驱动代码目录
cd /home/yourname/key-driver

# 编译驱动
make

8.3 烧录DTB并重启开发板

将生成的rk3399-ok3399-c.dtb烧录到开发板的dtb分区,然后重启。

8.4 加载并测试驱动

bash 复制代码
# 加载驱动
insmod ok3399_gpio_key.ko

# 查看内核输出
dmesg | tail

# 按下开发板电源键,观察输出
# 卸载驱动
rmmod ok3399_gpio_key

预期输出

复制代码
[12345.678901] ok3399-gpio-key my-gpio-key@0: 按键驱动probe开始
[12345.678912] ok3399-gpio-key my-gpio-key@0: 获取到GPIO号:5
[12345.678923] ok3399-gpio-key my-gpio-key@0: 获取到中断号:123
[12345.678934] ok3399-gpio-key my-gpio-key@0: 按键驱动加载成功!
[12346.123456] ok3399-gpio-key my-gpio-key@0: === 按键按下 ===
[12346.567890] ok3399-gpio-key my-gpio-key@0: === 按键松开 ===

9. 常见问题排查

问题1:驱动加载成功但按键没反应

  1. 检查设备树节点是否存在:ls /proc/device-tree/my-gpio-key/
  2. 检查中断是否注册成功:cat /proc/interrupts | grep ok3399-gpio-key
  3. 按下按键看中断计数是否增加,不增加则检查引脚配置和硬件连接

问题2:中断一直触发

  1. 检查触发方式是否正确:低电平有效按键用IRQ_TYPE_EDGE_FALLING
  2. 检查pinctrl是否开启了上拉电阻
  3. 增加消抖时间到20ms

问题3:驱动加载失败

  1. 确保内核版本与开发板运行版本一致
  2. 检查设备树compatible属性与驱动是否完全一致
  3. 查看dmesg输出的详细错误信息

10. 今日核心总结

  1. 板级DTS定位 :通过cat /proc/device-tree/compatible命令可100%准确找到对应的DTS文件
  2. 按键分类:OK3399-C板载按键分为ADC模拟按键和GPIO中断按键,后者是嵌入式开发主流方案
  3. 中断语法 :GPIO简写配置与标准中断配置可相互转换,核心是interrupt-parentinterrupts两个属性
  4. 中断架构:RK3399采用GICv3+GPIO组的二级中断级联架构,所有GPIO中断都遵循统一的配置规则
  5. 驱动最佳实践
    • 使用devm_*系列API自动管理资源,避免内存泄漏
    • 严格遵守顶半部/底部分工:顶半部只调度底半部,底半部处理耗时操作
    • 使用工作队列实现按键消抖,绝对不要在中断上下文调用msleep
    • 所有API调用都必须检查返回值,做好错误处理
相关推荐
大树8810 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠10 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质11 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush411 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52011 小时前
Linux 11 动态监控指令top
linux
Inhand陈工12 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智12 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
不会C语言的男孩12 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_12 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化
古城小栈12 小时前
Unix 与 Linux 异同小叙
linux·服务器·unix