Files
DCU_APP/GD32E508_DCU_APP/sys/task_runner.cpp
T
cbb f9ef1b8da2 feat(comm): 添加EtherCAT CAN桥接和PDO功能模块
添加了ethercat_can_bridge.h/cpp和ethercat_pdo.h/cpp四个文件,
扩展通信模块功能,支持CAN桥接和PDO相关操作。

fix(comm): 修复LAN9252 EXMC总线时序配置问题

将BUSLAT从1调整为4,确保满足数据手册要求的最小13ns间隔,
解决因时序余量不足导致的温漂偶发读错问题。

feat(comm): 配置ESC过程数据看门狗时间为1秒

修改ESC_REG_WD_TIME_PD寄存器值从默认100ms到1s,
防止主站调试周期触发SM2看门狗导致状态被拖回SAFEOP。
2026-09-05 16:54:38 +08:00

601 lines
24 KiB
C++
Raw 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 task_runner.cpp
* @brief 任务执行器实现。
*/
#include "main.h"
#include "device/adc.h"
#include "device/watchdog.h"
#include "device/spi/app_spi_command.h"
#include "device/canfd.h"
#include "device/motor_state.h"
#include "device/motor_version.h"
#include "sys/config.h"
#include "sys/ota_boot_request.h"
#include "sys/motor_ota_relay.h"
#include "sys/task_manager.h"
#include "sys/task_runner.h"
#include "comm/ethercat_slave.h"
#include "comm/ethercat_sync.h"
#include "comm/ethercat_can_bridge.h"
#include "comm/esc_exmc_bus.h"
extern "C" {
void MainLoop(void);
}
namespace {
constexpr uint32_t kAdcFilterWindow = CONFIG_ADC_FILTER_SIZE + 1U;
/**
* @brief Push one sample into a fixed-size sliding window buffer.
* @param[in,out] buffer Sample buffer with `kAdcFilterWindow` elements.
* @param[in] value New sample value.
*/
static void pushSample(float *buffer, float value)
{
for(uint32_t i = 0U; i < CONFIG_ADC_FILTER_SIZE; ++i) {
buffer[i] = buffer[i + 1U];
}
buffer[CONFIG_ADC_FILTER_SIZE] = value;
}
/**
* @brief Apply median filtering to a fixed-size sample window.
* @param[in] buffer Sample buffer with `kAdcFilterWindow` elements.
* @return Median value in the current window.
*/
static float medianFilter(const float *buffer)
{
float sorted[kAdcFilterWindow];
for(uint32_t i = 0U; i < kAdcFilterWindow; ++i) {
sorted[i] = buffer[i];
}
for(uint32_t i = 1U; i < kAdcFilterWindow; ++i) {
const float key = sorted[i];
uint32_t j = i;
while((j > 0U) && (sorted[j - 1U] > key)) {
sorted[j] = sorted[j - 1U];
--j;
}
sorted[j] = key;
}
return sorted[kAdcFilterWindow / 2U];
}
} // namespace
struct TaskRunner::Runtime {
Adc::Sample sample{};
float temperature_samples[kAdcFilterWindow] = {0.0f};
float vdda_samples[kAdcFilterWindow] = {0.0f};
float vin_adc_samples[kAdcFilterWindow] = {0.0f};
uint16_t last_vin_raw = 0U;
uint16_t last_vref_raw = 0U;
uint16_t last_temp_raw = 0U;
uint8_t last_rx_channel = 0U;
uint32_t last_rx_id = 0U;
uint8_t last_rx_len = 0U;
uint8_t last_rx_data[64] = {0};
};
TaskRunner::Runtime TaskRunner::s_runtime;
/**
* @brief 采样 VIN 与 VREF 原始 ADC 数值。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 从 ADC DMA 缓冲中读取外部 VIN_ADC 与内部 VREFINT 通道最新值。
*/
void TaskRunner::taskAdcVrefSample()
{
s_runtime.last_vin_raw = Adc::readVinRaw();
s_runtime.last_vref_raw = Adc::readVrefRaw();
}
/**
* @brief 采样内部温度通道原始 ADC 数值。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 读取 ADC DMA 缓冲中的内部温度通道最新值。
*/
void TaskRunner::taskAdcTempSample()
{
s_runtime.last_temp_raw = Adc::readTempRaw();
}
/**
* @brief 根据原始采样值计算工程量结果。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 输出 `vin_adc_v`、`vdda` 和 `temperature` 等可直接使用的工程值。
*/
void TaskRunner::taskAdcProcess()
{
auto &s = s_runtime.sample;
const float vdda = Adc::calculateVdda(s_runtime.last_vref_raw);
const float vin_adc_v = Adc::calculateVinAdcVoltage(s_runtime.last_vin_raw, vdda);
const float temperature = Adc::calculateTemperature(s_runtime.last_temp_raw, vdda);
const bool sampleValid = (s_runtime.last_vref_raw > 0U) &&
(s_runtime.last_vref_raw < 4095U) &&
(s_runtime.last_temp_raw > 0U) &&
(s_runtime.last_temp_raw < 4095U) &&
(vdda >= 2.7f) && (vdda <= 3.6f) &&
(temperature >= -40.0f) && (temperature <= 125.0f);
if(sampleValid) {
pushSample(s_runtime.vdda_samples, vdda);
pushSample(s_runtime.vin_adc_samples, vin_adc_v);
pushSample(s_runtime.temperature_samples, temperature);
}
s.vin_raw = s_runtime.last_vin_raw;
s.vref_raw = s_runtime.last_vref_raw;
s.temp_raw = s_runtime.last_temp_raw;
s.vdda = medianFilter(s_runtime.vdda_samples);
s.vin_adc_v = medianFilter(s_runtime.vin_adc_samples);
s.temperature = medianFilter(s_runtime.temperature_samples);
s.valid = sampleValid ? 1U : 0U;
}
/**
* @brief 执行 CAN 周期发送任务:每 10ms 在每个使能通道上发送一帧 CANFD 测试帧。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 帧格式:扩展帧 29-bit ID,CAN-FD,DLC=64,BRS 受 CONFIG_CAN_FD_BRS_ENABLE 控制。
* 载荷布局:
* [0] 固定标识 0xC0
* [1] 通道号
* [2..5] 发送序号(小端 uint32)
* [6..7] 系统运行秒数低 16 位
* [8..63] 填充 0xA0 + (index & 0x0F)
* periodic=true:仅允许一个邮箱保持自动重发,不主动 abort 旧帧,
* 跳过 TEC 保护以匹配诊断行为。
*/
void TaskRunner::taskCanTxSend()
{
#if (CONFIG_CAN_PERIODIC_TEST_TX_ENABLE != 0)
static uint32_t seq = 0U;
for (uint8_t ch = 0U; ch < 3U; ++ch) {
if (!CanFd::isChannelEnabled(ch)) {
continue;
}
uint8_t data[64];
data[0] = 0xC0U;
data[1] = ch;
const uint32_t s = seq;
data[2] = static_cast<uint8_t>(s & 0xFFU);
data[3] = static_cast<uint8_t>((s >> 8) & 0xFFU);
data[4] = static_cast<uint8_t>((s >> 16) & 0xFFU);
data[5] = static_cast<uint8_t>((s >> 24) & 0xFFU);
data[6] = static_cast<uint8_t>(g_app_uptime_sec & 0xFFU);
data[7] = static_cast<uint8_t>((g_app_uptime_sec >> 8) & 0xFFU);
for (uint8_t i = 8U; i < 64U; ++i) {
data[i] = static_cast<uint8_t>(0xA0U + (i & 0x0FU));
}
const uint32_t extId = 0x18FF5000U | static_cast<uint32_t>(ch);
if (CanFd::transmitFrame(ch, extId, data, sizeof(data), true) == CanFd::CanTxResult::kOk) {
seq++;
}
}
#else
/* CONFIG_CAN_PERIODIC_TEST_TX_ENABLE=0:测试帧无业务接收节点 ACK,
* 单发模式下会持续产生 MTEn 错误帧,整任务空跑。 */
(void)0;
#endif
}
/**
* @brief 批量解析并消费待处理的 CAN 接收报文。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 一次任务按预算尽量清空软件接收队列;若仍有积压,则重新置位解析任务。
*/
void TaskRunner::taskCanRxParse()
{
uint32_t consumed = 0U;
while(consumed < CONFIG_CAN_RX_PARSE_BUDGET) {
if(!CanFd::consumeRxFrame(&s_runtime.last_rx_id, s_runtime.last_rx_data, &s_runtime.last_rx_len, &s_runtime.last_rx_channel)) {
break;
}
if(OtaBootRequest::handleCanFrame(s_runtime.last_rx_channel,
s_runtime.last_rx_id,
s_runtime.last_rx_data,
s_runtime.last_rx_len)) {
OtaBootRequest::process();
return;
}
if(MotorOtaRelay::onCanFrame(s_runtime.last_rx_channel,
s_runtime.last_rx_id,
s_runtime.last_rx_data,
s_runtime.last_rx_len)) {
consumed++;
continue;
}
#if (CONFIG_MOTOR_STATE_ENABLE != 0)
MotorState::onCanFrame(s_runtime.last_rx_channel,
s_runtime.last_rx_id,
s_runtime.last_rx_data,
s_runtime.last_rx_len);
#endif
#if (CONFIG_MOTOR_VERSION_ENABLE != 0)
MotorVersion::onCanFrame(s_runtime.last_rx_channel,
s_runtime.last_rx_id,
s_runtime.last_rx_data,
s_runtime.last_rx_len);
#endif
/* 1.5.53:旁路投递给 EtherCAT PDO 桥。放在分发链末尾且**不消费**帧
* (不 return、不 continue),因此 OTA relay / 电机状态的取帧不受影响——
* 本队列只有 taskCanRxParse 一个消费者,新增第二个消费者会互相抢帧。 */
EthercatCanBridge::onCanFrame(s_runtime.last_rx_channel,
s_runtime.last_rx_id,
s_runtime.last_rx_data,
s_runtime.last_rx_len);
consumed++;
}
if(CanFd::hasRxFrame()) {
TaskManager::set(1UL << TASK_CAN_RX_PARSE);
}
}
/**
* @brief 执行系统心跳维护任务。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 推进 OTA Boot 请求、刷新 CAN 反馈快照、启动 ADC 1Hz 推送轮次。
*/
void TaskRunner::taskHeartbeat()
{
OtaBootRequest::process();
CanFd::refreshFeedbackSnapshot();
#if (CONFIG_MOTOR_STATE_ENABLE != 0)
MotorState::checkOffline();
#endif
}
/**
* @brief 拷贝当前 ADC 工程量快照(供 SPI 0xC4 等只读路径)。
* @param[out] out 非空时写入最新 Sample
* @return true 已写入(不保证 sample.valid==1)
*/
bool TaskRunner::copyAdcSample(Adc::Sample *out)
{
if (out == nullptr) {
return false;
}
*out = s_runtime.sample;
return true;
}
/* SSC 侧 AL 诊断快照(dc_app.c): [19:16]=AL 状态 [31:24]=AL 错误码 [15:0]=AL 事件寄存器 */
extern "C" uint32_t EcatDbg_ReadAlDiag(void);
/* SSC 侧 AL Event Mask(0x204) 守护(dc_app.c, 1.5.48):缺位自愈+WARN,返回实际值/0xFFFF=不适用 */
extern "C" uint32_t EcatDbg_GuardAleventMask(void);
/**
* @brief 1.5.48/1.5.49 AL Event Mask(0x204) 守护 + SM2 事件楔死自愈 + 1.5.52 邮箱
* SM 地址/长度失配自愈:250ms 周期。
* @note 实测"停止回 INIT 后重进 OP"概率性挂起(SAFEOP+0x001B, WKC=3/3 但
* bEcatFirstOutputsReceived 不置位,0x220 bit10 挂起 10s 无人处理)——
* PDI_Isr 通路楔死。守护在 SAFEOP 后 1s 内替 PDI_Isr 完成工作(置位
* 标志+应答事件),主站 12s OP 等待窗口内可自愈;WARN 携带全部内部旗标,
* 下次复现一行日志定位楔死路径(详见 dc_app.c 注释)。
* 1.5.52:周期 1s→250ms——新加的邮箱地址自愈(1.5.52 根治 SSC 侧
* u16EscAddrSendMbx 错位楔死,详见 dc_app.c)必须快过主站 SDO 超时
* (~700ms),否则自愈永远落在主站放弃之后,形同虚设。
*/
static void ethercatAleMaskGuard()
{
static uint32_t s_lastGuardMs = 0U;
if (!EthercatSync::isActive()) {
return;
}
const uint32_t nowMs = g_scheduler.tick_1ms;
if ((uint32_t)(nowMs - s_lastGuardMs) >= 250U) {
s_lastGuardMs = nowMs;
EcatDbg_GuardAleventMask();
}
}
/**
* @brief 1.5.32 ECAT 心跳:主循环存活证明 + ESC AL 状态/事件寄存器快照。
* @note 仅在同步 ISR 激活(SSC 在跑)时打印。心跳停 = 主循环卡死;
* al=0x1 且 evt bit0 长期置位 = SSC 没处理 AL 请求;
* al 与主站读到的不一致 = ESC 写回路径问题。
*/
static void ethercatAlHeartbeat()
{
if (!EthercatSync::isActive()) {
return;
}
const uint32_t alDiag = EcatDbg_ReadAlDiag();
printf("[ECAT][HB] al=0x%lX code=0x%02lX evt=0x%04lX (uptime=%lus)\r\n",
(unsigned long)((alDiag >> 16) & 0x0FU),
(unsigned long)((alDiag >> 24) & 0xFFU),
(unsigned long)(alDiag & 0xFFFFU),
(unsigned long)g_app_uptime_sec);
}
/**
* @brief 打印 EtherCAT 同步中断计数与速率(DC 通路验证)。
* @note 主站启用 DC(SYNC0 周期 T)后,sync0 速率应稳定 ≈ 1e6/T_us 次/秒;
* free-run 模式下 SYNC0 引脚无脉冲,三个计数器恒为 0(本函数不打印)。
* 仅在有增量时输出,避免未接主站时刷屏。
*/
static void ethercatSyncCounterPrint()
{
static uint32_t s_prevSync0 = 0U;
static uint32_t s_prevSync1 = 0U;
static uint32_t s_prevEscIrq = 0U;
static uint32_t s_prevSec = 0U;
const uint32_t sync0 = EthercatSync::getSync0Count();
const uint32_t sync1 = EthercatSync::getSync1Count();
const uint32_t escIrq = EthercatSync::getEscIrqCount();
const uint32_t d0 = sync0 - s_prevSync0; /* 无符号回绕自然成立 */
const uint32_t d1 = sync1 - s_prevSync1;
const uint32_t di = escIrq - s_prevEscIrq;
const uint32_t now = g_app_uptime_sec;
const uint32_t elapsed = (now > s_prevSec) ? (now - s_prevSec) : 1U;
s_prevSync0 = sync0;
s_prevSync1 = sync1;
s_prevEscIrq = escIrq;
s_prevSec = now;
if (d0 == 0U && d1 == 0U && di == 0U) {
return; /* 未接主站或 free-run:无脉冲,静默 */
}
printf("[ECAT][SYNC] sync0=%lu(+%lu, %lu/s) sync1=%lu(+%lu) escirq=%lu(+%lu) dt=%lus\r\n",
(unsigned long)sync0, (unsigned long)d0, (unsigned long)(d0 / elapsed),
(unsigned long)sync1, (unsigned long)d1,
(unsigned long)escIrq, (unsigned long)di,
(unsigned long)elapsed);
}
/**
* @brief 5s 诊断任务:默认仅状态变化/错误增量;可选全量 UART 摘要。
* @note CONFIG_UART_TEMP_PERIODIC_PRINT=1 时打印 ADC/CAN/ECAT 全量行(调试用)。
* 默认=0:相对上次快照有 CAN 错误计数增量、ADC valid 跳变、ECAT ready 变化或 ISR 超阈时才打印。
* 采用定点整数格式,避免依赖浮点 printf。
*/
void TaskRunner::taskDiagnosticPrint()
{
const auto *diag = CanFd::diag();
const auto &s = s_runtime.sample;
#if (CONFIG_UART_TEMP_PERIODIC_PRINT != 0)
/* ---- Debug 全量摘要:周期刷 ADC / CAN 吞吐 / ECAT ISR 统计 ---- */
{
const int32_t temp_x100 = static_cast<int32_t>(s.temperature * 100.0f);
const uint32_t vdda_mV = static_cast<uint32_t>(s.vdda * 1000.0f);
const uint32_t vin_adc_mV = static_cast<uint32_t>(s.vin_adc_v * 1000.0f);
#if (CONFIG_UART_DIAG_LEVEL == 0)
printf("[APP] uptime=%lus valid=%u, temp_raw=%u, vref_raw=%u, temp=%ld.%02ld C, vdda=%lu.%03lu V, vin_adc=%lu.%03lu V\r\n",
(unsigned long)g_app_uptime_sec,
static_cast<unsigned int>(s.valid),
static_cast<unsigned int>(s.temp_raw),
static_cast<unsigned int>(s.vref_raw),
static_cast<long>(temp_x100 / 100),
static_cast<long>((temp_x100 >= 0 ? temp_x100 : -temp_x100) % 100),
static_cast<unsigned long>(vdda_mV / 1000U),
static_cast<unsigned long>(vdda_mV % 1000U),
static_cast<unsigned long>(vin_adc_mV / 1000U),
static_cast<unsigned long>(vin_adc_mV % 1000U));
#else
printf("[APP] uptime=%lus valid=%u, temp=%ld.%02ld C, vdda=%lu.%03lu V, vin_adc=%lu.%03lu V, rx_ch=%u, rx_id=0x%08lX, rx_len=%u, tx_ch=%u, can_tx=%lu, can_rx=%lu, sw_drop=%lu, hw_full=%lu, hw_over=%lu, tx_abort=%lu, can_err=%lu\r\n",
(unsigned long)g_app_uptime_sec,
static_cast<unsigned int>(s.valid),
static_cast<long>(temp_x100 / 100),
static_cast<long>((temp_x100 >= 0 ? temp_x100 : -temp_x100) % 100),
static_cast<unsigned long>(vdda_mV / 1000U),
static_cast<unsigned long>(vdda_mV % 1000U),
static_cast<unsigned long>(vin_adc_mV / 1000U),
static_cast<unsigned long>(vin_adc_mV % 1000U),
static_cast<unsigned int>(s_runtime.last_rx_channel),
static_cast<unsigned long>(s_runtime.last_rx_id),
static_cast<unsigned int>(s_runtime.last_rx_len),
static_cast<unsigned int>(diag->last_tx_channel),
static_cast<unsigned long>(diag->tx_count),
static_cast<unsigned long>(diag->rx_count),
static_cast<unsigned long>(diag->rx_drop_count),
static_cast<unsigned long>(diag->rx_fifo_full_count),
static_cast<unsigned long>(diag->rx_fifo_overfull_count),
static_cast<unsigned long>(diag->tx_abort_count),
static_cast<unsigned long>(diag->error_count));
#endif
if (EthercatSlave::isEscReady()) {
/* 1.5.27:periodicIsr 已删除(死代码,无调用点),PDO 交换由 SSC
* Sync0_Isr → ECAT_Application → dc_app.c APPL_* 完成。 */
printf("[ECAT] ESC ready (chip id=0x%08lX), PDO via SSC Sync0_Isr\r\n",
(unsigned long)EthercatSlave::getChipId());
} else {
printf("[ECAT] ESC not detected (chip id=0x%08lX), EtherCAT disabled\r\n",
(unsigned long)EthercatSlave::getChipId());
}
ethercatSyncCounterPrint();
ethercatAlHeartbeat();
}
#else
/* ---- 默认静默路径:仅错误增量 / 状态变化,无 uptime 心跳 ---- */
{
static uint32_t s_prev_rx_drop = 0U;
static uint32_t s_prev_rx_fifo_full = 0U;
static uint32_t s_prev_rx_fifo_overfull = 0U;
static uint32_t s_prev_tx_abort = 0U;
static uint32_t s_prev_error = 0U;
static uint8_t s_prev_adc_valid = 0xFFU; /* 0xFF=尚未采样 */
static bool s_prev_esc_ready = false;
static bool s_esc_ready_inited = false;
const uint32_t rx_drop = diag->rx_drop_count;
const uint32_t rx_full = diag->rx_fifo_full_count;
const uint32_t rx_over = diag->rx_fifo_overfull_count;
const uint32_t tx_abort = diag->tx_abort_count;
const uint32_t can_err = diag->error_count;
if (rx_drop > s_prev_rx_drop || rx_full > s_prev_rx_fifo_full
|| rx_over > s_prev_rx_fifo_overfull || tx_abort > s_prev_tx_abort
|| can_err > s_prev_error) {
printf("[CAN-DIAG] uptime=%lus err=%lu(+%lu) drop=%lu(+%lu) full=%lu(+%lu) over=%lu(+%lu) abort=%lu(+%lu)\r\n",
(unsigned long)g_app_uptime_sec,
(unsigned long)can_err,
(unsigned long)(can_err - s_prev_error),
(unsigned long)rx_drop,
(unsigned long)(rx_drop - s_prev_rx_drop),
(unsigned long)rx_full,
(unsigned long)(rx_full - s_prev_rx_fifo_full),
(unsigned long)rx_over,
(unsigned long)(rx_over - s_prev_rx_fifo_overfull),
(unsigned long)tx_abort,
(unsigned long)(tx_abort - s_prev_tx_abort));
s_prev_rx_drop = rx_drop;
s_prev_rx_fifo_full = rx_full;
s_prev_rx_fifo_overfull = rx_over;
s_prev_tx_abort = tx_abort;
s_prev_error = can_err;
}
if (s_prev_adc_valid == 0xFFU) {
s_prev_adc_valid = s.valid;
} else if (s.valid != s_prev_adc_valid) {
printf("[APP] adc valid %u -> %u (uptime=%lus)\r\n",
static_cast<unsigned int>(s_prev_adc_valid),
static_cast<unsigned int>(s.valid),
(unsigned long)g_app_uptime_sec);
s_prev_adc_valid = s.valid;
}
const bool esc_ready = EthercatSlave::isEscReady();
if (!s_esc_ready_inited) {
s_prev_esc_ready = esc_ready;
s_esc_ready_inited = true;
} else if (esc_ready != s_prev_esc_ready) {
printf("[ECAT] ESC ready %u -> %u chip_id=0x%08lX (uptime=%lus)\r\n",
static_cast<unsigned int>(s_prev_esc_ready ? 1U : 0U),
static_cast<unsigned int>(esc_ready ? 1U : 0U),
(unsigned long)EthercatSlave::getChipId(),
(unsigned long)g_app_uptime_sec);
s_prev_esc_ready = esc_ready;
}
/* 1.5.27:periodicIsr ISR 统计随死代码一并删除 */
/* DC SYNC0 通路验证:仅在有脉冲增量时打印(未接主站时静默)。 */
ethercatSyncCounterPrint();
ethercatAlHeartbeat();
}
#endif
}
/**
* @brief 初始化任务执行器运行时上下文。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 执行一次完整 ADC 采样,并同步填充运行时缓存的原始值起点。
*/
void TaskRunner::init()
{
Adc::collectSample(&s_runtime.sample);
s_runtime.last_vin_raw = s_runtime.sample.vin_raw;
s_runtime.last_vref_raw = s_runtime.sample.vref_raw;
s_runtime.last_temp_raw = s_runtime.sample.temp_raw;
for(uint32_t i = 0U; i < kAdcFilterWindow; ++i) {
s_runtime.temperature_samples[i] = s_runtime.sample.temperature;
s_runtime.vdda_samples[i] = s_runtime.sample.vdda;
s_runtime.vin_adc_samples[i] = s_runtime.sample.vin_adc_v;
}
}
/**
* @brief 执行当前所有已置位的待处理任务。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 先原子取出任务位图;若当前没有待处理任务则直接返回。
*/
void TaskRunner::runPending()
{
Watchdog::feed();
/* === HBI 周期诊断:每 20 秒重新跑一次 EXMC/HBI 探活,方便热插拔/焊接后观察 ===
* 1.5.27:EtherCAT 同步 ISR 激活后停止(diagDump 会把 EXMC 引脚切成 GPIO,
* 拆毁运行中的总线;诊断的 bit-bang 也与 HBI 事务互斥冲突)。
* 1.5.33:整块由 CONFIG_ESC_EXMC_DIAG 门控,默认关闭——硬件已确认全通,
* 不再每 20s 拆总线做探针;换板/补焊排查浮空时置 1 恢复。 */
#if CONFIG_ESC_EXMC_DIAG
static uint32_t s_lastDiagSec = 0xFFFFFFFFU;
if (!EthercatSync::isActive()
&& (g_app_uptime_sec >= s_lastDiagSec + 20U || s_lastDiagSec == 0xFFFFFFFFU)) {
s_lastDiagSec = g_app_uptime_sec;
printf("[ECAT][DIAG] === 20s periodic HBI probe (uptime=%lus) ===\r\n",
(unsigned long)g_app_uptime_sec);
EscExmcBus::diagDump();
}
#endif
/* 延迟 EEPROM 烧录已删除(1.5.29):ESI 镜像改由 EtherCAT 主站侧刷写
* (tools/ecat/ecat_flash_eeprom.py 经 ESC 0x0500 EEPROM 接口),
* 固件不再承担烧录/校验职责。 */
/* poll() 含按铃超时看门狗(cancelRing),必须每轮无条件执行。
* 主动推送已随 2026-08 请求驱动改造移除,此处仅需消费请求响应的按铃。 */
AppSpiCommandBus.poll();
/* 0xB0 进 BOOT:必须每圈检查,不能只靠 1Hz TASK_HEARTBEAT。
* 否则 Flash 写/复位被拖到下一秒,ESP32 HELLO 一直 role=app 超时。 */
if (OtaBootRequest::isRebootPending()) {
OtaBootRequest::process();
}
/* 电机 OTA relay:活跃时每圈推进状态机(等 ACK / 超时 / 发下一包)。
* 非活跃时立即返回,开销可忽略。 */
MotorOtaRelay::process();
/* 数据发送推进:主循环每圈调用,按 SysTick 钟表控制每包间隔
* (替代原靠 tick100us 计数 + TASK_MOTOR_OTA_TX 的方式,消除计数器竞争)。 */
MotorOtaRelay::onTxTick();
/* 2026-08 请求驱动改造:0xC1 CAN 反馈 / 0xC5 电机状态 / 0xC4 ADC 已改为
* ESP32 周期查询(主动按铃推送全部移除,避免 OTA 期间悬空按铃刷屏)。
* 仅保留事件驱动、低频的 0xC6 电机版本推送。 */
motorVersionPushStep();
/* SSC 协议栈主循环:AL 状态机 + 邮箱处理 + FreeRun PDO。
* 每轮无条件调用,SSC 内部自行判断是否有待处理事件。
* DC 模式下 PDO 交换由 SYNC0 ISR (Sync0_Isr) 完成,此处仅处理低优先级逻辑。 */
MainLoop();
/* 1.5.48:AL Event Mask(0x204) 守护——每圈入口执行,内部 1s 节流。
* 放 MainLoop 旁而非诊断任务:守护必须独立于诊断打印频率,
* OP 等待期(12s)内从站进 SAFEOP 后 1s 内即介入自愈。 */
ethercatAleMaskGuard();
const uint32_t pending = TaskManager::takePending();
if(pending == 0U) return;
if(pending & (1UL << TASK_ADC_VREF_SAMPLE)) taskAdcVrefSample();
if(pending & (1UL << TASK_ADC_TEMP_SAMPLE)) taskAdcTempSample();
if(pending & (1UL << TASK_ADC_PROCESS)) taskAdcProcess();
if(pending & (1UL << TASK_CAN_RX_PARSE)) taskCanRxParse();
if(pending & (1UL << TASK_CAN_TX_SEND)) taskCanTxSend();
if(pending & (1UL << TASK_HEARTBEAT)) taskHeartbeat();
if(pending & (1UL << TASK_DIAGNOSTIC_PRINT)) taskDiagnosticPrint();
}