Skip to content

DracoWhitefire/plumbob

Repository files navigation

plumbob

CI crates.io docs.rs License: MPL-2.0 Rust 1.85+

FRL link training state machine for HDMI 2.1.

plumbob implements the Fixed Rate Link (FRL) training procedure defined in the HDMI 2.1 specification. It owns the four-phase state machine — configuration, readiness polling, initiation polling, and the LTP pattern loop — and defines the ScdcClient interface its SCDC implementation must satisfy. Callers supply an ScdcClient and an HdmiPhy, call train_at_rate for each rate to attempt, and handle the TrainingOutcome.

Rate fallback policy, SCDC register decoding, and PHY vendor sequences are all out of scope: plumbob implements the spec, not the strategy around it.

Usage

[dependencies]
plumbob = "0.1"

Implement ScdcClient against your SCDC transport:

use plumbob::{ScdcClient, CedCounters, FrlConfig, TrainingStatus};

struct MyScdcClient { /* I²C / DDC transport */ }

impl ScdcClient for MyScdcClient {
    type Error = MyError;

    fn write_frl_config(&mut self, config: FrlConfig) -> Result<(), MyError> {
        // write rate and FFE level count to SCDC Config_0
        todo!()
    }

    fn read_training_status(&mut self) -> Result<TrainingStatus, MyError> {
        // read flt_ready, frl_start, ltp_req from SCDC Status_Flags
        todo!()
    }

    fn read_ced(&mut self) -> Result<CedCounters, MyError> {
        // read per-lane character error counts from SCDC
        todo!()
    }
}

Construct an FrlTrainer and step down through rates until one succeeds:

use display_types::cea861::hdmi_forum::HdmiForumFrl;
use plumbob::{FrlTrainer, TrainingConfig, TrainingOutcome};

let mut trainer = FrlTrainer::new(scdc, phy);
let config = TrainingConfig::default();

let rates = [
    HdmiForumFrl::Rate12Gbps4Lanes,
    HdmiForumFrl::Rate10Gbps4Lanes,
    HdmiForumFrl::Rate6Gbps4Lanes,
    HdmiForumFrl::Rate3Gbps4Lanes,
];

for rate in rates {
    match trainer.train_at_rate(rate, &config)? {
        TrainingOutcome::Success { achieved_rate } => {
            println!("Trained at {achieved_rate:?}");
            break;
        }
        TrainingOutcome::FallbackRequired => continue,
    }
}

For a complete worked example with simulated SCDC and PHY backends, see examples/simulate.

Training procedure

train_at_rate runs all four phases for a single rate attempt and returns a typed terminal result. FallbackRequired means the link did not converge at this rate; TrainingError means a hard I/O failure from the SCDC client or PHY. The two are kept distinct so a caller diagnosing a failure knows whether it came from the protocol or the bus.

flowchart TD
    A["train_at_rate(rate, config)"]
    B["Phase 1\nwrite Config_0 · set_frl_rate"]
    C["Phase 2\npoll flt_ready"]
    D["Phase 3\npoll frl_start"]
    E["Phase 4\nLTP loop · drive patterns"]
    F(["Success { achieved_rate }"])
    G(["FallbackRequired"])
    H(["TrainingError"])

    A --> B --> C
    C -- asserted --> D
    C -- timeout --> G
    D -- asserted --> E
    D -- timeout --> G
    E -- "ltp_req == None" --> F
    E -- timeout --> G
    B & C & D & E -- I/O error --> H
Loading

Phase 1 writes the target rate and FFE level count to Config_0 and configures the PHY lanes for this rate.

Phase 2 polls until the sink asserts flt_ready, signalling it has completed internal preparation at the requested rate. Times out after TrainingConfig::flt_ready_timeout iterations.

Phase 3 polls until the sink asserts frl_start, signalling it is ready for the LTP loop to begin. Times out after TrainingConfig::frl_start_timeout iterations.

Phase 4 drives the sink-requested LTP pattern on the PHY lanes on each iteration until ltp_req reaches None (all lanes satisfied), or until TrainingConfig::ltp_timeout iterations have elapsed.

Each timeout value is an exact iteration count: a value of N means at most N poll attempts are made before the phase gives up. The inter-poll delay is the implementer's responsibility and belongs inside the ScdcClient methods.

Diagnostics

Enable the alloc feature to get train_at_rate_traced, which returns a TrainingTrace alongside the outcome. The trace records the rate, the TrainingConfig in force, and an ordered TrainingEvent log covering the full attempt:

let (outcome, trace) = trainer.train_at_rate_traced(rate, &config)?;

println!("Outcome: {outcome:?}");
for event in &trace.events {
    println!("  {event:?}");
}

A complete successful trace looks like:

RateConfigured { rate: Rate9Gbps3Lanes, ffe_levels: Ffe0 }
FltReadyReceived { after_iterations: 5 }
FrlStartReceived { after_iterations: 12 }
LtpPatternRequested { pattern: Lfsr0 }
LtpPatternRequested { pattern: Lfsr2 }
AllLanesSatisfied { after_iterations: 47 }

A trace that timed out in phase 4 — the sink requested patterns but lanes failed to lock:

RateConfigured { rate: Rate12Gbps4Lanes, ffe_levels: Ffe0 }
FltReadyReceived { after_iterations: 4 }
FrlStartReceived { after_iterations: 3 }
LtpPatternRequested { pattern: Lfsr1 }
LtpPatternRequested { pattern: Lfsr3 }
LtpLoopTimeout { iterations_elapsed: 1000 }

LtpPatternRequested is emitted only on transitions, so a sink that holds the same pattern for many iterations produces one event, not one per poll. Each timeout event names the phase it came from — a phase 2 timeout means the sink did not complete internal preparation at this rate; a phase 4 timeout points toward signal integrity or equalization.

Features

Feature Default Description
std no Implies alloc; no additional API surface
alloc no Enables TrainingTrace, TrainingEvent, and train_at_rate_traced

No features are enabled by default. The bare crate provides the full four-phase state machine without an allocator.

no_std builds

plumbob declares #![no_std] throughout.

Bare no_std (no features) — the complete training state machine is available. FrlTrainer, TrainingConfig, TrainingOutcome, TrainingError, and all owned protocol types (LtpReq, FfeLevels, TrainingStatus, CedCounters) are stack-allocated. No heap use anywhere in the training loop. This tier covers bare-metal and firmware targets.

no_std + alloc — adds TrainingTrace and train_at_rate_traced:

plumbob = { version = "0.1", features = ["alloc"] }

std — implies alloc:

plumbob = { version = "0.1", features = ["std"] }

Stack position

plumbob sits between the SCDC/PHY implementations and the integration layer that orchestrates rate selection and fallback. It defines one interface (ScdcClient) and implements one (LinkTrainer) from the layer above.

flowchart LR
    dt["display-types"]
    hal["hdmi-hal"]
    culvert["culvert"]
    plumbob["plumbob"]
    integration["integration layer"]

    dt --> plumbob
    hal --> plumbob
    culvert -->|"implements ScdcClient"| plumbob
    plumbob -->|"implements LinkTrainer"| integration
Loading

plumbob does not depend on culvert. The relationship runs the other way: culvert implements plumbob::ScdcClient for Scdc<T>, gated behind a plumbob cargo feature. Any crate that implements ScdcClient is substitutable.

Out of scope

  • Rate fallback policytrain_at_rate returns FallbackRequired; the caller decides the retry sequence and the fallback-to-TMDS threshold.
  • SCDC register decoding — plumbob reads typed values from ScdcClient and does not decode raw register bytes or know SCDC register addresses.
  • PHY vendor sequences — plumbob calls HdmiPhy::set_frl_rate and send_ltp; the register sequences for lane reconfiguration live in platform PHY backends.
  • Timing — plumbob is synchronous and poll-based. Inter-poll delay is implicit in the transport; the iteration limits in TrainingConfig are the only timeout mechanism.
  • TMDS link setup — plumbob handles FRL training only.

Documentation

  • doc/architecture.md — role, scope, interface boundaries, design principles, and the async roadmap

License

Licensed under the Mozilla Public License 2.0.

About

A Rust library that implements the HDMI 2.1 Fixed Rate Link (FRL) training procedure

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages