10. Linux内核PWM子系统

脉冲宽度调制(Pulse Width Modulation,PWM)是一种通过周期性的高低电平脉冲信号,调节信号占空比来实现模拟电压输出、功率控制或时序同步的技术。 在嵌入式Linux系统中,PWM被广泛应用于电机调速、屏幕亮度调节、电源管理等场景。

Linux内核为了统一管理各类PWM硬件(如SOC内置PWM控制器、外接PWM芯片),提供了标准化的PWM子系统。 该子系统屏蔽了不同硬件的底层差异,为驱动开发者提供了统一的API接口,简化了PWM驱动的开发流程,同时保证了驱动的可移植性和兼容性。

10.1. PWM概念

在深入学习Linux内核PWM子系统之前,需先掌握PWM的核心概念和关键参数,这些参数是理解PWM子系统工作机制和驱动开发的基础。

10.1.1. 核心参数

  • 周期(Period):PWM信号一个完整脉冲周期的时间长度,单位通常为纳秒(ns)或微秒(us)。周期决定了PWM信号的频率,频率=1/周期,如周期10000ns,对应频率100kHz。

  • 占空比(Duty Cycle):PWM信号一个周期内,高电平(或低电平)持续的时间与周期的比值,通常用百分比表示,也可直接用时间(ns)表示。例如,周期10000ns、占空比50%,表示高电平持续5000ns、低电平持续5000ns。

  • 极性(Polarity):PWM信号的电平极性,分为正常极性(默认)和反相极性。正常极性表示周期内先高电平、后低电平;反相极性则相反,先低电平、后高电平。

  • 使能(Enable):控制PWM信号的输出状态,使能时输出指定周期和占空比的脉冲信号,禁用时输出固定电平,通常为低电平,具体由硬件决定。

10.1.2. 工作原理

PWM的核心原理是通过定时器产生周期性的脉冲信号,通过调节高电平(或低电平)的持续时间,改变信号的占空比,从而实现“模拟”电压的输出。 例如,当PWM周期固定时,占空比越高,高电平持续时间越长,等效输出电压越高。

10.2. PWM子系统

10.2.1. 核心定义

PWM子系统是Linux内核中负责统一管理和控制各类PWM硬件的软件框架,其核心作用是屏蔽不同PWM控制器、PWM芯片的底层硬件差异, 为上层应用和驱动开发者提供标准化、统一的API接口,实现PWM资源的高效管理、配置与控制。 其核心目标是简化PWM驱动开发流程,提升驱动可移植性和兼容性,同时规范PWM资源的分配与使用,确保不同PWM设备在Linux系统中稳定、可靠运行。

10.2.2. 分层架构

Linux内核PWM子系统采用分层架构,从上到下分为三层,实现了硬件与上层应用的解耦,架构如下:

../_images/subsystem_pwm_subsystem_0.jpg
  1. 用户层:作为PWM子系统的最上层,面向应用程序开发者,提供便捷的操作接口,主要通过字符设备节点或sysfs文件系统接口,向内核发送PWM配置指令(如设置周期、占空比、使能/禁用),同时可读取PWM当前工作状态,无需关注底层硬件细节。

  2. 内核层:作为PWM子系统的核心中间层,衔接用户层与硬件层,分为核心层(Core)和驱动层(Driver)。核心层提供统一的API接口(如pwm_get、pwm_config等),负责PWM资源的管理、参数解析和调度;驱动层对应具体的PWM硬件,实现核心层定义的操作接口,将软件配置转换为硬件可执行的控制信号。

  3. 硬件层:作为PWM子系统的最底层,包含各类PWM硬件设备,如SOC内置PWM控制器、外接PWM芯片等,负责接收内核层驱动的控制信号,产生并输出符合配置要求的PWM脉冲信号,是PWM功能的最终实现载体。

10.3. PWM子系统核心结构体

Linux内核PWM子系统的核心结构体定义于内核源码/include/linux/pwm.h,以下重点讲解驱动开发中最常用的4个核心结构体。

10.3.1. PWM设备结构体

PWM设备结构体(struct pwm_device)是Linux内核PWM子系统中描述单个PWM通道的核心结构体,每个PWM硬件通道对应一个该结构体实例。 该结构体贯穿PWM子系统的核心层与驱动层,核心层通过该结构体管理PWM通道的资源分配、参数配置和状态查询,驱动层通过该结构体获取通道的硬件信息和当前状态,完成底层硬件的控制操作。

pwm_device结构体(内核源码/include/linux/pwm.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct pwm_device {
    const char *label;        /* PWM设备标签,用于标识具体的PWM通道 */
    unsigned long flags;      /* PWM设备标志位,用于标识设备的特殊状态或特性 */
    unsigned int hwpwm;       /* PWM设备在所属芯片内的硬件通道编号 */
    unsigned int pwm;         /* PWM设备的逻辑通道编号,用于上层应用和核心层识别通道,与hwpwm可不一致 */
    struct pwm_chip *chip;    /* 指向该PWM设备所属的PWM芯片结构体,建立PWM通道与芯片的关联,通过该指针可访问芯片的操作集和私有数据 */
    void *chip_data;          /* 芯片私有数据指针,用于存储该PWM通道专属的硬件相关信息,如通道对应的寄存器偏移、硬件配置参数等 */

    struct pwm_args args;     /* PWM设备的参数结构体,存储通道的静态参数(如默认周期、极性等),通常由设备树解析或芯片驱动初始化时填充 */
    struct pwm_state state;   /* PWM设备的当前工作状态结构体,存储通道的动态参数(如当前周期、当前极性等),实时反映通道的工作情况 */
};

其中:

  • hwpwm与pwm的区分是关键:hwpwm对应硬件层面的通道编号,用于驱动层操作具体硬件寄存器;

  • pwm对应软件层面的逻辑编号,用于上层应用和核心层的资源管理,二者可通过芯片结构体建立映射关系。

  • struct pwm_args和struct pwm_state分离了通道的静态参数与动态状态,便于参数的初始化和实时更新。

10.3.2. PWM芯片结构体

PWM芯片结构体(struct pwm_chip)是Linux内核PWM子系统中描述PWM芯片(或PWM控制器)的核心结构体,每个PWM硬件控制器对应一个该结构体实例。 该结构体是连接PWM子系统核心层与底层硬件的关键载体,核心层通过该结构体管理芯片的所有PWM通道,驱动层通过填充该结构体及关联的pwm_ops操作集,实现对底层硬件的控制。

pwm_chip结构体(内核源码/include/linux/pwm.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct pwm_chip {
    struct device *dev;                /* 指向该PWM芯片关联的设备结构体,关联PWM控制器硬件设备 */
    struct list_head list;             /* 链表节点,用于将当前PWM芯片加入PWM子系统核心层的全局芯片链表,便于核心层统一枚举、管理所有PWM芯片 */
    const struct pwm_ops *ops;         /* 指向该PWM芯片的操作集结构体,是芯片驱动的核心,由驱动开发者实现 */
    int base;                          /* PWM芯片的基础逻辑通道号,用于分配该芯片下所有PWM通道的逻辑编号,若为负数则由核心层自动分配,便于上层识别不同芯片的通道 */
    unsigned int npwm;                 /* 该PWM芯片包含的硬件通道总数,即芯片支持的PWM通道数量,由硬件规格决定 */

    struct pwm_device *pwms;           /* 指向该芯片下所有PWM通道的结构体数组,数组长度为npwm,每个元素对应一个PWM通道(struct pwm_device实例),由芯片驱动初始化 */

    struct pwm_device * (*of_xlate)(struct pwm_chip *pc,
                    const struct of_phandle_args *args); /* 设备树解析翻译函数指针,可选实现,用于将设备树中描述的PWM通道参数翻译为对应的struct pwm_device实例,实现设备树与PWM通道的关联 */
    unsigned int of_pwm_n_cells;       /* 设备树中描述PWM通道所需的细胞数(cell数),与PWM控制器设备树节点的#pwm-cells属性一致,用于解析设备树中pwms属性的参数格式 */
};

其中:

  • ops成员是芯片驱动的核心,必须实现核心控制接口;

  • base成员用于区分不同芯片的逻辑通道号,避免多芯片环境下通道编号冲突;

  • npwm则明确芯片的硬件通道上限,是核心层分配和管理通道资源的重要依据;

  • pwms数组管理芯片下所有通道,与struct pwm_device结构体一一对应;

  • of_xlate和of_pwm_n_cells成员用于设备树解析,实现PWM通道与设备树节点的动态关联。

10.3.3. PWM操作集结构体

PWM操作集结构体(struct pwm_ops)是PWM芯片驱动的核心操作集结构体,定义了PWM子系统核心层与底层驱动层的交互接口, 是连接struct pwm_chip与硬件控制逻辑的关键。该结构体的成员均为函数指针,由芯片原厂驱动开发者根据具体PWM硬件的特性实现。

pwm_ops结构体(内核源码/include/linux/pwm.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct pwm_ops {
    /* 申请PWM通道时的回调函数,用于初始化通道硬件 */
    int (*request)(struct pwm_chip *chip, struct pwm_device *pwm);
    /* 释放PWM通道时的回调函数,用于释放通道资源 */
    void (*free)(struct pwm_chip *chip, struct pwm_device *pwm);
    /* 配置PWM通道的周期和占空比 */
    int (*config)(struct pwm_chip *chip, struct pwm_device *pwm,
                int duty_ns, int period_ns);
    /* 设置PWM通道的极性 */
    int (*set_polarity)(struct pwm_chip *chip, struct pwm_device *pwm,
                        enum pwm_polarity polarity);
    /* 使能PWM输出 */
    int (*enable)(struct pwm_chip *chip, struct pwm_device *pwm);
    /* 禁用PWM输出 */
    void (*disable)(struct pwm_chip *chip, struct pwm_device *pwm);
    /* 应用PWM通道状态,用于批量更新PWM的周期、占空比、极性等状态 */
    int (*apply)(struct pwm_chip *chip, struct pwm_device *pwm,
                struct pwm_state *state);
    /* 获取PWM通道当前工作状态,用于读取通道实时配置参数 */
    void (*get_state)(struct pwm_chip *chip, struct pwm_device *pwm,
                    struct pwm_state *state);
    /* 模块指针,指向该操作集所属的内核模块 */
    struct module *owner;
};

struct pwm_ops与前两个结构体的关联:struct pwm_chip的ops成员指向该结构体实例,核心层通过调用struct pwm_chip->ops中的接口,间接调用底层驱动实现的硬件控制逻辑, 进而操作struct pwm_device对应的PWM通道,完成周期、占空比配置、使能/禁用等功能,三者协同构成PWM子系统驱动的核心框架。

10.3.4. PWM设备状态结构体

PWM设备状态结构体(struct pwm_state)是描述PWM通道实时工作状态的核心结构体,用于统一管理PWM通道的动态参数,是PWM子系统中“状态管理”的关键载体。 该结构体与struct pwm_device紧密关联,struct pwm_device中的state成员就是该结构体实例,用于存储对应PWM通道的实时状态。

pwm_state结构体(内核源码/include/linux/pwm.h)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct pwm_state {
    u64 period;                     /* PWM通道当前的周期,单位:纳秒(ns),实时反映通道的周期配置 */
    u64 duty_cycle;                 /* PWM通道当前的占空比,单位:纳秒(ns),必须小于等于period,实时同步通道的占空比配置 */
    enum pwm_polarity polarity;     /* PWM通道当前的极性,枚举类型,可选正常/反相极性 */
    enum pwm_output_type output_type; /* PWM输出类型,可选普通PWM、互补PWM等,由硬件特性决定,可选实现 */
    struct pwm_output_pattern *output_pattern; /* PWM输出模式指针,用于配置特殊输出波形(如自定义脉冲序列),不常用,可选实现 */
#ifdef CONFIG_PWM_ROCKCHIP_ONESHOT
    u64 oneshot_count;              /* 单次模式计数,仅在开启ROCKCHIP_ONESHOT配置时有效,用于控制单次PWM脉冲输出次数 */
#endif /* CONFIG_PWM_ROCKCHIP_ONESHOT */
    bool enabled;                   /* PWM通道当前的使能状态,true=已使能(输出PWM信号),false=已禁用(停止输出) */
};

常用核心成员为period、duty_cycle、polarity、enabled,覆盖了PWM通道的核心工作状态,是驱动开发中访问和修改PWM状态的主要对象。

10.4. PWM子系统核心函数

Linux内核PWM子系统提供了一系列通用核心函数,涵盖PWM芯片注册、PWM通道申请/释放、PWM参数配置、使能/禁用等功能。

10.4.1. PWM芯片管理函数

10.4.1.1. 注册PWM芯片(pwmchip_add)

pwmchip_add函数用于将初始化好的PWM芯片(struct pwm_chip)注册到PWM子系统核心层,核心层会将该芯片加入全局PWM芯片链表,开始管理其下的所有PWM通道;注册成功后,上层可通过核心API访问该芯片的PWM通道。

函数原型:

1
int pwmchip_add(struct pwm_chip *chip);

参数说明:

  • chip:指向已初始化完成的struct pwm_chip结构体指针,需确保chip->ops、chip->pwms、chip->npwm等核心成员已正确配置。

返回值:注册成功返回0;注册失败返回负数错误码。

10.4.1.2. 注销PWM芯片(pwmchip_remove)

pwmchip_remove函数用于将已注册的PWM芯片从核心层注销,释放该芯片及其下所有PWM通道的相关资源(如内存、链表节点); 注销前会自动禁用所有已使能的PWM通道,确保硬件处于安全状态。

函数原型:

1
int pwmchip_remove(struct pwm_chip *chip);

返回值:注销成功返回0;注销失败返回负数错误码。

10.4.2. PWM通道申请与释放函数

10.4.2.1. 申请PWM通道(pwm_get)

pwm_get函数用于从指定的设备中申请一个PWM通道,通常用于外设驱动申请PWM通道;申请成功后,该通道会被标记为“已占用”,其他驱动无法再次申请,直到被释放。

函数原型:

1
struct pwm_device *pwm_get(struct device *dev, const char *con_id);

参数说明:

  • dev:指向申请PWM通道的外设设备结构体。

  • con_id:PWM通道的连接标识,对应设备树中pwms属性的名称,若为NULL,则默认申请第一个可用通道。

返回值说明:

  • 成功:返回申请到的struct pwm_device结构体指针。

  • 失败:返回ERR_PTR(错误指针)。

10.4.2.2. 设备管理型PWM通道申请(devm_pwm_get)

devm_pwm_get函数与pwm_get功能一致,用于申请PWM通道,区别在于该函数支持设备管理(devm)机制,会将申请的PWM通道与设备绑定; 当设备被卸载时,内核会自动释放该PWM通道,无需手动调用pwm_put,简化资源管理。

函数原型:

1
struct pwm_device *devm_pwm_get(struct device *dev, const char *con_id);

参数说明:

  • dev:指向申请PWM通道的外设设备结构体。

  • con_id:PWM通道的连接标识,对应设备树中pwms属性的名称,若为NULL,则默认申请第一个可用通道。

返回值说明:

  • 成功:返回申请到的struct pwm_device结构体指针。

  • 失败:返回ERR_PTR(错误指针)。

10.4.2.3. 释放PWM通道(pwm_put)

pwm_put函数用于释放已申请的PWM通道,释放后该通道会被标记为“未占用”,其他驱动可再次申请;释放前会自动禁用该PWM通道,确保硬件处于安全状态。

函数原型:

1
void pwm_put(struct pwm_device *pwm);

参数说明:

  • pwm:指向已申请的struct pwm_device结构体指针。

10.4.3. PWM参数配置函数

10.4.3.1. 配置周期和占空比(pwm_config)

pwm_config函数用于配置指定PWM通道的周期和占空比,是PWM控制最常用的函数; 该函数会调用struct pwm_ops中的config接口,将参数传递到底层驱动,完成硬件配置;同时更新struct pwm_state中的period和duty_cycle成员。

函数原型:

1
int pwm_config(struct pwm_device *pwm, unsigned int duty_ns, unsigned int period_ns);

参数说明:

  • pwm:指向struct pwm_device的指针,需已成功申请。

  • duty_ns:PWM占空比,单位:纳秒(ns),必须小于等于period_ns。

  • period_ns:PWM周期,单位:纳秒(ns),最小值由硬件决定。

返回值:配置成功返回0,配置失败返回负数。

10.4.3.2. 设置PWM极性(pwm_set_polarity)

pwm_set_polarity函数用于配置指定PWM通道的极性(正常或反相);该函数会调用struct pwm_ops中的set_polarity接口,完成硬件极性配置; 同时更新struct pwm_state中的polarity成员。

函数原型:

1
int pwm_set_polarity(struct pwm_device *pwm, enum pwm_polarity polarity);

参数说明:

  • pwm:指向PWM通道实例的指针。

  • polarity:PWM极性,枚举类型,可选值:

    • PWM_POLARITY_NORMAL:正常极性(默认),先高电平后低电平。

    • PWM_POLARITY_INVERSED:反相极性,先低电平后高电平。

返回值:配置成功返回0,配置失败返回负数。

10.4.4. PWM使能与禁用函数

10.4.4.1. 使能PWM输出(pwm_enable)

pwm_enable函数用于使能指定的PWM通道,开始输出PWM信号;该函数会调用struct pwm_ops中的enable接口,完成硬件使能操作; 同时更新struct pwm_state中的enabled成员为true。

函数原型:

1
int pwm_enable(struct pwm_device *pwm);

参数说明:

  • pwm:指向PWM通道实例的指针,需已成功申请并配置参数。

返回值:使能成功返回0,使能失败返回负数。

10.4.4.2. 禁用PWM输出(pwm_disable)

pwm_disable函数用于禁用指定的PWM通道,停止输出PWM信号;该函数会调用struct pwm_ops中的disable接口,完成硬件禁用操作; 同时更新struct pwm_state中的enabled成员为false;

函数原型:

1
void pwm_disable(struct pwm_device *pwm);

参数说明:

  • pwm:指向PWM通道实例的指针。

10.5. PWM子系统实验

本次实验采用“设备树+platform驱动+字符设备+应用程序”的架构,各模块功能如下:

  1. 设备树:配置PWM控制器(使能、引脚配置),定义PWM测试设备节点,指定兼容属性用于驱动匹配。

  2. PWM驱动:基于platform驱动架构,通过设备树匹配探针,注册字符设备,提供open、write、release接口,调用PWM核心函数完成参数配置。

  3. 应用程序:通过命令行参数传入PWM配置(周期、占空比等),打开字符设备,将配置参数写入驱动,实现PWM输出控制。

本章的示例代码目录为: linux_driver/28_pwm_subsystem

10.5.1. 设备树插件详解

本实验设备树插件(lubancat-pwm-test-overlay.dts)用于配置PWM控制器和PWM测试设备节点,完整代码如下:

设备树插件(位于linux_driver/28_pwm_subsystem/lubancat-pwm-test-overlay.dts)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/dts-v1/;
/plugin/;

#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/pinctrl/rockchip.h>

/ {
    fragment@0 {
        target = <&pwm9>;

        __overlay__ {
            pinctrl-names = "active";
            pinctrl-0 = <&pwm9m0_pins>;
            status = "okay";
        };
    };

    fragment@1 {
        target-path = "/";

        __overlay__ {
            pwm_test: pwm-test {
                status = "okay";
                compatible = "fire,pwm_subsystem";
                pwms = <&pwm9 0 10000 0>;
            };
        };
    };
};

关键配置说明:

  • target = <&pwm9>:指定要修改的PWM控制器为pwm9,需与实际要使用的硬件接口一致;

  • pinctrl-names = “active”:属性 必须 设置为active,原因是PWM控制器的官方原生驱动代码里,写死了只识别active这个名字,修改为其他名字PWM将无法正常输出;

  • pinctrl-0 = <&pwm9m0_pins>:此处使用的具体PWM引脚为pwm9m0;

  • compatible = “fire,pwm_subsystem”:驱动匹配的核心标识,需与PWM驱动中of_match_table的属性完全一致;

  • pwms = <&pwm9 0 10000 0>:描述PWM资源,格式为<PWM控制器 通道 周期 极性>,此处默认周期10000ns,极性0(正常),驱动可通过devm_pwm_get获取该资源。

注意

以上设备树插件是以PWM9_M0为例,需根据板卡实际接口进行修改!

如果不清楚自己板卡有哪些可用的I2C接口,可在板卡执行以下命令确认:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#查看配置文件可用pwm插件
cat /boot/uEnv/uEnv.txt | grep pwm

#以LubanCat2-V2板卡为例,信息打印如下
#dtoverlay=/dtb/overlay/rk356x-lubancat-pwm7-ir-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-pwm8-m0-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-pwm9-m0-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-pwm10-m0-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-pwm11-ir-m0-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-pwm12-m0-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-pwm12-m1-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-pwm13-m0-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-pwm13-m1-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-pwm14-m0-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-pwm14-m1-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-pwm15-ir-m0-overlay.dtbo
#dtoverlay=/dtb/overlay/rk356x-lubancat-pwm15-ir-m1-overlay.dtbo

从可用的设备树插件可以确认,LubanCat2-V2支持pwm7、pwm8-m0、pwm9-m0等PWM接口,从内核源码中找到对应的设备树插件源码如下,以pwm9-m0、pwm12-m1为例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#查看pwm9-m0设备树插件源码内容
cat kernel/arch/arm64/boot/dts/rockchip/overlay/rk356x-lubancat-pwm9-m0-overlay.dts

#信息打印如下
/dts-v1/;
/plugin/;

/ {
    compatible = "rockchip,rk3568";

    fragment@0 {
        target = <&pwm9>;

        __overlay__ {
            pinctrl-names = "active";   // 这行是作者补充的,源码默认没有
            pinctrl-0 = <&pwm9m0_pins>; // 这行是作者补充的,源码默认没有
            status = "okay";
        };
    };
};

#查看pwm12-m1设备树插件源码内容
cat kernel/arch/arm64/boot/dts/rockchip/overlay/rk356x-lubancat-pwm12-m1-overlay.dts

#信息打印如下
/dts-v1/;
/plugin/;

/ {
    compatible = "rockchip,rk3568";

    fragment@0 {
        target = <&pwm12>;

        __overlay__ {
            status = "okay";
            pinctrl-names = "active";
            pinctrl-0 = <&pwm12m1_pins>;
        };
    };
};

提示

某些pwmX-m0的设备树插件没有pinctrl-0配置,说明在dtsi默认就是用的pwmX_pins或pwmXm0_pins,可省略也可以自行添加上去,具体可以查看芯片的dtsi文件,如rk3568查看rk3568.dtsi。

可知,如果LubanCat2-V2使用pwm9-m0接口,lubancat-pwm-test-overlay.dts配置的target和pinctrl-0就是 target = <&pwm9>;pinctrl-0 = <&pwm9m0_pins>;

结合板卡配套的快速使用手册的40pin引脚对照图章节,可确认pwm9-m0接口对应的物理引脚,如下图:

../_images/subsystem_pwm_subsystem_1.jpg

10.5.2. 驱动代码详解

核心定义与数据结构

核心定义与数据结构(位于linux_driver/28_pwm_subsystem/pwm_subsystem.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* PWM控制参数结构体 */
typedef struct pwm_config_struct {
    unsigned int period;      /* PWM周期,单位:ns */
    unsigned int duty_cycle;  /* PWM占空比,单位:ns */
    unsigned int polarity;    /* PWM极性:0-正常 1-反相 */
    unsigned int enable;      /* PWM使能:0-关闭 1-开启 */
} pwm_config_struct;

/* PWM私有设备结构体 */
struct pwm_dev {
    dev_t devno;               /* 字符设备号 */
    struct cdev cdev;          /* 字符设备结构体 */
    struct class *class;       /* 设备类 */
    struct device *device;     /* 设备节点 */
    struct pwm_device *pwm;    /* PWM设备句柄 */
};

/* 静态全局PWM设备实例 */
static struct pwm_dev pwm_dev;
  • pwm_config_struct:用于接收应用层传入的PWM配置参数,与应用层结构体完全一致,确保数据传输正确。

  • pwm_dev:私有设备结构体,整合字符设备、PWM设备句柄等资源,便于统一管理和释放。

platform驱动实现

platform驱动实现(位于linux_driver/28_pwm_subsystem/pwm_subsystem.c)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/* PWM驱动 probe 实现 */
static int pwm_probe(struct platform_device *pdev)
{
    /* 定义返回值变量 */
    int ret = 0;
    /* 定义主设备号变量 */
    int major;
    /* 定义次设备号变量 */
    int minor;

    printk(KERN_INFO "pwm driver probe\n");

    /* 分配设备号 */
    ret = alloc_chrdev_region(&pwm_dev.devno, 0, DEV_CNT, DEV_NAME);
    if (ret) {
        printk(KERN_ERR "pwm alloc chrdev failed\n");
        return ret;
    }

    /* 获取主设备号 */
    major = MAJOR(pwm_dev.devno);
    /* 获取次设备号 */
    minor = MINOR(pwm_dev.devno);
    /* 打印主设备号和次设备号 */
    printk(KERN_INFO "major=%d, minor=%d\n", major, minor);

    /* 初始化字符设备 */
    cdev_init(&pwm_dev.cdev, &pwm_fops);
    pwm_dev.cdev.owner = THIS_MODULE;

    /* 添加字符设备 */
    ret = cdev_add(&pwm_dev.cdev, pwm_dev.devno, DEV_CNT);
    if (ret) {
        printk(KERN_ERR "pwm cdev add failed\n");
        goto unreg_chrdev;
    }

    /* 创建设备类 */
    pwm_dev.class = class_create(THIS_MODULE, DEV_NAME);
    if (IS_ERR(pwm_dev.class)) {
        ret = PTR_ERR(pwm_dev.class);
        printk(KERN_ERR "pwm class create failed\n");
        goto del_cdev;
    }

    /* 创建设备节点 */
    pwm_dev.device = device_create(pwm_dev.class, &pdev->dev,
                                pwm_dev.devno, NULL, DEV_NAME);
    if (IS_ERR(pwm_dev.device)) {
        ret = PTR_ERR(pwm_dev.device);
        printk(KERN_ERR "pwm device create failed\n");
        goto destroy_class;
    }

    /* 从设备树获取PWM设备资源 */
    pwm_dev.pwm = devm_pwm_get(&pdev->dev, NULL);
    if (IS_ERR(pwm_dev.pwm)) {
        ret = PTR_ERR(pwm_dev.pwm);
        printk(KERN_ERR "pwm get resource failed\n");
        goto destroy_device;
    }

    /* PWM配置默认周期占空比 */
    ret = pwm_config(pwm_dev.pwm, 5000, 10000);
    if (ret) {
        printk(KERN_ERR "pwm default config failed\n");
        goto destroy_device;
    }

    /* PWM配置默认极性 */
    ret = pwm_set_polarity(pwm_dev.pwm, PWM_POLARITY_NORMAL);
    if (ret) {
        printk(KERN_ERR "pwm default polarity failed\n");
        goto destroy_device;
    }

    return 0;

destroy_device:
    /* 销毁设备节点 */
    device_destroy(pwm_dev.class, pwm_dev.devno);

destroy_class:
    /* 销毁设备类 */
    class_destroy(pwm_dev.class);

del_cdev:
    /* 删除字符设备 */
    cdev_del(&pwm_dev.cdev);

unreg_chrdev:
    /* 释放设备号 */
    unregister_chrdev_region(pwm_dev.devno, DEV_CNT);
    return ret;
}

/* PWM驱动 remove 实现 */
static int pwm_remove(struct platform_device *pdev)
{
    /* 关闭PWM输出 */
    pwm_disable(pwm_dev.pwm);

    /* 销毁设备节点 */
    device_destroy(pwm_dev.class, pwm_dev.devno);
    /* 删除字符设备 */
    cdev_del(&pwm_dev.cdev);
    /* 释放字符设备号 */
    unregister_chrdev_region(pwm_dev.devno, DEV_CNT);
    /* 销毁设备类 */
    class_destroy(pwm_dev.class);

    /* 清空PWM设备句柄 */
    pwm_dev.pwm = NULL;

    printk(KERN_INFO "pwm driver remove\n");
    return 0;
}

/* 定义设备树匹配表 */
static const struct of_device_id pwm_of_match[] = {
    { .compatible = "fire,pwm_subsystem" },
    { }
};

/* 导出设备树匹配表 */
MODULE_DEVICE_TABLE(of, pwm_of_match);

/* 平台驱动结构体 */
static struct platform_driver pwm_driver = {
    .probe      = pwm_probe,
    .remove     = pwm_remove,
    .driver = {
        .name  = "pwm_demo",
        .owner = THIS_MODULE,
        .of_match_table = pwm_of_match,
    },
};

/* 驱动初始化函数 */
static int __init pwm_init(void)
{
    return platform_driver_register(&pwm_driver);
}

/* 驱动注销函数 */
static void __exit pwm_exit(void)
{
    platform_driver_unregister(&pwm_driver);
}

module_init(pwm_init);
module_exit(pwm_exit);
  • 第56行:调用devm_pwm_get从设备树获取PWM资源,NULL表示自动匹配设备树中的pwms属性,无需指定PWM名称;

  • 第64行:调用pwm_config设置默认周期和占空比;

  • 第71行:调用pwm_set_polarity设置默认极性为PWM_POLARITY_NORMAL(正常极性)

  • 第101和第113行:卸载驱动时调用pwm_disable关闭PWM输出,并将PWM设备句柄设置为NULL。

字符设备操作集

字符设备操作集(位于linux_driver/28_pwm_subsystem/pwm_subsystem.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/* 字符设备 open 实现 */
static int pwm_open(struct inode *inode, struct file *filp)
{
    printk(KERN_INFO "pwm device open\n");

    return 0;
}

/*
* 函数功能:字符设备 write 实现
* 应用层write写入pwm_config_struct,内核解析并配置PWM
*/
static ssize_t pwm_write(struct file *filp, const char __user *buf,
                        size_t count, loff_t *off)
{
    int ret = 0;
    /* 定义PWM配置参数结构体 */
    pwm_config_struct *pwm_data;

    /* 校验传入数据长度是否合法 */
    if (count != sizeof(pwm_config_struct)) {
        printk(KERN_ERR "pwm write data length error\n");
        return -EINVAL;
    }

    /* 申请内核内存,存储用户空间数据 */
    pwm_data = kzalloc(count, GFP_KERNEL);
    if (!pwm_data) {
        printk(KERN_ERR "pwm malloc failed\n");
        return -ENOMEM;
    }

    /* 从用户空间拷贝PWM配置数据到内核空间 */
    ret = copy_from_user(pwm_data, buf, count);
    if (ret) {
        printk(KERN_ERR "pwm copy from user failed\n");
        ret = -EFAULT;
        goto free_mem;
    }

    /* 配置PWM周期和占空比 */
    ret = pwm_config(pwm_dev.pwm, pwm_data->duty_cycle, pwm_data->period);
    if (ret) {
        printk(KERN_ERR "pwm config failed\n");
        goto free_mem;
    }

    /* 配置PWM极性 */
    ret = pwm_set_polarity(pwm_dev.pwm, pwm_data->polarity);
    if (ret) {
        printk(KERN_ERR "pwm set polarity failed\n");
        goto free_mem;
    }

    /* 配置PWM使能/关闭 */
    if (pwm_data->enable) {
        pwm_enable(pwm_dev.pwm);
    } else {
        pwm_disable(pwm_dev.pwm);
    }

    /* 打印配置信息 */
    printk(KERN_INFO "pwm set: period=%u ns, duty=%u ns, polarity=%u, enable=%u\n",
        pwm_data->period, pwm_data->duty_cycle,
        pwm_data->polarity, pwm_data->enable);

    /* 释放内核内存 */
    kfree(pwm_data);

    /* 返回成功写入的字节数 */
    return count;

/* 内存释放错误处理 */
free_mem:
    kfree(pwm_data);
    return ret;
}

/* 字符设备 release 实现 */
static int pwm_release(struct inode *inode, struct file *filp)
{
    printk(KERN_INFO "pwm device release\n");

    return 0;
}

/* 字符设备操作集 */
static const struct file_operations pwm_fops = {
    .owner      = THIS_MODULE,
    .open       = pwm_open,
    .write      = pwm_write,
    .release    = pwm_release,
};

关键解析:

  • pwm_write:核心接口,负责接收应用层传入的配置参数,步骤为:校验数据长度->申请内存->拷贝用户数据->配置PWM参数->释放内存。

  • 数据校验:确保传入的数据长度与pwm_config_struct结构体大小一致,避免解析错误;copy_from_user用于实现用户空间到内核空间的数据传输。

  • PWM配置:依次调用pwm_config、pwm_set_polarity、pwm_enable/pwm_disable,完成PWM的完整配置。

10.5.3. 应用代码详解

应用层通过字符设备节点(/dev/pwm_subsystem),写入PWM各参数,包括周期、占空比、极性、使能,完整代码如下:

pwm_subsystem_app.c(位于linux_driver/28_pwm_subsystem/pwm_subsystem_app.c)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

/* PWM控制参数结构体,与内核驱动一致 */
typedef struct pwm_config_struct {
    unsigned int period;      /* PWM周期,单位:ns */
    unsigned int duty_cycle;  /* PWM占空比,单位:ns */
    unsigned int polarity;    /* 极性:0-正常 1-反相 */
    unsigned int enable;      /* 使能:0-关闭 1-开启 */
} pwm_config_struct;

int main(int argc, char *argv[])
{
    int fd;
    ssize_t write_ret;

    /* 初始化默认PWM配置,无参数时使用 */
    pwm_config_struct cfg = {
        .period      = 10000,   /* 默认周期:10000ns */
        .duty_cycle  = 5000,    /* 默认占空比:5000ns */
        .polarity    = 0,       /* 默认极性:正常 */
        .enable      = 1        /* 默认使能:开启 */
    };

    /* 参数校验:至少传入设备节点路径 */
    if (argc < 2) {
        printf("Usage: %s /dev/设备节点 [周期(ns)] [占空比(ns)] [极性] [使能]\n", argv[0]);
        printf("参数说明:\n");
        printf("  设备节点    : 必须输入\n");
        printf("  周期(ns)    : 可选,默认 10000\n");
        printf("  占空比(ns)  : 可选,默认 5000\n");
        printf("  极性        : 可选,0=正常 1=反相,默认 0\n");
        printf("  使能        : 可选,0=关闭 1=开启,默认 1\n");
        printf("示例1(默认): %s /dev/pwm_subsystem\n", argv[0]);
        printf("示例2(自定义):%s /dev/pwm_subsystem 20000 10000 0 1\n", argv[0]);
        return -1;
    }

    /* 根据传入的参数数量,覆盖默认配置 */
    if (argc >= 3)  cfg.period     = atoi(argv[2]);  // 第3个参数:周期
    if (argc >= 4)  cfg.duty_cycle = atoi(argv[3]);  // 第4个参数:占空比
    if (argc >= 5)  cfg.polarity   = atoi(argv[4]);  // 第5个参数:极性
    if (argc >= 6)  cfg.enable     = atoi(argv[5]);  // 第6个参数:使能

    /* 打开设备 */
    fd = open(argv[1], O_RDWR);
    if (fd < 0) {
        perror("open device failed");
        return -1;
    }

    /* 写入配置到内核驱动 */
    write_ret = write(fd, &cfg, sizeof(cfg));
    if (write_ret != sizeof(cfg))
    {
        perror("write pwm config failed");
        close(fd);
        return -1;
    }

    close(fd);
    return 0;
}

关键解析:

  • 应用层的pwm_config_struct必须与驱动完全一致,包括字段类型、顺序,否则数据拷贝会出错。

  • 支持命令行参数传入自定义配置,未传入则使用默认值,周期10000ns、占空比50%。

  • 通过write系统调用,将配置参数写入驱动的pwm_write接口,实现PWM参数的动态配置。

10.5.4. 编译设备树和驱动

10.5.4.1. 编译设备树

修改内核目录/arch/arm64/boot/dts/rockchip/overlays下的Makefile文件,添加我们编辑好的设备树插件,并把设备树插件文件放在和Makefile文件同级目录下,以进行设备树插件的编译。

../_images/subsystem_pwm_subsystem_2.jpg

然后在内核源码顶层目录执行以下命令编译设备树插件:

1
2
3
#这里以rk356x系列4.19.232内核配置文件为例
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- lubancat2_defconfig
make ARCH=arm64 -j4 CROSS_COMPILE=aarch64-linux-gnu- dtbs

提示

其余系列板卡参考 使用内核的构建脚本编译设备树插件 章节进行编译。

10.5.4.2. 加载设备树

编译出来的设备树插件位于 内核源码/arch/arm64/boot/dts/rockchip/overlay/lubancat-pwm-test-overlay.dtbo, 将设备树插件先传到板卡,再拷贝到板卡的 /boot/dtb/overlay/ 目录下。

1
2
3
4
#先传输到板卡

#再拷贝到板卡的/boot/dtb/overlay/目录下
sudo cp -f lubancat-pwm-test-overlay.dtbo /boot/dtb/overlay/

然后在 /boot/uEnv/uEnv.txt 按照格式添加我们的设备树插件,需要在#overlay_start和#overlay_end之间添加,然后重启开发板,那么系统就会加载我们编译的设备树插件。

../_images/subsystem_pwm_subsystem_3.jpg

重启板卡可以在uboot启动信息中看到设备树插件加载。

../_images/subsystem_pwm_subsystem_4.jpg

10.5.4.3. 编译驱动

在实验目录下输入 make 即可编译驱动和应用程序,编译得到内核模块pwm_subsystem.ko和应用程序pwm_subsystem_app。

10.5.5. 程序运行结果

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

10.5.5.1. 实验操作

使用以下命令加载驱动和运行测试程序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#加载驱动
sudo insmod pwm_subsystem.ko

#信息输出如下
[  295.130691] pwm driver probe
[  295.130864] major=236, minor=0

#查看pwm注册情况
sudo cat /sys/kernel/debug/pwm

#信息输出如下
platform/fe6f0010.pwm, 1 PWM device
pwm-0   (pwm-test            ): requested period: 10000 ns duty: 5000 ns polarity: normal

platform/fe6e0010.pwm, 1 PWM device
pwm-0   (backlight1          ): requested period: 50000 ns duty: 0 ns polarity: normal

platform/fe6e0000.pwm, 1 PWM device
pwm-0   (backlight           ): requested period: 50000 ns duty: 0 ns polarity: normal

platform/fdd70000.pwm, 1 PWM device
pwm-0   ((null)              ): period: 0 ns duty: 0 ns polarity: inverse

可以看到名字为pwm-test就是我们驱动申请的PWM接口,当前周期为10000 ns,占空比5000 ns,极性为normal,此时PWM状态为被申请(requested)但未使能,还没有PWM波形输出。

使用以下命令运行应用程序进行测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#运行应用程序,使用默认值
sudo ./pwm_subsystem_app /dev/pwm_subsystem

#信息输出如下
[  608.230477] pwm device open
[  608.230723] pwm set: period=10000 ns, duty=5000 ns, polarity=0, enable=1
[  608.230864] pwm device release

#查看pwm注册情况
sudo cat /sys/kernel/debug/pwm

#信息输出如下
platform/fe6f0010.pwm, 1 PWM device
pwm-0   (pwm-test            ): requested enabled period: 10000 ns duty: 5000 ns polarity: normal

platform/fe6e0010.pwm, 1 PWM device
pwm-0   (backlight1          ): requested period: 50000 ns duty: 0 ns polarity: normal

platform/fe6e0000.pwm, 1 PWM device
pwm-0   (backlight           ): requested period: 50000 ns duty: 0 ns polarity: normal

platform/fdd70000.pwm, 1 PWM device
pwm-0   ((null)              ): period: 0 ns duty: 0 ns polarity: inverse

可以看到运行应用程序后,内核打印了当前PWM的周期、占空比、极性以及使能情况,通过查看pwm注册情况可看到pwm-test的各值和驱动设置一致,此时状态为被申请且使能(requested enabled)。

使用示波器量对应引脚的波形如下图所示:

../_images/subsystem_pwm_subsystem_5.jpg

测量到波形为方波,频率为100.006khz(频率=1/周期),占空比49.99%,和程序设置的一致。

使用以下命令运行应用程序进行自定义参数测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#运行应用程序,使用默认值
sudo ./pwm_subsystem_app /dev/pwm_subsystem 20000 10000 0 1

#信息输出如下
[ 1731.707859] pwm device open
[ 1731.708094] pwm set: period=20000 ns, duty=10000 ns, polarity=0, enable=1
[ 1731.708334] pwm device release


#查看pwm注册情况
sudo cat /sys/kernel/debug/pwm

#信息输出如下
platform/fe6f0010.pwm, 1 PWM device
pwm-0   (pwm-test            ): requested enabled period: 20000 ns duty: 10000 ns polarity: normal

platform/fe6e0010.pwm, 1 PWM device
pwm-0   (backlight1          ): requested period: 50000 ns duty: 0 ns polarity: normal

platform/fe6e0000.pwm, 1 PWM device
pwm-0   (backlight           ): requested period: 50000 ns duty: 0 ns polarity: normal

platform/fdd70000.pwm, 1 PWM device
pwm-0   ((null)              ): period: 0 ns duty: 0 ns polarity: inverse

可以看到运行应用程序后,内核打印了当前PWM的周期、占空比、极性以及使能情况,和应用程序设置的值一致。

10.5.6. 实验注意事项

  • 硬件连接:需确认开发板PWM对应的物理引脚,避免接错引脚导致无波形输出;示波器探头连接时,接地夹需可靠接地,避免干扰导致波形失真,影响实验验证。

  • 设备树配置:pinctrl-names需设置为active;pwms属性的格式需正确,周期和极性的默认值需符合PWM控制器的规格,不可随意设置超出控制器支持范围的值。

  • PWM控制参数结构体:pwm_config_struct结构体需与应用层完全一致,包括字段类型、顺序,否则copy_from_user会出现数据解析错误。

  • 若无PWM波形输出:先通过dmesg查看“pwm set”日志,确认配置已生效;再检查硬件连接是否正确、PWM是否已使能;最后用示波器排查引脚是否有信号输出。

  • 若波形异常:如果波形周期、占空比与配置不符,需检查应用层传入的参数是否正确,驱动中pwm_config函数的参数是否正确。