Linux驱动-PWM-pwm输入捕获实验(二)- 字符设备-中断捕获
文章目录
- 前言
- 一、参考资料
- 二、创建设备树
- 三、字符设备框架-基础中的基础
- 四、中断捕获-基础知识点
- 探针函数-capture_probe
- 1、分配内存-设备托管-devm_kzalloc
- 2、记录 PWM 捕获驱动当前 状态
- 3、从设备树 reg 拿到**外设物理地址** - platform_get_resource
- 4、物理地址转虚拟地址-devm_ioremap_resource
- 5、 获取设备树中指定的时钟句柄-devm_clk_get
- 6、从设备树节点中读取32位无符号整数属性,并存储到变量中
- 7、从 platform_device 中获取中断
- 8、将驱动程序私有数据(driver private data)与 platform_device 关联存储 - platform_set_drvdata(特别重要)
- 9、注册一个中断处理函数,并启用设备资源管理(devm)自动释放机制-devm_request_irq
- 10、时钟使能-clk_prepare_enable
- 总结
前言
为什么总结一下,这里本身是做PWM捕获实验的,但是用到了前面很多很多知识点,特别有必要梳理一下,慢慢梳理然后了解到熟悉。
一、参考资料
PWM 相关:
Linux驱动-PWM-pwm输入捕获实验(一)-基础知识
二、创建设备树
路径:kernel/arch/arm64/boot/dts/rockchip/rk3568-evb.dtsi
重写 pwm3 子节点,把之前的屏蔽掉:
&pwm3 {
status = "okay";
compatible = "pwm-capture";
pinctrl-names = "default";
pwm-channel = <3>;
};
这里我们再看看 pwm3 原始节点控制器:后面会用到的
pwm3: pwm@fdd70030 {
compatible = "rockchip,rk3568-pwm", "rockchip,rk3328-pwm";
reg = <0x0 0xfdd70030 0x0 0x10>;
interrupts = <GIC_SPI 82 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 86 IRQ_TYPE_LEVEL_HIGH>;
#pwm-cells = <3>;
pinctrl-names = "active";
pinctrl-0 = <&pwm3_pins>;
clocks = <&pmucru CLK_PWM0>, <&pmucru PCLK_PWM0>;
clock-names = "pwm", "pclk";
status = "disabled";
};

为什么选择控制器:pwm3
RK3568 处理器 pwm 接口只有 pwm3、 pwm7、 pwm11 和 pwm15 这 4 路 pwm 通道可以进行输入捕获, 使用 pwm3 进行输入捕获演示, 默认情况下 pwm3 被用作红外接收功能(这也是为什么要把已有的pw3 的红外屏功能屏蔽掉自己重新写捕获功能 的原因)。
三、字符设备框架-基础中的基础
PWM 驱动捕获中用到了字符设备,这是驱动基础中的基础,这里写一个字符驱动框架:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/uaccess.h>
#include <linux/pwm.h>
#include "rockchip_pwm_capture.h"
dev_t dev_num;
struct cdev cdev_test;
struct class *class;
struct device *device;
// 设备探测函数
static int capture_probe(struct platform_device *pdev) {
int ret;
// 申请设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name");
if (ret < 0) {
printk("alloc_chrdev_region is error\n");
}
ret = cdev_add(&cdev_test, dev_num, 1);// 添加字符设备
class = class_create(THIS_MODULE, "test"); // 创建设备类
device = device_create(class, NULL, dev_num, NULL, "pwm-capture"); // 创建设备
printk("capture_probe successful\n");
return 0;
}
// 设备移除函数
static int capture_remove(struct platform_device *pdev) {
device_destroy(class, dev_num); // 删除设备
class_destroy(class); // 删除设备类
cdev_del(&cdev_test); // 删除字符设备
unregister_chrdev_region(dev_num, 1); // 释放设备号
printk("capture_remove successful\n");
return 0;
}
// 设备树匹配表
static const struct of_device_id capture_of_device_id[] = {
{.compatible = "pwm-capture"},
{},
};
MODULE_DEVICE_TABLE(of, capture_of_device_id);
// 定义平台驱动
static struct platform_driver capture_platform_driver = {
.driver = {
.name = "capture",
.of_match_table = capture_of_device_id,
},
.probe = capture_probe,
.remove = capture_remove,
};
// 模块初始化函数
static int __init modulecdev_init(void) {
int ret;
// 注册平台驱动
ret = platform_driver_register(&capture_platform_driver);
if (ret) {
printk("platform_driver_register is error\n");
return ret;
}
printk("platform_driver_register is ok\n");
return 0;
}
// 模块退出函数
static void __exit modulecdev_exit(void) {
// 注销平台驱动
platform_driver_unregister(&capture_platform_driver);
printk("bye bye\n");
}
// 声明模块许可证和作者
module_init(modulecdev_init);
module_exit(modulecdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("topeet");
这里核心就是:
- 字符设备的创建、创建类、创建设备。
- 探测回针函数
四、中断捕获-基础知识点
探针函数-capture_probe
// 定义平台驱动
static struct platform_driver capture_platform_driver = {
.driver = {
.name = "capture",
.of_match_table = capture_of_device_id,
},
.probe = capture_probe,
.remove = capture_remove,
};
先看一下方法代码:
// 设备探针函数
static int capture_probe(struct platform_device *pdev) {
int ret;
struct rkxx_capture_drvdata *ddata;
struct resource *r; // 资源相关结构体定义
struct clk *clk;
struct clk *p_clk;
struct device_node *np = pdev->dev.of_node;
int pwm_channel; // 定义一个变量 来保存从设备树获取得到的channel 值
int irq;
// 申请设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, "alloc_name");
if (ret < 0) {
printk("alloc_chrdev_region is error\n");
}
ret = cdev_add(&cdev_test, dev_num, 1);// 添加字符设备
class = class_create(THIS_MODULE, "test"); // 创建设备类
device = device_create(class, NULL, dev_num, NULL, "pwm-capture"); // 创建设备
ddata = devm_kzalloc(&pdev->dev,sizeof(struct rkxx_capture_drvdata),GFP_KERNEL);
ddata ->state = RMC_IDLE;
// ====================解析reg 属性==========================================
r = platform_get_resource(pdev,IORESOURCE_MEM,0); // IORESOURCE_MEM 指定要获取内存类型资源(对应DTS中的reg属性)
ddata->base = devm_ioremap_resource(&pdev->dev,r); // 物理地址转虚拟地址
ddata->dev = pdev->dev;
// ====================时钟操作=================================================
clk = devm_clk_get(&pdev->dev,"pwm");
ddata->clk=clk;
p_clk = devm_clk_get(&pdev->dev,"pclk");
ddata->p_clk=p_clk;
//=================解析通道===============================================================
of_property_read_u32(np,"pwm_channel",&pwm_channel);
pwm_channel %= 4 ;
ddata->pwm_channel=pwm_channel;
//=================解析中断===============================================================
irq = platform_get_irq(pdev,0);
ddata->irq=irq;
// 设置为私有数据 在其它用到地方就获取使用比较方便
platform_set_drvdata(pdev,ddata);
// 请求中断
ret=devm_request_irq(&pdev->dev,irq,rk_pwm_capture,IRQF_NO_SUSPEND,"rk_pwm_capture_irq",ddata);
//===============使能时钟================================
clk_prepare_enable(ddata->clk);
clk_prepare_enable(ddata->p_clk);
// =========设置pwm 模式===========================================
rk_pwm_capture_init(ddata->base,ddata->pwm_channel);
printk("capture_probe successful\n");
return 0;
};
1、分配内存-设备托管-devm_kzalloc
struct rkxx_capture_drvdata *ddata;
ddata = devm_kzalloc(&pdev->dev,sizeof(struct rkxx_capture_drvdata),GFP_KERNEL);
2、记录 PWM 捕获驱动当前 状态
ddata ->state = RMC_IDLE;
3、从设备树 reg 拿到外设物理地址 - platform_get_resource
r = platform_get_resource(pdev,IORESOURCE_MEM,0); // IORESOURCE_MEM 指定要获取内存类型资源(对应DTS中的reg属性)
再看看设备树:
pwm3: pwm@fdd70030 {
compatible = "rockchip,rk3568-pwm", "rockchip,rk3328-pwm";
reg = <0x0 0xfdd70030 0x0 0x10>;
interrupts = <GIC_SPI 82 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 86 IRQ_TYPE_LEVEL_HIGH>;
#pwm-cells = <3>;
pinctrl-names = "active";
pinctrl-0 = <&pwm3_pins>;
clocks = <&pmucru CLK_PWM0>, <&pmucru PCLK_PWM0>;
clock-names = "pwm", "pclk";
status = "disabled";
};
reg = <0x0 0xfdd70030 0x0 0x10>; 每个数据的含义
reg = <地址高32位 地址低32位 大小高32位 大小低32位>;
| 数据 | 含义 | 说明 |
|---|---|---|
| 第1个 0x0 | 地址的高32位 | 64位地址的高位部分,这里是0 |
| 第2个 0xfdd70030 | 地址的低32位 | 64位地址的低位部分 |
| 第3个 0x0 | 大小的高32位 | 资源大小的高位部分,这里是0 |
| 第4个 0x10 | 大小的低32位 | 资源大小的低位部分(16字节) |
组合后的完整地址 = 0x00000000fdd70030 = 0xfdd70030(因为高32位为0)
资源大小 = 0x0000000000000010 = 0x10(16字节)
platform_get_resource(pdev, IORESOURCE_MEM, 0) 得到的是什么
struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
platform_get_resource 返回一个 struct resource 指针,其中包含:
struct resource {
resource_size_t start; // 资源起始地址
resource_size_t end; // 资源结束地址
const char *name; // 资源名称
unsigned long flags; // 资源类型标志
struct resource *parent, *sibling, *child;
};
具体得到的数据:
| 成员 | 值 | 说明 |
|---|---|---|
| res->start | 0xfdd70030 | 起始物理地址 |
| res->end | 0xfdd7003f | 结束物理地址(start + size - 1 = 0xfdd70030 + 0x10 - 1) |
| res->name | “pwm@fdd70030” 或设备名 | 资源名称 |
| res->flags | IORESOURCE_MEM | 表示这是内存映射IO资源 |
关键点:
- platform_get_resource 解析设备树的 reg 属性,自动提取出第一个地址范围
- 返回的资源结构体中,start 和 end 是已经解析好的物理地址范围
- 驱动程序通过 devm_ioremap_resource() 可以将这个物理地址映射到虚拟地址空间供CPU访问
结束地址的计算方法
结束地址的计算公式是:结束地址 = 起始地址 + 资源大小 - 1
具体计算过程:
- 起始地址 = 0xfdd70030(低32位)+ 0x0(高32位)= 0xfdd70030
- 资源大小 = 0x10(低32位)+ 0x0(高32位)= 0x10 = 16字节
- 结束地址 = 0xfdd70030 + 0x10 - 1 = 0xfdd70030 + 0x0F = 0xfdd7003f
为什么是 + size - 1?
因为地址范围是包含起始地址和结束地址的闭区间 [start, end]。
举个例子:如果资源大小是 16 字节,起始地址是 0x00:
占用的地址是:0x00, 0x01, 0x02, ..., 0x0F(共16个地址)
结束地址 = 0x00 + 16 - 1 = 0x0F
4、物理地址转虚拟地址-devm_ioremap_resource
ddata->base = devm_ioremap_resource(&pdev->dev,r); // 物理地址转虚拟地址
5、 获取设备树中指定的时钟句柄-devm_clk_get
获取时钟
先看设备树
pwm3: pwm@fdd70030 {
clocks = <&pmucru CLK_PWM0>, <&pmucru PCLK_PWM0>;
clock-names = "pwm", "pclk";
...
};
- clocks 属性定义了两个时钟
- clock-names 属性定义了对应的名字
第1个时钟:"pwm" → &pmucru CLK_PWM0
第2个时钟:"pclk" → &pmucru PCLK_PWM0
所以 devm_clk_get(&pdev->dev, “pwm”) 会匹配 clock-names 中的 “pwm”,获取对应的时钟:
| 参数 | 匹配结果 |
|---|---|
| “pwm” | 获取 CLK_PWM0(PWM工作时钟) |
| “pclk” | 获取 PCLK_PWM0(PWM总线时钟) |
devm 前缀的含义
devm_ 是 device resource management(设备资源管理) 的缩写,特点是:
- 自动释放:当设备驱动移除或卸载时,内核会自动调用 clk_put() 释放时钟资源
- 无需手动清理:不需要在 remove() 函数中显式调用 clk_put()
- 简化错误处理:如果后续操作失败,不需要手动释放已获取的时钟
6、从设备树节点中读取32位无符号整数属性,并存储到变量中
继续看设备树:
&pwm3 {
status = "okay";
compatible = "pwm-capture";
pinctrl-names = "default";
pwm-channel = <3>;
};
函数原型
int of_property_read_u32(const struct device_node *np,
const char *propname,
u32 *out_value);
| 参数 | 类型 | 说明 |
|---|---|---|
| np const | struct device_node * | 设备树节点指针(指向当前设备节点) |
| propname | const char * | 要读取的属性名称(字符串) |
| out_value | u32 * | 输出参数,存储读取到的32位值 |
设备树中的使用
在驱动对应的设备树节点中,应该有这样的属性:
pwm3: pwm@fdd70030 {
compatible = "rockchip,rk3568-pwm", "rockchip,rk3328-pwm";
reg = <0x0 0xfdd70030 0x0 0x10>;
pwm_channel = <3>; // 自定义属性,指定PWM通道号
// 或者
pwm_channel = <0>; // 指定使用通道0
...
};
设备树属性值不都是32位的-其它读取方案对比
设备树中的属性值可以是以下几种类型:
| 类型 | 格式 | 示例 | 读取函数 |
|---|---|---|---|
| 32位整数 | <数字> | pwm_channel = <3>; | of_property_read_u32() |
| 64位整数 | <高32位 低32位> | reg = <0x0 0xfdd70030>; | of_property_read_u64() |
| 8位/16位整数 | <数字>(但用对应函数) | bus-width = <8>; | of_property_read_u8()/u16() |
| 字符串 | “字符串” | status = “okay”; | of_property_read_string() |
| 字符串数组 | “str1”, “str2” | clock-names = “pwm”, “pclk”; | of_property_read_string_index() |
| 布尔值 | 存在即为 true | dma-coherent; | of_property_read_bool() |
| 字节数组 | [十六进制] | mac-address = [00 11 22 33 44 55]; | 直接读取 |
| 混合类型 | 组合使用 | interrupts = <GIC_SPI 82 4>; | 按需解析 |
为什么你看到的大多是 32 位?
- 最常用:大多数配置值(通道号、频率、标志位等)用32位就够
- CPU架构:ARM32和ARM64都是32位寄存器宽度,读取32位最自然
- 历史习惯:设备树最早在PowerPC上使用,32位是标准
- 简洁性:<3> 比 <0x0 0x3> 更简洁
7、从 platform_device 中获取中断
从 platform_device 中获取指定索引的中断号(IRQ number)- platform_get_irq(pdev, 0)
设备树中的中断描述
pwm3: pwm@fdd70030 {
compatible = "rockchip,rk3568-pwm", "rockchip,rk3328-pwm";
reg = <0x0 0xfdd70030 0x0 0x10>;
interrupts = <GIC_SPI 82 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 86 IRQ_TYPE_LEVEL_HIGH>;
...
};
interrupts 属性定义了两个中断:
- 索引0:<GIC_SPI 82 IRQ_TYPE_LEVEL_HIGH> → PWM 中断
- 索引1:<GIC_SPI 86 IRQ_TYPE_LEVEL_HIGH> → 可能是其他中断
platform_get_irq(pdev, 0) 获取索引0的中断:
- 返回内核中断号(对应硬件中断82)
中断号解析
设备树中的中断描述:
interrupts = <GIC_SPI 82 IRQ_TYPE_LEVEL_HIGH>;
|字段| 含义 |说明|
|--|--|--|
|GIC_SPI |中断类型 |GIC(通用中断控制器)的共享外设中断|
|82 |硬件中断号 |硬件中断编号(SPI 82)|
|IRQ_TYPE_LEVEL_HIGH| 触发方式 |高电平触发|
platform_get_irq() 会将这个硬件中断号转换为Linux内核使用的虚拟中断号。
相关函数对比
| 函数 | 用途 | 示例 |
|---|---|---|
| platform_get_irq(pdev, 0) | 获取指定索引的中断号 | irq = platform_get_irq(pdev, 0); |
| platform_get_irq_byname(pdev, “name”) | 通过名字获取中断号 | irq = platform_get_irq_byname(pdev, “pwm_irq”); |
| devm_request_irq() | 申请中断(自动管理) | devm_request_irq(dev, irq, handler, flags, name, dev_id); |
| request_irq() | 申请中断(手动管理) | request_irq(irq, handler, flags, name, dev_id); |
| free_irq() | 释放中断 | free_irq(irq, dev_id); |
8、将驱动程序私有数据(driver private data)与 platform_device 关联存储 - platform_set_drvdata(特别重要)
设置私有数据
直接看源码,在probe 函数中设置了私有数据:
// 设置为私有数据 在其它用到地方就获取使用比较方便
platform_set_drvdata(pdev,ddata);
// 中断函数
irqreturn_t rk_pwm_capture(int irq,void *dev_id){
struct rkxx_capture_drvdata *ddata = dev_id;
// 虚拟地址 = 基地址+偏移地址
unsigned int channel = ddata->pwm_channel;
//读取中断寄存器的地址的值
int val, lpr, hpr;
val = readl_relaxed(ddata->base + PWM_REG_INTSTS(channel));
// 读取中断寄存器 判断中断有没有发生
if((val & PWM_CH_INT(channel))==0){
return IRQ_NONE;
}
// 根据极性读取 lpr 或 hpr
if ((val & PWM_CH_POL(channel)) == 0) {
if (ddata->state != RMC_DONE) {
lpr = readl_relaxed(ddata->base + PWM_REG_LPR);
ddata->lpr = lpr;
}
} else {
if (ddata->state != RMC_DONE) {
hpr = readl_relaxed(ddata->base + PWM_REG_HPR);
ddata->hpr = hpr;
}
}
// 清除中断状态
writel_relaxed(PWM_CH_INT(channel), ddata->base + PWM_REG_INTSTS(channel));
return IRQ_HANDLED;
}
典型使用场景
场景1:probe → remove 传递
static int probe(struct platform_device *pdev)
{
struct my_data *data = devm_kzalloc(...);
platform_set_drvdata(pdev, data);
return 0;
}
static int remove(struct platform_device *pdev)
{
struct my_data *data = platform_get_drvdata(pdev);
// 使用 data 清理资源
return 0;
}
场景2:中断处理函数
static irqreturn_t irq_handler(int irq, void *dev_id)
{
struct my_data *data = dev_id; // dev_id 就是 set_drvdata 的数据
// 处理中断
}
static int probe(struct platform_device *pdev)
{
struct my_data *data = ...;
platform_set_drvdata(pdev, data);
request_irq(irq, irq_handler, flags, name, data); // 传递 data
}
场景3:sysfs 回调
static ssize_t show_value(struct device *dev,
struct device_attribute *attr,
char *buf)
{
struct platform_device *pdev = to_platform_device(dev);
struct my_data *data = platform_get_drvdata(pdev);
return sprintf(buf, "%d\n", data->value);
}
与其他存储方式的对比
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| platform_set_drvdata | 标准、类型安全、随设备生命周期 | 需要设备指针 | 推荐方式 |
| 全局变量 | 简单直接 | 多设备冲突、不安全 | 不推荐 |
| dev_get_drvdata/dev_set_drvdata | 更通用(任何 device) | 需要 device 指针 | 非 platform 设备 |
| 私有数据作为 dev_id | 中断专用 | 仅限中断上下文 | 中断处理 |
9、注册一个中断处理函数,并启用设备资源管理(devm)自动释放机制-devm_request_irq
函数原型
int devm_request_irq(struct device *dev, unsigned int irq,
irq_handler_t handler, unsigned long irqflags,
const char *devname, void *dev_id);
| 参数 | 值 | 说明 |
|---|---|---|
| dev | &pdev->dev | 设备指针,用于资源管理 |
| irq | irq | 要注册的中断号(由 platform_get_irq() 获取) |
| handler | rk_pwm_capture | 中断处理函数指针 |
| irqflags | IRQF_NO_SUSPEND | 中断标志(见下文详解) |
| devname | “rk_pwm_capture_irq” | 中断名称(显示在 /proc/interrupts) |
| dev_id | ddata | 传递给中断处理函数的设备ID(私有数据) |
IRQF_NO_SUSPEND 标志详解-中断标志位对比
IRQF_NO_SUSPEND 是中断标志位,表示这个中断在系统休眠(suspend)期间不会被禁用。
| 标志 | 含义 | 使用场景 |
|---|---|---|
| IRQF_NO_SUSPEND | 休眠期间不禁用中断 | 需要唤醒系统的中断(如按键、RTC闹钟) |
| IRQF_TRIGGER_RISING | 上升沿触发 | 边缘触发的中断 |
| IRQF_TRIGGER_FALLING | 下降沿触发 | 边缘触发的中断 |
| IRQF_TRIGGER_HIGH | 高电平触发 | 电平触发的中断 |
| IRQF_TRIGGER_LOW | 低电平触发 | 电平触发的中断 |
| IRQF_SHARED | 共享中断 | 多个设备共享同一中断线 |
| IRQF_ONESHOT | 一次性触发 | 需要硬件清除中断标志 |
| IRQF_TIMER | 定时器中断 | 定时器相关 |
10、时钟使能-clk_prepare_enable
// clk_prepare_enable() 等价于:
clk_prepare(clk); // 准备时钟(可能睡眠)
clk_enable(clk); // 使能时钟(原子操作)
工作原理
时钟使用计数(refcount)管理:
clk_prepare_enable()
↓
clk_prepare()
↓ 准备硬件(可能睡眠)
↓ - 开启PLL
↓ - 设置分频器
↓ - 准备时钟源
↓
clk_enable()
↓ 使能时钟(原子操作)
↓ - 计数器 +1
↓ - 如果计数器从0→1,真正开启硬件时钟
↓
返回成功
总结
- 这里都是基础知识点,务必掌握 方便后续捕获知识点理解。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/ItJavawfc/article/details/164339208




