手头有块STM32F407VET6最小系统板,一直吃灰的话,我强烈建议你把它拿来跑一辆“避障+测温”双功能智能小车。这个项目不复杂,电路简单、逻辑清晰,代码量也不算大,却能一次性摸到单片机开发里最常见的几根主线:GPIO控制、定时器PWM输出、外部信号采集、串口/OLED显示,还有工程崩溃的无数种姿势。这篇文章我会从硬件选型和接线讲起,把避障逻辑、DS18B20测温、Keil工程搭建和调试过程遇到的各种坑全部过一遍,关键代码放在工程文件里,你手头有F407板子就能一比一复现。
1. 项目全景:F407避障测温小车到底在做什么
1.1 这个项目解决了什么问题
做智能小车这个方向,很多人一上来就用51单片机或者Arduino。51性能实在太老,跑个超声波避障还行,一旦想加舵机扫描、多路传感器、OLED刷新,CPU就开始真正受罪。Arduino胜在例程多,写起来快,但它帮你把底层全包了,很难学到芯片层面的初始化逻辑和时序控制。
这套F407方案要解决的问题很直接:用一块性能对得起“进阶”二字的M4核心板,自己动手配置GPIO、定时器、外部中断和单总线时序,做出一台能自动避开障碍、顺带实时测温并上报的小车。整个过程不依赖任何模块化积木式框架,从底层寄存器到应用逻辑,全部自己动手,最终你会得到一个能直接运行到底的Keil工程,而不是一堆丢失依赖的碎片代码。
1.2 为什么选STM32F407VET6而不是F103或Arduino
STM32F407VET6是一颗基于Cortex-M4F内核的芯片,最高主频168MHz,带硬件浮点运算单元,Flash有512KB,RAM有192KB,放在智能小车这种应用里属于妥妥的“大马拉小车”。选它不是单纯为了炫配置,而是给后续扩展留余地。比如你今天只做避障和测温,明天想加摄像头做色彩追踪,或者装个OpenMV、做个视觉循迹,F4仍然顶得住。
F103当然也能做这些,但它72MHz的主频和M3内核让实时性天花板低了不少。最典型的差别是同时启用多路定时器、ADC和外部中断时,代码响应速度和处理余量一眼就能拉开。另外DS18B20这类单总线传感器对时序要求苛刻,F4在高频下做微秒级延时比F103更从容,长期跑也不容易出现时序漂移。
Arduino就更不用说了,它能让你三分钟点亮OLED,但也会让你永远不知道GPIO复用配置到底在哪里出错。用Keil自己搭工程的感觉,就像做饭的时候自己切菜备料,虽然慢一点,但哪味菜是干辣椒哪味是豆瓣酱,心里一清二楚。
1.3 整体功能设计和运行流程
这辆小车同时干两件事:
- 一边通过超声波传感器探测前方障碍,配合舵机旋转探头,向左、前、右三个方向扫描,做出转向决策,实现自动避障。
- 一边读取DS18B20温度传感器,每隔一段时间把采集到的温度数据通过串口发送到上位机,同时刷新到OLED小屏上。
整个运行流程可以拆成三层看:
- 传感器层:负责把物理世界的距离和温度转换成电信号。
- 决策层:根据距离值和当前小车状态,决定直行、左转、右转还是原地等待。
- 执行层:通过PWM控制左右两个直流电机转速和方向,让小车把决策变成实际动作。
三层之间靠一个简单的状态机串起来,比传统“一直盲跑、碰到墙才转弯”的做法稳定得多,也方便后续扩展。
2. 硬件选型与接线细节
2.1 需要准备的硬件清单
| 模块 | 型号/规格 | 作用 | 备注 |
|---|---|---|---|
| 主控板 | STM32F407VET6最小系统板 | 核心控制 | 推荐带ST-Link接口的版本 |
| 电机驱动 | TB6612FNG或L298N | 驱动左右直流电机 | TB6612体积小,功耗低 |
| 直流电机 | 四驱或两驱小车底盘 | 运动执行 | 常见N20或者TT马达 |
| 超声波模块 | HC-SR04 | 前方测距 | 5V供电,注意回波电平 |
| 舵机 | SG90 | 旋转超声波探头 | 控制信号接PWM输出 |
| 温度传感器 | DS18B20 | 环境/目标温度检测 | 单总线协议,防水头版本更好用 |
| 显示模块 | OLED 0.96寸 I2C | 显示温度和状态 | 非必须,但调试时非常爽 |
| 调试工具 | ST-Link V2或串口模块 | 烧录和调试 | 建议直接用ST-Link |
电源部分我单独强调一下:不建议用同一个电源给电机和单片机供电。电机启动瞬间电流冲击很大,很容易把单片机拉复位。我通常用两套供电,主控用USB口或AMS1117稳压出来的5V/3.3V,电机单独用18650电池组或4节AA电池供电,然后把两个电源的地接在一起。这个共地操作千万不要忘记,否则PWM信号没有参考地,电机转起来会疯。
2.2 引脚分配与接线对照表
我用的是轴承很常见的“TIM3输出PWM给电机,TIM2输出PWM给舵机”方案,串口1接调试信息,I2C1接OLED,GPIOB普通IO读超声波和DS18B20。下面这张表是实测能跑的接线方式,你可以直接抄:
| 功能 | 模块引脚 | STM32F407VET6引脚 | 说明 |
|---|---|---|---|
| 左电机PWM | TB6612 PWMA | PA6 / TIM3_CH1 | 通道1输出,调速 |
| 右电机PWM | TB6612 PWMB | PA7 / TIM3_CH2 | 通道2输出,调速 |
| 左电机方向1 | TB6612 AIN1 | PB0 | GPIO输出 |
| 左电机方向2 | TB6612 AIN2 | PB1 | GPIO输出 |
| 右电机方向1 | TB6612 BIN1 | PB2 | GPIO输出 |
| 右电机方向2 | TB6612 BIN2 | PB3 | GPIO输出 |
| 超声波Trig | HC-SR04 Trig | PB4 | 输出触发信号 |
| 超声波Echo | HC-SR04 Echo | PB5 | 输入回波,建议分压 |
| 舵机信号 | SG90 PWM | PA1 / TIM2_CH2 | 20ms周期,1~2ms高电平 |
| DS18B20 | DQ | PB6 | 开漏模式或切换输入输出 |
| OLED SCL | OLED SCL | PB8 | I2C1时钟 |
| OLED SDA | OLED SDA | PB9 | I2C1数据 |
注意几个细节:
- HC-SR04模块的回波引脚输出的是5V电平,STM32F407虽然是5V容忍引脚,但保险起见,最好在Echo引脚串一个1k电阻,或者用两个电阻分压到3.3V。我实测直接接虽然也能工作,但偶尔会出现相邻引脚被干扰触发读数的现象,串电阻之后一次也没出问题。
- DS18B20的数据线要接一个4.7k上拉电阻到3.3V,如果模块上已经自带,可以不加。
- SG90舵机必须接外部电源正极,不能直接从小系统板的3.3V取电,否则舵机堵转瞬间能把板子拉熄火。
2.3 电源方案里的隐藏大坑
这个坑我在第一次调试时踩得很惨。当时用一个5V 2A的适配器同时给32和电机驱动供电,看起来功率够,但电机启动瞬间压降能掉到3V以下,单片机直接复位,小车刚起步就“咔哒”一声跟死机一样。后来把电源分成两路、共地之后才稳定下来。
如果用的是L298N驱动板,它通常自带一个5V输出,可以给主控板供电,但前提是驱动板的输入电压高于7V。L298N自身压降大,如果拿两节锂电池串成7.4V,喂给L298N再输出5V给主控还行。但要明白这个5V经过稳压芯片后电流能力有限,如果你再接舵机和多个传感器,建议另外加一个DC-DC降压模块给舵机单独供5V。
3. 避障与测温原理和代码拆解
3.1 HC-SR04超声波测距的时序原理
HC-SR04测距靠的是声波在空气中的往返时间。先由主控给Trig引脚一个10us以上的高电平脉冲,模块内部发射8个40kHz的超声波脉冲,然后Echo引脚输出一个高电平,它的持续时间就是声波从发射到碰到障碍物再返回的总时间。距离公式就是小学物理的“路程=速度乘时间再除以2”:
距离(cm) = 高电平时间(us) / 58
为什么是58而不是除以340再换算?因为声速在空气中约340m/s,即0.034cm/us。声波走一个往返,实际距离是“0.034厘米每微秒 × 时间(us) / 2”,简化之后就是时间除以58。这个常数在普通室温下误差很小,20℃时声速344m/s,换算成常数接近58.5,除以58算出的距离会有一点点偏短,但做避障完全够用。
实现的时候有一个关键点:测量Echo高电平时间时,千万不要用delay死等。正确做法是用定时器输入捕获或者简单的GPIO轮询加微秒计时。我工程里用的是定时器计时方案:在Echo变高的瞬间读取定时器计数值,等Echo变低再读一次,两者相减就是持续时间。
3.2 DS18B20单总线测温时序要点
DS18B20用的是单总线协议,一根数据线既做供电又要传数据,所以在初始化之后,每次读写都需要精确控制时隙。它在序列上分三步:复位脉冲、存在脉冲、ROM命令、功能命令。
复位时序:主机把总线拉低至少480us,然后释放,DS18B20会等待15~60us,然后把总线拉低60~240us表示存在响应。主机在这个时候切换成输入模式,读取这个响应。
写时序:主机要写0时,拉低总线60~120us;要写1时,拉低总线1~15us然后释放。写时序之间必须间隔至少1us。
读时序:主机拉低总线1us以上,然后释放,在释放后15us内读取总线电平。读0时传感器会把总线拉低,读1时总线保持高电平。
这些时序对延时函数的精度要求比较高,在F407的168MHz主频下,我直接用SysTick做的微秒级延时,比for循环空转可靠得多。还有一个很多人忽略的点:如果工程开了中断,在读取DS18B20过程中最好关闭对应中断,否则一个中断插入就可能导致时序超出规范,读回来的数据全变成0x85。
3.3 避障逻辑状态机
避障逻辑不要写成一坨if else嵌套,后期改起来非常痛苦。我用一个简单的状态机,五个状态来回切换:
typedef enum { CAR_FORWARD, CAR_BACKOFF, CAR_SCAN_LEFT, CAR_SCAN_RIGHT, CAR_TURN_LEFT, CAR_TURN_RIGHT } CarState;正常行进时,小车处于CAR_FORWARD状态,定时每50ms读一次超声波距离。如果前方距离小于25cm,就进入CAR_BACKOFF,先后退一小段,防止小车直接顶到障碍物上。然后舵机转到左边测一次,再转到右边测一次,根据左右两边的距离大小决定转向:哪边距离更大就转向哪边。转向完成后回到CAR_FORWARD。
转向时间怎么定?我采用的是“固定时长转向”方式,即给左电机和右电机设置相反方向的PWM,延时400ms,幅度90度左右。更高级的做法是加编码器轮子,做闭环角度控制,但那要额外硬件,入门阶段先用固定时长跑通逻辑。
3.4 核心代码关键片段
Ultraschall测量:
uint32_t distance_cm; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM1) { if (capture_index == 0) { rising_time = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); capture_index = 1; } else if (capture_index == 1) { falling_time = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); capture_index = 0; duration = falling_time - rising_time; distance_cm = duration / 58; } } }当然如果你不用输入捕获,也可以用轮询方式,每次读取Echo引脚电平,配合DWT计数器计算时间差,代码要短一些。
DS18B20读取温度核心:
float ds18b20_read_temp(void) { uint8_t low, high; uint16_t raw; float temp; ds18b20_reset(); ds18b20_write_byte(0xCC); // 跳过ROM ds18b20_write_byte(0x44); // 启动温度转换 HAL_Delay(750); // 12位精度转换时间上限 ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); // 读取暂存器 low = ds18b20_read_byte(); high = ds18b20_read_byte(); raw = (high << 8) | low; temp = (float)raw * 0.0625f; // 12位精度LSB为0.0625℃ return temp; }注意DS18B20是LSB在前,低位字节是低8位,高位字节是符号位和高位数据。0xFFF0代表-1℃,不要算反了。
舵机角度控制:
void servo_set_angle(uint8_t angle) { uint32_t pulse = 500 + (uint32_t)angle * 1000 / 180; __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, pulse); }SG90的标准PWM周期是20ms,高电平0.5ms对应0度,2.5ms对应180度。F407的TIM2工作在1MHz计数频率下,ARR设为19999得到20ms周期,比较值从500到2500微秒之间变化就是对应角度。
4. Keil工程搭建、编译与下载
4.1 标准外设库还是HAL库
这个选择一直有争议。我个人的建议是:如果你第一次接触F4,用标准外设库先跑通一个完整项目,比一上来就上HAL库要好理解得多。HAL库封装层次高,调API很省事,但一出问题你就会看到一长串Callback和Handle搞得人头晕。标准外设库虽然代码繁琐,但GPIO配置就是GPIO_InitStructure、RCC就是RCC_AHB1PeriphClockCmd,每一行都看得懂。
不过现在Keil工程新建时,MDK默认会把F4系列支持包从Pack Installer下载,你需要在Pack里选择“Keil.STM32F4xx_DFP”版本,然后指定F407VET6芯片。工程文件里预处理宏一定要加两个:USE_STDPERIPH_DRIVER和STM32F40_41xx。少写一个,编译直接报错让你找不到头文件。
4.2 从零新建Keil工程的步骤
- 新建一个工程文件夹,里面分好App、Drivers、User、Doc等子目录。
- 打开Keil MDK,Project -> New uVision Project,选择STM32F407VET6。
- 拷贝标准外设库源码到Drivers目录,把stm32f4xx_rcc.c、stm32f4xx_gpio.c、stm32f4xx_tim.c、stm32f4xx_usart.c这些你真正用到的外设源文件加入工程。
- 在Options for Target里的C/C++选项卡,把Include Paths添加好,路径里不要用中文。
- 预处理宏写上USE_STDPERIPH_DRIVER,STM32F40_41xx。
- 配置调试器为ST-Link Debugger或者J-Link,Flash Download勾选Reset and Run。
- 编译,把生成的.hex通过ST-Link下载到板子上。
4.3 烧录方式与工程配置注意点
大多数F407VET6最小系统板都板载ST-Link接口,用四根线就能烧录:SWDIO、SWCLK、GND、3.3V。也有板子没有ST-Link,但至少有CH340串口芯片,那就要用串口ISP下载。串口下载在F4上比F1麻烦一点,因为F4没有内置自举引导程序,通常需要把BOOT0拉高,按一下复位,再用FlyMcu或者STM32CubeProgrammer烧录。烧完后把BOOT0拉回低电平,重新上电才能运行。这个操作我在新手期至少忘记了十次。
Keil工程配置里还有几个细节:
- Flash算法要选“STM32F4xx Flash”,512K的型号不用改Size。
- 如果用的外接8MHz晶振,且工程里没有正确设置HSE_VALUE,串口波特率会明显偏离,烧进去后OLED或者串口输出乱码。建议在stm32f4xx.h里明确#define HSE_VALUE 8000000,或者在SystemInit前确认启动文件里定义的时钟频率和实际晶振一致。
- 开启FPU:在Target页面把Floating Point选成“Single Precision”,否则浮点运算会走软件AEE调用,DS18B20温度算起来会慢很多。
5. 调试中遇到的问题速查与实战经验
5.1 常见问题清单
我把做这个项目过程中踩过以及群里同学反复问过的问题整理成了一张速查表:
| 现象 | 直接原因 | 处理方式 |
|---|---|---|
| OLED不亮或显示乱码 | I2C地址错误/接线错 | 用I2C扫描程序读地址,确认是0x3C还是0x3D |
| 电机嗡嗡响但不转 | PWM频率太低/占空比不够 | 把PWM频率提高到10kHz以上,初始占空比给30% |
| 小车跑偏 | 两个电机转速不一致 | 在代码里加一个修正系数,左电机和右电机乘不同比例 |
| 超声波一直显示0 | Echo高电平没读到/接线松 | 用示波器或万用表量Echo,看是否真有脉冲 |
| DS18B20读回85℃ | 芯片没初始化成功/时序被打断 | 检查上拉电阻,关闭外部中断再读 |
| 烧录时报Flash Download失败 | 没有选对Flash算法/板子没复位 | 在Flash Download勾选“Reset and Run”,重新上电烧录 |
| 串口打印全是乱码 | 波特率不匹配或晶振频率不对 | 核对串口助手波特率,核对HSE_VALUE定义 |
| 舵机一直抖动 | 供电电流不够/信号线干扰 | 舵机独立供电,信号线尽量远离电机线 |
5.2 排查逻辑和工具使用方法
遇到问题别慌,先看现象是“完全不动”还是“动作不对”,这决定了排查方向。如果小车完全不动,先用万用表量板子供电电压,再看ST-Link能不能连接到芯片,最后查GPIO输出状态。如果动作不对,比如速度慢或者转向乱串,大概率是PWM配置或者传感器数据异常。
我在调试时最喜欢用功能是Keil的调试器模式。在Debug模式下,你可以直接打开Watch窗口,输入全局变量名,比如distance_cm或者temp_value,然后单步执行或者打断点看数值。F407断点打多了会慢,但看几个关键变量完全没问题。结构体变量也能看,在Watch窗口输入结构体名后点开前面的箭头就能展开成员,不用再printf整个结构体。
如果要查程序卡在哪个死循环,可以全速运行然后点Halt,停在哪个函数里一眼就能看到。这是排查逻辑死锁最有效的办法。如果程序运行了一阵子突然抽风,把延时调大、把中断优先级调高,通常能快速缩小范围。
5.3 几个减少无效工作的好习惯
第一,代码里多留串口日志,用串口助手盯着关键变量。我每一次加新功能,都会先打印“当前状态:FORWARD,距离:20cm,温度:25.3℃”这样的信息,而不是等小车撞到墙了再去猜原因。
第二,接线前先画一张引脚分配表,在代码里用宏定义把引脚集中管理。不要在硬件里用PA6,又在另一个模块里把PA6偷偷初始化成别的功能,到时候排查到你怀疑人生。
第三,硬件改动前先断电,电容放完电再接。F407板子上的滤波电容不小,有时候拔掉电源后还带着电压,你直接拔传感器容易把引脚烧掉。我烧过一块板子的PB5,就是因为带电插拔Ultrasonic的Echo线,教训很深刻。
6. 后续扩展:这块F407小车还能继续加什么
跑通避障和测温之后,这台小车的基础能力已经到位了。如果你觉得不过瘾,我建议按下面几条线继续扩展。
第一条线是加通信模块。装一个蓝牙模块HC-05或者ESP8266,把温度数据实时发到手机,你就能远程监控小车周围环境温度,再配一个红外测温模块MLX90614,就能做成非接触测温巡检小车。有人拿这种小车去测机房机柜温度,效果比人拿测温枪逐台扫要快很多。
第二条线是改闭环控制。给电机换一个带编码器的版本,用TIM的编码器模式读取轮子转速,做个简单的PID调速,你会发现小车走直线稳了很多。这不只对小车有用,以后你做任何运动控制项目,PID都是一道绕不过去的坎。
第三条线是加路径规划。现在的基础避障是“遇到障碍再躲”,属于反应式。你可以给小车加一块TCS34725颜色传感器,识别地面上的色带,把避障和循迹结合起来,做一个能送快递模型的“智能搬运小车”。再往后可以用DWA动态窗口法做局部路径规划,这就上升到机器人算法层面了,F407跑起来也不吃力。
我在实际调试中最强烈的感受是:不要一开始追求完美,先把“能跑”这个目标达成,再回头改代码和硬件。这个项目看起来简单,真跑起来的时候你会遇到电机卡死、传感器误报、供电不稳等各种妖魔鬼怪。但恰恰是这些问题,才让一块51倍率的STM32F407学得值。等你把这台小车调得稳稳当当,再用Keil去碰别的F4项目,你会发现自己已经很自然地在按这套流程走:先看原理图,再配GPIO,然后写逻辑,最后调稳定,整个进阶过程就自然发生了。