【单片机】通过定时器实现模拟任意路PWM通道(带实例和计算方法)
前言说明
现在有很多单片机的硬件定时器都具备PWM输出功能,不过有时候会因为器件成本或硬件设计等原因,导致数量不够或者所使用的引脚不支持定时器输出。尴尬的是,笔者接手的项目两种情况都存在,项目需要支持 8 个电机,每个电机支持正反转,因此需要 8 * 2 = 16 路 PWM 信号。
思考了一阵,觉得可以基于一个普通的定时器来实现一个易于移植和扩展使用的PWM控制接口库,该接口库所支持的 PWM 通道数量仅受单片机的主频限制。
完整实例
以下实例代码是基于 GD32E230C 实现支持 16 路 PWM 通道的代码,可以实现调整每一路通道的占空比和频率,也可以单独关闭和启用某一个通道。用于电机控制上,代码预设的所有通道为 40Hz 频率,占空比范围 0 ~ 100。
/**
******************************************************************************
* @文件 pwm_control.h
* @版本 V1.0.0
* @日期
* @概要 PWM 控制接口
* @作者 lmx
* @邮箱 lovemengx@qq.com
******************************************************************************
* @注意
*
******************************************************************************
*/
#ifndef PWM_CONTROL_H
#define PWM_CONTROL_H
void pwm_control_init(); // 初始化 PWM 控制接口
void pwm_control_release(); // 释放 PWM 控制接口
void pwm_control_suspend(); // PWM 控制器暂停运行, 在比较多PWM通道需要同时生效的情况下使用
void pwm_control_resume(); // PWM 控制器暂停运行, 在比较多PWM通道需要同时生效的情况下使用
void pwm_control_set(unsigned int channel, unsigned int ratio); // 设置指定通道的占空比
void pwm_control_setall(unsigned int ratio); // 设置所有通道的占空比
void pwm_control_set_percent(unsigned int channel, unsigned char percent); // 以百分比设置指定通道的占空比
void pwm_control_setall_percent(unsigned char percent); // 以百分比设置所有通道的占空比
void pwm_control_enable(unsigned int channel); // 启用指定通道
void pwm_control_disable(unsigned int channel); // 关闭指定通道
#endif
/**
******************************************************************************
* @文件 pwm_control.h
* @版本 V1.0.0
* @日期
* @概要 PWM 控制接口
* @作者 lmx
* @邮箱 lovemengx@qq.com
******************************************************************************
* @注意
*
******************************************************************************
*/
#include "gd32e23x.h"
#include "pwm_control.h"
// PWM 配置
typedef volatile struct
{
unsigned char en:1; // 使能开关
unsigned int gpio_group; // 指定引脚组
unsigned int gpio_pin; // 指定引脚
unsigned int pwm_high; // 高电平维持次数(占空比)
unsigned int pwm_max; // 最多统计次数(周期)
unsigned int pwm_cnt; // 计数器
}pwm_config_t;
// 定时器配置
#define CFG_PWM_TIMER TIMER5
#define CFG_PWM_RCU_CLOCK RCU_TIMER5
#define CFG_PWM_IRQ_NUMBER TIMER5_IRQn
#define CFG_PWM_IRQ_FUNCTION TIMER5_IRQHandler
#define PWM_CNT_MAX 100
#define PWM_CONFIG_GPIO(__GROUP__, __PIN__) {1,__GROUP__,__PIN__,0,PWM_CNT_MAX,0}
volatile pwm_config_t pwm_configs[] = {
PWM_CONFIG_GPIO(GPIOA, GPIO_PIN_8), PWM_CONFIG_GPIO(GPIOA, GPIO_PIN_9),
PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_12), PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_13),
PWM_CONFIG_GPIO(GPIOA, GPIO_PIN_12), PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_8),
PWM_CONFIG_GPIO(GPIOA, GPIO_PIN_10), PWM_CONFIG_GPIO(GPIOA, GPIO_PIN_11),
PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_14), PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_15),
PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_3), PWM_CONFIG_GPIO(GPIOA, GPIO_PIN_15),
PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_5), PWM_CONFIG_GPIO(GPIOB, GPIO_PIN_4),
PWM_CONFIG_GPIO(GPIOC, GPIO_PIN_13), PWM_CONFIG_GPIO(GPIOC, GPIO_PIN_15),
};
#define PWM_CONFIG_MAX (sizeof(pwm_configs) / sizeof(pwm_configs[0]))
// 定时器中断程序
void CFG_PWM_IRQ_FUNCTION()
{
volatile unsigned int i = 0;
if( SET != timer_interrupt_flag_get(CFG_PWM_TIMER, TIMER_INT_FLAG_UP) ){
return ;
}
timer_interrupt_flag_clear(CFG_PWM_TIMER, TIMER_INT_FLAG_UP);
for(i = 0; i < PWM_CONFIG_MAX; i++)
{
// 未使能则不检测
if(0 == pwm_configs[i].en)
continue;
// 计数器累加
pwm_configs[i].pwm_cnt = pwm_configs[i].pwm_cnt >= pwm_configs[i].pwm_max ? 1 : pwm_configs[i].pwm_cnt+1;
// 判断高电平维持次数是否结束, 如果结束则至低电平
if(pwm_configs[i].pwm_cnt <= pwm_configs[i].pwm_high)
gpio_bit_set(pwm_configs[i].gpio_group, pwm_configs[i].gpio_pin);
else
gpio_bit_reset(pwm_configs[i].gpio_group, pwm_configs[i].gpio_pin);
}
return ;
}
// PWM 配置初始化
static void pwm_config_init()
{
// 初始化 GPIO 配置
for(unsigned char i = 0; i < PWM_CONFIG_MAX; i++){
gpio_mode_set(pwm_configs[i].gpio_group, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, pwm_configs[i].gpio_pin);
gpio_output_options_set(pwm_configs[i].gpio_group, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, pwm_configs[i].gpio_pin);
gpio_bit_reset(pwm_configs[i].gpio_group, pwm_configs[i].gpio_pin);
}
return;
}
// PWM 配置释放
static void pwm_config_release()
{
// 释放 GPIO
for(unsigned char i = 0; i < PWM_CONFIG_MAX; i++){
pwm_configs[i].en = 0;
gpio_bit_reset(pwm_configs[i].gpio_group, pwm_configs[i].gpio_pin);
}
}
// 定时器初始化
static void pwm_timer_init()
{
timer_parameter_struct para;
rcu_periph_clock_enable(CFG_PWM_RCU_CLOCK);
// 72000000/72/250 = 4KHz;
// 1s/4000Hz = 0.00025s = 0.25ms = 250us
timer_deinit(CFG_PWM_TIMER);
para.prescaler = 72-1; // 设置预分频率
para.period = 250-1; // 设置为 4KHz 频率
para.alignedmode = TIMER_COUNTER_EDGE;
para.counterdirection = TIMER_COUNTER_UP; // 向上计数
para.clockdivision = 0; // 不分频
para.repetitioncounter = 0; // 计数重复值0
timer_init(CFG_PWM_TIMER, ¶);
nvic_irq_enable(CFG_PWM_IRQ_NUMBER, 0U);
timer_interrupt_enable(CFG_PWM_TIMER, TIMER_INT_UP);
timer_auto_reload_shadow_enable(CFG_PWM_TIMER);
timer_enable(CFG_PWM_TIMER);
return ;
}
// 定时器释放
static void pwm_timer_release()
{
timer_interrupt_disable(CFG_PWM_TIMER, TIMER_INT_UP);
nvic_irq_disable(CFG_PWM_IRQ_NUMBER);
timer_disable(CFG_PWM_TIMER);
rcu_periph_clock_disable(CFG_PWM_RCU_CLOCK);
return ;
}
// PWM 控制器初始化
void pwm_control_init()
{
pwm_config_init();
pwm_timer_init();
}
// PWM 控制器释放
void pwm_control_release()
{
pwm_timer_release();
pwm_config_release();
}
// PWM 控制器暂停运行, 在比较多PWM通道需要同时生效的情况下使用
void pwm_control_suspend()
{
timer_interrupt_disable(CFG_PWM_TIMER, TIMER_INT_UP);
}
// PWM 控制器暂停运行, 在比较多PWM通道需要同时生效的情况下使用
void pwm_control_resume()
{
timer_interrupt_enable(CFG_PWM_TIMER, TIMER_INT_UP);
}
// 设置指定通道的占空比
void pwm_control_set(unsigned int channel, unsigned int ratio)
{
if( channel >= PWM_CONFIG_MAX){
return ;
}
pwm_configs[channel].en = 0;
pwm_configs[channel].pwm_high = ratio;
pwm_configs[channel].en = 1;
return ;
}
// 设置所有通道的占空比
void pwm_control_setall(unsigned int ratio)
{
timer_interrupt_disable(CFG_PWM_TIMER, TIMER_INT_UP);
for(unsigned char i = 0; i < PWM_CONFIG_MAX; i++){
pwm_configs[i].pwm_cnt = 0;
pwm_configs[i].pwm_high = ratio;
}
timer_interrupt_enable(CFG_PWM_TIMER, TIMER_INT_UP);
return ;
}
//以百分比来设置占空比比例
void pwm_control_set_percent(unsigned int channel, unsigned char percent)
{
pwm_control_set(channel, (unsigned int)(((float)percent / 100) * pwm_configs[channel].pwm_max));
return;
}
//以百分比来设置所有占空比比例
void pwm_control_setall_percent(unsigned char percent)
{
pwm_control_setall((unsigned int)(((float)percent / 100) * pwm_configs[channel].pwm_max));
return ;
}
// 启用指定通道
void pwm_control_enable(unsigned int channel)
{
if( channel >= PWM_CONFIG_MAX){
return ;
}
pwm_configs[channel].en = 1;
return ;
}
// 关闭指定通道
void pwm_control_disable(unsigned int channel)
{
if( channel >= PWM_CONFIG_MAX){
return ;
}
pwm_configs[channel].en = 0;
gpio_bit_reset(pwm_configs[channel].gpio_group, pwm_configs[channel].gpio_pin);
return ;
}
/**
******************************************************************************
* @文件 main.c
* @版本 V1.0.0
* @日期
* @概要 主程序文件
* @作者 lmx
* @邮箱 lovemengx@qq.com
******************************************************************************
* @注意
*
******************************************************************************
*/
#include <stdlib.h>
#include <stdio.h>
#include "pwm_control.h"
int main(void)
{
unsigned char percent = 0;
// 公共的时钟使能和关闭应当由专门的源文件集中处理, 不应当与具体的某个功能模块挂钩
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_GPIOB);
rcu_periph_clock_enable(RCU_GPIOC);
rcu_periph_clock_enable(RCU_GPIOF);
pwm_control_init();
pwm_control_set(9, 50);
while(1)
{
percent = percent >= 100 ? 0 : percent + 1;
pwm_control_suspend();
pwm_control_setall_percent(percent);
pwm_control_resume();
delay_1ms(100);
}
return 0;
}
实现原理
借助定时器来产生固定频率的中断,通过对中断进行计数来确定何时设置 GPIO 的高低电平状态,如设定 100 个中断为一个周期,拉高 GPIO 维持50个中断,再拉低 GPIO 维持 50 个中断,即可实现一个 50% 占空比的 PWM 方波。这里的一个周期为 100 个中断,即决定了产生 PWM 的频率,而维持多少数量中断设定 GPIO 高低电平,则决定了占空比多少。如果调整个数无法依然无法满足所需要的频率,可以适当通过调整定时器的中断频率来进一步提升,但过高的定时器中断频率会加重 CPU 的负担,可能会影响其他的业务逻辑处理。
要实现众多数量的 PWM 信号,通过设计良好的数据结构,将这些必要的参数组织起来:GPIO、高电平维护次数、一周期次数、当前计数、控制开关。在定时器中断里面根据这些配置参数去决定 GPIO 的输出状态,即可实现各路不同频率、不同占空比比例、可单独开关的PWM 信号输出。
// PWM 配置
typedef struct
{
unsigned char en:1; // 使能开关
unsigned int gpio_group; // 指定引脚组
unsigned int gpio_pin; // 指定引脚
unsigned int pwm_high; // 高电平维持次数,决定占空比
unsigned int pwm_max; // 最多统计次数,PWM周期,决定输出频率
unsigned int pwm_cnt; // 计数器
}pwm_config_t;
定时器配置计算方法
如本文提供的例子,可输出 16 路 40Hz 频率的 PWM 信号,占空比精度为 100,我的 SDK 中设定的 GD32E230C 工作频率为 72MHz,因此计算方法为:
- 计算 40Hz 一周期的耗时时间:1秒 / 40Hz = 0.025秒
- 计算每一级占空比所的的时间:0.025秒 / 100 = 0.00025秒
- 计算定时器最低要求的中断频率:1秒 / 0.00025秒 = 4000Hz
- 计算以 72 为预分频参数的定时器频率:72MHz / 72 = 1MHz = 1000Khz = 1000000Hz
- 计算定时器产生所需中断频率的周期参数:1000000Hz / 4000Hz =250 个周期,定时器设定参数如下:
timer_parameter_struct para;
timer_deinit(TIMER5);
para.prescaler = 72-1; // 设置时钟频率为 1MHz
para.period = 250-1; // 72MHz=72000000Hz/72prescaler/250period = 4000Hz = 4KHz
para.alignedmode = TIMER_COUNTER_EDGE;
para.counterdirection = TIMER_COUNTER_UP; // 向上计数
para.clockdivision = 0; // 不分频
para.repetitioncounter = 0; // 计数重复值0
timer_init(TIMER5, ¶);
若希望默认输出 40Hz 频率,50% 占空比的信号,则配置 pwm_config_t.pwm_max=100,配置 pwm_config_t.pwm_hight=50 即可。
若希望默认输出 80Hz 频率,50% 占空比的信号,则配置 pwm_config_t.pwm_max=50,配置 pwm_config_t.pwm_hight=25 即可。
如果希望输出的频率,通过修改 pwm_max 无法实现,那么可以通过适当的调整定时器的中断频率来配合实现。但过高的定时器中断频率会增加 CPU 的负担,如果影响了业务逻辑处理,那么可以通过降低占空比的精度来降低定时器的中断频率。
如上例要实现 40Hz 频率输出,但却需要定时器产生 4KHz 频率的中断,我们可以将占空比的精度由 100 降至 15,即可将定时器频率降低至 600Hz:
- 配置 pwm_config_t.pwm_max=15
- 计算 40Hz 一周期的耗时时间:1秒 / 40Hz = 0.025秒
- 计算每一级占空比所的的时间:0.025秒 / 15 = 0.0016666666666667秒
- 计算定时器最低要求的中断频率:1秒 / 0.0016666666666667秒 = 599.999999999988Hz ≈ 600Hz
- 计算以 72 为预分频参数的定时器频率:72MHz / 72 = 1MHz = 1000Khz = 1000000Hz
- 计算定时器产生所需中断频率的周期参数:1000000Hz / 600Hz =1,666.6666666667≈1667个周期,定时器设定参数如下:
timer_parameter_struct para;
timer_deinit(TIMER5);
para.prescaler = 72-1; // 设置时钟频率为 1MHz
para.period = 1667-1; // 72MHz=72000000Hz/72prescaler/1667period ≈ 600Hz
para.alignedmode = TIMER_COUNTER_EDGE;
para.counterdirection = TIMER_COUNTER_UP; // 向上计数
para.clockdivision = 0; // 不分频
para.repetitioncounter = 0; // 计数重复值0
timer_init(TIMER5, ¶);
如果过高的中断频率影响到了其他硬件控制器的中断响应时间,可以降低定时器的中断优先级。