野火少年头像
关注
Linux驱动-PWM-pwm输入捕获实验(二)- 字符设备-中断捕获基础知识点封面图

Linux驱动-PWM-pwm输入捕获实验(二)- 字符设备-中断捕获基础知识点

Linux驱动-PWM-pwm输入捕获实验(二)- 字符设备-中断捕获

文章目录


前言

为什么总结一下,这里本身是做PWM捕获实验的,但是用到了前面很多很多知识点,特别有必要梳理一下,慢慢梳理然后了解到熟悉。

一、参考资料

PWM 相关:
Linux驱动-PWM-pwm输入捕获实验(一)-基础知识

Linux驱动-设备树-获取节点属性of函数分析

二、创建设备树

路径: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->start0xfdd70030起始物理地址
res->end0xfdd7003f结束物理地址(start + size - 1 = 0xfdd70030 + 0x10 - 1)
res->name“pwm@fdd70030” 或设备名资源名称
res->flagsIORESOURCE_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_PWM02个时钟:"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>;
};

Linux驱动-设备树-获取节点属性of函数分析

函数原型
int of_property_read_u32(const struct device_node *np, 
                         const char *propname, 
                         u32 *out_value);
参数类型说明
np conststruct device_node *设备树节点指针(指向当前设备节点)
propnameconst char *要读取的属性名称(字符串)
out_valueu32 *输出参数,存储读取到的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()
布尔值存在即为 truedma-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设备指针,用于资源管理
irqirq要注册的中断号(由 platform_get_irq() 获取)
handlerrk_pwm_capture中断处理函数指针
irqflagsIRQF_NO_SUSPEND中断标志(见下文详解)
devname“rk_pwm_capture_irq”中断名称(显示在 /proc/interrupts)
dev_idddata传递给中断处理函数的设备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- 如果计数器从01,真正开启硬件时钟
    ↓
返回成功

总结

  • 这里都是基础知识点,务必掌握 方便后续捕获知识点理解。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/ItJavawfc/article/details/164339208

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--