简介:一套基于STM32 F103微控制器、通过CAN通信命令控制无刷电机的完整工程资料,适合嵌入式开发初学者、电机控制爱好者及参加相关课设或竞赛的学生。内容涵盖CAN协议报文帧结构、STM32 CAN控制器配置、三相逆变器驱动、霍尔传感器位置检测以及PWM调速等关键环节,并涉及六步换相与FOC磁场定向控制等常见策略。资料包内共202个文件,以C源文件、头文件为主,包含55个.h与54个.c,另有编译生成的.o、.d、.axf、.hex及Keil工程文件,便于直接查看、编译与二次开发;整体压缩包约577KB,结构紧凑。目前已有661人学习使用,适合对照源码理解无刷电机控制与CAN通信的实战实现。开发者还整理了相关的学习笔记和示例程序,可帮助快速掌握从CAN消息收发到电机命令控制的完整流程,并根据自身需求进行修改优化。
1. 为什么是 F103+CAN 玩无刷电机:这套控制链路先组成形
做轮式机器人底盘时,我最初的想法是让 STM32F103 直接驱动无刷电机,试了几天发现这条路走不通:F103 的通用定时器顶多输出 PWM,但无刷电机要三相换相,还需要电流闭环和编码器换向,这些活全部压在单片机上根本不现实。后来的方案改成 F103 作为 CAN 主节点,把速度、位置命令发给带 CAN 接口的电机驱动器,驱动器负责换相和控制,通信帧里带设备 ID、命令字和数据,这就是基于 CAN 通信命令控制无刷电机最常见的一套架构。这类项目在网上有完整工程模板,源码里包含 bxCAN 初始化、命令协议、速度环位置环参数,适合机器人、AGV、课程设计和毕设二次开发。拿到手之后,关键是先把「F103 到底负责什么」想清楚:它不碰三相线,只管总线调度和上层逻辑,剩下的交给驱动器。
2. 硬件接线的三件事:驱动方案、收发器与电机相序
2.1 驱动方案怎么选:带 CAN 的 FOC 驱动器 vs 普通 PWM 电调
无刷电机的驱动链路一般有三种常见形态。第一种是 F103 输出 PWM 给航模电调(ESC),电调内部自己换相,这种方案最便宜,一套几十块就能跑起来,但电调只有开环调速能力,没有位置反馈,电机转没转、转到哪,F103 完全不知道。第二种是 F103 直接驱动三相逆变桥配合位置传感器做换相,这就是把 FOC 算法全部写在单片机上,对 F103 来说 72MHz 主频扛得住但算得紧张,而且霍尔安装、死区补偿、电流采样全是硬骨头,不适合工程落地。第三种是我们这篇要展开的:F103 通过 CAN 总线向电机驱动器发命令帧,驱动器内部完成 FOC 换相、电流环和速度环,再把编码器数据回传。
选第三种不是因为它听起来高级,而是它把控制边界切得干净。F103 只负责「发什么指令」,驱动器负责「怎么转」,两边都专注自己的事。下表是我在实际对比后整理的选型参考:
| 对比项 | 带 CAN 接口的 FOC 驱动器 | 普通 PWM 电调 |
|---|---|---|
| 控制指令 | CAN 命令帧,标准帧 11 位 ID | PWM 脉宽,一般 50Hz~400Hz |
| 反馈信息 | 位置、速度、电流、温度、故障码 | 无,最多有转速脉冲输出 |
| 支持的位置控制 | 支持,驱动器内部闭环 | 不支持 |
| 接线复杂度 | 电源+三相+编码器+CAN 四线 | 电源+三相+BEC+PWM 一线 |
| 调试难度 | 中,协议需先定好 | 低,调脉宽就行 |
| 成本 | 高 | 低 |
我一般建议,只要预算允许,优先选带编码器接口且支持 CAN 命令的驱动器,最好能配置成「接收到命令帧才使能,不上电默认待机」。这一点很关键,后面排查章节会专门讲上电飞车问题。如果只是做演示、验证换相能否跑通,普通电调也能用,但别指望它做位置闭环。
2.2 CAN 收发器接线与终端电阻:从 PA11/PA12 到总线
F103 内部的 bxCAN 外设输出的是 TTL 电平,不能直接挂到 CAN_H 和 CAN_L 差分线上,中间必须加一颗 CAN 收发器,例如 TJA1050 或 SN65HVD230。前者是 5V 供电,后者是 3.3V 供电,接法大同小异。F103 的 CAN1 收发引脚固定复用 PA11(RX)和 PA12(TX),工程里需要在 CubeMX 里把 PA11、PA12 的复用功能打开。
收器接线我列了一张表,照着接不会错:
| 信号 | 单片机/收发器引脚 | 去向 |
|---|---|---|
| TX | PA12 → 收发器 TXD | F103 发送数据 |
| RX | PA11 ← 收发器 RXD | F103 接收数据 |
| CAN_H | 收发器 CANH | 总线高线 |
| CAN_L | 收发器 CANL | 总线低线 |
| GND | 收发器 GND | 与 F103 GND 共地 |
| VCC | 按收发器要求接 5V 或 3.3V | 收发器供电,注意加 0.1uF 去耦电容 |
总线两端要各接一个 120Ω 终端电阻,这是 CAN 标准的要求。如果调试时只有两个节点、距离也很短,只在一端接 120Ω 也能工作,但我建议养成两端都接的习惯,否则后面线一长,反射信号会变成错误帧。终端电阻接在 CANH 和 CAN_L 之间,不是串在线上。注意区分:终端电阻是并联在差分线对上的,不是串联在单线里。
电源方面还有个容易翻车的地方。电机动力电源和单片机逻辑电源最好分开,两套电源的 GND 只在一点相连。我踩过的坑是把驱动器和 F103 共用一个电源,电机一启动,母线电压被拉低,CAN 收发器供电跟着波动,总线立刻出现错误帧。后来改成动力 24V、逻辑 5V 独立供电,GND 单点连接,通信才稳定下来。
2.3 无刷电机接线顺序:U/V/W 与编码器线的正确顺序
无刷电机的三相线 U、V、W 要一一对应接到驱动器输出端,不能靠颜色猜,一定要对照驱动器丝印或手册确认。相序接反的典型现象是电机上电后发出嗡嗡声,抖动但不转,或者转起来方向反了、电流很大。如果驱动器支持调相,可以在上位机里交换两相重新标定;如果不支持,就得手动换线。换线时务必先断电,驱动器母线电容残留电压也可能伤人,至少等一分钟以上。
编码器线一般有 VCC、GND、A、B、Z 五根,或者走 SPI 接口。接线时留意编码器供电电压是多少,常见的是 3.3V 或 5V,接错可能直接烧编码器。AB 相线务必用双绞线或者带屏蔽的线,屏蔽层在驱动器端单点接地,不要随便接到 F103 的 GND 上。Z 相是零位脉冲,做位置控制时一定要接,否则每次上电都找不到机械原点。
第一次上电不要带负载,让电机空转,确认换相顺序正确后再装轮子或机械臂。新电机里面没有磁极位置信息,部分驱动器上电后会自动执行转子对齐,转到零位,这个过程看着像「自己动了一下」,其实是正常的。如果上电没有任何动作,查一下使能信号和 CAN 报文,大概率不是电机的问题。
3. 命令帧与 CAN 代码实现:报文设计、初始化与收发函数
3.1 命令帧设计:8 字节报文这样编排才有扩展性
CAN 标准帧的数据区最多 8 字节,控制命令怎么塞进这 8 个字节,协议设计决定了后续扩展是否痛苦。我常用的做法是固定 DLC 为 8,用设备 ID 区分总线上的节点,用命令字区分功能,数据字段按低位在前(小端)排列,最后加一个校验字节。为什么固定 8 字节?因为固定长度可以让接收端不用翻文档猜这一帧到底几个字节,解析逻辑也简单。
一个通用的命令帧格式如下:
| 字节 | 含义 | 说明 |
|---|---|---|
| Byte0 | 命令字 | 0x01 使能,0x02 速度模式,0x03 位置模式,0x04 归零,0x0F 停用 |
| Byte1-2 | 目标速度 | int16,单位 0.1 RPM,低字节在前 |
| Byte3-4 | 目标位置 | int32 拆成两段,或单独定义,低字节在前 |
| Byte5 | 加速度 | uint8,单位 10 RPM/s |
| Byte6 | 标志位 | bit0 相对位置,bit1 立即生效,bit2 清故障 |
| Byte7 | 校验和 | Byte0 到 Byte6 累加取低 8 位 |
这个设计里有一个被很多人忽视的点:位置是 int32 类型,如果要用速度模式,位置字段就闲置了,但千万别在速度模式里把位置字段填 0 然后发给驱动器,因为驱动器可能把它解析成「去绝对位置 0」。我的习惯是速度模式下 Byte3-4 固定填 0,同时 Byte6 的标志位 bit0 置 0,明确告诉驱动器忽略位置字段。这是血泪教训换来的,我在调一个移动平台时,速度命令里带了残留位置值,电机每次启动先猛冲一下,差点把底盘撞墙。
设备 ID 的分配建议留出扩展位。我一般把 F103 主控设为 ID 0x01,电机驱动器从 0x11 开始按顺序分配。回传状态帧把 ID 加 0x80 区分方向,比如驱动器 0x11 回传帧 ID 就是 0x91,这样主控收到 0x91 就知道是对应 0x11 的应答,不需要额外字段。总线帧过滤就靠这个 ID 规则来写。
3.2 CAN 初始化、滤波器与中断接收代码
下面这份初始化代码用 STM32 HAL 库,工程创建时在 CubeMX 里勾选 CAN1,时钟配置 APB1 为 36MHz。先看滤波器配置部分:
void CAN_Init(void) { CAN_FilterTypeDef filter; filter.FilterBank = 0; // 使用过滤器组0 filter.FilterMode = CAN_FILTERMODE_IDMASK; // 掩码模式 filter.FilterScale = CAN_FILTERSCALE_32BIT; // 32位宽度 filter.FilterIdHigh = 0x0000; // 标准帧ID高16位 filter.FilterIdLow = 0x0000; // 标准帧ID低16位 filter.FilterMaskIdHigh = 0x0000; // 掩码全0 = 不屏蔽 filter.FilterMaskIdLow = 0x0000; filter.FilterFIFOAssignment = CAN_RX_FIFO0; // 挂到FIFO0 filter.FilterActivation = CAN_FILTER_ENABLE; // 激活过滤器 HAL_CAN_ConfigFilter(&hcan, &filter); HAL_CAN_Start(&hcan); HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING); }这里要解释两个关键参数。掩码模式下,Mask 位为 0 表示该位不参与匹配,全部放行;Mask 位为 1 表示对应 ID 位必须匹配。我调试初期把 Mask 全部置 0,意思是不管收到什么 ID 都进 FIFO0,先把链路跑通,等通信稳定后再收紧掩码只接收 0x01 和 0x11。FilterBank = 0 是因为 F103 只有 14 组滤波器,这个值可以任意选,但要和实际使用保持一致。FIFO 分配我挂在 FIFO0 上,对应的中断回调函数是 HAL_CAN_RxFifo0MsgPendingCallback,如果你挂到 FIFO1,回调函数名就不同,这个细节坑过不少人。
接收中断的回调函数写法如下:
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rxHeader; uint8_t rxData[8]; if (hcan->Instance == CAN1) { HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, &rxHeader, rxData); // 中断里只记录帧数据,不处理业务逻辑 g_can_rx_id = rxHeader.StdId; memcpy(g_can_rx_data, rxData, 8); g_can_rx_flag = 1; } }为什么中断里只做拷贝不做解析?因为 CAN 中断优先级如果设置得比较高,在中断里做协议解析和电机控制逻辑可能抢占主循环,导致时序错乱。我一般把中断回调里的事情压缩到最少,置一个标志位,主循环检测到标志再去处理。rxHeader.StdId 是标准帧 ID,如果用到扩展帧,要判断 rxHeader.IDE 然后取 ExtId。这里的 g_can_rx_flag 要定义为 volatile 类型,否则编译器优化后主循环可能永远看不到标志变化。
发送函数也不能省,这是我打包好的常用写法:
HAL_StatusTypeDef CAN_SendFrame(uint32_t id, uint8_t *data, uint8_t len) { CAN_TxHeaderTypeDef txHeader; uint32_t mailbox = 0; uint8_t retry = 0; txHeader.IDE = CAN_ID_STD; // 使用标准帧 txHeader.RTR = CAN_RTR_DATA; // 数据帧 txHeader.DLC = len; // 数据长度 1~8 txHeader.StdId = id; // 设备ID // 等待有空闲邮箱,最多等1ms while (HAL_CAN_GetTxMailboxesFreeLevel(&hcan) == 0 && retry < 10) { HAL_Delay(1); retry++; } if (HAL_CAN_AddTxMessage(&hcan, &txHeader, data, &mailbox) != HAL_OK) { return HAL_ERROR; } return HAL_OK; }F103 的 bxCAN 有三个发送邮箱,如果三个全占满,AddTxMessage 会返回错误。我加了一个循环等待空邮箱的重试逻辑,最多等 10 毫秒。要注意 HAL_CAN_GetTxMailboxesFreeLevel 的返回值,三个邮箱全空时返回 3,全满时返回 0。发送高优先级帧时还可以在 AddTxMessage 之前调用 HAL_CAN_AbortTxRequest 取消低优先级帧,但对 F103 这种小工程来说,先到先发配合重试基本够用。
3.3 主循环里的命令调度:使能、速度设定、位置归零
有了收发函数,接下来是命令解析和调度。我习惯把控制逻辑拆成两个函数:一个是命令解析,按 ID 和命令字分派;另一个是电机控制,负责把目标值写入驱动器。下面这段是命令解析的最小实现:
void CAN_CommandProcess(void) { uint8_t cmd; int16_t speed; int32_t position; if (!g_can_rx_flag) return; g_can_rx_flag = 0; if (g_can_rx_id == 0x01) // 来自上位机的命令 { cmd = g_can_rx_data[0]; switch (cmd) { case 0x01: // 使能电机 CAN_SendFrame(0x11, (uint8_t[]){0x01, 0, 0, 0, 0, 0, 0x00, 0x01}, 8); break; case 0x02: // 速度模式 speed = (int16_t)(g_can_rx_data[1] | (g_can_rx_data[2] << 8)); // 将speed写入驱动器命令帧,Byte0=0x02 break; case 0x03: // 位置模式 position = (int32_t)(g_can_rx_data[3] | (g_can_rx_data[4] << 8) | (g_can_rx_data[5] << 16) | (g_can_rx_data[6] << 24)); // 将position写入驱动器命令帧 break; default: break; } } }这段代码的逻辑说明要展开:speed 是小端格式,低字节在 data[1],高字节在 data[2],组合成 int16 时先按 uint8 读取再移位,避免符号扩展问题。如果只写(int16_t)(data[1] | (data[2] << 8)),当 speed 是负数时,因为移位前 data[2] 是 uint8 类型,左移 8 位变成 0xFFFF 和谐,类型转换后符号位正确,这个写法没问题。位置值同理,int32 拆成四个字节再拼回来。
命令分派里还隐含了一个设计决策:F103 接收到上位机命令后,不是直接驱动电机,而是重新构造一帧转发给驱动器,这相当于主控做了协议转换和校验。我见过有人把上位机的命令帧直接透传给驱动器,省掉中间层,但这样一旦需要做限位保护、急停过滤,就得改上位机逻辑,远不如在 F103 里加一道关卡灵活。我的工程里 F103 的代码体积不大,但每次命令都要先过一过安全逻辑,比如速度上限、位置边界、急停状态,这是我非常坚持的一个习惯。
4. 排查:四类现场与修复步骤
CAN 总线调试最大的问题是「黑匣子」特性:没有错误指示灯,没有调试串口,出问题只能靠现象推测。我在现场踩过的坑可以归成四类,每类都有明显的现象特征和对应解法。
4.1 一直接收不到数据
现象:F103 程序跑起来,定时器在走,但 HAL_CAN_RxFifo0MsgPendingCallback 断点进不去,或者打断点后发现根本没触发。
原因:我后来逐项排查发现,这个问题八成出在滤波器掩码配置上。FilterMaskIdLow 和 FilterMaskIdHigh 如果写成匹配特定 ID,而总线上实际发的 ID 不在匹配范围内,帧会被硬件过滤掉,中断根本不会触发。另一个常见原因是 CubeMX 里只开启了 CAN1 外设时钟,但 NVIC 中断没有勾选,导致软件中断向量没挂上。
解决:第一步,把滤波器改成掩码全 0,即不屏蔽任何 ID,先验证总线上是不是真的有帧进来。第二步,确认 CubeMX 的 NVIC 设置里 CAN1 RX0 interrupt 被打勾,并且中断优先级给一个合适的抢占优先级。第三步,用一个 USB-CAN 分析仪或者另一块 F103 开发板做回环测试,A 板发 B 板收,排除驱动器和收发器硬件问题。我那次折腾了一天,最后发现是掩码配置写反了:IdHigh 和 MaskIdHigh 填的是同一个值,本应该是掩码的 0xFFFF 结果被写成了 0x0000,等于把匹配位全屏蔽了。
4.2 波特率不匹配导致通信时通时断
现象:两台设备偶尔能通、偶尔不通,用示波器看 CAN_H 和 CAN_L 波形完全正常,但 HAL_CAN_GetError 出现的错误计数不断上涨,最后进入 BusOff 状态。
原因:F103 的 CAN 外设时钟来自 APB1,不是 APB2,这是最容易算错的地方。如果 APB1 实际是 36MHz,而代码按 72MHz 去配置预分频,实际波特率会翻倍,总线上的两个节点波特率不一致,CAN 协议本身没有容错机制,结果就是错误帧不断。另外,采样点设置太靠前或太后,在总线较长时也容易误判电平。
解决:先确认时钟树。CubeMX 里把 APB1 分频系数设为 2,得到 36MHz,这是 F103 最常见的外设时钟。然后按位时间公式反推:位时间 tq 数 = 1(同步段)+ BS1 + BS2。想要 500kbps,可以用预分频 6、BS1=7、BS2=4,即 CAN 时钟 36MHz/6=6MHz,位时间 6MHz/(1+7+4)=500kbps。我实际调试时常用 500kbps 作为默认速率,因为它在稳定性和速率之间比较均衡。总线超过 5 米时降到 250kbps 更稳妥。
4.3 电机上电瞬间飞车或乱转
现象:整个系统一上电,电机还没收到任何指令就自己转起来,或者转一下停一下,静止时发出高频噪声。
原因:这个现象我碰到过三次,三次的原因还不一样。第一次是驱动器默认使能,上电瞬间 F103 还没初始化完成,CAN 总线上没有主控报文,驱动器处于自由状态,自己按内部默认值开始运行。第二次是 F103 复位期间 IO 口处于高阻态,CAN 收发器的 TXD 引脚被上拉或下拉到某个电平,产生了一帧垃圾数据,驱动器把这帧数据误解析为「速度 50 RPM 使能」。第三次最玄学:驱动器上电自检过程中会自动执行转子对齐,电机会先动一下,这个动作本身是自己转,不是飞车。
解决:根本思路是让驱动器「默认失能,收到明确命令才使能」。具体做法有三个层面。第一,如果驱动器支持配置,把上电默认状态改为 Disable,不响应任何速度命令,只响应使能命令。第二,F103 的主循环里加一个上电延时,先静默 500ms,等 CAN 总线电平稳定后,先发一帧「Disable」命令再发「Enable」命令,避免上次残留的命令被复用。第三,在命令帧的 Byte6 标志位里加一个 bit2,表示「清故障后使能」,确保驱动器收到使能命令时先清除自身错误状态。从那以后我每次上电都强制走一遍「静默-失能-使能」流程,再也没遇到飞车。
4.4 重载时 CAN 偶发掉线
现象:电机空转的时候通信一切正常,一挂负载、电流一大,CAN 通信就开始报错,严重时驱动器自动离线。
原因:这是典型的电源和地线问题。电机大电流工作时,动力电源的 GND 电位会产生波动,如果信号地(F103 和收发器的 GND)和动力地走的是同一条线,地电位差直接叠加在 CAN 收发器的参考地上,总线差分信号被破坏。另一个原因是 CAN_H 和 CAN_L 布线太靠近电机动力线,大电流瞬间的电磁干扰耦合进差分对。
解决:把逻辑电源和动力电源彻底分开,两套 GND 只在电源输入端单点相连。CAN 双绞线远离电机三相线,走线不要平行,交叉时要垂直。终端电阻确认两端都有,必要时在收发器的 CANH 和 CANL 之间加一个小共模电感滤干扰。还有一招是降低波特率到 250kbps,低波特率对噪声的容忍度更高。我最后那个项目是同时改了共地和终端电阻,才彻底根治,单独改其中一个都只能缓解。
4.5 电机抖动但不转
现象:上电后电机不转,发出明显的嗡嗡声,用手拧转子感觉有阻力,但就是没有连续旋转。
原因:可以先听声音判断。如果是高频吱吱声,大概率是 FOC 电流环参数没调好或者编码器方向反了,驱动器内部的角度反馈和实际转子位置对不上,电流一直在正负间震荡。如果是低频嗡嗡声,大概率是三相线相序接错,换相顺序不对,转子被锁住。
解决:先断电换任意两相线,比如 U 和 V 对调,再上电试一次。如果换相后正常转了,说明原来相序反了。编码器方向导致的抖动,需要看驱动器有没有「编码器方向取反」的参数,FOC 正转时编码器计数值增加,反转时减少,如果方向和电机转向定义矛盾,电流环就会震荡。还有一种可能是 PWM 开关频率设置偏低,无刷电机一般建议 16kHz 以上,低了人耳能听到明显噪声,还会加剧换相抖动。调参数时一步步来,每次只动一个变量,叠了三个参数一起调,出了问题根本分不清是哪个引起的。
5. 进阶验证:环回模式与上电前的三个检查习惯
调试 CAN 通信不一定每次都要接总线和驱动器,STM32F103 的 bxCAN 内置了一个环回模式,可以在不接外部总线的情况下验证软件链路。把 hcan.Init.Mode 改成 CAN_MODE_LOOPBACK,再用 CAN_SendFrame 发一帧数据,这帧数据会自动被自己接收,进入 FIFO0 触发接收回调。这个模式适合在焊好板子但没有驱动器的时候,先确认初始化、发送、接收三块代码是通的。切记环回模式是单节点自测,它验证不了收发器硬件,也验证不了总线上的其他节点。
调试链路由短到长,我总结了一个检查顺序,每次踩完坑之后都会强制走一遍:
| 检查项 | 操作 | 预期结果 |
|---|---|---|
| 软件链路 | 环回模式自测 | 发送帧能被自己收到 |
| 收发器硬件 | 示波器测 CAN_H、CAN_L 对 GND 波形 | 差分幅值 2V 左右,帧间隔有隐性电平 |
| 终端电阻 | 断电状态下万用表量 CANH-CANL | 两端接入后总电阻约 60Ω |
| 波特率一致性 | 上位机与 F103 相同配置 | 无错误帧,能正常收发 |
| 上电顺序 | 先上位机、再总线上电、最后发使能命令 | 电机待机,不飞车 |
这个表看起来简单,但每一条背后都有真实翻车的代价。我印象最深的一次是在实验室调四轮底盘,四个电机都用 CAN 挂在一个 F103 上,我从早上开始调,一直调不通,最后发现是驱动器的 CAN 收发器的 VCC 引脚没有接去耦电容,电机一启动电源纹波干扰收发器,导致错误帧率飙升。从那以后,我每次上电前都强制走一遍这个检查清单:先示波器看波形,再量终端电阻,最后环回自测,确认无误才给驱动器发使能命令。这条习惯救了我很多次,不只是省时间,更重要的是避免在问题链路里反复怀疑代码,把简单问题复杂化。
如果你拿到的工程模板里已经有 CAN 初始化、滤波器、命令分派的框架,建议你先把环回模式跑通,理解每一帧数据从进入到出来的完整路径,再接到真实电机驱动器上。希望帮到你。
本文还有配套的精品资源,点击获取