脉冲宽度调制 ( 脉宽调制)就像一个不断循环开关一样运行。它是一种硬件特性,用于控制伺服电机、电压调节等。脉宽调制最著名的应用有:
- 电动机转速控制
- 灯光变暗
- 电压调整
现在,让我们用一个简单的下图来介绍脉宽调制:
上图描述了一个完整的脉宽调制周期,介绍了在深入了解内核脉宽调制框架之前我们需要澄清的一些术语:
Ton
:这是信号为高电平的持续时间。Toff
:这是信号为低电平的持续时间。Period
:这是一个完整的 PWM 周期的持续时间。它代表脉宽调制信号的Ton
和Toff
之和。Duty cycle
:表示在脉宽调制信号期间保持开启的时间信号的百分比。
不同的公式详述如下:
You can find details about PWM at https://en.wikipedia.org/wiki/Pulse-width_modulation.
Linux 脉宽调制框架有两个接口:
- 控制器界面:暴露 PWM 线的那个。是 PWM 芯片,也就是生产者。
- 消费接口:消耗控制器暴露的 PWM 线的设备。这种设备的驱动使用控制器通过通用脉宽调制框架输出的辅助功能。
使用者或生产者接口取决于以下头文件:
#include <linux/pwm.h>
在本章中,我们将讨论:
- 用于控制器和消费者的脉宽调制驱动架构和数据结构,以及虚拟驱动
- 在设备树中实例化脉宽调制设备和控制器
- 请求和消费脉宽调制设备
- 通过 sysfs 接口从用户空间使用脉宽调制
当您在编写 GPIO 控制器驱动时需要struct gpio_chip
,在编写 IRQ 控制器驱动时需要struct irq_chip
,一个脉宽调制控制器在内核中被表示为struct pwm_chip
结构的一个实例。
PWM controller and devices
struct pwm_chip {
struct device *dev;
const struct pwm_ops *ops;
int base;
unsigned int npwm;
struct pwm_device *pwms;
struct pwm_device * (*of_xlate)(struct pwm_chip *pc,
const struct of_phandle_args *args);
unsigned int of_pwm_n_cells;
bool can_sleep;
};
以下是结构中每个元素的含义:
dev
:表示与该芯片关联的设备。Ops
:这是一个数据结构,提供了这个芯片向消费者驱动公开的回调函数。Base
:这是这个芯片控制的第一个 PWM 的个数。如果chip->base < 0
那么,内核会动态分配一个基数。can_sleep
:如果操作场的.config()
、.enable()
或.disable()
操作可能休眠,芯片驱动应将其设置为true
。npwm
:这是这个芯片提供的 PWM 通道(器件)数量。pwms
:这是这个芯片的 PWM 器件的阵列,由框架分配给消费者驱动。of_xlate
:这是一个可选的回调,用于请求给定 DT PWM 说明符的 PWM 设备。如果没有定义,它将被脉宽调制核心设置为of_pwm_simple_xlate
,这将迫使of_pwm_n_cells
也变为2
。of_pwm_n_cells
:这是脉宽调制说明符的 DT 中预期的单元数。
PWM 控制器/芯片的添加和移除依赖于两个基本功能,pwmchip_add()
和pwmchip_remove()
。每个函数都应该有一个填充的struct pwm_chip
结构作为参数。它们各自的原型如下:
int pwmchip_add(struct pwm_chip *chip)
int pwmchip_remove(struct pwm_chip *chip)
与其他没有返回值的框架移除函数不同,pwmchip_remove()
有一个返回值。成功时返回0
,如果芯片有一条脉宽调制线仍在使用(仍被请求),则返回-EBUSY
。
每个脉宽调制驱动必须通过struct pwm_ops
字段实现一些挂钩,该字段由脉宽调制核心或消费者接口使用,以便配置和充分利用其脉宽调制通道。其中一些是可选的。
struct pwm_ops {
int (*request)(struct pwm_chip *chip, struct pwm_device *pwm);
void (*free)(struct pwm_chip *chip, struct pwm_device *pwm);
int (*config)(struct pwm_chip *chip, struct pwm_device *pwm,
int duty_ns, int period_ns);
int (*set_polarity)(struct pwm_chip *chip, struct pwm_device *pwm,
enum pwm_polarity polarity);
int (*enable)(struct pwm_chip *chip,struct pwm_device *pwm);
void (*disable)(struct pwm_chip *chip, struct pwm_device *pwm);
void (*get_state)(struct pwm_chip *chip, struct pwm_device *pwm,
struct pwm_state *state); /* since kernel v4.7 */
struct module *owner;
};
让我们看看结构中的每个元素意味着什么:
request
:这是一个可选的钩子,如果提供的话,在脉宽调制通道请求期间执行。free
:这与请求相同,在脉宽调制释放期间运行。config
:这是 PMW 配置钩子。它为该脉宽调制配置占空比和周期长度。set_polarity
:这个钩子配置这个 PWM 的极性。Enable
:这使能脉宽调制线,开始输出切换。Disable
:这将禁用脉宽调制线,停止输出切换。Apply
:这自动应用了一个新的脉宽调制配置。状态参数应该根据实际的硬件配置进行调整。get_state
:返回当前 PWM 状态。注册脉宽调制芯片时,每个脉宽调制器件只调用一次该功能。Owner
:这是拥有这个芯片的模块,通常是THIS_MODULE
。
在 PWM 控制器驱动的probe
功能中,好的做法是检索 DT 资源,初始化硬件,填充一个struct pwm_chip
及其struct pwm_ops
,然后,添加带有pwmchip_add
功能的 PWM 芯片。
现在,让我们通过为一个脉宽调制控制器编写一个虚拟驱动来总结一下,该控制器有三个通道:
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/pwm.h>
struct fake_chip {
struct pwm_chip chip;
int foo;
int bar;
/* put the client structure here (SPI/I2C) */
};
static inline struct fake_chip *to_fake_chip(struct pwm_chip *chip)
{
return container_of(chip, struct fake_chip, chip);
}
static int fake_pwm_request(struct pwm_chip *chip,
struct pwm_device *pwm)
{
/*
* One may need to do some initialization when a PWM channel
* of the controller is requested. This should be done here.
*
* One may do something like
* prepare_pwm_device(struct pwm_chip *chip, pwm->hwpwm);
*/
return 0;
}
static int fake_pwm_config(struct pwm_chip *chip,
struct pwm_device *pwm,
int duty_ns, int period_ns)
{
/*
* In this function, one ne can do something like:
* struct fake_chip *priv = to_fake_chip(chip);
*
* return send_command_to_set_config(priv,
* duty_ns, period_ns);
*/
return 0;
}
static int fake_pwm_enable(struct pwm_chip *chip, struct pwm_device *pwm)
{
/*
* In this function, one ne can do something like:
* struct fake_chip *priv = to_fake_chip(chip);
*
* return foo_chip_set_pwm_enable(priv, pwm->hwpwm, true);
*/
pr_info("Somebody enabled PWM device number %d of this chip",
pwm->hwpwm);
return 0;
}
static void fake_pwm_disable(struct pwm_chip *chip,
struct pwm_device *pwm)
{
/*
* In this function, one ne can do something like:
* struct fake_chip *priv = to_fake_chip(chip);
*
* return foo_chip_set_pwm_enable(priv, pwm->hwpwm, false);
*/
pr_info("Somebody disabled PWM device number %d of this chip",
pwm->hwpwm);
}
static const struct pwm_ops fake_pwm_ops = {
.request = fake_pwm_request,
.config = fake_pwm_config,
.enable = fake_pwm_enable,
.disable = fake_pwm_disable,
.owner = THIS_MODULE,
};
static int fake_pwm_probe(struct platform_device *pdev)
{
struct fake_chip *priv;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
priv->chip.ops = &fake_pwm_ops;
priv->chip.dev = &pdev->dev;
priv->chip.base = -1; /* Dynamic base */
priv->chip.npwm = 3; /* 3 channel controller */
platform_set_drvdata(pdev, priv);
return pwmchip_add(&priv->chip);
}
static int fake_pwm_remove(struct platform_device *pdev)
{
struct fake_chip *priv = platform_get_drvdata(pdev);
return pwmchip_remove(&priv->chip);
}
static const struct of_device_id fake_pwm_dt_ids[] = {
{ .compatible = "packt,fake-pwm", },
{ }
};
MODULE_DEVICE_TABLE(of, fake_pwm_dt_ids);
static struct platform_driver fake_pwm_driver = {
.driver = {
.name = KBUILD_MODNAME,
.owner = THIS_MODULE,
.of_match_table = of_match_ptr(fake_pwm_dt_ids),
},
.probe = fake_pwm_probe,
.remove = fake_pwm_remove,
};
module_platform_driver(fake_pwm_driver);
MODULE_AUTHOR("John Madieu <john.madieu@gmail.com>");
MODULE_DESCRIPTION("Fake pwm driver");
MODULE_LICENSE("GPL");
从 DT 内部绑定 PWM 控制器时,最重要的属性是#pwm-cells
。它表示用于表示该控制器的脉宽调制设备的单元数量。如果你记得的话,在struct pwm_chip
结构中,of_xlate
钩子用于翻译给定的脉宽调制说明符。如果没有设置钩子,这里的pwm-cells
必须设置为 2,否则应该设置为与of_pwm_n_cells
相同的值。下面是一个例子的脉宽调制控制器节点在 DT,为一个 i.MX6 系统芯片。
pwm3: pwm@02088000 {
#pwm-cells = <2>;
compatible = "fsl,imx6q-pwm", "fsl,imx27-pwm";
reg = <0x02088000 0x4000>;
interrupts = <0 85 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6QDL_CLK_IPG>,
<&clks IMX6QDL_CLK_PWM3>;
clock-names = "ipg", "per";
status = "disabled";
};
另一方面,对应于假 pwm 驱动的节点看起来像:
fake_pwm: pwm@0 {
#pwm-cells = <2>;
compatible = "packt,fake-pwm";
/*
* Our driver does not use resource
* neither mem, IRQ, nor Clock)
*/
};
消费者是实际使用脉宽调制通道的设备。脉宽调制通道在内核中表示为struct pwm_device
结构的一个实例:
struct pwm_device {
const char *label;
unsigned long flags;
unsigned int hwpwm;
unsigned int pwm;
struct pwm_chip *chip;
void *chip_data;
unsigned int period; /* in nanoseconds */
unsigned int duty_cycle; /* in nanoseconds */
enum pwm_polarity polarity;
};
Label
:这是这个 PWM 设备的名字Flags
:这表示与脉宽调制设备相关的标志hwpw
:这是脉宽调制器件的相对指数,在芯片本地pwm
:这是 PWM 设备的系统全局索引chip
:这是一个 PWM 芯片,提供这个 PWM 器件的控制器chip_data
:这是与这个 PWM 设备相关的芯片私有数据
从内核 v4.7 开始,结构变为:
struct pwm_device {
const char *label;
unsigned long flags;
unsigned int hwpwm;
unsigned int pwm;
struct pwm_chip *chip;
void *chip_data;
struct pwm_args args;
struct pwm_state state;
};
args
:这表示附加到该脉宽调制设备的依赖于板的脉宽调制参数,通常从脉宽调制查找表或设备树中检索。脉宽调制参数代表用户希望在该脉宽调制设备上使用的初始配置,而不是当前的脉宽调制硬件状态。state
:表示当前 PWM 通道状态。
struct pwm_args {
unsigned int period; /* Device's nitial period */
enum pwm_polarity polarity;
};
struct pwm_state {
unsigned int period; /* PWM period (in nanoseconds) */
unsigned int duty_cycle; /* PWM duty cycle (in nanoseconds) */
enum pwm_polarity polarity; /* PWM polarity */
bool enabled; /* PWM enabled status */
}
随着 Linux 的发展,脉宽调制框架面临着一些变化。这些变化关系到用户侧请求脉宽调制设备的方式。我们可以将消费者界面分成两部分,或者更准确地说,分成两个版本。
旧版,使用pwm_request()
和pwm_free()
是为了申请一个 PWM 设备,使用后释放。
新增推荐 API ,使用pwm_get()
和pwm_put()
功能。前者被赋予消费设备和通道名作为请求脉宽调制设备的参数,而后者被赋予要释放的脉宽调制设备作为参数。这些功能的托管变体devm_pwm_get()
和devm_pwm_put()
也存在。
struct pwm_device *pwm_get(struct device *dev, const char *con_id)
void pwm_put(struct pwm_device *pwm)
pwm_request()
/pwm_get()
and pwm_free()
/pwm_put()
cannot be called from an atomic context, since the PWM core make use of mutexes, which may sleep.
请求后,必须使用以下方式配置脉宽调制:
int pwm_config(struct pwm_device *pwm, int duty_ns, int period_ns);
要启动/停止切换脉宽调制输出,请使用pwm_enable()
/ pwm_disable()
。这两个函数都以指向一个struct pwm_device
的指针作为参数,并且都是控制器通过pwm_chip.pwm_ops
字段暴露的钩子的包装器。
int pwm_enable(struct pwm_device *pwm)
void pwm_disable(struct pwm_device *pwm)
pwm_enable()
成功返回0
,失败返回负错误代码。脉宽调制消费驱动的一个很好的例子是内核源代码树中的drivers/leds/leds-pwm.c
。以下是驱动脉宽调制发光二极管的消费者代码示例:
static void pwm_led_drive(struct pwm_device *pwm,
struct private_data *priv)
{
/* Configure the PWM, applying a period and duty cycle */
pwm_config(pwm, priv->duty, priv->pwm_period);
/* Start toggling */
pwm_enable(pchip->pwmd);
[...] /* Do some work */
/* And then stop toggling*/
pwm_disable(pchip->pwmd);
}
脉宽调制设备可以从以下方面分配给用户:
- 设备树
- 高级配置与电源接口(Advanced Configuration and Power Interface)
- 静态查找表,在板
init
文件中。
这本书只讨论 DT 装订,因为这是推荐的方法。当将一个脉宽调制消费程序(客户端)绑定到它的驱动时,您需要提供它所链接到的控制器的指针。
建议你给 PWM 属性起个名字pwms
;由于脉宽调制设备被命名为资源,您可以提供一个可选属性pwm-names,
,它包含一个字符串列表来命名在pwms
属性中列出的每个脉宽调制设备。如果没有给出pwm-names
属性,用户节点的名称将作为回退。
使用多个脉宽调制设备的设备驱动可以使用pwm-names
属性将pwm_get()
调用请求的脉宽调制设备名称映射到由pwms
属性给出的列表索引中。
下面的例子描述了一个基于脉宽调制的背光设备,这是从关于脉宽调制设备绑定的内核文档中摘录的(参见文档/设备树/绑定/脉宽调制/脉宽调制. txt ):
pwm: pwm {
#pwm-cells = <2>;
};
[...]
bl: backlight {
pwms = <&pwm 0 5000000>;
pwm-names = "backlight";
};
脉宽调制说明符通常以纳秒为单位对芯片相关的脉宽调制数和脉宽调制周期进行编码。其内容如下:
pwms = <&pwm 0 5000000>;
0
对应相对于控制器的 PWM 指数,5000000
代表以纳秒为单位的周期。请注意,在前面的示例中,指定pwm-names
是多余的,因为名称backlight
无论如何都将用作后备。因此,驾驶员必须呼叫:
static int my_consummer_probe(struct platform_device *pdev)
{
struct pwm_device *pwm;
pwm = pwm_get(&pdev->dev, "backlight");
if (IS_ERR(pwm)) {
pr_info("unable to request PWM, trying legacy API\n");
/*
* Some drivers use the legacy API as fallback, in order
* to request a PWM ID, global to the system
* pwm = pwm_request(global_pwm_id, "pwm beeper");
*/
}
[...]
return 0;
}
The PWM-specifier typically encodes the chip-relative PWM number and the PWM period in nanoseconds.
PWM 核心sysfs
根路径为/sys/class/pwm/
。管理脉宽调制设备是用户空间的方式。添加到系统中的每个脉宽调制控制器/芯片在sysfs
根路径下创建一个pwmchipN
目录条目,其中N
是脉宽调制芯片的基础。该目录包含以下文件:
npwm
:这是一个只读文件,打印这个芯片支持的 PWM 通道数Export
:这是一个只写文件,允许导出一个 PWM 通道供sysfs
使用(该功能相当于 GPIO sysfs 接口)Unexport
:从sysfs
中取消导出一个脉宽调制通道(只写)
脉宽调制通道使用从 0 到pwm<n-1>
的索引进行编号。这些数字是芯片本地的。每个脉宽调制通道输出在pwmchipN
中创建一个pwmX
目录,该目录与包含所用export
文件的目录相同。 X 是出口的通道号。每个频道目录包含以下文件:
-
Period
:这是一个可读/可写的文件,用来获取/设置 PWM 信号的总周期。值以纳秒为单位。 -
duty_cycle
:这是一个可读/可写的文件,用于获取/设置 PWM 信号的占空比。它表示脉宽调制信号的有效时间。值以纳秒为单位,必须始终小于周期。 -
Polarity
:这是一个可读/可写的文件,只有在这个 PWM 器件的芯片支持极性反转的情况下才能使用。最好仅在该脉宽调制未启用时改变极性。接受值为字符串正常或反转。 -
Enable
:这是一个可读/可写的文件,用于启用(开始切换)/禁用(停止切换)脉宽调制信号。接受的值有: -
0:已禁用
-
1:已启用
以下是通过sysfs
界面从用户空间使用脉宽调制的示例:
- 启用脉宽调制:
# echo 1 > /sys/class/pwm/pwmchip<pwmchipnr>/pwm<pwmnr>/enable
- 设置脉宽调制周期:
# echo **<value in nanoseconds> >** /sys/class/pwm/pwmchip**<pwmchipnr>**/pwm**<pwmnr>**/period
- 设置脉宽调制占空比:占空比的值必须小于脉宽调制周期的值:
# echo **<value in nanoseconds>** > /sys/class/pwm/pwmchip**<pwmchipnr>**/pwm**<pwmnr>**/duty_cycle
- 禁用脉宽调制:
# echo 0 > /sys/class/pwm/pwmchip<pwmchipnr>/pwm<pwmnr>/enable
The complete PWM framework API and sysfs description is available in the Documentation/pwm.txt file, in the kernel source tree.
到本章结束时,您已经为任何脉宽调制控制器做好了准备,无论它是内存映射的,还是外部总线上的。本章中描述的应用编程接口将足以编写和增强控制器驱动作为消费设备驱动。如果你还不习惯脉宽调制内核端,你可以充分利用用户空间 sysfs 接口。也就是说,在下一章中,我们将讨论调节器,它有时由脉宽调制驱动。所以,请坚持住,我们快完成了。