☰
C620驱动M3508:STM32F427的PWM/CAN二选一Demo
2026/10/29 19:05:01 网站建设 项目流程

简介:《C620电调使用程序说明书1》是一份面向RoboMaster电控入门与进阶开发者的PDF技术文档,围绕C620电调Demo程序展开,说明如何基于STM32F427主控开发板,以PWM或CAN模式驱动M3508电机,并通过按键交互改变转速。文档结合Keil5、STM32CubeMX与HAL固件库,梳理环境搭建、固件库安装、C620三相输出线及7-Pin数据线接M3508等硬件连线、工程分组及FreeRTOS任务框架,重点解析CAN底层驱动、PID参数初始化与计算、任务控制代码和CAN/PWM宏切换。压缩包仅含1个PDF文件,大小约905KB,内容集中,适合调试时快速查阅。已有3353人学习,正文覆盖CAN1与TIM5接线差异、电流闭环与转速闭环区别,并提示CAN和PWM信号线不可同时连接、供电应使用动力电池等易错点。对需要完成赛前联调、代码移植和排错复盘的读者,这份文档能提供较完整的流程参照与硬件注意事项。

1. C620 电调驱动 M3508:先想清楚 PWM 与 CAN 为什么只能二选一

C620 电调配 M3508 电机这套组合,新手最容易翻车的点往往不在 PID,而在上电那一秒:CAN 线和 PWM 线同时插着,电机像抽风一样抖两下就停,回头翻代码半小时也找不到原因。这份 C620 电调 Demo 程序说明书针对的正是这种落地场景:它基于 STM32F427 主控开发板,用 CubeMX 加 HAL 固件库生成 Keil5 工程,把 PWM 与 CAN 两条控制链路都跑通,靠一个宏定义切换,按键改转速。

适合三类人:手上已经有这套电调和电机、想先让电机转起来的硬件新手;需要在 STM32 工程里补一块 CAN 收发和 PID 闭环代码的嵌入式开发者;以及带学生做底盘、想把控制链路吃透的工程负责人。它不涉及电机内部 FOC 算法,讲的是主控这一侧该做什么、参数怎么给、接错线会怎样。先把两条链路的边界划清楚,后面每一步才不会白折腾。

2. CubeMX 与 Keil5 环境:从 Java 依赖到固件包落地的三步

2.1 安装顺序反了会白折腾:先 JRE 后 CubeMX

CubeMX 本身是 Java 写的桌面程序,启动器要去系统里找一个可用的 Java 运行环境。机器上没装 JRE 时,表现不是弹报错,而是双击图标没反应、或者进程一闪就退,任务管理器里也看不到残留,很容易被误判成安装包损坏。我一般的顺序是:先确认 Java 运行环境就位,再装 CubeMX,最后才去碰固件包。

安装路径上有一个老生常谈但每次都能绊住人的细节:CubeMX 的安装目录、以及后面生成工程的目录,都不要带中文和空格。CubeMX 内部会把工程路径拼进脚本和 Makefile 风格的配置里,一旦路径里有空格,生成阶段可能出现文件写不进去、或者 Keil5 打开工程后某些组是空的。装完先别急着建工程,打开软件确认能正常进主界面,再往下走。

账号这块要有心理准备:官网下载安装包需要登录账号,没有就现场注册一个。嫌流程长去找第三方离线包也行,但要注意安装包版本和后面要装的固件包版本得对得上,版本跨度太大时,CubeMX 里的芯片列表可能找不到对应的固件包条目。这一步没有捷径,环境不稳,后面所有调试都是在流沙上盖楼。

2.2 固件包只能走 Install New Libraries,绿色勾选才算装好

这是整份说明书里最容易被跳过、后果又最严重的一步。CubeMX 只是图形化配置工具,它本身不含任何外设驱动代码;真正生成HAL_GPIO_Init、HAL_CAN_Init这些函数体的,是各芯片系列对应的 Firmware Package(俗称 pack)。只装 CubeMX 不装 pack,配置界面照样能点,工程照样能生成,但生成出来的外设初始化是空壳——编译能过,下载进去外设不动,这种“静默失败”最耗时间。

操作路径如下,按顺序点:

  1. 从官网或镜像下好对应系列的固件库 zip 包,先不要解压;
  2. 打开 CubeMX,菜单Help→Install New Libraries;
  3. 弹窗里选From Local,定位到刚才那个 zip,点Open;
  4. 等待解压安装,完成后左侧芯片系列旁对应版本号的 checkbox 会变成绿色。

那个绿色勾选是唯一可信的“装好了”标志。如果只是把 zip 解压到某个目录、然后在工程里手动加 include 路径,CubeMX 生成代码时依然找不到驱动源文件,最后还是要回来重装。另外注意新版 CubeMX 把这个入口改成了Manage embedded software packages,菜单名变了但逻辑一样,找不到Install New Libraries时先去Help下面逐个翻。

装完还要看一眼仓库路径。固件包默认落在用户目录下的STM32Cube/Repository,如果当前 Windows 用户名是中文,或者文档目录被重定向到带空格的路径,安装过程可能报写入失败。解决方式是在 CubeMX 的Project设置里把Repository Folder改到一个纯英文短路径,比如D:/STM32Cube/Repository,再重新安装一次。

2.3 生成 Keil5 工程时那几个必勾的选项与时钟参数

配置完引脚和外设之后,Project Manager页面有几个选项直接决定工程能不能用。下面这张表是我每次建工程都会核对一遍的清单,缺一项后面就要返工:

配置项取值为什么必须这样
Toolchain / IDEMDK-ARM V5对应 Keil5,选错会生成 IAR 或 Makefile 工程
Code GeneratorCopy only necessary library files只拷贝用到的驱动,工程体积可控
Generate peripheral init as pair of .c/.h勾选外设初始化按模块拆文件,后期好维护
HAL 时基SysTickFreeRTOS 场景下要留意与系统节拍的关系
FreeRTOS勾选,接口选 CMSIS_V1老例程兼容性最好,移植现成代码改动最小
时钟源HSE 8MHz 外部晶振板上没焊晶振时改回 HSI,但 CAN 波特率误差会变大

时钟树这一块,STM32F427 常见的配法是 HSE 8MHz 经 PLL 倍频到 168MHz 主频,APB1 分频后 42MHz,APB2 分频后 84MHz。这个配法不是随便定的:CAN 挂在 APB1 上,42MHz 是它允许的上限,超了外设直接不工作;而 TIM5 挂在 APB2 上,84MHz 经过定时器内部的倍频机制实际按 168MHz 计数,后面算 PWM 预分频要用到这个数。

生成出来的时钟初始化大致是这样,重点看三处分频:

void SystemClock_Config(void) { RCC_OscInitTypeDef RCC_OscInitStruct = {0}; RCC_ClkInitTypeDef RCC_ClkInitStruct = {0}; /* HSE 8MHz + PLL:M=8, N=336, P=2 -> 168MHz */ RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE; RCC_OscInitStruct.HSEState = RCC_HSE_ON; RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON; RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE; RCC_OscInitStruct.PLL.PLLM = 8; RCC_OscInitStruct.PLL.PLLN = 336; RCC_OscInitStruct.PLL.PLLP = 2; RCC_OscInitStruct.PLL.PLLQ = 7; /* USB/SDIO 用,CAN 不依赖 */ HAL_RCC_OscConfig(&RCC_OscInitStruct); /* AHB=168M, APB1=42M(CAN 上限), APB2=84M(TIM5 实际计数 168M) */ RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1; RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4; RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2; HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5); }

PLLM/PLLN/PLLP三个参数决定主频,改成别的组合时记得同步把FLASH_LATENCY调对,168MHz 对应 5 个等待周期,给少了跑飞、给多了性能浪费。APB1CLKDivider写成RCC_HCLK_DIV4得到 42MHz,这是 CAN 能正常通信的前提,如果这里手滑写成 DIV2,CAN 初始化会直接返回错误,现象是HAL_CAN_Init一直失败。时钟配置错了,后面无论怎么调波特率都不对,所以建完工程第一件事是确认这三行分频值。

还有一处容易被忽略:FreeRTOS 的堆大小。默认configTOTAL_HEAP_SIZE往往只有几 KB,加上 CAN 收发队列、任务栈之后就吃紧了,运行一段时间会进vApplicationMallocFailedHook死循环。我习惯在 CubeMX 的 FreeRTOS 配置页把TOTAL_HEAP_SIZE调到 15360 字节起步,任务栈按实际用量给,主控制任务 512 字(word)基本够。

3. 接线与端口映射:CAN1、TIM5 与 24V 供电的物理顺序

3.1 三相线、7-Pin 数据线、电源线的连接顺序与检查点

接线这一步有明确的先后逻辑,顺序反了轻则不转、重则烧板。标准流程是:先断电,接电机侧,再接主控侧,最后上电。

  1. 把 C620 电调的三相输出线(三根粗线,按颜色区分)接到 M3508 电机的三相输入端,插头有防呆结构,插不到底就是方向反了,不要硬怼;
  2. 把 7-Pin 数据线(黑色)一端接电调、一端接电机的霍尔/编码器接口,这根线负责把转子角度和转速送回电调;
  3. 把电调的电源线(黑色粗线)接到主控开发板的 24V 电源端口,注意正负极标识;
  4. 最后才接信号线——CAN 的 2-Pin 或者 PWM 的 3-Pin,二选一;
  5. 全部插好后目视复查一遍,再给主控板上电。

这里有两个硬性检查点。第一,三相线如果接反,上电后电机会出现明显的堵转或者异响,不会安静地待机,遇到这种情况立刻断电换线序。第二,7-Pin 线不能漏接,少了它电调拿不到转子位置,CAN 反馈帧里的转速字段会是无效值或者零,你会误以为是通信问题,实际是反馈链路断了。

上电顺序也值得说一句:所有线接完再上电,不要带电插拔信号线。带电插拔的瞬间地电位不一致,CAN 收发器最容易先受伤,坏一次就是换板子的成本。我见过不止一次“调了半天通信,最后发现收发器已经挂了”的情况,所以插线前先摸一下自己有没有静电,冬天尤其注意。

3.2 CAN 模式:1Mbps 波特率、终端电阻与滤波器配置

选 CAN 控制时,把 2-Pin 的 CAN 信号线分别接到电调和主控板的 CAN1 端口,电调的 PWM 信号口空着不接。C620 这条总线的波特率固定按 1Mbps 走,主控侧必须配成一致,配置时在 CubeMX 的 CAN1 参数页里把Prescaler、Time Quanta那几项配成 1Mbps,具体数值由 APB1 的 42MHz 除以分频链得到,配完可以直接看 CubeMX 显示的波特率计算结果,对不上就回来改分频。

终端电阻是另一处高频翻车点。CAN 是差分总线,两端各需要一个 120Ω 终端电阻,总线才不至于因为反射导致误码。很多开发板板上已经焊了一个可选电阻,用电调单独接的时候,另一端(电调这侧)通常内部有,但线束长了、节点多了就要额外补。现象很好认:短距离、低速偶尔能通,稍微一动线就丢帧。

滤波器配置决定了你能不能收到反馈帧。C620 的反馈帧 ID 在0x201到0x204之间,对应四个电机编号;主控发控制帧用0x200。最省事的做法是把滤波器配成接收全部 ID,调试期先保证能收到数据,等功能稳了再收窄掩码。下面这段是初始化骨架:

CAN_FilterTypeDef filter = {0}; /* 1Mbps:APB1 42MHz,分频 3 -> 14MHz,再按 1+BS1(11)+BS2(2)=14 得到 1Mbps */ hcan1.Init.Prescaler = 3; hcan1.Init.TimeSeg1 = CAN_BS1_11TQ; hcan1.Init.TimeSeg2 = CAN_BS2_2TQ; hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ; hcan1.Init.Mode = CAN_MODE_NORMAL; hcan1.Init.AutoRetransmission= ENABLE; HAL_CAN_Init(&hcan1); /* 调试期全通滤波:掩码全 0,全部 ID 进 FIFO0 */ filter.FilterBank = 0; filter.FilterMode = CAN_FILTERMODE_IDMASK; filter.FilterScale = CAN_FILTERSCALE_32BIT; filter.FilterIdHigh = 0x0000; filter.FilterIdLow = 0x0000; filter.FilterMaskIdHigh = 0x0000; filter.FilterMaskIdLow = 0x0000; filter.FilterFIFOAssignment = CAN_FILTER_FIFO0; filter.FilterActivation = ENABLE; HAL_CAN_ConfigFilter(&hcan1, &filter); HAL_CAN_Start(&hcan1); HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);

Prescaler和两个时间段加起来必须正好凑出 1Mbps,3 × 14 = 42,对上了 APB1 的 42MHz。改动主频之后这几个值全都要重算,不能照抄。FilterActivation一定要置ENABLE,只调HAL_CAN_Init不配滤波器,接收中断永远不触发,你会在那里怀疑人生。最后那行ActivateNotification打开的是 FIFO0 挂起中断,回调函数里读帧,读完记得清标志。

3.3 PWM 模式:TIM5 通道映射与 50Hz 脉宽标定

选 PWM 控制时,把 3-Pin 信号线接到主控板的 PWM 输出口。这份 Demo 用 TIM5 做信号源,通道 1 到 4 在 STM32F427 上常见的引脚映射是 PA0 到 PA3,具体到某块板子上要以原理图为准,别照着网上截图接。接到 C1、C2、C3 或 C4 任意一个都行,程序里对应改通道号。

PWM 参数的算法很固定:定时器时钟 168MHz,要出 50Hz 的周期信号,预分频取 167 得到 1MHz 计数频率,自动重装值取 19999 得到 20ms 周期,此时计数器每加 1 就是 1 微秒。脉宽落在 1000 到 2000 之间,对应 1ms 到 2ms 高电平,1500 是停转点。

TIM_HandleTypeDef htim5; /* 168MHz / (167+1) = 1MHz 计数,1us 一步 */ htim5.Init.Prescaler = 167; htim5.Init.Period = 19999; /* 20ms 周期,50Hz */ htim5.Init.CounterMode = TIM_COUNTERMODE_UP; htim5.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim5); /* 通道 1:CCR 1000~2000 -> 1ms~2ms,电调按下式换算目标转速 */ 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); /* 运行中改转速只动 CCR */ __HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, pulse_us); /* pulse_us: 1000~2000 */

Pulse初始值给 1500 是个安全习惯,上电瞬间电调收到中位脉宽,电机保持停转,不会因为初始化过程中引脚电平抖动而转一下。__HAL_TIM_SET_COMPARE写的是比较值,本质是微秒数,做按键调速时按步长加减就完事,不需要重配定时器。脉宽超出 1000 到 2000 这个区间,电调要么不响应,要么进入保护,所以按键调速的边界一定要卡死,别让值跑到 999。

4. 程序框架落地:BSP 分组、FreeRTOS 节拍与两种闭环

4.1 把 bsp_can.c/h 和 pid.c/h 放进 BSP 组

CubeMX 生成的 Keil5 工程,文件分组是自动排的:Application/User放main.c、freertos.c和各类外设的xxx.c,Drivers下面是 HAL 库和 CMSIS,中间件那层是 FreeRTOS。自己写的代码不要往Drivers里塞,否则重新生成工程时可能被清掉。

正确做法是在 Keil5 里新建一个BSP组,把bsp_can.c/h、pid.c/h加进去。bsp_can.c承担三件事:CAN 滤波器配置、控制帧的组帧发送、接收中断回调里的解帧;pid.c只放位置式或增量式 PID 的结构体定义、参数初始化、单次计算。职责分开的好处是换控制对象时只改 BSP,PID 那套代码原样搬走。

新建组之后必须去Options for Target→C/C++→Include Paths里把BSP目录加进去,否则编译时报一堆cannot open source input file "bsp_can.h"。这一步漏了的新手特别多,加完路径记得保存并全编译一次,确认没有隐藏报错再继续。

4.2 StartDefaultTask 里的 10ms 节拍与宏开关切换

控制代码的位置在freertos.c的StartDefaultTask函数里,CubeMX 已经把这个任务建好,默认是个空的while(1)。所有周期性动作都塞进这个循环:按键扫描、PID 计算、CAN 发送。

void StartDefaultTask(void const * argument) { /* USER CODE BEGIN 5 */ PID_Init(&speed_pid, 8.0f, 0.5f, 0.0f, 16384.0f); /* Kp/Ki/Kd/输出限幅 */ BSP_CAN_Init(&hcan1); int16_t target_rpm = 0; for (;;) { #if defined(CAN_CONTROL) /* CAN 模式:读反馈 -> 算 PID -> 发电流值 */ if (BSP_CAN_GetRotorSpeed(&hcan1, &now_rpm)) { int16_t out = PID_Calc(&speed_pid, target_rpm, now_rpm); BSP_CAN_SendCurrent(&hcan1, 0x200, out, 0, 0, 0); } #elif defined(PWM_CONTROL) /* PWM 模式:电调内部做转速闭环,这里只按目标转速换算脉宽 */ uint16_t pulse = 1500 + (int32_t)target_rpm * 500 / MAX_RPM; __HAL_TIM_SET_COMPARE(&htim5, TIM_CHANNEL_1, pulse); #endif Key_Scan(&target_rpm); /* 按键改目标转速 */ osDelay(10); /* 10ms 节拍 */ } /* USER CODE END 5 */ }

osDelay(10)定下 10ms 的控制周期,也就是 100Hz 的闭环频率。为什么是 10ms 而不是 1ms:电调反馈帧虽然 1ms 来一次,但转速环本身有机械惯性,控制周期压到 1ms 只会让 CAN 总线负载变高、CPU 空转,对响应速度没有实质提升。真正需要快的是电流环,那一层电调内部自己做掉了。

宏切换的写法有两个细节。一是#if defined(CAN_CONTROL)和#elif defined(PWM_CONTROL)要成对出现,如果两个宏同时被#define了,#if只认第一个,程序能编过但行为不是你想要的;二是宏定义最好集中在文件顶部一个区块里,注释掉一个、打开另一个,别散落在代码中间。开关宏放在freertos.c里而不是main.c,是因为控制逻辑本来就住在这个文件,改起来不用跳来跳去。

4.3 CAN 模式:电流闭环与 0x201~0x204 反馈帧解码

CAN 模式的核心认知是:电调对电机做的是电流闭环,不是转速闭环。也就是说,你发给电调的是“我要多少安培”,电机最终转多快取决于负载和电压,主控必须自己从反馈帧里读转速、再做一个转速外环,把转速误差换成电流指令。这一点想不明白,调参方向会全错。

发送帧用标准数据帧,ID 固定0x200,长度 8 字节,每个电机占 2 字节,高字节在前,取值-16384到16384线性对应-20A到20A。四路电机拼在同一帧里,你不用哪路就给哪路写 0。

反馈帧 ID 是0x201到0x204,分别对应编号 1 到 4 的电机。每帧 8 字节的排布是固定的:字节 0 到 1 是转子机械角度,字节 2 到 3 是转速(单位 rpm,有符号),字节 4 到 5 是实际电流,字节 6 是温度,字节 7 保留。解帧时按大端拼回int16_t。

/* 组帧发送:4 路电机的电流值,每路 2 字节,大端在前 */ void BSP_CAN_SendCurrent(CAN_HandleTypeDef *hcan, uint32_t id, int16_t c1, int16_t c2, int16_t c3, int16_t c4) { uint8_t tx[8]; tx[0] = c1 >> 8; tx[1] = c1 & 0xFF; tx[2] = c2 >> 8; tx[3] = c2 & 0xFF; tx[4] = c3 >> 8; tx[5] = c3 & 0xFF; tx[6] = c4 >> 8; tx[7] = c4 & 0xFF; CAN_TxHeaderTypeDef hdr = {0}; uint32_t mailbox; hdr.StdId = id; hdr.IDE = CAN_ID_STD; hdr.RTR = CAN_RTR_DATA; hdr.DLC = 8; HAL_CAN_AddTxMessage(hcan, &hdr, tx, &mailbox); } /* 接收回调:解出转速,rpm 有符号,负载大时可能为负 */ void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef hdr; uint8_t rx[8]; HAL_CAN_GetRxMessage(hcan, CAN_FILTER_FIFO0, &hdr, rx); if (hdr.StdId >= 0x201 && hdr.StdId <= 0x204) { motor[hdr.StdId - 0x201].angle = (int16_t)(rx[0] << 8 | rx[1]); motor[hdr.StdId - 0x201].rpm = (int16_t)(rx[2] << 8 | rx[3]); motor[hdr.StdId - 0x201].cur = (int16_t)(rx[4] << 8 | rx[5]); motor[hdr.StdId - 0x201].temp = rx[6]; } }

hdr.StdId - 0x201这个偏移把 ID 映射成数组下标,0 到 3 正好对应四个电机,不用写四个if。拼字节时的移位顺序不能反,小端拼出来是个乱七八糟的大数,转速直接爆表。回调里只做解析、不做运算,真正的 PID 放在任务里跑,中断里耗时越短越好。

4.4 PWM 模式:转速闭环由电调完成,主控只负责给脉宽

PWM 模式的逻辑简单得多,因为转速闭环在电调内部就完成了,主控给出的脉宽本身就是一个转速指令。这意味着pid.c在这个模式下完全用不上,代码里那套 PID 初始化可以直接被宏包起来跳过。

换算关系依赖电调内部的标定曲线,一般是线性的:1500 微秒停转,往 2000 走是正转加速,往 1000 走是反转加速。所以程序里常见做法是维护一个target_rpm,按1500 + target_rpm * 500 / MAX_RPM折算成脉宽,MAX_RPM取电机在额定电压下的空载转速。这个折算不需要精确,因为最终转速由电调闭环保证,主控给的只是个比例指令。

两种模式的代码放在同一个StartDefaultTask里,靠#if / #elif隔开,编译时只保留一支。好处是切换模式只改一行宏定义,坏处是两个分支的变量容易混用,比如在 PWM 分支里误用了 CAN 分支的output。我习惯在文件顶部给两种模式各建一个命名前缀,一眼就能看出这段属于哪条链路。

5. 排查与避坑:五条上电后最容易翻车的记录

5.1 现象:电机一通电就满速狂转 → 双信号线同时接了

现象很吓人:板子刚上电,电机不待机,直接冲到高转速,按键完全不起作用。原因通常不是代码,而是硬件——CAN 的 2-Pin 和 PWM 的 3-Pin 同时插在电调和主控之间。电调同时收到两种控制源的指令,内部状态机在两边来回切,输出就成了随机的满占空比。

解决方式是把不用的那根信号线彻底拔掉,不是悬空、不是用胶带包起来放旁边,而是从电调端和主控端都断开。然后在代码侧把对应的宏也关掉,让软件和硬件保持一致。这件事我现在的习惯是接线前先确认宏定义改没改,两边对齐再上电,比事后查线快得多。

5.2 现象:CubeMX 生成代码里外设初始化是空的 → 只装了软件没装 pack

编译一切正常,下载进去 GPIO 不翻转、CAN 不初始化,打开生成的can.c发现HAL_CAN_MspInit里只有个空函数体。原因在 2.2 节讲过:只装了 CubeMX 没装对应芯片系列的固件包,工具找不到驱动源码,只能生成空壳。

解决方式是回到Install New Libraries重新从本地 zip 安装,装完左侧对应版本的 checkbox 变绿,再重新生成一次工程。判断是否装好的快捷办法是在 CubeMX 里点开芯片型号,看有没有可选的固件包版本号,有就说明 pack 在。这一步没有绕过的方法,工程删了重建也一样,根子在包没装。

5.3 现象:CAN 发出去没回应 → 波特率、终端电阻、滤波器三连

主控侧HAL_CAN_AddTxMessage返回HAL_OK,邮箱也发出去了,但接收回调从来不进,now_rpm一直是 0。按概率从高到低排三个原因:波特率不是 1Mbps、总线缺终端电阻、滤波器没使能。

排查顺序是先量后查。用示波器或逻辑分析仪看 CAN_H 和 CAN_L 上的差分波形,位时间宽度对不上就是波特率问题,回去核对 APB1 分频和Prescaler;波形正常但通信断续,多半是终端电阻缺失,总线两端各补一个 120Ω;波形、电阻都对还收不到,检查FilterActivation是不是ENABLE,以及接收中断有没有真正ActivateNotification。三样都过了还不行,就要怀疑收发器本身是不是在带电插拔时被打坏了,换个模块验证最快。

5.4 现象:电机啸叫、电流飙高 → PID 输出没限幅

电机在目标转速附近高频抖动,发出尖锐啸叫,用手摸电调外壳明显发烫,反馈帧里的电流值顶在 ±16384 不动。这是典型的积分饱和加输出未限幅:误差长期存在,积分项越累越大,输出冲到满量程,电机被推到饱和区后误差反向,积分又反向累加,来回振荡。

解决分两步。第一步在 PID 计算里硬性限幅,输出钳在 ±16384 以内,积分项单独再钳一次,防止它先于输出饱和;第二步把 Kp 从大往小降,先把静差消掉再补 Ki,Kd 在转速环上一般给 0,给大了只会放大噪声。整定过程中把target_rpm设成阶梯值,观察电流曲线,什么时候电流平滑地跟上去、没有过冲尖峰,参数就差不多了。

5.5 现象:改的代码重新生成后没了 → 没写在 USER CODE 区

CubeMX 重新生成工程会覆盖main.c、freertos.c里的部分内容,但凡是夹在/* USER CODE BEGIN xxx */和/* USER CODE END xxx */之间的代码会被保留。把初始化、任务主体写在这两个注释外面,下次点生成就全没了,这种丢失没有提示,只能靠 git 或者记忆找回来。

解决办法很朴素:所有手写代码一律写在 USER CODE 区块内;bsp_can.c、pid.c这类自建文件不受影响,因为它们不在生成范围内。我一般还会在每次生成前提交一次代码,生成后看一遍 diff,确认没有任何一段被吞掉。吃过一次亏之后,这个动作就成了肌肉记忆。

6. 验证与调试:用阶跃响应和串口日志把参数钉死

参数调完之后不要直接上整机跑,先在空载下做三步验证,每一步都有明确的通过标准。

第一步是低速点动:target_rpm给 200 到 500 之间的小值,按键加减,看电机能不能平稳跟随、停转时有没有余转。这一步主要验证方向对不对、脉宽或电流指令的符号有没有搞反。第二步做阶跃:目标从 0 直接跳到 1500 rpm,同时用串口以 10ms 的间隔把now_rpm打出来,记录超调量和调节时间。第三步带载复测,把同样的阶跃再做一遍,观察电流是否长时间贴着限幅值。

串口日志的格式我一般固定成这三列,方便事后直接粘进表格里看:

/* 10ms 节拍里打印,9600 或 115200 都够用 */ printf("%d,%d,%d\r\n", target_rpm, now_rpm, speed_pid.output);

target和now两列画在一起看响应形状,output那一列用来判断有没有打到限幅。如果output长时间贴在 ±16384,说明电流环已经饱和,电机带不动当前负载,这时候再加 Kp 只会让振荡更严重,应该先降目标或者检查机械有没有卡滞。

参数调整的方向可以记成一张小表,出问题时对照着改,比凭感觉拧螺丝靠谱:

观察到的现象优先改哪个调整方向
响应慢、跟不上目标Kp加大,直到出现轻微过冲再回退一档
有静差、稳态差几十 rpmKi小幅加大,每次加 10% 左右
高频抖动、啸叫Kp 或 KdKp 降下来,转速环 Kd 一般保持 0
阶跃后长时间回不来积分限幅收紧积分上限,防止饱和
电流顶死、电机发烫目标值 / 负载先降目标,再查机械阻力

有一件事我踩过不止一次:调参的时候顺手把MAX_RPM改大了,PWM 折算出来的脉宽跟着变,结果同样的目标转速下电机转得比以前快,PID 参数看起来“失效”了。后来我养成习惯,改任何常数之前先在纸上算一遍它对输出量纲的影响,算完再动手。

从那以后,我每次上电前都强制走一遍三件事:确认只有一根信号线插着、确认宏定义和接线一致、确认 USER CODE 区块外的代码一行没动。这三步花不了一分钟,省下的往往是半天的排查。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询