From 40ee976236403694bbb3c25ff422d0cc75a8787f Mon Sep 17 00:00:00 2001 From: kirl <18827922006@163.com> Date: Wed, 29 Jul 2026 21:11:16 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A3=81=E9=93=81=E3=80=81=E8=88=B5=E6=9C=BA?= =?UTF-8?q?=E3=80=81=E7=BD=91=E9=A1=B5=E6=8E=A7=E5=88=B6=E3=80=81=E9=80=9A?= =?UTF-8?q?=E4=BF=A1=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/emm_v5/emm_v5.h | 100 ++++++ components/magnet/CMakeLists.txt | 3 + components/magnet/magnet.c | 24 ++ components/magnet/magnet.h | 32 ++ components/protocol/CMakeLists.txt | 3 + components/protocol/PROTOCOL.md | 254 ++++++++++++++ components/protocol/protocol.c | 490 ++++++++++++++++++++++++++ components/protocol/protocol.h | 233 ++++++++++++ components/servo/CMakeLists.txt | 3 + components/servo/servo.c | 89 +++++ components/servo/servo.h | 49 +++ components/web_ctrl/CMakeLists.txt | 3 + components/web_ctrl/web_ctrl.c | 546 +++++++++++++++++++++++++++++ components/web_ctrl/web_ctrl.h | 26 ++ main/CMakeLists.txt | 2 +- main/main.c | 49 +-- partitions.csv | 5 + 17 files changed, 1879 insertions(+), 32 deletions(-) create mode 100644 components/magnet/CMakeLists.txt create mode 100644 components/magnet/magnet.c create mode 100644 components/magnet/magnet.h create mode 100644 components/protocol/CMakeLists.txt create mode 100644 components/protocol/PROTOCOL.md create mode 100644 components/protocol/protocol.c create mode 100644 components/protocol/protocol.h create mode 100644 components/servo/CMakeLists.txt create mode 100644 components/servo/servo.c create mode 100644 components/servo/servo.h create mode 100644 components/web_ctrl/CMakeLists.txt create mode 100644 components/web_ctrl/web_ctrl.c create mode 100644 components/web_ctrl/web_ctrl.h create mode 100644 partitions.csv diff --git a/components/emm_v5/emm_v5.h b/components/emm_v5/emm_v5.h index a9779e7..182e5cd 100644 --- a/components/emm_v5/emm_v5.h +++ b/components/emm_v5/emm_v5.h @@ -64,22 +64,122 @@ typedef enum { * @param rx_pin RX 引脚, 如 GPIO_NUM_7 */ void Emm_V5_Init(uart_port_t port, int tx_pin, int rx_pin); + +/** + * @brief 将当前位置清零 + * @param addr 电机地址 + */ void Emm_V5_Reset_CurPos_To_Zero(uint8_t addr); + +/** + * @brief 解除堵转保护 + * @param addr 电机地址 + */ void Emm_V5_Reset_Clog_Pro(uint8_t addr); + +/** + * @brief 读取系统参数 + * @param addr 电机地址 + * @param s 系统参数类型 (@see EmmSysParams_t) + */ void Emm_V5_Read_Sys_Params(uint8_t addr, EmmSysParams_t s); + +/** + * @brief 修改开环/闭环控制模式 + * @param addr 电机地址 + * @param svF 是否存储, false=不存储, true=存储 + * @param ctrl_mode 控制模式: 0=关闭脉冲输入, 1=开环, 2=闭环, 3=En复用为限位/到位输出 + */ void Emm_V5_Modify_Ctrl_Mode(uint8_t addr, bool svF, uint8_t ctrl_mode); + +/** + * @brief 使能信号控制 + * @param addr 电机地址 + * @param state 使能状态, true=使能, false=关闭 + * @param snF 多机同步标志, false=不启用, true=启用 + */ void Emm_V5_En_Control(uint8_t addr, bool state, bool snF); + +/** + * @brief 速度模式控制 + * @param addr 电机地址 + * @param dir 方向, 0=CW, 其他=CCW + * @param vel 速度 0~5000 RPM + * @param acc 加速度 0~255 (0=直接启动) + * @param snF 多机同步标志, false=不启用, true=启用 + */ void Emm_V5_Vel_Control(uint8_t addr, uint8_t dir, uint16_t vel, uint8_t acc, bool snF); + +/** + * @brief 位置模式控制 + * @param addr 电机地址 + * @param dir 方向, 0=CW, 其他=CCW + * @param vel 速度 0~5000 RPM + * @param acc 加速度 0~255 (0=直接启动) + * @param clk 脉冲数 0~(2^32-1) + * @param raF 相位/绝对标志, false=相对运动, true=绝对运动 + * @param snF 多机同步标志, false=不启用, true=启用 + */ void Emm_V5_Pos_Control(uint8_t addr, uint8_t dir, uint16_t vel, uint8_t acc, uint32_t clk, bool raF, bool snF); + +/** + * @brief 立即停止电机 (所有控制模式通用) + * @param addr 电机地址 + * @param snF 多机同步标志, false=不启用, true=启用 + */ void Emm_V5_Stop_Now(uint8_t addr, bool snF); + +/** + * @brief 触发多机同步开始运动 + * @param addr 电机地址 + */ void Emm_V5_Synchronous_motion(uint8_t addr); + +/** + * @brief 设置单圈回零的零点位置 + * @param addr 电机地址 + * @param svF 是否存储, false=不存储, true=存储 + */ void Emm_V5_Origin_Set_O(uint8_t addr, bool svF); + +/** + * @brief 修改回零参数 + * @param addr 电机地址 + * @param svF 是否存储, false=不存储, true=存储 + * @param o_mode 回零模式: 0=单圈就近, 1=单圈方向, 2=多圈无限位碰撞, 3=多圈有限位开关 + * @param o_dir 回零方向, 0=CW, 其他=CCW + * @param o_vel 回零速度 (RPM) + * @param o_tm 回零超时时间 (ms) + * @param sl_vel 碰撞检测转速 (RPM) + * @param sl_ma 碰撞检测电流 (mA) + * @param sl_ms 碰撞检测时间 (ms) + * @param potF 上电自动回零, false=不使能, true=使能 + */ void Emm_V5_Origin_Modify_Params(uint8_t addr, bool svF, uint8_t o_mode, uint8_t o_dir, uint16_t o_vel, uint32_t o_tm, uint16_t sl_vel, uint16_t sl_ma, uint16_t sl_ms, bool potF); + +/** + * @brief 触发回零 + * @param addr 电机地址 + * @param o_mode 回零模式: 0=单圈就近, 1=单圈方向, 2=多圈无限位碰撞, 3=多圈有限位开关 + * @param snF 多机同步标志, false=不启用, true=启用 + */ void Emm_V5_Origin_Trigger_Return(uint8_t addr, uint8_t o_mode, bool snF); + +/** + * @brief 强制中断并退出回零 + * @param addr 电机地址 + */ void Emm_V5_Origin_Interrupt(uint8_t addr); + +/** + * @brief 接收电机返回数据 (100ms 超时判定帧结束) + * @param rxCmd 接收缓冲区 + * @param maxLen 缓冲区最大长度 + * @return 实际接收到的字节数 + */ uint8_t Emm_V5_Receive_Data(uint8_t *rxCmd, uint8_t maxLen); #ifdef __cplusplus diff --git a/components/magnet/CMakeLists.txt b/components/magnet/CMakeLists.txt new file mode 100644 index 0000000..80753e9 --- /dev/null +++ b/components/magnet/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "magnet.c" + INCLUDE_DIRS "." + REQUIRES esp_driver_gpio driver) diff --git a/components/magnet/magnet.c b/components/magnet/magnet.c new file mode 100644 index 0000000..29da5af --- /dev/null +++ b/components/magnet/magnet.c @@ -0,0 +1,24 @@ +/** + * @file magnet.c + * @brief 单 GPIO 电磁铁控制实现 + */ + +#include "magnet.h" + +static int s_gpio = -1; /* 未初始化 */ + +void magnet_init(int gpio) +{ + s_gpio = gpio; + + gpio_reset_pin(s_gpio); + gpio_set_direction(s_gpio, GPIO_MODE_OUTPUT); + gpio_set_level(s_gpio, 0); /* 默认关闭 */ +} + +void magnet_set(bool on) +{ + if (s_gpio >= 0) { + gpio_set_level(s_gpio, on ? 1 : 0); + } +} diff --git a/components/magnet/magnet.h b/components/magnet/magnet.h new file mode 100644 index 0000000..d771d61 --- /dev/null +++ b/components/magnet/magnet.h @@ -0,0 +1,32 @@ +/** + * @file magnet.h + * @brief 单 GPIO 电磁铁控制 + */ + +#ifndef MAGNET_H +#define MAGNET_H + +#include +#include "driver/gpio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief 初始化电磁铁 GPIO (推挽输出, 默认关闭) + * @param gpio GPIO 引脚号, 如 GPIO_NUM_5 + */ +void magnet_init(int gpio); + +/** + * @brief 设置电磁铁状态 + * @param on true=吸合, false=释放 + */ +void magnet_set(bool on); + +#ifdef __cplusplus +} +#endif + +#endif /* MAGNET_H */ diff --git a/components/protocol/CMakeLists.txt b/components/protocol/CMakeLists.txt new file mode 100644 index 0000000..c923278 --- /dev/null +++ b/components/protocol/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "protocol.c" + INCLUDE_DIRS "." + REQUIRES driver esp_driver_i2c esp_driver_uart esp_driver_gpio freertos) diff --git a/components/protocol/PROTOCOL.md b/components/protocol/PROTOCOL.md new file mode 100644 index 0000000..b0e50dd --- /dev/null +++ b/components/protocol/PROTOCOL.md @@ -0,0 +1,254 @@ +# EDC 通讯协议 + +## 概述 + +本协议用于 MPU 与 MCU (ESP32-C5) 之间的数据交互,支持 **I2C** 与 **UART** 双通道冗余传输。 + +| 属性 | 值 | +|------|-----| +| 帧头 | `0xAA 0x55` | +| 字节序 | 大端 (Big-Endian) | +| CRC | CRC-16/MODBUS (多项式 `0x8005`) | +| I2C 模式 | ESP32 从机, 地址 `0x2A` | +| UART 波特率 | `115200`, 8N1 | + +--- + +## 1. 数据模型 + +``` +Point = { x: uint16, y: uint16, angle_x10: uint16 } +Fragment = { source: Point, target: Point } +Cycle = { cycle_id: uint16, frag_count: uint8 (2~4), fragments: Fragment[] } +``` + +- **Point**: 单个坐标点, `angle_x10 = 角度 × 10` (如 `450` = 45.0°) +- **Fragment**: 一次运动片段, 从 `source` 移动到 `target` +- **Cycle**: 一个执行周期, 包含 2~4 个 Fragment + +--- + +## 2. 帧格式 + +``` +┌────────┬────────┬────────┬────────┬───────────────────┬──────────────┐ +│ SOF1 │ SOF2 │ CMD │ LEN │ DATA (N bytes) │ CRC16 (LE) │ +│ 0xAA │ 0x55 │ 1B │ 1B │ variable │ 2B │ +└────────┴────────┴────────┴────────┴───────────────────┴──────────────┘ + ◄──────────── CRC 计算范围 (CMD + LEN + DATA) ────────────► +``` + +- **SOF**: 帧头, 固定 `0xAA 0x55` +- **CMD**: 命令字 (见 §3) +- **LEN**: DATA 区字节数 +- **DATA**: 有效载荷 +- **CRC**: CRC-16/MODBUS, 低位在前 + +--- + +## 3. 命令字 + +| 命令 | 值 | 方向 | 说明 | +|------|-----|------|------| +| `CYCLE_DATA` | `0x01` | MPU → MCU | 下发 Cycle 数据 | +| `CYCLE_DONE` | `0x02` | MCU → MPU | Cycle 执行完成 | +| `CYCLE_REQUEST` | `0x03` | MCU → MPU | 请求下一个 Cycle | +| `ACK` | `0x80` | 双向 | 通用应答 (预留) | +| `NACK` | `0xFF` | 双向 | 错误 (预留) | + +--- + +## 4. 帧类型详解 + +### 4.1 CYCLE_DATA (MPU → MCU) + +``` +SOF(2) + CMD=0x01 + LEN + DATA + CRC(2) + +DATA: +┌──────────────┬────────────┬─────────────────────────┐ +│ cycle_id │ frag_count │ fragments │ +│ uint16 (BE) │ uint8 │ N × 12B │ +│ 2B │ 1B │ 12~48B │ +└──────────────┴────────────┴─────────────────────────┘ + +LEN = 3 + frag_count × 12 +``` + +**Fragment (12B)**: +``` +┌──────────┬──────────┬───────────────┬──────────┬──────────┬───────────────┐ +│ src.x │ src.y │ src.angle_x10 │ dst.x │ dst.y │ dst.angle_x10 │ +│ uint16 │ uint16 │ uint16 │ uint16 │ uint16 │ uint16 │ +│ 2B │ 2B │ 2B │ 2B │ 2B │ 2B │ +└──────────┴──────────┴───────────────┴──────────┴──────────┴───────────────┘ +``` + +**示例** (2 个 Fragment, `cycle_id=1`): + +``` +AA 55 01 1B 00 01 02 +00 64 00 C8 01 C2 01 2C 01 90 02 58 ← Fragment 0 +00 96 01 2C 03 84 01 F4 02 BC 04 B0 ← Fragment 1 +XX XX ← CRC16 +``` + +解析: +| 字段 | 值 | 含义 | +|------|-----|------| +| cycle_id | `0x0001` = 1 | 第 1 个 Cycle | +| frag_count | `0x02` = 2 | 2 个 Fragment | +| F0 src | (100, 200), 45.0° | | +| F0 dst | (300, 400), 60.0° | | +| F1 src | (150, 300), 90.0° | | +| F1 dst | (500, 700), 120.0° | | + +### 4.2 CYCLE_DONE (MCU → MPU) + +``` +SOF(2) + CMD=0x02 + LEN=2 + cycle_id(2B, BE) + CRC(2) + +总长度: 8 字节 +``` + +**示例** (`cycle_id=1` 完成): +``` +AA 55 02 02 00 01 XX XX +``` + +### 4.3 CYCLE_REQUEST (MCU → MPU) + +``` +SOF(2) + CMD=0x03 + LEN=0 + CRC(2) + +总长度: 6 字节 +``` + +**示例**: +``` +AA 55 03 00 XX XX +``` + +--- + +## 5. CRC-16/MODBUS + +| 参数 | 值 | +|------|-----| +| 多项式 | `0x8005` (逆序 `0xA001`) | +| 初始值 | `0xFFFF` | +| 输入反射 | 是 | +| 输出反射 | 是 | +| 异或输出 | `0x0000` | +| 校验范围 | CMD + LEN + DATA | + +### 参考实现 (C) + +```c +uint16_t edc_crc16(const uint8_t *data, size_t len) +{ + uint16_t crc = 0xFFFF; + for (size_t i = 0; i < len; i++) { + crc ^= data[i]; + for (int j = 0; j < 8; j++) { + if (crc & 0x0001) + crc = (crc >> 1) ^ 0xA001; + else + crc >>= 1; + } + } + return crc; +} +``` + +--- + +## 6. 传输通道 + +### 6.1 I2C (冗余接收) + +- ESP32 作为 **I2C 从机**, 地址 `0x2A` (7-bit) +- MPU 作为 I2C 主机 +- MPU 将完整帧直接 `Write` 到从机地址 +- ESP32 通过 ISR 回调接收并 CRC 校验 +- 默认引脚: SDA=GPIO2, SCL=GPIO3 + +### 6.2 UART (双向) + +- **RX**: 接收 CYCLE_DATA (冗余备份) +- **TX**: 发送 CYCLE_DONE / CYCLE_REQUEST +- 默认引脚: TX=GPIO8, RX=GPIO9 +- 波特率: 115200, 8N1 + +### 6.3 冗余策略 + +同一帧 CYCLE_DATA 同时通过 I2C 和 UART 发送, **任一通道先收到有效帧即采纳**, 通过 `edc_proto_stat_t.last_channel` 可判断来源。 + +--- + +## 7. 交互流程 + +``` +MPU MCU (ESP32-C5) + │ │ + │──── CYCLE_DATA (I2C+UART) ────────►│ 下发 Cycle + │ │ + │ │ 执行 fragments[0..N] + │ │ + │◄──── CYCLE_DONE (UART) ───────────│ 上报完成 + │ │ + │──── CYCLE_DATA (I2C+UART) ────────►│ 下发下一个... + │ │ + — 或 — + │ │ + │ (超时无数据) │ + │ │ + │◄──── CYCLE_REQUEST (UART) ────────│ 主动请求 +``` + +--- + +## 8. MCU 端 API + +```c +#include "protocol.h" + +// 初始化 (I2C 从机 + UART) +edc_proto_init(NULL); + +// 方式一: 手动轮询 +edc_cycle_t cycle; +if (edc_proto_poll(&cycle) > 0) { + // 处理 cycle.fragments[0..frag_count-1] + edc_proto_send_done(cycle.cycle_id); +} + +// 方式二: FreeRTOS 任务 + 回调 +xTaskCreate(edc_proto_task, "proto", 4096, NULL, 5, NULL); + +void edc_proto_on_cycle(const edc_cycle_t *cycle, edc_channel_t ch) { + // ch: EDC_CH_I2C 或 EDC_CH_UART + // 执行完调用: + edc_proto_send_done(cycle->cycle_id); +} + +// 超时请求 +edc_proto_send_request(); + +// 查看统计 +edc_proto_stat_t stat; +edc_proto_get_stats(&stat); +// stat.i2c.cycles_ok, stat.uart.cycles_err, stat.done_sent ... +``` + +--- + +## 9. 帧总长速查 + +| frag_count | DATA 长度 | 帧总长 (SOF+CMD+LEN+DATA+CRC) | +|------------|-----------|------| +| 2 | 3 + 24 = 27 | 2 + 1 + 1 + 27 + 2 = **33** | +| 3 | 3 + 36 = 39 | 2 + 1 + 1 + 39 + 2 = **45** | +| 4 | 3 + 48 = 51 | 2 + 1 + 1 + 51 + 2 = **57** | +| DONE | 2 | **8** | +| REQUEST | 0 | **6** | diff --git a/components/protocol/protocol.c b/components/protocol/protocol.c new file mode 100644 index 0000000..c0e10ef --- /dev/null +++ b/components/protocol/protocol.c @@ -0,0 +1,490 @@ +/** + * @file protocol.c + * @brief EDC 通讯协议实现 — CRC16 / 组帧 / 解析 / I2C / UART + */ + +#include "protocol.h" +#include +#include "esp_log.h" +#include "freertos/task.h" + +static const char *TAG = "EDC_PROTO"; + +/* ================================================================ + * CRC-16/MODBUS + * + * 多项式: 0x8005 (逆序 0xA001) + * 初始值: 0xFFFF + * 输入/输出: 反射 + * 异或输出: 0x0000 + * 校验范围: CMD + LEN + DATA + * ================================================================ */ + +uint16_t edc_crc16(const uint8_t *data, size_t len) +{ + uint16_t crc = 0xFFFF; + for (size_t i = 0; i < len; i++) { + crc ^= data[i]; + for (int j = 0; j < 8; j++) { + if (crc & 0x0001) { + crc = (crc >> 1) ^ 0xA001; + } else { + crc >>= 1; + } + } + } + return crc; +} + +/* ================================================================ + * 大端序 uint16_t 写入 / 读取 + * ================================================================ */ + +static inline void write_u16_be(uint8_t *buf, uint16_t val) +{ + buf[0] = (uint8_t)(val >> 8); + buf[1] = (uint8_t)(val); +} + +static inline uint16_t read_u16_be(const uint8_t *buf) +{ + return ((uint16_t)buf[0] << 8) | buf[1]; +} + +/* ================================================================ + * Cycle 组帧 + * + * DATA 区: cycle_id(2B,BE) + frag_count(1B) + fragments(N×12B) + * Fragment: src.x(2B) src.y(2B) src.angle(2B) dst.x(2B) dst.y(2B) dst.angle(2B) + * ================================================================ */ + +int edc_cycle_pack(const edc_cycle_t *cycle, uint8_t *out) +{ + uint8_t count = cycle->frag_count; + if (count < EDC_FRAG_MIN) count = EDC_FRAG_MIN; + if (count > EDC_FRAG_MAX) count = EDC_FRAG_MAX; + + uint8_t data_len = 3 + count * EDC_FRAG_BYTES; /* id(2) + cnt(1) + frags */ + + out[0] = EDC_PROTO_SOF1; + out[1] = EDC_PROTO_SOF2; + out[2] = EDC_CMD_CYCLE_DATA; + out[3] = data_len; + + /* cycle_id (大端) */ + uint8_t *p = &out[4]; + write_u16_be(p, cycle->cycle_id); + p += 2; + + /* frag_count */ + *p++ = count; + + /* fragments */ + for (int i = 0; i < count; i++) { + const edc_fragment_t *f = &cycle->fragments[i]; + write_u16_be(p, f->source.x); p += 2; + write_u16_be(p, f->source.y); p += 2; + write_u16_be(p, f->source.angle_x10); p += 2; + write_u16_be(p, f->target.x); p += 2; + write_u16_be(p, f->target.y); p += 2; + write_u16_be(p, f->target.angle_x10); p += 2; + } + + /* CRC16 over [CMD, LEN, DATA] */ + uint16_t crc = edc_crc16(&out[2], 2 + data_len); + *p++ = (uint8_t)(crc); + *p++ = (uint8_t)(crc >> 8); + + return (int)(p - out); /* 2 + 1 + 1 + data_len + 2 */ +} + +/* ================================================================ + * Cycle 解析 (逐字节) + * ================================================================ */ + +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: + ctx->idx = 0; + ctx->state = EDC_PARSE_WAITING; + /* fall through */ + case EDC_PARSE_WAITING: + default: + if (byte == EDC_PROTO_SOF1) { + ctx->buf[0] = byte; + ctx->idx = 1; + } + return EDC_PARSE_WAITING; + } + + /* ---- 收帧中 (idx >= 1) ---- */ + + if (ctx->idx == 1) { + if (byte != EDC_PROTO_SOF2) { + ctx->idx = 0; + if (byte == EDC_PROTO_SOF1) { ctx->buf[0] = byte; ctx->idx = 1; } + return EDC_PARSE_WAITING; + } + ctx->buf[1] = byte; + ctx->idx = 2; + return EDC_PARSE_WAITING; + } + + if (ctx->idx < EDC_PROTO_FRAME_MAX) { + ctx->buf[ctx->idx] = byte; + } + ctx->idx++; + + /* 确定帧总长 */ + if (ctx->idx == 4) { + uint8_t data_len = ctx->buf[3]; + if (data_len < 3 || data_len > EDC_PROTO_FRAME_MAX - 6) { + ctx->idx = 0; ctx->state = EDC_PARSE_WAITING; + return EDC_PARSE_LEN_ERR; + } + ctx->len = 2 + 1 + 1 + data_len + 2; /* SOF+CMD+LEN+DATA+CRC */ + } + + /* 收满一帧 */ + if (ctx->idx >= ctx->len) { + ctx->idx = ctx->len; + + /* CRC 校验 */ + uint16_t crc_calc = edc_crc16(&ctx->buf[2], 2 + ctx->buf[3]); + uint16_t crc_recv = ctx->buf[ctx->len - 2] | ((uint16_t)ctx->buf[ctx->len - 1] << 8); + if (crc_calc != crc_recv) { + ctx->idx = 0; ctx->state = EDC_PARSE_WAITING; + return EDC_PARSE_CRC_ERR; + } + + /* 仅处理 CYCLE_DATA 命令 */ + uint8_t cmd = ctx->buf[2]; + if (cmd != EDC_CMD_CYCLE_DATA) { + ctx->idx = 0; ctx->state = EDC_PARSE_WAITING; + return EDC_PARSE_CMD_ERR; + } + + /* 解析 DATA 区 */ + const uint8_t *d = &ctx->buf[4]; + cycle->cycle_id = read_u16_be(d); d += 2; + cycle->frag_count = *d; d += 1; + + if (cycle->frag_count < EDC_FRAG_MIN || cycle->frag_count > EDC_FRAG_MAX) { + ctx->idx = 0; ctx->state = EDC_PARSE_WAITING; + return EDC_PARSE_LEN_ERR; + } + + for (int i = 0; i < cycle->frag_count; i++) { + edc_fragment_t *f = &cycle->fragments[i]; + f->source.x = read_u16_be(d); d += 2; + f->source.y = read_u16_be(d); d += 2; + f->source.angle_x10 = read_u16_be(d); d += 2; + f->target.x = read_u16_be(d); d += 2; + f->target.y = read_u16_be(d); d += 2; + f->target.angle_x10 = read_u16_be(d); d += 2; + } + cycle->timestamp_ms = 0; + + ctx->state = EDC_PARSE_OK; + return EDC_PARSE_OK; + } + + return EDC_PARSE_WAITING; +} + +/* ================================================================ + * Cycle 解析 (一次性) + * ================================================================ */ + +edc_parse_result_t edc_cycle_parse_buf(const uint8_t *data, size_t len, + edc_cycle_t *cycle) +{ + edc_frame_ctx_t ctx = { .state = EDC_PARSE_WAITING }; + edc_parse_result_t res; + for (size_t i = 0; i < len; i++) { + res = edc_cycle_parse_byte(&ctx, data[i], cycle); + if (res != EDC_PARSE_WAITING) return res; + } + return EDC_PARSE_WAITING; +} + +/* ================================================================ + * 统一收发器 + * ================================================================ */ + +/* ---- 共享最新 Cycle ---- */ +static edc_cycle_t s_latest_cycle; +static volatile bool s_cycle_updated; +static portMUX_TYPE s_lock = portMUX_INITIALIZER_UNLOCKED; + +/* ---- 统计 ---- */ +static edc_proto_stat_t s_stat; + +/* ---- 通道使能 ---- */ +static bool s_i2c_enabled; +static bool s_uart_enabled; + +/* ---- I2C ---- */ +static i2c_slave_dev_handle_t s_i2c_handle; +static edc_cycle_t s_i2c_rx_cycle; +static volatile bool s_i2c_rx_pending; + +/* ---- UART ---- */ +static uart_port_t s_uart_port; +static edc_frame_ctx_t s_uart_ctx; + +/* ================================================================ + * 内部: 存入共享 Cycle + * ================================================================ */ + +static void store_cycle(const edc_cycle_t *cycle, edc_channel_t ch) +{ + portENTER_CRITICAL(&s_lock); + memcpy(&s_latest_cycle, cycle, sizeof(edc_cycle_t)); + s_latest_cycle.timestamp_ms = xTaskGetTickCount() * portTICK_PERIOD_MS; + s_cycle_updated = true; + portEXIT_CRITICAL(&s_lock); + + if (ch == EDC_CH_I2C) s_stat.i2c.cycles_ok++; + else if (ch == EDC_CH_UART) s_stat.uart.cycles_ok++; + s_stat.last_channel = ch; + s_stat.last_tick = xTaskGetTickCount(); +} + +/* ================================================================ + * I2C 从机 接收 + * ================================================================ */ + +static bool on_i2c_rx(i2c_slave_dev_handle_t handle, + const i2c_slave_rx_done_event_data_t *evt, + void *arg) +{ + s_stat.i2c.bytes_total += evt->length; + + edc_cycle_t cycle; + if (edc_cycle_parse_buf(evt->buffer, evt->length, &cycle) == EDC_PARSE_OK) { + memcpy(&s_i2c_rx_cycle, &cycle, sizeof(edc_cycle_t)); + s_i2c_rx_cycle.timestamp_ms = xTaskGetTickCountFromISR() * portTICK_PERIOD_MS; + s_i2c_rx_pending = true; + } + return false; +} + +static bool i2c_poll_once(void) +{ + if (!s_i2c_rx_pending) return false; + + edc_cycle_t cycle; + portENTER_CRITICAL(&s_lock); + memcpy(&cycle, &s_i2c_rx_cycle, sizeof(edc_cycle_t)); + s_i2c_rx_pending = false; + portEXIT_CRITICAL(&s_lock); + + store_cycle(&cycle, EDC_CH_I2C); + ESP_LOGD(TAG, "I2C cycle OK (id=%u frags=%u)", cycle.cycle_id, cycle.frag_count); + return true; +} + +/* ================================================================ + * UART 接收 + * ================================================================ */ + +static bool uart_poll_once(void) +{ + uint8_t byte; + bool got = false; + + while (uart_read_bytes(s_uart_port, &byte, 1, pdMS_TO_TICKS(0)) > 0) { + s_stat.uart.bytes_total++; + + edc_cycle_t cycle; + edc_parse_result_t res = edc_cycle_parse_byte(&s_uart_ctx, byte, &cycle); + if (res == EDC_PARSE_OK) { + store_cycle(&cycle, EDC_CH_UART); + got = true; + ESP_LOGD(TAG, "UART cycle OK (id=%u frags=%u)", cycle.cycle_id, cycle.frag_count); + } else if (res < 0) { + s_stat.uart.cycles_err++; + ESP_LOGW(TAG, "UART parse err: %d", res); + } + } + return got; +} + +/* ================================================================ + * 内部: 通过 UART 发送短帧 + * ================================================================ */ + +static void uart_send_frame(uint8_t cmd, const uint8_t *data, uint8_t data_len) +{ + uint8_t buf[EDC_PROTO_FRAME_MAX]; + buf[0] = EDC_PROTO_SOF1; + buf[1] = EDC_PROTO_SOF2; + buf[2] = cmd; + buf[3] = data_len; + if (data && data_len) memcpy(&buf[4], data, data_len); + + uint16_t crc = edc_crc16(&buf[2], 2 + data_len); + buf[4 + data_len] = (uint8_t)(crc); + buf[4 + data_len + 1] = (uint8_t)(crc >> 8); + + int total = 4 + data_len + 2; + uart_write_bytes(s_uart_port, (const char *)buf, total); +} + +/* ================================================================ + * 初始化 + * ================================================================ */ + +void edc_proto_init(const edc_proto_config_t *cfg) +{ + edc_proto_config_t dft = { + .i2c_enable = true, + .i2c_port = EDC_I2C_PORT_DEFAULT, + .i2c_addr = EDC_I2C_SLAVE_ADDR, + .i2c_sda = EDC_I2C_SDA_DEFAULT, + .i2c_scl = EDC_I2C_SCL_DEFAULT, + .uart_enable = true, + .uart_port = EDC_UART_PORT_DEFAULT, + .uart_baudrate = EDC_UART_BAUDRATE, + .uart_rx = EDC_UART_RX_DEFAULT, + .uart_tx = EDC_UART_TX_DEFAULT, + }; + if (cfg) dft = *cfg; + + s_i2c_enabled = dft.i2c_enable; + s_uart_enabled = dft.uart_enable; + memset(&s_stat, 0, sizeof(s_stat)); + + /* I2C */ + if (s_i2c_enabled) { + i2c_slave_config_t i2c_cfg = { + .i2c_port = dft.i2c_port, + .sda_io_num = dft.i2c_sda, + .scl_io_num = dft.i2c_scl, + .clk_source = I2C_CLK_SRC_DEFAULT, + .send_buf_depth = 0, + .receive_buf_depth = EDC_I2C_BUF_SIZE, + .slave_addr = dft.i2c_addr, + .addr_bit_len = I2C_ADDR_BIT_LEN_7, + .intr_priority = 0, + .flags.enable_internal_pullup = true, + }; + ESP_ERROR_CHECK(i2c_new_slave_device(&i2c_cfg, &s_i2c_handle)); + + i2c_slave_event_callbacks_t cbs = { + .on_receive = on_i2c_rx, + .on_request = NULL, + }; + ESP_ERROR_CHECK(i2c_slave_register_event_callbacks(s_i2c_handle, &cbs, NULL)); + ESP_LOGI(TAG, "I2C slave OK (0x%02X SDA=%d SCL=%d)", + dft.i2c_addr, dft.i2c_sda, dft.i2c_scl); + } + + /* UART */ + if (s_uart_enabled) { + s_uart_port = dft.uart_port; + memset(&s_uart_ctx, 0, sizeof(s_uart_ctx)); + s_uart_ctx.state = EDC_PARSE_WAITING; + + uart_config_t uart_cfg = { + .baud_rate = dft.uart_baudrate, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE, + .source_clk = UART_SCLK_DEFAULT, + }; + ESP_ERROR_CHECK(uart_driver_install(dft.uart_port, + EDC_UART_BUF_SIZE, EDC_UART_BUF_SIZE, + 0, NULL, 0)); + ESP_ERROR_CHECK(uart_param_config(dft.uart_port, &uart_cfg)); + ESP_ERROR_CHECK(uart_set_pin(dft.uart_port, + dft.uart_tx, dft.uart_rx, + UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE)); + ESP_LOGI(TAG, "UART OK (port=%d baud=%d RX=%d TX=%d)", + dft.uart_port, dft.uart_baudrate, dft.uart_rx, dft.uart_tx); + } +} + +/* ================================================================ + * 轮询 + * ================================================================ */ + +int edc_proto_poll(edc_cycle_t *cycle) +{ + bool got = false; + if (s_i2c_enabled) got |= i2c_poll_once(); + if (s_uart_enabled) got |= uart_poll_once(); + + if (got) { + portENTER_CRITICAL(&s_lock); + memcpy(cycle, &s_latest_cycle, sizeof(edc_cycle_t)); + s_cycle_updated = false; + portEXIT_CRITICAL(&s_lock); + return 1; + } + return 0; +} + +/* ================================================================ + * 发送 DONE / REQUEST + * ================================================================ */ + +void edc_proto_send_done(uint16_t cycle_id) +{ + if (!s_uart_enabled) return; + uint8_t data[2]; + write_u16_be(data, cycle_id); + uart_send_frame(EDC_CMD_CYCLE_DONE, data, 2); + s_stat.done_sent++; + ESP_LOGI(TAG, "DONE sent (cycle_id=%u)", cycle_id); +} + +void edc_proto_send_request(void) +{ + if (!s_uart_enabled) return; + uart_send_frame(EDC_CMD_CYCLE_REQUEST, NULL, 0); + s_stat.request_sent++; + ESP_LOGI(TAG, "REQUEST sent"); +} + +/* ================================================================ + * 统计 + * ================================================================ */ + +void edc_proto_get_stats(edc_proto_stat_t *stat) +{ + memcpy(stat, &s_stat, sizeof(edc_proto_stat_t)); +} + +/* ================================================================ + * 弱回调 + * ================================================================ */ + +__attribute__((weak)) +void edc_proto_on_cycle(const edc_cycle_t *cycle, edc_channel_t ch) +{ + (void)cycle; (void)ch; +} + +/* ================================================================ + * FreeRTOS 任务 + * ================================================================ */ + +void edc_proto_task(void *pvParam) +{ + edc_cycle_t cycle; + while (1) { + if (edc_proto_poll(&cycle) > 0) { + edc_channel_t ch; + portENTER_CRITICAL(&s_lock); + ch = s_stat.last_channel; + portEXIT_CRITICAL(&s_lock); + edc_proto_on_cycle(&cycle, ch); + } + vTaskDelay(pdMS_TO_TICKS(2)); + } +} diff --git a/components/protocol/protocol.h b/components/protocol/protocol.h new file mode 100644 index 0000000..17fa747 --- /dev/null +++ b/components/protocol/protocol.h @@ -0,0 +1,233 @@ +/** + * @file protocol.h + * @brief EDC 通讯协议 — I2C / UART 双通道, Cycle 收发, CRC16 校验 + * + * 顶层概念: + * Point = { x, y, angle_x10 } + * Fragment = { source: Point, target: Point } + * Cycle = { cycle_id, frag_count (2~4), fragments[] } + * + * 帧格式 (大端序): + * ┌──────┬──────┬──────┬──────┬───────────────────┬──────────┐ + * │ SOF1 │ SOF2 │ CMD │ LEN │ DATA (N bytes) │ CRC16 │ + * │ 0xAA │ 0x55 │ 1B │ 1B │ variable │ 2B (LE) │ + * └──────┴──────┴──────┴──────┴───────────────────┴──────────┘ + * ◄──────── CRC 计算范围 (CMD + LEN + DATA) ─────────► + * + * Cycle 数据帧 DATA 区: + * cycle_id(2B, BE) + frag_count(1B, 2~4) + fragments(N × 12B) + * + * Fragment (12B, 大端): + * src.x(2B) src.y(2B) src.angle(2B) dst.x(2B) dst.y(2B) dst.angle(2B) + * + * 交互流程: + * MPU ──[CYCLE_DATA]──► MCU (I2C + UART 冗余下发) + * MCU ──[CYCLE_DONE]──► MPU (UART 上报完成, 含 cycle_id) + * MCU ──[CYCLE_REQ]───► MPU (UART 请求下一 Cycle) + */ + +#ifndef EDC_PROTOCOL_H +#define EDC_PROTOCOL_H + +#include +#include +#include +#include "driver/gpio.h" +#include "driver/uart.h" +#include "driver/i2c_slave.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* ================================================================ + * 协议常量 + * ================================================================ */ + +#define EDC_PROTO_SOF1 0xAA +#define EDC_PROTO_SOF2 0x55 + +#define EDC_PROTO_FRAME_MAX 80 /* 最大帧长: 2+1+1+3+4*12+2 = 57 < 80 */ +#define EDC_PROTO_FRAME_MIN 6 /* SOF(2) + CMD(1) + LEN(1) + CRC(2) */ + +#define EDC_FRAG_MIN 2 /* 每 Cycle 最少 fragment 数 */ +#define EDC_FRAG_MAX 4 /* 每 Cycle 最多 fragment 数 */ +#define EDC_FRAG_BYTES 12 /* 单个 fragment 字节数 (3×uint16×2) */ + +/* ---- I2C 默认 ---- */ +#define EDC_I2C_PORT_DEFAULT 0 +#define EDC_I2C_SLAVE_ADDR 0x2A +#define EDC_I2C_SDA_DEFAULT GPIO_NUM_2 +#define EDC_I2C_SCL_DEFAULT GPIO_NUM_3 +#define EDC_I2C_BUF_SIZE 256 + +/* ---- 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_BUF_SIZE 512 + +/* ================================================================ + * 命令字 + * ================================================================ */ + +typedef enum { + EDC_CMD_CYCLE_DATA = 0x01, /* MPU → MCU: 下发 Cycle 数据 */ + EDC_CMD_CYCLE_DONE = 0x02, /* MCU → MPU: Cycle 执行完成 */ + EDC_CMD_CYCLE_REQUEST = 0x03, /* MCU → MPU: 请求下一个 Cycle */ + EDC_CMD_ACK = 0x80, /* 通用应答 */ + EDC_CMD_NACK = 0xFF, /* 错误 */ +} edc_cmd_t; + +/* ================================================================ + * 数据结构 + * ================================================================ */ + +/** 单个坐标点 (source 或 target) */ +typedef struct { + uint16_t x; + uint16_t y; + uint16_t angle_x10; /* 角度 × 10, 如 450 = 45.0° */ +} edc_point_t; + +/** 一个运动片段: 从 source 移动到 target */ +typedef struct { + edc_point_t source; + edc_point_t target; +} edc_fragment_t; + +/** 一个 Cycle: 包含 2~4 个 fragment */ +typedef struct { + uint16_t cycle_id; + uint8_t frag_count; /* 实际 fragment 数 (2~4) */ + edc_fragment_t fragments[EDC_FRAG_MAX]; + uint32_t timestamp_ms; /* 本地时间戳 (不随帧传输) */ +} edc_cycle_t; + +/** 帧来源通道 */ +typedef enum { + EDC_CH_NONE = 0, + EDC_CH_I2C = 1, + EDC_CH_UART = 2, +} edc_channel_t; + +/** 每通道统计 */ +typedef struct { + uint32_t cycles_ok; + uint32_t cycles_err; + uint32_t bytes_total; +} edc_channel_stat_t; + +/** 统一收发统计 */ +typedef struct { + edc_channel_stat_t i2c; + edc_channel_stat_t uart; + edc_channel_t last_channel; + uint32_t last_tick; + uint32_t done_sent; /* 已发送 DONE 次数 */ + uint32_t request_sent; /* 已发送 REQUEST 次数 */ +} edc_proto_stat_t; + +/** 帧解析状态 */ +typedef enum { + EDC_PARSE_OK = 0, + EDC_PARSE_WAITING = 1, + EDC_PARSE_CRC_ERR = -1, + EDC_PARSE_LEN_ERR = -2, + EDC_PARSE_SOF_ERR = -3, + EDC_PARSE_CMD_ERR = -4, +} edc_parse_result_t; + +/** 帧解析上下文 (逐字节喂入) */ +typedef struct { + uint8_t buf[EDC_PROTO_FRAME_MAX]; + uint8_t len; + uint8_t idx; + edc_parse_result_t state; +} edc_frame_ctx_t; + +/* ================================================================ + * 配置 + * ================================================================ */ + +typedef struct { + bool i2c_enable; + int i2c_port; + uint16_t i2c_addr; + gpio_num_t i2c_sda; + gpio_num_t i2c_scl; + + bool uart_enable; + uart_port_t uart_port; + int uart_baudrate; + gpio_num_t uart_rx; + gpio_num_t uart_tx; +} edc_proto_config_t; + +/* ================================================================ + * CRC16 / 组帧 / 逐字节解析 (底层) + * ================================================================ */ + +uint16_t edc_crc16(const uint8_t *data, size_t len); + +/** 将 Cycle 序列化为协议字节流 (含帧头与 CRC) */ +int edc_cycle_pack(const edc_cycle_t *cycle, uint8_t *out); + +/** 逐字节解析器 (与旧版兼容, 内部使用 edc_cycle_parse_byte) */ +edc_parse_result_t edc_cycle_parse_byte(edc_frame_ctx_t *ctx, uint8_t byte, + edc_cycle_t *cycle); + +/** 从完整缓冲区一次性解析 Cycle */ +edc_parse_result_t edc_cycle_parse_buf(const uint8_t *data, size_t len, + edc_cycle_t *cycle); + +/* ================================================================ + * 统一收发器 API (I2C 从机 + UART 双向) + * ================================================================ */ + +/** + * @brief 初始化协议栈 (I2C 从机 + UART) + * @param cfg 传 NULL 使用默认值 + */ +void edc_proto_init(const edc_proto_config_t *cfg); + +/** + * @brief 轮询双通道接收 + 返回最新 Cycle (非阻塞) + * @return >0: 有新 Cycle; 0: 无; <0: 错误 + */ +int edc_proto_poll(edc_cycle_t *cycle); + +/** + * @brief 发送 CYCLE_DONE (通过 UART) + * @param cycle_id 已完成的 Cycle ID + */ +void edc_proto_send_done(uint16_t cycle_id); + +/** + * @brief 发送 CYCLE_REQUEST (通过 UART) + */ +void edc_proto_send_request(void); + +/** + * @brief 获取统计信息 + */ +void edc_proto_get_stats(edc_proto_stat_t *stat); + +/* ---- FreeRTOS 任务 + 回调 ---- */ + +void edc_proto_task(void *pvParam); + +/** + * @brief Cycle 回调 (弱符号, 用户实现) + * @param cycle 已校验的有效 Cycle + * @param ch 来源通道 + */ +void edc_proto_on_cycle(const edc_cycle_t *cycle, edc_channel_t ch); + +#ifdef __cplusplus +} +#endif + +#endif /* EDC_PROTOCOL_H */ + diff --git a/components/servo/CMakeLists.txt b/components/servo/CMakeLists.txt new file mode 100644 index 0000000..1d2c363 --- /dev/null +++ b/components/servo/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "servo.c" + INCLUDE_DIRS "." + REQUIRES esp_driver_ledc esp_driver_gpio driver) diff --git a/components/servo/servo.c b/components/servo/servo.c new file mode 100644 index 0000000..03dcf27 --- /dev/null +++ b/components/servo/servo.c @@ -0,0 +1,89 @@ +/** + * @file servo.c + * @brief 180° PWM 舵机控制实现 (LEDC, 50Hz, 0.5~2.5ms) + */ + +#include "servo.h" +#include "driver/ledc.h" +#include "esp_log.h" + +#define SERVO_FREQ_HZ 50 +#define SERVO_TIMER LEDC_TIMER_0 +#define SERVO_MODE LEDC_LOW_SPEED_MODE +#define SERVO_DUTY_RES LEDC_TIMER_16_BIT /* 0~65535 */ +#define SERVO_MAX_DUTY 65535 + +/* 脉宽范围 (微秒) */ +#define PULSE_MIN_US 500 +#define PULSE_MAX_US 2500 +#define PERIOD_US 20000 /* 50Hz = 20ms */ + +static const char *TAG = "SERVO"; +static ledc_channel_t s_channel; +static float s_limit_up = 180.0f; +static float s_limit_down = 0.0f; + +/* 角度 → 占空比 */ +static uint32_t angle_to_duty(float angle) +{ + if (angle < 0.0f) angle = 0.0f; + if (angle > 180.0f) angle = 180.0f; + + float pulse_us = PULSE_MIN_US + (angle / 180.0f) * (PULSE_MAX_US - PULSE_MIN_US); + return (uint32_t)((pulse_us / PERIOD_US) * SERVO_MAX_DUTY); +} + +void servo_init(int gpio) +{ + /* 分配 LEDC 通道 */ + static int chan_idx = 0; + s_channel = (chan_idx++ == 0) ? LEDC_CHANNEL_0 : LEDC_CHANNEL_1; + + ledc_timer_config_t timer_cfg = { + .speed_mode = SERVO_MODE, + .duty_resolution = SERVO_DUTY_RES, + .timer_num = SERVO_TIMER, + .freq_hz = SERVO_FREQ_HZ, + .clk_cfg = LEDC_AUTO_CLK, + }; + ESP_ERROR_CHECK(ledc_timer_config(&timer_cfg)); + + ledc_channel_config_t chan_cfg = { + .gpio_num = gpio, + .speed_mode = SERVO_MODE, + .channel = s_channel, + .intr_type = LEDC_INTR_DISABLE, + .timer_sel = SERVO_TIMER, + .duty = angle_to_duty(0), /* 初始: 0° */ + .hpoint = 0, + .flags.output_invert = 0, + }; + ESP_ERROR_CHECK(ledc_channel_config(&chan_cfg)); + + ESP_LOGI(TAG, "初始化完成 (GPIO%d, 50Hz, 通道%d)", gpio, s_channel); +} + +void servo_set_angle(float angle) +{ + uint32_t duty = angle_to_duty(angle); + ESP_ERROR_CHECK(ledc_set_duty(SERVO_MODE, s_channel, duty)); + ESP_ERROR_CHECK(ledc_update_duty(SERVO_MODE, s_channel)); + ESP_LOGD(TAG, "角度: %.1f° (duty=%lu)", angle, duty); +} + +void servo_set_up(void) +{ + servo_set_angle(s_limit_up); +} + +void servo_set_down(void) +{ + servo_set_angle(s_limit_down); +} + +void servo_set_limits(float up, float down) +{ + s_limit_up = up; + s_limit_down = down; + ESP_LOGI(TAG, "限位: 上=%.0f° 下=%.0f°", up, down); +} diff --git a/components/servo/servo.h b/components/servo/servo.h new file mode 100644 index 0000000..62c787f --- /dev/null +++ b/components/servo/servo.h @@ -0,0 +1,49 @@ +/** + * @file servo.h + * @brief 180° PWM 舵机控制 (LEDC) + */ + +#ifndef SERVO_H +#define SERVO_H + +#include +#include "driver/gpio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief 初始化舵机 (50Hz PWM) + * @param gpio PWM 输出引脚 + */ +void servo_init(int gpio); + +/** + * @brief 设置舵机角度 + * @param angle 角度 0.0 ~ 180.0 + */ +void servo_set_angle(float angle); + +/** + * @brief 设置舵机到上限位 (默认 180°) + */ +void servo_set_up(void); + +/** + * @brief 设置舵机到下限位 (默认 0°) + */ +void servo_set_down(void); + +/** + * @brief 自定义上下限位角度 + * @param up 上限位角度 (默认 180°) + * @param down 下限位角度 (默认 0°) + */ +void servo_set_limits(float up, float down); + +#ifdef __cplusplus +} +#endif + +#endif /* SERVO_H */ diff --git a/components/web_ctrl/CMakeLists.txt b/components/web_ctrl/CMakeLists.txt new file mode 100644 index 0000000..acc97f7 --- /dev/null +++ b/components/web_ctrl/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "web_ctrl.c" + INCLUDE_DIRS "." + REQUIRES emm_v5 magnet servo esp_http_server esp_wifi nvs_flash) diff --git a/components/web_ctrl/web_ctrl.c b/components/web_ctrl/web_ctrl.c new file mode 100644 index 0000000..961a3a5 --- /dev/null +++ b/components/web_ctrl/web_ctrl.c @@ -0,0 +1,546 @@ +/** + * @file web_ctrl.c + * @brief WiFi STA + HTTP 服务器 + 网页控制面板 + */ + +#include "web_ctrl.h" +#include "emm_v5.h" +#include "magnet.h" +#include "servo.h" + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" +#include "esp_wifi.h" +#include "esp_event.h" +#include "esp_http_server.h" +#include "esp_log.h" +#include "esp_netif.h" +#include "nvs_flash.h" + +static const char *TAG = "WEB_CTRL"; + +/* ---- WiFi 配置 ---- */ +#define WIFI_SSID "ICE" +#define WIFI_PASS "12345678" +#define WIFI_MAX_RETRY 5 +#define WIFI_CONNECT_TIMEOUT_MS 30000 + +static EventGroupHandle_t s_wifi_event_group; +#define WIFI_CONNECTED_BIT BIT0 +#define WIFI_FAIL_BIT BIT1 +static int s_retry_num = 0; + +/* 保存 IP 供 /api/status 使用 */ +static char s_ip_str[16] = "0.0.0.0"; + +/* ---- HTML 页面 ---- */ +static const char INDEX_HTML[] = R"raw( + + + + +EDC 步进电机控制 + + + +
+

EDC 步进电机控制

+
+ WiFi: -- + IP: -- +
+
+ +
+
+ +
+

电机使能

+
+
+ + +
+
+ +
+

速度模式

+
+
+
RPM
+
+
+ + +
+
+ +
+

位置模式

+
+
+
RPM
+
+
+
+
+ + +
+
+ +
+

舵机控制

+
+ + + 90° +
+ +
+ + +
+
+ +
+

电磁铁

+
+ + +
+
+ +
+ +
+

运行日志

+
等待指令...
+
+
+ + + +)raw"; + +/* ============================================================ + * WiFi 事件处理 + * ============================================================ */ + +static void wifi_event_handler(void *arg, esp_event_base_t base, + int32_t id, void *data) +{ + if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + } else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) { + wifi_event_sta_disconnected_t *ev = (wifi_event_sta_disconnected_t *)data; + ESP_LOGW(TAG, "WiFi 断开 (reason=%d), 重试 %d/%d", + ev->reason, s_retry_num + 1, WIFI_MAX_RETRY); + if (s_retry_num < WIFI_MAX_RETRY) { + s_retry_num++; + esp_wifi_connect(); + } else { + xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); + } + } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t *ev = (ip_event_got_ip_t *)data; + snprintf(s_ip_str, sizeof(s_ip_str), IPSTR, IP2STR(&ev->ip_info.ip)); + ESP_LOGI(TAG, "========================================"); + ESP_LOGI(TAG, " WiFi 已连接!"); + ESP_LOGI(TAG, " SSID: %s", WIFI_SSID); + ESP_LOGI(TAG, " IP: %s", s_ip_str); + ESP_LOGI(TAG, " 浏览器打开 http://%s", s_ip_str); + ESP_LOGI(TAG, "========================================"); + s_retry_num = 0; + xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + } +} + +static void wifi_init_sta(void) +{ + s_wifi_event_group = xEventGroupCreate(); + + ESP_ERROR_CHECK(nvs_flash_init()); + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_netif_create_default_wifi_sta(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + esp_event_handler_instance_t inst_any_id, inst_got_ip; + ESP_ERROR_CHECK(esp_event_handler_instance_register( + WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, &inst_any_id)); + ESP_ERROR_CHECK(esp_event_handler_instance_register( + IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL, &inst_got_ip)); + + wifi_config_t wifi_cfg = { + .sta = { + .ssid = WIFI_SSID, + .password = WIFI_PASS, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg)); + ESP_ERROR_CHECK(esp_wifi_start()); + + ESP_LOGI(TAG, "正在连接 WiFi: %s ...", WIFI_SSID); + + /* 等待连接成功或失败 (30 秒超时) */ + EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, + WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, + pdFALSE, pdFALSE, pdMS_TO_TICKS(WIFI_CONNECT_TIMEOUT_MS)); + + if (bits & WIFI_CONNECTED_BIT) { + ESP_LOGI(TAG, "WiFi 就绪 (IP: %s)", s_ip_str); + } else if (bits & WIFI_FAIL_BIT) { + ESP_LOGE(TAG, "WiFi 连接失败! 请检查 SSID/密码"); + } else { + ESP_LOGE(TAG, "WiFi 连接超时 (%d秒)!", WIFI_CONNECT_TIMEOUT_MS / 1000); + } + /* 无论 WiFi 是否成功, 都继续启动 HTTP 服务器 */ +} + +/* ============================================================ + * HTTP 查询字符串解析 + * ============================================================ */ + +static int get_query_int(httpd_req_t *req, const char *key, int def) +{ + char buf[128]; + size_t len = httpd_req_get_url_query_len(req) + 1; + if (len > sizeof(buf)) len = sizeof(buf); + if (httpd_req_get_url_query_str(req, buf, len) == ESP_OK) { + char val[16]; + if (httpd_query_key_value(buf, key, val, sizeof(val)) == ESP_OK) { + return atoi(val); + } + } + return def; +} + +/* ============================================================ + * HTTP 处理器 + * ============================================================ */ + +static esp_err_t root_handler(httpd_req_t *req) +{ + const char *html = INDEX_HTML; + size_t total = strlen(html); + size_t offset = 0; + + httpd_resp_set_type(req, "text/html; charset=utf-8"); + httpd_resp_set_hdr(req, "Connection", "close"); + + while (offset < total) { + size_t chunk = total - offset; + if (chunk > 1024) chunk = 1024; + esp_err_t err = httpd_resp_send_chunk(req, html + offset, (int)chunk); + if (err != ESP_OK) { + ESP_LOGE(TAG, "发送 HTML 失败: %d", err); + return err; + } + offset += chunk; + } + httpd_resp_send_chunk(req, NULL, 0); /* 结束分块 */ + return ESP_OK; +} + +static esp_err_t api_enable_handler(httpd_req_t *req) +{ + int addr = get_query_int(req, "addr", 1); + int state = get_query_int(req, "state", 0); + Emm_V5_En_Control((uint8_t)addr, (bool)state, false); + char resp[64]; + snprintf(resp, sizeof(resp), "电机 %d %s", addr, state ? "使能" : "关闭"); + httpd_resp_set_type(req, "text/plain; charset=utf-8"); + httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); + return ESP_OK; +} + +static esp_err_t api_velocity_handler(httpd_req_t *req) +{ + int addr = get_query_int(req, "addr", 1); + int dir = get_query_int(req, "dir", 0); + int vel = get_query_int(req, "vel", 100); + int acc = get_query_int(req, "acc", 10); + Emm_V5_Vel_Control((uint8_t)addr, (uint8_t)dir, (uint16_t)vel, (uint8_t)acc, false); + char resp[64]; + snprintf(resp, sizeof(resp), "速度模式: 地址%d %s %dRPM acc=%d", + addr, dir ? "CCW" : "CW", vel, acc); + httpd_resp_set_type(req, "text/plain; charset=utf-8"); + httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); + return ESP_OK; +} + +static esp_err_t api_position_handler(httpd_req_t *req) +{ + int addr = get_query_int(req, "addr", 1); + int dir = get_query_int(req, "dir", 0); + int vel = get_query_int(req, "vel", 100); + int acc = get_query_int(req, "acc", 10); + int clk = get_query_int(req, "clk", 2000); + int raf = get_query_int(req, "raf", 0); + Emm_V5_Pos_Control((uint8_t)addr, (uint8_t)dir, (uint16_t)vel, (uint8_t)acc, + (uint32_t)clk, (bool)raf, false); + char resp[80]; + snprintf(resp, sizeof(resp), "位置模式: 地址%d %s %dRPM %d脉冲 %s", + addr, dir ? "CCW" : "CW", vel, clk, raf ? "绝对" : "相对"); + httpd_resp_set_type(req, "text/plain; charset=utf-8"); + httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); + return ESP_OK; +} + +static esp_err_t api_stop_handler(httpd_req_t *req) +{ + int addr = get_query_int(req, "addr", 1); + Emm_V5_Stop_Now((uint8_t)addr, false); + char resp[32]; + snprintf(resp, sizeof(resp), "电机 %d 已停止", addr); + httpd_resp_set_type(req, "text/plain; charset=utf-8"); + httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); + return ESP_OK; +} + +static esp_err_t api_magnet_handler(httpd_req_t *req) +{ + int state = get_query_int(req, "state", 0); + magnet_set((bool)state); + char resp[32]; + snprintf(resp, sizeof(resp), "电磁铁 %s", state ? "吸合" : "释放"); + httpd_resp_set_type(req, "text/plain; charset=utf-8"); + httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); + return ESP_OK; +} + +static esp_err_t api_status_handler(httpd_req_t *req) +{ + char json[128]; + wifi_ap_record_t ap; + esp_err_t err = esp_wifi_sta_get_ap_info(&ap); + + snprintf(json, sizeof(json), + "{\"ssid\":\"%s\",\"ip\":\"%s\"}", + (err == ESP_OK) ? (char *)ap.ssid : "N/A", + s_ip_str); + httpd_resp_set_type(req, "application/json"); + httpd_resp_send(req, json, HTTPD_RESP_USE_STRLEN); + return ESP_OK; +} + +static esp_err_t api_servo_handler(httpd_req_t *req) +{ + char buf[128]; + size_t len = httpd_req_get_url_query_len(req) + 1; + if (len > sizeof(buf)) len = sizeof(buf); + + if (httpd_req_get_url_query_str(req, buf, len) == ESP_OK) { + char val[16]; + if (httpd_query_key_value(buf, "pos", val, sizeof(val)) == ESP_OK) { + if (strcmp(val, "up") == 0) { + servo_set_up(); + } else { + servo_set_down(); + } + char resp[32]; + snprintf(resp, sizeof(resp), "舵机: %s", val); + httpd_resp_set_type(req, "text/plain; charset=utf-8"); + httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); + return ESP_OK; + } + if (httpd_query_key_value(buf, "angle", val, sizeof(val)) == ESP_OK) { + float angle = atof(val); + servo_set_angle(angle); + char resp[32]; + snprintf(resp, sizeof(resp), "舵机: %.1f°", (double)angle); + 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, "缺少 angle 或 pos 参数"); + return ESP_FAIL; +} + +/* favicon — 204 No Content, 避免浏览器反复请求 */ +static esp_err_t favicon_handler(httpd_req_t *req) +{ + httpd_resp_set_status(req, "204 No Content"); + httpd_resp_send(req, NULL, 0); + return ESP_OK; +} + +static void http_server_init(void) +{ + httpd_handle_t server = NULL; + httpd_config_t cfg = HTTPD_DEFAULT_CONFIG(); + cfg.stack_size = 8192; /* 默认 4096 不够, 增大 */ + cfg.max_uri_handlers = 16; + cfg.max_open_sockets = 4; + cfg.lru_purge_enable = true; + + ESP_ERROR_CHECK(httpd_start(&server, &cfg)); + + httpd_uri_t uri_root = { + .uri = "/", + .method = HTTP_GET, + .handler = root_handler, + .user_ctx = NULL + }; + httpd_register_uri_handler(server, &uri_root); + + httpd_uri_t uri_enable = { + .uri = "/api/enable", + .method = HTTP_GET, + .handler = api_enable_handler, + .user_ctx = NULL + }; + httpd_register_uri_handler(server, &uri_enable); + + httpd_uri_t uri_velocity = { + .uri = "/api/velocity", + .method = HTTP_GET, + .handler = api_velocity_handler, + .user_ctx = NULL + }; + httpd_register_uri_handler(server, &uri_velocity); + + httpd_uri_t uri_position = { + .uri = "/api/position", + .method = HTTP_GET, + .handler = api_position_handler, + .user_ctx = NULL + }; + httpd_register_uri_handler(server, &uri_position); + + httpd_uri_t uri_stop = { + .uri = "/api/stop", + .method = HTTP_GET, + .handler = api_stop_handler, + .user_ctx = NULL + }; + httpd_register_uri_handler(server, &uri_stop); + + httpd_uri_t uri_magnet = { + .uri = "/api/magnet", + .method = HTTP_GET, + .handler = api_magnet_handler, + .user_ctx = NULL + }; + httpd_register_uri_handler(server, &uri_magnet); + + httpd_uri_t uri_status = { + .uri = "/api/status", + .method = HTTP_GET, + .handler = api_status_handler, + .user_ctx = NULL + }; + httpd_register_uri_handler(server, &uri_status); + + httpd_uri_t uri_servo = { + .uri = "/api/servo", + .method = HTTP_GET, + .handler = api_servo_handler, + .user_ctx = NULL + }; + httpd_register_uri_handler(server, &uri_servo); + + /* favicon.ico — 返回空, 避免 404 警告 */ + httpd_uri_t uri_favicon = { + .uri = "/favicon.ico", + .method = HTTP_GET, + .handler = favicon_handler, + .user_ctx = NULL + }; + httpd_register_uri_handler(server, &uri_favicon); + + ESP_LOGI(TAG, "HTTP 服务器已启动 (端口 %d)", cfg.server_port); +} + +/* ============================================================ + * 公开接口 + * ============================================================ */ + +void web_ctrl_init(void) +{ + wifi_init_sta(); + http_server_init(); +} diff --git a/components/web_ctrl/web_ctrl.h b/components/web_ctrl/web_ctrl.h new file mode 100644 index 0000000..afa77dd --- /dev/null +++ b/components/web_ctrl/web_ctrl.h @@ -0,0 +1,26 @@ +/** + * @file web_ctrl.h + * @brief WiFi + HTTP 网页控制步进电机 + * + * 连接 WiFi "ICE" (密码 12345678), 启动 HTTP 服务器, + * 通过网页控制 Emm_V5 步进电机。 + */ + +#ifndef WEB_CTRL_H +#define WEB_CTRL_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief 初始化 WiFi (STA 模式) 并启动 HTTP 服务器 + * @note 阻塞直到 WiFi 连接成功, 然后启动 HTTP 服务器 + */ +void web_ctrl_init(void); + +#ifdef __cplusplus +} +#endif + +#endif /* WEB_CTRL_H */ diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index cdeace2..b2068dc 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) + PRIV_REQUIRES emm_v5 magnet servo web_ctrl) diff --git a/main/main.c b/main/main.c index 33f6677..ee4e049 100644 --- a/main/main.c +++ b/main/main.c @@ -1,56 +1,43 @@ /** * @file main.c - * @brief Emm_V5.0 步进闭环电机驱动演示 (ESP-IDF) + * @brief EDC 步进电机网页控制 (ESP-IDF) * * 硬件接线: * ESP32 TX (GPIO6) -> 电机 RX * ESP32 RX (GPIO7) -> 电机 TX + * ESP32 GPIO5 -> 电磁铁 + * ESP32 GPIO4 -> 舵机 * ESP32 GND -> 电机 GND + * + * WiFi: 连接 ICE (密码 12345678), 获取 IP 后用浏览器打开控制页面 */ #include "emm_v5.h" +#include "magnet.h" +#include "servo.h" +#include "web_ctrl.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" void app_main(void) { - uint8_t rx_buf[128] = {0}; - uint8_t rx_len; - - /* 初始化驱动 (UART1, TX=GPIO6, RX=GPIO7, LED=GPIO2) */ + /* 初始化电机驱动 (UART1, TX=GPIO6, RX=GPIO7, LED=GPIO2) */ Emm_V5_Init(UART_NUM_1, GPIO_NUM_6, GPIO_NUM_7); + /* 初始化电磁铁 (GPIO5) */ + magnet_init(GPIO_NUM_5); + + /* 初始化舵机 (GPIO4, 上限180° 下限0°) */ + servo_init(GPIO_NUM_4); + /* 上电延时 2 秒, 等待电机闭环初始化完毕 */ ESP_LOGI("MAIN", "等待电机初始化..."); vTaskDelay(pdMS_TO_TICKS(2000)); - /* ---- 速度模式演示 ---- */ - ESP_LOGI("MAIN", "发送速度控制命令: 地址1, CW, 1000RPM, 加速度10"); - Emm_V5_Vel_Control(1, 0, 100, 10, false); + /* 启动 WiFi + HTTP 网页控制服务器 */ + web_ctrl_init(); - /* 接收电机返回的数据 */ - rx_len = Emm_V5_Receive_Data(rx_buf, sizeof(rx_buf)); - - /* 验证校验字节 (0x6B), 点亮/熄灭 LED */ - if (rx_len > 0 && rx_buf[rx_len - 1] == 0x6B) { - gpio_set_level(EMM_V5_LED_GPIO, 1); - ESP_LOGI("MAIN", "通信正常 (校验通过, LED亮)"); - } else { - gpio_set_level(EMM_V5_LED_GPIO, 0); - ESP_LOGW("MAIN", "通信异常 (校验失败, LED灭)"); - } - - /* 打印返回的原始数据 (调试用, 每个字节+1 避免与电机地址冲突) */ - ESP_LOGI("MAIN", "返回数据长度: %d", rx_len); - if (rx_len > 0) { - printf("返回数据(+1偏移): "); - for (int i = 0; i < rx_len; i++) { - printf("%02X ", rx_buf[i] + 1); - } - printf("\n"); - } - - ESP_LOGI("MAIN", "演示完成, 程序挂起"); + ESP_LOGI("MAIN", "系统就绪, 等待网页指令..."); while (1) { vTaskDelay(pdMS_TO_TICKS(1000)); } diff --git a/partitions.csv b/partitions.csv new file mode 100644 index 0000000..134d541 --- /dev/null +++ b/partitions.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size, Flags +# 4MB Flash 自定义分区表 +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 0x3E0000,