Linux驱动-使用软定时器实现PWM输出
在没有pwm外设的情况下,可以使用定时器+GPIO的方法来实现pwm输出,实现pwm频率和占空比可控的功能。本文实现了一个Linux内核驱动,使用两个软定时器来实现pwm输出功能。但是受软定时器时间精度的影响,这种方式实现的pwm输出频率不可能非常高,其占空比可调粒度也较小,但是对于一般的风扇控制是足够了。
1. 设备树节点
/ {
pwm-soft {
compatible = "thj,pwm-soft"; /* compatible属性 */
gpios = <&gpio0 8 0>; /* pwm输出gpio */
npwm = <1>; /* pwm通道个数 */
};
};
pwm-soft
节点的compatible
是驱动匹配属性,gpios
是输出pwm使用的gpio,这个可以根据具体的硬件平台自行配置,npwm
是pwm的通道个数。
2. 内核驱动实现
该驱动文件名为pwm-soft.c
,采用platform
平台驱动进行编写,驱动的具体实现采用内核的pwm驱动框架。将产生pwm所需要的资源封装成一个结构进行统一管理:
struct pwm_soft_dev {
struct pwm_chip chip; /* pwm设备 */
int gpio; /* pwm输出gpio */
struct device_node *node; /* pwm设备节点 */
struct timer_list period_timer; /* 周期定时器 */
struct timer_list duty_timer; /* 占空比定时器 */
unsigned int period; /* pwm周期 */
unsigned int duty; /* pwm占空比 */
};
其中,chip
成员是内核pwm驱动框架的注册对象。gpio
是用于输出pwm信号,node
为pwm-soft
设备树节点;period_timer
和duty_timer
这两个定时器的作用是控制pwm的周期和占空比;period
和duty
为pwm周期和占空比的时间,单位为毫秒,这是因为Linux内核软定时器精度较低。
驱动的实现采用了platform
框架,在加载驱动的时候会根据of_match_table
中的compatible
属性与设备树中的compatible
属性进行匹配,如果匹配成功则probe
函数将会执行,platform_driver
的注册如下:
static const struct of_device_id pwm_soft_of_match[] = {
{.compatible = "thj,pwm-soft"},
{}
};
MODULE_DEVICE_TABLE(of, pwm_soft_of_match);
static struct platform_driver pwm_soft_driver = {
.driver = {
.name = "pwm-soft",
.of_match_table = pwm_soft_of_match
},
.probe = pwm_soft_probe,
.remove = pwm_soft_remove
};
module_platform_driver(pwm_soft_driver);
驱动的匹配表为pwm_soft_of_match
,设备树节点pwm-soft
的compatible
和匹配表的compatible
匹配成功后,pwm_soft_probe
就会主动执行。
在pwm_soft_probe
中的主要工作就是,获取设备树节点中指定的pwm输出gpio,初始化周期和占空比定时器,注册pwm设备。
int pwm_soft_probe(struct platform_device *pdev) {
struct pwm_soft_dev *pwm_soft;
int ret;
/* 申请pwm_soft_dev对象 */
pwm_soft = devm_kzalloc(&pdev->dev, sizeof(struct pwm_soft_dev), GFP_KERNEL);
if(!pwm_soft) {
printk(KERN_ERR"pwm_soft:failed malloc pwm_soft_dev\n");
return -ENOMEM;
}
/* 获取pwm-soft设备树节点 */
pwm_soft->node = of_find_node_by_path("/pwm-soft");
if(pwm_soft->node == NULL) {
printk(KERN_ERR"pwm_soft:failed to get pwm-soft node\n");
return -EINVAL;
}
/* 获取pwm输出gpio */
pwm_soft->gpio = of_get_named_gpio(pwm_soft->node, "gpios", 0);
if(!gpio_is_valid(pwm_soft->gpio)) {
printk(KERN_ERR"pwm_soft:failed to get pwm gpio\n");
return -EINVAL;
}
/* 申请gpio */
ret = gpio_request(pwm_soft->gpio, "pwm-gpio");
if(ret) {
printk(KERN_ERR"pwm_soft:failed to request pwm gpio\n");
}
/* 设置管脚为输出模式 */
gpio_direction_output(pwm_soft->gpio, 0);
/* 初始化周期控制定时器 */
init_timer(&pwm_soft->period_timer);
pwm_soft->period_timer.function = period_timer_function;
pwm_soft->period_timer.data = (unsigned long)pwm_soft;
/* 初始化占空比控制定时器 */
init_timer(&pwm_soft->duty_timer);
pwm_soft->duty_timer.function = duty_timer_function;
pwm_soft->duty_timer.data = (unsigned long)pwm_soft;
/* 注册pwm设备 */
pwm_soft->chip.dev = &pdev->dev;
pwm_soft->chip.ops = &pwm_soft_ops;
pwm_soft->chip.base = 0;
ret = of_property_read_u32(pwm_soft->node, "npwm", &pwm_soft->chip.npwm);
if(ret) {
dev_err(&pdev->dev, "failed to read npwm\n");
return ret;
}
ret = pwmchip_add(&pwm_soft->chip);
if(ret < 0) {
dev_err(&pdev->dev, "pwmchip_add failed:%d\n", ret);
return ret;
}
platform_set_drvdata(pdev, pwm_soft);
return ret;
}
第47~50行代码是对pwm_chip
进行初始化。在内核pwm驱动框架中,用struct pwm_chip
来描述一个pwm设备,该数据结构中包含成员struct pwm_ops
,这个结构体中的函数为pwm设备的底层操作方法,在编写pwm驱动的时候需要实现这些操作方法。最后使用pwmchip_add
将pwm_chip
注册到pwm核心层。
pwm设备的操作方法定义如下:
static const struct pwm_ops pwm_soft_ops = {
.owner = THIS_MODULE,
.config = pwm_soft_config,
.enable = pwm_soft_enable,
.disable = pwm_soft_disable
};
可见驱动中并没有实现pwm_ops
中的所有函数,只实现了其中几个。其中config
用于配置pwm设备的周期和占空比,enable
用于使能pwm输出,disable
用于关闭pwm输出。
pwm_soft_config
实现如下:
int pwm_soft_config(struct pwm_chip *chip, struct pwm_device *pwm,
int duty_ns, int period_ns) {
struct pwm_soft_dev *pwm_soft = to_pwm_soft_dev(chip);
/* 占空比和周期都以ms为单位 */
if(duty_ns >= period_ns) {
duty_ns = period_ns - 1;
}
pwm_soft->period = period_ns;
pwm_soft->duty = duty_ns;
return 0;
}
在该函数中,主要是将应用层配置的周期period_us
和占空比duty_us
赋值给pwm_soft->period
和pwm_soft->duty
,需要注意的是,由于使用的是内核软定时器实现pwm输出,其时间精度较低,在此以1ms为单位。另外,duty_ns = period_ns - 1
目的是防止满占空比的时候两个定时器同时到到,造成输出电平逻辑错误。pwm_soft_enable
和pwm_soft_disable
实现如下:
int pwm_soft_enable(struct pwm_chip *chip, struct pwm_device *pwm) {
struct pwm_soft_dev *pwm_soft = to_pwm_soft_dev(chip);
/* gpio拉高 */
gpio_set_value(pwm_soft->gpio, 1);
/* 开启周期定时器和占空比定时器 */
mod_timer(&pwm_soft->period_timer, jiffies + msecs_to_jiffies(pwm_soft->period));
mod_timer(&pwm_soft->duty_timer, jiffies + msecs_to_jiffies(pwm_soft->duty));
return 0;
}
void pwm_soft_disable(struct pwm_chip *chip, struct pwm_device *pwm) {
struct pwm_soft_dev *pwm_soft = to_pwm_soft_dev(chip);
/* 删除定时器 */
del_timer_sync(&pwm_soft->duty_timer);
del_timer_sync(&pwm_soft->period_timer);
/* gpio拉低*/
gpio_set_value(pwm_soft->gpio, 0);
}
上面函数都用到了to_pwm_soft_dev
,其定义如下:
static inline struct pwm_soft_dev *to_pwm_soft_dev(struct pwm_chip *chip) {
return container_of(chip, struct pwm_soft_dev, chip);
}
container_of
这个宏应该很熟悉了,在此不做介绍。该函数的作用是通过chip
地址得到自定义数据结构pwm_soft_dev
的地址。pwm_soft_enable
和pwm_soft_disable
的实现很简单,就是设置gpio默认输出电平和对两个定时器进行启动和删除。
在pwm_soft_probe
函数中,设置的周期和占空比的超时回调函数为:period_timer_function
,duty_timer_function
。
static void period_timer_function(unsigned long arg) {
struct pwm_soft_dev *pwm_soft = (struct pwm_soft_dev *)arg;
/*设置 gpio为1 */
gpio_set_value(pwm_soft->gpio, 1);
/* 重新开启周期定时器和占空比定时器 */
mod_timer(&pwm_soft->period_timer, jiffies + msecs_to_jiffies(pwm_soft->period));
mod_timer(&pwm_soft->duty_timer, jiffies + msecs_to_jiffies(pwm_soft->duty));
}
static void duty_timer_function(unsigned long arg) {
struct pwm_soft_dev *pwm_soft = (struct pwm_soft_dev *)arg;
/* 设置gpio为0 */
gpio_set_value(pwm_soft->gpio, 0);
}
这两个超时回调函数就是输出pwm的逻辑,当pwm_soft_enable
执行后,gpio被拉高,两个定时器启动。首先超时的是占空比定时器,在这个超时回调中会将gpio拉低,一直保持到周期定时器超时。在周期定时器超时回调中,会将gpio拉高然后根据周期定时时间和占空比定时器时间重启两个定时器,如此循环就实现了pwm输出。
3. 使用
使用内核自带的pwm驱动框架的好处就是,使用非常简单。使用pwmchip_add
将pwm_chip
注册到pwm核心层和使能pwm内核驱动后,会生成/sys/class/pwm/pwmchip0/
目录,直接操作其中的文件就可以控制pwm输出,如下:
cd /sys/class/pwm/pwmchip0
echo 0 > export #导入pwm通道0
cd ./pwm0
echo 500 > period #设置周期为500ms
echo 250 > duty_cycle #设置占空比为250ms
echo 1 > enable #使能pwm输出
echo 0 > enable #禁止pwm输出
上面是通过shell命令进行pwm控制的,也可以使用文件操作函数进行编程,实现pwm控制。
4. 整体代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/pwm.h>
#include <linux/io.h>
#include <linux/clk.h>
#include <linux/of.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/timer.h>
struct pwm_soft_dev {
struct pwm_chip chip; /* pwm设备 */
int gpio; /* pwm输出gpio */
struct device_node *node; /* pwm设备节点 */
struct timer_list period_timer; /* 周期定时器 */
struct timer_list duty_timer; /* 占空比定时器 */
unsigned int period; /* pwm周期 */
unsigned int duty; /* pwm占空比 */
};
static inline struct pwm_soft_dev *to_pwm_soft_dev(struct pwm_chip *chip) {
return container_of(chip, struct pwm_soft_dev, chip);
}
int pwm_soft_config(struct pwm_chip *chip, struct pwm_device *pwm,
int duty_ns, int period_ns) {
struct pwm_soft_dev *pwm_soft = to_pwm_soft_dev(chip);
/* 占空比和周期都以ms为单位 */
if(duty_ns >= period_ns) {
duty_ns = period_ns - 1;
}
pwm_soft->period = period_ns;
pwm_soft->duty = duty_ns;
return 0;
}
int pwm_soft_enable(struct pwm_chip *chip, struct pwm_device *pwm) {
struct pwm_soft_dev *pwm_soft = to_pwm_soft_dev(chip);
/* gpio拉高 */
gpio_set_value(pwm_soft->gpio, 1);
/* 开启周期定时器和占空比定时器 */
mod_timer(&pwm_soft->period_timer, jiffies + msecs_to_jiffies(pwm_soft->period));
mod_timer(&pwm_soft->duty_timer, jiffies + msecs_to_jiffies(pwm_soft->duty));
return 0;
}
void pwm_soft_disable(struct pwm_chip *chip, struct pwm_device *pwm) {
struct pwm_soft_dev *pwm_soft = to_pwm_soft_dev(chip);
/* 删除定时器 */
del_timer_sync(&pwm_soft->duty_timer);
del_timer_sync(&pwm_soft->period_timer);
/* gpio拉低*/
gpio_set_value(pwm_soft->gpio, 0);
}
static const struct pwm_ops pwm_soft_ops = {
.owner = THIS_MODULE,
.config = pwm_soft_config,
.enable = pwm_soft_enable,
.disable = pwm_soft_disable
};
static void period_timer_function(unsigned long arg) {
struct pwm_soft_dev *pwm_soft = (struct pwm_soft_dev *)arg;
/*设置 gpio为1 */
gpio_set_value(pwm_soft->gpio, 1);
/* 重新开启周期定时器和占空比定时器 */
mod_timer(&pwm_soft->period_timer, jiffies + msecs_to_jiffies(pwm_soft->period));
mod_timer(&pwm_soft->duty_timer, jiffies + msecs_to_jiffies(pwm_soft->duty));
}
static void duty_timer_function(unsigned long arg) {
struct pwm_soft_dev *pwm_soft = (struct pwm_soft_dev *)arg;
/* 设置gpio为0 */
gpio_set_value(pwm_soft->gpio, 0);
}
int pwm_soft_probe(struct platform_device *pdev) {
struct pwm_soft_dev *pwm_soft;
int ret;
/* 申请pwm_soft_dev对象 */
pwm_soft = devm_kzalloc(&pdev->dev, sizeof(struct pwm_soft_dev), GFP_KERNEL);
if(!pwm_soft) {
printk(KERN_ERR"pwm_soft:failed malloc pwm_soft_dev\n");
return -ENOMEM;
}
/* 获取pwm-soft设备树节点 */
pwm_soft->node = of_find_node_by_path("/pwm-soft");
if(pwm_soft->node == NULL) {
printk(KERN_ERR"pwm_soft:failed to get pwm-soft node\n");
return -EINVAL;
}
/* 获取pwm输出gpio */
pwm_soft->gpio = of_get_named_gpio(pwm_soft->node, "gpios", 0);
if(!gpio_is_valid(pwm_soft->gpio)) {
printk(KERN_ERR"pwm_soft:failed to get pwm gpio\n");
return -EINVAL;
}
/* 申请gpio */
ret = gpio_request(pwm_soft->gpio, "pwm-gpio");
if(ret) {
printk(KERN_ERR"pwm_soft:failed to request pwm gpio\n");
}
/* 设置管脚为输出模式 */
gpio_direction_output(pwm_soft->gpio, 0);
/* 初始化周期控制定时器 */
init_timer(&pwm_soft->period_timer);
pwm_soft->period_timer.function = period_timer_function;
pwm_soft->period_timer.data = (unsigned long)pwm_soft;
/* 初始化占空比控制定时器 */
init_timer(&pwm_soft->duty_timer);
pwm_soft->duty_timer.function = duty_timer_function;
pwm_soft->duty_timer.data = (unsigned long)pwm_soft;
/* 注册pwm设备 */
pwm_soft->chip.dev = &pdev->dev;
pwm_soft->chip.ops = &pwm_soft_ops;
pwm_soft->chip.base = 0;
ret = of_property_read_u32(pwm_soft->node, "npwm", &pwm_soft->chip.npwm);
if(ret) {
dev_err(&pdev->dev, "failed to read npwm\n");
return ret;
}
ret = pwmchip_add(&pwm_soft->chip);
if(ret < 0) {
dev_err(&pdev->dev, "pwmchip_add failed:%d\n", ret);
return ret;
}
platform_set_drvdata(pdev, pwm_soft);
return ret;
}
int pwm_soft_remove(struct platform_device *pdev) {
struct pwm_soft_dev *pwm_soft = platform_get_drvdata(pdev);
/* 删除定时器 */
del_timer_sync(&pwm_soft->duty_timer);
del_timer_sync(&pwm_soft->period_timer);
/* 释放gpio */
gpio_set_value(pwm_soft->gpio, 0);
gpio_free(pwm_soft->gpio);
/* 卸载pwm设备 */
return pwmchip_remove(&pwm_soft->chip);
}
static const struct of_device_id pwm_soft_of_match[] = {
{.compatible = "thj,pwm-soft"},
{}
};
MODULE_DEVICE_TABLE(of, pwm_soft_of_match);
static struct platform_driver pwm_soft_driver = {
.driver = {
.name = "pwm-soft",
.of_match_table = pwm_soft_of_match
},
.probe = pwm_soft_probe,
.remove = pwm_soft_remove
};
module_platform_driver(pwm_soft_driver);
MODULE_AUTHOR("thj");
MODULE_DESCRIPTION("use timer create pwm output");
MODULE_LICENSE("GPL");