embeddedci 2.2.0__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {embeddedci-2.2.0 → embeddedci-2.4.0}/CHANGELOG.md +50 -1
- {embeddedci-2.2.0 → embeddedci-2.4.0}/PKG-INFO +151 -6
- {embeddedci-2.2.0 → embeddedci-2.4.0}/README.md +150 -5
- {embeddedci-2.2.0 → embeddedci-2.4.0}/pyproject.toml +1 -1
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/__init__.py +18 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/capabilities.py +16 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/client.py +353 -32
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/constants.py +12 -6
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/dsp.py +4 -1
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/lowlevel.py +3 -3
- embeddedci-2.4.0/src/embeddedci/benchpod/motor_emulator.py +657 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/pytest_plugin.py +35 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/replay.py +1 -1
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/results.py +3 -0
- embeddedci-2.4.0/src/embeddedci/benchpod/spi.py +240 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/state.py +76 -2
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/transport/tcp.py +20 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/waveforms.py +23 -4
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/api_surface.json +150 -6
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/e2e/README.md +1 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/e2e/conftest.py +2 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/e2e/test_e2e_gpio.py +32 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/e2e/test_e2e_la13_14.py +7 -2
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/e2e/test_e2e_pod.py +229 -0
- embeddedci-2.4.0/tests/e2e/test_e2e_spi.py +83 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_client_v2.py +115 -1
- embeddedci-2.4.0/tests/test_current_out_waveforms.py +198 -0
- embeddedci-2.4.0/tests/test_motor_emulator.py +549 -0
- embeddedci-2.4.0/tests/test_spi.py +205 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_tcp_transport.py +40 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/.gitignore +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/LICENSE +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/docs/event-uart-design.md +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/examples/README.md +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/examples/test_bmp280.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/__init__.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/can.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/capture.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/ci.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/cloud_auth.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/connection.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/control_loop.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/decode.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/discovery.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/errors.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/flash.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/gpio.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/i2c.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/lease.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/power.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/protocol.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/sensor.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/server_api.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/transport/__init__.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/transport/base.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/transport/cloud.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/transport/serial.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/uart.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/benchpod/wiring.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/py.typed +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/src/embeddedci/upload_build.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/conftest.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/e2e/e2e_helpers.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/e2e/test_e2e_cloud.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/e2e/test_e2e_dut.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/e2e/test_e2e_rev3.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/e2e/test_e2e_usb.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/examples/test_bmp280_hil.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/examples/test_can_ecu_demo.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/examples/test_can_loopback_hil.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/examples/test_smoke.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_adc_fft_hw.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_api_surface.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_can.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_capabilities.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_capture.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_capture_replay_hw.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_ci.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_cloud.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_connection.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_control_loop.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_control_loop_hw.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_dac_path_ranges.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_decode.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_dsp.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_flash_bridge.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_gpio.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_i2c_decode.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_i2c_parity.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_image_switching.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_integration.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_la_voltage_fixture.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_lease.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_pins_bias.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_protocol.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_replay.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_sensor.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_serial_autodetect.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_serial_json.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_serial_text_console.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_timed_power.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_timing.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_trigger_power.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_uart_capture.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_uart_session.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_upload_build.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_waveforms.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/test_wiring.py +0 -0
- {embeddedci-2.2.0 → embeddedci-2.4.0}/tests/testdata/i2c_decode_vectors.json +0 -0
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
# Changelog — `embeddedci`
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 2.4.0
|
|
4
|
+
|
|
5
|
+
- **Breaking:** the 4-20 mA measurement terminal (J8) is now `"current_in"` as an ADC source
|
|
6
|
+
and analog path, and `lowlevel.cal_switch(current_in=...)` for its relay. The old name read as
|
|
7
|
+
"amplifier". Firmware 3.4.0 no longer accepts the old name, so `AdcSource` and
|
|
8
|
+
`AnalogPath` drop it without a major version. `adc_read("current_in")` also returns the loop
|
|
9
|
+
current in amps as `AdcReading.current`.
|
|
10
|
+
- Calibration: `BenchPod.calibrate()` makes the pod measure the offset of its 4-20 mA input
|
|
11
|
+
(`current_in`, J8 disconnected) and store it in flash; `calibration()` reads it back and
|
|
12
|
+
`clear_calibration()` removes it. All three return a `Calibration` (`calibrated`, `offset` in
|
|
13
|
+
volts, the fit `a` / `b`). `adc_read("current_in")` has the offset taken out and reports it as
|
|
14
|
+
`AdcReading.offset`. New `CalibrateSource` and `Capabilities.calibrate`. Needs firmware 3.4.0.
|
|
15
|
+
- 4-20 mA output: `BenchPod.current_out(amps)` holds a current on terminal J9 and returns a
|
|
16
|
+
`CurrentOutput` with the current actually held, the 16-bit DAC code and the range the output
|
|
17
|
+
can do; `current_out_range()` reads the range only. The pod converts and refuses a current
|
|
18
|
+
outside its range. The output is loop powered (it needs an external floating supply) and shares the DAC with the voltage outputs,
|
|
19
|
+
which `current_out` switches off. New analog path `"current_out"` and
|
|
20
|
+
`Capabilities.current_out`. Needs firmware 3.4.0.
|
|
21
|
+
- Waveforms on the 4-20 mA output: `generate`, `replay` and `replay_waveform` take
|
|
22
|
+
`dac_path="current_out"` (`DacPath` gains it), with levels in amps and the range the pod
|
|
23
|
+
reports. Stopping the returned handle returns the loop to 4 mA.
|
|
24
|
+
- Captures of the 4-20 mA input in amps: `capture_adc(source="current_in")` scales with the pod's
|
|
25
|
+
own fit and fills the new `Capture.currents`. `capture_correlated` takes `source` too, so the
|
|
26
|
+
loop current is captured next to the logic channels off one trigger. Such a capture replays as the same current on
|
|
27
|
+
`current_out`, and `save_capture_as_recording` stores it as a current. `Waveform.unit` says
|
|
28
|
+
whether a library entry is in `"V"` or `"mA"`; `WaveformLibrary.save_recording` takes `unit`,
|
|
29
|
+
and `save_segments` takes amps on the `current_out` path. Needs server support for the unit.
|
|
30
|
+
- Motor & battery emulator: `BenchPod.open_motor_emulator()` returns a `MotorEmulator` for the
|
|
31
|
+
ECP5 emulator boards on the SPI link. It configures every board at once (`configure`, slave
|
|
32
|
+
SPI with PROGRAMN and DONE), reads and writes the link registers by name (`read`, `write`,
|
|
33
|
+
`broadcast=True`), and converts the common ones to units with an `EmulatorCalibration`: bridge
|
|
34
|
+
PWM and duties, sinc samples, protection and trips, the battery model (`BatteryModel`, OCV curve,
|
|
35
|
+
R0 / RC branch, state of charge), the back-EMF shape, the logging stream (`read_log` returns
|
|
36
|
+
whole sample sets) and the board EEPROM. Needs emulator gateware 0x0007+.
|
|
37
|
+
- `SpiSession.stream(data, head=...)` sends data of any size in one CS frame through the pod's
|
|
38
|
+
PSRAM (`load_bin` staging, then the `spi_stream` command). New `SpiStreamResult` and
|
|
39
|
+
`Capabilities.spi_stream`.
|
|
40
|
+
|
|
41
|
+
## 2.3.0
|
|
42
|
+
|
|
43
|
+
- SPI master on the LA pins: `BenchPod.open_spi()` returns an `SpiSession` for raw full-duplex
|
|
44
|
+
transfers (`transfer`, CS held across calls with `hold_cs`) and SPI NOR flash work
|
|
45
|
+
(`flash_id`, `flash_read`, `flash_erase`, `flash_write`, `flash_program`).
|
|
46
|
+
`BenchPod.spi_flash(image, addr)` erases, writes and verifies a whole image in one call, and can
|
|
47
|
+
hold the DUT in reset while it runs (`hold_reset=True`). Pins come from the wiring profile
|
|
48
|
+
(`spi_sclk`, `spi_mosi`, `spi_miso`, `spi_cs`) or as arguments; up to 6 MHz, modes 0 and 3.
|
|
49
|
+
Needs firmware 3.3+ with gateware v45 (capability `spi_master`, new `Capabilities.spi_master`).
|
|
50
|
+
Works over the LAN, USB and the cloud.
|
|
51
|
+
- pytest plugin: with `BENCHPOD_LIFT_DAC_LIMITS=1` a session fixture clears the pod's DAC output
|
|
52
|
+
limits for the run and writes the same limits back at the end, even when tests fail.
|
|
4
53
|
|
|
5
54
|
## 2.2.0
|
|
6
55
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: embeddedci
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: BenchPod SDK and pytest plugin for EmbeddedCI hardware-in-the-loop testing
|
|
5
5
|
Project-URL: Homepage, https://embeddedci.com
|
|
6
6
|
Project-URL: Repository, https://github.com/embeddedci-com/embeddedci-python
|
|
@@ -243,7 +243,7 @@ automatically.)
|
|
|
243
243
|
* **Invalid arguments raise `ValueError`**. A `BenchPodError` subclass always means a device,
|
|
244
244
|
transport or server failure — see [Errors](#errors).
|
|
245
245
|
* **Device state is typed.** Methods return frozen dataclasses (`LaVoltage`, `TargetStatus`,
|
|
246
|
-
`PowerStatus`, `PullState`, `DacOutput`, `AdcReading`, …) from `embeddedci.benchpod.state`; each
|
|
246
|
+
`PowerStatus`, `PullState`, `DacOutput`, `CurrentOutput`, `AdcReading`, …) from `embeddedci.benchpod.state`; each
|
|
247
247
|
keeps the untouched firmware reply in `.raw`.
|
|
248
248
|
* **String options are `Literal` types** and are validated: `DacPath`, `DacOutputPath`,
|
|
249
249
|
`AnalogPath`, `AdcSource`, `LoopSource`, `Waveshape`, `ReplayMapping`, `DecodeProtocol`,
|
|
@@ -504,7 +504,7 @@ how finely a transient was resolved. Measured on a v2 pod:
|
|
|
504
504
|
| eFuse 2 (external) | 5-20 V on the external terminal | ≈ 3.0 A | ≈ 21.8 V | 167 µA |
|
|
505
505
|
|
|
506
506
|
An overload is held at the limit for about 2.8 ms, then the eFuse trips (`TargetStatus` `fault`) and
|
|
507
|
-
retries after ≈ 110 ms. A trip shorter than one sample does not show in a profile. The ADC's `
|
|
507
|
+
retries after ≈ 110 ms. A trip shorter than one sample does not show in a profile. The ADC's `current_in`
|
|
508
508
|
input is a 4-20 mA loop terminal (249 Ω), not a way to measure the target's supply.
|
|
509
509
|
|
|
510
510
|
## Flashing
|
|
@@ -541,6 +541,65 @@ assert result.ok, result.stderr # also: returncode, stdout, target_unreachab
|
|
|
541
541
|
empties the target config's clock-boost reset events, which otherwise race the SWD link.
|
|
542
542
|
* OpenOCD runs on the machine running the test, on every transport including the cloud.
|
|
543
543
|
|
|
544
|
+
## SPI flash and SPI devices
|
|
545
|
+
|
|
546
|
+
Pods with gateware v45+ (`bp.capabilities.spi_master`) run an SPI master on any four LA pins, up
|
|
547
|
+
to 6 MHz, modes 0 and 3. Wire a 25-series SPI NOR flash (W25Q, MX25, GD25, ...) with its SCK, DI,
|
|
548
|
+
DO and CS on LA channels (power it at the LA voltage, tie /WP and /HOLD high) and program it:
|
|
549
|
+
|
|
550
|
+
```python
|
|
551
|
+
res = bp.spi_flash("fw.bin", 0x0, sck=13, mosi=14, miso=6, cs=5, hold_reset=True)
|
|
552
|
+
print(res.jedec_id, res.length, res.seconds) # erased, written and verified
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Or work with a session (pins from the wiring profile's `spi_sclk`, `spi_mosi`, `spi_miso`,
|
|
556
|
+
`spi_cs` when omitted):
|
|
557
|
+
|
|
558
|
+
```python
|
|
559
|
+
with bp.open_spi(hz=6_000_000) as spi:
|
|
560
|
+
print(spi.flash_id()) # SpiFlashInfo(jedec_id='ef4017', present=True, ...)
|
|
561
|
+
data = spi.flash_read(0x0, 4096)
|
|
562
|
+
spi.flash_erase(0x10000, 65536)
|
|
563
|
+
spi.flash_write(0x10000, payload) # 768 B per command, each verified
|
|
564
|
+
rx = spi.transfer(b"\x9f\x00\x00\x00") # raw full-duplex, for any SPI device
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
`hold_reset=True` holds the DUT in reset for the job (rev3 pods), so its own controller does not
|
|
568
|
+
drive the same bus. The SPI master shares the SWD engine: an SWD flash cannot run while a session
|
|
569
|
+
is open. Expect about 30 KB/s written (verified) over the LAN, less over the cloud.
|
|
570
|
+
|
|
571
|
+
`spi.stream(data, head=b"...")` sends data of any size in one CS frame, write-only: it is uploaded
|
|
572
|
+
into the pod's PSRAM first, then clocked out 512 bytes at a time (an FPGA bitstream into a
|
|
573
|
+
slave-SPI configuration port). It needs a LAN or cloud connection and firmware with the
|
|
574
|
+
`spi_stream` command (`bp.capabilities.spi_stream`).
|
|
575
|
+
|
|
576
|
+
## Motor & battery emulator
|
|
577
|
+
|
|
578
|
+
The BenchPod motor emulator boards (ECP5, up to four on one SPI link) are driven with
|
|
579
|
+
`bp.open_motor_emulator()`. The SPI pins come from the wiring profile, as do `PROGRAMN` (open
|
|
580
|
+
drain) and `DONE` (input) when it names them:
|
|
581
|
+
|
|
582
|
+
```python
|
|
583
|
+
from embeddedci.benchpod import BatteryModel
|
|
584
|
+
|
|
585
|
+
with bp.open_motor_emulator() as emu:
|
|
586
|
+
emu.configure("emu.bit") # every board at once; waits for DONE
|
|
587
|
+
print(emu.probe(0)) # BoardInfo(version=7, ...)
|
|
588
|
+
emu.set_protection(0, overcurrent_a=8, overvoltage_on_v=30, overvoltage_off_v=29)
|
|
589
|
+
emu.set_battery(BatteryModel(capacity_ah=2.0, ocv=[25.2, 23.4, 22.2, 18.0], r0_ohm=0.02))
|
|
590
|
+
emu.set_pwm(200_000)
|
|
591
|
+
emu.arm(0)
|
|
592
|
+
emu.set_control(0, pwm=True)
|
|
593
|
+
print(emu.sample(0).phase_currents, emu.battery_state(0).soc)
|
|
594
|
+
emu.start_log(["a", "bus"])
|
|
595
|
+
log = emu.read_log() # whole sample sets, sequence-checked
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
Every register of the link protocol is reachable by name with `emu.read("SPEED")` and
|
|
599
|
+
`emu.write("KE", 1200, board=1)` (`motor_emulator.REGISTERS`); `broadcast=True` writes every
|
|
600
|
+
board. Units use `EmulatorCalibration` (the design values unless you pass a bench calibration).
|
|
601
|
+
Needs emulator gateware 0x0007 or later.
|
|
602
|
+
|
|
544
603
|
## UART
|
|
545
604
|
|
|
546
605
|
`rx` is the LA channel the pod **samples** (wire the DUT's TX here); `tx` is the channel the pod
|
|
@@ -713,7 +772,8 @@ one step:
|
|
|
713
772
|
| `"dac_3v3"`, `"dac_5v"`, `"dac_12v"` | route the DAC to that output (`dac_12v` is the bipolar ±12 V output) |
|
|
714
773
|
| `"adc_ext"` | connect the ADC to the front SMA |
|
|
715
774
|
| `"cal1"`, `"cal2"` | loop the 5 V / 12 V DAC output back into the ADC |
|
|
716
|
-
| `"
|
|
775
|
+
| `"current_in"` | read the 4-20 mA measurement terminal |
|
|
776
|
+
| `"current_out"` | the 4-20 mA output: switch the DAC voltage outputs off |
|
|
717
777
|
| `"off"` | park everything |
|
|
718
778
|
|
|
719
779
|
```python
|
|
@@ -727,9 +787,94 @@ print(bp.adc_read("ext").voltage) # the true front-SMA voltage (the ÷1
|
|
|
727
787
|
```
|
|
728
788
|
|
|
729
789
|
`dac_output` takes calibrated volts (`"3v3"`, `"5v"`, `"12v"`, or `"off"`); without `volts` it only
|
|
730
|
-
routes. `adc_read` routes the source (`"ext"`, `"cal1"`, `"cal2"`, `"
|
|
790
|
+
routes. `adc_read` routes the source (`"ext"`, `"cal1"`, `"cal2"`, `"current_in"`) and returns one averaged,
|
|
731
791
|
calibrated reading; the pod refuses it while the input is still moving (e.g. a DAC left running).
|
|
732
792
|
|
|
793
|
+
### Calibrating the 4-20 mA input
|
|
794
|
+
|
|
795
|
+
`"current_in"` reads the 4-20 mA terminal (J8) across a 249 Ω resistor, so 4 mA is 0.996 V and 20 mA is
|
|
796
|
+
4.98 V. Each pod is off by a few mV there. The pod can measure that offset itself and keep it in
|
|
797
|
+
its flash:
|
|
798
|
+
|
|
799
|
+
```python
|
|
800
|
+
cal = bp.calibrate() # disconnect J8 first. Calibration(source, calibrated, offset, a, b, ...)
|
|
801
|
+
print(cal.offset) # 0.004356: what the open input read, now taken out
|
|
802
|
+
print(bp.adc_read("current_in").voltage) # 0.0 with J8 still open
|
|
803
|
+
print(bp.adc_read("current_in").current) # loop current in amps: 0.004 to 0.020 for a live loop
|
|
804
|
+
|
|
805
|
+
bp.calibration() # what the pod has stored
|
|
806
|
+
bp.clear_calibration() # back to the built-in fit
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
Calibrate once per pod: it survives a reboot and a firmware update. The pod refuses
|
|
810
|
+
(`BenchPodError`) when something is driving J8 (more than 50 mV) and keeps what it had. `a` and `b`
|
|
811
|
+
are the fit the pod now uses for `current_in` (`volts = a + b * count`). Only the offset of `current_in` is
|
|
812
|
+
calibrated; the other sources use the built-in fits. Needs firmware 3.4.0 (capability
|
|
813
|
+
`calibrate`, `Capabilities.calibrate`).
|
|
814
|
+
|
|
815
|
+
### The 4-20 mA output
|
|
816
|
+
|
|
817
|
+
Terminal J9 is a two-wire 4-20 mA transmitter. `current_out` holds a current on it, in amps:
|
|
818
|
+
|
|
819
|
+
```python
|
|
820
|
+
out = bp.current_out(0.012) # CurrentOutput(current, code, min_current, max_current)
|
|
821
|
+
print(out.current) # 0.012: the current actually held (16-bit, 0.25 µA steps)
|
|
822
|
+
print(out.min_current, out.max_current) # 0.004056 0.020094 on a rev3 pod: what the output can do
|
|
823
|
+
|
|
824
|
+
bp.current_out(0.004) # back to the live zero
|
|
825
|
+
print(bp.current_out_range().max_current) # the range only, nothing moves
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
- **It is loop powered.** J9 pin 1 (plus) goes to the plus of an external floating loop supply, pin 2
|
|
829
|
+
(minus) through your receiver to the supply minus. Use 8 V plus 20 mA times the loop
|
|
830
|
+
resistance, 30 V at most (24 V drives up to 800 Ω).
|
|
831
|
+
- **The loop must float.** Pin 2 is not pod ground, and nothing in the loop may touch pod
|
|
832
|
+
ground: not the supply minus, and not a receiver that shares a ground with the pod (through
|
|
833
|
+
SWD or UART wiring to the same target). If it does, the current is no longer regulated.
|
|
834
|
+
- **Do not wire J9 straight into J8.** The output cannot regulate that way.
|
|
835
|
+
- The pod cannot see the loop. `current_out` succeeds with the supply off or the loop open.
|
|
836
|
+
- The output cannot go below its live zero (a little above 4 mA) or above about 20.1 mA: there is no
|
|
837
|
+
0 mA and no 21 mA level. `0.004` gives the live zero. Anything else outside the range raises
|
|
838
|
+
`BenchPodError` with the pod's message. A value of 1 or more raises `ValueError` (amps, not mA).
|
|
839
|
+
- **The DAC is shared** with the 3.3 V / 5 V / ±12 V outputs. `current_out` switches those off
|
|
840
|
+
first. `dac_output`, `generate` and `replay` also move the loop current. `dac_stop` leaves the
|
|
841
|
+
loop where it was: call `current_out(0.004)` to go back to 4 mA.
|
|
842
|
+
- While DAC limits are set (an output stage), the pod refuses `current_out`.
|
|
843
|
+
|
|
844
|
+
Waveforms work on it too. `generate`, `replay` and `replay_waveform` take
|
|
845
|
+
`dac_path="current_out"`, and their levels are then amps:
|
|
846
|
+
|
|
847
|
+
```python
|
|
848
|
+
with bp.generate("sine", freq_hz=2, amplitude=0.006, offset=0.012, dac_path="current_out"):
|
|
849
|
+
... # 6 to 18 mA; leaving the block returns the loop to 4 mA
|
|
850
|
+
|
|
851
|
+
ramp = [0.004 + 0.016 * i / 999 for i in range(1000)]
|
|
852
|
+
bp.replay(ramp, dac_path="current_out", sample_rate_hz=1000) # 4 to 20 mA in 1 s, looping
|
|
853
|
+
|
|
854
|
+
loop = bp.capture_adc(4096, sample_rate_hz=100_000, source="current_in")
|
|
855
|
+
print(max(loop.currents)) # a capture of J8 carries the loop current in amps
|
|
856
|
+
bp.replay(loop, dac_path="current_out") # play the captured loop back on J9
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
- `generate` builds 8-bit levels: steps of about 63 µA. `replay` is 16-bit.
|
|
860
|
+
- Stopping the returned handle returns the loop to 4 mA. A bare `dac_stop()` does not.
|
|
861
|
+
- A capture or recording of a voltage has no current to reproduce: on `current_out` it needs
|
|
862
|
+
`mapping="fit"`, which stretches its shape over the output's range. The same holds for a
|
|
863
|
+
current recording (`Waveform.unit == "mA"`) on a voltage output.
|
|
864
|
+
- In the cloud library, `save_capture_as_recording` stores a capture of `current_in` as a current
|
|
865
|
+
(`unit="mA"`), and `waveforms.save_segments(..., dac_path="current_out")` takes amps.
|
|
866
|
+
- Below 400 kS/s a capture of `current_in` reads about 10 to 20 µA higher than `adc_read`.
|
|
867
|
+
- The FPGA timing features work on the 4-20 mA terminals exactly as on the voltage ones, because
|
|
868
|
+
they are the same DAC and ADC: `capture_correlated(..., source="current_in")` captures the loop
|
|
869
|
+
current and the logic channels off one trigger, `on_capture=True` starts a current waveform at
|
|
870
|
+
the capture's t0, `stop_dac_after` freezes it (the loop then holds that current), and
|
|
871
|
+
`trigger=` starts a capture on an LA edge. There is one ADC and one DAC: a capture is the SMA
|
|
872
|
+
voltage or the J8 current, and the output is a voltage or a current, never both at once.
|
|
873
|
+
|
|
874
|
+
The pod converts with a fit for its board revision (measured on one rev3 pod; the nominal part
|
|
875
|
+
values on v2), the same on every pod of that revision: the output has no per-pod calibration.
|
|
876
|
+
Expect a few tens of µA between pods. Needs firmware 3.4.0 or later (capability `current_out`, `Capabilities.current_out`).
|
|
877
|
+
|
|
733
878
|
## Captures
|
|
734
879
|
|
|
735
880
|
Captures return data with the scaling applied: an ADC `Capture` holds **calibrated volts** (the
|
|
@@ -1192,7 +1337,7 @@ These sit below the stable API and are **not** covered by the stability promise:
|
|
|
1192
1337
|
(raises `FirmwareError` when the pod refuses). Works over TCP, USB and the cloud.
|
|
1193
1338
|
* `bp.transport` — the underlying transport object.
|
|
1194
1339
|
* `bp.lowlevel` — individual analog switches and raw DAC codes for bring-up and diagnostics:
|
|
1195
|
-
`dac_mux(ctrl1=, ctrl2=)`, `dac_mux_status()`, `cal_switch(cal1=, cal2=,
|
|
1340
|
+
`dac_mux(ctrl1=, ctrl2=)`, `dac_mux_status()`, `cal_switch(cal1=, cal2=, current_in=, cal_path=)`,
|
|
1196
1341
|
`cal_switch_status()`, `dac_set(code, divider=)`. Prefer the named paths — these can leave the
|
|
1197
1342
|
front end in a state no named path describes.
|
|
1198
1343
|
|
|
@@ -205,7 +205,7 @@ automatically.)
|
|
|
205
205
|
* **Invalid arguments raise `ValueError`**. A `BenchPodError` subclass always means a device,
|
|
206
206
|
transport or server failure — see [Errors](#errors).
|
|
207
207
|
* **Device state is typed.** Methods return frozen dataclasses (`LaVoltage`, `TargetStatus`,
|
|
208
|
-
`PowerStatus`, `PullState`, `DacOutput`, `AdcReading`, …) from `embeddedci.benchpod.state`; each
|
|
208
|
+
`PowerStatus`, `PullState`, `DacOutput`, `CurrentOutput`, `AdcReading`, …) from `embeddedci.benchpod.state`; each
|
|
209
209
|
keeps the untouched firmware reply in `.raw`.
|
|
210
210
|
* **String options are `Literal` types** and are validated: `DacPath`, `DacOutputPath`,
|
|
211
211
|
`AnalogPath`, `AdcSource`, `LoopSource`, `Waveshape`, `ReplayMapping`, `DecodeProtocol`,
|
|
@@ -466,7 +466,7 @@ how finely a transient was resolved. Measured on a v2 pod:
|
|
|
466
466
|
| eFuse 2 (external) | 5-20 V on the external terminal | ≈ 3.0 A | ≈ 21.8 V | 167 µA |
|
|
467
467
|
|
|
468
468
|
An overload is held at the limit for about 2.8 ms, then the eFuse trips (`TargetStatus` `fault`) and
|
|
469
|
-
retries after ≈ 110 ms. A trip shorter than one sample does not show in a profile. The ADC's `
|
|
469
|
+
retries after ≈ 110 ms. A trip shorter than one sample does not show in a profile. The ADC's `current_in`
|
|
470
470
|
input is a 4-20 mA loop terminal (249 Ω), not a way to measure the target's supply.
|
|
471
471
|
|
|
472
472
|
## Flashing
|
|
@@ -503,6 +503,65 @@ assert result.ok, result.stderr # also: returncode, stdout, target_unreachab
|
|
|
503
503
|
empties the target config's clock-boost reset events, which otherwise race the SWD link.
|
|
504
504
|
* OpenOCD runs on the machine running the test, on every transport including the cloud.
|
|
505
505
|
|
|
506
|
+
## SPI flash and SPI devices
|
|
507
|
+
|
|
508
|
+
Pods with gateware v45+ (`bp.capabilities.spi_master`) run an SPI master on any four LA pins, up
|
|
509
|
+
to 6 MHz, modes 0 and 3. Wire a 25-series SPI NOR flash (W25Q, MX25, GD25, ...) with its SCK, DI,
|
|
510
|
+
DO and CS on LA channels (power it at the LA voltage, tie /WP and /HOLD high) and program it:
|
|
511
|
+
|
|
512
|
+
```python
|
|
513
|
+
res = bp.spi_flash("fw.bin", 0x0, sck=13, mosi=14, miso=6, cs=5, hold_reset=True)
|
|
514
|
+
print(res.jedec_id, res.length, res.seconds) # erased, written and verified
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Or work with a session (pins from the wiring profile's `spi_sclk`, `spi_mosi`, `spi_miso`,
|
|
518
|
+
`spi_cs` when omitted):
|
|
519
|
+
|
|
520
|
+
```python
|
|
521
|
+
with bp.open_spi(hz=6_000_000) as spi:
|
|
522
|
+
print(spi.flash_id()) # SpiFlashInfo(jedec_id='ef4017', present=True, ...)
|
|
523
|
+
data = spi.flash_read(0x0, 4096)
|
|
524
|
+
spi.flash_erase(0x10000, 65536)
|
|
525
|
+
spi.flash_write(0x10000, payload) # 768 B per command, each verified
|
|
526
|
+
rx = spi.transfer(b"\x9f\x00\x00\x00") # raw full-duplex, for any SPI device
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
`hold_reset=True` holds the DUT in reset for the job (rev3 pods), so its own controller does not
|
|
530
|
+
drive the same bus. The SPI master shares the SWD engine: an SWD flash cannot run while a session
|
|
531
|
+
is open. Expect about 30 KB/s written (verified) over the LAN, less over the cloud.
|
|
532
|
+
|
|
533
|
+
`spi.stream(data, head=b"...")` sends data of any size in one CS frame, write-only: it is uploaded
|
|
534
|
+
into the pod's PSRAM first, then clocked out 512 bytes at a time (an FPGA bitstream into a
|
|
535
|
+
slave-SPI configuration port). It needs a LAN or cloud connection and firmware with the
|
|
536
|
+
`spi_stream` command (`bp.capabilities.spi_stream`).
|
|
537
|
+
|
|
538
|
+
## Motor & battery emulator
|
|
539
|
+
|
|
540
|
+
The BenchPod motor emulator boards (ECP5, up to four on one SPI link) are driven with
|
|
541
|
+
`bp.open_motor_emulator()`. The SPI pins come from the wiring profile, as do `PROGRAMN` (open
|
|
542
|
+
drain) and `DONE` (input) when it names them:
|
|
543
|
+
|
|
544
|
+
```python
|
|
545
|
+
from embeddedci.benchpod import BatteryModel
|
|
546
|
+
|
|
547
|
+
with bp.open_motor_emulator() as emu:
|
|
548
|
+
emu.configure("emu.bit") # every board at once; waits for DONE
|
|
549
|
+
print(emu.probe(0)) # BoardInfo(version=7, ...)
|
|
550
|
+
emu.set_protection(0, overcurrent_a=8, overvoltage_on_v=30, overvoltage_off_v=29)
|
|
551
|
+
emu.set_battery(BatteryModel(capacity_ah=2.0, ocv=[25.2, 23.4, 22.2, 18.0], r0_ohm=0.02))
|
|
552
|
+
emu.set_pwm(200_000)
|
|
553
|
+
emu.arm(0)
|
|
554
|
+
emu.set_control(0, pwm=True)
|
|
555
|
+
print(emu.sample(0).phase_currents, emu.battery_state(0).soc)
|
|
556
|
+
emu.start_log(["a", "bus"])
|
|
557
|
+
log = emu.read_log() # whole sample sets, sequence-checked
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
Every register of the link protocol is reachable by name with `emu.read("SPEED")` and
|
|
561
|
+
`emu.write("KE", 1200, board=1)` (`motor_emulator.REGISTERS`); `broadcast=True` writes every
|
|
562
|
+
board. Units use `EmulatorCalibration` (the design values unless you pass a bench calibration).
|
|
563
|
+
Needs emulator gateware 0x0007 or later.
|
|
564
|
+
|
|
506
565
|
## UART
|
|
507
566
|
|
|
508
567
|
`rx` is the LA channel the pod **samples** (wire the DUT's TX here); `tx` is the channel the pod
|
|
@@ -675,7 +734,8 @@ one step:
|
|
|
675
734
|
| `"dac_3v3"`, `"dac_5v"`, `"dac_12v"` | route the DAC to that output (`dac_12v` is the bipolar ±12 V output) |
|
|
676
735
|
| `"adc_ext"` | connect the ADC to the front SMA |
|
|
677
736
|
| `"cal1"`, `"cal2"` | loop the 5 V / 12 V DAC output back into the ADC |
|
|
678
|
-
| `"
|
|
737
|
+
| `"current_in"` | read the 4-20 mA measurement terminal |
|
|
738
|
+
| `"current_out"` | the 4-20 mA output: switch the DAC voltage outputs off |
|
|
679
739
|
| `"off"` | park everything |
|
|
680
740
|
|
|
681
741
|
```python
|
|
@@ -689,9 +749,94 @@ print(bp.adc_read("ext").voltage) # the true front-SMA voltage (the ÷1
|
|
|
689
749
|
```
|
|
690
750
|
|
|
691
751
|
`dac_output` takes calibrated volts (`"3v3"`, `"5v"`, `"12v"`, or `"off"`); without `volts` it only
|
|
692
|
-
routes. `adc_read` routes the source (`"ext"`, `"cal1"`, `"cal2"`, `"
|
|
752
|
+
routes. `adc_read` routes the source (`"ext"`, `"cal1"`, `"cal2"`, `"current_in"`) and returns one averaged,
|
|
693
753
|
calibrated reading; the pod refuses it while the input is still moving (e.g. a DAC left running).
|
|
694
754
|
|
|
755
|
+
### Calibrating the 4-20 mA input
|
|
756
|
+
|
|
757
|
+
`"current_in"` reads the 4-20 mA terminal (J8) across a 249 Ω resistor, so 4 mA is 0.996 V and 20 mA is
|
|
758
|
+
4.98 V. Each pod is off by a few mV there. The pod can measure that offset itself and keep it in
|
|
759
|
+
its flash:
|
|
760
|
+
|
|
761
|
+
```python
|
|
762
|
+
cal = bp.calibrate() # disconnect J8 first. Calibration(source, calibrated, offset, a, b, ...)
|
|
763
|
+
print(cal.offset) # 0.004356: what the open input read, now taken out
|
|
764
|
+
print(bp.adc_read("current_in").voltage) # 0.0 with J8 still open
|
|
765
|
+
print(bp.adc_read("current_in").current) # loop current in amps: 0.004 to 0.020 for a live loop
|
|
766
|
+
|
|
767
|
+
bp.calibration() # what the pod has stored
|
|
768
|
+
bp.clear_calibration() # back to the built-in fit
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
Calibrate once per pod: it survives a reboot and a firmware update. The pod refuses
|
|
772
|
+
(`BenchPodError`) when something is driving J8 (more than 50 mV) and keeps what it had. `a` and `b`
|
|
773
|
+
are the fit the pod now uses for `current_in` (`volts = a + b * count`). Only the offset of `current_in` is
|
|
774
|
+
calibrated; the other sources use the built-in fits. Needs firmware 3.4.0 (capability
|
|
775
|
+
`calibrate`, `Capabilities.calibrate`).
|
|
776
|
+
|
|
777
|
+
### The 4-20 mA output
|
|
778
|
+
|
|
779
|
+
Terminal J9 is a two-wire 4-20 mA transmitter. `current_out` holds a current on it, in amps:
|
|
780
|
+
|
|
781
|
+
```python
|
|
782
|
+
out = bp.current_out(0.012) # CurrentOutput(current, code, min_current, max_current)
|
|
783
|
+
print(out.current) # 0.012: the current actually held (16-bit, 0.25 µA steps)
|
|
784
|
+
print(out.min_current, out.max_current) # 0.004056 0.020094 on a rev3 pod: what the output can do
|
|
785
|
+
|
|
786
|
+
bp.current_out(0.004) # back to the live zero
|
|
787
|
+
print(bp.current_out_range().max_current) # the range only, nothing moves
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
- **It is loop powered.** J9 pin 1 (plus) goes to the plus of an external floating loop supply, pin 2
|
|
791
|
+
(minus) through your receiver to the supply minus. Use 8 V plus 20 mA times the loop
|
|
792
|
+
resistance, 30 V at most (24 V drives up to 800 Ω).
|
|
793
|
+
- **The loop must float.** Pin 2 is not pod ground, and nothing in the loop may touch pod
|
|
794
|
+
ground: not the supply minus, and not a receiver that shares a ground with the pod (through
|
|
795
|
+
SWD or UART wiring to the same target). If it does, the current is no longer regulated.
|
|
796
|
+
- **Do not wire J9 straight into J8.** The output cannot regulate that way.
|
|
797
|
+
- The pod cannot see the loop. `current_out` succeeds with the supply off or the loop open.
|
|
798
|
+
- The output cannot go below its live zero (a little above 4 mA) or above about 20.1 mA: there is no
|
|
799
|
+
0 mA and no 21 mA level. `0.004` gives the live zero. Anything else outside the range raises
|
|
800
|
+
`BenchPodError` with the pod's message. A value of 1 or more raises `ValueError` (amps, not mA).
|
|
801
|
+
- **The DAC is shared** with the 3.3 V / 5 V / ±12 V outputs. `current_out` switches those off
|
|
802
|
+
first. `dac_output`, `generate` and `replay` also move the loop current. `dac_stop` leaves the
|
|
803
|
+
loop where it was: call `current_out(0.004)` to go back to 4 mA.
|
|
804
|
+
- While DAC limits are set (an output stage), the pod refuses `current_out`.
|
|
805
|
+
|
|
806
|
+
Waveforms work on it too. `generate`, `replay` and `replay_waveform` take
|
|
807
|
+
`dac_path="current_out"`, and their levels are then amps:
|
|
808
|
+
|
|
809
|
+
```python
|
|
810
|
+
with bp.generate("sine", freq_hz=2, amplitude=0.006, offset=0.012, dac_path="current_out"):
|
|
811
|
+
... # 6 to 18 mA; leaving the block returns the loop to 4 mA
|
|
812
|
+
|
|
813
|
+
ramp = [0.004 + 0.016 * i / 999 for i in range(1000)]
|
|
814
|
+
bp.replay(ramp, dac_path="current_out", sample_rate_hz=1000) # 4 to 20 mA in 1 s, looping
|
|
815
|
+
|
|
816
|
+
loop = bp.capture_adc(4096, sample_rate_hz=100_000, source="current_in")
|
|
817
|
+
print(max(loop.currents)) # a capture of J8 carries the loop current in amps
|
|
818
|
+
bp.replay(loop, dac_path="current_out") # play the captured loop back on J9
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
- `generate` builds 8-bit levels: steps of about 63 µA. `replay` is 16-bit.
|
|
822
|
+
- Stopping the returned handle returns the loop to 4 mA. A bare `dac_stop()` does not.
|
|
823
|
+
- A capture or recording of a voltage has no current to reproduce: on `current_out` it needs
|
|
824
|
+
`mapping="fit"`, which stretches its shape over the output's range. The same holds for a
|
|
825
|
+
current recording (`Waveform.unit == "mA"`) on a voltage output.
|
|
826
|
+
- In the cloud library, `save_capture_as_recording` stores a capture of `current_in` as a current
|
|
827
|
+
(`unit="mA"`), and `waveforms.save_segments(..., dac_path="current_out")` takes amps.
|
|
828
|
+
- Below 400 kS/s a capture of `current_in` reads about 10 to 20 µA higher than `adc_read`.
|
|
829
|
+
- The FPGA timing features work on the 4-20 mA terminals exactly as on the voltage ones, because
|
|
830
|
+
they are the same DAC and ADC: `capture_correlated(..., source="current_in")` captures the loop
|
|
831
|
+
current and the logic channels off one trigger, `on_capture=True` starts a current waveform at
|
|
832
|
+
the capture's t0, `stop_dac_after` freezes it (the loop then holds that current), and
|
|
833
|
+
`trigger=` starts a capture on an LA edge. There is one ADC and one DAC: a capture is the SMA
|
|
834
|
+
voltage or the J8 current, and the output is a voltage or a current, never both at once.
|
|
835
|
+
|
|
836
|
+
The pod converts with a fit for its board revision (measured on one rev3 pod; the nominal part
|
|
837
|
+
values on v2), the same on every pod of that revision: the output has no per-pod calibration.
|
|
838
|
+
Expect a few tens of µA between pods. Needs firmware 3.4.0 or later (capability `current_out`, `Capabilities.current_out`).
|
|
839
|
+
|
|
695
840
|
## Captures
|
|
696
841
|
|
|
697
842
|
Captures return data with the scaling applied: an ADC `Capture` holds **calibrated volts** (the
|
|
@@ -1154,7 +1299,7 @@ These sit below the stable API and are **not** covered by the stability promise:
|
|
|
1154
1299
|
(raises `FirmwareError` when the pod refuses). Works over TCP, USB and the cloud.
|
|
1155
1300
|
* `bp.transport` — the underlying transport object.
|
|
1156
1301
|
* `bp.lowlevel` — individual analog switches and raw DAC codes for bring-up and diagnostics:
|
|
1157
|
-
`dac_mux(ctrl1=, ctrl2=)`, `dac_mux_status()`, `cal_switch(cal1=, cal2=,
|
|
1302
|
+
`dac_mux(ctrl1=, ctrl2=)`, `dac_mux_status()`, `cal_switch(cal1=, cal2=, current_in=, cal_path=)`,
|
|
1158
1303
|
`cal_switch_status()`, `dac_set(code, divider=)`. Prefer the named paths — these can leave the
|
|
1159
1304
|
front end in a state no named path describes.
|
|
1160
1305
|
|
|
@@ -49,6 +49,7 @@ from .constants import (
|
|
|
49
49
|
PIN13,
|
|
50
50
|
PIN14,
|
|
51
51
|
AdcSource,
|
|
52
|
+
CalibrateSource,
|
|
52
53
|
AnalogPath,
|
|
53
54
|
CanMode,
|
|
54
55
|
DacOutputPath,
|
|
@@ -105,6 +106,8 @@ from .server_api import ServerApi, ServerApiError
|
|
|
105
106
|
from .state import (
|
|
106
107
|
AdcReading,
|
|
107
108
|
AnalogPathState,
|
|
109
|
+
Calibration,
|
|
110
|
+
CurrentOutput,
|
|
108
111
|
DacOutput,
|
|
109
112
|
EfuseState,
|
|
110
113
|
FpgaImageInfo,
|
|
@@ -118,6 +121,9 @@ from .state import (
|
|
|
118
121
|
UsbCcStatus,
|
|
119
122
|
)
|
|
120
123
|
from .uart import UartCapture, UartSession
|
|
124
|
+
from .spi import SpiFlashInfo, SpiFlashResult, SpiSession, SpiStreamResult
|
|
125
|
+
from . import motor_emulator
|
|
126
|
+
from .motor_emulator import BatteryModel, EmulatorCalibration, MotorEmulator
|
|
121
127
|
from .waveforms import Waveform, WaveformLibrary
|
|
122
128
|
from .wiring import Signal, Wiring
|
|
123
129
|
|
|
@@ -144,12 +150,23 @@ __all__ = [
|
|
|
144
150
|
"AnalogPathState",
|
|
145
151
|
"DacOutput",
|
|
146
152
|
"AdcReading",
|
|
153
|
+
"Calibration",
|
|
154
|
+
"CurrentOutput",
|
|
147
155
|
"FpgaImageInfo",
|
|
148
156
|
"LoopState",
|
|
149
157
|
# flash + UART
|
|
150
158
|
"FlashResult",
|
|
151
159
|
"UartCapture",
|
|
152
160
|
"UartSession",
|
|
161
|
+
"SpiSession",
|
|
162
|
+
"SpiFlashInfo",
|
|
163
|
+
"SpiFlashResult",
|
|
164
|
+
"SpiStreamResult",
|
|
165
|
+
# motor & battery emulator
|
|
166
|
+
"motor_emulator",
|
|
167
|
+
"MotorEmulator",
|
|
168
|
+
"EmulatorCalibration",
|
|
169
|
+
"BatteryModel",
|
|
153
170
|
# CAN
|
|
154
171
|
"can",
|
|
155
172
|
"CanBus",
|
|
@@ -222,6 +239,7 @@ __all__ = [
|
|
|
222
239
|
"DacOutputPath",
|
|
223
240
|
"AnalogPath",
|
|
224
241
|
"AdcSource",
|
|
242
|
+
"CalibrateSource",
|
|
225
243
|
"Waveshape",
|
|
226
244
|
"ReplayMapping",
|
|
227
245
|
"LoopSource",
|
|
@@ -158,6 +158,16 @@ class Capabilities:
|
|
|
158
158
|
nrst_pin: bool = False
|
|
159
159
|
#: USB-C CC-line monitoring (rev3 pods): :meth:`BenchPod.usb_cc`.
|
|
160
160
|
usb_cc: bool = False
|
|
161
|
+
#: SPI master on four LA pins and SPI NOR flash programming (gateware >= v45):
|
|
162
|
+
#: :meth:`BenchPod.open_spi`, :meth:`BenchPod.spi_flash`.
|
|
163
|
+
spi_master: bool = False
|
|
164
|
+
#: ``spi_stream``: send an upload staged in PSRAM through the SPI master in one CS frame
|
|
165
|
+
#: (an FPGA bitstream): :meth:`SpiSession.stream <embeddedci.benchpod.spi.SpiSession.stream>`.
|
|
166
|
+
spi_stream: bool = False
|
|
167
|
+
#: The pod can calibrate its own ADC and keeps the result: :meth:`BenchPod.calibrate`.
|
|
168
|
+
calibrate: bool = False
|
|
169
|
+
#: The pod can hold a current on its 4-20 mA output (J9): :meth:`BenchPod.current_out`.
|
|
170
|
+
current_out: bool = False
|
|
161
171
|
|
|
162
172
|
#: LA I/O-bank voltage the pod currently reports (mV), if known.
|
|
163
173
|
la_vccio_mv: int = 0
|
|
@@ -233,6 +243,10 @@ class Capabilities:
|
|
|
233
243
|
("capture_b64", "capture_b64"),
|
|
234
244
|
("nrst_pin", "nrst_pin"),
|
|
235
245
|
("usb_cc", "usb_cc"),
|
|
246
|
+
("spi_master", "spi_master"),
|
|
247
|
+
("spi_stream", "spi_stream"),
|
|
248
|
+
("calibrate", "calibrate"),
|
|
249
|
+
("current_out", "current_out"),
|
|
236
250
|
):
|
|
237
251
|
if name in names and hasattr(c, attr):
|
|
238
252
|
setattr(c, attr, True)
|
|
@@ -282,6 +296,8 @@ class Capabilities:
|
|
|
282
296
|
("capture_trigger", "cap.capture_trigger"), ("power_profile", "cap.power_profile"),
|
|
283
297
|
("capture_b64", "cap.capture_b64"),
|
|
284
298
|
("nrst_pin", "cap.nrst_pin"), ("usb_cc", "cap.usb_cc"),
|
|
299
|
+
("spi_master", "cap.spi_master"), ("spi_stream", "cap.spi_stream"),
|
|
300
|
+
("calibrate", "cap.calibrate"), ("current_out", "cap.current_out"),
|
|
285
301
|
):
|
|
286
302
|
b = _as_bool(params, key)
|
|
287
303
|
if b is not None:
|