Files
DCU_APP/GD32E508_DCU_APP/sys/task_runner.cpp
T
cbb 33037bc5a3 feat(spi): 实现SPI同步通信协议和主动推送机制
- 实现新的SPI同步帧格式,采用0xA5 0x5A起始符和CRC32校验,提升通信可靠性
- 添加GD32与ESP32间的握手机制,通过PB6开漏输出实现SPIready线与控制
- 实现异步OTA启动机制,将Flash操作延迟到主循环执行,避免SPI ISR阻塞
- 改进CAN反馈命令处理,提取通用payload构建函数支持主动推送模式
- 实现GD32主动按铃机制,循环推送CAN反馈数据到ESP32,减少轮询开销
- 添加SPI通信超时保护和死锁防护机制,确保系统稳定性
2026-07-08 02:51:24 +08:00

412 lines
15 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(); }
/**
* @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();
/* 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();
if(pending & (1UL << TASK_SPI_LOG)) AppSpiCommandBus.poll();
if(pending & (1UL << TASK_SPI_CMD_PROCESS)) taskSpiCmdProcess();
if(pending & (1UL << TASK_ETHERCAT_PROCESS)) taskEthercatProcess();
}