From af4f0edc08db57fc01a8747ac3c0083a8a05da4e Mon Sep 17 00:00:00 2001 From: kirl <18827922006@163.com> Date: Wed, 29 Jul 2026 23:24:31 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8D=8F=E8=AE=AE=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=E5=99=A8,=20=E6=89=93=E9=80=9A=E4=B8=8E=20MPU=20?= =?UTF-8?q?=E7=9A=84=E5=8F=8C=E5=90=91=E9=80=9A=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - protocol: 修复 edc_cycle_parse_byte 状态机死代码 (switch 恒 return, 收帧逻辑不可达), 并加 LEN 未就绪保护 - protocol: UART 默认引脚改为 GPIO11/12 (UART0) - sdkconfig.defaults: 控制台切至 USB Serial/JTAG, 避免与协议 UART0 冲突 - web_ctrl: 新增协议状态 API 与 Cycle 快照; on_cycle 回 DONE 并按 cycle_id 去重 - main: 接入协议初始化与接收任务, 更新接线注释 - 实测: MPU 每秒下发 Cycle, I2C/UART 双通道 12/12 收, DONE 12/12 回 --- .gitignore | 2 + components/protocol/PROTOCOL.md | 3 +- components/protocol/protocol.c | 25 +++-- components/protocol/protocol.h | 4 +- components/web_ctrl/CMakeLists.txt | 2 +- components/web_ctrl/web_ctrl.c | 174 +++++++++++++++++++++++++++++ main/CMakeLists.txt | 2 +- main/main.c | 23 +++- sdkconfig.defaults | 3 + 9 files changed, 218 insertions(+), 20 deletions(-) create mode 100644 sdkconfig.defaults diff --git a/.gitignore b/.gitignore index 7805557..160d0d7 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,5 @@ Desktop.ini *.workspace # General workspace files, can be from various tools *.suo # Visual Studio Solution User Options *.sln.docstates # Visual Studio + +.cache/ \ No newline at end of file diff --git a/components/protocol/PROTOCOL.md b/components/protocol/PROTOCOL.md index b0e50dd..9c835d3 100644 --- a/components/protocol/PROTOCOL.md +++ b/components/protocol/PROTOCOL.md @@ -177,8 +177,9 @@ uint16_t edc_crc16(const uint8_t *data, size_t len) - **RX**: 接收 CYCLE_DATA (冗余备份) - **TX**: 发送 CYCLE_DONE / CYCLE_REQUEST -- 默认引脚: TX=GPIO8, RX=GPIO9 +- 默认引脚: TX=GPIO11, RX=GPIO12 (UART0) - 波特率: 115200, 8N1 +- 注: ESP32-C5 日志控制台使用 USB Serial/JTAG, UART0 专用于本协议 ### 6.3 冗余策略 diff --git a/components/protocol/protocol.c b/components/protocol/protocol.c index c0e10ef..c2cd7d0 100644 --- a/components/protocol/protocol.c +++ b/components/protocol/protocol.c @@ -105,13 +105,14 @@ int edc_cycle_pack(const edc_cycle_t *cycle, uint8_t *out) edc_parse_result_t edc_cycle_parse_byte(edc_frame_ctx_t *ctx, uint8_t byte, edc_cycle_t *cycle) { - switch (ctx->state) { - case EDC_PARSE_OK: + /* 上一帧解析完成后, 复位准备接收下一帧 */ + if (ctx->state == EDC_PARSE_OK) { ctx->idx = 0; ctx->state = EDC_PARSE_WAITING; - /* fall through */ - case EDC_PARSE_WAITING: - default: + } + + /* ---- 等待 SOF1 ---- */ + if (ctx->idx == 0) { if (byte == EDC_PROTO_SOF1) { ctx->buf[0] = byte; ctx->idx = 1; @@ -119,8 +120,7 @@ edc_parse_result_t edc_cycle_parse_byte(edc_frame_ctx_t *ctx, uint8_t byte, return EDC_PARSE_WAITING; } - /* ---- 收帧中 (idx >= 1) ---- */ - + /* ---- 等待 SOF2 ---- */ if (ctx->idx == 1) { if (byte != EDC_PROTO_SOF2) { ctx->idx = 0; @@ -147,8 +147,8 @@ edc_parse_result_t edc_cycle_parse_byte(edc_frame_ctx_t *ctx, uint8_t byte, ctx->len = 2 + 1 + 1 + data_len + 2; /* SOF+CMD+LEN+DATA+CRC */ } - /* 收满一帧 */ - if (ctx->idx >= ctx->len) { + /* 收满一帧 (idx>=4 保证 ctx->len 已按 LEN 字段设置) */ + if (ctx->idx >= 4 && ctx->idx >= ctx->len) { ctx->idx = ctx->len; /* CRC 校验 */ @@ -295,8 +295,9 @@ static bool uart_poll_once(void) { uint8_t byte; bool got = false; + int limit = 64; /* 每次最多处理 64 字节, 防止饿死 IDLE 任务 */ - while (uart_read_bytes(s_uart_port, &byte, 1, pdMS_TO_TICKS(0)) > 0) { + while (limit-- > 0 && uart_read_bytes(s_uart_port, &byte, 1, pdMS_TO_TICKS(0)) > 0) { s_stat.uart.bytes_total++; edc_cycle_t cycle; @@ -365,7 +366,7 @@ void edc_proto_init(const edc_proto_config_t *cfg) .sda_io_num = dft.i2c_sda, .scl_io_num = dft.i2c_scl, .clk_source = I2C_CLK_SRC_DEFAULT, - .send_buf_depth = 0, + .send_buf_depth = 8, /* 最少需要非零值 */ .receive_buf_depth = EDC_I2C_BUF_SIZE, .slave_addr = dft.i2c_addr, .addr_bit_len = I2C_ADDR_BIT_LEN_7, @@ -485,6 +486,6 @@ void edc_proto_task(void *pvParam) portEXIT_CRITICAL(&s_lock); edc_proto_on_cycle(&cycle, ch); } - vTaskDelay(pdMS_TO_TICKS(2)); + vTaskDelay(pdMS_TO_TICKS(10)); } } diff --git a/components/protocol/protocol.h b/components/protocol/protocol.h index 17fa747..7354518 100644 --- a/components/protocol/protocol.h +++ b/components/protocol/protocol.h @@ -64,8 +64,8 @@ extern "C" { /* ---- UART 默认 ---- */ #define EDC_UART_PORT_DEFAULT UART_NUM_0 #define EDC_UART_BAUDRATE 115200 -#define EDC_UART_RX_DEFAULT GPIO_NUM_9 -#define EDC_UART_TX_DEFAULT GPIO_NUM_8 +#define EDC_UART_RX_DEFAULT GPIO_NUM_12 +#define EDC_UART_TX_DEFAULT GPIO_NUM_11 #define EDC_UART_BUF_SIZE 512 /* ================================================================ diff --git a/components/web_ctrl/CMakeLists.txt b/components/web_ctrl/CMakeLists.txt index acc97f7..2de0881 100644 --- a/components/web_ctrl/CMakeLists.txt +++ b/components/web_ctrl/CMakeLists.txt @@ -1,3 +1,3 @@ idf_component_register(SRCS "web_ctrl.c" INCLUDE_DIRS "." - REQUIRES emm_v5 magnet servo esp_http_server esp_wifi nvs_flash) + REQUIRES emm_v5 magnet servo esp_http_server esp_wifi nvs_flash protocol) diff --git a/components/web_ctrl/web_ctrl.c b/components/web_ctrl/web_ctrl.c index 961a3a5..ad0dbd7 100644 --- a/components/web_ctrl/web_ctrl.c +++ b/components/web_ctrl/web_ctrl.c @@ -7,6 +7,7 @@ #include "emm_v5.h" #include "magnet.h" #include "servo.h" +#include "protocol.h" #include #include @@ -175,6 +176,20 @@ button:active{opacity:.7} +
+

通讯协议

+
+ I2C: - + UART: - + 通道: - +
+
等待 Cycle...
+
+ + +
+
+

运行日志

等待指令...
@@ -197,6 +212,30 @@ fetch('/api/status').then(function(r){return r.json()}).then(function(d){ document.getElementById('wifi').innerHTML='WiFi: '+d.ssid; document.getElementById('ip').textContent='IP: '+d.ip }) +/* 每秒刷新协议状态 */ +function pollProto(){ + fetch('/api/proto/status').then(function(r){return r.json()}).then(function(d){ + document.getElementById('pi2c').textContent = d.i2c_ok + '/' + (d.i2c_ok+d.i2c_err); + document.getElementById('puart').textContent = d.uart_ok + '/' + (d.uart_ok+d.uart_err); + document.getElementById('pch').textContent = d.last_ch; + if(d.cycle){ + document.getElementById('pcid').value = d.cycle.id; + var html = 'Cycle #'+d.cycle.id + ' ('+d.cycle.frags+' frags)
'; + for(var i=0;i'; + } + document.getElementById('pcycle').innerHTML = html; + } + }).catch(function(){}); +} +/* 隐藏的 cycle_id 存储 */ +(function(){ + var inp=document.createElement('input'); + inp.type='hidden';inp.id='pcid';inp.value='0'; + document.getElementById('proto-card').appendChild(inp); +})(); +pollProto();setInterval(pollProto,1000); )raw"; @@ -448,6 +487,116 @@ static esp_err_t favicon_handler(httpd_req_t *req) return ESP_OK; } +/* ============================================================ + * 协议状态 API + * ============================================================ */ + +/* 最近收到的 Cycle 快照 (由 edc_proto_on_cycle 更新, web 读取) */ +static edc_cycle_t s_web_cycle; +static volatile bool s_web_cycle_valid; +static portMUX_TYPE s_web_lock = portMUX_INITIALIZER_UNLOCKED; + +void edc_proto_on_cycle(const edc_cycle_t *cycle, edc_channel_t ch) +{ + static uint16_t s_last_done_id = 0xFFFF; + + portENTER_CRITICAL(&s_web_lock); + memcpy(&s_web_cycle, cycle, sizeof(edc_cycle_t)); + s_web_cycle_valid = true; + portEXIT_CRITICAL(&s_web_lock); + + ESP_LOGI(TAG, "Cycle #%u via %s, %u frags", + cycle->cycle_id, ch == EDC_CH_I2C ? "I2C" : "UART", + cycle->frag_count); + + /* I2C / UART 冗余下发, 同一 cycle_id 只回一次 DONE */ + if (cycle->cycle_id == s_last_done_id) { + return; + } + s_last_done_id = cycle->cycle_id; + + /* 回复 MPU: Cycle 已收到 (后续实际执行电机后应移到执行完毕处) */ + edc_proto_send_done(cycle->cycle_id); +} + +static esp_err_t api_proto_status_handler(httpd_req_t *req) +{ + edc_proto_stat_t stat; + edc_proto_get_stats(&stat); + + char json[1024]; + int off = snprintf(json, sizeof(json), + "{\"i2c_ok\":%lu,\"i2c_err\":%lu,\"uart_ok\":%lu,\"uart_err\":%lu," + "\"last_ch\":\"%s\",\"done\":%lu,\"req\":%lu", + stat.i2c.cycles_ok, stat.i2c.cycles_err, + stat.uart.cycles_ok, stat.uart.cycles_err, + (stat.last_channel == EDC_CH_I2C) ? "I2C" : + (stat.last_channel == EDC_CH_UART) ? "UART" : "none", + stat.done_sent, stat.request_sent); + + portENTER_CRITICAL(&s_web_lock); + bool valid = s_web_cycle_valid; + edc_cycle_t cycle; + if (valid) memcpy(&cycle, &s_web_cycle, sizeof(edc_cycle_t)); + portEXIT_CRITICAL(&s_web_lock); + + if (valid) { + off += snprintf(json + off, sizeof(json) - off, + ",\"cycle\":{\"id\":%u,\"frags\":%u,\"data\":[", + cycle.cycle_id, cycle.frag_count); + for (int i = 0; i < cycle.frag_count; i++) { + edc_fragment_t *f = &cycle.fragments[i]; + off += snprintf(json + off, sizeof(json) - off, + "%s{\"sx\":%u,\"sy\":%u,\"sa\":%.1f,\"tx\":%u,\"ty\":%u,\"ta\":%.1f}", + (i > 0) ? "," : "", + f->source.x, f->source.y, f->source.angle_x10 / 10.0f, + f->target.x, f->target.y, f->target.angle_x10 / 10.0f); + } + off += snprintf(json + off, sizeof(json) - off, "]}"); + } + off += snprintf(json + off, sizeof(json) - off, "}"); + + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, json, HTTPD_RESP_USE_STRLEN); + return ESP_OK; +} + +static esp_err_t api_proto_request_handler(httpd_req_t *req) +{ + edc_proto_send_request(); + httpd_resp_set_type(req, "text/plain; charset=utf-8"); + httpd_resp_send(req, "已发送 CYCLE_REQUEST", HTTPD_RESP_USE_STRLEN); + return ESP_OK; +} + +static esp_err_t api_proto_done_handler(httpd_req_t *req) +{ + edc_cycle_t cycle; + edc_proto_stat_t stat; + edc_proto_get_stats(&stat); + /* 对最近收到的 cycle 发送 DONE */ + if (stat.last_channel != EDC_CH_NONE) { + char buf[128]; + size_t len = httpd_req_get_url_query_len(req) + 1; + if (len > sizeof(buf)) len = sizeof(buf); + uint16_t cid = 0; + if (httpd_req_get_url_query_str(req, buf, len) == ESP_OK) { + char val[16]; + if (httpd_query_key_value(buf, "id", val, sizeof(val)) == ESP_OK) { + cid = (uint16_t)atoi(val); + } + } + edc_proto_send_done(cid); + char resp[64]; + snprintf(resp, sizeof(resp), "已发送 CYCLE_DONE (id=%u)", cid); + httpd_resp_set_type(req, "text/plain; charset=utf-8"); + httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); + return ESP_OK; + } + httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "尚无收到 Cycle"); + return ESP_FAIL; +} + static void http_server_init(void) { httpd_handle_t server = NULL; @@ -532,6 +681,31 @@ static void http_server_init(void) }; httpd_register_uri_handler(server, &uri_favicon); + /* ---- 协议状态 API ---- */ + httpd_uri_t uri_proto_status = { + .uri = "/api/proto/status", + .method = HTTP_GET, + .handler = api_proto_status_handler, + .user_ctx = NULL + }; + httpd_register_uri_handler(server, &uri_proto_status); + + httpd_uri_t uri_proto_request = { + .uri = "/api/proto/request", + .method = HTTP_GET, + .handler = api_proto_request_handler, + .user_ctx = NULL + }; + httpd_register_uri_handler(server, &uri_proto_request); + + httpd_uri_t uri_proto_done = { + .uri = "/api/proto/done", + .method = HTTP_GET, + .handler = api_proto_done_handler, + .user_ctx = NULL + }; + httpd_register_uri_handler(server, &uri_proto_done); + ESP_LOGI(TAG, "HTTP 服务器已启动 (端口 %d)", cfg.server_port); } diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index b2068dc..4b3bc9a 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -1,3 +1,3 @@ idf_component_register(SRCS "main.c" INCLUDE_DIRS "." - PRIV_REQUIRES emm_v5 magnet servo web_ctrl) + PRIV_REQUIRES emm_v5 magnet servo web_ctrl protocol) diff --git a/main/main.c b/main/main.c index ee4e049..3f9073c 100644 --- a/main/main.c +++ b/main/main.c @@ -7,7 +7,14 @@ * ESP32 RX (GPIO7) -> 电机 TX * ESP32 GPIO5 -> 电磁铁 * ESP32 GPIO4 -> 舵机 - * ESP32 GND -> 电机 GND + * ESP32 GPIO11 -> 协议 UART TX (→ MPU) (UART0) + * ESP32 GPIO12 -> 协议 UART RX (← MPU) (UART0) + * ESP32 GPIO2 -> I2C SDA (← MPU) + * ESP32 GPIO3 -> I2C SCL + * ESP32 GND -> 电机 GND / MPU GND + * + * 注: 日志控制台已切换到 USB Serial/JTAG (COM 口监视), + * UART0 (GPIO11/12) 专用于 MPU 协议通讯。 * * WiFi: 连接 ICE (密码 12345678), 获取 IP 后用浏览器打开控制页面 */ @@ -16,8 +23,12 @@ #include "magnet.h" #include "servo.h" #include "web_ctrl.h" +#include "protocol.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" +#include "esp_log.h" + +static const char *TAG = "MAIN"; void app_main(void) { @@ -30,14 +41,20 @@ void app_main(void) /* 初始化舵机 (GPIO4, 上限180° 下限0°) */ servo_init(GPIO_NUM_4); + /* 初始化通讯协议 (I2C 从机 GPIO2/3 + UART0 GPIO11/12) */ + edc_proto_init(NULL); + + /* 启动协议接收任务 */ + xTaskCreate(edc_proto_task, "proto", 4096, NULL, 2, NULL); + /* 上电延时 2 秒, 等待电机闭环初始化完毕 */ - ESP_LOGI("MAIN", "等待电机初始化..."); + ESP_LOGI(TAG, "等待电机初始化..."); vTaskDelay(pdMS_TO_TICKS(2000)); /* 启动 WiFi + HTTP 网页控制服务器 */ web_ctrl_init(); - ESP_LOGI("MAIN", "系统就绪, 等待网页指令..."); + ESP_LOGI(TAG, "系统就绪, 等待网页指令..."); while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); } diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 0000000..5bb0a28 --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,3 @@ +# 日志控制台输出到 USB Serial/JTAG, UART0 (GPIO11/12) 专用于 MPU 协议通讯 +CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y +CONFIG_ESP_CONSOLE_SECONDARY_NONE=y