之前有段时间因为机器狗项目的缘故,一直在使用小米微电机,但是苦于没有一个详尽的奶妈级教程,在控制电机的学习中踩了不少的坑。今天咱们就从头至尾一步一步的实现使用按键控制小米微电机。本文将会分析小米电机驱动库,并简要介绍相关的CAN通信知识。阅读本文之前建议先看一遍小米电机说明书,直接百度就有。

一、前置知识

小米微电机是一款伺服电机,那什么是伺服电机呢?
伺服电机的最大特征要素是伺服机构。
伺服机构是以物体的位置、方位、姿态等控制量,跟随目标(或给定值)变化的自动控制系统。伺服的英文“servo”以拉丁语中表示“奴隶”的“servus”为词根,意思是按照指令动作的控制
就拿小米电机来说,有多种控制模式,其运控模式流程图如下

不过对我们来说,可以把小米微电机看做一个黑盒,只要给他供电之后,再向它发送命令,它就可以安装命令进行转动,我们不需要关系电机的电流电压或者相关的控制算法,因为这些都已经被打包在伺服系统之中


CAN通信是什么?如何进行CAN通信?
CAN(Controller Area Network),是ISO国际标准化的串行通信协议
它的诞生是为了满足汽车产业的“减少线束的数量”、“通过多个LAN,进行大量数据的高速通信”的需求。

低速CAN(ISO11519)通信速率10~125Kbps,总线长度可达1000
高速CAN(ISO11898)通信速率125Kbps~1Mbps,总线长度≤40米
CAN FD 通信速率可达5Mbps,并且兼容经典CAN,遵循ISO 11898-1 做数据收发

CAN总线以“帧”形式进行通信。CAN协议定义了5种类型的帧:数据帧、遥控帧、错误帧、过载帧、间隔帧,其中数据帧最为常用。数据帧又分为标准帧和扩展帧。


以上内容了解即可,不需要我们重点关注
我们只需要知道:小米微电机使用的是:高速CAN,波特率1M,扩展帧格式!

二、小米电机通信协议

电机通信为 CAN 2.0 通信接口,波特率 1Mbps,采用扩展帧格式,如下所示:


电机支持的控制模式包括:
运控模式:给定电机运控 5 个参数;
电流模式:给定电机指定的 Iq 电流;
速度模式:给定电机指定的运行速度;
位置模式:给定电机指定的位置,电机将运行到该指定的位置;

哇!CAN通信的数据帧那么复杂,这个通信协议也不简单,我们该怎么理解呢?
这里我们先看一个软件的截图

看下面的操作区,有没有发现需要我们关注和设置只有ID和数据呀?
因为CAN通信的数据帧中我们能改变的,或者说传递信息的只有ID和数据。
小米的通信协议也是这样,通过设置ID和数据(十六进制格式)然后发送给电机就能控制电机了


此处用获取电机信息举例

我们只要把数据帧设置为如下内容即可:

图中前两个为电机开机启动时发送的数据帧,第三行为我们发送的数据帧。
这里我们设置ID为:0x00000023,数据设置为:0x00 00 00 00 00 00 00 00
可以看到电机应答帧为图中第四个数据帧。
至于其他的通信类型,都大差不差,大家可以自行阅读说明书中的通信协议。

三、小米电机驱动库

这里鸣谢大佬ZDYukino。我对此库进行些许改动,使其可以在105上实现在两个can上发送
不过这个库没有详细的解释,之前初学的我还是踩了不少的坑,代码将放到文末
这里我们将概括性的分析此驱动库,并将在下一章中结合实例驱动电机

主逻辑&功能:

  1. 定义了一些全局变量和宏定义,包括 CAN 通信相关的数据结构和变量。

  2. 实现了一些辅助函数:

    • Float_to_Byte 将一个浮点数转换为字节数组。
    • uint16_to_float 将一个 16 位无符号整数转换为浮点数。
    • float_to_uint 将一个浮点数转换为一个指定位数的无符号整数。
    • Set_Motor_Parameter 设置电机的参数,根据参数类型将参数值转换为字节数组,并通过 CAN 发送给电机。
    • Get_Motor_ID 从接收到的 CAN ID 中提取电机的 ID。
  3. 实现了一些控制电机的函数:

    • chack_cybergear 检查电机的状态。
    • start_cybergear 启动电机。
    • stop_cybergear 停止电机。
    • set_mode_cybergear 设置电机的工作模式。
    • set_current_cybergear 设置电机的电流。
    • set_zeropos_cybergear 设置电机的零点位置。
    • set_CANID_cybergear 设置电机的 CAN ID。
    • init_cybergear 初始化电机,设置电机的 ID 和模式,并启动电机。
    • motor_controlmode 控制电机的运动模式,包括力矩、机械位置、速度、控制参数等。
  4. 实现了一个 CAN 接收回调函数 HAL_CAN_RxFifo1MsgPendingCallback,当有 CAN 消息到达时触发该函数。在该函数中,根据接收到的电机 ID,将接收到的数据提取出来并保存到对应的电机结构体中。

对于头文件:

  1. 定义了一些宏,包括一些控制参数的最小值和最大值,以及通信命令的宏定义。

  2. 定义了一个枚举类型 CONTROL_MODE,用于表示电机的控制模式,包括运控模式、位置模式、速度模式和电流模式。

  3. 定义了一个枚举类型 ERROR_TAG,用于表示电机的错误状态。

  4. 定义了一个结构体 MI_Motor,表示小米电机。该结构体包含了电机的一些状态信息,如 CAN ID、MCU ID、角度、速度、力矩、温度等。还包含了一些设置电机参数的变量,如设定电流、设定速度、设定位置等。

  5. 声明了一些函数的原型,包括检查电机状态、启动电机、停止电机、设置电机工作模式、设置电机电流、设置电机零点位置、设置电机的 CAN ID、初始化电机和控制电机运动模式等。

/**
  ****************************(C)SWJTU_ROBOTCON****************************
  * @file       cybergear.c/h
  * @brief      小米电机函数库
  * @note       
  * @history
  *  Version    Date            Author          Modification
  *  V1.0.0     1-10-2023       ZDYukino        1. done
  *
  @verbatim
  =========================================================================
  =========================================================================
  @endverbatim
  ****************************(C)SWJTU_ROBOTCON****************************
  **/
#include "main.h"
#include "can.h"
#include "cybergear.h"
//#include "vofa.h"

CAN_RxHeaderTypeDef rxMsg;//发送接收结构体
CAN_TxHeaderTypeDef txMsg;//发送配置结构体
uint8_t rx_data[8];       //接收数据
uint32_t Motor_Can_ID;    //接收数据电机ID
uint8_t byte[4];          //转换临时数据
uint32_t send_mail_box = {0};//NONE

#define can_txd() HAL_CAN_AddTxMessage(&hcan1, &txMsg, tx_data, &send_mail_box)//CAN发送宏定义

MI_Motor mi_motor[4];//预先定义四个小米电机

/**
  * @brief          浮点数转4字节函数
  * @param[in]      f:浮点数
  * @retval         4字节数组
  * @description  : IEEE 754 协议
  */
static uint8_t* Float_to_Byte(float f)
{
    unsigned long longdata = 0;
    longdata = *(unsigned long*)&f;       
    byte[0] = (longdata & 0xFF000000) >> 24;
    byte[1] = (longdata & 0x00FF0000) >> 16;                                                                                                                                                                                                         
    byte[2] = (longdata & 0x0000FF00) >> 8;
    byte[3] = (longdata & 0x000000FF);
    return byte;
}

/**
  * @brief          小米电机回文16位数据转浮点
  * @param[in]      x:16位回文
  * @param[in]      x_min:对应参数下限
  * @param[in]      x_max:对应参数上限
  * @param[in]      bits:参数位数
  * @retval         返回浮点值
  */
static float uint16_to_float(uint16_t x,float x_min,float x_max,int bits)
{
    uint32_t span = (1 << bits) - 1;
    float offset = x_max - x_min;
    return offset * x / span + x_min;
}

/**
  * @brief          小米电机发送浮点转16位数据
  * @param[in]      x:浮点
  * @param[in]      x_min:对应参数下限
  * @param[in]      x_max:对应参数上限
  * @param[in]      bits:参数位数
  * @retval         返回浮点值
  */
static int float_to_uint(float x, float x_min, float x_max, int bits)
{
  float span = x_max - x_min;
  float offset = x_min;
  if(x > x_max) x=x_max;
  else if(x < x_min) x= x_min;
  return (int) ((x-offset)*((float)((1<<bits)-1))/span);
}

/**
  * @brief          写入电机参数
  * @param[in]      Motor:对应控制电机结构体
  * @param[in]      Index:写入参数对应地址
  * @param[in]      Value:写入参数值
  * @param[in]      Value_type:写入参数数据类型
  * @retval         none
  */
static void Set_Motor_Parameter(MI_Motor *Motor,uint16_t Index,float Value,char Value_type){
    uint8_t tx_data[8];
    txMsg.ExtId = Communication_Type_SetSingleParameter<<24|Master_CAN_ID<<8|Motor->CAN_ID;
    tx_data[0]=Index;
    tx_data[1]=Index>>8;
    tx_data[2]=0x00;
    tx_data[3]=0x00;
    if(Value_type == 'f'){
        Float_to_Byte(Value);
        tx_data[4]=byte[3];
        tx_data[5]=byte[2];
        tx_data[6]=byte[1];
        tx_data[7]=byte[0];        
    }
    else if(Value_type == 's'){
        tx_data[4]=(uint8_t)Value;
        tx_data[5]=0x00;
        tx_data[6]=0x00;
        tx_data[7]=0x00;                
    }
    can_txd();    
}

/**
  * @brief          提取电机回复帧扩展ID中的电机CANID
  * @param[in]      CAN_ID_Frame:电机回复帧中的扩展CANID   
  * @retval         电机CANID
  */
static uint32_t Get_Motor_ID(uint32_t CAN_ID_Frame)
{
    return (CAN_ID_Frame&0xFFFF)>>8;
}

/**
  * @brief          电机回复帧数据处理函数
  * @param[in]      Motor:对应控制电机结构体   
  * @param[in]      DataFrame:数据帧
  * @param[in]      IDFrame:扩展ID帧
  * @retval         None
  */
static void Motor_Data_Handler(MI_Motor *Motor,uint8_t DataFrame[8],uint32_t IDFrame)
{    
        Motor->Angle=uint16_to_float(DataFrame[0]<<8|DataFrame[1],MIN_P,MAX_P,16);
        Motor->Speed=uint16_to_float(DataFrame[2]<<8|DataFrame[3],V_MIN,V_MAX,16);            
        Motor->Torque=uint16_to_float(DataFrame[4]<<8|DataFrame[5],T_MIN,T_MAX,16);                
        Motor->Temp=(DataFrame[6]<<8|DataFrame[7])*Temp_Gain;
        Motor->error_code=(IDFrame&0x1F0000)>>16;    
}

/**
  * @brief          小米电机ID检查
  * @param[in]      id:  对应控制电机结构体
  * @retval         none
  */
void chack_cybergear(uint8_t ID)
{
    uint8_t tx_data[8] = {0};
    txMsg.ExtId = Communication_Type_GetID<<24|Master_CAN_ID<<8|ID;
    can_txd();
}

/**
  * @brief          使能小米电机
  * @param[in]      Motor:对应控制电机结构体   
  * @retval         none
  */
void start_cybergear(MI_Motor *Motor)
{
    uint8_t tx_data[8] = {0}; 
    txMsg.ExtId = Communication_Type_MotorEnable<<24|Master_CAN_ID<<8|Motor->CAN_ID;
    can_txd();
}

/**
  * @brief          停止电机
  * @param[in]      Motor:对应控制电机结构体   
  * @param[in]      clear_error:清除错误位(0 不清除 1清除)
  * @retval         None
  */
void stop_cybergear(MI_Motor *Motor,uint8_t clear_error)
{
    uint8_t tx_data[8]={0};
    tx_data[0]=clear_error;//清除错误位设置
    txMsg.ExtId = Communication_Type_MotorStop<<24|Master_CAN_ID<<8|Motor->CAN_ID;
    can_txd();
}

/**
  * @brief          设置电机模式(必须停止时调整!)
  * @param[in]      Motor:  电机结构体
  * @param[in]      Mode:   电机工作模式(1.运动模式Motion_mode 2. 位置模式Position_mode 3. 速度模式Speed_mode 4. 电流模式Current_mode)
  * @retval         none
  */
void set_mode_cybergear(MI_Motor *Motor,uint8_t Mode)
{    
    Set_Motor_Parameter(Motor,Run_mode,Mode,'s');
}

/**
  * @brief          电流控制模式下设置电流
  * @param[in]      Motor:  电机结构体
  * @param[in]      Current:电流设置
  * @retval         none
  */
void set_current_cybergear(MI_Motor *Motor,float Current)
{
    Set_Motor_Parameter(Motor,Iq_Ref,Current,'f');
}

/**
  * @brief          设置电机零点
  * @param[in]      Motor:  电机结构体
  * @retval         none
  */
void set_zeropos_cybergear(MI_Motor *Motor)
{
    uint8_t tx_data[8]={0};
    txMsg.ExtId = Communication_Type_SetPosZero<<24|Master_CAN_ID<<8|Motor->CAN_ID;
    can_txd();        
}

/**
  * @brief          设置电机CANID
  * @param[in]      Motor:  电机结构体
  * @param[in]      Motor:  设置新ID
  * @retval         none
  */
void set_CANID_cybergear(MI_Motor *Motor,uint8_t CAN_ID)
{
    uint8_t tx_data[8]={0};
    txMsg.ExtId = Communication_Type_CanID<<24|CAN_ID<<16|Master_CAN_ID<<8|Motor->CAN_ID;
    Motor->CAN_ID = CAN_ID;//将新的ID导入电机结构体
    can_txd();    
}
/**
  * @brief          小米电机初始化
  * @param[in]      Motor:  电机结构体
  * @param[in]      Can_Id: 小米电机ID(默认0x7F)
  * @param[in]      Motor_Num: 电机编号
  * @param[in]      mode: 电机工作模式(0.运动模式Motion_mode 1. 位置模式Position_mode 2. 速度模式Speed_mode 3. 电流模式Current_mode)
  * @retval         none
  */
void init_cybergear(MI_Motor *Motor,uint8_t Can_Id, uint8_t mode)
{
    txMsg.StdId = 0;            //配置CAN发送:标准帧清零 
    txMsg.ExtId = 0;            //配置CAN发送:扩展帧清零     
    txMsg.IDE = CAN_ID_EXT;     //配置CAN发送:扩展帧
    txMsg.RTR = CAN_RTR_DATA;   //配置CAN发送:数据帧
    txMsg.DLC = 0x08;           //配置CAN发送:数据长度

    Motor->CAN_ID=Can_Id;       //ID设置 
    set_mode_cybergear(Motor,mode);//设置电机模式
    start_cybergear(Motor);        //使能电机
}

/**
  * @brief          小米运控模式指令
  * @param[in]      Motor:  目标电机结构体
  * @param[in]      torque: 力矩设置[-12,12] N*M
  * @param[in]      MechPosition: 位置设置[-12.5,12.5] rad
  * @param[in]      speed: 速度设置[-30,30] rpm
  * @param[in]      kp: 比例参数设置
  * @param[in]      kd: 微分参数设置
  * @retval         none
  */
void motor_controlmode(MI_Motor *Motor,float torque, float MechPosition, float speed, float kp, float kd)
{   
    uint8_t tx_data[8];//发送数据初始化
    //装填发送数据
    tx_data[0]=float_to_uint(MechPosition,P_MIN,P_MAX,16)>>8;  
    tx_data[1]=float_to_uint(MechPosition,P_MIN,P_MAX,16);  
    tx_data[2]=float_to_uint(speed,V_MIN,V_MAX,16)>>8;  
    tx_data[3]=float_to_uint(speed,V_MIN,V_MAX,16);  
    tx_data[4]=float_to_uint(kp,KP_MIN,KP_MAX,16)>>8;  
    tx_data[5]=float_to_uint(kp,KP_MIN,KP_MAX,16);  
    tx_data[6]=float_to_uint(kd,KD_MIN,KD_MAX,16)>>8;  
    tx_data[7]=float_to_uint(kd,KD_MIN,KD_MAX,16); 

    txMsg.ExtId = Communication_Type_MotionControl<<24|float_to_uint(torque,T_MIN,T_MAX,16)<<8|Motor->CAN_ID;//装填扩展帧数据
    can_txd();
}

/*****************************回调函数 负责接回传信息 可转移至别处*****************************/
/**
  * @brief          hal库CAN回调函数,接收电机数据
  * @param[in]      hcan:CAN句柄指针
  * @retval         none
  */
void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    //HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin);              //LED闪烁指示
    HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO1, &rxMsg, rx_data);//接收数据
    Motor_Can_ID=Get_Motor_ID(rxMsg.ExtId);//首先获取回传电机ID信息  
    switch(Motor_Can_ID)                   //将对应ID电机信息提取至对应结构体
    {
        case 0X7F:  
            if(rxMsg.ExtId>>24 != 0)               //检查是否为广播模式
                Motor_Data_Handler(&mi_motor[0],rx_data,rxMsg.ExtId);
            else 
                mi_motor[0].MCU_ID = rx_data[0];
            break;           
        default:
            break;        
    }
}
/**
  ****************************(C)SWJTU_ROBOTCON****************************
  * @file       cybergear.c/h
  * @brief      小米电机函数库
  * @note       
  * @history
  *  Version    Date            Author          Modification
  *  V1.0.0     1-10-2023       ZDYukino        1. done
  *
  @verbatim
  =========================================================================
  =========================================================================
  @endverbatim
  ****************************(C)SWJTU_ROBOTCON****************************
  **/
#include "main.h"
#include "can.h"
//控制参数最值,谨慎更改
#define P_MIN -12.5f
#define P_MAX 12.5f
#define V_MIN -30.0f
#define V_MAX 30.0f
#define KP_MIN 0.0f
#define KP_MAX 500.0f
#define KD_MIN 0.0f
#define KD_MAX 5.0f
#define T_MIN -12.0f
#define T_MAX 12.0f
#define MAX_P 720
#define MIN_P -720
//主机CANID设置
#define Master_CAN_ID 0x00                      //主机ID
//控制命令宏定义
#define Communication_Type_GetID 0x00           //获取设备的ID和64位MCU唯一标识符
#define Communication_Type_MotionControl 0x01     //用来向主机发送控制指令
#define Communication_Type_MotorRequest 0x02    //用来向主机反馈电机运行状态
#define Communication_Type_MotorEnable 0x03        //电机使能运行
#define Communication_Type_MotorStop 0x04        //电机停止运行
#define Communication_Type_SetPosZero 0x06        //设置电机机械零位
#define Communication_Type_CanID 0x07            //更改当前电机CAN_ID
#define Communication_Type_Control_Mode 0x12
#define Communication_Type_GetSingleParameter 0x11    //读取单个参数
#define Communication_Type_SetSingleParameter 0x12    //设定单个参数
#define Communication_Type_ErrorFeedback 0x15        //故障反馈帧
//参数读取宏定义
#define Run_mode 0x7005    
#define Iq_Ref   0x7006
#define Spd_Ref  0x700A
#define Limit_Torque 0x700B
#define Cur_Kp 0x7010
#define Cur_Ki 0x7011
#define Cur_Filt_Gain 0x7014
#define Loc_Ref 0x7016
#define Limit_Spd 0x7017
#define Limit_Cur 0x7018

#define Gain_Angle 720/32767.0
#define Bias_Angle 0x8000
#define Gain_Speed 30/32767.0
#define Bias_Speed 0x8000
#define Gain_Torque 12/32767.0
#define Bias_Torque 0x8000
#define Temp_Gain   0.1

#define Motor_Error 0x00
#define Motor_OK 0X01

enum CONTROL_MODE   //控制模式定义
{
    Motion_mode = 0,//运控模式  
    Position_mode,  //位置模式
    Speed_mode,     //位置模式  
    Current_mode    //电流模式
};
enum ERROR_TAG      //错误回传对照
{
    OK                 = 0,//无故障
    BAT_LOW_ERR        = 1,//欠压故障
    OVER_CURRENT_ERR   = 2,//过流
    OVER_TEMP_ERR      = 3,//过温
    MAGNETIC_ERR       = 4,//磁编码故障
    HALL_ERR_ERR       = 5,//HALL编码故障
    NO_CALIBRATION_ERR = 6//未标定
};

typedef struct{           //小米电机结构体
    uint8_t CAN_ID;       //CAN ID
    uint8_t MCU_ID;       //MCU唯一标识符[后8位,共64位]
    float Angle;          //回传角度
    float Speed;          //回传速度
    float Torque;         //回传力矩
    float Temp;

    uint16_t set_current;
    uint16_t set_speed;
    uint16_t set_position;

    uint8_t error_code;

    float Angle_Bias;

}MI_Motor;
extern MI_Motor mi_motor[4];//预先定义四个小米电机

extern void chack_cybergear(uint8_t ID);
extern void start_cybergear(MI_Motor *Motor);
extern void stop_cybergear(MI_Motor *Motor, uint8_t clear_error);
extern void set_mode_cybergear(MI_Motor *Motor, uint8_t Mode);
extern void set_current_cybergear(MI_Motor *Motor, float Current);
extern void set_zeropos_cybergear(MI_Motor *Motor);
extern void set_CANID_cybergear(MI_Motor *Motor, uint8_t CAN_ID);
extern void init_cybergear(MI_Motor *Motor, uint8_t Can_Id, uint8_t mode);
extern void motor_controlmode(MI_Motor *Motor,float torque, float MechPosition, float speed, float kp, float kd);