docs(ota_mutex): 添加函数注释说明

This commit is contained in:
小龙虾
2026-06-15 01:14:29 +08:00
parent 929faf1d38
commit 2875ef2145
17 changed files with 1026 additions and 0 deletions
+2
View File
@@ -14,6 +14,7 @@ extern volatile bool g_esp32OtaOccupied;
// 获取全局 OTA 占用标志(带超时),成功返回 true
// 内部: Take Mutex → 检查标志 → 若空闲则置 true → Give Mutex
/** @brief 获取全局 OTA 占用标志(带超时),成功返回 true。 */
static inline bool esp32_ota_mutex_acquire(uint32_t timeout_ms) {
if (!g_esp32OtaMutex) return false;
if (xSemaphoreTake(g_esp32OtaMutex, pdMS_TO_TICKS(timeout_ms)) != pdTRUE) return false;
@@ -28,6 +29,7 @@ static inline bool esp32_ota_mutex_acquire(uint32_t timeout_ms) {
// 释放全局 OTA 占用标志
// 内部: Take Mutex → 清除标志 → Give Mutex
/** @brief 释放全局 OTA 占用标志。 */
static inline void esp32_ota_mutex_release(void) {
if (!g_esp32OtaMutex) return;
xSemaphoreTake(g_esp32OtaMutex, portMAX_DELAY);
+49
View File
@@ -33,6 +33,10 @@ static const char *TAG = "DEBUG_LOG";
// ============================================================
// 单例
// ============================================================
/**
* @brief 获取 DebugLog 单例实例。
* @return DebugLog 引用。
*/
DebugLog& DebugLog::instance()
{
static DebugLog inst;
@@ -42,6 +46,10 @@ DebugLog& DebugLog::instance()
// ============================================================
// 日志环形缓冲区(供 HTTP /logs 拉取)
// ============================================================
/**
* @brief 获取当前环形日志缓冲区中的日志条数。
* @return 日志条数。
*/
int DebugLog::ringCount()
{
if (!m_logRingMutex) return 0;
@@ -51,6 +59,11 @@ int DebugLog::ringCount()
return count;
}
/**
* @brief 读取并清空环形日志缓冲区,返回 JSON 数组字符串。
* @param out_len 输出参数,返回 JSON 字符串长度。
* @return 指向 JSON 字符串的指针,需调用者 free。
*/
char *DebugLog::ringDrainJson(int *out_len)
{
if (!m_logRingMutex) {
@@ -117,6 +130,11 @@ char *DebugLog::ringDrainJson(int *out_len)
return json;
}
/**
* @brief 向环形日志缓冲区中推送一条日志数据。
* @param data 日志数据指针。
* @param len 数据长度。
*/
void DebugLog::ringPush(const char *data, int len)
{
if (!m_logRingMutex || len <= 0) return;
@@ -139,6 +157,10 @@ void DebugLog::ringPush(const char *data, int len)
// ============================================================
// UART 调试日志
// ============================================================
/**
* @brief 初始化 UART0 调试串口。
* @return esp_err_t 错误码。
*/
esp_err_t DebugLog::uartInit()
{
if (m_uartInitialized) {
@@ -180,6 +202,12 @@ esp_err_t DebugLog::uartInit()
return ESP_OK;
}
/**
* @brief 通过 UART0 直接发送原始字节数据。
* @param data 待发送数据指针。
* @param len 数据长度。
* @return esp_err_t 错误码。
*/
esp_err_t DebugLog::uartSend(const uint8_t *data, size_t len)
{
if (!m_uartInitialized) {
@@ -197,6 +225,10 @@ esp_err_t DebugLog::uartSend(const uint8_t *data, size_t len)
// ============================================================
// 初始化(UART 立即可用,UDP socket 延迟到首次发送时创建)
// ============================================================
/**
* @brief 初始化日志系统,包括 UART 输出、UDP 目标和内部缓冲区。
* @return esp_err_t 错误码。
*/
esp_err_t DebugLog::init()
{
if (m_initialized) {
@@ -219,6 +251,10 @@ esp_err_t DebugLog::init()
return ESP_OK;
}
/**
* @brief 确保 UDP socket 已创建(延迟创建)。
* @return esp_err_t 错误码。
*/
esp_err_t DebugLog::ensureUdpSocket()
{
if (m_udpSock >= 0) {
@@ -235,6 +271,12 @@ esp_err_t DebugLog::ensureUdpSocket()
return ESP_OK;
}
/**
* @brief 发送原始日志数据到底层输出通道。
* @param data 待发送数据指针。
* @param len 数据长度。
* @return esp_err_t 错误码。
*/
esp_err_t DebugLog::send(const uint8_t *data, size_t len)
{
esp_err_t udp_ret = ESP_FAIL;
@@ -270,6 +312,13 @@ esp_err_t DebugLog::send(const uint8_t *data, size_t len)
return ESP_FAIL;
}
/**
* @brief 按日志级别与标签格式化输出一条日志。
* @param level 日志级别。
* @param tag 日志标签。
* @param fmt 格式化字符串。
* @param ... 可变参数。
*/
void DebugLog::log(debug_log_level_t level, const char *tag, const char *fmt, ...)
{
if (!m_initialized) {
+4
View File
@@ -48,6 +48,10 @@ typedef enum {
*/
class DebugLog {
public:
/**
* @brief 获取 DebugLog 单例实例。
* @return DebugLog 引用。
*/
static DebugLog& instance();
/** @brief 初始化日志系统,包括 UART 输出、UDP 目标和内部缓冲区。 */
+55
View File
@@ -23,6 +23,10 @@
// ============================================================
// 单例
// ============================================================
/**
* @brief 获取 HealthMonitor 单例实例。
* @return HealthMonitor 引用。
*/
HealthMonitor& HealthMonitor::instance()
{
static HealthMonitor inst;
@@ -32,6 +36,9 @@ HealthMonitor& HealthMonitor::instance()
// ============================================================
// 初始化
// ============================================================
/**
* @brief 初始化健康监控模块,创建内部互斥锁与基础状态。
*/
void HealthMonitor::init()
{
m_mutex = xSemaphoreCreateMutex();
@@ -43,6 +50,10 @@ void HealthMonitor::init()
// ============================================================
// 线程安全的健康状态读写
// ============================================================
/**
* @brief 加锁读取当前健康状态。
* @return 当前健康状态。
*/
health_state_t HealthMonitor::getState()
{
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
@@ -51,6 +62,10 @@ health_state_t HealthMonitor::getState()
return s;
}
/**
* @brief 加锁设置健康状态。
* @param newState 新的健康状态。
*/
void HealthMonitor::setState(health_state_t newState)
{
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
@@ -61,11 +76,19 @@ void HealthMonitor::setState(health_state_t newState)
// ============================================================
// 获取当前健康状态(public 接口)
// ============================================================
/**
* @brief 获取当前系统健康状态(线程安全)。
* @return 当前健康状态。
*/
health_state_t HealthMonitor::getCurrent()
{
return getState();
}
/**
* @brief 获取最近一次芯片温度。
* @return 温度值(摄氏度)。
*/
float HealthMonitor::getLastTemperature()
{
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
@@ -74,6 +97,12 @@ float HealthMonitor::getLastTemperature()
return t;
}
/**
* @brief 获取最近一次 WiFi RSSI 及其年龄。
* @param out_rssi 输出参数,返回 RSSI 值。
* @param out_age_ms 输出参数,返回样本年龄(毫秒)。
* @return 是否有有效数据。
*/
bool HealthMonitor::getLastWifiRssi(int8_t *out_rssi, uint32_t *out_age_ms)
{
if (m_mutex) xSemaphoreTake(m_mutex, portMAX_DELAY);
@@ -96,6 +125,10 @@ bool HealthMonitor::getLastWifiRssi(int8_t *out_rssi, uint32_t *out_age_ms)
// ============================================================
// 温度监控(定时器回调)
// ============================================================
/**
* @brief 温度监控定时器回调函数。
* @param arg 用户参数(未使用)。
*/
void HealthMonitor::tempMonitorCallback(void *arg)
{
auto& self = instance();
@@ -127,6 +160,10 @@ void HealthMonitor::tempMonitorCallback(void *arg)
// ============================================================
// FreeRTOS 任务入口
// ============================================================
/**
* @brief 健康监控 FreeRTOS 任务入口。
* @param pvParameters 任务参数。
*/
void HealthMonitor::taskEntry(void *pvParameters)
{
auto& self = instance();
@@ -173,6 +210,11 @@ void HealthMonitor::taskEntry(void *pvParameters)
// ============================================================
// 健康事件日志
// ============================================================
/**
* @brief 记录一条健康事件日志。
* @param event 事件名称。
* @param value 事件值。
*/
void HealthMonitor::logEvent(const char *event, int value)
{
DEBUG_INFO("HEALTH", "[健康事件] %s: %d", event, value);
@@ -181,6 +223,10 @@ void HealthMonitor::logEvent(const char *event, int value)
// ============================================================
// OTA 健康检查
// ============================================================
/**
* @brief OTA 传输前执行健康检查。
* @return 健康检查状态。
*/
health_status_t HealthMonitor::checkBeforeOta()
{
health_status_t status = HEALTH_MON_OK;
@@ -227,6 +273,10 @@ health_status_t HealthMonitor::checkBeforeOta()
return status;
}
/**
* @brief OTA 中继转发阶段执行健康检查(跳过 WiFi 检查)。
* @return 健康检查状态。
*/
health_status_t HealthMonitor::checkBeforeOtaRelay()
{
health_status_t status = HEALTH_MON_OK;
@@ -261,6 +311,11 @@ health_status_t HealthMonitor::checkBeforeOtaRelay()
// ============================================================
// WiFi RSSI 更新
// ============================================================
/**
* @brief 根据 WiFi RSSI 更新健康状态。
* @param rssi WiFi RSSI 值。
* @return 健康检查状态。
*/
health_status_t HealthMonitor::updateFromWifiRssi(int8_t rssi)
{
if (rssi < WIFI_RSSI_CRITICAL) {
+4
View File
@@ -37,6 +37,10 @@ typedef enum {
*/
class HealthMonitor {
public:
/**
* @brief 获取 HealthMonitor 单例实例。
* @return HealthMonitor 引用。
*/
static HealthMonitor& instance();
/** @brief 初始化健康监控模块,创建内部互斥锁与基础状态。 */
+37
View File
@@ -23,12 +23,19 @@
#define TAG "OTA_CORE"
/**
* @brief 获取 Esp32OtaCore 单例实例。
* @return Esp32OtaCore 引用。
*/
Esp32OtaCore& Esp32OtaCore::instance()
{
static Esp32OtaCore inst;
return inst;
}
/**
* @brief 复位所有状态变量。
*/
void Esp32OtaCore::resetState()
{
m_active = false;
@@ -40,6 +47,11 @@ void Esp32OtaCore::resetState()
m_ended = false;
}
/**
* @brief 开启 OTA 会话。
* @param total_size 固件总大小(字节)。
* @return ESP_OK 成功,ESP_ERR_INVALID_STATE 被占用,ESP_FAIL 其他错误。
*/
esp_err_t Esp32OtaCore::begin(size_t total_size)
{
// 若本核心已有未完成会话,先中止并释放全局互斥
@@ -109,6 +121,12 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
return ESP_OK;
}
/**
* @brief 写入固件数据块。
* @param data 数据缓冲区。
* @param len 数据长度。
* @return ESP_OK 成功,否则错误码。
*/
esp_err_t Esp32OtaCore::write(const uint8_t *data, size_t len)
{
if (!m_active || m_partition == nullptr) {
@@ -124,6 +142,10 @@ esp_err_t Esp32OtaCore::write(const uint8_t *data, size_t len)
return err;
}
/**
* @brief 从 OTA 分区读回数据并计算 SHA-256。
* @return ESP_OK 成功,否则错误码。
*/
esp_err_t Esp32OtaCore::computeShaFromPartition()
{
if (m_partition == NULL || m_received == 0) {
@@ -172,6 +194,11 @@ esp_err_t Esp32OtaCore::computeShaFromPartition()
return ESP_OK;
}
/**
* @brief 计算设备端 SHA-256 并输出 hex 字符串。
* @param out_device_sha_hex 输出缓冲区,至少 65 字节。
* @return ESP_OK 成功,否则错误码。
*/
esp_err_t Esp32OtaCore::computeDeviceSha(char *out_device_sha_hex)
{
if (!m_active || m_partition == NULL) {
@@ -191,6 +218,13 @@ esp_err_t Esp32OtaCore::computeDeviceSha(char *out_device_sha_hex)
return ESP_OK;
}
/**
* @brief 收尾 OTA 会话:end、校验 SHA、set_boot、可选重启。
* @param frontend_sha_hex 前端提供的 64 字符 hex SHA-256。
* @param reboot 成功后是否重启。
* @param out_device_sha_hex 输出设备端 SHA hex 缓冲区。
* @return ESP_OK 成功,ESP_ERR_INVALID_VERSION SHA 不匹配,否则错误码。
*/
esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
char *out_device_sha_hex)
{
@@ -287,6 +321,9 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
return ESP_OK;
}
/**
* @brief 中止 OTA 会话并清理资源。
*/
void Esp32OtaCore::abort()
{
if (!m_active) {
+41
View File
@@ -22,15 +22,30 @@
class Esp32OtaCore {
public:
/**
* @brief 获取 Esp32OtaCore 单例实例。
* @return Esp32OtaCore 引用。
*/
static Esp32OtaCore& instance();
// ── 阶段 1:开启 OTA 会话 ──
// total_size: 固件总大小(字节),用于分区大小校验和传给 esp_ota_begin
// 返回 ESP_OK / ESP_ERR_INVALID_STATE(被其它通道占用)/ ESP_FAIL
/**
* @brief 开启 OTA 会话。
* @param total_size 固件总大小(字节)。
* @return ESP_OK 成功,ESP_ERR_INVALID_STATE 被占用,ESP_FAIL 其他错误。
*/
esp_err_t begin(size_t total_size);
// ── 阶段 2:写入固件数据(可在 begin 后多次调用)──
// 纯 esp_ota_write,不做增量 SHA(SHA 在 finalize 时统一从分区读回计算)
/**
* @brief 写入固件数据块。
* @param data 数据缓冲区。
* @param len 数据长度。
* @return ESP_OK 成功,否则错误码。
*/
esp_err_t write(const uint8_t *data, size_t len);
// ── 阶段 3:收尾(end → 校验 SHA → set_boot → 可选重启)──
@@ -41,6 +56,13 @@ public:
// ESP_OK 成功(若 reboot=true,本调用不会返回)
// ESP_ERR_INVALID_VERSION SHA 不匹配(已 abort 并清理)
// 其他 end/set_boot 失败(已清理)
/**
* @brief 收尾 OTA 会话:end、校验 SHA、set_boot、可选重启。
* @param frontend_sha_hex 前端提供的 64 字符 hex SHA-256。
* @param reboot 成功后是否重启。
* @param out_device_sha_hex 输出设备端 SHA hex 缓冲区。
* @return ESP_OK 成功,ESP_ERR_INVALID_VERSION SHA 不匹配。
*/
esp_err_t finalize(const char *frontend_sha_hex, bool reboot,
char *out_device_sha_hex);
@@ -48,25 +70,44 @@ public:
// 供前端只读校验场景(如 WiFi verify_only 模式)
// out_device_sha_hex: 输出缓冲区,至少 65 字节
// 返回 ESP_OK / 失败
/**
* @brief 仅计算设备端 SHA(不 end、不 set_boot、不重启)。
* @param out_device_sha_hex 输出缓冲区,至少 65 字节。
* @return ESP_OK 成功,否则错误码。
*/
esp_err_t computeDeviceSha(char *out_device_sha_hex);
// ── 中止并清理(abort + 释放 mutex + 状态复位)──
// 若 esp_ota_end 已成功(m_ended=true),则跳过 esp_ota_abort 仅复位状态
/**
* @brief 中止并清理 OTA 会话。
*/
void abort();
// ── 状态查询 ──
/** @brief 查询 OTA 是否激活。 */
bool isActive() const { return m_active; }
/** @brief 获取已接收的字节数。 */
size_t getReceived() const { return m_received; }
/** @brief 查询是否等待重启。 */
bool isRebootPending() const { return m_rebootPending; }
/** @brief 设置重启待处理标志。 */
void setRebootPending(bool v) { m_rebootPending = v; }
private:
Esp32OtaCore() = default;
// 从 OTA 分区读回计算 SHA-256(WiFi 风格),结果写入 m_sha256,置 m_shaComputed=true
/**
* @brief 从 OTA 分区读回计算 SHA-256。
* @return ESP_OK 成功,否则错误码。
*/
esp_err_t computeShaFromPartition();
// 状态全复位(不调 esp_ota_abort,由调用方根据 m_ended 决定)
/**
* @brief 复位所有状态(不调 esp_ota_abort)。
*/
void resetState();
bool m_active = false;
+162
View File
@@ -36,11 +36,18 @@ QueueHandle_t OtaRelay::dispatchQueue = NULL;
QueueHandle_t OtaRelay::resultQueue = NULL;
SemaphoreHandle_t OtaRelay::s_stateMutex = NULL;
/**
* @brief 获取OtaRelay单例引用.
* @return OtaRelay单例引用.
*/
OtaRelay& OtaRelay::instance()
{
static OtaRelay inst;
return inst;
}
/**
* @brief 初始化状态互斥锁.
*/
void OtaRelay::initMutex()
{
if (!s_stateMutex) {
@@ -48,30 +55,50 @@ void OtaRelay::initMutex()
}
}
/**
* @brief 获取当前中继状态.
* @return 当前中继状态.
*/
relay_state_t OtaRelay::getState()
{
StateLockGuard lock;
return relayState;
}
/**
* @brief 获取中继当前序号.
* @return 当前序号值.
*/
uint32_t OtaRelay::getRelaySeq()
{
StateLockGuard lock;
return relaySeq;
}
/**
* @brief 获取中继总包数.
* @return 总包数.
*/
uint32_t OtaRelay::getRelayTotalPackets()
{
StateLockGuard lock;
return relayTotalPackets;
}
/**
* @brief 获取GD32 APP存活标志位.
* @return true 存活,false 未存活.
*/
bool OtaRelay::getGd32AppAlive()
{
StateLockGuard lock;
return gd32AppAlive;
}
/**
* @brief 获取GD32固件版本.
* @param out 输出参数,版本结构体指针.
*/
void OtaRelay::getGd32Version(gd32_version_t *out)
{
if (!out) return;
@@ -79,30 +106,50 @@ void OtaRelay::getGd32Version(gd32_version_t *out)
*out = gd32Version;
}
/**
* @brief 设置中继状态.
* @param s 目标状态值.
*/
void OtaRelay::setState(relay_state_t s)
{
StateLockGuard lock;
relayState = s;
}
/**
* @brief 设置中继当前序号.
* @param seq 序号值.
*/
void OtaRelay::setRelaySeq(uint32_t seq)
{
StateLockGuard lock;
relaySeq = seq;
}
/**
* @brief 设置中继总包数.
* @param total 总包数.
*/
void OtaRelay::setRelayTotalPackets(uint32_t total)
{
StateLockGuard lock;
relayTotalPackets = total;
}
/**
* @brief 设置GD32 APP存活标志位.
* @param alive 存活状态.
*/
void OtaRelay::setGd32AppAlive(bool alive)
{
StateLockGuard lock;
gd32AppAlive = alive;
}
/**
* @brief 设置GD32固件版本.
* @param ver 版本结构体引用.
*/
void OtaRelay::setGd32Version(const gd32_version_t &ver)
{
StateLockGuard lock;
@@ -115,6 +162,13 @@ void OtaRelay::setGd32Version(const gd32_version_t &ver)
// 内部工具函数
// ============================================================
/**
* @brief 检查缓冲区是否全部为指定值.
* @param buf 缓冲区指针.
* @param len 缓冲区长度.
* @param value 待检查的值.
* @return true 全部为指定值,false 不是.
*/
static bool bytesAllValue(const uint8_t *buf, size_t len, uint8_t value)
{
if (!buf || len == 0) return false;
@@ -124,6 +178,13 @@ static bool bytesAllValue(const uint8_t *buf, size_t len, uint8_t value)
return true;
}
/**
* @brief 使用AES-128-ECB加密单个16字节块.
* @param key 16字节密钥.
* @param in 16字节输入数据.
* @param out 16字节输出数据.
* @return 0 成功,-1 失败.
*/
int OtaRelay::aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, uint8_t *out)
{
esp_aes_context ctx;
@@ -134,6 +195,13 @@ int OtaRelay::aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in,
return ret;
}
/**
* @brief 记录响应载荷日志.
* @param stage 阶段名称字符串.
* @param resp_type 响应类型.
* @param payload 载荷指针.
* @param len 载荷长度.
*/
void OtaRelay::logRespPayload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len)
{
char hex[193] = {0};
@@ -145,6 +213,17 @@ void OtaRelay::logRespPayload(const char *stage, uint8_t resp_type, const uint8_
n > 0 ? hex : "<empty>", len > n ? "..." : "");
}
/**
* @brief 循环等待直到响应不再是WAIT状态.
* @param stage 阶段名称字符串.
* @param resp_type 响应类型输出.
* @param resp_payload 响应载荷缓冲区.
* @param resp_max 缓冲区最大长度.
* @param resp_len 实际响应长度输出.
* @param timeout_ms 单次轮询超时毫秒.
* @param max_wait_rounds 最大WAIT轮次.
* @return ESP_OK 成功,其他失败.
*/
esp_err_t OtaRelay::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)
@@ -162,6 +241,13 @@ esp_err_t OtaRelay::waitUntilNotWait(const char *stage, uint8_t *resp_type,
return ESP_OK;
}
/**
* @brief 判断是否为带序号的结构化ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @param expected_seq 期望的序号.
* @return true 匹配成功,false 不匹配.
*/
bool OtaRelay::isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32_t expected_seq)
{
if (!payload || len < 6) return false;
@@ -171,6 +257,14 @@ bool OtaRelay::isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32
return seq == expected_seq;
}
/**
* @brief 解析结构化错误帧.
* @param payload 载荷指针.
* @param len 载荷长度.
* @param seq_out 输出错误序号.
* @param err_out 输出错误码.
* @return true 解析成功,false 解析失败.
*/
bool OtaRelay::parseStructuredError(const uint8_t *payload, size_t len, uint32_t *seq_out, uint8_t *err_out)
{
if (!payload || len < 7) return false;
@@ -181,18 +275,48 @@ bool OtaRelay::parseStructuredError(const uint8_t *payload, size_t len, uint32_t
return true;
}
/**
* @brief 判断是否为握手回显ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是握手回显ACK,false 不是.
*/
bool OtaRelay::isHandshakeEchoAck(const uint8_t *payload, size_t len)
{ return payload && len >= 2U && payload[0] == 0x00U && payload[1] == 0xACU; }
/**
* @brief 判断是否为挑战应答ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是挑战应答ACK,false 不是.
*/
bool OtaRelay::isChallengePayloadAck(const uint8_t *payload, size_t len)
{ return payload && len == GD32_CHALLENGE_RESPONSE_LEN; }
/**
* @brief 判断是否为普通确认ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是确认ACK,false 不是.
*/
bool OtaRelay::isConfirmPlainAck(const uint8_t *payload, size_t len)
{ return payload && len == 2U && payload[0] == 0xBAU && payload[1] == 0xC0U; }
/**
* @brief 判断是否为大小序号0的ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是大小序号0ACK,false 不是.
*/
bool OtaRelay::isSizeSeq0Ack(const uint8_t *payload, size_t len)
{ return isStructuredAckWithSeq(payload, len, 0U); }
/**
* @brief 判断是否为普通的结束ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是结束ACK,false 不是.
*/
bool OtaRelay::isFinishPlainAck(const uint8_t *payload, size_t len)
{
if (!payload) return false;
@@ -201,6 +325,19 @@ bool OtaRelay::isFinishPlainAck(const uint8_t *payload, size_t len)
return false;
}
/**
* @brief 轮询直到收到预期的ACK形状.
* @param stage 阶段名称字符串.
* @param resp_type 响应类型输出.
* @param resp_payload 响应载荷缓冲区.
* @param resp_max 缓冲区最大长度.
* @param resp_len 实际响应长度输出.
* @param poll_timeout_ms 轮询超时毫秒.
* @param wait_rounds WAIT状态最大轮询次数.
* @param maxExtraPolls 额外轮询次数.
* @param matcher 匹配回调函数.
* @return ESP_OK 成功,其他失败.
*/
esp_err_t OtaRelay::pollUntilExpectedAckShape(const char *stage,
uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len,
@@ -233,6 +370,14 @@ esp_err_t OtaRelay::pollUntilExpectedAckShape(const char *stage,
return ESP_OK;
}
/**
* @brief 解析并验证挑战应答载荷.
* @param resp_payload 响应载荷指针.
* @param resp_len 响应长度.
* @param out_k_session 输出会话密钥.
* @param out_uid 输出GD32 UID.
* @return true 验证通过,false 验证失败.
*/
bool OtaRelay::parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_t resp_len,
uint8_t *out_k_session, uint8_t *out_uid)
{
@@ -265,6 +410,13 @@ bool OtaRelay::parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_
return true;
}
/**
* @brief 使用AES-128-ECB加密48字节数据.
* @param key 16字节密钥.
* @param in 48字节输入数据.
* @param out 48字节输出数据.
* @return true 加密成功,false 加密失败.
*/
bool OtaRelay::aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, uint8_t *out)
{
for (int i = 0; i < 3; ++i)
@@ -272,6 +424,12 @@ bool OtaRelay::aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, ui
return true;
}
/**
* @brief 使用密钥计算48字节数据的CRC32.
* @param key 16字节密钥.
* @param data48 48字节输入数据.
* @param output 输出4字节CRC32结果.
*/
void OtaRelay::calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, uint8_t *output)
{
uint32_t crc = 0xFFFFFFFFU;
@@ -613,6 +771,10 @@ esp_err_t OtaRelay::requestGd32AppEnterBootloader()
}
/**
* @brief 阶段1 - 发送握手请求并等待确认.
* @return 操作结果码.
*/
ota_result_t OtaRelay::sendHandshakeReq()
{
DEBUG_INFO("OTA", "=== 阶段1: 握手开始 ===");
+246
View File
@@ -57,9 +57,21 @@ typedef enum {
class OtaRelay {
public:
/**
* @brief 获取OtaRelay单例引用.
* @return OtaRelay单例引用.
*/
static OtaRelay& instance();
/**
* @brief OTA中继FreeRTOS任务入口.
* @param pvParameters FreeRTOS任务参数.
*/
static void taskEntry(void *pvParameters);
/**
* @brief 从Flash向GD32转发固件(主流程).
* @return 操作结果码.
*/
ota_result_t forwardFirmware();
/* ---- 线程安全的状态访问接口 ----
@@ -67,27 +79,117 @@ public:
* 状态机内部(OTA 任务)可直接写 static 成员,
* 跨任务读/写必须走这些 API。
*/
/**
* @brief 获取当前中继状态.
* @return 当前中继状态.
*/
static relay_state_t getState();
/**
* @brief 获取中继当前序号.
* @return 当前序号值.
*/
static uint32_t getRelaySeq();
/**
* @brief 获取中继总包数.
* @return 总包数.
*/
static uint32_t getRelayTotalPackets();
/**
* @brief 获取GD32 APP存活标志位.
* @return true 存活,false 未存活.
*/
static bool getGd32AppAlive();
/**
* @brief 获取GD32固件版本.
* @param out 输出参数,版本结构体指针.
*/
static void getGd32Version(gd32_version_t *out);
/**
* @brief 设置中继状态.
* @param s 目标状态值.
*/
static void setState(relay_state_t s);
/**
* @brief 设置中继当前序号.
* @param seq 序号值.
*/
static void setRelaySeq(uint32_t seq);
/**
* @brief 设置中继总包数.
* @param total 总包数.
*/
static void setRelayTotalPackets(uint32_t total);
/**
* @brief 设置GD32 APP存活标志位.
* @param alive 存活状态.
*/
static void setGd32AppAlive(bool alive);
/**
* @brief 获取GD32固件版本.
* @param ver 待设置的版本结构体引用.
*/
static void setGd32Version(const gd32_version_t &ver);
/**
* @brief 重置状态机.
*/
void reset();
/**
* @brief Flash写入接口(支持非4字节对齐写入).
* @param offset 分区内写入偏移.
* @param data 待写入的数据指针.
* @param len 数据长度(字节).
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len);
/**
* @brief 从OTA缓存分区读取数据块.
* @param offset 分区内读取偏移.
* @param data 读取缓冲区指针.
* @param len 读取长度(字节).
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t flashReadChunk(uint32_t offset, uint8_t *data, size_t len);
/**
* @brief 从Flash加载元数据.
* @param meta 输出参数,加载后的元数据结构体指针.
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t metaLoad(ota_meta_t *meta);
/**
* @brief 保存元数据到Flash.
* @param meta 待保存的元数据结构体指针.
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t metaSave(const ota_meta_t *meta);
/**
* @brief 传输过程中更新元数据.
* @param stored_size 已存储的固件大小(字节).
* @param last_seq 最后成功的分块序号.
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t metaUpdate(uint32_t stored_size, uint32_t last_seq);
/**
* @brief OTA完成或中止时清除元数据.
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t metaClear();
/**
* @brief 计算gd32_ota分区中已存储固件的SHA-256.
* @param out_sha256 输出32字节SHA-256摘要.
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t computeFlashSha256(uint8_t out_sha256[32]);
/**
* @brief 固件接收完毕后计算SHA-256并写入元数据.
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t metaFinalizeChecksum();
/**
* @brief 校验固件完整性(SHA-256).
* @return ESP_OK 校验通过,ESP_ERR_INVALID_CRC 校验失败.
*/
static esp_err_t metaVerifyChecksum();
// 公开状态(状态机内部直接访问;跨任务请用 getter/setter)
@@ -101,17 +203,31 @@ public:
// 线程安全锁
static SemaphoreHandle_t s_stateMutex;
/**
* @brief 初始化状态互斥锁.
*/
static void initMutex();
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50);
class StateLockGuard {
public:
/**
* @brief 构造时获取互斥锁.
* @param timeout 等待超时(时钟节拍),默认50ms.
*/
explicit StateLockGuard(TickType_t timeout = kLockTimeout) {
m_locked = (s_stateMutex && xSemaphoreTake(s_stateMutex, timeout) == pdTRUE);
}
/**
* @brief 析构时释放互斥锁.
*/
~StateLockGuard() {
if (m_locked && s_stateMutex) { xSemaphoreGive(s_stateMutex); }
}
/**
* @brief 检查是否成功获取锁.
* @return true 已锁定,false 未锁定.
*/
bool isLocked() const { return m_locked; }
StateLockGuard(const StateLockGuard&) = delete;
StateLockGuard& operator=(const StateLockGuard&) = delete;
@@ -129,28 +245,137 @@ public:
static constexpr size_t GD32_UID_LEN = 12;
static constexpr size_t GD32_CHALLENGE_RESPONSE_LEN = 36;
/**
* @brief 请求GD32 APP进入Bootloader.
* @return 操作结果码.
*/
esp_err_t requestGd32AppEnterBootloader();
/**
* @brief 发送握手请求.
* @return 操作结果码.
*/
ota_result_t sendHandshakeReq();
/**
* @brief 发送随机数并与GD32相互认证.
* @return 操作结果码.
*/
ota_result_t sendNonceAndAuthenticate();
/**
* @brief 向GD32发送固件总大小.
* @param size 固件总字节数.
* @return 操作结果码.
*/
ota_result_t sendFirmwareSize(uint32_t size);
/**
* @brief 从Flash向GD32转发固件包.
* @param deadline_tick 截止时间戳(可选),0表示无超时.
* @return 操作结果码.
*/
ota_result_t relayPacketsFromFlash(TickType_t deadline_tick = 0);
/**
* @brief 完成OTA并发送结束指令.
* @return 操作结果码.
*/
ota_result_t finishOta();
/**
* @brief 使用AES-128-ECB加密单个16字节块.
* @param key 16字节密钥.
* @param in 16字节输入数据.
* @param out 16字节输出数据.
* @return 0 成功,-1 失败.
*/
static int aes128EcbEncryptBlockLocal(const uint8_t *key, const uint8_t *in, uint8_t *out);
/**
* @brief 记录响应载荷日志.
* @param stage 阶段名称字符串.
* @param resp_type 响应类型.
* @param payload 载荷指针.
* @param len 载荷长度.
*/
static void logRespPayload(const char *stage, uint8_t resp_type, const uint8_t *payload, size_t len);
/**
* @brief 循环等待直到响应不再是WAIT状态.
* @param stage 阶段名称字符串.
* @param resp_type 响应类型输出.
* @param resp_payload 响应载荷缓冲区.
* @param resp_max 缓冲区最大长度.
* @param resp_len 实际响应长度输出.
* @param timeout_ms 单次轮询超时毫秒.
* @param max_wait_rounds 最大WAIT轮次.
* @return ESP_OK 成功,其他失败.
*/
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 判断是否为带序号的结构化ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @param expected_seq 期望的序号.
* @return true 匹配成功,false 不匹配.
*/
static bool isStructuredAckWithSeq(const uint8_t *payload, size_t len, uint32_t expected_seq);
/**
* @brief 解析结构化错误帧.
* @param payload 载荷指针.
* @param len 载荷长度.
* @param seq_out 输出错误序号.
* @param err_out 输出错误码.
* @return true 解析成功,false 解析失败.
*/
static bool parseStructuredError(const uint8_t *payload, size_t len, uint32_t *seq_out, uint8_t *err_out);
/**
* @brief 判断是否为握手回显ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是握手回显ACK,false 不是.
*/
static bool isHandshakeEchoAck(const uint8_t *payload, size_t len);
/**
* @brief 判断是否为挑战应答ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是挑战应答ACK,false 不是.
*/
static bool isChallengePayloadAck(const uint8_t *payload, size_t len);
/**
* @brief 判断是否为普通确认ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是确认ACK,false 不是.
*/
static bool isConfirmPlainAck(const uint8_t *payload, size_t len);
/**
* @brief 判断是否为大小序号0的ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是大小序号0ACK,false 不是.
*/
static bool isSizeSeq0Ack(const uint8_t *payload, size_t len);
/**
* @brief 判断是否为普通的结束ACK.
* @param payload 载荷指针.
* @param len 载荷长度.
* @return true 是结束ACK,false 不是.
*/
static bool isFinishPlainAck(const uint8_t *payload, size_t len);
/**
* @brief 轮询直到收到预期的ACK形状.
* @param stage 阶段名称字符串.
* @param resp_type 响应类型输出.
* @param resp_payload 响应载荷缓冲区.
* @param resp_max 响应缓冲区最大长度.
* @param resp_len 实际响应长度输出.
* @param poll_timeout_ms 轮询超时毫秒.
* @param wait_rounds WAIT状态最大轮询次数.
* @param maxExtraPolls 额外轮询次数.
* @param matcher 匹配回调函数.
* @return ESP_OK 成功,其他失败.
*/
static esp_err_t pollUntilExpectedAckShape(const char *stage,
uint8_t *resp_type,
uint8_t *resp_payload,
@@ -160,9 +385,30 @@ public:
uint32_t wait_rounds,
uint32_t maxExtraPolls,
bool (*matcher)(const uint8_t *, size_t));
/**
* @brief 解析并验证挑战应答载荷.
* @param resp_payload 响应载荷指针.
* @param resp_len 响应长度.
* @param out_k_session 输出会话密钥.
* @param out_uid 输出GD32 UID.
* @return true 验证通过,false 验证失败.
*/
static bool parseAndVerifyChallengePayload(const uint8_t *resp_payload, size_t resp_len,
uint8_t *out_k_session, uint8_t *out_uid);
/**
* @brief 使用AES-128-ECB加密48字节数据.
* @param key 16字节密钥.
* @param in 48字节输入数据.
* @param out 48字节输出数据.
* @return true 加密成功,false 加密失败.
*/
static bool aes128EcbEncrypt48Bytes(const uint8_t *key, const uint8_t *in, uint8_t *out);
/**
* @brief 使用密钥计算48字节数据的CRC32.
* @param key 16字节密钥.
* @param data48 48字节输入数据.
* @param output 输出4字节CRC32结果.
*/
static void calculateCrc32WithKey(const uint8_t *key, const uint8_t *data48, uint8_t *output);
private:
+8
View File
@@ -71,6 +71,10 @@ static void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes);
static void sendFinishEvent(uint8_t stage, uint32_t value);
// ============================================================
/**
* @brief 获取 BleOta 单例实例。
* @return BleOta 类引用。
*/
BleOta& BleOta::instance()
{
static BleOta inst;
@@ -103,6 +107,10 @@ static void ble_esp32_restart_task(void *pvParameters)
// 不会返回
}
/**
* @brief 异步完成 ESP32 OTA 收尾并触发重启。
* @param pvParameters 前端 SHA-256 十六进制字符串(堆分配,任务负责释放)。
*/
static void ble_esp32_finish_and_reboot_task(void *pvParameters)
{
// pvParameters: heap-allocated char[65] frontend SHA hex string (or nullptr)
+25
View File
@@ -14,12 +14,37 @@
class BleOta {
public:
/**
* @brief 获取 BleOta 单例实例。
* @return BleOta 类引用。
*/
static BleOta& instance();
/**
* @brief BLE OTA FreeRTOS 任务入口。
* @param pvParameters 任务参数,未使用。
*/
static void taskEntry(void *pvParameters);
/**
* @brief 启动 BLE GATT 服务器并注册 OTA 服务。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t serverStart();
/**
* @brief 通过 BLE Notify 推送响应数据。
* @param data 待推送的数据指针。
* @param len 数据长度。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t notifyResponse(const uint8_t *data, size_t len);
/**
* @brief 检查 BLE 客户端连接状态。
* @return 已连接返回 true,未连接返回 false。
*/
bool isConnected();
/**
* @brief 触发中继模块向 GD32 转发固件。
*/
void signalRelay();
// 以下状态为 public,供 C 回调函数访问
+10
View File
@@ -34,6 +34,10 @@
// ============================================================
/**
* @brief 获取 WifiOta 单例实例(静态局部变量实现)。
* @return WifiOta 单例引用。
*/
WifiOta& WifiOta::instance()
{
static WifiOta inst;
@@ -42,6 +46,12 @@ WifiOta& WifiOta::instance()
static constexpr uint32_t GD32_OTA_FLASH_SECTOR_SIZE = 4096U;
/**
* @brief 按指定对齐值向上取整。
* @param value 待对齐的值。
* @param alignment 对齐字节数。
* @return 对齐后的值。
*/
static uint32_t align_up_u32(uint32_t value, uint32_t alignment)
{
return ((value + alignment - 1U) / alignment) * alignment;
+71
View File
@@ -21,31 +21,71 @@ typedef enum {
class WifiOta {
public:
/**
* @brief 获取 WifiOta 单例实例。
* @return WifiOta 单例引用。
*/
static WifiOta& instance();
/**
* @brief 在调度器启动前初始化 WiFi 并尝试 STA 连接。
* @return true 表示 STA 初始化中,false 需回退至 AP 模式。
*/
bool earlyInit();
/**
* @brief WiFi OTA FreeRTOS 任务入口,提供 HTTP 上传和中继触发。
* @param pvParameters FreeRTOS 任务参数。
*/
static void taskEntry(void *pvParameters);
/**
* @brief 通知 OTA 中继开始向 GD32 转发固件。
*/
void signalRelay();
/**
* @brief 获取当前 WiFi 工作模式。
* @return 当前 WiFi 模式(AP 或 STA)。
*/
static ota_wifi_mode_t getMode();
/**
* @brief 获取当前 WiFi 信号强度。
* @return 信号强度值(dBm)。
*/
static int8_t getRssi();
/**
* @brief 判断 STA 是否已连接到上级 WiFi。
* @return true 已连接,false 未连接。
*/
static bool isConnected();
// ── WiFi 连接状态(WiFi 事件回调 / HTTP / reconnect task 共享,需 mutex 保护)──
/** @brief 获取 STA 连接状态。 */
bool getStaConnected() const { return m_staConnected.load(); }
/** @brief 设置 STA 连接状态。 */
void setStaConnected(bool v) { m_staConnected.store(v); }
/** @brief 获取 STA 重试次数。 */
int getStaRetryCount() const { return m_staRetryCount.load(); }
/** @brief 设置 STA 重试次数。 */
void setStaRetryCount(int v) { m_staRetryCount.store(v); }
/** @brief 获取 STA 重试是否已耗尽。 */
bool getStaRetryExhausted() const { return m_staRetryExhausted.load(); }
/** @brief 设置 STA 重试耗尽标志。 */
void setStaRetryExhausted(bool v) { m_staRetryExhausted.store(v); }
/** @brief 获取 GD32 固件存储完成标志。 */
bool getGd32StorageDone() const { return m_gd32StorageDone.load(); }
/** @brief 设置 GD32 固件存储完成标志。 */
void setGd32StorageDone(bool v) { m_gd32StorageDone.store(v); }
/** @brief 获取中继触发标志。 */
bool getRelayTriggered() const { return m_relayTriggered.load(); }
/** @brief 设置中继触发标志。 */
void setRelayTriggered(bool v) { m_relayTriggered.store(v); }
// ── 状态互斥锁(保护复合读-改-写操作)──
/** @brief 获取状态互斥锁句柄。 */
SemaphoreHandle_t getStateMutex() const { return m_stateMutex; }
/** @brief 锁定状态互斥锁。 */
void stateLock() { if (m_stateMutex) xSemaphoreTake(m_stateMutex, portMAX_DELAY); }
/** @brief 解锁状态互斥锁。 */
void stateUnlock() { if (m_stateMutex) xSemaphoreGive(m_stateMutex); }
// ── 初始化/运行时状态(仅 OTA 任务访问,无需额外锁)──
@@ -66,13 +106,44 @@ private:
std::atomic<bool> m_relayTriggered{false};
SemaphoreHandle_t m_stateMutex = NULL;
/**
* @brief 初始化 SoftAP 模式,供局域网 OTA 使用。
*/
void initAp();
/**
* @brief 初始化 STA 模式并连接预配置路由器。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t initSta();
/**
* @brief 设置 STA 静态 IP、网关和子网掩码。
*/
void setStaticIp();
/**
* @brief WiFi/IP 事件回调,处理连接、断开和 IP 获取。
* @param arg 回调用户参数。
* @param event_base 事件基类。
* @param event_id 具体事件 ID。
* @param event_data 事件附带数据。
*/
static void wifiEventHandler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data);
/**
* @brief STA 后台重连任务,处理退避延迟和重连。
* @param pvParameters FreeRTOS 任务参数。
*/
static void reconnectTask(void *pvParameters);
/**
* @brief 启动 HTTP OTA 服务器,注册各接口路由。
* @return ESP_OK 成功,其他失败。
*/
esp_err_t httpServerStart();
/**
* @brief 等待 STA 连接成功并获取 IP 地址。
* @param ip_str 输出缓冲区,存放 IP 字符串。
* @param ip_len 缓冲区长度。
* @return ESP_OK 连接成功,ESP_ERR_TIMEOUT 超时。
*/
esp_err_t waitStaConnected(char *ip_str, size_t ip_len);
};
+7
View File
@@ -20,12 +20,19 @@
// ============================================================
// Protocol 单例
// ============================================================
/**
* @brief 获取 Protocol 单例实例。
* @return Protocol 单例引用。
*/
Protocol& Protocol::instance()
{
static Protocol inst;
return inst;
}
/**
* @brief 初始化协议层,打印初始化信息并设置初始化标志。
*/
void Protocol::init()
{
if (m_initialized) return;
+24
View File
@@ -37,13 +37,37 @@ typedef struct {
*/
class Protocol {
public:
/**
* @brief 获取 Protocol 单例实例。
* @return Protocol 单例引用。
*/
static Protocol& instance();
/**
* @brief 初始化协议层。
*/
void init();
/**
* @brief 生成基于时间戳、地址和随机种子的扰码 nonce。
* @param timestamp 时间戳。
* @param mem_addr 内存地址。
* @param rand_seed 随机种子。
* @param nonce_out 输出的 16 字节 nonce 缓冲区。
*/
static void scrambleGenerate(uint32_t timestamp, uint32_t mem_addr,
uint32_t rand_seed, uint8_t *nonce_out);
/**
* @brief 会话密钥初始化,将密钥拷贝到会话上下文。
* @param sess 会话上下文指针。
* @param k_session 会话密钥数据。
* @return 成功返回 0,参数无效返回 -1。
*/
static int sessionKeyInit(session_ctx_t *sess, const uint8_t *k_session);
/**
* @brief 清除会话上下文的密钥和 IV 数据。
* @param sess 会话上下文指针。
*/
static void sessionKeyClear(session_ctx_t *sess);
private:
+173
View File
@@ -23,6 +23,10 @@ static constexpr size_t kSpiTransferMaxBytes = 256U;
// ============================================================
// 单例
// ============================================================
/**
* @brief 获取 SpiMaster 单例实例(单例模式)。
* @return SpiMaster 单例引用。
*/
SpiMaster& SpiMaster::instance()
{
static SpiMaster inst;
@@ -32,6 +36,15 @@ SpiMaster& SpiMaster::instance()
// ============================================================
// 内部 SPI 传输
// ============================================================
/**
* @brief 执行底层 SPI 时钟同步传输。
* @param tx_buf 发送数据缓冲区,可为 NULL。
* @param tx_len 发送数据长度。
* @param rx_buf 接收数据缓冲区,可为 NULL。
* @param rx_len 接收数据长度。
* @param timeout_ms 超时时间(毫秒),0 表示无限等待。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len,
uint8_t *rx_buf, size_t rx_len,
uint32_t timeout_ms)
@@ -100,6 +113,16 @@ esp_err_t SpiMaster::transferClocked(const uint8_t *tx_buf, size_t tx_len,
return ESP_OK;
}
/**
* @brief 执行 SPI 请求-响应组合传输。
* @param cmd 命令字节(0 表示无命令)。
* @param payload 负载数据缓冲区。
* @param payload_len 负载数据长度。
* @param rx_buf 接收数据缓冲区。
* @param rx_len 期望接收的数据长度。
* @param timeout_ms 超时时间(毫秒),0 表示无限等待。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::transferRequestResponse(uint8_t cmd,
const uint8_t *payload, size_t payload_len,
uint8_t *rx_buf, size_t rx_len,
@@ -146,6 +169,12 @@ esp_err_t SpiMaster::transferRequestResponse(uint8_t cmd,
// ============================================================
// OTA 帧工具
// ============================================================
/**
* @brief 计算 OTA 帧的 8 位 CRC 校验和。
* @param data 数据缓冲区。
* @param len 数据长度。
* @return 计算得到的 CRC 值。
*/
uint8_t SpiMaster::otaCrc8(const uint8_t *data, size_t len)
{
uint8_t crc = 0x00;
@@ -155,6 +184,15 @@ uint8_t SpiMaster::otaCrc8(const uint8_t *data, size_t len)
return crc;
}
/**
* @brief 构建 OTA 协议帧。
* @param type 帧类型。
* @param payload 负载数据缓冲区。
* @param payload_len 负载数据长度。
* @param out 输出帧缓冲区。
* @param out_len 输出实际帧长度。
* @return true 表示构建成功,false 表示失败。
*/
bool SpiMaster::otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len,
uint8_t *out, size_t *out_len)
{
@@ -173,6 +211,15 @@ bool SpiMaster::otaBuildFrame(uint8_t type, const uint8_t *payload, size_t paylo
return true;
}
/**
* @brief 解析 OTA 协议帧。
* @param frame 帧数据缓冲区。
* @param frame_len 帧数据长度。
* @param type 输出解析得到的帧类型。
* @param payload 输出指向负载数据的指针。
* @param payload_len 输出负载数据长度。
* @return true 表示解析成功,false 表示失败。
*/
bool SpiMaster::otaParseFrame(const uint8_t *frame, size_t frame_len,
uint8_t *type, const uint8_t **payload, size_t *payload_len)
{
@@ -192,6 +239,16 @@ bool SpiMaster::otaParseFrame(const uint8_t *frame, size_t frame_len,
return true;
}
/**
* @brief 在数据缓冲区中搜索并解析 OTA 帧。
* @param buf 数据缓冲区。
* @param buf_len 数据缓冲区长度。
* @param type 输出帧类型。
* @param payload 输出指向负载数据的指针。
* @param payload_len 输出负载数据长度。
* @param frame_offset 输出帧在缓冲区中的偏移位置。
* @return true 表示找到并成功解析,false 表示未找到。
*/
bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len,
uint8_t *type, const uint8_t **payload, size_t *payload_len,
size_t *frame_offset)
@@ -222,6 +279,10 @@ bool SpiMaster::otaFindFrame(const uint8_t *buf, size_t buf_len,
// ============================================================
// 初始化
// ============================================================
/**
* @brief 初始化 SPI2 总线与目标从设备。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::init()
{
LockGuard lock(m_mutex);
@@ -274,6 +335,16 @@ esp_err_t SpiMaster::init()
// ============================================================
// 公开 SPI 传输接口
// ============================================================
/**
* @brief 执行一次同步 SPI 事务,先发送请求,再读取响应字节。
* @param cmd 命令字节。
* @param payload 负载数据缓冲区。
* @param payload_len 负载数据长度。
* @param recv_buf 接收数据缓冲区。
* @param recv_len 接收数据长度。
* @param timeout_ms 超时时间(毫秒)。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::syncTransfer(uint8_t cmd, const uint8_t *payload,
size_t payload_len, uint8_t *recv_buf,
size_t recv_len, uint32_t timeout_ms)
@@ -282,12 +353,27 @@ esp_err_t SpiMaster::syncTransfer(uint8_t cmd, const uint8_t *payload,
return transferRequestResponse(cmd, payload, payload_len, recv_buf, recv_len, timeout_ms);
}
/**
* @brief 仅发送 SPI 请求数据,不关心返回内容。
* @param cmd 命令字节。
* @param payload 负载数据缓冲区。
* @param len 负载数据长度。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::send(uint8_t cmd, const uint8_t *payload, size_t len)
{
LockGuard lock(m_mutex);
return transferRequestResponse(cmd, payload, len, NULL, 0, 0U);
}
/**
* @brief 仅接收 SPI 数据,必要时先发送命令字节。
* @param cmd 命令字节。
* @param recv_buf 接收数据缓冲区。
* @param len 接收数据长度。
* @param timeout_ms 超时时间(毫秒)。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t timeout_ms)
{
LockGuard lock(m_mutex);
@@ -297,6 +383,18 @@ esp_err_t SpiMaster::recv(uint8_t cmd, uint8_t *recv_buf, size_t len, uint32_t t
// ============================================================
// SPI OTA 协议
// ============================================================
/**
* @brief 按 GD32 SPI OTA 最小协议执行一次请求加响应读取。
* @param type 请求帧类型。
* @param payload 负载数据缓冲区。
* @param payload_len 负载数据长度。
* @param resp_type 输出响应类型。
* @param resp_payload 输出响应负载缓冲区。
* @param resp_max 响应负载缓冲区最大长度。
* @param resp_len 输出实际响应长度。
* @param timeout_ms 超时时间(毫秒)。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::otaTransfer(uint8_t type, const uint8_t *payload, size_t payload_len,
uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
@@ -319,6 +417,15 @@ esp_err_t SpiMaster::otaTransfer(uint8_t type, const uint8_t *payload, size_t pa
return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, timeout_ms);
}
/**
* @brief 仅执行 SPI OTA 响应读取事务,不重复发送请求帧。
* @param resp_type 输出响应类型。
* @param resp_payload 输出响应负载缓冲区。
* @param resp_max 响应负载缓冲区最大长度。
* @param resp_len 输出实际响应长度。
* @param timeout_ms 超时时间(毫秒)。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
{
@@ -326,6 +433,15 @@ esp_err_t SpiMaster::otaPollResponse(uint8_t *resp_type, uint8_t *resp_payload,
return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, timeout_ms);
}
/**
* @brief 无锁版本的 OTA 响应轮询实现。
* @param resp_type 输出响应类型。
* @param resp_payload 输出响应负载缓冲区。
* @param resp_max 响应负载缓冲区最大长度。
* @param resp_len 输出实际响应长度。
* @param timeout_ms 超时时间(毫秒)。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len, uint32_t timeout_ms)
{
@@ -390,6 +506,10 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p
return ESP_ERR_TIMEOUT;
}
/**
* @brief 通知 GD32 DCU_APP 设置 OTA 标志并软复位进入 Bootloader。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::appRequestBootloader()
{
LockGuard lock(m_mutex);
@@ -429,6 +549,13 @@ esp_err_t SpiMaster::appRequestBootloader()
return ESP_ERR_TIMEOUT;
}
/**
* @brief 向 GD32 APP 发送存活探测命令,等待 APP 回复 ALIVE ACK。
* @param timeout_ms 超时时间(毫秒)。
* @param uptime_sec 输出设备运行时间(秒)。
* @param version 输出 GD32 版本信息(可选)。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_version_t *version)
{
LockGuard lock(m_mutex);
@@ -546,6 +673,16 @@ esp_err_t SpiMaster::appPing(uint32_t timeout_ms, uint32_t *uptime_sec, gd32_ver
return ESP_ERR_TIMEOUT;
}
/**
* @brief 执行带重试的 SPI 同步传输。
* @param cmd 命令字节。
* @param payload 负载数据缓冲区。
* @param payload_len 负载数据长度。
* @param recv_buf 接收数据缓冲区。
* @param recv_len 接收数据长度。
* @param max_retries 最大重试次数。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::transferWithRetry(uint8_t cmd, const uint8_t *payload,
size_t payload_len, uint8_t *recv_buf,
size_t recv_len, int max_retries)
@@ -579,6 +716,17 @@ esp_err_t SpiMaster::transferWithRetry(uint8_t cmd, const uint8_t *payload,
// ============================================================
// CANFD 接口
// ============================================================
/**
* @brief 发送一帧 CANFD 数据,可选等待并读取响应内容。
* @param channel CAN 通道编号(0~2)。
* @param ext_id 扩展帧 ID(29 位)。
* @param data CAN 数据负载。
* @param dlc 数据长度。
* @param recv_buf 接收数据缓冲区。
* @param recv_max 接收缓冲区最大长度。
* @param timeout_ms 超时时间(毫秒)。
* @return 成功时返回接收数据长度,失败返回 -1。
*/
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,
@@ -608,6 +756,14 @@ int SpiMaster::canSend(can_channel_t channel, uint32_t ext_id,
return (int)recv_max;
}
/**
* @brief 发送一帧无需响应的 CANFD 数据。
* @param channel CAN 通道编号(0~2)。
* @param ext_id 扩展帧 ID(29 位)。
* @param data CAN 数据负载。
* @param dlc 数据长度。
* @return 成功返回 0,失败返回 -1。
*/
int SpiMaster::canSendNoReply(can_channel_t channel, uint32_t ext_id,
const uint8_t *data, uint8_t dlc)
{
@@ -638,6 +794,16 @@ int SpiMaster::canSendNoReply(can_channel_t channel, uint32_t ext_id,
// ============================================================
// 无锁内部版本实现
// ============================================================
/**
* @brief 无锁版本的同步 SPI 传输实现。
* @param cmd 命令字节。
* @param payload 负载数据缓冲区。
* @param payload_len 负载数据长度。
* @param recv_buf 接收数据缓冲区。
* @param recv_len 接收数据长度。
* @param timeout_ms 超时时间(毫秒)。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::syncTransferUnlocked(uint8_t cmd, const uint8_t *payload,
size_t payload_len, uint8_t *recv_buf,
size_t recv_len, uint32_t timeout_ms)
@@ -645,6 +811,13 @@ esp_err_t SpiMaster::syncTransferUnlocked(uint8_t cmd, const uint8_t *payload,
return transferRequestResponse(cmd, payload, payload_len, recv_buf, recv_len, timeout_ms);
}
/**
* @brief 无锁版本的 SPI 发送实现。
* @param cmd 命令字节。
* @param payload 负载数据缓冲区。
* @param len 负载数据长度。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t SpiMaster::sendUnlocked(uint8_t cmd, const uint8_t *payload, size_t len)
{
return transferRequestResponse(cmd, payload, len, NULL, 0, 0U);
+108
View File
@@ -74,6 +74,10 @@ typedef struct {
*/
class SpiMaster {
public:
/**
* @brief 获取 SpiMaster 单例实例。
* @return SpiMaster 单例引用。
*/
static SpiMaster& instance();
/** @brief 初始化 SPI2 总线与目标从设备。 */
@@ -136,9 +140,21 @@ public:
spi_device_handle_t deviceHandle() const { return m_spiDevice; }
private:
/**
* @brief 私有默认构造函数。
*/
SpiMaster() = default;
/**
* @brief 私有析构函数。
*/
~SpiMaster() = default;
/**
* @brief 禁止复制构造。
*/
SpiMaster(const SpiMaster&) = delete;
/**
* @brief 禁止复制赋值。
*/
SpiMaster& operator=(const SpiMaster&) = delete;
spi_device_handle_t m_spiDevice = NULL;
@@ -147,40 +163,132 @@ private:
// RAII 锁守卫
class LockGuard {
public:
/**
* @brief 构造加锁守卫,获取互斥锁。
* @param mutex 互斥锁句柄。
*/
explicit LockGuard(SemaphoreHandle_t mutex) : m_lock(mutex) {
if (m_lock) { xSemaphoreTake(m_lock, portMAX_DELAY); }
}
/**
* @brief 析构解锁守卫,释放互斥锁。
*/
~LockGuard() {
if (m_lock) { xSemaphoreGive(m_lock); }
}
/**
* @brief 禁止复制构造。
*/
LockGuard(const LockGuard&) = delete;
/**
* @brief 禁止复制赋值。
*/
LockGuard& operator=(const LockGuard&) = delete;
private:
SemaphoreHandle_t m_lock;
};
// 内部 SPI 传输
/**
* @brief 执行底层 SPI 时钟同步传输。
* @param tx_buf 发送数据缓冲区。
* @param tx_len 发送数据长度。
* @param rx_buf 接收数据缓冲区。
* @param rx_len 接收数据长度。
* @param timeout_ms 超时时间(毫秒),0 表示无限等待。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t transferClocked(const uint8_t *tx_buf, size_t tx_len,
uint8_t *rx_buf, size_t rx_len,
uint32_t timeout_ms = 0U);
/**
* @brief 执行 SPI 请求-响应组合传输。
* @param cmd 命令字节(0 表示无命令)。
* @param payload 负载数据缓冲区。
* @param payload_len 负载数据长度。
* @param rx_buf 接收数据缓冲区。
* @param rx_len 期望接收的数据长度。
* @param timeout_ms 超时时间(毫秒),0 表示无限等待。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t transferRequestResponse(uint8_t cmd, const uint8_t *payload, size_t payload_len,
uint8_t *rx_buf, size_t rx_len,
uint32_t timeout_ms = 0U);
// 无锁内部版本(供已持锁的公共方法调用,避免递归死锁)
/**
* @brief 无锁版本的同步 SPI 传输(调用者须已持有锁)。
* @param cmd 命令字节。
* @param payload 负载数据缓冲区。
* @param payload_len 负载数据长度。
* @param recv_buf 接收数据缓冲区。
* @param recv_len 接收数据长度。
* @param timeout_ms 超时时间(毫秒)。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t syncTransferUnlocked(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 命令字节。
* @param payload 负载数据缓冲区。
* @param len 负载数据长度。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t sendUnlocked(uint8_t cmd, const uint8_t *payload, size_t len);
/**
* @brief 无锁版本的 OTA 响应轮询(调用者须已持有锁)。
* @param resp_type 输出响应类型。
* @param resp_payload 输出响应负载缓冲区。
* @param resp_max 响应负载缓冲区最大长度。
* @param resp_len 输出实际响应长度。
* @param timeout_ms 超时时间(毫秒)。
* @return ESP_OK 表示成功,否则返回错误码。
*/
esp_err_t otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_payload,
size_t resp_max, size_t *resp_len, uint32_t timeout_ms);
// OTA 帧工具(静态,无状态)
/**
* @brief 计算 OTA 帧的 8 位 CRC 校验和。
* @param data 数据缓冲区。
* @param len 数据长度。
* @return 计算得到的 CRC 值。
*/
static uint8_t otaCrc8(const uint8_t *data, size_t len);
/**
* @brief 构建 OTA 协议帧。
* @param type 帧类型。
* @param payload 负载数据缓冲区。
* @param payload_len 负载数据长度。
* @param out 输出帧缓冲区。
* @param out_len 输出实际帧长度。
* @return true 表示构建成功,false 表示失败。
*/
static bool otaBuildFrame(uint8_t type, const uint8_t *payload, size_t payload_len,
uint8_t *out, size_t *out_len);
/**
* @brief 解析 OTA 协议帧。
* @param frame 帧数据缓冲区。
* @param frame_len 帧数据长度。
* @param type 输出解析得到的帧类型。
* @param payload 输出指向负载数据的指针。
* @param payload_len 输出负载数据长度。
* @return true 表示解析成功,false 表示失败。
*/
static bool otaParseFrame(const uint8_t *frame, size_t frame_len,
uint8_t *type, const uint8_t **payload, size_t *payload_len);
/**
* @brief 在数据缓冲区中搜索并解析 OTA 帧。
* @param buf 数据缓冲区。
* @param buf_len 数据缓冲区长度。
* @param type 输出帧类型。
* @param payload 输出指向负载数据的指针。
* @param payload_len 输出负载数据长度。
* @param frame_offset 输出帧在缓冲区中的偏移位置。
* @return true 表示找到并成功解析,false 表示未找到。
*/
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);