Files
DCU_APP/GD32E508_DCU_APP/sys/task_runner.cpp
T
cbb 3691cf2aba refactor(spi): 优化CAN反馈推送的节流机制
将CAN反馈推送改为1Hz节流,由taskHeartbeat每秒调用
canFeedbackStartRound激活新轮次,避免与CANFD刷新频率不匹配。

添加s_roundActive标志控制推送节奏,确保每轮完成后等待下一秒
再开始新的推送循环。

---

feat(spi): 实现SPI就绪状态检测和链路状态机

新增SpiLinkState类管理SPI链路状态,通过EXTI6检测PB6引脚
的上升沿来判断ESP32是否完成上一帧处理。

配置SPI Ready引脚的外部中断,实现可靠的ESP32完成信号检测,
避免在ESP32未就绪时过早发起新的SPI通信。

修复SPI接收溢出处理,改为丢弃溢出字节而非重置整个接收缓冲区,
提高SPI通信稳定性。
2026-07-09 02:47:22 +08:00

419 lines
16 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/temp_sensor.h"
#include "sys/config.h"
#include "sys/ota_boot_request.h"
#include "sys/task_manager.h"
#include "sys/task_runner.h"
#include "comm/ethercat_slave.h"
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 {
temp_sensor_data_t 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 接收轮询任务。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 当前三路 CANFD 接收已由 RX0 中断驱动,该任务仅保留接口兼容性。
*/
void TaskRunner::taskCanRxPoll() { CanFd::poll(); }
/**
* @brief 执行预留的 CAN 发送执行任务。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 当前阶段不做周期性发送调度;若后续需要显式验证某一路发送能力,可由调用方先 prepare,再直接调用 `CanFd::sendPrepared()`。
*/
void TaskRunner::taskCanTxSend()
{
/* 启动后每 100ms 轮流发送一路 CAN 通道的 CANFD+BRS 64 字节测试帧。
* 三路通道时分复用:CH0→CH1→CH2→CH0→...,每 100ms 只发一路,
* 避免多路同时发送导致总线仲裁冲突(当多路并接到同一总线时)。
* 帧格式:扩展帧 29-bit ID,CANFD+BRS,DLC=64。
* 载荷布局:
* [0] 通道号 0/1/2
* [1] 保留
* [2..5] 发送序号(小端 uint32,每通道独立递增)
* [6..9] 系统运行秒数 g_app_uptime_sec(小端 uint32)
* [10..63] 递增填充,按通道偏移便于抓包区分 */
static uint32_t seq[3] = {0U, 0U, 0U};
static uint8_t round = 0U; /* 0=CH0, 1=CH1, 2=CH2, 轮流 */
const uint8_t ch = round;
round = (round + 1U) % 3U;
/* 跳过已禁用的通道 */
if (!CanFd::isChannelEnabled(ch)) {
return;
}
uint8_t data[64];
data[0] = ch;
data[1] = 0U;
const uint32_t s = seq[ch];
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);
data[8] = static_cast<uint8_t>((g_app_uptime_sec >> 16) & 0xFFU);
data[9] = static_cast<uint8_t>((g_app_uptime_sec >> 24) & 0xFFU);
for (uint8_t i = 10U; i < 64U; ++i) {
data[i] = static_cast<uint8_t>(i + ch * 0x40U);
}
const uint32_t extId = 0x1FFFFF00U | static_cast<uint32_t>(ch);
CanFd::transmitFrame(ch, extId, data, 64U);
seq[ch]++;
}
/**
* @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;
}
consumed++;
}
if(CanFd::hasRxFrame()) {
TaskManager::set(1UL << TASK_CAN_RX_PARSE);
}
}
/**
* @brief 执行预留的 CAN 发送准备任务。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 当前阶段三路 CANFD 仅要求底层收发能力可用,不做周期性业务状态广播。
*/
void TaskRunner::taskCanTxPrepare()
{
/* 当前阶段不做周期性业务状态广播。 */
}
/**
* @brief 执行状态灯维护任务。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 当前仅保留任务入口。
*/
void TaskRunner::taskStatusLed() { /* 预留 */ }
/**
* @brief 执行系统心跳维护任务。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 当前仅保留任务入口。
*/
void TaskRunner::taskHeartbeat() { OtaBootRequest::process(); CanFd::refreshFeedbackSnapshot(); canFeedbackStartRound(); }
/**
* @brief 输出当前温度、电压与 CAN 诊断信息。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note 由 1 秒节拍触发,通过 UART 打印当前温度、电压;采用定点整数格式,避免依赖浮点 `printf` 支持。
*/
void TaskRunner::taskDiagnosticPrint()
{
const auto *diag = CanFd::diag();
const auto &s = s_runtime.sample;
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_drop=%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_drop_count),
static_cast<unsigned long>(diag->tx_abort_count),
static_cast<unsigned long>(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());
}
}
}
/**
* @brief 执行 SPI CS release 后推迟的命令解析(从 ISR 移出)。
* @param[in] 无。
* @param[out] 无。
* @retval 无返回值。
* @note ISR 瘦身:onCsRelease() 仅置位 TASK_SPI_CMD_PROCESS,实际解析在此执行。
*/
void TaskRunner::taskSpiCmdProcess()
{
AppSpiCommandBus.poll(); // 命令处理已移入 ISR,此处仅日志打印+按铃看护
}
/**
* @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();
/* poll() 含按铃超时看门狗(cancelRing),必须每轮无条件执行。
* 否则日志关闭时(CONFIG_SPI_APP_LOG_ENABLE=0) m_logPending 永远为 false,
* TASK_SPI_LOG 永不置位,poll() 永不执行,m_ringPending 卡死导致
* canFeedbackPushStep() 永久 return -> SPI 反馈链路死锁。
* 放在 canFeedbackPushStep() 前确保卡死的按铃先被清掉再推送下一帧。 */
AppSpiCommandBus.poll();
/* CAN 反馈主动推送:GD32 APP 循环推送 3 通道 × 2 页反馈帧到 ESP32,
* 替代 ESP32 主动轮询。非阻塞:上一帧未读走时立即返回。
* 每次主循环迭代调用一次,ESP32 读走一帧后 CS 释放中断唤醒主循环继续推送。 */
canFeedbackPushStep();
/* 周期路径( SYNC0 ISR preempt=0 )用 s_periodicSeq 序列号通知主循环,
* 不再走 task_flags,避免 BASEPRI 临界区与 preempt=0 的 RMW 竞态。
* 优先于 takePending() 处理,确保周期数据及时消费。
* ESC 未焊接时 selfTest 失败,isEscReady()=false,中断未使能,
* s_periodicSeq 不会递增,此分支自然不触发——双重守卫仅为防御性编程。 */
if (EthercatSlave::isEscReady() && EthercatSlave::hasPendingPeriodic()) {
EthercatSlave::markPeriodicProcessed();
taskEthercatProcess();
}
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_POLL)) taskCanRxPoll();
if(pending & (1UL << TASK_CAN_RX_PARSE)) taskCanRxParse();
if(pending & (1UL << TASK_CAN_TX_SEND)) taskCanTxSend();
if(pending & (1UL << TASK_STATUS_LED)) taskStatusLed();
if(pending & (1UL << TASK_HEARTBEAT)) taskHeartbeat();
if(pending & (1UL << TASK_DIAGNOSTIC_PRINT)) taskDiagnosticPrint();
/* TASK_SPI_LOG 的 poll() 已移至 runPending() 开头无条件调用(含按铃看门狗) */
if(pending & (1UL << TASK_SPI_CMD_PROCESS)) taskSpiCmdProcess();
if(pending & (1UL << TASK_ETHERCAT_PROCESS)) taskEthercatProcess();
}