简介:这是一份基于STM32F407芯片的CAN总线电机控制工程包,面向嵌入式开发者、自动化专业学生以及工业控制爱好者,完整演示了从CAN模块配置到伺服电机闭环控制的流程。压缩包包含一百五十一个文件,大小约二点三三兆,其中以C语言源文件与头文件为核心,覆盖CAN初始化、滤波器设置、波特率配置、消息收发与中断服务程序;另有Keil工程文件、编译生成的HEX、AXF以及调试辅助文件,方便直接打开工程对照学习。目前已有八千五百九十四人浏览学习。项目以“可旋转自行车停车程序”为实际应用场景,利用陀螺仪与加速度计获取姿态信息,结合伺服电机的精确定位,展示了CAN2.0B协议在标准帧与扩展帧下的通信机制。通过研读源码,读者可以掌握驱动编写、比例积分微分控制算法集成、实时状态回传以及多文件工程组织等技能,适合作为课程设计或工业总线控制入门的参考资料。
1. CAN电机控制不是玄学,是工业现场绕不开的确定性
串口和USB在很多场景下“看起来够用”,但真到电机控制这个环节,你会发现设备之间距离动不动几十米、电磁环境嘈杂、还要保证一条线上挂五六个驱动器不死机,这时候串口的主从轮询和USB的星形拓扑根本撑不住。CAN通信控制电机的核心价值,在于它把“多个节点怎么避免冲突、错误帧如何自动重发、实时性怎么保障”这些问题下沉到了硬件层,软件上你只需要关心报文ID和数据字节。这篇文章要讲的是,从STM32的CAN外设接线、波特率配置到应用层协议设计的一条完整落地路径,适合正在调电机驱动器、或者想把多个电机控制器联网的硬件工程师和嵌入式开发。我按自己做过的方案讲,不绕弯子。
2. 硬件选型与总线拓扑:先让信号能跑起来,再谈代码
用CAN控制电机,第一步不是打开IDE写初始化,而是把总线物理层铺对。很多翻车现场不是代码问题,是收发器型号选错、终端电阻没配、或者把CAN_H和CAN_L接到地线上去了。这一章先把硬件的几个决定性问题讲透,后面代码才能有调试的意义。
2.1 收发器选型:3.3V与5V的边界,以及TJA1050/SN65HVD230怎么选
STM32内部集成了CAN控制器,但它输出的只是单端差分信号前的逻辑电平,真正把逻辑电平变成总线上的差分电压、同时承受总线短路和雷击浪涌的,是旁边的CAN收发器芯片。型号上见得最多的是TJA1050和SN65HVD230,还有MCP2551这类老面孔。
选型的第一个分叉是供电电压。TJA1050是5V供电的,SN65HVD230是3.3V供电。STM32本身是3.3V系统,如果你用TJA1050,TXD/RXD引脚逻辑高电平是5V,好在绝大多数STM32的GPIO引脚是兼容5V输入的,可以直接连。如果用的是某些不支持5V容忍的IO,就必须加电平转换,否则长时间运行会出现偶发误码。我的习惯是:手头有5V电源、距离不超过20米、环境不算恶劣时用TJA1050;设备端只提供3.3V、或者总线附近有大功率变频器时,用SN65HVD230更稳妥,因为3.3V收发器的共模输入范围在恶劣电磁环境下表现更好,还自带宽输入共模范围,不容易因为地电位漂移打坏芯片。
2.2 终端电阻:不是“加上就行”,位置和数量都有讲究
CAN总线两端必须各接一个120Ω终端电阻,注意是“两端各一个”,不是“随便哪里放一个”。我见过有人只在STM32这端焊了一个120Ω,结果近距离调试没问题,一旦线拉到十几米,反射信号把波形给吃了,表现为总线错误计数器不断飙升。从信号完整性角度讲,CAN收发器输出的差分信号到达总线末端时如果不被吸收,会反射回来和原始信号叠加,在波特率较高时直接造成位错误。
现场做法是这样的:如果只有两个节点,分别放在两个节点的CAN_H和CAN_L之间;如果有多个节点,放在拓扑上最远的那两个节点。中间节点即使再方便,也尽量不要加电阻,否则总线等效阻抗变小,差分信号幅值会被拉低。还有个容易被忽略的点:有些电机驱动器内部已经焊了120Ω电阻,你再用外部电阻就等于两个120Ω并联成了60Ω,总线会异常繁忙,错误帧一个接一个。上电前用万用表量一下总线的DC阻抗,正常应该在60Ω左右——这是两个120Ω电阻并联的结果。如果量出来是40Ω或者更低,说明中间某个节点多加了一个电阻,逐个拔通信线排查。
2.3 电机驱动器的CAN接口:CANopen从站与裸CAN指令的差异
和电机驱动器对接时,必须搞清楚对方用的是标准CANopen协议还是裸CAN报文。这两种模式的软件工作量差别很大。CANopen是建立在CAN数据链路层之上的应用层协议,包含对象字典、PDO、SDO、NMT状态机这些概念。如果你买的驱动器支持CANopen,那通常意味着你不需要自己定义“第0字节是速度高8位”这种映射,而是通过配置工具把映射关系写进对象字典,运行后用PDO高速传输控制字和目标速度。代价是协议栈代码量不小,如果MCU资源紧张,跑完整协议栈费劲。
裸CAN驱动器则简单直接:厂商会给一张协议表,比如“标准帧ID 0x001,第0字节高8位速度、第1字节低8位速度、第2字节控制字”。这种方式没有协议栈,一个发送函数加一个接收中断就齐活,特别适合STM32F103这类资源不算富裕的芯片。两种方案我选型时的判断标准是:需要和PLC、触摸屏等上位机互联时走CANopen,自己控制所有节点时走裸CAN。此外还要确认驱动器的默认波特率,工业驱动器最常见的默认波特率是125k或500k,不是1M,很多人上来按1M配然后怎么都通不了,就是这个原因。
3. 最小可运行工程:CAN外设初始化、波特率计算与收发代码
物理层准备好后,接下来是让STM32的CAN外设发出第一条报文。这一章给一份最小可运行参考实现,用的是HAL库,覆盖引脚复用、波特率计算、发送和中断接收完整链路。你需要根据自己板子的晶振和时钟树微调参数,但流程是不变的。
3.1 引脚复用与时钟使能:PB8/PB9还是PD0/PD1,别接错
以常见的STM32F405为例,CAN1的引脚复用有两组:PB8是CAN1_RX,PB9是CAN1_TX;另一组是PD0(CAN1_RX)和PD1(CAN1_TX)。如果芯片型号或封装中没有PB8/PB9,就用PD0/PD1。这两组不能混用,即不能把TX接到PB9上、RX接到PD0上,否则不进中断。复用功能选AF9。时钟使能时,注意CAN外设挂在APB1总线上,除此之外还要打开GPIO时钟。SPI、定时器等其他外设和CAN共用APB1,如果你把APB1预分频器改了,要注意会不会影响同一总线上的其他外设。下面是一份MX_GPIO_Init风格的引脚配置代码:
/* 将PB8和PB9复用为CAN1的RX/TX,注意不是普通推挽输出 */ static void MX_CAN1_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_GPIOD_CLK_ENABLE(); /* 也可以启用GPIOB,取决于用哪组引脚 */ __HAL_RCC_CAN1_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1; /* PD0=RX, PD1=TX */ GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; /* 复用推挽,不是开漏 */ GPIO_InitStruct.Pull = GPIO_PULLUP; /* CAN引脚内部上拉,配合总线显性电平 */ GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; GPIO_InitStruct.Alternate = GPIO_AF9_CAN1; /* 查数据手册确认AF编号 */ HAL_GPIO_Init(GPIOD, &GPIO_InitStruct); }这里的核心是Alternate不能填错,很多HAL库示例默认PB8/PB9,换了引脚后忘了改AF导致总线完全没有活动。CAN收发器在总线上没有数据传输时是隐性电平,靠收发器内部和终端电阻维持,所以内部上拉要打开,防止引脚悬空时电平漂移造成随机错误帧。另外,GPIO_MODE_AF_PP是复用推挽输出,CAN是差分输出,不是开漏,和I2C完全不同,别套用习惯。
3.2 波特率与采样点:1Mbps在42MHz时钟下的寄存器推算
CAN的波特率不是随便填一个数就能跑起来的,它由预分频器(Prescaler)、时间段1(BS1)、时间段2(BS2)共同决定。公式是:波特率 = CAN外设时钟 / (Prescaler × (1 + BS1 + BS2))。注意这里的BS1和BS2都以时间量子(TQ)为单位。CAN规范要求一个位时间至少由8个时间量子组成,采样点推荐在75%~85%之间,BS2太小会导致采样点太靠前,总线较长时容易采样到bit边沿。
我在F405上常用这样一组参数:CAN外设时钟42MHz,目标波特率1Mbps,Prescaler=2,BS1=13TQ,BS2=7TQ,则总时间量子=1+13+7=21,42MHz除以预分频2得到21MHz,再除以21个TQ正好是1MHz。采样点位置=(1+BS1)/(1+BS1+BS2)=14/21≈66.7%,这个值对于1Mbps和不超过10米的短总线够用;如果总线上要挂十几个节点或距离超过20米,建议把采样点往75%方向调,也就是增加BS1、减小BS2。
static void MX_CAN1_Init(void) { hcan1.Instance = CAN1; hcan1.Init.Prescaler = 2; /* 42MHz CAN时钟下,预分频2得到21MHz */ hcan1.Init.Mode = CAN_MODE_NORMAL; /* 调试时可用CAN_MODE_LOOPBACK确保中断链路正常 */ hcan1.Init.SyncJumpWidth = CAN_SJW_1TQ; /* 重新同步跳跃宽度,1TQ最保守 */ hcan1.Init.TimeSeg1 = CAN_BS1_13TQ; hcan1.Init.TimeSeg2 = CAN_BS2_7TQ; hcan1.Init.TimeTriggeredMode = DISABLE; hcan1.Init.AutoBusOff = ENABLE; /* 总线错误累计到255后自动恢复 */ hcan1.Init.AutoWakeUp = DISABLE; hcan1.Init.AutoRetransmission = ENABLE; /* 硬件自动重发,丢帧率更低 */ hcan1.Init.ReceiveFifoLocked = DISABLE; hcan1.Init.TransmitFifoPriority = DISABLE; if (HAL_CAN_Init(&hcan1) != HAL_OK) { Error_Handler(); } }AutoBusOff建议打开。CAN节点在总线严重错误时会进入Bus-Off状态,主动断开总线,避免故障节点持续干扰全局。AutoRetransmission打开后,发送失败的帧会由硬件自动重发,不占CPU,这是CAN“确定性通信”的重要硬件支撑。但要注意,自动重发在总线故障时会一直重发同一帧,挤占其他节点带宽,所以如果系统里有故障诊断需求,可以在发送超时后人为干预。
3.3 报文发送与接收中断:一份可以直接烧录的参考实现
发送电机的控制指令,本质上就是填充一个CAN报文的数据域,然后调用HAL_CAN_AddTxMessage。这里强调几个容易踩的细节:标准帧的ID是11位,如果驱动器协议用的是扩展帧,IDE字段要设成CAN_ID_EXT,同时StdId字段会变成ExtId,这个混了以后收发会全部失败。数据字节序按驱动器协议约定的对齐,常见的是大端在前,也就是高字节放数组低位。
static CAN_TxHeaderTypeDef txHeader; static uint8_t txData[8]; static uint32_t txMailbox; void Motor_SendControl(int16_t speed1000, uint8_t ctrlWord) { txHeader.StdId = 0x001; /* 控制报文ID,要和驱动器建立通信的ID一致 */ txHeader.IDE = CAN_ID_STD; /* 标准帧 */ txHeader.RTR = CAN_RTR_DATA; /* 必须数据帧,RTR远程帧在此无用 */ txHeader.DLC = 8; /* 8字节数据,即使没用满也发8字节 */ txHeader.TransmitGlobalTime = DISABLE; txData[0] = (uint8_t)(speed1000 >> 8); /* 速度高8位 */ txData[1] = (uint8_t)(speed1000 & 0xFF); /* 速度低8位 */ txData[2] = ctrlWord; /* 使能/方向/刹车等控制位 */ if (HAL_CAN_AddTxMessage(&hcan1, &txHeader, txData, &txMailbox) != HAL_OK) { /* 失败原因通常是邮箱全满,或节点处于Bus-Off */ Motor_SendRetryCount++; } }DLC统一发8字节是为了让接收端解析更简单,但如果你总线带宽紧张,完全可以按实际使用字节数设DLC=3或DLC=4。发送返回值不为HAL_OK时,第一时间查CAN的状态寄存器,而不是盲目重试。接着是接收中断,这也是最容易“写了但没生效”的地方。初始化时要用HAL_CAN_ActivateNotification打开FIFO0报文挂起中断,然后再启动CAN外设,顺序反了会造成收不到中断但总线电平正常,这一度是我的黑匣子问题。
HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING); HAL_CAN_Start(&hcan1); 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); if (rxHeader.StdId == 0x101) { /* 驱动器反馈报文:转速、状态、故障码 */ motor_feedback.speed_rpm = (rxData[0] << 8) | rxData[1]; motor_feedback.state = rxData[2]; motor_feedback.fault = rxData[3]; } } }中断回调里尽量只做数据搬移和状态更新,不要在CAN中断里做PID计算或延时,否则处理长帧时会影响下一帧接收。如果控制频率要求每秒1000次,接收和发送都频繁,可以把回调里的处理进一步拆分,但初版先跑通这个结构。
4. 应用层协议设计:把“转得快慢”变成CAN报文里的字节
驱动器收到底层CAN帧后,并不知道你第0个字节是速度还是模式切换。应用层协议的作用就是让两端对这个字节数组的含义达成一致。这不是CAN规范规定的,是使用者自己定义的,但如果定义得不好,后续加功能就要推倒重来。这一章讲我常用的一套电机控制协议结构,可以直接照搬,也能当参考设计。
4.1 速度指令的编码:符号位、小数位与±1000的比例映射
速度值直接发整数转每分(RPM)在寄存器控制场景很常见,但不同电机额定转速不同,比如直流电机额定3000RPM、步进电机可能只有600RPM。为了协议通用,我习惯用标幺值:把速度映射到±1000,读作“千分之速度”。例如100.0%正向速度就是+1000,-50.0%就是-500。用int16_t存,高字节在前,低字节在后。
int16_t speed_permille = (int16_t)(target_percent * 10.0f); if (speed_permille > 1000) speed_permille = 1000; if (speed_permille < -1000) speed_permille = -1000; txData[0] = (uint8_t)(speed_permille >> 8); txData[1] = (uint8_t)(speed_permille & 0xFF);为什么用±1000而不是直接用带小数的浮点?因为CAN数据域是字节数组,没有浮点概念,把浮点转成字节要处理大小端和隐式转换,代码可读性和调试性都变差。±1000的标幺值还有一个好处:在人机界面上显示百分比时几乎不用换算,上位机直接除以10就是百分比。如果以后要放宽速度分辨率,可以改成±10000,但字段还是2字节,只是比例系数变了。
4.2 方向与使能位:位运算合并使能、方向、刹车
控制字建议单独用一个字节,每个bit定义一种功能,不要直接用一个int型去表示方向,否则扩展性很差。常见的位定义是:bit0使能、bit1方向(0正转1反转)、bit2刹车、bit3故障复位、bit4~bit7保留。这个字节在发送前由上层逻辑组装:
#define CTRL_ENABLE 0x01 #define CTRL_DIR 0x02 #define CTRL_BRAKE 0x04 #define CTRL_FAULT_CLR 0x08 uint8_t ctrlWord = 0; if (motor_enable) ctrlWord |= CTRL_ENABLE; if (motor_direction == REVERSE) ctrlWord |= CTRL_DIR; if (need_brake) ctrlWord |= CTRL_BRAKE;位域方式的优势是接收端可以独立判断每个控制信号,而不会因为一个字节的数值被错误赋值导致方向、使能一起变。很多驱动器协议本身也采用这种位定义方式。注意故障复位位要保持一个脉冲高电平然后自动变低,不能一直置1,否则部分驱动器的故障寄存器无法真正清除。
4.3 反馈报文回读:转速、电流和故障码的解析流程
驱动器反馈报文通常周期性上报,比如每10ms一帧。反馈帧长一般是8字节,我常用的排列是:Byte0~1为实际转速(int16_t,单位RPM),Byte2为母线电流(单位0.1A),Byte3为状态字(bit0使能中、bit1速度到达、bit2故障中),Byte4为故障码。解析时最忌讳的是在主循环里直接检查运行时数据而不加锁,因为接收中断和主循环访问同一个变量可能引发数据撕裂。简单做法是把反馈结构体声明为volatile,复杂系统则用临界区保护。
typedef struct { volatile int16_t speed_rpm; volatile uint16_t current_01a; volatile uint8_t status; volatile uint8_t fault_code; } MotorFeedback; MotorFeedback motor_feedback;解析流程上,收到0x101帧后先把原始数据搬进反馈结构体,再在主循环里做逻辑判断,不要在中断里直接执行“如果故障码等于3就停机”这类动作。原因是CAN中断优先级一般较高,在中断里做多条件判断容易干扰其他实时任务。故障码含义别靠记忆,在工程里维护一张对照表,比如0x01过压、0x02过流、0x03过温、0x04编码器异常,调试时直接打印十六进制并和驱动器的用户手册对照。
5. 常见问题排查:总线错误、丢帧与电机抖动的三个现场
这一章是血泪经验集合。我调试CAN电机控制时碰到的问题,绝大多数可以归为下面三类:总线物理层没跑通、报文没能正确进入FIFO、以及控制周期抖动导致电机运行不平稳。每条按现象、原因、解决列出来,遇到问题时可以按这个顺序排查。
5.1 现象:接线全对但总线错误计数器飙升,报文发不出去
原因多半在终端电阻和共地上。我曾经在模拟项目X里试过把两个节点距离拉到15米,只在发送端加了120Ω电阻,结果接收端波形边沿严重过冲,错误帧每隔几毫秒出现一个。另一个隐蔽原因是控制器和驱动器用了不同的电源适配器,两个电源的地没有相连,导致CAN_H/CAN_L的共模电压差超出收发器允许范围。判断方法很简单:万用表测CAN_H和CAN_L之间的直流电阻,正常约60Ω;再开机测CAN_H对地电压(约2.5V)和CAN_L对地电压(约2.5V),两者差值应该在0V附近。如果CAN_H对地是3.3V而CAN_L是1.7V,说明收发器供电或终端电阻匹配出了问题。解决就是两端各加120Ω终端电阻,并把多个节点的GND用粗线连起来。
5.2 现象:示波器上波形正常,但STM32的接收中断从来没触发过
很多人在CAN总线上用逻辑分析仪抓到了正确的报文,但程序就是不进HAL_CAN_RxFifo0MsgPendingCallback回调。原因有三类,最常见的是过滤器的掩码和ID配置把报文过滤掉了。CAN过滤器的工作方式是:要接收的报文ID必须与某个过滤器规则匹配。初始化时如果配置成所有报文都过滤掉,FIFO里永远是空的。检查方法是读CAN的FIFO0寄存器FSR.RFPM,如果报文到达但FIFO计数为0,那就是过滤器被挡了。把过滤器掩码配置为接收所有标准帧和扩展帧是最稳妥的开始方式。
CAN_FilterTypeDef filter; filter.FilterBank = 0; filter.FilterMode = CAN_FILTERMODE_IDMASK; filter.FilterScale = CAN_FILTERSCALE_32BIT; filter.FilterIdHigh = 0x0000; filter.FilterIdLow = 0x0000; filter.FilterMaskIdHigh = 0x0000; /* 全掩码,不关心ID */ filter.FilterMaskIdLow = 0x0000; filter.FilterFIFOAssignment = CAN_RX_FIFO0; filter.FilterActivation = ENABLE; HAL_CAN_ConfigFilter(&hcan1, &filter);如果第一个原因排除了,第二个原因是中断没有激活。HAL库的HAL_CAN_ActivateNotification要在HAL_CAN_Start之后或之前调用都可以,但我见过有人在Start之后再调用ActivateNotification,这没问题,千万不能漏的是使能NVIC中的CAN1_RX0_IRQn中断,HAL库不会自动帮你打开NVIC。第三个原因少见但发生即隐蔽:回调函数里如果用了HAL_CAN_GetRxMessage,而第一个参数FIFO选择填错,比如报文进入FIFO1却去FIFO0取数据,也会表现为既不报错也不进回调。
5.3 现象:电机能转但运行中持续抖动,转速不稳
这种抖动通常不是PID参数的问题,而是CAN控制报文的发送周期不稳定。用示波器看CAN总线时,报文发送间隔忽长忽短,长的时候20ms,短的时候5ms。原因是发送函数放在主循环while(1)里,主循环里还有其他任务诸如按键扫描、OLED刷新、日志打印,这些任务耗时波动大,导致控制指令到达驱动器的时刻不稳定。低速电机对到达时间不敏感,一旦额定转速高、负载变化快,这种抖动就会被放大,直观表现就是运行噪声大、发热增加。解决思路是把发送任务放到定时器中断或实时操作系统的周期任务里,固定每隔5ms或1ms发一次速度指令。定时器频率选择和控制周期匹配,比如控制周期1kHz,就配置定时器1ms触发中断,在定时器中断里调用发送函数。同时关掉中断里的浮点运算,速度值提前计算好。
此外还要注意发送邮箱的使用。HAL_CAN_AddTxMessage会自动选择空邮箱,但如果上一次发送失败且AutoRetransmission开启,邮箱会一直被同一个帧占用,后续帧排队等待,实际周期就会被拖长。检查CAN_TxMailBox状态寄存器或者发完帧后把txMailbox变量打印出来,看看是否总在2号邮箱排队,如果发生这种情况,排查总线上是否有节点处于异常状态。
6. 进阶:从单电机到多电机协调,示波器验证最终时序
单电机跑通只是开始。实际设备往往是一条总线上挂多个驱动器和多个传感器节点,这时我的做法是给每台电机分配不同的报文ID。比如1号电机控制帧ID=0x001,2号电机控制帧ID=0x002,以此类推。不要尝试用同一帧ID只改数据域来区分子设备,因为CAN的仲裁机制靠ID优先级,ID也是节点身份的标识,混用会导致某个节点的过滤器把另一节点的报文当成自己的,产生“幽灵指令”。
多电机协调调度上,一种简单可靠的方式是分时发送:主控定时器每5ms扫描一遍所有节点,给每台电机发一帧控制报文。这种方式把总线占用控制在固定范围内,代价是电机数多了以后,每个节点的控制周期会被拉长。比如挂三个节点,5ms轮询一圈,单个节点的控制周期实际上是15ms,对高动态响应应用来说可能不够。另一种方式是无条件PDO,即每个驱动器周期性自发上报状态,主控仅在状态异常时发送干预指令,这种方式占用带宽少,但主控对电机的实时控制能力变弱。我一般的取舍是:需要精确协同的多电机,用分时调度;只是状态监控加远程启停的,用事件触发就够。
验证CAN时序是否正确,别只依赖软件做波特率检测。把示波器探头的CH1接到CAN_H,CH2接到CAN_L,差分模式只显示总线两根线的差值波形。发一帧数据后,测隐性电平到显性电平的跳变,配合触发功能抓单帧。一帧1Mbps的标准帧数据帧,位时间正好1μs,帧头是SOF显性位,然后逐位核对ID和CRC。你可以用示波器的光标功能测量连续两个下降沿之间的时间,如果稳定在1μs,说明波特率设置和硬件匹配;如果偏到1.1μs或0.9μs,多半是MCU的APB1时钟不是预期值,把实际时钟代入波特率公式反推修正参数。
多电机联动还有一个容易忽略的时序问题:两台电机同时收到启动指令,但它们各自驱动器内部的上电使能时序不同,会有一个先转一个后转的间隔。如果设备对这个间隔有严格要求,单纯靠CAN指令不够,需要驱动器支持“目标速度就绪”信号回传,主控等到所有节点都回复就绪后再发使能指令。这个流程说白了就是多节点握手,用帧ID和状态字节实现。我踩过最典型的一次坑,是控制两台电机同时升降,因为没做握手,一侧电机先到位后系统直接报错停机,后来加了状态同步流程才解决。
这些经验一句话总结:CAN控制电机难点不在CAN协议本身,而在物理层匹配和应用层约定。你把这两块做扎实,剩下的软件架构都是顺手的事。希望帮到你。
本文还有配套的精品资源,点击获取