嵌入式CAN总线通信实战:从硬件选型到STM32代码实现与深度调试 1. 从零到一为什么CAN总线是嵌入式开发的必修课如果你刚接触汽车电子、工业控制或者机器人领域那么“CAN总线”这个词你肯定绕不过去。它不像I2C、SPI那样在单片机开发板上随处可见但却是连接现代复杂机电系统的“神经系统”。我第一次接触CAN是在一个车载控制器项目里当时面对着一堆陌生的术语——报文、仲裁、ID、DLC——感觉比学一门新语言还难。但当我真正用代码让两个节点通过两根线CAN_H和CAN_L稳定地“对话”时那种豁然开朗的感觉至今难忘。CAN通信的实例不仅仅是几行驱动代码的调用它背后是一套完整的、为高可靠实时通信而生的设计哲学。这篇文章我就以一个典型的嵌入式开发场景为例手把手带你实现一个完整的CAN通信实例从硬件选型、环境搭建到驱动编写、数据收发最后再到实际测试与深度排错。我会附上所有关键代码并解释每一行代码背后的“为什么”让你不仅能抄作业更能懂原理下次遇到问题自己能搞定。2. 硬件基石如何为你的CAN项目选择合适的控制器与收发器在写第一行代码之前硬件平台的确定是重中之重。很多新手会直接跳到软件部分结果发现程序怎么调都不通最后才发现是硬件链路没搭对。CAN通信的硬件核心有两部分CAN控制器和CAN物理层收发器。2.1 CAN控制器的选择集成与外置的权衡现代主流的微控制器MCU几乎都集成了CAN控制器这大大简化了我们的设计。比如ST的STM32F1/F4系列NXP的LPC17xx系列TI的C2000系列等。选择集成CAN控制器的MCU是第一原则因为它节省了PCB空间、降低了BOM成本更重要的是厂商提供的标准外设库如STM32的HAL库或标准库已经封装好了底层的寄存器操作我们只需调用API即可。注意即使MCU集成了CAN控制器它也只是处理协议层数据链路层的东西比如报文组装、校验、仲裁、错误处理等。它输出的信号是逻辑电平通常是TTL或CMOS电平无法直接驱动长长的CAN总线。那么什么时候需要考虑外置独立的CAN控制器芯片如MCP2515呢主要在这几种场景主控MCU没有集成CAN控制器比如你用的是一款古老的8051或者某些低成本的ARM Cortex-M0芯片。需要扩展CAN通道数量一个MCU的集成CAN控制器通常只有1-2路如果你的系统需要连接3条以上的独立CAN网络外置多路控制器是更经济的选择。作为学习或调试的过渡MCP2515通过SPI接口与MCU通信其编程模型相对直观有助于理解CAN报文的结构。但在实际产品中除非不得已否则优先选用集成方案。对于我们的实例我选择STM32F103C8T6也就是常说的“蓝色药丸”Blue Pill作为主控。它价格低廉资源丰富且集成了一个bxCAN控制器Basic Extended CAN足以完成从基础到进阶的学习。2.2 CAN收发器的选型与电路设计要点CAN控制器产生的逻辑信号必须通过CAN收发器转换成差分信号才能抗干扰地在总线上传输。最经典、最通用的芯片是NXP的TJA1050高速CAN最高1Mbps或TJA1040。它们功能类似引脚兼容。收发器电路的设计有几个关键点直接关系到通信的稳定性终端电阻CAN总线两端最远的两个节点必须各接一个120欧姆的终端电阻。它的作用是阻抗匹配消除信号在总线末端的反射保证信号完整性。这是新手最容易忽略导致通信失败的原因。在我们的两点通信实例中两个节点各带一个120欧姆电阻即可。电源与地确保为收发器提供稳定的5V或3.3V电源根据型号而定并且数字地MCU侧与模拟地总线侧通过0欧姆电阻或磁珠单点连接以减少噪声干扰。斜率控制与待机模式TJA1050有一个S引脚斜率控制。接高电平VCC时为高速模式通过一个电阻接地时可调节斜率以降低EMI。在一般1Mbps应用中直接接高电平即可。STB引脚是待机模式控制低电平有效正常工作时应接高电平或通过MCU GPIO控制。一个最小系统的连接示意图如下STM32F103C8T6 TJA1050 PA11 (CAN_RX) --------- TXD PA12 (CAN_TX) ---------- RXD (需串联120-220欧电阻限流) GND ------------ GND (共地至关重要) VCC (5V) --- VCC CAN_H --- 总线CAN_H CAN_L --- 总线CAN_L (在TJA1050的CAN_H和CAN_L之间建议并联一个几十pF的电容到地用于滤除高频噪声)在PCB布局时CAN收发器应尽量靠近MCU的CAN引脚和连接器走线尽可能短且CAN_H和CAN_L应作为差分对紧耦合走线等长等距。3. 软件环境搭建与CubeMX配置硬件准备就绪后我们进入软件环节。对于STM32我强烈推荐使用STM32CubeMX进行图形化初始化配置它能自动生成HAL库代码框架避免手动配置寄存器时出错。3.1 CubeMX工程创建与CAN外设初始化选择MCU在CubeMX中新建工程选择STM32F103C8Tx。配置时钟在RCC选项中将高速外部时钟HSE设置为Crystal/Ceramic Resonator。然后进入Clock Configuration标签页将系统时钟源切换到HSE并配置主PLL使系统时钟SYSCLK达到72MHz这是F103的典型最高频率。CAN外设的时钟来源于APB1总线其最高频率为36MHz72MHz系统时钟下APB1分频后正好是36MHz符合要求。配置CAN在Connectivity下找到CAN1。将CAN1的工作模式设置为Normal正常模式。调试初期也可以先用Loopback回环模式自测试这样不需要连接外部硬件就能验证软件逻辑。关键参数配置在Parameter Settings标签页Prescaler (for Time Quantum)时间份额分频器。这是决定通信波特率的核心。时间份额tq 1 / (CAN时钟频率 / Prescaler)。CAN时钟就是APB1的时钟36MHz。我们目标波特率是500kbps一个经典的配置是Prescaler 6。Time Quanta in Bit Segment 1相位缓冲段1。设置为13 tq。Time Quanta in Bit Segment 2相位缓冲段2。设置为2 tq。Time Quanta in ReSynchronization Jump Width再同步跳转宽度。设置为1 tq。计算验证总的时间份额数 1(同步段) 13(段1) 2(段2) 16 tq。时间份额长度 1 / (36MHz / 6) ≈ 166.67 ns。位时间 16 * 166.67 ns ≈ 2.667 us。波特率 1 / 位时间 ≈ 375 kHz等等这里出错了。我们目标是500kbps位时间应为2 us。重新计算tq 位时间 / 总tq数 2us / 16 125 ns。所需Prescaler CAN时钟频率 * tq 36MHz * 125ns 4.5。Prescaler必须为整数所以我们取Prescaler 9。此时tq 1/(36MHz/9) 250 ns。位时间 16 * 250 ns 4 us波特率 250 kbps。要得到500kbps需要将总tq数减少。让我们调整配置设Prescaler 4Segment1 10 tqSegment2 3 tqSJW 1 tq。总tq14。tq长度1/(36MHz/4)≈111.11 ns。位时间14*111.11 ns≈1.556 us波特率≈642 kbps。还是不对。实际上标准500kbps的常见配置是Prescaler9BS14BS23总tq8。tq250ns位时间2us波特率500kbps。所以最终配置应为Prescaler9 Time Quanta in Bit Segment 14 Time Quanta in Bit Segment 23。配置GPIOCAN的RX(PA11)和TX(PA12)会自动配置无需手动改动。生成代码在Project Manager中设置好工程路径、IDE如Keil MDK或STM32CubeIDE然后生成代码。3.2 过滤器配置理解CAN的“收件箱”机制CAN控制器有一个非常重要的功能报文过滤器。总线上可能有很多报文但我们的节点可能只关心其中几种。过滤器就像邮局的分类员只把符合条件的“信件”报文放入对应的“邮箱”FIFO从而减轻CPU处理中断的负担。在CubeMX的CAN配置中过滤器部分略显复杂。对于初学者我们可以先在代码中手动配置一个简单的“接收所有报文”的过滤器以便测试。// 在main.c的CAN初始化函数MX_CAN1_Init之后或在其末尾添加 CAN_FilterTypeDef can_filter; can_filter.FilterBank 0; // 使用过滤器组0 can_filter.FilterMode CAN_FILTERMODE_IDMASK; // 掩码模式 can_filter.FilterScale CAN_FILTERSCALE_32BIT; // 32位宽 can_filter.FilterIdHigh 0x0000; // 检查ID的高位 can_filter.FilterIdLow 0x0000; // 检查ID的低位 can_filter.FilterMaskIdHigh 0x0000; // 掩码高位0表示不关心对应位 can_filter.FilterMaskIdLow 0x0000; // 掩码低位0表示不关心 can_filter.FilterFIFOAssignment CAN_RX_FIFO0; // 通过过滤器的报文放入FIFO0 can_filter.FilterActivation ENABLE; // 启用该过滤器 can_filter.SlaveStartFilterBank 14; // 对于单CAN设备此参数忽略 if (HAL_CAN_ConfigFilter(hcan1, can_filter) ! HAL_OK) { Error_Handler(); }这段配置意味着设置了一个32位的掩码过滤器ID和掩码都设为0。在掩码模式下掩码位为0表示“不关心”对应ID位为1表示“必须匹配”。这里全0掩码意味着不关心任何ID位因此所有报文都会被接收。这在开发和调试阶段非常有用。4. 核心代码实现发送与接收的完整流程生成工程后我们进入main.c开始编写应用层代码。HAL库将CAN操作封装成了几个核心函数我们的任务就是正确地调用它们。4.1 CAN外设启动与中断使能首先在初始化后启动CAN外设并开启接收中断。// 启动CAN if (HAL_CAN_Start(hcan1) ! HAL_OK) { Error_Handler(); } // 使能FIFO0收到新报文的中断 if (HAL_CAN_ActivateNotification(hcan1, CAN_IT_RX_FIFO0_MSG_PENDING) ! HAL_OK) { Error_Handler(); }使能中断后当有报文存入FIFO0时就会触发中断执行我们定义的回调函数。4.2 组装与发送一帧CAN报文发送一帧数据我们需要填充一个CAN_TxHeaderTypeDef结构体它定义了这帧报文的“信封信息”然后调用发送函数。// 定义一个发送报文头结构体 CAN_TxHeaderTypeDef tx_header; uint32_t tx_mailbox; // 用于返回发送邮箱编号 uint8_t tx_data[8] {0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}; // 要发送的数据 // 填充报文头信息 tx_header.StdId 0x123; // 标准ID范围0-0x7FF。我们设为0x123 tx_header.ExtId 0; // 扩展ID标准帧时设为0 tx_header.IDE CAN_ID_STD; // 标识符类型标准帧 tx_header.RTR CAN_RTR_DATA; // 帧类型数据帧远程帧是CAN_RTR_REMOTE tx_header.DLC 8; // 数据长度码0-8表示后面data数组的有效字节数 tx_header.TransmitGlobalTime DISABLE; // 是否记录时间戳一般禁用 // 开始发送 if (HAL_CAN_AddTxMessage(hcan1, tx_header, tx_data, tx_mailbox) ! HAL_OK) { // 发送请求失败可能是所有发送邮箱都满了 // 这里可以加入重试或错误处理逻辑 } else { // 发送请求成功报文已进入发送邮箱将由硬件自动发送 // tx_mailbox 返回了使用的是哪个邮箱012可用于后续查询发送状态 }这里有几个关键点StdId vs ExtId标准帧ID是11位扩展帧是29位。通过IDE字段选择。工业上常用标准帧汽车领域扩展帧更常见。DLC一定要和你实际准备的tx_data数组有效长度一致。即使数组定义是8字节如果你只想发3字节DLC就设为3。发送是异步的HAL_CAN_AddTxMessage只是把报文放入发送邮箱硬件缓冲区真正的发送由CAN控制器在总线空闲时自动完成。你可以通过HAL_CAN_GetTxMailboxesFullLevel或HAL_CAN_IsTxMessagePending等函数查询发送状态或者使能发送完成中断。4.3 接收中断回调函数与报文解析接收我们采用中断方式效率更高。我们需要重写HAL库的弱定义回调函数HAL_CAN_RxFifo0MsgPendingCallback。// 在main.c的用户代码区/* USER CODE BEGIN 4 */定义这个回调函数 void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rx_header; // 用于存放接收到的报文头 uint8_t rx_data[8]; // 用于存放接收到的数据 // 从FIFO0中读取报文 if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rx_header, rx_data) HAL_OK) { // 成功读取到一帧报文 // 现在可以解析rx_header和rx_data了 // 示例通过串口打印接收到的信息假设已初始化串口 printf(CAN Message Received!\r\n); printf( ID: 0x%03X, , rx_header.StdId); // 打印标准ID printf(Type: %s, , (rx_header.IDE CAN_ID_STD) ? STD : EXT); printf(DLC: %d\r\n, rx_header.DLC); printf( Data: ); for (int i 0; i rx_header.DLC; i) { printf(%02X , rx_data[i]); } printf(\r\n); // 根据ID进行业务逻辑处理 if (rx_header.StdId 0x123) { // 处理ID为0x123的报文 // 例如将第一个字节的数据赋值给某个变量 // my_variable rx_data[0]; } } }这个回调函数会在每次FIFO0中有新报文时被自动调用。HAL_CAN_GetRxMessage函数会从硬件FIFO中取出最早的一帧报文并将其从FIFO中移除。务必注意在中断服务函数或回调函数中处理逻辑应尽可能简短避免长时间占用中断。像printf这种可能阻塞的函数在实时性要求高的系统中要慎用最好是通过设置标志位在主循环中处理数据。4.4 主循环中的综合应用示例我们将发送和接收结合起来做一个简单的自发自收回环模式和双机通信的示例。// 在main函数的初始化部分之后 HAL_CAN_Start(hcan1); HAL_CAN_ActivateNotification(hcan1, CAN_IT_RX_FIFO0_MSG_PENDING); uint32_t last_send_tick 0; const uint32_t send_interval 1000; // 发送间隔单位ms while (1) { uint32_t current_tick HAL_GetTick(); // 每隔1秒发送一帧数据 if (current_tick - last_send_tick send_interval) { last_send_tick current_tick; CAN_TxHeaderTypeDef tx_header; uint32_t tx_mailbox; uint8_t tx_data[8]; // 填充一些变化的数据比如发送系统运行的时间低32位 uint32_t time HAL_GetTick(); tx_data[0] (time 0) 0xFF; tx_data[1] (time 8) 0xFF; tx_data[2] (time 16) 0xFF; tx_data[3] (time 24) 0xFF; tx_data[4] 0xAA; tx_data[5] 0xBB; tx_data[6] 0xCC; tx_data[7] 0xDD; tx_header.StdId 0x456; // 使用另一个ID tx_header.ExtId 0; tx_header.IDE CAN_ID_STD; tx_header.RTR CAN_RTR_DATA; tx_header.DLC 8; tx_header.TransmitGlobalTime DISABLE; if (HAL_CAN_AddTxMessage(hcan1, tx_header, tx_data, tx_mailbox) HAL_OK) { // 可以点亮一个LED指示发送成功 HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } } // 主循环中可以处理其他任务 // 接收处理已在中断回调函数中完成 HAL_Delay(10); // 短暂延时防止CPU空转耗电 }5. 实战调试与深度排错当通信不通时该怎么办代码写好了硬件连好了但两个板子之间就是没有数据——这是每个工程师的必经之路。别慌按照以下步骤系统性排查绝大多数问题都能解决。5.1 硬件链路检查万用表与示波器是你的好朋友电源与地首先用万用表测量两个节点的电源电压是否稳定5V或3.3V地线是否连通。这是所有通信的基础。终端电阻这是最高频的故障点。用万用表电阻档测量总线的CAN_H和CAN_L之间的电阻。在总线只有两个节点且各带一个120Ω终端电阻的情况下并联后的总阻值应该是60Ω左右。如果测得120Ω说明只有一个终端电阻生效如果测得开路或阻值很大说明终端电阻没接或虚焊如果阻值远小于60Ω可能有短路。务必确保总电阻在55-65Ω之间。差分信号如果有示波器这是最直观的调试工具。将示波器的两个通道分别接CAN_H和CAN_L设置为差分测量或相减。在节点发送数据时你应该能看到清晰的差分信号波形。一个标准的位比如500kbps下2us宽的差分电压幅值应该在2V左右CAN_H - CAN_L。如果看不到波形说明发送端可能没工作如果波形畸变严重如过冲、振铃可能是终端电阻不匹配或布线问题。5.2 软件配置验证波特率与工作模式是隐形杀手波特率一致性这是软件层面第一要检查的。两个通信节点的波特率必须精确一致。哪怕有千分之一的误差长时间累积也会导致错位。请反复核对双方CubeMX中Prescaler、BS1、BS2的设置是否完全相同。一个技巧是将双方都先设置为回环模式Loopback。在此模式下节点自己发送的数据会被自己接收不经过物理总线。如果回环模式下自己能收到自己发的数据说明软件驱动和基本配置是正确的。工作模式确认双方都处于正常模式Normal而不是静默模式Silent或只监听模式。静默模式下可以接收但不能发送只监听模式则只接收不发送也不应答常用于网络分析。过滤器配置检查接收方的过滤器是否过于严格把目标报文过滤掉了。调试阶段可以像我之前那样配置一个“接收所有”的过滤器。5.3 进阶问题错误状态与总线负载当通信基本打通后可能会遇到间歇性丢帧或错误。这时需要关注CAN控制器的错误状态。// 可以定期查询CAN控制器的错误状态 CAN_HandleTypeDef* hcan hcan1; // 假设 uint32_t error_status HAL_CAN_GetError(hcan); if (error_status ! HAL_CAN_ERROR_NONE) { printf(CAN Error: 0x%08lX\r\n, error_status); if (error_status HAL_CAN_ERROR_EWG) { printf( - Error Warning State\r\n); } if (error_status HAL_CAN_ERROR_EPV) { printf( - Error Passive State\r\n); } if (error_status HAL_CAN_ERROR_BOF) { printf( - Bus-Off State\r\n); } if (error_status HAL_CAN_ERROR_STF) { printf( - Stuff Error\r\n); } if (error_status HAL_CAN_ERROR_FOR) { printf( - Form Error\r\n); } if (error_status HAL_CAN_ERROR_ACK) { printf( - Acknowledgment Error\r\n); } if (error_status HAL_CAN_ERROR_BR) { printf( - Bit Recessive Error\r\n); } if (error_status HAL_CAN_ERROR_BD) { printf( - Bit Dominant Error\r\n); } if (error_status HAL_CAN_ERROR_CRC) { printf( - CRC Error\r\n); } // 发生严重错误如Bus-Off后可能需要软件干预恢复 if (error_status HAL_CAN_ERROR_BOF) { // 先停止CAN HAL_CAN_Stop(hcan); // 等待一段时间 HAL_Delay(100); // 重新初始化并启动CAN可以调用MX_CAN1_Init()和HAL_CAN_Start // 注意简单的重启可能不够需要根据具体错误分析原因如总线持续短路 } }错误被动Error Passive通常由偶尔的位错误引起节点仍能通信但会在错误帧后发送被动错误标志。如果频繁进入需检查总线物理环境干扰、接地。总线关闭Bus-Off这是最严重的状态通常由节点自身持续发送错误如与总线断开导致。控制器会自动尝试恢复根据标准在检测到128次11个连续的隐性位后但软件最好也做监控和日志。5.4 使用CAN分析仪从上帝视角看总线对于复杂的网络一个USB-CAN分析仪如PCAN ZLG的USBCAN或者开源的CANable是终极调试利器。它作为一个独立的、可靠的节点接入总线可以监听所有报文并以清晰的时间戳、ID、数据格式显示出来。你可以用它来验证发送你的节点声称发送了报文分析仪上能看到吗ID和数据对吗验证接收总线上确实有目标报文吗还是发送方根本没发出来分析错误帧分析仪能捕获并显示错误帧的类型和位置是定位干扰、波特率失配等问题的最直接证据。压力测试模拟发送大量报文测试你节点的接收处理能力和稳定性。6. 从实例到产品可靠性设计与代码框架优化跑通一个点对点的例子只是开始。要把CAN用于实际产品还需要考虑更多工程化问题。6.1 通信协议设计让数据有意义原始的CAN帧ID8字节数据只是载体你需要定义一套高层应用层协议让数据变得有意义。这包括ID规划为不同类型的报文分配不同的ID。通常高优先级重要的、实时性要求高的报文分配更小的ID因为CAN仲裁机制ID值小的优先级高。数据编码定义每个字节甚至每个位的含义。例如一个4字节的电机转速值是直接用uint32_t的二进制形式发送还是转换成字符串通常采用二进制以节省带宽。需要考虑字节序Endianness即多字节数据在总线上的传输顺序。通常统一使用大端序Big-Endian或小端序Little-Endian并在文档中明确说明。报文拆分如果需要传输的数据超过8字节怎么办这就需要定义多帧传输协议。常见的如CANopen的SDO块传输、UDS的多帧传输等。你需要设计帧头来标识分段序号、总长度等。6.2 软件架构优化中断与主循环的分工前面的例子中接收在中断回调里直接处理如打印这在简单系统中可行但在复杂系统中会阻塞中断。更好的做法是// 定义一个环形缓冲区队列结构 typedef struct { CAN_RxHeaderTypeDef header; uint8_t data[8]; uint32_t timestamp; // 可选记录接收时间 } CanRxMsg_t; #define CAN_RX_QUEUE_SIZE 32 CanRxMsg_t can_rx_queue[CAN_RX_QUEUE_SIZE]; volatile uint16_t can_rx_queue_head 0; volatile uint16_t can_rx_queue_tail 0; // 在中断回调中只将报文存入队列 void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CanRxMsg_t new_msg; if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, new_msg.header, new_msg.data) HAL_OK) { new_msg.timestamp HAL_GetTick(); uint16_t next_head (can_rx_queue_head 1) % CAN_RX_QUEUE_SIZE; if (next_head ! can_rx_queue_tail) { // 队列未满 can_rx_queue[can_rx_queue_head] new_msg; can_rx_queue_head next_head; } else { // 队列溢出处理错误如丢弃最旧数据或报错 } } } // 在主循环中从队列取出并处理报文 void ProcessCanMessages(void) { while (can_rx_queue_tail ! can_rx_queue_head) { CanRxMsg_t msg can_rx_queue[can_rx_queue_tail]; can_rx_queue_tail (can_rx_queue_tail 1) % CAN_RX_QUEUE_SIZE; // 在这里进行耗时的报文解析和业务逻辑处理 HandleCanMessage(msg.header, msg.data); } } // 然后在main的while(1)循环中调用ProcessCanMessages()这样中断服务函数变得非常短只负责“搬数据”将耗时的解析工作留给主循环提高了系统的实时性和稳定性。6.3 超时与重发机制对于重要的命令或数据需要有应答和重发机制。例如节点A发送一个“设置参数”的命令帧ID 0x100给节点B节点B执行成功后应回复一个“应答”帧ID 0x101。节点A发送后启动一个定时器如果在规定时间内如100ms没收到0x101的应答则认为通信失败进行重发最多重试3次。这需要在应用层协议中设计。6.4 总线管理心跳与节点状态检测在由多个节点组成的网络中需要知道哪些节点在线。一个常见的做法是让每个节点周期性地发送“心跳”或“生命信号”帧。主节点或其他节点监听这些心跳。如果某个节点的心跳超时未到就可以判断该节点可能掉线或故障从而触发相应的安全处理逻辑。7. 常见问题与经验之谈最后分享几个我踩过坑后总结的经验上电顺序与总线干扰多个节点同时上电瞬间CAN总线可能会产生剧烈的瞬态干扰导致一些节点误入错误状态甚至Bus-Off。可以在软件初始化CAN外设前增加一个几十毫秒的延时。或者在硬件上CAN收发器的STB待机引脚由MCU控制等系统电源稳定后再将其拉高激活收发器。地环路干扰如果两个节点距离较远且分别接地可能会形成地环路引入共模噪声。使用带隔离的CAN收发器模块内部有光耦或磁耦隔离是解决此问题的最佳方案虽然成本增加但系统可靠性大幅提升。线缆选择与布线CAN总线推荐使用双绞线并且屏蔽层单点接地。线径不宜过细建议不低于0.75mm²长距离通信超过100米时需降低波特率如125kbps或50kbps。ID冲突确保网络中所有节点的发送ID是唯一的。如果两个节点用同一个ID发送数据它们会不断仲裁导致总线异常。最好在项目初期就规划好ID分配表。HAL库的阻塞超时HAL_CAN_AddTxMessage函数在某些配置下如果所有发送邮箱都满可能会等待并超时。要留意其第三个参数Timeout。在实时性要求高的场合可以设置为0非阻塞然后通过查询或中断的方式等待发送邮箱空闲。

本月热点