| name | add-wch-pac-family |
| description | End-to-end workflow for adding a new WCH RISC-V chip family (CH32x / CH5xx / CH6xx) to the ch32-rs repo — start from a raw vendor SVD and produce a buildable PAC crate. Use this whenever the user mentions adding/integrating a new WCH chip, regenerating ch32-rs for a new MCU, dealing with `xmllint --schema CMSIS-SVD.xsd` validation failures on a WCH SVD, hitting `svdtools` errors like `MissingRegisterError`, or asking why the generated PACs don't use the `riscv` crate. Walks through SVD cleanup (`fix-svd-order.py`), `devices/*.yaml` authoring, `generate.sh` and `makecrates.py` registration, and the WCH-specific PFIC/Qingke gotchas. |
Add a new WCH chip family to ch32-rs
The repo's per-crate sources are autogenerated by scripts/generate.sh,
which orchestrates svdtools patch → xmllint → svd2rust → rustfmt.
This skill documents the surrounding human-driven work: prepping the SVD,
authoring the patch yaml, registering the family with the build scripts,
and the WCH-specific gotchas that don't apply to other vendors' PACs.
Naming conventions (three names, often distinct)
| Concept | Example | Lives in |
|---|
| Vendor SVD basename | CH32H417xx.svd | svd/vendor/ (CamelCase, often has xx suffix) |
| Working basename / module name | ch32h417 | svd/fixed/<chip>.svd, devices/<chip>.yaml, generated <crate_dir>/src/<chip>/mod.rs |
| Crate / family directory | ch32h4 | top-level ch32h4/ (short, groups multiple chips in a family) |
The patched SVD lives at svd/fixed/<chip>.svd.patched; that basename
must equal <chip> because svd2rust uses it to derive the Rust module
name. Don't break the equality.
Workflow
1. Drop the vendor SVD into svd/vendor/
Keep the manufacturer's original filename (e.g. CH32H417xx.svd) so future
vendor releases can be diffed cleanly. Never edit this copy.
2. Create the working copy at svd/fixed/<chip>.svd
cp svd/vendor/CH32H417xx.svd svd/fixed/ch32h417.svd
Lowercase, no xx suffix. This is what devices/<chip>.yaml references
via _svd: and what svdtools patches.
3. Fix CMSIS-SVD schema violations
WCH vendor SVDs commonly have child elements out of order. The schema
mandates a fixed sequence inside <peripheral>, <register>, and
<field>; ordering violations make schema validation fail.
Check first:
xmllint --schema svd/CMSIS-SVD.xsd --noout svd/fixed/ch32h417.svd
If it fails, run the bundled fixer (stable-sorts children into schema
order, preserving relative order of same-tag siblings):
python3 scripts/fix-svd-order.py svd/fixed/ch32h417.svd
The four common WCH violations and where the schema actually expects them:
| Element | Wrong placement | Correct position |
|---|
<alternateRegister> (register) | after <addressOffset> | before <addressOffset> |
<addressBlock> (peripheral) | after <interrupt> | before <interrupt> |
<access> (register) | after <resetValue> | between <size> and <protection> |
<access> (field) | before <bitOffset>/<bitWidth> | after them |
Element counts (peripherals / registers / fields / interrupts) must match
before and after — fix-svd-order.py only reorders, never adds or drops.
4. Strip register-prefix hungarians from dual-role names
WCH encodes "this register/field has two roles (e.g. USB device vs USB
host) sharing the same bits" as XXX__YYY. svdtools' _strip (next
step) only removes a single leading prefix, so the R8_/R16_/R32_/RB_
that sits after the __ separator survives into generated code as
ugly noise. Run the bundled normalizer to strip those inner prefixes
while keeping the __ separator (it carries the "dual-role" signal
downstream):
python3 scripts/strip-svd-dual-prefixes.py svd/fixed/<chip>.svd
Example renames:
RB_UH_DP_PIN__RB_UD_DP_PIN → RB_UH_DP_PIN__UD_DP_PIN
R8_UEP2_3_MOD__R8_UH_EP_MOD → R8_UEP2_3_MOD__UH_EP_MOD
R32_UEP2_DMA__R32_UH_RX_DMA → R32_UEP2_DMA__UH_RX_DMA
The leading prefix of the first half (R8_/RB_/R32_) is left alone —
svdtools _strip handles it in step 6. Element counts are unchanged.
Caveat: if your devices/<chip>.yaml already references the un-stripped
form (e.g. _delete: [UEP2_3_MOD__R8_UH_EP_MOD] in ch32v20x.yaml),
update those references to the new form before re-patching, otherwise
svdtools will fail with MissingRegisterError on the now-renamed entry.
5. Author devices/<chip>.yaml
This is the svdtools patch spec. Minimal template:
_svd: ../svd/fixed/<chip>.svd
"*":
_strip:
- "R8_"
- "R16_"
- "R32_"
"*":
_strip:
- "R8_"
- "R16_"
- "R32_"
- "RB_"
- " "
_strip_end:
- "_"
_modify:
name: <CHIP_UPPER>
_include:
- ../peripherals/systick_vN.yaml
What each block does:
_strip removes WCH's hungarian-notation prefixes (R8_FOO → FOO,
RB_BAR → BAR). Outer block applies to peripheral/register names,
inner "*":"*" applies to register/field names.
_modify.name sets the top-level device name in the generated PAC.
_include pulls in shared peripheral patches. Existing options in
peripherals/: systick_v2.yaml (V2 cores), systick_v3.yaml,
systick_v4.yaml, systick_v5.yaml, plus flash.yaml,
gpio_ch59x.yaml, gpio_from_sys.yaml, pioc.yaml, serdes.yaml,
usbd.yaml. Pick the systick variant matching the chip's core revision
(e.g. ch32h4 uses systick_v5).
Reference real examples: devices/ch32v30x.yaml (typical V4 chip with
includes), devices/ch56x.yaml (uses _delete to drop unused
peripherals), devices/ch32h417.yaml (uses _delete: [UHSIF] and
systick_v5).
6. Run svdtools patch and resolve errors
svd patch devices/<chip>.yaml
Error: MissingRegisterError: Could not find <PERIPH>:*
Means a peripheral in the SVD has zero <register> children and no
derivedFrom="..." attribute — so the "*":"*" rule has nothing to
match. Typical for stub peripherals the vendor declared but didn't fill
in (e.g. UHSIF on CH32H417).
Diagnose to confirm it's a stub:
python3 -c "
from lxml import etree
t = etree.parse('svd/fixed/<chip>.svd')
for p in t.findall('.//peripheral'):
name = p.findtext('name')
df = p.get('derivedFrom') or '-'
regs = len(p.findall('registers/register'))
if regs == 0 and df == '-':
print(name, 'is an orphan zero-register peripheral')
"
Fix by deleting it in the yaml (until the vendor provides registers):
_delete:
- UHSIF
This matches ch56x.yaml's existing pattern (_delete: [USBSS, SERDES, ETH]).
Other svdtools errors
Usually mean a _modify / _add rule references a name that doesn't
exist after stripping. Cross-check the names against the SVD: grep '<name>' svd/fixed/<chip>.svd | head.
7. Validate the patched SVD
xmllint --schema svd/CMSIS-SVD.xsd --noout svd/fixed/<chip>.svd.patched
Should print ... validates. A small number of chips use SVD features
beyond the schema (currently ch32v103, ch59x); those entries get
novalidate in the CHIPS array (next step).
8. Register in scripts/generate.sh
Add one row to the CHIPS array:
CHIPS=(
"<crate_dir> <chip> [novalidate]"
)
- Column 1: top-level crate dir (e.g.
ch32h4).
- Column 2: working basename / module name (e.g.
ch32h417). Must
equal the devices/<chip>.yaml basename and svd/fixed/<chip>.svd
basename.
- Column 3 (optional): literal
novalidate to skip xmllint if the SVD
uses newer schema features.
9. Register the family in scripts/makecrates.py
If this is a new family (not just another chip in an existing
family), add entries to both maps:
CRATE_DOC_FEATURES = {
"ch32h4": ["rt", "ch32h417", "critical-section"],
}
CRATE_DOC_TARGETS = {
"ch32h4": "riscv32imac-unknown-none-elf",
}
Target selection:
riscv32i-unknown-none-elf — Qingke V2 / V0 cores (rv32ec/rv32i, no
M extension). Used by ch32v0, ch641.
riscv32imac-unknown-none-elf — Qingke V3/V4/V5 cores. Used by
everything else.
Optionally bump CRATE_VERSIONS[<family>] if the family should have an
independent version from the default VERSION.
main() derives the family from the device yaml basename. The current
regex special-cases ch643, ch641, and ch5*, and falls back to
ch32[a-z]*[0-9] for everything else. New families that follow either
pattern need no code changes; truly novel prefixes do.
10. Generate the crate
./scripts/generate.sh <crate_dir>
The single-argument form processes only the matching CHIPS entry,
which is much faster than a full rebuild. No argument regenerates all
families. An unknown argument prints the list of valid crate_dir
values without touching anything.
11. Verify with cargo doc
cd <crate_dir>
cargo doc --no-deps \
--features "rt,<chip>,critical-section" \
--target <CRATE_DOC_TARGETS value>
Successful doc build means the SVD round-trips into valid Rust. Spot-check
peripherals via the rendered HTML index. If you only care about
compilation (not docs), cargo check works too.
Things that will look broken but aren't
svd2rust warns No settings file provided for RISC-V target. Using legacy interrupts rendering
This is correct for WCH — don't try to fix it by writing a settings YAML.
svd2rust 0.37's new --settings settings.yaml path emits code referencing
riscv::pac_enum, riscv::interrupt::Interrupt as CoreInterrupt,
riscv_peripheral::clint::CLINT, riscv_rt::core_interrupt, etc. —
assuming a standard RISC-V CLINT + PLIC interrupt model. WCH chips
use Qingke cores with a flat PFIC (up to 256 vectors, vectors 0–31
private/system: RESET, NMI, HardFault, Ecall-M/U, BreakPoint, SysTick0/1,
SW, IPC, HSEM, …; vectors 32+ peripherals). The mcause encoding for
exceptions also doesn't line up with WCH's vector indices.
Filling in riscv_config would generate code that calls
riscv::interrupt::* APIs operating on standard mstatus.mie / mtvec /
PLIC MMIO — wrong CSR semantics for PFIC. Legacy rendering produces
pure register access only, which is what the downstream qingke /
qingke-rt crates expect to drive.
This is also why generated Cargo.toml has no riscv dependency — the
legacy path emits zero riscv::* references, so the dep was dead weight
and was deliberately removed from CARGO_TOML_TPL.
git status doesn't show the new <crate_dir>/ as untracked
Auto-generated crate dirs are gitignored. scripts/generate.sh is the
source of truth; only edit inputs (devices/*.yaml, peripherals/*.yaml,
svd/fixed/*.svd, scripts/*). If you accidentally hand-edit a
generated file, the next generate.sh run will overwrite it.
Files touched per family (summary)
svd/vendor/<CamelCase>.svd # vendor original, manually placed
svd/fixed/<chip>.svd # working copy; may need fix-svd-order.py
svd/fixed/<chip>.svd.patched # produced by `svd patch` (gitignored)
devices/<chip>.yaml # patch spec; you author
peripherals/<shared>.yaml # shared patches; reuse existing
scripts/generate.sh # +1 row in CHIPS array
scripts/makecrates.py # +CRATE_DOC_FEATURES / _TARGETS for new family
<crate_dir>/ # entirely autogenerated, gitignored
Quick check sequence for a new chip
xmllint --schema svd/CMSIS-SVD.xsd --noout svd/fixed/<chip>.svd
python3 scripts/fix-svd-order.py svd/fixed/<chip>.svd
python3 scripts/strip-svd-dual-prefixes.py svd/fixed/<chip>.svd
svd patch devices/<chip>.yaml
xmllint --schema svd/CMSIS-SVD.xsd --noout svd/fixed/<chip>.svd.patched
./scripts/generate.sh <crate_dir>
cd <crate_dir> && cargo doc --no-deps \
--features "rt,<chip>,critical-section" \
--target <target>