资讯详情

STM32F103 CAN总线无刷电机控制:协议解析与工程实现

📅 2026/10/20 11:55:30 | 华诺云谱 👁 阅读
STM32F103 CAN总线无刷电机控制:协议解析与工程实现
简介一套基于STM32F103单片机与CAN通信实现无刷电机命令控制的完整工程资源面向嵌入式开发者和电机控制初学者适用于无人机、电动车及工业自动化等需要远程调速与状态监控的场景。项目完整覆盖CAN报文收发、三相逆变器驱动、霍尔传感器位置检测与PWM换相控制能够展示从上位机下发指令到电机实际响应的全链路实现帮助读者建立系统级认识。资源包共202个文件压缩后约577KB以C源码和头文件为主同时含有编译生成的axf、hex固件以及Keil工程配置便于直接打开研究或烧录验证。已有661人浏览学习包内除工程源码外还整理了CAN模块配置方法、六步换相与FOC控制思路、模块化程序结构及开发者学习笔记清除了上手障碍。对于希望快速入门STM32 CAN总线电机控制或基于F103做二次开发的读者是一份兼具参考性和实用性的资料。1. 项目定位与技术选型为什么是F103CAN无刷电机在设备调试现场最让人头疼的往往不是算法写不出来而是多台无刷电机各自为政PWM线、方向线、使能线拉了一捆一上电就互相干扰。这个项目就是经典的“STM32F103 CAN通信 无刷电机”组合上位机或主控通过CAN总线发送命令帧节点板解析帧ID和Data字段控制一台无刷电机启停、调速、回报状态。核心价值在于把多电机控制从“每台电机一对线”变成“一根总线挂所有节点”适合正在做机器人底盘、分拣机构、多轴联动设备的开发者也适合想把电机控制纳入总线协议体系的同学拿去当工程框架。2. 系统架构与硬件设计从CAN收发器到电机驱动桥的完整链路2.1 整体架构CAN总线如何把主机和电机节点串起来这个系统在物理上分成两类节点主机节点和电机控制节点。主机可以是另一片STM32F103也可以是带CAN口的工控板、树莓派加扩展模块甚至是一台USB-CAN分析仪。电机控制节点才是项目的主体它要做的事情可以拆成三条链路接收链路CAN收发器→bxCAN外设→接收中断→命令解析、控制链路命令解析结果→定时器PWM→栅极驱动→三相全桥→无刷电机、反馈链路霍尔传感器→GPIO/输入捕获→换相和测速。用CAN而不是I²C或UART是因为无刷电机控制现场有两个硬指标一是实时性命令帧从总线上出现到电机开始响应整个过程应该在几百微秒内完成二是抗干扰电机驱动部分的大电流开关会让普通串口信号劣化而CAN的差分信号和错误检测机制能容忍较高的共模噪声。再加上CAN总线天然支持多主机仲裁这意味着以后增加节点时不需要改任何现有代码的地址配置直接把新节点挂到总线上就行。协议层面项目采用了CAN 2.0标准帧波特率按500kbps配置应用层协议则完全自定义。主机发命令帧节点回状态帧用帧ID区分命令类型用Data段携带参数。这种“物理层标准、应用层自定义”的做法在电机控制场景里非常常见既保留了CAN的可靠性又让协议足够精简便于在中断里快速解析。2.2 器件选型和接线每个引脚都要有数主控芯片用的是STM32F103C8T672MHz主频128KB Flash20KB RAM。在方波六步换相方案下这颗芯片的计算余量非常充足即使把PID闭环、状态回报、命令解析全部跑在中断里CPU占用率依然很低。CAN收发器选择的是常见的TJA1050它把MCU侧的TXD/RXD电平转换成CAN_H和CAN_L差分信号直接驱动双绞线。电机驱动部分有两种做法一种是直接购买集成好的无刷驱动模块MCU只输出PWM和方向信号风险低但控制粒度受模块限制另一种是用三相栅极驱动芯片加6个MOS管自己搭全桥灵活性高适合需要深入改驱动时序的项目。本文按后者讲解因为源码工程里配套的是分立方案。引脚分配遵循一个原则CAN和霍尔这类实时信号优先挂在有中断映射的引脚上PWM输出挂在高级定时器通道上。推荐分配如下引脚功能接线对象PA11CAN_RXTJA1050的RXDPA12CAN_TXTJA1050的TXDPA0/PA1/PA2霍尔A/B/C电机霍尔传感器输出PA8/PA9/PA10PWM/方向栅极驱动芯片输入PB0/PB1使能/故障输入驱动板Fault信号霍尔传感器通常是开漏输出必须在PA0/PA1/PA2上各接一个4.7kΩ到10kΩ的上拉电阻否则电平浮空会导致换相状态乱跳。供电方面电机母线建议单独供电控制板用LDO降压到5V给MCU和收发器供电两者只共地不在同一块铜皮上走大电流。总线两端各接一个120Ω终端电阻其中一个可以直接焊在节点板的CAN_H和CAN_L之间方便单节点调试。3. 核心代码实现CAN初始化、命令解析与PWM调速三段式3.1 CAN外设初始化波特率、过滤器与接收中断用标准外设库配置CAN1首先打开GPIOA和CAN1的时钟把PA11、PA12配置为复用推挽输出。这里有个容易踩的细节CAN的TX和RX引脚在F103上属于“复用功能开漏”还是“复用功能推挽”取决于收发器类型对于TJA1050这种电平型收发器配置成复用推挽即可。void CAN_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; CAN_InitTypeDef CAN_InitStructure; CAN_FilterInitTypeDef CAN_FilterInitStructure; NVIC_InitTypeDef NVIC_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_11 | GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); CAN_InitStructure.CAN_TTCM DISABLE; CAN_InitStructure.CAN_ABOM ENABLE; // 自动离线恢复 CAN_InitStructure.CAN_AWUM ENABLE; // 自动唤醒 CAN_InitStructure.CAN_NART DISABLE; // 禁止自动重发便于调试 CAN_InitStructure.CAN_RFLM DISABLE; // FIFO不锁定 CAN_InitStructure.CAN_TXFP DISABLE; // 按ID优先级发送 CAN_InitStructure.CAN_Mode CAN_Mode_Normal; // 位时间 1 BS1 BS2 8 tq // 36MHz / 9 / 8 500kbps采样点75% CAN_InitStructure.CAN_SJW CAN_SJW_1tq; CAN_InitStructure.CAN_BS1 CAN_BS1_5tq; CAN_InitStructure.CAN_BS2 CAN_BS2_2tq; CAN_InitStructure.CAN_Prescaler 9; CAN_Init(CAN1, CAN_InitStructure); // 掩码模式不过滤任何ID CAN_FilterInitStructure.CAN_FilterNumber 0; CAN_FilterInitStructure.CAN_FilterMode CAN_FilterMode_IdMask; CAN_FilterInitStructure.CAN_FilterScale CAN_FilterScale_32bit; CAN_FilterInitStructure.CAN_FilterIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdHigh 0x0000; CAN_FilterInitStructure.CAN_FilterMaskIdLow 0x0000; CAN_FilterInitStructure.CAN_FilterFIFOAssignment CAN_Filter_FIFO0; CAN_FilterInitStructure.CAN_FilterActivation ENABLE; CAN_FilterInit(CAN1, CAN_FilterInitStructure); CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); // FIFO0消息挂号中断 NVIC_InitStructure.NVIC_IRQChannel USB_LP_CAN1_RX0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }CAN挂在APB1总线上主频72MHz时APB1是36MHz。重点看波特率计算位时间由同步段1tq、传播/相位段BS1 5tq、相位段BS2 2tq组成共8tq。36MHz除以预分频9得到4MHz的tq时钟再除以8得到500kbps采样点位于75%处这是CAN总线最常用的配置。如果换用8MHz外部晶振以外的时钟源需要重新计算这三个参数代码里写死的数值不能照搬。过滤器配置成掩码模式且全部置0表示接收所有帧。实际项目中建议后期改成CID掩码过滤只接收主机下发的命令帧减少中断频率。CAN_ABOM置ENABLE是关键——总线出现严重错误导致节点离线时硬件会自动恢复否则节点会一直沉默到复位。3.2 报文接收与命令解析把CAN帧变成电机控制参数接收中断里做的事情越少越好。进入中断后先从FIFO0读出CanRxMsg结构体根据标准帧ID做switch分发解析Data段参数然后立刻清中断标志。项目里命令帧ID按功能划分0x01启停、0x02目标速度、0x03查询状态全部采用标准帧。void USB_LP_CAN1_RX0_IRQHandler(void) { CanRxMsg RxMsg; int16_t speed_value; uint8_t err_code 0; CAN_Receive(CAN1, CAN_FIFO0, RxMsg); if (RxMsg.IDE CAN_Id_Standard) { switch (RxMsg.StdId) { case CMD_START: // 0x01 Motor_Enable(1); break; case CMD_STOP: // 0x02 Motor_Enable(0); break; case CMD_SET_SPEED: // 0x03 // Data[0]高字节Data[1]低字节int16带符号单位RPM speed_value (int16_t)((RxMsg.Data[0] 8) | RxMsg.Data[1]); Speed_SetTarget(speed_value); break; case CMD_QUERY_STATUS: // 0x04 err_code Motor_GetErrorCode(); Status_Report(err_code); break; default: break; } } CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); }命令解析的关键在Data字段的边界定义。速度值用int16传输范围是-32768到32767RPM对绝大多数无刷电机够用如果后续要支持更高的分辨率可以把单位改成“0.1RPM”或者把速度值拆成转速和方向两个字节。这里使用大端序高字节在前和CAN分析仪里常见的十六进制显示习惯一致方便调试时直接对照。状态帧的组装在另一个函数里完成节点主动上报当前转速、母线电压和错误码。状态上报的意义不只是给上位机看它也是心跳机制的基础——主机通过“连续一段时间没收到某节点的状态帧”来判断节点是否掉线后面第4章会展开讲。3.3 方波换相与PID调速霍尔反馈闭环方波驱动即六步换相根据三路霍尔信号的状态组合决定三相桥臂的通断组合。霍尔信号是三个引脚的二进制组合8种状态中6种有效对应6个电压矢量。换相表是这类项目的核心数据务必和实际接线顺序保持一致。const uint8_t Hall_CommutTable[8] { 0, // 0b000 非法状态 PWM_PHASE_AB, // 0b001 PWM_PHASE_AC, // 0b010 PWM_PHASE_BA, // 0b011 PWM_PHASE_BC, // 0b100 PWM_PHASE_CA, // 0b101 PWM_PHASE_CB, // 0b110 0, // 0b111 非法状态 }; void Hall_Update(void) { uint8_t hall_raw; uint8_t phase; hall_raw (uint8_t)(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) | (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) 1) | (GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_2) 2)); phase Hall_CommutTable[hall_raw]; if (phase ! 0) { TIM_SetCompare1(TIM1, PWM_ON); // 上桥臂PWM TIM_SetCompare2(TIM1, PWM_OFF); // 对应相下桥臂关闭 // 其余桥臂按phase定义切换 } }霍尔信号每变化一次就代表转子转过一个电气60度角代码里用GPIO读取的方式完成换相。更高效的做法是把三路霍尔接到定时器的输入捕获通道利用定时器编码器模式直接得到速度和方向但GPIO读表方式简单直接适合工程起步。转速闭环用增量式PID实现。目标速度来自CAN命令帧当前速度通过霍尔换相频率换算转速(rpm) 电气频率 × 60 ÷ 极对数而电气频率可以从单位时间内的换相次数算出来也可以用输入捕获测霍尔周期。PID更新放在定时器中断里以1kHz频率执行。int16_t Speed_PID_Update(int16_t target_rpm, int16_t current_rpm) { int32_t error target_rpm - current_rpm; static int32_t integral 0; int32_t derivative error - last_error; int32_t output; integral error; if (integral INTEGRAL_LIMIT) integral INTEGRAL_LIMIT; if (integral -INTEGRAL_LIMIT) integral -INTEGRAL_LIMIT; output KP * error KI * integral KD * derivative; output CLIP(output, -PWM_PERIOD, PWM_PERIOD); // 限幅到PWM周期 last_error error; return (int16_t)output; }PID参数整定有个血泪经验先把KI和KD置0只保留KP从很小的值开始加直到电机出现轻微震荡再退回到70%左右然后加KI消除静差最后加KD抑制超调。积分限幅INTEGRAL_LIMIT一定要有否则电机堵转时积分会无限增大输出饱和后转速恢复会异常缓慢。4. 调试与组网从单节点调通到一主多从的状态上报4.1 通信物理层终端电阻、电平和波特率验证CAN通信调不通时先别急着看代码。用万用表量一下节点板上CAN_H和CAN_L之间的电阻如果只有一根总线且两端都没接终端电阻测出来是“断路”接上两个120Ω后应该在60Ω左右。这一步能排除一大半“通信时好时坏”的问题。再看总线电平示波器挂CAN_H对地静态时应约2.5V通信时显性电平会拉到3.5V左右CAN_L则相反显性时掉到1.5V。如果测到的静态电平是0V或5V说明收发器没有正常工作大概率是TXD/RXD接反或者收发器的电源和地没共好。差分信号的好处是两条线互相绞合后抗共模干扰强但前提是收发器电源必须与控制板共地浮地会导致CAN_H和CAN_L对地电平整体漂移表现为能发不能收。波特率验证更直接用USB-CAN分析仪挂在总线上把分析仪配成500kbps如果能正常看到主机发的帧说明物理层和波特率都没问题。如果分析仪上显示总线错误或者CRC错误帧优先怀疑本机的BS1/BS2/Prescaler组合算错了波特率而不是怀疑干扰。4.2 一主多从组网ID分配、心跳和状态帧单节点调通后扩展多节点只需要做两件事给每个节点分配不同的基础ID让主机下发命令时带上节点地址。推荐按地址段划分ID空间给未来协议升级留出余地帧类型ID范围方向说明命令帧0x101~0x1FF主机→节点低字节为节点地址状态帧0x201~0x2FF节点→主机低字节为节点地址广播帧0x300主机→所有节点全体启停/急停这种划分方式的优势在于过滤器配置非常简单主机只收0x2xx范围内的状态帧节点只收0x1xx范围内的命令帧用掩码模式一条过滤器即可完成不需要为每个节点单独配置。心跳机制是组网稳定性的核心。每个节点以500ms为周期主动上报状态帧帧内Data[0]是当前转速高字节Data[1]是低字节Data[2]是错误码Data[3]是节点自检标志。主机在接收中断里更新该节点的时间戳主循环里周期性轮询所有节点时间戳超过1.5秒没更新就标记该节点掉线触发急停或声音报警。这样即使某个节点因为过流保护复位主机也能在2秒内感知到而不是等到操作人员发现设备不转了才排查。4.3 调试工具与上位机快速验证调试阶段我一般不用完整上位机而是用Python的python-can库写一个几十行的脚本模拟主机发送命令帧。这样可以精确控制发送时机和帧内容配合CAN分析仪抓总线报文比在嵌入式代码里打断点高效得多。import can import time bus can.interface.Bus(channelcan0, interfacesocketcan, bitrate500000) # 启动电机 bus.send(can.Message(arbitration_id0x101, data[], is_extended_idFalse)) time.sleep(0.5) # 设置目标速度 600 RPM speed 600 data [(speed 8) 0xFF, speed 0xFF] bus.send(can.Message(arbitration_id0x103, datadata, is_extended_idFalse)) # 等待节点状态帧 while True: msg bus.recv(timeout1.0) if msg and 0x201 msg.arbitration_id 0x2FF: rpm (msg.data[0] 8) | msg.data[1] print(fcurrent rpm: {rpm})Python脚本的每个字段都与嵌入式端协议严格对应0x101是1号节点的启动命令0x103是1号节点的调速命令Data字段大端序。用这种“脚本当主机”的方式调通一个节点后再逐步仿真多节点场景比如循环给1到4号节点轮流发调速命令观察每个节点的实际转速是否跟上目标值。5. 踩坑实录CAN通信与电机驱动的五个常见问题5.1 现象一FIFO里有报文但接收中断一次都不触发中断就是进不去但用CAN分析仪能看到总线上确实有帧。排查后发现总线上的帧确实进了CAN1的FIFO1而不是FIFO0。原因初始化时把过滤器分配给了FIFO1却开启了CAN_IT_FMP0中断使能。报文进FIFO1时触发的是FMP1中断两个中断向量编号不同使能错位就收不到。解决检查CAN_FilterInitStructure.CAN_FilterFIFOAssignment要么把它改成CAN_Filter_FIFO0要么把中断使能改成CAN_IT_FMP1并注册CAN1_RX1_IRQn。顺手确认NVIC里注册的中断通道名和FIFO编号一致这是标准库工程里翻车频率最高的一处。5.2 现象二电机上电后高频异响转子抖动不转上电后电机没有平滑转动而是“咔咔”响手拨一下转子能转一小段然后又卡住。用逻辑分析仪抓霍尔信号发现三路信号变化顺序和换相表里的状态映射对不上。原因霍尔传感器接线顺序和代码中换相表定义的A/B/C顺序不一致。比如霍尔物理上按A-C-B排列代码却按A-B-C读取导致同一个转子位置被解释成另一个电气角换相矢量落后或超前。解决用一根细线逐个交换霍尔输入引脚观察哪个组合下电机能顺畅启动。找到正确顺序后要么改硬件接线要么改Hall_CommutTable数组的映射顺序。特别注意无刷电机有电角度对齐问题霍尔装反会导致零位误差换相表和实际的60度区间对不上。5.3 现象三两个节点点对点通信正常挂第三个节点后随机丢帧两个节点时一切正常加入第三个节点后主机偶尔收不到状态帧用CAN分析仪看还有错误帧出现。万用表量总线电阻只有一端有120Ω另一端没接。原因组网后总线上的反射信号增强缺少终端电阻导致信号边沿振铃在500kbps波特率下采样点刚好踩在振铃区域就会采样错误。解决总线两端各接一个120Ω终端电阻。注意“终端电阻”不是“每个节点都接”是物理最远的两端各一个。如果节点间距离较远还要检查分支线长度CAN规范里分支应该尽可能短过长的支线相当于一根天线会反射信号。5.4 现象四电机空载正常带负载后速度上不去还伴随电流尖峰给电机加上负载后实际转速明显低于目标转速母线电流波动剧烈。代码里PID输出已经到限幅值但转速就是提不上去。原因方波驱动在低速大扭矩场景下换相噪声大PID调节速度跟不上负载突变更关键的是没有做电流限幅大电流冲击下母线电压跌落又反过来影响霍尔信号质量形成恶性循环。解决先把加速过程做斜坡限制目标速度变化时每次只允许50RPM/10ms的步进让电流平滑上升同时在PWM周期里加入电流采样超过阈值直接关闭PWM输出而不是等PID慢慢回调。从控制逻辑上看方波驱动的扭矩能力上限由母线电压决定PID参数调得再激进也无法突破物理极限。5.5 现象五把CAN引脚重映射到PB8/PB9后代码没改就收不到帧为了避开PA11/PA12上的其他复用功能把CAN1重映射到PB8/PB9但只改了GPIO初始化没有重映射AFIO结果总线上一片寂静。原因F103的CAN1重映射需要打开AFIO时钟并调用GPIO_PinRemapConfig(GPIO_Remap1_CAN1, ENABLE)否则GPIO虽然配置了但内部信号还走默认引脚PA11/PA12。解决重映射三步走——打开AFIO时钟、调用GPIO_PinRemapConfig、把GPIO_Pin换成PB8/PB9。这类“引脚没反应”的问题有一个共同排查思路先看外设时钟开没开再看AFIO重映射开没开最后看NVIC对应中断通道注册没注册顺序反了都会静默失败。6. 二次开发把单节点命令控制扩展成分布式运动系统这套框架改造方向很多我实际用过的一个路径是往“位置模式”扩展。霍尔六步换相只能提供电气60度分辨率定位精度不够改造方法是加一颗磁编码器比如AS5600这类I²C输出芯片替换霍尔作为转子位置反馈源速度环保留原来的PID结构位置环在其外层CAN命令从“目标转速”扩展成“目标角度”帧ID增加一条0x05Data字段变成目标角度值。协议扩展时有一个技巧不要在现有switch-case里无限加case而是把命令解析升级成状态机加命令表。命令表结构体包含帧ID、数据长度、处理函数指针新命令进来时查表调用对应函数。这样后续加命令、加节点、加功能都不再动中断里的主逻辑只需要在表格里追加一行。我习惯在每张命令表初始化时把帧ID、方向、长度、单位写进注释里因为CAN帧协议最怕的就是改了字段后两边不同步。如果目标是FOC无感控制那这套工程里能用的是CAN命令解析和PID框架换相部分要全部重写。FOC需要Clark/Park变换、SVPWM和角度观测器F103的算力做单电机FOC勉强够但建议换成F103的进阶型号留出余量给观测器和电流环。验证方法很朴素先用脚本单发一条位置命令看电机是否转到目标角度并稳定再连续下100条随机位置命令记录每次的到位误差和超调量。从那以后我每次改协议都会先在帧格式表里写好ID、长度和单位三者对应的关系再动手改代码几乎没再犯过“字段解析错位”的毛病。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑