PWM, 又称脉冲宽度调制模块,是一种重要的电子控制组件,其工作原理和应用十分广泛。PWM模块通过调整占空比来改变电路中的电流或电压,从而实现对设备的控制。常见的支持PWM输出控制的设备有LED,屏幕背光调节,风扇转速管理,电机控制(直流电机)以及电源管理等。
对于PWM模块来说,需要关注的的配置项有周期,占空比以及极性,详细说明如下。
当实现PWM驱动时,就需要在系统中配置PWM的接口,以及驱动中实现对PWM的寄存器操作,后续分别进行说明。PWM属于芯片内部的模块,不属于具体的总线,由内部platform总线统一管理。PWM的框架如下所示。
可以看的,PWM作为SOC内部模块,由platform总线管理。PWM子系统一方面配合设备树来管理硬件寄存器,控制PWM的输出配置,另一方面作为PWM provider,可以被其它PWM Consumer或者应用层进行访问控制。PWM一般不单独作为独立硬件,而是配合Consumer成为控制其它模块的组件,如背光,温控风扇,调压模块等,实现更复杂的功能。
向内核添加PWM驱动的相关接口比较简单,主要包含devm_pwmchip_add()和pwmchip_remove()
// pwm设备注册到系统控制的接口(内核管理,驱动移除自动释放资源)
int devm_pwmchip_add(struct device *dev, struct pwm_chip *chip)
// 向系统注册pwm控制器
int pwmchip_add(struct pwm_chip *chip)
// 从系统中注销pwm控制器
void pwmchip_remove(struct pwm_chip *chip)
//此函数执行后,会在/sys/class/pwm下创建pwmchinX的目录,管理对应pwm信息
//其中X是根据驱动加载的顺序,按照编号从0,1,2...向上增加
pwm设备在系统中由pwm设备子系统管理,通过结构体”struct pwm_chip”管理,结构如下所示。
//pwm节点控制信息
struct pwm_chip {
struct device *dev; //pwm控制器对应的设备节点
const struct pwm_ops *ops; //pwm定义的应用层访问接口
int base; //控制器管理的第一个pwm的值
unsigned int npwm; //控制器管理的pwm个数
struct pwm_device * (*of_xlate)(struct pwm_chip *pc,
const struct of_phandle_args *args); //请求一个PWM设备给定一个设备树脉宽指示
unsigned int of_pwm_n_cells; //在设备树PWM指示符中预期的单元数
/* only used internally by the PWM framework */
struct list_head list; //供内部使用的列表节点
struct pwm_device *pwms; //由框架分配的PWM器件列表
};
//pwm设备结构,包含的设备信息
struct pwm_device {
const char *label; //pwm设备名称
unsigned long flags; //与PWM相关联的标志
unsigned int hwpwm; //pwm对于每个芯片的相对索引
unsigned int pwm; //pwm的全局索引
struct pwm_chip *chip; //所属的pwm控制器
void *chip_data; //与pwm设备相关的控制器私有数据
struct pwm_args args; //pwm变量
struct pwm_state state; //pwm最后应用状态
struct pwm_state last; //pwm最后实现的状态(用于测试)
};
//pwm操作接口,最关键的是apply和get_state
struct pwm_ops {
int (*request)(struct pwm_chip *chip, struct pwm_device *pwm); //请求pwm资源执行的钩子函数
void (*free)(struct pwm_chip *chip, struct pwm_device *pwm); //释放pwm资源执行的钩子函数
int (*capture)(struct pwm_chip *chip, struct pwm_device *pwm,
struct pwm_capture *result, unsigned long timeout); //捕获并报告pwm信号
int (*apply)(struct pwm_chip *chip, struct pwm_device *pwm,
const struct pwm_state *state); //用于应用新的pwm配置,支持设置pwm需要实现
int (*get_state)(struct pwm_chip *chip, struct pwm_device *pwm,
struct pwm_state *state); //获取当前的pwm配置,支持获取pwm状态需要实现
struct module *owner; //有助于防止删除导出活动pwm的模块
};
了解了PWM模块的相关接口,这里讲解pwm驱动实现的流程。pwm作为内部模块,也由platform总线进行管理,其实现流程如下所示。
在实现设备树前,首先确定硬件使用的PWM接口。这里使用的为: CSI_VSYNC/PWM7_OUT,对于设备树的修改如下所示。
// 芯片原厂提供pwm7设备树
pwm7: pwm@20f8000 {
compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm"; //pwm标签,用于设备树匹配
reg = <0x020f8000 0x4000>; //控制pwm模块的寄存器
interrupts = <GIC_SPI 116 IRQ_TYPE_LEVEL_HIGH>; //pwm中断,<中断控制器 中断线号 中断触发电平>
clocks = <&clks IMX6UL_CLK_PWM7>, //pwm时钟,ipg为pwm的时钟源,per为pwm经过分频的时钟
<&clks IMX6UL_CLK_PWM7>;
clock-names = "ipg", "per";
#pwm-cells = <3>; //描述引用pwm后,pwm配置项的个数
status = "disabled"; //pwm状态,关闭
};
// 用户扩展pwm7设备树
&pwm7 {
compatible = "rmk,pwm"; //覆盖pwm标签,用于驱动匹配
pinctrl-names = "default"; //引脚复用名称
pinctrl-0 = <&pinctrl_pwm7>; //引脚复用配置
status = "okay"; //pwm状态,打开
};
&iomuxc {
//...
pinctrl_pwm7: pwm7grp {
fsl,pins = <
MX6UL_PAD_CSI_VSYNC__PWM7_OUT 0x110b0 //指定引脚的复用功能,PWM输出模式
>;
};
}
这一步主要完成驱动匹配设备树节点,加载到内核的接口,代码实现如下所示。
// 匹配设备树的硬件节点
static const struct of_device_id pwm_driver_dt_ids[] = {
{ .compatible = "rmk,pwm", },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, pwm_driver_dt_ids);
static struct platform_driver pwm_driver = {
.driver = {
.name = "rmk-pwm",
.of_match_table = pwm_driver_dt_ids,
},
.probe = pwm_driver_probe,
};
static int __init pwm_module_init(void)
{
platform_driver_register(&pwm_driver);
return 0;
}
static void __exit pwm_module_exit(void)
{
platform_driver_unregister(&pwm_driver);
}
在驱动中操作硬件时,和对于单片机的操作类似,主要包含以下操作。
static int pwm_driver_probe(struct platform_device *pdev)
{
//......
//2.获取PWM相关的管理时钟
info->clk_ipg = devm_clk_get(&pdev->dev, "ipg");
if (IS_ERR(info->clk_ipg)) {
dev_err(&pdev->dev, "getting ipg clock failed\r\n!");
return -ENXIO;
}
info->clk_per = devm_clk_get(&pdev->dev, "per");
if (IS_ERR(info->clk_per)) {
dev_err(&pdev->dev, "getting per clock failed\r\n!");
return -ENXIO;
}
/* 模块时钟管理,模块开启则打开时钟,反之则关闭 */
ret = pwm_clk_enable(info);
if (ret) {
dev_err(&pdev->dev, "clk perpare enabled failed!\r\n");
return ret;
}
//3.获取PWM相关的寄存器资源
info->mmio_base = devm_platform_ioremap_resource(pdev, 0);
if (IS_ERR(info->mmio_base)) {
dev_err(&pdev->dev, "getting reg resource failed\r\n!");
return -ENXIO;
}
pwmcr = readl(info->mmio_base + MX3_PWMCR); //需要开启时钟后,才能读取寄存器
if (!(pwmcr & MX3_PWMCR_EN)) {
pwm_clk_disable(info);
}
//......
}
struct pwm_state {
u64 period; //PWM周期时间,单位ns
u64 duty_cycle; //PWM有效时间(占空比时间),单位ns
enum pwm_polarity polarity; //PWM极性,有效电平是高电平还是低电平
bool enabled; //PWM使能状态
bool usage_power; //如果设置,PWM驱动器只需要保持功率输出,但在信号形式方面有更多的自由,可通过移相改变EMI特性(I.MX6ULL不支持)
};
static int pwm_driver_apply(struct pwm_chip *chip, struct pwm_device *pwm, const struct pwm_state *state)
{
unsigned long period_cycles, duty_cycles, prescale, counter_check, flags;
struct pwm_driver_info *info;
void __iomem *reg_sar;
__force u32 sar_last, sar_current;
struct pwm_state cstate;
unsigned long long c;
unsigned long long clkrate;
int ret;
u32 cr, timeout = 1000;
/*1. 调用pwm_imx27_apply, 获取当前pwm设置状态 */
info = container_of(chip, struct pwm_driver_info, chip);
reg_sar = info->mmio_base + MX3_PWMSAR;
pwm_get_state(pwm, &cstate);
clkrate = clk_get_rate(info->clk_per);
/*2. 判断当前pwm状态,如果开启则等待缓存中处理完,未开启则复位pwm模块*/
if (cstate.enabled) {
pwm_wait_fifo_slot(info, pwm);
} else {
ret = pwm_clk_enable(info);
if (ret) {
return ret;
}
pwm_sw_reset(info);
}
/* 3. 根据周期时间,计算分频和设置周期数,period的单位是ns*/
c = clkrate * state->period;
do_div(c, NSEC_PER_SEC);
period_cycles = c;
prescale = period_cycles / 0x10000 + 1; //根据周期计算分频系数,peroid为16位,所以分频后要保证peroid小于16位
period_cycles /= prescale;
if (period_cycles > 2) {
period_cycles -= 2;
}else {
period_cycles = 0;
}
/* 4.根据duty_cycles和分频,确定duty_cycles的设置值 */
c = clkrate * state->duty_cycle;
do_div(c, NSEC_PER_SEC);
duty_cycles = c;
duty_cycles /= prescale;
/* 5.更新duty_cycle fifo更新
(1)duty_cycle比当前值PWM设置大时,直接写入
(2)duty_cycle比当前值PWM设置小时,根据当前计数器的值,先写入上一次PWM值,在修改为当前值(应该是为了解决PWM切换时波形不完整畸变的问题)
*/
if (duty_cycles < info->duty_cycle) {
c = clkrate * 1500;
do_div(c, NSEC_PER_SEC);
counter_check = c;
sar_last = cpu_to_le32(info->duty_cycle);
sar_current = cpu_to_le32(duty_cycles);
spin_lock_irqsave(&info->lock, flags);
if (state->period >= 2000) {
while ((period_cycles - readl_relaxed(info->mmio_base + MX3_PWMCNR)) < counter_check) {
if (!--timeout) {
break;
}
}
}
if (!(MX3_PWMSR_FIFOAV & readl_relaxed(info->mmio_base + MX3_PWMSR))) {
__raw_writel(sar_last, reg_sar);
}
__raw_writel(sar_current, reg_sar);
spin_unlock_irqrestore(&info->lock, flags);
} else {
writel(duty_cycles, info->mmio_base + MX3_PWMSAR);
}
info->duty_cycle = duty_cycles;
/* 6. 写入周期寄存器, 配置寄存器,使能PWM */
writel(period_cycles, info->mmio_base + MX3_PWMPR);
cr = MX3_PWMCR_PRESCALER_SET(prescale) |
MX3_PWMCR_STOPEN | MX3_PWMCR_DOZEN | MX3_PWMCR_WAITEN |
FIELD_PREP(MX3_PWMCR_CLKSRC, MX3_PWMCR_CLKSRC_IPG_HIGH) |
MX3_PWMCR_DBGEN;
if (state->polarity == PWM_POLARITY_INVERSED)
cr |= FIELD_PREP(MX3_PWMCR_POUTC,
MX3_PWMCR_POUTC_INVERTED);
if (state->enabled)
cr |= MX3_PWMCR_EN;
writel(cr, info->mmio_base + MX3_PWMCR);
/* 7. 未开启pwm时,关闭PWM时钟 */
if (!state->enabled) {
pwm_clk_disable(info);
}
//......
return 0;
}
可以看到,这里基本就是整个实现就是根据当前设置的PWM参数实现如下步骤。
至于pwm_driver_get_state的实现,就是读取当前的寄存器值,还原成PWM的配置信息,返回到应用层。
static int pwm_driver_get_state(struct pwm_chip *chip, struct pwm_device *pwm, struct pwm_state *state)
{
struct pwm_driver_info *info;
int ret;
u32 period, prescaler, pwm_clk, val;
u64 tmp;
info = container_of(chip, struct pwm_driver_info, chip);
ret = pwm_clk_enable(info);
if(ret) {
return ret;
}
val = readl(info->mmio_base + MX3_PWMCR);
/*1. 获取PWM使能状态 */
if (val & MX3_PWMCR_EN) {
state->enabled = true;
} else {
state->enabled = false;
}
/*2. 获取当前pwm极性*/
switch (FIELD_GET(MX3_PWMCR_POUTC, val)) {
case MX3_PWMCR_POUTC_NORMAL:
state->polarity = PWM_POLARITY_NORMAL;
break;
case MX3_PWMCR_POUTC_INVERTED:
state->polarity = PWM_POLARITY_INVERSED;
break;
default:
dev_warn(chip->dev, "can't set polarity, output disconnected\n");
}
/*3. 获取周期 */
prescaler = MX3_PWMCR_PRESCALER_GET(val);
pwm_clk = clk_get_rate(info->clk_per);
val = readl(info->mmio_base + MX3_PWMPR);
period = val >= MX3_PWMPR_MAX ? MX3_PWMPR_MAX : val;
tmp = NSEC_PER_SEC * (u64)(period + 2) * prescaler;
state->period = DIV_ROUND_UP_ULL(tmp, pwm_clk);
/*4. 获取占空比, 管理时钟 */
if (state->enabled) {
val = readl(info->mmio_base + MX3_PWMSAR);
} else {
val = info->duty_cycle;
pwm_clk_disable(info);
}
tmp = NSEC_PER_SEC * (u64)(val) * prescaler;
state->duty_cycle = DIV_ROUND_UP_ULL(tmp, pwm_clk);
dev_info(chip->dev, "enable:%d, period:%lld, duty_cycle:%lld\n",
state->enabled,
state->period,
state->duty_cycle);
return 0;
}
可以看到,对于硬件的访问,是使用最基础的writel和readl接口,驱动寄存器值,并将配置值写入寄存器操作;理解驱动,一定要去理解芯片模块的内部寄存器的功能和值,否则可能是空中楼阁。
最后则是将pwmchip注册到内核的实现。主要实现.apply和.get_state接口,分别用于设置PWM值和获取PWM状态,在配合上面的硬件访问接口进行设置;关联和注册pwmchip设备。
struct pwm_driver_info
{
//...
//pwm控制器结构
struct pwm_chip chip;
};
//pwm管理结构体
static const struct pwm_ops pwm_driver_ops = {
.apply = pwm_driver_apply, /* 设置PWM值 */
.get_state = pwm_driver_get_state, /* 获取PWM状态 */
.owner = THIS_MODULE,
};
static int pwm_driver_probe(struct platform_device *pdev)
{
//1. 申请pwm管理资源
struct pwm_driver_info *info;
info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL);
if(!info) {
dev_err(&pdev->dev, "memory malloc failed!\r\n");
return -ENOMEM;
}
//...
//4.增加pwmchip设备到系统中(/sys/class/pwm/pwmchipX/export)
info->duty_cycle = 0;
info->chip.ops = &pwm_driver_ops;
info->chip.dev = &pdev->dev;
info->chip.npwm = 1;
ret = devm_pwmchip_add(&pdev->dev, &info->chip);
if (ret) {
dev_err(&pdev->dev, "pwmchip add failed!\r\n");
return ret;
}
spin_lock_init(&info->lock);
dev_info(&pdev->dev, "pwm init success!\r\n");
}
至此,作为pwm模块的驱动实现完毕。将pwm注册到内核,就可以被应用层和其它pwm consumer进行访问。
关于PWM相关的代码详细见: PWM驱动代码实现
在前面说明过,驱动执行后会在/sys/class/pwm目录下增加pwmchipX的目录,根据驱动加载的顺序,本例中虽然使用PWM7,但是因为驱动第二个加载,因此对应pwmchip1。
/sys/class/pwm/pwmchip1/export # 导出pwm,开启pwm控制显示
/sys/class/pwm/pwmchip1/unexport # 移除pwm,关闭pwm控制显示
/sys/class/pwm/pwmchip1/pwm0/enable # 使能pwm输出,0关闭,1开启
/sys/class/pwm/pwmchip1/pwm0/period # 设置pwm输出周期时间,单位ns
/sys/class/pwm/pwmchip1/pwm0/duty_cycle # 设置pwm输出占空比时间,单位ns
/sys/class/pwm/pwmchip1/pwm0/polarity # 设置pwm极性,normal高电平有效,inversed低电平有效
在Linux shell中测试如下。
echo 0 > /sys/class/pwm/pwmchip1/export # 关闭pwm0控制目录节点(0为pwmchip下支持的节点编号,默认为0)
echo 0 > /sys/class/pwm/pwmchip1/unexport # 关闭pwm0控制目录节点
echo 50000 > /sys/class/pwm/pwmchip1/pwm0/period # 设置pwm周期时间,50000ns
echo 10000 > /sys/class/pwm/pwmchip1/pwm0/duty_cycle # 设置pwm占空比时间,10000ns
echo normal > /sys/class/pwm/pwmchip1/pwm0/polarity # 设置pwm极性,高电平有效
echo 1 > /sys/class/pwm/pwmchip1/pwm0/enable
在C语言中可以使用文件接口进行访问,实现如下。
#define PWM_EXPORT "/sys/class/pwm/pwmchip1/export"
#define PWM_ENABLE "/sys/class/pwm/pwmchip1/pwm0/enable"
#define PWM_PEROID "/sys/class/pwm/pwmchip1/pwm0/period"
#define PWM_DUTYCLE "/sys/class/pwm/pwmchip1/pwm0/duty_cycle"
#define MAX_SIZE 64
int pwm_setup(int state, unsigned int peroid, unsigned int duty_cycle)
{
int fd_state, fd_peroid, fd_duty;
int ret = 0, num;
char str[MAX_SIZE];
fd_state = open(PWM_ENABLE, O_RDWR);
fd_peroid = open(PWM_PEROID, O_RDWR);
fd_duty = open(PWM_DUTYCLE, O_RDWR);
if (fd_state<0 || fd_peroid<0 || fd_duty<0) {
ret = -1;
printf("fd open failed\n");
goto __exit;
}
//1. 设置pwm周期
num = snprintf(str, MAX_SIZE, "%d", peroid);
ret = write(fd_peroid, str, num);
if (ret < 0) {
printf("fd_peroid write failed\n");
goto __exit;
}
//2. 设置pwm占空比
num = snprintf(str, MAX_SIZE, "%d", duty_cycle);
ret = write(fd_duty, str, num);
if (ret < 0) {
printf("fd_duty write failed\n");
goto __exit;
}
//3. 开启pwm输出
num = snprintf(str, MAX_SIZE, "%d", state);
ret = write(fd_state, str, num);
if(ret < 0) {
printf("fd_state write failed\n");
goto __exit;
}
__exit:
close(fd_state);
close(fd_peroid);
close(fd_duty);
return ret;
}
int main(int argc, char *argv[])
{
int fd, ret;
int peroid, duty;
//1. 导出pwmchip下的pwm设备
fd = open(PWM_EXPORT, O_WRONLY);
if (fd < 0) {
printf("export open failed\n");
return -1;
}
ret = write(fd, "0", strlen("0"));
if (ret < 0) {
printf("export failed\n");
return -1;
}
close(fd);
//2. 设置pwm的配置信息
if (argc > 2) {
peroid = atoi(argv[1]);
duty = atoi(argv[2]);
pwm_setup(1, peroid, duty);
}
return 0;
}
可以使用./pwm_test [peroid] [duty_cycle]来设置PWM的周期时间和占空比时间,结果如下所示。
关于pwm应用的代码可以参考: pwm应用代码实现
pwm的注册也为其它驱动模块提供了统一访问的接口,这些调用到pwm的驱动被称为pwm consumer。
关于pwm consumer常见的有PWM背光调节,PWM散热风扇等。
其中背光参考此文件说明: 背光驱动管理框架 散热风扇作为thermal_cooling设备,参考此文件说明: 系统温控驱动管理框架
至于其它设备驱动访问pwm的接口则如下所示。
// 根据名称从deivce,device-node中获取pwm节点信息
struct pwm_device *pwm_get(struct device *dev, const char *con_id);
struct pwm_device *of_pwm_get(struct device_node *np, const char *con_id);
struct pwm_device *devm_pwm_get(struct device *dev, const char *con_id);
struct pwm_device *devm_of_pwm_get(struct device *dev, struct device_node *np,
const char *con_id);
// 释放已经获取的pwm设备
void pwm_put(struct pwm_device *pwm);
void devm_pwm_put(struct device *dev, struct pwm_device *pwm)
// 设置pwm参数
static inline int pwm_config(struct pwm_device *pwm, int duty_ns,
int period_ns)
// 使能pwm输出
static inline int pwm_enable(struct pwm_device *pwm)
// 关闭pwm输出
static inline void pwm_disable(struct pwm_device *pwm)
// 设置pwm极性,控制开启时的电平
static inline int pwm_set_polarity(struct pwm_device *pwm,
enum pwm_polarity polarity)
至此,关于pwm的驱动实现,应用层访问,以及其它设备驱动如何访问pwm的说明实现完毕。
直接开始下一节说明: 背光驱动管理框架