【单片机】通过定时器实现模拟任意路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, &para);
	
	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,因此计算方法为:

  1. 计算 40Hz 一周期的耗时时间:1秒 / 40Hz = 0.025秒
  2. 计算每一级占空比所的的时间:0.025秒 / 100 = 0.00025秒
  3. 计算定时器最低要求的中断频率:1秒 / 0.00025秒 = 4000Hz
  4. 计算以 72 为预分频参数的定时器频率:72MHz / 72 = 1MHz = 1000Khz = 1000000Hz
  5. 计算定时器产生所需中断频率的周期参数: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, &para);

若希望默认输出 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:

  1. 配置 pwm_config_t.pwm_max=15 
  2. 计算 40Hz 一周期的耗时时间:1秒 / 40Hz = 0.025秒
  3. 计算每一级占空比所的的时间:0.025秒 / 15 = 0.0016666666666667秒
  4. 计算定时器最低要求的中断频率:1秒 / 0.0016666666666667秒 = 599.999999999988Hz ≈ 600Hz
  5. 计算以 72 为预分频参数的定时器频率:72MHz / 72 = 1MHz = 1000Khz = 1000000Hz
  6. 计算定时器产生所需中断频率的周期参数: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, &para);

 如果过高的中断频率影响到了其他硬件控制器的中断响应时间,可以降低定时器的中断优先级。

 

STM32系列微控制器中的定时器可以用于模拟PWM(脉冲宽度调制)输出,这是一种常见的控制信号生成技术,特别适合于驱动电机、LED灯等设备。下面是一个基本步骤: 1. **配置定时器**: 首先选择一个合适的定时器,如TIM1或TIM2,它们通常都有PWM功能。设置定时器的工作模式,比如通用定时器模式(PWM mode),并配置预分频器以设定占空比范围。 2. **初始化通道**: 选择需要输出PWM定时器通道,比如TIMx_CH1。配置该通道PWM模式,设置自动装载值(例如,周期时间)和 Compare Match Value (CMR) 以确定高电平持续时间。 3. **设置占空比**: 通过调整比较寄存器的值来改变输出波形的占空比。一般来说,较低的数值对应较短的高电平,较高的数值对应较长的高电平。 4. **启用输出**: 开启定时器的计数器,并使能输出PWM信号的输出锁存器,使其实际驱动外部负载。 5. **软件更新 PWM**: 如果需要动态改变占空比,可以在运行时修改比较寄存器的值,然后重新启动定时器。 **示例代码片段**(仅供参考,具体取决于所使用的STM32 HAL库版本): ```c TIM_HandleTypeDef htim; // 初始化TIM1 for PWM htim.Instance = TIM1; htim.Init.Prescaler = ...; // 根据系统时钟和所需周期计算预分频值 htim.Init.CounterMode = TIM_COUNTERMODE_UP; htim.Init.Period = ...; // 设置周期 htim.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim.Init.OversamplingMode = TIM_OVERSAMPLING_DISABLE; HAL_TIM_PWM_Init(&htim); HAL_TIM_PWM_MspInit(&htim); // 初始化GPIO // 设置PWM通道 TIM_OC_InitTypeDef oc InitOC; InitOC.OCMode = TIM_OCMODE_PWM1; InitOC.Pulse = ...; // 初始占空比 InitOC.OCPolarity = TIM_OCPOLARITY_HIGH; InitOC.OCNPolarity = TIM_OCNPOLARITY active low; InitOC.OCTRigger = TIM_TRIGER_RISING; InitOC.OCPulseNumber = ...; // 对应周期内的高电平次数 HAL_TIM_PWM_ConfigChannel(&htim, &InitOC); // 开始PWM输出 HAL_TIM_PWM_Start(&htim, TIM_CHANNEL_1); ```
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值