Files
DCU_APP/GD32E508_DCU_APP/sys/task_runner.cpp
T
cbb 28d8426052 feat(ethercat): 添加ESP32使能控制功能通过EtherCAT SDO
添加0x2503对象字典项用于通过EtherCAT SDO控制ESP32 WiFi使能信号,
解决WiFi关断后无法重新开启的问题。同时添加sys/esp32_en.h/.cpp文件
提供GPIO控制接口。

SPI诊断命令(0xA2)扩展支持ESP32 EN引脚控制,用于诊断备援。
ESP32掉电重启后自动清理SPI链路状态,防止通信异常。
2026-09-24 03:18:18 +08:00

647 lines
26 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/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/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;
}
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
/* 旁路投递给 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<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()) {
/* 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;
}
/* 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();
if (motorOtaActive) {
CpuLoad::beginWork();
}
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();
}