diff --git a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml index 98262a3..41e6657 100644 --- a/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml +++ b/GD32E508_DCU_APP/GD32E508_DCU_APP.cproject.yml @@ -1,4 +1,4 @@ -# A project translates into one executable or library. +# A project translates into one executable or library. project: groups: @@ -18,6 +18,7 @@ project: - file: ./sys/task_runner.cpp - file: ./sys/ota_boot_request.h - file: ./sys/ota_boot_request.cpp + - file: ./sys/cpp_stubs.cpp - file: ./sys/config.h - group: device files: @@ -29,6 +30,12 @@ project: - file: ./device/CANFD.cpp - file: ./device/app_spi_command.h - file: ./device/app_spi_command.cpp + - file: ./device/spi_command_handler.h + - file: ./device/app_spi_command_defs.h + - file: ./device/app_spi_cmd_boot_request.cpp + - file: ./device/app_spi_cmd_ota_wait.cpp + - file: ./device/app_spi_cmd_ping.cpp + - file: ./device/app_spi_cmd_can_bridge.cpp - file: ./device/Timer.h - file: ./device/Timer.cpp - file: ./device/temp_sensor.h diff --git a/GD32E508_DCU_APP/device/app_spi_cmd_boot_request.cpp b/GD32E508_DCU_APP/device/app_spi_cmd_boot_request.cpp new file mode 100644 index 0000000..0ab5f37 --- /dev/null +++ b/GD32E508_DCU_APP/device/app_spi_cmd_boot_request.cpp @@ -0,0 +1,40 @@ +#include "device/spi_command_handler.h" +#include "device/app_spi_command.h" +#include "device/app_spi_command_defs.h" +#include "sys/ota_boot_request.h" +#include "sys/config.h" +#include "sys/task_manager.h" + +namespace { + +class SpiBootRequestHandler final : public ISpiCommandHandler { +public: + bool canHandle(uint8_t frame_type) const override + { + return frame_type == SpiDefs::kTypeBootReq; + } + + void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override + { + bus.postLog("BOOT_REQ → 进BOOT"); + + const bool ok = OtaBootRequest::requestEnterBoot(); + uint8_t ack = ok ? CONFIG_SPI_APP_OTA_BOOT_ACK_OK + : CONFIG_SPI_APP_OTA_BOOT_ACK_ERROR; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspAck, &ack, 1U); + + if (ok) { + TaskManager::set(1UL << TASK_HEARTBEAT); + } + } +}; + +static SpiBootRequestHandler s_handler; + +} // namespace + +void registerSpiBootRequestCommand(AppSpiCommand &bus) +{ + bus.registerHandler(&s_handler); +} diff --git a/GD32E508_DCU_APP/device/app_spi_cmd_can_bridge.cpp b/GD32E508_DCU_APP/device/app_spi_cmd_can_bridge.cpp new file mode 100644 index 0000000..6f947c8 --- /dev/null +++ b/GD32E508_DCU_APP/device/app_spi_cmd_can_bridge.cpp @@ -0,0 +1,63 @@ +#include "device/spi_command_handler.h" +#include "device/app_spi_command.h" +#include "device/app_spi_command_defs.h" +#include "device/canfd.h" + +namespace { + +class SpiCanBridgeHandler final : public ISpiCommandHandler { +public: + bool canHandle(uint8_t frame_type) const override + { + return frame_type == SpiDefs::kTypeCanSend; + } + + void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override + { + if (ctx.payload_len < 6U) { + bus.postLog("[SPI-APP] CAN_SEND 载荷太短 → ERROR"); + uint8_t err = 0xE0U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + return; + } + + const uint8_t can_ch = ctx.payload[0]; + const uint32_t can_id = (static_cast(ctx.payload[1]) << 24) + | (static_cast(ctx.payload[2]) << 16) + | (static_cast(ctx.payload[3]) << 8) + | static_cast(ctx.payload[4]); + const uint8_t *can_data = &ctx.payload[5]; + const uint8_t can_dlc = static_cast(ctx.payload_len - 5U); + + if (can_ch >= 3U) { + bus.postLogFmt("[SPI-APP] CAN_SEND 通道非法: CH%d → ERROR", can_ch); + uint8_t err = 0xE1U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + return; + } + + const bool ok = CanFd::transmitFrame(can_ch, can_id, can_data, can_dlc); + if (ok) { + bus.postLogFmt("[SPI-APP] CAN_SEND CH%d → OK", can_ch); + const uint8_t ack = 0x00U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspAck, &ack, 1U); + } else { + bus.postLogFmt("[SPI-APP] CAN_SEND CH%d → BUSY", can_ch); + uint8_t err = 0xE2U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspError, &err, 1U); + } + } +}; + +static SpiCanBridgeHandler s_handler; + +} // namespace + +void registerSpiCanBridgeCommand(AppSpiCommand &bus) +{ + bus.registerHandler(&s_handler); +} diff --git a/GD32E508_DCU_APP/device/app_spi_cmd_ota_wait.cpp b/GD32E508_DCU_APP/device/app_spi_cmd_ota_wait.cpp new file mode 100644 index 0000000..fa8e438 --- /dev/null +++ b/GD32E508_DCU_APP/device/app_spi_cmd_ota_wait.cpp @@ -0,0 +1,38 @@ +#include "device/spi_command_handler.h" +#include "device/app_spi_command.h" +#include "device/app_spi_command_defs.h" + +namespace { + +class SpiOtaWaitHandler final : public ISpiCommandHandler { +public: + bool canHandle(uint8_t frame_type) const override + { + switch (frame_type) { + case SpiDefs::kTypeOtaReq: + case SpiDefs::kTypeOtaData: + case SpiDefs::kTypeOtaCancel: + return true; + default: + return false; + } + } + + void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override + { + bus.postLogFmt("[SPI-APP] OTA type=0x%02X → WAIT", ctx.frame_type); + + const uint8_t hint = 0x01U; + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspWait, &hint, 1U); + } +}; + +static SpiOtaWaitHandler s_handler; + +} // namespace + +void registerSpiOtaWaitCommand(AppSpiCommand &bus) +{ + bus.registerHandler(&s_handler); +} diff --git a/GD32E508_DCU_APP/device/app_spi_cmd_ping.cpp b/GD32E508_DCU_APP/device/app_spi_cmd_ping.cpp new file mode 100644 index 0000000..320490a --- /dev/null +++ b/GD32E508_DCU_APP/device/app_spi_cmd_ping.cpp @@ -0,0 +1,56 @@ +#include "device/spi_command_handler.h" +#include "device/app_spi_command.h" +#include "device/app_spi_command_defs.h" +#include "sys/config.h" +#include "main.h" +#include + +namespace { + +class SpiAppPingHandler final : public ISpiCommandHandler { +public: + bool canHandle(uint8_t frame_type) const override + { + return frame_type == SpiDefs::kTypeAppPing; + } + + void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override + { + + uint8_t alive_payload[19]; + std::memset(alive_payload, 0, sizeof(alive_payload)); + alive_payload[0] = 0xA1U; /* magic */ + alive_payload[1] = 0x55U; /* APP version tag */ + alive_payload[2] = (uint8_t)APP_FW_VERSION_MAJOR; + alive_payload[3] = (uint8_t)APP_FW_VERSION_MINOR; + alive_payload[4] = (uint8_t)APP_FW_VERSION_PATCH; + alive_payload[5] = (uint8_t)APP_FW_VERSION_BUILD; + alive_payload[6] = 0xAAU; /* BOOT version tag */ + + 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; + alive_payload[7] = (uint8_t)(bw0 & 0xFFU); + alive_payload[8] = (uint8_t)((bw0 >> 16) & 0xFFU); + alive_payload[9] = (uint8_t)(bw1 & 0xFFU); + alive_payload[10] = (uint8_t)((bw1 >> 16) & 0xFFU); + + alive_payload[11] = (uint8_t)(g_app_uptime_sec & 0xFFU); + alive_payload[12] = (uint8_t)((g_app_uptime_sec >> 8) & 0xFFU); + alive_payload[13] = (uint8_t)((g_app_uptime_sec >> 16) & 0xFFU); + alive_payload[14] = (uint8_t)((g_app_uptime_sec >> 24) & 0xFFU); + + bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len, + SpiDefs::kRspAck, alive_payload, sizeof(alive_payload)); + } +}; + +static SpiAppPingHandler s_handler; + +} // namespace + +void registerSpiAppPingCommand(AppSpiCommand &bus) +{ + bus.registerHandler(&s_handler); +} diff --git a/GD32E508_DCU_APP/device/app_spi_command.cpp b/GD32E508_DCU_APP/device/app_spi_command.cpp index c5ecdeb..ea9e37f 100644 --- a/GD32E508_DCU_APP/device/app_spi_command.cpp +++ b/GD32E508_DCU_APP/device/app_spi_command.cpp @@ -1,18 +1,21 @@ /** * @file app_spi_command.cpp - * @brief APP 运行期 SPI 命令从机实现。 + * @brief APP 运行期 SPI 链路与命令路由实现。 * - * 复用 BOOT 的 SpiOta 协议帧格式(SOF+TYPE+LEN+PAYLOAD+CRC), - * APP 只处理进入 Bootloader 命令(TYPE=0xB0), - * 收到 OTA 命令帧(TYPE=0x01/0x02/0x03)时回复 WAIT 帧告知 - * ESP32 "当前不在 BOOT 模式"。 + * 本模块现在只负责: + * - SPI0 从机链路初始化(GPIO/SPI/DMA/EXTI) + * - CS、DMA、中断驱动的帧接收与发送 + * - 公共帧格式解析与应答构建 + * - 业务命令的注册与分发 * - * 调试日志采用延迟输出:processCommand() 只写 s_logBuf + 置 s_logPending, - * 同时置位 TASK_SPI_LOG 唤醒主循环,poll() 中立刻 printf。 + * 具体命令处理逻辑已经拆到独立的 `app_spi_cmd_*.cpp`, + * 通过 `registerHandler()` 注册进来,后续新增 SPI 业务时 + * 只需要新增命令处理器,不再继续膨胀本文件。 */ #include "device/app_spi_command.h" -#include "device/canfd.h" +#include "device/spi_command_handler.h" +#include "device/app_spi_command_defs.h" extern "C" { #include "gd32e50x_exti.h" @@ -23,30 +26,27 @@ extern "C" { #include "gd32e50x_dma.h" #include "gd32e50x_spi.h" #include "sys/config.h" -#include "sys/ota_boot_request.h" #include "sys/task_manager.h" +#include "sys/ota_boot_request.h" #include "sys/systick.h" #include "main.h" -#include -#include +#include +#include /* ============================================================ - * 协议常量(与 BOOT SpiOta 一致) + * 本地别名:保持当前实现文件内部风格不变 * ============================================================ */ -static constexpr uint8_t kSof = 0xA5U; -static constexpr uint8_t kTypeBootReq = 0xB0U; -static constexpr uint8_t kTypeCanSend = 0xC0U; -static constexpr uint8_t kTypeOtaReq = 0x01U; -static constexpr uint8_t kTypeAppPing = 0xA1U; -static constexpr uint8_t kTypeOtaData = 0x02U; -static constexpr uint8_t kTypeOtaCancel = 0x03U; -static constexpr uint8_t kRspAck = 0x80U; -static constexpr uint8_t kRspWait = 0x81U; -static constexpr uint8_t kRspError = 0x82U; -static constexpr uint16_t kFrameOverhead = 5U; -static constexpr uint16_t kRxBufSize = 128U; -static constexpr uint16_t kTxBufSize = 64U; -static constexpr uint16_t kLogBufSize = 128U; +static constexpr uint8_t kSof = SpiDefs::kSof; +static constexpr uint8_t kTypeBootReq = SpiDefs::kTypeBootReq; +static constexpr uint8_t kTypeCanSend = SpiDefs::kTypeCanSend; +static constexpr uint8_t kTypeOtaReq = SpiDefs::kTypeOtaReq; +static constexpr uint8_t kTypeAppPing = SpiDefs::kTypeAppPing; +static constexpr uint8_t kTypeOtaData = SpiDefs::kTypeOtaData; +static constexpr uint8_t kTypeOtaCancel = SpiDefs::kTypeOtaCancel; +static constexpr uint8_t kRspAck = SpiDefs::kRspAck; +static constexpr uint8_t kRspWait = SpiDefs::kRspWait; +static constexpr uint8_t kRspError = SpiDefs::kRspError; +static constexpr uint16_t kFrameOverhead = SpiDefs::kFrameOverhead; /* DMA 配置(与 BOOT SpiOta 一致:DMA0_CH2 用于 SPI0 TX) */ #define SPI_TX_DMA_PERIPH DMA0 @@ -92,18 +92,6 @@ static bool shouldIgnoreTransaction(const uint8_t *data, uint8_t len) !(len == 1U && data[0] == CONFIG_SPI_APP_OTA_BOOT_CMD); } -/* ============================================================ - * 静态成员 - * ============================================================ */ -volatile bool AppSpiCommand::s_csActive = false; -volatile uint8_t AppSpiCommand::s_rxLen = 0U; -volatile uint8_t AppSpiCommand::s_txLen = 0U; -volatile uint8_t AppSpiCommand::s_txPos = 0U; -volatile bool AppSpiCommand::s_logPending = false; -uint8_t AppSpiCommand::s_rxBuf[kRxBufSize] = {}; -uint8_t AppSpiCommand::s_txBuf[kTxBufSize] = {}; -char AppSpiCommand::s_logBuf[kLogBufSize] = {}; - /* ============================================================ * CRC8 / 帧构建 / 帧解析(与 BOOT SpiOta 一致) * ============================================================ */ @@ -122,7 +110,7 @@ static bool spi_build_frame(uint8_t type, const uint8_t *payload, uint16_t paylo { if (!out || !out_len) { return false; } uint16_t total = (uint16_t)(kFrameOverhead + payload_len); - if (total > kTxBufSize) { return false; } + if (total > AppSpiCommand::kTxBufSize) { return false; } out[0] = kSof; out[1] = type; out[2] = (uint8_t)(payload_len & 0xFFU); @@ -138,34 +126,76 @@ static bool spi_build_frame(uint8_t type, const uint8_t *payload, uint16_t paylo static bool spi_try_parse_frame(const uint8_t *frame, uint16_t frame_len, uint8_t *type, const uint8_t **payload, uint16_t *payload_len) { - if (!frame || frame_len < kFrameOverhead) { return false; } + if (!frame || !type || !payload || !payload_len) { return false; } + + /* 兼容首字节延迟:在帧头前面可能出现若干 0xFF / 0x00 残留字节, + * 搜索到第一个 kSof 后尝试按合法帧长度校验 CRC。 */ + uint16_t s = 0U; + while (s < frame_len && frame[s] != kSof) { ++s; } + if (s > 0U && s < frame_len) { + const uint16_t shifted_len = (uint16_t)(frame_len - s); + if (shifted_len >= kFrameOverhead) { + const uint16_t len_field = (uint16_t)frame[s + 2U] | ((uint16_t)frame[s + 3U] << 8); + const uint16_t expected = (uint16_t)(kFrameOverhead + len_field); + if (shifted_len >= expected && + spi_crc8(&frame[s], (uint16_t)(4U + len_field)) == frame[s + 4U + len_field]) { + *type = frame[s + 1U]; + *payload = &frame[s + 4U]; + *payload_len = len_field; + return true; + } + } + } + + if (frame_len < kFrameOverhead) { return false; } if (frame[0] != kSof) { return false; } - uint16_t len = (uint16_t)(frame[2] | ((uint16_t)frame[3] << 8)); + + uint16_t len = (uint16_t)frame[2] | ((uint16_t)frame[3] << 8); uint16_t expected = (uint16_t)(kFrameOverhead + len); - if (expected != frame_len) { return false; } + if (frame_len < expected) { return false; } if (spi_crc8(frame, (uint16_t)(4U + len)) != frame[4U + len]) { return false; } - if (type) { *type = frame[1]; } - if (payload) { *payload = &frame[4]; } - if (payload_len) { *payload_len = len; } + + *type = frame[1]; + *payload = &frame[4]; + *payload_len = len; return true; } /* ============================================================ - * 延迟日志:写入缓冲(ISR 安全,不调用 printf) + * 调试日志辅助 * ============================================================ */ -void AppSpiCommand::logDeferred(const char *msg) +void AppSpiCommand::postLog(const char *msg) { - if (s_logPending) { return; } - snprintf(s_logBuf, kLogBufSize, "[SPI-APP] %s", msg); - s_logPending = true; + if (!msg) return; + snprintf(m_logBuf, kLogBufSize, "[SPI-APP] %s", msg); + m_logPending = true; } -void AppSpiCommand::logDeferredFmt(const char *fmt, uint8_t val) +void AppSpiCommand::postLogFmt(const char *fmt, uint8_t val) { - if (s_logPending) { return; } - snprintf(s_logBuf, kLogBufSize, fmt, val); - s_logPending = true; + if (!fmt) return; + snprintf(m_logBuf, kLogBufSize, fmt, val); + m_logPending = true; +} + +void AppSpiCommand::buildResponse(uint8_t *tx_buf, uint16_t tx_buf_size, + uint16_t *tx_len, uint8_t rsp_type, + const uint8_t *payload, uint16_t payload_len) +{ + if (!tx_buf || !tx_len || tx_buf_size == 0U) { + return; + } + + uint16_t built_len = 0U; + const bool ok = spi_build_frame(rsp_type, payload, payload_len, tx_buf, &built_len); + if (ok) { + *tx_len = built_len; + } else { + uint8_t err = 0xEEU; + (void)spi_build_frame(kRspError, &err, 1U, tx_buf, &built_len); + *tx_len = built_len; + } } /* ============================================================ @@ -221,7 +251,7 @@ void AppSpiCommand::configureDma() dma_struct_para_init(&dma_cfg); dma_cfg.periph_addr = (uint32_t)(SPI0 + 0x0CU); /* SPI0_DATA 寄存器 */ dma_cfg.periph_width = DMA_PERIPHERAL_WIDTH_8BIT; - dma_cfg.memory_addr = (uint32_t)s_txBuf; + dma_cfg.memory_addr = (uint32_t)m_txBuf; dma_cfg.memory_width = DMA_MEMORY_WIDTH_8BIT; dma_cfg.number = 0U; dma_cfg.priority = DMA_PRIORITY_HIGH; @@ -242,34 +272,53 @@ void AppSpiCommand::configureExti() nvic_irq_enable(EXTI10_15_IRQn, 1U, 1U); } -/* ============================================================ - * 初始化 - * ============================================================ */ +/* ============================================================ + * 默认命令注册入口(由各 app_spi_cmd_*.cpp 提供) + * ============================================================ */ +extern void registerSpiBootRequestCommand(AppSpiCommand &bus); +extern void registerSpiOtaWaitCommand(AppSpiCommand &bus); +extern void registerSpiAppPingCommand(AppSpiCommand &bus); +extern void registerSpiCanBridgeCommand(AppSpiCommand &bus); +AppSpiCommand AppSpiCommandBus; void AppSpiCommand::init() { - s_csActive = false; - s_rxLen = 0U; - s_txLen = 0U; - s_txPos = 0U; - s_logPending = false; - memset(s_rxBuf, 0, sizeof(s_rxBuf)); - memset(s_txBuf, 0xFF, sizeof(s_txBuf)); - memset(s_logBuf, 0, sizeof(s_logBuf)); + m_csActive = false; + m_rxLen = 0U; + m_txLen = 0U; + m_txPos = 0U; + m_logPending = false; + m_commandCount = 0U; + memset(m_txBuf, 0xFF, sizeof(m_txBuf)); + memset(m_logBuf, 0, sizeof(m_logBuf)); + memset(m_commands, 0, sizeof(m_commands)); + + registerSpiBootRequestCommand(AppSpiCommandBus); + registerSpiOtaWaitCommand(AppSpiCommandBus); + registerSpiAppPingCommand(AppSpiCommandBus); + registerSpiCanBridgeCommand(AppSpiCommandBus); configureGpio(); - configureDma(); configureSpi0(); + configureDma(); configureExti(); } /* ============================================================ - * 帧处理:CS 释放后解析帧并准备回复(不调用 printf) + * 命令路由 * ============================================================ */ +void AppSpiCommand::registerHandler(ISpiCommandHandler *handler) +{ + if (!handler || m_commandCount >= kMaxCommands) { + return; + } + m_commands[m_commandCount++] = handler; +} + void AppSpiCommand::processCommand() { - if (s_rxLen == 0U) { + if (m_rxLen == 0U) { return; } @@ -278,158 +327,91 @@ void AppSpiCommand::processCommand() const uint8_t *payload = nullptr; uint16_t payload_len = 0U; - if (spi_try_parse_frame(s_rxBuf, s_rxLen, &frame_type, &payload, &payload_len)) { - /* ★ 合法帧:按 TYPE 分发 */ + if (spi_try_parse_frame(m_rxBuf, m_rxLen, &frame_type, &payload, &payload_len)) { + /* 合法帧:按 TYPE 分发给已注册命令处理器 */ uint16_t tx_len = 0U; - switch (frame_type) { - case kTypeBootReq: - /* 请求进入 Bootloader */ - logDeferred("BOOT_REQ → 进BOOT"); - { + bool handled = false; + + SpiCommandContext ctx{}; + ctx.frame_type = frame_type; + ctx.payload = payload; + ctx.payload_len = payload_len; + ctx.tx_buf = m_txBuf; + ctx.tx_buf_size = kTxBufSize; + ctx.tx_len = &tx_len; + + for (uint8_t i = 0U; i < m_commandCount; ++i) { + ISpiCommandHandler *handler = m_commands[i]; + if (handler && handler->canHandle(frame_type)) { + handler->handle(AppSpiCommandBus, ctx); + handled = true; + break; + } + } + + if (!handled) { + postLogFmt("[SPI-APP] 未知 type=0x%02X → ERROR", frame_type); + uint8_t err = 0xEEU; + spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); + } + + if (tx_len == 0U) { + /* 兜底:避免残留上一次应答帧 */ + uint8_t err = 0xEEU; + spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); + } + + m_txLen = (uint8_t)tx_len; + m_txPos = 0U; + } else { + /* 非标准帧:保留旧式单字节 0xB0 通道,后续新命令建议走注册式帧协议 */ + const uint8_t first_valid = m_rxBuf[0U]; + if (first_valid == CONFIG_SPI_APP_OTA_BOOT_CMD) { + postLog("旧式0xB0 → 进BOOT"); + + uint16_t tx_len_legacy = 0U; + SpiCommandContext ctx{}; + ctx.frame_type = first_valid; + ctx.payload = nullptr; + ctx.payload_len = 0U; + ctx.tx_buf = m_txBuf; + ctx.tx_buf_size = kTxBufSize; + ctx.tx_len = &tx_len_legacy; + + bool handled = false; + for (uint8_t i = 0U; i < m_commandCount; ++i) { + ISpiCommandHandler *handler = m_commands[i]; + if (handler && handler->canHandle(first_valid)) { + handler->handle(AppSpiCommandBus, ctx); + handled = true; + break; + } + } + + if (!handled) { const bool ok = OtaBootRequest::requestEnterBoot(); uint8_t ack = ok ? CONFIG_SPI_APP_OTA_BOOT_ACK_OK : CONFIG_SPI_APP_OTA_BOOT_ACK_ERROR; - spi_build_frame(kRspAck, &ack, 1U, s_txBuf, &tx_len); + m_txBuf[0] = CONFIG_SPI_APP_OTA_BOOT_CMD; + m_txBuf[1] = ack; + m_txBuf[2] = 0x4FU; + m_txBuf[3] = 0x54U; + tx_len_legacy = 4U; if (ok) { TaskManager::set(1UL << TASK_HEARTBEAT); } } - break; - case kTypeOtaReq: - case kTypeOtaData: - case kTypeOtaCancel: - /* OTA 命令帧 — APP 不处理,回复 WAIT */ - logDeferredFmt("[SPI-APP] OTA type=0x%02X → WAIT", frame_type); - { - uint8_t hint = 0x01U; - spi_build_frame(kRspWait, &hint, 1U, s_txBuf, &tx_len); - } - break; - - case kTypeAppPing: - /* APP 存活探测 — 回复 ACK 携带带标签的版本号 - * - * 因 SPI 从机首字节延迟导致帧偏移不稳定, - * 采用“标签+值”格式,ESP32 通过扫描标签定位字段。 - * - * payload 布局 (19B): - * [0] = 0xA1 魔术字 - * [1] = 0x55 APP 版本标签 - * [2..5] = APP 版本 (4 x uint8: major, minor, patch, build) - * [6] = 0xAA BOOT 版本标签 - * [7..10]= BOOT 版本 (4 x uint8: major, minor, patch, build) - * [11..14]= uptime (uint32_t LE) - * [15..18]= 填充 0 */ - { - extern volatile uint32_t g_app_uptime_sec; - uint8_t alive_payload[19]; - memset(alive_payload, 0, sizeof(alive_payload)); - alive_payload[0] = 0xA1U; /* magic */ - alive_payload[1] = 0x55U; /* APP version tag */ - alive_payload[2] = (uint8_t)APP_FW_VERSION_MAJOR; - alive_payload[3] = (uint8_t)APP_FW_VERSION_MINOR; - alive_payload[4] = (uint8_t)APP_FW_VERSION_PATCH; - alive_payload[5] = (uint8_t)APP_FW_VERSION_BUILD; - alive_payload[6] = 0xAAU; /* BOOT version tag */ - - /* BOOT 版本 (从 Flash OTA 配置页 word6/word7 读取) */ - 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; - alive_payload[7] = (uint8_t)(bw0 & 0xFFU); /* boot major */ - alive_payload[8] = (uint8_t)((bw0 >> 16) & 0xFFU); /* boot minor */ - alive_payload[9] = (uint8_t)(bw1 & 0xFFU); /* boot patch */ - alive_payload[10] = (uint8_t)((bw1 >> 16) & 0xFFU); /* boot build */ - - /* uptime (uint32_t LE) */ - alive_payload[11] = (uint8_t)(g_app_uptime_sec & 0xFFU); - alive_payload[12] = (uint8_t)((g_app_uptime_sec >> 8) & 0xFFU); - alive_payload[13] = (uint8_t)((g_app_uptime_sec >> 16) & 0xFFU); - alive_payload[14] = (uint8_t)((g_app_uptime_sec >> 24) & 0xFFU); - - spi_build_frame(kRspAck, alive_payload, sizeof(alive_payload), s_txBuf, &tx_len); - } - break; - - default: - /* 未知帧类型 */ - logDeferredFmt("[SPI-APP] 未知 type=0x%02X → ERROR", frame_type); - { - uint8_t err = 0xEEU; - spi_build_frame(kRspError, &err, 1U, s_txBuf, &tx_len); - } - break; - - case kTypeCanSend: - /* SPI→CAN 桥接:解析载荷并通过 CanFd 直接发送到 CAN 总线 - * - * payload 布局 (6~69B): - * [0] = channel (0/1/2) - * [1..4] = ext_id (32-bit Big-Endian) - * [5..] = CAN data (1~64 bytes, 实际长度 = payload_len - 5) - */ - if (payload_len < 6U) { - /* 载荷太短:至少需要 1B channel + 4B ext_id + 1B data */ - logDeferred("[SPI-APP] CAN_SEND 载荷太短 → ERROR"); - uint8_t err = 0xE0U; - spi_build_frame(kRspError, &err, 1U, s_txBuf, &tx_len); - } else { - uint8_t can_ch = payload[0]; - uint32_t can_id = (static_cast(payload[1]) << 24) - | (static_cast(payload[2]) << 16) - | (static_cast(payload[3]) << 8) - | static_cast(payload[4]); - const uint8_t *can_data = &payload[5]; - uint8_t can_dlc = static_cast(payload_len - 5U); - - if (can_ch >= 3U) { - logDeferredFmt("[SPI-APP] CAN_SEND 通道非法: CH%d → ERROR", can_ch); - uint8_t err = 0xE1U; - spi_build_frame(kRspError, &err, 1U, s_txBuf, &tx_len); - } else { - bool ok = CanFd::transmitFrame(can_ch, can_id, can_data, can_dlc); - if (ok) { - logDeferredFmt("[SPI-APP] CAN_SEND CH%d → OK", can_ch); - uint8_t ack = 0x00U; /* 成功 */ - spi_build_frame(kRspAck, &ack, 1U, s_txBuf, &tx_len); - } else { - logDeferredFmt("[SPI-APP] CAN_SEND CH%d FIFO满 → ERROR", can_ch); - uint8_t err = 0xE2U; /* FIFO 满 */ - spi_build_frame(kRspError, &err, 1U, s_txBuf, &tx_len); - } - } - } - break; - } - s_txLen = (uint8_t)tx_len; - s_txPos = 0U; - } else { - /* ★ 不符合协议帧 — 兼容旧逻辑 */ - if (s_rxBuf[0] == CONFIG_SPI_APP_OTA_BOOT_CMD) { - /* 旧式单字节 0xB0 */ - logDeferred("旧式0xB0 → 进BOOT"); - const bool ok = OtaBootRequest::requestEnterBoot(); - uint8_t ack = ok ? CONFIG_SPI_APP_OTA_BOOT_ACK_OK - : CONFIG_SPI_APP_OTA_BOOT_ACK_ERROR; - s_txBuf[0] = CONFIG_SPI_APP_OTA_BOOT_CMD; - s_txBuf[1] = ack; - s_txBuf[2] = 0x4FU; - s_txBuf[3] = 0x54U; - s_txLen = 4U; - s_txPos = 0U; - if (ok) { - TaskManager::set(1UL << TASK_HEARTBEAT); - } + m_txLen = (uint8_t)tx_len_legacy; + m_txPos = 0U; } else { /* 无法识别 */ - logDeferredFmt("[SPI-APP] 非协议 0x%02X → ERROR", s_rxBuf[0]); + postLogFmt("[SPI-APP] 非协议 0x%02X → ERROR", first_valid); uint16_t tx_len = 0U; uint8_t err = 0xEEU; - spi_build_frame(kRspError, &err, 1U, s_txBuf, &tx_len); - s_txLen = (uint8_t)tx_len; - s_txPos = 0U; + spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len); + m_txLen = (uint8_t)tx_len; + m_txPos = 0U; } } @@ -447,9 +429,9 @@ void AppSpiCommand::poll() bool has_log = false; __disable_irq(); - if (s_logPending) { - memcpy(log_copy, s_logBuf, sizeof(log_copy)); - s_logPending = false; + if (m_logPending) { + memcpy(log_copy, m_logBuf, sizeof(log_copy)); + m_logPending = false; has_log = true; } __enable_irq(); @@ -465,12 +447,12 @@ void AppSpiCommand::poll() void AppSpiCommand::onCsAssert() { - if (s_csActive) { + if (m_csActive) { return; } - s_csActive = true; - s_rxLen = 0U; - s_txPos = 0U; + m_csActive = true; + m_rxLen = 0U; + m_txPos = 0U; /* 停止 DMA 并清理旧状态 */ DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; @@ -491,22 +473,22 @@ void AppSpiCommand::onCsAssert() } /* 启动 DMA 发送响应帧(与 BOOT 完全一致) */ - if (s_txLen > 0U) { + if (m_txLen > 0U) { spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); - startSpiTxDma(s_txBuf, s_txLen); + startSpiTxDma(m_txBuf, m_txLen); } else { /* 无响应时发送全 0xFF 填充 */ spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); - startSpiTxDma(s_txBuf, kTxBufSize); + startSpiTxDma(m_txBuf, kTxBufSize); } } void AppSpiCommand::onCsRelease() { - if (!s_csActive) { + if (!m_csActive) { return; } - s_csActive = false; + m_csActive = false; /* 停止 DMA */ DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) &= ~DMA_CHXCTL_CHEN; @@ -514,13 +496,13 @@ void AppSpiCommand::onCsRelease() 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 (s_rxLen < sizeof(s_rxBuf)) { - s_rxBuf[s_rxLen++] = last_byte; + if (m_rxLen < sizeof(m_rxBuf)) { + m_rxBuf[m_rxLen++] = last_byte; } } - if (shouldIgnoreTransaction(s_rxBuf, s_rxLen)) { - s_rxLen = 0U; + if (shouldIgnoreTransaction(m_rxBuf, m_rxLen)) { + m_rxLen = 0U; return; } processCommand(); @@ -533,12 +515,12 @@ void AppSpiCommand::onCsRelease() void AppSpiCommand::onInterrupt() { if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RBNE) != RESET) { - if (!s_csActive && gpio_input_bit_get(GPIOA, GPIO_PIN_15) == 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 (s_csActive && s_rxLen < sizeof(s_rxBuf)) { - s_rxBuf[s_rxLen++] = byte; + if (m_csActive && m_rxLen < sizeof(m_rxBuf)) { + m_rxBuf[m_rxLen++] = byte; } } @@ -546,7 +528,7 @@ void AppSpiCommand::onInterrupt() if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_RXORERR) != RESET) { (void)spi_i2s_data_receive(SPI0); - s_rxLen = 0U; + m_rxLen = 0U; } if (spi_i2s_interrupt_flag_get(SPI0, SPI_I2S_INT_FLAG_FERR) != RESET) { @@ -568,10 +550,10 @@ void AppSpiCommand::onCsExtiInterrupt() extern "C" void SPI0_IRQHandler(void) { - AppSpiCommand::onInterrupt(); + AppSpiCommandBus.onInterrupt(); } extern "C" void EXTI10_15_IRQHandler(void) { - AppSpiCommand::onCsExtiInterrupt(); + AppSpiCommandBus.onCsExtiInterrupt(); } diff --git a/GD32E508_DCU_APP/device/app_spi_command.h b/GD32E508_DCU_APP/device/app_spi_command.h index ebe97b5..e5a3713 100644 --- a/GD32E508_DCU_APP/device/app_spi_command.h +++ b/GD32E508_DCU_APP/device/app_spi_command.h @@ -1,59 +1,87 @@ #ifndef DEVICE_APP_SPI_COMMAND_H #define DEVICE_APP_SPI_COMMAND_H -#include +#include + +class ISpiCommandHandler; /** - * @brief APP 运行期 SPI 命令从机模块。 + * @brief APP 运行期 SPI 链路与命令路由模块。 * - * 复用 BOOT 的 SpiOta 协议帧格式(SOF+TYPE+LEN+PAYLOAD+CRC): - * - TYPE=0xB0 → 请求进入 Bootloader,回复 ACK 帧 - * - TYPE=0x01/0x02/0x03(OTA 命令)→ 回复 WAIT 帧(不在 BOOT 模式) - * - 旧式单字节 0xB0 命令仍兼容 + * 本类现在主要承担两件事: + * - 维护 SPI0 从机的硬件链路(GPIO/SPI/DMA/EXTI) + * - 将接收到的合法帧按 TYPE 注册式分发给 ISpiCommandHandler * - * 调试日志采用延迟输出:processCommand() 只写缓冲+置标志, - * poll() 中再 printf,避免在帧处理路径上耗时。 - * - * 发送路径使用 DMA0_CH2 推 s_txBuf → SPI0_DATA, - * 与 BOOT 的 SpiOta 实现保持一致;接收仍用 RBNE 字节中断。 + * 具体命令处理逻辑不再放在这里,而是拆到 `device/app_spi_cmd_*.cpp`, + * 通过 `registerHandler()` 注册进来,保持本模块可扩展。 */ class AppSpiCommand { public: - /** @brief 初始化 SPI0 从机命令通道。 */ - static void init(); + /** @brief 初始化 SPI0 从机命令通道并注册默认命令处理器。 */ + void init(); /** @brief 主循环轮询:打印延迟日志。 */ - static void poll(); + void poll(); /** @brief SPI0 中断处理入口。 */ - static void onInterrupt(); + void onInterrupt(); /** @brief CS 边沿中断处理入口。 */ - static void onCsExtiInterrupt(); + void onCsExtiInterrupt(); + + /** + * @brief 注册一个 SPI 命令处理器,用于 TYPE 路由。 + * @param[in] handler 命令处理器指针,调用方需保证其生命周期有效。 + */ + void registerHandler(ISpiCommandHandler *handler); + + /** @brief 记录一条待输出日志,主循环统一打印。 */ + void postLog(const char *msg); + + /** @brief 记录一条带 1 字节参数的待输出日志。 */ + void postLogFmt(const char *fmt, uint8_t val); + + /** + * @brief 将应答帧构建到指定发送缓冲区。 + * @param[out] tx_buf 发送缓冲区。 + * @param[in] tx_buf_size 发送缓冲区大小。 + * @param[out] tx_len 构建完成后的帧长度。 + * @param[in] rsp_type 响应类型。 + * @param[in] payload 响应载荷。 + * @param[in] payload_len 响应载荷长度。 + */ + void buildResponse(uint8_t *tx_buf, uint16_t tx_buf_size, + uint16_t *tx_len, uint8_t rsp_type, + const uint8_t *payload, uint16_t payload_len); -private: static constexpr uint16_t kRxBufSize = 128U; static constexpr uint16_t kTxBufSize = 64U; static constexpr uint16_t kLogBufSize = 128U; - static volatile bool s_csActive; - static volatile uint8_t s_rxLen; - static volatile uint8_t s_txLen; - static volatile uint8_t s_txPos; - static volatile bool s_logPending; - static uint8_t s_rxBuf[kRxBufSize]; - static uint8_t s_txBuf[kTxBufSize]; - static char s_logBuf[kLogBufSize]; +private: + static constexpr uint8_t kMaxCommands = 8U; - static void configureGpio(); - static void configureSpi0(); - static void configureDma(); - static void configureExti(); - static void onCsAssert(); - static void onCsRelease(); - static void processCommand(); - static void logDeferred(const char *msg); - static void logDeferredFmt(const char *fmt, uint8_t val); + volatile bool m_csActive = false; + volatile uint8_t m_rxLen = 0U; + uint8_t m_txLen = 0U; + volatile uint8_t m_txPos = 0U; + volatile bool m_logPending = false; + uint8_t m_rxBuf[kRxBufSize] = {}; + uint8_t m_txBuf[kTxBufSize] = {}; + char m_logBuf[kLogBufSize] = {}; + uint8_t m_commandCount = 0U; + ISpiCommandHandler *m_commands[kMaxCommands] = {}; + + void configureGpio(); + void configureSpi0(); + void configureDma(); + void configureExti(); + void onCsAssert(); + void onCsRelease(); + void processCommand(); }; +/** @brief 全局 SPI 命令总线实例。 */ +extern AppSpiCommand AppSpiCommandBus; + #endif /* DEVICE_APP_SPI_COMMAND_H */ diff --git a/GD32E508_DCU_APP/device/app_spi_command_defs.h b/GD32E508_DCU_APP/device/app_spi_command_defs.h new file mode 100644 index 0000000..1685d33 --- /dev/null +++ b/GD32E508_DCU_APP/device/app_spi_command_defs.h @@ -0,0 +1,36 @@ +#ifndef DEVICE_APP_SPI_COMMAND_DEFS_H +#define DEVICE_APP_SPI_COMMAND_DEFS_H + +#include + +/** + * @file app_spi_command_defs.h + * @brief SPI 命令总线公共协议常量。 + * + * 将原来散落在 `app_spi_command.cpp` 中的帧类型与响应类型抽到公共头文件, + * 方便后续在不同命令处理器之间复用;当前仅服务于 APP 运行期 SPI 命令通道, + * 未来若 BOOT 与 APP 需要继续共享协议常量,也可从这里统一收敛。 + */ + +namespace SpiDefs { + +static constexpr uint8_t kSof = 0xA5U; + +/* 帧类型 */ +static constexpr uint8_t kTypeOtaReq = 0x01U; +static constexpr uint8_t kTypeOtaData = 0x02U; +static constexpr uint8_t kTypeOtaCancel = 0x03U; +static constexpr uint8_t kTypeAppPing = 0xA1U; +static constexpr uint8_t kTypeBootReq = 0xB0U; +static constexpr uint8_t kTypeCanSend = 0xC0U; + +/* 响应类型 */ +static constexpr uint8_t kRspAck = 0x80U; +static constexpr uint8_t kRspWait = 0x81U; +static constexpr uint8_t kRspError = 0x82U; + +static constexpr uint16_t kFrameOverhead = 5U; + +} // namespace SpiDefs + +#endif /* DEVICE_APP_SPI_COMMAND_DEFS_H */ diff --git a/GD32E508_DCU_APP/device/spi_command_handler.h b/GD32E508_DCU_APP/device/spi_command_handler.h new file mode 100644 index 0000000..0cfa43f --- /dev/null +++ b/GD32E508_DCU_APP/device/spi_command_handler.h @@ -0,0 +1,25 @@ +#ifndef DEVICE_SPI_COMMAND_HANDLER_H +#define DEVICE_SPI_COMMAND_HANDLER_H + +#include + +class AppSpiCommand; + +struct SpiCommandContext { + uint8_t frame_type; + const uint8_t *payload; + uint16_t payload_len; + uint8_t *tx_buf; + uint16_t tx_buf_size; + uint16_t *tx_len; +}; + +class ISpiCommandHandler { +public: + ~ISpiCommandHandler() {} + + virtual bool canHandle(uint8_t frame_type) const = 0; + virtual void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) = 0; +}; + +#endif /* DEVICE_SPI_COMMAND_HANDLER_H */ diff --git a/GD32E508_DCU_APP/sys/config.h b/GD32E508_DCU_APP/sys/config.h index e0170f9..1a9bff4 100644 --- a/GD32E508_DCU_APP/sys/config.h +++ b/GD32E508_DCU_APP/sys/config.h @@ -125,9 +125,9 @@ /** @brief APP 次版本号。 */ #define APP_FW_VERSION_MINOR 0U /** @brief APP 补丁版本号。 */ -#define APP_FW_VERSION_PATCH 0U +#define APP_FW_VERSION_PATCH 1U /** @brief APP 构建编号。 */ -#define APP_FW_VERSION_BUILD 2U +#define APP_FW_VERSION_BUILD 0U /* ============================================================ * BOOT 版本在 OTA 配置页中的 Flash 存储地址 diff --git a/GD32E508_DCU_APP/sys/cpp_stubs.cpp b/GD32E508_DCU_APP/sys/cpp_stubs.cpp new file mode 100644 index 0000000..d1db55f --- /dev/null +++ b/GD32E508_DCU_APP/sys/cpp_stubs.cpp @@ -0,0 +1,22 @@ +/** + * @file cpp_stubs.cpp + * @brief C++ runtime stubs for bare-metal microlib. + * + * microlib 的 aeabi_atexit.o 需要两个外部符号: + * - _atexit_init(void) : 初始化 atexit 互斥量(裸机无 RTOS,空实现) + * - _atexit_mutex (int) : 互斥量变量,aeabi_atexit 用它做线程安全守卫 + * + * 嵌入式常驻程序不存在 exit-time 析构,这里提供最小空实现让链接通过。 + */ + +extern "C" { + +void _atexit_init(void) +{ + /* 裸机无 RTOS,互斥量不需要真正初始化 */ +} + +/* aeabi_atexit.o 将此符号当作 int 变量读写,不是函数。 */ +int _atexit_mutex = 0; + +} // extern "C" diff --git a/GD32E508_DCU_APP/sys/initial.cpp b/GD32E508_DCU_APP/sys/initial.cpp index 8139035..f01bf7b 100644 --- a/GD32E508_DCU_APP/sys/initial.cpp +++ b/GD32E508_DCU_APP/sys/initial.cpp @@ -1,4 +1,4 @@ -/** +/** * @file initial.cpp * @brief 应用启动初始化实现。 */ @@ -37,7 +37,7 @@ void AppInitializer::init() printf("[APP] init: OtaBootRequest...\r\n"); OtaBootRequest::init(); printf("[APP] init: AppSpiCommand...\r\n"); - AppSpiCommand::init(); + AppSpiCommandBus.init(); printf("[APP] init: TaskManager...\r\n"); TaskManager::init(); printf("[APP] init: SchedulerTimer...\r\n"); diff --git a/GD32E508_DCU_APP/sys/task_runner.cpp b/GD32E508_DCU_APP/sys/task_runner.cpp index 844b7fb..688bae5 100644 --- a/GD32E508_DCU_APP/sys/task_runner.cpp +++ b/GD32E508_DCU_APP/sys/task_runner.cpp @@ -1,4 +1,4 @@ -/** +/** * @file task_runner.cpp * @brief 任务执行器实现。 */ @@ -307,5 +307,5 @@ void TaskRunner::runPending() 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)) AppSpiCommand::poll(); + if(pending & (1UL << TASK_SPI_LOG)) AppSpiCommandBus.poll(); } diff --git a/README.md b/README.md index 8586f42..c678bda 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# GD32E508_DCU_APP README +# GD32E508_DCU_APP README 本 README 用来介绍 `/workzone/ROBOT_V2/DCU/DCU_APP/GD32E508_DCU_APP` 目录下各个代码文件的作用、分层关系和阅读顺序。 @@ -10,7 +10,7 @@ - **应用入口层**:`main.cpp` / `main.h` - **系统层(sys)**:负责初始化、节拍、任务调度、任务执行 -- **设备层(device)**:负责 ADC、CANFD、Timer、串口、温度采样封装 +- **设备层(device)**:负责 ADC、CANFD、Timer、串口、温度采样封装,以及 SPI 命令总线与命令处理器拆分 - **RTE / Device 层**:芯片启动文件、链接脚本、厂商外设驱动 当前整体执行逻辑是: @@ -201,6 +201,16 @@ ## 5. 设备层 `device/` 文件说明 +### SPI 命令总线拆分说明 +- `device/spi_command_handler.h`:SPI 命令处理器抽象与上下文结构。 +- `device/app_spi_command_defs.h`:APP 运行期 SPI 协议公共常量。 +- `device/app_spi_command.cpp`:当前仅负责 SPI 链路和命令路由。 +- `device/app_spi_cmd_boot_request.cpp`:进入 Bootloader 的 SPI 命令处理器。 +- `device/app_spi_cmd_ota_wait.cpp`:OTA 相关命令的 WAIT 响应处理器。 +- `device/app_spi_cmd_ping.cpp`:APP Ping 命令处理器。 +- `device/app_spi_cmd_can_bridge.cpp`:SPI→CAN 桥接命令处理器。 +- 新增 SPI 业务时优先新增 `device/app_spi_cmd_*.cpp`,而不是继续膨胀 `app_spi_command.cpp`。 + ### `device/adc.h` **作用:** - 定义 ADC 采样结果结构 `adc_sample_t`