
上一讲我们聊了 UART、SPI、I2C 这些常见的板级通信协议这一讲进入嵌入式面试和实际项目里几乎必问的 CAN 总线协议。CAN 在汽车电子、工业控制、医疗设备、机器人里面大量出现很多入行嵌入式的人第一次接触多节点通信就是从 CAN 开始的。这一讲会直接围绕几个核心问题展开CAN 协议到底解决什么问题、报文格式怎么读、仲裁机制怎么理解、STM32 上怎么快速跑通收发、Linux 下怎么用 SocketCAN 抓包调试、遇到通信不稳定怎么排查。文章里的配置示例和代码都是可以直接照抄到工程里验证的针对的也是入门阶段最常踩的坑。1. CAN 核心能力速览能力项说明协议类型差分信号、多主从、总线式串行通信协议典型速率经典 CAN 最高 1 MbpsCAN FD 最高可达 5 Mbps 以上具体由控制器决定节点数量理论上受总线负载和收发器驱动能力限制常见几十个节点通信距离速率越低距离越长5 kbps 下可达千米级1 Mbps 下通常 40 m 左右错误处理硬件级 CRC 校验、错误帧、节点离线机制仲裁方式非破坏性位仲裁ID 越小优先级越高开发方式MCU 内置 CAN 控制器 外部收发器或 MCU SPI 转 CAN 模块调试工具逻辑分析仪、USB 转 CAN 分析仪、SocketCAN、PCAN 等适合场景车载网络、工业现场总线、运动控制、传感器网络、设备间长距离抗干扰通信从这张表可以看出CAN 不是用来替代 SPI/I2C 的它更擅长的是多节点、远距离、强干扰环境下的可靠通信。入门时不要只盯协议本身要从“为什么需要它”来理解。2. 适用场景与使用边界2.1 适合用 CAN 的场景CAN 首选用在多个控制器之间需要交换实时数据的场景。最典型的是汽车发动机 ECU、ABS、气囊、仪表盘、车窗控制模块分布在车身不同位置如果每两个模块都拉一组独立信号线线束会复杂到无法维护。CAN 总线用两根线把几十个节点串起来任何一个节点都能往总线上发数据所有节点都能同时收到这种一对多的数据分发能力是 UART 和 I2C 比不了的。工业控制里 CAN 同样常见比如伺服驱动器、变频器、PLC 之间的现场总线控制靠的就是 CAN 的抗干扰能力和短帧实时性。机器人领域里机械臂关节电机控制器之间的通信也大量使用 CAN。只要通信距离超过一两米、现场有电机或变频器这类干扰源、节点数量超过两个CAN 的优势就会非常明显。2.2 不适合用 CAN 的场景如果只是开发板上两个芯片之间短距离传数据SPI 和 I2C 更简单吞吐量也更高。CAN 的波特率上限在经典 CAN 里是 1 Mbps跟 SPI 动辄几十 Mbps 的速率差得很远。如果节点之间数据量很大比如要传音频或图像CAN 并不合适应该换以太网或者 USB。另外 CAN 总线需要所有节点约定好相同的波特率一旦某个节点波特率配错整个总线都会受到干扰严重时会导致所有节点报错。这个特性决定了 CAN 的调试门槛比 UART 高新手第一次上手时最容易在这个地方卡住。2.3 使用边界与工程合规提醒实际项目里使用 CAN 要注意以下几点CAN 报文内容本身没有加密和认证机制做安全敏感应用时必须在上层加鉴权和校验。不要随意向真实设备总线发送未经测试的报文尤其是车辆、医疗设备等场景一定要在隔离的测试环境里验证。抓包、监听、模拟节点等操作仅限自有设备和实验室环境不要对他人系统进行未授权操作。涉及产品的 EMC、布线和终端电阻设计要参考具体收发器芯片手册和行业规范。3. 环境准备与前置条件3.1 硬件准备学习 CAN 不需要一开始就买整车环境一套入门组合就够了一块带 CAN 控制器的 MCU 开发板比如 STM32F103、STM32F407、GD32、国民技术等常用型号。两个 CAN 收发器模块常见的是 TJA1050、SN65HVD230或者直接用集成收发器的开发板。两根杜邦线或者双绞线用于连接 CAN_H 和 CAN_L。两个 120 欧终端电阻用于总线两端匹配。一个 USB 转 CAN 分析仪推荐带 CANFD 功能的入门款方便用上位机观察报文。如果手头没有两块开发板也可以用一块开发板加一个 USB 转 CAN 分析仪来验证单节点发送或者直接使用开发板内部的回环模式。3.2 软件准备软件方面按开发路径选MCU 裸机或 RTOS 开发STM32CubeMX 或厂家 IDE配置 CAN 外设生成初始化代码。Linux 环境调试安装 SocketCAN 工具集包括can-utils内核一般已经自带 CAN 驱动框架。上位机分析安装 USB 转 CAN 分析仪自带的调试软件或者用开源工具如 Bus Master、PCAN-View 等。如果要直接用 Python 在 PC 上通过 USB 转 CAN 抓包分析可以安装python-can库后面会给出调用示例。3.3 前置知识学习 CAN 之前建议先把下面几个概念搞清楚串行通信的基本概念波特率、起始位、停止位、校验位。MCU 外设的基本配置流程时钟、引脚复用、中断或轮询。十六进制与二进制的转换因为 CAN 报文里的 ID、数据域都是按位操作的。简单了解差分信号的含义这关系到 CAN 为什么抗干扰。这些基础如果还不熟悉建议先回到 UART 和 GPIO 章节补一下再来读 CAN 会顺畅很多。4. CAN 协议基础原理解析4.1 CAN 的总线结构CAN 总线物理上只需要两根线通常称为 CAN_H 和 CAN_L。发送节点通过收发器把逻辑电平转换成差分电压接收节点再从差分电压还原逻辑电平。显性电平对应逻辑 0隐性电平对应逻辑 1。当总线上没有节点发送时总线处于隐性状态任意节点发送显性位时总线呈现显性状态。这个“显性优先于隐性”的机制是 CAN 仲裁的基础。总线两端必须各接一个 120 欧终端电阻用来消除信号反射。没有终端电阻的 CAN 网络在高速率下很容易出现波形畸变和通信错误。4.2 CAN 报文帧格式CAN 报文分为标准帧和扩展帧两种格式。标准帧使用 11 位标识符扩展帧使用 29 位标识符。实际项目里 11 位标准帧使用最普遍下面以标准数据帧为例看一帧报文包含哪些关键字段字段说明SOF帧起始一个显性位ID标识符标准帧 11 位决定优先级RTR远程帧标志数据帧为显性远程帧为隐性IDE标识符扩展位标准帧为显性DLC数据长度代码4 位表示数据域字节数0 到 8Data数据域0 到 8 字节CRC15 位循环冗余校验用于检测传输错误ACK应答位接收节点发送显性位确认EOF帧结束7 个隐性位一帧标准数据帧最多携带 8 字节数据。这个限制是刻意的短帧能保证单帧传输时间短、实时性高也让总线错误扩散的范围减小。CAN FD 把数据域扩展到最多 64 字节但那是后话入门阶段先把经典 CAN 的数据帧结构吃透。4.3 仲裁机制如果两个节点同时往总线上发送报文如何处理CAN 的仲裁机制是逐位比较 ID。每个节点发送时会同时监听总线电平如果自己发送的是隐性位1但总线上读到的是显性位0说明有更高优先级的节点正在发送这个节点立即停止发送转为接收状态。ID 越小优先级越高。因此实际工程里会把重要的报文分配小 ID比如发动机转速、刹车状态这类实时性要求高的信号ID 通常很小。非破坏性仲裁的意思是胜出的节点不需要重新开始发送直接继续发完剩余位整个过程没有时间浪费。4.4 错误处理机制CAN 控制器内置了强大的错误检测机制包括位错误、填充错误、CRC 错误、格式错误、应答错误。任何一个节点发现错误就会发送错误帧将当前报文作废。同时每个节点维护两个错误计数器接收错误计数和发送错误计数。计数超过阈值后节点会进入错误被动状态再严重会进入总线关闭状态彻底停止参与通信。这套机制保证了单个节点出问题不会一直拖垮整条总线但也会导致“某个节点配置错误 → 反复发错误帧 → 影响全部节点”的现象。入门阶段记住一点总线上出现大量错误帧先检查是不是有节点的波特率不匹配或者终端电阻缺失。5. STM32 上跑通 CAN 收发5.1 初始化配置以 STM32 系列为例推荐直接用 STM32CubeMX 生成初始化代码。配置步骤选择 MCU 型号使能 CAN1 外设。配置 CAN 引脚为复用功能通常为 PA11RX和 PA12TX。设置波特率例如 APB1 时钟 36 MHz预分频 4同步跳转宽度 1时间段1为 7时间段2为 6可以得到 500 kbps。使能 CAN 接收中断。生成工程后在用户代码区添加发送和接收逻辑。CubeMX 生成的初始化函数大致如下void MX_CAN1_Init(void) { CAN_FilterTypeDef can_filter_init {0}; hcan1.Instance CAN1; hcan1.Init.Prescaler 4; hcan1.Init.Mode CAN_MODE_NORMAL; hcan1.Init.SyncJumpWidth CAN_SJW_1TQ; hcan1.Init.TimeSeg1 CAN_BS1_7TQ; hcan1.Init.TimeSeg2 CAN_BS2_6TQ; hcan1.Init.TimeTriggeredMode DISABLE; hcan1.Init.AutoBusOff ENABLE; 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(); } can_filter_init.FilterBank 0; can_filter_init.FilterMode CAN_FILTERMODE_IDMASK; can_filter_init.FilterScale CAN_FILTERSCALE_32BIT; can_filter_init.FilterIdHigh 0x0000; can_filter_init.FilterIdLow 0x0000; can_filter_init.FilterMaskIdHigh 0x0000; can_filter_init.FilterMaskIdLow 0x0000; can_filter_init.FilterFIFOAssignment CAN_RX_FIFO0; can_filter_init.FilterActivation ENABLE; if (HAL_CAN_ConfigFilter(hcan1, can_filter_init) ! HAL_OK) { Error_Handler(); } if (HAL_CAN_Start(hcan1) ! HAL_OK) { Error_Handler(); } if (HAL_CAN_ActivateNotification(hcan1, CAN_IT_RX_FIFO0_MSG_PENDING) ! HAL_OK) { Error_Handler(); } }滤波器设置为全接收模式ID Mask 全部为 0表示不过滤任何报文。实际项目里要根据需要设置滤波器只接收感兴趣的 ID减少 CPU 中断负担。5.2 发送数据CAN 发送的流程是填充发送报文结构体调用 HAL_CAN_AddTxMessage。发送函数可以做成一个通用接口方便上层调用。uint8_t Can_SendMessage(uint32_t id, uint8_t *data, uint8_t len) { CAN_TxHeaderTypeDef tx_header; uint32_t tx_mailbox 0; tx_header.ExtId 0; tx_header.IDE CAN_ID_STD; tx_header.RTR CAN_RTR_DATA; tx_header.DLC len; if (id 0x7FF) { tx_header.IDE CAN_ID_EXT; tx_header.ExtId id; } else { tx_header.StdId id; } return HAL_CAN_AddTxMessage(hcan1, tx_header, data, tx_mailbox); }调用示例uint8_t test_data[8] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}; Can_SendMessage(0x123, test_data, 8);发送后可以检查返回值返回 HAL_OK 表示成功加入发送邮箱。CAN 控制器发送完成后会产生发送邮箱空中断也可以在 HAL_CAN_TxMailbox0CompleteCallback 回调里做后续处理。5.3 接收数据接收使用中断方式在回调函数里读取报文。CubeMX 生成的启动代码已经使能了接收中断这里只需要实现回调。void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { CAN_RxHeaderTypeDef rx_header; uint8_t rx_data[8] {0}; if (hcan-Instance CAN1) { if (HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, rx_header, rx_data) HAL_OK) { // 这里处理收到的数据 // rx_header.StdId 是报文 ID // rx_header.DLC 是数据长度 // rx_data 是数据内容 } } }接收回调里不要做耗时操作比如打印日志、驱动显示屏这些都应该把数据拷贝到缓冲区然后交给主循环或者 RTOS 任务处理。中断里耗时过长会丢帧。5.4 回环测试验证回环模式是入门验证 CAN 控制器最好的方式。把初始化里的 Mode 改成 CAN_MODE_LOOPBACK发送的报文会自动回到接收缓冲区不需要连接外部节点。hcan1.Init.Mode CAN_MODE_LOOPBACK;配置成回环模式后调用发送函数发一帧数据然后在接收回调里打断点或者串口打印。能收到自己发的报文说明 CAN 控制器的时钟配置、引脚复用、中断配置都正确。回环通过后再切换到正常模式连接两块板子或者连接 USB 分析仪做双节点测试。6. Linux 下用 SocketCAN 调试 CAN 总线6.1 启用 SocketCAN 接口嵌入式 Linux 开发板上通常会有多个 CAN 接口比如can0、can1。使用前需要先配置波特率并启动接口# 设置 CAN0 波特率为 500k sudo ip link set can0 type can bitrate 500000 # 启动 can0 接口 sudo ip link set can0 up关闭接口的命令是sudo ip link set can0 down如果调试期间需要反复配置可以把命令写成一个脚本。需要注意配置波特率前必须先关闭接口否则会报错。6.2 使用 can-utils 抓包Linux 下最常用的 CAN 调试工具是can-utils。安装方式根据发行版不同略有差异Debian/Ubuntu 系统sudo apt install can-utils安装完成后抓包可以使用candumpcandump can0设备上连接另一块板子发送一帧 ID 为 0x123、数据为 01 02 03 04 05 06 07 08 的报文终端会输出类似下面这样的信息can0 123 [8] 01 02 03 04 05 06 07 08如果总线上有错误帧会看到(error frame)的标记。CAN FD 报文在 can-utils 里也能识别经典的 candump 工具支持-d参数区分 CAN 和 CAN FD 格式。发送测试报文使用cansendcansend can0 123#0102030405060708如果只想周期性发送某个报文可以配合cangen# 每隔 100ms 发送一次随机报文 cangen can0 -i 100 -g 100cangen的-i是报文 ID-g是发送间隔毫秒数。这个工具在做总线压力测试时很好用。6.3 查看 can0 统计信息调试时经常需要确认总线上收发了多少帧、有没有错误。用ip -details -statistics link show can0查看详细统计ip -details -statistics link show can0输出会包含RX和TX的帧计数、错误计数、状态信息。如果bus-off计数不断增加说明总线错误严重需要优先排查物理层和波特率。7. 上位机接口与批量收发示例7.1 通过 USB 转 CAN 分析仪调试USB 转 CAN 分析仪在入门调试里非常实用它相当于把开发板的 CAN 总线接到电脑上。不同品牌的设备使用的上位机软件和驱动不同但基本操作流程一致安装设备驱动连接 USB。打开上位机软件选择设备类型。设置波特率必须与开发板一致。打开设备开始监听总线上所有报文。在软件里可以直接发送指定 ID 和数据验证开发板的接收逻辑。连上分析仪后开发板发送的每一帧报文都能在电脑上看到这是排查“板子到底有没有发出来”的最快方式。如果分析仪能收到但另一块开发板收不到基本可以排除发送端问题重点查接收端的滤波器、波特率和中断配置。7.2 用 python-can 做批量收发python-can是 Python 生态里最常用的 CAN 库配合 USB 转 CAN 分析仪可以直接在 PC 上写脚本处理报文。安装pip install python-can读取总线上所有报文打印 ID 和数据import can bus can.interface.Bus(channelcan0, interfacesocketcan, bitrate500000) for msg in bus: print(fID: {msg.arbitration_id:03X} DLC: {msg.dlc} Data: {msg.data.hex( )})批量发送一组报文import can import time bus can.interface.Bus(channelcan0, interfacesocketcan, bitrate500000) messages [ {id: 0x100, data: [0x01, 0x02, 0x03, 0x04]}, {id: 0x200, data: [0x10, 0x20, 0x30, 0x40, 0x50]}, {id: 0x300, data: [0xAA, 0xBB]}, ] for m in messages: msg can.Message( arbitration_idm[id], datam[data], is_extended_idFalse ) bus.send(msg) print(fSent: 0x{m[id]:03X}) time.sleep(0.01)如果是用 USB 转 CAN 设备interface 参数需要改成对应设备的驱动名称比如pcan、canalystii等。具体以设备驱动文档为准。7.3 批量任务设计建议做批量收发测试时不要把所有报文一次性灌到总线上否则可能因为总线繁忙导致发送失败。建议批量发送加上间隔控制并记录发送成功和失败的帧数。import can import time bus can.interface.Bus(channelcan0, interfacesocketcan) success 0 failed 0 for i in range(100): msg can.Message( arbitration_id0x123, data[i, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], is_extended_idFalse ) try: bus.send(msg) success 1 except can.CanError: failed 1 time.sleep(0.005) print(fSuccess: {success}, Failed: {failed})发送频率过高时SocketCAN 底层会缓存一部分报文但应用层循环快于总线带宽时依然会报错。实际项目中需要根据波特率和报文长度计算总线负载率避免无脑高频发送。8. 资源占用与性能观察8.1 波特率与位时间CAN 的通信速率由波特率决定常见的波特率有 125 kbps、250 kbps、500 kbps、1 Mbps。波特率不是越高越好高速率下对总线长度、终端电阻、线缆质量的要求都会提高。每个波特率对应的位时间1 秒除以波特率必须满足 CAN 控制器的时序要求。STM32 的位时间由预分频、时间段1、时间段2、同步跳转宽度共同决定配置的核心目标是让采样点落在位时间的 70% 到 80% 左右。CubeMX 会自动计算但手动配置时要注意这个原则。8.2 总线负载率总线负载率是指实际数据占用总线时间的比例。计算方法总线负载率 所有帧传输时间总和 / 观测时间以 500 kbps、标准帧 8 字节数据为例一帧数据大约占用约 129 位时间包含填充位等。总线上如果有 10 个节点每个节点每 10 ms 发一帧粗略估算负载率就是10 帧 × 129 位 / 0.01 秒 / 500000 位每秒 ≈ 2.58%这个负载率很低CAN 完全没问题。但如果每个节点 1 ms 发一帧负载率会飙升到 25.8%再加上错误帧重传总线可能接近饱和。设计时建议预留至少 30% 到 50% 的余量。8.3 如何观察资源占用嵌入式侧观察 CAN 资源占用主要看发送邮箱状态HAL_CAN_GetTxMailboxesFreeLevel 可以查询空闲邮箱数量。中断频率接收中断次数和主循环处理能力的匹配度。CPU 占用轮询接收模式和中断接收模式的 CPU 占用差异。错误计数通过 CAN 控制器的错误寄存器读取。Linux 侧重点看ip -statistics link show can0的统计信息如果RX overrun持续增加说明应用层读取速度跟不上内核接收速度。9. 常见问题与排查方法问题现象可能原因排查方式解决方案总线上一帧都收不到波特率不一致用分析仪确认节点配置统一所有节点波特率能发不能收滤波器配置错误检查滤波器 ID/Mask滤波器改为全接收测试通信时好时坏缺少终端电阻检查总线两端 120 欧电阻两端各接一个大量错误帧波特率不匹配或物理层干扰candump 看 error frame检查节点配置和布线发送返回超时总线忙或邮箱满查发送返回值增加发送重试和延时中断里收不到数据中断未使能或优先级低检查 NVIC 配置使能 CAN 接收中断CAN 控制器进入 Bus Off错误帧过多读错误计数器检查总线短路和波特率回环模式正常双节点失败收发器接线错误检查 CAN_H/CAN_L 是否正确连接CAN_H 接 CAN_HCAN_L 接 CAN_L分析仪连不上驱动或端口占用换 USB 口重装驱动重启上位机软件第一条“波特率不一致”是入门阶段最常见的问题。CAN 不像 UART 会自动适配波特率所有节点的位时间必须一致否则接收端采样的位置完全错位必然导致错误帧。排查时先确认每个节点的配置是否相同。第二条“滤波器配置错误”也很隐蔽。有些开发板例程默认滤波器是屏蔽所有报文新手改配置后忘了改回来导致总线上明明有数据接收中断就是进不去。在 debug 时把滤波器设为 Mask 全 0、ID 全 0可以快速排除滤波器问题。10. 最佳实践与使用建议10.1 通信设计建议报文 ID 要有规划。给不同功能模块分配 ID 段比如动力系统 0x100-0x1FF、车身系统 0x200-0x2FF便于维护和问题定位。周期报文和事件报文分开设计。周期性上报状态的报文用固定周期发送紧急事件用低 ID 报文优先抢占总线。预留诊断报文空间。量产项目里诊断是刚需设计阶段就要留出一段 ID 给 UDS 或私有诊断协议。控制报文和状态报文不要混在一个 ID 里。发送方和接收方的关注点不同混在一起会降低可读性也容易在扩展功能时破坏兼容性。10.2 代码工程建议封装统一的 CAN 发送和接收接口不要在业务代码里直接操作 HAL 函数。接收数据用环形缓冲区中断只负责写入主循环或任务负责解析。发送失败要处理检查邮箱状态、重试机制、记录错误日志。配置参数集中管理波特率、滤波器、ID 定义放在独立头文件里。日志里同时记录 ID、DLC、数据内容和时间戳便于回放现场。10.3 硬件调试建议开发阶段就在 CAN_H 和 CAN_L 之间并联一个 120 欧电阻模拟单节点终端电阻防止一端悬空导致信号反射。使用双绞线连接 CAN_H 和 CAN_L不要用普通飞线长距离走线。收发器电源要稳定CAN 收发器对电源噪声比较敏感电源纹波大会导致总线错误率上升。测试时先短距离、低速率跑通再逐步提高速率和距离不要一开始就 1 Mbps 拉几十米线。10.4 合规与授权提醒调试 CAN 总线时抓包和模拟发送都只能针对自己搭建的测试环境和自有设备。汽车、医疗、工业现场等领域的实车或在用设备任何未经授权的总线访问都可能带来安全风险务必确认测试边界。11. 总结与下一步CAN 协议入门的关键不是背帧格式而是理解它为什么这样设计短帧是为了实时性差分信号是为了抗干扰非破坏性仲裁是为了多节点竞争时不浪费时间丰富的错误机制是为了在恶劣电磁环境下稳定工作。这些设计思路在后面的 CAN FD、CANopen、J1939、UDS 里都会延续。这一讲的内容按步骤操作一遍你应该能完成三件事第一在 STM32 上跑通 CAN 回环收发第二在 Linux 下用 SocketCAN 抓包并发送报文第三遇到通信失败时能够按照“波特率 → 终端电阻 → 滤波器 → 错误帧”的顺序排查问题。建议先收藏这篇文章等开发板和 USB 转 CAN 分析仪到手后照着做一遍比只看不练效果好得多。下一步可以继续学习 CAN FD 与经典 CAN 的差异了解 CANopen 协议栈的节点状态机和对象字典也可以研究一下 UDS 诊断协议的基础服务比如 0x10 会话控制、0x22 读取数据、0x2E 写入数据。这些在车载嵌入式岗位的笔试和面试里出现频率很高也是从 CAN 入门走向实际项目的重要跳板。