[MSPM0G3507]电机驱动

目录

前言

一、设置syscfg

二、电机调速

1.引入电机相关的参数,结构体变量等

2.控制电机的代码

总结


前言

  在今年的电赛H题中,要求使用ti的MSPM0系列开发板。此文章是基于MSPM0G3507的电机驱动及调速。使用的电机型号为MG513X,电机减速比为28。我使用的电机驱动模块为TB6612。该模块需要开发板输入两路PWM以及四路方向控制引脚。并分别输出两路控制端口到电机,从而驱动双轮车。同时电机上的编码器引脚需要接入开发板上。开发软件为CCS Theia1.4.1。

MSPM0G3507开发板 


一、设置syscfg

首先在ccs中新建工程,打开syscfg,设置PWM的相关参数。这里我使用了默认的时钟频率32MHz,设置PWM的输出频率为32kHz,这样电机基本不会产生噪声。再设置PWM的通道0和通道1为输出通道,对应开发板上的PA12,PA13两个引脚。

 只需要设置PWM的频率以及输出通道即可,占空比在实际电机调速时才会调节。

需要注意的是,在PWM Mode那一栏,如果选择了Edge-aligned Up-Counting(向上计数)那么当你设置的输出比较值越大,PWM的占空比就越大,反之,如果选择Edge-aligned Down-Counting(向下计数),设置的输出比较值越大占空比越小,这个可以看个人喜好设置。设置输出比较值的范围为0——PWM Period Count,这里是1000 。也就是说设置输出比较值为500时PWM的占空比为50%。

接下来设置电机的方向控制引脚,在GPIO中新建一个组,这里我命名为GPIO_MOTOR

 接着加入四个引脚,分别命名为AIN1,AIN2,BIN1,BIN2,对应连接TB6612模块上的引脚。

引脚复用可以自由选择,这里我选择了PA26,PB24,PB9,PA27,分别对应 AIN1,AIN2,BIN1,BIN2这四个GPIO口。

由于ti的MSPM0G3507开发板上只有一个编码器输入端口(QEI),而我们需要驱动的电机有两个,因此我们需要使用定时器的输入捕获中断来模拟编码器。

我们知道,电机的编码器分为A相与B相。两相输出的PWM相位差为90°。这里我们只需要捕获A相的上升沿,在中断中判断B相的电平,即可实现编码器测速。但使用输入捕获模拟编码器测速的缺点也很明显,在编码器模式中,我们可以两相都设置为多边沿捕获,从而实现4倍频测速,这样测出的电机速度会更精确。而使用输入捕获只能设置触发单边沿的中断,也就是1倍频,精度会稍低,但也足够使用了。

这里我创建了两个输入捕获的通道,分别为两个电机的编码器A相 

这里设置成多少都无所谓,但是一定要给一个值,否则不能工作

 

 在捕获选项这里,选择下降计数,上升沿捕获

 在中断选项里,选择对应的通道,开启中断,中断的优先级可以不设置。

回到GPIO的选项中,新建一个组,命名为GPIO_ENCODER

接着加入两个GPIO口,我这里分别命名为ENCODER1B和ENCODER2B,与之前的引脚相对应,需要注意的是ENCODER1A与ENCODER1B对应的是同一个电机的编码器AB相,第二个电机同理。 GPIO的Direction设置为Input(输入) ,引脚可以自由选择,我这里分别选择了PB7,PB0

最后,我们还需要在TIMER中打开一个定时器,使得测速,计算PID,设置电机的速度均在同一频率下。由于我们是裸机开发,因此这种按照频率工作的效果,相比放入主循环轮询会更高效。

 我将这个定时器的名称命名为CLOCK,设置成100Hz下降计数

 在中断中设置成0事件触发,每次当计时器的值为0时就会进入CLOCK定时器的中断。

二、电机调速

        这里建议大家可以新建一个motor.c,motor.h文件,将代码写在里面,再到主循环中调用已经封装好的接口函数。

1.引入电机相关的参数,结构体变量等

#ifndef __MOTOR_H_
#define __MOTOR_H_
#include "stdlib.h"
#include "ti_msp_dl_config.h"

#define MOTOR_SPEED_RERATIO 28    //电机减速比
#define PULSE_PRE_ROUND 13       //每圈脉冲数
#define ENCODER_MULTIPLE 1.0     //编码器倍频
#define PULSE_PER_CYCLE  (MOTOR_SPEED_RERATIO*PULSE_PRE_ROUND*ENCODER_MULTIPLE)//每圈脉冲数
#define RADIUS_OF_TYRE 3.3  //轮子直径 cm
#define LINE_SPEED_C  RADIUS_OF_TYRE * 2 * 3.14  //轮子周长 cm
#define SPEED_RECORD_NUM 20  //速度记录值,用于均值滤波

首先在 motor.h中,我们宏定义一些和电机有关的变量

然后,分别创建与电机有关的结构体,这里我创建了三个结构体,包括编码器,PID,电机。

typedef struct {
uint8_t dierct;     //方向
int32_t countnum;   //总计数值
int32_t lastcount;  //上一次计数值
float speed;        //电机速度
float speed_Record[SPEED_RECORD_NUM];  //记录电机近20次测速的结果
}  encoder_t;

 由于int32_t的范围是-2,147,483,647到2,147,483,647,因此不必担心countnum会溢出的情况。

typedef  struct{
float kp,ki,kd;     //pid参数
float err,last_err; //误差,上一次误差
float integral,max_integral; //积分值,积分限幅
float output,maxoutput; //输出,输出限幅
float dead_zone; //pid死区
} pid_t ;

PID结构体较为常规,这里不赘述

typedef struct {
uint8_t dir;          //方向
uint8_t timer_index;  //PWM定时器通道
int32_t pos_pulse;    //位置环目标脉冲
int32_t forward_GPIO_PIN; //前进GPIO引脚
int32_t reverse_GPIO_PIN;  //后退GPIO引脚
float pwmDuty; //PWM占空比
float speed;   //目标速度
float pos ;   //目标位置
encoder_t encoder; //编码器
pid_t pos_pid;    //速度环pid
pid_t speed_pid; //位置环pid
GPIO_Regs *forward_GPIO_PORT;  //前进GPIO端口
GPIO_Regs *reverse_GPIO_PORT;  / 后退GPIO端口
} motor_t;

接下来是电机结构体的创建,这里稍微注意PWM通道,GPIO引脚,以及GPIO端口需的类型。

2.控制电机的代码

回到motor.c中,开始书写有关电机驱动的代码,这里记得要引用motor.h以及相关的头文件。

首先是Motor_Init()函数,在进入主循环之前需要先调用,以便对电机进行初始化。这里我创建了两个电机,分别为左电机,右电机

 motor_t motor_l;
 motor_t motor_r;
void motor_Init(){
DL_TimerG_startCounter(PWM_0_INST);  //pwm初始化
/*使能编码器输入捕获中断*/
NVIC_EnableIRQ(ENCODER1A_INST_INT_IRQN);
DL_TimerA_startCounter(ENCODER1A_INST);
NVIC_EnableIRQ(ENCODER2A_INST_INT_IRQN);
DL_TimerG_startCounter(ENCODER2A_INST);
/*使能时钟定时器中断*/
NVIC_EnableIRQ(CLOCK_INST_INT_IRQN);
DL_TimerA_startCounter(CLOCK_INST);
/*motor_l init*/
motor_l.pwmDuty=0;
motor_l.speed=0;
motor_l.timer_index=DL_TIMER_CC_0_INDEX;
motor_l.forward_GPIO_PORT=GPIO_MOTOR_AIN1_PORT;
motor_l.reverse_GPIO_PORT=GPIO_MOTOR_AIN2_PORT;
motor_l.forward_GPIO_PIN=GPIO_MOTOR_AIN1_PIN;
motor_l.reverse_GPIO_PIN=GPIO_MOTOR_AIN2_PIN;
motor_l.speed_pid.kp=0;
motor_l.speed_pid.ki=0;
motor_l.speed_pid.kd=0;
motor_l.speed_pid.max_integral=1000;
motor_l.speed_pid.maxoutput=1000;
motor_l.speed_pid.dead_zone=0;
motor_l.pos_pid.kp=0;
motor_l.pos_pid.ki=0;
motor_l.pos_pid.kd=0;
motor_l.pos_pid.max_integral=1000;
motor_l.pos_pid.maxoutput=1000;
motor_l.pos_pid.dead_zone=0;
/*motor_r init*/
motor_r.pwmDuty=0;
motor_r.speed=0;
motor_r.timer_index=DL_TIMER_CC_1_INDEX;
motor_r.forward_GPIO_PORT=GPIO_MOTOR_BIN1_PORT;
motor_r.reverse_GPIO_PORT=GPIO_MOTOR_BIN2_PORT;
motor_r.forward_GPIO_PIN=GPIO_MOTOR_BIN1_PIN;
motor_r.reverse_GPIO_PIN=GPIO_MOTOR_BIN2_PIN;
motor_r.speed_pid.kp=0;
motor_r.speed_pid.ki=0;
motor_r.speed_pid.kd=0;
motor_r.speed_pid.max_integral=1000;
motor_r.speed_pid.maxoutput=1000;
motor_r.speed_pid.dead_zone=0;
motor_r.pos_pid.kp=0;
motor_r.pos_pid.ki=0;
motor_r.pos_pid.kd=0;
motor_r.pos_pid.max_integral=1000;
motor_r.pos_pid.maxoutput=1000;
motor_r.pos_pid.dead_zone=0;
}

在初始化函数中对各个参数进行赋值,当然也可以在创建结构体时就赋予初值,效果是相同的。


首先来写编码器以及时钟中断的代码,由于这部分中断函数是单片机在中断中默认调用的,因此不需要在motor.h中声明。

/*左电机的编码器测速*/
void TIMA0_IRQHandler(void) {
  switch (DL_TimerA_getPendingInterrupt(ENCODER1A_INST)) {
  case DL_TIMERA_IIDX_CC0_DN:
   motor_l.encoder.dierct = DL_GPIO_readPins(GPIO_ENCODER_PORT, GPIO_ENCODER_ENCODER1B_PIN);//读取IO电平获取电机旋转方向
   motor_l.encoder.countnum=
       motor_l.encoder.dierct? (motor_l.encoder.countnum + 1) : (motor_l.encoder.countnum - 1);//通过判断旋转方向来决定countnum增加还是减少
    break;
  default:
    break;
  }
}

/*右电机的编码器测速*/
void TIMG6_IRQHandler(void) {
  switch (DL_TimerG_getPendingInterrupt(ENCODER2A_INST)) {
  case DL_TIMERG_IIDX_CC0_DN:
    motor_r.encoder.dierct= DL_GPIO_readPins(GPIO_ENCODER_PORT, GPIO_ENCODER_ENCODER2B_PIN);//读取IO电平获取电机旋转方向
   motor_r.encoder.countnum =
        motor_r.encoder.dierct ? ( motor_r.encoder.countnum + 1) : ( motor_r.encoder.countnum- 1);//通过判断旋转方向来决定countnum增加还是减少
    break;
  default:
    break;
  }
}

/*period 100Hz*/
void TIMA1_IRQHandler(void){
switch (DL_TimerA_getPendingInterrupt(CLOCK_INST)) {
case DL_TIMER_IIDX_ZERO:
DL_TimerA_clearInterruptStatus(CLOCK_INST,DL_TIMER_IIDX_ZERO);
motor_l.encoder.speed=(float)(motor_l.encoder.countnum-motor_l.encoder.lastcount)*6000/PULSE_PER_CYCLE;//rpm
motor_r.encoder.speed=(float)(motor_r.encoder.countnum-motor_r.encoder.lastcount)*6000/PULSE_PER_CYCLE;//rpm
motor_l.encoder.speed=Speed_Low_Filter(motor_l.encoder.speed,&motor_l.encoder);
motor_r.encoder.speed=Speed_Low_Filter(motor_r.encoder.speed,&motor_r.encoder);
motor_l.encoder.lastcount=motor_l.encoder.countnum;
motor_r.encoder.lastcount=motor_r.encoder.countnum;
}
}

在对电机测速时,需要进行均值滤波,否则由于编码器测速的特性,电机测出来的速度只能是某一个值的整数倍。均值滤波的函数如下

float Speed_Low_Filter(float new_Spe,encoder_t *encoder)
{
    float sum = 0.0f;
    uint32_t test_Speed = new_Spe;
    for(uint8_t i=SPEED_RECORD_NUM-1;i>0;i--)
    {
        encoder->speed_Record[i] = encoder->speed_Record[i-1];
        sum += encoder->speed_Record[i-1];
    }
    encoder->speed_Record[0] = new_Spe;
    sum += new_Spe;
    test_Speed = sum/SPEED_RECORD_NUM;
    return sum/SPEED_RECORD_NUM;
}

 然后是PID计算的代码,我使用的是位置式PID,较为简便

float PID_Realize(pid_t* pid,float target,float feedback)
{
    pid->err = target - feedback;
    if(pid->err < pid->dead_zone && pid->err > -pid->dead_zone) pid->err = 0;//pid死区
    pid->integral += pid->err;
    
    if(pid->ki * pid->integral < -pid->max_integral) pid->integral = -pid->max_integral / pid->ki;//积分限幅
    else if(pid->ki * pid->integral > pid->max_integral) pid->integral = pid->max_integral / pid->ki;

    pid->output = (pid->kp * pid->err) + (pid->ki * pid->integral) + (pid->kd * (pid->err - pid->last_err));

    //输出限幅
 if(pid->output > pid->maxoutput) pid->output = pid->maxoutput;
 else  if(pid->output < -pid->maxoutput) pid->output = -pid->maxoutput;
    pid->last_err = pid->err;

    return pid->output;
}

 有了以上的铺垫,终于可以开始控制电机了,我们的思路是,最底层为控制pwm的占空比,而控制速度就必须让PID输出一个值给占空比,这个PID的输出值可正可负,但是占空比只能为正,这时候我们要做进一步处理。

void Motor_Set_Duty(motor_t *motor){
    if (motor->pwmDuty>=0){//正转
DL_TimerG_setCaptureCompareValue(PWM_0_INST,motor->pwmDuty,motor->timer_index);
DL_GPIO_setPins(motor->forward_GPIO_PORT, motor->forward_GPIO_PIN);
DL_GPIO_clearPins(motor->reverse_GPIO_PORT, motor->reverse_GPIO_PIN);
    }
    else if(motor->pwmDuty<0){//反转
DL_TimerG_setCaptureCompareValue(PWM_0_INST,-motor->pwmDuty,motor->timer_index);
DL_GPIO_setPins(motor->reverse_GPIO_PORT, motor->reverse_GPIO_PIN);
DL_GPIO_clearPins(motor->forward_GPIO_PORT, motor->forward_GPIO_PIN);

    }
}

 这样我们就解决了设置占空比的问题,接下来是设置电机的速度,就需要用到PID以及编码器测速了

void Motor_Set_Speed(motor_t *motor){
motor->pwmDuty=PID_Realize(&motor->speed_pid,motor->speed,motor->encoder.speed);
Motor_Set_Duty(motor);
}

 速度环设置好了以后是位置环

void Motor_Set_Position(motor_t *motor){
motor->pos_pulse=(int)((motor->pos*PULSE_PER_CYCLE)/(LINE_SPEED_C));
motor->speed=PID_Realize(&motor->pos_pid, motor->pos_pulse, motor->encoder.countnum);
Motor_Set_Speed(motor);
}

这里稍微不同的是,我设置速度的单位为RPM,但位置环的单位是cm,需要稍微进行换算。

void Motor_Stop(motor_t *motor){
DL_TimerG_setCaptureCompareValue(PWM_0_INST,0,motor->timer_index);
DL_GPIO_clearPins(motor->forward_GPIO_PORT, motor->forward_GPIO_PIN);
DL_GPIO_clearPins(motor->reverse_GPIO_PORT, motor->reverse_GPIO_PIN);
}

最后是电机停止的代码。 注意除了中断函数外,其余的函数都需在motor.h中声明。

 到这里,所有的电机底层代码都写好了,怎么调用这些函数进行电机调速呢?我们可以在100Hz的时钟里设置一个标志位,在主循环中,当检测到这个标志位时,就进行电机的调速。具体操作不再赘述。电机调速需要更改pid参数的值,可以使用VOFA+上位机查看波形并调节参数。

总结

        本文通过syscfg的设置以及代码的书写,为读者使用ccs Theia开发MSPM0G3507开发板提供了一些思路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值