【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调用都必须检查返回值,做好错误处理
相关推荐
weixin_548444261 小时前
爆红处理APK 自动化编译流水线 v2026(英文名:APK AutoPipeline)
运维·自动化
難釋懷2 小时前
Nginx-UrlRewrite
运维·nginx
qq_白羊座2 小时前
CI/CD 与 DevOps 四
运维·ci/cd·devops
杨云龙UP2 小时前
Oracle Recycle Bin 回收站详解:DROP TABLE 后还能找回吗?
linux·运维·数据库·sql·mysql·oracle
GISer_Jing2 小时前
AI数字营销全链路自动化闭环_CSDN
运维·人工智能·自动化
蠢货爱好者3 小时前
Docker基础操作
运维·docker·容器
Drache_long3 小时前
DevOps
运维·devops
不像程序员的程序媛4 小时前
nginx日志配置
运维·nginx
hopsky4 小时前
phoenix docker 启动
运维·docker·容器