feat(comm): 1.6.14~1.6.20 —— CAN-FD 数据段 5M、批量区改定长覆盖窗口(输入 PDO 496→600)、drop 口径修复、差分写仪表
1.6.14 CAN-FD 数据段 2Mbps→5Mbps:CONFIG_CAN_DATA_PRESCALER 9→3、BS1 3→4TQ (90MHz/(3×6TQ),采样点 80%→83.3%),仲裁段仍 1M。TDC filter/offset 维持 4 不做 等比缩放(TDCMOD=CALC_AND_OFFSET 下硬件已自动测回路延时;放到 12 会让本节点回采 错开约 6 个数据位,自发热帧被判错误帧)。配套电机 APP/BOOT data_prescaler=2。 1.6.15 段4 批量接收由"环形队列 + 变长紧排排空"改为 5×44B 定长覆盖窗口,输入 PDO 496→600(75×64bit),段5 诊断区 484→584。动机两条:现场 4000 帧/s 超旧容量 3 帧/拍 恒丢 1 帧;更关键是排空赤字把队列长期压在饱和态,而 1.6.9~1.6.13 追的错标/拼接竞态 正是饱和态喂出来的(判满用的 tail 滞后→生产者写进消费者刚读过的槽)。定长消灭 tail/判满/pending,边界越界写在结构上不可能再犯。每槽带 u32 seqlock:buildInput 在 PDI_Isr(preempt=0) 抢占主循环写侧,无它则主站会拿到"新 ID + 旧数据"拼出的假帧。 out+in=984 ≤ 1024 硬上限,DPRAM 留 120B。语义变更:批量区从"不丢的事件流"变"最近 N 帧窗口",去重与丢帧判据移交主站按 seq 归一。三端同步 size.h/bridge/dc_app/ gen_eeprom/DCU_ESI.xml,必须重烧固件 + 烧 EEPROM + 断电冷启动,只烧一边 SAFEOP 报 AL 0x001A/0x0011。 1.6.16 段1/段3 快照 drop 计数口径修复(纯判据,PDO 布局逐字节不动):旧判据 s_rxValid 置位后永不清零 ⇒ 开机首帧后每帧都算丢弃,drop ≡ rx−1,是零灵敏度假仪表 且把网页状态栏刷成 ERR。新增 s_rxPublished,判据改 s_rxValid && !s_rxPublished。 1.6.17~19 [DIFF-STATS] 差分写取证仪表,零行为变更,只加 0x2000 sub13..23(11 个只读 UINT32) ⇒ 不动 PDO 长度/映射,无需重生成 SII。实测:每拍 27.4% 字节真变化(164B)、 run 峰值 12~13、段2 占变化量 ~53%、段4 ~25%。 1.6.19 推翻 1.6.18 的自我诊断:比对开销虚报不是 -Oz 把 memcpy 编成调用(手拼移位 反而 21.3µs→41µs),真根因是 alignas(4) 到 const uint8_t* 形参处丢失,优化器无法 证明 4 字节对齐。__builtin_assume_aligned + memcpy 后循环体缩成 ldr/ldr/cmp/bne、 bl 归零。教训:测量工具自身的开销会直接变成结论。 1.6.20 基线对照版(当前提交状态 CONFIG_ECAT_DIFF_STATS=0):同工况(电机在线无指令) 实测 load 49.1% vs 仪表开的 51.6% ⇒ 仪表自身成本 2.4 个点,差分改造收益 = 0。 按实测变化量推算 A1(段粒度条件写) 只值约 1 点(段2/段4 每拍都变,必然整块重写), 仅 A2(8B 块粒度 + run 上限退化为整写) 有空间 —— 推算值,未做 A/B 实测不作为结论。
This commit is contained in:
@@ -19,6 +19,7 @@ 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;
|
||||
volatile uint32_t EthercatCanBridge::s_xrxSeq = 0U;
|
||||
volatile uint8_t EthercatCanBridge::s_rxCh = 0U;
|
||||
volatile uint8_t EthercatCanBridge::s_rxDlc = 0U;
|
||||
@@ -27,11 +28,12 @@ volatile uint32_t EthercatCanBridge::s_rxId = 0U;
|
||||
volatile uint32_t EthercatCanBridge::s_rxCount = 0U;
|
||||
uint8_t EthercatCanBridge::s_rxData[kEcatCanXRxDataMax] = {0};
|
||||
|
||||
EthercatCanBridge::RxRingItem
|
||||
EthercatCanBridge::s_rxRing[kEcatRxRingSlots] = {};
|
||||
volatile uint8_t EthercatCanBridge::s_rxRingHead = 0U;
|
||||
volatile uint8_t EthercatCanBridge::s_rxRingTail = 0U;
|
||||
volatile uint8_t EthercatCanBridge::s_rxRingLost = 0U;
|
||||
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};
|
||||
|
||||
namespace {
|
||||
/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端,不依赖编译器结构体布局。 */
|
||||
@@ -106,64 +108,55 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
||||
return sent;
|
||||
}
|
||||
|
||||
void EthercatCanBridge::fillRxBatch(uint8_t *in)
|
||||
void EthercatCanBridge::fillRxWindow(uint8_t *in)
|
||||
{
|
||||
uint8_t count = 0U;
|
||||
uint16_t off = ECAT_RXB_OFF_ITEMS;
|
||||
uint8_t tail = s_rxRingTail;
|
||||
const uint8_t head = s_rxRingHead; /* 快照一次:生产者可能边填边推进 */
|
||||
/* ── 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 是"自上次回填以来"的增量 */
|
||||
|
||||
while (tail != head && count < kEcatRxBatchMax) {
|
||||
/* 整条先拷到局部副本再用:生产者(主循环 onCanFrame)与本函数(PDI_Isr)
|
||||
* 共用这块槽,直接按引用逐字段取会让 id/dlc/data 分属不同帧。 */
|
||||
const RxRingItem it = s_rxRing[tail];
|
||||
uint8_t dlc = it.dlc;
|
||||
if (dlc > kEcatCanFdDataMax) {
|
||||
dlc = kEcatCanFdDataMax;
|
||||
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.6.1: PDO 内数据条目 32B 上限——DLC 字节填**真值**,超出部分截断并打
|
||||
* flags bit1;环形队列里仍是全量 64B,截断只发生在 PDO 快照这一步。
|
||||
* 主站按 "8 + min(dlc, kEcatRxBatchDataMax)" 推进游标。 */
|
||||
uint8_t dlcWire = dlc;
|
||||
if (dlcWire > kEcatRxBatchDataMax) {
|
||||
dlcWire = kEcatRxBatchDataMax;
|
||||
}
|
||||
/* 装不下就停,**且不推进 tail** —— 该帧留到下个周期,不丢。
|
||||
* 边界是段 5 诊断区起点(ECAT_DIAG_OFF_LOAD=484),不是 PDO 全长(496):
|
||||
* 1.6.10 修正。旧判据允许条目尾部最多越进诊断区 12B,把负载率/撕裂计数/
|
||||
* 序号盖成帧数据 —— 当前 kEcatRxBatchMax=3 时最坏占用恰好 484 触不到,
|
||||
* 属潜伏 bug,但容量一旦按瘦身布局放宽(32B 步长)就会立刻咬上来。 */
|
||||
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE + dlcWire) > ECAT_DIAG_OFF_LOAD) {
|
||||
break;
|
||||
__DMB(); /* 判稳先于取值:不许把下面的读重排到奇偶检查之前 */
|
||||
if ((sl.flags & 0x01U) == 0U) {
|
||||
continue; /* 从未写过:留零,主站按 flags bit0 判空 */
|
||||
}
|
||||
|
||||
uint8_t *p = &in[off];
|
||||
p[ECAT_FRM_OFF_CH] = it.ch;
|
||||
p[ECAT_FRM_OFF_DLC] = dlc;
|
||||
p[ECAT_FRM_OFF_FLAGS] = (uint8_t)(0x01U | (dlc > kEcatRxBatchDataMax ? 0x02U : 0x00U));
|
||||
wr32le(&p[ECAT_FRM_OFF_ID], it.id);
|
||||
memcpy(&p[ECAT_FRM_HDR_SIZE], it.data, dlcWire);
|
||||
/* 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];
|
||||
|
||||
off = (uint16_t)(off + ECAT_FRM_HDR_SIZE + dlcWire);
|
||||
tail = (uint8_t)((tail + 1U) % kEcatRxRingSlots);
|
||||
++count;
|
||||
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);
|
||||
|
||||
/* **每条读完立刻发布 tail**(1.6.9)。原来攒到循环末才发布:生产者判满
|
||||
* 用的正是这个滞后的 s_rxRingTail,于是它以为队列里还有本拍已被读走的
|
||||
* 那几个空位没腾出来 —— 反过来说,它会把新帧写进消费者**刚读过但未发布**
|
||||
* 的槽。队列一旦饱和(实测 pending 恒 8/12、lost 几乎每拍都有、count 恒
|
||||
* 取满 3),这个窗口每拍都在,于是拷进 PDO 的 id/dlc/data 来自不同帧,
|
||||
* 表现为"CONFIG 的 ID 配 STATUS 的内容"这类错标(实测 1.77%)。
|
||||
* 逐条发布后生产者的判满始终基于真实进度,覆写窗口消失。 */
|
||||
__DMB(); /* 本条读取先于 tail 发布落地 */
|
||||
s_rxRingTail = tail;
|
||||
/* 标记"主站已取过":生产者下次覆盖本槽时就不算丢帧。只有"未取先覆"
|
||||
* 才计 OVF —— 覆盖已送达的帧是窗口的设计意图,不是损失。 */
|
||||
s_rxWinSeen[i] = 1U;
|
||||
}
|
||||
|
||||
const uint8_t pending = (uint8_t)((head + kEcatRxRingSlots - tail) % kEcatRxRingSlots);
|
||||
in[ECAT_RXB_OFF_COUNT] = count;
|
||||
in[ECAT_RXB_OFF_LOST] = s_rxRingLost;
|
||||
in[ECAT_RXB_OFF_PENDING] = pending;
|
||||
s_rxRingLost = 0U; /* 回填后清零:lost 是"自上次回填以来"的增量 */
|
||||
}
|
||||
|
||||
bool EthercatCanBridge::peekOutput(const uint8_t *head)
|
||||
@@ -220,6 +213,9 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
|
||||
s_txCount = 0U;
|
||||
s_dropCount = 0U;
|
||||
s_rxCount = 0U;
|
||||
/* 快照区里残留的那帧早已发布过;不置 published 会让清零后的第一帧
|
||||
* 被误计成一次"未取先覆",清零口径就脏了。 */
|
||||
s_rxPublished = true;
|
||||
}
|
||||
|
||||
if (seq == s_lastSeq) {
|
||||
@@ -276,7 +272,8 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
|
||||
/* seqlock 判稳(见 onCanFrame):奇数=生产者正在写这组快照。单核下 ISR
|
||||
* 执行期间生产者冻结,读一次即可定性;本周期跳过段 1/段 3(开头已清零,
|
||||
* 语义=无有效快照),下周期自愈。批量区(段 4)走环形队列,不受影响。 */
|
||||
* 语义=无有效快照),下周期自愈。批量区(段 4)每槽另有自己的 seqlock,
|
||||
* 不受本组影响。 */
|
||||
const bool xrxStable = ((s_xrxSeq & 1U) == 0U);
|
||||
|
||||
if (xrxStable && s_rxValid) {
|
||||
@@ -290,6 +287,9 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
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;
|
||||
}
|
||||
|
||||
const uint8_t ch = s_rxValid ? s_rxCh : 0U;
|
||||
@@ -354,11 +354,11 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax);
|
||||
}
|
||||
|
||||
/* ── 段 4(360..639):CANFD 批量接收区 ──
|
||||
/* ── 段 4(360..583):CANFD 批量接收覆盖窗口 ──
|
||||
* 段 1/段 3 只带"最近一帧",高速多帧场景下周期间到达的帧会互相覆盖
|
||||
* (s_dropCount 就是在数这个)。批量区从环形队列里一次回收多帧,
|
||||
* 让主站在一个周期内拿到完整的帧序列而不是采样点。 */
|
||||
fillRxBatch(in);
|
||||
* (s_dropCount 就是在数这个)。批量区保留最近 N 帧的窗口,让主站在一个
|
||||
* 周期内拿到帧序列而不是单个采样点;1.6.15 起改为定长覆盖(见 fillRxWindow)。 */
|
||||
fillRxWindow(in);
|
||||
}
|
||||
|
||||
void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
@@ -368,12 +368,6 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
return;
|
||||
}
|
||||
|
||||
/* 上一帧还没被 PDO 周期取走就来了新帧:记一次丢弃(覆盖旧帧,
|
||||
* 保证主站看到的永远是最新状态,而不是卡在一帧陈旧数据上)。 */
|
||||
if (s_rxValid) {
|
||||
++s_dropCount;
|
||||
}
|
||||
|
||||
/* 缓存区已扩到 32B(段 3 用),truncated 仍按段 1 的 8B 口径上报,
|
||||
* 这样 ecat.html 的 CAN 调试页语义不变。 */
|
||||
uint8_t copyLen = length;
|
||||
@@ -396,6 +390,15 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
++s_xrxSeq; /* → 奇数:快照进入"正在写" */
|
||||
__DMB();
|
||||
|
||||
/* 「未取先覆」计数:上一帧还没被任何一拍 PDO 发布就被覆盖才算丢。
|
||||
* 原判据只判 s_rxValid,而它置位后永不清零 → 第一帧之后每帧都计,
|
||||
* drop 恒等于 rx−1(u16 回绕),变成跟着收帧速率涨、零灵敏度的假仪表。
|
||||
* 放在奇数窗口内:消费者判到奇会跳过段 1/段 3、不会在此刻置 published,
|
||||
* 故"读上一帧发布状态 + 自增"对消费者等效原子,不会多计一次。 */
|
||||
if (s_rxValid && !s_rxPublished) {
|
||||
++s_dropCount;
|
||||
}
|
||||
|
||||
memcpy(s_rxData, data, copyLen);
|
||||
if (copyLen < kEcatCanXRxDataMax) {
|
||||
memset(&s_rxData[copyLen], 0, (size_t)(kEcatCanXRxDataMax - copyLen));
|
||||
@@ -407,44 +410,62 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
s_rxTruncated = (length > kEcatCanRxDataMax);
|
||||
++s_rxCount;
|
||||
s_rxValid = true;
|
||||
s_rxPublished = false; /* 这一帧还没被任何一拍 PDO 发布 */
|
||||
|
||||
__DMB();
|
||||
++s_xrxSeq; /* → 偶数:快照整体一致 */
|
||||
|
||||
/* ── 推入批量接收环形队列(1.5.56)──
|
||||
* 与上面的"最近一帧快照"并行维护:快照会被后帧覆盖(这是它的设计语义),
|
||||
* 队列则保序累积,供 fillRxBatch 一次回收多帧。
|
||||
* 队列满时丢**新**帧并计数,而不是覆盖旧帧 —— 批量区的价值在于保序完整,
|
||||
* 覆盖会在序列中间留下空洞,比明确丢尾更难排查。
|
||||
/* ── 写入批量接收覆盖窗口(1.6.15,取代 1.5.56 的环形队列)──
|
||||
* 与上面的"最近一帧快照"并行维护:快照只留最新一帧,窗口留最近 N 帧的序列。
|
||||
* 生产者按自己的游标写、绕圈覆盖,**不等消费者** —— 没有判满、没有背压、
|
||||
* 没有 pending。代价是语义从"事件流"变成"最近 N 帧窗口":一帧在窗口里活到
|
||||
* 被覆盖为止,主站按每槽 seq 去重后才归一成事件流。
|
||||
*
|
||||
* **STATUS 帧不入队**(1.6.9):批量区存在的意义是捞那些段 2 装不下的帧
|
||||
* **STATUS 帧不入窗**(1.6.9):批量区存在的意义是捞那些段 2 装不下的帧
|
||||
* (CONFIG / 版本 / 节点应答),而完整 STATUS 本来就由段 2 电机状态窗口按槽
|
||||
* 上报;主站 transport-ecat.js 的 drainRx() 拿到 STATUS 也是直接 continue
|
||||
* 丢掉。让它占队列是纯重复,而且正是它把队列压爆的:实测 CAN 侧约 4000 帧/s,
|
||||
* 批量区每拍最多 3 条(=3000 帧/s),净积压使 pending 恒 8/12、lost 几乎每拍
|
||||
* 都有 —— 队列长期饱和,才逼出了生产者覆写消费者槽位的竞态(错标率 1.77%,
|
||||
* 表现为"CONFIG 的 ID 配 STATUS 的内容")。滤掉 STATUS 后入队流量降到 CONFIG
|
||||
* 量级,队列不再饱和,竞态窗口随之消失。 */
|
||||
* 丢掉。让它占窗口是纯重复,而且正是它把队列压爆的:实测 CAN 侧约 4000 帧/s,
|
||||
* 旧批量区每拍最多回收 3 条(=3000 帧/s),净积压使队列长期饱和,才逼出了
|
||||
* 生产者覆写消费者槽位的竞态(错标率 1.77%,表现为"CONFIG 的 ID 配 STATUS
|
||||
* 的内容")。滤掉 STATUS 后入窗流量降到 CONFIG 量级。
|
||||
*
|
||||
* OVF 只数"**未取先覆**":覆盖一帧主站已经取走的数据是窗口的设计意图(不计),
|
||||
* 覆盖一帧主站还没来得及取的数据才是真丢帧(计数)。该数与主站按 seq 空洞
|
||||
* 算出的丢帧数应当一致 —— 两侧独立仪表互相对账,任一侧骗人都当场暴露。 */
|
||||
if ((id & 0xFFFFU) == 0x0100U) {
|
||||
return;
|
||||
}
|
||||
const uint8_t next = (uint8_t)((s_rxRingHead + 1U) % kEcatRxRingSlots);
|
||||
if (next == s_rxRingTail) {
|
||||
if (s_rxRingLost < 0xFFU) {
|
||||
++s_rxRingLost;
|
||||
{
|
||||
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 取走就被覆盖 */
|
||||
}
|
||||
} else {
|
||||
RxRingItem &it = s_rxRing[s_rxRingHead];
|
||||
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;
|
||||
}
|
||||
it.id = id;
|
||||
it.ch = channel;
|
||||
it.dlc = n;
|
||||
memcpy(it.data, data, n);
|
||||
__DMB(); /* 数据写入先于 head 发布落地(volatile 不约束非 volatile 的 memcpy) */
|
||||
s_rxRingHead = next; /* 最后推进 head:消费者读到时数据必已写完 */
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user