feat(spi): 实现SPI同步通信协议和主动推送机制
- 实现新的SPI同步帧格式,采用0xA5 0x5A起始符和CRC32校验,提升通信可靠性 - 添加GD32与ESP32间的握手机制,通过PB6开漏输出实现SPIready线与控制 - 实现异步OTA启动机制,将Flash操作延迟到主循环执行,避免SPI ISR阻塞 - 改进CAN反馈命令处理,提取通用payload构建函数支持主动推送模式 - 实现GD32主动按铃机制,循环推送CAN反馈数据到ESP32,减少轮询开销 - 添加SPI通信超时保护和死锁防护机制,确保系统稳定性
This commit is contained in:
@@ -16,17 +16,28 @@ public:
|
||||
|
||||
void handle(AppSpiCommand &bus, const SpiCommandContext &ctx) override
|
||||
{
|
||||
bus.postLog("BOOT_REQ → 进BOOT");
|
||||
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);
|
||||
/* 立即装 0xB5 ACK,让 ESP32 尽快收到确认。
|
||||
* Flash 操作延迟到主循环执行,避免阻塞 SPI ISR。
|
||||
*
|
||||
* ESP32 appRequestBootloader() 发送单字节 0xB0 后,逐字节轮询读回 0xB5。
|
||||
* 必须返回裸字节 ACK,不能用 buildResponse() 构建帧式 ACK
|
||||
* (帧式 ACK 以 0xA5 开头,ESP32 永远匹配不到 0xB5)。
|
||||
* tx_buf[0] 必须是 ack 字节本身,因为 ESP32 只读首个 MISO 字节。 */
|
||||
if (ctx.tx_buf && ctx.tx_len && ctx.tx_buf_size >= 4U) {
|
||||
ctx.tx_buf[0] = CONFIG_SPI_APP_OTA_BOOT_ACK_OK;
|
||||
ctx.tx_buf[1] = CONFIG_SPI_APP_OTA_BOOT_CMD;
|
||||
ctx.tx_buf[2] = 0x4FU;
|
||||
ctx.tx_buf[3] = 0x54U;
|
||||
*ctx.tx_len = 4U;
|
||||
}
|
||||
|
||||
/* 设置 pending 标志,主循环执行 Flash 操作 + 复位。
|
||||
* 原来在 ISR 内直接调 requestEnterBoot() 会执行 Flash 页擦除(20-30ms),
|
||||
* 导致 CPU 总线停顿、SPI RBNE 中断无法响应、ESP32 ACK 轮询丢字节。 */
|
||||
OtaBootRequest::requestEnterBootAsync();
|
||||
TaskManager::set(1UL << TASK_HEARTBEAT);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -38,17 +38,19 @@
|
||||
#include "device/spi/app_spi_command_defs.h"
|
||||
#include "device/canfd.h"
|
||||
#include "sys/config.h"
|
||||
#include "sys/ota_boot_request.h"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
/* ---- 文件内共享常量与辅助函数(供匿名命名空间内的 handler 与
|
||||
* 全局作用域的 canFeedbackPushStep() 共同复用) ---- */
|
||||
|
||||
constexpr uint8_t kFeedbackChannelCount = 3U;
|
||||
constexpr uint16_t kFeedbackPayloadLen = 123U; /* kTxBufSize=128 - overhead=5 */
|
||||
constexpr uint8_t kPageEntries = 8U;
|
||||
constexpr uint16_t kFixedHeaderLen = 28U; /* ch(1)+page(1)+total(1)+cnt(1)+mode(1)+seq(4)+rx(4)+tx(4)+drop(4)+err(4)+tec(1)+rec(1)+bus_state(1) */
|
||||
static constexpr uint8_t kFeedbackChannelCount = 3U;
|
||||
static constexpr uint16_t kFeedbackPayloadLen = 123U; /* kTxBufSize=128 - overhead=5 */
|
||||
static constexpr uint8_t kPageEntries = 8U;
|
||||
static constexpr uint16_t kFixedHeaderLen = 28U; /* ch(1)+page(1)+total(1)+cnt(1)+mode(1)+seq(4)+rx(4)+tx(4)+drop(4)+err(4)+tec(1)+rec(1)+bus_state(1) */
|
||||
|
||||
void writeU32Le(uint8_t *dst, uint32_t value)
|
||||
static void writeU32Le(uint8_t *dst, uint32_t value)
|
||||
{
|
||||
dst[0] = static_cast<uint8_t>(value & 0xFFU);
|
||||
dst[1] = static_cast<uint8_t>((value >> 8) & 0xFFU);
|
||||
@@ -56,6 +58,90 @@ void writeU32Le(uint8_t *dst, uint32_t value)
|
||||
dst[3] = static_cast<uint8_t>((value >> 24) & 0xFFU);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 构建一页 CAN 反馈 payload(与 ESP32 canFdPollFeedback 解析格式一致)。
|
||||
* @param[in] channel 目标通道 0/1/2。
|
||||
* @param[in] page 页号 0/1。
|
||||
* @param[out] payload 输出缓冲(至少 kFeedbackPayloadLen 字节)。
|
||||
* @param[in] max_len 缓冲最大长度。
|
||||
* @return 构建完成的 payload 长度;0 表示参数错误。
|
||||
*
|
||||
* 提取自 handleFeedback(),供被动响应与主动推送复用。
|
||||
*/
|
||||
static uint16_t buildFeedbackPayload(uint8_t channel, uint8_t page,
|
||||
uint8_t *payload, uint16_t max_len)
|
||||
{
|
||||
if (channel >= kFeedbackChannelCount || page >= 2U) {
|
||||
return 0U;
|
||||
}
|
||||
if (max_len < kFeedbackPayloadLen) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
/* 拷贝统计快照 */
|
||||
CanFd::ChannelFeedback snap{};
|
||||
CanFd::copyFeedbackSnapshot(channel, &snap);
|
||||
|
||||
/* 拷贝 ID 快照表 */
|
||||
CanFd::IdSnapshotTable table{};
|
||||
CanFd::copyIdSnapshotTable(channel, &table);
|
||||
|
||||
const uint8_t start = static_cast<uint8_t>(page * kPageEntries);
|
||||
if (start >= CANFD_ID_SNAPSHOT_MAX) {
|
||||
return 0U;
|
||||
}
|
||||
|
||||
/* 固定头 */
|
||||
payload[0] = channel;
|
||||
payload[1] = page;
|
||||
payload[2] = table.count;
|
||||
/* payload[3] = entry_count 稍后填 */
|
||||
payload[4] = static_cast<uint8_t>(CanFd::mode());
|
||||
writeU32Le(&payload[5], snap.snapshot_seq);
|
||||
writeU32Le(&payload[9], snap.stats.rx_count);
|
||||
writeU32Le(&payload[13], snap.stats.tx_count);
|
||||
writeU32Le(&payload[17], snap.stats.rx_drop_count);
|
||||
writeU32Le(&payload[21], snap.stats.error_count);
|
||||
payload[25] = snap.stats.tec;
|
||||
payload[26] = snap.stats.rec;
|
||||
payload[27] = snap.stats.bus_state;
|
||||
|
||||
/* 打包 ID 条目 */
|
||||
uint16_t offset = kFixedHeaderLen;
|
||||
uint8_t packed = 0U;
|
||||
|
||||
const uint8_t end = static_cast<uint8_t>(
|
||||
(start + kPageEntries < CANFD_ID_SNAPSHOT_MAX)
|
||||
? (start + kPageEntries) : CANFD_ID_SNAPSHOT_MAX);
|
||||
|
||||
for (uint8_t i = start; i < end; ++i) {
|
||||
const auto &e = table.entries[i];
|
||||
if (!e.occupied) continue;
|
||||
|
||||
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
|
||||
? e.dlc : static_cast<uint8_t>(CANFD_ID_SNAPSHOT_DATA_MAX);
|
||||
const uint16_t entrySize = static_cast<uint16_t>(8U + dataLen);
|
||||
|
||||
if (offset + entrySize > kFeedbackPayloadLen) break;
|
||||
|
||||
writeU32Le(&payload[offset], e.id);
|
||||
payload[offset + 4U] = e.dlc;
|
||||
payload[offset + 5U] = static_cast<uint8_t>(
|
||||
(e.fd_flag ? 1U : 0U) | (e.brs ? 2U : 0U) | (e.esi ? 4U : 0U));
|
||||
payload[offset + 6U] = 0U; /* reserved */
|
||||
payload[offset + 7U] = 0U; /* reserved */
|
||||
memcpy(&payload[offset + 8U], e.data, dataLen);
|
||||
|
||||
offset += entrySize;
|
||||
packed++;
|
||||
}
|
||||
|
||||
payload[3] = packed;
|
||||
return offset;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
class SpiCanFeedbackHandler final : public ISpiCommandHandler {
|
||||
public:
|
||||
bool canHandle(uint8_t frame_type) const override
|
||||
@@ -86,80 +172,17 @@ private:
|
||||
const uint8_t channel = ctx.payload[0];
|
||||
const uint8_t page = ctx.payload[1];
|
||||
|
||||
if (channel >= kFeedbackChannelCount) {
|
||||
uint8_t err = 0xE1U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspError, &err, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
/* 拷贝统计快照 */
|
||||
CanFd::ChannelFeedback snap{};
|
||||
CanFd::copyFeedbackSnapshot(channel, &snap);
|
||||
|
||||
/* 拷贝 ID 快照表 */
|
||||
CanFd::IdSnapshotTable table{};
|
||||
CanFd::copyIdSnapshotTable(channel, &table);
|
||||
|
||||
const uint8_t start = static_cast<uint8_t>(page * kPageEntries);
|
||||
if (start >= CANFD_ID_SNAPSHOT_MAX) {
|
||||
uint8_t payload[kFeedbackPayloadLen] = {};
|
||||
uint16_t payload_len = buildFeedbackPayload(channel, page, payload, sizeof(payload));
|
||||
if (payload_len == 0U) {
|
||||
uint8_t err = 0xE3U;
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspError, &err, 1U);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t payload[kFeedbackPayloadLen] = {};
|
||||
|
||||
/* 固定头 */
|
||||
payload[0] = channel;
|
||||
payload[1] = page;
|
||||
payload[2] = table.count;
|
||||
/* payload[3] = entry_count 稍后填 */
|
||||
payload[4] = static_cast<uint8_t>(CanFd::mode());
|
||||
writeU32Le(&payload[5], snap.snapshot_seq);
|
||||
writeU32Le(&payload[9], snap.stats.rx_count);
|
||||
writeU32Le(&payload[13], snap.stats.tx_count);
|
||||
writeU32Le(&payload[17], snap.stats.rx_drop_count);
|
||||
writeU32Le(&payload[21], snap.stats.error_count);
|
||||
payload[25] = snap.stats.tec;
|
||||
payload[26] = snap.stats.rec;
|
||||
payload[27] = snap.stats.bus_state;
|
||||
|
||||
/* 打包 ID 条目 */
|
||||
uint16_t offset = kFixedHeaderLen;
|
||||
uint8_t packed = 0U;
|
||||
|
||||
const uint8_t end = static_cast<uint8_t>(
|
||||
(start + kPageEntries < CANFD_ID_SNAPSHOT_MAX)
|
||||
? (start + kPageEntries) : CANFD_ID_SNAPSHOT_MAX);
|
||||
|
||||
for (uint8_t i = start; i < end; ++i) {
|
||||
const auto &e = table.entries[i];
|
||||
if (!e.occupied) continue;
|
||||
|
||||
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
|
||||
? e.dlc : static_cast<uint8_t>(CANFD_ID_SNAPSHOT_DATA_MAX);
|
||||
const uint16_t entrySize = static_cast<uint16_t>(8U + dataLen);
|
||||
|
||||
if (offset + entrySize > kFeedbackPayloadLen) break;
|
||||
|
||||
writeU32Le(&payload[offset], e.id);
|
||||
payload[offset + 4U] = e.dlc;
|
||||
payload[offset + 5U] = static_cast<uint8_t>(
|
||||
(e.fd_flag ? 1U : 0U) | (e.brs ? 2U : 0U) | (e.esi ? 4U : 0U));
|
||||
payload[offset + 6U] = 0U; /* reserved */
|
||||
payload[offset + 7U] = 0U; /* reserved */
|
||||
memcpy(&payload[offset + 8U], e.data, dataLen);
|
||||
|
||||
offset += entrySize;
|
||||
packed++;
|
||||
}
|
||||
|
||||
payload[3] = packed;
|
||||
|
||||
bus.buildResponse(ctx.tx_buf, ctx.tx_buf_size, ctx.tx_len,
|
||||
SpiDefs::kRspAck, payload, offset);
|
||||
SpiDefs::kRspAck, payload, payload_len);
|
||||
}
|
||||
|
||||
void handleSetMode(AppSpiCommand &bus, const SpiCommandContext &ctx)
|
||||
@@ -207,6 +230,49 @@ static SpiCanFeedbackHandler s_handler;
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief CAN 反馈主动推送一步(非阻塞)。
|
||||
*
|
||||
* 由主循环每次迭代调用。维护内部 (channel, page) 游标,循环遍历 3 通道 × 2 页。
|
||||
* 若上一帧按铃尚未被 ESP32 读走(m_ringPending)则立即返回等待;
|
||||
* 否则构建当前页反馈 payload 并通过 requestEsp32Read(0xC1) 主动推送到 ESP32,
|
||||
* 然后推进游标。ESP32 检测到 SPIready 下降沿后发起读事务取走该帧,
|
||||
* CS 释放后 GD32 中断唤醒主循环继续推送下一帧。
|
||||
*/
|
||||
void canFeedbackPushStep()
|
||||
{
|
||||
static uint8_t s_channel = 0U;
|
||||
static uint8_t s_page = 0U;
|
||||
|
||||
/* 已收到 0xB0 进入 Bootloader 请求,停止主动按铃避免干扰 OTA 流程。
|
||||
* 复位前主循环仍在运行,但不再下拉 SPIready,让 ESP32 的
|
||||
* waitForBootloaderReady 信号检测不受干扰。 */
|
||||
if (OtaBootRequest::isRebootPending()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* 上一帧尚未被 ESP32 读走,等待(不占用主线带宽) */
|
||||
if (AppSpiCommandBus.isRingPending()) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t payload[kFeedbackPayloadLen] = {};
|
||||
uint16_t payload_len = buildFeedbackPayload(s_channel, s_page,
|
||||
payload, sizeof(payload));
|
||||
if (payload_len > 0U) {
|
||||
(void)AppSpiCommandBus.requestEsp32Read(SpiDefs::kTypeCanFeedback,
|
||||
payload, payload_len);
|
||||
}
|
||||
|
||||
/* 推进到下一页/下一通道,无限循环 3 通道 × 2 页 */
|
||||
if (++s_page >= 2U) {
|
||||
s_page = 0U;
|
||||
if (++s_channel >= kFeedbackChannelCount) {
|
||||
s_channel = 0U;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void registerSpiCanFeedbackCommand(AppSpiCommand &bus)
|
||||
{
|
||||
bus.registerHandler(&s_handler);
|
||||
|
||||
@@ -8,9 +8,14 @@
|
||||
* - 公共帧格式解析与应答构建
|
||||
* - 业务命令的注册与分发
|
||||
*
|
||||
* 具体命令处理逻辑已经拆到独立的 `app_spi_cmd_*.cpp`,
|
||||
* 通过 `registerHandler()` 注册进来,后续新增 SPI 业务时
|
||||
* 具体命令处理逻辑已经拆到独立的 app_spi_cmd_*.cpp,
|
||||
* 通过 registerHandler() 注册进来,后续新增 SPI 业务时
|
||||
* 只需要新增命令处理器,不再继续膨胀本文件。
|
||||
*
|
||||
* 握手架构(线与 SPIready,PB6<->ESP32 GPIO10 开漏单线):
|
||||
* - 命令处理在 onCsRelease() EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame),
|
||||
* 不再推迟到主循环,消除 PB6 拉高延迟。
|
||||
* - GD32 可通过 requestEsp32Read() 下拉 SPIready 主动按铃请求 ESP32 来读。
|
||||
*/
|
||||
|
||||
#include "device/spi/app_spi_command.h"
|
||||
@@ -62,12 +67,12 @@ static void startSpiTxDma(const uint8_t *buf, uint16_t len)
|
||||
DMA_CHCTL(SPI_TX_DMA_PERIPH, SPI_TX_DMA_CH) |= DMA_CHXCTL_CHEN;
|
||||
}
|
||||
|
||||
static bool isAllBytesValue(const uint8_t *data, uint8_t len, uint8_t value)
|
||||
static bool isAllBytesValue(const uint8_t *data, uint16_t len, uint8_t value)
|
||||
{
|
||||
if (!data || len == 0U) {
|
||||
return false;
|
||||
}
|
||||
for (uint8_t i = 0U; i < len; ++i) {
|
||||
for (uint16_t i = 0U; i < len; ++i) {
|
||||
if (data[i] != value) {
|
||||
return false;
|
||||
}
|
||||
@@ -75,7 +80,7 @@ static bool isAllBytesValue(const uint8_t *data, uint8_t len, uint8_t value)
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool shouldIgnoreTransaction(const uint8_t *data, uint8_t len)
|
||||
static bool shouldIgnoreTransaction(const uint8_t *data, uint16_t len)
|
||||
{
|
||||
if (!data || len == 0U) {
|
||||
return true;
|
||||
@@ -110,40 +115,111 @@ static bool isFlashRelatedFrame(uint8_t frame_type)
|
||||
* CRC8 / 帧构建 / 帧解析(与 BOOT SpiOta 一致)
|
||||
* ============================================================ */
|
||||
|
||||
static uint8_t spi_crc8(const uint8_t *data, uint16_t len)
|
||||
static constexpr uint8_t kSyncSof0 = 0xA5U;
|
||||
static constexpr uint8_t kSyncSof1 = 0x5AU;
|
||||
static constexpr uint16_t kSyncHeaderLen = 6U;
|
||||
static constexpr uint16_t kSyncCrcLen = 4U;
|
||||
static constexpr uint16_t kSyncOverhead = (uint16_t)(kSyncHeaderLen + kSyncCrcLen);
|
||||
|
||||
/* GD32 APP 侧同步状态机(ISR 与主循环只读/原子写,避免临界区开销)。 */
|
||||
static volatile uint8_t g_localCredit = 8U;
|
||||
static volatile uint8_t g_remoteCredit = 8U;
|
||||
|
||||
static void putU16Le(uint8_t *pp, uint16_t v)
|
||||
{
|
||||
uint8_t crc = 0x00U;
|
||||
pp[0] = (uint8_t)(v & 0xFFU);
|
||||
pp[1] = (uint8_t)((v >> 8) & 0xFFU);
|
||||
}
|
||||
|
||||
static uint16_t getU16Le(const uint8_t *pp)
|
||||
{
|
||||
return (uint16_t)(pp[0] | ((uint16_t)pp[1] << 8));
|
||||
}
|
||||
|
||||
static void putU32Le(uint8_t *pp, uint32_t v)
|
||||
{
|
||||
pp[0] = (uint8_t)(v & 0xFFU);
|
||||
pp[1] = (uint8_t)((v >> 8) & 0xFFU);
|
||||
pp[2] = (uint8_t)((v >> 16) & 0xFFU);
|
||||
pp[3] = (uint8_t)((v >> 24) & 0xFFU);
|
||||
}
|
||||
|
||||
static uint32_t getU32Le(const uint8_t *pp)
|
||||
{
|
||||
return (uint32_t)pp[0] | ((uint32_t)pp[1] << 8) |
|
||||
((uint32_t)pp[2] << 16) | ((uint32_t)pp[3] << 24);
|
||||
}
|
||||
|
||||
static uint32_t spi_crc32(const uint8_t *data, uint16_t len)
|
||||
{
|
||||
uint32_t crc = 0xFFFFFFFFU;
|
||||
for (uint16_t i = 0U; i < len; ++i) {
|
||||
crc ^= data[i];
|
||||
crc ^= (uint32_t)data[i] << 24;
|
||||
for (int b = 0; b < 8; ++b) {
|
||||
if (crc & 0x80000000U) {
|
||||
crc = (crc << 1) ^ 0x04C11DB7U;
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
static uint8_t spi_crc8(const uint8_t *data, uint16_t len)
|
||||
{
|
||||
uint8_t crc = 0x00U;
|
||||
for (uint16_t i = 0U; i < len; ++i) { crc ^= data[i]; }
|
||||
return crc;
|
||||
}
|
||||
|
||||
/* 构建新同步帧;上层接口(type/payload)保持不变,handler 无需改动。
|
||||
* kFrameOverhead 旧常量仍保留供旧路径使用。 */
|
||||
static bool spi_build_frame(uint8_t type, const uint8_t *payload, uint16_t payload_len,
|
||||
uint8_t *out, uint16_t *out_len)
|
||||
{
|
||||
if (!out || !out_len) { return false; }
|
||||
uint16_t total = (uint16_t)(kFrameOverhead + payload_len);
|
||||
uint16_t total = (uint16_t)(kSyncOverhead + payload_len);
|
||||
if (total > AppSpiCommand::kTxBufSize) { return false; }
|
||||
out[0] = kSof;
|
||||
out[1] = type;
|
||||
out[2] = (uint8_t)(payload_len & 0xFFU);
|
||||
out[3] = (uint8_t)((payload_len >> 8) & 0xFFU);
|
||||
|
||||
out[0] = kSyncSof0;
|
||||
out[1] = kSyncSof1;
|
||||
out[2] = g_localCredit;
|
||||
out[3] = type;
|
||||
putU16Le(&out[4], payload_len);
|
||||
if (payload && payload_len > 0U) {
|
||||
memcpy(&out[4], payload, payload_len);
|
||||
memcpy(&out[kSyncHeaderLen], payload, payload_len);
|
||||
}
|
||||
out[4 + payload_len] = spi_crc8(out, (uint16_t)(4U + payload_len));
|
||||
uint32_t crc = spi_crc32(out, (uint16_t)(kSyncHeaderLen + payload_len));
|
||||
putU32Le(&out[kSyncHeaderLen + payload_len], crc);
|
||||
*out_len = total;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* 解析:新同步帧优先;失败回退旧 0xA5 帧(含首字节偏移容错)。 */
|
||||
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 || !type || !payload || !payload_len) { return false; }
|
||||
|
||||
/* 兼容首字节延迟:在帧头前面可能出现若干 0xFF / 0x00 残留字节,
|
||||
* 搜索到第一个 kSof 后尝试按合法帧长度校验 CRC。 */
|
||||
for (uint16_t s = 0U; s + kSyncOverhead <= frame_len; ++s) {
|
||||
if (frame[s] != kSyncSof0 || frame[s + 1U] != kSyncSof1) { continue; }
|
||||
uint16_t plen = getU16Le(&frame[s + 4U]);
|
||||
uint16_t total = (uint16_t)(kSyncOverhead + plen);
|
||||
if ((uint16_t)(frame_len - s) < total) { continue; }
|
||||
uint32_t expect = getU32Le(&frame[s + kSyncHeaderLen + plen]);
|
||||
uint32_t calc = spi_crc32(&frame[s], (uint16_t)(kSyncHeaderLen + plen));
|
||||
if (expect != calc) { continue; }
|
||||
|
||||
uint8_t credit = frame[s + 2U];
|
||||
*type = frame[s + 3U];
|
||||
*payload = (plen > 0U) ? &frame[s + kSyncHeaderLen] : nullptr;
|
||||
*payload_len = plen;
|
||||
g_remoteCredit = credit;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* 旧 0xA5 帧兼容:首字节可能偏移,扫描 SOF。 */
|
||||
uint16_t s = 0U;
|
||||
while (s < frame_len && frame[s] != kSof) { ++s; }
|
||||
if (s > 0U && s < frame_len) {
|
||||
@@ -214,15 +290,11 @@ void AppSpiCommand::buildResponse(uint8_t *tx_buf, uint16_t tx_buf_size,
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 硬件配置(与 BOOT 完全一致)
|
||||
* 硬件配置
|
||||
* ============================================================ */
|
||||
|
||||
void AppSpiCommand::configureGpio()
|
||||
@@ -238,13 +310,17 @@ void AppSpiCommand::configureGpio()
|
||||
gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_4);
|
||||
gpio_init(GPIOA, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_15);
|
||||
|
||||
/* PB6 复用为 SPI Ready 握手输出,默认 HIGH(就绪),允许主机立刻发起首帧 */
|
||||
gpio_init(CONFIG_SPI_READY_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, CONFIG_SPI_READY_PIN);
|
||||
/* PB6 复用为 SPI Ready 握手线(开漏输出,线与)。
|
||||
* 开漏模式:写 BRR = 下拉驱动低;写 BSRR = 高阻释放,靠外部/对端上拉拉高。
|
||||
* 默认释放(HIGH),允许主机立刻发起首帧。 */
|
||||
gpio_init(CONFIG_SPI_READY_PORT, GPIO_MODE_OUT_OD, GPIO_OSPEED_50MHZ, CONFIG_SPI_READY_PIN);
|
||||
gpio_bit_set(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN);
|
||||
}
|
||||
|
||||
void AppSpiCommand::setSpiReady(bool ready)
|
||||
{
|
||||
/* 开漏线与:ready=true 释放(BSRR,高阻,靠上拉拉高);
|
||||
* ready=false 下拉(BRR,驱动低)。 */
|
||||
if (ready) {
|
||||
gpio_bit_set(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN);
|
||||
} else {
|
||||
@@ -328,6 +404,9 @@ void AppSpiCommand::init()
|
||||
m_logPending = false;
|
||||
m_txActiveThisCs = false;
|
||||
m_commandCount = 0U;
|
||||
m_ringPending = false;
|
||||
m_ringDeadline = 0U;
|
||||
m_ringTxLen = 0U;
|
||||
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
||||
memset(m_logBuf, 0, sizeof(m_logBuf));
|
||||
memset(m_commands, 0, sizeof(m_commands));
|
||||
@@ -395,7 +474,7 @@ void AppSpiCommand::processCommand()
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
postLogFmt("[SPI-APP] 未知 type=0x%02X → ERROR", frame_type);
|
||||
postLogFmt("[SPI-APP] 未知 type=0x%02X -> ERROR", frame_type);
|
||||
uint8_t err = 0xEEU;
|
||||
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
|
||||
}
|
||||
@@ -406,13 +485,13 @@ void AppSpiCommand::processCommand()
|
||||
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
|
||||
}
|
||||
|
||||
m_txLen = (uint8_t)tx_len;
|
||||
m_txLen = 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");
|
||||
postLog("旧式0xB0 -> 进BOOT");
|
||||
|
||||
uint16_t tx_len_legacy = 0U;
|
||||
SpiCommandContext ctx{};
|
||||
@@ -434,28 +513,27 @@ void AppSpiCommand::processCommand()
|
||||
}
|
||||
|
||||
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;
|
||||
m_txBuf[0] = CONFIG_SPI_APP_OTA_BOOT_CMD;
|
||||
m_txBuf[1] = ack;
|
||||
/* 兜底路径(handler 未注册时的死代码保护):
|
||||
* 同样使用异步路径,避免在 ISR 内执行 Flash 操作。
|
||||
* 先装 0xB5 ACK,再设 pending 标志。 */
|
||||
m_txBuf[0] = CONFIG_SPI_APP_OTA_BOOT_ACK_OK;
|
||||
m_txBuf[1] = CONFIG_SPI_APP_OTA_BOOT_CMD;
|
||||
m_txBuf[2] = 0x4FU;
|
||||
m_txBuf[3] = 0x54U;
|
||||
tx_len_legacy = 4U;
|
||||
if (ok) {
|
||||
TaskManager::set(1UL << TASK_HEARTBEAT);
|
||||
}
|
||||
OtaBootRequest::requestEnterBootAsync();
|
||||
TaskManager::set(1UL << TASK_HEARTBEAT);
|
||||
}
|
||||
|
||||
m_txLen = (uint8_t)tx_len_legacy;
|
||||
m_txLen = tx_len_legacy;
|
||||
m_txPos = 0U;
|
||||
} else {
|
||||
/* 无法识别 */
|
||||
postLogFmt("[SPI-APP] 非协议 0x%02X → ERROR", first_valid);
|
||||
postLogFmt("[SPI-APP] 非协议 0x%02X -> ERROR", first_valid);
|
||||
uint16_t tx_len = 0U;
|
||||
uint8_t err = 0xEEU;
|
||||
spi_build_frame(kRspError, &err, 1U, m_txBuf, &tx_len);
|
||||
m_txLen = (uint8_t)tx_len;
|
||||
m_txLen = tx_len;
|
||||
m_txPos = 0U;
|
||||
}
|
||||
}
|
||||
@@ -465,20 +543,65 @@ void AppSpiCommand::processCommand()
|
||||
TaskManager::set(1UL << TASK_SPI_LOG);
|
||||
}
|
||||
|
||||
/* 应答帧已就绪:若解析期间未再收到新帧(m_cmdPending 仍为 false),
|
||||
* 拉高 PB6 通知主机可发起下一笔事务;否则保持 LOW,等待下一轮
|
||||
* processCommand 完成后再翻高,避免主机在从机未就绪时抢跑。 */
|
||||
if (!m_cmdPending) {
|
||||
setSpiReady(true);
|
||||
/* 应答帧已就绪:拉高 PB6(释放 SPIready)通知主机可发起下一笔事务。
|
||||
* 命令处理已在 ISR 内同步完成,无需 m_cmdPending 推迟机制。 */
|
||||
|
||||
/* 有响应待发时,设置按铃标志阻止主循环 canFeedbackPushStep() 覆盖 m_txBuf。
|
||||
* ESP32 下次 CS assert 时 DMA 输出响应,onCsRelease 清除 m_ringPending。
|
||||
* 50ms 超时后 cancelRing() 清除,恢复 CAN 反馈推送。 */
|
||||
if (m_txLen > 0U) {
|
||||
m_ringPending = true;
|
||||
m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
|
||||
}
|
||||
setSpiReady(true);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 主循环轮询:打印延迟日志
|
||||
* GD32 主动按铃:下拉 SPIready 请求 ESP32 来读
|
||||
* ============================================================ */
|
||||
|
||||
void AppSpiCommand::cancelRing()
|
||||
{
|
||||
m_ringPending = false;
|
||||
m_ringTxLen = 0U;
|
||||
/* 清除待发响应,防止超时后残留帧在下一次 CS assert 时被错误输出 */
|
||||
m_txLen = 0U;
|
||||
m_txPos = 0U;
|
||||
/* 释放 SPIready(高阻),避免永久占用线 */
|
||||
setSpiReady(true);
|
||||
}
|
||||
|
||||
bool AppSpiCommand::requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len)
|
||||
{
|
||||
/* 若上一次按铃尚未被 ESP32 读走,拒绝重复按铃 */
|
||||
if (m_ringPending) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 构建请求帧到 m_txBuf(ESP32 下次读事务时通过 DMA 输出到 MISO) */
|
||||
uint16_t built_len = 0U;
|
||||
if (!spi_build_frame(frame_type, payload, payload_len, m_txBuf, &built_len)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_ringTxLen = built_len;
|
||||
m_txLen = built_len;
|
||||
m_txPos = 0U;
|
||||
m_ringDeadline = SysTickTimer::getMs() + kRingTimeoutMs;
|
||||
m_ringPending = true;
|
||||
|
||||
/* 下拉 SPIready,通知 ESP32「我有请求帧要给你读」 */
|
||||
setSpiReady(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* 主循环轮询:打印延迟日志 + 按铃超时看护
|
||||
* ============================================================ */
|
||||
|
||||
void AppSpiCommand::poll()
|
||||
{
|
||||
/* 打印 ISR 推迟的日志 */
|
||||
char log_copy[kLogBufSize] = {};
|
||||
bool has_log = false;
|
||||
|
||||
@@ -493,15 +616,18 @@ void AppSpiCommand::poll()
|
||||
if (has_log) {
|
||||
printf("%s\r\n", log_copy);
|
||||
}
|
||||
}
|
||||
|
||||
void AppSpiCommand::processPendingCommand()
|
||||
{
|
||||
if (!m_cmdPending) {
|
||||
return;
|
||||
/* 按铃超时看护:ESP32 未在 kRingTimeoutMs 内来读则放弃本次按铃。
|
||||
* 防止 ESP32 不响应时永久占用 SPIready 线导致双方死锁。 */
|
||||
if (m_ringPending) {
|
||||
uint32_t save2 = RT_CRITICAL_SAVE();
|
||||
if (m_ringPending) {
|
||||
if ((int32_t)(SysTickTimer::getMs() - m_ringDeadline) >= 0) {
|
||||
cancelRing();
|
||||
}
|
||||
}
|
||||
RT_CRITICAL_RESTORE(save2);
|
||||
}
|
||||
m_cmdPending = false;
|
||||
processCommand();
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -514,8 +640,8 @@ void AppSpiCommand::onCsAssert()
|
||||
return;
|
||||
}
|
||||
m_csActive = true;
|
||||
/* CS 下降沿立即拉低 PB6:表示从机即将忙碌,
|
||||
* 防止主机在 onCsRelease 拉低 PB6 前抢跑发起下一笔 CS。
|
||||
/* CS 下降沿立即拉低 PB6:表示从机即将忙碌,
|
||||
* 防止主机在 onCsRelease 准备好响应前抢跑发起下一笔 CS。
|
||||
* (若此帧被 shouldIgnoreTransaction 忽略,onCsRelease 会恢复 HIGH) */
|
||||
setSpiReady(false);
|
||||
m_rxLen = 0U;
|
||||
@@ -570,6 +696,48 @@ void AppSpiCommand::onCsRelease()
|
||||
}
|
||||
}
|
||||
|
||||
/* 若本次 CS 事务是 ESP32 响应 GD32 按铃来读请求帧,
|
||||
* 则 GD32 的请求帧已通过 DMA 输出到 MISO,取消按铃状态。
|
||||
*
|
||||
* 但需区分:ESP32 发的是 dummy 读(按铃响应)还是 0xB0 命令(OTA 请求)。
|
||||
* 若 GD32 正在按铃时 ESP32 发 0xB0,会因 m_ringPending=true 被误吞,
|
||||
* 导致 OTA 无法启动。检测到 0xB0 时取消按铃并走正常命令处理。 */
|
||||
if (m_ringPending) {
|
||||
if (m_rxLen >= 1U && m_rxBuf[0] == CONFIG_SPI_APP_OTA_BOOT_CMD) {
|
||||
/* ESP32 发的是 0xB0 Bootloader 请求,不是按铃响应。
|
||||
* 取消按铃,清空 TX 让 processCommand() 重新装载 0xB5 应答。
|
||||
* 不释放 SPIready,processCommand() 末尾会释放。 */
|
||||
m_ringPending = false;
|
||||
m_ringTxLen = 0U;
|
||||
m_txLen = 0U;
|
||||
m_txPos = 0U;
|
||||
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
||||
/* 继续走 shouldIgnoreTransaction + processCommand(),不 return */
|
||||
} else if (m_rxLen >= 2U && m_rxBuf[0] == kSyncSof0 && m_rxBuf[1] == kSyncSof1) {
|
||||
/* ESP32 发的是协议帧(如 PING/OTA 等),不是按铃响应。
|
||||
* 取消按铃,清空 TX 让 processCommand() 重新装载对应应答。
|
||||
* 不释放 SPIready,processCommand() 末尾会释放。 */
|
||||
m_ringPending = false;
|
||||
m_ringTxLen = 0U;
|
||||
m_txLen = 0U;
|
||||
m_txPos = 0U;
|
||||
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
||||
/* 继续走 shouldIgnoreTransaction + processCommand(),不 return */
|
||||
} else {
|
||||
/* 正常按铃响应:请求帧已被读走,取消按铃状态 */
|
||||
m_ringPending = false;
|
||||
m_ringTxLen = 0U;
|
||||
/* 请求帧已被读走,清空 TX 防止下次事务重复输出 */
|
||||
m_txLen = 0U;
|
||||
m_txPos = 0U;
|
||||
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
||||
/* 释放 SPIready,通知 ESP32 本帧完成 */
|
||||
setSpiReady(true);
|
||||
/* 上层若需要 ESP32 处理该请求帧,由 ESP32 侧读到的帧 TYPE 决定后续动作 */
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldIgnoreTransaction(m_rxBuf, m_rxLen)) {
|
||||
m_rxLen = 0U;
|
||||
if (m_txActiveThisCs) {
|
||||
@@ -578,8 +746,8 @@ void AppSpiCommand::onCsRelease()
|
||||
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
||||
}
|
||||
m_txActiveThisCs = false;
|
||||
/* 忽略事务(空轮询/残包)后恢复 PB6=HIGH,
|
||||
* 避免 PB6 卡 LOW 导致 ESP32 waitSpiReady 超时 */
|
||||
/* 忽略事务(空轮询/残包)后恢复 PB6=HIGH,
|
||||
* 避免 PB6 卡 LOW 导致 ESP32 等待上升沿超时 */
|
||||
setSpiReady(true);
|
||||
return;
|
||||
}
|
||||
@@ -588,10 +756,10 @@ void AppSpiCommand::onCsRelease()
|
||||
m_txPos = 0U;
|
||||
m_txActiveThisCs = false;
|
||||
memset(m_txBuf, 0xFF, sizeof(m_txBuf));
|
||||
m_cmdPending = true;
|
||||
/* 拉低 PB6 通知主机:从机正忙,在主循环解析完前不要再发起 CS */
|
||||
setSpiReady(false);
|
||||
TaskManager::set(1UL << TASK_SPI_CMD_PROCESS);
|
||||
|
||||
/* 命令处理在 EXTI ISR 内同步完成(对齐 BOOT 的 isrProcessFrame 模式),
|
||||
* 消除主循环延迟。处理完成后 processCommand() 末尾释放 SPIready。 */
|
||||
processCommand();
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
|
||||
@@ -12,20 +12,26 @@ class ISpiCommandHandler;
|
||||
* - 维护 SPI0 从机的硬件链路(GPIO/SPI/DMA/EXTI)
|
||||
* - 将接收到的合法帧按 TYPE 注册式分发给 ISpiCommandHandler
|
||||
*
|
||||
* 具体命令处理逻辑不再放在这里,而是拆到 `device/spi/app_spi_cmd_*.cpp`,
|
||||
* 通过 `registerHandler()` 注册进来,保持本模块可扩展。
|
||||
* 具体命令处理逻辑不再放在这里,而是拆到 device/spi/app_spi_cmd_*.cpp,
|
||||
* 通过 registerHandler() 注册进来,保持本模块可扩展。
|
||||
*
|
||||
* 握手架构(线与 SPIready,PB6<->ESP32 GPIO10 开漏单线):
|
||||
* - 空闲态线 HIGH(双方释放,靠上拉)。
|
||||
* - ESP32 主动发起:下拉 SPIready -> 立刻 CS 发请求 -> CS 释放后保持下拉,
|
||||
* GD32 在 onCsRelease() ISR 内完成帧处理+响应装载后释放 SPIready(高),
|
||||
* ESP32 检测到上升沿后发起 CS#2 读响应。
|
||||
* - GD32 主动按铃:下拉 SPIready -> ESP32 下降沿中断唤醒后发起读事务,
|
||||
* GD32 在 onCsRelease() 中装入请求帧并释放 SPIready。
|
||||
* - PB6 配置为开漏输出,setSpiReady(true)=高阻释放,setSpiReady(false)=下拉。
|
||||
*/
|
||||
class AppSpiCommand {
|
||||
public:
|
||||
/** @brief 初始化 SPI0 从机命令通道并注册默认命令处理器。 */
|
||||
void init();
|
||||
|
||||
/** @brief 主循环轮询:打印延迟日志。 */
|
||||
/** @brief 主循环轮询:打印延迟日志 + GD32 主动按铃超时看护。 */
|
||||
void poll();
|
||||
|
||||
/** @brief 主循环调用:执行 ISR 中推迟的 SPI 命令解析(从 onCsRelease 移出)。 */
|
||||
void processPendingCommand();
|
||||
|
||||
/** @brief SPI0 中断处理入口。 */
|
||||
void onInterrupt();
|
||||
|
||||
@@ -57,19 +63,41 @@ public:
|
||||
uint16_t *tx_len, uint8_t rsp_type,
|
||||
const uint8_t *payload, uint16_t payload_len);
|
||||
|
||||
static constexpr uint16_t kRxBufSize = 128U;
|
||||
static constexpr uint16_t kTxBufSize = 128U;
|
||||
/**
|
||||
* @brief GD32 主动按铃:下拉 SPIready,通知 ESP32「我有请求帧要给你读」。
|
||||
*
|
||||
* 将提供的请求帧装入内部 TX 缓冲区(供下次 ESP32 读事务通过 DMA 输出到 MISO),
|
||||
* 然后下拉 SPIready 线。ESP32 检测到下降沿后会发起一次 SPI 读事务把该帧读走。
|
||||
* 调用方负责确保 frame_type 与 payload 符合协议帧格式。
|
||||
*
|
||||
* @param[in] frame_type 请求帧类型(复用 SPI 协议帧 TYPE)。
|
||||
* @param[in] payload 请求帧载荷,可为 nullptr。
|
||||
* @param[in] payload_len 载荷长度。
|
||||
* @return true 成功下拉并装入 TX;false 当前已有未读按铃或帧构建失败。
|
||||
*
|
||||
* @note 可在主循环或 ISR 中调用。若上一次按铃尚未被 ESP32 读走(m_ringPending),
|
||||
* 则返回 false,调用方应稍后重试。按铃后启动 m_ringDeadline 超时看护,
|
||||
* 超时则放弃本次按铃(防止 ESP32 不响应时永久占用 SPIready 线)。
|
||||
*/
|
||||
bool requestEsp32Read(uint8_t frame_type, const uint8_t *payload, uint16_t payload_len);
|
||||
|
||||
/** @brief 查询当前是否有未读按铃(ESP32 尚未来读)。 */
|
||||
bool isRingPending() const { return m_ringPending; }
|
||||
|
||||
static constexpr uint16_t kRxBufSize = 256U;
|
||||
static constexpr uint16_t kTxBufSize = 256U;
|
||||
static constexpr uint16_t kLogBufSize = 256U;
|
||||
|
||||
private:
|
||||
static constexpr uint8_t kMaxCommands = 8U;
|
||||
/** GD32 主动按铃超时(ms):ESP32 未在该时间内来读则放弃。 */
|
||||
static constexpr uint32_t kRingTimeoutMs = 50U;
|
||||
|
||||
volatile bool m_csActive = false;
|
||||
volatile uint8_t m_rxLen = 0U;
|
||||
uint8_t m_txLen = 0U;
|
||||
volatile uint16_t m_rxLen = 0U;
|
||||
uint16_t m_txLen = 0U;
|
||||
volatile uint8_t m_txPos = 0U;
|
||||
volatile bool m_logPending = false;
|
||||
volatile bool m_cmdPending = false; /**< ISR 置位,标记有待主循环解析的 SPI 命令。 */
|
||||
bool m_txActiveThisCs = false;
|
||||
uint8_t m_rxBuf[kRxBufSize] = {};
|
||||
uint8_t m_txBuf[kTxBufSize] = {};
|
||||
@@ -77,6 +105,13 @@ private:
|
||||
uint8_t m_commandCount = 0U;
|
||||
ISpiCommandHandler *m_commands[kMaxCommands] = {};
|
||||
|
||||
/** GD32 主动按铃状态:true 表示已下拉 SPIready 等待 ESP32 来读。 */
|
||||
volatile bool m_ringPending = false;
|
||||
/** 按铃超时截止时刻(ms,基于 SysTickTimer::getMs)。 */
|
||||
volatile uint32_t m_ringDeadline = 0U;
|
||||
/** 按铃期间暂存的请求帧长度(供 onCsAssert 装入 DMA)。 */
|
||||
volatile uint16_t m_ringTxLen = 0U;
|
||||
|
||||
void configureGpio();
|
||||
void configureSpi0();
|
||||
void configureDma();
|
||||
@@ -85,13 +120,27 @@ private:
|
||||
void onCsRelease();
|
||||
void processCommand();
|
||||
|
||||
/** @brief 翻转 SPI Ready 握手引脚(PB6)电平。
|
||||
* @param[in] ready true=就绪(HIGH), false=忙(LOW)。
|
||||
* @note GPIO BSRR/BRR 写入原子,无需临界区;可由 ISR 与主循环共同调用。
|
||||
/** @brief 翻转 SPI Ready 握手引脚(PB6)电平(开漏线与)。
|
||||
* @param[in] ready true=释放(高阻,靠上拉拉高), false=下拉(驱动低)。
|
||||
* @note GPIO BRR 写入原子,无需临界区;可由 ISR 与主循环共同调用。
|
||||
* 开漏模式下 setSpiReady(true) 仅写 BRR 不驱动高,由外部上拉拉高。
|
||||
*/
|
||||
void setSpiReady(bool ready);
|
||||
|
||||
/** @brief 取消未完成的按铃(释放 SPIready、清 m_ringPending)。 */
|
||||
void cancelRing();
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief CAN 反馈主动推送一步(非阻塞,由主循环每次迭代调用)。
|
||||
*
|
||||
* GD32 APP 主动循环推送 3 通道 × 2 页 CAN 反馈帧到 ESP32,替代 ESP32
|
||||
* 主动轮询。每次调用推进一帧:若上一帧尚未被 ESP32 读走则立即返回,
|
||||
* 否则构建当前 (channel, page) 的反馈 payload 并 requestEsp32Read() 推送,
|
||||
* 然后推进到下一页/下一通道。
|
||||
*/
|
||||
void canFeedbackPushStep();
|
||||
|
||||
/** @brief 全局 SPI 命令总线实例。 */
|
||||
extern AppSpiCommand AppSpiCommandBus;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ constexpr const char *APP_BANNER_TEXT = "GD32E508 DCU task scheduler start";
|
||||
constexpr uint32_t APP_FW_VERSION_MAJOR = 1U;
|
||||
constexpr uint32_t APP_FW_VERSION_MINOR = 2U;
|
||||
constexpr uint32_t APP_FW_VERSION_PATCH = 3U;
|
||||
constexpr uint32_t APP_FW_VERSION_BUILD = 1U;
|
||||
constexpr uint32_t APP_FW_VERSION_BUILD = 2U;
|
||||
|
||||
|
||||
/** @brief USART0 默认波特率。 */
|
||||
@@ -109,7 +109,7 @@ constexpr uint32_t CONFIG_CAN_DATA_BS2 = CAN_BT_BS2_1TQ;
|
||||
#define CONFIG_CAN_FD_BRS_ENABLE 1U
|
||||
|
||||
/** @brief CAN0 通道使能开关:true=启用初始化/收发/自测,false=完全禁用。 */
|
||||
constexpr bool CONFIG_CAN_CH0_ENABLE = true;
|
||||
constexpr bool CONFIG_CAN_CH0_ENABLE = false;
|
||||
/** @brief CAN1 通道使能开关。 */
|
||||
constexpr bool CONFIG_CAN_CH1_ENABLE = false;
|
||||
/** @brief CAN2 通道使能开关。 */
|
||||
|
||||
@@ -89,6 +89,7 @@ static bool requestMagicValid(const uint8_t *data, uint8_t length)
|
||||
|
||||
/** @brief 已写入 OTA 标志并等待软复位的挂起标志。 */
|
||||
volatile bool OtaBootRequest::s_rebootPending = false;
|
||||
volatile bool OtaBootRequest::s_bootRequestPending = false;
|
||||
|
||||
/**
|
||||
* @brief 初始化进入 Bootloader 请求模块的运行态。
|
||||
@@ -96,6 +97,7 @@ volatile bool OtaBootRequest::s_rebootPending = false;
|
||||
void OtaBootRequest::init()
|
||||
{
|
||||
s_rebootPending = false;
|
||||
s_bootRequestPending = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,6 +119,16 @@ bool OtaBootRequest::requestEnterBoot()
|
||||
return ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 异步请求进入 Bootloader(ISR 安全)。
|
||||
* @note 仅设置 pending 标志,Flash 操作和复位由 process() 在主循环执行。
|
||||
* 用于 SPI EXTI ISR 内的 0xB0 命令处理,避免 Flash 擦除阻塞 SPI 中断。
|
||||
*/
|
||||
void OtaBootRequest::requestEnterBootAsync()
|
||||
{
|
||||
s_bootRequestPending = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 处理备用 CAN 入口的进入 Bootloader 请求。
|
||||
* @note 当前主路径是 SPI 命令;CAN 入口只负责识别魔术字并触发同一套标志写入逻辑。
|
||||
@@ -138,19 +150,34 @@ bool OtaBootRequest::handleCanFrame(uint8_t channel, uint32_t id, const uint8_t
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 在主循环任务中执行短延迟软复位。
|
||||
* @note 不在 SPI/CAN 中断里直接复位,避免应答还没发出就重启。
|
||||
* 这里使用毫秒级短延时,而不是 1s 心跳计数;否则一次 SPI 请求
|
||||
* 实际会拖到 2~3 秒后才复位,ESP32 第二次 OTA 时很容易在 APP 还没
|
||||
* 真正重启进入 BOOT 之前就发出 HANDSHAKE_REQ。
|
||||
* @brief 在主循环任务中执行异步 Flash 操作和短延迟软复位。
|
||||
* @note 不在 SPI/CAN 中断里直接复位,避免应答还没有发出就重启。
|
||||
* 异步路径:SPI ISR 收到 0xB0 后只设标志,Flash 操作(可能擦页 20-30ms)
|
||||
* 在此主循环执行,避免阻塞 SPI 中断导致 ESP32 ACK 轮询丢字节。
|
||||
*/
|
||||
void OtaBootRequest::process()
|
||||
{
|
||||
/* 异步处理 0xB0 请求:在主循环执行 Flash 操作(20-30ms),不阻塞 SPI ISR */
|
||||
if(s_bootRequestPending) {
|
||||
s_bootRequestPending = false;
|
||||
const bool ok = requestEnterBoot();
|
||||
if(ok) {
|
||||
s_rebootPending = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!s_rebootPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
s_rebootPending = false;
|
||||
|
||||
/* 方案A: 拉低 PB6 通知 ESP32 "即将复位"。
|
||||
* ESP32 通过 LOW→HIGH 序列识别 Bootloader 就绪:
|
||||
* APP 拉低 → 复位(μs级HIGH) → Bootloader极早拉低 → init完成释放HIGH
|
||||
* 20ms 延时确保 ESP32 5ms 轮询能捕获到 LOW 信号后再复位。 */
|
||||
gpio_bit_reset(CONFIG_SPI_READY_PORT, CONFIG_SPI_READY_PIN);
|
||||
|
||||
SysTickTimer::delayMs(kRebootDelayMs);
|
||||
__disable_irq();
|
||||
NVIC_SystemReset();
|
||||
|
||||
@@ -28,14 +28,27 @@ public:
|
||||
* @brief 设置 Bootloader OTA 等待标志并安排软复位。
|
||||
* @retval true 标志写入成功,稍后会复位进入 Bootloader。
|
||||
* @retval false Flash 标志写入失败。
|
||||
* @note 此函数会执行 Flash 操作(可能擦页 20-30ms),不可在 ISR 内调用。
|
||||
*/
|
||||
static bool requestEnterBoot();
|
||||
|
||||
/**
|
||||
* @brief 异步请求进入 Bootloader(ISR 安全)。
|
||||
* @note 仅设置 pending 标志,Flash 操作和复位由 process() 在主循环执行。
|
||||
* 用于 SPI EXTI ISR 内的 0xB0 命令处理,避免 Flash 擦除阻塞 SPI 中断。
|
||||
*/
|
||||
static void requestEnterBootAsync();
|
||||
|
||||
/** @brief 若已确认进入 BOOT 请求,则延迟片刻后复位。 */
|
||||
static void process();
|
||||
|
||||
/** @brief 查询是否已收到进入 Bootloader 请求(等待复位中)。
|
||||
* 供主循环在复位前停止主动按铃等可能干扰 OTA 的操作。 */
|
||||
static bool isRebootPending() { return s_rebootPending; }
|
||||
|
||||
private:
|
||||
static volatile bool s_rebootPending;
|
||||
static volatile bool s_bootRequestPending; ///< 0xB0 异步请求标志,process() 中执行 Flash 操作
|
||||
};
|
||||
|
||||
#endif /* SYS_OTA_BOOT_REQUEST_H */
|
||||
|
||||
@@ -35,6 +35,13 @@ public:
|
||||
*/
|
||||
static void onInterrupt();
|
||||
|
||||
/**
|
||||
* @brief 获取当前系统毫秒节拍数。
|
||||
* @return 自启动以来的毫秒数(32 位回绕)。
|
||||
* @note 用于超时判定(如 SPI 按铃看护)。读取 volatile 原子。
|
||||
*/
|
||||
static uint32_t getMs() { return s_ms; }
|
||||
|
||||
private:
|
||||
static volatile uint32_t s_ms;
|
||||
};
|
||||
|
||||
@@ -331,7 +331,7 @@ void TaskRunner::taskDiagnosticPrint()
|
||||
*/
|
||||
void TaskRunner::taskSpiCmdProcess()
|
||||
{
|
||||
AppSpiCommandBus.processPendingCommand();
|
||||
AppSpiCommandBus.poll(); // 命令处理已移入 ISR,此处仅日志打印+按铃看护
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -378,6 +378,11 @@ void TaskRunner::runPending()
|
||||
{
|
||||
Watchdog::feed();
|
||||
|
||||
/* CAN 反馈主动推送:GD32 APP 循环推送 3 通道 × 2 页反馈帧到 ESP32,
|
||||
* 替代 ESP32 主动轮询。非阻塞:上一帧未读走时立即返回。
|
||||
* 每次主循环迭代调用一次,ESP32 读走一帧后 CS 释放中断唤醒主循环继续推送。 */
|
||||
canFeedbackPushStep();
|
||||
|
||||
/* 周期路径( SYNC0 ISR preempt=0 )用 s_periodicSeq 序列号通知主循环,
|
||||
* 不再走 task_flags,避免 BASEPRI 临界区与 preempt=0 的 RMW 竞态。
|
||||
* 优先于 takePending() 处理,确保周期数据及时消费。
|
||||
|
||||
Reference in New Issue
Block a user