添加ESP32-C3 GPIO驱动组件以支持SPI Ready握手机制,新增 SPI_READY_GPIO(10)引脚配置用于监测GD32 PB6就绪状态,避免SPI 通信时从机未准备好的丢帧问题。
792 lines
30 KiB
C++
792 lines
30 KiB
C++
/*
|
||
* spi_master.cpp - SPI Master 驱动实现
|
||
* T1.1 - T1.4
|
||
* ESP-IDF 6.0 适配版本
|
||
* CANFD 3通道发送接口
|
||
*
|
||
* 所有功能封装为 SpiMaster 类方法。
|
||
*/
|
||
#include <string.h>
|
||
#include <stdio.h>
|
||
#include "freertos/FreeRTOS.h"
|
||
#include "freertos/task.h"
|
||
#include "freertos/semphr.h"
|
||
#include "driver/spi_master.h"
|
||
#include "driver/gpio.h"
|
||
#include "soc/spi_struct.h"
|
||
#include "esp_err.h"
|
||
#include "esp_log.h"
|
||
|
||
#include "spi_master.h"
|
||
#include "monitor/debug_log.h"
|
||
|
||
|
||
static constexpr size_t kSpiTransferMaxBytes = 256U;
|
||
|
||
// ============================================================
|
||
// 单例
|
||
// ============================================================
|
||
/**
|
||
* @brief 获取 SpiMaster 单例实例(单例模式)。
|
||
* @return SpiMaster 单例引用。
|
||
*/
|
||
SpiMaster& SpiMaster::instance()
|
||
{
|
||
static SpiMaster inst;
|
||
return inst;
|
||
}
|
||
|
||
/**
|
||
* @brief 等待 GD32 PB6 SPI Ready 握手引脚拉高(就绪)。
|
||
* GD32 从机在 CS 断言瞬间拉低 PB6(忙),在响应帧就绪后拉高(就绪)。
|
||
* 主机每次发起 CS 前调用,确保从机已准备好接收下一笔事务。
|
||
* @param timeout_ms 超时时间(毫秒),0 表示不等待直接返回。
|
||
* @return ESP_OK 表示从机就绪;ESP_ERR_TIMEOUT 表示超时(退化为继续发起)。
|
||
*/
|
||
esp_err_t SpiMaster::waitSpiReady(uint32_t timeout_ms)
|
||
{
|
||
if (timeout_ms == 0U) {
|
||
return ESP_OK;
|
||
}
|
||
|
||
const TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
|
||
for (;;) {
|
||
int level = gpio_get_level((gpio_num_t)SPI_READY_GPIO);
|
||
if (level == 1) {
|
||
/* 从机就绪:复位超时计数器,下次掉线时首次超时又能立即告警 */
|
||
m_spiReadyTimeoutCount = 0U;
|
||
return ESP_OK;
|
||
}
|
||
if (xTaskGetTickCount() >= deadline) {
|
||
/* 限频:首次超时立即告警,之后每 kSpiReadyWarnEvery 次打印一条,
|
||
* 避免 GD32 掉线/卡死时高频日志刷屏。计数器无锁:仅在持锁的
|
||
* transferClocked 路径调用,单任务访问,无需保护。 */
|
||
++m_spiReadyTimeoutCount;
|
||
if (m_spiReadyTimeoutCount == 1U ||
|
||
(m_spiReadyTimeoutCount % kSpiReadyWarnEvery) == 0U) {
|
||
DEBUG_WARN("SPI", "waitSpiReady 超时: GPIO%d 仍为 LOW(从机忙), timeout=%ums, 累计=%lu",
|
||
SPI_READY_GPIO, (unsigned)timeout_ms,
|
||
(unsigned long)m_spiReadyTimeoutCount);
|
||
}
|
||
return ESP_ERR_TIMEOUT;
|
||
}
|
||
vTaskDelay(pdMS_TO_TICKS(1));
|
||
}
|
||
}
|
||
|
||
// ============================================================
|
||
// 内部 SPI 传输
|
||
// ============================================================
|
||
/**
|
||
* @brief 执行底层 SPI 时钟同步传输。
|
||
* @param tx_buf 发送数据缓冲区,可为 NULL。
|
||
* @param tx_len 发送数据长度。
|
||
* @param rx_buf 接收数据缓冲区,可为 NULL。
|
||
* @param rx_len 接收数据长度。
|
||
* @param timeout_ms 超时时间(毫秒),0 表示无限等待。
|
||
* @return ESP_OK 表示成功,否则返回错误码。
|
||
*/
|
||
esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len,
|
||
uint8_t *rx_buf, size_t rx_len,
|
||
uint32_t timeout_ms)
|
||
{
|
||
if (m_spiDevice == NULL) {
|
||
return ESP_ERR_INVALID_STATE;
|
||
}
|
||
|
||
if ((tx_len > 0U && tx_buf == NULL) || (rx_len > 0U && rx_buf == NULL)) {
|
||
DEBUG_WARN("SPI", "transferClocked INVALID_ARG: tx_len=%u tx_buf=%p rx_len=%u rx_buf=%p dev=%p",
|
||
(unsigned)tx_len, tx_buf, (unsigned)rx_len, rx_buf, m_spiDevice);
|
||
return ESP_ERR_INVALID_ARG;
|
||
}
|
||
|
||
const size_t total_len = (tx_len > rx_len) ? tx_len : rx_len;
|
||
if (total_len == 0U) {
|
||
return ESP_OK;
|
||
}
|
||
if (total_len > kSpiTransferMaxBytes) {
|
||
return ESP_ERR_INVALID_SIZE;
|
||
}
|
||
|
||
/* CS 断言前等待 GD32 PB6 拉高(就绪):从机在上一笔 onCsRelease/
|
||
* processCommand 末尾才会拉高 PB6,提前发起 CS 会因从机未装好
|
||
* DMA/响应帧而丢失字节。超时退化为继续发起,保留原有容错路径。 */
|
||
(void)waitSpiReady(SPI_READY_TIMEOUT_MS);
|
||
|
||
uint8_t tx_staging[kSpiTransferMaxBytes];
|
||
uint8_t rx_staging[kSpiTransferMaxBytes];
|
||
const uint8_t *tx_ptr = tx_buf;
|
||
uint8_t *rx_ptr = rx_buf;
|
||
|
||
if (tx_len < total_len) {
|
||
memset(tx_staging, 0xFF, total_len);
|
||
if (tx_buf != NULL && tx_len > 0U) {
|
||
memcpy(tx_staging, tx_buf, tx_len);
|
||
}
|
||
tx_ptr = tx_staging;
|
||
}
|
||
|
||
if (rx_len > 0U && rx_len < total_len) {
|
||
rx_ptr = rx_staging;
|
||
}
|
||
|
||
spi_transaction_t t = {};
|
||
t.length = total_len * 8U;
|
||
t.rxlength = (rx_len > 0U) ? (total_len * 8U) : 0U;
|
||
t.tx_buffer = tx_ptr;
|
||
t.rx_buffer = (rx_len > 0U) ? rx_ptr : NULL;
|
||
|
||
TickType_t ticks = (timeout_ms == 0U) ? portMAX_DELAY : pdMS_TO_TICKS(timeout_ms);
|
||
esp_err_t ret = spi_device_queue_trans(m_spiDevice, &t, ticks);
|
||
if (ret != ESP_OK) {
|
||
DEBUG_WARN("SPI", "spi_device_queue_trans fail: %s len=%u",
|
||
esp_err_to_name(ret), (unsigned)(total_len * 8U));
|
||
return ret;
|
||
}
|
||
|
||
spi_transaction_t *ret_trans = NULL;
|
||
ret = spi_device_get_trans_result(m_spiDevice, &ret_trans, ticks);
|
||
(void)ret_trans;
|
||
if (ret != ESP_OK) {
|
||
DEBUG_WARN("SPI", "spi_device_get_trans_result timeout/fail: %s len=%u tx=%p rx=%p",
|
||
esp_err_to_name(ret), (unsigned)(total_len * 8U), t.tx_buffer, t.rx_buffer);
|
||
return ret;
|
||
}
|
||
|
||
if (rx_len > 0U && rx_ptr != rx_buf) {
|
||
memcpy(rx_buf, rx_ptr, rx_len);
|
||
}
|
||
return ESP_OK;
|
||
}
|
||
|
||
/**
|
||
* @brief 执行 SPI 请求-响应组合传输。
|
||
* @param cmd 命令字节(0 表示无命令)。
|
||
* @param payload 负载数据缓冲区。
|
||
* @param payload_len 负载数据长度。
|
||
* @param rx_buf 接收数据缓冲区。
|
||
* @param rx_len 期望接收的数据长度。
|
||
* @param timeout_ms 超时时间(毫秒),0 表示无限等待。
|
||
* @return ESP_OK 表示成功,否则返回错误码。
|
||
*/
|
||
esp_err_t SpiMaster::transferRequestResponse(uint8_t cmd,
|
||
const uint8_t *payload, size_t payload_len,
|
||
uint8_t *rx_buf, size_t rx_len,
|
||
uint32_t timeout_ms)
|
||
{
|
||
if ((payload_len > 0U && payload == NULL) || (rx_len > 0U && rx_buf == NULL)) {
|
||
return ESP_ERR_INVALID_ARG;
|
||
}
|
||
|
||
const size_t cmd_len = (cmd != 0U) ? 1U : 0U;
|
||
const size_t total_len = cmd_len + payload_len + rx_len;
|
||
if (total_len == 0U) {
|
||
return ESP_OK;
|
||
}
|
||
if (total_len > kSpiTransferMaxBytes) {
|
||
return ESP_ERR_INVALID_SIZE;
|
||
}
|
||
|
||
uint8_t tx_staging[kSpiTransferMaxBytes];
|
||
uint8_t rx_staging[kSpiTransferMaxBytes];
|
||
size_t tx_pos = 0U;
|
||
|
||
if (cmd_len > 0U) {
|
||
tx_staging[tx_pos++] = cmd;
|
||
}
|
||
if (payload_len > 0U) {
|
||
memcpy(&tx_staging[tx_pos], payload, payload_len);
|
||
tx_pos += payload_len;
|
||
}
|
||
if (tx_pos < total_len) {
|
||
memset(&tx_staging[tx_pos], 0xFF, total_len - tx_pos);
|
||
}
|
||
|
||
esp_err_t ret = transferClocked(tx_staging, total_len,
|
||
(rx_len > 0U) ? rx_staging : NULL,
|
||
(rx_len > 0U) ? total_len : 0U,
|
||
timeout_ms);
|
||
if (ret == ESP_OK && rx_len > 0U) {
|
||
memcpy(rx_buf, &rx_staging[cmd_len + payload_len], rx_len);
|
||
}
|
||
return ret;
|
||
}
|
||
|
||
// ============================================================
|
||
// OTA 帧工具
|
||
// ============================================================
|
||
/**
|
||
* @brief 计算 OTA 帧的 8 位 CRC 校验和。
|
||
* @param data 数据缓冲区。
|
||
* @param len 数据长度。
|
||
* @return 计算得到的 CRC 值。
|
||
*/
|
||
uint8_t SpiMaster::otaCrc8(const uint8_t *data, size_t len)
|
||
{
|
||
uint8_t crc = 0x00;
|
||
for (size_t i = 0; i < len; ++i) {
|
||
crc ^= data[i];
|
||
}
|
||
return crc;
|
||
}
|
||
|
||
/**
|
||
* @brief 构建 OTA 协议帧。
|
||
* @param type 帧类型。
|
||
* @param payload 负载数据缓冲区。
|
||
* @param payload_len 负载数据长度。
|
||
* @param out 输出帧缓冲区。
|
||
* @param out_len 输出实际帧长度。
|
||
* @return true 表示构建成功,false 表示失败。
|
||
*/
|
||
bool SpiMaster::otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len,
|
||
uint8_t *out, size_t *out_len)
|
||
{
|
||
if (!out || !out_len) return false;
|
||
size_t total = 5 + payload_len;
|
||
if (total > SPI_OTA_FRAME_MAX) return false;
|
||
out[0] = SPI_OTA_SOF;
|
||
out[1] = type;
|
||
out[2] = (uint8_t)(payload_len & 0xFF);
|
||
out[3] = (uint8_t)((payload_len >> 8) & 0xFF);
|
||
if (payload && payload_len > 0) {
|
||
memcpy(&out[4], payload, payload_len);
|
||
}
|
||
out[4 + payload_len] = otaCrc8(out, 4 + payload_len);
|
||
*out_len = total;
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* @brief 解析 OTA 协议帧。
|
||
* @param frame 帧数据缓冲区。
|
||
* @param frame_len 帧数据长度。
|
||
* @param type 输出解析得到的帧类型。
|
||
* @param payload 输出指向负载数据的指针。
|
||
* @param payload_len 输出负载数据长度。
|
||
* @return true 表示解析成功,false 表示失败。
|
||
*/
|
||
bool SpiMaster::otaParseFrame(const uint8_t *frame, size_t frame_len,
|
||
uint8_t *type, const uint8_t **payload, size_t *payload_len)
|
||
{
|
||
if (!frame || frame_len < 5) return false;
|
||
if (frame[0] != SPI_OTA_SOF) return false;
|
||
|
||
size_t plen = (size_t)frame[2] | ((size_t)frame[3] << 8);
|
||
size_t total = 5 + plen;
|
||
if (frame_len < total) return false;
|
||
|
||
uint8_t expected_crc = otaCrc8(frame, 4 + plen);
|
||
if (frame[4 + plen] != expected_crc) return false;
|
||
|
||
if (type) *type = frame[1];
|
||
if (payload) *payload = (plen > 0) ? &frame[4] : nullptr;
|
||
if (payload_len) *payload_len = plen;
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* @brief 在数据缓冲区中搜索并解析 OTA 帧。
|
||
* @param buf 数据缓冲区。
|
||
* @param buf_len 数据缓冲区长度。
|
||
* @param type 输出帧类型。
|
||
* @param payload 输出指向负载数据的指针。
|
||
* @param payload_len 输出负载数据长度。
|
||
* @param frame_offset 输出帧在缓冲区中的偏移位置。
|
||
* @return true 表示找到并成功解析,false 表示未找到。
|
||
*/
|
||
bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len,
|
||
uint8_t *type, const uint8_t **payload, size_t *payload_len,
|
||
size_t *frame_offset)
|
||
{
|
||
for (size_t i = 0; i + 5 <= buf_len; ++i) {
|
||
if (buf[i] != SPI_OTA_SOF) continue;
|
||
|
||
size_t plen = (size_t)buf[i + 2] | ((size_t)buf[i + 3] << 8);
|
||
size_t total = 5 + plen;
|
||
if (i + total > buf_len) continue;
|
||
|
||
uint8_t expected_crc = otaCrc8(&buf[i], 4 + plen);
|
||
if (buf[i + 4 + plen] != expected_crc) {
|
||
DEBUG_WARN("SPI", "CRC mismatch at offset %u: plen=%u expected=0x%02X actual=0x%02X",
|
||
(unsigned)i, (unsigned)plen, expected_crc, buf[i + 4 + plen]);
|
||
continue;
|
||
}
|
||
|
||
if (type) *type = buf[i + 1];
|
||
if (payload) *payload = (plen > 0) ? &buf[i + 4] : nullptr;
|
||
if (payload_len) *payload_len = plen;
|
||
if (frame_offset) *frame_offset = i;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
// ============================================================
|
||
// 初始化
|
||
// ============================================================
|
||
/**
|
||
* @brief 初始化 SPI2 总线与目标从设备。
|
||
* @return ESP_OK 表示成功,否则返回错误码。
|
||
*/
|
||
esp_err_t SpiMaster::init()
|
||
{
|
||
// ★ 修复:先创建 mutex,再使用 LockGuard。
|
||
// 旧代码在 LockGuard 构造时 m_mutex 仍为 NULL,
|
||
// 虽然 LockGuard 会跳过 NULL take/give,但在极端情况下
|
||
// 其他任务可能在 mutex 创建后立即尝试 take,产生竞态。
|
||
if (!m_mutex) {
|
||
m_mutex = xSemaphoreCreateMutex();
|
||
if (m_mutex == NULL) {
|
||
DEBUG_ERR("SPI", "SPI 互斥锁创建失败");
|
||
return ESP_ERR_NO_MEM;
|
||
}
|
||
}
|
||
LockGuard lock(m_mutex);
|
||
|
||
DEBUG_INFO("SPI", "=== 初始化 SPI2 总线 ===");
|
||
|
||
spi_bus_config_t bus_cfg = {};
|
||
bus_cfg.miso_io_num = 7;
|
||
bus_cfg.mosi_io_num = 5;
|
||
bus_cfg.sclk_io_num = 6;
|
||
bus_cfg.quadwp_io_num = -1;
|
||
bus_cfg.quadhd_io_num = -1;
|
||
bus_cfg.max_transfer_sz = kSpiTransferMaxBytes;
|
||
|
||
esp_err_t ret = spi_bus_initialize(SPI2_HOST, &bus_cfg, SPI_DMA_CH_AUTO);
|
||
if (ret != ESP_OK) {
|
||
DEBUG_ERR("SPI", "SPI 总线初始化失败: %s", esp_err_to_name(ret));
|
||
return ret;
|
||
}
|
||
|
||
spi_device_interface_config_t dev_cfg = {};
|
||
dev_cfg.clock_speed_hz = SPI_CLOCK_HZ;
|
||
dev_cfg.mode = SPI_MODE;
|
||
dev_cfg.spics_io_num = SPI_MASTER_CS_GPIO;
|
||
dev_cfg.queue_size = SPI_QUEUE_SIZE;
|
||
dev_cfg.command_bits = 0;
|
||
dev_cfg.address_bits = 0;
|
||
dev_cfg.dummy_bits = 0;
|
||
dev_cfg.cs_ena_posttrans = 10;
|
||
dev_cfg.cs_ena_pretrans = 20;
|
||
|
||
ret = spi_bus_add_device(SPI2_HOST, &dev_cfg, &m_spiDevice);
|
||
if (ret != ESP_OK) {
|
||
DEBUG_ERR("SPI", "SPI 设备添加失败: %s", esp_err_to_name(ret));
|
||
return ret;
|
||
}
|
||
|
||
DEBUG_INFO("SPI", "SPI2 总线初始化完成: MOSI=GPIO5, MISO=GPIO7, SCLK=GPIO6, CS=GPIO%d, CLK=%d Hz",
|
||
SPI_MASTER_CS_GPIO, SPI_CLOCK_HZ);
|
||
|
||
/* 配置 SPI Ready 握手输入引脚(GPIO10),监测 GD32 PB6 电平。
|
||
* GD32 在 CS 下降沿拉低 PB6(忙),在 onCsRelease/processCommand 末尾拉高(就绪)。
|
||
* 主机每次发起 CS 前须等待 PB6=HIGH,避免从机未就绪时抢跑丢帧。 */
|
||
gpio_config_t ready_cfg = {};
|
||
ready_cfg.pin_bit_mask = (1ULL << SPI_READY_GPIO);
|
||
ready_cfg.mode = GPIO_MODE_INPUT;
|
||
ready_cfg.pull_up_en = GPIO_PULLUP_ENABLE;
|
||
ready_cfg.pull_down_en = GPIO_PULLDOWN_DISABLE;
|
||
ready_cfg.intr_type = GPIO_INTR_DISABLE;
|
||
esp_err_t gpio_ret = gpio_config(&ready_cfg);
|
||
if (gpio_ret != ESP_OK) {
|
||
DEBUG_WARN("SPI", "SPI Ready 握手引脚 GPIO%d 配置失败: %s", SPI_READY_GPIO, esp_err_to_name(gpio_ret));
|
||
/* 非致命:握手失败时退化为不等待,保持原有行为 */
|
||
} else {
|
||
DEBUG_INFO("SPI", "SPI Ready 握手输入已配置: GPIO%d (监测 GD32 PB6)", SPI_READY_GPIO);
|
||
}
|
||
|
||
return ESP_OK;
|
||
}
|
||
|
||
// ============================================================
|
||
// 公开 SPI 传输接口
|
||
// ============================================================
|
||
/**
|
||
* @brief 执行一次同步 SPI 事务,先发送请求,再读取响应字节。
|
||
* @param cmd 命令字节。
|
||
* @param payload 负载数据缓冲区。
|
||
* @param payload_len 负载数据长度。
|
||
* @param recv_buf 接收数据缓冲区。
|
||
* @param recv_len 接收数据长度。
|
||
* @param timeout_ms 超时时间(毫秒)。
|
||
* @return ESP_OK 表示成功,否则返回错误码。
|
||
*/
|
||
esp_err_t SpiMaster::syncTransfer(uint8_t cmd, const uint8_t *payload,
|
||
size_t payload_len, uint8_t *recv_buf,
|
||
size_t recv_len, uint32_t timeout_ms)
|
||
{
|
||
LockGuard lock(m_mutex);
|
||
return transferRequestResponse(cmd, payload, payload_len, recv_buf, recv_len, timeout_ms);
|
||
}
|
||
|
||
/**
|
||
* @brief 仅发送 SPI 请求数据,不关心返回内容。
|
||
* @param cmd 命令字节。
|
||
* @param payload 负载数据缓冲区。
|
||
* @param len 负载数据长度。
|
||
* @return ESP_OK 表示成功,否则返回错误码。
|
||
*/
|
||
esp_err_t SpiMaster::send(uint8_t cmd, const uint8_t *payload, size_t len)
|
||
{
|
||
LockGuard lock(m_mutex);
|
||
return transferRequestResponse(cmd, payload, len, NULL, 0, 0U);
|
||
}
|
||
|
||
/**
|
||
* @brief 仅接收 SPI 数据,必要时先发送命令字节。
|
||
* @param cmd 命令字节。
|
||
* @param recv_buf 接收数据缓冲区。
|
||
* @param len 接收数据长度。
|
||
* @param timeout_ms 超时时间(毫秒)。
|
||
* @return ESP_OK 表示成功,否则返回错误码。
|
||
*/
|
||
esp_err_t SpiMaster::recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms)
|
||
{
|
||
LockGuard lock(m_mutex);
|
||
return transferRequestResponse(cmd, NULL, 0, recv_buf, len, timeout_ms);
|
||
}
|
||
|
||
// ============================================================
|
||
// SPI OTA 协议
|
||
// ============================================================
|
||
/**
|
||
* @brief 按 GD32 SPI OTA 最小协议执行一次请求加响应读取。
|
||
* @param type 请求帧类型。
|
||
* @param payload 负载数据缓冲区。
|
||
* @param payload_len 负载数据长度。
|
||
* @param resp_type 输出响应类型。
|
||
* @param resp_payload 输出响应负载缓冲区。
|
||
* @param resp_max 响应负载缓冲区最大长度。
|
||
* @param resp_len 输出实际响应长度。
|
||
* @param timeout_ms 超时时间(毫秒)。
|
||
* @return ESP_OK 表示成功,否则返回错误码。
|
||
*/
|
||
esp_err_t SpiMaster::otaTransfer(uint8_t type, const uint8_t *payload, size_t payload_len,
|
||
uint8_t *resp_type, uint8_t *resp_payload,
|
||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
|
||
{
|
||
LockGuard lock(m_mutex);
|
||
uint8_t tx_frame[SPI_OTA_FRAME_MAX];
|
||
size_t tx_len = 0;
|
||
if (!otaBuildFrame(type, payload, payload_len, tx_frame, &tx_len)) {
|
||
return ESP_ERR_INVALID_ARG;
|
||
}
|
||
|
||
DEBUG_DBG("SPI", "OTA TX: type=0x%02X len=%u first4=%02X%02X%02X%02X",
|
||
type, (unsigned)tx_len, tx_frame[0], tx_frame[1], tx_frame[2], tx_frame[3]);
|
||
esp_err_t ret = transferClocked(tx_frame, tx_len, NULL, 0, timeout_ms);
|
||
if (ret != ESP_OK) {
|
||
DEBUG_WARN("SPI", "OTA 请求发送失败: %s", esp_err_to_name(ret));
|
||
return ret;
|
||
}
|
||
|
||
return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, timeout_ms);
|
||
}
|
||
|
||
/**
|
||
* @brief 仅执行 SPI OTA 响应读取事务,不重复发送请求帧。
|
||
* @param resp_type 输出响应类型。
|
||
* @param resp_payload 输出响应负载缓冲区。
|
||
* @param resp_max 响应负载缓冲区最大长度。
|
||
* @param resp_len 输出实际响应长度。
|
||
* @param timeout_ms 超时时间(毫秒)。
|
||
* @return ESP_OK 表示成功,否则返回错误码。
|
||
*/
|
||
esp_err_t SpiMaster::otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload,
|
||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
|
||
{
|
||
LockGuard lock(m_mutex);
|
||
return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, timeout_ms);
|
||
}
|
||
|
||
/**
|
||
* @brief 无锁版本的 OTA 响应轮询实现。
|
||
* @param resp_type 输出响应类型。
|
||
* @param resp_payload 输出响应负载缓冲区。
|
||
* @param resp_max 响应负载缓冲区最大长度。
|
||
* @param resp_len 输出实际响应长度。
|
||
* @param timeout_ms 超时时间(毫秒)。
|
||
* @return ESP_OK 表示成功,否则返回错误码。
|
||
*/
|
||
esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_payload,
|
||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
|
||
{
|
||
constexpr size_t kPollBytes = 160U;
|
||
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
|
||
|
||
while (xTaskGetTickCount() <= deadline) {
|
||
uint8_t rx_raw[kPollBytes];
|
||
uint8_t dummy[kPollBytes];
|
||
memset(dummy, 0xFF, sizeof(dummy));
|
||
|
||
/* 用剩余时间作为单次 SPI 事务超时,避免总等待远超 deadline */
|
||
TickType_t now = xTaskGetTickCount();
|
||
uint32_t remain_ms = (deadline > now) ? ((deadline - now) * portTICK_PERIOD_MS) : 0U;
|
||
if (remain_ms == 0U) break;
|
||
|
||
esp_err_t ret = transferClocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw), remain_ms);
|
||
if (ret != ESP_OK) {
|
||
DEBUG_WARN("SPI", "OTA 响应读取失败: %s", esp_err_to_name(ret));
|
||
vTaskDelay(pdMS_TO_TICKS(10));
|
||
continue;
|
||
}
|
||
|
||
uint8_t found_type = 0;
|
||
const uint8_t *found_payload = nullptr;
|
||
size_t found_len = 0;
|
||
size_t frame_offset = 0;
|
||
|
||
if (otaFindFrame(rx_raw, sizeof(rx_raw), &found_type, &found_payload, &found_len, &frame_offset)) {
|
||
/* 有效性检查:ACK/ERROR 帧必须有 payload,空 payload 的 ACK 是帧截断/错位,
|
||
* 继续轮询而非当作有效响应返回。 */
|
||
if ((found_type == SPI_OTA_RSP_ACK || found_type == SPI_OTA_RSP_ERROR) && found_len == 0) {
|
||
DEBUG_DBG("SPI", "OTA 响应帧截断: type=0x%02X len=0 offset=%u, 继续轮询",
|
||
found_type, (unsigned)frame_offset);
|
||
vTaskDelay(pdMS_TO_TICKS(5));
|
||
continue;
|
||
}
|
||
|
||
if (resp_type) *resp_type = found_type;
|
||
if (resp_payload && resp_max > 0) {
|
||
size_t copy_len = (found_len < resp_max) ? found_len : resp_max;
|
||
if (found_payload && copy_len > 0) {
|
||
memcpy(resp_payload, found_payload, copy_len);
|
||
}
|
||
}
|
||
if (resp_len) *resp_len = found_len;
|
||
|
||
DEBUG_DBG("SPI", "OTA 响应: type=0x%02X len=%u offset=%u",
|
||
found_type, (unsigned)found_len, (unsigned)frame_offset);
|
||
|
||
if (found_type == SPI_OTA_RSP_WAIT) {
|
||
vTaskDelay(pdMS_TO_TICKS(10));
|
||
continue;
|
||
}
|
||
return ESP_OK;
|
||
}
|
||
|
||
vTaskDelay(pdMS_TO_TICKS(10));
|
||
}
|
||
|
||
DEBUG_WARN("SPI", "OTA 响应超时: %lu ms", (unsigned long)timeout_ms);
|
||
return ESP_ERR_TIMEOUT;
|
||
}
|
||
|
||
/**
|
||
* @brief 通知 GD32 DCU_APP 设置 OTA 标志并软复位进入 Bootloader。
|
||
* @return ESP_OK 表示成功,否则返回错误码。
|
||
*/
|
||
esp_err_t SpiMaster::appRequestBootloader()
|
||
{
|
||
LockGuard lock(m_mutex);
|
||
DEBUG_INFO("SPI", "请求 GD32 APP 进入 Bootloader...");
|
||
|
||
constexpr uint32_t kMaxRetries = 3U;
|
||
constexpr uint32_t kPerRetryAckMs = 200U;
|
||
constexpr uint32_t kRetryGapMs = 50U;
|
||
|
||
for (uint32_t retry = 0U; retry < kMaxRetries; ++retry) {
|
||
if (retry > 0U) {
|
||
DEBUG_WARN("SPI", "Bootloader 请求重发 (%lu/%lu)", (unsigned long)retry, (unsigned long)kMaxRetries);
|
||
vTaskDelay(pdMS_TO_TICKS(kRetryGapMs));
|
||
}
|
||
|
||
uint8_t cmd = SPI_APP_ENTER_BOOT_CMD;
|
||
esp_err_t ret = transferClocked(&cmd, 1, NULL, 0);
|
||
if (ret != ESP_OK) {
|
||
DEBUG_WARN("SPI", "发送 Bootloader 请求失败: %s", esp_err_to_name(ret));
|
||
continue;
|
||
}
|
||
|
||
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(kPerRetryAckMs);
|
||
while (xTaskGetTickCount() <= deadline) {
|
||
uint8_t rx = 0;
|
||
uint8_t dummy = 0xFF;
|
||
ret = transferClocked(&dummy, 1, &rx, 1);
|
||
if (ret == ESP_OK && rx == SPI_APP_ENTER_BOOT_ACK_OK) {
|
||
DEBUG_INFO("SPI", "GD32 APP 已确认进入 Bootloader (retry=%lu)", (unsigned long)retry);
|
||
return ESP_OK;
|
||
}
|
||
vTaskDelay(pdMS_TO_TICKS(10));
|
||
}
|
||
}
|
||
|
||
DEBUG_WARN("SPI", "GD32 APP 进入 Bootloader 确认超时 (retries exhausted)");
|
||
return ESP_ERR_TIMEOUT;
|
||
}
|
||
|
||
/**
|
||
* @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。
|
||
* @param timeout_ms 超时时间(毫秒)。
|
||
* @param uptime_sec 输出设备运行时间(秒)。
|
||
* @param version 输出 GD32 版本信息(可选)。
|
||
* @return ESP_OK 表示成功,否则返回错误码。
|
||
*/
|
||
esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version)
|
||
{
|
||
LockGuard lock(m_mutex);
|
||
constexpr uint8_t kAppPingType = 0xA1U;
|
||
|
||
uint8_t tx_frame[8] = {0};
|
||
size_t tx_len = 0U;
|
||
if (!otaBuildFrame(kAppPingType, NULL, 0U, tx_frame, &tx_len)) {
|
||
return ESP_ERR_INVALID_ARG;
|
||
}
|
||
|
||
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
|
||
uint32_t attempt = 0U;
|
||
|
||
while (xTaskGetTickCount() <= deadline) {
|
||
++attempt;
|
||
|
||
/* 发送 PING 帧,填充至 16 字节:
|
||
* GD32 SPI 从机可能在 CS 断言时丢失首字节(EXTI/SPI 中断竞态),
|
||
* 填充确保即使首字节丢失,后续仍有完整 0xA5 帧头可供扫描定位。 */
|
||
uint8_t tx_padded[16];
|
||
memset(tx_padded, 0xFF, sizeof(tx_padded));
|
||
memcpy(tx_padded, tx_frame, tx_len); /* [0..4]=PING帧, [5..15]=0xFF */
|
||
|
||
esp_err_t err = transferClocked(tx_padded, sizeof(tx_padded), NULL, 0U);
|
||
if (err != ESP_OK) {
|
||
DEBUG_DBG("SPI", "APP PING 请求发送失败[%lu]: %s",
|
||
(unsigned long)attempt, esp_err_to_name(err));
|
||
vTaskDelay(pdMS_TO_TICKS(5));
|
||
continue;
|
||
}
|
||
|
||
/* 给 GD32 充足时间处理 CS 释放中断并准备响应帧 */
|
||
vTaskDelay(pdMS_TO_TICKS(10));
|
||
|
||
uint8_t rx_raw[48]; /* 增大缓冲区,吸收 SPI 字节偏移 */
|
||
uint8_t dummy[48];
|
||
memset(dummy, 0xFF, sizeof(dummy));
|
||
memset(rx_raw, 0xFF, sizeof(rx_raw));
|
||
|
||
err = transferClocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw));
|
||
if (err != ESP_OK) {
|
||
DEBUG_DBG("SPI", "APP PING 响应读取失败[%lu]: %s",
|
||
(unsigned long)attempt, esp_err_to_name(err));
|
||
vTaskDelay(pdMS_TO_TICKS(5));
|
||
continue;
|
||
}
|
||
|
||
bool found = false;
|
||
|
||
/* GD32 SPI 从机字节偏移不可预测,直接在 raw buffer 中
|
||
* 搜索完整的标签化 PING 响应模式:
|
||
* 0xA1 [0x55...] app_vers [0xAA...] boot_vers
|
||
* 找到 0xA1 后跳过重复字节,验证 0x55 和 0xAA 标签。
|
||
*/
|
||
for (size_t i = 0; i + 14 < sizeof(rx_raw); ++i) {
|
||
if (rx_raw[i] != kAppPingType) continue; /* 找 0xA1 */
|
||
size_t p = i + 1;
|
||
/* 跳过最多 1 个重复的 A1 字节(SPI 偏移最多产生 1 个重复) */
|
||
if (p < sizeof(rx_raw) && rx_raw[p] == kAppPingType) ++p;
|
||
if (p >= sizeof(rx_raw) || rx_raw[p] != 0x55U) continue;
|
||
/* 跳过最多 1 个重复的 0x55 */
|
||
if (p + 1 < sizeof(rx_raw) && rx_raw[p + 1] == 0x55U) ++p;
|
||
size_t app_start = p + 1; /* APP 版本起始 */
|
||
if (app_start + 4 > sizeof(rx_raw)) continue;
|
||
size_t bp = app_start + 4;
|
||
/* 跳过最多 1 个重复字节 */
|
||
if (bp < sizeof(rx_raw) && bp > 0 && rx_raw[bp] == rx_raw[bp - 1]) ++bp;
|
||
if (bp >= sizeof(rx_raw) || rx_raw[bp] != 0xAAU) continue;
|
||
/* 跳过最多 1 个重复的 0xAA */
|
||
if (bp + 1 < sizeof(rx_raw) && rx_raw[bp + 1] == 0xAAU) ++bp;
|
||
size_t boot_start = bp + 1;
|
||
if (boot_start + 4 > sizeof(rx_raw)) continue;
|
||
|
||
/* 模式匹配成功!提取版本数据 */
|
||
found = true;
|
||
|
||
if (version) {
|
||
memset(version, 0, sizeof(gd32_version_t));
|
||
version->app_major = rx_raw[app_start];
|
||
version->app_minor = rx_raw[app_start + 1];
|
||
version->app_patch = rx_raw[app_start + 2];
|
||
version->app_build = rx_raw[app_start + 3];
|
||
version->boot_major = rx_raw[boot_start];
|
||
version->boot_minor = rx_raw[boot_start + 1];
|
||
version->boot_patch = rx_raw[boot_start + 2];
|
||
version->boot_build = rx_raw[boot_start + 3];
|
||
}
|
||
if (uptime_sec) {
|
||
size_t ut = boot_start + 4;
|
||
if (ut + 3 < sizeof(rx_raw)) {
|
||
*uptime_sec = (uint32_t)rx_raw[ut]
|
||
| ((uint32_t)rx_raw[ut + 1] << 8)
|
||
| ((uint32_t)rx_raw[ut + 2] << 16)
|
||
| ((uint32_t)rx_raw[ut + 3] << 24);
|
||
} else {
|
||
*uptime_sec = 0;
|
||
}
|
||
}
|
||
DEBUG_WARN("SPI", "APP PING 标签匹配: A1@%u APP@%u BOOT@%u",
|
||
(unsigned)i, (unsigned)app_start, (unsigned)boot_start);
|
||
break;
|
||
}
|
||
|
||
if (found) {
|
||
DEBUG_INFO("SPI", "APP PING 成功: uptime=%lu sec, attempt=%lu",
|
||
uptime_sec ? (unsigned long)*uptime_sec : 0UL,
|
||
(unsigned long)attempt);
|
||
return ESP_OK;
|
||
}
|
||
|
||
DEBUG_WARN("SPI", "APP PING 无匹配[%lu]: first8=%02X%02X%02X%02X%02X%02X%02X%02X",
|
||
(unsigned long)attempt,
|
||
rx_raw[0], rx_raw[1], rx_raw[2], rx_raw[3],
|
||
rx_raw[4], rx_raw[5], rx_raw[6], rx_raw[7]);
|
||
|
||
vTaskDelay(pdMS_TO_TICKS(5));
|
||
}
|
||
|
||
DEBUG_DBG("SPI", "APP PING 超时: timeout=%ums", (unsigned)timeout_ms);
|
||
return ESP_ERR_TIMEOUT;
|
||
}
|
||
|
||
/**
|
||
* @brief 执行带重试的 SPI 同步传输。
|
||
* @param cmd 命令字节。
|
||
* @param payload 负载数据缓冲区。
|
||
* @param payload_len 负载数据长度。
|
||
* @param recv_buf 接收数据缓冲区。
|
||
* @param recv_len 接收数据长度。
|
||
* @param max_retries 最大重试次数。
|
||
* @return ESP_OK 表示成功,否则返回错误码。
|
||
*/
|
||
esp_err_t SpiMaster::transferWithRetry(uint8_t cmd, const uint8_t *payload,
|
||
size_t payload_len, uint8_t *recv_buf,
|
||
size_t recv_len, int max_retries)
|
||
{
|
||
LockGuard lock(m_mutex);
|
||
esp_err_t ret;
|
||
int retry_count = 0;
|
||
|
||
DEBUG_INFO("SPI", "SPI 传输 (带重试): cmd=0x%02X, 最大重试=%d", cmd, max_retries);
|
||
|
||
while (retry_count < max_retries) {
|
||
ret = transferRequestResponse(cmd, payload, payload_len,
|
||
recv_buf, recv_len, 3000);
|
||
if (ret == ESP_OK) {
|
||
if (retry_count > 0) {
|
||
DEBUG_INFO("SPI", "SPI 传输成功 (重试 %d 次后成功)", retry_count);
|
||
}
|
||
return ESP_OK;
|
||
}
|
||
|
||
retry_count++;
|
||
DEBUG_WARN("SPI", "SPI 重试 %d/%d, cmd=0x%02X, err=%s",
|
||
retry_count, max_retries, cmd, esp_err_to_name(ret));
|
||
vTaskDelay(pdMS_TO_TICKS(100));
|
||
}
|
||
|
||
DEBUG_ERR("SPI", "SPI 传输 %d 次重试后失败, cmd=0x%02X", max_retries, cmd);
|
||
return ESP_ERR_TIMEOUT;
|
||
}
|
||
|
||
// CANFD 接口已迁移至 transport/canfd_bridge.cpp(M4 解耦)
|
||
|