← 返回博客列表

一、什么是 CAN 总线

CAN(Controller Area Network,控制器局域网)是一种串行、异步、多主、差分信号的现场总线。它由 Bosch 公司 1986 年为汽车电子设计,目的是用一根总线代替汽车里成百上千根点对点线束,让发动机、变速箱、仪表盘、ABS 等控制器共享一条通信"公路"。

CAN 总线出现前 vs 出现后 传统线束(点对点) ECU1 ECU2 ECU3 ECU4 ECU5 ECU6 N 个节点 → N(N-1)/2 条线 CAN 总线 ECU1 ECU2 ECU3 ..N ECU4 ECU5 ECU6 ..N N 个节点 → 只需 1 条总线(两根线)
CAN 总线将 N(N-1)/2 条线简化为 2 根线

1.1 CAN 的核心特征

一句话理解 CAN
CAN 是一条"带优先级的多车道广播公路":所有车(节点)都能上路,堵车时优先级高的车先过,低的自动让道;路边所有房子都能听到广播,自己决定要不要理会。

1.2 CAN vs 传统串口:三个维度的代差

维度RS-232 / RS-485CAN
通信方式RS-232 点对点;RS-485 主从轮询多主竞争 + 非破坏性仲裁
可靠性校验靠软件(如 CRC16 in Modbus)硬件层 CRC15 + 位填充 + ACK
实时性RS-485 轮询延迟随节点数线性增长最高优先级消息延迟恒定且极小
典型场景工业仪表、低速采集汽车电子、工业控制、医疗

二、CAN 发展简史与版本

CAN 发展时间线 1986 Bosch 提出 CAN 概念发布 1991 CAN 2.0A/B 规范 11/29 位 ID 1993 ISO 11898 标准 高速 CAN 2003 ISO 11898-2 高速 CAN 标准 2015 CAN FD 标准 ISO 11898-1:2015 2019+ CAN XL 下一代 从汽车电子到工业 / 医疗 / 航空,CAN 持续进化
CAN 协议发展演进图

2.1 CAN 三个主要版本

版本发布标志关键特性
CAN 2.0A199111 位 ID标准帧,2048 个 ID,8 字节数据,1 Mbps
CAN 2.0B199129 位 ID扩展帧,5 亿+ ID,与 2.0A 兼容
CAN FD2015ISO 11898-1:2015数据段可达 64 字节,数据段位速率 5~8 Mbps
CAN XL2019+研发中帧长 2048 字节,融入 10BASE-T1S 以太网
最常用:CAN 2.0A/B
目前绝大多数汽车和工业应用仍使用经典 CAN 2.0A(11 位 ID)。CAN FD 在新款车型和高带宽工业场景中快速普及。

三、物理层:差分信号与总线拓扑

3.1 CAN_H 与 CAN_L:差分双线

CAN 使用两根线:CAN_H(高)和 CAN_L(低)。与 RS-485 一样,信号靠"两线电压差"判断,而非绝对电平。

CAN 显性 vs 隐性电平 显性 Dominant(逻辑 0) CAN_H CAN_L CAN_H ≈ 3.5V, CAN_L ≈ 1.5V, 差分 ≈ 2V 隐性 Recessive(逻辑 1) CAN_H CAN_L CAN_H ≈ 2.5V, CAN_L ≈ 2.5V, 差分 ≈ 0V
CAN 采用"线与"机制:显性覆盖隐性,仲裁得以实现
"线与"是 CAN 的灵魂
显性位(0)会覆盖隐性位(1)。这意味着多个节点同时发,只要有任何一个发 0,总线上就是 0 —— 这正是非破坏性仲裁的物理基础。

3.2 总线拓扑:菊花链 + 两端终端电阻

CAN 总线物理拓扑 H L 节点1 节点2 节点3 节点N 120Ω 120Ω 终端电阻 终端电阻 总线两端各接一个 120Ω 终端电阻,总阻抗 ≈ 60Ω 节点通过"分支"接入总线,分支长度应 < 0.3m(高速)或 < 3m(低速) 总线最大长度随速率下降而增加:1Mbps→40m / 500kbps→100m / 125kbps→500m / 50kbps→1km
CAN 总线物理拓扑与终端电阻
终端电阻为什么是 120Ω?
CAN 双绞线特征阻抗约 120Ω。两端各接 120Ω 并联后总阻抗 60Ω,与线缆匹配。信号到末端被吸收,不反射。如果只接一端或不接,信号会反射造成"振铃",误码率飙升。终端电阻位置必须在总线物理最远的两端,不能放在中间。

3.3 收发器:CAN 控制器与物理总线的桥梁

CAN 节点内部结构 MCU CPU + 应用程序 CAN 控制器 CAN 收发器 TJA1050/SN65HVD230 TX/RX CAN_H CAN_L CAN 总线(双绞线) 节点 节点 节点 CAN 控制器(数字逻辑)→ CAN 收发器(电平转换)→ 物理总线 常见集成方案:MCU 内置 CAN 控制器(如 STM32)+ 外置收发器芯片
CAN 节点分层架构

常见收发器芯片对比

型号厂商供电特点
TJA1050NXP5V经典 5V 高速收发器,汽车级
SN65HVD230TI3.3V3.3V 低功耗,工业常用
MCP2551Microchip5V经典 5V,性价比高
ISO1042TI5V隔离收发器,5000V AC 隔离

四、CAN 帧结构详解

CAN 有 4 种帧类型:数据帧(最常用)、远程帧(请求数据)、错误帧(报错)、过载帧(请求延迟)。这里重点讲数据帧。

CAN 标准数据帧结构(11 位 ID) SOF1 bit Arbitration11 bit ID + RTR ControlIDE+r0+DLC Data0~8 bytes CRC15 bit + 界定符 ACK2 bit EOF7 bit IFS3 bit 空闲 一帧(不含位填充,最短 44 bit / 最长 111 bit) SOF 帧起始 1个显性位 同步用 仲裁段 11位ID + RTR ID决定优先级 同时发: 小ID胜 控制段 IDE + r0 + DLC DLC: 数据长度 0~8字节 数据段 0~8字节 有效载荷 应用层定义 CRC段 15位CRC +1位界定符 硬件自动计算 ACK段 ACK槽+界定 接收方回显性 确认收到 EOF 7个隐性位 帧结束标记 IFS 帧间间隔 3个隐性位 位填充:连续 5 个同电平位后自动插入 1 个反相位(防止丢失同步) 扩展帧(29位 ID)的仲裁段更长,结构相同
CAN 标准数据帧各字段详解

4.1 各字段含义

字段位数含义
SOF1帧起始,1 个显性位,用于硬同步
Arbitration12(标准)/ 32(扩展)11/29 位 ID + RTR + IDE(扩展标识)
Control6IDE + r0(保留)+ 4 位 DLC(数据长度码 0~8)
Data0~64有效数据,长度由 DLC 指定
CRC1615 位 CRC 序列 + 1 位 CRC 界定符(隐性)
ACK2ACK 槽(发送方发隐性,接收方覆写显性)+ 界定符
EOF77 个连续隐性位,标志帧结束
IFS3帧间间隔,3 个隐性位,给控制器恢复时间
ACK 段是 CAN 的精髓之一
发送方发完 CRC 后,会发一个"隐性"的 ACK 槽。如果总线有任何一个节点正确接收,它会把这个位"拉成显性"。发送方检测到显性位 = 至少一个节点收到。如果没有节点回 ACK → 发送方报 ACK Error → 重发。

4.2 远程帧:请求数据的"礼貌问法"

远程帧(Remote Frame)结构同数据帧,但 RTR = 隐性(1),且数据段长度为 0。它表示:"有谁能发 ID=xxx 的数据给我?"

数据帧 vs 远程帧 SOF ID(11) RTR=0 Control Data(0~8B) CRC ACK+EOF 数据帧:RTR=0,有数据 SOF ID(11) RTR=1 Control 无数据 CRC ACK+EOF 远程帧:RTR=1,请求数据
数据帧与远程帧的唯一差异:RTR 位和数据段

五、仲裁机制:越小的 ID 越优先

当多个节点同时开始发送,CAN 会在仲裁段(ID + RTR)逐位比较,ID 数值小的(高位先出现 0 的)获胜。败者立即切到接收模式,等总线空闲后再重发。

非破坏性仲裁过程示例 节点A ID=0x123 节点B ID=0x345 总线实际 bit A 发 B 发 线上 说明 10 0 0 0 都发0,相同 9 0 0 0 相同 8 0 1 0 A 的 0 覆盖 B 的 1 7 - - A续 B 检测到冲突,停止发送 6~0 - - A续 A 独占总线 0x123 < 0x345 → A 胜出,B 的数据不丢(稍后重发) 关键:仲裁发生在 ID 的逐位比较,显性(0)"吃掉"隐性(1) 败者检测到自己发 1 但线上是 0 → 知道有人优先级更高 → 立即退让
非破坏性仲裁过程:A(ID=0x123) 与 B(ID=0x345) 同时发送
非破坏性仲裁的工程价值
① 无数据丢失:胜者完整发完,败者稍后重发
② 实时性可预测:最高优先级 ID 的最坏延迟 = 一帧最长传输时间(约 111 bit × 1us/bit = 111us @ 1Mbps)
③ 无需中央调度:各节点自治,无主节点单点故障

5.1 ID 分配原则(工程经验)

六、错误处理与故障 confinement

CAN 有5 种错误类型和3 种错误状态,让故障节点自动"降级"甚至"离线",避免拖垮整个总线。

6.1 五种错误类型

错误类型触发条件检测机制
位错误 Bit Error发送的位 ≠ 总线实际位回读自己发送的位
填充错误 Stuff Error连续 6 个同电平位(应 5 位后填充)位填充规则检查
CRC 错误CRC 计算值 ≠ 接收值多项式校验
格式错误 Form Error固定格式位(如 EOF)电平不符帧格式检查
ACK 错误发送方未收到 ACK 显性位ACK 槽回读

6.2 三种节点错误状态

CAN 节点错误状态机 主动错误 Error Active TEC < 128 / REC < 128 正常收发,发主动错误帧 被动错误 Error Passive 127 < TEC/REC < 256 仍可收发,发被动错误帧 总线关闭 Bus Off TEC ≥ 256 完全离线,不再收发 错误累积 错误超限 错误恢复 128 × 11 个隐性位后 总线空闲 128 × 11 位后恢复 故障节点自动降级 → 严重故障自动离线,保护总线不被拖垮
CAN 错误状态机与自动恢复

6.3 错误计数器

每个节点有两个 8 位计数器:TEC(发送错误计数器)和 REC(接收错误计数器)。

Bus-Off 是救命设计
如果一个节点硬件故障,持续发错误帧,CAN 会让它在 TEC 达到 255 后自动"闭嘴"(Bus-Off),从总线上离线,其他节点仍正常工作。这避免了"一颗老鼠坏一锅汤"。

七、CAN 能力矩阵:速率/距离/节点

7.1 速率与距离的反比关系

CAN 位速率 vs 总线长度 总线长度(米) 位速率 40 100 500 1000 5000 1M 500k 250k 125k 50k 1Mbps/40m 500kbps/100m 250kbps/250m 125kbps/1km 50kbps/5km 速率 × 长度 ≈ 常数(约 40 Mbps·m)
CAN 位速率与总线长度的反比关系

7.2 CAN 综合能力表

维度经典 CAN 2.0CAN FD
仲裁段速率1 Mbps1 Mbps(兼容)
数据段速率1 Mbps5~8 Mbps
数据长度0~8 字节0~64 字节
最大节点30(标准)/ 64(扩展)同左
最大总线长度1000m @ 50kbps同左
错误检测CRC15 + 位填充 + ACKCRC17/21 + 固定位填充
ID 长度11 位(标准)/ 29 位(扩展)同左
位填充每 5 同电平位插入 1 反相位固定 4 位后填充(数据段)

八、CAN FD:经典 CAN 的带宽升级

CAN FD(Flexible Data-rate)是 CAN 的自然进化:仲裁段保持 1 Mbps(兼容性),数据段加速到 5~8 Mbps,数据长度从 8 字节扩到 64 字节。

CAN 2.0 vs CAN FD 帧对比 CAN 2.0(全程 1 Mbps) SOF 仲裁(12b) 控制 数据(8B) CRC(16b) ACK EOF 最短 ≈ 44 bit @ 1Mbps ≈ 44us CAN FD(仲裁 1Mbps / 数据 5Mbps) SOF 仲裁(12b) 控制 数据(64B) 5 Mbps 加速! CRC(20b) ACK EOF 64B @ 5Mbps ≈ 100us
CAN FD 的双速率传输机制
CAN FD 三大升级
① 更长数据:8 → 64 字节,减少拆包开销
② 更高带宽:数据段 5~8 Mbps,总吞吐 5~8 倍提升
③ 更强 CRC:CRC17/21,覆盖 64 字节无盲区

九、上层应用协议全景

CAN 本身只是"运输公司"(提供可靠传输),但没规定"运什么货"。上层协议定义了消息含义、ID 分配、参数组、网络管理等。

CAN 协议栈分层 应用层 CANopen | J1939 | OBD-II | NMEA 2000 | DeviceNet 数据链路层 CAN 2.0 / CAN FD 物理层 ISO 11898-2(高速)/ -3(低速) 传输介质 双绞线 / 光纤 / 无线 CAN 只定义了物理层 + 数据链路层,上层协议由各行业自行定义 这就是为什么同一个 CAN 总线,汽车跑 J1939,工业跑 CANopen,船用跑 NMEA 2000
CAN 协议栈与上层应用协议关系

9.1 CANopen:工业领域的瑞士军刀

CANopen由 CiA(CAN in Automation)组织制定,是工业自动化最流行的 CAN 上层协议。核心概念:

CANopen 通信模型 NMT Master Node 1 Node 2 Node 3 Node N NMT 主→从 状态控制 PDO 广播过程数据 SDO 点对点配置 SYNC 同步触发
CANopen 四大通信对象

9.2 SAE J1939:商用车/工程机械标准

J1939是 SAE 协会为商用车(卡车、客车、挖掘机)制定的 CAN 上层协议。基于 29 位扩展 ID。

J1939 的 29 位 CAN ID 结构 优先级3 bit 保留1b PGN (18 bit)参数组号 PDU 格式8 bit PS8 bit 源地址8 bit 29 位 ID = 优先级(3) + 保留(1) + DP(1) + PF(8) + PS(8) + SA(8) PGN = 优先级剥离后的 18 位(DP + PF + PS 或 PF + 0) 示例:EEC1(发动机转速)PGN=61444,周期 50ms
J1939 CAN ID 的 29 位字段划分

9.3 OBD-II:汽车诊断协议

OBD-II(On-Board Diagnostics II)是汽车排放诊断标准,通过 CAN 总线读取 ECU 数据。

OBD-II 查发动机转速实例
请求帧:ID=0x7DF, Data=[02 01 0C 00 00 00 00 00](2 字节有效:模式 01 + PID 0C)
响应帧:ID=0x7E8, Data=[04 41 0C 1A 28 00 00 00](4 字节有效:04 长度 + 41 模式响应 + 0C PID + 1A 28 数据)
RPM = (0x1A × 256 + 0x28) / 4 = 1704 RPM

9.4 其他上层协议速览

协议领域特点
DeviceNet工业自动化罗克韦尔主导,主从模型,基于 CAN 2.0B
NMEA 2000船舶航海基于 CANopen,即插即用设备网络
CCP/XCPECU 标定标定与测量,实时数据采集
iSAFE功能安全SIL2/3 安全通信扩展
EnergyBus电动车辆电池/电机能源管理

十、CANoe:Vector 仿真利器

CANoe是德国 Vector Informatik 公司的旗舰工具,是汽车电子行业事实标准的网络分析、仿真、测试平台。

10.1 CANoe 能做什么

CANoe 在开发流程中的位置 需求分析 规范定义 仿真模型 CANoe + CAPL 集成测试 HIL 硬件在环 实车测试 路测记录 量产 CANoe 贯穿全流程 网络规范设计 残余总线仿真 HIL 实时仿真 路测数据分析 售后诊断 从设计到量产,CANoe 是"全能工具人" 支持 CAN / CAN FD / LIN / FlexRay / 车载以太网
CANoe 在 V 型开发流程中的全流程覆盖

10.2 CAPL:CANoe 的编程语言

CAPL(Communication Access Programming Language)是 CANoe 专用脚本语言,类 C 语法,事件驱动。

/* CAPL 示例:模拟发动机 ECU 周期发送转速信号 */
variables {
    msTimer tmrEngineData;
    int engineRpm = 800;     // 怠速 800 RPM
    float coolantTemp = 85.5; // 水温 85.5°C
    message EngineMsg msgEngine;     // 引用 DBC 中定义的报文
}

on start() {
    setTimer(tmrEngineData, 50); // 50ms 周期
}

on timer tmrEngineData {
    engineRpm += (int)random(100) - 50; // 模拟波动
    if (engineRpm < 600) engineRpm = 600;
    if (engineRpm > 3000) engineRpm = 3000;

    msgEngine.EngineSpeed = engineRpm;           // 写入信号值
    msgEngine.CoolantTemp = (int)(coolantTemp + 0.5);
    output(msgEngine);                          // 发送到总线

    setTimer(tmrEngineData, 50);                // 重启定时器
}

on message BrakeMsg msgBrake {
    if (msgBrake.BrakePressed == 1) {
        write("Brake pressed! Reducing RPM");
        engineRpm = 800; // 模拟刹车降速
    }
}

10.3 DBC 文件:信号定义的"字典"

DBC(Data Base CAN)文件是 Vector 定义的 CAN 网络描述文件,定义了报文、信号、节点、缩放等。

// DBC 文件片段示例
VERSION ""

NS_ :
    NS_DESC_
    CM_
    BA_DEF_
    BA_
    VAL_
    ...

BU_: Engine Brake Instrument Gateway

BO_ 61444 EngineMsg: 8 Engine
   SG_ EngineSpeed : 0|16@1+ (0.125,0) [0|8000] "rpm" Engine,Instrument
   SG_ CoolantTemp : 16|8@1+ (1,-40) [-40|215] "degC" Engine,Instrument
   SG_ EngineStatus : 24|2@1+ (1,0) [0|3] "" Engine

BO_ 600 BrakeMsg: 2 Brake
   SG_ BrakePressed : 0|1@1+ (1,0) [0|1] "" Engine,Instrument
DBC 三个核心字段解读
BO_ = Message(报文):ID + 长度 + 发送节点
SG_ = Signal(信号):起始位 | 长度 @字节序+符号 缩放(factor,offset) [最小|最大] "单位" 接收节点
BU_ = Node(节点):网络中所有 ECU

10.4 CANoe 工作流程

  1. 导入 DBC:File → Import → CANdb++,加载信号定义
  2. 配置通道:Configuration → Channel Usage,映射硬件通道
  3. 编写 CAPL:在 CAPL Browser 中编写节点仿真逻辑
  4. 设计面板:Panel Editor 拖拽控件,绑定信号
  5. 开始测量:Start,实时观察 Trace 窗口、Graphics 窗口、Data 窗口
  6. 分析数据:离线回放、统计、过滤、生成报告
CANoe 界面布局示意 主工作区 Trace 窗口(报文列表) Time ID Name Dir DLC Data 12.345 0x123 EngineMsg Rx 8 01 0C 1A 28... 12.395 0x258 BrakeMsg Tx 2 01 00 Graphics 窗口(信号曲线) EngineSpeed (rpm) Panel(交互面板) 转速表 1850 水温 87°C BRAKE Start Stop 模拟控制台
CANoe 典型工作界面:Trace + Graphics + Panel

十一、代码实战:Linux SocketCAN + Qt/C++

Linux 内置 SocketCAN 框架,把 CAN 总线抽象成网络套接字,用标准 socket API 编程。下面用 Qt/C++ 封装一个 CAN 通信类。

11.1 Linux 环境准备

# 1. 加载虚拟 CAN 控制器(用于无硬件测试)
sudo modprobe vcan
sudo ip link add dev can0 type vcan
sudo ip link set up can0

# 2. 真实硬件:设置比特率并启动
sudo ip link set can0 type can bitrate 500000
sudo ip link set can0 up

# 3. 安装 can-utils(命令行工具)
sudo apt install can-utils

# 4. 抓包测试
candump can0            # 持续监听
cansend can0 123#0102030405060708  # 发送一帧

11.2 Qt/C++ CAN 通信类

// CanBus.h —— SocketCAN Qt 封装
#ifndef CANBUS_H
#define CANBUS_H

#include <QObject>
#include <QByteArray>
#include <QSocketNotifier>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <sys/socket.h>
#include <net/if.h>

struct CanFrame {
    quint32 id;
    bool extended;      // 29位扩展帧
    bool rtr;           // 远程帧
    bool fdFrame;       // CAN FD 帧
    QByteArray data;    // 数据段
};

class CanBus : public QObject {
    Q_OBJECT
public:
    explicit CanBus(QObject *parent = nullptr);
    ~CanBus();

    // 打开 CAN 设备(如 "can0"),返回是否成功
    bool open(const QString &ifName);
    void close();

    // 发送一帧
    bool send(const CanFrame &frame);

    // 设置接收过滤器(只收指定 ID)
    void setFilter(const QVector<quint32> &ids);

signals:
    void frameReceived(const CanFrame &frame);
    void errorOccurred(const QString &msg);

private slots:
    void onReadyRead();

private:
    int m_sock = -1;
    QSocketNotifier *m_notifier = nullptr;
};

#endif // CANBUS_H
// CanBus.cpp —— 实现
#include "CanBus.h"
#include <QDebug>
#include <unistd.h>
#include <sys/ioctl.h>

CanBus::CanBus(QObject *parent) : QObject(parent) {}

CanBus::~CanBus() { close(); }

bool CanBus::open(const QString &ifName) {
    // 1. 创建 RAW CAN 套接字
    m_sock = socket(PF_CAN, SOCK_RAW, CAN_RAW);
    if (m_sock < 0) {
        emit errorOccurred("无法创建 CAN socket");
        return false;
    }

    // 2. 指定网络接口(如 can0)
    struct ifreq ifr;
    std::strncpy(ifr.ifr_name, ifName.toUtf8().constData(), sizeof(ifr.ifr_name));
    ioctl(m_sock, SIOCGIFINDEX, &ifr);

    struct sockaddr_can addr;
    memset(&addr, 0, sizeof(addr));
    addr.can_family = AF_CAN;
    addr.can_ifindex = ifr.ifr_ifindex;

    // 3. 绑定
    if (bind(m_sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
        emit errorOccurred("绑定 CAN socket 失败");
        ::close(m_sock); m_sock = -1;
        return false;
    }

    // 4. 用 QSocketNotifier 监听可读事件(Qt 事件循环集成)
    m_notifier = new QSocketNotifier(m_sock, QSocketNotifier::Read, this);
    connect(m_notifier, &QSocketNotifier::activated, this, &CanBus::onReadyRead);

    qDebug() << "CAN socket opened on" << ifName;
    return true;
}

void CanBus::close() {
    if (m_notifier) { delete m_notifier; m_notifier = nullptr; }
    if (m_sock >= 0) { ::close(m_sock); m_sock = -1; }
}

bool CanBus::send(const CanFrame &frame) {
    struct can_frame cf;
    memset(&cf, 0, sizeof(cf));

    cf.can_id = frame.id;
    if (frame.extended) cf.can_id |= CAN_EFF_FLAG;  // 29 位扩展帧标志
    if (frame.rtr)      cf.can_id |= CAN_RTR_FLAG; // 远程帧标志

    cf.can_dlc = qMin(frame.data.size(), 8); // 经典 CAN 最多 8 字节
    memcpy(cf.data, frame.data.constData(), cf.can_dlc);

    ssize_t n = ::write(m_sock, &cf, sizeof(cf));
    if (n != sizeof(cf)) {
        emit errorOccurred("发送失败");
        return false;
    }
    return true;
}

void CanBus::setFilter(const QVector<quint32> &ids) {
    if (ids.isEmpty()) {
        // 空列表 = 不设过滤,接收所有
        setsockopt(m_sock, SOL_CAN_RAW, CAN_RAW_FILTER, nullptr, 0);
        return;
    }
    QVector<can_filter> filters;
    for (quint32 id : ids) {
        can_filter f;
        f.can_id = id;
        f.can_mask = CAN_EFF_MASK; // 精确匹配 ID
        filters.append(f);
    }
    setsockopt(m_sock, SOL_CAN_RAW, CAN_RAW_FILTER,
                 filters.data(), sizeof(can_filter) * filters.size());
}

void CanBus::onReadyRead() {
    struct can_frame cf;
    while (true) {
        ssize_t n = ::read(m_sock, &cf, sizeof(cf));
        if (n <= 0) break;  // 无数据或错误

        CanFrame frame;
        frame.id = cf.can_id & CAN_EFF_MASK;
        frame.extended = (cf.can_id & CAN_EFF_FLAG) != 0;
        frame.rtr = (cf.can_id & CAN_RTR_FLAG) != 0;
        frame.data = QByteArray(reinterpret_cast<const char*>(cf.data), cf.can_dlc);

        emit frameReceived(frame);
    }
}

11.3 使用示例:模拟 OBD-II 查发动机转速

// main.cpp —— 使用 CanBus 类模拟 OBD-II 查询
#include "CanBus.h"
#include <QCoreApplication>
#include <QDebug>

int main(int argc, char *argv[]) {
    QCoreApplication app(argc, argv);

    CanBus can;

    // 接收响应处理
    QObject::connect(&can, &CanBus::frameReceived, [](const CanFrame &f) {
        // 只处理 OBD-II 响应 ID: 0x7E8
        if (f.id != 0x7E8) return;

        // 响应格式: [长度] [41=模式01响应] [0C=PID] [高字节] [低字节]
        if (f.data.size() >= 4 &&
            (quint8)f.data[1] == 0x41 &&
            (quint8)f.data[2] == 0x0C) {

            quint16 raw = ((quint8)f.data[3] << 8) | (quint8)f.data[4];
            double rpm = raw / 4.0;
            qDebug() << "Engine RPM:" << rpm;
        }
    });

    if (!can.open("can0")) {
        qDebug() << "打开 can0 失败";
        return -1;
    }

    // 发送 OBD-II 请求:ID=0x7DF, Data=[02 01 0C 00 00 00 00 00]
    CanFrame req;
    req.id = 0x7DF;
    req.extended = false;
    req.rtr = false;
    req.data = QByteArray::fromHex("02010C0000000000");
    can.send(req);

    qDebug() << "OBD-II 查询已发送,等待响应...";

    return app.exec();
}
SocketCAN 的精妙设计
Linux 内核把 CAN 设备抽象成网络接口,用 socket(PF_CAN, SOCK_RAW, CAN_RAW) 创建套接字。这意味着:
① select/poll/epoll 都能用,天然支持高并发
② 过滤在内核层:setsockopt 设 CAN_RAW_FILTER,不感兴趣的帧根本不进用户态
③ 与 Qt 无缝集成:用 QSocketNotifier 包一下,立即融入 Qt 事件循环

11.4 Windows 平台方案

Windows 没有 SocketCAN,需要用厂商提供的 DLL(如 PCAN 的 PCANBasic.dll、Vector 的 vxlapi.dll)或 SocketCAN 的 Windows 移植版。

// Windows 方案:Qt SerialBus 模块(跨平台推荐)
// Qt 5.7+ 提供 QCanBus 抽象层,统一封装各厂商驱动

#include <QCanBus>
#include <QCanBusFrame>
#include <QCanBusDevice>

// 列举系统支持的 CAN 插件
QStringList plugins = QCanBus::instance()->availableDevices("socketcan");
for (const QString &p : plugins)
    qDebug() << "Found:" << p;

// 创建设备连接
QCanBusDevice *device = QCanBus::instance()->createDevice(
    "socketcan", "can0");

if (!device || !device->connectDevice()) {
    qDebug() << "连接失败";
    return;
}

// 接收
QObject::connect(device, &QCanBusDevice::framesReceived, [=]() {
    while (device->framesAvailable()) {
        QCanBusFrame frame = device->readFrame();
        qDebug() << "ID:" << QString::number(frame.frameId(), 16)
                 << "Data:" << frame.payload().toHex();
    }
});

// 发送
QCanBusFrame sendFrame;
sendFrame.setFrameId(0x123);
sendFrame.setPayload(QByteArray::fromHex("0102030405060708"));
device->writeFrame(sendFrame);
Qt SerialBus 的优势
统一 API 接口,底层切换不同厂商只需换插件名:
"socketcan"(Linux)| "peakcan"(PCAN)| "systecan"(SYSTEC)| "vectorcan"(Vector)| "virtualcan"(虚拟,无硬件测试)

十二、应用场景与选型建议

12.1 典型应用场景

CAN 典型应用场景全景 汽车电子 • 发动机/变速箱 ECU • ABS/ESP 刹车控制 • 仪表盘/车身控制 • OBD-II 诊断接口 • 车窗/座椅/空调 工业自动化 • CANopen 伺服驱动 • 机器人关节控制 • PLC I/O 模块 • 传感器网络 • 物流分拣系统 商用车/工程机械 • J1939 重卡/客车 • 挖掘机/起重机 • 拖拉机/农机 • 叉车/AGV • 船舶主机控制 其他领域 • 医疗设备 • 电梯控制 • 楼宇自控 • 灯光控制 • 无人机 CAN 选型决策树 需要实时 + 多主 + 高可靠 + 适度带宽?→ CAN 需要超大带宽(100Mbps+)?→ 车载以太网 需要长距离(km 级)+ 主从?→ RS-485 需要短距离点对点?→ RS-232 需要本质安全 + 抗雷击?→ 隔离 CAN(ISO1042)
CAN 应用场景全景与选型决策树

12.2 CAN vs 其他总线对比

维度CAN 2.0CAN FDRS-485车载以太网
通信方式多主竞争多主竞争主从轮询交换式
最大速率1 Mbps8 Mbps10 Mbps1 Gbps+
最大距离1 km @ 50kbps同左1.2 km @ 100kbps100 m
实时性优秀(ID 仲裁)优秀差(轮询延迟)优秀(TSN)
错误检测CRC15 + 多层CRC17/21软件 CRCFCS
成本低中极低高
典型用途汽车/工业新款汽车仪表/低速ADAS/座舱

十三、调试技巧与常见问题

13.1 调试三板斧

  1. 看总线电平:万用表测 CAN_H 和 CAN_L 对地电压。空闲时都约 2.5V,通信时 CAN_H 升到 3.5V,CAN_L 降到 1.5V
  2. 测终端电阻:断电用万用表测 CAN_H-CAN_L 之间,应为 60Ω(两个 120Ω 并联)。如果是 120Ω 说明少接一个,如果是 0Ω 说明短路
  3. 抓包对比:用 candump(Linux)或 PCAN-View(Windows)抓原始帧,与预期 ID/数据对比

13.2 常见问题速查

现象可能原因排查
完全收不到数据终端电阻缺失/总线断线/波特率不匹配测电阻 60Ω;确认两端电阻;核对波特率
偶发错误帧总线太长/分支太长/干扰降波特率;缩短分支;换双绞屏蔽线
特定节点 Bus-Off节点硬件故障/收发器损坏换收发器;查 TEC 寄存器
高优先级帧阻塞低优先级ID 分配不合理调整 ID,给低优先级合理间隙
ACK 错误频繁总线上只有自己(无接收方)确保至少 2 个节点在线
数据偶发错误线缆质量差/接头氧化换高质量双绞线;防水接头
CAN 调试三大坑
① 共地:CAN_H/CAN_L 两线虽是差分,但收发器需要参考地。不同设备不共地会导致共模电压超限,烧收发器
② 终端电阻位置:必须放在总线物理最远两端,不能放在中间节点。常见错误是"每个节点都接 120Ω",导致总阻抗过低
③ 波特率容差:所有节点波特率误差必须 < 0.6%。晶振用 ±50ppm 以上的,别用内部 RC 振荡器

13.3 调试工具链

工具平台用途
candump/cansendLinux命令行抓包/发包
PCAN-View + PCAN-USBWin/Linux图形化抓包,入门首选
CANoeWindows专业仿真/测试/分析
Wireshark + SocketCANLinux用 Wireshark 解析 CAN
can-utils(Linux)Linuxcandump/cansend/canplayer 等

十四、总结

本文知识全景

  1. CAN 本质:多主、差分、短帧的现场总线,用 2 根线替代 N(N-1) 根线束
  2. 物理层:CAN_H/CAN_L 差分信号,显性(0)覆盖隐性(1)= 线与机制
  3. 帧结构:SOF + 仲裁(11/29位ID) + 控制 + 数据(8B) + CRC + ACK + EOF
  4. 非破坏性仲裁:逐位比较 ID,小的胜出,败者退让无损重发
  5. 错误处理:5 种错误 + 3 种状态(Active→Passive→Bus-Off)+ TEC/REC 计数
  6. CAN FD:数据段 5~8Mbps + 64 字节数据 + CRC17/21
  7. 上层协议:CANopen(工业)、J1939(商用车)、OBD-II(诊断)、NMEA 2000(船舶)
  8. CANoe:Vector 仿真利器,CAPL 脚本 + DBC 信号定义 + Panel 交互
  9. SocketCAN:Linux 把 CAN 抽象为 socket,用 Qt/C++ 标准 API 编程
  10. 调试要点:终端电阻 60Ω、共地、波特率容差 < 0.6%
CAN 协议一图速记 物理层 差分 CAN_H/CAN_L 显性=0 覆盖隐性=1 双绞线 + 120Ω×2 终端 1Mbps/40m ~ 50kbps/5km 30~64 节点 数据链路层 帧: SOF+仲裁+控制+数据 +CRC+ACK+EOF 仲裁: 小ID优先 错误: Active→Passive→Off FD: 64B@8Mbps 应用层 CANopen: 工业自动化 J1939: 商用车/工程机械 OBD-II: 汽车诊断 NMEA 2000: 船舶 DeviceNet: 工业 CAN = 物理层(差分) + 链路层(仲裁) + 应用层(行业定义)
CAN 三层架构一图速记
最后的话
CAN 总线是嵌入式工程师从"消费电子"迈向"工业/汽车电子"的关键分水岭。它看似简单(两根线),背后却是非破坏性仲裁、线与物理、硬件 CRC、故障自动降级等精妙设计。

理解 CAN 后,再学 LIN(低成本车身网络)、FlexRay(高安全冗余)、车载以太网(高带宽)就有了参照系。它们都在解决同一类问题:如何在严苛电磁环境下,让多个控制器可靠、实时、有序地通信。

动手建议:买一个 USB-CAN 工具(如 PCAN-USB 或国产优易特),在 Linux 上用 SocketCAN + 本文代码,搭建虚拟 CAN 总线,发送/接收几帧数据,胜过读百遍文章。