小米电机CyberGear STM32HAL 使用指南
在23年8月底 小米正式发售了用于其铁蛋2代的小米“微电机”,准确来说就是目前机器人方向流行的关节电机。根据其参数可知,在同等重量下,小米此款电机不仅在额定扭矩上达到了4NM,峰值扭矩达到了12NM的水平,同时在价格上也基本上算是全网最低。笔者也是通过预购,在发售之初拿到了“年轻人的第一个微电机”。故想发出此文,和各位一起,通过最简单,最流行的硬件,完成对电机的基础控制。
硬件介绍
由于小米电机采用了当前在关节电机行业上比较流行的TX30 2+2的接口,通过一个接口就可以同时完成信号和供电的传输,使得线路连接非常简单,且几乎不存在反插问题(CAN信号线需要额外注意与控制板的连接顺序)。但是此种连接方式还是有些细节方面的问题,比如插头连接松动,端子容易虚焊或损坏,以及价格偏贵等问题,故在使用此种插头时,一定要确保连接可靠,同时can信号线最好需要严格双绞以保证型号传输的稳定性。
主控方面,笔者采用的是自己设计的用来做其他项目的F103C6T6主控,包含了采用VP230的can通信硬件电路,故使用此板作为电机控制实验的主控板,其中CAN接口采用2.54 3P的接口。
至此,完成控制电路的硬件连接。
软件编写
新建一个工程,采用CUBEMX进行工程建立,极大地提高了效率,降低了学习成本。
选择了对应芯片之后,完成基础配置:外置晶振,DEBUG,UART,TIM2定时器(2ms中断)以及CAN通信:
时钟配置(8MHz外置晶振,主控倍频至72MHz)
can配置(1M波特率,CAN接收中断)
uart配置(115200波特率,DMA接收中断)
TIM配置(2ms定时中断)
生成工程
其他
自行配置LED GPIO等 方便调试
首次生成工程编译通过之后,进行系统初始化:
/* USER CODE BEGIN 2 */ //CAN过滤器初始化 CAN_FilterTypeDef can_filter_st; can_filter_st.FilterActivation = ENABLE; can_filter_st.FilterMode = CAN_FILTERMODE_IDMASK; can_filter_st.FilterScale = CAN_FILTERSCALE_32BIT; can_filter_st.FilterIdHigh = 0x0000; can_filter_st.FilterIdLow = 0x0000; can_filter_st.FilterMaskIdHigh = 0x0000; can_filter_st.FilterMaskIdLow = 0x0000; can_filter_st.FilterBank = 0; can_filter_st.FilterFIFOAssignment = CAN_RX_FIFO0; can_filter_st.SlaveStartFilterBank = 14; HAL_CAN_ConfigFilter(&hcan, &can_filter_st); HAL_CAN_Start(&hcan); HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING); HAL_TIM_Base_Start_IT(&htim2);//定时中断初始化 /* USER CODE END 2 */
导入笔者编写的cybergear.c/.h文件,并加入编译目录中(不会的话移步其他人的基础教程)
至此,完成控制电机的全部准备工作。
控制电机:
对于控制电机的命令,大致分为三种:
- 启停控制: 需要让电机运动起来,首先需通过命令要开启电机,电机开启之后会有轻微震动。
- 运动控制: 使用运控指令完成对电机的控制,一般将其放入一个中断内定时调用,以期精细控制并及时得到回传数据。
- 数据读取: 一般用于更改电机的参数,一般在首次使用或者更换使用场景,调整电机性能时对电机使用。
调试电机:
1.调用初始化函数,设置电机工作模式,使能电机
/* USER CODE BEGIN 2 */ HAL_Delay(500); //一定时间延时 等待电机初始化完成 init_cybergear(&mi_motor[0], 0x7F, Motion_mode);//小米电机 启动! /* USER CODE END 2 */
2.调用运动控制函数(放入主循环或者中断中),控制电机转动
/* USER CODE BEGIN 3 */ motor_controlmode(&mi_motor[0], 0, 0, 0, 0 , 0); HAL_Delay(2); /* USER CODE END 3 */
运控函数使用可通过官方说明书给出的传递函数作为参考,完成对电机的控制。
其中,有三种典型控制方法:
- 力矩控制:仅输入力矩,其他均为0,即为纯力矩模式,单位为NM,注意在无负载情况下,很容易达到最大转速;
- 速度控制:输入期望速度,kd,其他均为0,即为速度控制模式,其中kd大小影响其响应速度,一般可取0.1-1;
- 位置控制:输入期望位置,kp,kd,其他均为0,即为位置控制模式,其中kp大小影响响应速度(到达位置快慢),kd大小影响着电机阻尼,过小会震荡,过大电机会震动明显。kp一般1-10,kd一般0.5左右。
- 其他控制方式就有待广大爱好者发掘,这里不再深究。
踩坑:
由于笔者在首次调试的时候没有官方can转usb调试器,无法通过官方上位机调试故只能盲调,控制电机几经失败后发现小米电机出厂ID为极其阴间的0X7F,其他踩坑暂无。
最后:
感谢广大群友相助,在此为两大关节电机群打个广告:
174204312 达妙电机交流群
869911140 大然电机交流群
特别的,借鉴并参考了乌苏哥写的小米电机STM32库函数
本文也是笔者首次编写,如有不足,错误之处,烦请各位大佬指正。
附上代码
cybergear.c
/** ****************************(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" 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(&hcan, &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: 控制电机CAN_ID【出厂默认0x7F】 * @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_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin); //LED闪烁指示 HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &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; } }
cybergear.h
/** ****************************(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);