msp430fr5969-hal 0.2.0

Hardware abstraction layer (embedded-hal) for the TI MSP430FR5969 microcontroller
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
//! Timer_B0 **and Timer_A** pulse-width modulation
//! (`embedded_hal::pwm::SetDutyCycle`).
//!
//! Where [`crate::timer`] *reads* a free-running counter to measure time, PWM
//! *drives* a pin: the same kind of counter, but each tick is compared against
//! two thresholds and the comparison toggles an output pin in hardware — no CPU
//! involvement once it is set up. The result is a square wave whose **duty
//! cycle** (high-time ÷ period) you control by writing one register, which is
//! how you dim an LED, set a servo angle, or synthesize an analog level through
//! a filter.
//!
//! # How one Timer_B0 makes up to six PWM channels
//!
//! Timer0_B7 has a 16-bit counter `TB0R` and **seven** capture/compare
//! registers, `TB0CCR0..TB0CCR6`. Run the counter in **up mode** (`MC = 1`): it
//! counts `0, 1, … TB0CCR0, 0, 1, …`. So `TB0CCR0` is special — it sets the
//! **period** (the counter's top). That leaves `TB0CCR1..6` as **six
//! independent duty-cycle comparators**, all sharing the one period. Each drives
//! its own pin `TB0.1..TB0.6`.
//!
//! Each channel's output is generated by its `OUTMOD` field. We use
//! **`OUTMOD = 7` (Reset/Set)**: the output is *set* (high) when the counter
//! wraps to 0 at the top, and *reset* (low) when the counter reaches that
//! channel's `TB0CCRn`. So the pin is high for `TB0CCRn` ticks out of every
//! `TB0CCR0 + 1` — i.e. **duty = `TB0CCRn / (TB0CCR0 + 1)`**. Raising `TB0CCRn`
//! widens the pulse; that single write is the whole API.
//!
//! ```text
//!   counter:  0 ............ CCRn ............ CCR0 (top) ┐ wrap
//!   output:   ┌──────────────┐                           └─ set ──► high again
//!             │   high       └──── low ───────────────────┘
//!             └ set at wrap        reset at CCRn
//! ```
//!
//! # Clean 0% and 100%
//!
//! `OUTMOD = 7` cannot quite reach the rails: `CCRn = 0` still glitches high for
//! one tick at the wrap, and there is no value that holds the pin high across a
//! whole period without a one-tick low. `embedded-hal` requires
//! [`set_duty_cycle(0)`](embedded_hal::pwm::SetDutyCycle::set_duty_cycle) to be
//! *fully* off and `set_duty_cycle(max)` *fully* on, so at those two endpoints we
//! switch the channel to **`OUTMOD = 0` (output the `OUT` bit)** and park `OUT`
//! low or high — a constant level with no edges. Everything in between uses the
//! Reset/Set comparator.
//!
//! # Resolution vs. frequency
//!
//! The period `TB0CCR0` is also the **duty resolution**: a period of 8000 gives
//! 8000 distinct duty steps. Period and PWM frequency trade off directly —
//! `f_pwm = f_clock / ((TB0CCR0 + 1) · divider)` — so a higher PWM frequency
//! (shorter period) means coarser duty steps. [`Pwm::new_smclk`] picks the
//! input divider (`ID` × `TBIDEX`, ÷1…÷64) that yields the **largest** period
//! fitting 16 bits for your target frequency, i.e. the finest resolution
//! achievable at that frequency.
//!
//! # Channel ↔ pin map (MSP430FR5969, Timer_B0)
//!
//! | Channel | `TB0.n` | Pin   | Shared with        |
//! |---------|---------|-------|--------------------|
//! | 1       | TB0.1   | P1.4  | UCA0STE / A4       |
//! | 2       | TB0.2   | P1.5  | UCA0CLK / A5       |
//! | 3       | TB0.3   | P1.6  | **eUSCI_B0 SIMO/SDA** |
//! | 4       | TB0.4   | P1.7  | **eUSCI_B0 SOMI/SCL** |
//!
//! `TB0.1` (P1.4) and `TB0.2` (P1.5) are the clean choices — they are broken out
//! on the LaunchPad headers and collide with nothing. `TB0.3`/`TB0.4` live on
//! the P1.6/P1.7 pins the SPI and I2C demos use, so the typestate lets you pick
//! one role or the other, never both at once. (`TB0.0`/`TB0.5`/`TB0.6` exist on
//! other pins but `TB0.0` is consumed as the period, so it is not a usable PWM
//! output here.)
//!
//! # Timer_A PWM ([`PwmTimerA`]) — the same machinery on TA0/TA1
//!
//! Timer_A blocks generate PWM exactly the same way (up mode, `CCR0` period,
//! `OUTMOD = 7` per channel), and [`PwmTimerA`] drives it on **TA0 or TA1**
//! (instance-generic over [`crate::capture::Instance`], the capture module's
//! raw-pointer trait — a Timer_A3 block has `CCR1`/`CCR2`, so two outputs per
//! block). What this buys:
//!
//! - **PWM without Timer_B0** — TB0 stays free, and with it the P1.4–P1.7
//!   pins; in particular PWM no longer has to contend with **eUSCI_B0** for
//!   P1.6/P1.7 (the SPI/I2C conflict the TB0 table above describes).
//! - PWM **and** capture concurrently: PWM on one Timer_A block while the
//!   other captures (each block is consumed by move — PWM-vs-capture on the
//!   *same* block stays exclusive, and `TA0` PWM also excludes
//!   [`crate::timer::Counter`], which lives on TA0).
//!
//! | Output | Pin  | Shared with                     |
//! |--------|------|---------------------------------|
//! | TA0.1  | P1.0 | green LED2 / TA0.CCI1A          |
//! | TA0.2  | P1.1 | button S2 / REFOUT / TA0.CCI2A  |
//! | TA1.1  | P1.2 | TA1.CCI1A (otherwise free)      |
//! | TA1.2  | P1.3 | TA1.CCI2A (otherwise free)      |
//!
//! **TA1 is the natural PWM block** (P1.2/P1.3 are free pins); avoid driving
//! P1.1 while button S2 could be pressed — the LaunchPad wires S2 straight to
//! ground, so a pressed button shorts the driven-high pad.
//!
//! # Example
//!
//! ```ignore
//! let clocks = hal::clocks::configure(p.cs);          // SMCLK = 8 MHz
//! hal::gpio::unlock_pins(&p.pmm);
//! let (port1, _port2) = p.port_1_2.split();
//! let p14 = port1.pin4.into_timer_b_output();         // P1.4 → TB0.1
//! let pwm = Pwm::new_smclk(p.timer_0_b7, &clocks, 1_000); // ~1 kHz
//! let mut ch = pwm.channel(p14);
//! ch.set_duty_cycle_percent(25).ok();                 // 25% brightness
//! ```

use core::convert::Infallible;
use core::marker::PhantomData;

use embedded_hal::pwm::{ErrorType, SetDutyCycle};

use crate::capture::Instance;
use crate::clocks::Clocks;
use crate::gpio::{Pin, TimerA, TimerB, P1};
use crate::pac;

/// A Timer_B0 PWM generator in up mode. Owns the Timer0_B7 peripheral and fixes
/// the shared **period** (and thus the PWM frequency and duty resolution); vends
/// per-pin [`PwmChannel`]s that each control one duty cycle.
pub struct Pwm {
    timer: pac::Timer0B7,
    period: u16,
    freq_hz: u32,
}

impl Pwm {
    /// Configure Timer_B0 for PWM clocked from **SMCLK**, targeting `freq_hz`.
    ///
    /// Picks the input divider that gives the largest 16-bit period at that
    /// frequency (finest duty resolution — see the module docs) and programs up
    /// mode with `TB0CCR0 = period`. The counter starts immediately, but every
    /// channel begins at 0% (output low) until you call
    /// [`channel`](Pwm::channel) and set a duty.
    ///
    /// The achieved frequency is `SMCLK / ((period + 1) · divider)`, which may
    /// differ slightly from the request because the period is an integer; read
    /// it back with [`frequency`](Pwm::frequency).
    pub fn new_smclk(timer: pac::Timer0B7, clocks: &Clocks, freq_hz: u32) -> Self {
        let smclk = clocks.smclk();
        let (id_code, idex_code, period) = compute_divider(smclk, freq_hz);

        // TBIDEX: the second-stage input divider (÷1…÷8).
        timer.tb0ex0().write(|w| match idex_code {
            0 => w.tbidex().tbidex_0(),
            1 => w.tbidex().tbidex_1(),
            2 => w.tbidex().tbidex_2(),
            3 => w.tbidex().tbidex_3(),
            4 => w.tbidex().tbidex_4(),
            5 => w.tbidex().tbidex_5(),
            6 => w.tbidex().tbidex_6(),
            _ => w.tbidex().tbidex_7(),
        });

        // TB0CCR0 = period (the counter's top in up mode = duty resolution).
        // SAFETY: `bits` on a full-width value register is an unsafe writer.
        timer.tb0ccr0().write(|w| unsafe { w.bits(period) });

        // TB0CTL: source = SMCLK, first-stage divider = ID, up mode, and TBCLR
        // to reset the counter + divider so the first period starts clean.
        timer.tb0ctl().write(|w| {
            w.tbssel().tbssel_2(); // SMCLK
            match id_code {
                0 => w.id().id_0(),
                1 => w.id().id_1(),
                2 => w.id().id_2(),
                _ => w.id().id_3(),
            };
            w.mc().mc_1(); // up mode (0..=TB0CCR0)
            w.tbclr().set_bit()
        });

        let total_div = id_div(id_code) * (idex_code as u32 + 1);
        Pwm {
            timer,
            period,
            freq_hz: smclk / ((period as u32 + 1) * total_div),
        }
    }

    /// The period value (`TB0CCR0`). Equals the duty resolution and the value
    /// [`SetDutyCycle::max_duty_cycle`] returns for every channel.
    pub const fn period(&self) -> u16 {
        self.period
    }

    /// The actual PWM frequency in Hz, after integer-period rounding.
    pub const fn frequency(&self) -> u32 {
        self.freq_hz
    }

    /// Bind a routed `TB0.n` pin to its PWM channel.
    ///
    /// Consumes a pin already switched to [`TimerB`] mode (via
    /// [`Pin::into_timer_b_output`](crate::gpio::Pin::into_timer_b_output)); the
    /// [`PwmPin`] bound proves the pin really has that timer function, so the
    /// channel number is correct by construction. Initializes the channel at
    /// **0% (output low)** and returns a [`PwmChannel`] implementing
    /// [`SetDutyCycle`]. The pin is consumed because the timer now owns the pad.
    pub fn channel<P: PwmPin>(&self, _pin: P) -> PwmChannel {
        let ch = PwmChannel {
            channel: P::CHANNEL,
            period: self.period,
        };
        // Start defined-off (constant low) until the caller sets a duty.
        ch.apply(0);
        ch
    }

    /// Release the underlying timer (left running). Stop it via the returned
    /// peripheral if desired.
    pub fn free(self) -> pac::Timer0B7 {
        self.timer
    }
}

/// One Timer_B0 PWM channel: a single duty-cycle output on `TB0.n`.
///
/// Created by [`Pwm::channel`]. Holds only the channel index and the shared
/// period; it reaches its `TB0CCRn`/`TB0CCTLn` registers through a stolen
/// peripheral handle. That is sound because every channel touches **only its
/// own** compare register and control word — disjoint from the period register
/// the [`Pwm`] owns and from every other channel — the same disjoint-register
/// pattern [`crate::timer`]'s ISR helpers use.
pub struct PwmChannel {
    channel: u8,
    period: u16,
}

impl PwmChannel {
    /// Apply a duty value, handling the clean-rail endpoints (see module docs).
    fn apply(&self, duty: u16) {
        // SAFETY: a stolen Timer0_B7 handle that writes only this channel's
        // TB0CCRn + TB0CCTLn — disjoint from the period register and all other
        // channels, so it cannot race the Pwm-owning code or sibling channels.
        let t = unsafe { pac::Timer0B7::steal() };

        // Per-channel apply: constant low at 0, constant high at full scale,
        // Reset/Set comparator in between. The register set differs per channel
        // (distinct PAC types), so this expands once per CCR via the macro.
        macro_rules! apply_to {
            ($ccr:ident, $cctl:ident) => {{
                if duty == 0 {
                    // OUTMOD 0: output follows OUT; park low.
                    t.$cctl().write(|w| w.outmod().outmod_0().out().clear_bit());
                } else if duty >= self.period {
                    // OUTMOD 0: park high.
                    t.$cctl().write(|w| w.outmod().outmod_0().out().set_bit());
                } else {
                    t.$ccr().write(|w| unsafe { w.bits(duty) });
                    t.$cctl().write(|w| w.outmod().outmod_7()); // Reset/Set
                }
            }};
        }

        match self.channel {
            1 => apply_to!(tb0ccr1, tb0cctl1),
            2 => apply_to!(tb0ccr2, tb0cctl2),
            3 => apply_to!(tb0ccr3, tb0cctl3),
            4 => apply_to!(tb0ccr4, tb0cctl4),
            5 => apply_to!(tb0ccr5, tb0cctl5),
            6 => apply_to!(tb0ccr6, tb0cctl6),
            _ => {}
        }
    }
}

impl ErrorType for PwmChannel {
    // Setting a duty is just register writes — it cannot fail.
    type Error = Infallible;
}

impl SetDutyCycle for PwmChannel {
    fn max_duty_cycle(&self) -> u16 {
        self.period
    }

    fn set_duty_cycle(&mut self, duty: u16) -> Result<(), Self::Error> {
        self.apply(duty.min(self.period));
        Ok(())
    }
}

// ---------------------------------------------------------------------------
// Input-divider selection
// ---------------------------------------------------------------------------

/// The integer divisor the `ID` field code 0..=3 represents (÷1, ÷2, ÷4, ÷8).
const fn id_div(code: u8) -> u32 {
    1 << code
}

/// Choose `(ID code, TBIDEX code, period)` for `freq_hz` from `clock_hz`.
///
/// The total input divider is `ID (1/2/4/8) × TBIDEX (1..8)`, ÷1…÷64. We want
/// the **smallest** total divider whose resulting period fits 16 bits, because a
/// smaller divider leaves a larger period — and the period is the duty
/// resolution. Falls back to the coarsest divider (and accepts a frequency
/// error) if even ÷64 cannot fit the period, and clamps the period to ≥ 1.
const fn compute_divider(clock_hz: u32, freq_hz: u32) -> (u8, u8, u16) {
    // Source ticks per PWM period before the extra divider.
    let counts = if freq_hz == 0 { u32::MAX } else { clock_hz / freq_hz };

    let mut id_code = 0u8;
    while id_code < 4 {
        let mut idex_code = 0u8;
        while idex_code < 8 {
            let total = id_div(id_code) * (idex_code as u32 + 1);
            let per = counts / total;
            if per >= 2 && per <= 65536 {
                return (id_code, idex_code, (per - 1) as u16);
            }
            idex_code += 1;
        }
        id_code += 1;
    }

    // freq too low for even ÷64 (period > 65536) or too high (period < 2):
    // pick the coarsest divider and the widest period, clamped into range.
    if counts < 2 {
        (0, 0, 1) // fastest the timer can go
    } else {
        (3, 7, 65535) // ÷64, max period — closest reachable to a very low freq
    }
}

// ---------------------------------------------------------------------------
// Typed Timer_B0 output pins → channel numbers
// ---------------------------------------------------------------------------

mod sealed {
    pub trait Sealed {}
}

/// A pin routed to a Timer_B0 output (`TB0.n`). Implemented only for the
/// [`TimerB`]-mode pins that correspond to real silicon channels; the associated
/// constant is that channel's `TB0CCRn` index (1..=6). Sealed — the set of
/// `TB0.n` pins is fixed by the package.
pub trait PwmPin: sealed::Sealed {
    /// The `TB0CCRn` channel index (1..=6) this pin's output is driven by.
    const CHANNEL: u8;
}

macro_rules! pwm_pins {
    ($($Port:ident $N:literal => $ch:literal),+ $(,)?) => {$(
        impl sealed::Sealed for Pin<$Port, $N, TimerB> {}
        impl PwmPin for Pin<$Port, $N, TimerB> {
            const CHANNEL: u8 = $ch;
        }
    )+};
}

// MSP430FR5969 Timer_B0 outputs reachable with SEL = 01 (datasheet SLAS704G
// Port P1 function table). P1.6/P1.7 also carry eUSCI_B0 SIMO/SDA & SOMI/SCL —
// the typestate makes that an exclusive choice.
pwm_pins! {
    P1 4 => 1, // TB0.1
    P1 5 => 2, // TB0.2
    P1 6 => 3, // TB0.3 (shared with eUSCI_B0 SIMO/SDA)
    P1 7 => 4, // TB0.4 (shared with eUSCI_B0 SOMI/SCL)
}

// ---------------------------------------------------------------------------
// Timer_A PWM (TA0/TA1, instance-generic)
// ---------------------------------------------------------------------------
//
// Same up-mode/OUTMOD machinery as Timer_B0 above, on the two pin-connected
// Timer_A3 blocks. Register access is raw pointers off the capture module's
// `Instance::BASE` (the PWM path touches registers the same way `capture`
// does, and the trait already fixes the base addresses and the
// consume-by-move exclusivity). Offsets and field values are the Timer_A
// map (SLAU367P): identical layout to Timer_B for everything PWM needs.

const TA_CCTL0: usize = 0x02; // TAxCCTL0 (CCTLn = CCTL0 + 2n)
const TA_CCR0: usize = 0x12; // TAxCCR0 (CCRn = CCR0 + 2n)
const TA_EX0: usize = 0x20; // TAxEX0 (TAIDEX second-stage divider)

// TAxCTL: SMCLK source, ID divider at bits 7:6, up mode, counter clear.
const TA_TASSEL_SMCLK: u16 = 0x0200;
const TA_MC_UP: u16 = 0x0010;
const TA_TACLR: u16 = 0x0004;

// TAxCCTLn whole-word values for the three output states. A full write also
// zeroes CAP (compare mode) and every interrupt bit — exactly the clean
// state a PWM channel wants.
const TA_OUTMOD7: u16 = 0x00E0; // Reset/Set comparator
const TA_PARK_LOW: u16 = 0x0000; // OUTMOD 0, OUT = 0
const TA_PARK_HIGH: u16 = 0x0004; // OUTMOD 0, OUT = 1

/// A Timer_A PWM generator in up mode — [`Pwm`]'s instance-generic sibling
/// for **TA0/TA1** (two outputs per block, `TAx.1`/`TAx.2`; `CCR0` is the
/// shared period). Owns the PAC peripheral by move, so PWM-vs-capture-vs-
/// [`crate::timer::Counter`] on one block is an exclusive choice.
pub struct PwmTimerA<T: Instance> {
    timer: T,
    period: u16,
    freq_hz: u32,
}

impl<T: Instance> PwmTimerA<T> {
    /// Configure this Timer_A block for PWM clocked from **SMCLK**, targeting
    /// `freq_hz` — divider selection, integer-period rounding, and the
    /// start-at-0%-per-channel contract all as in [`Pwm::new_smclk`].
    pub fn new_smclk(timer: T, clocks: &Clocks, freq_hz: u32) -> Self {
        let smclk = clocks.smclk();
        let (id_code, idex_code, period) = compute_divider(smclk, freq_hz);

        // SAFETY: raw whole-register writes to this owned block's EX0, CCR0
        // and CTL — the same access pattern the capture module uses on the
        // same registers.
        unsafe {
            // TAIDEX: second-stage divider (÷1…÷8). Latched cleanly by the
            // TACLR below, per SLAU367's TAIDEX note.
            ((T::BASE + TA_EX0) as *mut u16).write_volatile(idex_code as u16);
            // TAxCCR0 = period (the counter's top in up mode).
            ((T::BASE + TA_CCR0) as *mut u16).write_volatile(period);
            // TAxCTL: SMCLK, first-stage ID divider, up mode, TACLR.
            ((T::BASE) as *mut u16).write_volatile(
                TA_TASSEL_SMCLK | ((id_code as u16) << 6) | TA_MC_UP | TA_TACLR,
            );
        }

        let total_div = id_div(id_code) * (idex_code as u32 + 1);
        PwmTimerA {
            timer,
            period,
            freq_hz: smclk / ((period as u32 + 1) * total_div),
        }
    }

    /// The period value (`TAxCCR0`) — the duty resolution, and every
    /// channel's [`SetDutyCycle::max_duty_cycle`].
    pub const fn period(&self) -> u16 {
        self.period
    }

    /// The actual PWM frequency in Hz, after integer-period rounding.
    pub const fn frequency(&self) -> u32 {
        self.freq_hz
    }

    /// Bind a routed `TAx.n` pin to its PWM channel.
    ///
    /// Consumes a pin switched to output-direction [`TimerA`] mode (via
    /// [`Pin::into_timer_a_output`](crate::gpio::Pin::into_timer_a_output) —
    /// **not** `into_timer_a_capture`, whose input direction leaves the pad
    /// undriven); the [`PwmPinA`] bound ties the pin to this timer instance
    /// *and* its channel number, so a TA0 pin cannot be bound to a TA1
    /// generator. Initializes the channel at **0% (output low)**.
    pub fn channel<P: PwmPinA<T>>(&self, _pin: P) -> PwmChannelA<T> {
        let ch = PwmChannelA {
            channel: P::CHANNEL,
            period: self.period,
            _timer: PhantomData,
        };
        ch.apply(0);
        ch
    }

    /// Release the underlying timer (left running). Stop it via the returned
    /// peripheral if desired.
    pub fn free(self) -> T {
        self.timer
    }
}

/// One Timer_A PWM channel: a single duty-cycle output on `TAx.n`.
///
/// Created by [`PwmTimerA::channel`]. Holds only the channel index and the
/// shared period; register access is raw pointers to **only this channel's**
/// `TAxCCRn`/`TAxCCTLn` — disjoint from the period register the generator
/// owns and from the sibling channel (the [`PwmChannel`] soundness argument,
/// with addresses instead of a stolen PAC handle).
pub struct PwmChannelA<T: Instance> {
    channel: u8,
    period: u16,
    _timer: PhantomData<T>,
}

impl<T: Instance> PwmChannelA<T> {
    /// Apply a duty value, handling the clean-rail endpoints exactly as the
    /// Timer_B channels do (park via OUTMOD 0 at the rails, Reset/Set
    /// in between).
    fn apply(&self, duty: u16) {
        let n = self.channel as usize;
        let cctl = (T::BASE + TA_CCTL0 + 2 * n) as *mut u16;
        let ccr = (T::BASE + TA_CCR0 + 2 * n) as *mut u16;
        // SAFETY: whole-word writes to this channel's own CCTLn/CCRn only.
        unsafe {
            if duty == 0 {
                cctl.write_volatile(TA_PARK_LOW);
            } else if duty >= self.period {
                cctl.write_volatile(TA_PARK_HIGH);
            } else {
                ccr.write_volatile(duty);
                cctl.write_volatile(TA_OUTMOD7);
            }
        }
    }
}

impl<T: Instance> ErrorType for PwmChannelA<T> {
    // Setting a duty is just register writes — it cannot fail.
    type Error = Infallible;
}

impl<T: Instance> SetDutyCycle for PwmChannelA<T> {
    fn max_duty_cycle(&self) -> u16 {
        self.period
    }

    fn set_duty_cycle(&mut self, duty: u16) -> Result<(), Self::Error> {
        self.apply(duty.min(self.period));
        Ok(())
    }
}

/// A pin routed to a Timer_A compare output (`TAx.n`), in output-direction
/// [`TimerA`] mode via [`Pin::into_timer_a_output`](crate::gpio::Pin::
/// into_timer_a_output). Parameterized by the timer instance so the
/// pin↔block wiring is checked at compile time; the associated constant is
/// the `TAxCCRn` channel. Sealed — the map is fixed by the package.
pub trait PwmPinA<T: Instance>: sealed::Sealed {
    /// The `TAxCCRn` channel (1 or 2) that drives this pin.
    const CHANNEL: u8;
}

macro_rules! pwm_pins_a {
    ($($Timer:ty: $Port:ident $N:literal => $ch:literal),+ $(,)?) => {$(
        impl sealed::Sealed for Pin<$Port, $N, TimerA> {}
        impl PwmPinA<$Timer> for Pin<$Port, $N, TimerA> {
            const CHANNEL: u8 = $ch;
        }
    )+};
}

// The same pads that serve as TAx.CCInA capture inputs drive TAx.n when the
// direction is output (SLAS704G Port P1 function table) — see the Timer_A
// table in the module docs for the LaunchPad sharing caveats (P1.0 = green
// LED, P1.1 = button S2).
pwm_pins_a! {
    pac::Timer0A3: P1 0 => 1, // TA0.1 (green LED2)
    pac::Timer0A3: P1 1 => 2, // TA0.2 (button S2 / REFOUT)
    pac::Timer1A3: P1 2 => 1, // TA1.1
    pac::Timer1A3: P1 3 => 2, // TA1.2
}