feat(spi): 增强 SPI master:性能剖析与执行耗时跟踪
- 新增 `lastWorkerExecUs()`:获取最近一次 worker 任务执行耗时(µs)。 - 引入 `SpiProfile` 结构体:事务级详细剖析,记录 SPI 事务各阶段的时间戳。 - 实现 `lastProfile()`:诊断用,获取最近一次事务剖析快照。 - SPI 时钟配置提升至 13.3 MHz,改善性能。 - 修改前端 JavaScript 轮询逻辑:新增 uptime 检查,更好地处理 ESP32 重启状态。 - SPI 测速 HTML 的计时单位由毫秒改为微秒显示。 - 新建 `spi_ring_cache.h`:共享 SPI ring push 缓存工具。 - 新增 `http_util.h`:共享 HTTP 处理器工具,含 JSON 拼接与查询参数解析。 - 引入 `json_parse.h`:轻量级 JSON 解析器,针对扁平 JSON 对象设计。 - 新增 Windows 环境下的项目构建批处理脚本。 - 重构 CAN 反馈处理:从推送式改为请求驱动架构,降低内存占用、提升效率。
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
## 方案:/can/feedback 改为请求驱动架构
|
||||
|
||||
### 目标
|
||||
- ESP32 不再被动缓存 GD32 的 0xC1 推送,改为前端请求时主动向 GD32 查询
|
||||
- GD32 停止主动推送 0xC1(降低 SPI 资源占用)
|
||||
- 删除 `s_latest[3]`/`s_work[3]` 常驻 BSS(省 ~7.8KB)+ handler 栈上 `results[3]`(省 ~3.9KB 栈)
|
||||
|
||||
### 依据
|
||||
- GD32 `handleFeedback` 已支持 0xC1 请求查询(`[channel, page]` → ACK + 页数据),无需新增协议
|
||||
- ESP32 `transact(0xC1, ...)` 是现成请求/响应机制,`canFdSend`/`canFdSetRxMode` 已在用
|
||||
- `canFdParseFeedbackPage` 可直接复用(只解析 payload 字节,不关心来源)
|
||||
- 0xC4/0xC5/0xC6/0xD2 推送独立,不受影响
|
||||
|
||||
### 改动清单
|
||||
|
||||
#### 1. ESP32 侧 — `main/transport/canfd_bridge.cpp` / `.h`
|
||||
新增 `canFdQueryFeedback(channel, page, can_feedback_result_t *out, uint8_t *entryIdx)`:
|
||||
```cpp
|
||||
int canFdQueryFeedback(uint8_t channel, uint8_t page,
|
||||
can_feedback_result_t *out, uint8_t *entryIdx);
|
||||
```
|
||||
- 发 `transact(0xC1, {channel, page}, ...)`,超时 200ms
|
||||
- 成功(resp_type==ACK)→ 调 `canFdParseFeedbackPage(resp_buf, resp_len, out, entryIdx)`
|
||||
- 返回 0=成功,-1=SPI/参数失败,-2=GD32 拒绝(ERROR)
|
||||
|
||||
#### 2. ESP32 侧 — `main/server/http_handlers.cpp` `can_feedback_handler`
|
||||
- 删除 `results[CAN_CHANNEL_MAX]` 栈数组 + `getLatestCanFeedback` 循环
|
||||
- 改为:对每个使能通道,调 `canFdQueryFeedback(ch, 0, &result, &entryIdx)` + `canFdQueryFeedback(ch, 1, &result, &entryIdx)`,在栈上用单个 `can_feedback_result_t result`(1.3KB,用完即释放)
|
||||
- lite 路径:只需 page 0(统计字段在 page0 header),1 次 SPI 查询即可
|
||||
- full 路径:page 0 + page 1,2 次查询,然后序列化
|
||||
- 查询失败 → 该通道 `valid=false`(与原来无快照时行为一致)
|
||||
- `pauseCanFeedbackTask` 调用保留(send_batch 仍需独占 SPI)
|
||||
|
||||
#### 3. ESP32 侧 — `can_mode_handler` GET
|
||||
- 原来 `getLatestCanFeedback(0, &fb)` 读缓存 mode → 改为 `canFdQueryFeedback(0, 0, &fb, &entryIdx)` 取 page0 的 mode 字段
|
||||
- 原来的 fallback `canFdSetRxMode` 保留(查询也失败时兜底)
|
||||
|
||||
#### 4. ESP32 侧 — `main/monitor/can_feedback_task.cpp`
|
||||
- **删除** `s_latest[3]`、`s_valid[3]`、`s_work[3]`、`s_workIdx[3]`、`s_lastLoggedError[3]`、`publish()`、`getLatestCanFeedback()`
|
||||
- **删除** taskLoop 里的 0xC1 分支(`canFdParseFeedbackPage` + `publish`)
|
||||
- **保留** 0xC4/0xC5/0xC6/0xD2 事件分发(这些仍走推送)
|
||||
- **保留** `pauseCanFeedbackTask`(send_batch / OTA 仍需暂停事件消费)
|
||||
- `s_mutex` 删除(仅保护 s_latest/s_valid,不再需要)
|
||||
|
||||
#### 5. ESP32 侧 — `main/monitor/can_feedback_task.h`
|
||||
- 删除 `getLatestCanFeedback` 声明
|
||||
- 保留 `startCanFeedbackTask`、`pauseCanFeedbackTask`
|
||||
|
||||
#### 6. GD32 侧 — `app_spi_cmd_can_feedback.cpp`
|
||||
- `canFeedbackStartRound()`:改为空函数(或加 `s_pushEnabled=false` 永久禁用推送)
|
||||
- `canFeedbackPushStep()`:开头加 `if (!s_pushEnabled) return;` 或直接 return
|
||||
- **保留** `handleFeedback`(请求查询处理程序不动)
|
||||
- **保留** `buildFeedbackPayload`(查询和推送共用)
|
||||
|
||||
#### 7. GD32 侧 — `task_runner.cpp`
|
||||
- `canFeedbackStartRound()` 调用可保留(变成空函数无副作用)或删除
|
||||
|
||||
### 内存收益
|
||||
| 项 | 改前 | 改后 | 省 |
|
||||
|---|---|---|---|
|
||||
| `s_latest[3]` BSS | 3.9KB | 0 | 3.9KB |
|
||||
| `s_work[3]` BSS | 3.9KB | 0 | 3.9KB |
|
||||
| handler 栈 `results[3]` | 3.9KB | 1.3KB(单通道) | 2.6KB 栈 |
|
||||
| **合计常驻 BSS** | | | **~7.8KB** |
|
||||
|
||||
### 性能影响
|
||||
- 原来读缓存:<1ms
|
||||
- 改后查询:lite 路径 1 次 SPI 往返 ~2-4ms;full 路径 6 次往返(3ch×2page)~12-24ms
|
||||
- 前端 1Hz 轮询,24ms 完全可接受(httpd 单线程最坏阻塞 <50ms)
|
||||
- GD32 不再 1Hz 推送 6 帧,省下 SPI 带宽和 PB6 中断
|
||||
|
||||
### 风险与缓解
|
||||
- **SPI 查询失败**:GD32 不响应时 `transact` 超时返回,handler 返回 valid=false,前端显示"--"(与原无快照行为一致)
|
||||
- **OTA 期间查询**:send_batch/OTA 已 `pauseCanFeedbackTask`,且 SPI worker 串行化,查询不会与 OTA 数据流冲突
|
||||
- **can_monitor.html full 路径延迟**:6 次 SPI 查询 ~24ms + JSON 序列化,总 <50ms,远小于 1s 轮询间隔
|
||||
|
||||
### 不改动的部分
|
||||
- 0xC4 ADC、0xC5 motor state、0xC6 motor version、0xD2 motor OTA 推送机制不变
|
||||
- `canFdParseFeedbackPage` 解析逻辑不变
|
||||
- GD32 `handleFeedback` 查询处理程序不变
|
||||
- 前端 JS 不变(API 响应格式不变)
|
||||
@@ -1,8 +0,0 @@
|
||||
idf_component_register(
|
||||
SRCS "cJSON.c"
|
||||
INCLUDE_DIRS "."
|
||||
REQUIRES
|
||||
)
|
||||
|
||||
# ESP-IDF uses no DLL, export all symbols
|
||||
target_compile_definitions(${COMPONENT_LIB} PUBLIC CJSON_HIDE_SYMBOLS=0)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,306 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#ifndef cJSON__h
|
||||
#define cJSON__h
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
|
||||
#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32))
|
||||
#define __WINDOWS__
|
||||
#endif
|
||||
|
||||
#ifdef __WINDOWS__
|
||||
|
||||
/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options:
|
||||
|
||||
CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols
|
||||
CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default)
|
||||
CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol
|
||||
|
||||
For *nix builds that support visibility attribute, you can define similar behavior by
|
||||
|
||||
setting default visibility to hidden by adding
|
||||
-fvisibility=hidden (for gcc)
|
||||
or
|
||||
-xldscope=hidden (for sun cc)
|
||||
to CFLAGS
|
||||
|
||||
then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does
|
||||
|
||||
*/
|
||||
|
||||
#define CJSON_CDECL __cdecl
|
||||
#define CJSON_STDCALL __stdcall
|
||||
|
||||
/* export symbols by default, this is necessary for copy pasting the C and header file */
|
||||
#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS)
|
||||
#define CJSON_EXPORT_SYMBOLS
|
||||
#endif
|
||||
|
||||
#if defined(CJSON_HIDE_SYMBOLS)
|
||||
#define CJSON_PUBLIC(type) type CJSON_STDCALL
|
||||
#elif defined(CJSON_EXPORT_SYMBOLS)
|
||||
#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL
|
||||
#elif defined(CJSON_IMPORT_SYMBOLS)
|
||||
#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL
|
||||
#endif
|
||||
#else /* !__WINDOWS__ */
|
||||
#define CJSON_CDECL
|
||||
#define CJSON_STDCALL
|
||||
|
||||
#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY)
|
||||
#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type
|
||||
#else
|
||||
#define CJSON_PUBLIC(type) type
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* project version */
|
||||
#define CJSON_VERSION_MAJOR 1
|
||||
#define CJSON_VERSION_MINOR 7
|
||||
#define CJSON_VERSION_PATCH 19
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* cJSON Types: */
|
||||
#define cJSON_Invalid (0)
|
||||
#define cJSON_False (1 << 0)
|
||||
#define cJSON_True (1 << 1)
|
||||
#define cJSON_NULL (1 << 2)
|
||||
#define cJSON_Number (1 << 3)
|
||||
#define cJSON_String (1 << 4)
|
||||
#define cJSON_Array (1 << 5)
|
||||
#define cJSON_Object (1 << 6)
|
||||
#define cJSON_Raw (1 << 7) /* raw json */
|
||||
|
||||
#define cJSON_IsReference 256
|
||||
#define cJSON_StringIsConst 512
|
||||
|
||||
/* The cJSON structure: */
|
||||
typedef struct cJSON
|
||||
{
|
||||
/* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
|
||||
struct cJSON *next;
|
||||
struct cJSON *prev;
|
||||
/* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
|
||||
struct cJSON *child;
|
||||
|
||||
/* The type of the item, as above. */
|
||||
int type;
|
||||
|
||||
/* The item's string, if type==cJSON_String and type == cJSON_Raw */
|
||||
char *valuestring;
|
||||
/* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
|
||||
int valueint;
|
||||
/* The item's number, if type==cJSON_Number */
|
||||
double valuedouble;
|
||||
|
||||
/* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
|
||||
char *string;
|
||||
} cJSON;
|
||||
|
||||
typedef struct cJSON_Hooks
|
||||
{
|
||||
/* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */
|
||||
void *(CJSON_CDECL *malloc_fn)(size_t sz);
|
||||
void (CJSON_CDECL *free_fn)(void *ptr);
|
||||
} cJSON_Hooks;
|
||||
|
||||
typedef int cJSON_bool;
|
||||
|
||||
/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them.
|
||||
* This is to prevent stack overflows. */
|
||||
#ifndef CJSON_NESTING_LIMIT
|
||||
#define CJSON_NESTING_LIMIT 1000
|
||||
#endif
|
||||
|
||||
/* Limits the length of circular references can be before cJSON rejects to parse them.
|
||||
* This is to prevent stack overflows. */
|
||||
#ifndef CJSON_CIRCULAR_LIMIT
|
||||
#define CJSON_CIRCULAR_LIMIT 10000
|
||||
#endif
|
||||
|
||||
/* returns the version of cJSON as a string */
|
||||
CJSON_PUBLIC(const char*) cJSON_Version(void);
|
||||
|
||||
/* Supply malloc, realloc and free functions to cJSON */
|
||||
CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks);
|
||||
|
||||
/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */
|
||||
/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length);
|
||||
/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */
|
||||
/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated);
|
||||
|
||||
/* Render a cJSON entity to text for transfer/storage. */
|
||||
CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);
|
||||
/* Render a cJSON entity to text for transfer/storage without any formatting. */
|
||||
CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item);
|
||||
/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */
|
||||
CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt);
|
||||
/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */
|
||||
/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format);
|
||||
/* Delete a cJSON entity and all subentities. */
|
||||
CJSON_PUBLIC(void) cJSON_Delete(cJSON *item);
|
||||
|
||||
/* Returns the number of items in an array (or object). */
|
||||
CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array);
|
||||
/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index);
|
||||
/* Get item "string" from object. Case insensitive. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string);
|
||||
/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
|
||||
CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void);
|
||||
|
||||
/* Check item type and return its value */
|
||||
CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item);
|
||||
CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item);
|
||||
|
||||
/* These functions check the type of an item */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item);
|
||||
|
||||
/* These calls create a cJSON item of the appropriate type. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string);
|
||||
/* raw json */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void);
|
||||
|
||||
/* Create a string where valuestring references a string so
|
||||
* it will not be freed by cJSON_Delete */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string);
|
||||
/* Create an object/array that only references it's elements so
|
||||
* they will not be freed by cJSON_Delete */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child);
|
||||
|
||||
/* These utilities create an Array of count items.
|
||||
* The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count);
|
||||
|
||||
/* Append item to the specified array/object. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item);
|
||||
/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object.
|
||||
* WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before
|
||||
* writing to `item->string` */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item);
|
||||
/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item);
|
||||
|
||||
/* Remove/Detach items from Arrays/Objects. */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which);
|
||||
CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string);
|
||||
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string);
|
||||
CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string);
|
||||
CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string);
|
||||
|
||||
/* Update array items. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem);
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem);
|
||||
|
||||
/* Duplicate a cJSON item */
|
||||
CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse);
|
||||
/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will
|
||||
* need to be released. With recurse!=0, it will duplicate any children connected to the item.
|
||||
* The item->next and ->prev pointers are always zero on return from Duplicate. */
|
||||
/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal.
|
||||
* case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */
|
||||
CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive);
|
||||
|
||||
/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings.
|
||||
* The input pointer json cannot point to a read-only address area, such as a string constant,
|
||||
* but should point to a readable and writable address area. */
|
||||
CJSON_PUBLIC(void) cJSON_Minify(char *json);
|
||||
|
||||
/* Helper functions for creating and adding items to an object at the same time.
|
||||
* They return the added item or NULL on failure. */
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name);
|
||||
CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name);
|
||||
|
||||
/* When assigning an integer value, it needs to be propagated to valuedouble too. */
|
||||
#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number))
|
||||
/* helper for the cJSON_SetNumberValue macro */
|
||||
CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number);
|
||||
#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number))
|
||||
/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */
|
||||
CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring);
|
||||
|
||||
/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/
|
||||
#define cJSON_SetBoolValue(object, boolValue) ( \
|
||||
(object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \
|
||||
(object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \
|
||||
cJSON_Invalid\
|
||||
)
|
||||
|
||||
/* Macro for iterating over an array or object */
|
||||
#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next)
|
||||
|
||||
/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */
|
||||
CJSON_PUBLIC(void *) cJSON_malloc(size_t size);
|
||||
CJSON_PUBLIC(void) cJSON_free(void *object);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -35,7 +35,6 @@ idf_component_register(
|
||||
REQUIRES
|
||||
transport # M5: 传输层组件(SPI/CANFD/协议)
|
||||
app_update # ESP32 OTA API
|
||||
cjson # JSON 解析/生成
|
||||
esp_driver_tsens # 温度传感器驱动
|
||||
esp_driver_uart # UART 驱动(调试日志)
|
||||
esp_wifi # WiFi STA/AP 模式
|
||||
|
||||
+3
-1
@@ -132,7 +132,9 @@ extern "C" void app_main(void)
|
||||
SystemBoot::mark(BOOT_OTA_VALID);
|
||||
}
|
||||
|
||||
DEBUG_INFO("MAIN", "ESP32-C3 OTA Relay %s, heap=%lu, wifi_early=%s",
|
||||
/* ★ banner 文案必须包含「启动」,前端 polling.js 靠
|
||||
* /OTA Relay .*启动/ 匹配本行判定 ESP32 重启完成(阶段 6)。 */
|
||||
DEBUG_INFO("MAIN", "ESP32-C3 OTA Relay %s 启动, heap=%lu, wifi_early=%s",
|
||||
FW_VERSION,
|
||||
(unsigned long)esp_get_free_heap_size(),
|
||||
wifi_early_started_sta ? "STA" : "AP/other");
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
// 固件版本
|
||||
#define FW_VERSION_MAJOR 0
|
||||
#define FW_VERSION_MINOR 3
|
||||
#define FW_VERSION_PATCH 74
|
||||
#define FW_VERSION_STRING "0.3.74" // can batch: 帧间隔 5→1ms;前端 BATCH_CHUNK=64 + fetch 超时加固 + finally 兜底 renderHistory
|
||||
#define FW_VERSION_PATCH 76
|
||||
#define FW_VERSION_STRING "0.3.76" // 重启恢复提速2:httpd就绪后再打 MAIN「启动」banner(防环缩容裁掉);前端用 uptime<120s 判重启完成,不再干等10s
|
||||
|
||||
// Git 信息(手动填写)
|
||||
#define GIT_HASH "770ae14"
|
||||
@@ -1,14 +1,11 @@
|
||||
/*
|
||||
* can_feedback_task.cpp - 被动监听 GD32 主动推送的 CAN 反馈快照
|
||||
* can_feedback_task.cpp - 被动监听 GD32 主动推送的事件
|
||||
*
|
||||
* 架构(GD32 主动推送,ESP32 被动接收):
|
||||
* GD32 APP 在主循环中调用 canFeedbackPushStep(),循环遍历 3 通道 × 2 页,
|
||||
* 通过 requestEsp32Read(0xC1) 下拉 DATA_READY 主动按铃,将每页反馈 payload
|
||||
* 推送给 ESP32。ESP32 本任务 SpiMaster::takeEvent() 阻塞等待按铃/事件,
|
||||
* 读到一帧后用 canFdParseFeedbackPage() 解析并累积到 s_work[channel],再
|
||||
* publish() 到 s_latest[channel] 供 Web 读取。
|
||||
*
|
||||
* 替代了原先 ESP32 1Hz 主动发 0xC1 轮询的逻辑,避免占用主线 SPI 带宽。
|
||||
* 架构(2026-08 改造后):
|
||||
* 0xC1 CAN 反馈已改为请求驱动(前端请求时 ESP32 主动向 GD32 查询),
|
||||
* 不再被动缓存。本任务仅保留 0xC4/0xC5/0xC6/0xD2 事件分发:
|
||||
* 0xC4 ADC 调试反馈、0xC5 多电机状态、0xC6 多电机版本、0xD2 电机 OTA 进度
|
||||
* 这些仍由 GD32 主动推送,ESP32 被动接收。
|
||||
*
|
||||
* OTA 期间通过 pauseCanFeedbackTask(true) 暂停本任务,避免与 OTA 抢占 SPI。
|
||||
*/
|
||||
@@ -17,7 +14,6 @@
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#include "app_config.h"
|
||||
#include "transport/canfd_bridge.h"
|
||||
#include "transport/spi_master.h"
|
||||
#include "monitor/debug_log.h"
|
||||
#include "monitor/can_feedback_task.h"
|
||||
@@ -25,7 +21,6 @@
|
||||
#include "monitor/motor_state.h"
|
||||
#include "monitor/motor_version.h"
|
||||
#include "monitor/motor_ota_relay.h"
|
||||
#include "app_config.h"
|
||||
#include "esp_system.h"
|
||||
|
||||
#include <string.h>
|
||||
@@ -34,7 +29,6 @@ namespace {
|
||||
|
||||
constexpr uint32_t kWaitRingMs = 500U; ///< 单次等待 GD32 按铃超时
|
||||
constexpr uint32_t kPauseDelayMs = 100U; ///< 暂停时的轮询间隔
|
||||
constexpr uint8_t kSpiCanFeedbackType = 0xC1U;
|
||||
constexpr uint8_t kSpiAdcFeedbackType = 0xC4U; ///< GD32 ADC 调试反馈
|
||||
constexpr uint8_t kSpiMotorStateType = 0xC5U; ///< GD32 多电机状态推送
|
||||
constexpr uint8_t kSpiMotorVersionType = 0xC6U; ///< GD32 多电机版本推送(事件驱动)
|
||||
@@ -42,44 +36,22 @@ constexpr uint8_t kSpiMotorOtaStatusType = 0xD2U; ///< GD32 电机 OTA relay
|
||||
constexpr size_t kRingPayloadMax = SPI_OTA_FRAME_MAX; ///< 与变长读一致,避免截断
|
||||
constexpr const char *kTag = "CAN_FB";
|
||||
|
||||
static can_feedback_result_t s_latest[CAN_CHANNEL_MAX];
|
||||
static bool s_valid [CAN_CHANNEL_MAX];
|
||||
static SemaphoreHandle_t s_mutex = nullptr;
|
||||
static volatile bool s_paused = false;
|
||||
|
||||
/* 累积工作缓冲:跨 page 0/1 累积每通道的完整反馈 */
|
||||
static can_feedback_result_t s_work [CAN_CHANNEL_MAX];
|
||||
static uint8_t s_workIdx[CAN_CHANNEL_MAX];
|
||||
static uint32_t s_lastLoggedError[CAN_CHANNEL_MAX];
|
||||
|
||||
/**
|
||||
* @brief 将某通道完整反馈发布到 s_latest(线程安全)。
|
||||
* @param ch 通道 0..2
|
||||
* @param fb 解析后的快照
|
||||
*/
|
||||
static void publish(uint8_t ch, const can_feedback_result_t &fb)
|
||||
{
|
||||
if (s_mutex == nullptr) return;
|
||||
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
s_latest[ch] = fb;
|
||||
s_valid[ch] = true;
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief can_fb 任务主循环:takeEvent 分发 0xC1 CAN / 0xC4 ADC。
|
||||
* @brief can_fb 任务主循环:takeEvent 分发 0xC4/0xC5/0xC6/0xD2 推送事件。
|
||||
* 0xC1 CAN 反馈已改为请求驱动(canFdQueryFeedback),不再在此处理。
|
||||
*/
|
||||
static void taskLoop(void *)
|
||||
{
|
||||
DEBUG_INFO(kTag, "CAN 反馈被动监听任务启动 (GD32 主动推送, 0xC1)");
|
||||
DEBUG_INFO(kTag, "事件监听任务启动 (0xC4 ADC / 0xC5 motor / 0xC6 ver / 0xD2 ota)");
|
||||
|
||||
/* 冷启动延后开始监听,错开上电初始化窗口 */
|
||||
{
|
||||
esp_reset_reason_t rr = esp_reset_reason();
|
||||
if (rr == ESP_RST_POWERON || rr == ESP_RST_BROWNOUT) {
|
||||
constexpr uint32_t kCanFbDeferMs = 1500U;
|
||||
DEBUG_INFO(kTag, "冷启动: %lu ms 后开始 CAN 反馈监听",
|
||||
DEBUG_INFO(kTag, "冷启动: %lu ms 后开始事件监听",
|
||||
(unsigned long)kCanFbDeferMs);
|
||||
vTaskDelay(pdMS_TO_TICKS(kCanFbDeferMs));
|
||||
}
|
||||
@@ -126,39 +98,14 @@ static void taskLoop(void *)
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ring_type != kSpiCanFeedbackType || ring_len < 28U) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint8_t ch = ring_payload[0];
|
||||
if (ch >= CAN_CHANNEL_MAX) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 解析本页并累积到工作缓冲,page=0 会自动重置 */
|
||||
if (canFdParseFeedbackPage(ring_payload, ring_len,
|
||||
&s_work[ch], &s_workIdx[ch]) == 0) {
|
||||
publish(ch, s_work[ch]);
|
||||
|
||||
/* 累计错误发生变化时才告警一次;P0/P1 携带相同统计,不能重复打印。 */
|
||||
if (s_work[ch].stats.error_count != 0U
|
||||
&& s_work[ch].stats.error_count != s_lastLoggedError[ch]) {
|
||||
DEBUG_WARN(kTag,
|
||||
"CH%u err=%lu TEC=%u REC=%u",
|
||||
static_cast<unsigned>(ch),
|
||||
static_cast<unsigned long>(s_work[ch].stats.error_count),
|
||||
static_cast<unsigned>(s_work[ch].stats.tec),
|
||||
static_cast<unsigned>(s_work[ch].stats.rec));
|
||||
s_lastLoggedError[ch] = s_work[ch].stats.error_count;
|
||||
}
|
||||
}
|
||||
/* 0xC1 已改为请求驱动,不再在此处理;未知类型静默跳过 */
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief 启动 CAN 反馈被动监听任务(幂等)。
|
||||
* @brief 启动事件监听任务(幂等)。
|
||||
* @return true 表示任务已存在或创建成功。
|
||||
*/
|
||||
bool startCanFeedbackTask()
|
||||
@@ -166,19 +113,6 @@ bool startCanFeedbackTask()
|
||||
static bool s_started = false;
|
||||
if (s_started) return true;
|
||||
|
||||
if (s_mutex == nullptr) {
|
||||
s_mutex = xSemaphoreCreateMutex();
|
||||
if (s_mutex == nullptr) {
|
||||
DEBUG_ERR(kTag, "互斥量创建失败");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
memset(s_latest, 0, sizeof(s_latest));
|
||||
memset(s_lastLoggedError, 0, sizeof(s_lastLoggedError));
|
||||
memset(s_valid, 0, sizeof(s_valid));
|
||||
memset(s_work, 0, sizeof(s_work));
|
||||
memset(s_workIdx, 0, sizeof(s_workIdx));
|
||||
|
||||
BaseType_t xRet = xTaskCreatePinnedToCore(
|
||||
taskLoop,
|
||||
"can_fb",
|
||||
@@ -196,7 +130,7 @@ bool startCanFeedbackTask()
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 暂停/恢复 CAN(及同任务分发的 ADC)事件监听。
|
||||
* @brief 暂停/恢复事件监听。
|
||||
* @param pause true=OTA 期间暂停;false=恢复
|
||||
*/
|
||||
void pauseCanFeedbackTask(bool pause)
|
||||
@@ -204,28 +138,17 @@ void pauseCanFeedbackTask(bool pause)
|
||||
if (s_paused == pause) return; // 幂等:状态未变时不重复日志
|
||||
s_paused = pause;
|
||||
if (pause) {
|
||||
DEBUG_DBG(kTag, "CAN 监听已暂停(OTA 进行中)");
|
||||
DEBUG_DBG(kTag, "事件监听已暂停(OTA 进行中)");
|
||||
} else {
|
||||
DEBUG_DBG(kTag, "CAN 监听已恢复");
|
||||
DEBUG_DBG(kTag, "事件监听已恢复");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 拷贝某通道的最新快照(线程安全)。
|
||||
* @return true 表示拿到一份有效数据。
|
||||
* @brief 查询是否处于暂停(OTA / send_batch 期间为 true)。
|
||||
* 暂停期间 HTTP handler 应跳过 0xC1 查询,避免与 OTA 抢占 SPI。
|
||||
*/
|
||||
bool getLatestCanFeedback(uint8_t channel, can_feedback_result_t *out)
|
||||
bool canFeedbackPaused()
|
||||
{
|
||||
if (channel >= CAN_CHANNEL_MAX || out == nullptr || s_mutex == nullptr) {
|
||||
return false;
|
||||
}
|
||||
bool ok = false;
|
||||
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
if (s_valid[channel]) {
|
||||
*out = s_latest[channel];
|
||||
ok = true;
|
||||
}
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
return ok;
|
||||
return s_paused;
|
||||
}
|
||||
|
||||
@@ -1,35 +1,29 @@
|
||||
/*
|
||||
* can_feedback_task.h - 被动接收 GD32 主动推送的 CAN 反馈快照
|
||||
* can_feedback_task.h - 事件监听任务(0xC4/0xC5/0xC6/0xD2 推送)
|
||||
*
|
||||
* ESP32 侧不主动 1Hz 轮询 0xC1;由 GD32 按铃推送,本任务 takeEvent 接收。
|
||||
* 2026-08 架构改造:0xC1 CAN 反馈已改为请求驱动(canFdQueryFeedback),
|
||||
* 不再被动缓存。本任务仅分发 GD32 主动推送的 ADC/电机状态/版本/OTA 进度事件。
|
||||
*/
|
||||
#ifndef CAN_FEEDBACK_TASK_H
|
||||
#define CAN_FEEDBACK_TASK_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include "transport/canfd_bridge.h"
|
||||
|
||||
/** @brief 暂停/恢复 CAN 反馈监听(OTA 期间调用 pause=true)。 */
|
||||
/** @brief 暂停/恢复事件监听(OTA 期间调用 pause=true)。 */
|
||||
void pauseCanFeedbackTask(bool pause);
|
||||
|
||||
/** @brief 查询监听是否处于暂停(OTA / send_batch 期间为 true)。
|
||||
* 请求驱动的 SPI 查询(canFdQueryFeedback)在暂停期间应跳过。 */
|
||||
bool canFeedbackPaused();
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief 启动 CAN 反馈被动监听任务(幂等)。
|
||||
* @brief 启动事件监听任务(幂等)。
|
||||
* @return true 表示任务已存在或创建成功。
|
||||
*/
|
||||
bool startCanFeedbackTask();
|
||||
|
||||
/**
|
||||
* @brief 拷贝某通道的最新反馈快照(线程安全)。
|
||||
* @param channel 0/1/2。
|
||||
* @param out 输出缓冲。
|
||||
* @return true 表示拿到一份有效数据,false 表示通道无效或尚未拿到第一份。
|
||||
*/
|
||||
bool getLatestCanFeedback(uint8_t channel, can_feedback_result_t *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -11,8 +11,12 @@
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
/** 单次快照最多跟踪的任务数 */
|
||||
static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 32;
|
||||
/** 单次快照最多跟踪的任务数。
|
||||
* 系统常驻任务 ~14(app)+ ~5(IDF: tiT/wifi/nimble/hci/ble)= ~19,
|
||||
* 按需短暂任务(ble_finish/restart/cancel/gd32_wait/ota_feed)不常驻。
|
||||
* 20 覆盖常驻集 + 余量 1;偶发按需任务多出时 uxTaskGetSystemState 截断,
|
||||
* 仅影响该拍 CPU 统计显示,不影响功能。原 32 浪费 ~1KB BSS。 */
|
||||
static constexpr size_t CPU_TASK_SNAPSHOT_MAX = 20;
|
||||
|
||||
/** 采样周期(ms)— 窗口 sample_ms 应对齐此值 */
|
||||
static constexpr uint32_t CPU_STATS_SAMPLE_MS = 1000;
|
||||
|
||||
@@ -509,7 +509,12 @@ esp_err_t DebugLog::uartSend(const uint8_t *data, size_t len)
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
const int ret = uart_write_bytes(UART_NUM_0, data, len);
|
||||
/* 非阻塞直写 FIFO:满了丢弃(日志必进 ring,web /logs 仍完整)。
|
||||
* 旧实现 uart_write_bytes 在 TX buffer=0 时同步阻塞到整行发完
|
||||
* (115200 下 ~5-8ms/条),OTA 期间日志密集会把 ota_relay(prio 23)
|
||||
* / spi_worker(prio 6) 阻塞数 ms~数十 ms——50ms ACK 窗口被拖爆,
|
||||
* 表现为改短重试后超时重发频繁(线上 12/777)。 */
|
||||
const int ret = uart_tx_chars(UART_NUM_0, (const char *)data, (int)len);
|
||||
if (ret < 0) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
@@ -646,7 +651,10 @@ esp_err_t DebugLog::send(const uint8_t *data, size_t len)
|
||||
|
||||
if (sock >= 0) {
|
||||
const size_t send_len = len > 1024 ? 1024 : len;
|
||||
const int ret = ::sendto(sock, data, send_len, 0,
|
||||
/* MSG_DONTWAIT:非阻塞发送。lwIP 在 ARP 未解析/路由未就绪时
|
||||
* sendto 会排队等待(可达 SO_SNDTIMEO 200ms),会阻塞打日志的
|
||||
* 业务任务(OTA relay/worker)——日志转发必须尽力而为。 */
|
||||
const int ret = ::sendto(sock, data, send_len, MSG_DONTWAIT,
|
||||
(struct sockaddr *)&m_debugAddr, sizeof(m_debugAddr));
|
||||
if (ret >= 0) {
|
||||
udp_ret = ESP_OK;
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
* gd32_adc_feedback.cpp - GD32 ADC 调试反馈解析与缓存
|
||||
*/
|
||||
#include "monitor/gd32_adc_feedback.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "monitor/spi_ring_cache.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
@@ -17,29 +15,6 @@ static SemaphoreHandle_t s_mux = nullptr;
|
||||
static gd32_adc_feedback_t s_latest = {};
|
||||
static bool s_have = false;
|
||||
|
||||
/** @brief 读小端 u16。 */
|
||||
static uint16_t rdU16Le(const uint8_t *p)
|
||||
{
|
||||
return static_cast<uint16_t>(p[0] | (static_cast<uint16_t>(p[1]) << 8));
|
||||
}
|
||||
|
||||
/** @brief 读小端 u32。 */
|
||||
static uint32_t rdU32Le(const uint8_t *p)
|
||||
{
|
||||
return static_cast<uint32_t>(p[0])
|
||||
| (static_cast<uint32_t>(p[1]) << 8)
|
||||
| (static_cast<uint32_t>(p[2]) << 16)
|
||||
| (static_cast<uint32_t>(p[3]) << 24);
|
||||
}
|
||||
|
||||
/** @brief 懒创建快照互斥量。 */
|
||||
static void ensureMux()
|
||||
{
|
||||
if (s_mux == nullptr) {
|
||||
s_mux = xSemaphoreCreateMutex();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
@@ -80,7 +55,7 @@ bool onGd32AdcFeedbackEvent(const uint8_t *payload, size_t len)
|
||||
fb.uptime_sec = uptime;
|
||||
fb.recv_ms = esp_timer_get_time() / 1000;
|
||||
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) {
|
||||
return false;
|
||||
}
|
||||
@@ -103,7 +78,7 @@ bool getLatestGd32AdcFeedback(gd32_adc_feedback_t *out)
|
||||
if (out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) {
|
||||
return false;
|
||||
}
|
||||
@@ -124,7 +99,7 @@ bool getLatestGd32AdcFeedback(gd32_adc_feedback_t *out)
|
||||
*/
|
||||
uint32_t gd32AdcFeedbackAgeMs(void)
|
||||
{
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) {
|
||||
return UINT32_MAX;
|
||||
}
|
||||
@@ -135,16 +110,5 @@ uint32_t gd32AdcFeedbackAgeMs(void)
|
||||
recv = s_latest.recv_ms;
|
||||
xSemaphoreGive(s_mux);
|
||||
}
|
||||
if (!have || recv <= 0) {
|
||||
return UINT32_MAX;
|
||||
}
|
||||
const int64_t now = esp_timer_get_time() / 1000;
|
||||
if (now < recv) {
|
||||
return 0U;
|
||||
}
|
||||
const int64_t age = now - recv;
|
||||
if (age > static_cast<int64_t>(UINT32_MAX)) {
|
||||
return UINT32_MAX;
|
||||
}
|
||||
return static_cast<uint32_t>(age);
|
||||
return ringCacheAgeMs(have, recv);
|
||||
}
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "spi_master.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_netif_types.h"
|
||||
#include "server/http_util.h"
|
||||
|
||||
// ============================================================
|
||||
// heap hook:捕获"破新低时的最后一次 alloc"(最后一击,未必是主因)
|
||||
@@ -399,8 +400,13 @@ void HealthMonitor::taskEntry(void *pvParameters)
|
||||
* 必须在 maybeRecord 之前,否则 est 长期偏低会误触发 hook 快照。 */
|
||||
hookResyncEstFree(free_heap);
|
||||
|
||||
// 任务栈指标:本周期只采集一次,堆低水位快照和栈水位巡检共用。
|
||||
TaskStackMetric metrics[STACK_TASK_MAX];
|
||||
size_t count = collectTaskStackMetrics(metrics, STACK_TASK_MAX);
|
||||
|
||||
// 堆低水位快照:仅在"最低空闲"创新低时记录一条上下文(5s 检测足够)
|
||||
self.maybeRecordHeapLowWater();
|
||||
// 传入已采集的 metrics,避免 maybeRecordHeapLowWater 内部二次遍历任务表。
|
||||
self.maybeRecordHeapLowWater(metrics, count);
|
||||
|
||||
if (free_heap < HEAP_WARNING_MIN) {
|
||||
/* 堆告警节流:OTA 期间阈值更低、间隔更长 */
|
||||
@@ -426,10 +432,8 @@ void HealthMonitor::taskEntry(void *pvParameters)
|
||||
DEBUG_ERR("MON", "健康状态危险: %d", cur);
|
||||
}
|
||||
|
||||
// 栈水位主动巡检:复用 /system/status 的任务表,低水位时 DEBUG_ERR 主动告警。
|
||||
// 栈水位主动巡检:复用已采集的任务表,低水位时 DEBUG_ERR 主动告警。
|
||||
// health_mon 任务不被 OTA 暂停,告警经 UDP/UART 直发,穿透 OTA 门控。
|
||||
TaskStackMetric metrics[STACK_TASK_MAX];
|
||||
size_t count = collectTaskStackMetrics(metrics, STACK_TASK_MAX);
|
||||
for (size_t i = 0; i < count && i < STACK_TASK_MAX; ++i) {
|
||||
if (metrics[i].handle == NULL || metrics[i].stack_size_bytes == 0) {
|
||||
continue;
|
||||
@@ -604,23 +608,6 @@ health_status_t HealthMonitor::updateFromWifiRssi(int8_t rssi)
|
||||
// ============================================================
|
||||
// 堆低水位快照("最低空闲"创新低时记录上下文)
|
||||
// ============================================================
|
||||
namespace {
|
||||
/** @brief snprintf 追加到 buf 的安全封装:截断但不越界,返回新 offset。 */
|
||||
int appendJsonfSafe(char *buf, size_t cap, int offset, const char *fmt, ...)
|
||||
{
|
||||
if (offset < 0 || (size_t)offset >= cap) return offset;
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
int n = vsnprintf(buf + offset, cap - (size_t)offset, fmt, ap);
|
||||
va_end(ap);
|
||||
if (n < 0) return offset;
|
||||
if ((size_t)n >= cap - (size_t)offset) {
|
||||
// 被截断:vsnprintf 返回的是"本应写入"的长度,按截断后修正 offset
|
||||
return (int)cap - 1;
|
||||
}
|
||||
return offset + n;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
* @brief 检测"内部堆最低空闲"是否创新低,是则覆盖写入唯一快照槽。
|
||||
@@ -630,7 +617,7 @@ int appendJsonfSafe(char *buf, size_t cap, int offset, const char *fmt, ...)
|
||||
* 所有外部读取(OtaCoordinator::getSnapshot / collectTaskStackMetrics / rssi / temp)
|
||||
* 都先在锁外取,避免持锁期间嵌套调用其它模块的锁;持锁后只做字段拷贝。
|
||||
*/
|
||||
void HealthMonitor::maybeRecordHeapLowWater()
|
||||
void HealthMonitor::maybeRecordHeapLowWater(const TaskStackMetric *pre_metrics, size_t pre_count)
|
||||
{
|
||||
// 与 http_handlers.cpp 同口径:内部 DRAM 数据堆
|
||||
static constexpr uint32_t CAPS = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT;
|
||||
@@ -691,8 +678,17 @@ void HealthMonitor::maybeRecordHeapLowWater()
|
||||
HeapLowTaskStack stacks[8];
|
||||
uint8_t stack_count = 0;
|
||||
{
|
||||
TaskStackMetric metrics[STACK_TASK_MAX];
|
||||
size_t count = collectTaskStackMetrics(metrics, STACK_TASK_MAX);
|
||||
// 优先复用巡检循环已采集的指标,避免二次遍历
|
||||
TaskStackMetric local_metrics[STACK_TASK_MAX];
|
||||
const TaskStackMetric *metrics;
|
||||
size_t count;
|
||||
if (pre_metrics && pre_count > 0) {
|
||||
metrics = pre_metrics;
|
||||
count = pre_count;
|
||||
} else {
|
||||
count = collectTaskStackMetrics(local_metrics, STACK_TASK_MAX);
|
||||
metrics = local_metrics;
|
||||
}
|
||||
// 先全部采集
|
||||
struct Item { char name[16]; uint32_t rem; };
|
||||
Item items[STACK_TASK_MAX];
|
||||
@@ -816,7 +812,7 @@ int HealthMonitor::dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const
|
||||
LockGuard lock(m_mutex);
|
||||
|
||||
if (!m_heapLowWaterValid) {
|
||||
return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
}
|
||||
|
||||
const HeapLowWaterRecord &r = m_heapLowWater;
|
||||
@@ -824,7 +820,7 @@ int HealthMonitor::dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const
|
||||
* hook_confidence 告知前端置信度:hook 命中=est_low_watermark,未命中=none。
|
||||
* 前端应据此区分 estimated 与 measured(free_heap/min_free_heap 为实测)。 */
|
||||
const char *hook_confidence = r.hook_fired ? "est_low_watermark" : "none";
|
||||
int pos = appendJsonfSafe(buf, cap, offset,
|
||||
int pos = http_json_append(buf, cap, offset,
|
||||
"{\"count\":1,\"records\":[{"
|
||||
"\"ts_ms\":%llu,\"free_heap\":%lu,\"min_free_heap\":%lu,"
|
||||
"\"largest_free_block\":%lu,\"sys_state\":%u,\"ota_channel\":%u,"
|
||||
@@ -863,24 +859,24 @@ int HealthMonitor::dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const
|
||||
(unsigned long)r.hook_fired,
|
||||
(unsigned long)r.ip_event_count);
|
||||
if (pos == offset) {
|
||||
return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
}
|
||||
|
||||
for (uint8_t k = 0; k < r.task_stack_count && k < 8; ++k) {
|
||||
int after = appendJsonfSafe(buf, cap, pos,
|
||||
int after = http_json_append(buf, cap, pos,
|
||||
"%s{\"name\":\"%s\",\"remaining\":%lu}",
|
||||
k == 0 ? "" : ",",
|
||||
r.task_stacks[k].name,
|
||||
(unsigned long)r.task_stacks[k].remaining);
|
||||
if (after == pos) {
|
||||
return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
}
|
||||
pos = after;
|
||||
}
|
||||
|
||||
int closed = appendJsonfSafe(buf, cap, pos, "]}]}");
|
||||
int closed = http_json_append(buf, cap, pos, "]}]}");
|
||||
if (closed == pos) {
|
||||
return appendJsonfSafe(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
return http_json_append(buf, cap, offset, "{\"count\":0,\"records\":[]}");
|
||||
}
|
||||
return closed;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "driver/temperature_sensor.h"
|
||||
#include "esp_timer.h"
|
||||
#include "app_config.h"
|
||||
#include "task_stack.h" // TaskStackMetric(maybeRecordHeapLowWater 可选参数)
|
||||
|
||||
// T9.3: 健康状态等级
|
||||
typedef enum {
|
||||
@@ -133,7 +134,11 @@ public:
|
||||
/** @brief 检测并可能覆盖写入唯一快照槽(线程安全)。
|
||||
* 由 health_mon 5s 巡检调用,仅当 internal_min_free 出现新低时写入。
|
||||
* 设备只保留最新 1 条;多条历史由网页 IndexedDB 累积。 */
|
||||
void maybeRecordHeapLowWater();
|
||||
/** @brief 检测并可能覆盖写入唯一快照槽(线程安全)。
|
||||
* 由 health_mon 5s 巡检调用,仅当 internal_min_free 出现新低时写入。
|
||||
* 设备只保留最新 1 条;多条历史由网页 IndexedDB 累积。
|
||||
* @param pre_metrics 巡检循环已采集的任务栈指标(避免二次遍历);null 时内部自采 */
|
||||
void maybeRecordHeapLowWater(const TaskStackMetric *pre_metrics = nullptr, size_t pre_count = 0);
|
||||
|
||||
/** @brief 将唯一快照序列化为 JSON:{"count":0|1,"records":[...]}。线程安全。 */
|
||||
int dumpHeapLowWaterJson(char *buf, size_t cap, int offset) const;
|
||||
|
||||
@@ -35,6 +35,59 @@ static int64_t nowMs() {
|
||||
return esp_timer_get_time() / 1000;
|
||||
}
|
||||
|
||||
/* ── 共享辅助:终态判断 + QUERY SPI 事务 ── */
|
||||
|
||||
/** @brief GD32 侧已进终态(IDLE/FINISHED/ERROR),喂数据应停止。 */
|
||||
static inline bool isTerminalState(motor_ota_state_t s)
|
||||
{
|
||||
return s == MOTOR_OTA_IDLE || s == MOTOR_OTA_FINISHED || s == MOTOR_OTA_ERROR;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 发 SPI 0xD0 QUERY 查询 GD32 真实状态。
|
||||
* @param out_state 非 null 时写入 GD32 返回的 state
|
||||
* @param out_err 非 null 时写入 error_code
|
||||
* @param update_status true 时把结果同步写回 s_status(持锁)
|
||||
* @return true=QUERY 成功且 ACK 有有效数据;false=SPI 失败或无 ACK
|
||||
*
|
||||
* 消除 feedTask 中 4 处重复的 qcmd[1]={2} + transact + parse qp[0..5] 块。
|
||||
*/
|
||||
static bool queryMotorOtaStatus(motor_ota_state_t *out_state = nullptr,
|
||||
uint8_t *out_err = nullptr,
|
||||
bool update_status = false)
|
||||
{
|
||||
uint8_t qcmd[1] = {2U}; /* cmd=QUERY */
|
||||
uint8_t qt = 0, qp[16] = {0};
|
||||
size_t ql = 0;
|
||||
if (SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, qcmd, 1, &qt, qp, sizeof(qp), &ql, 300U) != ESP_OK
|
||||
|| qt != kRspAck || ql < 1U) {
|
||||
return false;
|
||||
}
|
||||
const motor_ota_state_t qst = static_cast<motor_ota_state_t>(qp[0]);
|
||||
if (out_state) *out_state = qst;
|
||||
if (out_err) *out_err = (ql >= 2U) ? qp[1] : 0U;
|
||||
if (update_status && s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
s_status.state = qst;
|
||||
s_status.error_code = (ql >= 2U) ? qp[1] : 0U;
|
||||
if (ql >= 6U) {
|
||||
s_status.packets_acked = static_cast<uint16_t>(qp[2])
|
||||
| (static_cast<uint16_t>(qp[3]) << 8);
|
||||
s_status.total_packets = static_cast<uint16_t>(qp[4])
|
||||
| (static_cast<uint16_t>(qp[5]) << 8);
|
||||
}
|
||||
if (ql >= 10U) {
|
||||
s_status.fw_size = static_cast<uint32_t>(qp[6])
|
||||
| (static_cast<uint32_t>(qp[7]) << 8)
|
||||
| (static_cast<uint32_t>(qp[8]) << 16)
|
||||
| (static_cast<uint32_t>(qp[9]) << 24);
|
||||
}
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ═══════════════════ 喂数据 task ═══════════════════ */
|
||||
|
||||
static void feedTask(void *arg)
|
||||
@@ -87,8 +140,7 @@ static void feedTask(void *arg)
|
||||
* (0xD2 ring 延迟到达) 覆写 s_status 导致 feedTask 误判退出发 CANCEL。 */
|
||||
const motor_ota_status_t st = motorOtaGetStatus();
|
||||
if (nowMs() >= grace_end_ms) {
|
||||
if (st.state == MOTOR_OTA_IDLE || st.state == MOTOR_OTA_FINISHED ||
|
||||
st.state == MOTOR_OTA_ERROR) {
|
||||
if (isTerminalState(st.state)) {
|
||||
ESP_LOGW(TAG, "GD32 终态 0x%02X,停止喂数据 offset=%u/%u",
|
||||
(int)st.state, (unsigned)s_fw_offset, (unsigned)s_fw_size);
|
||||
break;
|
||||
@@ -129,34 +181,15 @@ const uint32_t remaining = s_fw_size - s_fw_offset;
|
||||
* 避免 GD32 已进 ERROR 终态但 feedTask 不知道、盲重试空转到 120s 超时。 */
|
||||
if (++consec_fail >= kQueryAfterFails) {
|
||||
consec_fail = 0U;
|
||||
uint8_t qcmd[1] = {2U}; /* cmd=QUERY */
|
||||
uint8_t qt = 0, qp[16] = {0};
|
||||
size_t ql = 0;
|
||||
if (SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, qcmd, 1, &qt, qp, sizeof(qp), &ql, 300U) == ESP_OK
|
||||
&& qt == kRspAck && ql >= 1U) {
|
||||
motor_ota_state_t qst = static_cast<motor_ota_state_t>(qp[0]);
|
||||
if (qst == MOTOR_OTA_IDLE || qst == MOTOR_OTA_FINISHED ||
|
||||
qst == MOTOR_OTA_ERROR) {
|
||||
motor_ota_state_t qst;
|
||||
uint8_t qerr;
|
||||
if (queryMotorOtaStatus(&qst, &qerr, true) && isTerminalState(qst)) {
|
||||
ESP_LOGW(TAG, "QUERY 检测到 GD32 终态 0x%02X err=0x%02X,停止喂数据 offset=%u/%u",
|
||||
(int)qst, (unsigned)qp[1],
|
||||
(int)qst, (unsigned)qerr,
|
||||
(unsigned)s_fw_offset, (unsigned)s_fw_size);
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
s_status.state = qst;
|
||||
s_status.error_code = qp[1];
|
||||
if (ql >= 6U) {
|
||||
s_status.packets_acked = static_cast<uint16_t>(qp[2])
|
||||
| (static_cast<uint16_t>(qp[3]) << 8);
|
||||
s_status.total_packets = static_cast<uint16_t>(qp[4])
|
||||
| (static_cast<uint16_t>(qp[5]) << 8);
|
||||
}
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue; /* 不推进 offset,同 offset 重发 */
|
||||
}
|
||||
consec_fail = 0U;
|
||||
@@ -177,13 +210,8 @@ const uint32_t remaining = s_fw_size - s_fw_offset;
|
||||
|
||||
/* 握手未确认时定期 QUERY:确认 GD32 已过握手阶段才标 TRANSFER */
|
||||
if (!handshake_confirmed && (ack_n & 7U) == 0U) {
|
||||
uint8_t qcmd[1] = {2U};
|
||||
uint8_t qt = 0, qp[16] = {0};
|
||||
size_t ql = 0;
|
||||
if (SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, qcmd, 1, &qt, qp, sizeof(qp), &ql, 300U) == ESP_OK
|
||||
&& qt == kRspAck && ql >= 1U) {
|
||||
motor_ota_state_t qst = static_cast<motor_ota_state_t>(qp[0]);
|
||||
motor_ota_state_t qst;
|
||||
if (queryMotorOtaStatus(&qst, nullptr, false)) {
|
||||
if (qst == MOTOR_OTA_ERASE || qst == MOTOR_OTA_TRANSFER ||
|
||||
qst == MOTOR_OTA_FINISH_OTA || qst == MOTOR_OTA_WAIT_APP_RUN) {
|
||||
handshake_confirmed = true;
|
||||
@@ -218,14 +246,9 @@ const uint32_t remaining = s_fw_size - s_fw_offset;
|
||||
* 当前 offset 被 gap 拒收(永远不会变 ACK)。QUERY 确认真实状态。 */
|
||||
if (++consec_wait >= kQueryAfterWaits) {
|
||||
consec_wait = 0U;
|
||||
uint8_t qcmd[1] = {2U}; /* cmd=QUERY */
|
||||
uint8_t qt = 0, qp[16] = {0};
|
||||
size_t ql = 0;
|
||||
if (SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, qcmd, 1, &qt, qp, sizeof(qp), &ql, 300U) == ESP_OK
|
||||
&& qt == kRspAck && ql >= 2U) {
|
||||
const motor_ota_state_t qst = static_cast<motor_ota_state_t>(qp[0]);
|
||||
const uint8_t qerr = qp[1];
|
||||
motor_ota_state_t qst;
|
||||
uint8_t qerr;
|
||||
if (queryMotorOtaStatus(&qst, &qerr, false)) {
|
||||
if (qst != MOTOR_OTA_TRANSFER) {
|
||||
/* GD32 不在传输态(auto-retry 回了 kPreCancel/握手):
|
||||
* 重置 offset 从头喂,配合 GD32 重新擦除+握手。 */
|
||||
@@ -238,8 +261,7 @@ const uint32_t remaining = s_fw_size - s_fw_offset;
|
||||
handshake_confirmed = true; /* GD32 在 transfer,确认握手完成 */
|
||||
}
|
||||
/* 终态仍 break */
|
||||
if (qst == MOTOR_OTA_IDLE || qst == MOTOR_OTA_FINISHED ||
|
||||
qst == MOTOR_OTA_ERROR) {
|
||||
if (isTerminalState(qst)) {
|
||||
ESP_LOGW(TAG, "QUERY(WAIT) 终态 0x%02X err=0x%02X,停止",
|
||||
(unsigned)qst, (unsigned)qerr);
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(50)) == pdTRUE) {
|
||||
@@ -276,8 +298,7 @@ const uint32_t remaining = s_fw_size - s_fw_offset;
|
||||
/* 优先吃 ring 推送(已恢复 suppress) */
|
||||
{
|
||||
const motor_ota_status_t st = motorOtaGetStatus();
|
||||
if (st.state == MOTOR_OTA_FINISHED || st.state == MOTOR_OTA_ERROR ||
|
||||
st.state == MOTOR_OTA_IDLE) {
|
||||
if (isTerminalState(st.state)) {
|
||||
ESP_LOGI(TAG, "feedTask 退出,ring 终态: state=%u err=%u pkt=%u/%u",
|
||||
(unsigned)st.state, (unsigned)st.error_code,
|
||||
(unsigned)st.packets_acked, (unsigned)st.total_packets);
|
||||
@@ -285,37 +306,14 @@ const uint32_t remaining = s_fw_size - s_fw_offset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
uint8_t qcmd[1] = {2U}; /* cmd=QUERY */
|
||||
uint8_t qresp_type = 0;
|
||||
uint8_t qresp[16] = {0};
|
||||
size_t qresp_len = 0;
|
||||
esp_err_t qr = SpiMaster::instance().transact(
|
||||
kSpiMotorOtaCmd, qcmd, 1,
|
||||
&qresp_type, qresp, sizeof(qresp), &qresp_len, 300U);
|
||||
if (qr == ESP_OK && qresp_type == kRspAck && qresp_len >= 10U) {
|
||||
const motor_ota_state_t qst = static_cast<motor_ota_state_t>(qresp[0]);
|
||||
if (s_mutex && xSemaphoreTake(s_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
s_status.state = qst;
|
||||
s_status.error_code = qresp[1];
|
||||
s_status.packets_acked = static_cast<uint16_t>(qresp[2])
|
||||
| (static_cast<uint16_t>(qresp[3]) << 8);
|
||||
s_status.total_packets = static_cast<uint16_t>(qresp[4])
|
||||
| (static_cast<uint16_t>(qresp[5]) << 8);
|
||||
s_status.fw_size = static_cast<uint32_t>(qresp[6])
|
||||
| (static_cast<uint32_t>(qresp[7]) << 8)
|
||||
| (static_cast<uint32_t>(qresp[8]) << 16)
|
||||
| (static_cast<uint32_t>(qresp[9]) << 24);
|
||||
s_status.last_update_ms = nowMs();
|
||||
xSemaphoreGive(s_mutex);
|
||||
}
|
||||
if (qst == MOTOR_OTA_FINISHED || qst == MOTOR_OTA_ERROR ||
|
||||
qst == MOTOR_OTA_IDLE) {
|
||||
motor_ota_state_t qst;
|
||||
uint8_t qerr;
|
||||
if (queryMotorOtaStatus(&qst, &qerr, true)) {
|
||||
if (isTerminalState(qst)) {
|
||||
const motor_ota_status_t snap = motorOtaGetStatus();
|
||||
ESP_LOGI(TAG, "feedTask 退出,QUERY 终态: state=%u err=%u pkt=%u/%u",
|
||||
(unsigned)qst, (unsigned)qresp[1],
|
||||
(unsigned)(static_cast<uint16_t>(qresp[2])
|
||||
| (static_cast<uint16_t>(qresp[3]) << 8)),
|
||||
(unsigned)(static_cast<uint16_t>(qresp[4])
|
||||
| (static_cast<uint16_t>(qresp[5]) << 8)));
|
||||
(unsigned)qst, (unsigned)qerr,
|
||||
(unsigned)snap.packets_acked, (unsigned)snap.total_packets);
|
||||
got_terminal = true;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
* motor_state.cpp - GD32 多电机状态推送解析与缓存 (SPI TYPE 0xC5)
|
||||
*/
|
||||
#include "monitor/motor_state.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "monitor/spi_ring_cache.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
@@ -13,21 +11,6 @@ namespace {
|
||||
static SemaphoreHandle_t s_mux = nullptr;
|
||||
static motor_state_table_t s_table = {};
|
||||
|
||||
static uint32_t rdU32Le(const uint8_t *p)
|
||||
{
|
||||
return static_cast<uint32_t>(p[0])
|
||||
| (static_cast<uint32_t>(p[1]) << 8)
|
||||
| (static_cast<uint32_t>(p[2]) << 16)
|
||||
| (static_cast<uint32_t>(p[3]) << 24);
|
||||
}
|
||||
|
||||
static void ensureMux()
|
||||
{
|
||||
if (s_mux == nullptr) {
|
||||
s_mux = xSemaphoreCreateMutex();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool onMotorStateEvent(const uint8_t *payload, size_t len)
|
||||
@@ -51,7 +34,7 @@ bool onMotorStateEvent(const uint8_t *payload, size_t len)
|
||||
return false;
|
||||
}
|
||||
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) return false;
|
||||
if (xSemaphoreTake(s_mux, pdMS_TO_TICKS(20)) != pdTRUE) return false;
|
||||
|
||||
@@ -102,7 +85,7 @@ bool onMotorStateEvent(const uint8_t *payload, size_t len)
|
||||
bool getLatestMotorState(motor_state_table_t *out)
|
||||
{
|
||||
if (out == nullptr) return false;
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) return false;
|
||||
|
||||
bool ok = false;
|
||||
@@ -118,7 +101,7 @@ bool getLatestMotorState(motor_state_table_t *out)
|
||||
|
||||
uint32_t motorStateAgeMs(void)
|
||||
{
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) return UINT32_MAX;
|
||||
|
||||
int64_t recv = 0;
|
||||
@@ -128,11 +111,5 @@ uint32_t motorStateAgeMs(void)
|
||||
recv = s_table.recv_ms;
|
||||
xSemaphoreGive(s_mux);
|
||||
}
|
||||
if (!have || recv <= 0) return UINT32_MAX;
|
||||
|
||||
const int64_t now = esp_timer_get_time() / 1000;
|
||||
if (now < recv) return 0U;
|
||||
const int64_t age = now - recv;
|
||||
return (age > static_cast<int64_t>(UINT32_MAX)) ? UINT32_MAX
|
||||
: static_cast<uint32_t>(age);
|
||||
return ringCacheAgeMs(have, recv);
|
||||
}
|
||||
@@ -4,9 +4,7 @@
|
||||
* 镜像 motor_state.cpp 的互斥保护缓存模型。
|
||||
*/
|
||||
#include "monitor/motor_version.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "monitor/spi_ring_cache.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
@@ -15,21 +13,6 @@ namespace {
|
||||
static SemaphoreHandle_t s_mux = nullptr;
|
||||
static motor_version_table_t s_table = {};
|
||||
|
||||
static uint32_t rdU32Le(const uint8_t *p)
|
||||
{
|
||||
return static_cast<uint32_t>(p[0])
|
||||
| (static_cast<uint32_t>(p[1]) << 8)
|
||||
| (static_cast<uint32_t>(p[2]) << 16)
|
||||
| (static_cast<uint32_t>(p[3]) << 24);
|
||||
}
|
||||
|
||||
static void ensureMux()
|
||||
{
|
||||
if (s_mux == nullptr) {
|
||||
s_mux = xSemaphoreCreateMutex();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool onMotorVersionEvent(const uint8_t *payload, size_t len)
|
||||
@@ -40,7 +23,7 @@ bool onMotorVersionEvent(const uint8_t *payload, size_t len)
|
||||
|
||||
const uint8_t count = payload[0];
|
||||
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) return false;
|
||||
if (xSemaphoreTake(s_mux, pdMS_TO_TICKS(20)) != pdTRUE) return false;
|
||||
|
||||
@@ -69,7 +52,7 @@ bool onMotorVersionEvent(const uint8_t *payload, size_t len)
|
||||
bool getLatestMotorVersion(motor_version_table_t *out)
|
||||
{
|
||||
if (out == nullptr) return false;
|
||||
ensureMux();
|
||||
ringCacheEnsureMux(&s_mux);
|
||||
if (s_mux == nullptr) return false;
|
||||
|
||||
bool ok = false;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* spi_ring_cache.h - SPI ring push 缓存共享辅助
|
||||
*
|
||||
* gd32_adc_feedback / motor_state / motor_version 三个模块共用相同的
|
||||
* 互斥懒创建 + 小端读 + ageMs 计算样板,提取到此处消除三份拷贝。
|
||||
*/
|
||||
#ifndef SPI_RING_CACHE_H
|
||||
#define SPI_RING_CACHE_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ── 小端读 ── */
|
||||
|
||||
static inline uint16_t rdU16Le(const uint8_t *p)
|
||||
{
|
||||
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
||||
}
|
||||
|
||||
static inline uint32_t rdU32Le(const uint8_t *p)
|
||||
{
|
||||
return (uint32_t)p[0]
|
||||
| ((uint32_t)p[1] << 8)
|
||||
| ((uint32_t)p[2] << 16)
|
||||
| ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
/* ── 互斥懒创建 ── */
|
||||
|
||||
static inline void ringCacheEnsureMux(SemaphoreHandle_t *mux)
|
||||
{
|
||||
if (*mux == nullptr) {
|
||||
*mux = xSemaphoreCreateMutex();
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 通用 ageMs ──
|
||||
* 传入缓存的 have 标志 + recv_ms 时间戳,返回距 now 的毫秒数。
|
||||
* 无数据或 recv_ms<=0 返回 UINT32_MAX。
|
||||
*/
|
||||
static inline uint32_t ringCacheAgeMs(bool have, int64_t recv_ms)
|
||||
{
|
||||
if (!have || recv_ms <= 0) return UINT32_MAX;
|
||||
const int64_t now = esp_timer_get_time() / 1000;
|
||||
if (now < recv_ms) return 0U;
|
||||
const int64_t age = now - recv_ms;
|
||||
return (age > (int64_t)UINT32_MAX) ? UINT32_MAX : (uint32_t)age;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // SPI_RING_CACHE_H
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "esp_timer.h"
|
||||
#include "spi_master.h"
|
||||
#include "http_handlers.h" /* http_set_cors_headers */
|
||||
#include "server/http_util.h"
|
||||
#include "monitor/debug_log.h"
|
||||
|
||||
namespace {
|
||||
@@ -37,30 +38,14 @@ constexpr size_t kPayloadDefault = 236U; /* 满帧:等效 0xD1 电机 OTA
|
||||
constexpr size_t kPayloadMax = 236U; /* APP kTxBufSize=256 - V2_OVERHEAD */
|
||||
constexpr uint32_t kFrameTimeoutMs = 500U;
|
||||
|
||||
/** @brief 从 query 里取无符号整型参数,缺失/非法时返回默认值。 */
|
||||
uint32_t queryU32(httpd_req_t *req, const char *key, uint32_t defval)
|
||||
{
|
||||
char qbuf[64];
|
||||
char vbuf[16];
|
||||
if (httpd_req_get_url_query_str(req, qbuf, sizeof(qbuf)) != ESP_OK) {
|
||||
return defval;
|
||||
}
|
||||
if (httpd_query_key_value(qbuf, key, vbuf, sizeof(vbuf)) != ESP_OK) {
|
||||
return defval;
|
||||
}
|
||||
char *end = nullptr;
|
||||
const unsigned long v = strtoul(vbuf, &end, 10);
|
||||
return (end && *end == '\0') ? (uint32_t)v : defval;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
esp_err_t spi_speedtest_handler(httpd_req_t *req)
|
||||
{
|
||||
uint32_t frames = queryU32(req, "frames", kFramesDefault);
|
||||
uint32_t frames = http_query_u32(req, "frames", kFramesDefault);
|
||||
if (frames < 1U) { frames = 1U; }
|
||||
if (frames > kFramesMax) { frames = kFramesMax; }
|
||||
size_t payload_len = (size_t)queryU32(req, "payload", (uint32_t)kPayloadDefault);
|
||||
size_t payload_len = (size_t)http_query_u32(req, "payload", (uint32_t)kPayloadDefault);
|
||||
if (payload_len > kPayloadMax) { payload_len = kPayloadMax; }
|
||||
|
||||
/* 探针 payload:递增填充(CRC32 覆盖,顺带校验数据路径完整性) */
|
||||
@@ -74,9 +59,18 @@ esp_err_t spi_speedtest_handler(httpd_req_t *req)
|
||||
uint64_t rtt_sum_us = 0U;
|
||||
uint64_t rtt_min_us = UINT64_MAX;
|
||||
uint64_t rtt_max_us = 0U;
|
||||
/* RTT 分布直方图:<2 / 2-3 / 3-4 / 4-5 / >=5 ms,用于判断抖动形态
|
||||
* (双峰=离散 +1ms 碰撞;均匀=tick 相位量化) */
|
||||
uint32_t histo[5] = {0U, 0U, 0U, 0U, 0U};
|
||||
/* RTT 分布直方图(µs 计时,<2ms 区间细化为 0.5ms 粒度):
|
||||
* <0.5 / 0.5-1 / 1-1.5 / 1.5-2 / 2-3 / 3-5 / >=5 ms,
|
||||
* 用于判断抖动形态(<2ms 内的尖峰/双峰 vs 离散 +1ms 碰撞) */
|
||||
uint32_t histo[7] = {0U, 0U, 0U, 0U, 0U, 0U, 0U};
|
||||
/* 事务耗时(worker 内执行段,SpiMaster::lastWorkerExecUs)与
|
||||
* 调度/唤醒开销(RTT − 事务)拆解:事务慢=GD32 应答/SPI 时序;
|
||||
* 调度慢=ESP32 任务抢占(WiFi/esp_timer 等)。 */
|
||||
uint64_t w_sum_us = 0U, w_min_us = UINT64_MAX, w_max_us = 0U;
|
||||
uint64_t s_sum_us = 0U, s_min_us = UINT64_MAX, s_max_us = 0U;
|
||||
/* 5 段细分(SpiMaster::SpiProfile):提交/调度、发送帧、等应答、读响应、唤醒 */
|
||||
uint64_t seg_sum[5] = {0U, 0U, 0U, 0U, 0U};
|
||||
uint64_t seg_max[5] = {0U, 0U, 0U, 0U, 0U};
|
||||
|
||||
DEBUG_INFO("SPI", "[SPEEDTEST] start frames=%u payload=%u",
|
||||
(unsigned)frames, (unsigned)payload_len);
|
||||
@@ -100,12 +94,34 @@ esp_err_t spi_speedtest_handler(httpd_req_t *req)
|
||||
rtt_sum_us += rtt;
|
||||
if (rtt < rtt_min_us) { rtt_min_us = rtt; }
|
||||
if (rtt > rtt_max_us) { rtt_max_us = rtt; }
|
||||
const uint32_t ms_bucket = (uint32_t)(rtt / 1000U);
|
||||
if (ms_bucket < 2U) { ++histo[0]; }
|
||||
else if (ms_bucket < 3U) { ++histo[1]; }
|
||||
else if (ms_bucket < 4U) { ++histo[2]; }
|
||||
else if (ms_bucket < 5U) { ++histo[3]; }
|
||||
else { ++histo[4]; }
|
||||
const uint64_t wus = SpiMaster::instance().lastWorkerExecUs();
|
||||
const uint64_t sus = (rtt > wus) ? (rtt - wus) : 0U;
|
||||
w_sum_us += wus;
|
||||
if (wus < w_min_us) { w_min_us = wus; }
|
||||
if (wus > w_max_us) { w_max_us = wus; }
|
||||
s_sum_us += sus;
|
||||
if (sus < s_min_us) { s_min_us = sus; }
|
||||
if (sus > s_max_us) { s_max_us = sus; }
|
||||
/* 5 段细分(防回绕钳 0) */
|
||||
const SpiMaster::SpiProfile &pf = SpiMaster::instance().lastProfile();
|
||||
const uint64_t seg[5] = {
|
||||
(pf.t_w0 > pf.t_submit0) ? (pf.t_w0 - pf.t_submit0) : 0U,
|
||||
(pf.t_send1 > pf.t_send0) ? (pf.t_send1 - pf.t_send0) : 0U,
|
||||
(pf.t_wait1 > pf.t_wait0) ? (pf.t_wait1 - pf.t_wait0) : 0U,
|
||||
(pf.t_read1 > pf.t_wait1) ? (pf.t_read1 - pf.t_wait1) : 0U,
|
||||
(pf.t_submit1 > pf.t_w1) ? (pf.t_submit1 - pf.t_w1) : 0U,
|
||||
};
|
||||
for (int k = 0; k < 5; ++k) {
|
||||
seg_sum[k] += seg[k];
|
||||
if (seg[k] > seg_max[k]) { seg_max[k] = seg[k]; }
|
||||
}
|
||||
if (rtt < 500U) { ++histo[0]; }
|
||||
else if (rtt < 1000U) { ++histo[1]; }
|
||||
else if (rtt < 1500U) { ++histo[2]; }
|
||||
else if (rtt < 2000U) { ++histo[3]; }
|
||||
else if (rtt < 3000U) { ++histo[4]; }
|
||||
else if (rtt < 5000U) { ++histo[5]; }
|
||||
else { ++histo[6]; }
|
||||
} else {
|
||||
++n_fail;
|
||||
}
|
||||
@@ -120,25 +136,44 @@ esp_err_t spi_speedtest_handler(httpd_req_t *req)
|
||||
tput_kbps = ((double)n_ok * (double)payload_len) / ((double)elapsed_us / 1000000.0) / 1024.0;
|
||||
}
|
||||
if (rtt_min_us == UINT64_MAX) { rtt_min_us = 0U; }
|
||||
if (w_min_us == UINT64_MAX) { w_min_us = 0U; }
|
||||
if (s_min_us == UINT64_MAX) { s_min_us = 0U; }
|
||||
|
||||
DEBUG_INFO("SPI", "[SPEEDTEST] done ok=%u fail=%u avg=%uus tput=%.1fKB/s",
|
||||
(unsigned)n_ok, (unsigned)n_fail, (unsigned)rtt_avg_us, tput_kbps);
|
||||
|
||||
char text[480];
|
||||
char text[640];
|
||||
const int len = snprintf(text, sizeof(text),
|
||||
"SPI 测速结果\n"
|
||||
"帧数: %u 成功: %u 失败: %u%s\n"
|
||||
"载荷: %u B/帧 总耗时: %u ms\n"
|
||||
"往返延迟: 平均 %.1f ms 最快 %.1f ms 最慢 %.1f ms\n"
|
||||
"延迟分布: <2ms:%u 2-3ms:%u 3-4ms:%u 4-5ms:%u >=5ms:%u\n"
|
||||
"往返延迟: 平均 %llu µs 最快 %llu µs 最慢 %llu µs\n"
|
||||
"SPI 事务: 平均 %llu µs 最快 %llu µs 最慢 %llu µs\n"
|
||||
"调度开销: 平均 %llu µs 最快 %llu µs 最慢 %llu µs\n"
|
||||
"单帧分段 平均: 提交/调度 %llu | 发送帧 %llu | 等应答 %llu | 读响应 %llu | 唤醒 %llu µs\n"
|
||||
"单帧分段 最慢: 提交/调度 %llu | 发送帧 %llu | 等应答 %llu | 读响应 %llu | 唤醒 %llu µs\n"
|
||||
"延迟分布: <0.5ms:%u 0.5-1ms:%u 1-1.5ms:%u 1.5-2ms:%u 2-3ms:%u 3-5ms:%u >=5ms:%u\n"
|
||||
"等效吞吐: %.1f KB/s\n",
|
||||
(unsigned)frames, (unsigned)n_ok, (unsigned)n_fail,
|
||||
(n_fail > 0U) ? " <-- 存在失败帧, 链路异常!" : "",
|
||||
(unsigned)payload_len, (unsigned)elapsed_ms,
|
||||
(double)rtt_avg_us / 1000.0, (double)rtt_min_us / 1000.0,
|
||||
(double)rtt_max_us / 1000.0,
|
||||
(unsigned long long)rtt_avg_us, (unsigned long long)rtt_min_us,
|
||||
(unsigned long long)rtt_max_us,
|
||||
(unsigned long long)(w_sum_us / (n_ok > 0U ? n_ok : 1U)),
|
||||
(unsigned long long)w_min_us, (unsigned long long)w_max_us,
|
||||
(unsigned long long)(s_sum_us / (n_ok > 0U ? n_ok : 1U)),
|
||||
(unsigned long long)s_min_us, (unsigned long long)s_max_us,
|
||||
(unsigned long long)(seg_sum[0] / (n_ok > 0U ? n_ok : 1U)),
|
||||
(unsigned long long)(seg_sum[1] / (n_ok > 0U ? n_ok : 1U)),
|
||||
(unsigned long long)(seg_sum[2] / (n_ok > 0U ? n_ok : 1U)),
|
||||
(unsigned long long)(seg_sum[3] / (n_ok > 0U ? n_ok : 1U)),
|
||||
(unsigned long long)(seg_sum[4] / (n_ok > 0U ? n_ok : 1U)),
|
||||
(unsigned long long)seg_max[0], (unsigned long long)seg_max[1],
|
||||
(unsigned long long)seg_max[2], (unsigned long long)seg_max[3],
|
||||
(unsigned long long)seg_max[4],
|
||||
(unsigned)histo[0], (unsigned)histo[1], (unsigned)histo[2],
|
||||
(unsigned)histo[3], (unsigned)histo[4], tput_kbps);
|
||||
(unsigned)histo[3], (unsigned)histo[4],
|
||||
(unsigned)histo[5], (unsigned)histo[6], tput_kbps);
|
||||
|
||||
http_set_cors_headers(req);
|
||||
httpd_resp_set_type(req, "text/plain; charset=utf-8");
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "esp32_ota_core.h"
|
||||
#include "app_config.h"
|
||||
#include "monitor/debug_log.h"
|
||||
#include "spi_master.h" /* setSpiPaused:OTA 期间暂停 SPI,避免 flash 写与 SPI 读并发 */
|
||||
|
||||
#define TAG "OTA_CORE"
|
||||
|
||||
@@ -110,6 +111,7 @@ esp_err_t Esp32OtaCore::finalizeCleanupAndReturn(esp_err_t code)
|
||||
}
|
||||
m_ended = false;
|
||||
releaseMutex();
|
||||
SpiMaster::instance().setSpiPaused(false); /* OTA 结束(失败也恢复 SPI) */
|
||||
resetState();
|
||||
return code;
|
||||
}
|
||||
@@ -201,6 +203,12 @@ esp_err_t Esp32OtaCore::begin(size_t total_size)
|
||||
m_ended = false;
|
||||
memset(m_sha256, 0, sizeof(m_sha256));
|
||||
|
||||
/* ESP32 自我 OTA 期间暂停 SPI(GD32 按铃读 + 业务 transact 全部停摆):
|
||||
* esp_ota_write 写 flash 时 cache 反复失效,SPI worker 的 flash 驻留代码
|
||||
* 访问会被拖慢甚至触发 Cache error,且此时 GD32 数据无意义。
|
||||
* finalize()/abort() 恢复。 */
|
||||
SpiMaster::instance().setSpiPaused(true);
|
||||
|
||||
DEBUG_DBG(TAG, "ESP32 OTA begin part=%s size=%u (erase-on-write)",
|
||||
m_partition->label, (unsigned)total_size);
|
||||
return ESP_OK;
|
||||
@@ -382,11 +390,13 @@ esp_err_t Esp32OtaCore::finalize(const char *frontend_sha_hex, bool reboot,
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR(TAG, "esp_ota_set_boot_partition 失败: %s", esp_err_to_name(err));
|
||||
releaseMutex();
|
||||
SpiMaster::instance().setSpiPaused(false); /* OTA 失败也恢复 SPI */
|
||||
resetState();
|
||||
return err;
|
||||
}
|
||||
|
||||
releaseMutex();
|
||||
SpiMaster::instance().setSpiPaused(false); /* OTA 收尾完成,恢复 SPI 业务 */
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 收尾完成,共 %u 字节", (unsigned)m_received);
|
||||
|
||||
// 保持 m_active 直到重启或下次 begin;resetState 由 begin/abort 处理
|
||||
@@ -413,6 +423,7 @@ void Esp32OtaCore::abort()
|
||||
esp_ota_abort(m_handle);
|
||||
}
|
||||
releaseMutex();
|
||||
SpiMaster::instance().setSpiPaused(false); /* OTA 中止,恢复 SPI 业务 */
|
||||
resetState();
|
||||
DEBUG_INFO(TAG, "ESP32 OTA 已中止并清理");
|
||||
}
|
||||
|
||||
@@ -100,6 +100,24 @@ void OtaCoordinator::watchdogCheckLocked()
|
||||
doAbortLocked();
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::beginLockedWithChecks(const char *tag, bool &out_lock)
|
||||
{
|
||||
out_lock = lock();
|
||||
if (!out_lock) return ESP_ERR_INVALID_STATE;
|
||||
|
||||
// ★ 卡死自愈检查
|
||||
watchdogCheckLocked();
|
||||
|
||||
// ★ 电机 OTA 互斥:电机喂数期间禁止新的 GD32/ESP32 OTA
|
||||
if (motorOtaIsActive()) {
|
||||
DEBUG_WARN(TAG, "电机 OTA 进行中,拒绝 %s", tag);
|
||||
unlock(true);
|
||||
out_lock = false;
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 初始化
|
||||
// ============================================================
|
||||
@@ -138,24 +156,15 @@ void OtaCoordinator::init()
|
||||
// ============================================================
|
||||
esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size, ota_channel_t channel)
|
||||
{
|
||||
bool h = lock();
|
||||
|
||||
// ★ 先做卡死自愈检查:若之前忙状态卡死,自动转 ERROR 继续后续流程
|
||||
if (h) watchdogCheckLocked();
|
||||
|
||||
// ★ 电机 OTA 互斥:电机喂数任务与 GD32 固件共用 gd32_ota 分区,
|
||||
// 电机喂数期间禁止新的 GD32 存储,避免分区被并发覆写。
|
||||
if (h && motorOtaIsActive()) {
|
||||
DEBUG_WARN(TAG, "电机 OTA 进行中,拒绝 GD32 存储");
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
bool h;
|
||||
esp_err_t pre = beginLockedWithChecks("GD32 存储", h);
|
||||
if (pre != ESP_OK) return pre;
|
||||
|
||||
// ★ 同类型重启:若上一次 GD32 存储仍在进行中(STORING)且来自同一通道,
|
||||
// 自动中止旧存储并重新开始。覆盖"上传过程中刷新网页再上传"场景。
|
||||
// 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。
|
||||
// RELAYING 是中继阶段,不可中断,也拒绝。
|
||||
if (h && m_sysState == OTA_SYS_GD32_STORING) {
|
||||
if (m_sysState == OTA_SYS_GD32_STORING) {
|
||||
if (m_otaChannel != channel) {
|
||||
DEBUG_WARN(TAG, "GD32存中 ch=%d 拒跨通道 ch=%d",
|
||||
(int)m_otaChannel, (int)channel);
|
||||
@@ -169,23 +178,20 @@ esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size, ota_channel_t ch
|
||||
|
||||
// 系统忙检查:仅拒绝真正"在传输"的活跃状态
|
||||
// STORED / COMPLETE 是"待中继/已中继"的非活跃状态 → 允许新存储覆盖
|
||||
if (h && (m_sysState == OTA_SYS_ESP32_RECEIVING ||
|
||||
if (m_sysState == OTA_SYS_ESP32_RECEIVING ||
|
||||
m_sysState == OTA_SYS_ESP32_FINALIZING ||
|
||||
m_sysState == OTA_SYS_ESP32_REBOOTING ||
|
||||
m_sysState == OTA_SYS_GD32_RELAYING)) {
|
||||
m_sysState == OTA_SYS_GD32_RELAYING) {
|
||||
DEBUG_WARN(TAG, "系统忙 (state=%d),拒绝 GD32 存储请求", (int)m_sysState);
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
// 如果之前已有存储但未中继(STORED)或刚中继完(COMPLETE),允许新存储覆盖。
|
||||
// 注意:清理 NVS meta 不在此处做,避免与紧跟的 prepareGd32Storage 内
|
||||
// clearGd32Meta + saveGd32Meta 重复一次写入,仅做状态注释说明。
|
||||
if (h && (m_sysState == OTA_SYS_GD32_STORED || m_sysState == OTA_SYS_GD32_COMPLETE)) {
|
||||
if (m_sysState == OTA_SYS_GD32_STORED || m_sysState == OTA_SYS_GD32_COMPLETE) {
|
||||
DEBUG_INFO(TAG, "覆盖旧 GD32 存储状态,开始新存储 (size=%" PRIu32 ")", total_size);
|
||||
}
|
||||
|
||||
if (h) {
|
||||
m_otaEpoch++; // ★ 新会话开始,递增 epoch
|
||||
m_otaChannel = channel; // ★ 记录发起通道
|
||||
m_sysState = OTA_SYS_GD32_STORING;
|
||||
@@ -193,9 +199,10 @@ esp_err_t OtaCoordinator::beginGd32Storage(uint32_t total_size, ota_channel_t ch
|
||||
m_busySinceTick = xTaskGetTickCount(); // ★ 记录忙状态起始时刻
|
||||
DEBUG_DBG(TAG, "GD32存开始 %" PRIu32 "B →STORING ep=%u ch=%d",
|
||||
total_size, m_otaEpoch, (int)channel);
|
||||
}
|
||||
// ★ 预分配 flashWriteChunk 对齐缓冲(OTA 期消除逐块 malloc;非 OTA 期零占用)
|
||||
Gd32FlashStore::acquireAlignBufForOta();
|
||||
unlock(h);
|
||||
return h ? ESP_OK : ESP_ERR_INVALID_STATE;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t OtaCoordinator::prepareGd32Storage(uint32_t total_size, ota_channel_t channel)
|
||||
@@ -416,23 +423,14 @@ void OtaCoordinator::notifyBleCoexEnd()
|
||||
// ============================================================
|
||||
esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t channel)
|
||||
{
|
||||
bool h = lock();
|
||||
|
||||
// ★ 卡死自愈检查
|
||||
if (h) watchdogCheckLocked();
|
||||
|
||||
// ★ 电机 OTA 互斥:ESP32 自身 OTA 会重启设备,中断正在进行的电机喂数,
|
||||
// 电机 OTA 期间拒绝 ESP32 OTA。
|
||||
if (h && motorOtaIsActive()) {
|
||||
DEBUG_WARN(TAG, "电机 OTA 进行中,拒绝 ESP32 OTA");
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
bool h;
|
||||
esp_err_t pre = beginLockedWithChecks("ESP32 OTA", h);
|
||||
if (pre != ESP_OK) return pre;
|
||||
|
||||
// ★ 同类型重启:若上一次 ESP32 OTA 仍在接收中(RECEIVING)且来自同一通道,
|
||||
// 自动中止旧会话并重新开始。覆盖"上传完成后刷新网页再上传"场景。
|
||||
// 跨通道(BLE→WiFi 或 WiFi→BLE)仍拒绝,保持互斥。
|
||||
if (h && m_sysState == OTA_SYS_ESP32_RECEIVING) {
|
||||
if (m_sysState == OTA_SYS_ESP32_RECEIVING) {
|
||||
if (m_otaChannel != channel) {
|
||||
DEBUG_WARN(TAG, "ESP32收中 ch=%d 拒跨通道 ch=%d",
|
||||
(int)m_otaChannel, (int)channel);
|
||||
@@ -447,7 +445,7 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann
|
||||
// ★ FINALIZING 残留自愈:SHA 不匹配时 core 已 abort,但旧固件可能未把协调器
|
||||
// 复位到 IDLE。core 已空闲 ⇒ 协调器状态是假占用,允许清掉后开新会话。
|
||||
// core 仍 active ⇒ 真正在收尾,拒绝(不可 forceCleanup,防并发写分区)。
|
||||
if (h && m_sysState == OTA_SYS_ESP32_FINALIZING) {
|
||||
if (m_sysState == OTA_SYS_ESP32_FINALIZING) {
|
||||
if (!Esp32OtaCore::instance().isActive()) {
|
||||
DEBUG_WARN(TAG, "FINALIZING残留(core空闲),强制复位");
|
||||
doAbortLocked();
|
||||
@@ -459,19 +457,17 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann
|
||||
}
|
||||
|
||||
// 系统忙检查:GD32 传输/中继进行中,或 ESP32 重启中,均拒绝。
|
||||
if (h && (m_sysState == OTA_SYS_GD32_STORING ||
|
||||
if (m_sysState == OTA_SYS_GD32_STORING ||
|
||||
m_sysState == OTA_SYS_GD32_RELAYING ||
|
||||
m_sysState == OTA_SYS_ESP32_REBOOTING)) {
|
||||
m_sysState == OTA_SYS_ESP32_REBOOTING) {
|
||||
DEBUG_WARN(TAG, "忙 state=%d(%s) 拒ESP32 OTA",
|
||||
(int)m_sysState, stateToString(m_sysState));
|
||||
unlock(h);
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
esp_err_t err = ESP_ERR_INVALID_STATE;
|
||||
if (h) {
|
||||
// 委托 Esp32OtaCore 处理分区/互斥逻辑
|
||||
err = Esp32OtaCore::instance().begin(total_size);
|
||||
esp_err_t err = Esp32OtaCore::instance().begin(total_size);
|
||||
if (err == ESP_OK) {
|
||||
m_otaEpoch++; // ★ 新会话开始,递增 epoch(使旧 handler 的 abortCurrentIfEpoch 失效)
|
||||
m_otaChannel = channel; // ★ 记录发起通道
|
||||
@@ -481,7 +477,6 @@ esp_err_t OtaCoordinator::beginEsp32Ota(uint32_t total_size, ota_channel_t chann
|
||||
} else {
|
||||
DEBUG_WARN(TAG, "ESP32 OTA begin 失败: %s", esp_err_to_name(err));
|
||||
}
|
||||
}
|
||||
|
||||
unlock(h);
|
||||
return err;
|
||||
@@ -622,9 +617,11 @@ void OtaCoordinator::doAbortLocked()
|
||||
|
||||
if (m_sysState == OTA_SYS_GD32_STORING) {
|
||||
Gd32FlashStore::instance().metaClear();
|
||||
Gd32FlashStore::releaseAlignBufForOta(); // 释放 OTA 期对齐缓冲
|
||||
} else if (m_sysState == OTA_SYS_GD32_RELAYING) {
|
||||
/* 中继中止必须 CANCEL 远端,否则 GD32 停在 WAITING_* 半会话 */
|
||||
OtaRelay::instance().abortRemoteAndReset(true);
|
||||
Gd32FlashStore::releaseAlignBufForOta(); // 中继阶段也可能写过 flash
|
||||
} else if (m_sysState == OTA_SYS_ESP32_RECEIVING ||
|
||||
m_sysState == OTA_SYS_ESP32_FINALIZING) {
|
||||
Esp32OtaCore::instance().abort();
|
||||
@@ -761,12 +758,6 @@ const char* OtaCoordinator::stateToString(ota_system_state_t s)
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief 获取 GD32 APP 存活状态(委托 OtaRelay)。 */
|
||||
bool OtaCoordinator::getGd32AppAlive() const
|
||||
{
|
||||
return OtaRelay::getGd32AppAlive();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// OTA 状态快照
|
||||
// ============================================================
|
||||
|
||||
@@ -420,12 +420,6 @@ public:
|
||||
bool isIdle() const;
|
||||
|
||||
// ── GD32 状态(供 status API 使用,委托给 OtaRelay 查询)──
|
||||
/**
|
||||
* @brief 获取 GD32 APP 存活状态。
|
||||
* @return true 表示 GD32 APP 存活。
|
||||
*/
|
||||
bool getGd32AppAlive() const;
|
||||
|
||||
// ── 完全复位 ──
|
||||
/**
|
||||
* @brief 完全复位协调器和所有子系统状态。
|
||||
@@ -479,6 +473,13 @@ private:
|
||||
// 已持锁中止:清理子系统并将状态复位为 IDLE
|
||||
void doAbortLocked();
|
||||
|
||||
/** @brief 统一 "lock + watchdog 自愈 + 电机 OTA 互斥拒绝" 前缀检查。
|
||||
* beginGd32Storage / beginEsp32Ota 共用此逻辑。
|
||||
* @param tag 日志标签(如 "GD32 存储" / "ESP32 OTA")
|
||||
* @param out_lock 成功时输出锁句柄(调用方负责 unlock)
|
||||
* @return ESP_OK=前缀通过可继续;ESP_ERR_INVALID_STATE=被拒绝(已 unlock) */
|
||||
esp_err_t beginLockedWithChecks(const char *tag, bool &out_lock);
|
||||
|
||||
// ★ 已持锁版本的 ESP32 状态切换 helper:设置 m_sysState 并打印一条 INFO 日志。
|
||||
// 供 notifyEsp32*Complete / notifyEsp32Rebooting 等锁内路径复用,避免重复样板代码。
|
||||
void setEsp32StateLocked(ota_system_state_t s, const char *dbg_msg);
|
||||
|
||||
+75
-71
@@ -349,10 +349,14 @@ ota_result_t OtaRelay::handleTransferring(relay_event_t event, const uint8_t *pa
|
||||
|
||||
DEBUG_INFO("OTA", "=== 阶段3: 数据包传输 (%" PRIu32 " 包) ===", m_transferTotalPackets);
|
||||
|
||||
/* 转发预算:自本阶段起算。正常 ~15ms/包,预算 40ms/包 + 20s 底(上限 120s)。 */
|
||||
constexpr uint32_t kXferMinMs = 20U * 1000U;
|
||||
/* 转发预算:自本阶段起算。正常 ~2ms/包(12MHz+ 读预算收紧后实测),
|
||||
* 单包失败重发最坏 ~95ms/轮 × 3 轮 ≈ 285ms(底层 3 次尝试 + relay 50ms 窗口)。
|
||||
* 预算按"正常 + 重发余量":25ms/包 ≈ 正常 2ms + 约 10% 包重发 1 次的平均,
|
||||
* 底 10s 覆盖慢启动/握手余量(777 包正常仅 ~1.6s);上限 120s 兜极端慢速。
|
||||
* 跨包卡死(10s 无成功帧)由 kStallTimeoutMs 单独检测,总预算不重复兜底。 */
|
||||
constexpr uint32_t kXferMinMs = 10U * 1000U;
|
||||
constexpr uint32_t kXferMaxMs = 120U * 1000U;
|
||||
constexpr uint32_t kXferPerPktMs = 40U;
|
||||
constexpr uint32_t kXferPerPktMs = 25U;
|
||||
uint32_t xfer_ms = kXferMinMs + m_transferTotalPackets * kXferPerPktMs;
|
||||
if (xfer_ms > kXferMaxMs) {
|
||||
xfer_ms = kXferMaxMs;
|
||||
@@ -448,9 +452,6 @@ ota_result_t OtaRelay::forwardFirmware()
|
||||
|
||||
DEBUG_INFO("OTA", "开始固件转发: %" PRIu32 " 字节", meta.total_size);
|
||||
|
||||
/* 中继全程禁止 worker 把 OTA ACK/WAIT 当 EVENT 偷读 */
|
||||
SpiMaster::instance().setSuppressRingService(true);
|
||||
|
||||
/* 完整性门控(避免与 /ota/relay·BLE 触发前的 verifyGd32Firmware 二次全盘 SHA):
|
||||
* - meta 必须完整 stored≥total
|
||||
* - checksum 非全零:触发路径已 finalize+比对,此处信任 meta,跳过再扫 Flash
|
||||
@@ -500,10 +501,8 @@ ota_result_t OtaRelay::forwardFirmware()
|
||||
DEBUG_ERR("OTA", "中继失败: result=%d, state=%d", (int)res, (int)getState());
|
||||
/* 必须通知 GD32 CANCEL,否则远端停在 WAITING_* 半会话 */
|
||||
abortRemoteAndReset(true);
|
||||
} else {
|
||||
/* 成功路径:解除按铃抑制(reset 也会做,成功时状态机可能停在 DONE) */
|
||||
SpiMaster::instance().setSuppressRingService(false);
|
||||
}
|
||||
/* 成功/失败统一由 reset() 解除 ring 抑制(relay task 侧持有 ref) */
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -1174,7 +1173,10 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
|
||||
* - 等 ACK 期间可预组下一包(与从机处理重叠)。
|
||||
*/
|
||||
constexpr uint32_t kMaxDataSends = 3U;
|
||||
constexpr uint32_t kAckWaitMs = 6000U; /* 单次发送后等 ACK 上限(覆盖 GD32 增量 drain) */
|
||||
/* 单次发送后等 ACK 上限:GD32 正常 ms 级回 ACK(ISR 回 WAIT、主循环回 ACK),
|
||||
* 50ms 未到即重发(DUP-SEQ 回放捞回),不再干等 6s(用户指定 50ms)。
|
||||
* credit==0 的排空积压由 transact 内部 credit 背压消化,此处无需长等。 */
|
||||
constexpr uint32_t kAckWaitMs = 50U;
|
||||
constexpr uint32_t kPollSliceMs = 50U; /* 略紧:DATA_READY 驱动,缩短空切片 */
|
||||
|
||||
TickType_t pkt_start = xTaskGetTickCount();
|
||||
@@ -1204,9 +1206,11 @@ bool OtaRelay::sendRelayPacketWithRetry(uint32_t seq, const uint8_t tx_buf[64],
|
||||
{
|
||||
resp_len = 0;
|
||||
resp_type = 0;
|
||||
/* 等首响应上限 10ms:一次 SPI 事务最多 5ms(用户指定),
|
||||
* 超时立即进入下方 50ms poll 窗口,不再干等 200ms。 */
|
||||
esp_err_t err = SpiMaster::instance().transact(
|
||||
SPI_OTA_CMD_DATA, tx_buf, 64,
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 1200);
|
||||
&resp_type, resp_payload, sizeof(resp_payload), &resp_len, 10);
|
||||
logRespPayload("DATA_PACKET", resp_type, resp_payload, resp_len);
|
||||
if (err == ESP_OK && resp_type == SPI_OTA_RSP_WAIT) {
|
||||
saw_wait = true;
|
||||
@@ -1412,6 +1416,58 @@ bool OtaRelay::probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t *out_uptime)
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 统一 "HELLO(role=APP) → 可选 PING 探测" 循环(doWaitForAppAlive + taskEntry 共用)。
|
||||
*
|
||||
* 两处原循环差异仅:taskEntry 在 HELLO 已带身份(gd32IdKnown)时跳过 PING 直接成功。
|
||||
* 用 allow_hello_shortcut 参数统一:true 时启用 HELLO 身份快路径。
|
||||
*
|
||||
* @param timeout_ms PING 超时
|
||||
* @param ping_interval_ms 每轮间隔
|
||||
* @param max_attempts 最大轮数
|
||||
* @param allow_hello_shortcut true=HELLO 带身份即成功(taskEntry 场景)
|
||||
* @return true=APP 存活已确认
|
||||
*/
|
||||
bool OtaRelay::helloAndProbeApp(uint32_t timeout_ms, uint32_t ping_interval_ms,
|
||||
uint32_t max_attempts, bool allow_hello_shortcut)
|
||||
{
|
||||
bool session_ok = false;
|
||||
for (uint32_t attempt = 0; attempt < max_attempts; ++attempt) {
|
||||
vTaskDelay(pdMS_TO_TICKS(ping_interval_ms));
|
||||
|
||||
if (!session_ok) {
|
||||
if (SpiMaster::instance().hello(1500U, V2_ROLE_APP) == ESP_OK &&
|
||||
SpiMaster::instance().gd32Role() == V2_ROLE_APP) {
|
||||
session_ok = true;
|
||||
/* HELLO 已带身份时直接缓存版本/开关,无需再等 PING 才亮版本 */
|
||||
if (allow_hello_shortcut && SpiMaster::instance().gd32IdKnown()) {
|
||||
StateLockGuard lock;
|
||||
gd32Version = SpiMaster::instance().gd32VersionCached();
|
||||
gd32AppAlive = true;
|
||||
DEBUG_INFO("OTA", "HELLO就绪 APP=v%d.%d.%d.%d",
|
||||
gd32Version.app_major, gd32Version.app_minor,
|
||||
gd32Version.app_patch, gd32Version.app_build);
|
||||
DEBUG_INFO("OTA", "HELLO就绪 BOOT=v%d.%d.%d.%d",
|
||||
gd32Version.boot_major, gd32Version.boot_minor,
|
||||
gd32Version.boot_patch, gd32Version.boot_build);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/%u)", attempt + 1U, (unsigned)max_attempts);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
/* HELLO 成功但无身份(旧固件),或 doWaitForAppAlive 路径:回退 APP PING */
|
||||
if (probeAndCacheAppAlive(timeout_ms)) {
|
||||
return true;
|
||||
}
|
||||
/* PING 失败:清 session,下轮重 HELLO(避免 sticky 假会话) */
|
||||
session_ok = false;
|
||||
DEBUG_INFO("OTA", "等待 GD32 APP 存活... (%" PRIu32 "/%u)", attempt + 1U, (unsigned)max_attempts);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @brief 协议子步骤:发送 FINISH_OTA 指令并等待 GD32 ACK(10 次重试)。 */
|
||||
ota_result_t OtaRelay::doSendFinishOta()
|
||||
{
|
||||
@@ -1485,30 +1541,8 @@ ota_result_t OtaRelay::doWaitForAppAlive(uint32_t timeout_ms)
|
||||
/* FINISH 后短暂安静,避免 jump/APP SPI arm 窗口被 HELLO 打乱(对齐既有 300ms 节拍)。 */
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
|
||||
/* APP 起机约数秒;HELLO 须 role=APP 再建 session,再 appPing。 */
|
||||
bool session_established = false;
|
||||
for (uint32_t attempt = 0; attempt < 20; ++attempt) {
|
||||
vTaskDelay(pdMS_TO_TICKS(300));
|
||||
|
||||
if (!session_established) {
|
||||
esp_err_t hr = SpiMaster::instance().hello(1500U, V2_ROLE_APP);
|
||||
if (hr == ESP_OK && SpiMaster::instance().gd32Role() == V2_ROLE_APP) {
|
||||
session_established = true;
|
||||
} else {
|
||||
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (probeAndCacheAppAlive(timeout_ms)) {
|
||||
break;
|
||||
}
|
||||
/* PING 失败:清 session,下轮重 HELLO(避免 sticky 假会话) */
|
||||
session_established = false;
|
||||
DEBUG_INFO("OTA", "等待 GD32 APP 启动... (%" PRIu32 "/20)", attempt + 1);
|
||||
}
|
||||
|
||||
if (!getGd32AppAlive()) {
|
||||
/* doWaitForAppAlive 不启用 HELLO 身份快路径:OTA 完成后必须 PING 确认 APP 真正运行 */
|
||||
if (!helloAndProbeApp(timeout_ms, 300U, 20U, /*allow_hello_shortcut=*/false)) {
|
||||
DEBUG_ERR("OTA", "GD32 APP 启动确认超时,OTA 失败");
|
||||
/* APP 未起来时 CANCEL 可能无效,仍尽力清理 BOOT 半会话 */
|
||||
abortRemoteAndReset(true);
|
||||
@@ -1562,42 +1596,7 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
/* 启动 GD32 探测:先 hello 建 v2 session,再 appPing。 */
|
||||
{
|
||||
DEBUG_INFO("OTA", "启动初始 GD32 探测 (hello+probe)...");
|
||||
bool session_ok = false;
|
||||
bool alive = false;
|
||||
for (uint32_t attempt = 0; attempt < 20U; ++attempt) {
|
||||
vTaskDelay(pdMS_TO_TICKS(300));
|
||||
if (!session_ok) {
|
||||
if (SpiMaster::instance().hello(1500U, V2_ROLE_APP) == ESP_OK &&
|
||||
SpiMaster::instance().gd32Role() == V2_ROLE_APP) {
|
||||
session_ok = true;
|
||||
/* HELLO 已带身份时直接缓存版本/开关,无需再等 PING 才亮版本 */
|
||||
if (SpiMaster::instance().gd32IdKnown()) {
|
||||
StateLockGuard lock;
|
||||
gd32Version = SpiMaster::instance().gd32VersionCached();
|
||||
gd32AppAlive = true;
|
||||
alive = true;
|
||||
DEBUG_INFO("OTA", "HELLO就绪 APP=v%d.%d.%d.%d",
|
||||
gd32Version.app_major, gd32Version.app_minor,
|
||||
gd32Version.app_patch, gd32Version.app_build);
|
||||
DEBUG_INFO("OTA", "HELLO就绪 BOOT=v%d.%d.%d.%d",
|
||||
gd32Version.boot_major, gd32Version.boot_minor,
|
||||
gd32Version.boot_patch, gd32Version.boot_build);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
DEBUG_INFO("OTA", "等待 GD32 APP... (%" PRIu32 "/20)", attempt + 1U);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
/* 旧固件无 HELLO 身份:回退 APP PING */
|
||||
if (probeAndCacheAppAlive(2000)) {
|
||||
alive = true;
|
||||
break;
|
||||
}
|
||||
session_ok = false;
|
||||
DEBUG_INFO("OTA", "等待 GD32 APP 存活... (%" PRIu32 "/20)", attempt + 1U);
|
||||
}
|
||||
if (!alive) {
|
||||
if (!helloAndProbeApp(2000U, 300U, 20U, /*allow_hello_shortcut=*/true)) {
|
||||
DEBUG_WARN("OTA", "启动 GD32 探测未成功(中继前仍可再 hello)");
|
||||
}
|
||||
}
|
||||
@@ -1610,6 +1609,11 @@ void OtaRelay::taskEntry(void *pvParameters)
|
||||
}
|
||||
|
||||
DEBUG_INFO("OTA", "收到 OTA 调度信号: %" PRIu32 ",开始转发...", msg);
|
||||
/* 尽早抑制 ring service:onRelayStarted 可能因协调器锁(/system/status
|
||||
* 等争锁)阻塞,此窗口内 worker 会把上一阶段残留 RESPONSE 当 EVENT 偷读
|
||||
* (线上 log#1:TRANSFERRING 起始瞬间 serviceRing 读到 type=0x80)。
|
||||
* forwardFirmware 不再重复设置;释放统一走 reset()。 */
|
||||
SpiMaster::instance().setSuppressRingService(true);
|
||||
// 通过 IRelayListener 接口回调通知协调器
|
||||
IRelayListener *listener = getRelayListener();
|
||||
if (listener) listener->onRelayStarted();
|
||||
|
||||
+13
-40
@@ -118,16 +118,6 @@ public:
|
||||
*/
|
||||
ota_result_t forwardFirmware();
|
||||
|
||||
// ── 事件驱动接口 ──
|
||||
/**
|
||||
* @brief 向状态机投递事件,驱动状态转换.
|
||||
* @param event 事件类型.
|
||||
* @param payload 关联数据(响应载荷等),可为nullptr.
|
||||
* @param len 数据长度.
|
||||
* @return 当前操作结果码(OTA_RESULT_OK 表示事件已处理且可继续).
|
||||
*/
|
||||
ota_result_t processEvent(relay_event_t event, const uint8_t *payload, size_t len);
|
||||
|
||||
// ── 状态查询(线程安全)──
|
||||
/**
|
||||
* @brief 获取当前中继状态.
|
||||
@@ -145,31 +135,6 @@ public:
|
||||
* @return 中继进度快照。
|
||||
*/
|
||||
static RelaySnapshot getSnapshot();
|
||||
/**
|
||||
* @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 true 表示存活。
|
||||
*/
|
||||
static void setGd32AppAlive(bool alive);
|
||||
/**
|
||||
* @brief 设置GD32固件版本.
|
||||
* @param ver 版本信息结构体引用。
|
||||
*/
|
||||
static void setGd32Version(const gd32_version_t &ver);
|
||||
/**
|
||||
* @brief 获取 OTA 中继任务句柄。
|
||||
* @return FreeRTOS 任务句柄。
|
||||
@@ -195,11 +160,6 @@ public:
|
||||
* @param listener 监听器指针,传 nullptr 取消注册。
|
||||
*/
|
||||
static void setRelayListener(IRelayListener *listener);
|
||||
/**
|
||||
* @brief 获取当前注册的监听器。
|
||||
* @return 监听器指针,未注册时为 nullptr。
|
||||
*/
|
||||
static IRelayListener* getRelayListener();
|
||||
|
||||
// ── 队列访问器 ──
|
||||
/**
|
||||
@@ -223,6 +183,19 @@ public:
|
||||
*/
|
||||
static bool probeAndCacheAppAlive(uint32_t timeout_ms, uint32_t *out_uptime = nullptr);
|
||||
|
||||
/** @brief 统一 "HELLO(role=APP) → PING 探测" 循环(doWaitForAppAlive + taskEntry 共用)。 */
|
||||
static bool helloAndProbeApp(uint32_t timeout_ms, uint32_t ping_interval_ms,
|
||||
uint32_t max_attempts, bool allow_hello_shortcut);
|
||||
|
||||
// ── 内部状态写入(仅 ota_relay.cpp 使用,无外部调用)──
|
||||
static void setState(relay_state_t s);
|
||||
static void setRelaySeq(uint32_t seq);
|
||||
static void setRelayTotalPackets(uint32_t total);
|
||||
static void setGd32AppAlive(bool alive);
|
||||
static void setGd32Version(const gd32_version_t &ver);
|
||||
static IRelayListener* getRelayListener();
|
||||
ota_result_t processEvent(relay_event_t event, const uint8_t *payload, size_t len);
|
||||
|
||||
// ── 线程安全锁 ──
|
||||
static void initMutex(); ///< 初始化状态互斥锁(taskEntry 首行调用)
|
||||
static constexpr TickType_t kLockTimeout = pdMS_TO_TICKS(50); ///< 锁获取超时时间
|
||||
|
||||
@@ -11,7 +11,11 @@ void drainOtaDataQueue()
|
||||
{
|
||||
BleOtaDataBatch stale;
|
||||
while (s_otaDataQueue && xQueueReceive(s_otaDataQueue, &stale, 0) == pdTRUE) {
|
||||
if (stale.data) free(stale.data);
|
||||
if (stale.pool_idx >= 0) {
|
||||
ble_ota_pool_release(stale.pool_idx); // 静态池槽:归还
|
||||
} else if (stale.data) {
|
||||
free(stale.data); // heap 回退路径:释放
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,132 @@ ota_meta_t s_ble_gd32_meta = {};
|
||||
bool s_ble_gd32_meta_active = false;
|
||||
bool s_conn_fallback_requested = false;
|
||||
|
||||
// ============================================================
|
||||
// DATA_IN 缓冲池(2026-08 内存优化)
|
||||
//
|
||||
// 背景:原 DATA_IN 路径每包 malloc(recv_len) + worker free(item.data),
|
||||
// BLE 高速下发时高频堆抖动制造碎片,挤掉 lwip pbuf 连续块导致网络断连。
|
||||
//
|
||||
// 策略:OTA begin 时一次性 calloc 池(连续大块,比高频 per-pack malloc
|
||||
// 碎片小得多),DATA_IN 借槽拷贝 → 入队 → worker 处理后归还;
|
||||
// OTA end/abort/断连时 free 整个池。
|
||||
// - 非 OTA 期间:池指针为 NULL,零占用,堆基线不受影响。
|
||||
// - OTA 期间:借/还 O(1) 标志位操作,无堆抖动。
|
||||
// - 池满/包过大回退 malloc + 负 ACK(保留原语义,避免功能回退)。
|
||||
//
|
||||
// 池容量 8 槽 × 512B = 4KB(2026-08-01 修正):
|
||||
// 原 8×4096B=32KB 使 OTA 全程多占 28KB 堆,刷写 1MB 固件收尾时
|
||||
// (0xF8 后需创建 finish task + image verify)直接 ESP_ERR_NO_MEM。
|
||||
// 单次 ATT 写入上限 = MTU 512B(BLE_MTU_SIZE),常态包 ≤512B 全走池;
|
||||
// NimBLE 合并的大 batch(≤4096B,经 BLE_OTA_BUF_SIZE 截断)回退
|
||||
// malloc,与无池时代行为一致。4KB 固定占用对收尾任务足够安全。
|
||||
// ============================================================
|
||||
struct BleDataPool {
|
||||
static constexpr size_t kSlots = 8;
|
||||
static constexpr size_t kSlotSize = 512; // 单次 ATT 写入上限(MTU);大 batch 回退 malloc
|
||||
uint8_t *buf[kSlots]; // kSlots 个独立槽(calloc 一次分配 kSlots*kSlotSize 后切分)
|
||||
volatile bool in_use[kSlots];
|
||||
};
|
||||
static BleDataPool *s_data_pool = nullptr;
|
||||
|
||||
// 前向声明(poolAlloc 与 poolFree 互相引用)
|
||||
static void poolFree();
|
||||
|
||||
/** @brief OTA begin 时分配池。失败返回 false(调用方回退纯 malloc 路径)。
|
||||
* 安全性:上次会话遗留的池仅当全部槽空闲才复用。若 worker 仍持有在途
|
||||
* batch(in_use=true),强制复位复用会让新会话 DATA_IN 覆写旧 worker
|
||||
* 正在解析的内存 → flash 写坏 → 收尾 SHA/verify 失败。此时本会话回退
|
||||
* malloc,旧池留待 worker 归还后由下次 poolFree 释放。 */
|
||||
static bool poolAlloc()
|
||||
{
|
||||
if (s_data_pool) {
|
||||
// 上次会话遗留池:检查是否全部空闲(in_use 仅由 worker/队列消费侧复位)
|
||||
for (size_t i = 0; i < BleDataPool::kSlots; ++i) {
|
||||
if (s_data_pool->in_use[i]) {
|
||||
DEBUG_WARN(TAG, "DATA_IN 池槽仍被占用,本会话回退 malloc");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true; // 全部空闲:安全复用
|
||||
}
|
||||
s_data_pool = (BleDataPool *)calloc(1, sizeof(BleDataPool));
|
||||
if (!s_data_pool) return false;
|
||||
// 一次性分配所有槽的负载数据(连续大块,碎片友好)
|
||||
uint8_t *blob = (uint8_t *)calloc(BleDataPool::kSlots, BleDataPool::kSlotSize);
|
||||
if (!blob) {
|
||||
free(s_data_pool);
|
||||
s_data_pool = nullptr;
|
||||
return false;
|
||||
}
|
||||
for (size_t i = 0; i < BleDataPool::kSlots; ++i) {
|
||||
s_data_pool->buf[i] = blob + i * BleDataPool::kSlotSize;
|
||||
s_data_pool->in_use[i] = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @brief OTA end/abort/断连时释放整个池。
|
||||
* 安全性:仅当所有槽均空闲时才释放;若 worker 仍有 in-flight batch
|
||||
* 占用槽(in_use=true),跳过本次释放,留待下次 poolFree 全部空闲时
|
||||
* 再释放。这避免了 worker 访问已释放内存的竞态(单核抢占:
|
||||
* session_reset 运行在 NimBLE host task,worker 是独立任务,二者仅
|
||||
* 通过队列交接,无显式同步)。 */
|
||||
static void poolFree()
|
||||
{
|
||||
if (!s_data_pool) return;
|
||||
for (size_t i = 0; i < BleDataPool::kSlots; ++i) {
|
||||
if (s_data_pool->in_use[i]) {
|
||||
// 有槽仍被 worker 占用,跳过释放(下次 poolFree 再试)
|
||||
DEBUG_DBG(TAG, "DATA_IN 池延迟释放: 槽%u in_use", (unsigned)i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// buf[0] 是连续 blob 的起始(一次 calloc 分配)
|
||||
if (s_data_pool->buf[0]) {
|
||||
free(s_data_pool->buf[0]);
|
||||
}
|
||||
free(s_data_pool);
|
||||
s_data_pool = nullptr;
|
||||
}
|
||||
|
||||
/** @brief 从池借一个槽,返回索引;无空闲或池未分配返回 -1。仅 DATA_IN(host task)调用。 */
|
||||
static int poolAcquire()
|
||||
{
|
||||
if (!s_data_pool) return -1;
|
||||
for (size_t i = 0; i < BleDataPool::kSlots; ++i) {
|
||||
if (!s_data_pool->in_use[i]) {
|
||||
s_data_pool->in_use[i] = true;
|
||||
return (int)i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/** @brief 归还槽到池。仅 worker / drain(队列消费侧)调用。 */
|
||||
static void poolRelease(int idx)
|
||||
{
|
||||
if (!s_data_pool) return;
|
||||
if (idx >= 0 && idx < (int)BleDataPool::kSlots) {
|
||||
s_data_pool->in_use[idx] = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief 借槽拷贝数据;返回槽索引或 -1(池满/未分配/包过大)。 */
|
||||
static int poolCopyIn(const uint8_t *src, size_t len)
|
||||
{
|
||||
if (!s_data_pool || len > BleDataPool::kSlotSize) return -1;
|
||||
int idx = poolAcquire();
|
||||
if (idx < 0) return -1;
|
||||
memcpy(s_data_pool->buf[idx], src, len);
|
||||
return idx;
|
||||
}
|
||||
|
||||
/** @brief 归还池槽(跨 TU 调用入口:drainOtaDataQueue / 会话清理)。 */
|
||||
void ble_ota_pool_release(int idx)
|
||||
{
|
||||
poolRelease(idx);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// DATA_IN 丢包负反馈:把 malloc 失败 / 队列满从"静默丢包"变成
|
||||
// "主动发缺口 ACK 让前端按 completed_bytes 续传退避"。
|
||||
@@ -394,6 +520,11 @@ esp_err_t ble_esp32_ota_begin(size_t total_size)
|
||||
BleOta::instance().m_esp32LastSeq = 0xFFFFU;
|
||||
// ★ 分配 reorder buffer(失败则自动回退丢弃策略)
|
||||
esp32_reorder_alloc();
|
||||
// ★ 分配 DATA_IN 缓冲池(失败则 DATA_IN 回退 per-pack malloc 路径)
|
||||
if (!poolAlloc()) {
|
||||
DEBUG_WARN(TAG, "DATA_IN 池分配失败,回退 malloc free=%u",
|
||||
(unsigned)esp_get_free_heap_size());
|
||||
}
|
||||
// ★ BLE OTA 开始:降低 WiFi 射频占用
|
||||
ble_ota_boost_wifi_ps();
|
||||
/* 刷写前再要一次高速连接参数(连接时若未谈妥,此处补刀) */
|
||||
@@ -439,6 +570,8 @@ void ble_ota_session_reset()
|
||||
BleOta::instance().m_esp32RebootPending = false;
|
||||
// 释放 ESP32 reorder buffer(无 buffer 时为空操作)
|
||||
esp32_reorder_free();
|
||||
// 释放 DATA_IN 缓冲池(非 OTA 期间零占用,恢复堆基线)
|
||||
poolFree();
|
||||
// 复位 GD32 路径乱序包计数器(节流 ACK 计数),保证下次 GD32 上传从干净状态开始
|
||||
s_gd32_ooo_count = 0;
|
||||
s_ble_gd32_meta = {};
|
||||
@@ -697,21 +830,33 @@ int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
|
||||
}
|
||||
|
||||
// 将数据拷贝到堆上并送入工作队列,避免在 NimBLE host task 中执行
|
||||
// 将数据拷贝到队列缓冲并送入工作线程,避免在 NimBLE host task 中执行
|
||||
// 耗时的 flash 操作(esp_ota_begin / esp_ota_write)导致 BLE 连接断开。
|
||||
// ★ 优先走静态池(消除高频 malloc/free 堆抖动);池满回退 malloc + 负 ACK。
|
||||
if (s_otaDataQueue) {
|
||||
// 解析 cmd 字节供负 ACK 区分 ESP32/GD32 续传分支(与 worker 解析一致)
|
||||
uint8_t cmd = parse_cmd_from_batch(buf, (size_t)recv_len);
|
||||
int pool_idx = poolCopyIn(buf, (size_t)recv_len);
|
||||
if (pool_idx >= 0) {
|
||||
BleOtaDataBatch item = { s_data_pool->buf[pool_idx], (size_t)recv_len, pool_idx };
|
||||
if (xQueueSend(s_otaDataQueue, &item, 0) != pdTRUE) {
|
||||
// 队列满:归还池槽 + 发缺口 ACK 让前端按 completed_bytes 续传退避,
|
||||
// 而非等满 8s 窗口超时后整窗口重发 → 重传风暴叠加低堆二次失败
|
||||
s_ota_drop_queue_full += 1;
|
||||
poolRelease(pool_idx);
|
||||
DEBUG_WARN(TAG, "DATA_IN满 丢%dB 等重传", recv_len);
|
||||
sendNegativeAckIfDue(cmd);
|
||||
}
|
||||
} else {
|
||||
// 池满或包过大:回退 malloc(保留原负 ACK 语义,避免静默丢包)
|
||||
uint8_t *heap_buf = (uint8_t *)malloc((size_t)recv_len);
|
||||
if (heap_buf) {
|
||||
memcpy(heap_buf, buf, (size_t)recv_len);
|
||||
BleOtaDataBatch item = { heap_buf, (size_t)recv_len };
|
||||
BleOtaDataBatch item = { heap_buf, (size_t)recv_len, -1 };
|
||||
if (xQueueSend(s_otaDataQueue, &item, 0) != pdTRUE) {
|
||||
// 队列满:发缺口 ACK 让前端按 completed_bytes 续传退避,
|
||||
// 而非等满 8s 窗口超时后整窗口重发 → 重传风暴叠加低堆二次失败
|
||||
s_ota_drop_queue_full += 1;
|
||||
DEBUG_WARN(TAG, "DATA_IN满 丢%dB 等重传", recv_len);
|
||||
free(heap_buf);
|
||||
DEBUG_WARN(TAG, "DATA_IN满 丢%dB 等重传", recv_len);
|
||||
sendNegativeAckIfDue(cmd);
|
||||
}
|
||||
} else {
|
||||
@@ -721,6 +866,7 @@ int ble_ota_data_in_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
sendNegativeAckIfDue(cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1096,7 +1242,8 @@ void ble_ota_data_worker_task(void *pvParameters)
|
||||
BleOtaDataBatch item;
|
||||
if (xQueueReceive(s_otaDataQueue, &item, portMAX_DELAY) == pdTRUE) {
|
||||
if (!item.data || item.len == 0) {
|
||||
free(item.data);
|
||||
if (item.pool_idx >= 0) poolRelease(item.pool_idx);
|
||||
else free(item.data);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1104,12 +1251,14 @@ void ble_ota_data_worker_task(void *pvParameters)
|
||||
if (!BleOta::instance().isConnected() ||
|
||||
BleOta::instance().isSuspendedForWifiOta()) {
|
||||
DEBUG_DBG(TAG, "BLE不可用 丢%zuB队列", item.len);
|
||||
free(item.data);
|
||||
if (item.pool_idx >= 0) poolRelease(item.pool_idx);
|
||||
else free(item.data);
|
||||
continue;
|
||||
}
|
||||
|
||||
ble_ota_process_data_batch(item.data, item.len);
|
||||
free(item.data);
|
||||
if (item.pool_idx >= 0) poolRelease(item.pool_idx);
|
||||
else free(item.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,7 +227,8 @@ int ble_ota_ctrl_access(uint16_t conn_handle, uint16_t attr_handle,
|
||||
BleOtaDataBatch stale;
|
||||
size_t dropped = 0;
|
||||
while (xQueueReceive(s_otaDataQueue, &stale, 0) == pdTRUE) {
|
||||
free(stale.data);
|
||||
if (stale.pool_idx >= 0) ble_ota_pool_release(stale.pool_idx);
|
||||
else free(stale.data);
|
||||
dropped++;
|
||||
}
|
||||
if (dropped > 0) {
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
struct BleOtaDataBatch {
|
||||
uint8_t *data;
|
||||
size_t len;
|
||||
int pool_idx; ///< 静态池槽索引(>=0 为池内,<0 为 heap malloc,需 free)
|
||||
};
|
||||
|
||||
struct BleGd32RelayWaitCtx {
|
||||
@@ -87,6 +88,8 @@ extern bool s_wifi_ps_boosted;
|
||||
extern bool s_conn_fallback_requested;
|
||||
|
||||
void drainOtaDataQueue();
|
||||
/** @brief 归还 DATA_IN 静态池槽(drainOtaDataQueue / 会话清理跨 TU 调用)。 */
|
||||
void ble_ota_pool_release(int idx);
|
||||
void ble_ota_start_advertising(void);
|
||||
void sendWindowAck(uint8_t cmd, uint16_t seq, uint32_t completed_bytes);
|
||||
void sendFinishEvent(uint8_t stage, uint32_t value);
|
||||
|
||||
+385
-325
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* http_util.h - HTTP handler 共享工具(JSON 追加、query 解析、hex 编解码)
|
||||
*
|
||||
* 消除 http_handlers.cpp / health_monitor.cpp / spi_speed_test.cpp 中的重复样板。
|
||||
*/
|
||||
#ifndef HTTP_UTIL_H
|
||||
#define HTTP_UTIL_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* ── JSON buffer 追加 ──────────────────────────────────────────
|
||||
* 统一替代 feedback_append / append_jsonf / appendJsonfSafe 三份拷贝。
|
||||
* 截断时不越界,返回新 offset;出错返回 -1 或原 offset(视调用方约定)。
|
||||
* 返回 (int)cap-1 表示已截断填满。 */
|
||||
static inline int http_json_append(char *buf, size_t cap, int offset, const char *fmt, ...)
|
||||
{
|
||||
if (!buf || cap == 0 || offset < 0 || (size_t)offset >= cap) return offset;
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
int n = vsnprintf(buf + offset, cap - (size_t)offset, fmt, ap);
|
||||
va_end(ap);
|
||||
if (n < 0) return offset;
|
||||
if ((size_t)n >= cap - (size_t)offset) return (int)cap - 1; /* 截断 */
|
||||
return offset + n;
|
||||
}
|
||||
|
||||
/* ── Query 参数解析 ────────────────────────────────────────────
|
||||
* 统一替代 6 处内联 ?lite=1 / ?bin=1 / ?ch=N 解析块。 */
|
||||
|
||||
/** 查询布尔型参数:值为 "1"/"y"/"t"/"T" 时返回 true,缺失或其它返回 false。 */
|
||||
static inline bool http_query_bool(httpd_req_t *req, const char *key)
|
||||
{
|
||||
char query[48];
|
||||
if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK) return false;
|
||||
char val[8] = {};
|
||||
if (httpd_query_key_value(query, key, val, sizeof(val)) != ESP_OK) return false;
|
||||
return val[0] == '1' || val[0] == 'y' || val[0] == 'Y' ||
|
||||
val[0] == 't' || val[0] == 'T';
|
||||
}
|
||||
|
||||
/** 查询无符号整型参数:缺失/非法时返回 defval。 */
|
||||
static inline uint32_t http_query_u32(httpd_req_t *req, const char *key, uint32_t defval)
|
||||
{
|
||||
char query[64];
|
||||
if (httpd_req_get_url_query_str(req, query, sizeof(query)) != ESP_OK) return defval;
|
||||
char val[16] = {};
|
||||
if (httpd_query_key_value(query, key, val, sizeof(val)) != ESP_OK) return defval;
|
||||
return (uint32_t)strtoul(val, NULL, 10);
|
||||
}
|
||||
|
||||
/* ── Hex 编解码 ────────────────────────────────────────────────
|
||||
* 统一替代内联 strtoul 循环和 snprintf("%02X") 循环。 */
|
||||
|
||||
/** hex 字符串 → 字节数组,返回写入字节数。 */
|
||||
static inline size_t http_hex_to_bytes(const char *hex, uint8_t *out, size_t max_out)
|
||||
{
|
||||
size_t hex_len = strlen(hex);
|
||||
size_t n = 0;
|
||||
for (size_t i = 0; i + 1 < hex_len && n < max_out; i += 2) {
|
||||
char byte_str[3] = { hex[i], hex[i + 1], 0 };
|
||||
out[n++] = (uint8_t)strtol(byte_str, NULL, 16);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/** hex 字符串(带长度)→ 字节数组,返回写入字节数。
|
||||
* 供 json_parse.h 的非 NUL 终止字符串值使用。 */
|
||||
static inline size_t http_hex_to_bytes_n(const char *hex, size_t hex_len, uint8_t *out, size_t max_out)
|
||||
{
|
||||
size_t n = 0;
|
||||
for (size_t i = 0; i + 1 < hex_len && n < max_out; i += 2) {
|
||||
char byte_str[3] = { hex[i], hex[i + 1], 0 };
|
||||
out[n++] = (uint8_t)strtol(byte_str, NULL, 16);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/** 字节数组 → 大写 hex 字符串(out 需至少 2*n+1 字节)。 */
|
||||
static inline void http_bytes_to_hex_upper(const uint8_t *in, size_t n, char *out)
|
||||
{
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
snprintf(out + i * 2, 3, "%02X", in[i]);
|
||||
}
|
||||
out[n * 2] = '\0';
|
||||
}
|
||||
|
||||
/** 字节数组 → 小写 hex 字符串(out 需至少 2*n+1 字节)。 */
|
||||
static inline void http_bytes_to_hex_lower(const uint8_t *in, size_t n, char *out)
|
||||
{
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
snprintf(out + i * 2, 3, "%02x", in[i]);
|
||||
}
|
||||
out[n * 2] = '\0';
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // HTTP_UTIL_H
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* json_parse.h - 极简 JSON 扁平对象解析器(cJSON 零堆分配替代)
|
||||
*
|
||||
* 项目中对 JSON 的使用全部是"解析前端发来的扁平 JSON 对象":
|
||||
* { "sha256": "...", "verify_only": true, "total_size": 12345, "type": "esp32" }
|
||||
* 无嵌套对象、无数组(can_send_batch 已自研流式解析)。
|
||||
* 本解析器直接在调用方提供的可写字符串缓冲上做扫描,
|
||||
* 不分配堆内存,字符串值通过就地 NUL 终止返回指针。
|
||||
*
|
||||
* 用法:
|
||||
* JsonView root = jsonParse(buf); // buf 须为可写 char[]
|
||||
* if (!root.valid) return error;
|
||||
* JsonView sha = jsonGet(&root, "sha256");
|
||||
* if (sha.type == JSON_STRING) { use(sha.str); }
|
||||
* JsonView sz = jsonGet(&root, "total_size");
|
||||
* if (sz.type == JSON_NUMBER) { uint32_t n = (uint32_t)sz.num; }
|
||||
* // 无需 free —— 指针指向 buf,buf 由调用方管理
|
||||
*/
|
||||
#ifndef JSON_PARSE_H
|
||||
#define JSON_PARSE_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef enum {
|
||||
JSON_INVALID = 0,
|
||||
JSON_NULL,
|
||||
JSON_BOOL,
|
||||
JSON_NUMBER,
|
||||
JSON_STRING,
|
||||
JSON_OBJECT,
|
||||
} JsonType;
|
||||
|
||||
typedef struct {
|
||||
JsonType type;
|
||||
bool valid;
|
||||
char *str; /* JSON_STRING:指向 buf 内字符串(不一定 NUL 终止!) */
|
||||
size_t str_len; /* JSON_STRING:字符串长度(配合 str 使用,避免就地写 NUL) */
|
||||
double num; /* JSON_NUMBER / JSON_BOOL */
|
||||
char *obj_start; /* JSON_OBJECT:'{' 后的内容区 */
|
||||
size_t obj_len;
|
||||
} JsonView;
|
||||
|
||||
/**
|
||||
* @brief 解析 JSON 字符串为根对象视图(就地扫描,可能写入 NUL 终止符)。
|
||||
* @param json 可写 NUL 终止 JSON 字符串(须为顶层 { ... } 扁平对象)
|
||||
* @return type=JSON_OBJECT 且 valid=true,或 valid=false 解析失败
|
||||
*/
|
||||
static inline JsonView jsonParse(char *json)
|
||||
{
|
||||
JsonView v = {};
|
||||
if (!json) return v;
|
||||
|
||||
char *p = json;
|
||||
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
|
||||
if (*p != '{') return v;
|
||||
|
||||
p++;
|
||||
char *content = p;
|
||||
int depth = 1;
|
||||
while (*p) {
|
||||
if (*p == '"') {
|
||||
p++;
|
||||
while (*p && *p != '"') {
|
||||
if (*p == '\\' && p[1]) p++;
|
||||
p++;
|
||||
}
|
||||
if (*p) p++;
|
||||
} else if (*p == '{') {
|
||||
depth++; p++;
|
||||
} else if (*p == '}') {
|
||||
depth--;
|
||||
if (depth == 0) break;
|
||||
p++;
|
||||
} else {
|
||||
p++;
|
||||
}
|
||||
}
|
||||
if (depth != 0) return v;
|
||||
|
||||
v.type = JSON_OBJECT;
|
||||
v.valid = true;
|
||||
v.obj_start = content;
|
||||
v.obj_len = (size_t)(p - content);
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 在扁平 JSON 对象中查找指定 key 的值。
|
||||
* @return valid=true 表示找到;字符串值经就地 NUL 终止后通过 .str 返回
|
||||
*/
|
||||
static inline JsonView jsonGet(const JsonView *obj, const char *key)
|
||||
{
|
||||
JsonView result = {};
|
||||
if (!obj || !obj->valid || obj->type != JSON_OBJECT || !key) return result;
|
||||
|
||||
char *p = obj->obj_start;
|
||||
char *end = obj->obj_start + obj->obj_len;
|
||||
|
||||
while (p < end) {
|
||||
while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',')) p++;
|
||||
if (p >= end) break;
|
||||
|
||||
/* "key" */
|
||||
if (*p != '"') break;
|
||||
p++;
|
||||
char *key_start = p;
|
||||
while (p < end && *p != '"') p++;
|
||||
if (p >= end) break;
|
||||
size_t klen = (size_t)(p - key_start);
|
||||
p++;
|
||||
|
||||
while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
|
||||
if (p >= end || *p != ':') break;
|
||||
p++;
|
||||
while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) p++;
|
||||
if (p >= end) break;
|
||||
|
||||
bool match = (strlen(key) == klen && strncmp(key_start, key, klen) == 0);
|
||||
|
||||
if (*p == '"') {
|
||||
p++;
|
||||
char *val_start = p;
|
||||
while (p < end && *p != '"') {
|
||||
if (*p == '\\' && p + 1 < end) p++;
|
||||
p++;
|
||||
}
|
||||
if (p >= end) break;
|
||||
if (match) {
|
||||
/* 不就地写 NUL(会破坏后续 jsonGet 扫描);用 str_len 返回长度 */
|
||||
result.type = JSON_STRING;
|
||||
result.valid = true;
|
||||
result.str = val_start;
|
||||
result.str_len = (size_t)(p - val_start);
|
||||
return result;
|
||||
}
|
||||
p++;
|
||||
} else if (*p == 't' || *p == 'f') {
|
||||
if (match) {
|
||||
result.type = JSON_BOOL;
|
||||
result.valid = true;
|
||||
result.num = (*p == 't') ? 1.0 : 0.0;
|
||||
return result;
|
||||
}
|
||||
while (p < end && *p != ',' && *p != '}' && *p != ' ' && *p != '\n' && *p != '\r') p++;
|
||||
} else if (*p == 'n') {
|
||||
if (match) {
|
||||
result.type = JSON_NULL;
|
||||
result.valid = true;
|
||||
return result;
|
||||
}
|
||||
while (p < end && *p != ',' && *p != '}' && *p != ' ' && *p != '\n' && *p != '\r') p++;
|
||||
} else {
|
||||
char *val_start = p;
|
||||
if (*p == '-') p++;
|
||||
while (p < end && ((*p >= '0' && *p <= '9') || *p == '.' || *p == 'e' || *p == 'E' || *p == '+' || *p == '-')) p++;
|
||||
if (match) {
|
||||
/* 不就地写 NUL;strtod 可在非 NUL 终止串上工作(指定长度)。
|
||||
* 但 strtod 需要终止符——用临时方式:记录起止,用 sscanf 或手动解析。
|
||||
* 更简方案:buf 本身是 NUL 终止的(jsonParse 要求 NUL 终止输入),
|
||||
* 而 *p 此处是数字后的分隔符(',' 或 '}' 或空白),不是 NUL。
|
||||
* 我们临时保存 *p、写 NUL、解析、恢复 *p。 */
|
||||
char saved = *p;
|
||||
*p = '\0';
|
||||
result.type = JSON_NUMBER;
|
||||
result.valid = true;
|
||||
result.num = strtod(val_start, nullptr);
|
||||
*p = saved; /* 恢复,不影响后续 jsonGet 扫描 */
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/* ── 便捷谓词(兼容 cJSON 调用风格)── */
|
||||
|
||||
static inline bool jsonIsString(const JsonView *item) {
|
||||
return item && item->valid && item->type == JSON_STRING;
|
||||
}
|
||||
static inline bool jsonIsNumber(const JsonView *item) {
|
||||
return item && item->valid && item->type == JSON_NUMBER;
|
||||
}
|
||||
static inline bool jsonIsBool(const JsonView *item) {
|
||||
return item && item->valid && item->type == JSON_BOOL;
|
||||
}
|
||||
static inline bool jsonIsTrue(const JsonView *item) {
|
||||
return item && item->valid && item->type == JSON_BOOL && item->num != 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 获取字符串值指针和长度(字符串不一定 NUL 终止,须配合 str_len 使用)。
|
||||
* @param item JSON 值视图
|
||||
* @param out_len 可选,输出字符串长度
|
||||
* @return 字符串指针(指向 buf 内部),或 nullptr
|
||||
*/
|
||||
static inline const char *jsonGetStringValue(const JsonView *item, size_t *out_len = nullptr) {
|
||||
if (item && item->valid && item->type == JSON_STRING) {
|
||||
if (out_len) *out_len = item->str_len;
|
||||
return item->str;
|
||||
}
|
||||
if (out_len) *out_len = 0;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 比较字符串值是否等于指定 C 字符串。
|
||||
* 因 jsonGet 不写 NUL 终止符,不能用 strcmp,须用此函数。
|
||||
*/
|
||||
static inline bool jsonStringEquals(const JsonView *item, const char *cstr) {
|
||||
if (!jsonIsString(item) || !cstr) return false;
|
||||
size_t clen = strlen(cstr);
|
||||
return clen == item->str_len && strncmp(item->str, cstr, clen) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将字符串值拷贝到 NUL 终止的输出缓冲(安全截断)。
|
||||
* @return 实际拷贝长度(不含 NUL)
|
||||
*/
|
||||
static inline size_t jsonGetStringCopy(const JsonView *item, char *out, size_t out_max) {
|
||||
if (!jsonIsString(item) || !out || out_max == 0) return 0;
|
||||
size_t n = (item->str_len < out_max - 1) ? item->str_len : out_max - 1;
|
||||
memcpy(out, item->str, n);
|
||||
out[n] = '\0';
|
||||
return n;
|
||||
}
|
||||
|
||||
static inline double jsonGetNumberValue(const JsonView *item) {
|
||||
return (item && item->valid && item->type == JSON_NUMBER) ? item->num : 0.0;
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // JSON_PARSE_H
|
||||
@@ -299,7 +299,10 @@ esp_err_t WifiOta::initSta()
|
||||
country.policy = WIFI_COUNTRY_POLICY_MANUAL;
|
||||
|
||||
wifi_config_t wifi_config = {};
|
||||
wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
|
||||
/* FAST_SCAN:扫到目标 SSID 即停并连接(多 AP 同名时取先扫到的,可能非最强)。
|
||||
* 原 ALL_CHANNEL_SCAN 要把 13 信道全扫完才连,OTA 重启后页面恢复要多等 3~8s。
|
||||
* esp_restart 不清 nvs.net80211 缓存,驱动可复用上次 AP 信息,恢复更快。 */
|
||||
wifi_config.sta.scan_method = WIFI_FAST_SCAN;
|
||||
wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL;
|
||||
wifi_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
|
||||
wifi_config.sta.pmf_cfg.capable = true;
|
||||
@@ -822,6 +825,12 @@ void WifiOta::taskEntry(void *pvParameters)
|
||||
/* 启动日志 burst 已过:环 64→16 槽,释放大约 7.5KB 给业务堆 */
|
||||
DebugLog::instance().shrinkRingAfterBoot();
|
||||
|
||||
/* ★ 重启完成判定信号(前端 polling.js 匹配 /OTA Relay .*启动/ + tag=MAIN):
|
||||
* 必须在本函数(httpd 就绪、shrinkRingAfterBoot 之后)打印——main.cpp 的
|
||||
* 早期 banner 在环缩容时会被裁掉,前端首轮 /logs 拉不到,只能干等 10s
|
||||
* 安全阈值才恢复页面。此处是环内最新日志,前端一定能拉到。 */
|
||||
DEBUG_INFO("MAIN", "ESP32-C3 OTA Relay v%s 启动", FW_VERSION);
|
||||
|
||||
// WiFi 状态监控
|
||||
// 每秒一次循环:每 10 秒检查 RSSI
|
||||
uint32_t tick = 0;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "mbedtls/private/sha256.h"
|
||||
|
||||
#include "gd32_flash_store.h"
|
||||
#include "app_config.h" // OTA_BLOCK_SIZE(flashWriteChunk 静态对齐缓冲容量)
|
||||
#include "monitor/debug_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
@@ -147,6 +148,54 @@ esp_err_t Gd32FlashStore::ensureErasedForWrite(uint32_t offset, size_t len)
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// OTA 期对齐缓冲池(2026-08 内存优化)
|
||||
//
|
||||
// flashWriteChunk 在非 4 字节对齐写入时需要临时缓冲读回 padding。
|
||||
// 原实现每次 malloc/free,OTA 收流时与非对齐块叠加产生堆抖动。
|
||||
// 现改为 OTA begin 时一次性 calloc,end/abort 时 free,消除抖动
|
||||
// 又不增加常驻 BSS(非 OTA 期零占用)。
|
||||
// ============================================================
|
||||
static uint8_t *s_align_buf = nullptr;
|
||||
static constexpr size_t kAlignBufCap = OTA_BLOCK_SIZE + 8;
|
||||
|
||||
void Gd32FlashStore::acquireAlignBufForOta()
|
||||
{
|
||||
if (!s_align_buf) {
|
||||
s_align_buf = (uint8_t *)calloc(1, kAlignBufCap);
|
||||
if (!s_align_buf) {
|
||||
DEBUG_WARN("OTA", "对齐缓冲分配失败,回退 malloc free=%u",
|
||||
(unsigned)esp_get_free_heap_size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Gd32FlashStore::releaseAlignBufForOta()
|
||||
{
|
||||
if (s_align_buf) {
|
||||
free(s_align_buf);
|
||||
s_align_buf = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief 取对齐缓冲。优先用 OTA 期预分配;未分配或太小则临时 malloc。 */
|
||||
static uint8_t *acquireAlignBuf(size_t needed)
|
||||
{
|
||||
if (s_align_buf && needed <= kAlignBufCap) {
|
||||
return s_align_buf;
|
||||
}
|
||||
// OTA 未预分配(begin 失败)或包过大:回退临时 malloc
|
||||
return (uint8_t *)malloc(needed);
|
||||
}
|
||||
|
||||
/** @brief 归还对齐缓冲。OTA 池的不释放;临时 malloc 的 free。 */
|
||||
static void releaseAlignBuf(uint8_t *buf)
|
||||
{
|
||||
if (buf && buf != s_align_buf) {
|
||||
free(buf);
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief 向 gd32_ota 分区写入数据块,自动处理非 4 字节对齐的读写。 */
|
||||
esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data, size_t len)
|
||||
{
|
||||
@@ -188,9 +237,12 @@ esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data,
|
||||
offset, len, head_pad);
|
||||
}
|
||||
|
||||
uint8_t *aligned_buf = (uint8_t *)malloc(aligned_len);
|
||||
/* OTA 期按需对齐缓冲(OTA begin 时 calloc,end/abort 时 free):
|
||||
* 消除每非对齐块的 malloc/free 抖动,又不增加常驻 BSS。
|
||||
* 容量 = OTA_BLOCK_SIZE(4KB) + 最多 3 字节 tail pad。 */
|
||||
uint8_t *aligned_buf = acquireAlignBuf(aligned_len);
|
||||
if (!aligned_buf) {
|
||||
DEBUG_ERR("OTA", "flashWriteChunk malloc失败: %zu字节", aligned_len);
|
||||
DEBUG_ERR("OTA", "flashWriteChunk 对齐缓冲不足/未分配: %zu", aligned_len);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
@@ -200,7 +252,7 @@ esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data,
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "flashWriteChunk读回head失败: offset=%" PRIu32 " head=%zu err=%s",
|
||||
aligned_offset, head_pad, esp_err_to_name(err));
|
||||
free(aligned_buf);
|
||||
releaseAlignBuf(aligned_buf);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
@@ -215,13 +267,13 @@ esp_err_t Gd32FlashStore::flashWriteChunk(uint32_t offset, const uint8_t *data,
|
||||
if (err != ESP_OK) {
|
||||
DEBUG_ERR("OTA", "flashWriteChunk读回tail失败: offset=%" PRIu32 " tail=%zu err=%s",
|
||||
aligned_offset + total, tail_pad, esp_err_to_name(err));
|
||||
free(aligned_buf);
|
||||
releaseAlignBuf(aligned_buf);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t err = esp_partition_write(part, aligned_offset, aligned_buf, aligned_len);
|
||||
free(aligned_buf);
|
||||
releaseAlignBuf(aligned_buf);
|
||||
return err;
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,12 @@ public:
|
||||
*/
|
||||
const esp_partition_t* getPartition();
|
||||
|
||||
// ── OTA 期对齐缓冲生命周期 ──
|
||||
/** @brief GD32 OTA begin 时调用:预分配对齐缓冲(消除 flashWriteChunk 逐块 malloc)。 */
|
||||
static void acquireAlignBufForOta();
|
||||
/** @brief GD32 OTA end/abort 时调用:释放对齐缓冲(非 OTA 期零占用)。 */
|
||||
static void releaseAlignBufForOta();
|
||||
|
||||
private:
|
||||
Gd32FlashStore();
|
||||
~Gd32FlashStore() = default;
|
||||
|
||||
@@ -61,11 +61,13 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
|
||||
uint8_t resp_type = 0U;
|
||||
size_t resp_len = 0U;
|
||||
esp_err_t ret = ESP_OK;
|
||||
/* CAN 发送只会收到 ACK(0x80) 或 ERROR(0x82)。若读到 WAIT(0x81),必是上一轮电机 OTA
|
||||
/* 0xC0 发送只会收到 ACK(0x80) 或 ERROR(0x82)。若读到 WAIT(0x81),必是上一轮电机 OTA
|
||||
* 喂数(0xD1→WAIT)残留在 SPI 响应管道里的陈旧帧(0xC0 命令本身永不产生 WAIT)。
|
||||
* 陈旧 WAIT 会被下方误判为 ERROR → resp_len=0 → gd32_err=0xFF → 无声拒绝发送(
|
||||
* 表现为“失败一次就再也发不出去”)。此处最多重试几次排空陈旧帧,
|
||||
* 直到读到本命令真正的 ACK/ERROR。 */
|
||||
* 表现为“失败一次就再也发不出去”)。此处最多重试 4 次排空陈旧帧,
|
||||
* 直到读到本命令真正的 ACK/ERROR。
|
||||
* SPI 传输失败/无响应的重发已由 SpiMaster::otaSendAndPoll 统一处理
|
||||
* (同 TX_SEQ,最多 3 次尝试),此处不再各自重试;GD32 拒绝(ERROR)同样不重试。 */
|
||||
for (uint8_t attempt = 0U; attempt < 4U; ++attempt) {
|
||||
resp_type = 0U;
|
||||
resp_len = 0U;
|
||||
@@ -73,8 +75,8 @@ static int sendCanFrameOverSpi(can_channel_t channel, uint32_t ext_id,
|
||||
kSpiCanFrameType,
|
||||
tx_buf, tx_len,
|
||||
&resp_type, spi_rx, spi_rx_max, &resp_len, timeout_ms);
|
||||
if (ret != ESP_OK || resp_type != SPI_OTA_RSP_WAIT) {
|
||||
break; /* SPI 失败或已拿到真正响应(ACK/ERROR) */
|
||||
if (resp_type != SPI_OTA_RSP_WAIT) {
|
||||
break; /* 已拿到真正响应(ACK/ERROR);SPI 失败(ret!=OK)也退出交上层处理 */
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(3)); /* 让 GD32 更新响应,再读一次排空陈旧 WAIT */
|
||||
}
|
||||
@@ -241,8 +243,10 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
|
||||
const uint8_t dataLen = (e.dlc < CANFD_ID_SNAPSHOT_DATA_MAX)
|
||||
? e.dlc : static_cast<uint8_t>(CANFD_ID_SNAPSHOT_DATA_MAX);
|
||||
const uint16_t entrySize = static_cast<uint16_t>(8U + dataLen);
|
||||
if (offset + entrySize > len) {
|
||||
// 线协议条目实际大小按原始 dlc 计算(CANFD 帧可能 64B),offset 必须按
|
||||
// 实际条目推进,否则后续条目错位。data 拷贝按 dataLen 截断。
|
||||
const uint16_t wireEntrySize = static_cast<uint16_t>(8U + e.dlc);
|
||||
if (offset + wireEntrySize > len) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -251,7 +255,7 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
e.data[d] = 0U;
|
||||
}
|
||||
|
||||
offset += entrySize;
|
||||
offset += wireEntrySize;
|
||||
(*entryIdx)++;
|
||||
}
|
||||
|
||||
@@ -264,6 +268,55 @@ int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── 0xC1 请求/响应查询(2026-08 架构改造:替代被动推送缓存)── */
|
||||
|
||||
int canFdQueryFeedback(uint8_t channel, uint8_t page,
|
||||
can_feedback_result_t *out, uint8_t *entryIdx)
|
||||
{
|
||||
if (out == nullptr || entryIdx == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
if (channel >= CAN_CHANNEL_MAX || page > 1U) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
uint8_t req[2] = { channel, page };
|
||||
uint8_t resp_type = 0;
|
||||
/* SPI v2 payload 上限 236B(256 帧 - 20 开销),与 GD32 kFeedbackPayloadLen 一致 */
|
||||
uint8_t resp_buf[SPI_OTA_FRAME_MAX];
|
||||
size_t resp_len = 0;
|
||||
|
||||
esp_err_t ret = SpiMaster::instance().transact(
|
||||
0xC1, req, sizeof(req),
|
||||
&resp_type, resp_buf, sizeof(resp_buf), &resp_len, 200U);
|
||||
|
||||
if (ret != ESP_OK) {
|
||||
DEBUG_WARN("CAN", "queryFeedback ch=%u p=%u SPI失败: %s",
|
||||
(unsigned)channel, (unsigned)page, esp_err_to_name(ret));
|
||||
return -1;
|
||||
}
|
||||
if (resp_type == SPI_OTA_RSP_ERROR) {
|
||||
uint8_t gd32_err = (resp_len > 0) ? resp_buf[0] : 0;
|
||||
DEBUG_WARN("CAN", "queryFeedback ch=%u p=%u GD32拒绝 err=0x%02X",
|
||||
(unsigned)channel, (unsigned)page, (unsigned)gd32_err);
|
||||
return -2;
|
||||
}
|
||||
if (resp_type != SPI_OTA_RSP_ACK || resp_len < 1U) {
|
||||
DEBUG_WARN("CAN", "queryFeedback ch=%u p=%u 非ACK type=0x%02X len=%zu",
|
||||
(unsigned)channel, (unsigned)page, (unsigned)resp_type, resp_len);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 解析复用现有 canFdParseFeedbackPage(payload 格式与推送一致) */
|
||||
int parse_rc = canFdParseFeedbackPage(resp_buf, resp_len, out, entryIdx);
|
||||
if (parse_rc != 0) {
|
||||
DEBUG_WARN("CAN", "queryFeedback ch=%u p=%u 解析失败 rc=%d len=%zu",
|
||||
(unsigned)channel, (unsigned)page, parse_rc, resp_len);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 切换 GD32 三路 CAN 的接收过滤模式(SPI 0xC2)。
|
||||
* @return 0 成功;-1 参数非法或 SPI/应答失败。
|
||||
|
||||
@@ -57,7 +57,7 @@ int canFdSend(can_channel_t channel, uint32_t ext_id,
|
||||
// 不重复 ID 快照表(SPI 0xC3 协议配套)
|
||||
// ============================================================
|
||||
#define CANFD_ID_SNAPSHOT_MAX 16 ///< 每通道最大不重复 ID 条目数
|
||||
#define CANFD_ID_SNAPSHOT_DATA_MAX 64 ///< 快照条目最大数据字节数
|
||||
#define CANFD_ID_SNAPSHOT_DATA_MAX 64 ///< 快照条目最大数据字节数(CANFD 最大 64B,保持完整不截断)
|
||||
|
||||
/**
|
||||
* @brief 单个不重复 ID 的最新帧快照条目。
|
||||
@@ -131,6 +131,22 @@ typedef struct {
|
||||
int canFdParseFeedbackPage(const uint8_t *payload, size_t len,
|
||||
can_feedback_result_t *out, uint8_t *entryIdx);
|
||||
|
||||
/**
|
||||
* @brief 主动向 GD32 查询一页 CAN 反馈(SPI 0xC1 请求/响应)。
|
||||
*
|
||||
* 2026-08 架构改造:ESP32 不再被动缓存 GD32 推送,改为前端请求时按需查询。
|
||||
* GD32 handleFeedback 接收 [channel, page] 返回 ACK + 页数据(与推送 payload 格式一致)。
|
||||
* 查询成功后调 canFdParseFeedbackPage 解析,复用现有累积逻辑。
|
||||
*
|
||||
* @param channel 目标通道 0/1/2。
|
||||
* @param page 页号 0 或 1(page 0 重置累积 + 填统计,page 1 追加 entries)。
|
||||
* @param out 累积结果(跨 page 0/1 累积,调用方按通道维护)。
|
||||
* @param entryIdx 当前已填充条目数(page 0 时被重置为 0)。
|
||||
* @return 0 成功;-1 参数/SPI 失败;-2 GD32 返回 ERROR。
|
||||
*/
|
||||
int canFdQueryFeedback(uint8_t channel, uint8_t page,
|
||||
can_feedback_result_t *out, uint8_t *entryIdx);
|
||||
|
||||
/**
|
||||
* @brief 切换 GD32 三路 CAN 的接收过滤模式(SPI 0xC2)。
|
||||
* @param mode 目标模式(0=全收,1=按 ID 过滤,2=CAN0 桥接到 1/2)。
|
||||
|
||||
+176
-42
@@ -21,6 +21,7 @@
|
||||
#include "esp_log.h"
|
||||
#include "esp_random.h" // esp_random() for HELLO session_id
|
||||
#include "esp_intr_alloc.h" // ESP_INTR_FLAG_IRAM
|
||||
#include "esp_timer.h" // esp_timer_get_time():worker job 执行耗时采样
|
||||
#include "spi_master.h"
|
||||
#include "spi_link_v2.h"
|
||||
#include "app_config.h"
|
||||
@@ -67,8 +68,27 @@ static size_t spiRespReadBudget(void)
|
||||
return SPI_APP_TX_BUF_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 已知"短响应"的主动请求类型:应答帧 ≤ ~70B(实测 0xA1 PING ~60-70B、
|
||||
* DATA/REQ/FINISH ACK ~30-40B、0xB5/0xC0/0xC2/0xD1 ACK 均短)。
|
||||
* 对这些类型的主动 poll 可把读预算从 256 收紧到 96B,省 ~160B 0xFF 填充时钟
|
||||
* (@13.3MHz ≈ 96µs/帧)。排除 0xC3(ID 快照响应可能很长)与被动 0xC1 事件读。
|
||||
*/
|
||||
static bool isShortRespType(uint8_t type)
|
||||
{
|
||||
switch (type) {
|
||||
case 0xA1U: case 0x02U: case 0x01U: case 0xF7U:
|
||||
case 0xB0U: case 0xC0U: case 0xC2U: case 0xD1U:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/** 短响应读预算(覆盖最长 ~70B 应答 + 余量)。 */
|
||||
static constexpr size_t kShortRespReadBudget = 96U;
|
||||
|
||||
/** APP 独有业务 TYPE:见到即可纠正 m_gd32Role(BOOT 不会发这些)。 */
|
||||
static bool isAppOnlySpiType(uint8_t type)
|
||||
bool SpiMaster::isAppOnlySpiType(uint8_t type)
|
||||
{
|
||||
switch (type) {
|
||||
case 0xC1U: /* CAN feedback */
|
||||
@@ -85,6 +105,34 @@ static bool isAppOnlySpiType(uint8_t type)
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 去重辅助实现 ── */
|
||||
|
||||
void SpiMaster::copyFoundFrame(uint8_t *resp_type, uint8_t found_type,
|
||||
uint8_t *resp_payload, size_t resp_max,
|
||||
const uint8_t *found_payload, size_t found_len,
|
||||
size_t *resp_len)
|
||||
{
|
||||
if (resp_type) *resp_type = found_type;
|
||||
if (resp_payload && resp_max > 0) {
|
||||
size_t copy_len = (found_len < resp_max) ? found_len : resp_max;
|
||||
if (found_payload && copy_len > 0) {
|
||||
memcpy(resp_payload, found_payload, copy_len);
|
||||
} else if (copy_len > 0) {
|
||||
memset(resp_payload, 0, copy_len);
|
||||
}
|
||||
}
|
||||
if (resp_len) *resp_len = found_len;
|
||||
}
|
||||
|
||||
void SpiMaster::correctRoleFromType(uint8_t found_type, const char *src)
|
||||
{
|
||||
if (isAppOnlySpiType(found_type) && m_gd32Role != V2_ROLE_APP) {
|
||||
DEBUG_INFO("SPI", "role %u→APP (saw type=0x%02X, %s)",
|
||||
(unsigned)m_gd32Role, found_type, src);
|
||||
m_gd32Role = V2_ROLE_APP;
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief ISR-safe GPIO 电平读取(不调用 Flash 中的 gpio_get_level)。
|
||||
* 用于 SPIready 边沿中断,避免在 cache 被禁用期间访问 Flash 代码。
|
||||
*/
|
||||
@@ -189,18 +237,52 @@ bool SpiMaster::popEvent(uint8_t *type, uint8_t *payload, size_t max_len, size_t
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 暂停/恢复对 GD32 主动按铃的 serviceRing 处理。
|
||||
* @param suppress true=OTA 等关键路径禁止偷读 PB6;false=恢复
|
||||
* @brief 暂停/恢复对 GD32 主动按铃的 serviceRing 处理(引用计数)。
|
||||
* @param suppress true=+1(OTA 等关键路径禁止偷读 PB6);false=-1(钳位到 0)。
|
||||
*
|
||||
* GD32 中继(forwardFirmware)与电机 OTA feedTask 都可能持有抑制;任一
|
||||
* 持有者退出时若直接清位,会把另一方的保护误清(电机 OTA 收尾曾把进行中
|
||||
* 的 GD32 中继抑制清掉 → worker 偷读 OTA ACK/WAIT → poll 空等 6s)。
|
||||
*/
|
||||
void SpiMaster::setSuppressRingService(bool suppress)
|
||||
{
|
||||
m_suppressRingService = suppress;
|
||||
taskENTER_CRITICAL(&s_spiJobMux);
|
||||
uint32_t refs = m_ringSuppressRefs;
|
||||
if (suppress) {
|
||||
++refs;
|
||||
} else if (refs > 0U) {
|
||||
--refs;
|
||||
}
|
||||
m_ringSuppressRefs = refs;
|
||||
const bool now_suppressed = (refs > 0U);
|
||||
taskEXIT_CRITICAL(&s_spiJobMux);
|
||||
|
||||
m_suppressRingService = now_suppressed;
|
||||
if (now_suppressed) {
|
||||
/* 丢弃已锁存的按铃标志,避免解除后立刻误读残留边沿 */
|
||||
m_gd32RequestPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief ESP32 自我 OTA 期间暂停/恢复全部 SPI 活动。
|
||||
*
|
||||
* 暂停:worker 空转不读 SPI(GD32 按铃丢弃),submitJob 快速拒绝
|
||||
* (业务调用立即返回 ESP_ERR_INVALID_STATE,不排队不阻塞)。
|
||||
* 恢复:唤醒 worker 恢复正常。幂等,可多次调用。
|
||||
*/
|
||||
void SpiMaster::setSpiPaused(bool paused)
|
||||
{
|
||||
m_spiPaused = paused;
|
||||
if (paused) {
|
||||
/* 丢弃已锁存的按铃标志:暂停期间不读 GD32,解除后勿误读残留边沿 */
|
||||
m_gd32RequestPending = false;
|
||||
}
|
||||
if (m_workerTask != nullptr) {
|
||||
xTaskNotify(m_workerTask, 0x10U, eSetBits); /* 唤醒 worker 处理状态变化 */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 业务侧确认本端 TX 序号已被对端接受,推进 m_txSeq。
|
||||
* @note 在收到明确业务 ACK 后调用,避免重发与序号失配。
|
||||
@@ -278,11 +360,7 @@ esp_err_t SpiMaster::serviceRingOnce()
|
||||
{
|
||||
LockGuard lock(m_mutex);
|
||||
/* APP 独有事件 → 纠正粘滞的 boot role(OTA 后 HELLO 未刷新时常见) */
|
||||
if (isAppOnlySpiType(found_type) && m_gd32Role != V2_ROLE_APP) {
|
||||
DEBUG_INFO("SPI", "role %u→APP (saw type=0x%02X)",
|
||||
(unsigned)m_gd32Role, found_type);
|
||||
m_gd32Role = V2_ROLE_APP;
|
||||
}
|
||||
correctRoleFromType(found_type, "ring");
|
||||
(void)pushEvent(found_type, found_payload, found_len);
|
||||
}
|
||||
return ESP_OK;
|
||||
@@ -428,6 +506,10 @@ void SpiMaster::finishPoolJob(Job *job)
|
||||
*/
|
||||
esp_err_t SpiMaster::submitJob(Job &job)
|
||||
{
|
||||
/* ESP32 自我 OTA 暂停:快速拒绝,不排队不等待(worker 空转中)。 */
|
||||
if (m_spiPaused) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (m_jobQueue == nullptr || m_workerTask == nullptr || onWorker()) {
|
||||
executeJob(job);
|
||||
return job.result;
|
||||
@@ -529,6 +611,15 @@ void SpiMaster::workerLoop()
|
||||
DEBUG_INFO("SPI", "SPI worker started, handle=%p", (void *)m_workerTask);
|
||||
|
||||
for (;;) {
|
||||
/* ESP32 自我 OTA 暂停:空转不读 SPI(flash 写期间 cache 失效会拖慢
|
||||
* flash 驻留代码,且此时 GD32 数据无意义)。恢复由 setSpiPaused(false)
|
||||
* 通知唤醒。 */
|
||||
if (m_spiPaused) {
|
||||
m_gd32RequestPending = false;
|
||||
uint32_t bits = 0U;
|
||||
(void)xTaskNotifyWait(0U, 0xFFFFFFFFU, &bits, pdMS_TO_TICKS(50));
|
||||
continue;
|
||||
}
|
||||
/* ★ 只在 ISR 置位 m_gd32RequestPending 时读 EVENT。
|
||||
* 禁止见 DATA_READY 低就 serviceRing:命令 HELLO_ACK/OTA 响应也会拉低
|
||||
* PB6,会被偷走当事件 → 业务 poll 全 0xFF,OTA/ENTER_BOOT 全挂。
|
||||
@@ -541,7 +632,16 @@ void SpiMaster::workerLoop()
|
||||
|
||||
Job *job = nullptr;
|
||||
if (xQueueReceive(m_jobQueue, &job, pdMS_TO_TICKS(1)) == pdTRUE && job != nullptr) {
|
||||
/* 计时 worker 实际执行段(取帧→发→读响应→完成):
|
||||
* 测速页 RTT − 本值 = 提交/调度/唤醒开销,用于区分
|
||||
* "ESP32 侧任务抢占" vs "GD32 应答慢" 的抖动归属。 */
|
||||
const int64_t w0 = esp_timer_get_time();
|
||||
m_lastProfile.t_w0 = (uint64_t)w0;
|
||||
executeJob(*job);
|
||||
const int64_t w1 = esp_timer_get_time();
|
||||
m_lastProfile.t_w1 = (uint64_t)w1;
|
||||
const int64_t wus = w1 - w0;
|
||||
m_lastWorkerExecUs = (wus > 0) ? (uint32_t)wus : 0U;
|
||||
finishPoolJob(job);
|
||||
continue;
|
||||
}
|
||||
@@ -698,7 +798,9 @@ esp_err_t SpiMaster::transferClockedFlags(const uint8_t *tx_buf, size_t tx_len,
|
||||
t.tx_buffer = tx_ptr;
|
||||
t.rx_buffer = (rx_len > 0U) ? rx_ptr : NULL;
|
||||
|
||||
/* SPI 事务等待使用有界超时 */
|
||||
/* 中断模式(queue+result 信号量):2026-08-02 实测 polling 模式在 C3 单核上
|
||||
* 整体更慢——传输段虽略快(-14~-17µs),但 worker 忙等占死 CPU,提交/调度
|
||||
* 13→140µs、唤醒 18→76µs、2-5ms 尾巴 23→129 帧,RTT 658→822µs。回退。 */
|
||||
const uint32_t effective_ms = (timeout_ms == 0U) ? 500U
|
||||
: (timeout_ms < 50U ? 50U : timeout_ms);
|
||||
const TickType_t queue_wait = pdMS_TO_TICKS(effective_ms);
|
||||
@@ -755,7 +857,7 @@ esp_err_t SpiMaster::transferClockedFlags(const uint8_t *tx_buf, size_t tx_len,
|
||||
* 时钟预算见 spiRespReadBudget():BOOT=128,APP=256。
|
||||
*/
|
||||
esp_err_t SpiMaster::readV2FrameVarlen(uint8_t *out_frame, size_t out_max, size_t *out_len,
|
||||
uint32_t timeout_ms)
|
||||
uint32_t timeout_ms, size_t budget_hint)
|
||||
{
|
||||
if (out_frame == nullptr || out_len == nullptr || out_max < V2_HEADER_LEN) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
@@ -767,6 +869,9 @@ esp_err_t SpiMaster::readV2FrameVarlen(uint8_t *out_frame, size_t out_max, size_
|
||||
*out_len = 0U;
|
||||
|
||||
size_t budget = spiRespReadBudget();
|
||||
if (budget_hint > 0U && budget_hint < budget) {
|
||||
budget = budget_hint; /* 主动 poll 按请求类型收紧,省 0xFF 填充时钟 */
|
||||
}
|
||||
if (budget > out_max) {
|
||||
budget = out_max;
|
||||
}
|
||||
@@ -1069,7 +1174,11 @@ esp_err_t SpiMaster::init()
|
||||
}
|
||||
}
|
||||
if (m_workerTask == nullptr) {
|
||||
BaseType_t ok = xTaskCreate(workerEntry, "spi_worker", TASK_STACK_SPI_WORKER, this, 6, &m_workerTask);
|
||||
/* 优先级 8(2026-08-02 测速实验):原 6 会被 ota_feed(7) 抢占,
|
||||
* SPI 完成 ISR 唤醒 worker 后还要等 feed 让出,拖长 RTT 唤醒链。
|
||||
* 8 仍低于 health_mon(21)/ota_relay(23);worker 阻塞循环为主,
|
||||
* 仅 job 执行时运行,不会饿死 ota_feed/httpd。 */
|
||||
BaseType_t ok = xTaskCreate(workerEntry, "spi_worker", TASK_STACK_SPI_WORKER, this, 8, &m_workerTask);
|
||||
if (ok != pdPASS || m_workerTask == nullptr) {
|
||||
DEBUG_ERR("SPI", "SPI worker task create failed");
|
||||
m_workerTask = nullptr;
|
||||
@@ -1124,6 +1233,8 @@ esp_err_t SpiMaster::otaTransferUnlocked(uint8_t type, const uint8_t *payload, s
|
||||
if (!otaBuildFrame(type, payload, payload_len, tx_frame, &tx_len)) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
/* 记录最近一次主动请求类型:poll 读预算按它收紧(isShortRespType)。 */
|
||||
m_lastReqType = type;
|
||||
|
||||
DEBUG_DBG("SPI", "OTA TX: type=0x%02X len=%u", type, (unsigned)tx_len);
|
||||
|
||||
@@ -1194,17 +1305,8 @@ esp_err_t SpiMaster::otaTransferUnlocked(uint8_t type, const uint8_t *payload, s
|
||||
/* 与 Git 稳定路径一致:step1 陈旧帧原样解析即可(otaSendAndPoll 传 nullptr 丢弃)。
|
||||
* 勿在此处过滤 LINK/EVENT,否则改变 delayed-by-one 语义。 */
|
||||
if (otaFindFrame(rx_raw, xfer_len, &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 = (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;
|
||||
if (isAppOnlySpiType(found_type) && m_gd32Role != V2_ROLE_APP) {
|
||||
m_gd32Role = V2_ROLE_APP;
|
||||
}
|
||||
copyFoundFrame(resp_type, found_type, resp_payload, resp_max, found_payload, found_len, resp_len);
|
||||
correctRoleFromType(found_type, "send");
|
||||
/* expect_stale(两步法 step1)读到的是延迟一拍陈旧帧,序号由 poll 成功时推进。
|
||||
* 此处若自增会导致 job 间隙/超时后 resend 使用错误 TX_SEQ。 */
|
||||
if (!expect_stale) {
|
||||
@@ -1286,7 +1388,12 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p
|
||||
? ((timeout_ms >= 80U) ? 40U : timeout_ms)
|
||||
: 15U;
|
||||
const uint32_t wait_ms = (ready_slice < remain_ms) ? ready_slice : remain_ms;
|
||||
/* 分段剖析:等 READY 段 = GD32 收到帧→拉低 PB6 的处理时间 */
|
||||
m_lastProfile.t_wait0 = (uint64_t)esp_timer_get_time();
|
||||
const bool ready = (waitSpiDataReady(wait_ms) == ESP_OK);
|
||||
if (ready) {
|
||||
m_lastProfile.t_wait1 = (uint64_t)esp_timer_get_time();
|
||||
}
|
||||
|
||||
now = xTaskGetTickCount();
|
||||
remain_ms = (deadline > now) ? ((deadline - now) * portTICK_PERIOD_MS) : 0U;
|
||||
@@ -1303,7 +1410,12 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p
|
||||
|
||||
uint8_t rx_raw[SPI_OTA_FRAME_MAX];
|
||||
size_t rx_len = 0U;
|
||||
esp_err_t ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, spi_timeout);
|
||||
/* 读预算按最近请求类型收紧(短响应类型 96B,其余按角色默认) */
|
||||
const size_t budget_hint = isShortRespType(m_lastReqType) ? kShortRespReadBudget : 0U;
|
||||
esp_err_t ret = readV2FrameVarlen(rx_raw, sizeof(rx_raw), &rx_len, spi_timeout, budget_hint);
|
||||
if (ret == ESP_OK) {
|
||||
m_lastProfile.t_read1 = (uint64_t)esp_timer_get_time();
|
||||
}
|
||||
did_spi_read = true;
|
||||
if (ret != ESP_OK) {
|
||||
if (ret == ESP_ERR_INVALID_RESPONSE) {
|
||||
@@ -1347,11 +1459,7 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p
|
||||
}
|
||||
|
||||
if ((found_flags & V2_FLAG_EVENT) != 0U) {
|
||||
if (isAppOnlySpiType(found_type) && m_gd32Role != V2_ROLE_APP) {
|
||||
DEBUG_INFO("SPI", "role %u→APP (poll saw type=0x%02X)",
|
||||
(unsigned)m_gd32Role, found_type);
|
||||
m_gd32Role = V2_ROLE_APP;
|
||||
}
|
||||
correctRoleFromType(found_type, "poll");
|
||||
DEBUG_DBG("SPI", "poll skip EVENT type=0x%02X", found_type);
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
continue;
|
||||
@@ -1365,16 +1473,7 @@ esp_err_t SpiMaster::otaPollResponseUnlocked(uint8_t *resp_type, uint8_t *resp_p
|
||||
continue;
|
||||
}
|
||||
|
||||
if (resp_type) *resp_type = found_type;
|
||||
if (resp_payload && resp_max > 0) {
|
||||
size_t copy_len = (found_len < resp_max) ? found_len : resp_max;
|
||||
if (found_payload && copy_len > 0) {
|
||||
memcpy(resp_payload, found_payload, copy_len);
|
||||
} else if (copy_len > 0) {
|
||||
memset(resp_payload, 0, copy_len);
|
||||
}
|
||||
}
|
||||
if (resp_len) *resp_len = found_len;
|
||||
copyFoundFrame(resp_type, found_type, resp_payload, resp_max, found_payload, found_len, resp_len);
|
||||
|
||||
/* WAIT/ACK/ERROR:只要 ack_seq 匹配,GD32 已消费本 TX_SEQ,必须推进。
|
||||
* ★ 电机 OTA:feedData 缓冲满回 WAIT 时若不清 seq,ESP32 同 seq 重发
|
||||
@@ -1429,7 +1528,11 @@ esp_err_t SpiMaster::transact(uint8_t type, const uint8_t *payload, size_t paylo
|
||||
job.out_payload = resp_payload;
|
||||
job.out_max = resp_max;
|
||||
job.out_len = resp_len;
|
||||
return submitJob(job);
|
||||
/* 分段剖析:记录调用方侧两个时间戳(worker 侧在 workerLoop/otaSendAndPoll 内写) */
|
||||
m_lastProfile.t_submit0 = (uint64_t)esp_timer_get_time();
|
||||
const esp_err_t p_ret = submitJob(job);
|
||||
m_lastProfile.t_submit1 = (uint64_t)esp_timer_get_time();
|
||||
return p_ret;
|
||||
}
|
||||
return otaSendAndPoll(type, payload, payload_len,
|
||||
resp_type, resp_payload, resp_max, resp_len, timeout_ms);
|
||||
@@ -1512,10 +1615,28 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t
|
||||
poll_timeout = 1U;
|
||||
}
|
||||
|
||||
/* 统一重发机制(嵌入 worker,所有业务共用,调用方无需各自重试):
|
||||
* 仅当"没读到任何有效响应"时同 TX_SEQ 重发(失败路径 seq 不推进):
|
||||
* - 发送级失败(transferClocked 错误/credit 拒绝):帧大概率未送达 GD32;
|
||||
* - poll 超时(无响应):帧可能未送达或响应丢失,重发靠 GD32 DUP-SEQ 去重兜底。
|
||||
* 读到任何帧(WAIT/ACK/ERROR/陈旧)即成功返回,不重发:
|
||||
* - 保证 GD32 明确拒绝(ERROR 0x82)天然不重试(如 CAN 负载高/BUS_OFF);
|
||||
* - OTA relay 的 WAIT 语义、旧帧排空语义均不受影响。 */
|
||||
constexpr uint32_t kTxRetryMax = 3U; /* 总尝试次数(1 次首发 + 2 次重发) */
|
||||
for (uint32_t attempt = 0U; attempt < kTxRetryMax; ++attempt) {
|
||||
m_lastProfile.t_send0 = (uint64_t)esp_timer_get_time();
|
||||
esp_err_t send_ret = otaTransferUnlocked(type, payload, payload_len,
|
||||
nullptr, nullptr, 0U, nullptr, send_timeout,
|
||||
/*expect_stale=*/true);
|
||||
m_lastProfile.t_send1 = (uint64_t)esp_timer_get_time();
|
||||
if (send_ret != ESP_OK && send_ret != ESP_ERR_INVALID_RESPONSE) {
|
||||
if (attempt + 1U < kTxRetryMax) {
|
||||
DEBUG_DBG("SPI", "TX retry(%u/%u) send fail type=0x%02X %s",
|
||||
(unsigned)(attempt + 1U), (unsigned)kTxRetryMax,
|
||||
type, esp_err_to_name(send_ret));
|
||||
vTaskDelay(pdMS_TO_TICKS(3));
|
||||
continue;
|
||||
}
|
||||
return send_ret;
|
||||
}
|
||||
|
||||
@@ -1531,8 +1652,21 @@ esp_err_t SpiMaster::otaSendAndPoll(uint8_t type, const uint8_t *payload, size_t
|
||||
}
|
||||
|
||||
/* 步骤 2: 轮询读取真实响应(变长读,不重发命令) */
|
||||
return otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len, poll_timeout,
|
||||
/*expected_type=*/0xFFU);
|
||||
esp_err_t poll_ret = otaPollResponseUnlocked(resp_type, resp_payload, resp_max, resp_len,
|
||||
poll_timeout, /*expected_type=*/0xFFU);
|
||||
if (poll_ret == ESP_OK) {
|
||||
return ESP_OK; /* 已读到有效响应帧(ACK/ERROR/陈旧) */
|
||||
}
|
||||
if (attempt + 1U < kTxRetryMax) {
|
||||
DEBUG_DBG("SPI", "TX retry(%u/%u) no resp type=0x%02X %s",
|
||||
(unsigned)(attempt + 1U), (unsigned)kTxRetryMax,
|
||||
type, esp_err_to_name(poll_ret));
|
||||
vTaskDelay(pdMS_TO_TICKS(3));
|
||||
continue;
|
||||
}
|
||||
return poll_ret;
|
||||
}
|
||||
return ESP_ERR_TIMEOUT; /* 不可达:循环必有返回 */
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -100,6 +100,31 @@ public:
|
||||
/** @brief SPI worker 任务句柄(栈水位 /system/status 与 health 巡检用)。 */
|
||||
TaskHandle_t getWorkerTaskHandle() const { return m_workerTask; }
|
||||
|
||||
/** @brief 最近一次 worker job 实际执行耗时(µs,worker 内计时)。
|
||||
* 测速页用 RTT − 本值拆出"提交/调度/唤醒"开销,定位抖动归属。 */
|
||||
uint32_t lastWorkerExecUs() const { return m_lastWorkerExecUs; }
|
||||
|
||||
/**
|
||||
* @brief 单次 transact 的分段耗时剖析(测速页诊断用)。
|
||||
* 5 段:提交/调度(t_submit0→t_w0)、发送帧(t_send0→t_send1)、
|
||||
* 等应答(t_wait0→t_wait1)、读响应(t_wait1→t_read1)、唤醒(t_w1→t_submit1)。
|
||||
* 写入顺序由 done 信号量保证 happens-before(worker 先写全再 give,调用方 take 后读)。
|
||||
*/
|
||||
struct SpiProfile {
|
||||
uint64_t t_submit0 = 0; ///< 调用方 transact 入口
|
||||
uint64_t t_w0 = 0; ///< worker executeJob 开始
|
||||
uint64_t t_send0 = 0; ///< 发送帧开始
|
||||
uint64_t t_send1 = 0; ///< 发送帧结束
|
||||
uint64_t t_wait0 = 0; ///< 进入 poll(等 READY)
|
||||
uint64_t t_wait1 = 0; ///< READY 就绪(wait 段结束)
|
||||
uint64_t t_read1 = 0; ///< 响应帧读取完成
|
||||
uint64_t t_w1 = 0; ///< worker executeJob 结束
|
||||
uint64_t t_submit1 = 0; ///< 调用方 submitJob 返回
|
||||
};
|
||||
|
||||
/** @brief 最近一次 transact 的剖析快照(仅测速/诊断用)。 */
|
||||
SpiProfile lastProfile() const { return m_lastProfile; }
|
||||
|
||||
/** @brief SPI 任务队列当前待处理 job 数(固定 Job pool 槽,无堆分配)。
|
||||
* 供 /system/heap_low_log 堆低水位快照采样,定位"低谷是不是 SPI 队列堆积引起"。 */
|
||||
UBaseType_t getPendingJobCount() const { return m_jobQueue ? uxQueueMessagesWaiting(m_jobQueue) : 0; }
|
||||
@@ -107,9 +132,18 @@ public:
|
||||
/**
|
||||
* @brief OTA 中继期间禁止 worker 将 DATA_READY 当 EVENT 读取。
|
||||
* 中继开始设 true,结束/复位设 false。
|
||||
* 实现为引用计数:GD32 中继与电机 OTA feed 可并发持有,任一持有者
|
||||
* 退出都不会提前解除抑制(避免电机 OTA 收尾把 GD32 中继的抑制误清)。
|
||||
*/
|
||||
void setSuppressRingService(bool suppress);
|
||||
|
||||
/**
|
||||
* @brief ESP32 自我 OTA 期间暂停全部 SPI 活动(GD32 按铃读 + 业务 transact)。
|
||||
* 暂停后 worker 空转不读 SPI,submitJob 快速返回 ESP_ERR_INVALID_STATE
|
||||
* (业务立即失败,不阻塞排队);恢复时唤醒 worker 恢复正常。幂等。
|
||||
*/
|
||||
void setSpiPaused(bool paused);
|
||||
|
||||
/**
|
||||
* @brief 业务层确认当前请求已成功时推进/对齐 m_txSeq。
|
||||
*/
|
||||
@@ -282,12 +316,22 @@ private:
|
||||
SpiLinkState m_linkState;
|
||||
/** GD32 主动按铃标志:GPIO 下降沿 ISR 置位,仅 SPI worker 消费。 */
|
||||
volatile bool m_gd32RequestPending = false;
|
||||
/** ring service 抑制引用计数(setSuppressRingService 内部使用,临界区保护)。 */
|
||||
uint32_t m_ringSuppressRefs = 0;
|
||||
/** OTA 中继时为 true:worker 不 serviceRing,避免偷读 OTA ACK/WAIT。 */
|
||||
volatile bool m_suppressRingService = false;
|
||||
/** ESP32 自我 OTA 期间为 true:worker 空转、submitJob 快速拒绝。 */
|
||||
volatile bool m_spiPaused = false;
|
||||
/** 最近一次主动请求类型(otaTransferUnlocked 发送时记录;poll 读预算按它收紧)。 */
|
||||
volatile uint8_t m_lastReqType = 0xFFU;
|
||||
/** SPI worker 任务句柄(ISR 唯一通知目标)。 */
|
||||
TaskHandle_t m_spiTaskHandle = NULL;
|
||||
TaskHandle_t m_workerTask = NULL;
|
||||
QueueHandle_t m_jobQueue = NULL;
|
||||
/** 最近一次 job 执行耗时(µs,worker 写入、业务只读;单核 32 位读原子)。 */
|
||||
uint32_t m_lastWorkerExecUs = 0;
|
||||
/** 最近一次 transact 分段剖析(worker 与调用方交叉写入,done 信号量同步)。 */
|
||||
SpiProfile m_lastProfile;
|
||||
/** 事件计数信号量(worker push / takeEvent pop)。 */
|
||||
SemaphoreHandle_t m_eventSem = NULL;
|
||||
|
||||
@@ -405,6 +449,19 @@ private:
|
||||
/** @brief GPIO 边沿中断回调(注册到 ESP-IDF GPIO ISR 服务)。 */
|
||||
static void IRAM_ATTR onReadyEdgeISR(void *arg);
|
||||
|
||||
/** @brief APP 独有业务 TYPE:见到即可纠正 m_gd32Role(BOOT 不会发这些)。 */
|
||||
static bool isAppOnlySpiType(uint8_t type);
|
||||
|
||||
// ── 去重辅助 ──
|
||||
/** @brief 把 otaFindFrame 找到的帧拷贝到调用方输出缓冲(安全截断)。 */
|
||||
static void copyFoundFrame(uint8_t *resp_type, uint8_t found_type,
|
||||
uint8_t *resp_payload, size_t resp_max,
|
||||
const uint8_t *found_payload, size_t found_len,
|
||||
size_t *resp_len);
|
||||
|
||||
/** @brief 见到 APP 独有 TYPE 时纠正粘滞的 boot role(3 处调用合并)。 */
|
||||
void correctRoleFromType(uint8_t found_type, const char *src);
|
||||
|
||||
// 内部 SPI 传输
|
||||
/**
|
||||
* @brief 执行底层 SPI 时钟同步传输。
|
||||
@@ -434,9 +491,11 @@ private:
|
||||
* @param out_max 输出缓冲容量
|
||||
* @param out_len 实际帧长
|
||||
* @param timeout_ms 单次 SPI 超时
|
||||
* @param budget_hint 读预算提示(0=按角色默认;>0 时取 min(hint, 角色预算),
|
||||
* 主动 poll 按最近请求类型收紧,省 0xFF 填充时钟)
|
||||
*/
|
||||
esp_err_t readV2FrameVarlen(uint8_t *out_frame, size_t out_max, size_t *out_len,
|
||||
uint32_t timeout_ms);
|
||||
uint32_t timeout_ms, size_t budget_hint = 0U);
|
||||
|
||||
// 无锁内部版本(供已持锁的公共方法调用,避免递归死锁)
|
||||
/**
|
||||
|
||||
+4
-4
@@ -12,10 +12,10 @@ CONFIG_DCU_OTA_FINISH_TIMEOUT_MS=10000
|
||||
# 不要再开 QIO,除非确认 flash 型号与硬件连接均兼容。
|
||||
CONFIG_ESPTOOLPY_FLASHMODE_DIO=y
|
||||
|
||||
# DCU SPI clock -- 8 MHz(2026-07-31 提速:事件驱动握手后时钟占周期 ~78%,
|
||||
# 8MHz 实测测速页/relay/PING 零错误后启用;半周期 62.5ns,GD32 从机 MISO
|
||||
# 输出延迟 + ESP32 GPIO 矩阵输入延迟 ~45ns 仍有余量;≥16MHz 全双工不可行)
|
||||
CONFIG_DCU_SPI_CLOCK_HZ=8000000
|
||||
# DCU SPI clock -- 13.3 MHz(2026-08-02:80MHz/6 整除,比 12M 再省 ~8%;
|
||||
# 半周期 37.5ns,MISO 输出延迟 + GPIO 矩阵延迟 ~45ns 预算已近上限,需实测;
|
||||
# ≥16MHz 全双工不可行。写 13333333:若写 13000000 驱动取整 80/7=11.4M 反而更慢)
|
||||
CONFIG_DCU_SPI_CLOCK_HZ=13333333
|
||||
|
||||
# GD32 OTA 包间节奏:不用固定 PACKET_INTERVAL;依赖 SpiMaster CREDIT 背压
|
||||
# (flash 槽满时 CREDIT=0,等 CREDIT_UPDATE / 帧头 credit 恢复后再发)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
@echo off
|
||||
REM Clear MSYS/MINGW env vars to bypass IDF MSys detection when called from Git Bash
|
||||
set MSYSTEM=
|
||||
set MSYSTEM_CHOST=
|
||||
set MSYSTEM_PREFIX=
|
||||
set MINGW_CHOST=
|
||||
set MINGW_PREFIX=
|
||||
set MINGW_PACKAGE_PREFIX=
|
||||
set CHERE_INVOKING=
|
||||
set MSYS2_PATH_TYPE=
|
||||
|
||||
REM Toolchain installed at C:\Espressif (see .vscode/settings.json idf.toolsPath)
|
||||
set IDF_TOOLS_PATH=C:\Espressif
|
||||
|
||||
call "F:\software\.espressif\v6.0\esp-idf\export.bat"
|
||||
if errorlevel 1 (
|
||||
echo EXPORT_FAILED
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
cd /d "G:\Robot_v2\DCU\ESP32_COMM"
|
||||
idf.py build
|
||||
exit /b %errorlevel%
|
||||
@@ -216,11 +216,19 @@ export async function pollStatus() {
|
||||
if (window.isEsp32Rebooting ||
|
||||
window.otaSessionState === window.OtaSessionState.REBOOTING) {
|
||||
var rbElapsed = Math.round((Date.now() - window.rebootStartTime) / 1000);
|
||||
// pre-reboot 窗口:后端发响应后约 1s 才 esp_restart,期间设备仍响应轮询,
|
||||
// 保持"重启中"状态,不清除标志,不推进阶段。
|
||||
// ★ 真正的"重启完成"由 pollLogs 收到启动 banner 触发;
|
||||
// 这里只在已观察到下线(rebootObservedDown)或超 10s 安全阈值时接受恢复。
|
||||
if (window.rebootObservedDown || rbElapsed >= 10) {
|
||||
/* 重启完成的证据(任一即可):
|
||||
* 1) rebootObservedDown:已观察到设备下线后恢复。
|
||||
* 2) freshBoot:设备回报 uptime 很小(<120s,与 resolveEsp32VerifyAmbiguous
|
||||
* 的 freshBoot 阈值一致)。★ 关键:设备掉线仅 3~5s 时(FAST_SCAN 生效后
|
||||
* 常见),在途轮询的 TCP SYN 重传会在设备起来后完成握手、全部成功,
|
||||
* rebootObservedDown 永远不置位,旧逻辑只能干等 10s 兜底——
|
||||
* 实测设备 3s 就绪、页面却 10s 才恢复。uptime 是新固件启动的权威信号。
|
||||
* 3) rbElapsed >= 10:安全兜底(兼容 uptime 缺失 / 设备未真重启)。
|
||||
* pre-restart 窗口(finalize 响应后约 1s 设备仍在跑旧固件)uptime 很大,
|
||||
* 不会误判为 freshBoot。 */
|
||||
var upS = Number(data && data.uptime_sec);
|
||||
var freshBoot = Number.isFinite(upS) && upS >= 0 && upS < 120;
|
||||
if (window.rebootObservedDown || freshBoot || rbElapsed >= 10) {
|
||||
markEsp32RebootComplete();
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -113,15 +113,15 @@
|
||||
function addHistory(frames, payload, text) {
|
||||
/* 从设备中文文本里提取关键数字做历史行 */
|
||||
var fail = (text.match(/失败:\s*(\d+)/) || [])[1] || "?";
|
||||
var avg = (text.match(/平均\s*([\d.]+)\s*ms/) || [])[1] || "?";
|
||||
var min = (text.match(/最快\s*([\d.]+)\s*ms/) || [])[1] || "?";
|
||||
var avg = (text.match(/平均\s*([\d.]+)\s*µs/) || [])[1] || "?";
|
||||
var min = (text.match(/最快\s*([\d.]+)\s*µs/) || [])[1] || "?";
|
||||
var tput = (text.match(/等效吞吐:\s*([\d.]+)\s*KB\/s/) || [])[1] || "-";
|
||||
var d = new Date();
|
||||
var tr = document.createElement("tr");
|
||||
var cells = [
|
||||
pad2(d.getHours()) + ":" + pad2(d.getMinutes()) + ":" + pad2(d.getSeconds()),
|
||||
String(frames), String(payload) + "B", fail,
|
||||
avg + "ms", min + "ms", tput === "-" ? "-" : tput + "KB/s"
|
||||
avg + "µs", min + "µs", tput === "-" ? "-" : tput + "KB/s"
|
||||
];
|
||||
for (var i = 0; i < cells.length; i++) {
|
||||
var td = document.createElement("td");
|
||||
|
||||
Reference in New Issue
Block a user