解决的需求
自主配置定时器的参数,输出PWM,控制电机
在查阅本文章前请先对STM32基本定时器的时基单元进行了解,具体可以参考我的这篇文章:
基本定时器编写延时函数
具体的时钟配置和分频设置本篇文章不细讲,只着重讲高级定时器在时基单元与基本定时器不同的地方。以下附上我的时钟分配:
高级定时器的功能太复杂,内容繁多,这里着重讲解解决需求的功能
一、高级定时器TIM1/TIM8
1、时基单元
高级定时器TIM挂载在APB2上,具体得到的时钟频率看CubeMX的时钟树
数值上,CK_INT=72M
CK_PSC是进入预分频器前获得的实际频率,在选择内部时钟作为时钟源时,一般CK_PSC=CK_INT;
预分频器是如何实现分频的呢?
预分频器通过分频计数器,通过计数来管理CK_CNT的输出来实现分频,因为是计数的形式,按照硬件是从0开始计数的思想,72M要实现72分频,分频计数器装载的值为72-1;
在预分频器的作用下,CK_PSC被分频,分频后的频率就是CK_CNT,提供给计数器运行。计数器的运行速度只与频率CK_CNT的值有关。
图中红色框住的部分就是重复计数器和它的影子寄存器。
重复计数器对应的是重复计数寄存器,是高级定时器在时基部分和基本/通用定时器不同的地方,他的作用是延长更新事件产生的周期,只有当该寄存器内的值递减为0时,计数器上溢/下溢才会产生更新事件。
在PWM输出中,我们对更新事件的产生频率要根据项目的实际情况设置,一般设重复计数寄存器的值为0就好。
2、更新操作与更新事件
1、更新操作:
我们都知道,在递增计数时,当计数值与用户设置的计数值(自动重装载寄存器的值)一样时,计数器CNT会置0,开始新的一轮计数;在递减计数时也同理,会回到用户设置的计数值再次减为0;这两种情况分别叫做计数上溢和计数下溢;
当分频计数达到用户设置的分频值-1时,也会回到0重新开始计数,实现对CK_PSC的分频进而得到CK_CNT;
当重复计数器的值递减为0时,先产生更新事件,再产生更新操作,它的值回到用户设定的值。
CNT计数值,PSC计数值和RCR计数值被自动重置的过程,但影子寄存器没被更新,相关更新标志没被置位的过程,叫做更新操作
因为重复计数器只有高级定时器才有,在基本定时器和通用定时器,更新操作和更新事件是并发的,而在高级定时器两者才分离开来产生。
2、更新事件:
产生条件:重复计数器的值递减为0
含有影子寄存器的寄存器:
时基单元:预分频器PSC,自动重装载寄存器,重复计数寄存器
功能部分:4个捕获/比较寄存器
更新事件进行的工作:
①把预装载寄存器的值更新到影子寄存器(更新操作没有进行这项工作)
②CNT计数值,PSC计数值和RCR计数值被重新初始化(更新操作有进行这项工作)
③相关更新中断请求被置位
产生更新操作不一定产生更新事件,产生更新事件一定伴随更新操作。
中断的请求要结合具体场景展开分析,这里不做论述,先知道有这么回事。
3、与其他定时器的比较
①、与其他定时器时基单元的比较
显然基本定时器和通用定时器的更新操作和更新事件是同步发生的,因为他们没有重复计数器。
②、与通用定时器的比较
4、CK_INT与CK_CNT
在选择内部时钟作为时钟源的情况下:
①CK_CNT决定了CNT计数器的运行速度,而输出比较和输入捕获又是在CNT计数器的基础上运行的,进而也决定了输出比较和输入比较的运行速度。
②CK_INT是高级定时器整个外设的时钟源, CK_CNT是外设时钟源在外设内部再一次进行分支管理的结果。
CK_INT决定了死区时间的计算,而非CK_CNT,这点要特别注意。
关于这点会在死区时间部分进行具体介绍。
二、高级定时器TIM1/TIM8的输出比较功能
1、细说PWM
<1>通俗讲什么是PWM
PWM中文名称是脉冲宽度调制,对于小白来说,这个名字起得听了也不知道是啥。来看图:
PWM信号其实就是方波,输出PWM信号就是输出方波,而且方波的高电平时间和低电平时间是可以调制的,意思就是可以根据用户需要来设置。从数字电路上理解,用1代表高电平;用0代表低电平。PWM信号是周期性变化的方波,图中周期为T,在一个周期内,它某个电平维持时间叫做脉宽(图中为τ),脉宽可以通过对定时器进行配置来调整,脉宽的长短决定了方波的电压有效值。脉宽是相对而言的,可以是指低电平的脉宽,也可以指高电平的脉宽,一般我们说的脉宽都是高电平的脉宽。
<2>PWM信号的来源
PWM信号它的本质来源是捕获/比较x寄存器产生的输出比较参考信号,这个信号用OCxREF(x指通道号)表示,英文全名Output(输出) Comparison(比较) Reference(参考)。后面的输出OCx和OCxN全都是在OCxREF基础上产生的,所以OCxREF的跳变就意味着OCx和OCxN的跳变。
包括死区时间都是相对OCxREF产生的,并非两路互补信号才能产生死区时间,单路PWM输出也能产生死区时间,因为死区时间是相对OCxREF产生的,而非OCxN相对OCx比较来产生,或OCx相对OCxN比较来产生。
如果设置了死区时间,参考信号就会进入DTG,DTG产生的信号进入输出控制区域,然后再输出OCx或OCxN到外部引脚。
如果不设置死区时间,那么参考信号直接进入输出控制区域。
参考信号是什么?
参考信号也是方波,是输出PWM信号的参考来源,它高电平代表有效电平,低电平代表无效电平
OCx和OCxN在参考OCxREF基础上进行产生,所以OCxREF跳变,也意味着OCx和OCxN跳变。
全图可看:
<3>输出比较中“比较”怎么理解
占空比+:方波中高电平时间在一个周期时间中的占比。
占空比-:方波中低电平时间在一个周期时间中的占比。
一般我们说的占空比都是占空比+
“比较”其实是计数器CNT在跟比较寄存器CCR的值比较,比较寄存器CCR的值决定了最终输出PWM信号的占空比,在PWM1模式中,如果CNT<CCR,对应通道的参考信号OCxREF为有效状态(电平为"1");如果CNT>CCR,对应通道的参考信号OCxREF为无效状态(电平为"0"),所以CNT=CCR是参考信号OCxREF发生跳变的临界点。
PWM2模式则与PWM1模式相反。
<4>PWM信号及其互补信号的极性
官方数据手册这里的描述非常有矛盾。其实他这个配置项的意思是:参考信号OCxREF的高电平代表有效,OCx的PWM正输出中有效代表高电平,OC1高电平有效的意思就是,OCxREF处于有效电平时,OCx输出高电平;OCxREF处于无效电平时,OCx输出低电平。最终OCx输出给外部引脚。
PWM信号正输出与其互补信号输出用的是同一个参考信号OCxREF,所以OCxREF的高电平代表有效是不变的原则。
互补信号OCxN的输出中有效代表低电平,OC1N高电平有效的意思就是,OCxREF处于有效电平时,OCxN输出低电平;OCxREF处于无效电平时,OCx输出高电平。
2、PWM单路输出的配置与代码
对CK_PSC,也就是72M进行72分频,CNT得到CK_CNT,也就是1M的频率,计数一次是1us,这里计数200us,所以输出的PWM信号周期是200us;
这里配置的是捕获/比较寄存器CCRx的值,并不是说输入60占空比就是60%;
CNT计数200次,从0到119计数了120次,从120计数到(200-1)计数了80次,两者合计就是计数200次,刚好200us一个周期;这里Pulse填入的,就是选择参考信号OCxREF在CNT计数到哪里时进行跳变,当CNT和CCRx比较,并达到CCRx存的值时,OCxREF会跳变,OCx和OCxN的电平状态自然而然也就跟着跳变。
/* TIM1 init function */
void MX_TIM1_Init(void)
{
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
TIM_MasterConfigTypeDef sMasterConfig = {0};
TIM_OC_InitTypeDef sConfigOC = {0};
TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0};
htim1.Instance = TIM1;
htim1.Init.Prescaler = 72-1; //72分频
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;//向上计数
htim1.Init.Period = 200-1; //一轮数200次
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;//CK_INT不分频
htim1.Init.RepetitionCounter = 0; //重复计数器为0
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;//使能自动重装载
if (HAL_TIM_Base_Init(&htim1) != HAL_OK)
{
Error_Handler();
}
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;//选择内部时钟CK_INT作为时钟源
if (HAL_TIM_ConfigClockSource(&htim1, &sClockSourceConfig) != HAL_OK)
{
Error_Handler();
}
if (HAL_TIM_PWM_Init(&htim1) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;//关闭主输出触发
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;//关闭主从模式
if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
sConfigOC.OCMode = TIM_OCMODE_PWM1;//PWM1输出模式
sConfigOC.Pulse = 120;//在CNT为120时PWM信号发生跳变,120/200=60%占空比
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;//信号极性:高电平有效
sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;//互补信号极性:高电平有效
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;
if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
{
Error_Handler();
}
sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE;
sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE;
sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
sBreakDeadTimeConfig.DeadTime = 0;//不设死区时间
sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE;//关闭刹车功能
sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH;
sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE;
if (HAL_TIMEx_ConfigBreakDeadTime(&htim1, &sBreakDeadTimeConfig) != HAL_OK)
{
Error_Handler();
}
HAL_TIM_MspPostInit(&htim1);//初始化相关的GPIO
}
int main(void)
{
...
HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);//开启单路PWM输出
while (1)
{
}
}
3、PWM双路互补输出的配置与代码
这里的配置只是使能了CH1和CH1N的输出,并不是说配好了这里就能马上双路互补输出了。
两者要双路互补输出还需要以下配置
至于为什么两者相等才能互补输出,请看二中1的第<4>点。
PWM双路互补输出在TIM相关结构体的初始化上,与单路PWM输出无区别,区别在于要对互补信号的输出引脚进行好相应的GPIO初始化。
/**
******************************************************************************
* @brief 定时器GPIO的初始化
******************************************************************************
**/
void HAL_TIM_MspPostInit(TIM_HandleTypeDef* timHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(timHandle->Instance==TIM1)
{
__HAL_RCC_GPIOA_CLK_ENABLE();
/**TIM1 GPIO Configuration
PA7 ------> TIM1_CH1N
PA8 ------> TIM1_CH1
*/
GPIO_InitStruct.Pin = GPIO_PIN_7|GPIO_PIN_8;
//单路PWM输出中只有CH1-PA8被初始化,现在CH1N-PA7也加入了GPIO初始化
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
__HAL_AFIO_REMAP_TIM1_PARTIAL();
}
}
int main(void)
{
...
HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);//开启PWM输出
HAL_TIMEx_PWMN_Start(&htim1,TIM_CHANNEL_1);//使能PWM互补信号输出
while (1)
{
//可以双路互补输出了
}
}
4、演示效果
①占空比为60%的单路PWM输出
周期为200us,符合本例设定的预期。
②占空比为60%的双路PWM输出
周期200us,理论上占空比+是60%,但因为加入了死区时间,在60%基础上存在一定时间占比的削减,属于正常误差;互补信号的占空比+,占空比-与正向输出信号的情况是相反的。
三、PWM双路互补输出的死区时间与刹车实现
这里再重申一遍:
死区时间都是相对OCxREF产生的,并非两路互补信号才能产生死区时间,单路PWM输出也能产生死区时间,因为死区时间是相对OCxREF产生的,而非OCxN相对OCx比较来产生,或OCx相对OCxN比较来产生。
从这里开始以双路互补输出的配置为基础展开描述
1、死区时间
①死区时间的相关寄存器
②死区时间的配置与代码实现
给死区寄存器UTG(7:0)赋值158,具体计算出来的死区时间是多少请看下面解析。
void MX_TIM1_Init(void)
{
....
sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE;
sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE;
sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
sBreakDeadTimeConfig.DeadTime = 158;//死区时间只需要配置这里,给寄存器赋值158
sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE;
sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_LOW;
sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE;
if (HAL_TIMEx_ConfigBreakDeadTime(&htim1, &sBreakDeadTimeConfig) != HAL_OK)
{
Error_Handler();
}
HAL_TIM_MspPostInit(&htim1);
}
③死区时间的计算
①的死区时间的相关寄存器第一幅图中,时钟分频因子CKD决定了CK_INT分频后在死区发生器最终得到的频率,本例中CK_INT为72MHz
①的死区时间的相关寄存器第三幅图中,DTG的值决定了死区时间。
下表用十进制来表示,理解了它的配置公式后进行自主计算
他们的步长和取值范围可以用数学上解析一次函数的方法来进行分析。
④演示效果
本例设定DTG的值是158,在上表的第二个范围。
死区时间=[64+(158-128)]/36=2.61us,在该范围附近,设置正确。
2、刹车功能
①何为“刹车”?
PWM输出可以控制电机进行转动,当PWM停止输出时电机停转。
所以“刹车”,就是紧急关停PWM的输出,来应对可能烧坏电机的情况
②“刹车”功能的配置与代码实现
这里设置输入引脚给低电平时触发“刹车”
void MX_TIM1_Init(void)
{
....
sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE;
sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE;
sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
sBreakDeadTimeConfig.DeadTime = 158;
sBreakDeadTimeConfig.BreakState = TIM_BREAK_ENABLE;//使能刹车
sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_LOW;//检测到低电平时刹车
sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_ENABLE;//开启自动输出
if (HAL_TIMEx_ConfigBreakDeadTime(&htim1, &sBreakDeadTimeConfig) != HAL_OK)
{
Error_Handler();
}
HAL_TIM_MspPostInit(&htim1);
}
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(tim_baseHandle->Instance==TIM1)
{
/* TIM1 clock enable */
__HAL_RCC_TIM1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;//刹车输入引脚是浮空输入模式
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
__HAL_AFIO_REMAP_TIM1_PARTIAL();
}
}
因为“刹车”的输入引脚是浮空输入,所以它的电平状态取决于外部电路,可以通过按键等来触发它的刹车
③自动输出使能AOE对刹车功能的影响
如果开启自动输出,刹车信号来了之后,PWM信号紧急关停,刹车信号失效后,通过产生更新事件重新使能MOE,PWM的输出又被开启,就有一个自动恢复的过程。
但如果没有开启自动输出,PWM信号紧急关停后不再回复,需要修改程序软件置位MOE位才能重新开启PWM信号。
sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_ENABLE;//开启自动输出
或
sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE;//关闭自动输出
四、PWM是如何控制直流电机的?
1、PWM信号的增益
在看这里之前,首先要知道一个前提:电机转速与其输入电压有关,电机转向与输入电压极性有关;至于为什么,这里这不展开证明。
我们都知道,占空比越高,电机转速越快,但这里并不是为了将这个结论重申一遍,笔者要做的,是告诉大家这个结论是怎么来的。
但是问题也来了,PWM的输出是单片机的信号,最高电压输出值也才3V3,根本不足以驱动电机。
这时我们可以通过外部电路来对PWM进行增益,放大PWM信号的在高电平时的电压值,一般我们用TB6612FNG模块来增益。
2、复习有效值的概念
在这里强调:从理论角度,用方波的面积来看待PWM对直流电机的控制是错误的,就连野火的电机教程也犯了这个小错误。有过高数基础的都知道积分可以表示面积,这种情况一般是函数方程与微分dt相乘,假如这个函数就是我们的方波U(t),那么积分U(t)*dt,就是电压乘以时间的累加,我们也学过物理,电压乘以时间不表示任何物理意义,所以这种用方波面积来看PWM对直流电机的控制是错误的!
如果一个周期变化的电压与某个电压的直流电在这个周期产生的热效应(能量)相等,那么就可以认为该直流电的电压就是这个变化电压的有效值,并且这个有效值存在通式。
显而易见,有效值的推算是用能量守恒的思想去进行的。
3、从课本典例感悟有效值的推导
正弦电压是呈周期性变化的,它的变化周期是T,假设存在这样的一个直流电,在T时间内电路消耗的能量与正弦电压源所在回路消耗的能量相等。
对于规律变化的电压源,在一个周期内,电阻消耗能量,而电抗总体不耗能,所以计算有效值时,只需要考虑纯电阻电路。
计算过程:
令T=Π和令T=2Π还是得到同样的结果。
4、推导PWM信号增益后电压的有效值
核心思想是一个周期内能量守恒;PWM信号的波形可以看作是一个周期函数,我们把它的一个周期剥离出来,写一个分段函数,用积分来求各自功率在时间上的累加得到怎样的值,来进行对比:
显然在一个周期内,峰值都是U直的直流电和方波在有效值上不等。
用U代表方波的有效值,来进行计算。
5、方波的占空比与电压有效值的关系
若方波低电平期间电压值为0,有效值与开根占空比+高电平电压值成正比,占空比越高,有效值越大