plumbob 0.3.1

FRL link training state machine for HDMI 2.1
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
//! The link training state machine, independent of how its I/O is performed.
//!
//! [`run`] is the whole procedure — LTS:2 → LTS:3 → LTS:P, with LTS:4 and LTS:L — written
//! once as a function returning a future, over [`TrainingIo`]. It performs no I/O itself: every SCDC and PHY
//! operation goes through the `TrainingIo` it is given.
//!
//! [`FrlTrainer`](crate::FrlTrainer) drives it synchronously over an
//! [`ScdcClient`](crate::ScdcClient) and an `HdmiPhy`, without an async runtime: its I/O
//! completes immediately, so `run` never waits. `plumbob-async` drives the same `run` over
//! async I/O. Most users want `FrlTrainer`; this module is for writing such a driver.

use display_types::cea861::hdmi_forum::HdmiForumFrl;
use hdmi_hal::phy::{
    EqParams, FrlOutput, LaneEqParams, LanePatterns, LtpPattern, TxFfeLevel, TxFfeRestrictions,
};

use crate::trace::TrainingEvent;
use crate::training::{
    ExitError, FallbackReason, TmdsExit, TrainingConfig, TrainingError, TrainingOutcome,
};
use crate::types::{
    ClearableUpdateFlags, FfeLevels, FrlConfig, LtpReq, LtpRequests, SourceTestConfig, UpdateFlags,
};
use crate::warning::Trained;

/// The SCDC and PHY operations the link training state machine performs.
///
/// The SCDC methods correspond to [`ScdcClient`](crate::ScdcClient)'s (without
/// `read_ced`, which training does not use) and the PHY methods to `HdmiPhy`'s; see those
/// traits for what each one does and what implementations must guarantee.
// `async fn` in a public trait does not promise `Send` futures. That is intended: the
// state machine targets single-threaded executors and the sync driver, as in
// `hdmi-hal-async`.
#[allow(async_fn_in_trait)]
pub trait TrainingIo {
    /// Error type of the SCDC operations.
    type ScdcError;
    /// Error type of the PHY operations.
    type PhyError;

    /// Read `FLT_ready`.
    async fn read_flt_ready(&mut self) -> Result<bool, Self::ScdcError>;
    /// Read the `Update_0` training flags.
    async fn read_update_flags(&mut self) -> Result<UpdateFlags, Self::ScdcError>;
    /// Clear the given `Update_0` flags.
    async fn clear_update_flags(
        &mut self,
        flags: ClearableUpdateFlags,
    ) -> Result<(), Self::ScdcError>;
    /// Read the per-lane link training requests.
    async fn read_ltp_requests(&mut self) -> Result<LtpRequests, Self::ScdcError>;
    /// Read `Source_Test_Configuration`.
    async fn read_source_test_config(&mut self) -> Result<SourceTestConfig, Self::ScdcError>;
    /// Write `Config_0` with read requests disabled and `FLT_no_retrain` clear.
    async fn write_config_0_defaults(&mut self) -> Result<(), Self::ScdcError>;
    /// Write `Config_1`.
    async fn write_frl_config(&mut self, config: FrlConfig) -> Result<(), Self::ScdcError>;
    /// Select the FRL rate (or TMDS) on the PHY.
    async fn set_frl_rate(&mut self, rate: HdmiForumFrl) -> Result<(), Self::PhyError>;
    /// Drive the given link training patterns, one per lane.
    async fn send_ltp(&mut self, patterns: LanePatterns) -> Result<(), Self::PhyError>;
    /// Select what the transmitter sends on the FRL lanes.
    async fn set_frl_output(&mut self, output: FrlOutput) -> Result<(), Self::PhyError>;
    /// Apply per-lane equalization (TxFFE levels).
    async fn adjust_equalization(&mut self, params: EqParams) -> Result<(), Self::PhyError>;
}

/// Runs one training attempt over `rates`, in order, through `io`.
///
/// This is the procedure behind [`FrlTrainer::train`](crate::FrlTrainer::train): the same
/// outcomes, errors and events, with `record` called for each [`TrainingEvent`] as it
/// occurs. A list the procedure cannot run returns an error without any I/O: an empty one
/// [`TrainingError::NoRates`], one with `NotSupported` or a rate not strictly lower than
/// the one before it [`TrainingError::InvalidRates`].
///
/// The outcome comes with the [`TrainingWarning`](crate::TrainingWarning)s the attempt
/// produced, built from the same events `record` receives.
///
/// On an SCDC or PHY error, `run` performs LTS:L before returning, unless
/// [`TrainingConfig::exit_to_tmds_on_error`] is off; the returned [`TrainingError`] says
/// whether both ends reached TMDS.
// A plain `fn` returning the machine's future, rather than an `async fn` awaiting it:
// one async layer fewer in the future the caller holds.
pub fn run<Io: TrainingIo, F: FnMut(TrainingEvent)>(
    io: &mut Io,
    rates: &[HdmiForumFrl],
    config: &TrainingConfig,
    record: &mut F,
) -> impl Future<Output = Result<Trained, Error<Io>>> {
    Machine { io }.run(rates, config, record)
}

type Error<Io> = TrainingError<<Io as TrainingIo>::ScdcError, <Io as TrainingIo>::PhyError>;

/// An SCDC or PHY error, before LTS:L has run.
enum Fault<ScdcErr, PhyErr> {
    Scdc(ScdcErr),
    Phy(PhyErr),
}

type Failure<Io> = Fault<<Io as TrainingIo>::ScdcError, <Io as TrainingIo>::PhyError>;

/// Performs LTS:L through `io`: stops the training patterns, returns the PHY to TMDS,
/// turns FRL off in `Config_1` and clears `FLT_update` if it is set.
///
/// This is [`FrlTrainer::exit_to_tmds`](crate::FrlTrainer::exit_to_tmds), with `record`
/// called with `ExitedToTmds` or `ExitToTmdsFailed`. Every step is attempted even when
/// one fails; on failure, the error has each end's first error.
pub async fn exit_to_tmds<Io: TrainingIo, F: FnMut(TrainingEvent)>(
    io: &mut Io,
    record: &mut F,
) -> Result<(), ExitError<Io::ScdcError, Io::PhyError>> {
    Machine { io }.exit_to_tmds(record).await
}

// The flags plumbob clears, one at a time: those it acts on, and no others.
pub(crate) const FLT_UPDATE: ClearableUpdateFlags =
    ClearableUpdateFlags::new(false, false, false, false, true, false);

pub(crate) const FRL_START: ClearableUpdateFlags =
    ClearableUpdateFlags::new(false, false, false, true, false, false);

pub(crate) const SOURCE_TEST_UPDATE: ClearableUpdateFlags =
    ClearableUpdateFlags::new(false, false, true, false, false, false);

/// The first rate in `rates` the procedure cannot train at: `NotSupported` (not an FRL
/// rate), or one that is not strictly lower than the rate before it (LTS:4 steps down).
/// The discriminants of `HdmiForumFrl` are in bandwidth order.
fn invalid_rate(rates: &[HdmiForumFrl]) -> Option<(usize, &HdmiForumFrl)> {
    rates.iter().enumerate().find(|&(index, &rate)| {
        rate == HdmiForumFrl::NotSupported
            || index
                .checked_sub(1)
                .is_some_and(|previous| rate as u8 >= rates[previous] as u8)
    })
}

/// The number of lanes in use at `rate`: 3 for the 3-lane rates, 4 otherwise.
fn lane_count(rate: HdmiForumFrl) -> usize {
    match rate {
        HdmiForumFrl::Rate3Gbps3Lanes | HdmiForumFrl::Rate6Gbps3Lanes => 3,
        _ => 4,
    }
}

/// The equalization settings for one lane at the given TxFFE level.
fn lane_eq(level: u8) -> LaneEqParams {
    let mut params = LaneEqParams::default();
    // Levels are kept at or below `FfeLevels::MAX` (7), so the fallback is never used.
    params.tx_ffe_level = TxFfeLevel::new(level).unwrap_or(TxFfeLevel::MAX);
    params
}

/// Each lane's current pattern and TxFFE level, held by plumbob. The PHY applies exactly
/// what it is given, and the lanes always hold what it was last told: LTS:P's stop clears
/// the patterns, LTS:4 resets everything.
#[derive(Debug)]
pub(crate) struct Lanes {
    count: usize,
    patterns: [Option<LtpPattern>; 4],
    levels: [u8; 4],
}

impl Lanes {
    /// Every lane with no pattern and TxFFE level 0.
    pub(crate) fn new(rate: HdmiForumFrl) -> Self {
        Self {
            count: lane_count(rate),
            patterns: [None; 4],
            levels: [0; 4],
        }
    }

    /// Stops every lane's pattern, keeping its TxFFE level: what LTS:P tells the PHY.
    fn stop_patterns(&mut self) {
        self.patterns = [None; 4];
    }

    /// Updates each lane from the sink's request for it, recording each TxFFE raise.
    /// Returns whether any TxFFE level changed.
    fn apply<F: FnMut(TrainingEvent)>(
        &mut self,
        requests: LtpRequests,
        no_timeout: bool,
        max_level: u8,
        record: &mut F,
    ) -> bool {
        let requests = [
            requests.lane0,
            requests.lane1,
            requests.lane2,
            requests.lane3,
        ];
        let mut ffe_changed = false;
        for (lane, request) in requests.into_iter().enumerate() {
            let in_use = lane < self.count;
            match request {
                // An undefined value is ignored, as the Xilinx driver does: a lane in use
                // keeps its pattern and level (see `record_undefined`). The Intel series
                // rebuilds the patterns on each request, leaving such a lane without one.
                // Lane 3 at a 3-lane rate is not in use.
                LtpReq::Reserved(_) => {}
                _ if !in_use => {}
                // Without FLT_no_timeout the lane keeps its previous pattern, as the
                // Xilinx driver does (spec Table 6-32, LTP3 row).
                LtpReq::NyquistClock if !no_timeout => {}
                LtpReq::FfeChange => {
                    // Held at the maximum once reached.
                    if self.levels[lane] < max_level {
                        self.levels[lane] += 1;
                        ffe_changed = true;
                        record(TrainingEvent::FfeRaised {
                            // At most 4 lanes and level 7: both fit in a u8.
                            lane: lane as u8,
                            level: self.levels[lane],
                        });
                    }
                }
                // 0x0 (and a 0xF on only some lanes) keeps the lane's pattern and level.
                request => {
                    if let Some(pattern) = request.pattern() {
                        self.patterns[lane] = Some(pattern);
                    }
                }
            }
        }
        ffe_changed
    }

    /// Records each undefined request (0x9–0xD), on every lane, in use or not, whatever
    /// the round leads to.
    fn record_undefined<F: FnMut(TrainingEvent)>(&self, requests: LtpRequests, record: &mut F) {
        let requests = [
            requests.lane0,
            requests.lane1,
            requests.lane2,
            requests.lane3,
        ];
        for (lane, request) in requests.into_iter().enumerate() {
            if let LtpReq::Reserved(value) = request {
                record(TrainingEvent::UndefinedLtpRequest {
                    // At most 4 lanes: fits in a u8.
                    lane: lane as u8,
                    value,
                    in_use: lane < self.count,
                });
            }
        }
    }

    /// Whether every lane in use made the given request.
    fn all(&self, requests: LtpRequests, request: LtpReq) -> bool {
        [
            requests.lane0,
            requests.lane1,
            requests.lane2,
            requests.lane3,
        ][..self.count]
            .iter()
            .all(|r| *r == request)
    }

    fn patterns(&self) -> LanePatterns {
        let lane = |i: usize| {
            if i < self.count {
                self.patterns[i]
            } else {
                None
            }
        };
        LanePatterns {
            lane0: lane(0),
            lane1: lane(1),
            lane2: lane(2),
            lane3: lane(3),
        }
    }

    pub(crate) fn eq_params(&self) -> EqParams {
        let mut params = EqParams::new();
        params.lane0 = lane_eq(self.levels[0]);
        params.lane1 = lane_eq(self.levels[1]);
        params.lane2 = lane_eq(self.levels[2]);
        params.lane3 = (self.count == 4).then(|| lane_eq(self.levels[3]));
        params
    }
}

/// The link training states.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
    /// LTS:2.
    Prepare,
    /// The end of LTS:2, and of LTS:4 at the next rate: configure the rate.
    ConfigureRate,
    /// LTS:3.
    Train,
    /// LTS:P.
    Pass,
    /// LTS:4.
    LowerRate,
    /// Training passed and the sink set `FRL_start`.
    Success,
    /// The attempt ends without a link.
    Fallback(FallbackReason),
    /// The `FLT_no_timeout` poll cap ran out: the link is left as it is.
    Hold,
}

/// How the states end, before LTS:L.
enum End {
    Success(HdmiForumFrl),
    Fallback(FallbackReason),
    Hold(HdmiForumFrl),
}

/// The state of one call to `train`.
#[derive(Debug)]
struct Attempt {
    rate: HdmiForumFrl,
    lanes: Lanes,
    /// The sink's `FLT_no_timeout`, as last read.
    no_timeout: bool,
    /// The TxFFE restrictions in `Source_Test_Configuration`, as last read. Sent to the PHY
    /// with every equalization.
    restrictions: TxFfeRestrictions,
    /// Returns from LTS:P to LTS:3 so far.
    retrains: u32,
}

impl Attempt {
    /// The equalization for the PHY: each lane's TxFFE level, within the restrictions.
    fn eq_params(&self) -> EqParams {
        let mut params = self.lanes.eq_params();
        params.tx_ffe_restrictions = self.restrictions;
        params
    }

    /// The poll limit for LTS:2, LTS:3 and LTS:P: `normal`, or the cap while the sink
    /// sets `FLT_no_timeout`.
    fn limit(&self, config: &TrainingConfig, normal: u32) -> u32 {
        if self.no_timeout {
            config.no_timeout_poll_cap
        } else {
            normal
        }
    }

    /// A state's poll limit ran out after `polls` polls. Normally the attempt falls back
    /// for `reason`, recording `timeout`. Under `FLT_no_timeout` the cap ran out instead:
    /// the test equipment is in control, so the attempt holds the link as it is rather
    /// than leaving FRL on its own timer, as the Xilinx and AMD drivers do.
    fn timed_out<F: FnMut(TrainingEvent)>(
        &self,
        polls: u32,
        timeout: TrainingEvent,
        reason: FallbackReason,
        record: &mut F,
    ) -> State {
        if self.no_timeout {
            record(TrainingEvent::NoTimeoutCapReached { polls });
            State::Hold
        } else {
            record(timeout);
            State::Fallback(reason)
        }
    }
}

/// What one attempt's events pass through on their way to the caller's `record`.
///
/// A plain struct read between the attempt's steps, not state shared with a long-lived
/// closure: a `Cell` shared that way would make the attempt's future not `Send`, so it
/// could not run on a multi-threaded executor.
struct Observer<'a, F> {
    /// Collects the warnings while the attempt runs; the outcome is set at the end.
    trained: Trained,
    /// The sink's `FLT_no_timeout` as last read, as the states hold it: an error under
    /// test does not take the link out of FRL.
    under_test: bool,
    record: &'a mut F,
}

impl<F: FnMut(TrainingEvent)> Observer<'_, F> {
    fn observe(&mut self, event: TrainingEvent) {
        if let TrainingEvent::SourceTestConfigRead { config } = event {
            self.under_test = config.flt_no_timeout;
        }
        self.trained.observe(&event);
        (self.record)(event);
    }
}

/// The state machine, borrowing the I/O for one attempt.
struct Machine<'a, Io> {
    io: &'a mut Io,
}

impl<Io: TrainingIo> Machine<'_, Io> {
    /// One attempt: the states, then LTS:L on a fallback or an error. `record` is called
    /// with each [`TrainingEvent`] as it occurs; the warnings are collected from the same
    /// events.
    async fn run<F: FnMut(TrainingEvent)>(
        mut self,
        rates: &[HdmiForumFrl],
        config: &TrainingConfig,
        record: &mut F,
    ) -> Result<Trained, Error<Io>> {
        if let Some((index, &rate)) = invalid_rate(rates) {
            return Err(TrainingError::InvalidRates { index, rate });
        }
        let Some((&rate, lower)) = rates.split_first() else {
            return Err(TrainingError::NoRates);
        };
        let mut observer = Observer {
            trained: Trained::new(TrainingOutcome::FallbackRequired {
                reason: FallbackReason::RatesExhausted,
            }),
            under_test: false,
            record,
        };
        // Each call gets a closure of its own, so `observer` can be read between them.
        let end = self
            .states(rate, lower, config, &mut |event| observer.observe(event))
            .await;
        let outcome = match end {
            Ok(End::Success(achieved_rate)) => TrainingOutcome::Success { achieved_rate },
            // Under FLT_no_timeout the test equipment is in control: no LTS:L.
            Ok(End::Hold(rate)) => TrainingOutcome::NoTimeoutHold { rate },
            Ok(End::Fallback(reason)) => {
                match self
                    .exit_to_tmds(&mut |event| observer.observe(event))
                    .await
                {
                    Ok(()) => TrainingOutcome::FallbackRequired { reason },
                    Err(error) => return Err(TrainingError::ExitFailed { reason, error }),
                }
            }
            Err(fault) => {
                let exit = if !config.exit_to_tmds_on_error {
                    TmdsExit::Skipped
                } else if observer.under_test {
                    // Under FLT_no_timeout the test equipment is in control, as with a hold.
                    TmdsExit::HeldUnderTest
                } else {
                    match self
                        .exit_to_tmds(&mut |event| observer.observe(event))
                        .await
                    {
                        Ok(()) => TmdsExit::Exited,
                        Err(error) => TmdsExit::Failed(error),
                    }
                };
                return Err(match fault {
                    Fault::Scdc(error) => TrainingError::Scdc { error, exit },
                    Fault::Phy(error) => TrainingError::Phy { error, exit },
                });
            }
        };
        observer.trained.outcome = outcome;
        Ok(observer.trained)
    }

    /// LTS:2 → LTS:3 → LTS:P, with LTS:4, from `rate` down through `lower`. Returns the
    /// rate trained at, or why the attempt falls back.
    async fn states<F: FnMut(TrainingEvent)>(
        &mut self,
        rate: HdmiForumFrl,
        lower: &[HdmiForumFrl],
        config: &TrainingConfig,
        record: &mut F,
    ) -> Result<End, Failure<Io>> {
        let mut rates = lower.iter().copied();
        let mut attempt = Attempt {
            rate,
            lanes: Lanes::new(rate),
            no_timeout: false,
            restrictions: TxFfeRestrictions::default(),
            retrains: 0,
        };
        let mut state = State::Prepare;
        loop {
            state = match state {
                State::Prepare => self.prepare(&mut attempt, config, record).await?,
                State::ConfigureRate => {
                    self.configure_rate(&attempt, config, record).await?;
                    State::Train
                }
                State::Train => self.train_lanes(&mut attempt, config, record).await?,
                State::Pass => self.pass(&mut attempt, config, record).await?,
                State::LowerRate => match rates.next() {
                    Some(next) => self.lower_rate(&mut attempt, next, record).await?,
                    None => {
                        record(TrainingEvent::RatesExhausted);
                        State::Fallback(FallbackReason::RatesExhausted)
                    }
                },
                State::Success => return Ok(End::Success(attempt.rate)),
                State::Fallback(reason) => return Ok(End::Fallback(reason)),
                State::Hold => return Ok(End::Hold(attempt.rate)),
            };
        }
    }

    /// LTS:2: wait for the sink, configure the PHY and write `Config_0` and `Config_1`.
    async fn prepare<F: FnMut(TrainingEvent)>(
        &mut self,
        attempt: &mut Attempt,
        config: &TrainingConfig,
        record: &mut F,
    ) -> Result<State, Failure<Io>> {
        // Read on every attempt, not only when `Source_Test_Update` says it changed: a
        // tester sets `FLT_no_timeout` and leaves it set, and the flag was cleared by an
        // earlier attempt. The Xilinx and AMD drivers read it unconditionally too (Xilinx
        // also re-reads it while waiting for FLT_ready; plumbob does not).
        let update = self.read_update_flags().await?.source_test_update;
        // The PHY gets the restrictions with its first equalization, in `configure_rate`.
        self.read_source_test(attempt, update, record).await?;

        let limit = attempt.limit(config, config.flt_ready_polls);
        match self.poll_flt_ready(limit).await? {
            Some(polls) => record(TrainingEvent::FltReady { after_polls: polls }),
            None => {
                return Ok(attempt.timed_out(
                    limit,
                    TrainingEvent::FltReadyTimeout { polls: limit },
                    FallbackReason::FltReadyTimeout,
                    record,
                ));
            }
        }

        Ok(State::ConfigureRate)
    }

    /// The end of LTS:2, and of LTS:4 at the next rate: clear `FLT_update`, bring the PHY
    /// to `attempt.rate` with the lanes in the state the lane model holds, and write
    /// `Config_0` and `Config_1`. LTS:3 starts from here.
    async fn configure_rate<F: FnMut(TrainingEvent)>(
        &mut self,
        attempt: &Attempt,
        config: &TrainingConfig,
        record: &mut F,
    ) -> Result<(), Failure<Io>> {
        self.clear(FLT_UPDATE).await?;

        // Any transmitter bring-up (such as holding a clock pattern until the PLL locks)
        // is the PHY's, inside `set_frl_rate`: only the PHY knows when its link is up.
        self.io
            .set_frl_rate(attempt.rate)
            .await
            .map_err(Fault::Phy)?;
        // After the rate change, so a PHY that resets its lanes or leaves a bring-up
        // pattern running still ends up with what the lane model holds.
        self.adjust_equalization(attempt.eq_params()).await?;
        self.send_ltp(attempt.lanes.patterns()).await?;
        self.set_frl_output(FrlOutput::GapOnly).await?;

        self.io
            .write_config_0_defaults()
            .await
            .map_err(Fault::Scdc)?;
        self.write_rate(attempt.rate, config, record).await
    }

    /// LTS:3: follow the sink's per-lane requests until it passes the lanes, asks for a
    /// lower rate, or the poll limit runs out.
    async fn train_lanes<F: FnMut(TrainingEvent)>(
        &mut self,
        attempt: &mut Attempt,
        config: &TrainingConfig,
        record: &mut F,
    ) -> Result<State, Failure<Io>> {
        let max_level = config.ffe_levels.limited_to(attempt.rate).value();
        let mut polls = 0;
        loop {
            let limit = attempt.limit(config, config.ltp_polls);
            if polls >= limit {
                // `polls`, not `limit`: the limit is recomputed on every poll, and drops
                // below the polls already made when the tester clears `FLT_no_timeout`.
                return Ok(attempt.timed_out(
                    polls,
                    TrainingEvent::TrainingTimeout { polls },
                    FallbackReason::TrainingTimeout,
                    record,
                ));
            }
            let flags = self.read_update_flags().await?;
            polls += 1;
            // A tester can change the TxFFE restrictions (or FLT_no_timeout) at any point:
            // re-read on every Source_Test_Update, whether or not the sink also posted
            // requests, and re-send the equalization at once if the restrictions changed,
            // with the levels the lanes have.
            if flags.source_test_update && self.read_source_test(attempt, true, record).await? {
                self.adjust_equalization(attempt.eq_params()).await?;
            }
            if !flags.flt_update {
                continue;
            }

            let requests = self.io.read_ltp_requests().await.map_err(Fault::Scdc)?;
            let passed = attempt.lanes.all(requests, LtpReq::None);
            if !passed {
                record(TrainingEvent::LtpRequested { requests });
            }
            attempt.lanes.record_undefined(requests, record);
            if passed {
                record(TrainingEvent::TrainingPassed { after_polls: polls });
                return Ok(State::Pass);
            }
            if attempt.lanes.all(requests, LtpReq::RateChange) {
                return Ok(State::LowerRate);
            }

            let ffe_changed = attempt
                .lanes
                .apply(requests, attempt.no_timeout, max_level, record);
            self.send_ltp(attempt.lanes.patterns()).await?;
            if ffe_changed {
                self.adjust_equalization(attempt.eq_params()).await?;
            }
            self.clear(FLT_UPDATE).await?;
        }
    }

    /// LTS:P: send gap characters until the sink sets `FRL_start`, or asks to retrain.
    /// A retrain returns to LTS:3 while `max_retrains` allows; the next one falls back.
    async fn pass<F: FnMut(TrainingEvent)>(
        &mut self,
        attempt: &mut Attempt,
        config: &TrainingConfig,
        record: &mut F,
    ) -> Result<State, Failure<Io>> {
        // The lanes always hold what the PHY was last told: stopping the patterns here
        // means a retrain starts LTS:3 from no pattern, as the PHY does. The TxFFE
        // levels are not touched, so they carry over.
        attempt.lanes.stop_patterns();
        self.send_ltp(attempt.lanes.patterns()).await?;
        self.set_frl_output(FrlOutput::GapOnly).await?;
        self.clear(FLT_UPDATE).await?;

        let limit = attempt.limit(config, config.frl_start_polls);
        for polls in 1..=limit {
            let flags = self.read_update_flags().await?;
            // As in LTS:3, and before FRL_start can end training: the PHY must not start
            // video on restrictions the tester has replaced.
            if flags.source_test_update && self.read_source_test(attempt, true, record).await? {
                self.adjust_equalization(attempt.eq_params()).await?;
            }
            if flags.flt_update {
                // The sink asks to retrain. If it set FRL_start as well, the retrain wins,
                // as in the Xilinx driver: starting video on a link the sink wants
                // retrained would only have to be undone. FRL_start is cleared so it
                // cannot end the next LTS:P early.
                if flags.frl_start {
                    self.clear(FRL_START).await?;
                    record(TrainingEvent::FrlStartWithRetrain);
                }
                if attempt.retrains >= config.max_retrains {
                    record(TrainingEvent::RetrainsExhausted {
                        retrains: attempt.retrains,
                    });
                    return Ok(State::Fallback(FallbackReason::RetrainsExhausted));
                }
                attempt.retrains += 1;
                record(TrainingEvent::RetrainRequested);
                return Ok(State::Train);
            }
            if flags.frl_start {
                self.clear(FRL_START).await?;
                record(TrainingEvent::FrlStart { after_polls: polls });
                return Ok(State::Success);
            }
        }
        Ok(attempt.timed_out(
            limit,
            TrainingEvent::FrlStartTimeout { polls: limit },
            FallbackReason::FrlStartTimeout,
            record,
        ))
    }

    /// LTS:4: continue training at `rate`, the next one in the list. The sink stays in
    /// FRL; `FLT_ready` is not awaited again.
    async fn lower_rate<F: FnMut(TrainingEvent)>(
        &mut self,
        attempt: &mut Attempt,
        rate: HdmiForumFrl,
        record: &mut F,
    ) -> Result<State, Failure<Io>> {
        self.send_ltp(LanePatterns::default()).await?;
        record(TrainingEvent::RateLowered {
            from: attempt.rate,
            to: rate,
        });

        attempt.rate = rate;
        attempt.lanes = Lanes::new(rate);
        // The rest is LTS:2's, as in the Xilinx driver, whose LTS:4 goes on through the
        // same initialisation as its LTS:2 (patterns cleared, gap only, `Config_0`).
        Ok(State::ConfigureRate)
    }

    /// LTS:L: leave both ends in TMDS. Stops the training patterns, returns the PHY to
    /// TMDS, turns FRL off in `Config_1` and clears `FLT_update` if it is set, so a failed
    /// attempt never leaves the sink configured for a rate the source is not driving.
    ///
    /// Every step is attempted even when an earlier one fails, so a PHY error cannot keep
    /// the sink in FRL, nor an SCDC error the PHY. On failure, returns each end's first
    /// error.
    async fn exit_to_tmds<F: FnMut(TrainingEvent)>(
        &mut self,
        record: &mut F,
    ) -> Result<(), ExitError<Io::ScdcError, Io::PhyError>> {
        let patterns = self.io.send_ltp(LanePatterns::default()).await;
        let phy_rate = self.io.set_frl_rate(HdmiForumFrl::NotSupported).await;
        let config = self
            .io
            .write_frl_config(FrlConfig {
                frl_rate: HdmiForumFrl::NotSupported,
                ffe_levels: FfeLevels::default(),
            })
            .await;
        let flt_update = match self.io.read_update_flags().await {
            Ok(flags) if flags.flt_update => self.io.clear_update_flags(FLT_UPDATE).await,
            Ok(_) => Ok(()),
            Err(e) => Err(e),
        };
        let phy = patterns.and(phy_rate).err();
        let scdc = config.and(flt_update).err();
        if phy.is_none() && scdc.is_none() {
            record(TrainingEvent::ExitedToTmds);
            return Ok(());
        }
        record(TrainingEvent::ExitToTmdsFailed {
            scdc: scdc.is_some(),
            phy: phy.is_some(),
        });
        Err(ExitError { scdc, phy })
    }

    /// Writes `Config_1` with `rate` and the FFE levels limited for it.
    async fn write_rate<F: FnMut(TrainingEvent)>(
        &mut self,
        rate: HdmiForumFrl,
        config: &TrainingConfig,
        record: &mut F,
    ) -> Result<(), Failure<Io>> {
        let ffe_levels = config.ffe_levels.limited_to(rate);
        self.io
            .write_frl_config(FrlConfig {
                frl_rate: rate,
                ffe_levels,
            })
            .await
            .map_err(Fault::Scdc)?;
        record(TrainingEvent::RateConfigured { rate, ffe_levels });
        Ok(())
    }

    /// Polls `FLT_ready` up to `limit` times. Returns the number of polls it took to
    /// assert, or `None` if it did not.
    async fn poll_flt_ready(&mut self, limit: u32) -> Result<Option<u32>, Failure<Io>> {
        for polls in 1..=limit {
            if self.io.read_flt_ready().await.map_err(Fault::Scdc)? {
                return Ok(Some(polls));
            }
        }
        Ok(None)
    }

    /// Reads `Source_Test_Configuration` into the attempt, and clears `Source_Test_Update`
    /// if `update` says it is set. Returns whether the TxFFE restrictions changed.
    async fn read_source_test<F: FnMut(TrainingEvent)>(
        &mut self,
        attempt: &mut Attempt,
        update: bool,
        record: &mut F,
    ) -> Result<bool, Failure<Io>> {
        let source_test = self
            .io
            .read_source_test_config()
            .await
            .map_err(Fault::Scdc)?;
        attempt.no_timeout = source_test.flt_no_timeout;
        let restrictions = TxFfeRestrictions::new(
            source_test.txffe_pre_shoot_only,
            source_test.txffe_de_emphasis_only,
            source_test.txffe_no_ffe,
        );
        let changed = restrictions != attempt.restrictions;
        attempt.restrictions = restrictions;
        record(TrainingEvent::SourceTestConfigRead {
            config: source_test,
        });
        if update {
            self.clear(SOURCE_TEST_UPDATE).await?;
        }
        Ok(changed)
    }

    async fn read_update_flags(&mut self) -> Result<UpdateFlags, Failure<Io>> {
        self.io.read_update_flags().await.map_err(Fault::Scdc)
    }

    async fn clear(&mut self, flags: ClearableUpdateFlags) -> Result<(), Failure<Io>> {
        self.io.clear_update_flags(flags).await.map_err(Fault::Scdc)
    }

    async fn send_ltp(&mut self, patterns: LanePatterns) -> Result<(), Failure<Io>> {
        self.io.send_ltp(patterns).await.map_err(Fault::Phy)
    }

    async fn set_frl_output(&mut self, output: FrlOutput) -> Result<(), Failure<Io>> {
        self.io.set_frl_output(output).await.map_err(Fault::Phy)
    }

    async fn adjust_equalization(&mut self, params: EqParams) -> Result<(), Failure<Io>> {
        self.io
            .adjust_equalization(params)
            .await
            .map_err(Fault::Phy)
    }
}