feat(ethercat): 添加EtherCAT从站功能支持并优化同步机制
- 添加CAN反馈和ID快照SPI命令文件 - 移除EXMC组件依赖,精简项目配置 - 添加RAM函数段链接脚本,优化Flash等待周期抖动 - 实现EscExmcBus类,提供16位并行总线访问LAN9252 - 实现EscLan9252驱动,支持寄存器和PDI内存访问 - 实现EthercatSlave类,采用三缓冲机制进行PDO数据交换 - 实现EthercatSync同步管理,支持SYNC0/SYNC1中断和定时器模式 - 添加ISR执行时间统计和诊断功能 - 分离周期路径和事件路径通知机制,避免RMW竞态
This commit is contained in:
@@ -5,8 +5,6 @@
|
||||
#include "comm/esc_exmc_bus.h"
|
||||
#include "sys/config.h"
|
||||
|
||||
#if CONFIG_ETHERCAT_ENABLE
|
||||
|
||||
extern "C" {
|
||||
#include "gd32e50x_rcu.h"
|
||||
#include "gd32e50x_gpio.h"
|
||||
@@ -68,32 +66,18 @@ void EscExmcBus::init()
|
||||
exmc_norsram_init(&norsram_init);
|
||||
exmc_norsram_enable(CONFIG_ESC_EXMC_BANK_NOR_SRAM_REGION1);
|
||||
#else
|
||||
/* EXMC 外设库未安装:仅占位,CONFIG_ETHERCAT_ENABLE=1 时需先安装 EXMC 组件。 */
|
||||
/* EXMC 外设库未安装:仅占位,启用 EtherCAT 时需先安装 EXMC 组件。 */
|
||||
#endif
|
||||
}
|
||||
|
||||
__attribute__((section(".ramfunc"), noinline))
|
||||
uint16_t EscExmcBus::read16(uint32_t offset)
|
||||
{
|
||||
return ESC_EXMC_REG16(offset);
|
||||
}
|
||||
|
||||
__attribute__((section(".ramfunc"), noinline))
|
||||
void EscExmcBus::write16(uint32_t offset, uint16_t data)
|
||||
{
|
||||
ESC_EXMC_REG16(offset) = data;
|
||||
}
|
||||
|
||||
void EscExmcBus::readBlock(uint32_t offset, uint16_t *buf, size_t count)
|
||||
{
|
||||
for (size_t i = 0U; i < count; ++i) {
|
||||
buf[i] = ESC_EXMC_REG16(offset + (uint32_t)(i * 2U));
|
||||
}
|
||||
}
|
||||
|
||||
void EscExmcBus::writeBlock(uint32_t offset, const uint16_t *buf, size_t count)
|
||||
{
|
||||
for (size_t i = 0U; i < count; ++i) {
|
||||
ESC_EXMC_REG16(offset + (uint32_t)(i * 2U)) = buf[i];
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CONFIG_ETHERCAT_ENABLE */
|
||||
|
||||
@@ -19,35 +19,11 @@ public:
|
||||
/** @brief 初始化 EXMC NOR/SRAM Region1 与全部 GPIO(AF 复用)。 */
|
||||
static void init();
|
||||
|
||||
/**
|
||||
* @brief 从指定偏移读取 16-bit 数据。
|
||||
* @param[in] offset 地址偏移(字节)。
|
||||
* @return uint16_t 读取的 16-bit 数据。
|
||||
*/
|
||||
/** @brief ISR-only:仅供 preempt=0 调用,无锁。 */
|
||||
static uint16_t read16(uint32_t offset);
|
||||
|
||||
/**
|
||||
* @brief 向指定偏移写入 16-bit 数据。
|
||||
* @param[in] offset 地址偏移(字节)。
|
||||
* @param[in] data 待写入的 16-bit 数据。
|
||||
*/
|
||||
/** @brief ISR-only:仅供 preempt=0 调用,无锁。 */
|
||||
static void write16(uint32_t offset, uint16_t data);
|
||||
|
||||
/**
|
||||
* @brief 从指定偏移连续读取一块 16-bit 数据。
|
||||
* @param[in] offset 地址偏移(字节)。
|
||||
* @param[out] buf 输出缓冲(以 uint16_t 为单位)。
|
||||
* @param[in] count 读取的 16-bit 半字数量。
|
||||
*/
|
||||
static void readBlock(uint32_t offset, uint16_t *buf, size_t count);
|
||||
|
||||
/**
|
||||
* @brief 向指定偏移连续写入一块 16-bit 数据。
|
||||
* @param[in] offset 地址偏移(字节)。
|
||||
* @param[in] buf 数据缓冲(以 uint16_t 为单位)。
|
||||
* @param[in] count 写入的 16-bit 半字数量。
|
||||
*/
|
||||
static void writeBlock(uint32_t offset, const uint16_t *buf, size_t count);
|
||||
};
|
||||
|
||||
#endif /* COMM_ESC_EXMC_BUS_H */
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
#include "comm/esc_lan9252.h"
|
||||
#include "comm/esc_exmc_bus.h"
|
||||
#include "sys/config.h"
|
||||
|
||||
#if CONFIG_ETHERCAT_ENABLE
|
||||
#include "RTE_Components.h"
|
||||
#include CMSIS_device_header
|
||||
|
||||
namespace {
|
||||
/* LAN9252 ESC 关键寄存器地址(字节偏移,EXMC 访问) */
|
||||
@@ -20,16 +20,55 @@ constexpr uint16_t LAN9252_PDI_BASE_OFFSET = 0x1000U;
|
||||
|
||||
void EscLan9252::init()
|
||||
{
|
||||
/* 使能 DWT 周期计数器,供 periodicIsr 执行时间测量 */
|
||||
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
||||
DWT->CYCCNT = 0U;
|
||||
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
||||
|
||||
EscExmcBus::init();
|
||||
/* TODO: 配置 ESC 中断使能、AL 控制等基本寄存器(留待协议栈集成阶段实现) */
|
||||
|
||||
/* ============================================================
|
||||
* TODO: 完整 ESC 寄存器配置(需 SSC 或等价协议栈代码,本 spec 不实现)
|
||||
* ============================================================
|
||||
* 当前仅完成 EXMC 总线初始化与芯片 ID 读取。要使 SYNC0 实际触发,还需配置:
|
||||
*
|
||||
* 1. SyncManager(SM)初始化:
|
||||
* - SM0/SM1:邮箱输入/输出(IN/OUT),地址 0x0520-0x0530
|
||||
* - SM2:TxPDO(输入),地址 0x0530,配置为 3-buffer 模式
|
||||
* - SM3:RxPDO(输出),地址 0x0540,配置为 3-buffer 模式
|
||||
* - 各 SM 的 StartAddr/Length/Control/Status/Activate 寄存器
|
||||
*
|
||||
* 2. FMMU(Fieldbus Memory Mapping Unit)配置:
|
||||
* - FMMU0:TxPDO 映射(逻辑地址 → 物理地址,读)
|
||||
* - FMMU1:RxPDO 映射(逻辑地址 → 物理地址,写)
|
||||
* - 地址 0x0580-0x0590
|
||||
*
|
||||
* 3. AL Control(0x0120):写入 0x08 请求 OP 状态(需主站配合完成状态迁移)
|
||||
*
|
||||
* 4. SYNC0 生成寄存器:
|
||||
* - 0x0981-0x0982:SYNC0 周期(cycle time,单位 ns)
|
||||
* - 0x0983:SYNC0 脉冲长度
|
||||
* - 0x0984:SYNC0 控制(使能/触发模式)
|
||||
* - 需主站 DC 同步后由 ESC 自动生成 SYNC0 脉冲
|
||||
*
|
||||
* 这些步骤通常由 EtherCAT SSC(Slave Stack Code)或等价协议栈代码完成,
|
||||
* 涉及 AL 状态机管理、邮箱通信、PDO 映射协商等复杂逻辑。
|
||||
* 本 spec 仅通过 EthercatSlave::isEscReady() 的 AL 状态守卫使
|
||||
* "SYNC0 不会触发"在运行期可观测,完整配置留待 SSC 集成 spec。
|
||||
* ============================================================ */
|
||||
}
|
||||
|
||||
bool EscLan9252::selfTest()
|
||||
{
|
||||
const uint16_t id_low = readReg(LAN9252_REG_CHIP_ID_L);
|
||||
const uint16_t id_high = readReg(LAN9252_REG_CHIP_ID_H);
|
||||
const uint32_t chip_id = ((uint32_t)id_high << 16) | id_low;
|
||||
return (chip_id == LAN9252_CHIP_ID_VALUE);
|
||||
return (readChipId() == LAN9252_CHIP_ID_VALUE);
|
||||
}
|
||||
|
||||
uint32_t EscLan9252::readChipId()
|
||||
{
|
||||
/* INIT-ONLY:init 阶段中断未使能,但仍走 initOnly 路径保持一致性 */
|
||||
const uint16_t id_low = initOnlyReadReg(LAN9252_REG_CHIP_ID_L);
|
||||
const uint16_t id_high = initOnlyReadReg(LAN9252_REG_CHIP_ID_H);
|
||||
return ((uint32_t)id_high << 16) | id_low;
|
||||
}
|
||||
|
||||
uint16_t EscLan9252::readReg(uint16_t addr)
|
||||
@@ -43,6 +82,7 @@ void EscLan9252::writeReg(uint16_t addr, uint16_t val)
|
||||
EscExmcBus::write16((uint32_t)addr << 1U, val);
|
||||
}
|
||||
|
||||
__attribute__((section(".ramfunc"), noinline))
|
||||
void EscLan9252::readPdi(uint16_t offset, uint8_t *buf, size_t len)
|
||||
{
|
||||
if (buf == nullptr || len == 0U) {
|
||||
@@ -63,6 +103,7 @@ void EscLan9252::readPdi(uint16_t offset, uint8_t *buf, size_t len)
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((section(".ramfunc"), noinline))
|
||||
void EscLan9252::writePdi(uint16_t offset, const uint8_t *buf, size_t len)
|
||||
{
|
||||
if (buf == nullptr || len == 0U) {
|
||||
@@ -79,4 +120,25 @@ void EscLan9252::writePdi(uint16_t offset, const uint8_t *buf, size_t len)
|
||||
}
|
||||
}
|
||||
|
||||
#endif /* CONFIG_ETHERCAT_ENABLE */
|
||||
/* ---- initOnly* 接口:INIT-ONLY,仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用 ----
|
||||
* 运行期禁止调用;需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。
|
||||
* 注:不再使用 PRIMASK 关中断(违反项目硬约束"Critical sections MUST use BASEPRI")。 */
|
||||
uint16_t EscLan9252::initOnlyReadReg(uint16_t addr)
|
||||
{
|
||||
return readReg(addr);
|
||||
}
|
||||
|
||||
void EscLan9252::initOnlyWriteReg(uint16_t addr, uint16_t val)
|
||||
{
|
||||
writeReg(addr, val);
|
||||
}
|
||||
|
||||
void EscLan9252::initOnlyReadPdi(uint16_t offset, uint8_t *buf, size_t len)
|
||||
{
|
||||
readPdi(offset, buf, len);
|
||||
}
|
||||
|
||||
void EscLan9252::initOnlyWritePdi(uint16_t offset, const uint8_t *buf, size_t len)
|
||||
{
|
||||
writePdi(offset, buf, len);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
* @file esc_lan9252.h
|
||||
* @brief LAN9252 EtherCAT 从站控制器(ESC)驱动接口。
|
||||
* @note 通过 EscExmcBus 16-bit 并行总线访问 LAN9252 寄存器与 PDI 内存。
|
||||
*
|
||||
* 接口调用约束(避免 EXMC 总线争用):
|
||||
* - readReg/writeReg/readPdi/writePdi:ISR-only,仅供 preempt=0 的 periodicIsr() 调用,无锁。
|
||||
* - initOnlyReadReg/initOnlyWriteReg/initOnlyReadPdi/initOnlyWritePdi:INIT-ONLY,仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。
|
||||
*
|
||||
* PDI offset 语义:offset 参数为 SyncManager 内存区相对偏移,输入 PDO(TxPDO)与输出 PDO(RxPDO)必须使用不同 offset,参见 CONFIG_ESC_PDI_INPUT_OFFSET / CONFIG_ESC_PDI_OUTPUT_OFFSET。
|
||||
*/
|
||||
#ifndef COMM_ESC_LAN9252_H
|
||||
#define COMM_ESC_LAN9252_H
|
||||
@@ -25,34 +31,34 @@ public:
|
||||
static bool selfTest();
|
||||
|
||||
/**
|
||||
* @brief 读取 ESC 寄存器(16-bit)。
|
||||
* @param[in] addr 寄存器地址(ESC 寄存器空间偏移)。
|
||||
* @return uint16_t 寄存器值。
|
||||
* @brief 读取 ESC 芯片 ID(INIT-ONLY,供 init 打印诊断用)。
|
||||
* @return uint32_t 32-bit 芯片 ID(期望值 0x87654321)。
|
||||
*/
|
||||
static uint32_t readChipId();
|
||||
|
||||
/** @brief ISR-only:仅供 preempt=0 的 periodicIsr() 调用,无锁。主循环必须用 initOnly 版本。 */
|
||||
static uint16_t readReg(uint16_t addr);
|
||||
|
||||
/**
|
||||
* @brief 写入 ESC 寄存器(16-bit)。
|
||||
* @param[in] addr 寄存器地址。
|
||||
* @param[in] val 待写入值。
|
||||
*/
|
||||
/** @brief ISR-only:仅供 preempt=0 的 periodicIsr() 调用,无锁。主循环必须用 initOnly 版本。 */
|
||||
static void writeReg(uint16_t addr, uint16_t val);
|
||||
|
||||
/**
|
||||
* @brief 从 PDI 内存读取一块数据。
|
||||
* @param[in] offset PDI 偏移(字节)。
|
||||
* @param[out] buf 输出缓冲。
|
||||
* @param[in] len 读取字节数。
|
||||
*/
|
||||
/** @brief ISR-only:仅供 preempt=0 的 periodicIsr() 调用,无锁。主循环必须用 initOnly 版本。 */
|
||||
static void readPdi(uint16_t offset, uint8_t *buf, size_t len);
|
||||
|
||||
/**
|
||||
* @brief 向 PDI 内存写入一块数据。
|
||||
* @param[in] offset PDI 偏移(字节)。
|
||||
* @param[in] buf 数据缓冲。
|
||||
* @param[in] len 写入字节数。
|
||||
*/
|
||||
/** @brief ISR-only:仅供 preempt=0 的 periodicIsr() 调用,无锁。主循环必须用 initOnly 版本。 */
|
||||
static void writePdi(uint16_t offset, const uint8_t *buf, size_t len);
|
||||
|
||||
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */
|
||||
static uint16_t initOnlyReadReg(uint16_t addr);
|
||||
|
||||
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */
|
||||
static void initOnlyWriteReg(uint16_t addr, uint16_t val);
|
||||
|
||||
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */
|
||||
static void initOnlyReadPdi(uint16_t offset, uint8_t *buf, size_t len);
|
||||
|
||||
/** @brief INIT-ONLY:仅允许在 EthercatSync::init() 使能 EXTI NVIC 之前调用;运行期禁止调用,需 ESC 寄存器访问须通过 SYNC0 ISR 代访队列(本 spec 不涵盖)。 */
|
||||
static void initOnlyWritePdi(uint16_t offset, const uint8_t *buf, size_t len);
|
||||
};
|
||||
|
||||
#endif /* COMM_ESC_LAN9252_H */
|
||||
|
||||
@@ -8,46 +8,92 @@
|
||||
#include "sys/task_manager.h"
|
||||
#include "main.h"
|
||||
|
||||
#if CONFIG_ETHERCAT_ENABLE
|
||||
PdoBuffer EthercatSlave::s_inputs[3] = {};
|
||||
PdoBuffer EthercatSlave::s_outputs[3] = {};
|
||||
PdoBuffer EthercatSlave::s_appInputs = {};
|
||||
PdoBuffer EthercatSlave::s_appOutputs = {};
|
||||
volatile uint8_t EthercatSlave::s_inputWriteIdx = 0U;
|
||||
volatile uint8_t EthercatSlave::s_outputReadIdx = 0U;
|
||||
volatile uint16_t EthercatSlave::s_periodicSeq = 0U;
|
||||
uint16_t EthercatSlave::s_processedSeq = 0U;
|
||||
volatile uint32_t EthercatSlave::s_isrMaxCycles = 0U;
|
||||
volatile uint32_t EthercatSlave::s_isrAvgCycles = 0U;
|
||||
volatile uint32_t EthercatSlave::s_isrCount = 0U;
|
||||
bool EthercatSlave::s_escReady = false;
|
||||
uint32_t EthercatSlave::s_chipId = 0U;
|
||||
uint8_t EthercatSlave::s_alState = 0U;
|
||||
|
||||
PdoBuffer EthercatSlave::s_inputs[2] = {};
|
||||
PdoBuffer EthercatSlave::s_outputs[2] = {};
|
||||
volatile uint8_t EthercatSlave::s_inputReadIndex = 0U;
|
||||
volatile uint8_t EthercatSlave::s_outputWriteIndex = 0U;
|
||||
/* ISR 执行时间评估:
|
||||
* - 若 DWT 实测 max > CONFIG_ETHERCAT_ISR_MAX_US_THRESHOLD(5µs),需评估:
|
||||
* a) 减小 CONFIG_ETHERCAT_PDO_SIZE(默认 32 字节)
|
||||
* b) 优化 EXMC 时序参数(esc_exmc_bus.cpp 中 read_latency/write_latency)
|
||||
* - 若 max ≤ 5µs,接受现状;preempt=0 阻塞窗口 ≤ 5µs,CAN/SPI 中断需容忍此窗口。
|
||||
* SPI0 从机在此窗口内不会 RX 溢出(假设 SPI 时钟 ≤ 10MHz,5µs ≥ 50 bit,远小于一帧)。 */
|
||||
|
||||
void EthercatSlave::init()
|
||||
{
|
||||
EscLan9252::init();
|
||||
if (EscLan9252::selfTest()) {
|
||||
/* ESC 自检通过:TODO 启用周期同步 */
|
||||
|
||||
/* 第 1 层调试可观测性:读取芯片 ID 并打印 selfTest 结果 */
|
||||
s_chipId = EscLan9252::readChipId();
|
||||
const bool chipIdOk = (s_chipId == 0x87654321U);
|
||||
|
||||
/* AL 状态守卫:仅在芯片 ID 匹配时读取 AL 状态(ID 不匹配时 EXMC 可能读回 0/0xFFFF,无意义)。
|
||||
* AL ≥ SAFEOP(0x04) 才认为 SYNC0 可触发;完整 ESC 配置(SM/FMMU/AL Control/SYNC0)需 SSC 集成。 */
|
||||
s_alState = 0U;
|
||||
if (chipIdOk) {
|
||||
s_alState = (uint8_t)(EscLan9252::initOnlyReadReg(0x0130U) & 0x000FU);
|
||||
}
|
||||
s_inputReadIndex = 0U;
|
||||
s_outputWriteIndex = 0U;
|
||||
s_escReady = chipIdOk && (s_alState >= AL_STATE_SAFEOP);
|
||||
|
||||
if (!chipIdOk) {
|
||||
printf("[ECAT] ESC chip id=0x%08lX selfTest=FAIL\r\n", (unsigned long)s_chipId);
|
||||
printf("[ECAT] ESC NOT ready, sync ISR disabled (check EXMC wiring/timing)\r\n");
|
||||
} else if (s_alState < AL_STATE_SAFEOP) {
|
||||
printf("[ECAT] ESC chip id=0x%08lX selfTest=PASS, but AL state=0x%02X < SAFEOP(0x%02X)\r\n",
|
||||
(unsigned long)s_chipId, (unsigned)s_alState, (unsigned)AL_STATE_SAFEOP);
|
||||
printf("[ECAT] SYNC0 will not fire, full ESC config requires SSC integration\r\n");
|
||||
} else {
|
||||
printf("[ECAT] ESC ready, chip id=0x%08lX, AL state=0x%02X\r\n",
|
||||
(unsigned long)s_chipId, (unsigned)s_alState);
|
||||
}
|
||||
|
||||
s_inputWriteIdx = 0U;
|
||||
s_outputReadIdx = 0U;
|
||||
s_periodicSeq = 0U;
|
||||
s_processedSeq = 0U;
|
||||
}
|
||||
|
||||
__attribute__((section(".ramfunc")))
|
||||
__attribute__((section(".ramfunc"), noinline))
|
||||
void EthercatSlave::periodicIsr()
|
||||
{
|
||||
/* 1. 读取输入 PDO 到另一块缓冲(当前可读缓冲的配对) */
|
||||
const uint8_t write_idx = s_inputReadIndex ^ 1U;
|
||||
EscLan9252::readPdi(0x0000U, s_inputs[write_idx].data, CONFIG_ETHERCAT_PDO_SIZE);
|
||||
s_inputs[write_idx].len = CONFIG_ETHERCAT_PDO_SIZE;
|
||||
const uint32_t cycStart = DWT->CYCCNT;
|
||||
|
||||
/* 2. 写入输出 PDO(从应用层未写入的那个缓冲读取) */
|
||||
const uint8_t out_read_idx = s_outputWriteIndex ^ 1U;
|
||||
EscLan9252::writePdi(0x0000U, s_outputs[out_read_idx].data, s_outputs[out_read_idx].len);
|
||||
/* 1. 读取输入 PDO 到当前写入缓冲 */
|
||||
EscLan9252::readPdi(CONFIG_ESC_PDI_INPUT_OFFSET, s_inputs[s_inputWriteIdx].data, CONFIG_ETHERCAT_PDO_SIZE);
|
||||
s_inputs[s_inputWriteIdx].len = CONFIG_ETHERCAT_PDO_SIZE;
|
||||
s_inputWriteIdx = (s_inputWriteIdx + 1U) % 3U;
|
||||
|
||||
/* 3. 翻转输入缓冲指针,使新完成的缓冲成为可读缓冲 */
|
||||
s_inputReadIndex = write_idx;
|
||||
/* 2. 写入输出 PDO(从当前读取缓冲) */
|
||||
EscLan9252::writePdi(CONFIG_ESC_PDI_OUTPUT_OFFSET, s_outputs[s_outputReadIdx].data, s_outputs[s_outputReadIdx].len);
|
||||
s_outputReadIdx = (s_outputReadIdx + 1U) % 3U;
|
||||
|
||||
/* 4. 置位非实时任务,通知主循环处理应用层 PDO 映射 */
|
||||
TaskManager::set(1UL << TASK_ETHERCAT_PROCESS);
|
||||
/* 3. 递增周期序列号通知主循环(替代 TaskManager::set,避免 BASEPRI 竞态) */
|
||||
s_periodicSeq++;
|
||||
|
||||
const uint32_t elapsed = DWT->CYCCNT - cycStart;
|
||||
s_isrCount++;
|
||||
if (elapsed > s_isrMaxCycles) {
|
||||
s_isrMaxCycles = elapsed;
|
||||
}
|
||||
/* 滑动平均:avg = avg + (elapsed - avg) / 16 (近似 EMA,避免除法精度问题) */
|
||||
s_isrAvgCycles = s_isrAvgCycles + ((int32_t)elapsed - (int32_t)s_isrAvgCycles) / 16;
|
||||
}
|
||||
|
||||
const PdoBuffer* EthercatSlave::getLatestInputs()
|
||||
{
|
||||
/* s_inputReadIndex 始终指向最新完成的输入缓冲 */
|
||||
return &s_inputs[s_inputReadIndex];
|
||||
/* 返回最新完成的缓冲 = (writeIdx + 2) % 3,与 ISR 当前写入位置隔一个 */
|
||||
return &s_inputs[(s_inputWriteIdx + 2U) % 3U];
|
||||
}
|
||||
|
||||
void EthercatSlave::setOutputs(const PdoBuffer *outputs)
|
||||
@@ -55,16 +101,53 @@ void EthercatSlave::setOutputs(const PdoBuffer *outputs)
|
||||
if (outputs == nullptr) {
|
||||
return;
|
||||
}
|
||||
/* 写入应用层缓冲(ISR 当前不读取的那个) */
|
||||
const uint8_t write_idx = s_outputWriteIndex ^ 1U;
|
||||
/* 写入与 ISR 当前读取位置隔 2 个缓冲,提供 2 周期宽限。
|
||||
* 2 周期宽限下拷贝竞态窗口消除,前提是主循环处理时间 < 2 × 周期时间。 */
|
||||
const uint8_t write_idx = (s_outputReadIdx + 2U) % 3U;
|
||||
s_outputs[write_idx] = *outputs;
|
||||
s_outputWriteIndex = write_idx;
|
||||
}
|
||||
|
||||
void EthercatSlave::process()
|
||||
{
|
||||
/* 应用层 PDO 映射入口(非实时,由主循环 TASK_ETHERCAT_PROCESS 调用)。
|
||||
* TODO: 将 getLatestInputs() 数据映射到应用对象字典;将应用输出填入 setOutputs()。 */
|
||||
/* 应用层 PDO 桥接(非实时,由主循环周期序列号或 ESC IRQ 触发)。
|
||||
* 运行期 PDO 交换仅通过 getLatestInputs()/setOutputs() 三缓冲进行;ESC 寄存器访问仅限 init 阶段。
|
||||
*
|
||||
* 框架级桥接:从三缓冲取最新输入到 s_appInputs,把 s_appOutputs 推回三缓冲。
|
||||
* TODO: 应用对象字典映射(具体输入字节对应哪个应用变量,如字节 0-3=温度浮点),
|
||||
* 由后续应用层 spec 定义;应用层通过 getApplicationInputs()/setApplicationOutputs() 访问快照。 */
|
||||
const PdoBuffer *latest = getLatestInputs();
|
||||
if (latest != nullptr) {
|
||||
s_appInputs = *latest;
|
||||
}
|
||||
setOutputs(&s_appOutputs);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_ETHERCAT_ENABLE */
|
||||
const PdoBuffer* EthercatSlave::getApplicationInputs()
|
||||
{
|
||||
return &s_appInputs;
|
||||
}
|
||||
|
||||
void EthercatSlave::setApplicationOutputs(const PdoBuffer *outputs)
|
||||
{
|
||||
if (outputs == nullptr) {
|
||||
return;
|
||||
}
|
||||
s_appOutputs = *outputs;
|
||||
}
|
||||
|
||||
bool EthercatSlave::hasPendingPeriodic()
|
||||
{
|
||||
return s_periodicSeq != s_processedSeq;
|
||||
}
|
||||
|
||||
void EthercatSlave::markPeriodicProcessed()
|
||||
{
|
||||
s_processedSeq = s_periodicSeq;
|
||||
}
|
||||
|
||||
void EthercatSlave::getIsrStats(uint32_t *maxCycles, uint32_t *avgCycles, uint32_t *count)
|
||||
{
|
||||
if (maxCycles) *maxCycles = s_isrMaxCycles;
|
||||
if (avgCycles) *avgCycles = s_isrAvgCycles;
|
||||
if (count) *count = s_isrCount;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
/**
|
||||
* @file ethercat_slave.h
|
||||
* @brief EtherCAT 从站周期处理接口与 PDO 双缓冲(ping-pong)结构。
|
||||
* @brief EtherCAT 从站周期处理接口与 PDO 三缓冲(triple buffer)结构。
|
||||
* @note 周期 ISR 调用 periodicIsr() 完成输入 PDO 读取 + 输出 PDO 写入;
|
||||
* 应用层通过 getLatestInputs()/setOutputs() 与周期路径无锁交换数据。
|
||||
*
|
||||
* 通知路径分离:
|
||||
* 周期路径(periodicIsr)用 s_periodicSeq 序列号通知主循环;
|
||||
* 事件路径(ESC IRQ)用 TASK_ETHERCAT_PROCESS 标志。
|
||||
* 两者独立,避免 preempt=0 ISR 与 BASEPRI 临界区的 RMW 竞态。
|
||||
*/
|
||||
#ifndef COMM_ETHERCAT_SLAVE_H
|
||||
#define COMM_ETHERCAT_SLAVE_H
|
||||
@@ -13,9 +18,16 @@
|
||||
/** @brief 默认 PDO 大小(可按需调整)。 */
|
||||
constexpr size_t CONFIG_ETHERCAT_PDO_SIZE = 32U;
|
||||
|
||||
/** @brief EtherCAT AL 状态码(EtherCAT 协议定义,ESC AL Status 寄存器低 4 位)。 */
|
||||
constexpr uint8_t AL_STATE_INIT = 0x01U; /**< INIT 状态。 */
|
||||
constexpr uint8_t AL_STATE_PREOP = 0x02U; /**< PREOP 状态。 */
|
||||
constexpr uint8_t AL_STATE_BOOT = 0x03U; /**< BOOT 状态。 */
|
||||
constexpr uint8_t AL_STATE_SAFEOP = 0x04U; /**< SAFEOP 状态(SYNC0 可触发下限)。 */
|
||||
constexpr uint8_t AL_STATE_OP = 0x08U; /**< OP 状态。 */
|
||||
|
||||
/**
|
||||
* @brief PDO 数据缓冲(输入/输出共用结构)。
|
||||
* @note 周期 ISR 与应用层通过双缓冲无锁交换。
|
||||
* @note 周期 ISR 与应用层通过三缓冲无锁交换。
|
||||
*/
|
||||
struct PdoBuffer {
|
||||
uint8_t data[CONFIG_ETHERCAT_PDO_SIZE] = {0}; /**< 数据载荷。 */
|
||||
@@ -24,44 +36,111 @@ struct PdoBuffer {
|
||||
|
||||
/**
|
||||
* @brief EtherCAT 从站周期处理。
|
||||
* @note 周期路径(periodicIsr)必须极简:读输入→写输出→翻转指针→置位任务。
|
||||
* @note 周期路径(periodicIsr)必须极简:读输入→写输出→模 3 轮转指针→递增序列号。
|
||||
*/
|
||||
class EthercatSlave {
|
||||
public:
|
||||
/** @brief 初始化 ESC 与双缓冲。 */
|
||||
/** @brief 初始化 ESC 与三缓冲。 */
|
||||
static void init();
|
||||
|
||||
/**
|
||||
* @brief 周期中断处理(由 SYNC0 EXTI 触发)。
|
||||
* @note 放入 RAM 执行段(.ramfunc)以降低 Flash 等待带来的抖动。
|
||||
* 流程:读输入 PDO → 写输出 PDO → 翻转输入缓冲指针 → 置位 TASK_ETHERCAT_PROCESS。
|
||||
* 流程:读输入 PDO → 写输出 PDO → 模 3 轮转索引 → 递增 s_periodicSeq。
|
||||
* 不再调用 TaskManager::set,避免 preempt=0 与 BASEPRI 临界区的 RMW 竞态。
|
||||
*/
|
||||
static void periodicIsr();
|
||||
|
||||
/**
|
||||
* @brief 获取最新输入 PDO(主循环调用,无锁)。
|
||||
* @return const PdoBuffer* 最新输入缓冲只读指针;周期 ISR 不会修改该缓冲。
|
||||
* @return const PdoBuffer* 最新输入缓冲只读指针;返回与 ISR 当前写入位置隔一个的缓冲。
|
||||
* @note 三缓冲容忍主循环跨 1 个周期,主循环处理必须 < 2 × 周期时间。
|
||||
*/
|
||||
static const PdoBuffer* getLatestInputs();
|
||||
|
||||
/**
|
||||
* @brief 设置下次周期要发送的输出 PDO(主循环调用)。
|
||||
* @param[in] outputs 输出数据指针(内容会被拷贝到内部输出缓冲)。
|
||||
* @note 调用方在 ISR 不持有期间调用即可,内部使用乒乓缓冲避免竞争。
|
||||
* @note 写入与 ISR 当前读取位置隔一个的缓冲,无需任何临界区。
|
||||
*/
|
||||
static void setOutputs(const PdoBuffer *outputs);
|
||||
|
||||
/**
|
||||
* @brief 应用层 PDO 映射入口(非实时,由 TASK_ETHERCAT_PROCESS 调用)。
|
||||
* @brief 应用层 PDO 映射入口(非实时,由 TASK_ETHERCAT_PROCESS 或周期序列号触发)。
|
||||
* @note 留接口,具体映射逻辑由协议栈后续实现。
|
||||
*/
|
||||
static void process();
|
||||
|
||||
/**
|
||||
* @brief 获取应用层输入 PDO 快照(主循环调用)。
|
||||
* @return const PdoBuffer* 最近一次 process() 拷贝的输入快照只读指针。
|
||||
* @note 返回的指针在下次 process() 调用前有效;应用层应在 process() 后立即读取。
|
||||
*/
|
||||
static const PdoBuffer* getApplicationInputs();
|
||||
|
||||
/**
|
||||
* @brief 设置应用层输出 PDO 快照(主循环调用)。
|
||||
* @param[in] outputs 输出数据指针(内容会被拷贝到内部快照)。
|
||||
* @note 下次 process() 调用时,内部快照通过 setOutputs() 推入三缓冲。
|
||||
*/
|
||||
static void setApplicationOutputs(const PdoBuffer *outputs);
|
||||
|
||||
/**
|
||||
* @brief 检查是否有新的周期数据待主循环处理。
|
||||
* @return bool true 表示 s_periodicSeq 与已处理序号不同。
|
||||
* @note 主循环 runPending() 优先调用此接口,替代原 TASK_ETHERCAT_PROCESS 周期路径。
|
||||
*/
|
||||
static bool hasPendingPeriodic();
|
||||
|
||||
/**
|
||||
* @brief 标记当前周期数据已被主循环处理。
|
||||
* @note 调用此接口前应已调用 taskEthercatProcess() 完成本次 PDO 映射。
|
||||
*/
|
||||
static void markPeriodicProcessed();
|
||||
|
||||
/** @brief 读取 ISR 执行时间诊断(max/avg 单位:CPU 周期数,除以 180 得 µs)。 */
|
||||
static void getIsrStats(uint32_t *maxCycles, uint32_t *avgCycles, uint32_t *count);
|
||||
|
||||
/** @brief ESC 自检是否通过(init 后可查询,用于决定是否使能同步中断)。 */
|
||||
static bool isEscReady() { return s_escReady; }
|
||||
|
||||
/** @brief 读取 ESC 芯片 ID(供诊断打印;未就绪时为上次读到的值)。 */
|
||||
static uint32_t getChipId() { return s_chipId; }
|
||||
|
||||
/** @brief 读取 ESC AL 状态码(供诊断打印与 SSC 集成;未就绪时为上次读到的值)。 */
|
||||
static uint8_t getAlState() { return s_alState; }
|
||||
|
||||
private:
|
||||
static PdoBuffer s_inputs[2]; /**< 输入双缓冲(pong-pong)。 */
|
||||
static PdoBuffer s_outputs[2]; /**< 输出双缓冲(ping-pong)。 */
|
||||
static volatile uint8_t s_inputReadIndex; /**< ISR 当前写入索引;getLatestInputs 读另一个。 */
|
||||
static volatile uint8_t s_outputWriteIndex; /**< 应用层写入索引;ISR 读另一个。 */
|
||||
/**
|
||||
* @brief 输入三缓冲。
|
||||
* ISR 按 s_inputWriteIdx 模 3 轮转写入;getLatestInputs() 读取 (writeIdx + 2) % 3。
|
||||
*/
|
||||
static PdoBuffer s_inputs[3];
|
||||
/**
|
||||
* @brief 输出三缓冲。
|
||||
* ISR 按 s_outputReadIdx 模 3 轮转读取;setOutputs() 写入 (readIdx + 2) % 3,
|
||||
* 与 ISR 当前读取位置隔 2 个缓冲,提供 2 周期宽限消除拷贝竞态。
|
||||
*/
|
||||
static PdoBuffer s_outputs[3];
|
||||
/** @brief 应用层输入快照(主循环访问,process() 从三缓冲拷贝;无需三缓冲)。 */
|
||||
static PdoBuffer s_appInputs;
|
||||
/** @brief 应用层输出快照(主循环访问,process() 推入三缓冲;无需三缓冲)。 */
|
||||
static PdoBuffer s_appOutputs;
|
||||
static volatile uint8_t s_inputWriteIdx; /**< ISR 当前写入索引;模 3 轮转。 */
|
||||
static volatile uint8_t s_outputReadIdx; /**< ISR 当前读取索引;模 3 轮转。 */
|
||||
|
||||
/** @brief 周期路径通知序列号(ISR 单字原子递增,主循环比对;uint16_t 提供 6.55s 回绕周期)。 */
|
||||
static volatile uint16_t s_periodicSeq;
|
||||
/** @brief 主循环已处理序号(由 hasPendingPeriodic/markPeriodicProcessed 维护)。 */
|
||||
static uint16_t s_processedSeq;
|
||||
|
||||
static volatile uint32_t s_isrMaxCycles; /**< ISR 最大执行周期数(180MHz 基准)。 */
|
||||
static volatile uint32_t s_isrAvgCycles; /**< ISR 平均执行周期数(滑动累计)。 */
|
||||
static volatile uint32_t s_isrCount; /**< ISR 执行次数计数。 */
|
||||
|
||||
static bool s_escReady; /**< ESC 自检结果(init 后置位)。 */
|
||||
static uint32_t s_chipId; /**< ESC 芯片 ID(诊断用)。 */
|
||||
static uint8_t s_alState; /**< ESC AL 状态码(init 后可查询,供 isEscReady 守卫)。 */
|
||||
};
|
||||
|
||||
#endif /* COMM_ETHERCAT_SLAVE_H */
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
#include "sys/task_manager.h"
|
||||
#include "main.h"
|
||||
|
||||
#if CONFIG_ETHERCAT_ENABLE
|
||||
|
||||
extern "C" {
|
||||
#include "gd32e50x_rcu.h"
|
||||
#include "gd32e50x_gpio.h"
|
||||
@@ -68,13 +66,29 @@ void EthercatSync::init()
|
||||
timer_interrupt_enable(TIMER0, TIMER_INT_UP);
|
||||
nvic_irq_enable(TIMER0_IRQn, CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC, 0U);
|
||||
timer_enable(TIMER0);
|
||||
|
||||
/* ESC IRQ:PB1 -> EXTI1,下降沿,preempt=1 */
|
||||
gpio_exti_source_select(CONFIG_ESC_IRQ_EXTI_SOURCE_PORT, CONFIG_ESC_IRQ_EXTI_SOURCE_PIN);
|
||||
exti_init(CONFIG_ESC_IRQ_EXTI, EXTI_INTERRUPT, EXTI_TRIG_FALLING);
|
||||
exti_interrupt_enable(CONFIG_ESC_IRQ_EXTI);
|
||||
exti_interrupt_flag_clear(CONFIG_ESC_IRQ_EXTI);
|
||||
nvic_irq_enable(CONFIG_ESC_IRQ_IRQn, CONFIG_IRQ_PREEMPT_ETHERCAT_ESC, 0U);
|
||||
|
||||
/* SYNC1:PB2 -> EXTI2,下降沿,preempt=1(备用) */
|
||||
gpio_exti_source_select(CONFIG_ESC_SYNC1_EXTI_SOURCE_PORT, CONFIG_ESC_SYNC1_EXTI_SOURCE_PIN);
|
||||
exti_init(CONFIG_ESC_SYNC1_EXTI, EXTI_INTERRUPT, EXTI_TRIG_FALLING);
|
||||
exti_interrupt_enable(CONFIG_ESC_SYNC1_EXTI);
|
||||
exti_interrupt_flag_clear(CONFIG_ESC_SYNC1_EXTI);
|
||||
nvic_irq_enable(CONFIG_ESC_SYNC1_IRQn, CONFIG_IRQ_PREEMPT_ETHERCAT_SYNC1, 0U);
|
||||
#endif
|
||||
}
|
||||
|
||||
__attribute__((section(".ramfunc"), noinline))
|
||||
void EthercatSync::onSync0Exti()
|
||||
{
|
||||
if (exti_interrupt_flag_get(CONFIG_ESC_SYNC0_EXTI) != RESET) {
|
||||
exti_interrupt_flag_clear(CONFIG_ESC_SYNC0_EXTI);
|
||||
/* 直接寄存器访问,绕过 GD32 库函数以消除可变抖动 */
|
||||
if ((EXTI_PD & (uint32_t)CONFIG_ESC_SYNC0_EXTI) != RESET) {
|
||||
EXTI_PD = (uint32_t)CONFIG_ESC_SYNC0_EXTI;
|
||||
EthercatSlave::periodicIsr();
|
||||
}
|
||||
}
|
||||
@@ -110,13 +124,14 @@ void EthercatSync::onTimerIrq()
|
||||
extern "C" {
|
||||
|
||||
#if (CONFIG_ETHERCAT_SYNC_MODE == 0)
|
||||
__attribute__((section(".ramfunc"), noinline))
|
||||
void EXTI0_IRQHandler(void) { EthercatSync::onSync0Exti(); }
|
||||
void EXTI1_IRQHandler(void) { EthercatSync::onEscIrqExti(); }
|
||||
void EXTI2_IRQHandler(void) { EthercatSync::onSync1Exti(); }
|
||||
#else
|
||||
void TIMER0_IRQHandler(void) { EthercatSync::onTimerIrq(); }
|
||||
void EXTI1_IRQHandler(void) { EthercatSync::onEscIrqExti(); }
|
||||
void EXTI2_IRQHandler(void) { EthercatSync::onSync1Exti(); }
|
||||
#endif
|
||||
|
||||
} /* extern "C" */
|
||||
|
||||
#endif /* CONFIG_ETHERCAT_ENABLE */
|
||||
|
||||
Reference in New Issue
Block a user