现在需要实现计时,输出PWM和输入捕获。其中计时实现0.1ms加1,用于陀螺仪积分计时;输出4路PWM波频率为400Hz,用于驱动无人机电机;输入捕获频率1MHz,用于测量输出的 PWM周期、占空比等,看看PWM波是否正常。
这些功能都需要定时器,STM32F4有14个定时器TIM1~TIM14,但是不同定时器功能并不尽相同,该怎样选择(并且实现)呢?
此处主讲定时器,也在之前的程序基础上做改动。之前使用 TIM3做基本的计时,此处开始改为 TIM6。
此文对应的程序源码见 Gitee 链接:
https://gitee.com/gengstrong/UAV021/raw/master/UAV021_5-PWM_4Channel_Capture.zip
如上图所示,TIM6和TIM7为基本定时器、TIM2~TIM5,TIM9~TIM14为通用定时器,TIM1和TIM8为高级定时器。
首先,要做0.1ms基本定时,基本定时器是首选,因为只是简单定时,不涉及IO应用,基本定时器已经能够满足功能。
其次,要输出4路 400Hz的PWM波,不用互补输出功能但要输出4路,因此TIM2~TIM5 皆可。考虑到 400Hz 对应周期 2.5ms,假设将时钟源90MHz通过90分频得到1MHz的定时器,16位定时器可计数 65535个,最大周期为 65.535ms,已经远远满足 2.5ms 的需求了。因此 16 位定时器即可,此处选择 TIM3。
最后,对于输入捕获,测量高电平时间、测量周期等。假如使用1MHz定时器时钟,16位时可以计数 216= 65535us = 65ms,使用用 32位定时器 TIM5,使用1MHz 的定时器时钟,可以计数 232 us = 4294s。虽然使用16位定时器已经能满足此处PWM波的测量,但是使用32位定时器能够满足秒级别的测量,后期使用测量也可以直接使用,故采用32位的TIM5。
配置定时器实现计数功能,包括以下几个步骤:
按照以上步骤,配置如下:
/* 注意:1. 中断相关名称。注意基本定时器TIM6 的中断名为 TIM6_DAC_IRQn,< TIM6 global and DAC1&2 underrun error interrupts>,其他一些定时器为 TIMx_IRQn(x=2,3,4,7 etc) */
/* 中断函数名为 TIM6_DAC_IRQHandler(),而不是 TIM6_IRQHandler() */
/* 3. 基本定时器(TIM6, TIM7),通用定时器,高级定时器(TIM1, TIM8),功能越来越多,此处只做个简单计时,基本定时器足够了 */
#include "timer.h"
uint32_t tim = 0; // 全局变量,每0.1ms加1
TIM_HandleTypeDef TIM6_Handler; //定时器句柄
/* 基本定时器6初始化 */
/* 实现 0.1ms 计时中断 */
/* 90M/90=1M,1M/100 = 10kHz = 0.1ms */
void TIM6_Init(void)
{
TIM6_Handler.Instance = TIM6; // 基本定时器
TIM6_Handler.Init.Prescaler = 90-1; // 分频系数 90,得到 90M/90=1M 时钟
TIM6_Handler.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数器
TIM6_Handler.Init.Period = 100-1; // 自动装载值 100,每 100/1M = 100us=0.1ms中断一次
TIM6_Handler.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // 时钟分频因子
HAL_TIM_Base_Init(&TIM6_Handler);
HAL_TIM_Base_Start_IT(&TIM6_Handler); //使能定时器3和定时器3更新中断:TIM_IT_UPDATE
}
/* 定时器底册驱动,开启时钟,设置中断优先级 */
/* 此函数会被HAL_TIM_Base_Init()函数调用 */
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef *htim)
{
if(htim->Instance==TIM6)
{
__HAL_RCC_TIM6_CLK_ENABLE(); // 使能TIM6时钟,
HAL_NVIC_SetPriority(TIM6_DAC_IRQn,1,3); // 中断优先级:抢占优先级1,子优先级3。
HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn); // 开启ITM6中断
}
}
/* 定时器6中断服务函数 */
void TIM6_DAC_IRQHandler(void)
{
if(__HAL_TIM_GET_IT_SOURCE(&TIM6_Handler, TIM_IT_UPDATE) !=RESET)
{
__HAL_TIM_CLEAR_IT(&TIM6_Handler, TIM_IT_UPDATE); // 清除中断标志位
}
tim ++; // 又是0.1ms,全局时间计数加1
}
要注意中断及中断函数名称,对于TIM6为 TIM6_DAC_IRQn、TIM6_DAC_IRQHandler()。
还有分频系数与时钟分频因子,为什么有了分频系数还要分频因子呢?不是很理解,可以参考这篇文章。
与定时不同,配置输出PWM波需要配置I/O口,相应地需要配置相应通道;输出PWM波时可不配置中断。步骤如下:
此处,航模电机驱动频率选择常用的 400Hz,定时器重装值选择 5000-1。为了方便实际中调节各个电机的占空比,编写设置于获取占空比的函数,占空比调节精度为 1/1000。
对于I/O选择,使用了TIM3 CH1~CH3,对应引脚参考核心板原理图:
#define PWM_PIN GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_0 | GPIO_PIN_1
#define PWM_GPIO GPIOB
#define PWM_ENCLK() __HAL_RCC_TIM3_CLK_ENABLE(); \
__HAL_RCC_GPIOB_CLK_ENABLE(); // 使能定时器3,开启GPIOB时钟
/* 四个电机枚举 */
typedef enum
{
MOTOR1 = 0,
MOTOR2 = 1,
MOTOR3 = 2,
MOTOR4 = 3
};
/* TIM3 4路 PWM波输出初始化 */
void TIM3_PWM_Init(void)
{
TIM3_Handler.Instance = TIM3; //定时器3
TIM3_Handler.Init.Prescaler = 45-1; //定时器分频 45,得到 90M/45=2M 时钟
TIM3_Handler.Init.CounterMode = TIM_COUNTERMODE_UP; //向上计数模式
TIM3_Handler.Init.Period = 5000-1; //自动重装载值 5000,得到PWM频率 2M/5k=400Hz
TIM3_Handler.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&TIM3_Handler); //初始化PWM
TIM3_CHxHandler.OCMode = TIM_OCMODE_PWM1; //模式选择PWM1
TIM3_CHxHandler.Pulse = 0; //设置比较值,此值用来确定占空比,默认为0
TIM3_CHxHandler.OCPolarity = TIM_OCPOLARITY_HIGH; //输出比较极性为高
HAL_TIM_PWM_ConfigChannel(&TIM3_Handler,&TIM3_CHxHandler,TIM_CHANNEL_1); //配置TIM3通道1
HAL_TIM_PWM_Start(&TIM3_Handler, TIM_CHANNEL_1); //开启PWM通道1
HAL_TIM_PWM_ConfigChannel(&TIM3_Handler,&TIM3_CHxHandler,TIM_CHANNEL_2); //配置TIM3通道2
HAL_TIM_PWM_Start(&TIM3_Handler, TIM_CHANNEL_2); //开启PWM通道2
HAL_TIM_PWM_ConfigChannel(&TIM3_Handler,&TIM3_CHxHandler,TIM_CHANNEL_3); //配置TIM3通道3
HAL_TIM_PWM_Start(&TIM3_Handler, TIM_CHANNEL_3); //开启PWM通道3
HAL_TIM_PWM_ConfigChannel(&TIM3_Handler,&TIM3_CHxHandler,TIM_CHANNEL_4); //配置TIM3通道4
HAL_TIM_PWM_Start(&TIM3_Handler, TIM_CHANNEL_4); //开启PWM通道4
}
/* 定时器底层驱动,时钟使能,引脚配置 */
/* 此函数会被HAL_TIM_PWM_Init()调用 */
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
GPIO_InitTypeDef GPIO_Initure;
PWM_ENCLK();
GPIO_Initure.Pin = PWM_PIN;
GPIO_Initure.Mode = GPIO_MODE_AF_PP; // 复用推完输出
GPIO_Initure.Pull = GPIO_PULLUP; // 上拉
GPIO_Initure.Speed = GPIO_SPEED_HIGH; // 高速
GPIO_Initure.Alternate = GPIO_AF2_TIM3; // 引脚复用
HAL_GPIO_Init(PWM_GPIO, &GPIO_Initure);
}
/* 设置电机占空比 */
/* motor: 选择电机,可填MOTOR1, MOTOR2, MOTOR3, MOTOR4 */
/* duty: 占空比为千分之 duty */
void SetMotorDuty(uint8_t motor, uint16_t duty)
{
if (duty > 1000)
duty = 1000;
else if (duty < 0)
duty = 0;
if (motor == MOTOR1)
TIM3->CCR1 = duty * 5 - 1; // 由于计数器重装值ARR为 5000,精度为千分之一,故此处乘以5,减一是因为从0开始计数
else if (motor == MOTOR2)
TIM3->CCR2 = duty * 5 - 1;
else if (motor == MOTOR3)
TIM3->CCR3 = duty * 5 - 1;
else if (motor == MOTOR4)
TIM3->CCR4 = duty * 5 - 1;
}
/* 获取电机占空比 */
/* motor选择电机,可填MOTOR1, MOTOR2, MOTOR3, MOTOR4,占空比为千分之返回值 */
uint16_t GetMotorDuty(uint8_t motor)
{
uint32_t cap_value;
if (motor == MOTOR1)
cap_value = HAL_TIM_ReadCapturedValue(&TIM3_Handler,TIM_CHANNEL_1); // 读取比较寄存器的值
else if (motor == MOTOR2)
cap_value = HAL_TIM_ReadCapturedValue(&TIM3_Handler,TIM_CHANNEL_1);
else if (motor == MOTOR3)
cap_value = HAL_TIM_ReadCapturedValue(&TIM3_Handler,TIM_CHANNEL_1);
else if (motor == MOTOR4)
cap_value = HAL_TIM_ReadCapturedValue(&TIM3_Handler,TIM_CHANNEL_1);
return (cap_value+1) / 5;
}
输入捕获也需要配置对应的 I/O 口于通道,而且需要在中断服务函数里实现定时中断、高/低电平时间的测量。步骤如下:
具体程序如下:
#define CAP_PIN GPIO_PIN_0
#define CAP_GPIO GPIOA
#define CAP_ENCLK() __HAL_RCC_TIM5_CLK_ENABLE(); \
__HAL_RCC_GPIOA_CLK_ENABLE(); //使能TIM5时钟,开启GPIOA时钟
TIM_HandleTypeDef TIM5_Handler; //定时器5句柄
uint32_t cap_value1 = 0; // 检测到上升沿时的计数
uint32_t cap_value2 = 0; // 检测到下降沿时的计数, cap_value2 - cap_value1 即为正脉冲时间
uint32_t cap_value3 = 0; // 再次检测到上升沿时的计数, cap_value3 - cap_value1 即为周期
uint8_t cap_sta = 0; // 输入捕获状态, 0:没有完成捕获,去捕获边沿吧 1:捕获边沿完成,去计算周期吧
uint8_t cap_times = 0; // 捕获次数
/* 定时器5通道1输入捕获配置 */
/* 频率:90M/90/1 = 1MHz */
void TIM5_CH1_Cap_Init(void)
{
TIM_IC_InitTypeDef TIM5_CH1Config;
TIM5_Handler.Instance = TIM5; // 通用定时器5
TIM5_Handler.Init.Prescaler = 90-1; // 分频系数
TIM5_Handler.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数器
TIM5_Handler.Init.Period = 0XFFFFFFFF; // 自动装载值
TIM5_Handler.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // 时钟分频银子
HAL_TIM_IC_Init(&TIM5_Handler); // 初始化输入捕获时基参数
TIM5_CH1Config.ICPolarity = TIM_ICPOLARITY_RISING; // 上升沿捕获
TIM5_CH1Config.ICSelection = TIM_ICSELECTION_DIRECTTI; // 设置引脚与寄存器关系。CH1、CH2用TI1,CH3、CH4用TI2
TIM5_CH1Config.ICPrescaler = TIM_ICPSC_DIV1; // 配置输入分频,不分频
TIM5_CH1Config.ICFilter=0; // 配置输入滤波器,不滤波
HAL_TIM_IC_ConfigChannel(&TIM5_Handler,&TIM5_CH1Config,TIM_CHANNEL_1);//配置TIM5通道1
HAL_TIM_IC_Start_IT(&TIM5_Handler,TIM_CHANNEL_1); // 开启TIM5的捕获通道1,并且开启捕获中断
__HAL_TIM_ENABLE_IT(&TIM5_Handler,TIM_IT_UPDATE); // 使能更新中断
}
/* 定时器5底层驱动,时钟使能,引脚配置 */
/* 此函数会被HAL_TIM_IC_Init()调用 */
void HAL_TIM_IC_MspInit(TIM_HandleTypeDef *htim)
{
GPIO_InitTypeDef GPIO_Initure;
CAP_ENCLK();
GPIO_Initure.Pin = CAP_PIN; // 引脚
GPIO_Initure.Mode = GPIO_MODE_AF_PP; // 复用推挽输出
GPIO_Initure.Pull = GPIO_PULLDOWN; // 下拉
GPIO_Initure.Speed = GPIO_SPEED_HIGH; // 高速
GPIO_Initure.Alternate = GPIO_AF2_TIM5; // PA0复用为TIM5通道1
HAL_GPIO_Init(CAP_GPIO,&GPIO_Initure);
HAL_NVIC_SetPriority(TIM5_IRQn,2,0); // 设置中断优先级,抢占优先级2,子优先级0
HAL_NVIC_EnableIRQ(TIM5_IRQn); // 开启ITM5中断通道
}
/* 定时器5中断服务函数 */
void TIM5_IRQHandler(void)
{
HAL_TIM_IRQHandler(&TIM5_Handler); //定时器共用处理函数
}
/* 定时器输入捕获中断处理回调函数,该函数在HAL_TIM_IRQHandler中会被调用 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) //更新中断(溢出)发生时执行
{
if (cap_sta == 0)
{
switch (cap_times)
{
case 0: // 清空一切,准备捕获
cap_value1 = 0;
cap_value2 = 0;
cap_value3 = 0;
__HAL_TIM_DISABLE(&TIM5_Handler); //关闭定时器5
__HAL_TIM_SET_COUNTER(&TIM5_Handler,0);
TIM_RESET_CAPTUREPOLARITY(&TIM5_Handler,TIM_CHANNEL_1); //一定要先清除原来的设置!!
TIM_SET_CAPTUREPOLARITY(&TIM5_Handler,TIM_CHANNEL_1,TIM_ICPOLARITY_FALLING);//定时器5通道1设置为下降沿捕获
__HAL_TIM_ENABLE(&TIM5_Handler);
cap_times = 1;
break;
case 1: // 捕获了一次
cap_value1 = HAL_TIM_ReadCapturedValue(&TIM5_Handler,TIM_CHANNEL_1); // 读数一次
TIM_RESET_CAPTUREPOLARITY(&TIM5_Handler,TIM_CHANNEL_1); //一定要先清除原来的设置!!
TIM_SET_CAPTUREPOLARITY(&TIM5_Handler,TIM_CHANNEL_1,TIM_ICPOLARITY_RISING);//定时器5通道1设置为上升沿捕获
cap_times = 2;
break;
case 2: // 捕获了两次
cap_value2 = HAL_TIM_ReadCapturedValue(&TIM5_Handler,TIM_CHANNEL_1); // 读数一次
TIM_RESET_CAPTUREPOLARITY(&TIM5_Handler,TIM_CHANNEL_1); //一定要先清除原来的设置!!
TIM_SET_CAPTUREPOLARITY(&TIM5_Handler,TIM_CHANNEL_1,TIM_ICPOLARITY_FALLING);//定时器5通道1设置为下降沿捕获
cap_times = 3;
break;
case 3: // 捕获了三次
cap_value3 = HAL_TIM_ReadCapturedValue(&TIM5_Handler,TIM_CHANNEL_1); // 读数一次
TIM_RESET_CAPTUREPOLARITY(&TIM5_Handler,TIM_CHANNEL_1); //一定要先清除原来的设置!!
TIM_SET_CAPTUREPOLARITY(&TIM5_Handler,TIM_CHANNEL_1,TIM_ICPOLARITY_RISING); //定时器5通道1设置为上升沿捕获
cap_times = 0;
cap_sta = 1; // 所有捕获完成,可以去计算了
break;
}
}
}
输入捕获现在主要为了测试PWM波是否正常,因此将这部分内容放置在 pwm.c 里(想想在飞控里单独弄个 capture.c 总让人感觉怪怪的)。同时,编写以下测试任务,将PWM输出引脚与输入捕获引脚连接在一起,即可测试。
void TestPwmTask(void *arg)
{
const uint16_t test_postime = 100; // 正常时间为 test_postime / 1000 * 2500 = test_postime * 2.5
uint32_t pos_time; // 高电平时间
uint32_t cycle; // 周期
uint32_t freq; // 频率
float pos_duty; // 正占空比
SetMotorDuty(MOTOR1, test_postime); // 电机PWM 400Hz = 2500 us,占空比 100/1000,高电平时间 250us
SetMotorDuty(MOTOR2, test_postime);
SetMotorDuty(MOTOR3, test_postime);
SetMotorDuty(MOTOR4, test_postime);
while(1)
{
if (cap_sta == 1) //成功捕获到了一次高电平
{
pos_time = cap_value3 - cap_value2;
cycle = cap_value3 - cap_value1;
freq = (uint32_t)1000000 / cycle;
pos_duty = (float) pos_time / (float) cycle;
printf("周期:%d us\t 正脉冲宽:%d us \r\n", cycle, pos_time);
printf("频率:%d Hz\t 正占空比:%.2f %%\r\n\r\n", freq, pos_duty*100);
cap_sta = 0; // 搞完事情,开启下一次捕获
}
delay_ms(200);
}
}
用杜邦线将 PB4/PB5/PB0/PB1中的其中一个与 PA0 连接在一起,测量结果如下:
可见,确实是我们想要的400Hz,占空比也是我们测试设置的 10%(误差忽略不计)。
— 完 —