Compare commits
5
Commits
f9ef1b8da2
...
0cd3d9bed2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cd3d9bed2 | ||
|
|
ace5a1671b | ||
|
|
9f1c2d25dd | ||
|
|
70daa4b0c6 | ||
|
|
2ebd4ff1d2 |
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
@@ -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 低位,硬件处理字节对齐。 */
|
||||
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) {}
|
||||
* 有效数据总在 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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" */
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
Binary file not shown.
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 错误码。
|
||||
|
||||
@@ -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,21 +196,48 @@ 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
|
||||
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();
|
||||
|
||||
HW_EscReadIsr(((MEM_ADDR *)aPdOutputData), nEscAddrOutputData, nPdOutputSize );
|
||||
/* 1.6.0:输出按需全量读——探针 12B 判定是否真的有新输出 */
|
||||
UINT8 head[12];
|
||||
HW_EscReadIsr(((MEM_ADDR *) head), nEscAddrOutputData, 12U );
|
||||
|
||||
APPL_OutputMapping((UINT16*) aPdOutputData);
|
||||
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,12 +635,17 @@ void MainLoop(void)
|
||||
}
|
||||
|
||||
DISABLE_ESC_INT();
|
||||
ECAT_Application();
|
||||
|
||||
if ( bEcatInputUpdateRunning )
|
||||
/*ECAT_DBG(1.6.0 剖分:free-run 应用+输入映射块)*/
|
||||
{
|
||||
/* EtherCAT slave is at least in SAFE-OPERATIONAL, update inputs */
|
||||
PDO_InputMapping();
|
||||
const UINT32 dbgT1 = ecatdbg_dwt();
|
||||
ECAT_Application();
|
||||
|
||||
if ( bEcatInputUpdateRunning )
|
||||
{
|
||||
/* EtherCAT slave is at least in SAFE-OPERATIONAL, update inputs */
|
||||
PDO_InputMapping();
|
||||
}
|
||||
g_dbg_freerunCyc += ecatdbg_dwt() - dbgT1;
|
||||
}
|
||||
ENABLE_ESC_INT();
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,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();
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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++;
|
||||
|
||||
@@ -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,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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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 桥接吞吐) */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user