From a napkin sketch to a working PCB — the complete journey of turning requirements into silicon, copper, and firmware.
You've been asked to build a wearable health monitor. The requirements are brutally specific: measure heart rate and blood oxygen saturation (SpO2), transmit data via Bluetooth Low Energy, survive 7 days on a single CR2032 coin cell battery. Budget: $8 total bill of materials. Maximum PCB size: 25mm × 25mm.
Where do you even start? You can't just grab random parts off Digikey and hope they work together. Every component choice cascades through the entire system. Pick the wrong MCU and you blow your power budget in 6 hours. Choose the wrong sensor interface and you run out of pins. Select a wireless chip that needs an external antenna and you've exceeded your board size.
The design process follows a strict sequence. Skip a step, and you'll pay for it later — with redesigns, blown budgets, or products that don't work in the field.
Let's walk through our wearable example. The requirements decompose into concrete engineering constraints:
| Requirement | Engineering Constraint |
|---|---|
| Heart rate + SpO2 | Need PPG sensor (e.g., MAX30102), I²C interface |
| BLE transmission | Need 2.4GHz radio + antenna. nRF52 or similar. |
| 7 days on CR2032 | CR2032 = 225mAh. Budget = 225/168h = 1.34mA average |
| $8 BOM | MCU+radio: $3, sensor: $2, passives+PCB: $3 |
| 25mm × 25mm | QFN packages, 0402 passives, chip antenna |
Click each stage to see the key decisions and tradeoffs for our wearable health monitor.
Notice how the power budget calculation immediately told us something crucial: we need aggressive duty cycling. That's the beauty of the requirements-first approach — you discover constraints BEFORE you commit to hardware.
python # Power budget calculation for wearable health monitor battery_mAh = 225 # CR2032 capacity target_days = 7 target_hours = target_days * 24 # 168 hours avg_current_mA = battery_mAh / target_hours print(f"Max average current: {avg_current_mA:.2f} mA") # Output: Max average current: 1.34 mA # Duty cycle budget breakdown: sleep_current_uA = 3 # MCU deep sleep + RTC measure_current_mA = 12 # PPG sensor + ADC active ble_tx_current_mA = 8 # BLE advertisement packet # If we measure 2s every 30s and TX for 5ms: duty_measure = 2 / 30 # 6.7% active duty_tx = 0.005 / 30 # 0.017% TX duty_sleep = 1 - duty_measure - duty_tx avg = (sleep_current_uA/1000 * duty_sleep + measure_current_mA * duty_measure + ble_tx_current_mA * duty_tx) print(f"Actual average: {avg:.3f} mA") # Output: Actual average: 0.803 mA ✓ (under 1.34 mA budget)
Before you can wire components together, you need to understand how digital chips talk to each other. There are four dominant protocols in embedded systems, each with different tradeoffs. Picking the wrong bus for a component means either wasted pins, insufficient speed, or unnecessary complexity.
SPI uses 4 wires: SCLK (clock), MOSI (master out, slave in), MISO (master in, slave out), and CS (chip select, one per device). It's full-duplex — data flows both directions simultaneously. The master generates the clock, so there's no baud rate negotiation. Speeds easily reach 10-50 MHz.
Use SPI for: displays (ILI9341), external Flash (W25Q128), SD cards, high-speed ADCs. Anything that needs to move lots of data fast.
I²C uses just 2 wires: SDA (data) and SCL (clock). Devices are addressed with a 7-bit address, so up to 127 devices can share the same two wires. Standard mode runs at 100 kHz, fast mode at 400 kHz, fast-mode-plus at 1 MHz. It's half-duplex — one direction at a time.
Use I²C for: temperature sensors (TMP102), accelerometers (MPU-6050), EEPROM (AT24C256), pressure sensors (BMP280). Anything that sends small packets infrequently.
UART uses 2 wires: TX (transmit) and RX (receive). There's no clock — both sides must agree on a baud rate beforehand (9600, 115200, etc.). It's point-to-point: one transmitter, one receiver. Full-duplex but asynchronous.
Use UART for: GPS modules (NEO-6M), debug printf output, Bluetooth modules (HC-05), cellular modems. Anything that streams text-like data or needs a simple debug port.
PDM uses just 1 data wire + 1 clock wire. It encodes audio as a stream of 1s and 0s at very high frequency (1-3 MHz). More 1s = louder signal. A decimation filter in the MCU converts this to PCM audio. Used exclusively for digital MEMS microphones.
Use PDM for: MEMS microphones (SPH0645, MP34DT05). That's basically it — it's a single-purpose protocol optimized for one-bit sigma-delta audio streams.
| Protocol | Wires | Speed | Topology | Best For |
|---|---|---|---|---|
| SPI | 4 + 1/slave | 10-50 MHz | 1 master, N slaves | Displays, Flash, ADCs |
| I²C | 2 (shared) | 100k-1M Hz | Multi-master, 127 addr | Sensors, EEPROM |
| UART | 2 (P2P) | 9600-921600 bps | Point-to-point | GPS, debug, modems |
| PDM | 2 (CLK+DAT) | 1-3 MHz | Point-to-point | MEMS microphones |
Select a protocol to watch a byte (0x42 = 0b01000010) transmitted. Observe clock vs data timing.
Look at the SPI waveform: MOSI changes on the falling edge of SCLK, and the slave samples on the rising edge. This is "CPOL=0, CPHA=0" — the most common SPI mode. Notice the CS line goes LOW to select the device.
Now look at I²C: it starts with a start condition (SDA drops while SCL is high), then sends 7 address bits + R/W bit, waits for an ACK from the slave, THEN sends the data byte. More overhead, but only 2 wires for everything.
c // I2C read example: read temperature from TMP102 at address 0x48 #include "stm32l4xx_hal.h" uint16_t read_temp(I2C_HandleTypeDef *hi2c) { uint8_t buf[2]; uint8_t addr = 0x48 << 1; // 7-bit addr shifted left // Read 2 bytes from register 0x00 (temperature) HAL_I2C_Mem_Read(hi2c, addr, 0x00, I2C_MEMADD_SIZE_8BIT, buf, 2, 100); // TMP102 returns 12-bit temp, MSB first int16_t raw = (buf[0] << 4) | (buf[1] >> 4); // Each LSB = 0.0625°C return raw; // Multiply by 0.0625 for °C }
The microcontroller is the brain of your system. It runs your firmware, talks to sensors, drives displays, and manages power states. Choosing the right one is the single most consequential decision in your hardware design — get it wrong, and you either overpay, overheat, or run out of resources mid-development.
Nearly all modern embedded MCUs use ARM Cortex-M cores. The family spans from ultra-low-power to high-performance:
| Core | Class | Clock | Features | Cost | Example |
|---|---|---|---|---|---|
| Cortex-M0/M0+ | Ultra-low-power | 24-64 MHz | Minimal gate count, lowest sleep current | $0.30-1.00 | STM32L0, nRF51 |
| Cortex-M4 | Mainstream DSP | 64-180 MHz | FPU, DSP instructions, SIMD | $2-5 | STM32L4, nRF52840 |
| Cortex-M7 | High-performance | 200-600 MHz | Double-precision FPU, cache, TCM | $5-15 | STM32H7, i.MX RT1060 |
| Cortex-M33 | Security-focused | 64-160 MHz | TrustZone, M4-class + security | $3-8 | STM32U5, nRF5340 |
Don't pick an MCU by looking at specs in isolation. Build a decision matrix that weights your actual requirements:
| Parameter | STM32L4R5 | ESP32-S3 | nRF52840 | RP2040 |
|---|---|---|---|---|
| Core | M4 @ 120MHz | Xtensa LX7 dual @ 240MHz | M4 @ 64MHz | M0+ dual @ 133MHz |
| Flash | 2MB internal | 8MB external | 1MB internal | External QSPI |
| SRAM | 640KB | 512KB | 256KB | 264KB |
| Wireless | None | WiFi + BLE 5 | BLE 5, Thread, Zigbee | None |
| Sleep current | 1.0 μA (shutdown) | ~5 μA (deep sleep) | 1.5 μA (system OFF) | 0.8 μA (dormant) |
| Active (mA/MHz) | ~0.1 | ~0.3 | ~0.1 | ~0.1 |
| FPU | Yes (single) | Yes (single) | Yes (single) | No |
| Price (1k qty) | ~$5 | ~$2.50 | ~$3.50 | ~$0.80 |
| Best for | Ultra-low-power sensing | WiFi IoT, AI/ML edge | BLE wearables | Education, cost-sens. |
Click MCU names to toggle them on the radar. Compare performance, power efficiency, cost, peripherals, and memory.
c // nRF52840: Typical low-power BLE application structure #include "nrf_sdh.h" #include "nrf_sdh_ble.h" #include "nrf_pwr_mgmt.h" int main(void) { // 1. Init clocks — use 64MHz HFCLK only when active nrf_drv_clock_init(); // 2. Init BLE SoftDevice (handles radio in background) nrf_sdh_enable_request(); ble_stack_init(); // 3. Start advertising — 1 packet every 1000ms advertising_init(ADV_INTERVAL_1000MS); advertising_start(); // 4. Main loop: MCU sleeps between BLE events for (;;) { nrf_pwr_mgmt_run(); // Enters System ON sleep // Wake on BLE event, timer, or GPIO interrupt } }
Memory in embedded systems is nothing like a PC where you have gigabytes of unified RAM. In an MCU, you're working with kilobytes, every byte has a specific purpose, and different memory types have wildly different access speeds and power costs. Understanding the memory hierarchy is essential for writing firmware that actually fits and runs fast.
Internal Flash stores your compiled firmware code. It's non-volatile (survives power loss), but slow to write (must erase entire sectors first). Typical sizes: 128KB to 2MB. Your code executes directly from Flash (XIP — execute in place) or gets copied to SRAM for faster execution.
Internal SRAM is your working memory — variables, stack, heap, DMA buffers. It's fast (single-cycle access), volatile (lost on power loss), and precious. Typical sizes: 16KB to 1MB. Every global variable, every buffer, every stack frame lives here.
External QSPI Flash is for bulk storage — fonts, images, audio clips, data logs. Connected via SPI or QSPI (quad-SPI for 4x bandwidth). Typical sizes: 1MB to 128MB. Access is slower than internal Flash, but you can get enormous capacities cheaply.
External SDRAM/PSRAM provides large working memory for frame buffers, ML model weights, or audio processing. Connected via a dedicated memory controller. Typical sizes: 2MB to 64MB. Only found on high-end MCUs (STM32H7, i.MX RT).
| Type | Speed | Size | Cost | Volatile? | Use For |
|---|---|---|---|---|---|
| Internal Flash | ~50ns (0 wait @ low clock) | 128KB-2MB | Included | No | Firmware code |
| Internal SRAM | ~6ns (single cycle) | 16KB-1MB | Included | Yes | Variables, stack, DMA |
| External QSPI Flash | ~100ns (memory-mapped) | 1-128MB | $0.20-2.00 | No | Assets, logs, OTA images |
| External SDRAM | ~10ns (burst) | 2-64MB | $1-5 | Yes | Frame buffers, ML weights |
Our wearable has an nRF52840 with 1MB Flash and 256KB SRAM. Let's check if our application fits:
text Memory Budget — nRF52840 Wearable Health Monitor ═══ FLASH (1024 KB total) ═══ SoftDevice (BLE stack): 152 KB # Nordic's S140 BLE stack Bootloader (DFU capable): 24 KB # For over-the-air updates Application code: ~80 KB # Sensor drivers + BLE services PPG signal processing (DSP): ~30 KB # FFT, peak detection, SpO2 calc Configuration storage: 4 KB # User settings, calibration ──────────────────────────────────────── Total used: 290 KB Remaining: 734 KB ✓ Plenty of headroom ═══ SRAM (256 KB total) ═══ SoftDevice (BLE stack): ~8 KB # Connection buffers Stack (main + ISR): 8 KB # Conservative Heap: 4 KB # Minimal dynamic allocation PPG sample buffer (4s @ 100Hz): 1.6 KB # 400 samples × 4 bytes FFT working buffer: 4 KB # 512-point complex FFT BLE TX buffer: ~2 KB # GATT attribute table ──────────────────────────────────────── Total used: ~28 KB Remaining: 228 KB ✓ Very comfortable
Explore the memory layout. Hover over regions to see what lives there and why.
When an external Flash chip is memory-mapped (via QSPI XIP mode), the MCU can read it like internal memory — just dereference a pointer. The QSPI controller transparently fetches data. This is how fonts and images are typically stored: the code just reads from an address in the 0x90000000 range.
Without memory mapping, you must explicitly issue SPI read commands, copy data into SRAM, then use it. This is fine for occasional access but terrible for random reads (each access requires a full SPI transaction).
Sensors are where the physical world meets the digital one. A temperature sensor converts thermal energy into a voltage. An accelerometer converts motion into a capacitance change. An optical sensor converts photons into current. But raw sensor outputs are messy — they're noisy, small, and often at the wrong voltage level. Signal conditioning is the art of cleaning up that mess before your ADC digitizes it.
| Parameter | What It Means | Example (BMP280 pressure sensor) |
|---|---|---|
| Range | Min/max measurable value | 300-1100 hPa |
| Resolution | Smallest detectable change | 0.16 Pa (20-bit mode) |
| Accuracy | How close to true value | ±1 hPa absolute |
| Interface | How it talks to MCU | I²C or SPI |
| Supply voltage | What power rail it needs | 1.7V-3.6V |
| Current draw | How much power in active/sleep | 2.7μA @ 1Hz, 0.1μA sleep |
| Output type | Digital (processed) or analog (raw) | Digital (compensated) |
When you DO use an analog sensor, the signal path looks like this:
You have an analog temperature sensor that outputs 10mV per °C. Your target range is 0-100°C. Your MCU's ADC reference voltage is 3.3V at 12-bit resolution.
python # Signal conditioning calculation sensor_sensitivity = 0.010 # 10mV/°C = 0.01 V/°C temp_range = (0, 100) # °C # Raw sensor output range v_min = temp_range[0] * sensor_sensitivity # 0V at 0°C v_max = temp_range[1] * sensor_sensitivity # 1.0V at 100°C print(f"Sensor output: {v_min}V to {v_max}V") # ADC parameters adc_ref = 3.3 # Volts adc_bits = 12 adc_counts = 2**adc_bits # 4096 levels adc_lsb = adc_ref / adc_counts print(f"ADC LSB: {adc_lsb*1000:.2f} mV") # 0.81 mV # Without amplification: sensor uses only 1V of 3.3V range effective_bits_no_amp = 12 - 1.74 # log2(3.3/1.0) = 1.74 bits lost temp_resolution_no_amp = 100 / (1.0 / adc_lsb) print(f"Resolution without amp: {temp_resolution_no_amp:.3f} °C") # 0.081°C # With amplification: gain = 3.3/1.0 = 3.3x (fill the ADC range) gain = adc_ref / v_max # 3.3x temp_resolution_with_amp = 100 / adc_counts print(f"Resolution with 3.3x amp: {temp_resolution_with_amp:.3f} °C") # 0.024°C # RC low-pass filter: cut off above 10Hz (sensor can't change faster) import math f_cutoff = 10 # Hz R = 10e3 # 10kΩ (typical) C = 1 / (2 * math.pi * f_cutoff * R) print(f"Filter cap: {C*1e6:.2f} µF") # 1.59 µF → use 1.5µF standard
Adjust gain and filter cutoff to clean up the noisy sensor signal. Watch the signal transform from raw to ADC-ready.
Adding wireless to an embedded system is where battery life goes to die — or thrive, depending on your choice. The four dominant wireless technologies each occupy a different niche in the range-power-bandwidth design space. Choose wrong, and you'll either drain your battery in hours or fail to reach your gateway.
WiFi is the obvious choice because it's everywhere — but it's a terrible choice for battery-powered devices. A WiFi TX burst draws 100-300mA. The association handshake alone takes seconds. And the MCU needs a full TCP/IP stack (LWIP), eating 30-80KB of SRAM.
Use WiFi when: you have wall power, need internet access (HTTP, MQTT), high throughput (>1 Mbps), or are building a gateway/hub. Examples: smart plugs, security cameras, home automation hubs.
BLE was designed from the ground up for coin-cell devices. A single connection event (TX + RX) takes ~3ms and draws ~8mA peak. Between events, the radio sleeps. With a 1-second connection interval, average current is ~15μA. A CR2032 lasts years.
BLE uses GATT profiles — structured data services. Your heart rate monitor exposes a "Heart Rate Service" with a "Heart Rate Measurement" characteristic. Any BLE-capable phone can read it. Maximum throughput: ~1 Mbps (BLE 5), practical: ~100-200 kbps.
Sub-GHz radios (433 MHz, 868 MHz, 915 MHz) trade bandwidth for range. LoRa achieves 10-15km line-of-sight at ~300 bps. TX current is ~30mA but transmissions are short (50-100ms). Sleep current: <1μA.
Use Sub-GHz when: your sensor is in a field, a building basement, or anywhere far from infrastructure. Examples: agricultural sensors, utility meters, asset trackers, weather stations.
USB isn't wireless, but it's the most common wired connection. USB 2.0 Full Speed (12 Mbps) is standard on MCUs. USB provides both data AND power (500mA from USB 2.0 host). For development, USB-CDC gives you a virtual serial port — printf debugging without a separate UART adapter.
| Technology | Range | TX Current | Throughput | Sleep | Battery Life |
|---|---|---|---|---|---|
| WiFi | ~50m indoor | 100-300mA | 1-100 Mbps | ~5μA | Hours-days |
| BLE | ~30m indoor | 5-15mA | 100-200 kbps | ~1.5μA | Months-years |
| LoRa | 2-15 km | 30-120mA | 0.3-50 kbps | <1μA | Years |
| USB 2.0 | 5m cable | N/A (bus-powered) | 12 Mbps (FS) | ~0.5mA (suspend) | N/A (wired) |
python # Compare battery life: WiFi vs BLE for sending 20 bytes every 10 seconds battery_mAh = 225 # CR2032 # WiFi: wake → associate (2s @ 100mA) → send (10ms @ 150mA) → sleep wifi_assoc_ms = 2000 wifi_tx_ms = 10 wifi_sleep_ms = 10000 - wifi_assoc_ms - wifi_tx_ms wifi_avg_mA = (100*wifi_assoc_ms + 150*wifi_tx_ms + 0.005*wifi_sleep_ms) / 10000 wifi_hours = battery_mAh / wifi_avg_mA print(f"WiFi: avg {wifi_avg_mA:.1f} mA → {wifi_hours:.1f} hours") # WiFi: avg 20.2 mA → 11.2 hours ← DEAD IN HALF A DAY # BLE: connection event (3ms @ 8mA) → sleep ble_tx_ms = 3 ble_sleep_ms = 10000 - ble_tx_ms ble_avg_mA = (8*ble_tx_ms + 0.0015*ble_sleep_ms) / 10000 ble_hours = battery_mAh / ble_avg_mA print(f"BLE: avg {ble_avg_mA:.4f} mA → {ble_hours:.0f} hours ({ble_hours/24:.0f} days)") # BLE: avg 0.0039 mA → 57692 hours (2404 days) ← 6.5 YEARS
Click each technology to highlight it. Compare range vs power consumption vs bandwidth.
The PCB (Printed Circuit Board) is where your schematic becomes a physical object. Every trace is a wire, every via is a tunnel between layers, and every millimeter of copper matters. A bad PCB layout can introduce noise that corrupts your sensor data, create antenna dead zones, or even cause your board to catch fire if trace widths are too narrow for the current.
Most embedded boards use either 2 or 4 layers:
| Layers | Cost | Use When | Example |
|---|---|---|---|
| 2-layer | $5-15 (qty 5) | Simple MCU + sensors, low speed, tight budget | Arduino shields, breakout boards |
| 4-layer | $15-40 (qty 5) | BLE/WiFi (need ground plane), >50MHz signals, USB | Our wearable, any wireless device |
| 6+ layer | $50+ (qty 5) | DDR memory, HDMI, GbE, dense BGA routing | Single-board computers (RPi-class) |
A 4-layer board typically stacks as: Signal → Ground → Power → Signal. The internal ground plane provides a low-impedance return path for high-speed signals (critical for BLE antenna performance) and shields top signals from bottom signals.
How wide must a trace be to carry a given current without overheating? The IPC-2221 standard gives us:
Where: I = current (Amps), k = 0.048 for outer layers (0.024 for inner), ΔT = temperature rise (°C), w = trace width (mils), t = copper thickness (oz, typically 1oz = 1.4 mils).
Rearranging to solve for width:
python # IPC-2221 trace width calculator def trace_width_mils(current_A, temp_rise_C=10, copper_oz=1, internal=False): """Calculate minimum trace width in mils (thousandths of an inch).""" k = 0.024 if internal else 0.048 t = copper_oz * 1.378 # mils thickness # Area in mil² = (I / (k * dT^0.44))^(1/0.725) area = (current_A / (k * temp_rise_C**0.44))**(1/0.725) width = area / t return width # Examples: print(f"100mA signal trace: {trace_width_mils(0.1):.1f} mils") # 2.2 mils print(f"500mA power trace: {trace_width_mils(0.5):.1f} mils") # 10.3 mils print(f"1A power trace: {trace_width_mils(1.0):.1f} mils") # 19.0 mils print(f"3A motor trace: {trace_width_mils(3.0):.1f} mils") # 50+ mils
Placement follows a strict priority order. Getting this wrong means noisy measurements, poor wireless performance, or thermal issues:
| Rule | Typical Value (4-layer) | Why |
|---|---|---|
| Trace/space | 6/6 mil (0.15mm) | Manufacturing minimum for standard PCB fabs |
| Via drill | 0.3mm hole, 0.6mm pad | Annular ring = (pad - drill)/2 = 0.15mm minimum |
| Board edge clearance | 0.3mm | Router bit tolerance during panel depanelization |
| Solder mask expansion | 0.05mm | Prevents solder bridging between adjacent pads |
| Antenna keep-out | 3mm all sides | Ground plane and components detune the antenna |
Click and drag components to place them. Red highlights show DRC violations. Green means correctly placed. Try to place all components following the rules above.
Not everything needs to be designed from scratch. COTS (Commercial Off-The-Shelf) modules let you buy pre-certified, pre-tested subsystems and drop them onto your board. The key question is always: should I build this circuit myself, or buy a module?
| Subsystem | Buy (Module) | Build (Discrete) | Verdict |
|---|---|---|---|
| WiFi/BLE radio | ESP32 module ($2-4) | Custom antenna + balun + matching | BUY — certification alone costs $10k+ |
| GPS/GNSS | u-blox NEO-6M ($10) | Custom RF front-end + baseband IC | BUY — GPS sensitivity requires expert RF |
| Power management | Generic PMIC module | Custom LDO/buck for your battery | BUILD — tailored efficiency saves battery life |
| IMU/Sensors | Breakout board ($5-15) | Sensor IC + decoupling ($1-3) | BUILD for production, BUY for prototyping |
| LoRa radio | RFM95W module ($6) | SX1276 + antenna matching | BUY — Sub-GHz matching is painful |
| USB-C connector | N/A (always discrete) | Connector + ESD diodes + CC resistors | BUILD — trivial circuit, modules don't exist |
ESP32-WROOM-32 ($2.80 @ 1k qty): WiFi + BLE, 4MB Flash, 520KB SRAM, FCC/CE certified. 18mm × 25mm. Includes PCB antenna, crystal, and Flash — just add power and decoupling. The workhorse of IoT.
nRF52840 module (Raytac MDBT50Q) ($4.50 @ 1k): BLE 5 + Thread/Zigbee, 1MB Flash, 256KB SRAM, chip antenna, FCC/CE/IC certified. 10mm × 15mm. Our wearable could use this instead of bare nRF52840 IC to skip antenna design entirely.
u-blox NEO-M9N ($15): Multi-band GNSS (GPS + Galileo + GLONASS). 12mm × 16mm. UART/I²C/SPI interface. 1-meter accuracy. Just connect VCC, GND, TX, RX, and an external antenna.
text Module Integration Checklist: □ Voltage compatibility Module VCC range vs your power rail? I/O voltage levels match MCU (3.3V vs 1.8V)? □ Footprint Do you have the KiCad/Altium symbol + footprint? Castellated pads or pin headers? Reflow or hand-solder? □ Antenna keep-out Does the module have a PCB antenna? (Most WiFi/BLE do) Keep-out zone marked in datasheet — NO copper, no components □ Power sequencing Does it need to power up before/after other chips? Does it have an enable pin for power gating? □ Thermal WiFi modules can draw 300mA+ peak. Copper pour for heat sinking? Adequate power trace width to module VCC pin? □ Firmware support Does the module need its own firmware (ESP32: yes, GPS: built-in)? AT commands? Binary protocol? SDK available?
Every wireless module datasheet specifies a keep-out zone around its antenna. This is NOT optional. Violating it causes:
Typical keep-out: 3-5mm on all sides of the antenna area, extending to the board edge. No copper fill, no traces, no components, no solder mask (sometimes).
Visual comparison of popular COTS modules by size, cost, and capability.
Your PCB arrives from the fab house. Beautiful purple solder mask, perfectly aligned silkscreen. You plug in USB power. Nothing happens. No LED, no UART output, no BLE advertisement. Now what? Welcome to board bring-up — the most frustrating and most rewarding phase of hardware development.
Never try to run your full application on a fresh board. Bring up subsystems one at a time, in this exact order:
| Tool | Cost | What It Shows | When To Use |
|---|---|---|---|
| Multimeter | $20 | Voltage, resistance, continuity | First: verify power rails exist |
| SWD Debugger | $20-60 | CPU registers, memory, breakpoints | Firmware crashes, hard faults |
| Logic Analyzer | $10-150 | Digital signals, protocol decode (I²C, SPI, UART) | Bus communication problems |
| Oscilloscope | $300+ | Analog waveforms, rise times, noise | Signal integrity, power rail noise |
| Current meter | $50-200 | μA-level current measurement | Battery life validation |
text ╔═══════════════════════════════════════════════════════════════╗ ║ SYMPTOM │ LIKELY CAUSE │ FIX ║ ╠═══════════════════════════════════════════════════════════════╣ ║ Nothing happens │ No power / wrong │ Check VCC ║ ║ │ polarity │ with DMM ║ ║───────────────────────┼─────────────────────────┼────────────║ ║ Debugger won't │ SWD pins swapped / │ Check ║ ║ connect │ no power / reset held │ wiring ║ ║───────────────────────┼─────────────────────────┼────────────║ ║ I²C returns NACK │ Wrong address / no │ I²C scan ║ ║ │ pull-ups / wrong VCC │ + scope ║ ║───────────────────────┼─────────────────────────┼────────────║ ║ UART garbage chars │ Wrong baud rate / │ Verify ║ ║ │ TX/RX swapped │ with LA ║ ║───────────────────────┼─────────────────────────┼────────────║ ║ BLE not advertising │ Crystal not starting / │ Scope on ║ ║ │ wrong load caps │ XTAL pins ║ ║───────────────────────┼─────────────────────────┼────────────║ ║ ADC reads 0xFFF │ Floating input pin / │ Add pull ║ ║ constantly │ broken trace │ -down res ║ ╚═══════════════════════════════════════════════════════════════╝
Unit testing firmware means mocking hardware. You can't run a sensor driver test on your PC without faking the I²C bus. Frameworks like CppUTest or Unity let you write tests that verify logic without real hardware.
c // Unit test: verify SpO2 calculation from PPG data #include "unity.h" #include "spo2_algo.h" void test_spo2_normal_ratio(void) { // R = (AC_red/DC_red) / (AC_ir/DC_ir) // R = 0.4 → SpO2 ≈ 100% (healthy) float spo2 = calculate_spo2(0.4); TEST_ASSERT_FLOAT_WITHIN(2.0, 98.0, spo2); } void test_spo2_low_ratio(void) { // R = 1.0 → SpO2 ≈ 85% (hypoxic) float spo2 = calculate_spo2(1.0); TEST_ASSERT_FLOAT_WITHIN(3.0, 85.0, spo2); } void test_spo2_invalid_ratio(void) { // R > 2.0 → invalid reading (sensor not on finger) float spo2 = calculate_spo2(2.5); TEST_ASSERT_FLOAT_IS_NEG_INF(spo2); // Sentinel for error }
Integration testing runs on real hardware. You connect the actual board, exercise each peripheral, and verify outputs against known-good references. Automated test jigs (pogo pins + Raspberry Pi) enable this at scale.
Environmental testing verifies the product works in the real world: temperature cycling (-20 to +60°C), humidity (85% RH), ESD strikes (8kV contact, 15kV air), and vibration. This is where cheap solder joints crack, marginal crystals stop oscillating, and poorly routed traces pick up interference.
Click symptoms to trace through the diagnostic flow. Each path leads to the most likely root cause.
You now have the complete mental model for embedded hardware design: from requirements decomposition, through component selection, to PCB layout and firmware validation. Let's consolidate with tools and challenges.
Adjust component choices to see total BOM cost and power budget.
Here's one valid architecture:
| Subsystem | Component | Interface | Cost |
|---|---|---|---|
| MCU | STM32L031 (Cortex-M0+, 32KB Flash) | — | $0.90 |
| LoRa Radio | RFM95W module (SX1276) | SPI | $5.50 |
| Temp/Humidity | SHT30-DIS | I²C | $2.20 |
| Soil Moisture | Capacitive probe (custom PCB) | ADC (555 timer freq) | $0.50 |
| Solar Charger | BQ25504 (energy harvester) | Analog | $2.50 |
| Battery | LiPo 500mAh (external) | Power | $2.00 |
| Passives + PCB | Caps, resistors, antenna, PCB fab | — | $1.40 |
| Total BOM | $15.00 | ||
python # Power budget for soil sensor node battery_mAh = 500 solar_charge_mA = 50 # Average (accounting for clouds, night) sun_hours_per_day = 5 # Conservative daily_solar_mAh = solar_charge_mA * sun_hours_per_day # 250 mAh/day # Power consumption per cycle (every 15 min = 96 cycles/day) cycles_per_day = 96 # Per cycle: wake (5ms) + read sensors (50ms) + LoRa TX (100ms) + sleep wake_mA = 5 # MCU active sensor_mA = 2 # SHT30 measurement lora_tx_mA = 120 # RFM95W at +20dBm sleep_uA = 2 # STM32L0 stop mode + RTC active_ms_per_cycle = 5 + 50 + 100 # 155ms sleep_ms_per_cycle = 15 * 60 * 1000 - active_ms_per_cycle # ~900s avg_active_mA = (wake_mA*5 + sensor_mA*50 + lora_tx_mA*100) / active_ms_per_cycle daily_active_mAh = avg_active_mA * (active_ms_per_cycle * cycles_per_day / 3600000) daily_sleep_mAh = sleep_uA/1000 * (sleep_ms_per_cycle * cycles_per_day / 3600000) daily_total_mAh = daily_active_mAh + daily_sleep_mAh print(f"Daily consumption: {daily_total_mAh:.2f} mAh") print(f"Daily solar input: {daily_solar_mAh:.0f} mAh") print(f"Net daily budget: +{daily_solar_mAh - daily_total_mAh:.1f} mAh") # Result: solar easily exceeds consumption → runs indefinitely!
| This Lesson | Next Steps |
|---|---|
| Communication protocols (SPI, I²C, UART, PDM) | DMA transfers, interrupt-driven vs polled I/O |
| MCU selection basics | RTOS (FreeRTOS, Zephyr), real-time constraints |
| Memory types and sizing | Memory protection units (MPU), cache optimization |
| Sensor signal conditioning | Kalman filtering, sensor fusion, calibration |
| Wireless overview | BLE GATT services, LoRaWAN network architecture |
| PCB design rules | EMC compliance, impedance control, flex PCBs |
| Firmware bring-up | CI/CD for embedded, HIL testing, OTA updates |