单片机学习小组
直播中

张娟

7年用户 1993经验值
私信 关注

如何使用单个定时器驱动多路模拟PWM输出?

如何使用单个定时器驱动多路模拟PWM输出?

回帖(1)

李叱镡

2022-2-8 14:45:18
背景
现在的主流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"


#ifdef USE_VIRTUAL_PWM
/**
* 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
#endif /* LIBRARIES_SYSEXTEND_INC_VIRTUAL_PWM_H_ */


源文件 virtual_pwm.c
/*
* virtual_pwm.c
*
*  Created on: 2020年8月12日
*      Author: Tao
*/


#include "virtual_pwm.h"


#ifdef USE_VIRTUAL_PWM
/**
* 使用虚拟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;
        }
       
        //设置驱动虚拟PWM的定时器更新中断周期
        VirPwm_SetFreq(VirPwmDef, VirPwmDef->Frequency);
        //开启或关闭驱动虚拟PWM的定时器
        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;
        }


        //占空比为100,则直接打开PWM输出,将端口设置为busy状态
        if(VirPwmDef->DutyCycle >= 100)
        {
                if(VirPwmDef->Idle != 0)
                {
                        VirPwmDef->SetOut(0);
                }
                else
                {
                        VirPwmDef->SetOut(1);
                }


                return;
        }


        //占空比为1~99时,正常处理
        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;
}


#endif


使用说明
需要设置不同占空比的PWM应当分别定义一个VirPwm结构体变量:


VirPwm VirPwmDef1, VirPwmDef2;
1
建议在声明的时候直接初始化这些结构体变量,如:


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)去设置这些结构体变量的成员。


需要特别注意的是void VirPwm_SetFreq(VirPwm *VirPwmDef, uint16_t freq)函数不仅改变结构体变量的Frequency成员的值,同时也设定了驱动定时器的更新频率。当然只要正确初始化了虚拟PWM的结构体变量,在void VirPwm_SetStatus(VirPwm *VirPwmDef, uint8_t status)函数中打开或关闭PWM输出总开关的时候,其内部已经调用了一次void VirPwm_SetFreq(VirPwm *VirPwmDef, uint16_t freq)函数以完成驱动定时器更新频率的刷新:


void VirPwm_SetStatus(VirPwm *VirPwmDef, uint8_t status)
{
        /*此处省略了无关内容*/
       
        //设置驱动虚拟PWM的定时器更新中断周期
        VirPwm_SetFreq(VirPwmDef, VirPwmDef->Frequency);
        //开启或关闭驱动虚拟PWM的定时器
        TIM_Cmd(VirPwmDef->Basetimer, VirPwmDef->Status);
       
        /*此处省略了无关内容*/
}


如果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);
举报

更多回帖

发帖
×
20
完善资料,
赚取积分