Skip to content

ardave/MSP430FR5969

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

49 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MSP430FR5969

Bare-metal Rust firmware support for the Texas Instruments MSP430FR5969 microcontroller — a Peripheral Access Crate (PAC) and a Hardware Abstraction Layer (HAL) built on top of it, plus getting-started examples, a copy-out starter project, demo/consumer binaries, and a two-tier hardware-in-the-loop test suite (single-board fixtures, plus a two-LaunchPad cross-wired rig).

Both library crates are published: msp430fr5969 (docs) and msp430fr5969-hal (docs).

This is #![no_std] / #![no_main] embedded development: no RTOS, no operating system, no allocator. Startup is provided by msp430-rt — its Reset handler is the reset-vector entry point (stack pointer, .bss/.data init) and calls the #[entry] fn main() -> !. The project targets msp430-none-elf and requires the nightly toolchain (for -Z build-std=core).

Workspace layout

Crate Purpose
pac/ Peripheral Access Crate, auto-generated by svd2rust from msp430fr5969.svd. Typed register access for every peripheral.
hal/ Hardware Abstraction Layer built on the PAC: GPIO, UART, SPI, I²C (master + slave), DMA, ADC12 (single / sequence-of-channels / window comparator), timers (incl. input capture), PWM (Timer_B0 + Timer_A), RTC (calendar + alarm + prescaler ticks), watchdog, comparator, capacitive touch sensing (CAPTIO), CRC16/AES256 accelerators, FRAM MPU, clock profiles up to 16 MHz, power modes down to LPM4.5, FRAM — see the module list below. Getting-started examples in hal/examples/.
template/ Copy-out starter project depending on the published crates: minimal main.rs, the full .cargo/config.toml link recipe, and a README with toolchain setup. Not a workspace member — copy the directory out and rename it (see template/README.md).
pac_consumer/ Example binary that exercises the PAC directly.
single_board_test_firmwares/ On-target demo/test binaries that exercise the HAL (SPI loopback, I²C scan, I²C slave register file, PWM fade, RTC clock + alarm + ticks, DMA, ADC, LPMx.5, accelerators, comparator, capacitive touch, MPU, input capture, clock profiles, …).
single_board_test_orchestrators/ Host-side hardware-in-the-loop runner: talks to the flashed test binaries over the UART backchannel and asserts their verdicts (detached from the workspace; builds for the host).
two_board_test_firmwares/ One command-server fixture (two_board_fixture) flashed identically to both LaunchPads of the permanently-wired two-board rig: single-byte commands over the UART backchannel, framed report/verdict responses.
two_board_test_orchestrators/ Host-side runner for the cross-board suites — builds and flashes the fixture to both boards, discovers which is parent/child (identity in Info FRAM), and drives the suites: identity, i2c_bridge (HAL master vs. the peer's I²C slave — the slave driver's hardware verification), uart_link, gpio_edge, lpm4_wake, pwm_cross. Wiring table and setup in two_board_test_orchestrators/README.md (detached from the workspace; builds for the host).
unit_tests/ Host-target unit tests for the HAL's pure math, one module per subject — baud rate, timer ticks, FRAM addressing, ADC calibration + sequence encoding, the ISR→main RX queue, the CRC16 software model, the comparator ladder math, MPU segmentation, capture timestamp math, RTC alarm + prescaler-tick encoding, I²C slave address/IV math, and the CAPTIO control-word encoding (detached from the workspace).
tools/ Toolchain-discovery wrappers: msp430-linker.sh (finds TI's msp430-elf-gcc) and flash.sh (finds DSLite and flashes an ELF; an optional second argument selects a target-configuration ccxml, which the two-board runner uses to address each of two attached probes). Both honor env-var overrides (MSP430_GCC, MSP430_DSLITE), so no machine-specific paths are checked in.

Building

Prerequisites: the nightly Rust toolchain with rust-src (for -Z build-std=core), and — to link flashable ELFs — TI's free MSP430-GCC toolchain, found automatically via PATH, the MSP430_GCC env var, or the usual install locations (tools/msp430-linker.sh does the discovery; nothing machine-specific is checked in).

# Build (debug; the dev profile is size-tuned to fit 48 KB of FRAM)
cargo +nightly build

# Type-check only
cargo +nightly check

# Build the getting-started examples (hal/examples/)
cargo +nightly build -p msp430fr5969-hal --examples --features rt,critical-section

# Flash any built ELF (DSLite discovered via MSP430_DSLITE / PATH / CCS installs)
tools/flash.sh target/msp430-none-elf/debug/pwm_fade
# ...or let cargo's runner do the same:
cargo +nightly run --bin pwm_fade

# Run the host-side math tests (NOT on the msp430 target)
cd unit_tests && cargo +nightly test

# Run the hardware-in-the-loop suite (device flashed with the matching runner)
cd single_board_test_orchestrators && cargo +nightly run            # default suite
cd single_board_test_orchestrators && cargo +nightly run -- spi     # named suite

# Run the cross-board suites (both boards of the two-board rig attached)
cd two_board_test_orchestrators && cargo +nightly run

To start your own out-of-tree project against the published crates, copy the template/ directory. Per-demo board wiring lives in CLAUDE.md; the two-board rig's wiring table lives in two_board_test_orchestrators/README.md.


PAC crate

The pac crate (pac/) is generated by svd2rust (v0.37.1, --target msp430) from the device SVD file. It provides typed, read/modify/write register access for all MSP430FR5969 peripherals, a Peripherals::take() singleton, and (under the rt feature) the interrupt vector table plus the memory.x/device.x linker scripts. pac/src/lib.rs is ~73K lines of generated code and should not be edited by hand — fixes are made by patching the SVD and regenerating. Regeneration is reproducible (last done 2026-07-11, picking up the MPU block): the msp430 target flavor emits the vector table correctly at source — u16 slots and extern "msp430-interrupt" handler declarations, matching the ABI msp430-rt's #[interrupt] macro emits — leaving one small hand-patch to re-apply, gating the nightly ABI feature and the Vector union on the rt feature so non-rt builds don't need it (see CLAUDE.md, "PAC generation").

TODO

Discrepancies found between the MSP430FR5969 datasheet and the generated PAC. Full context for each item — datasheet references, SVD locations, and fix notes — lives in TODO_pac_datasheet_discrepancies.md.

  • High impact (both resolved)
    • Timer_A0 is missing 2 capture/compare channels — datasheet erratum, not an SVD gap (HW-established 2026-07-11). TA0 on this silicon really has exactly 3 CC channels; SLAS704G's TA0 register-offset table (which lists TA0CCR3/4) contradicts its own prose and Table 6-13, and a one-off silicon probe found no state, no CCIFG, and no IV slot at the putative addresses. The SVD, PAC, and Timer0_A3 name are all correct.
    • MPU defined in SVD but missing from PAC — resolved by the 2026-07-11 regeneration. svd2rust never dropped anything; the old PAC had been generated from an earlier SVD than the one checked in. The PAC (v0.2.0) now has the mpu module at 0x05A0 and Peripherals.mpu, and hal::mpu owns pac::Mpu with typed register access. (MPUCTL0 deliberately stays raw byte lanes — the SVD doesn't model the MPUPW password byte, so a PAC modify on it would write a wrong password and PUC.)
  • Low impact (cosmetic / modeling differences)
    • Capacitive Touch I/O base address is the register, not the module. The PAC uses 0x043E/0x047E (the CAPTIOxCTL register address) as the base instead of the datasheet's module base 0x0430/0x0470. Functionally correct since there is only one register.
    • Hardware multiplier split into two PAC peripherals. The datasheet documents one unified MPY module at 0x04C0; the PAC models it as Mpy16 (0x04C0) and Mpy32 (0x04D0). Different organization, same registers.
    • DMA channels flattened into a single peripheral. The datasheet splits DMA into a general-control block plus per-channel bases; the PAC combines everything into one Dma peripheral at 0x0500.
  • Code review follow-ups
    • Interrupt handlers use the wrong ABI. The old checked-in PAC's vector-table declarations were extern "C"; MSP430 ISRs need extern "msp430-interrupt" (RETI, not RET). Resolved at source by the 2026-07-11 regeneration (the msp430 target flavor emits the correct ABI); real ISRs are in use and hardware-verified against the regenerated table.
    • rt path is wired to msp430-rt. Previously latent-broken: the PAC emits the table into .vector_table.interrupts but the old memory.x kept *(.vector_table) (no match) and emitted no .reset_vector/__RESET_VECTOR. Resolved by depending on msp430-rt (its link.x supplies the SECTIONS and reset glue and INCLUDEs memory.x/device.x) and reducing memory.x to RAM/ROM/VECTORS regions; the Vector union's u16 slot width (a u32 there doubled the table width and overran VECTORS) is now emitted at source by the regeneration. Binaries link and Reset begins with mov #0x2400, r1.
    • Packaging metadata is absent (high). Resolved: pac/Cargo.toml (and hal/Cargo.toml) now carry license, description, repository, documentation, keywords, categories, and [package.metadata.docs.rs] (default-target msp430-none-elf, docs built with the rt/critical-section features and build-std). Both crates are published on crates.io (v0.2.0, 2026-07-11/12) with green docs.rs builds.
    • static mut DEVICE_PERIPHERALS uses the old singleton idiom (medium). Newer svd2rust emits an AtomicBool; the current form will warn under the static_mut_refs lint. The 2026-07-11 regeneration with v0.37.1 (msp430 flavor) still emits this form, so a plain regen does not fix it.
    • eUSCI UART-mode block omits UCAxIE/UCAxIFG (low). These registers exist in silicon but aren't modeled, which is exactly why the HAL has to drop to raw pointers for IFG. Patch the SVD so the PAC exposes them.

HAL crate

The hal crate (hal/) is a hand-written abstraction layer on top of the PAC. It re-exports pac, embedded_hal, embedded_hal_nb, embedded_io, and embedded_storage, and passes the rt and critical-section features through to the PAC. The crate-root hal::peripherals::take(watchdog::WdtMode) is the boot front door: watchdog policy + Peripherals::take(), fused in guaranteed order. Implemented modules (hardware-verified on the MSP430FR5969 LaunchPad unless noted otherwise):

  • gpio — zero-sized, typestate-typed pins (Pin<PORT, N, MODE>) with the embedded-hal digital traits, mode transitions, and port edge interrupts (ISR-side gpio::read_iv).
  • serial — eUSCI_A0/A1 UART (embedded-hal-nb + embedded-io), with a datasheet-derived baud generator, RX interrupts into an application-owned queue, and DMA-paced TX/RX.
  • spi — instance-generic 3-pin SPI master over eUSCI_A0/A1/B0 (embedded-hal SpiBus), plus SpiDma (two-channel DMA pacing).
  • i2c — eUSCI_B0 I²C master (embedded-hal I²C traits) and slave (native event-pump API, hardware SCL stretching; hardware-verified 2026-07-18 via the two-board rig's i2c_bridge suite in polled mode — the UCB0IV table and the interrupt-driven path remain documentation-pinned).
  • dma — 3-channel DMA controller: safe CPU-halting block copies, the peripheral-pacing primitives serial/spi/adc build on, and the DMA-vector demux dma::read_iv.
  • adc — ADC12_B: ratiometric and VREF-referenced reads, conversion-complete interrupts, repeat-mode conversions drained by DMA (including the workaround for an undocumented ADC12→DMA trigger-latch erratum), hardware sequence-of-channels scans (≤ 8 members, per-member reference selection, polled or DMA-drained — encoding math in adc_seq, host-tested), and the window comparator (threshold checks in silicon: one-shot windowed reads and a free-running zero-CPU monitor with above/below/within interrupts).
  • ref_a / tlv / adc calibration — REF_A shared 1.2/2.0/2.5 V reference (incl. REFOUT onto P1.1), factory TLV calibration constants, and the full gain/offset/reference correction chain for absolute-mV and die temperature readings.
  • comp_e — Comp_E analog comparator: typed channel pins, reference-ladder thresholds with hysteresis (comp_ladder math), both-edge interrupts via comp_e::read_iv, LPM wake.
  • crc / aes — CRC16-CCITT and AES256 hardware accelerators: both CRC bit conventions plus the four catalog one-shots (host-tested software model in crc_soft); AES ECB/CBC with transparent key reload across encrypt↔decrypt switches.
  • timer / pwm — Timer_A free-running counter/tick math; PWM (embedded_hal::pwm::SetDutyCycle) with glitch-free 0/100 % endpoints on Timer_B0 (four pins) and, instance-generically, on Timer_A0/A1 (PwmTimerA — frees TB0 and the P1.6/P1.7 eUSCI_B0 pin conflict).
  • capture — Timer_A input capture, instance-generic over TA0/TA1: hardware edge timestamps from typed pins (CCIxA), the internal Comp_E output (CCI1B) and ACLK (CCI2B — hands-free DCO-vs-crystal measurement), software-triggered captures, overcapture (COV) semantics split between paired reads and span bracketing (capture_math, host-tested), and the TAxIV demux.
  • captio — Capacitive Touch I/O: any port pad becomes a relaxation oscillator whose frequency tracks the pad's capacitance, counted by the instance's silicon-paired internal timer (CAPTIO0→TA2, CAPTIO1→TA3, both consumed so the pairing can't be crossed). Typed-pin routing and re-routing scans, gated frequency measurement with a wrap refusal instead of aliasing (bare pads oscillate at 3–7 MHz — measured), raw count deltas as the touch primitive, and a count-overflow interrupt that wakes LPM0 (the oscillator is self-clocked). Control-word encoding host-tested in captio_ctl.
  • rtc — RTC_B calendar (native API): RTCRDY-gated reads, event/second interrupts, the hardware alarm (per-field minute/hour/weekday/day enables, set_alarm with SLAU367's disable-first/clear-flag-last update order, polled or RTCIV-demuxed interrupt delivery that wakes LPM3 — encoding math in rtc_alarm, host-tested), sub-second crystal-accurate prescaler ticks (sixteen power-of-two rates, 16.384 kHz–0.5 Hz, LPM3 wake — rate math in rtc_tick, host-tested), and Rtc::attach for reclaiming the calendar after an LPM3.5 wake.
  • clocks / delay — clock-system (CS) configuration: four profiles (8 MHz SMCLK default, 1 MHz low-power with LFXT crystal startup, and two 16 MHz DCO profiles whose FRAM wait-state (NWAITS) programming is fused with the DCO switch so the ordering can't be gotten wrong), and a cycle-counted busy-wait delay.
  • power / sys — LPM0/3/4 entry (GIE set atomically with sleeping) and the regulator-off LPM3.5/LPM4.5 modes with FRAM-persisted state and sys::ResetReason wake-cause reporting.
  • fram — FRAM controller support with embedded-storage traits and overflow-safe bounds checks (fram_addr).
  • mpu — FRAM Memory Protection Unit: three movable main-memory segments plus the fixed Info segment, per-segment R/W/X enables with flag/NMI/PUC violation policies, and lock() (frozen until BOR). Owns the PAC's Mpu block (consume-by-move) with typed 16-bit register access; only MPUCTL0 stays raw byte lanes, because the SVD doesn't model its MPUPW password byte. Border and MPUSAM math host-tested in mpu_seg.
  • watchdog — WDT_A: disable/feed/force-reset plus interval-timer mode (periodic WDT-vector interrupt).
  • rx_queueCell-based SPSC byte queue for ISR→main handoff (host-tested).

TODO

Remaining coverage and follow-ups from code review.

  • Open
    • Hardware-confirm the I²C slave UCB0IV table and interrupt-driven mode. The slave itself is now hardware-verified in polled mode (see Done below), but the UCB0IV slot values are still documentation-pinned only (the RTCIV table was wrong once already) and the interrupt-driven slave path has not run on silicon.
    • Timer_B0 input capture. capture is instance-generic over TA0/TA1 only; TB0's seven CCRs also do capture. Low urgency — the Timer_A coverage serves the use cases, and TB0 is the main PWM block.
    • ADC12 differential-pair mode (ADC12DIF) and the sequence auto-restart (CONSEQ=3) variants.
    • eUSCI SPI slave mode — all three SPI instances are master-only.
    • I²C multi-master and 10-bit addressing — the master is single-master 7-bit; the slave answers one 7-bit own address (the silicon offers three more own-address slots plus an address mask).
    • UART automatic baud-rate detection (UCABDEN) and IrDA framing.
    • embedded-hal-async — an async story would be a genuine differentiator (MSP430 has no atomics, so a critical-section-based waker design), but it is a large bet; polling + interrupts serve today.
  • Done (coverage milestones)
    • Capacitive Touch I/Ocaptio::TouchSense (routing, gated frequency measurement, LPM0-waking overflow interrupt), host-tested encoding in captio_ctl, and the hands-free captio HiL suite. All eight verdicts hardware-verified 2026-07-11 (the cosmetic PAC base-address quirk noted above stands — the driver addresses the one register directly).
    • I²C slave mode — implemented (i2c::I2cSlave, a native event-pump API: own-address setup with general-call support, hardware SCL stretching, repeated-START turnarounds, speculative-TX-byte flush at STOP, UCB0IV demux; pure math host-tested in i2c_slave). Hardware-verified 2026-07-18 via the two-board rig's i2c_bridge suite — the HAL's own master on the peer board ran probe/ID/write-read/read-only/ wrap with an exact 8-transaction event tally, including the speculative-TXBUF flush and clock stretching (which exposed and fixed a master-side zero-length-write STOP wedge). The UCB0IV table and interrupt-driven mode are the residual open item above.
    • Two-board integration rig — two LaunchPads permanently wired together (10 signal wires through series resistors, fixed pin directions so contention is impossible), giving every suite a genuinely independent partner: a second clock domain, a second calibrated analog chain, real external edges, and a live bus master. All six default suites hardware-verified 2026-07-18 (identity, i2c_bridge, uart_link, gpio_edge, lpm4_wake, pwm_cross — cross-DCO PWM measured 991/1009 Hz against a 1 kHz ±5% gate).
    • Timer capture modecapture implements Timer_A input capture (TA0/TA1 generic; pin, Comp_E, ACLK, and software sources). Hardware-verified 2026-07-08 via the capture suite (the two jumper-dependent PWM verdicts run in the interactive capture_jumper variant).
    • High-speed clock profiles — the two 16 MHz DCO profiles (configure_high_speed, configure_max_speed incl. the fused FRCTL0.NWAITS wait-state programming) hardware-verified 2026-07-08, each under its own default-set suite.
    • MPU write-protectionmpu implements the FRAM MPU (hardware-verified 2026-07-05; all eight verdicts re-verified 2026-07-11 through the typed driver after the PAC regeneration added the Mpu block).
    • Timing/Clocksclocks (CS driver incl. LFXT), delay, and timer are implemented and hardware-verified.
    • I2c (master) — eUSCI_B0 master implemented (embedded-hal I²C traits).
    • SPI — eUSCI_A0/A1/B0 master implemented, including DMA pacing.
    • FRAM — implemented with embedded-storage traits.
  • Code review follow-ups
    • Shared-register read-modify-writes are not atomic (high) — partially addressed. Interrupt-related registers (PxIE, PxIES, PxIFG, shared SFRIE1) are now RMW'd only inside critical_section::with. Data/direction registers (PxOUT, PxDIR, PxSELx) still use plain volatile read/modify/write, which races if a pin is driven from an ISR while main touches another pin on the same port.
    • TX/RX pins aren't owned, so two drivers can claim the same pins (high). into_uart muxes the P2 PxSEL bits directly but never consumes the matching Pin objects. (SPI/PWM/comparator pins do go through the typestate; the UART still has this hole.)
    • The HAL duplicates the memory map the PAC already owns (medium). GPIO/serial/SPI hardcode absolute addresses (0x0200, 0x05C0, …) that the PAC already encodes. Derive bases from the consumed peripheral so a PAC regen can't silently desync them. (Partly forced by the PAC's missing UCAxIE/UCAxIFG modeling, above.)
    • flush waits on UCBUSY, which is also set during RX (medium). A flush() issued mid-reception blocks on unrelated RX traffic. Conventional for eUSCI (no dedicated TX-shift-complete flag), but worth a doc note.
    • Missing crate-level lints and # Safety docs (low). Add #![deny(missing_docs)] / #![warn(unsafe_op_in_unsafe_fn)] and document the invariants on the unsafe fn MMIO helpers.
    • No free()/release path (low) — mostly done. free(self) -> PAC_PERIPHERAL now exists on timer, pwm, rtc, adc, ref_a, aes, crc, comp_e, watchdog, and i2c::I2cSlave; serial, gpio, and the I²C master remain one-way.
    • Pin/Serial Send/Sync are left to auto-trait inference (low). They alias global MMIO yet derive Send + Sync — reason about these deliberately.
    • Pin<PORT, const N: u8, MODE> has no 0..=7 bound (nit). Safe today because the constructor is private, but 1 << N would wrap for N >= 8 with overflow checks off.
    • compute_baud lacks guards for baud == 0 / baud > clock_freq (nit). Add debug_assert!s on the precondition.

About

PAC + HAL for Texas Instruments MSP430FR5969

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages