研究目标
目标任务为使用TIM输出PWM来驱动步进电机(一共三路PWM),其中的难点即为在利用尽可能少的资源的情况下,精确控制PWM脉冲数。之前尝试了最基础的即使用三个TIM,每个TIM使用一个CH输出PWM,且开启每个TIM的中断,在中断内作步数的自减,减到零即停止PWM输出。这样操作,能针对精确的步数来作一些梯形加减速等算法处理,但是使用下来发现,TIM的中断占用的资源太多,且TIM中断的打断导致整个程序的时序有些混乱。
为了节省资源,首先想到的是使用DMA加TIM的方案,当时参考了这篇文章:https://www.amobbs.com/thread-5646984-1-1.html。但是在后续发现,我的步进电机的驱动器细分系数调的比较高,这样一来步数就会很大,这种方法会导致内存占用太大,详细的内容就不再介绍,因为最终也没有采用这种方法。
所以继续换一种方法,即只使用一个定时器作为MASTER TIM,利用他的三个通道的PWM,独立驱动三个SLAVE TIM。
实现过程
选取TIM2为MASTER TIM,先写出其三通道输出PWM的相关代码(使用STM32C8T6):
【T2C2-PA1-X T2C3-PA2-Y T2C4-PA3-Z】
void Motor_PWM_Init(void) //使用TIM2输出PWM T2C2-PA1-X T2C3-PA2-Y T2C4-PA3-Z
{
/*
MASTER TIM INIT BEGIN
*/
/* MASTER TIM BASE */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
TIM_TimeBaseInitTypeDef TIM_InitType;
TIM_InitType.TIM_Prescaler = PSC_DEFAULT; //72M/144
TIM_InitType.TIM_Period = ARR_DEFAULT; //10KHZ(ARR 50)
TIM_InitType.TIM_CounterMode = TIM_CounterMode_Up;
TIM_InitType.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2,&TIM_InitType);
/* MASTER PWM GPIO */ //T2C2-PA1 T2C3-PA2 T2C4-PA3
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO,ENABLE);
//直接复用即可,无需重映射
GPIO_InitTypeDef GPIO_InitType;
GPIO_InitType.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitType.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitType.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;
GPIO_Init(GPIOA,&GPIO_InitType);
GPIO_ResetBits(GPIOA,GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);
/* MASTER TIM PWM*/
TIM_OCInitTypeDef TIM_OCInitType;
TIM_OCInitType.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitType.TIM_Pulse = ARR_DEFAULT/2; //Duty 50
TIM_OCInitType.TIM_OCPolarity = TIM_OCPolarity_High; //有效电平:高电平
TIM_OCInitType.TIM_OutputState = TIM_OutputState_Enable;//比较输出使能
TIM_OCInitType.TIM_OCIdleState = TIM_OCIdleState_Reset; //空闲状态低电平
TIM_OC2Init(TIM2,&TIM_OCInitType); //T2C2
TIM_OC3Init(TIM2,&TIM_OCInitType); //T2C3
TIM_OC4Init(TIM2,&TIM_OCInitType); //T2C4
TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable);
TIM_OC3PreloadConfig(TIM2, TIM_OCPreload_Enable);
TIM_OC4PreloadConfig(TIM2, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM2, ENABLE);
//TIM_Cmd(TIM2,ENABLE);
软仿结果成功:
如此一来即可将其配置成MASTER TIM了,添加代码(该部分代码有问题,文章后续有分析,请勿直接使用):
TIM_SelectOutputTrigger(TIM2,TIM_TRGOSource_OC2Ref|TIM_TRGOSource_OC3Ref|TIM_TRGOSource_OC4Ref);
TIM_SelectMasterSlaveMode(TIM2,TIM_MasterSlaveMode_Enable);
再来配置SLAVE TIM相关(TIM1\3\4):
/*
SLAVE TIM INIT BEGIN
*/
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3|RCC_APB1Periph_TIM4,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE);
TIM_InitType.TIM_Prescaler = 0;
TIM_InitType.TIM_Period = 0;
TIM_InitType.TIM_CounterMode = TIM_CounterMode_Up;
TIM_InitType.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM1,&TIM_InitType);TIM_TimeBaseInit(TIM3,&TIM_InitType);TIM_TimeBaseInit(TIM4,&TIM_InitType);
TIM_ARRPreloadConfig(TIM1, ENABLE); TIM_ARRPreloadConfig(TIM3, ENABLE); TIM_ARRPreloadConfig(TIM4, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);
NVIC_InitTypeDef TIM_NVIC_InitType;
TIM_NVIC_InitType.NVIC_IRQChannelPreemptionPriority = 0; //确保及时关闭
TIM_NVIC_InitType.NVIC_IRQChannelSubPriority = 0;
TIM_NVIC_InitType.NVIC_IRQChannelCmd = ENABLE;
TIM_NVIC_InitType.NVIC_IRQChannel = TIM1_UP_IRQn; NVIC_Init(&TIM_NVIC_InitType);
TIM_ITConfig(TIM1,TIM_IT_Update,ENABLE); TIM_ClearITPendingBit(TIM1,TIM_IT_Update);
TIM_NVIC_InitType.NVIC_IRQChannel = TIM3_IRQn; NVIC_Init(&TIM_NVIC_InitType);
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); TIM_ClearITPendingBit(TIM3,TIM_IT_Update);
TIM_NVIC_InitType.NVIC_IRQChannel = TIM4_IRQn; NVIC_Init(&TIM_NVIC_InitType);
TIM_ITConfig(TIM4,TIM_IT_Update,ENABLE); TIM_ClearITPendingBit(TIM4,TIM_IT_Update);
/* 配置触发源 */
TIM_SelectInputTrigger(TIM1,TIM_TS_ITR1);TIM_SelectInputTrigger(TIM3,TIM_TS_ITR1);TIM_SelectInputTrigger(TIM4,TIM_TS_ITR1);
TIM_SelectSlaveMode(TIM1,TIM_SlaveMode_External1);TIM_SelectSlaveMode(TIM3,TIM_SlaveMode_External1);TIM_SelectSlaveMode(TIM4,TIM_SlaveMode_External1);
TIM_SelectMasterSlaveMode(TIM1,TIM_MasterSlaveMode_Enable);TIM_SelectMasterSlaveMode(TIM3,TIM_MasterSlaveMode_Enable);TIM_SelectMasterSlaveMode(TIM4,TIM_MasterSlaveMode_Enable);
TIM1->CNT=0;TIM3->CNT=0;TIM4->CNT=0;
/* 更新为单脉冲模式 */
TIM1->CR1 |= TIM_CR1_OPM;TIM3->CR1 |= TIM_CR1_OPM;TIM4->CR1 |= TIM_CR1_OPM;
TIM_Cmd(TIM1,ENABLE);TIM_Cmd(TIM3,ENABLE);TIM_Cmd(TIM4,ENABLE);
触发源表如下(以TIM1为例)
原理即,把从定时器的CNT的触发源选择为对应的TIM的PWM输出通道,此番一来,通过设置TIM1\3\4的ARR,当PWM脉冲数触发了CNT达到ARR的值时,就进入中断来停止对应通道的PWM输出,下面展示TIM1的中断:
#define X_NO_OUTPUT {TIM2->CCR2 = 0;}
void TIM1_UP_IRQHandler(void)
{
if(TIM_GetITStatus(TIM1,TIM_IT_Update) != RESET)
{
X_NO_OUTPUT;
TIM_ClearITPendingBit(TIM1,TIM_IT_Update);
}
}
所以设置CNT的值就是来设置对应通道的PWM的脉冲的个数。
其中注意的是,对从定时器我使用了单脉冲模式:
即CNT达到ARR值后CNT计数直接停止,即TIM的使能位CEN清零。所以在启动PWM输出时记得也把对应从定时器的计数使能:
#define X_OUTPUT {TIM2->CCR2 = ARR_DEFAULT/2; TIM1->CR1 |= TIM_CR1_CEN;}
软彷看一下效果:
结果实验成功。
就当我认为逻辑无误时,我在之后多种情况的软彷过程中发现,有时候TIM1和TIM3的CNT并不会因为TIM2 CH2 CH3在输出PWM而增加,而且当TIM1和TIM3的CNT与ARR一致时,还需要等CH4(TIM2)的上升沿到达才会触发。
这时通过观察TIM1\TIM3的CNT的变化情况,分析出的可能原因就是,实际上TIM1\3并没有被TIM2的CH2\3驱动而是被CH4驱动。这时回头看SLAVE TIM初始化的阶段,其实触发源的选择并没有具体到TIM2的哪个通道,而是只指向,连接到了TIM2。
然后又看到了TIM2初始化的过程中的这行代码:
TIM_SelectOutputTrigger(TIM2,TIM_TRGOSource_OC2Ref|TIM_TRGOSource_OC3Ref|TIM_TRGOSource_OC4Ref);
我的本意是把TIM2的OC2\3\4都作为TRGO,但是再次带着疑问翻看寄存器手册,发现CR2 MMS看样子只能选择一个通道的信号作为输出的驱动触发信号。
#define TIM_TRGOSource_OC1Ref ((uint16_t)0x0040)
#define TIM_TRGOSource_OC2Ref ((uint16_t)0x0050)
#define TIM_TRGOSource_OC3Ref ((uint16_t)0x0060)
#define TIM_TRGOSource_OC4Ref ((uint16_t)0x0070)
void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource)
{
/* Check the parameters */
assert_param(IS_TIM_LIST7_PERIPH(TIMx));
assert_param(IS_TIM_TRGO_SOURCE(TIM_TRGOSource));
/* Reset the MMS Bits */
TIMx->CR2 &= (uint16_t)~((uint16_t)TIM_CR2_MMS);
/* Select the TRGO source */
TIMx->CR2 |= TIM_TRGOSource;
}
所以我的TRGO选择的代码中,或运算后,TIM2->CR2的MMS只是被设置成了111,即OC4REF作为TRGO,所以在仿真时,TIM1\3的CNT只会在CH4的上升沿到达才会触发,所以符合实际现象,问题解决。
那么难道就不能用这种方法吗?当然不是。
根据实际控制情况来看,Z轴运动和XY运动肯定不能同时进行,所以Z轴运动时,可以将TIM2的输出的触发信号选择为CH4,且CH4开始输出PWM,此时TIM4的ARR就设置成需要的脉冲数,而TIM1\3的ARR保持为0,CH2\3不输出PWM即可。
在XY轴运动时,首先CH4不输出PWM,且TIM4的ARR设置为0。假设X轴需要的脉冲数大于Y轴需要的脉冲数,那么就把CH2作为TIM2输出的触发信号,因为若以脉冲数小的一方Y轴对应的CH3作为触发信号,当进入中断后,会把对应的PWM信号停止,那么此时X轴的CNT不再会被触发增长,即不会进入X轴对应的中断,那么X就不会及时停下。
修改代码如下:
/* 选择通道输出 */
#define X_NO_OUTPUT {TIM2->CCR2 = 0;TIM1->ARR = 0;}
#define Y_NO_OUTPUT {TIM2->CCR3 = 0;TIM3->ARR = 0;}
#define Z_NO_OUTPUT {TIM2->CCR4 = 0;TIM4->ARR = 0;}
#define X_OUTPUT {TIM2->CCR2 = ARR_DEFAULT/2; TIM1->CR1 |= TIM_CR1_CEN; Z_NO_OUTPUT;}
#define Y_OUTPUT {TIM2->CCR3 = ARR_DEFAULT/2; TIM3->CR1 |= TIM_CR1_CEN; Z_NO_OUTPUT;}
#define Z_OUTPUT {TIM2->CCR4 = ARR_DEFAULT/2; TIM4->CR1 |= TIM_CR1_CEN; X_NO_OUTPUT;Y_NO_OUTPUT;}
测试任务如下:
软彷结果为: 成功实现目标:主定时器多通道PWM独立驱动多个从定时器