如何使用单个定时器驱动多路模拟PWM输出?
背景
现在的主流MCU都支持硬件PWM输出,以STM32F103为例,通用定时器可以支持4路占空比可调的PWM输出,高级定时器可以支持4路带互补输出的PWM输出。硬件产生PWM,具有稳定可靠、执行效率高的特点。
但是,硬件产生的PWM也有一些限制,例如:1.输出引脚位置固定,PCB连线可能会不方便;2.输出引脚的数量有限,在一些需要多通道输出的应用中(如多路控温)会占用过多定时器。
虚拟PWM库特性
由于项目需要,笔者编写了VirPwm库,以实现对任意GPIO的产生PWM的驱动。它具有如下特性:
- 资源占用小。只需要在一个定时器(软件定时器、硬件定时器均可)周期性的调用
void VirPwm_TimIRQHandler(VirPwm *VirPwmDef)
即可。 - 可移植性好。本库是基于STM32F103开发的,但是充分考虑了一直到其他平台上的可能性。通过为
VirPwmDef->SetOut
函数指针注册端口输出函数以及修改void VirPwm_SetFreq(VirPwm *VirPwmDef, uint16_t freq)
的实现,可以顺利实现移植。 - 灵活性强。由于开发了端口设置函数指针
typedef void (*VirPwm_SetOutput)(uint8_t state)
,使用者可以*编写函数(如同时驱动多路GPIO输出PWM)作为回调。 - 一个定时器可以驱动多路占空比不同的PWM。通过结构体
VirPwm
抽象了虚拟PWM,定义不同的结构体变量,设置它们的不同的占空比,在定时器的更新中断中周期性的调用void VirPwm_TimIRQHandler(VirPwm *VirPwmDef)
函数,并依次传入不同的结构体变量参数,即可实现驱动频率相同、占空比不同的多组PWM输出。
源码介绍
头文件 virtual_pwm.h
/*
* virtual_pwm.h
*
* Created on: 2020年8月12日
* Author: Tao
*/
#ifndef LIBRARIES_SYSEXTEND_INC_VIRTUAL_PWM_H_
#define LIBRARIES_SYSEXTEND_INC_VIRTUAL_PWM_H_
#include "stm32f10x.h"
#include "stm32f10x_conf.h"
/**
* VirPwm_SetOutput是函数指针类型变量,用来回调端口输出函数
*/
typedef void (*VirPwm_SetOutput)(uint8_t state);
/**
* 虚拟PWM的结构体类型,通过此变量类型可以完整的定义一个虚拟PWM,以供本库的函数操作
*/
typedef struct{
uint8_t Status;
uint8_t Idle;
uint16_t Frequency;
uint16_t DutyCycle;
TIM_TypeDef *Basetimer;
VirPwm_SetOutput SetOut;
} VirPwm;
/**
* 与平台相关, 操作GPIO的宏定义
*/
#define DIG_OUTPUT_1 GPIOA_OUT(5)
extern VirPwm VirPwmDef;
void VirPwm_Init(VirPwm *VirPwmDef,uint16_t freq,uint16_t dutyCycle,uint8_t idle, VirPwm_SetOutput setOutHandler);
void VirPwm_SetStatus(VirPwm *VirPwmDef, uint8_t status);
void VirPwm_SetFreq(VirPwm *VirPwmDef, uint16_t freq);
void VirPwm_SetDutyCycle(VirPwm *VirPwmDef, uint16_t dutyCycle);
void VirPwm_SetIdle(VirPwm *VirPwmDef, uint8_t idle);
void VirPwm_TimIRQHandler(VirPwm *VirPwmDef);
void VirPwm_SetOutHandler(uint8_t state);
#endif /* LIBRARIES_SYSEXTEND_INC_VIRTUAL_PWM_H_ */
源文件 vir_pwm.c
/*
* virtual_pwm.c
*
* Created on: 2020年8月12日
* Author: Tao
*/
#include "virtual_pwm.h"
/**
* 使用虚拟PWM库时,应当在他处配置好定时器(由宏定义VIRPWM_TIMER指定),
* 并在该定时器的中断服务函数中调用void VirPwm_TimIRQHandler()。
* 同时,应该在外部为void (*VirPwm_SetOutputHandler)(uint8_t state)注册一个函数,
* 以实现端口的电平翻转操作。
*/
VirPwm VirPwmDef = {
.Status = 0,
.Idle = 0,
.Frequency = 100,
.DutyCycle = 50,
.Basetimer = TIM4,
.SetOut = VirPwm_SetOutHandler,
.count_P = 0,
.count_N = 100
};
/**
* @brief 完成初始化VirPwmDef结构体指针
* 也可以直接在定义结构体的时候完成初始化,而不必调用本函数
* @param VirPwm: 虚拟PWM的定义结构体指针
* @param freq: 虚拟PWM的频率
* @param dutyCycle: 虚拟PWM的占空比
* @param idle: 虚拟PWM的空闲状态
*/
void VirPwm_Init(VirPwm *VirPwmDef,uint16_t freq,uint16_t dutyCycle,uint8_t idle)
{
VirPwm_SetFreq(VirPwmDef,freq);
VirPwm_SetDutyCycle(VirPwmDef, dutyCycle);
VirPwm_SetIdle(VirPwmDef, idle);
}
/**
* @brief 设置虚拟PWM的工作状态
* @param VirPwm: 需要操作的虚拟PWM
* @param status: 0, disable; 1, enable
*/
void VirPwm_SetStatus(VirPwm *VirPwmDef, uint8_t status)
{
if(status != 0)
{
VirPwmDef->Status = 1;
}
else
{
VirPwmDef->Status = 0;
}
TIM_Cmd(VirPwmDef->Basetimer, VirPwmDef->Status);
//关闭虚拟PWM之后,将输出电平恢复为idle状态
if(VirPwmDef->Status == 0)
{
if(VirPwmDef->Idle != 0)
{
VirPwmDef->SetOut(1);
}
else
{
VirPwmDef->SetOut(0);
}
}
}
/**
* @brief 设置虚拟PWM的频率
* 虚拟PWM的频率*100即为定时器的更新频
* @param VirPwm: 需要操作的虚拟PWM
* @param freq: frequency of the virtual pwm, ranged from 1~1000 Hz
* 由于虚拟PWM拥有较低的效率,占用较多的CPU时间,因此不能设置较高的工作频率,否则会影响系统的正常工作。
*/
void VirPwm_SetFreq(VirPwm *VirPwmDef, uint16_t freq)
{
//0~100 duty cycle
uint32_t timer_freq;
//Range of frequency is 1 to 1000.
if (freq > 1000)
{
freq = 1000;
}
if(freq < 1)
{
freq = 1;
}
VirPwmDef->Frequency = freq;
timer_freq = VirPwmDef->Frequency*100; //100~100k
//timer_freq < 1k, psc = 7200, f = 10kHz 100*100 Hz (freq < 10)
if(timer_freq<1000)
{
VirPwmDef->Basetimer->PSC = 7200 - 1;
}
//timer_freq < 10k, psc = 720, f = 100kHz 1k*100 Hz (freq < 100)
else if(timer_freq <10000)
{
VirPwmDef->Basetimer->PSC = 720 - 1;
}
//timer_freq < 100k, psc = 72, f = 1MHz 10k*100 Hz (freq < 1000)
else
{
VirPwmDef->Basetimer->PSC = 72 - 1;
}
//Set the update frequency of the timer.
VirPwmDef->Basetimer->ARR = 72000000.0 / (VirPwmDef->Basetimer->PSC + 1) / timer_freq - 1;
}
/**
* @brief 设置虚拟PWM的占空比
* @param VirPwm: 需要操作的虚拟PWM
* @param dutyCycle: duty cycle of the virtual pwm, ranged from 1~100 (%).
* 由于性能限制,虚拟PWM支持的占空比分辨率为1%,范围为0~100%。
*/
void VirPwm_SetDutyCycle(VirPwm *VirPwmDef, uint16_t dutyCycle)
{
//Range of duty cycle is 0 to 100.
if (dutyCycle > 100)
{
dutyCycle = 100;
}
VirPwmDef->DutyCycle = dutyCycle;
}
/**
* @brief 设置虚拟PWM的极性
* @param VirPwm: 需要操作的虚拟PWM
* @param idle: 虚拟PWM在空闲状态下的电平
*/
void VirPwm_SetIdle(VirPwm *VirPwmDef, uint8_t idle)
{
if(idle != 0)
{
VirPwmDef->Idle = 1;
}
else
{
VirPwmDef->Idle = 0;
}
}
/**
* @brief 在定时器中断中周期性调用
* @param VirPwm: 需要操作的虚拟PWM
*/
void VirPwm_TimIRQHandler(VirPwm *VirPwmDef)
{
//占空比为0,则直接关闭PWM输出,将端口设置为idle状态
if(VirPwmDef->DutyCycle == 0)
{
if(VirPwmDef->Idle != 0)
{
VirPwmDef->SetOut(1);
}
else
{
VirPwmDef->SetOut(0);
}
return;
}
if(VirPwmDef->count_P == 0)
{
if(VirPwmDef->count_N == 0)
{
VirPwmDef->count_P = VirPwmDef->DutyCycle;
VirPwmDef->count_N = 100 - VirPwmDef->DutyCycle;
VirPwmDef->SetOut(1);
}
else
{
VirPwmDef->count_N--;
VirPwmDef->SetOut(0);
}
}
else
{
VirPwmDef->count_P--;
VirPwmDef->SetOut(1);
}
}
/**
* @brief 注册为实现端口输出SetOut的服务函数(与具体项目相关)
* @param VirPwm: 需要操作的虚拟PWM
*/
void VirPwm_SetOutHandler(uint8_t state)
{
DIG_OUTPUT_1 = state;
}
使用说明
需要设置不同占空比的PWM应当分别定义一个VirPwm
结构体变量:
VirPwm VirPwmDef1, VirPwmDef2;
建议在声明的时候直接初始化这些结构体变量,如:
VirPwm VirPwmDef1 = {
.Status = 0,
.Idle = 0,
.Frequency = 100,
.DutyCycle = 50,
.Basetimer = TIM4,
.SetOut = VirPwm_SetOutHandler1
.count_P = 0,
.count_N = 100
};
VirPwm VirPwmDef2 = {
.Status = 0,
.Idle = 0,
.Frequency = 100,
.DutyCycle = 75,
.Basetimer = TIM4,
.SetOut = VirPwm_SetOutHandler2
.count_P = 0,
.count_N = 100
};
也可以使用void VirPwm_Init(VirPwm *VirPwmDef,uint16_t freq,uint16_t dutyCycle,uint8_t idle)
, void VirPwm_SetFreq(VirPwm *VirPwmDef, uint16_t freq)
, void VirPwm_SetDutyCycle(VirPwm *VirPwmDef, uint16_t dutyCycle)
, void VirPwm_SetIdle(VirPwm *VirPwmDef, uint8_t idle)
去设置这些结构体变量的成员。
如果PWM结构体的频率相同,可以共同使用同一个定时器,例如上面.Basetimer = TIM4
。在定时器的更新中断处理函数中顺序调用void VirPwm_TimIRQHandler(VirPwm *VirPwmDef)
,并传入相应的PWM结构体变量。如果PWM结构体的频率不同,.Basetimer
需要设置不同的定时器,这是因为定时器的更新频率设置为PWM频率的100倍,如果频率不同的PWM设置了同一个定时器,或造成频率混乱。
对于不同的PWM结构体,应该有不同的端口操作回调函数注册到.SetOut
。虽然理论上可将同一个端口操作回调函数注册到不同的PWM结构体.SetOut
成员中,但不建议这么做,因为可能导致控制逻辑的混乱。示例如下:
VirPwm VirPwmDef1 = {
/*省略无关代码*/
.SetOut = VirPwm_SetOutHandler1
/*省略无关代码*/
};
VirPwm VirPwmDef2 = {
/*省略无关代码*/
.SetOut = VirPwm_SetOutHandler2
/*省略无关代码*/
};
/**
* @brief 注册为实现端口输出SetOut的服务函数(输出两路PWM)
* @param VirPwm: 需要操作的虚拟PWM
*/
void VirPwm_SetOutHandler1(uint8_t state)
{
DIG_OUTPUT_1 = state;
DIG_OUTPUT_2 = state;
}
/**
* @brief 注册为实现端口输出SetOut的服务函数(输出三路PWM)
* @param VirPwm: 需要操作的虚拟PWM
*/
void VirPwm_SetOutHandler2(uint8_t state)
{
DIG_OUTPUT_3 = state;
DIG_OUTPUT_4 = state;
DIG_OUTPUT_5 = state;
}
关于PWM结构体变量的定义完成之后,在项目其他位置完成初始设置(示例如下)。如果在声明PWM结构体变量的时候,已经完成了初始化,理论上无需编写下述代码。
VirPwm_SetStatus(&VirPwmDef1,0);
VirPwm_Init(&VirPwmDef1, 100, 50, 0);
VirPwm_SetStatus(&VirPwmDef2,0);
VirPwm_Init(&VirPwmDef2, 100, 50, 0);
在定时器的更新中断服务函数中,调用void VirPwm_TimIRQHandler(VirPwm *VirPwmDef)
,并传入相应的PWM结构体变量指针。
void TIM4_IRQHandler(void)
{
if(TIM_GetITStatus(TIM4,TIM_IT_Update)!=RESET)
{
VirPwm_TimIRQHandler(&VirPwmDef1);
VirPwm_TimIRQHandler(&VirPwmDef2);
TIM_ClearFlag(TIM4,TIM_FLAG_Update);
TIM_ClearITPendingBit(TIM4,TIM_IT_Update);
}
}
在需要开启或关闭PWM的时候,如果每个定时器只驱动了一个虚拟PWM,可以通过如下方式控制:
VirPwm_SetStatus(&VirPwmDef1,1);
VirPwm_SetStatus(&VirPwmDef2,0);
如果定时器驱动了不止一个虚拟PWM,则不能使用上述函数分别开启或关闭PWM。因为上述函数是通过打开、关闭定时器的方式,会将该定时器驱动的全部PWM打开、关闭。如果需要单独关闭某个虚拟PWM,可以通过设置占空比为0的方式关闭该PWM。
VirPwm_SetDutyCycle(&VirPwmDef1, 0);
联智万物·鲸控未来 联鲸科技
本文地址:https://blog.csdn.net/u013441358/article/details/107975885