项目描述
本项目基于Decawave(现已被Qorvo收购) DWM3001CDK开发板构建了一套融合超宽带(Ultra Wideband, UWB)测距与蓝牙低能耗(Bluetooth Low Energy, BLE)通信的智能感知系统,在开源社区贡献方面具有特殊意义。作者通过逆向工程修改Nordic官方SDK,首次实现了DWM3001CDK在Arduino IDE开发环境的完整支持(开源地址:https://github.com/qqice/Work-on-DWM3001CDK),这一突破性工作使得开发者能够充分利用Arduino平台丰富的开源库资源。在实际开发中,通过集成SPI显示屏驱动库、BLEPeripheral库等成熟组件,显著降低了外设兼容开发难度,使开发者可以用更简洁的代码实现复杂功能。系统采用硬件端Arduino固件与移动端微信小程序协同架构,其中硬件层负责UWB测距、IMU数据采集及显示屏控制,软件层基于JavaScript构建跨平台BLE交互界面。这种分层设计充分发挥了Arduino在实时控制方面的优势,同时利用微信小程序生态实现快速移动端部署,使系统兼具硬件实时性与软件可扩展性。
技术概念简介
本系统涉及超宽带(Ultra Wideband, UWB)测距与蓝牙低能耗(Bluetooth Low Energy, BLE)通信两大核心技术。UWB技术通过纳秒级窄脉冲实现厘米级测距精度,其双向飞行时间(Two Way Ranging, TWR)算法通过计算信号往返时间差确定距离,而到达角(Arrival of Angle, AoA)算法需多天线阵列通过相位差计算角度。BLE技术采用中心设备(Central)与外围设备(Peripheral)架构,通过服务(Service)和特征值(Characteristic)构建数据交互模型,其中特征值的通知(Notify)属性和写(Write)属性分别实现数据推送与指令接收功能。
UWB技术
Ultra Wideband(UWB)是一项基于IEEE 802.15.4A和802.15.4Z标准的无线电技术,可以非常准确地衡量无线电信号飞行时间,从而进行厘米精度的距离/位置测量。
除了这种独特的功能外,UWB还提供了数据通信能力,同时具备极低的功耗——仅使用纽扣电池便可运行数年,。通过结合准确的位置和通信,UWB还提供了一种新的在无线上安全通信的方法,从而为新形式的安全通信打开了大门。
图1 不同定位方式对比(图源:https://www.qorvo.com/innovation/ultra-wideband/technology)
根据图1的对比,我们可以看出,UWB技术在应用场景、定位精度、可靠性、定位范围、数据传输速率、安全性、延迟、功耗和成本等方面均较其他传统定位技术(如蓝牙、Wi-Fi、RFID、GPS)有明显优势,是新一代的定位技术。
TWR方法
TWR方法依赖于两个设备之间的双向通信。当它们进行交流时,设备还测量了UWB RF信号之间的飞行时间。通过将信号的往返时间乘以光速,然后除以2,您可以得出两个设备之间的实际距离。如果您在两个设备之间应用TWR方案,则将获得两个设备之间的距离(d)。根据TWR方案,您还可以通过测量移动标签和固定信标之间的距离来实现2D甚至3D位置 - 这称为三角剖分。
图2 TWR测距示意图(图源:https://www.qorvo.com/innovation/ultra-wideband/technology)
优点:
- 不需要系统同步
- 轻松部署
- 双向通信启用下行链路数据和控制
缺点:
- 随着设备使用双向通信的较高功耗 - 几个月到一年的电池寿命(用例依赖)
- 移动设备数量有限(数百个)
目标应用:
- 在家里找到丢失的物品
- 根据您的接近度锁定并解锁PC
- 用智能遥控器或手机指向并控制IoT设备
BLE技术
在低功耗蓝牙(BLE)技术框架中,Central(中心设备)与Peripheral(外围设备)的关系如同城市中的探索者与信息驿站——当你在街头漫步时,Peripheral设备就像一盏盏亮着柔和灯光的公告牌,持续向外广播着自己的存在与能力(例如"我是温度传感器"或"我提供导航服务"),而Central设备则如同手持地图的旅行者,不断扫描周围的信号。下图描述了Central(中心设备)与Peripheral(外围设备)的拓扑关系:
图3 BLE拓扑逻辑图(图源:https://learn.adafruit.com/introduction-to-bluetooth-low-energy)
当旅行者发现某个公告牌的内容符合需求时,便会驻足建立连接,此时二者的角色进一步深化:Peripheral化身为数据仓库,将传感器读数、设备状态等信息分门别类存储在"服务货架"(Service)上,每个货架又摆放着多个"特征值储物格"(Characteristic)。下图则描述了Service与Characteristic的关系:
图4 Service与Characteristic关系图(图源:https://learn.adafruit.com/introduction-to-bluetooth-low-energy)
而Central则像一位精明的采购员,既可以实时查看储物格中动态更新的数据(Notify特性),也能向特定格子中写入指令来调整设备行为,比如修改电子墨水屏的显示模式。这种关系并非单向传递——当Central向Peripheral的"可写储物格"投入新指令时,Peripheral会立即响应并执行操作,就像自动售货机在接收硬币后弹出商品,二者通过无形的数据纽带实现了精准的协同作业。
硬件介绍
DWM3001CDK开发平台
本系统核心硬件采用Decawave DWM3001CDK评估板,其搭载的DWM3001C模块集成Nordic nRF52833主控芯片与DW3110 UWB收发器。nRF52833提供64MHz Cortex-M4F处理器、512KB Flash及128KB RAM,支持蓝牙5.1双模协议。DW3110可以工作在6.5GHz频段(UWB通道5)或8GHz频段(UWB通道9),最大发射功率-8dBm/MHz,符合IEEE 802.15.4z标准。开发板内置LIS2DH12三轴加速度计(±2g量程)和温度传感器(±0.5℃精度),但受限于单天线设计,其UWB模块仅支持TWR测距无法独立实现AoA测量。实验表明,当尝试通过RSSI估算角度时,误差范围超过±30°,这验证了苹果U1芯片通过四天线阵列实现精准空间感知的技术优势。
JDI全反射显示屏
系统外接Japan Display Inc.生产的0.85英寸Memory-in-Pixel(MIP)全反射式显示屏,分辨率72*144。该屏幕采用液晶技术,在无背光条件下依靠环境光反射显示内容,待机功耗仅3μW。屏幕通过SPI接口与主控芯片通信,支持8色显示(黑白红黄蓝绿橙紫)和局部刷新功能。
功能实现
项目的设计思路是自顶向下的:先设计整个系统的功能,再逐步分解为功能模块;而实现形式则是自底向上的:先用代码实现通用的功能模块,再像搭积木一样搭建起完整的系统代码。
代码流程图
图5 代码流程图
该流程图描述了程序的主要执行流程:
1. 初始化阶段:设置各个硬件组件
2. 主循环:发送测距信号、获取数据并更新显示
3. 中断处理:接收响应信息并计算距离
4. 辅助功能:处理IMU数据、更新BLE和屏幕显示
UWB技术实现细节
系统采用TWR改进算法,在固件层实现精确时钟同步补偿。当Anchor与Tag设备建立通信后,通过三次握手协议交换时间戳:Tag发送Poll报文,Anchor回应Resp报文并记录收发时间差Δt1,Tag最终发送Final报文携带Δt1与本地计算的Δt2。距离计算公式为:d = c×[(Δt1×Δt2 - Δt2²)/2(Δt1 + Δt2)],其中c为光速。该算法有效消除时钟偏移误差,实测精度可达±10cm。由于DWM3001C模块仅配备单天线,无法通过相位差分实现AoA测量,这解释了题目2为何需要手机协同完成完整空间定位。
BLE通信架构
系统构建了包含单个服务的GATT协议栈:环境感知服务(UUID: 0x1234)包含距离、加速度、温度三组通知型特征值,还包含可写特征值用于接收显示指令。采用128位自定义UUID避免与其他服务冲突,特征值设计遵循最小数据单元原则,单个数据包封装距离(float)、三轴加速度(float×3)、温度(float),显示控制条长度(uint8_t)共21字节数据,通过位域压缩确保传输效率。
BLE功能实现流程
在实现BLE通信功能时,我的实现方式是:先设计需要通过BLE传输的数据,再在Arduino中进行初步的实现;
图6 Arduino中设计Characteristic
初步实现完成后,使用移动端的LightBlue App对每个Service和Characteristic进行分别测试,保证其内容和功能符合预期;
图7 LightBlue App——BLE设备搜索
图8 LightBlue App——列出Service和Characteristic
图9 LightBlue App——Characteristic内部数据调试
在确认功能正确后,最后再在微信小程序中进行对应功能的实现,并进行真机调试,确认功能无误。
图10 微信小程序真机调试
关键代码
本项目的代码共分为3部分:Anchor,Tag和微信小程序。其中,Anchor和Tag分别使用Arduino分别运行在一块DWM3001CDK开发板上:为简化设计流程,Tag部分使用官方SDK中的示例代码,而Anchor部分则在官方的示例代码基础上进行了优化,并添加了BLE、显示、IMU等部分。微信小程序部分则基于微信官方文档中的示例代码进行了二次开发,以测试小程序的形式先经过微信开发者工具IDE的模拟器调试后,再进一步进行真机调试。接下来选取部分关键代码进行展示:
Arduino部分
/* 经本人测试,最适合DWM3001CDK的参数值。 数值低了会导致收不到数据超时,数值高了实时性差,浪费性能。 */
#define POLL_TX_TO_RESP_RX_DLY_UUS 300
#define RESP_RX_TIMEOUT_UUS 1500
/*将UWB的信号处理部分抽取出来,单独使用中断进行实现,以提高代码运行效率和距离测定精确度。*/
void rxIRQ(){
// UART_puts("IRQ Triggered!\r\n");
uint32_t frame_len;
/* 清除DW IC状态寄存器中的良好RX帧事件。 */
dwt_write32bitreg(SYS_STATUS_ID, SYS_STATUS_RXFCG_BIT_MASK);
/* 已接收到一个帧,将其读入本地缓冲区。 */
frame_len = dwt_read32bitreg(RX_FINFO_ID) & RXFLEN_MASK;
if (frame_len <= sizeof(rx_buffer))
{
// UART_puts("RX LEN OK\r\n");
dwt_readrxdata(rx_buffer, frame_len, 0);
/* 检查帧是否是来自配套"SS TWR responder"示例的预期响应。
* 由于帧的序列号字段不相关,因此将其清除以简化帧验证。 */
rx_buffer[ALL_MSG_SN_IDX] = 0;
if (memcmp(rx_buffer, rx_resp_msg, ALL_MSG_COMMON_LEN) == 0)
{
// UART_puts("RX MSG OK\r\n");
uint32_t poll_tx_ts, resp_rx_ts, poll_rx_ts, resp_tx_ts;
int32_t rtd_init, rtd_resp;
float clockOffsetRatio ;
/* 检索轮询传输和响应接收时间戳。请参见下面的注释9。 */
poll_tx_ts = dwt_readtxtimestamplo32();
resp_rx_ts = dwt_readrxtimestamplo32();
/* 读取载波积分器值并计算时钟偏移比率。请参见下面的注释11。 */
clockOffsetRatio = ((float)dwt_readclockoffset()) / (uint32_t)(1<<26);
/* 获取响应消息中嵌入的时间戳。 */
resp_msg_get_ts(&rx_buffer[RESP_MSG_POLL_RX_TS_IDX], &poll_rx_ts);
resp_msg_get_ts(&rx_buffer[RESP_MSG_RESP_TX_TS_IDX], &resp_tx_ts);
/* 计算飞行时间和距离,使用时钟偏移比率来校正不同的本地和远程时钟速率 */
rtd_init = resp_rx_ts - poll_tx_ts;
rtd_resp = resp_tx_ts - poll_rx_ts;
tof = ((rtd_init - rtd_resp * (1 - clockOffsetRatio)) / 2.0) * DWT_TIME_UNITS;
distance = tof * SPEED_OF_LIGHT;
/* 在全反射屏上显示计算出的距离。 */
snprintf(dist_str, sizeof(dist_str), "DIST: %3.2f m", distance);
test_run_info((unsigned char *)dist_str);
currentDist = distance;
}
}
}
/*BLE初始化部分*/
void BLEInit(){
Bluefruit.autoConnLed(true);
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.begin();
Bluefruit.setTxPower(4);
Bluefruit.Periph.setConnectCallback(connect_callback);
Bluefruit.Periph.setDisconnectCallback(disconnect_callback);
startAdv();
}
void startAdv(void)
{
// Advertising packet
Bluefruit.Advertising.clearData();
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
myService.begin();
distChar.setProperties(CHR_PROPS_NOTIFY);
distChar.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
distChar.setFixedLen(4);
distChar.begin();
accelXChar.setProperties(CHR_PROPS_NOTIFY);
accelXChar.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
accelXChar.setFixedLen(4);
accelXChar.begin();
accelYChar.setProperties(CHR_PROPS_NOTIFY);
accelYChar.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
accelYChar.setFixedLen(4);
accelYChar.begin();
accelZChar.setProperties(CHR_PROPS_NOTIFY);
accelZChar.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
accelZChar.setFixedLen(4);
accelZChar.begin();
tempChar.setProperties(CHR_PROPS_NOTIFY);
tempChar.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
tempChar.setFixedLen(4);
tempChar.begin();
barChar.setProperties(CHR_PROPS_READ | CHR_PROPS_WRITE | CHR_PROPS_NOTIFY);
//barChar.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
barChar.setFixedLen(1);
barChar.begin();
barChar.write8(barLength);
Bluefruit.ScanResponse.addName();
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
Bluefruit.Advertising.start(0); // 0 = Don't stop advertising after n seconds
}
/*更新BLE和绘制界面*/
void updateBLE(){
if (connectedFlag == 1) {
distChar.notify32(float(currentDist));
accelXChar.notify32(float(accelX));
accelYChar.notify32(float(accelY));
accelZChar.notify32(float(accelZ));
tempChar.notify32(float(tempC));
barLength = barChar.read8();
barChar.notify8(barLength);
}
}
void updateScreen(){
jdi_display.fillRect(0, 0, 72, 144, COLOR_WHITE);
jdi_display.setTextColor(COLOR_BLACK);
jdi_display.setTextSize(2);
jdi_display.setCursor(5, 5);
jdi_display.printf("DIST: %2.2fm",currentDist);
jdi_display.setTextSize(1);
// X轴数据和进度条
jdi_display.setCursor(5, 40);
jdi_display.printf("X:%3.2fmG",accelX);
// 绘制X轴进度条
int barX = 36; // 屏幕中点
int barWidth = 70; // 总宽度
int barHeight = 5; // 高度
jdi_display.drawRect(barX-barWidth/2, 50, barWidth, barHeight, COLOR_BLACK);
// 计算进度条长度,范围为-2000到2000
int xBarLength = map(constrain(accelX, -2000, 2000), -2000, 2000, -barWidth/2+1, barWidth/2-1);
if (xBarLength >= 0) {
jdi_display.fillRect(barX, 51, xBarLength, barHeight-2, COLOR_RED); // 正值用红色
} else {
jdi_display.fillRect(barX+xBarLength, 51, -xBarLength, barHeight-2, COLOR_BLUE); // 负值用蓝色
}
// Y轴数据和进度条
jdi_display.setCursor(5, 60);
jdi_display.printf("Y:%3.2fmG",accelY);
// 绘制Y轴进度条
jdi_display.drawRect(barX-barWidth/2, 70, barWidth, barHeight, COLOR_BLACK);
int yBarLength = map(constrain(accelY, -2000, 2000), -2000, 2000, -barWidth/2+1, barWidth/2-1);
if (yBarLength >= 0) {
jdi_display.fillRect(barX, 71, yBarLength, barHeight-2, COLOR_MAGENTA); // 正值用紫色
} else {
jdi_display.fillRect(barX+yBarLength, 71, -yBarLength, barHeight-2, COLOR_CYAN); // 负值用青色
}
// Z轴数据和进度条
jdi_display.setCursor(5, 80);
jdi_display.printf("Z:%3.2fmG",accelZ);
// 绘制Z轴进度条
jdi_display.drawRect(barX-barWidth/2, 90, barWidth, barHeight, COLOR_BLACK);
int zBarLength = map(constrain(accelZ, -2000, 2000), -2000, 2000, -barWidth/2+1, barWidth/2-1);
if (zBarLength >= 0) {
jdi_display.fillRect(barX, 91, zBarLength, barHeight-2, COLOR_YELLOW); // 正值用黄色
} else {
jdi_display.fillRect(barX+zBarLength, 91, -zBarLength, barHeight-2, COLOR_GREEN); // 负值用绿色
}
jdi_display.setCursor(5, 100);
jdi_display.printf("T:%2.2fC",tempC);
jdi_display.setCursor(5, 120);
jdi_display.drawRect(0, 120, 72, 12, COLOR_BLACK);
jdi_display.fillRect(1, 121, barLength, 10, colors[barLength % NUMBER_COLORS]);
jdi_display.refresh();
}
微信小程序部分
// 根据指定的格式转换ArrayBuffer数据
function convertValueByFormat(buffer, format) {
switch(format) {
case 'hex':
return ab2hex(buffer);
case 'uint8':
const uint8Array = ab2uint8(buffer);
return Array.from(uint8Array).join(', ');
case 'float':
const floatArray = ab2float(buffer);
return Array.from(floatArray).join(', ');
default:
return ab2hex(buffer); // 默认使用hex格式
}
}
// 数据监听与解析
wx.onBLECharacteristicValueChange((characteristic) => {
const idx = inArray(this.data.chs, 'uuid', characteristic.characteristicId)
const data = {}
const shortUuid = characteristic.characteristicId.substring(4, 8); // 获取短UUID用于查找名称
const characteristicName = characteristicNames[shortUuid] || '未知特性';
const shouldDisplay = this.data.showAllCharacteristics || displayableCharacteristics[shortUuid];
if (shouldDisplay) {
// 获取当前显示格式或设置默认格式
const currentFormat = (idx >= 0) ? this.data.chs[idx].displayFormat : (defaultCharacteristicFormats[shortUuid] || 'hex');
if (idx === -1) {
data[`chs[${this.data.chs.length}]`] = {
uuid: characteristic.characteristicId,
shortUuid: shortUuid,
name: characteristicName,
value: convertValueByFormat(characteristic.value, currentFormat), // 使用当前格式转换值
rawValue: characteristic.value,
displayFormat: currentFormat // 保持当前格式
}
} else {
data[`chs[${idx}]`] = {
uuid: characteristic.characteristicId,
shortUuid: shortUuid,
name: characteristicName,
value: convertValueByFormat(characteristic.value, currentFormat), // 使用当前格式转换值
rawValue: characteristic.value,
displayFormat: currentFormat // 保持当前格式
}
}
this.setData(data)
}
})
},
// 控制数据写入
writeBLECharacteristicValue(value) {
// 如果没有提供参数,则使用随机值作为默认值
let uint8Value = value !== undefined ? value : (Math.random() * 255 | 0);
let buffer = new ArrayBuffer(1);
let dataView = new DataView(buffer);
dataView.setUint8(0, uint8Value);
wx.writeBLECharacteristicValue({
deviceId: this._deviceId,
serviceId: this._serviceId, // 修正原代码中的错误,使用正确的serviceId
characteristicId: this._characteristicId,
value: buffer,
});
},
<view class="connected_info" wx:if="{{connected}}">
<view>
<text>已连接到 {{name}}</text>
<view class="operation">
<button size="mini" bindtap="closeBLEConnection">断开连接</button>
</view>
</view>
<!-- 添加滑动条 -->
<view class="slider-container" wx:if="{{canWrite}}">
<text>调节数值: {{sliderValue}}</text>
<slider bindchange="onSliderChange" min="0" max="70" value="{{sliderValue}}" show-value/>
</view>
<!-- 添加特性筛选开关 -->
<view class="filter-container">
<text>特性筛选: </text>
<switch checked="{{showAllCharacteristics}}" bindchange="toggleCharacteristicFilter"/>
<text>{{showAllCharacteristics ? '显示全部' : '显示重要'}}</text>
</view>
<view wx:for="{{chs}}" wx:key="index" class="characteristic-item">
<view>特性UUID: {{item.uuid}}</view>
<view>特性名称: {{item.name}}</view>
<view class="value-container">
<text>特性值: {{item.value}}</text>
<view class="format-selector">
<text class="format-option {{item.displayFormat === 'hex' ? 'active' : ''}}"
data-index="{{index}}" data-format="hex"
bindtap="changeDisplayFormat">Hex</text>
<text class="format-option {{item.displayFormat === 'uint8' ? 'active' : ''}}"
data-index="{{index}}" data-format="uint8"
bindtap="changeDisplayFormat">Uint8</text>
<text class="format-option {{item.displayFormat === 'float' ? 'active' : ''}}"
data-index="{{index}}" data-format="float"
bindtap="changeDisplayFormat">Float</text>
</view>
</view>
</view>
</view>
功能展示
数据采集功能
首先,为Anchor上电。此时由于Tag不在线,Anchor发送的信号无法被响应,Dist(距离)值为0.00m;IMU正常运作,显示屏上将三轴加速度分别以mG为单位进行输出,并以彩色进度条的形式展示在对应数值的下面。
UWB TWR距离测量功能
接下来,为Tag上电。待Tag正确运行,Anchor与Tag之间建立通信后,Anchor的显示屏上开始显示Anchor与Tag之间通过TWR技术测定出的距离。此时Dist为0.11m,表明测量出的距离值为0.11m。
为验证距离精度,我们使用一块约30cm的PCB尺用于提供参照。可以看到,显示的距离值为0.32m,精度还是可以的。
移动端数据采集与设备控制功能
接下来,打开微信小程序,扫描Anchor设备并连接,可以看到微信小程序端可以正确接收到Anchor设备传来的各项传感器数值,并用正确的格式进行解析。
拖动“调节数值”下方的滑动输入条,观察到Anchor连接的显示屏上也作出了相应的反应。
项目中遇到的难题和解决方法
问题1:官方的Segger Embedded Studio易用性差,NRF SDK功能强大但入门门槛高
解决方法:为开发板添加Arduino支持,支持更多外设
问题2:UWB TWR例程直接运行无法建立通信,无法测定距离
解决方法:通过断点调试、打印日志等方式,结合查看DW3110的数据手册,对照程序打印的寄存器值定位问题,最终确定是设备响应超时导致的。调整等待时间参数后,问题解决。
/* 经本人测试,最适合DWM3001CDK的参数值。 数值低了会导致收不到数据超时,数值高了实时性差,浪费性能。 */
#define POLL_TX_TO_RESP_RX_DLY_UUS 300
#define RESP_RX_TIMEOUT_UUS 1500
问题3:仅使用两块板卡无法测量方位角
解决方法:通过查阅相关资料得知,UWB技术里常用的方位角测算方法是AoA,而AoA技术需要单个芯片上接复数个天线才能进行方位角测量,但DWM3001CDK上只有一块UWB天线,故而在UWB中无法通过较为简便的办法测定方位角。
心得体会
在一月初,学校组织我们参观了本地的一家法院,在参观的途中,讲解员告诉我们法院里通过一项叫做UWB的技术,能够实现来访人员在法院内的高精度三维定位,以确保来访者能够待在正确的位置,不会进入办公区域。当时我就对这项技术非常感兴趣,回到家正好看到本期Funpack里有一块UWB开发板,就果断下单研究了。虽然以前有进行过基于NRF52芯片的BLE开发,有一些理论基础,但对于UWB这项较新的技术我还是有点摸不着头脑的。网上关于UWB技术的文章也并不算多,所幸在Funpack交流群里能和群友们高强度交流,受到了很多启发,也学到了不少知识,最终完成了这个项目。也感谢硬禾和得捷电子提供的这个机会,能够免费玩DWM3001CDK这块开发板。等活动结束后,我还想在群里再收几块群友的DWM3001CDK,争取复刻出法院的UWB三维定位系统,做个小demo。