“乱造轮子“刷不动屏的STM32F103C8T6驱动57步进电机

前言

  闲来无事重新造了一遍步进电机的"轮子",发现还是那么艰难,本人能力有限,实现方案粗糙,希望给位看客就当图一乐,不要因为浪费了人生中的几分钟而生气,工程文件塞在最下面了:)

一、如何计算特定转速下步进电机需要的脉冲的频率?

1.细分数与步进电机可控转角的关系:

  市面上常见的步进有42,57步进电机,步进电机主要应用与对扭矩要求较大,但是对于转速要求不是太高的场合。
  以57步进电机为例子,市面上常见的57步进电机都会自带20细分,那么问题来了,什么叫细分数?说的简单一点就是把一圈分成了特定的份数,这个份数就是细分数。所以说,一般的57步进电机是自带将一圈分成20份的能力的。每一份也就是1.8°咯。举个例子,如果我想让电机转360°,那么电机实际上是1.8,3.6 ,5.4,7.2.....360按照这样的角度运行的。
  除去57步进自带的细分数,驱动也自带有细分数,前面也提到了,每一份竟然有1.8°,如果我想让步进电机转0.1125°,这显然是不可能的实现的,所以伟大的人类发明了外接的驱动,将1.8°又划分成了N份,取N=16,这样就能实现转0.1125°(1.8/16=0.1125)了。

2.步进电机频率指定细分数与转速情况下脉冲频率的计算:

  通过前面的相关介绍现在已经知道了步进电机的单次最小可控转角为1.8/DIV(DIV就是上面所说的N),步进电机的工作原理,就是你给它一个脉冲,他就朝着你指定的方向旋转一个最小的单位转角。
  举个栗子如果你想让你的步进电机在DIV为16的情况下转速为1r/s,那就意味着1s的时间内步进电机要得到360°/(1.8°/16)=3200个脉冲即脉冲频率为3200HZ。于此给出指定转速为n,驱动细分数为DIV下的步进电机需要脉冲频率的计算公式:f=360*n*DIV/1.8=200*n*DIV
  而且可以发现,给一个脉冲步进电机就转一个最小角度,这说明步进电机的角位移只与你给他的脉冲个数有关。

3.通过stm32定时器的配置参数与步进电机所需脉冲频率的对应关系

  stm32定时器,以F1系列系统时钟默认为72MHZ为例,通过定时器产生的PWM脉冲的频率公式为f=72M/(psc+1)/(arr+1)。
  于是可以得到一个等式:f=72M/(psc+1)/(arr+1)=200*n*DIV,在n,DIV都已经知道的情况下,只要将arr,psc其中之一设为固定值,改变另外一个值即可实现转速的调节。
  再举个栗子吧:取DIV=16,固定psc=3,可以得出arr=5625/n-1

4.如何“温柔”的调速:

  测试就会发现,当你为了调节步进电机的转速而对于其脉冲频率调整的跨度非常大的时候,电机往往就会发生堵转,如:你希望脉冲频率为3200HZ,对应转速为1r/s的运行状态,一下子变成脉冲频率为32000HZ对应转速为10r/s的运行状态,你的步进电机往往会堵转,此时你只能同时复位使能端ENA端来使得电机重新工作。
  平滑的调速方法有很多,若希望对于步进电机的控制格外的准确,编码器是不可或缺的,甚至需要设置特定的启动速度,加减速曲线,然而我并没有那么专业,在查阅了一些相关的控制方法后,我用“野路子”基于T型加减速的方法勉强完成了步进电机的调速以及位移控制。T型加减速就是让电机的频率改变跨度变小,可以理解为转速的直线插值调整,比如说,希望从0r/s到0.4r/s只需要每隔一个固定的时间使转速(频率)上升一个比较小的值如0.04r/s即0,0.04,0.08,0.12...........0.4r/s,这个间隔的固定的时间是需要试凑的。
放张图吧....

在这里插入图片描述

二、步进电机以及主控芯片的选型

1.步进电机的选型:

  馬云网上买的步进电机加驱动加24V的开关电源,合起来150大洋左右。

电机的图片以及参数(现在看到1.8°是不是感觉特别亲切了呢)

在这里插入图片描述

2.主控的选型:

除了刷不了LCD啥都能干的stm32F103C8T6 (话说最近芯片是不是贵的离谱)

在这里插入图片描述

3.驱动的选型:

驱动选型:还记得上面说过的能使1.8°再被细分的伟大发明么

在这里插入图片描述

三.硬件的接线部分

驱动与步进电机接线示意图如下:
在这里插入图片描述



stm32与驱动接线如下:
PA11(TIM1CH4)------------PUL+
PB12------------ENA+(注意!低电平时使能电机)
PB13------------DIR+
(高电平时正转,怎么样算正转看你自己怎么看了)
ENA-,PUL-,DIR-全部都跟单片机的地接到一起去

四.软件的实现

  软件的实现可以分为各外设初始化,串口通讯协议,T型加减速设置,以及开环位移读取几个部分。这里大致讲一下思路:首先完成各个外设的初始化,在接受到串口发送的指令前,失能电机。通过TIM3完成T型调速所需要的时间间隔,通过TIM1产生对应频率的脉冲(每次都在TIM3的定时中断中调整arr的值以实现)。


心急的小伙伴建议直接跳到最下方,完整工程放在最下方。

1.外设的初始化:

  主要有串口,PWM,驱动对应IO的初始化

串口初始化(串口空闲中断+DMA)
/**
	*@brief USART1初始化函数:开启串口空闲中断,DMA搬运数据  
	*@param baud:与上位机通讯的波特率
	*@retval None
*/
void MY_USART_CONFIG(uint32_t baud) //串口初始化函数
{
    
    
    //配置DMA
    DMA_Config();
    //初始化时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE);
    //串口复位
    USART_DeInit(USART1);
    //使用串口1进行通信
    GPIO_InitTypeDef GPIO_InitStructure;
    USART_InitTypeDef USART_InitStructure;
    //TXD PA9配置
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    //RXD PA10配置
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
    //串口的通讯协议设置
    USART_InitStructure.USART_BaudRate = baud;                                      //波特率
    USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //有无硬件流
    USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;                 //收发模式全开
    USART_InitStructure.USART_Parity = USART_Parity_No;                             //无校验
    USART_InitStructure.USART_StopBits = USART_StopBits_1;                          //一停止位
    USART_InitStructure.USART_WordLength = USART_WordLength_8b;                     //发送字节长度
    USART_Init(USART1, &USART_InitStructure);
    //开启串口空闲接受中断
    USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);
    //开启串口DMA接收
    USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
    //开启串口时钟
    USART_Cmd(USART1, ENABLE);
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
    NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
    NVIC_Init(&NVIC_InitStructure);
}
/**
	*@brief DMA-----USART1配置函数将串口接受数据搬运至内存 
	*@param None
	*@retval None
*/
static void DMA_Config(void)
{
    
    
    DMA_InitTypeDef DMA_Initstructure;
    /*开启DMA时钟*/
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    DMA_DeInit(DMA1_Channel5);
    /*DMA配置接受配置:传输方向 USART1->DR    ---------      Receive_Buffer_USART1[DATA_LENGTH]*/
    DMA_Initstructure.DMA_PeripheralBaseAddr = (uint32_t)(&USART1->DR);
    DMA_Initstructure.DMA_MemoryBaseAddr = (uint32_t)Receive_Buffer_USART1;
    DMA_Initstructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_Initstructure.DMA_BufferSize = DATA_LENGTH;
    DMA_Initstructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_Initstructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_Initstructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_Initstructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_Initstructure.DMA_Mode = DMA_Mode_Normal;
    DMA_Initstructure.DMA_Priority = DMA_Priority_High;
    DMA_Initstructure.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel5, &DMA_Initstructure);
    DMA_Cmd(DMA1_Channel5, ENABLE);
}
/**
	*@brief USART1中断处理函数:用于重新配置DMA并对发送数据进行分析打印结果 
	*@param None
	*@retval None
*/
void USART1_IRQHandler()
{
    
    
    uint8_t length = 0;
    if (USART_GetITStatus(USART1, USART_IT_IDLE) == SET) //接收到数据
    {
    
    
        /*清除空闲中断标志(软件序列清除)*/
        length = USART1->SR;
        length = USART1->DR;
        length = DATA_LENGTH - DMA_GetCurrDataCounter(DMA1_Channel5); //实际接受的数据长度
        /*当DMA通道配置为非循环模式时,传输结束后,为了开始新的DMA传输命令,得把DMA失能,并重新设置接受数据长度*/
        DMA_Cmd(DMA1_Channel5, DISABLE);
        DMA1_Channel5->CNDTR = DATA_LENGTH; //并重新设置接受数据长度
        DMA_Cmd(DMA1_Channel5, ENABLE);     //开启DMA传输命令
        /*根据自定义通讯协议进行译码操作*/
        if (Data_Transfer(Receive_Buffer_USART1, length) == 0)
        {
    
    
            printf("数据格式错误\n");
        }
        memset(Receive_Buffer_USART1, 0, sizeof(Receive_Buffer_USART1)); //清空数组为下一次接受做好准备
    }
}

2.串口解析函数:

  上位机发送数据解析函数:那个串口调试助手照着注释发送控制命令即可StrToFloat()函数可以用stdlib.h中的atof()函数代替。此处值得一提的是,即使我们将最小调整角度已经设置成了1.8°/16,仍然无法完成对于特定角度的调节,如360.1°,因此可以朝着最接近360.1°,且可以整除1.8°/16的角度。而前文中所提的每隔一定的时间进行位移调整,实际是通过TIM3的定时中断来实现的,在不需要进行速度调整不需要进行位移调整的情况下,将TIM3失能,以提高cpu效率。在接收到指令后,再将其开启。
/**
	*@brief 根据自定义协议的译码函数
		位移控制命令:AxXXXXXXXND 转速控制命令NxXXXXXXND 
		x:代表的是被控电机的编号;
		XXXXXX可以是带符号以及小数点的字符,表示被控电机的被控参数(处理后四舍五入默认保留一位小数)
		如A1+113.25ND表示将一号电机正转113.3°
	*@param p_trans:待解析的字符串
	*@param length:待解析字符串中元素的个数
	*@retval 0:发送过来的数据格式错误  1:发送过来的数据格式正确
*/
uint8_t Data_Transfer(const char *p_trans, uint8_t length)
{
    
    
    char p[10] = {
    
    0}, motor_no = 0;
    if (p_trans[length - 2] != 'N' || p_trans[length - 1] != 'D' || (p_trans[0] != 'A' && p_trans[0] != 'N'))
    {
    
    
        return 0;
    }
    switch (p_trans[0])
    {
    
    
    case 'A':
        /*得到被控制电机的号数*/
        motor_no = p_trans[1] - 0x30 - 1;
        if (motor_no > MOTOR_NUM)
        {
    
    
            printf("Motor%d号电机不存在\n", p_trans[1] - 0x30 + 1);
            return 0;
        }
        else
        {
    
    
            printf("/********角度设置********/\n");
            strncpy(p, p_trans + 2, length - 4);
            /*设定位移值为当前位移值+在此基础上希望偏转的角度*/
            Motor.Angle_SV[motor_no] = StrToFloat(p, 0) + Motor.Angle_CV[motor_no];
            if (Motor.Angle_SV[motor_no] > Motor.Angle_CV[motor_no])
            {
    
    
                Motor.N_SV[motor_no] = 1; // 1/rs的速度进行位移调整
                Motor.Dir[motor_no] = cw;
            }
            else if (Motor.Angle_SV[motor_no] < Motor.Angle_CV[motor_no])
            {
    
    
                Motor.N_SV[motor_no] = -1; // -1/rs的速度进行位移调整
                Motor.Dir[motor_no] = ccw;
            }
            else
            {
    
    
                Motor.Dir[motor_no] = stop;
            }
            /*使能定时器3开始进行T调速*/
            TIM3_ON();
            /*开启定时器1的中断*/
            TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE);
        }
        printf("Motor[%d]'s information:\n", motor_no + 1);
        printf("当前的角度:%lf\n", Motor.Angle_CV[0]);
        printf("修正后角度:%lf°\n需要脉冲个数为%lf\n", Motor.Angle_SV[motor_no],
               ((Motor.Angle_SV[motor_no] - Motor.Angle_CV[motor_no]) * DRIVER_DIV / DEG));
        /*计算得到期望脉冲个数*/
        pulse_exp = (int64_t)((Motor.Angle_SV[motor_no] - Motor.Angle_CV[motor_no]) * DRIVER_DIV / DEG);
        break;
    case 'N':
        motor_no = p_trans[1] - 0x30 - 1;
        if (motor_no > MOTOR_NUM)
        {
    
    
            printf("Motor%d号电机不存在\n", p_trans[1] - 0x30 + 1);
            return 0;
        }
        else
        {
    
    
            printf("/********转速设置********/\n");
            strncpy(p, p_trans + 2, length - 4);
            Motor.N_SV[motor_no] = StrToFloat(p, 1);
            TIM3_ON();
            /*开启定时器1的中断*/
            TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE);
            /*突然转速切换将设定置变为当前值防止混乱*/
            Motor.Angle_SV[motor_no] = Motor.Angle_CV[motor_no];
            pulse_count = 0;
            pulse_exp = 0;
        }
        printf("Motor.N_SV[%d]:%lfr/s\n", motor_no + 1, Motor.N_SV[motor_no]);
        printf("Motor.N_CV[%d]:%lfr/s\n", motor_no + 1, Motor.N_CV[motor_no]);
        break;
    default:
        break;
    }
    return 1;
}

3.T型调速函数的实现:

  T型调速函数,放在TIM3的定时中断中实现思路如我前面所讲,通过比较设定转速以及实际转速是否相等,朝着设定转速的方向逐渐增大或者减小即可。
/**
 * @brief 设置1号步进电机转速采用T型加减速法,自带反向切换(可以通过设置步进电机转速死区达到相同效果)
 * 若需求是控制多部电机加减速可仿照该函数修改
 * @param None
 * @retval None
 * */
void T_N_Set(Motor_State *motor)
{
    
    
    uint16_t arr = 0;
    int16_t temp2 = 0, temp1 = 0;
    //printf("/**********进入调速函数**********/\n");
    temp1 = (int16_t)(motor->N_SV[0] * 100 + 0.00005);
    temp2 = (int16_t)(motor->N_CV[0] * 100 + 0.00005);
    /*反向切换*/
    if (temp1 * temp2 < 0)
    {
    
    
        motor->Dir[0] = stop;
        motor->N_CV[0] = 0;
        temp2 = 0;
        Step_Motor_EME_Stop();
        return;
    }
    /*电机停转*/
    if (temp1 == 0)
    {
    
    
        motor->Dir[0] = stop; //此时电机为0转速
        motor->N_CV[0] = 0;
        motor->N_SV[0] = 0;
        temp2 = 0;
        Step_Motor_EME_Stop();
        printf("Motor.Angle_SV[%d]:%lf°\n", 1, Motor.Angle_SV[0]);
        printf("Motor.Angle_CV[%d]:%lf°\n", 1, Motor.Angle_CV[0]);
    }
    /*更新一个周期内的转速*/
    if (temp1 > temp2) //期望转速>当前转速==>需要+0.05
    {
    
    
        motor->N_CV[0] += 0.05;
        temp2 += 5;
    }
    else if (temp1 < temp2) //期望转速<当前转速==>需要-0.05
    {
    
    
        motor->N_CV[0] -= 0.05;
        temp2 -= 5;
    }
    else //此时已经完成调速
    {
    
    
        TIM3_OFF();
        printf("完成调速\n");
        printf("Motor.N_SV[%d]:%lfr/s\n", 1, Motor.N_SV[0]);
        printf("Motor.N_CV[%d]:%lfr/s\n", 1, Motor.N_CV[0]);
        TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE);
        return;
    }
    /*根据当前转速需求设置PWM,以及电机的方向*/
    if (temp2 < 0)
    {
    
    
        motor->Dir[0] = ccw; //此时电机为负转向
        arr = (uint16_t)(5625 / (-1 * motor->N_CV[0]));
        EN1_0();  //使能电机
        DIR1_0(); //反转
    }
    else if (temp2 > 0)
    {
    
    
        motor->Dir[0] = cw; //此时电机为正转向
        arr = (uint16_t)(5625 / (motor->N_CV[0]));
        EN1_0();  //使能电机
        DIR1_1(); //正转
    }
    else
    {
    
    
        motor->Dir[0] = stop;
        EN1_1(); //失能电机
    }
    /*ARR限幅*/
    if (arr > 56250)
        TIM1->ARR = 56250; //频率限幅
    else
    {
    
    
        TIM1->ARR = arr;
    }
    TIM1->CCR4 = arr >> 1; //设置50%占空比即可
    TIM_ITConfig(TIM1, TIM_IT_Update, ENABLE);
}

4.实时角位移的实现:

  实时角位移得到以及角位移调整实现过程,最重要的就是得到进入脉冲的个数,由于缺少编码器,可以通过计算TIM1(用于产生PWM脉冲)中进入定时中断的次数来再乘以1.8/DIV来得到。因为在使能了TIM1的定时中断后,没产生了一个脉冲,相当于一次定时中断。代码太长了不贴了,估计也没多少小伙伴愿意坚持看到这里。

总结

按照协议发送指令效果如下(请无视HelloWorld,只是在测试程序是否卡死在定时器中断中):
在这里插入图片描述

工程下载链接

乱造轮子版本步进电机

猜你喜欢

转载自blog.csdn.net/weixin_43543822/article/details/113101039