STM32+VL53L1X单点激光测距工程:I2C驱动到料位检测

发布时间:2026/10/26 17:06:54
STM32+VL53L1X单点激光测距工程:I2C驱动到料位检测 简介STM32_VL53.zip 是一份面向嵌入式初学者与 STM32 开发者的激光测距实践工程围绕 STM32F103VET6 主控与 VL53L1X ToF 传感器展开重点解决 I2C 接线、传感器初始化、轮询读取距离及异常处理等入门问题可用于室内导航、物体检测与自动对焦等场景的原型验证。压缩包共 417 个文件约 19.83MB包含 C 源码、头文件、编译中间文件、工程配置与可执行输出等其中 c/h 文件便于阅读驱动与业务逻辑o/crf/d 及 axf/hex 等反映编译构建与烧录结果uvprojx 工程可直接导入开发环境。已有 2719 人学习下载说明其作为参考工程具有一定认可度。读者可据此梳理 PA2/PA3 的 SDA/SCL 连接、PA4 的 XShut 控制思路理解单次与连续测距配置、数据读取流程及通信错误排查方法并借助完整工程结构快速复现非接触式测距实验为后续传感器融合与低功耗设计积累可复用的调试经验。1. 一个料位检测场景引入的单点测距工程去年帮一个做小料仓的团队改测距方案要求把探头装在仓顶量程 40 厘米到 1.5 米环境有粉尘。原来的超声波探头被粉尘糊住以后回波明显漂移换成了 VL53L1X 单点激光模组主控还是最普通的 STM32F103。这个压缩包里是那套裸机工程的收敛版I2C 驱动、初始化、单次和连续测距、串口打印全部在一个可以直接编译的 Keil 工程里。它不涉及复杂的算法也不依赖 RTOS就是把「STM32 VL53L1X 激光测距」这一条链路完整打通。适合正在做接近检测、液面/料位、简易避障的嵌入式开发者如果你手上刚好一块 F1 或者 F4 的板子加一个 ToF 模组拿过去改几个宏定义就能跑。2. 技术选型与工程骨架I2C 单点测距最小闭环2.1 硬件连接VDDIO、XSHUT 和上拉电阻一个都不能省VL53L1X 是 16 位寄存器地址、数据大端的 I2C 从机7 位地址默认 0x29HAL 库里要写成 0x52左移一位。模组本身有两路供电一路是内部激光和模拟部分的 AVDD另一路是 IO 侧的 VDDIO。大部分成品小板自带 LDO只引出一组 3.3V 和一个 VDDIO 跳线如果你买的是裸模组VDDIO 必须和主控的 IO 电平一致不能拿 5V 直接灌。接线上有四根是必须的SCL、SDA、GND、VDDIO。另外两根强烈建议接上XSHUT 接一个普通推挽 GPIOGPIO1 接一个带外部上拉的输入。XSHUT 是硬复位脚拉低超过 1 微秒器件就掉电重启多模组组网时全靠它做地址分配GPIO1 是中断输出默认配置下测距完成会拉低用它可以把轮询从主循环里拿掉。I2C 上拉别省。400kHz 下 4.7k 是常见值但如果你的排线超过 20 厘米建议降到 2.2k同时把速率降到 100kHz 试试。我遇到过 SCL 上的波形在上升沿有明显的台阶逻辑分析仪上看是典型的上升时间不够换小阻值上拉后立刻正常——这种问题在示波器上看一眼就能定位但在代码里会表现为随机的 HAL_BUSY非常像玄学。2.2 三层切分ULD 内核、平台适配、应用逻辑工程按三层来分这是官方 ULDUltra Lite Driver驱动包本来的结构也是我建议你保留的结构最底下是平台层VL53L1_platform.c只做一件事把抽象的「读/写 16 位寄存器地址」翻译成 HAL 的HAL_I2C_Mem_Read/Write。这一层是唯一跟具体芯片型号、具体 I2C 外设绑定的一层换 MCU 只改这里。中间是驱动内核包括vl53l1x_api.c和一大堆vl53l1x_*.c。这里面封装了VL53L1X_SensorInit、VL53L1X_StartRanging、VL53L1X_GetDistance这些接口寄存器地址、时序延时的最小值、参数配置表的解析全在这里。这一层不要动除非你有明确理由。最上面是应用层一个app_range.c加上main.c里的主循环。它负责什么时候开始测、多久读一次、读到的值怎么滤波、串口怎么上报。业务逻辑全部收在这里将来从料位改成避障只改这一层。分层的价值在多模组时候特别明显驱动内核是被多个实例共用的只有平台层需要额外接受一个「当前操作哪个地址」的参数。如果没有分层两个模组就意味着两套几乎一样的驱动文件后面维护起来十分麻烦。2.3 目录结构与文件职责解压后大致是下面这个结构Keil 工程文件在根目录路径职责是否需要改Core/Src/main.c外设初始化、主循环、串口上报需要业务入口Core/Src/i2c.cI2C1 初始化400kHz按板子改引脚Core/Src/gpio.cXSHUT、GPIO1、串口引脚按板子改引脚Drivers/VL53L1X/vl53l1_platform.c寄存器读写适配 HAL基本不改Drivers/VL53L1X/vl53l1x_api.c对外的初始化/测距接口不改Drivers/VL53L1X/vl53l1x_core.c内部寄存器配置与计算不改Drivers/VL53L1X/vl53l1x_platform.h平台层函数声明不改App/app_range.c测距流程封装、状态判定需要主要改这里真正需要动手的基本只有三处main.c的引脚和外设、app_range.c的测距策略、i2c.c的速率。其余文件当成黑盒用就行除非你要换成别的 ToF 型号。这也是我判断一个驱动工程好不好用的标准——需要改的文件越少说明它的边界切得越干净。3. 初始化链路从上电复位到读出 0xEA3.1 XSHUT 时序与 boot 等待上电顺序错了后面所有代码都是白写。VL53L1X 内部有一小块固件XSHUT 释放以后它需要时间从内部 ROM 搬固件、初始化模拟前端这期间它不响应 I2C。如果主控在这时候就去读寄存器会稳定收到 NACK很多人第一反应是地址错了或者上拉不对其实是时序没等够。正确的做法是XSHUT 先拉低保持至少 1 微秒实际工程里给 10 毫秒更稳妥然后释放等大约 1.2 毫秒固件启动完成最后不要盲等直接去轮询 ID 寄存器。/* bsp_vl53l1x.c —— 硬复位时序 */ #include bsp_vl53l1x.h #include main.h void VL53LX_HardReset(void) { /* 拉低 XSHUT器件掉电I2C 从机失效 */ HAL_GPIO_WritePin(VL53_XSHUT_GPIO_Port, VL53_XSHUT_Pin, GPIO_PIN_RESET); HAL_Delay(10); /* 手册最小值 1us留足余量 */ /* 释放复位内部固件开始从 ROM 启动 */ HAL_GPIO_WritePin(VL53_XSHUT_GPIO_Port, VL53_XSHUT_Pin, GPIO_PIN_SET); HAL_Delay(2); /* 启动约需 1.2ms这里给 2ms */ } /* 轮询 0x010F 直到读到 0xEA说明固件已经跑起来了 */ uint8_t VL53LX_WaitBoot(uint32_t timeout_ms) { uint8_t model_id 0; uint32_t tick HAL_GetTick(); while ((HAL_GetTick() - tick) timeout_ms) { if (VL53L1_RdByte(VL53L1_I2C_ADDR, 0x010F, model_id) 0) { if (model_id 0xEA) { /* 0xEA 是 VL53L1X 的固定型号 ID */ return 0; } } HAL_Delay(1); } return 1; /* 超时通常是供电或 I2C 硬件问题 */ }这段代码有两个细节值得单独说。HAL_Delay(2)是保守值官方示例里给的是 1.2 毫秒级别的等待但不同批次的模组冷启动时间有差异多等 1 毫秒不会有人抱怨少等就可能偶发失败。超时用HAL_GetTick()而不是循环计数这样即使主频改了你也不用重新调参。返回值为 1 的时候不要去猜直接量波形SCL 有没有时钟、器件有没有 ACK、VDDIO 有没有真的到 3.3V。3.2 SensorInit 展开软复位、参数装载与中断清理硬复位之后还要走一遍软初始化。VL53L1X_SensorInit()内部做的事比看起来多写 0x0000 寄存器触发软复位等待固件状态位重新确认型号 ID把一整张默认配置表按顺序刷进寄存器设置默认的 timing budget 和 distance mode配置 GPIO1 为中断输出最后清理一次中断标志让第一次测距从一个干净的状态开始。刷配置表那一步耗时最长因为里面有些寄存器之间对写入顺序有要求中间还夹着若干十毫秒级的等待。整个SensorInit在 400kHz 下大概要几十毫秒所以不要在中断里调用它也别在主循环的实时任务中间插进去。/* app_range.c —— 初始化封装把返回值拆成阶段码方便定位 */ uint8_t Range_Init(void) { VL53LX_HardReset(); /* 1. 硬复位 等启动 */ if (VL53LX_WaitBoot(100) ! 0) return 0x11; if (VL53L1X_SensorInit(VL53L1_I2C_ADDR) ! 0) return 0x12; /* 2. 软初始化 */ /* 3. 测距模式1 长距2 短距以模组实际表现选 */ if (VL53L1X_SetDistanceMode(VL53L1_I2C_ADDR, 2) ! 0) return 0x13; /* 4. 时间预算 100ms抗噪与刷新率的折中 */ if (VL53L1X_SetTimingBudgetInMs(VL53L1_I2C_ADDR, 100) ! 0) return 0x14; /* 5. 两个参数必须成对写顺序不能反 */ if (VL53L1X_SetInterMeasurementInMs(VL53L1_I2C_ADDR, 100) ! 0) return 0x15; /* 6. ROI 收到 8x8视场角变窄减少边框反射干扰 */ if (VL53L1X_SetROI(VL53L1_I2C_ADDR, 8, 8) ! 0) return 0x16; return 0; }这里每个失败分支返回不同的码是我踩过坑之后养成的习惯。早期图省事统一返回 1结果现场出问题是「初始化失败」只能靠猜是哪一步。分成阶段码之后串口打印一个0x14就知道是 timing budget 没写进去直接跳到对应寄存器查省掉大半天。还要注意VL53L1X_SetTimingBudgetInMs和VL53L1X_SetInterMeasurementInMs的依赖关系timing budget 不能大于 inter-measurement否则模组会返回非法参数。顺序上先设 inter-measurement 再设 timing budget或者两者设成相等都不容易出问题。3.3 三个参数决定精度Timing Budget、Distance Mode、ROI调这三个参数基本等同于调整个测距系统的性格。它们之间互相牵制不能各自单独设参数取值范围调大之后调小之后Timing Budget20/33/50/100/200/500 ms信噪比升量程和稳定性上升刷新率下降刷新率上升噪声明显变大Distance Mode1 长距 / 2 短距量程更远近距离容易截断抗环境光更好量程缩短ROI4x4 到 16x16视场角大容易打到边框产生串扰定位更准对安装对准要求高经验区间是这样的长距模式配 100ms 时间预算白墙大概能到 3 米50ms 时噪声肉眼可见静止目标的读数会在 ±15 毫米里抖20ms 基本只能当「有没有东西」用。短距模式在 100ms 以内就一直很稳但量程被压到 1.3 米左右适合近距离高刷新率的场景。ROI 这个参数最容易被忽略。默认 16x16 是整个视场角全开如果你的模组装在一个开孔的壳子里孔边沿会把激光反射回来形成固定的近距离串扰表现为不管前面有没有东西都报一个几十毫米的值。把 ROI 缩到 8x8 或者 4x4相当于把视场角收窄边框就被排除在外面了。我一般先把 ROI 收到 8x8 再调其他参数因为串扰是系统性偏差靠滤波滤不掉。4. 一次测距的完整链路从 StartRanging 到毫米数4.1 触发与就绪判定连续测距模式下VL53L1X_StartRanging只是把模组启动它会按照 inter-measurement 设定的节奏自己循环测量主控要做的就是「等一帧好了没有」。判断方式有两种轮询 GPIO 状态寄存器或者等 GPIO1 的中断。轮询的写法是读 0x0031 这个寄存器的最低位为 0 表示有新数据。这个寄存器名字里带 GPIO实际反映的是中断线的电平状态。读到 0 之后再调VL53L1X_GetDistance拿结果拿完必须显式清一次中断写 0x0086否则电平不会恢复下一轮就永远等不到。/* app_range.c —— 阻塞式取一帧超时返回错误码 */ uint8_t Range_ReadOnce(uint16_t *distance_mm, uint8_t *status) { uint8_t ready 0; uint32_t tick HAL_GetTick(); /* 等中断线拉低最多等 timing_budget 50ms 的余量 */ while ((HAL_GetTick() - tick) (RANGE_BUDGET_MS 50)) { if (VL53L1X_GetMeasurementDataReady(VL53L1_I2C_ADDR, ready) ! 0) { return 0x21; /* I2C 通信异常 */ } if (ready 1) break; HAL_Delay(1); } if (ready ! 1) return 0x22; /* 超时模组没出数据 */ VL53L1X_GetDistance(VL53L1_I2C_ADDR, distance_mm); /* 单位 mm */ VL53L1X_GetRangeStatus(VL53L1_I2C_ADDR, status); /* 0 表示可信 */ /* 关键一步不清中断下一帧就再也等不到 ready */ VL53L1X_ClearInterrupt(VL53L1_I2C_ADDR); return 0; }超时余量给budget 50ms是保守估计。HAL_Delay(1)的轮询粒度意味着最多会多等 1 毫秒加上 I2C 读寄存器的开销一帧的总耗时通常在 budget 的 1.1 倍左右。如果你的主循环里还有别的活超时值要相应放宽否则会偶发0x22。4.2 读结果距离、状态、信号强度GetDistance只是把 0x0096 这个寄存器的两个字节拼成 16 位整数单位毫米。但这个值不能直接用。VL53L1X 在很多异常情况下仍然会返回一个数字没有目标时返回 8190 或 8191信号太弱时返回一个明显偏小的值相位异常时甚至会给一个看起来完全合理的距离。真正的可信度判断在 range status 上。0 表示这一帧数据可信非 0 对应信号强度不足、噪声超限、近距离截断、相位异常等情况具体的编码含义以驱动头文件里的宏定义为准不同版本的 ULD 名字略有差异。工程里我一般写一个白名单只接受 0其他全部丢弃。除了这两个还有两个值值得一起读出来0x0098 的峰值信号强度和 0x009A 的环境光强度。前者反映目标反射率白纸和黑绒布能差一个数量级后者反映背景光干扰太阳直射下会飙到很高。把这两个值打到串口上调试时基本能一眼看出问题出在目标太暗还是环境太亮。/* app_range.c —— 带质量阈值的取值返回 0 才算一帧有效数据 */ uint8_t Range_ReadValid(uint16_t *distance_mm) { uint16_t dist 0; uint8_t status 0; uint16_t peak 0; if (Range_ReadOnce(dist, status) ! 0) return 1; /* 丢弃无效帧状态非 0 或者距离是饱和值 */ if (status ! 0) return 2; if (dist 8190) return 3; VL53L1X_GetSignalRate(VL53L1_I2C_ADDR, peak); if (peak 1) return 4; /* 回波太弱多半是深色目标 */ *distance_mm dist; return 0; }这个函数返回非 0 的时候调用方要做的是「跳过这一帧继续下一帧」而不是停下来报错。ToF 测量本来就是概率性的连续 10 帧里丢 1 到 2 帧很正常靠时间上的多数表决比靠单帧的阈值可靠得多。4.3 轮询、中断与 DMA 的取舍工程默认是轮询好处是逻辑直白坏处是阻塞。100ms 的时间预算意味着主循环里有一百毫秒被占着如果同时还要刷屏、处理串口命令就会明显卡顿。改成 GPIO1 外部中断是最划算的一步。把中断配成下降沿触发中断服务函数里只置一个标志位主循环看到标志位再去读数据。这样主循环可以自由安排其他任务测距变成了后台行为。再往上是用 DMA 做 I2C 传输把读寄存器也从 CPU 手里拿走。实际收益有限因为一次读只有两三个字节I2C 的瓶颈在总线速率上而不在 CPU除非你同时挂了好几个传感器否则不建议一上来就上 DMA调试复杂度会陡增。下面这几条判断标准可以直接照用刷新率要求低于 10Hz 且主循环空闲就用轮询刷新率高于 10Hz 或者主循环有别的实时任务上中断总线挂了三颗以上从机且都是 400kHz再考虑 DMA。5. 避坑与排查读不到 ID、数据跳变、地址冲突的现场记录5.1 现象I2C 读寄存器永远返回 HAL_ERROR现象很统一仿真器单步进去HAL_I2C_Mem_Read返回HAL_ERROR或者HAL_TIMEOUT串口打印的型号 ID 一直是 0x00 或者 0xFF。换一块板子有时又好了。原因通常有三个。第一是地址写错7 位地址 0x29 在 HAL 里要左移一位变成 0x52很多人直接把 0x29 传进去HAL 会把它当成 8 位地址的低 7 位用实际总线上出现的是别的从机地址。第二是 XSHUT 悬空模组一直在复位状态从机根本不响应。第三是 VDDIO 没接或者接到了 5VIO 侧没电SDA/SCL 上没有有效电平。解决办法按顺序排查先用逻辑分析仪抓一次起始信号后的第一个字节看总线上真实出现的是 0x52 还是 0x29这一步能直接确诊地址问题然后量 XSHUT 的静态电平释放后应该是高最后量 VDDIO 和 SDA 的上拉电压是否一致。三样都对了还读不到就把 I2C 速率降到 100kHz 再试排除上升时间不足。5.2 现象能读到 0xEA但测距值恒为 8190/8191初始化全部通过型号 ID 也对一启动测距读回来就是 8190 或者 8191从来没变过。这个值在 VL53L1X 里是「没有有效回波」的饱和标记。第一个原因是没清中断。中断标志在初始化时可能已经是置位状态StartRanging之后第一次读 ready 立刻返回 1此时读到的其实是上一次残留的空结果。修正方法是在StartRanging之后、进入循环之前先无条件调一次ClearInterrupt。第二个原因是目标不在有效量程内。短距模式的有效量程只有 1.3 米左右如果你的测试距离是 2 米读回来必然是饱和值。这时候看 range status 会是「信号强度不足」相关的编码一看就能区分。第三个原因是分钟级别的低级错误模组的保护膜没撕。上面那层半透明贴膜对红外激光的衰减非常厉害撕掉之后量程能差一倍。这个坑我见过不止一次血泪经验是拿到新模组第一件事就是撕膜。5.3 现象静止目标读数在 0 和真实值之间乱跳目标完全不动串口打出来的距离却在 0 毫米和真实值之间来回跳偶尔还夹着几个明显偏小的值。最可能的原因是光学串扰。模组装在开孔外壳里孔壁或者盖板玻璃把发射光直接反射回接收端形成一条固定的短光路。这条光路的距离是固定的、很近的所以它会被算成一个接近 0 的读数和真实目标交替出现。判断方法是把模组从壳子里拆出来裸测如果跳变消失就是串扰无疑。解决办法有两个物理上加大开孔、把模组往孔里缩进一点、或者在窗口贴一层针对 940nm 的窄带滤光片软件上把 ROI 收小到 8x8 甚至 4x4让视场角避开孔壁同时把 range status 非 0 的帧全部丢弃。极端情况下还可以设置一个最小距离门限小于这个值的直接判为无效——反正是料位检测40 厘米以内的数据本来就没意义。5.4 现象两个模组挂在同一条总线上只有一个能用两个模组的 XSHUT 都直接拉到 VDD上电后两个器件同时启动地址都是 0x29读谁都是读同一个。VL53L1X 支持改地址但前提是同一时刻总线上只有一个器件响应 0x29。正确流程是把所有模组的 XSHUT 全部拉低保持住然后逐个释放释放一个就立刻把它地址改成新地址写 0x0001 寄存器改完再做下一个全部改完之后每个模组都有独立地址才能正常并发工作。/* multi_vl53.c —— 逐个释放并分配新地址 */ static const uint8_t addr_table[2] {0x2A, 0x2B}; /* 7 位新地址 */ uint8_t VL53_MultiInit(void) { /* 1. 全部按在复位态 */ HAL_GPIO_WritePin(XSHUT1_PORT, XSHUT1_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(XSHUT2_PORT, XSHUT2_PIN, GPIO_PIN_RESET); HAL_Delay(10); for (uint8_t i 0; i 2; i) { /* 2. 只放开当前这一颗 */ if (i 0) HAL_GPIO_WritePin(XSHUT1_PORT, XSHUT1_PIN, GPIO_PIN_SET); else HAL_GPIO_WritePin(XSHUT2_PORT, XSHUT2_PIN, GPIO_PIN_SET); HAL_Delay(2); /* 3. 用默认地址 0x29 访问改成表里的新地址 */ if (VL53L1X_SetI2CAddress(0x29, addr_table[i]) ! 0) return 1; /* 4. 改完立刻用新地址验证一次避免后面才发现问题 */ if (VL53L1X_SensorInit(addr_table[i]) ! 0) return 2; } return 0; }这里有个细节容易翻车SetI2CAddress写完之后器件立刻切到新地址之后的任何一次通信都必须用新地址包括紧接着的SensorInit。我第一次写这段的时候忘了改第二颗模组的初始化一直失败查了半天以为是地址分配冲突实际上是自己在用旧地址跟它说话。6. 二次开发多模组地址分配与中断化改造把单点工程改成多点核心改动其实只有两处但每一处都要验证到位。第一处是地址分配上面那段VL53_MultiInit可以直接拿去用注意addr_table里的值要避开总线上已有的从机地址比如常见的 EEPROM 是 0x50、OLED 是 0x3C别撞上。地址分配完之后给每个模组存一个句柄结构体里面放 7 位地址、对应的 XSHUT 引脚、最近一次有效距离和状态计数上层按句柄访问代码就不会乱。第二处是把轮询改成中断。两个模组的 GPIO1 分别接两个 EXTI 引脚中断服务函数里只做一件事把对应的volatile uint8_t flag置 1。主循环里扫这两个标志谁置位就读谁、清谁。这里有个坑是共用中断线的情况——如果两个引脚落在同一个 EXTI 线上服务函数里必须先判断是哪个引脚触发的否则会出现「清了一个中断另一个永远等不到」的假死。改造完怎么验证我一般分三步走。第一步静态验证目标固定在 200mm、500mm、1000mm 三个位置每个位置连测 100 帧统计有效帧率和标准差标准差超过 20mm 说明参数还没调好。第二步动态验证拿一块白板从远到近匀速移动看距离曲线是不是单调的中间有没有掉帧或者跳点。第三步干扰验证用手机手电筒直射模组窗口模拟强环境光看有效帧率掉多少掉到 50% 以下就得考虑加滤光片或者缩 RO别等到装到现场才发现白天不能用。从那以后我每次改完测距参数都会强制走一遍这三步验证尤其是第一步的 100 帧统计——它比盯着串口看十条数据靠谱得多很多偶发的跳变只有靠统计才暴露得出来。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询