欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

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

程序员文章站 2022-07-03 09:03:04
目录背景虚拟PWM库特性源码介绍头文件 virtual_pwm.h源文件 vir_pwm.c使用说明背景现在的主流MCU都支持硬件PWM输出,以STM32F103为例,通用定时器可以支持4路占空比可调的PWM输出,高级定时器可以支持4路带互补输出的PWM输出。硬件产生PWM,具有稳定可靠、执行效率高的特点。但是,硬件产生的PWM也有一些限制,例如:1.输出引脚位置固定,PCB连线可能会不方便;2.输出引脚的数量有限,在一些需要多通道输出的应用中(如多路控温)会占用过多定时器。虚拟PWM库特性由于项...

背景

现在的主流MCU都支持硬件PWM输出,以STM32F103为例,通用定时器可以支持4路占空比可调的PWM输出,高级定时器可以支持4路带互补输出的PWM输出。硬件产生PWM,具有稳定可靠、执行效率高的特点。

但是,硬件产生的PWM也有一些限制,例如:1.输出引脚位置固定,PCB连线可能会不方便;2.输出引脚的数量有限,在一些需要多通道输出的应用中(如多路控温)会占用过多定时器。

虚拟PWM库特性

由于项目需要,笔者编写了VirPwm库,以实现对任意GPIO的产生PWM的驱动。它具有如下特性:

  1. 资源占用小。只需要在一个定时器(软件定时器、硬件定时器均可)周期性的调用
    void VirPwm_TimIRQHandler(VirPwm *VirPwmDef)即可。
  2. 可移植性好。本库是基于STM32F103开发的,但是充分考虑了一直到其他平台上的可能性。通过为VirPwmDef->SetOut函数指针注册端口输出函数以及修改void VirPwm_SetFreq(VirPwm *VirPwmDef, uint16_t freq)的实现,可以顺利实现移植。
  3. 灵活性强。由于开发了端口设置函数指针typedef void (*VirPwm_SetOutput)(uint8_t state),使用者可以*编写函数(如同时驱动多路GPIO输出PWM)作为回调。
  4. 一个定时器可以驱动多路占空比不同的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