feat(comm): 1.6.21 段2改电机整轮密排, 删段4历史覆盖窗
- 段2(32..543): count=实际电机数(0..9), 空节点不占位, 槽序按 node_id 升序 - 同号多通道取 seq 最新; magic@35=0xA5 区分密排与旧滑窗 - 差分填充: 节点集合+seq/online 未变时整段复用影子 - 删段4 fillRxWindow; 非STATUS仍走段1/3最近一帧 - 输入PDO总长仍600B, SII无需重刷; 主站需按count密排解码 - 动机: 接3电机却看到5状态帧(空槽占位); win_base滑窗对已识别电机无意义
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @file ethercat_can_bridge.cpp
|
||||
* @brief EtherCAT PDO ↔ CAN 转发桥实现(1.5.56)。
|
||||
* @brief EtherCAT PDO ↔ CAN 转发桥实现(1.6.21:段 2 电机整轮密排)。
|
||||
*/
|
||||
#include "comm/ethercat_can_bridge.h"
|
||||
#include "device/canfd.h"
|
||||
@@ -16,7 +16,6 @@ volatile bool EthercatCanBridge::s_seqPrimed = false;
|
||||
volatile uint8_t EthercatCanBridge::s_lastTxResult = 0U;
|
||||
volatile uint16_t EthercatCanBridge::s_txCount = 0U;
|
||||
volatile uint16_t EthercatCanBridge::s_dropCount = 0U;
|
||||
volatile uint8_t EthercatCanBridge::s_winBase = 0U;
|
||||
|
||||
volatile bool EthercatCanBridge::s_rxValid = false;
|
||||
volatile bool EthercatCanBridge::s_rxPublished = false;
|
||||
@@ -28,15 +27,15 @@ volatile uint32_t EthercatCanBridge::s_rxId = 0U;
|
||||
volatile uint32_t EthercatCanBridge::s_rxCount = 0U;
|
||||
uint8_t EthercatCanBridge::s_rxData[kEcatCanXRxDataMax] = {0};
|
||||
|
||||
EthercatCanBridge::RxWinSlot
|
||||
EthercatCanBridge::s_rxWin[ECAT_RXB_SLOTS] = {};
|
||||
volatile uint8_t EthercatCanBridge::s_rxWinWr = 0U;
|
||||
volatile uint32_t EthercatCanBridge::s_rxWinSeq = 0U;
|
||||
volatile uint8_t EthercatCanBridge::s_rxWinOvf = 0U;
|
||||
volatile uint8_t EthercatCanBridge::s_rxWinSeen[ECAT_RXB_SLOTS] = {0};
|
||||
uint8_t EthercatCanBridge::s_motShadow[kEcatMotRegionBytes] = {0};
|
||||
uint32_t EthercatCanBridge::s_motPubSeq[kMotorMaxNodes + 1U] = {0};
|
||||
uint8_t EthercatCanBridge::s_motPubOnline[kMotorMaxNodes + 1U] = {0};
|
||||
uint16_t EthercatCanBridge::s_motPubMask = 0U;
|
||||
uint8_t EthercatCanBridge::s_motPubCount = 0U;
|
||||
bool EthercatCanBridge::s_motShadowValid = false;
|
||||
|
||||
namespace {
|
||||
/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端,不依赖编译器结构体布局。 */
|
||||
/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端。 */
|
||||
inline uint32_t rd32le(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
||||
@@ -51,13 +50,19 @@ inline void wr16le(uint8_t *p, uint16_t v)
|
||||
{
|
||||
p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8);
|
||||
}
|
||||
|
||||
/** 段 2 头 + count 槽在影子区中的总长度。 */
|
||||
inline uint16_t motRegionLen(uint8_t count)
|
||||
{
|
||||
return (uint16_t)(kEcatMotHdrSize + (uint16_t)count * kEcatMotSlotSize);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
||||
{
|
||||
uint8_t want = out[ECAT_TXB_OFF_COUNT];
|
||||
if (want == 0U) {
|
||||
return 0U; /* 未使用批量区:行为与 1.5.55 完全一致 */
|
||||
return 0U;
|
||||
}
|
||||
if (want > kEcatTxBatchMax) {
|
||||
want = kEcatTxBatchMax;
|
||||
@@ -68,8 +73,6 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
||||
uint16_t off = ECAT_TXB_OFF_ITEMS;
|
||||
|
||||
for (uint8_t i = 0U; i < want; ++i) {
|
||||
/* 不信任主站给的 count/dlc:每步都验证头与数据都完整落在 PDO 内。
|
||||
* 越界即停 —— 这是唯一的边界判据,count 只是上限提示。 */
|
||||
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE) > kEcatPdoOutBytes) {
|
||||
break;
|
||||
}
|
||||
@@ -87,9 +90,6 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
||||
if (!CanFd::isChannelEnabled(ch)) {
|
||||
r = CanFd::CanTxResult::kErrInvalid;
|
||||
} else {
|
||||
/* 与单帧路径同样用 enqueueTx:批量下发必然连续打同一通道,
|
||||
* transmitFrame 的 allow_abort 会让后帧顶掉前帧(历史上 7 帧
|
||||
* 只发出 3 帧的根因),队列语义才不静默丢帧。 */
|
||||
r = CanFd::enqueueTx(ch, rd32le(&f[ECAT_FRM_OFF_ID]),
|
||||
&f[ECAT_FRM_HDR_SIZE], dlc);
|
||||
}
|
||||
@@ -108,55 +108,109 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
||||
return sent;
|
||||
}
|
||||
|
||||
void EthercatCanBridge::fillRxWindow(uint8_t *in)
|
||||
/**
|
||||
* 段 2:电机整轮密排。
|
||||
*
|
||||
* 1. 扫 MotorState,每个节点号 1..9 在 3 个通道里取 seq 最新的一条;
|
||||
* 2. 若节点集合与各节点 seq 均与上一拍发布相同 → 整段复用影子区(跳过重读);
|
||||
* 3. 否则按 node_id 升序密排写入,空节点不占位;seqlock 奇数(写入中)的槽
|
||||
* 本拍省略,下拍自愈 —— 不发半新半旧的 48B。
|
||||
*/
|
||||
uint8_t EthercatCanBridge::fillMotorRound(uint8_t *in)
|
||||
{
|
||||
/* ── 1.6.15:定长覆盖窗口,整块取值 ──
|
||||
* 生产者不等人:每拍把 N 个槽原样拷进 PDO,槽里是谁就是谁。同一帧会在连续
|
||||
* 几拍里重复出现,直到被下一帧覆盖。语义是"最近 N 帧窗口",不是"保证不丢的
|
||||
* 事件流"——主站按每槽 seq 去重后才归一成事件流上报给上层。
|
||||
*
|
||||
* 相比旧的环形队列排空,这里没有 tail/head/判满/pending:1.6.9 记录的那个
|
||||
* "滞后的 tail 让生产者写进消费者刚读过的槽"错标竞态,其载体(tail 握手)
|
||||
* 在本布局里根本不存在。定长也让主站不必推游标,1.6.10 那类边界越界写诊断区
|
||||
* 的错在结构上不可能再犯。
|
||||
*
|
||||
* 每槽 seqlock 判稳:奇数=生产者正在写这 12B 头 + 32B 数据,本拍留零跳过该槽,
|
||||
* 下拍自愈(与段 1/段 2/段 3 既有约定一致)。本函数跑在 PDI_Isr/Sync0_Isr
|
||||
* (preempt=0),单核下执行期间生产者必然冻结,判一次奇偶即可定性。 */
|
||||
in[ECAT_RXB_OFF_SLOT_COUNT] = ECAT_RXB_SLOTS;
|
||||
in[ECAT_RXB_OFF_WR_IDX] = s_rxWinWr;
|
||||
in[ECAT_RXB_OFF_OVF] = s_rxWinOvf;
|
||||
s_rxWinOvf = 0U; /* 回填后清零:OVF 是"自上次回填以来"的增量 */
|
||||
const MotorStateTable &mt = MotorState::table();
|
||||
|
||||
for (uint8_t i = 0U; i < ECAT_RXB_SLOTS; ++i) {
|
||||
const RxWinSlot &sl = s_rxWin[i];
|
||||
const uint32_t seq = sl.seq;
|
||||
if ((seq & 1U) != 0U) {
|
||||
continue; /* 生产者正在写这槽:本拍不报,下拍自愈 */
|
||||
/* ── 1. 每节点选一条最新槽 ── */
|
||||
const MotorSlot *picks[kMotorMaxNodes];
|
||||
uint8_t nodes[kMotorMaxNodes];
|
||||
uint8_t n = 0U;
|
||||
|
||||
for (uint8_t node = 1U; node <= kMotorMaxNodes; ++node) {
|
||||
const MotorSlot *best = nullptr;
|
||||
for (uint8_t ch = 0U; ch < kMotorChannels; ++ch) {
|
||||
const MotorSlot &s =
|
||||
mt.slots[(uint8_t)((node - 1U) * kMotorChannels + ch)];
|
||||
if (s.node_id == 0U) {
|
||||
continue;
|
||||
}
|
||||
if (best == nullptr || s.seq >= best->seq) {
|
||||
best = &s;
|
||||
}
|
||||
}
|
||||
__DMB(); /* 判稳先于取值:不许把下面的读重排到奇偶检查之前 */
|
||||
if ((sl.flags & 0x01U) == 0U) {
|
||||
continue; /* 从未写过:留零,主站按 flags bit0 判空 */
|
||||
if (best != nullptr) {
|
||||
picks[n] = best;
|
||||
nodes[n] = node;
|
||||
++n;
|
||||
}
|
||||
|
||||
/* RAM 侧存全量 64B,只有拷进 PDO 这一步按 32B 截断;DLC 字节填**原始值**,
|
||||
* 主站据此 + flags bit1 判断是否被截。 */
|
||||
const uint8_t dlc = sl.dlc;
|
||||
const uint8_t wire = (dlc > kEcatRxBatchDataMax) ? kEcatRxBatchDataMax : dlc;
|
||||
uint8_t *p = &in[ECAT_RXB_OFF_ITEMS + (uint16_t)i * ECAT_RXB_SLOT_SIZE];
|
||||
|
||||
p[ECAT_RXW_OFF_CH] = sl.ch;
|
||||
p[ECAT_RXW_OFF_DLC] = dlc;
|
||||
p[ECAT_RXW_OFF_FLAGS] = (uint8_t)(0x01U |
|
||||
(dlc > kEcatRxBatchDataMax ? 0x02U : 0x00U));
|
||||
wr32le(&p[ECAT_RXW_OFF_ID], sl.id);
|
||||
wr32le(&p[ECAT_RXW_OFF_SEQ], seq);
|
||||
memcpy(&p[ECAT_RXW_OFF_DATA], sl.data, wire);
|
||||
|
||||
/* 标记"主站已取过":生产者下次覆盖本槽时就不算丢帧。只有"未取先覆"
|
||||
* 才计 OVF —— 覆盖已送达的帧是窗口的设计意图,不是损失。 */
|
||||
s_rxWinSeen[i] = 1U;
|
||||
}
|
||||
|
||||
/* ── 2. 判「上一轮已填充且未变」──
|
||||
* 节点集合一致 + 每节点 seq/online 一致 + 头字段(active/boot)一致 + 写侧
|
||||
* seqlock 偶数 → 复用影子。online 必须比:checkOffline 超时置 0 时不 bump
|
||||
* seq,只比 seq 会把「已离线」永远报成在线。 */
|
||||
bool unchanged = s_motShadowValid && (n == s_motPubCount)
|
||||
&& mt.active_count == s_motShadow[ECAT_MOTW_OFF_ACTIVE - ECAT_MOTW_OFF_COUNT]
|
||||
&& mt.boot_active == s_motShadow[ECAT_MOTW_OFF_BOOT - ECAT_MOTW_OFF_COUNT]
|
||||
&& s_motShadow[ECAT_MOTW_OFF_MAGIC - ECAT_MOTW_OFF_COUNT]
|
||||
== kEcatMotLayoutMagic;
|
||||
if (unchanged) {
|
||||
for (uint8_t i = 0U; i < n; ++i) {
|
||||
const uint8_t node = nodes[i];
|
||||
const MotorSlot &s = *picks[i];
|
||||
if ((s.wr_seq & 1U) != 0U
|
||||
|| s.seq != s_motPubSeq[node]
|
||||
|| s.online != s_motPubOnline[node]
|
||||
|| (s_motPubMask & (uint16_t)(1U << node)) == 0U) {
|
||||
unchanged = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const uint16_t region = motRegionLen(unchanged ? s_motPubCount : n);
|
||||
|
||||
if (unchanged) {
|
||||
memcpy(&in[ECAT_MOTW_OFF_COUNT], s_motShadow, region);
|
||||
return s_motPubCount;
|
||||
}
|
||||
|
||||
/* ── 3. 重建密排区 ── */
|
||||
in[ECAT_MOTW_OFF_ACTIVE] = mt.active_count;
|
||||
in[ECAT_MOTW_OFF_BOOT] = mt.boot_active;
|
||||
in[ECAT_MOTW_OFF_MAGIC] = kEcatMotLayoutMagic;
|
||||
wr32le(&in[ECAT_MOTW_OFF_GLOBAL_SEQ], mt.global_seq);
|
||||
|
||||
uint8_t w = 0U;
|
||||
uint16_t newMask = 0U;
|
||||
for (uint8_t i = 0U; i < n; ++i) {
|
||||
const MotorSlot &s = *picks[i];
|
||||
if ((s.wr_seq & 1U) != 0U) {
|
||||
continue; /* 写入中:本拍省略,下拍自愈 */
|
||||
}
|
||||
__DMB();
|
||||
|
||||
uint8_t *p = &in[ECAT_MOTW_OFF_SLOTS + (uint16_t)w * kEcatMotSlotSize];
|
||||
p[ECAT_MOT_OFF_NODE] = nodes[i];
|
||||
p[ECAT_MOT_OFF_CH] = s.channel;
|
||||
p[ECAT_MOT_OFF_ONLINE] = s.online;
|
||||
p[ECAT_MOT_OFF_DLC] = s.dlc;
|
||||
wr32le(&p[ECAT_MOT_OFF_SEQ], s.seq);
|
||||
memcpy(&p[ECAT_MOT_OFF_STATUS], s.data, kMotorStatusDlc);
|
||||
|
||||
s_motPubSeq[nodes[i]] = s.seq;
|
||||
s_motPubOnline[nodes[i]] = s.online;
|
||||
newMask |= (uint16_t)(1U << nodes[i]);
|
||||
++w;
|
||||
}
|
||||
|
||||
in[ECAT_MOTW_OFF_COUNT] = w;
|
||||
|
||||
/* 更新影子:下次集合+seq 未变时整段复用 */
|
||||
memcpy(s_motShadow, &in[ECAT_MOTW_OFF_COUNT], motRegionLen(w));
|
||||
s_motPubMask = newMask;
|
||||
s_motPubCount = w;
|
||||
s_motShadowValid = true;
|
||||
return w;
|
||||
}
|
||||
|
||||
bool EthercatCanBridge::peekOutput(const uint8_t *head)
|
||||
@@ -166,20 +220,14 @@ bool EthercatCanBridge::peekOutput(const uint8_t *head)
|
||||
}
|
||||
const uint8_t seq = head[ECAT_CAN_TX_OFF_SEQ];
|
||||
|
||||
/* 窗口下标每周期都必须刷新(主站靠滚动 WIN 扫 27 槽),与 seq 沿无关。
|
||||
* 越界值钳到 0,避免读到槽数组外(与 processOutput 同款钳制)。 */
|
||||
const uint8_t win = head[ECAT_CAN_TX_OFF_WIN];
|
||||
s_winBase = (win < kMotorSlotCount) ? win : 0U;
|
||||
/* WIN@8 不再使用:1.6.21 电机整轮密排,无滑窗。 */
|
||||
|
||||
/* 上电/重连后首帧只记基准(与 processOutput 的 priming 分支语义一致) */
|
||||
if (!s_seqPrimed) {
|
||||
s_seqPrimed = true;
|
||||
s_lastSeq = seq;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* seq 沿 → 全量读;FLAGS bit0(清统计请求)允许不带 seq 沿(同 processOutput)。
|
||||
* s_lastSeq 不在这里推进:全量路径上 processOutput 的沿处理只做一次。 */
|
||||
if (seq != s_lastSeq) {
|
||||
return true;
|
||||
}
|
||||
@@ -194,32 +242,21 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
|
||||
|
||||
const uint8_t seq = out[ECAT_CAN_TX_OFF_SEQ];
|
||||
|
||||
/* WIN 不在这里取:1.6.0 起 out 只在 seq 沿那拍才是新鲜的,无沿的周期里它是
|
||||
* 上一次全量读的残值。在这里回写 s_winBase,等于用开机首拍的 0 每拍盖掉
|
||||
* peekOutput() 刚取到的新值 —— 窗口被钉死在基址 0,槽位 7..26(node3 及
|
||||
* 以上)永不上报,直到主站真发一帧触发一次全量读才复位。
|
||||
* 每拍刷新由 peekOutput()(SSC PDO_OutputMapping 的 12B 探针)独占负责。 */
|
||||
|
||||
/* 上电/重连后第一次看到输出数据时只记录基准,不发送:否则主站残留的
|
||||
* 非零 seq 会被当成一次新请求,在进 OP 的瞬间凭空发出一帧。 */
|
||||
if (!s_seqPrimed) {
|
||||
s_seqPrimed = true;
|
||||
s_lastSeq = seq;
|
||||
return;
|
||||
}
|
||||
|
||||
/* flags bit0:清零统计(一次性,不依赖 seq 沿,便于主站随时归零) */
|
||||
if ((out[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U) {
|
||||
s_txCount = 0U;
|
||||
s_dropCount = 0U;
|
||||
s_rxCount = 0U;
|
||||
/* 快照区里残留的那帧早已发布过;不置 published 会让清零后的第一帧
|
||||
* 被误计成一次"未取先覆",清零口径就脏了。 */
|
||||
s_rxPublished = true;
|
||||
}
|
||||
|
||||
if (seq == s_lastSeq) {
|
||||
return; /* 没有新的发送请求 */
|
||||
return;
|
||||
}
|
||||
s_lastSeq = seq;
|
||||
|
||||
@@ -229,17 +266,10 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
|
||||
dlc = kEcatCanTxDataMax;
|
||||
}
|
||||
|
||||
/* 单帧区先发(保持 1.5.55 语义),再发批量区。
|
||||
* dlc==0 表示本次只用批量区,不发单帧 —— 否则会凭空发出一帧 0 长度报文。 */
|
||||
if (dlc > 0U) {
|
||||
/* isChannelEnabled 内含越界检查,是"通道号合法且可用"的唯一判据 */
|
||||
if (!CanFd::isChannelEnabled(ch)) {
|
||||
s_lastTxResult = (uint8_t)CanFd::CanTxResult::kErrInvalid;
|
||||
} else {
|
||||
/* 用 enqueueTx 而非 transmitFrame:PDO 是周期性场景,可能连续多帧打到
|
||||
* 同一通道。transmitFrame 默认 allow_abort=true 会在邮箱满时顶掉已装载
|
||||
* 的旧帧(历史上 7 帧突发只发出 3 帧的根因);enqueueTx 队列空时直通、
|
||||
* 满时排队,不静默丢帧。 */
|
||||
const CanFd::CanTxResult r =
|
||||
CanFd::enqueueTx(ch, rd32le(&out[ECAT_CAN_TX_OFF_ID]),
|
||||
&out[ECAT_CAN_TX_OFF_DATA], dlc);
|
||||
@@ -250,7 +280,6 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
|
||||
}
|
||||
}
|
||||
|
||||
/* 批量区:同一个 seq 沿里一次性发 N 帧,供多轴同步下发。 */
|
||||
(void)sendBatch(out);
|
||||
}
|
||||
|
||||
@@ -259,21 +288,15 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
if (in == nullptr) {
|
||||
return;
|
||||
}
|
||||
/* 必须按新的全长清零。旧代码写死 memset(in,0,32),扩容后会把 32 字节以后
|
||||
* 的槽区留成上一周期的残留数据,主站读到的是"看起来合法但已过期"的电机状态。 */
|
||||
memset(in, 0, kEcatPdoInBytes);
|
||||
|
||||
/* ── 段 1(0..31):桥状态 + 短快照,布局与 1.5.53 逐字节兼容 ── */
|
||||
/* ── 段 1(0..31):桥状态 + 短快照 ── */
|
||||
in[ECAT_CAN_RX_OFF_ACK] = s_lastSeq;
|
||||
in[ECAT_CAN_RX_OFF_TXRES] = s_lastTxResult;
|
||||
wr16le(&in[ECAT_CAN_RX_OFF_TXCNT], s_txCount);
|
||||
wr16le(&in[ECAT_CAN_RX_OFF_DROP], s_dropCount);
|
||||
wr32le(&in[ECAT_CAN_RX_OFF_RXCNT], s_rxCount);
|
||||
|
||||
/* seqlock 判稳(见 onCanFrame):奇数=生产者正在写这组快照。单核下 ISR
|
||||
* 执行期间生产者冻结,读一次即可定性;本周期跳过段 1/段 3(开头已清零,
|
||||
* 语义=无有效快照),下周期自愈。批量区(段 4)每槽另有自己的 seqlock,
|
||||
* 不受本组影响。 */
|
||||
const bool xrxStable = ((s_xrxSeq & 1U) == 0U);
|
||||
|
||||
if (xrxStable && s_rxValid) {
|
||||
@@ -285,10 +308,7 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
in[ECAT_CAN_RX_OFF_DLC] = s_rxDlc;
|
||||
in[ECAT_CAN_RX_OFF_FLAGS] = flags;
|
||||
wr32le(&in[ECAT_CAN_RX_OFF_ID], s_rxId);
|
||||
/* 段 1 只放前 8 字节(兼容),完整 payload 见段 3 */
|
||||
memcpy(&in[ECAT_CAN_RX_OFF_DATA], s_rxData, kEcatCanRxDataMax);
|
||||
/* 本拍已把这一帧发布进过程数据映像(段 3 与本块同门控,故只在此处置位一次):
|
||||
* 之后再被新帧覆盖属于"最新值"设计意图,不算丢。 */
|
||||
s_rxPublished = true;
|
||||
}
|
||||
|
||||
@@ -296,56 +316,11 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
const uint32_t backlog = CanFd::isChannelEnabled(ch) ? CanFd::txQueueBacklog(ch) : 0U;
|
||||
in[ECAT_CAN_RX_OFF_BACKLOG] = (backlog > 255U) ? 255U : (uint8_t)backlog;
|
||||
|
||||
/* ── 段 2(32..319):电机状态窗口 ──
|
||||
* 直接读 MotorState 聚合表,不再依赖"最近一帧 CAN"。窗口起点由主站通过
|
||||
* 输出 PDO 的 WIN 字节指定,主站每周期 +5 即可 6 周期扫完 27 个槽。
|
||||
*
|
||||
* 上下文安全(1.6.5 修正):状态表由主循环的 taskCanRxParse 写,而本函数跑在
|
||||
* PDI_Isr(SM 同步模式)/Sync0_Isr(DC 模式),会抢占写侧那次
|
||||
* memcpy(slot.data, 48B)。旧注释称"最坏是 seq 与 data 不同步一拍,主站按 seq
|
||||
* 判新旧即可自愈"是错的:写侧先 seq 后 data,抢占留下的恰好是"新 seq +
|
||||
* 半新半旧 data",而主站只拿 seq 判新鲜度,分辨不出同一 seq 下 payload 换过
|
||||
* 内容,混合值照原样进界面(边界落在 B16/B20/B24 这类 float 符号/指数字节上
|
||||
* 就是离谱值)。改用与段 1/段 3 同一套 seqlock(MotorSlot.wr_seq):奇数即
|
||||
* 跳过本槽,与本片既有的"本拍不报、下拍自愈"约定一致。 */
|
||||
const MotorStateTable &mt = MotorState::table();
|
||||
const uint8_t base = (s_winBase < kMotorSlotCount) ? s_winBase : 0U;
|
||||
/* ── 段 2(32..543):电机整轮密排(空槽不发,count=实际电机数)── */
|
||||
(void)fillMotorRound(in);
|
||||
|
||||
in[ECAT_MOTW_OFF_ACTIVE] = mt.active_count;
|
||||
in[ECAT_MOTW_OFF_BOOT] = mt.boot_active;
|
||||
in[ECAT_MOTW_OFF_WIN_BASE] = base;
|
||||
wr32le(&in[ECAT_MOTW_OFF_GLOBAL_SEQ], mt.global_seq);
|
||||
|
||||
uint8_t count = 0U;
|
||||
for (uint8_t i = 0U; i < kEcatMotSlotsPerPdo; ++i) {
|
||||
const uint8_t idx = (uint8_t)(base + i);
|
||||
if (idx >= kMotorSlotCount) {
|
||||
break; /* 窗口尾部越界即停,不回绕:主站据 win_count 判断 */
|
||||
}
|
||||
const MotorSlot &s = mt.slots[idx];
|
||||
uint8_t *p = &in[ECAT_MOTW_OFF_SLOTS + (uint16_t)i * kEcatMotSlotSize];
|
||||
|
||||
/* 槽按 i 定位,跳过的那格照样占位,所以 win_count 报的是跨度而不是实际
|
||||
* 填充数:主站 decode_input 对 node_id==0 的块直接 continue。 */
|
||||
count = static_cast<uint8_t>(i + 1U);
|
||||
if ((s.wr_seq & 1U) != 0U) {
|
||||
continue; /* 生产者正在写这 48B:本拍留零,下拍自愈 */
|
||||
}
|
||||
__DMB(); /* 判稳先于取值:不许把下面的读重排到奇偶检查之前 */
|
||||
|
||||
p[ECAT_MOT_OFF_NODE] = s.node_id;
|
||||
p[ECAT_MOT_OFF_CH] = s.channel;
|
||||
p[ECAT_MOT_OFF_ONLINE] = s.online;
|
||||
p[ECAT_MOT_OFF_DLC] = s.dlc;
|
||||
wr32le(&p[ECAT_MOT_OFF_SEQ], s.seq);
|
||||
memcpy(&p[ECAT_MOT_OFF_STATUS], s.data, kMotorStatusDlc);
|
||||
}
|
||||
in[ECAT_MOTW_OFF_WIN_COUNT] = count;
|
||||
|
||||
/* ── 段 3(320..359):最近一帧完整快照 ──
|
||||
* 版本帧(0x00FF)/CONFIG 帧(0x0101) 都是 DLC 24,段 1 的 8 字节装不下。
|
||||
* 与段 1 共用 xrxStable 门控:两段必须来自同一帧,否则 ecat.html 的短快照
|
||||
* 和电机页的长快照会各自解出半新半旧的 ID/数据。 */
|
||||
/* ── 段 3(544..583):最近一帧完整快照 ──
|
||||
* 与段 1 共用 xrxStable 门控,两段必须来自同一帧。 */
|
||||
if (xrxStable && s_rxValid) {
|
||||
in[ECAT_XRX_OFF_CH] = s_rxCh;
|
||||
in[ECAT_XRX_OFF_DLC] = s_rxDlc;
|
||||
@@ -354,11 +329,7 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax);
|
||||
}
|
||||
|
||||
/* ── 段 4(360..583):CANFD 批量接收覆盖窗口 ──
|
||||
* 段 1/段 3 只带"最近一帧",高速多帧场景下周期间到达的帧会互相覆盖
|
||||
* (s_dropCount 就是在数这个)。批量区保留最近 N 帧的窗口,让主站在一个
|
||||
* 周期内拿到帧序列而不是单个采样点;1.6.15 起改为定长覆盖(见 fillRxWindow)。 */
|
||||
fillRxWindow(in);
|
||||
/* 段 4(历史覆盖窗口)已删除;段 5 由 EthercatPdo::fillDiagTail 写。 */
|
||||
}
|
||||
|
||||
void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
@@ -368,33 +339,15 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
return;
|
||||
}
|
||||
|
||||
/* 缓存区已扩到 32B(段 3 用),truncated 仍按段 1 的 8B 口径上报,
|
||||
* 这样 ecat.html 的 CAN 调试页语义不变。 */
|
||||
uint8_t copyLen = length;
|
||||
if (copyLen > kEcatCanXRxDataMax) {
|
||||
copyLen = kEcatCanXRxDataMax;
|
||||
}
|
||||
|
||||
/* ── 快照 seqlock(1.5.58)──
|
||||
* s_rxData/s_rxCh/s_rxId/s_rxDlc/s_rxTruncated/s_rxValid 是一组,必须整体
|
||||
* 被 buildInput 读到。DC 模式下 buildInput 跑在 Sync0_Isr(preempt=0),会
|
||||
* 抢占本函数(主循环 taskCanRxParse)。原注释声称"valid 最后置位保证一致"
|
||||
* 是错的:valid 一旦置位就永不清零,ISR 落在"新 payload 已写完、id 还没
|
||||
* 更新"的窗口里,会把新帧数据配旧帧 ID 发出去——STATUS 字节被标成 CONFIG
|
||||
* (0x000N0101),前端按 CONFIG 布局解码出 -3e26 这类离谱目标值。
|
||||
* 协议:seq 奇数=正在写。消费者在单核 ISR 里生产者必然冻结,判一次奇偶
|
||||
* 即可,奇数则本周期跳过(段 1/段 3 已清零=无效),下周期自愈。
|
||||
* 不关中断:critical.h 的 BASEPRI 方案放行 preempt=0,挡不住 Sync0;
|
||||
* 而 seqlock 零抖动,与本文件既有无锁约定一致。
|
||||
* __DMB 防编译器把非 volatile 的 memcpy 挪出两次 seq 自增之间。 */
|
||||
++s_xrxSeq; /* → 奇数:快照进入"正在写" */
|
||||
/* ── 快照 seqlock:奇数=正在写,消费者本拍跳过段 1/段 3 ── */
|
||||
++s_xrxSeq;
|
||||
__DMB();
|
||||
|
||||
/* 「未取先覆」计数:上一帧还没被任何一拍 PDO 发布就被覆盖才算丢。
|
||||
* 原判据只判 s_rxValid,而它置位后永不清零 → 第一帧之后每帧都计,
|
||||
* drop 恒等于 rx−1(u16 回绕),变成跟着收帧速率涨、零灵敏度的假仪表。
|
||||
* 放在奇数窗口内:消费者判到奇会跳过段 1/段 3、不会在此刻置 published,
|
||||
* 故"读上一帧发布状态 + 自增"对消费者等效原子,不会多计一次。 */
|
||||
if (s_rxValid && !s_rxPublished) {
|
||||
++s_dropCount;
|
||||
}
|
||||
@@ -406,67 +359,17 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
|
||||
s_rxCh = channel;
|
||||
s_rxId = id;
|
||||
s_rxDlc = length; /* 报告原始长度,便于主站发现被截断 */
|
||||
s_rxDlc = length;
|
||||
s_rxTruncated = (length > kEcatCanRxDataMax);
|
||||
++s_rxCount;
|
||||
s_rxValid = true;
|
||||
s_rxPublished = false; /* 这一帧还没被任何一拍 PDO 发布 */
|
||||
s_rxPublished = false;
|
||||
|
||||
__DMB();
|
||||
++s_xrxSeq; /* → 偶数:快照整体一致 */
|
||||
++s_xrxSeq;
|
||||
|
||||
/* ── 写入批量接收覆盖窗口(1.6.15,取代 1.5.56 的环形队列)──
|
||||
* 与上面的"最近一帧快照"并行维护:快照只留最新一帧,窗口留最近 N 帧的序列。
|
||||
* 生产者按自己的游标写、绕圈覆盖,**不等消费者** —— 没有判满、没有背压、
|
||||
* 没有 pending。代价是语义从"事件流"变成"最近 N 帧窗口":一帧在窗口里活到
|
||||
* 被覆盖为止,主站按每槽 seq 去重后才归一成事件流。
|
||||
*
|
||||
* **STATUS 帧不入窗**(1.6.9):批量区存在的意义是捞那些段 2 装不下的帧
|
||||
* (CONFIG / 版本 / 节点应答),而完整 STATUS 本来就由段 2 电机状态窗口按槽
|
||||
* 上报;主站 transport-ecat.js 的 drainRx() 拿到 STATUS 也是直接 continue
|
||||
* 丢掉。让它占窗口是纯重复,而且正是它把队列压爆的:实测 CAN 侧约 4000 帧/s,
|
||||
* 旧批量区每拍最多回收 3 条(=3000 帧/s),净积压使队列长期饱和,才逼出了
|
||||
* 生产者覆写消费者槽位的竞态(错标率 1.77%,表现为"CONFIG 的 ID 配 STATUS
|
||||
* 的内容")。滤掉 STATUS 后入窗流量降到 CONFIG 量级。
|
||||
*
|
||||
* OVF 只数"**未取先覆**":覆盖一帧主站已经取走的数据是窗口的设计意图(不计),
|
||||
* 覆盖一帧主站还没来得及取的数据才是真丢帧(计数)。该数与主站按 seq 空洞
|
||||
* 算出的丢帧数应当一致 —— 两侧独立仪表互相对账,任一侧骗人都当场暴露。 */
|
||||
if ((id & 0xFFFFU) == 0x0100U) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
const uint8_t idx = s_rxWinWr;
|
||||
RxWinSlot &sl = s_rxWin[idx];
|
||||
|
||||
if (s_rxWinSeen[idx] == 0U && sl.seq != 0U && s_rxWinOvf < 0xFFU) {
|
||||
++s_rxWinOvf; /* 上一帧还没被任何一拍 PDO 取走就被覆盖 */
|
||||
}
|
||||
s_rxWinSeen[idx] = 0U;
|
||||
|
||||
/* 每槽 seqlock:奇数=正在写。消费者(PDI_Isr)落在这一瞬就跳过该槽,下拍
|
||||
* 自愈。没有它,抢占在"新 payload 已写完、id 还没更新"之间,主站会拿到
|
||||
* 一条现实中不存在、但看起来完全合法的假帧 —— 那不是"差一个周期"。
|
||||
* 发布值取自全局计数器 s_rxWinSeq,故跨槽可比大小、可数空洞。 */
|
||||
sl.seq = s_rxWinSeq + 1U; /* → 奇数:本槽进入"正在写" */
|
||||
__DMB();
|
||||
|
||||
uint8_t n = length;
|
||||
if (n > kEcatCanFdDataMax) {
|
||||
n = kEcatCanFdDataMax;
|
||||
}
|
||||
sl.ch = channel;
|
||||
sl.dlc = n;
|
||||
sl.id = id;
|
||||
sl.flags = 0x01U; /* bit0=本槽至少写过一帧 */
|
||||
memcpy(sl.data, data, n);
|
||||
|
||||
s_rxWinSeq += 2U; /* 下一个发布值(恒偶) */
|
||||
__DMB(); /* 字段写入先于 seq 发布落地 */
|
||||
sl.seq = s_rxWinSeq; /* → 偶数:整槽一致 */
|
||||
|
||||
s_rxWinWr = (uint8_t)((idx + 1U) % ECAT_RXB_SLOTS);
|
||||
}
|
||||
/* STATUS 走 MotorState → 段 2 整轮;非 STATUS 只更新段 1/3 最近一帧快照。
|
||||
* 1.6.21 起不再写入历史覆盖窗口。 */
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
Reference in New Issue
Block a user