From a52d94687bb4f0ff0e9a5debdf08e2148b44fc48 Mon Sep 17 00:00:00 2001 From: cbb <2625274797@qq.com> Date: Wed, 15 Jul 2026 00:34:56 +0800 Subject: [PATCH] =?UTF-8?q?refactor(SPI):=20=E5=AE=9E=E7=8E=B0=E5=85=A8?= =?UTF-8?q?=E5=8F=8C=E5=B7=A5DMA=E4=BC=A0=E8=BE=93=E4=BB=A5=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E9=80=9A=E4=BF=A1=E5=8F=AF=E9=9D=A0=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 配置DMA0_CH1用于SPI0_RX接收,替代原有的RBNE中断逐字节处理 - 添加startSpiRxDma、stopSpiDmas、drainSpiRxHw等DMA控制函数 - 在CS空闲期预装下一笔DMA传输,避免EXTI CS下降沿相对于SCK偏晚导致收0字节 - 与BOOT工程SpiOta的DMA配置对齐,确保通信稳定性 refactor(SPI): 优化CS边沿处理逻辑和命令解析时机 - 移除原有的EXTI5_9_IRQHandler用于SPI Ready引脚处理 - 将命令处理从异步任务调度改为在CS释放中断中同步完成 - 重构onCsAssert和onCsRelease逻辑,简化DMA启停流程 - 删除TASK_SPI_CMD_PROCESS任务类型,减少不必要的任务调度 feat(CANFD): 修改transmitFrame返回类型为枚举值以提供详细错误信息 - 将CanFd::transmitFrame返回类型从bool改为CanTxResult枚举 - 新增CanTxResult枚举定义,包含kOk、kErrBusOff、kErrTecHigh、kErrMailbox、kErrInvalid - 更新SPI桥接命令处理中对transmitFrame返回值的处理逻辑 - 枚举值设计与SPI错误响应码对齐,便于直接透传错误信息 refactor(SPI): 实现全双工DMA传输以提升通信可靠性 - 配置DMA0_CH1用于SPI0_RX接收,替代原有的RBNE中断逐字节处理 - 添加startSpiRxDma、stopSpiDmas、drainSpiRxHw等DMA控制函数 - 在CS空闲期预装下一笔DMA传输,避免EXTI CS下降沿相对于SCK偏晚导致收0字节 - 与BOOT工程SpiOta的DMA配置对齐,确保通信稳定性 refactor(SPI): 优化CS边沿处理逻辑和命令解析时机 - 移除原有的EXTI5_9_IRQHandler用于SPI Ready引脚处理 - 将命令处理从异步任务调度改为在CS释放中断中同步完成 - 重构onCsAssert和onCsRelease逻辑,简化DMA启停流程 - 删除TASK_SPI_CMD_PROCESS任务类型,减少不必要的任务调度 feat(OTA): 添加BOOT版本号读取功能并优化版本信息获取 - 在ota_boot_request模块中添加readBootVersion函数 - 从OTA配置页Flash读取BOOT版本号信息 - 在SPI PING命令响应中使用新的版本读取接口 - 统一版本号获取方式,避免裸指针访问Flash地址 refactor(task): 调整诊断打印任务调度周期为5秒间隔 - 新增div_diag_print调度分频计数器用于诊断打印 - 将诊断打印任务从心跳任务中分离,独立控制执行周期 - 修改诊断打印函数注释,明确其由5秒节拍触发 - 优化任务调度逻辑,提高系统运行效率 --- GD32E508_DCU_APP/device/CANFD.cpp | 18 +- GD32E508_DCU_APP/device/canfd.h | 17 +- .../device/spi/app_spi_cmd_can_bridge.cpp | 6 +- .../device/spi/app_spi_cmd_ping.cpp | 16 +- .../device/spi/app_spi_command.cpp | 240 +++++++++++------- GD32E508_DCU_APP/device/spi/app_spi_command.h | 13 +- GD32E508_DCU_APP/main.h | 5 +- GD32E508_DCU_APP/sys/config.h | 6 +- GD32E508_DCU_APP/sys/ota_boot_request.cpp | 23 +- GD32E508_DCU_APP/sys/ota_boot_request.h | 27 ++ GD32E508_DCU_APP/sys/task_manager.cpp | 12 +- GD32E508_DCU_APP/sys/task_runner.cpp | 17 +- GD32E508_DCU_APP/sys/task_runner.h | 2 - 13 files changed, 254 insertions(+), 148 deletions(-) diff --git a/GD32E508_DCU_APP/device/CANFD.cpp b/GD32E508_DCU_APP/device/CANFD.cpp index 39f24b7..0a9746b 100644 --- a/GD32E508_DCU_APP/device/CANFD.cpp +++ b/GD32E508_DCU_APP/device/CANFD.cpp @@ -812,16 +812,16 @@ bool CanFd::consumeRxFrame(uint32_t *id, uint8_t *data, uint8_t *length, uint8_t * @param[in] dlc 数据长度(1~64)。 * @param[in] periodic 是否为周期性发送;true 时仅允许一个邮箱保持自动重发, * 不主动 abort 旧帧,且跳过 TEC 保护以匹配诊断行为。 - * @retval bool true 表示发送请求已进入硬件邮箱,false 表示参数无效或邮箱已满。 + * @retval CanTxResult kOk 表示发送请求已进入硬件邮箱;其它值表示具体失败原因。 * @note 绕过软件发送队列,直接调用硬件发送;适用于 SPI 桥接等即时发送场景。 * 网页发送数仅在 TX 中断确认 MTFNERR 后增加,不能用本函数返回值判断总线 ACK。 */ -bool CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id, - const uint8_t *data, uint8_t dlc, bool periodic) +CanFd::CanTxResult CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id, + const uint8_t *data, uint8_t dlc, bool periodic) { if (channelIndex >= kChannelCount || !kChannelEnabled[channelIndex] || data == nullptr || dlc == 0U || dlc > 64U) { - return false; + return CanTxResult::kErrInvalid; } /* 总线健康检查:BUS_OFF 或 TEC 过高时跳过发送,防止测试帧把通道推向 BUS_OFF */ @@ -840,7 +840,7 @@ bool CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id, static_cast(CAN_TSTAT(canPeriph))); s_lastBusOffLogSeq[channelIndex] = s_feedbackSeq; } - return false; /* 通道处于 BUS_OFF,不发送 */ + return CanTxResult::kErrBusOff; /* 通道处于 BUS_OFF,不发送 */ } const uint32_t tec = (errReg >> 16) & 0xFFU; if (tec > 96U && !periodic) { @@ -856,7 +856,7 @@ bool CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id, static_cast(CAN_TSTAT(canPeriph))); s_lastTecLogSeq[channelIndex] = s_feedbackSeq; } - return false; /* TEC 过高(>75% 错误被动阈值),暂停发送保护通道 */ + return CanTxResult::kErrTecHigh; /* TEC 过高(>75% 错误被动阈值),暂停发送保护通道 */ } /* 周期发送只允许一个邮箱保持自动重发。无 ACK 时同一帧会持续产生 @@ -865,7 +865,7 @@ bool CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id, if (periodic) { for (uint8_t mb = CAN_MAILBOX0; mb <= CAN_MAILBOX2; ++mb) { if (can_transmit_states(canPeriph, mb) == CAN_TRANSMIT_PENDING) { - return false; + return CanTxResult::kErrMailbox; } } } @@ -911,7 +911,7 @@ bool CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id, s_diag.last_tx_id = ext_id; s_diag.last_tx_channel = channelIndex; s_diag.last_payload_length = dlc; - return true; + return CanTxResult::kOk; } s_diag.error_count++; s_channelStats[channelIndex].error_count++; @@ -928,7 +928,7 @@ bool CanFd::transmitFrame(uint8_t channelIndex, uint32_t ext_id, static_cast(CAN_TSTAT(canPeriph))); s_lastMailboxLogSeq[channelIndex] = s_feedbackSeq; } - return false; + return CanTxResult::kErrMailbox; } /** diff --git a/GD32E508_DCU_APP/device/canfd.h b/GD32E508_DCU_APP/device/canfd.h index 534b3aa..7e7f2f1 100644 --- a/GD32E508_DCU_APP/device/canfd.h +++ b/GD32E508_DCU_APP/device/canfd.h @@ -28,6 +28,19 @@ public: kModeBridge0To12 = 2U, /**< CAN0 桥接模式:硬件过滤等同 AllPass,CAN0 收到的每一帧按原帧格式自动转发到 CAN1 与 CAN2。 */ }; + /** + * @brief CAN 发送结果枚举,枚举值即 SPI 错误响应码,便于 handler 直接透传。 + * @note 0xE0/0xE1 保留给 handler 入口校验(payload 过短/通道非法), + * 不经 transmitFrame 返回;0xE2~0xE5 由 transmitFrame 区分四种失败原因。 + */ + enum class CanTxResult : uint8_t { + kOk = 0x00U, /**< 发送成功,已进入硬件邮箱。 */ + kErrBusOff = 0xE2U, /**< 通道处于 BUS_OFF,不发送。 */ + kErrTecHigh = 0xE3U, /**< TEC > 96,暂停发送保护通道。 */ + kErrMailbox = 0xE4U, /**< 3 个硬件邮箱全 PENDING,abort+重投仍失败。 */ + kErrInvalid = 0xE5U, /**< 入口参数无效(channelIndex/dlc/data 等)。 */ + }; + /** * @brief CANFD 运行诊断统计信息。 */ @@ -122,8 +135,8 @@ public: static void poll(); static bool hasRxFrame(); static bool consumeRxFrame(uint32_t *id, uint8_t *data, uint8_t *length, uint8_t *channel = nullptr); - static bool transmitFrame(uint8_t channelIndex, uint32_t ext_id, - const uint8_t *data, uint8_t dlc, bool periodic = false); + static CanTxResult transmitFrame(uint8_t channelIndex, uint32_t ext_id, + const uint8_t *data, uint8_t dlc, bool periodic = false); static const Diag *diag(); /** diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_bridge.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_bridge.cpp index 17748f7..1dd7fe0 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_bridge.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_can_bridge.cpp @@ -80,15 +80,15 @@ public: return; } - const bool ok = CanFd::transmitFrame(can_ch, can_id, can_data, can_dlc); - if (ok) { + const CanFd::CanTxResult result = CanFd::transmitFrame(can_ch, can_id, can_data, can_dlc); + if (result == CanFd::CanTxResult::kOk) { postCanSendLog(bus, "OK", can_ch, can_id, can_data, can_dlc); const uint8_t ack = 0x00U; bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, SpiDefs::kRspAck, &ack, 1U); } else { postCanSendLog(bus, "BUSY", can_ch, can_id, can_data, can_dlc); - uint8_t err = 0xE2U; + const uint8_t err = static_cast(result); /* 枚举值即 SPI 错误码 */ bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, SpiDefs::kRspError, &err, 1U); } diff --git a/GD32E508_DCU_APP/device/spi/app_spi_cmd_ping.cpp b/GD32E508_DCU_APP/device/spi/app_spi_cmd_ping.cpp index eba9760..93fd570 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_cmd_ping.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_cmd_ping.cpp @@ -2,6 +2,7 @@ #include "device/spi/app_spi_command.h" #include "device/spi/app_spi_command_defs.h" #include "sys/config.h" +#include "sys/ota_boot_request.h" #include "sys/systick.h" #include "main.h" #include @@ -30,15 +31,12 @@ public: ping_payload[2] = (uint8_t)APP_FW_VERSION_PATCH; ping_payload[3] = (uint8_t)APP_FW_VERSION_BUILD; - /* BOOT 版本号从 OTA 配置页 Flash 读取 */ - const volatile uint32_t *bv0 = (const volatile uint32_t *)CONFIG_BOOT_VERSION_ADDR0; - const volatile uint32_t *bv1 = (const volatile uint32_t *)CONFIG_BOOT_VERSION_ADDR1; - uint32_t bw0 = *bv0; - uint32_t bw1 = *bv1; - ping_payload[4] = (uint8_t)(bw0 & 0xFFU); - ping_payload[5] = (uint8_t)((bw0 >> 16) & 0xFFU); - ping_payload[6] = (uint8_t)(bw1 & 0xFFU); - ping_payload[7] = (uint8_t)((bw1 >> 16) & 0xFFU); + /* BOOT 版本号从 OTA 配置页 Flash 读取(封装函数,避免裸指针散落) */ + const BootVersion boot_ver = OtaBootRequest::readBootVersion(); + ping_payload[4] = boot_ver.major; + ping_payload[5] = boot_ver.minor; + ping_payload[6] = boot_ver.patch; + ping_payload[7] = boot_ver.build; const uint32_t uptime_ms = SysTickTimer::getMs(); ping_payload[8] = (uint8_t)(uptime_ms & 0xFFU); diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp index a2b00fc..8d60c48 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command.cpp +++ b/GD32E508_DCU_APP/device/spi/app_spi_command.cpp @@ -55,9 +55,13 @@ static constexpr uint8_t kRspAck = SpiDefs::kRspAck; static constexpr uint8_t kRspWait = SpiDefs::kRspWait; static constexpr uint8_t kRspError = SpiDefs::kRspError; -/* DMA 配置(与 BOOT SpiOta 一致:DMA0_CH2 用于 SPI0 TX) */ +/* DMA 全双工配置(与 BOOT SpiOta 一致): + * DMA0_CH1: SPI0_RX DMA0_CH2: SPI0_TX + * RX 走 DMA 后禁用 RBNE 逐字节中断,避免 256B 事务 RX-OVR/SOF 丢失。 */ #define SPI_TX_DMA_PERIPH DMA0 #define SPI_TX_DMA_CH DMA_CH2 +#define SPI_RX_DMA_PERIPH DMA0 +#define SPI_RX_DMA_CH DMA_CH1 static void startSpiTxDma(const uint8_t *buf, uint16_t len) { @@ -68,6 +72,45 @@ static void startSpiTxDma(const uint8_t *buf, uint16_t len) DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) |= DMA_CHXCTL_CHEN; } +/** @brief 启动 DMA 接收 SPI0_DATA → m_rxBuf(容量 kRxBufSize)。 + * 每次 CS 空闲期预装,下一笔 CS 上升沿时由 stopSpiDmas() 读 CHCNT 反算实际长度。 */ +static void startSpiRxDma(uint8_t *rx_buf, uint16_t rx_cap) +{ + DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + DMA_INTC(SPI_RX_DMA_PERIPH) |= DMA_FLAG_ADD(DMA_CHINTF_RESET_VALUE, SPI_RX_DMA_CH); + DMA_CHMADDR(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) = (uint32_t)rx_buf; + DMA_CHCNT(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) = rx_cap; + DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) |= DMA_CHXCTL_CHEN; +} + +/** @brief 清空 SPI 接收通路残留(含 OVR),对齐 BOOT drainSpiRxHw。 */ +static void drainSpiRxHw(void) +{ + while (spi_i2s_flag_get(SPI0, SPI_FLAG_RBNE) != RESET) { + (void)spi_i2s_data_receive(SPI0); + } + if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { + (void)spi_i2s_data_receive(SPI0); + (void)SPI_STAT(SPI0); + } +} + +/** @brief 停止双 DMA;返回本 CS 实际 RX 字节数(必须在 disable 前读 CHCNT)。 + * 对齐 BOOT stopSpiDmas:先读 remaining 再 disable,否则计数器被清。 */ +static uint16_t stopSpiDmas(uint16_t rx_cap) +{ + const uint16_t remaining = (uint16_t)DMA_CHCNT(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); + spi_dma_disable(SPI0, SPI_DMA_RECEIVE); + uint16_t got = 0U; + if (remaining <= rx_cap) { + got = (uint16_t)(rx_cap - remaining); + } + return got; +} + static bool isAllBytesValue(const uint8_t *data, uint16_t len, uint8_t value) { if (!data || len == 0U) { @@ -264,6 +307,7 @@ void AppSpiCommand::configureSpi0() { rcu_periph_clock_enable(RCU_SPI0); spi_i2s_deinit(SPI0); + (void)spi_i2s_data_receive(SPI0); /* 清空 SPI_DATA 残留值 */ spi_parameter_struct spi_init_struct; spi_struct_para_init(&spi_init_struct); @@ -276,11 +320,14 @@ void AppSpiCommand::configureSpi0() spi_init_struct.prescale = SPI_PSC_8; spi_init(SPI0, &spi_init_struct); + /* 内部 NSS 常低:SPI 从机始终就绪,不依赖 EXTI ISR 切换 internal NSS。 + * PA15 仅作 EXTI 事务边界信号,用于 onCsAssert/onCsRelease 控制 DMA 装载。 */ spi_nss_internal_low(SPI0); (void)spi_i2s_data_receive(SPI0); spi_i2s_data_transmit(SPI0, 0xFFU); - spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_RBNE); - spi_i2s_interrupt_enable(SPI0, SPI_I2S_INT_ERR); + /* RX 改走 DMA0_CH1:禁用 RBNE 逐字节中断,避免 256B 事务 RX-OVR/SOF 丢失。 */ + spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_RBNE); + spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_ERR); spi_i2s_interrupt_disable(SPI0, SPI_I2S_INT_TBE); nvic_irq_enable(SPI0_IRQn, 2U, 0U); spi_enable(SPI0); @@ -289,8 +336,9 @@ void AppSpiCommand::configureSpi0() void AppSpiCommand::configureDma() { rcu_periph_clock_enable(RCU_DMA0); - dma_deinit(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); + /* TX: memory → SPI0_DATA */ + dma_deinit(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); dma_parameter_struct dma_cfg; dma_struct_para_init(&dma_cfg); dma_cfg.periph_addr = (uint32_t)(SPI0 + 0x0CU); /* SPI0_DATA 寄存器 */ @@ -305,6 +353,23 @@ void AppSpiCommand::configureDma() dma_init(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH, &dma_cfg); dma_circulation_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); dma_channel_disable(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH); + + /* RX: SPI0_DATA → m_rxBuf(替代旧 RBNE 逐字节累积)。 + * 与 BOOT SpiOta 对齐:DMA0_CH1 固定映射 SPI0_RX。 */ + dma_deinit(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); + dma_struct_para_init(&dma_cfg); + dma_cfg.periph_addr = (uint32_t)(SPI0 + 0x0CU); + dma_cfg.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; + dma_cfg.memory_addr = (uint32_t)m_rxBuf; + dma_cfg.memory_width = DMA_MEMORY_WIDTH_8BIT; + dma_cfg.number = kRxBufSize; + dma_cfg.priority = DMA_PRIORITY_HIGH; + dma_cfg.periph_inc = DMA_PERIPH_INCREASE_DISABLE; + dma_cfg.memory_inc = DMA_MEMORY_INCREASE_ENABLE; + dma_cfg.direction = DMA_PERIPHERAL_TO_MEMORY; + dma_init(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH, &dma_cfg); + dma_circulation_disable(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); + dma_channel_disable(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH); } void AppSpiCommand::configureExti() @@ -316,26 +381,6 @@ void AppSpiCommand::configureExti() nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U); } -void AppSpiCommand::configureSpiReadyExti() -{ - /* PB6 已在 configureGpio() 配置为开漏输出,此处保留 EXTI6 中断使能。 - * v2 DATA_READY: ESP32 不再操作 PB6,EXTI6 上升沿无业务含义, - * onSpiReadyExti() 仅清标志避免反复触发。 */ - gpio_exti_source_select(GPIO_PORT_SOURCE_GPIOB, GPIO_PIN_SOURCE_6); - exti_init(EXTI_6, EXTI_INTERRUPT, EXTI_TRIG_RISING); - exti_interrupt_enable(EXTI_6); - exti_interrupt_flag_clear(EXTI_6); - nvic_irq_enable(EXTI5_9_IRQn, CONFIG_IRQ_PREEMPT_SPI_USART, 1U); -} - -void AppSpiCommand::onSpiReadyExti() -{ - /* v2 DATA_READY: ESP32 不再操作 PB6,EXTI6 上升沿不再有意义。 - * 清除中断标志避免反复触发。 */ - if (exti_interrupt_flag_get(EXTI_6) != RESET) { - exti_interrupt_flag_clear(EXTI_6); - } -} /* ============================================================ @@ -373,10 +418,55 @@ void AppSpiCommand::init() registerSpiIdSnapshotCommand(AppSpiCommandBus); configureGpio(); - configureSpi0(); configureDma(); + configureSpi0(); configureExti(); - configureSpiReadyExti(); + /* 首笔 CS 到来前预装 DMA,避免 EXTI 晚于首 SCK。 + * 必须在 configureSpi0/configureDma 之后调用。 */ + armSpiDmasForNextCs(); +} + +/** @brief 在 CS 空闲期预装下一笔全双工 DMA。 + * + * 必须在首个 SCK 之前完成:EXTI CS 下降相对 ESP32 时钟经常偏晚, + * 若在 onCsAssert 里才 start DMA,整笔事务会收 0 字节 → 永不 HELLO_ACK → MISO 全 0xFF。 + * + * TX 始终按 kTxBufSize 装载:响应帧在头部,其余 0xFF。 + * 若只 DMA 实际帧长(~26B),主机打 96 时钟时 TX underrun,后续 MISO 不定。 + * + * 对齐 BOOT SpiOta::armSpiDmasForNextCs。 + */ +void AppSpiCommand::armSpiDmasForNextCs() +{ + DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + DMA_CHCTL(SPI_RX_DMA_PERIPH, SPI_RX_DMA_CH) &= ~DMA_CHXCTL_CHEN; + spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); + spi_dma_disable(SPI0, SPI_DMA_RECEIVE); + drainSpiRxHw(); + + /* 预填 TX:有挂起响应用响应帧(头部)+ 0xFF 填充到满缓冲 */ + if (m_txLen > 0U && m_txLen <= kTxBufSize) { + if (m_txLen < kTxBufSize) { + memset(&m_txBuf[m_txLen], 0xFF, (size_t)(kTxBufSize - m_txLen)); + } + } else { + m_txLen = 0U; + memset(m_txBuf, 0xFF, kTxBufSize); + } + + /* RX 缓冲清零(便于诊断时区分有效字节与残留) */ + memset(m_rxBuf, 0x00, kRxBufSize); + m_rxLen = 0U; + + /* 预装 SPI_DATA,避免 DMA 启动前首拍 TBE 吐旧值 */ + spi_i2s_data_transmit(SPI0, m_txBuf[0]); + + /* 先使能 SPI DMA 请求,再开 DMA 通道,保证首字节即可搬运 */ + spi_dma_enable(SPI0, SPI_DMA_RECEIVE); + spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); + startSpiRxDma(m_rxBuf, kRxBufSize); + /* TX 永远满缓冲长度,与主机 96~256B 事务匹配,避免 underrun */ + startSpiTxDma(m_txBuf, kTxBufSize); } /* ============================================================ @@ -715,59 +805,36 @@ void AppSpiCommand::onCsAssert() return; } m_csActive = true; - /* v2 DATA_READY: CS 本身即互斥,PB6 不再表示「忙碌」,不在 CS 下降沿拉低。 - * PB6 仅由 updateDataReady() 依据待发数据驱动,避免与队列语义冲突。 */ - m_rxLen = 0U; + /* CS 下降:DMA 已在空闲期由 armSpiDmasForNextCs() 预装,此处只标记忙碌。 + * v2 DATA_READY: CS 本身即互斥,PB6 不再表示「忙碌」,不在 CS 下降沿拉低。 + * 切勿在这里 stop/restart DMA,否则赶不上 ESP32 已发出的 SCK(对齐 BOOT)。 */ m_txPos = 0U; m_txActiveThisCs = false; - - /* 停止 DMA 并清理旧状态 */ - DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; - spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); - - /* 与 BOOT 一致:清除 SPI 双缓冲中的残留数据 */ - spi_i2s_data_transmit(SPI0, 0xFFU); - if (spi_i2s_flag_get(SPI0, SPI_FLAG_TBE) != RESET) { - spi_i2s_data_transmit(SPI0, 0xFFU); - } - - /* 清除错误标志 */ - if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { - (void)spi_i2s_data_receive(SPI0); - } - if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { - spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR); - } - - /* 启动 DMA 发送响应帧(与 BOOT 完全一致) */ - if (m_txLen > 0U) { - m_txActiveThisCs = true; - spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); - startSpiTxDma(m_txBuf, m_txLen); - } else { - /* 无响应时发送全 0xFF 填充 */ - spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); - startSpiTxDma(m_txBuf, kTxBufSize); - } } void AppSpiCommand::onCsRelease() { if (!m_csActive) { + /* EXTI 丢失下降沿时:仍尝试收尾并 re-arm,避免永久失步(对齐 BOOT) */ + m_rxLen = stopSpiDmas(kRxBufSize); + drainSpiRxHw(); + if (m_txLen > 0U) { + m_txLen = 0U; + m_txPos = 0U; + memset(m_txBuf, 0xFF, sizeof(m_txBuf)); + } + if (m_rxLen >= V2_OVERHEAD) { + processCommand(); + } + armSpiDmasForNextCs(); + updateDataReady(); return; } m_csActive = false; - /* 停止 DMA */ - DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; - spi_dma_disable(SPI0, SPI_DMA_TRANSMIT); - - if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) { - uint8_t last_byte = (uint8_t)(spi_i2s_data_receive(SPI0) & 0xFFU); - if (m_rxLen < sizeof(m_rxBuf)) { - m_rxBuf[m_rxLen++] = last_byte; - } - } + /* 停 DMA 并取本笔 RX 长度(先读 CHCNT 再 disable,否则计数器被清) */ + m_rxLen = stopSpiDmas(kRxBufSize); + drainSpiRxHw(); /* 若本次 CS 事务是 ESP32 响应 GD32 按铃来读请求帧, * 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。 @@ -810,8 +877,9 @@ void AppSpiCommand::onCsRelease() /* v2 DATA_READY: 已清 m_txLen=0 与 m_ringPending=false, * updateDataReady() 释放为高;ESP32 完成 CS 即视为已读走确认。 */ m_linkState.onEsp32Complete(); + /* 重新预装下一笔 DMA,保证下一 CS 首字节即可收发 */ + armSpiDmasForNextCs(); updateDataReady(); - /* 上层若需要 ESP32 处理该请求帧,由 ESP32 侧读到的帧 TYPE 决定后续动作 */ return; } } @@ -826,6 +894,7 @@ void AppSpiCommand::onCsRelease() m_txActiveThisCs = false; /* v2 DATA_READY: shouldIgnoreTransaction 路径已清 m_txLen=0, * updateDataReady() 释放为高,避免 PB6 卡 LOW。 */ + armSpiDmasForNextCs(); updateDataReady(); return; } @@ -836,8 +905,12 @@ void AppSpiCommand::onCsRelease() memset(m_txBuf, 0xFF, sizeof(m_txBuf)); /* 命令处理在 EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame 模式), - * 消除主循环延迟。处理完成后 processCommand() 末尾释放 SPIready。 */ + * 消除主循环延迟。处理完成后 processCommand() 末尾再 re-arm DMA + 释放 SPIready。 */ processCommand(); + + /* 处理完立即预装下一笔,保证下一 CS 首字节即可收发 */ + armSpiDmasForNextCs(); + updateDataReady(); } /* ============================================================ @@ -846,26 +919,14 @@ void AppSpiCommand::onCsRelease() void AppSpiCommand::onInterrupt() { - if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) { - if (!m_csActive && gpio_input_bit_get(GPIOA, GPIO_PIN_15) == RESET) { - onCsAssert(); - } - uint8_t byte = (uint8_t)(spi_i2s_data_receive(SPI0) & 0xFFU); - if (m_csActive && m_rxLen < sizeof(m_rxBuf)) { - m_rxBuf[m_rxLen++] = byte; - } - } - - /* TBE 已由 DMA 替代,不再处理 */ - + /* RX 已改走 DMA0_CH1:RBNE 逐字节中断已禁用,本 ISR 只保留错误标志清理。 + * CS 边沿处理全部在 onCsExtiInterrupt 的 EXTI15 ISR 里完成。 */ if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { - /* 读取并丢弃 overrun 字节以清除标志,但保留已收到的帧数据。 - * 丢失的字节会导致 CRC 校验失败,由 spi_try_parse_frame 优雅处理 - * (返回 false -> processCommand 构建 ERROR 响应), - * 而不是重置 m_rxLen 导致整帧丢失。 */ + /* RXORERR 理论上不应再出现(RX 走 DMA),但兜底清理避免标志卡死。 + * 读 SPI_DATA + SPI_STAT 清除 OVR 标志。 */ (void)spi_i2s_data_receive(SPI0); + (void)SPI_STAT(SPI0); } - if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { spi_i2s_format_error_clear(SPI0, SPI_FLAG_FERR); } @@ -892,8 +953,3 @@ extern "C" void EXTI10_15_IRQHandler(void) { AppSpiCommandBus.onCsExtiInterrupt(); } - -extern "C" void EXTI5_9_IRQHandler(void) -{ - AppSpiCommandBus.onSpiReadyExti(); -} diff --git a/GD32E508_DCU_APP/device/spi/app_spi_command.h b/GD32E508_DCU_APP/device/spi/app_spi_command.h index 943d854..2e0adea 100644 --- a/GD32E508_DCU_APP/device/spi/app_spi_command.h +++ b/GD32E508_DCU_APP/device/spi/app_spi_command.h @@ -71,10 +71,6 @@ public: /** @brief CS 边沿中断处理入口。 */ void onCsExtiInterrupt(); - /** @brief SPI Ready(PB6) EXTI6 中断入口。 - * v2 DATA_READY: ESP32 不再操作 PB6,EXTI6 仅清标志。 */ - void onSpiReadyExti(); - /** * @brief 注册一个 SPI 命令处理器,用于 TYPE 路由。 * @param[in] handler 命令处理器指针,调用方需保证其生命周期有效。 @@ -145,11 +141,14 @@ private: static constexpr uint32_t kRingTimeoutMs = 50U; volatile bool m_csActive = false; + /** 本笔 CS 实际收到的字节数(CS 上升沿时由 RX DMA CHCNT 反算得到)。 */ volatile uint16_t m_rxLen = 0U; uint16_t m_txLen = 0U; volatile uint8_t m_txPos = 0U; volatile bool m_logPending = false; bool m_txActiveThisCs = false; + /** RX DMA 接收缓冲(替代旧 RBNE 逐字节累积的 m_rxBuf)。 + * CS 上升沿时 m_rxLen 为本笔实际收到的字节数,本缓冲前 m_rxLen 字节有效。 */ uint8_t m_rxBuf[kRxBufSize] = {}; uint8_t m_txBuf[kTxBufSize] = {}; char m_logBuf[kLogBufSize] = {}; @@ -170,11 +169,15 @@ private: void configureSpi0(); void configureDma(); void configureExti(); - void configureSpiReadyExti(); void onCsAssert(); void onCsRelease(); void processCommand(); + /** @brief 在 CS 空闲期预装下一笔全双工 DMA(TX + RX)。 + * 必须在首个 SCK 之前完成,否则 EXTI CS 下降相对 ESP32 时钟偏晚, + * 整笔事务会收 0 字节。对齐 BOOT SpiOta::armSpiDmasForNextCs。 */ + void armSpiDmasForNextCs(); + /** @brief 翻转 SPI Ready 握手引脚(PB6)电平(开漏线与)。 * @param[in] ready true=释放(高阻,靠上拉拉高), false=下拉(驱动低)。 * @note GPIO BRR 写入原子,无需临界区;可由 ISR 与主循环共同调用。 diff --git a/GD32E508_DCU_APP/main.h b/GD32E508_DCU_APP/main.h index 4c47d5c..3db432e 100644 --- a/GD32E508_DCU_APP/main.h +++ b/GD32E508_DCU_APP/main.h @@ -23,7 +23,6 @@ enum system_task_t { TASK_DIAGNOSTIC_PRINT, /**< 诊断信息打印任务。 */ TASK_SPI_LOG, /**< SPI 延迟日志打印任务。 */ TASK_ETHERCAT_PROCESS, /**< EtherCAT 周期/事件处理任务(非实时应用层 PDO 映射)。 */ - TASK_SPI_CMD_PROCESS, /**< SPI CS release 后的命令解析任务(从 ISR 移出)。 */ TASK_COUNT /**< 任务总数,用于边界或统计场景。 */ }; @@ -49,8 +48,10 @@ struct task_scheduler_t { volatile uint16_t div_can_tx; /** @brief 调度分频计数:状态灯(100ms)。 */ volatile uint16_t div_status_led; - /** @brief 调度分频计数:心跳与诊断打印。 */ + /** @brief 调度分频计数:心跳(1s)。 */ volatile uint16_t div_heartbeat; + /** @brief 调度分频计数:APP 状态/诊断打印(5s)。 */ + volatile uint16_t div_diag_print; }; /** diff --git a/GD32E508_DCU_APP/sys/config.h b/GD32E508_DCU_APP/sys/config.h index 9ace2f8..4dc9dcf 100644 --- a/GD32E508_DCU_APP/sys/config.h +++ b/GD32E508_DCU_APP/sys/config.h @@ -1,4 +1,4 @@ -/** +/** * @file config.h * @brief 应用级集中配置项定义。 * @note 用于统一管理当前项目中可调的外设、时序、通信与算法常量。 @@ -161,8 +161,10 @@ constexpr uint32_t CONFIG_TASK_PERIOD_CAN_RX_TICKS = 100U; constexpr uint32_t CONFIG_TASK_PERIOD_CAN_TX_TICKS = 10U; /** @brief 调度基准节拍:状态灯任务周期,1000 tick = 100ms。 */ constexpr uint32_t CONFIG_TASK_PERIOD_STATUS_LED_TICKS = 1000U; -/** @brief 调度基准节拍:心跳/诊断打印周期,单位 100us tick。 */ +/** @brief 调度基准节拍:心跳周期,单位 100us tick。 */ constexpr uint32_t CONFIG_TASK_PERIOD_HEARTBEAT_TICKS = 10000U; +/** @brief 调度基准节拍:APP 状态/诊断打印周期,50000 tick = 5s。 */ +constexpr uint32_t CONFIG_TASK_PERIOD_DIAG_PRINT_TICKS = 50000U; /** @brief 周期温度/电压 UART 打印开关。@note 保留 #define:task_manager.cpp #if 使用。 */ #define CONFIG_UART_TEMP_PERIODIC_PRINT 1 diff --git a/GD32E508_DCU_APP/sys/ota_boot_request.cpp b/GD32E508_DCU_APP/sys/ota_boot_request.cpp index 166ce59..c346b88 100644 --- a/GD32E508_DCU_APP/sys/ota_boot_request.cpp +++ b/GD32E508_DCU_APP/sys/ota_boot_request.cpp @@ -15,6 +15,9 @@ constexpr uint32_t kOtaConfigPageAddr = 0x0807E000U; constexpr uint32_t kAppLatestAvalAddr = kOtaConfigPageAddr + 0x00U; constexpr uint32_t kAppLatestEnAddr = kOtaConfigPageAddr + 0x08U; constexpr uint32_t kAppLatestCheckAddr = kOtaConfigPageAddr + 0x10U; +/** BOOT 版本号在 OTA 配置页中的存储地址:word6=major|minor, word7=patch|build。 */ +constexpr uint32_t kBootVersionAddr0 = kOtaConfigPageAddr + 0x18U; +constexpr uint32_t kBootVersionAddr1 = kOtaConfigPageAddr + 0x1CU; constexpr uint32_t kOtaAval = 0x00000000U; constexpr uint32_t kOtaNotAval = 0xFFFFFFFFU; constexpr uint32_t kFmcPageSize = 0x00000800U; @@ -102,6 +105,23 @@ void OtaBootRequest::init() s_spiFrozenForBoot = false; } +BootVersion OtaBootRequest::readBootVersion() +{ + /* BOOT 启动时将自身版本写入 OTA 配置页 word6/word7, + * 字布局:word6=[15:0]major|[31:16]minor, word7=[15:0]patch|[31:16]build。 + * Flash 未写入(擦除态 0xFFFFFFFF)时各字段返回 0xFF,调用方可据此判断有效性。 */ + const volatile uint32_t *bv0 = (const volatile uint32_t *)kBootVersionAddr0; + const volatile uint32_t *bv1 = (const volatile uint32_t *)kBootVersionAddr1; + const uint32_t w0 = *bv0; + const uint32_t w1 = *bv1; + return BootVersion{ + (uint8_t)(w0 & 0xFFU), + (uint8_t)((w0 >> 16) & 0xFFU), + (uint8_t)(w1 & 0xFFU), + (uint8_t)((w1 >> 16) & 0xFFU) + }; +} + /** * @brief 写入 Bootloader OTA 等待标志,并安排稍后软复位。 * @return true 表示标志写入成功;false 表示 Flash 配置页写入失败。 @@ -172,14 +192,12 @@ void OtaBootRequest::process() s_spiFrozenForBoot = true; nvic_irq_disable(SPI0_IRQn); nvic_irq_disable(EXTI10_15_IRQn); - nvic_irq_disable(EXTI5_9_IRQn); const bool ok = requestEnterBoot(); if(!ok) { s_spiFrozenForBoot = false; nvic_irq_enable(SPI0_IRQn, 2U, 0U); nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U); - nvic_irq_enable(EXTI5_9_IRQn, CONFIG_IRQ_PREEMPT_SPI_USART, 1U); /* requestEnterBoot 失败时 s_rebootPending 仍为 false */ return; } @@ -194,7 +212,6 @@ void OtaBootRequest::process() s_spiFrozenForBoot = true; nvic_irq_disable(SPI0_IRQn); nvic_irq_disable(EXTI10_15_IRQn); - nvic_irq_disable(EXTI5_9_IRQn); /* v2 DATA_READY: Boot 切换不再用 PB6 通知对端, * BOOT 起机后再由 HELLO role=boot 确认。 */ diff --git a/GD32E508_DCU_APP/sys/ota_boot_request.h b/GD32E508_DCU_APP/sys/ota_boot_request.h index 238e163..c5e709a 100644 --- a/GD32E508_DCU_APP/sys/ota_boot_request.h +++ b/GD32E508_DCU_APP/sys/ota_boot_request.h @@ -3,6 +3,20 @@ #include +/** + * @brief BOOT 版本号结构体(从 Flash OTA 配置页读取)。 + * + * Flash 字布局(与 BOOT 工程 sys/config.h 对称): + * word6 (ADDR0): [15:0]=major [31:16]=minor + * word7 (ADDR1): [15:0]=patch [31:16]=build + */ +struct BootVersion { + uint8_t major; + uint8_t minor; + uint8_t patch; + uint8_t build; +}; + /** * @brief DCU_APP 进入 Bootloader OTA 窗口控制模块。 * @note ESP32 通过 CAN-FD 发送受保护命令,APP 写 OTA 标志后软复位, @@ -13,6 +27,19 @@ public: /** @brief 初始化内部状态。 */ static void init(); + /** + * @brief 从 OTA 配置页 Flash 读取 BOOT 版本号。 + * + * BOOT 启动时将自身版本写入 0x0807E018/0x0807E01C, + * APP 运行期通过本函数读出,用于 SPI PING 响应、日志等场景。 + * + * @return BootVersion 结构体,字段已按 Flash 字布局拆包。 + * + * @note 若 Flash 未写入(擦除态 0xFFFFFFFF),返回值各字段为 0xFF, + * 调用方可据此判断 BOOT 版本是否有效。 + */ + static BootVersion readBootVersion(); + /** * @brief 尝试处理一帧 CAN 接收报文。 * @param[in] channel 报文来源 CAN 通道。 diff --git a/GD32E508_DCU_APP/sys/task_manager.cpp b/GD32E508_DCU_APP/sys/task_manager.cpp index 0a0a335..2daf93c 100644 --- a/GD32E508_DCU_APP/sys/task_manager.cpp +++ b/GD32E508_DCU_APP/sys/task_manager.cpp @@ -33,6 +33,7 @@ void TaskManager::init() g_scheduler.div_can_tx = 0U; g_scheduler.div_status_led = 0U; g_scheduler.div_heartbeat = 0U; + g_scheduler.div_diag_print = 0U; } /** @@ -109,12 +110,15 @@ void TaskManager::tick100us() g_scheduler.div_heartbeat = 0U; g_scheduler.tick_1s++; g_app_uptime_sec++; -#if (CONFIG_UART_TEMP_PERIODIC_PRINT != 0) - set((1UL << TASK_HEARTBEAT) | (1UL << TASK_DIAGNOSTIC_PRINT)); -#else set(1UL << TASK_HEARTBEAT); -#endif } + +#if (CONFIG_UART_TEMP_PERIODIC_PRINT != 0) + if(++g_scheduler.div_diag_print >= CONFIG_TASK_PERIOD_DIAG_PRINT_TICKS) { + g_scheduler.div_diag_print = 0U; + set(1UL << TASK_DIAGNOSTIC_PRINT); + } +#endif } /** diff --git a/GD32E508_DCU_APP/sys/task_runner.cpp b/GD32E508_DCU_APP/sys/task_runner.cpp index a176bcd..6b7e15c 100644 --- a/GD32E508_DCU_APP/sys/task_runner.cpp +++ b/GD32E508_DCU_APP/sys/task_runner.cpp @@ -182,7 +182,7 @@ void TaskRunner::taskCanTxSend() } const uint32_t extId = 0x18FF5000U | static_cast(ch); - if (CanFd::transmitFrame(ch, extId, data, sizeof(data), true)) { + if (CanFd::transmitFrame(ch, extId, data, sizeof(data), true) == CanFd::CanTxResult::kOk) { seq++; } } @@ -240,7 +240,7 @@ void TaskRunner::taskHeartbeat() { OtaBootRequest::process(); CanFd::refres * @param[in] 无。 * @param[out] 无。 * @retval 无返回值。 - * @note 由 1 秒节拍触发,通过 UART 打印当前温度、电压;采用定点整数格式,避免依赖浮点 `printf` 支持。 + * @note 由 5 秒节拍触发,通过 UART 打印当前温度、电压;采用定点整数格式,避免依赖浮点 `printf` 支持。 */ void TaskRunner::taskDiagnosticPrint() { @@ -304,18 +304,6 @@ void TaskRunner::taskDiagnosticPrint() } } -/** - * @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] 无。 @@ -401,6 +389,5 @@ void TaskRunner::runPending() 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(); } diff --git a/GD32E508_DCU_APP/sys/task_runner.h b/GD32E508_DCU_APP/sys/task_runner.h index 79ce7b5..e7c361f 100644 --- a/GD32E508_DCU_APP/sys/task_runner.h +++ b/GD32E508_DCU_APP/sys/task_runner.h @@ -47,8 +47,6 @@ private: static void taskHeartbeat(); /** @brief 诊断打印任务。 */ static void taskDiagnosticPrint(); - /** @brief SPI CS release 后推迟的命令解析(从 ISR 移出)。 */ - static void taskSpiCmdProcess(); /** @brief EtherCAT 周期/事件处理(非实时应用层 PDO 映射)。 */ static void taskEthercatProcess(); };