第 11 章 Pinctrl 子系统和 GPIO 子系统
直接操作寄存器的驱动方式兼容性差(芯片寄存器改动需重写驱动),而设备树可抽离外设信息(如寄存器地址),驱动通过接口灵活获取,提升适配性。
通过设备树,换设备了不用换驱动,直接动态读取设备信息。
更通用的方案是采用内核驱动框架 ------ 内核抽离同类硬件驱动的共性部分实现,仅留差异化接口给开发者,实现资源统一管理与系统稳定。
本章介绍内核 pinctrl 子系统、GPIO 子系统基本概念、数据结构及 Rockchip pinctrl 控制器。
11.1 Pinctrl 子系统
多数 SoC 内置多个 pin 控制器,可通过寄存器配置引脚功能与特性。
Linux 内核 pinctrl 子系统用于统一管理各厂商 pin 脚,
核心作用:
-
系统初始化时枚举并标识所有可控 pin;
-
配置引脚功能复用(如 GPIO、SPI);
-
设定引脚特性(上下拉、驱动强度、去抖等)。
下文将描述其结构。
其他驱动通过 pinctrl子系统抽象层,
调用soc提供的pin控制器驱动,
得到Pin复用、电气特性配置等能力
pinctrl 核心层是内核抽象层,向下为各 SoC 的 pin 控制器驱动提供通信接口,向上为其他驱动(含 GPIO 子系统)提供 pin 复用、电气特性配置等控制能力。
pin 控制器驱动层提供具体 pin 操作方法。其源码位于内核 /drivers/pinctrl 目录,包含核心文件、接口头文件、底层驱动接口及各厂商驱动文件。
11.1.1 重要的概念
pinctrl 核心层通过 struct pinctrl_desc,让厂商抽象 pin 控制器实例,
借助其中 pins 和 npins 描述系统所有引脚并建立索引,对应需实现 pin 控制器驱动。
在驱动代码或应用代码使用 pinctrl子系统对引脚进行抽象时,
需要先在设备树中引用 &pinctrl节点进行硬件配置,设置引脚功能和复用。
cpp
/* Pinctrl驱动通过抽象pin groups和function管理引脚,以下为rk3568-lubancat2.dts示例 */
&pinctrl { // 引用pin控制器节点,统一管理所有引脚配置
pmic { // function:PMIC相关功能组
// pin group:PMIC中断引脚配置
pmic_int: pmic_int {
// 0号控制器,RK_PA3引脚,复用为GPIO,上拉配置
rockchip,pins = <0 RK_PA3 RK_FUNC_GPIO &pcfg_pull_up>;
};
// pin group:休眠引脚GPIO功能配置(下拉输出低)
soc_slppin_gpio: soc_slppin_gpio {
rockchip,pins = <0 RK_PA2 RK_FUNC_GPIO &pcfg_output_low_pull_down>;
};
// pin group:休眠引脚功能1配置(上拉)
soc_slppin_slp: soc_slppin_slp {
rockchip,pins = <0 RK_PA2 RK_FUNC_1 &pcfg_pull_up>;
};
// pin group:休眠引脚功能2配置(无上下拉)
soc_slppin_rst: soc_slppin_rst {
rockchip,pins = <0 RK_PA2 RK_FUNC_2 &pcfg_pull_none>;
};
// pin group:扬声器控制GPIO(上拉)
spk_ctl_gpio: spk_ctl_gpio {
rockchip,pins = <3 RK_PC5 RK_FUNC_GPIO &pcfg_pull_up>;
};
};
/* ........ */
spi { // function:SPI相关功能组
// pin group:SPI3片选0(上拉,驱动等级1)
spi3_cs0: spi3-cs0 {
rockchip,pins = <4 RK_PC6 RK_FUNC_GPIO &pcfg_pull_up_drv_level_1>;
/*这里示例的是单个引脚的引脚组,如果引脚组含多个引脚,
则 <>,<>*/
};
// pin group:SPI3片选1(上拉,驱动等级1)
spi3_cs1: spi3-cs1 {
rockchip,pins = <4 RK_PC4 RK_FUNC_GPIO &pcfg_pull_up_drv_level_1>;
};
};
/* ........ */
};
上述代码是 设备树源文件(.dts) 的片段,属于 Rockchip 平台(rk3568 芯片)Lubancat2 开发板的引脚配置部分,用于描述 pinctrl 子系统的引脚分组、功能复用及电气特性配置。
在属性处, rockchip,pins 这种格式。
rockchip 是厂商前缀,pins 是属性核心名称,
引脚属性配置格式为 <控制器编号 引脚标识 功能复用 电气配置>
Pin groups:
即 DTS 中如spi3_cs0: spi3-cs0的引脚组,对应一组功能引脚(如 SPI 的片选、PMIC 的控制引脚),含引脚电气特性配置(上下拉、驱动能力等),内核用struct group_desc描述。
Function:
如 DTS 中spi,指引脚的具体功能,同一引脚仅能启用一个功能(避免冲突,如 GPIO 或 SPI_CS 二选一)。
Pin state:
设备某工作状态下唯一的引脚与功能组合,DTS 中通过pinctrl-names(状态名)和pinctrl-x(引脚配置)描述,其他驱动可直接调用对应状态。
11.1.2 主要的数据结构和接口

pin控制器描述符
从面向对象角度,内核将 pinctrl 驱动抽象为 pinctrl_desc对象,厂商 pinctrl 驱动是其实例,实例化引脚信息与控制接口后注册到内核。
cpp
/*pinctrl_desc 描述符*/
struct pinctrl_desc {
const char *name;
const struct pinctrl_pin_desc *pins; // 描述 pin 控制器的所有引脚
unsigned int npins; // 该 pin 控制器的引脚总数
const struct pinctrl_ops *pctlops; // 引脚全局控制操作函数(如描述引脚、获取引脚等)
const struct pinmux_ops *pmxops; // 引脚复用相关操作函数
const struct pinconf_ops *confops; // 引脚配置相关操作函数
struct module *owner;
#ifdef CONFIG_GENERIC_PINCONF
unsigned int num_custom_params;
const struct pinconf_generic_params *custom_params;
const struct pin_config_item *custom_conf_items;
#endif
};
pin控制器注册函数和pin控制器设备
一般控制器驱动匹配设备,调用 probe,最后会调用 pinctrl_register 函数,向内核注册 pinctrl,产生 pinctrl_dev,该函数如下:
cpp
struct pinctrl_dev *pinctrl_register(
struct pinctrl_desc *pctldesc, // 引脚控制器描述符,包含控制器相关信息
struct device *dev, // 关联的设备结构体
void *driver_data // 驱动私有数据,供驱动内部使用
);
单个引脚的描述结构体
描述一个引脚的结构体 struct pinctrl_pin_desc:
cpp
struct pinctrl_pin_desc {
unsigned number; // 引脚编号,标识具体引脚
const char *name; // 引脚名称,用于标识和描述引脚
void *drv_data; // 驱动私有数据,供驱动使用的自定义数据
};
pin组合结构体
很多 pin 组合在一起,实现特定功能,使用 struct group_desc:
cpp
struct group_desc {
const char *name; // 引脚组名称,用于标识一组相关引脚
int *pins; // 引脚编号数组,存储该组包含的引脚编号
int num_pins; // 引脚数量,即pins数组的元素个数
void *data; // 私有数据,可存放该引脚组的额外信息
};
完整的使用步骤
pinctrl_pin_desc 数组 定义控制器管理的所有引脚,
group_desc 将 pinctrl_pin_desc打包成组,
实现 pinctrl_ops 的操作函数 ,
完善 pinctrl_desc 结构体 ,
pinctrl_register 在 probe函数中注册驱动关联到的 platform_device 设备和 pinctrl_desc结构体
定义单个引脚描述符(pinctrl_pin_desc)
1、先通过 pinctrl_pin_desc 数组定义控制器管理的所有引脚:
cpp
// 定义引脚:编号、名称(需与硬件手册对应)
static const struct pinctrl_pin_desc my_pins[] = {
PINCTRL_PIN(10, "pin10"), // 引脚10
PINCTRL_PIN(11, "pin11"), // 引脚11
PINCTRL_PIN(12, "pin12"), // 引脚12(可作为I2C_SDA)
PINCTRL_PIN(13, "pin13"), // 引脚13(可作为I2C_SCL)
};
2、定义引脚组(group_desc)
将功能相关的引脚打包成组(例如 I2C 功能需要 pin12 和 pin13):
cpp
// 定义I2C功能的引脚组(包含pin12和pin13)
static int i2c_pins[] = {12, 13}; // 引脚编号数组
static struct group_desc i2c_group = {
.name = "i2c_pins", // 组名称(用于引用)
.pins = i2c_pins, // 该组包含的引脚
.num_pins = ARRAY_SIZE(i2c_pins), // 引脚数量(2个)
.data = NULL, // 私有数据(可选)
};
// 所有引脚组的集合(供控制器管理)
static struct group_desc *my_groups[] = {
&i2c_group,
// 可添加其他组(如UART、SPI等)
};
3、实现引脚控制器操作函数(pinctrl_ops)
这些函数是硬件控制的核心,需根据芯片手册实现寄存器操作:
cpp
// 1. 配置引脚组的复用功能(例如将i2c_group配置为I2C功能)
static int my_set_mux(struct pinctrl_dev *pctldev,
struct group_desc *group,
unsigned function) {
if (group == &i2c_group && function == 0) { // 假设function=0表示I2C功能
// 硬件操作:写入寄存器,将pin12和pin13配置为I2C模式
writel(0x1, I2C_MODE_REG); // 示例:配置复用寄存器
return 0;
}
return -EINVAL;
}
// 2. 配置引脚属性(如上拉、驱动强度等)
static int my_set_config(struct pinctrl_dev *pctldev,
unsigned pin,
unsigned long config) {
if (pin == 12 || pin == 13) { // 针对I2C引脚配置上拉
if (config == PIN_CONFIG_BIAS_PULLUP) {
// 硬件操作:使能pin12/pin13的上拉电阻
writel(0x1, PULLUP_REG(pin)); // 示例:配置上拉寄存器
return 0;
}
}
return -EINVAL;
}
// 3. 定义控制器支持的操作集
static const struct pinctrl_ops my_pinctrl_ops = {
.set_mux = my_set_mux, // 复用配置
.set_config = my_set_config, // 属性配置
.get_groups_count = my_get_groups_count, // 返回组数量
.get_group_name = my_get_group_name, // 返回组名称
// 其他必要操作(如获取引脚状态等)
};
4、定义引脚控制器描述符(pinctrl_desc)
cpp
static struct pinctrl_desc my_pinctrl_desc = {
.name = "my-pinctrl", // 控制器名称
.pins = my_pins, // 所有引脚的描述
.npins = ARRAY_SIZE(my_pins), // 引脚总数
.groups = my_groups, // 所有引脚组
.ngroups = ARRAY_SIZE(my_groups), // 组数量
.ops = &my_pinctrl_ops, // 操作函数集
.owner = THIS_MODULE, // 所属模块
};
5、注册引脚控制器(pinctrl_register)
cpp
static struct pinctrl_dev *my_pinctrl_dev;
static int my_pinctrl_probe(struct platform_device *pdev) {
// 注册控制器,返回句柄my_pinctrl_dev
my_pinctrl_dev = pinctrl_register(&my_pinctrl_desc, &pdev->dev, NULL);
if (IS_ERR(my_pinctrl_dev)) {
dev_err(&pdev->dev, "注册失败: %ld\n", PTR_ERR(my_pinctrl_dev));
return PTR_ERR(my_pinctrl_dev);
}
return 0;
}
// 驱动退出时注销
static int my_pinctrl_remove(struct platform_device *pdev) {
pinctrl_unregister(my_pinctrl_dev);
return 0;
}
6、应用层 / 其他驱动调用控制
其他驱动(如 I2C 驱动)可通过设备树引用引脚组,内核会自动调用上述 my_set_mux 等函数完成配置:
cpp
// 设备树中引用I2C引脚组进行硬件配置
i2c@12340000 {
pinctrl-names = "default";
//引用名称为"i2c_pins"的引脚组
pinctrl-0 = <&i2c_pins>; //pinctrl子系统的标准属属性,用于关联当前设备与一个具体的引脚组配置
status = "okay";
};
// 驱动中通过 compatible在probe通过pinctrl注册并关联 pinctrl硬件
/*
//另外设备的 compatible = "vendor,chip-i2c"; 标识
//驱动通过
static const struct of_device_id my_i2c_of_match[] = {
{ .compatible = "vendor,chip-i2c" }, // 与设备树节点的compatible值一致
{ 哨兵 }
}; 匹配设备
*/
11.2 GPIO 子系统
pinctrl 子系统 将引脚初始化为普通 GPIO 后,可通过 GPIO 子系统 接口操作电平、中断等。
设备树中定义引脚配置:
cpp
//这个设备树节点不需要给地址因为它不是一个独立的硬件设备,
//而是引脚配置的描述性节点,用于告诉 pinctrl 子系统如何配置引脚
// 引脚配置子节点:定义LED所使用引脚的功能配置
led_gpio: led-gpio { // 节点标签(供其他节点引用,如通过&led_gpio)
pins = "gpio5"; // 指定需要配置的引脚(对应pinctrl_pin_desc中的name)
function = "gpio"; // 配置引脚功能为普通GPIO(由pinctrl子系统执行初始化)
};
led@0 { // LED设备节点
compatible = "gpio-leds"; // 匹配GPIO LED驱动
gpios = <&gpio_controller 5 GPIO_ACTIVE_HIGH>; // 关联GPIO控制器的5号引脚,高电平有效
};
驱动中操作:
cpp
// 解析"gpios"属性,获取GPIO编号
int gpio = of_get_named_gpio(pdev->dev.of_node, //当前设备在设备树中的节点指针
"gpios", //设备树属性名
0); //属性中的索引,0是第一个
// 向内核申请使用指定的引脚,确保引脚未被其他驱动占用
gpio_request(gpio, //要申请的GPIO全局编号
"led_gpio"); //给该 GPIO 分配的名称(用于调试,如通过 /sys/class/gpio 查看占用情况)。
// 设置为输出并点亮LED(高电平)
gpio_direction_output(gpio, 1);
// 熄灭LED(低电平)
gpio_set_value(gpio, 0);
/*
这里用到了gpio子系统提供的API
*/
无论底层硬件(如不同的 GPIO 控制器、不同引脚编号)如何,内核都会为每个可操作的 GPIO 引脚分配一个唯一的全局编号。
开发者在设备树添加 gpio 信息,即可用其 API 操作,方便开发,
相关代码位于内核 /drivers/gpio/ 目录。
GPIO子系统简单描述
GPIO 核心是 gpiolib 框架:
向上提供接口供其他驱动调用(如 LED 驱动申请、设置 GPIO),
向下提供 GPIO 资源注册函数(供 SOC 厂商编写的控制器驱动注册引脚数量、操作函数等)。
11.2.1 重要结构体
GPIO 子系统,
主要的数据结构有
gpio_device、
gpio_chip、
gpio_desc 等,
主要是为了描述 gpio 控制器,有引脚信息,中断信息,以及相关操作函数等。
一个 gpio_device 用来表示一个 GPIO 控制器,
GPIO Controller 中每一个引脚用 gpio_desc 表示,
引脚的相关操作函数和中断相关在 gpio_chip 中。
gpio_device
cpp
/*gpio_device(内核源码/drivers/gpio/gpiolib.h)*/
struct gpio_device {
int id; // gpio 控制器的 id,也就是第几个
struct device dev; // 设备结构体
struct cdev chrdev; // 字符设备结构体
struct device *mockdev; // 模拟设备指针
struct module *owner; // 所属模块
struct gpio_chip *chip; // 指向对应的 gpio_chip 结构体
struct gpio_desc *descs; // 指向 gpio 描述符数组
int base; // gpio 在内核中的编号,申请 gpio 口时根据此编号查找
u16 ngpio; // 该 gpio 控制器拥有的引脚数量
const char *label; // 标签
void *data; // 私有数据
struct list_head list; // 用于链表操作的节点
#ifdef CONFIG_PINCTRL
/*
* 若启用 CONFIG_PINCTRL,gpio 控制器可选择性描述其在 SoC 中服务的实际引脚范围。
* 此信息会被 pinctrl 子系统用于配置相应引脚为 gpio 功能。
*/
struct list_head pin_ranges; // 引脚范围链表
#endif
};
gpio_chip
cpp
/*gpio_chip(内核源码/include/linux/gpio/driver.h)*/
struct gpio_chip {
const char *label; // GPIO 端口的名字(标签)
struct device *dev; // 关联的设备结构体
struct module *owner; // 所属模块
/* 操作 gpio 口的方法(函数指针集合) */
// 申请 GPIO 引脚
int (*request)(struct gpio_chip *chip, unsigned offset);
// 释放 GPIO 引脚
void (*free)(struct gpio_chip *chip, unsigned offset);
// 配置 GPIO 为输入方向
int (*direction_input)(struct gpio_chip *chip, unsigned offset);
// 获取 GPIO 输入电平
int (*get)(struct gpio_chip *chip, unsigned offset);
// 配置 GPIO 为输出方向,并设置初始电平
int (*direction_output)(struct gpio_chip *chip, unsigned offset, int value);
// 设置 GPIO 消抖时间
int (*set_debounce)(struct gpio_chip *chip, unsigned offset, unsigned debounce);
// 设置 GPIO 输出电平
void (*set)(struct gpio_chip *chip, unsigned offset, int value);
// 将 GPIO 引脚映射为中断号
int (*to_irq)(struct gpio_chip *chip, unsigned offset);
/*.....*/
int base; // GPIO 在内核中的全局起始编号,申请时用于查找
u16 ngpio; // 该控制器管理的 GPIO 引脚总数
const char *const *names; // 各 GPIO 引脚的名称数组(可选)
unsigned can_sleep; // 标识 GPIO 操作是否可能休眠(如基于 I2C/SPI 的扩展 GPIO)
/*......*/
};
gpio_desc
cpp
/*gpio_desc(内核源码/drivers/gpio/gpiolib.h)*/
struct gpio_desc {
struct gpio_device *gdev; // gpio_device 里面描述了 gpio 信息,详细看下内核源码/drivers/gpio/gpiolib.h
unsigned long flags;
/* flag symbols are bit numbers */
#define FLAG_REQUESTED 0 //
#define FLAG_IS_OUT 1 //
#define FLAG_EXPORT 2 // protected by sysfs_lock
#define FLAG_SYSFS 3 // exported via /sys/class/gpio/, control
#define FLAG_ACTIVE_LOW 6 // value has active low
#define FLAG_OPEN_DRAIN 7 // Gpio is open drain type
#define FLAG_OPEN_SOURCE 8 // Gpio is open source type
#define FLAG_USED_AS_IRQ 9 // GPIO is connected to an IRQ
#define FLAG_IS_HOGGED 11 // GPIO is hogged
#define FLAG_TRANSITORY 12 // GPIO may lose value in sleep or reset
/* Connection label */
const char *label;
/* Name of the GPIO */
const char *name;
};
11.2.2 常用 API 函数讲解
GPIO 子系统有两套接口:
描述符接口:函数以 gpiod_为前缀,部分带 devm_(设备资源管理,自动释放资源)。
旧接口:函数以 gpio_为前缀。
1. 获取 GPIO 编号函数 of_get_named_gpio
GPIO 编号可以通过 of_get_named_gpio 函数从设备树中获取。
cpp
static inline int of_get_named_gpio(struct device_node *np, // 设备节点指针
const char *propname, // 要查找的属性名
int index) // 属性中GPIO的索引
2. GPIO 申请函数 gpio_request
使用gpio前需要向内核申请,避免其他驱动在使用该GPIO
cpp
static inline int gpio_request(unsigned gpio, // 要请求的GPIO编号
const char *label) // 为申请到的GPIO起的别名
3. GPIO 释放函数
释放正在使用的GPIO
cpp
static inline void gpio_free(unsigned gpio);//要释放的GPIO编号
4. GPIO 输出设置函数 gpio_direction_output
将引脚设置为输出模式,
cpp
static inline int gpio_direction_output(unsigned gpio, // 要配置为输出的GPIO编号
int value) // 初始输出值(通常为0或1)
5. GPIO 输入设置函数 gpio_direction_input
将引脚设置为输入模式,
cpp
static inline int gpio_direction_input(unsigned gpio) // 要配置为输入的GPIO编号
6. 获取 GPIO 引脚值函数 gpio_get_value
cpp
static inline int gpio_get_value(unsigned gpio); // 要读取值的GPIO编号
7. 设置 GPIO 输出值 gpio_set_value
该函数只用于那些设置为输出模式的 GPIO。
cpp
static inline int gpio_direction_output(unsigned gpio, // 要设置为输出方向的GPIO编号
int value) // GPIO输出的初始电平值(0或1)
常用的新接口函数
|-----------------------|----------------------------------------------|
| gpiod_get | 设备树中 gpio 属性只有一组值时,获取 GPIO |
| gpiod_get_index | 设备树 gpio 有多组值时,通过 INDEX 获取 GPIO |
| gpiod_get_direction | 获取 gpio 的状态(输入或输出) |
| gpiod_direction_input | 将 GPIO 配置为输入 |
| gpiod_get_value | 获取 GPIO 的逻辑值 |
| gpiod_set_value | 设置 gpio 引脚值 |
| gpiod_put | 释放通过 gpiod_get 和 gpiod_get_index 申请的 gpio 资源 |
11.2.3 GPIO 子系统和 sysfs
sysfs 的 GPIO 接口使用说明:
目录结构
- 路径:/sys/class/gpio
- 主要文件 / 目录:
- export/unexport:用户空间申请 / 释放 GPIO 的接口
- gpiochipX:每个对应一个 GPIO 控制器,包含:
- base:控制器的基引脚编号
- ngpio:控制器的 GPIO 数量
- label:设备树节点标签
基本操作
申请 GPIO
向 export 写入引脚编号,创建对应节点(如引脚 6):
bash
echo 6 | sudo tee export > /dev/null # 生成 /sys/class/gpio/gpio6
操作 GPIO
进入 gpio6 目录,可配置:
direction:设置输入 / 输出(如echo out | sudo tee direction)value:设置输出电平(如echo 1 | sudo tee value输出高电平)active_low/edge:配置极性 / 中断触发方式
释放 GPIO
向 unexport 写入引脚编号,删除节点:
bash
echo 6 | sudo tee unexport > /dev/null # 移除 /sys/class/gpio/gpio6
引脚编号规则

11.3 GPIO 子系统和 Pinctrl 子系统之间的耦合关系
Pinctrl 子系统管理所有引脚,GPIO 是引脚的一种用途。
通常先由 pinctrl 子系统 完**成引脚的复用(指定为 GPIO)**和电气参数配置,
再由 GPIO 子系统对该 GPIO 引脚进行功能控制(如输入输出、中断等)
比如当一个引脚组被 pinctrl 配置为 I2C2 功能时,这些引脚已被复用为外设功能(非 GPIO),因此不可以通过GPIO子系统进行控制。
第 12 章 Pinctrl 子系统和 GPIO 子系统
------LED 实验
12.1 pinctrl 子系统
pinctrl 子系统主要管理芯片引脚,负责引脚复用、上下拉、驱动能力等配置。
以 rk3568 为例,其片上外设(如 I2C、SPI、LCD 等)需通过引脚连接外部设备,但芯片可用引脚有限,因此多数引脚设计为可复用为多个外设功能,以提升硬件设计灵活性。
查阅 <<Rockchip_RK3568_Datasheet_xxx>> 数据手册,如下图所示。
<<Rockchip_RK3568_Datasheet_xxx>> 数据手册
GPIO1_A6 等引脚可复用为 GPIO、I2S、串口等多种功能,硬件设计时需选定功能。
编程时需配置引脚复用及 PAD 属性,但手动设置存在工作量大、移植性差、可重用性低的问题,更易出现引脚重复定义(如 I2C 和 UART 驱动重复使用同一引脚),且错误难排查。
pinctrl 子系统由芯片厂商实现,负责统一管理引脚并自动完成初始化,用户只需在设备树中按格式配置参数即可。
12.1.1 pinctrl 子系统编写格式以及引脚属性详解
12.1.1.1 pinctrl 设备树节点介绍
cpp
/*内核源码/arch/arm64/boot/dts/rockchip/rk3568.dtsi 文件*/
/*pinctrl节点*/
pinctrl: pinctrl {
/*节点配置信息*/
compatible = "rockchip,rk3568-pinctrl"; // 适配RK3568芯片的pinctrl控制器
rockchip,grf = <&grf>; // 全局寄存器文件引用
rockchip,pmu = <&pmugrf>; // PMU寄存器文件引用
#address-cells = <2>; // 地址单元格数量
#size-cells = <2>; // 大小单元格数量
ranges; // 地址映射范围
/*子节点配置,gpio控制器*/
gpio0: gpio@fdd60000 { // GPIO0控制器节点,基地址0xfdd60000
compatible = "rockchip,gpio-bank"; // 适配RK GPIO-bank
reg = <0x0 0xfdd60000 0x0 0x100>; // 寄存器地址及大小
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>; // 中断配置
clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>; // 时钟源
gpio-controller; // 标识为GPIO控制器
#gpio-cells = <2>; // GPIO属性单元格数量(编号+标志)
gpio-ranges = <&pinctrl 0 0 32>; // GPIO与pinctrl引脚映射(32个引脚)
interrupt-controller; // 标识为中断控制器
#interrupt-cells = <2>; // 中断属性单元格数量
};
/* 剩下内容省略 */
};
rk3568.dtsi 是芯片厂商将芯片的通用部分单独提取出来的一些设备树配置,soc节点汇总了所需引脚的配置信息,pinctrl 子系统存储使用着的节点信息。
我们的设备树主配置文件为内核源码里的 rk3568-lubancat2.dts,
在其中搜索 "&pinctrl" 可找到引用该节点的位置。
cpp
/*pinctrl节点配置*/
&pinctrl {
/*pmic功能配置*/
pmic {
pmic_int: pmic_int { // PMIC中断引脚配置:PA3(GPIO功能,上拉)
rockchip,pins = <0 RK_PA3 RK_FUNC_GPIO &pcfg_pull_up>;
};
soc_slppin_gpio: soc_slppin_gpio { // 休眠相关引脚(GPIO功能,下拉输出低)
rockchip,pins = <0 RK_PA2 RK_FUNC_GPIO &pcfg_output_low_pull_down>;
};
soc_slppin_slp: soc_slppin_slp { // 休眠相关引脚(功能1,上拉)
rockchip,pins = <0 RK_PA2 RK_FUNC_1 &pcfg_pull_up>;
};
soc_slppin_rst: soc_slppin_rst { // 休眠相关引脚(功能2,无上下拉)
rockchip,pins = <0 RK_PA2 RK_FUNC_2 &pcfg_pull_none>;
};
spk_ctl_gpio: spk_ctl_gpio { // 扬声器控制引脚:PC5(GPIO功能,上拉)
rockchip,pins = <3 RK_PC5 RK_FUNC_GPIO &pcfg_pull_up>;
};
};
/*耳机功能配置*/
headphone {
hp_det: hp-det { // 耳机检测引脚:PA5(GPIO功能,无上下拉)
rockchip,pins = <0 RK_PA5 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
/**/usb功能配置
usb {
vcc5v0_usb20_host_en: vcc5v0-usb20-host-en { // USB2.0主机使能:PD5(GPIO功能,无上下拉)
rockchip,pins = <0 RK_PD5 RK_FUNC_GPIO &pcfg_pull_none>;
};
vcc5v0_usb30_host_en: vcc5v0-usb30-host-en { // USB3.0主机使能:PD6(GPIO功能,无上下拉)
rockchip,pins = <0 RK_PD6 RK_FUNC_GPIO &pcfg_pull_none>;
};
vcc5v0_otg_vbus_en: vcc5v0-otg-vbus-en { // OTG电源使能:PD3(GPIO功能,无上下拉)
rockchip,pins = <0 RK_PD3 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
/* 剩下内容省略 */
};
通过 "&pinctrl" 在其节点下追加内容,
如 pmic 节点描述电源管理引脚功能,子节点用 "rockchip,pins" 指定引脚复用;
headphone 子节点指定引脚电气特性,其余均为 pinctrl 下子节点,按规范描述外设引脚及复用功能。
下面以 & sdmmc1 外设节点为例,说明何时使用 pinctrl,
cpp
&sdmmc1 {
pinctrl-names = "default", "opendrain", "sleep"; // 引脚控制状态名称
pinctrl-0 = <&sdmmc1_b4_pins_a>; // default状态对应引脚配置
pinctrl-1 = <&sdmmc1_b4_od_pins_a>; // opendrain状态对应引脚配置
pinctrl-2 = <&sdmmc1_b4_sleep_pins_a>; // sleep状态对应引脚配置
broken-cd; // 无卡检测功能
st,neg-edge; // 负极沿触发
bus-width = <4>; // 4位总线宽度
vmmc-supply = <&v3v3>; // 电源由v3v3提供
status = "okay"; // 使能该节点
};
设备树中,pinctrl-0、pinctrl-1、pinctrl-2 是外设引脚控制属性,
与 pinctrl-names 中定义的状态一一对应:
pinctrl-0 对应第一个状态(如示例 "default"),关联默认工作状态的引脚配置;
pinctrl-1 对应第二个状态(如示例 "opendrain"),关联开漏模式的引脚配置;
pinctrl-2 对应第三个状态(如示例 "sleep"),关联休眠状态的引脚配置。
它们用于外设在不同工作状态下切换对应的引脚功能和电气特性配置。
这样我们就指定了这个外设用到的引脚及其状态。
12.1.1.2 pinctrl 子节点编写格式
cpp
&pinctrl {
// 定义名为xxx的引脚控制节点
xxx: xxx {
// 引脚配置子节点
pins {
// 配置引脚:编号0,RK_PA6引脚,功能为GPIO,无上下拉(引用pcfg_pull_none配置)
rockchip,pins = <0 RK_PA6 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
};
外设 xxx 使用 GPIO0_A6 引脚,
配置为 GPIO 功能,无上下拉(引用 & pcfg_pull_none,可参考内核 rockchip-pinconf.dtsi 文件)。
引脚复用功能需查手册,pinctrl 子节点格式为芯片厂商自定义,非设备树规范,新增时可参照示例格式编写。
关于 pinctrl 节点如何去描述,我们可以在内核文档目录中查找芯片产商给出的文档。如 rockchip 官方的 pinctrl 文档目录如下:
Documentation/devicetree/bindings/pinctrl/rockchip,pinctrl.txt 文档
12.1.2 将 RGB 灯引脚添加到 pinctrl 子系统
本小节从原理图入手,将 LED 引脚添加到 pinctrl 子系统,具体引脚需参考实际板卡原理图。
12.1.2.1 查找 LED 灯使用的引脚
以 lubuncat2 为例,系统 LED 灯对应的原理图如图所示
根据网络名在核心板上找到对应的引脚是: GPIO0_C7。
以 lubuncat4 为例,系统 LED 灯对应的原理图如图所示
根据网络名在核心板上找到对应的引脚是: GPIO4_B5。
12.1.2.2 在 pinctrl 节点中添加 pinctrl 子节点
添加子节点只需按格式将引脚信息写入设备树的 pinctrl 子节点,
例如在 lubuncat2 的 rk3568-lubancat2.dts 中添加对应内容。
cpp
/*以 lubuncat2 为例,在 rk3568-lubancat2.dts 添加以下内容*/
&pinctrl {
/* 新增LED测试引脚配置 */
led_test {
// LED测试引脚节点定义
led_test_pin: led_test_pin {
// 配置引脚:编号0,RK_PC7引脚,功能为GPIO,无上下拉
rockchip,pins = <0 RK_PC7 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
};
cpp
/*以 lubuncat4 为例,在 rk3588s-lubancat-4.dts 添加以下内容*/
&pinctrl {
/* 新增LED测试引脚配置 */
led_test {
// LED测试引脚节点
led_test_pin: led_test_pin {
// 引脚配置:编号4,RK_PB5引脚,功能为GPIO,无上下拉
rockchip,pins = <4 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
};
新增节点名 "led_test" 可自定义(同节点下不重复,≤32 字符,宜表意);
"led_test_pin : led_test_pin" 为节点标签 :节点名,供引用;
led_test 是一个容器节点,用于归类相关的引脚配置;
led_test_pin 是实际的引脚控制描述符,包含了具体的引脚配置信息(如 rockchip,pins 属性),并通过标签 led_test_pin: 被其他设备节点引用,实现引脚功能的绑定。
"rockchip,pins" 是固定格式,"厂商,属性名",用于配置 LED 的 GPIO 引脚功能。
"编号" 通常是芯片厂商定义的引脚组(bank)编号,用于区分不同的引脚 bank。
比如
cpp
rockchip,pins = <4 RK_PB5 RK_FUNC_GPIO &pcfg_pull_none>;
// GPIO BANK 4 , PB的第5号引脚, 复用为GPIO ,无上下拉
引脚组以 GPIOx 命名(x 为组编号),
例如 GPIO0、GPIO1、GPIO2 ... 等,每组内的引脚按端口(如 A、B、C 等)和序号(0~7 或更多)细分,最终引脚标识格式为 GPIOx_yn(如 GPIO0_A6、GPIO4_B5)
pinctrl 因芯片厂商而异,详情参考官方文档,配置后系统会将引脚初始化为 GPIO 功能。

12.2 GPIO 子系统
无 GPIO 子系统时,需手动操作 LED 相关配置寄存器来控制 LED;
有了 GPIO 子系统后,其会代做这些工作,只需调用其提供的 API 即可控制 GPIO。
rk3568.dtsi 的 pinctrl 子节点记录 GPIO 控制器寄存器地址,
以下以 GPIO0 为例介绍其相关内容。
cpp
/ {
pinctrl: pinctrl {
compatible = "rockchip,rk3568-pinctrl"; // 绑定RK3568 pinctrl驱动
//这里是写了一个兼容驱动,多个则 "","","" 有无厂商标识视情况而定
rockchip,grf = <&grf>; // 关联全局寄存器文件
rockchip,pmu = <&pmugrf>; // 关联PMU寄存器文件
#address-cells = <2>; // 地址单元格数(2个32位组成64位地址)
#size-cells = <2>; // 大小单元格数(同上)
ranges; // 启用地址映射
// GPIO0控制器节点:基地址0xfdd60000,大小0x100
gpio0: gpio@fdd60000 {
compatible = "rockchip,gpio-bank"; // 绑定GPIO bank驱动
reg = <0x0 0xfdd60000 0x0 0x100>; // 寄存器地址范围(基址+大小)
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>; // 中断配置(SPI中断33,高电平触发)
clocks = <&pmucru PCLK_GPIO0>, <&pmucru DBCLK_GPIO0>; // 关联GPIO0时钟
gpio-controller; // 标识为GPIO控制器
#gpio-cells = <2>; // GPIO单元格数(引脚号+标志)
gpio-ranges = <&pinctrl 0 0 32>; // GPIO引脚映射(32个引脚:0~31)
interrupt-controller; // 标识为中断控制器
#interrupt-cells = <2>; // 中断单元格数(触发方式+标志)
};
/* 剩余内容省略 */
};
};
gpio0 节点描述整个 gpio0,使用 GPIO 子系统时,需在设备树添加设备节点,驱动或应用中调用其 API 即可控制 GPIO。
12.2.1 在设备树中添加 RGB 灯的设备树节点
相比未用 GPIO 子系统的 LED 设备节点,基于 GPIO 子系统的 led_test 节点只需增加 GPIO 属性定义。
以 lubuncat2 为例,在 rk3568-lubancat2.dts 的根节点添加后,设备树如下。
cpp
/* 添加LED测试设备节点 */
led_test: led_test {
status = "okay"; // 启用节点
compatible = "fire,led_test"; // 匹配驱动的兼容标识
default-state = "on"; // 默认状态为点亮
gpios = <&gpio0 RK_PC7 GPIO_ACTIVE_HIGH>; // 关联GPIO0的RK_PC7引脚,高电平有效
pinctrl-names = "default"; // 引脚配置状态名
pinctrl-0 = <&led_test_pin>; // 绑定之前定义的引脚配置节点
};
以 lubuncat4 为例,在 rk3588s-lubancat-4.dts 根节点添加后,设备树如下。
cpp
/* 添加LED测试设备节点 */
led_test: led_test {
status = "okay"; // 启用该节点
compatible = "fire,led_test"; // 用于匹配对应驱动
default-state = "on"; // 默认状态为点亮
gpios = <&gpio4 RK_PB5 GPIO_ACTIVE_HIGH>; // 关联GPIO4的RK_PB5引脚,高电平有效
pinctrl-names = "default"; // 引脚配置状态名称
pinctrl-0 = <&led_test_pin>; // 绑定引脚配置节点,引脚配置节点通过pinctrl决定了引脚的功能复用
};
12.2.2 在设备树中注释 sys 灯的设备树节点
系统 LED 使用内核自带驱动,会与实验冲突,需屏蔽:找到 leds 节点,将 status 设为 disabled 以关闭该节点。
cpp
leds: leds {
/*status = "okay";*/
status = "disabled"; // 禁用leds节点,避免与实验冲突
compatible = "gpio-leds";
sys_status_led: sys-status-led {
label = "sys_status_led";
linux,default-trigger = "heartbeat";
default-state = "on";
gpios = <&gpio0 RK_PC7 GPIO_ACTIVE_LOW>;
pinctrl-names = "default";
pinctrl-0 = <&sys_status_led_pin>;
};
};
也可在对应板卡 dts 的合适位置追加信息,将 leds 节点状态设为 disabled。
cpp
&leds {
status = "disabled"; // 禁用leds节点
};
12.2.3 编译、下载设备树验证修改结果
编译内核时会自动编译设备树,缺点是耗时。
可以不编译内核只编译设备树。
bash
#RK356X系列板卡
# 加载配置
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig
# 单独编译设备树(-j4指定4线程)
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs
defconfig是板卡专属配置文件,执行后生成.config文件。
编译目标 dtbs 生成 dtb文件。
bash
# RK3588系列板卡
# 加载配置
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat_linux_rk3588_defconfig
# 单独编译设备树
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs
编译生成的 dtb 文件位于内核源码 arch/arm64/boot/dts/rockchip/ 目录,
替换开发板 /boot/dtb/ 下对应文件并重启,
正常情况下 /proc/device-tree/ 会生成 led_test 节点。
新添加的 led_test节点
从上图可以看到"/proc/device-tree"目录下有"led_test"和"leds"节点,
并且"led_test"节点的状态为"okay","leds"节点的状态为"disabled"。leds是野火自己加的设备树的节点。led_test是我们新增的。
12.3 实验说明与代码讲解*重要
本节实验使用到 lubancat2 板上的 led 灯。
12.3.1 实验代码讲解
程序包含两个 C 语言文件,
一个是驱动程序,驱动程序在平台总线基础上编写。
另一个是一个简单的测试程序,用于测试驱动是否正常。
12.3.1.1 驱动程序讲解
驱动程序分三部分:
-
编写平台设备驱动入口与出口函数;
-
编写 probe 函数,实现字符设备注册及 LED 灯初始化;
-
编写字符设备函数集,实现 open 与 write 函数。
平台驱动入口和出口函数实现
匹配表,id_tables,入口初始化 init 函数和出口 exit函数。
驱动与设备匹配后触发 probe 函数,实现 RGB 初始化及字符设备注册;后续通过字符设备 open、write 函数控制 RGB 灯。
cpp
/*平台驱动框架*/
// 设备树匹配表:指定兼容属性,用于设备与驱动匹配
static const struct of_device_id led_ids[] = {
{ .compatible = "fire,led_test" },
{ /* 哨兵项,标记数组结束 */ }
};
// 平台驱动结构体:关联probe函数、驱动名及设备树匹配表
struct platform_driver led_platform_driver = {
.probe = led_probe,
.driver = {
.name = "leds-platform",
.owner = THIS_MODULE,
.of_match_table = led_ids, // 绑定设备树匹配表
}
};
// 驱动初始化函数:注册平台驱动
static int __init led_platform_driver_init(void)
{
int DriverState;
DriverState = platform_driver_register(&led_platform_driver);
printk(KERN_EMERG "\tDriverState is %d\n", DriverState);
return 0;
}
// 驱动注销函数:注销平台驱动及字符设备相关资源
static void __exit led_platform_driver_exit(void)
{
printk(KERN_EMERG "led_test exit!\n");
device_destroy(class_led, led_devno); // 销毁设备
class_destroy(class_led); // 销毁设备类
cdev_del(&led_chr_dev); // 删除字符设备
unregister_chrdev_region(led_devno, DEV_CNT); // 注销设备号
platform_driver_unregister(&led_platform_driver); // 注销平台驱动
}
// 模块入口/出口宏:指定初始化和注销函数
module_init(led_platform_driver_init);
module_exit(led_platform_driver_exit);
MODULE_LICENSE("GPL"); // 模块许可证声明
平台驱动.probe 函数实现
驱动与设备匹配后触发 probe 函数,实现 RGB 初始化及字符设备注册;
后续通过字符设备 open、write 函数控制 RGB 灯。
cpp
/* 平台驱动probe函数:设备匹配成功后执行 */
static int led_probe(struct platform_device *pdv)
{
int ret = 0; // 用于保存设备号申请结果
printk("match successed\n");
/* 获取RGB灯的设备树节点 */
led_device_node = of_find_node_by_path("/led_test");
if (led_device_node == NULL) {
printk(KERN_EMERG "get led_test failed! \n");
}
// 从设备树获取GPIO编号
led = of_get_named_gpio(led_device_node, "gpios", 0);
printk("led = %d \n", led);
// 配置GPIO为输出高电平
gpio_direction_output(led, 1);
/* 字符设备注册流程 */
// 1. 动态分配设备号(次设备号0,设备名rgb-leds)
ret = alloc_chrdev_region(&led_devno, 0, DEV_CNT, DEV_NAME);
if (ret < 0) {
printk("fail to alloc led_devno\n");
goto alloc_err; // 分配失败跳转错误处理
}
// 2. 关联字符设备与文件操作集
led_chr_dev.owner = THIS_MODULE;
cdev_init(&led_chr_dev, &led_chr_dev_fops);
// 3. 将字符设备添加到系统
ret = cdev_add(&led_chr_dev, led_devno, DEV_CNT);
if (ret < 0) {
printk("fail to add cdev\n");
goto add_err; // 添加失败跳转错误处理
}
// 4. 创建设备类和设备节点
class_led = class_create(THIS_MODULE, DEV_NAME); // 创建类
device = device_create(class_led, NULL, led_devno, NULL, DEV_NAME); // 创建设备
return 0;
add_err:
unregister_chrdev_region(led_devno, DEV_CNT); // 注销已分配的设备号
printk("\n error! \n");
alloc_err:
return -1;
}
在驱动文件的 probe函数中,
这里 获取 设备树节点,用到了 of 操作函数。
获取节点的 gpio编号、配置 gpio为高电平,用到了 gpio子系统。
动态分配设备号、关联字符设备与文件操作集,初始化字符设备、添加字符设备到散列表,创建设备类和设备节点,用到的是字符设备框架。
实现字符设备函数
字符设备函数我们只需要实现 open 函数和 write 函数。
这里 open 函数虽然只打印了信息,但从功能上看是合法的。
当用户空间调用 open("/dev/xxx", O_RDWR) 时,内核会先查找对应的设备驱动,然后自动分配文件描述符,并关联 struct file 结构体(通过 filp 参数传递给驱动的 open 函数)。
驱动的 open 函数无需手动处理文件描述符的创建,只需关注设备本身的状态。
write 函数 通过 copy_ftom_user 从用户空间拷贝数据到内核空间。再根据接收到的数据通过GPIO子系统控制引脚电平。
cpp
/* 第一部分:字符设备操作函数集定义 */
static struct file_operations led_chr_dev_fops = {
.owner = THIS_MODULE, // 指向模块自身,防止模块被意外卸载
.open = led_chr_dev_open, // 关联open操作函数
.write = led_chr_dev_write // 关联write操作函数
};
/* 第二部分:open函数实现 */
static int led_chr_dev_open(struct inode *inode, struct file *filp)
{
printk("open \n"); // 打印打开信息
return 0; // 打开成功
}
/* 第三部分:write函数实现 */
static ssize_t led_chr_dev_write(struct file *filp, const char __user *buf,
size_t cnt, loff_t *offt)
{
unsigned char write_data; // 存储用户空间传来的数据
// 从用户空间拷贝数据到内核空间
int error = copy_from_user(&write_data, buf, cnt);
if (error < 0) {
return -1; // 拷贝失败返回错误
}
// 根据接收的数据控制LED电平(1:高电平灭,0:低电平亮)
if (write_data) {
gpio_direction_output(led, 1);
} else {
gpio_direction_output(led, 0);
}
return 0; // 写入成功
}
write函数为什么可以操作gpio子系统控制引脚电平,是怎么找到引脚地址的
write函数能通过GPIO 子系统控制引脚电平,核心是通过设备树解析获取引脚编号,再通过 GPIO 子系统接口操作该编号对应的硬件引脚。
cpp
led_test {
compatible = "fire,led_test"; // 与驱动的of_device_id匹配
gpios = <&gpio1 3 GPIO_ACTIVE_LOW>; // 绑定GPIO引脚:控制器gpio1的第3号引脚
};
cpp
/* GPIO控制器节点定义(以GPIO1为例) */
gpio1: gpio@209c000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio"; /* 兼容属性,用于匹配驱动 */
reg = <0x209c000 0x400>; /* 控制器寄存器基地址(0x209c000)和大小(0x400) */
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>; /* 中断信息:SPI中断号66,高电平触发 */
gpio-controller; /* 标记为GPIO控制器,必备属性 */
#gpio-cells = <2>; /* 描述子节点引用时需传入的参数个数(通常为2:引脚号+标志) */
interrupt-controller; /* 标记为中断控制器(GPIO引脚可作为中断源时需要) */
#interrupt-cells = <2>; /* 中断参数个数(通常为2:引脚号+触发方式) */
clocks = <&clks IMX6UL_CLK_GPIO1>; /* 关联的时钟源 */
status = "okay"; /* 使能该控制器 */
};
这个 gpio节点通常会有个芯片厂商定义的 Pinctrl父节点,
比如有个 pinctrl控制器,负责所有引脚功能的复用,
然后定义了一个 gpio1控制器节点,在子控制器节点中通过
pinctrl-names 定义了配置名称,再通过pinctrl-0、pinctrl-1之类的属性去引用 pinctrl配置的复用属性。
cpp
/* 顶层 Pinctrl 控制器(仅负责引脚复用,不含 gpio-controller) */
iomuxc: pinctrl@20e0000 {
compatible = "fsl,imx6ul-iomuxc";
reg = <0x20e0000 0x4000>;
pinctrl_gpio1: gpio1grp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO03__GPIO1_IO03 0x17059
>;
};
};
/* GPIO 控制器节点(此处才需要 gpio-controller 属性) */
gpio1: gpio@209c000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x209c000 0x400>;
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>;
gpio-controller; /* 必须写在 GPIO 控制器节点中,标记为 GPIO 控制器 */
#gpio-cells = <2>; /* 配合 gpio-controller 使用,定义引用格式 */
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_gpio1>; /* 引用 Pinctrl 配置 */
clocks = <&clks IMX6UL_CLK_GPIO1>;
status = "okay";
};
12.3.1.2 应用程序讲解
即使在yinguyong
cpp
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h> // 补充atoi函数依赖头文件
int main(int argc, char *argv[])
{
printf("led test\n");
// 校验命令行参数:必须传入1个参数(0或1)
if (argc != 2) {
printf("command error ! \n");
printf("usage : sudo test_app num [num can be 0 or 1]\n");
return -1;
}
// 打开LED字符设备文件(路径需与驱动中device_create的设备名一致)
int fd = open("/dev/led_test", O_RDWR);
if (fd < 0) {
printf("open file /dev/led_test failed !\n"); // 修正错误提示(原argv[0]为程序名,非设备名)
return -1;
}
// 将命令行参数(字符串)转为数字(0:亮灯,1:灭灯,对应驱动逻辑)
unsigned char command = atoi(argv[1]);
// 向驱动写入控制命令
int error = write(fd, &command, sizeof(command));
if (error < 0) {
printf("write file error! \n");
close(fd); // 写入失败时先关闭文件再退出
return -1;
}
// 关闭设备文件,释放文件描述符
error = close(fd);
if (error < 0) {
printf("close file error! \n");
return -1;
}
return 0;
}
12.3.2 实验准备
板卡部分 GPIO 可能被系统占用,导致运行代码出现 "Device or resource busy" 或卡死,需修改 / 注释设备树后重启以释放引脚;
若提示 "Permission denied",需用 sudo 或以 root 用户权限运行程序(操作硬件外设需 root 权限)。
12.3.2.1 Makefile 修改说明
修改 Makefile 并编译生成驱动程序
Makefile 程序并没有大的变化,修改后的 Makefile 如下所示。
bash
KERNEL_DIR = ../../kernel/ #内核所在路径
ARCH = arm64
CROSS_COMPILE = aarch64-linux-gnu-
export ARCH CROSS_COMPILE
obj-m := led_test.o #与驱动源码名对应
out = led_app #输出的驱动名称
all:
#编译内核驱动源码
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
#调用make工具,进入内核目录,指定外部模块(当前驱动)所在目录,内核makefile的编译目标
#作用:
# 结合当前内核的配置(.config 文件)和架构(ARCH=arm64),
# 使用交叉编译器(CROSS_COMPILE=aarch64-linux-gnu-)编译模块,
# 生成 .ko 文件(如 led_test.ko),并输出到当前目录。
#编译 .c测试文件
$(CROSS_COMPILE)gcc -o $(out) led_app.c #调用
.PHONY: clean #用于标记 clean 为 "伪目标"(Phony Target),避免cmake认为其是实际文件
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
rm -f $(out) # 补充清理应用程序可执行文件
关于伪目标:
Make 工具的默认逻辑是 "检查目标是否为一个已存在的文件"。
如果当前目录下恰好有一个名为 clean 的文件,那么执行 make clean 时,Make 会认为 "目标 clean 已存在且最新",从而跳过执行 clean 对应的清理命令
通过 .PHONY: clean 声明后,Make 会强制将 clean 视为 "伪目标"(不对应任何实际文件),无论当前目录是否存在 clean 文件,执行 make clean 时都会无条件执行 clean 目标下的命令。
bash
#执行makefile,正常情况下会在当前目录生成 led_test.ko 驱动文件和 led_app 应用程序
make
12.3.3 下载验证
bash
#加载驱动
insmod ./led_test.ko
驱动与设备匹配成功后,probe 函数执行,会注册字符设备并创建设备文件,正常情况下 /dev/ 目录下会生成 led_test 设备节点。

bash
#运行测试程序
./led_app < 命令>

第 13 章 中断子系统
本章讲解中断知识,含内核中断框架、概念,借鉴手册简述 ARM GIC v3 中断控制器,分析内核中断控制器初始化源码及中断驱动常用 API,具体包括:
中断子系统框架、
GIC v3 控制器、
内核中断驱动核心分析、
中断相关 API 与数据结构。
13.1 中断子系统框架
13.1.1 中断硬件简单描述
中断硬件主要有三种器件参与,各个外设 、中断控制器 和 CPU。
在 ARMv8 体系结构中,
将处理器处理事务的抽象过程定义为 PE(Processing Element),
可以将 PE 简单理解为处理器核心
1、外设触发中断时,通过电气信号向中断控制器请求处理;
2、中断控制器(如 ARM 的 GIC)管理多外设中断,接收信号后标记为 FIQ/IRQ 等,转发给 CPU;
3、CPU 专注运算,不处理中断优先级,接收 GIC 的 IRQ/FIQ 信号后,进入对应模式执行中断处理。
13.1.2 软件框架
Linux 内核中断子系统分 4 部分:
-
通用中断处理模块(硬件无关,抽象公共逻辑,提供统一接口);
-
CPU 架构相关中断处理;
-
中断控制器驱动;
-
普通驱动(调用通用模块 API 实现逻辑)。
常见概念:
HW interrupt ID:GIC 硬件定义的 irq ID;
IRQ number:中断请求编号,标识外设中断;
IRQ domain:负责 GIC 的 hwirq 与逻辑 irq 映射;
中断向量表:内存中存储中断处理程序入口地址的列表;
中断处理架构:分上半部(处理紧急任务如清中断标志)和下半部(处理繁重任务),提升系统吞吐率。
HW interrupt ID:Hardware Interrupt ID(硬件中断 ID,即 GIC 硬件定义的 irq ID)
IRQ number:Interrupt Request Number(中断请求编号,即软件虚拟编号,用于标识外设中断)
IRQ domain:Interrupt Request Domain(中断请求域,负责 GIC 的 hwirq 与逻辑 irq 的映射)
13.2 GIC v3 中断控制器简介
ARM 多核处理器常用中断控制器 GIC(Generic Interrupt Controller),可灵活扩展,支持单核到大型多核设计,核心功能是接收硬件中断并分发至对应 CPU 处理。
GIC 由 ARM 制定规范(V1-V4),常用 V2.0、V3.1、V4.1,其中 GICv3 适用于 Armv8-A/Armv9-A 架构。
ARM 提供设计参考(如 GIC-400/V2、GIC-500/V3、GIC-600/V3/V4),芯片厂商可自主实现或采购。
本章简述 GICv3 基本结构与实现,详情参考官方规范文档。
Arm Generic Interrupt Controller Architecture Specification GIC Architecture Version 3.1 and 4.0
13.2.1 GIC v3 中断类型
GIC v3 支持 4 类中断源,各中断均由 INTID 标识:
SGI:软件触发中断,通过写 GICD_SGIR 寄存器触发,用于核间通信(如内核 IPI);
PPI:私有外设中断,来自外设,仅由特定 CPU 核心处理;
SPI:共享外设中断,所有 CPU 核心共享,可分发至任一 CPU;
LPI:局域-专用外设中断,GICv3 新增,基于消息的中断,配置存储于表中(非寄存器)。
中断 ID和中断类型,
SGI,软件触发中断,
PPI,私有外设中断,
SPI,共享外设中断,
LPI,局域专用外设中断
SGI:Software Generated Interrupt(软件触发中断)
SPI:Shared Peripheral Interrupt(共享外设中断)
PPI:Private Peripheral Interrupt(私有外设中断,注:原表述中对应关系颠倒,已修正)
LPI:Locality-specific Peripheral Interrupt(局域专用外设中断)
13.2.2 GIC v3 基本结构
GIC V3的逻辑组成

GIC v3 主要包括
分发器 Distributor、
CPU接口 CPU interface、
重分发器 Redistributor、
中断翻译服务 ITS,
其 CPU interface 已从 GIC 中抽离至 CPU 内,通过 AXI Stream 与 GIC 通信;
AXI Stream 的全称是 Advanced eXtensible Interface Stream,高级可扩展接口
GIC 发送中断命令后,CPU interface 依据中断线映射配置,通过 IRQ 或 FIQ 管脚向 CPU 转发中断。
Distributor:管理 SPI 中断,负责仲裁与分发至 Redistributor,可全局或单独开关中断、控制优先级、指定目标 CPU、设置中断触发方式(边缘 / 电平)。
Redistributor:管理 SGI、PPI、LPI 中断并发送至 CPU interface,功能包括开关 SGI/PPI、设置其优先级与触发方式、分配中断组、控制状态,以及管理 LPI 相关内存数据结构和电源管理。
CPU interface:为处理器提供接口,可开关发往 CPU 的中断、确认中断、通知处理完毕,还能设置优先级掩码(仅高优先级中断可送达 CPU)、抢占策略及查询最高优先级挂起中断。
ITS(中断翻译服务):GICv3 可选硬件,将基于消息的中断转换为 LPI,解析后发送至对应 Redistributor,再由其转发给 CPU interface。
SPI:Shared Peripheral Interrupt(共享外设中断)
SGI:Software Generated Interrupt(软件触发中断)
PPI:Private Peripheral Interrupt(私有外设中断,注:原表述中对应关系颠倒,已修正)
LPI:Locality-specific Peripheral Interrupt(局域外设中断)
13.2.3 中断状态和处理流程
每个中断都维护一个状态机,支持 Inactive、Pending、Active、Active and pending。
未激活 、挂起、 激活、 激活且挂起
中断状态和处理流程
挂起的本质是该中断的 "挂起标志" 被置位,等待当前处理完成后优先被 CPU 响应,
而非进入普通队列排队 简单中断处理流程:
外设发起中断→
Distributor 根据优先级、使能状态等特性分发至对应 Redistributor→
Redistributor 转发至 CPU interface→
CPU interface 向处理器触发中断异常→
处理器接收并处理。
13.2.4 相关寄存器介绍
寄存器参考 GIC v3 手册,部分寄存器通用;
本节描述基于 armv8 arch64 架构,寄存器值可能有偏差,仅供参考,以实际手册为准。
GIC V3寄存器的分布图
图中寄存器前缀含义:
GICD(Distributor)、
GICR(Redistributor)、
GICC(CPU interface)、
GITS(ITS)。
CPU interface 寄存器支持两种访问方式:
前缀 ICC/ICV/ICH 为系统寄存器,
GICC/GICV 等为内存映射(memory-mapped)寄存器。
Distributor 相关寄存器
前文提及的 GIC Distributor 编程接口即寄存器,本节简要介绍部分相关寄存器,详情参考 ARM 中断手册。
GIC Distributor寄存器表
表中仅给出 GIC Distributor 相关寄存器的基地址偏移(仅详细介绍所用寄存器);
"默认值" 标注 "待定"(对应手册 "IMPLEMENTATION DEFINED"),因表格源自通用手册,不针对具体芯片,默认值由厂商决定;
"地址偏移" 若为范围(如中断使能寄存器 0x100-0x17C),是因对应寄存器数量较多,偏移覆盖该范围。
部分寄存器简单介绍如下:
中断使能寄存器 GICD_ISENABLERn
中断使能寄存器与中断屏蔽寄存器(GICD_ICENABLERn)一一对应,GIC Distributor 将中断的使能与屏蔽独立配置。
中断使能寄存器
据《ARM® Generic Interrupt Controller》,共 1020 个中断(0~1019),
需多个中断使能寄存器(GICD_ISENABLER)分别控制;
其地址偏移为 0x100-0x17C,
寄存器从 GICD_ISENABLER0 至 GICD_ISENABLERn 依次排布于此区间。
32 个寄存器 × 32 位 / 寄存器 = 1024 位 → 可覆盖 1020 个中断(0~1019),剩余 4 位为保留位。单个中断仅占用 1 个寄存器位。
已知中断号 m,开启 / 关闭中断的方法:
中断号/32(取整数) +
GIC Distributor 中中断使能寄存器组的起始地址偏移+
中断号/32(取余数)
0x100 是 GIC Distributor 中中断使能寄存器组(GICD_ISENABLERn)的起始地址偏移。
寄存器支持按位读写:值为 1 表示中断启用,0 为禁用。
中断优先级设置寄存器 GICD_IPRIORITYRn
查表可知组寄存器位于 0x400-0x7F8 偏移地址处。
中断优先级设置寄存器,也是寄存器组,这里只显示了组中的一个
每个中断优先级占 8 位(值越小优先级越高)。但是仅高四位有效。
已知中断号 m,找对应优先级寄存器的方法:
寄存器编号 n = m / 4(整数除法),地址偏移为 0x400 + 4×n;
寄存器内位偏移 offset = m % 4,对应 8 位字段(如 m=65 时,n=16,offset=1,对应 GICD_IPRIORITYR16 [15:8])。
中断号/4因为每个寄存器编号可处理4个中断,每个中断占用8位。
CPU 接口内存映射寄存器
GIC v3 的 CPU 接口模块的编程接口即寄存器,其新增系统寄存器访问方式。
CPU interface 寄存器支持两种访问方式:
前缀 ICC/ICV/ICH 为系统寄存器,
GICC/GICV 等为内存映射(memory-mapped)寄存器。
以下介绍常用的 CPU 接口内存映射(memory-mapped)寄存器。
常用的 CPU接口内存映射寄存器,适用于GIC V2及更早版本
中断优先掩码寄存器 GICC_PMR
GIC 分发器 的 GICD_IPRIORITYRn 寄存器用于设置优先级,每个中断占 8 位配置优先级;
CPU 接口 的 GICC_PMR 寄存器 (8 位)用于设置中断优先级阈值,仅高于该阈值的中断可送达 CPU,具体如下。
CPU的优先级掩码寄存器,
低八位用于设置优先级阈值,其他位保留,
需要注意仅这八位的高四位有效
GICC_PMR 寄存器 [7:0]用于设置中断优先级阈值(格式与 GICD_IPRIORITYR 一致),仅高于该值的中断可送达 CPU;注意其 8 位中仅高 4 位有效 ,这 4 位分为抢占优先级 和子优先级,优先级分组时再详述(同 STM32 机制)。
中断优先级分组寄存器 GICC_BPR
中断优先级分组寄存器(GICC_BPR) 将 8 位优先级分为抢占优先级 和子优先级,机制同 STM32,具体如下。
CPU的优先级分组寄存器,仅低三位有效
BPR,Binary point register
中断优先级分组寄存器的后三位 用于设置中断优先级分组,
中断优先级分组表,
优先级分组寄存器仅低三位有效,
用来表示子优先级占优先级寄存器的多少位
每个中断 8 位优先级仅高 4 位有效,故 GICC_BPR [2:0] 设为 1-3 效果相同,仅 16 级抢占优先级,无子女优先级。
比如优先级分组寄存器的值为0~3的时候,代表优先级寄存器8位中的低4位用来表示子优先级。但低4位是不用的。
中断确认寄存器 GICC_IAR
中断确认寄存器 GICC_IAR 保存当前挂起的最高优先级中断。
中断确认寄存器 IAR Acknowledge
GICC_IAR 寄存器含 CPUID [10:12](存请求中断的 CPU ID,供多核处理用)和 interrupt ID [0:9](记录当前挂起的最高优先级中断)。
读该寄存器结果为 1023 时,表明无可用中断,常见情况:GIC 分发器或 CPU 接口禁止向 CPU 发中断请求;CPU 接口无挂起中断或其优先级≤GICC_PMR 设定值。
CPU 接口系统寄存器
这张图列出了多个 GIC 系统寄存器(如 ICC_PMR_EL1、ICC_IAR0_EL1 等),
适用于 GIC V3版本,
包含它们的位宽、访问指令编码(Op0、Op1、CRn、CRm、Op2)以及读写属性等信息。
CPU 接口从 GIC 内部剥离,集成于 PE 内,通过系统寄存器实现中断快速响应,其与内存映射寄存器类似。上述寄存器后缀含 EL1,为 GIC 系统寄存器接口的异常级别;
ARMv8 中异常等级分 EL0 至 EL3,数字越大等级越高。
#汇编指令读写系统寄存器
; 读取
MRS X0, ICC_BPRO_EL1
; 写入
MOV X0, #0xFFFF
MSR ICC_BPRO_EL1, X0
中断优先掩码寄存器 ICC_PMR_EL1
该寄存器提供中断优先级功能,仅优先级高于设定值的中断会通知 PE;
与 GICC_PMR 类似,其低 8 位(0~7)用于设优先级,仅高 4 位有效。
内存映射寄存器与系统寄存器的区别与联系

13.2.5 GIC-600 简单介绍
RK3568 的中断控制器为 GIC-600,它支持 GIC v3 版本,是 ARM 的一款实际控制器设计参考,其 GIC 流程框图如下。
RK3568 中 Cortex-A55 四核处理器经 ADB400 AXI Stream 与 GIC600 中断控制器连接,
AXI,高级可扩展接口,
且 GIC600 通过 PPI 向处理器传输中断的流程
rk3568 的 256 个 SPI 中断号简单分配表 (部分):
256个SPI简单分配表(部分)
结合 rk3568 的设备树,可以看下 GIC-600 寄存器地址:
分发器
中断翻译服务
重分发器
13.3 中断驱动简单分析
13.3.1 设备树中的中断信息
前面已通过 GIC 控制器背景知识及汇编代码讲解中断使用过程,Linux 系统已封装大部分底层细节,只需在框架内使用,下文将以 RK3568 中断为例,简单讲解中断控制初始化(内核源码获取参考环境搭建章节)。
先了解设备树对中断系统的描述:
打开内核源码 /arch/arm64/boot/dts/rockchip 目录下的 rk3568.dtsi,找到 "interrupt-controller" 节点,内容如下。
cpp
#gic中断控制器节点
gic: interrupt-controller@fd400000 {
compatible = "arm,gic-v3"; /* 兼容GICv3架构 */
#interrupt-cells = <3>; /* 中断描述需3个参数 */
#address-cells = <2>; /* 地址编码占2个单元格 */
#size-cells = <2>; /* 大小编码占2个单元格 */
//"cells" 指用于描述地址、大小或中断等信息的32 位无符号整数(u32),
// 一个 cell 就是一个 32 位值两个 Cell代表地址、大小等信息需要64位来描述
ranges; /* 启用地址映射 */
interrupt-controller; /* 标识为中断控制器节点 */
/*地址编码占两个单元格,大小编码占两个单元格*/
reg = <0x0 0xfd400000 0 0x10000>, /* GICD(分发器)物理地址及大小 <地址1> <大小1> <地址2> <大小2>*/
<0x0 0xfd460000 0 0xc0000>; /* GICR(CPU接口)物理地址及大小 */
/*中断描述占3个参数 中断类型 中断号 触发类型*/
interrupts = <GIC_PPI 9 IRQ_TYPE_LEVEL_HIGH>; /* 关联PPI中断(高电平触发) */
its: interrupt-controller@fd440000 { /* ITS(中断翻译服务)子节点 */
compatible = "arm,gic-v3-its"; /* 兼容GICv3 ITS */
msi-controller; /* 标识为MSI控制器 */ //MSI 消息信号中断
#msi-cells = <1>; /* MSI描述需1个参数 */
reg = <0x0 0xfd440000 0x0 0x20000>; /* ITS寄存器物理地址及大小 */
};
};

GIC 架构包含 Distributor、Redistributor、CPU Interface,上述设备树节点即用于描述整个 GIC 控制器。
GIC 中断控制器使用实例(uart3 为例(rk3568.dtsi))
根节点定义全局中断父控制器,
子节点定义串口设备。
cpp
/ {
compatible = "rockchip,rk3568"; // 兼容瑞芯微RK3568芯片
/*<全局中断父控制器>*/
interrupt-parent = <&gic>; // 全局中断父控制器为GIC
#address-cells = <2>; // 地址用2个32位值描述(64位地址)
#size-cells = <2>; // 大小用2个32位值描述(64位大小)
/* 其他节点省略 */
/*这里定义了串口设备,是中断请求的发起者*/
/*由于根节点定义了全局中断父控制器,因此子节点会继承中断控制器*/
uart3: serial@fe670000 { // UART3串口设备,基地址0xfe670000
compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart"; // 兼容RK3568 UART及Synopsys APB UART
reg = <0x0 0xfe670000 0x0 0x100>; // 寄存器地址范围(基地址+大小)
/*描述中断资源*/
interrupts = <GIC_SPI 119 IRQ_TYPE_LEVEL_HIGH>; // 中断信息(SPI类型,中断号119,高电平触发)
clocks = <&cru SCLK_UART3>, <&cru PCLK_UART3>; // 引用的时钟(波特率时钟、APB总线时钟)
clock-names = "baudclk", "apb_pclk"; // 时钟名称,与clocks对应
reg-shift = <2>; // 寄存器地址偏移量(单位:字节)
reg-io-width = <4>; // 寄存器访问宽度(4字节)
dmas = <&dmac0 6>, <&dmac0 7>; // 引用的DMA通道(发送、接收)
pinctrl-names = "default"; // 引脚配置名称
pinctrl-0 = <&uart3m0_xfer>; // 默认引脚配置引用
status = "disabled"; // 设备初始状态(禁用)
};
/* 其他节点省略 */
};
uart3 作为根节点子节点,继承根节点指定的 GIC 中断控制器,通过 interrupts 描述中断资源:
因 GIC 节点规定 #interrupt-cells = <3>,故 interrupts 含三个参数:
中断类型(如 SPI 共享中断,外部中断常用);
中断编号(范围与类型相关,SPI 为 0-256,PPI 为 0-15);
中断触发方式(u32 类型,后 4 位设触发类型,可使用系统宏定义)。
cpp
/*中断触发方式设置 irq.h*/
#define IRQ_TYPE_NONE 0 // 无触发方式
#define IRQ_TYPE_EDGE_RISING 1 // 上升沿触发
#define IRQ_TYPE_EDGE_FALLING 2 // 下降沿触发
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING) // 双边沿触发
#define IRQ_TYPE_LEVEL_HIGH 4 // 高电平触发
#define IRQ_TYPE_LEVEL_LOW 8 // 低电平触发
interrupts 第三个参数(触发类型)的 [8-15] 位,仅在 PPI 中断中用于 CPU 屏蔽配置。
多核系统中,这 8 位对应 8 个 CPU,某一位为 1 时,PPI 中断发送至对应 CPU,为 0 则屏蔽该 CPU 的中断。
cpp
timer {
compatible = "arm,armv8-timer"; // 兼容ARMv8架构定时器
// 4个PPI中断配置:类型为PPI,中断号13/14/11/10,高电平触发
// 结合CPU屏蔽(4核系统,允许中断发送至所有4个CPU)
interrupts = <GIC_PPI 13 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>,
<GIC_PPI 14 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>,
<GIC_PPI 11 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>,
<GIC_PPI 10 (GIC_CPU_MASK_SIMPLE(4) | IRQ_TYPE_LEVEL_HIGH)>;
arm,no-tick-in-suspend; // 休眠时停止定时器计数
};
这里 timer 节点定义 ARMv8 系统定时器,通过 interrupts 声明 4 个 PPI 私有中断(中断号 10、11、13、14,对应不同定时器功能),触发方式为高电平,GIC_CPU_MASK_SIMPLE (4) 指定 4 核系统中可发送至所有 CPU。(若为8和则前4个cpu,原理是低位置1)
GIC_CPU_MASK_SIMPLE(4) 生成的掩码为 0b00001111(二进制),即允许中断发送至 CPU0~CPU3。
定时器中断的触发时机
什么是虚拟定时器中断
什么是虚拟化场景
13.3.2 GIC v3 中断控制器的代码
中断控制器通过 IRQCHIP_DECLARE 宏注册至**__irqchip_of_table**。
cpp
/*内核源码/drivers/irqchip/irq-gic-v3.c*/
//初始化一个 struct of_device_id 静态常量,并存入 __irqchip_of_table(中断控制器设备树匹配表) 中
IRQCHIP_DECLARE(
gic_v3, // 标识名,用于生成唯一的静态变量名
"arm,gic-v3", // 设备树兼容字符串,用于匹配GICv3中断控制器节点
gicv3_of_init // 初始化回调函数,匹配成功后执行GICv3的初始化逻辑
);
IRQCHIP_DECLARE 负责 "注册匹配信息",
of_irq_init 负责 "查找并触发初始化"
系统开机初始化时,
of_irq_init 函数会根据IRQ_CHIPDECLARE注册的节点信息查找设备节点,
通过__irqchip_of_table 中的 "arm,gic-v3" 匹配设备树 "gic" 节点,
获取信息后执行 IRQCHIP_DECLARE 声明的回调函数 gicv3_of_init。
IRQ_CHIP_DECLARE宏,注册节点信息
声明中断控制器设备树匹配表
/*唯一标识符, 内核内部区分不同的中断控制器声明
设备树兼容属性, 用于与设备树对应的硬件设备匹配
初始化函数指针*/
cpp
// 定义IRQCHIP_DECLARE宏,用于声明中断控制器与设备树的匹配关系及初始化函数
#define IRQCHIP_DECLARE(name, compat, fn) \ /*唯一标识符,设备树兼容属性,初始化函数指针*/
OF_DECLARE_2(irqchip, name, compat, fn)
// 二阶宏,指定表类型为irqchip,封装设备树匹配信息
#define OF_DECLARE_2(table, name, compat, fn) \
_OF_DECLARE(table, name, compat, fn, of_init_fn_2)
// 底层宏,生成struct of_device_id实例并放入指定段
#define _OF_DECLARE(table, name, compat, fn, fn_type) \
static const struct of_device_id __of_table_##name \
__used __section(__##table##_of_table) \
__aligned(__alignof__(struct of_device_id)) = { \
.compatible = compat, \
.data = (fn == (fn_type)NULL) ? fn : fn \
}
该宏初始化了一个 struct of_device_id 静态常量,放到并放置在 __irqchip_of_table 段。
__irqchip_of_table 设备树控制器匹配表
gicv3_of_init 初始化函数
根据 IRQ_CHIPDECLARE 注册的节点信息查找设备节点,
cpp
/*内核源码/drivers/irqchip/irq-gic-v3.c*/
static int __init gicv3_of_init(struct device_node *node, struct device_node *parent)
{
void __iomem *dist_base; // GIC分发器(GICD)寄存器映射地址
struct redist_region *rdist_regs; // 重分布器(GICR)区域配置结构体
u64 redist_stride; // 重分布器地址步长
u32 nr_redist_regions; // 重分布器区域数量
int err, i;
// 映射GIC分发器(GICD)寄存器地址空间
dist_base = of_iomap(node, 0);
if (!dist_base) {
pr_err("%pOF: unable to map gic dist registers\n", node);
return -ENXIO;
}
// 验证GIC硬件版本是否为GICv3/GICv4
err = gic_validate_dist_version(dist_base);
if (err) {
pr_err("%pOF: no distributor detected, giving up\n", node);
goto out_unmap_dist;
}
// 读取设备树#redistributor-regions属性,无则默认1个区域
if (of_property_read_u32(node, "#redistributor-regions", &nr_redist_regions))
nr_redist_regions = 1;
// 为重分布器区域配置结构体分配内存
rdist_regs = kcalloc(nr_redist_regions, sizeof(*rdist_regs), GFP_KERNEL);
if (!rdist_regs) {
err = -ENOMEM;
goto out_unmap_dist;
}
// 映射每个重分布器(GICR)区域的寄存器地址
for (i = 0; i < nr_redist_regions; i++) {
struct resource res;
int ret;
ret = of_address_to_resource(node, 1 + i, &res);
rdist_regs[i].redist_base = of_iomap(node, 1 + i);
if (ret || !rdist_regs[i].redist_base) {
pr_err("%pOF: couldn't map region %d\n", node, i);
err = -ENODEV;
goto out_unmap_rdist;
}
rdist_regs[i].phys_base = res.start; // 记录重分布器物理基地址
}
// 读取设备树redistributor-stride属性,无则默认0
if (of_property_read_u64(node, "redistributor-stride", &redist_stride))
redist_stride = 0;
// GICv3核心初始化:绑定分发器、重分布器等硬件资源
err = gic_init_bases(dist_base, rdist_regs, nr_redist_regions,
redist_stride, &node->fwnode);
if (err)
goto out_unmap_rdist;
// 配置PPI中断的CPU亲和性(指定PPI可发送至哪些CPU)
gic_populate_ppi_partitions(node);
// 若支持deactivate密钥,配置KVM相关的GIC信息(虚拟化场景)
if (static_branch_likely(&supports_deactivate_key))
gic_of_setup_kvm_info(node);
return 0;
// 错误处理:卸载重分布器寄存器映射并释放内存
out_unmap_rdist:
for (i = 0; i < nr_redist_regions; i++)
if (rdist_regs[i].redist_base)
iounmap(rdist_regs[i].redist_base);
kfree(rdist_regs);
// 错误处理:卸载分发器寄存器映射
out_unmap_dist:
iounmap(dist_base);
return err;
}
gicv3_of_init 函数的核心流程:
映射 GIC 寄存器基地址,通过读取 GICD_PIDR2 寄存器 bit [7:4](值为 0x3 时确认是 GICv3)获取版本信息,读取设备树属性后,调用 gic_init_bases 函数。
cpp
static int __init gic_init_bases(void __iomem *dist_base,
struct redist_region *rdist_regs,
u32 nr_redist_regions,
u64 redist_stride,
struct fwnode_handle *handle)
{
u32 typer; // GIC distributor 控制寄存器值
int gic_irqs; // 支持的中断总数
int err; // 错误码
// 非虚拟化模式下禁用 split EOI/Deactivate 模式
if (!is_hyp_mode_available())
static_branch_disable(&supports_deactivate_key);
// 打印 EOI/Deactivate 模式信息
if (static_branch_likely(&supports_deactivate_key))
pr_info("GIC: Using split EOI/Deactivate mode\n");
// 初始化 GIC 核心数据结构(关联硬件资源)
gic_data.fwnode = handle; // 关联设备节点句柄
gic_data.dist_base = dist_base; // 保存 distributor 基地址
gic_data.redist_regions = rdist_regs; // 保存 redistributor 区域信息
gic_data.nr_redist_regions = nr_redist_regions; // redistributor 区域数量
gic_data.redist_stride = redist_stride; // redistributor 地址步长
// 计算支持的中断总数(最大 1020 个)
typer = readl_relaxed(gic_data.dist_base + GICD_TYPER); // 读取 distributor 类型寄存器
gic_data.rdists.gicd_typer = typer;
gic_irqs = GICD_TYPER_IRQS(typer); // 从寄存器提取中断数量
if (gic_irqs > 1020)
gic_irqs = 1020;
gic_data.irq_nr = gic_irqs; // 保存中断总数
// 创建中断域(irq domain),关联 GIC 操作集
gic_data.domain = irq_domain_create_tree(handle, &gic_irq_domain_ops, &gic_data);
irq_domain_update_bus_token(gic_data.domain, DOMAIN_BUS_WIRED);
// 为每个 CPU 分配 redistributor 数据结构
gic_data.rdists.rdist = alloc_percpu(typeof(*gic_data.rdists.rdist));
gic_data.rdists.has_vlpis = true; // 标记支持虚拟 LPI 中断
gic_data.rdists.has_direct_lpi = true; // 标记支持直接 LPI 中断
// 检查资源分配是否成功
if (WARN_ON(!gic_data.domain) || WARN_ON(!gic_data.rdists.rdist)) {
err = -ENOMEM;
goto out_free;
}
// 检测并打印 Range Selector 支持状态
gic_data.has_rss = !!(typer & GICD_TYPER_RSS);
pr_info("Distributor has %sRange Selector support\n", gic_data.has_rss ? "" : "no ");
// 初始化 MBIs(消息基于中断)功能(如支持)
if (typer & GICD_TYPER_MBIS) {
err = mbi_init(handle, gic_data.domain);
if (err)
pr_err("Failed to initialize MBIs\n");
}
// 设置全局中断处理入口函数
set_handle_irq(gic_handle_irq);
// 更新虚拟 LPI 相关属性
gic_update_vlpi_properties();
// 初始化 ITS(中断翻译服务)(如启用配置且硬件支持)
if (IS_ENABLED(CONFIG_ARM_GIC_V3_ITS) && gic_dist_supports_lpis())
its_init(handle, &gic_data.rdists, gic_data.domain);
// 初始化 SMP 相关(核间中断等)
gic_smp_init();
// 初始化 distributor 硬件
gic_dist_init();
// 初始化 CPU 接口
gic_cpu_init();
// 初始化电源管理相关功能
gic_cpu_pm_init();
return 0;
out_free:
// 释放已分配的资源
if (gic_data.domain)
irq_domain_remove(gic_data.domain);
free_percpu(gic_data.rdists.rdist);
return err;
}
13.4 中断常用 API 和数据结构
13.4.1 中断申请和释放函数
request_irq() 函数
cpp
// 申请中断线,需手动释放
static inline int __must_check request_irq(
unsigned int irq, // 中断号
irq_handler_t handler, // 中断处理函数
unsigned long flags, // 中断触发方式(如IRQF_TRIGGER_RISING等)
const char *name, // 中断名称(用于/proc/interrupts显示)
void *dev // 传给中断处理函数的私有数据(共享中断时用于区分)
);

"共享中断"指的是多个驱动程序共用同一个中断。
中断发生后,内核会依次调用这些驱动的中断服务函数,
因此需要判断中断是否来自本驱动
中断标志可选如下
cpp
// 中断触发方式相关宏定义
#define IRQF_TRIGGER_NONE 0x00000000 // 无触发(默认,通常用于电平触发的默认状态或无需特定触发)
#define IRQF_TRIGGER_RISING 0x00000001 // 上升沿触发(信号从低到高跳变时触发中断)
#define IRQF_TRIGGER_FALLING 0x00000002 // 下降沿触发(信号从高到低跳变时触发中断)
#define IRQF_TRIGGER_HIGH 0x00000004 // 高电平触发(信号维持高电平时持续触发)
#define IRQF_TRIGGER_LOW 0x00000008 // 低电平触发(信号维持低电平时持续触发)
// 触发方式掩码(用于提取或判断触发方式标志)
#define IRQF_TRIGGER_MASK (IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010 // 探测模式触发(用于驱动探测阶段,暂不实际使能中断)
// 中断共享相关宏定义
#define IRQF_SHARED 0x00000080 // 允许中断线共享(多个设备可共用同一中断号,需通过dev_id区分)
// /*-----------以下宏定义省略------------*/
free_irq() 函数
cpp
void free_irq(unsigned int irq, //从设备树中得到或者转换得到的中断编号
void *dev); //与 request_irq 函数中 dev 传入的参数一致
devm_request_irq() 函数
devm_request_irq () 与 request_irq () 的区别:前者申请的是内核托管资源,无需在出错处理或 remove () 中显式释放。
cpp
// 设备管理型中断申请,随设备释放自动释放中断
int devm_request_irq(
struct device *dev, // 关联的设备结构体
unsigned int irq, // 中断号
irq_handler_t handler, // 中断处理函数
unsigned long irqflags, // 中断标志
const char *devname, // 中断名称
void *dev_id // 私有数据(共享中断区分用)
);
13.4.2 中断处理函数
在中断申请时需要指定一个中断处理函数,
cpp
// 中断处理函数类型定义
irqreturn_t (*irq_handler_t)(int irq, //申请的中断号
void *dev); //申请中断时传入的私有数据(用于区分共享中断等场景)
返回值,irqreturn_t 类型:枚举类型变量
cpp
enum irqreturn {
IRQ_NONE = (0 << 0), // 中断未被本驱动处理
IRQ_HANDLED = (1 << 0), // 中断已被本驱动成功处理
IRQ_WAKE_THREAD = (1 << 1) // 唤醒中断底半部线程处理
};
typedef enum irqreturn irqreturn_t; // 中断处理函数返回值类型别名
IRQ_WAKE_THREAD 可与 IRQ_HANDLED 通过 "|" 组合
(如 IRQ_HANDLED | IRQ_WAKE_THREAD),既标记中断已处理,又触发底半部线程。
什么是底半部线程
共享中断中,若中断非本驱动产生,返回 IRQ_NONE;
未开启共享中断或共享中断为本驱动产生,返回 IRQ_HANDLED。若中断处理分上下半部,返回 IRQ_WAKE_THREAD。
13.4.3 中断的使能和屏蔽函数
中断屏蔽和使能
cpp
// 启用指定中断
void enable_irq(unsigned int irq); //中断号
// 禁用指定中断(等待当前中断处理完成后生效)
void disable_irq(unsigned int irq);
屏蔽/恢复本cpu内所有中断
四个接口均作用于当前 CPU,而非指定中断号,用于全局控制本 CPU 的中断开关,
local_irq_save/restore 成对使用,可在禁用中断后恢复原有状态,避免影响其他逻辑。
运行于中断上下文或持有自旋锁时,需谨慎使用,防止死锁或中断丢失。
cpp
// 启用当前CPU的本地中断
local_irq_enable();
// 禁用当前CPU的本地中断
local_irq_disable();
// 禁用当前CPU本地中断,并保存中断状态到flags
local_irq_save(unsigned long flags);
// 恢复当前CPU的本地中断状态(基于之前保存的flags)
local_irq_restore(unsigned long flags);

全局中断操作需注意:
关闭前用 local_irq_save 保存状态,开启后用 local_irq_restore 恢复。
在 armv8-arch64 架构中,local_irq_disable() 仅操作 daif 标志位关闭当前 CPU 异常 ,与中断控制器无关;disable_irq() 则通过控制中断控制器关闭指定中断。
local_irq_enable/disable
在 ARMv8-A Arch64 架构中,local_irq_disable() 通过设置 DAIF 标志位(Debug、SError、IRQ、FIQ)关闭当前 CPU 对异常的响应,这种操作的核心场景是需要在短时间内保证临界区代码不被任何异常打断。
粗暴地关闭 DAIF 异常。
cpp
// 关闭中断进入临界区
local_irq_disable();
// 执行需要原子性的操作(如修改共享数据)
critical_operation();
// 完成后重新启用中断
local_irq_enable();
IRQ 普通优先级中断
FIQ 快速响应中断
在 ARMV8-AARCH64 架构中,
二者和 Debug、SError一起,被看作异常,
构成 DSIF 异常标志位

DAIF异常标志位
local_irq_save/restore
状态保存式,适合屏蔽 DAIF中断后又恢复的场景
cpp
unsigned long flags;
// 保存当前中断状态并禁用中断
local_irq_save(flags);
// 执行临界操作
critical_operation();
// 恢复到之前的中断状态(可能是启用或禁用)
local_irq_restore(flags);
第 14 章 中断子系统实验
14.1 按键中断程序实验
14.1.1 设备树插件实现
中断控制器由厂商预先实现,我们只需在设备树节点中引用其对应的父节点,并配置相关中断信息即可。
这里我们编写成设备树插件的形式 (也可以使用设备树),方便使用,
新增了按键引脚作为设备节点,
通过 pinctrl 配置了引脚的复用功能(复用为GPIO)和上下拉情况,
cpp
/dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/interrupt-controller/irq.h>
&{/} {
/*在根节点下新增定义按钮中断设备节点*/
button_interrupt: button_interrupt {
status = "okay";
compatible = "button_interrupt";
/*配置按键GPIO引脚*/
button-gpios = <&gpio0 RK_PB0 GPIO_ACTIVE_LOW>;
/*引脚配置集名称,默认配置*/
pinctrl-names = "default";
/*关联引脚配置节点,引用下方的button_interrupt_pin节点*/
pinctrl-0 = <&button_interrupt_pin>;
/*引用中断控制器父节点*/
interrupt-parent = <&gpio0>; // 引用gpio0作为中断控制器父节点
interrupts = <RK_PB0 IRQ_TYPE_LEVEL_LOW>; // 配置中断引脚和触发方式(低电平触发)
};
};
/*定义pinctrl引脚控制器*/
&{/pinctrl} {
pinctrl_button {
button_interrupt_pin: button_interrupt_pin {
rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; // 配置引脚为GPIO功能,无上下拉
};
};
};
14.1.2 按键中断驱动程序实现
该驱动为简单字符设备驱动,虽使用设备树插件,但不与设备树节点匹配(此与 "读设备树" 无关)。
驱动源码主要包含两部分:驱动入口 / 出口函数实现,以及字符设备操作函数实现。
14.1.2.1 驱动入口和出口函数实现
按键驱动初始化流程:
动态分配设备号(alloc_chrdev_region)
初始化 cdev 并关联文件操作集绑定(cdev_init)
注册字符设备到系统(cdev_add)
添加设备到内核管理的设备列表,内核可识别并调度该设备
创建设备类(class_create)
在 /sys/class/ 下创建类目录,统一管理同类设备的属性。
创建设备节点(device_create)
在 /dev/ 下生成设备文件,供用户空间程序通过文件操作访问硬件
错误处理:逐层释放已分配资源
cpp
/*
* 驱动初始化函数
*/
static int __init button_driver_init(void)
{
int error = -1;
/* 采用动态分配的方式,获取设备编号,次设备号为 0 */
error = alloc_chrdev_region(&button_devno, 0, DEV_CNT, DEV_NAME);
if (error < 0) {
printk("fail to alloc button_devno\n");
goto alloc_err;
}
/* 关联字符设备结构体 cdev 与文件操作结构体 file_operations */
button_chr_dev.owner = THIS_MODULE;
cdev_init(&button_chr_dev, &button_chr_dev_fops);
/* 添加设备至 cdev_map 散列表中 */
error = cdev_add(&button_chr_dev, button_devno, DEV_CNT);
if (error < 0) {
pr_err("fail to add cdev\n");
goto cdev_add_err;
}
/* 创建设备类 */
class_button = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(class_button)) {
pr_err("fail to create class\n");
error = PTR_ERR(class_button);
goto class_create_err;
}
/* 创建设备节点 */
device_button = device_create(class_button, NULL, button_devno, NULL, DEV_NAME);
if (IS_ERR(device_button)) {
pr_err("fail to create device\n");
error = PTR_ERR(device_button);
goto device_create_err;
}
pr_info("button driver init success\n");
return 0;
// 错误处理流程
device_create_err:
class_destroy(class_button);
class_create_err:
cdev_del(&button_chr_dev);
cdev_add_err:
unregister_chrdev_region(button_devno, DEV_CNT);
alloc_err:
return error;
}
/*
* 驱动注销函数
*/
static void __exit button_driver_exit(void)
{
pr_info("button_driver_exit\n");
/* 清除设备相关资源 */
device_destroy(class_button, button_devno); // 清除设备
class_destroy(class_button); // 清除类
cdev_del(&button_chr_dev); // 清除字符设备
unregister_chrdev_region(button_devno, DEV_CNT); // 取消注册字符设备号
}
module_init(button_driver_init); // 注册驱动入口函数
module_exit(button_driver_exit); // 注册驱动出口函数
MODULE_LICENSE("GPL"); // 模块许可证(GPL协议)
MODULE_DESCRIPTION("embedfire lubuncat2_RK, interrupt"); // 模块描述信息
MODULE_AUTHOR("Your Name"); // 作者信息
/sys/class 和 /dev 的区别

14.1.2.2 .open 函数实现
通过 of 函数从设备树找节点,
通过 of 函数,节点指针和属性名获取设备树节点配置的 GPIO 编号。
通过 GPIO 函数申请 GPIO资源。
通过 GPIO 函数配置 GPIO为输入模式。(复用和初始上下拉在设备树中通过pinctrl定义了)
通过 irq_of_parse_and_map 获取对应中断索引的中断配置,转换为内核中断号。
通过 request_irq 申请中断,绑定中断号和中断服务函数,设置触发方式。

cpp
irq_of_parse_and_map(button_device_node, //节点
0); //中断索引
cpp
static int button_open(struct inode *inode, struct file *filp)
{
int error = -1;
/* 1. 从设备树中查找按键对应的节点(路径为"/button_interrupt") */
button_device_node = of_find_node_by_path("/button_interrupt");
if (NULL == button_device_node) {
printk("of_find_node_by_path error!");
return -1;
}
/* 2. 从设备树节点中获取"button-gpios"属性配置的GPIO编号 */
button_GPIO_number = of_get_named_gpio(button_device_node, "button-gpios", 0);
if (0 == button_GPIO_number) {
printk("of_get_named_gpio error");
return -1;
}
/* 3. 申请GPIO资源(失败则释放资源) */
error = gpio_request(button_GPIO_number, "button_gpio");
if (error < 0) {
printk("gpio_request error");
gpio_free(button_GPIO_number);
return -1;
}
/* 4. 配置GPIO为输入模式(按键读取状态) */
error = gpio_direction_input(button_GPIO_number);
/* 5. 解析设备树节点的中断信息,获取中断号 */
interrupt_number = irq_of_parse_and_map(button_device_node, 0);
printk("\n irq_of_parse_and_map! = %d \n", interrupt_number);
/* 6. 申请中断(上升沿触发),绑定中断处理函数button_irq_hander */
error = request_irq(interrupt_number, button_irq_hander,
IRQF_TRIGGER_RISING, "button_interrupt", NULL);
if (error != 0) {
printk("request_irq error");
free_irq(interrupt_number, NULL);
return -1;
}
return 0;
}
14.1.2.3 中断服务函数实现
在 open 函数申请中断时要指定中断服务函数,
cpp
// 定义整型原子变量,保存按键状态(初始值为 0)
atomic_t button_status = ATOMIC_INIT(0);
// 按键中断处理函数:中断触发时执行
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{
atomic_inc(&button_status); // 原子操作:按键状态计数+1(避免并发竞争)
return IRQ_HANDLED; // 告知内核中断已处理完成
}
14.1.2.4 .read 和.release 函数实现
.read 函数的工作是向用户空间返回按键状态值,
.release 函数实现退出之前的清理工作。
cpp
// 读取按键状态(用户空间读取设备时调用)
static int button_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int error = -1;
int button_countervc = 0;
button_countervc = atomic_read(&button_status); // 读取原子变量中的按键触发次数
// 将状态值拷贝到用户空间缓冲区
error = copy_to_user(buf, &button_countervc, sizeof(button_countervc));
if (error < 0) {
printk_red("copy_to_user error");
return -1;
}
atomic_set(&button_status, 0); // 读取后清零状态值,准备下次计数
return 0;
}
// 释放设备(用户空间关闭设备时调用)
static int button_release(struct inode *inode, struct file *filp)
{
gpio_free(button_GPIO_number); // 释放之前申请的GPIO资源
free_irq(interrupt_number, device_button); // 释放申请的中断
return 0;
}
14.1.3 测试应用程序实现
前面增加了按键的设备树,并配置了中断引脚,在 open函数中申请了中断,设置了触发方式,并关联了中断回调函数。
测试应用程序工作是读取按键状态然后打印状态。
测试程序直接循环读 /dev/下的文件,通过read获取 读函数通过 copy_to_user拷贝到用户空间的值即可。
cpp
int main(int argc, char *argv[])
{
int error = -20;
int button_status = 0;
// 打开按键设备文件(/dev/button)
int fd = open("/dev/button", O_RDWR);
if (fd < 0) {
printf("open file : /dev/button error!\n");
return -1;
}
printf("wait button down... \n");
// 循环读取按键状态,直到检测到按键触发
do {
// 读取驱动中的按键状态值
error = read(fd, &button_status, sizeof(button_status));
if (error < 0) {
printf("read file error! \n");
}
usleep(1000 * 100); // 延时100毫秒,降低CPU占用
} while (0 == button_status);
printf("button Down !\n");
// 关闭设备文件
error = close(fd);
if (error < 0) {
printf("close file error! \n");
}
return 0;
}
14.1.4 实验准备
板卡部分 GPIO 可能被系统占用,导致设备树加载失败或驱动资源申请失败(如提示 "Device or resource busy" 或卡死),可注释其他设备树插件解决。
编译设备树插件步骤:
将插件文件放入内核目录/arch/arm64/boot/dts/rockchip/overlays;
修改该目录下的 Makefile,添加插件编译配置。
源文件放到Makefile同一目录,
在Makefile增加要编译的设备树插件
在内核根目录下执行如下指令,
bash
# 加载针对lubancat2板卡预定义的配置文件
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig
# 编译设备树文件
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs
内核编译依赖严格的目录结构(如arch/、drivers/、Makefile等核心文件均在根目录),且编译过程会读取根目录的Makefile和配置文件(如.config,由第一步生成),并按内核目录规则查找设备树源文件(如arch/arm64/boot/dts/下的.dts文件)。
编译后生成的 lubancat-button-overlay.dtbo
位于内核目录 arch/arm64/boot/dts/rockchip/overlays 下,获取该文件后,加载至系统即可。
14.1.4.1 添加设备树插件文件
将编译生成的 lubancat-button-overlay.dtbo 放入板卡 /boot/dtb/overlays/ 目录;
编辑 /boot/uEnv/uEnv.txt,按 dtoverlay=<设备树插件路径> 格式添加加载配置;
重启开发板,执行 ls /proc/device-tree/,若存在 button_interrupt 目录则加载成功(UBoot 加载细节参考环境搭建章节)。
/proc/device-tree/ 是内核在系统启动后,将设备树(Device Tree)信息以文件系统形式暴露给用户空间的目录。
14.1.4.2 编译驱动程序及测试程序
bash
# 定义内核源码目录路径(相对当前目录的上级目录下的kernel文件夹)
KERNEL_DIR = ../../kernel/
# 指定目标架构为ARM64,交叉编译工具链前缀
ARCH = arm64
CROSS_COMPILE = aarch64-linux-gnu-
export ARCH CROSS_COMPILE # 导出环境变量,供后续编译使用
# 指定要编译的内核模块(.o文件名为interrupt,对应源码interrupt.c)
obj-m := interrupt.o
# 指定用户态测试程序输出文件名
out = test_app
# 默认编译目标:同时编译内核模块和用户态程序
all:
# 编译内核模块(-C指定内核目录,M指定当前模块源码目录)
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
# 编译用户态测试程序test_app.c,生成可执行文件test_app
$(CROSS_COMPILE)gcc -o $(out) test_app.c
# 伪目标:清理编译产物
.PHONY: clean
clean:
# 清理内核模块编译产物(.ko、.o等)
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
# 删除用户态测试程序可执行文件
rm -f $(out)
将 interrupt 驱动代码放在内核同级目录,进入驱动目录执行编译命令,即可编译驱动模块与测试程序。
cpp
make
14.1.5 实验现象
编译好的驱动、应用程序及设备树插件,按前文方法拷贝至开发板。
加载模块前,检查 /boot/uEnv.txt,注释原有 KEY 相关设备树插件,确保已添加按键中断设备树插件,重启开发板即可。
cpp
ls /proc/device-tree

加载内核模块,执行测试程序:

查看 /boot/uEnv.txt 中是否有 dtoverlay= 开头且含 KEY 相关的配置,即可判断是否加载原有 KEY 设备树插件;
无按键时,可通过杜邦线短接 40pin 的 GND 与 3.3V 模拟引脚电平(加载驱动并将 IO 设为输入后操作,避免接超 3.3V 电源损坏主控);
/boot/uEnv.txt 和 /boot/uEnv/uEnv.txt的作用类似
执行 test_app 时,可通过对应命令查看驱动加载及初始化状态。
cpp
cat /proc/interrupts

/proc/interrupts查看中断
14.2 中断使用进阶
Linux 中断有两个核心规则:不支持中断嵌套、中断服务函数需快进快出。
对于网络传输等需耗时处理的中断,为避免阻塞其他中断响应,Linux 引入 "中断上下半部" 机制:上半部在中断服务函数中执行简单处理,耗时操作放到下半部执行,提升系统实时性。
并非所有中断都需分层,如按键中断这类简单处理,仅用上半部即可。
下半部实现机制包括软中断 、tasklet 、工作队列 、线程 irq,下面通过模拟耗时操作,学习中断分层的使用。
14.2.1 软中断和 tasklet
tasklet 是基于软中断实现,它们有很多相似之处,我们把它两个放到一块介绍。

tasklet和软中断的底层差异
14.2.1.1 软中断 (softirq)
Linux 4.xx 内核支持的软中断数量有限(仅十个左右,具体因内核版本略有差异),内核中通过枚举变量枚举所有可用软中断。
cpp
/*软中断的中断种类*/
enum {
HI_SOFTIRQ = 0, // 高优先级软中断
TIMER_SOFTIRQ, // 定时器相关软中断
NET_TX_SOFTIRQ, // 网络发送软中断
NET_RX_SOFTIRQ, // 网络接收软中断
BLOCK_SOFTIRQ, // 块设备软中断
IRQ_POLL_SOFTIRQ, // 中断轮询软中断
TASKLET_SOFTIRQ, // tasklet机制对应的软中断
SCHED_SOFTIRQ, // 调度相关软中断
HRTIMER_SOFTIRQ, // 高精度定时器软中断(未使用,保留编号兼容工具)
RCU_SOFTIRQ, // RCU(读写锁)软中断(建议作为最后一个软中断)
NR_SOFTIRQS // 软中断总数(用于边界检查)
};
该枚举类似硬中断的中断编号,是软中断注册和触发的标识。
软中断注册函数
软中断注册函数如下:
cpp
// 注册软中断处理函数
void open_softirq(int nr, //指定要"注册"的软中断中断编号
void (*action)(struct softirq_action *)) //指定软中断的中断服务函数
{
softirq_vec[nr].action = action; // 将处理函数绑定到指定软中断编号(nr)
}
函数实现仅一条赋值语句,核心在于 softirq_vec 变量,
内核源码中该变量定义如下:
cpp
static struct softirq_action softirq_vec[NR_SOFTIRQS]
softirq_vec 是长度为 NR_SOFTIRQS(10)的 struct softirq_action 类型全局数组,相当于软中断的中断向量表。
其元素类型定义如下:
cpp
struct softirq_action {
void (*action)(struct softirq_action *);
};
struct softirq_action 仅含一个参数(即 open_softirq 注册的软中断函数)。
可见,softirq_vec 数组就是软中断的中断向量表,注册软中断本质 是按中断号将服务函数地址写入该数组对应位置。
软中断触发函数
软中断注册后需调用触发函数来触发,以执行其服务函数,函数如下:
cpp
void raise_softirq(unsigned int nr);//要触发的软中断号
14.2.1.2 taskle
tasklet 结构体
cpp
/*tasklet_struct 是内核中描述 tasklet 的核心结构体,
通过链表管理,状态和计数控制调度时机,回调函数与参数实现具体功能。*/
struct tasklet_struct {
struct tasklet_struct *next; // 链表节点,用于链接多个tasklet
unsigned long state; // 状态位
// state 为 0:未被调度;
// TASKLET_STATE_SCHED:已调度,待运行;
// TASKLET_STATE_RUN:正在运行。
atomic_t count; // 引用计数(0表示可调度,非0表示被禁用)
void (*func)(unsigned long); // tasklet执行的回调函数
unsigned long data; // 传递给回调函数的参数
};
tasklet 初始化函数
cpp
void tasklet_init(struct tasklet_struct *t, //tasklet结构体
void (*func)(unsigned long), //tasklet回调函数,等同于中断处理函数
unsigned long data) { //tasklet回调函数的参数
t->next = NULL;
t->state = 0; // 初始化为未调度状态
atomic_set(&t->count, 0); // 初始化为可调度(引用计数0)
t->func = func; // 绑定回调函数
t->data = data; // 设置回调函数参数
}
tasklet 触发函数
和软中断一样,需要一个触发函数触发 tasklet
cpp
static inline void tasklet_schedule(struct tasklet_struct *t) {
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t); // 若未被调度,则标记为待调度并实际调度
}
14.2.1.3 tasklet 中断分层实验
tasklet 使用四步:
定义 struct tasklet_struct 结构体对象 →
写处理函数 →
tasklet_init 初始化绑定 tasklet结构体对象和处理函数 →
在上半部中断使用tasklet_schedule 调度触发 tasklet对象。
cpp
/* 1. 定义 tasklet 对象 */
static struct tasklet_struct btn_tl;
/* 2. tasklet 底半部处理函数 */
static void btn_tl_func(unsigned long d)
{
int c = 1;
/* 连续打印 5 次,每次延时 200ms */
while (c <= 5) {
mdelay(200);
printk(KERN_ERR "btn_tl_func counter=%d\n", c++);
}
}
/* 3. 打开设备时初始化 tasklet */
static int btn_open(struct inode *i, struct file *f)
{
tasklet_init(&btn_tl, btn_tl_func, 0);
return 0;
}
/* 4. 按键中断顶半部:登记 tasklet */
static irqreturn_t btn_irq(int irq, void *id)
{
atomic_inc(&btn_status); /* 记录按键状态 */
tasklet_schedule(&btn_tl); /* 触发底半部 */
return IRQ_HANDLED;
}
14.2.1.4 下载验证
编译添加设备树插件
按照前面的章节,通过设备树插件的形式,
在根节点下新增按键中断设备节点,
由中断引脚设备节点的 pinctrl-names关联引脚复用配置集。
关联到通过 pinctrl子系统配置引脚复用和上下拉。
cpp
/dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/interrupt-controller/irq.h>
&{/} {
/*在根节点下新增定义按钮中断设备节点*/
button_interrupt: button_interrupt {
status = "okay";
compatible = "button_interrupt";
/*配置按键GPIO引脚*/
button-gpios = <&gpio0 RK_PB0 GPIO_ACTIVE_LOW>;
/*引脚配置集名称,默认配置*/
pinctrl-names = "default";
/*关联引脚配置节点,引用下方的button_interrupt_pin节点*/
pinctrl-0 = <&button_interrupt_pin>;
/*引用中断控制器父节点*/
interrupt-parent = <&gpio0>; // 引用gpio0作为中断控制器父节点
interrupts = <RK_PB0 IRQ_TYPE_LEVEL_LOW>; // 配置中断引脚和触发方式(低电平触发)
};
};
/*定义pinctrl引脚控制器*/
&{/pinctrl} {
pinctrl_button {
button_interrupt_pin: button_interrupt_pin {
rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>; // 配置引脚为GPIO功能,无上下拉
};
};
};
编译设备树插件步骤:
将插件文件放入内核目录/arch/arm64/boot/dts/rockchip/overlays;
修改该目录下的 Makefile,添加插件编译配置。
源文件放到Makefile同一目录,
在Makefile增加要编译的设备树插件
在内核根目录下执行如下指令,
bash
# 加载针对lubancat2板卡预定义的配置文件
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig
# 编译设备树文件
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs
内核编译依赖严格的目录结构(如arch/、drivers/、Makefile等核心文件均在根目录),且编译过程会读取根目录的Makefile和配置文件(如.config,由第一步生成),并按内核目录规则查找设备树源文件(如arch/arm64/boot/dts/下的.dts文件)。
编译后生成的 lubancat-button-overlay.dtbo
位于内核目录 arch/arm64/boot/dts/rockchip/overlays 下,获取该文件后,加载至系统即可。
将编译生成的 lubancat-button-overlay.dtbo 放入板卡 /boot/dtb/overlays/ 目录;
编辑 /boot/uEnv/uEnv.txt,按 dtoverlay=<设备树插件路径> 格式添加加载配置;
重启开发板,执行 ls /proc/device-tree/,若存在 button_interrupt 目录则加载成功(UBoot 加载细节参考环境搭建章节)。
/proc/device-tree/ 是内核在系统启动后,将设备树(Device Tree)信息以文件系统形式暴露给用户空间的目录。
编译加载驱动
makefile参考前面的写即可,此处不再赘述。
测试文件也不再赘述。
按下按键后触发tasklet软中断,仅单cpu执行,无需处理多cpu并发和重入问题
14.2.2 工作队列
工作队列基于内核线程,可睡眠与调度;
用法与 tasklet 类似:定义、初始化、触发。
14.2.2.1 工作结构体
工作队列对内透明,用户只需定义并初始化工作结构体即可提交任务。
cpp
struct work_struct {
atomic_long_t data; // 私有数据(通常不用)
struct list_head entry; // 链表节点(内核挂队列用)
work_func_t func; // 工作函数指针(用户实现)
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map; // 调试锁依赖(可忽略)
#endif
};
重点关心参数 "work_func_t func;"
该参数用于指定"工作"的处理函数。
cpp
void (*work_func_t)(struct work_struct *work);
14.2.2.2 工作初始化函数
内核提初始化宏定义如下所示。
cpp
#define INIT_WORK(_work, // _work 用于指定要初始化的工作结构体
_func) // _func 用于指定工作的处理函数。
14.2.2.3 启动工作函数
内核线程在提交的工作被调度时,调用 work_struct.func 完成下半部任务。
cpp
static inline bool schedule_work(struct work_struct *work)
{
// 把 work 挂到系统默认工作队列,稍后内核线程回调 func
return queue_work(system_wq, work);
}
14.2.2.4 工作队列实验
工作队列实验:仅改驱动,在按键中断里把下半部换成 work_struct,其余照旧。
cpp
/* 1. 定义工作 */
static struct work_struct button_work;
/* 2. 工作函数:可睡眠,顺序打印 5 次计数值 */
static void work_hander(struct work_struct *work)
{
int counter = 1;
for (; counter <= 5; ++counter) {
mdelay(200); //delay是忙等待不是睡眠,不会让出cpu使用权
printk(KERN_ERR "work_hander counter = %d\n", counter);
}
}
/* 3. 打开设备时初始化工作 */
static int button_open(struct inode *inode, struct file *filp)
{
INIT_WORK(&button_work, work_hander); // 绑定处理函数
return 0;
}
/* 4. 中断上半部:计数+1,提交工作 */
static irqreturn_t button_irq_hander(int irq, void *dev_id)
{
atomic_inc(&button_status); // 快速标记按键触发
schedule_work(&button_work); // 把下半部交工作队列
return IRQ_HANDLED;
}
按键测试程序实验结果
14.2.3 线程化 IRQ
中断线程化:
用 devm_request_threaded_irq() 把中断变成可调度、可设优先级的内核线程,高负载时让路给更实时任务;
直接替换原 request_irq() 即可。
cpp
int request_threaded_irq(unsigned int irq, //中断号
irq_handler_t handler, // 硬中断上半部(快速处理,NULL 则内核用默认)
irq_handler_t thread_fn, // 线程化下半部(可睡眠,NULL 退化为传统中断)
unsigned long flags, // 中断标志,如 IRQF_ONESHOT
const char *name, // 设备名(/proc/interrupts 显示)
void *dev); // 私有数据,中断时透传
第 15 章 输入子系统
Linux 输入子系统统一接管所有输入设备;
驱动只需上报事件,应用层见同一张"输入"接口。
下文用 GPIO 按键示例,源码在 /linux_driver/input_sub_system。
15.1 输入子系统简介
输入子系统三层:
驱动层 → 核心层 → 事件处理层,
统一上报输入事件。
各类物理设备(USB、PS/2、Serial...)→
Drivers →
Input Core →
Handlers →
对应字符设备节点(/dev/input/eventX、mouseX、jsX 等),
最终到达用户空间。
驱动层:访问硬件、设中断,把事件按 Input Core 规范上报。
核心层:中转站,统一格式并派发。
处理层:纯软件,按设备类型生成应用可见的输入节点。
Input Core 是内核输入子系统的"中转站":
给驱动规定统一的事件格式(input_event 结构)和上报接口(input_report_*)。
把驱动报来的事件转发给合适的 Handler,由 Handler 生成 /dev/input/eventX 等节点供用户空间读取。
输入事件结构体
最终所有输入设备的输入信息将被抽象成以下结构体:
cpp
/*struct input_event 结构体 (内核源码/include/uapi/linux/input.h)*/
struct input_event {
struct timeval time; // 时间戳
__u16 type; // 事件类别(EV_KEY/EV_ABS/EV_REL...)
__u16 code; // 具体部件(按键码/轴号...)
__s16 value; // 状态值(按下/抬起/坐标/位移...)
};
本节目标:用 input 层 API 把按键中断包装成标准输入事件。
核心文件:input.c + input.h;几组函数完成注册→上报→注销即可。
15.1.1 input_dev 结构体
struct input_dev 是输入子系统中"一个输入设备"的内核对象;
按需填充其关键字段(name、id、evbit、keybit 等)后注册即可,其余成员内核自动维护。
cpp
struct input_dev {
const char *name; // 设备名(用户可见)
const char *phys; // 物理路径(sysfs 节点名)
const char *uniq; // 唯一标识串
struct input_id id; // 总线/厂商/产品/版本号
unsigned long propbit[BITS_TO_LONGS(INPUT_PROP_CNT)]; // 设备属性
unsigned long evbit[BITS_TO_LONGS(EV_CNT)]; // 支持的事件类型(EV_KEY/EV_REL...)
unsigned long keybit[BITS_TO_LONGS(KEY_CNT)]; // 支持的按键码表
unsigned long relbit[BITS_TO_LONGS(REL_CNT)]; // 支持的相对轴
unsigned long absbit[BITS_TO_LONGS(ABS_CNT)]; // 支持的绝对轴
unsigned long mscbit[BITS_TO_LONGS(MSC_CNT)]; // 杂项事件
unsigned long ledbit[BITS_TO_LONGS(LED_CNT)]; // LED 控制
unsigned long sndbit[BITS_TO_LONGS(SND_CNT)]; // 声音反馈
unsigned long ffbit[BITS_TO_LONGS(FF_CNT)]; // 力反馈
unsigned long swbit[BITS_TO_LONGS(SW_CNT)]; // 开关状态
/* 其余成员由内核维护,驱动通常不直接访问 */
};
BITS_TO_LONGS(n) 把位数 n 换算成需要多少个 unsigned long 才能装下这些位------即数组长度,保证位图足够覆盖所有标志位。
关键位图: evbit ------ 先声明设备能产哪类事件(EV_KEY/EV_REL/EV_ABS...)
keybit/relbit/absbit... ------ 再细化具体键码或轴号。
cpp
#define EV_SYN 0x00 // 同步事件(包结尾)
#define EV_KEY 0x01 // 按键/按钮
#define EV_REL 0x02 // 相对坐标(如鼠标)
#define EV_ABS 0x03 // 绝对坐标(如触摸屏)
#define EV_MSC 0x04 // 杂项
#define EV_SW 0x05 // 开关
#define EV_LED 0x11 // LED 控制
#define EV_SND 0x12 // 声音
#define EV_REP 0x14 // 自动重复
#define EV_FF 0x15 // 力反馈
#define EV_PWR 0x16 // 电源
#define EV_FF_STATUS 0x17 // 力反馈状态
#define EV_MAX 0x1f
#define EV_CNT (EV_MAX + 1)
常用类型见上,完整列表见 Documentation/input/event-codes.rst;
按键选 EV_KEY,再用 keybit 置位对应键码即可。
cpp
#define KEY_RESERVED 0
#define KEY_ESC 1
#define KEY_1 2
#define KEY_2 3
#define KEY_3 4
#define KEY_4 5
/* 更多键值省略 */
键值即数字,设备按键对应即可。
若 evbit 仅选 EV_KEY,则无需设置 relbit、absbit 等,按需配置,input_dev 成员虽多,仅用所需。
设置键码的意义
15.1.2 input_dev 结构体的申请和释放
cpp
#申请 input_dev
struct input_dev *input_allocate_device(void)
cpp
#释放input_dev
struct input_dev *input_allocate_device(void)
input_dev 对应一个输入设备,占一个次设备号。
用子系统提供的接口统一申请/释放,避免手工初始化麻烦。
定义 input_dev结构体 ->
kzalloc申请内存 ->
填充字段 ->
初始化 ->
一系列并发原语初始化 ->
链表头初始化->sys/devices/...路径生成->引用计数
cpp
struct input_dev *input_allocate_device(void)
{
static atomic_t input_no = ATOMIC_INIT(-1);
struct input_dev *dev;
/* 1. 申请内存 */
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return NULL;
/* 2. 嵌入的 struct device 初始化 */
dev->dev.type = &input_dev_type;
dev->dev.class = &input_class;
device_initialize(&dev->dev);
/* 3. 各种同步原语 */
mutex_init(&dev->mutex); /* 结构体互斥 */
spin_lock_init(&dev->event_lock); /* 事件自旋锁 */
timer_setup(&dev->timer, NULL, 0);/* 自动重复定时器 */
/* 4. 链表头 */
INIT_LIST_HEAD(&dev->h_list); /* 关联的 handle 链表 */
INIT_LIST_HEAD(&dev->node); /* 全局 input_dev 链表 */
/* 5. 生成 /sys/devices/.../inputN 名称 */
dev_set_name(&dev->dev, "input%lu",
(unsigned long)atomic_inc_return(&input_no));
/* 6. 模块引用计数 */
__module_get(THIS_MODULE);
return dev;
}
15.1.3 注册和注销 input_dev 结构体
input_dev 使用 input_alloc_device申请成功后,
需按实际输入设备配置 input_dev 结构体,
配置完成后通过对应注册、注销函数,将其接入或移出输入子系统,
相关函数如下:
cpp
/*input_dev注册函数*/
int input_register_device(struct input_dev *dev)
input_register_device 用于将 input_allocate_device 申请到的 input_dev 注册进输入子系统。
注册前需完成 input_dev 配置。
注册失败→input_free_device 释放。
注册成功→仅需input_unregister_device 注销,勿再 free。
cpp
/*注销函数*/
void input_unregister_device(struct input_dev *dev)
15.1.4 上报事件函数和上报结束函数
cpp
void input_event(struct input_dev *dev, //指定输入设备 input_dev结构体
unsigned int type, //事件类型 比如配置dev时候给的是evbit,则填evbit类型
unsigned int code, //按键键值编码
int value); //指定事件的值
cpp
/* 告知核心层:完整帧事件已结束 */
static inline void input_sync(struct input_dev *dev)
{
input_event(dev, EV_SYN, SYN_REPORT, 0);
}
/* 上报按键状态;value 被规整为 0/1 */
static inline void input_report_key(struct input_dev *dev,
unsigned int code, int value)
{
input_event(dev, EV_KEY, code, !!value);
}
input 子系统接口皆封装自 input_event:
input_report_key 上报按键;
input_sync 发送帧结束同步信号。
15.2 输入子系统实验
本章以 GPIO 按键为例,演示输入子系统两种用法:
轮询:定时读 GPIO 电平,上报按键状态。
中断:GPIO 触发中断时检测电平并上报事件。
示例代码采用中断方式,源码与设备树插件见 /linux_driver/input_sub_system。
15.2.1 设备树插件实现
沿用上一章中断实验设备树插件,仅把中断触发改为"双边沿(上升+下降)"。
cpp
/dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/interrupt-controller/irq.h>
/* 按键节点:双边沿中断,GPIO0_B0 */
&{/} {
input_button {
status = "okay";
compatible = "input_button";
button-gpios = <&gpio0 RK_PB0 GPIO_ACTIVE_HIGH>;//引脚可根据具体板卡修改
pinctrl-names = "default";
pinctrl-0 = <&input_button_pin>;
interrupt-parent = <&gpio0>;
interrupts = <RK_PB0 IRQ_TYPE_EDGE_BOTH>;
};
};
/* 引脚配置:无上下拉 */
&{/pinctrl} {
pinctrl_button {
input_button_pin: input_button_pin {
rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>;
};
};
};
15.2.2 驱动程序实现
15.2.2.1 驱动入口函数
cpp
static int button_probe(struct platform_device *pdev)
{
struct button_data *priv;
struct gpio_desc *gpiod;
struct input_dev *i_dev;
int ret;
pr_info("button_probe\n");
// 分配私有数据结构
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
// 分配输入设备
i_dev = input_allocate_device();
if (!i_dev)
return -ENOMEM;
// 设置输入设备的回调函数和属性
i_dev->open = btn_open;
i_dev->close = btn_close;
i_dev->name = "key input";
i_dev->dev.parent = &pdev->dev;
priv->button_input_dev = i_dev;
priv->pdev = pdev;
// 设置输入事件类型和按键值
set_bit(EV_KEY, i_dev->evbit); // 设置事件类型为按键事件
set_bit(BTN_0, i_dev->keybit); // 设置按键值为BTN_0
// 获取GPIO并设置为输入
gpiod = gpiod_get(&pdev->dev, "button", GPIOD_IN);
if (IS_ERR(gpiod))
return -ENODEV;
// 获取GPIO中断号
priv->irq = gpiod_to_irq(gpiod);
priv->button_input_gpiod = gpiod;
// 注册输入设备
ret = input_register_device(priv->button_input_dev);
if (ret) {
pr_err("Failed to register input device\n");
goto err_input;
}
// 设置平台驱动数据
platform_set_drvdata(pdev, priv);
// 申请GPIO中断
ret = request_any_context_irq(priv->irq, button_input_irq_handler,
IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
"input-button", priv);
if (ret < 0) {
dev_err(&pdev->dev, "Failed to request GPIO interrupt\n");
goto err_btn;
}
return 0;
err_btn:
gpiod_put(priv->button_input_gpiod);
err_input:
input_free_device(priv->button_input_dev);
return ret;
}

cpp
struct gpio_desc *__must_check gpiod_get(struct device *dev, // 设备指针
// 引脚组名称 (不包含前缀)。
//如引脚组名为 button-gpios。则 con_id="button"。
const char *con_id,
enum gpiod_flags flags) // GPIO标志
{
return gpiod_get_index(dev, con_id, 0, flags); // 调用 gpiod_get_index 获取 GPIO
}
cpp
enum gpiod_flags {
GPIOD_ASIS = 0, // 不初始化 GPIO,方向必须使用其他函数设置
GPIOD_IN = GPIOD_FLAGS_BIT_DIR_SET, // 初始化 GPIO 为输入
GPIOD_OUT_LOW = GPIOD_FLAGS_BIT_DIR_SET | GPIOD_FLAGS_BIT_DIR_OUT, // 初始化 GPIO 为输出,输出低电平
GPIOD_OUT_HIGH = GPIOD_FLAGS_BIT_DIR_SET | GPIOD_FLAGS_BIT_DIR_OUT | GPIOD_FLAGS_BIT_DIR_VAL, // 初始化 GPIO 为输出,输出高电平
GPIOD_OUT_LOW_OPEN_DRAIN = GPIOD_OUT_LOW | GPIOD_FLAGS_BIT_OPEN_DRAIN, // 初始化 GPIO 为开漏输出,输出低电平
GPIOD_OUT_HIGH_OPEN_DRAIN = GPIOD_OUT_HIGH | GPIOD_FLAGS_BIT_OPEN_DRAIN, // 初始化 GPIO 为开漏输出,输出高电平
};
15.2.2.2 驱动出口函数
cpp
static int button_remove(struct platform_device *pdev)
{
struct button_data *priv; // 获取私有数据结构
priv = platform_get_drvdata(pdev); // 从平台设备中获取私有数据
input_unregister_device(priv->button_input_dev); // 注销输入设备
input_free_device(priv->button_input_dev); // 释放输入设备资源
free_irq(priv->irq, priv); // 释放中断资源
gpiod_put(priv->button_input_gpiod); // 释放 GPIO 资源
return 0; // 返回成功
}
15.2.2.3 中断服务函数
中断服务函数中我们读取按键输入引脚的状态判断按键是按下还是松开。
cpp
static irqreturn_t button_input_irq_handler(int irq, void *dev_id)
{
struct button_data *priv = dev_id; // 获取私有数据
int button_status; // 按键状态变量
// 读取 GPIO 引脚电平,判断按键状态
button_status = (gpiod_get_value(priv->button_input_gpiod) & 1);
if (button_status) {
// 按键按下
input_report_key(priv->button_input_dev, BTN_0, 1);
} else {
// 按键释放
input_report_key(priv->button_input_dev, BTN_0, 0);
}
input_sync(priv->button_input_dev); // 同步输入事件
return IRQ_HANDLED; // 返回中断已处理
}
15.2.3 测试应用程序实现
测试应用程序中读取按键键值,打印按键状态。
cpp
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <linux/input.h> // 包含 input_event 结构体定义
struct input_event button_input_event;
int main(int argc, char *argv[])
{
int error = -20;
int fd = open("/dev/input/event4", O_RDONLY); // 打开输入设备文件
if (fd < 0) {
printf("open file: /dev/input/event4 error!\n");
return -1;
}
printf("wait button down...\n");
do {
error = read(fd, &button_input_event, sizeof(button_input_event)); // 读取按键事件
if (error < 0) {
printf("read file error!\n");
}
// 判断按键事件类型和代码
if ((button_input_event.type == EV_KEY) && (button_input_event.code == 0x100)) {
if (button_input_event.value == 0) {
printf("button up\n");
} else if (button_input_event.value == 1) {
printf("button down\n");
}
}
} while (1);
// 关闭文件
error = close(fd);
if (error < 0) {
printf("close file error!\n");
}
return 0;
}
15.2.4 实验准备
在使用板卡的 GPIO 时,需确保所用 GPIO 未被其他驱动占用,否则可能导致"Device or resource busy"错误或程序卡死。
若出现"Permission denied",需以 root 用户权限运行程序,例如使用 sudo。
若需使用自定义设备树插件(如 lubancat-button-input-overlay.dts),需将其放置在内核目录 /arch/arm64/boot/dts/rockchip/overlays 下,修改该目录下的 Makefile 文件,添加插件进行编译。
在内核根目录下执行以下命令即可。
bash
# 生成 lubancat2 的默认配置
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig
# 构建设备树二进制文件 (dtbs)
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

生成的 .dtbo 文件位于内核根目录下的 arch/arm64/boot/dts/rockchip/overlays 目录中。
例如,设备树插件 lubancat-button-input-overlay.dts 编译后会生成同名的 lubancat-button-input-overlay.dtbo 文件。
得到 .dtbo 文件后,下一步是将其加载到系统中。
15.2.4.1 添加设备树插件文件

15.2.4.2 编译驱动程序及测试程序
bash
KERNEL_DIR = ../../kernel/
ARCH = arm64
CROSS_COMPILE = aarch64-linux-gnu-
export ARCH CROSS_COMPILE
obj-m := input_sub_system.o
out := test_app
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
$(CROSS_COMPILE)gcc -o $(out) test_app.c
.PHONY: clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
rm -f $(out)
15.2.5 加载驱动验证

bash
#查看申请的中断
cat /proc/interrupts

bash
#查看设备信息
udevadm info /dev/input/event4

驱动加载成功后,运行测试程序 ./test_app,按下按键(或改变引脚电平),终端会输出按键状态。

也可以使用 extest命令测试,
cpp
/* /dev/input/event4 是根据自己加载驱动, 增加的设备文件 */
sudo evtest /dev/input/event4
//evtest 工具用于读取和显示 Linux 输入设备的事件。

第 16 章 PWM 子系统
PWM 子系统用于管理 PWM 波输出,其具体实现代码通常由芯片厂商提供并编译进内核。
我们可以通过内核提供的接口函数使用 PWM,例如控制显示屏背光、蜂鸣器、伺服电机等。
PWM 子系统功能较为单一,通常不单独使用。
本章通过一个简单的 PWM 驱动示例,展示使用 PWM 控制 LED 灯光或输出 PWM 波形。
16.1 PWM 子系统简介
PWM(脉冲宽度调制),内核 PWM 驱动虽简洁,但基础框架完整。
PWM框架
这张图展示了Linux 内核中 PWM 子系统的分层架构与交互流程,可从三层精简解释:
用户层:通过/sys/class/pwm/下的 sysfs 节点(如enable)或应用程序,借助sysfs.c提供的接口操作 PWM。
内核层:core.c作为 PWM 核心,向上提供统一的用户 API(供其他驱动调用),向下通过pwmchip_add对接具体 PWM 控制器驱动(如pwm-rockchip.c);驱动需实现 pwm_ops结构体来完成硬件操作(如rockchip_pwm_ops)。
硬件层:**PWM 控制器通道(pwm channel)**与外部设备进行 PWM 信号的输入 / 输出交互。
整体流程是用户 / 其他驱动通过内核核心层,调用厂商实现的控制器驱动,最终完成对硬件 PWM 通道的控制。
内核 PWM 核心(PWM core):
向下为控制器驱动提供 pwm_chip 结构体 ,厂商需注册该结构体 、配置 private_data 、实例化 pwm_ops 并实现具体函数;向上通过 pwm_device 关联 pwm_chip,提供统一调用接口。
厂商已完成 PWM 控制器驱动,开发时需在设备树(或插件)中启用控制器节点、描述 PWM 设备节点 ,再通过内核 PWM 接口实现驱动控制。
完整程序参考源码 "linux_driver/pwm_sub_system" 目录。
16.1.1 PWM 设备结构体
驱动中,pwm_device 结构体用于表征一个 PWM 设备。
cpp
struct pwm_device {
const char *label; // 设备标签(标识PWM设备)
unsigned long flags; // 设备标志(特性/配置标识)
unsigned int hwpwm; // 硬件通道号(对应控制器物理通道)
unsigned int pwm; // 逻辑通道号(内核分配的逻辑标识),全局索引,可用于申请Pwm
struct pwm_chip *chip; // 关联的PWM控制器(指向所属硬件控制器)
void *chip_data; // 控制器私有数据(厂商自定义硬件相关数据)
struct pwm_args args; // 设备固有参数(出厂默认配置)
struct pwm_state state; // 设备当前状态(运行时可配置参数)
};
struct pwm_args {
u64 period; // 固有周期(单位:ns,硬件默认周期)
enum pwm_polarity polarity; // 固有极性(高/低电平有效,硬件默认)
};
struct pwm_state {
u64 period; // 当前周期(单位:ns,运行时可调整)
u64 duty_cycle; // 当前占空比(单位:ns,高/低电平持续时间)
enum pwm_polarity polarity; // 当前极性(运行时可切换)
enum pwm_output_type output_type; // 输出类型(如普通PWM、方波等)
struct pwm_output_pattern *output_pattern; // 输出模式(自定义波形模式,可选)
#ifdef CONFIG_PWM_ROCKCHIP_ONESHOT
u64 oneshot_count; // 单次触发计数(Rockchip平台独有的单次模式参数)
#endif /* CONFIG_PWM_ROCKCHIP_ONESHOT */
bool enabled; // 使能状态(true=启用,false=禁用)
};
pwm_device关键参数
cpp
/*pwm_device的输出极性参数*/
enum pwm_polarity {
PWM_POLARITY_NORMAL, // 正常极性(默认,高电平为有效电平)
PWM_POLARITY_INVERSED // 反相极性(输出信号翻转,低电平为有效电平)
};
16.1.2 pwm 申请/释放函数
旧的 PWM申请/释放函数
PWM 使用前需申请,用完需释放,相关函数分四组,
核心逻辑:先申请再使用,用完及时释放。
cpp
// 根据全局索引申请PWM设备,成功返回pwm_device指针,失败返回错误指针
struct pwm_device *pwm_request(int pwm, // 全局PWM索引(对应pwm_device->pwm)
const char *label);// 设备用途标签(调试/标识用)
// 释放已申请的PWM设备
void pwm_free(struct pwm_device *pwm); // 待释放的PWM设备指针(pwm_request返回值)
全局索引对应 pwm_device 结构体中的 pwm 成员 (即你之前接触的 unsigned int pwm 字段),是内核为每个 PWM 设备分配的唯一数字标识(类似 "设备编号")。
上面是旧版 PWM 申请 / 释放函数,已弃用,仅作认知了解即可。
PWM申请/释放函数
cpp
// 从指定设备的设备树节点中,按连接标识获取PWM设备,成功返回pwm_device指针,失败返回错误指针
struct pwm_device *pwm_get(struct device *dev, // 关联的设备指针(对应设备树节点)
const char *con_id); // 设备树PWM连接标识(匹配pwm-<con_id>或默认"pwm")
// 释放通过pwm_get获取的PWM设备
void pwm_put(struct pwm_device *pwm); // 待释放的PWM设备指针(pwm_get返回的有效指针)
con_id 需与设备树中目标设备节点的 pwm-<con_id> 也即 pwms 属性中的标识对应,
cpp
backlight {
compatible = "xxx-backlight";
pwms = <&pwm1 0 1000000>; // 这里隐含 con_id 默认为 "pwm"
// 或显式指定:pwm-backlight = <&pwm1 0 1000000>; 此时 con_id 为 "backlight"
};
这里 "pwms 属性中的标识"并非 pwms 属性里的某个独立参数,而是指 pwms 属性对应的 "默认绑定标识"------ 即当设备树用 pwms (无前缀)属性配置 PWM 时,内核默认其绑定标识为字符串 "pwm",这个默认标识就是 con_id 的匹配目标。
PWM申请释放函数-devm
cpp
// 设备树绑定获取PWM(devres自动管理),成功返回pwm_device指针,失败返回错误指针
struct pwm_device *devm_pwm_get(struct device *dev, // 关联设备指针(对应设备树节点)
const char *con_id); // 设备树PWM连接标识(匹配pwm-<con_id>或默认"pwm")
// 释放devres管理的PWM设备
void devm_pwm_put(struct device *dev, // 关联设备指针(与devm_pwm_get一致)
struct pwm_device *pwm); // 待释放PWM设备指针(devm_pwm_get返回值)
这一组函数是对上一组函数的封装,使用方法和第二组相同,优点是当驱动移除时自动注销申请的 pwm。
PWM申请释放函数-of
cpp
// 第四组:基于设备树节点直接获取PWM的核心API
// 基础版:直接从设备树节点获取PWM,需手动释放资源
struct pwm_device *of_pwm_get(struct device_node *np, // 设备树节点指针(需提前通过of函数获取,含pwms属性)
const char *con_id); // PWM通道名(匹配pwm-names,NULL匹配第一个)
// devres管理版(推荐):自动释放PWM资源,避免内存泄漏
struct pwm_device *devm_of_pwm_get(struct device *dev, // 设备指针(如probe中pdev->dev,关联驱动与硬件)
struct device_node *np, // 设备树节点指针(含pwms属性的目标节点)
const char *con_id); // PWM通道名(匹配pwm-names,NULL匹配第一个)
devres是什么
devres(Device Resource Management,设备资源管理) 是 Linux 内核提供的自动资源管理机制,核心作用是:简化驱动中资源的申请与释放,避免资源泄漏。
16.1.3 pwm 配置函数和使能/停用函数
PWM 申请成功后,只需通过对应函数配置频率、占空比并使能,
即可在指定引脚输出 PWM 波。
cpp
// 配置PWM占空比和周期
int pwm_config(struct pwm_device *pwm, // 目标PWM设备指针
int duty_ns, // 占空比(单位:ns,≤period_ns)
int period_ns); // 周期(单位:ns,决定输出频率)
// 设置PWM输出极性
int pwm_set_polarity(struct pwm_device *pwm, // 目标PWM设备指针
enum pwm_polarity polarity); // 极性(NORMAL/INVERSED)
// 使能PWM输出
int pwm_enable(struct pwm_device *pwm); // 目标PWM设备指针
// 禁用PWM输出
void pwm_disable(struct pwm_device *pwm); // 目标PWM设备指针
pwm_config:
通过周期(ns)和高电平时间(ns)配置 PWM 频率与占空比;
pwm_set_polarity:
设置 PWM 极性(NORMAL/INVERSED),反相时 duty_ns 表示低电平时间;
pwm_enable/pwm_disable:
分别使能、禁止 PWM 输出。
16.2 pwm 输出实验
以下是极简示例驱动(以 lubancat2 板卡为例),演示 PWM 子系统使用:
设备树(插件):配置 GPIO0_C7 为 PWM 输出引脚(其他引出 IO 亦可);
测试驱动:配置 PWM 周期 / 占空比(控制灯光亮度),可通过示波器查看波形。
注:lubancat 系列板卡需确认可用 PWM 输出引脚。
16.2.1 添加 pwm 相关设备树插件
先简要说明设备树中 PWM 相关配置:打开 "rk3568.dtsi" 文件,搜索 "pwm0" 即可找到对应 PWM 控制器节点内容。
设备树中描述 pwm控制器节点的 @address 是控制器节点的物理基地址。
cpp
//设备树的dts节点
pwm0: pwm@fdd70000 { /* 带phandle标签+地址后缀 */
compatible = "rockchip,rk3568-pwm", "rockchip,rk3328-pwm"; // 兼容的PWM控制器型号
reg = <0x0 0xfdd70000 0x0 0x10>; // 寄存器地址与大小
#pwm-cells = <3>; // pwms属性需3个参数(phandle、hwpwm、period)
pinctrl-names = "active"; // 引脚配置名称
pinctrl-0 = <&pwm0m0_pins>; // 关联的引脚配置
clocks = <&pmucru CLK_PWM0>, <&pmucru PCLK_PWM0>; // 依赖的时钟
clock-names = "pwm", "pclk"; // 时钟名称
status = "disabled"; // 默认禁用,需启用可改为"okay"
};
//子节点容器
pwm0 { /* 无phandle标签+无地址后缀 */
// PWM0控制器的m0模式引脚配置(对应引脚:PB7)
pwm0m0_pins: pwm0m0-pins {
rockchip,pins = <0 RK_PB7 1 &pcfg_pull_none>; // 0=端口A/B/C,1=PWM功能,无上下拉
};
// PWM0控制器的m1模式引脚配置(对应引脚:PC7)
pwm0m1_pins: pwm0m1-pins {
rockchip,pins = <0 RK_PC7 2 &pcfg_pull_none>; // 0=端口A/B/C,2=PWM功能,无上下拉
};
};
/*
带@地址后缀(@fdd70000),这是设备树中 "硬件功能节点" 的标准命名方式,
地址唯一对应芯片上的 PWM0 控制器硬件,是节点的 "唯一标识";
无@地址后缀,也无compatible属性(不对应任何硬件设备),
本质是一个 "子节点容器";
*/
32 位地址最大支持 4GB(0xFFFFFFFF),64 位芯片(如 RK3568)需更大寻址空间时,
会用 "高位 + 低位" 组合表示 64 位地址,
其中地址偏移扩展即地址高位,
用于补全超 32 位的地址部分,
避免溢出冲突(例:0x100000000 拆为高位 0x1 + 低位 0x0)
PWM0 的设备树控制节点已在rk3568.dtsi定义,
其引脚配置(pwm0m0_pins/pwm0m1_pins)在rk3568-pinctrl.dtsi中描述。
本次使用pwm0m1_pins(对应 PC7 引脚),需在设备树中引用该节点并补充必要属性,
配置示例如下:
cpp
/*设备树pwm控制节点配置示例*/
pwms = <&PWMn // 指向PWM控制器节点(如&pwm0)
id // 通道标识(hwpwm/模式ID,对应m0/m1等)
period_ns // PWM周期(单位:ns)
PWM_POLARITY_INVERTED>; // 可选,指定反相极性(默认正相)
pwm-names = "name"; // PWM通道名称(多PWM场景用于区分,与con_id对应)
gpio - pwm设备树配置实验
本实验只使用了一个 gpio 设备树插件源码如下所示:
cpp
/dts-v1/;
/plugin/;
// 引入必要的设备树绑定头文件(GPIO、引脚控制、时钟、PWM相关)
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/clock/rk3568-cru.h>
#include <dt-bindings/pwm/pwm.h>
// 引用dts中厂商定义的pwm控制器节点,设为启用状态
// [pinctrl-names 引脚配置命名]告诉系统:当 PWM0 处于工作状态时,使用 pinctrl-0 指定的引脚配置
// pinctrl-0 绑定具体的引脚配置:pwm0m1_pins 是芯片 DTS 中已定义的 "引脚配置子节点"
&pwm0 {
status = "okay"; // 使能 PWM0 外设
pinctrl-names = "active"; // 引脚配置命名(激活状态)
pinctrl-0 = <&pwm0m1_pins>; // 绑定 PWM0 的 m1 模式引脚(硬件引脚配置)
};
// 新增 PWM 演示设备节点(用户自定义设备)
&{/} {
pwm_demo: pwm_demo {
status = "okay"; // 使能该演示设备
compatible = "pwm_demo"; // 设备兼容属性(匹配驱动使用)
// 背光/输出通道配置(可根据实际功能修改命名)
back {
pwm-names = "pwm-demo"; // PWM 通道命名(便于驱动识别)
// 绑定 PWM0 控制器:通道0、周期10000ns(10kHz)、极性1(高电平有效)
pwms = <&pwm0 0 10000 1>;
duty_ns = <5000>; // 占空比 50%(5000ns / 10000ns)
/*前面我们给pwm0配置了绑定了active模式下的引脚匹配,
这里由 back节点直接引用并配置参数*/
};
};
};
注意这里 pwm子节点用了个 duty_ns 属性来设置占空比。
16.2.2 驱动程序实现
cpp
#include <linux/platform_device.h> // 平台驱动核心头文件(需手动添加,否则编译报错)
// 设备树匹配表:用于驱动与设备树节点(compatible="pwm_demo")绑定
static const struct of_device_id of_pwm_leds_match[] = {
{.compatible = "pwm_demo"}, // 匹配设备树中对应兼容属性的节点
{}, // 空条目,标记匹配表结束(必须)
};
// 平台驱动结构体:封装驱动核心逻辑与匹配信息
static struct platform_driver pwm_demo_driver = {
.probe = pwm_demo_probe_new, // 设备匹配成功后执行(初始化PWM、申请资源等)
.remove = pwm_demo_remove, // 设备移除时执行(释放PWM、资源清理等)
.driver = {
.name = "pwm_demo", // 驱动名称(sysfs中可见,用于驱动管理)
.of_match_table = of_pwm_leds_match, // 关联设备树匹配表
},
};
// 宏定义:自动完成驱动注册/注销(替代传统module_init/module_exit)
module_platform_driver(pwm_demo_driver);
我们在.prob 函数中申请、设置、使能 PWM,
cpp
#include <linux/platform_device.h>
#include <linux/pwm.h>
#include <linux/of.h>
// 全局PWM设备指针(用于probe/remove函数间共享)
static struct pwm_device *pwm_test;
/**
* pwm_demo_probe - 设备匹配成功后初始化(核心初始化逻辑)
* @pdev: 平台设备指针,关联设备树节点与驱动
* 返回:0=成功,非0=失败
*/
static int pwm_demo_probe(struct platform_device *pdev)
{
int ret = 0;
struct device_node *child; // 设备树子节点(如"back"节点)
struct device *dev = &pdev->dev;
printk("PWM demo device match success!\n");
/* 1. 获取设备树子节点(对应设备树中pwm_demo下的功能子节点) */
child = of_get_next_child(dev->of_node, NULL);
if (!child) { // 子节点获取失败
dev_err(dev, "Failed to get device tree child node!\n");
return -EINVAL;
}
/* 2. 从子节点中获取PWM设备(自动解析设备树pwms属性) */
pwm_test = devm_of_pwm_get(dev, //设备指针
child, //设备树子节点指针
NULL); //通道名,null默认获取第一个
if (IS_ERR(pwm_test)) { // PWM获取失败
dev_err(dev, "Failed to get PWM device!\n");
return PTR_ERR(pwm_test); // 返回具体错误码(替代固定-1,更规范)
}
/* 3. 配置并使能PWM输出 */
pwm_config(pwm_test, 1000, 5000); // 周期5000ns(200kHz),占空比1000ns(20%)
pwm_set_polarity(pwm_test, PWM_POLARITY_INVERSED); // 极性:反相(低电平有效)
pwm_enable(pwm_test); // 使能PWM输出
return ret;
}
/**
* pwm_demo_remove - 设备卸载时资源清理
* @pdev: 平台设备指针
* 返回:0=清理成功
*/
static int pwm_demo_remove(struct platform_device *pdev)
{
pwm_config(pwm_test, 0, 5000); // 占空比设0(停止有效输出)
// 注:devm_xxx分配的PWM无需手动pwm_free,内核自动释放,此处可省略pwm_free
return 0;
}
板卡部分 GPIO 可能被系统占用,占用后设备树加载 / 驱动申请资源会失败(如 "Device or resource busy"、卡死),需确保所用 GPIO 未被其他驱动占用;
若出现 "Permission denied",需以 root 权限(如加 sudo)执行操作,硬件外设操作通常需该权限。
16.2.3.1 通过内核工具编译设备树插件
设备树与设备树插件均通过 DTC 工具编译,前者生成 .dtb 文件,后者生成 .dtbo 文件,推荐通过修改指定 Makefile 编译设备树插件,更高效少错。
bash
#配置内核编译参数
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig
#编译设备树
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs
PWM 子系统设备树插件 "lubancat-pwm0-m1-demo-overlay.dts" 编译后,
会在内核根目录 "arch/arm64/boot/dts/rockchip/overlays" 下生成同名.dtbo 文件,
后续需将该文件加载到系统。
16.2.3.2 添加设备树插件文件
将编译好的 lubancat-pwm0-m1-demo-overlay.dtbo 上传至开发板 /boot/dtb/overlays/ 目录;
打开 /boot/uEnv/uEnvLubanCat2.txt,按 dtoverlay=<设备树插件路径> 格式添加配置;
重启开发板,执行 ls /proc/device-tree/,若存在 pwm_demo 目录则加载成功。
16.2.3.3 编译驱动程序及测试程序
编译 ARM64 架构 PWM 内核模块的 Makefile,
将pwm_sub_system.c源码编译为可加载到嵌入式 Linux 系统的内核模块(.ko文件)。
bash
# 内核源码目录(根据实际路径调整) [编译内核模块必须依赖内核源码]
KERNEL_DIR = ../../kernel/
# 目标架构(ARM64)
ARCH = arm64
# 交叉编译器前缀
CROSS_COMPILE = aarch64-linux-gnu-
# 导出环境变量
export ARCH CROSS_COMPILE
# 要编译的内核模块(目标文件:pwm_sub_system.ko)
obj-m := pwm_sub_system.o
# 编译目标:调用内核Makefile编译模块
all:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) modules
# 清理目标:删除编译生成的文件
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(CURDIR) clean
# 声明伪目标(避免与同名文件冲突)
.PHONY: clean
将配套的驱动代码如:linux_driver/放置在与内核同级目录下,并到 pwm_sub_system 驱动目录中输入如下命令来编译驱动模块及测试程序:
cpp
make
obj-m 是内核模块编译的专用变量,
含义是「要生成可加载内核模块(.ko),其目标文件为 pwm_sub_system.o」。
内核的 Makefile 会默认认为:pwm_sub_system.o 是由同名的 pwm_sub_system.c 编译生成的(这是内核模块编译的「默认约定」,无需额外写编译规则)。
16.2.4 下载验证
重启后,直接使用 insmod 命令加载驱动。
bash
#查看当前pwm状态
cat /sys/kernel/debug/pwm
使用示波器可看到设定的 PWM 波 (pwm 频率为 200KHz,占空比 80%)。
命令输出是 Linux 系统中 PWM 子系统的调试信息,用于展示当前 PWM 设备的工作状态。

这里展示了两个 pwm设备,一个是背光控制,占空比为0,
另一个是 pwm-demo,
这里显示的 pwm设备名称是在设备树子节点中通过 pwm-names定义的,

/sys/kernel/debug/pwm 显示的pwm名称是 pwm通道标识,pwm-names
第 17 章 I2C 子系统--mpu6050 驱动实验
I2C 是嵌入式系统中常用的同步半双工通信总线,可连接 EEPROM、RTC 芯片、传感器等设备。本章以板载 MPU6050 为例,讲解 I2C 驱动程序编写,核心分为五部分:
-
I2C 物理总线与通信协议基础;
-
Linux 下 I2C 驱动框架;
-
I2C 总线驱动代码拆解;
-
I2C 设备驱动核心函数;
-
MPU6050 驱动及测试程序。
17.1 i2c 基本知识
17.1.1 i2c 物理总线
IIC物理总线
I2C 支持一主多从(各设备地址独立),标准模式传输速率 100kbit/s、快速模式 400kbit/s。
总线通过上拉电阻接电源,设备空闲时输出高阻态,此时总线被拉为高电平;物理总线含两条线路 ------SCL(时钟线,同步数据收发)和 SDA(数据线,传输具体数据)。
17.1.2 i2c 基本通信协议
17.1.2.1 起始信号 (S) 与停止信号 (P)

I2C 通信开始标志(S):
SCL 为高电平时,SDA 由高变低的下降沿;然后SCL拉低,SDA传数据。
结束标志(P):
SCL 为高电平时(由低拉高),SDA 由低变高的上升沿,未发结束标志前总线保持忙状态。
17.1.2.2 数据格式与应答信号 (ACK/NACK)

I2C 数据字节为 8 位,单次传输总字节数无限制;
每传输 1 字节需伴随从设备的应答(ACK)------ 主设备提供时钟,SCL 高电平时从设备拉低 SDA 实现 ACK。若从设备忙,可拉低 SCL 使主设备等待,从设备空闲释放 SCL 后,传输继续。


17.1.2.3 主机与从机通信
这张图展示了 I2C 完整通信时序:
起始(S) :SCL 高电平时 SDA 下降沿;
传输阶段: 先传地址(含 R/W 位)、再传多字节数据,
每字节后有从设备应答(ACK,SCL 高时 SDA 拉低);
结束(P):SCL 高电平时 SDA 上升沿。
I2C 通信起始(S)后,主设备 先传 7 位从设备地址 + 第 8 位R/W 位(表示读写方向),随后释放 SDA 等待从设备应答(ACK,SCL 高时从设备拉低 SDA);每字节传输后都有应答位,传输以停止标志(P)结束,主设备也可发重复起始信号操作其他从设备。需注意:除开始、结束标志外,SDA 信号变化均在 SCL 低电平时进行。
在传输阶段,SCL产生8个脉冲,每个脉冲对应一位数据
应答阶段:主设备在第 9个时钟脉冲(高电平),
从设备在SCL高电平时拉SDA(表示应答成功)
17.1.2.4 i2c 对 mpu6050 进行数据读写
写操作时序

单字节写入:
主设备(Master)发起始(S)、
从地址 + 写(AD+W)、
寄存器地址(RA)、
1 字节数据(DATA),
每步后从设备(Slave)应答(ACK),
最后发停止(P)。
连续字节写入:
流程与单字节类似,仅 DATA 部分传输多字节,每字节后从设备均应答,最后发停止。
MPU6050 写操作时序(其实就是IIC的写操作时序,设备和寄存器地址填mpu6050对应的):
主设备发起始标志(S)+ 写地址(地址位 + R/W=0),待 MPU6050 应答后,传寄存器地址(RA),接收应答再传数据(连续写时多字节依次传输),每步均需应答。
读操作时序

单字节读出:
主设备先发从设备7位地址+1位写标识(AD+W)、
发寄存器地址(RA),
从设备应答,
发重复起始 (S)+ 读地址(AD+R),
从设备应答,
读到 1 字节数据,
主设备发非应答(NACK),
最后发停止(P)。
| 应答(ACK) | 非应答(NACK) |
|---|---|
| 已成功接收当前字节,允许继续传输 | 未接收 / 无需更多数据,终止后续传输 |
| SCL 高电平时,接收方拉低 SDA | SCL 高电平时,SDA 保持高电平(接收方释放 SDA,由上拉电阻拉高) |
| 写操作:从设备;读操作:主设备 | 仅主设备发起(用于终止读数据) |
| 多字节传输中,确认每字节接收成功;单字节传输中确认地址 / 寄存器地址接收 | 读操作最后 1 字节后,告知从设备无需再发;接收失败时拒绝后续传输 |
连续字节读出:
流程类似单字节,
仅从设备连续传多字节数据,
主设备对前 n-1 字节发应答(ACK)、
最后 1 字节发非应答(NACK),
结束后发停止。
MPU6050 读操作:
主设备发起始(S)+ 读地址(地址位 + R/W=1),
待其应答后发寄存器地址,
再发重复起始(S)+ 读地址,
MPU6050 应答后传数据,
最终主设备发非应答(NACK)和停止(P)结束。
17.2 i2c 驱动框架
编写单片机裸机 I2C 驱动需手动配置寄存器实现各类信号;Linux 采用总线 - 设备驱动模型,类似平台设备(平台总线为虚拟)。
I2C 总线(如 i2c1)可挂多个设备(MPU6050、OLED 等),对应两种驱动:
I2C 总线驱动 :由芯片厂商提供,复杂但经测试,可直接使用;
具体设备驱动(如 MPU6050):可能需从厂商获取或自行编写。
使用设备需同时具备这两种驱动。
IIC驱动框架
I2C 驱动框架含总线驱动与具体设备驱动。
I2C 总线包含 i2c_client(设备)和 i2c_driver(驱动),注册时按匹配规则配对,成功后通过 driver 的 prob 函数创建设备驱动。
现代 Linux 中,I2C 设备通过设备树引入 ,需先将 I2C 总线包装为平台总线,设备树节点转为平台设备后,再转为 I2C 设备注册到总线。
设备驱动创建后,需实现 file_operations 接口,其中会用到内核 I2C 核心函数,涉及 i2c 适配器(即 I2C 控制器,被抽象为适配器对象)。适配器含 Algorithm 成员,其函数指针指向实际硬件操作代码。
17.2.1 关键数据结构
strct i2c_adapter
struct i2c_adapter 对应 i2c 控制器,是标识物理 i2c 总线及访问算法的结构。
cpp
struct i2c_adapter {
struct module *owner; // 模块所有者
unsigned int class; // 允许探测的设备类
const struct i2c_algorithm *algo; // [访问总线的算法]
void *algo_data; // 算法私有数据
/* 对所有设备有效的有效的数据字段 */
struct rt_mutex bus_lock; // 总线锁
int timeout; // 超时时间(jiffies单位)
int retries; // 重试次数
struct device dev; // [适配器对应的设备结构体]
int nr; // 适配器编号
char name[48]; // 适配器名称
struct completion dev_released; // 设备释放完成量
struct mutex userspace_clients_lock;// 用户空间客户端锁
struct list_head userspace_clients; // 用户空间客户端链表
struct i2c_bus_recovery_info *bus_recovery_info; // 总线恢复信息
const struct i2c_adapter_quirks *quirks; // 适配器特性/限制
};
algo: struct i2c_algorithm 结构体,访问总线的算法;
dev: struct device 结构体,控制器,表明这是一个设备。
struct i2c_algorithm 是 I2C 通信方法的抽象接口,使不同芯片的 I2C 外设可共用总线模型。
其含函数指针,由厂商依硬件特性实现 I2C 传输功能,供 MPU6050、OLED 等设备通过总线收发数据,总线驱动会实现部分函数。
i2c_algorithm
cpp
struct i2c_algorithm {
/* 若适配器算法不支持I2C级访问,设master_xfer为NULL;
* 若支持SMBus访问,设smbus_xfer,为NULL则通过I2C消息模拟SMBus协议
* master_xfer返回成功处理的消息数,错误返回负值
*/
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
int (*smbus_xfer)(struct i2c_adapter *adap, u16 addr, unsigned short flags,
char read_write, u8 command, int size, union i2c_smbus_data *data);
/* 确定适配器支持的功能 */
u32 (*functionality)(struct i2c_adapter *);
#if IS_ENABLED(CONFIG_I2C_SLAVE)
int (*reg_slave)(struct i2c_client *client); // 注册从设备
int (*unreg_slave)(struct i2c_client *client); // 注销从设备
#endif
};
master_xfer:作为主设备时的发送函数 ,应该返回成功处理的消息数,或者在出错时返回
负值。
smbus_xfer: smbus 是一种 i2c 协议的协议,如硬件上支持,可以实现这个接口。
iic_client
struct i2c_client 表示i2c 从设备.
cpp
struct i2c_client {
unsigned short flags; // 设备标志(如地址类型等)
unsigned short addr; // 芯片地址(注:默认7bit)
char name[I2C_NAME_SIZE]; // 设备名称
struct i2c_adapter *adapter; // 挂载的I2C适配器
struct device dev; // 对应的设备结构体
int init_irq; // 初始化时设置的中断号
int irq; // 设备触发的中断号
struct list_head detected; // 已探测设备链表节点
#if IS_ENABLED(CONFIG_I2C_SLAVE)
i2c_slave_cb_t slave_cb; // 从设备模式回调函数
#endif
};
struct i2c_driver
struct i2c_driver 表示 i2c 设备驱动程序。
cpp
struct i2c_driver {
unsigned int class; // 驱动支持的设备类
int (*probe)(struct i2c_client *, const struct i2c_device_id *); // 设备匹配成功后执行
int (*remove)(struct i2c_client *); // 设备移除时执行
struct device_driver driver; // 通用设备驱动结构体
const struct i2c_device_id *id_table; // 驱动支持的设备ID表
int (*detect)(struct i2c_client *, struct i2c_board_info *); // 设备探测函数
const unsigned short *address_list; // 支持的设备地址列表
struct list_head clients; // 驱动管理的客户端链表
...
};
probe: i2c 设备和 i2c 驱动匹配后,回调该函数指针。
id_table: struct i2c_device_id 要匹配的从设备信息。
address_list:设备地址
clients:设备链表
detect:设备探测函数
17.3 i2c 总线驱动
RK 官方 Linux 内核已内置 I2C 总线驱动,且默认编译进内核,其运行机制核心如下:
1、注册 I2C 总线;
2、将 I2C 驱动添加至总线驱动链表;
3、遍历总线设备链表,经 i2c_device_match 函数匹配后,调用 i2c_device_probe;
4、i2c_device_probe 进一步触发 I2C 驱动的 probe 函数。
i2c 总线定义
cpp
// 定义I2C总线类型结构体,是I2C子系统的核心骨架
struct bus_type i2c_bus_type = {
.name = "i2c", // 总线名称,用于 sysfs 等
.match = i2c_device_match, // 设备与驱动的匹配函数
.probe = i2c_device_probe, // 匹配成功后调用的探测函数
.remove = i2c_device_remove, // 设备移除时调用
.shutdown = i2c_device_shutdown, // 系统关机时调用
};
i2c 总线维护着两个链表 (I2C 驱动、I2C 设备),管理 I2C 设备和 I2C 驱动的匹配和删除等。
i2c 总线注册
linux 启动之后,默认执行 i2c_init。
cpp
/**
* i2c_init - I2C 子系统的核心初始化函数
* @return: 0 表示成功,非 0 表示失败
*/
static int __init i2c_init(void)
{
int retval;
// 1. 注册 I2C 总线类型到内核总线系统
// 这是 I2C 子系统工作的基础,后续设备和驱动都依赖于此
retval = bus_register(&i2c_bus_type);
if (retval)
return retval;
is_registered = true; // 标记 I2C 总线已注册
// 2. 添加一个 "dummy"(虚拟)驱动
// 该驱动不对应任何真实硬件,主要用于协助内核管理和枚举 I2C 适配器
retval = i2c_add_driver(&dummy_driver);
if (retval)
goto class_err; // 如果失败,则跳转到错误处理流程
// 3. 如果启用了设备树动态配置,注册一个通知器
// 用于接收设备树动态变更(如热插拔)事件
if (IS_ENABLED(CONFIG_OF_DYNAMIC))
WARN_ON(of_reconfig_notifier_register(&i2c_of_notifier));
// 4. 如果启用了 ACPI,注册一个 ACPI 通知器
// 用于处理 ACPI 命名空间中的 I2C 设备枚举和管理
if (IS_ENABLED(CONFIG_ACPI))
WARN_ON(acpi_reconfig_notifier_register(&i2c_acpi_notifier));
return 0; // 所有初始化成功
class_err:
// 错误处理:如果 dummy_driver 添加失败,需要注销已注册的总线
bus_unregister(&i2c_bus_type);
return retval;
}
ACPI(高级配置与电源接口,Advanced Configuration and Power Interface)是操作系统与硬件间的标准接口,核心用于统一管理硬件电源配置及设备枚举。
在 I2C 子系统中,ACPI 的作用是提供设备枚举信息。
当内核启用 ACPI 支持时,i2c_init函数会注册i2c_acpi_notifier通知器,接收 ACPI 子系统的设备枚举事件,使内核能自动发现并配置 I2C 设备,无需在设备树或驱动中硬编码设备信息。
i2c 设备和 i2c 驱动匹配规则
cpp
/**
* i2c_device_match - I2C总线的设备与驱动匹配函数
* @dev: 待匹配的设备(I2C从设备)
* @drv: 待匹配的驱动
*
* 该函数按照设备树、ACPI、传统ID表的优先级顺序进行匹配。
* 返回:1 表示匹配成功,0 表示匹配失败。
*/
static int i2c_device_match(struct device *dev, struct device_driver *drv)
{
struct i2c_client *client = i2c_verify_client(dev);
struct i2c_driver *driver;
// 1. 优先使用设备树(OF)风格匹配
// 检查设备节点的compatible属性是否与驱动的of_match_table匹配
if (i2c_of_match_device(drv->of_match_table, client))
return 1;
// 2. 其次使用ACPI风格匹配
// 检查ACPI命名空间中的设备ID是否与驱动匹配
if (acpi_driver_match_device(dev, drv))
return 1;
// 将通用device_driver指针转换为I2C专用的i2c_driver指针
driver = to_i2c_driver(drv);
// 3. 最后使用传统的I2C ID表匹配
// 检查设备的name或id是否存在于驱动的id_table中
if (i2c_match_id(driver->id_table, client))
return 1;
// 所有匹配方式均失败
return 0;
}
of_driver_match_device:设备树匹配方式,比较设备节点的 compatible 属性与驱动 of_device_id 表中的对应属性。
acpi_driver_match_device:ACPI 匹配方式,利用 ACPI 命名空间信息进行设备与驱动的匹配。
i2c_match_id:I2C 传统匹配方式,比较 I2C 设备名与驱动 id_table 中的 name 字段。
查看驱动程序时,优先找位于驱动末尾的入口和出口函数(以 rk3568 为例)。
cpp
/*驱动入口和出口函数 (内核源码/drivers/i2c/busses/i2c-rk3x.c)*/
// 定义平台驱动结构体,用于注册RK3x系列I2C控制器驱动
static struct platform_driver rk3x_i2c_driver = {
.probe = rk3x_i2c_probe, // 驱动匹配成功后调用的初始化函数
.remove = rk3x_i2c_remove, // 驱动被移除时调用的清理函数
.driver = {
.name = "rk3x-i2c", // 驱动名称,用于sysfs和匹配
.of_match_table = rk3x_i2c_match, // 设备树匹配表,指定兼容的设备节点
.pm = &rk3x_i2c_pm_ops, // 电源管理操作集,支持休眠/唤醒等
},
};
// 宏定义,用于注册平台驱动(替代传统的platform_driver_register/unregister)
module_platform_driver(rk3x_i2c_driver);
驱动注册宏 module_platform_driver(定义于内核 include/linux/platform_device.h),详情可参考动态设备树章节。
由此可知,该 I2C 驱动为平台驱动,其核心平台驱动结构体为 rk3x_i2c_driver。
cpp
/*平台设备驱动结构体,内核源码/drivers/i2c/busses/i2c-rk3x.c)*/
// 设备树匹配表:定义了该驱动支持的所有Rockchip SoC的I2C控制器
// 每个条目包含一个compatible字符串和一个指向对应SoC私有数据的指针
static const struct of_device_id rk3x_i2c_match[] = {
{ .compatible = "rockchip,rv1108-i2c", .data = &rv1108_soc_data },
{ .compatible = "rockchip,rv1126-i2c", .data = &rv1126_soc_data },
{ .compatible = "rockchip,rk3066-i2c", .data = &rk3066_soc_data },
{ .compatible = "rockchip,rk3188-i2c", .data = &rk3188_soc_data },
{ .compatible = "rockchip,rk3228-i2c", .data = &rk3228_soc_data },
{ .compatible = "rockchip,rk3288-i2c", .data = &rk3288_soc_data },
{ .compatible = "rockchip,rk3399-i2c", .data = &rk3399_soc_data },
{ /* Sentinel: 哨兵条目,用于标识数组结束 */ }
};
// 将该匹配表暴露给内核,使得内核可以在模块加载时找到它
MODULE_DEVICE_TABLE(of, rk3x_i2c_match);
// 平台驱动结构体:RK3x系列I2C控制器驱动的核心描述
static struct platform_driver rk3x_i2c_driver = {
.probe = rk3x_i2c_probe, // 驱动匹配成功后执行的初始化函数
.remove = rk3x_i2c_remove, // 驱动被移除时执行的清理函数
.driver = {
.name = "rk3x-i2c", // 驱动名称,用于sysfs和调试
.of_match_table = rk3x_i2c_match, // 指向上述设备树匹配表
.pm = &rk3x_i2c_pm_ops, // 电源管理操作集,支持休眠/唤醒
},
};
以下是.porbe 函数的内容。
cpp
/*rk3568 的 i2c 控制器驱动 .probe 函数 (内核源码/drivers/i2c/busses/i2c-rk3x.c)*/
static int rk3x_i2c_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
const struct of_device_id *match;
struct rk3x_i2c *i2c;
struct resource *mem;
int ret = 0;
u32 value;
int irq;
unsigned long clk_rate;
// 1. 分配并初始化 rk3x_i2c 结构体内存
i2c = devm_kzalloc(&pdev->dev, sizeof(struct rk3x_i2c), GFP_KERNEL);
if (!i2c)
return -ENOMEM;
// 2. 匹配设备树节点,获取对应 SoC 私有数据
match = of_match_node(rk3x_i2c_match, np);
i2c->soc_data = match->data;
// 3. 解析 I2C 时序配置
i2c_parse_fw_timings(&pdev->dev, &i2c->t, true);
// 4. 初始化 I2C 适配器(adap)核心参数
strlcpy(i2c->adap.name, "rk3x-i2c", sizeof(i2c->adap.name));
i2c->adap.owner = THIS_MODULE;
i2c->adap.algo = &rk3x_i2c_algorithm; // 绑定 I2C 通信算法
i2c->adap.retries = 3;
i2c->adap.dev.of_node = np;
i2c->adap.algo_data = i2c;
i2c->adap.dev.parent = &pdev->dev;
i2c->dev = &pdev->dev;
// 5. 初始化同步锁和等待队列
spin_lock_init(&i2c->lock);
init_waitqueue_head(&i2c->wait);
// 6. 注册系统重启通知回调
i2c->i2c_restart_nb.notifier_call = rk3x_i2c_restart_notify;
i2c->i2c_restart_nb.priority = 128;
ret = register_pre_restart_handler(&i2c->i2c_restart_nb);
if (ret) {
dev_err(&pdev->dev, "failed to setup i2c restart handler.\n");
return ret;
}
// 7. 申请并映射 I2C 控制器寄存器内存
mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
i2c->regs = devm_ioremap_resource(&pdev->dev, mem);
if (IS_ERR(i2c->regs))
return PTR_ERR(i2c->regs);
// 8. 配置 GRF 寄存器(根据 SoC 型号和总线号设置)
if (i2c->soc_data->grf_offset >= 0) {
struct regmap *grf = syscon_regmap_lookup_by_phandle(np, "rockchip,grf");
if (!IS_ERR(grf)) {
int bus_nr = of_alias_get_id(np, "i2c");
if (bus_nr < 0) {
dev_err(&pdev->dev, "rk3x-i2c needs i2cX alias");
return -EINVAL;
}
// 根据不同 SoC 和总线号设置 GRF 寄存器值
if (i2c->soc_data == &rv1108_soc_data && bus_nr == 2)
value = BIT(26) | BIT(10);
else if (i2c->soc_data == &rv1126_soc_data && bus_nr == 2)
value = BIT(20) | BIT(4);
else
value = BIT(27 + bus_nr) | BIT(11 + bus_nr);
ret = regmap_write(grf, i2c->soc_data->grf_offset, value);
if (ret != 0) {
dev_err(i2c->dev, "Could not write to GRF: %d\n", ret);
return ret;
}
}
}
// 9. 申请并注册 IRQ 中断
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(&pdev->dev, "cannot find rk3x IRQ\n");
return irq;
}
ret = devm_request_irq(&pdev->dev, irq, rk3x_i2c_irq, 0, dev_name(&pdev->dev), i2c);
if (ret < 0) {
dev_err(&pdev->dev, "cannot request IRQ\n");
return ret;
}
// 10. 保存驱动私有数据到平台设备
platform_set_drvdata(pdev, i2c);
// 11. 获取并准备时钟(总线时钟 + 外设时钟)
if (i2c->soc_data->calc_timings == rk3x_i2c_v0_calc_timings) {
i2c->clk = devm_clk_get(&pdev->dev, NULL);
i2c->pclk = i2c->clk;
} else {
i2c->clk = devm_clk_get(&pdev->dev, "i2c");
i2c->pclk = devm_clk_get(&pdev->dev, "pclk");
}
if (IS_ERR(i2c->clk)) {
ret = PTR_ERR(i2c->clk);
if (ret != -EPROBE_DEFER)
dev_err(&pdev->dev, "Can't get bus clk: %d\n", ret);
return ret;
}
if (IS_ERR(i2c->pclk)) {
ret = PTR_ERR(i2c->pclk);
if (ret != -EPROBE_DEFER)
dev_err(&pdev->dev, "Can't get periph clk: %d\n", ret);
return ret;
}
// 12. 准备时钟并注册时钟通知器
ret = clk_prepare(i2c->clk);
if (ret < 0) {
dev_err(&pdev->dev, "Can't prepare bus clk: %d\n", ret);
return ret;
}
ret = clk_prepare(i2c->pclk);
if (ret < 0) {
dev_err(&pdev->dev, "Can't prepare periph clock: %d\n", ret);
goto err_clk;
}
i2c->clk_rate_nb.notifier_call = rk3x_i2c_clk_notifier_cb;
ret = clk_notifier_register(i2c->clk, &i2c->clk_rate_nb);
if (ret != 0) {
dev_err(&pdev->dev, "Unable to register clock notifier\n");
goto err_pclk;
}
// 13. 计算并配置 I2C 总线分频系数
clk_rate = clk_get_rate(i2c->clk);
rk3x_i2c_adapt_div(i2c, clk_rate);
// 14. 注册 I2C 适配器到内核 I2C 子系统
ret = i2c_add_adapter(&i2c->adap);
if (ret < 0)
goto err_clk_notifier;
return 0;
// 错误处理流程:反向释放已申请资源
err_clk_notifier:
clk_notifier_unregister(i2c->clk, &i2c->clk_rate_nb);
err_pclk:
clk_unprepare(i2c->pclk);
err_clk:
clk_unprepare(i2c->clk);
return ret;
}
rk3x_i2c_probe 函数的核心资源申请与配置步骤总结如下:
为 rk3x_i2c 私有结构体分配内存;
通过 platform_get_resource 获取 I2C 控制器基地址,经 devm_ioremap_resource 映射为虚拟地址;
借助 irq_of_parse_and_map 获取设备树中定义的中断号,用于后续中断申请;
获取 I2C 相关时钟并完成配置。
rk3x_i2c 结构体,它是切实用于产商芯片和 linux 平台关联的桥梁。
cpp
struct rk3x_i2c {
struct i2c_adapter adap; // I2C 适配器结构体,内核I2C子系统核心
struct device *dev; // 关联的设备指针
const struct rk3x_i2c_soc_data *soc_data; // 对应SoC的私有配置数据(如寄存器偏移等)
/* 硬件资源相关 */
void __iomem *regs; // I2C控制器寄存器虚拟地址
struct clk *clk; // I2C总线时钟
struct clk *pclk; // 外设时钟
struct notifier_block clk_rate_nb; // 时钟频率变更通知器
/* 配置参数 */
struct i2c_timings t; // I2C通信时序配置
/* 同步与通知机制 */
spinlock_t lock; // 自旋锁,保障并发安全
wait_queue_head_t wait; // 等待队列,用于异步通信等待
bool busy; // 总线忙状态标记
/* 当前传输消息信息 */
struct i2c_msg *msg; // 正在传输的I2C消息
u8 addr; // 从设备地址
unsigned int mode; // 传输模式(读/写等)
bool is_last_msg; // 是否为当前传输批次的最后一条消息
/* I2C状态机相关 */
enum rk3x_i2c_state state; // 状态机当前状态
unsigned int processed; // 已处理的数据长度
int error; // 传输错误码
unsigned int suspended:1; // 挂起状态标记(位域)
/* 系统重启相关 */
struct notifier_block i2c_restart_nb; // 系统重启通知器
bool system_restarting; // 系统重启状态标记
};

在前面的 probe 函数函数中,初始化 rk3x_i2c 结构体中的 adap 成员。
我们重点看 60 行的:"i2c->adap.algo = &rk3x_i2c_algorithm",它就是用于初始化"访问 i2c 总线的传输算法"。"st_i2c_algo"定义如下。
cpp
// 定义I2C通信算法结构体,封装了RK3x I2C控制器的底层通信实现
static const struct i2c_algorithm rk3x_i2c_algorithm = {
.master_xfer = rk3x_i2c_xfer, // I2C主机传输函数:负责发起读/写操作
.functionality = rk3x_i2c_func, // 功能查询函数:返回控制器支持的功能(如是否支持10位地址、SMBus等)
};
rk3x_i2c_algorithm 结构体提供了 I2C 总线的外部访问接口:
rk3x_i2c_func:返回 I2C 总线支持的功能;
rk3x_i2c_xfer:实现 I2C 外设访问与数据传输的核心逻辑。
rk3x_i2c_xfer 函数定义如下:
cpp
static int rk3x_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
struct rk3x_i2c *i2c = (struct rk3x_i2c *)adap->algo_data;
unsigned long timeout, flags;
u32 val;
int ret = 0;
int i;
// 若总线处于挂起状态,直接返回访问失败
if (i2c->suspended)
return -EACCES;
// 加自旋锁,保障临界区操作原子性(禁止中断并保存中断状态)
spin_lock_irqsave(&i2c->lock, flags);
// 使能I2C总线时钟和外设时钟
clk_enable(i2c->clk);
clk_enable(i2c->pclk);
i2c->is_last_msg = false;
// 循环处理所有待传输的I2C消息
for (i = 0; i < num; i += ret) {
// 初始化当前消息的传输参数(地址、长度、读写模式等)
ret = rk3x_i2c_setup(i2c, msgs + i, num - i);
if (ret < 0) {
dev_err(i2c->dev, "rk3x_i2c_setup() failed\n");
break;
}
// 标记当前是否为最后一条消息(用于生成STOP信号)
if (i + ret >= num)
i2c->is_last_msg = true;
// 发起I2C传输启动信号(SDA/SCL线电平控制)
rk3x_i2c_start(i2c);
// 解锁,等待传输完成(通过等待队列阻塞)
spin_unlock_irqrestore(&i2c->lock, flags);
// 等待传输完成,超时时间为WAIT_TIMEOUT(单位:毫秒)
timeout = wait_event_timeout(i2c->wait, !i2c->busy, msecs_to_jiffies(WAIT_TIMEOUT));
// 重新加锁,处理传输结果
spin_lock_irqsave(&i2c->lock, flags);
// 处理超时情况:强制发送STOP信号,重置状态并返回超时错误
if (timeout == 0) {
dev_err(i2c->dev, "timeout, ipd: 0x%02x, state: %d\n",
i2c_readl(i2c, REG_IPD), i2c->state);
rk3x_i2c_disable_irq(i2c);
val = i2c_readl(i2c, REG_CON) & REG_CON_TUNING_MASK;
val |= REG_CON_EN | REG_CON_STOP;
i2c_writel(i2c, val, REG_CON);
i2c->state = STATE_IDLE;
ret = -ETIMEDOUT;
break;
}
// 处理传输错误:返回具体错误码
if (i2c->error) {
ret = i2c->error;
break;
}
}
// 传输结束:禁用中断、关闭I2C控制器、关闭时钟
rk3x_i2c_disable_irq(i2c);
rk3x_i2c_disable(i2c);
clk_disable(i2c->pclk);
clk_disable(i2c->clk);
// 释放自旋锁,恢复中断状态
spin_unlock_irqrestore(&i2c->lock, flags);
// 返回结果:错误码(<0)或成功传输的消息总数(=num)
return ret < 0 ? ret : num;
}
编写 MPU6050 等 I2C 设备驱动时,会调用 i2c_transfer 函数执行数据传输,而该函数最终会调用芯片特定的 rk3x_i2c_xfer 函数,由其实现底层的硬件收发操作。
在 rk3x_i2c_xfer 中,实际的收发又是通过 rk3x_i2c_setup 来完成,函数实现如下:
cpp
static int rk3x_i2c_setup(struct rk3x_i2c *i2c, struct i2c_msg *msgs, int num)
{
u32 addr = (msgs[0].addr & 0x7f) << 1; // 处理从设备地址(7位地址左移1位,预留读写位)
int ret = 0;
/*
* 支持"写+读"组合传输(SMBus风格寄存器读取优化):
* 先发送短写包(长度<4),再接收数据,硬件可自动处理,提升效率
*/
if (num >= 2 && msgs[0].len < 4 &&
!(msgs[0].flags & I2C_M_RD) && (msgs[1].flags & I2C_M_RD)) {
u32 reg_addr = 0;
int i;
dev_dbg(i2c->dev, "Combined write/read from addr 0x%x\n", addr >> 1);
// 拼接寄存器地址(从第一个消息的缓冲区提取)
for (i = 0; i < msgs[0].len; ++i) {
reg_addr |= msgs[0].buf[i] << (i * 8);
reg_addr |= REG_MRXADDR_VALID(i);
}
i2c->msg = &msgs[1]; // 绑定待读取的消息
i2c->mode = REG_CON_MOD_REGISTER_TX; // 配置传输模式为"寄存器读写"
// 写入从设备地址和寄存器地址到硬件寄存器
i2c_writel(i2c, addr | REG_MRXADDR_VALID(0), REG_MRXADDR);
i2c_writel(i2c, reg_addr, REG_MRXRADDR);
ret = 2; // 标记本次处理2条消息(写+读)
} else {
// 普通传输:逐条处理消息
if (msgs[0].flags & I2C_M_RD) {
addr |= 1; // 读操作:置位地址的读写位
i2c->mode = REG_CON_MOD_REGISTER_TX;
// 写入从设备地址(无寄存器地址)
i2c_writel(i2c, addr | REG_MRXADDR_VALID(0), REG_MRXADDR);
i2c_writel(i2c, 0, REG_MRXRADDR);
} else {
i2c->mode = REG_CON_MOD_TX; // 写操作:配置传输模式为普通发送
}
i2c->msg = &msgs[0]; // 绑定当前待处理消息
ret = 1; // 标记本次处理1条消息
}
// 初始化传输状态:地址、忙标记、已处理长度、错误码
i2c->addr = msgs[0].addr;
i2c->busy = true;
i2c->processed = 0;
i2c->error = 0;
rk3x_i2c_clean_ipd(i2c); // 清理接收数据寄存器
return ret;
}
I2C 平台驱动核心总结:probe 函数完成两大核心工作:
初始化 I2C 硬件(时钟、中断、寄存器等);
初始化 i2c_adapter 结构体(绑定通信算法 i2c_algorithm),并将其注册到系统,为上层提供 I2C 外设访问接口。
底层寄存器操作细节可参考代码注释,核心是通过 i2c_algorithm 中的 rk3x_i2c_xfer 实现数据收发,上层驱动(如 MPU6050)可通过 i2c_transfer 调用该接口。
17.4 i2c 设备驱动核心函数
i2c_add_adapter() 函数
向驱动注册一个iic适配器。
cpp
// 1. 自动分配 I2C 适配器编号
// 内核会自动为 adapter 分配一个未被使用的编号,并写入 adapter->nr
int i2c_add_adapter(struct i2c_adapter *adapter);
// 2. 手动指定 I2C 适配器编号
// 调用者必须提前在 adapter->nr 中设置好想要使用的编号
int i2c_add_numbered_adapter(struct i2c_adapter *adapter);
i2c_add_driver() 宏
cpp
/*注册一个iic驱动*/
#define i2c_add_driver(driver)
这个宏函数的本质是调用了 i2c_register_driver() 函数,函数如下。
i2c_register_driver() 函数
cpp
/*注册一个iic驱动*/
int i2c_register_driver(struct module *owner, //一般为 THIS_MODULE
struct i2c_driver *driver) //要注册的 iic驱动
i2c_transfer() 函数
i2c_transfer() 函数最终就是调用我们前面讲到的 rk3x_i2c_xfer() 函数来实现数据传输。
cpp
int i2c_transfer(
struct i2c_adapter *adap, // I2C适配器指针
struct i2c_msg *msgs, // I2C消息数组指针
int num // 消息数量
);
i2c_msg 结构体
cpp
struct i2c_msg {
__u16 addr; /* 从设备地址 */
__u16 flags; /* I2C_M_RD:表示读取消息;0:表示发送消息 */
......
__u16 len; /* 消息数据长度 */
__u8 *buf; /* 指向消息数据的缓冲区指针 */
};
i2c_master_send() 函数
cpp
/*发送一个iic消息*/
static inline int i2c_master_send(
const struct i2c_client *client, // I2C从设备客户端指针
const char *buf, // 要发送的数据缓冲区
int count // 要发送的数据字节数
);
i2c_master_recv() 函数
cpp
/*接受iic消息,同步阻塞*/
static inline int i2c_master_recv(
const struct i2c_client *client, // I2C从设备客户端指针
char *buf, // 用于存放接收数据的缓冲区
int count // 期望接收的数据字节数
)
{
return i2c_transfer_buffer_flags(client, buf, count, I2C_M_RD);
}
i2c_transfer_buffer_flags() 函数
cpp
/*执行单次iic消息传输*/
int i2c_transfer_buffer_flags(
const struct i2c_client *client, // I2C从设备客户端指针(含地址、适配器等信息)
char *buf, // 数据缓冲区(写发/读存)
int count, // 数据长度(字节数)
u16 flags // 传输标志(如I2C_M_RD=读,0=写)
{
int ret;
// 初始化I2C消息结构体
struct i2c_msg msg = {
.addr = client->addr, // 从设备地址(来自client)
.flags = flags | (client->flags & I2C_M_TEN), // 合并传输标志和10位地址标志
.len = count, // 数据长度
.buf = buf // 数据缓冲区
};
// 调用i2c_transfer执行单次消息传输
ret = i2c_transfer(client->adapter, &msg, 1);
// 处理返回值:成功传输1条消息则返回数据长度,否则返回错误码
return (ret == 1) ? count : ret;
}
17.5 mpu6050 驱动实验
17.5.1 硬件介绍

17.5.1.1 设备树
以下是 LubanCat2(RK3568)通过 I2C3 M0 与 MPU6050 通信的设备树精简配置,
mpu6050核心设备树代码:
cpp
// MPU6050 设备树插件(核心配置)
/dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/clock/rk3568-cru.h>
&i2c3 {
status = "okay"; // 启用 I2C3 控制器
pinctrl-names = "default";
pinctrl-0 = <&i2c3m0_xfer>; // 复用 GPIO0_A1(SCL)/GPIO0_A0(SDA) 为 I2C3 引脚
#address-cells = <1>;
#size-cells = <0>;
mpu6050@68 { // MPU6050 子节点(地址 0x68)
compatible = "fire,i2c_mpu6050"; // 驱动匹配属性(也可启用内核自带的 "invensense,mpu6050")
reg = <0x68>; // MPU6050 从机地址
status = "okay"; // 启用该子节点
};
};
// (参考:RK3568 原生引脚定义)
i2c3m0_xfer: i2c3m0-xfer { // I2C3 M0 模式引脚:GPIO0_A1(SCL)、GPIO0_A0(SDA)
rockchip,pins = <1 RK_PA1 1 &pcfg_pull_none_smt>,
<1 RK_PA0 1 &pcfg_pull_none_smt>;
};
i2c3m1_xfer: i2c3m1-xfer { // I2C3 M1 模式引脚(备用):GPIO3_PB5(SCL)、GPIO3_PB6(SDA)
rockchip,pins = <3 RK_PB5 4 &pcfg_pull_none_smt>,
<3 RK_PB6 4 &pcfg_pull_none_smt>;
};

cpp
/*在驱动中通过设备状态的切换函数,切换不同的设备状态*/
static int mydevice_suspend(struct device *dev)
{
// 请求 pinctrl 子系统将引脚配置切换到 "sleep" 状态
dev_pm_set_pin_state(dev, PINCTRL_STATE_SLEEP);
// ... 其他休眠操作 ...
return 0;
}
static int mydevice_resume(struct device *dev)
{
// 请求 pinctrl 子系统将引脚配置切换回 "default" 状态
dev_pm_set_pin_state(dev, PINCTRL_STATE_DEFAULT);
// ... 其他唤醒操作 ...
return 0;
}
static const struct dev_pm_ops mydevice_pm_ops = {
.suspend = mydevice_suspend,
.resume = mydevice_resume,
};
关键配置备注
I2C3 控制器启用:status = "okay" 打开 I2C3 功能(原生默认禁用)。
引脚复用:pinctrl-0 = <&i2c3m0_xfer> 指定使用 M0 模式引脚,若需换引脚可改为 &i2c3m1_xfer(对应 M1 模式)。
MPU6050 子节点:
compatible 需与驱动匹配,fire,i2c_mpu6050 对应自定义驱动,invensense,mpu6050 适配内核自带 IIO 子系统驱动。
reg = <0x68> 对应 MPU6050 AD0 接地后的从机地址。
17.5.2.2 mpu6050 驱动实现
由于 Rockchip 官方已经写好了 I2C 总线驱动,MPU6050 这个设备驱动就变得很简单,下面结合代码介绍 MPU6050 设备驱动实现。和平台设备驱动类似,MPU6050 驱动程序结构如下。
一个典型的 Linux 字符设备驱动与 I2C 子系统结合的结构,用于驱动 MPU6050 传感器。

这里的示例代码没有给出设备的注册过程,在下一章节补充了 probe函数的设备注册
cpp
#include <linux/module.h>
#include <linux/i2c.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/regmap.h>
/************************************************************************
* 第一部分:MPU6050 I2C 底层读写函数
* 这部分是驱动的底层核心,直接与硬件通过 I2C 总线通信。
* 它们被上层的字符设备操作函数调用。
************************************************************************/
/**
* i2c_write_mpu6050 - 向 MPU6050 的指定寄存器写入一个字节
* @client: I2C 客户端设备结构体
* @address: 要写入的寄存器地址
* @data: 要写入的数据
*
* 返回: 成功返回 0,失败返回负的错误码
*/
static int i2c_write_mpu6050(struct i2c_client *client, u8 address, u8 data)
{
// 使用 regmap 进行更安全和高效的 I2C 写操作
// 假设在 probe 函数中已经初始化了 regmap
struct regmap *regmap = i2c_get_clientdata(client);
if (!regmap)
return -EINVAL;
return regmap_write(regmap, address, data);
}
/**
* i2c_read_mpu6050 - 从 MPU6050 的指定寄存器读取多个字节
* @client: I2C 客户端设备结构体
* @address: 要读取的起始寄存器地址
* @data: 用于存放读取数据的缓冲区
* @length: 要读取的数据长度
*
* 返回: 成功返回 0,失败返回负的错误码
*/
static int i2c_read_mpu6050(struct i2c_client *client, u8 address, void *data, u32 length)
{
struct regmap *regmap = i2c_get_clientdata(client);
if (!regmap)
return -EINVAL;
return regmap_bulk_read(regmap, address, data, length);
}
/**
* mpu6050_init - MPU6050 传感器硬件初始化
*
* 该函数在设备被探测到时调用,负责配置传感器的工作模式,
* 例如唤醒设备、设置采样率、量程等。
*/
static int mpu6050_init(struct i2c_client *client)
{
int ret;
// 唤醒 MPU6050 (退出睡眠模式)
ret = i2c_write_mpu6050(client, 0x6B, 0x00);
if (ret) {
dev_err(&client->dev, "Failed to wake up MPU6050\n");
return ret;
}
// 可以在这里添加更多初始化代码,如设置加速度计和陀螺仪量程
// ...
dev_info(&client->dev, "MPU6050 initialized successfully\n");
return 0;
}
/************************************************************************
* 第二部分:字符设备操作函数集 (file_operations)
* 这部分是驱动提供给用户空间的接口。当应用程序调用 open, read, write 等
* 系统调用时,内核会跳转到这里对应的函数执行。
************************************************************************/
// 设备的主设备号
static int mpu6050_major;
/**
* mpu6050_open - 对应应用程序的 open() 调用
* 当应用程序打开 /dev/mpu6050 设备文件时,此函数被调用。
*/
static int mpu6050_open(struct inode *inode, struct file *filp)
{
// 将 I2C 客户端指针附加到文件私有数据中,方便后续操作
struct i2c_client *client = container_of(inode->i_cdev, struct i2c_client, dev);
filp->private_data = client;
return 0;
}
/**
* mpu6050_read - 对应应用程序的 read() 调用
* 应用程序通过此函数读取传感器数据。
*/
static ssize_t mpu6050_read(struct file *filp, char __user *buf, size_t count, loff_t *off)
{
struct i2c_client *client = filp->private_data;
int16_t sensor_data[7]; // 用于存放原始传感器数据
int ret;
// 从 MPU6050 读取 14 字节数据 ( accel_x, accel_y, accel_z, temp, gyro_x, gyro_y, gyro_z )
ret = i2c_read_mpu6050(client, 0x3B, sensor_data, 14);
if (ret) {
dev_err(&client->dev, "Failed to read sensor data\n");
return ret;
}
// 将数据从内核空间拷贝到用户空间
if (copy_to_user(buf, sensor_data, min(count, sizeof(sensor_data)))) {
return -EFAULT;
}
return min(count, sizeof(sensor_data));
}
/**
* mpu6050_release - 对应应用程序的 close() 调用
* 当应用程序关闭设备文件时,此函数被调用,可用于释放资源。
*/
static int mpu6050_release(struct inode *inode, struct file *filp)
{
// 本驱动中无需特殊操作,直接返回
return 0;
}
// 字符设备操作函数集
static const struct file_operations mpu6050_chr_dev_fops = {
.owner = THIS_MODULE,
.open = mpu6050_open,
.read = mpu6050_read,
.release = mpu6050_release,
// .write 可以根据需要实现,用于向传感器写入配置
};
/************************************************************************
* 第三部分:I2C 驱动注册与匹配 (probe/remove)
* 这部分是 I2C 驱动的核心,负责与 I2C 总线上枚举到的设备进行匹配。
* 当匹配成功时,probe 函数被调用。
************************************************************************/
/**
* mpu6050_probe - I2C 设备探测函数
* 当内核发现一个与本驱动匹配的 I2C 设备时,此函数被调用。
* 它负责执行所有初始化工作,并注册字符设备。
*/
static int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
int ret;
struct regmap *regmap;
dev_info(&client->dev, "MPU6050 device probed\n");
// 初始化 regmap,简化 I2C 寄存器操作
regmap = devm_regmap_init_i2c(client, &(struct regmap_config){
.reg_bits = 8,
.val_bits = 8,
});
if (IS_ERR(regmap)) {
dev_err(&client->dev, "Failed to initialize regmap\n");
return PTR_ERR(regmap);
}
i2c_set_clientdata(client, regmap);
// 初始化 MPU6050 硬件
ret = mpu6050_init(client);
if (ret)
return ret;
// 注册字符设备
mpu6050_major = register_chrdev(0, "mpu6050", &mpu6050_chr_dev_fops);
if (mpu6050_major < 0) {
dev_err(&client->dev, "Failed to register the character device\n");
return mpu6050_major;
}
dev_info(&client->dev, "Character device registered with major number %d\n", mpu6050_major);
return 0;
}
/**
* mpu6050_remove - I2C 设备移除函数
* 当设备被移除或驱动被卸载时,此函数被调用,用于清理资源。
*/
static int mpu6050_remove(struct i2c_client *client)
{
// 注销字符设备
unregister_chrdev(mpu6050_major, "mpu6050");
dev_info(&client->dev, "MPU6050 driver removed\n");
return 0;
}
// I2C 设备 ID 表,用于与传统非设备树系统匹配
static const struct i2c_device_id mpu6050_id[] = {
{ "mpu6050", 0 },
{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(i2c, mpu6050_id);
// 设备树匹配表,用于与设备树中的节点匹配
static const struct of_device_id mpu6050_of_match[] = {
{ .compatible = "invensense,mpu6050" },
{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of, mpu6050_of_match);
// I2C 驱动结构体
static struct i2c_driver mpu6050_driver = {
.probe = mpu6050_probe,
.remove = mpu6050_remove,
.id_table = mpu6050_id,
.driver = {
.name = "mpu6050_driver",
.of_match_table = mpu6050_of_match,
},
};
/************************************************************************
* 第四部分:驱动模块加载与卸载
* 这是驱动模块的入口和出口,负责将整个 I2C 驱动注册到内核或从内核中注销。
************************************************************************/
/**
* mpu6050_driver_init - 驱动模块加载函数 (入口)
* 当使用 insmod 或 modprobe 加载模块时,此函数被内核调用。
*/
static int __init mpu6050_driver_init(void)
{
dev_info(&mpu6050_driver.driver, "Loading MPU6050 I2C driver\n");
return i2c_add_driver(&mpu6050_driver);
}
/**
* mpu6050_driver_exit - 驱动模块卸载函数 (出口)
* 当使用 rmmod 卸载模块时,此函数被内核调用。
*/
static void __exit mpu6050_driver_exit(void)
{
i2c_del_driver(&mpu6050_driver);
dev_info(&mpu6050_driver.driver, "MPU6050 I2C driver unloaded\n");
}
// 声明模块的入口和出口函数
module_init(mpu6050_driver_init);
module_exit(mpu6050_driver_exit);
// 模块许可证声明
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("MPU6050 I2C Driver");
MODULE_AUTHOR("Your Name");





17.5.2.2.1 驱动入口和出口函数实现
下面给出的代码是 iic驱动--for mpu6050 程序的框架。
驱动入口和出口函数仅仅用于注册、注销 i2c 设备驱动,
cpp
#include <linux/module.h>
#include <linux/i2c.h>
/*
* 1. I2C 设备 ID 匹配表
* 用于传统的、非设备树的驱动匹配方式。
* 当 I2C 总线上的设备在没有设备树描述时,会根据此表中的名字进行匹配。
*/
static const struct i2c_device_id gtp_device_id[] = {
{ "fire,i2c_mpu6050", 0 }, // 设备名称和数据
{ } // 哨兵,表示列表结束
};
MODULE_DEVICE_TABLE(i2c, gtp_device_id); // 将此表暴露给内核
/*
* 2. 设备树匹配表 (Of Device ID Table)
* 用于支持设备树的现代 Linux 系统。
* 驱动会通过此表中的 .compatible 属性与设备树中的节点进行匹配。
*/
static const struct of_device_id mpu6050_of_match_table[] = {
{ .compatible = "fire,i2c_mpu6050" }, // 与设备树中节点的 compatible 属性对应
{ /* Sentinel */ } // 哨兵,表示列表结束
};
MODULE_DEVICE_TABLE(of, mpu6050_of_match_table); // 将此表暴露给内核
/*
* 3. I2C 驱动结构体
* 这个结构体代表了一个 I2C 驱动,是驱动的核心。
* 它包含了驱动的名称、所有者、匹配表以及关键的操作函数指针。
*/
static struct i2c_driver mpu6050_driver = {
.probe = mpu6050_probe, // 【关键】当驱动与设备匹配成功后,内核调用此函数进行初始化
.remove = mpu6050_remove, // 【关键】当设备被移除或驱动被卸载时,内核调用此函数进行清理
.id_table = gtp_device_id, // 指向上面定义的传统 ID 匹配表
.driver = {
.name = "fire,i2c_mpu6050", // 驱动的名称,会在 sysfs 中显示
.owner = THIS_MODULE, // 声明驱动模块的所有者
.of_match_table = mpu6050_of_match_table, // 指向上面定义的设备树匹配表
},
};
/*
* 4. 驱动入口函数 (模块加载时执行)
* 当使用 `insmod` 或 `modprobe` 加载驱动模块时,内核会调用此函数。
* 它的主要工作是将我们的 I2C 驱动注册到内核的 I2C 子系统中。
*/
static int __init mpu6050_driver_init(void)
{
int ret;
pr_info("MPU6050: Driver initialization started\n");
// i2c_add_driver() 是注册 I2C 驱动的核心函数
ret = i2c_add_driver(&mpu6050_driver);
if (ret) {
pr_err("MPU6050: Failed to register driver\n");
} else {
pr_info("MPU6050: Driver registered successfully\n");
}
return ret;
}
/*
* 5. 驱动出口函数 (模块卸载时执行)
* 当使用 `rmmod` 卸载驱动模块时,内核会调用此函数。
* 它的主要工作是从内核中注销我们的 I2C 驱动,释放相关资源。
*/
static void __exit mpu6050_driver_exit(void)
{
// i2c_del_driver() 是注销 I2C 驱动的核心函数
i2c_del_driver(&mpu6050_driver);
pr_info("MPU6050: Driver unregistered\n");
}
// 6. 声明模块的入口和出口函数
module_init(mpu6050_driver_init);
module_exit(mpu6050_driver_exit);
// 7. 模块许可证声明 (必须)
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("I2C Driver for MPU6050 6-axis Motion Sensor");
MODULE_AUTHOR("Your Name");




17.5.2.2.2 .prob 函数和.remove 函数实现
在 probe函数中注册设备节点
cpp
/**
* mpu6050_probe - I2C 设备探测与初始化函数
* @client: 指向 I2C 客户端设备结构体的指针,代表一个匹配上的 I2C 设备
* @id: 指向 I2C 设备 ID 结构体的指针,包含了匹配到的设备信息
*
* 当内核发现一个与本驱动匹配的 I2C 设备时,此函数被调用。
* 它的主要职责是:
* 1. 申请设备号
* 2. 初始化字符设备 (cdev) 并将其添加到内核
* 3. 创建并注册设备类 (class)
* 4. 在 /dev 目录下创建设备节点
* 5. 初始化硬件(如 MPU6050 芯片本身)
* 6. 申请其他资源(如 GPIO、中断等)
*
* 返回: 成功返回 0,失败返回负的错误码
*/
static int mpu6050_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
int ret = -1;
printk(KERN_INFO "MPU6050: Device matched successfully.\n");
// 1. 动态分配字符设备编号
// - mpu6050_devno: 用于存放分配到的设备号
// - 0: 起始的次设备号
// - DEV_CNT: 要分配的设备数量
// - DEV_NAME: 设备名称,会在 /proc/devices 中显示
ret = alloc_chrdev_region(&mpu6050_devno, 0, DEV_CNT, DEV_NAME);
if (ret < 0) {
printk(KERN_ERR "MPU6050: Failed to allocate chrdev region.\n");
goto alloc_err; // 如果失败,跳转到错误处理标签
}
// 2. 初始化 cdev 结构体并将其与 file_operations 绑定
cdev_init(&mpu6050_chr_dev, &mpu6050_chr_dev_fops);
mpu6050_chr_dev.owner = THIS_MODULE;
// 3. 将字符设备添加到内核
ret = cdev_add(&mpu6050_chr_dev, mpu6050_devno, DEV_CNT);
if (ret < 0) {
printk(KERN_ERR "MPU6050: Failed to add cdev.\n");
goto add_err; // 如果失败,跳转到相应的错误处理标签
}
// 4. 创建一个设备类,用于在 sysfs 中组织设备
class_mpu6050 = class_create(THIS_MODULE, DEV_NAME);
if (IS_ERR(class_mpu6050)) {
printk(KERN_ERR "MPU6050: Failed to create class.\n");
ret = PTR_ERR(class_mpu6050);
goto class_err; // 如果失败,跳转到相应的错误处理标签
}
// 5. 在 /dev 目录下创建设备节点
device_create(class_mpu6050, NULL, mpu6050_devno, NULL, DEV_NAME);
printk(KERN_INFO "MPU6050: Driver probed successfully. Device node created: /dev/%s\n", DEV_NAME);
// 在这里可以继续添加硬件初始化代码,例如:
// - 读取 WHO_AM_I 寄存器确认设备
// - 配置传感器(唤醒、设置采样率等)
// - 申请中断等
return 0; // 成功
// 错误处理流程
class_err:
cdev_del(&mpu6050_chr_dev); // 移除已添加的 cdev
add_err:
unregister_chrdev_region(mpu6050_devno, DEV_CNT); // 释放已分配的设备号
alloc_err:
return ret; // 返回错误码
}
cpp
/**
* mpu6050_remove - I2C 设备移除与资源清理函数
* @client: 指向 I2C 客户端设备结构体的指针
*
* 当设备被物理移除或驱动模块被卸载时,内核调用此函数。
* 它的职责是释放 `probe` 函数中申请的所有资源,是 `probe` 函数的逆过程。
*
* 返回: 总是返回 0
*/
static int mpu6050_remove(struct i2c_client *client)
{
printk(KERN_INFO "MPU6050: Removing driver.\n");
// 1. 从 /dev 目录中删除设备节点
device_destroy(class_mpu6050, mpu6050_devno);
// 2. 删除在 sysfs 中创建的设备类
class_destroy(class_mpu6050);
// 3. 从内核中移除字符设备
cdev_del(&mpu6050_chr_dev);
// 4. 释放之前分配的设备编号
unregister_chrdev_region(mpu6050_devno, DEV_CNT);
printk(KERN_INFO "MPU6050: Driver removed successfully.\n");
return 0;
}

17.5.2.2.3 实现字符设备操作函数集



通过 iic_transfer传入 i2c_msg结构体,指定读写参数和缓冲区
mpu6050_open
cpp
#include <linux/i2c.h>
#include <linux/module.h>
// 函数声明
static int i2c_write_mpu6050(struct i2c_client *client, u8 address, u8 data);
static int mpu6050_init(struct i2c_client *client);
/*
* mpu6050_open - 字符设备打开函数
* @inode: 文件索引节点
* @filp: 文件描述符指针
*
* 当应用程序打开 /dev/mpu6050 时,此函数被调用。
* 它的主要工作是调用 mpu6050_init 函数来初始化传感器硬件。
*/
static int mpu6050_open(struct inode *inode, struct file *filp)
{
// 从 inode 中获取 i2c_client 指针
struct i2c_client *client = container_of(inode->i_cdev, struct i2c_client, dev);
// 将 client 指针保存到文件私有数据中,供其他函数使用
filp->private_data = client;
// 调用初始化函数,配置 MPU6050 进入工作状态
if (mpu6050_init(client)) {
return -EIO; // 初始化失败
}
return 0;
}
mpu6050_init
cpp
/*
* mpu6050_init - MPU6050 硬件配置初始化
* @client: I2C 客户端设备结构体
*
* 通过 I2C 总线向 MPU6050 的多个配置寄存器写入值,
* 以设置其工作模式,例如唤醒、采样率、滤波器等。
* 返回: 成功返回 0,失败返回 -1。
*/
static int mpu6050_init(struct i2c_client *client)
{
// 配置电源管理寄存器,唤醒 MPU6050 (退出睡眠模式)
if (i2c_write_mpu6050(client, 0x6B, 0x00)) return -1;
// 配置采样率分频器,采样率 = 1kHz / (1 + 7) = 125Hz
if (i2c_write_mpu6050(client, 0x19, 0x07)) return -1;
// 配置低通滤波器,带宽为 5Hz
if (i2c_write_mpu6050(client, 0x1A, 0x06)) return -1;
// 配置加速度计量程为 ±2g
if (i2c_write_mpu6050(client, 0x1C, 0x00)) return -1; // 原代码为0x01,对应±4g,这里修正为±2g作为示例
return 0;
}
i2c_write_mpu6050
cpp
/*
* i2c_write_mpu6050 - 向 MPU6050 的指定寄存器写入一个字节
* @client: I2C 客户端设备结构体
* @address: MPU6050 内部寄存器地址
* @data: 要写入的数据
*
* 这是一个底层辅助函数,它封装了 I2C 协议的写操作。
* 返回: 成功返回 0,失败返回 -1。
*/
static int i2c_write_mpu6050(struct i2c_client *client, u8 address, u8 data)
{
// I2C 传输需要的消息结构体
struct i2c_msg msg;
// 发送缓冲区:第一个字节是寄存器地址,第二个字节是数据
u8 buf[2] = {address, data};
// 填充 i2c_msg 结构体
msg.addr = client->addr; // 从设备地址 (MPU6050 的 I2C 地址)
msg.flags = 0; // 0 表示写操作
msg.buf = buf; // 指向要发送的数据缓冲区
msg.len = sizeof(buf); // 要发送的字节数
// 执行 I2C 传输。client->adapter 是 I2C 适配器,&msg 是消息指针,1 是消息数量
if (i2c_transfer(client->adapter, &msg, 1) != 1) {
printk(KERN_ERR "I2C write to register 0x%02X failed\n", address);
return -1;
}
return 0;
}
mpu6050_read
mpu6050_read 函数实现了字符设备驱动的 read 方法,负责从 MPU6050 传感器读取数据并返回给用户空间。
cpp
/**
* mpu6050_read - 读取 MPU6050 传感器数据
* @filp: 文件描述符指针
* @buf: 用户空间缓冲区,用于存放读取的数据
* @cnt: 用户请求读取的字节数
* @off: 文件偏移量(对于此设备,通常忽略)
*
* 此函数从 MPU6050 读取 3 轴加速度计和 3 轴陀螺仪的原始数据,
* 并将其通过 copy_to_user 函数拷贝到用户空间。
*
* 返回: 成功返回读取的字节数,失败返回负的错误码。
*/
static ssize_t mpu6050_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
// 从文件私有数据中获取 i2c_client 指针
struct i2c_client *client = filp->private_data;
// 用于存储从传感器读取的高低字节
char data_H, data_L;
// 用于存储合并后的 16 位原始传感器数据
short mpu6050_result[6]; // [0-2]: 加速度计 XYZ, [3-5]: 陀螺仪 XYZ
// 用于检查 copy_to_user 的返回值
int error;
// --- 读取加速度计数据 ---
// X 轴
i2c_read_mpu6050(client, ACCEL_XOUT_H, &data_H, 1);
i2c_read_mpu6050(client, ACCEL_XOUT_L, &data_L, 1);
mpu6050_result[0] = (data_H << 8) | data_L;
// Y 轴
i2c_read_mpu6050(client, ACCEL_YOUT_H, &data_H, 1);
i2c_read_mpu6050(client, ACCEL_YOUT_L, &data_L, 1);
mpu6050_result[1] = (data_H << 8) | data_L;
// Z 轴
i2c_read_mpu6050(client, ACCEL_ZOUT_H, &data_H, 1);
i2c_read_mpu6050(client, ACCEL_ZOUT_L, &data_L, 1);
mpu6050_result[2] = (data_H << 8) | data_L;
// --- 读取陀螺仪数据 ---
// X 轴
i2c_read_mpu6050(client, GYRO_XOUT_H, &data_H, 1);
i2c_read_mpu6050(client, GYRO_XOUT_L, &data_L, 1);
mpu6050_result[3] = (data_H << 8) | data_L;
// Y 轴
i2c_read_mpu6050(client, GYRO_YOUT_H, &data_H, 1);
i2c_read_mpu6050(client, GYRO_YOUT_L, &data_L, 1);
mpu6050_result[4] = (data_H << 8) | data_L;
// Z 轴
i2c_read_mpu6050(client, GYRO_ZOUT_H, &data_H, 1);
i2c_read_mpu6050(client, GYRO_ZOUT_L, &data_L, 1);
mpu6050_result[5] = (data_H << 8) | data_L;
// 将内核空间的数据拷贝到用户空间
// min(cnt, sizeof(mpu6050_result)) 确保不拷贝超出缓冲区大小的数据
error = copy_to_user(buf, mpu6050_result, min(cnt, sizeof(mpu6050_result)));
if (error != 0) {
dev_err(&client->dev, "Failed to copy data to user space\n");
return -EFAULT; // 返回用户空间访问错误
}
// 返回实际读取的字节数
return min(cnt, sizeof(mpu6050_result));
}
为什么需要从内核空间拷贝到用户空间
i2c_read_mpu6050
i2c_read_mpu6050 函数、
i2c_write_mpu6050 函数是 I2C 驱动中最基础的读写操作封装,它们在结构上非常相似,但针对不同的传输方向。
cpp
/**
* i2c_read_mpu6050 - 从 MPU6050 的指定寄存器读取一个字节
* @client: I2C 客户端设备结构体
* @address: 要读取的寄存器地址
* @data: 用于存放读取数据的缓冲区指针
* @length: 要读取的数据长度(此处固定为 1)
*
* 这是一个底层辅助函数,封装了 I2C 读寄存器的操作。
* 它先发送寄存器地址,然后再读取数据。
*
* 返回: 成功返回 0,失败返回 -1。
*/
static int i2c_read_mpu6050(struct i2c_client *client, u8 address, void *data, u32 length)
{
// I2C 传输需要两个消息:一个用于发送地址,一个用于接收数据
struct i2c_msg msg[2];
// 消息 1: 发送要读取的寄存器地址
msg[0].addr = client->addr;
msg[0].flags = 0; // 写操作
msg[0].buf = &address;
msg[0].len = 1;
// 消息 2: 从该寄存器读取数据
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD; // 读操作
msg[1].buf = data;
msg[1].len = length;
// 执行 I2C 传输,发送两个消息
if (i2c_transfer(client->adapter, msg, 2) != 2) {
dev_err(&client->dev, "I2C read from register 0x%02X failed\n", address);
return -1;
}
return 0;
}
17.5.2.3 mpu6050 测试应用程序实现
cpp
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
// 用于存放从驱动读取的原始传感器数据
// resive_data[0-2] 分别为加速度计 X, Y, Z 轴数据
// resive_data[3-5] 分别为陀螺仪 X, Y, Z 轴数据
short resive_data[6];
int fd;
int ret;
printf("MPU6050 User Space Test Program\n");
// 1. 打开字符设备文件
// "/dev/I2C1_mpu6050" 是驱动在 /dev 目录下创建的设备节点
// O_RDWR 表示以读写方式打开
fd = open("/dev/I2C1_mpu6050", O_RDWR);
if (fd < 0) {
perror("Failed to open device file"); // perror 会打印系统错误信息
return EXIT_FAILURE;
}
printf("Device file opened successfully. File descriptor: %d\n", fd);
// 2. 从设备读取数据
// resive_data: 用于存放数据的缓冲区
// sizeof(resive_data): 请求读取的数据大小 (6 * 2 = 12 字节)
ret = read(fd, resive_data, sizeof(resive_data));
if (ret < 0) {
perror("Failed to read from device");
close(fd); // 读取失败时,要记得关闭已经打开的文件
return EXIT_FAILURE;
}
printf("Read %d bytes of data successfully.\n", ret);
// 3. 解析并打印数据
// 传感器数据为 16 位有符号整数 (short)
printf("Accelerometer (AX, AY, AZ): (%d, %d, %d)\n",
resive_data[0], resive_data[1], resive_data[2]);
printf("Gyroscope (GX, GY, GZ): (%d, %d, %d)\n",
resive_data[3], resive_data[4], resive_data[5]);
// 4. 关闭设备文件
ret = close(fd);
if (ret < 0) {
perror("Failed to close device file");
return EXIT_FAILURE;
}
printf("Device file closed successfully.\n");
return EXIT_SUCCESS;
}
17.5.3 实验准备
17.5.3.1 通过内核工具编译设备树插件
修改内核目录/arch/arm/boot/dts/overlays 下的 Makefile 文件,添加我们编辑好的设备树插
件。并把设备树插件文件 放在和 Makefile 文件同级目录下。以进行设备树插件的编译。

cpp
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig
/*
告诉内核,使用为 LubanCat2 开发板预定义的默认配置文件。
这个配置文件(通常是 arch/arm64/configs/lubancat2_defconfig)包含了该开发板所有硬件(如 CPU、内存、外设)的使能和配置信息。
执行此命令后,会生成一个 .config 文件,后续的编译过程将依据此文件进行。
*/
生成 .config文件 和 编译设备树插件生成 .dtb,
.dtsb指的是多个.dtb文件
cpp
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs
/*
执行此命令时,Make 会查找内核源码中所有与当前 .config 配置匹配的设备树源文件(.dts 和 .dtsi)。
然后,它会调用设备树编译器(DTC)将这些源文件编译成二进制的设备树 blob 文件(.dtb)。
编译好的 .dtb 文件通常位于 arch/arm64/boot/dts/ 目录下。
*/
17.5.3.2 编译驱动程序和应用程序
将 linux_driver/ 拷贝到内核源码同级目录,进入 I2c_MPU6050/ 目录,执行 make 命令,生成 i2c_mpu6050.ko 和 6050_test_app
Linux 驱动模块(.ko 文件)的编译并不是一个独立的过程。它必须依赖于你要运行该驱动的目标内核的头文件、配置文件(.config)和构建系统。这个过程称为 **"外部模块编译"**(Out-of-Tree Module Build)。
为什么编译驱动的时候要切换到内核源码同级目录,(其实只要指定内核源码目录即可)
这个 Makefile 可能没有使用 uname -r 来动态获取当前运行内核的路径,
而是硬编码或通过相对路径来指定内核源码树的位置
由于编译驱动的makefile给的是相对目录,因此才需要放到指定目录层级
实际上是指定的内核源码最顶层的Makefile,执行 module 目标,并给出了当前源码所在的目录。
17.5.4 程序运行结果
17.5.4.1 加载设备树插件
把设备树插件 lubancat-mpu6050-overlay.dtbo 复制到 /boot/dtb/overlay/ 目录下。
该实验以 lubancat2 为例,打开/boot/ueEnv 目录下的 uEnvLubanCat2.txt 文件,修改内容。
在 /boot/uEnv下添加设备树插件的.dtbo文件
重启开发板,设备树插件加载成功后
/proc/device-tree目录下会有设备名称
17.5.4.2 测试效果
将先前编译好的 i2c_mpu6050.ko 驱动及测试 app 上传至开发板中。
加载 i2c_mpu6050.ko,改变 mpu6050 的姿态,运行 6050_test_app 即可看到如下效果。
由于这里采集的是原始数据,所以波动较大是正常的
为什么编译驱动的时候要切换到内核源码同级目录,(其实只要指定内核源码目录即可)
由于编译驱动的makefile给的是相对目录,因此才需要放到指定目录层级