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.
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.
(This is a very incomplete work in progress!)
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:
- set
OTP_MAN(bit 0) inOTP_CFG - write the OTP register address (7 bits) to
OTP_ADDR - clear
OTP_MAN(bit 0) and setOTP_READ(bit 1) inOTP_CFG - Read the value (32 bits) from
OTP_RDATA - clear
OTP_READ(bit 1) inOTP_CFG
Writing OTP memory is more involved (and more dangerous!) but not typically necessary.
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_KICKinOTP_CFG - set
BIAS_KICK(undocumented bit 8!) inOTP_CFG - copy bits 16-20 (5 bits) from OTP
BIASTUNE_CALinto bits 0-5 ofBIAS_CTRL - copy bits 0-5 (6 bits) from OTP
XTAL_TrimintoXTAL
From dwt_configure() in deca_device.c
- set
DGC_KICKand (if ch9)DGC_SELinOTP_CFG - set
OPS_KICKand appropriateOPS_SELbits inOTP_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 (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 inLDO_CTRL - Before reading
RX_CAL_RESI/RESQ, bit 16 inRX_CAL_CFG(the low bit ofCOMP_DLY) must be set - (After calibration, the previous value of
LDO_CTRLshould 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.
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.
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.
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).
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.
These registers are named in deca_regs.h but not in the user manual:
02:34 LCSS_MARGIN- unused in Qorvo driver03:1C DGC_CFG(0,1) - hardcoded if OTP DGC data is missing03:38 DGC_LUT_(0-6)_CFG- hardcoded if OTP DGC data is missing07:10 RF_RX_CTRL_HI- loaded with magic value0x08B5A833for ch90E:1E PGF_DELAY_COMP_(LO,HI) - unused in Qorvo driver11:10 PWR_UP_TIMES_LO-TXFSEQ, but at11:10instead of11:12(??)
There are also numerous undocumented fields in otherwise-documented registers (e.g. BIAS_KICK in OTP_CFG).
- If using a 16MHz PRF (PCODE 3 or 4), set
RX_TUNE_ENinDGC_CFG - Always change
THR_64inDGC_CFGto0x32 - Always clear
DT0B4inDTUNE0 - Always change
COMP_DLYinRX_CALto0x2 - Always change
LDO_RLOADto0x14 - Always change
RF_TX_CTRL_1to0x0E - Always change
RF_TX_CTRL_2to0x1C071134(ch5) or0x1C010034(ch9) - Always change
PLL_CFGto0x1F3C(ch5) or0x0F3C(ch9) - Always change
PLL_CFG_LDinPLL_CALto0x8(documented as0x81but that's the whole register) - For accurate ranging you need to calibrate antenna delay (see APS014)
- DW3000 user manual
- v1.2 DW3000 API, binary blob
- v1.1 DW3000 API, with source
- DW1000 user manual (for the older chip, but more complete-- good to cross check for insight)
- NConcepts/Makerfabs driver for the ESP32
- Emin Eminof's driver for ATMega328p Arduinos
- Port of the v1.1 Qorvo driver to the Zephyr RTOS
- Rust driver for the DW3000
- Raspberry Pi/Python access to the DW1000 (for the older chip, but good reading)
- "Sadly we only distribute this release package under NDA" (forum post)
- Missing and ambiguous information in DW3000 user manual (forum post)
- "Missing manual" for register-level access to DW3000 (discussion of this doc!)

I have decompiled and analyzed the newest driver available and found the following differences:
Architecture Differences
The driver no longer ships as a single binary for a single chip but instead is made up of:
deca_compatdeca_interfacedw3000_devicedw3700_devicedw3720_deviceAccording to the forums,
dw37XXis a transitional chip that never went to market.1deca_compatnow uses anioctlto send commands to the*_deviceand dynamically loads a driver indwt_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..=29and, if enabled, will enable theCIA(channel impulse analyzer).This means:
0x0B:08:OPS_KICKof reserved (01)0x0E:0C:IP_NTM = 6,IP_PMULT = 0,1into bit9,IP_RTM = 00x0E:0E:IP_RTM = 0,STS_NTM = 0,STS_PMULT = 00x0E:12:STS_NTM = 0xA(instead of0xC), bits13 = 0,14 = 1,STS_MNTH = 120x0E:16:RES_B0to0x9D, instead of the suggested0x9Bfrom the user manualAn interesting part is how
0x0E:0Epartially overwrites what was set in0x0E:0Cand is overwritten partially by the0x0E:12, and instead of using thedwt_write16bitoffsetreg, thedwt_write32bitoffsetregwas 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 thatSCPis 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()XTALis now set via the first6bits (via0x3Fmask) instead of the first7(via0x7Fmask). (_dwt_otpread(XTRIM_ADDRESS) & 0x3fvs_dwt_otpread(XTRIM_ADDRESS) & 0x7f)This now also loads
0x35(PLL_LOCK_CODE) of the OTP into the0x09:04(PLL coarse code) register, but only if that value is!= 0dwt_configure()I do not know if this were compiler optimizations at play here, but the equation for the
ststhresholdis no longer(int16_t)((((uint32_t)sts_len) * 8) * STSQUAL_THRESH_64);, but instead(uint16_t)(sts_len * 0x26668 >> 0xf).If SCP is not enabled after
MNTHhas been calculated and written into0x0E:12, regardless ifSTSis enabled, the driver will now set in0x0E:16(0xBFFFFF00:0x94):RES_B0 = 0x94Note: This deviates from the default described in the user manualSTS_SS_EN = 0DTUNE0now setsDT0B4to0only ifPDoAmode 1, and otherwise always sets it to1, previouslyDT0B4was never touched.The code for another default of
DTUNE3ifSTS_NDwas selected was removed,0xAF5F35CCis now set regardless ofSTSmode.At the end a new undocumented register is written to, dubbed
DTUNE4, with the following code:👂 Open Questions
The default timeout is now:
I am unsure if this is a decompilation thing or an actual field is reset, further investigation is required 🔍
dwt_setdwstateDWT_DW_IDLEBefore setting
AINIT2IDLE, this now setsCAL_EN = 1andUSE_OLD = 1in the0x09:08register.dwt_pgf_calBefore
dwt_run_pgfcal, the chip will sleep fordeca_usleep(20).It is also odd that in
dwt_run_pgfcal, (in v04 and v06), before reading the calibration results,COMP_DLYis set to0x3. This is undocumented, and the user manual states no other value than0x02should be used.dwt_enable_rf_txThis function no longer distinguishes between channels 5 and 7 and instead writes:
0x2003c00toRF_ENABLE_ID.dwt_enable_rftx_blocksThis function no longer distinguishes between channels 5 and 7 and writes:
0x2003c00toRF_CTRL_MASK_ID.dwt_getframelengthAFAIK this is a new function
dwt_rxenableOn
DWT_START_RX_DLY_RSandDWT_START_RX_DLY_TSrespectively, a new function is called_dwt_adjust_delaytimewith0as arg forDWT_START_RX_DLY_RS, otherwise1. The source code of_dwt_adjust_delaytimeis roughly:It seems like it now accounts for the delay of each antenna (tho if
dx_time < *_antenna_delaythen 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
https://forum.qorvo.com/t/what-is-dw3700/12533 ↩
https://www.firaconsortium.org/sites/default/files/2022-08/FIRA-Whitepaper-UWB-Secure-Ranging-August-2022.pdf ↩
https://globalplatform.org/wp-content/uploads/2017/09/GPC_2_3_F_SCP11_v1.2_PublicRelease.pdf ↩