/** * @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/esc_eeprom.h" #include "comm/esc_exmc_bus.h" #include "comm/ssc/port/eeprom_image.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(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 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; } /** * @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()) { uint32_t maxCyc = 0U, avgCyc = 0U, cnt = 0U; EthercatSlave::getIsrStats(&maxCyc, &avgCyc, &cnt); /* 180MHz: cycles / 180 = µs;用 *1000/180 获得亚微秒精度避免浮点 */ const uint32_t maxUs_x1000 = maxCyc * 1000U / 180U; const uint32_t avgUs_x1000 = avgCyc * 1000U / 180U; printf("[ECAT] periodicIsr cnt=%lu max=%lu.%03luus avg=%lu.%03luus\r\n", (unsigned long)cnt, (unsigned long)(maxUs_x1000 / 1000U), (unsigned long)(maxUs_x1000 % 1000U), (unsigned long)(avgUs_x1000 / 1000U), (unsigned long)(avgUs_x1000 % 1000U)); } else { printf("[ECAT] ESC not detected (chip id=0x%08lX), EtherCAT disabled\r\n", (unsigned long)EthercatSlave::getChipId()); } } #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; static uint32_t s_prev_ecat_max_cyc = 0U; 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; } if (esc_ready) { uint32_t maxCyc = 0U, avgCyc = 0U, cnt = 0U; EthercatSlave::getIsrStats(&maxCyc, &avgCyc, &cnt); /* 阈值单位 µs → CPU 周期(180 MHz) */ const uint32_t threshCyc = CONFIG_ETHERCAT_ISR_MAX_US_THRESHOLD * 180U; if (maxCyc > threshCyc && maxCyc > s_prev_ecat_max_cyc) { const uint32_t maxUs_x1000 = maxCyc * 1000U / 180U; printf("[ECAT] ISR max over threshold: max=%lu.%03luus thr=%luus cnt=%lu (uptime=%lus)\r\n", (unsigned long)(maxUs_x1000 / 1000U), (unsigned long)(maxUs_x1000 % 1000U), (unsigned long)CONFIG_ETHERCAT_ISR_MAX_US_THRESHOLD, (unsigned long)cnt, (unsigned long)g_app_uptime_sec); s_prev_ecat_max_cyc = maxCyc; } } } #endif } /** * @brief 执行 EtherCAT 周期/事件处理(非实时应用层 PDO 映射)。 * @param[in] 无。 * @param[out] 无。 * @retval 无返回值。 * @note 由周期 ISR(EXTI0 SYNC0)或 ESC 事件 ISR(EXTI1 IRQ)置位 TASK_ETHERCAT_PROCESS。 */ void TaskRunner::taskEthercatProcess() { EthercatSlave::process(); } /** * @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 探活,方便热插拔/焊接后观察 === */ static uint32_t s_lastDiagSec = 0xFFFFFFFFU; if (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(); } /* 延迟 EEPROM 烧录:init 不阻塞,主循环第一轮执行一次。 * 校验编译期镜像与 AT24C32 内容,不匹配则全量烧录(~20s,期间看门狗正常喂)。 */ static bool s_eepromDone = false; if (!s_eepromDone) { s_eepromDone = true; if (EscEeprom::init()) { if (EscEeprom::verifyEsi(ESI_EEPROM_IMAGE, ESI_EEPROM_SIZE)) { printf("[ECAT][EEPROM] content OK, skip burn\r\n"); } else { printf("[ECAT][EEPROM] content mismatch, burning ESI (%u bytes)...\r\n", (unsigned)ESI_EEPROM_SIZE); if (EscEeprom::writeEsi(ESI_EEPROM_IMAGE, ESI_EEPROM_SIZE)) { if (EscEeprom::verifyEsi(ESI_EEPROM_IMAGE, ESI_EEPROM_SIZE)) { EscEeprom::reload(); printf("[ECAT][EEPROM] burn+verify OK, reload issued\r\n"); } else { printf("[ECAT][EEPROM] post-burn verify FAIL\r\n"); } } else { printf("[ECAT][EEPROM] burn FAIL\r\n"); } } } else { printf("[ECAT][EEPROM] init FAIL (PDI control not acquired)\r\n"); } } /* 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(); 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(); }