基于ESP32的蓝牙遥控小车

引言

在嵌入式技术与智能控制领域快速发展的背景下,智能小车作为集机械结构、电子电路、传感器技术、自动控制算法于一体的综合性平台,已成为高校自动化、电子信息等专业实践教学的重要载体[1]。传统遥控小车多采用红外遥控或专用遥控器,存在控制距离有限、功能单一、扩展性差等问题,难以满足复杂场景下的智能控制需求。例如,传统红外遥控的有效距离通常小于 10 米,且易受障碍物遮挡影响;专用遥控器的按键功能固定,无法根据需求灵活扩展自动避障、路径规划等高级功能[2]

随着物联网技术的发展,基于无线通信的智能控制方案逐渐成为主流。ESP32 作为一款集成蓝牙 BLE(Bluetooth Low Energy)和 Wi-Fi 功能的高性能单片机,凭借其低功耗、高集成度、强大的运算能力等优势,在智能设备开发中得到广泛应用[3]。相较于传统的 51 单片机或 STM32 基础型号,ESP32 不仅能实现无线通信功能,还能通过内置的双核处理器高效运行复杂控制算法(如 PID 调速),为智能小车的多功能扩展提供了硬件基础。

本设计基于 ESP32 单片机开发一款蓝牙遥控小车,融合了嵌入式编程、无线通信、传感器应用与自动控制等技术,主要实现以下功能:

手机 APP 远程控制:通过 APP Inventor 开发的应用程序,实现对小车前进、后退、左转、右转的无线控制;

速度闭环控制:采用 PID 算法实现直流电机的转速调节,确保小车在负载变化时仍能保持稳定速度;

自动避障功能:利用超声波传感器检测障碍物,通过程序控制实现遇障减速、转向避让;

实时数据反馈:通过蓝牙 BLE 将小车的速度、距离等状态信息回传至手机 APP,实现双向通信。

本设计的创新点在于:

模块化架构:将硬件系统划分为控制核心、电源、驱动、传感等独立模块,便于调试与功能扩展;

多协议融合:采用蓝牙 BLE 协议实现近距离无线通信,同时预留 Wi-Fi 模块接口(支持 TCP/IP 协议),可扩展至远程控制;

智能算法优化:针对 PID 控制中的积分饱和问题,采用抗积分饱和算法,提高调速系统的动态性能;

跨平台开发:结合 Arduino 的便捷编程与 APP Inventor 的可视化开发,降低开发门槛,适合高校学生实践。

从理论意义来看,本设计深入探讨了单闭环直流调速系统的设计方法、PID 参数整定技巧及无线通信协议的实现机制,为自动控制理论的工程应用提供了实践案例。从应用价值来看,该系统可作为智能移动机器人的原型,拓展至家庭服务、仓储物流、教育演示等领域,具有较强的实用价值[4]

本报告将按照系统设计、硬件开发、软件实现、调试优化的逻辑展开,详细阐述从方案构思到功能实现的全过程。报告结构如下:第 2 章介绍系统总体设计,包括架构与关键技术;第 3 章详述硬件系统的选型与电路设计;第 4 章分析软件系统的编程实现与流程;第 5 章说明调试过程中的问题及解决方案;最后为总结与展望。

系统总体设计

设计目标与功能需求

本设计的核心目标是开发一款基于 ESP32 的蓝牙遥控小车,实现远程控制、速度调节、自动避障等功能,并通过模块化设计确保系统的稳定性与可扩展性。具体目标如下:

性能指标

1. 控制距离:蓝牙 BLE 通信有效距离不小于 15 米,无遮挡环境下通信延迟小于 100ms;

2. 调速范围:直流电机转速可在 50-350RPM(转 / 分钟)范围内连续调节,稳态速度误差不超过 ±5%;

3. 避障精度:超声波测距误差小于 ±2cm,检测距离范围 2-400cm,遇障响应时间小于 300ms;

4. 续航能力:采用 2 节 18650 锂电池(总容量 4400mAh)供电,满负载运行时间不小于 2 小时;

5. 运动性能:小车直线行驶偏差小于 5cm/m,转弯半径可通过差速控制调节。

功能需求

根据操作与系统自主控制的双重需求,将功能划分为以下模块,如表2. 1所示

表2. 1功能需求

功能模块具体需求
远程控制手机 APP 发送控制指令(前进 / 后退 / 左转 / 右转 / 停止),小车实时响应
速度调节APP 可设置速度等级(如低速 / 中速 / 高速),对应 PWM 占空比可调
状态反馈小车通过蓝牙回传当前速度、距离障碍物距离等信息至 APP 显示
自动避障检测到障碍物时自动减速或转向,避障完成后恢复原路径
故障保护电池电压低于 3.6V 时自动报警,电机堵转时切断输出保护电路

设计约束

1. 尺寸限制:小车整体尺寸不超过 30cm×20cm×15cm(长 × 宽 × 高),便于在桌面或地面运行;

2. 成本控制:核心元器件总成本不超过 200 元,适合学生实践项目;

3. 功耗要求:待机电流小于 50mA,运行电流小于 500mA(不含电机峰值电流);

4. 环境适应性:工作温度范围 0-40℃,相对湿度≤80%(无凝结)。

系统架构设计

本系统采用 “分层模块化” 架构,从上至下分为应用层、控制层、硬件层,各层通过标准化接口实现数据交互,具体架构如图 2.1 所示。

图片2.1.png
图2.1系统结构模式
Figure 2.1 System structure pattern

应用层

应用层负责用户交互与数据展示,主要包括:

手机 APP:通过 APP Inventor 开发,提供控制界面(方向键、速度滑块)与状态显示界面(速度值、距离值);

数据通信接口:定义与控制层的通信协议,如前进指令编码为 “F”,后退为 “B”,速度值采用 ASCII 码传输。

控制层

控制层是系统的核心,基于 ESP32 单片机实现,包括:

中央控制模块:负责解析 APP 指令、调度各功能模块、处理传感器数据;

通信模块:实现蓝牙 BLE 协议,完成指令接收与状态回传;

电机控制模块:根据指令生成 PWM 信号,驱动电机正反转与转速调节;

传感器数据处理模块:对测速模块、超声波模块的原始数据进行滤波、转换(如将脉冲数转换为转速);

控制算法模块:运行 PID 调速算法与避障决策算法。

硬件层

硬件层为系统提供物理载体与执行机构,包括:

核心控制硬件:ESP32 单片机及其最小系统(电源、复位电路等);

电源模块:2 节 18650 锂电池、电池盒、DC/DC 降压电路(输出 3.3V/5V);

驱动硬件:MX1919 直流电机驱动模块、直流电机;

传感硬件:测速模块(含 20 格码盘)、超声波模块(HC-SR04);

连接硬件:杜邦线、螺丝、车架等结构件。

数据流向

系统数据流向遵循以下规则:

控制指令流:手机 APP→蓝牙 BLE→ESP32→电机驱动模块→直流电机;

状态反馈流:传感器(测速 / 超声波)→ESP32→蓝牙 BLE→手机 APP;

控制算法流:传感器数据→ESP32 控制算法→生成 PWM 信号→电机驱动模块。

例如,当用户在 APP 点击 “前进” 并设置速度为 200RPM 时,数据流向为:

ESP32 解析指令后,启动 PID 算法,以 200RPM 为目标转速;

测速模块实时采集电机转速并反馈至 ESP32;

ESP32 根据转速误差调整 PWM 占空比,通过 MX1919 模块驱动电机正转;

同时,ESP32 将当前转速(如 198RPM)通过蓝牙回传至 APP 显示。

关键技术选型

嵌入式控制技术

本设计采用 ESP32 作为控制核心,相较于传统单片机(如 STM32F103、Arduino Uno)具有以下优势:

双核处理器:集成 2 个 Xtensa® 32 位 LX6 微处理器,主频最高 240MHz,可同时运行控制算法与通信协议,提高系统响应速度;

无线集成:内置蓝牙 BLE 5.0 和 Wi-Fi 802.11b/g/n,无需外接模块即可实现无线通信;

丰富外设:包含 18 个 ADC 通道、3 个 UART、2 个 I2C、4 个 SPI、16 个 PWM 通道,满足多传感器与驱动模块的接口需求;

低功耗特性:支持多种休眠模式,深度睡眠电流可低至 5μA,延长续航时间[5]

ESP32 的软件开发采用 Arduino IDE,其优势在于:

简化编程:提供丰富的库函数(如 BLE 库、电机控制库),无需深入底层寄存器配置;

跨平台兼容:支持 Windows、macOS、Linux 系统,且代码可无缝移植到其他 Arduino 兼容板;

社区支持:拥有庞大的开发者社区,可快速获取调试经验与代码示例。

图片2.2.png
图2.2单片机技术应用组成图
Figure 2.2 The composition of the application of microcontroller technology

无线通信技术

本设计选用蓝牙 BLE 5.0 作为通信协议,主要考虑以下因素:

低功耗:BLE(低功耗蓝牙)的发射电流约为 10mA,远低于传统蓝牙的 30mA,适合电池供电设备;

传输效率:BLE 5.0 的传输速率可达 2Mbps,传输距离最远 300 米(理论值),满足小车控制需求;

配对便捷:支持快速配对,无需输入密码,手机 APP 可直接搜索并连接 “ESP32-Car” 设备;

协议简化:采用通用属性配置文件(GATT),数据传输格式为 “服务 - 特征值”,便于编程实现。

蓝牙通信的数据包格式定义如下:

指令包:由操作码 + 参数组成,如 “F,200” 表示前进,目标转速 200RPM;

状态包:由数据类型 + 数值组成,如 “S,198,D,25” 表示当前转速 198RPM,距离障碍物 25cm;

校验机制:每个数据包末尾添加 CRC 校验位,确保数据传输正确性。

传感器融合技术

本设计采用两种传感器实现环境感知与状态检测:

测速模块(光电编码器)

工作原理:通过红外对管检测 20 格码盘的脉冲信号,每转动一圈产生 20 个脉冲;

技术参数:响应频率≤10kHz,检测距离 2-10mm,工作电压 5V;

数据处理:ESP32 通过外部中断计数脉冲,结合定时器计算单位时间内的脉冲数,进而转换为转速。

超声波模块(HC-SR04)

工作原理:通过发射 40kHz 超声波,接收反射波并计算传播时间,进而得到距离;

技术参数:测量范围 2-400cm,精度 ±0.3cm,工作电压 5V;

数据处理:采用中位数滤波法去除异常值,提高测距稳定性。

传感器数据融合流程如下:

测速模块每 100ms 产生一次中断,ESP32 在中断服务程序中记录脉冲数;

超声波模块每 500ms 触发一次测距,通过 GPIO 口发送触发信号并接收回声信号;

主程序周期性(如 100ms)读取传感器数据,进行单位转换后用于控制算法。

自动控制算法

1. PID 调速算法

用途:实现直流电机转速的闭环控制,减小负载变化对速度的影响;

优势:结构简单、鲁棒性强,适合嵌入式系统实现;

改进措施:针对积分饱和问题,采用限幅抗积分饱和算法。

2. 避障决策算法

逻辑:当超声波检测距离<20cm 时,小车停止并转向;距离在 20-30cm 时,减速行驶;

转向策略:随机选择左转或右转(如通过随机数生成器),避免陷入局部障碍区。

硬件系统开发设计

硬件系统组成框图设计

硬件系统是小车实现各项功能的物理基础,其组成框图如图 3.1 所示,主要包括 6 大模块:ESP32 控制模块、电源模块、电机驱动模块、直流电机、传感器模块(测速 + 超声波)、辅助结构(车架、接线等)。各模块通过电气连接形成有机整体,其中 ESP32 作为核心,负责协调各模块工作。

图片3.1.png
图3.1系统硬件组成框图
Figure 3.1 System hardware composition block diagram

各模块的功能如下:

ESP32 控制模块:接收 APP 指令,处理传感器数据,运行控制算法,输出控制信号;

电源模块:将锂电池电压(7.4V)转换为 5V(给电机驱动、超声波模块供电)和 3.3V(给 ESP32 供电);

电机驱动模块:将 ESP32 输出的低电压 PWM 信号转换为高电流信号,驱动直流电机;

直流电机:提供动力,通过正反转与转速差异实现小车的前进、后退、转向;

测速模块:实时检测电机转速,为 PID 调速提供反馈信号;

超声波模块:检测前方障碍物距离,为避障功能提供数据支持。

核心控制模块:ESP32 单片机

ESP32 芯片特性与优势

ESP32 是乐鑫信息科技(Espressif)推出的一款高性能 Wi-Fi + 蓝牙双模单片机,其核心特性如表3. 1所示:

表3. 1ESP32的核心特性

类别特性参数
处理器内核双核 Xtensa® LX6,主频 160/240MHz
性能支持单精度浮点运算,峰值算力 600 DMIPS
存储片内 RAM520KB SRAM
外部存储支持 SPI Flash(最大 16MB)、SPI RAM(最大 8MB)
无线通信Wi-Fi802.11 b/g/n,支持 2.4GHz 频段,速率最高 150Mbps
蓝牙BLE 5.0 + 传统蓝牙,支持长距离模式(Coded PHY)
外设GPIO34 个可编程 I/O 口(含 18 个 ADC、2 个 DAC)
定时器4 个通用定时器(16 位)、2 个看门狗定时器
通信接口3×UART、2×I2C、4×SPI、1×I2S、1×CAN
电源工作电压3.0-3.6V
功耗深度睡眠模式:5μA;活跃模式:约 80mA
环境工作温度-40℃ ~ 85℃

与同类产品(如 STM32F407、Arduino Nano 33 BLE)相比,ESP32 在本设计中的优势体现在:

无线集成度高:无需外接 Wi-Fi 或蓝牙模块,减少硬件体积与接线复杂度,特别适合小车等小型设备;

运算能力强:双核处理器可并行处理通信与控制任务(如核 0 运行蓝牙协议,核 1 运行 PID 算法),提高系统响应速度;

外设丰富:16 个 PWM 通道可独立控制左右电机的转速与方向,18 个 ADC 通道可同时采集多个传感器数据;

成本优势:单价约 30 元,远低于 STM32F407(约 80 元),适合低成本项目[6]

ESP32 最小系统设计

ESP32 最小系统是保证芯片正常工作的基础电路,包括电源电路、复位电路、时钟电路、下载电路,具体设计如下:

电源电路

输入:由 DC/DC 降压模块提供 3.3V 稳定电压;

滤波:在电源输入端并联 1 个 10μF 电解电容和 1 个 0.1μF 陶瓷电容,滤除高频噪声;

保护:串联 1 个自恢复保险丝(1A),防止过流损坏芯片。

复位电路

采用外部复位方式:通过 1 个 10kΩ 上拉电阻将 RESET 引脚接 3.3V,1 个按钮接地;

复位逻辑:按下按钮时,RESET 引脚电平拉低,芯片复位;松开后,电平恢复高电平,芯片正常工作。

时钟电路

采用内置 RC 振荡器(默认),无需外部晶振;如需更高精度,可外接 32.768kHz RTC 晶振。

下载电路

通过 USB 转 TTL 模块(如 CH340)实现与电脑的通信;

接线:CH340 的 TXD 接 ESP32 的 RXD(GPIO3),RXD 接 ESP32 的 TXD(GPIO1),GND 共地;

下载控制:通过 GPIO0 引脚控制(接地时进入下载模式,上拉时进入运行模式)。

图片3.2.png
图3.2供电系统
Figure 3.2 Power supply system

电源系统设计

电源系统为整个小车提供稳定的能量供应,其性能直接影响系统的稳定性与续航能力。本设计采用锂电池 + DC/DC 降压的方案,具体如下:

锂电池供电模块

1. 电池选型

选用 2 节 18650 锂电池串联,参数如下:

单节电压:3.7V(标称),满电 4.2V,放电截止 3.0V;

串联总电压:7.4V(标称),范围 6.0-8.4V;

容量:每节 2200mAh,总容量 2200mAh(串联容量不变);

放电电流:持续放电电流 1A,最大脉冲电流 3A。

2. 电池盒与保护电路

电池盒:2 节串联式 18650 电池盒,带引线(红正黑负);

保护电路:集成过充保护(充电电压>4.25V 时截止)、过放保护(放电电压<2.75V 时截止)、过流保护(电流>3A 时截止),防止电池损坏或起火[7]

供电分配

锂电池直接给 MX1919 电机驱动模块供电(输入电压 6-12V);

通过 DC/DC 降压模块转换为 5V 和 3.3V,给其他模块供电:

3.3V:超声波模块、测速模块;

5V:ESP32 单片机。

DC/DC 降压电路

为满足不同模块的供电需求,设计两级 DC/DC 降压电路:

5V 降压模块(LM1117-5)

输入:锂电池 7.4V(6-8.4V);

输出:稳定 5V,最大输出电流 800mA;

电路设计:

输入电容:10μF 电解电容,滤除输入纹波;

输出电容:10μF 电解电容 + 0.1μF 陶瓷电容,稳定输出电压;

散热:LM1117-5 加装小型散热片,防止满负载时过热。

3.3V 降压模块(AMS1117-3.3)

输入:5V(来自前级降压);

输出:稳定 3.3V,最大输出电流 1A;

电路设计:同 5V 模块,输出电容选用低 ESR(等效串联电阻)电容,提高稳定性。

降压模块效率测试数据如表3. 2所示,在典型负载(ESP32 + 传感器约 100mA)下,总效率约 85%,满足设计要求。

表3. 2降压模块效率

输入电压(V)输入电流(mA)输出电压(V)输出电流(mA)效率(%)
7.4855.010081.5
7.41705.020083.8
5.0663.310099.1
5.01323.320099.2

注:效率计算公式为:η =(输出电压 × 输出电流)/(输入电压 × 输入电流)×100%

电机驱动模块

MX1919 驱动模块原理

MX1919 是一款双通道直流电机驱动模块,适合驱动电压 6-12V、电流≤1.5A 的直流电机,其内部结构与工作原理如下:

内部结构

核心芯片:2 个 H 桥驱动芯片(如 TB6612FNG),每个 H 桥可独立控制 1 个电机;

逻辑电路:包含电平转换电路,支持 3.3V/5V 逻辑输入(兼容 ESP32 的 GPIO 输出);

保护电路:过流保护(阈值 2A)、过热保护(芯片温度>150℃时关断输出)。

工作原理

H 桥电路通过 4 个 MOS 管的导通与关断控制电机电流方向,实现正反转:

正转:MOS 管 Q1、Q4 导通,Q2、Q3 截止,电流从 A→B;

反转:MOS 管 Q2、Q3 导通,Q1、Q4 截止,电流从 B→A;

停止:所有 MOS 管截止,电机无电流。

转速调节:通过 PWM 信号控制 MOS 管的导通时间(占空比),占空比越大,平均电流越大,转速越高。

引脚功能

MX1919 模块的引脚定义如表3. 3所示:

表3. 3MX1919模块引脚

引脚名称功能描述连接对象
VCC逻辑电源输入ESP32 的 5V 输出
GND接地系统地(与 ESP32 共地)
VM电机电源输入电池正极(7.4V)
IN1电机 A 控制信号 1ESP32 的 GPIO2
IN2电机 A 控制信号 2ESP32 的 GPIO3
IN3电机 B 控制信号 1ESP32 的 GPIO10
IN4电机 B 控制信号 2ESP32 的 GPIO6
A+电机 A 正极输出左电机正极
A-电机 A 负极输出左电机负极
B+电机 B 正极输出右电机正极
B-电机 B 负极输出电机负极

电机驱动电路设计

基于 MX1919 模块的电机驱动电路如图 3.3 所示,主要包括电源滤波、信号隔离、电机接线三部分:

电源滤波

在 VM 引脚与地之间并联 1 个 100μF 电解电容和 1 个 104 陶瓷电容,滤除电机启动时的电流尖峰,保护锂电池;

VCC 引脚串联 1 个 100Ω 限流电阻,防止 ESP32 的 GPIO 口过流。

信号隔离

IN1-IN4 引脚与 ESP32 的 GPIO 口之间串联 1kΩ 电阻,降低逻辑电路的相互干扰;

所有 GND 引脚共地,确保信号电平参考一致。

电机接线

左电机连接 A+、A-,右电机连接 B+、B-;

电机引线采用 0.5mm² 导线,两端焊接 XT30 插头,便于拆卸。

驱动模块的逻辑真值表如表3. 4所示,通过控制 IN1-IN4 的电平组合,实现电机的正转、反转、停止:

表3. 4驱动模块的逻辑真值表

电机状态IN1IN2IN3IN4PWM 信号
左电机正转HLXXIN1 或 IN2 输入 PWM
左电机反转LHXXIN1 或 IN2 输入 PWM
左电机停止LLXX
右电机正转XXHLIN3 或 IN4 输入 PWM
右电机反转XXLHIN3 或 IN4 输入 PWM
右电机停止XXLL

注:H 为高电平(3.3V),L 为低电平(0V),X 为任意电平。

执行机构:直流电机

直流电机工作原理

本设计选用 6V 直流减速电机(带减速箱),主要参数如下:

额定电压:6V;

额定电流:150mA;

额定转速:350RPM(空载);

减速比:1:100;

输出扭矩:≥0.5kg・cm。

直流电机的工作原理基于电磁感应定律:当电枢绕组通入直流电时,在定子磁场中受到电磁力,形成电磁转矩,驱动电机旋转。其转速公式为:

n = \frac{U - I_{a}R_{a}}{K_{e}\Phi}

其中:

n为电机转速(r/min);

U为电枢电压(V);

I_{a}为电枢电流(A);

R_{a}为电枢电阻(Ω);

K_{e}为电动势常数;

\Phi为每极磁通(Wb)。

由公式可知,改变电枢电压U可线性调节转速,这是本设计采用 PWM 调节转速的理论基础。当负载I_{a}增加时,电枢电流增大,转速n下降,因此需要通过闭环控制(如 PID)补偿转速损失。

电机的输出转矩公式为:

T = K_{t}\Phi I_{a}

其中K_{t}为转矩常数,与K_{e}满足K_{t} = 9.55K_{e}。当负载转矩I_{a}增大时,电枢电流增大,若超过额定电流,可能导致电机过热损坏,因此需在驱动电路中加入过流保护。

电机正反转控制电路

电机的正反转通过 MX1919 模块的 H 桥电路实现,具体控制逻辑如下:

左电机控制

正转:IN1=H,IN2=L,PWM 信号从 IN1 输入;

反转:IN1=L,IN2=H,PWM 信号从 IN2 输入;

转速调节:PWM 占空比D(0-100%)决定平均电压,即U_{avg} = D \times 7.4V

右电机控制

正转:IN3=H,IN4=L,PWM 信号从 IN3 输入;

反转:IN3=L,IN4=H,PWM 信号从 IN4 输入;

转速调节:与左电机原理相同,通过独立 PWM 占空比控制。

PWM 信号由 ESP32 的 PWM 控制器生成,参数设置如下:

频率:25kHz(高于人耳可听范围,避免电机产生噪音);

分辨率:10 位(即占空比可分为 1024 级,精度 0.1%)。

传感器模块

测速模块与码盘

1. 模块组成

光电传感器:由红外发射管和接收管组成,工作电压 5V,输出数字信号;

20 格码盘:黑色塑料圆盘,均匀分布 20 个透光槽,安装在电机轴上;

固定支架:将传感器与码盘对齐,确保检测距离为 5mm。

2. 工作原理

红外发射管发射红外线,当码盘的透光槽转动至传感器处时,红外线被接收管接收,输出低电平;

当遮光部分转动至传感器处时,红外线被遮挡,接收管输出高电平;

电机每转动一圈,传感器输出 20 个脉冲(高低电平交替)。

接口电路

VCC:接 3.3V 电源;

GND:接系统地;

OUT:接 ESP32 的外部中断引脚(如 GPIO0、GPIO1)。

转速计算

T为采样时间(单位:s),N为采样时间内的脉冲数,则:

\text{转速}(RPM) = \frac{N}{20} \times \frac{60}{T}

例如,采样时间T = 0.1s,脉冲数N = 10,则转速为:

\frac{10}{20} \times \frac{60}{0.1} = 300\,\text{RPM}

超声波测距模块

本设计采用 HC-SR04 超声波模块,其工作原理与电路设计如下:

工作原理

触发(Trig):输入一个 10μs 以上的高电平脉冲,模块发射 8 个 40kHz 超声波;

回声(Echo):接收反射波后,输出高电平,高电平持续时间等于超声波往返时间;

距离计算:\text{距离}(cm) = \frac{\text{回声时间}(\mu s) \times 0.034}{2},其中 0.034 为超声波在空气中的传播速度(cm/\mu s)。

关键参数

测量范围:2-400cm;

精度:±0.3cm;

工作电压:5V;

工作电流:15mA;

触发信号:10 \mu s TTL 脉冲。

接口电路

VCC:接 3.3V 电源;

GND:接系统地;

Trig:接 ESP32 的 GPIO18(输出触发信号);

Echo:接 ESP32 的 GPIO19(输入回声信号)。

数据处理

采用中位数滤波法:连续采集 5 次距离值,去除最大值和最小值,取中间 3 次的平均值,减小测量误差;

异常值处理:当测量距离>400cm 或<2cm 时,视为无效值,采用上一次有效测量值。

硬件接线设计

系统总接线原理图

系统总接线原理图如图 3.7 所示,展示了各模块之间的电气连接关系,具体接线规则如下:

电源连接

锂电池正极→MX1919 的 VM 引脚;

锂电池负极→系统地(GND);

DC/DC 降压模块输入→锂电池正负极;

DC/DC 5V 输出→ESP32 的 5V 引脚、超声波模块 VCC、测速模块 VCC;

DC/DC 3.3V 输出→ESP32 的 3.3V 引脚。

控制信号连接

ESP32 GPIO2→MX1919 IN1;

ESP32 GPIO3→MX1919 IN2;

ESP32 GPIO10→MX1919 IN3;

ESP32 GPIO6→MX1919 IN4;

左测速模块 OUT→ESP32 GPIO0;

右测速模块 OUT→ESP32 GPIO1;

超声波 Trig→ESP32 GPIO18;

超声波 Echo→ESP32 GPIO19。

电机连接

左电机→MX1919 A+、A-;

右电机→MX1919 B+、B-。

模块间连接细节

接线规范

电源线路:采用红色导线(正极)和黑色导线(负极),线径≥0.5mm²;

信号线路:采用杜邦线(26AWG),颜色区分功能(如黄色接 PWM 信号,蓝色接 GPIO);

长度控制:传感器信号线长度≤30cm,避免信号衰减;电机电源线长度≤20cm,减少线阻。

抗干扰措施

电机线与信号线分离布线,避免电机电流产生的电磁干扰影响传感器信号;

在 ESP32 的电源引脚附近粘贴铜箔,增强接地效果;

所有模块的 GND 通过星形接法连接,避免地环路干扰。

机械固定

ESP32、MX1919 模块通过 M3 螺丝固定在车架上,底部垫 3mm 厚海绵,减震缓冲;

锂电池用魔术贴固定在车架底部,降低重心,提高小车稳定性;

超声波模块通过支架安装在车头中央,距离地面高度 10cm,确保测距方向水平。

软件设计

系统软件开发环境、工具

软件开发环境

本设计的软件开发涉及嵌入式程序开发与手机 APP 开发两大领域,分别基于不同的环境搭建,具体如下:

ESP32 程序开发环境

采用Arduino IDE 2.2.1作为开发平台,其优势在于:

支持 ESP32 开发板的快速配置,通过「工具→开发板→ESP32 Arduino→ESP32 Dev Module」即可完成环境搭建;

内置丰富的库管理器,可直接下载蓝牙 BLE 库(BLEDevice.h)、电机控制库(ESP32Servo.h)等;

集成代码编译、上传功能,支持一键将程序烧录至 ESP32(需通过 USB 转 TTL 模块连接);

兼容 Windows 10/11、macOS Ventura 等操作系统,满足不同用户的使用需求[7]

手机 APP 开发环境

采用MIT App Inventor 2在线开发平台(http://appinventor.mit.edu/ ),其特点为:

可视化编程界面,通过拖拽组件与积木块实现逻辑设计,无需编写代码;

支持蓝牙 BLE 组件(BluetoothLE),可直接与 ESP32 建立通信;

提供实时测试功能,通过「AI 伴侣」APP 可在手机上即时运行调试;

支持生成 APK 安装包,兼容 Android 6.0 及以上系统[8]

软件开发工具

为提高开发效率与代码质量,用到的辅助工具如表4. 1所示。

表4. 1辅助工具表

工具类型具体工具用途
代码编辑器VS Code(+Arduino 插件)编写 ESP32 代码,支持语法高亮与自动补全
仿真工具Proteus 8.15硬件电路仿真,验证接线逻辑是否正确
调试工具Serial Monitor查看 ESP32 的串口输出,调试程序运行状态
流程图工具Visio 2021绘制系统流程图、算法流程图
蓝牙调试工具nRF Connect测试 ESP32 的蓝牙 BLE 广播与数据传输功能

例如,使用 Serial Monitor 调试时,可通过Serial.println()函数输出变量值(如当前转速、距离),实时观察程序运行状态,快速定位逻辑错误。

软件框架设计

系统软件采用模块化设计,将功能划分为独立模块,通过主程序调度协同工作。软件框架如图 4.1 所示,分为ESP32 程序框架与APP 程序框架两部分。

ESP32 程序框架

ESP32 程序以setup()和loop()为核心,包含以下模块:

初始化模块

硬件初始化:GPIO、PWM、中断、蓝牙 BLE、传感器(测速 / 超声波);

变量初始化:速度目标值、PID 参数、蓝牙名称("ESP32-Car")。

蓝牙通信模块

接收 APP 指令:解析前进、后退、转向、速度调节等指令;

发送状态数据:将当前转速、距离障碍物距离等信息回传至 APP。

电机控制模块

正反转控制:根据指令设置 IN1-IN4 引脚电平;

PWM 调速:生成指定占空比的 PWM 信号,控制电机转速。

传感器数据处理模块

测速模块:通过外部中断计数脉冲,计算转速;

超声波模块:触发测距并计算距离,进行滤波处理。

控制算法模块

PID 调速算法:根据目标转速与实际转速的误差,动态调整 PWM 占空比;

避障决策算法:根据超声波测距结果,生成避障控制指令。

主程序模块

循环调度各模块,周期为 10ms;

处理异常情况(如电机堵转、蓝牙断开)。

APP 程序框架

手机 APP 程序以界面交互与蓝牙通信为核心,包含以下模块:

界面显示模块

控制界面:方向键(上 / 下 / 左 / 右)、速度滑块、停止按钮;

状态界面:显示当前速度(RPM)、距离障碍物距离(cm)、蓝牙连接状态。

蓝牙通信模块

设备搜索:扫描周围 BLE 设备,显示 "ESP32-Car";

连接管理:建立 / 断开与 ESP32 的蓝牙连接;

数据收发:发送控制指令(如 "F,200" 表示前进,速度 200RPM),接收状态数据。

逻辑控制模块

指令编码:将用户操作转换为约定格式的字符串(如左转指令为 "L,150");

数据解析:将 ESP32 回传的状态数据(如 "S,198,D,25")转换为可读格式并显示。

图片4.1.png
图4.1软件框架
Figure 4.1 Software Architecture

软件流程图

ESP32 主程序流程图

ESP32 主程序的工作流程如图 4.2 所示,具体步骤如下:

系统上电后,执行setup()函数:

初始化硬件(GPIO、PWM、中断、传感器);

初始化蓝牙 BLE,设置设备名称为 "ESP32-Car",开启广播;

初始化变量(目标转速 = 0,PID 参数 = 默认值)。

进入loop()循环(周期 10ms):

检查蓝牙连接状态:若断开,停止电机并等待重连;

接收蓝牙指令:若有新指令,解析并更新目标状态(如前进、转速 200RPM);

读取传感器数据:获取当前转速与障碍物距离;

执行控制算法:

若处于手动模式,直接按目标转速输出 PWM;

若处于自动避障模式,根据距离调整转速或转向;

发送状态数据:通过蓝牙回传当前转速与距离;

延时 10ms,进入下一次循环。

图片4.2.png
图4.2热控模式系统软件流程
Figure 4.2 Thermal control mode system software process

图片4.3.png
图4.3(1)声控模式系统软件流程(2)光控模式系统软件流程
Figure 4.3 (1) Voice Control Mode System Software Process (2) Optical Control Mode System Software Process

PID 调速算法流程图

PID 调速算法的流程图如图 4.3 所示,具体步骤如下:

设定目标转速n_{target}(如 200RPM);

读取测速模块的实际转速n_{actual}

计算转速误差e = n_{target} - n_{actual}

计算 PID 输出:

比例项:

P = K_{p} \times e

积分项:

I = I_{prev} + K_{i} \times e \times T \quad (T\text{ 为采样周期 }0.01\,s)

微分项:

D = K_{d} \times (e - e_{prev})/T

总输出:

U = P + I + D

限幅处理:将U限制在 0-100(对应 PWM 占空比 0%-100%);

抗积分饱和处理:若U达到上限 / 下限,停止积分项累加;

更新参数:保存当前误差e与积分项I,用于下一次计算;

输出 PWM 占空比U,控制电机转速。

自动避障程序流程图

自动避障程序的流程图如图 4.4 所示,具体步骤如下:

读取超声波模块的测距结果d(单位:cm);

判断距离范围:

若d≥30cm:正常行驶,按当前速度前进;

若20cm≤d<30cm:减速行驶,速度降至 50%;

若d<20cm:

停止前进(0.5s);

随机选择左转或右转(通过随机数生成器决定);

转向持续随机毫秒数(400ms-1000ms)后,恢复前进。

硬件的设置与初始化

硬件初始化是确保系统正常工作的基础,通过代码配置 ESP32 的外设与传感器,具体初始化函数如下:、

GPIO 初始化

void initGPIO() {

// 电机驱动引脚(IN1-IN4)

pinMode(IN1, OUTPUT); // GPIO2

pinMode(IN2, OUTPUT); // GPIO3

pinMode(IN3, OUTPUT); // GPIO10

pinMode(IN4, OUTPUT); // GPIO6

// 超声波模块引脚

pinMode(TRIG, OUTPUT); // GPIO18

pinMode(ECHO, INPUT); // GPIO19

// 测速模块引脚(外部中断)

pinMode(LEFT_SPEED, INPUT_PULLUP); // GPIO0

pinMode(RIGHT_SPEED, INPUT_PULLUP); // GPIO1

// 初始化电机停止状态

digitalWrite(IN1, LOW);

digitalWrite(IN2, LOW);

digitalWrite(IN3, LOW);

digitalWrite(IN4, LOW);

}

PWM 初始化

void initPWM() {

// 左电机PWM(IN1为PWM输出)

ledcSetup(LEFT_PWM_CHANNEL, 25000, 10); // 频率25kHz,分辨率10位(0-1023)

ledcAttachPin(IN1, LEFT_PWM_CHANNEL); // 绑定IN1(GPIO2)到PWM通道0

ledcAttachPin(IN2, LEFT_PWM_CHANNEL); // 绑定IN2(GPIO3)到PWM通道0

// 右电机PWM(IN3为PWM输出)

ledcSetup(RIGHT_PWM_CHANNEL, 25000, 10); // 频率25kHz,分辨率10位

ledcAttachPin(IN3, RIGHT_PWM_CHANNEL); // 绑定IN3(GPIO10)到PWM通道1

ledcAttachPin(IN4, RIGHT_PWM_CHANNEL); // 绑定IN4(GPIO6)到PWM通道1

// 初始占空比0(停止)

ledcWrite(LEFT_PWM_CHANNEL, 0);

ledcWrite(RIGHT_PWM_CHANNEL, 0);

}

外部中断初始化(测速模块)

void initInterrupt() {

// 左测速模块:上升沿触发中断

attachInterrupt(digitalPinToInterrupt(LEFT_SPEED), leftSpeedISR, RISING);

// 右测速模块:上升沿触发中断

attachInterrupt(digitalPinToInterrupt(RIGHT_SPEED), rightSpeedISR, RISING);

}

// 左测速中断服务程序(计数脉冲)

void leftSpeedISR() {

leftPulseCount++;

}

// 右测速中断服务程序(计数脉冲)

void rightSpeedISR() {

rightPulseCount++;

}

蓝牙 BLE 初始化

void initBLE() {

BLEDevice::init("ESP32-Car"); // 设备名称

BLEServer \*pServer = BLEDevice::createServer(); // 创建BLE服务器

BLEService \*pService = pServer-\>createService(SERVICE_UUID); // 创建服务

// 创建特征值(用于收发数据)

pTxCharacteristic = pService-\>createCharacteristic(

CHARACTERISTIC_UUID_TX,

BLECharacteristic::PROPERTY_READ \| BLECharacteristic::PROPERTY_NOTIFY

);

pRxCharacteristic = pService-\>createCharacteristic(

CHARACTERISTIC_UUID_RX,

BLECharacteristic::PROPERTY_WRITE

);

// 设置接收回调函数

pRxCharacteristic-\>setCallbacks(new MyCallbacks());

pService-\>start(); // 启动服务

pServer-\>getAdvertising()-\>start(); // 开始广播

Serial.println("BLE初始化完成,等待连接...");

}

定时器初始化(用于 PID 计算)

void initTimer() {

// 定时器0:10ms触发一次(用于PID计算)

timer = timerBegin(0, 80, true); // 预分频80(时钟80MHz/80=1MHz)

timerAttachInterrupt(timer, &timerISR, true); // 绑定中断函数

timerAlarmWrite(timer, 10000, true); // 10000us = 10ms

timerAlarmEnable(timer); // 使能定时器

}

// 定时器中断服务程序(执行PID计算)

void IRAM_ATTR timerISR() {

pidFlag = true; // 置位PID计算标志

}

数据采集与工程量的转换

转速计算(基于测速模块)

1. 原始数据:单位时间内的脉冲数(leftPulseCount、rightPulseCount);

2. 转换公式:

已知码盘为 20 格,即电机每转 1 圈产生 20 个脉冲,采样时间T=0.1s(10 次 10ms 定时器中断):

\text{转速}(RPM) = \frac{\text{脉冲数} \times 60}{20 \times T} = \frac{\text{脉冲数} \times 60}{20 \times 0.1} = \text{脉冲数} \times 30

代码实现:

void calculateSpeed() {

// 读取脉冲数(关闭中断防止计数错误)

noInterrupts();

leftPulse = leftPulseCount;

rightPulse = rightPulseCount;

leftPulseCount = 0;

rightPulseCount = 0;

interrupts();

// 计算转速(RPM)

leftSpeed = leftPulse \* 30;

rightSpeed = rightPulse \* 30;

}

距离计算(基于超声波模块)

1. 原始数据:回声信号高电平持续时间(duration,单位:μs);

2. 转换公式:

超声波在空气中传播速度为 340m/s = 0.034cm/μs,距离为往返路程的一半:

\text{距离}(cm) = \frac{duration \times 0.034}{2}

3. 代码实现:

float measureDistance() {

digitalWrite(TRIG, LOW);

delayMicroseconds(2);

digitalWrite(TRIG, HIGH);

delayMicroseconds(10);

digitalWrite(TRIG, LOW);

long duration = pulseIn(ECHO, HIGH); // 读取回声时间(μs)

float distance = duration \* 0.034 / 2; // 计算距离(cm)

// 中位数滤波(5次采样)

static float distBuffer\[5\];

static int bufIndex = 0;

distBuffer\[bufIndex++\] = distance;

if (bufIndex \>= 5) bufIndex = 0;

// 排序取中位数

float sorted\[5\];

memcpy(sorted, distBuffer, sizeof(distBuffer));

for (int i = 0; i \< 4; i++) {

for (int j = 0; j \< 4 - i; j++) {

if (sorted\[j\] \> sorted\[j + 1\]) {

float temp = sorted\[j\];

sorted\[j\] = sorted\[j + 1\];

sorted\[j + 1\] = temp;

}

}

}

return sorted\[2\]; // 返回中位数

}

数据校验与异常处理

1. 转速异常处理:

当转速>500RPM(超过电机最大转速),视为异常,采用上一次有效值;

当连续 5 次转速为 0 且目标转速>0,判定为电机堵转,触发保护(停止输出 500ms)。

2. 距离异常处理:

当距离<2cm 或>400cm,视为无效值,采用上一次有效值;

当连续 3 次测距失败,点亮 ESP32 板载 LED 报警。

电机控制程序设计

电机控制程序需实现正反转控制、转速调节、差速转向等功能,基于 MX1919 驱动模块的逻辑真值表设计。

正反转控制

1. 前进控制:

void setMotorForward(int speed) {

// 左电机正转:IN1=H,IN2=L

digitalWrite(IN1, HIGH);

digitalWrite(IN2, LOW);

// 右电机正转:IN3=H,IN4=L

digitalWrite(IN3, HIGH);

digitalWrite(IN4, LOW);

// 设置PWM占空比(转速)

setMotorSpeed(speed, speed);

}

2. 后退控制:

void setMotorBackward(int speed) {

// 左电机反转:IN1=L,IN2=H

digitalWrite(IN1, LOW);

digitalWrite(IN2, HIGH);

// 右电机反转:IN3=L,IN4=H

digitalWrite(IN3, LOW);

digitalWrite(IN4, HIGH);

// 设置PWM占空比(转速)

setMotorSpeed(speed, speed);

}

3. 停止控制:

void setMotorStop() {

// 所有控制引脚置低

digitalWrite(IN1, LOW);

digitalWrite(IN2, LOW);

digitalWrite(IN3, LOW);

digitalWrite(IN4, LOW);

// PWM占空比0

setMotorSpeed(0, 0);

}

转向控制(差速原理)

1. 左转控制:左电机减速,右电机正常转速

void setMotorLeft(int speed) {

digitalWrite(IN1, HIGH);

digitalWrite(IN2, LOW);

digitalWrite(IN3, HIGH);

digitalWrite(IN4, LOW);

// 左电机转速为右电机的50%(差速转向)

setMotorSpeed(speed \* 0.5, speed);

}

2. 右转控制:右电机减速,左电机正常转速

void setMotorRight(int speed) {

digitalWrite(IN1, HIGH);

digitalWrite(IN2, LOW);

digitalWrite(IN3, HIGH);

digitalWrite(IN4, LOW);

// 右电机转速为左电机的50%(差速转向)

setMotorSpeed(speed, speed \* 0.5);

}

PWM 占空比转换

PWM 占空比(0-100%)需转换为 ESP32 的 PWM 寄存器值(0-1023,10 位分辨率):

void setMotorSpeed(int leftSpeed, int rightSpeed) {

// 限制转速范围0-350RPM(电机最大转速)

leftSpeed = constrain(leftSpeed, 0, 350);

rightSpeed = constrain(rightSpeed, 0, 350);

// 转速→占空比映射(350RPM对应100%占空比)

int leftDuty = map(leftSpeed, 0, 350, 0, 1023);

int rightDuty = map(rightSpeed, 0, 350, 0, 1023);

// 输出PWM

ledcWrite(LEFT_PWM_CHANNEL, leftDuty);

ledcWrite(RIGHT_PWM_CHANNEL, rightDuty);

}

PID 调速系统程序设计

PID 参数初始化

// PID参数结构体

typedef struct {

float kp; // 比例系数

float ki; // 积分系数

float kd; // 微分系数

float target; // 目标值

float actual; // 实际值

float error; // 当前误差

float lastError;// 上一次误差

float integral; // 积分项

float derivative;// 微分项

float output; // 输出

float maxOut; // 输出最大值

float minOut; // 输出最小值

} PID_t;

// 左、右电机PID实例

PID_t leftPID = {1.2, 0.05, 0.1, 0, 0, 0, 0, 0, 0, 0, 1023, 0};

PID_t rightPID = {1.2, 0.05, 0.1, 0, 0, 0, 0, 0, 0, 0, 1023, 0};

PID 计算函数(含抗积分饱和)

void pidCalculate(PID_t \*pid) {

// 计算当前误差

pid-\>error = pid-\>target - pid-\>actual;

// 比例项

pid-\>output = pid-\>kp \* pid-\>error;

// 积分项(抗积分饱和:输出未达限幅时才累加)

if (pid-\>output \< pid-\>maxOut && pid-\>output \> pid-\>minOut) {

pid-\>integral += pid-\>ki \* pid-\>error \* 0.01; // 采样周期0.01s

// 积分限幅(防止积分饱和)

pid-\>integral = constrain(pid-\>integral, -pid-\>maxOut, pid-\>maxOut);

pid-\>output += pid-\>integral;

}

// 微分项(抑制高频噪声)

pid-\>derivative = pid-\>kd \* (pid-\>error - pid-\>lastError) / 0.01;

pid-\>output += pid-\>derivative;

// 输出限幅

pid-\>output = constrain(pid-\>output, pid-\>minOut, pid-\>maxOut);

// 保存当前误差

pid-\>lastError = pid-\>error;

}

PID 调速主程序

在定时器中断中周期性(10ms)执行 PID 计算:

void pidControlLoop() {

// 更新实际转速

leftPID.actual = leftSpeed;

rightPID.actual = rightSpeed;

// 计算PID输出

pidCalculate(&leftPID);

pidCalculate(&rightPID);

// 输出PWM

ledcWrite(LEFT_PWM_CHANNEL, (int)leftPID.output);

ledcWrite(RIGHT_PWM_CHANNEL, (int)rightPID.output);

}

积分饱和问题及解决

积分饱和现象:当目标值与实际值偏差较大时,积分项持续累加导致输出超过限幅,系统响应延迟。

解决方法:采用抗积分饱和算法,即当输出达到限幅时停止积分项累加(已在pidCalculate函数中实现)。

自动避障程序设计

避障逻辑实现

void obstacleAvoidance() {

float distance = measureDistance(); // 获取距离

if (distance \>= 30) {

// 距离充足,正常前进

setMotorForward(leftPID.target);

} else if (distance \>= 20 && distance \< 30) {

// 距离较近,减速前进

setMotorForward(leftPID.target \* 0.5);

} else {

// 距离过近,避障

setMotorStop();

delay(500); // 停止0.5s

// 随机左转或右转

if (random(2) == 0) {

setMotorLeft(leftPID.target \* 0.7);

} else {

setMotorRight(leftPID.target \* 0.7);

}

delay(1000); // 转向1s

}

}

避障状态机设计

typedef enum {

STATE_FORWARD, // 前进

STATE_DECELERATE,// 减速

STATE_STOP, // 停止

STATE_TURN_LEFT, // 左转

STATE_TURN_RIGHT // 右转

} ObstacleState;

ObstacleState currentState = STATE_FORWARD;

void obstacleStateMachine() {

float distance = measureDistance();

switch (currentState) {

case STATE_FORWARD:

if (distance \< 20) currentState = STATE_STOP;

else if (distance \< 30) currentState = STATE_DECELERATE;

break;

case STATE_DECELERATE:

if (distance \>= 30) currentState = STATE_FORWARD;

else if (distance \< 20) currentState = STATE_STOP;

break;

case STATE_STOP:

currentState = (random(2) == 0) ? STATE_TURN_LEFT : STATE_TURN_RIGHT;

break;

case STATE_TURN_LEFT:

case STATE_TURN_RIGHT:

currentState = STATE_FORWARD; // 转向后恢复前进

break;

}

// 执行状态动作

switch (currentState) {

case STATE_FORWARD: setMotorForward(leftPID.target); break;

case STATE_DECELERATE: setMotorForward(leftPID.target \* 0.5); break;

case STATE_STOP: setMotorStop(); break;

case STATE_TURN_LEFT: setMotorLeft(leftPID.target \* 0.7); break;

case STATE_TURN_RIGHT: setMotorRight(leftPID.target \* 0.7); break;

}

}

APP Inventor 程序设计

APP 界面设计

1. 组件布局:

顶部:蓝牙连接按钮、连接状态标签;

中部:方向控制区(上 / 下 / 左 / 右按钮)、停止按钮;

底部:速度滑块(0-350RPM)、状态显示区(速度、距离)。

2. 核心组件属性:

蓝牙组件(BluetoothLE):用于搜索和连接 ESP32;

按钮(Button):btnForward、btnBackward等,触发控制指令;

滑块(Slider):sldSpeed,范围 0-350,步长 10;

标签(Label):lblSpeed、lblDistance,显示状态数据。

APP 逻辑设计(积木块)

1. 蓝牙连接逻辑

2. 控制指令发送

3. 状态数据接收

系统调试与性能验证

在本文的前几章主要介绍了课题的系统结构和要确定的实施方案,完成系统硬件的选择、软件系统的搭建,这就为后期的数据调试打下好的软件基础。本章主要介绍系统调试的主要步骤如软硬件调试、关键器件测试、调试数据与问题分析等。

系统调试环境搭建

硬件调试环境

1. 工具清单:

万用表:测量电压、电流,检查电路通断;

示波器:观察 PWM 波形、传感器信号(如超声波回声);

2. 搭建步骤:

第一步:将 ESP32、电机驱动模块、传感器等组装到小车上。

第二步:通过面包板临时搭建电路,采用杜邦线连接各模块,便于修改接线;

第三步:将小车固定在调试台(避免调试时移动),电机轴空载(未安装车轮);

第四步:安装电池、示波器探头(监测 PWM 输出引脚)。

软件调试环境

1. 调试工具配置:

Arduino IDE 串口监视器:波特率 115200,用于输出调试信息(如转速、距离);

ESP32 内置 JTAG 接口:通过 OpenOCD 工具进行在线调试,设置断点查看变量值;

nRF Connect:手机蓝牙调试 APP,用于测试 ESP32 的 BLE 广播与数据传输功能。

2. 调试步骤:

第一步:编写最小测试程序(如仅初始化蓝牙并发送固定字符串),验证基础功能;

第二步:逐步添加模块代码(电机控制→传感器采集→PID 算法),每步测试通过后再进行下一步;

第三步:使用串口打印关键变量(如Serial.print("leftSpeed: "); Serial.println(leftSpeed);),观察数据变化。

硬件调试与问题解决

系统调试主要分为软件调试与硬件调试。软件调试包含在线仿真、模块运行参数调试及软件优先级的判断;硬件调试主要是时硬件系统的实际应用性。除了软件和硬件调试还有关键器件的调试包含了芯片参数确定、输入信号和输出信号的灵敏度测试。

电源系统调试

1. 测试内容:

测量锂电池输出电压:空载 7.4V,带载(电机运行)7.2V,符合设计要求;

测量 DC/DC 降压模块输出:5V 模块输出 4.98V,3.3V 模块输出 3.29V,误差<1%;

测试最大电流:双电机满速运行时总电流 1.2A,小于锂电池最大放电电流 3A。

2. 问题与解决:

问题 1:ESP32 频繁复位,测量 3.3V 电压发现波动>0.2V。

解决:在 3.3V 输出端并联 100μF 电解电容,滤除电机启动产生的电压尖峰。

问题 2:电机运行时,超声波模块测距误差增大(±5cm)。

解决:将超声波模块电源与电机驱动电源分离,单独从 5V 降压模块取电,减少干扰。

电机驱动模块调试

1. 测试内容:

手动控制 IN1-IN4 引脚电平,验证电机正反转是否正常;

输出不同占空比的 PWM 信号,用示波器观察波形(频率 25kHz,占空比 0-100% 连续可调);

测量电机电流:空载电流 50mA,额定负载 150mA,符合参数要求。

2. 问题与解决:

问题 1:电机正转正常,反转时转速明显偏低。

解决:检查 MX1919 模块接线,发现 IN2 引脚虚焊,重新焊接后恢复正常。

问题 2:PWM 占空比>80% 时,电机运行噪声增大。

解决:将 PWM 频率从 20kHz 提高至 25kHz(超出人耳可听范围),噪声消除。

传感器模块调试

1. 测速模块测试:

手动转动电机轴,用逻辑分析仪观察脉冲信号:每转一圈产生 20 个脉冲,波形清晰无毛刺;

电机转速 200RPM 时,计算脉冲频率:f = (200/60) \times 20 = 66.7Hz,实测 66.5Hz,误差<0.3%。

2. 超声波模块测试:

固定障碍物距离(50cm),连续测量 10 次,结果为 49.8±0.2cm,精度符合要求;

测试不同距离(20cm、100cm、300cm),测量值与实际值误差均<1cm。

3. 问题与解决:

问题 1:测速模块偶尔漏记脉冲,导致转速计算偏低。

解决:将传感器与码盘距离从 8mm 调整至 5mm,增强红外信号接收强度。

问题 2:超声波模块在强光下测距误差增大。

解决:在传感器表面粘贴黑色遮光胶带,减少环境光干扰。

软件调试与问题解决

蓝牙通信调试

1. 测试内容:

手机 APP 搜索 "ESP32-Car" 设备,连接成功率;

发送控制指令(如 "F,200"),ESP32 接收并解析正确率;

连续传输 1000 条状态数据,丢包率<0.5%。

2. 问题与解决:

问题 1:APP程序报错,无法请求获取到蓝牙权限。

解决:更换高app inventor版本。

问题 2:手机能搜索小车到但是无法连接到蓝牙。

解决:经过多次实验,更换多次app inventor版本,以及更换UUID,最终发现只有在老师的示例程序上修改才能正常连接,重做APP解决。

PID 调速算法调试

1. 参数整定步骤:

第一步:置K_{i} = 0K_{d} = 0,逐步增大K_{p}至系统出现轻微振荡;

第二步:增大K_{d}抑制振荡,至响应速度最快且无超调;

第三步:逐步增大K_{i}消除稳态误差。

2. 动态性能测试:

目标转速从 100RPM 阶跃至 200RPM,超调量<5%,调节时间<0.5s;

加载测试:在电机轴上增加 50g 负载,转速下降<3%,PID 算法 100ms 内补偿至目标值。

3. 问题与解决:

问题 1:启动时转速超调量大(>15%)。

解决:增加积分分离策略。

问题 2:低速(<50RPM)时转速波动大(±10RPM)。

解决:在 PID 输出中加入死区补偿,增强低速稳定性。

自动避障程序调试

1. 测试场景:

场景 1:前方 30cm 处放置障碍物,小车减速至 50% 转速;

场景 2:前方 15cm 处放置障碍物,小车停止并转向。

2. 问题与解决:

问题 1:转向后偶尔偏离原方向,出现原地打转。

解决:优化转向时间计算,根据当前速度动态调整(速度越高,转向时间越短)。

问题 2:超声波模块偶尔误报近距离(如实际 30cm,测量为 15cm)。

解决:在避障逻辑中增加连续判断,需 3 次测量均<20cm 才触发避障动作。

系统性能验证

控制距离测试

在无遮挡环境下,测试手机 APP 与 ESP32 的蓝牙通信距离:

有效控制距离:18 米(超出后指令延迟>100ms);

最大通信距离:25 米(超出后连接断开);

有遮挡(1 堵 24cm 砖墙):有效距离 8 米,满足室内使用需求。

直线行驶精度测试

在平整地面上标记 10 米直线,测试小车直线行驶偏差:

低速(100RPM):终点偏差 0.2cm;

中速(200RPM):终点偏差 0.5cm;

高速(350RPM):终点偏差 0.8cm;

结论:偏差均<10cm,满足设计要求(<5cm/m)。

避障响应时间测试

设置障碍物距离从 50cm 快速移动至 15cm,测试系统响应时间:

从检测到障碍物到开始减速:100ms;

从检测到近距离到完全停止:200ms;

结论:响应时间<300ms,符合设计指标。

本章小结

本章通过硬件调试验证了电源系统、电机驱动、传感器模块的稳定性,解决了电压波动、信号干扰等问题;通过软件调试优化了蓝牙通信可靠性、PID 参数与避障逻辑,使系统动态性能达到设计目标。性能测试结果表明,小车的控制距离、续航时间、直线精度、避障响应均满足预期要求,系统整体功能实现完整。

总结与心得体会

总结

本设计基于 ESP32 单片机开发了一款集蓝牙遥控、PID 调速、自动避障功能于一体的智能小车,主要成果如下:

硬件设计:采用模块化架构,完成 ESP32 最小系统、电源模块(锂电池 + DC/DC 降压)、电机驱动模块(MX1919)、传感器模块(测速 + 超声波)的选型与接线,硬件整体尺寸为 25cm×15cm×10cm,满足小型化要求。

软件实现:

嵌入式程序:基于 Arduino IDE 开发,实现蓝牙 BLE 通信、PID 调速、自动避障等功能,代码模块化率>80%,便于维护与扩展;

手机 APP:通过 APP Inventor 开发,提供直观的控制界面与状态显示,支持前进、后退、转向等操作。

性能指标:

通信:蓝牙有效距离 18 米,指令延迟<100ms;

调速:转速范围 50-350RPM,稳态误差<±3%;

避障:检测距离 2-400cm,响应时间<300ms。

创新点:

采用抗积分饱和 PID 算法,提高调速系统的动态性能;

融合蓝牙通信与传感器数据,实现手动控制与自动避障的无缝切换;

模块化设计降低开发难度。

心得体会

通过本次课程设计,我深刻体会到理论与实践相结合的重要性,具体收获如下:

1. 知识整合能力:将《电力电子技术》中的直流调速原理、《自动控制原理》中的 PID 算法、《嵌入式系统》中的单片机编程等知识融会贯通,理解了各学科知识在实际系统中的应用逻辑。

2. 问题解决能力:调试过程中遇到了诸多问题(如蓝牙断连、电机转速波动),通过查阅资料(如 ESP32 数据手册、PID 参数整定方法)、对比分析(更换元件测试、修改参数验证),逐步找到解决方案,培养了系统性思维。

3. 团队协作意识:同组成员分工合作(硬件焊接、软件编程、文档撰写),遇到问题共同讨论,如在 PID 参数整定时,通过对比不同组合的调试结果,最终确定最优参数,效率显著提升。

4. 工程实践认知:认识到理论设计与实际实现的差距,如仿真中稳定的 PID 参数在实际系统中可能因噪声干扰而失效,需要根据硬件特性动态调整,体现了工程实践的灵活性与严谨性。

本次设计虽已完成,但仍有改进空间:可增加 Wi-Fi 模块实现远程控制,引入循迹传感器扩展路径规划功能,通过web控制小车,避免APP出现问题,也可以实时更新。通过本次实践,我不仅掌握了智能小车的开发方法,更培养了工程实践能力,为后续学习与工作奠定了坚实基础。

参考文献

  1. 梅晓榕,柏桂珍,张卯瑞,等。自动控制元件及线路 [M]. 北京:科学出版社,2005:9-10.

  2. 王兆安,刘进军。电力电子技术 [M]. 北京:机械工业出版社,2019:120-125.

  3. 张毅刚,彭喜元。单片机原理及接口技术 [M]. 北京:高等教育出版社,2020:88-92.

  4. 谢龙汉,黄书铭.MATLAB/Simulink 从入门到精通 [M]. 北京:北京航空航天大学出版社,2018:210-215.

  5. Espressif Systems. ESP32 Technical Reference Manual[Z]. 2022.

  6. 陈立万,李太福。智能控制技术与应用 [M]. 重庆:重庆大学出版社,2017:56-60.

  7. 周润景,张丽敏.Altium Designer 原理图与 PCB 设计 [M]. 北京:电子工业出版社,2021:30-35.

  8. 王树森,刘军。蓝牙技术原理与应用 [M]. 北京:清华大学出版社,2019:78-83.

完整代码

#include <Arduino.h>
#include <BLE2902.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
// PID 控制参数
struct PIDConfig {
    float Kp = 15.0;          // 比例系数
    float Ki = 0.0;           // 积分系数
    float Kd = 0.0;           // 微分系数
    float targetRPM = 0.0;    // 目标转速 (RPM)
    float currentRPM = 0.0;   // 当前转速 (RPM)
    float currentPWM = 0.0;   // 当前PWM值
    float lastSetRPM = 100.0; // 保存最后设置的RPM值,默认100
    float outputMin = 0.0;
    float outputMax = 1023.0; // 最大PWM值为1023
    float errorPrev = 0.0;
    float integral = 0.0;
    bool isFirstMove = true;    // 用于跟踪是否是初始移动
    const float maxRPM = 350.0; // 最大转速为350 RPM
    // 打印当前PID参数
    void printPIDParams() {
        Serial.println("\n当前PID参数:");
        Serial.print("Kp = ");
        Serial.println(Kp, 3);
        Serial.print("Ki = ");
        Serial.println(Ki, 3);
        Serial.print("Kd = ");
        Serial.println(Kd, 3);
        Serial.println("------------------------");
    }
};
// 创建全局PIDConfig实例
PIDConfig pidConfig;
// 串口绘图开关
bool enablePlotter = false;
BLEServer *pServer = NULL;
BLECharacteristic *pTxCharacteristic;
bool deviceConnected = false;
int PWM_Left;
int PWM_Right;
int rcv;
int IN1 = 2;
int IN2 = 3;
int IN3 = 10;
int IN4 = 6;
int ControlMode;
const int freq = 25000;    // 设置频率
const int resolution = 10; // 计数位数,取值 0 ~ 20
int max_PWMValue = pow(2, resolution) - 1;
float dutyCycle = 0.8;
double SpeedLeft;
double SpeedRight;
String String_Received;
unsigned long lastMsg;
// 超声波传感器引脚定义
const int trigPin = 18;
const int echoPin = 19;
// 避障距离阈值(厘米)
const int SLOW_DISTANCE = 30; // 减速距离阈值
const int STOP_DISTANCE = 20; // 停止距离阈值
// 避障状态标志
bool isAvoidingObstacle = false;
// 避障持续时间(毫秒)
unsigned long obstacleAvoidanceStartTime = 0;
const unsigned long obstacleAvoidanceDuration = 2000; // 避障持续2秒
// UUID 定义
#define SERVICE_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_RX "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
#define CHARACTERISTIC_UUID_TX "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
// 存储避障前的状态
struct AvoidanceMemory {
    float originalPWM;
    int originalDirection;
    float originalRPM;
} avoidanceMemory;
// 将0-1023的PWM值映射到0-255范围
int mapPWM(int value) { return map(value, 0, 1023, 0, 255); }
// 提前声明所有控制函数
void stop() {
    analogWrite(IN1, 0);
    analogWrite(IN2, 0);
    analogWrite(IN3, 0);
    analogWrite(IN4, 0);
    Serial.println("Stop");
}
void Forward();    // 前向声明
void Backward();   // 前向声明
void turn_left();  // 前向声明
void turn_right(); // 前向声明
class MyServerCallbacks : public BLEServerCallbacks {
    void onConnect(BLEServer *pServer) {
        Serial.println("蓝牙已连接");
        deviceConnected = true;
    };
    void onDisconnect(BLEServer *pServer) {
        Serial.println("蓝牙已断开");
        deviceConnected = false;
        delay(500);
        pServer->startAdvertising();
    }
};
// 数据接收回调类
class MyCallbacks : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
        String rxValue(pCharacteristic->getValue().c_str());
        if (rxValue.length() > 0) {
            Serial.println("*********");
            Serial.print("收到新信息: ");
            String_Received = "";
            for (int i = 0; i < rxValue.length(); i++) {
                String_Received += (char)rxValue[i];
                Serial.print(rxValue[i]);
            }
            Serial.println("*********");
            // 检查是否是RPM设置命令
            if (String_Received.endsWith("RPM")) {
                String TempString = String_Received.substring(0, String_Received.length() - 3);
                // 直接使用输入值作为RPM,范围0-255
                float newRPM = constrain(TempString.toFloat(), 0, pidConfig.maxRPM);
                // 设置目标RPM和保存的RPM
                pidConfig.targetRPM = newRPM;
                pidConfig.lastSetRPM = newRPM; // 保存设置的RPM值
                // 初始PWM估计值 (非线性映射,考虑电机特性)
                float initialPWM;
                if (newRPM < 70) { // 低速区域
                    initialPWM = map(newRPM, 0, 70, 0, 400);
                } else if (newRPM < 200) { // 中速区域
                    initialPWM = map(newRPM, 70, 200, 400, 700);
                } else { // 高速区域
                    initialPWM = map(newRPM, 200, 350, 700, pidConfig.outputMax);
                }
                // 设置PWM值
                pidConfig.currentPWM = initialPWM;
                PWM_Left = initialPWM;
                PWM_Right = initialPWM;
                Serial.print("设置目标RPM为: ");
                Serial.print(newRPM, 2);
                Serial.print(" 初始PWM值: ");
                Serial.println(initialPWM);
            } else {
                rcv = String_Received.toInt();
                if (rcv == 5) { // 停止命令
                    pidConfig.targetRPM = 0;
                    pidConfig.currentPWM = 0;
                    PWM_Left = 0;
                    PWM_Right = 0;
                    stop();
                } else if (rcv >= 1 && rcv <= 4) {
                    // 使用保存的RPM值
                    pidConfig.targetRPM = pidConfig.lastSetRPM;
                    // 根据保存的RPM值计算初始PWM
                    float initialPWM;
                    if (pidConfig.lastSetRPM < 70) {
                        initialPWM = map(pidConfig.lastSetRPM, 0, 70, 0, 400);
                    } else if (pidConfig.lastSetRPM < 200) {
                        initialPWM = map(pidConfig.lastSetRPM, 70, 200, 400, 700);
                    } else {
                        initialPWM = map(pidConfig.lastSetRPM, 200, 350, 700, pidConfig.outputMax);
                    }
                    pidConfig.currentPWM = initialPWM;
                    PWM_Left = initialPWM;
                    PWM_Right = initialPWM;
                    Serial.print("使用保存的RPM值: ");
                    Serial.print(pidConfig.lastSetRPM);
                    Serial.print(" PWM值: ");
                    Serial.println(initialPWM);
                }
                Serial.print("rcv: ");
                Serial.println(rcv);
            }
        }
    }
};
// 实现其他控制函数
void Forward() {
    // 同步更新两个轮子的PWM值
    int mappedLeft = (PWM_Left < 50) ? 0 : mapPWM(PWM_Left);
    int mappedRight = (PWM_Right < 50) ? 0 : mapPWM(PWM_Right);
    // 同时设置所有PWM值 - 两个电机都向前
    analogWrite(IN1, 0);
    analogWrite(IN2, mappedRight);
    analogWrite(IN3, mappedLeft);
    analogWrite(IN4, 0);
    Serial.print("Forward - PWM Left: ");
    Serial.print(PWM_Left);
    Serial.print(" PWM Right: ");
    Serial.println(PWM_Right);
}
void Backward() {
    // 同步更新两个轮子的PWM值
    int mappedLeft = (PWM_Left < 50) ? 0 : mapPWM(PWM_Left);
    int mappedRight = (PWM_Right < 50) ? 0 : mapPWM(PWM_Right);
    // 同时设置所有PWM值 - 两个电机都向后
    analogWrite(IN1, mappedRight);
    analogWrite(IN2, 0);
    analogWrite(IN3, 0);
    analogWrite(IN4, mappedLeft);
    Serial.print("Backward - PWM Left: ");
    Serial.print(PWM_Left);
    Serial.print(" PWM Right: ");
    Serial.println(PWM_Right);
}
void turn_left() {
    // 左转时保持两个轮子的独立控制
    int mappedLeft = (PWM_Left < 50) ? 0 : mapPWM(PWM_Left);
    int mappedRight = (PWM_Right < 50) ? 0 : mapPWM(PWM_Right);
    // 左轮后退,右轮前进
    analogWrite(IN1, 0);
    analogWrite(IN2, mappedRight);
    analogWrite(IN3, 0);
    analogWrite(IN4, mappedLeft);
    Serial.print("Turn Left - PWM Left: ");
    Serial.print(PWM_Left);
    Serial.print(" PWM Right: ");
    Serial.println(PWM_Right);
}
void turn_right() {
    // 右转时保持两个轮子的独立控制
    int mappedLeft = (PWM_Left < 50) ? 0 : mapPWM(PWM_Left);
    int mappedRight = (PWM_Right < 50) ? 0 : mapPWM(PWM_Right);
    // 左轮前进,右轮后退
    analogWrite(IN1, mappedRight);
    analogWrite(IN2, 0);
    analogWrite(IN3, mappedLeft);
    analogWrite(IN4, 0);
    Serial.print("Turn Right - PWM Left: ");
    Serial.print(PWM_Left);
    Serial.print(" PWM Right: ");
    Serial.println(PWM_Right);
}
struct SystemConfig {
    const int interruptPinLeft = 0;
    const int interruptPinRight = 1;
    const int pulsesPerRevolution = 40;
    const float wheelCircumference = 20;
    const long interval = 100;
    const int serialBaudRate = 115200;
};
struct SensorData {
    volatile int counter_left = 0;
    volatile int counter_right = 0;
    unsigned long previousMillis = 0;
};
struct SpeedData {
    float leftRPM = 0.0;
    float rightRPM = 0.0;
    float leftSpeed = 0.0;
    float rightSpeed = 0.0;
    int leftPulses = 0;
    int rightPulses = 0;
};
SystemConfig config;
SensorData sensorData;
SpeedData speedData;
void IRAM_ATTR handleInterruptLeft() { sensorData.counter_left++; }
void IRAM_ATTR handleInterruptRight() { sensorData.counter_right++; }
void setupSystem() {
    Serial.begin(config.serialBaudRate);
    while (!Serial) {
        ;
    }
    Serial.println("ESP32-C3 智能小车测速系统启动");
    pinMode(config.interruptPinLeft, INPUT_PULLDOWN);
    pinMode(config.interruptPinRight, INPUT_PULLDOWN);
    attachInterrupt(digitalPinToInterrupt(config.interruptPinLeft), handleInterruptLeft, CHANGE);
    attachInterrupt(digitalPinToInterrupt(config.interruptPinRight), handleInterruptRight, CHANGE);
    // 超声波传感器初始化
    pinMode(trigPin, OUTPUT);
    pinMode(echoPin, INPUT);
    Serial.println("\n系统配置:");
    Serial.print("脉冲/圈: ");
    Serial.println(config.pulsesPerRevolution);
    Serial.print("轮子周长(米): ");
    Serial.println(config.wheelCircumference);
    Serial.print("测速间隔(毫秒): ");
    Serial.println(config.interval);
    Serial.print("超声波避障阈值(厘米): ");
    Serial.println(SLOW_DISTANCE);
    Serial.println("------------------------");
}
// PID 控制器函数
float computePID(float targetRPM, float currentRPM, float /*currentPWM unused now*/) {
    // 如果目标转速为 0,直接返回 0 并重置状态
    if (targetRPM == 0) {
        pidConfig.integral = 0;
        pidConfig.errorPrev = 0;
        return 0;
    }
    const float dt = 0.1f;                // 采样周期 (s)
    float error = targetRPM - currentRPM; // 误差 (RPM)
    // ---------- P ----------
    float pTerm = pidConfig.Kp * error;
    // ---------- I ---------- (带防积分饱和)
    pidConfig.integral += error * dt;
    pidConfig.integral = constrain(pidConfig.integral, -2000.0f, 2000.0f);
    float iTerm = pidConfig.Ki * pidConfig.integral;
    // ---------- D ----------
    float derivative = (error - pidConfig.errorPrev) / dt;
    float dTerm = pidConfig.Kd * derivative;
    pidConfig.errorPrev = error;
    // PID 输出 (绝对 PWM)
    float output = pTerm + iTerm + dTerm;
    float newPWM = constrain(output, pidConfig.outputMin, pidConfig.outputMax);
    // 抗积分饱和:如果输出饱和,扣除本次积分增量
    if (newPWM == pidConfig.outputMax || newPWM == pidConfig.outputMin) {
        pidConfig.integral -= error * dt; // 撤销本次积分
    }
    // 绘图输出 (目标RPM,当前RPM,误差,P,I,D,PWM)
    if (enablePlotter) {
        Serial.print(targetRPM);
        Serial.print(",");
        Serial.print(currentRPM);
        Serial.print(",");
        Serial.print(error);
        Serial.print(",");
        Serial.print(pTerm);
        Serial.print(",");
        Serial.print(iTerm);
        Serial.print(",");
        Serial.print(dTerm);
        Serial.print(",");
        Serial.println(newPWM);
    }
    return newPWM;
}
// 计算速度
void calculateSpeed() {
    noInterrupts();
    speedData.leftPulses = sensorData.counter_left;
    speedData.rightPulses = sensorData.counter_right;
    sensorData.counter_left = 0;
    sensorData.counter_right = 0;
    interrupts();
    // 计算每秒转数
    float timeSeconds = config.interval / 1000.0;
    float leftRevolutionsPerSecond = (speedData.leftPulses / (float)config.pulsesPerRevolution) / timeSeconds;
    float rightRevolutionsPerSecond = (speedData.rightPulses / (float)config.pulsesPerRevolution) / timeSeconds;
    // 计算RPM并应用低通滤波
    const float alpha = 0.3; // 滤波系数 (0-1)
    float newLeftRPM = leftRevolutionsPerSecond * 60.0;
    float newRightRPM = rightRevolutionsPerSecond * 60.0;
    speedData.leftRPM = alpha * newLeftRPM + (1 - alpha) * speedData.leftRPM;
    speedData.rightRPM = alpha * newRightRPM + (1 - alpha) * speedData.rightRPM;
    // 计算线速度 (cm/s)
    speedData.leftSpeed = leftRevolutionsPerSecond * config.wheelCircumference;
    speedData.rightSpeed = rightRevolutionsPerSecond * config.wheelCircumference;
    if (enablePlotter) {
        Serial.print("Raw RPM: ");
        Serial.print(newLeftRPM);
        Serial.print(" Filtered RPM: ");
        Serial.println(speedData.leftRPM);
    }
}
// 打印速度数据
void printSpeedData() {
    Serial.print("左轮: 脉冲=");
    Serial.print(speedData.leftPulses);
    Serial.print(" | 转速=");
    Serial.print(speedData.leftRPM, 2);
    Serial.print(" RPM | 线速度=");
    Serial.print(speedData.leftSpeed, 3);
    Serial.print(" cm/s | ");
    Serial.print("右轮: 脉冲=");
    Serial.print(speedData.rightPulses);
    Serial.print(" | 转速=");
    Serial.print(speedData.rightRPM, 2);
    Serial.print(" RPM | 线速度=");
    Serial.print(speedData.rightSpeed, 3);
    Serial.println(" cm/s");
}
// 检测前方障碍物距离
float getObstacleDistance() {
    // 发送触发信号
    digitalWrite(trigPin, LOW);
    delayMicroseconds(2);
    digitalWrite(trigPin, HIGH);
    delayMicroseconds(10);
    digitalWrite(trigPin, LOW);
    // 读取回声信号
    long duration = pulseIn(echoPin, HIGH);
    // 计算距离(声速 340m/s,来回除以 2)
    float distance = duration * 0.034 / 2;
    return distance;
}
// 执行避障动作
void performObstacleAvoidance() {
    Serial.println("检测到障碍物,执行避障");
    isAvoidingObstacle = true;
    obstacleAvoidanceStartTime = millis();
    // 保存当前状态
    avoidanceMemory.originalPWM = pidConfig.currentPWM;
    avoidanceMemory.originalDirection = rcv;
    avoidanceMemory.originalRPM = pidConfig.targetRPM;
    // 先停止
    stop();
    delay(200); // 短暂停顿
    // 后退 - 使用较低的速度
    rcv = 2;                   // 设置为后退模式
    pidConfig.targetRPM = 100; // 使用较低的避障速度
    float backupPWM = map(100, 0, 255, 0, pidConfig.outputMax);
    pidConfig.currentPWM = backupPWM;
    PWM_Left = backupPWM;
    PWM_Right = backupPWM;
    Backward();  // 使用Backward来后退
    delay(1000); // 后退1秒
    // 随机选择转向和转向时间
    int turnTime = random(400, 1000); // 随机生成400-1000毫秒的转向时间
    if (random(2) == 0) {
        Serial.print("避障:向左转 ");
        Serial.print(turnTime);
        Serial.println("毫秒");
        rcv = 4;
        turn_left();
    } else {
        Serial.print("避障:向右转 ");
        Serial.print(turnTime);
        Serial.println("毫秒");
        rcv = 3;
        turn_right();
    }
    delay(turnTime); // 使用随机时间进行转向
    // 恢复原来的方向和速度
    rcv = avoidanceMemory.originalDirection;
    pidConfig.targetRPM = avoidanceMemory.originalRPM;
    pidConfig.currentPWM = avoidanceMemory.originalPWM;
    PWM_Left = avoidanceMemory.originalPWM;
    PWM_Right = avoidanceMemory.originalPWM;
    isAvoidingObstacle = false;
    Serial.println("避障完成,恢复原始状态");
    Serial.print("恢复方向: ");
    switch (rcv) {
    case 1:
        Serial.println("前进");
        Forward();
        break;
    case 2:
        Serial.println("后退");
        Backward();
        break;
    case 3:
        Serial.println("右转");
        turn_right();
        break;
    case 4:
        Serial.println("左转");
        turn_left();
        break;
    default:
        Serial.println("停止");
        stop();
        break;
    }
    Serial.print("恢复速度 RPM: ");
    Serial.print(pidConfig.targetRPM);
    Serial.print(" PWM: ");
    Serial.println(pidConfig.currentPWM);
}
// 根据距离调整速度
void adjustSpeedForObstacle(float distance) {
    if (distance < STOP_DISTANCE && rcv == 1) { // 只在前进时执行避障
        // 距离小于20cm,执行避障
        performObstacleAvoidance();
    } else if (distance < SLOW_DISTANCE && rcv == 1) { // 只在前进时执行减速
        // 距离小于30cm,减速
        float speedRatio = (distance - STOP_DISTANCE) / (SLOW_DISTANCE - STOP_DISTANCE);
        float targetPWM = avoidanceMemory.originalPWM * speedRatio;
        // 逐渐减速
        pidConfig.currentPWM = targetPWM;
        PWM_Left = targetPWM;
        PWM_Right = targetPWM;
        Serial.print("减速运行 - 距离: ");
        Serial.print(distance);
        Serial.print("cm, 速度比例: ");
        Serial.println(speedRatio);
    }
}
// 处理串口命令
void processSerialCommand() {
    if (Serial.available() > 0) {
        String command = Serial.readStringUntil('\n');
        command.trim(); // 移除多余的空格和换行符
        // 解析命令
        if (command.startsWith("PID")) {
            // 格式:PID p i d
            // 示例:PID 15.0 0.0 0.0
            command = command.substring(4); // 跳过"PID "
            float p = command.substring(0, command.indexOf(' ')).toFloat();
            command = command.substring(command.indexOf(' ') + 1);
            float i = command.substring(0, command.indexOf(' ')).toFloat();
            command = command.substring(command.indexOf(' ') + 1);
            float d = command.toFloat();
            // 更新PID参数
            pidConfig.Kp = p;
            pidConfig.Ki = i;
            pidConfig.Kd = d;
            // 打印更新后的参数
            if (!enablePlotter) {
                pidConfig.printPIDParams();
            }
        } else if (command == "GET_PID") {
            // 打印当前PID参数
            if (!enablePlotter) {
                pidConfig.printPIDParams();
            }
        } else if (command == "PLOT_ON") {
            enablePlotter = true;
            Serial.println("目标RPM,当前RPM,PWM值,误差");
        } else if (command == "PLOT_OFF") {
            enablePlotter = false;
            Serial.println("串口绘图已关闭");
        } else if (command == "HELP") {
            // 打印帮助信息
            if (!enablePlotter) {
                Serial.println("\nPID参数调整命令说明:");
                Serial.println("1. 设置PID参数:PID p i d");
                Serial.println(" 示例:PID 15.0 0.0 0.0");
                Serial.println("2. 获取当前PID参数:GET_PID");
                Serial.println("3. 开启串口绘图:PLOT_ON");
                Serial.println("4. 关闭串口绘图:PLOT_OFF");
                Serial.println("5. 显示帮助信息:HELP");
                Serial.println("------------------------");
            }
        }
    }
}
// 输出绘图数据
void outputPlotData() {
    if (enablePlotter) {
        float error = pidConfig.targetRPM - pidConfig.currentRPM;
        // 计算各个控制项的值
        float pTerm = pidConfig.Kp * error;
        float iTerm = pidConfig.Ki * pidConfig.integral;
        float dTerm = pidConfig.Kd * (error - pidConfig.errorPrev) / 0.1;
        Serial.print("目标RPM:");
        Serial.print(pidConfig.targetRPM);
        Serial.print(",当前RPM:");
        Serial.print(pidConfig.currentRPM);
        Serial.print(",PWM:");
        Serial.print(pidConfig.currentPWM);
        Serial.print(",P项:");
        Serial.print(pTerm);
        Serial.print(",I项:");
        Serial.print(iTerm);
        Serial.print(",D项:");
        Serial.println(dTerm);
    }
}
void setup() {
    setupSystem();
    Serial.begin(115200);
    lastMsg = 0;
    BLEBegin();
    ControlMode = 1;
    // 设置引脚模式
    pinMode(IN1, OUTPUT);
    pinMode(IN2, OUTPUT);
    pinMode(IN3, OUTPUT);
    pinMode(IN4, OUTPUT);
    // 初始化为停止状态
    stop();
    rcv = 5;
}
void loop() {
    unsigned long currentMillis = millis();
    // 处理串口命令
    processSerialCommand();
    // 距离检测与避障逻辑
    static unsigned long lastObstacleCheck = 0;
    if (currentMillis - lastObstacleCheck >= 100) { // 提高检测频率到100ms
        if (!isAvoidingObstacle && rcv != 5) {      // 只在非停止状态检测障碍物
            float distance = getObstacleDistance();
            // 打印距离信息(降低打印频率)
            static unsigned long lastDistancePrint = 0;
            if (currentMillis - lastDistancePrint >= 500 && !enablePlotter) {
                Serial.print("前方距离: ");
                Serial.print(distance);
                Serial.println(" cm");
                lastDistancePrint = currentMillis;
            }
            // 根据距离调整速度或执行避障
            adjustSpeedForObstacle(distance);
        }
        lastObstacleCheck = currentMillis;
    }
    // 更新速度数据
    if (currentMillis - sensorData.previousMillis >= 100) {
        sensorData.previousMillis = currentMillis;
        // 计算速度
        calculateSpeed();
        // 更新当前转速 (使用左轮速度作为参考)
        pidConfig.currentRPM = speedData.leftRPM;
        // 仅在非停止状态下应用PID控制
        if (rcv != 5) {
            // 计算新的PWM值
            float newPWM = computePID(pidConfig.targetRPM, pidConfig.currentRPM, pidConfig.currentPWM);
            pidConfig.currentPWM = newPWM;
            // 更新两个轮子的PWM值
            PWM_Left = (int)newPWM;
            PWM_Right = (int)newPWM;
            // 根据当前控制模式执行相应动作
            switch (rcv) {
            case 1:
                Forward();
                break;
            case 2:
                Backward();
                break;
            case 3:
                turn_right();
                break;
            case 4:
                turn_left();
                break;
            default:
                if (PWM_Left > 0) {
                    Forward(); // 默认前进
                }
                break;
            }
        }
        // 输出绘图数据
        outputPlotData();
        // 打印调试信息(降低打印频率,且只在非绘图模式下打印)
        static unsigned long lastPrint = 0;
        if (currentMillis - lastPrint >= 500 && !enablePlotter) {
            float errorRPM = pidConfig.targetRPM - pidConfig.currentRPM;
            float errorPercent = (pidConfig.targetRPM != 0) ? (errorRPM / pidConfig.targetRPM) * 100 : 0;
            Serial.println("\n-------- 控制状态信息 --------");
            Serial.print("控制模式: ");
            switch (rcv) {
            case 1:
                Serial.println("前进");
                break;
            case 2:
                Serial.println("后退");
                break;
            case 3:
                Serial.println("右转");
                break;
            case 4:
                Serial.println("左转");
                break;
            case 5:
                Serial.println("停止");
                break;
            default:
                Serial.println("未知");
                break;
            }
            Serial.println("\n速度数据:");
            Serial.print("目标RPM: ");
            Serial.print(pidConfig.targetRPM);
            Serial.print(" | 当前RPM: ");
            Serial.print(pidConfig.currentRPM);
            Serial.print(" | 误差RPM: ");
            Serial.print(errorRPM);
            Serial.print(" (");
            Serial.print(errorPercent, 1);
            Serial.print("%)");
            Serial.print(" | PWM值: ");
            Serial.println(pidConfig.currentPWM);
            Serial.println("\n------------------------\n");
            lastPrint = currentMillis;
        }
    }
    // 蓝牙通信部分
    if (deviceConnected) {
        unsigned long now = millis();
        if (now - lastMsg > 1000) {
            String stra = String(PWM_Left);
            String strb = ",";
            String strc = String(PWM_Right);
            String str_Send = stra + strb + strc;
            pTxCharacteristic->setValue((char *)str_Send.c_str());
            pTxCharacteristic->notify();
            lastMsg = now;
        }
    }
}
void BLEBegin() {
    BLEDevice::init("C3");
    pServer = BLEDevice::createServer();
    pServer->setCallbacks(new MyServerCallbacks());
    BLEService *pService = pServer->createService(SERVICE_UUID);
    pTxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_UUID_TX, BLECharacteristic::PROPERTY_NOTIFY);
    pTxCharacteristic->addDescriptor(new BLE2902());
    BLECharacteristic *pRxCharacteristic =
        pService->createCharacteristic(CHARACTERISTIC_UUID_RX, BLECharacteristic::PROPERTY_WRITE);
    pRxCharacteristic->setCallbacks(new MyCallbacks());
    pService->start();
    pServer->getAdvertising()->start();
    Serial.println("Waiting a client connection to notify...");
}