Skip to content

Instantly share code, notes, and snippets.

@egnor
Last active March 11, 2024 18:29
Show Gist options
  • Star 20 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save egnor/455d510e11c22deafdec14b09da5bf54 to your computer and use it in GitHub Desktop.
Save egnor/455d510e11c22deafdec14b09da5bf54 to your computer and use it in GitHub Desktop.

DW3000 "missing manual" notes

Overall

The DW3000 is an exciting part, available as a convenient Arduino-shield eval board with good distribution. HOWEVER, this is NOT a "maker friendly" part with SparkFun or Adafruit type tutorials and examples! It is a sophisticated radio that can be the heart of a positioning system, but you have to do quite a lot of heavy lifting to get there.

For basic use, the older-but-still-good DW1000 may be a better choice; interface libraries are available for Arduino and Raspberry Pi. Or look into packaged location-system vendors, like Estimote, Pozyx, Ubitrack and many others.

Documentation and APIs

The DW3000 user manual is actually pretty decent. Expect to cuddle up with this tome. However, it IS incomplete; some important notes are missing, and other parts refer you to the reference driver implementation for details (e.g. accessing OTP memory).

Qorvo would very much like you to use that reference driver library ("API Software and API Guide" here) to access the part. This reference implementation encapsulates lots of undocumented wisdom. HOWEVER, the current version (v1.2) is only available as a binary blob, packaged for the ST NUCLEO-F429ZI or Nordic nRF52840-DK dev boards. You MIGHT be able to use these blobs on another Cortex-M4 or Cortex-M33 based system, writing your own hardware access layer (for SPI and interrupts).

For the moment, source code for an older Qorvo driver version (v1.1) is still available for download; look for the nested DW3000_API_C0_rev4p0.zip. You can compile this for your platform, or just use it as reference. There is even some Raspberry Pi support! Of course, it will be buggier and less polished than newer versions. (I have confirmed with Qorvo's distributor Symmetry Electronics that source code for the current version of the dwt_uwb_driver core is not available to customers, even under NDA. However, support for more micros is in the works.)

As an alternative, you can use the Makerfabs driver for the ESP32 or Emin Eminof's driver for the Arduino, both of which seem to be based on Qorvo's source code.

If you want to write your own driver (or debug an existing one), you'll be running into hidden gotchas. This document is intended to help.

Known undocumented magic

(This is a very incomplete work in progress!)

Accessing OTP memory

The user manual describes the layout of OTP (One Time Programmable) memory but refers to driver functions for reading and writing those values. The Qorvo driver uses these steps to read an OTP register:

  1. set OTP_MAN (bit 0) in OTP_CFG
  2. write the OTP register address (7 bits) to OTP_ADDR
  3. clear OTP_MAN (bit 0) and set OTP_READ (bit 1) in OTP_CFG
  4. Read the value (32 bits) from OTP_RDATA
  5. clear OTP_READ (bit 1) in OTP_CFG

Writing OTP memory is more involved (and more dangerous!) but not typically necessary.

Register initialization from OTP

The OTP memory contains calibration/tuning values which should be loaded into operating registers on startup (and possibly wake from sleep?). There are "KICK" bits in the OTP_SF register to do this, but apparently they don't do a complete job.

From dwt_initialise() in deca_device.c (before PLL setup)

  • set LDO_KICK in OTP_CFG
  • set BIAS_KICK (undocumented bit 8!) in OTP_CFG
  • copy bits 16-20 (5 bits) from OTP BIASTUNE_CAL into bits 0-5 of BIAS_CTRL
  • copy bits 0-5 (6 bits) from OTP XTAL_Trim into XTAL

From dwt_configure() in deca_device.c

  • set DGC_KICK and (if ch9) DGC_SEL in OTP_CFG
  • set OPS_KICK and appropriate OPS_SEL bits in OTP_CFG

The Qorvo driver checks if the OTP values are 0, and uses hardcoded values instead of setting KICK bits if so. On DWM (Arduino-shield) boards, these OTP values should all be programmed.

Receiver calibration

Receiver calibration (aka "PGF calibration") must run successfully at startup (and after wakeup or 20°C temperature change) for decent performance. The manual describes how to start calibration with RX_CAL and check results in RX_CAL_RESI and RX_CAL_RESQ but misses some details:

  • Before running calibration, bits 0 (VDDMS1), 2 (VDDMS3), and 8 (VDDIF2) must be set in LDO_CTRL
  • Before reading RX_CAL_RESI/RESQ, bit 16 in RX_CAL_CFG (the low bit of COMP_DLY) must be set
  • (After calibration, the previous value of LDO_CTRL should be restored to save power.)

Without these steps, calibration will fail (missing LDOs) and the failure won't be noticed (result values not being read properly), but the radio will perform very badly.

DTUNE3

The manual says to always change the default 0xAF5F584C to 0xAF5F35CC. However, the Qorvo driver keeps the default (...584C) in most cases, only using the replacement (...35CC) when sending packets with no data.

RX_CTRL_HI

The Qorvo driver sets this undocumented register at 07:10 to 0x08B5A833 for ch9 (it is left alone for ch5), but this value is the default anyway so you don't have to worry about it.

Delayed TX and HPDWARN

The User Manual says this about detecting too-late submission of a delayed TX request (section 9.4.1):

Due to an errata in the DW3000, there is a case when neither the HPDWARN event gets set nor does the packet get transmitted. ... The host can check for this issue by reading the PMSC_STATE. When this bug occurs, the PMSC_STATE will be “TX” but TX_STATE will be “IDLE”, the TXFRS event will never be set, see state descriptions in 8.2.14.19 Sub-register 0x0F:30 – System state. The host should abort the transmission in this case. This check and recovery is implemented in the published DW3000 dwt-starttx() [sic] API.

However, the PMSC_STATE (aka TSE_STATE) bitfield is inconsistently described, and there are several "TX" states. The Qorvo driver reports an error if SYS_STATE == 0x000D0000 (which looks for a specific "TX" substate).

"SCP" mode

According to the API documentation, preamble codes 25-29 are associated with "SCP", as distinct from 16MHz or 64MHz PRF codes. A header comment describes SCP as "UWB PRF ~100MHz", and the Qorvo driver uses different parameter sets when SCP mode is selected. The chip user manual doesn't mention any of this.

The significance and purpose of "SCP" mode remains a mystery.

List of undocumented registers

These registers are named in deca_regs.h but not in the user manual:

  • 02:34 LCSS_MARGIN - unused in Qorvo driver
  • 03:1C DGC_CFG(0, 1) - hardcoded if OTP DGC data is missing
  • 03:38 DGC_LUT_(0-6)_CFG - hardcoded if OTP DGC data is missing
  • 07:10 RF_RX_CTRL_HI - loaded with magic value 0x08B5A833 for ch9
  • 0E:1E PGF_DELAY_COMP_(LO, HI) - unused in Qorvo driver
  • 11:10 PWR_UP_TIMES_LO - TXFSEQ, but at 11:10 instead of 11:12 (??)

There are also numerous undocumented fields in otherwise-documented registers (e.g. BIAS_KICK in OTP_CFG).

Things which are actually documented but easy to miss

  • If using a 16MHz PRF (PCODE 3 or 4), set RX_TUNE_EN in DGC_CFG
  • Always change THR_64 in DGC_CFG to 0x32
  • Always clear DT0B4 in DTUNE0
  • Always change COMP_DLY in RX_CAL to 0x2
  • Always change LDO_RLOAD to 0x14
  • Always change RF_TX_CTRL_1 to 0x0E
  • Always change RF_TX_CTRL_2 to 0x1C071134 (ch5) or 0x1C010034 (ch9)
  • Always change PLL_CFG to 0x1F3C (ch5) or 0x0F3C (ch9)
  • Always change PLL_CFG_LD in PLL_CAL to 0x8 (documented as 0x81 but that's the whole register)
  • For accurate ranging you need to calibrate antenna delay (see APS014)

References

Qorvo official

Community projects

Notable forum discussions

@indietyp
Copy link

indietyp commented Jan 4, 2023

Thank you very much. This has been an enormous help in my voyage to decode the mysteries of the DW3000 driver code.

@indietyp
Copy link

indietyp commented Jan 5, 2023

I have decompiled and analyzed the newest driver available and found the following differences:

The version analysed is 06.00.07

Architecture Differences

The driver no longer ships as a single binary for a single chip but instead is made up of:

  • deca_compat
  • deca_interface
  • dw3000_device
  • dw3700_device
  • dw3720_device

According to the forums, dw37XX is a transitional chip that never went to market.1

deca_compat now uses an ioctl to send commands to the *_device and dynamically loads a driver in dwt_probe(). Most of the commands are relayed that way to the specific device driver.

Speculations

SCP

While SCP is not documented at all, it seems that SCP mode is in the range of 25..=29 and, if enabled, will enable the CIA (channel impulse analyzer).

This means:

  • 0x0B:08: OPS_KICK of reserved (01)
  • 0x0E:0C: IP_NTM = 6, IP_PMULT = 0, 1 into bit 9, IP_RTM = 0
  • 0x0E:0E: IP_RTM = 0, STS_NTM = 0, STS_PMULT = 0
  • 0x0E:12: STS_NTM = 0xA (instead of 0xC), bits 13 = 0,14 = 1, STS_MNTH = 12
  • 0x0E:16: RES_B0 to 0x9D, instead of the suggested 0x9B from the user manual

An interesting part is how 0x0E:0E partially overwrites what was set in 0x0E:0C and is overwritten partially by the 0x0E:12, and instead of using the dwt_write16bitoffsetreg, the dwt_write32bitoffsetreg was chosen instead. This seems odd (given how all the other code is written) and suggests that SCP mode may be a relic of the past or not tested(?).

What is SCP?

The FiRa Consortium is a non-profit tasked with standardizing UWB. Qorvo is a member, and I found a whitepaper from qorvo2, which includes SCP. It turns out that SCP is the Secure Channel Protocol3, which is primarily used on smart cards.

Changes between versions

The new version is essentially the same, with some minor differences.

dwt_initialise()

XTAL is now set via the first 6 bits (via 0x3F mask) instead of the first 7 (via 0x7F mask). (_dwt_otpread(XTRIM_ADDRESS) & 0x3f vs _dwt_otpread(XTRIM_ADDRESS) & 0x7f)

This now also loads 0x35 (PLL_LOCK_CODE) of the OTP into the 0x09:04 (PLL coarse code) register, but only if that value is != 0

dwt_configure()

I do not know if this were compiler optimizations at play here, but the equation for the ststhreshold is no longer (int16_t)((((uint32_t)sts_len) * 8) * STSQUAL_THRESH_64);, but instead (uint16_t)(sts_len * 0x26668 >> 0xf).

Length v04 v06
32 28 19
64 57 38
128 115 76
256 230 153
512 460 307
1024 921 614
2048 1843 1228

If SCP is not enabled after MNTH has been calculated and written into 0x0E:12, regardless if STS is enabled, the driver will now set in 0x0E:16 (0xBFFFFF00:0x94):

  • RES_B0 = 0x94 Note: This deviates from the default described in the user manual
  • STS_SS_EN = 0

DTUNE0 now sets DT0B4 to 0 only if PDoA mode 1, and otherwise always sets it to 1, previously DT0B4 was never touched.

The code for another default of DTUNE3 if STS_ND was selected was removed, 0xAF5F35CC is now set regardless of STS mode.

At the end a new undocumented register is written to, dubbed DTUNE4, with the following code:

  if (tx_preamble_length < 65) {
    uVar2 = 0x14000000;
  }
  else {
    uVar2 = 0x20000000;
  }

  dwt_modify32bitoffsetreg(chip,0x60010,0,0xffffff,uVar2);

👂 Open Questions

The default timeout is now:

  if (config->sfdTO == 0) {
    *(undefined *)&config->sfdTO = 0x81;
    *(undefined *)((int)&config->sfdTO + 1) = 0;
  }

I am unsure if this is a decompilation thing or an actual field is reset, further investigation is required 🔍

dwt_setdwstate

DWT_DW_IDLE

Before setting AINIT2IDLE, this now sets CAL_EN = 1 and USE_OLD = 1 in the 0x09:08 register.

dwt_pgf_cal

Before dwt_run_pgfcal, the chip will sleep for deca_usleep(20).

It is also odd that in dwt_run_pgfcal, (in v04 and v06), before reading the calibration results, COMP_DLY is set to 0x3. This is undocumented, and the user manual states no other value than 0x02 should be used.

dwt_or8bitoffsetreg(RX_CAL_CFG_ID, 0x2, 0x1); //enable reading

dwt_enable_rf_tx

This function no longer distinguishes between channels 5 and 7 and instead writes: 0x2003c00 to RF_ENABLE_ID.

dwt_enable_rftx_blocks

This function no longer distinguishes between channels 5 and 7 and writes: 0x2003c00 to RF_CTRL_MASK_ID.

dwt_getframelength

AFAIK this is a new function

dwt_rxenable

On DWT_START_RX_DLY_RS and DWT_START_RX_DLY_TS respectively, a new function is called _dwt_adjust_delaytime with 0 as arg for DWT_START_RX_DLY_RS, otherwise 1. The source code of _dwt_adjust_delaytime is roughly:

void _dwt_adjust_delaytime(dwchip_s *chip,int mode)

{
  byte rx_antenna_delay_hi;
  byte tx_antenna_delay_hi;
  uint32_t dx_time;
  
  if (mode == 0) {
    dx_time = dwt_read32bitoffsetreg(chip,0x2c,0);
    rx_antenna_delay_hi = dwt_read8bitoffsetreg(chip,0xe0000,1);
    dwt_write32bitoffsetreg(chip,0x2c,0,dx_time - rx_antenna_delay_hi);
  }
  else {
    dx_time = dwt_read32bitoffsetreg(chip,0x2c,0);
    tx_antenna_delay_hi = dwt_read8bitoffsetreg(chip,0x10004,1);
    dwt_write32bitoffsetreg(chip,0x2c,0,dx_time - tx_antenna_delay_hi);
  }
  return;
}

It seems like it now accounts for the delay of each antenna (tho if dx_time < *_antenna_delay then a underflow would occur)

Other Discoveries

It seems that you can read and write from the Scratch RAM (located at 0x16), but you are unable to use masked write transactions on that particular memory region.


I will update my notes with more information once available

Footnotes

  1. https://forum.qorvo.com/t/what-is-dw3700/12533

  2. https://www.firaconsortium.org/sites/default/files/2022-08/FIRA-Whitepaper-UWB-Secure-Ranging-August-2022.pdf

  3. https://globalplatform.org/wp-content/uploads/2017/09/GPC_2_3_F_SCP11_v1.2_PublicRelease.pdf

@egnor
Copy link
Author

egnor commented Jan 5, 2023

(Thanks!! Let me know when things are settled and I can update the main gist)

@Fhilb
Copy link

Fhilb commented Feb 15, 2023

Hey, thank you very much! That is a great piece of help.
One question tho, either the dw3000 documentation or you got it wrong. You are stating the following:
"Always change PLL_CFG_LD in PLL_CAL to 0x8". The original Documentation states it should be changed to 0x81 (which is not fitting inside a 6 bit register space).
So I suppose your approach is right here?

@egnor
Copy link
Author

egnor commented Feb 16, 2023

4 bits, actually. Here's the manual section I was working from
image
I interpreted the 0x31 and 0x81 as (incorrectly and misleadingly) referring to the value of the entire PLL_CAL register, not just the PLL_CFG_LD nybble. This seems likely because the default value of the entire PLL_CAL register is in fact 0x31, and because as you point out those 8-bit values can't fit in a 4-bit subfield. I'll update the gist to clarify. (Still need to see if I can work in @indietyp's notes!)

@cl0rm
Copy link

cl0rm commented Mar 23, 2023

A huge thanks to @indietyp and @egnor for this. Currently, CH9 is not working very well for me (seems like PLL is not calibrated very well), maybe I will get it working using this.

@cl0rm
Copy link

cl0rm commented Mar 23, 2023

Also, another bit of information I got from Qorvo via Email:
SCP stands for Short Compressed Packet. It allows TXing at 27 MBit/s but is a proprietary mode.
They couldn't say much more about it to me, however they stated that is unsupported.
I'm pretty sure that it uses a PRF of 124,8 MHz, which should also be the "~120MHz" PLL clock frequency. It is exactly double the 62,4 MHz 15.4z PRF. I'm guessing it's similar to the 15.4z High Pulse Rate ERDEV (which supports 124,8 MHz and 249,6 MHz PRF)

@egnor
Copy link
Author

egnor commented Mar 23, 2023

I really need to work all these comments into the original doc for clarity! (Or if one of you wants to fork it and make those edits, I can copy them back. I don't think there's such a thing as a PR for gists? Maybe this should live in a proper repo.)

@gabeart10
Copy link

gabeart10 commented Nov 1, 2023

Thank you for creating these notes! Would anyone happen to have a copy of the DW3000_API_C0_rev4p0.zip? The links to the old v1.1 API seem to only have the newer API (DW3XXX_API_rev9p3.zip) now and I haven't been able to find a copy on the web.

@nickd4
Copy link

nickd4 commented Mar 9, 2024

You can get it from:
https://download.csdn.net/download/weixin_44845841/86263459
However, foreigners can't join CSDN. The way I did it was to buy the product from:
https://duyhieu.com/product/download-form-csdn/
This cost me USD2. And once you complete the purchase you get access to a downloader menu which downloads the file from CSDN on your behalf, you have 3 uses of the downloader menu and no refunds. Hmm. Anyway, it worked for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment