修复协议解析器, 打通与 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:
1 parent
40ee976236
commit
af4f0edc08
9 files changed
+218
-20
No files matched your search
@@ -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/
|
||||
@@ -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 冗余策略
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
/* ================================================================
|
||||
|
||||
@@ -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)
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "emm_v5.h"
|
||||
#include "magnet.h"
|
||||
#include "servo.h"
|
||||
#include "protocol.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
@@ -175,6 +176,20 @@ button:active{opacity:.7}
|
||||
|
||||
</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">
|
||||
<h2><i class="tag green"></i>运行日志</h2>
|
||||
<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('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>
|
||||
</body>
|
||||
</html>)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);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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)
|
||||
+20
-3
@@ -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));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# 日志控制台输出到 USB Serial/JTAG, UART0 (GPIO11/12) 专用于 MPU 协议通讯
|
||||
CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y
|
||||
CONFIG_ESP_CONSOLE_SECONDARY_NONE=y
|
||||
Reference in new issue
Block a user