ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ESP32+Python自动驾驶小车:从硬件接线到PID闭环避障

ESP32+Python自动驾驶小车:从硬件接线到PID闭环避障 简介面向2021年越南STEAM挑战赛设计的自动驾驶机器人系统源码包适合嵌入式竞赛选手、机器人爱好者与Python开发者有助于快速掌握基于ESP32CAM的图像采集、WiFi图像传输、车道线识别与自动转向控制的完整方案同时支持通过键盘进行手动遥控。压缩包共25个文件涵盖6个Python脚本图像读取、车道线检测、自动行驶、键盘控制、2个C固件源码ESP32CAM与VIA MakerBot电机驱动、依赖清单与配置文件以及多张实物接线图和PDF/Word版环境搭建指南整体大小仅1.81MB轻量且便于部署。目前已有59人学习浏览。资源按examples、auto_drive、firmware、docs、images等模块清晰组织既可直接运行键盘控制与自动驾驶示例也能结合文档和接线图还原越南STEAM挑战赛的赛题场景。通过阅读源码可理清UDP网络通信、图像处理和电机控制之间的协作关系后续也可作为无人车项目的改造模板为二次开发节省时间。1. 用ESP32和Python搭一套自动驾驶机器人系统先看闭环怎么形成一辆四轮小车底盘上堆着电机驱动板、几路灰度传感器和一枚超声波模块就是最常见的自动驾驶机器人实验平台。把它从“上电原地转圈”带到“沿黑线巡航、遇到纸箱自动绕开”核心不是换更贵的硬件而是把Python写的控制逻辑跑在ESP32的MicroPython环境里让感知、决策、执行在一个主循环中闭合。这套组合的吸引力在于分工明确传感器读取、PWM输出交给machine底层驱动路径规划、转向PID、避障状态机全用Python表达改动一行逻辑立刻就能在REPL里验证。适合的人群很具体做毕设或机器人竞赛的学生、从Web后端转硬件的工程师、以及想给玩具车加“脑子”的业余玩家。需要的基础是懂一点Python语法、认识VCC/GND/SDA这类引脚名称不需要会写寄存器级驱动也不用先啃完嵌入式内核源码。下面按自己搭一套底盘的实际顺序写先选型与接线再烧录固件然后把偏差计算和PID调通最后用日志和排查表把系统调到能连续跑完一整圈。2. 底盘选型与接线ESP32自动驾驶机器人的硬件清单和引脚分配2.1 主控对比选ESP32是因为执行与调试循环更短做自动驾驶小车的主控有三条常见路线Arduino、树莓派、ESP32。Arduino开发简单但单线程跑不了复杂逻辑RAM只有2KB连双路PID加超声波滤波都会捉襟见肘。树莓派性能强可以把摄像头、激光雷达都压上去但启动慢、供电要求高、一块板子价格够买三块ESP32用在轮式小车上是拿大炮打蚊子。ESP32落在两者之间双核240MHz520KB SRAM4MB Flash自带WiFi和蓝牙MicroPython固件跑起来后你既能直接操作GPIO又能用socket收发数据这是其他两块板子给不了的组合。另一个常被忽略的点是ESP32的调试成本低。Arduino改程序要重新编译烧录树莓派维依赖也很费精力。ESP32跑MicroPython可以交互式执行输入from machine import Pin马上就能点亮一个LED这种反馈速度对调PID这种试错密集的任务非常重要。如果后续要把小车接进ROS 2生态ESP32上还能跑micro-ROS组件把里程计和传感器作为话题发布出去——这是从单机小车走向机器人操作系统的自然过渡。2.2 电机驱动、传感器与供电三张清单和一张引脚表底盘建议买常见的2驱或4驱亚克力小车套件配两个带编码器的直流减速电机。电机驱动选DRV8833或TB6612它们比L298N轻、压降小10A以内的堵转电流也能扛住。传感器方面循迹用3路或5路灰度模块原理是红外发射管照射地面黑色赛道反射率低模拟量输出高避障用HC-SR04超声波有预算再加一个MPU6050做转向姿态参考。供电是整个系统最容易翻车的环节建议两节18650串联给电机驱动再通过DC-DC降压到5V给ESP32和传感器供电电机电源和逻辑电源从源头分开只把GND连到一起。外设推荐型号ESP32引脚说明左电机PWMDRV8833GPIO 13接AIN1/PWM右电机PWMDRV8833GPIO 12接BIN1/PWM左编码器A相Hall编码器GPIO 34输入模式上拉右编码器A相Hall编码器GPIO 35输入模式上拉左侧灰度3路红外模块GPIO 36 (VP)ADC通道中间灰度3路红外模块GPIO 39 (VN)ADC通道右侧灰度3路红外模块GPIO 32ADC通道超声波TrigHC-SR04GPIO 15输出10us脉冲超声波EchoHC-SR04GPIO 14输入3.3V电平I2C数据线可选OLEDGPIO 21SDAI2C时钟线可选OLEDGPIO 22SCL接线有个容易踩的坑HC-SR04的Echo输出是5V电平直接接到ESP32的3.3V引脚上可能烧GPIO串一个1K电阻分压最省事。灰度传感器是模拟量输出必须接支持ADC的引脚ESP32的ADC1GPIO 36/39/34/35/32在MicroPython里可以直接用machine.ADC读取ADC2的引脚在WiFi启用后会部分失效布线时优先避开。2.3 感知-决策-执行主循环代码分层不能乱硬件接好后先写一个能体现控制回路的最小程序不用考虑算法只为确认每个模块的数据方向正确from machine import Pin, ADC, PWM import time motor_a PWM(Pin(13), freq1000) adc_left ADC(Pin(36)) adc_left.atten(ADC.ATTN_11DB) while True: left_val adc_left.read() if left_val 2000: motor_a.duty(512) else: motor_a.duty(0) time.sleep_ms(20)PWM(Pin(13), freq1000)把GPIO13配置成1kHz的PWM输出freq选1000Hz对直流减速电机足够高于电机能响应的频率只会增加驱动管损耗。atten(ADC.ATTN_11DB)将ADC量程扩展到0~3.3V读取值范围是0~4095黑色赛道反射红外少电压更高具体阈值要在实际路面上采集后再定。time.sleep_ms(20)决定了主循环周期是20ms也就是50Hz控制频率本文后面所有PID参数都基于这个频率设计改循环周期必须同步改PID参数。这段代码的价值不在于“能让车跑”而在于它建立了正确的数据流认知传感器的模拟值经过比较变成状态状态决定PWM输出输出驱动车轮改变传感器读数形成一个反馈环。后面做PID和避障只是把这个循环里的“比较器”换成更聪明的决策逻辑。3. 在ESP32上烧录MicroPython并跑通最小系统从命令行到无线调试3.1 固件烧录esptool.py擦除与写入命令MicroPython固件可以从官方下载页面挑支持你芯片版本的文件ESP32、ESP32-S3、ESP32-C3各有对应镜像。烧录工具推荐用esptool.py它是乐鑫官方Python工具pip安装后即可使用。pip install esptool esptool.py --chip esp32 --port /dev/ttyUSB0 erase_flash esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 460800 write_flash -z 0x1000 ESP32_GENERIC-20240602-v1.23.0.bin--chip esp32指定目标芯片如果用的是ESP32-S3或C3要改成对应型号写错会在连接阶段报错。erase_flash不是可选项旧固件里的分区表会和新镜像冲突不擦除轻则启动崩溃重则烧完仍进不了REPL。write_flash -z 0x1000后面的0x1000是烧录起始地址ESP32的引导程序固定从0x1000开始移到别处开不了机。Windows用户把/dev/ttyUSB0换成设备管理器里的COM口macOS上一般是/dev/cu.usbserial-*。写完固件后按住板子上的BOOT键再上电或者用--before default_reset让esptool自动复位避免进入下载模式失败。烧录完成后用串口工具打开115200波特率如果看到提示符就说明MicroPython已经活了。3.2 REPL与代码部署Thonny、VS Code和main.py的关系MicroPython的交互式REPL是这套开发流程最大的效率来源。推荐先用Thonny菜单里选择“解释器”为MicroPython (ESP32)它会自动识别串口并建立REPL连接。此时在下方的Shell里输入print(11)板子会立即返回结果和本地Python解释器体验一致。调试传感器时我习惯在REPL里逐行敲adc.read()观察数值变化。正式的项目源码不建议全部写在REPL里而是按模块组织文件用Thonny的“保存到”功能把脚本传到板子Flash。文件结构通常这样安排boot.py # 开机先执行配置WiFi、启动WebREPL main.py # 再执行主控制循环 lib/ pid.py # PID控制器类 sensor.py # 灰度/超声波读取封装 motor.py # 电机驱动封装main.py里的while True会阻塞整个进程所以开机后如果有WiFi连接、文件同步、日志上传这类操作全部放进boot.py。Thonny里也可以配置VS Code环境用Pymakr扩展或直接用串口监视器配合ampy命令行工具上传文件例如ampy --port COM3 put main.py。写大型工程时VS Code的代码补全和Git集成更舒服小项目用Thonny一次就能完成编辑、上传、调试三件事选哪个取决于你手头项目的复杂度。3.3 无线化WebREPL、OTA与手机蓝牙控制反复插拔USB线调试小车很影响心情MicroPython原生提供WebREPL方案。在boot.py里写入以下代码开发板每次上电都会自动连接WiFi并启动WebREPL服务import network, webrepl, time sta network.WLAN(network.STA_IF) sta.active(True) sta.connect(你的SSID, 密码) for _ in range(20): if sta.isconnected(): break time.sleep(0.5) if sta.isconnected(): webrepl.start(password12345678) print(IP:, sta.ifconfig()[0]) else: print(WiFi连接失败)用network.WLAN(network.STA_IF)进入Station模式connect传入2.4G频段的WiFi账号5G频段默认连不上。webrepl.start(password...)会监听8266端口手机或电脑浏览器打开WebREPL页面填入IP和密码就能远程操作REPLwebrepl_cli.py命令还能直接上传下载文件。调试小车时电脑放在赛道边上连WiFi车在赛道上跑所有print输出都能实时看到。OTA升级的思路是在此基础上叠加一层先下载新固件到Flash的备用分区校验成功后切换分区并重启。社区常见做法是用micro_ros_espidf_component配合ROS 2做更结构化的通信但演示级别的OTAWebREPL通过WiFi传.bin文件也能完成只是Flash分区布局要预留空间。手机遥控则更直接ESP32的BLE模块广播一个Service手机App连接后发三个字节控制左右电机速度和方向这块逻辑和主控制循环跑在同一个线程里网络接收要考虑超时不能因为蓝牙断了就让小车失去控制。4. 自动驾驶机器人的巡线与避障用Python写PID和状态机4.1 灰度循迹从AD值到车道偏差把三个灰度传感器横排在车头下方中间对准黑线左右各偏一个车宽。理想情况下车在黑线正上方时左、右侧传感器读数对称中间传感器数值明显区别于两侧。定义偏差error时常见做法是计算左右传感器归一化差值from machine import ADC, Pin adc_left ADC(Pin(36)); adc_left.atten(ADC.ATTN_11DB) adc_right ADC(Pin(39)); adc_right.atten(ADC.ATTN_11DB) def read_line_error(): left adc_left.read() right adc_right.read() if left right 0: return 0 error (left - right) / (left right) return errorerror被归一化到-1到1之间正值表示左传感器检测到更多反射车偏右需要左转修正。为什么要归一化因为灰度模块读到的绝对值受环境光、地面材质、电池电压影响同一个地面早上和下午差500都有可能。用除法消掉共模量阈值和曝光变化就不会直接炸掉你的转向逻辑。5路灰度同理把每路按位置加权再除以总和输出的误差更平滑但计算量对ESP32双核来说也可忽略。4.2 转向控制位置式PID实现与三参数调优有了误差信号下一步是决定左右轮的速度差。比例项P是最直观的做法误差大就猛打方向误差小就微调。但纯P控制在弯道会持续存在稳态误差因为车必须有一个固定转角才能过弯而P控制为了维持这个转角需要不断修正表现就是蛇形走线。加入积分项I消除稳态误差加入微分项D抑制过冲三者相加就是完整的PID控制器class PID: def __init__(self, kp0.8, ki0.05, kd0.15): self.kp kp self.ki ki self.kd kd self.last_error 0 self.integral 0 def update(self, error, dt0.02): self.integral error * dt if self.integral 100: self.integral 100 elif self.integral -100: self.integral -100 derivative (error - self.last_error) / dt self.last_error error return self.kp * error self.ki * self.integral self.kd * derivativeupdate输入归一化误差输出是转向量叠加到左右电机的PWM占空比上。积分限制在±100是抗饱和的关键不然小车在发车台被挡住时积分会一直累积松开后原地转圈。dt0.02必须与主循环实际周期一致如果你在循环里加了超声波读取导致周期变成30ms这里必须同步修改否则微分项计算出的数值会整体偏大。调参按P到I到D的顺序来。先把ki、kd调成0kp从0.3开始加观察直线段是否小幅震荡震荡后把kp降回稳定值再每次加0.01的ki直到弯道不跑偏最后加kd消除入弯时的过冲。这个表是我在实际小车上的调试顺序参考参数调大后的直观效果调过头的现象kp转向更有力响应变快直线上左右画龙电机发热ki弯道跟随更紧直线更直过弯过冲停车后车头持续抖动kd入弯平滑抑制震荡传感器噪声被放大电机高频嗡嗡响4.3 超声波避障测距时序与状态机切换超声波模块测距的原理是发射10us的TTL脉冲Echo引脚输出与声波往返时间等长的电平用time_pulse_us测量这个高电平持续时间就能换算出距离from machine import Pin, time_pulse_us import time trig Pin(15, Pin.OUT) echo Pin(14, Pin.IN) def measure_distance(): trig.value(0) time.sleep_us(2) trig.value(1) time.sleep_us(10) trig.value(0) duration time_pulse_us(echo, 1, 30000) if duration 0: return None return duration * 0.0343 / 2声速在空气中约为343m/s往返时间除以2就是单程距离得到的是厘米。time_pulse_us的第三个参数30000是超时上限对应约510cm量程如果返回-1说明没有收到回波可能是目标太近模块盲区约2cm或接线松动直接返回None让上层逻辑做超时处理不能让系统拿一个非法值去决策。避障决策用有限状态机比一堆if else嵌套可靠。定义三个状态LINE_FOLLOW默认循迹、OBSTACLE_STOP检测到障碍、AVOID_TURN绕行动作。状态转移条件放在一个函数里每次循环先查距离小于25cm就切到停车状态停车200ms后再根据障碍在左还是在右决定转向方向完成绕行后重新回到循迹状态。状态机的好处是把“现在该干什么”和“怎么干”分离开加一个掉头逻辑不需要改动循迹代码只要在转移条件里加一个语句。5. 联调验证与排错让ESP32小车从能跑变成跑得可靠5.1 一个完整的main.py骨架import time from pid import PID from sensor import read_line_error, measure_distance from motor import set_motor BASE_SPEED 640 pid PID(kp0.8, ki0.05, kd0.15) state LINE_FOLLOW while True: distance measure_distance() if distance is not None and distance 25: set_motor(0, 0) state OBSTACLE_STOP else: error read_line_error() steer pid.update(error) set_motor(int(BASE_SPEED - steer), int(BASE_SPEED steer)) state LINE_FOLLOW time.sleep(0.02)控制逻辑按优先级排列安全检测永远先于循迹因为传感器读到障碍物时车头可能已经离目标很近了。int()转换很重要PWM占空比接受整数直接把浮点传进去会报类型错误。5.2 校准值掉电保存与启动自检每次开机重新校准灰度阈值很烦。把校准结果用JSON存进Flash上电后直接从传感器模块读取import json, os calib {black: 2500, white: 800} with open(/calib.json, w) as f: json.dump(calib, f)启动自检时读回/calib.json如果文件不存在或字段缺失就让小车原地转一圈采样取所有读数的中位数作为阈值这样换场地后不需要连电脑重新配参数。自检还能把超声波模块的供电状态测一下连续三次测量全返回None就给车上的LED一个快闪提示。5.3 回传数据看PID曲线建一个UDP的socket把误差和转向量发到局域网里的电脑import socket, json, time sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.sendto(json.dumps({error: error, steer: steer}), (192.168.1.100, 9000))电脑上跑一个Python脚本监听9000端口把收到的数据画成曲线。跑完整圈之后观察曲线如果误差频繁穿越零线且幅度大说明kp偏大如果曲线长时间偏离零线贴在上方说明ki不够或积分被限幅压住。这个可视化过程比盯着车跑要直观得多。日常遇到最顽固的几个问题按这个方向排查现象排查方向小车开机不执行任何程序boot.py或main.py语法崩溃串口看Traceback循迹过弯冲出赛道灰度传感器离地太高应控制在1~2cm弯道前减速超声波距离值乱跳Echo分压电阻缺失或电池压降导致供电纹波大左右电机转速不一致编码器计数方向反了把A/B相接反再验证电池满电但车速下降PWM频率选太高降到1000Hz以下试试调试到后期最值得做的事是把主循环的实际周期打印出来。很多莫名其妙的抖动不是PID参数问题而是time_pulse_us在近距离会阻塞太久导致控制周期从20ms漂到50ms。在关键路径上打ticks_ms()时间戳你会看到传感器读取顺序对时序的影响比想象中大得多。本文还有配套的精品资源点击获取
返回列表