diff --git a/README.md b/README.md index 2740f41..8caf147 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ cm_interface/ │ ├─ clock_checks.py # cm-clock-checks: read-only Si5395/Si5341 status checks │ ├─ clock_dump.py # cm-clock-dump: clock register dump (--out-dir) │ ├─ exercise_mcu.py # cm-exercise-mcu: exercise the MCU pages; writes/control prompt unless --yes +│ ├─ ff_cdr.py # cm-ff-cdr: report/enable/disable Firefly CDR per channel +│ ├─ ff_rx_eq.py # cm-ff-rx-eq: report/set Firefly Rx amplitude and pre-emphasis per channel │ ├─ ff_status.py # cm-ff-status: Firefly status summary (--out-dir) │ ├─ lga80d_checks.py # cm-lga80d-checks: read-only LGA80D frequency/temperature/timing checks │ ├─ lga80d_dump.py # cm-lga80d-dump: LGA80D register dump (--snapshot adds the SNAPSHOT read) @@ -73,7 +75,147 @@ register JSON maps. Install a downloaded binary RPM with The RPM also installs the command-line tools as `cm-` (for example `cm-lga80d-dump --all`). The same tool runs from a checkout as `./tools/lga80d_dump.py --all`; the `cm-*` names exist only after the RPM is -installed. +installed. See [Command-line tools](#command-line-tools) for each tool's usage. + +## Command-line tools + +The RPM installs these as `cm-` in `/usr/bin`. Every tool has `--help` +with the full details (register addresses, datasheet tables, caveats). + +### Common options + +| Option | Meaning | +|---|---| +| `NAME ...` / `--all` | Devices to work on. Give names together, not split around options (`cm-ff-status F1_1 F2_6 --out-dir d`, not `cm-ff-status F1_1 --out-dir d F2_6`). Dumps and Firefly tools need one or the other; checks default to all. | +| `--out-dir DIR` | Dumps only: write `DIR/cm_.txt` instead of printing; stdout then shows only the `wrote` lines. Existing files are overwritten. | +| `--force` | Clock and Firefly tools only: run even if the MCU power state machine is not `POWER_ON` (see below). | +| `--dev-path DEV` | ProgCom UART (default `/dev/ttyUL4`). | +| `--debug` | Print every UART transaction. | + +**Power gate.** The clock and Firefly tools (`cm-clock-checks`, +`cm-clock-dump`, `cm-ff-status`, `cm-ff-cdr`, `cm-ff-rx-eq`) read the MCU +power state first. If it is not `POWER_ON` they print +`power is off: ... nothing read` and exit 1; `--force` reads anyway. If the +state can't be read (old firmware) they run. `cm-clock-dump`, `cm-ff-status` +and `cm-lga80d-dump` record the power state, with any latched fault flags, in +their header. The LGA80D tools are not gated: +the modules answer PMBus with the board off. + +The dump, check and Firefly tools exit 1 if any read or write failed, or a +check failed. + +### Clocks + +| Command | What it does | +|---|---| +| `cm-clock-checks` | Read-only Si5395/Si5341 checks: `id`, `status` (live LOS/OOF/LOL/holdover), `flags` (sticky). Alarms the configuration explains (disabled input, free run) are shown but don't fail. Ends with a clock x check OK/FAIL table. | +| `cm-clock-dump` | Read-only summary dump per clock: status, outputs with R dividers, P/M/N dividers and derived frequencies. | + +``` +cm-clock-checks # all checks, all clocks +cm-clock-checks R0A R1B --check flags +cm-clock-dump --all --out-dir DIR +cm-clock-dump R0A --hw-rev 2 # board EEPROM not programmed +``` + +Clock names: `R0A R0B R1A R1B R1C`. The chip at each position comes from the +board revision the MCU reports; `--hw-rev N` overrides it. + +### Fireflies + +Firefly locations are `F1_1` ... `F2_7`; which exist depends on the board +revision. A location selects both halves (Tx and Rx) of an x12 pair. Which +modules are fitted comes from the MCU's present mask, so `--all` means every +populated module on this board. A module not in the MCU's user mask is listed +but not accessed. + +| Command | What it does | +|---|---| +| `cm-ff-status` | Read-only status per module: part, serial, temperature, Vcc, decoded alarm latches (LOS, faults, CDR loss of lock, Rx power). | +| `cm-ff-cdr` | Report or change CDR enable per channel. **Writes** with `--enable`/`--disable`. | +| `cm-ff-rx-eq` | Report or change Rx output amplitude and pre-emphasis per channel. **Writes** with `--amplitude`/`--preemphasis`. | + +``` +cm-ff-status --all +cm-ff-status F1_1 F2_6 --out-dir DIR + +cm-ff-cdr --all # report +cm-ff-cdr F1_1 --side rx --disable --channels 3,10 +cm-ff-cdr F2_6 --side tx --enable --channels 2 +cm-ff-cdr F1_1 F1_2 --enable # all channels, both sides + +cm-ff-rx-eq --all # report +cm-ff-rx-eq F1_1 --amplitude 5 --channels 1,3,5-8 +cm-ff-rx-eq F1_1 --preemphasis 3 --channels 12 +cm-ff-rx-eq F2_6 --preemphasis 3 # all 4 channels of an x4 +``` + +Per-channel changes: + +- `--channels` takes a comma list with ranges (`1,3,5-8`); without it the + change goes to every channel. It only applies together with a change + option; a report always shows every channel. +- Channels are 1-based: 1-12 on x12, 1-4 on x4. The x12 datasheet's Rx00-Rx11 + are channels 1-12. +- One run sets one value. For different values on different channels, run + once per value. +- `--side tx|rx` (`cm-ff-cdr`) limits the change and report to one side. +- Each changed module is read back and the read-back is printed. + +Values are the raw 4-bit codes; the report shows what each means. + +| | x12 (25G Rx) | x4 | +|---|---|---| +| Amplitude | 0-15: 80 mV + 76 mV/step, 7-15 = 612 mV (default 4 = 384 mV) | 0-3: 300, 450, 600, 900 mV (default 0) | +| Pre-emphasis | 0-15: 4.8 % of amplitude per step, 7-15 = 33.6 % (default 2 = 9.6 %) | 0-7: 0-6 dB, 7 = 7.5 dB (default 0) | + +Caveats: the x4 datasheet calls the amplitude a global parameter for the four +channels, so a per-channel amplitude may not take effect independently. +CERN-B and 14G x12 modules are skipped by `cm-ff-cdr` and `cm-ff-rx-eq`. +If one module fails (for example `--channels 10` on an x4), the others are +still changed and the exit status is 1. + +### LGA80D DC-DC converters + +Supply names: `3V3/1V8 F1VCCINT1 F1VCCINT2 F2VCCINT1 F2VCCINT2 F1AVTT/CC F2AVTT/CC`. + +| Command | What it does | +|---|---| +| `cm-lga80d-status` | Read-only decoded PMBus status registers (STATUS_WORD, VOUT/IOUT/INPUT/TEMP/CML/MFR). | +| `cm-lga80d-dump` | Read-only dump of every readable PMBus register, both output pages. `--snapshot` also captures the 32-byte SNAPSHOT through the MCU (not a pure read; needs firmware with the SN device). | +| `cm-lga80d-checks` | Read-only checks: `freq` (did LGA80D_init take effect; run powered on), `temp` (die vs DrMOS temperature; run under load), `timing` (turn-on/off settings vs the firmware's PG check). | + +``` +cm-lga80d-status --all +cm-lga80d-dump F1VCCINT1 --page 0 +cm-lga80d-dump --all --out-dir DIR +cm-lga80d-dump F1VCCINT1 --snapshot +cm-lga80d-checks --check freq +cm-lga80d-checks F1VCCINT2 --check temp +``` + +`cm-lga80d-dump --include-unconfirmed` also reads registers the datasheet +doesn't list as supported; that can set the "invalid command" bit in +STATUS_CML. + +### MCU + +| Command | What it does | +|---|---| +| `cm-mcu-dump` | Read-only dump of the MCU's ProgCom pages (System, Power, Alarm, Config, Runtime, ADC). Uptime, RTC, heap/stack and ADC values change on every read. | +| `cm-mcu-set-rtc` | Set the MCU RTC to this host's local time, then read it back. **Writes.** Check the host clock first. | +| `cm-exercise-mcu` | Read every MCU page and report each independently. With an option it **writes** and asks for confirmation (`--yes` skips it). | + +``` +cm-mcu-dump --out-dir DIR +cm-mcu-set-rtc --dry-run # show what would be written +cm-mcu-set-rtc +cm-exercise-mcu +cm-exercise-mcu --send-control CLEAR_ALARM_LATCHES +cm-exercise-mcu --power-cycle # power inhibit for 15 s +cm-exercise-mcu --set-alarm-temp FPGA 85 # persists to EEPROM; 50-100 C +cm-exercise-mcu --set-volt-threshold 5 # persists to EEPROM; 1-10 % +``` ## Basic usage ```python diff --git a/device/firefly.py b/device/firefly.py index f7429ab..c966347 100644 --- a/device/firefly.py +++ b/device/firefly.py @@ -43,6 +43,17 @@ def _encode_command(self, reg: int, write: bool, payload: bytes = b"", def _decode_response(self, raw: bytes) -> bytes: return self._decode_ascii_response(raw) + def write_reg(self, reg: int, data: bytes) -> None: + """Write ``data`` one byte per transaction: the MCU's ProgCom ``FF`` + write takes exactly one data byte. A multi-byte write is therefore + not atomic.""" + if (reg & 0xFF) + len(data) > 0x100: + raise ValueError( + f"write of {len(data)} bytes at 0x{reg:04X} crosses a page boundary" + ) + for i, value in enumerate(data): + super().write_reg(reg + i, bytes([value])) + @property def temperature(self) -> int: """Return the module temperature in signed integer degrees Celsius.""" @@ -142,8 +153,9 @@ def _validate_channels(channels: list, max_channel: int) -> None: f"invalid firefly channel: {ch!r} (must be an int 1-{max_channel})" ) - def _disable_cdr_12ch(self, base_reg: int, channels=None) -> None: - """Clear the requested channels' CDR-enable bits on a 12-channel device. + def _set_cdr_12ch(self, base_reg: int, channels, enable: bool) -> None: + """Set or clear the requested channels' CDR-enable bits on a 12-channel + device. ``base_reg`` and ``base_reg + 1`` together hold a 12-bit mask: the low nibble of ``base_reg`` covers channels 9-12, and ``base_reg + 1`` @@ -159,14 +171,55 @@ def _disable_cdr_12ch(self, base_reg: int, channels=None) -> None: hi, lo = self.read_reg(base_reg, size=2) mask = ((hi & 0x0F) << 8) | lo for ch in channels: - mask &= ~(1 << (ch - 1)) + if enable: + mask |= 1 << (ch - 1) + else: + mask &= ~(1 << (ch - 1)) new_hi = (hi & 0xF0) | ((mask >> 8) & 0x0F) new_lo = mask & 0xFF self.write_reg(base_reg, bytes([new_hi, new_lo])) + @staticmethod + def _nibble_position(index: int) -> Tuple[int, int]: + """(byte offset, shift) of the index-th nibble, high nibble first.""" + return index // 2, 4 if index % 2 == 0 else 0 + + def _read_nibbles(self, reg: int, order: list, channels=None) -> dict: + """Return {channel: 4-bit value} from a packed nibble field. ``order`` + lists the channel held by each nibble, starting at ``reg``'s high + nibble.""" + channels = self._normalize_channels(channels) + if channels is None: + channels = sorted(order) + self._validate_channels(channels, len(order)) + raw = self.read_block(reg, len(order) // 2) + result = {} + for ch in channels: + byte, shift = self._nibble_position(order.index(ch)) + result[ch] = (raw[byte] >> shift) & 0xF + return result + + def _write_nibbles(self, reg: int, order: list, value: int, channels=None) -> None: + """Set the requested channels' nibbles (default: all) to ``value``, + preserving the others; writes only the bytes that change.""" + channels = self._normalize_channels(channels) + if channels is None: + channels = sorted(order) + self._validate_channels(channels, len(order)) + if not isinstance(value, int) or not 0 <= value <= 0xF: + raise ValueError(f"nibble value must be an int 0-15, got {value!r}") + raw = self.read_block(reg, len(order) // 2) + new = bytearray(raw) + for ch in channels: + byte, shift = self._nibble_position(order.index(ch)) + new[byte] = (new[byte] & ~(0xF << shift) & 0xFF) | (value << shift) + for i, (old, updated) in enumerate(zip(raw, new)): + if old != updated: + self.write_reg(reg + i, bytes([updated])) + def _cdr_status_12ch(self, base_reg: int, channels=None) -> int: """Return a 12-bit mask (bit0=ch1 .. bit11=ch12) from a 2-byte status - register laid out like :meth:`_disable_cdr_12ch`, optionally + register laid out like :meth:`_set_cdr_12ch`, optionally restricted to ``channels``.""" channels = self._normalize_channels(channels) if channels is not None: @@ -226,7 +279,17 @@ def disable_cdr(self, channels: list = None) -> None: Channel number(s) (1-12). Default: all channels. Invalid channels raise ``ValueError`` before any UART transaction. """ - self._disable_cdr_12ch(self.Reg.CDR_ENABLE_BASE, channels) + self._set_cdr_12ch(self.Reg.CDR_ENABLE_BASE, channels, enable=False) + + def enable_cdr(self, channels: list = None) -> None: + """Enable CDR on specified channels (default: all 12); see + :meth:`disable_cdr`.""" + self._set_cdr_12ch(self.Reg.CDR_ENABLE_BASE, channels, enable=True) + + def get_cdr_enable(self, channels: list = None) -> int: + """Return the CDR enable mask: bit0=ch1 .. bit11=ch12, 1 = enabled, + optionally restricted to ``channels``.""" + return self._cdr_status_12ch(self.Reg.CDR_ENABLE_BASE, channels) def get_cdr_status(self, channels: list = None) -> int: """Get CDR Loss of Lock status. @@ -305,7 +368,17 @@ def disable_cdr(self, channels: list = None) -> None: Channel number(s) (1-12). Default: all channels. Invalid channels raise ``ValueError`` before any UART transaction. """ - self._disable_cdr_12ch(self.Reg.CDR_ENABLE_BASE, channels) + self._set_cdr_12ch(self.Reg.CDR_ENABLE_BASE, channels, enable=False) + + def enable_cdr(self, channels: list = None) -> None: + """Enable CDR on specified channels (default: all 12); see + :meth:`disable_cdr`.""" + self._set_cdr_12ch(self.Reg.CDR_ENABLE_BASE, channels, enable=True) + + def get_cdr_enable(self, channels: list = None) -> int: + """Return the CDR enable mask: bit0=ch1 .. bit11=ch12, 1 = enabled, + optionally restricted to ``channels``.""" + return self._cdr_status_12ch(self.Reg.CDR_ENABLE_BASE, channels) def get_cdr_status(self, channels: list = None) -> int: """Get CDR Loss of Lock status. @@ -323,6 +396,30 @@ def get_cdr_status(self, channels: list = None) -> int: """ return self._cdr_status_12ch(self.Reg.CDR_LOL_STATUS_BASE, channels) + # Tables 43/44: the first byte's high nibble is Rx11 (channel 12), down + # to the last byte's low nibble, Rx00 (channel 1). + NIBBLE_ORDER = list(range(12, 0, -1)) + # Raw codes: swing 80 mV + 76 mV/step, 7-15 = 612 mV (default 4 = 384 mV); + # pre-emphasis 4.8 %/step of amplitude, 7-15 = 33.6 % (default 2 = 9.6 %). + AMPLITUDE_CODES = {code: "%d mV" % (80 + 76 * min(code, 7)) for code in range(16)} + PREEMPHASIS_CODES = {code: "%.1f %%" % (4.8 * min(code, 7)) for code in range(16)} + + def get_output_amplitude(self, channels: list = None) -> dict: + """Return {channel: raw 4-bit output swing code} (Table 43).""" + return self._read_nibbles(self.Reg.RX_OUTPUT_AMP, self.NIBBLE_ORDER, channels) + + def set_output_amplitude(self, value: int, channels: list = None) -> None: + """Set the output swing code (0-15) on channels (default: all 12).""" + self._write_nibbles(self.Reg.RX_OUTPUT_AMP, self.NIBBLE_ORDER, value, channels) + + def get_output_preemphasis(self, channels: list = None) -> dict: + """Return {channel: raw 4-bit output pre-emphasis code} (Table 44).""" + return self._read_nibbles(self.Reg.RX_DEEMPHASIS, self.NIBBLE_ORDER, channels) + + def set_output_preemphasis(self, value: int, channels: list = None) -> None: + """Set the output pre-emphasis code (0-15) on channels (default: all 12).""" + self._write_nibbles(self.Reg.RX_DEEMPHASIS, self.NIBBLE_ORDER, value, channels) + def print_status(self) -> None: status = self.read_reg(self.Reg.STATUS)[0] if hasattr(self.Reg, 'STATUS') else 0 print(f"{self.location} Rx (12-ch): Status=0x{status:02X}, part_id={self.part_id}") @@ -392,6 +489,10 @@ class Reg(IntEnum): TEMP_MASK = 0x67 # Byte 103 VCC_MASK = 0x68 # Byte 104 PAGE_SELECT = 0x7F + # Upper page 03h, packed as page << 8 | address: the MCU selects the + # page and accesses the register under one I2C semaphore hold. + RX_OUTPUT_PREEMPHASIS = 0x03EC # Bytes 236-237 (Table 48) + RX_OUTPUT_AMP = 0x03EE # Bytes 238-239 (Table 49) @property def status_summary(self) -> int: @@ -437,26 +538,59 @@ def disable_cdr(self, channels: list = None, tx: bool = True, rx: bool = True) - rx : bool, optional Disable CDR on Rx side. Default: True. """ + self._set_cdr(channels, tx, rx, enable=False) + + def enable_cdr(self, channels: list = None, tx: bool = True, rx: bool = True) -> None: + """Enable CDR on specified channels; see :meth:`disable_cdr`.""" + self._set_cdr(channels, tx, rx, enable=True) + + def _set_cdr(self, channels, tx: bool, rx: bool, enable: bool) -> None: channels = self._normalize_channels(channels) if channels is None: channels = [1, 2, 3, 4] self._validate_channels(channels, 4) - # Read current CDR enable byte cdr_enable = self.read_reg(self.Reg.CDR_ENABLE)[0] - - # Clear bits for specified channels + bits = 0 for ch in channels: if tx: - # Tx channels: bits 4-7 (Tx1=bit4, Tx4=bit7) - cdr_enable &= ~(1 << (ch + 3)) + bits |= 1 << (ch + 3) # Tx1=bit4 .. Tx4=bit7 if rx: - # Rx channels: bits 0-3 (Rx1=bit0, Rx4=bit3) - cdr_enable &= ~(1 << (ch - 1)) - - # Write back + bits |= 1 << (ch - 1) # Rx1=bit0 .. Rx4=bit3 + cdr_enable = (cdr_enable | bits) if enable else (cdr_enable & ~bits) self.write_reg(self.Reg.CDR_ENABLE, bytes([cdr_enable])) + # Tables 48/49: the first byte's high nibble is Rx1, down to the second + # byte's low nibble, Rx4. + NIBBLE_ORDER = [1, 2, 3, 4] + # Codes not listed are reserved and rejected by the setters. + AMPLITUDE_CODES = {0: "300 mV", 1: "450 mV", 2: "600 mV", 3: "900 mV"} + PREEMPHASIS_CODES = {0: "0 dB", 1: "1 dB", 2: "2 dB", 3: "3 dB", 4: "4 dB", + 5: "5 dB", 6: "6 dB", 7: "7.5 dB"} + + def get_output_amplitude(self, channels: list = None) -> dict: + """Return {channel: raw 4-bit Rx output amplitude code} (Table 49).""" + return self._read_nibbles(self.Reg.RX_OUTPUT_AMP, self.NIBBLE_ORDER, channels) + + def set_output_amplitude(self, value: int, channels: list = None) -> None: + """Set the Rx output amplitude code (0-3) on channels (default: all 4). + The datasheet calls this a global parameter for the four channels, + so per-channel values may not take effect independently.""" + if value not in self.AMPLITUDE_CODES: + raise ValueError(f"x4 amplitude code must be 0-3, got {value!r}") + self._write_nibbles(self.Reg.RX_OUTPUT_AMP, self.NIBBLE_ORDER, value, channels) + + def get_output_preemphasis(self, channels: list = None) -> dict: + """Return {channel: raw 4-bit Rx output de-emphasis code} (Table 48).""" + return self._read_nibbles(self.Reg.RX_OUTPUT_PREEMPHASIS, self.NIBBLE_ORDER, channels) + + def set_output_preemphasis(self, value: int, channels: list = None) -> None: + """Set the Rx output de-emphasis code (0-7) on channels (default: all 4).""" + if value not in self.PREEMPHASIS_CODES: + raise ValueError(f"x4 pre-emphasis code must be 0-7, got {value!r}") + self._write_nibbles(self.Reg.RX_OUTPUT_PREEMPHASIS, self.NIBBLE_ORDER, value, + channels) + def get_cdr_status(self, channels: list = None) -> dict: """Get CDR enable status for channels. diff --git a/packaging/cm_interface.spec b/packaging/cm_interface.spec index 2b52ee7..6c99328 100644 --- a/packaging/cm_interface.spec +++ b/packaging/cm_interface.spec @@ -36,8 +36,8 @@ install -m 0644 tools/*.py %{buildroot}%{python3_sitelib}/cm_interface/tools/ # vccint_2phase rewrites LGA80D rail configuration; keep it out of the RPM. rm %{buildroot}%{python3_sitelib}/cm_interface/tools/vccint_2phase.py # Each tool module gets a cm- launcher (underscores become dashes). -for tool in clock_checks clock_dump exercise_mcu ff_status lga80d_checks \ - lga80d_dump lga80d_status mcu_dump mcu_set_rtc; do +for tool in clock_checks clock_dump exercise_mcu ff_cdr ff_rx_eq ff_status \ + lga80d_checks lga80d_dump lga80d_status mcu_dump mcu_set_rtc; do launcher=%{buildroot}%{_bindir}/cm-$(echo "$tool" | tr _ -) printf '#!%{__python3}\nimport sys\nfrom cm_interface.tools.%s import main\nsys.exit(main())\n' \ "$tool" > "$launcher" diff --git a/registers/firefly12.json b/registers/firefly12.json index c9f6486..276145f 100644 --- a/registers/firefly12.json +++ b/registers/firefly12.json @@ -36,8 +36,8 @@ "VCC_ALARM_LATCH": {"addr": "0x12", "size": 1, "access": "RO", "bits": {"7": "VCC High", "6": "VCC Low", "5-0": "Reserved"}}, "RX_DISABLE": {"addr": "0x34", "size": 2, "access": "RW", "description": "Per-channel RX disable (bits 0-11, 0-based). Table 40. NOT the same as RX_OUTPUT_DISABLE."}, "RX_OUTPUT_DISABLE": {"addr": "0x36", "size": 2, "access": "RW", "description": "Per-channel electrical RX output disable. Table 41. A successful write here does not by itself prove the receiver channel (RX_DISABLE) was disabled."}, - "RX_OUTPUT_AMP": {"addr": "0x3E", "size": 6, "access": "RW", "description": "Per-channel RX output amplitude, Table 43: 6 bytes packing two channel nibbles per byte (12 channels total, not 12 bytes)."}, - "RX_DEEMPHASIS": {"addr": "0x44", "size": 6, "access": "RW", "description": "Per-channel RX de-emphasis, 6 bytes packing two channel nibbles per byte."}, + "RX_OUTPUT_AMP": {"addr": "0x3E", "size": 6, "access": "RW", "description": "Per-channel RX output amplitude, Table 43: 6 bytes packing two channel nibbles per byte (12 channels total, not 12 bytes). First byte high nibble = channel 12 (Rx11), last byte low nibble = channel 1 (Rx00)."}, + "RX_DEEMPHASIS": {"addr": "0x44", "size": 6, "access": "RW", "description": "Per-channel RX output pre-emphasis, Table 44: 6 bytes packing two channel nibbles per byte, same channel order as RX_OUTPUT_AMP."}, "CDR_ENABLE": {"addr": "0x4A", "size": 2, "access": "RW", "description": "Bytes 74-75 (Tables 22/45). Low nibble of byte 74 (0x4A) = channels 8-11 (0-based); byte 75 (0x4B) = channels 0-7. High nibble of byte 74 is reserved and must be preserved on write."}, "CDR_LOL_STATUS": {"addr": "0x14", "size": 2, "access": "RO", "description": "Bytes 20-21 (Tables 17/39), same channel/nibble layout as CDR_ENABLE. 1 = loss of lock for that channel."}, "CDR_LOL_MASK": {"addr": "0x6C", "size": 2, "access": "RW", "description": "Bytes 108-109 (Tables 27/50), same channel/nibble layout as CDR_ENABLE."}, diff --git a/registers/firefly4.json b/registers/firefly4.json index aefac26..9770c14 100644 --- a/registers/firefly4.json +++ b/registers/firefly4.json @@ -55,8 +55,8 @@ "VCC3V3_ALARM_THRESH_HI": {"addr": "0x90", "size": 2, "access": "RW (see note above)", "description": "Bytes 144-145, 100 µV units, default 0x875A = 3.465V."}, "VCC3V3_ALARM_THRESH_LO": {"addr": "0x92", "size": 2, "access": "RW (see note above)", "description": "Bytes 146-147, 100 µV units, default 0x7A76 = 3.135V."}, "TX_POLARITY_INV": {"addr": "0xE2", "size": 1, "access": "RW", "description": "Byte 226, per-channel TX/RX polarity invert (packed fields), Table 46."}, - "RX_OUTPUT_PREEMPHASIS": {"addr": "0xEC", "size": 2, "access": "RW", "description": "Bytes 236-237, Table 48."}, - "RX_OUTPUT_AMP": {"addr": "0xEE", "size": 2, "access": "RW", "description": "Bytes 238-239, Table 49 (packed fields; manufacturer text notes amplitude is not fully independent per channel)."}, + "RX_OUTPUT_PREEMPHASIS": {"addr": "0xEC", "size": 2, "access": "RW", "description": "Upper page 03h, bytes 236-237, Table 48. First byte high nibble = Rx1, second byte low nibble = Rx4."}, + "RX_OUTPUT_AMP": {"addr": "0xEE", "size": 2, "access": "RW", "description": "Upper page 03h, bytes 238-239, Table 49, same nibble order as RX_OUTPUT_PREEMPHASIS (packed fields; manufacturer text notes amplitude is not fully independent per channel)."}, "SQUELCH_DISABLE": {"addr": "0xF0", "size": 1, "access": "RW", "description": "Byte 240, Table 50."}, "RX_OUTPUT_DISABLE": {"addr": "0xF1", "size": 1, "access": "RW", "description": "Byte 241, per-channel electrical RX output disable, Table 51. There is no corresponding RX channel-disable control on this variant (see lower_page_registers.RX_DISABLE)."} }, diff --git a/tests/test_ff_status.py b/tests/test_ff_status.py index 07d729f..c9dbf59 100644 --- a/tests/test_ff_status.py +++ b/tests/test_ff_status.py @@ -1,3 +1,4 @@ +import argparse import unittest from cm_interface.tools import ff_status @@ -25,5 +26,38 @@ def test_named_absent_and_unpopulated_locations_are_reported(self): self.assertEqual(to_read, [9]) +class ParseChannelsTest(unittest.TestCase): + def test_lists_and_ranges(self): + self.assertEqual(ff_status.parse_channels("5-7,1,6"), [1, 5, 6, 7]) + + def test_bad_list_is_an_argparse_error(self): + with self.assertRaises(argparse.ArgumentTypeError): + ff_status.parse_channels("1,x") + + +class FakeInfo: + hardware_revision = 3 + map_minor = 1 + ff_present_mask = (1 << 0) | (1 << 1) | (1 << 9) + ff_user_mask = (1 << 0) | (1 << 9) + + +class FakeRegistry: + uart = None + + class mcu: + system_info = FakeInfo() + + +class FindModulesTest(unittest.TestCase): + def test_user_mask_and_classes(self): + _, notes, modules = ff_status.find_modules(FakeRegistry(), ["F1_1", "F1_6"]) + self.assertEqual(notes, ["F1_1 Rx: present but not in user mask; MCU will not access it"]) + self.assertEqual([(n, kind, type(ff).__name__, ff.address, ff.location) + for n, _, kind, ff in modules], + [(0, "Tx", "FireflyTx", 0x20, "F1_1"), + (9, "4", "Firefly4", 0x29, "F1_6")]) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_firefly.py b/tests/test_firefly.py index b89154b..360bb59 100644 --- a/tests/test_firefly.py +++ b/tests/test_firefly.py @@ -24,55 +24,59 @@ def readline(self): class FireflyRxTest(unittest.TestCase): def test_disable_and_output_disable_addresses(self): - uart = FakeUART(b"c\n") + uart = FakeUART(b"c\n", b"c\n") rx = FireflyRx(uart, address=0x21, location="F1_1") rx.write_reg(rx.Reg.RX_DISABLE, b"\x00\x00") - self.assertEqual(uart.writes, [b"w FF 1 0 34 00 00\n"]) + # ProgCom FF writes take one data byte, so two transactions. + self.assertEqual(uart.writes, [b"w FF 1 0 34 00\n", b"w FF 1 0 35 00\n"]) - uart2 = FakeUART(b"c\n") + uart2 = FakeUART(b"c\n", b"c\n") rx2 = FireflyRx(uart2, address=0x21, location="F1_1") rx2.write_reg(rx2.Reg.RX_OUTPUT_DISABLE, b"\x00\x00") - self.assertEqual(uart2.writes, [b"w FF 1 0 36 00 00\n"]) + self.assertEqual(uart2.writes, [b"w FF 1 0 36 00\n", b"w FF 1 0 37 00\n"]) def test_disable_cdr_all_channels_preserves_reserved_bits(self): # hi=0xFA (reserved nibble 1111, ch9-12 bits: ch9=0,ch10=1,ch11=0,ch12=1) # lo=0xFF (ch1-8 all enabled) - uart = FakeUART(b"d FA FF\n", b"c\n") + uart = FakeUART(b"d FA FF\n", b"c\n", b"c\n") rx = FireflyRx(uart, address=0x20, location="F1_1") rx.disable_cdr() self.assertEqual(uart.writes, [ b"r FF 0 0 4A 2\n", - b"w FF 0 0 4A F0 00\n", + b"w FF 0 0 4A F0\n", + b"w FF 0 0 4B 00\n", ]) def test_disable_cdr_partial_mask(self): - uart = FakeUART(b"d FA FF\n", b"c\n") + uart = FakeUART(b"d FA FF\n", b"c\n", b"c\n") rx = FireflyRx(uart, address=0x20, location="F1_1") rx.disable_cdr(channels=[3, 10]) self.assertEqual(uart.writes, [ b"r FF 0 0 4A 2\n", - b"w FF 0 0 4A F8 FB\n", + b"w FF 0 0 4A F8\n", + b"w FF 0 0 4B FB\n", ]) def test_disable_cdr_accepts_single_int_channel(self): - uart = FakeUART(b"d F0 FF\n", b"c\n") + uart = FakeUART(b"d F0 FF\n", b"c\n", b"c\n") rx = FireflyRx(uart, address=0x20, location="F1_1") rx.disable_cdr(channels=1) self.assertEqual(uart.writes, [ b"r FF 0 0 4A 2\n", - b"w FF 0 0 4A F0 FE\n", + b"w FF 0 0 4A F0\n", + b"w FF 0 0 4B FE\n", ]) def test_disable_cdr_reaches_channels_9_through_12(self): # All 12 enabled: hi=0xFF (ch9-12 all set), lo=0xFF - uart = FakeUART(b"d FF FF\n", b"c\n") + uart = FakeUART(b"d FF FF\n", b"c\n", b"c\n") rx = FireflyRx(uart, address=0x20, location="F1_1") rx.disable_cdr(channels=[9, 10, 11, 12]) @@ -80,7 +84,8 @@ def test_disable_cdr_reaches_channels_9_through_12(self): # ch9..12 = mask bits 8..11, all cleared -> hi low nibble = 0 self.assertEqual(uart.writes, [ b"r FF 0 0 4A 2\n", - b"w FF 0 0 4A F0 FF\n", + b"w FF 0 0 4A F0\n", + b"w FF 0 0 4B FF\n", ]) def test_disable_cdr_rejects_invalid_channels_with_zero_writes(self): @@ -109,6 +114,56 @@ def test_get_cdr_status_channel_filter(self): self.assertEqual(status, (1 << 0) | (1 << 9)) + def test_enable_cdr_sets_bits_and_preserves_reserved(self): + uart = FakeUART(b"d F0 00\n", b"c\n", b"c\n") + rx = FireflyRx(uart, address=0x20, location="F1_1") + + rx.enable_cdr(channels=[1, 12]) + + self.assertEqual(uart.writes, [ + b"r FF 0 0 4A 2\n", + b"w FF 0 0 4A F8\n", + b"w FF 0 0 4B 01\n", + ]) + + def test_get_cdr_enable_reads_enable_register(self): + # ff_cdr_ena on hardware prints 0xff0f: byte 0x4A=0x0F, 0x4B=0xFF. + uart = FakeUART(b"d 0F FF\n") + rx = FireflyRx(uart, address=0x20, location="F1_1") + self.assertEqual(rx.get_cdr_enable(), 0xFFF) + self.assertEqual(uart.writes, [b"r FF 0 0 4A 2\n"]) + + def test_output_amplitude_nibble_order(self): + # Table 43: 0x3E high nibble = ch12 ... 0x43 low nibble = ch1. + uart = FakeUART(b"d C1 23 45 67\n", b"d 89 AB\n") + rx = FireflyRx(uart, address=0x20, location="F1_1") + + values = rx.get_output_amplitude() + + self.assertEqual(values[12], 0xC) + self.assertEqual(values[11], 0x1) + self.assertEqual(values[2], 0xA) + self.assertEqual(values[1], 0xB) + self.assertEqual(uart.writes, [b"r FF 0 0 3E 4\n", b"r FF 0 0 42 2\n"]) + + def test_set_output_preemphasis_writes_only_changed_bytes(self): + uart = FakeUART(b"d 22 22 22 22\n", b"d 22 22\n", b"c\n", b"c\n") + rx = FireflyRx(uart, address=0x20, location="F1_1") + + rx.set_output_preemphasis(5, channels=[1, 12]) + + self.assertEqual(uart.writes[2:], [ + b"w FF 0 0 44 52\n", + b"w FF 0 0 49 25\n", + ]) + + def test_set_output_amplitude_rejects_bad_value_before_writing(self): + uart = FakeUART() + rx = FireflyRx(uart, address=0x20, location="F1_1") + with self.assertRaises(ValueError): + rx.set_output_amplitude(16) + self.assertEqual(uart.writes, []) + def test_capability_guards_return_none(self): rx = FireflyRx(FakeUART(), address=0x20, location="F1_1") self.assertIsNone(rx.vendor_name) @@ -117,14 +172,15 @@ def test_capability_guards_return_none(self): class FireflyTxTest(unittest.TestCase): def test_disable_cdr_shares_rx_bug_fix(self): - uart = FakeUART(b"d FA FF\n", b"c\n") + uart = FakeUART(b"d FA FF\n", b"c\n", b"c\n") tx = FireflyTx(uart, address=0x20, location="F2_3") tx.disable_cdr() self.assertEqual(uart.writes, [ b"r FF 0 0 4A 2\n", - b"w FF 0 0 4A F0 00\n", + b"w FF 0 0 4A F0\n", + b"w FF 0 0 4B 00\n", ]) def test_capability_guards_return_none(self): @@ -210,6 +266,40 @@ def test_disable_cdr_channels_1_to_4_round_trip(self): b"w FF 0 0 62 6F\n", ]) + def test_enable_cdr_rx_only(self): + uart = FakeUART(b"d 00\n", b"c\n") + f = Firefly4(uart, address=0x20, location="F1_5") + + f.enable_cdr(channels=[2], tx=False, rx=True) + + self.assertEqual(uart.writes, [b"r FF 0 0 62 1\n", b"w FF 0 0 62 02\n"]) + + def test_output_amplitude_uses_upper_page_3(self): + # Table 49: 0xEE high nibble = Rx1 ... 0xEF low nibble = Rx4. The page + # goes in the ProgCom command, not a separate PAGE_SELECT write. + uart = FakeUART(b"d 12 30\n") + f = Firefly4(uart, address=0x33, location="F2_6") + + self.assertEqual(f.get_output_amplitude(), {1: 1, 2: 2, 3: 3, 4: 0}) + self.assertEqual(uart.writes, [b"r FF 13 3 EE 2\n"]) + + def test_set_output_preemphasis_upper_page_3(self): + uart = FakeUART(b"d 00 00\n", b"c\n") + f = Firefly4(uart, address=0x20, location="F1_5") + + f.set_output_preemphasis(7, channels=[3]) + + self.assertEqual(uart.writes, [b"r FF 0 3 EC 2\n", b"w FF 0 3 ED 70\n"]) + + def test_reserved_codes_rejected_before_any_traffic(self): + uart = FakeUART() + f = Firefly4(uart, address=0x20, location="F1_5") + with self.assertRaises(ValueError): + f.set_output_amplitude(4) + with self.assertRaises(ValueError): + f.set_output_preemphasis(8) + self.assertEqual(uart.writes, []) + def test_get_cdr_status_filters_and_validates(self): uart = FakeUART(b"d FF\n") f = Firefly4(uart, address=0x20, location="F1_5") diff --git a/tools/_common.py b/tools/_common.py index 50beb42..27dc117 100644 --- a/tools/_common.py +++ b/tools/_common.py @@ -9,6 +9,7 @@ import os import sys +from cm_interface.device.mcu import PowerFlags, PowerFsmState from cm_interface.registry import Registry DEFAULT_DEV_PATH = "/dev/ttyUL4" @@ -88,6 +89,44 @@ def board_info(reg): ], info.board_id +def add_force_arg(parser): + parser.add_argument("--force", action="store_true", + help="read even if the MCU power state machine is not POWER_ON") + + +def power_info(reg): + """Return (text, state): the MCU power state machine's state, with any + latched fault flags, for headers, and the PowerFsmState or None if it + can't be read (e.g. old firmware). Never prints.""" + try: + power = reg.get_mcu().read_power() + except Exception as exc: + return "unavailable (%s)" % exc, None + latched = [f.name for f in (PowerFlags.POWER_FAULT_LATCH, PowerFlags.ALARM_SHUTDOWN_LATCH) + if power.flags & f] + text = power.fsm_state.name + if latched: + text += " (latched: %s)" % ", ".join(latched) + return text, power.fsm_state + + +def power_is_on(state, force=False): + """Return False, after printing why, if state (from power_info) is not + POWER_ON and force is not set. An unreadable state (None) lets the tool run.""" + if state is None: + print("MCU power state: unknown; reading anyway") + return True + if state == PowerFsmState.POWER_ON: + return True + if force: + print("WARNING: MCU power state machine is %s, not POWER_ON; " + "reading anyway (--force)" % state.name) + return True + print("power is off: MCU power state machine is %s, not POWER_ON; " + "nothing read (--force to read anyway)" % state.name) + return False + + def _cell(row, check): if check not in row: return "-" diff --git a/tools/clock_checks.py b/tools/clock_checks.py index 26e5da0..6a99571 100755 --- a/tools/clock_checks.py +++ b/tools/clock_checks.py @@ -83,8 +83,9 @@ from cm_interface.device.si5341 import Si5341Reg from cm_interface.device.si5395 import Si5395Reg from cm_interface.errors import ClockIdentityError, CMError -from cm_interface.tools._common import (add_common_args, add_device_args, make_registry, - print_summary, selected_devices) +from cm_interface.tools._common import (add_common_args, add_device_args, add_force_arg, + make_registry, power_info, power_is_on, print_summary, + selected_devices) EXPECTED_ERRORS = (CMError, OSError, TimeoutError, ValueError, RuntimeError) @@ -492,12 +493,17 @@ def main(): parser.add_argument("--hw-rev", type=int, metavar="N", help="board revision to assume instead of the MCU's " "HARDWARE_REVISION (e.g. EEPROM not programmed)") + add_force_arg(parser) add_common_args(parser) args = parser.parse_args() checks = [c for c in CHECKS if c in (args.check or CHECKS)] names = selected_devices(parser, args, list(CORE_CONFIG.clocks), default_all=True) reg = make_registry(args, hw_rev=args.hw_rev) + power_text, power_state = power_info(reg) + print("MCU power state: %s" % power_text) + if not power_is_on(power_state, args.force): + return 1 try: clocks = reg.clocks except EXPECTED_ERRORS as exc: diff --git a/tools/clock_dump.py b/tools/clock_dump.py index bbf6486..b1689b0 100755 --- a/tools/clock_dump.py +++ b/tools/clock_dump.py @@ -49,9 +49,9 @@ from cm_interface.core_config import CORE_CONFIG from cm_interface.device.clock import DEVICE_READY_VALUE -from cm_interface.tools._common import (add_common_args, add_device_args, add_out_dir_arg, - board_info, make_registry, out_path, - selected_devices, write_lines) +from cm_interface.tools._common import (add_common_args, add_device_args, add_force_arg, + add_out_dir_arg, board_info, make_registry, out_path, + power_info, power_is_on, selected_devices, write_lines) MAX_READ_BYTES = 4 @@ -503,12 +503,17 @@ def main(): parser.add_argument("--hw-rev", type=int, metavar="N", help="board revision to assume instead of the MCU's " "HARDWARE_REVISION (e.g. EEPROM not programmed)") + add_force_arg(parser) add_common_args(parser) args = parser.parse_args() names = selected_devices(parser, args, list(CORE_CONFIG.clocks)) reg = make_registry(args, hw_rev=args.hw_rev) + power_text, power_state = power_info(reg) + if not power_is_on(power_state, args.force): + return 1 board_lines, board_id = board_info(reg) + board_lines = board_lines + [" power: %s" % power_text] if args.hw_rev is not None: board_lines = board_lines + [" clocks for hw rev %d (--hw-rev)" % args.hw_rev] try: diff --git a/tools/examples.py b/tools/examples.py index 0fc29d2..b05a5ee 100755 --- a/tools/examples.py +++ b/tools/examples.py @@ -4,8 +4,15 @@ This module demonstrates common operations for managing Si5395 clocks, Firefly optical transceivers, and LGA80D DC-DC converters. + +Running it runs every example against the board, with the TF Firefly preset. +NOT READ ONLY: the first two examples disable CDR on Fireflies. + +Usage: + python -m cm_interface.tools.examples [--dev-path DEV] [--debug] """ +import argparse import os import sys @@ -20,6 +27,7 @@ from cm_interface.device.si5395 import Si5395Reg from cm_interface.device.firefly import Firefly12, Firefly4, FireflyTx, FireflyRx from cm_interface.errors import CMError +from cm_interface.tools._common import add_common_args EXPECTED_HARDWARE_ERRORS = (CMError, OSError, TimeoutError, ValueError) @@ -69,7 +77,7 @@ def example_disable_cdr_all_fireflies(): print("=" * 70) # Initialize registry with TF (Track Finder) configuration - reg = Registry(setup='tf', dev_path='/dev/ttyUSB2') + reg = Registry(setup='tf', dev_path='/dev/ttyUL4') # Iterate over all firefly devices for loc, device in reg.fireflies.items(): @@ -104,7 +112,7 @@ def example_selective_cdr_control(): print("Example 2: Selective CDR Control") print("=" * 70) - reg = Registry(setup='tf', dev_path='/dev/ttyUSB2') + reg = Registry(setup='tf', dev_path='/dev/ttyUL4') # Disable CDR only on channels 1-4 of F1_5 (4-channel XCVR) f1_5 = _get_configured_firefly(reg, 'F1_5') @@ -151,7 +159,7 @@ def example_cdr_status_check(): print("Example 3: CDR Status Check") print("=" * 70) - reg = Registry(setup='tf', dev_path='/dev/ttyUSB2') + reg = Registry(setup='tf', dev_path='/dev/ttyUL4') # Check status for F2_5 (4-channel XCVR) f2_5 = _get_configured_firefly(reg, 'F2_5') @@ -185,7 +193,7 @@ def example_clock_monitoring(): print("Example 4: Clock Status Monitoring") print("=" * 70) - reg = Registry(setup='tf', dev_path='/dev/ttyUSB2') + reg = Registry(setup='tf', dev_path='/dev/ttyUL4') # Iterate over all clocks for name, clock in reg.clocks.items(): @@ -227,7 +235,7 @@ def example_lga80d_monitoring(): print("Example 5: LGA80D Voltage Monitoring") print("=" * 70) - reg = Registry(setup='tf', dev_path='/dev/ttyUSB2') + reg = Registry(setup='tf', dev_path='/dev/ttyUL4') # Iterate over all LGA80D devices for name, lga in reg.lga80d.items(): @@ -275,7 +283,7 @@ def example_system_status_report(): print("Example 6: Complete System Status Report") print("=" * 70) - reg = Registry(setup='tf', dev_path='/dev/ttyUSB2') + reg = Registry(setup='tf', dev_path='/dev/ttyUL4') print(f"\nRegistry initialized with TF configuration") print(f" Clocks: {len(reg.clocks)}") @@ -315,7 +323,7 @@ def example_firefly_identification(): print("Example 7: Firefly Device Identification") print("=" * 70) - reg = Registry(setup='tf', dev_path='/dev/ttyUSB2') + reg = Registry(setup='tf', dev_path='/dev/ttyUL4') # Group devices by type xcvr_4ch = [] @@ -366,7 +374,7 @@ def example_register_access_patterns(): print("Example 8: Register Access Patterns") print("=" * 70) - reg = Registry(setup='tf', dev_path='/dev/ttyUSB2') + reg = Registry(setup='tf', dev_path='/dev/ttyUL4') clock = reg.get_clock('R0B') # Si5395 on every board revision print("\nThree ways to read the LOL status register:") @@ -395,6 +403,15 @@ def example_register_access_patterns(): def main(): """Run every example, continuing past expected hardware failures.""" + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + add_common_args(parser) + args = parser.parse_args() + # Registry is a singleton: this first creation fixes the UART for every + # example, whatever dev_path the examples themselves pass. + Registry(setup='tf', dev_path=args.dev_path, + debug=sys.stdout if args.debug else None) + print("\n") print("╔" + "=" * 68 + "╗") print("║" + " " * 15 + "CM Interface Worked Examples" + " " * 25 + "║") diff --git a/tools/ff_cdr.py b/tools/ff_cdr.py new file mode 100755 index 0000000..612b78e --- /dev/null +++ b/tools/ff_cdr.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 +"""Report, enable or disable Firefly CDR (clock and data recovery) per channel. + +Works on the populated Fireflies at the named locations (or --all), found the +same way as cm-ff-status: MCU present and user masks plus the board revision's +slot table. A location selects both halves of an x12 pair; --side limits it. + + * 25G x12 Tx/Rx: bytes 74-75 (Tables 22/45), channels 1-12. + * x4: byte 98, Tx bits 7-4 and Rx bits 3-0, channels 1-4. + * CERN-B and 14G x12 have no CDR control here (the MCU does not read CDR + on them either) and are skipped. + +Without --enable/--disable the enable state is only read. With either, each +module is read, changed and read back; the read-back is what is printed. + +Usage: + cm-ff-cdr --all + cm-ff-cdr F1_1 --side rx --disable --channels 3,10 + cm-ff-cdr F2_6 --enable +""" +import argparse +import os +import sys + +# Permit direct execution from a checkout (./tools/NAME.py) without the RPM. +if not __package__: + _package_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + _package_parent = os.path.dirname(_package_dir) + if _package_parent not in sys.path: + sys.path.insert(0, _package_parent) + +from cm_interface.tools._common import (add_common_args, add_device_args, add_force_arg, + make_registry, power_info, power_is_on, + selected_devices) +from cm_interface.tools.ff_status import LOCATIONS, family, find_modules, parse_channels + + +def channel_list(channels): + return ",".join(str(c) for c in channels) or "none" + + +def cdr_line(name, side, enabled, nchan): + disabled = [ch for ch in range(1, nchan + 1) if ch not in enabled] + state = "all enabled" if not disabled else "disabled ch %s" % channel_list(disabled) + return "%-8s %-3s CDR %s" % (name.split()[0], side, state) + + +def handle(ff, name, kind, sides, args): + """Apply the requested change, then return the report lines.""" + if kind == "4": + tx, rx = "tx" in sides, "rx" in sides + if args.enable: + ff.enable_cdr(args.channels, tx=tx, rx=rx) + elif args.disable: + ff.disable_cdr(args.channels, tx=tx, rx=rx) + status = ff.get_cdr_status() + return [cdr_line(name, side.capitalize(), + [ch for ch in range(1, 5) if status[side] & (1 << (ch - 1))], 4) + for side in ("tx", "rx") if side in sides] + if args.enable: + ff.enable_cdr(args.channels) + elif args.disable: + ff.disable_cdr(args.channels) + mask = ff.get_cdr_enable() + return [cdr_line(name, kind, [ch for ch in range(1, 13) if mask & (1 << (ch - 1))], 12)] + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + add_device_args(parser, "Firefly location", LOCATIONS) + parser.add_argument("--side", choices=("tx", "rx"), + help="only this side (default: both)") + parser.add_argument("--channels", type=parse_channels, metavar="LIST", + help="channels to change, e.g. 1,3,5-8 (default: all)") + action = parser.add_mutually_exclusive_group() + action.add_argument("--enable", action="store_true", help="enable CDR") + action.add_argument("--disable", action="store_true", help="disable CDR") + add_force_arg(parser) + add_common_args(parser) + args = parser.parse_args() + wanted = selected_devices(parser, args, LOCATIONS) + if args.all: + wanted = None + if args.channels and not (args.enable or args.disable): + parser.error("--channels needs --enable or --disable") + sides = (args.side,) if args.side else ("tx", "rx") + + reg = make_registry(args) + _, power_state = power_info(reg) + if not power_is_on(power_state, args.force): + return 1 + try: + _, notes, modules = find_modules(reg, wanted) + except ValueError as exc: + print(exc) + return 1 + for note in notes: + print(note) + + failed = False + for _, name, kind, ff in modules: + if kind != "4" and kind.lower() not in sides: + continue + try: + if kind != "4": + fam = family(ff.part_id) + if fam != "25g": + print("%-8s %-3s skipped: %s part has no CDR control" % ( + name.split()[0], kind, fam)) + continue + for line in handle(ff, name, kind, sides, args): + print(line) + except Exception as exc: # keep going so one bad module doesn't hide the rest + failed = True + print("%-8s %-3s FAILED: %s" % (name.split()[0], "x4" if kind == "4" else kind, + exc)) + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/ff_rx_eq.py b/tools/ff_rx_eq.py new file mode 100755 index 0000000..a0e39f3 --- /dev/null +++ b/tools/ff_rx_eq.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +"""Report or set Firefly Rx output amplitude and pre-emphasis per channel. + +Works on the populated Rx-capable Fireflies at the named locations (or --all), +found the same way as cm-ff-status. Values are the raw 4-bit codes; the +report shows what each code means. + + * 25G x12 Rx: swing bytes 62-67 (Table 43) and pre-emphasis bytes 68-73 + (Table 44). Swing is 80 mV + 76 mV per step, codes 7-15 = 612 mV + (default 4 = 384 mV); pre-emphasis is 4.8 % of amplitude per step, codes + 7-15 = 33.6 % (default 2 = 9.6 %). The first byte's high nibble is + channel 12, the last byte's low nibble channel 1. + * x4: upper page 03h bytes 238-239 amplitude (Table 49, codes 0-3 = + 300/450/600/900 mV) and 236-237 de-emphasis (Table 48, codes 0-7 = + 0-6 dB, 7.5 dB). The datasheet calls the x4 amplitude a global parameter, + so per-channel amplitude settings may not take effect independently. + * CERN-B and 14G x12 are skipped (not checked against their manuals). + +With --amplitude/--preemphasis, each module is read, changed and read back; +the read-back is what is printed. + +Usage: + cm-ff-rx-eq --all + cm-ff-rx-eq F1_1 --amplitude 5 --channels 1-4 + cm-ff-rx-eq F2_6 --preemphasis 3 +""" +import argparse +import os +import sys + +# Permit direct execution from a checkout (./tools/NAME.py) without the RPM. +if not __package__: + _package_dir = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + _package_parent = os.path.dirname(_package_dir) + if _package_parent not in sys.path: + sys.path.insert(0, _package_parent) + +from cm_interface.tools._common import (add_common_args, add_device_args, add_force_arg, + make_registry, power_info, power_is_on, + selected_devices) +from cm_interface.tools.ff_status import LOCATIONS, family, find_modules, parse_channels + + +def int_code(text): + try: + return int(text, 0) + except ValueError: + raise argparse.ArgumentTypeError("bad code %r" % text) + + +def handle(ff, args): + """Apply the requested changes, then return the report lines.""" + if args.amplitude is not None: + ff.set_output_amplitude(args.amplitude, args.channels) + if args.preemphasis is not None: + ff.set_output_preemphasis(args.preemphasis, args.channels) + amp = ff.get_output_amplitude() + pre = ff.get_output_preemphasis() + lines = [" %3s %-14s %s" % ("ch", "amplitude", "pre-emphasis")] + for ch in sorted(amp): + lines.append(" %3d 0x%X %-10s 0x%X %s" % ( + ch, amp[ch], ff.AMPLITUDE_CODES.get(amp[ch], "reserved"), + pre[ch], ff.PREEMPHASIS_CODES.get(pre[ch], "reserved"))) + return lines + + +def main(): + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + add_device_args(parser, "Firefly location", LOCATIONS) + parser.add_argument("--amplitude", type=int_code, metavar="CODE", + help="set the output amplitude code (x12: 0-15, x4: 0-3)") + parser.add_argument("--preemphasis", type=int_code, metavar="CODE", + help="set the output pre-emphasis code (x12: 0-15, x4: 0-7)") + parser.add_argument("--channels", type=parse_channels, metavar="LIST", + help="channels to change, e.g. 1,3,5-8 (default: all)") + add_force_arg(parser) + add_common_args(parser) + args = parser.parse_args() + wanted = selected_devices(parser, args, LOCATIONS) + if args.all: + wanted = None + if args.channels and args.amplitude is None and args.preemphasis is None: + parser.error("--channels needs --amplitude or --preemphasis") + + reg = make_registry(args) + _, power_state = power_info(reg) + if not power_is_on(power_state, args.force): + return 1 + try: + _, notes, modules = find_modules(reg, wanted) + except ValueError as exc: + print(exc) + return 1 + for note in notes: + print(note) + + failed = False + for _, name, kind, ff in modules: + if kind == "Tx": + continue + label = "%-8s %s" % (name.split()[0], "x4" if kind == "4" else "Rx") + try: + if kind == "Rx": + fam = family(ff.part_id) + if fam != "25g": + print("%s skipped: %s part not supported" % (label, fam)) + continue + lines = handle(ff, args) + except Exception as exc: # keep going so one bad module doesn't hide the rest + failed = True + print("%s FAILED: %s" % (label, exc)) + continue + print(label) + print("\n".join(lines)) + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/ff_status.py b/tools/ff_status.py index c0ddc35..339e787 100755 --- a/tools/ff_status.py +++ b/tools/ff_status.py @@ -32,9 +32,9 @@ sys.path.insert(0, _package_parent) from cm_interface.device.firefly import Firefly4, FireflyReg, FireflyRx, FireflyTx -from cm_interface.tools._common import (add_common_args, add_device_args, add_out_dir_arg, - make_registry, out_path, selected_devices, - write_lines) +from cm_interface.tools._common import (add_common_args, add_device_args, add_force_arg, + add_out_dir_arg, make_registry, out_path, power_info, + power_is_on, selected_devices, write_lines) # Firmware ff_moni2c_addrs[] (cm_mcu LocalTasks.c) by board revision; the # device number is the index. Kind: "Tx"/"Rx" = x12 half, "4" = x4 XCVR. @@ -80,6 +80,45 @@ def select_slots(slots, populated, wanted, hw_rev): return notes, [n for n in populated if location(slots[n][0]) in wanted] +def find_modules(reg, wanted): + """Return (system info, note lines, [(slot number, slot name, kind, device)]) + for the populated modules at the wanted locations (None = all of this + board revision's slots). Modules not in the MCU user mask get a note + instead of a device. Raises ValueError if the MCU can't say what is + fitted.""" + info = reg.mcu.system_info + if info.ff_present_mask is None: + raise ValueError("MCU map minor %d has no Firefly masks (needs >= 1)" + % info.map_minor) + slots = SLOTS.get(info.hardware_revision) + if slots is None: + raise ValueError("no Firefly slot table for board revision %d" + % info.hardware_revision) + populated = [n for n in range(len(slots)) if info.ff_present_mask & (1 << n)] + notes, to_read = select_slots(slots, populated, wanted, info.hardware_revision) + modules = [] + for n in to_read: + name, kind = slots[n] + if not info.ff_user_mask & (1 << n): + notes.append("%s: present but not in user mask; MCU will not access it" % name) + continue + modules.append((n, name, kind, CLASSES[kind](reg.uart, address=0x20 + n, + location=location(name)))) + return info, notes, modules + + +def parse_channels(text): + """'1,3,5-8' -> [1, 3, 5, 6, 7, 8]; for argparse type=.""" + channels = [] + try: + for part in text.split(","): + first, _, last = part.partition("-") + channels += range(int(first), int(last or first) + 1) + except ValueError: + raise argparse.ArgumentTypeError("bad channel list %r (e.g. 1,3,5-8)" % text) + return sorted(set(channels)) + + # x12 status summary byte 6 (Tables 12/34; CERN-B Tables 24/39). Bit 0 # (init complete) clears on read and is not an alarm, so it is not listed. SUMMARY_BITS = { @@ -205,6 +244,7 @@ def main(): formatter_class=argparse.RawDescriptionHelpFormatter) add_device_args(parser, "Firefly location", LOCATIONS) add_out_dir_arg(parser, "cm_ff_status.txt") + add_force_arg(parser) add_common_args(parser) args = parser.parse_args() wanted = selected_devices(parser, args, LOCATIONS) @@ -212,6 +252,9 @@ def main(): wanted = None # this board revision's slots, not every revision's reg = make_registry(args) + power_text, power_state = power_info(reg) + if not power_is_on(power_state, args.force): + return 1 info = reg.mcu.system_info if info.ff_present_mask is None: print("MCU map minor %d has no Firefly masks (needs >= 1)" % info.map_minor) @@ -227,6 +270,7 @@ def main(): "Firefly status %s" % time.strftime("%Y-%m-%d %H:%M:%S"), "board %d rev %d fw %s" % (info.board_id, info.hardware_revision, info.git_version), + "power %s" % power_text, "present mask 0x%05X user mask 0x%05X populated %d of %d" % (present, user, len(populated), len(slots)), ] diff --git a/tools/lga80d_checks.py b/tools/lga80d_checks.py index 130045e..21bcd09 100755 --- a/tools/lga80d_checks.py +++ b/tools/lga80d_checks.py @@ -48,7 +48,7 @@ from cm_interface.device.mcu import PowerFsmState from cm_interface.errors import CMError from cm_interface.tools._common import (add_common_args, add_device_args, make_registry, - print_summary, selected_devices) + power_info, print_summary, selected_devices) from cm_interface.utils import decode_linear11, decode_linear16u EXPECTED_ERRORS = (CMError, OSError, TimeoutError, ValueError, RuntimeError) @@ -152,16 +152,6 @@ def _onoff(enabled): return "/".join("on" if e else "off" for e in enabled) -def read_power_state(reg): - try: - state = reg.get_mcu().read_power().fsm_state - print("MCU power state: %s" % state.name) - return state - except EXPECTED_ERRORS as exc: - print("MCU power state: unknown (%s)" % _format_error(exc)) - return None - - def check_freq(reg, names, power_state): """FREQUENCY_SWITCH / READ_FREQUENCY / VOUT_DROOP / ramp gain vs LGA80D_init. Returns name -> True if read, False if a read failed (the notes don't fail).""" @@ -361,7 +351,8 @@ def main(): names = selected_devices(parser, args, list(CORE_CONFIG.lga80d), default_all=True) reg = make_registry(args) - power_state = read_power_state(reg) + power_text, power_state = power_info(reg) + print("MCU power state: %s" % power_text) results = {name: {} for name in names} for check in checks: if check == "freq": diff --git a/tools/lga80d_dump.py b/tools/lga80d_dump.py index c18785d..2de1ca5 100755 --- a/tools/lga80d_dump.py +++ b/tools/lga80d_dump.py @@ -39,7 +39,7 @@ from cm_interface.core_config import CORE_CONFIG from cm_interface.tools._common import (add_common_args, add_device_args, add_out_dir_arg, - board_info, make_registry, out_path, + board_info, make_registry, out_path, power_info, selected_devices, write_lines) from cm_interface.utils import decode_linear11, decode_linear16u @@ -244,6 +244,7 @@ def main(): reg = make_registry(args) board_lines, board_id = board_info(reg) + board_lines = board_lines + [" power: %s" % power_info(reg)[0]] registers = load_registers(args.include_unconfirmed) pages = (0, 1) if args.page is None else (args.page,) if args.out_dir: