![]()
控制器局域網(wǎng)絡(luò)(CAN)總線是一種廣泛應(yīng)用于汽車電子、工業(yè)控制等領(lǐng)域的串行通信協(xié)議。其核心特點(diǎn)是多主競(jìng)爭(zhēng)、非破壞性仲裁,這得益于精心設(shè)計(jì)的幀格式。本文將詳細(xì)拆解 CAN 數(shù)據(jù)幀(標(biāo)準(zhǔn)幀 CAN 2.0A)的結(jié)構(gòu),解釋數(shù)據(jù)如何被打包成比特流并在總線上傳輸,最后給出 C++ 代碼示例,演示打包與解析過程。
一、CAN 的幀類型
CAN 總線共有 5 種幀類型:
數(shù)據(jù)幀:發(fā)送節(jié)點(diǎn)向其他節(jié)點(diǎn)傳輸數(shù)據(jù)(最常用)。
遠(yuǎn)程幀:請(qǐng)求某個(gè)節(jié)點(diǎn)發(fā)送具有指定 ID 的數(shù)據(jù)幀。
錯(cuò)誤幀:節(jié)點(diǎn)檢測(cè)到總線錯(cuò)誤時(shí)主動(dòng)發(fā)出的幀。
過載幀:節(jié)點(diǎn)尚未準(zhǔn)備好接收時(shí),延遲后續(xù)數(shù)據(jù)幀。
幀間隔:用于分隔上述幀。
本文聚焦數(shù)據(jù)幀,它完整展示了數(shù)據(jù)打包傳輸?shù)娜^程。
二、標(biāo)準(zhǔn)數(shù)據(jù)幀(CAN 2.0A)結(jié)構(gòu)
標(biāo)準(zhǔn)數(shù)據(jù)幀由 7 個(gè)不同場(chǎng)(Field)組成,其結(jié)構(gòu)如下圖所示:
┌─────┬─────────────┬─────┬───────────┬────────────┬─────────┬─────┬─────────┐
│ SOF │ 仲裁場(chǎng)(12b) │控制場(chǎng)(6b)│ 數(shù)據(jù)場(chǎng)(0~8B) │ CRC場(chǎng)(16b) │ ACK場(chǎng)(2b) │ EOF(7b) │
│ 1b │ ID(11b)+RTR │IDE+r0+DLC│ │ CRC序列+界定符│ 槽+界定符 │ │
└─────┴─────────────┴─────┴───────────┴────────────┴─────────┴─────┴─────────┘
各字段的詳細(xì)功能如下:
字段
位數(shù)
SOF
(幀起始)
1
顯性位(邏輯0),用于同步所有節(jié)點(diǎn)的內(nèi)部時(shí)鐘,標(biāo)志著幀的開始。
仲裁場(chǎng)
12
包含11 位標(biāo)識(shí)符(ID)和RTR位。ID 決定幀優(yōu)先級(jí)(數(shù)值越小優(yōu)先級(jí)越高);RTR=0 表示數(shù)據(jù)幀,RTR=1 表示遠(yuǎn)程幀。
控制場(chǎng)
6
包含 IDE 位(標(biāo)識(shí)符擴(kuò)展,標(biāo)準(zhǔn)幀為顯性0)、保留位 r0(顯性0)以及4 位 DLC(數(shù)據(jù)長(zhǎng)度碼,指示數(shù)據(jù)場(chǎng)字節(jié)數(shù),取值 0~8)。
數(shù)據(jù)場(chǎng)
0~64
實(shí)際要傳輸?shù)臄?shù)據(jù),最多 8 字節(jié)。高位字節(jié)先發(fā)送(MSB first)。
CRC 場(chǎng)
16
15 位循環(huán)冗余校驗(yàn)碼 + 1 位隱性界定符。發(fā)送方根據(jù)幀起始到數(shù)據(jù)場(chǎng)結(jié)束計(jì)算 CRC,接收方重新計(jì)算并比較,以檢測(cè)傳輸錯(cuò)誤。
ACK 場(chǎng)
2
包含ACK 槽(1 位)和ACK 界定符(1 位隱性)。發(fā)送方在 ACK 槽發(fā)送隱性位;接收方如果正確接收到幀,則在 ACK 槽發(fā)出顯性位(覆蓋隱性),表示應(yīng)答。
EOF
(幀結(jié)束)
7
7 個(gè)連續(xù)隱性位,標(biāo)志幀的結(jié)束。
注:實(shí)際 CAN 總線使用 NRZ(不歸零)編碼,并引入位填充(每 5 個(gè)相同電平后插入一個(gè)相反電平)以保證同步。CRC 計(jì)算和位填充通常由 CAN 控制器硬件完成,軟件開發(fā)者只需關(guān)注 ID、DLC 和數(shù)據(jù)場(chǎng)。三、擴(kuò)展數(shù)據(jù)幀(CAN 2.0B)簡(jiǎn)介
CAN 2.0B 將標(biāo)準(zhǔn)幀的 11 位 ID 擴(kuò)展為29 位(最多可標(biāo)識(shí) 5.36 億個(gè)節(jié)點(diǎn)),主要用于 CANopen、J1939 等需要大量 ID 的協(xié)議。其結(jié)構(gòu)差異在于:
仲裁場(chǎng)中IDE 位為隱性(1),隨后增加18 位擴(kuò)展 ID。
控制場(chǎng)中的r0 位被替換為 RTR 位(擴(kuò)展幀中稱為 SRR,替代遠(yuǎn)程請(qǐng)求),整體結(jié)構(gòu)更復(fù)雜。
擴(kuò)展幀與標(biāo)準(zhǔn)幀可以共存于同一總線,通過 IDE 位區(qū)分。
四、數(shù)據(jù)打包與傳輸流程
應(yīng)用層準(zhǔn)備數(shù)據(jù):決定目標(biāo) ID、DLC(0~8)和最多 8 字節(jié)數(shù)據(jù)。
構(gòu)建幀字段:依次填充 SOF、ID、RTR、控制位、DLC、數(shù)據(jù)。
計(jì)算 CRC:對(duì) SOF 到數(shù)據(jù)場(chǎng)末尾的位序列進(jìn)行多項(xiàng)式除法,得到 15 位 CRC 序列。
位填充:在 SOF 到 CRC 序列(不含界定符)之間,每連續(xù) 5 個(gè)相同電平插入一個(gè)相反電平。
生成幀:組裝所有字段(包括填充位),依次發(fā)送到總線。
接收與校驗(yàn):接收節(jié)點(diǎn)去除填充位,重新計(jì)算 CRC 并與接收到的 CRC 比較;若一致且無錯(cuò)誤,則在 ACK 槽位發(fā)送顯性位應(yīng)答。
結(jié)束:發(fā)送節(jié)點(diǎn)檢測(cè)到 ACK 顯性位后,發(fā)送 EOF,完成一次通信。
以下 C++ 代碼演示了如何將高層 CAN 幀結(jié)構(gòu)(ID、DLC、數(shù)據(jù))打包成符合標(biāo)準(zhǔn)幀格式的比特流(字節(jié)數(shù)組),并從中解析出原始數(shù)據(jù)。為簡(jiǎn)化教學(xué),忽略 CRC 計(jì)算和位填充(實(shí)際由硬件或?qū)S脦焱瓿桑攸c(diǎn)展示字段的排列與位操作。
代碼說明#include
#include
#include
#include
// 高層 CAN 標(biāo)準(zhǔn)幀表示(應(yīng)用層直接使用的結(jié)構(gòu))
struct CAN_StandardFrame {
uint32_t id; // 11 位標(biāo)識(shí)符 (0x000 ~ 0x7FF)
uint8_t dlc; // 數(shù)據(jù)長(zhǎng)度碼 (0 ~ 8)
uint8_t data[8]; // 數(shù)據(jù)場(chǎng),最多 8 字節(jié)
};
/**
* 將標(biāo)準(zhǔn)幀打包成字節(jié)數(shù)組(模擬總線上的比特流,不含位填充和有效 CRC)
* @param frame 輸入的高層幀結(jié)構(gòu)
* @return 打包后的字節(jié)數(shù)組,按 CAN 總線順序排列(每字節(jié)內(nèi) MSB 先發(fā)送)
*/
std::vector pack_standard_frame(const CAN_StandardFrame& frame) {
// 標(biāo)準(zhǔn)幀最大長(zhǎng)度(不含填充):SOF(1)+ID(11)+RTR(1)+IDE(1)+r0(1)+DLC(4)+數(shù)據(jù)(0~64)+CRC(15)+CRC界定符(1)+ACK槽(1)+ACK界定符(1)+EOF(7) = 44~108位 ≈ 6~14字節(jié)
// 我們固定分配 14 字節(jié),未使用的位填 0(隱性)。
std::vector raw(14, 0);
// 輔助函數(shù):在字節(jié)數(shù)組的指定位位置寫入一個(gè)比特(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. 仲裁場(chǎng):11 位 ID(高位先發(fā))
for (int i = 10; i >= 0; --i) {
set_bit(bit_pos++, (frame.id >> i) & 0x01);
}
// RTR:數(shù)據(jù)幀為 0(顯性)
set_bit(bit_pos++, false);
// 3. 控制場(chǎng):IDE(0) + r0(0) + DLC(4位)
set_bit(bit_pos++, false); // IDE = 0(標(biāo)準(zhǔn)幀)
set_bit(bit_pos++, false); // r0 = 0(保留)
for (int i = 3; i >= 0; --i) {
set_bit(bit_pos++, (frame.dlc >> i) & 0x01);
}
// 4. 數(shù)據(jù)場(chǎng):按字節(jié)發(fā)送,每個(gè)字節(jié)高位先發(fā)
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 場(chǎng)(15 位 CRC + 1 位界定符):這里僅占位,實(shí)際應(yīng)由硬件計(jì)算
for (int i = 0; i < 15; ++i) set_bit(bit_pos++, false); // 全 0(隱性,實(shí)際取決于 CRC 結(jié)果)
set_bit(bit_pos++, true); // CRC 界定符(隱性)
// 6. ACK 場(chǎng):發(fā)送方在 ACK 槽發(fā)送隱性,界定符隱性
set_bit(bit_pos++, true); // ACK 槽(發(fā)送隱性,等待接收方置顯性)
set_bit(bit_pos++, true); // ACK 界定符
// 7. EOF:7 個(gè)隱性位
for (int i = 0; i < 7; ++i) set_bit(bit_pos++, true);
return raw;
}
/**
* 從字節(jié)數(shù)組中解析出標(biāo)準(zhǔn)幀(忽略 CRC/ACK 校驗(yàn))
* @param raw 輸入的比特流字節(jié)數(shù)組(與 pack_standard_frame 格式一致)
* @return 解析出的高層幀結(jié)構(gòu)
*/
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 檢查(應(yīng)為 0)
bool sof = get_bit(bit_pos++);
if (sof != false) {
std::cerr << "警告:無效 SOF(應(yīng)顯性 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 位(數(shù)據(jù)幀應(yīng) 0)
bool rtr = get_bit(bit_pos++);
if (rtr != false) {
std::cerr << "警告:RTR 位不為 0,非數(shù)據(jù)幀" << std::endl;
}
// IDE(標(biāo)準(zhǔn)幀應(yīng)為 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;
// 數(shù)據(jù)場(chǎng)
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;
}
// 后續(xù) CRC、ACK、EOF 可忽略(本示例不校驗(yàn))
return frame;
}
// 輔助函數(shù):打印字節(jié)數(shù)組的二進(jìn)制形式(便于觀察字段布局)
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() {
// 示例:發(fā)送一個(gè) ID=0x123,DLC=4,數(shù)據(jù)為 {0xDE, 0xAD, 0xBE, 0xEF} 的標(biāo)準(zhǔn)數(shù)據(jù)幀
CAN_StandardFrame tx_frame;
tx_frame.id = 0x123; // 二進(jìn)制 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字節(jié),不含填充和有效CRC)===" << std::endl;
print_binary(bus_stream);
// 解析
CAN_StandardFrame rx_frame = parse_standard_frame(bus_stream);
std::cout << "=== 解析結(jié)果 ===" << 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 << "數(shù)據(jù): ";
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 標(biāo)準(zhǔn)幀的位順序,將 ID、DLC、數(shù)據(jù)等字段依次寫入一個(gè)字節(jié)數(shù)組。每個(gè)字節(jié)內(nèi)采用MSB first順序(與 CAN 總線一致)。CRC 場(chǎng)和 ACK 場(chǎng)使用占位值,實(shí)際項(xiàng)目中應(yīng)調(diào)用硬件或 CRC 庫計(jì)算。**
parse_standard_frame**:逆向讀取位流,還原出 ID、DLC 和數(shù)據(jù)。同時(shí)進(jìn)行簡(jiǎn)單的合法性檢查(SOF、RTR 等)。main函數(shù):演示一個(gè)完整的打包-解析流程,并打印比特流和解析結(jié)果。
運(yùn)行該程序,你將看到類似下面的輸出(比特流因固定占位而包含大量 0,但 ID、DLC 和數(shù)據(jù)位置正確):
=== 打包后的比特流(14字節(jié),不含填充和有效CRC)===
00000000 00000000 10010001 00110000 ...
...
=== 解析結(jié)果 ===
ID: 0x123
DLC: 4
數(shù)據(jù): de ad be ef
六、擴(kuò)展幀的代碼差異若需支持?jǐn)U展幀(29 位 ID),只需修改打包/解析函數(shù)中的位寬和字段順序:
仲裁場(chǎng):11 位基本 ID + SRR(1) + IDE(1) + 18 位擴(kuò)展 ID。
控制場(chǎng):RTR(1) + r1(1) + r0(1) + DLC(4)。
總長(zhǎng)度增加,CRC 計(jì)算范圍也相應(yīng)變化。
通常在實(shí)際工程中,我們會(huì)使用成熟的 CAN 庫(如 Linux SocketCAN 的struct can_frame和struct canfd_frame),它們已經(jīng)封裝好了標(biāo)準(zhǔn)幀和擴(kuò)展幀的打包邏輯。
七、總結(jié)
CAN 數(shù)據(jù)幀通過精心劃分的字段,實(shí)現(xiàn)了高可靠性的數(shù)據(jù)打包傳輸:
SOF同步所有節(jié)點(diǎn);
ID 仲裁決定總線競(jìng)爭(zhēng)勝者;
DLC靈活指示數(shù)據(jù)長(zhǎng)度;
CRC保證數(shù)據(jù)完整性;
ACK實(shí)現(xiàn)確認(rèn)機(jī)制。
雖然 CRC 計(jì)算和位填充細(xì)節(jié)較為復(fù)雜,但 CAN 控制器硬件已幫開發(fā)者屏蔽了底層細(xì)節(jié)。理解幀格式能幫助我們更好地調(diào)試 CAN 通信問題,以及編寫正確的應(yīng)用層協(xié)議。
希望本文的講解和代碼示例能幫助你掌握 CAN 數(shù)據(jù)打包傳輸?shù)暮诵脑怼T趯?shí)際開發(fā)中,建議直接使用硬件廠商提供的 CAN 驅(qū)動(dòng)或操作系統(tǒng)級(jí)的 SocketCAN 接口,以簡(jiǎn)化開發(fā)流程。
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺(tái)“網(wǎng)易號(hào)”用戶上傳并發(fā)布,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。
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.