简介:基于STM32F103系列单片机的智能小车工程包,给出了遥控控制、避障、循迹三种经典控制模式的完整实现,适合嵌入式学习者、电子竞赛队伍以及毕业设计参考。压缩包共466个文件,约8.94MB,除h/c源码工程外,还包含Keil工程配置、hex烧录文件以及map/lst等编译调试文件,便于直接打开验证和二次开发。已有11770人浏览学习,实用性得到较多认可。项目内拆分出三个独立工程,分别对应三种控制模式,可对照学习不同功能模块的编程思路;代码涉及L298N电机驱动、传感器检测、PWM调速等关键环节,有助于快速掌握单片机控车的基本方法与调试技巧。 很多初学者拿到STM32最小系统板后的第一个实战项目,就是做智能小车。遥控控制、避障、循迹这三个功能拆开看都不算难,但把它们放到同一辆车上,让三种模式稳定切换、互不干扰,才是真正考验基本功的地方。这篇文章我直接以STM32F103ZET6为核心,把一套能跑通的智能小车方案完整梳理一遍:硬件怎么选、三个功能怎么共存、循迹避障的核心逻辑怎么写、踩过哪些坑,尽量把我实际操作中的经验都写出来,给正在做课程设计或者想入门嵌入式的朋友一个可复现的参考。
1. 整车硬件选型:基础版配置与替代方案
1.1 主控选型:为什么推荐F103ZET6
很多人纠结用STM32F103C8T6还是ZET6。我的建议很直接:如果是课程设计或者比赛,ZET6会更省心。原因不是性能差多少,而是引脚数量和调试便利性。C8T6只有48脚,做小车要接电机驱动、蓝牙、超声波、五路循迹、OLED屏幕、按键,引脚会被占得满满当当,走线乱不说,后期想加一个功能还得飞线或者改板子。
ZET6是144脚,Flash有512KB,RAM有64KB,外设资源非常充裕。多出来的引脚哪怕只当扩展IO用,也能减少很多布局上的麻烦。更重要的是,ZET6的FSMC接口可以接TFTLCD屏,如果后续想做可视化菜单或者显示调试波形,这个优势会非常明显。价格上ZET6比C8T6贵十几块钱,但省下的折腾时间远超这点成本。
1.2 电机驱动与供电:电流和电压必须一起算
电机驱动我用的是TB6612FNG,而不是很多教程里推荐的L298N。TB6612的峰值电流是3.2A,额定电流1.2A,驱动普通TT马达或者带减速箱的直流电机完全够用。关键是它内部是MOSFET管,压降小,发热低,不需要像L298N那样拖个大散热片。L298N的压降能达到2V以上,电池电压稍微低一点,电机转速就明显下降,这个差距在长时间跑动时特别明显。
供电这块是最容易翻车的。我的方案是:两节18650锂电池串联,标称7.4V,接TB6612的VM电机电源引脚,同时引一路出来接LM2596降压模块,降到5V给STM32、蓝牙模块和循迹传感器供电。注意,千万不要把7.4V直接灌进STM32的5V引脚,也尽量不要用AMS1117-3.3线性降压,因为从7.4V降到3.3V时压差太大,电流一上来芯片会烫得厉害。
提示:电池电压在满电时能到8.4V,不要用7.4V这个参数去算所有器件的耐压,最好预留1V左右的余量。
1.3 传感器选型:循迹、避障、遥控各自用啥
循迹我选的是五路红外反射传感器,型号一般是TCRT5000的模组。五路相比三路的优势很明显:检测范围更宽,直线跑的时候有中间三路做稳定参考,过十字路口和大角度弯道时五路能提供更完整的黑线位置信息,程序判断的时候容错空间大很多。
避障选的是HC-SR04超声波模块加三个红外避障传感器。超声波负责远距离探测,测距范围2cm到400cm,用于判断前方障碍物距离;红外避障传感器负责近距离的左右两侧检测,防止超声波盲区里已经贴近侧面的障碍物撞上去。遥控方面用HC-05蓝牙模块,手机APP通过串口发指令,成本低、协议简单,入门阶段够用了。如果有比赛需求,后面可以换成2.4G无线模块或者NRF24L01,但调试思路是一样的。
2. 三种功能如何共存:状态机与优先级设计
2.1 为什么不建议用“三个if叠一起”的方案
很多第一次做小车的朋友会这样写:先检测蓝牙有没有数据,有就去执行遥控;没有数据就检测避障,避障没触发就去循迹。这样确实能跑,但问题非常明显。蓝牙遥控的时候,如果前面有障碍物,程序会跳到避障逻辑里,遥控就失灵了,小车会因为无法后退而卡在墙角。循迹跑到一半,蓝牙发一个转弯指令,如果两个功能同时抢占电机控制,电机PWM会被反复修改,小车会出现明显抖动。
根源在于:三个功能没有明确的主次关系,也没有说清楚“什么时候该听谁的”。智能小车本质上是一个多输入、单输出的控制系统,所有传感器输入最终要汇聚成“电机怎么转”这一个输出,所以必须有一个仲裁机制。
2.2 状态机框架与切换规则
我采用的状态机设计是这样的:系统上电后默认进入“待机模式”,等待遥控器发送模式切换指令;收到指令后进入对应模式。遥控模式优先级最高,避障模式次之,循迹模式只在没有遥控指令、且避障没有触发时运行。
用伪代码来表达就是:
typedef enum { MODE_REMOTE, MODE_OBSTACLE, MODE_TRACKING } CarMode; CarMode currentMode = MODE_REMOTE; while (1) { uint8_t cmd = get_ble_cmd(); // 非阻塞读取蓝牙数据 if (cmd != 0xFF) { currentMode = parseMode(cmd); } if (currentMode == MODE_REMOTE) { remote_control(); } else if (currentMode == MODE_OBSTACLE) { obstacle_avoidance(); } else { line_tracking(); } }一个小技巧是:蓝牙指令不要直接执行,而是先解析成目标模式再执行。比如发送字符“A”进入避障模式,“B”进入循迹模式,“C”返回遥控模式,这样遥控、避障、循迹之间可以随时主动切换,而不是靠“有没有数据”来猜。实测下来,这种显式切换比自动抢占的方式稳定得多,至少不会出现“你明明想让它循迹,它却因为蓝牙残留数据停在原地”的尴尬情况。
3. 循迹的核心逻辑:五路传感器与PID差速
3.1 五路模块怎么读、怎么校准
五路循迹传感器每个探头输出的是数字量还是模拟量,取决于模组型号。我用的模组带比较器,输出0或1,黑色线检测到低电平,白色地面检测到高电平。读取方式就是五个GPIO直接读,不需要ADC。上电第一时间要先做校准:把小车放在全白的地面上,读一遍五个探头的输出值存为基准;再放在全黑(或者黑线上)读一遍。这样即使环境光线变化,也能通过阈值判断当前输出是高还是低。
摆放位置有个细节:五路探头要均匀分布在车底中央,中间一路对准车头中轴线,两边的探头间距要跟黑线宽度匹配。我的黑线宽度是2cm,探头间距设计成1.5cm,这样中间三路能够稳稳压住黑线,左右两路检测偏离。如果间距太大,会出现小车在黑线上蛇形走位;如果太小,直线行驶时左右两路会频繁误触发。
3.2 从if-else到PID:差速转向的平滑处理
新手写法通常是:左偏就右转,右偏就左转,全偏就掉头。这种二值控制的问题在于转向是跳变的,小车速度稍微快一点就会冲过弯道。我的做法是引入PID偏差计算,先根据五路传感器的状态映射出一个偏差量error:
int error_map[32] = { // 五路输出组合 -> 偏差值,0表示正中 // 例如 0x18(左数第二三路压线)对应 -2 // 0x0C(右数第二三路压线)对应 +2 };然后对偏差做PID计算,输出转向修正量:
int speed_left = BASE_SPEED + PID_Calculate(error); int speed_right = BASE_SPEED - PID_Calculate(error);在实际调参里,P值起步给30左右,I值可以设得非常小或者先设0,D值给20左右。跑起来以后先观察小车是不是在中线附近振荡,如果有振荡就减P,如果过弯跟不上就加P。循迹速度我一般控制在0.3m/s到0.5m/s之间,再快的话普通TT马达的响应速度就开始吃力了。
提示:让电机PWM输出和转向修正有死区范围,error小于2时不做转向修正,否则电机一直在微调,电池耗得快,电机也容易发热。
4. 避障的决策逻辑:超声波不是越近越要停
4.1 超声波与红外避障传感器的选型取舍
HC-SR04超声波传感器的测距原理是发一个10us的TRIG脉冲,然后测量ECHO引脚高电平的持续时间,距离等于时间乘以声速除以2。具体实现代码几乎每个教程都有,但我想强调一个容易忽略的点:超声波模块发射角大概有15度,如果前方障碍物是窄柱子或者桌腿,超声波可能会扫不到,等红外传感器检测到的时候已经离得很近了。
所以我的方案是超声波负责前方35cm以内的大范围探测,左右两侧各装一个红外避障模块负责侧方近距离检测,夹角朝外45度左右。红外避障模块原理是发射红外光,接收反射光强弱来判断是否有障碍物,比较器阈值通过电位器调节,调节距离范围大概在2cm到30cm之间。实测下来,用超声波决定“减速还是转向”,用红外决定“转向是否安全”,这个分工最合理。
4.2 避障决策表的构建
避障逻辑不要写成满屏if嵌套,我建议做成查表决策。把超声波距离分成三档,S(近)、M(中)、F(远),左右红外各一档(有/无),一共3×2×2=12种组合,每种组合对应一个动作。
typedef struct { uint8_t distLevel; uint8_t leftBlock; uint8_t rightBlock; void (*action)(void); } ObstacleDecision; ObstacleDecision decisionTable[] = { {LEVEL_NEAR, 0, 0, TurnLeft180}, {LEVEL_NEAR, 1, 0, TurnRight}, {LEVEL_NEAR, 0, 1, TurnLeft}, {LEVEL_NEAR, 1, 1, StopAndBeep}, // 其余组合按同样方式填充 };这样做的好处是决策逻辑一目了然,后期想调整优先级或者增加新动作,只需要改表,不需要改程序骨架。而且查表的运行效率比多个if else高很多,虽然在小车上这点性能差异感受不到,但代码的可维护性提升了一大截。
有一个经验值得分享:前方近距离有障碍,左右两侧都安全时,不要默认向左转。很多小车设计成固定左转,跑几次就会发现总往一个方向绕,场地利用率很低。我建议加上一个“上次转向方向”的记忆变量,这次向左,下次就优先尝试向右,能有效避免小车在狭窄区域反复打转。
5. 遥控链路:蓝牙串口协议与指令解析
5.1 蓝牙模块配置与串口对接
HC-05蓝牙模块上电后,按住模块上的按键再上电就能进入AT指令模式,串口波特率默认是38400。先发AT,返回OK;然后AT+NAME=Car,AT+ROLE=1(设为从机可以被手机连接),AT+UART=9600,0,0(把通信波特率改成9600),改完波特率之后断开重新上电,用手机蓝牙连接,配对码一般是1234。
模块的TXD接STM32的USART1_RX(PA10),RXD接USART1_TX(PA9),一定要共地。串口初始化设置9600波特率、8位数据、无校验、1位停止位,然后就能互相收发数据了。我用的是HAL库的串口中断接收,每次接收一个字节存入环形缓冲区,主循环在空闲时去取,这样不会因为蓝牙数据太多而卡死主流程。
5.2 指令协议的定义与容错
遥控指令不要用单个字符代表一个动作,因为蓝牙传输过程中很容易丢包或者误码,单个字符一旦错了,小车会执行一个完全未知的动作。我用的协议是帧头+数据+校验:
typedef struct { uint8_t header; // 固定0xAA uint8_t carMode; // 'R'遥控, 'O'避障, 'T'循迹 int8_t speed; // -100 ~ 100 int8_t turn; // -100 ~ 100 uint8_t checksum; // 前四个字节异或和 } RemoteFrame;接收端必须校验帧头、长度和校验和,任何一个不对就丢弃整帧。这样做表面上好像增加了开销,但实际效果是误触率几乎降为零。手机端我用的是现成的蓝牙串口调试APP,它支持自定义按键发送十六进制字符串,只需要把每一帧数据做成固定字符串发出去就行。如果环境里有多个蓝牙设备,也不用担心串频,因为校验和保护住了。
6. 联调实测中的坑与排查思路
6.1 一个典型的“车轮乱转”问题排查全过程
第一次把三个功能合到一起的时候,我遇到了一个非常诡异的问题:单独测试遥控正常,单独测试循迹也正常,但一进入避障模式,左轮明显发抖,右轮偶尔反转。排查过程是这样的:
第一步检查电源波形。用万用表量TB6612的VM引脚,发现避障模式下电压波动比遥控模式大得多,但是并没有跌破电机最低工作电压。
第二步检查PWM信号。用逻辑分析仪抓PA6和PA7的波形,发现PWM频率是对的,但占空比在一个很短的时间内跳来跳去,像是被另一个进程改了。
第三步查代码。避障模式里确实没有直接改PWM的函数,但是避障循环里有每次20ms的超声波测距阻塞延时,而PWM输出是用TIM2定时器中断更新的。超声波测距期间,如果刚好发生定时器中断,而中断处理函数里读取了某个和其他线程共享的全局变量,就可能读到中间状态。问题出在我不小心在定时器中断里直接修改了全局的速度变量,而主循环也在改,导致了竞态。
解决办法很简单:所有跨中断和主循环共享的变量都改成volatile,并且给速度更新加一个临界区保护。嵌入式里这种问题非常典型,不用逻辑分析仪很难定位。
6.2 电源跌落引发的连锁反应
另一个坑发生在电池电量从7.4V掉到6.5V左右的时候。现象是循迹小车突然变得很“神经质”,黑线明明在中间,它却频繁左右摆头。排查到最后发现,供电电压不足导致五个循迹传感器因为比较器参考电压波动,输出信号变得不稳定。
这个问题不是代码能解决的,根源是供电架构设计不科学。我后来做了两件事:第一,把降压模块的输出电容加大到470uF,并且把降压后的5V单独走一路给传感器,避免和电机驱动共享稳压源;第二,在程序里加了低电量检测,ADC采集电池电压,低于6.8V就在OLED上显示警告,并限制最高速度,防止电机抢电导致单片机复位。电压问题如果等到“单片机复位”才意识到,往往已经晚了,因为复位可能是瞬间的,你根本来不及观察现象。
另外,电机启动瞬间的反电动势会通过地线回流,导致STM32的ADC采样值抖动。我给电机两端并联了104陶瓷电容和反向续流二极管,TB6612输出到电机的线尽量短且双绞,这部分硬件噪声处理好了,很多“玄学”问题会自动消失。
我在实际调试中的体会是,这类小车项目90%的难度不在“写代码”,而在“定位问题”和“供电设计”。如果你发现自己调了很久还是一头雾水,先别急着改算法,用万用表和逻辑分析仪把信号波形、电压波形全部拉出来看一遍,很多时候答案就在波形里。这个项目做完以后,你对定时器、PWM、串口中断、ADC采集、PID控制这些嵌入式最基础的知识,都会有一个非常扎实的理解,后面再去做平衡车、四轴、机械臂,很多思路都能直接迁移过去。
本文还有配套的精品资源,点击获取