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协议栈增加新的诊断数据区
This commit is contained in:
cbb
2026-09-05 20:07:22 +08:00
parent 2ebd4ff1d2
commit 70daa4b0c6
19 changed files with 398 additions and 27 deletions
@@ -137,6 +137,18 @@ enum : uint16_t {
ECAT_RXB_OFF_ITEMS = 364U, /**< 帧数组起点,项布局同 ECAT_FRM_OFF_* */
};
/** 输入 PDO 段 5(632..639):设备诊断区(1.5.61 新增,尾部追加)。
* 批量接收区最坏情况(全 DLC64 满帧)只用到 364+3×72=580,580..639 永久空闲,
* 诊断区锚在 632 起的 8 字节,写侧是 EthercatPdo::process()。 */
enum : uint16_t {
ECAT_DIAG_OFF_LOAD = 632U, /**< 负载率,小端 uint16,单位 0.1%(1000=100%)。
中断 + 非延迟处理段(任务派发/OTA relay)占墙钟比,
不含常开轮询与空转;1s 滚动窗口(见 sys/cpu_load.h) */
ECAT_DIAG_OFF_RSVD = 634U, /**< 保留 2B */
ECAT_DIAG_OFF_SEQ = 636U, /**< 诊断区序号,小端 uint32,每完成一个统计窗口 +1,
主站据此判断负载值是否仍在滚动刷新 */
};
/** 单帧数据在输出 PDO / 长帧快照区中的最大字节数(电机 CMD 是 DLC 32)。 */
constexpr uint8_t kEcatCanTxDataMax = 32U;
/** 段 1 短快照数据区字节数(保持 1.5.53 兼容,勿改)。 */
@@ -183,6 +195,9 @@ static_assert(ECAT_TXB_OFF_ITEMS < kEcatPdoOutBytes, "批量发送区起点越
/* 批量区至少要装得下一帧满长 CANFD,否则"批量"没有意义 */
static_assert(kEcatTxBatchMax >= 1U, "批量发送区装不下一帧 DLC64");
static_assert(kEcatRxBatchMax >= 1U, "批量接收区装不下一帧 DLC64");
/* 段 5 诊断区必须在批量接收区的最坏占用之外(改批量区布局时一起核对) */
static_assert(ECAT_RXB_OFF_ITEMS + (uint16_t)kEcatRxBatchMax * (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax)
<= ECAT_DIAG_OFF_LOAD, "批量接收区最坏占用与段5诊断区重叠");
/**
* @brief EtherCAT ↔ CAN 转发桥。
+21
View File
@@ -4,6 +4,7 @@
*/
#include "comm/ethercat_pdo.h"
#include "comm/ethercat_can_bridge.h"
#include "sys/cpu_load.h"
#include <cstring>
@@ -32,6 +33,26 @@ void EthercatPdo::process()
* processOutput 还负责取走主站请求的状态窗口下标,也必须在 buildInput 之前。 */
EthercatCanBridge::processOutput(s_out);
EthercatCanBridge::buildInput(s_in);
fillDiagTail(s_in);
}
/**
* @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);
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);
}
void EthercatPdo::writeInputs(uint8_t *esc)
+8
View File
@@ -77,6 +77,14 @@ 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 待交回主站的输入数据。 */
+17 -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" {
@@ -168,13 +169,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 每周期拉取) ---- */
+44 -3
View File
@@ -389,8 +389,8 @@ UINT32 EcatDbg_ReadAlDiag(void)
}
/*---------------------------------------------------------------------------
* 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 一直
@@ -401,6 +401,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 该做的事:置位
@@ -408,6 +415,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)
{
@@ -474,7 +490,12 @@ UINT32 EcatDbg_GuardAleventMask(void)
evt = HW_GetALEventRegister();
HW_EscReadWord(actual, ESC_AL_EVENTMASK_OFFSET);
/* (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);
@@ -498,5 +519,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;
}
+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)
{