最近几个月断断续续花了一些时间在无人机上实现了 DW3000 的驱动,这里记录一下具体实现细节。

DWM3000 简介

DWM3000 是 Decawave 推出的新一代 UWB 芯片模组,DWM3000 上集成了天线、DW3110 IC、电源管理、时钟控制器等模块,支持最新的 IEEE802.15.4z 标准。

我们实验室之前一直在使用的是 Bitcraze 官方推出的 Loco Positioning Deck,其上所搭载的是 DWM1000 模组(下图 Loco Deck 的绿色部分)。DWM3000 相比 DWM1000 具有更低的功耗,更精准的测距性能,支持更多的信道,支持更多的 MAC 层特性和物理层标准等,而且根据 Decawave 的开发进度,后续大概率会放开与搭载 Apple U1 芯片的产品进行交互的接口。

DW1000和DW3000

虽然 DWM3000 引入了诸多特性,大幅改善了芯片性能,但可能是人手不足或团队工作重心转移的原因,Bitcraze 官方目前仍未将 DWM3000 引入 Crazyflie 开发生态,这是我们进行 DWM3000 驱动移植与开发工作的主要原因。经过前期调研和性能测试,我们发现 DWM3000 的测距精度相比 DWM1000 具有较大提升且支持更多的 MAC 层和物理层标准,而且未来也有和手机等设备交互的可能性,实现 DWM3000 驱动的收益较高,因此我们最终决定自己动手在 Crazyflie 上驱动 DWM3000。

基本原理

引脚定义

如下图所示,DWM3000 的引脚向下兼容 DWM1000,因此在硬件上只需要将 Loco Deck 上的 DWM1000 替换为 DWM3000 即可。由于 DWM1000 和 DWM3000 的 SPI 消息头的格式规定有所不同,且寄存器的地址和相应含义也发生了变化,因此无法通过修改现有 DWM1000 的相关代码来驱动 DWM3000。此外,注意到 CPU 没有引脚与 DWM3000 的 WAKEUP 引脚相连,因此我们无法通过 WAKEUP 引脚来唤醒处于 SLEEP 状态的 DWM3000。

DWM3000和LocoDeck的引脚定义

状态模型

DW3000状态模型

想要开发和调试 DWM3000 驱动首先需要了解 DWM3000 的状态模型,如上图所示。DWM3000 的状态模型主要包括 OFF、WAKEUP、INIT_RC、SLEEP、IDLE_RC、IDLE_PLL、TX、RX 等状态,每个状态都由状态寄存器(SYS_STATUS)中的某个状态位表示,如下图所示:

DW3000状态寄存器

SPI 总线数据交换

我们与 DWM3000 的芯片所有交互都依托于 SPI 总线。通过 SPI 总线发送控制指令,我们可以读写 DWM3000 的寄存器数据,改变 DWM3000 的工作状态、读取或写入数据到 DWM3000 的寄存器中。DWM3000 的 SPI 总线数据交换的格式如下图所示:

![DW3000 SPI 总线](DW3000 SPI 总线.png)

数据的发送和接收

DWM3000 采用半双工的工作模式,其在某一时刻只能处于发送状态(Transmission,TX)或者接收状态(Receiption,RX)。当 DWM3000 处在 RX 状态时,其天线会一直监听环境中的信号,当收到合适的前导码(Preamble)与帧定界符(Start of Frame Delimiter,SFD)后就意味着其感应到了环境中的数据,之后 DWM3000 会持续将收到的数据存储到接收寄存器中并在数据接收完毕后将状态寄存器的接收成功比特位置 1,同时其也会将状态寄存器中的中断比特位置 1 以通知 CPU 来处理当前中断。当 DWM3000 处在 TX 状态时,其会按照发送前配置的发送模式,将模拟出的数据通过天线广播到环境中并在发送成功后进入发送完成状态。

DW3000数据处理逻辑

由于 DWM3000 的半双工特性,我们在发送和接收数据之前需要依照状态模型的状态转移路径,首先将 DWM3000 置为 IDLE 状态,之后再进入发送/接收状态。如果没有按照规定的状态转移路径控制 DWM3000 则会出现意料之外的状况。

驱动实现

Decawave 为我们提供了一个通用版本的 DW3000 驱动实现,其中已经为我们实现了大部分驱动逻辑,主要包括 DWM3000 芯片初始化、芯片配置、SPI 消息头的构造、中断的检测和处理函数等并在 DW3000 API Guide 中给出了标准的 DW3000 驱动移植框架,如下图所示:

DW3000驱动总体框架

对于我们而言,只需要实现三个关键驱动接口 writetospi()readfromspi()dwt_isr() 即可驱动 DWM3000,其中 writetospi()readfromspi() 是 SPI 读写接口,其根据传入的控制参数和寄存器信息来与 DWM3000 进行数据交互与状态控制,dwt_isr() 为中断处理程序,其主要供 CPU 的中断处理函数调用以处理 DWM3000 引发的外部中断。此外,对于在无人机上实现的上层应用而言,我们一般还需要实现相应的中断回调函数以供中断处理程序 dwt_isr() 调用,如下所示,其中的 cbTxDone 为用户传入的自定义函数指针,相当于用户程序订阅了 DWM3000 的数据发送状态,当中断处理程序 dwt_isr() 检测到 DWM3000 成功发送过数据,那么则会触发用户程序定义的相应处理逻辑,即 cbTxDone()

void dwt_isr(void) {
/*....省略了一些代码....*/
    if(fstat & FINT_STAT_TXOK_BIT_MASK)
    {
        // Clear TX events after the callback - this lets the host schedule another TX/RX inside the callback
        dwt_write8bitoffsetreg(SYS_STATUS_ID, 0, (uint8_t)SYS_STATUS_ALL_TX); // Clear TX event bits to clear the interrupt
    
        // Call the corresponding callback if present
        if(pdw3000local->cbTxDone != NULL)
        {
            pdw3000local->cbTxDone(&pdw3000local->cbData);
        }
/*....省略了一些代码....*/
    }
}

SPI 读写与中断处理

对于 SPI 读写函数 writetospi()readfromspi() ,由于 Bitcraze 官方已经实现了一套质量很高且线程安全的 SPI 读写逻辑,所以我们这里直接复用了相应的代码。

对于中断处理而言,有多种实现方式可供选择,比如我们最初在替换了 DWM3000 的 Loco Positioning Node 上按照如下图所示的方式实现了中断处理逻辑:

LocoNode上的中断处理逻辑

因为我们对 Loco Node 的代码进行了简化,Loco Node 在启动后系统里只有两个任务在运行,因此上述实现方式是可行的,而对于无人机而言,由于其上可以搭载的 Deck 众多,且往往一个 Deck 对应一个任务,而且还会有其它附加任务在运行,因此对于中断处理逻辑的实现也有所不同,我们目前在无人机上实现的中断处理逻辑如下图所示,当 CPU 检测到 DWM3000 发出的外部中断后,其会调用 DWM3000 的中断引脚对应的中断处理函数,之后中断处理函数会在清理中断后调用中断回调函数,而在中断回调函数则会通过 FreeRTOS 内置的任务通知机制通知 uwbTask 去处理中断,而 uwbTask 内部则是在循环调用 dwt_isr() 来检测 DWM3000 的中断类型,复位相应状态寄存器对应的状态位并调用相应的回调函数。

DW3000中断处理逻辑

中断回调函数和 uwbTask 的源码如下所示,这里为了方便展示代码逻辑,省略了部分宏指令和代码。

// 中断回调函数
void __attribute__((used)) EXTI11_Callback(void)
{
  portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;

  // 通知 uwbTask 去处理 DWM3000 发起的中断
  vTaskNotifyGiveFromISR(uwbTaskHandle, &xHigherPriorityTaskWoken);

  if (xHigherPriorityTaskWoken) {
    portYIELD();
  }
}
// uwbTask
static void uwbTask(void *parameters) {
  systemWaitStart();
  
  while (1) {
    // 如果检测到通知,则会激活 dwt_isr(),否则挂起任务
    if (ulTaskNotifyTake(pdTRUE, portMAX_DELAY)) {
      do {
        dwt_isr();
      } while (digitalRead(GPIO_PIN_IRQ) != 0);
    }
  }
}

驱动加载

DWM1000 使用的驱动 libdw1000 是 Bitcraze 官方团队从 C++ 实现的开源驱动移植而来,其为我们在 Crazyflie 平台为自定义的 Deck 加载驱动提供了一个良好的示范。

在 Crazyflie 平台上加载自定义 Deck 的驱动非常简单,只需要通过一个静态代码块中定义一个 DeckDriver 结构体并传入到 DECK_DRIVER() 宏中即可。DeckDriver 结构体中需要填入 Deck 的初始化与测试逻辑,Deck 的 PID 和 VID,以及引脚使用情况等信息,如下所示:

/*********** Deck driver initialization ***************/
static void dwm3000Init(DeckInfo *info) {
  pinInit();
  queueInit();
  rangingTableSetInit(&rangingTableSet);
#ifndef CONFIG_DECK_ADHOCDECK_USE_UART2_PINS
  if (uwbInit() == DWT_SUCCESS) {
    uwbStart();
    isInit = true;
  } else {
    isInit = false;
  }
#else
  uwbStart();
  isInit = true;
#endif
}

static bool dwm3000Test() {
  if (!isInit) {
    DEBUG_PRINT("Error while initializing DWM3000\n");
  }

  return isInit;
}

static const DeckDriver dwm3000_deck = {
    .vid = 0xBC,
    .pid = 0x06,
    .name = "DWM3000",

#ifdef CONFIG_DECK_ADHOCDECK_USE_ALT_PINS
    .usedGpio = DECK_USING_IO_1 | DECK_USING_IO_2 | DECK_USING_IO_4,
#elif defined(CONFIG_DECK_ADHOCDECK_USE_UART2_PINS)
    .usedGpio = DECK_USING_IO_1 | DECK_USING_UART2,
#else
    .usedGpio = DECK_USING_IO_1 | DECK_USING_UART1,
#endif
    .usedPeriph = DECK_USING_SPI,
    .requiredEstimator = kalmanEstimator,
#ifdef ADHOCDECK_NO_LOW_INTERFERENCE
    .requiredLowInterferenceRadioMode = false,
#else
    .requiredLowInterferenceRadioMode = true,
#endif

    .init = dwm3000Init,
    .test = dwm3000Test,
};

DECK_DRIVER(dwm3000_deck);

无人机在通过 SystemTask 启动系统时会调用 deckInit() 来扫描所有通过 DECK_DRIVER() 注册的驱动,并依次调用这些驱动在结构体中定义的 .init() 函数进行初始化并在最后调用 deckTest() 以测试所有注册过的驱动是否得以成功初始化。我们在 DeckDriver 结构体中定义的函数就是在这两个时刻被分别调用的,在这些函数中打印的信息会被输出到 console 中,我们用 cfclient 连接无人机后看到的关于 Deck 是否初始化成功的信息就是从这里来的,如下所示:

void deckInit()
{
  deckDriverCount();
  deckInfoInit();
  deckMemoryInit();

  int nDecks;
  int i;

  nDecks = deckCount();

  DEBUG_PRINT("%d deck(s) found\n", nDecks);

  for (i=0; i<nDecks; i++) {
    DeckInfo *deck = deckInfo(i);

    if (deck->driver->init) {
      if (deck->driver->name) {
        DEBUG_PRINT("Calling INIT on driver %s for deck %i\n", deck->driver->name, i);
      } else {
        DEBUG_PRINT("Calling INIT for deck %i\n", i);
      }

      deck->driver->init(deck);
    }
  }
}

bool deckTest()
{
  bool pass = true;
  int nDecks;
  int i;

  nDecks = deckCount();

  for (i=0; i<nDecks; i++) {
    DeckInfo *deck = deckInfo(i);

    if (deck->driver->test) {
      if (deck->driver->test()) {
        DEBUG_PRINT("Deck %i test [OK].\n", i);
      } else {
        DEBUG_PRINT("Deck %i test [FAIL].\n", i);
        pass = false;
      }
    }
  }

  return pass;
}

此外,无人机还会检测各个 Deck 定义的引脚是否有冲突,比如上面的代码段中我们根据不同的宏配置定义了不同的模式(UART1 | IO2 & IO3 | UART2)下的引脚使用情况,如果和无人机其他 Deck 的引脚有冲突的话,无人机会报错并停止启动。

模式切换

为了与更多的 Deck 进行兼容以达到单个无人机上同时使用多个 Deck 的目的,我们对搭载 DWM3000 的 Loco Deck 进行了定制,使其可以通过修改电路以支持在三种不同的引脚组合下工作,我们称之为三种不同的模式,包括 UART1、IO2 & IO3、以及 UART2。

在介绍这部分内容之前,我们需要先了解一下无人机扩展板的引脚占用情况,如下所示:

无人机扩展板引脚分配情况

Bitcraze 团队预留了自定义引脚方案,上述表格中被 (括号包围住的部分) 表明可以通过修改电路的方式将相应的功能引脚进行替换,比如对于 Loco Deck 而言,我们可以通过修改 Deck 的电路使得原本分配在 RX1 和 TX1 上的 IRQ 和 RST 分别替换到 IO2 和 IO3 引脚上。

下图是我们定制的 DW3000 Loco Deck 的背部,其中用红色、绿色和蓝色框住的部分分别对应三种模式(UART1、IO2 & IO3、 UART2)。模式切换是通过在方框部分水平焊接零欧电阻来实现的,比如上图中的蓝色框内部的焊盘被两个零欧电阻水平连接到了一起。UART2 模式是我们定制的 Deck 才具有的模式,该模式使用 TX2 作为 IRQ,RX2 作为 RST。

定制的DW3000扩展板

Bitcraze 官方的 Loco Deck 只支持在 UART1 和 IO2 & IO3 模式间切换,并且其底座背部是没有上图中的蓝色部分的。蓝色部分是我们单独定制的 Deck 底座独有的,其用来将 DWM3000 切换至 UART2 模式。

在实现 DWM3000 驱动的同时我们也趁机对核心代码进行了重写,抛弃了原先代码的历史包袱,不仅按照 Bitcraze 官方推荐的标准格式实现了 DWM3000 驱动,同时也将构建系统切换到了 Kbuild,这是目前 Crazyflie 官方固件的主要构建模式。关于 Kbuild 可以参考 Bitcraze 的官方博客我们自己总结的文档,这里不再赘述。

想要成功在无人机上驱动 DWM3000,还需要在固件上切换至正确的模式,首先需要利用宏来配置不同模式下的引脚使用情况,如下所示,我们在 DeckDriver 中依照模式的不同设置了所使用的引脚。

static const DeckDriver dwm3000_deck = {
    .vid = 0xBC,
    .pid = 0x06,
    .name = "DWM3000",

#ifdef CONFIG_DECK_ADHOCDECK_USE_ALT_PINS
    .usedGpio = DECK_USING_IO_1 | DECK_USING_IO_2 | DECK_USING_IO_4,
#elif defined(CONFIG_DECK_ADHOCDECK_USE_UART2_PINS)
    .usedGpio = DECK_USING_IO_1 | DECK_USING_UART2,
#else
    .usedGpio = DECK_USING_IO_1 | DECK_USING_UART1,
#endif
    .usedPeriph = DECK_USING_SPI,
    .requiredEstimator = kalmanEstimator,
#ifdef ADHOCDECK_NO_LOW_INTERFERENCE
    .requiredLowInterferenceRadioMode = false,
#else
    .requiredLowInterferenceRadioMode = true,
#endif

    .init = dwm3000Init,
    .test = dwm3000Test,
};

接着我们还需要利用宏对不同模式下中断引脚对应的中断回调函数(IRQCallback)进行设置以保证 CPU 可以正常接收到 DWM3000 的中断请求并进行响应。

CPU 在接收到某个引脚的中断请求后会去调用相应的中断处理函数(IRQHandler),如下所示是某个中断引脚发出中断请求后,CPU 调用的中断处理函数,可以看到该函数实际上做了两件事,清空了当前引脚的中断标志以保证在此引脚上发起的后续中断不会被屏蔽,并在之后调用了该引脚的中断回调函数,这里是 EXTI2_Callback()。而 EXTI2_Callback() 函数则是预先定义的弱函数。其它引脚同样如此,这些弱函数的定义可以在 exti.c 源码中找到。

void __attribute__((used)) EXTI2_IRQHandler(void)
{
  NVIC_ClearPendingIRQ(EXTI2_IRQn);
  EXTI_ClearITPendingBit(EXTI_Line2);
  EXTI2_Callback();
}

void __attribute__((weak)) EXTI2_Callback(void) { }

可以注意到,如果我们没有去定义一个同名的强函数去覆盖弱函数,那么即使 CPU 检测到了 DWM3000 发来的中断,其也只是调用了一个空的中断处理逻辑。因此我们还需要去实现相应引脚的中断回调函数,如下所示,其中 EXTI11_Callback()EXTI5_Callback()EXTI2_Callback() 是三种模式对应中断引脚的中断回调函数。由于三种模式只是中断引脚的定义发生了变化,它们回调函数的逻辑是相同的,因此只需要使用宏来切换函数定义即可。

#ifdef CONFIG_DECK_ADHOCDECK_USE_ALT_PINS
void __attribute__((used)) EXTI5_Callback(void)
#elif defined(CONFIG_DECK_ADHOCDECK_USE_UART2_PINS)
void __attribute__((used)) EXTI2_Callback(void)
#else
void __attribute__((used)) EXTI11_Callback(void)
#endif
{
  portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;

  // Unlock interrupt handling task
  vTaskNotifyGiveFromISR(uwbTaskHandle, &xHigherPriorityTaskWoken);

  if (xHigherPriorityTaskWoken) {
    portYIELD();
  }
}

最后,我们只需要在 Kbuild 的配置文件中定义上述模式切换相关的宏DWM3000 DeckDWM3000 驱动的编译指令即可通过 Kbuild 和宏来动态切换当前无人机上所搭载的 DWM3000 的模式。

这里所谓的模式切换实际上是在无人机固件层面上的切换,即根据无人机所搭载的 DWM3000 所处的模式来切换不同的配置文件以使得驱动符合当前 DWM3000 的电路特性。硬件上切换 DWM3000 的工作模式则需要重新焊接零欧电阻,比如想要硬件切换到 UART1 模式,则需要将 UART1 对应的焊盘位置水平焊接上两个零欧电阻并把其余焊盘位置的零欧电阻吹掉。

代码示例

初始化

DWM3000 在初始化时会需要加载一些初始化配置,如下所示:

/* TX options */
static dwt_txconfig_t txconfig_options = {
    .PGcount = 0x0,
    .PGdly = 0x34,
    .power = 0xfdfdfdfd
};

/* PHR configuration */
static dwt_config_t config = {
    5,            /* Channel number. */
    DWT_PLEN_128, /* Preamble length. Used in TX only. */
    DWT_PAC8,     /* Preamble acquisition chunk size. Used in RX only. */
    9,            /* TX preamble code. Used in TX only. */
    9,            /* RX preamble code. Used in RX only. */
    1, /* 0 to use standard 8 symbol SFD, 1 to use non-standard 8 symbol, 2 for
          non-standard 16 symbol SFD and 3 for 4z 8 symbol SDF type */
    DWT_BR_6M8,      /* Data rate. */
#ifdef ENABLE_PHR_EXT_MODE
    DWT_PHRMODE_EXT, /* Extended PHY header mode. */
#else
    DWT_PHRMODE_STD, /* Standard PHY header mode. */
#endif
    DWT_PHRRATE_STD, /* PHY header rate. */
    (129 + 8 - 8), /* SFD timeout (preamble length + 1 + SFD length - PAC size).
                      Used in RX only. */
    DWT_STS_MODE_OFF,
    DWT_STS_LEN_64, /* STS length, see allowed values in Enum dwt_sts_lengths_e
                     */
    DWT_PDOA_M0     /* PDOA mode off */
};

一般情况下,DWM3000 Deck 在其 init() 函数中会初始化引脚配置以及其它相关的数据结构并重置 DWM3000 芯片状态,之后会调用 uwbInit() 函数来启动 DWM3000,如下所示:

static int uwbInit() {
  /* 在初始化之前需要确保 DWM3000 处于 IDLE_RC 状态*/
  while (!dwt_checkidlerc()) {}
  
  /* 初始化 DWM3000 芯片*/
  if (dwt_initialise(DWT_DW_INIT) == DWT_ERROR) {
    return DWT_ERROR;
  }
  /* 配置 DWM3000 的物理层特性*/
  if (dwt_configure(&config) == DWT_ERROR) {
    return DWT_ERROR;
  }
  
  /* 配置 LED */
  dwt_setleds(DWT_LEDS_ENABLE | DWT_LEDS_INIT_BLINK);

  /* 设置发送时的参数 (power, PG delay and PG count) */
  dwt_configuretxrf(&txconfig_options);

  /* 设置天线延迟 */
  dwt_setrxantennadelay(RX_ANT_DLY);
  dwt_settxantennadelay(TX_ANT_DLY);
  
  /* 设置处于接收状态的超时时间 */
  dwt_setrxtimeout(DEFAULT_RX_TIMEOUT);
  
  /* 设置每种类型的中断所对应的回调函数 */
  dwt_setcallbacks(&txCallback, &rxCallback, &rxTimeoutCallback, &rxErrorCallback, NULL, NULL);
  
  /* 启用部分中断,未被启用的中断则会被 DWM3000 在硬件层面上屏蔽 */
  dwt_setinterrupt(SYS_ENABLE_LO_TXFRS_ENABLE_BIT_MASK |
                       SYS_ENABLE_LO_RXFCG_ENABLE_BIT_MASK |
                       SYS_ENABLE_LO_RXFTO_ENABLE_BIT_MASK |
                       SYS_ENABLE_LO_RXPTO_ENABLE_BIT_MASK |
                       SYS_ENABLE_LO_RXPHE_ENABLE_BIT_MASK |
                       SYS_ENABLE_LO_RXFCE_ENABLE_BIT_MASK |
                       SYS_ENABLE_LO_RXFSL_ENABLE_BIT_MASK |
                       SYS_ENABLE_LO_RXSTO_ENABLE_BIT_MASK,
                   0, DWT_ENABLE_INT);

  /* 清空 SPI Ready 中断 */
  dwt_write32bitreg(SYS_STATUS_ID,
                    SYS_STATUS_RCINIT_BIT_MASK | SYS_STATUS_SPIRDY_BIT_MASK);
                    
  algoSemaphore = xSemaphoreCreateMutex();

  return DWT_SUCCESS;
}

数据发送

static void uwbTxTask(void *parameters) {
  systemWaitStart();

  Ranging_Message_t packetCache;

  while (true) {
    if (xQueueReceive(txQueue, &packetCache, portMAX_DELAY)) {
      /* 强制 DWM3000 进入 IDLE 状态 */
      dwt_forcetrxoff();
      /* 设置需要传入的数据 */
      dwt_writetxdata(packetCache.header.msgLength, (uint8_t *) &packetCache, 0);
      /* 设置需要发送多长的数据,以及本次发送是否需要发送记录时间戳 */
      dwt_writetxfctrl(packetCache.header.msgLength + FCS_LEN, 0, 1);
      /* 调用 dwt_starttx() 发送数据,可以传入参数设置延迟多少秒发送,
         或者发送后立刻进入接收状态,具体设置参数可以参考 API 手册与函数注释 */
      if (dwt_starttx(DWT_START_TX_IMMEDIATE | DWT_RESPONSE_EXPECTED) ==
          DWT_ERROR) {
        DEBUG_PRINT("uwbTxTask:  TX ERROR\n");
      }
    }
  }
}

数据接收

static void rxCallback() {
  BaseType_t xHigherPriorityTaskWoken = pdFALSE;
  /* 获取当前接收到的数据的总长度 */
  uint32_t dataLength = dwt_read32bitreg(RX_FINFO_ID) & RX_FINFO_RXFLEN_BIT_MASK;
  if (dataLength != 0 && dataLength <= FRAME_LEN_MAX) {
    /* 读取接收到的数据到缓存中,注意接收数据的长度要去掉 FCS */
    dwt_readrxdata(rxBuffer, dataLength - FCS_LEN, 0);
  }
  dwTime_t rxTime;
  /* 读取接收到该数据的时间戳*/
  dwt_readrxtimestamp((uint8_t *) &rxTime.raw);
  
  /* 数据处理逻辑 */
  Ranging_Message_With_Timestamp_t rxMessageWithTimestamp;
  rxMessageWithTimestamp.rxTime = rxTime;
  Ranging_Message_t *rangingMessage = (Ranging_Message_t *) &rxBuffer;
  rxMessageWithTimestamp.rangingMessage = *rangingMessage;
  xQueueSendFromISR(rxQueue, &rxMessageWithTimestamp, &xHigherPriorityTaskWoken);
  
  /* 切换到 IDLE 状态 */
  dwt_forcetrxoff();
  /* 紧接着切换到接收状态 */
  dwt_rxenable(DWT_START_RX_IMMEDIATE);
}

static void rxTimeoutCallback() {
  dwt_forcetrxoff();
  dwt_rxenable(DWT_START_RX_IMMEDIATE);
}

这里需要注意,如果想保证 DWM3000 在不发送数据的时候一直保持接收状态,不仅需要在 rxCallback() 的最后手动转换到接收状态,还需要在接收超时中断中重新进入接收状态,最后还需要在每次发送数据时传入 DWT_RESPONSE_EXPECTED 参数或在发送数据后手动进入接收状态。

固件编译和烧录

在 Windows 下推荐使用 CLion + WSL2 进行开发,但如果需要用到外部的 USB,则只能通过虚拟机或者 Linux🐧 物理机来进行开发与调试。

由于采用了 Kbuild 来构建项目,因此在编译之前需要先选择当前 Kbuild 的编译配置。在项目根目录的 configs 文件夹下有一些默认的配置文件,如下图所示:

配置文件结构

其中 adhoc_alt_defconfigadhoc_alt_defconfigadhoc_uart2_defconfig 分别为 UART1、IO2 & IO3、UART2 模式的默认配置,根据无人机所搭载的 DWM3000 硬件工作模式的不同,我们在编译时需要选择对应的配置:

make adhoc_alt_defconfig

当然我们也可以随便选择一个默认配置,之后通过make menuconfig手动配置想要开启的功能以及 DWM3000 固件的工作模式:

使用Kbuild切换模式

在不勾选 adhoc deck alternative IRQ and RESET pinsadhoc deck use UART2 (TX2, RX2) pins 选项时默认为 UART1 模式。

在完成上述步骤后即可在当前配置文件下编译和烧录固件:

make -j
make cload

总结

驱动开发还是比较复杂的,不同的芯片有不同的电路特性,DW3000 的用户手册对于芯片的某些细节介绍不是很深入,很多时候需要自己写代码去测试。而硬件调试起来又比较复杂,出了问题只能靠猜加人肉 DEBUG😵,最难受的是由于芯片比较新,相关参考资料非常少,整个调试过程还是挺痛苦的。

其实一开始我对硬件驱动开发可以说完全一窍不通,但由于从 DW1000 芯片换到 DW3000 芯片的性能提升太过于诱人,从零开始调研芯片本身的特性到了解驱动开发的具体细节、学习基础的电路知识、再到最后驱动实现和调试本身,都花了我不少时间和精力。比较吊诡的是在完全了解芯片和驱动的工作原理后,最终实际花在编码上的时间并不多,主要时间还是在翻芯片手册和硬件调试上。

折腾了那么久,最终还是从零开始实现了 DW3000 的驱动,成就感满满。亲手从硬件调研到驱动实现的整个流程走下来,算是触类旁通了,对于各类硬件设备和驱动的工作原理也有了比较深刻的认识。