STM32 3508电机的pid速度闭环

最近又接触了robomaster比赛要用到的大疆3508(stm32f407)

只是简单的用CAN发收数据的速度闭环

先贴上基本资料(后有博主链接)

关于电机的说明请参照电机配套的C620电调说明书,这里简单介绍一下。C620电调可以通过两种方式控制电机,一种是接收PWM信号,这种方式接线与控制都比较简单,直接使用PWM进行速度控制(已经是闭环),另一种方式就是CAN通信,这种方式电调定期上传电机的电流、速度、位置等信息,控制器读取CAN报文之后通过PID计算得到输出量,再通过CAN总线下发报文,从而实现速度PID控制。虽然PWM方式简单易行,但经实际测试效果并不好,速度稳定性不行。

CAN上传报文:

CAN上传报文是电调发给单片机的,上传电机数据。

标识符:0×200+电调ID (如:ID为1,该标识符为0×201)(ID值位C620绿灯亮的次数)
帧类型:标准帧
帧格式:DATA
DLC:8字节

 (角度环读取DATA[0],DATA[1]   速度环读取DATA[2],DATA[3])

这还是很重要的

发送频率:1KHz(默认值,可在 RoboMaster Assistant软件中修改发送频率)
转子机械角度值范围:0~ 8191(对应转子机械角度为0~360°)
转子转速值的单位为:RPM
电机温度的单位为:℃

可以看到每个电调以1KHz的频率上报电机数据,每帧数据包含电机的位置、转速、转矩电流、温度四个信息量,我们最关注的就是转速,通过上报的转速与设定的速度比较,进行PID控制。

CAN下发报文:

CAN下发报文是单片机发给电调的,设置电调的电流输出

两个标识符(0200和0x1FF)各自对应控制4个电调。控制电流值范围-16384~ 0~ 16384,对应电调输出的转矩电流范围-20~ 0~ 20A。

标识符: 0x200 帧格式:DATA 帧类型: 标准帧 DLC:8字节

 标识符: 0x1FF 帧格式:DATA 帧类型: 标准帧 DLC:8字节

下发的报文只包含电流值信息,这是PID计算的输出结果,至于为什么是电流后面会说,每帧数据控制四个电机,最多通过两组不同ID的报文控制8个电机。

其他的设置电调ID的方法以及校准、指示灯等请阅读说明书。

三. 电机PID控制原理
电机是一种通电就会转的东东,正负极反接就会反转,(3508也不例外)电压增大转速就会增大,但是我们希望能够精准控制速度,甚至控制它的位置,所以需要有电机驱动器。最低级的电机驱动器只提供功率放大或整流,PID计算需要通过控制器计算;一些高级点的驱动器内部做了PID控制,控制器只需要给速度指令或位置指令就行了,这就是控制的分层。我们使用的M3508电机配套的C620电调,在用PWM控制时它是自带速度PID的,使用CAN总线控制时它就不带PID,需要单片机来计算。

伺服电机有位置伺服、速度伺服、力伺服,大家可能也听过电机的三闭环控制,也就是位置环——速度环——电流环,如图所示,根据控制目标的不同,从外环到内环分别对应电机的位置伺服、速度伺服与力伺服,也可以说三种模式:位置模式、速度模式、转矩模式。

我们的目标是控制速度,上图变成下面这样:

加减速处理的目的是防止偏差值太大,导致速度环PID输出结果太大,超过电机的响应范围,结果可能导致死机、断续旋转。3508就有这个问题的,加入加减速就是让电机速度变化更加平缓。

由于速度换输出的是电流值,所以这也解释了为什么单片机通过CAN报文下发的是转矩电流。至于电流环在哪?那是硬件决定的。
原文链接:https://blog.csdn.net/qq_30267617/article/details/119651826

这篇文章只写了速度闭环

贴上代码   具体代码解释

CAN.C

#include "can.h"
#include "Encorde.h"

//CAN初始化
//tsjw:重新同步跳跃时间单元.范围:CAN_SJW_1tq~ CAN_SJW_4tq
//tbs2:时间段2的时间单元.   范围:CAN_BS2_1tq~CAN_BS2_8tq;
//tbs1:时间段1的时间单元.   范围:CAN_BS1_1tq ~CAN_BS1_16tq
//brp :波特率分频器.范围:1~1024; tq=(brp)*tpclk1
//波特率=Fpclk1/((tbs1+1+tbs2+1+1)*brp);
//mode:CAN_Mode_Normal,普通模式;CAN_Mode_LoopBack,回环模式;
//Fpclk1的时钟在初始化的时候设置为42M,如果设置CAN1_Mode_Init(CAN_SJW_1tq,CAN_BS2_6tq,CAN_BS1_7tq,6,CAN_Mode_LoopBack);
//则波特率为:42M/((6+7+1)*6)=500Kbps
//返回值:0,初始化OK;
//    其他,初始化失败; 
void CAN_Configure()
{
    CAN_InitTypeDef        can;  //CAN结构体
    CAN_FilterInitTypeDef  can_filter;  //筛选器结构体
    GPIO_InitTypeDef       GPIO;
    NVIC_InitTypeDef       nvic;

    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);//系统总线 高性能模块(如CPU、DMA和DSP等)之间的连接
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);//低带宽的周边外设之间的连接,例如UART

    GPIO_PinAFConfig(GPIOA, GPIO_PinSource11, GPIO_AF_CAN1);
    GPIO_PinAFConfig(GPIOA, GPIO_PinSource12, GPIO_AF_CAN1);

    GPIO.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_12;
    GPIO.GPIO_Mode = GPIO_Mode_AF;
    GPIO_Init(GPIOA, & GPIO);

    nvic.NVIC_IRQChannel = CAN1_RX0_IRQn;
    nvic.NVIC_IRQChannelPreemptionPriority = 2;
    nvic.NVIC_IRQChannelSubPriority = 1;
    nvic.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&nvic);

    nvic.NVIC_IRQChannel = CAN1_TX_IRQn;
    nvic.NVIC_IRQChannelPreemptionPriority = 1;
    nvic.NVIC_IRQChannelSubPriority = 1;
    nvic.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&nvic);    

    CAN_DeInit(CAN1);
    CAN_StructInit(&can);

    can.CAN_TTCM = DISABLE;  //非时间触发通信模式 
    can.CAN_ABOM = DISABLE;  //软件自动离线管理    
    can.CAN_AWUM = DISABLE;  //睡眠模式通过软件唤醒(清除CAN->MCR的SLEEP位)
    can.CAN_NART = DISABLE;//禁止报文自动传送 
    can.CAN_RFLM = DISABLE;//报文不锁定,新的覆盖旧的
    can.CAN_TXFP = ENABLE;    //优先级由报文标识符决定 
    can.CAN_Mode = CAN_Mode_Normal;//四个模式  正常模式
    can.CAN_SJW  = CAN_SJW_1tq;//重新同步跳跃宽度(Tsjw)为tsjw+1个时间单位 CAN_SJW_1tq~CAN_SJW_4tq
    can.CAN_BS1 = CAN_BS1_9tq;//Tbs1范围CAN_BS1_1tq ~CAN_BS1_16tq
    can.CAN_BS2 = CAN_BS2_4tq;//Tbs2范围CAN_BS2_1tq ~    CAN_BS2_8tq
    can.CAN_Prescaler = 3;   //CAN BaudRate 42/(1+9+4)/3=1Mbps
    CAN_Init(CAN1, &can);  //初始化CAN1

    can_filter.CAN_FilterNumber=0;  //过滤器0
    can_filter.CAN_FilterMode=CAN_FilterMode_IdMask;
    can_filter.CAN_FilterScale=CAN_FilterScale_32bit;//32
    can_filter.CAN_FilterIdHigh=0x0000;  //32位ID高八位低八位
    can_filter.CAN_FilterIdLow=0x0000;
    can_filter.CAN_FilterMaskIdHigh=0x0000;//32位高低八位MASK
    can_filter.CAN_FilterMaskIdLow=0x0000;
    can_filter.CAN_FilterFIFOAssignment=0;//the message which pass the filter save in fifo0
    can_filter.CAN_FilterActivation=ENABLE;//激活过滤筛选器0
    CAN_FilterInit(&can_filter);   //滤波器初始化

    CAN_ITConfig(CAN1,CAN_IT_FMP0,ENABLE);
    CAN_ITConfig(CAN1,CAN_IT_TME,ENABLE); 
}
/**
  *****************************************************************************
* 函数名 :      can发送中断  电调发送给单片机
  ******************************************************************************
  */
void CAN1_TX_IRQHandler(void) //CAN TX
{
    if (CAN_GetITStatus(CAN1,CAN_IT_TME)!= RESET) 
    {
        CAN_ClearITPendingBit(CAN1,CAN_IT_TME);
  }
}
/**
  *****************************************************************************
*函数名 :       can接收中断 单片机发送电调
  ******************************************************************************
  */
CanRxMsg rx_message;    
void CAN1_RX0_IRQHandler(void)
{   

    if (CAN_GetITStatus(CAN1,CAN_IT_FMP0)!= RESET)
      {
        CAN_ClearITPendingBit(CAN1, CAN_IT_FF0);
            CAN_ClearFlag(CAN1, CAN_FLAG_FF0); 
            CAN_Receive(CAN1, CAN_FIFO0, &rx_message);
            CanReceiveMsgProcess(&rx_message);
    }
}

void Set_ChassisMotor_Current(int16_t cm1_iq, int16_t cm2_iq, int16_t cm3_iq, int16_t cm4_iq)
{
    CanTxMsg tx_message;
    tx_message.StdId = 0x200;
    tx_message.IDE = CAN_Id_Standard;
    tx_message.RTR = CAN_RTR_Data;
    tx_message.DLC = 0x08;
    
    tx_message.Data[0] = (uint8_t)(cm1_iq >> 8);   //cm1_iq 234 分别视自己ID设值
    tx_message.Data[1] = (uint8_t)cm1_iq;
    tx_message.Data[2] = (uint8_t)(cm2_iq >> 8);
    tx_message.Data[3] = (uint8_t)cm2_iq;
    tx_message.Data[4] = (uint8_t)(cm3_iq >> 8);
    tx_message.Data[5] = (uint8_t)cm3_iq;
    tx_message.Data[6] = (uint8_t)(cm4_iq >> 8);   //ID为4  则调用时只需要设置cm4_iq 为 CM_LF_Speedloop.Out(PID返回值)
    tx_message.Data[7] = (uint8_t)cm4_iq;
    CAN_Transmit(CAN1,&tx_message);
}

CAN.h

/**
  ******************************************************************************
  * 文件名:  can底层驱动文件头文件
  * 函数列表:can配置函数
  *              can接收函数
  *                  can接收中断
  ******************************************************************************  
  */ 
  
#ifndef _CAN_
#define _CAN_
#include "sys.h"

//地盘四个电机的can id
#define CAN_ID_CM1 0x201
#define CAN_ID_CM2 0x202
#define CAN_ID_CM3 0x203
#define CAN_ID_CM4 0x204

void CAN_Configure(void);
void CanReceiveMsgProcess(CanRxMsg *message);
void CAN1_RX0_IRQHandler(void);
void CAN1_TX_IRQHandler(void) ;//CAN TX;
 

#endif

Encorde.c

#include "Encorde.h"
#include "can.h"
#include "motor.h"

volatile Encoder CM1Encoder = {0,0,0,0,0,0,0,0,0};
volatile Encoder CM2Encoder = {0,0,0,0,0,0,0,0,0};
volatile Encoder CM3Encoder = {0,0,0,0,0,0,0,0,0};
volatile Encoder CM4Encoder = {0,0,0,0,0,0,0,0,0};
volatile Encoder GMYawEncoder = {0,0,0,0,0,0,0,0,0};
volatile Encoder GMPitchEncoder = {0,0,0,0,0,0,0,0,0};

void CanReceiveMsgProcess(CanRxMsg *message)
{
    switch(message->StdId)
    {
        case CAN_ID_CM4:     //ID不同时记得gai'bian
        {
                 getEncoderData(&CM4Encoder, message);
        }break;
        case CAN_ID_PITCH:
        {
                 getEncoderData(&GMPitchEncoder, message);
        }break;
    }
}

void getEncoderData(volatile Encoder *v, CanRxMsg * msg)  // 接收3508电机通过CAN发过来的信息 
{
    int i=0;
    int32_t temp_sum = 0;    
    v->last_raw_value = v->raw_value;

    v->raw_value = (msg->Data[2]<<8)|msg->Data[3];
    v->diff = v->raw_value – v->last_raw_value;
    
    if(v->diff < -7500)    //两次编码器的反馈值差别太大,表示圈数发生了改变
    {
        v->round_cnt++;
        v->ecd_raw_rate = v->diff + 8192;
    }
    else if(v->diff>7500)
    {
        v->round_cnt–;
        v->ecd_raw_rate = v->diff – 8192;
    }        
    else
    {
        v->ecd_raw_rate = v->diff;
    }
    
    //计算得到连续的编码器输出值
    v->ecd_value = v->raw_value + v->round_cnt * 8192;
    //计算得到角度值,范围正负无穷大
    v->ecd_angle = (float)(v->raw_value – v->ecd_bias)*360/8192 + v->round_cnt * 360;
    v->rate_buf[v->buf_count++] = v->ecd_raw_rate;
    if(v->buf_count == RATE_BUF_SIZE)
    {
        v->buf_count = 0;
    }
    //计算速度平均值
    for(i = 0;i < RATE_BUF_SIZE; i++)
    {
        temp_sum += v->rate_buf[i];
    }
    v->filter_rate = (int32_t)(temp_sum/RATE_BUF_SIZE);        
}

void TIM3_Int_Init(void)     //配置定时器中断
{
    TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
    NVIC_InitTypeDef NVIC_InitStructure;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);    //使能相应的时钟
   
    TIM_TimeBaseInitStructure.TIM_Period =99;           
    TIM_TimeBaseInitStructure.TIM_Prescaler=839;  
    TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; 
    TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1;     //分频   //10ms(0.01s)中断一次

    TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);

    TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE); 

    NVIC_InitStructure.NVIC_IRQChannel=TIM3_IRQn;     //中断
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1; 
    NVIC_InitStructure.NVIC_IRQChannelSubPriority=3; 
    NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;      //使能中断
    NVIC_Init(&NVIC_InitStructure);

    TIM_SetCounter(TIM3,0);      //定时器3清零
    TIM_Cmd(TIM3, ENABLE); 
}

void TIM3_IRQHandler(void)  //通过溢出中断来读取圈数
{
    if(TIM_GetITStatus(TIM3,TIM_IT_Update)==SET)
    {      
     ChassisMotor_Ctrl();            
     ChassisMotor_Out();
    }
   TIM_ClearITPendingBit(TIM3,TIM_IT_Update); 
}

Encorde.h

#ifndef _ENCORDE_
#define _ENCORDE_

#include "stm32f4xx.h"

#define RATE_BUF_SIZE 6
typedef struct
    {
    int32_t raw_value;                          //编码器不经处理的原始值
    int32_t last_raw_value;                     //上一次的编码器原始值
    int32_t ecd_value;               //经过处理后连续的编码器值
    int32_t diff;                                   //两次编码器之间的差值
    int32_t temp_count;              //计数用
    uint8_t buf_count;                           //滤波更新buf用
    int32_t ecd_bias;                             //初始编码器值    
    int32_t ecd_raw_rate;                       //通过编码器计算得到的速度原始值
    int32_t rate_buf[RATE_BUF_SIZE]; //buf,for filter
    int32_t round_cnt;                           //圈数
    int32_t filter_rate;                       //速度
    float ecd_angle;                             //角度
  }Encoder;

void CanReceiveMsgProcess(CanRxMsg *message);             //接收3508电机通过CAN发过来的信息
void getEncoderData(volatile Encoder *v, CanRxMsg * msg); //得到编码器数据

void Quad_Encoder_Configuration(void);  //初始化定时器4
int32_t GetQuadEncoderDiff(void);   //得到圈数
void Encoder_Start(void);
 

motor.c

#include "motor.h"
#include "can.h"
#include "Encorde.h"

ChassisMotorPID CM_LF_Speedloop;
extern volatile Encoder CM4Encoder;
int b;
/**
  *****************************************************************************
  *函数名 :       3508电机配置函数
  *函数功能描述 : 用于配置化电机有关的各项参数 
  *函数参数 :     无
  *函数返回值 :   无
  ******************************************************************************
  */
void ChassisMotor_Configure()
{
  CM_LF_Speedloop.Kp = 1.0f;
    CM_LF_Speedloop.Ki = 3.0f;
    CM_LF_Speedloop.Kd = 0.0f;
    CM_LF_Speedloop.PoutMax = 5000.0f;
    CM_LF_Speedloop.IoutMax = 100.0f;
    CM_LF_Speedloop.DoutMax = 1000.0f;
    CM_LF_Speedloop.OutMax = 7000.0f;
    CM_LF_Speedloop.Set = 800;
    CM_LF_Speedloop.Real = 0.0f;
    CM_LF_Speedloop.Out = 0.0f;
    CM_LF_Speedloop.err = 0.0f;
    CM_LF_Speedloop.err_last = 0.0f;
    CM_LF_Speedloop.err_llast = 0.0f;
    CM_LF_Speedloop.integral = 0.0f;
}
/**
  *****************************************************************************
  *函数名 :       3508电机控制函数
  *函数功能描述 : 用于pid的实际计算 定时调用,周期4ms 实际值依靠编码器
  *函数参数 :     无
  ******************************************************************************
  */
void ChassisMotor_Ctrl()
{
  float Pout = 0.0f;
    float Iout = 0.0f;
    float Dout = 0.0f;
    

//  电机速度环    
  CM_LF_Speedloop.Real = CM4Encoder.filter_rate;
    
    CM_LF_Speedloop.err_last = CM_LF_Speedloop.err;
    CM_LF_Speedloop.err = CM_LF_Speedloop.Set – CM_LF_Speedloop.Real;
    CM_LF_Speedloop.integral += CM_LF_Speedloop.err;
    
    Pout = CM_LF_Speedloop.Kp * CM_LF_Speedloop.err;
    Pout = Pout < CM_LF_Speedloop.PoutMax ? Pout : CM_LF_Speedloop.PoutMax;
    Pout = Pout > -CM_LF_Speedloop.PoutMax ? Pout : -CM_LF_Speedloop.PoutMax;

    Iout = CM_LF_Speedloop.Ki * CM_LF_Speedloop.integral;
    Iout = Iout < CM_LF_Speedloop.IoutMax ? Iout : CM_LF_Speedloop.IoutMax;
    Iout = Iout > -CM_LF_Speedloop.IoutMax ? Iout : -CM_LF_Speedloop.IoutMax;

    Dout = CM_LF_Speedloop.Kd * (CM_LF_Speedloop.err – CM_LF_Speedloop.err_last);
    Dout = Dout < CM_LF_Speedloop.DoutMax ? Dout : CM_LF_Speedloop.DoutMax;
    Dout = Dout > -CM_LF_Speedloop.DoutMax ? Dout : -CM_LF_Speedloop.DoutMax;
    
    CM_LF_Speedloop.Out = Pout + Iout + Dout;
    CM_LF_Speedloop.Out = CM_LF_Speedloop.Out < CM_LF_Speedloop.OutMax ? CM_LF_Speedloop.Out : CM_LF_Speedloop.OutMax;
    CM_LF_Speedloop.Out = CM_LF_Speedloop.Out > -CM_LF_Speedloop.OutMax ? CM_LF_Speedloop.Out : -CM_LF_Speedloop.OutMax;
    
}
/**
  *****************************************************************************
  *函数名 :       3508电机输出函数 
  *函数功能描述 : 执行pid解算出的数据
  *函数参数 :     无
  ******************************************************************************
  */
void ChassisMotor_Out()
{
      b=CM_LF_Speedloop.Out;
    Set_ChassisMotor_Current(0,0, 0,CM_LF_Speedloop.Out);
}

motor.h

/*********************************************************************************
  *Copyright(C),SLDX->Ambition
  *文件名:   地盘电机 头文件  
            本文件的函数依赖于CAN头文件和Encoder头文件,主要利用其获取和发送数据
            到电机。本文件主要通过定时调用ChassisMotor_Ctrl函数进行pid解算,通过
            调用ChassisMotor_Out函数进行实际输出。ChassisMotor_Configure进行初始
            化电机各项参数
  *主要函数列表:  
     1.ChassisMotor_Configur
     2.ChassisMotor_Ctrl
       3.ChassisMotor_Out
**********************************************************************************/

#ifndef _MOTOR_
#define _MOTOR_

typedef struct ChassisMotorPID 
{
    float Kp,Ki,Kd;    //定义 
    float PoutMax,IoutMax,DoutMax;
    float OutMax;
    
    float Set;    //定义设定值
    float Real; //编码器采样值
  float Out; //输出值
    
  float err;         //定义偏差值
  float err_last; //上一次偏差值
  float err_llast; //最上次偏差值
    float integral; //误差累计
}ChassisMotorPID;

extern ChassisMotorPID CM_LF_Speedloop;

void ChassisMotor_Configure(void);  //pid解算
void ChassisMotor_Ctrl(void);       //初始化电机各项参数
void ChassisMotor_Out(void);       //实际输出

#endif

main.c

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "can.h"
#include "motor.h"
#include "encorde.h"

int main(void)
 {     
    u8 canbuf[8];
    delay_init(168);                                                             //延时函数初始化      
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);    //设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
  Quad_Encoder_Configuration();
  TIM3_Int_Init();
  1 CAN_Configure();                                //can初始化
     
    ChassisMotor_Configure();
//    Set_ChassisMotor_Current(0, 0, 0, 500);  //不能一开始直接给定值!!会让电机一直以最大速度转!!注释掉这行代码
     while(1)
    {
    }
}
 

所需的核心代码粘贴完毕啦

我再理一理思路!

1.  在20ms一次中断的定时器里进行  每次计算出pid的计算值  CM_LF_Speedloop.Out

再在  Set_ChassisMotor_Current(0,0, 0,CM_LF_Speedloop.Out);  中将 CM_LF_Speedloop.Out

通过Set_ChassisMotor_Current()函数以can形式在CAN1_RX0_IRQHandler  //can接受中断中 发送给电调   电调收到后在can发送中断CAN1_TX_IRQHandler中发送给电机  后电机通过接受值来调节电流

2. 电机是如何收到电机真实值

  CM_LF_Speedloop.Real = CM4Encoder.filvolatile

void CanReceiveMsgProcess(CanRxMsg *message)

getEncoderData(&CM4Encoder, message);

Encoder CM4Encoder = {0,0,0,0,0,0,0,0,0};ter_rate; 

  v->raw_value = (msg->Data[2]<<8)|msg->Data[3];

作者:JUDITH-99

物联沃分享整理
物联沃-IOTWORD物联网 » STM32 3508电机的pid速度闭环

发表回复