STM32 + UWB 无感解锁门锁自制教程:从测距原理到工程实现
智能门锁发展到现在已经很少有人愿意再掏钥匙开门了。可很多人的真实体验却是提着快递站到门口指纹按了好几次才通过天太黑、手指脏污、角度不对门锁都会让你在自家门口多“罚站”几秒。如果你想要的效果是“人走到门附近门自己就解锁不需要任何多余动作”那么 UWB 无感解锁就是非常值得研究的方案。本文将围绕“STM32 UWB 自制无感解锁”这条主线展开从 UWB 测距原理、硬件选型、无线报文设计到门锁端无感解锁状态机代码最后补充常见故障排查和工程建议。适合以下几类读者想入门 UWB、了解超宽带测距的嵌入式开发者想自己动手做一套接近感应门锁的硬件爱好者正在做智能门锁、门禁产品预研需要评估无感解锁可行性的开发者。文章不会罗列一大堆底层寄存器而是先把关键理论讲透再给出一套可落地的最小工程框架。1. UWB 无感解锁是什么能解决什么问题1.1 为什么“靠近自动开门”这件事这么难传统感应门锁常见方案有几种红外人体感应只能检测到“有人”无法精确判断距离且容易受阳光、温度干扰蓝牙低功耗BLE接近感应通过信号强度 RSSI 估算距离但 RSSI 受环境影响很大人在走廊晃一下也可能被判断为“靠近”人脸识别需要用户站到固定区域、抬头配合摄像头指纹识别本质上还要用户主动伸出手指。所以真正意义上的“无感”解锁不是识别更快而是让用户完全省略开门动作。要做到这一点最重要的技术前提是系统能稳定知道“随身设备离门锁有多近”并且这个距离判断足够精准、足够快、足够抗干扰。1.2 UWB 为什么能做到“厘米级”距离感知UWB 全称是 Ultra Wideband中文叫超宽带是一种使用纳秒级窄脉冲传输数据的无线通信技术。和蓝牙、Wi-Fi 不一样UWB 不是靠信号强度估距离而是通过测量无线电信号的飞行时间Time of Flight来计算距离。射频信号在空气中的传播速度接近光速如果系统能测出信号从设备 A 飞到设备 B 用了多少时间用这个时间乘以光速就能算出发射端到接收端的距离。这里就体现出 UWB 的巨大优势了UWB 信号带宽通常达到 500 MHz 以上时间分辨率高抗多径干扰能力强不会因为墙壁反射导致距离大幅波动在理想环境下测距精度可以做到厘米级远好于蓝牙 RSSI 的米级精度。因此苹果很早就把 UWB 用于 AirTag 查找汽车数字钥匙 CCC 标准也将 UWB 列入关键技术。现在越来越多的智能家居、门锁、室内定位系统开始关注 UWB。1.3 UWB 定位和无感解锁是一回事吗经常有读者搜索“UWB 定位”后看到错位的内容。这里先做区分UWB 定位通常指利用多个固定在空间中的锚点Anchor解算移动标签Tag在房间里的二维或三维坐标。典型应用是工厂人员定位、隧道施工人员定位、医院设备管理。UWB 无感解锁核心并不需要算出精准坐标只需要得到“Tag 与门锁之间的距离”并判断是否进入解锁范围。它是 UWB 测距能力的一种简化应用场景。换句话说无感解锁不一定要做完整的“UWB 定位”只要做出一对一的双向测距即可。这个定位通常叫“单基站测距”也是个人 DIY 最合适的切入口。2. 自制 UWB 无感解锁系统的整体方案2.1 系统由哪几个部分组成一套完整的自制 UWB 无感解锁系统最少需要两个节点门锁端Anchor固定在门或门框附近与门锁控制器连接。因为它通常有市电供电所以可以是常听状态。随身端Tag做成卡片、钥匙扣或嵌入手机壳里由纽扣电池或锂电池供电。门锁端和随身端之间通过 UWB 报文完成双向测距。业务逻辑如下门锁端周期性发起一轮测距随身端收到请求后回复响应报文门锁端根据收发时间戳计算距离当距离小于解锁阈值且持续稳定时门锁端控制电机或电磁锁执行解锁。如果你已有智能锁也可以把门锁端电路输出的“解锁信号”接到智能锁原开关上模拟一次按键如果你只是做原型验证用继电器控制一个电磁锁即可。2.2 主控与 UWB 模块怎么选当前自制 UWB 最常用的模块是 DWM1000 模块它在内部集成了 Decawave DW1000 芯片、晶振、天线以及射频匹配电路MCU 只需要通过 SPI 接口读写寄存器就能完成测距。考虑到 STM32 资料多、上手资料丰富本文示例采用 STM32F103 系列 MCU你也可以用其他带 SPI 接口的 MCU比如 GD32、APM32、nRF52840。选择主控时重点看三点有 SPI 外设速率能达到 2 Mbps 以上有外部中断引脚可接收 DWM1000 的中断信号如果是 Tag还要考虑低功耗睡眠唤醒能力。这里需要说明DWM1000 模块对 SPI 时序、供电质量比较敏感建议优先购买自带天线的成熟模组而不是买裸芯片自己画射频电路。UWB 的天线设计和射频匹配对新手来说很难一次做成功。2.3 测距方案选型TOF、TDOA、AOA 怎么选UWB 定位常见的三种技术可以简单对比一下方案英文缩写基本思路适合场景到达时间差TDOA利用多个接收节点收到信号的时间差解算位置需要锚点间严格时间同步大面积定位需要部署多个基站到达角AOA通过天线阵列测量信号到达角度高精度寻物、汽车钥匙飞行时间/双向测距TOF / TWR两个节点之间直接测距离无感解锁、接近感应、小范围定位对于“一人一锁”的无感开门场景TOF / TWR 是最自然的方案。它不需要组网不需要额外基站只需要让 Tag 和 Anchor 完成一问一答。3. 测距原理从时间戳到真实距离3.1 单边双向测距 SS-TWR 的工作过程先看最基础的 SS-TWRSingle-Sided Two-Way Ranging过程设备 A门锁发送一条 Poll 消息并在发送瞬间记录本地时间 t1设备 BTag收到 Poll 消息后经过一个固定回复延迟 T_reply发送 Response 消息设备 A 收到 Response 消息后记录本地时间 t2。如果 T_reply 是一个已知的固定值那么信号一来一回的飞行总时间可以用下面的方式估计往返总时长 (t2 - t1 - T_reply)由于这条总时长里包含了两次空中传输时间因此单程飞行时间 ≈ 往返总时长 / 2距离 单程飞行时间 × 光速这个方案在协议实现上很简单入门时可以用它先把整个链路跑通。但它也有很明显的问题T_reply 虽然在协议上约定为固定值实际却在 Tag 的本地晶振控制下产生两个设备的晶振频率偏差会直接影响测距精度。如果 Tag 和 Anchor 的晶振偏差达到 20 ppm在几百纳秒量级的往返时间里误差可能不大但系统如果希望做到厘米级稳定度最好再升级到 DS-TWRDouble-Sided Two-Way Ranging。DS-TWR 也很容易理解就是把上面的过程扩展成三轮消息A 发 PollB 回 ResponseA 再发 FinalB 根据本地两个时间戳和 A 在 Final 里携带的时间信息用更复杂一点的平均公式抵消时钟偏移。所以如果你希望自制系统更接近商业锁的效果底层驱动建议直接使用官方 DS-TWR 例程业务代码只需关心最终算出的距离值。本文后续的示例会按“测距模块返回距离值”的视角来实现无感解锁。3.2 无线协议帧里要带哪些内容UWB 模块虽然能发数据但它终究不是像蓝牙那样自带完整应用层协议。我们需要自己约定一个简洁的报文格式。一套适合无感解锁的报文字段可以设计为字段长度说明帧头2 字节固定为 0xAA 0x55用于帧同步报文类型1 字节0x01 Poll0x02 Response0x03 Final设备 ID2 字节门锁端或 Tag 的身份标识用来过滤无关设备序号1 字节每次测距递增防止旧报文重复影响状态时间戳4 字节或 8 字节本轮报文的时间信息根据测距算法需要填写CRC 校验2 字节校验报文完整性设备 ID 的作用很重要。如果楼下邻居也做了一个 Tag或者走廊里有无关的 UWB 设备门锁端必须在协议层直接丢弃不属于自己系统的报文。否则可能出现“别人路过你家门开了”的安全事故。3.3 天线延迟和校准问题很多人第一次做 UWB 时会发现明明两个模块只有半米程序算出来却有 3 米。这种情况通常不是算法写错而是忘了做天线延迟校准。无线电信号从芯片出来到天线辐射出去需要经过板内走线、滤波器、匹配网络这些路径都会产生额外延迟。DWM1000 驱动里有单独的“天线延迟”配置项你需要拿着两个模块放在已知距离上反向测量并调整天线延迟值直到读出的距离与实际距离一致。建议每套硬件都单独校准不要直接拷贝别人工程的配置参数因为不同模块批次、不同 PCB 布局天线延迟可能不同。4. 硬件准备与开发环境搭建4.1 DIY 材料清单下面这份清单适合先跑通“一锁一钥匙”的原型件名数量用途STM32F103C8T6 最小系统板2 块一块做门锁端一块做 TagDWM1000 模块2 个UWB 通信和测距3.3V 稳压电源1 个门锁端供电建议能够提供 500mA 以上电流锂电池或 3.7V 电池3.3V LDO1 套Tag 端移动供电电磁锁或继电器模块1 个模拟门锁执行机构ST-LINK / USB 转 TTL各 1 个下载程序与调试杜邦线若干-实验初期连接模块需要特别提醒DWM1000 在发射瞬间电流会明显升高不要在 STM32 板载 LDO 后面再串一个 TTL 模块的 3.3V 输出直接给它供电尽量给 UWB 模块单独一路电源并在 VCC 和 GND 之间靠近模块引脚的位置加一个 100uF 电容避免发射瞬间电压跌落。4.2 STM32 与 DWM1000 接线参考以 STM32F103C8T6 的 SPI1 为例推荐接线如下STM32 引脚DWM1000 模块引脚说明3.3VVCC电源GNDGND共地PA5SCLKSPI 时钟PA6MISOSPI 输入PA7MOSISPI 输出PA4CS片选低电平有效PB0RSTN复位信号PB1IRQ中断请求接 STM32 外部中断不同模块的引脚名称可能略有差异接线前先查你手上模块的原理图或丝印确定 RST、IRQ、CS 是否带电平转换。4.3 开发工具与软件环境开发 IDEKeil MDK5也可以使用 STM32CubeIDE初始化代码STM32CubeMX 生成 GPIO、SPI、USART、定时器初始化代码底层驱动使用 DWM1000 官方驱动或者你自己购买的 DWM1000 模块厂商提供的 SDK串口调试助手用于观察门锁端日志和测距结果。如果你的模组是兼容 DWM1000 的国产模组通常会提供整套参考驱动先把参考工程在开发板上编译下载成功再替换为自己的业务逻辑这样最稳妥。5. 核心代码实现无感解锁最小工程因为底层 DWM1000 驱动比较庞大本教程不把寄存器读写函数完整贴出来而是采用工程实践中最常用的一层封装先通过驱动拿到距离值再在此基础上实现测距状态机和门锁状态机。下面代码基于 STM32 HAL 库与 DWM1000 模组编写函数名会尽量贴近常见 SDK但具体 API 以你手上驱动版本为准。5.1 定义距离结果结构体和无感解锁状态在app_unlock.h中定义业务数据结构#ifndef __APP_UNLOCK_H #define __APP_UNLOCK_H typedef enum { LOCK_STATE_IDLE 0, // 空闲无测距结果 LOCK_STATE_AWAY, // 检测到 Tag 在远处 LOCK_STATE_CLOSE, // 检测到 Tag 进入近场 LOCK_STATE_WAIT_CONFIRM, // 需要连续确认防抖 LOCK_STATE_UNLOCKED // 已解锁 } lock_state_t; typedef struct { float distance_m; // 测距结果单位米 uint8_t range_valid; // 本次测距是否有效 uint8_t seq; // 报文序号用于排查重复帧 } uwb_range_t; void app_unlock_init(void); void app_unlock_on_range(const uwb_range_t *range); #endifLOCK_STATE_WAIT_CONFIRM是为了解决“距离刚好在阈值附近来回跳动”的问题。只有连续多次满足进入条件系统才允许解锁。5.2 门锁端无感解锁状态机实现在app_unlock.c中实现状态切换逻辑。#include app_unlock.h #define UNLOCK_THRESHOLD_M 1.2f // 进入解锁范围阈值 #define RELOCK_THRESHOLD_M 2.0f // 离开范围后重新上锁阈值 #define CONFIRM_COUNT 3 // 连续确认次数 static lock_state_t s_state LOCK_STATE_AWAY; static uint8_t s_confirm_cnt 0; static void lock_do_unlock(void) { // 在这里控制继电器或电磁锁 // 例如HAL_GPIO_WritePin(LOCK_GPIO_Port, LOCK_Pin, GPIO_PIN_SET); // 实际量产品中应增加解锁成功反馈和日志记录 } static void lock_do_relock(void) { // 在这里执行上锁动作 s_confirm_cnt 0; } void app_unlock_on_range(const uwb_range_t *range) { if (range NULL || range-range_valid 0) { // 测距失败时不做动作也可以累计失败次数 return; } switch (s_state) { case LOCK_STATE_AWAY: if (range-distance_m UNLOCK_THRESHOLD_M) { s_confirm_cnt; if (s_confirm_cnt CONFIRM_COUNT) { s_state LOCK_STATE_UNLOCKED; lock_do_unlock(); } else { s_state LOCK_STATE_CLOSE; } } else { s_confirm_cnt 0; } break; case LOCK_STATE_CLOSE: if (range-distance_m UNLOCK_THRESHOLD_M) { s_confirm_cnt; if (s_confirm_cnt CONFIRM_COUNT) { s_state LOCK_STATE_UNLOCKED; lock_do_unlock(); } } else { // 用户走远了重新回到 AWAY s_state LOCK_STATE_AWAY; s_confirm_cnt 0; } break; case LOCK_STATE_WAIT_CONFIRM: // 该状态在多数简易场景可省略保留用于扩展“二次校验” break; case LOCK_STATE_UNLOCKED: if (range-distance_m RELOCK_THRESHOLD_M) { s_state LOCK_STATE_AWAY; lock_do_relock(); } break; default: s_state LOCK_STATE_AWAY; s_confirm_cnt 0; break; } }这段代码的核心思想是从远处进入 1.2 米以内时不立即解锁而是连续 3 次测距都满足条件才解锁解锁后如果用户已经走到 2 米以外再自动重新上锁在范围的边界上留出迟滞区间避免用户站在解锁阈值附近时门反复开锁、上锁。5.3 主程序如何驱动测距门锁端主程序大致逻辑#include main.h #include spi.h #include usart.h #include app_unlock.h #include uwb_drv.h extern UART_HandleTypeDef huart1; static uwb_range_t s_range; static void print_range(const uwb_range_t *range) { char buf[64]; snprintf(buf, sizeof(buf), dist%.3fm valid%d\r\n, range-distance_m, range-range_valid); HAL_UART_Transmit(huart1, (uint8_t *)buf, strlen(buf), 100); } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_SPI1_Init(); MX_USART1_UART_Init(); uwb_drv_init(); app_unlock_init(); while (1) { // 执行一轮测距结果写入 s_range if (uwb_drv_do_ranging(s_range) 0) { app_unlock_on_range(s_range); print_range(s_range); } HAL_Delay(50); // 根据实际场景调整测距频率 } }uwb_drv_do_ranging()是底层测距封装函数在官方 DS-TWR 例程的基础上把最终距离转换成 float 类型的距离值再通过结构体返回。这部分函数在不同模组 SDK 中的内部实现差异很大但接口可以保持不变方便后续换模组或升级底层算法。5.4 Tab 端需要实现哪些逻辑Tag 端和门锁端不同不需要跑距离判断状态机只需要监听 UWB 报文并按照协议自动回复。在低功耗要求高的场合Tag 平时尽量进入睡眠模式定时醒来监听一小段时间收到 Poll 后立刻回复 Response。简易 Tag 主程序示意如下while (1) { // 唤醒 UWB 驱动 uwb_drv_wakeup(); // 等待门锁端发来 Poll if (uwb_drv_wait_poll(100) 0) { // 收到后自动回复并携带固定应答延迟 uwb_drv_send_response(); } // 继续回到睡眠 uwb_drv_sleep(); }注意实际 DS-TWR 不只是简单的“收到 Poll 回 Response”Tag 还要根据消息类型完成多轮回复。建议直接使用官方 DS-TWR 的代码流程先把测距跑通再改低功耗睡眠策略。6. 运行验证与门锁执行机构6.1 串口观察测距结果当门锁端程序烧录成功后打开串口调试助手波特率设置为 115200正常情况下会看到类似输出dist3.214m valid1 dist3.108m valid1 dist1.856m valid1 dist0.723m valid1 dist0.521m valid1如果你拿着 Tag 从 3 米外走近门锁串口打印的距离会随之下降说明整个系统链路已经打通。6.2 控制门锁执行机构原型阶段可使用继电器模块将继电器常开端并联到电磁锁的开关两端。代码中lock_do_unlock()只需要执行一次 GPIO 拉高并持续 1 到 2 秒后自动释放即可模拟开门动作。static void lock_do_unlock(void) { HAL_GPIO_WritePin(LOCK_GPIO_Port, LOCK_Pin, GPIO_PIN_SET); // 模拟开门动作保持 1s 后释放 for (int i 0; i 20; i) { HAL_Delay(50); } HAL_GPIO_WritePin(LOCK_GPIO_Port, LOCK_Pin, GPIO_PIN_RESET); }实际智能门锁联动时不要直接去驱动原锁的电机最好通过光耦或继电器模拟门锁的“开门按键”并且要确保外部系统和门锁之间是电气隔离的。7. 常见问题与排查思路自制 UWB 无感解锁过程中很容易遇到下面几类问题建议按表格顺序排查。问题现象可能原因排查与解决思路程序下载后串口无输出MCU 串口没有初始化或串口线接反检查 TX/RX 是否交叉连接确认串口波特率和打印代码是否执行DWM1000 无法初始化SPI 引脚接错、复位时序不对、供电不足先用逻辑分析仪看 SPI 波形确保 CS、SCLK、MOSI、MISO 对应正确测距结果一直很大没有校准天线延迟在已知距离下测量并调整天线延迟配置测距值跳动很大附近有金属遮挡、模块天线朝下、环境多径严重尽量让模块天线方向面向空间避免贴身放在金属物品中走到门口了还不解锁解锁阈值太小或连续确认次数太多调大阈值或降低连续确认次数观察串口距离是否低于阈值人走了门又自动解锁重新上锁阈值和进入阈值太接近将重新上锁阈值设置为明显大于进入阈值例如进入 1.2m离开 2.0mTag 距离有偏差但很稳定两个模块的晶振偏差或天线延迟不匹配升级到 DS-TWR并分别在两块板子上分别校准天线延迟如果遇到“模块之间一直抓不到报文”的情况最有效的办法是先使用官方测距例程固定两个模块位置确认设备 ID、信道号、脉冲频率、数据速率等配置一致再回去检查自己的业务代码。8. 安全边界与实际工程建议8.1 距离阈值不能当成唯一判据UWB 测距虽然能精确测量物理距离但“物理距离近”不代表“安全身份可信”。在自制阶段把 UWB 作为无感解锁的唯一条件是可以接受的毕竟是为了学习。但如果你想把它用在真正的大门上建议至少还要加一层身份认证能力两个设备之间使用设备 ID 白名单报文加入加密校验防止有人伪造测距请求如果可能把 UWB 作为其中一种传感器配合蓝牙或密码才能解锁。同时也要了解无线电中继攻击的风险。通过专门的射频设备把两端信号“搬运”过来可以让门锁误以为授权的 Tag 就在附近。正规产品通常需要加入更完整的挑战应答机制和安全芯片来抵抗这种攻击。8.2 阈值和迟滞设计商业门锁里的“无感”体验很大程度上取决于状态控制策略做得是否舒服。建议一开始就使用双阈值解锁阈值1.2 米重新上锁阈值2.0 米。如果两个阈值设得太近用户站在门边犹豫一下系统就可能频繁执行“解锁—上锁—再解锁”不仅体验差还会缩短电磁锁或电机寿命。连续确认次数建议设置在 2 到 5 次之间过低容易误开过高会导致站门口等太久。8.3 低功耗设计思路Tag 如果做成随身卡片功耗是最关键的问题。需要注意不要一直让 DWM1000 处于接收状态接收电流会持续消耗使用 MCU 的低功耗定时器周期唤醒每 100500 毫秒醒来一次在测距失败时不要立即重试可以让模块在更长时间之后再唤醒门锁端有外部电源可以保持常听它不需要考虑复杂低功耗调度。静态功耗和动态功耗要分开测试手持万用表测量平均电流时建议使用带有积分或记录功能的万用表否则很难捕捉到 UWB 发射瞬间的电流尖峰。8.4 天线布板与安装建议UWB 测距受天线周围环境影响很大。实际安装门锁端时尽量不要把模块塞进金属门框内部因为金属屏蔽会严重削弱信号。推荐把 UWB 模块放在门内侧靠近门锁的位置并且让天线方向朝向门外走道。如果你后续要自己画 PCB记得给 UWB 电路预留足够的天线净空区天线下方所有层都不要铺铜避免影响天线辐射效率。9. 下一步学习路线做一套自制 UWB 无感解锁系统其实是把几块知识拼在一起的过程第一步先学会用 STM32 操作 DWM1000跑通官方测距例程第二步把距离数据通过串口打印出来理解 SS-TWR 和 DS-TWR 的计算差异第三步本文的无感解锁状态机可以帮你把测距结果落成门锁动作第四步进一步加入设备白名单、报文加密、低功耗唤醒策略第五步如果还想做多设备联动可以继续研究 UWB 定位中的 TDOA 组网方案。当你把“拿到稳定距离→做出合理决策→可靠执行门锁动作”这一条链路跑通以后你会对无线测距协议、时间戳同步、状态机设计、嵌入式低功耗这些概念有非常深的理解。这也是我从 UWB 无感解锁项目里学到最多的地方看似只是做一个“开门功能”实际上牵涉到的工程点远比想象中多。如果本文对你有帮助可以顺手收藏备用。等你用 STM32 UWB 打出第一版原型时欢迎回来交流你踩到的坑。