从零构建智能避障小车:硬件选型、电路连接与核心算法详解

发布时间:2026/8/20 6:34:27
从零构建智能避障小车:硬件选型、电路连接与核心算法详解 1. 项目概述从玩具到智能移动平台的跨越“避障小车机器人”听起来是不是很像小时候玩过的遥控车但当你真正开始动手做就会发现它远不止于此。这玩意儿本质上是一个集成了传感器、控制器和执行器的微型智能移动平台是机器人学、嵌入式开发和自动控制领域一个绝佳的入门项目。我当年第一次做避障小车纯粹是觉得好玩但一路做下来它几乎串联了我对单片机编程、传感器原理、电机控制和简单算法逻辑的所有认知。它解决的问题很直观让一个移动装置在未知或动态环境中自主地、安全地从A点移动到B点而不会撞上任何东西。这听起来简单背后却涉及感知、决策、执行三个核心环节的闭环。这个项目非常适合谁呢如果你是电子爱好者、自动化或计算机相关专业的学生或者任何对“让东西自己动起来”感兴趣的人避障小车都是一个完美的起点。它不需要你具备高深的数学或算法知识当然有的话可以玩得更深从基础的电路焊接、Arduino编程到逐步引入更复杂的传感器融合和路径规划算法它的可扩展性极强。你可以把它当作一个验证想法的快速原型平台也可以深入打磨让它成为你学习PID控制、SLAM即时定位与地图构建甚至机器视觉的跳板。接下来我就结合自己踩过的坑和积累的经验把这个项目从里到外拆解一遍。2. 核心系统设计与方案选型做一个小车第一步不是急着买零件而是想清楚你要它做到什么程度。这直接决定了你的硬件选型、软件架构乃至最终成本。避障方案是核心通常有几种主流思路各有优劣。2.1 传感器方案眼睛和耳朵的选择小车的“避障”能力完全取决于它的“感知”系统。市面上常见的传感器方案可以类比为人的不同感官。超声波传感器HC-SR04这几乎是所有入门项目的首选价格低廉十几块钱一对原理简单发射超声波并接收回波通过时间差计算距离。它的探测范围通常在2cm到400cm之间角度大约15度像一个简单的“声呐”。优点是成本低、接口简单、测距相对准确。但缺点也很明显探测角度窄容易漏掉侧面障碍物对柔软、吸音的材料如窗帘、毛绒玩具探测不准同时使用多个时容易相互干扰。我的经验是用它做正前方的单点测距预警没问题但想实现全面的环境感知至少需要2-3个组成阵列分别负责左、中、右三个方向这会增加代码复杂度和IO口占用。红外避障传感器常见的有TCRT5000等模块。它通过发射红外光并检测反射光来判断前方是否有物体输出是简单的数字信号有障碍/无障碍。它的探测距离短几厘米到二三十厘米且受环境光特别是日光和物体颜色黑色吸光影响大。但它响应速度快价格极低。我通常不把它作为主避障传感器而是用作“悬崖传感器”或近距离接触式检测的补充比如防止小车从桌子边缘掉下去。激光雷达Lidar比如常见的RPLIDAR A1或思岚科技的Slamware模块。这是“降维打击”的方案能提供周围环境360度的二维点云数据。有了它你不仅可以避障还能建图、定位实现真正的自主导航。但缺点也突出价格昂贵几百到上千元、数据处理复杂需要上位机或较强的MCU、功耗较高。对于初学者或预算有限的爱好者我不建议一上来就搞激光雷达容易从入门到放弃。视觉方案摄像头使用如OpenMV、树莓派加摄像头模块通过图像识别来避障。这属于更高阶的玩法可以实现语义避障识别出什么是椅子、什么是人然后绕开。但开发难度大对处理器算力要求高实时性处理是个挑战。对于绝大多数入门和中级项目我的推荐方案是“超声波主测距 红外辅助贴近检测”的组合。成本可控效果足够应对大多数室内平坦环境。例如用两个超声波传感器一个朝前偏左一个朝前偏右就能覆盖一个扇面区域车体左右两侧和后方可以各加一个红外避障模块防止转弯或倒车时刮蹭。2.2 控制器大脑Arduino还是STM32传感器收集了信息需要有个“大脑”来处理并做出决策。Arduino Uno/Mega绝对是新手之友。开发环境简单库资源丰富社区支持强大。对于实现基本的“探测-停止-转向”逻辑Uno的ATmega328P芯片性能绰绰有余。它的模拟输入口可以方便地读取超声波传感器的测距值数字口可以控制电机驱动和读取红外传感器。最大的优势是快速原型开发你可以在半小时内让小车跑起来。但缺点是性能有限引脚数量可能紧张特别是当你用了多个传感器和电机驱动时难以处理更复杂的算法或多任务。STM32系列如STM32F103C8T6俗称“蓝色药丸”这是进阶的选择。基于ARM Cortex-M内核主频更高72MHz vs Arduino的16MHz资源更丰富更多的IO口、定时器、ADC通道价格甚至比Arduino Uno还便宜。但开发环境如Keil、STM32CubeIDE和编程方式更接近底层常用HAL库或直接寄存器操作对新手有一定门槛。如果你打算未来加入编码器测速实现PID控制、或者使用更复杂的串口通信传感器STM32是更好的选择。树莓派Raspberry Pi这已经是一个微型电脑了。如果你计划做图像识别、复杂路径规划、或者通过网络远程控制树莓派是唯一选择。但它本身不能直接驱动电机需要额外扩展板或驱动板系统复杂实时性不如单片机。通常采用“树莓派单片机”的架构树莓派做高级决策单片机负责实时电机控制。我的建议是纯新手从Arduino Uno开始它能让你专注于逻辑和算法而不是纠缠于底层配置。当你觉得Arduino的性能或引脚成为瓶颈时就是转向STM32的好时机。2.3 动力与执行系统让小车动起来决策有了需要强有力的“手脚”来执行。电机选择最常见的是TT减速电机带塑料齿轮箱价格便宜扭力尚可但精度和寿命一般。好一点的选择是直流减速电机金属齿轮箱扭力更大更耐用。如果需要精确控制移动距离和速度那么直流电机编码器是必选项编码器可以反馈电机的实际转速和转动圈数是实现PID速度控制的基础。电机驱动模块单片机IO口的电流太小无法直接驱动电机必须通过驱动模块。最经典的是L298N双H桥驱动模块可以驱动两个直流电机支持正反转和PWM调速。它的优点是皮实、简单但效率较低发热大。更现代的选择是TB6612FNG驱动芯片模块效率高、发热小、体积小巧是当前的主流选择。电源管理这是最容易忽视却至关重要的部分。单片机如Arduino、传感器和电机驱动必须分开供电或者至少要做好电源隔离。电机启动和堵转时会产生巨大的电流波动和电压跌落如果共用一个电源会直接导致单片机复位或传感器工作异常。我的标准做法是使用两节18650锂电池串联约7.4V给电机驱动供电然后通过一个降压模块如LM2596将电压降至5V单独给Arduino和传感器供电。同时在电机电源输入端加上一个大电容如470uF/16V来缓冲电流冲击。车体与结构你可以购买现成的亚克力或金属小车底盘套件这省去了很多机械结构的麻烦。注意轮子的选择差速转向的小车需要两个主动轮通常是左右轮和至少一个万向轮或从动轮来保持平衡。3. 硬件搭建与电路连接实战理论说再多不如动手接一次线。这里我以最经典的“Arduino Uno 双超声波HC-SR04 TB6612电机驱动 四轮小车底盘”为例详细走一遍硬件搭建流程。3.1 物料清单与工具准备在开始焊接或接线前请务必清点好所有物料控制器Arduino Uno R3 开发板 x1传感器HC-SR04 超声波模块 x2电机驱动TB6612FNG 电机驱动模块 x1动力TT减速电机带轮 x4 如果使用两轮差速结构则是x2个主动轮电机 x2个万向轮车体四轮小车底盘套件含螺丝、铜柱等 x1套电源18650锂电池 x2 配套电池盒 x1 LM2596降压模块 x1连接杜邦线公对公、公对母若干面包板可选用于测试x1工具电烙铁、焊锡、螺丝刀、剥线钳、万用表强烈建议备一个用于排查电源问题注意焊接时注意安全避免烫伤。给锂电池充电务必使用专用充电器切勿过充或短路有安全隐患。3.2 核心电路连接详解连接的原则是先电源后信号先模块后整合。不要一上来就把所有线都插上。第一步电源系统搭建将两节18650锂电池装入电池盒输出约为7.4V。这个电压直接连接到TB6612驱动板的VM电机电源引脚。将LM2596降压模块的输入端也接到这个7.4V上。调节LM2596上的电位器用万用表测量其输出端将其输出电压精确调整到5.0V。将这个稳定的5V输出连接到Arduino Uno的Vin引脚或者断开Uno上的电源选择跳线直接接到5V引脚但要注意不要从USB和这里同时供电。同时这个5V也是所有传感器HC-SR04的VCC电源。第二步电机与驱动连接将左侧两个电机的线通常一红一黑分别接到TB6612的A01、A02输出端。右侧两个电机接到B01、B02。如果电机转动方向与你期望的相反只需将这两根线对调即可。TB6612需要三个控制信号来控制一个电机PWMAPWM速度信号、AIN1、AIN2方向控制信号。将它们分别连接到Arduino的数字引脚。例如左电机PWMA- D5,AIN1- D6,AIN2- D7右电机PWMB- D10,BIN1- D8,BIN2- D9将TB6612的GND地线与Arduino的GND、LM2596的GND、所有传感器的GND全部连接在一起共地是电路正常工作的基础。将TB6612的VCC逻辑电源接到Arduino的5V引脚为其内部逻辑电路供电。第三步传感器连接每个HC-SR04有4个引脚VCC接5V、GND接地、Trig触发控制、Echo回响接收。超声波传感器1左前Trig1- D2,Echo1- D3超声波传感器2右前Trig2- D4,Echo2- D11实操心得Echo引脚输出的是5V电平而Arduino Uno的IO口耐受电压也是5V可以直接连接。但如果你使用的是STM323.3V电平则必须在Echo引脚和STM32的IO口之间加一个分压电路例如两个电阻组成1:2的分压否则可能烧毁芯片这是新手极易踩的坑。第四步整体装配与检查将Arduino、驱动板、电池、降压模块用铜柱和螺丝固定在小车底盘上注意重心分布尽量让电池最重的部分放在车体中部或稍靠后位置避免前重后轻导致万向轮压不住。连接好所有线束后用扎带整理避免缠绕进轮子里。上电前用万用表最后检查一遍测量电机驱动板VM引脚电压应为7.4V左右。测量Arduino5V引脚对GND电压应为稳定的5.0V。检查所有GND点之间是否导通电阻接近0。4. 核心避障逻辑与代码实现硬件是躯体软件是灵魂。避障逻辑的核心是一个不断循环的“感知-决策-执行”过程。下面我们一步步实现它。4.1 基础驱动与传感器读数函数首先我们要写好最底层的函数让电机动起来以及读取超声波距离。// 引脚定义 // 电机驱动引脚 const int PWMA 5, AIN1 6, AIN2 7; // 左电机 const int PWMB 10, BIN1 8, BIN2 9; // 右电机 // 超声波引脚 const int trigL 2, echoL 3; // 左传感器 const int trigR 4, echoR 11; // 右传感器 // 电机控制函数 void setMotor(int motor, int speed, int direction) { // motor: 0-左电机 1-右电机 // speed: 0-255, direction: 0-停止 1-前进 2-后退 int pwmPin, in1Pin, in2Pin; if (motor 0) { pwmPin PWMA; in1Pin AIN1; in2Pin AIN2; } else { pwmPin PWMB; in1Pin BIN1; in2Pin BIN2; } analogWrite(pwmPin, speed); // 设置PWM速度 if (direction 1) { // 前进 digitalWrite(in1Pin, HIGH); digitalWrite(in2Pin, LOW); } else if (direction 2) { // 后退 digitalWrite(in1Pin, LOW); digitalWrite(in2Pin, HIGH); } else { // 停止 digitalWrite(in1Pin, LOW); digitalWrite(in2Pin, LOW); } } // 超声波测距函数 (单位厘米) float getDistance(int trigPin, int echoPin) { digitalWrite(trigPin, LOW); delayMicroseconds(2); digitalWrite(trigPin, HIGH); delayMicroseconds(10); // 发出10微秒的高电平脉冲 digitalWrite(trigPin, LOW); long duration pulseIn(echoPin, HIGH); // 读取高电平持续时间 float distance duration * 0.034 / 2; // 声速340m/s 除以2是往返距离 return distance; } void setup() { // 初始化所有引脚模式 pinMode(PWMA, OUTPUT); pinMode(AIN1, OUTPUT); pinMode(AIN2, OUTPUT); pinMode(PWMB, OUTPUT); pinMode(BIN1, OUTPUT); pinMode(BIN2, OUTPUT); pinMode(trigL, OUTPUT); pinMode(echoL, INPUT); pinMode(trigR, OUTPUT); pinMode(echoR, INPUT); Serial.begin(9600); // 用于调试输出距离信息 }4.2 状态机与决策逻辑设计最简单的避障逻辑可以是一个“状态机”。小车通常处于“前进”状态当检测到障碍物时根据障碍物的位置切换到“避障”状态如转向或后退避障完成后恢复“前进”。// 定义状态 enum CarState { STATE_FORWARD, STATE_TURN_LEFT, STATE_TURN_RIGHT, STATE_BACK_AND_TURN }; CarState currentState STATE_FORWARD; const float SAFE_DISTANCE 20.0; // 安全距离单位厘米 const float SIDE_SAFE_DISTANCE 15.0; // 侧面安全距离如果装了侧面传感器 void loop() { float distL getDistance(trigL, echoL); float distR getDistance(trigR, echoR); // 调试输出实际运行时可以注释掉 Serial.print(L: ); Serial.print(distL); Serial.print( cm, R: ); Serial.print(distR); Serial.println( cm); // 决策逻辑 switch (currentState) { case STATE_FORWARD: // 正常前进 setMotor(0, 150, 1); // 左电机前进速度150 setMotor(1, 150, 1); // 右电机前进速度150 // 判断是否需要避障 if (distL SAFE_DISTANCE distR SAFE_DISTANCE) { // 正前方有障碍需要后退然后转向 currentState STATE_BACK_AND_TURN; } else if (distL SAFE_DISTANCE) { // 左边有障碍向右转 currentState STATE_TURN_RIGHT; } else if (distR SAFE_DISTANCE) { // 右边有障碍向左转 currentState STATE_TURN_LEFT; } break; case STATE_TURN_LEFT: // 左转右电机前进左电机停止或后退 setMotor(0, 0, 0); // 左电机停 setMotor(1, 180, 1); // 右电机前进速度稍快 delay(300); // 左转一段时间这个时间需要根据小车调整 // 检查是否已经避开 if (distL SAFE_DISTANCE distR SAFE_DISTANCE) { currentState STATE_FORWARD; } break; case STATE_TURN_RIGHT: // 右转左电机前进右电机停止或后退 setMotor(0, 180, 1); setMotor(1, 0, 0); delay(300); if (distL SAFE_DISTANCE distR SAFE_DISTANCE) { currentState STATE_FORWARD; } break; case STATE_BACK_AND_TURN: // 先后退 setMotor(0, 150, 2); setMotor(1, 150, 2); delay(500); // 后退0.5秒 // 然后随机向左或向右转一个较大的角度 if (random(2) 0) { // 随机选择0或1 // 左转 setMotor(0, 0, 0); setMotor(1, 200, 1); } else { // 右转 setMotor(0, 200, 1); setMotor(1, 0, 0); } delay(600); // 转向时间较长 currentState STATE_FORWARD; break; } delay(50); // 主循环延迟控制决策频率 }这个代码实现了一个非常基础的避障逻辑。它的优点是简单直观但缺点也很明显延迟delay()函数会阻塞程序。在转弯或后退的delay期间传感器数据无法更新小车处于“盲”状态如果此时有新的障碍物出现它就会撞上去。4.3 进阶优化非阻塞编程与更智能的算法为了解决阻塞问题我们必须采用非阻塞Non-blocking的编程方式依靠millis()函数来计时。unsigned long previousMillis 0; const long interval 100; // 主控制循环间隔100毫秒 CarState currentState STATE_FORWARD; unsigned long stateStartTime 0; const unsigned long turnDuration 300; // 转向持续时间 const unsigned long backDuration 500; // 后退持续时间 void loop() { unsigned long currentMillis millis(); // 每隔100毫秒执行一次控制逻辑而不是用delay if (currentMillis - previousMillis interval) { previousMillis currentMillis; float distL getDistance(trigL, echoL); float distR getDistance(trigR, echoR); switch (currentState) { case STATE_FORWARD: setMotor(0, 150, 1); setMotor(1, 150, 1); if (distL SAFE_DISTANCE || distR SAFE_DISTANCE) { // 进入避障状态并记录进入时间 currentState STATE_BACK_AND_TURN; stateStartTime currentMillis; // 可以先停一下 setMotor(0, 0, 0); setMotor(1, 0, 0); } break; case STATE_BACK_AND_TURN: // 判断在当前状态中持续了多久 if (currentMillis - stateStartTime backDuration) { // 后退阶段 setMotor(0, 150, 2); setMotor(1, 150, 2); } else if (currentMillis - stateStartTime backDuration turnDuration) { // 转向阶段这里固定向右转为例 setMotor(0, 200, 1); // 右转 setMotor(1, 0, 0); } else { // 避障动作完成返回前进状态 currentState STATE_FORWARD; } // 注意在避障状态中我们仍然在持续读取传感器数据 // 理论上可以加入更高级的中断逻辑比如后退时发现后面也有障碍物立即停止。 break; } } }更进一步我们可以引入模糊逻辑或势场法的简单思想让决策更“平滑”。例如不简单地用“有/无障碍”来判断而是根据距离的远近动态调整电机的速度差实现“逐渐转向远离障碍物”的效果而不是生硬的“停止-转向”。void loop() { float distL getDistance(trigL, echoL); float distR getDistance(trigR, echoR); int baseSpeed 150; int leftSpeed baseSpeed; int rightSpeed baseSpeed; // 如果左边距离近就降低左轮速度让车向右转 if (distL SAFE_DISTANCE) { // 距离越近减速越多用一个比例系数 float factor distL / SAFE_DISTANCE; // factor 在0~1之间越近越小 leftSpeed (int)(baseSpeed * factor); } // 如果右边距离近就降低右轮速度让车向左转 if (distR SAFE_DISTANCE) { float factor distR / SAFE_DISTANCE; rightSpeed (int)(baseSpeed * factor); } // 设置电机速度 setMotor(0, constrain(leftSpeed, 0, 255), 1); // constrain函数限制速度在0-255 setMotor(1, constrain(rightSpeed, 0, 255), 1); delay(50); }这种方法下小车在接近障碍物时会自然而流畅地绕开行为更像一个生物而不是一个机械的自动机。你可以调整SAFE_DISTANCE和速度计算因子来改变它的“性格”。5. 调试、优化与功能扩展代码烧录进去小车能跑起来只是第一步。让它跑得稳、跑得聪明才是真正的挑战。5.1 传感器数据处理与滤波超声波传感器读数是出了名的“跳”。你可能看到距离值在20cm到50cm之间乱跳。直接使用这个原始数据会导致小车“抽风”。滤波是必须的。移动平均滤波这是最简单有效的方法。保存最近N次的测量值取平均值作为最终结果。const int numReadings 5; float readingsL[numReadings]; float readingsR[numReadings]; int readIndex 0; float totalL 0, totalR 0; float averageL 0, averageR 0; void setup() { // ... 其他初始化 for (int i 0; i numReadings; i) { readingsL[i] 0; readingsR[i] 0; } } float getFilteredDistance(int trigPin, int echoPin, float *readings, float total) { // 去掉最旧的数据 total total - readings[readIndex]; // 读取新数据 float newDist getDistance(trigPin, echoPin); // 对新数据做简单限幅排除明显错误值如500cm或2cm if (newDist 400.0 || newDist 2.0) { newDist 400.0; // 视为无效赋一个最大值 } // 存入新数据 readings[readIndex] newDist; total total newDist; // 计算平均值 float average total / numReadings; // 更新索引 readIndex (readIndex 1) % numReadings; return average; } void loop() { // 使用滤波后的数据 float distL getFilteredDistance(trigL, echoL, readingsL, totalL); float distR getFilteredDistance(trigR, echoR, readingsR, totalR); // ... 后续决策逻辑 }中值滤波对于偶尔出现的尖峰脉冲干扰比如另一个超声波的干扰回波中值滤波取N个值排序后的中间值效果更好。可以将两者结合先中值滤波去尖峰再移动平均平滑。5.2 电机控制优化与PID调速如果你用的是带编码器的电机那么恭喜你可以玩更高级的——PID速度闭环控制。这能让你精确控制小车走直线差速驱动的小车两个电机特性有微小差异开环控制下很容易跑偏。PID是比例Proportional、积分Integral、微分Derivative控制的缩写。对于电机调速P比例当前速度与目标速度的差值乘以一个系数。差越大输出控制量越大。单纯用P会存在静差最终速度达不到目标值。I积分累积历史误差。可以消除P产生的静差但积分太强会引起超调和不稳定。D微分预测误差变化趋势。可以抑制振荡让系统更快稳定。Arduino有现成的PID库。实现后你可以设定左轮和右轮的目标转速相同PID控制器会自动调整PWM占空比来克服两个电机的差异从而实现精准直行。这是让小车行为上档次的关键一步。5.3 常见问题排查与解决小车不动或抽搐检查电源用万用表测量电机驱动板VM电压和单片机5V电压是否正常。电机启动瞬间电压跌落严重是常见问题。检查共地确保所有模块的GND都连接到了同一个公共地。检查代码确认电机控制引脚定义是否正确setup()里是否设置了pinMode为OUTPUT。检查驱动模块使能端有些驱动板有独立的使能引脚如STBY需要接高电平才能工作。超声波读数一直为0或超大值检查接线Trig和Echo线是否接反VCC和GND是否接对检查电源传感器供电不足会导致工作不正常。检查物理遮挡传感器表面是否有污渍两个传感器是否离得太近互相干扰尝试拉开距离或错开发射时间。代码排查pulseIn函数可能会因为超时返回0。可以增加超时判断pulseIn(echoPin, HIGH, 30000)30ms超时。避障反应迟钝或乱撞调整安全距离SAFE_DISTANCE设得太小可能来不及刹车。建议从30cm开始调试。加入减速带不要等到小于安全距离才急停。可以设置一个“预警距离”如50cm当距离小于预警距离但大于安全距离时就线性降低速度。检查传感器安装角度和高度传感器应该水平向前安装高度要与你主要想避开的障碍物如桌腿、墙脚匹配。别让它对着天花板或地面。引入“状态锁存”避免在临界距离附近频繁切换状态。例如从“前进”进入“避障”后即使距离暂时大于安全距离也维持避障动作一小段时间避免“反复横跳”。小车跑不直开环系统固有缺陷没有编码器反馈电机本身存在差异。可以通过实验微调左右电机的基准PWM值来补偿。例如左电机给150右电机给145。机械问题检查车轮是否安装牢固轮胎是否打滑车轴是否对齐。上PID闭环控制这是根本解决方案。5.4 功能扩展方向当基础避障实现后这个平台可以玩出很多花样跟随模式将一个超声波传感器朝前下方安装检测地面或者用一个红外传感器检测前方特定高度的物体实现巡线或跟随人腿移动。手机蓝牙/Wi-Fi控制增加HC-05蓝牙模块或ESP8266 Wi-Fi模块用手机APP实现遥控和传感器数据监控。地图构建与路径规划升级到激光雷达如RPLIDAR和树莓派运行ROS机器人操作系统实现SLAM建图和自主导航到指定点。机械臂集成在小车上加一个舵机控制的机械臂实现移动抓取。这涉及到运动学正逆解等更深入的知识。多车协同做两个或多个小车让它们通过无线通信如NRF24L01交换位置和障碍信息实现简单的编队或协作避障。从一块Arduino板和几个传感器开始避障小车这个项目就像一棵技能树它的枝干可以延伸到机器人技术的各个领域。每一次调试成功每一次算法优化带来的成就感是实实在在的。最重要的是动手去做在调试和解决问题的过程中那些书本上的概念才会真正变成你自己的东西。我自己的那台最初级的小车现在虽然已经退役但它的主板还挂在我的工作台前提醒我一切复杂的系统都始于一个简单而坚定的“Hello, World”。