变更点: 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、评审过程稿
284 lines
14 KiB
C++
284 lines
14 KiB
C++
/**
|
||
* @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 */
|