本文以 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 |
interrupts | GIC 中断控制器,SPI 类型,中断号 116,高电平触发 |
clocks | ipg 时钟(总线时钟)+ 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 模型继续学习。
- 返回嵌入式 Linux 学习路线,确认本文在完整知识结构中的位置。
- Beep Platform 驱动开发(miscdevice 框架)
- Linux Platform 设备驱动模型




