这篇主要介绍如何实时调节PWM的频率和占空比以及直接捕获PWM测量频率。
1.直接捕获测量
在上一片讲过测量原理了。直接贴图上代码
这次使用TIM3的CH2通道(PA11)直接捕获,选择上升沿捕获,我也尝试了TIM16的直接捕获,似乎不好用,非常不准。
if(htim->Instance == TIM3)
{
if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2)
{
TIM3_IC_period_count= HAL_TIM_ReadCapturedValue(&htim3,TIM_CHANNEL_2);
TIM3_IC_period=1000000/TIM3_IC_period_count;
}
}
2.PWM占空比不变频率梯度变化
在这之前,明确一件事情,在保持预分频系数不变的情况下,PWM的频率由ARR寄存器的值决定的。占空比则由各通道的捕获比较寄存器决定的(CCR1……4)。
然后呢,其实HAL库有自己的改变频率和设置占空比的函数,就是下图,但是吧,我调用不了,他一直显示未定义,网上的针对F1的办法不适用,我也没具体深究咋回事,反正是对寄存器操作,自己直接写就行了。
上图文件可以通过全局直接搜索:__HAL_TIM_GetAutoreload 找到。也可以:打开任意一个HAL库文件的.h文件找到:"stm32g4xx_hal_def.h",在这个头文件里有#include "Legacy/stm32_hal_legacy.h",这样就找到了。
时钟寄存器的HAL代码:stm32g431xx.h
typedef struct
{
__IO uint32_t CR1; /*!< TIM control register 1, Address offset: 0x00 */
__IO uint32_t CR2; /*!< TIM control register 2, Address offset: 0x04 */
__IO uint32_t SMCR; /*!< TIM slave mode control register, Address offset: 0x08 */
__IO uint32_t DIER; /*!< TIM DMA/interrupt enable register, Address offset: 0x0C */
__IO uint32_t SR; /*!< TIM status register, Address offset: 0x10 */
__IO uint32_t EGR; /*!< TIM event generation register, Address offset: 0x14 */
__IO uint32_t CCMR1; /*!< TIM capture/compare mode register 1, Address offset: 0x18 */
__IO uint32_t CCMR2; /*!< TIM capture/compare mode register 2, Address offset: 0x1C */
__IO uint32_t CCER; /*!< TIM capture/compare enable register, Address offset: 0x20 */
__IO uint32_t CNT; /*!< TIM counter register, Address offset: 0x24 */
__IO uint32_t PSC; /*!< TIM prescaler, Address offset: 0x28 */
__IO uint32_t ARR; /*!< TIM auto-reload register, Address offset: 0x2C */
__IO uint32_t RCR; /*!< TIM repetition counter register, Address offset: 0x30 */
__IO uint32_t CCR1; /*!< TIM capture/compare register 1, Address offset: 0x34 */
__IO uint32_t CCR2; /*!< TIM capture/compare register 2, Address offset: 0x38 */
__IO uint32_t CCR3; /*!< TIM capture/compare register 3, Address offset: 0x3C */
__IO uint32_t CCR4; /*!< TIM capture/compare register 4, Address offset: 0x40 */
__IO uint32_t BDTR; /*!< TIM break and dead-time register, Address offset: 0x44 */
__IO uint32_t CCR5; /*!< TIM capture/compare register 5, Address offset: 0x48 */
__IO uint32_t CCR6; /*!< TIM capture/compare register 6, Address offset: 0x4C */
__IO uint32_t CCMR3; /*!< TIM capture/compare mode register 3, Address offset: 0x50 */
__IO uint32_t DTR2; /*!< TIM deadtime register 2, Address offset: 0x54 */
__IO uint32_t ECR; /*!< TIM encoder control register, Address offset: 0x58 */
__IO uint32_t TISEL; /*!< TIM Input Selection register, Address offset: 0x5C */
__IO uint32_t AF1; /*!< TIM alternate function option register 1, Address offset: 0x60 */
__IO uint32_t AF2; /*!< TIM alternate function option register 2, Address offset: 0x64 */
__IO uint32_t OR ; /*!< TIM option register, Address offset: 0x68 */
uint32_t RESERVED0[220];/*!< Reserved, Address offset: 0x6C */
__IO uint32_t DCR; /*!< TIM DMA control register, Address offset: 0x3DC */
__IO uint32_t DMAR; /*!< TIM DMA address for full transfer, Address offset: 0x3E0 */
} TIM_TypeDef;
先写一个设置频率和占空比的函数吧
void HAL_TIM_SetAutoreload(TIM_HandleTypeDef *htim, uint32_t autoreload) {
htim->Instance->ARR = autoreload;
htim->Init.Period = autoreload;
}
void HAL_TIM_SetCompare(TIM_HandleTypeDef *htim,uint32_t Channel,uint32_t compare)
{
switch(Channel)
{
case TIM_CHANNEL_1:
htim->Instance->CCR1=compare;
break;
case TIM_CHANNEL_2:
htim->Instance->CCR2=compare;
break;
case TIM_CHANNEL_3:
htim->Instance->CCR3=compare;
break;
case TIM_CHANNEL_4:
htim->Instance->CCR4=compare;
break;
default:break;
};
}
uint32_t HAL_TIM_GetAutoreload(TIM_HandleTypeDef *htim)
{
return htim->Instance->ARR;
}
htim->Init.Period = autoreload;这句其实用在时钟的初始化函数里,在那里这个量的设定其实是为了检测重装载值设定的符不符合寄存器范围,似乎没用?这里写其实是同步的意思,改了ARR,就也把它改一下吧,保持同步。占空比实际也简单直接改CCRx的寄存器的值。
然后就是核心功能梯度改变频率了,14届考过,下面简单实现一下,初始4000HZ,目标8000HZ,要求5s内完成升或者降(在我的示例里就按下B4上升,5s后再按下降。5s内不许按且占空比不变),步进频率不大于200,那就选个100吧,好算。跨度4000,步进频率100,那就是说要改变40次频率,5s内就需要没125ms改一次频率。
先通过函数获得当前的ARR值,计算上一次的频率,将频率加上步进频率100获得本次频率,使用1000000除这个新频率获得新频率的ARR值,然后更改。
currentFrequency_count=1000000/(1000000/HAL_TIM_GetAutoreload(&htim2)+100);
currentFrequency=1000000/currentFrequency_count;
HAL_TIM_SetAutoreload(&htim2,currentFrequency_count);
改变了频率,要想保持占空比不变,也要相应修正占空比,原来的占空比是70%,那就将现在的ARR×0.7设置为新的比较值(CCRX)即可。
HAL_TIM_SetCompare(&htim2,TIM_CHANNEL_2,currentFrequency_count*0.7);
对于增加频率,当现在频率大于等于目标频率就可以停止定时器了,因为计算除法时并不都是整除,会有误差,所以实际显示的频率有可能有几十HZ的误差。
if(currentFrequency>=targetFrequency)
{
HAL_TIM_Base_Stop_IT(&htim1);
lock=1;
}
lock就是KEY的锁,计时器不停,按键功能不开,这就保证了5s内不可按的功能。
3.互补输出与死区
3.1理论
互补输出通常用于驱动功率半桥、全桥或其他形式的功率驱动电路。在这种情况下,两个输出信号被认为是互补的,它们的电平在一个输出上上升时,在另一个输出上下降,并且它们之间存在一定的延迟,这个延迟称为死区。在功率转换器中,互补输出的死区可以防止上下半桥之间的 MOSFET 或 IGBT 同时导通。如果没有死区,上下半桥之间的交叉导通会导致短路。
上图中圈出的tim_oc1ref(tim_ocxref)是一个参考信号,PWM等输出信号参考它而来。经过死区寄存器后分两路输出,这两路输出是互补的CH1高电,那CH1N就是低电。
我们逐步讲解上图。
首先看①左侧部分,这部分是通过计数器比较和一些寄存器配置和一些信号来生成一个参考信号(tim_oc1ref,下文均称参考信号),PWM是参考这个输出的,我们这次讲的互补输出也是参照这个信号生成的。
-
tim_ocref_clr_int 输入信号: 如果定时器配置了
tim_ocref_clr_int
输入信号,当该输入信号上出现高电平时,TIM_OC1REF
信号会被清除。 -
tim_etrf 输入信号: 如果定时器配置了
tim_etrf
输入信号,同样地,当tim_etrf
输入信号上出现高电平时,TIM_OC1REF
信号也会被清除。
这两个信号通常由外部电路提供,可以连接到 MCU 的某个 GPIO 引脚,或者直接连接到定时器模块的某个输入引脚上。用于及时清除 TIM_OC1REF
信号,以控制 PWM 输出的状态。通过配置这些输入信号,可以在需要时对 PWM 输出进行及时的清除操作,以满足特定的应用需求。
这两个位是干什么的呢
翻译一下,这个位为零参考信号不受影响,位为1,上面刚说的两个信号有一个为高就清空参考信号,这样PWM就没了。对了上面提的两个信号在这里有一个会被选择为tim_ocref_clr_int 信号,怎么选的下面会讲。
这个位上一篇文章有提到就是配置输出模式的。
这个OCCS实际就是选哪个信号作为tim_ocref_clr_int 的。这个信号会去控制清不清除参考信号
接下来就到了配置死区时间这里。图中只给了一个寄存器TIMx_BDTR,实际上死区设置还分为对称和不对称,对称就只需要配置TIMx_BDTR的DTG即可。而非对称还有一个寄存器要配置TIMx_DTR2寄存器的DTGF。这点在下图有很好的体现:
观察图片可以看出两个波形的死区位置并不相同。看下图解释:
这个输出波形什么样,怎样配置寄存器后面讲。
先来看看DTG和DTGF:
DTGF所配置的死区时间在配置时以及在cubemx的配置里有一个名字叫下降死区时间,这个下降是对于参考信号而言,观察上面非对称死区时间的参考信号和OCN信号就能看出来。
如上面所说,如果我们选择对称的死区时间就不需要使能不对称死区时间,这时只需要填写Dead Time即可,他会配置DTG的时间。
下面这张图来自于HAL库配置Dead Time的函数:
下面这张是配置Falling Dead Time:
接下来我们看一下DTG再寄存器里的描述:
那这块开发板的的怎么算,首先死区时间计算使用的时钟不是分频后的,使用的是系统时钟,那我们的,所以死区时间就是我们设置的Dead Time(Falling time)的数值×12.5ns,得到的结果就是死区时间,那为什么使用第一条公式算?看下面:
在配置死区时间的函数:
HAL_StatusTypeDef HAL_TIMEx_ConfigBreakDeadTime(TIM_HandleTypeDef *htim, TIM_BreakDeadTimeConfigTypeDef *sBreakDeadTimeConfig)
有这样一句代码:
MODIFY_REG(tmpbdtr, TIM_BDTR_DTG, sBreakDeadTimeConfig->DeadTime);
tmpbdtr是寄存器地址。函数给的是0.
它的实现涉及这样3句代码:
#define WRITE_REG(REG, VAL) ((REG) = (VAL))
#define READ_REG(REG) ((REG))
#define MODIFY_REG(REG, CLEARMASK, SETMASK) WRITE_REG((REG), (((READ_REG(REG)) & (~(CLEARMASK))) | (SETMASK)))
第三句什么意思:清除TIMx_DTR2寄存器的低八位(为什么看下面的TIM_BDTR_DTG定义),并保留其他位,然后将新的死区时间(SETMASK)写入低八位,然后将这个新的寄存器值写入寄存器。我们配置Dead Time为计数100(十六进制为64)。
先给出关于TIM_BDTR_DTG的定义:
#define TIM_BDTR_DTG_Pos (0U)
#define TIM_BDTR_DTG_Msk (0xFFUL << TIM_BDTR_DTG_Pos) /*!< 0x000000FF */
#define TIM_BDTR_DTG TIM_BDTR_DTG_Msk /*!<DTG[0:7] bits (Dead-Time Generator set-up) */
#define TIM_BDTR_DTG_0 (0x01UL << TIM_BDTR_DTG_Pos) /*!< 0x00000001 */
那新的寄存器值等于多少呢(0&~(0x000000FF)|64)=0xFFFFFF64
最后八位用二进制表示:01100100,所以使用的第一句计算。这里只是解释学习一下代码,实际使用cubemx直接写就行了(只能写0~255,8位嘛)。
对于CC1E和CC1NE,是用来开启通道输出的:
CC1P和CC1NP是用来配置输出极性的:
配置为0,就是参考信号下降,对应的OC1就下降(OC1N则在经历死区后上升),反之参考信号上升,对应OC1下降(OC1N上升)。
其他几位MOE、OSSI、OSSR还有其他相关位的寄存器描述如下:
MOE
OSSI OSSR
然后具体会输出什么参考下表(不同配置下不同的输出):可参考B站UP的视频:
【【2023年】STM32 HAL库精讲 手把手写程序 入门教程_——持续更新中】 https://www.bilibili.com/video/BV1kL411Y7Uf/?p=75&share_source=copy_web&vd_source=ea95b73b3ed6cfd5784fe3d81ff94a16
3.2Cubemx的配置