CH32 + DW1000 DS-TWR 测距工程技术手册

1. 文档说明

本文档面向当前 CH32V307 / CH32V108 + DW1000 双角色测距工程的软件结构、启动流程、DS-TWR 空口交互、RX/TX 代码路径、持久化配置、串口配置工具、网络上报方式以及构建产物。本文档的目标读者包括研发工程师、集成工程师、调试工程师以及交付工程师。

当前分支实现的是标准 DS-TWR 测距链路,按编译宏区分 RX_NODETX_NODE。其中:

  • RX_NODE 作为应答端,负责接收 POLL、发送 ACK、接收 FINAL 并计算距离
  • TX_NODE 作为发起端,负责发送 POLL、等待 ACK、发送延迟 FINAL
  • CH32V307 在具备以太网能力时,可将测距结果通过 TCP 以字符串形式上报
  • CH32V307 / CH32V108 均支持 AT24C02 持久化参数存储
  • OLEDUART 在当前工程中均已接入运行时显示与调试输出

2. 工程介绍

当前工程采用 DW1000 作为 UWB 收发芯片,采用 CH32V307CH32V108 作为主控平台,构成一套双角色 DS-TWR 测距系统。工程支持在两个 MCU 平台上分别编译为 RXTX 节点,支持 OLED 显示、串口调试、AT24C02 持久化配置以及 CH32V307 上的 TCP 上报能力。测距流程完整覆盖了 POLL -> ACK -> FINAL 三帧交互、时间戳采集、距离计算、结果缓存、前后台分工处理和对外输出。当前分支同时集成了串口配置工具,可以通过 show / set / save / defaults / eeprom_test 对设备参数进行读写和持久化验证。综合来看,这条分支已经不是单纯的底层示例,而是一套具备工程化测距、网络链路和配置管理能力的 DW1000 测距固件版本。

3. 系统组成

3.1 硬件角色

角色 主控 UWB器件 主要职责
RX_NODE CH32V307CH32V108 DW1000 接收 POLL、发送 ACK、接收 FINAL、计算距离
TX_NODE CH32V307CH32V108 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.crx_main.c 分别实现 DS-TWR 发起端与应答端逻辑。
  5. 通过 tcpclinet_init.cCH32V307 平台上提供 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,使用三类消息:

  • PPOLL
  • AACK
  • FFINAL

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 上报形式

当前分支的 RXTX 上报不是 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 打开时,RXTX 都会启用 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 特点:

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

15. 调试说明

15.1 TX 超时的含义

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

  • poll timeout
  • final start failed
  • final timeout

这些日志含义如下:

  • poll timeoutPOLL 发射完成等待异常
  • 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 双角色
  • OLEDUART 本地输出
  • AT24C02 持久化配置
  • CH32V307TCP 字符串上报
  • PC 侧串口配置工具

从软件结构上看,该分支已经清晰地分为:

  • 启动与平台层
  • 通用 UWB 初始化层
  • 协议状态机层
  • 持久化配置层
  • 网络上报层
  • 上位机配置工具层

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