ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32 CAN双机通信实战:CubeMX+HAL库配置与代码详解

STM32 CAN双机通信实战:CubeMX+HAL库配置与代码详解 刚接触STM32的CAN通信时我最直观的感觉是串口太简单CAN才是工业场景里真正耐打的东西。这次不绕弯子直接拿两块STM32F103最小系统板加两个CAN收发器模块用CubeMX配合HAL库从头到尾把双机通信这件事跑通。文章里会把CubeMX的每一项配置、每一行关键代码、排查思路都讲清楚适合刚入门CAN总线、想用HAL库快速实现通信或者之前用寄存器写过CAN但想转到CubeMX生态的开发者参考。1. 项目概述与方案选型1.1 为什么选STM32F103 CubeMX HAL库市面上关于STM32 CAN通信的教程不少但大多是寄存器版本移植性差换个芯片型号就得重新啃参考手册。选CubeMX加HAL库核心原因是它的工程生成效率实在太高。时钟树怎么走、外设时钟开没开、中断优先级怎么分这些事图形化配置一次代码自动生成比起对着参考手册翻寄存器位定义省下的精力可以用来真正理解CAN协议本身。很多人纠结HAL库和LL库哪个好。我的观点是对于CAN这种带硬件FIFO和硬件位定时器的外设HAL库的性能开销完全在可接受范围内。LL库虽然更接近寄存器操作但代码量会成倍增加调试起来也更费劲。STM32F103的bxCAN控制器最高支持1Mbps的通信速率配套的HAL库封装了报文发送、接收、错误管理、滤波器配置等核心操作对绝大多数工控和车载场景完全够用。另外一个选型理由是生态。CubeMX生成的工程自带完整的外设初始化框架后续加个串口打印调试信息、加个定时器做周期发送都是几分钟的事。这次双机通信做完了如果你想在这个基础上扩展成多机CAN网络或者CANopen协议栈HAL库的驱动框架也能直接复用不用推翻重写。1.2 硬件清单与双机连接方法硬件需求不复杂清单如下器件型号/规格数量备注MCU板STM32F103C8T6最小系统板2块带板载LED和按键更佳CAN收发器TJA1050 或 SN65HVD230 模块2个买带120欧姆终端电阻配置的模块连接线杜邦线或焊接线若干尽量短CAN总线建议双绞线调试工具ST-Link 或 J-Link2个单仿真器也可先烧录再脱机运行示波器/逻辑分析仪可选1台排查波形问题用CAN控制器的收发引脚是固定的STM32F103C8T6的CAN1_TX在PA12CAN1_RX在PA11。接线分两个层面MCU与收发器模块之间是一组TTL电平信号两个模块之间是差分总线。节点A和节点B各自独立连接PA12 (CAN_TX) 连接收发器模块的TXDPA11 (CAN_RX) 连接收发器模块的RXDVCC和GND按模块要求供电TJA1050需要5V供电SN65HVD230可以用3.3V两个节点之间的总线连接节点A的CANH接节点B的CANH节点A的CANL接节点B的CANL两个节点的GND必须共地否则差分信号的共模电压会漂移严重时直接烧毁收发器终端电阻是两个120欧姆分别并联在总线的物理最两端。如果用的是现成的收发器模块很多模块上已经焊好了120欧姆电阻并且用跳线帽控制确认一下即可。两个节点通信时如果总线上只有一个120欧姆电阻在中间位置反射问题会导致波形畸变短距离低速可能侥幸能通但实际项目里千万别这么干。1.3 CAN总线基础一帧数据长什么样CAN和串口最大的区别是CAN总线上所有节点共享一对差分线没有主机从机之分谁想发就发靠标识符ID决定优先级靠滤波决定谁接收。一帧标准CAN数据帧由这些部分组成SOF帧起始一个显性位标志一帧开始仲裁场11位标识符加RTR位这个ID就是优先级数值越小优先级越高控制场IDE位、DLC数据长度代码表示后面跟几个数据字节数据场0到8个字节这就是我们传输的业务数据CRC场循环冗余校验接收方用它判断数据是否被干扰ACK场发送节点在ACK槽发送显性位任何接收正常的节点会在此处回一个显性位进行应答。这个机制很关键它决定了单节点在总线上无法正常发送EOF和IFS帧结束和帧间隔这里最值得强调的是ACK机制。每当你往CAN总线上发一帧数据发送节点的收发器会在ACK槽等待总线上出现显性电平。如果总线上没有任何其他节点正常接收ACK槽就会一直是隐性电平发送节点就会判定发送失败重试到一定次数后报错。这意味着哪怕你只是测试发送功能也必须在总线上挂至少一个能正常接收的节点。数据长度最多8字节这在看惯了串口大包传输的场景里会觉得不够用。但实际工业协议像CANopen的PDO、J1939都是基于8字节载荷设计的对于状态量、控制量、传感器数值这类小数据包8字节绰绰有余。如果确实要传更长的数据可以自己去定义多帧组包协议那是后话。2. CubeMX工程配置全流程2.1 时钟树与CAN挂载总线分析CAN外设挂在APB1总线上这一点在配置时钟树时特别容易忽略。很多人习惯把APB1分频设成4倍即72MHz系统时钟下APB1只有36MHz而CAN的位定时器就是从这个36MHz时钟派生出来的。波特率配置时如果用错时钟源数值计算出的波特率会差整整一倍。我这次采用的时钟配置是外部8MHz晶振HSE作为时钟源PLL倍频9倍系统时钟72MHzAPB1分频系数2APB1外设时钟36MHzAPB1定时器时钟倍频到72MHz在CubeMX的Clock Configuration页面操作时把HCLK输入72软件会自动算好各总线分频。注意CAN外设的时钟源是APB1外设时钟也就是36MHz不是72MHz。记住这个数字下面算波特率全靠它。顺带提一句工程生成时在Project Manager页面里把Generate peripheral initialization as a pair of .c/.h files per peripheral勾上这样每个外设生成独立的c和h文件代码结构清晰很多。后续加外设时也不会所有代码都堆在main.c里。2.2 CAN外设参数配置与波特率计算方法在CubeMX的Connectivity里找到CAN1勾选Activate后进入参数配置。CAN的位时序由三段组成同步段Sync Segment固定为1个时间量子Time Quantum简称Tq时间段1BS1包含传播段和相位缓冲段1时间段2BS2相位缓冲段2波特率计算公式是 波特率 APB1时钟 / (预分频值 × (1 BS1 BS2))CubeMX里对应的参数名分别是Prescaler、Time Quanta in Bit Segment 1、Time Quantum in Bit Segment 2。这里的1就是同步段默认占用的那一个Tq。我要配一个最常见的500kbpsAPB1是36MHz。反推 36MHz / 500kbps 72个Tq这72个Tq要分给预分频值和位时序。一种常见的分配方式是预分频6位时序总共12个Tq即BS1取7BS2取4因为1 7 4 126 × 12 72正好500kHz。CubeMX界面上按此填写Prescaler (for Time Quantum)6Time Quanta in Bit Segment 17Time Quanta in Bit Segment 24ReSynchronization Jump Width3ReSynchronization Jump Width是重同步跳转宽度一般取BS1和BS2中较小值的约1/4这里取3。采样点位置为(1 7) / (1 7 4) ≈ 66.7%这个采样点对于500kbps是比较推荐的范围。如果你要跑1Mbps则总Tq数变成36预分频3加位时序12即可跑250kbps则可以预分频12加位时序12。关键在于让总Tq数配合预分频落在合理范围。注意BS1范围是1到16BS2范围是1到8超出要调整组合。2.3 滤波器和中断的配置CAN的接收滤波器是一个很容易栽跟头的地方。STM32F103的bxCAN有28个滤波器组每个滤波器组可以配置成屏蔽模式或者列表模式可以关联到FIFO0或FIFO1。屏蔽模式的意思是掩码位为1的位必须精确匹配掩码位为0的位不关心。如果掩码全部为0那这个滤波器就是接受所有报文这对调试阶段最友好。列表模式则是精确匹配掩码位用不到ID必须完全一致才能通过。这次双机通信的调试阶段我建议两个节点都先用接收所有帧的配置也就是掩码全0。先把链路调通再根据项目需求收紧滤波规则。滤波器配置CubeMX没有图形化界面需要在代码里手动写。中断方面CAN接收有两种方式处理轮询加中断。由于报文到达是异步的轮询会浪费CPU尤其在多节点、高负载的场合所以推荐用中断。CubeMX中在CAN1的NVIC Settings里使能CAN RX0 interrupt也就是FIFO0消息挂起中断。发送完成中断在双机场景可以不开发送时通过函数返回值判断结果就够用了。关于中断接收和DMA接收的取舍这里多说一句。STM32F103的bxCAN接收FIFO本身就是硬件缓冲每个FIFO深度3帧加上HAL库的中断回调机制处理一个8字节的报文只在中断里待几微秒。对绝大多数场景来说中断接收完全够用。DMA更适合串口那种高频大流量数据流形态CAN报文本身小而离散强行上DMA收益不大还白费很多配置精力。3. HAL库双机通信代码实现3.1 CAN初始化与启动流程CubeMX生成的CAN初始化代码主要做了两件事调用HAL_CAN_Init配置位时序调用HAL_CAN_MspInit配置GPIO复用和时钟。MspInit里PA11、PA12被配置成复用推挽输出和复用输入这些代码框架自动生成不用动。自己需要补的代码有两段。第一段是滤波器配置要放在HAL_CAN_Init之后。第二段是启动CAN外设和使能接收中断我习惯放在main函数里MX_CAN1_Init之后用一个单独的CAN_User_Init函数管理。滤波器配置代码如下void CAN_Filter_Config(void) { CAN_FilterTypeDef can_filter_config; can_filter_config.FilterActivation ENABLE; can_filter_config.FilterBank 0; can_filter_config.FilterMode CAN_FILTERMODE_IDMASK; can_filter_config.FilterScale CAN_FILTERSCALE_32BIT; can_filter_config.FilterIdHigh 0; can_filter_config.FilterIdLow 0; can_filter_config.FilterMaskIdHigh 0; can_filter_config.FilterMaskIdLow 0; can_filter_config.FilterFIFOAssignment CAN_RX_FIFO0; HAL_CAN_ConfigFilter(hcan, can_filter_config); }FilterBank写0到27任意值都可以前提是多个CAN外设之间不要冲突。FilterFIFOAssignment绑定到FIFO0这样接收中断就只关心FIFO0的回调。掩码全0所有ID都能进。启动和使能中断的代码void CAN_User_Init(void) { HAL_CAN_Start(hcan); HAL_CAN_ActivateNotification(hcan, CAN_IT_RX_FIFO0_MSG_PENDING); }这里有个顺序问题要强调必须先HAL_CAN_Start再ActivateNotification。虽然反过来也能跑但先注册通知再启动外设存在一种极端时序中断在外设启动的瞬间触发但通知还没完全准备好可能丢第一帧报文。配置完成后可以用HAL_CAN_IsTxMessagePending查询发送状态或者直接用HAL_CAN_GetTxMailboxesFreeLevel看一下邮箱余量。调试时我会把这两个信息通过串口打印出来非常直观。3.2 发送端代码从数据到报文HAL库发送CAN报文的核心函数是HAL_CAN_AddTxMessage签名如下HAL_StatusTypeDef HAL_CAN_AddTxMessage(CAN_HandleTypeDef *hcan, CAN_TxHeaderTypeDef *pHeader, uint8_t aData[], uint32_t *pTxMailbox);发送前要填充CAN_TxHeaderTypeDef结构体。以下是我在一个按键触发发送示例中的完整代码CAN_TxHeaderTypeDef TxHeader; uint8_t TxData[8] {0}; uint32_t TxMailbox; TxHeader.StdId 0x01; // 标准帧ID TxHeader.ExtId 0; // 扩展帧ID用不到置0 TxHeader.IDE CAN_ID_STD; // 使用标准帧 TxHeader.RTR CAN_RTR_DATA; // 数据帧 TxHeader.DLC 8; // 发送8字节 TxData[0] 0xAA; TxData[1] send_counter; // 其他字节自行填充 if (HAL_CAN_AddTxMessage(hcan, TxHeader, TxData, TxMailbox) ! HAL_OK) { Error_Handler(); }StdId我设为0x01是作为这台节点的身份标识。另一个节点发回来的数据ID用0x02这样每台设备发出去的数据接收方看一眼ID就知道是谁发的。关于IDE和RTRIDE决定用标准帧还是扩展帧扩展帧多出18位ID总线上本来资源就紧张没必要用它。RTR通常用数据帧远程帧在实际应用中使用较少至少双机通信这个阶段完全不需要。HAL_CAN_AddTxMessage的返回值值得仔细看。返回HAL_OK表示报文已经进入邮箱等待发送并不代表总线上发送成功。真正的发送结果要看总线是否产生错误。如果总线上只有发送节点这一个节点ACK没人回应这个函数可能反复重发然后返回HAL_ERROR底层错误码是HAL_CAN_ERROR_ACK。发送端还有一个经验之谈不要在主循环里无延时地持续猛发报文尤其是用阻塞方式调用HAL_CAN_AddTxMessage时邮箱满了会一直等待。我一般用定时器做周期发送或者用按键做事件触发这样既方便观察又不会阻塞主循环。3.3 接收端代码中断回调与数据处理CAN接收中断使能后当FIFO0收到一帧报文HAL库会回调HAL_CAN_RxFifo0MsgPendingCallback。需要在自己的用户代码里重写这个函数。接收报文用HAL_CAN_GetRxMessage它会把报文从FIFO取出来放入我们提供的结构体和数组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 0x01) { received_counter RxData[1]; HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } } }这里有几个细节需要特别注意。第一RxFF0MsgPendingCallback是弱函数CubeMX生成的main.c里没有默认实现你在哪个c文件里写都可以只要函数名正确链接时就会覆盖弱定义。我习惯在单独的user_can.c里维护所有CAN相关回调这样工程结构清爽。第二GetRxMessage调用后FIFO就会释放一个新空位。所以这个函数必须在中断回调里及时调用否则FIFO存满3帧后后续报文会被硬件丢弃而且不会产生新的中断通知。第三中断回调里定义的RxData是栈上的局部变量数据处理完就失效。如果只是用一下这个数据比如翻转LED、比较某个值没问题。但如果你想跨函数保存数据必须用全局数组或者结构体拷贝一份否则出了回调数据就没了。这个坑很多新手踩过。第四回调函数里尽量不做耗时操作比如串口打印大量日志、调用HAL_Delay这些统统避免。CAN报文进来时中断上下文中不允许被其他同优先级或低优先级中断打扰太久。如果需要区分不同ID的报文走不同处理逻辑可以用switch或者多个if判断RxHeader.StdId。多机场景下这个ID分发逻辑会扩展成一张路由表但双机阶段简单判断即可。3.4 双机交互逻辑设计双机通信不只是把数据发出去就算完事而是要有来有往形成一个可验证的闭环。我设计了一个简单但完整的交互逻辑节点AID 0x01按键PA0触发发送发送8字节其中第2字节是递增计数器节点BID 0x02当收到ID为0x01的数据帧翻转板载LED然后原样回发一帧同样的数据ID改为0x02节点A收到ID为0x02的回复帧后把接收计数器的值通过串口打印出来这个逻辑的好处是按键触发发送可以人为控制通信节奏方便观察按键每按下一次节点B的LED就应该翻转一次这是肉眼可见的证据节点A串口打印的接收计数验证了双向链路都是通的如果A能发出去但收不到回复大概率是B的接收或发送有问题问题定位范围一下就缩小了节点B的接收回调代码如下void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef RxHeader; uint8_t RxData[8]; uint32_t TxMailbox; if (hcan-Instance CAN1) { HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, RxHeader, RxData); if (RxHeader.StdId 0x01) { HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); TxHeader.StdId 0x02; TxHeader.IDE CAN_ID_STD; TxHeader.RTR CAN_RTR_DATA; TxHeader.DLC 8; HAL_CAN_AddTxMessage(hcan, TxHeader, RxData, TxMailbox); } } }回发时我没有重新填充TxData直接把接收缓冲区的RxData作为发送数据用因为数据格式一样这也体现了8字节载荷的拷贝成本很低。如果要进一步加上接收确认机制可以在节点A收到回复后把接收到的计数器和本地发送计数器做差值比较如果对不上就说明链路丢帧。这个验证方法可以留着等你想测CAN总线在干扰环境下的稳定性时再用。4. 联调验证、常见问题与扩展思考4.1 上电联调如何判断通信是否建立硬件连接完成后先别急着烧代码。第一步做静态检查万用表量一下CANH和CANL之间的电阻在总线上所有节点都断电时理论上应该测到60欧姆左右。因为两个120欧姆终端电阻并联这是最直接的判断总线连接是否正确的方法。如果测到120欧姆说明只有一端接了终端电阻如果测到无穷大说明两端都没接或者总线断线。第二步是烧录后观察现象。按节点A的按键观察节点B的LED是否翻转。理论上按一次节点B上的LED就应该闪一次。如果你的收发器模块上带总线活动指示灯能看到CAN总线上的波形活动。第三步用串口看数据。把节点A的PA9USART1_TX接USB转TTL打印发送和接收到的计数器值。看到数值递增说明完整链路正常。如果一切正常你将会看到的现象有按A按键A串口打印Send: 3过几十毫秒打印Recv: 3B的LED随每次按键翻转一次两个节点的计数器数值一致表明确实是同一帧数据被原样送回有条件的可以上示波器看CANH和CANL之间的差分波形。空闲时CANH和CANL都是2.5V差分电压为0显性位时CANH拉到3.5VCANL拉到1.5V差分约2V。这个波形一出整个链路的状态就一览无余了。4.2 常见问题排查速查表排查问题时保持一个信念CAN总线的问题大多是物理层问题和配置问题逻辑问题反而很少。我从实际调试中整理了一些高频问题做成速查表现象可能原因排查方法发送函数返回HAL_ERROR错误码为ACK总线上只有发送节点一个节点无应答确认另一个节点已正确烧录并接入总线发送函数一直超时邮箱满之前发送未完成检查是否有节点应答断电重启接收中断从不触发滤波器配置错误或未使能中断检查FilterMaskId是否全0检查NVIC是否使能能接收但ID不对标准帧扩展帧设置不一致确认收发两端IDE字段一致总线波形异常畸变缺少终端电阻或共地不好测CANH-CANL电阻确认共地收发器芯片发烫供电电压错误TJA1050用5VSN65HVD230用3.3V两个节点都在总线却收不到波特率不一致用逻辑分析仪抓波形数位宽还有一类典型问题是CubeMX配置里CAN引脚被复用成其他功能。比如你同时开了USB、SPI或者调试串口PA11和PA12可能被其他外设占据导致CAN引脚在MspInit里没有被正确配置成CAN复用功能。这时可以检查一下GPIO_InitStruct的Alternate参数是否正确。关于波特率不一致的排查我这里分享一个土办法。用逻辑分析仪抓到CAN_TX引脚发送端的波形数一个位的宽度。500kbps的位宽是2微秒250kbps是4微秒。数出来多少微秒波特率就是1除以位宽。这个方法比看代码配置直观多了。4.3 从双机到多机项目进阶方向与避坑双机通信跑通后往多机扩展是自然而然的方向。CAN总线理论上最多支持110个节点但实际受限于收发器驱动能力和总线电容推荐节点数在32个以内比较稳妥。多机场景下ID规划和滤波策略变得更加重要。我给一个建议方案。把ID划分成几个区段0x01到0x0F用于主站到从站的控制指令从站ID不同对应不同设备0x10到0x1F用于从站上报状态给主站0x20到0x2F用于设备间点对点通信。每个从站的滤波器配置成只接收和自己的ID或者广播ID匹配的帧可以大幅降低主站的接收负载。滤波器配置成列表模式时FilterIdHigh和FilterIdLow按位拆分。32位滤波器后标准帧ID占前11位RTR位占第12位其他位可以设置成0。直接给一段示例代码can_filter_config.FilterMode CAN_FILTERMODE_IDLIST; can_filter_config.FilterScale CAN_FILTERSCALE_32BIT; // 列表模式ID匹配0x111标准帧 can_filter_config.FilterIdHigh (0x111 5); can_filter_config.FilterIdLow 0x0000; can_filter_config.FilterMaskIdHigh (0x222 5); can_filter_config.FilterMaskIdLow 0x0000;这种配置对新手不太友好建议先用掩码全0跑通功能再升级成列表模式做精准过滤。多机场景还有一个必须考虑的问题总线负载。负载率 实际传输的位时间 / 可用总时间。一帧标准CAN数据帧在500kbps下大约占222微秒。假设你要用20ms周期发送10帧报文总占用时间大约是222微秒×10等于2.22毫秒负载率就是2.22/20约11.1%。对于总线设计负载率建议控制在50%以下超过70%就要警惕错误帧和丢失报文。另外如果你使用的场景对实时性要求高建议打开CAN的硬件自动重传不影响但要做好错误处理。HAL库提供了HAL_CAN_ErrorCallback回调可以在总线错误时捕获错误码。实际项目中这些错误信息最好能记录下来方便后续分析总线健康度。最后说一个调试技巧。如果调不通不要一直加打印信息看现象。先用逻辑分析仪抓CAN_TX引脚看发送端有没有波形。有波形但没接收问题在后级连波形都没有问题在前期配置或者引脚复用。这个排查思路能帮你把问题范围快速缩小到具体的分层避免在错误的方向上反复猜测。一些额外的收尾体会双机CAN通信做到这里其实已经把一个工业级现场总线从应用层到物理层完整过了一遍。我自己做过不少CAN相关的项目最大的体会是CAN总线的强大不在于它单帧能传多少数据而在于它的可靠性和确定性。搞清楚波特率怎么算、ID怎么规划、滤波器怎么配、错误怎么排查这四件事万变不离其宗。最后再分享一个小技巧。调试阶段如果两个节点需要同时连接调试器但又只有一个电脑最好先把两个程序都烧录好然后断开烧录器用外接电源给板子供电跑脱机。因为调试器接地和CAN总线共地有时候会形成地环路导致偶发通信异常排查起来非常折腾。断开调试器用独立供电这个问题就从根上避免了。
返回列表