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:
cbb
2026-09-08 00:52:35 +08:00
parent ace5a1671b
commit 0cd3d9bed2
13 changed files with 518 additions and 111 deletions
+26 -53
View File
@@ -369,14 +369,13 @@ void EscLan9252::pramRead(uint16_t addr, uint8_t *buf, uint16_t len)
* 首 DWORD 有效字节从 (addr&3) 起(表 12-7),末 DWORD 尾部无效(表 12-8)。
* 别名口 0x00-0x1C 功能相同,轮换寻址即官方突发用法;每读满一个 DWORD 弹一项。
*
* 1.6.0 批量搬运(负载优化):旧实现每搬一个 DWORD 都重新轮询一次 CMD 的
* AVAIL 位——每次轮询是 setHbiIndex+读低/高半字共 3 次 EXMC 事务,数据本身
* 又 3 次,单周期 1024B 过程数据因此跑 ~1600 次 EXMC 访问,是 GD32 侧 CPU
* 占用的大头。DS 12.13.6:ECAT_PRAM_RD_CMD bits[12:8] = PRAM_READ_AVAIL_CNT,
* 指示"无需再查状态即可继续读 FIFO 的次数"(≤16,每次整 DWORD 读自减)。
* 轮询命中时顺手取该计数,一口气读 min(CNT, 8[别名窗口一程], 剩余 DWORD)个,
* 轮询开销从"每 DWORD 一次"降到"每批一次"。字节装配/lead 逻辑逐字节不变,
* 任意 (addr,len) 组合的搬运结果与旧实现完全一致。 */
* 1.6.2 回退逐 DWORD 轮询,弹前必查 AVAIL 位。1.6.0 曾信 CMD 的
* AVAIL_CNT(bits[12:8])一次弹一批:实测 12B 短读(1.6.0 输出探针,主站窗口
* 请求 WIN@8 靠它送达)首 DWORD 新鲜、后续 DWORD 取到 FIFO 陈旧项,窗口
* 基址冻结,槽位 7..26 的电机状态因此永不上报(仅 seq 沿触发的 384B 全量读
* 路径能拿到新 WIN)。批量模式 1.6.0 上机对拍本来就是零收益,384/496B 长读
* 未见损坏,但邮箱短读/探针这类小传输只验证过"逐 DWORD 查 AVAIL"的旧模式
* ——在弄清该计数器对短读的精确语义之前,不要恢复批量弹 DWORD。 */
if (len == 0U) {
return;
}
@@ -386,31 +385,18 @@ void EscLan9252::pramRead(uint16_t addr, uint8_t *buf, uint16_t len)
uint16_t alias = 0U;
while (done < len) {
uint32_t n = POLL_MAX;
uint32_t cmd;
while (((cmd = readReg32(ECAT_PRAM_RD_CMD)) & PRAM_AVAIL) == 0U && --n != 0U) {}
while ((readReg32(ECAT_PRAM_RD_CMD) & PRAM_AVAIL) == 0U && --n != 0U) {}
if (n == 0U) {
break; /* 芯片异常:避免死等,交出残缺数据 */
}
uint32_t batch = (cmd >> 8) & 0x1FU;
if (batch == 0U || batch > 8U) {
batch = 1U; /* 计数字段异常时退回单拍,等价旧实现 */
}
/* 本批以"吃完全部剩余字节"为上限(lead 只在首 DWORD 出现) */
const uint32_t lead0 = (done == 0U) ? (uint32_t)(addr & 3U) : 0U;
const uint32_t needDW = ((uint32_t)(len - done) + lead0 + 3U) / 4U;
if (batch > needDW) {
batch = needDW;
}
while (batch-- != 0U) {
const uint32_t dw = readReg32((uint16_t)(ECAT_PRAM_RD_DATA + alias));
alias = (uint16_t)((alias + 4U) & 0x1FU);
const uint8_t lead = (done == 0U) ? (uint8_t)(addr & 3U) : 0U;
const uint8_t take = (uint8_t)((len - done) < (4U - lead) ? (len - done) : (4U - lead));
for (uint8_t i = 0U; i < take; ++i) {
buf[done + i] = (uint8_t)(dw >> (8U * (lead + i)));
}
done = (uint16_t)(done + take);
const uint32_t dw = readReg32((uint16_t)(ECAT_PRAM_RD_DATA + alias));
alias = (uint16_t)((alias + 4U) & 0x1FU);
const uint8_t lead = (done == 0U) ? (uint8_t)(addr & 3U) : 0U;
const uint8_t take = (uint8_t)((len - done) < (4U - lead) ? (len - done) : (4U - lead));
for (uint8_t i = 0U; i < take; ++i) {
buf[done + i] = (uint8_t)(dw >> (8U * (lead + i)));
}
done = (uint16_t)(done + take);
}
}
@@ -418,9 +404,8 @@ void EscLan9252::pramWrite(uint16_t addr, const uint8_t *buf, uint16_t len)
{
/* DS 12.11.4:写 ADDR_LEN 再置 BUSY 启动;首 DWORD 有效字节从 (addr&3) 起,
* 引擎只取有效字节;别名口 0x20-0x3C 轮换;写满一个 DWORD 推一项。
* 1.6.0 批量搬运:与 pramRead 同理,DS 12.13.7 bits[12:8] =
* PRAM_WRITE_AVAIL_CNT 指示"无需再查状态即可继续写 FIFO 的次数",
* 轮询从"每 DWORD 一次"降到"每批一次",单周期 EXMC 事务约减半。 */
* 1.6.2 与 pramRead 同步回退逐 DWORD 轮询(推前必查 AVAIL 位):1.6.0 批量
* 模式实测零收益,且与短读损坏同源(见 pramRead 注),邮箱小 PDU 写同类风险。 */
if (len == 0U) {
return;
}
@@ -430,31 +415,19 @@ void EscLan9252::pramWrite(uint16_t addr, const uint8_t *buf, uint16_t len)
uint16_t alias = 0U;
while (done < len) {
uint32_t n = POLL_MAX;
uint32_t cmd;
while (((cmd = readReg32(ECAT_PRAM_WR_CMD)) & PRAM_AVAIL) == 0U && --n != 0U) {}
while ((readReg32(ECAT_PRAM_WR_CMD) & PRAM_AVAIL) == 0U && --n != 0U) {}
if (n == 0U) {
break; /* 芯片异常:放弃剩余字节 */
}
uint32_t batch = (cmd >> 8) & 0x1FU;
if (batch == 0U || batch > 8U) {
batch = 1U; /* 计数字段异常时退回单拍,等价旧实现 */
}
const uint32_t lead0 = (uint32_t)((uint16_t)(addr + done) & 3U);
const uint32_t needDW = ((uint32_t)(len - done) + lead0 + 3U) / 4U;
if (batch > needDW) {
batch = needDW;
}
while (batch-- != 0U) {
const uint8_t lead = (done == 0U) ? (uint8_t)((uint16_t)(addr + done) & 3U) : 0U;
const uint8_t take = (uint8_t)((len - done) < (4U - lead) ? (len - done) : (4U - lead));
uint32_t dw = 0U;
for (uint8_t i = 0U; i < take; ++i) {
dw |= (uint32_t)buf[done + i] << (8U * (lead + i));
}
writeReg32((uint16_t)(ECAT_PRAM_WR_DATA + alias), dw);
alias = (uint16_t)((alias + 4U) & 0x1FU);
done = (uint16_t)(done + take);
const uint8_t lead = (done == 0U) ? (uint8_t)((uint16_t)(addr + done) & 3U) : 0U;
const uint8_t take = (uint8_t)((len - done) < (4U - lead) ? (len - done) : (4U - lead));
uint32_t dw = 0U;
for (uint8_t i = 0U; i < take; ++i) {
dw |= (uint32_t)buf[done + i] << (8U * (lead + i));
}
writeReg32((uint16_t)(ECAT_PRAM_WR_DATA + alias), dw);
alias = (uint16_t)((alias + 4U) & 0x1FU);
done = (uint16_t)(done + take);
}
uint32_t n = POLL_MAX;
while ((readReg32(ECAT_PRAM_WR_CMD) & PRAM_BUSY) != 0U && --n != 0U) {}
+54 -15
View File
@@ -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) {
+42 -14
View File
@@ -104,19 +104,19 @@ enum : uint16_t {
ECAT_MOTW_OFF_ACTIVE = 32U, /**< 在线电机数 */
ECAT_MOTW_OFF_BOOT = 33U, /**< 1=近 3s 收到过 0x103 BOOT 心跳 */
ECAT_MOTW_OFF_WIN_BASE = 34U, /**< 本帧携带的槽窗口起始下标(回显主站请求) */
ECAT_MOTW_OFF_WIN_COUNT = 35U, /**< 本帧携带的有效槽数(≤7) */
ECAT_MOTW_OFF_WIN_COUNT = 35U, /**< 本帧携带的有效槽数(≤5) */
ECAT_MOTW_OFF_GLOBAL_SEQ = 36U,/**< 状态表全局刷新序号,小端 uint32 */
ECAT_MOTW_OFF_SLOTS = 40U, /**< 槽数组起点,7×40B */
ECAT_MOTW_OFF_SLOTS = 40U, /**< 槽数组起点,5×56B(40..319,与段 3 正好相接) */
};
/** 电机状态槽内部布局(40 字节)。 */
/** 电机状态槽内部布局(56 字节)。 */
enum : uint8_t {
ECAT_MOT_OFF_NODE = 0U, /**< 节点号 1..9,0=空槽 */
ECAT_MOT_OFF_CH = 1U, /**< CAN 通道 0/1/2 */
ECAT_MOT_OFF_ONLINE = 2U, /**< 1=在线 */
ECAT_MOT_OFF_DLC = 3U, /**< 最后一帧 STATUS 的 DLC(正常 32) */
ECAT_MOT_OFF_DLC = 3U, /**< 最后一帧 STATUS 的 DLC(正常 48) */
ECAT_MOT_OFF_SEQ = 4U, /**< STATUS 刷新序号,小端 uint32 */
ECAT_MOT_OFF_STATUS = 8U, /**< 原始 STATUS payload,32 字节 */
ECAT_MOT_OFF_STATUS = 8U, /**< 原始 STATUS payload,48 字节(v0.74:B32-35=InitStatus) */
};
/** 输入 PDO 段 3(320..359):最近一帧**完整**快照(版本/CONFIG 帧要 24B)。 */
@@ -147,9 +147,31 @@ enum : uint16_t {
ECAT_DIAG_OFF_LOAD = 484U, /**< 负载率,小端 uint16,单位 0.1%(1000=100%)。
中断 + 非延迟处理段(任务派发/OTA relay)占墙钟比,
不含常开轮询与空转;1s 滚动窗口(见 sys/cpu_load.h) */
ECAT_DIAG_OFF_RSVD = 486U, /**< 保留 2B */
ECAT_DIAG_OFF_SEQ = 488U, /**< 诊断区序号,小端 uint32,每完成一个统计窗口 +1,
主站据此判断负载值是否仍在滚动刷新 */
ECAT_DIAG_OFF_TORN = 486U, /**< CAN 取帧撕裂丢帧数(低 16 位,小端,饱和不回绕)。
1.6.6 起:ISR 取帧时若 MP 的 TS/DLENC 前后不一致,
说明邮箱窗口在取数途中被硬件换帧,该帧整帧丢弃并
在此计数。这是"读到半条帧"的唯一可观测量 —— 主站
据此判断修复是否生效、总线负载是否高到频繁触发。 */
ECAT_DIAG_OFF_SEQ = 488U, /**< 诊断区序号,小端 uint16(1.6.12 起,原 u32 让出
高 2 字节),每完成一个 1s 统计窗口 +1,主站据此
判断负载值是否仍在滚动刷新。u16 足够:主站只判
"非 0 且在涨",18h 才回绕。 */
ECAT_DIAG_OFF_OVERFULL = 490U, /**< CAN RX FIFO 满溢计数 RFO0(u16 饱和,1.6.12)。
1.6.11 把溢出策略改成"满则丢新帧"后拼接并未消失
(段5@494 以 ~384/s 增长),必须直接观测溢出本身
与拼接率是否同涨,才能判定残余拼接是否仍源自溢出
(即本 IP 在 DISCARD 模式下照样毁窗口)。 */
ECAT_DIAG_OFF_RELTIMEOUT = 492U, /**< RFD0 释放握手超时计数(u16 饱和,1.6.12)。
原为 1.6.7 的 PDO 重入计数 ECAT_DIAG_OFF_REENTRY
(引入至今恒 0,不再上线,仍留内部计数)。取帧后
等 RFD0 自清 2000 次未果 = dequeue 未完成,窗口
与读指针可能已失同步 —— 拼接的直接前兆。 */
ECAT_DIAG_OFF_REJECT = 494U, /**< CAN 内容级拼接帧命中率(u16 饱和,1.6.11)。
入队前结构自洽校验(STATUS 温度量程+斜率、CONFIG
未使用区/模式)命中即计数。1.6.12 起**只计不丢**
—— 数据管道必须诚实,该计数退化为"源头是否还在
产生拼接帧"的纯测量仪表:修好 = 恒 0,没修好 =
网页看到什么,计数就是什么的倍率。 */
};
/** 单帧数据在输出 PDO / 长帧快照区中的最大字节数(电机 CMD 是 DLC 32)。 */
@@ -159,10 +181,14 @@ constexpr uint8_t kEcatCanRxDataMax = 8U;
/** 段 3 长快照数据区字节数。 */
constexpr uint8_t kEcatCanXRxDataMax = 32U;
/** 单个电机状态槽字节数。 */
constexpr uint8_t kEcatMotSlotSize = 40U;
/** 一份输入 PDO 携带的电机状态槽数。 */
constexpr uint8_t kEcatMotSlotsPerPdo = 7U;
/** 单个电机状态槽字节数:8B 头 + 48B STATUS(v0.74)。
* 1.6.4 前是 40B(只装得下 32B STATUS),于是 buildInput 按 48B 拷贝时会溢出
* 16B 盖到段 3,且主站永远拿不到 B32-35 的 InitStatus。 */
constexpr uint8_t kEcatMotSlotSize = 56U;
/** 一份输入 PDO 携带的电机状态槽数:段 2 可用 280B(40..319)÷ 56 = 5。
* 27 槽需 ⌈27/5⌉ = 6 个基址扫完。槽数×槽长仍是 280B,输入 PDO 总长不变,
* 所以 EEPROM/SII 无需重刷。 */
constexpr uint8_t kEcatMotSlotsPerPdo = 5U;
/** 过程数据长度,唯一真源见 comm/ethercat_pdo_size.h(C/C++ 共用)。 */
constexpr uint16_t kEcatPdoOutBytes = ECAT_PDO_OUT_BYTES; /* 48×64bit = 384B */
@@ -181,8 +207,10 @@ constexpr uint16_t kEcatTxBatchBytes = (uint16_t)(kEcatPdoOutBytes - ECAT_TXB_OF
constexpr uint8_t kEcatTxBatchMax =
(uint8_t)(kEcatTxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax));
/** 批量接收区可用字节数与最坏情况(全 DLC32 截断满帧)帧数上限。 */
constexpr uint16_t kEcatRxBatchBytes = (uint16_t)(kEcatPdoInBytes - ECAT_RXB_OFF_ITEMS);
/** 批量接收区可用字节数与最坏情况(全 DLC32 截断满帧)帧数上限。
* 可用区到段 5 诊断区起点为止(1.6.10 修正口径:旧式按 PDO 全长 496 算,
* 与 fillRxBatch 的旧边界判据同错;数值上 120/40 与 132/40 同为 3,无行为变化)。 */
constexpr uint16_t kEcatRxBatchBytes = (uint16_t)(ECAT_DIAG_OFF_LOAD - ECAT_RXB_OFF_ITEMS);
constexpr uint8_t kEcatRxBatchMax =
(uint8_t)(kEcatRxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatRxBatchDataMax));
+72 -7
View File
@@ -4,15 +4,20 @@
*/
#include "comm/ethercat_pdo.h"
#include "comm/ethercat_can_bridge.h"
#include "device/canfd.h"
#include "sys/cpu_load.h"
#include <cstring>
uint8_t EthercatPdo::s_out[EthercatPdo::kPdoOutBytes] = {0};
uint8_t EthercatPdo::s_in[EthercatPdo::kPdoInBytes] = {0};
/* 输入 PDO 的工作缓冲:buildInput 填这里,填完整块发布到 s_in(见 process())。
* 读侧 writeInputs 只读 s_in,因此永远看不到"填到一半"的镜像。 */
uint8_t EthercatPdo::s_build[EthercatPdo::kPdoInBytes] = {0};
volatile uint32_t EthercatPdo::s_outputCycles = 0U;
volatile uint32_t EthercatPdo::s_inputCycles = 0U;
volatile uint32_t EthercatPdo::s_reentryCount = 0U;
void EthercatPdo::readOutputs(const uint8_t *esc)
{
@@ -30,15 +35,45 @@ bool EthercatPdo::outputProbe(const uint8_t *head)
void EthercatPdo::process()
{
/* 重入防护(1.6.7):SSC 的 ECAT_Application()→本函数 在**四个上下文**都会被
* 调用 —— PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop(见 ssc/src/ecatappl.c
* 388、499、527、641 行)。而 MainLoop 那条路只用 DISABLE_ESC_INT() 关了
* EXTI1(ESC IRQ),**挡不住 EXTI0(Sync0,preempt=0) 和 EXTI2(Sync1,preempt=1)**。
*
* 竞态:MainLoop 把 s_in 填到一半(如刚写完批量区第 1 条)时被 Sync0/Sync1
* 抢占 → 内层重入本函数,memset 清空后重填整个 496B,随后 PDO_InputMapping()
* 把这一份发给 ESC;外层回来又按自己那轮的进度接着往下写。于是发出去的镜像里
* 批量区相邻两条来自**不同轮次**,而两轮之间环形队列 tail 已推进 —— 第 2 条
* 就带上了另一条帧的内容。实测特征与此吻合:批量区第 1 条几乎不坏(0.06%)、
* 第 2 条 30.75% 坏且内容是一条完整 CONFIG 帧;段 1/段 3 干净,因为它们每轮
* 内容相同,被覆盖也看不出来。
*
* 处理:已在填充中就直接返回。输入 PDO 是周期量,跳过一次下一拍即补,
* 远好过发出一份跨轮次拼接的镜像。 */
static volatile bool s_building = false;
if (s_building) {
++s_reentryCount;
return;
}
s_building = true;
/* PDO ↔ CAN 转发:输出 PDO 的 seq 字节变化沿触发一次 CAN 发送,
* 输入 PDO 回填桥状态 + 电机状态窗口(布局见 ethercat_can_bridge.h)。
*
* 顺序必须是先 processOutput 后 buildInput:buildInput 会把本周期刚执行的
* seq 回显成 ack,主站因此能在下一周期立刻确认送达;反过来则 ack 恒滞后一拍。
* processOutput 还负责取走主站请求的状态窗口下标,也必须在 buildInput 之前。 */
* processOutput 还负责取走主站请求的状态窗口下标,也必须在 buildInput 之前。
*
* 填的是**工作缓冲** s_build,填完整体发布到 s_in:只有重入门还不够 ——
* 内层被门挡住后,它随后的 PDO_InputMapping() 仍会去读 s_in,若 buildInput
* 直接写 s_in,读到的就是外层写到一半的半成品。填在别处、整块发布之后,
* 读侧要么拿到上一份完整镜像、要么拿到这一份完整镜像,不存在中间态。 */
EthercatCanBridge::processOutput(s_out);
EthercatCanBridge::buildInput(s_in);
fillDiagTail(s_in);
EthercatCanBridge::buildInput(s_build);
fillDiagTail(s_build);
memcpy(s_in, s_build, kPdoInBytes);
s_building = false;
}
/**
@@ -53,11 +88,41 @@ void EthercatPdo::fillDiagTail(uint8_t *in)
in[ECAT_DIAG_OFF_LOAD] = (uint8_t)(load & 0xFFU);
in[ECAT_DIAG_OFF_LOAD + 1] = (uint8_t)(load >> 8);
/* CAN 取帧撕裂丢帧数(1.6.6)。只有 16 位,饱和不回绕 —— 主站看的是
* "有没有涨、涨多快",回绕会被误读成计数清零。 */
const CanFd::Diag *cd = CanFd::diag();
const uint32_t torn = (cd != nullptr) ? cd->rx_torn_count : 0U;
const uint16_t torn16 = (torn > 0xFFFFU) ? 0xFFFFU : (uint16_t)torn;
in[ECAT_DIAG_OFF_TORN] = (uint8_t)(torn16 & 0xFFU);
in[ECAT_DIAG_OFF_TORN + 1] = (uint8_t)(torn16 >> 8);
const uint32_t seq = CpuLoad::windowSeq();
in[ECAT_DIAG_OFF_SEQ] = (uint8_t)(seq & 0xFFU);
in[ECAT_DIAG_OFF_SEQ + 1] = (uint8_t)((seq >> 8) & 0xFFU);
in[ECAT_DIAG_OFF_SEQ + 2] = (uint8_t)((seq >> 16) & 0xFFU);
in[ECAT_DIAG_OFF_SEQ + 3] = (uint8_t)((seq >> 24) & 0xFFU);
const uint16_t seq16 = (uint16_t)seq; /* 1.6.12:u32 拆成 u16,高 2 字节让给
overfull 计数。主站只判"非 0 且在涨",
按 u32 读到 seq|overfull<<16 同样成立。 */
in[ECAT_DIAG_OFF_SEQ] = (uint8_t)(seq16 & 0xFFU);
in[ECAT_DIAG_OFF_SEQ + 1] = (uint8_t)(seq16 >> 8);
/* CAN RX FIFO 满溢计数 RFO0(1.6.12 测量):1.6.11 改"满则丢新"后拼接未消失,
* 本计数与 reject@494 是否同涨,是"残余拼接是否仍源自溢出"的判决性观测。 */
const uint32_t of = (cd != nullptr) ? cd->rx_fifo_overfull_count : 0U;
const uint16_t of16 = (of > 0xFFFFU) ? 0xFFFFU : (uint16_t)of;
in[ECAT_DIAG_OFF_OVERFULL] = (uint8_t)(of16 & 0xFFU);
in[ECAT_DIAG_OFF_OVERFULL + 1] = (uint8_t)(of16 >> 8);
/* RFD0 释放握手超时计数(1.6.12,占用原 1.6.7 重入计数位 492——该计数引入
* 至今恒 0,转为纯内部)。dequeue 握手没完成 = 窗口/读指针失同步前兆。 */
const uint32_t rt = (cd != nullptr) ? cd->rx_release_timeout_count : 0U;
const uint16_t rt16 = (rt > 0xFFFFU) ? 0xFFFFU : (uint16_t)rt;
in[ECAT_DIAG_OFF_RELTIMEOUT] = (uint8_t)(rt16 & 0xFFU);
in[ECAT_DIAG_OFF_RELTIMEOUT + 1] = (uint8_t)(rt16 >> 8);
/* 内容级拼接帧拦截次数(1.6.11):MI/MP 校验挡不住的指针错位拼接由入队前
* 结构自洽校验兜底,语义同上 —— 主站看"涨不涨"判断拼接是否仍在发生。 */
const uint32_t rj = (cd != nullptr) ? cd->rx_content_reject_count : 0U;
const uint16_t rj16 = (rj > 0xFFFFU) ? 0xFFFFU : (uint16_t)rj;
in[ECAT_DIAG_OFF_REJECT] = (uint8_t)(rj16 & 0xFFU);
in[ECAT_DIAG_OFF_REJECT + 1] = (uint8_t)(rj16 >> 8);
}
void EthercatPdo::writeInputs(uint8_t *esc)
+11 -1
View File
@@ -75,6 +75,10 @@ public:
/** @brief 累计交回输入的周期数。 */
static uint32_t inputCycles() { return s_inputCycles; }
/** @brief process() 被重入拦下的次数。持续增长 = 跨上下文抢占确实在发生
* (PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 互相打断),也就证明了
* 批量区跨轮次拼接的成因;恒为 0 则说明该机制推断有误。 */
static uint32_t reentryCount() { return s_reentryCount; }
/** @brief 是否已开始过程数据交换(收到过至少一个输出周期)。 */
static bool isExchanging() { return s_outputCycles != 0U; }
@@ -96,8 +100,14 @@ private:
/** @brief 最近一份主站下发的输出数据。 */
static uint8_t s_out[kPdoOutBytes];
/** @brief 待交回主站的输入数据。 */
/** @brief 待交回主站的输入数据(已发布的完整镜像,writeInputs 只读它)。 */
static uint8_t s_in[kPdoInBytes];
/** @brief 输入 PDO 工作缓冲:buildInput 填这里,填完整块发布到 s_in。
* ECAT_Application() 会在 PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 四个
* 上下文被调用并互相抢占,直接写 s_in 会让读侧取到跨轮次的半成品。 */
static uint8_t s_build[kPdoInBytes];
/** @brief process() 被重入拦下的次数(诊断用,见 reentryCount())。 */
static volatile uint32_t s_reentryCount;
static volatile uint32_t s_outputCycles;
static volatile uint32_t s_inputCycles;
+3
View File
@@ -130,6 +130,9 @@ void EthercatSync::onEscIrqExti()
* 代价是 PDI_Isr 剩余时长(µs 级)计入 SYNC0 抖动。 */
const uint32_t prevSync0 = NVIC_GetEnableIRQ(CONFIG_ESC_SYNC0_IRQn);
NVIC_DisableIRQ(CONFIG_ESC_SYNC0_IRQn);
/* 1.6.12 曾在此预抽 CAN FIFO(pumpAllChannels),实测溢出大降但拼接照旧
* —— 病理在 357µs 阻塞窗口本身,1.6.13 直接把 CAN RX 提到 preempt=0
* 根治(见 sys/config.h),预抽失去意义且引入 PDI/ISR 双消费者风险,撤除。 */
PDI_Isr();
if (prevSync0 != 0U) {
NVIC_EnableIRQ(CONFIG_ESC_SYNC0_IRQn);
+239 -9
View File
@@ -19,6 +19,107 @@ static inline void spinDelay(uint32_t loops) {
for (volatile uint32_t d = 0; d < loops; ++d) { }
}
/* DLENC>8 的长度映射,与厂商库 g_can_fdlength_table 一致 */
constexpr uint8_t kFdLenTable[7] = { 12U, 16U, 20U, 24U, 32U, 48U, 64U };
/** 释放 FIFO0 邮箱并等待 RFD0 由硬件自动清 0(手册 26.3.4 第 3 步)。
*
* 两处与厂商库 can_message_receive() 的差别,都是它漏掉的:
* 1) 用**直接赋值**而不是 `|=`。CAN_RFIFO0 里 RFF0(bit3)/RFO0(bit4) 是 rc_w1
* (写 1 清除),读-改-写会把这期间硬件刚置起来的"FIFO 满/溢出"标志顺手
* 清掉 —— 溢出证据就是这么丢的。写 0 对 rc_w1 位无副作用。
* 2) 置位后**等它自清**。手册原文:"将 RFD 置 1 释放邮箱,并且等待其由硬件
* 自动清 0"。不等就接着读下一条,会撞上 dequeue 尚未完成的邮箱窗口。
* @return true=已确认释放完成;false=超时(异常,计数后照常继续,不死等)。 */
static inline bool releaseFifo0(uint32_t canPeriph)
{
CAN_RFIFO0(canPeriph) = CAN_RFIFO0_RFD0;
for (uint32_t n = 0U; n < 2000U; ++n) {
if ((CAN_RFIFO0(canPeriph) & CAN_RFIFO0_RFD0) == 0U) {
/* 1.6.11 注:这里曾加过 128/2048 拍的 settle 延迟,假设"自清后 MDATA
* 还在搬运"是装载时间竞态 —— 实测无效(正弦指令负载下拼接率不变)。
* 节点指纹实验证明拼接是 FIFO 缓冲指针与帧边界失同步(node3 的
* STATUS 帧头 24B = node2 的完整 CONFIG,错位恰为 CONFIG 帧长 6 字),
* 不是延迟能挡的;结构矛盾的帧由 handleRxInterrupt 的内容校验计数
* (1.6.12 起只计不丢 —— 管道必须诚实;源头修复见 1.6.13 的
* CONFIG_IRQ_PREEMPT_CAN_RX=0)。 */
return true;
}
}
return false;
}
/** 取一帧 FIFO0 报文,并校验取数期间硬件没有换帧。
*
* 为什么自己实现(不用厂商库 can_message_receive):一条 CANFD 报文要对同一个
* 地址 CAN_RFIFOMDATA0 连读 dlen/4 次才取完(48B = 12 次),加上头部两次,一条
* 帧就是十几次有先后依赖的总线访问,整个过程**不是原子的**。若这期间硬件把下
* 一帧搬进邮箱窗口,就会读出"前几个字属于上一帧、后几个字属于这一帧"的拼接
* 体,而 id/dlc 取自最开始那次读 —— 实测表现为 STATUS 帧的 ID 配上 CONFIG 帧
* 的前 12~24 字节,且混合边界 100% 落在 4 字节边界上。
*
* 判据用 MP 寄存器:TS[31:16] 是硬件接收时间戳、DLENC[3:0] 是长度,同一帧内两
* 者恒定。取数前后各读一次,只要有一个变了就说明窗口内容已不是原来那帧,整帧
* 作废(计入 rx_torn_count)—— 宁可丢一帧,也不把拼接数据递上去。
*
* @return true=取到一帧完整报文;false=本帧判为撕裂、应丢弃(邮箱仍需释放)。 */
static bool receiveFrameChecked(uint32_t canPeriph, can_receive_message_struct *msg)
{
/* 整段取数放进 BASEPRI 临界区:屏蔽 preempt>=1,把 EtherCAT 的 ESC(1)/SYNC1(1)
* 挡在外面。取数是十几次有先后依赖的寄存器读,被抢占就会读出跨帧拼接体。
* 1.6.13 起本 ISR 自身就是 preempt=0(CONFIG_IRQ_PREEMPT_CAN_RX),与 SYNC0
* 同级互不抢占,取数自此真正原子;MI/MP 回读校验保留,作为对硬件自发换帧
* (dequeue 异步完成等)的兜底观测 —— rx_torn 计数即"该病理是否还在发生"。 */
const uint32_t basepri = RT_CRITICAL_SAVE();
const uint32_t mi0 = CAN_RFIFOMI(canPeriph, CAN_FIFO0);
const uint32_t mp0 = CAN_RFIFOMP(canPeriph, CAN_FIFO0);
msg->rx_ff = static_cast<uint8_t>(CAN_RFIFOMI_FF & mi0);
if (CAN_FF_STANDARD == msg->rx_ff) {
msg->rx_sfid = static_cast<uint32_t>(GET_RFIFOMI_SFID(mi0));
} else {
msg->rx_efid = static_cast<uint32_t>(GET_RFIFOMI_EFID(mi0));
}
msg->rx_ft = static_cast<uint8_t>(CAN_RFIFOMI_FT & mi0);
msg->rx_fi = static_cast<uint8_t>(GET_RFIFOMP_FI(mp0));
msg->fd_flag = static_cast<uint8_t>((CAN_RFIFOMP_FDF & mp0) >> 7);
msg->fd_brs = static_cast<uint8_t>((mp0 & CAN_RFIFOMP_BRS) >> 5);
msg->fd_esi = static_cast<uint8_t>((mp0 & CAN_RFIFOMP_ESI) >> 4);
const uint8_t dlenc = static_cast<uint8_t>(GET_RFIFOMP_DLENC(mp0));
const uint8_t dlen = (dlenc <= 8U) ? dlenc
: ((dlenc <= 15U) ? kFdLenTable[dlenc - 9U] : 64U);
msg->rx_dlen = dlen;
/* 按字取数:与厂商库同样是对同一地址重复读(硬件读指针自动前进)。
* 5~7 字节要多读一个字才取全,与厂商库口径一致。 */
uint32_t words = static_cast<uint32_t>(dlen) / 4U;
if ((1U == words) && (4U != dlen)) {
words++;
}
if (0U == words) {
words = 1U; /* dlen<4 也要读走一个字 */
}
for (uint32_t i = 0U; i < words; ++i) {
const uint32_t w = CAN_RFIFOMDATA0(canPeriph, CAN_FIFO0);
std::memcpy(&msg->rx_data[i * 4U], &w, sizeof(w));
}
/* 取数后回读 MI+MP:任一变化 ⇒ 邮箱窗口在取数期间被换帧,这一帧是拼接体。
* 必须连 MI(完整 ID)一起比 —— 只比 MP 的 TS/DLENC 会漏:TS 是 16 位计数器,
* 相邻两帧可能同值,DLENC 也常常相同(都是 48),于是"ID 报 STATUS、数据却是
* CONFIG"这种错配能从校验里溜过去(实测占坏帧的绝大多数)。ID 变了就一定是
* 换了帧,这条判据没有巧合空间。 */
const uint32_t keep = CAN_RFIFOMP_TS | CAN_RFIFOMP_DLENC;
const uint32_t mp1 = CAN_RFIFOMP(canPeriph, CAN_FIFO0);
const uint32_t mi1 = CAN_RFIFOMI(canPeriph, CAN_FIFO0);
const bool intact = ((mp1 & keep) == (mp0 & keep)) && (mi1 == mi0);
RT_CRITICAL_RESTORE(basepri);
return intact;
}
struct CanChannelConfig {
uint32_t canPeriph;
rcu_periph_enum canRcu;
@@ -166,6 +267,7 @@ CanFd::ChannelFeedback CanFd::s_feedback[3]{};
uint32_t CanFd::s_feedbackSeq = 0U;
CanFd::IdSnapshotTable CanFd::s_idSnapshot[3]{};
bool CanFd::s_channelHwOk[3]{};
bool CanFd::s_brsSuppress[3]{};
/* TX 错误事件标志(ARD=1 单发丢帧通知):TX 完成 ISR 置位,主循环
* consumeTxError() 读清;单写单读,无锁。 */
@@ -274,7 +376,16 @@ bool CanFd::initNominalTiming(uint32_t canPeriph)
can_parameter.auto_bus_off_recovery = ENABLE;
can_parameter.auto_wake_up = DISABLE;
can_parameter.auto_retrans = ENABLE; /* 默认自动重发兜底偶发 ACK 丢失;OTA 停等协议期间由 setChannelAutoRetrans(ch,false) 临时关闭,避免重发打穿电机单缓冲 RX */
can_parameter.rec_fifo_overwrite = ENABLE;
/* 1.6.11 真根因:这里曾是 ENABLE —— FIFO 满时**覆盖最旧邮箱**,而最旧邮箱
* 正是 CPU 正在读取的输出窗口。buildInput 在 PDI_Isr(preempt1) 占 ~357µs
* 会压住 CAN ISR(preempt2),发指令时电机 CONFIG 翻倍到 2kHz/节点、总线
* ~8kHz,阻塞窗口内到达 ~3 帧恰好打满 FIFO 深度 → 覆盖进行到一半被读走
* = 跨节点/跨类型拼接帧(指纹实验实锤:node2 的 CONFIG 条目带 node3 的
* 速度限 150 与反相角度;settle 延迟/重取/MI-MP 校验都挡不住,因为
* MI/MP 是覆盖序列里最后更新的一环)。DISABLE = 溢出丢新帧并置 RFO0
* (rx_fifo_overfull_count 已在统计):STATUS/CONFIG 均周期量,丢一拍下拍
* 即补;覆盖递上来的才是网页乱码。与本文件软件队列"满丢新不覆旧"同则。 */
can_parameter.rec_fifo_overwrite = DISABLE;
can_parameter.trans_fifo_order = ENABLE;
can_parameter.working_mode = CAN_NORMAL_MODE;
can_parameter.resync_jump_width = CONFIG_CAN_NOMINAL_SJW;
@@ -422,7 +533,8 @@ void CanFd::initChannelHardware(uint8_t i)
CAN_TSTAT(ch.canPeriph) = CAN_TSTAT_MTF0 | CAN_TSTAT_MTF1 | CAN_TSTAT_MTF2;
can_interrupt_enable(ch.canPeriph, CAN_INT_TME | CAN_INT_RFNE0 | CAN_INT_RFF0 | CAN_INT_RFO0);
nvic_irq_enable(ch.txIrq, CONFIG_IRQ_PREEMPT_CAN, static_cast<uint8_t>(i));
nvic_irq_enable(ch.rx0Irq, CONFIG_IRQ_PREEMPT_CAN, static_cast<uint8_t>(i));
/* 1.6.13:RX 提到 preempt=0 —— 见 sys/config.h CONFIG_IRQ_PREEMPT_CAN_RX 注释。 */
nvic_irq_enable(ch.rx0Irq, CONFIG_IRQ_PREEMPT_CAN_RX, static_cast<uint8_t>(i));
s_rxQueues[i].head = 0U;
s_rxQueues[i].tail = 0U;
@@ -987,7 +1099,9 @@ CanFd::CanTxResult CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id,
#ifdef GD32E508
tx_msg.fd_flag = (dlc > 8U) ? ENABLE : DISABLE;
#if (CONFIG_CAN_FD_BRS_ENABLE != 0)
tx_msg.fd_brs = (dlc > 8U) ? ENABLE : DISABLE;
/* 电机 BOOT 收不了 BRS 数据段,OTA 期间 relay 对该通道压制(setChannelBrsSuppress),
* DLC>8 帧回仲裁段速率(FD-no-BRS,OTA 长期验证过的组合)。 */
tx_msg.fd_brs = (dlc > 8U && !s_brsSuppress[channelIndex]) ? ENABLE : DISABLE;
#else
tx_msg.fd_brs = DISABLE;
#endif
@@ -1182,6 +1296,17 @@ void CanFd::setChannelAutoRetrans(uint8_t channelIndex, bool enable)
(CAN_CTL(canPeriph) & CAN_CTL_ARD) ? 1U : 0U);
}
void CanFd::setChannelBrsSuppress(uint8_t channelIndex, bool suppress)
{
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
return;
}
/* 仅置运行时标志,打点在 transmitFrame:BRS 是逐帧属性(fd_brs 位),无需动寄存器。
* 写方(relay enterState 钩子)与读方(主循环/CS 中断的 transmitFrame)间 bool 单写单读,原子。 */
s_brsSuppress[channelIndex] = suppress;
CAN_LOGI("[CAN] CH%u set brs_suppress=%u\r\n", (unsigned)channelIndex, (unsigned)suppress);
}
uint32_t CanFd::readChannelErrorReg(uint8_t channelIndex)
{
if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex]) {
@@ -1292,7 +1417,113 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
can_receive_message_struct rx_message{};
can_struct_para_init(CAN_RX_MESSAGE_STRUCT, &rx_message);
can_message_receive(ch.canPeriph, CAN_FIFO0, &rx_message);
/* 自己取帧并校验(见 receiveFrameChecked):厂商库不校验也不等 RFD 自清,
* 会把"半条上一帧 + 半条本帧"的拼接体当正常帧交上来。
*
* 1.6.10:**校验失败只丢不重取**。1.6.7 起这里是"原地重取最多 3 次",
* 实测被证伪为撕裂的直接制造者:RFIFOMDATA0 是"重复读同一地址、硬件读
* 指针自动前进"(厂商驱动证实),attempt1 失败时读指针已随取数前进若干
* 字,重取的 attempt2 从错位指针开读 —— MI/MP 回读比对的是"窗口身份",
* 管不住"指针相对窗口起点的错位",于是校验通过、内容却是"上一帧的尾巴
* 拼下一帧的开头"。自测实锤:重取版 1.6.9 在电机活跃时错标率 9.155%
* (56835/620838),而 rx_torn 全程恒 0 —— 拼接恰好都从校验里溜过去。
* 丢帧无害:STATUS/CONFIG 都是 1kHz 周期量,下拍即补;递上去才会变乱码。
* CAN ISR 是 preempt=2,EtherCAT 的 SYNC(0)/ESC(1)/SYNC1(1) 都能抢占它,
* 这正是取数被打断的来源,也解释了为什么只有 EtherCAT 链路上看得到拼接、
* WiFi 链路干净。 */
const bool intact = receiveFrameChecked(ch.canPeriph, &rx_message);
if (!releaseFifo0(ch.canPeriph)) {
s_diag.rx_release_timeout_count++;
}
if (!intact) {
/* 校验判为拼接:整帧作废,不计 rx_count、不入队、不转发。
* 本版起 rx_torn_count 恢复真实语义 —— 它恒 0 说明校验没在挡东西。 */
s_diag.rx_torn_count++;
continue;
}
/* 本帧的 ID 用局部量,不要读 s_diag.last_rx_id ——三路通道共用那个全局,
* 嵌套/交错时会串出"A 通道的 id 配 B 通道的数据"。 */
const uint32_t rxId = (rx_message.rx_ff == CAN_FF_STANDARD)
? rx_message.rx_sfid : rx_message.rx_efid;
/* ── 1.6.11:内容级拼接帧拦截 ──
* MI/MP 回读校验管不住 FIFO 缓冲指针与帧边界的失同步(1.6.10 的
* settle 延迟实测无效,指纹实验证明 node3 的 STATUS 帧头 24B 可以是
* node2 的完整 CONFIG —— 错位恰为 CONFIG 帧长 6 字,与混合帧长交错
* 相关,是 CAN IP 的指针病理,软件无法从取数时序上根除)。
* 改为在入队前做结构自洽校验:拼接帧的 (id, data) 必矛盾,而真帧必自洽。
* STATUS(0x000N0100, dlc48):CONFIG 头灌入必然把 MCU/Motor 温度推出
* [-40,125]℃ 物理量程(CONFIG B0-1 = 03+限压低字节 → 123~250℃ 或
* 负值),母线电压同步失序;
* CONFIG(0x000N0101, dlc24):DBC v0.74 规定 B15-23 未使用恒 0,
* STATUS 体灌入几乎必非 0;模式字节限 0..7(电机模式表最大 7)。
* 命中即计数(1.6.12 起不再丢帧——管道必须诚实,计数仅做"源头是否
* 还在产拼接帧"的测量):源头修复生效后本计数应恒 0,非 0 即失效告警。
* 计数经段5@494 上报。
* 残余风险:限压字节恰使温度落回量程内的理论组合(24V 台架实测限压
* ≥15V → 对应"温度"≥153℃,仍在量程外),可接受。 */
{
const uint16_t idLo = (uint16_t)(rxId & 0xFFFFU);
bool reject = false;
/* 上一帧已接受的温度/电压(按节点):拼接帧的 CONFIG 字节可能恰好落进
* 静态量程(实测 vel=150 指纹时 0301→2.59℃、5a00→0.9℃、cace→52.9V
* 全部"合法"漏过),但绝不可能在 1ms 帧间隔里物理地跳过去 —— 斜率判据
* 是静态量程补不上的洞。阈值留 3 个数量级余量:温度 20℃/帧(实际
* ≤0.005℃),电压 5V/帧(实际 ≤0.05V)。 */
static int16_t s_lastMcu[16];
static int16_t s_lastMot[16];
static uint16_t s_lastPwr[16];
static uint8_t s_lastOk[16];
if (idLo == 0x0100U && rx_message.rx_dlen == 48U) {
const int16_t mcu = (int16_t)((uint16_t)rx_message.rx_data[0] |
((uint16_t)rx_message.rx_data[1] << 8));
const int16_t mot = (int16_t)((uint16_t)rx_message.rx_data[2] |
((uint16_t)rx_message.rx_data[3] << 8));
const uint16_t pwr = (uint16_t)rx_message.rx_data[11] |
((uint16_t)rx_message.rx_data[12] << 8);
if (mcu < -4000 || mcu > 12500 || mot < -4000 || mot > 12500 ||
pwr < 8000 || pwr > 60000) {
reject = true;
}
const uint8_t nd = (uint8_t)((rxId >> 16) & 0x0FU);
if (!reject && nd >= 1U && nd < 16U && (s_lastOk[nd] & 1U)) {
int16_t dmcu = (int16_t)(mcu - s_lastMcu[nd]);
int16_t dmot = (int16_t)(mot - s_lastMot[nd]);
if (dmcu < 0) { dmcu = (int16_t)-dmcu; }
if (dmot < 0) { dmot = (int16_t)-dmot; }
/* 只比温度斜率:拼接帧的 CONFIG 头必然把 mcu/mot 推出 20℃/帧
* (实测最小跳变 35℃,而真实温度 ≤0.005℃/帧,4 个数量级余量)。
* 电压不比斜率 —— 正弦换向/堵转时母线真实跌落可超 5V/帧,
* 误杀会让显示冻结;拼接的电压特征(≈48V)已由量程+温度斜率兜住。 */
if (dmcu > 2000 || dmot > 2000) {
reject = true;
}
}
if (!reject && nd >= 1U && nd < 16U) {
s_lastMcu[nd] = mcu;
s_lastMot[nd] = mot;
s_lastPwr[nd] = pwr;
s_lastOk[nd] = 1U;
}
} else if (idLo == 0x0101U && rx_message.rx_dlen == 24U) {
if (rx_message.rx_data[0] > 7U) {
reject = true;
}
for (uint8_t i = 15U; !reject && i < 24U; ++i) {
if (rx_message.rx_data[i] != 0U) {
reject = true;
}
}
}
if (reject) {
/* 1.6.12:只计不丢 —— 数据管道必须诚实(用户裁定:过滤是遮羞布,
* 修的是采集源头)。网页/ESP32/主站看到的就是源头产出的原始字节,
* 本计数退化为纯测量仪表:源头修好 = 恒 0;还在涨 = 源头问题仍在,
* 且网页如实显示着,不存在"看着干净其实坏了"。 */
s_diag.rx_content_reject_count++;
}
}
s_diag.rx_count++;
s_diag.last_rx_channel = channelIndex;
@@ -1300,14 +1531,14 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
s_diag.last_rx_fd_flag = rx_message.fd_flag;
s_diag.last_rx_brs = rx_message.fd_brs;
s_diag.last_rx_esi = rx_message.fd_esi;
s_diag.last_rx_id = (rx_message.rx_ff == CAN_FF_STANDARD) ? rx_message.rx_sfid : rx_message.rx_efid;
s_diag.last_rx_id = rxId;
/* CAN0 桥接模式:将刚收到的原帧透传到 CAN1 与 CAN2,保留 11/29 位、FD、BRS、ESI。 */
if (channelIndex == 0U && s_mode == Mode::kModeBridge0To12) {
for (uint8_t bridgeCh = 1U; bridgeCh <= 2U; ++bridgeCh) {
const uint32_t bridgePeriph = kChannels[bridgeCh].canPeriph;
const bool ok = bridgeTransmitRaw(bridgePeriph, rx_message);
if (ok) {
s_diag.last_tx_id = s_diag.last_rx_id;
s_diag.last_tx_id = rxId;
s_diag.last_tx_channel = bridgeCh;
s_diag.last_payload_length = rx_message.rx_dlen;
} else {
@@ -1320,7 +1551,7 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
/* 维护每通道独立计数与最近一帧快照,用于 1Hz 反馈给 ESP32。 */
auto &stats = s_channelStats[channelIndex];
stats.rx_count++;
stats.last_rx_id = s_diag.last_rx_id;
stats.last_rx_id = rxId;
stats.last_rx_dlc = rx_message.rx_dlen;
stats.last_rx_fd_flag = rx_message.fd_flag;
stats.last_rx_brs = rx_message.fd_brs;
@@ -1336,7 +1567,6 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
/* 维护每通道不重复 ID 快照表,用于 1Hz 分页拉取给 ESP32。 */
{
auto &table = s_idSnapshot[channelIndex];
const uint32_t rxId = s_diag.last_rx_id;
/* 查找已有条目 */
uint8_t found = CANFD_ID_SNAPSHOT_MAX;
for (uint8_t si = 0U; si < table.count; ++si) {
@@ -1387,7 +1617,7 @@ void CanFd::handleRxInterrupt(uint8_t channelIndex)
auto &frame = queue.frames[queue.tail];
frame.channel = channelIndex;
frame.id = s_diag.last_rx_id;
frame.id = rxId;
frame.length = rx_message.rx_dlen;
for(uint8_t i = 0; i < rx_message.rx_dlen; ++i) {
frame.data[i] = rx_message.rx_data[i];
+15
View File
@@ -53,6 +53,15 @@ public:
uint32_t rx_drop_count;
uint32_t rx_fifo_full_count;
uint32_t rx_fifo_overfull_count;
/** 取帧期间被硬件换帧、整帧丢弃的次数(1.6.6,TS/DLC 前后不一致即判定)。
* 这是"读到半条帧"的唯一可观测量,稳态下应当只在高负载时缓慢增长。 */
uint32_t rx_torn_count;
/** 释放邮箱后等待 RFD 硬件自清超时的次数(手册 26.3.4 第 3 步要求等待)。 */
uint32_t rx_release_timeout_count;
/** 内容级拼接帧拦截次数(1.6.11):(id, data) 结构自相矛盾(STATUS 的
* 温度/电压出量程、CONFIG 的未使用区非 0)⇒ 整帧作废。与 rx_torn 互补:
* torn 挡"取数期间窗口被换",这个挡"窗口没换但 FIFO 缓冲指针错位"。 */
uint32_t rx_content_reject_count;
uint32_t tx_abort_count;
uint32_t last_rx_id;
uint32_t last_tx_id;
@@ -173,6 +182,11 @@ public:
/** @brief 运行时切换指定通道 CAN 自动重发(ARD 位,需 init-mode);停等式 OTA 期间关闭、结束后恢复。 */
static void setChannelAutoRetrans(uint8_t channelIndex, bool enable);
/** @brief 运行时压制指定通道 TX 帧的 BRS:压制中 DLC>8 的 FD 帧数据段回退仲裁段速率。
* 电机 BOOT 无法正确接收 BRS 高速段(见 sys/config.h CONFIG_CAN_FD_BRS_ENABLE 注释),
* 电机 OTA relay 经 enterState 钩子与 setChannelAutoRetrans 同点切换,OTA 结束必恢复。 */
static void setChannelBrsSuppress(uint8_t channelIndex, bool suppress);
/** @brief 读取指定通道 CAN_ERR 寄存器原始值(事件时刻只读快照,不依赖 1Hz 采样)。
* 解码:TEC=bit16-23,REC=bit24-31,ERRN=bit4-6;无效通道返回 0。仅供诊断日志,禁用于发送门控。 */
static uint32_t readChannelErrorReg(uint8_t channelIndex);
@@ -294,6 +308,7 @@ private:
static uint8_t s_nextConsumeChannel;
static volatile Mode s_mode;
static ChannelStats s_channelStats[3];
static bool s_brsSuppress[3]; /**< TX BRS 压制(电机 OTA 期间该通道 DLC>8 帧禁 BRS) */
static ChannelFeedback s_feedback[3];
/** 各通道硬件 init(时序+退出 init mode)是否成功 */
static bool s_channelHwOk[3];
+20 -6
View File
@@ -2,8 +2,10 @@
* @file motor_state.cpp
* @brief 多电机状态表实现 — CAN STATUS 帧识别、内存缓存、SPI 推送序列化。
*
* 所有函数仅在主循环上下文调用(taskCanRxParse / taskHeartbeat / pushStep),
* 无 ISR 竞态,不需要临界区保护。
* 本模块的函数只在主循环上下文调用(taskCanRxParse / taskHeartbeat / pushStep),
* 但消费者不止主循环:EtherCAT 的 buildInput 跑在 PDI_Isr,会抢占 STATUS 写侧,
* 故 STATUS 的 seq/data 由 MotorSlot.wr_seq(seqlock)保护,见 onCanFrame。
* SPI 侧的 buildPayload 与写侧同上下文,天然无此问题。
*/
#include "device/motor_state.h"
@@ -36,7 +38,7 @@ void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len)
const uint32_t masked = id & kMotorStatusIdMask;
/* STATUS 帧 0x000N0100 (DLC 32, 实时值) */
/* STATUS 帧 0x000N0100 (DLC 48, 实时值,字节 32~35 = InitStatus) */
if (masked == kMotorStatusIdMatch) {
const uint8_t node = static_cast<uint8_t>((id >> 16) & 0x0FU);
if (node < 1U || node > kMotorMaxNodes || channel >= kMotorChannels) {
@@ -45,6 +47,14 @@ void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len)
MotorSlot &slot = s_motorTable.slots[(node - 1U) * kMotorChannels + channel];
/* seqlock 写侧(1.6.5):奇数=本槽 seq/data 正在改。EtherCAT 的 buildInput
* 跑在 PDI_Isr,会抢占这里;抢占落在下面那次 memcpy 中途就会发布
* "新 seq + 半新半旧 48B"——两帧字节拼成一个 STATUS,边界若落在 float 的
* 符号/指数字节上就是网页上的离谱值。缺的不是顺序而是让消费者能认出
* "这一段没写完"的标记。 */
++slot.wr_seq; /* → 奇数 */
__DMB(); /* 普通写不得重排到奇/偶两次自增之外 */
slot.node_id = node;
slot.channel = channel;
slot.online = 1U;
@@ -57,6 +67,9 @@ void onCanFrame(uint8_t channel, uint32_t id, const uint8_t *data, uint8_t len)
if (slot.dlc < kMotorStatusDlc) {
std::memset(&slot.data[slot.dlc], 0, kMotorStatusDlc - slot.dlc);
}
__DMB();
++slot.wr_seq; /* → 偶数:本槽整体一致 */
return;
}
@@ -159,9 +172,10 @@ uint16_t buildPayload(uint8_t page, uint8_t *buf, uint16_t maxLen)
writeU32Le(&buf[offset + 4U], slot.rx_count);
writeU32Le(&buf[offset + 8U], slot.seq);
std::memcpy(&buf[offset + 12U], slot.data, kMotorStatusDlc);
writeU32Le(&buf[offset + 44U], slot.config_rx_count);
writeU32Le(&buf[offset + 48U], slot.config_seq);
std::memcpy(&buf[offset + 52U], slot.config_data, kMotorConfigDlc);
/* STATUS DLC 48 后,CONFIG 字段整体后移 16B */
writeU32Le(&buf[offset + 60U], slot.config_rx_count);
writeU32Le(&buf[offset + 64U], slot.config_seq);
std::memcpy(&buf[offset + 68U], slot.config_data, kMotorConfigDlc);
offset += kMotorEntrySize;
count++;
+10 -5
View File
@@ -32,7 +32,7 @@ static constexpr uint8_t kMotorMaxNodes = CONFIG_MOTOR_MAX_NODES;
* 状态表按 node×channel 双键存储,各通道独立槽位互不覆盖。 */
static constexpr uint8_t kMotorChannels = 3U;
static constexpr uint8_t kMotorSlotCount = kMotorMaxNodes * kMotorChannels; /* 9×3 = 27 */
static constexpr uint8_t kMotorStatusDlc = 32U;
static constexpr uint8_t kMotorStatusDlc = 48U; /* v0.74:STATUS 帧 DLC 48(字节 32~35 = InitStatus) */
static constexpr uint8_t kMotorConfigDlc = 24U; /* CONFIG 帧 DLC */
static constexpr uint16_t kMotorOfflineSec = CONFIG_MOTOR_OFFLINE_MS / 1000U;
@@ -50,18 +50,18 @@ static constexpr uint32_t kBootHeartbeatTimeoutSec = 3U; /* >3s 无 0x103 →
static constexpr uint8_t kMotorActiveBootBit = 0x80U; /* 0xC5 page0 header active 字节 bit7 = boot_active */
/* ── 序列化常量 (0xC5 payload) ── */
static constexpr uint8_t kMotorEntrySize = 76U; /* 1+1+1+1+4+4+32+4+4+24 (STATUS+CONFIG) */
static constexpr uint8_t kMotorEntrySize = 92U; /* 1+1+1+1+4+4+48+4+4+24 (STATUS+CONFIG) */
static constexpr uint8_t kMotorPageHeaderLen = 4U; /* page+total+count+active */
static constexpr uint8_t kMotorEntriesPerPage = 3U; /* (236-4)/76 = 3.05 → 3 */
static constexpr uint8_t kMotorEntriesPerPage = 2U; /* (236-4)/92 = 2.52 → 2 */
static constexpr uint8_t kMotorTotalPages =
(kMotorSlotCount + kMotorEntriesPerPage - 1U) / kMotorEntriesPerPage; /* ceil(27/3) = 9 */
(kMotorSlotCount + kMotorEntriesPerPage - 1U) / kMotorEntriesPerPage; /* ceil(27/2) = 14 */
/// 单电机状态槽
struct MotorSlot {
uint8_t node_id; ///< 1-9, 0=空槽
uint8_t channel; ///< 最后收到帧的 CAN 通道 (0/1/2)
uint8_t online; ///< 1=在线, 0=离线
uint8_t dlc; ///< 最后帧 DLC (正常=32)
uint8_t dlc; ///< 最后帧 DLC (正常=48)
uint32_t rx_count; ///< 累计收到 STATUS 帧数
uint32_t last_rx_sec; ///< 最后收到帧时的 g_app_uptime_sec
uint32_t seq; ///< 更新序号 (每次刷新 +1)
@@ -69,6 +69,11 @@ struct MotorSlot {
uint32_t config_rx_count; ///< 累计收到 CONFIG 帧数
uint32_t config_seq; ///< CONFIG 更新序号
uint8_t config_data[kMotorConfigDlc]; ///< 原始 CONFIG payload (目标值回报)
/* STATUS 写侧 seqlock(1.6.5):奇数=正在写本槽的 seq/data。
* 只有主循环(onCanFrame 的 STATUS 分支)自增,ISR 只读 —— 单核下消费者是
* ISR,它执行期间生产者被冻结,故"读到偶数 ⇒ 生产者不在这段里"成立。
* 放结构体末尾,已有字段偏移全不变(SPI 序列化/PDO 布局照旧)。 */
volatile uint8_t wr_seq;
};
/// 全局电机状态表
+22 -1
View File
@@ -122,7 +122,17 @@ constexpr bool CONFIG_CAN_CH2_ENABLE = true;
* ============================================================ */
constexpr uint32_t APP_FW_VERSION_MAJOR = 1U;
constexpr uint32_t APP_FW_VERSION_MINOR = 6U;
constexpr uint32_t APP_FW_VERSION_PATCH = 1U; /* 1.6.1:输入 PDO 瘦身 640→496——批量接收条目数据 64→32B(真机 6086 帧 DLC 仅 24/32,超长帧截断打 flags bit1,环形队列仍存全量)、段5 诊断区 632→484;DWT 实测 640B PRAM 写 296µs/拍且与 SYNC0 相位无关(≈0.463µs/B 线性),预期写侧 −67µs/拍。五处一致(pdo_size/bridge/dc_app/gen_eeprom/ESI)已同步,需重烧固件+EEPROM 并断电冷启动,主站 decode_input 按 Ibytes 双布局自适应可任意先后升级。 */
constexpr uint32_t APP_FW_VERSION_PATCH = 13U; /* 1.6.13:根治 —— 1.6.12 的 PDI 入口预抽实测(60s 正弦指令负载,链路活性已断言):overfull 50/s→0 但指纹违规仍 ~92/s、torn 在无溢出阶段 1-7/s 持续涨 —— 判决:溢出只是共犯之一,**357µs 的 PDI 阻塞窗口本身**才是病理母体(窗口内 FIFO 满溢毁窗口 + 被冻结的取帧路径留着不依赖溢出的换帧路径,MI/MP 只抓到少数)。本版釜底抽薪:CAN RX ISR 提到 preempt=0(CONFIG_IRQ_PREEMPT_CAN_RX,与 SYNC0 同级互不抢占)—— RX 不再被 PDI/SYNC1/SCHED 压住,FIFO 深度 ≤2、溢出与冻结同时消失,receiveFrameChecked 的多字取数自此真正原子;TX ISR 留在 2(PDI 上下文有 TX 提交,提 0 会引入提交/释放竞争)。代价:SYNC0/PDI 偶发 µs 级延迟(RX ISR ~3µs/帧×8kHz≈2.4% 占空),相对 1ms 周期是零头。PDI 预抽 pumpAllChannels 随之撤除(失去意义且引入 PDI/ISR 双消费者风险)。保留 1.6.12 的诚实管道(校验只计不丢)与诊断布局(overfull@490、relto@492、seq u16@488)作为验证仪表:修好 = reject/torn/overfull 恒 0。 */
/* 1.6.12:源头修复 —— 用户裁定"过滤是遮羞布,修采集源头,前端只许诚实反馈"。1.6.11 上线后实测(repro_cmdload 正弦负载)reject@494 仍以 ~384/s 增长:RFOD=DISABLE 挡不住溢出毁窗口(本 IP 满溢时 DISCARD 语义存疑,或毁帧发生在满态下的读并发路径),内容校验丢弃只是把坏数据拦在计数前。本版两处:(1) **源头**:CanFd::pumpAllChannels() 挂进 onEscIrqExti 的 PDI_Isr 入口 —— 在 buildInput 357µs 长阻塞(CAN ISR preempt=2 全程被压)开始之前把三路 FIFO 抽干,阻塞窗口起点深度=0、窗口内到达 ≤2-3 帧,3 深度 FIFO 从"必然打满"变成"必然不满";preempt=1 + SYNC0 已关的上下文保证与 CAN ISR 取帧互斥,与主循环批量队列生产零交集。(2) **诚实管道**:内容校验改为只计不丢(reject@494 恒 0 = 源头修好;还在涨 = 源头未愈且页面如实显示),诊断区加挂判决性测量:seq 488 拆 u16、overfull(RFO0)@490、release_timeout@492(原重入计数位,恒 0 让出)。reject 与 overfull 是否同涨 = "残余拼接是否仍源自溢出"的判决。 */
/* 1.6.11:根治残余拼接 —— 1.6.10 的 settle 延迟实测无效(正弦指令负载下拼接率不变),节点指纹实验(两电机反相正弦+速度限 200/150 打指纹)实锤拼接是**跨节点**的:node3 的 STATUS 帧头 24B = node2 的完整 CONFIG(速度指纹 200.0、角度 1142° 均属 node2 轨迹),且 CONFIG↔CONFIG 同型拼接结构校验抓不到(两边都合法)—— 电机 TX 不可能发出别的电机的字节 ⇒ 混合点在 GD32 RX 侧。真根因:can_init 的 rec_fifo_overwrite=ENABLE —— FIFO 满时覆盖最旧邮箱,而最旧邮箱正是 CPU 正在读的输出窗口;buildInput 在 PDI_Isr 占 ~357µs 压住 CAN ISR,发指令时总线 ~8kHz、阻塞窗口内 ~3 帧打满 FIFO 深度 → 覆盖进行中被读走 = 拼接(MI/MP 回读挡不住:MI 是覆盖序列最后更新的一环;settle 延迟也挡不住:非时间竞态而是溢出策略)。本版:(1) rec_fifo_overwrite=DISABLE —— 溢出丢新帧置 RFO0(rx_fifo_overfull_count 已统计),周期量丢一拍下拍即补,与软件队列"满丢新不覆旧"同则;(2) 撤 settle 延迟(无效且白烧 CPU);(3) 保留 1.6.10 的重取循环删除与边界修正,并新增入队前内容校验(STATUS 温度/电压量程+帧间斜率、CONFIG 未使用区/模式)丢结构矛盾帧、计数段5@494 —— 作为溢出之外的纵深防御(实测能挡跨型拼接,挡不住同型 CONFIG↔CONFIG,故必须配合 (1))。验证:正弦指令负载 60s,指纹违规 40→0。 */
/* 1.6.10:根治拼接帧 —— 1.6.9 的"队列覆写"定性被自测推翻为**次要**:段 2 电机槽(MotorState 路径,与批量环形队列完全独立)出现同签名拼接(node2 槽头 = node3 CONFIG 前缀 03c12200,解出 mcuTemp=-161.25),且 rx_torn/pdo_reentry 自启动恒 0 —— 两条独立下游同时中招 ⇒ 混合点在共同上游 CAN RX ISR;校验恒不报 ⇒ 拼接发生在 MI/MP 判据盲区。真根因=1.6.7 的"校验失败原地重取":RFIFOMDATA0 读指针随读自动前进且不因换帧复位,attempt1 失败后 attempt2 从错位指针开读,MI/MP 比对的是窗口身份、管不住指针错位,于是"校验通过、内容为上一帧尾巴拼下一帧开头"。实测(自测脚本 scripts/selftest_torn.py,按拍去重+灵敏度自证):空闲 0%(35k 条),电机活跃时 9.155%(56835/620838,STATUS 错标为 CONFIG 占绝对多数)。本版:(1) 去掉重取循环,校验失败即 release+丢帧+rx_torn 计数(1.6.6 语义,STATUS/CONFIG 均 1kHz 周期量,丢一拍下拍即补);(2) fillRxBatch 边界改判段 5 起点 ECAT_DIAG_OFF_LOAD(484) 而非 PDO 全长(496)——潜伏越界写诊断区,当前容量下触不到,容量放宽即咬人;(3) releaseFifo0 自清后补 settle 延迟 —— 烧(1)(2)后实测活跃流量下仍残余 0.11% 跨节点拼接(node2 槽头=node3 CONFIG 前缀,且 rx_torn 已正常计数 146,证明窗口换帧路径已被挡住),定性为 RFD 自清只代表 dequeue 握手完成、下一帧 MDATA 还在逐字搬运(MI/MP 先落数据区后落),while 连排的第二次读紧跟 release 即拿到陈旧头+新鲜尾,MI/MP 回读对这种陈旧全盲;空闲每 ISR 单帧故 0%、活跃连排才中招,与实测分布吻合。 */
/* 1.6.9:拿到实证并定位真根因 —— 给主站加了同拍 raw+parsed 的只读诊断路由(/api/motor/rxraw)后抓到:**坏帧在原始 PDO 字节里就已经是错的**(如 off=72 处条目声称 id=0x20101/dlc=24,数据却是合法 STATUS:mcuTemp=35.92 motTemp=33.55 pwrV=23.972),而主站 parsed 与 raw 逐字节一致 —— 坏在从站写入侧,与主站解析无关。根因:批量接收环形队列长期饱和下,生产者覆写了消费者正在读的槽。实测证据:CAN 侧约 4000 帧/s 而批量区每拍最多 3 条(=3000 帧/s),净积压使 pending 恒 8/12、lost 几乎每拍都有、count 恒取满 3;消费者 fillRxBatch 又攒到循环末才发布 s_rxRingTail,生产者判满用的正是这个滞后值,于是把新帧写进消费者刚读过但未发布的槽,拷进 PDO 的 id/dlc/data 遂来自不同帧(错标率 1.77%)。两处修复:(1) onCanFrame 入队**过滤 STATUS 帧** —— 完整 STATUS 本就由段 2 按槽上报,主站 drainRx() 拿到 STATUS 也直接 continue 丢掉,让它占队列纯属重复,正是它把队列压爆;(2) fillRxBatch 改为整条拷贝到局部副本再用,并逐条发布 tail,使生产者判满始终基于真实进度。
1.6.8:曾判定为"输入 PDO 填充跨上下文重入"并加了双缓冲发布 + 重入计数(段5@492)。**该假说事后被证伪**:SM-Synchron 稳态下 MainLoop 分支的门控 (!bEscIntEnabled||!bEcatFirstOutputsReceived)&&!bDcSyncActive 恒为假不执行,Sync0 因 free-run 无 DC 脉冲不触发,Sync1 与 PDI_Isr 同为 preempt=1 不能抢占 —— buildInput 实际只在 PDI_Isr 单上下文跑。双缓冲发布本身是正确加固,予以保留。
1.6.7:接着 1.6.6 治 CAN 取帧拼接。1.6.6 的校验只读 MP 的 TS/DLENC,TS 是 16 位计数器、相邻帧可能同值,DLENC 更常同为 48,于是"ID 报 STATUS、数据却是 CONFIG 帧"这种错配从校验漏了过去。本版三处改动:(1) 回读校验再加上 MI(完整 ID/帧格式),ID 变了就一定是换了帧,没有巧合空间;(2) 校验失败改为原地重取最多 3 次而不是直接丢弃 —— 失败时窗口里躺着的正是完整的新帧,重读一次通常就能拿全;(3) 整段取数放进 BASEPRI 临界区,挡住 preempt>=1 的 EtherCAT 中断。注:事后(1.6.8)证明本版方向不是主因,但这三条本身仍是正确的加固,予以保留。
1.6.6:根治 ECAT/CAN 偶发乱码 —— 取帧不再用厂商库 can_message_receive。一条 48B CANFD 报文要对同一地址 CAN_RFIFOMDATA0 连读 12 次才取完,整个过程不原子;而厂商库置 RFD0 后既不等硬件自清也不校验,取数过程会横跨 dequeue、读出两帧拼接。现改为本地 receiveFrameChecked():取数前后各读一次 MP,比对 TS[31:16] 与 DLENC,不一致即判定邮箱窗口被换帧,整帧丢弃并计入 Diag::rx_torn_count(经输入 PDO 段 5 偏移 486 原保留位上报,PDO 长度不变、EEPROM 无需重刷);releaseFifo0() 置位后轮询等自清,且改用直接赋值 —— 原来的 |= 是读改写,会把期间硬件置起的 RFF0/RFO0(rc_w1) 顺手清掉,FIFO 溢出证据就是这么丢的。另修 ISR 里 frame.id/stats.last_rx_id 取自三路共享的 s_diag.last_rx_id(而 data 取自局部 rx_message,会串出"A 通道 id 配 B 通道数据"),一律改用本帧局部 rxId。
1.6.5:段 2 电机槽补 seqlock(MotorSlot.wr_seq)。buildInput 跑在 PDI_Isr,会抢占主循环的 MotorState::onCanFrame,而那里是"先 seq=++global_seq、后 memcpy(slot.data,48)"——抢占落在 memcpy 中途就会发布"新 seq + 半新半旧 48B",主站只按 seq 判新鲜度分辨不出,混合边界若落在 float 的符号/指数字节(B16/B20/B24)就是网页上的偶发离谱值。现消费者见 wr_seq 为奇数即跳过本槽(该 56B 留零,主站按 node_id==0 跳过、下拍自愈),win_count 改报窗口跨度而非填充数以保持按 i 定位。CONFIG 路径不套锁:它只写 config_* 与恒等的 node_id/channel,buildInput 不同读这些。槽布局不变 ⇒ 输入 PDO 仍 496B,EEPROM/SII 无需重刷。
1.6.4:电机槽 40B→56B(8B 头 + v0.74 的 48B STATUS),每拍槽数 7→5(段 2 的 280B 正好 5×56)。修掉两件事:buildInput 按 48B 拷贝时溢出 16B 盖住段 3 长帧快照;主站侧 STATUS 被截到 32B 使 InitStatus(B32-35) 永远拿不到、网页把"无数据"显示成 NORMAL。槽数×槽长不变 ⇒ 输入 PDO 仍 496B,EEPROM/SII 无需重刷;主站需同步 MOT_SLOT_SIZE / MOT_SLOTS_PER_PDO / MOT_STATUS_DLC。
1.6.3:processOutput 不再从 out 回读 WIN@8——1.6.0 起 out 只在 seq 沿那拍新鲜,无沿周期里它是上一次全量读的残值,于是每拍用开机首拍的 0 盖掉 peekOutput 刚取到的新值,电机状态扫描窗口被钉死在基址 0,槽位 7..26(node3 及以上)永不上报,只有主站真发一帧才复位。WIN 的每拍刷新改由 peekOutput 独占。判据:不发任何帧,从站回显 win_base 也应等于主站请求值。 */
constexpr uint32_t APP_FW_VERSION_BUILD = 0U;
@@ -298,6 +308,17 @@ constexpr uint32_t CONFIG_NVIC_PRIORITY_GROUP = NVIC_PRIGROUP_PRE4_SUB0
constexpr uint32_t CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC = 0U;
constexpr uint32_t CONFIG_IRQ_PREEMPT_SCHED = 1U;
constexpr uint32_t CONFIG_IRQ_PREEMPT_CAN = 2U;
/* 1.6.13:CAN RX 提到 preempt=0(与 EtherCAT SYNC0 同级)。
* 1.6.12 的 PDI 入口预抽实测:溢出确实大降(overfull 50/s→0)但拼接照旧
* (指纹违规 ~92/s,且 torn 在无溢出阶段仍 1-7/s 在涨)—— 357µs 的 PDI
* 阻塞窗口本身就是病理:窗口内 FIFO 满溢毁窗口的同时,被冻结的取帧路径
* 还留着不依赖溢出的换帧路径(MI/MP 只抓到少数)。preempt=0 一并掐断:
* RX ISR 不再被 PDI/SYNC1/SCHED 压住(FIFO 深度 ≤2,溢出与冻结同时消失),
* 且与 SYNC0 同级互不抢占 —— receiveFrameChecked 的多字取数自此真正原子。
* TX ISR 留在 2:PDI 上下文有 TX 提交,提到 0 会引入提交/释放竞争。
* 代价:SYNC0/PDI 偶发 µs 级延迟(RX ISR ~3µs/帧 × ~8kHz ≈ 2.4% 占空),
* 相对 1ms 周期与 165µs 总线时间是零头。 */
constexpr uint32_t CONFIG_IRQ_PREEMPT_CAN_RX = 0U;
constexpr uint32_t CONFIG_IRQ_PREEMPT_ETHERCAT_ESC = 1U;
constexpr uint32_t CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC1 = 1U;
constexpr uint32_t CONFIG_IRQ_PREEMPT_SPI_USART = 3U;
+4
View File
@@ -229,8 +229,12 @@ static void enterState(State st) {
|| prev == State::kFinishOta);
if (otaActive && !wasActive) {
CanFd::setChannelAutoRetrans(s_channel, false); /* OTA 开始:单发 */
CanFd::setChannelBrsSuppress(s_channel, true); /* OTA 开始:压 BRS——电机 BOOT 收不了
BRS 数据段,nonce/confirm/64B 数据包
(DLC>8) 会全部静默丢失 */
} else if (!otaActive && wasActive) {
CanFd::setChannelAutoRetrans(s_channel, true); /* OTA 结束:恢复重发 */
CanFd::setChannelBrsSuppress(s_channel, false); /* OTA 结束:恢复 BRS(EtherCAT 桥接吞吐) */
}
}