Files
cbb 5fd986299c feat(comm): 1.6.51 CAN 空通道自动静默 + PDO 诊断 + OTA 布局共用头
变更点:
1. CAN-FD(device/CANFD.cpp、sys/config.h)
   - 发送统一 FD+BRS(DLC≤8 不再回退经典 CAN);OTA 期仍按通道压制 BRS
   - 空通道自动识别:连续 5 个 1Hz 窗口无有效帧且有错误 → 静默上报;有帧即恢复
   - 三路 CAN 默认全开;RX parse budget 48→64
2. EtherCAT 桥 / PDO
   - 段1 增 txtxid/rx00ff;版本应答 0x00FF 粘滞发布,避免被 CONFIG 冲掉
   - STATUS 禁止写入 xrx 快照;processSeq 新鲜度救援
3. OTA / 配置
   - sys/config.h include 仓库内 shared/ota_layout.h(与 BOOT 共用,不再依赖仓外路径)
   - ota_boot_request 对齐配置页 word 布局
4. docs/audit_ecat_spi_wifi_ota_compatibility.md:ECAT/SPI/WiFi/OTA 兼容性审计
5. .gitignore:忽略 .trae/、docs/traework-import、评审过程稿
2026-09-20 22:58:09 +08:00

284 lines
14 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file ethercat_can_bridge.h
* @brief EtherCAT PDO ↔ CAN 转发桥。
*
* 输出 PDO(0x7000, 主站→从站, 384B) = 待发送的 CAN 帧(数据区 32B)
* 输入 PDO(0x6000, 从站→主站, 600B) = 桥状态 + 电机整轮状态 + 长帧快照 + 诊断
*
* 发送用**序号沿触发**:仅当 seq 字节变化时才真正发一帧。PDO 是周期性刷新的,
* 若按「内容非空即发」会把同一帧每个周期重发一次;seq 变化沿是 PDO 里表达
* 「一次性事件」的标准做法,主站只需每次发送前把 seq 加一。
*
* RX 侧不能自己去 CanFd::consumeRxFrame() —— 那个队列已有唯一消费者
* TaskRunner::taskCanRxParse(),两个消费者会互相抢帧,OTA relay / 电机状态
* 会随机丢帧。改为由 taskCanRxParse() 的分发链回调 onCanFrame() 投递。
*
* 段 2 语义:一帧 = 总线电机整轮状态。
*
* · 每一拍输入 PDO 携带**总线上已识别电机的完整一轮状态**(最多 9 个);
* · **空槽不发送**:密排,count = 本拍实际打包的电机数;
* · 主站按 count 迭代,每槽自带 node_id,自动识别,无需 win_base 滑窗;
* · 版本/CONFIG 走段 1/段 3「最近一帧」快照;
* · 上一轮已打包且 seq 未变的电机,本轮跳过 MotorState 重读与 48B 拷贝,
* 直接复用影子区(s_motShadow)。
*
* 输入 PDO 总长 600B;主站解码必须与段 2/3 偏移一致
* (tools/ecat 的 decode_input / transport-ecat.js 等)。
*/
#ifndef COMM_ETHERCAT_CAN_BRIDGE_H
#define COMM_ETHERCAT_CAN_BRIDGE_H
#include <cstdint>
#include "comm/ethercat_pdo_size.h"
#include "device/motor_state.h" /* kMotorMaxNodes / kMotorChannels */
/** 输出 PDO(主站→从站, 384B)段 1(0..47):单帧发送。 */
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, /**< 当前忽略(电机整轮密排,无滑窗);保留占位 */
/* 9..15 保留:让数据区落在 16 字节边界 */
ECAT_CAN_TX_OFF_DATA = 16U, /**< 数据区起点,32 字节(一帧电机 CMD) */
};
/** 输出 PDO 段 2(48..383):CANFD 批量发送区。 */
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):桥状态 + 最近一帧短快照 + 收发诊断。 */
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, /**< 段 1/段 3 快照「未取先覆」次数(u16 累计回绕) */
ECAT_CAN_RX_OFF_TXTXID = 26U, /**< 最近 enqueueTx 成功的 CAN ID,小端 uint32 */
ECAT_CAN_RX_OFF_RX00FF = 30U, /**< CAN RX 收到 0x00FF 次数,小端 uint16 */
};
/**
* 输入 PDO 段 2(32..543):电机整轮状态(密排)。
*
* 主站解析:
* count = in[32] // 0..9,本拍实际打包的电机数
* for (i = 0; i < count; i++)
* slot = in[40 + i*56] // node_id/online/dlc/seq/status
*
* 空节点不占位;槽序按 node_id 升序;同号多通道取 seq 最新的一条。
*/
enum : uint16_t {
ECAT_MOTW_OFF_COUNT = 32U, /**< 本拍密排电机数 0..9(主站按此迭代) */
ECAT_MOTW_OFF_ACTIVE = 33U, /**< 在线电机数(与 count 可不同:离线但仍报最后一轮) */
ECAT_MOTW_OFF_BOOT = 34U, /**< 1=近 3s 收到过 0x103 BOOT 心跳 */
ECAT_MOTW_OFF_MAGIC = 35U, /**< 恒 kEcatMotLayoutMagic(0xA5):密排布局标识 */
ECAT_MOTW_OFF_GLOBAL_SEQ = 36U, /**< 状态表全局刷新序号,小端 uint32 */
ECAT_MOTW_OFF_SLOTS = 40U, /**< 密排槽数组起点,count×56B */
};
/** 电机状态槽内部布局(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(正常 48) */
ECAT_MOT_OFF_SEQ = 4U, /**< STATUS 刷新序号,小端 uint32 */
ECAT_MOT_OFF_STATUS = 8U, /**< 原始 STATUS payload,48 字节 */
};
/** 输入 PDO 段 3(544..583):最近一帧完整快照(版本/CONFIG 帧要 24B+)。 */
enum : uint16_t {
ECAT_XRX_OFF_CH = 544U, /**< 通道 */
ECAT_XRX_OFF_DLC = 545U, /**< 原始 DLC(可达 32) */
ECAT_XRX_OFF_FLAGS = 546U, /**< bit0=有效 */
ECAT_XRX_OFF_RSVD = 547U,
ECAT_XRX_OFF_ID = 548U, /**< CAN ID,小端 uint32 */
ECAT_XRX_OFF_DATA = 552U, /**< 数据区,32 字节完整 payload */
};
/** 输入 PDO 段 5(584..595):设备诊断区(位置未变)。 */
enum : uint16_t {
ECAT_DIAG_OFF_LOAD = 584U, /**< 负载率,小端 uint16,单位 0.1%(1000=100%) */
ECAT_DIAG_OFF_TORN = 586U, /**< CAN 取帧撕裂丢帧数(低 16 位,饱和) */
ECAT_DIAG_OFF_SEQ = 588U, /**< 诊断区序号,小端 uint16,每完成 1s 窗口 +1 */
ECAT_DIAG_OFF_OVERFULL = 590U, /**< CAN RX FIFO 满溢计数 RFO0(u16 饱和) */
ECAT_DIAG_OFF_RELTIMEOUT = 592U, /**< RFD0 释放握手超时计数(u16 饱和) */
ECAT_DIAG_OFF_REJECT = 594U, /**< CAN 内容级拼接帧命中计数(u16 饱和) */
};
/** 单帧数据在输出 PDO / 长帧快照区中的最大字节数(电机 CMD 是 DLC 32)。 */
constexpr uint8_t kEcatCanTxDataMax = 32U;
/** 段 1 短快照数据区字节数。 */
constexpr uint8_t kEcatCanRxDataMax = 8U;
/** 段 3 长快照数据区字节数。 */
constexpr uint8_t kEcatCanXRxDataMax = 32U;
/** 单个电机状态槽字节数:8B 头 + 48B STATUS。 */
constexpr uint8_t kEcatMotSlotSize = 56U;
/** 一拍最多打包的电机数 = 节点号 1..9 各一槽(同号多通道取最新)。 */
constexpr uint8_t kEcatMotSlotsPerPdo = kMotorMaxNodes; /* 9 */
/** 段 2 头长度(COUNT..GLOBAL_SEQ,8B)。 */
constexpr uint8_t kEcatMotHdrSize = 8U;
/** 段 2 密排区最大字节:头 8B + 9×56B = 512B,落在 32..543。 */
constexpr uint16_t kEcatMotRegionBytes =
(uint16_t)(kEcatMotHdrSize
+ (uint16_t)kEcatMotSlotsPerPdo * kEcatMotSlotSize);
/** 布局魔数,写在 offset 35。主站据此区分密排布局与旧滑窗布局(两者
* Ibytes 同为 600,不能只靠长度分支)。0=非密排布局。 */
constexpr uint8_t kEcatMotLayoutMagic = 0xA5U;
/** 过程数据长度,唯一真源见 comm/ethercat_pdo_size.h。 */
constexpr uint16_t kEcatPdoOutBytes = ECAT_PDO_OUT_BYTES; /* 384B */
constexpr uint16_t kEcatPdoInBytes = ECAT_PDO_IN_BYTES; /* 600B */
/** CANFD 单帧数据上限。 */
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));
/* ── 布局守卫 ── */
static_assert(ECAT_MOTW_OFF_SLOTS
+ (uint16_t)kEcatMotSlotsPerPdo * kEcatMotSlotSize
== ECAT_XRX_OFF_CH,
"段2密排区必须与段3首尾相接");
static_assert(ECAT_XRX_OFF_DATA + kEcatCanXRxDataMax == ECAT_DIAG_OFF_LOAD,
"段3必须与段5首尾相接");
static_assert(ECAT_CAN_TX_OFF_DATA + kEcatCanTxDataMax <= ECAT_TXB_OFF_COUNT,
"单帧发送数据区与批量发送区重叠");
static_assert(ECAT_TXB_OFF_ITEMS < kEcatPdoOutBytes, "批量发送区起点越界");
static_assert(kEcatTxBatchMax >= 1U, "批量发送区装不下一帧 DLC64");
static_assert(ECAT_DIAG_OFF_REJECT + 2U <= kEcatPdoInBytes, "段5诊断区越界");
static_assert(ECAT_MOTW_OFF_COUNT + kEcatMotRegionBytes <= ECAT_XRX_OFF_CH,
"段2密排区越界");
/**
* @brief EtherCAT ↔ CAN 转发桥。
* @note 全静态;PDO 侧函数在 Sync0_Isr(preempt=0) 或主循环 MainLoop 上下文执行,
* onCanFrame() 在主循环 taskCanRxParse() 上下文执行。共享状态用 volatile +
* 单生产者/单消费者语义。多字段一组用 seqlock:
* - 段 1/段 3 的「最近一帧快照」→ s_xrxSeq;
* - 电机槽 48B payload → MotorSlot.wr_seq。
* Sync0 是 preempt=0,critical.h 的 BASEPRI 方案挡不住它,故只能无锁协议。
*/
class EthercatCanBridge {
public:
/**
* @brief 输出探针:seq 变化时请求全量读。
* @note 不再刷新 win_base(电机整轮密排,无滑窗);仍处理 seq 沿
* 与 FLAGS bit0 清统计。
*/
static bool peekOutput(const uint8_t *head);
/** @brief 复位输出 sequence 与冷启动同步状态。 */
static void resetSequenceState();
static void processOutput(const uint8_t *out);
/**
* @brief 填充输入 PDO:桥状态 + 电机整轮密排 + 长帧快照。
* @param[out] in kEcatPdoInBytes 字节输入过程数据。
*/
static void buildInput(uint8_t *in);
/**
* @brief CAN 收帧投递入口(旁路复制,不消费帧)。
*/
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 填充段 2 电机整轮密排区(含差分跳过)。
* @return 本拍打包的电机数(写入 count 字段)。
*/
static uint8_t fillMotorRound(uint8_t *in);
static uint8_t sendBatch(const uint8_t *out);
static volatile uint8_t s_lastSeq;
static volatile bool s_seqPrimed;
static volatile bool s_coldStartPending;
static volatile uint8_t s_lastTxResult;
static volatile uint16_t s_txCount;
static volatile uint16_t s_dropCount;
/** 最近一次 enqueueTx 成功的 CAN ID(诊断:ECAT 是否真的发出去) */
static volatile uint32_t s_lastTxId;
/** CAN RX 收到 0x00FF(版本应答)次数(诊断:电机是否回帧) */
static volatile uint16_t s_rx00ffCount;
/** processOutput 本拍读到的输出 PDO seq(诊断:ESC 输出是否到达应用层) */
static volatile uint8_t s_outSeenSeq;
/* 最近一帧接收快照:taskCanRxParse 写、PDO 读。s_xrxSeq 为 seqlock。
* STATUS(0x0100) 禁止写入本快照 —— 它由 MotorState 段2 密排承载;
* 否则高频 STATUS 会持续冲掉版本应答(0x00FF)等低频长帧。 */
static volatile bool s_rxValid;
static volatile bool s_rxPublished;
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];
/* 版本应答粘滞快照:0x00FF 到达后在 PDO 段3 保持发布一段时间,
* 避免被随后的 CONFIG 等帧在主站采样前覆盖。 */
static volatile bool s_verValid;
static volatile uint32_t s_verHoldUntilMs;
static uint8_t s_verCh;
static uint8_t s_verDlc;
static uint32_t s_verId;
static uint8_t s_verData[kEcatCanXRxDataMax];
/* ── 段 2 差分填充影子 ──
* 上一拍已发布的密排区副本 + 各节点上次发布的 seq/online + 头字段。
* 节点集合、seq、online、active/boot 全未变时整段复用,跳过 MotorState
* 重读与逐槽 48B 拷贝。掩码必须 u16:节点号 1..9,bit9 超出 uint8_t。 */
static uint8_t s_motShadow[kEcatMotRegionBytes];
static uint32_t s_motPubSeq[kMotorMaxNodes + 1U]; /* 下标=节点号 */
static uint8_t s_motPubOnline[kMotorMaxNodes + 1U]; /* checkOffline 不 bump seq,须单独比 */
static uint16_t s_motPubMask; /**< bit n=1:节点 n 在影子区中 */
static uint8_t s_motPubCount;
static bool s_motShadowValid;
};
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);
/** @brief 供 dc_app.c 调用的 shim:复位输出 sequence 状态机。 */
void EcatCan_ResetSequenceState(void);
}
#endif /* COMM_ETHERCAT_CAN_BRIDGE_H */