diff --git a/.gitignore b/.gitignore index 723ef36..517a439 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,16 @@ -.idea \ No newline at end of file +.idea +# Python 缓存 +__pycache__/ +*.pyc + +# 固件与二进制文件(量产固件建议通过 GitHub Release 发布,而非直接塞进源码仓库) +tools/firmware/*.bin + +# Arduino 编译临时目录 +build/ +.build/ + +.gitignore~ +..gitignore.un~ +*.un~ +*~ diff --git a/README.md b/README.md new file mode 100644 index 0000000..cdb9a39 --- /dev/null +++ b/README.md @@ -0,0 +1,143 @@ + +# ESP32 底盘控制程序 (esp32_base_control) + +本项目基于 ESP32-C3(支持 Geekble Mini ESP32-C3 开发板)开发,用于移动机器人底盘的底层核心控制。项目包含完整的 Arduino 源码工程,以及面向工厂/团队的高效批量自动烧录与全自动质检工具链。 + +--- + +## 🛠 场景一:源码开发与环境配置 (开发者适用) + +如果你需要修改底层控制逻辑或调试传感器,请使用图形化 IDE 进行开发。 + +### 1. 环境变量配置 (国内加速) +为避免国内网络环境下下载乐鑫依赖链极易卡死的问题,请在系统环境变量中添加加速源: +- **Windows**: 右键「此电脑」→「属性」→「高级系统设置」→「环境变量」,在用户变量中新建: + - 变量名:`IDF_GITHUB_ASSETS` + - 变量值:`dl.espressif.com/github_assets` +- **macOS / Linux**: 在终端配置文件(如 `~/.zshrc` 或 `~/.bash_profile`)中追加: + ```bash + export IDF_GITHUB_ASSETS="https://dl.espressif.com/github_assets" + ``` + + +### 2. Arduino IDE 配置 + +1. 打开 Arduino IDE,进入 `Preferences` (偏好设置)。 +2. 在 `Additional boards manager URLs` 中填入国内第三方加速索引源: + ```text + https://arduino.me/packages/esp32.json + ``` +3. 进入 `Boards Manager` (开发板管理器),搜索 `esp32` 并安装由 **Espressif Systems** 发布的开发板核心库。 + + +### 3. 编译与烧录 + +1. 用 Arduino IDE 打开工程主文件:`arduino/base_control/base_control.ino`。 +2. 在开发板选择中搜索并选中 `Geekble Mini ESP32-C3`。 +3. 选择对应的物理 USB 串口(macOS 通常为 `/dev/cu.usbmodemXXXX`,Windows 通常为 `COMX`)。 +4. 点击 **Upload (上传)** 开始编译并烧录。 + +--- + +## ⚡ 场景二:一键式批量自动烧录 (生产/量产适用) + +当底盘控制程序验证完毕、需要进行多台设备批量固件写入时,请使用项目内置的自动化量产工具。 + +### 1. 准备合并固件 + +在第一次使用 Arduino IDE 成功烧录后,从本地编译缓存中捞出合并好的单体固件 `base_control.ino.merged.bin`,并将其放置到本仓库的 `tools/firmware/` 目录下。 + +### 2. 安装量产依赖 + +确保本地已安装 Python 3 环境,在终端执行以下命令安装乐鑫官方烧录核心驱动: + +```bash +pip3 install esptool +``` + +### 3. 启动批量烧录系统 + +进入工具链目录并启动监听脚本: + +```bash +cd tools +python3 batch_burner.py +``` + +### 4. 流水线实操规程 + +1. **接入硬件**:使用 USB 数据线将新的底盘主控板接入电脑。 +2. **自动闪存**:脚本将自动捕获新端口并以最高稳定波特率(`921600`)执行顺序写入,耗时通常在 5~8 秒。 +3. **断开换板**:终端打印出绿色 `[成功]` 提示后,直接拔掉数据线,换下一块板子插入,全程无需触碰键盘鼠标。 + +--- + +## 🔍 场景三:底盘功能与通信协议全自动质检 (研发质检/品控适用) + +在固件烧录成功后,保持数据线连接,可直接使用项目内置的协议压力测试工具对底盘的底层状态机、双轴控制及编码器反馈进行高精度功能质检: + +### 1. 安装质检依赖 +确保本地安装了 Python 串口通信组件: +```bash +pip3 install pyserial +``` + +### 2. 查找当前主控板的正确串口号 + +把已经成功烧录好固件的 ESP32 底盘通过数据线连上 Mac/Linux。在终端确认它的端口(假设依然是刚才的通用端口): +```bash +ls /dev/cu.usb* +``` +假设获取到的端口号是:`/dev/cu.usbmodemXXXX` + + +### 3. 一键运行自动化品检 + +无需修改脚本源码,直接通过命令行传入当前板子在 Mac/Linux 下分配的动态串口路径: + +```bash +python3 tests/test_uart.py /dev/cu.usbmodemXXXX +``` + +*注:串口路径为必填参数。波特率默认对齐底盘源码的 `115200`。如需修改,可追加参数如 `--baud 9600`。* + + +### 4. 出厂判定标准 + +终端将瞬间瀑布式执行 26 项子协议断言测试。 + +* **合格**:结尾显示绿色 `[PASS]` 汇总,且 26/26 项全部通过,底盘准予出厂。 +* **不合格**:若出现红色 `[FAIL]` 或随机时序错乱,请重点排查底盘供电稳定度、编码器接线或芯片物理损坏。 + +--- + +## 🔍 场景四:烧录 + 26项功能全自动联动质检 (品控质检适用) + +为了确保烧录完成的底盘物理接线、编码器状态和通信状态机 100% 合格,本项目支持“烧录即质检”的流水线联动模式。 + +### 1. 安装质检依赖 +确保本地安装了 Python 串口通信组件: + +```bash +pip3 install pyserial +``` + +### 2. 启动联动质检流水线 + +在终端中附加 `--test` 参数启动烧录脚本: + +```bash +python3 tools/batch_burner.py --test +``` + +### 3. 联动流水线 SOP 规范 + +1. **插入新设备**:脚本自动捕获串口,并以最高稳定波特率(`921600`)执行固件烧录。 +2. **自动质检**:固件部署成功后,系统在原地等待 1 秒钟(等待硬件冷启动),随后**自动无缝调用** `tests/test_uart.py` 进行 26 项高精度串口通信与传感器状态校验。 +3. **出厂判定**: + * 若 26/26 项测试全部显示绿色 `[PASS]`,底盘合格。 + * 若出现红色 `[FAIL]` 或串口中断,请重点排查底盘供电、编码器物理接线或芯片损坏。 +4. **换板**:拔掉当前设备,接入下一块,系统自动进入下一轮“烧录+质检”循环。 + +--- + diff --git a/tests/test_uart.py b/tests/test_uart.py index e0e97d0..f672abc 100644 --- a/tests/test_uart.py +++ b/tests/test_uart.py @@ -8,10 +8,12 @@ import struct import sys import termios +import argparse # ── 配置 ────────────────────────────────────────────── -PORT = "/dev/ttyS1" -BAUD = 115200 +#PORT = "/dev/cu.usbmodem11301" +#PORT = "/dev/ttyS1" +#BAUD = 115200 TIMEOUT = 1.0 # 秒 # ── 命令/响应字 ─────────────────────────────────────── @@ -390,6 +392,16 @@ def test_reset(ser, t): # ── 主程序 ─────────────────────────────────────────── def main(): + + # 动态构建参数解析器 + parser = argparse.ArgumentParser(description="ESP32-C3 电机控制器 UART 协议全面测试") + parser.add_argument("port", type=str, help="物理设备串口路径 (如 Mac 下的 /dev/cu.usbmodem11101)") + parser.add_argument("--baud", type=int, default=115200, help="通讯波特率 (默认: 115200)") + args = parser.parse_args() + + PORT = args.port + BAUD = args.baud + print("="*50) print(" ESP32-C3 电机控制器 UART 协议全面测试") print(f" 端口: {PORT} 波特率: {BAUD}") @@ -419,10 +431,16 @@ def main(): test_unknown_cmd(ser, t) test_reset(ser, t) finally: - # 确保测试结束后电机停止 + # 确保测试结束后电机停止且清除缓冲区:退出前将底盘控制状态机复位,防止电机持续空转 try: - if ser.is_open: + # 在关闭串口前,发送复位信号,确保底盘恢复状态 + # if ser.is_open: + if 'ser' in locals() and ser.is_open: ser.write(build_frame(CMD_RESET)) + time.sleep(0.05) # 给硬件极短的重置反应时间 + ser.reset_input_buffer() + ser.reset_output_buffer() + except Exception: pass # 串口已断开,忽略错误 finally: diff --git a/tools/batch_burner.py b/tools/batch_burner.py new file mode 100755 index 0000000..2420c55 --- /dev/null +++ b/tools/batch_burner.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +import os +import sys +import time +import glob +import argparse +import subprocess + +# 确保脚本即使在后台运行也能实时刷新控制台输出 +sys.stdout.reconfigure(line_buffering=True) if hasattr(sys.stdout, 'reconfigure') else None + +def parse_arguments(): + """解析命令行参数,让脚本具备高通用性""" + parser = argparse.ArgumentParser(description="ESP32 批量流水线自动化烧录系统") + + # 默认寻找脚本同级或上级目录下的 firmware/base_control.ino.merged.bin + default_fw = os.path.join(os.path.dirname(os.path.abspath(__file__)), "firmware", "base_control.ino.merged.bin") + + parser.add_argument("--fw", type=str, default=default_fw, help="合并后的固件 (.bin) 路径") + parser.add_argument("--chip", type=str, default="esp32c3", help="目标芯片架构 (如 esp32, esp32c3, esp32s3)") + parser.add_argument("--baud", type=str, default="921600", help="烧录波特率") + + # 🌟 新增这一行:添加自动质检的布尔开关选项 + parser.add_argument("--test", action="store_true", help="启用该选项后,每块板子烧录完成后自动执行 26 项协议质检") + + return parser.parse_args() + +def check_env(fw_path, run_test): + """检查固件及依赖环境""" + # 1. 检查固件 + if not os.path.exists(fw_path): + print(f"\033[31m[错误] 找不到固件文件,请检查路径:\n{fw_path}\033[0m") + print("\033[33m提示:请在脚本同级目录下创建 'firmware' 文件夹,并将已编译合并的固件命名为 'base_control.ino.merged.bin' 放入其中。\033[0m") + sys.exit(1) + + # 2. 检查 esptool 依赖 + try: + import esptool + except ModuleNotFoundError: + print("\033[31m[错误] 当前 Python 环境中未安装 esptool 库!\033[0m") + print(f"\033[33m请在终端执行以下命令安装后重试:\n {sys.executable} -m pip install esptool\033[0m") + sys.exit(1) + + print(f"\033[32m[环境就绪] 固件载入成功: {os.path.basename(fw_path)} ({os.path.getsize(fw_path) / 1024 / 1024:.2f} MB)\033[0m") + + # 依赖检查逻辑 + # 如果用户开启了 `--test` 选项,脚本必须在启动时前置检查 `pyserial` 库以及 `test_uart.py` 脚本是否存在,防止烧录完后因找不到测试依赖而崩溃。 + if run_test: + try: + import serial + except ModuleNotFoundError: + print("\033[31m[错误] 启用了自动质检,但当前环境未安装 pyserial 库!\033[0m") + print(f"\033[33m请在终端执行以下命令安装后重试:\n {sys.executable} -m pip install pyserial\033[0m") + sys.exit(1) + + test_script = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "tests", "test_uart.py")) + if not os.path.exists(test_script): + print(f"\033[31m[错误] 找不到测试脚本,请检查路径是否正确:\n{test_script}\033[0m") + sys.exit(1) + print("\033[35m[联动激活] 已开启“烧录 + 26项高精度功能质检”一体化流水线模式。\033[0m") + + +def get_current_ports(): + """动态获取 macOS 和 Linux 下的常用串口设备""" + patterns = [ + "/dev/cu.usbmodem*", + "/dev/cu.usbserial*", + "/dev/ttyUSB*", + "/dev/ttyACM*" + ] + ports = [] + for pattern in patterns: + ports.extend(glob.glob(pattern)) + return set(ports) + + +# 用于在烧录成功后,使用当前 Python 解释器隐式调用test_uart.py 并实时透传 26 项彩色测试日志。 +def run_auto_test(port): + """🌟 新增函数:调用项目的核心质检脚本进行功能核验""" + print(f"\n\033[35m[正在质检] 正在对端口 {port} 发起 26 项高精度协议全自动质检...\033[0m") + + # 动态定位到 tests/test_uart.py 的绝对路径 + test_script = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "tests", "test_uart.py")) + + # 组合底层命令:python3 tests/test_uart.py /dev/cu.usbmodemXXXX + cmd = [sys.executable, test_script, port] + + try: + # 实时透传标准输出与标准错误,使 26 项全绿的瀑布流直接打在当前终端上 + result = subprocess.run(cmd, stdout=sys.stdout, stderr=sys.stderr, text=True) + return result.returncode == 0 + except Exception as e: + print(f"\033[31m[质检异常] 执行自动质检时发生进程错误: {e}\033[0m") + return False + + +def burn_firmware(port, args): + """调用当前环境的 esptool 模块执行单次写入""" + print(f"\n\033[34m[开始烧录] 正在向端口发送固件: {port} ...\033[0m") + + # 使用 sys.executable 锁定当前 Python 解释器,使用精确的硬件参数替代 "keep" 以防固件损坏 + cmd = [ + #"python3", "-m", "esptool", + sys.executable, "-m", "esptool", + "--chip", args.chip, + "--port", port, + "--baud", args.baud, + "--before", "default-reset", + "--after", "hard-reset", + "write-flash", + "-z", + "--flash_mode", "dio", + "--flash_freq", "80m", + "--flash_size", "4MB", + "0x0", args.fw + ] + + start_time = time.time() + try: + # 执行烧录并实时透传日志到终端 + result = subprocess.run(cmd, stdout=sys.stdout, stderr=sys.stderr, text=True) + if result.returncode == 0: + elapsed = time.time() - start_time + #print(f"\033[32m[成功] ====== 烧录耗时 {elapsed:.1f} 秒。请拔掉当前板子,换下一块 ====== \033[0m") + print(f"\033[32m[成功] 固件部署完毕,耗时 {elapsed:.1f} 秒。\033[0m") + + # 🌟 新增质检联动逻辑 + if args.test: + time.sleep(1.0) # 给予芯片足够的冷启动和 USB 重新握手时间 + run_auto_test(port) + + print(f"\n\033[32m====== 流程结束。请拔掉当前板子,换下一块 ====== \033[0m") + + return True + else: + print("\033[31m[失败] ====== esptool 写入中断,请检查线材供电或重新插拔 ====== \033[0m") + return False + except Exception as e: + print(f"\033[31m[异常] 进程通信失败: {e}\033[0m") + return False + +def main(): + args = parse_arguments() + + print("====================================================") + print(" ESP32 跨平台底盘控制程序流水线烧录系统") + print("====================================================") + + check_env(args.fw, args.test) + + # 记录脚本启动时已经插在电脑上的设备,防止误烧录 + last_ports = get_current_ports() + print("\n[循环监听中] 请插入新的 ESP32 底盘主控板...") + + while True: + time.sleep(0.3) + current_ports = get_current_ports() + + # 捕获新插入的端口 + new_ports = current_ports - last_ports + + if new_ports: + active_port = list(new_ports)[0] + print(f"\n\033[33m[检测到新硬件] 端口分配: {active_port}\033[0m") + time.sleep(0.6) # 给予硬件上电并稳定 USB 握手的时间 + + # 执行自动化烧录 + burn_firmware(active_port, args) + + print("\n[等待断开] 请拔出当前已烧录完毕的设备...") + # 阻塞,直到用户安全拔出设备 + while True: + time.sleep(0.3) + if active_port not in get_current_ports(): + print("[状态复位] 设备已安全拔出。") + break + + # 重新捕获当前基准端口状态 + last_ports = get_current_ports() + print("\n[继续监听] 请插入下一块新板子...") + else: + # 如果中途有人拔掉了原本就插着的无效线,实时同步状态 + last_ports = current_ports + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n\033[33m[提示] 批量烧录程序已安全退出。\033[0m") + sys.exit(0) diff --git a/tools/firmware/base_control.ino.merged.bin b/tools/firmware/base_control.ino.merged.bin new file mode 100644 index 0000000..5814fbc Binary files /dev/null and b/tools/firmware/base_control.ino.merged.bin differ