Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 81 additions & 101 deletions emikit/VALIDATION.md
Original file line number Diff line number Diff line change
@@ -1,118 +1,98 @@
# emikit validation

Three checks ship with the code.
## 1. loop formula

## 1. Loop formula calibration

`emikit/tests/calibration_test.cpp` -- pins `loop_e_field` against the
closed form in Henry Ott, *Electromagnetic Compatibility Engineering*
(Wiley 2009) Eq 11-2 (same constants in Paul Eq 8.62):
`emikit/tests/calibration_test.cpp` pins `loop_e_field` to Ott Eq 11-2
(also Paul Eq 8.62):

E (V/m) = (eta0 * pi * I * A * f^2) / (c^2 * r)

Five test cases. Reference point: 1 mA, 1 cm^2 loop, 100 MHz, 3 m ->
12.85 dBuV/m. Tolerance 0.05 dB. Tag `[calibration]`.
Reference point: 1 mA, 1 cm^2 loop, 100 MHz, 3 m -> 12.85 dBuV/m. Plus
three scaling-derived points. Tolerance 0.05 dB. Tag `[calibration]`.

## 2. Cable common-mode formula calibration
## 2. cable common-mode + estimator

`emikit/tests/cable_cm_test.cpp` -- pins `cable_cm_e_field` against the
Hockanson 1996 short-electric-dipole form (also Paul Eq 11.5):
`emikit/tests/cable_cm_test.cpp` pins `cable_cm_e_field` to Hockanson
1996 / Paul Eq 11.5:

E (V/m) = (eta0 / c) * I_cm * L * f / r

Reference point: 20 uA, 30 cm, 100 MHz, 10 m -> 37.55 dBuV/m. Plus the
TI ADS8686S working point used in section 3 below. Tag `[cable]`.

## 3. Real-board comparison: TI ADS8686S EVM

`emikit/tools/validate_ti.cpp` reconstructs the TI ADS8686SEVM-PDK setup
and compares emikit's prediction to chamber data published in
**TI SBAA548A**, "EMC Compliance Testing for Precision ADC Systems"
(April 2022, rev May 2022).

### TI's measurements

CISPR 11 Class A radiated emissions at 10 m, three SCLK rates:

| Test | SCLK | Peak freq | Margin | Measured E |
|------|---------|--------------|--------|--------------|
| 1 | 10 MHz | 600.05 MHz | 22.83 | 34.67 dBuV/m |
| 2 | 50 MHz | 479.96 MHz | 2.77 | 54.73 dBuV/m |
| 3 | 10 MHz | 479.83 MHz | 6.44 | 51.06 dBuV/m |

### Geometry (from SBAU319 EVM User's Guide, Section 7.2)

- 4-layer FR-4, solid GND on Layer 2
- SCLK trace ~30 mm from PHI connector to ADC pin
- Top-to-GND prepreg ~0.2 mm
- Trace width ~0.15 mm
- Drive: PHI MSP430-class 3.3 V CMOS, I_peak ~6 mA, t_r ~2 ns
- USB cable from PHI to host PC ~30 cm

### Result

Single 10 uA cable common-mode current applied across all three tests
(not per-test fitting -- this value comes from a few mV of estimated
ground bounce divided by ~200 ohm typical cable CM impedance):

== Test 1 (SCLK=10.0 MHz) ==
measured chamber: 34.67 dBuV/m at 600.0 MHz
emikit loop only: -320.28 dBuV/m (gap -354.95 dB)
cable CM (10.0 uA): 47.09 dBuV/m
combined (power-sum): 47.09 dBuV/m (gap +12.42 dB)

== Test 2 (SCLK=50.0 MHz) ==
measured chamber: 54.73 dBuV/m at 480.0 MHz
emikit loop only: -635.71 dBuV/m (gap -690.44 dB)
cable CM (10.0 uA): 45.15 dBuV/m
combined (power-sum): 45.15 dBuV/m (gap -9.58 dB)

== Test 3 (SCLK=10.0 MHz) ==
measured chamber: 51.06 dBuV/m at 479.8 MHz
emikit loop only: -335.15 dBuV/m (gap -386.21 dB)
cable CM (10.0 uA): 45.15 dBuV/m
combined (power-sum): 45.15 dBuV/m (gap -5.91 dB)

Loop-only is 350+ dB low (the trapezoidal sinc lands in a null at
TI's measured peak frequency). With cable CM added the combined
prediction lands within **+12 / -10 dB** of the chamber peak across
all three operating points, using a single 10 uA assumption.

### What this validates

* The differential-mode loop physics (LoopEmissions) matches the
textbook formula (section 1).
* The cable CM physics (CableCommonMode) matches the textbook formula
(section 2).
* When combined, the model lands within pre-compliance accuracy of
published chamber data for a representative digital board, **with
one un-tuned CM-current parameter**. Per-test tuning of I_cm by
activity factor would close the gap further but would be fitting.

### What this does NOT prove

* The 10 uA CM current was reverse-engineered from "what would close
the gap". emikit does not yet estimate CM current from first
principles -- the user supplies it. A future revision will derive it
from the per-net signal current and ground-plane impedance.
* The 30 mm SCLK trace length is reconstructed from a PCB layout
figure. Real number is probably 25-40 mm. The loop-only contribution
is so far below the cable contribution that this uncertainty does
not affect the combined result.
* Only one board, one cable, one frequency band tested. More reference
pairs would strengthen the claim.

### Reproducing
ground-bounce estimator:

I_cm(f) = (2 * L_gnd / (L_cable_per_m * cable_length)) * I_signal(f)

Hand-checked at 5 nH / 1 uH/m / 30 cm: ratio 3.33e-2. Tag `[cable]`.

## 3. TI ADS8686S EVM

`emikit/tools/validate_ti.cpp` compares against chamber data in TI
SBAA548A (April 2022) for the ADS8686SEVM-PDK at three SCLK rates.

Reconstructed setup (geometry from SBAU319):

| param | value | source |
|---------------|------------|-------------------------------------|
| trace length | 30 mm | EVM layer 1 figure, midpoint est |
| trace width | 0.15 mm | typical digital trace |
| loop height | 0.2 mm | typical TI 4-layer prepreg |
| drive current | 6 mA peak | PHI MSP430 3.3 V CMOS |
| rise time | 2 ns | nominal for that part class |
| cable length | 30 cm | PHI USB to host |
| L_gnd | 15 nH | mid-range "real digital board" |
| L_cable_per_m | 1.0 uH/m | typical unshielded USB |

Same parameters for all three tests.

### result

test 1 (SCLK=10 MHz, 9.7 kSPS)
chamber: 34.67 dBuV/m at 600.0 MHz
emikit: 31.64 dBuV/m (gap -3.0 dB)
estimated I_cm: 1.7 uA

test 2 (SCLK=50 MHz, 769 kSPS)
chamber: 54.73 dBuV/m at 480.0 MHz
emikit: 47.61 dBuV/m (gap -7.1 dB)
estimated I_cm: 13.4 uA

test 3 (SCLK=10 MHz, 232 kSPS)
chamber: 51.06 dBuV/m at 479.8 MHz
emikit: 33.63 dBuV/m (gap -17.4 dB)
estimated I_cm: 2.7 uA

Tests 1 and 2 land within +-7 dB of chamber across one set of
engineering parameters. Test 3 uses the same SCLK as test 1 but the
chamber measured 17 dB more because the higher sample rate drives more
activity on the data lines and SCLK bursts. The single-trace
continuous-clock input gives the same prediction for both runs.

## todo

- activity factor / burst modelling (would close the test 3 gap)
- multi-net summing
- empirical L_gnd from pdnkit plane impedance once that ships
- more reference pairs (Hockanson 1996 test board)
- finish openEMS cross-check (PR #62)
- cap on `cable_cm_e_field` for L > lambda/2 is conservative, not a
full-wave answer

## reproduce

cmake --build build --target emikit_validate_ti
./build/emikit/emikit_validate_ti

References:
Or from the CLI:

emikit check board.kicad_pcb --mask "CISPR 32 Class A (10 m)" \
--clock-hz 50e6 --rise-ns 2 --i-peak-ma 6 \
--cable-length-cm 30 --ground-inductance-nh 15

## refs

- TI SBAA548A: https://www.ti.com/lit/an/sbaa548a/sbaa548a.pdf
- TI SBAU319 (ADS8686SEVM-PDK User's Guide): https://www.ti.com/lit/pdf/SBAU319
- Hockanson, Drewniak, Hubing, Van Doren, "Investigation of fundamental
EMI source mechanisms driving common-mode radiation from printed
circuit boards with attached cables", IEEE Trans EMC 38(4), 1996.
- Paul, "Introduction to Electromagnetic Compatibility" 2nd ed.,
Wiley 2006, Ch 11.3 (Eq 11.5).
- Ott, "Electromagnetic Compatibility Engineering", Wiley 2009, Ch 11.6.
- TI SBAU319: https://www.ti.com/lit/pdf/SBAU319
- Hockanson, Drewniak, Hubing, Van Doren, IEEE Trans EMC 38(4), 1996
- Paul, Intro to EMC 2nd ed., Wiley 2006, Ch 11.3
- Ott, EMC Engineering, Wiley 2009, Ch 11.6
- Montrose, EMC and the Printed Circuit Board, Wiley 1999, Ch 5
51 changes: 49 additions & 2 deletions emikit/emi/BoardAnalysis.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ AnalysisResult analyze_board(
}

// Per-frequency drive current envelope -- shared across all nets in v1.
const auto drive_a = spectrum_sweep(config.drive, freqs);
// Use the worst-case envelope (Montrose form) rather than the
// exact harmonic magnitudes. Real EMI receivers see the envelope
// -- their IF bandwidth catches multiple harmonics and real
// signals have edge jitter that fills in the sinc nulls.
const auto drive_a = envelope_sweep(config.drive, freqs);

// Initialize worst-case envelope to -inf.
R.worst_case_dbuv.assign(freqs.size(), -1000.0);
Expand Down Expand Up @@ -95,7 +99,50 @@ AnalysisResult analyze_board(
R.nets.push_back(std::move(ne));
}

if (R.nets.empty()) {
// Cables. Each cable carries a common-mode current derived from
// the shared drive spectrum (or supplied explicitly) and radiates
// independently of the loop. The two contributions are summed in
// power per frequency.
for (const auto& cable : config.cables) {
if (cable.length_m <= 0.0) continue;
CableEmission ce;
ce.length_m = cable.length_m;
ce.cm_current_a = estimate_cm_current(cable, drive_a);
ce.e_dbuv.assign(freqs.size(), -1000.0);

for (std::size_t k = 0; k < freqs.size(); ++k) {
CableSpec instant = cable;
instant.cm_current_a = ce.cm_current_a[k];
const double e_v =
cable_cm_e_field(instant, freqs[k], config.test_distance_m);
if (e_v <= 0.0) continue;
ce.e_dbuv[k] = 20.0 * std::log10(e_v * 1.0e6);

// Power-sum into the worst-case envelope.
const double loop_dbuv = R.worst_case_dbuv[k];
const double loop_v = (loop_dbuv > -900.0)
? std::pow(10.0, loop_dbuv / 20.0)
: 0.0;
const double cable_v = ce.e_dbuv[k] > -900.0
? std::pow(10.0, ce.e_dbuv[k] / 20.0)
: 0.0;
const double total_v = std::sqrt(loop_v * loop_v +
cable_v * cable_v);
R.worst_case_dbuv[k] = 20.0 * std::log10(total_v);

if (R.worst_case_dbuv[k] > worst_overall_value) {
worst_overall_value = R.worst_case_dbuv[k];
// Tag cables in the verdict with their length to
// distinguish from nets.
worst_overall_net = "<cable " +
std::to_string(static_cast<int>(cable.length_m * 100.0)) +
" cm>";
}
}
R.cables.push_back(std::move(ce));
}

if (R.nets.empty() && R.cables.empty()) {
// Nothing matched -- absence of routed nets is not a PASS.
R.verdict.status = Verdict::Status::NoData;
return R;
Expand Down
22 changes: 20 additions & 2 deletions emikit/emi/BoardAnalysis.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "circuitcore/board/Board.h"
#include "emi/LoopEmissions.h"
#include "emi/Masks.h"
#include "emi/CableCommonMode.h"
#include "emi/Spectrum.h"

namespace emikit::emi {
Expand All @@ -44,6 +45,13 @@ struct AnalysisConfig {
// Only analyze nets whose names match any of these substrings
// (case-insensitive). Empty -> all nets with at least one segment.
std::vector<std::string> net_filter;

// Optional cables whose common-mode emission gets summed into the
// worst-case envelope. Each cable contributes via CableCommonMode;
// I_cm is either explicit (CableSpec::cm_current_a) or estimated
// from CableSpec::ground_inductance_h driven by the shared drive
// spectrum.
std::vector<CableSpec> cables;
};

struct NetEmission {
Expand All @@ -56,6 +64,14 @@ struct NetEmission {
std::vector<double> e_dbuv;
};

struct CableEmission {
double length_m = 0.0;
// Estimated or explicit common-mode current per frequency (A).
std::vector<double> cm_current_a;
// Resulting far-field E in dBuV/m per frequency.
std::vector<double> e_dbuv;
};

struct Verdict {
// NoData: no routed nets matched the filter, so nothing was scored.
// Distinct from Pass -- absence of emissions data is not the same
Expand All @@ -69,8 +85,10 @@ struct Verdict {

struct AnalysisResult {
std::vector<NetEmission> nets;
// For each freq_hz entry: the max E-field across all evaluated
// nets. This is the curve that gets compared to the mask.
std::vector<CableEmission> cables;
// For each freq_hz entry: the power-sum of all per-net loop
// contributions and all per-cable CM contributions. This is the
// curve that gets compared to the mask.
std::vector<double> worst_case_dbuv;
Verdict verdict;
};
Expand Down
35 changes: 30 additions & 5 deletions emikit/emi/CableCommonMode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,16 @@ double cable_cm_e_field(const CableSpec& cable,
return 0.0;
}

const double wavelength = kC / freq_hz;
const double L = cable.length_m;
const double I = std::abs(cable.cm_current_a);
const double r = distance_m;
const double L = cable.length_m;
const double I = std::abs(cable.cm_current_a);
const double r = distance_m;

// Short electric dipole over a ground plane (Hockanson eq 1):
// E = (eta0 / c) * I * L * f / r
// Accurate within a factor of ~2 for L < lambda/2. Beyond that the
// formula overestimates (real cables develop nodes in the current
// distribution that the short-dipole approximation does not capture)
// -- for pre-compliance work overestimation is the safe side.
(void)wavelength;
return (kEta0 / kC) * I * L * freq_hz / r;
}

Expand All @@ -42,4 +40,31 @@ double cable_cm_e_field_dbuv(const CableSpec& cable,
return 20.0 * std::log10(e * 1.0e6);
}

std::vector<double> estimate_cm_current(
const CableSpec& cable,
const std::vector<double>& signal_current_a_per_freq) {
std::vector<double> i_cm(signal_current_a_per_freq.size(), 0.0);

// Explicit override takes precedence.
if (cable.cm_current_a > 0.0) {
for (auto& v : i_cm) v = cable.cm_current_a;
return i_cm;
}

// Estimator needs both inductances to be set.
if (cable.ground_inductance_h <= 0.0 ||
cable.cable_cm_inductance_per_m_h <= 0.0 ||
cable.length_m <= 0.0) {
return i_cm;
}

// Ratio is frequency-independent: I_cm/I_sig = 2*L_gnd / (L_cable*length).
const double ratio = (2.0 * cable.ground_inductance_h) /
(cable.cable_cm_inductance_per_m_h * cable.length_m);
for (std::size_t k = 0; k < signal_current_a_per_freq.size(); ++k) {
i_cm[k] = ratio * std::abs(signal_current_a_per_freq[k]);
}
return i_cm;
}

} // namespace emikit::emi
Loading