ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C620电调+M3508电机:STM32 CAN/PWM双模式与PID闭环调试指南

C620电调+M3508电机:STM32 CAN/PWM双模式与PID闭环调试指南

简介:《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 时钟 45MHz,Prescaler 填 3 得到 15MHz 的时间量子时钟,BS1 填 11 tq、BS2 填 3 tq、SJW 填 1 tq,总位时间 15 tq,45MHz / 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 驱动)、Middleware(FreeRTOS、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~2000us

PWM 模式的核心是把定时器配成 50Hz、高电平 1000~2000us 的方波。TIM5 挂在 APB1 定时器时钟上,频率 90MHz,分频和重装值这样算:

/* TIM5 初始化:50Hz,1us 分辨率 */ htim5.Instance = TIM5; htim5.Init.Prescaler = 89; /* 90MHz / (89+1) = 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 到 2000,1500 是中位。调速时只需要改__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 共用控制帧0x200,ID 5~8 共用0x1FF。反馈帧是0x201到0x208,一 ID 一帧,1kHz 主动上报。

控制帧(0x200,DLC=8)的字节分配:

字节内容取值范围
0~1电机 1 电流指令-16384 ~ 16384
2~3电机 2 电流指令同上
4~5电机 3 电流指令同上
6~7电机 4 电流指令同上

电流值的换算关系是 16384 对应 20A,也就是 819.2 个计数对应 1A。所以-16384到16384覆盖-20A到20A的双向力矩范围,下发前必须做限幅,超范围会被电调按最大值处理。

反馈帧(0x201~0x208,DLC=8):

字节内容说明
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 在过零附近如果积分项处理不当,会出现明显的停顿或猛冲,这个现象在单向测试里完全看不出来。

从那以后我每次调完参数,都强制走一遍「阶跃 + 反向 + 加载」三步验证,不管当时觉得参数多完美。曾经有一次只做了空载阶跃,曲线漂亮得不行,装上负载跑了不到两分钟就过热报警,回头一看是电流限幅设得太保守、积分一直在顶格输出。参数是调出来的,更是验出来的,跳过验证的那一步,迟早要在别的地方还回来。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进