Files
DCU_APP/GD32E508_DCU_APP/comm/ethercat_pdo.h
T

174 lines
8.7 KiB
C++
Raw Normal View History

/**
* @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 */