简介《C620电调使用程序说明书1》是一份面向 RoboMaster 参赛队员、STM32 嵌入式初学者及电机控制调试人员的 PDF 技术文档围绕 C620 电调 Demo 程序展开说明如何以 RoboMaster 主控板和 STM32F427 为核心通过 PWM 或 CAN 两种模式驱动 M3508 电机。文档从 Keil5 μVision、STM32CubeMX 与 HAL 固件库的环境配置讲起涵盖固件库安装、电调与主控板的硬件连线、工程目录分层以及 FreeRTOS 任务中 CAN 滤波、PID 参数初始化与计算、速度闭环控制等关键模块并给出两种控制方式的宏切换思路和接线注意事项。压缩包仅含 1 个 PDF 文件大小约 905KB轻量便于随查随用。该文档已有 3353 人学习适合用来快速理解 C620 电调 Demo 的代码框架、控制流程与调试要点也可作为 RoboMaster 电机控制入门的参考材料。1. 电机一上电就失控多半不是代码问题刚拿到 C620 电调和 M3508 电机的人最常见的场景是照着 demo 把线插上编译下载一按按键电机要么纹丝不动要么直接满速狂转电调指示灯还在闪。这份《C620 电调使用程序说明书》解决的正是这一段——它把 RM 主控开发板STM32F427、Keil5、STM32CubeMX 与 HAL 固件库串成一条完整链路用 PWM 和 CAN 两种模式分别驱动 M3508并用按键改变目标转速。程序里既有 CAN 滤波器与收发中断也有一份速度闭环 PID还有 FreeRTOS 的默认任务把控制节拍固定下来。适合谁适合手上已经有电调、电机、主控板却卡在「报文怎么拼、PID 放哪儿算、两种模式到底差在哪」的嵌入式从业者和打比赛的学生。它不是一份芯片手册的复述而是一份能照着连、照着改、照着调的施工图。2. 环境与工程骨架CubeMX 装对后面才谈得上调这一章处理的是「跑起来之前」的所有事。很多人把时间耗在编译报错上其实问题出在固件包没装、时钟没配对、生成代码把用户代码覆盖了这三个地方。STM32 生态里CubeMX 是最常用的配置软件/插件它本身不带任何外设驱动驱动在固件包里这个关系必须先理清楚。2.1 CubeMX 与 F4 固件包装错这一步后面全是白工CubeMX 是 Java 写的机器上必须先有 JRE版本别太老否则双击没反应还以为下载坏了。安装完软件只是第一步真正决定能不能生成初始化代码的是固件包pack。操作路径是Help - Install New Libraries - From Local选中下载好的 zip 包Open 之后等待解压。装成功的标志很直观左侧器件系列那一栏对应版本号的 checkbox 会变成绿色。提示只装 CubeMX 不装 pack工程能建、代码能生成但生成出来是空壳stm32f4xx_hal_conf.h里的外设模块全是注释状态编译一过、运行全废。这里有个容易被忽略的细节F4 系列的 pack 内部是按外设拆分的驱动源码CAN、TIM、GPIO 各有一套.c/.h。所以装完不要急着新建工程先在 CubeMX 的库管理界面确认STM32CubeF4那一行的版本号是实心的而不是灰色的 not installed。我见过有人装了个 F0 的包然后对着 F427 的工程找了半天 CAN 驱动这个坑纯属白给。2.2 从 .ioc 到 Keil5 工程时钟树与三个必开外设新建工程选 STM32F427进入配置界面后按顺序来先定时钟再开外设最后开 FreeRTOS。时钟树里把 HCLK 拉到 180MHz注意 CAN1 挂在 APB1 上APB1 的分频结果直接决定 CAN 波特率能不能整除出 1Mbps。CAN 的位时序参数是新手最容易蒙的地方给一组能直接用的APB1 时钟 45MHzPrescaler 填 3 得到 15MHz 的时间量子时钟BS1 填 11 tq、BS2 填 3 tq、SJW 填 1 tq总位时间 15 tq45MHz / 3 / 15 1Mbps采样点落在 80% 的位置这是 CAN 总线上比较稳的一个取值。三个必开的外设CAN1模式选 Normal自动唤醒和自动总线关闭都打开接收中断 RX0 必须使能。TIM5通道 1 配成 PWM Generation CH1后面 PWM 模式靠它出波形。GPIO按键输入注意上下拉要和板子上的电路对上否则按键会「自己按自己」。FreeRTOS 在 Middleware 里勾上接口选 CMSIS_V1 或 V2 都行关键是把默认任务的栈和优先级定下来示例代码的控制逻辑就挂在StartDefaultTask里。/* main.c 中 CubeMX 生成的初始化顺序别手动打乱 */ MX_GPIO_Init(); MX_CAN1_Init(); MX_TIM5_Init(); /* FreeRTOS 的调度器启动在 main 末尾 */ osKernelStart(); /* 生成的代码只允许写在 USER CODE 保护区里否则下次重新生成会被抹掉 */ /* USER CODE BEGIN 2 */ Motor_Init(); /* 用户自己的初始化 */ /* USER CODE END 2 */上面这段的重点不是代码本身而是注释标注的USER CODE BEGIN/END区块。CubeMX 重新生成工程时只会保留这两个标记之间的内容写在标记外面的用户代码会被无声无息地删掉。这是我见过最隐蔽的一类「代码自己消失了」的问题排查半天以为是编译器抽风其实是被生成器覆盖了。参数说明MX_CAN1_Init()内部封装的是HAL_CAN_Init()里面的Prescaler、TimeSeg1、TimeSeg2、SyncJumpWidth就是前面讲的那四个值改波特率只改这几个不要去改 APB1 分频那会连带影响别的外设。2.3 BSP 分组与宏开关把 CAN 和 PWM 装进同一份工程生成的 Keil 工程自带分层结构最上面是用户代码往下是 Hardware 抽象层HAL 驱动、MiddlewareFreeRTOS、CMSIS、ARM core。示例工程在 HAL 之上加了一个 BSP 组把bsp_can.c/h、pid.c/h放进去。这个划分是有讲究的——BSP 层的职责是「把片上外设包装成好用的函数」比如can_send_current()PID 属于算法层不该混进 BSP否则换个控制对象就得把整个文件复制一遍。两种控制模式用编译期宏切换写在freertos.c顶部/* freertos.c 顶部二选一绝对不要同时打开 */ #define CAN_CONTROL /* #define PWM_CONTROL */ #if defined(CAN_CONTROL) defined(PWM_CONTROL) #error CAN_CONTROL 与 PWM_CONTROL 不能同时定义 #endif用#error把冲突挡在编译期比运行起来发现电机抽搐再去查线要省事得多。宏切换的本质是让不需要的那条分支根本不参与编译所以 CAN 模式下不会去初始化 TIM5 的 PWM 输出PWM 模式下也不会去启动 CAN 接收中断——这一点很关键因为两种信号线同时接到电调上电调本身就会控制失常软件层面再「双管齐下」只会让现象更难判断。编译前建议做一次全量 rebuild不是增量编译因为宏切换会改变条件编译的分支增量编译偶尔会留下上一次的目标文件出现「明明改了宏行为却没变」的玄学现象。3. 接线与双模式选型先想清楚谁来算闭环接线这一步错一根线的代价可能是电调和主控板一起报废。C620 的接口不多但每一个都有明确用途接错位置的后果不对等。3.1 四个接口逐一落位三相线、7-Pin、电源、信号C620 电调对外一共四组线接口形态接到哪里接错的后果三相输出3 根粗线三色M3508 电机三相相序错会反转或堵转霍尔/编码器7-Pin 黑色排线M3508 尾部数据口电调无法换相电机不转电源输入2 根黑色粗线主控板 24V 输出或动力电池极性反接直接损坏控制信号CAN 2-Pin 或 PWM 3-Pin主控板 CAN1 或 C1~C4见下一节三相线和 7-Pin 线是成对的缺一根电机都转不起来因为 M3508 是带编码器的无刷电机电调需要转子位置才能换相。电源线接主控板的 24V 端口接之前用万用表量一下极性别相信线材颜色。供电这块有一条血泪经验必须写进来用动力电池别用实验室电源箱。电机在减速或者堵转的瞬间会产生反电动势电压往回灌主控板上那颗做电源保护的二极管扛不住容易被击穿。这个损坏是不可逆的而且现象很迷惑——板子还能跑只是某一路电源莫名其妙没电了。注意CAN 信号线和 PWM 信号线绝对不允许同时接在同一个电调上说明书里明确写了会导致控制失常。切换模式的时候先把不用的那根拔掉再上电。3.2 CAN 与 PWM 的闭环位置不同选型先看谁来算这是整份说明书里最值得反复读的一段。两种模式不是「一种高级一种低级」的关系而是闭环放在了不同的地方维度CAN 控制PWM 控制电调内部闭环电流环转速环主控要做什么自己算转速 PID输出电流指令只输出对应脉宽的方波反馈数据转速、电流、角度、温度全都有无单向信号控制频率要求高报文间断会被判掉线低脉宽稳定即可上手难度需要理解报文与滤波器会配定时器就行适合阶段联调、需要力矩/多电机协同硬件验证、单电机调速理解这张表的办法是问一句电机转速的误差最终由谁去消除PWM 模式下你给 1500us 它就自己稳到某个转速主控完全不知道实际转了多少CAN 模式下电调只负责把电流变成力矩实际转速得靠主控读反馈、算 PID、再下发电流。所以说明书特意强调CAN 控制时必须「从电调获取电机转速并增加电机转速 PID 调节」否则就是一个纯开环的电流指令负载一变转速就飘。我的建议是新硬件到手先用 PWM 模式验证三相线、编码器线、电源三处接对了没有电机能平稳转起来再切 CAN。反过来直接上 CAN一旦不转你分不清是线接错了、滤波器配错了、还是 PID 参数太离谱排查面会大一倍。3.3 PWM 通道配置TIM5 的 50Hz 与 1000~2000usPWM 模式的核心是把定时器配成 50Hz、高电平 1000~2000us 的方波。TIM5 挂在 APB1 定时器时钟上频率 90MHz分频和重装值这样算/* TIM5 初始化50Hz1us 分辨率 */ htim5.Instance TIM5; htim5.Init.Prescaler 89; /* 90MHz / (891) 1MHz即 1us 一格 */ htim5.Init.Period 19999; /* 20000 格 20ms 50Hz */ htim5.Init.CounterMode TIM_COUNTERMODE_UP; HAL_TIM_PWM_Init(htim5); /* 通道 1 配置 */ sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 1500; /* 中位对应电机停转 */ sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim5, sConfigOC, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim5, TIM_CHANNEL_1);Prescaler 89是为了把 90MHz 压到 1MHz这样Pulse的数值直接就是微秒数改起来不用换算。Period 19999对应 20ms 周期。Pulse的有效区间是 1000 到 20001500 是中位。调速时只需要改__HAL_TIM_SET_COMPARE(htim5, TIM_CHANNEL_1, pulse)把目标转速映射到这个区间里。实际映射建议留死区1000 和 2000 这两个端点附近线性度差也容易触发保护我一般把可用范围收到 1100~1900再线性映射到目标转速。上电瞬间先输出 1500 保持几百毫秒再进入控制循环能避免电机「一上电就窜一下」。4. CAN 报文与 bsp_can滤波器、打包、PID 三处最容易翻车CAN 模式是这份 demo 的主体也是最容易卡住的部分。它涉及三件事报文格式、过滤器配置、闭环结构任何一处错了现象都是「电机不动」。4.1 0x200 发、0x201 收报文格式摊开C620 在 1Mbps 波特率下工作电机 ID 1~4 共用控制帧0x200ID 5~8 共用0x1FF。反馈帧是0x201到0x208一 ID 一帧1kHz 主动上报。控制帧0x200DLC8的字节分配字节内容取值范围0~1电机 1 电流指令-16384 ~ 163842~3电机 2 电流指令同上4~5电机 3 电流指令同上6~7电机 4 电流指令同上电流值的换算关系是 16384 对应 20A也就是 819.2 个计数对应 1A。所以-16384到16384覆盖-20A到20A的双向力矩范围下发前必须做限幅超范围会被电调按最大值处理。反馈帧0x201~0x208DLC8字节内容说明0~1机械角度0~8191对应 0~360°2~3转速int16单位 rpm转子侧4~5实际电流int16范围同控制帧6温度int8摄氏度7保留—注意转速是转子侧rpm而 M3508 的减速比约为 19.2:1输出轴转速是转子转速除以 19.2。做 PID 时反馈和目标必须用同一个量纲我一般统一用输出轴转速把反馈除一下减速比这样设定的 200rpm 就是肉眼看到的速度调试时直觉不会错。4.2 滤波器与中断接收bsp_can 里最容易写错的两处CAN 滤波器配不对现象是发送完全正常、接收一帧都没有很容易误判成硬件损坏。STM32 的过滤器寄存器里标准帧 ID 是左对齐存放在 bit31~bit21 的所以 HAL 库里传入的FilterIdHigh/Low必须是StdId 5之后再拆分。/* bsp_can.c只放行 0x201~0x208 这 8 个反馈帧 */ CAN_FilterTypeDef f {0}; f.FilterBank 0; f.FilterMode CAN_FILTERMODE_IDMASK; f.FilterScale CAN_FILTERSCALE_32BIT; /* 目标 ID 0x200 左移 5 位对齐到寄存器高位 */ f.FilterIdHigh (0x200 5) 16; f.FilterIdLow (0x200 5) 0xFFFF; /* 掩码 0x7F0低 4 位不参与比较正好覆盖 0x200~0x20F */ f.FilterMaskIdHigh (0x7F0 5) 16; f.FilterMaskIdLow (0x7F0 5) 0xFFFF; f.FilterFIFOAssignment CAN_RX_FIFO0; f.FilterActivation ENABLE; HAL_CAN_ConfigFilter(hcan1, f); /* 三个调用缺一不可漏掉任何一个都收不到数据 */ HAL_CAN_Start(hcan1); HAL_CAN_ActivateNotification(hcan1, CAN_IT_RX_FIFO0_MSG_PENDING); HAL_CAN_ActivateNotification(hcan1, CAN_IT_BUSOFF);逻辑说明0x200 5是为了把标准 ID 挪到 32 位过滤器寄存器中 STID 字段的位置掩码0x7F0表示高 7 位必须匹配、低 4 位随便是多少这样 0x201 到 0x208 全部通过而其它 ID 被挡在门外。参数说明FilterBank 0表示用第 0 组过滤器F4 系列有 14 组用了哪组要清楚两组同时占用同一个 FIFO 时行为会很怪。FilterFIFOAssignment选 FIFO0和后面激活的中断必须是同一个 FIFO这是第二个常见错误——滤波器配 FIFO0、中断开 FIFO1代码看起来都对数据就是进不来典型黑匣子问题。接收中断回调里做解码注意 DLC 判断和 ID 判断都要有void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rx; uint8_t data[8]; if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rx, data) ! HAL_OK) return; if (rx.IDE ! CAN_ID_STD) return; /* 丢弃扩展帧 */ uint16_t id rx.StdId; if (id 0x201 || id 0x208 || rx.DLC ! 8) return; uint8_t idx id - 0x201; /* 换算成 0~7 的电机下标 */ motor[idx].angle (int16_t)((data[0] 8) | data[1]); motor[idx].rpm (int16_t)((data[2] 8) | data[3]); motor[idx].cur (int16_t)((data[4] 8) | data[5]); motor[idx].temp (int8_t)data[6]; }这里用指针解包也可以但大端拼字节的写法更抗结构体对齐的折腾换编译器不会出问题。回调里只做数据落地不做计算计算放到任务里中断里算 PID 会拉长中断时间碰上高频反馈容易丢帧。4.3 电流打包与限幅一帧 8 字节怎么填发送函数负责把四个电机的电流值打进一帧并下发。限幅、类型转换、字节序三件事一次做完/* 下发电流指令单位原始计数 -16384~16384 */ void can_send_current(int16_t i1, int16_t i2, int16_t i3, int16_t i4) { CAN_TxHeaderTypeDef tx {0}; uint8_t buf[8]; uint32_t mail; /* 限幅超出会被电调按边界处理但显式裁掉更安全 */ i1 i1 16384 ? 16384 : (i1 -16384 ? -16384 : i1); i2 i2 16384 ? 16384 : (i2 -16384 ? -16384 : i2); i3 i3 16384 ? 16384 : (i3 -16384 ? -16384 : i3); i4 i4 16384 ? 16384 : (i4 -16384 ? -16384 : i4); buf[0] i1 8; buf[1] i1 0xFF; buf[2] i2 8; buf[3] i2 0xFF; buf[4] i3 8; buf[5] i3 0xFF; buf[6] i4 8; buf[7] i4 0xFF; tx.StdId 0x200; /* ID 1~4 用 0x200 */ tx.IDE CAN_ID_STD; tx.RTR CAN_RTR_DATA; tx.DLC 8; HAL_CAN_AddTxMessage(hcan1, tx, buf, mail); }逻辑说明int16_t右移 8 位取高字节注意这是算术右移负数也能正确补码不用担心符号问题。mail是邮箱编号F4 有 3 个发送邮箱用不完也无所谓但要传个有效指针进去传 NULL 会进 HardFault。参数说明当前只驱动一个电机时把另外三个位置填 0 而不是省略DLC 必须是 8电调按固定格式解析短帧会被丢弃。发送周期建议 5~10ms也就是 100~200Hz低于 50Hz 时电调可能判定信号丢失进入保护。示例程序用 10ms够用但如果负载变化剧烈把osDelay(10)改成osDelay(2)响应会明显好一个档次。4.4 PID 与 10ms 任务闭环放在哪一层速度闭环的实现位置在StartDefaultTask里一个 10ms 节拍完成「读反馈 → 算误差 → PID 输出 → 打包下发」四步。这四步必须在一个循环里中间不要加别的阻塞调用否则节拍抖动会直接体现在转速波动上。/* freertos.c 的 StartDefaultTask 简化版 */ void StartDefaultTask(void const *argument) { PID_Init(spd_pid, 8.0f, 0.05f, 0.0f); /* Kp, Ki, Kd 初值 */ spd_pid.out_max 16384.0f; spd_pid.i_max 8000.0f; /* 积分限幅防饱和 */ for (;;) { /* 1. 读按键修改目标转速输出轴 rpm */ if (key_pressed()) target_rpm 100; /* 2. 反馈换算到输出轴 */ float fb motor[0].rpm / 19.2f; /* 3. 位置式 PID */ float out PID_Calc(spd_pid, target_rpm, fb); /* 4. 下发电流指令其余三路填 0 */ can_send_current((int16_t)out, 0, 0, 0); osDelay(10); /* 100Hz 控制周期 */ } }逻辑说明target_rpm是输出轴目标转速motor[0].rpm是中断里存下来的转子侧转速除 19.2 之后两者量纲一致误差才有意义。out_max对应满量程电流i_max单独限制积分项防止目标突变时积分累积到饱和、反向时迟迟退不出来。参数说明Kp 的初值给 8 左右是基于「误差 1rpm 大约需要多少电流计数」估出来的实际要按负载调Ki 从小往大加先给 0.05加太大电机会低频抖动Kd 一般先关掉速度环有反馈噪声微分项容易放大噪声。这套初值只是起点绝不是可以直接上机的最终参数。5. 避坑与排查五条照着写就一定出事的记录这一章全是踩过的坑按现象、原因、解决三段写对号入座比从头推一遍快得多。5.1 接线与供电现象一上电后电机完全不转电调指示灯规律闪烁。原因有两个层面。第一层是物理连接7-Pin 编码器线没插到底或者 CAN_H 与 CAN_L 接反了。CAN 是差分总线反接之后主控发得出去、电调收不到用示波器量两根线都有波形特别容易误判成软件问题。第二层是供电只用 USB 给主控板供电24V 那一路没接电调根本没上电。解决先用万用表量电调电源端有没有 24V再把 CAN 两根线对调试一次最后确认 7-Pin 有咔哒一声卡到位。这三步做完再回去看代码。现象二换控制模式之后电机行为变得莫名其妙时转时不转。原因几乎可以确定是 CAN 线和 PWM 线同时接在了电调上。说明书里已经写死了不允许同时连接但实际操作中切模式时图省事只改了宏没拔线就会落进这个坑。解决改宏 → 断电 → 拔掉不用的那根信号线 → 重新上电。把「拔线」写进切换流程里别靠记忆。5.2 通信与代码现象三CAN 发送计数在涨HAL_CAN_GetRxMessage一次都没被调用。最常见的原因就是 4.2 节讲的过滤器问题标准 ID 忘了左移 5 位或者滤波器绑到 FIFO0 而中断开在 FIFO1。其次是漏掉HAL_CAN_Start()只调了HAL_CAN_Init()是不够的初始化只是配置Start 才让外设进入工作状态。还有一种情况是主控板上的 CAN 收发器没有使能引脚拉高这个要看板子原理图软件层面无能为力。解决按顺序查四件事——滤波器 ID 是否左移、FIFO 是否一致、HAL_CAN_Start有没有调、中断优先级分组是否和 FreeRTOS 冲突。FreeRTOS 工程里CAN 中断的优先级数值必须大于configMAX_SYSCALL_INTERRUPT_PRIORITY否则中断里调用 FreeRTOS API 会直接卡死这个坑非常深。现象四重新用 CubeMX 生成代码之后写到一半的函数不见了。原因是没有写在USER CODE BEGIN/END保护区里。CubeMX 重新生成时只保留标记之间的内容标在别处的用户代码会被静默覆盖编译还能过因为被删掉的可能只是函数体的一部分。解决养成习惯所有手写代码一律写在保护区工程用 Git 管起来重新生成之后先git diff一遍再编译被删了能立刻发现。5.3 控制与调参现象五电机能转但伴随着明显的嗡嗡声和高频抖动转速表在目标值附近来回跳。原因通常是三个叠加Kp 给太大、积分项没有限幅、控制周期不稳定。Kp 过大时系统在目标值附近来回过冲听起来就是啸叫积分项无上限时目标一变就要花很久才退饱和表现为「加速很猛、减速很肉」控制周期抖动则多半是任务里混进了阻塞调用。解决先把 Ki、Kd 清零只调 Kp加到刚好不振荡再回调 30%然后加 Ki配合积分限幅最后确认控制循环里没有printf、没有等待信号量、没有文件操作。串口打印是隐藏杀手一帧几十字节在 115200 下就要几毫秒足以把 10ms 的节拍打乱。6. 进阶用阶跃响应验证闭环比看波形管用调完参数别急着收工得有一套验证方法证明闭环真的在工作。我习惯做一次阶跃测试目标转速从 0 直接跳到 1000rpm输出轴每 10ms 通过串口把target_rpm和实际反馈打出来存成 CSV看三个指标。指标合格范围不达标说明上升时间200~500ms太长说明 Kp 偏小或电流限幅太低超调量小于 15%偏大说明 Kp 太大或 Ki 累积过多稳态误差小于 5rpm有静差说明 Ki 太小或积分被限死串口打印的代码不要塞进控制循环单独开一个低优先级任务用队列把数据传出去/* 采样任务与控制任务解耦避免打印拖慢节拍 */ void SampleTask(void const *argument) { for (;;) { /* 只做快照不做计算 */ log_buf.tgt target_rpm; log_buf.fb motor[0].rpm / 19.2f; log_buf.cur motor[0].cur; printf(%.1f,%.1f,%d\r\n, log_buf.tgt, log_buf.fb, log_buf.cur); osDelay(10); } }这个任务优先级设得比控制任务低osDelay(10)和控制的 10ms 节拍错开半个周期效果更好采样点不会总落在同一个相位上。CSV 拷到表格软件里画一张曲线超调和稳态误差一目了然比盯着电机听声音靠谱得多——耳朵能听出振荡但听不出 8% 的超调。还有一个容易被忽略的验证点把目标转速设成负数看电机能不能平滑反向。位置式 PID 在过零附近如果积分项处理不当会出现明显的停顿或猛冲这个现象在单向测试里完全看不出来。从那以后我每次调完参数都强制走一遍「阶跃 反向 加载」三步验证不管当时觉得参数多完美。曾经有一次只做了空载阶跃曲线漂亮得不行装上负载跑了不到两分钟就过热报警回头一看是电流限幅设得太保守、积分一直在顶格输出。参数是调出来的更是验出来的跳过验证的那一步迟早要在别的地方还回来。希望帮到你。本文还有配套的精品资源点击获取