feat(comm): 添加EtherCAT CAN桥接和PDO功能模块

添加了ethercat_can_bridge.h/cpp和ethercat_pdo.h/cpp四个文件,
扩展通信模块功能,支持CAN桥接和PDO相关操作。

fix(comm): 修复LAN9252 EXMC总线时序配置问题

将BUSLAT从1调整为4,确保满足数据手册要求的最小13ns间隔,
解决因时序余量不足导致的温漂偶发读错问题。

feat(comm): 配置ESC过程数据看门狗时间为1秒

修改ESC_REG_WD_TIME_PD寄存器值从默认100ms到1s,
防止主站调试周期触发SM2看门狗导致状态被拖回SAFEOP。
This commit is contained in:
cbb
2026-09-05 16:54:38 +08:00
parent 44d407390b
commit f9ef1b8da2
16 changed files with 2218 additions and 86 deletions
@@ -79,6 +79,10 @@ project:
- file: ./comm/ethercat_slave.cpp - file: ./comm/ethercat_slave.cpp
- file: ./comm/ethercat_sync.h - file: ./comm/ethercat_sync.h
- file: ./comm/ethercat_sync.cpp - file: ./comm/ethercat_sync.cpp
- file: ./comm/ethercat_can_bridge.h
- file: ./comm/ethercat_can_bridge.cpp
- file: ./comm/ethercat_pdo.h
- file: ./comm/ethercat_pdo.cpp
- group: comm/ssc - group: comm/ssc
files: files:
- file: ./comm/ssc/inc/ecatslv.h - file: ./comm/ssc/inc/ecatslv.h
+37 -4
View File
@@ -198,11 +198,22 @@ bool EscExmcBus::init() {
/* 读/写共用时序(非 extended mode) /* 读/写共用时序(非 extended mode)
* 2026-08-20 慢档测试(16/16/256 HCLK)无效,已还原:慢档时序未带来任何 * 2026-08-20 慢档测试(16/16/256 HCLK)无效,已还原:慢档时序未带来任何
* 读数变化,速度因素彻底排除(与 bit-bang 40-80us 仍失败一致)。 */ * 读数变化,速度因素彻底排除(与 bit-bang 40-80us 仍失败一致)。
* 注:那轮测试的失败根因是访问模型错误(直读核心 CSR 索引空间,1.5.39 修复),
* 与时序无关;时序余量本身仍需按 DS 表 9-4/9-5 独立核算,见下方 BUSLAT。 */
rw_timing.asyn_access_mode = EXMC_ACCESS_MODE_A; rw_timing.asyn_access_mode = EXMC_ACCESS_MODE_A;
rw_timing.syn_data_latency = EXMC_DATALAT_2_CLK; rw_timing.syn_data_latency = EXMC_DATALAT_2_CLK;
rw_timing.syn_clk_division = EXMC_SYN_CLOCK_RATIO_DISABLE; rw_timing.syn_clk_division = EXMC_SYN_CLOCK_RATIO_DISABLE;
rw_timing.bus_latency = 1U; /* BUSLAT: 1→4 (1.5.57)。DS 表 9-4/9-5 要求 trdrd/twrwr/twrrd ≥ 13ns,
* 该间隔由 EXMC 在两次访问之间插入的总线空闲期承担 = (1 固有 + BUSLAT) HCLK。
* BUSLAT=1 → 2 HCLK ≈ 11.1ns,低于 13ns 下限,仅靠 AHB 取指间隙偶然兜底:
* 常温刚好可用,温度升高(传播延迟增大)/电压下降(驱动减弱)即越界,表现为
* 偶发读到 0xFFFF 或错值——与实测的温漂偶发读错吻合。
* BUSLAT=4 → 5 HCLK ≈ 27.8ns,余量 2.1 倍,覆盖全温区与电压波动。
* 代价:每次 HBI 访问多 3 HCLK(16.7ns)。满载 1024B 过程数据按每 DWORD
* 两次 16-bit 访问计,增加约 256×2×16.7ns ≈ 8.6us,相对原 218us 增幅 3.9%,
* 4ms 周期占用从 5.4% 升至 5.7%,无实质影响。 */
rw_timing.bus_latency = 4U;
rw_timing.asyn_address_setuptime = 4U; /* ~22ns。DS 表9-4/9-5: trdrd/twrwr/twrrd rw_timing.asyn_address_setuptime = 4U; /* ~22ns。DS 表9-4/9-5: trdrd/twrwr/twrrd
≥13ns——选通间隔=ADDSET 相,2 HCLK≈11.1ns ≥13ns——选通间隔=ADDSET 相,2 HCLK≈11.1ns
不达标(仅靠 AHB 间隙兜底),4 HCLK 稳过 */ 不达标(仅靠 AHB 间隙兜底),4 HCLK 稳过 */
@@ -212,8 +223,11 @@ bool EscExmcBus::init() {
wr_timing = rw_timing; wr_timing = rw_timing;
ECAT_LOGI("[ECAT][EXMC] init: applying NOR/SRAM Region0 params (16b, async, " ECAT_LOGI("[ECAT][EXMC] init: applying NOR/SRAM Region0 params (16b, async, "
"t_data=%u HCLK)\r\n", "ADDSET=%u AHLD=%u DSET=%u BUSLAT=%u HCLK)\r\n",
(unsigned)rw_timing.asyn_data_setuptime); (unsigned)rw_timing.asyn_address_setuptime,
(unsigned)rw_timing.asyn_address_holdtime,
(unsigned)rw_timing.asyn_data_setuptime,
(unsigned)rw_timing.bus_latency);
exmc_norsram_init(&norsram_init); exmc_norsram_init(&norsram_init);
exmc_norsram_enable(CONFIG_ESC_EXMC_BANK_NOR_SRAM_REGION0); exmc_norsram_enable(CONFIG_ESC_EXMC_BANK_NOR_SRAM_REGION0);
@@ -229,6 +243,25 @@ bool EscExmcBus::init() {
ECAT_LOGI("[ECAT][EXMC] init: SNCTL0 fix: before=0x%08lX after=0x%08lX (bit7 reserved cleared)\r\n", ECAT_LOGI("[ECAT][EXMC] init: SNCTL0 fix: before=0x%08lX after=0x%08lX (bit7 reserved cleared)\r\n",
(unsigned long)snctl_before, (unsigned long)snctl_after); (unsigned long)snctl_before, (unsigned long)snctl_after);
/* 时序寄存器实测回读:确认写入的 BUSLAT 真正落到硬件(1.5.57)。
* SNTCFG0 位域: [3:0]=ASET-1 [7:4]=AHLD-1 [15:8]=DSET-1 [19:16]=BUSLAT-1。
* 期望 ASET=4/AHLD=2/DSET=20/BUSLAT=4 → 0x0313_0000|... 逐字段解码打印,
* 避免只看整字导致的位域误读;BUSLAT 实际值 <4 说明库或硬件截断,须立即排查。 */
{
const uint32_t tcfg = EXMC_SNTCFG0;
const unsigned aset = (unsigned)((tcfg & EXMC_SNTCFG_ASET) >> 0) + 1U;
const unsigned ahld = (unsigned)((tcfg & EXMC_SNTCFG_AHLD) >> 4) + 1U;
const unsigned dset = (unsigned)((tcfg & EXMC_SNTCFG_DSET) >> 8) + 1U;
const unsigned buslat = (unsigned)((tcfg & EXMC_SNTCFG_BUSLAT) >> 16) + 1U;
ECAT_LOGI("[ECAT][EXMC] init: SNTCFG0=0x%08lX -> ASET=%u AHLD=%u DSET=%u BUSLAT=%u\r\n",
(unsigned long)tcfg, aset, ahld, dset, buslat);
/* 5 HCLK @180MHz = 27.8ns,DS trdrd 下限 13ns → 需 BUSLAT>=2 才达标,取 4 留余量 */
if (buslat < 4U) {
ECAT_LOGI("[ECAT][EXMC] WARN: BUSLAT=%u < 4, trdrd 余量不足,"
" 温漂/压降时会偶发读到 0xFFFF 或错值\r\n", buslat);
}
}
/* 总线探活:LAN9252 处于 16-bit HBI Indexed Mode,无法通过固定偏移直接读取。 /* 总线探活:LAN9252 处于 16-bit HBI Indexed Mode,无法通过固定偏移直接读取。
* 真实的芯片 ID / AL 状态校验由 EscLan9252::init() 通过 HBI_IDX_0/HBI_DATA_0 * 真实的芯片 ID / AL 状态校验由 EscLan9252::init() 通过 HBI_IDX_0/HBI_DATA_0
* 间接访问完成。 注意:LAN9252 上电后第一次 Host Bus * 间接访问完成。 注意:LAN9252 上电后第一次 Host Bus
+26
View File
@@ -58,6 +58,13 @@ constexpr uint32_t IRQ_CFG_IRQ_POL = 1UL << 4; /**< 1=高有效,0=低有效(
constexpr uint32_t IRQ_CFG_IRQ_TYPE = 1UL << 0; /**< 1=推挽,0=开漏(开漏强制低有效)。 */ constexpr uint32_t IRQ_CFG_IRQ_TYPE = 1UL << 0; /**< 1=推挽,0=开漏(开漏强制低有效)。 */
constexpr uint32_t INT_EN_ECAT_INT = 1UL << 0; /**< 允许 EtherCAT 事件驱动 IRQ 引脚。 */ constexpr uint32_t INT_EN_ECAT_INT = 1UL << 0; /**< 允许 EtherCAT 事件驱动 IRQ 引脚。 */
/* ESC 过程数据看门狗(DS 12.14.41-43)。SII 不携带看门狗字段,芯片复位默认
* divider=0x09C2(100µs)/WD Time PD=0x03E8(1000) 即 100ms——低于实测可用的
* 100ms 手动调试周期,SM2 每 100ms 被判定超时,SSC 以 0x001B 拖回 SAFEOP。 */
constexpr uint16_t ESC_REG_WD_DIVIDER = 0x0400U; /**< 看门狗分频(100µs 基准,复位默认 0x09C2)。 */
constexpr uint16_t ESC_REG_WD_TIME_PD = 0x0420U; /**< 过程数据看门狗步数(复位默认 0x03E8=100ms)。 */
constexpr uint16_t ESC_WD_TIME_PD_STEPS = 10000U; /**< 10000 × 100µs = 1s。 */
/* LAN9252 直接映射的 ESC 访问端口(DS 表 12-13;经 HBI 索引直接读写) */ /* LAN9252 直接映射的 ESC 访问端口(DS 表 12-13;经 HBI 索引直接读写) */
constexpr uint16_t ECAT_PRAM_RD_DATA = 0x0000U; /**< PRAM 读 FIFO(0x00-0x1C 别名,功能相同,突发用) */ constexpr uint16_t ECAT_PRAM_RD_DATA = 0x0000U; /**< PRAM 读 FIFO(0x00-0x1C 别名,功能相同,突发用) */
constexpr uint16_t ECAT_PRAM_WR_DATA = 0x0020U; /**< PRAM 写 FIFO(0x20-0x3C 别名) */ constexpr uint16_t ECAT_PRAM_WR_DATA = 0x0020U; /**< PRAM 写 FIFO(0x20-0x3C 别名) */
@@ -257,6 +264,7 @@ bool EscLan9252::init()
printf("[ECAT][ESC] PDI operational, EtherCAT core ready (DL_STATUS=0x%04X, waited %ums)\r\n", printf("[ECAT][ESC] PDI operational, EtherCAT core ready (DL_STATUS=0x%04X, waited %ums)\r\n",
(unsigned)dlStatus, (unsigned)waited); (unsigned)dlStatus, (unsigned)waited);
enableEcatIrqOutput(); enableEcatIrqOutput();
configPdWatchdog();
} else { } else {
printf("[ECAT][ESC] init: FAIL, PDI not operational after %ums " printf("[ECAT][ESC] init: FAIL, PDI not operational after %ums "
"(DL_STATUS=0x%04X EEP_CSR=0x%04X)\r\n", "(DL_STATUS=0x%04X EEP_CSR=0x%04X)\r\n",
@@ -279,6 +287,24 @@ bool EscLan9252::init()
* 下降沿触发,故此处选推挽(IRQ_TYPE=1) + 低有效(IRQ_POL=0)。 * 下降沿触发,故此处选推挽(IRQ_TYPE=1) + 低有效(IRQ_POL=0)。
* 注意 AL Event Mask(0x204) 由 SSC 在状态迁移时自行维护,不在此处设置。 * 注意 AL Event Mask(0x204) 由 SSC 在状态迁移时自行维护,不在此处设置。
*/ */
void EscLan9252::configPdWatchdog()
{
/* 1.5.45:过程数据看门狗 100ms(复位默认)→1s。
* 主站侧手动单步/100ms 调试周期会触发 SM2 看门狗:SSC 在 CheckIfEcatError()
* 读 0x0440 发现已过期,调 AL_ControlInd(STATE_SAFEOP, 0x001B) 把 OP 拖回 SAFEOP,
* 表现为"100ms 周期全 WKC 错、10/50ms 正常"。分频保持默认 100µs,只延长步数;
* 正常周期(≤50ms)远小于 1s,行为不变。 */
csrWrite(ESC_REG_WD_TIME_PD, ESC_WD_TIME_PD_STEPS, 2U);
const uint32_t rd = csrRead(ESC_REG_WD_TIME_PD, 2U);
if ((rd & 0xFFFFU) == ESC_WD_TIME_PD_STEPS) {
printf("[ECAT][ESC] PD watchdog = 1s (0x0420=%u x 100us, default was 100ms)\r\n",
(unsigned)ESC_WD_TIME_PD_STEPS);
} else {
printf("[ECAT][ESC] WARN: PD watchdog writeback 0x%04lX != %u, 100ms 调试周期会被 0x001B 拖回 SAFEOP\r\n",
(unsigned long)(rd & 0xFFFFU), (unsigned)ESC_WD_TIME_PD_STEPS);
}
}
void EscLan9252::enableEcatIrqOutput() void EscLan9252::enableEcatIrqOutput()
{ {
/* 先清掉可能悬留的状态位(R/WC:写 1 清零),避免使能瞬间就有一个陈旧电平。 */ /* 先清掉可能悬留的状态位(R/WC:写 1 清零),避免使能瞬间就有一个陈旧电平。 */
+6
View File
@@ -93,6 +93,12 @@ private:
* bEcatFirstOutputsReceived 恒 FALSE 使 SAFEOP->OP 被无限挂起。 */ * bEcatFirstOutputsReceived 恒 FALSE 使 SAFEOP->OP 被无限挂起。 */
static void enableEcatIrqOutput(); static void enableEcatIrqOutput();
/** @brief 过程数据看门狗 100ms(复位默认)→1s(1.5.45)。
* @note 由 init() 在 PDI/核心就绪后调用。写 ESC_REG_WD_TIME_PD(0x0420)=10000
* (100µs 分频保持默认)。不写则主站 100ms 手动调试周期触发 SM2 看门狗,
* SSC 以 0x001B 拖回 SAFEOP。 */
static void configPdWatchdog();
static bool s_busReady; /**< init 完整成功(EXMC+IDX 探针+BYTE_TEST)后置位。 */ static bool s_busReady; /**< init 完整成功(EXMC+IDX 探针+BYTE_TEST)后置位。 */
static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位(1.5.39)。 */ static bool s_coreReady; /**< EEPROM 加载完成、EtherCAT 核心使能后置位(1.5.39)。 */
static bool s_resetDone; /**< hardwareReset 已执行门控(防二次复位清掉 SSC 配置)。 */ static bool s_resetDone; /**< hardwareReset 已执行门控(防二次复位清掉 SSC 配置)。 */
@@ -0,0 +1,390 @@
/**
* @file ethercat_can_bridge.cpp
* @brief EtherCAT PDO ↔ CAN 转发桥实现(1.5.56)。
*/
#include "comm/ethercat_can_bridge.h"
#include "device/canfd.h"
#include "device/motor_state.h"
#include "RTE_Components.h"
#include CMSIS_device_header /* __DMB():seqlock 的编译器/总线屏障 */
#include <cstring>
volatile uint8_t EthercatCanBridge::s_lastSeq = 0U;
volatile bool EthercatCanBridge::s_seqPrimed = false;
volatile uint8_t EthercatCanBridge::s_lastTxResult = 0U;
volatile uint16_t EthercatCanBridge::s_txCount = 0U;
volatile uint16_t EthercatCanBridge::s_dropCount = 0U;
volatile uint8_t EthercatCanBridge::s_winBase = 0U;
volatile bool EthercatCanBridge::s_rxValid = false;
volatile uint32_t EthercatCanBridge::s_xrxSeq = 0U;
volatile uint8_t EthercatCanBridge::s_rxCh = 0U;
volatile uint8_t EthercatCanBridge::s_rxDlc = 0U;
volatile bool EthercatCanBridge::s_rxTruncated = false;
volatile uint32_t EthercatCanBridge::s_rxId = 0U;
volatile uint32_t EthercatCanBridge::s_rxCount = 0U;
uint8_t EthercatCanBridge::s_rxData[kEcatCanXRxDataMax] = {0};
EthercatCanBridge::RxRingItem
EthercatCanBridge::s_rxRing[kEcatRxRingSlots] = {};
volatile uint8_t EthercatCanBridge::s_rxRingHead = 0U;
volatile uint8_t EthercatCanBridge::s_rxRingTail = 0U;
volatile uint8_t EthercatCanBridge::s_rxRingLost = 0U;
namespace {
/** 小端读写:PDO 字节序按 EtherCAT 惯例固定小端,不依赖编译器结构体布局。 */
inline uint32_t rd32le(const uint8_t *p)
{
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
inline void wr32le(uint8_t *p, uint32_t v)
{
p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8);
p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24);
}
inline void wr16le(uint8_t *p, uint16_t v)
{
p[0] = (uint8_t)(v); p[1] = (uint8_t)(v >> 8);
}
} // namespace
uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
{
uint8_t want = out[ECAT_TXB_OFF_COUNT];
if (want == 0U) {
return 0U; /* 未使用批量区:行为与 1.5.55 完全一致 */
}
if (want > kEcatTxBatchMax) {
want = kEcatTxBatchMax;
}
const bool stopOnError = (out[ECAT_TXB_OFF_FLAGS] & 0x01U) != 0U;
uint8_t sent = 0U;
uint16_t off = ECAT_TXB_OFF_ITEMS;
for (uint8_t i = 0U; i < want; ++i) {
/* 不信任主站给的 count/dlc:每步都验证头与数据都完整落在 PDO 内。
* 越界即停 —— 这是唯一的边界判据,count 只是上限提示。 */
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE) > kEcatPdoOutBytes) {
break;
}
const uint8_t *f = &out[off];
uint8_t dlc = f[ECAT_FRM_OFF_DLC];
if (dlc > kEcatCanFdDataMax) {
dlc = kEcatCanFdDataMax;
}
if ((uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc) > kEcatPdoOutBytes) {
break;
}
const uint8_t ch = f[ECAT_FRM_OFF_CH];
CanFd::CanTxResult r;
if (!CanFd::isChannelEnabled(ch)) {
r = CanFd::CanTxResult::kErrInvalid;
} else {
/* 与单帧路径同样用 enqueueTx:批量下发必然连续打同一通道,
* transmitFrame 的 allow_abort 会让后帧顶掉前帧(历史上 7 帧
* 只发出 3 帧的根因),队列语义才不静默丢帧。 */
r = CanFd::enqueueTx(ch, rd32le(&f[ECAT_FRM_OFF_ID]),
&f[ECAT_FRM_HDR_SIZE], dlc);
}
if (r == CanFd::CanTxResult::kOk) {
++s_txCount;
++sent;
} else {
s_lastTxResult = (uint8_t)r;
if (stopOnError) {
break;
}
}
off = (uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc);
}
return sent;
}
void EthercatCanBridge::fillRxBatch(uint8_t *in)
{
uint8_t count = 0U;
uint16_t off = ECAT_RXB_OFF_ITEMS;
uint8_t tail = s_rxRingTail;
const uint8_t head = s_rxRingHead; /* 快照一次:生产者可能边填边推进 */
while (tail != head && count < kEcatRxBatchMax) {
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) {
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;
wr32le(&p[ECAT_FRM_OFF_ID], it.id);
memcpy(&p[ECAT_FRM_HDR_SIZE], it.data, dlc);
off = (uint16_t)(off + ECAT_FRM_HDR_SIZE + dlc);
tail = (uint8_t)((tail + 1U) % kEcatRxRingSlots);
++count;
}
__DMB(); /* 槽位读取先于 tail 发布落地:否则生产者可能提前覆写还没读完的槽 */
s_rxRingTail = tail; /* 只在这里推进:消费者独占写 tail */
const uint8_t pending = (uint8_t)((head + kEcatRxRingSlots - tail) % kEcatRxRingSlots);
in[ECAT_RXB_OFF_COUNT] = count;
in[ECAT_RXB_OFF_LOST] = s_rxRingLost;
in[ECAT_RXB_OFF_PENDING] = pending;
s_rxRingLost = 0U; /* 回填后清零:lost 是"自上次回填以来"的增量 */
}
void EthercatCanBridge::processOutput(const uint8_t *out)
{
if (out == nullptr) {
return;
}
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;
/* 上电/重连后第一次看到输出数据时只记录基准,不发送:否则主站残留的
* 非零 seq 会被当成一次新请求,在进 OP 的瞬间凭空发出一帧。 */
if (!s_seqPrimed) {
s_seqPrimed = true;
s_lastSeq = seq;
return;
}
/* flags bit0:清零统计(一次性,不依赖 seq 沿,便于主站随时归零) */
if ((out[ECAT_CAN_TX_OFF_FLAGS] & 0x01U) != 0U) {
s_txCount = 0U;
s_dropCount = 0U;
s_rxCount = 0U;
}
if (seq == s_lastSeq) {
return; /* 没有新的发送请求 */
}
s_lastSeq = seq;
const uint8_t ch = out[ECAT_CAN_TX_OFF_CH];
uint8_t dlc = out[ECAT_CAN_TX_OFF_DLC];
if (dlc > kEcatCanTxDataMax) {
dlc = kEcatCanTxDataMax;
}
/* 单帧区先发(保持 1.5.55 语义),再发批量区。
* dlc==0 表示本次只用批量区,不发单帧 —— 否则会凭空发出一帧 0 长度报文。 */
if (dlc > 0U) {
/* isChannelEnabled 内含越界检查,是"通道号合法且可用"的唯一判据 */
if (!CanFd::isChannelEnabled(ch)) {
s_lastTxResult = (uint8_t)CanFd::CanTxResult::kErrInvalid;
} else {
/* 用 enqueueTx 而非 transmitFrame:PDO 是周期性场景,可能连续多帧打到
* 同一通道。transmitFrame 默认 allow_abort=true 会在邮箱满时顶掉已装载
* 的旧帧(历史上 7 帧突发只发出 3 帧的根因);enqueueTx 队列空时直通、
* 满时排队,不静默丢帧。 */
const CanFd::CanTxResult r =
CanFd::enqueueTx(ch, rd32le(&out[ECAT_CAN_TX_OFF_ID]),
&out[ECAT_CAN_TX_OFF_DATA], dlc);
s_lastTxResult = (uint8_t)r;
if (r == CanFd::CanTxResult::kOk) {
++s_txCount;
}
}
}
/* 批量区:同一个 seq 沿里一次性发 N 帧,供多轴同步下发。 */
(void)sendBatch(out);
}
void EthercatCanBridge::buildInput(uint8_t *in)
{
if (in == nullptr) {
return;
}
/* 必须按新的全长清零。旧代码写死 memset(in,0,32),扩容后会把 32 字节以后
* 的槽区留成上一周期的残留数据,主站读到的是"看起来合法但已过期"的电机状态。 */
memset(in, 0, kEcatPdoInBytes);
/* ── 段 1(0..31):桥状态 + 短快照,布局与 1.5.53 逐字节兼容 ── */
in[ECAT_CAN_RX_OFF_ACK] = s_lastSeq;
in[ECAT_CAN_RX_OFF_TXRES] = s_lastTxResult;
wr16le(&in[ECAT_CAN_RX_OFF_TXCNT], s_txCount);
wr16le(&in[ECAT_CAN_RX_OFF_DROP], s_dropCount);
wr32le(&in[ECAT_CAN_RX_OFF_RXCNT], s_rxCount);
/* seqlock 判稳(见 onCanFrame):奇数=生产者正在写这组快照。单核下 ISR
* 执行期间生产者冻结,读一次即可定性;本周期跳过段 1/段 3(开头已清零,
* 语义=无有效快照),下周期自愈。批量区(段 4)走环形队列,不受影响。 */
const bool xrxStable = ((s_xrxSeq & 1U) == 0U);
if (xrxStable && s_rxValid) {
uint8_t flags = 0x01U;
if (s_rxTruncated) {
flags |= 0x02U;
}
in[ECAT_CAN_RX_OFF_CH] = s_rxCh;
in[ECAT_CAN_RX_OFF_DLC] = s_rxDlc;
in[ECAT_CAN_RX_OFF_FLAGS] = flags;
wr32le(&in[ECAT_CAN_RX_OFF_ID], s_rxId);
/* 段 1 只放前 8 字节(兼容),完整 payload 见段 3 */
memcpy(&in[ECAT_CAN_RX_OFF_DATA], s_rxData, kEcatCanRxDataMax);
}
const uint8_t ch = s_rxValid ? s_rxCh : 0U;
const uint32_t backlog = CanFd::isChannelEnabled(ch) ? CanFd::txQueueBacklog(ch) : 0U;
in[ECAT_CAN_RX_OFF_BACKLOG] = (backlog > 255U) ? 255U : (uint8_t)backlog;
/* ── 段 2(32..319):电机状态窗口 ──
* 直接读 MotorState 聚合表,不再依赖"最近一帧 CAN"。窗口起点由主站通过
* 输出 PDO 的 WIN 字节指定,主站每周期 +7 即可 4 周期扫完 27 个槽。
*
* 上下文安全:buildInput 在主循环(free-run)或 Sync0_Isr 里跑,而状态表由
* 主循环的 taskCanRxParse 写。单字段撕裂最坏是某槽的 seq 与 data 不同步一拍,
* 主站按 seq 变化判新旧、下一周期即自愈,故不加临界区(与本文件既有约定一致)。 */
const MotorStateTable &mt = MotorState::table();
const uint8_t base = (s_winBase < kMotorSlotCount) ? s_winBase : 0U;
in[ECAT_MOTW_OFF_ACTIVE] = mt.active_count;
in[ECAT_MOTW_OFF_BOOT] = mt.boot_active;
in[ECAT_MOTW_OFF_WIN_BASE] = base;
wr32le(&in[ECAT_MOTW_OFF_GLOBAL_SEQ], mt.global_seq);
uint8_t count = 0U;
for (uint8_t i = 0U; i < kEcatMotSlotsPerPdo; ++i) {
const uint8_t idx = (uint8_t)(base + i);
if (idx >= kMotorSlotCount) {
break; /* 窗口尾部越界即停,不回绕:主站据 win_count 判断 */
}
const MotorSlot &s = mt.slots[idx];
uint8_t *p = &in[ECAT_MOTW_OFF_SLOTS + (uint16_t)i * kEcatMotSlotSize];
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;
/* ── 段 3(320..359):最近一帧完整快照 ──
* 版本帧(0x00FF)/CONFIG 帧(0x0101) 都是 DLC 24,段 1 的 8 字节装不下。
* 与段 1 共用 xrxStable 门控:两段必须来自同一帧,否则 ecat.html 的短快照
* 和电机页的长快照会各自解出半新半旧的 ID/数据。 */
if (xrxStable && s_rxValid) {
in[ECAT_XRX_OFF_CH] = s_rxCh;
in[ECAT_XRX_OFF_DLC] = s_rxDlc;
in[ECAT_XRX_OFF_FLAGS] = 0x01U;
wr32le(&in[ECAT_XRX_OFF_ID], s_rxId);
memcpy(&in[ECAT_XRX_OFF_DATA], s_rxData, kEcatCanXRxDataMax);
}
/* ── 段 4(360..639):CANFD 批量接收区 ──
* 段 1/段 3 只带"最近一帧",高速多帧场景下周期间到达的帧会互相覆盖
* (s_dropCount 就是在数这个)。批量区从环形队列里一次回收多帧,
* 让主站在一个周期内拿到完整的帧序列而不是采样点。 */
fillRxBatch(in);
}
void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
const uint8_t *data, uint8_t length)
{
if (data == nullptr) {
return;
}
/* 上一帧还没被 PDO 周期取走就来了新帧:记一次丢弃(覆盖旧帧,
* 保证主站看到的永远是最新状态,而不是卡在一帧陈旧数据上)。 */
if (s_rxValid) {
++s_dropCount;
}
/* 缓存区已扩到 32B(段 3 用),truncated 仍按段 1 的 8B 口径上报,
* 这样 ecat.html 的 CAN 调试页语义不变。 */
uint8_t copyLen = length;
if (copyLen > kEcatCanXRxDataMax) {
copyLen = kEcatCanXRxDataMax;
}
/* ── 快照 seqlock(1.5.58)──
* s_rxData/s_rxCh/s_rxId/s_rxDlc/s_rxTruncated/s_rxValid 是一组,必须整体
* 被 buildInput 读到。DC 模式下 buildInput 跑在 Sync0_Isr(preempt=0),会
* 抢占本函数(主循环 taskCanRxParse)。原注释声称"valid 最后置位保证一致"
* 是错的:valid 一旦置位就永不清零,ISR 落在"新 payload 已写完、id 还没
* 更新"的窗口里,会把新帧数据配旧帧 ID 发出去——STATUS 字节被标成 CONFIG
* (0x000N0101),前端按 CONFIG 布局解码出 -3e26 这类离谱目标值。
* 协议:seq 奇数=正在写。消费者在单核 ISR 里生产者必然冻结,判一次奇偶
* 即可,奇数则本周期跳过(段 1/段 3 已清零=无效),下周期自愈。
* 不关中断:critical.h 的 BASEPRI 方案放行 preempt=0,挡不住 Sync0;
* 而 seqlock 零抖动,与本文件既有无锁约定一致。
* __DMB 防编译器把非 volatile 的 memcpy 挪出两次 seq 自增之间。 */
++s_xrxSeq; /* → 奇数:快照进入"正在写" */
__DMB();
memcpy(s_rxData, data, copyLen);
if (copyLen < kEcatCanXRxDataMax) {
memset(&s_rxData[copyLen], 0, (size_t)(kEcatCanXRxDataMax - copyLen));
}
s_rxCh = channel;
s_rxId = id;
s_rxDlc = length; /* 报告原始长度,便于主站发现被截断 */
s_rxTruncated = (length > kEcatCanRxDataMax);
++s_rxCount;
s_rxValid = true;
__DMB();
++s_xrxSeq; /* → 偶数:快照整体一致 */
/* ── 推入批量接收环形队列(1.5.56)──
* 与上面的"最近一帧快照"并行维护:快照会被后帧覆盖(这是它的设计语义),
* 队列则保序累积,供 fillRxBatch 一次回收多帧。
* 队列满时丢**新**帧并计数,而不是覆盖旧帧 —— 批量区的价值在于保序完整,
* 覆盖会在序列中间留下空洞,比明确丢尾更难排查。 */
const uint8_t next = (uint8_t)((s_rxRingHead + 1U) % kEcatRxRingSlots);
if (next == s_rxRingTail) {
if (s_rxRingLost < 0xFFU) {
++s_rxRingLost;
}
} else {
RxRingItem &it = s_rxRing[s_rxRingHead];
uint8_t n = length;
if (n > kEcatCanFdDataMax) {
n = kEcatCanFdDataMax;
}
it.id = id;
it.ch = channel;
it.dlc = n;
memcpy(it.data, data, n);
__DMB(); /* 数据写入先于 head 发布落地(volatile 不约束非 volatile 的 memcpy) */
s_rxRingHead = next; /* 最后推进 head:消费者读到时数据必已写完 */
}
}
extern "C" {
void EcatCan_ProcessOutput(const uint8_t *out)
{
EthercatCanBridge::processOutput(out);
}
void EcatCan_BuildInput(uint8_t *in)
{
EthercatCanBridge::buildInput(in);
}
} /* extern "C" */
+278
View File
@@ -0,0 +1,278 @@
/**
* @file ethercat_can_bridge.h
* @brief EtherCAT PDO ↔ CAN 转发桥(1.5.55)。
*
* 输出 PDO(0x7000, 主站→从站, 64B) = 待发送的 CAN 帧(数据区 32B)
* 输入 PDO(0x6000, 从站→主站, 384B) = 桥状态 + 电机状态窗口 + 长帧快照
*
* 发送用**序号沿触发**:仅当 seq 字节变化时才真正发一帧。PDO 是周期性刷新的,
* 若按「内容非空即发」会把同一帧每个周期重发一次;seq 变化沿是 PDO 里表达
* 「一次性事件」的标准做法,主站只需每次发送前把 seq 加一。
*
* RX 侧不能自己去 CanFd::consumeRxFrame() —— 那个队列已有唯一消费者
* TaskRunner::taskCanRxParse(),两个消费者会互相抢帧,OTA relay / 电机状态
* 会随机丢帧。改为由 taskCanRxParse() 的分发链回调 onCanFrame() 投递。
*
* ── 1.5.55 为电机控制台扩容(32B/32B → 64B/384B)──
*
* 三个动机:
* 1. TX 数据区 24B→32B。电机 CMD 是 DLC 32,旧 processOutput 里
* `dlc > kEcatCanTxDataMax` 直接钳位到 24,后 8 字节被**静默丢弃**且不报错。
* 2. 输入侧加电机状态窗口。单帧快照对电机控制台不够用:9 节点各 32B STATUS,
* 一周期只带回一帧、新帧还覆盖旧帧,永远取不齐。改为直接从 MotorState
* 状态表(已按 node×channel 聚合,见 device/motor_state.h)取一个窗口,
* 主站移动 win_base 即可 4 个周期扫完全部 27 槽。
* 3. 加长帧快照区。旧 RX 数据区只有 8B,版本帧(DLC24)/CONFIG 帧(DLC24) 会被截断。
*
* **输入 PDO 前 32 字节与 1.5.53 逐字节兼容**,偏移与语义全部不变。这样
* tools/ecat 的 CAN 调试页(ecat.html)与 ecat_server.py 的 _can_decode_input
* 不改一行也照常工作,扩容只是在后面追加新区段。
*
* ── 1.5.56 扩到 DPRAM 硬上限并加 CANFD 批量帧区(64/384 → 384/640)──
*
* 动机:需要一个周期里合并下发/回收**多条 CANFD 帧**。1.5.55 的单帧槽
* (一个 seq 一帧)每周期最多一帧,4ms 周期就是 250 帧/s 上限,
* 而一次多轴同步下发就要 9 帧,靠排队要 36ms 才发完 —— 各轴指令不同拍,
* 失去"同步下发"的意义。
*
* 新增两个批量区,每条帧带 8B 头(ch/dlc/flags/rsvd + id)+ 数据:
* 输出 48..383(336B):批量发送,一个 seq 沿一次性发 N 帧
* 输入 360..639(280B):批量接收,一周期回收多帧而不是只带最近一帧
*
* 三条兼容性铁律(改动只做追加,不动既有偏移):
* 1. 输出 0..47 与 1.5.55 单帧布局完全一致(seq/ch/dlc/flags/id/win/data32);
* 2. 输入 0..359 与 1.5.55 完全一致(段1 桥状态 + 段2 电机窗口 + 段3 长帧);
* 3. 批量区**为空时**(count=0)行为与 1.5.55 完全相同,老主站照旧可用。
*
* 上限推导(为何是 384+640 而不是更大)见 comm/ethercat_pdo_size.h。
*/
#ifndef COMM_ETHERCAT_CAN_BRIDGE_H
#define COMM_ETHERCAT_CAN_BRIDGE_H
#include <cstdint>
#include "comm/ethercat_pdo_size.h"
/** 输出 PDO(主站→从站, 384B)段 1(0..47):单帧发送。**与 1.5.55 完全兼容**。 */
enum : uint8_t {
ECAT_CAN_TX_OFF_SEQ = 0U, /**< 发送序号:变化沿触发一次发送(单帧+批量一起) */
ECAT_CAN_TX_OFF_CH = 1U, /**< 目标通道 0/1/2 */
ECAT_CAN_TX_OFF_DLC = 2U, /**< 数据长度 0..32;0 表示本次不发单帧(只发批量) */
ECAT_CAN_TX_OFF_FLAGS = 3U, /**< bit0=1 请求清零统计;其余保留 */
ECAT_CAN_TX_OFF_ID = 4U, /**< CAN ID,小端 uint32(29 位扩展帧) */
ECAT_CAN_TX_OFF_WIN = 8U, /**< 请求回传的电机槽窗口起始下标 0..26 */
/* 9..15 保留:让数据区落在 16 字节边界,hexdump 排查时对齐好读 */
ECAT_CAN_TX_OFF_DATA = 16U, /**< 数据区起点,32 字节(一帧电机 CMD) */
};
/** 输出 PDO 段 2(48..383):CANFD 批量发送区(1.5.56 新增)。 */
enum : uint16_t {
ECAT_TXB_OFF_COUNT = 48U, /**< 本次要发的帧数 0..kEcatTxBatchMax;0=不发批量 */
ECAT_TXB_OFF_FLAGS = 49U, /**< bit0=1 遇错即停(默认继续发完并各自记结果) */
ECAT_TXB_OFF_RSVD = 50U, /**< 保留 2B,让帧数组从 52 起、每帧 8B 头对齐 */
ECAT_TXB_OFF_ITEMS = 52U, /**< 帧数组起点,每项 (8B 头 + dlc 数据) 紧密排列 */
};
/** 批量帧项内部布局(头 8 字节,之后紧跟 dlc 字节数据)。收发两向共用。 */
enum : uint8_t {
ECAT_FRM_OFF_CH = 0U, /**< 通道 0/1/2 */
ECAT_FRM_OFF_DLC = 1U, /**< 数据长度 0..64(CANFD 全量) */
ECAT_FRM_OFF_FLAGS = 2U, /**< 发送向:保留; 接收向 bit0=有效 */
ECAT_FRM_OFF_RES = 3U, /**< 发送向:从站回填该帧发送结果; 接收向:保留 */
ECAT_FRM_OFF_ID = 4U, /**< CAN ID,小端 uint32 */
ECAT_FRM_HDR_SIZE = 8U, /**< 头长度,数据紧跟其后 */
};
/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照。**与 1.5.53 完全兼容**。 */
enum : uint8_t {
ECAT_CAN_RX_OFF_ACK = 0U, /**< 回显已执行的发送序号,主站据此确认送达 */
ECAT_CAN_RX_OFF_CH = 1U, /**< 该帧来自哪个通道 */
ECAT_CAN_RX_OFF_DLC = 2U, /**< 该帧数据长度(原始 DLC,未被截断前的值) */
ECAT_CAN_RX_OFF_FLAGS = 3U, /**< bit0=有过接收;bit1=数据被截断到 8B */
ECAT_CAN_RX_OFF_ID = 4U, /**< 该帧 CAN ID,小端 uint32 */
ECAT_CAN_RX_OFF_DATA = 8U, /**< 数据区起点,8 字节(长帧见段 3) */
ECAT_CAN_RX_OFF_RXCNT = 16U, /**< 累计接收帧数,小端 uint32 */
ECAT_CAN_RX_OFF_TXRES = 20U, /**< 最近一次发送结果(CanTxResult 原值,0=OK) */
ECAT_CAN_RX_OFF_BACKLOG = 21U, /**< 该通道软件 TX 队列积压 */
ECAT_CAN_RX_OFF_TXCNT = 22U, /**< 累计发送受理帧数,小端 uint16 */
ECAT_CAN_RX_OFF_DROP = 24U, /**< 桥接层丢帧数(投递时上一帧还没被取走),小端 uint16 */
};
/** 输入 PDO 段 2(32..319):电机状态窗口头 + 槽数组。 */
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_GLOBAL_SEQ = 36U,/**< 状态表全局刷新序号,小端 uint32 */
ECAT_MOTW_OFF_SLOTS = 40U, /**< 槽数组起点,7×40B */
};
/** 电机状态槽内部布局(40 字节)。 */
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_SEQ = 4U, /**< STATUS 刷新序号,小端 uint32 */
ECAT_MOT_OFF_STATUS = 8U, /**< 原始 STATUS payload,32 字节 */
};
/** 输入 PDO 段 3(320..359):最近一帧**完整**快照(版本/CONFIG 帧要 24B)。 */
enum : uint16_t {
ECAT_XRX_OFF_CH = 320U, /**< 通道 */
ECAT_XRX_OFF_DLC = 321U, /**< 原始 DLC(可达 32) */
ECAT_XRX_OFF_FLAGS = 322U, /**< bit0=有效 */
ECAT_XRX_OFF_RSVD = 323U,
ECAT_XRX_OFF_ID = 324U, /**< CAN ID,小端 uint32 */
ECAT_XRX_OFF_DATA = 328U, /**< 数据区,32 字节完整 payload */
};
/** 输入 PDO 段 4(360..639):CANFD 批量接收区(1.5.56 新增)。 */
enum : uint16_t {
ECAT_RXB_OFF_COUNT = 360U, /**< 本周期回收的帧数 0..kEcatRxBatchMax */
ECAT_RXB_OFF_LOST = 361U, /**< 本周期因批量区放不下而丢弃的帧数 */
ECAT_RXB_OFF_PENDING = 362U, /**< 环形队列中仍待回收的帧数(主站据此提速) */
ECAT_RXB_OFF_RSVD = 363U,
ECAT_RXB_OFF_ITEMS = 364U, /**< 帧数组起点,项布局同 ECAT_FRM_OFF_* */
};
/** 单帧数据在输出 PDO / 长帧快照区中的最大字节数(电机 CMD 是 DLC 32)。 */
constexpr uint8_t kEcatCanTxDataMax = 32U;
/** 段 1 短快照数据区字节数(保持 1.5.53 兼容,勿改)。 */
constexpr uint8_t kEcatCanRxDataMax = 8U;
/** 段 3 长快照数据区字节数。 */
constexpr uint8_t kEcatCanXRxDataMax = 32U;
/** 单个电机状态槽字节数。 */
constexpr uint8_t kEcatMotSlotSize = 40U;
/** 一份输入 PDO 携带的电机状态槽数。 */
constexpr uint8_t kEcatMotSlotsPerPdo = 7U;
/** 过程数据长度,唯一真源见 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 */
/** CANFD 单帧数据上限(DLC 64)—— 批量区按此上限做容量推导。 */
constexpr uint8_t kEcatCanFdDataMax = 64U;
/** 批量发送区可用字节数与最坏情况(全 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);
constexpr uint8_t kEcatRxBatchMax =
(uint8_t)(kEcatRxBatchBytes / (ECAT_FRM_HDR_SIZE + kEcatCanFdDataMax));
/** 批量接收环形队列槽数。取 kEcatRxBatchMax 的 4 倍,让主站慢一两拍也不丢帧。
* 注意这是 RAM 成本(槽数 × 72B),不是 PDO 成本。 */
constexpr uint8_t kEcatRxRingSlots = (uint8_t)(kEcatRxBatchMax * 4U);
/* 布局守卫:段 2 的槽数组必须完整落在输入 PDO 内,段 3 也不能越界。
* 改槽数/槽大小时忘了同步 PDO 长度,会在这里编译期失败。 */
static_assert(ECAT_MOTW_OFF_SLOTS + kEcatMotSlotsPerPdo * kEcatMotSlotSize
<= ECAT_XRX_OFF_CH, "电机槽窗口与长帧快照区重叠");
static_assert(ECAT_XRX_OFF_DATA + kEcatCanXRxDataMax <= ECAT_RXB_OFF_COUNT,
"长帧快照区与批量接收区重叠");
static_assert(ECAT_CAN_TX_OFF_DATA + kEcatCanTxDataMax <= ECAT_TXB_OFF_COUNT,
"单帧发送数据区与批量发送区重叠");
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");
/**
* @brief EtherCAT ↔ CAN 转发桥。
* @note 全静态;PDO 侧函数在 Sync0_Isr(preempt=0) 或主循环 MainLoop 上下文执行,
* onCanFrame() 在主循环 taskCanRxParse() 上下文执行。共享状态用 volatile +
* 单生产者/单消费者语义,不需要临界区(投递只写、消费只读+清标志)。
* 例外:段 1/段 3 的"最近一帧快照"是多字段一组(id/dlc/data 必须配套),
* 用 s_xrxSeq seqlock 保证消费者不会读到"新数据+旧 ID"的混搭——
* Sync0 是 preempt=0,critical.h 的 BASEPRI 方案挡不住它,故只能无锁协议。
*/
class EthercatCanBridge {
public:
/**
* @brief 处理一份输出 PDO:seq 变化时向 CAN 发一帧。
* @param[in] out kEcatPdoOutBytes 字节输出过程数据。
* @note 由 APPL_Application() 每周期调用。
*/
static void processOutput(const uint8_t *out);
/**
* @brief 填充输入 PDO:桥状态 + 电机状态窗口 + 长帧快照。
* @param[out] in kEcatPdoInBytes 字节输入过程数据(本函数负责全部写满)。
*/
static void buildInput(uint8_t *in);
/**
* @brief CAN 收帧投递入口。
* @note 由 TaskRunner::taskCanRxParse() 在分发链里调用,不消费帧(不返回
* "已处理"),只做旁路复制,因此不影响 OTA relay / 电机状态。
*/
static void onCanFrame(uint8_t channel, uint32_t id,
const uint8_t *data, uint8_t length);
/** @brief 桥接是否已收到过至少一帧(诊断用)。 */
static bool hasReceived() { return s_rxValid; }
private:
/**
* @brief 处理输出 PDO 的批量发送区(seq 沿内调用一次)。
* @return 实际受理的帧数。
* @note 逐帧解析 (8B 头 + dlc),边界不足即停 —— 不信任主站给的 count,
* 越界的 count 只会让循环提前结束,不会读到 PDO 之外。
*/
static uint8_t sendBatch(const uint8_t *out);
/** @brief 把接收环形队列里的帧尽量装进输入 PDO 批量区。 */
static void fillRxBatch(uint8_t *in);
static volatile uint8_t s_lastSeq; /**< 上一次已执行的发送序号 */
static volatile bool s_seqPrimed; /**< 是否已记录过基准 seq(防上电首帧误发) */
static volatile uint8_t s_lastTxResult;
static volatile uint16_t s_txCount;
static volatile uint16_t s_dropCount;
/** 主站请求的电机槽窗口起始下标(输出 PDO 的 WIN 字节,每周期刷新)。 */
static volatile uint8_t s_winBase;
/* 最近一帧接收快照:taskCanRxParse 写、PDO 读。
* s_xrxSeq 为 seqlock:奇数=正在写,buildInput 本周期跳过段 1/段 3。
* 没有它,Sync0_Isr 抢占在"数据已写完、id 未更新"之间会把新帧数据
* 配旧帧 ID 发出去(STATUS 被标成 CONFIG → 前端解出离谱目标值)。 */
static volatile bool s_rxValid;
static volatile uint32_t s_xrxSeq;
static volatile uint8_t s_rxCh;
static volatile uint8_t s_rxDlc;
static volatile bool s_rxTruncated;
static volatile uint32_t s_rxId;
static volatile uint32_t s_rxCount;
static uint8_t s_rxData[kEcatCanXRxDataMax];
/* ── 批量接收环形队列(1.5.56)──
* 单生产者(taskCanRxParse 的 onCanFrame) / 单消费者(PDO 的 fillRxBatch)。
* head/tail 各由一侧独占写入,故无需临界区:生产者只推进 head、
* 消费者只推进 tail,最坏情况是某周期少读一帧,下周期即取到。 */
struct RxRingItem {
uint32_t id;
uint8_t ch;
uint8_t dlc;
uint8_t data[kEcatCanFdDataMax];
};
static RxRingItem s_rxRing[kEcatRxRingSlots];
static volatile uint8_t s_rxRingHead; /**< 生产者写入位置 */
static volatile uint8_t s_rxRingTail; /**< 消费者读取位置 */
static volatile uint8_t s_rxRingLost; /**< 队列满导致的丢帧数(累计,回填后清零) */
};
extern "C" {
/** @brief 供 dc_app.c(C 文件)调用的 shim:处理输出 PDO。 */
void EcatCan_ProcessOutput(const uint8_t *out);
/** @brief 供 dc_app.c 调用的 shim:填充输入 PDO。 */
void EcatCan_BuildInput(uint8_t *in);
}
#endif /* COMM_ETHERCAT_CAN_BRIDGE_H */
+71
View File
@@ -0,0 +1,71 @@
/**
* @file ethercat_pdo.cpp
* @brief EtherCAT 过程数据(PDO)收发模块实现(1.5.55)。
*/
#include "comm/ethercat_pdo.h"
#include "comm/ethercat_can_bridge.h"
#include <cstring>
uint8_t EthercatPdo::s_out[EthercatPdo::kPdoOutBytes] = {0};
uint8_t EthercatPdo::s_in[EthercatPdo::kPdoInBytes] = {0};
volatile uint32_t EthercatPdo::s_outputCycles = 0U;
volatile uint32_t EthercatPdo::s_inputCycles = 0U;
void EthercatPdo::readOutputs(const uint8_t *esc)
{
if (esc == nullptr) {
return;
}
memcpy(s_out, esc, kPdoOutBytes);
++s_outputCycles;
}
void EthercatPdo::process()
{
/* PDO ↔ CAN 转发:输出 PDO 的 seq 字节变化沿触发一次 CAN 发送,
* 输入 PDO 回填桥状态 + 电机状态窗口(布局见 ethercat_can_bridge.h)。
*
* 顺序必须是先 processOutput 后 buildInput:buildInput 会把本周期刚执行的
* seq 回显成 ack,主站因此能在下一周期立刻确认送达;反过来则 ack 恒滞后一拍。
* processOutput 还负责取走主站请求的状态窗口下标,也必须在 buildInput 之前。 */
EthercatCanBridge::processOutput(s_out);
EthercatCanBridge::buildInput(s_in);
}
void EthercatPdo::writeInputs(uint8_t *esc)
{
if (esc == nullptr) {
return;
}
memcpy(esc, s_in, kPdoInBytes);
++s_inputCycles;
}
void EthercatPdo::snapshotOutput(uint8_t *dst)
{
if (dst == nullptr) {
return;
}
memcpy(dst, s_out, kPdoOutBytes);
}
extern "C" {
void EcatPdo_ReadOutputs(const uint8_t *esc)
{
EthercatPdo::readOutputs(esc);
}
void EcatPdo_Process(void)
{
EthercatPdo::process();
}
void EcatPdo_WriteInputs(uint8_t *esc)
{
EthercatPdo::writeInputs(esc);
}
} /* extern "C" */
+98
View File
@@ -0,0 +1,98 @@
/**
* @file ethercat_pdo.h
* @brief EtherCAT 过程数据(PDO)收发模块(1.5.54 由 dc_app.c 的散装 C 代码类化)。
*
* 一个 EtherCAT 周期在 SSC 里被拆成三个回调,本类按这三步各提供一个入口:
* 1. APPL_OutputMapping() → readOutputs() 从 ESC 取走主站下发的输出数据
* 2. APPL_Application() → process() 跑应用逻辑(当前是 PDO↔CAN 转发)
* 3. APPL_InputMapping() → writeInputs() 把输入数据交回 ESC(从站→主站)
*
* 缓冲区归本类所有,dc_app.c 只保留 SSC 对象字典(0x7000/0x6000 的 packed 结构体
* 是 SDO 存储,必须留在 C 侧)与三行 extern "C" 转发。这样"收发"这件事只有一个
* 归属:想加新的过程数据语义只改本类,不必再动 SSC 移植层。
*
* @note 1.5.55 起输出/输入**长度不再相等**(64B / 384B),为电机控制台腾出
* 状态窗口空间。长度常量统一由 ethercat_can_bridge.h 提供,避免三处各写
* 一个魔数后失配。
*
* @note 上下文:free-run 时三个回调都在主循环(ECAT_Main / ECAT_Application);
* 启用 DC 后 readOutputs/writeInputs 改由 Sync0_Isr(preempt=0) 调用,而
* process() 仍在主循环。因此 s_out/s_in 是跨上下文共享的——两者都只在
* 单一写者下被整块读写,且 CM4 上 memcpy 的撕裂最坏只会让某个周期读到
* 上/下半帧混合。对 seq 沿触发语义无害(seq 是单字节,要么旧要么新,
* 不会产生第三个值),故不加临界区,避免在 ISR 里关中断拖累抖动。
*/
#ifndef COMM_ETHERCAT_PDO_H
#define COMM_ETHERCAT_PDO_H
#include <cstdint>
#include "comm/ethercat_can_bridge.h"
/**
* @brief EtherCAT 过程数据收发。
* @note 全静态类,与 EthercatSlave / EthercatSync 同风格。
*/
class EthercatPdo {
public:
/** @brief 输出过程数据长度(主站→从站)。 */
static constexpr uint16_t kPdoOutBytes = kEcatPdoOutBytes;
/** @brief 输入过程数据长度(从站→主站)。 */
static constexpr uint16_t kPdoInBytes = kEcatPdoInBytes;
/**
* @brief 取走主站下发的输出过程数据(主站→从站)。
* @param[in] esc 指向 ESC 输出缓冲的 kPdoOutBytes 字节。
* @note 由 APPL_OutputMapping() 每周期调用;只做搬运不做业务,
* 业务在 process() 里跑,保证 ISR 上下文(DC 模式)足够短。
*/
static void readOutputs(const uint8_t *esc);
/**
* @brief 跑一个应用周期:消费输出、生成输入。
* @note 由 APPL_Application() 在主循环调用。
*/
static void process();
/**
* @brief 交回输入过程数据(从站→主站)。
* @param[out] esc 指向 ESC 输入缓冲的 kPdoInBytes 字节。
* @note 由 APPL_InputMapping() 每周期调用。
*/
static void writeInputs(uint8_t *esc);
/** @brief 累计取走输出的周期数(主站在发过程数据的证据)。 */
static uint32_t outputCycles() { return s_outputCycles; }
/** @brief 累计交回输入的周期数。 */
static uint32_t inputCycles() { return s_inputCycles; }
/** @brief 是否已开始过程数据交换(收到过至少一个输出周期)。 */
static bool isExchanging() { return s_outputCycles != 0U; }
/**
* @brief 拷出最近一份输出过程数据快照(诊断用)。
* @param[out] dst 至少 kPdoOutBytes 字节。
*/
static void snapshotOutput(uint8_t *dst);
private:
/** @brief 最近一份主站下发的输出数据。 */
static uint8_t s_out[kPdoOutBytes];
/** @brief 待交回主站的输入数据。 */
static uint8_t s_in[kPdoInBytes];
static volatile uint32_t s_outputCycles;
static volatile uint32_t s_inputCycles;
};
extern "C" {
/** @brief 供 dc_app.c(C 文件)调用:取走输出过程数据。 */
void EcatPdo_ReadOutputs(const uint8_t *esc);
/** @brief 供 dc_app.c 调用:跑一个应用周期。 */
void EcatPdo_Process(void);
/** @brief 供 dc_app.c 调用:交回输入过程数据。 */
void EcatPdo_WriteInputs(uint8_t *esc);
}
#endif /* COMM_ETHERCAT_PDO_H */
+66
View File
@@ -0,0 +1,66 @@
/**
* @file ethercat_pdo_size.h
* @brief EtherCAT 过程数据长度 —— C / C++ 共用的唯一真源(1.5.55)。
*
* 过程数据长度必须在**五处**保持一致,任一处失配的表现都是运行期 AL 错误
* (PREOP→SAFEOP 报 0x001A/0x0011),排查成本远高于编译期报错:
*
* 1. comm/ssc/port/ecat_def.h MAX_PD_OUTPUT_SIZE / MAX_PD_INPUT_SIZE(编译门限)
* 2. comm/ssc/port/dc_app.c 0x7000/0x6000 对象尺寸 + APPL_GenerateMapping
* 3. comm/ssc/port/gen_eeprom.py PD_OUT_SIZE / PD_IN_SIZE(SII,需重刷 EEPROM)
* 4. comm/ssc/port/DCU_ESI.xml SM 长度 + PDO 条目
* 5. comm/ethercat_can_bridge.h 桥接层字节布局
*
* 本头只放纯 #define,故 C(dc_app.c)与 C++(ethercat_can_bridge.h)都能包含,
* 双方各自用 _Static_assert / static_assert 绑到这两个宏上。改尺寸时先改这里,
* 编译器会把所有没跟上的地方报出来;EEPROM 侧(3/4)没法编译期检查,改完必须
* 重新生成 SII 并**断电冷启动**,ESC 只在冷启动时重新加载 EEPROM。
*/
#ifndef COMM_ETHERCAT_PDO_SIZE_H
#define COMM_ETHERCAT_PDO_SIZE_H
/**
* ── 1.5.56 扩到 DPRAM 硬上限(64/384 → 384/640)──
*
* 「一次能发多大」的真实约束不是 EtherCAT 协议(单帧可载 ~1486B 数据报),
* 而是 LAN9252 的 4KB DPRAM 加 3-buffer 同步管理器:
*
* 邮箱 SM0/SM1 各 256B 固定占 0x1000-0x13FF;
* 过程数据区只剩 0x1400-0x1FFF = 3072B;
* SM2/SM3 都是 3-buffer 模式(control 0x64/0x20,bit1=0),实占 3×size。
* ⇒ (out + in) × 3 ≤ 3072 ⇒ **out + in ≤ 1024B 是硬上限**。
*
* 384 + 640 = 1024,SM2 占 0x1400-0x187F、SM3 占 0x1880-0x1FFF,
* 恰好用满到最后一字节,没有任何浪费也没有越界。
*
* 其余曾疑似的天花板都已实测排除,都不是瓶颈:
* - SII/EEPROM:640B 输入需 80 个映射条目,SII 共用 1284/4096B(余 2812B);
* nEntry 是单字节字段,80 远低于 255 上限。
* - EXMC/HBI 搬运:满载 1024B ≈ 218us/周期(每 DWORD 约 0.85us),
* 4ms 周期只占 5.4%,甚至 1ms 周期也留有余量。
* - MAX_PD_*_SIZE 只影响 ecatappl.c 的 aPdOutputData/aPdInputData 静态数组,
* 共 1024B RAM,在 128KB SRAM 上无压力。
*
* 若将来还要更大,只有两条路(都有代价,非必要不动):
* a) 邮箱压到 MIN_MBX_SIZE(0x22) 级别 → 上限约 1280B,但会拖慢 SDO/FoE;
* b) 改 1-buffer 模式(control bit1=1)→ 上限 3072B,但失去三缓冲的
* 读写并发保护,主从同时访问同一缓冲会读到半新半旧的过程数据。
*/
/** 输出过程数据字节数(主站→从站,SM2):48×64bit。 */
#define ECAT_PDO_OUT_BYTES 384
/** 输入过程数据字节数(从站→主站,SM3):80×64bit。 */
#define ECAT_PDO_IN_BYTES 640
/** DPRAM 中过程数据区起址与 SM 缓冲数 —— 上限推导的依据,供静态断言使用。 */
#define ECAT_PD_AREA_START 0x1400
#define ECAT_PD_AREA_END 0x1FFF
#define ECAT_PD_SM_BUFFERS 3
#if (ECAT_PDO_OUT_BYTES + ECAT_PDO_IN_BYTES) * ECAT_PD_SM_BUFFERS > \
(ECAT_PD_AREA_END - ECAT_PD_AREA_START + 1)
#error "out+in 超出 LAN9252 过程数据区(3-buffer 模式下 (out+in)*3 必须 <= 3072)"
#endif
#endif /* COMM_ETHERCAT_PDO_SIZE_H */
+851 -5
View File
@@ -23,11 +23,13 @@
<!-- SM1: Mailbox Send (Slave -> Master) --> <!-- SM1: Mailbox Send (Slave -> Master) -->
<Sm DefaultSize="256" StartAddress="#x1200" ControlByte="#x22" Enable="1">MBoxIn</Sm> <Sm DefaultSize="256" StartAddress="#x1200" ControlByte="#x22" Enable="1">MBoxIn</Sm>
<!-- SM2: Process Data Output (RxPDO, Master -> Slave) --> <!-- SM2: Process Data Output (RxPDO, Master -> Slave) -->
<Sm StartAddress="#x1400" ControlByte="#x64" Enable="1">Outputs</Sm> <Sm DefaultSize="384" StartAddress="#x1400" ControlByte="#x64" Enable="1">Outputs</Sm>
<!-- SM3: Process Data Input (TxPDO, Slave -> Master) --> <!-- SM3: Process Data Input (TxPDO, Slave -> Master) -->
<Sm StartAddress="#x1480" ControlByte="#x20" Enable="1">Inputs</Sm> <Sm DefaultSize="640" StartAddress="#x1880" ControlByte="#x20" Enable="1">Inputs</Sm>
<!-- RxPDO (Output, Master -> Slave): 索引 0x1600-0x17FF,映射 0x7000:01..04 <!-- RxPDO (Output, Master -> Slave): 索引 0x1600-0x17FF,映射 0x7000:01..48
(1.5.27:与 dc_app.c 对齐,4×64bit;原 TxPdo/RxPdo 元素互换 + 1×256bit 均已修正) --> 1.5.56 扩到 48 条目(384B):兼容单帧区 48B + CANFD 批量发送区 336B。
与 dc_app.c 的 PDO_OUT_ENTRIES / gen_eeprom.py 的 PD_OUT_SIZE 必须一致,
否则主站按旧长度建映射,SAFEOP 报 AL 0x001A/0x0011。 -->
<RxPdo Fixed="1" Mandatory="1" Sm="2"> <RxPdo Fixed="1" Mandatory="1" Sm="2">
<Index>#x1600</Index> <Index>#x1600</Index>
<Name>Output</Name> <Name>Output</Name>
@@ -59,8 +61,320 @@
<Name>Control Data 4</Name> <Name>Control Data 4</Name>
<DataType>ULINT</DataType> <DataType>ULINT</DataType>
</Entry> </Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>5</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 5</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>6</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 6</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>7</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 7</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>8</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 8</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>9</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 9</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>10</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 10</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>11</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 11</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>12</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 12</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>13</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 13</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>14</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 14</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>15</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 15</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>16</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 16</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>17</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 17</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>18</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 18</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>19</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 19</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>20</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 20</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>21</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 21</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>22</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 22</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>23</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 23</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>24</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 24</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>25</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 25</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>26</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 26</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>27</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 27</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>28</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 28</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>29</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 29</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>30</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 30</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>31</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 31</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>32</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 32</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>33</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 33</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>34</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 34</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>35</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 35</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>36</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 36</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>37</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 37</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>38</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 38</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>39</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 39</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>40</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 40</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>41</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 41</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>42</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 42</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>43</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 43</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>44</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 44</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>45</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 45</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>46</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 46</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>47</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 47</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x7000</Index>
<SubIndex>48</SubIndex>
<BitLen>64</BitLen>
<Name>Control Data 48</Name>
<DataType>ULINT</DataType>
</Entry>
</RxPdo> </RxPdo>
<!-- TxPDO (Input, Slave -> Master): 索引 0x1A00-0x1BFF,映射 0x6000:01..04 --> <!-- TxPDO (Input, Slave -> Master): 索引 0x1A00-0x1BFF,映射 0x6000:01..80
1.5.56 扩到 80 条目(640B):电机状态窗口 + CANFD 批量接收区 280B。
SM3 起址随之挪到 #x1880 —— SM2 是 3-buffer 模式,384B×3 占到 #x187F。
(384+640)×3 = 3072 = 过程数据区 0x1400-0x1FFF 全长,恰好用满。
撞址会被 SSC ecatslv.c 重叠检查拦下,报 INVALIDSMOUTCFG。 -->
<TxPdo Fixed="1" Mandatory="1" Sm="3"> <TxPdo Fixed="1" Mandatory="1" Sm="3">
<Index>#x1A00</Index> <Index>#x1A00</Index>
<Name>Input</Name> <Name>Input</Name>
@@ -92,6 +406,538 @@
<Name>Status Data 4</Name> <Name>Status Data 4</Name>
<DataType>ULINT</DataType> <DataType>ULINT</DataType>
</Entry> </Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>5</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 5</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>6</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 6</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>7</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 7</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>8</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 8</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>9</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 9</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>10</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 10</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>11</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 11</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>12</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 12</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>13</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 13</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>14</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 14</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>15</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 15</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>16</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 16</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>17</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 17</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>18</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 18</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>19</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 19</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>20</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 20</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>21</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 21</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>22</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 22</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>23</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 23</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>24</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 24</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>25</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 25</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>26</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 26</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>27</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 27</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>28</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 28</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>29</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 29</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>30</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 30</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>31</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 31</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>32</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 32</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>33</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 33</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>34</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 34</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>35</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 35</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>36</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 36</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>37</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 37</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>38</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 38</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>39</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 39</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>40</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 40</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>41</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 41</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>42</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 42</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>43</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 43</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>44</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 44</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>45</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 45</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>46</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 46</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>47</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 47</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>48</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 48</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>49</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 49</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>50</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 50</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>51</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 51</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>52</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 52</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>53</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 53</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>54</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 54</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>55</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 55</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>56</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 56</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>57</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 57</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>58</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 58</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>59</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 59</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>60</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 60</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>61</SubIndex>
<BitLen>64</BitLen>
<Name>Status Data 61</Name>
<DataType>ULINT</DataType>
</Entry>
<Entry>
<Index>#x6000</Index>
<SubIndex>62</SubIndex>
<BitLen>64</BitLen>
<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> </TxPdo>
<Mailbox DataLinkLayer="true"> <Mailbox DataLinkLayer="true">
<CoE SdoInfo="true" CompleteAccess="true" SegmentedSdo="true"/> <CoE SdoInfo="true" CompleteAccess="true" SegmentedSdo="true"/>
+233 -59
View File
@@ -4,12 +4,17 @@
* *
* 对象字典结构 (匹配 DCU_ESI.xml / gen_eeprom.py): * 对象字典结构 (匹配 DCU_ESI.xml / gen_eeprom.py):
* 0x1C12 RxPDO Assign → 0x1600 * 0x1C12 RxPDO Assign → 0x1600
* 0x1600 RxPDO Mapping → 0x7000:01..04 (4×64bit = 32 bytes) * 0x1600 RxPDO Mapping → 0x7000:01..08 (8×64bit = 64 bytes)
* 0x7000 Output Data (主站→从站, 32 bytes) * 0x7000 Output Data (主站→从站, 64 bytes)
* 0x1C13 TxPDO Assign → 0x1A00 * 0x1C13 TxPDO Assign → 0x1A00
* 0x1A00 TxPDO Mapping → 0x6000:01..04 (4×64bit = 32 bytes) * 0x1A00 TxPDO Mapping → 0x6000:01..48 (48×64bit = 384 bytes)
* 0x6000 Input Data (从站→主站, 32 bytes) * 0x6000 Input Data (从站→主站, 384 bytes)
* 0x2000 SYNC Diag (只读: sync0/sync1/escirq/uptime, 主站 SDO 轮询看 DC 通路) * 0x2000 SYNC Diag (只读: sync0/sync1/escirq/uptime, 主站 SDO 轮询看 DC 通路)
*
* 1.5.55:为电机控制台把过程数据从 32B/32B 扩到 64B/384B。
* 0x7000/0x6000/0x1600/0x1A00 由 OBJCODE_REC 改为 OBJCODE_ARR —— 关键原因见
* 下方 Entry Descriptions 注释:REC 会逐下标索引 pEntryDesc[Subindex],48 个
* 条目就得写 49 项描述符;ARR 由 OBJ_GetEntryDesc 复用 pEntryDesc[1],只需 2 项。
*/ */
#include "ecat_def.h" #include "ecat_def.h"
@@ -18,9 +23,11 @@
#include "ecatappl.h" #include "ecatappl.h"
#include "objdef.h" #include "objdef.h"
#include "dc_app.h" #include "dc_app.h"
#include "../../ethercat_pdo_size.h" /* ECAT_PDO_OUT_BYTES / ECAT_PDO_IN_BYTES */
#include <string.h> #include <string.h>
#include <stddef.h> /* offsetof:下方布局守卫断言使用 */ #include <stddef.h> /* offsetof:下方布局守卫断言使用 */
#include <stdio.h> /* printf:0x204 守护 WARN 日志(1.5.48) */
/*--------------------------------------------------------------------------- /*---------------------------------------------------------------------------
* el9800appl.h 桩 include (SSC 内部需要) * el9800appl.h 桩 include (SSC 内部需要)
@@ -31,31 +38,49 @@
/*--------------------------------------------------------------------------- /*---------------------------------------------------------------------------
* PDO 数据缓冲 * PDO 数据缓冲
*
* 1.5.56 扩到 DPRAM 硬上限:输出 48×64bit=384B,输入 80×64bit=640B(非对称)。
* 长度的唯一真源是 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 */
/** 输出数据对象 0x7000 (主站→从站, RxPDO) */ /** 输出数据对象 0x7000 (主站→从站, RxPDO) */
typedef struct OBJ_STRUCT_PACKED_START { typedef struct OBJ_STRUCT_PACKED_START {
UINT16 u16SubIndex0; UINT16 u16SubIndex0;
UINT64 u64Data[4]; /* 4×8 = 32 bytes */ UINT64 u64Data[PDO_OUT_ENTRIES];
} OBJ_STRUCT_PACKED_END } OBJ_STRUCT_PACKED_END
TOBJ7000; TOBJ7000;
/** 输入数据对象 0x6000 (从站→主站, TxPDO) */ /** 输入数据对象 0x6000 (从站→主站, TxPDO) */
typedef struct OBJ_STRUCT_PACKED_START { typedef struct OBJ_STRUCT_PACKED_START {
UINT16 u16SubIndex0; UINT16 u16SubIndex0;
UINT64 u64Data[4]; /* 4×8 = 32 bytes */ UINT64 u64Data[PDO_IN_ENTRIES];
} OBJ_STRUCT_PACKED_END } OBJ_STRUCT_PACKED_END
TOBJ6000; TOBJ6000;
static TOBJ7000 s_outputObj = {4, {0, 0, 0, 0}}; static TOBJ7000 s_outputObj = {PDO_OUT_ENTRIES, {0}};
static TOBJ6000 s_inputObj = {4, {0, 0, 0, 0}}; static TOBJ6000 s_inputObj = {PDO_IN_ENTRIES, {0}};
/* 布局守卫(1.5.42):SSC 的 SDO 单条目访问按"si0 占 16bit,其后按 BitLength 紧排" /* 布局守卫(1.5.42):SSC 的 SDO 单条目访问按"si0 占 16bit,其后按 BitLength 紧排"
* 计算偏移(OBJ_GetEntryOffset),故 UINT64 数组必须紧跟在 UINT16 之后落在字节 2。 * 计算偏移(OBJ_GetEntryOffset),故 UINT64 数组必须紧跟在 UINT16 之后落在字节 2。
* OBJ_STRUCT_PACKED_START 曾被留空导致其落在字节 8、SDO 读写错位,加断言防回归。 */ * OBJ_STRUCT_PACKED_START 曾被留空导致其落在字节 8、SDO 读写错位,加断言防回归。 */
_Static_assert(offsetof(TOBJ7000, u64Data) == 2, "TOBJ7000 未紧排:检查 OBJ_STRUCT_PACKED_START"); _Static_assert(offsetof(TOBJ7000, u64Data) == 2, "TOBJ7000 未紧排:检查 OBJ_STRUCT_PACKED_START");
_Static_assert(offsetof(TOBJ6000, u64Data) == 2, "TOBJ6000 未紧排:检查 OBJ_STRUCT_PACKED_START"); _Static_assert(offsetof(TOBJ6000, u64Data) == 2, "TOBJ6000 未紧排:检查 OBJ_STRUCT_PACKED_START");
_Static_assert(sizeof(TOBJ7000) == 34, "TOBJ7000 尺寸应为 2+32"); _Static_assert(sizeof(TOBJ7000) == 2 + 8 * PDO_OUT_ENTRIES, "TOBJ7000 尺寸应为 2+384");
_Static_assert(sizeof(TOBJ6000) == 2 + 8 * PDO_IN_ENTRIES, "TOBJ6000 尺寸应为 2+640");
/* 与 C++ 侧桥接层的长度约定绑定:任一处改了尺寸而忘了另一处,这里直接编译失败,
* 而不是等到运行期 SAFEOP 报 AL 0x001A/0x0011 再回头查。 */
_Static_assert(8 * PDO_OUT_ENTRIES == ECAT_PDO_OUT_BYTES, "输出 PDO 长度与桥接层不一致");
_Static_assert(8 * PDO_IN_ENTRIES == ECAT_PDO_IN_BYTES, "输入 PDO 长度与桥接层不一致");
/* SSC 编译期门限(ecat_def.h)必须容得下:否则 ecatslv.c 在 SM 校验时拒绝进 SAFEOP。 */
_Static_assert(8 * PDO_OUT_ENTRIES <= MAX_PD_OUTPUT_SIZE, "MAX_PD_OUTPUT_SIZE 太小");
_Static_assert(8 * PDO_IN_ENTRIES <= MAX_PD_INPUT_SIZE, "MAX_PD_INPUT_SIZE 太小");
/*--------------------------------------------------------------------------- /*---------------------------------------------------------------------------
* SYNC 诊断对象 0x2000 (只读, 主站 SDO 轮询) * SYNC 诊断对象 0x2000 (只读, 主站 SDO 轮询)
@@ -83,6 +108,13 @@ extern UINT32 EcatDbg_GetSync1(void);
extern UINT32 EcatDbg_GetEscIrq(void); extern UINT32 EcatDbg_GetEscIrq(void);
extern UINT32 EcatDbg_GetUptimeSec(void); extern UINT32 EcatDbg_GetUptimeSec(void);
/* 过程数据收发模块 (comm/ethercat_pdo.cpp 的 extern "C" shim, 1.5.54)。
* 1.5.53 时这里直接调 EcatCan_* 转发桥;1.5.54 把"一个 EtherCAT 周期怎么收发"
* 收拢进 EthercatPdo 类,本文件退回纯 SSC 移植层:只留对象字典 + 三行转发。 */
extern void EcatPdo_ReadOutputs(const UINT8 *esc);
extern void EcatPdo_Process(void);
extern void EcatPdo_WriteInputs(UINT8 *esc);
/*--------------------------------------------------------------------------- /*---------------------------------------------------------------------------
* PDO Assign 对象 (0x1C12 / 0x1C13) * PDO Assign 对象 (0x1C12 / 0x1C13)
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
@@ -98,30 +130,49 @@ static TOBJ_PDO_ASSIGN sTxPDOassign = {1, {0x1A00}};
/*--------------------------------------------------------------------------- /*---------------------------------------------------------------------------
* PDO Mapping 对象 (0x1600 / 0x1A00) * PDO Mapping 对象 (0x1600 / 0x1A00)
*
* 映射条目值 = Index<<16 | SubIndex<<8 | BitLen,这里 BitLen 恒为 0x40(64bit)。
* 48 个条目手写既冗长又容易错位,改用宏按 8 个一组展开。
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
#define PDO_MAP_ENTRY(idx, sub) (((UINT32)(idx) << 16) | ((UINT32)(sub) << 8) | 0x40u)
/* 一组 8 个:sub = base+1 .. base+8 */
#define PDO_MAP_8(idx, base) \
PDO_MAP_ENTRY(idx, (base) + 1), PDO_MAP_ENTRY(idx, (base) + 2), \
PDO_MAP_ENTRY(idx, (base) + 3), PDO_MAP_ENTRY(idx, (base) + 4), \
PDO_MAP_ENTRY(idx, (base) + 5), PDO_MAP_ENTRY(idx, (base) + 6), \
PDO_MAP_ENTRY(idx, (base) + 7), PDO_MAP_ENTRY(idx, (base) + 8)
typedef struct OBJ_STRUCT_PACKED_START { typedef struct OBJ_STRUCT_PACKED_START {
UINT16 u16SubIndex0; UINT16 u16SubIndex0;
UINT32 aEntries[4]; UINT32 aEntries[PDO_OUT_ENTRIES];
} OBJ_STRUCT_PACKED_END } OBJ_STRUCT_PACKED_END
TOBJ_PDO_MAPPING; TOBJ_RXPDO_MAPPING;
/* 0x1600: 映射 0x7000:01..04, 每个 64 bits */ typedef struct OBJ_STRUCT_PACKED_START {
static TOBJ_PDO_MAPPING sRxPDOMap = {4, { UINT16 u16SubIndex0;
0x70000140, /* Index=0x7000, SubIndex=0x01, BitLen=64 */ UINT32 aEntries[PDO_IN_ENTRIES];
0x70000240, /* Index=0x7000, SubIndex=0x02, BitLen=64 */ } OBJ_STRUCT_PACKED_END
0x70000340, /* Index=0x7000, SubIndex=0x03, BitLen=64 */ TOBJ_TXPDO_MAPPING;
0x70000440 /* Index=0x7000, SubIndex=0x04, BitLen=64 */
/* 0x1600: 映射 0x7000:01..48, 每个 64 bits */
static TOBJ_RXPDO_MAPPING sRxPDOMap = {PDO_OUT_ENTRIES, {
PDO_MAP_8(0x7000, 0), PDO_MAP_8(0x7000, 8), PDO_MAP_8(0x7000, 16),
PDO_MAP_8(0x7000, 24), PDO_MAP_8(0x7000, 32), PDO_MAP_8(0x7000, 40)
}}; }};
/* 0x1A00: 映射 0x6000:01..04, 每个 64 bits */ /* 0x1A00: 映射 0x6000:01..80, 每个 64 bits */
static TOBJ_PDO_MAPPING sTxPDOMap = {4, { static TOBJ_TXPDO_MAPPING sTxPDOMap = {PDO_IN_ENTRIES, {
0x60000140, /* Index=0x6000, SubIndex=0x01, BitLen=64 */ PDO_MAP_8(0x6000, 0), PDO_MAP_8(0x6000, 8), PDO_MAP_8(0x6000, 16),
0x60000240, /* Index=0x6000, SubIndex=0x02, BitLen=64 */ PDO_MAP_8(0x6000, 24), PDO_MAP_8(0x6000, 32), PDO_MAP_8(0x6000, 40),
0x60000340, /* Index=0x6000, SubIndex=0x03, BitLen=64 */ PDO_MAP_8(0x6000, 48), PDO_MAP_8(0x6000, 56), PDO_MAP_8(0x6000, 64),
0x60000440 /* Index=0x6000, SubIndex=0x04, BitLen=64 */ PDO_MAP_8(0x6000, 72)
}}; }};
_Static_assert(sizeof(sRxPDOMap.aEntries) / 4 == PDO_OUT_ENTRIES, "0x1600 条目数不符");
_Static_assert(sizeof(sTxPDOMap.aEntries) / 4 == PDO_IN_ENTRIES, "0x1A00 条目数不符");
/*--------------------------------------------------------------------------- /*---------------------------------------------------------------------------
* Entry Descriptions * Entry Descriptions
* *
@@ -131,7 +182,11 @@ static TOBJ_PDO_MAPPING sTxPDOMap = {4, {
* 相邻常量数组的垃圾, 主站 config_map 报 "Unsupported access to an object" / * 相邻常量数组的垃圾, 主站 config_map 报 "Unsupported access to an object" /
* "Attempt to read to a write only object"(实测 0x1600/0x1A00 的 sub0/3/4 报错 * "Attempt to read to a write only object"(实测 0x1600/0x1A00 的 sub0/3/4 报错
* 而 sub1/2 侥幸通过, 正是越界特征)。 * 而 sub1/2 侥幸通过, 正是越界特征)。
* 另: 可被 PDO 映射的数据子条目必须带 OBJACCESS_RxPDOMAPPING/TxPDOMAPPING, *
* 1.5.55: 输入 PDO 扩到 48 条目后, 继续用 REC 就得写 49 项描述符(且 0x1600/0x1A00
* 与 0x7000/0x6000 条目数不同, 不能再共用一个数组)。这些条目类型完全一致, 正是
* ARR 的语义, 故统一改为 OBJCODE_ARR —— 描述数组固定 2 项, 由 SSC 复用 [1]。
* 注意: 可被 PDO 映射的数据子条目必须带 OBJACCESS_RxPDOMAPPING/TxPDOMAPPING,
* 否则主站校验 0x1600/0x1A00 的映射目标时会拒绝(官方 sampleappl.h 同此)。 * 否则主站校验 0x1600/0x1A00 的映射目标时会拒绝(官方 sampleappl.h 同此)。
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
@@ -142,30 +197,21 @@ OBJCONST TSDOINFOENTRYDESC OBJMEM asPDOAssignEntryDesc[] = {
{DEFTYPE_UNSIGNED16, 0x10, ACCESS_READ} {DEFTYPE_UNSIGNED16, 0x10, ACCESS_READ}
}; };
/* PDO Mapping (0x1600/0x1A00): REC 含 4 个映射条目 -> 必须给 Sub0..Sub4 共 5 项。 */ /* PDO Mapping (0x1600/0x1A00): ARR of UINT32 映射条目, 2 项即可。 */
OBJCONST TSDOINFOENTRYDESC OBJMEM asPDOMappingEntryDesc[] = { OBJCONST TSDOINFOENTRYDESC OBJMEM asPDOMappingEntryDesc[] = {
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, /* Sub0: 条目数 */ {DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, /* Sub0: 条目数 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* Sub1 */ {DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ} /* Sub1..n: 通用项 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* Sub2 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ}, /* Sub3 */
{DEFTYPE_UNSIGNED32, 0x20, ACCESS_READ} /* Sub4 */
}; };
/* 0x7000 Output Data: REC, Sub0..Sub4 共 5 项; 子条目可被 RxPDO 映射。 */ /* 0x7000 Output Data: ARR of UINT64; 子条目可被 RxPDO 映射。 */
OBJCONST TSDOINFOENTRYDESC OBJMEM asOutputEntryDesc[] = { OBJCONST TSDOINFOENTRYDESC OBJMEM asOutputEntryDesc[] = {
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, {DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ},
{DEFTYPE_UNSIGNED64, 0x40, ACCESS_READWRITE | OBJACCESS_RXPDOMAPPING},
{DEFTYPE_UNSIGNED64, 0x40, ACCESS_READWRITE | OBJACCESS_RXPDOMAPPING},
{DEFTYPE_UNSIGNED64, 0x40, ACCESS_READWRITE | OBJACCESS_RXPDOMAPPING},
{DEFTYPE_UNSIGNED64, 0x40, ACCESS_READWRITE | OBJACCESS_RXPDOMAPPING} {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READWRITE | OBJACCESS_RXPDOMAPPING}
}; };
/* 0x6000 Input Data: REC, Sub0..Sub4 共 5 项; 子条目可被 TxPDO 映射, 输入侧只读。 */ /* 0x6000 Input Data: ARR of UINT64; 子条目可被 TxPDO 映射, 输入侧只读。 */
OBJCONST TSDOINFOENTRYDESC OBJMEM asInputEntryDesc[] = { OBJCONST TSDOINFOENTRYDESC OBJMEM asInputEntryDesc[] = {
{DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ}, {DEFTYPE_UNSIGNED8, 0x08, ACCESS_READ},
{DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING},
{DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING},
{DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING},
{DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING} {DEFTYPE_UNSIGNED64, 0x40, ACCESS_READ | OBJACCESS_TXPDOMAPPING}
}; };
@@ -199,24 +245,24 @@ TOBJECT OBJMEM ApplicationObjDicList[] = {
{NULL, NULL, 0x1C12, {DEFTYPE_UNSIGNED16, 1 | (OBJCODE_ARR << 8)}, {NULL, NULL, 0x1C12, {DEFTYPE_UNSIGNED16, 1 | (OBJCODE_ARR << 8)},
asPDOAssignEntryDesc, aName0x1C12, &sRxPDOassign, NULL, NULL, 0x0000}, asPDOAssignEntryDesc, aName0x1C12, &sRxPDOassign, NULL, NULL, 0x0000},
/* 0x1600: RxPDO Mapping (REC, 4 条目 -> 描述数组必须 5 项) */ /* 0x1600: RxPDO Mapping (ARR of UINT32, PDO_OUT_ENTRIES 个映射条目) */
{NULL, NULL, 0x1600, {DEFTYPE_PDOMAPPING, 4 | (OBJCODE_REC << 8)}, {NULL, NULL, 0x1600, {DEFTYPE_PDOMAPPING, PDO_OUT_ENTRIES | (OBJCODE_ARR << 8)},
asPDOMappingEntryDesc, aName0x1600, &sRxPDOMap, NULL, NULL, 0x0000}, asPDOMappingEntryDesc, aName0x1600, &sRxPDOMap, NULL, NULL, 0x0000},
/* 0x7000: Output Data (主站→从站) */ /* 0x7000: Output Data (主站→从站, ARR of UINT64) */
{NULL, NULL, 0x7000, {DEFTYPE_RECORD, 4 | (OBJCODE_REC << 8)}, {NULL, NULL, 0x7000, {DEFTYPE_UNSIGNED64, PDO_OUT_ENTRIES | (OBJCODE_ARR << 8)},
asOutputEntryDesc, aName0x7000, &s_outputObj, NULL, NULL, 0x0000}, asOutputEntryDesc, aName0x7000, &s_outputObj, NULL, NULL, 0x0000},
/* 0x1C13: TxPDO Assign */ /* 0x1C13: TxPDO Assign */
{NULL, NULL, 0x1C13, {DEFTYPE_UNSIGNED16, 1 | (OBJCODE_ARR << 8)}, {NULL, NULL, 0x1C13, {DEFTYPE_UNSIGNED16, 1 | (OBJCODE_ARR << 8)},
asPDOAssignEntryDesc, aName0x1C13, &sTxPDOassign, NULL, NULL, 0x0000}, asPDOAssignEntryDesc, aName0x1C13, &sTxPDOassign, NULL, NULL, 0x0000},
/* 0x1A00: TxPDO Mapping (REC, 4 条目 -> 描述数组必须 5 项) */ /* 0x1A00: TxPDO Mapping (ARR of UINT32, PDO_IN_ENTRIES 个映射条目) */
{NULL, NULL, 0x1A00, {DEFTYPE_PDOMAPPING, 4 | (OBJCODE_REC << 8)}, {NULL, NULL, 0x1A00, {DEFTYPE_PDOMAPPING, PDO_IN_ENTRIES | (OBJCODE_ARR << 8)},
asPDOMappingEntryDesc, aName0x1A00, &sTxPDOMap, NULL, NULL, 0x0000}, asPDOMappingEntryDesc, aName0x1A00, &sTxPDOMap, NULL, NULL, 0x0000},
/* 0x6000: Input Data (从站→主站) */ /* 0x6000: Input Data (从站→主站, ARR of UINT64) */
{NULL, NULL, 0x6000, {DEFTYPE_RECORD, 4 | (OBJCODE_REC << 8)}, {NULL, NULL, 0x6000, {DEFTYPE_UNSIGNED64, PDO_IN_ENTRIES | (OBJCODE_ARR << 8)},
asInputEntryDesc, aName0x6000, &s_inputObj, NULL, NULL, 0x0000}, asInputEntryDesc, aName0x6000, &s_inputObj, NULL, NULL, 0x0000},
/* 0x2000: SYNC 诊断 (只读, 主站 SDO 轮询验证 DC 通路) */ /* 0x2000: SYNC 诊断 (只读, 主站 SDO 轮询验证 DC 通路) */
@@ -234,8 +280,8 @@ TOBJECT OBJMEM * VARMEM ApplicationObjDic = ApplicationObjDicList;
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
UINT16 APPL_GenerateMapping(UINT16 *pInputSize, UINT16 *pOutputSize) UINT16 APPL_GenerateMapping(UINT16 *pInputSize, UINT16 *pOutputSize)
{ {
*pInputSize = 32; /* 4×UINT64 = 32 bytes */ *pInputSize = ECAT_PDO_IN_BYTES; /* 80×UINT64 = 640 bytes */
*pOutputSize = 32; *pOutputSize = ECAT_PDO_OUT_BYTES; /* 48×UINT64 = 384 bytes */
return 0; return 0;
} }
@@ -250,7 +296,19 @@ UINT16 APPL_StopMailboxHandler(void) { return 0; }
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
UINT16 APPL_StartInputHandler(UINT16 *pIntMask) UINT16 APPL_StartInputHandler(UINT16 *pIntMask)
{ {
*pIntMask = 0x0800U; /* SM3 AL Event */ /* 1.5.44:此处**不能**覆写 *pIntMask。
*
* pIntMask 指向 SSC 全局 u16ALEventMask,StartInputHandler() 已按同步模式算好:
* free-run + 有输出 PDO 时它是 PROCESS_OUTPUT_EVENT(0x0400)(ecatslv.c:959/1090),
* 即 SM2 收到输出这一事件才是 SM-Synchron 模式的同步节拍。
* 旧代码写死 0x0800(PROCESS_INPUT_EVENT) 把 0x0400 顶掉了,而随后的
* SetALEventMask() 只做“或”,于是寄存器 0x204 永远缺 SM2 位:
* 输出到达不再产生 IRQ,PDI_Isr 只能被主站“读输入”事件带起来,
* 输入刷新因此永远落在主站读之后 —— SM3 是 3-buffer 缓冲模式,
* 主站读到未被从站重写的缓冲不累加 WKC,表现为几乎每周期 wkc < expected_wkc。
*
* 官方 sampleappl.c / el9800appl.c 同样只 return,不碰 pIntMask。 */
(void)pIntMask;
return 0; return 0;
} }
@@ -283,7 +341,11 @@ void APPL_InputMapping(UINT16 *pInputData)
s_syncDiag.u32EscIrq = EcatDbg_GetEscIrq(); s_syncDiag.u32EscIrq = EcatDbg_GetEscIrq();
s_syncDiag.u32UptimeS = EcatDbg_GetUptimeSec(); s_syncDiag.u32UptimeS = EcatDbg_GetUptimeSec();
memcpy(pInputData, &s_inputObj.u64Data[0], 32); /* 1.5.54:先让 EthercatPdo 把本周期的输入数据写进 0x6000 的对象存储,
* 再交给 ESC。经由对象存储中转(而非直接写 pInputData)是为了让主站
* SDO 读 0x6000 时看到与过程数据一致的内容。 */
EcatPdo_WriteInputs((UINT8 *)&s_inputObj.u64Data[0]);
memcpy(pInputData, &s_inputObj.u64Data[0], ECAT_PDO_IN_BYTES);
} }
/*--------------------------------------------------------------------------- /*---------------------------------------------------------------------------
@@ -291,7 +353,8 @@ void APPL_InputMapping(UINT16 *pInputData)
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
void APPL_OutputMapping(UINT16 *pOutputData) void APPL_OutputMapping(UINT16 *pOutputData)
{ {
memcpy(&s_outputObj.u64Data[0], pOutputData, 32); memcpy(&s_outputObj.u64Data[0], pOutputData, ECAT_PDO_OUT_BYTES);
EcatPdo_ReadOutputs((const UINT8 *)&s_outputObj.u64Data[0]);
} }
/*--------------------------------------------------------------------------- /*---------------------------------------------------------------------------
@@ -299,15 +362,12 @@ void APPL_OutputMapping(UINT16 *pOutputData)
*---------------------------------------------------------------------------*/ *---------------------------------------------------------------------------*/
void APPL_Application(void) void APPL_Application(void)
{ {
/* Phase 2: 回环测试 — 将输出数据原样回填到输入数据。 /* 1.5.54:应用逻辑全部搬进 EthercatPdo::process()(当前实现是 PDO↔CAN 转发)。
* TwinCAT 主站写 0x7000 → ESC → APPL_OutputMapping → s_outputObj *
* APPL_Application: s_inputObj = s_outputObj (回环) * 原 Phase 2 的 32B 回环已被取代:回环的验证价值由输入 PDO 的 seq 回显
* APPL_InputMapping → ESC → 主站读 0x6000 * (ECAT_CAN_RX_OFF_ACK)承担——主站写 seq=N 后应在下一周期读回 ack=N,
* 主站可验证:写入的值 == 读回的值。 */ * 同样能证明 输出→应用→输入 整条过程数据链路通。 */
s_inputObj.u64Data[0] = s_outputObj.u64Data[0]; EcatPdo_Process();
s_inputObj.u64Data[1] = s_outputObj.u64Data[1];
s_inputObj.u64Data[2] = s_outputObj.u64Data[2];
s_inputObj.u64Data[3] = s_outputObj.u64Data[3];
} }
/*--------------------------------------------------------------------------- /*---------------------------------------------------------------------------
@@ -322,3 +382,117 @@ UINT32 EcatDbg_ReadAlDiag(void)
alStatus = SWAPWORD(alStatus); alStatus = SWAPWORD(alStatus);
return ((UINT32)alStatus << 16) | (UINT32)SWAPWORD(HW_GetALEventRegister()); return ((UINT32)alStatus << 16) | (UINT32)SWAPWORD(HW_GetALEventRegister());
} }
/*---------------------------------------------------------------------------
* AL Event Mask (0x204) 守护 + SM2 事件楔死自愈 — 1.5.48 新增, 1.5.49 扩展
* (C 链接, task_runner 1s 节流调用)
*
* 背景:实测"停止回 INIT 后重进 OP"概率性失败——主站 LRW WKC=3/3(输出
* 确实写进 SM2),但从站 9s ESM 超时报 0x001B:bEcatFirstOutputsReceived 一直
* 没被置位。该标志只在 PDI_Isr 收到 SM2 事件(AL Event 0x220 bit10)时置位。
* 01:03 串口日志钉死现场:evt=0x0400 在 0x220 挂起 10s 无人处理(PDI_Isr 的
* SM2 分支从未执行),escirq 2.6/s 证明 EXTI 只在 AL Control 事件时跳动——
* 即 PDI_Isr 在跑但 SM2 分支被某个内部旗标挡住(bEscIntEnabled 门控)。
*
* 守护(1)0x204 缺位自愈:从站 AL>=SAFEOP 且有输出 PDO 时,SSC 不变式是
* "0x204 ⊇ u16ALEventMask"。读实际值比对,缺位则补写并打 WARN。
*
* 自愈(2)SM2 事件楔死(1.5.49):SAFEOP 下 0x220 bit10 挂起 且 标志未置位
* = PDI_Isr 通路楔死。从主循环直接完成 PDI_Isr 该做的事:置位
* bEcatFirstOutputsReceived + 读 SM2 首/末字应答事件(ecatappl.c:317-320 同款)。
* 协议等价:bit10 挂起 = 主站已写过 SM2 = "收到输出"成立;AL_ControlRes 下一个
* MainLoop 周期即完成 SAFEOP→OP。WARN 附带全部内部旗标(bEscInt/bDcSync/
* 期望与实际 mask/看门狗状态),下次复现一行日志即可定位楔死路径。
*---------------------------------------------------------------------------*/
UINT32 EcatDbg_GuardAleventMask(void)
{
/* SSC 全局(ecatslv.c 定义;除 u16ALEventMask 外均已由 ecatslv.h PROTO 声明) */
extern UINT16 u16ALEventMask;
extern BOOL bMbxRunning; /* mailbox.c 定义(dc_app.c 未包含 mailbox.h,局部声明) */
UINT16 evt = 0U;
UINT16 actual = 0U;
UINT16 wdState = 0U;
UINT16 dummy = 0U;
const UINT8 al = (UINT8)(nAlStatus & 0x0FU);
if (al < 2U /* STATE_PREOP */) {
return 0xFFFFU; /* 状态不适用:返回哨兵值,调用方不打印 */
}
/* (0) 邮箱 SM 地址/长度失配自愈(2026-09-03 03:00 根治,1.5.52):
*
* 主站侧探针(楔死态)实锤的证据链:
* - 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 迁移,主循环上下文)被写,
* 守护同样在主循环——无并发风险。 */
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)) {
printf("[ECAT][MBX] WARN: SM addr/len mismatch, SSC rx=0x%04X/%u tx=0x%04X/%u "
"vs reg SM0=0x%04X/%u SM1=0x%04X/%u -> restored\r\n",
(unsigned)u16EscAddrReceiveMbx, (unsigned)u16ReceiveMbxSize,
(unsigned)u16EscAddrSendMbx, (unsigned)u16SendMbxSize,
(unsigned)sm0Start, (unsigned)sm0Len,
(unsigned)sm1Start, (unsigned)sm1Len);
u16EscAddrReceiveMbx = sm0Start;
u16ReceiveMbxSize = sm0Len;
u16EscAddrSendMbx = sm1Start;
u16SendMbxSize = sm1Len;
}
}
if (al < 4U /* STATE_SAFEOP */ || nPdOutputSize == 0U) {
return 0xFFFFU; /* 以下仅 SAFEOP+ 适用 */
}
evt = SWAPWORD(HW_GetALEventRegister());
HW_EscReadWord(actual, ESC_AL_EVENTMASK_OFFSET);
actual = SWAPWORD(actual);
/* (1) 0x204 缺位自愈 */
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);
}
/* (2) SM2 事件楔死自愈:仅 SAFEOP + 标志未置位 + bit10 挂起时触发
* (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));
printf("[ECAT][AEM] WARN: SM2 event stuck evt=0x%04X, PDI_Isr path wedged "
"(bEscInt=%u bDcSync=%u mask_exp=0x%04X mask_act=0x%04X wd=0x%04X) "
"-> flag set + event acked\r\n",
(unsigned)evt,
(unsigned)(bEscIntEnabled ? 1U : 0U),
(unsigned)(bDcSyncActive ? 1U : 0U),
(unsigned)u16ALEventMask, (unsigned)actual, (unsigned)wdState);
}
return actual;
}
+35 -7
View File
@@ -143,9 +143,25 @@
#define DC_SUPPORTED 1 #define DC_SUPPORTED 1
#endif #endif
/** 看门狗由定时器中断检查。 */ /** 看门狗由定时器中断检查。
* 1.5.47:1→0。ECAT_TIMER_INT=1 要求端口提供调用 ECAT_CheckTimer() 的 1ms 硬件
* 定时器 ISR——本工程从未实现,导致 ECAT_CheckTimer 在整个固件里零调用者:
* (a) EsmTimeoutCounter 永不递减 → 任何 pending 状态迁移(SAFEOP→OP 等)永不
* 超时,从站会无限期忽略一切 AL Control(实测"进 PREOP 失败: AL=SAFEOP
* code=0x0000"的卡死就是它);
* (b) DC_CheckWatchdog/PLL 计数器(i16WaitForPllRunningCnt)不前进 → 若 ESC
* 残留 DC 激活位(0x0981,上个 DC 会话遗留、跨重连不清),从站按 DC 同步
* 模式进 SAFEOP→OP,OP 等待 PLL 序列永远不满足 → SAFEOP+0x0000 永久挂起
* (00:27 复测日志: 等待期轨迹恒 SAFEOP+0x0000,LRW 每 7ms 也救不回)。
* 改 0 后 MainLoop 按 DWT CYCCNT 轮询(hw_gd32exmc.cpp 的 HW_GetTimerValue/
* HW_ClearTimerValue 本就是为此写的死代码),每 1ms 调一次 ECAT_CheckTimer。 */
#ifndef ECAT_TIMER_INT #ifndef ECAT_TIMER_INT
#define ECAT_TIMER_INT 1 #define ECAT_TIMER_INT 0
#endif
/** 1.5.47:DWT CYCCNT @180MHz 的每毫秒 tick 数(ECAT_TIMER_INT=0 时 MainLoop 用)。 */
#ifndef ECAT_TIMER_INC_P_MS
#define ECAT_TIMER_INC_P_MS 180000U
#endif #endif
#ifndef INTERRUPTS_SUPPORTED #ifndef INTERRUPTS_SUPPORTED
@@ -572,18 +588,30 @@
/*----------------------------------------------------------------------------------------- /*-----------------------------------------------------------------------------------------
------ ------
------ PDO Size (需与 DCU_ESI.xml / gen_eeprom.py 的 PDO 尺寸一致: 4×64bit=32B) ------ PDO Size (需与 DCU_ESI.xml / gen_eeprom.py / dc_app.c 三处一致)
------ ------
------ 1.5.56 扩到 DPRAM 硬上限:64B/384B → 384B/640B。
------ 输出 384B = 48×64bit:兼容单帧区 48B + CANFD 批量发送区 336B
------ 输入 640B = 80×64bit:桥状态 32B + 电机窗口 288B + 长帧 40B + 批量接收区 280B
------ 上限依据:邮箱占 0x1000-0x13FF,过程数据区仅 0x1400-0x1FFF=3072B,
------ SM2/SM3 均 3-buffer 实占 3×size ⇒ out+in ≤ 1024B。384+640 恰好用满。
------ 推导与实测(SII 余量/EXMC 耗时)见 comm/ethercat_pdo_size.h。
------
------ 这两个宏是**编译期硬门限**,必须先于其它改动生效:
------ - ecatappl.c:157 用它们定义 PD 暂存区 aPdOutputData/aPdInputData
------ - ecatslv.c:509/574 在 SM 校验时拿 SMLength 与之比较,超限直接返回
------ ALSTATUSCODE_INVALIDSMINCFG/OUTCFG,表现为 PREOP→SAFEOP 失败
------ 忘改这里会在进 SAFEOP 时报 AL 错误,而不是静默截断。
-----------------------------------------------------------------------------------------*/ -----------------------------------------------------------------------------------------*/
/** 最大输出 PDO 字节数(RxPDO,主站→从站)。 */ /** 最大输出 PDO 字节数(RxPDO,主站→从站)。1.5.56: 0x40 → 0x180 = 384B */
#ifndef MAX_PD_OUTPUT_SIZE #ifndef MAX_PD_OUTPUT_SIZE
#define MAX_PD_OUTPUT_SIZE 0x20 #define MAX_PD_OUTPUT_SIZE 0x180
#endif #endif
/** 最大输入 PDO 字节数(TxPDO,从站→主站)。 */ /** 最大输入 PDO 字节数(TxPDO,从站→主站)。1.5.56: 0x140 → 0x280 = 640B */
#ifndef MAX_PD_INPUT_SIZE #ifndef MAX_PD_INPUT_SIZE
#define MAX_PD_INPUT_SIZE 0x20 #define MAX_PD_INPUT_SIZE 0x280
#endif #endif
/*----------------------------------------------------------------------------------------- /*-----------------------------------------------------------------------------------------
+44 -10
View File
@@ -54,17 +54,44 @@ PRODUCT_CODE = 0x00009252
REVISION_NO = 0x00010000 REVISION_NO = 0x00010000
SERIAL_NO = 0x00000001 SERIAL_NO = 0x00000001
# Mailbox configuration (ESC process RAM: mailbox 0x1000-0x13FF, PDO 0x1400-0x14BF) # Mailbox configuration (ESC process RAM: mailbox 0x1000-0x13FF, PDO 0x1400-0x1FFF)
MBX_RX_OFFSET = 0x1000 # SM0: Master -> Slave (receive mailbox) MBX_RX_OFFSET = 0x1000 # SM0: Master -> Slave (receive mailbox)
MBX_RX_SIZE = 256 MBX_RX_SIZE = 256
MBX_TX_OFFSET = 0x1200 # SM1: Slave -> Master (send mailbox) MBX_TX_OFFSET = 0x1200 # SM1: Slave -> Master (send mailbox)
MBX_TX_SIZE = 256 MBX_TX_SIZE = 256
# Process Data configuration # Process Data configuration
#
# 1.5.56 扩到 DPRAM 硬上限 64/384 -> 384/640(需要一周期合并多条 CANFD 帧)。
# 必须与 comm/ethercat_pdo_size.h 的 ECAT_PDO_OUT_BYTES / ECAT_PDO_IN_BYTES 一致
# ——固件侧有 _Static_assert 守着,但 EEPROM 这一侧无法编译期校验,只能靠这里对齐。
#
# ★ 为何 out+in 最大只能是 1024 ★
# 邮箱 SM0/SM1 各 256B 固定占 0x1000-0x13FF,过程数据区只剩 0x1400-0x1FFF=3072B。
# SM2/SM3 都是 3-buffer 模式(control 0x64 / 0x20,bit1=0),实际占用 3×size:
# SM2 384B -> 0x1400-0x187F (384×3 = 1152B)
# SM3 640B -> 0x1880-0x1FFF (640×3 = 1920B) ← 恰好用满最后一字节
# (384+640)×3 = 3072 = 过程数据区全长,一字节不剩也一字节不越界。
# SSC 在 ecatslv.c:708-721 按 (start + size*buffer) 做重叠检查,撞了会在
# PREOP->SAFEOP 返回 ALSTATUSCODE_INVALIDSMOUTCFG,不是静默出错。
# 要再更大只能压邮箱(拖慢 SDO/FoE)或改 1-buffer(失去读写并发保护),见
# comm/ethercat_pdo_size.h 的完整推导。
PD_OUT_OFFSET = 0x1400 # SM2: RxPDO (Master -> Slave outputs) PD_OUT_OFFSET = 0x1400 # SM2: RxPDO (Master -> Slave outputs)
PD_OUT_SIZE = 32 PD_OUT_SIZE = 384 # 48×64bit
PD_IN_OFFSET = 0x1480 # SM3: TxPDO (Slave -> Master inputs) PD_IN_OFFSET = 0x1880 # SM3: TxPDO (Slave -> Master inputs)
PD_IN_SIZE = 32 PD_IN_SIZE = 640 # 80×64bit
# 3-buffer 占用与 DPRAM 上限自检:改尺寸/偏移时立刻在生成阶段拦住,
# 而不是等烧进 EEPROM、冷启动后再看 AL 错误码。
_PD_BUFFERS = 3
assert PD_OUT_OFFSET >= MBX_TX_OFFSET + MBX_TX_SIZE, \
"过程数据区起点压到了邮箱上(SSC ecatslv.c:708 会报 INVALIDSMINCFG)"
assert PD_OUT_OFFSET + PD_OUT_SIZE * _PD_BUFFERS <= PD_IN_OFFSET, \
"SM2 的 3 个缓冲会盖住 SM3,请把 PD_IN_OFFSET 往后挪"
assert PD_IN_OFFSET + PD_IN_SIZE * _PD_BUFFERS - 1 <= 0x1FFF, \
"SM3 超出 LAN9252 4KB DPRAM (0x1FFF)"
assert PD_OUT_SIZE % 8 == 0 and PD_IN_SIZE % 8 == 0, \
"PDO 尺寸必须是 8 的整数倍(映射条目是 64bit)"
# PDI type: LAN9252 HBI 16-bit indexed mode (DS Table 14-2: 0x8C=HBI 8-bit, # PDI type: LAN9252 HBI 16-bit indexed mode (DS Table 14-2: 0x8C=HBI 8-bit,
# 0x8D=HBI 16-bit). 0x05 is a reserved value on LAN9252 and disables the HBI PDI. # 0x8D=HBI 16-bit). 0x05 is a reserved value on LAN9252 and disables the HBI PDI.
@@ -214,17 +241,24 @@ for start, length, control, enable, sm_type in sm_entries:
sm_data.append(sm_type) sm_data.append(sm_type)
append_category(41, bytes(sm_data)) append_category(41, bytes(sm_data))
def append_pdo(cat_type, pdo_index, sm_index, obj_index): def append_pdo(cat_type, pdo_index, sm_index, obj_index, pd_size):
"""Table 24/25: header [index(2)][nEntry(1)][SyncM(1)][Sync(1)][NameIdx(1)] """Table 24/25: header [index(2)][nEntry(1)][SyncM(1)][Sync(1)][NameIdx(1)]
[Flags(2)] + entries [idx(2)][sub(1)][name(1)][dtype(1)][bitlen(1)][flags(2)].""" [Flags(2)] + entries [idx(2)][sub(1)][name(1)][dtype(1)][bitlen(1)][flags(2)].
nEntry 由过程数据长度推导(每条目 UNSIGNED64 = 8B),须与 dc_app.c 的
PDO_OUT_ENTRIES / PDO_IN_ENTRIES 一致;写死 4 会在扩容后让主站按 32B 建映射,
与从站 APPL_GenerateMapping 报的长度不符,SAFEOP 阶段报 AL 0x001A/0x0011。"""
n_entry = pd_size // 8
if not 1 <= n_entry <= 255:
raise SystemExit(f"PDO 条目数 {n_entry} 超出 SII 单字节 nEntry 范围")
data = bytearray() data = bytearray()
data.extend(struct.pack('<H', pdo_index)) data.extend(struct.pack('<H', pdo_index))
data.append(4) # nEntry: 0x7000/0x6000:01..04 (dc_app.c) data.append(n_entry) # nEntry: 0x7000/0x6000:01..n (dc_app.c)
data.append(sm_index) # SyncM data.append(sm_index) # SyncM
data.append(0) # DC Synchronization data.append(0) # DC Synchronization
data.append(0) # NameIdx data.append(0) # NameIdx
data.extend(struct.pack('<H', 0)) # Flags data.extend(struct.pack('<H', 0)) # Flags
for sub in range(1, 5): for sub in range(1, n_entry + 1):
data.extend(struct.pack('<H', obj_index)) data.extend(struct.pack('<H', obj_index))
data.append(sub) # SubIndex data.append(sub) # SubIndex
data.append(0) # Entry NameIdx data.append(0) # Entry NameIdx
@@ -233,8 +267,8 @@ def append_pdo(cat_type, pdo_index, sm_index, obj_index):
data.extend(struct.pack('<H', 0)) # Flags data.extend(struct.pack('<H', 0)) # Flags
append_category(cat_type, bytes(data)) append_category(cat_type, bytes(data))
append_pdo(51, 0x1600, 2, 0x7000) # RXPDO: outputs (SM2) append_pdo(51, 0x1600, 2, 0x7000, PD_OUT_SIZE) # RXPDO: outputs (SM2)
append_pdo(50, 0x1A00, 3, 0x6000) # TXPDO: inputs (SM3) append_pdo(50, 0x1A00, 3, 0x6000, PD_IN_SIZE) # TXPDO: inputs (SM3)
# DC (60) is "for future use" per Table 19 and has no defined payload; # DC (60) is "for future use" per Table 19 and has no defined payload;
# DC configuration comes from DCU_ESI.xml <Dc> - omitted on purpose. # DC configuration comes from DCU_ESI.xml <Dc> - omitted on purpose.
File diff suppressed because one or more lines are too long
+40
View File
@@ -17,6 +17,7 @@
#include "sys/task_runner.h" #include "sys/task_runner.h"
#include "comm/ethercat_slave.h" #include "comm/ethercat_slave.h"
#include "comm/ethercat_sync.h" #include "comm/ethercat_sync.h"
#include "comm/ethercat_can_bridge.h"
#include "comm/esc_exmc_bus.h" #include "comm/esc_exmc_bus.h"
extern "C" { extern "C" {
@@ -233,6 +234,13 @@ void TaskRunner::taskCanRxParse()
s_runtime.last_rx_data, s_runtime.last_rx_data,
s_runtime.last_rx_len); s_runtime.last_rx_len);
#endif #endif
/* 1.5.53:旁路投递给 EtherCAT PDO 桥。放在分发链末尾且**不消费**帧
* (不 return、不 continue),因此 OTA relay / 电机状态的取帧不受影响——
* 本队列只有 taskCanRxParse 一个消费者,新增第二个消费者会互相抢帧。 */
EthercatCanBridge::onCanFrame(s_runtime.last_rx_channel,
s_runtime.last_rx_id,
s_runtime.last_rx_data,
s_runtime.last_rx_len);
consumed++; consumed++;
} }
@@ -273,6 +281,33 @@ bool TaskRunner::copyAdcSample(Adc::Sample *out)
/* SSC 侧 AL 诊断快照(dc_app.c): [19:16]=AL 状态 [31:24]=AL 错误码 [15:0]=AL 事件寄存器 */ /* SSC 侧 AL 诊断快照(dc_app.c): [19:16]=AL 状态 [31:24]=AL 错误码 [15:0]=AL 事件寄存器 */
extern "C" uint32_t EcatDbg_ReadAlDiag(void); extern "C" uint32_t EcatDbg_ReadAlDiag(void);
/* SSC 侧 AL Event Mask(0x204) 守护(dc_app.c, 1.5.48):缺位自愈+WARN,返回实际值/0xFFFF=不适用 */
extern "C" uint32_t EcatDbg_GuardAleventMask(void);
/**
* @brief 1.5.48/1.5.49 AL Event Mask(0x204) 守护 + SM2 事件楔死自愈 + 1.5.52 邮箱
* SM 地址/长度失配自愈:250ms 周期。
* @note 实测"停止回 INIT 后重进 OP"概率性挂起(SAFEOP+0x001B, WKC=3/3 但
* bEcatFirstOutputsReceived 不置位,0x220 bit10 挂起 10s 无人处理)——
* PDI_Isr 通路楔死。守护在 SAFEOP 后 1s 内替 PDI_Isr 完成工作(置位
* 标志+应答事件),主站 12s OP 等待窗口内可自愈;WARN 携带全部内部旗标,
* 下次复现一行日志定位楔死路径(详见 dc_app.c 注释)。
* 1.5.52:周期 1s→250ms——新加的邮箱地址自愈(1.5.52 根治 SSC 侧
* u16EscAddrSendMbx 错位楔死,详见 dc_app.c)必须快过主站 SDO 超时
* (~700ms),否则自愈永远落在主站放弃之后,形同虚设。
*/
static void ethercatAleMaskGuard()
{
static uint32_t s_lastGuardMs = 0U;
if (!EthercatSync::isActive()) {
return;
}
const uint32_t nowMs = g_scheduler.tick_1ms;
if ((uint32_t)(nowMs - s_lastGuardMs) >= 250U) {
s_lastGuardMs = nowMs;
EcatDbg_GuardAleventMask();
}
}
/** /**
* @brief 1.5.32 ECAT 心跳:主循环存活证明 + ESC AL 状态/事件寄存器快照。 * @brief 1.5.32 ECAT 心跳:主循环存活证明 + ESC AL 状态/事件寄存器快照。
@@ -547,6 +582,11 @@ void TaskRunner::runPending()
* DC 模式下 PDO 交换由 SYNC0 ISR (Sync0_Isr) 完成,此处仅处理低优先级逻辑。 */ * DC 模式下 PDO 交换由 SYNC0 ISR (Sync0_Isr) 完成,此处仅处理低优先级逻辑。 */
MainLoop(); MainLoop();
/* 1.5.48:AL Event Mask(0x204) 守护——每圈入口执行,内部 1s 节流。
* 放 MainLoop 旁而非诊断任务:守护必须独立于诊断打印频率,
* OP 等待期(12s)内从站进 SAFEOP 后 1s 内即介入自愈。 */
ethercatAleMaskGuard();
const uint32_t pending = TaskManager::takePending(); const uint32_t pending = TaskManager::takePending();
if(pending == 0U) return; if(pending == 0U) return;