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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 143 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -73,7 +75,147 @@ register JSON maps. Install a downloaded binary RPM with
The RPM also installs the command-line tools as `cm-<name>` (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-<name>` 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<boardid>_<name>.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
Expand Down
164 changes: 149 additions & 15 deletions device/firefly.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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``
Expand All @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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}")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.

Expand Down
Loading
Loading