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

1. 文档说明

本文档面向当前的 CH32V307 / CH32V108 + DW3000 双角色测距工程,说明系统组成、软件分层、启动流程、DS-TWR 空口交互、RX/TX 代码流程、持久化配置机制、网络上报路径以及构建产物。本文档的目标读者包括研发工程师、集成工程师、现场调试工程师以及交付工程师。

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

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

2. 工程介绍

当前工程采用 DW3000 / DWM3000 作为 UWB 收发芯片,采用 CH32V307CH32V108 作为主控平台,构成一套双角色 DS-TWR 测距系统。工程支持在两个 MCU 平台上分别编译为 RXTX 节点,支持 OLED 显示、串口调试、AT24C02 持久化配置以及 CH32V307 上的 TCP 上报能力。测距流程完整覆盖了 POLL -> ACK -> FINAL 三帧交互、时间戳采集、距离计算、结果缓存、前后台分工处理和对外输出。在当前代码状态下,该工程已经不是简单 demo,而是一个具备真实部署接口的嵌入式测距工程基础版本,可用于同类模块互测、异构模块兼容性验证、协议联调以及上层系统集成。

3. 系统组成

3.1 硬件角色

角色 主控 UWB器件 主要职责
RX_NODE CH32V307CH32V108 DW3000 / DWM3000 接收 POLL、发送 ACK、接收 FINAL、计算距离
TX_NODE CH32V307CH32V108 DW3000 / DWM3000 发送 POLL、等待 ACK、发送延迟 FINAL、接收距离结果

3.2 软件模块

模块 文件 主要职责
系统入口 src/main.c 启动顺序控制与角色分流
通用UWB初始化 vendor/BP-UWB_LIB/Src/bphero_uwb.c DW3000 探测、配置、地址应用、发射功率配置
RX测距主流程 vendor/BP-UWB_LIB/Src/rx_main.c 应答端协议处理、距离计算、OLED/UART/TCP 输出
TX测距主流程 vendor/BP-UWB_LIB/Src/tx_main.c 发起端协议处理、周期测距、延迟 FINAL 发送、结果输出
持久化配置核心 vendor/User/persist_core.c EEPROM 读写、命令解析、运行时配置应用
EEPROM驱动 vendor/User/at24c02.c AT24C02 初始化、读写、自检
网络客户端 vendor/User/tcpclinet_init.c WCHNET 初始化与 TCP 上报
板级UWB初始化 vendor/User/uwb_init.c SPI、GPIO、中断与平台初始化

4. 总体设计方案

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

主要设计要点如下:

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

这一架构的好处是:

  • 无线初始化与协议状态机分离
  • 配置存储与业务逻辑分离
  • 网络层与测距主链路解耦
  • 便于在多个 MCU 平台和多个工程分支中复用

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. persist_core_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["persist_core_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 存储模型

当前分支使用统一的 persist_core 配置块,并从 EEPROM 地址 0x00 开始读写。配置结构包括:

  • magic
  • version
  • mcu_type
  • node_role
  • short_addr
  • pan_id
  • poll_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/persist_core.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 配置初始化流程图

flowchart TD
    A["persist_core_init()"] --> B["加载默认值"]
    B --> C["初始化 AT24C02"]
    C --> D["从 EEPROM 0x00 读取配置"]
    D --> E{"配置有效?"}
    E -->|否| F["保留默认值"]
    E -->|是| G["加载存储配置"]
    F --> H["finalize 配置"]
    G --> H
    H --> I["开启 UART RX IRQ"]
    I --> J["应用 UWB 地址配置"]
    J --> K["应用网络配置"]
    K --> L["打印 show 配置与 CLI ready"]

7. DW3000 初始化

7.1 默认无线参数

公共无线参数在 vendor/BP-UWB_LIB/Src/bphero_uwb.c 中定义。

参数 当前值
Channel 5
Preamble Length 128
PAC 8
TX Preamble Code 9
RX Preamble Code 9
SFD Type 0
Data Rate 850K
STS OFF
PDOA Mode DWT_PDOA_M0
TX PG Delay 0x34
TX Power 0xFEFEFEFE

7.2 初始化步骤

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

  1. 应用运行时无线参数覆盖
  2. 切换 SPI 低速
  3. 释放 DW3000 复位
  4. 等待器件上电稳定
  5. 通过 IO 唤醒器件
  6. dw3000_driver_probe()
  7. dwt_softreset(0)
  8. dwt_checkidlerc()
  9. dwt_initialise(DWT_DW_INIT)
  10. dwt_configure(&config)
  11. dwt_configuretxrf(&txconfig_options)
  12. 切换 SPI 高速
  13. 设置天线延时、PAN ID、短地址
  14. 清状态位
  15. 开中断
  16. 使能 LNA/PA 与器件 LED

7.3 初始化流程图

flowchart TD
    A["BPhero_UWB_Init()"] --> B["apply_runtime_radio_config()"]
    B --> C["spi_set_rate_low()"]
    C --> D["release reset / wait settle"]
    D --> E["wakeup_device_with_io()"]
    E --> F["dw3000_driver_probe()"]
    F --> G["dwt_softreset(0)"]
    G --> H["dwt_checkidlerc()"]
    H --> I["dwt_initialise(DWT_DW_INIT)"]
    I --> J["dwt_configure(&config)"]
    J --> K["dwt_configuretxrf(&txconfig_options)"]
    K --> L["spi_set_rate_high()"]
    L --> M["Apply PAN ID / short address"]
    M --> N["Enable IRQ / LNA / LEDs"]

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 公式计算飞行时间。

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 JSON

9.2 中断处理路径

中断核心逻辑在 Simple_Rx_Callback() 中。

收到 POLL

  1. 读取状态并校验帧
  2. 校验 STS(若启用)
  3. 解析源地址
  4. 构造 ACK
  5. 把最近缓存距离和功率写入 ACK payload
  6. 配置 RX timeoutRX-after-TX
  7. 立即发送 ACK
  8. 记录 poll_rx_ts
  9. 保持硬件 RX 持续等待 FINAL

收到 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. 加入 RX_DISTANCE_BIAS_MM
  8. tag 地址缓存距离与接收功率
  9. 设置前台打印标志

9.3 前台主循环

rx_main() 前台主循环负责:

  • 调用 persist_core_poll()
  • 调用 tcp_runtime_loop()
  • 在非中断上下文中消费距离结果
  • 打印 [UWB][RX][DIST] ...
  • 构造并发送 TCP JSON
  • 在超时或接收错误时重新进入 rx_enter_mode()

9.4 RX 前台流程图

flowchart TD
    A["rx_main() start"] --> B["rx_enter_mode()"]
    B --> C["前台主循环"]
    C --> D["persist_core_poll()"]
    D --> E["tcp_runtime_loop()"]
    E --> F{"是否有待输出距离?"}
    F -->|是| G["打印 UART 距离日志"]
    G --> H["发送 TCP JSON"]
    F -->|否| I{"是否 RX timeout / error?"}
    H --> I
    I -->|是| J["rx_enter_mode()"]
    I -->|否| C
    J --> C

9.5 RX 中断流程图

flowchart TD
    A["Simple_Rx_Callback()"] --> B{"SYS_STATUS_RXFCG?"}
    B -->|否| Z["清错误并重入 RX"]
    B -->|是| C["读取帧数据"]
    C --> D{"消息类型"}
    D -->|P| E["构造 ACK"]
    E --> F["发送 ACK + RESPONSE_EXPECTED"]
    F --> G["记录 poll_rx_ts"]
    G --> Y["保持 RX 持续等待"]
    D -->|F| H["读取时间戳"]
    H --> I["执行 DS-TWR 计算"]
    I --> J["加入距离偏置"]
    J --> K["缓存距离与功率"]
    K --> L["置位前台日志标志"]
    L --> Z
    D -->|M| M["中继文本到 UART/TCP"]
    M --> Z
    Y --> N{"keep_rx_armed?"}
    N -->|是| O["退出中断"]
    Z --> P["rx_enter_mode()"]
    P --> O

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 中提取锚点返回的距离和功率
  10. 缓存结果并置位前台输出标志
  11. 结束本轮状态机

收到异常帧或超时/错误时

  1. 清状态
  2. forcetrxoff
  3. 恢复到 TAG_INIT

10.4 前台主循环

tx_main() 前台主循环负责:

  • 调用 persist_core_poll()
  • 调用 tcp_runtime_loop()
  • 消费待输出距离结果
  • 若当前空闲,则调用 BPhero_Distance_Measure_Specail_TAG()
  • 若处于等待 ACK,则累加等待时间
  • 若超时,则打印 [UWB][TX][TIMEOUT] ... 并重置状态
  • poll_ms 延时

10.5 TX 前台流程图

flowchart TD
    A["tx_main() start"] --> B["渲染 OLED 初始布局"]
    B --> C["前台主循环"]
    C --> D["persist_core_poll()"]
    D --> E["tcp_runtime_loop()"]
    E --> F{"是否有待输出距离?"}
    F -->|是| G["打印 UART 日志 / 发送 TCP JSON"]
    F -->|否| H{"Tag_State == TAG_INIT?"}
    G --> H
    H -->|是| I["发送 POLL"]
    I --> J["进入等待 ACK 状态"]
    H -->|否| K["累计等待时间"]
    K --> L{"是否超时?"}
    L -->|是| M["打印 TIMEOUT 并重置状态"]
    L -->|否| N["Delay poll_ms"]
    J --> N
    M --> N
    N --> C

10.6 TX 中断流程图

flowchart TD
    A["Tx_Simple_Rx_Callback()"] --> B{"RXFCG?"}
    B -->|否| Z["清状态并结束本轮"]
    B -->|是| C["读取 ACK 帧"]
    C --> D{"ACK 且 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["解析锚点返回距离/功率"]
    J --> K["缓存结果"]
    K --> L["置位前台日志标志"]
    L --> 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

这些参数在启动时会被持久化配置覆盖。

11.3 网络运行时流程

tcpclient_init() 执行流程:

  1. 应用持久化网络参数
  2. 初始化 TIM2 作为 WCHNET 时基
  3. 初始化 Ethernet 库
  4. 创建 TCP client socket
  5. 发起连接

前台必须周期调用 tcp_runtime_loop(),以便:

  • 运行 WCHNET_MainTask()
  • 分发全局中断
  • 处理 socket connect / recv / disconnect / timeout
  • 保持遥测链路存活

11.4 JSON 上报格式

RX 侧示例:

{"type":"uwb_range","seq":12,"role":"rx","anchor":"0x0001","tag":"0x0002","dist_cm":153,"rx_dbm":"-78.25"}

TX 侧示例:

{"type":"uwb_range","seq":12,"role":"tx","tag":"0x0002","anchor":"0x0001","dist_cm":153,"rx_dbm":"-78.25"}

12. OLED 与 UART 输出

12.1 OLED

LCD_ENABLE 打开时,RXTX 都会启用 OLED 显示。

RX OLED 显示内容:

  • 角色名称
  • 基站地址
  • 标签地址
  • 距离与接收功率

TX OLED 显示内容:

  • 角色名称
  • 标签地址
  • 当前目标基站地址
  • 距离与接收功率

12.2 UART

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

  • Boot 启动日志
  • Config 配置日志
  • Net 网络日志
  • UWB 启动日志
  • 距离成功日志
  • 超时诊断日志

13. 编译系统

13.1 默认入口

顶层编译入口为:

make

默认展开为:

make four_hex

13.2 产物

four_hex 会生成:

  • ch32v307_demo_rx.hex
  • ch32v307_demo_tx.hex
  • ch32v108_demo_rx.hex
  • ch32v108_demo_tx.hex

14. 调试说明

14.1 TX TIMEOUT 的含义

[UWB][TX][TIMEOUT] 表示 TX 节点在 TAG_POLL_SENT 状态停留过久,本质含义是:

  • 没有按预期收到 ACK
  • 或者收发状态机没有正常完成

它不是距离公式错误,而是链路交互状态异常。

14.2 RX 恢复逻辑

RX 检测到 RX timeoutRX error 标志时,会调用 rx_enter_mode() 强制重新进入清洁接收态。这是当前代码里的标准恢复路径。

14.3 持久化配置失效

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

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

系统会回退到默认参数。

15. 文件职责总表

文件 职责
src/main.c 启动顺序与角色分流
vendor/BP-UWB_LIB/Src/bphero_uwb.c DW3000 公共初始化、地址应用、RF 配置
vendor/BP-UWB_LIB/Src/rx_main.c 应答端测距与 RX 侧结果输出
vendor/BP-UWB_LIB/Src/tx_main.c 发起端测距与 TX 侧结果输出
vendor/User/persist_core.c 持久化配置、UART CLI、运行时应用
vendor/User/at24c02.c EEPROM 驱动与自检
vendor/User/tcpclinet_init.c 以太网、TCP 初始化与上报
Makefile 双 MCU、双角色构建与导出

16. 结论

当前 main 分支已经具备一套完整的 DW3000 DS-TWR 测距固件框架,支持:

  • CH32V307 / CH32V108 双平台
  • RX / TX 双角色
  • OLEDUART 本地输出
  • AT24C02 持久化配置
  • CH32V307TCP 遥测上报

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

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

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