1. 项目缘起与整体设计思路
嵌入式测距这个方向,说新不新,说旧也不旧。超声波、红外、激光,各有各的适用场景。但这两年有个明显趋势:越来越多的项目开始用VL53LXX 系列的激光测距模块替代传统的超声波方案。原因很直接——超声波受温度、湿度影响大,测距锥角宽,遇到软性材质还容易丢波;而 VL53LXX 这类基于ToF(飞行时间)原理的传感器,体积小、精度高、响应快,I2C 接口直接读数,对 MCU 的资源占用也低。
我这次做的项目,核心就是STM32 + VL53LXX的测距功能实现。主控选的是 STM32F103C8T6,也就是大家常说的“蓝板”或最小系统板,传感器用的是 VL53L0X 和 VL53L1X 两个型号做对比测试。整个项目目标很明确:在 STM32 上稳定读取 VL53LXX 的距离数据,并通过串口输出,同时预留 OLED 显示和阈值报警的扩展接口。
为什么选这个组合?先说 STM32F103C8T6,72MHz 主频、64KB Flash、20KB RAM,对于跑 VL53LXX 的驱动来说绰绰有余。更重要的是,它的 I2C 外设成熟,社区资料多,踩坑成本低。VL53LXX 这边,VL53L0X 最远能测 2 米左右,VL53L1X 可以到 4 米,具体取决于模式和环境光条件。两者都是 ST 自家出的,官方提供了 API 库,移植起来不算复杂,但细节坑不少。
这个项目适合谁?如果你正在做机器人避障、液位检测、手势识别、智能小车这类需要非接触测距的场景,或者你单纯想入门 ToF 传感器,那这套方案可以直接参考。下面我会从硬件选型、I2C 通信配置、官方 API 移植、数据滤波、常见问题排查几个维度,把整个实现过程拆开讲清楚。
2. 核心细节解析与实操要点
2.1 VL53LXX 的测距原理与关键参数
VL53LXX 的核心是SPAD 阵列(单光子雪崩二极管)和VCSEL 激光发射器。它发射 940nm 的红外激光,遇到物体反射回来,SPAD 阵列接收光子,通过计算光子飞行时间得出距离。这个过程由传感器内部的MCU完成,主控只需要通过 I2C 读取结果寄存器就行。
这里有个关键点:VL53L0X 和 VL53L1X 的寄存器映射和 API 完全不同。VL53L0X 用的是 ST 的 VL53L0X API,VL53L1X 用的是 VL53L1X API,两者不能混用。我一开始以为都是 VL53LXX 系列,驱动应该差不多,结果直接把 L0X 的代码往 L1X 上套,读出来的数据全是乱的。后来查手册才发现,L1X 的测距模式更多,有短距、中距、长距三种,默认模式下的最大测距范围也不一样。
| 参数 | VL53L0X | VL53L1X |
|---|---|---|
| 最大测距 | 约 2m | 约 4m |
| 测距模式 | 单模式 | 短/中/长距可切换 |
| I2C 地址 | 0x52 (7位) | 0x52 (7位) |
| 供电电压 | 2.8V | 2.8V |
| 通信电平 | 2.8V/3.3V | 2.8V/3.3V |
注意供电电压这一栏。VL53LXX 的核心电压是 2.8V,但很多模块板载了 LDO 和电平转换,所以可以直接接 3.3V 或 5V。如果你买的是裸芯片,那必须给 2.8V,否则 I2C 电平不匹配,通信直接失败。我建议新手直接买带电平转换的模块,省事。
2.2 STM32 的 I2C 配置要点
STM32F103C8T6 有两个 I2C 外设,I2C1 在 PB6/PB7,I2C2 在 PB10/PB11。我用的是 I2C1,配置成标准模式 100kHz或者快速模式 400kHz都可以。VL53LXX 支持最高 400kHz,但实际跑下来,100kHz 更稳,尤其是在杜邦线较长的情况下。
配置 I2C 的时候有几个参数容易搞错:
- 时钟频率:STM32F103 的 I2C 时钟来自 APB1,默认 36MHz。在 CubeMX 里配置时,要确保 I2C 的时钟频率计算正确,否则实际通信速率会偏。
- 上拉电阻:I2C 总线必须加上拉电阻,一般 4.7kΩ 到 10kΩ。很多模块板载了上拉,但如果你接多个 I2C 设备,上拉电阻要重新算。我试过接两个 VL53L0X,地址冲突,后来用 XSHUT 引脚分时使能才解决。
- GPIO 模式:I2C 引脚要配置成开漏输出,复用功能开漏。这个在 CubeMX 里选 I2C 外设后会自动配好,但如果你手动写寄存器,别忘了。
提示:如果你用软件 I2C,时序要严格按照 VL53LXX 的手册来,尤其是启动阶段的初始化序列,延时不够会导致传感器不响应。
2.3 官方 API 的移植与裁剪
ST 官方给 VL53L0X 和 VL53L1X 都提供了 API 库,代码量不小。VL53L0X 的 API 大概有十几个文件,VL53L1X 更多。直接全塞进 STM32F103C8T6 的 64KB Flash 里,空间会有点紧张,但勉强够用。我的做法是只保留核心文件,把用不到的平台相关代码裁掉。
以 VL53L0X 为例,核心文件包括:
vl53l0x_api.c/vl53l0x_api.h:核心 APIvl53l0x_platform.c/vl53l0x_platform.h:平台相关,需要自己实现 I2C 读写vl53l0x_i2c_platform.c:I2C 底层,可以替换成自己的 HAL 库函数
移植的关键是实现VL53L0X_WriteMulti和VL53L0X_ReadMulti这两个函数。官方给的模板是基于 STM32 标准库的,我把它改成了 HAL 库版本。具体来说,就是调用HAL_I2C_Mem_Write和HAL_I2C_Mem_Read,注意寄存器地址是 8 位的,但 VL53LXX 的寄存器地址是 16 位的,所以要分两个字节发送。
// VL53L0X 写多字节 VL53L0X_Error VL53L0X_WriteMulti(uint8_t dev, uint8_t index, uint8_t *pdata, uint32_t count) { return HAL_I2C_Mem_Write(&hi2c1, dev, index, I2C_MEMADD_SIZE_8BIT, pdata, count, 100); } // VL53L0X 读多字节 VL53L0X_Error VL53L0X_ReadMulti(uint8_t dev, uint8_t index, uint8_t *pdata, uint32_t count) { return HAL_I2C_Mem_Read(&hi2c1, dev, index, I2C_MEMADD_SIZE_8BIT, pdata, count, 100); }这里有个坑:VL53L0X 的寄存器地址是 8 位的,但 VL53L1X 是 16 位的。如果你用 L1X,I2C_MEMADD_SIZE要改成I2C_MEMADD_SIZE_16BIT,而且地址要拆成高字节和低字节。我一开始没注意,L1X 死活读不出数据,后来用逻辑分析仪抓波形才发现地址发错了。
3. 实操过程与核心环节实现
3.1 硬件连接与供电检查
先列一下我用的物料清单:
- STM32F103C8T6 最小系统板 x1
- VL53L0X 模块 x1
- VL53L1X 模块 x1
- USB-TTL 串口模块 x1
- 杜邦线若干
- 0.96 寸 OLED(I2C)x1(可选)
接线很简单:
| STM32 引脚 | VL53LXX 引脚 | 说明 |
|---|---|---|
| PB6 | SCL | I2C 时钟 |
| PB7 | SDA | I2C 数据 |
| 3.3V | VIN | 供电 |
| GND | GND | 共地 |
| PA9 | TX | 串口输出 |
| PA10 | RX | 串口输入 |
接好之后,先别急着上电。用万用表量一下 VL53LXX 模块的 VIN 和 GND 之间有没有短路,确认无误再上电。我有个朋友就是没检查,模块电源引脚焊反了,一上电芯片直接冒烟。
上电后,先跑一个 I2C 扫描程序,确认 STM32 能识别到 VL53LXX 的地址。VL53L0X 和 VL53L1X 的默认 7 位地址都是0x29,也就是 8 位写地址 0x52,读地址 0x53。如果你扫描不到,先检查上拉电阻和接线。
3.2 VL53L0X 初始化与单次测距
初始化流程大致如下:
- 调用
VL53L0X_DataInit初始化数据结构 - 调用
VL53L0X_StaticInit加载默认配置 - 调用
VL53L0X_PerformRefCalibration做参考校准 - 调用
VL53L0X_PerformRefSpadManagement管理 SPAD - 设置测距模式(单次或连续)
- 调用
VL53L0X_StartMeasurement开始测距 - 轮询
VL53L0X_GetMeasurementDataReady直到数据就绪 - 调用
VL53L0X_GetRangingMeasurementData读取距离
这个过程看起来步骤多,但实际代码写下来也就几十行。关键是每一步都要检查返回值,如果某一步返回错误,后面就不用跑了。我见过有人不检查返回值,结果传感器没初始化成功,读出来的距离一直是 0,还以为是传感器坏了。
VL53L0X_Dev_t dev; VL53L0X_RangingMeasurementData_t rangingData; uint16_t distance; dev.I2cDevAddr = 0x52; dev.comms_type = 1; if (VL53L0X_DataInit(&dev) != VL53L0X_ERROR_NONE) { printf("DataInit failed\r\n"); return; } if (VL53L0X_StaticInit(&dev) != VL53L0X_ERROR_NONE) { printf("StaticInit failed\r\n"); return; } // 设置单次测距模式 VL53L0X_SetMeasurementTimingBudgetMicroSeconds(&dev, 33000); while (1) { VL53L0X_StartMeasurement(&dev); do { VL53L0X_GetMeasurementDataReady(&dev, &isReady); } while (!isReady); VL53L0X_GetRangingMeasurementData(&dev, &rangingData); distance = rangingData.RangeMilliMeter; printf("Distance: %d mm\r\n", distance); HAL_Delay(100); }这段代码里,MeasurementTimingBudgetMicroSeconds设的是 33ms,这是 VL53L0X 的默认值。如果你设得太小,测距精度会下降;设得太大,响应速度变慢。实测下来,33ms 是个比较平衡的值。
3.3 VL53L1X 的差异与适配
VL53L1X 的 API 和 L0X 不一样,初始化流程更复杂,但功能也更强。它支持短距、中距、长距三种模式,通过VL53L1_SetDistanceMode切换。短距模式最远 1.3m,中距 3m,长距 4m。但长距模式下,环境光的影响会明显增大,室内用中距就够了。
VL53L1X 的初始化代码大概长这样:
VL53L1_Dev_t devL1; VL53L1_RangingMeasurementData_t rangingDataL1; devL1.I2cDevAddr = 0x52; devL1.comms_type = 1; VL53L1_DataInit(&devL1); VL53L1_StaticInit(&devL1); VL53L1_SetDistanceMode(&devL1, VL53L1_DISTANCEMODE_MEDIUM); VL53L1_SetMeasurementTimingBudgetMicroSeconds(&devL1, 50000); while (1) { VL53L1_StartMeasurement(&devL1); do { VL53L1_GetMeasurementDataReady(&devL1, &isReady); } while (!isReady); VL53L1_GetRangingMeasurementData(&devL1, &rangingDataL1); printf("L1X Distance: %d mm\r\n", rangingDataL1.RangeMilliMeter); HAL_Delay(100); }注意SetMeasurementTimingBudgetMicroSeconds在 L1X 里设的是 50ms,比 L0X 长。这是因为 L1X 的测距算法更复杂,需要更多时间。如果你设得太短,数据会不稳定。
3.4 数据滤波与串口输出
原始数据跳变是常态,尤其是测远距离或者表面反射率低的时候。我在串口输出前加了一个滑动平均滤波,窗口大小取 5。效果很明显,数据曲线平滑了很多。
#define FILTER_WINDOW 5 uint16_t filterBuf[FILTER_WINDOW] = {0}; uint8_t filterIndex = 0; uint16_t filterDistance(uint16_t newVal) { filterBuf[filterIndex] = newVal; filterIndex = (filterIndex + 1) % FILTER_WINDOW; uint32_t sum = 0; for (int i = 0; i < FILTER_WINDOW; i++) { sum += filterBuf[i]; } return sum / FILTER_WINDOW; }串口输出我用的是printf重定向,波特率 115200。如果你用 Keil,需要在fputc里调用HAL_UART_Transmit。这个网上教程很多,不展开。
4. 常见问题与排查技巧实录
4.1 I2C 通信失败排查
I2C 通信失败是最常见的问题,表现是初始化卡死或者读出的数据全是 0xFF。排查思路如下:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 扫描不到地址 | 接线错误、上拉电阻缺失 | 检查 SDA/SCL 是否接反,补上 4.7kΩ 上拉 |
| 初始化卡死 | 传感器供电不足 | 测量 VIN 电压,确保 3.3V 稳定 |
| 读数据全 0xFF | I2C 地址错误 | 确认 7 位地址是 0x29,8 位写地址 0x52 |
| 数据偶尔跳变 | 杜邦线过长、干扰 | 缩短线长,加屏蔽,降低 I2C 速率到 100kHz |
我遇到过一次,VL53L0X 初始化一直失败,后来发现是模块的 XSHUT 引脚悬空。VL53LXX 的 XSHUT 是复位引脚,低电平复位,高电平正常工作。有些模块板载了上拉,有些没有。如果你的模块没有上拉,必须手动接 3.3V,否则传感器一直处于复位状态。
4.2 测距数据不准的几种情况
测距不准的原因很多,我整理了几种典型场景:
- 目标物体太近:VL53LXX 有最小测距距离,L0X 是 3cm,L1X 是 4cm。低于这个距离,数据会跳变或者报错。
- 目标物体反射率低:黑色物体、海绵、布料,反射率低,测距范围会缩短。官方手册里有个反射率对照表,深色物体的测距能力大概只有白色物体的 60%。
- 环境光太强:阳光直射下,红外背景噪声大,测距精度下降。室内日光灯影响不大,但如果是靠近窗户的中午,数据会飘。
- 玻璃或镜面:激光穿过玻璃或者被镜面反射到其他方向,测距结果会偏大或者直接报错。
注意:如果你的应用场景里有玻璃,建议加一个遮光罩,或者改用超声波方案。ToF 传感器对透明物体的测距能力很有限。
4.3 多传感器地址冲突的解决
如果你要接多个 VL53LXX,默认地址都是 0x29,肯定冲突。解决办法是利用XSHUT 引脚分时使能:
- 所有传感器的 XSHUT 接 MCU 的 GPIO
- 上电时,所有 XSHUT 拉低,传感器全部复位
- 逐个拉高 XSHUT,每拉高一个,就通过 I2C 修改它的地址
- 修改完地址后,再拉高下一个
这样每个传感器都有独立地址,可以挂在同一条 I2C 总线上。我试过接 3 个 VL53L0X,用这个方法成功分配了 0x30、0x31、0x32 三个地址。
4.4 中断模式与轮询模式的取舍
VL53LXX 支持中断输出,测距完成时 GPIO1 引脚会拉低。用中断模式可以省去轮询,降低 MCU 占用。但中断模式配置起来麻烦一点,需要设置中断阈值和模式。如果你的项目对实时性要求不高,轮询模式完全够用。我实测下来,轮询模式下 STM32F103 的 CPU 占用率大概在 15% 左右,跑其他任务完全没问题。
5. 扩展应用与个人经验
5.1 从测距到避障的简单实现
测距数据拿到之后,最简单的应用就是阈值报警。比如设一个 200mm 的阈值,距离小于 200mm 就点亮 LED 或者蜂鸣器。再进一步,可以做成扫描式避障:用舵机带动 VL53LXX 左右旋转,扫描前方 180 度,把距离数据映射成极坐标图,就能得到一个简易的“雷达”。
我做过一个版本,舵机每 10 度停一次,测距一次,通过串口把角度和距离发给上位机,用 Python 画了个实时极坐标图。效果还不错,虽然精度比不上激光雷达,但成本低了很多。
5.2 低功耗场景的注意事项
如果你的项目是电池供电,VL53LXX 的功耗要关注一下。L0X 在连续测距模式下大概 20mW,L1X 稍高。如果不需要连续测距,可以改成单次测距 + 休眠的模式,测完就进 STOP 模式,需要的时候再唤醒。STM32F103 的 STOP 模式电流大概 20uA,加上传感器的待机电流,整体可以做到很低。
不过要注意,VL53LXX 从待机唤醒后,需要重新初始化,不能直接读数据。所以如果你的应用是间歇性测距,每次唤醒都要跑一遍初始化流程,时间大概 10ms 左右。
5.3 我踩过的几个坑
第一个坑是I2C 地址搞错。VL53L0X 的 7 位地址是 0x29,但 HAL 库的HAL_I2C_Mem_Write需要的是 8 位地址,也就是 0x52。我一开始传了 0x29,结果一直 NACK。后来看手册才发现,HAL 库会自动把 7 位地址左移一位,所以如果你传 0x29,实际发出去的是 0x52,但读的时候又不一样。最稳妥的办法是直接传 0x52,然后I2C_MEMADD_SIZE_8BIT。
第二个坑是VL53L1X 的寄存器地址是 16 位的。我一开始用 L0X 的代码改,地址只发了 8 位,结果读出来的数据全是乱的。后来改成I2C_MEMADD_SIZE_16BIT,并且把地址拆成高字节和低字节,才正常。
第三个坑是电源噪声。我用 USB 供电的时候,数据很稳;换成电池供电,数据就开始跳。后来在 VL53LXX 的 VIN 和 GND 之间并了一个 10uF 的电容,问题解决。所以如果你发现数据不稳定,先检查电源,加个滤波电容试试。
5.4 后续可以扩展的方向
这套测距方案做出来之后,可以往几个方向扩展。一个是多传感器融合,比如 VL53LXX 加 IMU,做室内定位;另一个是数据上云,通过 ESP8266 或者 4G 模块把距离数据传到服务器,做远程监控。如果你做的是智能小车,可以把测距数据接到 PID 控制器里,做自动跟随或者避障。
我个人觉得,VL53LXX 最大的优势是体积小、接口简单、精度够用。如果你之前一直用超声波,不妨试试这个方案,代码移植一次,后面换项目直接复用就行。唯一要注意的是,ToF 传感器对环境和目标材质比较敏感,实际部署前一定要在真实场景里多测几组数据,别只看手册上的理想参数。