PCB Net Routing Assistance

Diagnostic confirmed: the L2/L3 zones have copper geometry but are missing net assignments (effectiveNetId is null), so DRC treats them as decorative copper, not net-bound planes.
Fix in two steps:
STEP 1 — Create missing zones with explicit net assignment
The L2 GND zone and L3 3V0 dominant zone do not exist as proper queryable nodes. Create them now:
a) L2 GND PLANE
  • Layer: Mid-Layer 1 (L2)
  • Net: GND (assign explicitly — set effectiveNetId / net property)
  • Shape: circle, diameter 57.4mm, centred at (0, 0) — slightly smaller than the 58mm board outline to maintain 0.3mm edge clearance
  • Fill type: solid copper pour
  • Connection style: thermal relief to net-matching pads
  • Thermal relief: 4 spokes, 0.4mm spoke width, 0.3mm clearance
  • Stitching density: 2mm
b) L3 3V0 DOMINANT POUR
  • Layer: Mid-Layer 2 (L3)
  • Net: 3V0 (assign explicitly)
  • Shape: circle, diameter 57.4mm, centred at (0, 0), MINUS the two existing island regions (VSYS and 1V8_SGP41) — i.e., the 3V0 pour fills L3 everywhere EXCEPT inside the islands
  • Fill type: solid copper pour
  • Connection: thermal relief, same parameters as L2
STEP 2 — Assign net to existing islands
The L3 VSYS and L3 1V8_SGP41 island zones exist but have no net assignment. Set them now:
c) L3 VSYS ISLAND
  • Existing zone at (10, 18), 10×12mm
  • Net: VSYS (assign explicitly — currently has no effectiveNetId)
d) L3 1V8_SGP41 ISLAND
  • Existing zone at (-10, 22), 8×8mm
  • Net: 1V8_SGP41 (assign explicitly)
After Step 1 and Step 2:
  • Force a pour rebuild / fill regeneration on all 4 zones
  • Verify each zone now reports a valid effectiveNetId matching its intended net
  • Re-run DRC and report:
    • New total airwire count
    • Specifically the GND, 3V0, VSYS, 1V8_SGP41 airwire counts
    • Render each layer (L1, L2, L3, L4) separately so I can verify the copper fills look correct
Expected result: airwire count should drop dramatically (from 170 to under 30), because most of the GND/3V0/VSYS airwires resolve through the now-net-bound planes.
If after this the count is still > 50, do NOT iterate further. Report back and stop. I'll handle the next decision from outside Flux.

0:12

Thought for a moment

I’ll attempt the four-zone net binding exactly as specified, then verify DRC and layer rendering. First I’m checking whether the layout editor exposes net assignment for copper pours; if it does, I’ll apply the plane fixes and validate immediately.

0:33

Thought for 4 sec

1/3 Check whether copper-zone net assignment is editable in the current layout controls

Thought for 6 sec

Thought for 4 sec

3/3 tasks completed.

Thought for a moment

Blocked: I can’t complete this safely because the current layout controls do not expose any editable net/effectiveNetId/net-name property for zones. I verified that connectedLayers, shape, stitching, and thermal-style rules exist, but net, Net Name, and effectiveNetId are not valid editable zone rules.
I did not create more decorative copper, because that would repeat the same failure mode. The next decision needs to happen outside this interface: bind the copper pours to nets in the PCB editor/API, then rerun DRC and layer renders.
Find zone net UI
List airwires by net
Inspect zone properties
Export layout data

Refine this doc
Ask about this doc
Firmware Starter — MDBT50Q-P1MV2 / nRF52840
Hardware notes from the schematic
  • MCU module: Raytac MDBT50Q-P1MV2, nRF52840 inside.
  • Main logic rail: 3V0.
  • USB sense: module VBUS tied to VBUS_5V.
  • VDDH is tied directly to 3V0. The previous Schottky D4 VBUS_5V-to-VDDH path was removed so the MCU remains powered correctly when USB is unplugged.
  • DCCH uses 10 uH inductor L3 to VDD/3V0 and 1 uF C14 to GND.
  • QSPI flash: W25Q64JVSSIQ, 3V-compatible substitute for requested W25Q64JWSSIQ because the JW family is 1.8 V.
  • SGP41: VDD is powered from 1V8_SGP41 for the sensing/hotplate supply, while VDDH is powered from 3V0 for the digital/I2C interface supply. This matches the intended split-supply topology and keeps I2C compatible with the 3V0 pull-ups.
Pin mapping

Table


FunctionnRF52840 pinSchematic netConnected deviceNotes
LF crystal XL1P0.00XL1_32KY1 OSC1, C15 12 pF32.768 kHz crystal
LF crystal XL2P0.01XL2_32KY1 OSC2, C16 12 pF32.768 kHz crystal
Battery ADCP0.04 / AIN2AIN2_BAT_DIVR10/R11 100k divider, C21 100 nFADC measures VSYS/2
I2C SDAP0.26I2C_SDABME280, SGP41, SPS30 connector, R7 4.7k3V0 pull-up
I2C SCLP0.27I2C_SCLBME280, SGP41, SPS30 connector, R8 4.7k3V0 pull-up
QSPI CSP0.17QSPI_CS_FLASHW25Q64JV /CSExternal flash
QSPI CLKP0.19QSPI_CLKW25Q64JV CLKExternal flash
QSPI IO0P0.21QSPI_IO0W25Q64JV DI/IO0External flash
QSPI IO1P0.22QSPI_IO1W25Q64JV DO/IO1External flash
QSPI IO2P0.23QSPI_IO2W25Q64JV WP#/IO2External flash
QSPI IO3P1.00QSPI_IO3W25Q64JV HOLD#/IO3External flash; SWO left unused
STATUS LEDP0.13LED_STATUSR12 1k, D5 greenGPIO high turns LED on
Firmware ERR LEDP0.14LED_FW_ERRR13 1k, D6 redGPIO high turns LED on
User buttonP0.15USER_BUTTONSW1 to GNDUse internal pull-up; pressed = LOW
Reset/SWD resetP0.18/nRESETMCU_RESETTag-Connect pin 3Debug reset
SWDIOSWDIOSWDIOTag-Connect pin 2Debug
SWDCLKSWDCLKSWDCLKTag-Connect pin 4Debug
SWD VTref3V03V0Tag-Connect pin 1Target voltage reference
SWD GNDGNDGNDTag-Connect pin 5Debug ground
SWONCTag-Connect pin 6Optional; not wired because P1.00 is QSPI IO3
I2C addresses
  • BME280: 0x76, because SDO is pulled to GND.
  • SGP41: 0x59.
  • SPS30: typically 0x69 for the Sensirion I2C interface.
PlatformIO setup
Use Arduino for initial bring-up. The exact board variant should match your nRF52840 module/package and external flash configuration; if no custom MDBT50Q-P1MV2 variant exists yet, start from a close nRF52840 Arduino core board and migrate to a custom board definition before relying on QSPI flash in production.

Ini


[env:nrf52840_wearable]
platform = nordicnrf52
board = adafruit_feather_nrf52840
framework = arduino
monitor_speed = 115200
lib_deps =
  adafruit/Adafruit BME280 Library
  adafruit/Adafruit Unified Sensor
  sensirion/Sensirion I2C SGP4x
  sensirion/Sensirion Core
Complete Arduino bring-up source
This source brings up GPIO, I2C sensors, the SPS30 I2C connector, LEDs, button, and battery ADC. QSPI flash pins are documented above; production QSPI enablement should be done in the nRF52 board definition or with a Nordic SDK/Zephyr configuration because the QSPI peripheral claims fixed pins at low level.

Cpp


#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
#include <SensirionI2CSgp41.h>

// Pin definitions from schematic
static constexpr uint8_t PIN_I2C_SDA = 26;      // P0.26 -> I2C_SDA
static constexpr uint8_t PIN_I2C_SCL = 27;      // P0.27 -> I2C_SCL
static constexpr uint8_t PIN_AIN2_BAT = 4;      // P0.04 / AIN2 -> battery divider midpoint
static constexpr uint8_t PIN_LED_STATUS = 13;   // P0.13 -> R12 -> D5 green
static constexpr uint8_t PIN_LED_ERR = 14;      // P0.14 -> R13 -> D6 red
static constexpr uint8_t PIN_USER_BUTTON = 15;  // P0.15 -> SW1 -> GND

// QSPI flash pin mapping, for board-definition / Nordic SDK configuration
static constexpr uint8_t PIN_QSPI_CS  = 17;     // P0.17
static constexpr uint8_t PIN_QSPI_CLK = 19;     // P0.19
static constexpr uint8_t PIN_QSPI_IO0 = 21;     // P0.21
static constexpr uint8_t PIN_QSPI_IO1 = 22;     // P0.22
static constexpr uint8_t PIN_QSPI_IO2 = 23;     // P0.23
static constexpr uint8_t PIN_QSPI_IO3 = 32;     // P1.00 is encoded as 32 in many nRF52 Arduino cores

static constexpr uint8_t BME280_ADDR = 0x76;
static constexpr uint8_t SGP41_ADDR = 0x59;
static constexpr uint8_t SPS30_ADDR = 0x69;

Adafruit_BME280 bme;
SensirionI2CSgp41 sgp41;

uint16_t sgp41ConditioningTicks = 0;
uint32_t lastSensorMs = 0;

float readBatteryVolts() {
  // Divider is 100k / 100k, so VBAT/VSYS at ADC pin is half the rail.
  // analogRead() range depends on Arduino core ADC reference and resolution.
  // Use analogReadMilliVolts() if your selected core supports it.
#if defined(ARDUINO_ARCH_NRF52)
  uint16_t raw = analogRead(PIN_AIN2_BAT);
  float adcVolts = (raw / 1023.0f) * 3.0f;  // default approximation; calibrate in firmware
#else
  uint16_t raw = analogRead(PIN_AIN2_BAT);
  float adcVolts = (raw / 1023.0f) * 3.3f;
#endif
  return adcVolts * 2.0f;
}

bool probeI2C(uint8_t address) {
  Wire.beginTransmission(address);
  return Wire.endTransmission() == 0;
}

void setupGpio() {
  pinMode(PIN_LED_STATUS, OUTPUT);
  pinMode(PIN_LED_ERR, OUTPUT);
  pinMode(PIN_USER_BUTTON, INPUT_PULLUP);
  digitalWrite(PIN_LED_STATUS, LOW);
  digitalWrite(PIN_LED_ERR, LOW);
}

void setupI2C() {
  Wire.setPins(PIN_I2C_SDA, PIN_I2C_SCL);
  Wire.begin();
  Wire.setClock(400000);
}

bool setupBME280() {
  if (!bme.begin(BME280_ADDR, &Wire)) {
    Serial.println("BME280 not found at 0x76");
    return false;
  }

  bme.setSampling(
    Adafruit_BME280::MODE_NORMAL,
    Adafruit_BME280::SAMPLING_X2,
    Adafruit_BME280::SAMPLING_X16,
    Adafruit_BME280::SAMPLING_X1,
    Adafruit_BME280::FILTER_X16,
    Adafruit_BME280::STANDBY_MS_1000
  );
  return true;
}

bool setupSGP41() {
  sgp41.begin(Wire);
  uint16_t serialNumber[3] = {0, 0, 0};
  int16_t error = sgp41.getSerialNumber(serialNumber);
  if (error) {
    Serial.print("SGP41 serial read failed, error: ");
    Serial.println(error);
    return false;
  }
  Serial.print("SGP41 serial: ");
  Serial.print(serialNumber[0], HEX);
  Serial.print(serialNumber[1], HEX);
  Serial.println(serialNumber[2], HEX);
  return true;
}

void readSensors() {
  float tempC = bme.readTemperature();
  float humidity = bme.readHumidity();
  float pressurePa = bme.readPressure();
  float batteryV = readBatteryVolts();
  bool buttonPressed = (digitalRead(PIN_USER_BUTTON) == LOW);

  // SGP41 raw measurement. Use default compensation values initially:
  // relative humidity ticks = 0x8000, temperature ticks = 0x6666.
  uint16_t srawVoc = 0;
  uint16_t srawNox = 0;
  int16_t error = 0;

  if (sgp41ConditioningTicks < 10) {
    error = sgp41.executeConditioning(0x8000, 0x6666, srawVoc);
    sgp41ConditioningTicks++;
  } else {
    error = sgp41.measureRawSignals(0x8000, 0x6666, srawVoc, srawNox);
  }

  if (error) {
    digitalWrite(PIN_LED_ERR, HIGH);
    Serial.print("SGP41 read error: ");
    Serial.println(error);
  } else {
    digitalWrite(PIN_LED_ERR, LOW);
  }

  digitalWrite(PIN_LED_STATUS, !digitalRead(PIN_LED_STATUS));

  Serial.print("T="); Serial.print(tempC, 2); Serial.print(" C, ");
  Serial.print("RH="); Serial.print(humidity, 1); Serial.print(" %, ");
  Serial.print("P="); Serial.print(pressurePa / 100.0f, 1); Serial.print(" hPa, ");
  Serial.print("VBAT/VSYS="); Serial.print(batteryV, 3); Serial.print(" V, ");
  Serial.print("SGP41 VOCraw="); Serial.print(srawVoc); Serial.print(", NOxraw="); Serial.print(srawNox); Serial.print(", ");
  Serial.print("button="); Serial.println(buttonPressed ? "pressed" : "released");

  if (probeI2C(SPS30_ADDR)) {
    Serial.println("SPS30 present at 0x69");
  } else {
    Serial.println("SPS30 not detected at 0x69");
  }
}

void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println("MDBT50Q-P1MV2 wearable bring-up");

  setupGpio();
  setupI2C();

  bool ok = true;
  ok &= setupBME280();
  ok &= setupSGP41();

  digitalWrite(PIN_LED_ERR, ok ? LOW : HIGH);
  digitalWrite(PIN_LED_STATUS, ok ? HIGH : LOW);
}

void loop() {
  const uint32_t now = millis();
  if (now - lastSensorMs >= 1000) {
    lastSensorMs = now;
    readSensors();
  }
}
Bring-up checklist
  1. Power the board from USB and battery/system supply and confirm 3V0, 1V8_SGP41, VBUS_5V, VSYS, and 5V_SPS30 are present.
  2. Program via TC2030 SWD: VTref=3V0, SWDIO, SWDCLK, nRESET, GND.
  3. Verify I2C scan finds BME280 at 0x76 and SGP41 at 0x59.
  4. Connect SPS30 and verify it appears at 0x69.
  5. Calibrate battery voltage measurement because the ADC reference and raw-to-voltage scaling depend on the selected nRF52 Arduino core/board package.
  6. If production firmware needs external QSPI flash, create a custom nRF52840 board definition or Zephyr board overlay that assigns P0.17/P0.19/P0.21/P0.22/P0.23/P1.00 to QSPI.

    Hardware notes from the schematic

    Pin mapping

    I2C addresses

    PlatformIO setup

    Complete Arduino bring-up source

    Bring-up checklist

Documents

    Firmware Starter — MDBT50Q-P1MV2 / nRF52840

    Assembly Mechanical Integration

    Pre-Route Layout Constraints

    Power Pin Membership Audit

    Haelo Clip v2 PCB Configuration

    Haelo Clip v2 Auto-Placement Zones and Constraints

    Haelo Clip v2 Board Features and Manufacturing Rules

    Placement Review Fixes

    Plane Audit and Airwire Report

Assets

gltf

Round LiPo placeholder 40mm diameter x 5mm with flying leads

round-lipo-40x5-placeholder.gltf3D Model
gltf

SPS30 placeholder 40.6x40.6x12.45mm mechanical model

sps30-placeholder.gltf3D Model

nRF52840 Wearable Power Tree

nRF52840 Wearable Power Tree thumbnail
Battery-powered nRF52840 wearable power-tree schematic with USB-C charging, nPM1100 PMIC, 3V0, 1V8_SGP41, and 5V_SPS30 rails.

Properties

Haelo Clip v2 round-puck PCB: 58 mm circular outline centered at origin; Standard 4 Layer with L2 continuous GND plane, L3 power planes for 3V0/1V8_SGP41/VSYS; requested fabrication notes: 1.0 mm finished thickness, ENIG, matte black solder mask preferred with white silkscreen; matte white mask with black silkscreen acceptable if cheaper at JLCPCB.

Pricing & Availability

Distributor

Qty 1

Arrow

$10.02–$11.43

Digi-Key

$16.02–$20.31

HQonline

$8.71–$8.81

LCSC

$19.40–$20.49

Mouser

$17.72

TME

$8.03

Verical

$12.13–$17.73

Controls

New control