![]()
控制器局域网络(CAN)总线是一种广泛应用于汽车电子、工业控制等领域的串行通信协议。其核心特点是多主竞争、非破坏性仲裁,这得益于精心设计的帧格式。本文将详细拆解 CAN 数据帧(标准帧 CAN 2.0A)的结构,解释数据如何被打包成比特流并在总线上传输,最后给出 C++ 代码示例,演示打包与解析过程。
一、CAN 的帧类型
CAN 总线共有 5 种帧类型:
数据帧:发送节点向其他节点传输数据(最常用)。
远程帧:请求某个节点发送具有指定 ID 的数据帧。
错误帧:节点检测到总线错误时主动发出的帧。
过载帧:节点尚未准备好接收时,延迟后续数据帧。
帧间隔:用于分隔上述帧。
本文聚焦数据帧,它完整展示了数据打包传输的全过程。
二、标准数据帧(CAN 2.0A)结构
标准数据帧由 7 个不同场(Field)组成,其结构如下图所示:
┌─────┬─────────────┬─────┬───────────┬────────────┬─────────┬─────┬─────────┐
│ SOF │ 仲裁场(12b) │控制场(6b)│ 数据场(0~8B) │ CRC场(16b) │ ACK场(2b) │ EOF(7b) │
│ 1b │ ID(11b)+RTR │IDE+r0+DLC│ │ CRC序列+界定符│ 槽+界定符 │ │
└─────┴─────────────┴─────┴───────────┴────────────┴─────────┴─────┴─────────┘
各字段的详细功能如下:
字段
位数
SOF
(帧起始)
1
显性位(逻辑0),用于同步所有节点的内部时钟,标志着帧的开始。
仲裁场
12
包含11 位标识符(ID)和RTR位。ID 决定帧优先级(数值越小优先级越高);RTR=0 表示数据帧,RTR=1 表示远程帧。
控制场
6
包含 IDE 位(标识符扩展,标准帧为显性0)、保留位 r0(显性0)以及4 位 DLC(数据长度码,指示数据场字节数,取值 0~8)。
数据场
0~64
实际要传输的数据,最多 8 字节。高位字节先发送(MSB first)。
CRC 场
16
15 位循环冗余校验码 + 1 位隐性界定符。发送方根据帧起始到数据场结束计算 CRC,接收方重新计算并比较,以检测传输错误。
ACK 场
2
包含ACK 槽(1 位)和ACK 界定符(1 位隐性)。发送方在 ACK 槽发送隐性位;接收方如果正确接收到帧,则在 ACK 槽发出显性位(覆盖隐性),表示应答。
EOF
(帧结束)
7
7 个连续隐性位,标志帧的结束。
注:实际 CAN 总线使用 NRZ(不归零)编码,并引入位填充(每 5 个相同电平后插入一个相反电平)以保证同步。CRC 计算和位填充通常由 CAN 控制器硬件完成,软件开发者只需关注 ID、DLC 和数据场。三、扩展数据帧(CAN 2.0B)简介
CAN 2.0B 将标准帧的 11 位 ID 扩展为29 位(最多可标识 5.36 亿个节点),主要用于 CANopen、J1939 等需要大量 ID 的协议。其结构差异在于:
仲裁场中IDE 位为隐性(1),随后增加18 位扩展 ID。
控制场中的r0 位被替换为 RTR 位(扩展帧中称为 SRR,替代远程请求),整体结构更复杂。
扩展帧与标准帧可以共存于同一总线,通过 IDE 位区分。
四、数据打包与传输流程
应用层准备数据:决定目标 ID、DLC(0~8)和最多 8 字节数据。
构建帧字段:依次填充 SOF、ID、RTR、控制位、DLC、数据。
计算 CRC:对 SOF 到数据场末尾的位序列进行多项式除法,得到 15 位 CRC 序列。
位填充:在 SOF 到 CRC 序列(不含界定符)之间,每连续 5 个相同电平插入一个相反电平。
生成帧:组装所有字段(包括填充位),依次发送到总线。
接收与校验:接收节点去除填充位,重新计算 CRC 并与接收到的 CRC 比较;若一致且无错误,则在 ACK 槽位发送显性位应答。
结束:发送节点检测到 ACK 显性位后,发送 EOF,完成一次通信。
以下 C++ 代码演示了如何将高层 CAN 帧结构(ID、DLC、数据)打包成符合标准帧格式的比特流(字节数组),并从中解析出原始数据。为简化教学,忽略 CRC 计算和位填充(实际由硬件或专用库完成),重点展示字段的排列与位操作。
代码说明#include
#include
#include
#include
// 高层 CAN 标准帧表示(应用层直接使用的结构)
struct CAN_StandardFrame {
uint32_t id; // 11 位标识符 (0x000 ~ 0x7FF)
uint8_t dlc; // 数据长度码 (0 ~ 8)
uint8_t data[8]; // 数据场,最多 8 字节
};
/**
* 将标准帧打包成字节数组(模拟总线上的比特流,不含位填充和有效 CRC)
* @param frame 输入的高层帧结构
* @return 打包后的字节数组,按 CAN 总线顺序排列(每字节内 MSB 先发送)
*/
std::vector pack_standard_frame(const CAN_StandardFrame& frame) {
// 标准帧最大长度(不含填充):SOF(1)+ID(11)+RTR(1)+IDE(1)+r0(1)+DLC(4)+数据(0~64)+CRC(15)+CRC界定符(1)+ACK槽(1)+ACK界定符(1)+EOF(7) = 44~108位 ≈ 6~14字节
// 我们固定分配 14 字节,未使用的位填 0(隐性)。
std::vector raw(14, 0);
// 辅助函数:在字节数组的指定位位置写入一个比特(MSB first)
auto set_bit = [&raw](int bit_pos, bool value) {
int byte_idx = bit_pos / 8;
int bit_offset = 7 - (bit_pos % 8); // 高位在前
if (value)
raw[byte_idx] |= (1 << bit_offset);
else
raw[byte_idx] &= ~(1 << bit_offset);
};
int bit_pos = 0;
// 1. SOF:显性位(逻辑 0)
set_bit(bit_pos++, false);
// 2. 仲裁场:11 位 ID(高位先发)
for (int i = 10; i >= 0; --i) {
set_bit(bit_pos++, (frame.id >> i) & 0x01);
}
// RTR:数据帧为 0(显性)
set_bit(bit_pos++, false);
// 3. 控制场:IDE(0) + r0(0) + DLC(4位)
set_bit(bit_pos++, false); // IDE = 0(标准帧)
set_bit(bit_pos++, false); // r0 = 0(保留)
for (int i = 3; i >= 0; --i) {
set_bit(bit_pos++, (frame.dlc >> i) & 0x01);
}
// 4. 数据场:按字节发送,每个字节高位先发
for (int byte = 0; byte < frame.dlc; ++byte) {
for (int i = 7; i >= 0; --i) {
set_bit(bit_pos++, (frame.data[byte] >> i) & 0x01);
}
}
// 5. CRC 场(15 位 CRC + 1 位界定符):这里仅占位,实际应由硬件计算
for (int i = 0; i < 15; ++i) set_bit(bit_pos++, false); // 全 0(隐性,实际取决于 CRC 结果)
set_bit(bit_pos++, true); // CRC 界定符(隐性)
// 6. ACK 场:发送方在 ACK 槽发送隐性,界定符隐性
set_bit(bit_pos++, true); // ACK 槽(发送隐性,等待接收方置显性)
set_bit(bit_pos++, true); // ACK 界定符
// 7. EOF:7 个隐性位
for (int i = 0; i < 7; ++i) set_bit(bit_pos++, true);
return raw;
}
/**
* 从字节数组中解析出标准帧(忽略 CRC/ACK 校验)
* @param raw 输入的比特流字节数组(与 pack_standard_frame 格式一致)
* @return 解析出的高层帧结构
*/
CAN_StandardFrame parse_standard_frame(const std::vector& raw) {
CAN_StandardFrame frame = {0};
auto get_bit = [&raw](int bit_pos) -> bool {
int byte_idx = bit_pos / 8;
int bit_offset = 7 - (bit_pos % 8);
return (raw[byte_idx] >> bit_offset) & 0x01;
};
int bit_pos = 0;
// SOF 检查(应为 0)
bool sof = get_bit(bit_pos++);
if (sof != false) {
std::cerr << "警告:无效 SOF(应显性 0)" << std::endl;
}
// 读取 ID(11 位)
uint32_t id = 0;
for (int i = 10; i >= 0; --i) {
id |= (static_cast(get_bit(bit_pos++)) << i);
}
frame.id = id;
// RTR 位(数据帧应 0)
bool rtr = get_bit(bit_pos++);
if (rtr != false) {
std::cerr << "警告:RTR 位不为 0,非数据帧" << std::endl;
}
// IDE(标准帧应为 0)
bool ide = get_bit(bit_pos++);
bool r0 = get_bit(bit_pos++);
// DLC
uint8_t dlc = 0;
for (int i = 3; i >= 0; --i) {
dlc |= (static_cast(get_bit(bit_pos++)) << i);
}
frame.dlc = dlc;
// 数据场
for (int byte = 0; byte < frame.dlc; ++byte) {
uint8_t data_byte = 0;
for (int i = 7; i >= 0; --i) {
data_byte |= (static_cast(get_bit(bit_pos++)) << i);
}
frame.data[byte] = data_byte;
}
// 后续 CRC、ACK、EOF 可忽略(本示例不校验)
return frame;
}
// 辅助函数:打印字节数组的二进制形式(便于观察字段布局)
void print_binary(const std::vector& data) {
for (size_t i = 0; i < data.size(); ++i) {
std::cout << std::bitset<8>(data[i]) << " ";
if ((i + 1) % 4 == 0) std::cout << std::endl;
}
std::cout << std::endl;
}int main() {
// 示例:发送一个 ID=0x123,DLC=4,数据为 {0xDE, 0xAD, 0xBE, 0xEF} 的标准数据帧
CAN_StandardFrame tx_frame;
tx_frame.id = 0x123; // 二进制 0001 0010 0011
tx_frame.dlc = 4;
tx_frame.data[0] = 0xDE;
tx_frame.data[1] = 0xAD;
tx_frame.data[2] = 0xBE;
tx_frame.data[3] = 0xEF;
// 打包
std::vector bus_stream = pack_standard_frame(tx_frame);
std::cout << "=== 打包后的比特流(14字节,不含填充和有效CRC)===" << std::endl;
print_binary(bus_stream);
// 解析
CAN_StandardFrame rx_frame = parse_standard_frame(bus_stream);
std::cout << "=== 解析结果 ===" << std::endl;
std::cout << "ID: 0x" << std::hex << rx_frame.id << std::dec << std::endl;
std::cout << "DLC: " << static_cast(rx_frame.dlc) << std::endl;
std::cout << "数据: ";
for (int i = 0; i < rx_frame.dlc; ++i) {
std::cout << std::hex << std::setw(2) << std::setfill('0')
<< static_cast(rx_frame.data[i]) << " ";
}
std::cout << std::dec << std::endl;
return 0;
}
**
pack_standard_frame**:按照 CAN 标准帧的位顺序,将 ID、DLC、数据等字段依次写入一个字节数组。每个字节内采用MSB first顺序(与 CAN 总线一致)。CRC 场和 ACK 场使用占位值,实际项目中应调用硬件或 CRC 库计算。**
parse_standard_frame**:逆向读取位流,还原出 ID、DLC 和数据。同时进行简单的合法性检查(SOF、RTR 等)。main函数:演示一个完整的打包-解析流程,并打印比特流和解析结果。
运行该程序,你将看到类似下面的输出(比特流因固定占位而包含大量 0,但 ID、DLC 和数据位置正确):
=== 打包后的比特流(14字节,不含填充和有效CRC)===
00000000 00000000 10010001 00110000 ...
...
=== 解析结果 ===
ID: 0x123
DLC: 4
数据: de ad be ef
六、扩展帧的代码差异若需支持扩展帧(29 位 ID),只需修改打包/解析函数中的位宽和字段顺序:
仲裁场:11 位基本 ID + SRR(1) + IDE(1) + 18 位扩展 ID。
控制场:RTR(1) + r1(1) + r0(1) + DLC(4)。
总长度增加,CRC 计算范围也相应变化。
通常在实际工程中,我们会使用成熟的 CAN 库(如 Linux SocketCAN 的struct can_frame和struct canfd_frame),它们已经封装好了标准帧和扩展帧的打包逻辑。
七、总结
CAN 数据帧通过精心划分的字段,实现了高可靠性的数据打包传输:
SOF同步所有节点;
ID 仲裁决定总线竞争胜者;
DLC灵活指示数据长度;
CRC保证数据完整性;
ACK实现确认机制。
虽然 CRC 计算和位填充细节较为复杂,但 CAN 控制器硬件已帮开发者屏蔽了底层细节。理解帧格式能帮助我们更好地调试 CAN 通信问题,以及编写正确的应用层协议。
希望本文的讲解和代码示例能帮助你掌握 CAN 数据打包传输的核心原理。在实际开发中,建议直接使用硬件厂商提供的 CAN 驱动或操作系统级的 SocketCAN 接口,以简化开发流程。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.