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、发送延迟 FINAL
  • CH32V307 在具备以太网能力时,可将测距结果通过 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. 总体设计方案

当前工程采用典型的“公共底层 + 角色协议逻辑 + 配置层 + 上报层”设计。

主要设计要点如下:

  1. 通过编译宏决定固件角色,不在运行时动态切换角色。
  2. 通过 app_config 在上电时加载 EEPROM 配置,并在运行时同步到 UWB 地址和网络参数。
  3. 通过 bphero_uwb.c 统一管理 DW1000 初始化、帧头、地址更新和发射功率设置。
  4. 通过 tx_main.c 与 rx_main.c 分别实现 DS-TWR 发起端与应答端逻辑。
  5. 通过 tcpclinet_init.c 在 CH32V307 平台上提供 TCP 遥测能力。

这一架构的特点是:

  • UWB 初始化与协议状态机分离
  • 持久化配置与业务逻辑分离
  • 网络层与测距主链路解耦
  • 便于在 CH32V307 / CH32V108 两个平台上统一维护

5. 启动流程

5.1 主启动链路

系统入口位于 src/main.c,启动顺序如下:

  1. SystemCoreClockUpdate()
  2. Delay_Init()
  3. USART_Printf_Init(115200)
  4. 打印欢迎信息、主频、芯片 ID
  5. GPIO_LED_INIT()
  6. uwb_led_init()
  7. app_config_init()
  8. tcpclient_init()
  9. uwb_init()
  10. 根据编译角色进入:
  11. RX_NODE -> rx_main()
  12. 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 开始读写。配置结构包括:

  • magic
  • version
  • mcu_type
  • node_role
  • pan_id
  • short_addr
  • poll_interval_ms
  • dest_addr
  • local_ip
  • gateway_ip
  • subnet_mask
  • server_ip
  • server_port
  • checksum

配置合法性校验条件:

  • magic 匹配
  • version 匹配
  • mcu_type 与当前主控一致
  • node_role 与当前角色一致
  • checksum 正确

任一条件不满足时,系统回退到默认配置。

6.2 串口命令集合

当前串口命令解析位于 vendor/User/app_config.c,统一支持:

  • show
  • set <key> <value>
  • save
  • defaults
  • eeprom_test

不同角色支持的键值不同。

无以太网 RX

  • short_addr
  • pan_id

有以太网 RX

  • short_addr
  • pan_id
  • local_ip
  • gateway_ip
  • subnet_mask
  • server_ip
  • server_port

无以太网 TX

  • short_addr
  • pan_id
  • poll_ms
  • dest_addr

有以太网 TX

  • short_addr
  • pan_id
  • poll_ms
  • dest_addr
  • local_ip
  • gateway_ip
  • subnet_mask
  • server_ip
  • server_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 = 0xC2
  • TX_POWER = 0x1F1F1F1F

即使文件中保留了按信道和 PRF 的功率查找表,当前函数仍然直接写死:

configTX.PGdly = 0xc2;
configTX.power = 0x1F1F1F1F;

因此,当前分支实际生效的发射功率配置应以 0x1F1F1F1F + PGdly 0xC2 为准,而不是查表值。

7.3 初始化步骤

BPhero_UWB_Init() 中的主要执行步骤如下:

  1. 复位 DW1000
  2. 切换 SPI 低速
  3. dwt_initialise(DWT_LOADUCODE)
  4. 切换 SPI 高速
  5. dwt_configure(&config)
  6. dwt_setleds(1)
  7. dwt_SetTxPower(&config)
  8. 设置 PAN ID
  9. 设置短地址
  10. 设置收发天线延时
  11. 开启 UWB 接收相关中断
  12. 读取并打印 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:POLL
  • A:ACK
  • F:FINAL

8.2 各帧职责

帧类型 发送方 作用
P TX 发起一轮测距
A RX 对 POLL 立即响应,并携带缓存距离
F TX 携带三组时间戳,用于 RX 端完成 DS-TWR 计算

8.3 时间戳集合

FINAL 中携带:

  • poll_tx_ts
  • resp_rx_ts
  • final_tx_ts

RX 端本地采集:

  • poll_rx_ts
  • resp_tx_ts
  • final_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 时

  1. 读取状态并校验帧
  2. 读取源地址
  3. 更新回复帧头
  4. 构造 ACK
  5. 将最近距离写入 ACK payload
  6. 立即发送 ACK
  7. 等待 TXFRS

收到 FINAL 时

  1. 获取 resp_tx_ts
  2. 获取 final_rx_ts
  3. 从 FINAL 中提取 poll_tx_ts / resp_rx_ts / final_tx_ts
  4. 计算 ra / rb / da / db
  5. 计算 tof_dtu
  6. 转换为米
  7. 调用 dwt_getrangebias() 进行偏置修正
  8. 调用 KalMan() 进行平滑
  9. 调用 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_INIT
  • TAG_POLL_SENT

TAG_INIT

  • 空闲状态
  • 前台主循环会发起下一轮测距

TAG_POLL_SENT

  • POLL 已发送,正在等待 ACK
  • 若接收错误则强制结束本轮

10.3 中断处理路径

核心中断逻辑位于 Tx_Simple_Rx_Callback()。

收到期望 ACK 时

  1. 读取帧和源地址
  2. 获取 poll_tx_ts
  3. 获取 resp_rx_ts
  4. 按当前系统时钟高位加固定偏移计算 final_tx_time
  5. 配置延迟发送时间
  6. 填充 FINAL 时间戳
  7. 发出延迟 FINAL
  8. 等待 TXFRS
  9. 从 ACK 中提取 Anchor 返回的缓存距离
  10. 输出结果
  11. 更新 OLED
  12. 结束本轮状态机

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() 执行流程:

  1. 打印 WCHNET 版本
  2. 打印 MAC、本地端点、远端端点
  3. 初始化 TIM2 作为 WCHNET 时基
  4. 调用 ETH_LibInit()
  5. 配置 keepalive
  6. 预创建 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 Node
  • RSSI
  • Tag + Distance

TX OLED 显示内容:

  • 固定标题
  • Tx Node
  • Distance
  • RSSI

12.2 UART

UART 默认 115200。当前典型输出内容包括:

  • 启动欢迎日志
  • 配置日志
  • 网络日志
  • UWB 启动日志
  • 距离成功日志
  • 发送超时日志
  • EEPROM 自检结果

13. 串口配置工具

13.1 工具组成

当前分支附带串口配置工具:

  • tools/run_serial_config_tool.py
  • tools/serial_config_gui.py

工具基于:

  • PySide6
  • pyserial

13.2 主要功能

串口配置工具支持:

  • 串口端口枚举与连接
  • show
  • save
  • defaults
  • eeprom_test
  • 基础 UWB 参数修改
  • 网络参数修改
  • 串口日志显示与自动滚动

13.3 典型使用流程

  1. 连接串口
  2. 点击 Show
  3. 修改 Short Addr / PAN ID / Dest Addr / Poll(ms) 等参数
  4. 点击 Apply
  5. 点击 Save
  6. 如需校验 EEPROM,执行 EEPROM Test

14. 编译系统

14.1 默认入口

顶层编译入口为:

make

默认展开为:

make four_hex

14.2 产物

four_hex 会生成:

  • CH32V307_RX.hex
  • CH32V307_TX.hex
  • CH32V108_RX.hex
  • CH32V108_TX.hex

14.3 构建策略

当前 Makefile 特点:

  • 默认支持 CH32V307 与 CH32V108
  • 默认支持 RX_NODE 与 TX_NODE
  • 先分别编译 dual_hex
  • 再汇总四个 HEX 到 build/
  • 当前分支已移除自动导出到远程挂载目录的动作

15. 调试说明

15.1 TX 超时的含义

在当前分支中,TX 的核心异常包括:

  • poll timeout
  • final start failed
  • final timeout

这些日志含义如下:

  • poll timeout:POLL 发射完成等待异常
  • final start failed:延迟 FINAL 启动失败
  • final timeout:延迟 FINAL 未在预期窗口内完成发送

15.2 RX 错误恢复

当 RX 端检测到接收错误时,会执行:

  • 清接收错误位
  • rx_enter_mode()

从而重新进入可接收状态。

15.3 持久化配置失效

当 EEPROM 中配置不满足以下条件时:

  • magic
  • version
  • MCU 类型
  • 角色
  • checksum

系统会回退到默认参数。

15.4 当前分支的代码事实

在阅读和维护当前分支时,需要特别注意以下几点:

  1. 当前工程使用的是 DW1000,不是 DW3000
  2. 当前无线配置是 Channel 2 + PRF64M + Preamble 1024 + 110K
  3. 当前发射功率函数实际写死为 0x1F1F1F1F
  4. 当前 TX FINAL 调度是按当前系统时间加固定偏移实现
  5. 当前上报格式是普通字符串,不是 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 初始化层
  • 协议状态机层
  • 持久化配置层
  • 网络上报层
  • 上位机配置工具层

这使得当前工程既适合直接做测距联调,也适合作为后续交付和扩展的基础版本。