4、UWB基础测距:TWR(双边双向测距)原理、DWM1000驱动移植与初始化
这一章我们聊聊UWB测距最核心的东西——TWR。说白了,就是两个模块怎么通过来回发信号,算出它们之间的距离。我当年第一次接触UWB时,觉得这东西挺玄乎,后来把TWR的时序图画出来一看,其实没那么复杂。
4.1 TWR测距原理:为什么是“双边双向”?
先看单边双向测距(SS-TWR)。设备A发一个数据包,设备B收到后立刻回复。A记录下从发出到收到回复的时间差,再减去B的处理时间,就能算出信号在空中飞了多久。但这里有个坑——B的处理时间必须精确已知,否则误差会很大。
双边双向测距(DS-TWR)就聪明多了。它让A和B各发一次,各收一次,总共四个时间戳。这样即使B的处理时间有偏差,也能通过数学运算抵消掉。嗯,这里要注意,DS-TWR不是简单地把SS-TWR做两次,而是有专门的时序设计。
核心公式:
T_prop = (T_round1 × T_round2 - T_reply1 × T_reply2) / (T_round1 + T_round2 + T_reply1 + T_reply2)
这个公式能自动消除时钟漂移带来的误差。我在项目中实测过,用DS-TWR比SS-TWR的测距精度提升了至少40%。
4.2 DWM1000模块简介
DWM1000是Decawave(现Qorvo)推出的集成UWB模块。它把天线、射频前端、基带处理器全封装在一个小模块里。你想想看,以前做UWB测距得自己搭射频电路,现在一个模块全搞定。
| 参数 | DWM1000 | 说明 |
|---|---|---|
| 工作频段 | 3.5GHz - 6.5GHz | 支持6个信道 |
| 数据速率 | 110kbps / 850kbps / 6.8Mbps | 速率越低,灵敏度越高 |
| 测距精度 | ±10cm(理想环境) | 实际项目中一般±30cm |
| 接口 | SPI | 最高20MHz时钟 |
| 功耗 | 发射约31mA,接收约23mA | 睡眠模式可到1μA |
选型建议:如果项目对功耗敏感,可以考虑DWM1000的睡眠模式。我有个做室内定位手环的朋友,用DWM1000的睡眠+定时唤醒方案,把平均功耗压到了50μA以下。
4.3 DWM1000驱动移植:从官方库到你的MCU
官方提供了DW1000的驱动库,但那是给STM32写的。如果你用其他MCU,就得自己移植。我个人习惯先把底层SPI读写搞定,再一层层往上封装。
4.3.1 底层SPI接口
DWM1000的SPI时序有点特殊。它要求先发一个字节的命令头,再跟数据。命令头的高两位决定操作类型:读、写、或读寄存器。
// 底层SPI读写函数
static int dw1000_spi_read(uint16_t reg_addr, uint8_t *buf, uint16_t len)
{
uint8_t header[2];
// 命令头:读操作
header[0] = (uint8_t)((reg_addr & 0x7F) | 0x80); // 最高位置1表示读
header[1] = (uint8_t)((reg_addr & 0x3F80) >> 7);
// 片选拉低
dw1000_cs_low();
// 发送命令头
spi_transmit(header, 2);
// 读取数据
spi_receive(buf, len);
// 片选拉高
dw1000_cs_high();
return 0;
}
static int dw1000_spi_write(uint16_t reg_addr, uint8_t *buf, uint16_t len)
{
uint8_t header[2];
// 命令头:写操作
header[0] = (uint8_t)(reg_addr & 0x7F); // 最高位置0表示写
header[1] = (uint8_t)((reg_addr & 0x3F80) >> 7);
dw1000_cs_low();
spi_transmit(header, 2);
spi_transmit(buf, len);
dw1000_cs_high();
return 0;
}
注意:DWM1000的SPI命令头是2字节的,不是常见的1字节。我曾经有个同事移植时只发了1字节命令头,结果读出来的数据全是0xFF,排查了整整一天才发现问题。
4.3.2 初始化流程
初始化DWM1000有固定的步骤。我建议按这个顺序来,不要跳步。
- 硬件复位:拉低RSTn引脚至少10ms,再拉高。
- 等待芯片就绪:读取设备ID寄存器(0x00),确认值为0xDECA0130。
- 配置系统参数:设置信道、数据速率、脉冲重复频率等。
- 配置天线延迟:校准天线带来的信号延迟。
- 使能中断:根据需要使能接收完成、发送完成等中断。
- 进入空闲状态:调用dwt_idle()让芯片进入待命模式。
// DWM1000初始化示例
void dwm1000_init(void)
{
uint32_t dev_id;
// 1. 硬件复位
dw1000_reset();
delay_ms(20);
// 2. 软件复位
dwt_softreset();
delay_ms(10);
// 3. 检查设备ID
dev_id = dwt_readdevid();
if (dev_id != 0xDECA0130) {
// 设备异常,打印错误
printf("DWM1000 not found! ID: 0x%08X\n", dev_id);
while(1);
}
// 4. 配置默认参数
dwt_config_t config = {
.channel = 5, // 信道5:6489.6MHz
.prf = DWT_PRF_64M, // 64MHz脉冲重复频率
.txPreambleLength = DWT_PLEN_128,
.rxPAC = DWT_PAC8,
.txCode = 3,
.rxCode = 3,
.nsSFD = 1,
.dataRate = DWT_BR_6M8, // 6.8Mbps
.phrMode = DWT_PHRMODE_STD,
.sfdTO = 129
};
dwt_configure(&config);
// 5. 配置天线延迟(需根据实际硬件校准)
dwt_setantennadelay(0x4050);
// 6. 使能中断
dwt_setinterrupt(DWT_INT_RXFCG | DWT_INT_TXFRS, 1);
// 7. 进入空闲
dwt_idle();
printf("DWM1000 initialized successfully!\n");
}
4.4 天线延迟校准:一个容易被忽略的细节
天线延迟校准是测距精度的关键。DWM1000内部有一个天线延迟寄存器,默认值是0x4050。但这个值只适用于官方评估板。你换了自己的天线和PCB布局,这个值就得重新校准。
校准方法其实很简单:把两个模块放在已知距离(比如1米),然后读取测距结果。如果测出来是1.2米,说明天线延迟偏大,需要减小寄存器值。反之亦然。我一般会取10次测量的平均值,再微调寄存器值,直到误差在±5cm以内。
经验值参考:
- 官方评估板:0x4050
- 陶瓷天线+50Ω微带线:0x3F80 - 0x4100
- PCB天线+匹配网络:0x3E00 - 0x4080
注意:这些只是参考值,每块板子都要单独校准。
4.5 初始化后的自检清单
初始化完成后,我建议做一次自检,确认模块工作正常。这是我个人习惯,能省去后面很多调试时间。
| 检查项 | 方法 | 正常结果 |
|---|---|---|
| 设备ID | 读取0x00寄存器 | 0xDECA0130 |
| SPI通信 | 写一个值再读回来 | 读写一致 |
| 发送测试 | 发送一个数据包 | 发送完成中断触发 |
| 接收测试 | 用另一个模块发数据 | 接收完成中断触发 |
| 测距测试 | 两个模块相距1米 | 测距值0.95-1.05米 |
嗯,到这里DWM1000的驱动移植和初始化就完成了。你可能会问,为什么初始化要这么复杂?其实每个步骤都有它的道理。比如天线延迟校准,不做的话测距误差可能到几十厘米。我刚开始做UWB项目时也嫌麻烦,后来被现场测试数据教育了一回,从此老老实实按流程走。