引言:UWB 技术与 PX4 飞控的 “精准联姻”
在无人机应用中,定位精度直接决定任务成败 —— 从室内仓储巡检的厘米级路径跟踪,到室外编队飞行的协同避障,再到农业植保的精准航线规划,传统 GPS 在室内、遮挡环境下的 “失效” 问题,让超宽带(UWB)技术成为关键补充。UWB 凭借厘米级定位精度、抗多径干扰强、低功耗三大核心优势,可完美填补 GPS / 北斗的覆盖盲区,为 PX4 飞控系统提供全场景定位能力。

PX4 作为开源无人机飞控的 “事实标准”,支持多类型传感器与执行器的灵活集成,但针对主流 UWB 芯片(如 NXP SR150、Qorvo DW3000)的官方适配仍需开发者深入探索。本文聚焦两大核心需求:
- NXP Trimension™ SR150 在 PX4 中的兼容实现:从 NuttX 实时操作系统驱动开发,到 PX4 飞控的软件集成与测试;
- Qorvo DW3000 在 PX4 中的完整开发流程:涵盖硬件连接、NuttX 驱动适配、PX4 应用层开发与定位数据融合。
全文以 “理论 + 实战” 为核心,用表格简化复杂参数与步骤,既适合新手入门 UWB 与 PX4 的集成逻辑,也为资深开发者提供驱动优化与问题排查的参考方案。
第一部分:基础认知铺垫 ——PX4、NuttX 与 UWB 核心原理
在深入驱动开发前,需先理清三大核心组件的底层逻辑:PX4 飞控的架构、NuttX 的驱动模型、UWB 的定位原理。这部分内容是后续实战的 “地基”,即使是零基础读者也能通过表格化对比快速理解。
1.1 PX4 飞控系统:开源飞控的 “生态骨架”
PX4 并非单一 “硬件” 或 “软件”,而是一套硬件无关的开源飞控生态,支持从微型无人机(如穿越机)到工业级无人机(如多旋翼、固定翼)的全场景适配。其核心价值在于 “模块化设计” 与 “多传感器融合能力”,可轻松集成 GPS、IMU、视觉、UWB 等设备。
1.1.1 PX4 软件架构:四层模块化结构
PX4 软件从底层到上层分为 “驱动层→中间件层→应用层→地面站交互层”,各层职责清晰,且通过uORB 通信协议实现跨层数据交互(uORB 是 PX4 的 “数据总线”,类似 ROS 的 Topic,支持传感器数据、控制指令的实时传输)。
|
层级 |
核心功能 |
关键模块 / 组件 |
与 UWB 相关的交互点 |
|
驱动层 |
硬件设备的底层控制(如 SPI/I2C 通信、中断) |
传感器驱动(imu、gps、uwb)、总线驱动 |
UWB 芯片的 NuttX 驱动需挂载至此层 |
|
中间件层 |
数据融合、时间同步、参数管理 |
uORB、传感器融合(EKF2)、参数服务器 |
UWB 定位数据通过 uORB 发布,供 EKF2 融合 |
|
应用层 |
飞行控制逻辑、任务规划 |
姿态控制、位置控制、任务调度器 |
读取 UWB 数据,修正位置控制指令 |
|
地面站交互层 |
固件烧录、参数配置、日志查看 |
MAVLink 协议、QGroundControl(QGC) |
通过 MAVLink 将 UWB 定位精度、状态上传 QGC |
表 1-1:PX4 软件四层架构与 UWB 的交互关系
1.1.2 PX4 核心通信协议:uORB—— 飞控的 “数据总线”
uORB 是 PX4 的 “神经中枢”,所有传感器数据(如 IMU 的角速度、UWB 的位置)都需通过 uORB“发布(Publish)”,再由需要该数据的模块 “订阅(Subscribe)”。例如:
- UWB 驱动在驱动层 “发布”uwb_position主题;
- 中间件层的 EKF2 模块 “订阅” 该主题,与 IMU、GPS 数据融合,生成更精准的位置估计;
- 应用层的位置控制模块 “订阅” 融合后的位置数据,生成电机控制指令。
uORB 的核心优势是 “低延迟”(实时性≤1ms)与 “轻量化”(仅占用少量内存),完全适配无人机对实时控制的需求。
1.1.3 PX4 硬件适配:主流飞控板与 UWB 接口
PX4 支持的飞控板(如 Pixhawk 4、FMUv5、CUAV v5+)均提供标准化的 “扩展接口”,可直接连接 UWB 模块。UWB 芯片通常通过SPI 总线与 PX4 通信(少数支持 I2C),因为 SPI 的 “高速同步通信” 特性更适合 UWB 的高频定位数据传输(如 10Hz~100Hz 的定位频率)。
|
主流 PX4 飞控板 |
支持的 UWB 接口 |
接口引脚定义(以 SPI 为例) |
最大通信速率 |
供电电压 |
|
Pixhawk 4 |
SPI2(扩展接口) |
SCK: PB13, MISO: PB14, MOSI: PB15, CS: PB12 |
20MHz |
3.3V |
|
FMUv5 |
SPI1(内部)+ SPI2 |
SCK: PA5, MISO: PA6, MOSI: PA7, CS: PA4 |
20MHz |
3.3V |
|
CUAV v5+ |
SPI2(GPS 接口复用) |
SCK: PB13, MISO: PB14, MOSI: PB15, CS: PC13 |
20MHz |
3.3V |
|
Holybro Kakute H7 |
SPI3 |
SCK: PC10, MISO: PC11, MOSI: PC12, CS: PD2 |
16MHz |
3.3V |
表 1-2:主流 PX4 飞控板的 UWB 接口参数
1.2 NuttX 实时操作系统:PX4 的 “底层引擎”
PX4 的底层操作系统是NuttX(部分硬件支持 Linux,但主流飞控仍以 NuttX 为主),其核心特性是 “实时性强”(任务调度延迟≤100us)、“可裁剪”(适合资源受限的嵌入式设备)、“驱动模型统一”(简化硬件适配)。
1.2.1 NuttX 核心特性:为何成为 PX4 的首选 OS?
相比其他实时操作系统(如 FreeRTOS、RT-Thread),NuttX 在无人机场景下的优势的体现在 “兼容性” 与 “工业级稳定性”:
|
操作系统 |
实时性(调度延迟) |
内存占用(最小) |
驱动模型 |
与 PX4 适配度 |
优势场景 |
|
NuttX |
≤100us |
~20KB |
统一 VFS 接口 |
★★★★★ |
资源受限的飞控设备 |
|
FreeRTOS |
≤50us |
~10KB |
无统一驱动模型 |
★★★☆☆ |
自定义硬件的快速开发 |
|
RT-Thread |
≤80us |
~15KB |
设备对象模型 |
★★★★☆ |
国产硬件适配 |
|
Linux |
≥1ms |
~1MB |
标准 Linux 驱动 |
★★☆☆☆ |
高性能计算(如视觉处理) |
表 1-3:主流 RTOS 与 NuttX 的特性对比
1.2.2 NuttX 驱动模型:UWB 驱动的 “设计规则”
NuttX 采用类 Linux 的驱动模型,所有硬件设备都被抽象为 “文件”,通过标准的文件操作接口(open()、read()、write()、ioctl())实现控制 —— 这意味着 UWB 驱动开发无需从零编写接口,只需遵循 NuttX 的驱动框架即可。
NuttX 驱动主要分为三类,UWB 芯片属于 “字符设备驱动”(数据以字节流形式传输,如串口、传感器):
|
驱动类型 |
适用设备 |
核心接口函数 |
UWB 驱动适配点 |
|
字符设备驱动 |
传感器、串口、UWB |
open(), read(), write(), ioctl() |
UWB 的定位数据读取、参数配置 |
|
块设备驱动 |
存储设备(SD 卡、Flash) |
read(), write(), ioctl() |
无(UWB 无需块存储) |
|
网络设备驱动 |
网卡(Wi-Fi、Ethernet) |
socket()接口 |
无(UWB 用 SPI 而非网络) |
表 1-4:NuttX 驱动类型与 UWB 适配关系
字符设备驱动的核心是 “驱动注册”—— 需将自定义的file_operations结构体(包含open/read等函数指针)注册到 NuttX 的设备管理器,后续应用层即可通过/dev/uwb0这类设备文件访问 UWB 芯片。
1.3 UWB 技术原理:从 “信号” 到 “位置” 的转化
UWB(超宽带)技术的核心是通过 “纳秒级窄脉冲信号” 实现高精度测距,再结合定位算法(如 TWR、TDoA、AoA)计算设备位置。理解这一过程,是后续驱动开发中 “数据解析” 的关键。
1.3.1 UWB 定位算法:三种核心方案对比
不同 UWB 芯片支持的定位算法不同(如 SR150 支持 3D AoA,DW3000 支持 TWR/TDoA),需根据应用场景选择:
|
定位算法 |
核心原理 |
所需设备数量 |
定位精度 |
适用场景 |
代表芯片支持 |
|
TWR(双向测距) |
两个设备通过信号往返时间计算距离 |
2 个(标签 + 锚点) |
≤10cm |
点对点定位(如无人机跟拍) |
SR150、DW3000 |
|
TDoA(到达时间差) |
多个锚点接收同一标签信号,通过时间差定位 |
≥3 个锚点 + 1 个标签 |
≤5cm |
大范围区域定位(如仓储) |
DW3000 |
|
AoA(到达角) |
通过天线阵列计算信号入射角度,结合距离定位 |
1 个锚点(多天线)+1 个标签 |
≤3°(角度) |
方向感知(如避障) |
SR150 |
表 1-5:UWB 三大定位算法对比
1.3.2 SR150 与 DW3000:核心参数与定位能力差异
本文聚焦的两款 UWB 芯片,在硬件特性与定位能力上有显著差异,直接决定了驱动开发的侧重点:
|
参数 |
NXP SR150 |
Qorvo DW3000 |
对驱动开发的影响 |
|
标准兼容 |
IEEE 802.15.4z + FiRa 1.0 |
IEEE 802.15.4z + FiRa 1.0 |
需适配 FiRa 协议栈的 MAC 层指令 |
|
定位算法 |
TWR + 3D AoA(方位角 + 仰角) |
TWR + TDoA(2D/3D) |
SR150 需处理角度数据,DW3000 需多锚点同步 |
|
工作频段 |
6.5-8.5 GHz(频道 5/9) |
6.5 GHz(频道 5)、8 GHz(频道 9) |
驱动需支持频段切换寄存器配置 |
|
通信接口 |
SPI(最高 10MHz) + I2C(可选) |
SPI(最高 38MHz) |
SR150 需兼容双总线,DW3000 需高 SPI 速率 |
|
功耗 |
接收 22mA,休眠 < 0.3μA |
接收 24mA,休眠 < 0.5μA |
驱动需实现低功耗模式切换 |
|
集成度 |
内置 Cortex-M33 + eSE(安全元件) |
无内置 MCU,需外部主控 |
SR150 可独立运行算法,DW3000 需 PX4 控制 |
|
苹果 U1 兼容性 |
支持(固件 v3.14.0+) |
不支持(需 DW3210) |
SR150 驱动需处理苹果生态的协议适配 |
表 1-6:SR150 与 DW3000 核心参数对比
第二部分:NXP SR150 在 PX4 中的兼容实现 —— 从 NuttX 驱动到集成测试
SR150 作为 NXP 第二代 UWB 芯片,其核心优势是 “3D AoA 角度测量” 与 “苹果 U1 兼容”,适合无人机的 “避障 + 定位” 双模需求。本节将从 “硬件连接→NuttX 驱动开发→PX4 集成→测试验证” 四个步骤,完整呈现 SR150 在 PX4 中的兼容过程。
2.1 硬件准备:SR150 模块与 PX4 飞控的连接
首先需明确 SR150 的硬件接口与 PX4 飞控的匹配关系 ——SR150 通常以 “模块形式” 存在(如 NXP 官方的 MKW41Z+SR150 评估板,或第三方模块如 SparkFun SR150 Breakout),需通过 SPI 总线与 PX4 连接,并注意供电电压(3.3V,不可接 5V)。
2.1.1 SR150 模块硬件接口定义
以 “SparkFun SR150 UWB 模块” 为例,其接口包含 SPI、I2C、中断、复位等引脚,与 PX4 飞控的连接需聚焦 “SPI + 中断 + 复位”(I2C 可选,用于低速率配置):
|
SR150 模块引脚 |
功能描述 |
连接到 PX4 飞控的引脚(以 Pixhawk 4 为例) |
备注 |
|
VDD |
电源输入 |
3.3V(扩展接口的 3.3V 引脚) |
电流最大 50mA,需确保飞控供电稳定 |
|
GND |
地 |
GND(扩展接口的 GND 引脚) |
必须共地,避免信号干扰 |
|
SCK |
SPI 时钟线 |
PB13(SPI2_SCK) |
同步通信时钟,速率最高 10MHz |
|
MISO |
SPI 主机输入 / 从机输出 |
PB14(SPI2_MISO) |
SR150 向 PX4 传输数据(如定位结果) |
|
MOSI |
SPI 主机输出 / 从机输入 |
PB15(SPI2_MOSI) |
PX4 向 SR150 发送配置指令(如频段切换) |
|
CS |
SPI 片选线 |
PB12(SPI2_CS) |
低电平有效,选中 SR150 进行通信 |
|
INT |
中断输出(从机→主机) |
PC4(外部中断引脚) |
SR150 有新定位数据时触发中断 |
|
RST |
复位输入(低电平复位) |
PC5(GPIO 输出引脚) |
PX4 可通过该引脚复位 SR150 |
|
SDA |
I2C 数据线(可选) |
PB9(I2C1_SDA) |
仅用于低速率配置,优先用 SPI |
|
SCL |
I2C 时钟线(可选) |
PB8(I2C1_SCL) |
与 SDA 配合使用 |
表 2-1:SR150 模块与 Pixhawk 4 的引脚连接表
2.1.2 硬件连接注意事项
- 信号干扰防控:UWB 对射频干扰敏感,SPI 线缆需尽量短(≤10cm),且避免与电源线、电机控制线并行布线;
- 复位引脚配置:SR150 上电后需复位才能正常工作,PX4 需在驱动初始化时拉低 RST 引脚 100ms,再拉高;
- 中断引脚电平:SR150 的 INT 引脚为 “高电平触发”,需在 PX4 的 GPIO 配置中设置中断触发方式为 “上升沿触发”;
- 供电稳定性:若 SR150 与其他高功耗设备(如激光雷达)共用 3.3V 电源,需添加 100μF 电容滤波,避免电压波动。
2.2 NuttX 驱动开发:SR150 的底层控制实现
NuttX 驱动是 SR150 与 PX4 的 “桥梁”—— 需实现 “硬件初始化→SPI 通信→中断处理→数据解析→驱动注册” 五大核心功能。本节以 “字符设备驱动” 框架为例,逐步骤讲解代码实现。
2.2.1 驱动开发环境搭建
首先需准备 PX4 与 NuttX 的开发环境,确保能编译驱动代码并烧录到飞控:
|
环境组件 |
版本要求 |
安装步骤(Ubuntu 22.04 为例) |
|
PX4 源码 |
v1.14.0+ |
git clone https://github.com/PX4/PX4-Autopilot.git && cd PX4-Autopilot && git checkout v1.14.0 |
|
NuttX 源码(PX4 内置) |
9.1.0+ |
PX4 源码已集成,路径:PX4-Autopilot/src/nuttx |
|
交叉编译工具链 |
arm-none-eabi-gcc 10.3.1 |
sudo apt install gcc-arm-none-eabi |
|
调试工具 |
JLink v7.80+ |
下载安装 JLink 软件,用于驱动调试 |
|
串口工具 |
screen/minicom |
sudo apt install screen,用于查看调试日志 |
表 2-2:SR150 驱动开发环境配置表
2.2.2 驱动核心结构:从 “头文件定义” 到 “函数实现”
SR150 的 NuttX 驱动代码需放在 PX4 源码的src/drivers/uwb/nxp_sr150/目录下,核心文件包括:
- sr150.h:寄存器定义、数据结构、函数声明;
- sr150.c:驱动核心逻辑(初始化、SPI 通信、中断处理);
- Kconfig:驱动编译配置选项(如是否启用 SR150);
- Make.defs:编译规则定义。
步骤 1:头文件sr150.h—— 定义硬件与数据结构
首先需定义 SR150 的关键寄存器(用于配置频段、定位模式、中断)与数据结构(存储设备状态、定位结果):
// sr150.h 关键内容节选
#include <nuttx/config.h>
#include <nuttx/fs/fs.h>
#include <nuttx/gpio/gpio.h>
#include <nuttx/spi/spi.h>
// 1. SR150关键寄存器地址
#define SR150_REG_MODE 0x00 // 工作模式寄存器
#define SR150_REG_FREQ 0x01 // 频段配置寄存器
#define SR150_REG_INT_EN 0x02 // 中断使能寄存器
#define SR150_REG_INT_STAT 0x03 // 中断状态寄存器
#define SR150_REG_RANGE_DATA 0x04 // 测距数据寄存器(距离)
#define SR150_REG_AOA_AZIMUTH 0x08 // AoA方位角寄存器(-180°~180°)
#define SR150_REG_AOA_ELEVATION 0x0A // AoA仰角寄存器(-90°~90°)
#define SR150_REG_RESET 0x10 // 复位寄存器
// 2. 工作模式枚举(对应REG_MODE寄存器值)
enum sr150_mode {
SR150_MODE_SLEEP = 0x00, // 休眠模式
SR150_MODE_STANDBY = 0x01, // 待机模式
SR150_MODE_TWR = 0x02, // TWR定位模式
SR150_MODE_AOA = 0x03 // AoA定位模式
};
// 3. 频段枚举(对应REG_FREQ寄存器值)
enum sr150_freq {
SR150_FREQ_CH5 = 0x05, // 6.5 GHz(频道5)
SR150_FREQ_CH9 = 0x09 // 8.0 GHz(频道9)
};
// 4. 定位结果数据结构(存储UWB输出的位置信息)
struct sr150_position {
float distance; // 距离(单位:m)
float azimuth; // 方位角(单位:°,-180~180)
float elevation; // 仰角(单位:°,-90~90)
uint8_t quality; // 定位质量(0~100,越高越准)
uint32_t timestamp; // 时间戳(ms,来自PX4系统时间)
};
// 5. 设备私有数据结构(存储SR150的状态)
struct sr150_dev_s {
struct spi_dev_s *spi; // SPI设备句柄
uint32_t cs_pin; // SPI片选引脚
uint32_t int_pin; // 中断引脚
uint32_t rst_pin; // 复位引脚
enum sr150_mode mode; // 当前工作模式
enum sr150_freq freq; // 当前频段
struct sr150_position pos; // 最新定位结果
sem_t sem; // 信号量(保护数据访问)
bool data_ready; // 数据就绪标志(中断触发后置位)
};
// 6. 驱动函数声明
int sr150_init(FAR struct spi_dev_s *spi, uint32_t cs_pin, uint32_t int_pin, uint32_t rst_pin);
int sr150_set_mode(FAR struct sr150_dev_s *dev, enum sr150_mode mode);
int sr150_get_position(FAR struct sr150_dev_s *dev, FAR struct sr150_position *pos);
步骤 2:SPI 通信实现 —— 读写 SR150 寄存器
SR150 的所有配置与数据读取都通过 SPI 实现,需封装sr150_spi_write_reg(写寄存器)与sr150_spi_read_reg(读寄存器)两个基础函数:
// sr150.c 中SPI通信函数
#include "sr150.h"
// 写SR150寄存器:reg_addr=寄存器地址,data=要写入的数据,len=数据长度
static int sr150_spi_write_reg(FAR struct sr150_dev_s *dev, uint8_t reg_addr,
FAR const uint8_t *data, uint8_t len) {
int ret;
uint8_t tx_buf[len + 1]; // 第一个字节是寄存器地址(写操作:地址最高位为0)
// 1. 构造SPI发送数据:地址+数据
tx_buf[0] = reg_addr & 0x7F; // 写操作:最高位清0
memcpy(&tx_buf[1], data, len);
// 2. 锁定SPI总线,避免多线程冲突
SPI_LOCK(dev->spi, true);
// 3. 拉低片选,选中SR150
gpio_set_value(dev->cs_pin, false);
// 4. 发送数据
ret = SPI_SEND(dev->spi, tx_buf, len + 1);
// 5. 拉高片选,释放SR150
gpio_set_value(dev->cs_pin, true);
// 6. 解锁SPI总线
SPI_LOCK(dev->spi, false);
return ret;
}
// 读SR150寄存器:reg_addr=寄存器地址,data=存储读取数据的缓冲区,len=数据长度
static int sr150_spi_read_reg(FAR struct sr150_dev_s *dev, uint8_t reg_addr,
FAR uint8_t *data, uint8_t len) {
int ret;
uint8_t tx_buf[1]; // 发送寄存器地址(读操作:地址最高位为1)
// 1. 构造SPI发送地址:读操作,最高位置1
tx_buf[0] = reg_addr | 0x80;
// 2. 锁定SPI总线
SPI_LOCK(dev->spi, true);
// 3. 拉低片选
gpio_set_value(dev->cs_pin, false);
// 4. 发送寄存器地址
ret = SPI_SEND(dev->spi, tx_buf, 1);
if (ret < 0) {
goto out;
}
// 5. 读取寄存器数据
ret = SPI_RECV(dev->spi, data, len);
out:
// 6. 拉高片选,解锁SPI总线
gpio_set_value(dev->cs_pin, true);
SPI_LOCK(dev->spi, false);
return ret;
}
关键注意点:
- SR150 的 SPI 读写通过 “寄存器地址最高位” 区分:写操作(最高位 0)、读操作(最高位 1);
- 每次 SPI 通信前需 “锁定总线”(SPI_LOCK),避免多线程(如 PX4 的任务)同时操作 SPI 导致数据错乱;
- 片选引脚(CS)需严格遵循 “低电平选中、高电平释放” 的时序,否则通信会失败。
步骤 3:设备初始化 ——sr150_init函数
初始化是驱动的 “启动入口”,需完成 “复位 SR150→配置 GPIO→初始化 SPI→配置寄存器→使能中断” 五大操作:
// sr150.c 中初始化函数
int sr150_init(FAR struct spi_dev_s *spi, uint32_t cs_pin, uint32_t int_pin, uint32_t rst_pin) {
struct sr150_dev_s *dev;
int ret;
uint8_t init_data;
// 1. 分配设备私有数据内存
dev = kmm_zalloc(sizeof(struct sr150_dev_s));
if (dev == NULL) {
return -ENOMEM; // 内存分配失败
}
// 2. 初始化SPI设备句柄与引脚
dev->spi = spi;
dev->cs_pin = cs_pin;
dev->int_pin = int_pin;
dev->rst_pin = rst_pin;
// 3. 配置GPIO引脚(CS、INT、RST)
// 3.1 CS引脚:推挽输出,初始高电平
gpio_configure_pin(cs_pin, GPIO_OUTPUT | GPIO_PUSHPULL | GPIO_DRIVE_HIGH);
gpio_set_value(cs_pin, true);
// 3.2 RST引脚:推挽输出,先复位SR150
gpio_configure_pin(rst_pin, GPIO_OUTPUT | GPIO_PUSHPULL | GPIO_DRIVE_HIGH);
gpio_set_value(rst_pin, false); // 拉低复位
nxsig_usleep(100000); // 保持复位100ms
gpio_set_value(rst_pin, true); // 释放复位
nxsig_usleep(50000); // 等待SR150启动(50ms)
// 3.3 INT引脚:输入,上升沿中断
gpio_configure_pin(int_pin, GPIO_INPUT | GPIO_INT_RISING);
// 注册中断服务函数(sr150_int_handler),后文实现
ret = irq_attach(gpio_pin2irq(int_pin), sr150_int_handler, dev);
if (ret < 0) {
kmm_free(dev);
return ret;
}
// 使能中断
irq_enable(gpio_pin2irq(int_pin));
// 4. 配置SR150寄存器(默认模式:TWR+频道9)
dev->mode = SR150_MODE_TWR;
dev->freq = SR150_FREQ_CH9;
// 4.1 配置工作模式(TWR)
init_data = dev->mode;
ret = sr150_spi_write_reg(dev, SR150_REG_MODE, &init_data, 1);
if (ret < 0) {
goto fail;
}
// 4.2 配置频段(频道9,8GHz)
init_data = dev->freq;
ret = sr150_spi_write_reg(dev, SR150_REG_FREQ, &init_data, 1);
if (ret < 0) {
goto fail;
}
// 4.3 使能“定位数据就绪”中断
init_data = 0x01; // 仅使能数据就绪中断
ret = sr150_spi_write_reg(dev, SR150_REG_INT_EN, &init_data, 1);
if (ret < 0) {
goto fail;
}
// 5. 初始化信号量(保护定位数据访问)
nxsem_init(&dev->sem, 0, 1); // 二值信号量,初始值1
dev->data_ready = false;
// 6. 将设备私有数据保存到SPI设备的私有数据字段(供后续调用)
SPI_SETPRIV(spi, dev);
return 0;
fail:
irq_detach(gpio_pin2irq(int_pin));
kmm_free(dev);
return ret;
}
步骤 4:中断处理 ——sr150_int_handler函数
SR150 在 “定位数据就绪” 时会触发 INT 引脚中断,需在中断服务函数中 “读取中断状态→清除中断标志→标记数据就绪”:
// sr150.c 中中断服务函数
static int sr150_int_handler(int irq, FAR void *context, FAR void *arg) {
struct sr150_dev_s *dev = (struct sr150_dev_s *)arg;
uint8_t int_stat;
// 1. 读取中断状态寄存器
sr150_spi_read_reg(dev, SR150_REG_INT_STAT, &int_stat, 1);
// 2. 判断是否为“数据就绪”中断
if (int_stat & 0x01) {
// 2.1 清除中断标志(写1清除)
uint8_t clear_data = 0x01;
sr150_spi_write_reg(dev, SR150_REG_INT_STAT, &clear_data, 1);
// 2.2 标记数据就绪(供应用层读取)
dev->data_ready = true;
// (可选)在中断中预读取定位数据,减少应用层延迟
sr150_read_position_from_reg(dev);
}
return OK;
}
// 辅助函数:从寄存器读取定位数据并存储到dev->pos
static void sr150_read_position_from_reg(FAR struct sr150_dev_s *dev) {
uint8_t buf[4]; // 存储寄存器读取的原始数据
// 1. 读取距离数据(REG_RANGE_DATA,4字节,小端模式,单位:mm)
sr150_spi_read_reg(dev, SR150_REG_RANGE_DATA, buf, 4);
uint32_t distance_mm = (buf[3] << 24) | (buf[2] << 16) | (buf[1] << 8) | buf[0];
dev->pos.distance = distance_mm / 1000.0f; // 转换为米
// 2. 读取方位角(REG_AOA_AZIMUTH,2字节,有符号,单位:0.1°)
sr150_spi_read_reg(dev, SR150_REG_AOA_AZIMUTH, buf, 2);
int16_t azimuth_01deg = (buf[1] << 8) | buf[0];
dev->pos.azimuth = azimuth_01deg / 10.0f; // 转换为度
// 3. 读取仰角(REG_AOA_ELEVATION,2字节,有符号,单位:0.1°)
sr150_spi_read_reg(dev, SR150_REG_AOA_ELEVATION, buf, 2);
int16_t elevation_01deg = (buf[1] << 8) | buf[0];
dev->pos.elevation = elevation_01deg / 10.0f; // 转换为度
// 4. 读取定位质量(简化:此处假设质量寄存器为1字节,实际需查手册)
sr150_spi_read_reg(dev, SR150_REG_QUALITY, &buf[0], 1);
dev->pos.quality = buf[0];
// 5. 记录时间戳(来自PX4的系统时间,单位:ms)
dev->pos.timestamp = px4_clock_get_ms();
}
中断处理注意事项:
- 中断服务函数需 “轻量化”,避免耗时操作(如复杂计算),否则会影响 PX4 的实时控制;
- 中断标志需 “写 1 清除”(SR150 的硬件特性),否则会持续触发中断;
- 定位数据的 “字节序” 需注意:SR150 默认小端模式,需转换为 PX4 的主机字节序(通常为小端,无需额外处理,但需确认手册)。
步骤 5:字符设备接口封装 ——open/read/ioctl
NuttX 的字符设备驱动需实现file_operations结构体,将驱动函数映射为标准文件操作接口,供 PX4 应用层调用:
// sr150.c 中字符设备接口实现
static int sr150_open(FAR struct file *filep) {
// 打开设备时无需额外操作(初始化已在驱动加载时完成)
return OK;
}
static int sr150_close(FAR struct file *filep) {
// 关闭设备时可将SR150切换到休眠模式,降低功耗
FAR struct inode *inode = filep->f_inode;
FAR struct sr150_dev_s *dev = inode->i_private;
uint8_t sleep_mode = SR150_MODE_SLEEP;
sr150_spi_write_reg(dev, SR150_REG_MODE, &sleep_mode, 1);
return OK;
}
static ssize_t sr150_read(FAR struct file *filep, FAR char *buf, size_t len) {
FAR struct inode *inode = filep->f_inode;
FAR struct sr150_dev_s *dev = inode->i_private;
struct sr150_position pos;
int ret;
// 1. 检查读取长度是否匹配定位数据结构大小
if (len != sizeof(struct sr150_position)) {
return -EINVAL; // 长度不匹配
}
// 2. 等待数据就绪(超时100ms)
uint32_t timeout = 100;
while (!dev->data_ready && timeout-- > 0) {
nxsig_usleep(1000); // 延迟1ms
}
if (!dev->data_ready) {
return -ETIMEDOUT; // 超时无数据
}
// 3. 锁定信号量,读取定位数据(避免中断修改数据)
nxsem_wait(&dev->sem);
pos = dev->pos;
dev->data_ready = false; // 重置数据就绪标志
nxsem_post(&dev->sem);
// 4. 将数据拷贝到应用层缓冲区
ret = memcpy(buf, &pos, sizeof(struct sr150_position));
if (ret != OK) {
return -EIO;
}
return sizeof(struct sr150_position); // 返回读取的字节数
}
static int sr150_ioctl(FAR struct file *filep, int cmd, unsigned long arg) {
FAR struct inode *inode = filep->f_inode;
FAR struct sr150_dev_s *dev = inode->i_private;
int ret = OK;
// 根据不同的cmd执行不同操作(如切换模式、配置频段)
switch (cmd) {
case SR150_IOCTL_SET_MODE: // 切换工作模式
ret = sr150_set_mode(dev, (enum sr150_mode)arg);
break;
case SR150_IOCTL_SET_FREQ: // 配置频段
ret = sr150_set_freq(dev, (enum sr150_freq)arg);
break;
case SR150_IOCTL_GET_QUALITY: // 获取定位质量
nxsem_wait(&dev->sem);
*(uint8_t *)arg = dev->pos.quality;
nxsem_post(&dev->sem);
break;
default:
ret = -ENOTTY; // 不支持的cmd
break;
}
return ret;
}
// 定义file_operations结构体,映射驱动函数
static const struct file_operations sr150_fops = {
.open = sr150_open,
.close = sr150_close,
.read = sr150_read,
.ioctl = sr150_ioctl,
.write = NULL, // SR150无需写数据(配置通过ioctl)
.seek = NULL, // 无seek需求
};
步骤 6:驱动注册 —— 将驱动挂载到 NuttX 设备节点
最后需实现sr150_register函数,将驱动注册为 NuttX 的字符设备(如/dev/uwb0),供 PX4 应用层访问:
// sr150.c 中驱动注册函数
int sr150_register(FAR const char *devpath, FAR struct spi_dev_s *spi,
uint32_t cs_pin, uint32_t int_pin, uint32_t rst_pin) {
int ret;
FAR struct sr150_dev_s *dev;
// 1. 初始化SR150设备
ret = sr150_init(spi, cs_pin, int_pin, rst_pin);
if (ret < 0) {
return ret;
}
// 2. 获取设备私有数据(已在sr150_init中保存到SPI设备)
dev = SPI_GETPRIV(spi);
// 3. 注册字符设备:devpath=设备节点路径(如"/dev/uwb0")
ret = register_chardev(0, devpath, &sr150_fops, dev);
if (ret < 0) {
kmm_free(dev);
return ret;
}
PX4_INFO("SR150 UWB driver registered at %s", devpath);
return OK;
}
2.2.3 驱动编译配置:Kconfig 与 Make.defs
为让 PX4 编译系统识别 SR150 驱动,需添加编译配置文件:
1. Kconfig文件 —— 编译选项配置
# src/drivers/uwb/nxp_sr150/Kconfig
config DRIVER_UWB_NXP_SR150
bool "NXP SR150 UWB Driver"
default n
depends on SPI
help
Enable support for the NXP SR150 UWB chip.
Requires SPI bus support.
该配置允许用户在编译 PX4 固件时,通过make menuconfig选择是否启用 SR150 驱动(DRIVER_UWB_NXP_SR150=y)。
2. Make.defs文件 —— 编译规则定义
# src/drivers/uwb/nxp_sr150/Make.defs
ifeq ($(CONFIG_DRIVER_UWB_NXP_SR150),y)
CSRCS += sr150.c
endif
该文件告诉 PX4 编译系统:若启用 SR150 驱动,则编译sr150.c文件。
2.3 PX4 应用层集成:从驱动到 uORB 主题
NuttX 驱动实现了 SR150 的底层控制,PX4 应用层需通过 “设备文件” 读取定位数据,并封装为 uORB 主题发布,供 EKF2、位置控制等模块使用。
2.3.1 创建 PX4 驱动应用:sr150_uwb.cpp
PX4 应用层代码需放在src/drivers/uwb/nxp_sr150/目录下,核心功能是 “打开设备文件→循环读取定位数据→发布 uORB 主题”:
// sr150_uwb.cpp
#include <px4_platform_common/px4_config.h>
#include <px4_platform_common/posix.h>
#include <px4_platform_common/log.h>
#include <drivers/drv_hrt.h>
#include <uORB/topics/uwb_position.h> // 自定义uORB主题(需提前定义)
#include "sr150.h"
// 自定义uORB主题:存储UWB定位数据(需在msg/uwb_position.msg中定义)
// msg/uwb_position.msg内容:
// float32 distance # 距离(m)
// float32 azimuth # 方位角(°)
// float32 elevation # 仰角(°)
// uint8 quality # 定位质量(0~100)
// uint64 timestamp # 时间戳(us)
extern "C" {
// 从NuttX驱动头文件引入函数声明
int sr150_register(const char *devpath, void *spi,
uint32_t cs_pin, uint32_t int_pin, uint32_t rst_pin);
}
class SR150UWB
{
public:
SR150UWB() : _dev_fd(-1) {}
~SR150UWB() { close(_dev_fd); }
// 初始化:注册驱动+打开设备文件
int init() {
// 1. 获取SPI设备句柄(以SPI2为例,对应Pixhawk 4的扩展SPI)
void *spi_dev = px4_spi_bus_initialize(2); // 2=SPI2
if (spi_dev == nullptr) {
PX4_ERR("SPI2 bus initialize failed");
return -1;
}
// 2. 注册SR150驱动(设备节点:/dev/uwb0)
int ret = sr150_register("/dev/uwb0", spi_dev,
PX4_GPIO_PB12, // CS引脚:PB12
PX4_GPIO_PC4, // INT引脚:PC4
PX4_GPIO_PC5); // RST引脚:PC5
if (ret != 0) {
PX4_ERR("SR150 register failed: %d", ret);
return ret;
}
// 3. 打开设备文件
_dev_fd = open("/dev/uwb0", O_RDONLY);
if (_dev_fd < 0) {
PX4_ERR("Open /dev/uwb0 failed: %d", errno);
return -errno;
}
// 4. 配置SR150为AoA模式(通过ioctl)
enum sr150_mode mode = SR150_MODE_AOA;
ret = ioctl(_dev_fd, SR150_IOCTL_SET_MODE, (unsigned long)mode);
if (ret != 0) {
PX4_ERR("Set AoA mode failed: %d", ret);
return ret;
}
PX4_INFO("SR150 UWB initialized successfully");
return 0;
}
// 循环运行:读取数据并发布uORB主题
void run() {
struct sr150_position pos; // 驱动返回的定位数据
struct uwb_position_s uwb_pos; // uORB主题数据
memset(&uwb_pos, 0, sizeof(uwb_pos));
while (!_should_exit) {
// 1. 读取定位数据(阻塞直到有数据,或超时)
ssize_t len = read(_dev_fd, &pos, sizeof(pos));
if (len != sizeof(pos)) {
PX4_WARN("Read UWB data failed: len=%d, errno=%d", len, errno);
px4_usleep(10000); // 延迟10ms重试
continue;
}
// 2. 填充uORB主题数据
uwb_pos.distance = pos.distance;
uwb_pos.azimuth = pos.azimuth;
uwb_pos.elevation = pos.elevation;
uwb_pos.quality = pos.quality;
uwb_pos.timestamp = hrt_absolute_time(); // PX4高精度时间戳(us)
// 3. 发布uORB主题
if (!_uwb_pos_pub.initialized()) {
_uwb_pos_pub.advertise();
}
_uwb_pos_pub.publish(uwb_pos);
// 4. 控制循环频率(10Hz,可根据需求调整)
px4_usleep(100000); // 延迟100ms
}
}
void stop() { _should_exit = true; }
private:
int _dev_fd; // 设备文件描述符
bool _should_exit = false; // 退出标志
uORB::Publication<uwb_position_s> _uwb_pos_pub{ORB_ID(uwb_position)}; // uORB发布者
};
// PX4模块入口函数
extern "C" __EXPORT int sr150_uwb_main(int argc, char *argv[]) {
SR150UWB uwb;
// 解析命令行参数(如"start"、"stop"、"status")
if (argc == 2 && strcmp(argv[1], "start") == 0) {
int ret = uwb.init();
if (ret == 0) {
uwb.run();
}
return ret;
} else if (argc == 2 && strcmp(argv[1], "stop") == 0) {
uwb.stop();
return 0;
} else if (argc == 2 && strcmp(argv[1], "status") == 0) {
PX4_INFO("SR150 UWB status: running");
return 0;
}
PX4_INFO("Usage: sr150_uwb {start|stop|status}");
return 1;
}
2.3.2 定义 uORB 主题:uwb_position.msg
需在 PX4 的msg/目录下添加uwb_position.msg文件,定义 UWB 定位数据的格式(供 uORB 工具生成代码):
# msg/uwb_position.msg
float32 distance # 距离(单位:m)
float32 azimuth # 方位角(单位:°,范围-180~180)
float32 elevation # 仰角(单位:°,范围-90~90)
uint8 quality # 定位质量(0~100,值越高精度越高)
uint64 timestamp # 时间戳(单位:us,来自hrt_absolute_time())
# 元数据(可选)
uint8 ORB_QUEUE_LENGTH = 4
添加后需在msg/CMakeLists.txt中加入该文件,确保编译时生成 uORB 相关代码:
# msg/CMakeLists.txt
set(msg_files
# ... 其他msg文件 ...
uwb_position.msg
)
2.3.3 配置 PX4 编译:CMakeLists.txt
为让 PX4 编译系统识别应用层代码,需在src/drivers/uwb/nxp_sr150/目录下添加CMakeLists.txt:
# src/drivers/uwb/nxp_sr150/CMakeLists.txt
px4_add_module(
MODULE drivers__uwb__nxp_sr150
MAIN sr150_uwb
STACK_MAIN 2048
SRCS
sr150_uwb.cpp
sr150.c
INCLUDES
.
DEPENDS
px4_platform_common
px4_driver_framework
uorb
COMPILE_FLAGS
-std=c99
)
该配置指定:
- 模块名称:drivers__uwb__nxp_sr150;
- 主函数所在文件:sr150_uwb.cpp;
- 依赖的库:px4_platform_common(PX4 基础库)、uorb(uORB 通信库);
- 编译标准:C99(适配 NuttX 驱动的 C 代码)。
2.4 兼容性测试与问题排查
SR150 驱动与 PX4 集成后,需通过 “硬件测试→功能测试→性能测试” 验证兼容性,常见问题需针对性排查。
2.4.1 测试环境与工具
|
测试环节 |
工具 / 设备 |
测试目的 |
|
硬件连接测试 |
万用表、逻辑分析仪 |
验证 SPI 引脚通断、中断信号是否正常 |
|
驱动加载测试 |
PX4 Shell(串口) |
查看驱动是否成功注册、设备文件是否存在 |
|
数据读取测试 |
QGroundControl(QGC)日志查看 |
验证 UWB 定位数据是否正确发布到 uORB 主题 |
|
定位精度测试 |
激光测距仪、坐标纸 |
对比 UWB 测量距离与实际距离,评估精度 |
|
稳定性测试 |
长时间运行(24 小时) |
验证驱动是否存在内存泄漏、通信中断问题 |
表 2-3:SR150 兼容性测试工具与目的
2.4.2 测试步骤与预期结果
步骤 1:硬件连接测试
- 用万用表测量 SR150 模块与 PX4 飞控的引脚通断(如 SCK、MISO、GND),确保无虚焊;
- 用逻辑分析仪抓取 SPI 通信波形(上电后),预期:CS 引脚拉低时,SCK 有时钟信号,MOSI 有配置指令发送;
- 观察 INT 引脚电平:SR150 启动后,每 100ms(定位频率 10Hz)应产生一次上升沿。
步骤 2:驱动加载测试
- 编译 PX4 固件(以 Pixhawk 4 为例):
make px4_fmu-v5_default
- 烧录固件到飞控,通过串口连接 PX4 Shell(波特率 57600);
- 执行sr150_uwb start启动驱动,预期输出:
SR150 UWB initialized successfully;
- 执行ls /dev/查看设备文件,预期存在/dev/uwb0;
- 执行uorb top查看 uORB 主题,预期uwb_position主题有数据更新(刷新率 10Hz)。
步骤 3:数据读取与精度测试
- 在 QGC 中打开 “日志查看器”,筛选uwb_position主题,记录距离、方位角数据;
- 将 SR150 模块(标签)与锚点分别固定在距离 1m、2m、3m 的位置,用激光测距仪测量实际距离;
- 对比 UWB 测量距离与实际距离,预期误差≤±10cm(符合 SR150 的定位精度);
- 旋转标签模块,观察方位角变化,预期与实际旋转角度一致(误差≤±3°)。
步骤 4:稳定性测试
- 让 SR150 驱动持续运行 24 小时,期间记录 uORB 主题的更新频率;
- 检查 PX4 飞控的内存使用(free命令),预期内存无持续增长(排除内存泄漏);
- 观察是否有通信中断(如uwb_position主题停止更新),预期无中断。
2.4.3 常见问题与解决方案
|
问题现象 |
可能原因 |
解决方案 |
|
驱动注册失败(sr150_register返回 - 1) |
SPI 总线未初始化;CS 引脚配置错误 |
1. 确认 SPI 总线编号正确(如 Pixhawk 4 是 SPI2);2. 检查 CS 引脚是否为推挽输出 |
|
读取数据超时(read返回 - ETIMEDOUT) |
中断引脚未触发;SR150 未配置为正确模式 |
1. 用逻辑分析仪查看 INT 引脚是否有上升沿;2. 通过 ioctl 重新配置为 TWR/AoA 模式 |
|
定位数据误差大(>20cm) |
频段选择错误;天线校准未做 |
1. 切换到频道 9(8GHz)避开干扰;2. 执行 SR150 天线校准(参考 NXP 手册) |
|
驱动运行中内存泄漏 |
中断处理中未释放信号量;动态内存未回收 |
1. 检查nxsem_post是否在nxsem_wait后调用;2. 排查kmm_zalloc是否有对应的kmm_free |
|
uORB 主题无数据(uorb top无更新) |
应用层未调用advertise;驱动未发布数据 |
1. 确认_uwb_pos_pub.advertise()已调用;2. 检查read函数是否成功读取数据 |
表 2-4:SR150 集成常见问题与解决方案
第三部分:Qorvo DW3000 在 PX4 中的开发实战 —— 从硬件到定位融合
Qorvo DW3000 作为工业级 UWB 芯片,其核心优势是 “TDoA 大范围定位” 与 “高抗干扰能力”,适合无人机编队飞行、仓储巡检等需要多锚点协同的场景。本节从 “硬件连接→NuttX 驱动→PX4 应用层→定位融合” 四个维度,完整呈现 DW3000 的开发流程。
3.1 硬件准备:DW3000 模块与 PX4 的连接
DW3000 通常以 “芯片 + 外围电路” 或 “模块形式” 存在(如 Qorvo 官方的 DWM3000 模块),需通过 SPI 总线与 PX4 连接,且需注意 “高频 SPI 通信” 的信号完整性(DW3000 支持最高 38MHz SPI 速率,高于 SR150 的 10MHz)。
3.1.1 DW3000 模块硬件接口定义
以 “Qorvo DWM3000 模块” 为例,其接口包含 SPI、中断、复位、时钟等引脚,与 PX4 飞控的连接需聚焦 “SPI + 中断 + 复位 + 外部时钟”(外部时钟用于保证测距精度):
|
DWM3000 模块引脚 |
功能描述 |
连接到 PX4 飞控的引脚(以 Pixhawk 4 为例) |
备注 |
|
VDD |
电源输入 |
3.3V(扩展接口的 3.3V 引脚) |
电流最大 60mA,需添加 100μF 滤波电容 |
|
GND |
地 |
GND(扩展接口的 GND 引脚) |
必须共地,避免射频干扰 |
|
SCK |
SPI 时钟线 |
PB13(SPI2_SCK) |
速率最高 38MHz,需短距离布线(≤5cm) |
|
MISO |
SPI 主机输入 / 从机输出 |
PB14(SPI2_MISO) |
DW3000 向 PX4 传输定位数据 |
|
MOSI |
SPI 主机输出 / 从机输入 |
PB15(SPI2_MOSI) |
PX4 向 DW3000 发送配置指令 |
|
CS |
SPI 片选线 |
PB12(SPI2_CS) |
低电平有效,选中 DW3000 进行通信 |
|
INT |
中断输出(从机→主机) |
PC4(外部中断引脚) |
数据就绪或错误时触发中断 |
|
RST |
复位输入(低电平复位) |
PC5(GPIO 输出引脚) |
PX4 可复位 DW3000 |
|
XTAL_IN |
外部时钟输入(可选) |
PC6(GPIO 输出,提供 32MHz 时钟) |
可选,用外部时钟提升测距精度 |
|
XTAL_OUT |
时钟输出(可选) |
悬空或连接到示波器(测试用) |
无需连接到 PX4 |
表 3-1:DWM3000 模块与 Pixhawk 4 的引脚连接表
3.1.2 硬件设计关键注意事项
- SPI 速率与布线:DW3000 支持最高 38MHz SPI 速率,但 PX4 飞控的 SPI 控制器(如 STM32F7 的 SPI2)最大速率为 20MHz,需在驱动中配置为 20MHz;SPI 线缆需用阻抗匹配线(50Ω),且长度≤5cm,避免信号反射;
- 外部时钟配置:若需最高精度,可让 PX4 通过 GPIO 输出 32MHz 时钟(需用定时器生成),连接到 DW3000 的 XTAL_IN 引脚;若无外部时钟,DW3000 将使用内置 32MHz 晶振(精度 ±20ppm);
- 射频干扰防控:DW3000 的天线需远离 PX4 的 GPS 天线(≥10cm),避免射频干扰;模块周围需预留至少 5mm 的净空区,确保天线辐射性能;
- 供电设计:DW3000 在发射模式下电流峰值达 45mA,需在模块电源引脚附近放置 0.1μF 和 10μF 电容,抑制电压波动。
3.2 NuttX 驱动开发:DW3000 的底层控制
DW3000 的 NuttX 驱动开发与 SR150 类似,均基于 “字符设备驱动” 框架,但需适配 DW3000 的特殊寄存器(如 TDoA 配置、多锚点同步)与测距流程。驱动代码放在src/drivers/uwb/qorvo_dw3000/目录下,核心文件包括dw3000.h、dw3000.c、Kconfig、CMakeLists.txt。
3.2.1 头文件dw3000.h—— 寄存器与数据结构定义
首先定义 DW3000 的关键寄存器(用于配置定位模式、频段、TDoA 参数)与数据结构(存储定位结果、设备状态):
// dw3000.h 关键内容节选
#include <nuttx/config.h>
#include <nuttx/fs/fs.h>
#include <nuttx/gpio/gpio.h>
#include <nuttx/spi/spi.h>
// 1. DW3000关键寄存器地址(参考Qorvo DW3000数据手册)
#define DW3000_REG_ID 0x00 // 芯片ID寄存器(用于识别芯片)
#define DW3000_REG_MODE 0x01 // 工作模式寄存器
#define DW3000_REG_FREQ 0x02 // 频段配置寄存器
#define DW3000_REG_TDOA_CFG 0x03 // TDoA配置寄存器(锚点/标签角色)
#define DW3000_REG_INT_EN 0x04 // 中断使能寄存器
#define DW3000_REG_INT_STAT 0x05 // 中断状态寄存器
#define DW3000_REG_TDOA_DATA 0x06 // TDoA定位数据寄存器(x/y/z坐标)
#define DW3000_REG_RANGE_DATA 0x0A // 测距数据寄存器(距离)
#define DW3000_REG_RESET 0x10 // 复位寄存器
// 2. 工作模式枚举(对应REG_MODE)
enum dw3000_mode {
DW3000_MODE_SLEEP = 0x00, // 休眠模式
DW3000_MODE_STANDBY = 0x01, // 待机模式
DW3000_MODE_TWR = 0x02, // TWR模式(点对点测距)
DW3000_MODE_TDOA_TAG = 0x03, // TDoA标签模式(无人机端)
DW3000_MODE_TDOA_ANCHOR</doubaocanvas>

2263

被折叠的 条评论
为什么被折叠?



