refactor(device): 重构SPI命令处理为模块化架构

- 将原有的 monolithic app_spi_command.cpp 拆分为多个专门的命令处理器文件
- 新增 spi_command_handler.h 定义命令处理器接口和上下文结构
- 新增 app_spi_command_defs.h 提取公共协议常量定义
- 新增多个具体命令处理器实现文件:
  - app_spi_cmd_boot_request.cpp (进入Bootloader命令)
  - app_spi_cmd_ota_wait.cpp (OTA等待响应命令)
  - app_spi_cmd_ping.cpp (APP心跳ping命令)
  - app_spi_cmd_can_bridge.cpp (SPI到CAN桥接命令)

- 修改 app_spi_command.cpp 仅负责SPI链路管理和命令路由逻辑
- 更新配置文件添加新源文件到项目编译列表
- 调整全局实例为对象实例(AppSpiCommandBus),移除静态方法依赖
- 更新README文档说明新的SPI命令总线拆分架构
- 微调版本号(patch: 0 -> 1, build: 2 -> 0)
This commit is contained in:
cbb
2026-06-23 01:49:33 +08:00
parent 01382a3fc1
commit dc91883f1e
14 changed files with 580 additions and 273 deletions
@@ -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
@@ -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);
}
@@ -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<uint32_t>(ctx.payload[1]) << 24)
| (static_cast<uint32_t>(ctx.payload[2]) << 16)
| (static_cast<uint32_t>(ctx.payload[3]) << 8)
| static_cast<uint32_t>(ctx.payload[4]);
const uint8_t *can_data = &ctx.payload[5];
const uint8_t can_dlc = static_cast<uint8_t>(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);
}
@@ -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);
}
@@ -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 <cstring>
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);
}
+212 -230
View File
@@ -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 <stdio.h>
#include <string.h>
#include <cstdio>
#include <cstring>
/* ============================================================
* 协议常量(与 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<uint32_t>(payload[1]) << 24)
| (static_cast<uint32_t>(payload[2]) << 16)
| (static_cast<uint32_t>(payload[3]) << 8)
| static_cast<uint32_t>(payload[4]);
const uint8_t *can_data = &payload[5];
uint8_t can_dlc = static_cast<uint8_t>(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();
}
+62 -34
View File
@@ -1,59 +1,87 @@
#ifndef DEVICE_APP_SPI_COMMAND_H
#define DEVICE_APP_SPI_COMMAND_H
#include <stdint.h>
#include <cstdint>
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 */
@@ -0,0 +1,36 @@
#ifndef DEVICE_APP_SPI_COMMAND_DEFS_H
#define DEVICE_APP_SPI_COMMAND_DEFS_H
#include <cstdint>
/**
* @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 */
@@ -0,0 +1,25 @@
#ifndef DEVICE_SPI_COMMAND_HANDLER_H
#define DEVICE_SPI_COMMAND_HANDLER_H
#include <cstdint>
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 */
+2 -2
View File
@@ -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 存储地址
+22
View File
@@ -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"
+2 -2
View File
@@ -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");
+2 -2
View File
@@ -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();
}
+12 -2
View File
@@ -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`