尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

M3508电机速度闭环实战:CAN通信与PID整定全解析

发布时间:2026/9/29 1:37:29

资讯中心
01
ARTICLE

M3508电机速度闭环实战:CAN通信与PID整定全解析

M3508电机速度闭环实战:CAN通信与PID整定全解析
做机器人底盘第一件事往往就是让大疆M3508电机稳稳转起来。M3508通常搭配C620电调通过CAN通信下发电流指令外部再用PID做速度闭环控制这套组合在RoboMaster比赛和DIY轮式机器人里几乎成了标配。很多人一开始以为C620电调自带速度环拿到手直接给个速度值就能用实际踩过坑才知道C620内部只有电流环力矩环它接收的是电流指令而不是速度指令速度闭环必须自己在MCU里写。这篇文章就从硬件方案、CAN通信协议、速度PID实现和调参避坑四个方向把整套链路讲透适合刚接触RM电机、想快速跑通底盘驱动的朋友参考。1. 方案拆解M3508为什么需要自己做速度闭环1.1 M3508与C620的配合关系大疆M3508是一款无刷减速电机外观上比普通航模电机多了一截行星减速箱输出扭矩大、结构紧凑RoboMaster比赛里大量用在底盘和云台动力上。它本身没有驱动电路必须搭配大疆C620电调使用。C620电调负责三相逆变、FOC磁场定向控制和电流采样说得直白一点电调只负责“出力气”你告诉它出多大力电流它就去执行。这里要特别纠正一个误区C620不是一个“速度伺服”它不能直接接收“请把转速稳定在300rpm”这种指令它只能接收一个-16384到16384的电流目标值。这个值本质上是一个力矩给定正负代表方向数值大小代表力矩大小。所以速度闭环的控制链路是MCU读取电调反馈的实际转速MCU把目标转速和实际转速一起交给PID算法PID算法算出需要的电流值MCU把电流值通过CAN报文发给C620C620驱动电机输出对应的力矩也就是说速度环这部分工作被交给了用户MCU来做。这是很多新手第一次接触M3508时最容易懵掉的地方也是为什么市面上大量关于M3508的教程都在讲PID闭环。1.2 CAN总线的选型价值M3508和C620之间用CAN总线通信而不是常见的PWM信号或串口这个设计是有明确考虑的。CAN总线是差分信号抗干扰能力强机器人底盘的电机属于大电流负载启动和刹车瞬间母线电流波动非常大CAN在这种环境下比普通单端信号可靠得多。另一个优势是总线拓扑。一辆四轮底盘通常有4个M3508如果用PWM控制每个电机要单独占一路PWM和一路反馈信号MCU资源紧张而且接线繁杂。CAN只需要两根线CAN_H和CAN_L把总线串起来所有电调挂在同一条总线上再加上MCU侧的CAN收发器最多可以挂8个C620电调控制线和反馈线复用同一对双绞线整车走线瞬间清爽。CAN还自带多主机仲裁机制传输稳定常用波特率1Mbps对于底盘电机的控制周期完全够用。哪怕以后项目升级到需要更高同步精度的工业伺服场景CAN总线这套思路依然是理解EtherCAT、CANopen等高级总线的基础。1.3 控制链路与数据流向在实际项目中控制链路大概是这样的MCUSTM32或其他带CAN外设的单片机CAN收发器TJA1050、SN65HVD230等或者带隔离的CTM1051C620电调挂在CAN总线上接收电流指令M3508电机由C620驱动内部编码器信号被电调读取并打包成反馈帧C620电调会把电机的机械角度、输出轴转速、当前转矩电流、电机温度实时打包通过CAN反馈帧回传给MCU。这意味着我们不需要额外加编码器角度和速度都由电调替你算好了这在做速度闭环时非常好用——你要做的只是把反馈值解析出来交给PID。控制周期方面我一般用1kHz每1ms执行一次速度环计算和CAN帧发送这个频率对M3508的响应能力来说是足够合适的。C620电调要求MCU持续周期性发送控制帧如果一段时间没收到有效报文电调会进入保护状态停止输出这也是设计上为了防止CAN断线后电机失控。2. CAN通信协议报文结构、驱动配置与收发实现2.1 报文ID、字节序、数据映射要调通M3508先得把CAN报文格式搞清楚。C620电调挂在CAN总线上使用的是CAN标准帧11位ID。控制帧和反馈帧的ID分配是这样的场景CAN ID说明控制指令帧0x200同时控制电机1~4控制指令帧0x1FF同时控制电机5~8电机1反馈帧0x201电机1状态回传电机2反馈帧0x202电机2状态回传电机3反馈帧0x203电机3状态回传电机4反馈帧0x204电机4状态回传电机5~8反馈帧0x205~0x208对应电机5~8状态回传控制帧0x200的数据长度固定为8字节每2个字节对应一个电机的电流目标值顺序是电机1、电机2、电机3、电机4。每个电流值都是有符号16位取值范围-16384到16384对应C620的力矩输出约-20A到20A。这里要特别留意字节序DJI协议里多字节数据是高字节在前大端。比如电流值0x1234要发成0x12、0x34而不是0x34、0x12。反馈帧同样大端存储8字节数据依次是字节偏移含义数据类型范围/说明data[0]~data[1]机械角度uint16_t0~8191对应0~360度data[2]~data[3]转子转速int16_t单位rpm带符号data[4]~data[5]转矩电流int16_t与发送的电流指令对应data[6]电机温度uint8_t单位摄氏度data[7]保留0一般不用解析的时候速度值必须用int16_t接不能用uint16_t否则负转速会变成65535这样的大数PID直接就被打蒙了。2.2 STM32 HAL库的CAN初始化要点我用STM32系列跑这套方案CubeMX配置CAN外设并不复杂。主要注意几个点波特率设置为1Mbps这是C620电调的默认波特率打开CAN接收中断或者使用FIFO轮询滤波器设置为接收标准帧过滤范围尽量放开后续多电机也方便CAN收发器的TXD、RXD要接到MCU对应的CAN引脚别接反CubeMX里配置完后程序里主要是初始化CAN和滤波器。这里贴一个HAL库的典型配置片段。CAN_FilterTypeDef can_filter; can_filter.FilterIdHigh 0x0000; can_filter.FilterIdLow 0x0000; can_filter.FilterMaskIdHigh 0x0000; can_filter.FilterMaskIdLow 0x0000; can_filter.FilterFIFOAssignment CAN_RX_FIFO0; can_filter.FilterBank 0; can_filter.FilterMode CAN_FILTERMODE_IDMASK; can_filter.FilterScale CAN_FILTERSCALE_32BIT; can_filter.FilterActivation ENABLE; HAL_CAN_ConfigFilter(hcan1, can_filter); HAL_CAN_Start(hcan1); HAL_CAN_ActivateNotification(hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);把Mask全部设为0等价于不过滤任何报文所有标准帧都会进FIFO0。这个配置在前期调试阶段最方便等你把通信跑通了再考虑收紧ID过滤减轻MCU负担。硬件上还有一个经常被忽略的点CAN总线两端要各接一个120欧姆终端电阻。很多CAN收发器模块自带120欧姆电阻如果MCU端模块和电调端都有电阻可能相当于并联了两个120欧姆变成60欧姆反而会导致通信异常。最好确认一下电调内部是否已经带终端电阻避免重复接入。2.3 控制指令发送与反馈接收的代码实现发送控制帧是周期性任务我喜欢放在定时器中断里1ms一次和速度环同步。发送函数大概长这样CAN_TxHeaderTypeDef tx_header; uint8_t tx_data[8]; uint32_t tx_mailbox; void motor_send_current(int16_t m1, int16_t m2, int16_t m3, int16_t m4) { tx_header.IDE CAN_ID_STD; tx_header.RTR CAN_RTR_DATA; tx_header.DLC 8; tx_header.StdId 0x200; tx_data[0] (uint8_t)(m1 8); tx_data[1] (uint8_t)(m1 0xFF); tx_data[2] (uint8_t)(m2 8); tx_data[3] (uint8_t)(m2 0xFF); tx_data[4] (uint8_t)(m3 8); tx_data[5] (uint8_t)(m3 0xFF); tx_data[6] (uint8_t)(m4 8); tx_data[7] (uint8_t)(m4 0xFF); HAL_CAN_AddTxMessage(hcan1, tx_header, tx_data, tx_mailbox); }如果只控制一个电机m2、m3、m4传0就行。注意电流指令不要直接给到满量程-16384、16384在绝大多数项目里都用不到而且瞬间大电流冲击对机械结构很不友好。我会在代码里加一个软件限幅先把PID输出限制在-10000到10000之间实际跑起来底盘也已经很有劲了。接收反馈帧在CAN接收中断回调里处理解析逻辑如下void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rx_header; uint8_t rx_data[8]; if (hcan-Instance ! CAN1) { return; } HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rx_header, rx_data); switch (rx_header.StdId) { case 0x201: motor[0].angle (uint16_t)((rx_data[0] 8) | rx_data[1]); motor[0].speed (int16_t)((rx_data[2] 8) | rx_data[3]); motor[0].torque (int16_t)((rx_data[4] 8) | rx_data[5]); motor[0].temperature rx_data[6]; break; case 0x202: // 电机2的解析 break; // 后面类推 } }有几个细节要提醒。第一CAN中断回调里不要做耗时操作比如 printf否则中断嵌套容易丢帧调试阶段可以把数据存到全局变量在主循环里统一输出。第二速度值一定要用int16_t接接成uint16_t会出现负数变成65536以上大数的问题。第三角度和速度都是大端解析公式就是高字节左移8位或低字节如果你做出来数值乱跳优先检查字节序。关于“掉线保护”这是C620非常关键的一个机制。电调上电后会一直等待CAN控制帧如果超过一定时间没有收到有效报文输出会直接关断且蜂鸣器报警。所以哪怕电机不需要转也要周期发送一个电流值为0的控制帧。很多项目在启动阶段只发了一帧指令然后电机转了一下就停了多半就是掉线保护触发了。3. 速度PID闭环从公式到可运行代码3.1 位置式PID与增量式PID的取舍PID这个东西入门不难但电机速度闭环里选哪种形式还是有讲究的。位置式PID输出的是控制量的绝对值公式是u(t) Kp * e(t) Ki * ∫e(t)dt Kd * de(t)/dt离散化以后就是u(k) Kp * e(k) Ki * T * Σe(i) Kd * (e(k) - e(k-1)) / T增量式PID输出的则是控制量的增量Δu(k) Kp * (e(k) - e(k-1)) Ki * T * e(k) Kd * (e(k) - 2e(k-1) e(k-2)) / T实际加到电机上的值是累加u(k) u(k-1) Δu(k)。做速度闭环时我更倾向用位置式PI也就是只保留比例项和积分项不加微分。原因是转速反馈本身来自C620虽然经过电调内部滤波但依旧有一定噪声微分项会放大噪声导致电流指令剧烈跳动电机跟着抖动尤其明显。如果确实需要加微分来抑制超调一定是给微分项加低通滤波或者使用不完全微分。位置式PI的离散公式写出来就是output Kp * error Ki * integral其中 integral 每个控制周期累加 error我习惯写成 integral error * dt这样积分的单位和时间挂钩调Ki的时候数值也直观一些。3.2 限幅、抗积分饱和与输出映射写PID代码的时候最容易被忽略却最容易踩雷的两个地方是积分限幅和输出限幅。如果不做积分限幅电机长时间被堵住或者目标速度突然大幅改变时误差一直存在积分项会无限累积。等堵转解除后PID输出还停留在很大的值导致电机猛冲一下非常危险。我一般在PID结构体里放一个i_max积分每次累加完夹在[-i_max, i_max]之间。输出限幅同样重要。前面说了CAN电流指令范围是-16384到16384但实际系统完全用不到这么大的输出。输出限幅设成多少取决于负载和你对电机的保护目标我调试底盘常用-10000到10000云台会更保守一些。还有一个技巧是积分清零判断当输出已经达到限幅值的时候如果误差和输出方向一致就不让积分继续累加否则积分会使输出长期“顶死”。实现也不复杂判断输出是否饱和、误差是否同号。3.3 速度环完整代码实战我这里写一个精简但完整的速度环实现适合放在STM32的1ms定时器中断里。先定义PID结构体typedef struct { float kp; float ki; float kd; float max_integral; float max_output; float integral; float prev_error; float output; } SpeedPID; float speed_pid_update(SpeedPID *pid, float target, float actual, float dt) { float error target - actual; // 积分累加 pid-integral error * dt; // 积分限幅 if (pid-integral pid-max_integral) pid-integral pid-max_integral; if (pid-integral -pid-max_integral) pid-integral -pid-max_integral; // 位置式PI也可以加一点微分但通常不需要 pid-output pid-kp * error pid-ki * pid-integral; // 输出限幅 if (pid-output pid-max_output) pid-output pid-max_output; if (pid-output -pid-max_output) pid-output -pid-max_output; // 更新上一拍的误差 pid-prev_error error; return pid-output; }调用的时候在定时器中断里这样处理void TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM6) // 1ms中断 { float target_speed 300.0f; // 目标转速300rpm float actual_speed (float)motor[0].speed; float output speed_pid_update(speed_pid, target_speed, actual_speed, 0.001f); int16_t current (int16_t)output; motor_send_current(current, 0, 0, 0); } }这套代码加完M3508就具备最基础的速度闭环能力了。但实际跑起来会发现如果目标转速从0直接给到300rpm电机会猛加速机械冲击大电流也容易瞬间爆表。解决办法是在目标值侧加斜坡不是一步到300rpm而是每1ms加一个很小步长让期望转速以例如300rpm/s的斜率爬升到目标值。这样不仅启动平缓调PID的时候也更安全。3.4 调参技巧先P后I观察曲线PID参数整定是个实践活但有固定套路。我调速度环的顺序通常是先把I设为0只保留P从一个很小的P开始比如0.5给一个目标转速观察实际转速曲线逐步增大P直到转速开始出现轻微震荡再退回到没有明显震荡的位置然后加I消除静差从小到大调整直到转速能精确稳定在目标值简单说就是“先P后ID能不加就不加”。有人会问怎么判断P大了如果电机转起来声音发闷、转速有周期性抖动、或者用上位机画出的曲线呈等幅振荡就是P太大。如果转速始终比目标低一截且电机负载越大偏差越大那就是I不足要加I。调参时强烈建议用上位机把目标转速、实际转速、PID输出三个值可视化出来。我一般用SF串口或者VOFA这类工具在代码里通过串口把数据发出去波形一出来立刻就能看出问题比靠耳朵听靠谱得多。把1ms中断里的实际转速和PID输出打包成标准浮点通过串口以CSV格式发送上位机就能实时画图。参数的量级方面因为反馈速度单位是rpmPID输出是电流指令无量纲整数常见的底盘速度环P值大概在0.5到3之间Ki在0.01到0.1之间。但这只是参考不同机构、不同负载、不同控制周期参数差异很大最终还是要看曲线调。4. 实际调试中的坑与排查方法4.1 电机完全没有反应这种情况最常见排障顺序是电源、接线、波特率、报文周期、指令值。先确认C620电调的电源有没有到位M3508通常用24V母线供电电调上的指示灯状态可以辅助判断。然后看CAN收发器的TXD/RXD有没有接反终端电阻是否正确。接着确认波特率是不是1Mbps如果MCU侧波特率和电调不一致控制帧发出去电调根本收不到同时CAN总线上会产生错误帧。还有个容易被忽视的点C620电调上电后需要一点时间自己初始化别上电后立刻发控制帧又马上认为通信有问题。我一般让电调上电后等1到2秒蜂鸣器响过之后再开始周期发送控制帧。刚上电那段时间发帧没有意义。如果通信正常仍然电机不转先把PID输出去掉直接手动给一个固定电流值比如current2000看看电机是否动作。这一步可以快速区分问题是出在PID计算还是出在通信链路。4.2 电机抖动、发烫、噪声大电机抖动通常有几种可能。第一PID的P过大速度环输出振荡表现是高频抖动和电流噪声第二控制周期不稳定如果你把PID计算放在主循环的大循环里状态一多时间就被拉长速度环的采样周期时快时慢PID参数再好也白搭解决方法是把速度环和控制帧发送都放到固定定时器中断里第三CAN总线受到干扰导致反馈帧偶尔丢失速度值跳变PID输入信号本身不干净。发烫的问题要分情况。M3508和C620在正常工作时本来就会发热但如果在空载状态下持续发热大概率是电流指令里有明显的直流分量或者高频振荡。这时候用上位机看电流输出曲线如果曲线毛刺很多就说明PID输出在抖。一个容易被忽视的原因是编码器速度信号里的少量异常值。偶发的一帧CAN报文解析错误比如瞬时速度跳成一个大数会让PID瞬间输出一个很大的电流尖峰。保险做法是对速度反馈做一个滑动平均值滤波在PID前面加一道平滑处理。4.3 反馈转速数值乱跳如果转速曲线看起来像“毛线团”先从解析代码检查字节序看是不是把高位低位搞反了。大疆的报文是高字节在前如果你用低字节左移8位或高字节算出来的数据会特别离谱。其次检查符号位。速度是int16_t最高位是符号位转速反转时会出现负数。如果你用uint16_t解析反方向转的时候会发现速度突然变成了60000多的虚假正数PID会把它当成超高速然后反方向猛打整台车可能瞬间抽搐。这个问题我见过太多次每次排查到最后基本都是数据类型的问题。还有一点M3508反馈的速度单位是rpm如果你在速度环里直接拿它的数值和目标值比较就不要再去换算轮子线速度除非你想做轮速换算。换个轮径或者减速度后要记得同步更新目标值。4.4 长期稳定运行的几点建议如果这套系统要装到比赛车上或者长时间运行我还有一些经验值得分享。CAN隔离尽量安排上。电机的启动电流很大地线上的电位波动会直接影响通信使用带隔离的CAN收发器比如CTM1051可以避免很多偶发通信问题。别省这几十块钱的成本它比你在代码里反复排查丢失帧省时得多。控制帧只要系统上电就要持续发即使电机不转也要发全0的电流值。不要只在速度环计算后才发帧防止电调因为长时间没收到报文进入掉线保护。温度保护建议自己做一层。M3508的反馈帧里有温度数据虽然C620本身有过温保护但是MCU这边多一道判断更安全。当温度超过80度时把最大电流输出限制调小超过90度直接让电机停下来防止机械和电调损伤。最后是代码结构。控制帧发送、PID计算、串口调试输出这三个功能尽量分离开。调试输出放在低优先级主循环里不要占用定时器中断的时间。这样即便串口调试阻塞了电机速度环依然能稳定运行不会一卡一卡的。5. 写在最后我最初调通M3508速度环的时候最深刻的体会不是PID公式有多难而是CAN通信的细节决定成败。0x200控制帧、0x201反馈帧、大端字节序、掉线保护、积分限幅任何一个点没做到位现象都很迷惑。后来我把这套逻辑抽出成通用模板换成M2006电机和不同底盘结构照样能跑。如果你正准备做自己的M3508底盘我建议从一开始就把电流输出限幅和积分限幅写好别等到电机乱跳再去补。加速斜坡也可以尽早加上调试过程会安全很多。调参的时候别急着追求响应快先把稳定性做出来再一点点逼近极限。这也是我做底盘项目学到的最实在一条经验。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。