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
//! FRAM Memory Protection Unit — hardware read/write/execute fences inside
//! the FRAM, because on this part "flash-like" memory is writable by any
//! stray `MOV`.
//!
//! # Why FRAM needs an MPU at all
//!
//! On a flash MCU, firmware is protected from itself by physics: overwriting
//! code takes an erase/program sequence behind an unlock dance. FRAM removed
//! all of that friction — a write is a plain store ([`crate::fram`]) — which
//! means a single wild pointer can rewrite the running program as easily as
//! it scribbles on a buffer. The MPU is the missing friction, reintroduced as
//! a bus-level checker: it carves main FRAM into **three contiguous
//! segments** with two movable borders (16-byte granularity), gives each
//! segment read/write/execute enables, and polices every access — CPU *and*
//! DMA. The 512 B Information FRAM gets a fourth, fixed segment.
//!
//! A violating access is **suppressed** (a blocked write leaves FRAM
//! untouched; a blocked fetch never executes), the segment's flag latches in
//! `MPUCTL1`, and then one of two things happens, per segment
//! ([`Violation`]): nothing more (poll the flag, or escalate to the `SYSNMI`
//! vector with [`Config::nmi`]), or a **PUC reset** — after which `SYSRSTIV`
//! names the guilty segment ([`crate::sys::ResetReason::MpuSeg1`] etc., and
//! the pre-reset flags survive into the next boot for forensics).
//!
//! The intended production posture: borders at the code/data split, code
//! segment [`Access::rx`] with [`Access::reset_on_violation`], data segment
//! writable. Firmware that corrupts itself then reboots into a recorded
//! reason instead of limping on executing garbage.
//!
//! ```ignore
//! use hal::mpu::{Access, Config, Mpu};
//! let mut mpu = Mpu::new(p.mpu);        // consume the PAC peripheral
//! mpu.enable(&Config {
//!     border1: 0x1_0000,          // seg1 = both lower-bank code+data,
//!     border2: 0x1_0000,          // seg2 = empty,
//!     seg1: Access::rwx(),        // seg3 = the upper 16 KB bank
//!     seg2: Access::rwx(),
//!     seg3: Access::rx(),         // HighFram log area: no stray writes
//!     info: Access::rwx(),
//!     nmi: true,                  // violations fire the SYSNMI vector
//! }).unwrap();
//! ```
//!
//! # Register access: typed, except the password lane
//!
//! The driver owns the PAC's [`pac::Mpu`] block (consume-by-move, like every
//! other peripheral here) and goes through its typed registers for reads and
//! for the plain 16-bit data registers (`MPUSEGB1`/`MPUSEGB2`/`MPUSAM`/
//! `MPUCTL1`). Two things stay raw byte pointers, deliberately:
//!
//! 1. **`MPUCTL0` writes.** Its high byte is the `MPUPW` password lane — a
//!    field the SVD does not model. Reads return `0x96` there, and every
//!    *word* write must carry `0xA5` or the chip takes a PUC — so a PAC
//!    `modify()` on `MPUCTL0` would read `0x96`, echo it back as the
//!    password, and reset the chip. The same landmine as `WDTCTL`/`PMMCTL0`
//!    (see [`crate::watchdog`]). **Never touch `MPUCTL0` through the PAC's
//!    field API.**
//! 2. **The open/close bracket.** The password is byte-granular: writing the
//!    high byte alone opens/closes the register file while leaving
//!    `MPUENA`/`MPULOCK`/`MPUSEGIE` in the low byte untouched — which is what
//!    lets a `SYSNMI` handler clear flags without momentarily dropping
//!    protection. The PAC's API is 16-bit-only (the same limitation
//!    [`crate::crc`] works around), so these byte-lane writes cannot be
//!    expressed through it. The addresses derive from [`pac::Mpu::PTR`], so
//!    the raw accesses stay anchored to the PAC's register map.
//!
//! # The password discipline (`MPUPW`)
//!
//! `MPUCTL0`'s high byte is a key: writes carrying `0xA5` there **open** all
//! MPU registers for writing; writing any other value to the high byte
//! **closes** them again (reads always work, and read back `0x96` in the key
//! byte). While closed, a write to an MPU register is itself a violation that
//! triggers a PUC (`SYSRSTIV` = `0x22`, [`crate::sys::ResetReason::MpuPassword`]).
//! This driver therefore brackets every register sequence with byte-writes to
//! `MPUCTL0_H` — open `0xA5`, work, close `0x00` — the sequence TI's own
//! examples use.
//!
//! **The NMI re-lock race:** MPU violation NMIs are non-maskable — a critical
//! section cannot hold them off. If a violation NMI fires *while thread-mode
//! code is inside an open→write→close bracket* and the handler runs its own
//! bracket ([`clear_violation_flags`]), its close re-locks the registers
//! under the interrupted sequence, whose next write then PUCs. The discipline
//! that keeps this theoretical: configure the MPU *before* the protected
//! regions can be touched, and treat reconfiguration as something you don't
//! do concurrently with code that may violate. (Maskable interrupts are no
//! hazard — only this module touches MPU registers, and [`Mpu`] owns the
//! peripheral, so a second driver instance cannot exist without `steal()`.)
//!
//! # Lock-until-BOR
//!
//! [`Mpu::lock`] sets `MPULOCK`: from then on the MPU registers cannot be
//! modified — password or not — until a **BOR-class** reset (power cycle,
//! reset pin, LPMx.5 wake). Note the asymmetry with violations' PUC: a PUC is
//! *weaker* than a BOR, so even a malicious `force_reset()` cannot unlock a
//! locked MPU. That is the point: lock is the tamper-resistance tier above
//! password protection.
//!
//! # Self-lockout (read the borders twice)
//!
//! Nothing stops a [`Config`] whose executing segment lacks
//! [`Access::execute`], or whose only writable data region excludes
//! statics the program needs. The validation here checks border geometry,
//! not intent — removing X from under the program counter hands the CPU a
//! suppressed fetch (and a PUC if so configured, else a wedge until the
//! watchdog or an NMI intervenes). Rust's linker script places all code and
//! `.rodata` in the lower bank (`0x4400..0xFF80`), so a border at
//! `0x1_0000` — protecting exactly the [`crate::fram::HighFram`] bank — is
//! the safe first move, and what the `mpu_test_runner` fixture does.

use crate::mpu_seg;
use crate::pac;

pub use crate::mpu_seg::{
    segment_containing, Access, MpuError as Error, Violation, MAIN_END, MAIN_START,
};

/// `MPUCTL0` low byte lane: `MPUENA` / `MPULOCK` / `MPUSEGIE`. Byte-granular
/// so the password lane above it is never written along with the bits.
/// (Derived from the PAC's register map at runtime — const pointer
/// arithmetic on the integer-derived `PTR` is rejected by const eval.)
#[inline]
fn mpuctl0_l() -> *mut u8 {
    pac::Mpu::PTR as *mut u8
}
/// `MPUCTL0` high byte lane: the password (write `0xA5` to open, anything
/// else to close; reads as `0x96`).
#[inline]
fn mpuctl0_h() -> *mut u8 {
    (pac::Mpu::PTR as usize + 1) as *mut u8
}

/// `MPUCTL0_H` open value (`MPUPW >> 8`).
const KEY_OPEN: u8 = 0xA5;
/// Any non-key value closes; `0x00` is the conventional one.
const KEY_CLOSE: u8 = 0x00;

// MPUCTL0 low-byte bits.
const ENA: u8 = 0x01;
const LOCK: u8 = 0x02;
const SEGIE: u8 = 0x10;

// MPUCTL1 flag bits.
const SEG1IFG: u16 = 0x0001;
const SEG2IFG: u16 = 0x0002;
const SEG3IFG: u16 = 0x0004;
const SEGIIFG: u16 = 0x0008;
const SEGIPIFG: u16 = 0x0010;

/// Open the MPU registers for writing: `0xA5` into the password lane only.
/// High-byte write, so `MPUENA`/`MPULOCK`/`MPUSEGIE` are untouched — the MPU
/// keeps enforcing while its registers are being edited.
#[inline]
fn open() {
    // SAFETY: the password byte lane exists whenever the MPU does; byte
    // writes to it are the TI-documented open sequence.
    unsafe { mpuctl0_h().write_volatile(KEY_OPEN) }
}

/// Close the MPU registers (any non-key value locks the password). Same
/// high-byte-only rule as `open`.
#[inline]
fn close() {
    // SAFETY: as `open`.
    unsafe { mpuctl0_h().write_volatile(KEY_CLOSE) }
}

/// Write the `MPUCTL0` low byte (`MPUENA`/`MPULOCK`/`MPUSEGIE`) without
/// touching the password lane. Callers must hold the bracket open — a write
/// while closed is a password violation and PUCs the chip.
#[inline]
fn write_ctl0_low(value: u8) {
    // SAFETY: byte lane of an always-present register; the password
    // discipline (open bracket) is upheld by every caller in this module.
    unsafe { mpuctl0_l().write_volatile(value) }
}

/// Read the `MPUCTL0` low byte (reads are never password-gated).
#[inline]
fn read_ctl0_low() -> u8 {
    // SAFETY: plain read of an always-present register byte lane.
    unsafe { mpuctl0_l().read_volatile() }
}

/// The five `MPUCTL1` violation flags, as one copyable snapshot.
///
/// Flags latch on violation and stay set until cleared by software
/// ([`clear_violation_flags`] / [`Mpu::clear_violations`]) — SLAU367 does not
/// promise the `SYSSNIV` read clears them (TI's own examples clear manually),
/// so this driver never relies on that.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Violations {
    bits: u16,
}

impl Violations {
    /// Main-memory segment 1 (`MPUSEG1IFG`).
    pub fn seg1(&self) -> bool {
        self.bits & SEG1IFG != 0
    }
    /// Main-memory segment 2 (`MPUSEG2IFG`).
    pub fn seg2(&self) -> bool {
        self.bits & SEG2IFG != 0
    }
    /// Main-memory segment 3 (`MPUSEG3IFG`).
    pub fn seg3(&self) -> bool {
        self.bits & SEG3IFG != 0
    }
    /// Information-memory segment (`MPUSEGIIFG`).
    pub fn info(&self) -> bool {
        self.bits & SEGIIFG != 0
    }
    /// Encapsulated-IP segment (`MPUSEGIPIFG`) — the FR5969 carries the flag
    /// even though this HAL does not drive IP encapsulation.
    pub fn ip(&self) -> bool {
        self.bits & SEGIPIFG != 0
    }
    /// Any violation latched?
    pub fn any(&self) -> bool {
        self.bits & (SEG1IFG | SEG2IFG | SEG3IFG | SEGIIFG | SEGIPIFG) != 0
    }
    /// The raw `MPUCTL1` value, for logging.
    pub fn bits(&self) -> u16 {
        self.bits
    }
}

/// A complete MPU configuration: two borders, four segment access settings,
/// and the NMI escalation switch. Installed atomically-enough by
/// [`Mpu::enable`] (segment registers first, the enable bit last).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Config {
    /// Border between segments 1 and 2: an absolute address in
    /// `0x4400..=0x14000`, 16-byte aligned. The border byte itself belongs to
    /// the *higher* segment.
    pub border1: u32,
    /// Border between segments 2 and 3; same rules, must be `>= border1`
    /// (equal ⇒ segment 2 is empty).
    pub border2: u32,
    /// Access for segment 1 (`MAIN_START..border1`).
    pub seg1: Access,
    /// Access for segment 2 (`border1..border2`).
    pub seg2: Access,
    /// Access for segment 3 (`border2..MAIN_END`).
    pub seg3: Access,
    /// Access for the Information FRAM segment (`0x1800..=0x19FF`, fixed).
    /// Guards [`crate::fram::InfoFram`]; the factory TLV block at `0x1A00` is
    /// separate silicon and not covered.
    pub info: Access,
    /// `MPUSEGIE`: escalate [`Violation::Flag`] violations to the `SYSNMI`
    /// vector. The handler demuxes with [`crate::sys::read_nmi_iv`] and must
    /// clear the flag via [`clear_violation_flags`]. Non-maskable — fires
    /// with GIE clear, and wakes any LPM.
    pub nmi: bool,
}

impl Config {
    /// The hardware's reset posture, spelled out: everything allowed,
    /// borders parked at the top. A starting point to restrict from.
    pub const fn allow_all() -> Config {
        Config {
            border1: MAIN_END,
            border2: MAIN_END,
            seg1: Access::rwx(),
            seg2: Access::rwx(),
            seg3: Access::rwx(),
            info: Access::rwx(),
            nmi: false,
        }
    }
}

/// The MPU driver. Owns the PAC peripheral (consume-by-move), so a second
/// driver instance — and with it a concurrent password bracket — cannot exist
/// without `steal()`.
#[derive(Debug)]
pub struct Mpu {
    regs: pac::Mpu,
}

impl Mpu {
    /// Create the MPU driver, consuming the PAC peripheral. No register
    /// access.
    pub const fn new(regs: pac::Mpu) -> Self {
        Mpu { regs }
    }

    /// Release the PAC peripheral (e.g. to re-`take()`-style plumbing in
    /// tests). The hardware keeps whatever configuration is installed.
    pub fn free(self) -> pac::Mpu {
        self.regs
    }

    /// Validate `config`, program it, and enable enforcement — one password
    /// bracket. Stale violation flags are cleared on the way (so a pre-reset
    /// violation cannot masquerade as a fresh one), and the segment registers
    /// are written *before* the enable bit, so enforcement never runs on a
    /// half-installed configuration. Fails with [`Error::Locked`] if
    /// `MPULOCK` is set (nothing can be changed until a BOR).
    ///
    /// Reconfiguring an already-enabled MPU is allowed but momentarily
    /// applies new borders under old access nibbles (register writes are
    /// sequential); [`disable`](Mpu::disable) first if code touching the
    /// affected segments could run concurrently (e.g. DMA).
    pub fn enable(&mut self, config: &Config) -> Result<(), Error> {
        if self.is_locked() {
            return Err(Error::Locked);
        }
        let b1 = mpu_seg::addr_to_border(config.border1)?;
        let b2 = mpu_seg::addr_to_border(config.border2)?;
        if b1 > b2 {
            return Err(Error::BordersReversed);
        }
        let sam = mpu_seg::sam_value(config.seg1, config.seg2, config.seg3, config.info);

        open();
        // SAFETY (bits): whole-register values computed by the host-tested
        // mpu_seg math; every bit pattern is architecturally valid.
        self.regs.mpusegb1().write(|w| unsafe { w.bits(b1) });
        self.regs.mpusegb2().write(|w| unsafe { w.bits(b2) });
        self.regs.mpusam().write(|w| unsafe { w.bits(sam) });
        self.regs.mpuctl1().write(|w| unsafe { w.bits(0) }); // discard stale flags
        write_ctl0_low(ENA | if config.nmi { SEGIE } else { 0 });
        close();
        Ok(())
    }

    /// Stop enforcing: clear `MPUENA` (and `MPUSEGIE`). The segment registers
    /// keep their values; latched violation flags stay latched for
    /// inspection. Fails with [`Error::Locked`] if `MPULOCK` is set.
    pub fn disable(&mut self) -> Result<(), Error> {
        if self.is_locked() {
            return Err(Error::Locked);
        }
        open();
        write_ctl0_low(0);
        close();
        Ok(())
    }

    /// Set `MPULOCK`: freeze the entire MPU register file — current borders,
    /// access rights, and enable state — until the next **BOR-class** reset.
    /// A PUC (watchdog, `force_reset`, an MPU violation itself) does *not*
    /// unlock; that asymmetry is the tamper-resistance (see module docs).
    /// One-way by design: there is no `unlock`.
    ///
    /// What happens to a register write while locked is not spelled out in
    /// SLAU367; HW-established 2026-07-05 (`mpu_test_runner` lock probe): a
    /// password-bracketed `MPUSEGB1` write is **silently ignored** — no PUC,
    /// borders unchanged. This driver refuses in software first
    /// ([`Error::Locked`]) so callers get an error instead of silence.
    pub fn lock(&mut self) {
        let current = read_ctl0_low() & (ENA | SEGIE);
        open();
        write_ctl0_low(current | LOCK);
        close();
    }

    /// Is enforcement on (`MPUENA`)?
    pub fn is_enabled(&self) -> bool {
        self.regs.mpuctl0().read().mpuena().bit_is_set()
    }

    /// Read back the installed borders as absolute addresses
    /// `(border1, border2)` — what the hardware is actually comparing
    /// against, decoded from `MPUSEGB1`/`MPUSEGB2`. Useful for verifying a
    /// configuration took (or, after [`lock`](Mpu::lock), that a write
    /// didn't).
    pub fn borders(&self) -> (u32, u32) {
        (
            mpu_seg::border_to_addr(self.regs.mpusegb1().read().bits()),
            mpu_seg::border_to_addr(self.regs.mpusegb2().read().bits()),
        )
    }

    /// Is the register file frozen until BOR (`MPULOCK`)?
    pub fn is_locked(&self) -> bool {
        self.regs.mpuctl0().read().mpulock().bit_is_set()
    }

    /// Snapshot the latched violation flags (plain read, no password).
    pub fn violations(&self) -> Violations {
        Violations {
            bits: self.regs.mpuctl1().read().bits(),
        }
    }

    /// Clear all latched violation flags. See [`clear_violation_flags`].
    pub fn clear_violations(&mut self) {
        open();
        self.regs.mpuctl1().write(|w| unsafe { w.bits(0) });
        close();
    }
}

/// Snapshot `MPUCTL1` — the thread-mode *or* ISR-side read (reads are never
/// password-gated). Free function per the ISR convention (driver structs stay
/// in thread mode; handlers use module-level free functions backed by
/// `steal()`).
pub fn violation_flags() -> Violations {
    // SAFETY: read-only access to a flag register; cannot race the owning
    // driver's writes in a way that corrupts state.
    let regs = unsafe { pac::Mpu::steal() };
    Violations {
        bits: regs.mpuctl1().read().bits(),
    }
}

/// Clear all five `MPUCTL1` violation flags — one open→clear→close password
/// bracket, safe to call from the `SYSNMI` handler.
///
/// A `SYSNMI` handler for MPU violations **must** call this (after
/// [`crate::sys::read_nmi_iv`] has identified the source): SLAU367 leaves the
/// flags' interaction with the `SYSSNIV` read unspecified, and a flag left
/// set keeps the source pending. Mind the re-lock race in the module docs if
/// thread code also brackets concurrently.
pub fn clear_violation_flags() {
    // SAFETY: ISR-side steal per the module convention; touches only the
    // flag register, under its own open/close bracket.
    let regs = unsafe { pac::Mpu::steal() };
    open();
    regs.mpuctl1().write(|w| unsafe { w.bits(0) });
    close();
}