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:
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* @file ethercat_can_bridge.cpp
|
||||
* @brief EtherCAT PDO ↔ CAN 转发桥实现(1.6.21:段 2 电机整轮密排)。
|
||||
* @brief EtherCAT PDO ↔ CAN 转发桥实现。
|
||||
*/
|
||||
#include "comm/ethercat_can_bridge.h"
|
||||
#include "device/canfd.h"
|
||||
#include "device/motor_state.h"
|
||||
#include "sys/systick.h"
|
||||
|
||||
#include "RTE_Components.h"
|
||||
#include CMSIS_device_header /* __DMB():seqlock 的编译器/总线屏障 */
|
||||
@@ -17,6 +18,9 @@ volatile bool EthercatCanBridge::s_coldStartPending = false;
|
||||
volatile uint8_t EthercatCanBridge::s_lastTxResult = 0U;
|
||||
volatile uint16_t EthercatCanBridge::s_txCount = 0U;
|
||||
volatile uint16_t EthercatCanBridge::s_dropCount = 0U;
|
||||
volatile uint32_t EthercatCanBridge::s_lastTxId = 0U;
|
||||
volatile uint16_t EthercatCanBridge::s_rx00ffCount = 0U;
|
||||
volatile uint8_t EthercatCanBridge::s_outSeenSeq = 0U;
|
||||
|
||||
volatile bool EthercatCanBridge::s_rxValid = false;
|
||||
volatile bool EthercatCanBridge::s_rxPublished = false;
|
||||
@@ -28,6 +32,20 @@ volatile uint32_t EthercatCanBridge::s_rxId = 0U;
|
||||
volatile uint32_t EthercatCanBridge::s_rxCount = 0U;
|
||||
uint8_t EthercatCanBridge::s_rxData[kEcatCanXRxDataMax] = {0};
|
||||
|
||||
volatile bool EthercatCanBridge::s_verValid = false;
|
||||
volatile uint32_t EthercatCanBridge::s_verHoldUntilMs = 0U;
|
||||
uint8_t EthercatCanBridge::s_verCh = 0U;
|
||||
uint8_t EthercatCanBridge::s_verDlc = 0U;
|
||||
uint32_t EthercatCanBridge::s_verId = 0U;
|
||||
uint8_t EthercatCanBridge::s_verData[kEcatCanXRxDataMax] = {0};
|
||||
|
||||
namespace {
|
||||
/** 版本应答在 PDO 段3 的保持时长(ms)。主站 1ms 周期下足够被采到。 */
|
||||
constexpr uint32_t kVerHoldMs = 300U;
|
||||
|
||||
inline uint16_t canSubId(uint32_t id) { return static_cast<uint16_t>(id & 0xFFFFU); }
|
||||
} // namespace
|
||||
|
||||
uint8_t EthercatCanBridge::s_motShadow[kEcatMotRegionBytes] = {0};
|
||||
uint32_t EthercatCanBridge::s_motPubSeq[kMotorMaxNodes + 1U] = {0};
|
||||
uint8_t EthercatCanBridge::s_motPubOnline[kMotorMaxNodes + 1U] = {0};
|
||||
@@ -98,6 +116,7 @@ uint8_t EthercatCanBridge::sendBatch(const uint8_t *out)
|
||||
if (r == CanFd::CanTxResult::kOk) {
|
||||
++s_txCount;
|
||||
++sent;
|
||||
s_lastTxId = rd32le(&f[ECAT_FRM_OFF_ID]);
|
||||
} else {
|
||||
s_lastTxResult = (uint8_t)r;
|
||||
if (stopOnError) {
|
||||
@@ -170,7 +189,9 @@ uint8_t EthercatCanBridge::fillMotorRound(uint8_t *in)
|
||||
|
||||
const uint16_t region = motRegionLen(unchanged ? s_motPubCount : n);
|
||||
|
||||
if (unchanged) {
|
||||
/* 集合+seq+online 未变:整段复用影子,避免 8kHz 下每拍重建密排区。
|
||||
* 头字段 ACTIVE/BOOT/MAGIC/GSEQ 在影子起点 COUNT 之后,一并复用。 */
|
||||
if (unchanged && s_motShadowValid && s_motPubCount > 0U) {
|
||||
memcpy(&in[ECAT_MOTW_OFF_COUNT], s_motShadow, region);
|
||||
return s_motPubCount;
|
||||
}
|
||||
@@ -216,13 +237,14 @@ uint8_t EthercatCanBridge::fillMotorRound(uint8_t *in)
|
||||
|
||||
bool EthercatCanBridge::peekOutput(const uint8_t *head)
|
||||
{
|
||||
/* 输出探针:12B 头里 SEQ@0 / FLAGS。seq 无变化沿且无清统计请求时返回 false,
|
||||
* PDO_OutputMapping 只补读 SM2 末字,跳过 384B 全量读。
|
||||
* 恒 true 会让 8kHz 每拍都全量读,单拍被 PRAM 打爆(6kHz 即掉速)。 */
|
||||
if (head == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const uint8_t seq = head[ECAT_CAN_TX_OFF_SEQ];
|
||||
|
||||
/* WIN@8 不再使用:1.6.21 电机整轮密排,无滑窗。 */
|
||||
|
||||
if (!s_seqPrimed) {
|
||||
s_seqPrimed = true;
|
||||
s_lastSeq = seq;
|
||||
@@ -250,10 +272,10 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
|
||||
}
|
||||
|
||||
const uint8_t seq = out[ECAT_CAN_TX_OFF_SEQ];
|
||||
s_outSeenSeq = seq; /* 诊断:processOutput 本拍看到的输出 seq */
|
||||
|
||||
if (!s_seqPrimed) {
|
||||
s_seqPrimed = true;
|
||||
s_lastSeq = seq;
|
||||
s_coldStartPending = true;
|
||||
}
|
||||
|
||||
@@ -262,20 +284,28 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
|
||||
s_txCount = 0U;
|
||||
s_dropCount = 0U;
|
||||
s_rxCount = 0U;
|
||||
s_rx00ffCount = 0U;
|
||||
s_rxPublished = true;
|
||||
}
|
||||
|
||||
const uint8_t dlcRaw = out[ECAT_CAN_TX_OFF_DLC];
|
||||
const uint8_t batchCount = out[ECAT_TXB_OFF_COUNT];
|
||||
const bool hasPayload = (dlcRaw > 0U) || (batchCount > 0U);
|
||||
|
||||
bool executeCommand = false;
|
||||
if (s_coldStartPending) {
|
||||
s_coldStartPending = false;
|
||||
/* 冷启动首包:若包含有效单帧命令或批量帧,立即执行 */
|
||||
const uint8_t dlc = out[ECAT_CAN_TX_OFF_DLC];
|
||||
const uint8_t batchCount = out[ECAT_TXB_OFF_COUNT];
|
||||
if (dlc > 0U || batchCount > 0U) {
|
||||
/* 冷启动:有载荷就发(不依赖 s_lastSeq 是否已被探针碰过) */
|
||||
if (hasPayload) {
|
||||
executeCommand = true;
|
||||
} else {
|
||||
s_lastSeq = seq;
|
||||
return;
|
||||
}
|
||||
} else if (seq != s_lastSeq) {
|
||||
executeCommand = true;
|
||||
} else if (hasPayload && seq != s_lastSeq) {
|
||||
executeCommand = true;
|
||||
}
|
||||
|
||||
if (!executeCommand) {
|
||||
@@ -284,7 +314,7 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
|
||||
s_lastSeq = seq;
|
||||
|
||||
const uint8_t ch = out[ECAT_CAN_TX_OFF_CH];
|
||||
uint8_t dlc = out[ECAT_CAN_TX_OFF_DLC];
|
||||
uint8_t dlc = dlcRaw;
|
||||
if (dlc > kEcatCanTxDataMax) {
|
||||
dlc = kEcatCanTxDataMax;
|
||||
}
|
||||
@@ -299,6 +329,7 @@ void EthercatCanBridge::processOutput(const uint8_t *out)
|
||||
s_lastTxResult = (uint8_t)r;
|
||||
if (r == CanFd::CanTxResult::kOk) {
|
||||
++s_txCount;
|
||||
s_lastTxId = rd32le(&out[ECAT_CAN_TX_OFF_ID]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,7 +342,9 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
if (in == nullptr) {
|
||||
return;
|
||||
}
|
||||
memset(in, 0, kEcatPdoInBytes);
|
||||
/* 不整包 memset(600):8kHz 下每拍 Process 都会走到这里,600B 清零
|
||||
* 与后续覆盖写重复。各段自己写满有效域;未用槽由 fillMotorRound
|
||||
* 按 count 截断,主站只迭代 count,空洞区可为残值。 */
|
||||
|
||||
/* ── 段 1(0..31):桥状态 + 短快照 ── */
|
||||
in[ECAT_CAN_RX_OFF_ACK] = s_lastSeq;
|
||||
@@ -319,6 +352,8 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
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);
|
||||
wr32le(&in[ECAT_CAN_RX_OFF_TXTXID], s_lastTxId);
|
||||
wr16le(&in[ECAT_CAN_RX_OFF_RX00FF], s_rx00ffCount);
|
||||
|
||||
const bool xrxStable = ((s_xrxSeq & 1U) == 0U);
|
||||
|
||||
@@ -333,26 +368,45 @@ void EthercatCanBridge::buildInput(uint8_t *in)
|
||||
wr32le(&in[ECAT_CAN_RX_OFF_ID], s_rxId);
|
||||
memcpy(&in[ECAT_CAN_RX_OFF_DATA], s_rxData, kEcatCanRxDataMax);
|
||||
s_rxPublished = true;
|
||||
} else {
|
||||
in[ECAT_CAN_RX_OFF_FLAGS] = 0U;
|
||||
in[ECAT_CAN_RX_OFF_CH] = 0U;
|
||||
in[ECAT_CAN_RX_OFF_DLC] = 0U;
|
||||
}
|
||||
|
||||
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..543):电机整轮密排(空槽不发,count=实际电机数)── */
|
||||
/* ── 段 2(32..543):电机整轮密排 ── */
|
||||
(void)fillMotorRound(in);
|
||||
|
||||
/* ── 段 3(544..583):最近一帧完整快照 ──
|
||||
* 与段 1 共用 xrxStable 门控,两段必须来自同一帧。 */
|
||||
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);
|
||||
/* ── 段 3(544..583):长帧快照(通用,不专宠版本) ──
|
||||
* 语义 =「最近一帧非 STATUS 长帧」:CONFIG/版本/节点ID 谁后到谁上。
|
||||
* STATUS(0x0100) 由段2 密排承载,不进本槽(否则 48B STATUS 会淹没一切)。
|
||||
* 例外:仅当刚收到 0x00FF 的短粘滞窗口内,优先摆版本应答,
|
||||
* 给主站一个稳定采样窗;窗口结束后立刻交还给 CONFIG 等,避免其它长帧失效。 */
|
||||
if (xrxStable) {
|
||||
const uint32_t nowMs = SysTickTimer::getMs();
|
||||
const bool stickyLive = s_verValid && static_cast<int32_t>(nowMs - s_verHoldUntilMs) < 0;
|
||||
/* 段3 RSVD@547:processOutput 看到的输出 seq(诊断用,不参与解析) */
|
||||
in[ECAT_XRX_OFF_RSVD] = s_outSeenSeq;
|
||||
if (stickyLive) {
|
||||
in[ECAT_XRX_OFF_CH] = s_verCh;
|
||||
in[ECAT_XRX_OFF_DLC] = s_verDlc;
|
||||
in[ECAT_XRX_OFF_FLAGS] = 0x01U;
|
||||
wr32le(&in[ECAT_XRX_OFF_ID], s_verId);
|
||||
memcpy(&in[ECAT_XRX_OFF_DATA], s_verData, kEcatCanXRxDataMax);
|
||||
} else if (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(历史覆盖窗口)已删除;段 5 由 EthercatPdo::fillDiagTail 写。 */
|
||||
/* 段 5 由 EthercatPdo::fillDiagTail 写。 */
|
||||
}
|
||||
|
||||
void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
@@ -362,6 +416,12 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
return;
|
||||
}
|
||||
|
||||
/* STATUS(0x0100) 由 MotorState 段2 密排承载,不写入 xrx。
|
||||
* 原注释本就如此约定;实现曾漏过滤,导致高频 STATUS 盖掉 CONFIG/版本。 */
|
||||
if (canSubId(id) == 0x0100U) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t copyLen = length;
|
||||
if (copyLen > kEcatCanXRxDataMax) {
|
||||
copyLen = kEcatCanXRxDataMax;
|
||||
@@ -388,11 +448,27 @@ void EthercatCanBridge::onCanFrame(uint8_t channel, uint32_t id,
|
||||
s_rxValid = true;
|
||||
s_rxPublished = false;
|
||||
|
||||
if (canSubId(id) == 0x00FFU) {
|
||||
if (s_rx00ffCount < 0xFFFFU) {
|
||||
++s_rx00ffCount;
|
||||
}
|
||||
}
|
||||
|
||||
/* 版本应答:只做短粘滞,供主站在窗口内采到;不长期霸占 xrx */
|
||||
if (canSubId(id) == 0x00FFU) {
|
||||
s_verCh = channel;
|
||||
s_verDlc = length;
|
||||
s_verId = id;
|
||||
memcpy(s_verData, data, copyLen);
|
||||
if (copyLen < kEcatCanXRxDataMax) {
|
||||
memset(&s_verData[copyLen], 0, (size_t)(kEcatCanXRxDataMax - copyLen));
|
||||
}
|
||||
s_verHoldUntilMs = SysTickTimer::getMs() + kVerHoldMs;
|
||||
s_verValid = true;
|
||||
}
|
||||
|
||||
__DMB();
|
||||
++s_xrxSeq;
|
||||
|
||||
/* STATUS 走 MotorState → 段 2 整轮;非 STATUS 只更新段 1/3 最近一帧快照。
|
||||
* 1.6.21 起不再写入历史覆盖窗口。 */
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
Reference in New Issue
Block a user