The Verdict: Why the Makerfabs ESP32 UWB DW3000?

If you are building an indoor positioning system (IPS) or high-precision ranging project, the Makerfabs ESP32 UWB DW3000 is currently the most practical off-the-shelf starting point. Raw Qorvo DW3000 breakout boards are notoriously frustrating to wire due to high-speed SPI routing requirements and strict impedance matching. Makerfabs solves this by integrating the DW3000 directly onto an ESP32-S3 carrier board, handling the RF layout and level-shifting for you.

Here is the decision path for choosing your UWB architecture. Do not overcomplicate your first build.

UWB Protocol Decision Tree
Your Scenario Recommended Protocol Hardware Required Complexity
Measuring distance between two points Two-Way Ranging (TWR) 2 Boards (1 Anchor, 1 Tag) Low
Tracking an asset in a 2D room Time Difference of Arrival (TDoA) 4+ Anchors, 1 Tag, wired sync High
Low-power mesh networking FiRa / IEEE 802.15.4z Multiple nodes + Gateway Expert
The Concrete Pick: If this is your first UWB project, buy exactly two Makerfabs ESP32-S3 UWB DW3000 boards and configure them for Two-Way Ranging (TWR). TDoA requires sub-nanosecond clock synchronization between anchors that will stall your progress for weeks. Master TWR first.

Hardware BOM and SPI Pin Mapping

The code and pinouts below target the Makerfabs ESP32-S3 UWB DW3000 (Model: EP32S3-UWB). This variant uses the ESP32-S3-WROOM-1-N8R8 module (8MB Flash, 8MB OPI PSRAM). Do not confuse this with their older ESP32 (original) DW1000 board; the silicon and register maps are entirely different.

Parts List

  • MCU + UWB Board: Makerfabs ESP32-S3 UWB DW3000 (~$55 - $65 USD)
  • Antenna: 6.5GHz UWB Patch Antenna with IPEX/U.FL connector (~$8 USD). Do not use 2.4GHz WiFi antennas; the VSWR mismatch will destroy your ranging accuracy.
  • Power: High-quality USB-C data cable and a 5V/2A power brick. UWB TX spikes can pull 150mA+ instantaneously; weak USB ports will cause brownouts.

SPI Pin Mapping (ESP32-S3 to DW3000)

The DW3000 communicates via SPI. While the ESP32-S3 allows flexible pin routing, Makerfabs hardwired the following GPIOs on the EP32S3-UWB PCB. You must use these exact pins in your firmware.

Makerfabs EP32S3-UWB Pinout Spec Sheet
DW3000 Pin ESP32-S3 GPIO Function / Notes
SPICLK GPIO 12 SPI Clock (Max 20MHz for init, 40MHz for data)
SPIMISO GPIO 13 Master In Slave Out
SPIMOSI GPIO 11 Master Out Slave In
SPICSn GPIO 10 Chip Select (Active LOW)
IRQ GPIO 4 Interrupt Request (Active HIGH)
RESET GPIO 5 Hardware Reset (Active LOW)
WAKEUP GPIO 6 Sleep Wakeup (Active HIGH)

Flashing the Baseline Anchor/Tag Firmware

Before writing complex ranging algorithms, you must verify the SPI bus is talking to the DW3000 silicon. The DW3000 has a specific Device ID register at address 0x00. For the DW3000, the expected 32-bit value is 0xDECA0302 (unlike the older DW1000 which returns 0xDECA0130).

Upload this exact code to your ESP32-S3 using the Arduino IDE. Set your board to ESP32S3 Dev Module, enable USB CDC On Boot, and set Flash Size to 8MB.

#include <SPI.h>

// --- Makerfabs ESP32-S3 UWB DW3000 Pin Definitions ---
#define DW3000_CS   10
#define DW3000_IRQ   4
#define DW3000_RST   5
#define DW3000_WAKE  6
#define SPI_SCK     12
#define SPI_MISO    13
#define SPI_MOSI    11

// DW3000 Register Addresses and Expected Values
#define DEV_ID_REG       0x00
#define DW3000_DEV_ID    0xDECA0302 // Expected ID for DW3000

SPISettings dw3000_SPI_Settings(8000000, MSBFIRST, SPI_MODE0); // 8MHz for safe init

void setup() {
  Serial.begin(115200);
  while(!Serial) { delay(10); }
  Serial.println("Makerfabs ESP32-S3 DW3000 SPI Init Test");

  // Configure Control Pins
  pinMode(DW3000_CS, OUTPUT);
  digitalWrite(DW3000_CS, HIGH); // Deselect
  pinMode(DW3000_RST, OUTPUT);
  pinMode(DW3000_WAKE, OUTPUT);
  
  // Hardware Reset Sequence
  Serial.println("Performing hardware reset...");
  digitalWrite(DW3000_RST, LOW);
  delay(50);
  digitalWrite(DW3000_RST, HIGH);
  delay(10); // Wait for DW3000 boot
  digitalWrite(DW3000_WAKE, HIGH); // Keep awake

  // Initialize SPI bus
  SPI.begin(SPI_SCK, SPI_MISO, SPI_MOSI, DW3000_CS);
  delay(100);

  // Read Device ID
  uint32_t dev_id = readDW3000DevID();
  
  if (dev_id == DW3000_DEV_ID) {
    Serial.printf("SUCCESS: DW3000 found. DEV_ID: 0x%08X\n", dev_id);
  } else {
    Serial.printf("ERROR: DW3000 DEV_ID mismatch. Read: 0x%08X, Expected: 0x%08X\n", dev_id, DW3000_DEV_ID);
    while(1) { delay(1000); } // Halt execution
  }
}

void loop() {
  // Idle loop - SPI verification is complete
  delay(1000);
}

// --- Helper Function: Read 32-bit DEV_ID ---
uint32_t readDW3000DevID() {
  uint32_t id = 0;
  SPI.beginTransaction(dw3000_SPI_Settings);
  digitalWrite(DW3000_CS, LOW);
  
  // Send read command (0x00 for DEV_ID register, bit 7 = 0 for read)
  SPI.transfer(0x00); 
  SPI.transfer(0x00); // Sub-address/length byte
  
  // Read 4 bytes (Little Endian in DW3000)
  id |= ((uint32_t)SPI.transfer(0x00) << 0);
  id |= ((uint32_t)SPI.transfer(0x00) << 8);
  id |= ((uint32_t)SPI.transfer(0x00) << 16);
  id |= ((uint32_t)SPI.transfer(0x00) << 24);
  
  digitalWrite(DW3000_CS, HIGH);
  SPI.endTransaction();
  return id;
}

Debugging: "DEV_ID Mismatch" and SPI Failures

If your serial monitor outputs the following exact string:

ERROR: DW3000 DEV_ID mismatch. Read: 0x00000000, Expected: 0xDECA0302

Do not immediately assume the chip is dead. A read of 0x00000000 or 0xFFFFFFFF means the ESP32-S3 is not seeing the DW3000 on the SPI bus. Here are the first three things to check, ranked by probability.

1. SPI Bus Contention and Pin Mapping (Most Likely)

The ESP32-S3 has multiple SPI buses (SPI2 and SPI3). The Arduino SPI library defaults to specific pins depending on the board definition. If you did not explicitly pass the pins to SPI.begin(SCK, MISO, MOSI, CS) as shown in the code above, the ESP32 might be toggling the wrong GPIOs. Fix: Verify your code uses the exact GPIOs from the spec sheet table. Use a multimeter in continuity mode to beep out the trace from the ESP32-S3 module pad to the DW3000 chip if you suspect a PCB manufacturing defect.

2. Power Supply Brownout During TX Init

The DW3000 requires a clean 3.3V rail. When the chip initializes its PLL and RF synthesizers, it can draw sudden current spikes exceeding 150mA. If you are powering the Makerfabs board from a cheap USB hub or a PC USB port limited to 500mA (shared with other devices), the 3.3V LDO on the board will droop, causing the DW3000 to silently reset or fail to initialize. Fix: Plug the board directly into a dedicated 5V/2A wall adapter. Measure the 3.3V rail with an oscilloscope; if you see dips below 3.1V during the readDW3000DevID() call, add a 100µF tantalum capacitor across the 3.3V and GND header pins.

3. SPI Clock Speed Too High for Initialization

According to the Qorvo DW3000 Datasheet, the SPI clock must not exceed 20MHz during the initial boot phase before the internal PLL is locked. Fix: Ensure your SPISettings is initialized to 8MHz or 10MHz for the first register read. Only increase to 20MHz+ after you have successfully read the DEV_ID and configured the internal clock multiplier.

Extending the Build: Moving to Two-Way Ranging

Once your SPI bus returns the correct 0xDECA0302 DEV_ID, you are ready to simplify or extend the build into actual ranging.

How to Simplify

If you just want to see UWB packets in the air without doing math, strip out the ranging logic and configure one board as a continuous Blink Transmitter and the other as a Sniffer/Receiver. This isolates RF transmission issues from timestamping math errors. Use the open-source Arduino DW3000 libraries available on GitHub, specifically looking for the BasicSender and BasicReceiver examples.

How to Extend (The TWR Path)

To get actual distance measurements in centimeters, you must implement Two-Way Ranging (TWR). TWR works by sending a poll, receiving a response, and calculating the time-of-flight (ToF) of the radio waves.

  1. Assign Roles: Flash Board A with the Anchor firmware (Listens for polls, sends responses). Flash Board B with the Tag firmware (Sends polls, calculates ToF).
  2. Antenna Delay Calibration: This is the step most hobbyists miss. The DW3000 timestamps packets at the digital baseband, but the signal takes time to travel through the PCB traces, the IPEX connector, and the antenna itself. You must manually calibrate the TX_ANT_DLY and RX_ANT_DLY registers. Place the two boards exactly 1.00 meters apart. Adjust the antenna delay values in code until the serial monitor reports exactly 1.00m.
  3. Line of Sight (LoS): UWB at 6.5GHz is heavily attenuated by human bodies and concrete. For your first tests, place the boards on wooden stands at least 1 meter off the ground to prevent floor multipath reflections from corrupting the leading-edge detection algorithm.

By starting with the Makerfabs integrated board, verifying the SPI bus with the baseline DEV_ID script, and strictly following the TWR calibration protocol, you bypass 90% of the hardware headaches that plague raw UWB implementations. Get your 1-meter baseline working perfectly before you even think about adding a third anchor.