目标:在现有 OLED 工程基础上,实现 VL53L0CXV0DH/1 ToF(Time-of-Flight)传感器测距, 并把距离值实时显示到 OLED 上。
1. 硬件
开发板上板载了VL53L0CXV0DH/1,原理图上编号为U6

通过开发板原理图的分析,该传感器通过i2c3接入stm32wb5mm,GPIO1经PD9,并有上拉电阻,XSHUT通过10K的电阻上拉到3.3v,即上电即可用。
经整理如下表所示:

2. 设备树
通过分析zephyr中stm32wb5mm-dk的设备树,i2c3已经是启用了的,在前几期的i2c中我们已经启用。
同时vl53l0x也是配置好了的。
D:\luglZephyrproject\zephyr\boards\st\stm32wb5mm_dk\stm32wb5mm_dk.dts:191:
&i2c3 {
pinctrl-0 = <&i2c3_scl_pb13 &i2c3_sda_pb11>;
status = "okay";
vl53l0x@29 {
compatible = "st,vl53l0x";
reg = <0x29>;
xshut-gpios = <&gpioc 6 GPIO_ACTIVE_LOW>; /* 与硬件不符,见第 2 步 */
};
};
但是开发板上的xshut是没有按照官方的设备树节点来连接的,PC6是应用到了触摸那里,因此如果要使用pc6的话,需要进行禁止。
我在boards/stm32wb5mm_dk.overlay里把他删除掉:
&i2c3 {
vl53l0x@29 {
/* 硬件上 XSHUT 经 R10 上拉到 3V3,未接 PC6;
* 删除上游 dts 中与实际硬件不符的属性 */
/delete-property/ xshut-gpios;
};
};
因此我们的boards/stm32wb5mm_dk.overlay源码如下:
#include <zephyr/dt-bindings/sensor/stts22h.h>
/{
chosen {
zephyr,display = &oled; /* display API 通过 chosen 找默认屏 */
};
};
&spi1 {
status = "okay";
pinctrl-0 = <&spi1_sck_pa1 &spi1_mosi_pa7>;
pinctrl-names = "default";
cs-gpios = <&gpioh 0 GPIO_ACTIVE_LOW>; /* 片选引脚 PH0 */
oled: ssd1306@0 {
compatible = "solomon,ssd1306";
reg = <0>; /* 片选为 0 */
spi-max-frequency = <10000000>;
width = <128>;
height = <64>;
segment-offset = <0>;
page-offset = <0>;
display-offset = <0>;
multiplex-ratio = <63>;
segment-remap;
com-invdir;
prechargep = <0x22>;
data-cmd-gpios = <&gpioc 9 GPIO_ACTIVE_HIGH>; /* 数据命令选择引脚 PC9 */
reset-gpios = <&gpioc 8 GPIO_ACTIVE_LOW>; /* 复位引脚 PC8 */
};
};
&i2c3 {
vl53l0x@29 {
/* 硬件上 XSHUT 经 R10 上拉到 3V3,未接 PC6;
* 删除上游 dts 中与实际硬件不符的属性 */
/delete-property/ xshut-gpios;
};
stts22h: stts22h@38 {
compatible = "st,stts22h";
reg = <0x38>; /* 7 位地址 0x38 */
sampling-rate = <STTS22H_1Hz>; /* 连续转换 1Hz */
};
};
3. Kconfig配置
编辑 rj.conf,在传感器配置区域加一行:
# I2C 总线与传感器框架
CONFIG_I2C=y
CONFIG_SENSOR=y
CONFIG_STTS22H=y
# VL53L0X ToF 测距传感器(板载 U6,I2C3 @ 0x29)
CONFIG_VL53L0X=y
说明:
CONFIG_VL53L0X 依赖 DT_HAS_ST_VL53L0X_ENABLED(dts 里有该节点就满足),** 并自动 select I2C 和 select HAS_STLIB(编译 ST 官方 VL53L0X API, **源码在 D:\luglZephyrproject\modules\hal\st\sensor\vl53l0x)
4. 修改src/main.c,加入测距逻辑
Zephyr 的 vl53l0x 驱动是单次轮询模式:每次调用 sensor_sample_fetch()会执行一次完整的测距(ST API 的 VL53L0X_PerformSingleRangingMeasurement,耗时约 30 ms 量级),再用 sensor_channel_get() 取结果。关键 API 行为(与驱动源码 zephyr\drivers\sensor\st\vl53l0x\vl53l0x.c 对应):

测距状态,0 表示本次测量有效
把距离换算成毫米:mm = val1 * 1000 + val2 / 1000。
注意:自定义通道 SENSOR_CHAN_VL53L0X_RANGE_STATUS 定义在
<zephyr/drivers/sensor/vl53l0x.h>,需要单独包含。
下面是把 OLED + 温度 + 测距 整合后的完整 main.c:
#include <zephyr/kernel.h>
#include <zephyr/shell/shell.h>
#include <zephyr/sys/printk.h>
#include <zephyr/sys/reboot.h>
#include <zephyr/drivers/display.h>
#include <zephyr/display/cfb.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/drivers/sensor/vl53l0x.h>
const struct device *dev = DEVICE_DT_GET(DT_CHOSEN(zephyr_display));
const struct device *temp_dev = DEVICE_DT_GET(DT_NODELABEL(stts22h));
const struct device *tof_dev = DEVICE_DT_GET(DT_INST(0, st_vl53l0x));
static int cmd_hello(const struct shell *shell, size_t argc, char **argv)
{
shell_print(shell, "Hello from shell demo, stm32wb5mm_dk!");
return 0;
}
static int cmd_reboot(const struct shell *shell, size_t argc, char **argv)
{
shell_print(shell, "Rebooting the system...");
sys_reboot(SYS_REBOOT_COLD);
return 0;
}
SHELL_CMD_REGISTER(hello, NULL, "Print hello message", cmd_hello);
SHELL_CMD_REGISTER(reboot, NULL, "Reboot the system", cmd_reboot);
int main(void)
{
printk("OLED + ToF demo application started.\n");
if (!device_is_ready(dev)) {
printk("Display device not ready\n");
return -ENODEV;
}
if (!device_is_ready(temp_dev)) {
printk("Temperature sensor device not ready\n");
return -ENODEV;
}
if (!device_is_ready(tof_dev)) {
/* 常见原因:CONFIG_VL53L0X 未打开,或 I2C 上 0x29 无应答 */
printk("VL53L0X device not ready\n");
return -ENODEV;
}
display_set_pixel_format(dev, PIXEL_FORMAT_MONO01);
display_blanking_off(dev);
if (cfb_framebuffer_init(dev)) {
printk("cfb_framebuffer_init failed\n");
return -EIO;
}
cfb_framebuffer_clear(dev, true);
while (1) {
struct sensor_value temp_value;
char buf[32];
/* --- 温度 --- */
if (sensor_sample_fetch(temp_dev) == 0) {
sensor_channel_get(temp_dev, SENSOR_CHAN_AMBIENT_TEMP, &temp_value);
snprintf(buf, sizeof(buf), "Temp: %d.%02d C",
temp_value.val1, temp_value.val2 / 10000);
} else {
snprintf(buf, sizeof(buf), "Temp: error");
}
/* 注意第二个参数用 false:只清内存里的帧缓冲,不立刻刷屏,
* 否则每帧会推两次全屏数据(一次全黑、一次内容),OLED 会明显闪一下 */
cfb_framebuffer_clear(dev, false);
cfb_print(dev, "VL53L0CX demo", 0, 0);
cfb_print(dev, buf, 0, 16);
/* --- 测距:fetch 触发一次单次测量 --- */
if (sensor_sample_fetch(tof_dev) == 0) {
struct sensor_value dist, status;
sensor_channel_get(tof_dev, SENSOR_CHAN_DISTANCE, &dist);
sensor_channel_get(tof_dev,
(enum sensor_channel)SENSOR_CHAN_VL53L0X_RANGE_STATUS,
&status);
int32_t mm = dist.val1 * 1000 + dist.val2 / 1000;
if (status.val1 == 0) {
snprintf(buf, sizeof(buf), "Dist: %d mm", (int)mm);
} else {
/* 超量程 / 目标反射率太低时状态非 0,读数不可信 */
snprintf(buf, sizeof(buf), "Dist: ---- mm");
}
printk("range status=%d, distance=%d mm\n",
(int)status.val1, (int)mm);
} else {
snprintf(buf, sizeof(buf), "Dist: error");
printk("VL53L0X sample fetch failed\n");
}
cfb_print(dev, buf, 0, 32);
cfb_framebuffer_finalize(dev);
k_sleep(K_MSEC(500));
}
return 0;
}
要点解释:
sensor_sample_fetch(tof_dev) 内部第一次调用时会完成 VL53L0X_DataInit 和单次模式配置,之后每次调用就是一次“测一次距”。
- 测距前先检查
device_is_ready(tof_dev)。如果这里就失败,去第 6 节排查。
- 循环周期 500 ms 足够;单次测量本身要几十毫秒,别把周期设得太小。
- 传感器最长量程约 2 m(实际取决于目标反射率和环境光); 超出量程时
RANGE_STATUS != 0,代码里显示 ----。
验证
