基于 STM32WB5MM-DK 的接近唤醒低功耗测距显示(Zephyr)
板载 VL53L0CX 激光测距传感器 + SSD1306 OLED:
有人靠近自动亮屏显示距离,无人 5 秒后熄屏进入低功耗。
全程只用 Zephyr 标准 API,代码不足 150 行。
1. 功能效果
- 活跃态:OLED 实时显示温度和距离,串口同步打印;
- 无人 5 秒:OLED 熄屏,停止温度采样,MCU 进入 STOP2 低功耗;
- 低功耗态:每 500ms 唤醒一次快速测距(耗时约 33ms),其余时间 MCU 与传感器都处于低功耗;
- 有人靠近(<500mm):立即亮屏恢复显示,响应延迟 <0.7 秒。
2. 硬件
STM32WB5MM-DK 板载 VL53L0CXV0DH/1(原理图 U6),与 VL53L0X 软件兼容:
| 信号 |
连接 |
| I2C |
I2C3(PB13 SCL / PB11 SDA),地址 0x29 |
| XSHUT |
硬件上拉到 3.3V,上电即工作,无需控制 |
| GPIO1 |
PD9(本方案不使用,纯定时轮询) |
Zephyr 官方板级 dts 已声明传感器节点,但 xshut-gpios 指向 PC6 与硬件不符 (PC6 实际用于触摸),需在 overlay 删除。
3. 工作原理
┌─────────────────────────────────────┐
│ 活 跃 态 │
│ 200ms 轮询,OLED 显示温度+距离 │
└──────────────┬──────────────────────┘
│ 连续 5 秒无 500mm 内目标
▼
┌─────────────────────────────────────┐
│ 低 功 耗 态 │
│ OLED 熄灭,MCU 停 STOP2 │
│ 每 500ms 被 LPTIM 唤醒测一次距 │
└──────────────┬──────────────────────┘
│ 测量有效且 <500mm
▼
回到活跃态
关键点:STOP2 下 SysTick 停摆,需要 LPTIM1 作低功耗 tick 源(WB5MM 模块 内置 32.768kHz LSE 晶振),否则第一次 k_sleep 就会睡死。
这是 Zephyr 在STM32WB 上开 CONFIG_PM 的必要配置。
4. 配置
4.1 prj.conf(增量)
# VL53L0X ToF 测距传感器(板载 U6,I2C3 @ 0x29)
CONFIG_VL53L0X=y
# 系统级电源管理:idle 时自动进 STOP0/1/2
CONFIG_PM=y
4.2 boards/stm32wb5mm_dk.overlay
#include <zephyr/dt-bindings/clock/stm32wb_clock.h>
/* 修正官方 dts 与硬件不符的属性(PC6 未接传感器 XSHUT) */
&i2c3 {
vl53l0x@29 {
/delete-property/ xshut-gpios;
};
};
/* PM 必需:LPTIM1 作低功耗 tick 源,STOP2 下继续运行 */
&clk_lse {
status = "okay";
};
stm32_lp_tick_source: &lptim1 {
clocks = <&rcc STM32_CLOCK(APB1, 31)>,
<&rcc STM32_SRC_LSE LPTIM1_SEL(3)>;
status = "okay";
};
5. 完整代码(src/main.c)
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/drivers/display.h>
#include <zephyr/display/cfb.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/drivers/sensor/vl53l0x.h>
#define NEAR_THRESHOLD_MM 500 /* 接近判定阈值 */
#define FAR_TIMEOUT_MS 5000 /* 无人多少毫秒后熄屏 */
#define SLEEP_POLL_MS 500 /* 低功耗态测距间隔 */
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));
int main(void)
{
if (!device_is_ready(dev) || !device_is_ready(temp_dev) ||
!device_is_ready(tof_dev)) {
printk("device not ready\n");
return -ENODEV;
}
display_set_pixel_format(dev, PIXEL_FORMAT_MONO01);
display_blanking_off(dev);
if (cfb_framebuffer_init(dev)) {
return -EIO;
}
cfb_framebuffer_clear(dev, true);
int64_t far_since = 0;
while (1) {
bool near = false;
struct sensor_value temp_value, dist, status;
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");
}
/* clear 第二参数必须 false,只清内存帧缓冲不刷屏,
* 否则每帧推两次全屏数据,OLED 会闪 */
cfb_framebuffer_clear(dev, false);
cfb_print(dev, "VL53L0CX demo", 0, 0);
cfb_print(dev, buf, 0, 16);
/* --- 单次测距 --- */
if (sensor_sample_fetch(tof_dev) == 0) {
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);
near = (mm < NEAR_THRESHOLD_MM);
} else {
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");
}
cfb_print(dev, buf, 0, 32);
cfb_framebuffer_finalize(dev);
if (near) {
far_since = 0;
k_sleep(K_MSEC(200));
continue;
}
if (far_since == 0) {
far_since = k_uptime_get();
}
if (k_uptime_get() - far_since >= FAR_TIMEOUT_MS) {
/* ===== 无人超时:熄屏进低功耗轮询 ===== */
display_blanking_on(dev);
for (;;) {
k_msleep(SLEEP_POLL_MS); /* 期间 MCU 自动停 STOP2 */
if (sensor_sample_fetch(tof_dev) != 0) {
continue;
}
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;
printk("sleep poll: status=%d mm=%d\n",
(int)status.val1, (int)mm);
if (status.val1 == 0 && mm < NEAR_THRESHOLD_MM) {
break; /* 有人靠近,退出休眠 */
}
}
display_blanking_off(dev);
far_since = 0;
}
k_sleep(K_MSEC(200));
}
}
6. 验证
range status=0, distance=2031 mm ← 活跃态,2m 墙稳定读出
...
(移开目标 5 秒,OLED 熄灭)
sleep poll: status=4 mm=8190 ← 低功耗轮询,无目标
sleep poll: status=0 mm=342 ← 手掌 30cm 靠近
(OLED 点亮,恢复实时显示)
- 2m 白墙实测误差 ±20mm;
- 唤醒响应 <0.7 秒(轮询间隔 500ms + 单次测量 ~33ms)。
7. 参数调优
| 参数 |
位置 |
说明 |
NEAR_THRESHOLD_MM |
500 |
接近判定距离,按产品需求改 |
FAR_TIMEOUT_MS |
5000 |
无人多久后熄屏 |
SLEEP_POLL_MS |
500 |
休眠测距间隔:调大更省电、唤醒更慢;调小相反 |
| 温度采样 |
低功耗态自动停止 |
STTS22H 自身功耗 µA 级,无需断电 |
功耗说明:低功耗态下 MCU 每 500ms 只工作约 33ms(测距),其余时间停 STOP2; 传感器每次测完自动回 standby(µA 级)。相比"传感器连续测距 + GPIO1 中断唤醒" 方案(传感器常~19mA),整机功耗低一个数量级,且规避了 VL53L0X 在量程边缘 阈值中断误触发的问题。
8. 编译烧录
west build -b stm32wb5mm_dk -p always app/stm32wb5mm_dk_vl53l0cxv -d build
west flash -d build
调试提示:板子熄屏处于 STOP2 时,串口 RX 和 SWD 调试器都不可用,属正常现象, 靠近唤醒后即恢复。