Skip to content

Add datasheet metadata extraction, UNICOMM/VREF/TIMB blocks, and four register fixes - #40

Draft
CordlessCoder wants to merge 137 commits into
mspm0-rs:masterfrom
CordlessCoder:low-power-metadata
Draft

CordlessCoder wants to merge 137 commits into
mspm0-rs:masterfrom
CordlessCoder:low-power-metadata

Conversation

@CordlessCoder

@CordlessCoder CordlessCoder commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

This started as low-power metadata and went well past it. Three themes, and a suggested review order
at the end.

This PR was created with LLM assistance, but I have reviewed all of the code as it was being written, and it's proven to work in my HAL branch.

Metadata the datasheets have and the machine-readable sources don't

TI's SysConfig and SDK headers say nothing about power modes, wake-up cost, timer capability or
errata. Eight extractors under tools/ read the datasheet and errata PDFs into per-family YAML under
data/. Re-run them after a source bump rather than editing the YAML by hand. Each reports a problem
per family it cannot answer, so a bump that breaks an extraction is not silent.

tool writes answers
operating_modes.py data/operating_modes/ what is retained and usable in each power mode
timers.py data/timers/ repeat counter, deadband, QEI, counter width per timer instance
wakeup.py data/wakeup/ time to reach RUN from each sleep sub-mode
errata.py data/errata/ which functional advisories apply
vref.py data/vref/ VREF Tstartup, and whether the reference drives the VREF+ pin
adc_channels.py data/adc_channels/ which ADC channels sample an internal signal
uart.py data/uart/ which extended-UART features each UART instance implements
comp.py data/comp/ the comparator's enable time per mode and its DAC's settling figures

The ADC channel mapping is the one of these the SDK does state machine-readably, and the datasheets
still won: the SDK keys it per SDK family, and a family overstates its parts. It routes the OPA
outputs on MSPM0G110x, which has no OPA; the per-part tables are right everywhere the two disagree.
The UART features repeat the lesson per instance: sysconfig's SYS_LIN_EN is 1 on main UARTs
across most older families, against their own datasheets, SVDs and driverlib.

Alongside them, curated per-family values in data/parts.yaml that have no machine-readable origin
either: the clock ceilings, sysosc_base_hz, the flash_wait_hz ladder, adc_clock_hz,
trng_clock_hz, standby1_timers and flash_sector_bytes, each with the datasheet it came from
named in a comment.

Chip and Peripheral grow the fields to carry all of this, plus interrupts linked to their
peripherals, memory regions, NVIC priority bits and the clock tree. The doc comments in
mspm0-data-types say what each field means and when it is None. verify.rs went from 10 checks
to 24 to keep them honest. Six of the old ten only asserted power domains and are gone, since the
corrections are now applied unconditionally. It now checks that wake times are ordered by depth, that
a timer's counter count is addressable by its register block, and that every peripheral type is one
the generator knows.

PowerMode carries the STOP and STANDBY sub-mode. STOP0/1/2 and STANDBY0/1 each disable a superset
of the one before, so they order cleanly. RUN and SLEEP stay whole, because their sub-modes are
clock-source policies rather than depths: RUN2 runs the CPU with SYSOSC off where the deeper SLEEP0
has it on.

New register blocks

  • unicomm_v1 and its four mode maps (unicommi2cc, unicommi2ct, unicommspi, unicommuart)
  • vref_v1
  • tim_btimer
  • sysctl_c1105_c1106. This one exists because SLAU893 documents two C-series SYSCTLs by name, and
    the C1105/C1106 one is a strict superset: crystal drivers, MCLKCFG.FLASHWAIT and the HSCLK mux.
  • sysctl_g518x. The G518x had been borrowing the G350x block, which misses its entire 0x3000
    flash-protection region and describes a PMUOPAMP and a CANCLKSRC the part does not have.
    TI's per-family header and SVD agree it is the G351x layout minus the SRAM bank-1 registers,
    plus the USB FLL — including EXCLKSRC position 6, which the G350x/G351x SVDs leave reserved.
  • comp_v1 and comp_dacout, for the 12 families with a comparator. One layout; dacout adds
    only CTL1.DACOUTEN (8-bit reference DAC to a pin) on the four families whose headers state
    COMP_SYS_DACOUT_EN. The G350x-era SVDs put the interrupt registers at the wrong offsets and
    drop GEN_EVENT entirely; the header, both TRMs and the newer SVDs agree on 0x1020/0x1050.
    IIDX.STAT is widened to three bits: every TI source masks it 0x3 while listing value 4h.
  • crc_v1, crc_16 and crc_p, covering all 18 families. 16 drops POLYSIZE (those devices
    are CRC-16 only, per the headers' CRC_SYS_CRC32_ENABLE = 0); p adds the
    programmable-polynomial register (TI's CRCP). The data registers are plain words.

Four fixes that change which bits generated code touches

  • cpuss_v1 gave the interrupt group's MIS, ISET and ICLR one bit each. hw_cpuss.h masks all
    five registers 0xFF.
  • gpio_v1 based both generic event blocks at bit 0. GEN_EVENT1 covers DIO16 to DIO31 at bits 16
    to 31.
  • vref_v1 had a single READY bit. There are three, and they are not evenly spaced: READY0 is
    bit 0, READY1 bit 2, READY2 bit 3.
  • CRC0 was reported in PD0 on the L112x and L211x families. It is PD1.

Plus three C-series SYSCTL field offsets and the software BOOTRST encoding on
sysctl_l110x_l130x_l134x. Two more came out of the operating-mode extractor: MSPM0H3216's table has
no SHUTDOWN column, so a trailing-column heuristic was eating its STANDBY1, and a footnote marker
rendered without parentheses cost the MSPM0C1106 GPIO row its deepest column.

The rest of the register changes are shape-neutral cleanup along the lines the README already lays
out. Each cleanup was checked against the generated Rust, and the offsets, masks and shifts are
unchanged.

data/registers/README.md is new. It records two things that previously lived only in commit
messages: the evidence for each place a block deliberately departs from the SVD, and a table of what
the cleanup checks still report and why each was left alone. chiptool fmt strips comments, so that
evidence could not live in the YAML.

Generator

build-metapac formats with prettyplease in-process instead of spawning rustfmt per file. That
was 33.6s of its 34s runtime, and is now about 2.6s. ./d check builds the chips in parallel over a
shared jobserver.

Additional cleanup: the source values are gathered into one Sources struct that narrows to a
family, the data/ readers share one loader, and the peripheral-type prefix matching is a table
instead of an if chain.

Two deliberate changes. The power-domain corrections are now stated once per series rather than as
per-family name lists, which had never been extended to the newer families; that is the CRC0 fix
above. And a perimap version now means the register block exists, which verify.rs checks. RTC
previously claimed legacy and b on 21 chips with neither block curated.

Suggested review order

  1. mspm0-data-types/src/lib.rs and mspm0-metapac-gen/res/metadata.rs, as final state rather than
    as the 25 commits that touch them. This is the public API, and the doc comments are the
    authoritative statement of what None means.
  2. mspm0-data-gen/src/verify.rs. It decides how much of the rest needs auditing by hand.
  3. data/parts.yaml and data/operating_mode_overrides.yaml: datasheet values that could not be
    extracted automatically.
  4. The seven register corrections above. Small diffs, disproportionate consequence, and the evidence
    for each is in data/registers/README.md.
  5. The new register blocks, about 6.3k lines of YAML. The perimap entries are worth more attention
    than the field lists, since they decide which family gets which block.
  6. The extractors. Ignore their output: the YAML is regenerable, and each module's docstring records
    its source table and the trap it works around.
  7. The generator refactor.
  8. The shape-neutral cleanups.
  9. ./d, Cargo.toml, the READMEs, transforms/.

The extractors, the cleanups and the script changes are independent of everything else, and could be
split into a separate PR.

CordlessCoder and others added 12 commits July 29, 2026 00:57
Signed-off-by: CordlessCoder <roman@vm.net.ua>
Signed-off-by: CordlessCoder <roman@vm.net.ua>
Signed-off-by: CordlessCoder <roman@vm.net.ua>
Signed-off-by: CordlessCoder <roman@vm.net.ua>
Signed-off-by: CordlessCoder <roman@vm.net.ua>
Signed-off-by: CordlessCoder <roman@vm.net.ua>
Signed-off-by: CordlessCoder <roman@vm.net.ua>
Signed-off-by: CordlessCoder <roman@vm.net.ua>
Comment thread mspm0-data-gen/src/verify.rs
@CordlessCoder
CordlessCoder requested a review from i509VCB August 1, 2026 10:14
CordlessCoder and others added 12 commits August 1, 2026 11:20
SLAU847 contradicts itself: its Reset Causes table lists 10 as reserved and
13 as the software-triggered BOOTRST, while the RSTCAUSE field description
for this SYSCTL variant says 10, and the SVD was generated from the latter.
Every other family, and SLAU846 throughout, agree on 13.

BOOTWWDT0 was already corrected against the same table.
SLAU893 puts all three at bit 0, as do every other family's register YAML and
SVD; only the C-series SVD says bit 2. SETUSEEXLF at bit 2 means LFCLK_IN can
never be selected, since the write lands on a reserved bit.
SLAU893 documents two C-series SYSCTLs by name, SYSCTL_C1103_C1104 and
SYSCTL_C1105_C1106, and sysctl_c110x describes the first. Sharing it left
MSPM0C1105/C1106 unable to reach its crystal or its 1-wait-state flash band.

Transcribed from SLAU893 2.7 and checked field-by-field against it.
The tools/ scripts read datasheets, errata sheets and reference manuals, and
until now there was no way to obtain them.
Every instance shares tim_v1, so nothing in the register YAMLs or the SVDs
says which one has a repeat counter or a 32-bit counter. The datasheet's TIMx
configuration table does, and is the only source which is both per-device and
complete; the module docs give the counterexamples which rule out the SVDs,
the TRM and sysconfig's SYS_FLAVOR.
Generated by tools/timers.py. Nothing reads it yet.
How long a device takes to reach RUN bounds which sleep mode is usable before
a given deadline.

Note the figures are typical rather than guaranteed: the datasheet's value
cell spans its MIN, TYP and MAX columns. STOP2 is also not slower to leave
than STOP1 everywhere, since it stops SYSOSC rather than throttling it.
Generated by tools/wakeup.py. Nothing reads it yet.
@bespsm

bespsm commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Well done! but I agree with @i509VCB , maybe group the changes into specific topics (for example sysctl, adc etc) and push dedicated PRs
It would be even more safe if you provide PR in embassy with the changes based on PR in metapac, or at least test it locally.

@CordlessCoder

CordlessCoder commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor Author

or at least test it locally.

My metapac branch is what backs my HAL branch, which I've extensively tested.
https://github.com/CordlessCoder/embassy/tree/mspm0-low-power
The changes are safe, the problem is making them actually reviewable. I'll split it up into PRs as best I can, might take a couple of days because I'm still updating the metapac and hal.

TEMP_SENSE0 is one ADC code and means nothing without the trim temperature,
the sensor's slope, the sample window and the reference the factory measured
against. None of the four has a machine-readable source.

The reference varies per device across three values, and three datasheets
contradict themselves about it. The overrides file records which half hardware
agreed with.
Also reports a family with no constants, and a positive temperature
coefficient, which is what a misread MIN/TYP/MAX column looks like.
The calibration sample window is the one worth spelling out: too short but
still past settling reads plausibly and drifts, where a wrong reference is off
by hundreds of degrees. Also names the g350x/g351x pair, which is the clearest
evidence that the reference is a device fact and not a document defect.
The earlier sweep left stride out because the sources looked split. They are
not. The SVDs, the shared hw_dma.h and driverlib all describe the IP superset
and carry the stride encodings on every device, so none of the three is a
per-device statement; the header constant and the datasheet's feature table
are, and they agree on all ten families which state both.

The TRMs' DMA_A/DMA_B comparison settles nothing either way. It grants DMA_A
stride, against both per-device sources, and denies it a repeated mode which
its own transfer-mode table types full-feature.
DMA_SYS_MMR_EM reads like a switch for the whole of DMACTL.EM and is not one.
The field selects four modes and the older DMA implements three: every
datasheet carrying the feature table ticks table and fill on its full channels
whether or not the constant is defined. Only gather follows it.

Recording the constant as an extended-mode bool would have denied fill and
table to the eleven families which have them.
CTL0.PWRDN resets to automatic power down, and the TRM pays for the wake out
of the programmed sample window rather than in hardware: SCOMPx has to cover
Twakeup on top of the sampling the signal needs. A window sized for the signal
alone samples an input that has not finished waking, and nothing reports it.

Nothing machine-readable carries the figure. sysconfig only warns that it
exists and points at the datasheet.

The maximum and the typical are separate keys because the datasheets fill
different columns: fifteen families state a ceiling, three state a typical and
no ceiling at all. One field would let a typical be read as a bound on the
three that publish none. The table is unruled, so the column is decided by
comparing the figure's position against the MIN/TYP/MAX headers.
The array is based at 0x1020 with a stride of 44, so index 1 - the
generic event group - resolves to 0x104c instead of 0x1050. hw_dma.h
places DMA_GEN_EVENT.IIDX at 0x1050, and the eleven SVDs which describe
the two groups as separate clusters agree. The ten which state one
two-element array give a dimIncrement of 44, which is the block's own
size rather than the distance between instances.
SYSCTL's SVD body carries every other peripheral's clock config as
SYSCTL_MGMT_<other>_CLKCFG, twelve registers on a G350x, each with the
field. Testing for it anywhere in the body reported SYSCTL as having a
bit it does not have, on 32 of 43 chips.

No SYSCTL in the SDK defines a CLKCFG at all. The only BLOCKASYNC-family
constant there is SYSOSCCFG.BLOCKASYNCALL, which masks every peripheral
at once and is a different thing.
SAMPCLK has no published ceiling, so what bounds a conversion is the window. A
SCOMPx shorter than tSample converts an input that has not finished settling,
and nothing reports it. It adds to Twakeup rather than replacing it.

Every figure sits in the MIN column on all eighteen families. tSample and
tSample_step are one quantity: only mspm0h321x states a Vstep, and it is the
only 4.5-5.5V part, so the older documents left the step implicit at the
device's own full scale. That family states the row twice and the larger is
recorded, since 188ns is not a bound at its own rail. Figures round up, so
62.5ns stores as 63.

tSample_PGA is keyed by gain rather than by channel, and at x32 it is an order
of magnitude above the bare-pin figure. Neither of its unpublished axes is
derivable: the G datasheets print six gains and the L datasheets two, and the
curves cross; and every row is conditioned GBW = 0x1, which nothing gives a
counterpart for. Seven datasheets print the table for the four families that
have an OPA, so the map is attached only where an instance exists.
An internal signal needs far longer than a package pin: 10us for the internal
reference against a 156ns pin on mspm0l211x. A window sized for the pin samples
a signal that has not settled, and nothing reports it.

Keyed by signal in the YAML because that is how the datasheets state it, and
attached per channel because the signal reaches a different channel number on
different families and two channels on some devices.

The temperature sensor and the OPA outputs are deliberately absent, so their
None means look elsewhere rather than unpublished. The sensor states two figures
which differ and are independently absent, so no scalar can carry it; the OPA
window is keyed by PGA gain.

The tSample_VREF row states the channel it was measured on, and generation now
fails if that disagrees with the channel mapping table. One page of the datasheet
against another, and the only independent check available on a channel map.
Recorded as 64KB against a 128KB part. SLASEW9E's device comparison table,
its family-members bullet and the SDK's mspm0g1507.cmd all say 128; G1506 is
the 64KB part.
parts.yaml states flash and SRAM by hand because sysconfig describes neither,
and nothing checked either. The datasheets' device comparison table states both
per orderable part, so the transcription is verifiable after the fact.

Checks rather than writes: the rest of a part number's entry has no
machine-readable source. Entries are summed by kind, since the Gx51x parts
spend one datasheet figure over RAM and RAM_BANK.
DAC0 has been in the metadata on ten chips with an address, pins and an
interrupt, and no register block behind any of it.

Extracted from the G151x SVD. The G150x and G350x SVDs give the block an
FSUB_1 at 404h which neither hw_dac12.h nor SLAU846 Table 20-7 has, and fold
the two interrupt groups into one array where the newer ones split CPU_INT and
GEN_EVENT. Both groups have the same layout and the same IIDX values, so they
share one fieldset.
The other direction of register_blocks_exist. A peripheral with no perimap
entry is described in the metadata and cannot be addressed, and nothing said
so. Sixteen types are in that state and are listed; a new one now fails
generation rather than joining them.
The package filter cannot tell a package this part is not offered in from one
sysconfig has no entry for, so four part numbers across g150x and g5187 have
been losing a package silently. Both datasheets sell it: MSPM0G150xSDGS28R and
MSPM0G5187SDGS20R.

Reported rather than fatal - the source is incomplete, not contradictory, and
parts.yaml should keep saying what TI sells.
62 instances across fifteen families have been in the metadata with no register
block, which is why no legacy SPI driver could be written against the PAC.

Extracted from the G151x SVD, which names the three interrupt groups as
hw_spi.h does. Twelve SVDs call them INT_EVENT0-2, the C110x and MSPS003FX pair
describe only the first, and the three L SVDs drop INTCTL; all of that is SVD
incompleteness. Those parts' own device headers declare SPI0 as a full
SPI_Regs, driverlib gates none of it per device, and SLAU847's SPI register
table lists INTCTL and all three groups.

Shaped to match unicommspi_v1, which describes the same SPI function.
The note said "legacy versus B" and left two register blocks to write. The
calendar registers are byte-identical in hw_rtc.h and hw_lfss.h, at the same
absolute address, so there are not two RTCs - there is an RTC-only peripheral
and an LFSS that wraps the same group with tamper I/O, a watchdog and
scratch-pad memory.

Two things a writer would otherwise get wrong. The legacy GEN_EVENT group is
spaced 4 bytes, alone in the portfolio; hw_rtc.h and SLAU846 SS37.4 both say so
and the SVD normalises it. And RTC_B is not a block: sysconfig emits it and
LFSS as two instances at one address on five families, while four others have
an LFSS and no RTC_B node at all.
GpAmp and Event are on the list for a different reason from the other
thirteen. Neither has registers of its own: the GPAMP is configured through
SYSCTL.PMUOPAMP, which every SYSCTL block already generates, and the event
fabric is the per-peripheral FSUB/FPUB registers every block already carries.
TI ships no base address, no register struct and no driverlib module for
either.

Both are reachable today, so reading the list as fifteen pending blocks
overstates the work by two.
Two event subscriber ports, which are the whole programmable surface: the
wake behaviour itself is automatic and SLAU847 SS3.3.1.3 calls it transparent
to software. They let a generic event channel wake the device rather than an
NVIC interrupt, and had no accessor.

CHANID is four bits, per SLAU847 Table 3-26 and the range in the field's own
description. The 21 SVDs give it 2, 3, 4 or 9 and hw_wuc.h masks it 0xFF;
adc_v1 and comp_dacout already carry the same field at four.
Three families called them WUC_FSUB0/1 and eight EVENT_SUB_PORT0/1, so a
consumer matching on the name got a different answer per family.

WUC_FSUB0 is the TRM's wording rather than the datasheets', and is chosen
against the datasheet on purpose: a group member becomes a variant of the
generated Group0 enum, and this name maps one-to-one onto the register a
driver writes. Existence still comes from the datasheet, which is why c110x,
l110x and msps003fx have neither.
Group membership came from the device header's IRQn entries, with the YAML
supplying only the IIDX. IRQn names peripherals, and the wake-up controller's
two event subscriber ports are not peripherals - no header on any device lists
them, though every datasheet's interrupt table gives them an IIDX.

So both entries were dropped on all eleven families which declare them, and
nothing could tell a consumer which group member had woken it after arming
WUC.FSUB. That is the whole point of the block added in the previous commit.
DMACTL.EM value 1h was absent, so the generated enum said _RESERVED_1 for a
mode the device implements and Dma::gather_mode already documents. Reaching it
meant Em::from_bits(1).

The block was extracted from an SVD of the older generation: those twelve omit
the value and the nine newer ones enumerate it, while hw_dma.h has defined
DMAEM_GATHERMODE as 1 throughout. Second time that extraction choice has cost
this block something, after SRCWDTH/DSTWDTH.

Every other enum here was diffed against the union of all 21 SVDs by value;
this was the only gap.
The useful half is the DSSM mailbox at 1100h-110Ch: two 32-bit buffers with
flow control carrying messages between a debug probe and the target over SWD,
which SLAU847 SS35.2.4 offers for talking to application software when no other
interface is present. PWRUPIFG and PWRDWNIFG report a probe attaching and
disconnecting, which is the only way software can know it is being debugged.

Extracted from the G518x SVD rather than the widest. The C110x one adds a PINCM
array holding SWCLK/SWDIO and a CPU_CONNECT_0 at 480h; hw_debugss.h has neither
and SLAU893 Table 22-11 - the C-series TRM, for that same family - lists the
fourteen registers here and calls every other offset reserved.

TX and RX are named from the probe's side, so TXD is read-only to the CPU and
reading it is the only way to clear TXCTL.TRANSMIT. TRANSMIT and RECEIVE keep
their EMPTY/FULL enums for that reason, where the equivalent bits in spi_v1
were dropped.
"IOMUX register index" left a consumer to guess between three things: the
register address, the zero-based accessor index, and TI's own one-based PINCM
number. A host tool reading it picked the first and landed one register high on
every pin.

Also record that it cannot be computed from the pin number. That holds on 14 of
the 43 part numbers and on the other 29 the difference spans -27 to +28.
…ata lands

`wakeup_pins` is None on 30 of the 43 part numbers, so a reader who meets it
without a count has no way to tell a vendor gap from a negative answer. The
obvious fallback is the IO structure, which SLAU846 Table 8-1 gets wrong on
the two families whose open-drain pins have no wakeup logic.

Both pin maps are top-level in the JSON and per-pin in the PAC, which has
already sent a consumer looking for a field that is not there.
Four conflicts, all against changes this branch already carried:

cpuss_v1 and gpio_v1 are mspm0-rs#46 coming back. The branch has the same widths
under the cleanup pass, which merges the five interrupt fieldsets and drops
the trivial enums, so the branch side was kept. Flattening both revisions to
register offset and field bit-ranges shows no difference: 15 registers in
cpuss_v1, 62 in gpio_v1.

./d takes the Darwin job count, which supersedes the plain nproc here.
GENERATE_PERIPHERALS takes both entries.
An entry keeps permitting the absence after the block arrives, so it would
swallow a later regression that dropped it again.
This is mspm0-rs#42 coming back. Its commits were cut from this branch, which has
since carried each of them further, so every conflict takes the branch
side. The merged tree is identical to the branch before the merge.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants