1.6.0 剖分探针(上一轮真机已验证, 随本轮一并入库): - ecatappl.c: 0x2000 sub5..12 DWT 周期累加器(PDI_Isr/输入映射/640B 写细分) - ethercat_pdo.cpp: 输出按需全量读(12B 探针判 seq 沿, 省每周期 ~190µs 全量读) - 段5 诊断区(负载率+seq)随 PDO 上报, 主站 /api/app/load 轮询 1.6.1 输入 PDO 瘦身 640→496(62×64bit; 0..359 与 1.5.61 逐字节兼容): - 依据: DWT 实测 640B 写恒 296µs/拍且与 SYNC0 相位无关(主站 dc_bench 五形态 对拍极差 0.04%), PRAM ≈0.463µs/B 线性; 真机 6086 帧 DLC 仅 24/32 - 批量接收条目 8B 头+64B 数据 → 8B+32B(DLC 真值保留, 截断打 flags bit1, 环形队列仍存全量 64B); 段5 诊断区 632→484 - 预期写侧 −67µs/拍(→~230µs, 占空 29.7%→23%); LRW 帧 1024→880B - 五处一致: ethercat_pdo_size.h(真源) / dc_app.c(62 条目+映射初始化器) / gen_eeprom.py(PD_IN_SIZE, nEntry 自动推导) / DCU_ESI.xml(SM3 496+62 条目) / bridge.h(kEcatRxBatchDataMax=32, 断言更新) - 主站 tools/ecat decode_input 按 Ibytes 双布局自适应, 固件/EEPROM 与主站可 任意先后升级; 混烧(只烧一侧)会被 SM 长度校验拦下(AL 0x001A) - AC6 编译 0 警告; 真机验收待: 烧 axf + ecat.bat flash + 断电冷启动 (流程与复验判据见 tools/ecat/MIGRATION.md 1.6.1 节)
425 lines
17 KiB
C++
425 lines
17 KiB
C++
/**
|
||
* @file ethercat_can_bridge.cpp
|
||
* @brief EtherCAT PDO ↔ CAN 转发桥实现(1.5.56)。
|
||
*/
|
||
#include "comm/ethercat_can_bridge.h"
|
||
#include "device/canfd.h"
|
||
#include "device/motor_state.h"
|
||
|
||
#include "RTE_Components.h"
|
||
#include CMSIS_device_header /* __DMB():seqlock 的编译器/总线屏障 */
|
||
|
||
#include <cstring>
|
||
|
||
volatile uint8_t EthercatCanBridge::s_lastSeq = 0U;
|
||
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 uint32_t EthercatCanBridge::s_xrxSeq = 0U;
|
||
volatile uint8_t EthercatCanBridge::s_rxCh = 0U;
|
||
volatile uint8_t EthercatCanBridge::s_rxDlc = 0U;
|
||
volatile bool EthercatCanBridge::s_rxTruncated = false;
|
||
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;
|
||
|
||
namespace {
|
||
/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端,不依赖编译器结构体布局。 */
|
||
inline uint32_t rd32le(const uint8_t *p)
|
||
{
|
||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
||
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||
}
|
||
inline void wr32le(uint8_t *p, uint32_t v)
|
||
{
|
||
p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8);
|
||
p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24);
|
||
}
|
||
inline void wr16le(uint8_t *p, uint16_t v)
|
||
{
|
||
p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8);
|
||
}
|
||
} // 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 完全一致 */
|
||
}
|
||
if (want > kEcatTxBatchMax) {
|
||
want = kEcatTxBatchMax;
|
||
}
|
||
const bool stopOnError = (out[ECAT_TXB_OFF_FLAGS] & 0x01U) != 0U;
|
||
|
||
uint8_t sent = 0U;
|
||
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;
|
||
}
|
||
const uint8_t *f = &out[off];
|
||
uint8_t dlc = f[ECAT_FRM_OFF_DLC];
|
||
if (dlc > kEcatCanFdDataMax) {
|
||
dlc = kEcatCanFdDataMax;
|
||
}
|
||
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc) > kEcatPdoOutBytes) {
|
||
break;
|
||
}
|
||
|
||
const uint8_t ch = f[ECAT_FRM_OFF_CH];
|
||
CanFd::CanTxResult r;
|
||
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);
|
||
}
|
||
|
||
if (r == CanFd::CanTxResult::kOk) {
|
||
++s_txCount;
|
||
++sent;
|
||
} else {
|
||
s_lastTxResult = (uint8_t)r;
|
||
if (stopOnError) {
|
||
break;
|
||
}
|
||
}
|
||
off = (uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc);
|
||
}
|
||
return sent;
|
||
}
|
||
|
||
void EthercatCanBridge::fillRxBatch(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; /* 快照一次:生产者可能边填边推进 */
|
||
|
||
while (tail != head && count < kEcatRxBatchMax) {
|
||
const RxRingItem &it = s_rxRing[tail];
|
||
uint8_t dlc = it.dlc;
|
||
if (dlc > kEcatCanFdDataMax) {
|
||
dlc = kEcatCanFdDataMax;
|
||
}
|
||
/* 1.6.1: PDO 内数据条目 32B 上限——DLC 字节填**真值**,超出部分截断并打
|
||
* flags bit1;环形队列里仍是全量 64B,截断只发生在 PDO 快照这一步。
|
||
* 主站按 "8 + min(dlc, kEcatRxBatchDataMax)" 推进游标。 */
|
||
uint8_t dlcWire = dlc;
|
||
if (dlcWire > kEcatRxBatchDataMax) {
|
||
dlcWire = kEcatRxBatchDataMax;
|
||
}
|
||
/* 装不下就停,**且不推进 tail** —— 该帧留到下个周期,不丢。 */
|
||
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE + dlcWire) > kEcatPdoInBytes) {
|
||
break;
|
||
}
|
||
|
||
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);
|
||
|
||
off = (uint16_t)(off + ECAT_FRM_HDR_SIZE + dlcWire);
|
||
tail = (uint8_t)((tail + 1U) % kEcatRxRingSlots);
|
||
++count;
|
||
}
|
||
__DMB(); /* 槽位读取先于 tail 发布落地:否则生产者可能提前覆写还没读完的槽 */
|
||
s_rxRingTail = tail; /* 只在这里推进:消费者独占写 tail */
|
||
|
||
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)
|
||
{
|
||
if (head == nullptr) {
|
||
return false;
|
||
}
|
||
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;
|
||
|
||
/* 上电/重连后首帧只记基准(与 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;
|
||
}
|
||
return (head[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U;
|
||
}
|
||
|
||
void EthercatCanBridge::processOutput(const uint8_t *out)
|
||
{
|
||
if (out == nullptr) {
|
||
return;
|
||
}
|
||
|
||
const uint8_t seq = out[ECAT_CAN_TX_OFF_SEQ];
|
||
|
||
/* 电机状态窗口下标每周期都取(与 seq 沿无关):主站靠移动这个值滚动扫表,
|
||
* 不该被「有没有新 CAN 帧要发」耦合。越界值钳到 0,避免读到槽数组外。 */
|
||
const uint8_t win = out[ECAT_CAN_TX_OFF_WIN];
|
||
s_winBase = (win < kMotorSlotCount) ? win : 0U;
|
||
|
||
/* 上电/重连后第一次看到输出数据时只记录基准,不发送:否则主站残留的
|
||
* 非零 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;
|
||
}
|
||
|
||
if (seq == s_lastSeq) {
|
||
return; /* 没有新的发送请求 */
|
||
}
|
||
s_lastSeq = seq;
|
||
|
||
const uint8_t ch = out[ECAT_CAN_TX_OFF_CH];
|
||
uint8_t dlc = out[ECAT_CAN_TX_OFF_DLC];
|
||
if (dlc > kEcatCanTxDataMax) {
|
||
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);
|
||
s_lastTxResult = (uint8_t)r;
|
||
if (r == CanFd::CanTxResult::kOk) {
|
||
++s_txCount;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 批量区:同一个 seq 沿里一次性发 N 帧,供多轴同步下发。 */
|
||
(void)sendBatch(out);
|
||
}
|
||
|
||
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 逐字节兼容 ── */
|
||
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)走环形队列,不受影响。 */
|
||
const bool xrxStable = ((s_xrxSeq & 1U) == 0U);
|
||
|
||
if (xrxStable && s_rxValid) {
|
||
uint8_t flags = 0x01U;
|
||
if (s_rxTruncated) {
|
||
flags |= 0x02U;
|
||
}
|
||
in[ECAT_CAN_RX_OFF_CH] = s_rxCh;
|
||
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);
|
||
}
|
||
|
||
const uint8_t ch = s_rxValid ? s_rxCh : 0U;
|
||
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 字节指定,主站每周期 +7 即可 4 周期扫完 27 个槽。
|
||
*
|
||
* 上下文安全:buildInput 在主循环(free-run)或 Sync0_Isr 里跑,而状态表由
|
||
* 主循环的 taskCanRxParse 写。单字段撕裂最坏是某槽的 seq 与 data 不同步一拍,
|
||
* 主站按 seq 变化判新旧、下一周期即自愈,故不加临界区(与本文件既有约定一致)。 */
|
||
const MotorStateTable &mt = MotorState::table();
|
||
const uint8_t base = (s_winBase < kMotorSlotCount) ? s_winBase : 0U;
|
||
|
||
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];
|
||
|
||
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);
|
||
++count;
|
||
}
|
||
in[ECAT_MOTW_OFF_WIN_COUNT] = count;
|
||
|
||
/* ── 段 3(320..359):最近一帧完整快照 ──
|
||
* 版本帧(0x00FF)/CONFIG 帧(0x0101) 都是 DLC 24,段 1 的 8 字节装不下。
|
||
* 与段 1 共用 xrxStable 门控:两段必须来自同一帧,否则 ecat.html 的短快照
|
||
* 和电机页的长快照会各自解出半新半旧的 ID/数据。 */
|
||
if (xrxStable && s_rxValid) {
|
||
in[ECAT_XRX_OFF_CH] = s_rxCh;
|
||
in[ECAT_XRX_OFF_DLC] = s_rxDlc;
|
||
in[ECAT_XRX_OFF_FLAGS] = 0x01U;
|
||
wr32le(&in[ECAT_XRX_OFF_ID], s_rxId);
|
||
memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax);
|
||
}
|
||
|
||
/* ── 段 4(360..639):CANFD 批量接收区 ──
|
||
* 段 1/段 3 只带"最近一帧",高速多帧场景下周期间到达的帧会互相覆盖
|
||
* (s_dropCount 就是在数这个)。批量区从环形队列里一次回收多帧,
|
||
* 让主站在一个周期内拿到完整的帧序列而不是采样点。 */
|
||
fillRxBatch(in);
|
||
}
|
||
|
||
void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||
const uint8_t *data, uint8_t length)
|
||
{
|
||
if (data == nullptr) {
|
||
return;
|
||
}
|
||
|
||
/* 上一帧还没被 PDO 周期取走就来了新帧:记一次丢弃(覆盖旧帧,
|
||
* 保证主站看到的永远是最新状态,而不是卡在一帧陈旧数据上)。 */
|
||
if (s_rxValid) {
|
||
++s_dropCount;
|
||
}
|
||
|
||
/* 缓存区已扩到 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; /* → 奇数:快照进入"正在写" */
|
||
__DMB();
|
||
|
||
memcpy(s_rxData, data, copyLen);
|
||
if (copyLen < kEcatCanXRxDataMax) {
|
||
memset(&s_rxData[copyLen], 0, (size_t)(kEcatCanXRxDataMax - copyLen));
|
||
}
|
||
|
||
s_rxCh = channel;
|
||
s_rxId = id;
|
||
s_rxDlc = length; /* 报告原始长度,便于主站发现被截断 */
|
||
s_rxTruncated = (length > kEcatCanRxDataMax);
|
||
++s_rxCount;
|
||
s_rxValid = true;
|
||
|
||
__DMB();
|
||
++s_xrxSeq; /* → 偶数:快照整体一致 */
|
||
|
||
/* ── 推入批量接收环形队列(1.5.56)──
|
||
* 与上面的"最近一帧快照"并行维护:快照会被后帧覆盖(这是它的设计语义),
|
||
* 队列则保序累积,供 fillRxBatch 一次回收多帧。
|
||
* 队列满时丢**新**帧并计数,而不是覆盖旧帧 —— 批量区的价值在于保序完整,
|
||
* 覆盖会在序列中间留下空洞,比明确丢尾更难排查。 */
|
||
const uint8_t next = (uint8_t)((s_rxRingHead + 1U) % kEcatRxRingSlots);
|
||
if (next == s_rxRingTail) {
|
||
if (s_rxRingLost < 0xFFU) {
|
||
++s_rxRingLost;
|
||
}
|
||
} else {
|
||
RxRingItem &it = s_rxRing[s_rxRingHead];
|
||
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:消费者读到时数据必已写完 */
|
||
}
|
||
}
|
||
|
||
extern "C" {
|
||
|
||
void EcatCan_ProcessOutput(const uint8_t *out)
|
||
{
|
||
EthercatCanBridge::processOutput(out);
|
||
}
|
||
|
||
void EcatCan_BuildInput(uint8_t *in)
|
||
{
|
||
EthercatCanBridge::buildInput(in);
|
||
}
|
||
|
||
} /* extern "C" */
|