STM32F103 CAN通信控制无刷电机:从协议到PWM输出

发布时间:2026/10/16 10:15:40
STM32F103 CAN通信控制无刷电机:从协议到PWM输出
简介基于STM32F103的无刷电机CAN通信命令控制项目面向嵌入式开发者和电机控制初学者解决通过CAN总线远程设定转速、方向并读取状态的应用需求。资源共202个文件以C源码和H头文件为主配合uvprojx工程文件、axf/hex编译输出及o目标文件便于直接打开、编译和烧录压缩包仅577KB适合快速下载。目前已有661人学习浏览可见该主题关注度较高对同类项目具有借鉴价值。内容围绕CAN通信实验展开涉及报文帧结构、标识符与数据字段解析、STM32的CAN控制器配置、滤波器设置等电机控制部分涵盖三相逆变器驱动、霍尔传感器位置检测、PWM脉宽调制以及六步换相和FOC磁场定向控制思路。代码采用模块化设计便于按需修改有助于系统理解CAN总线与无刷电机控制的完整链路。1. 一个CAN接口如何把无刷电机变成“听得懂命令的执行器”做机器人底盘或者AGV小车的时候最常遇到的一件事就是控制器说要让电机以某个速度转电机却只肯响应一根PWM信号线。PWM在实验台上好用一旦上了整机线一长、干扰一大、电源一抖油门信号就可能飘甚至把电调误触发。于是大家陆续把目光转到CAN命令控制而对STM32F103来说片上的bxCAN外设刚好就是干这个的。基于32单片机f103 can通信命令控制的无刷电机本质上是把CAN总线当作电机控制器的“遥控器”让F103去解析命令再输出电机驱动器能识别的油门信号顺带把转速、状态回传。这套玩法特别适合已经在用CAN组网的设备机械臂关节、移动底盘、多电机协同的传送系统。好处是接线从“很多根PWM线”变成“一根双绞线”抗干扰和扩展性都上一个档次。适合谁适合已经会点F103编程、手头有无刷电机和电调但不想一上来就啃FOC和换相算法的开发者。本文不追求把电机控制做到实验室级而是先打通“CAN收命令、F103解释、电机响应”这条主干再告诉你哪些地方是坑。2. 方案选型F103、CAN收发器和无刷电机驱动的三条主线STM32F103的CAN外设并不复杂但它和普通UART不一样不能随便拉两根线就让数据跑起来必须先谈好波特率、位时序、过滤器和终端电阻。多数人一上来就写代码结果报文发不出去最后发现是硬件上的终端电阻没焊、采样点不对。所以这一步先别急着写函数先把链路搭对。F103没有CAN收发器MCU的CAN_TX和CAN_RX是TTL电平要外接一个收发器芯片常见做法是TJA1050或者SN65HVD230前者是5V供电后者兼容3.3V主控。F103的PA11和PA12分别映射到CAN_RX和CAN_TX这是默认复用功能如果引脚被占用就得重映射但那种布线方式不如直接换用现成的CAN扩展板省事。收发器后端的CANH、CANL之间要放一个120Ω终端电阻而且只在总线两端放中间节点不放。很多人省这个电阻短距离调试能通跑几米就丢帧这是第一课。关于无刷电机本身怎么驱动有完全不同的三条路选错了后面全是血泪。2.1 三种驱动方式的取舍电调PWM、霍尔六步、FOC第一种是把无刷电机接在成熟的无刷电调后面电调接收标准PWM油门信号内部自己完成换相。你不需要管三相线怎么通电只需要给电调信号线送PWM脉冲宽度一般在1000μs到2000μs对应停止到全速。这是最省力的方案也最便宜一个普通电调几十块就能跑起来。F103只负责输出PWM和测转速转速从电调的转速计引脚读或者额外加霍尔传感器。第二种是霍尔六步换相。电机带三根霍尔信号线F103通过三个定时器输入捕获读取霍尔状态然后按扇区轮流给三路半桥驱动芯片送PWM。这种方案要求你自己写换相表和控制桥电路硬件成本也不高但调试量大换向瞬间电流尖刺明显需要花时间调驱动死区和换相时序。好处是不依赖外部电调控制器和电机之间没有黑匣子。第三种是FOC通过编码器或观测器获取转子角然后做Clarke/Park变换和SVPWM输出。F103在FOC上跑得动但资源余量很小调试曲线比六步陡得多不是新手首选。我一般建议为了“先跑通CAN命令控制”选第一种也就是电调PWM方案把电机驱动细节交给电调。后续如果要做高性能闭环再把霍尔方案和FOC一步一步加进来。驱动方式硬件成本F103工作量低速扭矩调试难度适用场景电调PWM低低一般低快速验证、机器人底盘霍尔六步中中较好中教学、自研驱动板FOC高高好高精密伺服、关节控制2.2 硬件接线MCU-CAN收发器-驱动器-电机的链路完整的通讯链路是这样的上位机或主控制器发出CAN帧经过收发器到F103F103解析之后用定时器输出PWM到电调信号线电调的BEC给F103供电同时电调通过三相线驱动无刷电机。如果要做转速闭环那就在电机上加霍尔传感器或者接上电调的测速输出把速度脉冲送进F103的定时器输入捕获引脚。接线时重点注意共地。CAN收发器、F103、电调三者必须共用一个系统地否则收发器端电压差会直接导致报文接收不到甚至烧芯片。电调的信号地不要单独飞线到F103的PA0旁边应该先汇集到电源地再统一给信号地。观感上可能觉得无所谓高压电机启动时地弹会把你查疯。PWM接线的默认做法是选一个通用定时器比如TIM2的CH1对应PA0。把PWM输出接到电调信号线信号线通常带着上拉电阻到接收端电源所以不需要额外接下拉。如果电调是高电平有效F103推挽输出即可。若电调信号线是集电极开路类型就要加一个10kΩ上拉到5V或者电调给的接收机电压轨否则PWM高电平到不了位。2.3 CAN波特率与终端电阻先定参数再写代码F103的bxCAN在500kbps下最稳更高的1Mbps对晶振精度和布线要求也更高一般的开发板用8MHz外部晶振跑1Mbps经常出问题。500kbps可以支持很长的一段室内小车控制链路报文字节不多时命令刷新率做到几百赫兹绰绰有余。波特率的具体配置写在后面代码里这里先说原则采样点尽量放在位时间的75%到85%之间这样对线缆传播延迟最宽容。终端电阻不是玄学。CAN总线的差分信号需要反射抑制总线两端缺一个120Ω电阻波形边沿会出现过冲MCU有时候还能解出来但距离一远就偶尔丢一帧。我分别在总线的两端放了120Ω电阻总线长度不到两米时没有任何区别但把线延长到七八米差异立刻体现出来。所以从一开始就焊好这两个电阻别等翻车才补。3. 命令协议设计让CAN帧成为电机唯一的指令来源CAN总线本身只保证把8个字节带到目的地它不管字节内容代表什么。所以命令协议要我们自己定。协议设计不只是写一个结构体那么简单它决定以后扩展多电机、多型号时要不要改上位机。最常见的做法是用标准帧11位ID报文最长8字节。我们把ID分组用来区分电机地址和消息类型数据段里再定义命令字和参数。要让F103解析起来简单命令头尽量固定比如第一个字节永远是命令码第二个和第三个字节是参数第四个字节是校验。有些开发者为了省事直接用CAN ID当命令码一帧只发一个数字。例如ID 0x100表示正转ID 0x200表示反转。这样写接受滤波时就能直接过滤但对多参数的命令比如“目标转速1234转/分”一个ID根本无法承载。折中的办法是ID的高4位表示设备地址低4位表示命令类型参数放在数据段。比如0x150表示设备0x15的转速设置命令0x151表示设备0x15的状态查询命令。这样总线上一共挂了几个电机每个电机发什么命令一眼就能看出来。3.1 协议字段ID分组、命令字、参数与校验我建议把CAN标准帧的11位ID拆成两段高4位是电机节点地址支持16个节点中间3位是命令类型支持8种命令低4位保留留作以后做应答或者同步标志。数据段的定义按固定格式来第0字节命令码第1、2字节参数低高字节第3、4字节备用或扩展参数第5字节校验。校验用最简单的异或校验把0到4字节逐位异或得到第5字节。虽然不如CRC强壮但在这个短帧场景里已经能拦下绝大多数总线干扰和软件错位。命令码不要用0x00因为一条全0的CAN帧在总线上是合法的但如果把它误当命令会直接停止电机。一般从0x01开始比如0x01使能、0x02失能/刹车、0x10设置转速、0x20读取状态。参数的类型固定为int16或者uint16不要在这个简单的协议里混搭float否则F103解析麻烦上位机组帧也麻烦。转速目标是整数RPMPWM目标是一个范围0到1000的比例值两者都能在2字节内表示干净。3.2 一个可用的命令表与CAN帧示例命令表的设计要考虑到电机开机默认状态。电机上电后不应该立刻转这既是安全考虑也是协议设计礼貌。表里要包含使能、失能、转速控制、PWM控制、状态查询。状态查询的应答帧必须固定格式这样上位机才能解析。下面这组命令是我的常用写法。设备地址0x15转速设置命令字段值说明CAN ID0x1500x15为节点地址命令类型3表示控制Byte00x10命令码设置目标转速Byte10x04转速低字节0x04D21234Byte20xD2转速高字节Byte30x00备用Byte40x00备用Byte50xC60x10^0x04^0xD2^0x00^0x00的异或结果使能命令字段值说明CAN ID0x150节点0x15Byte00x01使能Byte10x010表示不锁, 1表示锁转Byte2-40x00保留Byte50x01校验实际传输时数据段0字节到4字节的异或结果写入第5字节。接收端解析时先按同样的算法重新计算一遍和收到的第5字节比较不一致就直接丢弃当前帧。3.3 用C语言实现命令解析从报文到动作有了协议接下来就是解析。在中断收帧回调里只做“搬数据”的动作不要在这里执行电机控制因为HAL的回调上下文很浅它会把系统时间片拖长造成PWM抖动。常见做法是把收到的原始数据拷贝到环形缓冲主循环里再解析。typedef struct { uint8_t cmd; int16_t speed_rpm; uint16_t pwm_value; uint8_t valid; } MotorCommand; MotorCommand motor_cmd; uint8_t can_data[8]; volatile uint8_t rx_flag 0; void parse_can_frame(uint32_t std_id, uint8_t *data) { if (std_id ! 0x150) return; // 只处理目标节点地址 uint8_t checksum 0; for (int i 0; i 5; i) { checksum ^ data[i]; // 数据段前5字节异或 } if (checksum ! data[5]) { // 校验失败直接丢弃不做任何动作 return; } motor_cmd.cmd data[0]; if (motor_cmd.cmd 0x10) { // 转速设置低字节在data[1]高字节在data[2] motor_cmd.speed_rpm (int16_t)(data[1] (data[2] 8)); motor_cmd.pwm_value 0; motor_cmd.valid 1; } else if (motor_cmd.cmd 0x20) { // PWM直接设置0~1000对应油门脉宽 motor_cmd.pwm_value (uint16_t)(data[1] (data[2] 8)); motor_cmd.speed_rpm 0; motor_cmd.valid 1; } else { motor_cmd.valid 0; } }代码逻辑很直接先校验再判断命令码。这里有两个容易错的地方一是CAN数据段大小端我的协议是低字节在前高字节在后这样直接按数组索引转换如果改成高字节在前就要反过来二是校验范围只在第0到第4字节别把Wi-Fi之类的东西也算进去否则协议一扩展就全乱套。判断完valid之后主循环看到这个标志位为1再去更新PWM占空比这样把命令解析和电机控制彻底隔离开。4. 用F103的bxCAN收发命令初始化、过滤与中断接收协议定好了现在开始写F103底层的CAN代码。我基于HAL库写因为标准外设库虽然看着简单但中断和过滤器的扩展性不如HAL清晰而且HAL的CAN回调在CubeMX生成的工程里直接就能用。初始化CAN分三步配置GPIO复用、配置CAN时序、使能中断。GPIO这里用PA11和PA12分别配置为CAN_RX和CAN_TX。HAL库初始化这些引脚时要设置成复用开漏吗不用CAN收发器直接连接GPIO配置成复用推挽输出即可具体看收发器芯片的设计多数收发器带隐性电平上拉不需要外部上拉。CAN位时序是重点。F103挂载在APB1总线上最大36MHz如果系统时钟是72MHzAPB1分频系数是2所以CAN外设时钟就是36MHz。要得到500kbps需要满足公式波特率 CAN外设时钟 / (prescaler * (tseg1 tseg2 1))。我常用的组合是预分频4时间段1为7时间段2为6这样得到500kbps采样点是(71)/(761) 57.1%这个采样点偏低但仍然能用如果想让采样点靠近80%就调整时间段预分频4BS113BS22采样点93.3%也偏高。真正稳妥的是预分频8BS18BS21采样点81.2%但这样两个时间段加起来只有9对传播时间的容忍度一般。我更偏好把采样点凑到75%到80%之间比如预分频4、BS16、BS22采样点77.7%500kbps不变。4.1 CAN初始化参数配置下面是一段可以直接用的HAL初始化代码void MX_CAN1_Init(void) { CAN_HandleTypeDef hcan1; hcan1.Instance CAN1; hcan1.Init.Prescaler 4; // 预分频4 hcan1.Init.Mode CAN_MODE_NORMAL; // 正常模式调试时才用回环 hcan1.Init.SyncJumpWidth CAN_SJW_1TQ; hcan1.Init.TimeSeg1 CAN_BS1_6TQ; hcan1.Init.TimeSeg2 CAN_BS2_2TQ; hcan1.Init.TimeTriggeredMode DISABLE; hcan1.Init.AutoBusOff DISABLE; hcan1.Init.AutoWakeUp DISABLE; hcan1.Init.AutoRetransmission ENABLE; hcan1.Init.ReceiveFifoLocked DISABLE; hcan1.Init.TransmitFifoPriority DISABLE; if (HAL_CAN_Init(hcan1) ! HAL_OK) { // 初始化失败一般先查时钟配置和GPIO复用 } }这里的TimeSeg1TimeSeg21个时间单元乘以预分频4得到500kbps。AutoRetransmission建议保持ENABLE当总线上有冲突时CAN控制器会自动重发对命令控制电机来说重发不会造成指令重复执行因为上位机发的是“目标转速”而不是“加几个脉冲”。ReceiveFifoLocked保持DISABLE避免一个电机占用太多接收空间导致后面帧被锁。如果主循环处理慢可以把FifoLocked设为ENABLE来防止溢出但这会造成丢帧不如优化主循环。初始化完CAN之后还需要启动接收。HAL里只调HAL_CAN_Start只是打开了控制器中断回调不会触发。要让收到的帧进入回调还要在启动后激活接收中断HAL_CAN_Start(hcan1); HAL_CAN_ActivateNotification(hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);4.2 过滤器配置只接收我们关心的命令帧过滤器是CAN控制器用来减轻处理器负担的功能。F103有28个过滤器但很多初学者被过滤器挡在外面因为配置不对就一条报文都进不来。我的做法是先把所有标准帧都放进FIFO0然后在解析函数里用ID过滤。这样虽然浪费一点处理时间但调试时能看到总线上所有消息不会因为过滤器误伤命令。CAN_FilterTypeDef sFilterConfig; sFilterConfig.FilterBank 0; sFilterConfig.FilterMode CAN_FILTERMODE_IDMASK; sFilterConfig.FilterScale CAN_FILTERSCALE_32BIT; sFilterConfig.FilterIdHigh 0x0000; sFilterConfig.FilterIdLow 0x0000; sFilterConfig.FilterMaskIdHigh 0x0000; sFilterConfig.FilterMaskIdLow 0x0000; sFilterConfig.FilterFIFOAssignment CAN_RX_FIFO0; sFilterConfig.FilterActivation ENABLE; if (HAL_CAN_ConfigFilter(hcan1, sFilterConfig) ! HAL_OK) { // 过滤器配置失败 }ID掩码模式中Mask全为0时表示不关心任何位所有帧都接收。如果你确定总线上只有一条控制命令可以把Mask设成0x7FFFilter设成0x150这样只接收ID为0x150的帧。但我建议不要一开始就过滤得太窄因为你可能还需要总线上的状态回传帧调试时也需要看第三方设备发的帧。先把全收打开跑通了再收紧。接收中断的回调在HAL库中固定叫HAL_CAN_RxFifo0MsgPendingCallback只在这里拷贝数据然后设置标志。void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; if (hcan-Instance CAN1) { // 确认是CAN1的回调 HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rxHeader, rxData); if (rxHeader.IDE CAN_ID_STD) { // 只处理标准帧 parse_can_frame(rxHeader.StdId, rxData); rx_flag 1; } } }这段代码里的rx_flag是全局标志主循环发现它被置1就知道有新命令来了。注意回调里不要调用HAL_Delay、不要做PWM修改因为PWM寄存器更新需要精确时刻放在回调里很容易和主循环的PWM输出打架。拷完数据就返回让主循环去处理。4.3 把解析后的命令转成PWM占空比现在到了真正让电机转起来的关键一步。F103的定时器产生PWM占空比要被映射成电调油门信号。以最常见的标准电调为例脉冲宽度1000μs到2000μs周期20ms也就是50Hz油门频率。需要使用定时器输出PWM定时器时钟72MHz预分频72-171计数器时钟为1MHzARR20000-1这样PWM周期为20msCCR的值范围1000到2000对应油门脉宽。用TIM2的CH1代码片段如下static void pwm_init(uint16_t pulse_us) { // GPIO PA0 复用为TIM2_CH1 GPIO_InitTypeDef gpio {0}; __HAL_RCC_TIM2_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); gpio.Pin GPIO_PIN_0; gpio.Mode GPIO_MODE_AF_PP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, gpio); htim2.Instance TIM2; htim2.Init.Prescaler 72 - 1; // 1MHz计数频率 htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 20000 - 1; // 20ms周期 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(htim2); TIM_OC_InitTypeDef oc {0}; oc.OCMode TIM_OCMODE_PWM1; oc.Pulse pulse_us; // 初始脉宽 oc.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim2, oc, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); }PWM频率50Hz看起来很低但这和航模电调的标准兼容。不要在PWM初始化时把周期设成1kHz以上除非你确认电调支持多旋翼高速油门输入否则有些电调会直接不响应。参数上Prescaler71是关键它把72MHz降到1MHz这样ARR20000就正好是20ms。设置油门占空比的函数也很简单void set_motor_pulse(uint16_t pulse_us) { if (pulse_us 1000) pulse_us 1000; if (pulse_us 2000) pulse_us 2000; __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, pulse_us); }限幅必须做否则上位机发来一个错误值电机直接满油门转起来这个在机械结构上可能就是一场事故。无论命令是转速还是PWM值在这里统一转换后限幅是最后一道安全闸门。还要注意电调在接收到油门信号后通常需要一点“待机时间”首次上电立即给油门很多电调是没反应的所以工程上常用一个延时逻辑上电后先把脉宽设为1000μs延迟500ms再允许更新。5. 无刷电机控制的常见问题与避坑记录这套系统看起来简单但实际调起来每个环节都能坑你一把。我把自己踩过的坑按“现象-原因-解决”整理在这里基本都是血泪经验。5.1 电机不转先从PWM信号和上电时序找原因现象CAN命令发送正常F103也进入了解析流程PWM输出定时器在工作但电机毫无反应。原因这种情况多半不是CAN的问题而是电调没接收到有效油门信号。第一电调要求的油门信号脉冲宽度范围并不是所有电调都一样有些竞赛电调是1100μs到1900μs按1000μs发送等于还是零油门第二电调上电顺序错误很多电调必须先上电然后接收机给油门信号如果信号和电调上电同时进行电调会进入故障模式。解决先用示波器或者逻辑分析仪检查PA0输出的脉宽是不是在1000μs到2000μs。如果没有示波器把脉宽改成1500μs看电机是否转动。软件上做一个上电延迟先让电调正常启动再发油门信号。另外电调的信号线有些是带BEC供电的注意共地不共地时高电平信号无法被电调识别电机也是不转的。5.2 CAN通信偶发丢失过滤配置和中断优先级踩坑现象报文断断续续上位机显示发送成功F103却经常收不到命令有时候电机转几秒就停一下。原因一个是过滤器Mask设成只收某一个ID但实际总线上还有别的正常状态帧被过滤器卡住造成多节点通讯局部瘫痪。另一个原因是中断优先级问题F103的CAN接收中断优先级如果比定时器和PWM中断低主循环里修改PWM时还会被定时器抢占CAN中断就一直处于等待状态长期来看就会丢帧。解决把过滤器暂时全开确认所有帧都进入FIFO后再在解析里做ID筛选。然后把CAN接收中断优先级调到比定时器PWM中断更高可以使用NVIC设置抢占优先级为3PWM相关的为4。还要检查HAL_CAN_ActivateNotification是不是在每次收完帧后又被关闭了HAL库某些版本在回调处理后会暂停通知需要重新激活一次但这属于极少见情况。5.3 转速抖动PWM频率和油门曲线必须匹配现象电机启动后转速波动尤其在低速段转速忽高忽低电调发出明显的“咔哒”声音。转速闭环时更明显PWM输出来回跳。原因油门信号刷新周期和电调内部控制周期不匹配。标准电调在50Hz下工作如果F103的PWM频率设置成800Hz电调按自己的固件算法每个控制周期只采样一次会表现为油门非线性变化。另一个原因是油门曲线许多电调出厂默认线性油门但有些是S型曲线起始段输出变化很缓慢导致低速时转速闭环的积分项累积过头。解决先把PWM频率固定成和电调说明书一致的频率大部分用50Hz或400Hz。如果要闭环低速段不要用PWM直接映射目标转速而是在固定PWM的基础上做PI修正PWM只作为参考油门再通过霍尔测速反馈微调。这样电调工作点始终在加速区间内不至于进入死区和饱和区。5.4 霍尔信号异常换相逻辑和电气噪声现象在电机轴上加装霍尔传感器用于测速后转速值偶尔跳变甚至出现向上多跳几十转的情况而PWM输出没变。严重时霍尔信号波形毛刺多触发误中断。原因霍尔信号线离电机三相线太近换向瞬间的电流变化产生了强烈电磁干扰尤其是带电流斩波的驱动板噪声更夸张。另一方面是霍尔信号上拉了但上拉电阻太小线与线之间串扰造成误触发。解决霍尔信号线尽量和三相动力线分开走必须交叉时垂直交叉不要平行走线。在F103的输入捕获引脚上并联100nF滤波电容同时在软件里做消抖连续多次捕获到同一个状态才更新速度值。如果霍尔传感器是开漏输出上拉电阻用10kΩ到3.3V不要用1kΩ否则信号边沿还不够陡干扰更容易被当成有效触发。6. 进阶给无刷电机加一个速度闭环让CAN命令变成真正的转速目标前面的链路只能算“能转”距离“能控制转速”还差一步。CAN命令如果只控制PWM那电机转速会随负载变化负载重了转速掉下去上位机根本不知道。加了转速闭环CAN帧里的目标转速才能真正变成电机的实际转速。测速信号来源最简单的就是电调的测速输出通常每转产生一个或者多个脉冲。也可以用霍尔传感器的其中一个霍尔信号单位时间内统计上升沿数量。要算转速先要知道一圈几个脉冲。如果是三霍尔配合一对级电机六个换相周期一圈每个霍尔信号每一圈产生级对数个脉冲按电机参数换算成“每圈脉冲数”。速度计算公式是RPM 60 * 脉冲计数 / (每圈脉冲数 * 测量时间秒)。为了让转速数据稳定我一般测量100ms的脉冲数不测1秒间隔否则闭环带宽太低。100ms能得到每秒十次的速度更新对于底盘电机来说够用。然后写一个轻量PI控制器。PWM脉宽作为输出目标转速和实际转速的误差作为输入。typedef struct { float kp; float ki; float integral; float out_min; float out_max; } PidCtrl; int16_t speed_pid(PidCtrl *pid, int16_t target_rpm, int16_t actual_rpm) { float err (float)(target_rpm - actual_rpm); pid-integral err; if (pid-integral 2000) pid-integral 2000; if (pid-integral -2000) pid-integral -2000; float out pid-kp * err pid-ki * pid-integral; if (out pid-out_min) out pid-out_min; if (out pid-out_max) out pid-out_max; return (int16_t)out; }参数标定没有捷径。我先把kp设为0.2ki设为0.01输出范围限制在1000到1250脉宽让电调工作在中间区域然后慢慢加kp看转速会不会超调振荡。如果转速高频抖动就是kp太大如果低速段稳态误差大适当加ki。F103的浮点运算能力有限但这个PI控制每100ms才执行一次几乎不占CPU。积分限幅一定要做否则电机堵转时积分会积到几千一旦解锁电机会以最大油门瞬间飞转这是非常危险的。从能转到闭环投入产出比很高。闭环还没做的时候电机只是执行机构加了闭环之后电机成为一个有反馈的执行器CAN命令从“告诉PWM”变成“告诉转速”上位机写的运动控制代码一下子变得干净。需要注意的是闭环做完之后还要回头调CAN命令的超时处理如果超过几百毫秒没收到新命令自动转为失能不然通讯断了电机会按最后一条命令继续转。这段路走下来我的教训是不要急着上闭环先把开环PWM限制在安全油门区间确认CAN命令下发、PWM响应、电调和电机之间没有硬伤再往里加反馈。把油门上限限到1500μs可能让人感觉电机没跑出全力但那点“保守”在调试期救过我一次——上位机误发了一条全速命令电机没有冲出试验架就是因为做的第一版限幅就卡在1500μs。希望帮到你。本文还有配套的精品资源点击获取