PWM讲解+STM32任意频率、占空比、脉宽生成函数介绍

1.PWM讲解

脉冲宽度调制(PWM),是英文"Pulse Width Modulation"的缩写,简称脉宽调制。
脉宽调制

最开始使用PWM时,是做智能车时使用的舵机打角,电机驱动。这都属于比较浅显,普通的应用。下面和大家简单分享一下PWM的一些东西。

1.2 PWM参数

1.2.1 频率(重复频率,重频)

(注:重复频率、重频等是一些行业对频率的不同叫法,这都是一个东西)

PWM波是一串重复的矩形波,频率越高,周期越短(f=T/1),代表1S内重复出现的次数越多,这个应该不难理解。(并非是高电平出现次数,而是高+低,因为一个周期是高+低)
2kHz50%
5kHz50%

比如常用的50Hz对应周期20ms,含义就是1S内有50个重复的波形出现,每个完整波形出现的时间是20ms。1kHz对应周期1ms,5kHz对应周期200us。

1.2.2 占空比

占空比是PWM的第二大重要参数。一般来说,根据实际需要,PWM有输出极性的选择,高极性或低极性。指在有效输出的是高电平还是低电平。

占空比的定义是,在一个周期内,有效电平(此文章中指高电平)所占时间的百分比。
PWM占空比

比如下面这张图,我可以理解为2kHz20%高电平(正脉冲)的PWM波,也可以理解为2kHz80%低电平(负脉冲)的PWM波。

(本文的PWM讲解均以高极性示例。例:下图为20%占空比,指在一个周期中,20%时间为高电平)
2kHz20%
2kHz10%

下图为占空比从0%-100%的变化过程。
占空比在0%-100%之间调整

PWM还有一种作用,等效电平。

在一些条件受限无法使用dac,可以用PWM等效电压进行输出。

比如当某个IO口输出PWM时候,使用示波器测量端口,测到的就是矩形波。

但是你用万用表测量这个端口的电压你显然无法得到一系列图像,你得到的只有一个电压。

如下图,在频率固定的情况下,使用万用表测得的等效电压=幅值电压*占空比
等效电压

这就是使用数字量PWM波去等效模拟量电压。

还有一种PWM的高端玩法叫SPWM,就是利用这种原理来让PWM控制MOS管之类来等效正弦波输出,这里暂时不做介绍,大家有兴趣可以自行查阅。

1.2.3 脉宽

脉宽指的是PWM输出过程中有效电平(本文中指高电平)所占的时间长度

脉宽和占空比,在同频率是可以换算的。

以上文图示的1kHz/2kHz,20%占空比为例:

2kHz对应周期是 500us,20%占空比,则是500us*20%=100us。

1kHz对应周期是1000us,20%占空比,则是1000*20%=200us。

一般在说PWM参数时,更多的使用的是频率和占空比,因为单纯使用脉宽会出现相互矛盾的情况,而且也无法确认PWM唯一。

例如:要求2kHz,1ms脉宽。2kHz周期就已经是500us,我怎么可能给你生成1ms的脉宽???

例如:1ms脉宽,不设置频率。

那么我输出100Hz,10%占空比,200Hz,20%占空比,10Hz1%占空比,都是输出1ms的脉宽,我又怎么确定呢?

所以我一般在换算,参数设置的时候,都用占频率和占空比。只要频率,占空比确定了,输出的PWM就是确定的,不会存在参数冲突,不会存输出参数不唯一的情况。

2.基于STM32库函数的任意频率,任意占空比生产函数介绍

简单介绍一下STM32定时器的PWM输出设置。

在stm32的PWM初始化函数中,我们需要填两个参数,一个arr,一个psc。

这个arr在一些其他芯片也叫period,周期。
PWM输出原理

pwm的输出原理其实非常的简单粗暴,定时器不断的向上计数(根据设置模式不同,也有向下计数,中央对齐计数,本文以向上计数为例),不断与两个数做对比,一个是ccr,输出比较值,一个是arr自动装载值。

当定时器自身计数TIMx->CNT值大于CCRx的值,PWM输出引脚电平改变(高/低),

当TIMx->CNT大于ARR时,计数器归零,重新计数。

如此往复,所以定时器本质就是一个计数器,PWM输出的原理就是不断的比较。

当然,也因为这个原因,当一个定时器用作PWM输出时,就不可以进行其他功能,比如定时器中断,比如输入捕获等。

同时,一个定时器往往有多个通道,同一个定时器可以进行多通道输出,不同通道输出的占空比可以不一样,但是他们的频率一定是一样的。

原因就是ARRx是自动装载值,决定了周期,且一个定时器使用同一个装载值;

而控制占空比的CCRx每个通道都有一个,所以可以根据CCRx的不同,控制不同占空比输出。
相同频率, 不同占空比输出

好了,我们已经基本了解了PWM输出原理,下面开始根据需要设置输出。

设置PWM输出最重要的公式在下面:

注:上式的arr+1,psc+1是因为他们是分母,且是u16位的数据类型。如果人为赋值为零,会造成除0,会有无法预计的后果,单片机会自动给他+1,我们在设置时候记得手动减一即可。

上式中:CLK为单片机时钟频率,STM32F103系列一般为72M,是固定值;

arr为装载值,u16,数据范围0-65535;

psc为预分频系数,u16,数据范围0-65535;

f为需要输出的PWM频率;

还有一个值,ccr代表占空比。

下面先介绍占空比计算方法:占空比=ccr/arr*100%

(假设arr为100,ccr为20,那么计数器从0开始计数,计数到20时,计数器值大于ccr,输出电平改变,当计数到100时,计时器自动归零,重新计数。)

在数值范围不超u16范围限制情况下,arr越大,可调占空比精度越高。

假设ccr最大值为10k:

最大分辨率为1/10k*100%=0.01%;//万分之一

假设ccr最大值为50:

最大分辨率为1/50*100%=20%;//五分之一

我们再返回来看频率公式:

既然CLK是固定的,输出频率f固定的情况下,我们尽可能让psc降低,使arr变大。但是不能让arr太大,因为他是u16类型,最大也只有65535。那么我们根据不同的频率,调整psc的值,让arr始终在1k-10k,也就是千分之一到万分之一精度即可。

我的想法是做简单的判断,根据不同的频率,给不同的分频系数,代码参考如下:

cpp 复制代码
#define MAIN_CLK 72000000

u16 arr = 0;
u16 ccr = 0;
u16 psc = 0;
u16 ckl_temp = 0;

if(freq>=5000)//5kHz+,
{
   psc = 1;//72M/1=72M时钟
}
else if(100<=freq && freq<5000)//100-5000
{
   psc = 72;//72M/72=1M时钟
}
else //1Hz-100Hz
{
   psc = 7200;//72M/7200=10k时钟
}

ckl_temp = MAIN_CLK /psc;
arr =(u16)(ckl_temp /freq);
ccr =(u16)(arr*duty/100.0); 

TIMx_PWM_Init((u16)(ccr-1),(u16)(psc));

这里的分界线是100Hz和5000Hz。

1Hz-100Hz时,7200分频,72M/7200=10k时钟,

1Hz时, arr=10k/1=10k

100Hz时,arr=10k/100=100

100Hz-5000Hz时,72分频,72M/72=1M时钟,

100Hz时, arr=1M/100=10k

5000Hz时,arr=1M/5000=200

5000Hz以上,1分频,72M时钟,

5000Hz时,arr=72M/5000=14400

整体精度在百分之一以上,就是不优雅。

那我们为什么不能大胆一点,直接将arr设置为65535呢?

此时clk是确定值,输入的f也是确定值,arr我们人工设置为65535这样他就是确定值,解方程解出psc即可。

当然,如果f太大,以STM32的72M时钟来说,f=1098时

显然psc作为预分频系数是不能为0的,那我们需要简单处理一下,如果psc小于1那么让他保持为1。

这里也还需要考虑到下psc的溢出情况,首先我们可以限制f频率的输入,一般最小限制为1Hz,在STM32F103平台上,f越小,psc越大。如果f是1,psc也只是1098而已,不会溢出。

如果换一个操作平台,他的主频较高,会不会溢出呢?

答案是不会,计算过程如下:

我们让psc最大,为65536;让f最小,为1,求clk的值。(暂时先不考虑psc的-1问题)

ckl=psc*f*65535=65535*1*65535=4,294,836,225大概是4.29GHz的频率。

所以当主时钟小于4.2GHz时,最小输出1Hz情况下,大胆的让arr=65535吧,无需考虑psc的溢出问题。

参考代码如下:

cpp 复制代码
#define MAIN_CLK 72000000

float psc_temp= 0;
psc_temp=(MAIN_CLK /(freq * 65535.0))-1;//arr设定为65535
if(psc_temp < 1)
{
    psc_temp= 1;
}
psc = (u16) psc_temp;
arr = MAIN_CLK  / (freq * psc );    //arr计算
ccr = arr * duty / 100.0f; //通过占空比,计算比较值

//定时器初始化
TIM4_PWM_Init((u16)(arr - 1), psc);
TIM_SetCompare1(TIM4, (u16)(ccr));		//TIM4 CH1 -PD12

这里我们还可以再次改进,利用位操作,将除法改为位移,省去了判断0的步骤,代码更为优雅。

(此处灵感来源(抄袭)于逐飞科技,逐飞科技的驱动代码还是非常值得学习。在此感谢逐飞科技对我嵌入式学习的大力支持,我在大学买了他家很多东西,这也算是交过学费了吧)

cpp 复制代码
#define MAIN_CLK  72000000 //STM32时钟72M

/*-------------------------------------------------------------------------------------------------------------------
  @brief    定时器4初始化
  @param    arr 装载值
            psc 分频系数
  @return   null
  Sample    TIM4_PWM_Init((ccr - 1), psc - 1);
  @note     由其他函数调用,无需关心
-------------------------------------------------------------------------------------------------------------------*/
void TIM4_PWM_Init(u16 arr, u16 psc)
{
    //**结构体声明**//
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure; //声明定时器
    TIM_OCInitTypeDef  TIM_OCInitStructure;         //声明PWM通道
    GPIO_InitTypeDef GPIO_InitStructure;            //声明GPIO

    //**时钟使能**//
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE);	//使能定时器TIM4时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);	//使能PD端口时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);  //复用功能模块时钟
    GPIO_PinRemapConfig(GPIO_Remap_TIM4, ENABLE);	        //重映射

    //PD14
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;				//D14 端口配置
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; 	//复用推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	//IO口速度为50MHz
    GPIO_Init(GPIOD, &GPIO_InitStructure);					  //根据设定参数初始化GPIOD14
    GPIO_ResetBits(GPIOD, GPIO_Pin_14);

    //PD12
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;				 //PD12端口配置
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; 	 //复用推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	 //IO口速度为50MHz
    GPIO_Init(GPIOD, &GPIO_InitStructure);					   //根据设定参数初始化GPIOD12
    GPIO_ResetBits(GPIOD, GPIO_Pin_12);

    //初始化TIM4
    TIM_TimeBaseStructure.TIM_Period = arr; //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
    TIM_TimeBaseStructure.TIM_Prescaler = psc; //设置用来作为TIMx时钟频率除数的预分频值
    TIM_TimeBaseStructure.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;  //TIM向上计数模式
    TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位

    //初始化TIM4 Channel1 PWM模式
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //选择定时器模式:TIM脉冲宽度调制模式2
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; //输出极性:TIM输出比较极性高
    TIM_OC1Init(TIM4, &TIM_OCInitStructure);  //根据T指定的参数初始化外设TIM4 OC3

    //初始化TIM4 Channel3 PWM模式
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2; //选择定时器模式:TIM脉冲宽度调制模式2
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; //比较输出使能
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low; //输出极性:TIM输出比较极性高
    TIM_OC3Init(TIM4, &TIM_OCInitStructure);  //根据T指定的参数初始化外设TIM4 OC3

    TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable);  //使能TIM4_CH1预装载寄存器
    TIM_OC3PreloadConfig(TIM4, TIM_OCPreload_Enable);  //使能TIM4_CH3预装载寄存器

    TIM_Cmd(TIM4, DISABLE);  //默认失能TIM4
    //TIM_Cmd(TIM4, ENABLE); //使能TIM4
}

/*-------------------------------------------------------------------------------------------------------------------
  @brief    设置频率,占空比PWM输出
  @param    freq 频率查看宏定义
            duty 占空比0-100(代表0%-100%)
  @return   null
  Sample    Set_PWM_Duty_Output(5000,20);//5kHz,20%占空比
  @note     注意,0-100占空比代表0%-100%
	        输出通道为TIM4 CH1 -PD12,如需修改,在最后输出定时器,通道处修改即可
-------------------------------------------------------------------------------------------------------------------*/
void Set_PWM_Duty_Output(u32 freq, double duty)
{
    u16 psc = 0;     //分频系数
    u16 arr = 0;     //装载值
    u16 ccr = 0;     //比较值

    if(freq >= FREQ_MAX)
    {
        freq = FREQ_MAX;
    }
    else if(freq <= FREQ_MIN)
    {
        freq = FREQ_MIN;
    }

    if(duty >= DUTY_MAX)
    {
        duty = DUTY_MAX;
    }
    else if(duty <= DUTY_MIN)
    {
        duty = DUTY_MIN;
    }

    psc= (u16)((MAIN_CLK / freq) >> 16);    //多少分频
    arr= (u16)( MAIN_CLK /(freq*(psc+ 1))); //周期
    ccr= arr* duty / 100;                   //占空比

    TIM4_PWM_Init((u16)(arr- 1), psc);

    if(duty <= DUTY_MIN  )
    {
        ccr= DUTY_MIN  ;//零占空比时,compare给0即可
    }
    else if(duty >= DUTY_MAX  )
    {
        ccr= arr+ 1;//满占空比时,ccr要比arr大,不能相等
    }

    TIM_SetCompare1(TIM4, (u16)(ccr));		//TIM4 CH1 -PD12
//    TIM_SetCompare3(TIM4, (u16)(ccr));		//TIM4 CH3 -PD14

    TIM_Cmd(TIM4, ENABLE);  //使能TIM4
}

psc= (u16)((MAIN_CLK / freq) >> 16); //多少分频

这里的>>16含义如下:

1.除以65536,位运算更快

2.psc是16位,取结果的高16位,保证不溢出

3.高16位如果都是0,那么他的值就是0,不比考虑小数负数之类的事情

下一步为避免除0的问题,给psc+1再计算arr

其中有一小点需要注意,占空比为0,也就是低电平输出,占空比为100,高电平输出,需要特殊处理一下。因为ccr和arr值不能相等,不然会出现一个计数周期的脉冲跳变。

3.基于STM32库函数的任意频率,任意脉宽生产函数介绍

有了上面的任意占空比函数生成,任意脉宽生产也只需要增加一点占空比计算部分而已。

假设情况如下:

频率:5kHz~50kHz,频率步进100Hz

脉宽:0.5us~10us,步进0.1us

首先确认参数是否有冲突。

5kHz,周期为200us,0.5us占空比为1/400

5kHz,周期为200us,10us占空比为1/20

步进0.1us为占空比的1/2000

50kHz,周期为20us,0.5us占空比为1/40

50kHz,周期为20us,10us占空比为1/2

步进0.1us为占空比的1/200

没出现占空比超过100%,没出现占空比步进值过低的情况。

任意脉宽输出其实只是多了一个脉宽与占空比换算的部分而已。

具体计算过程其实也在上面写了,先算当前频率下的周期,然后计算当前脉宽占周期的值,这个值就是占空比。

计算完毕后考虑一下参数冲突的情况,做好边界处理。

(此处建议大家养成良好的编程习惯,对于所有已知数据范围的变量,一定要做好限幅处理)

其实核心代码只有两行,一个计算周期,一个计算当前脉宽占周期的多少,但是各种数据范围的判断,限制占了大量篇幅。

参考代码如下:

cpp 复制代码
#define MAIN_CLK  72000000 //STM32时钟72M
 
//这里
#define WIDTH_MAX 100000 //脉宽100000ns
#define WIDTH_MIN 10     //脉宽10ns
#define FREQ_MAX  10000  //频率10MHz
#define FREQ_MIN  1      //频率1Hz
#define DUTY_MAX  100    //占空比100%
#define DUTY_MIN  0      //占空比0%
/*-------------------------------------------------------------------------------------------------------------------
  @brief    设置频率,脉宽控制PWM输出
  @param    freq 频率 范围查看宏定义
            pulse_width 脉宽 范围查看宏定义
  @return   null
  Sample    Set_PWM_Width_Output(5000,1000);//5kHz,1us脉宽
  @note     注意,内部计算出占空比后,
	        内部调用Set_PWM_Duty_Output(u32 freq, double duty)实现,无需重复调用。
			输出通道为TIM4 CH1 -PD12
            可去Set_PWM_Duty_Output函数中自行替换输出通道
-------------------------------------------------------------------------------------------------------------------*/

void Set_PWM_Width_Output(u32 freq, u16 pulse_width)
{
    double duty = 0;
    double period = 0;

    if((0 == freq) && (0 == pulse_width))//输入为0,直接拉低
    {
        freq = FREQ_MIN;
        duty = DUTY_MIN;
    }
    else//输入非0,正常输出
    {
        if(freq >= FREQ_MAX)
        {
            freq = FREQ_MAX;
        }
        else if (freq <= FREQ_MIN)
        {
            freq = FREQ_MIN;
        }

        if(pulse_width >= WIDTH_MAX)
        {
            pulse_width = WIDTH_MAX;
        }
        else if(pulse_width <= WIDTH_MIN)
        {
            pulse_width = WIDTH_MIN;
        }

        period = 1000000000.0f / freq;      //计算周期,单位为ns,所以用1G除频率
        duty = pulse_width * 100.f / period;//计算当前脉宽占周期多少,单位是百分比,例:算出前脉宽占周期0.5(50%),此处值将是50
        
        if(duty <= DUTY_MIN )//如果输入参数矛盾,此处0占空比,满占空比输出
        {
            duty = DUTY_MIN;
        }
        else if( duty >= DUTY_MAX )
        {
            duty = DUTY_MAX;
        }
    }

    Set_PWM_Duty_Output(freq, duty);//调用占空比函数
}

这里再附上STM32硬件定时器通道表

(文件来自STM32F10XXX中文参考手册_V10,118页-119页)

4.完整文件在此

里面的频率限制完全可以打开,1Hz到很高都可以(显然不能超过72M主时钟频率),只是频率越高,占空比精度控制越差,具体原因上面都写了。
10MHz输出频率准确
50ns实际41.667ns

我在keil中仿真测试测试,10MHz,50%(50ns),实际输出10MHz,41.667ns。虽然与设定有点误差,但我觉得已经不错了。

实际情况下应该不会有人用stm32去产生10MHz的信号,这里仅供娱乐参考。

cpp 复制代码
#include "PWM.h"
#include "stm32f10x_tim.h"

/*-------------------------------------------------------------------------------------------------------------------
  @brief    定时器4初始化
  @param    arr 装载值
            psc 预分频系数
  @return   null
  Sample    TIM4_PWM_Init(ccr - 1, psc - 1);
  @note     由其他函数调用,无需关心
-------------------------------------------------------------------------------------------------------------------*/
void TIM4_PWM_Init(u16 arr, u16 psc)
{
    //**结构体声明**//
    TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure; //声明定时器
    TIM_OCInitTypeDef  TIM_OCInitStructure;         //声明PWM通道
    GPIO_InitTypeDef GPIO_InitStructure;            //声明GPIO
 
    //**时钟使能**//
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM4, ENABLE); //使能定时器TIM4时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);//使能PD端口时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); //复用功能模块时钟
    GPIO_PinRemapConfig(GPIO_Remap_TIM4, ENABLE);	     //重映射
 
    //PD14
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14;		  //D14 端口配置
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;   //复用推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
    GPIO_Init(GPIOD, &GPIO_InitStructure);		      //根据设定参数初始化GPIOD14
    GPIO_ResetBits(GPIOD, GPIO_Pin_14);
 
    //PD12
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;		 //PD12端口配置
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  //复用推挽输出
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//IO口速度为50MHz
    GPIO_Init(GPIOD, &GPIO_InitStructure);			 //根据设定参数初始化GPIOD12
    GPIO_ResetBits(GPIOD, GPIO_Pin_12);
 
    //初始化TIM4
    TIM_TimeBaseStructure.TIM_Period = arr;                     //设置在下一个更新事件装入活动的自动重装载寄存器周期的值
    TIM_TimeBaseStructure.TIM_Prescaler = psc;                  //设置用来作为TIMx时钟频率除数的预分频值
    TIM_TimeBaseStructure.TIM_ClockDivision = 0;                //设置时钟分割:TDTS = Tck_tim
    TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
    TIM_TimeBaseInit(TIM4, &TIM_TimeBaseStructure);             //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
 
    //初始化TIM4 Channel1 PWM模式
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2;            //选择定时器模式:TIM脉冲宽度调制模式2
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//比较输出使能
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;     //输出极性:TIM输出比较极性高
    TIM_OC1Init(TIM4, &TIM_OCInitStructure);                     //根据T指定的参数初始化外设TIM4 OC3
 
    //初始化TIM4 Channel3 PWM模式
    TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2;            //选择定时器模式:TIM脉冲宽度调制模式2
    TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;//比较输出使能
    TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;     //输出极性:TIM输出比较极性高
    TIM_OC3Init(TIM4, &TIM_OCInitStructure);                     //根据T指定的参数初始化外设TIM4 OC3
 
    TIM_OC1PreloadConfig(TIM4, TIM_OCPreload_Enable);  //使能TIM4_CH1预装载寄存器
    TIM_OC3PreloadConfig(TIM4, TIM_OCPreload_Enable);  //使能TIM4_CH3预装载寄存器
 
    TIM_Cmd(TIM4, DISABLE);  //默认失能TIM4
}
 
/*-------------------------------------------------------------------------------------------------------------------
  @brief    设置频率,占空比PWM输出
  @param    freq 频率查看宏定义范围
            duty 占空比0-100(代表0%-100%)
  @return   null
  Sample    Set_PWM_Duty_Output(5000,20);//5kHz,20%占空比
  @note     注意,0-100占空比代表0%-100%
	        输出通道为TIM4 CH1 -PD12,如需修改,在最后输出定时器,通道处修改即可
-------------------------------------------------------------------------------------------------------------------*/
void Set_PWM_Duty_Output(u32 freq, double duty)
{
    u16 psc = 0;     //分频系数
    u16 arr = 0;     //装载值
    u16 ccr = 0;     //比较值
 
    if(freq >= FREQ_MAX)
    {
        freq = FREQ_MAX;
    }
    else if(freq <= FREQ_MIN)
    {
        freq = FREQ_MIN;
    }
 
    if(duty >= DUTY_MAX)
    {
        duty = DUTY_MAX;
    }
    else if(duty <= DUTY_MIN)
    {
        duty = DUTY_MIN;
    }
 
    psc = (u16)((MAIN_CLK / freq) >> 16);    //多少分频
    arr = (u16)( MAIN_CLK /(freq*(psc+ 1))); //周期
    ccr = arr* duty / 100;                   //占空比
 
    TIM4_PWM_Init((u16)(arr- 1), psc);
 
    if(duty <= DUTY_MIN  )
    {
        ccr= DUTY_MIN  ;//零占空比时,compare给0即可
    }
    else if(duty >= DUTY_MAX  )
    {
        ccr= arr+ 1;//满占空比时,ccr要比arr大,不能相等
    }
 
    TIM_SetCompare1(TIM4, (u16)(ccr));		//TIM4 CH1 -PD12
//    TIM_SetCompare3(TIM4, (u16)(ccr));		//TIM4 CH3 -PD14
 
    TIM_Cmd(TIM4, ENABLE);  //使能TIM4
}

/*-------------------------------------------------------------------------------------------------------------------
  @brief    设置频率,脉宽控制PWM输出
  @param    freq 频率 查看宏定义
            pulse_width 查看宏定义
  @return   null
  Sample    Set_PWM_Width_Output(5000,1000);//5kHz,1us脉宽
  @note     注意,内部计算出占空比后,
	        内部调用Set_PWM_Duty_Output(u32 freq, double duty)实现,无需重复调用。
			输出通道为TIM4 CH1 -PD12
            可去Set_PWM_Duty_Output函数中自行替换输出定时器/通道
-------------------------------------------------------------------------------------------------------------------*/
 
void Set_PWM_Width_Output(u32 freq, u16 pulse_width)
{
    double duty = 0;
    double period = 0;
 
    if((0 == freq) && (0 == pulse_width))//输入为0,直接拉低
    {
        freq = FREQ_MIN;
        duty = DUTY_MIN;
    }
    else//输入非0,正常输出
    {
        if(freq >= FREQ_MAX)
        {
            freq = FREQ_MAX;
        }
        else if (freq <= FREQ_MIN)
        {
            freq = FREQ_MIN;
        }
 
        if(pulse_width >= WIDTH_MAX)
        {
            pulse_width = WIDTH_MAX;
        }
        else if(pulse_width <= WIDTH_MIN)
        {
            pulse_width = WIDTH_MIN;
        }
 
        period = 1000000000.0f / freq;      //计算周期,单位为ns,所以用1G除频率
        duty = pulse_width * 100.f / period;//计算当前脉宽占周期多少,单位是百分比,例:算出前脉宽占周期0.5(50%),此处值将是50
        
        if(duty <= DUTY_MIN )//如果输入参数矛盾,此将处0占空比,满占空比输出
        {
            duty = DUTY_MIN;
        }
        else if( duty >= DUTY_MAX )
        {
            duty = DUTY_MAX;
        }
    }
 
    Set_PWM_Duty_Output(freq, duty);//调用占空比函数
}
cpp 复制代码
#ifndef __PWM_H__
#define __PWM_H__

#include "sys.h"

#define MAIN_CLK  72000000 //STM32时钟72M
 
#define WIDTH_MAX 100000 //脉宽100000ns
#define WIDTH_MIN 10     //脉宽10ns
#define FREQ_MAX  10000  //频率10MHz
#define FREQ_MIN  1      //频率1Hz
#define DUTY_MAX  100    //占空比100%
#define DUTY_MIN  0      //占空比0%

void TIM4_PWM_Init(u16 arr, u16 psc);
void Set_PWM_Duty_Output(u32 freq, double duty);
void Set_PWM_Width_Output(u32 freq, u16 pulse_width);

#endif

希望能够帮助到一些人。

本人菜鸡一只,各位大佬发现问题欢迎留言指出。

相关推荐
不脱发的程序猿1 小时前
在超频单片机时,需要注意哪些稳定性问题?
单片机·嵌入式硬件
sword devil9001 小时前
STM32项目实战:ADC采集
stm32·单片机·嵌入式硬件
-liming-2 小时前
单片机设计_停车场车位管理系统(AT89C52、LCD1602)
单片机·嵌入式硬件·51单片机
不脱发的程序猿2 小时前
单片机超频怎么搞?
单片机·嵌入式硬件·单片机超频
hahaha60163 小时前
uart16550详细说明
stm32·单片机·嵌入式硬件
Invinciblenuonuo3 小时前
STM32八股【9】-----volatile关键字
stm32·单片机·嵌入式硬件
深圳市青牛科技实业有限公司 小芋圆3 小时前
CS4334:一款高性能的立体声音频数模转换器
科技·嵌入式硬件·音视频·智能家居·新能源·工控主板
赵谨言4 小时前
基于单片机路灯自动控制仪仿真设计
经验分享·单片机·毕业设计
YOYO--小天5 小时前
RK3588 ADB使用
linux·嵌入式硬件·adb