CH32 + DW1000 DS-TWR 测距工程技术手册
1. 文档说明
本文档面向当前 CH32V307 / CH32V108 + DW1000 双角色测距工程的软件结构、启动流程、DS-TWR 空口交互、RX/TX 代码路径、持久化配置、串口配置工具、网络上报方式以及构建产物。本文档的目标读者包括研发工程师、集成工程师、调试工程师以及交付工程师。
当前分支实现的是标准 DS-TWR 测距链路,按编译宏区分 RX_NODE 与 TX_NODE。其中:
RX_NODE作为应答端,负责接收POLL、发送ACK、接收FINAL并计算距离TX_NODE作为发起端,负责发送POLL、等待ACK、发送延迟FINALCH32V307在具备以太网能力时,可将测距结果通过TCP以字符串形式上报CH32V307 / CH32V108均支持AT24C02持久化参数存储OLED与UART在当前工程中均已接入运行时显示与调试输出
2. 工程介绍
当前工程采用 DW1000 作为 UWB 收发芯片,采用 CH32V307 与 CH32V108 作为主控平台,构成一套双角色 DS-TWR 测距系统。工程支持在两个 MCU 平台上分别编译为 RX 或 TX 节点,支持 OLED 显示、串口调试、AT24C02 持久化配置以及 CH32V307 上的 TCP 上报能力。测距流程完整覆盖了 POLL -> ACK -> FINAL 三帧交互、时间戳采集、距离计算、结果缓存、前后台分工处理和对外输出。当前分支同时集成了串口配置工具,可以通过 show / set / save / defaults / eeprom_test 对设备参数进行读写和持久化验证。综合来看,这条分支已经不是单纯的底层示例,而是一套具备工程化测距、网络链路和配置管理能力的 DW1000 测距固件版本。
3. 系统组成
3.1 硬件角色
| 角色 | 主控 | UWB器件 | 主要职责 |
|---|---|---|---|
RX_NODE |
CH32V307 或 CH32V108 |
DW1000 |
接收 POLL、发送 ACK、接收 FINAL、计算距离 |
TX_NODE |
CH32V307 或 CH32V108 |
DW1000 |
发送 POLL、等待 ACK、发送延迟 FINAL、接收距离结果 |
3.2 软件模块
| 模块 | 文件 | 主要职责 |
|---|---|---|
| 系统入口 | src/main.c |
启动顺序控制与角色分流 |
| 通用 UWB 初始化 | vendor/BP-UWB_LIB/Src/bphero_uwb.c |
DW1000 初始化、帧头更新、短地址应用、发射功率配置 |
| RX 测距主流程 | vendor/BP-UWB_LIB/Src/rx_main.c |
应答端协议处理、距离计算、OLED/UART/TCP 输出 |
| TX 测距主流程 | vendor/BP-UWB_LIB/Src/tx_main.c |
发起端协议处理、周期测距、延迟 FINAL 发送、结果输出 |
| 持久化配置核心 | vendor/User/app_config.c |
EEPROM 读写、命令解析、运行时配置应用 |
| EEPROM 驱动 | vendor/User/at24c02.c |
AT24C02 初始化、读写、自检 |
| 网络客户端 | vendor/User/tcpclinet_init.c |
WCHNET 初始化与 TCP 上报 |
| 串口配置工具 | tools/run_serial_config_tool.py / tools/serial_config_gui.py |
PC 侧串口参数配置与 EEPROM 自检 |
4. 总体设计方案
当前工程采用典型的“公共底层 + 角色协议逻辑 + 配置层 + 上报层”设计。
主要设计要点如下:
- 通过编译宏决定固件角色,不在运行时动态切换角色。
- 通过
app_config在上电时加载 EEPROM 配置,并在运行时同步到 UWB 地址和网络参数。 - 通过
bphero_uwb.c统一管理DW1000初始化、帧头、地址更新和发射功率设置。 - 通过
tx_main.c与rx_main.c分别实现DS-TWR发起端与应答端逻辑。 - 通过
tcpclinet_init.c在CH32V307平台上提供 TCP 遥测能力。
这一架构的特点是:
- UWB 初始化与协议状态机分离
- 持久化配置与业务逻辑分离
- 网络层与测距主链路解耦
- 便于在
CH32V307 / CH32V108两个平台上统一维护
5. 启动流程
5.1 主启动链路
系统入口位于 src/main.c,启动顺序如下:
SystemCoreClockUpdate()Delay_Init()USART_Printf_Init(115200)- 打印欢迎信息、主频、芯片 ID
GPIO_LED_INIT()uwb_led_init()app_config_init()tcpclient_init()uwb_init()- 根据编译角色进入:
RX_NODE -> rx_main()TX_NODE -> tx_main()
5.2 启动流程图
flowchart TD
A["系统上电 / 复位"] --> B["main()"]
B --> C["SystemCoreClockUpdate()"]
C --> D["Delay_Init()"]
D --> E["USART_Printf_Init(115200)"]
E --> F["GPIO_LED_INIT() / uwb_led_init()"]
F --> G["app_config_init()"]
G --> H["tcpclient_init()"]
H --> I["uwb_init()"]
I --> J{"编译角色"}
J -->|RX_NODE| K["rx_main()"]
J -->|TX_NODE| L["tx_main()"]
6. 持久化配置系统
6.1 存储模型
当前分支使用 app_config 作为配置块,并从 EEPROM 地址 0x00 开始读写。配置结构包括:
magicversionmcu_typenode_rolepan_idshort_addrpoll_interval_msdest_addrlocal_ipgateway_ipsubnet_maskserver_ipserver_portchecksum
配置合法性校验条件:
magic匹配version匹配mcu_type与当前主控一致node_role与当前角色一致checksum正确
任一条件不满足时,系统回退到默认配置。
6.2 串口命令集合
当前串口命令解析位于 vendor/User/app_config.c,统一支持:
showset <key> <value>savedefaultseeprom_test
不同角色支持的键值不同。
无以太网 RX
short_addrpan_id
有以太网 RX
short_addrpan_idlocal_ipgateway_ipsubnet_maskserver_ipserver_port
无以太网 TX
short_addrpan_idpoll_msdest_addr
有以太网 TX
short_addrpan_idpoll_msdest_addrlocal_ipgateway_ipsubnet_maskserver_ipserver_port
6.3 地址策略
当前代码对短地址做了角色相关校验:
- Anchor 地址范围:
0x0001 ~ 0x00FF - Tag 地址范围:
0x0101 ~ 0x01FF
但 TX 角色为了兼容历史默认地址,也接受低地址段配置。因此:
RX必须落在 Anchor 地址段TX推荐使用 Tag 地址段,但历史低地址也可读取与保存
6.4 配置初始化流程图
flowchart TD
A["app_config_init()"] --> B["加载默认值"]
B --> C["初始化 AT24C02"]
C --> D["从 EEPROM 0x00 读取配置"]
D --> E{"配置有效?"}
E -->|否| F["保留默认值"]
E -->|是| G["加载存储配置"]
F --> H["finalize 配置"]
G --> H
H --> I["应用网络运行时配置"]
I --> J["打印 CLI ready 与配置"]
7. DW1000 初始化
7.1 默认无线参数
公共无线参数在 vendor/BP-UWB_LIB/Src/bphero_uwb.c 中定义。
| 参数 | 当前值 |
|---|---|
| Channel | 2 |
| PRF | 64M |
| Preamble Length | 1024 |
| PAC | 32 |
| TX Preamble Code | 9 |
| RX Preamble Code | 9 |
| Non-standard SFD | 1 |
| Data Rate | 110K |
| PHR Mode | 标准 |
| SFD Timeout | 1025 + 64 - 32 |
7.2 发射功率实际配置
当前分支并不是通过静态 dwt_txconfig_t 常量配置发射功率,而是调用:
dwt_SetTxPower(&config);
在 vendor/BP-UWB_LIB/decadriver/deca_device.c 中,当前实际代码行为是:
PGdly = 0xC2TX_POWER = 0x1F1F1F1F
即使文件中保留了按信道和 PRF 的功率查找表,当前函数仍然直接写死:
configTX.PGdly = 0xc2;
configTX.power = 0x1F1F1F1F;
因此,当前分支实际生效的发射功率配置应以 0x1F1F1F1F + PGdly 0xC2 为准,而不是查表值。
7.3 初始化步骤
BPhero_UWB_Init() 中的主要执行步骤如下:
- 复位
DW1000 - 切换 SPI 低速
dwt_initialise(DWT_LOADUCODE)- 切换 SPI 高速
dwt_configure(&config)dwt_setleds(1)dwt_SetTxPower(&config)- 设置
PAN ID - 设置短地址
- 设置收发天线延时
- 开启 UWB 接收相关中断
- 读取并打印
DEV_ID
7.4 初始化流程图
flowchart TD
A["BPhero_UWB_Init()"] --> B["reset_DW1000()"]
B --> C["spi_set_rate_low()"]
C --> D["dwt_initialise(DWT_LOADUCODE)"]
D --> E["spi_set_rate_high()"]
E --> F["dwt_configure(&config)"]
F --> G["dwt_setleds(1)"]
G --> H["dwt_SetTxPower(&config)"]
H --> I["设置 PAN ID / short address"]
I --> J["设置天线延时"]
J --> K["开启中断"]
K --> L["打印 UWB 启动信息"]
8. DS-TWR 空口协议
8.1 帧类型
当前测距协议为标准 DS-TWR,使用三类消息:
P:POLLA:ACKF:FINAL
8.2 各帧职责
| 帧类型 | 发送方 | 作用 |
|---|---|---|
P |
TX |
发起一轮测距 |
A |
RX |
对 POLL 立即响应,并携带缓存距离 |
F |
TX |
携带三组时间戳,用于 RX 端完成 DS-TWR 计算 |
8.3 时间戳集合
FINAL 中携带:
poll_tx_tsresp_rx_tsfinal_tx_ts
RX 端本地采集:
poll_rx_tsresp_tx_tsfinal_rx_ts
随后 RX 端使用标准漂移补偿 DS-TWR 公式计算飞行时间,并进一步减去 dwt_getrangebias() 以及执行 KalMan() 平滑。
8.4 空口时序图
sequenceDiagram
participant TX as "TX Node"
participant RX as "RX Node"
TX->>RX: "POLL (P)"
Note right of RX: "记录 poll_rx_ts"
RX->>TX: "ACK (A)"
Note right of RX: "记录 resp_tx_ts"
Note left of TX: "记录 poll_tx_ts / resp_rx_ts"
TX->>RX: "FINAL (F)"
Note left of TX: "写入 poll_tx_ts / resp_rx_ts / final_tx_ts"
Note right of RX: "记录 final_rx_ts"
RX->>RX: "执行 DS-TWR 距离计算"
9. RX 节点代码流程
9.1 RX 角色说明
RX_NODE 是当前系统中的应答端。它的职责包括:
- 进入接收状态并开启帧过滤
- 接收
POLL - 发送
ACK - 接收
FINAL - 计算距离
- 更新 OLED
- 输出 UART 日志
- 在具备网络能力时发送 TCP 字符串
9.2 中断处理路径
中断核心逻辑在 Simple_Rx_Callback() 中。
收到 POLL 时
- 读取状态并校验帧
- 读取源地址
- 更新回复帧头
- 构造
ACK - 将最近距离写入
ACKpayload - 立即发送
ACK - 等待
TXFRS
收到 FINAL 时
- 获取
resp_tx_ts - 获取
final_rx_ts - 从
FINAL中提取poll_tx_ts / resp_rx_ts / final_tx_ts - 计算
ra / rb / da / db - 计算
tof_dtu - 转换为米
- 调用
dwt_getrangebias()进行偏置修正 - 调用
KalMan()进行平滑 - 调用
rx_report_distance()输出结果
9.3 前台主循环
rx_main() 前台主循环负责:
- 调用
tcp_runtime_loop() - 调用
app_config_poll() - 若检测到
SYS_STATUS_ALL_RX_ERR,重新进入rx_enter_mode() - 周期执行
Delay_Ms(10)
9.4 RX 前台流程图
flowchart TD
A["rx_main() start"] --> B["KalMan_Init()"]
B --> C["rx_enter_mode()"]
C --> D["前台主循环"]
D --> E["tcp_runtime_loop()"]
E --> F["app_config_poll()"]
F --> G{"是否有 RX 错误?"}
G -->|是| H["rx_enter_mode()"]
G -->|否| I["Delay_Ms(10)"]
H --> I
I --> D
9.5 RX 中断流程图
flowchart TD
A["Simple_Rx_Callback()"] --> B{"RXFCG?"}
B -->|否| Z["清错误并重入 RX"]
B -->|是| C["读取帧数据"]
C --> D{"消息类型"}
D -->|P| E["构造 ACK"]
E --> F["发送 ACK"]
F --> Z
D -->|F| G["读取 FINAL 时间戳"]
G --> H["执行 DS-TWR 计算"]
H --> I["bias 修正 + KalMan"]
I --> J["打印 / 上报距离"]
J --> Z
Z --> K["rx_enter_mode()"]
10. TX 节点代码流程
10.1 TX 角色说明
TX_NODE 是当前系统中的发起端。它的职责包括:
- 周期性启动一轮测距
- 发送
POLL - 等待
ACK - 按延迟时间发送
FINAL - 接收基站返回的缓存距离
- 更新 OLED
- 输出 UART 日志
- 在
CH32V307上可通过 TCP 输出结果
10.2 状态机
当前状态机包含两个状态:
TAG_INITTAG_POLL_SENT
TAG_INIT
- 空闲状态
- 前台主循环会发起下一轮测距
TAG_POLL_SENT
POLL已发送,正在等待ACK- 若接收错误则强制结束本轮
10.3 中断处理路径
核心中断逻辑位于 Tx_Simple_Rx_Callback()。
收到期望 ACK 时
- 读取帧和源地址
- 获取
poll_tx_ts - 获取
resp_rx_ts - 按当前系统时钟高位加固定偏移计算
final_tx_time - 配置延迟发送时间
- 填充
FINAL时间戳 - 发出延迟
FINAL - 等待
TXFRS - 从
ACK中提取 Anchor 返回的缓存距离 - 输出结果
- 更新 OLED
- 结束本轮状态机
10.4 当前 FINAL 调度策略
当前分支的 FINAL 发送不是从 ACK RX timestamp 推导,而是:
final_tx_time = dwt_readsystimestamphi32() + TX_FINAL_TX_DELAY_HI32;
其中:
TX_FINAL_TX_DELAY_UUS = 10000
因此当前策略本质是:
- 从“当前系统时间”往后推固定
10 ms发送FINAL - 目的是给较慢 MCU 留出足够的准备时间
10.5 前台主循环
tx_main() 前台主循环负责:
- 调用
tcp_runtime_loop() - 调用
app_config_poll() - 如果空闲则发起一轮测距
- 发起后按
poll_interval_ms延时 - 若接收状态出现错误,则结束本轮并回到初始态
10.6 TX 前台流程图
flowchart TD
A["tx_main() start"] --> B["显示 OLED 初始界面"]
B --> C["前台主循环"]
C --> D["tcp_runtime_loop()"]
D --> E["app_config_poll()"]
E --> F{"tag_state == TAG_INIT?"}
F -->|是| G["发送 POLL"]
G --> H["Delay poll_interval_ms"]
F -->|否| I{"是否 RX error?"}
I -->|是| J["tx_finish_cycle()"]
I -->|否| C
H --> C
J --> C
10.7 TX 中断流程图
flowchart TD
A["Tx_Simple_Rx_Callback()"] --> B{"RXFCG?"}
B -->|否| Z["清错误并结束本轮"]
B -->|是| C["读取 ACK 帧"]
C --> D{"ACK 且 tag_state=TAG_POLL_SENT?"}
D -->|否| Z
D -->|是| E["获取 poll_tx_ts / resp_rx_ts"]
E --> F["计算 delayed final_tx_time"]
F --> G["填充 FINAL 时间戳"]
G --> H["dwt_starttx(DWT_START_TX_DELAYED)"]
H --> I{"TXFRS 是否完成?"}
I -->|否| Z
I -->|是| J["解析 Anchor 缓存距离"]
J --> K["打印 / 上报结果"]
K --> Z
11. 网络上报链路
11.1 以太网能力模型
CH32V307 支持以太网和 WCHNET TCP 客户端;CH32V108 保留同一组接口,但网络路径为空实现,仅打印:
[NET] disabled for current MCU build
11.2 默认网络参数
vendor/User/tcpclinet_init.c 中当前默认值为:
- Local IP:
192.168.2.12 - Gateway IP:
192.168.2.1 - Subnet Mask:
255.255.255.0 - Remote IP:
192.168.2.249 - Remote Port:
8888
这些参数在启动时会被 app_config 覆盖。
11.3 网络运行时流程
tcpclient_init() 执行流程:
- 打印
WCHNET版本 - 打印 MAC、本地端点、远端端点
- 初始化
TIM2作为WCHNET时基 - 调用
ETH_LibInit() - 配置 keepalive
- 预创建 socket 连接
前台必须周期调用 tcp_runtime_loop(),以便:
- 运行
WCHNET_MainTask() - 分发全局中断
- 处理 connect / recv / disconnect / timeout
11.4 上报形式
当前分支的 RX 与 TX 上报不是 JSON,而是字符串日志直传:
RX 示例:
[UWB][RX][DIST] seq=12 rx=0x0001 tag=0x0101 dist=153cm
TX 示例:
[UWB][TX][DIST] seq=12 tag=0x0101 an=0x0001 dist=153cm
12. OLED 与 UART 输出
12.1 OLED
在 LCD_ENABLE 打开时,RX 和 TX 都会启用 OLED 显示。
RX OLED 显示内容:
- 固定标题
Rx NodeRSSITag + Distance
TX OLED 显示内容:
- 固定标题
Tx NodeDistanceRSSI
12.2 UART
UART 默认 115200。当前典型输出内容包括:
- 启动欢迎日志
- 配置日志
- 网络日志
- UWB 启动日志
- 距离成功日志
- 发送超时日志
- EEPROM 自检结果
13. 串口配置工具
13.1 工具组成
当前分支附带串口配置工具:
tools/run_serial_config_tool.pytools/serial_config_gui.py
工具基于:
PySide6pyserial
13.2 主要功能
串口配置工具支持:
- 串口端口枚举与连接
showsavedefaultseeprom_test- 基础 UWB 参数修改
- 网络参数修改
- 串口日志显示与自动滚动
13.3 典型使用流程
- 连接串口
- 点击
Show - 修改
Short Addr / PAN ID / Dest Addr / Poll(ms)等参数 - 点击
Apply - 点击
Save - 如需校验 EEPROM,执行
EEPROM Test
14. 编译系统
14.1 默认入口
顶层编译入口为:
make
默认展开为:
make four_hex
14.2 产物
four_hex 会生成:
CH32V307_RX.hexCH32V307_TX.hexCH32V108_RX.hexCH32V108_TX.hex
14.3 构建策略
当前 Makefile 特点:
- 默认支持
CH32V307与CH32V108 - 默认支持
RX_NODE与TX_NODE - 先分别编译
dual_hex - 再汇总四个 HEX 到
build/ - 当前分支已移除自动导出到远程挂载目录的动作
15. 调试说明
15.1 TX 超时的含义
在当前分支中,TX 的核心异常包括:
poll timeoutfinal start failedfinal timeout
这些日志含义如下:
poll timeout:POLL发射完成等待异常final start failed:延迟FINAL启动失败final timeout:延迟FINAL未在预期窗口内完成发送
15.2 RX 错误恢复
当 RX 端检测到接收错误时,会执行:
- 清接收错误位
rx_enter_mode()
从而重新进入可接收状态。
15.3 持久化配置失效
当 EEPROM 中配置不满足以下条件时:
magicversion- MCU 类型
- 角色
checksum
系统会回退到默认参数。
15.4 当前分支的代码事实
在阅读和维护当前分支时,需要特别注意以下几点:
- 当前工程使用的是
DW1000,不是DW3000 - 当前无线配置是
Channel 2 + PRF64M + Preamble 1024 + 110K - 当前发射功率函数实际写死为
0x1F1F1F1F - 当前
TX FINAL调度是按当前系统时间加固定偏移实现 - 当前上报格式是普通字符串,不是 JSON
16. 文件职责总表
| 文件 | 职责 |
|---|---|
src/main.c |
启动顺序与角色分流 |
vendor/BP-UWB_LIB/Src/bphero_uwb.c |
DW1000 公共初始化、帧头更新、地址应用、发射功率配置 |
vendor/BP-UWB_LIB/Src/rx_main.c |
应答端测距与 RX 侧结果输出 |
vendor/BP-UWB_LIB/Src/tx_main.c |
发起端测距与 TX 侧结果输出 |
vendor/User/app_config.c |
持久化配置、UART CLI、运行时应用 |
vendor/User/at24c02.c |
EEPROM 驱动与自检 |
vendor/User/tcpclinet_init.c |
以太网、TCP 初始化与上报 |
tools/serial_config_gui.py |
串口配置工具主界面 |
Makefile |
双 MCU、双角色构建 |
17. 结论
当前 ch32_dw1000_distance 分支已经具备一套完整的 DW1000 DS-TWR 测距固件框架,支持:
CH32V307 / CH32V108双平台RX / TX双角色OLED与UART本地输出AT24C02持久化配置CH32V307侧TCP字符串上报- PC 侧串口配置工具
从软件结构上看,该分支已经清晰地分为:
- 启动与平台层
- 通用 UWB 初始化层
- 协议状态机层
- 持久化配置层
- 网络上报层
- 上位机配置工具层
这使得当前工程既适合直接做测距联调,也适合作为后续交付和扩展的基础版本。