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、评审过程稿
This commit is contained in:
cbb
2026-09-20 22:58:09 +08:00
parent 63af85e7be
commit 5fd986299c
32 changed files with 2592 additions and 781 deletions
+34 -21
View File
@@ -1,6 +1,6 @@
/**
* @file ethercat_can_bridge.h
* @brief EtherCAT PDO ↔ CAN 转发桥(1.6.21:段 2 改 9 电机密排整轮)。
* @brief EtherCAT PDO ↔ CAN 转发桥。
*
* 输出 PDO(0x7000, 主站→从站, 384B) = 待发送的 CAN 帧(数据区 32B)
* 输入 PDO(0x6000, 从站→主站, 600B) = 桥状态 + 电机整轮状态 + 长帧快照 + 诊断
@@ -13,23 +13,17 @@
* TaskRunner::taskCanRxParse(),两个消费者会互相抢帧,OTA relay / 电机状态
* 会随机丢帧。改为由 taskCanRxParse() 的分发链回调 onCanFrame() 投递。
*
* ── 1.6.21:段 2 改「一帧 = 总线电机整轮状态」,删段 4 历史帧窗口 ──
* 段 2 语义:一帧 = 总线电机整轮状态。
*
* 旧布局的问题(用户实测:接 3 个电机网页却看到 5 个状态帧):
* 1. 段 2 是固定 5 槽滑窗(win_base 要 6 拍才扫完 27 槽),空槽 node_id=0
* 也占位,win_count 报的是跨度不是电机数 —— 主站若不滤空槽就会多报;
* 2. 段 4 覆盖窗口把最近几条非 STATUS 帧反复重发,属于「历史帧 / 重复帧」。
*
* 本版语义改为用户需求:
* · 每一拍输入 PDO 携带**总线上已识别电机的完整一轮状态**(最多 9 个);
* · **空槽不发送**:密排,count = 本拍实际打包的电机数;
* · 主站按 count 迭代,每槽自带 node_id,自动识别,无需 win_base 滑窗;
* · 不发段 4 历史帧;版本/CONFIG 仍走段 1/段 3「最近一帧」快照;
* · 版本/CONFIG 走段 1/段 3「最近一帧」快照;
* · 上一轮已打包且 seq 未变的电机,本轮跳过 MotorState 重读与 48B 拷贝,
* 直接复用影子区(s_motShadow)。
*
* 输入 PDO 总长仍 600B ⇒ 无需重刷 EEPROM/SII;段 2/3 偏移变了,**主站解码
* 必须同步**(tools/ecat 的 decode_input / transport-ecat.js 等)。
* 输入 PDO 总长 600B;主站解码必须与段 2/3 偏移一致
* (tools/ecat 的 decode_input / transport-ecat.js 等)。
*/
#ifndef COMM_ETHERCAT_CAN_BRIDGE_H
#define COMM_ETHERCAT_CAN_BRIDGE_H
@@ -39,14 +33,14 @@
#include "comm/ethercat_pdo_size.h"
#include "device/motor_state.h" /* kMotorMaxNodes / kMotorChannels */
/** 输出 PDO(主站→从站, 384B)段 1(0..47):单帧发送。布局与 1.5.55 兼容。 */
/** 输出 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, /**< 1.6.21 起忽略(电机整轮密排,无滑窗);保留占位 */
ECAT_CAN_TX_OFF_WIN = 8U, /**< 当前忽略(电机整轮密排,无滑窗);保留占位 */
/* 9..15 保留:让数据区落在 16 字节边界 */
ECAT_CAN_TX_OFF_DATA = 16U, /**< 数据区起点,32 字节(一帧电机 CMD) */
};
@@ -69,7 +63,7 @@ enum : uint8_t {
ECAT_FRM_HDR_SIZE = 8U, /**< 头长度,数据紧跟其后 */
};
/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照。 */
/** 输入 PDO 段 1(0..31):桥状态 + 最近一帧短快照 + 收发诊断。 */
enum : uint8_t {
ECAT_CAN_RX_OFF_ACK = 0U, /**< 回显已执行的发送序号,主站据此确认送达 */
ECAT_CAN_RX_OFF_CH = 1U, /**< 该帧来自哪个通道 */
@@ -82,10 +76,12 @@ enum : uint8_t {
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):电机整轮状态(密排,1.6.21)。
* 输入 PDO 段 2(32..543):电机整轮状态(密排)。
*
* 主站解析:
* count = in[32] // 0..9,本拍实际打包的电机数
@@ -98,7 +94,7 @@ 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):1.6.21 密排标识 */
ECAT_MOTW_OFF_MAGIC = 35U, /**< 恒 kEcatMotLayoutMagic(0xA5):密排布局标识 */
ECAT_MOTW_OFF_GLOBAL_SEQ = 36U, /**< 状态表全局刷新序号,小端 uint32 */
ECAT_MOTW_OFF_SLOTS = 40U, /**< 密排槽数组起点,count×56B */
};
@@ -150,8 +146,8 @@ constexpr uint8_t kEcatMotHdrSize = 8U;
constexpr uint16_t kEcatMotRegionBytes =
(uint16_t)(kEcatMotHdrSize
+ (uint16_t)kEcatMotSlotsPerPdo * kEcatMotSlotSize);
/** 布局魔数,写在 offset 35。主站据此区分 1.6.21 密排与 1.6.15 滑窗(两者
* Ibytes 同为 600,不能只靠长度分支)。0=旧布局。 */
/** 布局魔数,写在 offset 35。主站据此区分密排布局与旧滑窗布局(两者
* Ibytes 同为 600,不能只靠长度分支)。0=非密排布局。 */
constexpr uint8_t kEcatMotLayoutMagic = 0xA5U;
/** 过程数据长度,唯一真源见 comm/ethercat_pdo_size.h。 */
@@ -194,7 +190,7 @@ class EthercatCanBridge {
public:
/**
* @brief 输出探针:seq 变化时请求全量读。
* @note 1.6.21 起不再刷新 win_base(电机整轮密排,无滑窗);仍处理 seq 沿
* @note 不再刷新 win_base(电机整轮密排,无滑窗);仍处理 seq 沿
* 与 FLAGS bit0 清统计。
*/
static bool peekOutput(const uint8_t *head);
@@ -234,8 +230,16 @@ private:
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。 */
/* 最近一帧接收快照: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;
@@ -246,7 +250,16 @@ private:
static volatile uint32_t s_rxCount;
static uint8_t s_rxData[kEcatCanXRxDataMax];
/* ── 段 2 差分填充影子(1.6.21)──
/* 版本应答粘滞快照: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。 */