feat(main): 将全局函数重构为单例类模式
- 将 debug_log 模块重构为 DebugLog 单例类,封装 UART、UDP 和环形缓冲区日志输出 - 将 health_monitor 模块重构为 HealthMonitor 单例类,统一健康状态管理和监控逻辑 - 更新 main.cpp 中的初始化调用,使用单例实例替代原有全局函数 - 添加健康监控任务作为第四个 FreeRTOS 任务,增强系统监控能力 - 更新固件版本从 0.2.11 到 0.3.0,反映架构重大变更
This commit is contained in:
+16
-16
@@ -1,6 +1,8 @@
|
||||
/*
|
||||
/*
|
||||
* ESP32-C3FH4 OTA 中继 - 程序入口
|
||||
* 三个 FreeRTOS 任务:OTA Relay / WiFi Server / BLE GATT
|
||||
* 四个 FreeRTOS 任务:健康监控 / OTA Relay / WiFi Server / BLE GATT
|
||||
*
|
||||
* 使用各模块的单例类进行初始化和任务创建。
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
@@ -19,7 +21,7 @@
|
||||
#include "monitor/debug_log.h"
|
||||
|
||||
/**
|
||||
* @brief 程序主入口,初始化系统组件并创建各 FreeRTOS 任务。
|
||||
* @brief 程序主入口,初始化系统组件并创建各 FreeRTOS 任务。
|
||||
*/
|
||||
extern "C" void app_main(void)
|
||||
{
|
||||
@@ -31,13 +33,12 @@ extern "C" void app_main(void)
|
||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||
|
||||
// 初始化调试日志(UART0 先就绪,UDP socket 延迟到首次发送时创建)
|
||||
debug_log_init();
|
||||
DebugLog::instance().init();
|
||||
|
||||
// ★ WiFi 提前初始化(调度器启动前)
|
||||
// 包括:NVS → netif 创建 → event handler 注册 → WiFi driver init → 配置 → connect
|
||||
bool wifi_sta_ok = wifi_early_init();
|
||||
bool wifi_sta_ok = WifiOta::instance().earlyInit();
|
||||
|
||||
health_monitor_init();
|
||||
HealthMonitor::instance().init();
|
||||
DEBUG_INFO("MAIN", "健康监控状态已初始化");
|
||||
DEBUG_INFO("MAIN", "========================================");
|
||||
DEBUG_INFO("MAIN", "ESP32-C3 OTA Relay v1.0 启动");
|
||||
@@ -50,7 +51,7 @@ extern "C" void app_main(void)
|
||||
// 健康监控任务(温度 + WiFi RSSI)
|
||||
BaseType_t xRet;
|
||||
xRet = xTaskCreatePinnedToCore(
|
||||
health_monitor_task,
|
||||
HealthMonitor::taskEntry,
|
||||
"health_mon",
|
||||
4096,
|
||||
NULL,
|
||||
@@ -64,10 +65,10 @@ extern "C" void app_main(void)
|
||||
DEBUG_INFO("MAIN", "健康监控任务已创建");
|
||||
}
|
||||
|
||||
protocol_init();
|
||||
Protocol::instance().init();
|
||||
DEBUG_INFO("MAIN", "协议层初始化完成");
|
||||
|
||||
esp_err_t ret = spi_master_init();
|
||||
esp_err_t ret = SpiMaster::instance().init();
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_ERR("MAIN", "SPI 总线初始化失败: %d", ret);
|
||||
} else {
|
||||
@@ -76,7 +77,7 @@ extern "C" void app_main(void)
|
||||
|
||||
// OTA 中继任务(中高优先级)
|
||||
xRet = xTaskCreatePinnedToCore(
|
||||
ota_relay_task,
|
||||
OtaRelay::taskEntry,
|
||||
"ota_relay",
|
||||
8192,
|
||||
NULL,
|
||||
@@ -92,7 +93,7 @@ extern "C" void app_main(void)
|
||||
|
||||
// WiFi OTA 服务器任务
|
||||
xRet = xTaskCreatePinnedToCore(
|
||||
wifi_ota_task,
|
||||
WifiOta::taskEntry,
|
||||
"wifi_ota",
|
||||
8192,
|
||||
NULL,
|
||||
@@ -108,7 +109,7 @@ extern "C" void app_main(void)
|
||||
|
||||
// BLE OTA 任务
|
||||
xRet = xTaskCreatePinnedToCore(
|
||||
ble_ota_task,
|
||||
BleOta::taskEntry,
|
||||
"ble_ota",
|
||||
8192,
|
||||
NULL,
|
||||
@@ -124,8 +125,7 @@ extern "C" void app_main(void)
|
||||
|
||||
DEBUG_INFO("MAIN", "========================================");
|
||||
DEBUG_INFO("MAIN", "所有任务创建完成,OTA Relay 运行中");
|
||||
DEBUG_INFO("MAIN", "调试日志输出: UDP(%s:%d) + UART0(GPIO21/TX, 115200)",
|
||||
DEBUG_INFO("MAIN", "调试日志输出: UDP(%s:%d) + UART0(GPIO21/TX, 115200)",
|
||||
DEBUG_LOG_IP, DEBUG_LOG_PORT);
|
||||
DEBUG_INFO("MAIN", "========================================");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
// ============================================================
|
||||
// 固件版本
|
||||
// ============================================================
|
||||
inline constexpr const char *FW_VERSION = "0.2.11";
|
||||
inline constexpr const char *FW_VERSION = "0.3.0";
|
||||
|
||||
// ============================================================
|
||||
// WiFi 工作模式选择
|
||||
|
||||
+80
-149
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* debug_log.c - 调试日志输出实现
|
||||
/*
|
||||
* debug_log.cpp - 调试日志输出实现
|
||||
*
|
||||
* 日志会同时写入三处:
|
||||
* 1. UART0 串口(本地串口调试)
|
||||
@@ -11,7 +11,7 @@
|
||||
*
|
||||
* 时序说明:
|
||||
* esp_netif_init() 创建 tcpip 线程后才可安全使用 socket
|
||||
* 本模块在首次 debug_log_send() 时才创建 socket(延迟创建)
|
||||
* 本模块在首次 send() 时才创建 socket(延迟创建)
|
||||
* WiFi 连接前 sendto() 会失败(无路由),日志仍可写入 UART 与 HTTP 日志缓冲
|
||||
* WiFi 连接后 UDP 立即生效,无需额外操作
|
||||
*/
|
||||
@@ -30,60 +30,39 @@
|
||||
|
||||
static const char *TAG = "DEBUG_LOG";
|
||||
|
||||
/** @brief 当前调试日志过滤级别,运行时可动态调整。 */
|
||||
debug_log_level_t g_debug_log_level = DEBUG_LOG_INFO;
|
||||
|
||||
/** @brief UDP 调试日志 socket 句柄,首次发送时延迟创建。 */
|
||||
static int s_udp_sock = -1;
|
||||
/** @brief UDP 调试日志目标地址。 */
|
||||
static struct sockaddr_in s_debug_addr;
|
||||
/** @brief 日志模块初始化标志。 */
|
||||
static bool s_initialized = false;
|
||||
// ============================================================
|
||||
// 单例
|
||||
// ============================================================
|
||||
DebugLog& DebugLog::instance()
|
||||
{
|
||||
static DebugLog inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 日志环形缓冲区(供 HTTP /logs 拉取)
|
||||
// ============================================================
|
||||
#define LOG_RING_SLOTS 64
|
||||
#define LOG_RING_SIZE 256
|
||||
static char s_log_ring[LOG_RING_SLOTS][LOG_RING_SIZE];
|
||||
static int s_log_ring_lens[LOG_RING_SLOTS];
|
||||
static int s_log_ring_head = 0; // 下次写入位置
|
||||
static int s_log_ring_count = 0; // 已存条数(超过 LOG_RING_SLOTS 后不再增长,覆盖旧数据)
|
||||
/** @brief 环形缓冲区互斥锁,保护多线程并发读写。 */
|
||||
static SemaphoreHandle_t s_log_ring_mutex = NULL;
|
||||
|
||||
/**
|
||||
* @brief 获取日志缓冲区中的条数
|
||||
* @return 当前日志条数。
|
||||
*/
|
||||
int debug_log_ring_count(void)
|
||||
int DebugLog::ringCount()
|
||||
{
|
||||
if (!s_log_ring_mutex) return 0;
|
||||
xSemaphoreTake(s_log_ring_mutex, portMAX_DELAY);
|
||||
int count = s_log_ring_count;
|
||||
xSemaphoreGive(s_log_ring_mutex);
|
||||
if (!m_logRingMutex) return 0;
|
||||
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
|
||||
int count = m_logRingCount;
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
return count;
|
||||
}
|
||||
|
||||
// 读取并清空日志缓冲区,返回 JSON 数组字符串
|
||||
// 调用者负责 free 返回值
|
||||
/**
|
||||
* @brief 读取并清空环形日志缓冲区,返回 JSON 数组字符串。
|
||||
* @param out_len 输出 JSON 字符串长度(不含末尾 '\0')。
|
||||
* @return 成功返回需由调用者 free 的字符串;无日志或失败返回 NULL。
|
||||
*/
|
||||
char *debug_log_ring_drain_json(int *out_len)
|
||||
char *DebugLog::ringDrainJson(int *out_len)
|
||||
{
|
||||
if (!s_log_ring_mutex) {
|
||||
if (!m_logRingMutex) {
|
||||
*out_len = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
xSemaphoreTake(s_log_ring_mutex, portMAX_DELAY);
|
||||
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
|
||||
|
||||
int count = s_log_ring_count;
|
||||
int count = m_logRingCount;
|
||||
if (count == 0) {
|
||||
xSemaphoreGive(s_log_ring_mutex);
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
*out_len = 0;
|
||||
return NULL;
|
||||
}
|
||||
@@ -92,7 +71,7 @@ char *debug_log_ring_drain_json(int *out_len)
|
||||
size_t buf_size = (size_t)count * 280 + 16;
|
||||
char *json = (char *)malloc(buf_size);
|
||||
if (!json) {
|
||||
xSemaphoreGive(s_log_ring_mutex);
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
*out_len = 0;
|
||||
return NULL;
|
||||
}
|
||||
@@ -101,17 +80,17 @@ char *debug_log_ring_drain_json(int *out_len)
|
||||
json[pos++] = '[';
|
||||
|
||||
// 从最旧的开始读
|
||||
int start = (count < LOG_RING_SLOTS) ? 0 : s_log_ring_head;
|
||||
int start = (count < LOG_RING_SLOTS) ? 0 : m_logRingHead;
|
||||
for (int i = 0; i < count && pos < (int)buf_size - 200; i++) {
|
||||
int idx = (start + i) % LOG_RING_SLOTS;
|
||||
int len = s_log_ring_lens[idx];
|
||||
int len = m_logRingLens[idx];
|
||||
if (len <= 0) continue;
|
||||
|
||||
if (i > 0) json[pos++] = ',';
|
||||
|
||||
pos += snprintf(json + pos, buf_size - pos, "\"");
|
||||
for (int j = 0; j < len && pos < (int)buf_size - 10; j++) {
|
||||
char c = s_log_ring[idx][j];
|
||||
char c = m_logRing[idx][j];
|
||||
if (c == '"' || c == '\\') {
|
||||
json[pos++] = '\\';
|
||||
json[pos++] = c;
|
||||
@@ -129,109 +108,85 @@ char *debug_log_ring_drain_json(int *out_len)
|
||||
json[pos] = '\0';
|
||||
|
||||
// 清空缓冲区
|
||||
s_log_ring_head = 0;
|
||||
/** @brief 环形缓冲区中已存储的日志条数。 */
|
||||
s_log_ring_count = 0;
|
||||
m_logRingHead = 0;
|
||||
m_logRingCount = 0;
|
||||
|
||||
xSemaphoreGive(s_log_ring_mutex);
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
|
||||
*out_len = pos;
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 写入一条日志到环形缓冲区
|
||||
* @param data
|
||||
* @param len
|
||||
*/
|
||||
static void log_ring_push(const char *data, int len)
|
||||
void DebugLog::ringPush(const char *data, int len)
|
||||
{
|
||||
if (!s_log_ring_mutex || len <= 0) return;
|
||||
if (!m_logRingMutex || len <= 0) return;
|
||||
|
||||
xSemaphoreTake(s_log_ring_mutex, portMAX_DELAY);
|
||||
xSemaphoreTake(m_logRingMutex, portMAX_DELAY);
|
||||
|
||||
int copy_len = len;
|
||||
if (copy_len >= LOG_RING_SIZE) copy_len = LOG_RING_SIZE - 1;
|
||||
|
||||
memcpy(s_log_ring[s_log_ring_head], data, copy_len);
|
||||
s_log_ring[s_log_ring_head][copy_len] = '\0';
|
||||
s_log_ring_lens[s_log_ring_head] = copy_len;
|
||||
memcpy(m_logRing[m_logRingHead], data, copy_len);
|
||||
m_logRing[m_logRingHead][copy_len] = '\0';
|
||||
m_logRingLens[m_logRingHead] = copy_len;
|
||||
|
||||
/** @brief 环形缓冲区下一个写入位置。 */
|
||||
s_log_ring_head = (s_log_ring_head + 1) % LOG_RING_SLOTS;
|
||||
if (s_log_ring_count < LOG_RING_SLOTS) s_log_ring_count++;
|
||||
m_logRingHead = (m_logRingHead + 1) % LOG_RING_SLOTS;
|
||||
if (m_logRingCount < LOG_RING_SLOTS) m_logRingCount++;
|
||||
|
||||
xSemaphoreGive(s_log_ring_mutex);
|
||||
xSemaphoreGive(m_logRingMutex);
|
||||
}
|
||||
|
||||
// UART0 状态
|
||||
static bool s_uart_initialized = false;
|
||||
/** @brief 调试串口使用的 UART 端口号。 */
|
||||
static uart_port_t s_uart_port = UART_NUM_0;
|
||||
|
||||
// ============================================================
|
||||
// UART0 串口调试
|
||||
// UART 调试日志
|
||||
// ============================================================
|
||||
/**
|
||||
* @brief 初始化 UART0 调试串口,配置波特率和引脚映射。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t uart_debug_init(void)
|
||||
esp_err_t DebugLog::uartInit()
|
||||
{
|
||||
if (s_uart_initialized) {
|
||||
if (m_uartInitialized) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uart_config_t uart_config;
|
||||
memset(&uart_config, 0, sizeof(uart_config));
|
||||
const uart_port_t uart_port = UART_NUM_0;
|
||||
uart_config_t uart_config = {};
|
||||
uart_config.baud_rate = UART_DEBUG_BAUD_RATE;
|
||||
uart_config.data_bits = UART_DATA_8_BITS;
|
||||
uart_config.parity = UART_PARITY_DISABLE;
|
||||
uart_config.parity = UART_PARITY_DISABLE;
|
||||
uart_config.stop_bits = UART_STOP_BITS_1;
|
||||
uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
|
||||
uart_config.source_clk = UART_SCLK_DEFAULT;
|
||||
uart_config.rx_flow_ctrl_thresh = 0;
|
||||
|
||||
esp_err_t ret = uart_driver_install(s_uart_port, 1024, 0, 0, NULL, 0);
|
||||
esp_err_t ret = uart_driver_install(uart_port, 1024, 0, 0, NULL, 0);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "UART0 驱动安装失败: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = uart_param_config(s_uart_port, &uart_config);
|
||||
ret = uart_param_config(uart_port, &uart_config);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "UART0 参数配置失败: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret = uart_set_pin(s_uart_port, UART_DEBUG_TX_GPIO, UART_DEBUG_RX_GPIO,
|
||||
ret = uart_set_pin(uart_port, UART_DEBUG_TX_GPIO, UART_DEBUG_RX_GPIO,
|
||||
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
|
||||
if (ret != ESP_OK) {
|
||||
ESP_LOGE(TAG, "UART0 引脚配置失败: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
/** @brief UART0 初始化标志。 */
|
||||
s_uart_initialized = true;
|
||||
m_uartInitialized = true;
|
||||
ESP_LOGI(TAG, "UART0 调试输出已初始化 (TX=GPIO%d, RX=GPIO%d, %d baud)",
|
||||
UART_DEBUG_TX_GPIO, UART_DEBUG_RX_GPIO, UART_DEBUG_BAUD_RATE);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 通过 UART0 发送一段原始字节数据。
|
||||
* @param data 待发送的数据指针。
|
||||
* @param len 数据长度。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t uart_debug_send(const uint8_t *data, size_t len)
|
||||
esp_err_t DebugLog::uartSend(const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!s_uart_initialized) {
|
||||
if (!m_uartInitialized) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
int ret = uart_write_bytes(s_uart_port, data, len);
|
||||
int ret = uart_write_bytes(UART_NUM_0, data, len);
|
||||
if (ret < 0) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
@@ -240,50 +195,38 @@ esp_err_t uart_debug_send(const uint8_t *data, size_t len)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UDP 调试日志
|
||||
// 初始化(UART 立即可用,UDP socket 延迟到首次发送时创建)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @brief 初始化(UART 立即可用,UDP socket 延迟到首次发送时创建)
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t debug_log_init(void)
|
||||
esp_err_t DebugLog::init()
|
||||
{
|
||||
if (s_initialized) {
|
||||
if (m_initialized) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
uart_debug_init();
|
||||
uartInit();
|
||||
|
||||
/** @brief 环形缓冲区互斥锁,保护多线程并发读写。 */
|
||||
s_log_ring_mutex = xSemaphoreCreateMutex();
|
||||
m_logRingMutex = xSemaphoreCreateMutex();
|
||||
|
||||
// 目标地址:单播到上位机
|
||||
memset(&s_debug_addr, 0, sizeof(s_debug_addr));
|
||||
s_debug_addr.sin_family = AF_INET;
|
||||
s_debug_addr.sin_port = htons(DEBUG_LOG_PORT);
|
||||
s_debug_addr.sin_addr.s_addr = inet_addr(DEBUG_LOG_IP);
|
||||
memset(&m_debugAddr, 0, sizeof(m_debugAddr));
|
||||
m_debugAddr.sin_family = AF_INET;
|
||||
m_debugAddr.sin_port = htons(DEBUG_LOG_PORT);
|
||||
m_debugAddr.sin_addr.s_addr = inet_addr(DEBUG_LOG_IP);
|
||||
|
||||
/** @brief 日志模块初始化标志。 */
|
||||
s_initialized = true;
|
||||
m_initialized = true;
|
||||
ESP_LOGI(TAG, "调试日志已初始化,上位机 %s:%d", DEBUG_LOG_IP, DEBUG_LOG_PORT);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 延迟创建 UDP socket(首次发送时调用)
|
||||
* @return
|
||||
*/
|
||||
static esp_err_t ensure_udp_socket(void)
|
||||
esp_err_t DebugLog::ensureUdpSocket()
|
||||
{
|
||||
if (s_udp_sock >= 0) {
|
||||
if (m_udpSock >= 0) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/** @brief UDP 调试日志 socket 句柄,首次发送时延迟创建。 */
|
||||
s_udp_sock = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (s_udp_sock < 0) {
|
||||
m_udpSock = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (m_udpSock < 0) {
|
||||
ESP_LOGE(TAG, "创建 UDP socket 失败");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
@@ -292,32 +235,26 @@ static esp_err_t ensure_udp_socket(void)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送原始数据(UDP 单播 + UART 双通道)
|
||||
* @param data 日志数据指针。
|
||||
* @param len 数据长度。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t debug_log_send(const uint8_t *data, size_t len)
|
||||
esp_err_t DebugLog::send(const uint8_t *data, size_t len)
|
||||
{
|
||||
esp_err_t udp_ret = ESP_FAIL;
|
||||
esp_err_t uart_ret = ESP_FAIL;
|
||||
|
||||
// UART 通道
|
||||
if (s_uart_initialized) {
|
||||
uart_ret = uart_debug_send(data, len);
|
||||
if (m_uartInitialized) {
|
||||
uart_ret = uartSend(data, len);
|
||||
}
|
||||
|
||||
// UDP 通道(WiFi 未连接时 sendto 失败,静默跳过)
|
||||
if (s_initialized) {
|
||||
if (s_udp_sock < 0) {
|
||||
ensure_udp_socket();
|
||||
if (m_initialized) {
|
||||
if (m_udpSock < 0) {
|
||||
ensureUdpSocket();
|
||||
}
|
||||
|
||||
if (s_udp_sock >= 0) {
|
||||
if (m_udpSock >= 0) {
|
||||
size_t send_len = len > 1024 ? 1024 : len;
|
||||
int ret = sendto(s_udp_sock, data, send_len, 0,
|
||||
(struct sockaddr *)&s_debug_addr, sizeof(s_debug_addr));
|
||||
int ret = ::sendto(m_udpSock, data, send_len, 0,
|
||||
(struct sockaddr *)&m_debugAddr, sizeof(m_debugAddr));
|
||||
if (ret >= 0) {
|
||||
udp_ret = ESP_OK;
|
||||
}
|
||||
@@ -325,7 +262,7 @@ esp_err_t debug_log_send(const uint8_t *data, size_t len)
|
||||
}
|
||||
|
||||
// 同时写入环形缓冲区(供 HTTP /logs 拉取)
|
||||
log_ring_push((const char *)data, (int)len);
|
||||
ringPush((const char *)data, (int)len);
|
||||
|
||||
if (udp_ret == ESP_OK || uart_ret == ESP_OK) {
|
||||
return ESP_OK;
|
||||
@@ -333,19 +270,13 @@ esp_err_t debug_log_send(const uint8_t *data, size_t len)
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 格式化日志输出
|
||||
* @param level 日志级别。
|
||||
* @param tag 日志标签。
|
||||
* @param fmt 格式化字符串。
|
||||
*/
|
||||
void debug_log(debug_log_level_t level, const char *tag, const char *fmt, ...)
|
||||
void DebugLog::log(debug_log_level_t level, const char *tag, const char *fmt, ...)
|
||||
{
|
||||
if (!s_initialized) {
|
||||
debug_log_init();
|
||||
if (!m_initialized) {
|
||||
init();
|
||||
}
|
||||
|
||||
if (level > g_debug_log_level) {
|
||||
if (level > logLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -369,5 +300,5 @@ void debug_log(debug_log_level_t level, const char *tag, const char *fmt, ...)
|
||||
char log_buf[600];
|
||||
int total = snprintf(log_buf, sizeof(log_buf), "%s %s\n", prefix, msg);
|
||||
|
||||
debug_log_send((const uint8_t *)log_buf, total);
|
||||
}
|
||||
send((const uint8_t *)log_buf, total);
|
||||
}
|
||||
+58
-44
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* debug_log.h - 调试日志输出
|
||||
*
|
||||
* 当前日志会并行写入:
|
||||
@@ -17,7 +17,11 @@
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include "esp_err.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "app_config.h"
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
|
||||
// 调试日志配置(DEBUG_LOG_IP/PORT/ENABLED 已在 app_config.h 中定义)
|
||||
|
||||
@@ -36,57 +40,67 @@ typedef enum {
|
||||
DEBUG_LOG_ALL = 5 // 全部
|
||||
} debug_log_level_t;
|
||||
|
||||
// 当前日志级别(运行时可改)
|
||||
extern debug_log_level_t g_debug_log_level;
|
||||
|
||||
/**
|
||||
* @brief 初始化日志系统,包括 UART 输出、UDP 目标和内部缓冲区。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
* @brief 调试日志类(单例)
|
||||
*
|
||||
* 封装 UART0、UDP 和环形缓冲区三路日志输出通道,
|
||||
* 所有内部状态(socket、缓冲区、互斥锁等)均为私有成员。
|
||||
*/
|
||||
esp_err_t debug_log_init(void);
|
||||
class DebugLog {
|
||||
public:
|
||||
static DebugLog& instance();
|
||||
|
||||
/**
|
||||
* @brief 初始化 UART0 调试串口。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t uart_debug_init(void);
|
||||
/** @brief 初始化日志系统,包括 UART 输出、UDP 目标和内部缓冲区。 */
|
||||
esp_err_t init();
|
||||
|
||||
/**
|
||||
* @brief 通过 UART0 直接发送一段原始字节数据。
|
||||
* @param data 待发送的数据指针。
|
||||
* @param len 数据长度。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t uart_debug_send(const uint8_t *data, size_t len);
|
||||
/** @brief 初始化 UART0 调试串口。 */
|
||||
esp_err_t uartInit();
|
||||
|
||||
/**
|
||||
* @brief 发送原始日志数据到底层输出通道。
|
||||
* @param data 日志数据指针。
|
||||
* @param len 数据长度。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t debug_log_send(const uint8_t *data, size_t len);
|
||||
/** @brief 通过 UART0 直接发送一段原始字节数据。 */
|
||||
esp_err_t uartSend(const uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief 按日志级别与标签格式化输出一条日志。
|
||||
* @param level 日志级别。
|
||||
* @param tag 日志标签。
|
||||
* @param fmt 格式化字符串。
|
||||
*/
|
||||
void debug_log(debug_log_level_t level, const char *tag, const char *fmt, ...);
|
||||
/** @brief 发送原始日志数据到底层输出通道。 */
|
||||
esp_err_t send(const uint8_t *data, size_t len);
|
||||
|
||||
/**
|
||||
* @brief 获取当前环形日志缓冲区中的日志条数。
|
||||
* @return 当前日志条数。
|
||||
*/
|
||||
int debug_log_ring_count(void);
|
||||
/** @brief 按日志级别与标签格式化输出一条日志。 */
|
||||
void log(debug_log_level_t level, const char *tag, const char *fmt, ...);
|
||||
|
||||
// 读取并清空环形日志缓冲区,返回 JSON 数组字符串,需由调用者 free。
|
||||
char *debug_log_ring_drain_json(int *out_len); // 调用者 free
|
||||
/** @brief 获取当前环形日志缓冲区中的日志条数。 */
|
||||
int ringCount();
|
||||
|
||||
#define DEBUG_ERR(tag, fmt, ...) debug_log(DEBUG_LOG_ERR, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_WARN(tag, fmt, ...) debug_log(DEBUG_LOG_WARN, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_INFO(tag, fmt, ...) debug_log(DEBUG_LOG_INFO, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_DBG(tag, fmt, ...) debug_log(DEBUG_LOG_DBG, tag, fmt, ##__VA_ARGS__)
|
||||
/** @brief 读取并清空环形日志缓冲区,返回 JSON 数组字符串(需调用者 free)。 */
|
||||
char* ringDrainJson(int *out_len);
|
||||
|
||||
/** @brief 当前日志级别(运行时可改)。 */
|
||||
debug_log_level_t logLevel = DEBUG_LOG_INFO;
|
||||
|
||||
private:
|
||||
DebugLog() = default;
|
||||
~DebugLog() = default;
|
||||
DebugLog(const DebugLog&) = delete;
|
||||
DebugLog& operator=(const DebugLog&) = delete;
|
||||
|
||||
int m_udpSock = -1;
|
||||
struct sockaddr_in m_debugAddr;
|
||||
bool m_initialized = false;
|
||||
bool m_uartInitialized = false;
|
||||
|
||||
static constexpr int LOG_RING_SLOTS = 64;
|
||||
static constexpr int LOG_RING_SIZE = 256;
|
||||
char m_logRing[LOG_RING_SLOTS][LOG_RING_SIZE];
|
||||
int m_logRingLens[LOG_RING_SLOTS];
|
||||
int m_logRingHead = 0;
|
||||
int m_logRingCount = 0;
|
||||
SemaphoreHandle_t m_logRingMutex = NULL;
|
||||
|
||||
esp_err_t ensureUdpSocket();
|
||||
void ringPush(const char *data, int len);
|
||||
};
|
||||
|
||||
// 便捷宏 — 直接调用单例方法
|
||||
#define DEBUG_ERR(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_ERR, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_WARN(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_WARN, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_INFO(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_INFO, tag, fmt, ##__VA_ARGS__)
|
||||
#define DEBUG_DBG(tag, fmt, ...) DebugLog::instance().log(DEBUG_LOG_DBG, tag, fmt, ##__VA_ARGS__)
|
||||
|
||||
#endif // DEBUG_LOG_H
|
||||
+92
-129
@@ -1,6 +1,8 @@
|
||||
/*
|
||||
* health_monitor.cpp - 系统健康监控
|
||||
* 包含:健康状态管理 + WiFi RSSI 监控 + 温度监控
|
||||
*
|
||||
* 所有功能封装为 HealthMonitor 类方法。
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@@ -16,207 +18,173 @@
|
||||
#include "debug_log.h"
|
||||
#include "server/wifi_ota.h"
|
||||
|
||||
/** @brief 全局健康状态,受互斥锁保护。 */
|
||||
health_state_t g_health_state = SYS_NORMAL;
|
||||
// ============================================================
|
||||
// 静态成员
|
||||
// ============================================================
|
||||
health_state_t HealthMonitor::healthState = SYS_NORMAL;
|
||||
|
||||
/** @brief 健康状态互斥锁。 */
|
||||
static SemaphoreHandle_t g_health_mutex = NULL;
|
||||
/** @brief 内置温度传感器句柄。 */
|
||||
static temperature_sensor_handle_t s_temp_handle = NULL;
|
||||
/** @brief 最近一次采样的芯片温度(摄氏度)。 */
|
||||
static float s_last_temperature = 0.0f;
|
||||
/** @brief 最近一次健康监控采样得到的 WiFi RSSI。 */
|
||||
static int8_t s_last_wifi_rssi = -127;
|
||||
/** @brief 最近一次 WiFi RSSI 采样时间(毫秒)。 */
|
||||
static int64_t s_last_wifi_rssi_ms = 0;
|
||||
|
||||
// ----------------------------------------
|
||||
// 健康状态管理
|
||||
/**
|
||||
* @brief 初始化健康监控模块,创建互斥锁和基础状态。
|
||||
*/
|
||||
void health_monitor_init(void)
|
||||
// ============================================================
|
||||
// 单例
|
||||
// ============================================================
|
||||
HealthMonitor& HealthMonitor::instance()
|
||||
{
|
||||
/** @brief 健康状态互斥锁。 */
|
||||
g_health_mutex = xSemaphoreCreateMutex();
|
||||
if (g_health_mutex == NULL) {
|
||||
static HealthMonitor inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 初始化
|
||||
// ============================================================
|
||||
void HealthMonitor::init()
|
||||
{
|
||||
m_mutex = xSemaphoreCreateMutex();
|
||||
if (m_mutex == NULL) {
|
||||
DEBUG_ERR("HEALTH", "创建健康状态互斥锁失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取当前系统健康状态。
|
||||
* @return 当前健康状态枚举值。
|
||||
*/
|
||||
health_state_t health_get_current(void)
|
||||
// ============================================================
|
||||
// 获取当前健康状态
|
||||
// ============================================================
|
||||
health_state_t HealthMonitor::getCurrent()
|
||||
{
|
||||
health_state_t state;
|
||||
if (g_health_mutex) {
|
||||
xSemaphoreTake(g_health_mutex, portMAX_DELAY);
|
||||
if (m_mutex) {
|
||||
xSemaphoreTake(m_mutex, portMAX_DELAY);
|
||||
}
|
||||
state = g_health_state;
|
||||
if (g_health_mutex) {
|
||||
xSemaphoreGive(g_health_mutex);
|
||||
state = healthState;
|
||||
if (m_mutex) {
|
||||
xSemaphoreGive(m_mutex);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 返回最近一次采样到的芯片温度(摄氏度)。
|
||||
* @return 最近一次温度采样值(摄氏度)。
|
||||
*/
|
||||
float health_get_last_temperature(void)
|
||||
float HealthMonitor::getLastTemperature()
|
||||
{
|
||||
/** @brief 最近一次采样的芯片温度(摄氏度)。 */
|
||||
return s_last_temperature;
|
||||
return m_lastTemperature;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取最近一次采样到的 WiFi RSSI 及其年龄。
|
||||
* @param out_rssi 输出 RSSI 指针,可为 NULL。
|
||||
* @param out_age_ms 输出采样距今毫秒数指针,可为 NULL。
|
||||
* @return 有有效采样返回 true,否则返回 false。
|
||||
*/
|
||||
bool health_get_last_wifi_rssi(int8_t *out_rssi, uint32_t *out_age_ms)
|
||||
bool HealthMonitor::getLastWifiRssi(int8_t *out_rssi, uint32_t *out_age_ms)
|
||||
{
|
||||
if (s_last_wifi_rssi_ms <= 0) {
|
||||
if (m_lastWifiRssiMs <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int64_t now_ms = esp_timer_get_time() / 1000;
|
||||
uint32_t age_ms = (now_ms > s_last_wifi_rssi_ms) ? (uint32_t)(now_ms - s_last_wifi_rssi_ms) : 0;
|
||||
uint32_t age_ms = (now_ms > m_lastWifiRssiMs) ? (uint32_t)(now_ms - m_lastWifiRssiMs) : 0;
|
||||
|
||||
if (out_rssi) *out_rssi = s_last_wifi_rssi;
|
||||
if (out_rssi) *out_rssi = m_lastWifiRssi;
|
||||
if (out_age_ms) *out_age_ms = age_ms;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// ============================================================
|
||||
// 温度监控(定时器回调)
|
||||
/**
|
||||
* @brief 温度定时回调,采样并根据阈值更新健康状态。
|
||||
* @param arg
|
||||
*/
|
||||
static void temp_monitor_callback(void *arg)
|
||||
// ============================================================
|
||||
void HealthMonitor::tempMonitorCallback(void *arg)
|
||||
{
|
||||
auto& self = instance();
|
||||
float temp = 0.0f;
|
||||
esp_err_t err = temperature_sensor_get_celsius(s_temp_handle, &temp);
|
||||
esp_err_t err = temperature_sensor_get_celsius(self.m_tempHandle, &temp);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN("TEMP", "读取温度失败: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
/** @brief 最近一次采样的芯片温度(摄氏度)。 */
|
||||
s_last_temperature = temp;
|
||||
self.m_lastTemperature = temp;
|
||||
|
||||
if (temp >= TEMP_CRITICAL) {
|
||||
/** @brief 全局健康状态,受互斥锁保护。 */
|
||||
g_health_state = SYS_SAFE_SHUTDOWN;
|
||||
healthState = SYS_SAFE_SHUTDOWN;
|
||||
DEBUG_ERR("TEMP", "!!! 危险 !!! 温度 %.1f°C >= %d°C,触发安全关机", temp, TEMP_CRITICAL);
|
||||
DEBUG_ERR("TEMP", "健康状态变更为: SYS_SAFE_SHUTDOWN");
|
||||
} else if (temp >= TEMP_WARNING) {
|
||||
if (g_health_state < SYS_WARNING) {
|
||||
/** @brief 全局健康状态,受互斥锁保护。 */
|
||||
g_health_state = SYS_WARNING;
|
||||
if (healthState < SYS_WARNING) {
|
||||
healthState = SYS_WARNING;
|
||||
DEBUG_WARN("TEMP", "警告: 温度 %.1f°C >= %d°C", temp, TEMP_WARNING);
|
||||
DEBUG_WARN("TEMP", "健康状态变更为: SYS_WARNING");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// 温度监控任务入口
|
||||
/**
|
||||
* @brief 启动温度监控定时器,周期性巡检系统健康。
|
||||
* @param pvParameters FreeRTOS 任务参数,未使用。
|
||||
*/
|
||||
void health_monitor_task(void *pvParameters)
|
||||
// ============================================================
|
||||
// FreeRTOS 任务入口
|
||||
// ============================================================
|
||||
void HealthMonitor::taskEntry(void *pvParameters)
|
||||
{
|
||||
auto& self = instance();
|
||||
DEBUG_INFO("MON", "=== 健康监控任务启动 ===");
|
||||
|
||||
temperature_sensor_config_t temp_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(20, 80);
|
||||
esp_err_t err = temperature_sensor_install(&temp_config, &s_temp_handle);
|
||||
esp_err_t err = temperature_sensor_install(&temp_config, &self.m_tempHandle);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("MON", "温度传感器初始化失败: %s", esp_err_to_name(err));
|
||||
} else {
|
||||
ESP_ERROR_CHECK(temperature_sensor_enable(s_temp_handle));
|
||||
ESP_ERROR_CHECK(temperature_sensor_enable(self.m_tempHandle));
|
||||
DEBUG_INFO("MON", "温度传感器已启用 (范围 20°C ~ 80°C)");
|
||||
}
|
||||
|
||||
// 定时器,每1秒读取一次温度
|
||||
esp_timer_handle_t temp_timer;
|
||||
esp_timer_create_args_t timer_args = {};
|
||||
timer_args.callback = &temp_monitor_callback;
|
||||
timer_args.callback = &tempMonitorCallback;
|
||||
timer_args.arg = nullptr;
|
||||
timer_args.name = "temp_monitor";
|
||||
timer_args.dispatch_method = ESP_TIMER_TASK;
|
||||
timer_args.skip_unhandled_events = false;
|
||||
esp_timer_create(&timer_args, &temp_timer);
|
||||
esp_timer_start_periodic(temp_timer, 1 * 1000000); // 1秒
|
||||
|
||||
DEBUG_INFO("MON", "温度监控定时器已启动 (间隔 1 秒)");
|
||||
if (esp_timer_create(&timer_args, &temp_timer) == ESP_OK) {
|
||||
esp_timer_start_periodic(temp_timer, 1000000); // 1秒
|
||||
DEBUG_INFO("MON", "温度监控定时器已启动 (1秒间隔)");
|
||||
} else {
|
||||
DEBUG_ERR("MON", "创建温度监控定时器失败");
|
||||
}
|
||||
|
||||
// 保活循环:定时器由 esp_timer 任务驱动,此任务只需保持存活
|
||||
// 状态汇报已由 status_monitor_task 负责,此处只在异常时告警
|
||||
// 周期性巡检
|
||||
while (1) {
|
||||
int8_t rssi = wifi_get_rssi();
|
||||
if (wifi_is_connected() && rssi != -127) {
|
||||
s_last_wifi_rssi = rssi;
|
||||
s_last_wifi_rssi_ms = esp_timer_get_time() / 1000;
|
||||
}
|
||||
|
||||
uint32_t free_heap = esp_get_free_heap_size();
|
||||
if (free_heap < HEAP_WARNING_MIN) {
|
||||
DEBUG_WARN("MON", "空闲堆内存不足: %lu < %d", (unsigned long)free_heap, HEAP_WARNING_MIN);
|
||||
DEBUG_WARN("MON", "堆内存不足: %lu < %lu", free_heap, HEAP_WARNING_MIN);
|
||||
}
|
||||
if (g_health_state >= SYS_CRITICAL) {
|
||||
DEBUG_ERR("MON", "健康状态危险: %d", g_health_state);
|
||||
if (healthState >= SYS_CRITICAL) {
|
||||
DEBUG_ERR("MON", "健康状态危险: %d", healthState);
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(5000));
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// ============================================================
|
||||
// 健康事件日志
|
||||
/**
|
||||
* @brief 记录一条健康监控事件日志。
|
||||
* @param event 事件名称字符串。
|
||||
* @param value 事件关联的数值。
|
||||
*/
|
||||
void health_log_event(const char *event, int value)
|
||||
// ============================================================
|
||||
void HealthMonitor::logEvent(const char *event, int value)
|
||||
{
|
||||
DEBUG_INFO("HEALTH", "[健康事件] %s: %d", event, value);
|
||||
}
|
||||
|
||||
// ----------------------------------------
|
||||
// OTA 传输前的健康检查
|
||||
/**
|
||||
* @brief OTA 传输前执行健康检查,决定是否允许传输。
|
||||
* @return 健康检查结果,决定是否允许 OTA。
|
||||
*/
|
||||
health_status_t health_check_before_ota(void)
|
||||
// ============================================================
|
||||
// OTA 健康检查
|
||||
// ============================================================
|
||||
health_status_t HealthMonitor::checkBeforeOta()
|
||||
{
|
||||
health_status_t status = HEALTH_MON_OK;
|
||||
|
||||
if (g_health_state >= SYS_SAFE_SHUTDOWN) {
|
||||
if (healthState >= SYS_SAFE_SHUTDOWN) {
|
||||
DEBUG_WARN("HEALTH", "OTA 前检查: SYS_SAFE_SHUTDOWN,禁止传输");
|
||||
return HEALTH_MON_SHUTDOWN;
|
||||
} else if (g_health_state >= SYS_CRITICAL) {
|
||||
} else if (healthState >= SYS_CRITICAL) {
|
||||
DEBUG_WARN("HEALTH", "OTA 前检查: SYS_CRITICAL,禁止传输");
|
||||
return HEALTH_MON_CRITICAL;
|
||||
} else if (g_health_state >= SYS_WARNING) {
|
||||
} else if (healthState >= SYS_WARNING) {
|
||||
status = HEALTH_MON_WARNING;
|
||||
DEBUG_WARN("HEALTH", "OTA 前检查: SYS_WARNING,继续传输但需注意");
|
||||
}
|
||||
|
||||
if (!wifi_is_connected()) {
|
||||
if (!WifiOta::isConnected()) {
|
||||
DEBUG_WARN("HEALTH", "OTA 前检查: WiFi 未连接");
|
||||
return HEALTH_MON_CRITICAL;
|
||||
}
|
||||
|
||||
int8_t last_rssi = -127;
|
||||
uint32_t age_ms = 0;
|
||||
if (health_get_last_wifi_rssi(&last_rssi, &age_ms)) {
|
||||
if (getLastWifiRssi(&last_rssi, &age_ms)) {
|
||||
if (age_ms <= OTA_WIFI_RSSI_SAMPLE_MAX_AGE_MS) {
|
||||
if (last_rssi < OTA_WIFI_RSSI_CRITICAL) {
|
||||
DEBUG_WARN("HEALTH", "OTA 前检查: 最近 RSSI %d < %d (样本年龄 %lu ms)",
|
||||
@@ -239,21 +207,21 @@ health_status_t health_check_before_ota(void)
|
||||
return status;
|
||||
}
|
||||
|
||||
health_status_t health_check_before_ota_relay(void)
|
||||
health_status_t HealthMonitor::checkBeforeOtaRelay()
|
||||
{
|
||||
health_status_t status = HEALTH_MON_OK;
|
||||
|
||||
if (g_health_state >= SYS_SAFE_SHUTDOWN) {
|
||||
if (healthState >= SYS_SAFE_SHUTDOWN) {
|
||||
DEBUG_WARN("HEALTH", "OTA 中继检查: SYS_SAFE_SHUTDOWN,禁止传输");
|
||||
return HEALTH_MON_SHUTDOWN;
|
||||
} else if (g_health_state >= SYS_CRITICAL) {
|
||||
} else if (healthState >= SYS_CRITICAL) {
|
||||
/* SYS_CRITICAL 可能是由 WiFi RSSI 过低导致的,
|
||||
* 但 SPI 中继阶段不依赖 WiFi,只检查温度/硬件等本地状态。
|
||||
* 温度达到 TEMP_CRITICAL(100°C) 时 g_health_state 会被设为
|
||||
* SYS_SAFE_SHUTDOWN,在上面的分支已经拦截。
|
||||
* 所以 SYS_CRITICAL 在中继阶段只可能是 WiFi 导致的,允许继续。 */
|
||||
DEBUG_WARN("HEALTH", "OTA 中继检查: SYS_CRITICAL (WiFi 相关),SPI 中继允许继续");
|
||||
} else if (g_health_state >= SYS_WARNING) {
|
||||
} else if (healthState >= SYS_WARNING) {
|
||||
status = HEALTH_MON_WARNING;
|
||||
DEBUG_WARN("HEALTH", "OTA 中继检查: SYS_WARNING,继续传输");
|
||||
}
|
||||
@@ -269,35 +237,30 @@ health_status_t health_check_before_ota_relay(void)
|
||||
return status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 根据 WiFi RSSI 更新健康状态并返回检查结果。
|
||||
* @param rssi 当前 WiFi 信号强度(dBm)。
|
||||
* @return 健康检查结果。
|
||||
*/
|
||||
health_status_t health_update_from_wifi_rssi(int8_t rssi)
|
||||
// ============================================================
|
||||
// WiFi RSSI 更新
|
||||
// ============================================================
|
||||
health_status_t HealthMonitor::updateFromWifiRssi(int8_t rssi)
|
||||
{
|
||||
if (rssi < WIFI_RSSI_CRITICAL) {
|
||||
/** @brief 全局健康状态,受互斥锁保护。 */
|
||||
g_health_state = SYS_CRITICAL;
|
||||
healthState = SYS_CRITICAL;
|
||||
DEBUG_WARN("HEALTH", "WiFi RSSI 危险: %d dBm < %d dBm", rssi, WIFI_RSSI_CRITICAL);
|
||||
health_log_event("WIFI危险", rssi);
|
||||
logEvent("WIFI危险", rssi);
|
||||
return HEALTH_MON_CRITICAL;
|
||||
} else if (rssi < WIFI_RSSI_NORMAL) {
|
||||
if (g_health_state < SYS_WARNING) {
|
||||
/** @brief 全局健康状态,受互斥锁保护。 */
|
||||
g_health_state = SYS_WARNING;
|
||||
if (healthState < SYS_WARNING) {
|
||||
healthState = SYS_WARNING;
|
||||
DEBUG_WARN("HEALTH", "WiFi RSSI 警告: %d dBm < %d dBm", rssi, WIFI_RSSI_NORMAL);
|
||||
health_log_event("WIFI警告", rssi);
|
||||
logEvent("WIFI警告", rssi);
|
||||
}
|
||||
return HEALTH_MON_WARNING;
|
||||
}
|
||||
|
||||
if (g_health_state == SYS_WARNING && rssi >= WIFI_RSSI_NORMAL) {
|
||||
/** @brief 全局健康状态,受互斥锁保护。 */
|
||||
g_health_state = SYS_NORMAL;
|
||||
if (healthState == SYS_WARNING && rssi >= WIFI_RSSI_NORMAL) {
|
||||
healthState = SYS_NORMAL;
|
||||
DEBUG_INFO("HEALTH", "WiFi RSSI 恢复: %d dBm >= %d dBm", rssi, WIFI_RSSI_NORMAL);
|
||||
health_log_event("WIFI恢复", rssi);
|
||||
logEvent("WIFI恢复", rssi);
|
||||
}
|
||||
|
||||
return HEALTH_MON_OK;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,14 @@
|
||||
/*
|
||||
* health_monitor.h - 系统健康监控头文件
|
||||
*
|
||||
* HealthMonitor 类封装健康状态管理、温度监控和 WiFi RSSI 监控。
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "driver/temperature_sensor.h"
|
||||
#include "app_config.h"
|
||||
|
||||
// T9.3: 健康状态等级
|
||||
@@ -21,69 +26,60 @@ typedef enum {
|
||||
HEALTH_MON_SHUTDOWN = 3
|
||||
} health_status_t;
|
||||
|
||||
// 外部共享状态(互斥锁保护)
|
||||
extern health_state_t g_health_state;
|
||||
|
||||
// WiFi RSSI 阈值(来自 app_config.h)
|
||||
#define WIFI_RSSI_NORMAL WIFI_RSSI_WARNING
|
||||
#define WIFI_RSSI_WEAK WIFI_RSSI_CRITICAL
|
||||
|
||||
/**
|
||||
* @brief 初始化健康监控模块,创建内部互斥锁与基础状态。
|
||||
*/
|
||||
void health_monitor_init(void);
|
||||
|
||||
/**
|
||||
* @brief 健康监控任务入口,负责启动温度定时检测并周期性巡检系统状态。
|
||||
* @param pvParameters FreeRTOS 任务参数,未使用。
|
||||
*/
|
||||
void health_monitor_task(void *pvParameters);
|
||||
|
||||
/**
|
||||
* @brief 在 OTA 传输开始前执行健康检查,返回当前是否允许传输。
|
||||
* @return 健康检查结果,决定是否允许 OTA。
|
||||
*/
|
||||
health_status_t health_check_before_ota(void);
|
||||
|
||||
/**
|
||||
* @brief 在 OTA 中继转发阶段执行健康检查,跳过 WiFi 相关检查。
|
||||
* @brief 系统健康监控类(单例)
|
||||
*
|
||||
* SPI 中继阶段固件已存储在 ESP32 Flash 中,不依赖 WiFi 连接,
|
||||
* 因此跳过 WiFi 连接状态和 RSSI 检查,只检查温度等本地状态。
|
||||
* @return 健康检查结果,决定是否允许 OTA。
|
||||
* 封装健康状态管理、温度定时监控和 WiFi RSSI 监控。
|
||||
*/
|
||||
health_status_t health_check_before_ota_relay(void);
|
||||
class HealthMonitor {
|
||||
public:
|
||||
static HealthMonitor& instance();
|
||||
|
||||
/**
|
||||
* @brief 记录一条健康相关事件日志,便于故障追踪与状态回溯。
|
||||
* @param event 事件名称字符串。
|
||||
* @param value 事件关联的数值。
|
||||
*/
|
||||
void health_log_event(const char *event, int value);
|
||||
/** @brief 初始化健康监控模块,创建内部互斥锁与基础状态。 */
|
||||
void init();
|
||||
|
||||
/**
|
||||
* @brief 获取当前系统健康状态。
|
||||
* @return 当前健康状态枚举值。
|
||||
*/
|
||||
health_state_t health_get_current(void);
|
||||
/** @brief 健康监控 FreeRTOS 任务入口。 */
|
||||
static void taskEntry(void *pvParameters);
|
||||
|
||||
/**
|
||||
* @brief 根据最新 WiFi RSSI 更新健康状态,并返回对应健康检查结果。
|
||||
* @param rssi 当前 WiFi 信号强度(dBm)。
|
||||
* @return 健康检查结果。
|
||||
*/
|
||||
health_status_t health_update_from_wifi_rssi(int8_t rssi);
|
||||
/** @brief OTA 传输前执行健康检查。 */
|
||||
health_status_t checkBeforeOta();
|
||||
|
||||
/**
|
||||
* @brief 获取最近一次采样到的芯片温度(摄氏度)。
|
||||
* @return 最近一次温度采样值(摄氏度)。
|
||||
*/
|
||||
float health_get_last_temperature(void);
|
||||
/** @brief OTA 中继转发阶段执行健康检查(跳过 WiFi 检查)。 */
|
||||
health_status_t checkBeforeOtaRelay();
|
||||
|
||||
/**
|
||||
* @brief 获取最近一次健康监控采样得到的 WiFi RSSI。
|
||||
* @param out_rssi 输出 RSSI。
|
||||
* @param out_age_ms 输出样本距今时长(毫秒)。
|
||||
* @return true 有有效样本,false 当前无有效样本。
|
||||
*/
|
||||
bool health_get_last_wifi_rssi(int8_t *out_rssi, uint32_t *out_age_ms);
|
||||
/** @brief 记录一条健康事件日志。 */
|
||||
void logEvent(const char *event, int value);
|
||||
|
||||
/** @brief 获取当前系统健康状态。 */
|
||||
health_state_t getCurrent();
|
||||
|
||||
/** @brief 根据 WiFi RSSI 更新健康状态。 */
|
||||
health_status_t updateFromWifiRssi(int8_t rssi);
|
||||
|
||||
/** @brief 获取最近一次芯片温度。 */
|
||||
float getLastTemperature();
|
||||
|
||||
/** @brief 获取最近一次 WiFi RSSI 及其年龄。 */
|
||||
bool getLastWifiRssi(int8_t *out_rssi, uint32_t *out_age_ms);
|
||||
|
||||
/** @brief 全局健康状态(public,供外部读取)。 */
|
||||
static health_state_t healthState;
|
||||
|
||||
private:
|
||||
HealthMonitor() = default;
|
||||
~HealthMonitor() = default;
|
||||
HealthMonitor(const HealthMonitor&) = delete;
|
||||
HealthMonitor& operator=(const HealthMonitor&) = delete;
|
||||
|
||||
SemaphoreHandle_t m_mutex = NULL;
|
||||
temperature_sensor_handle_t m_tempHandle = NULL;
|
||||
float m_lastTemperature = 0.0f;
|
||||
int8_t m_lastWifiRssi = -127;
|
||||
int64_t m_lastWifiRssiMs = 0;
|
||||
|
||||
static void tempMonitorCallback(void *arg);
|
||||
};
|
||||
+311
-543
File diff suppressed because it is too large
Load Diff
+103
-119
@@ -1,6 +1,8 @@
|
||||
/*
|
||||
* ota_relay.h - OTA 中继核心状态机
|
||||
* T2.3, T3.1 - T3.7
|
||||
*
|
||||
* OtaRelay 类封装 OTA 中继状态机、Flash 存储和元数据管理。
|
||||
*/
|
||||
#ifndef OTA_RELAY_H
|
||||
#define OTA_RELAY_H
|
||||
@@ -8,151 +10,133 @@
|
||||
#include <stdint.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/queue.h"
|
||||
#include "transport/protocol.h"
|
||||
#include "esp_partition.h"
|
||||
|
||||
// T4.3: OTA 元数据结构,用于 NVS 存储
|
||||
#define OTA_META_NVS_NAMESPACE "ota_meta"
|
||||
#define OTA_META_NVS_KEY "meta"
|
||||
|
||||
typedef struct {
|
||||
uint32_t total_size; // 固件总大小(字节)
|
||||
uint32_t stored_size; // 当前已存储大小(字节)
|
||||
uint32_t last_seq; // 最后成功存储的序列号
|
||||
uint32_t chunk_count; // 预期总块数
|
||||
uint8_t checksum[32]; // 固件 SHA256 校验(可选)
|
||||
uint32_t total_size;
|
||||
uint32_t stored_size;
|
||||
uint32_t last_seq;
|
||||
uint32_t chunk_count;
|
||||
uint8_t checksum[32];
|
||||
} ota_meta_t;
|
||||
|
||||
// OTA 中继状态(对应 GD32 四阶段协议)
|
||||
typedef enum {
|
||||
// 阶段0: 空闲
|
||||
RELAY_IDLE = 0,
|
||||
|
||||
// 阶段1: 安全握手
|
||||
RELAY_HANDSHAKE_IDLE, // 等待 HANDSHAKE_REQ
|
||||
RELAY_HANDSHAKE_SENT, // 已发送 0x00 AC,等待 GD32 响应
|
||||
RELAY_CHALLENGE_SENT, // 已发送 nonce_server (16B),等待响应
|
||||
RELAY_CHALLENGE_RESPOND, // 已收到 GD32 握手数据包
|
||||
RELAY_SESSION_KEY_SENT, // 已发送变换后的随机数,等待 0xBA C0
|
||||
RELAY_AUTH_COMPLETE, // 已收到 0xBA C0,安全通道建立完成
|
||||
|
||||
// 阶段2: 传输准备
|
||||
RELAY_SIZE_SENT, // 已发送固件大小(4B LE)
|
||||
RELAY_SIZE_ACK, // 已收到 0xBA C0 + seq
|
||||
|
||||
// 阶段3: 数据传输
|
||||
RELAY_RECEIVING, // 接收并存储固件到 Flash
|
||||
RELAY_RELAY_ACTIVE, // 存储完成,开始向 GD32 转发
|
||||
RELAY_PACKET_SENT, // 已发送 DATA_PACKET,等待 GD32 ACK
|
||||
RELAY_PACKET_ACK_OK, // ACK 正常,继续下一包
|
||||
RELAY_PACKET_ACK_RETRY, // ACK 要求重发,重新发送当前包
|
||||
RELAY_RECEIVE_DONE, // 已发送 DATA_END,等待 TRANSFER_DONE
|
||||
|
||||
// 阶段4: 结束跳转
|
||||
RELAY_TRANSFER_DONE, // 已收到 0xBA C0 + FF FF FF FF
|
||||
RELAY_FINISH_SENT, // 已发送 0x0A AC,等待 GD32 确认
|
||||
RELAY_DONE, // GD32 回复 0x0A AC,OTA 完成
|
||||
RELAY_APP_VERIFY, // 等待 GD32 APP 启动并回复存活确认
|
||||
|
||||
// 错误状态
|
||||
RELAY_HANDSHAKE_IDLE,
|
||||
RELAY_HANDSHAKE_SENT,
|
||||
RELAY_CHALLENGE_SENT,
|
||||
RELAY_CHALLENGE_RESPOND,
|
||||
RELAY_SESSION_KEY_SENT,
|
||||
RELAY_AUTH_COMPLETE,
|
||||
RELAY_SIZE_SENT,
|
||||
RELAY_SIZE_ACK,
|
||||
RELAY_RECEIVING,
|
||||
RELAY_RELAY_ACTIVE,
|
||||
RELAY_PACKET_SENT,
|
||||
RELAY_PACKET_ACK_OK,
|
||||
RELAY_PACKET_ACK_RETRY,
|
||||
RELAY_RECEIVE_DONE,
|
||||
RELAY_TRANSFER_DONE,
|
||||
RELAY_FINISH_SENT,
|
||||
RELAY_DONE,
|
||||
RELAY_APP_VERIFY,
|
||||
RELAY_ERROR = 0xFF
|
||||
} relay_state_t;
|
||||
|
||||
// OTA 结果码
|
||||
typedef enum {
|
||||
OTA_RESULT_OK = 0,
|
||||
OTA_RESULT_ERR_TIMEOUT = -1, // GD32 超时无响应
|
||||
OTA_RESULT_ERR_CRC = -2, // CRC 校验失败
|
||||
OTA_RESULT_ERR_AUTH = -3, // 认证失败
|
||||
OTA_RESULT_ERR_STORAGE = -4, // Flash 存储错误
|
||||
OTA_RESULT_ERR_GD32 = -5, // GD32 返回错误
|
||||
OTA_RESULT_ERR_HEALTH = -6, // 健康状态异常中止
|
||||
OTA_RESULT_ERR_USER_ABORT = -7 // 用户中止
|
||||
OTA_RESULT_ERR_TIMEOUT = -1,
|
||||
OTA_RESULT_ERR_CRC = -2,
|
||||
OTA_RESULT_ERR_AUTH = -3,
|
||||
OTA_RESULT_ERR_STORAGE = -4,
|
||||
OTA_RESULT_ERR_GD32 = -5,
|
||||
OTA_RESULT_ERR_HEALTH = -6,
|
||||
OTA_RESULT_ERR_USER_ABORT = -7
|
||||
} ota_result_t;
|
||||
|
||||
// 外部队列句柄(在 app_main 中创建)
|
||||
extern QueueHandle_t g_ota_dispatch_queue; // 从 WiFi/BLE 到 OTA 中继
|
||||
extern QueueHandle_t g_ota_result_queue; // OTA 结果到 WiFi/BLE
|
||||
class OtaRelay {
|
||||
public:
|
||||
static OtaRelay& instance();
|
||||
|
||||
/**
|
||||
* @brief OTA 中继任务入口,负责执行握手、认证、分包转发和结束流程。
|
||||
* @param pvParameters FreeRTOS 任务参数,未使用。
|
||||
*/
|
||||
void ota_relay_task(void *pvParameters);
|
||||
static void taskEntry(void *pvParameters);
|
||||
ota_result_t startHandshake();
|
||||
ota_result_t forwardFirmware();
|
||||
relay_state_t getState();
|
||||
void reset();
|
||||
|
||||
/**
|
||||
* @brief 发起与 GD32 的握手流程,建立 OTA 传输前的安全会话。
|
||||
* @return 操作结果码。
|
||||
*/
|
||||
ota_result_t ota_relay_start_handshake(void);
|
||||
static esp_err_t flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len);
|
||||
static esp_err_t flashReadChunk(uint32_t offset, uint8_t *data, size_t len);
|
||||
static esp_err_t metaLoad(ota_meta_t *meta);
|
||||
static esp_err_t metaSave(const ota_meta_t *meta);
|
||||
static esp_err_t metaUpdate(uint32_t stored_size, uint32_t last_seq);
|
||||
static esp_err_t metaClear();
|
||||
|
||||
/**
|
||||
* @brief 从 Flash 中读取已缓存固件并按协议转发给 GD32。
|
||||
* @return 操作结果码。
|
||||
*/
|
||||
ota_result_t ota_relay_forward_firmware(void);
|
||||
// 公开状态
|
||||
static relay_state_t relayState;
|
||||
static uint32_t relaySeq;
|
||||
static uint32_t relayTotalPackets;
|
||||
static volatile bool gd32AppAlive;
|
||||
static QueueHandle_t dispatchQueue;
|
||||
static QueueHandle_t resultQueue;
|
||||
|
||||
/**
|
||||
* @brief 获取当前 OTA 中继状态机状态。
|
||||
* @return 当前中继状态枚举值。
|
||||
*/
|
||||
relay_state_t ota_relay_get_state(void);
|
||||
// 实例状态(public,供状态机方法访问)
|
||||
const esp_partition_t *m_gd32Partition = NULL;
|
||||
session_ctx_t m_sessionCtx;
|
||||
uint8_t m_nonceServer[16] = {0};
|
||||
uint8_t m_kSession[16] = {0};
|
||||
uint8_t m_gd32UidRuntime[12] = {0};
|
||||
|
||||
/**
|
||||
* @brief 重置 OTA 中继状态机,清理当前状态以便重新开始流程。
|
||||
*/
|
||||
void ota_relay_reset(void);
|
||||
static constexpr size_t GD32_UID_LEN = 12;
|
||||
static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36;
|
||||
|
||||
/**
|
||||
* @brief 向 OTA 缓存分区写入一段固件数据。
|
||||
* @param offset 分区内写入偏移。
|
||||
* @param data 待写入的数据指针。
|
||||
* @param len 数据长度(字节)。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t flash_write_chunk(uint32_t offset, const uint8_t *data, size_t len);
|
||||
ota_result_t sendHandshakeReq();
|
||||
ota_result_t sendNonceAndAuthenticate();
|
||||
ota_result_t sendFirmwareSize(uint32_t size);
|
||||
ota_result_t relayPacketsFromFlash();
|
||||
ota_result_t finishOta();
|
||||
|
||||
/**
|
||||
* @brief 从 OTA 缓存分区读取一段固件数据。
|
||||
* @param offset 分区内读取偏移。
|
||||
* @param data 读取缓冲区指针。
|
||||
* @param len 读取长度(字节)。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t flash_read_chunk(uint32_t offset, uint8_t *data, size_t len);
|
||||
static int aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, uint8_t *out);
|
||||
static void logRespPayload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len);
|
||||
esp_err_t waitUntilNotWait(const char *stage, uint8_t *resp_type,
|
||||
uint8_t *resp_payload, size_t resp_max,
|
||||
size_t *resp_len, uint32_t timeout_ms,
|
||||
uint32_t max_wait_rounds);
|
||||
|
||||
/**
|
||||
* @brief 从持久化文件中加载 OTA 元数据。
|
||||
* @param meta 输出参数,加载后的元数据结构体。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t meta_load(ota_meta_t *meta);
|
||||
static bool isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32_t expected_seq);
|
||||
static bool parseStructuredError(const uint8_t *payload, size_t len, uint32_t *seq_out, uint8_t *err_out);
|
||||
static bool isHandshakeEchoAck(const uint8_t *payload, size_t len);
|
||||
static bool isChallengePayloadAck(const uint8_t *payload, size_t len);
|
||||
static bool isConfirmPlainAck(const uint8_t *payload, size_t len);
|
||||
static bool isSizeSeq0Ack(const uint8_t *payload, size_t len);
|
||||
static bool isFinishPlainAck(const uint8_t *payload, size_t len);
|
||||
static bool isTransferDoneAck(const uint8_t *payload, size_t len);
|
||||
static bool isStructuredAckWithPayload(const uint8_t *payload, size_t len);
|
||||
|
||||
/**
|
||||
* @brief 将 OTA 元数据保存到持久化文件。
|
||||
* @param meta 待保存的元数据结构体。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t meta_save(const ota_meta_t *meta);
|
||||
static esp_err_t pollUntilExpectedAckShape(const char *stage,
|
||||
uint8_t *resp_type,
|
||||
uint8_t *resp_payload,
|
||||
size_t resp_max,
|
||||
size_t *resp_len,
|
||||
uint32_t poll_timeout_ms,
|
||||
uint32_t wait_rounds,
|
||||
uint32_t maxExtraPolls,
|
||||
bool (*matcher)(const uint8_t *, size_t));
|
||||
static bool parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_t resp_len,
|
||||
uint8_t *out_k_session, uint8_t *out_uid);
|
||||
static bool aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, uint8_t *out);
|
||||
static void calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, uint8_t *output);
|
||||
|
||||
/**
|
||||
* @brief 更新已存储大小与最后分块序号等 OTA 进度信息。
|
||||
* @param stored_size 已存储的固件大小(字节)。
|
||||
* @param last_seq 最后成功的分块序号。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t meta_update(uint32_t stored_size, uint32_t last_seq);
|
||||
void requestGd32AppEnterBootloader();
|
||||
|
||||
/**
|
||||
* @brief 清空 OTA 元数据,通常在完成或中止后调用。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t meta_clear(void);
|
||||
|
||||
// GD32 APP 存活标志(OTA 完成后由 APP PING 确认)
|
||||
extern volatile bool g_gd32_app_alive;
|
||||
|
||||
// 当前中继状态(供健康监控外部访问)
|
||||
extern relay_state_t g_relay_state;
|
||||
extern uint32_t g_relay_seq;
|
||||
extern uint32_t g_relay_total_packets;
|
||||
private:
|
||||
OtaRelay() = default;
|
||||
~OtaRelay() = default;
|
||||
OtaRelay(const OtaRelay&) = delete;
|
||||
OtaRelay& operator=(const OtaRelay&) = delete;
|
||||
};
|
||||
|
||||
#endif // OTA_RELAY_H
|
||||
+132
-133
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
/*
|
||||
* ble_ota.cpp - BLE OTA GATT 服务器实现 (ESP-IDF 6.0 NimBLE)
|
||||
* 所有功能封装为 BleOta 类方法。
|
||||
* T5.1 - T5.5
|
||||
*
|
||||
* 通过 BLE GATT 接收上位机固件,支持两种目标:
|
||||
@@ -57,27 +58,25 @@
|
||||
// 前向声明(GAP event handler 在广播函数之前定义,需要声明)
|
||||
// ============================================================
|
||||
static void ble_ota_start_advertising(void);
|
||||
static void ble_ota_send_window_ack(uint8_t cmd, uint16_t seq, uint32_t completed_bytes);
|
||||
static void ble_ota_send_finish_event(uint8_t stage, uint32_t value);
|
||||
static void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes);
|
||||
static void sendFinishEvent(uint8_t stage, uint32_t value);
|
||||
|
||||
// ============================================================
|
||||
// 静态状态
|
||||
// ============================================================
|
||||
|
||||
static bool s_ble_ready = false; // NimBLE 同步完成
|
||||
static bool s_ble_connected = false; // GATT 客户端已连接
|
||||
/** @brief 当前 BLE 连接句柄。 */
|
||||
static uint16_t s_conn_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||
static uint16_t s_data_out_val_handle = 0; // DATA_OUT 值句柄 (供通知用)
|
||||
|
||||
// ============================================================
|
||||
BleOta& BleOta::instance()
|
||||
{
|
||||
static BleOta inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
// File-scope val_handle for GATT service definition (struct literal needs compile-time address)
|
||||
static uint16_t s_data_out_val_handle = 0;
|
||||
|
||||
// ESP32 自身 OTA 状态
|
||||
static bool s_esp32_ota_active = false;
|
||||
static esp_ota_handle_t s_esp32_ota_handle = 0;
|
||||
static const esp_partition_t *s_esp32_update_partition = NULL;
|
||||
static size_t s_esp32_received = 0;
|
||||
static bool s_esp32_reboot_pending = false;
|
||||
static uint8_t s_esp32_last_cmd = 0;
|
||||
static uint16_t s_esp32_last_seq = 0xFFFFU;
|
||||
|
||||
/**
|
||||
* @brief 异步完成 ESP32 OTA 收尾并触发重启。
|
||||
@@ -88,80 +87,80 @@ static void ble_esp32_finish_and_reboot_task(void *pvParameters)
|
||||
(void)pvParameters;
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 异步收尾任务启动");
|
||||
|
||||
if (s_esp32_last_seq != 0xFFFFU) {
|
||||
ble_ota_send_window_ack(s_esp32_last_cmd, s_esp32_last_seq, (uint32_t)s_esp32_received);
|
||||
DEBUG_INFO(TAG, "ESP32 最终 ACK 已补发: seq=%u", s_esp32_last_seq);
|
||||
if (BleOta::instance().m_esp32LastSeq != 0xFFFFU) {
|
||||
sendWindowAck(BleOta::instance().m_esp32LastCmd, BleOta::instance().m_esp32LastSeq, (uint32_t)BleOta::instance().m_esp32Received);
|
||||
DEBUG_INFO(TAG, "ESP32 最终 ACK 已补发: seq=%u", BleOta::instance().m_esp32LastSeq);
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(150));
|
||||
|
||||
if (!s_esp32_ota_active || s_esp32_update_partition == NULL) {
|
||||
if (!BleOta::instance().m_esp32OtaActive || BleOta::instance().m_esp32UpdatePartition == NULL) {
|
||||
DEBUG_WARN(TAG, "异步收尾时 OTA 状态无效,放弃重启");
|
||||
s_esp32_reboot_pending = false;
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 收尾: 共 %u 字节, 设置启动分区...", (unsigned int)s_esp32_received);
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 收尾: 共 %u 字节, 设置启动分区...", (unsigned int)BleOta::instance().m_esp32Received);
|
||||
|
||||
int64_t t0_us = esp_timer_get_time();
|
||||
DEBUG_INFO(TAG, "[FINISH] 即将执行 esp_ota_end()");
|
||||
ble_ota_send_finish_event(BLE_OTA_FINISH_BEGIN_OTA_END, 0);
|
||||
esp_err_t err = esp_ota_end(s_esp32_ota_handle);
|
||||
sendFinishEvent(BLE_OTA_FINISH_BEGIN_OTA_END, 0);
|
||||
esp_err_t err = esp_ota_end(BleOta::instance().m_esp32OtaHandle);
|
||||
int64_t t1_us = esp_timer_get_time();
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "[FINISH] esp_ota_end 失败: %s (耗时=%lld ms)",
|
||||
esp_err_to_name(err), (long long)((t1_us - t0_us) / 1000));
|
||||
ble_ota_send_finish_event(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)err);
|
||||
esp_ota_abort(s_esp32_ota_handle);
|
||||
s_esp32_ota_active = false;
|
||||
s_esp32_ota_handle = 0;
|
||||
s_esp32_update_partition = NULL;
|
||||
s_esp32_received = 0;
|
||||
s_esp32_reboot_pending = false;
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_OTA_END, (uint32_t)err);
|
||||
esp_ota_abort(BleOta::instance().m_esp32OtaHandle);
|
||||
BleOta::instance().m_esp32OtaActive = false;
|
||||
BleOta::instance().m_esp32OtaHandle = 0;
|
||||
BleOta::instance().m_esp32UpdatePartition = NULL;
|
||||
BleOta::instance().m_esp32Received = 0;
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
DEBUG_INFO(TAG, "[FINISH] esp_ota_end 完成 (耗时=%lld ms)",
|
||||
(long long)((t1_us - t0_us) / 1000));
|
||||
ble_ota_send_finish_event(BLE_OTA_FINISH_DONE_OTA_END, (uint32_t)((t1_us - t0_us) / 1000));
|
||||
sendFinishEvent(BLE_OTA_FINISH_DONE_OTA_END, (uint32_t)((t1_us - t0_us) / 1000));
|
||||
|
||||
int64_t t2_us = esp_timer_get_time();
|
||||
DEBUG_INFO(TAG, "[FINISH] 即将执行 esp_ota_set_boot_partition()");
|
||||
ble_ota_send_finish_event(BLE_OTA_FINISH_BEGIN_SET_BOOT, 0);
|
||||
err = esp_ota_set_boot_partition(s_esp32_update_partition);
|
||||
sendFinishEvent(BLE_OTA_FINISH_BEGIN_SET_BOOT, 0);
|
||||
err = esp_ota_set_boot_partition(BleOta::instance().m_esp32UpdatePartition);
|
||||
int64_t t3_us = esp_timer_get_time();
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "[FINISH] 设置启动分区失败: %s (耗时=%lld ms)",
|
||||
esp_err_to_name(err), (long long)((t3_us - t2_us) / 1000));
|
||||
ble_ota_send_finish_event(BLE_OTA_FINISH_ERR_SET_BOOT, (uint32_t)err);
|
||||
s_esp32_ota_active = false;
|
||||
s_esp32_ota_handle = 0;
|
||||
s_esp32_update_partition = NULL;
|
||||
s_esp32_received = 0;
|
||||
s_esp32_reboot_pending = false;
|
||||
sendFinishEvent(BLE_OTA_FINISH_ERR_SET_BOOT, (uint32_t)err);
|
||||
BleOta::instance().m_esp32OtaActive = false;
|
||||
BleOta::instance().m_esp32OtaHandle = 0;
|
||||
BleOta::instance().m_esp32UpdatePartition = NULL;
|
||||
BleOta::instance().m_esp32Received = 0;
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
DEBUG_INFO(TAG, "[FINISH] esp_ota_set_boot_partition 完成 (耗时=%lld ms)",
|
||||
(long long)((t3_us - t2_us) / 1000));
|
||||
ble_ota_send_finish_event(BLE_OTA_FINISH_DONE_SET_BOOT, (uint32_t)((t3_us - t2_us) / 1000));
|
||||
sendFinishEvent(BLE_OTA_FINISH_DONE_SET_BOOT, (uint32_t)((t3_us - t2_us) / 1000));
|
||||
|
||||
s_esp32_ota_active = false;
|
||||
s_esp32_ota_handle = 0;
|
||||
s_esp32_update_partition = NULL;
|
||||
s_esp32_received = 0;
|
||||
BleOta::instance().m_esp32OtaActive = false;
|
||||
BleOta::instance().m_esp32OtaHandle = 0;
|
||||
BleOta::instance().m_esp32UpdatePartition = NULL;
|
||||
BleOta::instance().m_esp32Received = 0;
|
||||
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 收尾完成,800ms 后重启...");
|
||||
int64_t t4_us = esp_timer_get_time();
|
||||
DEBUG_INFO(TAG, "[FINISH] 即将进入 800ms 重启前等待");
|
||||
ble_ota_send_finish_event(BLE_OTA_FINISH_BEGIN_DELAY, 800);
|
||||
sendFinishEvent(BLE_OTA_FINISH_BEGIN_DELAY, 800);
|
||||
vTaskDelay(pdMS_TO_TICKS(800));
|
||||
int64_t t5_us = esp_timer_get_time();
|
||||
DEBUG_INFO(TAG, "[FINISH] 800ms 等待结束 (实际耗时=%lld ms)",
|
||||
(long long)((t5_us - t4_us) / 1000));
|
||||
ble_ota_send_finish_event(BLE_OTA_FINISH_DONE_DELAY, (uint32_t)((t5_us - t4_us) / 1000));
|
||||
sendFinishEvent(BLE_OTA_FINISH_DONE_DELAY, (uint32_t)((t5_us - t4_us) / 1000));
|
||||
DEBUG_INFO(TAG, "ESP32 即将重启");
|
||||
ble_ota_send_finish_event(BLE_OTA_FINISH_REBOOT_NOW, 0);
|
||||
sendFinishEvent(BLE_OTA_FINISH_REBOOT_NOW, 0);
|
||||
esp_restart();
|
||||
}
|
||||
|
||||
@@ -194,56 +193,56 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size)
|
||||
DEBUG_INFO(TAG, "当前启动分区: %s, size=%u, address=0x%lx",
|
||||
boot_partition->label, boot_partition->size, boot_partition->address);
|
||||
|
||||
if (s_esp32_ota_active) {
|
||||
if (BleOta::instance().m_esp32OtaActive) {
|
||||
DEBUG_WARN(TAG, "检测到未完成的 ESP32 OTA,会先中止旧会话");
|
||||
esp_ota_abort(s_esp32_ota_handle);
|
||||
s_esp32_ota_active = false;
|
||||
s_esp32_ota_handle = 0;
|
||||
s_esp32_update_partition = NULL;
|
||||
s_esp32_received = 0;
|
||||
s_esp32_reboot_pending = false;
|
||||
s_esp32_last_seq = 0xFFFFU;
|
||||
esp_ota_abort(BleOta::instance().m_esp32OtaHandle);
|
||||
BleOta::instance().m_esp32OtaActive = false;
|
||||
BleOta::instance().m_esp32OtaHandle = 0;
|
||||
BleOta::instance().m_esp32UpdatePartition = NULL;
|
||||
BleOta::instance().m_esp32Received = 0;
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
BleOta::instance().m_esp32LastSeq = 0xFFFFU;
|
||||
}
|
||||
|
||||
s_esp32_update_partition = esp_partition_find_first(
|
||||
BleOta::instance().m_esp32UpdatePartition = esp_partition_find_first(
|
||||
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_0, NULL);
|
||||
if (s_esp32_update_partition == NULL ||
|
||||
s_esp32_update_partition->address == boot_partition->address) {
|
||||
s_esp32_update_partition = esp_partition_find_first(
|
||||
if (BleOta::instance().m_esp32UpdatePartition == NULL ||
|
||||
BleOta::instance().m_esp32UpdatePartition->address == boot_partition->address) {
|
||||
BleOta::instance().m_esp32UpdatePartition = esp_partition_find_first(
|
||||
ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL);
|
||||
}
|
||||
|
||||
if (s_esp32_update_partition == NULL) {
|
||||
if (BleOta::instance().m_esp32UpdatePartition == NULL) {
|
||||
DEBUG_ERR(TAG, "找不到可用的 OTA 分区");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 目标分区: %s, size=%u, address=0x%lx",
|
||||
s_esp32_update_partition->label,
|
||||
s_esp32_update_partition->size,
|
||||
s_esp32_update_partition->address);
|
||||
BleOta::instance().m_esp32UpdatePartition->label,
|
||||
BleOta::instance().m_esp32UpdatePartition->size,
|
||||
BleOta::instance().m_esp32UpdatePartition->address);
|
||||
|
||||
if (total_size > s_esp32_update_partition->size) {
|
||||
if (total_size > BleOta::instance().m_esp32UpdatePartition->size) {
|
||||
DEBUG_ERR(TAG, "ESP32 固件太大: %u > 分区大小: %u",
|
||||
total_size, s_esp32_update_partition->size);
|
||||
s_esp32_update_partition = NULL;
|
||||
total_size, BleOta::instance().m_esp32UpdatePartition->size);
|
||||
BleOta::instance().m_esp32UpdatePartition = NULL;
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t err = esp_ota_begin(s_esp32_update_partition, total_size, &s_esp32_ota_handle);
|
||||
esp_err_t err = esp_ota_begin(BleOta::instance().m_esp32UpdatePartition, total_size, &BleOta::instance().m_esp32OtaHandle);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "esp_ota_begin 失败: %s", esp_err_to_name(err));
|
||||
s_esp32_update_partition = NULL;
|
||||
s_esp32_ota_handle = 0;
|
||||
BleOta::instance().m_esp32UpdatePartition = NULL;
|
||||
BleOta::instance().m_esp32OtaHandle = 0;
|
||||
return err;
|
||||
}
|
||||
|
||||
s_esp32_received = 0;
|
||||
s_esp32_ota_active = true;
|
||||
s_esp32_reboot_pending = false;
|
||||
s_esp32_last_cmd = 0;
|
||||
s_esp32_last_seq = 0xFFFFU;
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s", s_esp32_update_partition->label);
|
||||
BleOta::instance().m_esp32Received = 0;
|
||||
BleOta::instance().m_esp32OtaActive = true;
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
BleOta::instance().m_esp32LastCmd = 0;
|
||||
BleOta::instance().m_esp32LastSeq = 0xFFFFU;
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 开始,目标分区: %s", BleOta::instance().m_esp32UpdatePartition->label);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@@ -255,12 +254,12 @@ static esp_err_t ble_esp32_ota_begin(size_t total_size)
|
||||
*/
|
||||
static esp_err_t ble_esp32_ota_write(const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!s_esp32_ota_active || s_esp32_update_partition == NULL) {
|
||||
if (!BleOta::instance().m_esp32OtaActive || BleOta::instance().m_esp32UpdatePartition == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
esp_err_t err = esp_ota_write(s_esp32_ota_handle, data, len);
|
||||
esp_err_t err = esp_ota_write(BleOta::instance().m_esp32OtaHandle, data, len);
|
||||
if (err == ESP_OK) {
|
||||
s_esp32_received += len;
|
||||
BleOta::instance().m_esp32Received += len;
|
||||
}
|
||||
return err;
|
||||
}
|
||||
@@ -271,7 +270,7 @@ static esp_err_t ble_esp32_ota_write(const uint8_t *data, size_t len)
|
||||
* @param seq 当前确认的分块序号。
|
||||
* @param completed_bytes 已完成写入的累计字节数。
|
||||
*/
|
||||
static void ble_ota_send_window_ack(uint8_t cmd, uint16_t seq, uint32_t completed_bytes)
|
||||
static void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes)
|
||||
{
|
||||
uint8_t ack[8] = {0};
|
||||
ack[0] = BLE_OTA_NOTIFY_ACK;
|
||||
@@ -283,7 +282,7 @@ static void ble_ota_send_window_ack(uint8_t cmd, uint16_t seq, uint32_t complete
|
||||
ack[6] = (uint8_t)((completed_bytes >> 16) & 0xFFU);
|
||||
ack[7] = (uint8_t)((completed_bytes >> 24) & 0xFFU);
|
||||
|
||||
esp_err_t err = ble_ota_notify_response(ack, sizeof(ack));
|
||||
esp_err_t err = BleOta::instance().notifyResponse(ack, sizeof(ack));
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN(TAG, "发送 ACK 失败: cmd=0x%02X seq=%u err=%s",
|
||||
cmd, seq, esp_err_to_name(err));
|
||||
@@ -298,7 +297,7 @@ static void ble_ota_send_window_ack(uint8_t cmd, uint16_t seq, uint32_t complete
|
||||
* @param stage 阶段标识码。
|
||||
* @param value 阶段附带值(错误码或耗时等)。
|
||||
*/
|
||||
static void ble_ota_send_finish_event(uint8_t stage, uint32_t value)
|
||||
static void sendFinishEvent(uint8_t stage, uint32_t value)
|
||||
{
|
||||
uint8_t pkt[6] = {0};
|
||||
pkt[0] = BLE_OTA_NOTIFY_FINISH;
|
||||
@@ -307,7 +306,7 @@ static void ble_ota_send_finish_event(uint8_t stage, uint32_t value)
|
||||
pkt[3] = (uint8_t)((value >> 8) & 0xFFU);
|
||||
pkt[4] = (uint8_t)((value >> 16) & 0xFFU);
|
||||
pkt[5] = (uint8_t)((value >> 24) & 0xFFU);
|
||||
esp_err_t err = ble_ota_notify_response(pkt, sizeof(pkt));
|
||||
esp_err_t err = BleOta::instance().notifyResponse(pkt, sizeof(pkt));
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN(TAG, "发送 FINISH 事件失败: stage=0x%02X err=%s",
|
||||
stage, esp_err_to_name(err));
|
||||
@@ -354,28 +353,28 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
if (buf[0] == BLE_OTA_CTRL_TRIGGER_GD32) {
|
||||
// 检查固件是否存储完毕 → 触发 GD32 OTA 中继
|
||||
ota_meta_t meta;
|
||||
esp_err_t err = meta_load(&meta);
|
||||
esp_err_t err = OtaRelay::metaLoad(&meta);
|
||||
if (err == ESP_OK &&
|
||||
meta.total_size > 0 &&
|
||||
meta.stored_size >= meta.total_size) {
|
||||
DEBUG_INFO(TAG, "GD32 固件就绪 (%" PRIu32 " / %" PRIu32 " 字节),触发中继",
|
||||
meta.stored_size, meta.total_size);
|
||||
ble_ota_signal_relay();
|
||||
BleOta::instance().signalRelay();
|
||||
} else {
|
||||
DEBUG_WARN(TAG, "GD32 固件未就绪 (stored=%" PRIu32 ", total=%" PRIu32 ")",
|
||||
meta.stored_size, meta.total_size);
|
||||
}
|
||||
} else if (buf[0] == BLE_OTA_CTRL_TRIGGER_ESP32) {
|
||||
if (!s_esp32_ota_active || s_esp32_update_partition == NULL) {
|
||||
if (!BleOta::instance().m_esp32OtaActive || BleOta::instance().m_esp32UpdatePartition == NULL) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 未激活,无法触发升级");
|
||||
return 0;
|
||||
}
|
||||
if (s_esp32_reboot_pending) {
|
||||
if (BleOta::instance().m_esp32RebootPending) {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA 收尾已在进行中,忽略重复 CTRL");
|
||||
return 0;
|
||||
}
|
||||
|
||||
s_esp32_reboot_pending = true;
|
||||
BleOta::instance().m_esp32RebootPending = true;
|
||||
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
|
||||
"ble_ota_finish",
|
||||
4096,
|
||||
@@ -384,7 +383,7 @@ static int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
NULL);
|
||||
if (ok != pdPASS) {
|
||||
DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败");
|
||||
s_esp32_reboot_pending = false;
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
} else {
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建(由设备端异步完成)");
|
||||
}
|
||||
@@ -485,21 +484,21 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
}
|
||||
|
||||
DEBUG_DBG(TAG, "ESP32 OTA 写入成功: seq=%u received=%u",
|
||||
seq, (unsigned int)s_esp32_received);
|
||||
seq, (unsigned int)BleOta::instance().m_esp32Received);
|
||||
|
||||
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
|
||||
(total_size > 0 && s_esp32_received >= total_size)) {
|
||||
ble_ota_send_window_ack(cmd, seq, (uint32_t)s_esp32_received);
|
||||
(total_size > 0 && BleOta::instance().m_esp32Received >= total_size)) {
|
||||
sendWindowAck(cmd, seq, (uint32_t)BleOta::instance().m_esp32Received);
|
||||
}
|
||||
|
||||
if (total_size > 0 && s_esp32_received >= total_size) {
|
||||
if (total_size > 0 && BleOta::instance().m_esp32Received >= total_size) {
|
||||
DEBUG_INFO(TAG, "★ ESP32 固件接收完毕: %u / %" PRIu32 " 字节 (共 %u 块)",
|
||||
(unsigned int)s_esp32_received, total_size, seq + 1);
|
||||
s_esp32_last_cmd = cmd;
|
||||
s_esp32_last_seq = seq;
|
||||
(unsigned int)BleOta::instance().m_esp32Received, total_size, seq + 1);
|
||||
BleOta::instance().m_esp32LastCmd = cmd;
|
||||
BleOta::instance().m_esp32LastSeq = seq;
|
||||
|
||||
if (!s_esp32_reboot_pending) {
|
||||
s_esp32_reboot_pending = true;
|
||||
if (!BleOta::instance().m_esp32RebootPending) {
|
||||
BleOta::instance().m_esp32RebootPending = true;
|
||||
BaseType_t ok = xTaskCreate(ble_esp32_finish_and_reboot_task,
|
||||
"ble_ota_finish",
|
||||
4096,
|
||||
@@ -508,7 +507,7 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
NULL);
|
||||
if (ok != pdPASS) {
|
||||
DEBUG_ERR(TAG, "创建 ESP32 OTA 收尾任务失败");
|
||||
s_esp32_reboot_pending = false;
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 收尾任务已创建,等待设备端自动重启");
|
||||
@@ -534,7 +533,7 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
meta.last_seq = 0;
|
||||
meta.chunk_count = (total_size + (uint32_t)data_len - 1U) / (uint32_t)data_len;
|
||||
|
||||
esp_err_t err = meta_save(&meta);
|
||||
esp_err_t err = OtaRelay::metaSave(&meta);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "元数据保存失败");
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
@@ -545,7 +544,7 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
}
|
||||
|
||||
ota_meta_t meta = {};
|
||||
if (meta_load(&meta) != ESP_OK || meta.total_size == 0) {
|
||||
if (OtaRelay::metaLoad(&meta) != ESP_OK || meta.total_size == 0) {
|
||||
DEBUG_ERR(TAG, "GD32 元数据无效,拒绝写入: seq=%u", seq);
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
@@ -565,7 +564,7 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
return BLE_ATT_ERR_UNLIKELY;
|
||||
}
|
||||
|
||||
esp_err_t err = flash_write_chunk(flash_offset, &buf[data_offset], data_len);
|
||||
esp_err_t err = OtaRelay::flashWriteChunk(flash_offset, &buf[data_offset], data_len);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "Flash 写入失败: seq=%u offset=%" PRIu32 " err=%s",
|
||||
seq, flash_offset, esp_err_to_name(err));
|
||||
@@ -573,21 +572,21 @@ static int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
}
|
||||
|
||||
uint32_t stored_size = flash_offset + (uint32_t)data_len;
|
||||
err = meta_update(stored_size, seq);
|
||||
err = OtaRelay::metaUpdate(stored_size, seq);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_WARN(TAG, "元数据更新失败 (seq=%u)", seq);
|
||||
}
|
||||
|
||||
if ((((uint32_t)seq + 1U) % BLE_OTA_ACK_WINDOW) == 0U ||
|
||||
(meta.total_size > 0 && stored_size >= meta.total_size)) {
|
||||
ble_ota_send_window_ack(cmd, seq, stored_size);
|
||||
sendWindowAck(cmd, seq, stored_size);
|
||||
}
|
||||
|
||||
DEBUG_DBG(TAG, "Flash 写入成功: seq=%u offset=%" PRIu32 " stored=%" PRIu32,
|
||||
seq, flash_offset, stored_size);
|
||||
|
||||
// --- 检查 GD32 固件是否接收完毕 ---
|
||||
if (meta_load(&meta) == ESP_OK &&
|
||||
if (OtaRelay::metaLoad(&meta) == ESP_OK &&
|
||||
meta.total_size > 0 &&
|
||||
meta.stored_size >= meta.total_size) {
|
||||
DEBUG_INFO(TAG, "★ GD32 固件接收完毕: %" PRIu32 " / %" PRIu32 " 字节 (共 %u 块)",
|
||||
@@ -627,7 +626,7 @@ static int ble_ota_version_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
|
||||
/*
|
||||
* DATA_OUT (0x0003) - Notify only
|
||||
* 访问回调不做读写处理,通知由 ble_ota_notify_response() 发送
|
||||
* 访问回调不做读写处理,通知由 BleOta::instance().notifyResponse() 发送
|
||||
*/
|
||||
/**
|
||||
* @brief GATT DATA_OUT 特征访问回调(Notify-only,占位实现)。
|
||||
@@ -727,10 +726,9 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
||||
case BLE_GAP_EVENT_CONNECT:
|
||||
if (event->connect.status == 0) {
|
||||
// 连接成功
|
||||
s_ble_connected = true;
|
||||
/** @brief 当前 BLE 连接句柄。 */
|
||||
s_conn_handle = event->connect.conn_handle;
|
||||
DEBUG_INFO(TAG, "BLE 客户端已连接: handle=%d", s_conn_handle);
|
||||
BleOta::instance().m_bleConnected = true;
|
||||
BleOta::instance().m_connHandle = event->connect.conn_handle;
|
||||
DEBUG_INFO(TAG, "BLE 客户端已连接: handle=%d", BleOta::instance().m_connHandle);
|
||||
|
||||
// 向对端请求更快的连接参数,提升 OTA 吞吐
|
||||
struct ble_gap_upd_params up = {};
|
||||
@@ -740,7 +738,7 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
||||
up.supervision_timeout = 2000; // 2 秒监控超时
|
||||
up.min_ce_len = 0; // 控制器自行决定
|
||||
up.max_ce_len = 0;
|
||||
int rc = ble_gap_update_params(s_conn_handle, &up);
|
||||
int rc = ble_gap_update_params(BleOta::instance().m_connHandle, &up);
|
||||
if (rc != 0) {
|
||||
DEBUG_WARN(TAG, "请求更新连接参数失败: rc=%d(对端不一定接受,继续运行)", rc);
|
||||
} else {
|
||||
@@ -750,7 +748,7 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
||||
} else {
|
||||
DEBUG_WARN(TAG, "BLE 连接失败: status=%d", event->connect.status);
|
||||
/** @brief BLE GATT 客户端连接状态。 */
|
||||
s_ble_connected = false;
|
||||
BleOta::instance().m_bleConnected = false;
|
||||
}
|
||||
return 0;
|
||||
|
||||
@@ -758,9 +756,8 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
||||
DEBUG_INFO(TAG, "BLE 客户端已断开: reason=%d",
|
||||
event->disconnect.reason);
|
||||
/** @brief BLE GATT 客户端连接状态。 */
|
||||
s_ble_connected = false;
|
||||
/** @brief 当前 BLE 连接句柄。 */
|
||||
s_conn_handle = BLE_HS_CONN_HANDLE_NONE;
|
||||
BleOta::instance().m_bleConnected = false;
|
||||
BleOta::instance().m_connHandle = BLE_HS_CONN_HANDLE_NONE;
|
||||
// 恢复广播以接受新连接
|
||||
ble_ota_start_advertising();
|
||||
return 0;
|
||||
@@ -794,7 +791,7 @@ static int ble_ota_gap_event(struct ble_gap_event *event, void *arg)
|
||||
/**
|
||||
* @brief 启动 BLE 广播,使设备可被发现和连接。
|
||||
*/
|
||||
static void ble_ota_start_advertising(void)
|
||||
static void ble_ota_start_advertising(void) // kept as free function for BLE callback compatibility
|
||||
{
|
||||
int rc;
|
||||
|
||||
@@ -883,10 +880,10 @@ static void ble_ota_on_sync(void)
|
||||
{
|
||||
DEBUG_INFO(TAG, "NimBLE 协议栈同步完成");
|
||||
|
||||
// GATT 服务已在 ble_ota_server_start() 中注册,不再重复注册
|
||||
// GATT 服务已在 serverStart() 中注册,不再重复注册
|
||||
// 重复调用 ble_gatts_count_cfg / ble_gatts_add_svcs 会导致 BLE_HS_EBUSY
|
||||
|
||||
s_ble_ready = true;
|
||||
BleOta::instance().m_bleReady = true;
|
||||
ble_ota_start_advertising();
|
||||
}
|
||||
|
||||
@@ -898,7 +895,7 @@ static void ble_ota_on_reset(int reason)
|
||||
{
|
||||
DEBUG_WARN(TAG, "NimBLE 协议栈重置: reason=%d", reason);
|
||||
/** @brief NimBLE 协议栈同步完成标志。 */
|
||||
s_ble_ready = false;
|
||||
BleOta::instance().m_bleReady = false;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -924,7 +921,7 @@ static void ble_ota_nimble_host_task(void *param)
|
||||
* @brief 启动 BLE GATT 服务器并注册 OTA 服务。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t ble_ota_server_start(void)
|
||||
esp_err_t BleOta::serverStart()
|
||||
{
|
||||
int rc;
|
||||
|
||||
@@ -976,6 +973,8 @@ esp_err_t ble_ota_server_start(void)
|
||||
|
||||
nimble_port_freertos_init(ble_ota_nimble_host_task);
|
||||
|
||||
// Sync val_handle to class member
|
||||
BleOta::instance().mDataOutValHandle = s_data_out_val_handle;
|
||||
DEBUG_INFO(TAG, "BLE OTA 服务器初始化完成");
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -986,12 +985,12 @@ esp_err_t ble_ota_server_start(void)
|
||||
* @param len 数据长度(字节)。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t ble_ota_notify_response(const uint8_t *data, size_t len)
|
||||
esp_err_t BleOta::notifyResponse(const uint8_t *data, size_t len)
|
||||
{
|
||||
if (!s_ble_connected) {
|
||||
if (!BleOta::instance().m_bleConnected) {
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
if (s_data_out_val_handle == 0) {
|
||||
if (BleOta::instance().mDataOutValHandle == 0) {
|
||||
DEBUG_WARN(TAG, "DATA_OUT 值句柄未设置");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
@@ -1004,7 +1003,7 @@ esp_err_t ble_ota_notify_response(const uint8_t *data, size_t len)
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
int rc = ble_gattc_notify_custom(s_conn_handle, s_data_out_val_handle, om);
|
||||
int rc = ble_gattc_notify_custom(BleOta::instance().m_connHandle, BleOta::instance().mDataOutValHandle, om);
|
||||
if (rc != 0) {
|
||||
DEBUG_WARN(TAG, "通知发送失败: rc=%d", rc);
|
||||
return ESP_FAIL;
|
||||
@@ -1018,24 +1017,24 @@ esp_err_t ble_ota_notify_response(const uint8_t *data, size_t len)
|
||||
* @brief 检查当前是否有 BLE 客户端连接。
|
||||
* @return true 已连接,false 未连接。
|
||||
*/
|
||||
bool ble_ota_is_connected(void)
|
||||
bool BleOta::isConnected()
|
||||
{
|
||||
/** @brief BLE GATT 客户端连接状态。 */
|
||||
return s_ble_connected;
|
||||
return BleOta::instance().m_bleConnected;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 通知中继模块开始向 GD32 转发固件。
|
||||
*/
|
||||
void ble_ota_signal_relay(void)
|
||||
void BleOta::signalRelay()
|
||||
{
|
||||
if (g_ota_dispatch_queue == NULL) {
|
||||
if (OtaRelay::dispatchQueue == NULL) {
|
||||
DEBUG_ERR(TAG, "OTA 调度队列未初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t msg = 1;
|
||||
BaseType_t ret = xQueueSend(g_ota_dispatch_queue, &msg, pdMS_TO_TICKS(200));
|
||||
BaseType_t ret = xQueueSend(OtaRelay::dispatchQueue, &msg, pdMS_TO_TICKS(200));
|
||||
if (ret != pdTRUE) {
|
||||
DEBUG_ERR(TAG, "OTA 调度信号发送失败: 队列满");
|
||||
return;
|
||||
@@ -1049,18 +1048,18 @@ void ble_ota_signal_relay(void)
|
||||
//
|
||||
// 注意: 此任务的职责是初始化 BLE 服务器并保持存活。
|
||||
// 实际的 BLE 事件循环在独立的 NimBLE Host 任务中运行。
|
||||
// 为避免与 ota_relay_task 竞争,此任务不从 g_ota_dispatch_queue 消费。
|
||||
// 为避免与 ota_relay_task 竞争,此任务不从 OtaRelay::dispatchQueue 消费。
|
||||
// ============================================================
|
||||
/**
|
||||
* @brief BLE OTA FreeRTOS 任务,初始化 NimBLE 并运行 GATT 服务。
|
||||
* @param pvParameters FreeRTOS 任务参数,未使用。
|
||||
*/
|
||||
void ble_ota_task(void *pvParameters)
|
||||
void BleOta::taskEntry(void *pvParameters)
|
||||
{
|
||||
(void)pvParameters;
|
||||
DEBUG_INFO(TAG, "=== BLE OTA 任务启动 ===");
|
||||
|
||||
esp_err_t err = ble_ota_server_start();
|
||||
esp_err_t err = BleOta::instance().serverStart();
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "BLE 服务器启动失败: %s → BLE OTA 不可用",
|
||||
esp_err_to_name(err));
|
||||
@@ -1075,8 +1074,8 @@ void ble_ota_task(void *pvParameters)
|
||||
if (heartbeat % 6 == 0) {
|
||||
DEBUG_DBG(TAG, "心跳 #%lu | BLE %s | 连接: %s",
|
||||
(unsigned long)(heartbeat / 6),
|
||||
s_ble_ready ? "就绪" : "未就绪",
|
||||
s_ble_connected ? "已连接" : "未连接");
|
||||
BleOta::instance().m_bleReady ? "就绪" : "未就绪",
|
||||
BleOta::instance().m_bleConnected ? "已连接" : "未连接");
|
||||
}
|
||||
|
||||
vTaskDelay(pdMS_TO_TICKS(10000));
|
||||
|
||||
+25
-29
@@ -1,41 +1,37 @@
|
||||
/*
|
||||
/*
|
||||
* ble_ota.h - BLE OTA GATT 服务器
|
||||
* T5.1 - T5.5
|
||||
*/
|
||||
#ifndef BLE_OTA_H
|
||||
#define BLE_OTA_H
|
||||
|
||||
#include "ota_relay.h"
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
#include "esp_ota_ops.h"
|
||||
|
||||
/**
|
||||
* @brief BLE OTA 任务入口,负责初始化 NimBLE 并运行 GATT OTA 服务。
|
||||
* @param pvParameters FreeRTOS 任务参数,未使用。
|
||||
*/
|
||||
void ble_ota_task(void *pvParameters);
|
||||
class BleOta {
|
||||
public:
|
||||
static BleOta& instance();
|
||||
|
||||
/**
|
||||
* @brief 启动 BLE GATT 服务器并注册 OTA 相关服务与特征。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t ble_ota_server_start(void);
|
||||
static void taskEntry(void *pvParameters);
|
||||
esp_err_t serverStart();
|
||||
esp_err_t notifyResponse(const uint8_t *data, size_t len);
|
||||
bool isConnected();
|
||||
void signalRelay();
|
||||
|
||||
/**
|
||||
* @brief 通过 BLE Notify 将 GD32 的响应数据发送给上位机 APP。
|
||||
* @param data 待推送的数据指针。
|
||||
* @param len 数据长度(字节)。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t ble_ota_notify_response(const uint8_t *data, size_t len);
|
||||
// 以下状态为 public,供 C 回调函数访问
|
||||
bool m_bleReady = false;
|
||||
bool m_bleConnected = false;
|
||||
uint16_t m_connHandle = 0xFFFF;
|
||||
uint16_t mDataOutValHandle = 0;
|
||||
|
||||
/**
|
||||
* @brief 检查当前是否存在已连接的 BLE 客户端。
|
||||
* @return true 已连接,false 未连接。
|
||||
*/
|
||||
bool ble_ota_is_connected(void);
|
||||
|
||||
/**
|
||||
* @brief 在固件存储完成后通知中继模块开始向 GD32 转发。
|
||||
*/
|
||||
void ble_ota_signal_relay(void);
|
||||
bool m_esp32OtaActive = false;
|
||||
esp_ota_handle_t m_esp32OtaHandle = 0;
|
||||
const esp_partition_t *m_esp32UpdatePartition = NULL;
|
||||
size_t m_esp32Received = 0;
|
||||
bool m_esp32RebootPending = false;
|
||||
uint8_t m_esp32LastCmd = 0;
|
||||
uint16_t m_esp32LastSeq = 0xFFFFU;
|
||||
};
|
||||
|
||||
#endif // BLE_OTA_H
|
||||
+191
-239
File diff suppressed because it is too large
Load Diff
+41
-35
@@ -1,52 +1,58 @@
|
||||
/*
|
||||
/*
|
||||
* wifi_ota.h - WiFi OTA 服务器
|
||||
* T4.1 - T4.5
|
||||
*
|
||||
* WifiOta 类封装 WiFi 初始化、HTTP OTA 服务器和网络状态查询。
|
||||
*/
|
||||
#ifndef WIFI_OTA_H
|
||||
#define WIFI_OTA_H
|
||||
|
||||
#include "ota_relay.h"
|
||||
#include "esp_ota_ops.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_event.h"
|
||||
|
||||
// WiFi 模式: AP 或 STA
|
||||
typedef enum {
|
||||
OTA_WIFI_MODE_AP, // AP 模式(ESP32 作为热点)
|
||||
OTA_WIFI_MODE_STA // STA 模式(连接路由器)
|
||||
OTA_WIFI_MODE_AP,
|
||||
OTA_WIFI_MODE_STA
|
||||
} ota_wifi_mode_t;
|
||||
|
||||
/**
|
||||
* @brief WiFi OTA 任务入口,负责 HTTP 服务、上传处理和 OTA 触发流程。
|
||||
* @param pvParameters FreeRTOS 任务参数,未使用。
|
||||
*/
|
||||
void wifi_ota_task(void *pvParameters);
|
||||
class WifiOta {
|
||||
public:
|
||||
static WifiOta& instance();
|
||||
|
||||
// 在 app_main 早期执行 WiFi 初始化并尝试以 STA 模式联网。
|
||||
/**
|
||||
* @brief 返回 true 表示 STA 初始化已开始,false 表示后续应回退到 AP 模式。
|
||||
* @return true STA 初始化中,false 需回退 AP。
|
||||
*/
|
||||
bool wifi_early_init(void);
|
||||
bool earlyInit();
|
||||
static void taskEntry(void *pvParameters);
|
||||
void signalRelay();
|
||||
|
||||
/**
|
||||
* @brief 在固件存储完成后通知 OTA 中继开始向 GD32 转发。
|
||||
*/
|
||||
void wifi_ota_signal_relay(void);
|
||||
static ota_wifi_mode_t getMode();
|
||||
static int8_t getRssi();
|
||||
static bool isConnected();
|
||||
|
||||
/**
|
||||
* @brief 获取当前 WiFi 工作模式(AP 或 STA)。
|
||||
* @return 当前 WiFi 模式枚举值。
|
||||
*/
|
||||
ota_wifi_mode_t wifi_get_mode(void);
|
||||
// 以下状态为 public,供 C 回调函数访问
|
||||
ota_wifi_mode_t m_wifiMode = OTA_WIFI_MODE_AP;
|
||||
bool m_coldBoot = false;
|
||||
bool m_relayTriggered = false;
|
||||
bool m_staConnected = false;
|
||||
int m_staRetryCount = 0;
|
||||
bool m_staRetryExhausted = false;
|
||||
SemaphoreHandle_t m_staConnectedSem = NULL;
|
||||
SemaphoreHandle_t m_staGotIpSem = NULL;
|
||||
|
||||
/**
|
||||
* @brief 获取当前 WiFi RSSI,未连接时通常返回无效/默认值。
|
||||
* @return 信号强度(dBm),未连接时返回无效值。
|
||||
*/
|
||||
int8_t wifi_get_rssi(void);
|
||||
bool m_esp32OtaActive = false;
|
||||
esp_ota_handle_t m_esp32OtaHandle = 0;
|
||||
const esp_partition_t *m_esp32UpdatePartition = NULL;
|
||||
size_t m_esp32Received = 0;
|
||||
bool m_gd32StorageDone = false;
|
||||
httpd_handle_t m_httpServer = NULL;
|
||||
|
||||
/**
|
||||
* @brief 判断当前 STA 模式下是否已经连接到上级 WiFi。
|
||||
* @return true 已连接,false 未连接。
|
||||
*/
|
||||
bool wifi_is_connected(void);
|
||||
void initAp();
|
||||
esp_err_t initSta();
|
||||
void setStaticIp();
|
||||
static void wifiEventHandler(void *arg, esp_event_base_t event_base,
|
||||
int32_t event_id, void *event_data);
|
||||
esp_err_t httpServerStart();
|
||||
esp_err_t waitStaConnected(char *ip_str, size_t ip_len);
|
||||
};
|
||||
|
||||
#endif // WIFI_OTA_H
|
||||
#endif // WIFI_OTA_H
|
||||
+54
-92
@@ -1,9 +1,11 @@
|
||||
/*
|
||||
* protocol.c - 协议层实现
|
||||
/*
|
||||
* protocol.cpp - 协议层实现
|
||||
* T2.1, T2.2, T1.3
|
||||
* ESP-IDF 6.0 适配版本 - 使用硬件 AES-128-CBC + 硬件 RNG
|
||||
*
|
||||
* 加密实现使用 ESP32-C3 硬件 AES 加速器(通过 mbedtls port)
|
||||
*
|
||||
* 所有功能封装为 Protocol 类的静态方法。
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
@@ -13,31 +15,30 @@
|
||||
#include "protocol.h"
|
||||
#include "monitor/debug_log.h"
|
||||
|
||||
/** @brief 协议层初始化标志。 */
|
||||
static bool s_protocol_initialized = false;
|
||||
|
||||
/**
|
||||
* @brief 初始化协议层,准备 CRC 和 AES 模块。
|
||||
*/
|
||||
void protocol_init(void)
|
||||
// ============================================================
|
||||
// Protocol 单例
|
||||
// ============================================================
|
||||
Protocol& Protocol::instance()
|
||||
{
|
||||
if (s_protocol_initialized) return;
|
||||
static Protocol inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
void Protocol::init()
|
||||
{
|
||||
if (m_initialized) return;
|
||||
|
||||
DEBUG_INFO("PROT", "=== 初始化协议层 ===");
|
||||
DEBUG_INFO("PROT", "协议层功能: CRC16 Modbus + 硬件 AES-128-CBC");
|
||||
|
||||
/** @brief 协议层初始化标志。 */
|
||||
s_protocol_initialized = true;
|
||||
m_initialized = true;
|
||||
DEBUG_INFO("PROT", "协议层就绪");
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief CRC16 Modbus(LE,低字节在前)
|
||||
* @param data 待校验的数据指针。
|
||||
* @param len 数据长度(字节)。
|
||||
* @return CRC16 校验值。
|
||||
*/
|
||||
uint16_t crc16_modbus(const uint8_t *data, size_t len)
|
||||
uint16_t Protocol::crc16Modbus(const uint8_t *data, size_t len)
|
||||
{
|
||||
uint16_t crc = 0xFFFF;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
@@ -55,13 +56,8 @@ uint16_t crc16_modbus(const uint8_t *data, size_t len)
|
||||
|
||||
/**
|
||||
* @brief 构建 SPI 帧: SOF + CMD + LEN(2LE) + PAYLOAD + CRC16(2LE)
|
||||
* @param cmd 命令码。
|
||||
* @param payload 负载数据指针,可为 NULL。
|
||||
* @param len 负载长度(字节)。
|
||||
* @param out_frame 输出帧缓冲区。
|
||||
* @return 帧总长度,失败返回 -1。
|
||||
*/
|
||||
int frame_build(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_frame)
|
||||
int Protocol::buildFrame(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_frame)
|
||||
{
|
||||
if (len > FRAME_MAX_PAYLOAD) {
|
||||
DEBUG_ERR("PROT", "Payload 太大: %zu > %d", len, FRAME_MAX_PAYLOAD);
|
||||
@@ -79,7 +75,7 @@ int frame_build(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_fr
|
||||
idx += len;
|
||||
}
|
||||
|
||||
uint16_t crc = crc16_modbus(out_frame, idx);
|
||||
uint16_t crc = crc16Modbus(out_frame, idx);
|
||||
out_frame[idx++] = (uint8_t)(crc & 0xFF);
|
||||
out_frame[idx++] = (uint8_t)((crc >> 8) & 0xFF);
|
||||
|
||||
@@ -90,15 +86,9 @@ int frame_build(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_fr
|
||||
|
||||
/**
|
||||
* @brief 解析并校验一帧 SPI 协议数据。
|
||||
* @param frame 输入帧缓冲区。
|
||||
* @param frame_len 输入帧长度。
|
||||
* @param out_cmd 输出命令字节,可为 NULL。
|
||||
* @param out_payload 输出负载缓冲区,可为 NULL。
|
||||
* @param out_len 输出负载长度指针,可为 NULL。
|
||||
* @return 0 成功,-1 失败。
|
||||
*/
|
||||
int frame_parse(const uint8_t *frame, size_t frame_len,
|
||||
uint8_t *out_cmd, uint8_t *out_payload, size_t *out_len)
|
||||
int Protocol::parseFrame(const uint8_t *frame, size_t frame_len,
|
||||
uint8_t *out_cmd, uint8_t *out_payload, size_t *out_len)
|
||||
{
|
||||
if (frame_len < 6) {
|
||||
DEBUG_WARN("PROT", "帧太短: %zu < 6", frame_len);
|
||||
@@ -129,7 +119,7 @@ int frame_parse(const uint8_t *frame, size_t frame_len,
|
||||
}
|
||||
|
||||
uint16_t received_crc = frame[idx + payload_len] | (frame[idx + payload_len + 1] << 8);
|
||||
uint16_t calc_crc = crc16_modbus(frame, idx + payload_len);
|
||||
uint16_t calc_crc = crc16Modbus(frame, idx + payload_len);
|
||||
|
||||
if (received_crc != calc_crc) {
|
||||
DEBUG_WARN("PROT", "CRC 不匹配: 收到=0x%04X, 计算=0x%04X", received_crc, calc_crc);
|
||||
@@ -151,14 +141,8 @@ int frame_parse(const uint8_t *frame, size_t frame_len,
|
||||
|
||||
/**
|
||||
* @brief 使用 AES-128 CBC 对明文进行加密(PKCS#7 填充)。
|
||||
* @param plaintext 明文数据指针。
|
||||
* @param len 明文长度。
|
||||
* @param key 16 字节密钥。
|
||||
* @param iv 16 字节初始向量。
|
||||
* @param ciphertext_out 输出密文缓冲区。
|
||||
* @return >=0 为密文长度,-1 表示失败。
|
||||
*/
|
||||
int protocol_aes_encrypt(const uint8_t *plaintext, size_t len,
|
||||
int Protocol::aesEncrypt(const uint8_t *plaintext, size_t len,
|
||||
const uint8_t *key, const uint8_t *iv,
|
||||
uint8_t *ciphertext_out)
|
||||
{
|
||||
@@ -168,15 +152,18 @@ int protocol_aes_encrypt(const uint8_t *plaintext, size_t len,
|
||||
}
|
||||
|
||||
// PKCS#7 填充
|
||||
size_t padded_len = ((len + 15) / 16) * 16;
|
||||
uint8_t padded[256];
|
||||
memset(padded, 0, sizeof(padded));
|
||||
memcpy(padded, plaintext, len);
|
||||
uint8_t pad = (uint8_t)(16 - (len % 16));
|
||||
if (pad == 0) pad = 16;
|
||||
memset(&padded[len], pad, pad);
|
||||
size_t padded_len = ((len / 16) + 1) * 16;
|
||||
uint8_t padded[FRAME_MAX_PAYLOAD + 16];
|
||||
if (padded_len > sizeof(padded)) {
|
||||
DEBUG_ERR("PROT", "填充后长度超出缓冲区: %zu", padded_len);
|
||||
return -1;
|
||||
}
|
||||
|
||||
DEBUG_DBG("PROT", "AES 加密: 明文=%zu 字节, 填充后=%zu 字节, 填充值=0x%02X", len, padded_len, pad);
|
||||
memcpy(padded, plaintext, len);
|
||||
uint8_t pad_val = (uint8_t)(padded_len - len);
|
||||
memset(&padded[len], pad_val, pad_val);
|
||||
|
||||
DEBUG_DBG("PROT", "AES 加密: 明文=%zu 字节, 填充后=%zu 字节", len, padded_len);
|
||||
|
||||
// 硬件 AES-CBC 加密(使用 ESP32-C3 硬件 AES 加速器)
|
||||
esp_aes_context aes_ctx;
|
||||
@@ -196,25 +183,20 @@ int protocol_aes_encrypt(const uint8_t *plaintext, size_t len,
|
||||
|
||||
esp_aes_free(&aes_ctx);
|
||||
|
||||
if (ret == 0) {
|
||||
DEBUG_DBG("PROT", "AES 加密成功: %zu -> %zu 字节", len, padded_len);
|
||||
return padded_len;
|
||||
} else {
|
||||
if (ret != 0) {
|
||||
DEBUG_ERR("PROT", "esp_aes_crypt_cbc 加密失败: %d", ret);
|
||||
return -1;
|
||||
}
|
||||
|
||||
DEBUG_DBG("PROT", "AES 加密成功: %zu -> %zu 字节", len, padded_len);
|
||||
|
||||
return padded_len;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 使用 AES-128 CBC 解密密文并移除 PKCS#7 填充。
|
||||
* @param ciphertext 密文数据指针。
|
||||
* @param len 密文长度(必须是 16 的倍数)。
|
||||
* @param key 16 字节密钥。
|
||||
* @param iv 16 字节初始向量。
|
||||
* @param plaintext_out 输出明文缓冲区。
|
||||
* @return >=0 为明文长度,-1 表示失败。
|
||||
* @brief 使用 AES-128 CBC 对密文进行解密(去除 PKCS#7 填充)。
|
||||
*/
|
||||
int protocol_aes_decrypt(const uint8_t *ciphertext, size_t len,
|
||||
int Protocol::aesDecrypt(const uint8_t *ciphertext, size_t len,
|
||||
const uint8_t *key, const uint8_t *iv,
|
||||
uint8_t *plaintext_out)
|
||||
{
|
||||
@@ -268,13 +250,9 @@ int protocol_aes_decrypt(const uint8_t *ciphertext, size_t len,
|
||||
|
||||
/**
|
||||
* @brief 根据时间戳、地址和随机种子生成 16 字节 nonce。
|
||||
* @param timestamp 时间戳输入。
|
||||
* @param mem_addr 地址输入。
|
||||
* @param rand_seed 随机种子输入。
|
||||
* @param nonce_out 输出 nonce 缓冲区(16 字节)。
|
||||
*/
|
||||
void scramble_generate(uint32_t timestamp, uint32_t mem_addr,
|
||||
uint32_t rand_seed, uint8_t *nonce_out)
|
||||
void Protocol::scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
|
||||
uint32_t rand_seed, uint8_t *nonce_out)
|
||||
{
|
||||
uint8_t state[16];
|
||||
|
||||
@@ -302,11 +280,8 @@ void scramble_generate(uint32_t timestamp, uint32_t mem_addr,
|
||||
|
||||
/**
|
||||
* @brief 会话密钥初始化
|
||||
* @param sess 待初始化的会话上下文。
|
||||
* @param k_session 128 位会话密钥。
|
||||
* @return 0 成功,-1 失败。
|
||||
*/
|
||||
int session_key_init(session_ctx_t *sess, const uint8_t *k_session)
|
||||
int Protocol::sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session)
|
||||
{
|
||||
if (!sess || !k_session) return -1;
|
||||
|
||||
@@ -319,10 +294,9 @@ int session_key_init(session_ctx_t *sess, const uint8_t *k_session)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 清除会话上下文中的密钥和状态。
|
||||
* @param sess 待清除的会话上下文。
|
||||
* @brief 清除会话上下文中的密钥和状态。
|
||||
*/
|
||||
void session_key_clear(session_ctx_t *sess)
|
||||
void Protocol::sessionKeyClear(session_ctx_t *sess)
|
||||
{
|
||||
if (sess) {
|
||||
memset(sess->key, 0, 16);
|
||||
@@ -332,21 +306,16 @@ void session_key_clear(session_ctx_t *sess)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 使用会话上下文加密数据。
|
||||
* @param sess 会话上下文。
|
||||
* @param in 输入明文。
|
||||
* @param len 数据长度。
|
||||
* @param out 输出密文缓冲区。
|
||||
* @return 密文长度,失败返回 -1。
|
||||
* @brief 使用会话上下文加密数据。
|
||||
*/
|
||||
int session_encrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out)
|
||||
int Protocol::sessionEncrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out)
|
||||
{
|
||||
if (!sess || !in || !out) return -1;
|
||||
|
||||
uint8_t iv_copy[16];
|
||||
memcpy(iv_copy, sess->iv, 16);
|
||||
|
||||
int ret = protocol_aes_encrypt(in, len, sess->key, iv_copy, out);
|
||||
int ret = aesEncrypt(in, len, sess->key, iv_copy, out);
|
||||
|
||||
if (ret > 0) {
|
||||
memcpy(sess->iv, &out[ret - 16], 16);
|
||||
@@ -356,21 +325,16 @@ int session_encrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 使用会话上下文解密数据。
|
||||
* @param sess 会话上下文。
|
||||
* @param in 输入密文。
|
||||
* @param len 数据长度。
|
||||
* @param out 输出明文缓冲区。
|
||||
* @return 明文长度,失败返回 -1。
|
||||
* @brief 使用会话上下文解密数据。
|
||||
*/
|
||||
int session_decrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out)
|
||||
int Protocol::sessionDecrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out)
|
||||
{
|
||||
if (!sess || !in || !out) return -1;
|
||||
|
||||
uint8_t iv_copy[16];
|
||||
memcpy(iv_copy, sess->iv, 16);
|
||||
|
||||
int ret = protocol_aes_decrypt(in, len, sess->key, iv_copy, out);
|
||||
int ret = aesDecrypt(in, len, sess->key, iv_copy, out);
|
||||
|
||||
if (ret > 0) {
|
||||
memcpy(sess->iv, &in[len - 16], 16);
|
||||
@@ -381,10 +345,8 @@ int session_decrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t
|
||||
|
||||
/**
|
||||
* @brief 使用 ESP-IDF 硬件 RNG 生成随机数
|
||||
* @param nonce 输出缓冲区。
|
||||
* @param len 随机数字节数。
|
||||
*/
|
||||
void generate_random_nonce(uint8_t *nonce, size_t len)
|
||||
void Protocol::generateRandomNonce(uint8_t *nonce, size_t len)
|
||||
{
|
||||
esp_fill_random(nonce, len);
|
||||
}
|
||||
}
|
||||
+35
-106
@@ -1,7 +1,9 @@
|
||||
/*
|
||||
/*
|
||||
* protocol.h - 帧格式、CRC16、AES 加密解密
|
||||
* T2.2, T1.3
|
||||
* ESP-IDF 6.0 适配版本 - 使用硬件 AES-128-CBC + 硬件 RNG
|
||||
*
|
||||
* Protocol 类封装协议层功能。
|
||||
*/
|
||||
#ifndef PROTOCOL_H
|
||||
#define PROTOCOL_H
|
||||
@@ -50,115 +52,42 @@ typedef struct {
|
||||
} session_ctx_t;
|
||||
|
||||
/**
|
||||
* @brief 初始化协议层资源,建立 CRC/AES 等基础能力的可用状态。
|
||||
* @brief 协议层类(单例)
|
||||
*
|
||||
* 封装 CRC16、AES-128-CBC、帧构建/解析、会话管理等功能。
|
||||
* 大部分方法为静态工具方法,单例仅持有初始化标志。
|
||||
*/
|
||||
void protocol_init(void);
|
||||
class Protocol {
|
||||
public:
|
||||
static Protocol& instance();
|
||||
|
||||
/**
|
||||
* @brief 计算一段数据的 CRC16-Modbus 校验值。
|
||||
* @param data 待校验的数据指针。
|
||||
* @param len 数据长度(字节)。
|
||||
* @return CRC16 校验值。
|
||||
*/
|
||||
uint16_t crc16_modbus(const uint8_t *data, size_t len);
|
||||
void init();
|
||||
|
||||
/**
|
||||
* @brief 按协议格式构建一帧数据,返回生成后的总帧长度。
|
||||
* @param cmd 命令码。
|
||||
* @param payload 负载数据指针,可为 NULL。
|
||||
* @param len 负载长度(字节)。
|
||||
* @param out_frame 输出帧缓冲区。
|
||||
* @return 帧总长度,失败返回 -1。
|
||||
*/
|
||||
int frame_build(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_frame);
|
||||
static uint16_t crc16Modbus(const uint8_t *data, size_t len);
|
||||
static int buildFrame(uint8_t cmd, const uint8_t *payload, size_t len, uint8_t *out_frame);
|
||||
static int parseFrame(const uint8_t *frame, size_t frame_len,
|
||||
uint8_t *out_cmd, uint8_t *out_payload, size_t *out_len);
|
||||
static int aesEncrypt(const uint8_t *plaintext, size_t len,
|
||||
const uint8_t *key, const uint8_t *iv,
|
||||
uint8_t *ciphertext_out);
|
||||
static int aesDecrypt(const uint8_t *ciphertext, size_t len,
|
||||
const uint8_t *key, const uint8_t *iv,
|
||||
uint8_t *plaintext_out);
|
||||
static void scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
|
||||
uint32_t rand_seed, uint8_t *nonce_out);
|
||||
static int sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session);
|
||||
static void sessionKeyClear(session_ctx_t *sess);
|
||||
static int sessionEncrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out);
|
||||
static int sessionDecrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out);
|
||||
static void generateRandomNonce(uint8_t *nonce, size_t len);
|
||||
|
||||
/**
|
||||
* @brief 解析一帧协议数据,提取命令字、负载和长度,失败返回 -1。
|
||||
* @param frame 待解析的帧数据。
|
||||
* @param frame_len 帧数据总长度。
|
||||
* @param out_cmd 输出参数,解析出的命令码。
|
||||
* @param out_payload 输出缓冲区,存放解析出的负载。
|
||||
* @param out_len 输出参数,负载实际长度。
|
||||
* @return 0 成功,-1 失败。
|
||||
*/
|
||||
int frame_parse(const uint8_t *frame, size_t frame_len,
|
||||
uint8_t *out_cmd, uint8_t *out_payload, size_t *out_len);
|
||||
private:
|
||||
Protocol() = default;
|
||||
~Protocol() = default;
|
||||
Protocol(const Protocol&) = delete;
|
||||
Protocol& operator=(const Protocol&) = delete;
|
||||
|
||||
/**
|
||||
* @brief 使用 AES-128-CBC 对明文进行加密,返回密文长度,失败返回负值。
|
||||
* @param plaintext 明文数据。
|
||||
* @param len 明文长度(字节)。
|
||||
* @param key 128 位 AES 密钥。
|
||||
* @param iv 128 位 CBC 初始向量。
|
||||
* @param ciphertext_out 输出缓冲区,存放密文。
|
||||
* @return 密文长度,失败返回 -1。
|
||||
*/
|
||||
int protocol_aes_encrypt(const uint8_t *plaintext, size_t len,
|
||||
const uint8_t *key, const uint8_t *iv,
|
||||
uint8_t *ciphertext_out);
|
||||
|
||||
/**
|
||||
* @brief 使用 AES-128-CBC 对密文进行解密,返回去除填充后的明文长度。
|
||||
* @param ciphertext 密文数据。
|
||||
* @param len 密文长度(字节,须为 16 的倍数)。
|
||||
* @param key 128 位 AES 密钥。
|
||||
* @param iv 128 位 CBC 初始向量。
|
||||
* @param plaintext_out 输出缓冲区,存放解密后的明文。
|
||||
* @return 明文长度,失败返回 -1。
|
||||
*/
|
||||
int protocol_aes_decrypt(const uint8_t *ciphertext, size_t len,
|
||||
const uint8_t *key, const uint8_t *iv,
|
||||
uint8_t *plaintext_out);
|
||||
|
||||
/**
|
||||
* @brief 根据时间戳、内存地址和随机种子生成 16 字节随机数。
|
||||
* @param timestamp 时间戳种子。
|
||||
* @param mem_addr 内存地址种子。
|
||||
* @param rand_seed 随机数种子。
|
||||
* @param nonce_out 输出缓冲区,存放生成的 16 字节随机数。
|
||||
*/
|
||||
void scramble_generate(uint32_t timestamp, uint32_t mem_addr,
|
||||
uint32_t rand_seed, uint8_t *nonce_out);
|
||||
|
||||
/**
|
||||
* @brief 用给定会话密钥初始化会话上下文。
|
||||
* @param sess 待初始化的会话上下文。
|
||||
* @param k_session 128 位会话密钥。
|
||||
* @return 0 成功,-1 失败。
|
||||
*/
|
||||
int session_key_init(session_ctx_t *sess, const uint8_t *k_session);
|
||||
|
||||
/**
|
||||
* @brief 清除会话上下文中的密钥与状态信息。
|
||||
* @param sess 待清除的会话上下文。
|
||||
*/
|
||||
void session_key_clear(session_ctx_t *sess);
|
||||
|
||||
/**
|
||||
* @brief 使用当前会话上下文对输入数据执行加密。
|
||||
* @param sess 会话上下文。
|
||||
* @param in 输入明文。
|
||||
* @param len 数据长度。
|
||||
* @param out 输出密文缓冲区。
|
||||
* @return 密文长度,失败返回 -1。
|
||||
*/
|
||||
int session_encrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out);
|
||||
|
||||
/**
|
||||
* @brief 使用当前会话上下文对输入数据执行解密。
|
||||
* @param sess 会话上下文。
|
||||
* @param in 输入密文。
|
||||
* @param len 数据长度。
|
||||
* @param out 输出明文缓冲区。
|
||||
* @return 明文长度,失败返回 -1。
|
||||
*/
|
||||
int session_decrypt(session_ctx_t *sess, const uint8_t *in, size_t len, uint8_t *out);
|
||||
|
||||
/**
|
||||
* @brief 使用 ESP-IDF 硬件 RNG 生成指定长度的随机数。
|
||||
* @param nonce 输出缓冲区。
|
||||
* @param len 随机数字节数。
|
||||
*/
|
||||
void generate_random_nonce(uint8_t *nonce, size_t len);
|
||||
bool m_initialized = false;
|
||||
};
|
||||
|
||||
#endif // PROTOCOL_H
|
||||
+227
-462
@@ -1,8 +1,10 @@
|
||||
/*
|
||||
* spi_master.c - SPI Master 驱动实现
|
||||
/*
|
||||
* spi_master.cpp - SPI Master 驱动实现
|
||||
* T1.1 - T1.4
|
||||
* ESP-IDF 6.0 适配版本
|
||||
* CANFD 3通道发送接口
|
||||
*
|
||||
* 所有功能封装为 SpiMaster 类方法。
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
@@ -15,13 +17,24 @@
|
||||
#include "spi_master.h"
|
||||
#include "monitor/debug_log.h"
|
||||
|
||||
namespace {
|
||||
constexpr size_t kSpiTransferMaxBytes = 256U;
|
||||
static constexpr size_t kSpiTransferMaxBytes = 256U;
|
||||
|
||||
static esp_err_t spi_transfer_clocked(const uint8_t *tx_buf, size_t tx_len,
|
||||
uint8_t *rx_buf, size_t rx_len)
|
||||
// ============================================================
|
||||
// 单例
|
||||
// ============================================================
|
||||
SpiMaster& SpiMaster::instance()
|
||||
{
|
||||
if (g_spi_device == NULL) {
|
||||
static SpiMaster inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 内部 SPI 传输
|
||||
// ============================================================
|
||||
esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len,
|
||||
uint8_t *rx_buf, size_t rx_len)
|
||||
{
|
||||
if (m_spiDevice == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
@@ -60,16 +73,17 @@ static esp_err_t spi_transfer_clocked(const uint8_t *tx_buf, size_t tx_len,
|
||||
t.tx_buffer = tx_ptr;
|
||||
t.rx_buffer = (rx_len > 0U) ? rx_ptr : NULL;
|
||||
|
||||
esp_err_t ret = spi_device_transmit(g_spi_device, &t);
|
||||
esp_err_t ret = spi_device_transmit(m_spiDevice, &t);
|
||||
|
||||
if (ret == ESP_OK && rx_len > 0U && rx_ptr != rx_buf) {
|
||||
memcpy(rx_buf, rx_ptr, rx_len);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t spi_transfer_request_response(uint8_t cmd,
|
||||
const uint8_t *payload, size_t payload_len,
|
||||
uint8_t *rx_buf, size_t rx_len)
|
||||
esp_err_t SpiMaster::transferRequestResponse(uint8_t cmd,
|
||||
const uint8_t *payload, size_t payload_len,
|
||||
uint8_t *rx_buf, size_t rx_len)
|
||||
{
|
||||
if ((payload_len > 0U && payload == NULL) || (rx_len > 0U && rx_buf == NULL)) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
@@ -99,23 +113,19 @@ static esp_err_t spi_transfer_request_response(uint8_t cmd,
|
||||
memset(&tx_staging[tx_pos], 0xFF, total_len - tx_pos);
|
||||
}
|
||||
|
||||
esp_err_t ret = spi_transfer_clocked(tx_staging, total_len,
|
||||
(rx_len > 0U) ? rx_staging : NULL,
|
||||
(rx_len > 0U) ? total_len : 0U);
|
||||
esp_err_t ret = transferClocked(tx_staging, total_len,
|
||||
(rx_len > 0U) ? rx_staging : NULL,
|
||||
(rx_len > 0U) ? total_len : 0U);
|
||||
if (ret == ESP_OK && rx_len > 0U) {
|
||||
memcpy(rx_buf, &rx_staging[cmd_len + payload_len], rx_len);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief 计算 SPI OTA 帧头和负载的异或 CRC8。
|
||||
* @param data 输入数据指针。
|
||||
* @param len 输入长度。
|
||||
* @return 计算得到的 CRC8 值。
|
||||
*/
|
||||
static uint8_t spi_ota_crc8(const uint8_t *data, size_t len)
|
||||
// ============================================================
|
||||
// OTA 帧工具
|
||||
// ============================================================
|
||||
uint8_t SpiMaster::otaCrc8(const uint8_t *data, size_t len)
|
||||
{
|
||||
uint8_t crc = 0x00;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
@@ -124,17 +134,8 @@ static uint8_t spi_ota_crc8(const uint8_t *data, size_t len)
|
||||
return crc;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 构建 SPI OTA 协议帧(SOF + TYPE + LEN + PAYLOAD + CRC)。
|
||||
* @param type 帧类型。
|
||||
* @param payload 负载数据指针,可为 NULL。
|
||||
* @param payload_len 负载长度。
|
||||
* @param out 输出帧缓冲区。
|
||||
* @param out_len 输出帧长度。
|
||||
* @return 构建成功返回 true,否则返回 false。
|
||||
*/
|
||||
static bool spi_ota_build_frame(uint8_t type, const uint8_t *payload, size_t payload_len,
|
||||
uint8_t *out, size_t *out_len)
|
||||
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;
|
||||
@@ -146,91 +147,67 @@ static bool spi_ota_build_frame(uint8_t type, const uint8_t *payload, size_t pay
|
||||
if (payload && payload_len > 0) {
|
||||
memcpy(&out[4], payload, payload_len);
|
||||
}
|
||||
out[4 + payload_len] = spi_ota_crc8(out, 4 + payload_len);
|
||||
out[4 + payload_len] = otaCrc8(out, 4 + payload_len);
|
||||
*out_len = total;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 解析并校验一帧 SPI OTA 数据。
|
||||
* @param frame 输入帧指针。
|
||||
* @param frame_len 输入帧缓冲区长度。
|
||||
* @param type 输出帧类型,可为 NULL。
|
||||
* @param payload 输出负载指针,可为 NULL。
|
||||
* @param payload_len 输出负载长度,可为 NULL。
|
||||
* @return 解析并校验成功返回 true,否则返回 false。
|
||||
*/
|
||||
static bool spi_ota_parse_frame(const uint8_t *frame, size_t frame_len,
|
||||
uint8_t *type, const uint8_t **payload, size_t *payload_len)
|
||||
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 || frame[0] != SPI_OTA_SOF) return false;
|
||||
size_t len = (size_t)frame[2] | ((size_t)frame[3] << 8);
|
||||
size_t expected = 5 + len;
|
||||
if (expected > frame_len) return false;
|
||||
uint8_t crc = spi_ota_crc8(frame, 4 + len);
|
||||
if (crc != frame[4 + len]) return false;
|
||||
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 = &frame[4];
|
||||
if (payload_len) *payload_len = len;
|
||||
if (payload) *payload = (plen > 0) ? &frame[4] : nullptr;
|
||||
if (payload_len) *payload_len = plen;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 在接收缓冲区中扫描并定位第一帧有效 SPI OTA 数据。
|
||||
* @param buf 输入缓冲区。
|
||||
* @param buf_len 缓冲区长度。
|
||||
* @param type 输出帧类型,可为 NULL。
|
||||
* @param payload 输出负载指针,可为 NULL。
|
||||
* @param payload_len 输出负载长度,可为 NULL。
|
||||
* @param frame_offset 输出帧起始偏移,可为 NULL。
|
||||
* @return 找到有效帧返回 true,否则返回 false。
|
||||
*/
|
||||
static bool spi_ota_find_frame(const uint8_t *buf, size_t buf_len,
|
||||
uint8_t *type, const uint8_t **payload, size_t *payload_len,
|
||||
size_t *frame_offset)
|
||||
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)
|
||||
{
|
||||
if (!buf || buf_len < 5) return false;
|
||||
for (size_t i = 0; i + 5 <= buf_len; ++i) {
|
||||
if (buf[i] != SPI_OTA_SOF) continue;
|
||||
size_t len_field = (size_t)buf[i+2] | ((size_t)buf[i+3] << 8);
|
||||
size_t needed = 5 + len_field;
|
||||
if (i + needed > buf_len) continue;
|
||||
uint8_t crc = spi_ota_crc8(&buf[i], 4 + len_field);
|
||||
if (crc != buf[i + 4 + len_field]) {
|
||||
DEBUG_DBG("SPI", "帧@%u CRC错误: calc=0x%02X recv=0x%02X type=0x%02X len=%u",
|
||||
(unsigned)i, crc, buf[i + 4 + len_field], buf[i+1], (unsigned)len_field);
|
||||
continue;
|
||||
}
|
||||
if (spi_ota_parse_frame(&buf[i], buf_len - i, type, payload, payload_len)) {
|
||||
if (frame_offset) *frame_offset = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
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) 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 从设备句柄。 */
|
||||
spi_device_handle_t g_spi_device = NULL;
|
||||
|
||||
/**
|
||||
* @brief SPI2 总线初始化
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t spi_master_init(void)
|
||||
// ============================================================
|
||||
// 初始化
|
||||
// ============================================================
|
||||
esp_err_t SpiMaster::init()
|
||||
{
|
||||
DEBUG_INFO("SPI", "=== 初始化 SPI2 (VSPI) ===");
|
||||
DEBUG_INFO("SPI", "=== 初始化 SPI2 总线 ===");
|
||||
|
||||
spi_bus_config_t bus_cfg = {};
|
||||
bus_cfg.mosi_io_num = 5;
|
||||
bus_cfg.miso_io_num = 7;
|
||||
bus_cfg.miso_io_num = 2;
|
||||
bus_cfg.mosi_io_num = 7;
|
||||
bus_cfg.sclk_io_num = 6;
|
||||
bus_cfg.quadwp_io_num = -1;
|
||||
bus_cfg.quadhd_io_num = -1;
|
||||
bus_cfg.max_transfer_sz = 256;
|
||||
|
||||
DEBUG_INFO("SPI", "SPI2 引脚: MOSI=GPIO5, MISO=GPIO7, SCLK=GPIO6");
|
||||
DEBUG_INFO("SPI", "SPI2 配置: CLK=%d MHz, CS=GPIO%d, 最大传输=%d 字节",
|
||||
SPI_CLOCK_HZ / 1000000, SPI_MASTER_CS_GPIO, bus_cfg.max_transfer_sz);
|
||||
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) {
|
||||
@@ -243,408 +220,150 @@ esp_err_t spi_master_init(void)
|
||||
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;
|
||||
/* CS↑ 后保持 CS 高的时间(SPI 时钟周期数)。
|
||||
*
|
||||
* 0 = 最小 CS 高时间(仅满足 SPI 时序)。
|
||||
* 设大一点让 GD32 EXTI ISR 有足够时间完成 onCsRelease() 处理:
|
||||
* 切 internal NSS HIGH + 清 RBNE FIFO + 帧解析 + 准备响应帧。
|
||||
*
|
||||
* 10 × 250ns = 2.5µs,加上 SPI 驱动本身的 CS 间隮(~1µs),
|
||||
* GD32 实际有 ~3.5µs 的 CS 高窗口。足够完成关键操作。
|
||||
*/
|
||||
dev_cfg.cs_ena_posttrans = 10;
|
||||
/* CS↓ 后等待 20 个 SPI 时钟周期再开始发 SCK。
|
||||
*
|
||||
* 4MHz SPI → 20×250ns = 5µs 延时。
|
||||
*
|
||||
* GD32 SPI 从机在 CS↓ 时需要 EXTI ISR 执行 onCsAssert():
|
||||
* 设置 s_csActive=true → 清空接收缓冲 → 切 internal NSS LOW → 配置 DMA。
|
||||
*
|
||||
* 但 SPI0 RBNE 中断优先级 (1,0) 高于 EXTI (1,1),
|
||||
* 首字节 RBNE 会打断正在执行的 EXTI ISR →
|
||||
* s_csActive 还没设为 true → s_rxStreamLen 还没清零 →
|
||||
* 首字节存到旧位置 → EXTI 返回后清零 → 首字节丢失。
|
||||
*
|
||||
* cs_ena_pretrans=20 让 ESP32 在 CS↓ 后多等 5µs,
|
||||
* GD32 EXTI ISR 只需 ~1µs 就能完成关键初始化,
|
||||
* 5µs 足够让 onCsAssert() 执行完 s_csActive=true + s_rxStreamLen=0U。
|
||||
*
|
||||
* 代价:每次 SPI 事务多 5µs 延时。
|
||||
* 一次 OTA 事务 = 1次请求 + ~10次轮询 = ~55µs 额外开销,可接受。
|
||||
*/
|
||||
dev_cfg.cs_ena_pretrans = 20;
|
||||
dev_cfg.flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY;
|
||||
|
||||
ret = spi_bus_add_device(SPI2_HOST, &dev_cfg, &g_spi_device);
|
||||
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));
|
||||
DEBUG_ERR("SPI", "SPI 设备添加失败: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
DEBUG_INFO("SPI", "SPI2 已初始化: CLK=%dMHz, CS=GPIO%d, 模式=%d",
|
||||
SPI_CLOCK_HZ / 1000000, SPI_MASTER_CS_GPIO, SPI_MODE);
|
||||
DEBUG_INFO("SPI", "SPI 设备句柄: %p", g_spi_device);
|
||||
DEBUG_INFO("SPI", "SPI2 总线初始化完成: CS=GPIO%d, CLK=%d Hz",
|
||||
SPI_MASTER_CS_GPIO, SPI_CLOCK_HZ);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 执行一次同步 SPI 事务,先发送请求,再补时钟读取响应尾部。
|
||||
* @param cmd SPI 命令码;为 0 时不额外插入命令字节,直接发送 payload。
|
||||
* @param payload 发送数据指针。
|
||||
* @param payload_len 发送数据长度。
|
||||
* @param recv_buf 接收缓冲区指针。
|
||||
* @param recv_len 期望接收的响应字节数。
|
||||
* @param timeout_ms 预留超时参数(当前实现未直接使用)。
|
||||
* @return ESP_OK 成功,其他为失败错误码。
|
||||
*/
|
||||
esp_err_t spi_sync_transfer(uint8_t cmd, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *recv_buf,
|
||||
size_t recv_len, uint32_t timeout_ms)
|
||||
// ============================================================
|
||||
// 公开 SPI 传输接口
|
||||
// ============================================================
|
||||
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)
|
||||
{
|
||||
(void)timeout_ms;
|
||||
if (g_spi_device == NULL) {
|
||||
DEBUG_ERR("SPI", "SPI 设备未初始化");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
return transferRequestResponse(cmd, payload, payload_len, recv_buf, recv_len);
|
||||
}
|
||||
|
||||
esp_err_t SpiMaster::send(uint8_t cmd, const uint8_t *payload, size_t len)
|
||||
{
|
||||
return transferRequestResponse(cmd, payload, len, NULL, 0);
|
||||
}
|
||||
|
||||
esp_err_t SpiMaster::recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms)
|
||||
{
|
||||
(void)timeout_ms;
|
||||
return transferRequestResponse(cmd, NULL, 0, recv_buf, len);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SPI OTA 协议
|
||||
// ============================================================
|
||||
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)
|
||||
{
|
||||
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", "SPI 传输: cmd=0x%02X, 发送=%zu 字节, 接收=%zu 字节",
|
||||
cmd, payload_len, recv_len);
|
||||
|
||||
esp_err_t ret = spi_transfer_request_response(cmd, payload, payload_len, recv_buf, recv_len);
|
||||
esp_err_t ret = transferClocked(tx_frame, tx_len, NULL, 0);
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_WARN("SPI", "SPI 发送 cmd=0x%02X 失败: %s", cmd, esp_err_to_name(ret));
|
||||
} else {
|
||||
DEBUG_DBG("SPI", "SPI 传输完成: cmd=0x%02X", cmd);
|
||||
DEBUG_WARN("SPI", "OTA 请求发送失败: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
return ret;
|
||||
return otaPollResponse(resp_type, resp_payload, resp_max, resp_len, timeout_ms);
|
||||
}
|
||||
|
||||
// 仅发送
|
||||
/**
|
||||
* @brief 调用同步传输接口发送数据,不要求接收返回内容。
|
||||
* @param cmd SPI 命令码。
|
||||
* @param payload 发送数据指针。
|
||||
* @param len 发送数据长度。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t spi_send(uint8_t cmd, const uint8_t *payload, size_t len)
|
||||
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)
|
||||
{
|
||||
return spi_sync_transfer(cmd, payload, len, NULL, 0, 3000);
|
||||
}
|
||||
|
||||
// 仅接收
|
||||
/**
|
||||
* @brief 发送哑数据产生时钟,从从设备同步读取指定长度数据。
|
||||
* @param cmd SPI 命令码。
|
||||
* @param recv_buf 接收缓冲区。
|
||||
* @param len 接收长度。
|
||||
* @param timeout_ms 超时时间(毫秒)。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t spi_recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms)
|
||||
{
|
||||
return spi_sync_transfer(cmd, NULL, 0, recv_buf, len, timeout_ms);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 轮询读取 SPI OTA 响应帧,不重复发送请求帧。
|
||||
* @param resp_type 输出响应类型,可为 NULL。
|
||||
* @param resp_payload 输出响应负载缓冲区,可为 NULL。
|
||||
* @param resp_max 响应缓冲区最大长度。
|
||||
* @param resp_len 输出响应长度,可为 NULL。
|
||||
* @param timeout_ms 轮询超时时间(毫秒)。
|
||||
* @return ESP_OK 成功,ESP_ERR_TIMEOUT 超时,其他为底层错误码。
|
||||
*/
|
||||
esp_err_t spi_ota_poll_response(uint8_t *resp_type, uint8_t *resp_payload,
|
||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
|
||||
{
|
||||
if (g_spi_device == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (resp_len) *resp_len = 0;
|
||||
if (resp_type) *resp_type = 0;
|
||||
|
||||
uint8_t rx_raw[SPI_OTA_FRAME_MAX] = {0};
|
||||
uint8_t dummy[SPI_OTA_FRAME_MAX];
|
||||
memset(dummy, 0xFF, sizeof(dummy));
|
||||
|
||||
constexpr size_t kPollBytes = 96U;
|
||||
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
|
||||
uint32_t attempt = 0;
|
||||
|
||||
while (xTaskGetTickCount() <= deadline) {
|
||||
++attempt;
|
||||
memset(rx_raw, 0xFF, sizeof(rx_raw));
|
||||
esp_err_t ret = spi_transfer_clocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw));
|
||||
uint8_t rx_raw[kPollBytes];
|
||||
uint8_t dummy[kPollBytes];
|
||||
memset(dummy, 0xFF, sizeof(dummy));
|
||||
|
||||
esp_err_t ret = transferClocked(dummy, sizeof(dummy), rx_raw, sizeof(rx_raw));
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_WARN("SPI", "SPI OTA 响应读取失败[%u]: %s", attempt, esp_err_to_name(ret));
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
DEBUG_WARN("SPI", "OTA 响应读取失败: %s", esp_err_to_name(ret));
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t parsed_type = 0;
|
||||
const uint8_t *parsed_payload = nullptr;
|
||||
size_t parsed_len = 0;
|
||||
uint8_t found_type = 0;
|
||||
const uint8_t *found_payload = nullptr;
|
||||
size_t found_len = 0;
|
||||
size_t frame_offset = 0;
|
||||
if (spi_ota_find_frame(rx_raw, sizeof(rx_raw), &parsed_type, &parsed_payload, &parsed_len, &frame_offset)) {
|
||||
DEBUG_DBG("SPI", "OTA响应: type=0x%02X len=%u offset=%u attempt=%u",
|
||||
parsed_type, (unsigned)parsed_len, (unsigned)frame_offset, (unsigned)attempt);
|
||||
if (resp_type) *resp_type = parsed_type;
|
||||
|
||||
if (otaFindFrame(rx_raw, sizeof(rx_raw), &found_type, &found_payload, &found_len, &frame_offset)) {
|
||||
if (resp_type) *resp_type = found_type;
|
||||
if (resp_payload && resp_max > 0) {
|
||||
size_t copy_len = (parsed_len < resp_max) ? parsed_len : resp_max;
|
||||
memcpy(resp_payload, parsed_payload, copy_len);
|
||||
if (resp_len) *resp_len = copy_len;
|
||||
} else if (resp_len) {
|
||||
*resp_len = parsed_len;
|
||||
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;
|
||||
}
|
||||
|
||||
bool all_ff = true;
|
||||
for (size_t i = 0; i < sizeof(rx_raw); ++i) {
|
||||
if (rx_raw[i] != 0xFF) {
|
||||
all_ff = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
DEBUG_DBG("SPI", "OTA响应未就绪[%u]: %s first10=%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X",
|
||||
attempt, all_ff ? "all-FF" : "no-valid-SOF",
|
||||
rx_raw[0], rx_raw[1], rx_raw[2], rx_raw[3],
|
||||
rx_raw[4], rx_raw[5], rx_raw[6], rx_raw[7],
|
||||
rx_raw[8], rx_raw[9]);
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
|
||||
DEBUG_DBG("SPI", "OTA响应超时: timeout=%ums", (unsigned)timeout_ms);
|
||||
DEBUG_WARN("SPI", "OTA 响应超时: %lu ms", (unsigned long)timeout_ms);
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 执行一次 SPI OTA 请求事务并等待响应帧。
|
||||
* @param type 请求帧类型。
|
||||
* @param payload 请求负载。
|
||||
* @param payload_len 负载长度。
|
||||
* @param resp_type 输出响应类型,可为 NULL。
|
||||
* @param resp_payload 输出响应负载缓冲区,可为 NULL。
|
||||
* @param resp_max 响应负载缓冲区大小。
|
||||
* @param resp_len 输出响应长度,可为 NULL。
|
||||
* @param timeout_ms 响应等待超时时间(毫秒)。
|
||||
* @return ESP_OK 成功,其他为失败错误码。
|
||||
*/
|
||||
esp_err_t spi_ota_transfer(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)
|
||||
esp_err_t SpiMaster::appRequestBootloader(uint32_t timeout_ms)
|
||||
{
|
||||
if (g_spi_device == NULL) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (resp_len) *resp_len = 0;
|
||||
if (resp_type) *resp_type = 0;
|
||||
DEBUG_INFO("SPI", "请求 GD32 APP 进入 Bootloader...");
|
||||
|
||||
uint8_t tx_frame[SPI_OTA_FRAME_MAX] = {0};
|
||||
size_t tx_len = 0;
|
||||
if (!spi_ota_build_frame(type, payload, payload_len, tx_frame, &tx_len)) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
DEBUG_DBG("SPI", "OTA事务: req_type=0x%02X req_len=%u", type, (unsigned)payload_len);
|
||||
|
||||
/* ============================================================
|
||||
* 第1次 CS 周期:发送请求帧(只发不收)
|
||||
*
|
||||
* ESP32 发完请求帧后 CS↑,GD32 在 CS↑ 的 onCsRelease 中
|
||||
* 或 RBNE 中断的 tryProcessCompleteFrame 中处理帧,
|
||||
* 将 ACK 写入 s_txBuf 并启动 DMA。
|
||||
*
|
||||
* 注意:不能在同一个 CS 周期内发请求+读响应,因为:
|
||||
* 1. SPI 全双工:ESP32 发请求时 MISO 同时在输出旧数据
|
||||
* 2. GD32 收到请求帧后才能准备 ACK,需重启 DMA
|
||||
* 3. DMA 重启期间 MISO 会输出 0x00/0xFF,导致帧断裂
|
||||
* ============================================================ */
|
||||
esp_err_t ret = spi_transfer_clocked(tx_frame, tx_len, NULL, 0);
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_ERR("SPI", "SPI OTA 请求发送失败: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* 等 GD32 处理帧 + 准备响应。
|
||||
*
|
||||
* GD32 在 CS↑ 的 onCsRelease() 中处理帧:
|
||||
* isrProcessFrame() → OTA handler 回调 → queueTxFrame() → DMA 启动
|
||||
* 总耗时约 10-20µs(ISR 内执行,无调度延迟)。
|
||||
*
|
||||
* 但 EXTI ISR 可能被更高优先级的 RBNE 中断短暂抢占,
|
||||
* 实际完成时间可能到 30-50µs。
|
||||
*
|
||||
* 500µs 提供了 10x 以上的安全余量,
|
||||
* 同时对 OTA 事务延迟影响可忽略(一次 OTA 事务 ~1ms 级别)。 */
|
||||
esp_rom_delay_us(500);
|
||||
|
||||
/* ============================================================
|
||||
* 第2次 CS 周期:发 0xFF dummy 产生时钟,读 MISO 上的响应帧
|
||||
*
|
||||
* GD32 的 onCsAssert 会重启 DMA,将 s_txBuf(ACK帧)
|
||||
* 推到 MISO,ESP32 在 96 字节中扫描定位响应帧。
|
||||
* ============================================================ */
|
||||
return spi_ota_poll_response(resp_type, resp_payload, resp_max, resp_len, timeout_ms);
|
||||
}
|
||||
|
||||
// CANFD 帧发送
|
||||
// 帧格式: [通道(1B)] [扩展帧ID(4B BE)] [数据(1~64B)]
|
||||
/**
|
||||
* @brief 发送一帧 CANFD 数据到 GD32,并可选等待响应。
|
||||
* @param channel CAN 通道号。
|
||||
* @param ext_id 扩展帧 ID(29-bit)。
|
||||
* @param data 发送负载指针。
|
||||
* @param dlc 发送数据长度。
|
||||
* @param recv_buf 接收缓冲区,可为 NULL。
|
||||
* @param recv_max 接收缓冲区最大长度。
|
||||
* @param timeout_ms 等待响应超时(毫秒)。
|
||||
* @return >=0 为接收字节数,<0 为错误码。
|
||||
*/
|
||||
static int can_frame_send(can_channel_t channel, uint32_t ext_id,
|
||||
const uint8_t *data, uint8_t dlc,
|
||||
uint8_t *recv_buf, uint8_t recv_max,
|
||||
uint32_t timeout_ms)
|
||||
{
|
||||
if (g_spi_device == NULL) {
|
||||
DEBUG_ERR("SPI", "CAN 发送: SPI 设备未初始化");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (dlc == 0 || dlc > CAN_MAX_DLC) {
|
||||
DEBUG_ERR("SPI", "CAN[ch%d] DLC 无效: %d (范围 1~%d)", channel, dlc, CAN_MAX_DLC);
|
||||
return -2;
|
||||
}
|
||||
|
||||
if (channel >= CAN_CHANNEL_MAX) {
|
||||
DEBUG_ERR("SPI", "CAN 通道号无效: %d (范围 0~%d)", channel, CAN_CHANNEL_MAX - 1);
|
||||
return -3;
|
||||
}
|
||||
|
||||
DEBUG_DBG("SPI", "CAN[ch%d] ID=0x%08X 发送: DLC=%d", channel, ext_id, dlc);
|
||||
|
||||
// 构建 CANFD 帧: [通道(1B)] [EXT_ID(4B)] [DATA(dlc B)]
|
||||
uint8_t tx_buf[1 + 4 + CAN_MAX_DLC] = {0};
|
||||
tx_buf[0] = (uint8_t)channel;
|
||||
tx_buf[1] = (uint8_t)((ext_id >> 24) & 0xFF);
|
||||
tx_buf[2] = (uint8_t)((ext_id >> 16) & 0xFF);
|
||||
tx_buf[3] = (uint8_t)((ext_id >> 8) & 0xFF);
|
||||
tx_buf[4] = (uint8_t)(ext_id & 0xFF);
|
||||
memcpy(&tx_buf[5], data, dlc);
|
||||
|
||||
size_t tx_len = 5 + dlc;
|
||||
uint8_t rx_buf[5 + CAN_MAX_DLC] = {0};
|
||||
|
||||
const size_t rx_len = (recv_buf != NULL && recv_max > 0U) ? (size_t)(5U + recv_max) : 0U;
|
||||
esp_err_t ret = spi_transfer_clocked(tx_buf, tx_len, rx_buf, rx_len);
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_ERR("SPI", "CAN[ch%d] ID=0x%08X 发送失败: %s",
|
||||
channel, ext_id, esp_err_to_name(ret));
|
||||
return -4;
|
||||
}
|
||||
|
||||
if (recv_buf && recv_max > 0) {
|
||||
// 解析 GD32 响应帧
|
||||
uint8_t resp_channel = rx_buf[0];
|
||||
uint32_t resp_id = (rx_buf[1] << 24) | (rx_buf[2] << 16)
|
||||
| (rx_buf[3] << 8) | rx_buf[4];
|
||||
uint8_t resp_dlc = resp_id >> 24; // 复用了 ID 字段低字节作为 DLC
|
||||
resp_id &= 0x00FFFFFF;
|
||||
|
||||
uint8_t copy_len = (resp_dlc < recv_max) ? resp_dlc : recv_max;
|
||||
memcpy(recv_buf, &rx_buf[5], copy_len);
|
||||
|
||||
DEBUG_INFO("SPI", "CAN[ch%d] ID=0x%08X 响应: DLC=%d, 接收=%d 字节",
|
||||
resp_channel, resp_id, copy_len, copy_len);
|
||||
return copy_len;
|
||||
}
|
||||
|
||||
DEBUG_DBG("SPI", "CAN[ch%d] ID=0x%08X 发送完成 (无响应)", channel, ext_id);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 对外暴露的 GD32 CAN 发送接口,必要时修正默认 DLC。
|
||||
* @param channel CAN 通道。
|
||||
* @param ext_id 扩展帧 ID。
|
||||
* @param data 发送数据指针。
|
||||
* @param dlc 数据长度。
|
||||
* @param recv_buf 接收缓冲区,可为 NULL。
|
||||
* @param recv_max 接收缓冲区大小。
|
||||
* @param timeout_ms 等待响应超时(毫秒)。
|
||||
* @return >=0 为接收字节数,<0 为错误码。
|
||||
*/
|
||||
int gd32_can_send(can_channel_t channel, uint32_t ext_id,
|
||||
const uint8_t *data, uint8_t dlc,
|
||||
uint8_t *recv_buf, uint8_t recv_max,
|
||||
uint32_t timeout_ms)
|
||||
{
|
||||
return can_frame_send(channel, ext_id, data, dlc, recv_buf, recv_max, timeout_ms);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发送无需响应的 GD32 CAN 帧。
|
||||
* @param channel CAN 通道。
|
||||
* @param ext_id 扩展帧 ID。
|
||||
* @param data 发送数据指针。
|
||||
* @param dlc 数据长度。
|
||||
* @return 0 表示已发送完成,<0 为错误码。
|
||||
*/
|
||||
int gd32_can_send_no_reply(can_channel_t channel, uint32_t ext_id,
|
||||
const uint8_t *data, uint8_t dlc)
|
||||
{
|
||||
return can_frame_send(channel, ext_id, data, dlc, NULL, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 向正在运行的 GD32 DCU_APP 发送进入 Bootloader 的短 SPI 命令。
|
||||
* @param timeout_ms 等待 APP 返回确认的最长时间。
|
||||
* @return ESP_OK 表示 APP 已接收命令;超时则通常表示 GD32 已不在 APP 或 SPI 未就绪。
|
||||
*/
|
||||
esp_err_t gd32_app_request_bootloader(uint32_t timeout_ms)
|
||||
{
|
||||
uint8_t cmd = SPI_APP_ENTER_BOOT_CMD;
|
||||
esp_err_t ret = spi_transfer_clocked(&cmd, 1U, NULL, 0U);
|
||||
esp_err_t ret = transferClocked(&cmd, 1, NULL, 0);
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_WARN("SPI", "APP进入BOOT命令发送失败: %s", esp_err_to_name(ret));
|
||||
DEBUG_WARN("SPI", "发送 Bootloader 请求失败: %s", esp_err_to_name(ret));
|
||||
return ret;
|
||||
}
|
||||
|
||||
uint8_t rx[8] = {0};
|
||||
uint8_t dummy[8];
|
||||
memset(dummy, 0xFF, sizeof(dummy));
|
||||
TickType_t deadline = xTaskGetTickCount() + pdMS_TO_TICKS(timeout_ms);
|
||||
|
||||
while (xTaskGetTickCount() <= deadline) {
|
||||
memset(rx, 0xFF, sizeof(rx));
|
||||
ret = spi_transfer_clocked(dummy, sizeof(dummy), rx, sizeof(rx));
|
||||
if (ret == ESP_OK) {
|
||||
for (size_t i = 0; i + 3U < sizeof(rx); ++i) {
|
||||
if (rx[i] == SPI_APP_ENTER_BOOT_CMD && rx[i + 1U] == SPI_APP_ENTER_BOOT_ACK_OK) {
|
||||
DEBUG_INFO("SPI", "GD32 APP确认进入Bootloader OTA窗口");
|
||||
return ESP_OK;
|
||||
}
|
||||
}
|
||||
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");
|
||||
return ESP_OK;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
}
|
||||
|
||||
DEBUG_WARN("SPI", "GD32 APP进入Bootloader确认超时");
|
||||
DEBUG_WARN("SPI", "GD32 APP 进入 Bootloader 确认超时");
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
esp_err_t gd32_app_ping(uint32_t timeout_ms, uint32_t *uptime_sec)
|
||||
esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec)
|
||||
{
|
||||
/* APP 侧 SPI 从机当前是中断逐字节收发,不适合像 BOOT 那样一次读 96B 长轮询。
|
||||
* 这里改为:每次都完整重发一帧 PING 请求,然后只读一个短窗口寻找 ACK。
|
||||
* 这样即便某次响应窗口错过,也会在下一次重发时重新生成 APP_ALIVE。 */
|
||||
constexpr uint8_t kAppPingType = 0xA1U;
|
||||
constexpr size_t kAppPingPollBytes = 32U;
|
||||
|
||||
uint8_t tx_frame[8] = {0};
|
||||
size_t tx_len = 0U;
|
||||
if (!spi_ota_build_frame(kAppPingType, NULL, 0U, tx_frame, &tx_len)) {
|
||||
if (!otaBuildFrame(kAppPingType, NULL, 0U, tx_frame, &tx_len)) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
@@ -654,7 +373,7 @@ esp_err_t gd32_app_ping(uint32_t timeout_ms, uint32_t *uptime_sec)
|
||||
while (xTaskGetTickCount() <= deadline) {
|
||||
++attempt;
|
||||
|
||||
esp_err_t err = spi_transfer_clocked(tx_frame, tx_len, NULL, 0U);
|
||||
esp_err_t err = transferClocked(tx_frame, tx_len, NULL, 0U);
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_DBG("SPI", "APP PING 请求发送失败[%lu]: %s",
|
||||
(unsigned long)attempt, esp_err_to_name(err));
|
||||
@@ -669,7 +388,7 @@ esp_err_t gd32_app_ping(uint32_t timeout_ms, uint32_t *uptime_sec)
|
||||
memset(dummy, 0xFF, sizeof(dummy));
|
||||
memset(rx_raw, 0xFF, sizeof(rx_raw));
|
||||
|
||||
err = spi_transfer_clocked(dummy, sizeof(dummy), rx_raw, 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));
|
||||
@@ -681,8 +400,8 @@ esp_err_t gd32_app_ping(uint32_t timeout_ms, uint32_t *uptime_sec)
|
||||
const uint8_t *resp_payload = nullptr;
|
||||
size_t resp_len = 0U;
|
||||
size_t frame_offset = 0U;
|
||||
if (spi_ota_find_frame(rx_raw, sizeof(rx_raw),
|
||||
&resp_type, &resp_payload, &resp_len, &frame_offset)) {
|
||||
if (otaFindFrame(rx_raw, sizeof(rx_raw),
|
||||
&resp_type, &resp_payload, &resp_len, &frame_offset)) {
|
||||
if (resp_type == SPI_OTA_RSP_ACK && resp_len >= 3U && resp_payload[0] == kAppPingType) {
|
||||
if (uptime_sec) {
|
||||
*uptime_sec = (uint32_t)resp_payload[1] | ((uint32_t)resp_payload[2] << 8);
|
||||
@@ -714,20 +433,9 @@ esp_err_t gd32_app_ping(uint32_t timeout_ms, uint32_t *uptime_sec)
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
// 带重试的 SPI 传输
|
||||
/**
|
||||
* @brief 执行带重试的 SPI 同步传输。
|
||||
* @param cmd SPI 命令码。
|
||||
* @param payload 发送数据指针。
|
||||
* @param payload_len 发送长度。
|
||||
* @param recv_buf 接收缓冲区。
|
||||
* @param recv_len 接收长度。
|
||||
* @param max_retries 最大重试次数。
|
||||
* @return ESP_OK 成功,重试耗尽返回 ESP_ERR_TIMEOUT。
|
||||
*/
|
||||
esp_err_t spi_transfer_with_retry(uint8_t cmd, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *recv_buf,
|
||||
size_t recv_len, int max_retries)
|
||||
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)
|
||||
{
|
||||
esp_err_t ret;
|
||||
int retry_count = 0;
|
||||
@@ -735,8 +443,8 @@ esp_err_t spi_transfer_with_retry(uint8_t cmd, const uint8_t *payload,
|
||||
DEBUG_INFO("SPI", "SPI 传输 (带重试): cmd=0x%02X, 最大重试=%d", cmd, max_retries);
|
||||
|
||||
while (retry_count < max_retries) {
|
||||
ret = spi_sync_transfer(cmd, payload, payload_len,
|
||||
recv_buf, recv_len, 3000);
|
||||
ret = syncTransfer(cmd, payload, payload_len,
|
||||
recv_buf, recv_len, 3000);
|
||||
if (ret == ESP_OK) {
|
||||
if (retry_count > 0) {
|
||||
DEBUG_INFO("SPI", "SPI 传输成功 (重试 %d 次后成功)", retry_count);
|
||||
@@ -745,7 +453,7 @@ esp_err_t spi_transfer_with_retry(uint8_t cmd, const uint8_t *payload,
|
||||
}
|
||||
|
||||
retry_count++;
|
||||
DEBUG_WARN("SPI", "SPI 重试 %d/%d, cmd=0x%02X, err=%s",
|
||||
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));
|
||||
}
|
||||
@@ -753,3 +461,60 @@ esp_err_t spi_transfer_with_retry(uint8_t cmd, const uint8_t *payload,
|
||||
DEBUG_ERR("SPI", "SPI 传输 %d 次重试后失败, cmd=0x%02X", max_retries, cmd);
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CANFD 接口
|
||||
// ============================================================
|
||||
int SpiMaster::canSend(can_channel_t channel, uint32_t ext_id,
|
||||
const uint8_t *data, uint8_t dlc,
|
||||
uint8_t *recv_buf, uint8_t recv_max,
|
||||
uint32_t timeout_ms)
|
||||
{
|
||||
if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC || !data) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t tx_buf[72];
|
||||
size_t tx_len = 0;
|
||||
|
||||
tx_buf[tx_len++] = (uint8_t)(channel & CAN_CHANNEL_MASK);
|
||||
tx_buf[tx_len++] = (uint8_t)((ext_id >> 24) & 0xFF);
|
||||
tx_buf[tx_len++] = (uint8_t)((ext_id >> 16) & 0xFF);
|
||||
tx_buf[tx_len++] = (uint8_t)((ext_id >> 8) & 0xFF);
|
||||
tx_buf[tx_len++] = (uint8_t)(ext_id & 0xFF);
|
||||
memcpy(&tx_buf[tx_len], data, dlc);
|
||||
tx_len += dlc;
|
||||
|
||||
esp_err_t ret = syncTransfer(0, tx_buf, tx_len, recv_buf, recv_max, timeout_ms);
|
||||
if (ret != ESP_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return (int)recv_max;
|
||||
}
|
||||
|
||||
int SpiMaster::canSendNoReply(can_channel_t channel, uint32_t ext_id,
|
||||
const uint8_t *data, uint8_t dlc)
|
||||
{
|
||||
if (channel >= CAN_CHANNEL_MAX || dlc > CAN_MAX_DLC || !data) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t tx_buf[72];
|
||||
size_t tx_len = 0;
|
||||
|
||||
tx_buf[tx_len++] = (uint8_t)(channel & CAN_CHANNEL_MASK);
|
||||
tx_buf[tx_len++] = (uint8_t)((ext_id >> 24) & 0xFF);
|
||||
tx_buf[tx_len++] = (uint8_t)((ext_id >> 16) & 0xFF);
|
||||
tx_buf[tx_len++] = (uint8_t)((ext_id >> 8) & 0xFF);
|
||||
tx_buf[tx_len++] = (uint8_t)(ext_id & 0xFF);
|
||||
memcpy(&tx_buf[tx_len], data, dlc);
|
||||
tx_len += dlc;
|
||||
|
||||
esp_err_t ret = send(0, tx_buf, tx_len);
|
||||
if (ret != ESP_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
+85
-94
@@ -1,6 +1,8 @@
|
||||
/*
|
||||
/*
|
||||
* spi_master.h - SPI Master 驱动头文件
|
||||
* CANFD 3通道发送接口 + 底层 SPI 总线
|
||||
*
|
||||
* SpiMaster 类封装 SPI 总线初始化、CANFD 通信和 SPI OTA 协议。
|
||||
*/
|
||||
#ifndef SPI_MASTER_H
|
||||
#define SPI_MASTER_H
|
||||
@@ -26,9 +28,6 @@
|
||||
#define SPI_OTA_RSP_WAIT (0x81)
|
||||
#define SPI_OTA_RSP_ERROR (0x82)
|
||||
|
||||
// SPI 设备句柄(外部引用)
|
||||
extern spi_device_handle_t g_spi_device;
|
||||
|
||||
// ============================================================
|
||||
// CANFD 帧格式(3通道)
|
||||
// [通道编号: 1B] [扩展帧ID: 4B BE] [数据: 1~64B]
|
||||
@@ -55,104 +54,96 @@ typedef struct {
|
||||
} can_frame_t;
|
||||
|
||||
/**
|
||||
* @brief 发送一帧 CANFD 数据,可选等待并读取响应内容。
|
||||
* @param channel CAN 通道(0~2)。
|
||||
* @param ext_id 29 位扩展帧 ID。
|
||||
* @param data 数据负载指针。
|
||||
* @param dlc 数据长度,范围 1~64。
|
||||
* @param recv_buf 接收缓冲区,可为 NULL。
|
||||
* @param recv_max 期望最大接收字节数。
|
||||
* @param timeout_ms 超时时间(毫秒)。
|
||||
* @return >= 0 接收字节数,< 0 失败。
|
||||
* @brief SPI Master 驱动类(单例)
|
||||
*
|
||||
* 封装 SPI 总线初始化、同步传输、CANFD 通信和 SPI OTA 协议。
|
||||
*/
|
||||
int gd32_can_send(can_channel_t channel, uint32_t ext_id,
|
||||
const uint8_t *data, uint8_t dlc,
|
||||
uint8_t *recv_buf, uint8_t recv_max,
|
||||
uint32_t timeout_ms);
|
||||
class SpiMaster {
|
||||
public:
|
||||
static SpiMaster& instance();
|
||||
|
||||
/**
|
||||
* @brief 发送一帧无需响应的 CANFD 数据,适合广播或通知场景。
|
||||
* @param channel CAN 通道(0~2)。
|
||||
* @param ext_id 29 位扩展帧 ID。
|
||||
* @param data 数据负载指针。
|
||||
* @param dlc 数据长度,范围 1~64。
|
||||
* @return >= 0 成功,< 0 失败。
|
||||
*/
|
||||
int gd32_can_send_no_reply(can_channel_t channel, uint32_t ext_id,
|
||||
const uint8_t *data, uint8_t dlc);
|
||||
/** @brief 初始化 SPI2 总线与目标从设备。 */
|
||||
esp_err_t init();
|
||||
|
||||
/**
|
||||
* @brief 初始化 SPI2 总线与目标从设备。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t spi_master_init(void);
|
||||
// ============================================================
|
||||
// 底层 SPI 传输
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @brief 执行一次同步 SPI 事务,先发送请求,再读取请求之后的响应字节。
|
||||
* @param cmd SPI 命令码;为 0 时不额外插入命令字节。
|
||||
* @param payload 发送数据指针。
|
||||
* @param payload_len 发送数据长度。
|
||||
* @param recv_buf 接收缓冲区,可为 NULL。
|
||||
* @param recv_len 期望接收的响应长度。
|
||||
* @param timeout_ms 超时时间(毫秒)。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t spi_sync_transfer(uint8_t cmd, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *recv_buf,
|
||||
size_t recv_len, uint32_t timeout_ms);
|
||||
/** @brief 执行一次同步 SPI 事务,先发送请求,再读取响应字节。 */
|
||||
esp_err_t syncTransfer(uint8_t cmd, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *recv_buf,
|
||||
size_t recv_len, uint32_t timeout_ms);
|
||||
|
||||
/**
|
||||
* @brief 仅发送 SPI 请求数据,不关心返回内容。
|
||||
* @param cmd SPI 命令码;为 0 时仅发送 payload。
|
||||
* @param payload 发送数据指针。
|
||||
* @param len 发送数据长度。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t spi_send(uint8_t cmd, const uint8_t *payload, size_t len);
|
||||
/** @brief 仅发送 SPI 请求数据,不关心返回内容。 */
|
||||
esp_err_t send(uint8_t cmd, const uint8_t *payload, size_t len);
|
||||
|
||||
/**
|
||||
* @brief 仅接收 SPI 数据,必要时先发送命令字节,再发送哑数据产生时钟。
|
||||
* @param cmd SPI 命令码;为 0 时只发送哑数据。
|
||||
* @param recv_buf 接收缓冲区。
|
||||
* @param len 接收长度。
|
||||
* @param timeout_ms 超时时间(毫秒)。
|
||||
* @return ESP_OK 成功,其他失败。
|
||||
*/
|
||||
esp_err_t spi_recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms);
|
||||
/** @brief 仅接收 SPI 数据,必要时先发送命令字节。 */
|
||||
esp_err_t recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms);
|
||||
|
||||
/**
|
||||
* @brief 按 GD32 SPI OTA 最小协议执行一次请求事务 + 一次响应读取事务。
|
||||
* @param type 请求帧类型(REQ/DATA/CANCEL)。
|
||||
* @param payload 请求负载。
|
||||
* @param payload_len 负载长度。
|
||||
* @param resp_type 输出响应类型(ACK/WAIT/ERROR)。
|
||||
* @param resp_payload 输出响应负载缓冲区。
|
||||
* @param resp_max 输出响应负载最大长度。
|
||||
* @param resp_len 输出实际响应负载长度。
|
||||
* @param timeout_ms 超时。
|
||||
*/
|
||||
esp_err_t spi_ota_transfer(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);
|
||||
// ============================================================
|
||||
// SPI OTA 协议
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* @brief 仅执行 SPI OTA 响应读取事务,不重复发送请求帧。
|
||||
*/
|
||||
esp_err_t spi_ota_poll_response(uint8_t *resp_type, uint8_t *resp_payload,
|
||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
|
||||
/** @brief 按 GD32 SPI OTA 最小协议执行一次请求 + 响应读取。 */
|
||||
esp_err_t 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);
|
||||
|
||||
/**
|
||||
* @brief 通知正在运行的 GD32 DCU_APP 设置 OTA 标志并软复位进入 Bootloader。
|
||||
* @return ESP_OK 表示 APP 已确认;ESP_ERR_TIMEOUT 常表示 GD32 已在 Bootloader 或通信未就绪。
|
||||
*/
|
||||
esp_err_t gd32_app_request_bootloader(uint32_t timeout_ms);
|
||||
/** @brief 仅执行 SPI OTA 响应读取事务,不重复发送请求帧。 */
|
||||
esp_err_t otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload,
|
||||
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
|
||||
|
||||
/**
|
||||
* @brief 向 GD32 APP 发送存活探测命令 (TYPE=0xA1),等待 APP 回复 ALIVE ACK。
|
||||
* @param timeout_ms 超时时间(毫秒)。
|
||||
* @param uptime_sec 输出 APP 运行秒数,可为 NULL。
|
||||
* @return ESP_OK APP 存活并回复;ESP_ERR_TIMEOUT 超时无响应。
|
||||
*/
|
||||
esp_err_t gd32_app_ping(uint32_t timeout_ms, uint32_t *uptime_sec);
|
||||
/** @brief 通知 GD32 DCU_APP 设置 OTA 标志并软复位进入 Bootloader。 */
|
||||
esp_err_t appRequestBootloader(uint32_t timeout_ms);
|
||||
|
||||
#endif // SPI_MASTER_H
|
||||
/** @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。 */
|
||||
esp_err_t appPing(uint32_t timeout_ms, uint32_t *uptime_sec);
|
||||
|
||||
/** @brief 执行带重试的 SPI 同步传输。 */
|
||||
esp_err_t transferWithRetry(uint8_t cmd, const uint8_t *payload,
|
||||
size_t payload_len, uint8_t *recv_buf,
|
||||
size_t recv_len, int max_retries);
|
||||
|
||||
// ============================================================
|
||||
// CANFD 接口
|
||||
// ============================================================
|
||||
|
||||
/** @brief 发送一帧 CANFD 数据,可选等待并读取响应内容。 */
|
||||
int canSend(can_channel_t channel, uint32_t ext_id,
|
||||
const uint8_t *data, uint8_t dlc,
|
||||
uint8_t *recv_buf, uint8_t recv_max,
|
||||
uint32_t timeout_ms);
|
||||
|
||||
/** @brief 发送一帧无需响应的 CANFD 数据。 */
|
||||
int canSendNoReply(can_channel_t channel, uint32_t ext_id,
|
||||
const uint8_t *data, uint8_t dlc);
|
||||
|
||||
/** @brief 获取 SPI 设备句柄(供外部状态查询)。 */
|
||||
spi_device_handle_t deviceHandle() const { return m_spiDevice; }
|
||||
|
||||
private:
|
||||
SpiMaster() = default;
|
||||
~SpiMaster() = default;
|
||||
SpiMaster(const SpiMaster&) = delete;
|
||||
SpiMaster& operator=(const SpiMaster&) = delete;
|
||||
|
||||
spi_device_handle_t m_spiDevice = NULL;
|
||||
|
||||
// 内部 SPI 传输
|
||||
esp_err_t transferClocked(const uint8_t *tx_buf, size_t tx_len,
|
||||
uint8_t *rx_buf, size_t rx_len);
|
||||
esp_err_t transferRequestResponse(uint8_t cmd, const uint8_t *payload, size_t payload_len,
|
||||
uint8_t *rx_buf, size_t rx_len);
|
||||
|
||||
// OTA 帧工具(静态,无状态)
|
||||
static uint8_t otaCrc8(const uint8_t *data, size_t len);
|
||||
static bool otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len,
|
||||
uint8_t *out, size_t *out_len);
|
||||
static bool otaParseFrame(const uint8_t *frame, size_t frame_len,
|
||||
uint8_t *type, const uint8_t **payload, size_t *payload_len);
|
||||
static bool otaFindFrame(const uint8_t *buf, size_t buf_len,
|
||||
uint8_t *type, const uint8_t **payload, size_t *payload_len,
|
||||
size_t *frame_offset);
|
||||
};
|
||||
|
||||
#endif // SPI_MASTER_H
|
||||
Reference in New Issue
Block a user