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

174 lines
8.7 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_pdo.h
* @brief EtherCAT 过程数据(PDO)收发模块。
*
* 一个 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 输出/输入**长度不相等**(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 输出探针(输出按需全量读,配合 SSC PDO_OutputMapping)。
* @param[in] head 输出 PDO 头 12B(覆盖 SEQ@0 / WIN@8 / FLAGS@3)。
* @return true=需要全量读(seq 沿或清统计请求);false=本周期跳过全量读。
* @note 每周期由 PDI_Isr 在读 ESC 前调用:做 seq 基准 priming 与沿判定;
* s_lastSeq 的推进仍留在全量路径(processOutput)。不再
* 维护 win_base(段 2 电机整轮密排,无滑窗)。
*/
static bool outputProbe(const uint8_t *head);
/**
* @brief 跑一个应用周期:消费输出、生成输入。
* @note 由 APPL_Application() 在主循环调用。
*/
static void process();
/**
* @brief 交回输入过程数据(从站→主站)。
* @param[out] esc 指向 ESC 输入缓冲的 kPdoInBytes 字节。
* @note 由 APPL_InputMapping() 每周期调用。只填 RAM/对象存储;
* 真正写 ESC 的字节量由 PDO_InputMapping 经 sparseEscWritePlan 决定。
*/
static void writeInputs(uint8_t *esc);
/**
* @brief 稀疏写 ESC 的两段计划:段1+段2有效区、段3+段5;跳过空槽区。
* @param[out] off0/len0 第一段相对 PDO 起点的偏移与长度
* @param[out] off1/len1 第二段
* @return true=可按两段写;false=应回退整包 600B(开关关/非密排/count 越界/两段相接)
* @note 必须在 writeInputs() 之后调用,读的是已发布的 s_in。
*/
static bool sparseEscWritePlan(uint16_t *off0, uint16_t *len0,
uint16_t *off1, uint16_t *len1);
/**
* @brief 输入 PDO 写 ESC 的多段差分计划(未启用时回退稀疏/整包)。
* @param[out] offs 段偏移数组,至少 kEscPlanMaxSegs 项
* @param[out] lens 段长度数组,至少 kEscPlanMaxSegs 项
* @return 段数 1..kEscPlanMaxSegs;调用方逐段 HW_EscWriteIsr。
* @note 必须在 writeInputs()/s_in 发布之后、每拍一次调用。按三区差分:
* 段1(0..31 桥状态)、段2(32..543 电机密排,写时用稀疏有效长)、
* 段3+5(544..599)。变化后连续写 3 拍冲完 3-buffer 才允许跳过。
* 全段跳过时返回「末 2 字节」一段以完成 SM 事件。有副作用。
*/
static uint8_t escWritePlan(uint16_t *offs, uint16_t *lens);
/** escWritePlan 最多段数。 */
static constexpr uint8_t kEscPlanMaxSegs = 4U;
/** @brief 累计取走输出的周期数(主站在发过程数据的证据)。 */
static uint32_t outputCycles() { return s_outputCycles; }
/** @brief 累计交回输入的周期数。 */
static uint32_t inputCycles() { return s_inputCycles; }
/** @brief process() 完整填完一拍输入的序号。新鲜度救援看这个,而不是
* InputMapping 的 s_inMapSeq——后者只证明「在拷陈旧 s_in」。 */
static uint32_t processSeq() { return s_processSeq; }
/** @brief process() 被重入拦下的次数。持续增长 = 跨上下文抢占确实在发生
* (PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 互相打断),也就证明了
* 批量区跨轮次拼接的成因;恒为 0 则说明该机制推断有误。 */
static uint32_t reentryCount() { return s_reentryCount; }
/** @brief 是否已开始过程数据交换(收到过至少一个输出周期)。 */
static bool isExchanging() { return s_outputCycles != 0U; }
/**
* @brief 拷出最近一份输出过程数据快照(诊断用)。
* @param[out] dst 至少 kPdoOutBytes 字节。
*/
static void snapshotOutput(uint8_t *dst);
private:
/**
* @brief 填充输入 PDO 段 5 设备诊断区(尾部 8 字节)。
* @param[out] in 全长输入过程数据缓冲。
* @note 布局常量 ECAT_DIAG_OFF_* 见 ethercat_can_bridge.h;当前携带
* CPU 负载率(0.1% 单位) + 窗口序号,取自 sys/cpu_load.h。
*/
static void fillDiagTail(uint8_t *in);
/** @brief 最近一份主站下发的输出数据。
* @note alignas(4):让整块 memcpy(writeInputs / [DIFF-STATS] 的快照拷贝)按
* 字宽走而非逐字节。比对主循环本身不依赖它——已改成手拼 4 字节移位,
* 因为 -Oz 下 memcpy(&a,p,4) 会编成函数调用,见 ethercat_diff_stats.cpp。 */
alignas(4) static uint8_t s_out[kPdoOutBytes];
/** @brief 待交回主站的输入数据(已发布的完整镜像,writeInputs 只读它)。 */
alignas(4) static uint8_t s_in[kPdoInBytes];
/** @brief 输入 PDO 工作缓冲:buildInput 填这里,填完整块发布到 s_in。
* ECAT_Application() 会在 PDI_Isr / Sync0_Isr / Sync1_Isr / MainLoop 四个
* 上下文被调用并互相抢占,直接写 s_in 会让读侧取到跨轮次的半成品。 */
alignas(4) static uint8_t s_build[kPdoInBytes];
/** @brief 写 ESC 差分影子:段1 / 段2 / 段3+5 三区。 */
alignas(4) static uint8_t s_escSh1[32];
alignas(4) static uint8_t s_escSh2[ECAT_XRX_OFF_CH - 32U];
alignas(4) static uint8_t s_escSh3[kEcatPdoInBytes - ECAT_XRX_OFF_CH];
static uint8_t s_escFlush1;
static uint8_t s_escFlush2;
static uint8_t s_escFlush3;
static bool s_escSh1Valid;
static bool s_escSh2Valid;
static bool s_escSh3Valid;
/** @brief process() 被重入拦下的次数(诊断用,见 reentryCount())。 */
static volatile uint32_t s_reentryCount;
static volatile uint32_t s_outputCycles;
static volatile uint32_t s_inputCycles;
static volatile uint32_t s_processSeq;
};
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);
/** @brief 供 dc_app.c 新鲜度救援:process() 是否还在推进。 */
uint32_t EcatPdo_ProcessSeq(void);
/** @brief PDO_OutputMapping 探针命中并完成全量读后调用(本拍 s_out 已新鲜)。 */
void EcatPdo_NoteOutputMapped(void);
}
#endif /* COMM_ETHERCAT_PDO_H */