简介:面向需快速上手大疆动力系统的嵌入式开发者,这是一份基于正点原子探索者STM32F407的完整CAN通信控制工程,实现对大疆C610电调与M2006电机的驱动控制,可广泛应用于机器人竞赛、智能车底盘、云台动力等场景。工程内部分层清晰,包含标准外设库与硬件驱动源码,如定时器、ADC、LCD显示、FLASH读写等模块;Keil工程文件与链接脚本可直接打开编译,同时提供hex烧录文件供快速验证,htm配置说明与txt记录辅助核对参数。整个压缩包仅4.58MB,共187个文件,以49个c文件、52个h文件为主体源码,搭配o目标文件、crf编译依赖文件以及uvprojx工程文件,无论初学者按模块学习还是进阶者二次开发都很合适。目前已有3759人学习下载,对于需要掌握C610电调与M2006电机完整调试流程的读者具有直接参考价值。
1. STM32F407 驱动 C610 电调:把 M2006 电机从能转调到可控
做服务机器人底盘、桌面机械臂还是轮足小车,只要想用无刷电机,前几轮调试大概率绕不开 STM32F407、C610 电调和 M2006 电机这套组合。C610 电调把 FOC 算法封在内部,通过 CAN 总线收 8 字节数据就能驱动电机,M2006 自带磁编码器,角度、速度、电流都能读回来,看起来上电就能跑,真动手时卡在 CAN 初始化、报文字节序和电源保护上的新手不在少数。这份 .rar 里的东西正好是把三者串起来的完整路径:CubeMX 配置、CAN 发送接收、速度闭环 PID 和调参记录。照着移植一遍,能把“转起来”变成“控得住”。适合刚接手 C610 电调配 M2006 的开发人员,也适合给老工程补 CAN 驱动的熟手参考。
2. C610 电调与 M2006 电机的硬件基础:24V 供电、接口定义和总线选型
2.1 C610 电调关键参数:电压范围、持续电流与接线端子
先聊硬件。很多人在接触 C610 电调之前,先把电调想成航模电调,觉得给一路 PWM 就能转,这是第一个需要扭转的认知。C610 电调没有独立的油门信号线,输入侧只有电源正负、CAN 总线和三相电机线。官方参数里额定电压是 24V,持续电流能做到 10A,对于 M2006 这种额定扭矩只有 0.3N·m 级别的小电机来说,余量相当充足。
接线顺序有讲究。电源线选线径粗一点的,CAN_H 和 CAN_L 用双绞线,地线一定和电源地共地。电机端子 U/V/W 和 M2006 的三根相线一一对应,相序接错,FOC 角度就和实际转子位置对不上,轻则转不动,重则电流保护。上电前,我习惯在电调电源两端并一只 470μF 的电解电容,位置尽量靠近电调本体,这个电容用来吸收启动瞬间的母线反冲,能减少很多奇怪的过流保护。如果你用的电源是那种标称 24V 但动态响应很差的开关电源,不加电容基本没法跑到满电流。
接线完成后别急着上电,花两分钟过一遍检查清单:万用表量电源输入端有没有短路;CAN_H 和 CAN_L 之间电阻是否接近 60Ω(两端各一个 120Ω 终端电阻并联的结果);三根电机相线之间阻值是否平衡。这里经常翻车的是终端电阻,一块板子直连电调时,有的开发板板上已经带了 120Ω 电阻,你再外接一个就等于并联成 60Ω,波形会变形;不带电阻的板子则要注意,CAN 通信距离超过 20cm 就必须在终端补匹配。
一个隐蔽的坑在收发器芯片上。如果板子用的收发器带 STBY 或 RS 引脚,这个引脚必须拉到正常模式,否则上电后 CAN 一直处于只听不发状态。我遇到过两次“代码完全正常但电调没反应”的情况,最后都是这个引脚悬空造成的。
2.2 M2006 电机特性:减速比、额定转速与编码器
M2006 是一个无刷电机加上行星减速箱的组合体。输出轴额定转速在 740rpm 附近,减速比大约 36:1,意味着电机转子实际转速远高于输出轴。这个参数直接决定你在速度闭环里怎么选量纲:如果你把 740rpm 当成目标转速去推电流指令,必须区分这是输出轴侧还是转子侧,两边差 36 倍,PID 增益完全不在一个数量级。
电机内置霍尔磁编码器,一圈对应 8191 个码。反馈报文里返回的就是这个 13 位角度值。它有个好处是绝对位置,断电再上电角度值不丢失;但也有麻烦的地方:绝对编码器一样要面对“零点在哪儿”的问题。机械装配时如果没记录零点位置,位置闭环根本没法做,只能上电后手动找零。
扭矩边界也要心里有数。M2006 额定扭矩约 0.3N·m,短时峰值能到 1.2N·m 左右。换算到电流,我一般按经验值保守处理:持续运行电流不超过 3A,加速瞬间不超过 6A。没有电流计就不要硬拉极限,M2006 减速箱是尼龙材质,堵转时间长了轻则齿轮磨损,重则扫齿,等听到异响就来不及了。
2.3 总线选型:CAN 比 PWM 合适在哪
总有人问,能不能用 PWM 直接驱动?C610 不是不能,是它在 PWM 模式下失去了一半能力。PWM 只能单向传达油门占空比,M2006 的编码器、电流、温度数据全都要靠另一条线回传,PWM 方案做不了;其次,PWM 每路占一个定时器通道,带 4 个电机就要占 4 个 IO,而且多数板子上的高级定时器通道数量有限,资源很紧张。CAN 总线只需要一对差分线,挂电调数量上限高,报文自带 CRC 校验,抗干扰也更好。
所以结论是:给多电机、带反馈、要闭环的场合,CAN 总线是这套组合的默认选项。STM32F407 自带两组 CAN 外设,不需要额外接控制器,一个收发器芯片就能直连电调。学习阶段不用纠结,直接按 1Mbps 的波特率走,电调、电机、开发板三件套按同一份接线图接好,后面所有代码都建立在这个物理链路上。
3. CAN 协议解析:0x200 控制帧与 0x211 反馈帧的地址、字节和时序
3.1 电流控制帧 0x200:字节分配与数值范围
C610 电调的协议很紧凑:一条 8 字节标准帧可以同时控制 4 个电调,每个电调占 2 字节,存的是一个有符号电流指令,常用范围是 -1000 到 1000。这里要强调,这个数值和实际安培数不是简单的一比一关系,不同资料里给出的换算系数也不太一样,所以我的做法是先把这个数当成“归一化电流指令”,调试时从 ±100 起步,确认转向和响应再往上加。直接给到 ±1000,M2006 这种小电机会瞬间进入过流保护。
打包发送时,字节顺序很容易踩坑。官方协议里 16 位数据按高字节在前送出,以下是我在项目里稳定跑过一段时间的封包函数。
/* motor_current[4] 存放四个电调的电流指令,范围 -1000 ~ 1000 */ uint8_t can_tx_data[8] = {0}; CAN_TxHeaderTypeDef tx_header; for (uint8_t i = 0; i < 4; i++) { uint16_t val = (uint16_t)(motor_current[i] & 0xFFFF); can_tx_data[i * 2] = (uint8_t)(val >> 8); /* 高字节在前 */ can_tx_data[i * 2 + 1] = (uint8_t)(val & 0xFF); /* 低字节在后 */ } tx_header.StdId = 0x200; tx_header.IDE = CAN_ID_STD; tx_header.RTR = CAN_RTR_DATA; tx_header.DLC = 8; HAL_CAN_AddTxMessage(&hcan1, &tx_header, can_tx_data, &tx_mailbox);这段代码里有两处不能省:一是 can_tx_data 要清零,未用到的通道如果不处理会残留上一次的随机值,电调会把残留值当成电流指令,表现成电机自己抖动;二是 DLC 必须写成 8,C610 按固定 8 字节帧解析,短帧会被直接丢弃。HAL_CAN_AddTxMessage 的返回值也要检查,返回 HAL_BUSY 说明发送邮箱满了,说明你的发送频率超过 CAN 实际带宽,这时要先降频而不是加大缓冲。
3.2 反馈帧 0x211~0x218:角度、转速与电流解析
每个 C610 电调会以 0x211 加上电调编号减 1 的 ID 周期回传一帧数据,也就是第一个电调回 0x211,第二个回 0x212,依此类推。8 字节里前 6 个字节是核心:13 位角度、转速、母线电流,后面字节还包含温度和电压信息,不同固件版本字段略有差异,以你手上这份工程实际验证为准。
接收解析的代码要特别注意字节序,官方帧同样按高字节在前送出。以下是我用的解析方式。
typedef struct { volatile uint16_t raw_angle; /* 0 ~ 8191 */ volatile int16_t speed_rpm; /* 转速,单位 rpm */ volatile int16_t torque_cur; /* 电流反馈,有符号 */ volatile uint8_t temp; /* 电调温度 */ } m2006_fb_t; m2006_fb_t m2006_feedback; void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rx_header; uint8_t rx_data[8]; if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rx_header, rx_data) != HAL_OK) return; if (rx_header.StdId == 0x211) { m2006_feedback.raw_angle = (uint16_t)((rx_data[0] << 8) | rx_data[1]); m2006_feedback.speed_rpm = (int16_t)((rx_data[2] << 8) | rx_data[3]); m2006_feedback.torque_cur = (int16_t)((rx_data[4] << 8) | rx_data[5]); m2006_feedback.temp = rx_data[6]; } }角度 0~8191 对应一圈 360°,每个码约 0.044°,做完位置闭环后要用这个比例换算成弧度或度。转速单位是 rpm,出现负值代表反转。解析时最怕字节序写反,如果你在一份例程里看到用 rx_data[0] | (rx_data[1] << 8) 的低字节在前写法,不要急着照抄,先用手拨动电机输出轴,串口打印原始十六进制数据,确认角度值递增方向再说。反馈帧和电流指令的字节序必须一致,否则 PID 读到的永远是一个来回跳的坏数。
3.3 帧时序与 1kHz 控制节律:为什么不能想发就发
C610 电调内部跑的是 FOC 算法,官方推荐的控制周期是 1ms,也就是 1kHz。这个周期既影响控制带宽,也影响电调的在线检测机制:电调会统计接收帧的间隔,如果发现超过一定时间没收到新指令,就进入跛行保护状态,电机输出归零,必须重新收到有效帧才能恢复。
实测把周期放到 2~5ms 还能转,但一旦超过 10ms,电调基本就会判定丢线。听起来 10ms 很宽松,可如果你在 while 循环里随手调用发送函数,中间夹着串口打印、OLED 刷新、浮点运算,某次循环超过 10ms 很正常。我的做法是把发送放到定时器中断或者系统调度器的一个 1ms 周期任务里,优先级拉高,其他逻辑一律不准阻塞它。
还有一个容易忽略的点:反馈帧的更新速率和控制指令的发送速率需要保持一致节奏。如果你 1ms 发一次指令,但只能在主循环里偶然读到反馈,PID 计算就可能用了 5ms 前的旧数据,增益稍微调大就会振荡。更合理的结构是在定时器中断里读最新反馈,同步算 PID,算完立刻发 0x200,整个链路在同一时间基准上。
4. 基于 STM32F407 的 CAN 驱动与速度闭环:从 CubeMX 到 PID 输出
4.1 CubeMX 配置:CAN1 引脚、时钟和波特率
这一步按下图配置就能跑起来。先把时钟树拉满,F407 的 APB1 时钟 42MHz,CAN 外设时钟就来自这里。然后开 CAN1,引脚我习惯选 PB8 做 RX、PB9 做 TX,这个组合在绝大多数开发板上都做了直连收发器,不用改板子。
| 配置项 | 我用的值 | 说明 |
|---|---|---|
| CAN1 引脚 | PB8 / PB9 | RX 在前,TX 在后 |
| 工作模式 | Normal | 调试时可以先选 Loopback |
| APB1 时钟 | 42 MHz | 以实际时钟树为准 |
| 波特率 | 1 Mbps | C610 常用速率 |
| Prescaler | 3 | 42MHz / 3 = 14MHz |
| BS1 / BS2 | 6 / 7 Tq | 加上 1 Tq 同步段共 14 Tq |
波特率这段最容易在数字上翻车。CAN 位时间由同步段、BS1、BS2 组成,上面这组参数算出来是 14MHz 除以 14 个时间量子,正好 1MHz。不同开发板的晶振频率不同,Prescaler 和段配置也要跟着变,所以不要死记我给的数,以 CubeMX 波特率计算窗口里显示 1.0 Mbps 为准。
初始化代码不用手写,CubeMX 生成后会自动调用 MX_CAN1_Init()。这里有个习惯要养成:生成完后打开 can.c 看一眼,确认 CAN 时钟确实来自 APB1,并且波特率寄存器算出来的值和你预期一致。以前遇过有人把 CAN1 的时钟源改成了 APB2,波特率变成两倍,电调完全不理你,查了半天都是配置问题。
4.2 过滤器配置与启动:让反馈帧能进来
生成完代码后,还需要手动补一段过滤器配置。默认生成的代码只做了初始化,CAN 外设并没有启动,也不会往 FIFO 里收任何帧。我的做法是接收全部标准帧,然后在回调里按 ID 分流,代码量最小。
CAN_FilterTypeDef filter; filter.FilterIdHigh = 0x0000; filter.FilterIdLow = 0x0000; filter.FilterMaskIdHigh = 0x0000; filter.FilterMaskIdLow = 0x0000; filter.FilterFIFOAssignment = CAN_FILTER_FIFO0; filter.FilterBank = 0; filter.FilterMode = CAN_FILTERMODE_IDMASK; filter.FilterActivation = ENABLE; HAL_CAN_ConfigFilter(&hcan1, &filter); HAL_CAN_Start(&hcan1); HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);过滤掩码全零的意思是“不关心 ID 任何位”,所有帧都进 FIFO0。好处是省心,坏处是中断里得自己判断 ID,否则一串电调都接在总线上时,0x211 到 0x218 的帧会在同一个回调里挤成一团,你用 0x211 的解析逻辑去读 0x212 的帧,角度、转速全是错位数据。
4.3 速度闭环 PID:从反馈到电流指令的换算
这部分是核心。速度环的目标是让电机实际转速跟上设定转速,PID 输出作为电流指令,经 0x200 发出。我给出一份能直接落地的位置式 PID 结构。
typedef struct { float kp, ki, kd; float integral; float last_error; float output_max; } pid_t; float pid_calc(pid_t *pid, float target, float actual, float dt) { float error = target - actual; pid->integral += error * dt; if (pid->integral > pid->output_max) pid->integral = pid->output_max; if (pid->integral < -pid->output_max) pid->integral = -pid->output_max; float output = pid->kp * error + pid->ki * pid->integral + pid->kd * (error - pid->last_error) / dt; pid->last_error = error; if (output > pid->output_max) output = pid->output_max; if (output < -pid->output_max) output = -pid->output_max; return output; }调用时 dt 必须和发送周期一致。如果 1ms 发一次,dt 就是 0.001f,并且在中断里调用,不要在主循环里用累计时间。初始参数我给一组安全值:kp=0.3,ki=0.02,kd=0,output_max=500。这几个数对 M2006 这种小惯量电机不会一上来就飞,但不同负载下差别很大,必须实测调整。
积分限幅一定要做。电机在启动瞬间误差很大,积分项会迅速饱和,等你把转速拉到目标附近,积分还在顶着输出,超调就来了。限幅我直接复用输出上限,简单有效。微分项对噪声敏感,M2006 的转速反馈在低速时量化误差明显,kd 先给 0,等 P 和 I 调收敛了再根据振荡情况决定要不要加。
4.4 完整控制链路:开环标定到闭环切换
拿到电机先别闭环,做一次开环标定:给一个固定电流指令,比如 100,让电机空转,记录转速反馈,确认极性是否正确。如果给正电流电机反而反转,说明相序或者角度方向有误,这时候上闭环只会越调越乱。
确认开环正常后,把开环转速作为目标,切到速度环。切换时要注意 PID 积分清零,否则残留积分会让电机在切换瞬间猛冲一下。我一般在状态机里维护当前模式,开环标定完成后再使能闭环任务,中间留一个 100ms 过渡,电机从零转速开始跟踪目标。
5. 避坑指南:C610 与 M2006 调试中五个高频现场
5.1 现象:上电后电机嗒响一声,但发指令怎么都不转
这是最常见的第一坑。电机上电响一声是电调在给电机励磁并做转子位置检测,这个阶段一般持续 1 到 2 秒,期间电调不响应 0x200 指令,也不回反馈帧。很多人上电后立刻发指令,发现没反应,就开始怀疑代码。
原因分两类:一是没等电调完成初始化;二是 CAN 收发器处于只听模式,电调收到了数据但发不出反馈,你的代码又一直死等反馈帧,双方便卡住了。解决办法是上电后延时 800ms 再发第一帧,同时用串口打印发送错误码,确认没有 HAL_CAN_ERROR_TIMEOUT 之类的问题。我每次上电都会强制等待电调回第一帧 0x211,收到才进入控制逻辑,这样能避开很多无效调试。
提示:等反馈帧要在主循环里做超时计数,不要在中断里死等。CAN 总线上如果只有电调一个节点在线,接收不到是正常的,超时后把错误状态打出来更有用。
5.2 现象:一给电流指令就触发电源过流保护
现象是代码一跑,电源指示灯闪,电流瞬间到顶,电调红灯亮,电机像被卡住一样嗡一声不动。原因多半不是你电源不够,而是电流指令给太大,或者电机相序接反。
解决分三步走:第一步把电流指令降到 50,看电机是否缓慢转动;第二步逐步增加到 100、200,每档确认转向和声音;第三步如果 50 都过流,直接交换任意两根电机相线再试。另外要确认你用的电流指令范围到底是 -1000~1000 还是 -16384~16384,两个范围差出 16 倍,很多移植过来的工程死在这里。
5.3 现象:电机转几圈后急停,电调红灯闪烁
电机能转,说明通信链路没问题,转几圈急停,是电调触发了丢帧保护。C610 要求控制指令持续到达,一旦超过内部阈值,它认为主机离线,就会主动切断输出。红灯闪烁就是在提示“我没收到新指令”。
常见原因是你调试时打了断点,或者主循环里插了串口打印、浮点运算,某次循环超过 10ms。解决思路是把 0x200 发送放进独立的定时器中断,周期写死 1ms,并且确保这个中断不被其他代码关闭。我后来在项目里就是用定时器中断做发送,主循环随便停,电机都不会因为丢帧保护停下来。
5.4 现象:转速反馈来回跳,PID 输出振荡
M2006 的转速反馈来自编码器差分,低速时量化步长明显,PID 拿到的实际转速本身就带噪声。如果你把 kd 调得偏大,微分会把噪声放大,最终表现成电流指令高频抖动,电调发烫,电机嗡嗡响。
我踩过一轮后的处理办法是给速度反馈加一阶低通滤波,时间常数取 10ms。滤波放在中断里,只对 speed_rpm 做一次加权平均,再送进 PID。解决后转速波形平滑很多,kd 也能从 0 慢慢加上去。滤晚了就从 2ms 开始试,但效果没有 10ms 明显。
5.5 现象:角度反馈跳变,位置闭环没法回零
角度反馈跳变,优先怀疑字节序。13 位角度拆成两个字节传输,高位在前和低位在前是两种完全不同的解析结果,用错会让角度在 0 和 8191 之间乱跳。其次怀疑 CAN FIFO 溢出,中断处理时间太长,新帧覆盖旧帧,读出来就是半个新半个旧的数据拼出来的畸形值。
解决方法是先打印反馈帧的原始 8 字节,用手匀速转动电机输出轴,确认哪个字节在变化、变化方向是否一致。确认字节序正确后,把接收回调缩短成只拷贝数据,不做任何浮点运算,浮点和过滤全部放到主循环。如果还跳帧,就给 FIFO 加一个软件队列,每帧进来排一次队,消费端慢慢取。
6. 验证闭环的一个土办法:贴胶带、看串口、别只信眼睛
没有示波器和测功机,也能把速度环调到一个能用的状态,核心工具是胶带和串口。
第一步,在 M2006 输出轴上贴一条长胶带,做成一个明显的“指针”。设目标转速 100rpm,让电机空转。你先用眼睛看胶带转得是不是平稳,然后用手轻轻捏住胶带末端施加阻力,同时盯住串口打印的反馈转速。如果施加阻力后反馈转速下降,撤掉阻力又回到 100rpm 附近,说明速度环有基本收敛能力。如果一捏就掉,说明 kp 太小;如果捏住后转速来回冲,说明 kp 偏大。
第二步,验证角度反馈。设目标转速为 0,手动缓慢转动输出轴,每转 10° 左右停一下,看串口打印的角度值是否单调递增或递减,并折算回 8191 范围内没有跳变。这一步能同时验证 13 位角度解析和字节序,比任何上位机都直观。
第三步才动 PID。顺序是:先只加 P,从 kp=0.3 开始,每次乘 1.5,找到能回到目标值但开始轻微来回抖的临界点,再取临界点的 60% 作为 kp。然后加 I,ki 取 kp 的十分之一,让静差消失。最后才考虑 d,kd 从 kp 的二十分之一起试,如果加了 d 反而更吵,就果断保持 kd=0。整组参数在 load 固定的情况下,收敛时间能进 200ms 就算合格。
我踩过最狠的一次是上来把电流给到 800,想一步到位跑到额定转速,结果是电机瞬间锁死,差点烧了减速箱。从那以后,我每次拿到新的电调工程,都强制走一遍先 50、再 100、最后闭环的流程,速度环从来都是先把积分清零再切目标,这套土办法让我的调参时间缩短了大半。希望帮到你。
本文还有配套的精品资源,点击获取