news 2026/9/4 7:23:58

自制UWB无感解锁系统:基于STM32与DWM1000的测距门锁实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
自制UWB无感解锁系统:基于STM32与DWM1000的测距门锁实战

你有没有遇到过这样的场景:手里抱着快递、拎着垃圾袋,走到家门口还得先放下东西掏钥匙;或者走近电动车、工作间柜门,手刚碰到把手,却发现锁还紧紧闭着,必须腾出手来完成一次解锁操作。

我最近就在折腾一套基于 UWB(超宽带)的自制无感解锁系统。目标很简单:人走到门前,门锁自动解锁;人离开,门锁自动上锁。整个过程不用掏手机、不用刷指纹、不用按密码,站在门前等一两秒,门就开了。也就是标题里说的“开门不再罚站”。

如果你对“UWB 如何实现测距”“STM32 怎么驱动 UWB 模块”“距离判断逻辑怎么写”感兴趣,或者想自己动手做一个类似的硬件原型,这篇文章会是一份比较完整的参考。我会从原理讲起,给出硬件选型、系统架构、代码示例、常见坑点和工程化建议,尽量让新手能看懂,也能让有单片机基础的开发者直接照着改。

1. 什么是 UWB 无感解锁,它解决什么问题

1.1 无感解锁的本质

无感解锁,英文里常叫 Passive Entry / Passive Start,或者 Hands-free Access。它的核心不是“解锁”本身,而是“免操作”。用户不需要主动做任何动作,系统通过判断“合法持有者是否在附近”,自动完成开锁或闭锁。

这整套逻辑拆开以后其实只有三步:

  1. 身份验证:靠近的设备是不是我信任的设备。
  2. 距离测量:这个可信设备离门/车/锁有多远。
  3. 动作决策:当距离小于某个阈值时执行解锁,大于某个阈值时执行闭锁。

生活中最典型的例子是汽车的数字钥匙:你带着手机或智能手表走近车门,车辆自动解锁;走远后,车辆自动闭锁。

而本文要做的,就是把这套逻辑用 UWB 测距模块和 STM32 单片机复刻到普通门锁、柜锁或电动车上。它非常适合作为 UWB 定位、测距、物联网设备联动的入门项目。

1.2 为什么蓝牙方案容易“罚站”

很多人会问:蓝牙 BLE 也能测距,为什么还要上 UWB?

传统蓝牙方案做无感解锁,主要依赖 RSSI(Received Signal Strength Indicator,接收信号强度指示)来估算距离。信号强度会随距离衰减,理论上距离越近 RSSI 越大,距离越远 RSSI 越小。但实际环境中,人体遮挡、墙壁反射、多径效应都会让信号强度剧烈波动。

这就造成了几个体验问题:

  • 人在门外站了几秒,系统还没判断出“靠近”,这就是“罚站”。
  • 人在房间里面,门外的锁却因为手机信号穿墙误判为“靠近”,出现“隔空解锁”。
  • 站在门侧面和站在门正面,RSSI 值差异大,距离阈值很难调到稳定状态。

RSSI 测距的精度通常只有 3 到 10 米级别,而且波动大。做“靠近解锁”这种需要明确距离边界的功能,体验会非常不稳定。

1.3 UWB 的核心优势

UWB 全称 Ultra Wide Band,超宽带。它是一种使用极窄脉冲进行通信的无线技术,工作频段通常在 3.1 GHz 到 10.6 GHz 之间,占用带宽很大。UWB 测距不像蓝牙那样依赖信号强度,而是通过计算无线电波在设备之间飞行的时间来得到距离。

因为无线电波速度恒定,所以只要时间测量足够精确,距离精度就会很高。UWB 在室内环境下可以把测距误差控制在 10 厘米到 30 厘米范围内,远好于蓝牙 RSSI。

UWB 在消费电子和物联网里已经有几个成熟方向:

  • 手机与车之间的数字钥匙,比如 CCC(Car Connectivity Consortium)推广的 Digital Key 3.0。
  • 苹果 AirTag 这类寻物标签,利用 UWB 找东西。
  • 智能家居存在感知、UWB 雷达,检测人在房间里的位置。
  • 工业场景里的高精度人员或物资定位。

在“无感解锁”这个场景中,UWB 最大的价值是:它能明确告诉你,合法设备在门内还是门外、在 0.5 米内还是在 2 米外。这个“空间边界感”是蓝牙 RSSI 很难做到的。

2. 自制 UWB 无感解锁系统的整体架构

2.1 系统工作流程

我们做一个最小可用的 UWB 近距解锁原型,系统分为两个角色:

  • 固定端 Anchor(锚点):安装在门锁、柜锁或电动车内部,负责接收便携端测距请求,计算距离并控制锁具。
  • 便携端 Tag(标签):由用户随身携带,通常是一个带电池的 UWB 小模块,也可以集成在手机壳或卡套里。

工作流程简化后如下:

  1. Tag 每隔一段时间主动发送一条测距请求帧。
  2. Anchor 接收到请求后,回复一条确认帧。
  3. Tag 或 Anchor 根据两条帧的收发时间差计算出两者之间的物理距离。
  4. 当 Anchor 计算出的距离小于解锁阈值,比如 80 厘米,就触发开锁动作。
  5. 当距离连续多次大于闭锁阈值,比如 2 米,就触发闭锁动作。

如果需要识别“靠近的人是不是自己人”,还要在测距帧里携带设备 ID,Anchor 先在本地白名单里查一遍,只有白名单设备参与后续距离判断。

2.2 核心硬件选型

UWB 模块是整套系统里最关键的部分。目前最容易入手的方案是 Qorvo(原 Decawave)的 DW1000 和 DWM1000 模块,它们实现了 IEEE 802.15.4-2011 UWB 物理层。后来推出的 DWM3000 系列支持 802.15.4z,增加了更安全的测距特性,价格也更高。

如果做学习原型,推荐 DWM1000 模块,原因如下:

  • 资料多,官方 SDK 和网上教程都比较全。
  • 通过 SPI 接口与 MCU 通信,适合 STM32、Arduino、ESP32 等主控。
  • 测距精度可达 10 厘米左右,对解锁场景已经够用。
  • 模块价格相对 DWM3000 便宜很多,适合前期验证算法。

主控方面,可以用 STM32F103C8T6 这种经典单片机,也可以用 STM32F401、STM32F411 等带更多 RAM 和主频的芯片。Tag 端如果要低功耗,建议选 STM32L 系列,不过原型阶段不必纠结功耗,普通 STM32F103 就能跑起来。

锁具驱动部分,常见的选择:

  • 电磁锁:通过继电器或 MOS 管控制通断电。
  • 舵机:通过 PWM 占空比控制锁舌旋转。
  • 电机锁:通过电机驱动芯片正反转。

原型阶段建议用舵机或电磁锁,控制逻辑简单,安全风险也小。

我这里整理了一份供参考的硬件清单:

部件推荐型号/规格数量用途
主控 MCUSTM32F103C8T6 最小系统板2 块Anchor 和 Tag 各一块
UWB 模块DWM1000 模块2 块测距通信
舵机SG90 或 MG9951 个模拟锁舌开闭
电源5V USB 供电或锂电池2 套供电
调试工具USB-TTL 串口模块1 个查看测距日志
其他按键、LED、蜂鸣器若干状态指示和触发

这个清单不是绝对标准,你完全可以按照手头已有的模块替换。关键是理解测距这套逻辑,不限定具体厂商。

2.3 软件架构分层

软件部分不要全部堆在一个 main.c 里。即使原型代码规模不大,我也建议按下面几层拆分,方便后续迁移和调试:

  • 硬件驱动层:负责 SPI、GPIO、串口初始化和 DWM1000 模块底层的寄存器读写。
  • UWB 协议层:负责发送测距帧、接收测距帧、解析时间戳、计算距离。
  • 业务逻辑层:负责白名单判断、状态机切换、解锁/闭锁动作。
  • 应用层:main 函数里串联各模块,打印日志。

STM32 端的代码可以使用 STM32 HAL 库,也可以使用标准外设库。DWM1000 官方 SDK 提供了一组dwt_开头的 API,例如dwt_initialisedwt_configuredwt_rxtx等。这部分 API 在不同版本里略有差异,所以下文代码我会保留核心思路,并标注“示例代码,请结合你的 SDK 和模块型号调整”。

3. UWB 测距原理拆解

3.1 TOF 测距的基本思想

UWB 测距最常用的方法是 TOF(Time of Flight,飞行时间)。思路其实特别朴素:电磁波在空气中传播速度约为光速 3×10^8 m/s,如果我们测得信号从 A 到 B 花费的时间 t,那么距离 d = c × t / 2。

比如测距结果 10 纳秒对应约 1.5 米飞行距离,往返约 3 米。这里的关键问题是:普通时钟根本测不了纳秒级的时间差。DW1000 芯片内部集成了高精度时间测量电路,时间戳精度可以到 15.65 皮秒左右,这样才能实现厘米级测距。

3.2 SS-TWR 单边双向测距

在无感解锁这类自发自收的应用中,最常见的是 SS-TWR 单边双向测距(Single-Sided Two-Way Ranging)。流程如下:

  1. 设备 A 在时间 T1 发送 Poll 帧。
  2. 设备 B 在时间 T2 收到 Poll 帧。
  3. 设备 B 经过处理延迟 Treply 后,在时间 T3 发送 Response 帧。
  4. 设备 A 在时间 T4 收到 Response 帧。

设备 A 可以计算出整个往返时间 Tround = T4 - T1。设备 B 可以在 Response 帧里携带 Treply = T3 - T2,A 收到后,飞行时间 Tof = (Tround - Treply) / 2。

这种模式只需要一次请求、一次回复,实现简单,适合移动端和固定端之间的快速测距。缺点是对时钟晶振频率偏差比较敏感,误差会比 DS-TWR(双边双向测距)稍大。原型阶段可以先跑通 SS-TWR,后续再升级 DS-TWR。

3.3 DS-TWR 双边双向测距

DS-TWR 在 SS-TWR 基础上增加了一次额外的传输,完整通信过程类似:

  1. A 发送 Poll。
  2. B 回复 Response。
  3. A 再发送 Final。
  4. B 根据三段帧的时间戳计算出飞行时间。

DS-TWR 可以抵消大部分由晶振频率偏差引起的测距误差,精度更高。代价是单次测距耗时变长、功耗变大。很多商用数字钥匙最终会用 DS-TWR,或者结合 UWB 安全测距协议。

这里用一张简单顺序来理解三种方案的关系:

  • SS-TWR:两次传输,实现简单,精度一般。
  • DS-TWR:三次传输,精度高,适合车锁等对可靠性要求较高的场景。
  • TDOA:多个固定基站监听同一信号到达时间差,更适合定位系统,不适用于单 Tag 对单 Anchor 的解锁场景。

在自制原型中,建议先用 SS-TWR 把整个链路跑通,再用 DS-TWR 做精度优化。这部分代码逻辑比较固定,网上的开源实现也不少,但要根据硬件版本改天线延迟、帧格式和回调函数。

4. 完整实战:自组一套 UWB 无感门锁原型

4.1 项目结构与文件规划

为了让示例更清晰,我假设你在工程里至少要有下面这些文件:

project/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── uwb_app.h │ │ └── door_control.h │ └── Src/ │ ├── main.c │ ├── uwb_app.c │ └── door_control.c ├── Drivers/ │ ├── STM32F1xx_HAL_Driver/ │ └── deca_dwm1000/ │ ├── deca_device_api.c │ ├── deca_device_api.h │ ├── deca_regs.h │ └── ... └── Makefile 或 Keil/STM32CubeIDE 工程

deca_device_api.c是 DWM1000 官方驱动层。你需要从芯片厂商提供的 SDK 中获取这些文件,并将 SPI 读写回调函数对接为 STM32 HAL 库的 SPI 接口。

4.2 初始化 DWM1000 模块

不管是 Anchor 还是 Tag,首先要完成 DWM1000 模块的初始化。下面是一段示意性的初始化代码,实际调用请以你的 SDK 版本为准。

// 文件路径:Core/Src/uwb_app.c #include "uwb_app.h" #include "deca_device_api.h" #include "deca_regs.h" extern SPI_HandleTypeDef hspi1; static uint8_t uwb_rx_buffer[128]; static uint16_t uwb_rx_len = 0; // SPI 写字节回调,由 deca_device_api.c 内部调用 int spi_write_to_uwb(uint16_t headerLength, const uint8_t *headerBuffer, uint32_t bodyLength, const uint8_t *bodyBuffer) { // 先发送头部,再发送数据体 HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, (uint8_t *)headerBuffer, headerLength, 100); if (bodyLength > 0) { HAL_SPI_Transmit(&hspi1, (uint8_t *)bodyBuffer, bodyLength, 100); } HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET); return 0; } int spi_read_from_uwb(uint16_t headerLength, const uint8_t *headerBuffer, uint32_t bodyLength, uint8_t *bodyBuffer) { HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET); HAL_SPI_Transmit(&hspi1, (uint8_t *)headerBuffer, headerLength, 100); if (bodyLength > 0) { HAL_SPI_Receive(&hspi1, bodyBuffer, bodyLength, 100); } HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET); return 0; }

这段代码把deca_device_api.c里需要的 SPI 传输函数对接到了 STM32。注意CS_PORTCS_PIN需要根据你的硬件原理图定义。

初始化函数里,需要完成以下步骤:

void uwb_module_init(void) { // 1. 复位 DWM1000 模块 HAL_GPIO_WritePin(RST_PORT, RST_PIN, GPIO_PIN_RESET); HAL_Delay(10); HAL_GPIO_WritePin(RST_PORT, RST_PIN, GPIO_PIN_SET); HAL_Delay(10); // 2. 初始化 DW1000 芯片 if (dwt_initialise(DWT_LOADUCODE) < 0) { // 初始化失败,通常说明 SPI 通信有问题 printf("DWT init failed\r\n"); return; } // 3. 关闭帧过滤,先让所有 UWB 帧都能收到 dwt_setcallbacks(NULL, NULL); dwt_setinterrupt(DWT_INT_RX_PENDING, 0); // 4. 配置默认信道和速率 dwt_configure(&default_uwb_config); }

default_uwb_config来自官方示例,里面包含 PRF、数据速率、信道号、Preamble 码等参数。Anchor 和 Tag 两端必须保持一致,否则无法通信。比如信道 2、PRF 64 MHz、数据速率 6.8 Mbps,是一组比较常用的配置。

4.3 Anchor 端:门锁端测距主逻辑

Anchor 端固定在门上。它平时处于接收状态,收到 Tag 的测距请求后,回复确认帧,并在一轮测距完成后调用距离判断函数。

这里给出一个简化的 Anchor 主循环示例:

// 文件路径:Core/Src/main.c #include "uwb_app.h" #include "door_control.h" static volatile uint32_t measured_distance_mm = 0; static volatile uint8_t ranging_complete = 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == DWT_IRQ_PIN) { dwt_isr(); } } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); MX_USART1_UART_Init(); printf("UWB Anchor starting...\r\n"); uwb_module_init(); door_control_init(); while (1) { // 启动一轮测距:等待 Tag 发 Poll,收到后自动回复 Response if (uwb_start_ranging_as_anchor() == RANGING_OK) { // 返回值为最近一轮测距得到的距离,单位毫米 measured_distance_mm = uwb_get_last_range_mm(); printf("distance: %lu mm\r\n", measured_distance_mm); // 如果连续 N 次小于解锁阈值,执行开锁 if (measured_distance_mm < UNLOCK_DISTANCE_MM) { door_handle_near(); } else { door_handle_far(); } } HAL_Delay(50); } }

这里UNLOCK_DISTANCE_MM可以定义成 800,也就是 80 厘米。door_handle_neardoor_handle_far这两个函数内部需要做防抖处理,不能每次测距都直接驱动舵机,否则会造成锁舌频繁动作。

更接近工程实现的写法是维护一个“连续近距计数”:

// 文件路径:Core/Src/door_control.c #include "door_control.h" #define UNLOCK_DISTANCE_MM 800 #define LOCK_DISTANCE_MM 2000 #define NEAR_THRESHOLD_COUNT 3 #define FAR_THRESHOLD_COUNT 10 static uint8_t near_count = 0; static uint8_t far_count = 0; static door_state_t current_state = DOOR_LOCKED; void door_control_init(void) { // 初始化舵机引脚为 PWM 输出 // 上电默认让门锁处于锁定状态 servo_set_angle(DOOR_LOCK_ANGLE); } void door_handle_near(uint32_t distance_mm) { if (current_state == DOOR_LOCKED || current_state == DOOR_NEAR) { if (distance_mm < UNLOCK_DISTANCE_MM) { near_count++; if (near_count >= NEAR_THRESHOLD_COUNT) { near_count = 0; unlock_door(); current_state = DOOR_UNLOCKED; } } } } void door_handle_far(uint32_t distance_mm) { if (current_state == DOOR_UNLOCKED || current_state == DOOR_NEAR) { if (distance_mm > LOCK_DISTANCE_MM) { far_count++; if (far_count >= FAR_THRESHOLD_COUNT) { far_count = 0; lock_door(); current_state = DOOR_LOCKED; } } } } static void unlock_door(void) { servo_set_angle(DOOR_UNLOCK_ANGLE); printf("DOOR UNLOCKED\r\n"); } static void lock_door(void) { servo_set_angle(DOOR_LOCK_ANGLE); printf("DOOR LOCKED\r\n"); }

你没看错,这里要区分两个阈值:80 厘米以内的“解锁阈值”和 2 米以外的“闭锁阈值”。中间 80 厘米到 2 米这段是滞回区间,目的是防止用户站在门旁边徘徊时,锁反复开闭。这种“双阈值 + 连续计数”的设计,是实现稳定无感解锁非常重要的一环。

4.4 Tag 端:便携端主动发起测距

Tag 端逻辑更简单,它像“信标”一样,每隔一段时间发送一次测距请求。为了省电,实际产品里 Tag 通常不会一直全速发射,而是采用低频唤醒 + 事件触发方式。原型中先固定 200 毫秒间隔即可。

// 文件路径:Core/Src/tag_main.c(示意) #include "deca_device_api.h" #include "deca_regs.h" extern volatile uint8_t tx_done; extern volatile uint8_t rx_done; void tag_loop(void) { uint32_t poll_send_time = 0; uint32_t resp_receive_time = 0; uint32_t treply = 0; uint32_t tround = 0; uint32_t tof = 0; while (1) { // 1. 记录发送 Poll 帧的时间戳 poll_send_time = dwt_readsystimestamphi32() << 8 | dwt_readsystimestamp(); uwb_send_poll_frame(); // 2. 等待接收 Anchor 的 Response 帧 if (uwb_wait_response_frame() == RANGING_OK) { resp_receive_time = dwt_readsystimestamphi32() << 8 | dwt_readsystimestamp(); // 3. Response 帧里携带了 Treply,解析出来 treply = uwb_parse_treply_from_response(); // 4. 在 Tag 端也能算距离,但业务判断放在 Anchor 端更常见 tround = resp_receive_time - poll_send_time; tof = (tround - treply) / 2; printf("range estimate: %lu\r\n", tof); } HAL_Delay(200); } }

注意,这里的代码是简化的逻辑演示,并不是可以直接编译的最终代码。因为 DW1000 的时间戳读取、帧类型解析要依赖你的帧结构设计和 SDK 回调实现。

你可以根据自己的需求选择两种测距结果处理方式:

  • 在 Anchor 端计算距离并控制锁具,这样 Tag 端不用执行解锁指令,更安全。
  • 在 Tag 端计算距离后,通过蓝牙或 WiFi 发送解锁指令。这种适合“手机当 Tag”的方案,因为手机本身有更强的通信能力。

4.5 帧格式设计与设备识别

要让 Anchor 认识 Tag,还得在自定义帧里带上设备 ID 和帧类型。下面是一个极简帧格式,你可以参考:

字节偏移内容说明
0Frame Control服务数据帧标识,由 DW1000 SDK 自动处理
2Sequence Number序列号
3Tag ID2 字节设备 ID
5Frame Type0x01 Poll / 0x02 Response / 0x03 Final
6Timestamp4 字节时间戳,或由硬件时间戳辅助
10Payload预留扩展字段

Anchor 收到 Poll 帧后,先提取 Tag ID,并与本地保存的合法白名单比较:

const uint16_t allow_list[] = {0x0001, 0x0002}; const uint8_t allow_list_size = 2; int is_tag_allowed(uint16_t tag_id) { for (uint8_t i = 0; i < allow_list_size; i++) { if (allow_list[i] == tag_id) { return 1; } } return 0; }

只有在白名单内的 Tag 才继续后续测距,否则直接丢弃。这一步是“身份认证”的最基础形态。商用系统不会只用固定 Tag ID,因为 ID 可以被伪造。但在入门原型阶段,白名单已经够用。

4.6 编译、烧录与验证

整个工程建议使用 STM32CubeIDE、Keil MDK 或 PlatformIO + STM32Cube 框架来编译。烧录方式通常是 ST-Link 或 USB-TTL 串口 ISP。

验证分三个阶段:

  1. 模块通信验证:给两个设备烧录官方 Example 的tx_ss_twrrx_ss_twr例程,确认能互相测距。
  2. 本端串口验证:把 Anchor 和 Tag 都接上串口,观察测距结果是否随距离变化。比如人拿着 Tag 从 3 米走向门锁,串口打印的距离是否从 3000 mm 左右逐步下降到 200 mm 左右。
  3. 联动验证:Anchor 连接舵机,观察靠近和远离时舵机是否按预期动作。

如果发现测距结果来回跳变,可以先用官方上位机或者串口打印原始测量值做记录,再判断是天线方向、多径干扰还是参数配置问题。

5. 常见问题与排查思路

自制 UWB 项目踩坑比较多的集中在硬件连接、SPI 通信和测距稳定性三个方面。下面整理一个排查表,实际调试时能少走弯路。

问题现象常见原因解决思路
DWT 初始化失败SPI 引脚接错、模块供电不足、RST 时序不对检查 SPI 接线,用万用表确认电压,复位时序加长延时
双方都初始化成功但收不到帧信道、PRF、Preamble 码配置不一致将 Anchor 和 Tag 的默认配置打印出来对比
距离数值固定不变读取的时间戳变量被优化掉或帧里没有携带有效时间戳检查时间戳字节序,确认寄存器读取函数返回值有效
测距结果偶尔飘几十厘米天线附近有金属遮挡、移动速度太快调整天线位置,做多次滤波,校准天线延迟
开门响应太慢测距间隔太长或连续计数阈值太大适当缩短轮询周期,调低 NEAR_THRESHOLD_COUNT
人走远了门才锁闭锁距离阈值设置不合理把 LOCK_DISTANCE_MM 调小,并增加远距计数判断
使用手机作为 Tag 无法解析手机系统 UWB API 与 DW1000 私有帧不兼容自制原型可先使用 DWM1000 模块对模块,手机后续再评估

除此之外,还有两个容易忽略的细节:

  • DWM1000 模块的天线延迟参数。不同模块、不同 PCB 设计会导致天线延迟不同,需要在测距结果里做校准。你可以在 1 米处放一个固定 Anchor,把打印距离调整到接近 1000 mm。
  • UWB 模块功耗不低。如果你用锂电池给 Tag 供电,建议测距完成以后让 DW1000 进入休眠状态,降低平均电流。否则一个 500 mAh 的小电池可能撑不了多久。

6. 工程化建议与安全边界

6.1 让测距更稳定的小技巧

距离判断不能只看单次结果,至少要叠加滤波和状态机。我推荐的组合是:

  • 中值滤波:取最近 5 次测距结果,排序后取中间值作为当前有效距离。
  • 双阈值滞回:解锁阈值和闭锁阈值之间拉开距离,避免临界区抖动。
  • 连续计数:连续 N 次满足条件才动作,抵消瞬时干扰。
  • 测距超时处理:如果连续多次没有收到 UWB 帧,说明 Tag 可能已经离开或没电了,可以按远距离处理,触发闭锁。

示例中值滤波代码可以这样写:

#define FILTER_WINDOW 5 uint32_t distance_filter(uint32_t new_value) { static uint32_t history[FILTER_WINDOW]; static uint8_t index = 0; uint32_t sorted[FILTER_WINDOW]; uint32_t tmp; history[index] = new_value; index = (index + 1) % FILTER_WINDOW; // 拷贝并排序 for (uint8_t i = 0; i < FILTER_WINDOW; i++) { sorted[i] = history[i]; } for (uint8_t i = 0; i < FILTER_WINDOW - 1; i++) { for (uint8_t j = i + 1; j < FILTER_WINDOW; j++) { if (sorted[j] < sorted[i]) { tmp = sorted[i]; sorted[i] = sorted[j]; sorted[j] = tmp; } } } return sorted[FILTER_WINDOW / 2]; }

可以看到,中值滤波尤其适合处理 UWB 测距中偶发的尖峰跳变。比如真实距离是 50 厘米,突然一次测量变成 3 米,中值滤波能把这个异常值放在排序的另一端,不容易影响最终结果。

6.2 防误触与安全边界

无感解锁本质上是“靠近即开锁”,这带来一个不可避免的问题:误触风险和安全风险。

比如一个人带着 Tag 只是路过门口,如果他的路线正好离门 1 米内,系统可能误判为“靠近”并开锁。为了降低这种风险,可以在原型中加入运动方向判断或停留时间判断。更常用的做法是结合蓝牙 RSSI 做粗判断,只有蓝牙信号接近时才开始 UWB 测距,避免 UWB 一直高功耗空转。

关于安全边界,必须强调以下几点:

  • 自制代码里的固定 Tag ID 很容易被重放攻击,所以它不能替代门禁系统中的加密芯片方案。如果要做真实门锁,建议使用带有安全测距功能的 UWB 芯片,例如支持 802.15.4z 的 DWM3000,并增加加密认证。
  • 这套系统只能用于你自己拥有、或被合法授权的设备上,绝对不能用于非法开锁、绕过他人门禁等场景。涉及锁具改装时,务必评估机械和电气安全风险。
  • 如果要控制真实防盗门锁,建议先使用电磁锁或舵机在测试台上验证,不要一上来就改动原门锁结构。正式使用前需要做断电、断电重连、低电量等异常测试。

6.3 后续还可以怎么升级

原型跑通以后,可以往几个方向继续升级:

  • 低功耗改造:Tag 端增加休眠唤醒,使用 STM32L 系列,把待机电流降到微安级。
  • 手机作为 Tag:利用手机系统自带 UWB API 与门锁通信,但需要对接口协议和芯片兼容性做调研,难度比双 DWM1000 高不少。
  • 多 Anchor 融合定位:在房间内布多个 Anchor,用 TDOA 或三边定位判断用户是在门内还是门外,能解决单 Anchor 无法区隔方向的问题。
  • 安全升级:引入 AES 加密、滚动码防重放,配合高安全等级 UWB 芯片完成距离边界验证。

7. 总结与下一步方向

到这里,我们已经把一套自制 UWB 无感解锁原型从概念拆到了代码级别。核心收获可以总结成三条:

  • UWB 无感解锁的原理是飞行时间测距,不是 RSSI 猜距离。它用纳秒级时间测量换来了厘米级的空间边界感。
  • 一套最小原型需要 Anchor、Tag 两个 UWB 节点,加上白名单识别、距离阈值判断、防抖状态机这几个软件模块。
  • 真正好用的无感解锁不是“到点就开锁”,而是“稳定可靠地识别靠近和离开”,需要在滤波、阈值滞回、连续计数上做工程化设计。

下一步,我建议你先跑通两个 DWM1000 模块的官方测距示例,再逐步移植到自己的 STM32 工程里。等距离数据稳定之后,再加入舵机、白名单和状态机这些外围逻辑。这套路径可以让你在硬件出现问题时更容易定位,不至于代码和硬件问题混在一起排查。

如果你正在做类似的“靠近自动解锁”项目,或者已经在 DWM1000/DWM3000 上踩过坑,欢迎在评论区分享你的板型和调参经验。希望这篇笔记能帮你少走一些弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 7:22:21

07-03-并发-ConcurrentStack-T-无锁链式栈的CAS协议

ConcurrentStack<T>&#xff1a;无锁链式栈的 CAS 协议专栏&#xff1a;C# 与常用数据结构源码剖析 本文基线&#xff1a;.NET 8.0.0 发布标签中 System.Collections.Concurrent.ConcurrentStack<T> 的公开契约与私有实现 阅读原则&#xff1a;公开 API 是跨版本契…

作者头像 李华
网站建设 2026/9/4 7:21:45

STM32人流量检测工程实践:从Proteus仿真到工业级稳定部署

简介&#xff1a;本资源是一套基于STM32的嵌入式人流量检测系统完整工程代码&#xff0c;面向单片机初学者与嵌入式硬件开发者&#xff0c;解决实际场景中出入人数统计、时间同步与本地数据持久化等典型应用问题。压缩包共88个文件&#xff0c;含37个头文件&#xff08;.h&…

作者头像 李华
网站建设 2026/9/4 7:20:01

自建电子书库:BookLore部署与本地电子书管理全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 7:19:55

AI Agent完整运行流程拆解:从感知规划到行动反思的工程实践

现在聊 AI Agent 的人很多&#xff0c;但真能把一个 Agent 从接收用户请求到最终输出结果&#xff0c;中间每一步发生了什么、有哪些关键节点、哪些环节最容易翻车&#xff0c;讲清楚的人非常少。我在实际项目里接了好几个 Agent 相关的需求&#xff0c;自己也从零搭过几套完整…

作者头像 李华
网站建设 2026/9/4 7:18:26

秋叶ComfyUI V9.5整合包:一键部署AI绘画工作流,支持Win/Mac

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 7:18:21

Flux 3视频生成模型:以“分拆-重组”架构革新时序一致性

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华