【电赛/毕设降维打击】抛弃玩具直流电机!大疆 Robomaster (M3508/M2006) CAN 总线组网与多闭环硬核避坑指南

【电赛/毕设降维打击】抛弃玩具直流电机!大疆 Robomaster (M3508/M2006) CAN 总线组网与多闭环硬核避坑指南

前言
回忆一下你做四轮小车的痛苦:4 个带编码器的直流电机,每个电机有 2 根动力线 + 4 根编码器线。为了接好 4 个电机,你的单片机引出了密密麻麻24 根杜邦线,占用了 8 个定时器通道。只要有一根线松动,小车直接原地转圈打滑。而且低速时电机抖动严重,根本跑不直。

大人,时代变了!
现在的顶级电赛队伍和 RoboMaster 赛场上,全在使用大疆的M3508 / M2006 无刷减速电机 + C620 / C610 电调
它的降维打击体现在哪里?

  1. 极简连线:4 个电机,全部并联在仅仅2 根 CAN 总线上!单片机 2 个引脚控全场!

  2. 变态精度:自带高精度磁编码器(转一圈 8192 个脉冲)和出厂校准的 FOC 矢量控制。

  3. 工业暴力:瞬间输出扭矩极大,且在 1RPM 的极低转速下依然丝滑无声。

但天下没有免费的午餐,大疆电调只接受纯 CAN 报文,且只接受电流(转矩)指令。这就要求你必须亲自手写底层的解析协议与多环 PID!本文将手把手带你降服这头性能巨兽!

@TOC


一、 硬件底盘的革命:CAN 总线多电机组网拓扑

为什么 4 个电机能共用两根线?因为 CAN 总线是一个广播网络

1. 拨码开关:赋予电机灵魂的 ID

每个 C620 / C610 电调上,都有一个极其微小的拨码开关。
在把它们连到 STM32 的 CAN_H 和 CAN_L 之前,你必须给这 4 个电调分别拨上不同的 ID(比如 1、2、3、4)。
从此以后,电调 1 的物理发送地址就是 0x201,电调 2 就是 0x202,以此类推。

2. 叹为观止的指令压缩打包术

STM32 怎么给 4 个电机发控制指令?大疆的通信协议设计得堪称艺术:
你不需要发 4 次数据,你只需要向 0x200 这个神秘的公共地址广播一帧 8 字节(Byte)的 CAN 报文!

  • Byte 0, Byte 1:控制电机 1的电流大小(16位有符号整数)。

  • Byte 2, Byte 3:控制电机 2的电流大小。

  • Byte 4, Byte 5:控制电机 3的电流大小。

  • Byte 6, Byte 7:控制电机 4的电流大小。

STM32 高效发送 C 语言源码:

codeC

// 定义 CAN 发送报文的句柄和缓冲 CAN_TxHeaderTypeDef TxMessage; uint8_t TxData[8]; uint32_t pTxMailbox; /** * @brief 同时控制 4 个大疆电机的底层电流 * @param iq1, iq2, iq3, iq4: 四个电机的电流期望值 (-16384 ~ 16384) */ void DJI_Motor_Send_Current(int16_t iq1, int16_t iq2, int16_t iq3, int16_t iq4) { TxMessage.StdId = 0x200; // 前 4 个电机的公共控制 ID TxMessage.IDE = CAN_ID_STD; TxMessage.RTR = CAN_RTR_DATA; TxMessage.DLC = 8; // 数据长度 8 个字节 // 高级位操作:把 16位 拆成两个 8位 塞进数组 TxData[0] = (uint8_t)(iq1 >> 8); TxData[1] = (uint8_t)iq1; TxData[2] = (uint8_t)(iq2 >> 8); TxData[3] = (uint8_t)iq2; TxData[4] = (uint8_t)(iq3 >> 8); TxData[5] = (uint8_t)iq3; TxData[6] = (uint8_t)(iq4 >> 8); TxData[7] = (uint8_t)iq4; // 调用 HAL 库将报文打入 CAN 邮箱,硬件会自动广播给 4 个电调! HAL_CAN_AddTxMessage(&hcan1, &TxMessage, TxData, &pTxMailbox); }

威力:只要调用这个函数,总线上的 4 个电调会瞬间抓取属于自己的那两个字节去执行,四个电机的动作做到了绝对的、纳秒级的“同频共振”!这对于麦克纳姆轮的运动学逆解来说,是极其致命的优势!


二、 接收反馈:面向对象(OOP)的多电机数据解析

电机在转的时候,会以 1000Hz 的频率向 STM32 疯狂回传它的当前状态。
电调 1 回传的 ID 是 0x201,电调 2 是 0x202。回传的 8 字节包含:机械角度、当前转速(RPM)、实际转矩电流、电机温度

🚨 避坑:不要写面条代码!使用结构体数组管理

如果有 4 个电机,千万不要定义 16 个全局变量!用结构体完美封装它们!

codeC

// 1. 定义大疆电机的“类” typedef struct { uint16_t angle; // 原始机械角度 (0~8191) int16_t speed_rpm; // 当前转速 (转/分钟) int16_t real_current;// 实际电流 uint8_t temperature; // 电机温度 // 用于处理越界问题的连续变量 (见后文黑科技) int32_t total_angle; // 累计总角度 uint16_t last_angle; // 上次机械角度 int32_t round_cnt; // 转过的圈数 } DJI_Motor_t; // 2. 实例化 4 个电机对象! DJI_Motor_t Chassis_Motors[4]; // 3. 在 CAN 接收 FIFO 挂号中断中极速解析! void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef RxHeader; uint8_t RxData[8]; HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &RxHeader, RxData); // 检查 ID,0x201 对应数组索引 0,以此类推 if(RxHeader.StdId >= 0x201 && RxHeader.StdId <= 0x204) { uint8_t i = RxHeader.StdId - 0x201; // 计算索引 0~3 // 记录上次角度 (用于算总角度) Chassis_Motors[i].last_angle = Chassis_Motors[i].angle; // 解析报文 (高位在前,低位在后) Chassis_Motors[i].angle = (RxData[0] << 8) | RxData[1]; Chassis_Motors[i].speed_rpm = (RxData[2] << 8) | RxData[3]; Chassis_Motors[i].real_current = (RxData[4] << 8) | RxData[5]; Chassis_Motors[i].temperature = RxData[6]; // 调用神级“越界处理”算法更新总角度... DJI_Motor_Update_Angle(&Chassis_Motors[i]); } }

三、 史诗级暗坑爆发:编码器 8191 越界疯转现象

灾难现场:你给电机写了一个“位置环 PID”,让它转到 角度 10000 的位置。
大疆的磁编码器,转一圈输出的值是 0 ~ 8191。
当电机正转,角度从 8190 -> 8191 后,再往前走一步,角度会瞬间突变回 0!
此时,你的位置环 PID 计算误差:10000 - 0 = 10000,单片机以为电机离目标还差十万八千里,于是疯狂输出最大电流。
结果就是:你的机械臂或云台在越过零点的瞬间,像疯狗一样反向狂转,直接把线扯断,砸碎实验室的设备!

🏆 核心黑科技:解包“连续总角度”

我们必须自己写一段逻辑,识别出这种 8191 -> 0 或者 0 -> 8191 的突变,并把它转化为一个无限累加(不会突变)的总角度

必须抄进工程的防越界算法(只需判断差值):

codeC

void DJI_Motor_Update_Angle(DJI_Motor_t *motor) { // 1. 计算本次与上次的角度差值 int16_t diff = motor->angle - motor->last_angle; // 2. 判断越界方向!(8192的一半是4096) if (diff < -4096) { // 如果两次采样差值小于 -4096 (比如 8190 突变到 10,差值 -8180) // 实际上它是【正转】越过了零点!圈数 + 1 motor->round_cnt++; } else if (diff > 4096) { // 如果差值大于 4096 (比如 10 突变到 8190,差值 8180) // 实际上它是【反转】越过了零点!圈数 - 1 motor->round_cnt--; } // 3. 计算真实的无越界连续总角度! // 总角度 = 圈数 * 8192 + 当前机械角度 motor->total_angle = motor->round_cnt * 8192 + motor->angle; }

破局!经过这个算法过滤后,你的 total_angle 将变成一个极其连续的数字(比如正转三圈就是 24576)。你把这个连续的数字喂给位置环 PID,云台跨越零点时将如丝般顺滑,再无暴走风险!


四、 驯服猛兽:速度环 PID 的绝对禁忌

大疆电调内部只做了一件事:电流闭环(也就是力矩控制)
你通过 CAN 发过去的数据不是 PWM,也不是速度,而是**“电流期望值”**!
如果你想让小车以 300 RPM 的速度前进,你必须在 STM32 里面自己写一个速度环 PID

🚨 避坑:电机的力矩极其暴力,严禁裸跑!

M3508 的极限电流极大。如果你只写了一个简单的 PID,一旦 P 给大了,电机瞬间爆发最大扭矩,如果轮子被卡住,内部的减速齿轮会直接“咔嚓”扫齿报废(一个电机大几百块钱!)。

🏆 工业级速度环重构:带限幅的增量式/位置式 PID

在给大疆电机写 PID 时,总输出(电流)绝对、必须进行强制限幅

codeC

// 假设这是运行在 5ms 定时器里的底盘控制任务 void Chassis_Task(void) { for(int i = 0; i < 4; i++) { // 1. 目标速度 (比如 300 RPM) float target_speed = 300.0f; // 2. 从对象中取出大疆回传的真实转速 float current_speed = Chassis_Motors[i].speed_rpm; // 3. 经过速度环 PID 计算期望电流 float out_current = PID_Calc(&Speed_PID[i], target_speed, current_speed); // 4. 【保命操作】输出限幅! // C620电调允许的最大电流值是 16384,但平时切忌拉满! // 限制在 8000 已经极其暴力,足够推着几十斤的车满地跑了。 if(out_current > 8000.0f) out_current = 8000.0f; if(out_current < -8000.0f) out_current = -8000.0f; // 5. 保存到发送缓冲区 Tx_Current_Buf[i] = (int16_t)out_current; } // 6. 一键打包,通过 CAN 总线同时点火 4 个电机! DJI_Motor_Send_Current(Tx_Current_Buf[0], Tx_Current_Buf[1], Tx_Current_Buf[2], Tx_Current_Buf[3]); }

结语

从杂乱无章的杜邦线和低效的 L298N,升级到只有 CAN_H 和 CAN_L 的差分总线;从开环的盲目输出,升级到毫秒级的电流、速度、位置多环联动。

驾驭大疆 M3508 / M2006 系列电机,是每一个嵌入式控制工程师迈向“工业级复杂机电系统”的成人礼。
理解了 CAN 报文的高效拼接,看懂了 8191 越界的底层逻辑,亲手用 PID 锁死了转速,你就会发现:机械并不冰冷,在优雅的代码调度下,它们能像肌肉一样瞬间爆发,也能像抚摸羽毛一样极致轻柔。

预祝各位挑战高级机器人题目的硬核玩家:CAN 节点永不掉线,编码器丝滑过渡,总线指哪打哪,带着大疆动力系统碾压国奖现场!🏆