CAN(Controller Area Network)总线最核心的精妙设计之一就是 非破坏性位仲裁。当多个节点同时开始发送消息时,总线不会像以太网那样发生“碰撞”并丢弃数据,而是通过 ID 逐位比较,优先级高的节点继续发送,优先级低的节点自动退出,整个过程不丢失任何数据,也无需重传整个帧。
1. 仲裁原理
显性位 (0) 和 **隐性位 (1)**:CAN 总线采用“线与”逻辑,显性位(0)可以覆盖隐性位(1)。
仲裁过程 :所有节点同时发送自己的消息 ID(从高位到低位)。每个节点在发送每一位的同时,也监听总线状态。
如果节点发送的是隐性位 (1),但总线上检测到显性位 (0),说明有其他节点正在发送优先级更高的 ID(0 比 1 优先级高)。
该节点立即 退出仲裁 ,停止发送,转为接收模式。
仲裁获胜的节点(ID 数值最小,即显性位最多)继续发送完整的数据帧。
失败节点处理 :退出仲裁的节点会在总线空闲后 自动重发 ,无需软件干预。
为什么更适合实时控制? 以太网发生冲突需要随机退避重试,延迟不可预测;CAN 的仲裁机制保证了高优先级消息最多在一个帧时间内就能成功发送,延迟确定且极低。2. C++ 模拟代码
下面用 C++ 模拟三个节点同时发送的场景,演示位仲裁过程。为了清晰,我们将 CAN 帧简化为 11 位标准 ID(实际还有控制域、数据域等,但仲裁只依赖 ID)。
3. 运行结果示例#include
#include
#include
#include
#include
using namespace std;
// 模拟一个 CAN 节点
class CANNode {
public:
int nodeId; // 节点编号
int messageId; // 消息 ID(越小优先级越高)
bool sending; // 是否正在发送
bool arbitrationLost;// 仲裁是否失败
CANNode(int id, int msgId) : nodeId(id), messageId(msgId), sending(false), arbitrationLost(false) {}
// 返回 11 位 ID 的二进制字符串(高位在前)
string getIdBits() const {
return bitset<11>(messageId).to_string();
}
};
// 模拟总线仲裁
// 参数:参与仲裁的节点列表
// 返回值:获胜的节点指针,如果没有节点返回 nullptr
CANNode* arbitrate(vector & nodes) {
if (nodes.empty()) return nullptr;
// 所有节点同时开始发送
for (auto node : nodes) {
node->sending = true;
node->arbitrationLost = false;
}
cout << "\n========== 仲裁开始 ==========" << endl;
cout << "参与节点: ";
for (auto node : nodes) {
cout << "Node" << node->nodeId << "(ID=" << node->messageId << ") ";
}
cout << endl;
// 获取所有节点的 ID 位串
vector bitStrings;
for (auto node : nodes) {
bitStrings.push_back(node->getIdBits());
}
// 逐位仲裁(从高位到低位,即索引 0 到 10)
for (int bitPos = 0; bitPos < 11; ++bitPos) {
// 收集当前位所有节点发送的值(0 显性,1 隐性)
vector bits;
for (auto node : nodes) {
if (node->sending) { // 只考虑尚未退出的节点
int bit = node->getIdBits()[bitPos] - '0';
bits.push_back(bit);
}
}
if (bits.empty()) break; // 没有活跃节点了
// 总线状态:线与逻辑,只要有一个 0(显性),总线就是 0
int busState = 0;
for (int b : bits) {
if (b == 0) {
busState = 0;
break;
}
}
// 如果所有位都是 1,总线才是 1
bool allOne = true;
for (int b : bits) if (b != 1) { allOne = false; break; }
if (allOne) busState = 1;
// 输出当前位的比较情况
cout << "位 " << bitPos << " (从高位起): ";
for (size_t i = 0; i < nodes.size(); ++i) {
if (nodes[i]->sending) {
cout << "Node" << nodes[i]->nodeId << "=" << nodes[i]->getIdBits()[bitPos];
cout << " ";
}
}
cout << "| 总线=" << busState << endl;
// 检查哪些节点失败:发送了 1 但总线是 0
for (auto node : nodes) {
if (node->sending) {
int sentBit = node->getIdBits()[bitPos] - '0';
if (sentBit == 1 && busState == 0) {
node->sending = false;
node->arbitrationLost = true;
cout << " -> Node" << node->nodeId << " 仲裁失败,退出" << endl;
}
}
}
}
// 找出唯一获胜者(仍然 sending 为 true 且未失败)
CANNode* winner = nullptr;
for (auto node : nodes) {
if (node->sending && !node->arbitrationLost) {
winner = node;
break;
}
}
if (winner) {
cout << "\n★★★ 仲裁胜出: Node" << winner->nodeId
<< " (ID=" << winner->messageId << ") ★★★" << endl;
} else {
cout << "\n仲裁异常:无胜出节点" << endl;
}
cout << "========== 仲裁结束 ==========\n" << endl;
return winner;
}
int main() {
// 创建三个节点,消息 ID 分别为 0x123, 0x0FF, 0x100
// 注意:ID 是 11 位,范围 0~0x7FF。数值越小优先级越高。
CANNode nodeA(1, 0x123); // 二进制: 001 0010 0011 -> 0x123
CANNode nodeB(2, 0x0FF); // 二进制: 000 1111 1111 -> 0x0FF (更小,优先级高)
CANNode nodeC(3, 0x100); // 二进制: 001 0000 0000 -> 0x100
vector nodes = { &nodeA, &nodeB, &nodeC };
// 模拟同时发送
CANNode* winner = arbitrate(nodes);
// 输出仲裁后各节点的状态
cout << "仲裁结果:" << endl;
for (auto node : nodes) {
cout << "Node" << node->nodeId << " (ID=" << node->messageId << ") : "
<< (node->arbitrationLost ? "仲裁失败,等待重发" : "获胜,继续发送数据帧")
<< endl;
}
// 模拟失败节点在总线空闲后自动重发(此处简化:随机退避后重试)
cout << "\n[模拟] 总线空闲后,失败节点自动重发..." << endl;
vector failedNodes;
for (auto node : nodes) {
if (node->arbitrationLost) failedNodes.push_back(node);
}
if (!failedNodes.empty()) {
// 重新仲裁(实际会按优先级再次竞争)
arbitrate(failedNodes);
}return 0;
}
4. 代码核心逻辑解释========== 仲裁开始 ==========
参与节点: Node1(ID=291) Node2(ID=255) Node3(ID=256)
位 0 (从高位起): Node1=0 Node2=0 Node3=0 | 总线=0
位 1 (从高位起): Node1=0 Node2=0 Node3=0 | 总线=0
位 2 (从高位起): Node1=1 Node2=0 Node3=1 | 总线=0
-> Node1 仲裁失败,退出
-> Node3 仲裁失败,退出
位 3 (从高位起): Node2=1 | 总线=1
位 4 (从高位起): Node2=1 | 总线=1
位 5 (从高位起): Node2=1 | 总线=1
位 6 (从高位起): Node2=1 | 总线=1
位 7 (从高位起): Node2=1 | 总线=1
位 8 (从高位起): Node2=1 | 总线=1
位 9 (从高位起): Node2=1 | 总线=1
位 10 (从高位起): Node2=1 | 总线=1
★★★ 仲裁胜出: Node2 (ID=255) ★★★
========== 仲裁结束 ==========
仲裁结果:
Node1 (ID=291) : 仲裁失败,等待重发
Node2 (ID=255) : 获胜,继续发送数据帧
Node3 (ID=256) : 仲裁失败,等待重发
[模拟] 总线空闲后,失败节点自动重发...
========== 仲裁开始 ==========
参与节点: Node1(ID=291) Node3(ID=256)
位 0: Node1=0 Node3=0 | 总线=0
位 1: Node1=0 Node3=0 | 总线=0
位 2: Node1=1 Node3=1 | 总线=1
位 3: Node1=0 Node3=0 | 总线=0
-> Node1 仲裁失败,退出
位 4: Node3=0 | 总线=0
位 5: Node3=0 | 总线=0
位 6: Node3=0 | 总线=0
位 7: Node3=0 | 总线=0
位 8: Node3=0 | 总线=0
位 9: Node3=0 | 总线=0
位 10: Node3=0 | 总线=0★★★ 仲裁胜出: Node3 (ID=256) ★★★
========== 仲裁结束 ==========
**
getIdBits()**:将 11 位 ID 转换为二进制字符串,高位在前,便于逐位比较。**
arbitrate()**:收集所有节点发送的当前位。
计算总线状态(线与:任意 0 → 总线 0)。
检查每个活跃节点:如果自己发的是 1 但总线是 0,则仲裁失败,标记
sending=false。继续下一位,直到所有位比较完毕或只剩一个节点。
失败重试 :主函数中模拟了失败节点在总线空闲后重新参与仲裁(实际 CAN 控制器硬件自动完成)。
非破坏性仲裁 :低 ID 胜出,高 ID 自动退让, 无数据冲突 。
实时性保障 :高优先级消息最多等待一个最长帧时间(如 134 位时间)即可发送。
C++ 模拟 :清晰展示了逐位比较、显性覆盖隐性的过程,以及失败节点的自动重试机制。
这个机制使得 CAN 总线在工业控制、汽车电子等领域成为事实标准,完美兼顾了实时性和可靠性。
热门跟贴