/** * @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/initial.h" #include "sys/ota_boot_request.h" #include "sys/motor_ota_relay.h" #include "sys/task_manager.h" #include "sys/cpu_load.h" #include "sys/esp32_en.h" #include "sys/task_runner.h" #include "comm/ethercat_slave.h" #include "comm/ethercat_sync.h" #include "comm/ethercat_can_bridge.h" #include "comm/ecat_motor_ota.h" #include "comm/esc_exmc_bus.h" #include "comm/esc_lan9252.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; } if (MotorOtaRelay::isChannelLocked(ch)) { continue; /* 电机 OTA 独占:诊断帧也不许挤这条通道 */ } uint8_t data[64]; data[0] = 0xC0U; data[1] = ch; const uint32_t s = seq; data[2] = static_cast(s & 0xFFU); data[3] = static_cast((s >> 8) & 0xFFU); data[4] = static_cast((s >> 16) & 0xFFU); data[5] = static_cast((s >> 24) & 0xFFU); data[6] = static_cast(g_app_uptime_sec & 0xFFU); data[7] = static_cast((g_app_uptime_sec >> 8) & 0xFFU); for (uint8_t i = 8U; i < 64U; ++i) { data[i] = static_cast(0xA0U + (i & 0x0FU)); } const uint32_t extId = 0x18FF5000U | static_cast(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 /* 旁路投递给 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):缺位自愈+WARN,返回实际值/0xFFFF=不适用 */ extern "C" uint32_t EcatDbg_GuardAleventMask(void); /* OP 输入新鲜度不变量 — 输入 PDO 停更则主循环强制重写 */ extern "C" void EcatDbg_OpInputFreshness(void); /** * @brief AL Event Mask(0x204) 守护 + SM2 事件楔死自愈 + 邮箱 SM 地址/长度失配自愈: * 250ms 周期。 * @note PDI_Isr 通路楔死时(0x220 bit10 挂起)守护在 SAFEOP 后 1s 内替 PDI_Isr * 完成工作(置位标志+应答事件),主站 12s OP 等待窗口内可自愈;WARN 携带 * 全部内部旗标,下次复现一行日志定位楔死路径(详见 dc_app.c 注释)。 * 周期必须快过主站 SDO 超时(~700ms),否则自愈永远落在主站放弃之后。 * 邮箱几何已构建期常量化(mailbox.c),CSR 错值无进入缓存路径; * 本守护降级为运行期内存踩踏兜底。 */ static void ethercatAleMaskGuard() { static uint32_t s_lastGuardMs = 0U; if (!EthercatSync::isActive() || EscLan9252::pmSuspended()) { /* 睡眠挂起期一并静默——守护内部读 SM 起止/长度与 0x204/0x220 * 全是 HBI 访问,会把睡着的芯片叫醒 */ return; } const uint32_t nowMs = g_scheduler.tick_1ms; if ((uint32_t)(nowMs - s_lastGuardMs) >= 250U) { s_lastGuardMs = nowMs; EcatDbg_GuardAleventMask(); } } /** * @brief ECAT 心跳:主循环存活证明 + ESC AL 状态/事件寄存器快照。 * @note 仅在同步 ISR 激活(SSC 在跑)时打印。心跳停 = 主循环卡死; * al=0x1 且 evt bit0 长期置位 = SSC 没处理 AL 请求; * al 与主站读到的不一致 = ESC 写回路径问题。 */ static void ethercatAlHeartbeat() { if (!EthercatSync::isActive() || EscLan9252::pmSuspended()) { /* 睡眠挂起期静默——读 0x0130/0x0220 即 HBI 访问,读即唤醒 */ 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(s.temperature * 100.0f); const uint32_t vdda_mV = static_cast(s.vdda * 1000.0f); const uint32_t vin_adc_mV = static_cast(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(s.valid), static_cast(s.temp_raw), static_cast(s.vref_raw), static_cast(temp_x100 / 100), static_cast((temp_x100 >= 0 ? temp_x100 : -temp_x100) % 100), static_cast(vdda_mV / 1000U), static_cast(vdda_mV % 1000U), static_cast(vin_adc_mV / 1000U), static_cast(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(s.valid), static_cast(temp_x100 / 100), static_cast((temp_x100 >= 0 ? temp_x100 : -temp_x100) % 100), static_cast(vdda_mV / 1000U), static_cast(vdda_mV % 1000U), static_cast(vin_adc_mV / 1000U), static_cast(vin_adc_mV % 1000U), static_cast(s_runtime.last_rx_channel), static_cast(s_runtime.last_rx_id), static_cast(s_runtime.last_rx_len), static_cast(diag->last_tx_channel), static_cast(diag->tx_count), static_cast(diag->rx_count), static_cast(diag->rx_drop_count), static_cast(diag->rx_fifo_full_count), static_cast(diag->rx_fifo_overfull_count), static_cast(diag->tx_abort_count), static_cast(diag->error_count)); #endif if (EthercatSlave::isEscReady()) { /* 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(s_prev_adc_valid), static_cast(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(s_prev_esc_ready ? 1U : 0U), static_cast(esc_ready ? 1U : 0U), (unsigned long)EthercatSlave::getChipId(), (unsigned long)g_app_uptime_sec); s_prev_esc_ready = esc_ready; } /* 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(); /* 高 PDO 频率时:有帧就解析,但**每圈限一次**且预算内尽量清空。 * 每圈都跑满 budget 在 2kHz 可能抢太多主循环(实测达成率掉到 85%)。 * 8kHz 下主循环间隙极短,仍需入口排水,否则 gseq/s 从 ~3750 掉到数百。 */ if (CanFd::hasRxFrame()) { CpuLoad::beginWork(); taskCanRxParse(); CpuLoad::endWork(); } /* 喂狗稳定满 30s 才清 BOOT 崩溃计数(每上电一次)。init 就清会让 * 主循环 3s 内 FWDGT 崩溃永远攒不到 OTA_CRASH_STAY_MAX。 */ OtaBootRequest::maybeClearCrashCount(g_app_uptime_sec); /* === HBI 周期诊断:每 20 秒重新跑一次 EXMC/HBI 探活,方便热插拔/焊接后观察 === * EtherCAT 同步 ISR 激活后停止(diagDump 会把 EXMC 引脚切成 GPIO, * 拆毁运行中的总线;诊断的 bit-bang 也与 HBI 事务互斥冲突)。 * 整块由 CONFIG_ESC_EXMC_DIAG 门控,默认关闭;换板/补焊排查浮空时置 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 /* ESI 镜像由 EtherCAT 主站侧刷写 * (tools/ecat/ecat_flash_eeprom.py 经 ESC 0x0500 EEPROM 接口), * 固件不承担烧录/校验职责。 */ /* poll() 含按铃超时看门狗(cancelRing),必须每轮无条件执行。 * 主动推送已随 2026-08 请求驱动改造移除,此处仅需消费请求响应的按铃。 */ AppSpiCommandBus.poll(); /* PC9 拉高后 ESP32 起机窗口内的 SPI 补清(GPIO 毛刺会弄脏链路)。 */ Esp32En::poll(); /* 0xB0 进 BOOT:必须每圈检查,不能只靠 1Hz TASK_HEARTBEAT。 * 否则 Flash 写/复位被拖到下一秒,ESP32 HELLO 一直 role=app 超时。 */ if (OtaBootRequest::isRebootPending()) { CpuLoad::beginWork(); OtaBootRequest::process(); CpuLoad::endWork(); } /* 电机 OTA relay:活跃时每圈推进状态机(等 ACK / 超时 / 发下一包)。 * 非活跃时立即返回,开销可忽略。 */ /* 负载率挂钩:relay 活跃时才计 busy——isActive() 是廉价枚举判断, * 空闲路径零挂钩开销;计数段覆盖等 ACK/超时/发包的真实工作。 */ const bool motorOtaActive = MotorOtaRelay::isActive() || EcatMotorOta::sessionActive(); if (motorOtaActive) { CpuLoad::beginWork(); } /* EtherCAT 邮箱进料(comm/ecat_motor_ota.cpp):把本地 48KB 暂存区按环缓空位 * 泵进 relay。放在 relay::process() 之前,同圈刚泵进去的字节立刻能被弹包。 * WiFi/SPI 链路下 s_started=false,这里第一次判断就返回,开销可忽略。 */ EcatMotorOta::process(); MotorOtaRelay::process(); /* 数据发送推进:主循环每圈调用,按 SysTick 钟表控制每包间隔 * (替代原靠 tick100us 计数 + TASK_MOTOR_OTA_TX 的方式,消除计数器竞争)。 */ MotorOtaRelay::onTxTick(); if (motorOtaActive) { CpuLoad::endWork(); } /* 仅保留事件驱动、低频的 0xC6 电机版本推送。 */ motorVersionPushStep(); /* SSC 协议栈主循环:AL 状态机 + 邮箱处理 + FreeRun PDO。 * 每轮无条件调用,SSC 内部自行判断是否有待处理事件。 * DC 模式下 PDO 交换由 SYNC0 ISR (Sync0_Isr) 完成,此处仅处理低优先级逻辑。 * ESC 睡眠(D1-D3)挂起期整段停调——MainLoop 每圈必读 0x220(AL 事件 * 寄存器,经 HBI),而睡眠中任何 HBI 访问(含读)都会把芯片打回 D0。 * * 负载口径(恢复 1.5.63):MainLoop **不**计入 busy。 * 实时模式主循环是 NOP 忙等,每圈都调 MainLoop;整段计入会把 * load_perm 抬到恒 ~99%(250Hz 也「占满」,与历史 4kHz≈80% 曲线对不上)。 * 过程数据成本应由 PDI/Sync0 ISR 挂钩体现,见 sys/cpu_load.h。 */ if (!EscLan9252::pmSuspended()) { MainLoop(); /* OP 输入新鲜度不变量。与 MainLoop 同拍调用——正常时 * PDI_Isr 已在刷新,这里只做廉价 seq 比对;停更超 5ms 才强制重写。 * 睡眠期与 MainLoop 一并跳过(读 HBI 会唤醒芯片)。 */ EcatDbg_OpInputFreshness(); } /* AL Event Mask(0x204) 守护——每圈入口执行,内部 250ms 节流。 * 放 MainLoop 旁而非诊断任务:守护必须独立于诊断打印频率, * OP 等待期(12s)内从站进 SAFEOP 后 1s 内即介入自愈。 */ ethercatAleMaskGuard(); const uint32_t pending = TaskManager::takePending(); if(pending == 0U) return; /* 负载率挂钩:派发的周期任务属"非延迟工作",整段计入 busy; * 前面的常开轮询(Watchdog/poll/MainLoop/guard)不计入,见 sys/cpu_load.h。 */ CpuLoad::beginWork(); 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(); CpuLoad::endWork(); }