尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

嵌入式必学:CAN总线协议精讲与Arduino双节点收发实战

嵌入式必学:CAN总线协议精讲与Arduino双节点收发实战 嵌入式开发中CAN 总线协议是绝对绕不开的一个基础通信协议尤其在汽车电子、工业控制、医疗器械等领域CAN 总线几乎可以说是“事实标准”。很多入门嵌入式开发的同学前几讲还在点灯、串口打印学到 CAN 总线时突然发现为什么还要分 CAN_H 和 CAN_L为什么一组总线只有两根线却能挂几十个设备为什么需要终端电阻这一讲就来把这些概念一次讲清楚并且带大家动手跑一个完整的 CAN 收发实验。本文会从物理层、数据帧、仲裁机制、错误处理这几个核心角度拆解 CAN 总线协议再配合 Arduino 和 MCP2515 模块完成一个“双节点收发”的实战项目最后整理高频排错思路和工程实践经验。适合刚刚入门嵌入式通信、准备接触车载总线或者在项目中第一次使用 CAN 的开发者和学生。1. 为什么嵌入式开发要学 CAN 总线1.1 CAN 总线解决了什么问题CAN 是 Controller Area Network控制器局域网的缩写最初由 Bosch 公司为汽车电子设计目的是解决车内多个控制器之间的数据共享和信号传输问题。传统汽车里面发动机控制器、变速箱控制器、ABS 控制器、安全气囊控制器之间需要交换大量实时数据如果每个传感器都单独拉一根信号线到控制器线束会非常庞大、成本高、可靠性差。CAN 总线用一对差分信号线CAN_H 和 CAN_L把所有节点挂在同一条总线上节点之间通过报文广播方式交换数据。任何节点发送的数据总线上所有其他节点都能收到然后由每个节点自己决定“这条数据是否和我有关”。这样做的好处非常明显布线简单线束成本低。多主架构没有主机/从机之分任何一个节点都可以主动发起通信。实时性强高优先级报文可以优先抢占总线。抗干扰能力强差分信号对共模噪声有天然抑制能力。容错机制完善支持错误检测、错误通知和错误恢复。1.2 CAN 总线的主要应用场景CAN 总线虽然诞生于汽车但现在已经广泛用于各种嵌入式场景应用领域典型场景汽车电子发动机控制、车身控制、电池管理系统BMS、车载诊断OBD工业自动化PLC 与传感器/执行器通信、现场设备控制医疗器械医疗设备内部通信、监护仪数据采集机器人关节电机控制、舵机通信、传感器网络船舶与航空发动机监控、航电设备数据交互所以不管你是做嵌入式 Linux、STM32 单片机开发还是以后想进入汽车电子行业CAN 总线都是必须掌握的基本功。很多嵌入式面试题也会围绕 CAN 的帧格式、仲裁机制、错误帧出题这一讲的内容同样适合面试复习。2. CAN 总线物理层基础2.1 差分信号与电平定义CAN 总线使用两根线传输数据分别是 CAN_H高线和 CAN_L低线。它不采用单端地对地电平表示 0 和 1而是通过两根线之间的电压差来表达逻辑状态这种方式叫做差分信号。标准高速 CANISO 11898-2的逻辑电平定义如下显性电平Dominant对应逻辑 0此时 CAN_H 约为 3.5VCAN_L 约为 1.5V差分电压约为 2V。隐性电平Recessive对应逻辑 1此时 CAN_H 和 CAN_L 都约为 2.5V差分电压约为 0V。在总线上显性电平会“覆盖”隐性电平。也就是说只要有一个节点输出显性电平整条总线就会呈现显性状态只有当所有节点都输出隐性电平时总线才呈隐性状态。这个特性正是 CAN 总线仲裁机制的基础后面我们会细说。2.2 终端电阻的作用CAN 总线规范要求在总线两端各接一个 120Ω 的终端电阻。这个电阻的作用有两个匹配总线阻抗防止信号反射。保证总线空闲时 CAN_H 和 CAN_L 之间的差分电压能够稳定在隐性电平附近。很多人刚开始实验时只接了一个节点发现无法正常通信原因往往就是总线上没有第二个节点也没有终端电阻。CAN 协议规定发送节点发送报文后需要等待总线上的 ACK 应答如果总线上没有其他节点应答发送节点会认为出错。所以CAN 通信至少要两个节点才能正常工作。如果使用市面上常见的 MCP2515 TJA1050 CAN 模块很多模块已经板载了 120Ω 终端电阻但需要确认跳线帽或电阻是否默认接入。如果自己搭电路需要在总线两端各并联一个 120Ω 电阻两个电阻并联后总的等效电阻约为 60Ω。2.3 波特率与总线长度CAN 总线的通信速率叫波特率单位是 bit/s。标准 CAN 最高支持 1Mbps常见速率有 125Kbps、250Kbps、500Kbps、1Mbps 等。波特率越高总线允许的长度越短具体关系可以参考下表波特率最大总线长度参考1Mbps约 40m500Kbps约 100m250Kbps约 250m125Kbps约 500m需要注意的是实际最大长度还与线缆质量、节点数量、终端电阻精度有关。在一条 CAN 总线上所有节点的波特率必须一致否则接收节点采样到的位宽不对就会导致大量错误帧甚至无法通信。3. CAN 协议核心机制3.1 CAN 报文帧类型CAN 总线上的数据以“帧”为单位传输标准帧和扩展帧是两种最常见的报文格式。它们的区别主要在于 ID 长度标准帧11 位标识符ID最多可定义 2048 种不同 ID。扩展帧29 位标识符ID由 11 位基础 ID 和 18 位扩展 ID 组成可定义更多 ID。除了数据帧CAN 协议还定义了远程帧、错误帧和过载帧。其中远程帧用于请求某个 ID 的数据错误帧用于总线错误通知过载帧用于延迟后续数据帧。入门阶段我们重点掌握数据帧结构即可。3.2 数据帧的标准结构一个标准数据帧由以下部分组成场位数说明帧起始 SOF1显性电平表示帧开始仲裁场1211 位 ID 1 位 RTR远程帧标志控制场6IDE 位、保留位、DLC 数据长度数据场0~64实际发送的数据最多 8 字节CRC 场1615 位 CRC 校验 1 位 CRC 分隔符ACK 场2ACK 槽 ACK 分隔符帧结束 EOF7隐性电平表示帧结束这里有几个关键点值得注意CAN 数据帧最多携带 8 字节数据这是协议设计时为了保证实时性和总线占用时间而定的和很多人的直觉不同。如果数据超过 8 字节需要分成多帧发送或者使用 ISO-TP传输层协议进行分包。RTR 位用来区分数据帧和远程帧。数据帧 RTR0显性远程帧 RTR1隐性。ACK 槽比较特殊发送节点在 ACK 槽输出隐性电平接收节点如果正确收到帧就会在 ACK 槽输出显性电平来“应答”。发送节点检测到显性电平后才知道有节点成功接收了报文。CAN 数据帧的“长度”指的是 DLCData Length Code它是控制场里面的 4 个位可以表示 0~8 的长度对应数据场 0~8 字节。3.3 总线仲裁为什么 ID 越小优先级越高CAN 总线是多主架构多个节点可能同时向总线发送数据。当两个节点同时发送时如何决定谁先发答案是逐位仲裁。仲裁过程是基于“显性电平覆盖隐性电平”这个物理特性实现的。每个发送节点在发送 ID 每一位的同时会监测总线电平。如果自己发送的是隐性位1而总线上检测到的是显性位0说明有另一个节点发送了显性位那么当前节点就自动退出发送转为接收。最终发送显性位更多的节点赢得仲裁。由于 ID 是从高位到低位逐位发送的ID 数值越小前面出现显性位0的概率越高因此 ID 小的报文优先级更高。这也是为什么在实际工程中越紧急、越重要的报文分配的 ID 就越小。举个简化例子假设节点 A 发送 ID0x110节点 B 发送 ID0x120它们同时发起。从最高位开始逐位比较在某个低位上 A 的 ID 位是 0显性B 是 1隐性B 检测到总线被拉低于是主动退出A 继续发送。整个仲裁过程对上层无感知由 CAN 控制器硬件自动完成。3.4 位填充机制CAN 协议为了保证接收节点能正确恢复时钟要求发送节点连续发送 5 个相同电平之后必须插入一个相反电平的位这个机制叫做位填充Bit Stuffing。例如如果数据流中出现11111发送方会自动在它后面插入一个0变成111110。接收方接收到 5 个连续相同位之后会自动把后面那个反相位当作填充位并移除。这样做的好处是避免长时间不变的直流电平导致接收方失去同步。如果在接收过程中检测到连续 6 个相同电平就说明违反了位填充规则这会被当作格式错误处理。位填充机制是 CAN 错误检测的重要手段之一。3.5 CAN 的错误处理与错误帧CAN 总线的一个很大优势是具备完善的错误处理机制。CAN 控制器内置发送错误计数器TEC和接收错误计数器REC每检测到一个错误对应计数器会加 1每正确收发一帧计数器会减 1。根据计数器的数值节点会进入不同的错误状态错误状态条件行为主动错误Error Active错误计数 128正常收发检测到错误时发送主动错误帧被动错误Error Passive128 ≤ 错误计数 ≤ 255可以收但发送受限制发送被动错误帧总线关闭Bus Off错误计数 255节点与总线断开不再参与通信主动错误帧由 6 个显性位组成。当一个节点检测到错误时它会主动发送错误帧把总线上正在进行的错误帧“打断”通知所有节点本次传输无效。错误帧发出后所有节点丢弃当前错误的报文然后重新开始仲裁和发送。如果总线上没有一个节点应答、波特率不匹配、终端电阻缺失、电磁干扰严重都会导致错误计数不断累加最终节点可能进入 Bus Off 状态表现为“总线上完全看不到该节点发数据”。排查 CAN 通信问题时查看错误计数器是一个很重要的手段。4. 开发环境与硬件准备4.1 硬件选择做 CAN 总线实验最经典、也最容易上手的方案是 Arduino 或 STM32 开发板外接一个 SPI 接口的 CAN 控制器模块。常见的模块方案有MCP2515 TJA1050最经典的分立方案MCP2515 是 SPI 转 CAN 控制器TJA1050 是 CAN 收发器。模块便宜资料多适合学习。STM32 内置 bxCANSTM32 系列很多型号自带 CAN 控制器只需外接一个 CAN 收发器如 TJA1050、SN65HVD230即可。ESP32 内置 TWAIESP32 也内置 CAN/TWAI 控制器配合收发器模块使用。本文实战部分以 Arduino UNO MCP2515 模块为例因为代码简单、接线直观适合作为第一个 CAN 实验。如果你使用 STM32原理完全一致只是寄存器/HAL 库配置不同。4.2 软件工具Arduino IDE用于编写、编译和烧录 Arduino 程序下载地址为 arduino.cc版本选择最新稳定版即可。MCP_CAN 库这是 Seeed Studio 提供的 MCP2515 驱动库在 Arduino IDE 的“库管理器”中搜索MCP_CAN即可安装。CAN 分析工具如果手头有 USBCAN 分析仪、逻辑分析仪或示波器可以用于抓取总线数据、观察错误帧。没有也没关系两个开发板互发即可完成实验。4.3 实验接线准备两块 Arduino UNO 和两块 MCP2515 模块。MCP2515 模块与 Arduino 的接线如下MCP2515 模块引脚Arduino UNO 引脚VCC5VGNDGNDSCKD13SCKSIMOSID11MOSISOMISOD12MISOCSD10可自定义INTD2可自定义两块模块的 CAN_H 接 CAN_HCAN_L 接 CAN_L两个模块都要接 5V 和 GND。如果模块没有板载终端电阻需要在总线两端各并联一个 120Ω 电阻。如果模块有跳线或默认已经接入电阻就按模块说明处理。注意接线前断电操作避免带电插拔损坏模块。Arduino UNO 的 5V 输出能力有限如果使用多个 CAN 模块建议使用独立电源为模块供电并将 GND 与 Arduino 共地。5. CAN 总线实战双节点收发实验5.1 项目功能规划本实验实现两个节点通信节点 A定时发送一帧 CAN 报文ID 为 0x123数据为 8 字节递增数据。节点 B接收 CAN 报文并把收到的 ID、长度和数据通过串口打印到电脑。通过这个实验可以直观理解 CAN 报文的发送、接收、ID 分配、数据长度等基本概念。后续可以扩展成按键发送、报警帧、多节点互发等。5.2 发送节点代码文件路径can_sender/can_sender.ino#include SPI.h #include mcp_can.h // 定义片选引脚MCP2515 模块 CS 接到 D10 const int SPI_CS_PIN 10; // 创建 MCP_CAN 对象 MCP_CAN CAN0(SPI_CS_PIN); // 预留的报文数据 unsigned char data[8] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; void setup() { Serial.begin(115200); // 初始化 CAN波特率设置为 500Kbps while (CAN0.begin(CAN_500KBPS) ! CAN_OK) { Serial.println(CAN init failed, retrying...); delay(200); } Serial.println(CAN init ok); } void loop() { // sendMsgBuf(canId, ext, len, data) // canId: 报文 ID这里使用标准帧 ID 0x123 // ext: 0 表示标准帧11 位 ID1 表示扩展帧29 位 ID // len: 数据长度最多 8 字节 // data: 数据缓冲区 unsigned char sndStat CAN0.sendMsgBuf(0x123, 0, 8, data); if (sndStat CAN_OK) { Serial.println(send ok); } else { Serial.println(send fail); } // 每 1 秒发送一帧 delay(1000); }代码说明MCP_CAN CAN0(SPI_CS_PIN)创建 CAN 控制器对象传入 SPI 片选引脚。CAN0.begin(CAN_500KBPS)初始化控制器和收发器这里指定波特率 500Kbps。初始化失败时会一直重试。CAN0.sendMsgBuf()是发送报文的接口第一个参数是 ID第二个参数表示是否扩展帧第三个表示数据长度第四个是数据指针。这里把数据设成固定 8 字节递增序列方便接收端观察。5.3 接收节点代码文件路径can_receiver/can_receiver.ino#include SPI.h #include mcp_can.h const int SPI_CS_PIN 10; MCP_CAN CAN0(SPI_CS_PIN); void setup() { Serial.begin(115200); while (CAN0.begin(CAN_500KBPS) ! CAN_OK) { Serial.println(CAN init failed, retrying...); delay(200); } Serial.println(CAN init ok); } void loop() { // 检查是否收到新报文 if (CAN0.checkReceive() CAN_MSGAVAIL) { unsigned char len 0; unsigned char buf[8]; // 读取报文 CAN0.readMsgBuf(len, buf); // 获取报文 ID unsigned long canId CAN0.getCanId(); // 打印报文信息 Serial.print(ID: 0x); Serial.print(canId, HEX); Serial.print( Len: ); Serial.print(len); Serial.print( Data: ); for (int i 0; i len; i) { if (buf[i] 0x10) { Serial.print(0); } Serial.print(buf[i], HEX); Serial.print( ); } Serial.println(); } }代码说明CAN0.checkReceive()查询接收缓冲区中是否有数据。返回CAN_MSGAVAIL时表示有报文。CAN0.readMsgBuf(len, buf)读取报文长度和数据。CAN0.getCanId()读取当前报文的 ID便于打印。实际项目中可以把这里的串口打印换成业务处理比如根据 ID 分发到不同处理函数。5.4 编译烧录与运行验证把发送节点代码烧录到节点 A 的 Arduino接收节点代码烧录到节点 B 的 Arduino。两个节点分别打开串口监视器波特率设置为 115200。正常情况下节点 A 的串口会每隔 1 秒打印一行send ok节点 B 的串口会打印类似下面的内容CAN init ok ID: 0x123 Len: 8 Data: 01 02 03 04 05 06 07 08 ID: 0x123 Len: 8 Data: 01 02 03 04 05 06 07 08如果节点 A 打印的是send fail或者节点 B 收不到数据优先检查两方面两块板子的波特率是否一致。终端电阻是否接好CAN_H 和 CAN_L 是否正确连接。两根 CAN 模块的 GND 是否已经共地。5.5 进阶使用 CAN 分析仪观察总线如果条件允许可以在总线上并联接入一个 CAN 分析仪USBCAN 或逻辑分析仪用配套软件抓取总线报文。通过分析仪可以看到完整报文帧包括 ID、DLC、数据、CRC 校验结果等还能直接观察到错误帧。这是工程调试中最常用的手段之一。使用分析仪时要注意把分析仪的波特率设置成和总线一致比如 500Kbps。分析仪本身也是一个 CAN 节点它同样会参与 ACK 应答因此即使只有单个节点和分析仪挂在总线上也能完成通信实验。6. CAN 总线常见问题与排查思路6.1 高频问题排查表新手接触 CAN 总线时遇到的问题往往集中在物理连接、波特率配置和模块初始化三方面。下面整理了一张排查表问题现象常见原因解决思路初始化失败一直打印 init failedSPI 引脚接错、CS 引脚冲突、模块供电不足检查接线确认 SPI 引脚和 CS 引脚定义尝试外接电源发送失败send fail总线上没有第二个节点、缺少终端电阻、波特率不匹配至少接入两个节点检查终端电阻统一波特率能发送但收不到数据ID 过滤配置错误、CAN_H 和 CAN_L 接反检查接收屏蔽/过滤器确认接线收到错误帧或串口打印乱码波特率不一致、总线干扰、地电位差用分析仪抓包检查对地电压缩短总线长度节点过一段时间不通信错误计数累加进入 Bus Off查看错误寄存器解决物理层干扰重启恢复只有一个节点无法通信CAN 需要至少一个接收节点应答 ACK接入第二个节点或开启回环Loopback模式6.2 如何定位物理层问题当总线通信异常时先不要急着改代码。推荐按以下顺序排查用万用表测量 CAN_H 和 CAN_L 之间的电压。总线空闲时两者之间电压接近 0V通信时会出现约 2V 的差分电压。检查两个终端电阻。在断电状态下用万用表电阻档测量总线两端的阻抗应该在 60Ω 左右。如果测得约 120Ω说明只有一端有电阻如果测得接近 0Ω说明短路。检查 CAN_H 和 CAN_L 是否接反。CAN_H 必须接 CAN_HCAN_L 必须接 CAN_L。检查所有节点是否共地。不同节点使用独立电源时GND 必须连在一起否则差分信号的参考电压不一致会导致大量错误帧。如果使用长线连接避免把 CAN 线绕成线圈或靠近大功率器件。6.3 ID 过滤导致收不到数据MCP2515 模块支持接收过滤功能可以通过设置验收屏蔽寄存器和验收代码寄存器只接收特定 ID 的报文。MCP_CAN 库默认初始化时会清空过滤条件即接收所有报文。如果你自己配置过过滤条件需要检查过滤器是否把目标 ID 屏蔽掉了。在工程中合理使用 ID 过滤可以减轻 MCU 的处理负担比如一个空调控制器只需要关心车身模块发来的温度相关报文就可以只接收对应 ID 区间。但调试时建议先关闭过滤确保链路打通后再逐步增加过滤逻辑。7. 工程实践与设计建议7.1 合理规划 CAN IDCAN 报文 ID 不仅仅是一个地址它还决定了优先级。在汽车和工业项目中通常会提前制定一张 ID 规划表明确规定哪些报文属于实时控制类分配小 ID如 0x010~0x0FF。哪些报文属于状态上报类分配中等 ID。哪些报文属于诊断配置类分配大 ID。每个 ID 的发送周期、数据字节含义、信号格式。例如刹车控制报文的更新周期可能是 10msID 会分配得比较小而车窗状态报文可能 100ms 或按需发送ID 相对大一些。没有规范的 ID 规划后期联调会非常痛苦。7.2 建议使用 DBC 文件管理报文和信号当项目规模变大CAN 报文数量增多后直接靠代码里unsigned char data[8]管理信号很容易出错。推荐使用 DBCCAN Database文件来统一描述报文、信号和信号值范围再用工具如 CANdb、Vector CANoe、Kodiak自动生成代码或配置。DBC 文件里面可以定义每条报文用的哪个 CAN ID。报文的发送周期。数据场中每个信号的起始位、位长度、字节序Intel/Motorola、偏移量、缩放系数、物理范围。信号对应的值表比如 0x00 表示关闭0x01 表示打开。这样一来整车/整机联调时各模块只需要对照同一个 DBC 文件就能保证信号定义一致。很多车载总线面试题也会聊到 DBC这是从单片机裸机通信走向工程化通信的重要一步。7.3 错误处理与总线监控实际产品中不能只依赖控制器默认的错误重发机制。建议在应用层增加如下处理周期性检查 CAN 控制器的错误寄存器。监测发送失败次数如果连续失败达到阈值进入故障处理逻辑。捕获 Bus Off 事件后不要盲目立刻恢复可以根据产品需求设计恢复策略比如先等待一段时间再重新初始化。对于关键报文设计心跳机制。接收节点如果在规定时间内没有收到发送节点的心跳帧就认为对方离线。7.4 可维护性和测试工具想在生产环境中维护 CAN 总线网络仅靠万用表是不够的。下面几种工具对工程调试非常有帮助USBCAN 分析仪配合 PC 软件抓包、发送报文、模拟节点。逻辑分析仪观察 CAN 波形定位信号完整性问题。CAN 总线测试设备如 CANstress用于总线故障注入测试。软件工具Wireshark 配合 CAN 硬件可以解析高层协议。对于嵌入式 Linux 开发Linux 内核自带的 SocketCAN 也提供了完善的 CAN 应用接口使用can-utils工具集可以直接在命令行收发 CAN 报文。这部分内容适合后续单独开一讲本讲先把物理层和数据帧基础打牢。8. 总结与后续学习路线这一讲从 CAN 总线的物理层出发依次介绍了差分信号、终端电阻、波特率、数据帧结构、仲裁机制、位填充和错误处理并带着大家完成了 Arduino MCP2515 的双节点收发实验。相信你已经能够理解CAN 总线为什么用两根线就能完成多节点通信。为什么 ID 越小的报文优先级越高。为什么总线上至少需要两个节点才能正常收发。遇到错误帧和发送失败时应该从哪些方向去排查。下一步建议你按顺序做以下三件事把实验改成“按键发送”按下按键发送一帧松开再发送另一帧体会事件触发方式和周期发送方式的区别。在三块开发板之间组网给不同节点分配不同 ID观察总线仲裁过程。深入阅读 MCP2515 数据手册中关于验收过滤器、错误状态、中断标志位的部分这对你理解任何一款 CAN 控制器的寄存器都有帮助。如果你想继续往嵌入式方向深入可以接着学习更高层的 CANopen 协议、J1939 协议、ISO-TP 传输层以及 Linux 下的 SocketCAN 编程。CAN 总线只是一个通信管道真正工程化落地时还需要配合上层协议来约束数据格式、节点状态、诊断流程和网络管理。动手做实验永远比只看文章有效。建议你把这两个 Arduino 程序烧录进去亲自观察一遍send ok和接收端的数据打印然后故意拔掉一个终端电阻或者故意把波特率改成不一致再看看现象。这些踩坑经验比背诵帧结构更能帮你建立对 CAN 总线的直观理解。如果实验中遇到其他奇怪问题也欢迎带着报错信息一起交流。
返回列表