修复协议解析器, 打通与 MPU 的双向通信

- 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 回
This commit is contained in:
kirl committed 2026-07-29 23:24:31 +08:00
1 parent 40ee976236
commit af4f0edc08
9 files changed
+218 -20

No files matched your search

+2
View File
@@ -76,3 +76,5 @@ Desktop.ini
*.workspace # General workspace files, can be from various tools *.workspace # General workspace files, can be from various tools
*.suo # Visual Studio Solution User Options *.suo # Visual Studio Solution User Options
*.sln.docstates # Visual Studio *.sln.docstates # Visual Studio
.cache/
+2 -1
View File
@@ -177,8 +177,9 @@ uint16_t edc_crc16(const uint8_t *data, size_t len)
- **RX**: 接收 CYCLE_DATA (冗余备份) - **RX**: 接收 CYCLE_DATA (冗余备份)
- **TX**: 发送 CYCLE_DONE / CYCLE_REQUEST - **TX**: 发送 CYCLE_DONE / CYCLE_REQUEST
- 默认引脚: TX=GPIO8, RX=GPIO9 - 默认引脚: TX=GPIO11, RX=GPIO12 (UART0)
- 波特率: 115200, 8N1 - 波特率: 115200, 8N1
- 注: ESP32-C5 日志控制台使用 USB Serial/JTAG, UART0 专用于本协议
### 6.3 冗余策略 ### 6.3 冗余策略
+13 -12
View File
@@ -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_parse_result_t edc_cycle_parse_byte(edc_frame_ctx_t *ctx, uint8_t byte,
edc_cycle_t *cycle) edc_cycle_t *cycle)
{ {
switch (ctx->state) { /* 上一帧解析完成后, 复位准备接收下一帧 */
case EDC_PARSE_OK: if (ctx->state == EDC_PARSE_OK) {
ctx->idx = 0; ctx->idx = 0;
ctx->state = EDC_PARSE_WAITING; ctx->state = EDC_PARSE_WAITING;
/* fall through */ }
case EDC_PARSE_WAITING:
default: /* ---- 等待 SOF1 ---- */
if (ctx->idx == 0) {
if (byte == EDC_PROTO_SOF1) { if (byte == EDC_PROTO_SOF1) {
ctx->buf[0] = byte; ctx->buf[0] = byte;
ctx->idx = 1; 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; return EDC_PARSE_WAITING;
} }
/* ---- 收帧中 (idx >= 1) ---- */ /* ---- 等待 SOF2 ---- */
if (ctx->idx == 1) { if (ctx->idx == 1) {
if (byte != EDC_PROTO_SOF2) { if (byte != EDC_PROTO_SOF2) {
ctx->idx = 0; 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 */ ctx->len = 2 + 1 + 1 + data_len + 2; /* SOF+CMD+LEN+DATA+CRC */
} }
/* 收满一帧 */ /* 收满一帧 (idx>=4 保证 ctx->len 已按 LEN 字段设置) */
if (ctx->idx >= ctx->len) { if (ctx->idx >= 4 && ctx->idx >= ctx->len) {
ctx->idx = ctx->len; ctx->idx = ctx->len;
/* CRC 校验 */ /* CRC 校验 */
@@ -295,8 +295,9 @@ static bool uart_poll_once(void)
{ {
uint8_t byte; uint8_t byte;
bool got = false; 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++; s_stat.uart.bytes_total++;
edc_cycle_t cycle; edc_cycle_t cycle;
@@ -365,7 +366,7 @@ void edc_proto_init(const edc_proto_config_t *cfg)
.sda_io_num = dft.i2c_sda, .sda_io_num = dft.i2c_sda,
.scl_io_num = dft.i2c_scl, .scl_io_num = dft.i2c_scl,
.clk_source = I2C_CLK_SRC_DEFAULT, .clk_source = I2C_CLK_SRC_DEFAULT,
.send_buf_depth = 0, .send_buf_depth = 8, /* 最少需要非零值 */
.receive_buf_depth = EDC_I2C_BUF_SIZE, .receive_buf_depth = EDC_I2C_BUF_SIZE,
.slave_addr = dft.i2c_addr, .slave_addr = dft.i2c_addr,
.addr_bit_len = I2C_ADDR_BIT_LEN_7, .addr_bit_len = I2C_ADDR_BIT_LEN_7,
@@ -485,6 +486,6 @@ void edc_proto_task(void *pvParam)
portEXIT_CRITICAL(&s_lock); portEXIT_CRITICAL(&s_lock);
edc_proto_on_cycle(&cycle, ch); edc_proto_on_cycle(&cycle, ch);
} }
vTaskDelay(pdMS_TO_TICKS(2)); vTaskDelay(pdMS_TO_TICKS(10));
} }
} }
+2 -2
View File
@@ -64,8 +64,8 @@ extern "C" {
/* ---- UART 默认 ---- */ /* ---- UART 默认 ---- */
#define EDC_UART_PORT_DEFAULT UART_NUM_0 #define EDC_UART_PORT_DEFAULT UART_NUM_0
#define EDC_UART_BAUDRATE 115200 #define EDC_UART_BAUDRATE 115200
#define EDC_UART_RX_DEFAULT GPIO_NUM_9 #define EDC_UART_RX_DEFAULT GPIO_NUM_12
#define EDC_UART_TX_DEFAULT GPIO_NUM_8 #define EDC_UART_TX_DEFAULT GPIO_NUM_11
#define EDC_UART_BUF_SIZE 512 #define EDC_UART_BUF_SIZE 512
/* ================================================================ /* ================================================================
+1 -1
View File
@@ -1,3 +1,3 @@
idf_component_register(SRCS "web_ctrl.c" idf_component_register(SRCS "web_ctrl.c"
INCLUDE_DIRS "." 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)
+174
View File
@@ -7,6 +7,7 @@
#include "emm_v5.h" #include "emm_v5.h"
#include "magnet.h" #include "magnet.h"
#include "servo.h" #include "servo.h"
#include "protocol.h"
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
@@ -175,6 +176,20 @@ button:active{opacity:.7}
</div> </div>
<div class="card" id="proto-card">
<h2><i class="tag amber"></i>通讯协议</h2>
<div class="row" style="font-size:.82rem">
<span>I2C: <b id="pi2c">-</b></span>
<span>UART: <b id="puart">-</b></span>
<span>通道: <b id="pch">-</b></span>
</div>
<div id="pcycle" style="font-size:.78rem;color:var(--sub);margin-top:6px">等待 Cycle...</div>
<div class="btn-row" style="margin-top:10px">
<button class="btn-go" onclick="var c=document.getElementById('pcid');api('proto/done?id='+(c?c.value:0))">Done</button>
<button class="btn-stop" onclick="api('proto/request')">Request</button>
</div>
</div>
<div class="card log-card"> <div class="card log-card">
<h2><i class="tag green"></i>运行日志</h2> <h2><i class="tag green"></i>运行日志</h2>
<div class="log" id="log">等待指令...</div> <div class="log" id="log">等待指令...</div>
@@ -197,6 +212,30 @@ fetch('/api/status').then(function(r){return r.json()}).then(function(d){
document.getElementById('wifi').innerHTML='<i class="dot"></i>WiFi: '+d.ssid; document.getElementById('wifi').innerHTML='<i class="dot"></i>WiFi: '+d.ssid;
document.getElementById('ip').textContent='IP: '+d.ip 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)<br>';
for(var i=0;i<d.cycle.data.length;i++){
var f=d.cycle.data[i];
html += 'F'+i+': ('+f.sx+','+f.sy+' '+f.sa+'°) → ('+f.tx+','+f.ty+' '+f.ta+'°)<br>';
}
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);
</script> </script>
</body> </body>
</html>)raw"; </html>)raw";
@@ -448,6 +487,116 @@ static esp_err_t favicon_handler(httpd_req_t *req)
return ESP_OK; 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) static void http_server_init(void)
{ {
httpd_handle_t server = NULL; httpd_handle_t server = NULL;
@@ -532,6 +681,31 @@ static void http_server_init(void)
}; };
httpd_register_uri_handler(server, &uri_favicon); 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); ESP_LOGI(TAG, "HTTP 服务器已启动 (端口 %d)", cfg.server_port);
} }
+1 -1
View File
@@ -1,3 +1,3 @@
idf_component_register(SRCS "main.c" idf_component_register(SRCS "main.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
PRIV_REQUIRES emm_v5 magnet servo web_ctrl) PRIV_REQUIRES emm_v5 magnet servo web_ctrl protocol)
+20 -3
View File
@@ -7,7 +7,14 @@
* ESP32 RX (GPIO7) -> 电机 TX * ESP32 RX (GPIO7) -> 电机 TX
* ESP32 GPIO5 -> 电磁铁 * ESP32 GPIO5 -> 电磁铁
* ESP32 GPIO4 -> 舵机 * 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 后用浏览器打开控制页面 * WiFi: 连接 ICE (密码 12345678), 获取 IP 后用浏览器打开控制页面
*/ */
@@ -16,8 +23,12 @@
#include "magnet.h" #include "magnet.h"
#include "servo.h" #include "servo.h"
#include "web_ctrl.h" #include "web_ctrl.h"
#include "protocol.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "esp_log.h"
static const char *TAG = "MAIN";
void app_main(void) void app_main(void)
{ {
@@ -30,14 +41,20 @@ void app_main(void)
/* 初始化舵机 (GPIO4, 上限180° 下限0°) */ /* 初始化舵机 (GPIO4, 上限180° 下限0°) */
servo_init(GPIO_NUM_4); 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 秒, 等待电机闭环初始化完毕 */ /* 上电延时 2 秒, 等待电机闭环初始化完毕 */
ESP_LOGI("MAIN", "等待电机初始化..."); ESP_LOGI(TAG, "等待电机初始化...");
vTaskDelay(pdMS_TO_TICKS(2000)); vTaskDelay(pdMS_TO_TICKS(2000));
/* 启动 WiFi + HTTP 网页控制服务器 */ /* 启动 WiFi + HTTP 网页控制服务器 */
web_ctrl_init(); web_ctrl_init();
ESP_LOGI("MAIN", "系统就绪, 等待网页指令..."); ESP_LOGI(TAG, "系统就绪, 等待网页指令...");
while (1) { while (1) {
vTaskDelay(pdMS_TO_TICKS(1000)); vTaskDelay(pdMS_TO_TICKS(1000));
} }
+3
View File
@@ -0,0 +1,3 @@
# 日志控制台输出到 USB Serial/JTAG, UART0 (GPIO11/12) 专用于 MPU 协议通讯
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
CONFIG_ESP_CONSOLE_SECONDARY_NONE=y