1. 整车硬件选型:主控、传感器和电源的搭配逻辑
在实验室里埋头调了两周车,从最开始只让电机转起来,到后来能在桌子腿之间绕来绕去,我最大的感触是:智能小车这东西,硬件选型决定了你的下限,程序逻辑决定了你的上限。很多人一上来就花大量时间抠电机驱动代码,结果底盘还没跑稳就卡住了。这篇我打算完整复盘我用STM32F407VET6做主控的避障加测温小车项目,把硬件选型、工程搭建、核心代码逻辑和踩过的坑一次说清,希望能给正在做类似项目的朋友省点时间。
1.1 为什么是F407VET6而不是更常见的F103
说实话,很多新手教程推荐用STM32F103C8T6最小系统板,理由是便宜、资料多、够用。但如果你要做的不只是"跑起来",而是想加传感器、加显示、加通信,甚至后续想往视觉或更加复杂控制方向扩展,F103在很多场合就力不从心了。
F407VET6的核心优势有几个:
- 主频168MHz,Cortex-M4F内核,带硬件浮点运算单元FPU。这意味着做浮点运算时速度优势明显,比如后面滤波算法里的平均运算、温度换算、距离换算,如果用F103的Cortex-M3做,纯软件模拟浮点,运算会慢不少,而且代码会复杂。
- 512KB Flash,192KB RAM,不用像在F103上那样动不动就担心Flash不够放。
- 外设资源丰富:多个USART、多个SPI、多个I2C、多个ADC、十几个定时器。你想同时挂超声波模块、舵机、红外测温、OLED显示屏、电机编码器,完全不会为引脚和定时器冲突发愁。
- 100引脚LQFP封装,GPIO充足。像我的设计里,超声波用了2个引脚,舵机云台用了1路定时器PWM,MLX90614用了I2C2,电机驱动用了4个方向IO加2路PWM,OLED用了I2C1,还预留了串口1做调试输出,加起来也就二十来个引脚,非常宽裕。
当然,F407VET6也不是没有缺点,最典型的就是IO耐压。F407的GPIO基本是3.3V电平,不像F103的IO有5V容忍能力。这点在后面接HC-SR04超声波模块的时候是个大坑,我会在避障那节详细说。
1.2 测距传感器选型:超声波、红外还是激光
避障小车最核心的就是测距。市面上常见的选择主要有三种,我逐个说下我的看法:
红外避障模块(如TCRT5000):优点是便宜、响应快、接口简单,输出高低电平,直接就能用。缺点是可测距离非常短,一般只有几厘米到十几厘米,而且对环境光线非常敏感,在强光下容易误判。这种模块适合做边界检测、循迹沿边,不适合做动态避障,因为等你检测到障碍物的时候,基本已经没有时间转向了。
激光测距:精度高、距离远、速度快,比如常见的VL53L0X这类,但成本相对高,而且激光发射头在小车上安装角度比较讲究,测距范围在远距离是强项,近距离反而可能盲区比较大,对入门项目来说真的没必要。
超声波(HC-SR04):我最后选了它。优点是价格极其便宜(几块钱一个),测距范围从2cm到400cm左右,对新手很友好。缺点是响应速度相对慢,而且波束有扩散角度,对薄杆之类的小障碍物可能测不到。但对于室内环境下的桌面小车避障,超声波配合舵机云台做多方向扫描,是性价比和效果都很均衡的方案。
如果你决心做避障,建议至少准备两个超声波模块,或者像我一样用舵机云台带一个超声波模块做左、中、右三向扫描。单模块就装在车头正中,只能测前方,遇到侧面突出障碍物会很被动。
1.3 电源系统:最容易在小车上翻车的部分
我见过太多小车调试时一切正常,一上电池就各种复位、数据跳变——十有八九是电源没处理好。
我的供电方案是这样的:
- 动力电池:两节18650锂电池串联,标称7.4V,直接接L298N电机驱动模块的VS引脚。
- L298N模块上通常自带一个5V输出的稳压芯片(比如78M05),输出能力大概几百毫安,我用它给STM32F407VET6最小系统板供电。
- 舵机(SG90)从L298N的5V输出取电,因为这个舵机堵转时电流能上到几百毫安,如果和主控共用一路5V,容易把MCU电压拉低导致复位。我在舵机电源引脚旁边并了一个470uF的电解电容,效果很明显。
这里有一个很关键的原则:电机、舵机这类大电流负载和主控尽量隔离供电,至少不能在PCB走线上混在一起。如果实在没有条件分开供电,也要在靠近MCU电源脚的位置加上足够的退耦电容(我习惯用100nF加10uF组合)。
提示:电池电压会随着电量下降而跌落。我实测两节满电18650大概8.2V,用到7.0V以下时L298N的5V输出就开始不稳定,小车会出现自动复位或者蓝牙、WiFi模块断连的现象。所以程序里建议加一个低电压检测功能,电压低于一定阈值就报警或停车,而不是让它带病工作。
2. 工程搭建与Keil环境配置:标准库还是HAL库
硬件选好之后,接下来就是把Keil工程搞起来。这一步能劝退一半新手,哪怕是老手,换一个工程文件也经常因为版本不匹配、宏定义缺失等问题卡壳。
2.1 工程文件结构:拿到别人的工程怎么下手
我这套工程的目录结构是下面这样的,Keil工程文件和源码都打包在一起,方便直接打开编译:
STM32F407VET6_SmartCar/ ├─ USER/ │ ├─ main.c │ ├─ stm32f4xx_it.c │ └─ system_stm32f4xx.c ├─ HARDWARE/ │ ├─ motor/ │ ├─ ultrasonic/ │ ├─ servo/ │ ├─ temperature/ │ └─ oled/ ├─ CORE/ ├─ SYSTEM/ │ ├─ delay/ │ ├─ sys/ │ └─ usart/ ├─ OBJ/ └─ STM32F407VET6_SmartCar.uvprojx你拿到一个工程文件,第一件事不是急着点编译,而是先在Keil里检查几项关键设置:
- 点击魔术棒(Options for Target),在Device选项卡确认芯片型号是STM32F407VET6。
- 在C/C++选项卡的Define栏确认宏定义,我这里是
STM32F40_41xxx,USE_STDPERIPH_DRIVER。如果你用的是HAL库,宏定义是STM32F407xx,两者不能混淆,否则编译会报一堆函数未定义。 - 检查Debug选项卡,确认调试器是ST-Link还是J-Link,并且你实际连接了对应的调试器。我调试时用ST-Link V2,下载速度设置为1MHz比较稳定,有些高速设置会导致"Error: Flash Download failed"。
2.2 为什么选标准库而不是HAL库
现在STM32CubeMX加HAL库已经是主流趋势,很多新项目都用它生成代码。那我为什么还坚持用标准库?
- 资料传承:目前网上大量现成的例程、教程、参考代码都是标准库写的,包括很多电机驱动、传感器驱动的参考代码。你拿标准库工程去对照网上案例改代码,几乎是无缝衔接。
- 代码透明:HAL库封装层次多,出问题时追踪问题比较痛苦,尤其对于一个GPIO的初始化和复用功能配置,HAL库要追好几层函数。标准库的配置项基本是一目了然的,对学习底层原理更友好。
- 工程体积和编译速度:虽然现代电脑编译这级别代码都不成问题,但标准库编译确实更轻量。
当然,如果你已经完全掌握了HAL库的工作方式,用HAL库也没问题。关键不是库本身,而是你要清楚每一行配置最终作用在芯片寄存器上的效果。这也是我说标准库适合练基本功的原因。有一点我得说清楚:我这里提供的工程是基于标准库,外设库版本对应的是STM32F4xx_DSP_StdPeriph_Lib_V1.8.0,如果你用其它版本,个别文件可能有差异,但核心驱动代码是通用的。
2.3 时钟树配置:8MHz晶振倍频到168MHz
F407VET6最高主频168MHz,需要用外部8MHz晶振通过PLL锁相环倍频得到。这块配置一般放在system_stm32f4xx.c里,里面有一个SystemInit()函数,会在进入main之前自动执行。
有一个非常容易踩的坑:有些盗版或剪贴板拷贝来的工程,PLL_M、PLL_N、PLL_P这些参数被改得乱七八糟,导致系统时钟不是168MHz,而是很低或者超频。实际表现就是本来应该跑1ms延时的函数,实际变成了2ms或者0.5ms,整个算法时序全乱掉,而且很难从表面看出来。
我工程的配置是标准的外接8MHz晶振倍频至168MHz:
#define PLL_M 8 #define PLL_Q 7 #define PLL_N 336 #define PLL_P 2对应的系统时钟计算:
- VCO输入频率 = HSE / PLL_M = 8MHz / 8 = 1MHz
- VCO输出频率 = VCO输入频率 × PLL_N = 1MHz × 336 = 336MHz
- SYSCLK = VCO输出频率 / PLL_P = 336MHz / 2 = 168MHz
这个计算过程你最好自己过一遍,改完代码后进行延时验证:用一个GPIO翻转输出,接示波器测频率,看看和预期是否一致。没有示波器的话,最简单的办法是写一个LED闪烁程序,用秒表粗略估算,虽然精度有限,但至少能发现时钟严重配置错误的问题。
2.4 下载调试:ST-Link的连接与常见失败
ST-Link与F407VET6的SWD接口只需要四根线:SWDIO、SWCLK、GND、3V3。我习惯在给板子上电的情况下连接ST-Link,并且注意ST-Link的3V3引脚不要和目标板电源并联供电(除非你确认电流在允许范围内),这样能避免一些奇怪的供电问题。
下载时如果出现"No Target Connected":
- 先查线序,SWDIO和SWCLK是否接反,这个错误率最高。
- 查目标板是否上电,以及VCC引脚电压是否正常。
- 检查Keil的Debug设置是否选择了正确的下载器和接口(Connect under Reset有时候能救回锁死的芯片)。
3. 超声波避障的实现细节
避障是这辆车的核心任务,也是代码逻辑最复杂的地方。如果你只会写"遇到障碍就左转"这种固定逻辑,车子在稍微复杂一点的环境里就会像无头苍蝇。我这里给的是一个带舵机云台的三向扫描决策方案,实测在室内绕开椅子腿、纸箱、墙边都能跑得比较自然。
3.1 HC-SR04驱动原理:触发、回波和超时
HC-SR04的工作流程非常经典:MCU给Trig引脚拉高至少10us,模块内部会主动发出8个40kHz的超声波脉冲,然后Echo引脚输出一个高电平脉冲,这个高电平的持续时间就是声波从发射到遇到障碍物返回的总时间。
距离计算公式:
距离(cm) = 高电平时长(us) / 58为什么是58?因为声速在空气中大约是340m/s,即0.034cm/us。声波来回的距离是实际距离的两倍,所以:
距离(cm) = 时间(us) × 0.034 / 2 ≈ 时间(us) / 58我的驱动代码核心部分是这样写的:
uint32_t Ultrasonic_GetDistance(void) { uint32_t time; float distance; // 发送10us以上高电平触发脉冲 GPIO_SetBits(GPIOB, GPIO_Pin_8); // Trig高 Delay_us(20); GPIO_ResetBits(GPIOB, GPIO_Pin_8); // Trig低 // 等待Echo变高,记录时间 while (!GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9)); // 开启定时器计时,或者直接用Delay计数方式 // 这里用定时器2计时,单位us TIM_Cmd(TIM2, ENABLE); while (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9)); // 等待Echo变低 time = TIM2->CNT; TIM_SetCounter(TIM2, 0); TIM_Cmd(TIM2, DISABLE); // 超时保护:如果time大于40000us,认为超出量程 if(time > 40000) return 999; // 超量程返回999cm distance = (float)time / 58.0f; return (uint32_t)distance; }这里有一个细节非常关键:等待Echo变高的while循环必须有超时保护。如果模块没接好、或者前方没有任何反射物导致Echo一直不拉高,程序就会死等在这个while循环里,整辆车直接"卡死"。这在实时系统里是不可接受的。
我试过几种超时方案,最简单的就是用DWT的计数器实现一个get_us()函数,加一个起始时间点判断:
uint32_t start_us = DWT_GetUs(); while (!Echo_Read && (DWT_GetUs() - start_us) < 50000); // 50ms超时实测下来这套超时保护非常可靠,后续不管是拔线还是没装传感器,程序都不会死机。
3.2 5V电平带来的磨合:Echo引脚必须分压
前面提到F407的GPIO没有5V容忍度,而HC-SR04官方给的逻辑电平是5V。很多人直接拿Echo引脚接MCU的IO,短时间不会烧,但长期运行IO内部保护二极管可能反复导通或者直接损坏引脚。Echo为高时实际输出大约5V,直接加在3.3V供电的GPIO上,这是错误的。
我的解决方法是加一个简单的电阻分压:
- Echo -> 1k电阻 -> GPIOB9
- GPIOB9 -> 2k电阻 -> GND
这样Echo高电平时,GPIOB9上分到的电压约为 5 × 2 / (1 + 2) ≈ 3.33V,刚好在安全范围内。
如果你手头没有合适的电阻,也可以用一个1N4148二极管做钳位,阳极接GPIO,阴极接3.3V,这样超过3.3V的电压会被钳住。实测两种方案都稳定。这个细节在开发板连接HC-SR04的时候特别容易被忽略,但恰恰是保证长期稳定运行的关键。
3.3 舵机云台扫描:左中右三向测距
单方向测距的避障策略很呆板:只有一直检测正前方的障碍,侧面突然冒出来的东西根本反应不过来。为了让小车有简单的"路径规划"意识,我给超声波模块装到SG90舵机上,让它可以左右转动。
SG90舵机的控制信号是50Hz的PWM周期,即20ms,其中高电平脉宽在0.5ms~2.5ms之间,对应0度到180度。
我的定时器配置是PWM频率50Hz,即20ms周期,计数溢出值为168000000 / 50 = 3360000,这里我用的是定时器3,PWM通道2。
控制舵机到不同角度的比较值设定:
// 角度转比较值粗略换算:0度 -> 0.5ms -> 0.5*168MHz / 1000 = 84000? // 等等,这里是按PWM计数即1MHz为单位算的,如果计数单位是1us,比较值就是脉宽微秒数。 // 标准写法:配置定时器分频后计数频率为1MHz,则计数周期20ms对应20000,比较值等于脉宽us // 0度:500 // 90度:1500 // 180度:2500我在工程里把定时器3配置为:内部时钟84MHz,分频84-1,得到1MHz计数,这样PWM周期为20000个计数(20ms),舵机比较值直接等于脉宽微秒数。转向函数:
void Servo_SetAngle(uint8_t angle) { uint16_t compare = 500 + (uint16_t)((float)angle / 180.0f * 2000.0f); TIM_SetCompare2(TIM3, compare); Delay_ms(300); // 等舵机转到指定位置,300ms是个人实测的稳定时间 }这里延时300ms特别重要。有人忘了延时,扫描左中右三个位置时,超声波还在测任意角度,数据毫无意义。舵机物理转动需要时间,必须等它稳定后才能触发超声波测距。
三向扫描策略是这样跑的:
- 小车先停在原地,云台分别转到左侧45度、正前方90度、右侧135度(我这里云台安装在车头中轴线,角度参考是从左到右的绝对角度)。
- 每次转动到位后延时300ms,然后连续测三次超声波,取中位值作为该方向可靠距离。
- 三次测距结果决定下一步动作。
3.4 避障决策逻辑:有限状态机设计
光有距离数据还不行,还得有决策。我的避障逻辑是用一个简单的状态机实现的,状态包括:
- 前进(S_FORWARD)
- 扫描(S_SCAN)
- 左转(S_LEFT)
- 右转(S_RIGHT)
- 后退(S_BACKWARD)
核心决策逻辑如下:
void Obstacle_Avoid_Update(void) { switch (car_state) { case S_FORWARD: // 直行中,前方距离小于30cm时进入扫描 if (front_dist < 30) { car_state = S_SCAN; Servo_SetAngle(45); // 扫描左侧 Delay_ms(300); left_dist = Ultrasonic_GetDistance(); Servo_SetAngle(135); // 扫描右侧 Delay_ms(300); right_dist = Ultrasonic_GetDistance(); // 决策 if (left_dist > right_dist && left_dist > 30) { car_state = S_LEFT; } else if (right_dist >= left_dist && right_dist > 30) { car_state = S_RIGHT; } else { car_state = S_BACKWARD; } } else { Motor_Forward(30); // 30%占空比前进 } break; case S_LEFT: Motor_LeftTurn(30); Delay_ms(400); // 固定时间左转,实际量取决于车速和轮胎间距 car_state = S_FORWARD; Servo_SetAngle(90); // 云台回中 break; // 右转、后退类似 } }你可能已经注意到,我这里的决策比较简单——左距离大于右距离就往左转,否则右转。如果左右都近(比如离墙角很近),就后退再重新扫描。
更聪明的做法是给左右转向角度加一个系数:距离远的方向优先,但距离值除以一个预设的"期望保持距离"再比较,避免"左边有障碍但距离5cm,右边没障碍但距离3cm"这种极端情况下做出错误选择。用公式表示就是:
优先度 = 距离 x 权重方向优先度高的就先转。这个思路可以很自然地扩展成动态避障,只是我们没用复杂的DWA算法而已——对F407来说,这逻辑转起来毫无压力,但已经能让小车在大多数室内场景正常工作。
3.5 实测过程中的几个意外情况
我在调试过程中遇到最多的问题是超声波数据跳变。第一次上电测试,明明正对着一堵墙,测出来的距离却在10cm到40cm之间乱跳。
排查过程:
- 先怀疑供电:把舵机停在一个固定角度,再测超声波数据,发现还是跳变。
- 再怀疑干扰:用示波器看Echo引脚波形,发现高电平宽度在有的周期里严重偏长或偏短。
- 最后发现是代码逻辑问题:我没有在连续测距之间加入适当延时,导致在超声波还没完全结束回波处理时,又开始了新一轮触发。两个周期的回波混在一起了。
解决办法很简单:每次超声波触发之间至少间隔60ms以上,而且连续测3次取中位值。中位数比平均值更能抗异常值干扰,在数据采集中是很好用的方法。
另一个坑是舵机和超声波共用一个供电轨时,舵机转动瞬间会造成电压跌落,实测超声波读数会出现短暂异常。我的应对措施是让舵机静止至少200ms后再测距,恰好也配合了舵机的稳定时间,所以最后效果还可以。
4. 红外测温模块的接入
如果你做的是智能小车,只避障显得有点单调。我加了一个红外测温模块MLX90614,把它安装在车头侧面,可以在避障的同时扫描前方物体(比如一个杯子、一个人)的辐射温度,这让小车的应用场景更像一个"巡检机器人"。
4.1 MLX90614的I2C通信和寄存器读取
MLX90614是一个非常成熟的红外温度传感器,量程-40到+125度,分辨率0.02度,通过I2C接口读取。它默认I2C地址是0x5A(7位地址模式,写地址是0xB4,读地址是0xB5)。
读取物体温度的流程:
- 向设备发送读命令,目标寄存器地址为0x07。
- 读取两个字节的16位数据(高字节在前)。
- 将16位数据乘以0.02,得到开尔文温度。
- 减去273.15,得到摄氏温度。
同理,寄存器0x01是环境温度(模块自身附近的环境温度),0x06和0x07分别是环境温度补偿和物体温度。如果只需要测物体表面温度,读0x07就够了。
核心代码如下(使用标准库I2C2,引脚PB10和PB11):
float MLX90614_ReadTemperature(uint8_t reg) { uint16_t data = 0; uint8_t buf[3] = {0}; // I2C发送寄存器地址 I2C_GenerateSTART(I2C2, ENABLE); while(!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C2, 0x5A << 1, I2C_Direction_Transmitter); while(!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); I2C_SendData(I2C2, reg); while(!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 重新发起START,读两个字节 + NACK + STOP I2C_GenerateSTART(I2C2, ENABLE); while(!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C2, 0x5A << 1, I2C_Direction_Receiver); while(!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); // 读高字节 while(!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED)); buf[0] = I2C_ReceiveData(I2C2); // 读低字节,发送NACK I2C_AcknowledgeConfig(I2C2, DISABLE); while(!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_BYTE_RECEIVED)); buf[1] = I2C_ReceiveData(I2C2); I2C_AcknowledgeConfig(I2C2, ENABLE); I2C_GenerateSTOP(I2C2, ENABLE); data = (buf[0] << 8) | buf[1]; // 开尔文转摄氏度 return ((float)data * 0.02f) - 273.15f; }4.2 数据融合和显示方案
测到温度之后,光在串口里看数字不够直观。我用一块0.96寸OLED(I2C接口)把实时温度显示在屏幕上,分辨率128x64。OLED的驱动芯片是SSD1306,用标准库的软件I2C或者硬件I2C1都可以,我这里用的是硬件I2C1(PB6、PB7),引脚不冲突。
OLED上我同时显示三行信息:
- 第一行:当前环境温度(MLX90614寄存器0x01)
- 第二行:物体表面温度(MLX90614寄存器0x07)
- 第三行:当前的避障状态(前进/左转/右转/后退)
这个显示对调试帮助非常大。你能直接看到传感器数据和决策状态是否对应,不用每次都用电脑连串口线。
4.3 温度数据的滤波和校准
MLX90614有个特点:读取到的温度波动比预想的要大,尤其是一秒只采样一两次时,最后一位数字可能乱跳。我发现连续读5次取平均,数据就稳定很多。
另外,MLX90614出厂精度大概正负0.5度,如果用于人体测温等场景,建议和标准温度计做一个偏差校准。比如用红外测温枪对比实测,发现常温下差值稳定在0.3度,就在程序里加上这个固定偏移量。若你只是展示物体温度,出厂数据已经完全够用。
一个小提醒:MLX90614的视场角(FOV)比较窄,大概30度左右,测量方向很敏感。安装时尽量让它正对着被测物体,不要在倾斜状态下取读数,否则数值会偏低。
5. 系统联调的高频问题:从"能跑"到"跑得稳"
把避障和测温两个模块放到一个系统里联调,比单独调每个模块时多了一堆莫名其妙的问题。这里我说三个高频的坑,都是我实际踩过的,属于网上教程里很少详细讲的。
5.1 I2C通信偶尔死锁:总线占用无法释放
I2C是漏极开路结构,如果通信过程中突然断线或者时序错乱,SDA线可能被从设备拉低,导致总线卡住,后面所有读写操作全部超时。我们最怕的是程序在等待事件标志时死循环。
我的处理方式是给每次I2C等待加超时判断:
uint32_t timeout = 0xFFFF; while (!I2C_CheckEvent(I2C2, I2C_EVENT_MASTER_MODE_SELECT)) { if (--timeout == 0) { // 总线异常,软件复位I2C I2C_DeInit(I2C2); I2C_Cmd(I2C2, ENABLE); return -1; } }软件复位I2C外设能解决大部分软件层面的总线卡死。如果是SDA被外部拉低,还得检查是不是I2C上拉电阻太小或者线太长导致的信号边沿问题。实测在我的设计里,I2C2加了4.7k上拉电阻,线长控制在10cm以内,很稳定。
5.2 PWM驱动电机时引来的"串扰"
L298N电机驱动模块是市场上最常见的驱动模块之一,但如果在程序里把PWM频率设置得特别低(比如几十Hz),电机运行时会听到明显的"嗡嗡"声,同时OLED屏幕和超声波模块都会出现数据闪跳。
我的问题就出在PWM频率上。早期配置定时器输出PWM时,计数频率忘记配置到位,导致实际PWM频率只有50Hz左右,电机换向电流冲击非常强烈,对供电轨和周围采样电路造成明显干扰。
后来把PWM频率调到10kHz以上,两个电机(我用的TT马达加减速器)运行声音明显变小,传感器数据稳定多了。10kHz在大多数直流电机的听感和效率之间是很好的平衡点。
提示:如果你用的是L298N,别忘了把ENA和ENB引脚都接到MCU的PWM输出上,而不是直接接高电平。只接高电平意味着电机永远全速运转,速度完全不受控制,避障转向时会非常猛。
5.3 避障和测温同时工作时,主循环的时序分配
我把系统设计成一个简单的超循环结构:
while (1) { if (flag_500ms) { // 每500ms更新一次温度显示和避障状态 Temperature_Update(); Display_Update(); flag_500ms = 0; } if (flag_50ms) { // 每50ms更新一次避障决策 front_dist = Ultrasonic_GetDistance(); Obstacle_Avoid_Update(); flag_50ms = 0; } }这样温度读取和避障逻辑不互相阻塞,两台传感器都正常工作。如果以后你想加更多的传感器或功能,建议用定时器中断或者RTOS,而不是无限延长的阻塞式delay。
6. 工程文件说明和二次开发建议
工程文件打包好了,里面除了源码和Keil工程,我还放了一份README.md,写了引脚接线表。我在这边把关键信息也列出来。
6.1 引脚分配一览
| 模块 | 引脚 | 说明 |
|---|---|---|
| 超声波Trig | PB8 | 输出触发脉冲 |
| 超声波Echo | PB9 | 输入回波(经电阻分压) |
| 舵机信号 | PB1(TIM3_CH4) | 输出50Hz PWM |
| 左电机PWM | PA3(TIM2_CH4) | 调速,10kHz |
| 右电机PWM | PA2(TIM2_CH3) | 调速,10kHz |
| L298N IN1~IN4 | PE0~PE3 | 电机方向控制 |
| MLX90614 SDA | PB11 | I2C2数据 |
| MLX90614 SCL | PB10 | I2C2时钟 |
| OLED SDA | PB7 | I2C1数据 |
| OLED SCL | PB6 | I2C1时钟 |
| 调试串口 | PA9/PA10 | USART1,115200-8-N-1 |
拿到工程后,按照表格接线,确认ST-Link连接好,编译下载,小车至少能进入正常前进状态。之后你可以根据需求改距离阈值、转向角度、PWM占空比等参数。
6.2 扩展方向
我给这个项目留了几个扩展点:
- 加上蓝牙模块:通过USART1接一个HC-05,手机端发一个字符就能切换"遥控模式"和"自动避障模式"。我用过之后觉得这项目玩法突然就丰富起来了。
- 加上电机编码器和PID调速:目前用的是开环PWM调速,转向角度是模糊的(靠固定延时),如果要精确走直线、准确转90度,需要给电机加编码器做闭环PID。F407VET6的定时器编码器模式是现成的,不占额外资源。
- 从固定阈值避障改成连续路径规划:当你有两个超声波分别装在左右前方,或者用舵机扫描更多角度时,可以构造一个简单的障碍物分布图(极坐标),然后用DWA这类动态窗口法做局部路径规划。F407的浮点运算能力应付这种量级的算法绰绰有余。
6.3 一个值得尝试的优化:转向不固定时间
我在避障决策里用的是固定延时转向(400ms),这也导致转向角度受电池电压、地面摩擦影响很大——电池满电时转向角度大,快没电时转向角度变小。一个更好的方案是用编码器累加角度,或者用陀螺仪积分转向角。手头没这些传感器时,也可以根据PWM占空比反推一个修正系数,实测效果比固定延时稍好。这个"延时变修正系数"的思路其实就是最朴素的校准思想,建议认真体会一下。
这些天调试下来,我最大的体会是:智能小车开发从"会动"到"动得稳"之间隔着的全是细节。硬件上的电源隔离、电平匹配,软件上的超时保护、数据滤波、状态机设计,每一项单独拎出来都不难,但组合到一起,才是真正在考验整体思维。
最后再分享一个小技巧:调试避障逻辑时,别老在桌面上跑,真实环境里地面摩擦、光线、障碍物材质都和桌面不一样,至少要在客厅地板上试几次,你才能明白超声波在毛毯、玻璃、金属这些不同材质面前有多不靠谱。把这些经验代进你的程序里,你的小车才算是真正能"出门遛弯"了。