CAN(Controller Area Network)总线由博世公司在1986年开发,最初用于汽车内部ECU之间的通讯,凭借高可靠性、强抗干扰能力和低成本优势,逐步扩展到工业自动化、医疗设备、轨道交通、新能源等领域。在上位机开发中,CAN数据采集是汽车电子、电池管理、电机控制等场景的常见需求。本文从协议基础到代码实现,系统讲解CAN总线及上位机数据采集方案。
一、CAN协议基础
CAN是一种多主方式的串行通讯总线,采用差分信号传输,具有非破坏性仲裁机制和强大的错误检测能力。理解CAN的物理层和数据链路层特性是使用CAN的基础。
1.1 总线拓扑
CAN总线采用总线型拓扑,所有节点通过双绞线(CAN_H、CAN_L)并联到总线上,两端各安装一个120Ω终端电阻,与双绞线特征阻抗匹配以消除信号反射。总线最大长度与波特率负相关:1Mbps时约40米,500kbps时约100米,125kbps时约500米,50kbps时约1公里。
1.2 差分信号
CAN采用差分信号传输,通过CAN_H与CAN_L的电压差表示逻辑状态:
| 状态 | CAN_H | CAN_L | 电压差 | 逻辑 |
|---|---|---|---|---|
| 显性(Dominant) | 3.5V | 1.5V | 2.0V | 0 |
| 隐性(Recessive) | 2.5V | 2.5V | 0V | 1 |
显性位(0)会覆盖隐性位(1),这是CAN仲裁机制的基础:多个节点同时发送时,发送显性位的节点优先获得总线。差分传输方式对共模干扰有很强抑制能力,是CAN抗干扰能力强的根本原因。
1.3 仲裁机制
CAN采用"非破坏性按位仲裁"机制:当多个节点同时开始发送时,每个节点在发送每一位的同时监测总线电平。若节点发送隐性位(1)但读到显性位(0),说明有更高优先级的节点正在发送,该节点立即停止发送。仲裁通过报文ID完成,ID值越小优先级越高。
1.4 标准帧与扩展帧
CAN帧按ID长度分为标准帧(Standard Frame,11位ID)和扩展帧(Extended Frame,29位ID)。标准帧ID范围0x000-0x7FF,扩展帧ID范围0x00000000-0x1FFFFFFF。早期汽车电子多用标准帧,但随着ECU数量增多,扩展帧应用越来越广,特别是J1939协议强制使用扩展帧。
二、CAN帧结构详解
一个标准CAN数据帧由7个段组成:帧起始(SOF)、仲裁段、控制段、数据段、CRC段、ACK段、帧结束(EOF)。理解每一段的作用是协议解析的基础。
标准数据帧结构(最大130位):
┌──────┬───────────────┬──────────┬───────────────┬──────────┬───────┬───────┐
│ SOF │ 仲裁段 │ 控制段 │ 数据段 │ CRC段 │ ACK段 │ EOF │
│ 1位 │ 12位 (ID+RTR) │ 6位 │ 0-64位(0-8字节)│ 16位 │ 2位 │ 7位 │
└──────┴───────────────┴──────────┴───────────────┴──────────┴───────┴───────┘
扩展数据帧结构(最大150位):
┌──────┬──────────────────────────────────┬──────────┬───────────┬────────┬───────┬───────┐
│ SOF │ 仲裁段 (11位ID+SRR+IDE+18位ID+RTR)│ 控制段 │ 数据段 │ CRC段 │ ACK段 │ EOF │
│ 1位 │ 32位 │ 6位 │ 0-64位 │ 16位 │ 2位 │ 7位 │
└──────┴──────────────────────────────────┴──────────┴───────────┴────────┴───────┴───────┘
2.1 帧起始(SOF)
SOF占1位,为显性位(0),标志一帧的开始,同时同步所有节点的位定时。所有节点在SOF之后开始仲裁。
2.2 仲裁段
标准帧仲裁段包含11位ID + 1位RTR(远程请求帧标志)。扩展帧仲裁段包含11位基础ID + 1位SRR(替代远程请求,固定为1)+ 1位IDE(标识符扩展位,固定为1)+ 18位扩展ID + 1位RTR。RTR位为0表示数据帧,为1表示远程帧(请求具有相同ID的数据帧)。
2.3 控制段
控制段包含IDE位(标准帧为0,扩展帧在仲裁段已用)+ r0保留位 + 4位DLC(数据长度代码)。DLC取值0-8,表示数据段的字节数。CAN 2.0A/B规范限制数据段最多8字节,CAN FD(Flexible Data-rate)扩展到64字节。
2.4 数据段
数据段承载实际数据,0-8字节(CAN FD为0-64字节)。数据按字节顺序传输,每字节高位在前(MSB first)。
2.5 CRC段
CRC段包含15位CRC校验序列 + 1位CRC界定符(固定隐性1)。发送节点对SOF到数据段的所有位计算CRC15,接收节点同样计算并比对,不一致则产生错误帧。CRC多项式为x^15 + x^14 + x^10 + x^8 + x^7 + x^4 + x^3 + 1。
2.6 ACK段
ACK段包含1位ACK槽 + 1位ACK界定符。发送节点在ACK槽发送隐性位,所有正确接收到该帧的节点在此位发送显性位("应答")。若发送节点在ACK槽读到隐性位,说明没有节点正确接收,将触发重发。
2.7 帧结束(EOF)
EOF由7个连续隐性位组成,标志一帧结束。其后是ITM(帧间隔),3个隐性位,之后总线空闲,允许下一个节点开始发送。
三、常用CAN上层协议
CAN协议本身只规定了物理层和数据链路层,应用层协议需要由各行业自行定义。常见的CAN上层协议有CANopen、J1939、DeviceNet等。
| 协议 | 主导组织 | 主要应用 | 特点 |
|---|---|---|---|
| CANopen | CiA | 工业自动化、医疗 | 对象字典建模、PDO/SDO通信 |
| J1939 | SAE | 商用车、工程机械、船舶 | 基于PGN的参数化传输 |
| DeviceNet | ODVA | 工厂自动化 | 基于CAN的现场总线 |
| NMEA 2000 | NMEA | 船舶电子 | 基于J1939的海洋标准 |
| iCCP | 各厂商 | 电池管理系统BMS | 充电桩与BMS通讯 |
3.1 CANopen
CANopen由CiA(CAN in Automation)组织发布,广泛应用于工业自动化和医疗设备。其核心是对象字典(Object Dictionary),每个节点维护一个16位索引+8位子索引的对象字典,存储设备的所有参数与状态。通信模型包括:PDO(过程数据对象,周期性高速传输)和SDO(服务数据对象,按需读写对象字典)。PDO支持事件驱动或时间触发,适合实时控制;SDO采用主从请求响应,适合参数配置。
3.2 J1939
J1939是SAE(国际汽车工程师学会)发布的商用车通讯标准,应用于重卡、客车、工程机械、农业机械、船舶等领域。J1939基于CAN 2.0B扩展帧,将29位ID划分为:优先级(3位)+ 保留位(1位)+ 数据页(1位)+ PDU格式(8位)+ PDU特定(8位,目标地址或PF扩展)+ 源地址(8位)。参数通过PGN(参数组编号)标识,每个PGN对应一组参数。
3.3 DeviceNet
DeviceNet是ODVA发布的基于CAN的现场总线,主要用于工厂自动化。它在CAN之上定义了应用层协议、对象模型和设备描述。DeviceNet与CANopen都是工业控制领域的CAN应用层协议,但DeviceNet在北美市场更流行,CANopen在欧洲市场更流行。
四、CAN硬件接口选型
上位机要采集CAN数据,需要通过CAN硬件接口连接总线。常见的接口形式有USB-CAN、PCI/PCIe-CAN、CAN网关等。
| 接口类型 | 连接方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| USB-CAN | USB | 笔记本调试、移动设备 | 便携、即插即用 | USB带宽受限 |
| PCI/PCIe-CAN | PCIe插槽 | 工控机长期采集 | 稳定、带宽高 | 需开机箱、不便携 |
| 以太网CAN网关 | 以太网 | 多设备分布式采集 | 远距离、多设备共享 | 延迟略高 |
| WiFi-CAN | WiFi | 移动测试 | 无线便捷 | 稳定性受限 |
常见品牌:PEAK PCAN-USB(德国,软件生态完善,PCAN-Basic API免费)、Kvaser(瑞典,高端工业级)、周立功CANalyst-II(国产,性价比高)、ValueCAN(美国,适合汽车诊断)。选型时关注:支持的协议版本(CAN 2.0A/B、CAN FD)、通道数、最大波特率、缓冲区大小、SDK开放程度。
五、上位机CAN数据采集实现
下面以PEAK PCAN-USB为例,演示上位机CAN数据采集的C#代码实现。PCAN-Basic API是PEAK提供的免费SDK,支持C/C++/C#/Python等多种语言。
5.1 初始化与连接
using Peak.Can.Basic;
public class CanDevice : IDisposable
{
private ushort _handle = 0;
private bool _connected = false;
private readonly uint _baudrate;
public event Action<CanMessage> MessageReceived;
public event Action<string> LogMessage;
public CanDevice(uint baudrate = TPCANBaudrate.PCAN_BAUD_500K)
{
_baudrate = baudrate;
}
public bool Connect()
{
// 0x51 = PCAN_USBBUS1
TPCANStatus status = PCAN.Initialize(
TPCANHandle.PCAN_USBBUS1,
_baudrate,
TPCANType.PCAN_TYPE_NONE,
0, 0);
if (status != TPCANStatus.PCAN_ERROR_OK)
{
LogMessage?.Invoke($"[初始化失败] {PCAN.GetErrorText(status)}");
return false;
}
_handle = TPCANHandle.PCAN_USBBUS1;
_connected = true;
LogMessage?.Invoke("[初始化成功] PCAN-USB已连接");
return true;
}
public void StartReceiveLoop()
{
Task.Run(() => ReceiveLoop());
}
private void ReceiveLoop()
{
while (_connected)
{
TPCANStatus status = PCAN.Read(_handle, out TPCANMsg msg, out TPCANTimestamp timestamp);
if (status == TPCANStatus.PCAN_ERROR_OK)
{
var canMsg = new CanMessage
{
Id = msg.ID,
IsExtended = (msg.MSGTYPE & TPCANMessageType.PCAN_MESSAGE_EXTENDED) != 0,
IsRemote = (msg.MSGTYPE & TPCANMessageType.PCAN_MESSAGE_RTR) != 0,
DataLength = msg.LEN,
Data = msg.DATA,
Timestamp = timestamp.micros + timestamp.millis * 1000L
};
MessageReceived?.Invoke(canMsg);
}
else if (status != TPCANStatus.PCAN_ERROR_QRCVEMPTY)
{
// 非队列空错误,记录日志
LogMessage?.Invoke($"[读取错误] {PCAN.GetErrorText(status)}");
}
else
{
Thread.Sleep(1); // 队列空时短暂让出CPU
}
}
}
public bool Send(uint id, byte[] data, bool isExtended = false)
{
TPCANMsg msg = new TPCANMsg
{
ID = id,
MSGTYPE = isExtended ? TPCANMessageType.PCAN_MESSAGE_EXTENDED
: TPCANMessageType.PCAN_MESSAGE_STANDARD,
LEN = (byte)data.Length
};
for (int i = 0; i < data.Length && i < 8; i++)
msg.DATA[i] = data[i];
TPCANStatus status = PCAN.Write(_handle, ref msg);
return status == TPCANStatus.PCAN_ERROR_OK;
}
public void Dispose()
{
_connected = false;
PCAN.Uninitialize(_handle);
}
}
public class CanMessage
{
public uint Id;
public bool IsExtended;
public bool IsRemote;
public byte DataLength;
public byte[] Data;
public long Timestamp;
}
5.2 数据过滤与解析
public class CanDataProcessor
{
// ID白名单过滤:只接收指定ID的报文
private readonly HashSet<uint> _whiteList;
public CanDataProcessor(IEnumerable<uint> allowedIds)
{
_whiteList = new HashSet<uint>(allowedIds);
}
public bool Accept(CanMessage msg)
{
return _whiteList.Count == 0 || _whiteList.Contains(msg.Id);
}
// 解析8字节为多个信号(按位提取)
public static double ExtractSignal(byte[] data, int startBit, int bitLength,
double factor = 1.0, double offset = 0.0, bool isLittleEndian = false)
{
ulong raw = 0;
if (isLittleEndian)
{
// Intel字节序:低位字节在前
for (int i = 0; i < bitLength; i++)
{
int byteIdx = (startBit + i) / 8;
int bitIdx = (startBit + i) % 8;
if (((data[byteIdx] >> bitIdx) & 1) == 1)
raw |= (1UL << i);
}
}
else
{
// Motorola字节序:高位字节在前,跨字节时方向反转
for (int i = 0; i < bitLength; i++)
{
int absoluteBit = startBit - i;
int byteIdx = absoluteBit / 8;
int bitIdx = absoluteBit % 8;
if (((data[byteIdx] >> bitIdx) & 1) == 1)
raw |= (1UL << (bitLength - 1 - i));
}
}
return raw * factor + offset;
}
}
说明:CAN报文的数据长度虽然是8字节,但实际信号往往按位划分,需要根据DBC文件(数据库文件)描述的起始位、长度、字节序、缩放系数、偏移量来解析。Motorola字节序(大端)与Intel字节序(小端)在多字节信号上提取方式不同,是常见的解析错误来源。
六、CAN数据解析实战
6.1 J1939 PGN解析示例
J1939报文的29位ID解析为优先级、PGN、源地址三部分。下面给出PGN解析与一个典型参数的解码示例。
public class J1939Parser
{
// 解析29位CAN ID为J1939参数
public static (int priority, uint pgn, byte srcAddr) ParseId(uint canId)
{
int priority = (int)((canId >> 26) & 0x07);
byte pf = (byte)((canId >> 16) & 0xFF); // PDU Format
byte ps = (byte)((canId >> 8) & 0xFF); // PDU Specific
byte sa = (byte)(canId & 0xFF); // Source Address
uint pgn;
if (pf < 240)
pgn = (uint)(pf << 8); // PDU1: 目标地址在PS中,PGN不含PS
else
pgn = (uint)((pf << 8) | ps); // PDU2: 广播报文,PGN含PS
return (priority, pgn, sa);
}
}
// 示例:发动机转速 EECS1 (PGN 61444, SPN 190)
// 报文 ID = 0x0CF00400, 数据 = [A1 A2 03 FF FF FF FF FF]
var canId = 0x0CF00400u;
var data = new byte[] { 0xA1, 0xA2, 0x03, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
var (pri, pgn, src) = J1939Parser.ParseId(canId);
// pgn = 0xF004 = 61444
if (pgn == 61444)
{
// SPN 190: Engine Speed, 起始位24, 长度16, 分辨率0.125 rpm/bit, 偏移0
double raw = CanDataProcessor.ExtractSignal(data, 24, 16, isLittleEndian: true);
double engineSpeed = raw * 0.125;
Console.WriteLine($"发动机转速: {engineSpeed:F1} rpm");
// raw = (0x03 << 8) | 0xA2 = 930, 转速 = 930 * 0.125 = 116.25 rpm
}
6.2 CANopen PDO映射示例
CANopen的PDO(Process Data Object)用于周期性传输过程数据。PDO分TPDO(发送PDO,从节点上报)和RPDO(接收PDO,主站下发)。PDO映射通过对象字典0x1600-0x17FF(RPDO)和0x1A00-0x1BFF(TPDO)配置,描述每个字节映射到哪个对象。
// TPDO1 通信参数(对象字典0x1800+节点ID)
// COB-ID = 0x180 + NodeID
// 例:节点1的TPDO1,COB-ID = 0x181
// TPDO1映射参数(对象字典0x1A00)
// 映射条目格式:index(16bit) + subindex(8bit) + length(8bit)
// 例:映射对象 0x6000:01 (实际位置) 32位 到 TPDO1 字节0-3
// 0x60000120 = index=0x6000, sub=0x01, length=0x20 (32 bits)
public class CanOpenPdoParser
{
// 解析TPDO1数据,根据映射表还原各对象值
public static Dictionary<string, object> ParseTpdo(
byte[] pdoData, List<PdoMappingEntry> mapping)
{
var result = new Dictionary<string, object>();
int bitOffset = 0;
foreach (var entry in mapping)
{
int byteOffset = bitOffset / 8;
int bitLength = entry.Length;
ulong raw = 0;
// 按小端序提取
for (int i = 0; i < (bitLength + 7) / 8; i++)
{
if (byteOffset + i < pdoData.Length)
raw |= ((ulong)pdoData[byteOffset + i]) << (i * 8);
}
object value = entry.DataType switch
{
"INT8" => (sbyte)raw,
"UINT8" => (byte)raw,
"INT16" => (short)raw,
"UINT16" => (ushort)raw,
"INT32" => (int)raw,
"UINT32" => (uint)raw,
"REAL32" => BitConverter.Int32BitsToSingle((int)raw),
_ => raw
};
result[entry.Name] = value;
bitOffset += bitLength;
}
return result;
}
}
public class PdoMappingEntry
{
public string Name;
public int Length; // 位长度
public string DataType;
}
实际项目中PDO映射表通常由设备的EDS(Electronic Data Sheet)文件描述,可借助CANopen Magic或CANFestival工具自动解析。在赢式科技的医疗设备项目中,多轴伺服的位置、速度、状态字都通过TPDO周期上报,上位机根据PDO映射表实时还原各轴状态。
七、CAN总线常见问题排查
7.1 总线错误状态
CAN控制器有三个错误状态:错误主动(Error Active)、错误被动(Error Passive)、总线关闭(Bus Off)。TEC(发送错误计数器)和REC(接收错误计数器)超过127进入错误被动,超过255进入总线关闭。总线关闭后节点无法收发,需要软件干预才能恢复(通常重启CAN控制器)。
7.2 波特率不匹配
波特率不匹配是新手最常踩的坑。同一总线上所有节点必须配置相同的波特率,否则通信完全无法建立。常见波特率:50kbps(工程机械长距离)、125kbps(工业现场)、250kbps(CANopen标准)、500kbps(汽车电子常用)、1Mbps(高速场景)。建议先用示波器测量总线波特率,再配置上位机匹配。
7.3 终端电阻缺失
终端电阻缺失或阻值不对会导致信号反射、误码率飙升。规范要求总线上有且仅有两个120Ω终端电阻,分别位于总线两端。断电时用万用表测量CAN_H与CAN_L之间的电阻,应为60Ω(两个120Ω并联)。若测到120Ω说明少了一个电阻,若测到40Ω说明多了一个电阻。
7.4 数据丢帧排查
当上位机出现数据丢帧时,可按以下顺序排查:① 总线负载率:用CAN分析仪测量总线利用率,超过70%容易丢帧,建议控制在30%以下;② 上位机处理能力:USB-CAN的接收缓冲区有限,若上位机读取不及时会缓冲区溢出丢帧,建议使用专用接收线程+环形缓冲;③ 仲裁丢失:低优先级报文在高负载时被仲裁丢弃,可通过调整ID优先级或降低总线负载解决;④ 错误帧:用示波器或CAN分析仪查看是否有错误帧,错误帧过多说明物理层有问题。
7.5 CAN FD升级注意事项
CAN FD(Flexible Data-rate)支持最高64字节数据段和更高传输速率(数据段可达5Mbps甚至8Mbps),是新项目的趋势。升级CAN FD需要注意:① 总线上的所有节点必须都支持CAN FD,否则无法共存;② 收发器要选支持CAN FD的型号(如TJA1044、MCP2562FD),传统收发器在高波特率下信号失真严重;③ 终端电阻和拓扑要重新评估,CAN FD的上升沿陡峭对布线更敏感。
八、赢式科技CAN通讯开发服务介绍
上海赢式信息科技有限公司自2010年成立以来,长期深耕上位机与工业通讯领域,累计交付3000+定制化项目,服务覆盖汽车电子测试、电池管理系统BMS、电机控制器MCU、工程机械监控、轨道交通设备、医疗器械等CAN应用场景。公司可提供CAN/CAN FD/CANopen/J1939等多协议接入、上位机数据采集、DBC/EDS文件解析、实时数据可视化、协议仿真测试等完整工程化能力。如果您有CAN通讯或上位机开发需求,欢迎联系赢式科技获取需求评估与方案报价。
- 电话咨询:15001875806(工作日9:00-18:00)
- 在线咨询:点击免费咨询赢式科技工程师
- 相关服务:上位机系统定制开发 | 工业物联网系统开发