PWM驱动

脉冲宽度调制(PWM)的工作原理就像一个开关,不断循环开和关。它是一种硬件功能,用于控制伺服电机,进行电压调节等。PWM最广为人知的应用有:

  • 电机转速控制
  • 亮度调节
  • 电压调整

下面用一个简单的图表来介绍PWM:

 

 上图展示了一个完整的PWM周期,介绍了一些术语,在深入了解核心PWM框架之前,我们需要说明:

  • Ton: 信号为高的持续时间。
  • Toff: 信号为低的持续时间。
  • Period(周期): 这是一个完整的PWM周期的持续时间。它表示PWM信号的Ton和Toff之和。
  • Duty cycle(占空比):表示在一个PWM信号周期内,信号保持为高的时间占整个周期的百分比。

不同的公式详细如下:

  • PWM周期: 
  • 占空比:

您可以在https://en.wikipedia.org/wiki/Pulse-width_modulation上找到有关PWM的详细信息。

Linux PWM框架有两个接口:

  1. 控制器接口: 产生PWM输出的接口。它就是PWM芯片,也就是产生器。
  2. 消费者接口: 设备消费由控制器产生的PWM输出。这种设备的驱动程序使用 由控制器通过通用PWM框架导出的 辅助函数。

消费者或生产者接口都依赖于以下头文件:

#include <linux/pwm.h>

在这里,我们将讨论以下问题:

  • PWM驱动器的架构和数据结构,控制器和消费者,以及一个虚拟驱动程序
  • 在设备树中实例化PWM设备和控制器
  • 请求和消费PWM设备
  • 从用户空间使用PWM,通过sysfs接口

PWM控制器驱动程序

当编写GPIO-controller驱动程序时需要struct gpio_chip,编写IRQ-controller驱动程序时需要struct irq_chip, PWM控制器在内核中被表示为struct pwm_chip结构的实例:

 

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: 这表示与pwm chip相关联的设备。
  • ops:这是一个提供回调函数的数据结构,向消费者驱动程序公开。
  • base: 这个该pwm芯片控制的第一个PWM数字编号,如果chip->base < 0 ,然后内核将动态地分配一个基础编号。
  • can_sleep: 这应该被芯片驱动程序设置为true,如果.config()、.enable()、.disable()等ops字段的操作可能处于休眠状态。
  • npwm: 这是该芯片提供的PWM通道(设备)的数量。
  • pwms: 这是该芯片的PWM设备的数组,由框架分配给消费者驱动程序。
  • of_xlate: 这是一个可选的回调,用于请求PWM设备,给定DT PWM指示符。如果没有定义,它将被PWM核心设置为of_pwm_simple_xlate,这也将强制of_pwm_n_cells为2。
  • of_pwm_n_cells: 这是DT中PWM指示符所需的cell的数量。

PWM控制器/芯片的添加和移除依赖于两个基本函数:pwmchip_add()和pwmchip_remove()。每个函数都应该被赋予一个填充的struct pwm_chip结构体作为参数。它们各自的原型如下:

int pwmchip_add(struct pwm_chip *chip)
int pwmchip_remove(struct pwm_chip *chip)

与其他没有返回值的框架remove函数不同,pwmchip_remove()有返回值。如果成功,它将返回0,如果芯片仍在使用(仍然请求)PWM线路,则返回-EBUSY。

每个PWM驱动程序必须通过struct pwm_ops字段实现一些回调函数,PWM核心或消费者接口使用它来配置和充分利用其PWM通道。有些是可选的:

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);
  int (*apply)(struct pwm_chip *chip, struct pwm_device *pwm,
          struct pwm_state *state);   
void (*get_state)(struct pwm_chip *chip, struct pwm_device *pwm,               struct pwm_state *state); /* since kernel v4.7 */   struct module *owner; };

结构中每个元素的含义:

  • request: 这是一个可选的回调,如果提供,将在PWM通道请求期间执行。
  • free: 这和request一样,在PWM释放期间运行。
  • config: 这是PMW配置回调,它为该PWM配置占空比和周期长度。
  • set_polarity: 这个回调配置这个PWM的极性。
  • enable: 使能PWM线路,开始输出切换。
  • disable: 关闭PWM线路,停止输出切换
  • apply: 自动应用新的PWM配置。state参数应该根据实际硬件配置进行调整。
  • get_state: 返回当前的PWM状态。当PWM芯片注册时,这个函数在每个PWM器件中只被调用一次。
  • owner: 这是拥有PWM芯片的模块,通常是THIS_MODULE。

在PWM控制器驱动的probe函数中,检索DT资源,初始化硬件,填充struct pwm_chip及其struct pwm_ops,然后用pwmchip_add函数添加PWM芯片是一个很好的做法。

驱动例子

现在,让我们通过为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 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_DESCRIPTION("Fake pwm driver"); MODULE_LICENSE("GPL");

PWM控制器绑定 

当从DT内部绑定PWM控制器时,最重要的属性是#pwm-cells。它表示用于表示该控制器上PWM器件的单元数。在struct pwm_chip结构中,of_xlate钩子用于转换给定的PWM指示符。如果没有设置of_xlate回调,pwm-cells必须设置为2;否则,它应该设置为与of_pwm_n_cells相同的值。下面是一个i.MX6 SoC的DT中的PWM控制器节点示例:

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";
};

对应于我们的fake-pwm驱动程序的节点看起来像这样:

fake_pwm: pwm@0 {
  #pwm-cells = <2>;
  compatible = "packt,fake-pwm";
  /*
   * Our driver does not use resource
   * neither mem, IRQ, nor Clock)
   */
};

PWM消费者接口

消费者是实际使用PWM通道的设备。PWM通道在内核中表示为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: 表示与PWM设备相关的标志
  • hwpw: 这是PWM设备在芯片本地的相关索引
  • 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: 这表示附加到该PWM设备的板级相关的PWM参数,这些参数通常从PWM查找表或设备树中检索。PWM参数表示用户希望在该PWM设备上使用的初始配置,而不是当前的PWM硬件状态。
struct pwm_args {
  unsigned int period; /* Device's initial period */
  enum pwm_polarity polarity;
};
  • state: 表示当前PWM通道状态:
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框架面临着一些变化。这些变化与从消费者端请求PWM设备的方式有关。我们可以把消费者接口分成两部分,或者更准确地说,分成两个版本:

1. 旧版本,使用pwm_request()和pwm_free()来请求PWM设备和在使用后释放它。

2. 新且推荐的APIs,使用pwm_get()和pwm_put()函数。前者给出消费者设备和通道名称,作为请求PWM设备的参数,而后者给出PWM设备,作为释放的参数。这些函数的资源管理变体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()和pwm_free()/pwm_put(),因为PWM核心使用互斥对象,可能处于休眠状态。

请求后,必须配置PWM,使用以下命令:

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

要启动/停止PWM输出,使用pwm_enable()/pwm_disable()。这两个函数都以指向struct pwm_device的指针作为参数,它们都是控制器通过pwm_chip.ops字段公开的回调函数:

int pwm_enable(struct pwm_device *pwm)
void pwm_disable(struct pwm_device *pwm)

pwm_enable()成功时返回0,失败时返回负错误代码。PWM消费者驱动程序的一个很好的例子是内核源代码树中的drivers/leds/leds-pwm.c。下面是一个消费者代码的例子,驱动PWM LED:

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); }

 PWM客户端绑定

PWM设备可以从以下位置分配给消费者:

  • 设备树
  • ACPI
  • 静态查找表,在单板初始化文件中

这里只处理DT绑定,因为这是推荐的方法。当将PWM消费者(客户端)绑定到它的驱动程序时,您需要提供它所链接到的控制器的phandle。

建议将PWM属性命名为pwms;因为PWM设备是命名资源,你可以提供一个可选的属性,pwm-names,包含一个字符串列表,来命名pwms属性中列出的每个PWM设备。如果没有给出pwm-names属性,则用户节点的名称将用作备用。

对于使用多个PWM设备的设备,驱动程序可以使用pwm-names属性将pwm_get()调用请求的PWM设备的名称映射到pwms属性给出的列表中的索引。

下面的例子描述了一个基于PWM的背光设备,它摘自PWM设备绑定的内核文档(参见Documentation/devicetree/bindings/pwm/pwm.txt):

pwm: pwm {
  #pwm-cells = <2>;
};
[...]
bl: backlight {   pwms
= <&pwm 0 5000000>;   pwm-names = "backlight"; };

PWM-specifier通常编码相对于芯片的PWM号和以纳秒为单位的PWM周期,如下所示:

pwms = <&pwm 0 5000000>;

0对应PWM索引,相对于控制器。5000000表示周期,单位为纳秒。注意,在前面的示例中,指定pmm-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;
}

PWM指示器通常编码相对于芯片的PWM号和以纳秒为单位的PWM周期。

使用带有sysfs接口的PWMs

PWM核心sysfs根路径为“/sys/class/pwm/”。它是管理PWM设备的用户空间方式。添加到系统中的每个PWM控制器/芯片都会在sysfs根路径下创建一个pwmchipN目录条目,其中N是PWM芯片的基础索引。目录中包含以下文件:

  • npwm: 这是一个只读文件,表示芯片支持的PWM通道数量
  • 导出: 这是一个只写的文件,允许你导出用于sysfs的PWM通道(此功能类似于GPIO sysfs接口)
  • unexport: 从sysfs中取消导出PWM通道(只写)

PWM通道使用从0到pwm<n-1>的索引进行编号。这些编号是芯片的本地编号。每个PWM通道导出在pwmchipN中创建一个pwmX目录,该目录与包含使用的导出文件的目录相同。其中X为导出的通道号。每个通道目录包含以下文件:

  • period: 这是一个可读/可写的文件,用于获取/设置PWM信号的总周期。单位为纳秒。
  • duty_cycle: 这是一个可读/可写的文件,用于获取/设置PWM信号的占空比。它表示PWM信号的活动时间。该值以纳秒为单位,且必须小于周期。
  • polarity: 这是一个可读/可写的文件,只有当该PWM器件的芯片支持极性反转时才使用。最好只在该PWM未启用时才更改极性。接受的值为字符串"normal"或者“inversed”。
  • enable: 这是一个可读/可写的文件,用于启用(开始切换)或禁用(停止切换)PWM信号。接受的值如下:
    • 0: disabled
    • 1: enabled

下面是通过sysfs接口从用户空间使用PWM的示例:

  1. 使能PWM
    # echo 1 > /sys/class/pwm/pwmchip<pwmchipnr>/pwm<pwmnr>/enable
  2. 设置PWM周期:
    # echo <value in nanoseconds> > /sys/class/pwm/pwmchip<pwmchipnr>/pwm<pwmnr>/period
  3. 设置PWM占空比;占空比的值必须小于PWM周期的值:
    # echo <value in nanoseconds> > /sys/class/pwm/pwmchip<pwmchipnr>/pwm<pwmnr>/duty_cycle
  4. 禁用PWM:
    # echo 0 > /sys/class/pwm/pwmchip<pwmchipnr>/pwm<pwmnr>/enable

完整的PWM框架API和sysfs描述可在内核源代码树中的Documentation/pwm.txt文件中找到。

现在你应该已经准备好处理任何PWM控制器,无论是内存映射还是外部位于总线上。本章中描述的API足以编写和增强控制器驱动程序作为消费者设备驱动程序。如果您对PWM内核方面还不熟悉,可以完全使用用户空间sysfs接口。

posted @ 2023-02-20 20:21  闹闹爸爸  阅读(439)  评论(0编辑  收藏  举报