VL53L1CB多区域扫描:原理、代码与避障应用 简介VL53L1CB TOF 传感器开发系列第二篇聚焦多区域扫描模式面向嵌入式测距、激光导航与物联网应用开发者讲解如何将视场划分为多个感兴趣区域ROI并独立测距。该模式通过FoV分区域方式最多支持16个区域并行测量还可按需扩展至169个区域每个区域的测距结果独立上报适合投影机梯形校正、多点距离检测、环境映射等广域监控与多目标跟踪场景。资源包采用ZIP压缩格式体积约6.91MB可配合CSDN文字教程和B站教学视频同步学习。目前已有239人学习下载。这套配套资料能帮助读者快速理解多区域扫描的ROI配置流程、区域数量扩展上限以及测量结果读取机制掌握初始化与数据上报要点减少查阅数据手册和反复实验的时间既适合刚接触ToF传感器的开发者建立整体认知也适合有经验的工程师在实际项目中快速落地多区域测距方案。1. VL53L1CB 多区域扫描模式到底改了什么单颗 VL53L1CB 本身只输出一个距离值它的 SPAD 阵列可以只开一个小窗口。很多人把它当单点测距用却忽略了“移动小窗口”这件事。把整个接收视场切成 3×3、4×4 的小方块按顺序把 ROI 移过去测量就得到了一个低成本的多区域扫描框架。这套做法在避障、手势识别和简单 TOF 雷达原型里很常见。它不改变传感器硬件也不需要额外镜头完全是寄存器配置和测距时序的组合。缺点是总帧率会除以区域数区域越多数据刷新越慢优势是器件便宜、代码量不大一周就能调通。下面按实际开发顺序展开先讲原理和时序预算再给出可用的 API 代码然后处理标定和参数问题最后聊几个能直接用的应用技巧。适合已经能把 VL53L1CB 单点测距跑起来正打算扩展探测范围的开发者。2. 多区域扫描原理ROI 划分与 VL53L1CB 的测距时序2.1 SPAD阵列与ROI为什么单点传感器能扫多区域VL53L1CB 的接收端是一个 SPAD单光子雪崩二极管阵列不是单颗感光点。驱动里用 ROIRegion of Interest来规定哪一部分 SPAD 参与当前测距。ROI 由左上和右下两个点确定坐标单位是 SPAD 数默认坐标原点在光轴中心。这个设计本来是为了让用户把测距范围缩小到某个小区域但反过来用就是多区域扫描的基础。把 ROI 的坐标写成二维循环就得到了一个 M×N 网格。每个 ROI 之间只有偏移不同尺寸可以固定。比如要扫 3×3 区域每个区域用 4×4 SPAD步长也是 4。代码先生成全部 ROI 坐标再交给驱动逐个设置。#define GRID 3 #define STEP 4 VL53L1_UserRoi_t rois[GRID * GRID]; static void build_grid(void) { int center -STEP * (GRID / 2); // 3x3时中心块左边界是-4 for (int r 0; r GRID; r) { for (int c 0; c GRID; c) { int idx r * GRID c; rois[idx].TopLeftX center c * STEP; rois[idx].TopLeftY center r * STEP; rois[idx].BotRightX rois[idx].TopLeftX STEP - 1; rois[idx].BotRightY rois[idx].TopLeftY STEP - 1; } } }center根据网格奇偶调整3×3 时中心块左上角在 (-4,-4)整个网格覆盖约 12×12 SPAD正好是传感器阵列中间的一部分。实际视场角会随 ROI 位置略微变化靠近边缘的区域有效接收面积会下降后面校准阶段要单独处理。如果STEP调小到 2区域分辨率更高但每个区域能收到的光子更少距离抖动会变大调大到 6区域之间缝隙变大容易漏测。所以STEP的设置要配合目标物尺寸。2.2 距离模式与时序预算多区域扫描的成本一次 VL53L1CB 测距由发射、积分、读出三部分构成积分时间由“时序预算”控制。API 里写的是微秒常见的短距离模式允许很短的预算适合近距离环境中长距离模式下最短预算会变长。多区域扫描的总周期约等于“单区域预算×区域数I2C 开销”因此区域数翻倍帧率接近减半。表格近似值实际受 I2C 速率和驱动实现影响区域划分单次预算(ms)理论帧率(fps)典型用途1×1 全幅2050单点高刷新3×320约15小型避障4×420约10粗略环境扫描3×38约30近距离手势如果只给 8ms 预算测 3m 外的目标回波信号会很弱RangeStatus容易非零。所以快速扫描时距离范围必须收紧别让上层算法盲目信任短预算下的远距离值。可以用脚本预估总帧率zones9 budget_ms20 overhead_ms2 total_ms$((zones * (budget_ms overhead_ms))) awk BEGIN {printf \frame_rate%.1f\\n\, 1000/$total_ms}zones是总区域数overhead_ms是 I2C 读写和状态切换的固定损耗改这两个值就能估算新方案的帧率预算。2.3 连续模式下的区域切换顺序连续模式下VL53L1CB 每隔一个测量周期自动开始下一次测距。切换 ROI 的时机如果不正确会读到上一帧的旧数据。安全顺序是数据就绪 → 读取结果 → 设置下一 ROI → 清除中断。清除中断会触发下一帧因此设置 ROI 必须在清中断之前完成。注意清除中断之后才写 ROI下一帧可能已经按旧 ROI 开始造成同一区域连续测量两次甚至数据错位。切换开销还和 I2C 总线速度有关。400kHz 下写一次 ROI 坐标约几十微秒相比数千微秒的预算可忽略但有多个寄存器要更新不能让这个写操作打断 IRQ 处理。代码里一般把 ROI 设置放在业务线程或者放在测量完成中断里、清中断标志之前执行。具体调用顺序下一章直接给代码。3. 用 VL53L1 API 把多区域扫描跑起来3.1 初始化设备和基础参数ST 官方的 VL53L1 API 是所有实现的公共入口MCU 和 Linux 驱动都基于它。先定义设备句柄、测量结果和距离数组初始化函数负责等待上电、DataInit 和 StaticInit。StaticInit 会加载出厂校准数据千万别省。#include vl53l1_api.h #include vl53l1_platform.h static VL53L1_Dev_t g_dev; static VL53L1_RangingMeasurementData_t g_data; static int32_t g_range_mm[9]; static int32_t tof_init(void) { uint8_t error; g_dev.i2c_address 0x29; // 模块地址不同开发板可能不同 if (VL53L1_WaitDeviceBooted(g_dev) ! VL53L1_ERROR_NONE) return -1; VL53L1_DataInit(g_dev); VL53L1_StaticInit(g_dev); // 短距离模式适合3米以内环境 VL53L1_SetDistanceMode(g_dev, VL53L1_DISTANCEMODE_SHORT); // 20ms预算响应速度和精度折中 VL53L1_SetTimingBudgetMicroSeconds(g_dev, 20000); build_grid(); // 生成3x3 ROI VL53L1_SetROI(g_dev, rois[0]); VL53L1_StartRanging(g_dev); // 连续模式 return 0; }VL53L1_Dev_t结构体里包含 I2C 地址和平台句柄WaitDeviceBooted会等待传感器 XDV 引脚拉高。DataInit只是把寄存器恢复默认真正加载校准参数的是StaticInit。如果StaticInit之后马上跑扫描结果可能正常但精度没有保障至少要保持这句。SetDistanceMode选择短距离模式后最大测距约 1.3m但能够支持很短的时序预算。如果你目标距离 2m 以上要改成VL53L1_DISTANCEMODE_LONG同时预算不能小于 20ms帧率自然降下来。多区域扫描核心在时间模式选择直接决定你能扫多快。3.2 扫描主循环先写 ROI 再清中断扫描一圈 9 个区域核心循环如下static uint8_t zone_status[9]; void scan_all_zones(void) { for (int i 0; i 9; i) { // 等待当前区域数据就绪 VL53L1_WaitMeasurementDataReady(g_dev); VL53L1_GetRangingMeasurementData(g_dev, g_data); zone_status[i] g_data.RangeStatus; g_range_mm[i] (g_data.RangeStatus 0) ? g_data.RangeMilliMeter : -1; // 先写下一区域再清中断下一帧会用新ROI int next (i 1) % 9; VL53L1_SetROI(g_dev, rois[next]); VL53L1_ClearInterrupt(g_dev); } }WaitMeasurementDataReady是平台层函数在 STM32 上通常读状态寄存器等待在树莓派上等 GPIO 变化。建议给它加超时保护超过 20ms 没数据就StopRanging重置硬件避免死循环。GetRangingMeasurementData会把最近一次测量的距离、状态、信号率、环境率都填到g_data里。RangeStatus 0表示有效这里把非零值映射成-1是为了不污染上层数值计算。清除中断之后传感器立即开始下一帧测量使用的 ROI 就是刚写好的rois[next]。因为 ROI 设置发生在测量完成之后、下次启动之前中间没有竞态。这也解释了为什么不推荐在清除中断后马上改 ROI。3.3 测量模式对比和 API 选择如果应用驱动不想跑连续模式可以用单次模式static void scan_single_shot(void) { for (int i 0; i 9; i) { VL53L1_StopRanging(g_dev); VL53L1_SetROI(g_dev, rois[i]); VL53L1_StartRanging(g_dev); VL53L1_WaitMeasurementDataReady(g_dev); VL53L1_GetRangingMeasurementData(g_dev, g_data); g_range_mm[i] (g_data.RangeStatus 0) ? g_data.RangeMilliMeter : -1; } }单次模式逻辑清晰每次切换都干净但StopRanging和StartRanging之间有额外初始化开销总耗时比连续模式多出几毫秒。对 3×3 扫描来说差距可能从 60ms 拉到 75ms体感比较明显。API作用多区域扫描中要注意的点VL53L1_SetROI设置当前 ROI坐标越界会报错误调参后需重新设置VL53L1_StartRanging启动连续或单次测量单次模式首帧会有启动延迟VL53L1_ClearInterrupt清除中断并让测量继续必须在下一 ROI 写完之后调用VL53L1_WaitMeasurementDataReady等待测量完成实现层必须带超时VL53L1_GetRangingMeasurementData读测量结果不要在中断里长时间处理数据3.4 实战判断 ROI 切换是否生效调试时会遇到“所有 9 个区域读到的距离完全一样”。原因有两种一是上一节说的顺序写反连续两次都用同一个 ROI二是 I2C 写 ROI 失败寄存器没有更新。最简单的验证方法让一个目标物只出现在一个区域的视场内水平移动它观察 9 个区域的距离是否按预期变化。如果目标在一侧时另一侧区域的距离不变说明 ROI 切换已经生效。这一步不需要额外仪器用串口打印距离数组就行。打印格式可以用 CSV方便导到 Excel 做热图。接下去就是标定和参数细调。4. 让 VL53L1CB 多区域扫描更可靠标定与参数调优4.1 为什么每个区域都要单独标定ROI 移到视场边缘时接收的视场相对减小传感器增益、光学偏差都和中心区域不同。出厂校准值是针对整个 SPAD 阵列的不能直接照搬到角落 ROI。在实验里同样一块白板放同样距离中心区域和右上角区域可能差几十毫米。如果不按区域标定最后合成的 TOF 雷达数据会带系统性偏差。ST API 里提供了官方校准入口但不同 SDK 版本函数名不一致。下面提供一个不依赖具体函数的手动标定流程在任何驱动上都能用对每个区域读多次距离求平均值再用真实距离减平均值得偏移量。4.2 逐个区域执行偏移校准static int16_t cal_offset[9]; void calibrate_all_zones(int16_t target_mm) { int32_t sum; VL53L1_StopRanging(g_dev); for (int i 0; i 9; i) { VL53L1_SetROI(g_dev, rois[i]); VL53L1_StartRanging(g_dev); sum 0; for (int j 0; j 10; j) { VL53L1_WaitMeasurementDataReady(g_dev); VL53L1_GetRangingMeasurementData(g_dev, g_data); if (g_data.RangeStatus 0) { sum g_data.RangeMilliMeter; } else { j--; // 坏帧重测 } VL53L1_ClearInterrupt(g_dev); } cal_offset[i] target_mm - sum / 10; VL53L1_StopRanging(g_dev); } }校准目标建议选灰色或白色纸板位置距离在 100 到 300mm 之间。纸板面积要覆盖当前 ROI 的投影区域并且白板平面垂直于传感器光轴。如果校准用的距离和实际使用距离差很多偏移量中的非线性部分会被放大所以产品量产后最好在使用距离处重新做一次。校准输出的cal_offset要保存到文件或 Flash。运行时把每个区域的测距值减去相应偏移再把结果交给上层逻辑。不要每个周期都校准温度变化时校准值会漂移建议每隔几分钟用固定目标快速复核一次。4.3 参数表信号率、环境光率和 sigma 怎么用多区域扫描结果里除了距离还有信号率、环境率、sigma 和状态位。这四个值决定了这个距离能不能信。参数含义多区域场景中的异常建议值参考RangeStatus测量状态非0即为无效必须等于0SignalRateKcps返回信号强度距离远或黑色物体时偏低小于1.0判无效AmbientRateKcps环境光干扰阳光直射时偏高超过1000要处理SigmaMilliMeter距离一致性弱反射和短预算时变大大于20mm判无效这些阈值要参考VL53L1_RangingMeasurementData_t结构体定义。实际测试时先打印所有区域的这些值再定阈值不要抄一个固定值通吃。同样一块黑布短距离模式下信号率可能只有白板的五分之一。4.4 弱反射处理与串扰问题ROI 变小以后串扰会变得明显。VCSEL 发射的锥形红外光比 ROI 大ROI 外反射的光有可能通过内部光路进入 SPAD。黑色目标物反射率低串扰占比更高。如果应用有窗玻璃或亚克力罩必须在装完外壳后重新做串扰校准并把校准结果按区域缓存。另外区域间的“热串扰”也要注意。当目标物恰好位于两个 ROI 边界时两个区域都可能读出一个小距离值。在后处理里可以做一个 1×3 或 3×3 的中值滤波消除孤立噪声点。这个滤波逻辑不要放进中断放在业务线程即可。5. 把多区域扫描结果变成避障地图的几个技巧5.1 最短区域确定危险方向不需要建整张地图9 个区域的距离已经能给出危险方向。取所有有效距离的最小值它的区域索引就是最危险的方位。int danger_dir -1; int min_dist 2000; for (int i 0; i 9; i) { if (g_range_mm[i] 0 g_range_mm[i] min_dist) { min_dist g_range_mm[i]; danger_dir i; } } if (min_dist 300) { printf(danger zone %d, %dmm\n, danger_dir, min_dist); }这个逻辑 10 行以内完成适合低成本小车。把danger_dir换算成左右方向再配合电机控制就能实现简单避障。5.2 动态跳过静止区域提高刷新率全量扫描的帧率受限于最慢区域。实际场景中大部分区域变化很慢可以维护一个脏标记位。距离连续几次变化小于 20mm 的区域暂时跳过其他区域恢复扫描。这能把帧率提高近一倍。static uint16_t dirty_mask 0x01FF; static int16_t last_mm[9]; void scan_dirty(void) { for (int i 0; i 9; i) { if (!(dirty_mask (1 i))) continue; // 扫描该区域更新g_range_mm[i] // zone_scan_one(i) 为前文的单区域测量流程 if (abs(g_range_mm[i] - last_mm[i]) 20) { dirty_mask | (1 i); // 有变化继续扫 } else { dirty_mask ~(1 i); // 静止跳过 } last_mm[i] g_range_mm[i]; } }关键点是dirty_mask初始为全 1保证第一次全量扫描。跳过期间如果障碍突然出现需要在主循环定时全量刷新一次避免漏检。5.3 串口可视化调试开发时最好直接用字符画观察 9 个区域。下面这段输出距离等级比一长串数字直观得多for (int r 0; r 3; r) { for (int c 0; c 3; c) { int v g_range_mm[r * 3 c]; if (v 0) putchar(?); else if (v 200) putchar(#); // 危险近 else if (v 600) putchar(.); // 中距离 else putchar( ); // 远或空 } printf(\n); }配合延时观看你会看到一个 3×3 的“点阵”随物体移动而变化。这一步对验证 ROI 切换是否生效特别有效不用等跑通整个上层逻辑就能排查脏数据。把区域数据接到决策逻辑里之后这套 VL53L1CB 多区域扫描方案就可以当成一个袖珍 TOF 雷达来用了。本文还有配套的精品资源点击获取