PWM 子系统与 Host 驱动开发 文章封面

PWM 子系统与 Host 驱动开发

本文以 iMX6ULL 平台的 pwm-imx 驱动为例,讲解 Linux PWM 子系统的代码结构、Host 驱动的注册与 probe 流程、关键数据结构,以及 sysfs 用户接口的操作方法。


一、PWM 子系统架构概览

Linux PWM 子系统采用三层架构:

用户空间(echo/cat)
    ▼
sysfs 接口(drivers/pwm/sysfs.c)
    ▼
PWM 核心层(drivers/pwm/core.c)  ← 状态管理、API 封装
    ▼
PWM Host 驱动(如 pwm-imx.c)    ← 只操作芯片寄存器
    ▼
硬件寄存器

核心原则:Host 驱动的 ops 函数只负责写寄存器,不维护软件状态;状态管理(pwm_state)由核心层负责。在 probe 中初始化 PWM 时,必须通过核心层 API(pwm_config()、pwm_enable()、pwm_apply_state()),不能直接调用 Host 驱动内部函数。

📎 这个架构的典型踩坑见 PWM驱动sysfs属性与DTS不匹配 — 绕过核心层导致 sysfs 状态不同步的调试案例


二、驱动代码结构

PWM 驱动代码位于 kernel/drivers/pwm/,以 iMX6ULL 为例,编译后生成以下关键文件:

drivers/pwm/
├── core.c          → pwm-core.ko     PWM 核心层:状态管理、API 实现、chip 注册
├── sysfs.c         → pwm-sysfs.ko    sysfs 接口:/sys/class/pwm 的读写处理
├── pwm-imx.c       → pwm-imx.ko      iMX 系列 PWM Host 驱动(本文重点)
└── pwm-fsl-ftm.c   → pwm-fsl-ftm.ko  Freescale FTM 定时器 PWM 驱动

  • core + sysfs:平台无关,随内核编译,所有 Host 驱动共用
  • pwm-imx:iMX 系列芯片专用,实现寄存器级操作
  • pwm-fsl-ftm:基于 FTM(Flex Timer Module)的另一套实现,独立于 pwm-imx

三、Host 驱动注册

3.1 platform_driver 定义

/* drivers/pwm/pwm-imx.c */
static struct platform_driver imx_pwm_driver = {
    .driver = {
        .name           = "imx-pwm",
        .of_match_table = imx_pwm_dt_ids,   /* DTS 匹配表 */
    },
    .probe  = imx_pwm_probe,
    .remove = imx_pwm_remove,
};

3.2 DTS compatible 匹配

static const struct of_device_id imx_pwm_dt_ids[] = {
    { .compatible = "fsl,imx1-pwm",  .data = &imx_pwm_data_v1 },
    { .compatible = "fsl,imx27-pwm", .data = &imx_pwm_data_v2 },
    { /* sentinel */ }
};

内核在启动时扫描 DTS,当设备树中某个节点的 compatible 字段与 of_match_table 中的条目一致时,执行 probe()。

iMX6ULL 使用的是 imx6ul.dtsi,其 PWM 节点的 compatible 采用”芯片专用 + 通用兼容”两段写法:

/* arch/arm/boot/dts/imx6ul.dtsi */
pwm7: pwm@020f8000 {
    compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm";  /* 两个字段,后者匹配通用驱动 */
    reg = <0x020f8000 0x4000>;
    interrupts = <GIC_SPI 116 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_PWM7>,
             <&clks IMX6UL_CLK_PWM7>;
    clock-names = "ipg", "per";
    #pwm-cells = <2>;
    status = "disabled";   /* 板级 dts 覆盖为 "okay" 才会触发 probe */
};

DTS 字段说明:

字段说明
compatible两个值:芯片专用在前,向后兼容通用驱动在后
reg寄存器起始地址 0x020f8000,映射长度 0x4000
interruptsGIC 中断控制器,SPI 类型,中断号 116,高电平触发
clocksipg 时钟(总线时钟)+ per 时钟(功能时钟)
#pwm-cells外设引用此 PWM 时需提供的参数个数(2 = 通道号 + 极性标志)
status必须在板级 dts 设为 "okay" 才会触发 probe

四、probe 函数分析

匹配成功后,内核调用 imx_pwm_probe():

static int imx_pwm_probe(struct platform_device *pdev)
{
    const struct of_device_id *of_id =
        of_match_device(imx_pwm_dt_ids, &pdev->dev);
    const struct imx_pwm_data *data;
    struct imx_chip *imx;
    struct resource *r;
    int ret = 0;

    if (!of_id)
        return -ENODEV;

    data = of_id->data;   /* 取出匹配到的版本差异数据(v1/v2) */

    /* 1. 分配私有数据结构(devm 托管,remove 时自动释放) */
    imx = devm_kzalloc(&pdev->dev, sizeof(*imx), GFP_KERNEL);
    if (imx == NULL)
        return -ENOMEM;

    /* 2. 获取功能时钟 */
    imx->clk_per = devm_clk_get(&pdev->dev, "per");
    if (IS_ERR(imx->clk_per)) {
        dev_err(&pdev->dev, "getting per clock failed with %ldn",
                PTR_ERR(imx->clk_per));
        return PTR_ERR(imx->clk_per);
    }

    /* 3. 填充 pwm_chip(核心层通用结构体) */
    imx->chip.ops  = data->ops;   /* 绑定 Host 驱动的寄存器操作函数 */
    imx->chip.dev  = &pdev->dev;
    imx->chip.base = -1;          /* -1:由内核自动分配 PWM 全局编号 */
    imx->chip.npwm = 1;           /* 该控制器管理 1 路 PWM */

    if (data->polarity_supported) {
        imx->chip.of_xlate       = of_pwm_xlate_with_flags;
        imx->chip.of_pwm_n_cells = 3;   /* 支持极性参数时,DTS 引用需 3 个 cell */
    }

    /* 4. 映射寄存器地址(DTS reg → 内核虚拟地址) */
    r = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    imx->mmio_base = devm_ioremap_resource(&pdev->dev, r);
    if (IS_ERR(imx->mmio_base))
        return PTR_ERR(imx->mmio_base);

    /* 5. 向 PWM 核心层注册 chip,触发 /sys/class/pwm/pwmchipN 创建 */
    ret = pwmchip_add(&imx->chip);
    if (ret < 0)
        return ret;

    platform_set_drvdata(pdev, imx);
    return 0;
}

4.1 私有数据结构 struct imx_chip

struct imx_chip {
    struct clk      *clk_per;    /* 功能时钟句柄 */
    void __iomem    *mmio_base;  /* 寄存器虚拟基地址 */
    struct pwm_chip  chip;       /* 核心层通用结构体,必须嵌入 */
};

几乎所有 PWM Host 驱动都遵循这个模式:私有结构体嵌入 struct pwm_chip,其余成员是芯片相关资源(时钟、寄存器地址、版本标志等)。

4.2 核心层通用结构体 struct pwm_chip

struct pwm_chip {
    struct device          *dev;
    struct list_head        list;
    const struct pwm_ops   *ops;       /* Host 驱动提供的操作函数集 */
    int                     base;      /* 第一路 PWM 的全局编号 */
    unsigned int            npwm;      /* 该 chip 管理的 PWM 路数 */
    struct pwm_device      *pwms;      /* 核心层分配的 pwm_device 数组 */
    struct pwm_device *(*of_xlate)(struct pwm_chip *pc,
                                   const struct of_phandle_args *args);
    unsigned int            of_pwm_n_cells;
};

Host 驱动填充 pwm_chip 后调用 pwmchip_add() 注册,核心层接管后续的状态管理和 sysfs 节点创建。

4.3 Host 驱动操作接口 struct pwm_ops

static const struct pwm_ops imx_pwm_ops_v1 = {
    .enable  = imx_pwm_enable_v1,   /* 写使能寄存器,打开 PWM 输出 */
    .disable = imx_pwm_disable_v1,  /* 写使能寄存器,关闭 PWM 输出 */
    .config  = imx_pwm_config_v1,   /* 根据 duty_ns/period_ns 计算并写寄存器 */
    .owner   = THIS_MODULE,
};

pwm_ops 是 Host 驱动暴露给核心层的唯一接口。这三个函数只操作寄存器,不维护 pwm_state——状态更新是核心层在调用 ops 前后完成的职责。


五、sysfs 操作接口

pwmchip_add() 执行成功后,内核在 /sys/class/pwm/ 下创建对应的 chip 目录。

5.1 目录结构

/sys/class/pwm/
└── pwmchip0/           ← 对应一个 PWM 控制器
    ├── npwm            ← 只读,该 chip 的 PWM 通道总数
    ├── export          ← 写通道号,申请并创建 pwmN 子目录
    ├── unexport        ← 写通道号,释放通道
    └── pwm0/           ← export 后出现
        ├── period      ← 读写,周期(纳秒)
        ├── duty_cycle  ← 读写,高电平时长(纳秒,必须 ≤ period)
        ├── polarity    ← 读写,极性:normal / inversed
        └── enable      ← 读写,1=输出,0=停止

5.2 完整操作步骤

# 1. 申请 PWM 通道 0(创建 pwm0 目录)
echo 0 > /sys/class/pwm/pwmchip0/export

# 2. 设置周期(10000ns = 100kHz)
echo 10000 > /sys/class/pwm/pwmchip0/pwm0/period

# 3. 设置占空比(5000ns = 50%)
echo 5000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle

# 4. 使能输出
echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable

# 5. 运行时动态调整占空比
echo 7500 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle   # 75%

# 6. 关闭输出
echo 0 > /sys/class/pwm/pwmchip0/pwm0/enable

# 7. 释放通道
echo 0 > /sys/class/pwm/pwmchip0/unexport

5.3 注意事项

  • 操作顺序:必须先写 period,再写 duty_cycle,最后写 enable。若 period 还是 0 就写 duty_cycle 会报错(duty_cycle 不能大于 period)
  • 单位:全部是纳秒(ns)
  • polarity:必须在 enable=0 时修改;默认 normal(高有效)
  • 读取值:读到的是核心层 pwm_state 中的软件记录值,不是直接读硬件寄存器

关联笔记

  • PWM驱动sysfs属性与DTS不匹配 — 调试案例:probe 绕过核心层导致 sysfs 全部显示 0,与本文架构说明互为参照
  • 平台设备驱动模型 — platformdriver 注册、of_match_table 匹配、devm* 资源管理的通用机制

延伸阅读

理解 PWM Host 驱动后,可结合蜂鸣器场景和 Platform 模型继续学习。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部