fix(comm): 回退LAN9252批量读写优化并修复多个CAN-EtherCAT桥竞态问题
回退1.6.0引入的LAN9252 PRAM批量读写优化,因其AVAIL_CNT计数器对短读存在语义问题, 导致邮箱短读和探针传输时出现FIFO陈旧数据损坏问题。 同时修复EtherCAT-CAN桥中的多个竞态条件: - 修复消费者在fillRxBatch中直接访问共享ring buffer导致的数据混淆问题 - 修复生产者-消费者间tail指针发布时机不当造成的槽位覆盖竞态 - 修复WIN窗口基址更新位置错误导致电机状态窗口被锁定的问题 - 修复buildInput中断上下文与主循环间的数据竞争和内存撕裂问题 - 添加STATUS帧过滤机制减少批量区压力并解决队列饱和问题 此外还增加了多个诊断计数器用于监控撕裂、溢出和拼接错误,并调整了 输入PDO段布局和大小参数以支持新的48字节STATUS格式。 这些修改解决了上位机观察到的帧标签错误、电机状态丢失和数据拼接问题。
This commit is contained in:
@@ -114,7 +114,9 @@ void EthercatCanBridge::fillRxBatch(uint8_t *in)
|
||||
const uint8_t head = s_rxRingHead; /* 快照一次:生产者可能边填边推进 */
|
||||
|
||||
while (tail != head && count < kEcatRxBatchMax) {
|
||||
const RxRingItem &it = s_rxRing[tail];
|
||||
/* 整条先拷到局部副本再用:生产者(主循环 onCanFrame)与本函数(PDI_Isr)
|
||||
* 共用这块槽,直接按引用逐字段取会让 id/dlc/data 分属不同帧。 */
|
||||
const RxRingItem it = s_rxRing[tail];
|
||||
uint8_t dlc = it.dlc;
|
||||
if (dlc > kEcatCanFdDataMax) {
|
||||
dlc = kEcatCanFdDataMax;
|
||||
@@ -126,8 +128,12 @@ void EthercatCanBridge::fillRxBatch(uint8_t *in)
|
||||
if (dlcWire > kEcatRxBatchDataMax) {
|
||||
dlcWire = kEcatRxBatchDataMax;
|
||||
}
|
||||
/* 装不下就停,**且不推进 tail** —— 该帧留到下个周期,不丢。 */
|
||||
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE + dlcWire) > kEcatPdoInBytes) {
|
||||
/* 装不下就停,**且不推进 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;
|
||||
}
|
||||
|
||||
@@ -141,9 +147,17 @@ void EthercatCanBridge::fillRxBatch(uint8_t *in)
|
||||
off = (uint16_t)(off + ECAT_FRM_HDR_SIZE + dlcWire);
|
||||
tail = (uint8_t)((tail + 1U) % kEcatRxRingSlots);
|
||||
++count;
|
||||
|
||||
/* **每条读完立刻发布 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;
|
||||
}
|
||||
__DMB(); /* 槽位读取先于 tail 发布落地:否则生产者可能提前覆写还没读完的槽 */
|
||||
s_rxRingTail = tail; /* 只在这里推进:消费者独占写 tail */
|
||||
|
||||
const uint8_t pending = (uint8_t)((head + kEcatRxRingSlots - tail) % kEcatRxRingSlots);
|
||||
in[ECAT_RXB_OFF_COUNT] = count;
|
||||
@@ -187,10 +201,11 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
|
||||
|
||||
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;
|
||||
/* WIN 不在这里取:1.6.0 起 out 只在 seq 沿那拍才是新鲜的,无沿的周期里它是
|
||||
* 上一次全量读的残值。在这里回写 s_winBase,等于用开机首拍的 0 每拍盖掉
|
||||
* peekOutput() 刚取到的新值 —— 窗口被钉死在基址 0,槽位 7..26(node3 及
|
||||
* 以上)永不上报,直到主站真发一帧触发一次全量读才复位。
|
||||
* 每拍刷新由 peekOutput()(SSC PDO_OutputMapping 的 12B 探针)独占负责。 */
|
||||
|
||||
/* 上电/重连后第一次看到输出数据时只记录基准,不发送:否则主站残留的
|
||||
* 非零 seq 会被当成一次新请求,在进 OP 的瞬间凭空发出一帧。 */
|
||||
@@ -283,11 +298,16 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
|
||||
/* ── 段 2(32..319):电机状态窗口 ──
|
||||
* 直接读 MotorState 聚合表,不再依赖"最近一帧 CAN"。窗口起点由主站通过
|
||||
* 输出 PDO 的 WIN 字节指定,主站每周期 +7 即可 4 周期扫完 27 个槽。
|
||||
* 输出 PDO 的 WIN 字节指定,主站每周期 +5 即可 6 周期扫完 27 个槽。
|
||||
*
|
||||
* 上下文安全:buildInput 在主循环(free-run)或 Sync0_Isr 里跑,而状态表由
|
||||
* 主循环的 taskCanRxParse 写。单字段撕裂最坏是某槽的 seq 与 data 不同步一拍,
|
||||
* 主站按 seq 变化判新旧、下一周期即自愈,故不加临界区(与本文件既有约定一致)。 */
|
||||
* 上下文安全(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;
|
||||
|
||||
@@ -305,13 +325,20 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
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);
|
||||
++count;
|
||||
}
|
||||
in[ECAT_MOTW_OFF_WIN_COUNT] = count;
|
||||
|
||||
@@ -388,7 +415,19 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
* 与上面的"最近一帧快照"并行维护:快照会被后帧覆盖(这是它的设计语义),
|
||||
* 队列则保序累积,供 fillRxBatch 一次回收多帧。
|
||||
* 队列满时丢**新**帧并计数,而不是覆盖旧帧 —— 批量区的价值在于保序完整,
|
||||
* 覆盖会在序列中间留下空洞,比明确丢尾更难排查。 */
|
||||
* 覆盖会在序列中间留下空洞,比明确丢尾更难排查。
|
||||
*
|
||||
* **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
|
||||
* 量级,队列不再饱和,竞态窗口随之消失。 */
|
||||
if ((id & 0xFFFFU) == 0x0100U) {
|
||||
return;
|
||||
}
|
||||
const uint8_t next = (uint8_t)((s_rxRingHead + 1U) % kEcatRxRingSlots);
|
||||
if (next == s_rxRingTail) {
|
||||
if (s_rxRingLost < 0xFFU) {
|
||||
|
||||
Reference in New Issue
Block a user