目录
- 今日学习目标
- 第一步:定位飞凌OK3399-C板级DTS文件
- 原厂按键节点深度解析
- GPIO中断设备树语法详解
- RK3399中断架构与通用规则
- 驱动层中断获取API
- 完整实战代码:设备树+平台驱动+按键中断
- 编译与运行步骤
- 常见问题排查
- 今日核心总结
1. 今日学习目标
- 学会定位飞凌OK3399-C的板级设备树文件
- 看懂原厂按键节点的配置,区分ADC按键与GPIO中断按键
- 掌握GPIO中断的设备树标准写法与等价转换
- 理解RK3399的二级中断架构
- 编写完整的按键中断驱动,包含顶半部+底半部(工作队列消抖)
- 掌握驱动开发中资源管理与错误处理的最佳实践
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_B5GPIO_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中断配置的通用规则:
-
PMIC(RK808)中断:
dtsrk808: pmic@1b { interrupt-parent = <&gpio1>; interrupts = <21 IRQ_TYPE_LEVEL_LOW>; }; -
触摸屏中断:
dtspolytouch: 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-parent和interrupts属性 - 参数
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:驱动加载成功但按键没反应
- 检查设备树节点是否存在:
ls /proc/device-tree/my-gpio-key/ - 检查中断是否注册成功:
cat /proc/interrupts | grep ok3399-gpio-key - 按下按键看中断计数是否增加,不增加则检查引脚配置和硬件连接
问题2:中断一直触发
- 检查触发方式是否正确:低电平有效按键用
IRQ_TYPE_EDGE_FALLING - 检查pinctrl是否开启了上拉电阻
- 增加消抖时间到20ms
问题3:驱动加载失败
- 确保内核版本与开发板运行版本一致
- 检查设备树
compatible属性与驱动是否完全一致 - 查看dmesg输出的详细错误信息
10. 今日核心总结
- 板级DTS定位 :通过
cat /proc/device-tree/compatible命令可100%准确找到对应的DTS文件 - 按键分类:OK3399-C板载按键分为ADC模拟按键和GPIO中断按键,后者是嵌入式开发主流方案
- 中断语法 :GPIO简写配置与标准中断配置可相互转换,核心是
interrupt-parent和interrupts两个属性 - 中断架构:RK3399采用GICv3+GPIO组的二级中断级联架构,所有GPIO中断都遵循统一的配置规则
- 驱动最佳实践 :
- 使用
devm_*系列API自动管理资源,避免内存泄漏 - 严格遵守顶半部/底部分工:顶半部只调度底半部,底半部处理耗时操作
- 使用工作队列实现按键消抖,绝对不要在中断上下文调用
msleep - 所有API调用都必须检查返回值,做好错误处理
- 使用