Compare commits

...
5 Commits
Author SHA1 Message Date
cbb 0cd3d9bed2 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格式。

这些修改解决了上位机观察到的帧标签错误、电机状态丢失和数据拼接问题。
2026-09-08 00:52:35 +08:00
cbb ace5a1671b chore(config): 固件版本 bump 1.6.1(输入 PDO 瘦身记录, 见 9f1c2d2) 2026-09-06 00:20:51 +08:00
cbb 9f1c2d25dd feat!: 1.6.0 DWT 剖分探针 + 1.6.1 输入 PDO 瘦身 640→496(待重烧验收)
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 节)
2026-09-06 00:19:18 +08:00
cbb 70daa4b0c6 feat(DCU_APP): 添加CPU负载监控和EtherCAT诊断功能
- 在DCU_APP项目中添加cpu_load.h和cpu_load.cpp模块用于CPU负载率监测
- 修改csolution配置将构建目标从Debug切换为Release
- 在EtherCAT PDO协议栈中新增段5诊断区(632-639字节),包含负载率和序列号
- 在所有关键中断服务程序中添加CpuLoad::isrEnter()/isrExit()钩子用于准确统计中断占用
- 增强EtherCAT同步中断处理,改进AL事件掩码守护机制,修复OP状态下PDI中断门控问题
- 解决SSC源码中指针地址对齐警告和打包成员访问警告问题
- 在主循环中集成CPU负载率采样tick()调用,实现1秒滚动窗口统计

BREAKING CHANGE: 构建配置从Debug改为Release,EtherCAT协议栈增加新的诊断数据区
2026-09-05 20:07:22 +08:00
cbb 2ebd4ff1d2 fix(ssc): 根治邮箱楔死(1.5.59): 几何常量化消灭建立路径, 守护字节序修复
- ecat_def.h: 新增 MBX_SM0/1_START/SIZE 构建期常量(=SII 真值 0x1000/0x1200 各 256B,
  与 gen_eeprom.py/DCU_ESI.xml 三方一致, 改 SII 邮箱尺寸必须同步)
- mailbox.c: MBX_StartMailboxHandler 常量直达缓存, CSR 单次间接读降为交叉校验
  (失配打 [ECAT][MBX] WARN)。根因: INIT→PREOP 单次 CSR 读错值(0x1204/0x1208,
  BUSLAT 余量外的 EXMC 毛刺)被缓存成 u16EscAddrSendMbx → 响应写错址 → 断电才清;
  常量化后错值无进入缓存的路径, 运行期内存踩踏也会在每次 INIT→PREOP 被覆写
- esc_lan9252.cpp: csrRead 双读 CSR_DATA 一致校验+整事务重试(CSR_DATA 为锁存
  寄存器非 FIFO, 连读必一致; 不一致即本拍总线毛刺)。瞬态错读曾致 CheckSmSettings
  以 0x0016/0x001F 打断转换
- dc_app.c: 修复守护/心跳 SWAPWORD 字节序 bug——ESC 寄存器 PDI/CSR 读即主机序
  (与 GetSyncMan/mailbox 裸读一致), 交换使 sm0Len=0x0001<8 恒短路, 1.5.52~1.5.58
  地址失配守护从未生效过; 现降级为运行期内存踩踏兜底并真正可用; ReadAlDiag 同修,
  期间串口日志的 al/evt 均为字节交换错值, 历史日志需交换重读
- task_runner.cpp: 守护注释同步
- 新增 _syntax_check.bat + _syntaxcheck_stubs: 无 AC6 环境用 VS2022 cl /Zs 验 C 文件

真机验收: 140 次 op/start 全成功(40 轮 DC 4ms + 100 轮 DC/free-run 交替),
每轮进 OP、SDO 0x2000:3/4 全成功, 无 0x0016/0x001D/0x001B。
2026-09-05 17:38:35 +08:00
34 changed files with 1257 additions and 304 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ solution:
target-set:
- set:
images:
- project-context: GD32E508_DCU_APP.Debug
- project-context: GD32E508_DCU_APP.Release
debugger:
name: ST-Link@pyOCD
clock: 4000000
@@ -16,6 +16,8 @@ project:
- file: ./sys/task_manager.cpp
- file: ./sys/task_runner.h
- file: ./sys/task_runner.cpp
- file: ./sys/cpu_load.h
- file: ./sys/cpu_load.cpp
- file: ./sys/ota_boot_request.h
- file: ./sys/ota_boot_request.cpp
- file: ./sys/aes128.h
@@ -163,6 +165,9 @@ project:
- -DHXTAL_VALUE=8000000U
- -gdwarf-5
- -D__MICROLIB
# 启动文件经 legacy armasm 汇编:消 armclang 驱动"参数未使用"告警 + armasm 弃用告警(A1950W)
- -Wno-unused-command-line-argument
- -Wa,armasm,--diag_suppress=A1950W
Link:
- --library_type=microlib
- --entry=Reset_Handler
+11
View File
@@ -0,0 +1,11 @@
@echo off
call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat" >nul 2>&1
cd /d "G:\Robot_v2\DCU\DCU_APP\GD32E508_DCU_APP"
set PACKED=/D"STRUCT_PACKED_START=" /D"STRUCT_PACKED_END=" /D"MBX_STRUCT_PACKED_START=" /D"MBX_STRUCT_PACKED_END=" /D"OBJ_STRUCT_PACKED_START=" /D"OBJ_STRUCT_PACKED_END="
set INC=/I. /Icomm\ssc\inc /Icomm\ssc\port /IRTE\_Debug_GD32E508VE /I_syntaxcheck_stubs
echo === mailbox.c ===
cl /nologo /Zs /W0 /TC /std:c11 /utf-8 /Zp1 %PACKED% %INC% comm\ssc\src\mailbox.c
if errorlevel 1 (echo MAILBOX_FAIL) else (echo MAILBOX_OK)
echo === dc_app.c ===
cl /nologo /Zs /W0 /TC /std:c11 /utf-8 /Zp1 %PACKED% %INC% comm\ssc\port\dc_app.c
if errorlevel 1 (echo DC_APP_FAIL) else (echo DC_APP_OK)
@@ -0,0 +1 @@
/* syntax-check stub only: SSC C files never touch device registers directly */
+27 -3
View File
@@ -329,12 +329,26 @@ void EscLan9252::enableEcatIrqOutput()
uint32_t EscLan9252::csrRead(uint16_t addr, uint8_t size)
{
/* DS 12.11.1:写 CSR_CMD(BUSY|R_nW|SIZE|ADDR) 启动,busy 自清后读 CSR_DATA;
* 有效数据总在 CSR_DATA 低位,硬件处理字节对齐。 */
* 有效数据总在 CSR_DATA 低位,硬件处理字节对齐。
* 1.5.59 加固:CSR_DATA 是锁存寄存器(非 FIFO),命令不变时连读两次必一致;
* 不一致即本拍 EXMC 毛刺——历史上这类错读曾让 GetSyncMan 把 0x1204/0x1208
* 缓存成邮箱几何(→SSC 邮箱楔死,根治见 mailbox.c 构建期常量),或让
* CheckSmSettings 以 0x0016/0x001F 瞬态打断状态转换。对错读整事务重跑一次;
* 两轮仍不一致则交出末拍(与旧行为一致的兜底,邮箱几何另有常量兜底)。 */
for (uint8_t attempt = 0U; attempt < 2U; ++attempt)
{
uint32_t n = POLL_MAX;
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
writeReg32(ECAT_CSR_CMD_REG, CSR_BUSY | CSR_RNW | ((uint32_t)size << 16) | addr);
n = POLL_MAX;
while ((readReg32(ECAT_CSR_CMD_REG) & CSR_BUSY) != 0U && --n != 0U) {}
const uint32_t d1 = readReg32(ECAT_CSR_DATA_REG);
const uint32_t d2 = readReg32(ECAT_CSR_DATA_REG);
if (d1 == d2)
{
return d1;
}
}
return readReg32(ECAT_CSR_DATA_REG);
}
@@ -353,7 +367,15 @@ void EscLan9252::pramRead(uint16_t addr, uint8_t *buf, uint16_t len)
{
/* DS 12.11.3:写 ADDR_LEN 再置 BUSY 启动;引擎按 DWORD 向下对齐填充读 FIFO,
* 首 DWORD 有效字节从 (addr&3) 起(表 12-7),末 DWORD 尾部无效(表 12-8)。
* 别名口 0x00-0x1C 功能相同,轮换寻址即官方突发用法;每读满一个 DWORD 弹一项。 */
* 别名口 0x00-0x1C 功能相同,轮换寻址即官方突发用法;每读满一个 DWORD 弹一项。
*
* 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;
}
@@ -381,7 +403,9 @@ void EscLan9252::pramRead(uint16_t addr, uint8_t *buf, uint16_t len)
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 推一项。 */
* 引擎只取有效字节;别名口 0x20-0x3C 轮换;写满一个 DWORD 推一项。
* 1.6.2 与 pramRead 同步回退逐 DWORD 轮询(推前必查 AVAIL 位):1.6.0 批量
* 模式实测零收益,且与短读损坏同源(见 pramRead 注),邮箱小 PDU 写同类风险。 */
if (len == 0U) {
return;
}
+91 -18
View File
@@ -114,29 +114,50 @@ 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;
}
/* 装不下就停,**且不推进 tail** —— 该帧留到下个周期,不丢。 */
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc) > kEcatPdoInBytes) {
/* 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;
}
uint8_t *p = &in[off];
p[ECAT_FRM_OFF_CH] = it.ch;
p[ECAT_FRM_OFF_DLC] = dlc;
p[ECAT_FRM_OFF_FLAGS] = 0x01U;
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, dlc);
memcpy(&p[ECAT_FRM_HDR_SIZE], it.data, dlcWire);
off = (uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc);
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;
@@ -145,6 +166,33 @@ void EthercatCanBridge::fillRxBatch(uint8_t *in)
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) {
@@ -153,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 的瞬间凭空发出一帧。 */
@@ -249,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;
@@ -271,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;
@@ -354,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) {
+79 -17
View File
@@ -37,7 +37,8 @@
*
* 新增两个批量区,每条帧带 8B 头(ch/dlc/flags/rsvd + id)+ 数据:
* 输出 48..383(336B):批量发送,一个 seq 沿一次性发 N 帧
* 输入 360..639(280B):批量接收,一周期回收多帧而不是只带最近一帧
* 输入 360..483(124B):批量接收,一周期回收多帧而不是只带最近一帧
* (1.6.1 数据条目 64→32B,段 5 诊断区 484..491,原 360..639/280B)
*
* 三条兼容性铁律(改动只做追加,不动既有偏移):
* 1. 输出 0..47 与 1.5.55 单帧布局完全一致(seq/ch/dlc/flags/id/win/data32);
@@ -103,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)。 */
@@ -128,7 +129,9 @@ enum : uint16_t {
ECAT_XRX_OFF_DATA = 328U, /**< 数据区,32 字节完整 payload */
};
/** 输入 PDO 段 4(360..639):CANFD 批量接收区(1.5.56 新增)。 */
/** 输入 PDO 段 4(360..483):CANFD 批量接收区(1.5.56 新增,1.6.1 数据条目 64→32B)。
* 每项 8B 头 + 最多 32B 数据(kEcatRxBatchDataMax);DLC 字节仍填**原始值**,
* 超长帧被截断时 flags bit1=1 —— 主站按 "8 + min(dlc,32)" 推进游标。 */
enum : uint16_t {
ECAT_RXB_OFF_COUNT = 360U, /**< 本周期回收的帧数 0..kEcatRxBatchMax */
ECAT_RXB_OFF_LOST = 361U, /**< 本周期因批量区放不下而丢弃的帧数 */
@@ -137,6 +140,40 @@ enum : uint16_t {
ECAT_RXB_OFF_ITEMS = 364U, /**< 帧数组起点,项布局同 ECAT_FRM_OFF_* */
};
/** 输入 PDO 段 5(484..491):设备诊断区(1.5.61 尾部追加,1.6.1 随批量区
* 数据条目 64→32B 前移:364+3×40=484,诊断区锚在紧随其后的 8 字节,
* 写侧是 EthercatPdo::fillDiagTail())。 */
enum : uint16_t {
ECAT_DIAG_OFF_LOAD = 484U, /**< 负载率,小端 uint16,单位 0.1%(1000=100%)。
中断 + 非延迟处理段(任务派发/OTA relay)占墙钟比,
不含常开轮询与空转;1s 滚动窗口(见 sys/cpu_load.h) */
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)。 */
constexpr uint8_t kEcatCanTxDataMax = 32U;
/** 段 1 短快照数据区字节数(保持 1.5.53 兼容,勿改)。 */
@@ -144,27 +181,38 @@ 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 */
constexpr uint16_t kEcatPdoInBytes = ECAT_PDO_IN_BYTES; /* 80×64bit = 640B */
constexpr uint16_t kEcatPdoInBytes = ECAT_PDO_IN_BYTES; /* 62×64bit = 496B */
/** CANFD 单帧数据上限(DLC 64)—— 批量区按此上限做容量推导。 */
/** CANFD 单帧数据上限(DLC 64)—— 环形队列/发送批量区按此上限做容量推导。 */
constexpr uint8_t kEcatCanFdDataMax = 64U;
/** 批量**接收**条目在 PDO 内的数据字节数上限(1.6.1 从 64 压到 32)。
* 真机采样 6086 帧 DLC 只有 24/32 两档;超长帧在装填时截断(flags bit1),
* 环形队列 RxRingItem 仍存全量 64B,截断只发生在 PDO 快照这一步。 */
constexpr uint8_t kEcatRxBatchDataMax = 32U;
/** 批量发送区可用字节数与最坏情况(全 DLC64)帧数上限。 */
constexpr uint16_t kEcatTxBatchBytes = (uint16_t)(kEcatPdoOutBytes - ECAT_TXB_OFF_ITEMS);
constexpr uint8_t kEcatTxBatchMax =
(uint8_t)(kEcatTxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax));
/** 批量接收区可用字节数与最坏情况帧数上限。 */
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 + kEcatCanFdDataMax));
(uint8_t)(kEcatRxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatRxBatchDataMax));
/** 批量接收环形队列槽数。取 kEcatRxBatchMax 的 4 倍,让主站慢一两拍也不丢帧。
* 注意这是 RAM 成本(槽数 × 72B),不是 PDO 成本。 */
@@ -182,7 +230,10 @@ static_assert(ECAT_RXB_OFF_ITEMS < kEcatPdoInBytes, "批量接收区起点越界
static_assert(ECAT_TXB_OFF_ITEMS < kEcatPdoOutBytes, "批量发送区起点越界");
/* 批量区至少要装得下一帧满长 CANFD,否则"批量"没有意义 */
static_assert(kEcatTxBatchMax >= 1U, "批量发送区装不下一帧 DLC64");
static_assert(kEcatRxBatchMax >= 1U, "批量接收区装不下一帧 DLC64");
static_assert(kEcatRxBatchMax >= 1U, "批量接收区装不下一帧 DLC32");
/* 段 5 诊断区必须在批量接收区的最坏占用之外(改批量区布局时一起核对) */
static_assert(ECAT_RXB_OFF_ITEMS + (uint16_t)kEcatRxBatchMax * (ECAT_FRM_HDR_SIZE + kEcatRxBatchDataMax)
<= ECAT_DIAG_OFF_LOAD, "批量接收区最坏占用与段5诊断区重叠");
/**
* @brief EtherCAT ↔ CAN 转发桥。
@@ -200,6 +251,17 @@ public:
* @param[in] out kEcatPdoOutBytes 字节输出过程数据。
* @note 由 APPL_Application() 每周期调用。
*/
/** @brief 输出探针(1.6.0 输出按需全量读)。
*
* PDI_Isr 每周期先读输出 PDO 头 12B(覆盖 SEQ@0 与 WIN@8),由本函数判定
* 是否需要全量读 384B:
* - 始终从探针刷新 s_winBase(主站每周期移动窗口扫描,与 seq 沿无关);
* - 首帧只记 seq 基准(与 processOutput 的 priming 一致),返回 false;
* - seq 变化沿或 FLAGS bit0(清统计请求)→ 返回 true,调用方全量读。
* s_lastSeq 仍由 processOutput 在全量路径上推进,本函数不写,保证沿语义
* 只处理一次。 */
static bool peekOutput(const uint8_t *head);
static void processOutput(const uint8_t *out);
/**
+100 -2
View File
@@ -4,14 +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)
{
@@ -22,16 +28,101 @@ void EthercatPdo::readOutputs(const uint8_t *esc)
++s_outputCycles;
}
bool EthercatPdo::outputProbe(const uint8_t *head)
{
return EthercatCanBridge::peekOutput(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);
EthercatCanBridge::buildInput(s_build);
fillDiagTail(s_build);
memcpy(s_in, s_build, kPdoInBytes);
s_building = false;
}
/**
* @brief 填充输入 PDO 段 5 设备诊断区(632..639,1.5.61)。
* @param[out] in 全长输入过程数据缓冲(buildInput 刚清零+填完段 1~4)。
* @note 负载率 1s 滚动一个窗口,seq 随窗口递增——主站只认"seq 在变"的负载值。
* 每周期重写 6 字节比"值变化才写"的分支判断更便宜,也不引入隐藏状态。
*/
void EthercatPdo::fillDiagTail(uint8_t *in)
{
const uint16_t load = CpuLoad::loadPermille();
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();
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)
@@ -53,6 +144,13 @@ void EthercatPdo::snapshotOutput(uint8_t *dst)
extern "C" {
/* 1.6.0:输出探针,SSC PDI_Isr 的 PDO_OutputMapping 每周期调用(C 链接)。
* 返回 0/1;SSC 侧声明为 UINT8,小返回值零扩展,调用约定兼容。 */
uint8_t EcatPdo_OutputProbe(const uint8_t *head)
{
return EthercatPdo::outputProbe(head) ? 1U : 0U;
}
void EcatPdo_ReadOutputs(const uint8_t *esc)
{
EthercatPdo::readOutputs(esc);
+28 -1
View File
@@ -48,6 +48,15 @@ public:
*/
static void readOutputs(const uint8_t *esc);
/**
* @brief 输出探针(1.6.0 输出按需全量读,配合 SSC PDO_OutputMapping)。
* @param[in] head 输出 PDO 头 12B(覆盖 SEQ@0 / WIN@8 / FLAGS@3)。
* @return true=需要全量读(seq 沿或清统计请求);false=本周期跳过全量读。
* @note 每周期由 PDI_Isr 在读 ESC 前调用:内部刷新桥的 s_winBase 并做 seq
* 基准 priming;s_lastSeq 的推进仍留在全量路径(processOutput)。
*/
static bool outputProbe(const uint8_t *head);
/**
* @brief 跑一个应用周期:消费输出、生成输入。
* @note 由 APPL_Application() 在主循环调用。
@@ -66,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; }
@@ -77,10 +90,24 @@ public:
static void snapshotOutput(uint8_t *dst);
private:
/**
* @brief 填充输入 PDO 段 5 设备诊断区(尾部 8 字节,1.5.61)。
* @param[out] in 全长输入过程数据缓冲。
* @note 布局常量 ECAT_DIAG_OFF_* 见 ethercat_can_bridge.h;当前携带
* CPU 负载率(0.1% 单位) + 窗口序号,取自 sys/cpu_load.h。
*/
static void fillDiagTail(uint8_t *in);
/** @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;
+18 -2
View File
@@ -50,8 +50,24 @@
/** 输出过程数据字节数(主站→从站,SM2):48×64bit。 */
#define ECAT_PDO_OUT_BYTES 384
/** 输入过程数据字节数(从站→主站,SM3):80×64bit。 */
#define ECAT_PDO_IN_BYTES 640
/**
* 输入过程数据字节数(从站→主站,SM3):62×64bit = 496B。
*
* ── 1.6.1 瘦身 640 → 496(2026-09-05 真机剖分后的决定)──
*
* 固件 DWT 探针(0x2000 sub9/sub10)实测:PDO_InputMapping 里 HW_EscWriteIsr
* 640B 写恒为 296µs/拍(29.7% CPU),且与 SYNC0 相位**无关**(主站 dc_bench
* 五形态对拍极差 0.04%)——PRAM 通道 ≈0.463µs/B 线性,写多少省多少,唯一的
* 减法就是减字节。真机 CAN 流量采样(6086 帧)DLC 只有 24/32 两档,批量接收
* 区每帧 64B 数据是纯富余:
* - 批量接收条目 8B 头 + 64B 数据 → 8B 头 + 32B 数据(DLC 真值保留,
* 超长帧截断、flags bit1 标记;环形队列仍存全量 64B,不丢原始数据);
* - 段 5 诊断区从 632..639(批量区最坏占用后的尾部追加)挪到 484..491;
* - 段 1 / 电机窗口 / 长帧快照全部不动(0..359 与 1.5.61 逐字节兼容)。
* 496 = 62×8;SM3 实占 3×496=1488B(0x1880-0x1E4F),out+in=880 ≤ 1024 硬上限,
* DPRAM 反而留出 432B 余量。改这里必须同步重生成 SII(gen_eeprom.py)并断电冷启动。
*/
#define ECAT_PDO_IN_BYTES 496
/** DPRAM 中过程数据区起址与 SM 缓冲数 —— 上限推导的依据,供静态断言使用。 */
#define ECAT_PD_AREA_START 0x1400
+23 -6
View File
@@ -9,6 +9,7 @@
#include "comm/ethercat_sync.h"
#include "sys/config.h"
#include "main.h" /* g_app_uptime_sec */
#include "sys/cpu_load.h"
#include <cstdio>
extern "C" {
@@ -129,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);
@@ -168,13 +172,23 @@ extern "C" {
#if (CONFIG_ETHERCAT_SYNC_MODE == 0)
__attribute__((section(".ramfunc"), noinline))
void EXTI0_IRQHandler(void) { EthercatSync::onSync0Exti(); }
void EXTI1_IRQHandler(void) { EthercatSync::onEscIrqExti(); }
void EXTI2_IRQHandler(void) { EthercatSync::onSync1Exti(); }
void EXTI0_IRQHandler(void)
{
CpuLoad::isrEnter();
EthercatSync::onSync0Exti();
CpuLoad::isrExit();
}
void EXTI1_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onEscIrqExti(); CpuLoad::isrExit(); }
void EXTI2_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onSync1Exti(); CpuLoad::isrExit(); }
#else
void TIMER0_UP_TIMER9_IRQHandler(void) { EthercatSync::onTimerIrq(); }
void EXTI1_IRQHandler(void) { EthercatSync::onEscIrqExti(); }
void EXTI2_IRQHandler(void) { EthercatSync::onSync1Exti(); }
void TIMER0_UP_TIMER9_IRQHandler(void)
{
CpuLoad::isrEnter();
EthercatSync::onTimerIrq();
CpuLoad::isrExit();
}
void EXTI1_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onEscIrqExti(); CpuLoad::isrExit(); }
void EXTI2_IRQHandler(void) { CpuLoad::isrEnter(); EthercatSync::onSync1Exti(); CpuLoad::isrExit(); }
#endif
/* ---- SSC 诊断对象 0x2000 的 C 侧访问 (dc_app.c APPL_InputMapping 每周期拉取) ---- */
@@ -183,4 +197,7 @@ uint32_t EcatDbg_GetSync1(void) { return EthercatSync::getSync1Count(); }
uint32_t EcatDbg_GetEscIrq(void) { return EthercatSync::getEscIrqCount(); }
uint32_t EcatDbg_GetUptimeSec(void){ return g_app_uptime_sec; }
/* 1.6.0 剖分埋点:供 ecatappl.c (C) 读取 DWT 周期计数 */
uint32_t EcatDbg_Dwt(void) { return DWT->CYCCNT; }
} /* extern "C" */
+21 -127
View File
@@ -25,7 +25,7 @@
<!-- SM2: Process Data Output (RxPDO, Master -> Slave) -->
<Sm DefaultSize="384" StartAddress="#x1400" ControlByte="#x64" Enable="1">Outputs</Sm>
<!-- SM3: Process Data Input (TxPDO, Slave -> Master) -->
<Sm DefaultSize="640" StartAddress="#x1880" ControlByte="#x20" Enable="1">Inputs</Sm>
<Sm DefaultSize="496" StartAddress="#x1880" ControlByte="#x20" Enable="1">Inputs</Sm>
<!-- RxPDO (Output, Master -> Slave): 索引 0x1600-0x17FF,映射 0x7000:01..48
1.5.56 扩到 48 条目(384B):兼容单帧区 48B + CANFD 批量发送区 336B。
与 dc_app.c 的 PDO_OUT_ENTRIES / gen_eeprom.py 的 PD_OUT_SIZE 必须一致,
@@ -374,6 +374,8 @@
1.5.56 扩到 80 条目(640B):电机状态窗口 + CANFD 批量接收区 280B。
SM3 起址随之挪到 #x1880 —— SM2 是 3-buffer 模式,384B×3 占到 #x187F。
(384+640)×3 = 3072 = 过程数据区 0x1400-0x1FFF 全长,恰好用满。
1.6.1 输入瘦身 640→496: 批量接收条目数据 64→32B(截断见 bridge.h),
条目 80→62, SM3 实占 3×496=1488B (0x1880-0x1E4F), DPRAM 留 432B。
撞址会被 SSC ecatslv.c 重叠检查拦下,报 INVALIDSMOUTCFG。 -->
<TxPdo Fixed="1" Mandatory="1" Sm="3">
<Index>#x1A00</Index>
@@ -812,132 +814,24 @@
<Name>Status Data 62</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>63</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 63</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>64</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 64</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>65</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 65</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>66</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 66</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>67</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 67</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>68</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 68</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>69</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 69</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>70</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 70</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>71</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 71</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>72</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 72</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>73</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 73</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>74</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 74</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>75</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 75</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>76</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 76</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>77</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 77</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>78</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 78</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>79</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 79</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>80</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 80</Name>
<DataType>ULINT</DataType>
</Entry>
</TxPdo>
<Mailbox DataLinkLayer="true">
<CoE SdoInfo="true" CompleteAccess="true" SegmentedSdo="true"/>
+118 -33
View File
@@ -40,13 +40,15 @@
* PDO 数据缓冲
*
* 1.5.56 扩到 DPRAM 硬上限:输出 48×64bit=384B,输入 80×64bit=640B(非对称)。
* 1.6.1 输入瘦身 640→496(62×64bit):批量接收条目数据 64→32B + 诊断区前移,
* 依据与推导见 comm/ethercat_pdo_size.h 的 1.6.1 注。
* 长度的唯一真源是 comm/ethercat_pdo_size.h;本文件用 _Static_assert 与之绑定,
* 改一处漏一处会编译失败。为何硬上限是 out+in≤1024B 见该头文件的推导。
*---------------------------------------------------------------------------*/
/** 输出/输入 PDO 的 64bit 条目数。 */
#define PDO_OUT_ENTRIES 48u /* 48×8 = 384 B */
#define PDO_IN_ENTRIES 80u /* 80×8 = 640 B */
#define PDO_IN_ENTRIES 62u /* 62×8 = 496 B (1.6.1 瘦身,原 80×8=640) */
/** 输出数据对象 0x7000 (主站→从站, RxPDO) */
typedef struct OBJ_STRUCT_PACKED_START {
@@ -71,7 +73,7 @@ static TOBJ6000 s_inputObj = {PDO_IN_ENTRIES, {0}};
_Static_assert(offsetof(TOBJ7000, u64Data) == 2, "TOBJ7000 未紧排:检查 OBJ_STRUCT_PACKED_START");
_Static_assert(offsetof(TOBJ6000, u64Data) == 2, "TOBJ6000 未紧排:检查 OBJ_STRUCT_PACKED_START");
_Static_assert(sizeof(TOBJ7000) == 2 + 8 * PDO_OUT_ENTRIES, "TOBJ7000 尺寸应为 2+384");
_Static_assert(sizeof(TOBJ6000) == 2 + 8 * PDO_IN_ENTRIES, "TOBJ6000 尺寸应为 2+640");
_Static_assert(sizeof(TOBJ6000) == 2 + 8 * PDO_IN_ENTRIES, "TOBJ6000 尺寸应为 2+496");
/* 与 C++ 侧桥接层的长度约定绑定:任一处改了尺寸而忘了另一处,这里直接编译失败,
* 而不是等到运行期 SAFEOP 报 AL 0x001A/0x0011 再回头查。 */
@@ -94,10 +96,19 @@ typedef struct OBJ_STRUCT_PACKED_START {
UINT32 u32Sync1; /* sub2 */
UINT32 u32EscIrq; /* sub3 */
UINT32 u32UptimeS; /* sub4 */
/* 1.6.0 剖分埋点(sub5..12,DWT 周期/计数累加,主站 1s 差分;定位完即移除) */
UINT32 u32PdiCyc; /* sub5: PDI_Isr 全函数累计 */
UINT32 u32OutCyc; /* sub6: PDO_OutputMapping(输出探针/全量读) */
UINT32 u32FreeRunCyc; /* sub7: MainLoop free-run 块(SM 同步下恒 0) */
UINT32 u32PdiCnt; /* sub8: PDI_Isr 触发次数 */
UINT32 u32InCyc; /* sub9: PDO_InputMapping 全程(APPL+输入PDO写) */
UINT32 u32InWriteCyc; /* sub10: 其中 HW_EscWriteIsr 输入PDO 过程数据写 */
UINT32 u32AlCyc; /* sub11: PDI_Isr 内 AL 事件读×2+超限应答 */
UINT32 u32AppCyc; /* sub12: ECAT_Application(桥 processOutput) */
} OBJ_STRUCT_PACKED_END
TOBJ2000;
static TOBJ2000 s_syncDiag = {4, 0, 0, 0, 0};
static TOBJ2000 s_syncDiag = {12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
/* 同上:UINT32 首条目必须落在字节 2(未紧排时为 4)。 */
_Static_assert(offsetof(TOBJ2000, u32Sync0) == 2, "TOBJ2000 未紧排:检查 OBJ_STRUCT_PACKED_START");
@@ -107,6 +118,16 @@ extern UINT32 EcatDbg_GetSync0(void);
extern UINT32 EcatDbg_GetSync1(void);
extern UINT32 EcatDbg_GetEscIrq(void);
extern UINT32 EcatDbg_GetUptimeSec(void);
/* 1.6.0 剖分埋点:ecatappl.c 累加器 + ethercat_sync.cpp 的 DWT 读 */
extern UINT32 EcatDbg_Dwt(void);
extern volatile UINT32 g_dbg_pdiCyc;
extern volatile UINT32 g_dbg_outCyc;
extern volatile UINT32 g_dbg_freerunCyc;
extern volatile UINT32 g_dbg_pdiCnt;
extern volatile UINT32 g_dbg_inCyc;
extern volatile UINT32 g_dbg_inWriteCyc;
extern volatile UINT32 g_dbg_alCyc;
extern volatile UINT32 g_dbg_appCyc;
/* 过程数据收发模块 (comm/ethercat_pdo.cpp 的 extern "C" shim, 1.5.54)。
* 1.5.53 时这里直接调 EcatCan_* 转发桥;1.5.54 把"一个 EtherCAT 周期怎么收发"
@@ -161,12 +182,13 @@ static TOBJ_RXPDO_MAPPING sRxPDOMap = {PDO_OUT_ENTRIES, {
PDO_MAP_8(0x7000, 24), PDO_MAP_8(0x7000, 32), PDO_MAP_8(0x7000, 40)
}};
/* 0x1A00: 映射 0x6000:01..80, 每个 64 bits */
/* 0x1A00: 映射 0x6000:01..62, 每个 64 bits(1.6.1 瘦身 80→62:7×8 + 6) */
static TOBJ_TXPDO_MAPPING sTxPDOMap = {PDO_IN_ENTRIES, {
PDO_MAP_8(0x6000, 0), PDO_MAP_8(0x6000, 8), PDO_MAP_8(0x6000, 16),
PDO_MAP_8(0x6000, 24), PDO_MAP_8(0x6000, 32), PDO_MAP_8(0x6000, 40),
PDO_MAP_8(0x6000, 48), PDO_MAP_8(0x6000, 56), PDO_MAP_8(0x6000, 64),
PDO_MAP_8(0x6000, 72)
PDO_MAP_8(0x6000, 48),
PDO_MAP_ENTRY(0x6000, 57), PDO_MAP_ENTRY(0x6000, 58), PDO_MAP_ENTRY(0x6000, 59),
PDO_MAP_ENTRY(0x6000, 60), PDO_MAP_ENTRY(0x6000, 61), PDO_MAP_ENTRY(0x6000, 62)
}};
_Static_assert(sizeof(sRxPDOMap.aEntries) / 4 == PDO_OUT_ENTRIES, "0x1600 条目数不符");
@@ -215,12 +237,21 @@ OBJCONST TSDOINFOENTRYDESC OBJMEM asInputEntryDesc[] = {
{DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING}
};
/* SYNC 诊断 0x2000: REC, Sub0..Sub4 共 5 项, 纯只读不参与 PDO 映射。 */
/* SYNC 诊断 0x2000: REC, Sub0..Sub12 共 13 项, 纯只读不参与 PDO 映射。
* sub5..12 为 1.6.0 剖分埋点(DWT 周期/计数累加), 定位负载构成后移除。 */
OBJCONST TSDOINFOENTRYDESC OBJMEM asSyncDiagEntryDesc[] = {
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ},
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}
};
@@ -265,8 +296,8 @@ TOBJECT OBJMEM ApplicationObjDicList[] = {
{NULL, NULL, 0x6000, {DEFTYPE_UNSIGNED64, PDO_IN_ENTRIES | (OBJCODE_ARR << 8)},
asInputEntryDesc, aName0x6000, &s_inputObj, NULL, NULL, 0x0000},
/* 0x2000: SYNC 诊断 (只读, 主站 SDO 轮询验证 DC 通路) */
{NULL, NULL, 0x2000, {DEFTYPE_RECORD, 4 | (OBJCODE_REC << 8)},
/* 0x2000: SYNC 诊断 (只读, 主站 SDO 轮询验证 DC 通路; sub5..8 为 1.6.0 剖分埋点) */
{NULL, NULL, 0x2000, {DEFTYPE_RECORD, 12 | (OBJCODE_REC << 8)},
asSyncDiagEntryDesc, aName0x2000, &s_syncDiag, NULL, NULL, 0x0000},
/* 终止条目 */
@@ -280,7 +311,7 @@ TOBJECT OBJMEM * VARMEM ApplicationObjDic = ApplicationObjDicList;
*---------------------------------------------------------------------------*/
UINT16 APPL_GenerateMapping(UINT16 *pInputSize, UINT16 *pOutputSize)
{
*pInputSize = ECAT_PDO_IN_BYTES; /* 80×UINT64 = 640 bytes */
*pInputSize = ECAT_PDO_IN_BYTES; /* 62×UINT64 = 496 bytes (1.6.1) */
*pOutputSize = ECAT_PDO_OUT_BYTES; /* 48×UINT64 = 384 bytes */
return 0;
}
@@ -340,6 +371,15 @@ void APPL_InputMapping(UINT16 *pInputData)
s_syncDiag.u32Sync1 = EcatDbg_GetSync1();
s_syncDiag.u32EscIrq = EcatDbg_GetEscIrq();
s_syncDiag.u32UptimeS = EcatDbg_GetUptimeSec();
/* 1.6.0 剖分埋点快照(sub5..12) */
s_syncDiag.u32PdiCyc = g_dbg_pdiCyc;
s_syncDiag.u32OutCyc = g_dbg_outCyc;
s_syncDiag.u32FreeRunCyc = g_dbg_freerunCyc;
s_syncDiag.u32PdiCnt = g_dbg_pdiCnt;
s_syncDiag.u32InCyc = g_dbg_inCyc;
s_syncDiag.u32InWriteCyc = g_dbg_inWriteCyc;
s_syncDiag.u32AlCyc = g_dbg_alCyc;
s_syncDiag.u32AppCyc = g_dbg_appCyc;
/* 1.5.54:先让 EthercatPdo 把本周期的输入数据写进 0x6000 的对象存储,
* 再交给 ESC。经由对象存储中转(而非直接写 pInputData)是为了让主站
@@ -374,18 +414,23 @@ void APPL_Application(void)
* AL 诊断快照 — task_runner 5s 心跳用 (C 链接, C++ 侧 extern "C" 声明)
* [19:16]=AL 状态(0x0130 低 4 位) [31:24]=AL 错误码(0x0130 高 8 位)
* [15:0] =AL 事件寄存器(0x0220, bit0=主站 AL 控制请求待处理)
*
* 1.5.59 字节序修正:ESC 寄存器经 PDI/CSR 读出即主机序(LAN9252 CSR 引擎按
* 16-bit 主机总线给出寄存器"值";线上大端只存在于网络侧与 PRAM 原始帧数据),
* 与 SSC 自身 GetSyncMan/mailbox 的裸读约定一致。1.5.48~1.5.58 这里误加了
* SWAPWORD,心跳打印的 al/evt 全是字节交换后的错值——那段时期的串口日志
* (如 evt=0x0400)需按字节交换重新解读后才能当证据用。
*---------------------------------------------------------------------------*/
UINT32 EcatDbg_ReadAlDiag(void)
{
UINT16 alStatus = 0U;
HW_EscReadWord(alStatus, ESC_AL_STATUS_OFFSET);
alStatus = SWAPWORD(alStatus);
return ((UINT32)alStatus << 16) | (UINT32)SWAPWORD(HW_GetALEventRegister());
return ((UINT32)alStatus << 16) | (UINT32)HW_GetALEventRegister();
}
/*---------------------------------------------------------------------------
* AL Event Mask (0x204) 守护 + SM2 事件楔死自愈 — 1.5.48 新增, 1.5.49 扩展
* (C 链接, task_runner 1s 节流调用)
* AL Event Mask (0x204) 守护 + SM2 事件楔死自愈 — 1.5.48 新增, 1.5.49/1.5.60 扩展
* (C 链接, task_runner 250ms 节流调用)
*
* 背景:实测"停止回 INIT 后重进 OP"概率性失败——主站 LRW WKC=3/3(输出
* 确实写进 SM2),但从站 9s ESM 超时报 0x001B:bEcatFirstOutputsReceived 一直
@@ -396,6 +441,13 @@ UINT32 EcatDbg_ReadAlDiag(void)
*
* 守护(1)0x204 缺位自愈:从站 AL>=SAFEOP 且有输出 PDO 时,SSC 不变式是
* "0x204 ⊇ u16ALEventMask"。读实际值比对,缺位则补写并打 WARN。
* 1.5.60 加固:期望值 u16ALEventMask 本身也可能缺 SM2 位——下行转换路径
* (ecatslv.c AL_ControlInd 尾部)同时清 0x204 的 PDO 事件位、bEscIntEnabled、
* bEcatFirstOutputsReceived,恢复全靠 SAFEOP 重入的 StartInputHandler +
* SetALEventMask(ecatslv.c:1506);重入没走全时 0x204 丢位。旧判定只比
* "actual ⊇ 期望值",而期望值变量在下行路径并不清零——若错的是期望值本身,
* 比对恒通过、缺位永远补不回。SM-Synchron+有输出 PDO 的不变式是
* u16ALEventMask ⊇ PROCESS_OUTPUT_EVENT(ecatslv.c:959/1086),先补期望再比对。
*
* 自愈(2)SM2 事件楔死(1.5.49):SAFEOP 下 0x220 bit10 挂起 且 标志未置位
* = PDI_Isr 通路楔死。从主循环直接完成 PDI_Isr 该做的事:置位
@@ -403,6 +455,15 @@ UINT32 EcatDbg_ReadAlDiag(void)
* 协议等价:bit10 挂起 = 主站已写过 SM2 = "收到输出"成立;AL_ControlRes 下一个
* MainLoop 周期即完成 SAFEOP→OP。WARN 附带全部内部旗标(bEscInt/bDcSync/
* 期望与实际 mask/看门狗状态),下次复现一行日志即可定位楔死路径。
*
* 自愈(3)OP 态 PDI_Isr 门控(1.5.60):AL==OP 且 bEscIntEnabled==FALSE 不可
* 合法出现——门控只在下行转换尾部被清(ecatslv.c:1273),而下行必改 AL 状态;
* 守护与 ESM 同在主循环上下文串行,看到"OP 且门关着"即重入路径漏了恢复段。
* 此时 PDI_Isr 整体被门控(ecatappl.c:282),PDO_OutputMapping 停调、输入冻结
* ——主站侧表现即楔死看门狗盯的窗口回显冻结(2026-09-05 实测 250µs 档每
* 20~35s 触发一次重拉,wr 两小时涨 38)。修法:重开门 + 应答挂起的 SM2 事件
* (掩码补位由守护(1)在同次调用完成),下一帧 LRW 的 SM2 中断即恢复整条
* 过程数据链路,主站看门狗(2s 阈值)从此摸不到触发条件。
*---------------------------------------------------------------------------*/
UINT32 EcatDbg_GuardAleventMask(void)
{
@@ -420,30 +481,29 @@ UINT32 EcatDbg_GuardAleventMask(void)
return 0xFFFFU; /* 状态不适用:返回哨兵值,调用方不打印 */
}
/* (0) 邮箱 SM 地址/长度失配自愈(2026-09-03 03:00 根治,1.5.52):
/* (0) 邮箱 SM 地址/长度失配兜底(1.5.52 引入,1.5.59 修复字节序并降级为兜底):
*
* 主站侧探针(楔死态)实锤的证据链:
* 历史楔死(1.5.51 及更早,断电才清)的实锤证据链:
* - ESC/SSC 邮箱处理全程活着(注入合法 CoE 请求后 200ms 内 SM1 出响应);
* - 但响应帧头出现在 SM1 start+8 而非 start(且逐会话累积:+4→+8,断电才清零);
* - 网络侧(FPRD)读 SM0/SM1 寄存器 start 恒正确(0x1000/0x1200);
* => SSC 的 u16EscAddrSendMbx(Init→PreOP 时经 GetSyncMan/CSR 间接读)拿到了错值,
* 主站按协议从 0x1200 读 → 帧头永远错位 → SOEM 报 WkcError/MailboxError,
* 且预清理重枚举(重跑 StartMailboxHandler)重读仍错 → 死锁,只有断电恢复。
* 自愈:AL>=PREOP 时用寄存器真值(0x0800/0x0808)校验 SSC 缓存的 start/len,
* 失配即写回 + WARN(旧值/新值一并打印,下次串口复现可确凿定位 CSR 读错还是
* 变量污染——两种成因此写回均有效,写回后下一个邮箱事务即恢复)。
* 变量仅在 Stop/StartMailboxHandler(INIT<->PREOP 迁移,主循环上下文)被写,
* 守护同样在主循环——无并发风险。 */
* => SSC 缓存的 u16EscAddrSendMbx(Init→PreOP 时经 GetSyncMan/CSR 间接读)拿到了错值,
* 主站按协议从 0x1200 读 → 帧头永远错位 → SOEM 报 WkcError/MailboxError。
* 1.5.59 根治:mailbox.c 的 MBX_StartMailboxHandler 改用构建期常量直达
* (ecat_def.h:MBX_SM0/1_START/SIZE),CSR 错值从此没有进入缓存的路径——
* 楔死无建立点,本守护降级为"运行期内存踩踏"的最后兜底:AL>=PREOP 时用
* 寄存器真值校验 SSC 缓存的 start/len,失配即写回 + WARN。
*
* ★ 1.5.59 字节序修复:ESC 寄存器经 PDI/CSR 读出即主机序(与 SSC 自身
* GetSyncMan/mailbox 路径一致)。1.5.52~1.5.58 这里误加了 SWAPWORD——
* sm0Len 被换成 0x0001 < 8,下面的失配判定恒短路,守护自上线起从未真正
* 执行过("自愈"名存实亡,楔死实际靠重枚举重读碰运气恢复)。 */
if (al >= 2U && bMbxRunning) {
UINT16 sm0Start = 0U, sm0Len = 0U, sm1Start = 0U, sm1Len = 0U;
HW_EscReadWord(sm0Start, ESC_SYNCMAN_REG_OFFSET);
HW_EscReadWord(sm0Len, ESC_SYNCMAN_REG_OFFSET + 2U);
HW_EscReadWord(sm1Start, ESC_SYNCMAN_REG_OFFSET + SIZEOF_SM_REGISTER);
HW_EscReadWord(sm1Len, ESC_SYNCMAN_REG_OFFSET + SIZEOF_SM_REGISTER + 2U);
sm0Start = SWAPWORD(sm0Start);
sm0Len = SWAPWORD(sm0Len);
sm1Start = SWAPWORD(sm1Start);
sm1Len = SWAPWORD(sm1Len);
if (sm0Len >= 8U && sm1Len >= 8U
&& (u16EscAddrReceiveMbx != sm0Start || u16EscAddrSendMbx != sm1Start
|| u16ReceiveMbxSize != sm0Len || u16SendMbxSize != sm1Len)) {
@@ -464,16 +524,22 @@ UINT32 EcatDbg_GuardAleventMask(void)
return 0xFFFFU; /* 以下仅 SAFEOP+ 适用 */
}
evt = SWAPWORD(HW_GetALEventRegister());
/* 1.5.59:PDI 寄存器读即主机序,不再 SWAPWORD——1.5.48~1.5.58 交换出的
* evt/actual 是错值,0x204 缺位判定恒真(每 250ms 盲写一次 mask,值凑巧
* 正确故未暴露),SM2 事件判定则对在错位上。详见上方字节序修复说明。 */
evt = HW_GetALEventRegister();
HW_EscReadWord(actual, ESC_AL_EVENTMASK_OFFSET);
actual = SWAPWORD(actual);
/* (1) 0x204 缺位自愈 */
/* (1) 0x204 缺位自愈(1.5.60 加固:先补期望值,再比对补实际值) */
if ((u16ALEventMask & PROCESS_OUTPUT_EVENT) == 0U) {
u16ALEventMask |= PROCESS_OUTPUT_EVENT;
printf("[ECAT][AEM] WARN: expected AL mask lacked SM2 bit -> 0x%04X\r\n",
(unsigned)u16ALEventMask);
}
if ((UINT16)(actual & u16ALEventMask) != u16ALEventMask) {
const UINT16 before = actual;
HW_SetALEventMask(u16ALEventMask);
HW_EscReadWord(actual, ESC_AL_EVENTMASK_OFFSET);
actual = SWAPWORD(actual);
printf("[ECAT][AEM] WARN: AL Event Mask lost bits, actual=0x%04X expect=0x%04X -> restored 0x%04X\r\n",
(unsigned)before, (unsigned)u16ALEventMask, (unsigned)actual);
}
@@ -482,7 +548,6 @@ UINT32 EcatDbg_GuardAleventMask(void)
* (OP 态下 LRW 每周期置位/清除 bit10 属正常,不能误报) */
if (al == 4U /* SAFEOP */ && (evt & 0x0400U) != 0U && bEcatFirstOutputsReceived == FALSE) {
HW_EscReadWord(wdState, ESC_PD_WD_STATE);
wdState = SWAPWORD(wdState);
bEcatFirstOutputsReceived = TRUE;
HW_EscReadWord(dummy, nEscAddrOutputData);
HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U));
@@ -494,5 +559,25 @@ UINT32 EcatDbg_GuardAleventMask(void)
(unsigned)(bDcSyncActive ? 1U : 0U),
(unsigned)u16ALEventMask, (unsigned)actual, (unsigned)wdState);
}
/* (3) OP 态 PDI_Isr 门控自愈(1.5.60 新增):OP 下 bEscIntEnabled==FALSE
* 不可合法出现——门控只在下行转换尾部被清(ecatslv.c:1273),而下行必改
* AL 状态;守护与 ESM 同在主循环上下文串行,看到"OP 且门关着"即 SAFEOP
* 重入路径漏了恢复段。此时 PDI_Isr 整体被门控(ecatappl.c:282),
* PDO_OutputMapping 停调、输入冻结,只能靠主站看门狗重拉总线。
* 修法:重开门 + 应答挂起的 SM2 事件(ISR 非 OP 分支同款读首/末字);
* 0x204 掩码补位已由上面(1)在同次调用完成,下一帧 LRW 即恢复。 */
if (al == 8U /* OP */ && nPdOutputSize > 0U && bEscIntEnabled == FALSE) {
HW_EscReadWord(wdState, ESC_PD_WD_STATE);
bEscIntEnabled = TRUE;
HW_EscReadWord(dummy, nEscAddrOutputData);
HW_EscReadWord(dummy, (UINT16)(nEscAddrOutputData + nPdOutputSize - 2U));
printf("[ECAT][AEM] WARN: OP with PDI_Isr gated, evt=0x%04X "
"(bDcSync=%u mask_exp=0x%04X mask_act=0x%04X wd=0x%04X) "
"-> gate reopened + event acked\r\n",
(unsigned)evt,
(unsigned)(bDcSyncActive ? 1U : 0U),
(unsigned)u16ALEventMask, (unsigned)actual, (unsigned)wdState);
}
return actual;
}
+23
View File
@@ -708,6 +708,29 @@
#define MAX_MBX_READ_ADDRESS 0x1FFF
#endif
/* 1.5.59 根治邮箱楔死:本板邮箱几何构建期常量。
* 取值 = gen_eeprom.py 的 MBX_RX_OFFSET/SIZE(0x1000/256) 与 MBX_TX_OFFSET/SIZE
* (0x1200/256),即 DCU_ESI.xml <Sm> 与 ESC 上电自 SII 加载的寄存器真值;主站
* config_init 也只是照抄同一份 SII。MBX_StartMailboxHandler 以此直达缓存,
* 不再信任单次 CSR 间接读(错值曾以 0x1204/0x1208 进入缓存 → 所有 SDO 响应
* 写错址 → 断电才解的楔死),CSR 读仅作交叉校验(mailbox.c 打 WARN)。
* ★ 改 SII 邮箱几何必须同步改这里(gen_eeprom.py 顶部注释同样有约束)。★ */
#ifndef MBX_SM0_START
#define MBX_SM0_START 0x1000
#endif
#ifndef MBX_SM0_SIZE
#define MBX_SM0_SIZE 0x0100
#endif
#ifndef MBX_SM1_START
#define MBX_SM1_START 0x1200
#endif
#ifndef MBX_SM1_SIZE
#define MBX_SM1_SIZE 0x0100
#endif
/*-----------------------------------------------------------------------------------------
------
------ PDO Timing (DC sync parameters, unit: ns)
+4 -1
View File
@@ -79,7 +79,10 @@ MBX_TX_SIZE = 256
PD_OUT_OFFSET = 0x1400 # SM2: RxPDO (Master -> Slave outputs)
PD_OUT_SIZE = 384 # 48×64bit
PD_IN_OFFSET = 0x1880 # SM3: TxPDO (Slave -> Master inputs)
PD_IN_SIZE = 640 # 80×64bit
PD_IN_SIZE = 496 # 62×64bit (1.6.1 瘦身,原 80×64bit=640)
# 1.6.1 瘦身 640→496:批量接收条目数据 64→32B + 段5诊断区前移到 484。
# SM3 实占 3×496=1488B (0x1880-0x1E4F),DPRAM 留 432B 余量;
# 依据见 comm/ethercat_pdo_size.h 的 1.6.1 注。改后必须重烧 EEPROM 并断电冷启动。
# 3-buffer 占用与 DPRAM 上限自检:改尺寸/偏移时立刻在生成阶段拦住,
# 而不是等烧进 EEPROM、冷启动后再看 AL 错误码。
+63
View File
@@ -157,6 +157,24 @@ BOOL bCycleTimeMeasurementStarted; // indicates if the bus cycle measurement is
UINT16 aPdOutputData[(MAX_PD_OUTPUT_SIZE>>1)];
UINT16 aPdInputData[(MAX_PD_INPUT_SIZE>>1)];
/*ECAT_DBG_START(1.6.0 临时剖分埋点)*/
/* 1.6.0 负载归因埋点:DWT 周期累加器,经 0x2000 sub5..8 上报,主站 1s 差分出速率。
* 只为定位 free-run 模式下每周期 508µs 计入时间的真实去向;定位完成后整块移除。
* EcatDbg_Dwt() 定义在 comm/ethercat_sync.cpp (extern "C")。 */
extern UINT32 EcatDbg_Dwt(void);
#define ecatdbg_dwt() EcatDbg_Dwt()
/* 1.6.0:输出探针(实现见 comm/ethercat_pdo.cpp,C 链接) */
extern UINT8 EcatPdo_OutputProbe(UINT8 const *head);
volatile UINT32 g_dbg_pdiCyc = 0; /* sub5: PDI_Isr 全函数累计周期 */
volatile UINT32 g_dbg_outCyc = 0; /* sub6: PDO_OutputMapping(输出探针/全量读) */
volatile UINT32 g_dbg_freerunCyc = 0; /* sub7: MainLoop free-run 分支(SM 同步下恒 0) */
volatile UINT32 g_dbg_pdiCnt = 0; /* sub8: PDI_Isr 触发次数(=每周期 1 次) */
volatile UINT32 g_dbg_inCyc = 0; /* sub9: PDO_InputMapping 全程(APPL+640B 写) */
volatile UINT32 g_dbg_inWriteCyc = 0; /* sub10: 其中 HW_EscWriteIsr 640B 过程数据写 */
volatile UINT32 g_dbg_alCyc = 0; /* sub11: PDI_Isr 内 AL 事件读×2+超限应答 */
volatile UINT32 g_dbg_appCyc = 0; /* sub12: ECAT_Application(桥 processOutput) */
/*ECAT_DBG_END*/
/*variables are declared in ecatslv.c*/
extern VARVOLATILE UINT16 u16dummy;
BOOL bInitFinished = FALSE; /** < \brief indicates if the initialization is finished*/
@@ -178,22 +196,49 @@ BOOL bInitFinished = FALSE; /** < \brief indicates if the initialization is fini
*////////////////////////////////////////////////////////////////////////////////////////
void PDO_InputMapping(void)
{
/*ECAT_DBG(1.6.0 剖分:输入映射全程 + 640B 写细分)*/
const UINT32 dbgT0 = ecatdbg_dwt();
APPL_InputMapping((UINT16*)aPdInputData);
const UINT32 dbgT1 = ecatdbg_dwt();
HW_EscWriteIsr(((MEM_ADDR *) aPdInputData), nEscAddrInputData, nPdInputSize );
g_dbg_inWriteCyc += ecatdbg_dwt() - dbgT1;
g_dbg_inCyc += ecatdbg_dwt() - dbgT0;
}
/////////////////////////////////////////////////////////////////////////////////////////
/**
\brief This function will copies the outputs from the ESC memory to the local memory
to the hardware. This function is only called in case of an SM2
(output process data) event.
1.6.0 输出按需全量读(负载优化):每周期先读输出 PDO 头 12B 探针(覆盖 SEQ@0
与 WIN@8),经 EcatPdo_OutputProbe 判定——seq 无变化沿且无清统计请求时,
跳过 384B 全量读(只补读 SM2 末字完成事件应答),省掉每周期 ~190µs 的
PRAM 搬运(含 ESC 核仲裁等待)。主站滚动窗口扫槽依赖的 WIN 字节在探针内
每周期刷新,窗口扫描不受影响。SM2 应答语义不变:首字(探针内)+末字
(两分支各自覆盖)。
*////////////////////////////////////////////////////////////////////////////////////////
void PDO_OutputMapping(void)
{
/*ECAT_DBG(1.6.0 剖分:输出映射全程)*/
const UINT32 dbgT0 = ecatdbg_dwt();
/* 1.6.0:输出按需全量读——探针 12B 判定是否真的有新输出 */
UINT8 head[12];
HW_EscReadIsr(((MEM_ADDR *) head), nEscAddrOutputData, 12U );
if ( EcatPdo_OutputProbe(head) )
{
HW_EscReadIsr(((MEM_ADDR *)aPdOutputData), nEscAddrOutputData, nPdOutputSize );
APPL_OutputMapping((UINT16*) aPdOutputData);
}
else
{
/* 无新输出:补读 SM2 末字完成事件应答(首字已随探针读过) */
HW_EscReadWordIsr(u16dummy,(nEscAddrOutputData+nPdOutputSize-2));
}
g_dbg_outCyc += ecatdbg_dwt() - dbgT0;
}
/////////////////////////////////////////////////////////////////////////////////////////
/**
@@ -279,10 +324,14 @@ void HandleBusCycleCalculation(void)
void PDI_Isr(void)
{
/*ECAT_DBG(1.6.0 剖分:ISR 全程)*/
const UINT32 dbgT0 = ecatdbg_dwt();
if(bEscIntEnabled)
{
/* get the AL event register */
const UINT32 dbgTa = ecatdbg_dwt();
UINT16 ALEvent = HW_GetALEventRegister_Isr();
g_dbg_alCyc += ecatdbg_dwt() - dbgTa;
ALEvent = SWAPWORD(ALEvent);
if ( ALEvent & PROCESS_OUTPUT_EVENT )
@@ -335,7 +384,9 @@ void PDI_Isr(void)
if (sSyncManOutPar.u16SyncType == SYNCTYPE_SM_SYNCHRON)
{
/* The Application is synchronized to process data Sync Manager event*/
const UINT32 dbgTb = ecatdbg_dwt();
ECAT_Application();
g_dbg_appCyc += ecatdbg_dwt() - dbgTb;
}
if ( bEcatInputUpdateRunning
@@ -345,14 +396,18 @@ void PDI_Isr(void)
)
{
/* EtherCAT slave is at least in SAFE-OPERATIONAL, update inputs */
const UINT32 dbgTc = ecatdbg_dwt();
PDO_InputMapping();
g_dbg_inCyc += ecatdbg_dwt() - dbgTc;
}
/*
Check if cycle exceed
*/
/*if next SM event was triggered during runtime increment cycle exceed counter*/
const UINT32 dbgTd = ecatdbg_dwt();
ALEvent = HW_GetALEventRegister_Isr();
g_dbg_alCyc += ecatdbg_dwt() - dbgTd;
ALEvent = SWAPWORD(ALEvent);
if ( ALEvent & PROCESS_OUTPUT_EVENT )
@@ -365,6 +420,9 @@ void PDI_Isr(void)
HW_EscReadWordIsr(u16dummy,(nEscAddrOutputData+nPdOutputSize-2));
}
} //if(bEscIntEnabled)
/*ECAT_DBG(1.6.0 剖分:ISR 全程 收尾)*/
g_dbg_pdiCyc += ecatdbg_dwt() - dbgT0;
g_dbg_pdiCnt++;
}
void Sync0_Isr(void)
@@ -577,6 +635,9 @@ void MainLoop(void)
}
DISABLE_ESC_INT();
/*ECAT_DBG(1.6.0 剖分:free-run 应用+输入映射块)*/
{
const UINT32 dbgT1 = ecatdbg_dwt();
ECAT_Application();
if ( bEcatInputUpdateRunning )
@@ -584,6 +645,8 @@ void MainLoop(void)
/* EtherCAT slave is at least in SAFE-OPERATIONAL, update inputs */
PDO_InputMapping();
}
g_dbg_freerunCyc += ecatdbg_dwt() - dbgT1;
}
ENABLE_ESC_INT();
}
#if !ECAT_TIMER_INT
+30 -5
View File
@@ -133,6 +133,8 @@ V4.07 ECAT 1: The sources for SPI and MCI were merged (in ecat_def.h<br>
#include "ecatcoe.h"
#include <stdio.h> /* printf: SM 几何交叉校验 WARN(1.5.59,见 MBX_StartMailboxHandler) */
/*--------------------------------------------------------------------------------------
------
------ internal Types and Defines
@@ -282,24 +284,47 @@ void MBX_Init(void)
UINT16 MBX_StartMailboxHandler(void)
{
UINT16 result = 0;
/* get address of the receive mailbox sync manager (SM0) */
/* 1.5.59 根治"邮箱楔死"(响应帧头落在 SM1 start+4/+8、逐会话漂移,断电才清):
* 根因链 = INIT→PREOP 时经 GetSyncMan/CSR 单次间接读(EXMC HBI)把 SM0/SM1
* 几何缓存进 u16EscAddr* 与 u16*MbxSize 等变量;总线毛刺给出的"合法"错值
* (0x1204/0x1208 均为偶地址且在检查范围内)即被缓存,此后所有 SDO 响应写错址,
* 主站按协议从 0x1200 读永远错位,重枚举重读仍错 → 只有断电能解。本板邮箱
* 几何是 SII 固定值(gen_eeprom.py 的 MBX_RX/TX_OFFSET/SIZE,ESC 上电自行
* 加载,主站 config_init 也只是照抄),改为构建期常量(ecat_def.h:
* MBX_SM0/1_START/SIZE)直达缓存,CSR 读降级为交叉校验:失配打 WARN(说明
* ESC 寄存器被非常规改写,需查)但缓存仍取常量——CSR 错值从此没有进入
* 缓存的路径,楔死无建立点;运行期内存踩踏也会在每次 INIT→PREOP 被常量覆写。
* 注意:ESC 寄存器经 PDI/CSR 读出即主机序(与 GetSyncMan 既有路径一致,勿加
* SWAPWORD;PRAM 帧数据才是线上大端,那边交换是对的)。 */
/*ECATCHANGE_START(V5.11) HW1*/
TSYNCMAN ESCMEM * pSyncMan = (TSYNCMAN ESCMEM *)GetSyncMan(MAILBOX_WRITE);
/*ECATCHANGE_END(V5.11) HW1*/
if (pSyncMan->PhysicalStartAddress != MBX_SM0_START || pSyncMan->Length != MBX_SM0_SIZE)
{
printf("[ECAT][MBX] WARN: SM0 reg 0x%04X/%u != build-time 0x%04X/%u -> keep build-time\r\n",
(unsigned)pSyncMan->PhysicalStartAddress, (unsigned)pSyncMan->Length,
(unsigned)MBX_SM0_START, (unsigned)MBX_SM0_SIZE);
}
/* store size of the receive mailbox */
u16ReceiveMbxSize = pSyncMan->Length;
u16ReceiveMbxSize = MBX_SM0_SIZE;
/* store the address of the receive mailbox */
u16EscAddrReceiveMbx = pSyncMan->PhysicalStartAddress;
u16EscAddrReceiveMbx = MBX_SM0_START;
/* get address of the send mailbox sync manager (SM1) */
/*ECATCHANGE_START(V5.11) HW1*/
pSyncMan =(TSYNCMAN ESCMEM *) GetSyncMan(MAILBOX_READ);
/*ECATCHANGE_END(V5.11) HW1*/
if (pSyncMan->PhysicalStartAddress != MBX_SM1_START || pSyncMan->Length != MBX_SM1_SIZE)
{
printf("[ECAT][MBX] WARN: SM1 reg 0x%04X/%u != build-time 0x%04X/%u -> keep build-time\r\n",
(unsigned)pSyncMan->PhysicalStartAddress, (unsigned)pSyncMan->Length,
(unsigned)MBX_SM1_START, (unsigned)MBX_SM1_SIZE);
}
/* store the size of the send mailbox */
u16SendMbxSize = pSyncMan->Length;
u16SendMbxSize = MBX_SM1_SIZE;
/* store the address of the send mailbox */
u16EscAddrSendMbx = pSyncMan->PhysicalStartAddress;
u16EscAddrSendMbx = MBX_SM1_START;
// HBu 02.05.06: it should be checked if there are overlaps in the sync manager areas
if ((u16EscAddrReceiveMbx + u16ReceiveMbxSize) > u16EscAddrSendMbx && (u16EscAddrReceiveMbx < (u16EscAddrSendMbx + u16SendMbxSize)))
+3 -2
View File
@@ -887,14 +887,15 @@ UINT8 OBJ_Read( UINT16 index, UINT8 subindex, UINT32 objSize, OBJCONST TOBJECT O
p = (CHAR **) pVarPtr;
pVarPtr = (UINT16 MBXMEM *)p[subindex-1];
if((((UINT16)pVarPtr) & 0x1) == 0x1)
if((((UINT32)pVarPtr) & 0x1) == 0x1)
{
/*enum is stored at an odd address*/
UINT16 cnt = 0;
//get last even WORD address
/*ECATCHANGE_START(V5.11) SDO3*/
pVarPtr = (UINT16 MBXMEM *)(((MEM_ADDR)pVarPtr)& ~(MEM_ADDR)0x1);
/*MEM_ADDR=UINT16 会把 32 位指针截成 16 位,必须用完整位宽清 bit0*/
pVarPtr = (UINT16 MBXMEM *)(((UINT32)pVarPtr) & ~(UINT32)0x1);
/*ECATCHANGE_END(V5.11) SDO3*/
for(cnt = 0; cnt < (size / 2);cnt++)
+4 -3
View File
@@ -1025,7 +1025,8 @@ UINT8 SDOS_SdoInfoInd( TSDOINFORMATION MBXMEM *pSdoInfoInd )
UINT16 n = OBJ_GetNoOfObjects(i);
/* copy the number of objects of the list type in the SDO Information response */
((UINT16 MBXMEM *) &pSdoInfoInd->CoeHeader)[(SIZEOF_SDOINFOLISTSTRUCT>>1)+i] = SWAPWORD(n);
/*(UINT32) 中转:对 packed 成员直接取址强转 UINT16 指针会报 -Waddress-of-packed-member(邮箱缓冲实际偶对齐)*/
((UINT16 MBXMEM *)(UINT32)&pSdoInfoInd->CoeHeader)[(SIZEOF_SDOINFOLISTSTRUCT>>1)+i] = SWAPWORD(n);
}
/* size of the mailbox service response */
@@ -1045,7 +1046,7 @@ UINT8 SDOS_SdoInfoInd( TSDOINFORMATION MBXMEM *pSdoInfoInd )
size = u16SendMbxSize - SIZEOF_SDOINFO - MBX_HEADER_SIZE;
}
/* initialize pData with the pointer where the fragment has to be copied */
pData = &((UINT16 MBXMEM *) &pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFO>>1];
pData = &((UINT16 MBXMEM *)(UINT32)&pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFO>>1];
/* initialize index with the next index to be sent */
index = nSdoInfoIndex;
/* decrement the number of fragments to be sent */
@@ -1063,7 +1064,7 @@ UINT8 SDOS_SdoInfoInd( TSDOINFORMATION MBXMEM *pSdoInfoInd )
size = u16SendMbxSize - SIZEOF_SDOINFOLISTSTRUCT - MBX_HEADER_SIZE;
}
/* initialize pData with the pointer where the fragment has to be copied */
pData = &((UINT16 MBXMEM *) &pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFOLISTSTRUCT>>1];
pData = &((UINT16 MBXMEM *)(UINT32)&pSdoInfoInd->CoeHeader)[SIZEOF_SDOINFOLISTSTRUCT>>1];
/*Check if List need to be send in fragments*/
if( (n<<1) > size)
{
+252 -15
View File
@@ -7,6 +7,7 @@
#include "device/canfd.h"
#include "sys/config.h"
#include "sys/critical.h"
#include "sys/cpu_load.h"
#include "sys/systick.h"
#include "sys/task_manager.h"
#include <cstring>
@@ -18,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;
@@ -165,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() 读清;单写单读,无锁。 */
@@ -273,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;
@@ -421,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;
@@ -986,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
@@ -1181,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]) {
@@ -1291,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;
@@ -1299,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 {
@@ -1319,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;
@@ -1335,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) {
@@ -1386,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];
@@ -1593,10 +1824,12 @@ bool CanFd::copyFeedbackSnapshot(uint8_t channelIndex, ChannelFeedback *out)
* @retval 无返回值。
* @note C 向量包装函数,内部转调 `CanFd::handleRxInterrupt(0U)`。
*/
extern "C" void CAN0_RX0_IRQHandler(void) { CanFd::handleRxInterrupt(0U); }
extern "C" void CAN0_RX0_IRQHandler(void)
{ CpuLoad::isrEnter(); CanFd::handleRxInterrupt(0U); CpuLoad::isrExit(); }
/** @brief CAN0 TX 完成中断向量入口。 */
extern "C" void CAN0_TX_IRQHandler(void) { CanFd::handleTxInterrupt(0U); }
extern "C" void CAN0_TX_IRQHandler(void)
{ CpuLoad::isrEnter(); CanFd::handleTxInterrupt(0U); CpuLoad::isrExit(); }
/**
* @brief CAN1 RX0 中断向量入口。
@@ -1605,10 +1838,12 @@ extern "C" void CAN0_TX_IRQHandler(void) { CanFd::handleTxInterrupt(0U); }
* @retval 无返回值。
* @note C 向量包装函数,内部转调 `CanFd::handleRxInterrupt(1U)`。
*/
extern "C" void CAN1_RX0_IRQHandler(void) { CanFd::handleRxInterrupt(1U); }
extern "C" void CAN1_RX0_IRQHandler(void)
{ CpuLoad::isrEnter(); CanFd::handleRxInterrupt(1U); CpuLoad::isrExit(); }
/** @brief CAN1 TX 完成中断向量入口。 */
extern "C" void CAN1_TX_IRQHandler(void) { CanFd::handleTxInterrupt(1U); }
extern "C" void CAN1_TX_IRQHandler(void)
{ CpuLoad::isrEnter(); CanFd::handleTxInterrupt(1U); CpuLoad::isrExit(); }
/**
* @brief CAN2 RX0 中断向量入口。
@@ -1617,7 +1852,9 @@ extern "C" void CAN1_TX_IRQHandler(void) { CanFd::handleTxInterrupt(1U); }
* @retval 无返回值。
* @note C 向量包装函数,内部转调 `CanFd::handleRxInterrupt(2U)`。
*/
extern "C" void CAN2_RX0_IRQHandler(void) { CanFd::handleRxInterrupt(2U); }
extern "C" void CAN2_RX0_IRQHandler(void)
{ CpuLoad::isrEnter(); CanFd::handleRxInterrupt(2U); CpuLoad::isrExit(); }
/** @brief CAN2 TX 完成中断向量入口。 */
extern "C" void CAN2_TX_IRQHandler(void) { CanFd::handleTxInterrupt(2U); }
extern "C" void CAN2_TX_IRQHandler(void)
{ CpuLoad::isrEnter(); CanFd::handleTxInterrupt(2U); CpuLoad::isrExit(); }
+7 -1
View File
@@ -7,6 +7,7 @@
#include "device/timer.h"
#include "sys/task_manager.h"
#include "sys/systick.h"
#include "sys/cpu_load.h"
#include "sys/config.h"
/**
@@ -105,4 +106,9 @@ void SchedulerTimer::onInterrupt()
* @retval 无返回值。
* @note C 向量包装函数,内部转调 `SchedulerTimer::onInterrupt()`。
*/
extern "C" void TIMER2_IRQHandler(void) { SchedulerTimer::onInterrupt(); }
extern "C" void TIMER2_IRQHandler(void)
{
CpuLoad::isrEnter();
SchedulerTimer::onInterrupt();
CpuLoad::isrExit();
}
+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;
};
/// 全局电机状态表
@@ -37,6 +37,7 @@ extern "C" {
#include "sys/config.h"
#include "sys/critical.h"
#include "sys/systick.h"
#include "sys/cpu_load.h"
#include <cstdio>
#include <cstring>
@@ -878,10 +879,14 @@ void AppSpiCommand::onCsExtiInterrupt()
extern "C" void SPI0_IRQHandler(void)
{
CpuLoad::isrEnter();
AppSpiCommandBus.onInterrupt();
CpuLoad::isrExit();
}
extern "C" void EXTI10_15_IRQHandler(void)
{
CpuLoad::isrEnter();
AppSpiCommandBus.onCsExtiInterrupt();
CpuLoad::isrExit();
}
+7 -1
View File
@@ -7,6 +7,7 @@
#include "device/usart0.h"
#include "sys/config.h"
#include "sys/critical.h"
#include "sys/cpu_load.h"
uint8_t Usart0::s_txBuf[Usart0::kTxBufSize];
volatile uint16_t Usart0::s_txHead = 0U;
@@ -182,4 +183,9 @@ extern "C" int fputc(int ch, FILE *f) { return Usart0::putChar(ch, f); }
* @retval 无返回值。
* @note 该 C 包装函数内部转调 `Usart0::onInterrupt()`。
*/
extern "C" void USART0_IRQHandler(void) { Usart0::onInterrupt(); }
extern "C" void USART0_IRQHandler(void)
{
CpuLoad::isrEnter();
Usart0::onInterrupt();
CpuLoad::isrExit();
}
+8
View File
@@ -5,6 +5,7 @@
#include "main.h"
#include "sys/initial.h"
#include "sys/cpu_load.h"
#include "sys/task_manager.h"
#include "sys/task_runner.h"
#include "sys/config.h"
@@ -45,8 +46,15 @@ int App::run()
APP_LOGI("[APP] >>> TaskRunner ready, entering main loop\r\n");
printBanner();
CpuLoad::init();
while(1) {
/* 负载率测量(1.5.63 定义收窄):runPending 内部按"非延迟工作"分段挂钩
* (ISR 全量 + 任务派发 + 电机 OTA relay,见 sys/cpu_load.h);
* 常开轮询(SSC MainLoop / 各 poll)与 NOP 空转不计入。
* tick() 每圈无条件做 1s 窗口滚动检查——滚动不依赖本圈是否有挂钩段。 */
TaskRunner::runPending();
CpuLoad::tick();
#if (CONFIG_MAIN_LOOP_WFI_ENABLE != 0)
uint32_t save = RT_CRITICAL_SAVE();
if(TaskManager::state().task_flags == 0U) {
File diff suppressed because one or more lines are too long
+117
View File
@@ -0,0 +1,117 @@
/**
* @file cpu_load.cpp
* @brief CPU 负载率测量实现(1.5.61,度量定义与并发协议见 cpu_load.h)。
*
* 热路径(beginWork/endWork/isrEnter/isrExit/tick)放进 .ramfunc——它们会被包进
* EXTI0(Sync0, preempt=0) 等延迟敏感的 ISR 包装,与 EthercatSync::onSync0Exti
* 同款 section 属性,避免 flash 等待态给 Sync0 入口添抖动。1s 一次的窗口
* 结算(含 64 位除法)留在普通 flash 函数里。
*/
#include "sys/cpu_load.h"
#include "RTE_Components.h"
#include CMSIS_device_header
volatile uint32_t CpuLoad::s_depth = 0U;
volatile uint32_t CpuLoad::s_stamp = 0U;
volatile uint32_t CpuLoad::s_busy = 0U;
volatile uint32_t CpuLoad::s_winStart = 0U;
volatile uint16_t CpuLoad::s_loadPerm = 0U;
volatile uint32_t CpuLoad::s_seq = 0U;
/** @brief 区间并集的共享进入点。0→1 者负责盖时间戳;STREX 失败方重试时深度
* 已被抢占者修正,不会再重复盖戳。 */
__attribute__((always_inline)) static inline void cpuLoadEnter()
{
uint32_t d;
do {
d = __LDREXW(&CpuLoad::s_depth);
if (d == 0U) {
CpuLoad::s_stamp = DWT->CYCCNT;
__DMB();
}
} while (__STREXW(d + 1U, &CpuLoad::s_depth) != 0U);
__DMB();
}
/** @brief 区间并集的共享退出点。1→0 者把区间累进 s_busy(同样走独占访问,
* 防"ISR 恰好插在主循环退出的读-改-写之间"丢累加)。 */
__attribute__((always_inline)) static inline void cpuLoadLeave()
{
const uint32_t end = DWT->CYCCNT;
const uint32_t start = CpuLoad::s_stamp;
uint32_t d;
do {
d = __LDREXW(&CpuLoad::s_depth);
} while (__STREXW(d - 1U, &CpuLoad::s_depth) != 0U);
if (d == 1U) {
const uint32_t delta = end - start;
uint32_t b;
do {
b = __LDREXW(&CpuLoad::s_busy);
} while (__STREXW(b + delta, &CpuLoad::s_busy) != 0U);
}
}
void CpuLoad::init()
{
/* DWT 已由 EscLan9252::init 使能;这里幂等兜底(万一 EtherCAT 初始化被裁剪)。 */
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
s_depth = 0U;
s_busy = 0U;
s_winStart = DWT->CYCCNT;
s_loadPerm = 0U;
s_seq = 0U;
}
__attribute__((section(".ramfunc"), noinline)) void CpuLoad::beginWork()
{
cpuLoadEnter();
}
__attribute__((section(".ramfunc"), noinline)) void CpuLoad::isrEnter()
{
cpuLoadEnter();
}
__attribute__((section(".ramfunc"), noinline)) void CpuLoad::isrExit()
{
cpuLoadLeave();
}
/**
* @brief 窗口结算(普通 flash 函数,1s 才进一次)。
* @note s_busy 用独占访问换出:窗口边界上 ISR 的在途累加要么算进本窗口、
* 要么算进下窗口,不丢。
*/
static void cpuLoadRollWindow(uint32_t now)
{
const uint32_t elapsed = now - CpuLoad::s_winStart; /* 调用方已保证 >= 1s */
uint32_t busy;
do {
busy = __LDREXW(&CpuLoad::s_busy);
} while (__STREXW(0U, &CpuLoad::s_busy) != 0U);
CpuLoad::s_winStart = now;
uint32_t perm = (uint32_t)(((uint64_t)busy * 1000ULL) / (uint64_t)elapsed);
if (perm > 1000U) {
perm = 1000U;
}
CpuLoad::s_loadPerm = (uint16_t)perm;
CpuLoad::s_seq++;
}
__attribute__((section(".ramfunc"), noinline)) void CpuLoad::endWork()
{
cpuLoadLeave();
}
__attribute__((section(".ramfunc"), noinline)) void CpuLoad::tick()
{
const uint32_t now = DWT->CYCCNT;
if ((uint32_t)(now - s_winStart) >= SystemCoreClock) {
cpuLoadRollWindow(now);
}
}
+97
View File
@@ -0,0 +1,97 @@
/**
* @file cpu_load.h
* @brief CPU 负载率测量(1.5.63 定义收窄,机制沿用 1.5.61)。
*
* 度量定义:**中断 + 非延迟处理段(任务派发 / OTA 中继等请求驱动工作)占墙钟
* 时间的比值**,1s 滚动窗口,分辨率 0.1%(permille)。
*
* 为什么不计入主循环常开轮询:本架构在 EtherCAT 实时模式(CONFIG_MAIN_LOOP_WFI_ENABLE=0)
* 下主循环空闲时是 NOP 忙等,且 runPending() 每圈无条件跑 SSC MainLoop 轮询与各
* poll() 检查——把这部分计入"busy"的话负载率恒 ≈99%,失去参考价值(1.5.61 实机
* 实测 99.4%,其中绝大部分是自由运转的轮询而非在处理事件)。收窄后的定义回答
* 的问题变成"CPU 有多少时间在响应事件",随真实负载(CAN 流量、OTA 中继、
* DC 模式 Sync0 过程数据搬运)增长;常开轮询成本与空转时间不计入。
*
* 计入项(全走同一深度计数器求**区间并集**,嵌套不重不漏):
* - 全部挂钩 ISR:TIMER2(100µs 调度)、SysTick、EXTI0/1/2(EtherCAT Sync0/PDI/
* Sync1)、TIMER0(DC 定时档)、CAN0-2 RX+TX、SPI0+EXTI10_15、USART0。
* DC 模式下 Sync0_Isr 的 EXMC 过程数据搬运由此计入——这是大头,不能漏。
* - 主循环非延迟段:TaskManager 派发的周期任务、0xB0 进 BOOT 处理、
* 电机 OTA relay 推进(isActive() 门控,空闲时零挂钩开销)。
* 不计入项:SSC MainLoop 轮询、AppSpiCommandBus.poll()、看门狗喂狗等常开检查
* (free-run 调试模式下 PDO 应用跑在主循环轮询路径,不计入;生产走 DC 模式,
* 等价工作在 Sync0 ISR 内已计入)。
*
* 并发协议(无关中断、无临界区):enter/exit 用 LDREX/STREX 原子改深度,
* 任一优先级可抢占任意上下文而不破坏计数——STREX 失败方重试时会看到已修正的
* 深度并补上时间戳。已知残余误差:exit 在读时间戳与递减深度之间被高优先级
* 中断插入时,该次中断时长漏计(几 cycle 窗口,对 0.1% 分辨率可忽略)。
*
* @note DWT 已由 EscLan9252::init 使能;init() 幂等兜底,在主循环启动前调用。
*/
#ifndef SYS_CPU_LOAD_H
#define SYS_CPU_LOAD_H
#include <stdint.h>
/**
* @brief CPU 负载率测量。
* @note 全静态类,与 SysTickTimer 同风格。
*/
class CpuLoad {
public:
/**
* @brief 使能 DWT CYCCNT 并复位统计状态。
* @note 幂等;须在主循环启动前调用一次。
*/
static void init();
/** @brief 主循环圈起点:进入"busy"区间。与 endWork() 成对,每圈各一次。 */
static void beginWork();
/**
* @brief 主循环圈终点:退出"busy"区间。
* @note 与 beginWork() 成对;窗口滚动不在这里(begin/end 可能整圈不执行),
* 见 tick()。
*/
static void endWork();
/**
* @brief 主循环每圈无条件调用:检查 1s 窗口是否滚动。
* @note 只做一次 CYCCNT 读 + 比较(命中才进 flash 侧结算函数),滚动不再
* 依赖"本圈恰好有挂钩段执行",空闲系统 seq 也会按秒推进。
*/
static void tick();
/** @brief 中断入口挂钩:与 isrExit() 成对,包住 ISR 主体。 */
static void isrEnter();
/** @brief 中断出口挂钩。 */
static void isrExit();
/**
* @brief 最近一个完整窗口的负载率。
* @return 0..1000,单位 0.1%(1000=满载);窗口未滚动过时为 0。
*/
static uint16_t loadPermille() { return s_loadPerm; }
/**
* @brief 已完成的统计窗口数(自 init 起,单调递增回绕)。
* @note 随负载率一起进 EtherCAT 输入 PDO 段 5,主站据此判断数据是否仍在刷新。
*/
static uint32_t windowSeq() { return s_seq; }
public:
/* 静态成员开成 public:cpu_load.cpp 里 .ramfunc 的文件级内联热路径
* (cpuLoadEnter/cpuLoadLeave/cpuLoadRollWindow) 直接触碰它们,
* 经成员函数绕一道反而多一层调用。勿在模块外写这些状态。 */
static volatile uint32_t s_depth; /**< busy 区间嵌套深度 */
static volatile uint32_t s_stamp; /**< 深度 0→1 时刻的 CYCCNT 快照 */
static volatile uint32_t s_busy; /**< 当前窗口内累计 busy 周期数 */
static volatile uint32_t s_winStart; /**< 当前窗口起点 CYCCNT */
static volatile uint16_t s_loadPerm; /**< 最近一个完整窗口的负载率(0.1%) */
static volatile uint32_t s_seq; /**< 完成滚动的窗口计数 */
};
#endif /* SYS_CPU_LOAD_H */
+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 桥接吞吐) */
}
}
+7 -1
View File
@@ -5,6 +5,7 @@
#include "main.h"
#include "sys/systick.h"
#include "sys/cpu_load.h"
volatile uint32_t SysTickTimer::s_ms = 0U;
@@ -93,4 +94,9 @@ bool SysTickTimer::initMs()
* @retval 无返回值。
* @note C 向量包装函数,内部转调 `SysTickTimer::onInterrupt()`。
*/
extern "C" void SysTick_Handler(void) { SysTickTimer::onInterrupt(); }
extern "C" void SysTick_Handler(void)
{
CpuLoad::isrEnter();
SysTickTimer::onInterrupt();
CpuLoad::isrExit();
}
+19
View File
@@ -14,6 +14,7 @@
#include "sys/ota_boot_request.h"
#include "sys/motor_ota_relay.h"
#include "sys/task_manager.h"
#include "sys/cpu_load.h"
#include "sys/task_runner.h"
#include "comm/ethercat_slave.h"
#include "comm/ethercat_sync.h"
@@ -295,6 +296,9 @@ extern "C" uint32_t EcatDbg_GuardAleventMask(void);
* 1.5.52:周期 1s→250ms——新加的邮箱地址自愈(1.5.52 根治 SSC 侧
* u16EscAddrSendMbx 错位楔死,详见 dc_app.c)必须快过主站 SDO 超时
* (~700ms),否则自愈永远落在主站放弃之后,形同虚设。
* 1.5.59:邮箱楔死已根治(mailbox.c 几何常量化,CSR 错值无进入缓存路径),
* 本守护降级为运行期内存踩踏兜底;且修复了 1.5.52~1.5.58 的 SWAPWORD
* 字节序 bug——那段时期本守护的失配判定恒短路,实际从未生效过。
*/
static void ethercatAleMaskGuard()
{
@@ -562,15 +566,26 @@ void TaskRunner::runPending()
/* 0xB0 进 BOOT:必须每圈检查,不能只靠 1Hz TASK_HEARTBEAT。
* 否则 Flash 写/复位被拖到下一秒,ESP32 HELLO 一直 role=app 超时。 */
if (OtaBootRequest::isRebootPending()) {
CpuLoad::beginWork();
OtaBootRequest::process();
CpuLoad::endWork();
}
/* 电机 OTA relay:活跃时每圈推进状态机(等 ACK / 超时 / 发下一包)。
* 非活跃时立即返回,开销可忽略。 */
/* 负载率挂钩(1.5.63):relay 活跃时才计 busy——isActive() 是廉价枚举判断,
* 空闲路径零挂钩开销;计数段覆盖等 ACK/超时/发包的真实工作。 */
const bool motorOtaActive = MotorOtaRelay::isActive();
if (motorOtaActive) {
CpuLoad::beginWork();
}
MotorOtaRelay::process();
/* 数据发送推进:主循环每圈调用,按 SysTick 钟表控制每包间隔
* (替代原靠 tick100us 计数 + TASK_MOTOR_OTA_TX 的方式,消除计数器竞争)。 */
MotorOtaRelay::onTxTick();
if (motorOtaActive) {
CpuLoad::endWork();
}
/* 2026-08 请求驱动改造:0xC1 CAN 反馈 / 0xC5 电机状态 / 0xC4 ADC 已改为
* ESP32 周期查询(主动按铃推送全部移除,避免 OTA 期间悬空按铃刷屏)。
@@ -590,6 +605,9 @@ void TaskRunner::runPending()
const uint32_t pending = TaskManager::takePending();
if(pending == 0U) return;
/* 负载率挂钩(1.5.63):派发的周期任务属"非延迟工作",整段计入 busy;
* 前面的常开轮询(Watchdog/poll/MainLoop/guard)不计入,见 sys/cpu_load.h。 */
CpuLoad::beginWork();
if(pending & (1UL << TASK_ADC_VREF_SAMPLE)) taskAdcVrefSample();
if(pending & (1UL << TASK_ADC_TEMP_SAMPLE)) taskAdcTempSample();
if(pending & (1UL << TASK_ADC_PROCESS)) taskAdcProcess();
@@ -597,4 +615,5 @@ void TaskRunner::runPending()
if(pending & (1UL << TASK_CAN_TX_SEND)) taskCanTxSend();
if(pending & (1UL << TASK_HEARTBEAT)) taskHeartbeat();
if(pending & (1UL << TASK_DIAGNOSTIC_PRINT)) taskDiagnosticPrint();
CpuLoad::endWork();
}