2026-06-26 00:57:59 +08:00
|
|
|
/**
|
|
|
|
|
* @file ethercat_slave.cpp
|
|
|
|
|
* @brief EtherCAT 从站周期处理实现。
|
|
|
|
|
*/
|
|
|
|
|
#include "comm/ethercat_slave.h"
|
|
|
|
|
#include "comm/esc_lan9252.h"
|
|
|
|
|
#include "sys/config.h"
|
|
|
|
|
#include "sys/task_manager.h"
|
|
|
|
|
#include "main.h"
|
|
|
|
|
|
2026-06-29 01:51:02 +08:00
|
|
|
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;
|
2026-06-26 00:57:59 +08:00
|
|
|
|
2026-06-29 01:51:02 +08:00
|
|
|
/* 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,远小于一帧)。 */
|
2026-06-26 00:57:59 +08:00
|
|
|
|
|
|
|
|
void EthercatSlave::init()
|
|
|
|
|
{
|
2026-07-27 01:49:18 +08:00
|
|
|
ECAT_LOGI("[ECAT][SLAVE] init: begin\r\n");
|
2026-07-20 02:36:56 +08:00
|
|
|
|
|
|
|
|
if (!EscLan9252::init()) {
|
|
|
|
|
/* EXMC 总线未就绪:无法继续读芯片 ID 与 AL 状态,统一上报 FAIL。 */
|
|
|
|
|
s_chipId = 0U;
|
|
|
|
|
s_alState = 0U;
|
|
|
|
|
s_escReady = false;
|
2026-07-27 01:49:18 +08:00
|
|
|
LOG_ERR("[ECAT][SLAVE] init: FAIL, ESC bus not ready\r\n");
|
2026-07-20 02:36:56 +08:00
|
|
|
s_inputWriteIdx = 0U;
|
|
|
|
|
s_outputReadIdx = 0U;
|
|
|
|
|
s_periodicSeq = 0U;
|
|
|
|
|
s_processedSeq = 0U;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-29 01:51:02 +08:00
|
|
|
|
2026-08-09 03:27:21 +08:00
|
|
|
/* 第 1 层调试可观测性:读取 BYTE_TEST 寄存器(0x064,默认 0x87654321)并打印 selfTest 结果 */
|
2026-06-29 01:51:02 +08:00
|
|
|
s_chipId = EscLan9252::readChipId();
|
|
|
|
|
const bool chipIdOk = (s_chipId == 0x87654321U);
|
2026-08-09 03:27:21 +08:00
|
|
|
ECAT_LOGI("[ECAT][SLAVE] byte_test=0x%08lX expected=0x%08lX selfTest=%s\r\n",
|
2026-07-20 02:36:56 +08:00
|
|
|
(unsigned long)s_chipId, (unsigned long)0x87654321UL,
|
|
|
|
|
chipIdOk ? "PASS" : "FAIL");
|
2026-06-29 01:51:02 +08:00
|
|
|
|
|
|
|
|
/* AL 状态守卫:仅在芯片 ID 匹配时读取 AL 状态(ID 不匹配时 EXMC 可能读回 0/0xFFFF,无意义)。
|
|
|
|
|
* AL ≥ SAFEOP(0x04) 才认为 SYNC0 可触发;完整 ESC 配置(SM/FMMU/AL Control/SYNC0)需 SSC 集成。 */
|
|
|
|
|
s_alState = 0U;
|
2026-07-20 02:36:56 +08:00
|
|
|
uint16_t alStatusReg = 0U; /* 0x0130:AL Status(bit0-3=state,bit8-15=AL Status Code) */
|
2026-06-29 01:51:02 +08:00
|
|
|
if (chipIdOk) {
|
2026-07-20 02:36:56 +08:00
|
|
|
alStatusReg = EscLan9252::initOnlyReadReg(0x0130U);
|
|
|
|
|
s_alState = (uint8_t)(alStatusReg & 0x000FU);
|
2026-06-26 00:57:59 +08:00
|
|
|
}
|
2026-07-20 02:36:56 +08:00
|
|
|
const uint8_t alCode = (uint8_t)((alStatusReg >> 8U) & 0xFFU);
|
2026-06-29 01:51:02 +08:00
|
|
|
s_escReady = chipIdOk && (s_alState >= AL_STATE_SAFEOP);
|
|
|
|
|
|
2026-07-20 02:36:56 +08:00
|
|
|
/* AL 状态码 → 可读字符串(EtherCAT 状态机定义,见 ETG.1000.6)。
|
|
|
|
|
* 0x01=INIT, 0x02=PREOP, 0x03=BOOT, 0x04=SAFEOP, 0x08=OP。
|
|
|
|
|
* 本工程 AL_STATE_SAFEOP=0x04 与官方定义一致;守卫 s_alState>=0x04,
|
|
|
|
|
* 因此 OP(0x08) 也会通过,SAFEOP(0x04) 刚好通过,BOOT/PREOP/INIT 不通过。 */
|
|
|
|
|
const char *alStateName = "UNKNOWN";
|
|
|
|
|
switch (s_alState) {
|
|
|
|
|
case 0x00U: alStateName = "NONE"; break;
|
|
|
|
|
case 0x01U: alStateName = "INIT"; break;
|
|
|
|
|
case 0x02U: alStateName = "PREOP"; break;
|
|
|
|
|
case 0x03U: alStateName = "BOOT"; break;
|
|
|
|
|
case 0x04U: alStateName = "SAFEOP"; break;
|
|
|
|
|
case 0x08U: alStateName = "OP"; break;
|
|
|
|
|
default: break;
|
|
|
|
|
}
|
|
|
|
|
/* AL Status Code(寄存器 0x0130 高 8 位):非 0 表示从站进入错误状态
|
|
|
|
|
* (如 SM Watchdog=0x1B、FMMU 配置错、邮箱协议错),需主站介入。 */
|
|
|
|
|
|
2026-06-29 01:51:02 +08:00
|
|
|
if (!chipIdOk) {
|
2026-07-27 01:49:18 +08:00
|
|
|
LOG_ERR("[ECAT][SLAVE] ESC NOT ready, sync ISR disabled (check EXMC wiring/timing)\r\n");
|
2026-06-29 01:51:02 +08:00
|
|
|
} else if (s_alState < AL_STATE_SAFEOP) {
|
2026-07-27 01:49:18 +08:00
|
|
|
LOG_ERR("[ECAT][SLAVE] chip id PASS, but AL state=0x%02X(%s) < SAFEOP(0x%02X), code=0x%02X\r\n",
|
2026-07-20 02:36:56 +08:00
|
|
|
(unsigned)s_alState, alStateName, (unsigned)AL_STATE_SAFEOP, (unsigned)alCode);
|
2026-07-27 01:49:18 +08:00
|
|
|
LOG_ERR("[ECAT][SLAVE] SYNC0 will not fire, full ESC config requires SSC integration\r\n");
|
2026-06-29 01:51:02 +08:00
|
|
|
} else {
|
2026-07-27 01:49:18 +08:00
|
|
|
ECAT_LOGI("[ECAT][SLAVE] ESC ready, chip id=0x%08lX, AL state=0x%02X(%s), code=0x%02X\r\n",
|
2026-07-20 02:36:56 +08:00
|
|
|
(unsigned long)s_chipId, (unsigned)s_alState, alStateName, (unsigned)alCode);
|
2026-06-29 01:51:02 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s_inputWriteIdx = 0U;
|
|
|
|
|
s_outputReadIdx = 0U;
|
|
|
|
|
s_periodicSeq = 0U;
|
|
|
|
|
s_processedSeq = 0U;
|
2026-07-27 01:49:18 +08:00
|
|
|
LOG_ERR("[ECAT][SLAVE] init: %s\r\n", s_escReady ? "OK" : "FAIL(partial)");
|
2026-06-26 00:57:59 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-29 01:51:02 +08:00
|
|
|
__attribute__((section(".ramfunc"), noinline))
|
2026-06-26 00:57:59 +08:00
|
|
|
void EthercatSlave::periodicIsr()
|
|
|
|
|
{
|
2026-06-29 01:51:02 +08:00
|
|
|
const uint32_t cycStart = DWT->CYCCNT;
|
2026-06-26 00:57:59 +08:00
|
|
|
|
2026-06-29 01:51:02 +08:00
|
|
|
/* 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;
|
2026-06-26 00:57:59 +08:00
|
|
|
|
2026-06-29 01:51:02 +08:00
|
|
|
/* 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;
|
2026-06-26 00:57:59 +08:00
|
|
|
|
2026-06-29 01:51:02 +08:00
|
|
|
/* 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;
|
2026-06-26 00:57:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const PdoBuffer* EthercatSlave::getLatestInputs()
|
|
|
|
|
{
|
2026-06-29 01:51:02 +08:00
|
|
|
/* 返回最新完成的缓冲 = (writeIdx + 2) % 3,与 ISR 当前写入位置隔一个 */
|
|
|
|
|
return &s_inputs[(s_inputWriteIdx + 2U) % 3U];
|
2026-06-26 00:57:59 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EthercatSlave::setOutputs(const PdoBuffer *outputs)
|
|
|
|
|
{
|
|
|
|
|
if (outputs == nullptr) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-06-29 01:51:02 +08:00
|
|
|
/* 写入与 ISR 当前读取位置隔 2 个缓冲,提供 2 周期宽限。
|
|
|
|
|
* 2 周期宽限下拷贝竞态窗口消除,前提是主循环处理时间 < 2 × 周期时间。 */
|
|
|
|
|
const uint8_t write_idx = (s_outputReadIdx + 2U) % 3U;
|
2026-06-26 00:57:59 +08:00
|
|
|
s_outputs[write_idx] = *outputs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void EthercatSlave::process()
|
|
|
|
|
{
|
2026-06-29 01:51:02 +08:00
|
|
|
/* 应用层 PDO 桥接(非实时,由主循环周期序列号或 ESC IRQ 触发)。
|
|
|
|
|
* 运行期 PDO 交换仅通过 getLatestInputs()/setOutputs() 三缓冲进行;ESC 寄存器访问仅限 init 阶段。
|
|
|
|
|
*
|
|
|
|
|
* 框架级桥接:从三缓冲取最新输入到 s_appInputs,把 s_appOutputs 推回三缓冲。
|
2026-07-15 02:49:37 +08:00
|
|
|
* TODO(spec): 应用对象字典映射(具体输入字节对应哪个应用变量,如字节 0-3=温度浮点)
|
|
|
|
|
* 待后续应用层 spec 定义;
|
|
|
|
|
* 应用层通过 getApplicationInputs()/setApplicationOutputs() 访问快照。 */
|
2026-06-29 01:51:02 +08:00
|
|
|
const PdoBuffer *latest = getLatestInputs();
|
|
|
|
|
if (latest != nullptr) {
|
|
|
|
|
s_appInputs = *latest;
|
|
|
|
|
}
|
|
|
|
|
setOutputs(&s_appOutputs);
|
2026-06-26 00:57:59 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-29 01:51:02 +08:00
|
|
|
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;
|
|
|
|
|
}
|