3. UWB设备角色与网络拓扑

做嵌入式开发,第一步就是搭环境。这事说简单也简单,说烦人也烦人。我见过不少新手,代码写好了,结果卡在编译不过、下载不了这种基础问题上,一查就是环境没配对。

这一章,咱们就把UWB定位的开发环境从头捋一遍。我用的是STM32F4系列,DW1000模块。你手里如果是其他型号,流程一样,个别引脚改一下就行。

3.1 工具链选型:Keil还是IAR?

先解决选择困难症。Keil MDK和IAR Embedded Workbench,这两家我都用过。说实话,没有绝对的好坏,只有习惯问题。

对比项 Keil MDK IAR EWARM
编译器 ARMCC v6(基于Clang) IAR C/C++ Compiler
代码体积优化 中等,v6比v5好很多 公认的优化能力强,尤其对速度
调试器支持 J-Link、ST-Link原生支持 J-Link、ST-Link、I-jet
工程管理 分组管理,直观 层级目录,适合大项目
License价格 MDK-Plus约$2000 EWARM约$3000+
国内生态 教程多,资料好找 相对少,但官方文档更详细

我个人习惯用Keil。原因很简单:STM32CubeMX直接生成Keil工程,省事。而且国内做UWB定位的同行,十有七八都用Keil,遇到问题好交流。

我的建议:如果你刚入门,选Keil。上手快,坑少。等做产品级优化时,再考虑IAR的编译优化能力。

3.2 Keil环境配置要点

安装Keil MDK就不细说了,一路Next就行。但有几个关键点,我踩过坑,得提醒你。

3.2.1 器件包安装

装完MDK后,第一件事不是建工程,而是装器件包(Pack)。

  1. 打开Keil,点击菜单栏 Pack Installer 图标。
  2. 在搜索框输入你的芯片型号,比如 STM32F407VG。
  3. 勾选对应的 Device Family Pack,点击Install。

嗯,这里要注意:Pack版本别追新。我遇到过用最新版Pack编译老工程,结果一堆警告。建议选和CubeMX生成的版本一致。

3.2.2 编译器版本切换

Keil MDK v5默认用ARMCC v5,v6开始用ARMClang。两者语法有差异,尤其是内联汇编和__attribute__写法。

避坑:DW1000的驱动库有些文件用了GNU扩展语法。如果用ARMCC v6编译报错,切换到v5试试。我曾经在这上面折腾了一下午,最后发现是编译器版本问题。

切换方法:
Project → Options for Target → Target → ARM Compiler → 选择 Use default compiler version 5。

3.2.3 下载器配置

我用ST-Link比较多,便宜又稳定。配置如下:

  1. 点击 Options for Target → Debug。
  2. 选择 ST-Link Debugger。
  3. 点击 Settings,确认SWD模式,速度选 4MHz(别选太高,容易不稳定)。
  4. 切换到 Flash Download 标签,勾选 Reset and Run。

关键一步:勾选Reset and Run后,下载完程序会自动复位运行。不然每次都要手动按复位键,调试时很烦。

3.3 STM32CubeMX初始化

CubeMX是ST官方的图形化配置工具。说白了,就是帮你生成初始化代码,省得自己手写寄存器。

3.3.1 新建工程

  1. 打开CubeMX,点击 New Project。
  2. 在 Part Number Search 输入你的芯片型号,双击选中。
  3. 进入 Pinout & Configuration 界面。

3.3.2 时钟树配置

UWB定位对时间精度要求高,时钟必须稳。我一般这样配:

  • HSE:外部8MHz晶振
  • PLL:倍频到168MHz(F407最高主频)
  • APB1:42MHz(SPI、I2C用)
  • APB2:84MHz(GPIO、USART用)

在 Clock Configuration 标签页,把HCLK输入168,回车,系统会自动帮你算好分频系数。

经验之谈:DW1000的SPI时钟不要超过20MHz。虽然理论上能到25MHz,但我实测超过20MHz后,长距离通信偶尔会丢数据。所以APB1配42MHz,SPI分频2,刚好21MHz——嗯,稍微降一点,用20MHz更稳。

3.3.3 外设配置

UWB模块需要的外设不多,但每个都得配对。

外设 用途 配置要点
SPI1 与DW1000通信 全双工主机,CPOL=0, CPHA=0,数据位8,MSB First
USART1 串口打印调试 波特率115200,8N1
GPIO DW1000复位/中断 推挽输出/浮空输入
TIM2 系统心跳定时器 1ms中断,用于超时管理

具体操作:在 Pinout 界面,找到SPI1,模式选 Full-Duplex Master。然后手动分配片选引脚(NSS用软件控制,别用硬件自动)。

3.3.4 生成代码

点击 Project Manager 标签:

  • Project Name:填你的项目名,比如 uwb_location
  • Project Location:选个路径,别用中文
  • Toolchain / IDE:选 MDK-ARM V5
  • Generate Under Root:勾上,这样所有文件都在一个目录下

最后点 GENERATE CODE,等几秒,工程就生成了。

3.4 驱动库移植

驱动库移植,说白了就是把DW1000的官方驱动塞进你的工程里。我用的驱动库是Decawave官方的 deca_driver,版本v2.12。

3.4.1 文件结构

先看看驱动库长什么样:

deca_driver/
├── deca_device_api.c      // DW1000底层API
├── deca_device_api.h
├── deca_port.h            // 平台移植接口(重点)
├── deca_spi.c             // SPI读写实现(需要你改)
├── deca_spi.h
├── deca_irq.c             // 中断处理
└── deca_irq.h

把整个 deca_driver 文件夹复制到你的Keil工程目录下。然后在Keil里添加分组:

  1. 右键工程 → Manage Project Items
  2. 新建分组 DW1000_Driver
  3. 把上面那些.c文件加进去

3.4.2 平台移植接口

这是移植的核心。打开 deca_port.h,你会看到一堆宏定义和函数声明。你需要实现这几个:

// SPI读写函数
int writetospi(uint16_t headerLength, const uint8_t *headerBuffer,
               uint16_t bodyLength, const uint8_t *bodyBuffer);
int readfromspi(uint16_t headerLength, const uint8_t *headerBuffer,
                uint16_t readLength, uint8_t *readBuffer);

// 延时函数
void deca_sleep(unsigned int time_ms);

// 中断回调
void deca_irq_handler(void);

我一般这样实现SPI读写:

int writetospi(uint16_t headerLength, const uint8_t *headerBuffer,
               uint16_t bodyLength, const uint8_t *bodyBuffer)
{
    // 拉低片选
    HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_RESET);
    
    // 发送header
    HAL_SPI_Transmit(&hspi1, (uint8_t *)headerBuffer, headerLength, 100);
    
    // 发送body
    if (bodyLength && bodyBuffer)
        HAL_SPI_Transmit(&hspi1, (uint8_t *)bodyBuffer, bodyLength, 100);
    
    // 拉高片选
    HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET);
    
    return 0;
}
注意:DW1000的SPI时序要求片选在传输过程中不能抖动。我遇到过用DMA传输时,片选被中断打断导致通信失败。解决办法:在SPI传输前后关闭全局中断,或者用阻塞式传输。

3.4.3 中断配置

DW1000通过IRQ引脚通知MCU有数据到达。在CubeMX里把IRQ引脚配成外部中断,下降沿触发。

// 在stm32f4xx_it.c中
void EXTI0_IRQHandler(void)
{
    if (__HAL_GPIO_EXTI_GET_IT(DW_IRQ_Pin) != RESET)
    {
        __HAL_GPIO_EXTI_CLEAR_IT(DW_IRQ_Pin);
        deca_irq_handler();  // 调用驱动库的中断处理
    }
}

3.4.4 验证移植是否成功

写个简单的测试代码:

#include "deca_device_api.h"

void dw1000_init_test(void)
{
    // 复位DW1000
    reset_DW1000();
    
    // 读取设备ID
    uint32_t dev_id = dwt_readdevid();
    
    // 如果读到0xDECA0130,说明移植成功
    if (dev_id == 0xDECA0130)
    {
        printf("DW1000 detected! ID: 0x%08X\n", dev_id);
    }
    else
    {
        printf("Error: Wrong ID 0x%08X\n", dev_id);
    }
}

下载到板子上,串口输出 DW1000 detected! ID: 0xDECA0130,就说明环境搭好了。

3.5 本章知识体系

下面这张图,把开发环境搭建的流程串起来了。你照着这个顺序走,不会乱。

开发环境搭建流程 选择工具链 Keil / IAR 安装Pack / 配置 STM32CubeMX 时钟 / 外设配置 生成Keil工程 驱动库移植 SPI / 中断 / 延时 deca_port.h实现 验证移植 读取Device ID 0xDECA0130? 成功 → 进入下一章;失败 → 检查SPI/时钟

3.6 常见问题与避坑

最后,分享几个我实际项目中遇到过的问题,你遇到了别慌。

问题1:编译报错 undefined symbol
原因:驱动库文件没加进工程,或者头文件路径没配。
解决:检查工程分组,确认所有.c文件都在。然后在Options → C/C++ → Include Paths里加上 deca_driver 路径。

问题2:下载后芯片没反应
原因:多半是时钟没起振,或者复位电路有问题。
解决:用示波器量晶振引脚,看有没有8MHz波形。没有的话,检查CubeMX里HSE配置对不对。

问题3:读到的Device ID全是0xFF
原因:SPI通信失败。可能是引脚映射错了,或者片选逻辑反了。
解决:用逻辑分析仪抓SPI波形,确认CS、SCLK、MOSI、MISO都有信号。我曾经因为MOSI和MISO接反了,查了整整一上午。