电赛C题:我用星闪+UWB做了一个数字钥匙门锁系统,开源了
前言
今年电赛C题,要求做一套数字钥匙系统------钥匙能自动识别身份,门锁能判断钥匙距离,在范围内自动解锁
说实话这题不算难,但要做得好不容易。身份识别用什么通信方案?测距怎么保证精度?怎么滤掉噪声?区域判断的状态机怎么设计?
我的方案是:星闪SLE做身份识别 + 4锚点UWB做厘米级定位 + 卡尔曼滤波平滑数据 + 三级区域状态机控制门锁
最终效果:钥匙靠近2米黄灯欢迎,1米内绿灯解锁,离开自动上锁,全程OLED实时显示距离和角度。距离误差均为5cm内,角度误差7度内
很遗憾,今年电赛我们因为一些原因失利了,但是还是决定将项目分享出来,希望能够帮助到大家
代码已开源:https://gitcode.com/gcw_rVSV2mp6/diansai-C
下面分享一下整体思路和踩过的坑
系统架构
先放一张整体架构图:
星闪 SLE 通信
┌────────────┐ ID 广播 + Notify ┌─────────────────────┐
│ 钥匙 KEY │ ◄──────────────────────► │ 门锁 LOCK │
│ (H3863) │ UUID: 0x2828/0x2929 │ (H3863) │
│ │ │ SLE / UART×2 / OLED │
└────────────┘ │ LED×3 / 蜂鸣器 │
└─────────────────────┘
▲ ▲
UART1 │ │ UART2
▼ ▼
┌──────────┐ ┌──────────┐
│ 锚点 E │ │ 锚点 G │
│ 锚点 F │ │ 锚点 H │
│ STM32 + │ │ STM32 + │
│ DW1000 │ │ DW1000 │
└──────────┘ └──────────┘
整套系统分三部分:
- 钥匙端:一块BearPi-Pico H3863,通过星闪广播身份ID,一块STM32最小系统板加DWM1000模块当作标签
- 门锁端:一块BearPi-Pico H3863,接收ID + 双UART收测距数据 + OLED显示 + LED/蜂鸣器
- UWB锚点:4个STM32+DWM1000模块,分布在不同位置做测距,测完通过串口把距离发给门锁
为什么不用H3863直接驱动DWM1000?一开始我也试过,但H3863的SPI跟DW1000配合时序有问题,我一直没有移植成功。后来改成STM32专门负责UWB测距,通过串口把结果发给H3863处理,效果稳定多了
身份识别:星闪SLE
为什么选星闪?
题目要求"身份识别",传统方案是BLE蓝牙。但BearPi-Pico H3863原生支持星闪SLE(SparkLink Edge),这是华为主推的新一代短距通信技术,比BLE延迟更低、功耗更低
而且SDK里自带SSAP协议栈,开发起来跟BLE GATT很像,上手很快
通信流程
钥匙端 (Server) 门锁端 (Client)
│ │
│ ◄── 广播 "diansai2_key" ────── │ 扫描
│ ── 连接成功 ─────────────────► │
│ ── 配对完成 ─────────────────► │
│ ── MTU 交换 (520) ────────────► │
│ ── 服务发现 ──────────────────► │
│ ── Property Handle ──────────► │
│ │
│ ── Notify ID (0x0A) ─────────► │ 每500ms
│ ── Notify ID ────────────────► │
钥匙端做Server,注册一个Service(UUID 0x2828)和一个Property(UUID 0x2929),配对完成后每500ms通过Notify推送一次ID。
门锁端做Client,扫描发现钥匙后连接、配对、服务发现,然后等着收Notify
ID校验
钥匙发送的ID是4-bit(0~15),门锁端用拨码开关设置期望ID,收到后比对:
if (received_id == expected_id):
验证通过 -> 允许解锁
else:
验证失败 -> 保持锁定
OLED上会实时显示接收ID和期望ID,以及PASS/FAIL结果
UWB测距:4锚点DS-TWR
DS-TWR双向测距原理
UWB测距用的是DS-TWR(Double-Sided Two-Way Ranging)算法,分三步:
Tag (标签) Anchor (锚点)
│ │
│── Poll 包 ──────────────────────►│ T1 发送, T2 接收
│◄── Response 包 ──────────────────│ T3 发送, T4 接收
│── Final 包 (含T1,T4,T5) ────────►│ T5 发送, T6 接收
│ │
│ 锚点根据6个时间戳计算TOF │
飞行时间计算公式:
Ra = T4 - T1 (Round 1)
Rb = T6 - T3 (Round 2)
Da = T5 - T4 (Reply 2)
Db = T3 - T2 (Reply 1)
TOF = (Ra×Rb - Da×Db) / (Ra + Rb + Da + Db)
距离 = TOF × 光速
这个公式巧妙地消除了时钟偏移误差,不需要双方时钟同步
4锚点轮询
标签端不是只跟一个锚点测距,而是轮询4个锚点(E→F→G→H→E...),每个锚点独立测距,通过串口输出:
E D:0.56
F D:1.23
G D:0.89
H D:2.34
门锁端用两个UART并行接收:
- UART1 收锚点E、F
- UART2 收锚点G、H
锚点布局
60cm
E ┌──────────┐ G
│ │
│ │ 60cm
│ │
F └──────────┘ H
定位算法:从测距到坐标
卡尔曼滤波
UWB测距有噪声,直接用会跳得很厉害。每个锚点独立维护一个卡尔曼滤波器:
预测:
p_pred = p_est + Q (Q = 0.05)
更新:
K = p_pred / (p_pred + R) (R = 0.1)
x_est = x_est + K × (测量值 - x_est)
p_est = (1 - K) × p_pred
Q越大越相信测量值,R越大越相信预测值。0.05和0.1是我反复调试出来的参数
野值剔除
卡尔曼滤波能平滑小噪声,但对大跳变无能为力。所以加了一层野值剔除:
if |测量值 - 估计值| > 50cm:
用估计值替代,连续拒绝计数+1
if 连续拒绝 > 5次:
接受新值(强制重置,防止卡死)
else:
正常卡尔曼更新
50cm阈值和5次限制是经验值,太小会误杀正常值,太大会让噪声通过。
坐标解算
有了4个锚点的滤波后距离,怎么算位置?
用的是差分平方法,不需要解非线性方程,计算量小:
x = (d_G² - d_E²) / (2L) (L=60cm, 2L=120)
y = (d_H² - d_F²) / (2L)
原理很简单:如果G和E距离相等,x=0;如果离G更近,x>0。平方差正好消了距离的常数项
径向距离
坐标x,y算出来后,径向距离:
avg = (d_E + d_F + d_G + d_H) / 4
r = (avg - 61.0) / 1.105
61.0和1.105是实测校准值------在已知距离点采样,拟合出来的零偏和缩放系数。不同硬件需要重新标定
方位角
angle = -atan2(y, x) × (180/π) - 125°
-125°是锚点布局的安装偏移,跟实际摆放方向有关。
角度也做了指数平滑(α=0.3),并且处理了360°环绕问题:
diff = new_angle - old_angle
if diff > 180: diff -= 360
if diff < -180: diff += 360
smooth += 0.3 × diff
区域状态机
距离算好了,接下来是区域判断。分三级:
距离 < 1m
┌──────────┐ ─────────────► ┌──────────┐
│ SENSING │ │ UNLOCK │
│ (红灯) │ ◄───────────── │ (绿灯) │
└──────────┘ 距离 >= 1m └──────────┘
│
│ 距离 < 2m
▼
┌──────────┐
│ WELCOME │
│ (黄灯) │
└──────────┘
| 区域 | 距离 | LED | 蜂鸣器 |
|---|---|---|---|
| 解锁区 | < 1.0m | 绿灯 | 短响100ms |
| 欢迎区 | < 2.0m | 黄灯 | 长响200ms |
| 感知区 | >= 2.0m | 红灯 | - |
关键细节:只有ID验证通过才会解锁。如果ID不匹配,不管多近都是红灯锁定。
事件驱动蜂鸣器
蜂鸣器不是一直响,而是区域变化时才响:
- 进入解锁区 → 短响
- 离开解锁区 → 短响
- 进入欢迎区 → 长响
- 离开欢迎区 → 长响
这样不会太吵,但每次状态变化用户都能感知到。
OLED显示
门锁端有一块0.96寸SSD1306 OLED,用软件I2C驱动(因为硬件I2C被占用了)
显示内容
扫描中:
SLE: Scanning...
Exp: b0000
Key ID: ----
Zone: ----
已连接:
Key:b1010 Exp:b0000
Ver: PASS
Dist:1.23m A:+4.5
Zone: WELCOME
Event: Enter Welc
Hrz:1.53m
Status: WELCOME
第一行同时显示接收ID和期望ID的二进制,第二行PASS/FAIL,第三行距离和角度,第四行区域,第五行事件,第六行水平距离,第七行状态。
6x8字体,8行显示,信息密度刚刚好
字库
字库是自己做的6x8点阵,95个ASCII字符,每个6字节,一共570字节。直接写在头文件里,编译时写死到Flash
RTOS任务架构
门锁端跑LiteOS,两个并行任务:
┌─ Diansai2Lock (优先级 17) ──────────┐
│ · SLE Client 扫描/连接/配对 │
│ · 接收 ID + DIP 校验 │
│ · OLED 刷新 (200ms) │
│ · LED/蜂鸣器控制 │
│ · 区域判断 + 状态机 │
└────────────────────────────────────┘
┌─ Diansai2LockDist (优先级 15) ──────┐
│ · UART1 接收锚点 E/F (115200) │
│ · UART2 接收锚点 G/H (115200) │
│ · 解析 "X D:距离" 协议 │
│ · 卡尔曼滤波 (每锚点独立) │
│ · 野值剔除 │
│ · 径向距离 + 方位角计算 │
└────────────────────────────────────┘
Lock任务优先级高一点(17),因为身份识别和状态控制的实时性要求更高。Dist任务优先级15,测距数据有一定的容忍度,晚几十毫秒没关系。
两个任务通过全局变量通信(g_distance_cm、g_angle等),简单粗暴但有效
踩过的坑
1. H3863直接驱动DW1000时序不稳
一开始想用H3863的SPI直接驱动DW1000,但是SPI程序一直没有移植成功
解决方案:改用STM32专门驱动DW1000,测距完成后串口把距离发给H3863
2. UWB测距跳变
DW1000测距偶尔会跳一下,比如实际1米突然报5米。卡尔曼滤波能平滑小波动,但对这种大跳变没用
解决方案:加了野值剔除层,偏差超过50cm的测量值直接丢弃,用上一次的估计值替代。连续丢弃5次后才强制接受新值,防止滤波器卡死
3. 角度跳变
角度在±180°附近会突然跳变(比如从179°跳到-179°),普通的指数平滑会把这个跳变放大
解决方案:做平滑前先处理环绕:
diff = new - old
if diff > 180: diff -= 360
if diff < -180: diff += 360
smooth += α × diff
硬件清单
| 器件 | 数量 | 角色 |
|---|---|---|
| BearPi-Pico H3863 | 2 | 钥匙 + 门锁 |
| STM32F103 + DW1000 | 5 | 标签,UWB锚点 E/F/G/H |
| SSD1306 0.96" OLED | 1 | 门锁显示 |
| LED 红/绿/黄 | 3 | 状态指示 |
| 有源蜂鸣器 | 1 | 事件提示 |
| 4位拨码开关 | 2 | ID设置 |
主控是Hi3863,RISC-V 32bit,240MHz,集成Wi-Fi6/BLE/星闪SLE,606KB SRAM,4MB Flash。跑LiteOS实时操作系统
代码结构
diansai2/
├── README.md # 项目文档
├── LICENSE # CC BY-NC-SA 4.0
├── diansai2.h # 公共定义
├── diansai2_key.c # 钥匙端:SLE广播 + ID Notify
├── diansai2_lock.c # 门锁端:SLE接收 + UART测距 + 卡尔曼 + OLED + 状态机
├── diansai2_oled.c/.h # SSD1306软件I2C驱动
├── diansai2_oled_font.h # 6x8点阵字库
├── CMakeLists.txt # 构建配置
├── Kconfig # 引脚配置
└── stm32_uwb/ # STM32 UWB测距固件
├── 标签/ # Tag固件(轮询4锚点)
└── 基座/ # Anchor固件(测距+UART输出)
开源
项目地址:https://gitcode.com/gcw_rVSV2mp6/diansai-C
硬件开源地址: 2026电赛C题-小熊派解决方案 - 立创开源硬件平台
包含:
- H3863钥匙端+门锁端完整固件
- STM32 UWB标签+锚点完整固件(含DW1000驱动)
- README文档 + 项目介绍
许可证:CC BY-NC-SA 4.0(署名-非商业性使用-相同方式共享)
可以学习、研究、修改、分享,但不能商用,衍生作品也要用同样的许可证
总结
这个项目几个关键设计决策:
- 星闪SLE替代BLE:新技术尝鲜,延迟更低,SDK好用
- STM32+H3863架构分离:UWB测距交给STM32,H3863专注通信和逻辑
- 多级滤波:卡尔曼+野值剔除,两层防护
- 差分平方定位:计算量小,不需要解非线性方程
- 事件驱动状态机:蜂鸣器只在区域变化时响,不吵
做电赛最大的收获不是最后的结果,而是踩坑的过程。UWB时序、卡尔曼调参、角度环绕、OLED花屏......每个坑都花了不少时间,但解决之后对整个系统的理解会深很多
希望这篇分享对大家有帮助,代码已经开源,欢迎交流
本文项目开源地址:https://gitcode.com/gcw_rVSV2mp6/diansai-C
许可证:CC BY-NC-SA 4.0(禁止商用)