stm32用定时器通过驱动器控制多个步进电机
摘要如果使用驱动器如TB6600、DM542等驱动步进电机,需要通过PWM控制。每个PWM脉冲将驱动步进电机转过一个步距角,因此想要使步进电机转过指定角度需要输出指定的PWM波形。stm32的每个定时器具有4个输出通道,本文讲如何通过一个定时器的4个输出通道控制4个步进电机并使它们能够各自实现独立转动。PWM波形用50%占空比即可,但难点在于,一是输出指定个数脉冲后停止输出,收到指令后还能继续输出
摘要
如果使用驱动器如TB6600、DM542等驱动步进电机,需要通过PWM控制。每个PWM脉冲将驱动步进电机转过一个步距角,因此想要使步进电机转过指定角度需要输出指定的PWM波形。对多个步进电机调速的不同需求对应不同的方法。
方法一是通过一个定时器的4个输出通道控制4个步进电机,适用于多个电机不需要异步调速的场合,也就是说多个电机转速相同,但可单独控制单个电机的启停。
方法二是在定时中断里翻转IO,适用于多个电机需要异步调速且计算量小或精度要求不高的场合。
方法三是每个电机占用一个定时器,适用于多个电机需要异步调速且计算量大或精度要求高的场合。如果电机过多则需要单独添加扩展定时器电路,比如74系列芯片等。
优点 | 缺点 | |
---|---|---|
方法一 | 可同时控制大量电机,或在相同电机下节省定时器资源 | 每个定时器对应的4个电机转速相同不可单独调整 |
方法二 | 既可同时控制大量电机,也可单独调整每个电机的转速 | 调速精度低,且精度越高越占用CPU资源 |
方法三 | 可单独调整每个电机的转速且精度可以很高 | 浪费定时器资源 |
PWM波形用50%占空比即可,但难点在于,一是输出指定个数脉冲后停止输出,收到指令后还能继续输出指定个数脉冲,二是能够根据指令修改PWM频率并保持占空比不变。
本文中的部分图片来自官网手册 RM0008 reference manural。
方法一:单通道输出指定个数脉冲
stm32的每个定时器具有4个输出通道,方法一讲如何通过一个定时器的4个输出通道控制4个步进电机并使它们能够各自独立工作。
单脉冲模式
单脉冲模式的具体细节可参考其它博客。经过仔细研究后我发现该模式不合适,因为这一模式的功能是在输出一个完整的脉冲后计数器停止计数,这不是我想要的。
关闭通道输出
这一思路是使输出通道保持关闭,收到指定脉冲后打开通道输出,脉冲结束后关闭。如图所示,开闭输出通道可通过TIMx_CCER寄存器的CCyE标志位修改。
通道的库函数配置
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Disable;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
但如果只是简单的这么做的话会有点小问题,比如下图中的毛刺。(详情搜索keil软件仿真逻辑分析仪)
或修改成
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_Low;
结果是
精确设计波形
PWM有两种输出模式1和2,分别为先高后低和先低后高。因为关闭通道输出后的输出电平为低,所以应选择先低后高的PWM模式2,配置代码为
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM2;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Disable;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
但是根据图127及TIMx_CCER寄存器的解释,CCyP标志位设置低电平激活还是高电平激活,默认高电平,也就是相当于取反,理论上说PWM模式1加上低电平激活的效果是一样的,但结果是第一个脉冲的高电平持续了一整个周期,我也没想明白。
最后一个脉冲的毛刺也有问题,当一个周期结束后紧跟着的下一个周期先输出高电平,而进入中断并关闭输出需要时间,这就导致刚输出高电平就被关闭,造成毛刺,这不难理解,但数一下脉冲数发现第一个脉冲不见了,这也很奇怪,我还是没想明白。
PWM调频
调频的时候有个非常重要的地方是修改了寄存器后寄存器的值会不会立即生效。stm32中有个影子寄存器专门解决这个问题。需要改的寄存器是周期和占空比,这只需要
TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
TIM_OC2PreloadConfig(TIM3, TIM_OCPreload_Enable);
TIM_OC3PreloadConfig(TIM3, TIM_OCPreload_Enable);
TIM_OC4PreloadConfig(TIM3, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM3, ENABLE);
即可。
我后来发现这种做法有个很大的缺点就是这样做只能使4个电机的速度相同,所以我弃用了这部分代码,改成了普通的定时翻转io。
方法二:定时翻转io方法
配置定时器2生成100kHz进行基本计数。由于大部分步进电机驱动器都要求高电平脉宽不低于1.2us,所以100kHz比较合适。配置代码如下:
(注:将如下代码中的TIM2进行查找替换全换成TIM6或TIM7的话基本定时器无法正常工作,原因未知。当然也可使用systick定时器,如果定时器资源紧张的话)
void TIM2_Config(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
TIM_TimeBaseStructure.TIM_Period=9; //100kHz
TIM_TimeBaseStructure.TIM_Prescaler=71;
TIM_TimeBaseStructure.TIM_ClockDivision=TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode=TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseStructure);
TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);
NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=3;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_ARRPreloadConfig(TIM2,ENABLE);
TIM_Cmd(TIM2,ENABLE);
}
GPIO配置代码如下:
void GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
GPIO_InitStructure.GPIO_Pin=0x003F; //0,1,2,3,4,5
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin=0xC00F; //0,1,2,3,14,15
GPIO_Init(GPIOC,&GPIO_InitStructure);
}
定时器中断代码如下:
hort MotorBase[6]; //6个步进电机当前相对于原点的脉冲数
short HalfWidth[6]; //6个步进电机PWM的半周期*10us
short dir[6]; //6个步进电机的目标方向
u16 TimerCnt=0;
void TIM2_IRQHandler(void)
{
char i=TIM2->SR&0x01;
TIM2->SR=0;
if(!i) return;
for(i=0;i<6;++i){
if(!dir[i]) continue;
if((TimerCnt%(HalfWidth[i]+HalfWidth[i]))==0){
Motor_PWM_Output(i,1);
MotorBase[i]+=dir[i];
}
else if((TimerCnt%HalfWidth[i])==0)
Motor_PWM_Output(i,0);
}
TimerCnt++;
if(TimerCnt>=40000)
TimerCnt=0;
}
大概思路是通过串口将目标脉冲数发送给单片机,单片机根据目标脉冲数和步进电机当前的脉冲数生成有限个脉冲及方向输出。
方法三:定时器与电机一一对应
一个定时器驱动一个电机,我要用6个电机,使用的定时器是TIM2-7。电机驱动器使用公阳极接法,意思是在没有信号时保持高电平,下降沿为一个脉冲。实现这一方法的大致思路是,设置定时器时长为半个脉冲周期,也就是每半个脉冲周期结束后设置下一次的定时器时长和输出电平等。
在定时器初始化时就进入一次中断(解决STM32开启定时器时立即进入一次中断程序问题),但此时并不打开定时器,收到工作指令后再开启。中断服务函数如下
void TIM2_IRQHandler(void)
{
s16 TIM_Period;
if (!(TIM2->SR & TIM_IT_Update)) return;
if (!PWM_Generate(0, &TIM_Period))
TIM2->CCR1 &=~ TIM_CR1_CEN;
TIM2->ARR = TIM_Period;
TIM2->SR = ~TIM_IT_Update;
}
电机到达指定角度后要在中断服务函数中关闭定时器,PWM_Generate()
函数负责生成脉冲信号并返回是否需要关闭定时器。
s16 PWM_Generate(int n, s16 *period)
{
static u8 isHigh=0xFF;
if (isHigh & (1<<n)){
MOTOR_PWM_OUTPUT_HIGH(n);
if(dir[n]==-1) MOTOR_DIR_OUTPUT_LOW(n);
else MOTOR_DIR_OUTPUT_HIGH(n);
PulseBase[n]+=dir[n];
}
else
MOTOR_PWM_OUTPUT_LOW(n);
*period = HalfWidth[n];
isHigh ^= (1<<n);
return dir[n];
}
其中n
为电机序号(0~5
),`dir·表示各个电机的转动方向,1,0,-1分别表示正传、停转、反转。period表示该脉冲结束后下一个脉冲的长度。
另外还有细节要注意,方向信号应该提前于脉冲信号准备好,像这样
而不是这样
下面这个图是把方向再变回来的情况。另外脉冲信号的下降沿时方向信号应保持稳定,上升信号时不影响。
论文
我把这种控制方法发表了论文
张航宁,李文新,梁绪. 基于跟踪微分器的步进电机自适应速度曲线设计[J]. 空间控制技术与应用,2022,48(2):88-95. DOI:10.3969/j.issn.1674-1579.2022.02.011.
@article{article,
author={张航宁 and 李文新 and 梁绪},
title={基于跟踪微分器的步进电机自适应速度曲线设计},
organization={兰州空间技术物理研究所},
journal={空间控制技术与应用},
year={2022},
volume={48},
number={2},
pages={88-95},
month={4},
}
论文解读见 基于跟踪微分器的步进电机自适应速度曲线设计
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)