diff --git a/packages/embeddedci-mcp/CHANGELOG.md b/packages/embeddedci-mcp/CHANGELOG.md index 8ca82bb..5670390 100644 --- a/packages/embeddedci-mcp/CHANGELOG.md +++ b/packages/embeddedci-mcp/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog — `embeddedci-mcp` +## Unreleased + +- `calibration` and the calibrate tools report `adc_comp`, the ADC input's recorded frequency + response (firmware 3.9.5+). Reference only. + ## 2.13.4 - New tool `adc_range`: read or switch the ADC input range (normal ±32.8 V, low ±5.46 V at 6x the diff --git a/packages/embeddedci-mcp/src/embeddedci_mcp/models.py b/packages/embeddedci-mcp/src/embeddedci_mcp/models.py index 4991ea7..0ea6cc7 100644 --- a/packages/embeddedci-mcp/src/embeddedci_mcp/models.py +++ b/packages/embeddedci-mcp/src/embeddedci_mcp/models.py @@ -468,6 +468,9 @@ class CalibrationResult(BaseModel): adc_gain_ppm: Dict[str, Optional[int]] = Field(default_factory=dict, description=( "ADC gain correction per source (cal1, cal2, ext) in ppm; null = the built-in gain.")) dmm: bool = Field(False, description="True when any DMM-guided part is stored.") + adc_comp: Optional[Dict[str, float]] = Field(None, description=( + "The ADC input's recorded frequency response: hf_dc_gain (high-frequency gain against DC, 1.0 " + "is flat) and c_top_pf. Reference only, no reading is scaled with it. null: not measured.")) adc_low_gain_ppm: Optional[int] = Field(None, description=( "The ADC low range's gain correction in ppm (analog module v3r4+); null = the nominal divider ratio.")) zero_count: Optional[float] = Field(None, description=( diff --git a/packages/embeddedci-mcp/src/embeddedci_mcp/server.py b/packages/embeddedci-mcp/src/embeddedci_mcp/server.py index a3cfc60..94dea78 100644 --- a/packages/embeddedci-mcp/src/embeddedci_mcp/server.py +++ b/packages/embeddedci-mcp/src/embeddedci_mcp/server.py @@ -1391,6 +1391,8 @@ def _calibration_result(c: Any) -> m.CalibrationResult: count=c.count, span=c.span, samples=c.samples, dac=dac, adc_gain_ppm=dict(c.adc_gain_ppm or {}), dmm=c.dmm, measured=c.measured, adc_low_gain_ppm=c.adc_low_gain_ppm, + adc_comp=None if c.adc_comp is None else { + "hf_dc_gain": c.adc_comp.hf_dc_gain, "c_top_pf": c.adc_comp.c_top_pf}, zero_count=c.zero_count) diff --git a/packages/embeddedci/CHANGELOG.md b/packages/embeddedci/CHANGELOG.md index 32fcf9a..aacbdf5 100644 --- a/packages/embeddedci/CHANGELOG.md +++ b/packages/embeddedci/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog — `embeddedci` +## Unreleased + +- `BenchPod.store_adc_compensation(hf_dc_gain, c_top_pf)` records the ADC input's measured frequency + response on the pod (firmware 3.9.5+), and `Calibration.adc_comp` (`AdcCompensation`) reads it. + Reference only: no reading is scaled with it. + ## 2.13.4 - The ADC input range (pod firmware 3.9.5+, analog module v3r4, `Capabilities.adc_low_range`): diff --git a/packages/embeddedci/README.md b/packages/embeddedci/README.md index 2e3e319..f6c6796 100644 --- a/packages/embeddedci/README.md +++ b/packages/embeddedci/README.md @@ -1070,7 +1070,10 @@ bp.clear_calibration("dac") # one part: "offset", "dac" or `calibrate_dac(path, points)` takes two `(code16, volts)` pairs: the 16-bit code `dac_output` drove and the DMM reading. From then on `dac_output`, the calibrated `generate` and the DAC output limits convert with this pod's line (`DacLine`: `a`, `b` per 16-bit code, `offset`, `gain_ppm` -against the built-in line). `calibrate_adc_gain(source)` measures `cal1` through the 5 V output, +against the built-in line). `store_adc_compensation(hf_dc_gain, c_top_pf)` records the ADC input's +measured frequency response on the pod (firmware 3.9.5+), read back as `Calibration.adc_comp` +(`AdcCompensation`), for +reference only: no reading is scaled with it. `calibrate_adc_gain(source)` measures `cal1` through the 5 V output, `cal2` through the ±12 V output and `ext` (the front SMA, which `capture_adc` and the control loop use) through an SMA cable from the ±12 V output to the ADC input; `current_in` follows `cal1`. It needs the ADC offset and the driving output's DMM line, and `Calibration.measured` says what it diff --git a/packages/embeddedci/src/embeddedci/benchpod/__init__.py b/packages/embeddedci/src/embeddedci/benchpod/__init__.py index e1fe311..95fd99f 100644 --- a/packages/embeddedci/src/embeddedci/benchpod/__init__.py +++ b/packages/embeddedci/src/embeddedci/benchpod/__init__.py @@ -124,6 +124,7 @@ def test_boots(benchpod_target, firmware): # fixtures from the pytest plugin AdcReading, AnalogPathState, Calibration, + AdcCompensation, DacLine, CurrentOutput, DacOutput, @@ -172,6 +173,7 @@ def test_boots(benchpod_target, firmware): # fixtures from the pytest plugin "AdcRangeTestPoint", "AdcReading", "Calibration", + "AdcCompensation", "DacLine", "CurrentOutput", "FpgaImageInfo", diff --git a/packages/embeddedci/src/embeddedci/benchpod/client.py b/packages/embeddedci/src/embeddedci/benchpod/client.py index 4085807..5a234c0 100644 --- a/packages/embeddedci/src/embeddedci/benchpod/client.py +++ b/packages/embeddedci/src/embeddedci/benchpod/client.py @@ -1411,6 +1411,20 @@ def calibrate_adc_gain(self, source: AdcGainSource, *, range: AdcRange = "normal self._follow_adc_range("normal") return cal + def store_adc_compensation(self, hf_dc_gain: float, c_top_pf: float) -> Calibration: + """Record the ADC input's measured frequency response on the pod (firmware 3.9.5+). + + ``hf_dc_gain`` is the high-frequency gain against DC and ``c_top_pf`` the divider's fitted + top-leg capacitance, both from ``adc_comp_sweep.py`` in the firmware's calibration scripts. + Reference only: no reading is scaled with them. Read it back as ``Calibration.adc_comp``. + """ + self._require_capability("calibrate", "calibration") + cal = Calibration.from_reply(self.command({"cmd": "calibrate", "comp_hf_dc_e4": round(hf_dc_gain * 1e4), + "comp_c_top_ff": round(c_top_pf * 1e3)})) + if cal.adc_comp is None: + raise BenchPodError("this pod's firmware cannot record the ADC frequency response (needs 3.9.5+)") + return cal + # -- CAN (FDCAN1 / TCAN1044 transceiver) ----------------------------------- # Single-node board: use a loopback ``mode`` to self-test on one pod. :meth:`open_can` and # :class:`~embeddedci.benchpod.can.CanBus` are the convenient interface; these are the diff --git a/packages/embeddedci/src/embeddedci/benchpod/state.py b/packages/embeddedci/src/embeddedci/benchpod/state.py index c81a45a..e2b1673 100644 --- a/packages/embeddedci/src/embeddedci/benchpod/state.py +++ b/packages/embeddedci/src/embeddedci/benchpod/state.py @@ -410,6 +410,23 @@ def from_reply(cls, d: Any) -> Optional["DacLine"]: offset=(_opt_int(d, "offset_uv") or 0) / 1e6, gain_ppm=_opt_int(d, "gain_ppm") or 0) +@dataclass(frozen=True) +class AdcCompensation: + """The ADC input's frequency response recorded on this pod. Reference only: no reading is + scaled with it, and DC accuracy does not depend on it.""" + + #: The high-frequency gain of the ADC input divider against its DC gain. 1.0 is flat. + hf_dc_gain: float + #: The fitted capacitance of the divider's top leg, in pF. + c_top_pf: float + + @classmethod + def from_reply(cls, d: Any) -> Optional["AdcCompensation"]: + if not isinstance(d, dict) or _opt_int(d, "hf_dc_e4") is None or _opt_int(d, "c_top_ff") is None: + return None + return cls(hf_dc_gain=(_opt_int(d, "hf_dc_e4") or 0) / 1e4, c_top_pf=(_opt_int(d, "c_top_ff") or 0) / 1e3) + + @dataclass(frozen=True) class Calibration: """The pod's own calibration (``BenchPod.calibrate`` / ``calibration``). @@ -447,6 +464,8 @@ class Calibration: #: The ADC low range's gain correction in ppm (:meth:`BenchPod.calibrate_adc_gain` with #: ``range="low"``); ``None`` = the divider's nominal ratio, or a board without the low range. adc_low_gain_ppm: Optional[int] = None + #: The ADC input's recorded frequency response; ``None`` = not measured, or firmware before 3.9.5. + adc_comp: Optional[AdcCompensation] = None #: The raw zero the offset is derived from: the mean count ``current_in`` read with J8 open, #: unwrapped. ``None`` when not calibrated or on firmware before 3.9.4. zero_count: Optional[float] = None @@ -470,6 +489,7 @@ def from_reply(cls, reply: Any) -> "Calibration": dac={str(k): DacLine.from_reply(v) for k, v in dac.items()}, adc_gain_ppm={str(k): _opt_int(gains, k) for k in gains}, dmm=d.get("dmm") is True, adc_low_gain_ppm=_opt_int(d, "adc_low_gain_ppm"), + adc_comp=AdcCompensation.from_reply(d.get("adc_comp")), zero_count=None if _opt_int(d, "zero_count_mc") is None else _opt_int(d, "zero_count_mc") / 1000.0, measured=None if meas is None else { k[:-3]: (_opt_int(meas, k) or 0) / 1e6 for k in meas if k.endswith("_uv")}, diff --git a/packages/embeddedci/tests/api_surface.json b/packages/embeddedci/tests/api_surface.json index c049efe..071b61b 100644 --- a/packages/embeddedci/tests/api_surface.json +++ b/packages/embeddedci/tests/api_surface.json @@ -1,4 +1,11 @@ { + "AdcCompensation": { + "__fields__": [ + "hf_dc_gain", + "c_top_pf" + ], + "from_reply": "(cls, d: 'Any') -> \"Optional['AdcCompensation']\"" + }, "AdcGainSource": "Literal['cal1', 'cal2', 'ext']", "AdcRange": "Literal['normal', 'low']", "AdcRangeState": { @@ -174,6 +181,7 @@ "signal": "(self, name: 'str') -> 'GpioPin'", "spi_flash": "(self, image: \"Union[bytes, str, 'os.PathLike[str]']\", addr: 'int' = 0, *, erase: 'bool' = True, verify: 'bool' = True, hold_reset: 'bool' = False, sck: 'Union[Pin, int, str, None]' = None, mosi: 'Union[Pin, int, str, None]' = None, miso: 'Union[Pin, int, str, None]' = None, cs: 'Union[Pin, int, str, None]' = None, hz: 'int' = 6000000, mode: 'int' = 0, progress: 'Optional[_spi.Progress]' = None) -> '_spi.SpiFlashResult'", "status": "(self) -> 'Dict[str, Any]'", + "store_adc_compensation": "(self, hf_dc_gain: 'float', c_top_pf: 'float') -> 'Calibration'", "supports": "(self, feature: 'str') -> 'bool'", "target_power": "(self, efuse: 'Optional[Union[Efuse, int]]' = None, *, on: 'bool', delay: 'Optional[float]' = None) -> 'None'", "target_status": "(self) -> 'TargetStatus'", @@ -211,6 +219,7 @@ "adc_gain_ppm", "dmm", "adc_low_gain_ppm", + "adc_comp", "zero_count", "measured", "raw" @@ -1037,6 +1046,7 @@ "with_changes": "(self, **changes: 'Any') -> \"'Wiring'\"" }, "__all__": [ + "AdcCompensation", "AdcGainSource", "AdcRange", "AdcRangeState",