简介:基于STM32F103与VL53L0X的红外测距工程包,面向嵌入式初学者、电子竞赛选手以及自动化设备开发者,用于快速掌握TOF激光测距技术在单片机上的落地实现。工程以I2C总线将STM32F103微控制器与VL53L0X激光测距传感器相连,完整涵盖底层驱动初始化、I2C读写时序封装、传感器寄存器配置、距离数据读取、滤波处理与结果输出等关键环节。压缩包共239个文件,约6.55MB,以C语言源文件(.c/.h)为主,并包含Keil工程文件(.uvproj)、编译生成的.hex与.axf、调试映射.map、日志文件.txt以及编译中间文件(.o/.d/.crf)等,目录结构完整,可直接在Keil MDK中打开工程并编译烧录。已有1548人学习下载。读者可借此熟悉STM32的I2C外设编程、VL53L0X驱动移植与精度调优技巧,同时积累传感器异常排查与数据校准方法,适用于智能小车避障、机器人导航、无人机定高、物联网近距离物位检测等场景。 说起来挺有意思,这个项目的名字里有“红外”两个字,但 VL53L0X 其实不是传统意义上那种红外对管测距,而是用 940nm 红外激光做飞行时间测距的传感器。我最早接触这个模块时也被名字误导过,以为跟普通红外避障模块差不多,结果测下来精度和稳定性完全不是一个级别。这篇文章就把我用 STM32F103 驱动 VL53L0X 做测距的完整过程写下来,包括硬件接线、驱动移植、低功耗处理、常见坑点,以及一些实测数据,希望对正在做同类项目的朋友有帮助。
这个方案很适合几类人:想给毕设或竞赛作品加一个高精度近距离测距模块的、做智能小车或机器人搞避障的、还有纯粹想折腾一下 STM32 I2C 外设和传感器驱动的。VL53L0X 测距范围标称最大 2 米,实测在室内 1.2 米内很可靠,精度能到毫米级,响应速度也快,比超声波和普通红外好太多。
1. 项目整体思路与方案选型
1.1 VL53L0X 到底是什么,和普通红外测距有哪些区别
传统红外测距传感器,比如那种三引脚的 TCRT5000 或者红外发射管加接收管的组合,原理是红外光打到物体表面后反射回来,接收管根据反射光强度判断距离。这个东西在深色物体、强光环境和远距离场景下衰减特别厉害,测出来的数据基本只能做“有没有东西靠近”这种定性判断,想拿来量精确距离基本不现实。
VL53L0X 完全不同。它里面有垂直腔面发射激光器,发射的是 940nm 波长的红外激光,然后通过单光子雪崩二极管接收反射光,用飞行时间法(Time of Flight,ToF)直接算出光子从发射到接收的耗时,从而得到距离。因为测的是光飞行时间而不是光强,所以不受物体颜色影响,黑色物体也能测,精度普遍能做到正负 3% 以内。模块内部直接集成算法,MCU 只需要通过 I2C 接口发送指令和读取结果即可,不用自己处理复杂的数学过程。
这个传感器由 ST 官方提供完整的 API 驱动代码,从初始化到校准再到测距模式切换都有现成函数。你需要做的核心工作其实只有三个:配好 STM32 的 I2C、把官方 API 移植到自己的工程里、根据场景选择合适的测距模式。
1.2 为什么用 STM32F103 而不是其他主控
VL53L0X 对外接口就一个 I2C,理论上任何带 I2C 的 MCU 都能驱动,树莓派、ESP32、Arduino 也都能玩。但我个人还是推荐先用 STM32F103,理由有三点。
第一,STM32F103 的资料和例程极其丰富。不管你是用标准外设库还是 CubeMX 生成 HAL 库工程,网上都能找到大量现成驱动,踩坑成本低。第二,它的硬件 I2C 工作在 400kHz 快速模式下能满足 VL53L0X 的通信需求,不需要用 GPIO 模拟时序,稳定性好。第三,F103 的价格已经低到几块钱一片,最小系统板也便宜,拿来做个低功耗测距设备成本完全可以接受。
选型时还有个细节值得注意:如果你做的是手持设备或者电池供电设备,F103 本身就支持停止模式,配合 VL53L0X 的单次测量模式,可以把平均功耗压得非常低。这一点我在第 4 节会详细展开。如果只追求拿 Arduino 库调一调就出数据,那 F103 的潜力其实没被充分发挥出来。
1.3 模块选购:纯测距板和二合一板的区别
TB 上搜 VL53L0X 会看到两类模块。一类是单独的测距模块,很小,上面就只有 VL53L0X 芯片、LDO、几个电阻电容,引出 VCC、GND、SCL、SDA 和 XSHUT。另一类是把 0.96 寸 OLED 屏幕和 VL53L0X 做到一块 PCB 上的二合一模块,插上电屏幕就能直接显示距离,看起来确实方便。
这里要提醒一句:如果项目最终需要接到自己的 STM32 主板上,我建议买纯测距模块。二合一模块虽然也可以只接测距部分使用,但板载 OLED 初始化和背光会额外占用电流,而且有些二合一板为了省元件,I2C 上拉电阻位置可能不在你预期的地方,排查起来平添麻烦。纯测距模块板载 LDO 后供电范围也更宽,接 3.3V 或 5V 都能用(注意电平逻辑,下文会讲)。另外买的时候看清楚 XSHUT 引脚是否引出来,这个引脚在切换 I2C 地址时会用到。
2. STM32F103 侧的关键配置与硬件接线
2.1 最小系统搭建与供电注意事项
STM32F103 最小系统板最常见的是 “C8T6” 核心板,蓝板或黑板都有,板载一个 AMS1117 把 USB 5V 转 3.3V,还带一个 8MHz 外部晶振。如果你是自己画板子,只焊芯片的话,记得必须有 8MHz 晶振加两个 20pF 负载电容、BOOT0 下拉到 GND、复位电路、3.3V 供电。如果想省掉外部晶振,也可以让 F103 跑在内部 HSI 8MHz 上再倍频,这是可以工作的,但 I2C 时序精度会略差,内部 RC 振荡器精度常温下约 ±1%,对 400kHz I2C 来说通常能过,但如果你追求 100% 稳定出数据,还是配外部晶振更省心。
供电上有个坑必须说:VL53L0X 内部工作电压是 2.8V 左右,模块上一般焊了一个 LDO 把外部输入降到 2.8V,所以模块的 VCC 接 3.3V 或 5V 都能用。但 I2C 的电平是由模块 VCC 决定的,如果你给模块供 5V,那 SCL/SDA 也是 5V 电平,直接接 3.3V 的 STM32 就可能损坏 MCU 引脚。所以我建议统一用 3.3V 给 VL53L0X 供电,STM32F103 的 GPIO 也工作在 3.3V,两边直接互连。
顺带一提,F103 的数据手册里标有“FT”标识的引脚是 5V 容忍的,GPIO 输入确实能承受 5V,但这不代表你可以放心拿 5V 电平接 FT 引脚长期跑,更不意味着 VL53L0X 能承受 5V。别冒险,老老实实 3.3V 供电。
2.2 I2C 引脚和上拉电阻:最容易被忽略的坑
VL53L0X 的 I2C 接口,默认 7 位地址是 0x29。STM32F103 的硬件 I2C1 对应引脚是 PB6(SCL)和 PB7(SDA),I2C2 对应 PB10 和 PB11。用 CubeMX 或标准库配置时,GPIO 要设置为开漏输出模式,外部接上拉电阻到 3.3V,上拉阻值常用 4.7kΩ。如果模块板载已经有上拉电阻,你就不需要再外接,直接连就行。
这里有个很常见的翻车场景:模块板载上拉同时你还在主板上又加了 4.7kΩ 上拉,两个上拉并联后阻值减半,I2C 信号边沿会变缓,高频率通信时可能出现时序违规导致的随机死机。所以我建议,先用万用表测一下模块的 SCL 和 SDA 到 VCC 之间有没有 4.7k 或 10k 电阻,如果模块上有上拉,MCU 侧不要重复加。
信号线长度也要控制。VL53L0X 的 I2C 速率最高支持 400kHz,在面包板上用杜邦线短距离连接没问题,但如果你把传感器放在机械臂末端,线拖了 20cm 以上,建议把速率降到 100kHz,同时用双绞线或者屏蔽线走 I2C,必要时在模块端加个 100pF 电容对地滤波。实测下来,线长超过 30cm 时 400kHz 通信偶发卡死,降到 100kHz 就恢复正常。
2.3 XSHUT 引脚的两种用法
XSHUT 引脚在模块上通常有标注,作用相当于芯片的复位与使能脚,拉低后 VL53L0X 彻底进入待机。这个引脚有两大用途。
第一个用途:切换 I2C 地址。VL53L0X 默认地址 0x29,如果你要在一条 I2C 总线上挂多个 VL53L0X,就必须让每个模块用不同的地址。方法是:先拉低模块 A 的 XSHUT 让 A 待机,只让模块 B 保持工作,然后通过 I2C 向 B 发送地址更改指令,把 B 改成 0x30 之类的其他地址;再释放 A 的 XSHUT。核心点就是同一时刻总线上只能有一个模块是活跃状态,否则两个 0x29 会打架。
第二个用途:低功耗控制。XSHUT 拉低后 VL53L0X 功耗能降到微安级,这比单纯切换测距模式更省。用 STM32F103 的一个 GPIO 接 XSHUT,平时拉低,需要测距时拉高并等模块启动完成,测完再拉低,搭配 MCU 自身的停止模式,可以把整个系统的休眠功耗压得特别低。
3. 驱动移植与核心代码实现
3.1 驱动代码从哪来、怎么组织
VL53L0X 的官方驱动叫 VL53L0X API,在 ST 官网搜索就能找到,或者从 GitHub 上找 ST 官方仓库拉取。拿到压缩包后不需要把所有文件都用上,核心文件就这几个:
- vl53l0x_api.c / vl53l0x_api.h:对外的主接口,初始化、校准、测距模式切换、数据读取都在这里
- vl53l0x_api_strings.c:可选,提供状态码转字符串,调试时好用
- vl53l0x_platform.c / vl53l0x_platform.h:需要你重写的平台层,里面是 I2C 读写函数
平台层是关键。VL53L0X_WriteMulti、VL53L0X_ReadMulti、VL53L0X_WriteByte、VL53L0X_ReadByte 这几个函数在各平台下实现不同,你只需要把你的 I2C 发送和接收填进去就行。
3.2 初始化流程:校准不要跳
VL53L0X 的初始化不是只调一个函数就完事的,标准流程大致是:
VL53L0X_Dev_t dev; VL53L0X_Error status; dev.I2cDevAddr = 0x29; dev.comms_speed_khz = 400; status = VL53L0X_DataInit(&dev); status = VL53L0X_StaticInit(&dev); status = VL53L0X_PerformRefSpadManagement(&dev, &refSpadCount, &isApertureSpads); status = VL53L0X_PerformRefCalibration(&dev); status = VL53L0X_SetDeviceMode(&dev, VL53L0X_DEVICEMODE_SINGLE_RANGING);其中 PerformRefSpadManagement 和 PerformRefCalibration 是校准流程,分别做 SPAD 基准校准和参考距离校准。我发现有些人图省事把这两个函数跳过了,直接 SetDeviceMode 然后开始测,短距离也能出数据,但量程和精度会打折扣,特别是近距离 50mm 以内容易出现“读数偏大”或“计算超时”的问题。
注意一点:每次上电后都跑一遍校准是可以的,但校准过程需要一段时间,会让整个初始化变慢。如果你对精度要求不是极致,也可以在第一次校准时把校准结果保存到 MCU 内部 Flash,下次上电直接跳过校准加载备份值。这个思路和常见的“F103 掉电保存数据”做法是一样的,把校准值写入 Flash 最后一页,掉电不丢。
3.3 单次测距和连续测距:该怎么选
官方 API 支持很多种测距模式,项目里最常用的就两种:单次测距和连续测距。
单次测距模式:调用一次 VL53L0X_PerformSingleRangingMeasurement(&dev, &RangingData) 完成一次完整测量,测量完成后传感器返回结果。这个模式适合低功耗场景,测一次睡一次。
连续测距模式:先调用 VL53L0X_StartMeasurement(&dev),然后传感器会按一定的间隔持续测量。你需要在循环里不断查询数据是否就绪,就绪后调用 VL53L0X_GetRangingMeasurementData(&dev, &RangingData) 取结果。这个模式测量频率高,适合对实时性要求高的场景,但功耗也偏高。
还有一点经验:VL53L0X 的测量时间预算(timing budget)直接影响精度和功耗。预算越大,单次采样时间越长,信噪比越好;预算越短,刷新率越高但距离跳动大。默认的 33ms 预算在环境光不太强的室内够用。我在户外强光下测试,短预算时读数偶尔会跳到 8190mm,这个值是无效数据标记,遇到就代表当次测量失败,需要软件过滤。
3.4 数据读取与单位换算:别把毫米当厘米
VL53L0X 返回的距离数据在 RangingData.RangeMilliMeter 字段中,单位是毫米,拿到后直接就是毫米值,不需要再乘或除。这里没啥复杂换算,但很多新手会在这犯迷糊,测出来明明 200,显示出来却成了 200 厘米,明显是把单位理解错了。
读取前要检查 RangingData.RangeStatus 状态值。0 表示测量结果有效,非 0 值表示测量异常或超量程。别直接拿 RangeMilliMeter 字段去用,一定要先看状态。一个典型的处理逻辑是:
if (RangingData.RangeStatus == 0) { distance_mm = RangingData.RangeMilliMeter; } else { distance_mm = 0xFFFF; // 或标记为无效 }另外,如果你想做稍微复杂一点的应用,比如把多组测量值做滑动平均滤波,可以把 5 到 10 次有效测量的结果存数组求平均。实测下来,滤波后数据平滑度提升明显,10ms 单次测量连续采 10 次也就 100ms 左右,对小车的避障应用影响不大。
4. 低功耗场景下的组合玩法
4.1 从“不停测量”到“按需测量”
如果你的项目只是拿开发板连上模块,读个距离显示在串口上,那功耗怎么折腾都无所谓。但一旦要做成电池供电的小设备,比如智能感应垃圾桶、门磁感应器、便携测距仪,就必须考虑“按需测量”的思路。
VL53L0X 在连续测距模式下功耗能到 19mA 左右,而单次测距模式测完一次就自动回到待机,单次测量电流相对低很多。再把 XSHUT 拉低,模块功耗能进一步降到微安级。这个组合就是:系统默认插着电睡大觉,需要量距离时给传感器上电,测完继续睡。
4.2 STM32F103 停止模式与唤醒配置
STM32F103 的停止模式(Stop mode)能把内核时钟全部停掉,只保留部分外设时钟和备份域,电流在典型几 mA,但配合各种细节能优化到更低。进入停止模式的代码,标准外设库写法是:
PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);进入前需要先把 I2C 外设关闭或重新配置,因为停止模式唤醒后,外设状态可能已丢失。唤醒方式常用外部中断 EXTI,比如用一个 GPIO 接按键或传感器中断引脚,配置为下降沿触发。
结合 VL53L0X 的低功耗流程图大致是:
- 系统初始化,完成 VL53L0X 的初始化和校准
- 拉低 XSHUT,让 VL53L0X 进入待机
- 配置 EXTI,进入停止模式
- 外部事件触发唤醒,拉高 XSHUT,延时 50ms 等模块上电稳定
- 执行单次测距,读数据
- 再次拉低 XSHUT,进入停止模式
这里有个实际坑:VL53L0X 从 XSHUT 拉高到可以接受 I2C 命令,需要一定时间。我之前把延时写成 10ms,结果初始化偶尔失败,后来把延时改到 50ms 之后就没再出现过。
4.3 实测功耗数据与内部晶振的影响
我用一个额定容量 400mAh 的锂聚合物电池做了个简单测试。纯 F103 连续跑主频 72MHz 时电流约 25mA,VL53L0X 连续测距时系统总电流约 45mA。改成“停止模式 + XSHUT 待机 + 单次测距”的按需模式后,系统静态电流降到 mA 级别以下,平均电流取决于测距频率,一分钟测一次的话平均电流只有几百微安。这个水平对电池供电完全可接受。
如果在 F103 上跑内部 HSI 晶振,停止模式唤醒后需要等待 HSI 稳定,这会额外多花几十微秒时间。在低功耗设计里,这是个可以忽略的细节,但如果你同时用 FreeRTOS 管理系统任务,要注意唤醒后先重新初始化时钟和外设,再恢复任务调度,否则调度器跑在不稳定时钟上容易出现随机延时。这也是我在项目中实测后总结出来的:低功耗和 RTOS 结合时,外设恢复和时钟恢复的顺序要设计好。
5. 常见问题与排查实录
5.1 从实际调试中整理的问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| I2C 探测不到 0x29 地址 | 上拉电阻缺失、接线错误、模块 XSHUT 被拉低 | 用万用表量 XSHUT 电平;检查模块电源;给 XSHUT 接 3.3V |
| 数据一直返回 8190mm 左右 | 超出量程、目标物太远、强光干扰 | 缩短测试距离;调大测量时间预算;在模块加遮光罩 |
| 近距离读数偏大 | 校准未执行或环境变化 | 重新执行 PerformRefSpadManagement 和 PerformRefCalibration |
| 偶尔 I2C 卡死 | 线太长、速率过高、上拉阻值不对 | 降速到 100kHz;缩短杜邦线;去掉重复上拉 |
| 上电第一次读数异常 | VL53L0X 上电启动未完成 | 在 DataInit 前延时 200ms 以上 |
| OLED 和 VL53L0X 共用总线冲突 | 地址冲突或上拉不一致 | 确认 OLED 地址(常规 0x3C);分别单独测试 |
5.2 几个非常容易踩的细节坑
第一个坑:模块的 GPIO1 中断引脚。VL53L0X 模组上一般会引出 GPIO1,这个脚可以配置成测量完成中断输出。想用中断方式知道“测量完成了”会省掉轮询,有些人接错引脚或者配置错了极性,导致一直收不到中断。个人建议第一步不用这个引脚,直接轮询寄存器判断数据就绪,跑通了再优化成中断。
第二个坑:标准库和 HAL 库的代码差异。如果你从网上拷贝 VL53L0X 驱动,通常会看到 I2C 写函数里的 HAL_I2C_Mem_Write 调用。如果工程是标准外设库,就得改成 I2C_SendData 或 I2C_SendData7bit 那套接口。别直接复制粘贴,先搞清楚自己的工程用的是哪种库。比如你现在搜“stm32 在传统的库函数中,如何让 stm32f103 工作在内部晶振模式”这种问题时,说明你还在标准库环境下,那把官方 API 里的 HAL 调用都替换成标准库调用是第一步。
第三个坑:5V 系统和 3.3V 传感器的混接。前面讲过 F103 某些引脚是 5V 容忍的,但 VL53L0X 不是。我曾经见过有人在 5V 单片机上直接接 VL53L0X,结果是刚开始能工作,几次之后传感器彻底烧了。如果你项目主控是 5V 的,一定要做电平转换,最简单的方案是加一个 I2C 电平转换模块,或者用两个 MOS 管搭一个双向电平转换电路。
第四个坑:环境光干扰。VL53L0X 虽然比传统红外强很多,但在阳光直射、强红外光环境下依然会受影响。如果你的设备要做户外使用,最好在传感器上面加一个稍微遮光的结构件,或者用软件把测距频率调高后再做滑动平均。
5.3 关于内部晶振和 I2C 时序的一个实测体会
网上经常有人问 F103 能不能用内部晶振跑 I2C 和 VL53L0X。我专门做过实验:用默认 HSI 8MHz 倍频到 64MHz,I2C 速率配 400kHz,连续跑 1000 次读取,没有出现通信错误;但把速率配 400kHz 且线长加长到 30cm 后,偶发错误率上升到了 0.2% 左右。如果你也是类似环境,建议 I2C 速率保守一点,或者直接用软件模拟 I2C。软件模拟 I2C 在这种低速传感器场景下反而更皮实,只是占用 CPU 资源多一点。
写在最后的实际操作心得
这个项目做下来,我最大的体会是:VL53L0X 本身的高精度是硬件给的,但能不能稳定地把这个精度用起来,完全取决于你对 I2C 通信细节和系统功耗的把控。很多新手拿到模块,第一件事就是找个例程直接下载跑,出数据了就认为完事了,但后面真正做产品、做竞赛作品时,往往就卡在校准精度、低功耗唤醒、批量一致性这些环节上。建议把官方 API 的初始化流程完整走一遍,别急着跳步骤;PCB 或面包板接线时把电源、地、I2C 两根线分开走;每换一个模块,先跑一次校准。最后再分享一个小技巧:调试阶段把 VL53L0X 的 RangeStatus 和原始 RangeMilliMeter 一起通过串口打出来,这样你能实时看到数据是否有效,很多诡异问题一眼就能定位。
本文还有配套的精品资源,点击获取