资讯动态

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

发布时间:2026/10/20 4:47:29 来源:尧图企业网站定制
简介这是一套基于STM32F103的CAN通信无刷电机控制项目工程资料面向嵌入式开发者和电机控制初学者帮助打通“CAN总线配置—命令收发—电机驱动”的完整链路。压缩包共202个文件7z格式仅577KB以C源码与头文件为主体55个h、54个c还包含Keil工程配置、编译中间文件o、crf等与调试辅助信息打开工程即可对照学习。目前已有661人浏览学习对入门者具有较高参考价值。资料覆盖CAN协议报文帧结构、标识符与滤波器设置等通信基础也涉及六步换相、磁场定向控制等电机控制策略源码采用模块化设计清晰呈现电机控制、CAN收发和中断处理模块间的调用关系并附有开发者记录的问题排错思路。借助这套资料读者既能在Keil中复现实验也能按需修改代码实现转速与方向控制适合作为无刷电机与CAN通信结合开发的起步参考。1. 无刷电机CAN控制板用STM32F103把命令总线变成电机转速第一次拿STM32F103去接无刷电机很多人第一反应是去翻FOC库但实际项目里有一大批场景只需要“收到CAN命令、按指令让电机转起来”不需要高性能伺服。这套基于32单片机F103的CAN通信命令控制无刷电机方案就是用一个价格几块钱的M3内核芯片配合CAN收发器让上位机通过一条差分总线远程控制无刷电机转速和方向。适合做分布式运动控制、机器人关节、AGV小车、自动化产线设备的人也适合想入门STM32 CAN总线应用开发的学生。整份资源包含完整工程源码、原理说明和调试方法拿到手可以直接在自己的板子上跑通。2. 整体架构与通信协议一条CAN报文如何变成电机旋转CAN总线的核心优势是双线差分传输、多节点组网、抗干扰强但很多初学者一开始会被协议栈吓住。实际上在F103上做CAN控制无刷电机只需要搞清楚一条链路CAN收发器收到差分信号 → F103的bxCAN外设过滤并接收 → 中断回调里解析命令 → 修改定时器PWM占空比和方向IO → 电机驱动板输出动力。下面从硬件接线、软件分层、协议设计三个角度把这条链路拆开。2.1 硬件连接F103、CAN收发器与电调的接线关系硬件上F103内部集成了CAN控制器但它的输出是TTL电平的CAN_TX和CAN_RX必须外接CAN收发器芯片转成差分信号才能上总线。常见做法是用TJA1050或SN65HVD230这类收发器我一般用TJA1050兼容性好5V供电速率支持到1Mbps。接线关系如下STM32F103的PA11CAN_RX接收发器的RXDSTM32F103的PA12CAN_TX接收发器的TXD收发器的CANH、CANL分别接总线上的双绞线两端接120Ω终端电阻收发器VCC接5VGND与F103共地电调无刷电机驱动板的信号线接F103的PWM输出引脚比如PA0TIM2_CH1需要注意的是F103的CAN控制器和普通串口不一样它要求时钟配置到APB1且波特率通过预分频器、时间段1、时间段2组合计算。很多人在这里翻车因为直接把串口波特率那套思路套过来结果总线一直报错。后面章节会给出具体计算和代码。无刷电机本身一般不带驱动器所以F103输出的PWM不能直接驱动电机线圈必须经过电调或驱动板。常见的有两种一种是接收标准PWM信号50Hz~500Hz脉宽1ms~2ms的航模电调另一种是接收PWM和方向IO的直流无刷驱动器。这套方案默认接的是带方向接口的驱动器PWM占空比控制转速另一个IO引脚控制正反转。2.2 软件分层中断接收、协议解析、PWM输出三条通路软件上如果把整个工程当成黑匣子来看可以分三层驱动层、协议层、应用层。驱动层负责初始化F103的CAN外设、配对GPIO、配置定时器PWM输出协议层负责接收CAN数据帧、校验命令字、解析转速和方向参数应用层根据解析结果调整PWM占空比和方向IO电平。我在实际开发中把这三层放在不同文件里避免所有代码堆在main.c里。CAN驱动和中断回调放在can.c协议解析放在protocol.c电机控制放在motor.c。这样二次开发时改协议不需要动底层换电机驱动板也不需要动协议层。工程没有用复杂RTOS就是一个带优先级的中断裸机循环因为CAN通信是事件驱动中断回调里做短任务主循环里做非实时更新足够稳定。目录结构大致是Project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── can.h │ │ ├── protocol.h │ │ └── motor.h │ └── Src/ │ ├── main.c │ ├── can.c │ ├── protocol.c │ └── motor.c ├── Drivers/ │ ├── STM32F1xx_HAL_Driver/ │ └── CMSIS/ ├── .ioc 文件CubeMX配置 └── Makefile 或 .cproject用CubeMX生成基础工程可以省去手动配置时钟的麻烦但CAN的过滤器配置和中断回调需要自己写。注意HAL库的CAN接收回调有两种方式一种是轮询一种是用接收中断FIFO消息挂起。这里必须用中断否则主循环一旦被其他任务卡住命令帧就会丢失。2.3 命令协议定义帧ID和数据段的分工CAN报文本身是带ID的广播帧为了控制无刷电机需要自定义一个简单可靠的命令协议。我这里用的标准帧11位ID数据长度8字节。很多初学者会想“CAN帧ID这么多是不是可以随便用”但实际项目里一定要留扩展余地不然接多块板子时ID冲突会让你查半天。协议定义如下字节含义取值范围Byte 0命令字0x01表示转速控制0x02表示停止0x03表示方向切换Byte 1-2目标转速16位无符号数单位RPM低字节在前Byte 3方向0x00正转0x01反转Byte 4-5加速度16位单位RPM/s低字节在前Byte 6-7校验前6字节的异或校验高字节固定0xAA命令字0x01时电机按照目标转速和加速度平滑加速到目标值命令字0x02时PWM占空比立刻归零命令字0x03时只切换方向不改变转速。这个协议看起来简单但好处是单帧就能完成所有控制不需要握手和分包实时性强。帧ID我分配为0x01表示控制帧0x02表示状态反馈帧电机当前转速、母线电压、温度。这套约定在protocol.h里用宏定义写清楚上位机也按照这个格式发。如果以后要多电机组网就把ID改成0x01节点号比如节点1用0x11节点2用0x21后面章节会讲。3. 核心代码拆解CAN接收中断到PWM占空比的完整链路这一章直接上代码。工程基于STM32CubeMX HAL库F103型号C8T672MHz主频。代码分成三段来讲CAN外设初始化、中断接收与命令解析、PWM与方向控制。三段连起来就是一次完整的控制链路。3.1 引脚初始化与CAN外设配置CAN外设的初始化代码在can.c里。CubeMX会生成MX_CAN1_Init但过滤器配置和中断使能需要自己补。以下代码是完整的CAN初始化函数void MX_CAN1_Init(void) { CAN_FilterConfTypeDef sFilterConfig; CAN_ConfigTypeDef sConfig; hcan.Instance CAN1; hcan.Init.Prescaler 9; // 72MHz APB1时钟 / (9 * (176)) 500kbps hcan.Init.Mode CAN_MODE_NORMAL; hcan.Init.SyncJumpWidth CAN_SJW_1TQ; hcan.Init.TimeSeg1 CAN_BS1_7TQ; hcan.Init.TimeSeg2 CAN_BS2_6TQ; hcan.Init.TimeTriggeredMode DISABLE; hcan.Init.AutoBusOff ENABLE; hcan.Init.AutoWakeUp ENABLE; hcan.Init.AutoRetransmission ENABLE; hcan.Init.ReceiveFifoLocked DISABLE; hcan.Init.TransmitFifoPriority ENABLE; if (HAL_CAN_Init(hcan) ! HAL_OK) { Error_Handler(); } sFilterConfig.FilterIdHigh 0x0000; sFilterConfig.FilterIdLow 0x0000; sFilterConfig.FilterMaskIdHigh 0x0000; sFilterConfig.FilterMaskIdLow 0x0000; sFilterConfig.FilterFIFOAssignment CAN_FILTER_FIFO0; sFilterConfig.FilterBank 0; sFilterConfig.FilterMode CAN_FILTERMODE_IDMASK; sFilterConfig.FilterScale CAN_FILTERSCALE_32BIT; sFilterConfig.FilterActivation ENABLE; sFilterConfig.BankNumber 0; if (HAL_CAN_ConfigFilter(hcan, sFilterConfig) ! HAL_OK) { Error_Handler(); } // 激活CAN外设并开启接收中断 if (HAL_CAN_Start(hcan) ! HAL_OK) { Error_Handler(); } if (HAL_CAN_ActivateNotification(hcan, CAN_IT_RX_FIFO0_MSG_PENDING) ! HAL_OK) { Error_Handler(); } }这里波特率计算是关键。F103的APB1时钟是36MHz因为APB1最高36MCAN外设时钟挂在APB1上。上面的Prescaler9TimeSeg17TimeSeg26加上1TQ的Sync总计时间片为9 * (176) 126个TQ算下来是36MHz / 126 ≈ 285.7kbps不是标准的500kbps。这是我故意留的一个坑实际项目里如果发送端和接收端波特率不一致会导致总线一直进入错误状态。正确的500kbps配置是Prescaler4BS19TQBS28TQ总时间片4*(198)7236MHz/72500kHz。我在调试时会先用CAN分析仪确认波特率再用示波器看CANH对CANL的波形是否稳定。过滤器这里使用了ID掩码模式FilterIdLow为0x0000掩码也为0x0000意思是不过滤任何ID所有标准帧都进FIFO0。实际项目里建议按帧ID过滤比如只接收0x01写法是FilterIdLow (0x01 5)掩码的对应位设为0xFF。这样能降低无关数据对MCU的干扰。3.2 命令帧解析中断回调与状态机HAL库在收到CAN帧后会进入HAL_CAN_RxFifo0MsgPendingCallback回调。这个回调需要重写在can.c里。注意这个回调中断上下文里面不能调用HAL_Delay也不能做耗时的浮点运算否则CAN接收溢出。我的做法是把原始数据从硬件FIFO拷到全局变量然后抛给主循环处理。// 全局变量 uint8_t rx_data[8]; uint32_t rx_id; volatile uint8_t rx_flag 0; void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rx_header; if (hcan-Instance CAN1) { if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rx_header, rx_data) HAL_OK) { if (rx_header.IDE CAN_ID_STD rx_header.DLC 8) { rx_id rx_header.StdId; rx_flag 1; // 通知主循环 } } } }主循环里看到rx_flag置1就调用protocol_parse(rx_id, rx_data)做解析。这种设计避免了中断里处理逻辑也方便调试时打印观察。注意rx_flag要加volatile修饰否则编译器优化后可能一直读不到新值。协议解析函数在protocol.c里核心是校验和命令分发uint8_t protocol_parse(uint32_t id, uint8_t *data) { uint8_t checksum 0; uint8_t cmd; // 异或校验前6字节异或 for (int i 0; i 6; i) { checksum ^ data[i]; } if (checksum ! data[6] || data[7] ! 0xAA) { return 0; // 校验失败 } cmd data[0]; switch (cmd) { case CMD_SET_SPEED: { uint16_t speed (uint16_t)(data[1] | (data[2] 8)); uint8_t dir data[3]; motor_set_speed(speed, dir); break; } case CMD_STOP: motor_stop(); break; case CMD_SWITCH_DIR: motor_switch_dir(); break; default: break; } return 1; }这段代码完成的是把CAN帧里的数据变成电机控制动作。speed是16位小端整数上位机发数据时先发低字节再发高字节很多人拿串口助手的思路直接发高字节在前导致转速变成65535电机猛地全速转这是比较危险的一件事。所以我建议在协议解析里加一个速度上限判断比如超过5000RPM直接丢弃保护电机和负载。3.3 电机控制输出PWM占空比与方向逻辑motor.c里实现电机控制。假设驱动板接口是PWM引脚用于调速DIR引脚用于方向EN引脚下拉输出时使能。F103的TIM2_CH1映射在PA0TIM2_CH2映射在PA1我用PA0输出PWMPA1输出方向PD2输出使能。void motor_init(void) { // 使能GPIO和TIM2时钟 __HAL_RCC_TIM2_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio_init; gpio_init.Pin GPIO_PIN_0; gpio_init.Mode GPIO_MODE_AF_PP; gpio_init.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, gpio_init); gpio_init.Pin GPIO_PIN_1; gpio_init.Mode GPIO_MODE_OUTPUT_PP; HAL_GPIO_Init(GPIOA, gpio_init); // 定时器PWM输出配置 htim2.Init.Prescaler 72 - 1; // 1MHz计数频率 htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 1000 - 1; // 1kHz PWM频率 htim2.Init.ClockDivision 0; HAL_TIM_PWM_Init(htim2); TIM_OC_InitTypeDef oc_config; oc_config.OCMode TIM_OCMODE_PWM1; oc_config.Pulse 0; oc_config.OCPolarity TIM_OCPOLARITY_HIGH; HAL_TIM_PWM_ConfigChannel(htim2, oc_config, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); } void motor_set_speed(uint16_t speed_rpm, uint8_t dir) { // 限幅保护 if (speed_rpm MAX_RPM) speed_rpm MAX_RPM; // 转速映射占空比假设1000RPM对应10%占空比 uint16_t duty speed_rpm / 10; if (duty 950) duty 950; // 留5%死区 __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_1, duty); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_1, dir ? GPIO_PIN_SET : GPIO_PIN_RESET); }PWM频率1kHz是相对保守的选择。很多航模电调的PWM频率是50Hz~500Hz但工业无刷驱动器通常兼容到20kHz。这里用1kHz的原因是F103的定时器在72MHz主频下预分频72得到1MHz周期1000就是1kHz计算方便而且对大多数驱动板都有效。如果电调要求更高频率可以调整Period比如500Hz就把Period改成200020kHz就把预分频改成7200-1、Period改成50。每次改完要重新测脉宽否则占空比映射会错。方向切换这里简单地把一个IO拉高拉低但如果驱动板要求的是差分方向信号就需要两个IO配合。建议先看驱动板的说明书确认方向电平逻辑。之前遇到一个驱动板方向是低电平有效我按高电平有效写导致正转变成反转而且是运行时突然切换电机像被刹车一样猛停非常危险。4. 从编译到调速把代码烧进板子并用CAN工具控制电机代码写完之后最担心的不是编译报错而是烧进去之后电机不转。这一章把整个跑通流程走一遍从工程导入到发送第一条CAN命令每一步都会标注“如果出现XX现象应该查哪里”。4.1 工程导入与编译烧录步骤工程基于STM32CubeIDE或Keil都可以。如果用CubeIDE直接导入项目根目录下的.cproject如果用Keil打开MDK-ARM下的.uvprojx。第一次编译前确认芯片型号是STM32F103C8T6头文件路径包含Drivers/CMSIS和Drivers/STM32F1xx_HAL_Driver。编译烧录步骤打开工程右键项目选择Build Project观察Console输出0 Error。用ST-Link连接SWD接口SWDIO接PA13SWCLK接PA14GND共地。在CubeIDE里配置Debugger为ST-LINKResume运行。按下复位键进入调试模式单步跑过main函数开头确认HAL_CAN_Init返回HAL_OK。全速运行此时CAN收发器应该在监听总线但电机不会转命令字还未收到。这里有个容易被忽略的点F103的PA11和PA12默认可能是复用功能CubeMX生成初始化时会把它们配成AF_PP。如果你自己手动改过GPIO模式比如设成了推挽输出那CAN信号根本出不来。烧录后第一步应该用示波器或逻辑分析仪测PA12脚和TJA1050的TXD脚波形是否一致。4.2 发送控制命令实操一条完整报文上位机使用USB-CAN分析仪常用软件可以发送标准帧。我们把帧ID设为0x01数据段设为01 F4 01 00 00 00 00 AA含义是命令字0x01转速控制转速 0x01F4 500 RPM方向0x00正转加速度0校验字节 01^F4^01^00^00^00 0xF4data[7] 0xAA。注意校验是我口算的实际发送时应该用上位机脚本自动算避免手动错误。如果用上位机发送操作如下打开设备波特率选择500kbps与代码中HAL_CAN_Init的配置一致发送模式选择Normal帧类型Standard帧ID填1数据长度8逐字节填入01 F4 01 00 00 00 F4 AA点击发送发送成功后F103进入HAL_CAN_RxFifo0MsgPendingCallbackrx_flag置1主循环调用protocol_parse电机应该开始以500RPM转动。如果电机不转按顺序检查CAN分析仪的CANH和CANL是否接入总线终端电阻是否在两端收发器有没有上电TJA1050的Rs脚是否接地静音模式F103的PA0脉宽是否有输出用万用表量平均电压如果占空比5%平均电压应该是0.05*3.3V≈0.165V我在实验室里第一次连的时候电机不转查了一圈发现CAN分析仪的波特率是250kbps而代码里配的是500kbps。CAN总线错误计数器一直在累加AutoBusOff把外设关闭了但F103这边不会自动恢复必须重新初始化。后来我把AutoBusOff设为ENABLE也不管用最终是重新配置波特率才解决。从那以后我每次跑项目第一件事就是确认CAN波特率两边完全一致。4.3 观察运行效果并验证协议当电机转起来之后还要验证协议控制不只在某一帧生效。连续发送几组不同速度的命令比如把转速字节改成E8 03即1000RPM再改成D0 07即2000RPM观察电机转速是否响应。如果响应有延迟检查主循环里是否有其他阻塞任务。如果转速到了上限还在升高检查protocol.c里的MAX_RPM宏定义是否被改掉。验证校验功能很重要故意发送一个校验错误的帧比如data[6]改成0x00协议解析应该返回0电机保持上一帧的速度不变。这样可以确认校验生效而不是无脑接受所有帧。很多初学者省了校验结果总线上噪声随机命中命令字电机突然乱动这个问题在工业现场被干扰时特别明显。方向切换测试发送命令0x03电机应该减速到0然后反转。如果电机没有先减速直接瞬间反转说明motor_switch_dir函数没有做转速归零处理这样会冲击减速箱或机械负载。我一般在方向切换前强制把速度置零等待100ms再启动反向这个时间用HAL_Delay放在主循环里没问题但不要在中断里放。5. 避坑指南CAN控制无刷电机最常见的五个坑这章把我在调试过程中遇到的高频问题整理成清单每一条都是自己踩过的坑现象、原因、解决都会写出来。整理这个清单的初衷是CAN通信本身并不难难的是出问题之后你不知道该信示波器、逻辑分析仪还是代码。5.1 收发器发不出去TXD与RXD交叉错位现象F103代码运行正常HAL_CAN_Init返回HAL_OK但CAN分析仪收不到任何帧发送命令给F103也没反应。示波器量PA11有波形但收发器的CANH和CANL之间没有差分信号。原因PA11是CAN_RX接收发器的RXDPA12是CAN_TX接收发器的TXD。我一开始为了板子走线方便把PA11接到了TXDPA12接到了RXD等于收发器的输出接到了MCU的输出两边都往外推信号直接抵消。解决把CAN_RX和CAN_TX交叉对调。用万用表通断档检查PCB上PA11到收发器RXD的走线必须隔断后飞线。从此以后我画板子在原理图阶段就标注“MCU_RX → 收发器RXD”防止布线时头脑发热。5.2 总线进入Bus-Off后不恢复现象CAN分析仪和F103都配置为500kbps但通信一段时间后F103的CAN外设不再收发任何帧。代码里设了AutoBusOff ENABLE但还是卡死。原因HAL库的AutoBusOff只是允许硬件在收到显性位后自动恢复但恢复需要协议规定的128个隐性位。如果总线上有一个节点一直发送错误帧总线会持续被占用F103永远等不到128个空闲位。另外如果F103的波特率与总线实际波特率有微小偏差比如晶振误差导致实际不是精确500kbps它可能频繁进入错误状态。解决先用CAN分析仪监听总线确认总线上没有其他设备在乱发数据。如果只有F103一个节点可以把AutoBusOff设为ENABLE然后每次出现Bus-Off时在CAN_ErrorCallback里手动执行HAL_CAN_Start重新启动外设。我后来在错误回调里加了错误计数器读取连续出错超过30次就主动复位CAN外设并重新初始化这个缓兵之计在调试前期很有用。5.3 PWM频率与电调不匹配导致电机啸叫现象电机在低速时发出持续的尖锐啸叫声转速不稳定有抖动。用示波器看PWM波形正常但电机就是不听使唤。原因我最初把PWM频率设成100Hz模拟航模电调的常见脉宽控制但手里的工业无刷驱动器要求PWM频率至少20kHz。100Hz对驱动器来说等于一直在使能/停使能之间疯狂切换电机当然会抖。解决查驱动板说明书确认PWM频率范围。把TIM2的Period改到501MHz/5020kHz重新测占空比映射。高频PWM下电机噪音明显变小转速也更平稳。从那以后我拿到驱动板第一件事就是看它的PWM频率范围而不是套以前的配置。5.4 中断里调用延时导致FIFO溢出丢帧现象发送连续多帧命令时前几帧正常第5帧之后电机无响应。打印CAN错误寄存器发现FIFO溢出标志置位。原因我在HAL_CAN_RxFifo0MsgPendingCallback里直接调用了motor_set_speed而这个函数里面有一处HAL_Delay(10)用来等待驱动板反馈电平变化。中断被这个延时占住后续CAN帧到了硬件FIFO但F103没时间去读FIFO被新帧覆盖。解决中断里只做数据拷贝所有解析和电机操作放到主循环。这是一条通用的嵌入式铁律但很多人包括我在“就延时一下没事吧”的侥幸心理下翻车。改进后即使在1Mbps总线上连续发100帧也不会丢。5.5 电调上电瞬间误动作导致电机飞转现象系统上电时在没有任何CAN命令的情况下电机突然高速旋转几秒后才停下。有时候直接带着负载冲出去非常危险。原因电调在上电初始化的瞬间会读取PWM引脚的默认电平。F103的GPIO在复位期间是浮空输入PA0引脚电平不确定而某些电调把高电平脉宽误认为是最大油门信号于是全速旋转。等到F103程序跑起来CAN外设初始化好PWM输出设置为0电机才停住。解决在CubeMX里把PA0初始化为低电平输出模式并且在main函数最开头甚至是在HAL_Init之前就把PA0拉低。另外硬件上可以在电调信号线和GND之间加一个下拉电阻比如10kΩ确保上电瞬间信号线保持低电平。更稳妥的做法是给电调单独一个使能IO由F103控制上电后等CAN初始化完成再给使能信号。这套逻辑后来被我写进了所有电机项目里再也没有出现过上电飞车。6. 二次开发从单机调速到多关节闭环控制跑通基础版本之后这套工程更多是作为一个骨架来用。如果你想把它用在自己的机器人关节、小车底盘或者某个实验台上下面这两个改造方向是最常见的。改完以后你会发现CAN控制无刷电机的价值不只是“能转”而是“可组网、可闭环、可预测”。6.1 增加霍尔测速与PID闭环当前版本是开环控制转速只靠PWM占空比映射负载变化时转速会掉。要变成闭环最简单的方法是给电机加霍尔传感器或编码器F103复用TIM1的编码器接口模式读取转速。然后加上一个位置式PID在主循环中以固定周期更新占空比。// motor.c 中新增PID结构体 typedef struct { float kp; float ki; float kd; float target; float integral; float prev_error; } Pid_t; float pid_update(Pid_t *pid, float current) { float error pid-target - current; pid-integral error; float derivative error - pid-prev_error; pid-prev_error error; return pid-kp * error pid-ki * pid-integral pid-kd * derivative; }在主循环里每10ms读取一次编码器计数值算出实际转速然后调用pid_update得到占空比修正量再写入定时器比较寄存器。注意PID的积分项要设置饱和值否则电机长时间堵转时积分会爆炸恢复时会出现猛冲。我在调参时先把ki设0调kp到电机响应不震荡再加一点点ki消除稳态误差kd用来压过冲。6.2 多电机组网扩展帧ID与广播命令如果一台F103要控制4个电机或者一台总线上挂多个F103节点协议层需要小幅扩展。我的做法是把帧ID的高4位作为节点地址低4位作为命令类型。比如节点地址0x1控制转速命令为0x01则帧ID为0x11停止命令为0x02则帧ID为0x12。接收端在中断回调里先判断rx_id的高4位是否等于自己的节点地址等于才置标志位否则直接忽略。发送端则可以往0x00广播地址发命令所有节点同时执行。主循环里的轮询任务也要改每个电机对应一个PWM通道和一组PID参数。用结构体数组管理每个节点定义最大电机数量为4。这样做的最大好处是扩展一个电机不需要改动协议解析核心代码只需在motors[]数组里加一个元素。这套设计有一点像CANopen的设备对象字典虽然没有那么标准但对小型项目来说足够灵活。6.3 验证一帧完整命令是否被正确执行最后给一个验证技巧在protocol.c里加一个回环帧把收到的原始数据原封不动地发回总线上位机收到后比对是否一致。回环帧ID用0x7F数据段就是收到的8字节。这样整个链路从CAN物理层到应用层任何一个环节出错都能立刻定位。// 在主循环收到有效命令后构造回环帧 CAN_TxHeaderTypeDef tx_header; uint8_t tx_data[8]; uint32_t tx_mailbox; memcpy(tx_data, rx_data, 8); tx_header.StdId 0x7F; tx_header.IDE CAN_ID_STD; tx_header.RTR CAN_RTR_DATA; tx_header.DLC 8; HAL_CAN_AddTxMessage(hcan, tx_header, tx_data, tx_mailbox);实际调试中这个回环帮我查出了一个非常隐蔽的问题因为我在中断里把rx_data定义为全局数组而主循环解析还没结束时下一帧又覆盖了rx_data导致回环帧数据被篡改。后来改成主循环先拷贝一份局部缓存再解析问题才消失。从那以后我每次写CAN接收都会强制走一遍“中断拷贝 → 主循环处理 → 回环验证”的流程确保数据在每一跳里都可追踪。希望这个习惯也能帮你少走一些弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑