我先把话说在前面:智能小车这个项目,网上教程一抓一大把,但九成都是“代码能跑、车能走”就草草收尾。真正把原理讲清楚、把坑踩明白、让你换个传感器、换块板子也能自己改出来的内容,反而少见。这篇文章我不打算只给你贴代码,而是把我从选型、画电路、写驱动到整车联调的全过程拆开讲一遍,重点说清楚每个模块为什么这么接、每段代码为什么这么写,以及我在实测中遇到的几个典型问题是怎么定位和解决的。
先说结论:基于STM32单片机的智能小车,是入门嵌入式最值得动手做一遍的项目之一。它看着简单,实际覆盖了GPIO操作、定时器PWM、外部中断、串口通信、传感器采集、电机驱动、电源设计这几大块嵌入式核心知识。你把这个项目完整吃透,后面再去做平衡车、机械臂、甚至一些简单的物联网设备,思路基本都是通的。这篇文章适合正在学STM32但不知道怎么综合运用的人,也适合准备参加电子设计竞赛、还在纠结方案选型的同学,当然,纯小白只要愿意边看边查手册,跟着走一遍也能跑起来。
1. 项目概况与整体方案选型
1.1 这个项目到底做了什么
我做的这辆小车,功能定位是“基础三合一”:支持蓝牙遥控、黑线循迹、超声波避障。这三个功能单拎出来都不复杂,但组合到一起,就需要在代码层面做统一的任务调度和状态切换,这反而是整个项目里最锻炼人的地方。
硬件上用的是STM32F103C8T6最小系统板,电机驱动选的是DRV8833模块,电机是常见的TT马达(带减速齿轮箱那种),循迹用三路TCRT5000红外反射模块,避障用HC-SR04超声波模块,蓝牙用HC-05串口透传模块,供电用两节18650锂电池串联。整车结构是三层亚克力板,用铜柱支撑,属于最经典的4WD小车底盘方案。
代码部分,我按照模块化的思路拆成了motor.c、track.c、ultrasonic.c、bluetooth.c、main.c几个文件,每部分独立封装,方便单独调试。整篇博文后面会把这些模块的实现思路和关键代码都贴出来,你照着搭就能复现。
1.2 为什么选STM32,而不是Arduino
我知道很多新手上来会纠结一个问题:做智能小车,用Arduino不是更快吗?确实,Arduino封装性好,几行代码就能驱动电机,网上现成示例也多。但我的看法是:如果你想长期在嵌入式这条路上走下去,或者想彻底搞懂单片机底层到底是怎么工作的,STM32这个坎迟早要过。
举个最直观的例子。Arduino里你调analogWrite(pin, speed),几秒钟就能让电机转起来。但在STM32上,你要自己开通定时器时钟、配置预分频系数、设置自动重载值、选择PWM通道、配置输出比较模式、设置占空比……每一步都要明确告诉芯片“你要干什么”。这个过程很繁琐,但也会逼你把“PWM到底怎么产生的”“定时器时钟树怎么走的”这些问题真正搞懂。
用生活化的类比来说:Arduino像是开自动挡汽车,踩油门就走;STM32像是开手动挡,你得自己配合离合、油门、挡位。虽然上手慢,但一旦熟练了,你对“车子”的控制能力是完全不一样的。智能小车选STM32,最大的意义不是“完成一个项目”,而是“通过这个项目把单片机摸透”。
1.3 系统功能框架和硬件选型总览
整个系统的功能逻辑是这样的:上电后小车进入默认模式,通过蓝牙接收手机App或串口调试助手发来的指令,在遥控、循迹、避障三个模式之间切换。遥控模式下,手机发送方向指令,小车执行前进、后退、左转、右转、加速、减速;循迹模式下,三路红外传感器检测地面黑线,单片机根据检测结果实时调整左右轮速差,实现沿黑线行驶;避障模式下,超声波模块测量前方障碍物距离,小于设定阈值时自动转向。
硬件选型方面,我整理了一份对照表,方便你根据自己的情况决定用什么:
| 模块 | 我用的型号 | 替代方案 | 选择理由 |
|---|---|---|---|
| 主控 | STM32F103C8T6 | STM32F103ZET6 | C8T6性价比高、体积小,入门够用;ZET6资源更丰富,适合后续扩展摄像头 |
| 电机驱动 | DRV8833 | TB6612、L298N | DRV8833压降小、体积小,适合小电流TT马达;L298N压降大,不推荐给3V电机 |
| 电机 | TT马达(1:48减速比) | 带编码器TT马达 | 普通TT马达便宜;带编码器版本可以做闭环速度控制 |
| 循迹传感器 | 三路TCRT5000模块 | 五路灰度传感器 | 三路适合入门,五路精度更高但逻辑更复杂 |
| 避障传感器 | HC-SR04超声波 | VL53L0X激光测距 | HC-SR04便宜、资料多;VL53L0X精度高但价格贵 |
| 蓝牙模块 | HC-05 | HC-06、JDY-31 | HC-05支持AT指令配置,既能做主又能做从,灵活 |
| 电源 | 两节18650串联7.4V | 5V移动电源直供 | 18650动力足,适合驱动电机;但要注意降压给单片机供电 |
| 下载器 | DAPLink | ST-Link V2、J-Link | DAPLink便宜,SWD接口四根线就能下载调试 |
这套配置下来,整车的物料成本大概在150到200元之间,不算贵,而且每个模块都能单独买备件,做完之后拆下来还能用于其他项目,复用性很高。
2. 硬件清单与电路设计要点
2.1 主控、驱动、供电三大核心模块
硬件设计是整个项目的底盘,电路搞错了,软件写再多也白搭。我先讲三个最核心的模块:主控、电机驱动、供电。
主控我用的是STM32F103C8T6,这是Cortex-M3内核、72MHz主频,有64KB Flash和20KB SRAM。做智能小车属于“性能溢出”的状态,好处是程序怎么写都不会觉得资源紧张,也不用抠内存。C8T6的引脚是LQFP48封装,一共37个IO口,对于小车项目来说非常宽裕。后期如果你想加摄像头模块,推荐换成ZET6,引脚的丰富度和Flash空间都会大很多,网上也有大量现成的ZET6智能小车方案可以参考,代码迁移成本不高。
电机驱动选DRV8833,是一片双H桥驱动芯片,工作电压2.7V到10.8V,持续输出电流1.5A,峰值电流2A,驱动TT马达绰绰有余。重要的是DRV8833的内阻很低,压降小,这意味着电机能得到更接近电源电压的驱动电压,转速更足。相比之下L298N在电流较大的时候压降能达到2V以上,本来是7.4V的电池电压,到电机端可能只有5V多,车速和扭矩都会明显变弱。
供电方案是整个小车最容易出问题的地方。很多人喜欢用一块充电宝或者四节5号电池给整车的所有芯片供电,结果电机一转,单片机就复位。原因很简单:电机启动瞬间的电流可以冲到1A甚至更高,会导致电源电压瞬间跌落,单片机的供电电压只要低于其复位阈值(一般是2.0V左右),立刻重启。我的做法是动力电源和逻辑电源完全分开,两节18650电池(标称电压7.4V)直接接电机驱动VM引脚,给电机供电;同时从电池端引一路到降压模块,降到3.3V给单片机、传感器、蓝牙模块供电。这样一来,电机电流再怎么波动,也不会直接影响单片机的电源稳定性。
2.2 电机驱动电路与PWM调速原理
电机驱动原理图这件事,网上问的人特别多。其实搞清楚原理就不难。DRV8833模块上有两组输入输出:AIN1、AIN2控制左侧电机,BIN1、BIN2控制右侧电机。每组输入的逻辑状态决定电机的转动方向,如下表:
| AIN1 | AIN2 | 电机状态 |
|---|---|---|
| 0 | 0 | 刹车(滑行) |
| 0 | 1 | 正转 |
| 1 | 0 | 反转 |
| 1 | 1 | 刹车(制动) |
但是只靠这两个引脚,电机只能全速正转或反转,不能调速。所以要用PWM控制。PWM就是用一个高频方波,通过调整占空比——也就是高电平在整个周期中的比例——来控制加在电机上的平均电压,从而控制转速。开始的时候不用纠结高频到底多高,定时器默认配置一般都够用。
我在程序里把定时器2的通道1和通道2分别接到了STM32的PA0和PA1,也就是AIN1、BIN1输入脚,用来输出PWM调速信号;AIN2和BIN2则用普通GPIO控制方向。这样每个电机的控制方式就是:AIN2管方向,PA0的PWM管速度。右侧电机同理,BIN2管方向,PA1的PWM管速度。硬件接线如下(以我用的开发板引脚为例):
- DRV8833的VM接电池正极,GND接电池负极,同时GND一定要和STM32的GND连在一起,这是整个系统正常工作的前提,也就是“共地”。
- STM32的PA0 —> AIN1,PA1 —> BIN1,PB0 —> AIN2,PB1 —> BIN2。
- DRV8833的AO1、AO2接左侧电机两端,BO1、BO2接右侧电机两端。
- DRV8833的VCC接3.3V,用于逻辑供电(部分模块VCC需要接5V,具体看模块说明)。
注意:单片机PWM输出引脚要接电机驱动的逻辑输入脚,不能直接接电机,更不能用单片机IO口直接驱动电机。电机的工作电流远超IO口承受能力,烧引脚是小事,烧主控就麻烦了。
2.3 电源系统设计:动力电与逻辑电的分离
电源是整个智能小车项目里最容易踩坑、也最容易被忽视的部分。我见过太多人小车跑着跑着突然复位,或者蓝牙一连接就重启,最后排查来排查去,都是供电设计的问题。
我的方案是这样:两节18650电池串联,标称电压7.4V,满电时是8.4V。这路电压我直接供给DRV8833的VM端,作为电机电源。同时从电池正极引一路到降压模块(我用的是MP1584模块,输出调到3.3V),给STM32最小系统板供电。另外,传感器和蓝牙模块也统一从3.3V供电。
这里有个细节要注意:蓝牙模块HC-05的核心是3.3V逻辑,虽然部分模块板载LDO支持5V输入,但从单片机供电一致性的角度来说,放在3.3V这一侧最稳。超声波HC-SR04比较特殊,它本身是5V供电的模块,但VCC接5V时,回波引脚输出的高电平也是5V。这个5V电平如果直接接到STM32的PA口上,长期来看有烧引脚的风险(STM32大多数IO是5V耐压的,但不是全部)。稳妥做法是用两个电阻分压,把回波信号降到3.3V再进单片机。我用的是10K和20K电阻分压的方案,简单可靠。
还有一点,很多人容易忽略:电机启动瞬间会产生很大的反向电动势,通过电源线扩散后可能导致单片机供电纹波增大,影响稳定性。我的解决办法是:
- 在电池正负极之间并联一个470uF的电解电容,吸收大电流波动;
- 在STM32的3.3V供电入口并联一个100nF的陶瓷电容,滤除高频干扰;
- 电池到电机驱动的导线尽量短、尽量粗,减少线路压降。
实测下来,这样处理之后,即使电机急停急转,单片机也没有再出现过复位。这个经验对于所有带电机的小车项目都适用,不管你是做循迹小车还是搬运小车。
2.4 传感器选型与接线
循迹模块我选了三路TCRT5000红外反射传感器。这个模块的原理不复杂:TCRT5000内部有一个红外发射管和一个光电接收管,红外光打到地面上,黑色表面吸收大部分光线,接收管收到的反射光弱,输出高电平;浅色表面反射大部分光线,接收管收到的反射光强,输出低电平。模块上还有一路LM393电压比较器,把模拟信号转成数字信号,直接输出0或1给单片机,省去了自己做AD采集和阈值判断的麻烦。
三路传感器的安装位置有讲究。我建议三个传感器横向排列,间距比黑线宽度略宽一点,中间一路对准黑线中心,左右两路在黑线两侧,摆成“川”字结构。这样当小车偏左时,左侧传感器会扫到黑线输出一个信号,单片机就知道该往右修方向;偏右时同理。我实测下来这个方案在2到3厘米宽的黑色胶带线路上表现很稳定。
超声波HC-SR04的接线更简单:VCC接5V、GND接GND、Trig接一个GPIO、Echo接另一个GPIO(要配合电阻分压)。测距原理是用Trig发一个10us以上的高电平脉冲,模块内部会自动发出8个40kHz的超声波脉冲,然后Echo引脚拉高,高电平持续的时间就是声波往返的时间。距离等于高电平时间乘以声速(340m/s)再除以2。
注意:HC-SR04测距有一个盲区,大概2cm以内测不准,所以避障阈值尽量不要设到3cm以下,否则还没等小车反应就已经撞上了。我一般把避障阈值设为20cm到30cm之间,留足转向余量。
3. 软件架构与核心代码实现
3.1 工程组织方式与模块划分
软件部分的代码我放在STM32标准外设库的工程框架里,用Keil MDK开发。很多初学者容易犯一个错误:把所有代码全堆在main.c里,写个两三百行还觉得挺整齐。等你要加功能、要调试的时候就知道有多痛苦了。我自己早期也是这么干的,直到有一次想单独测超声波模块,发现整工程只改一行注释都要重新编译一遍,才下决心拆模块。
我做了一次重构,按功能把代码拆成了这么几个文件:
- main.c:系统初始化、主循环状态机调度
- motor.c / motor.h:电机方向控制、PWM调速、速度参数映射
- track.c / track.h:循迹传感器读取、循迹转向逻辑
- ultrasonic.c / ultrasonic.h:超声波测距、避障逻辑
- bluetooth.c / bluetooth.h:串口初始化、蓝牙指令接收与解析
- delay.c / delay.h:毫秒和微秒延时函数
每个模块提供的接口尽量精简。比如motor模块对外只暴露motor_init()、motor_set_speed(int left_speed, int right_speed)、motor_stop()几个函数,底层怎么配置定时器、怎么切换方向,外部完全不用关心。这样main.c里的逻辑就能写得很干净,实现“业务逻辑”和“硬件驱动”分离。
3.2 定时器PWM输出:让电机速度可控
在STM32上产生PWM信号的核心思路是使用定时器的输出比较功能。定时器以固定的频率不断计数,计数值到达自动重装载值后清零重新开始;同时有一个比较寄存器(CCR),当计数器值和CCR相等时,输出引脚电平翻转,这样就在引脚上产生了一个频率和占空比都可控的方波信号。
我用的定时器2是STM32F103C8T6上资源很充沛的通用定时器,它的复用通道映射到了PA0和PA1。配置过程分四步:
第一步,开启定时器2的时钟,同时设置PA0、PA1为复用推挽输出。复用推挽的意思是引脚状态由片上外设接管,而不是普通GPIO手动拉高拉低。
第二步,配置定时器基础参数,包括预分频器(PSC)、自动重装载值(ARR)和计数模式。我设置的PSC为71、ARR为999,系统时钟72MHz。定时器计数频率是72MHz除以(PSC+1)等于1MHz,也就是每计数一次需要1us;计数到1000次就是1ms,对应的PWM频率为1kHz。这个频率驱动电机是没问题的,如果追求更平顺可以再调高。
第三步,配置通道PWM模式。我选了PWM模式1:计数值小于CCR时输出有效电平,大于CCR时输出无效电平,这样通过调节CCR就能直接改变占空比。
第四步,使能定时器输出、使能通道。
关键代码我贴出来,你直接在标准外设库工程里就能用:
void TIM2_PWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; // 使能定时器2和相关引脚时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 定时器基础配置:72MHz / 72 = 1MHz,计数1000次 => 1kHz PWM TIM_TimeBaseStructure.TIM_Period = 999; TIM_TimeBaseStructure.TIM_Prescaler = 71; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure); // PWM模式1,输出极性不反相 TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStructure.TIM_Pulse = 0; TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OC1Init(TIM2, &TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM2, TIM_OCPreload_Enable); TIM_OC2Init(TIM2, &TIM_OCInitStructure); TIM_OC2PreloadConfig(TIM2, TIM_OCPreload_Enable); // 使能定时器 TIM_Cmd(TIM2, ENABLE); }设置占空比的操作很简单,比如想设置左电机PWM占空比40%,就调用TIM_SetCompare1(TIM2, 400),右侧电机对应TIM_SetCompare2(TIM2, 400)。CCR取值0到999,对应0%到99.9%的占空比,线性关系非常直观。
我实际用的调速逻辑是这样的:定义了一个基础速度参数base_speed,默认设60%占空比,也就是CCR=600,大概对应小车轮子的中速。循迹模式下,小车需要转弯时,左转就降低左电机速度、保持右电机速度,甚至可以给左电机一个短时反转来实现原地转向。具体差值要实测调整,后面联调部分会细说。
3.3 循迹算法:从数字量到转向控制
循迹逻辑是整个小车里最能体现编程思路的部分。三路TCRT5000传感器,每路输出0或1,三路组合起来只有8种情况,但实际运行中经常出现的只有下面几种:
| 左 | 中 | 右 | 状态判断 | 控制动作 |
|---|---|---|---|---|
| 0 | 0 | 0 | 全部出线 | 停车或沿直前方向继续行驶 |
| 0 | 0 | 1 | 偏右 | 向右修方向 |
| 0 | 1 | 0 | 正中 | 直线全速前进 |
| 0 | 1 | 1 | 略微偏左 | 向左微调 |
| 1 | 0 | 0 | 偏左 | 向左修方向 |
| 1 | 1 | 0 | 略微偏右 | 向右微调 |
| 1 | 1 | 1 | 十字路口 | 直行通过 |
代码逻辑我一开始用的是最简单的if-else判断,但这种写法问题很多:每次判断都要读取一遍GPIO,代码重复不说,也很难加入“微调”和“急转”的区分。后来我改成了查表法——把8种状态定义成枚举,再在当前状态和对应动作之间建立映射关系,代码一下子就清爽了许多。
查表实现的关键点是这样的:
typedef struct { uint8_t left; uint8_t middle; uint8_t right; void (*action)(void); } TrackAction; const TrackAction track_actions[] = { {0, 0, 0, track_all_lost}, {0, 0, 1, track_turn_right}, {0, 1, 0, track_go_straight}, {0, 1, 1, track_turn_right_slight}, {1, 0, 0, track_turn_left}, {1, 1, 0, track_turn_left_slight}, {1, 1, 1, track_go_straight_cross}, };然后主循环里把三路传感器读进来,查表找到对应的动作函数执行就完了。这样每添加一种轨道情况,只需要加一行表项,不需要改主循环结构。这种设计思路在做复杂一点的小车控制时非常实用,尤其是像电磁三轮智能小车循迹或者工创赛的智能物流搬运小车这类场景,赛道元素多,查表法能帮你快速适应新赛道。
具体到每种动作的实现,核心就是左右电机的速度差。比如向左转稍微大一点的时候,我设左边速度为30%占空比,右边速度为80%,这样车头会明显向左偏转,但仍保持前进。如果是原地左转,就直接左边刹车、右边正转,适合在十字路口掉头。
注意:循迹模块有个常见问题,就是环境光干扰。室内灯光直射时,红外反射量会变化,可能导致传感器误判。我实测在模块下方垫了两层黑色电工胶带,减少环境光从模块侧面进入,误判率明显降低。你也可以在已经调好阈值之后用热缩管把传感器侧面封住。
3.4 HC-SR04超声波测距与避障状态机
超声波测距的代码,最常用的方法是“发送Trig脉冲,然后等待Echo拉高,用定时器计时,Echo拉低时读取定时器值”。这里的关键是测距过程中不能阻塞太久,否则主循环里其他任务都会卡住。
我的做法是:在避障主循环里,每次先触发测距,然后等待Echo返回。等待过程用一个超时机制保护,如果300ms内没有返回就认为测距失败,直接返回一个远距离值,避免程序卡死。
float ultrasonic_get_distance(void) { float distance; uint32_t timeout = 0; // 发送10us以上高电平触发脉冲 GPIO_SetBits(GPIOC, GPIO_Pin_6); delay_us(15); GPIO_ResetBits(GPIOC, GPIO_Pin_6); // 等待Echo变高,开始计时 while (GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_7) == 0 && timeout < 3000) { timeout++; delay_us(1); } timeout = 0; // 等待Echo变低,计时结束 while (GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_7) == 1 && timeout < 3000) { timeout++; delay_us(10); } // 计算距离:声速340m/s,往返时间 // timeout * 10us 是时间,距离(cm)=时间(us)*0.017 distance = timeout * 10 * 0.017f; return distance; }这段代码用的是轮询加延时的方式,简单易懂,适合入门。如果你想把测距做得更精准或者更高效,可以把Echo接到定时器的输入捕获通道上,用硬件自动记录高电平时间,那样不占CPU,而且精度还高。我在后期扩展FreeRTOS版本的时候就是把超声波改成中断模式测距的。
避障逻辑我用状态机实现。状态机听起来高大上,其实本质就是一个变量记录当前的状态,根据条件决定跳转到哪个状态。我的避障状态只有4个:
- 前进:无障碍,全速前进
- 左转:检测到障碍,左转
- 右转:左转后仍检测到障碍,右转
- 后退:连续多次检测到障碍,后退再转向
代码主循环的结构大致是:
while (1) { if (mode == MODE_AVOID) { distance = ultrasonic_get_distance(); if (distance < 25.0f) { obstacle_count++; if (obstacle_count >= 2) { avoid_state = AVOID_TURN_LEFT; obstacle_count = 0; } } else { avoid_state = AVOID_FORWARD; obstacle_count = 0; } switch (avoid_state) { case AVOID_FORWARD: motor_set_speed(base_speed, base_speed); break; case AVOID_TURN_LEFT: motor_set_speed(-base_speed, base_speed); delay_ms(400); break; default: break; } } }这里obstacle_count起到滤波作用,防止超声波偶尔一次的数据抖动导致小车误动作。实测效果不错,小车碰到前方障碍会先停下来,再原地左转,转过之后如果前方还是障碍,就进入右转状态,基本能把墙角和箱子绕开。
3.5 蓝牙遥控与串口指令解析
蓝牙部分用的是HC-05模块,工作在全双工串口模式。HC-05默认出厂波特率是9600,和STM32的USART2(我用PA2、PA3)相连后,手机端用任意蓝牙串口App连接,发送的字节就会通过串口透传给STM32。
HC-05用之前可以先用AT指令配置一下,把模块名称改成你喜欢的名字,波特率改成115200。AT指令配置方法很简单:按住模块上的按键再上电,就进入了AT命令模式,此时直接用串口助手发AT指令就行。具体指令如下:
AT+NAME=MyCar:修改蓝牙名称AT+UART=115200,0,0:设置波特率115200AT+ROLE=1:设置为主模式(如果你想用两个蓝牙模块互连就用这个,普通手机连接保持从模式即可)
写完后重启模块,重新用手机搜索连接。然后STM32的串口初始化也要改成对应波特率。
串口接收我用的是中断方式。在串口接收中断服务函数里,把收到的一个字节存入缓冲区,同时在主循环里解析判断。这样单片机在收指令的同时还能干别的事(比如测距、控制电机),不会因为等待串口数据而卡住。
void USART2_IRQHandler(void) { uint8_t ch; if (USART_GetITStatus(USART2, USART_IT_RXNE) != RESET) { ch = USART_ReceiveData(USART2); uart_rx_buf[uart_rx_len++] = ch; uart_rx_len %= UART_RX_BUF_SIZE; } }指令协议我用的是最简单的一个字符一指令的方式:F前进、B后退、L左转、R右转、S停止、A切循迹模式、D切避障模式、M切遥控模式。这种协议最大的优点是好理解、好调试,适合自己玩。如果你想做得更规范一点,可以定义成“帧头+长度+数据+校验”的形式,比如AA 01 01 FC这样的格式,这样抗干扰能力更强,适合比赛中用。智能车比赛里的蓝牙遥控或者WiFi遥控基本都会用类似带帧头和校验的协议。
4. 系统联调与实测过程记录
4.1 分步点亮的最小系统验证法
整个系统组装完成后,别急着把所以功能都打开跑,那样出了问题都不知道从哪开始查。我的习惯是把项目拆成几个独立的验证步骤,每步只测一个模块,全部通过后再组合起来:
第一步,最小系统验证。给STM32最小系统板单独供电,用一个LED闪烁程序测试GPIO和时钟是否正常。这步能排除板子本身和下载链路的问题。
第二步,电机驱动验证。把电机驱动和电池接好,写一个最简单的测试程序:左边电机正转1秒、停1秒、反转1秒,右边电机同样来一遍。如果电机动作和预期一致,说明PWM配置、方向引脚、电机接线都没问题。
第三步,传感器验证。分别测试循迹模块和超声波模块。循迹模块可以用串口打印三路传感器的原始电平值,拿一张白纸和黑纸对比看输出是否正确变化;超声波模块用尺子量一个已知距离,串口打印测量值,确认误差在可接受范围。
第四步,蓝牙验证。串口连接HC-05,手机发指令,看单片机的串口接收是否正常,再结合电机控制,实现手机遥控车辆移动。
最后才是整合模式切换逻辑,把循迹、避障、遥控放在同一个程序里,通过蓝牙指令切换。每走一步都确认无误,后面出问题时定位范围就会小很多。我见过有人一上来就烧完整程序,车子不动的时候连是传感器问题还是电机问题都分不清,排查效率非常低。
4.2 循迹与避障的实测参数调整
实测过程中我遇到最多的问题是循迹参数不合理。一开始我把左右轮的速度差设得太小,左转也就20个点的占空比差值,小车在弯道直接冲出了黑线。后来改成比例式转向:偏移越小修正量越小、偏移越大修正量越大,循迹效果才稳定下来。
循迹效果好坏,核心在于合理调整几个速度参数:
- 基础速度:直线匀速行驶的速度,我实测60%占空比左右比较合适,太快转弯容易掉线,太慢赛道效率太低;
- 转弯修正量:发生偏移时左右轮占空比差值,默认基础速度的30%左右,根据实际赛道微调;
- 原地转向时间:十字路口掉头或原地换向时,需要保持反转状态的时间,我实测400到500ms比较合适,时间太短转不过去,时间太长容易过头。
避障模块的参数我用了一套比较保守的配置:测距周期200ms,障碍阈值25cm,左转维持时间400ms。说实话这套参数在小空间房间里跑没有出现过撞墙,唯一的问题是遇到又窄又长的走廊时,小车会在障碍物前面反复左转,进退两难,这就暴露出单纯靠“避障状态机”做导航的局限性——它没有全局地图,只是局部反应。
4.3 整车稳定性提升的几点经验
写到这一节,我想分享一些平时在实验室里反复试出来的经验,这些东西论文里不写、教程里很少提,但直接影响你的小车能不能稳定跑完一趟:
第一,车轮和地面之间的摩擦力影响远比你想象的大。我一开始用的是一套黑色硬塑料轮,在瓷砖地面上打滑严重,起步和刹车都容易跑偏。后来换了橡胶轮,情况立刻好转,循迹的过弯稳定性提升了一个档次。如果你的小车用着总觉得方向发飘,先别急着调代码,看看轮子是不是该换了。
第二,亚克力底盘的螺丝一定要拧紧。电机高速运转时,松动的底盘会产生共振,传感器跟着一起抖,结果就是循迹模块偶尔跳变,信号显示小车时而左偏时而右偏,实际上机械结构都没固定好。建议装完底盘后通电让电机空转一分钟,听听有没有异响,再检查一遍所有螺丝。
第三,注意线缆布局。动力线的电流大、干扰大,尽量和信号线分开走,不要跟循迹传感器的输出线绑在一起。我一开始图省事把所有线都扎在一起,结果一开电机,循迹数据就开始跳,后来把动力线和信号线分开走线,问题就消失了。这就是典型的电磁干扰问题。
5. 调试中的疑难问题与排查思路
5.1 电机不转或者只有单侧转
电机完全不转或只有一边转,是最常见的问题。我自己遇到的第一反应是先查硬件,再查软件。
硬件方面,检查顺序是:电池有没有电、开关是否导通、DRV8833模块上的电源指示灯是否亮、电机线有没有接牢、驱动模块GND和单片机GND是否共地。很多新手把驱动模块的GND和单片机的GND忘了连,结果信号电平完全没有参考电压,电机自然不动。
软件方面,用示波器(没有示波器就直接用万用表)看PWM引脚有没有波形输出。没有波形就重点检查定时器有没有开时钟、通道有没有使能;有波形但电机不动,就要查方向控制引脚的电平是否正确。我的一个亲身教训是,方向引脚默认是高电平,而高电平在部分驱动模块上对应刹车,所以程序里要先复位方向引脚为低电平再输出PWM,否则车子会一动不动。
5.2 小车跑一会儿就复位
这个问题在前面电源部分已经详细说过了,核心原因就是电压跌落。除了电源分离措施,还有一个容易忽略的点:如果电池电量已经比较低,内阻会变大,负载时电压跌落更明显,小车就会在电池快没电的时候频繁复位。我的经验是,18650电池用到电压低于3.5V左右就该换掉或者去充电了,不要硬扛。
另外,杜邦线质量也值得怀疑。劣质杜邦线内部铜芯细,电阻大,大电流下压降明显。我接电机驱动的线后来换成了硅胶软线,压降明显改善。如果你懒得换线,也可以把电池到电机驱动的线路尽量缩短,减少线损。
5.3 循迹跑飞与十字路口判断
循迹跑飞分两种情况。一种是直道上偏出,多半是速度太快或者转向修正量太小;一种是弯道上冲出去,多半是转向修正量跟不上的同时车速太高。解决思路是降速或者加大修正量,但这两者之间要找一个平衡,我建议先把速度降到40%左右,然后逐步提高修正量,直到过弯稳定再慢慢加回速度。
十字路口的判断也有讲究。三路传感器同时检测到黑线,也就是“111”状态,如果直接按直行处理,小车会在十字路口犹豫不决。我的做法是计数:连续检测到“111”状态超过一定次数(也就是行驶了一段距离)才认为真的到了十字路口,这时执行直行,避免因为地面花纹或杂色造成的误判。如果你需要小车在十字路口转弯,那就把“111”处理成对应的转向逻辑,具体看赛道要求。
5.4 烧录失败与下载器接线
STM32烧录失败也频繁遇到。我用的是DAPLink下载器,SWD接口,接线是SWDIO、SWCLK、GND、3.3V四根线,分别接STM32的PA13、PA14、GND、3.3V。烧录失败的常见原因有:
第一,接线接触不良。SWD接口对线序和接触质量比较敏感,杜邦线松动就会出现“连接不上目标设备”的提示。建议把线插紧,或者直接焊上去,一劳永逸。
第二,芯片被锁。如果你在程序里把SWD引脚复用成了其他功能,下次就下载不了了。解决办法是按住复位键,点击下载,看到开始下载的瞬间松开复位键。实在不行,把BOOT0拉高进入串口下载模式,擦除芯片后再拉回低电平,就能恢复了。这个方法我至少用了三次,每次都能救回来。
第三,电源不稳。如果DAPLink通过和目标板共用一个USB口供电,而USB口电流不足,下载过程中目标板复位也会导致下载失败。最好单独给目标板供电,只保留DAPLink的SWD三根线连接。
5.5 常见问题速查表
| 故障现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 电机不转 | 未共地、方向引脚电平错误、PWM未配置 | 万用表量电压,示波器看波形 | 补齐GND连线,方向引脚置低,检查定时器配置 |
| 只有单侧电机转 | 驱动模块某路损坏,或某路PWM未使能 | 交换电机线判断是否电机问题 | 更换故障通道,检查对应定时器通道初始化 |
| 上电后单片机不复位但程序不运行 | 启动模式不对,或芯片锁死 | 检查BOOT0跳线,尝试强制下载 | BOOT0拉高重新擦除,再拉回低电平 |
| 蓝牙连不上 | 波特率不匹配、模块处于AT模式 | 检查串口助手设置,重启模块 | 重新AT指令配置波特率,重启模块 |
| 循迹跑飞 | 速度快、修正量小、轮子打滑 | 降低速度,逐步提高修正量 | 换橡胶轮,调整PWM差值 |
| 小车自动复位 | 电压跌落、供电分离不足 | 万用表测电机启动时供电电压 | 动力电逻辑电分离,加储能电容 |
| 超声波测距异常 | 未加电阻分压导致回波损坏引脚、发射面遮挡 | 检测Echo引脚电平,检查模块接口 | 接电阻分压,清理发射面 |
6. 项目扩展方向与进阶思路
6.1 摄像头循迹与视觉导航
基础版小车做完之后,如果你还想继续进阶,第一个值得尝试的方向是摄像头循迹。方案上可以选择OpenMV或者K210这类带视觉处理的模块,通过串口把图像识别的结果发给STM32,STM32做运动控制,视觉部分交给摄像头模块上的处理器。这样架构比较清晰,也更好分工。
视觉循迹和红外循迹的核心区别是“感知范围更广”:红外模块只能看到前方一小段,而且只能判断有没有黑线;摄像头方案可以提前看到弯道、十字路口和障碍物,控制策略自然可以提前预判,车速也能相应提高。如果你参加的是智能车竞赛里的摄像头组,这个方向基本是必由之路。不过学习曲线相对也更陡,涉及色彩空间转换、阈值分割、透视变换、中心线提取等图像处理知识,可以作为一个中长期的进阶目标。
6.2 引入FreeRTOS实时操作系统
当你的小车功能越来越多——摄像头、蓝牙、多个传感器、屏幕显示、语音模块——你会发现裸机while循环里的任务调度非常脆弱,一个传感器等待时间长一点,其他任务就跟着卡顿。这时候就应该引入FreeRTOS了。
我把小车从裸机迁移到FreeRTOS之后的体会是:程序结构变得极其清晰。每个功能模块就是一个独立任务,比如超声波任务每200ms跑一次,蓝牙任务里用队列传指令,电机控制任务里用信号量去通知运动状态变化。任务之间的通信方式相比裸机“所有代码共享一个全局变量”的方式要明确得多。不过FreeRTOS入门也不难,其他可以搜“FreeRTOS项目实战”参考,关键是要理解任务优先级和阻塞调度的概念。
6.3 远程控制与IoT化
还有一个很有意思的扩展方向是给小车加上物联网能力。用ESP8266或者ESP32作为WiFi模块,和STM32通过USART通信,这样小车就能连到家里的WiFi路由器,再通过MQTT协议和控制端交互。控制端可以是一台电脑上的网页,也可以是一个微信小程序,甚至可以是云端服务器。这时候整套系统就变成了一个典型的端到云架构:传感器数据在STM32端采集,通过WiFi上传到云端的MQTT Broker,控制端再从云端订阅数据、下发指令。
我在后期就尝试了这种方案,用ESP8266把小车状态实时传到一个MQTT服务,手机App上能看到小车的速度和当前模式,也能远程切换模式。做完之后你会发现,原本的“智能小车”项目一下子升级成了“物联网智能终端”,可以对接很多实际应用场景,比如实验室里的远程巡检小车、工厂里的智能物流小车,核心架构都是相通的。
最后再分享一个小技巧:智能小车这类项目,最忌讳一次做完所有功能再通电调试。我自己的习惯是先让电机转起来,跑通最小闭环,再逐步加上循迹、避障、蓝牙这些模块。每一个模块加进来,都先在隔离状态下独立测试通过,再合入主程序。这样每次出问题都只在新增的那一小块代码里找,排查范围能缩小好几倍。做项目嘛,稳比快重要得多。