Skip to content

feat: V3F/V5F (CH32H4) dual-core support - #23

Open
andelf wants to merge 11 commits into
mainfrom
ch32h417-dualcore
Open

feat: V3F/V5F (CH32H4) dual-core support#23
andelf wants to merge 11 commits into
mainfrom
ch32h417-dualcore

Conversation

@andelf

@andelf andelf commented May 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the QingKe pieces needed to bring up CH32H4 dual-core MCUs (V3F as boot hart 0, V5F as hart 1). Companion to ch32-rs/ch32-hal H4 work.

  • qingke-rt: new v3f / v5f cargo features that select the right boot stub and ISR table per hart
  • qingke::pfic: HartId + wake_other_core helpers (gated behind a dual-core feature so single-hart V3 / V4 builds keep their existing surface)
  • qingke::register: typed wrapper for inestcr (interrupt-nest control), gated behind a _inestcr feature; `setup` now calls into it instead of a raw CSR write
  • qingke::register: V5-only ICache CSRs cache_strtg_ctlr / cache_pmp_ovr / opcache_ctlr so H4's V5F hart can configure its icache
  • pfic::disable_vtf: wrap the inner ptr ops in `unsafe { }` to satisfy Rust 2024's `unsafe_op_in_unsafe_fn` lint

Test plan

  • `cargo check --no-default-features --features _v3,unsafe-trust-wch-atomics` clean
  • `cargo check --no-default-features --features _v5,unsafe-trust-wch-atomics` clean
  • Downstream ch32-hal H4 worktree (`feat/h4-afio`) consumes this branch via local `[patch]` and the `examples/ch32h417` blinky compiles
  • Hardware bring-up on CH32H417EVT board (V3F boot, V5F core wake) — pending companion ch32-hal PR

Notes

  • The atomics guard (`compile_error!` unless `unsafe-trust-wch-atomics`) is still in place; we currently dodge it on H4 by using `+forced-atomics` in the example's target JSON. Verification plan for hardware atomics on V3F / V5F is documented in the ch32-hal-side `docs/atomic-verification.md`.

andelf added 9 commits May 15, 2026 06:00
Groundwork for CH32H417 dual-core (V3F + V5F) support.

`HartId::current()` decodes `PFIC_SCTLR[23:16]` (HART_ID field) into
`C0` / `C1`. Safe to call on any QingKe chip with a PFIC — single-core
chips always read 0 (`C0`), matching the documented reserved-bit
behavior.

`wake_other_core(entry)` is the Rust equivalent of WCH SDK's
`NVIC_WakeUp_V3F` / `NVIC_WakeUp_V5F`: writes the 1KB-aligned entry PC
into the other hart's `PFIC_WAKEIPx` (which also clears its
`SHUTDOWN_x` bit), then sets `PFIC_SCTLR.SENDEVENT` to deliver the
wake event. On single-core chips the WAKEIP write hits a reserved
register and is a no-op.

Register addresses verified against the CH32H417 Reference Manual
V1.7, §4.7.5.51-52 and §4.7.5.58.

No feature gate: HartId/wake_other_core are tiny and harmless on
single-core chips; gating them would add friction without value.
The existing _v3 setup_interrupts path only OR's a few bits into
mstatus and leaves corecfgr (0xBC0), nest level (0xBC1), and intsyscr
(0x804) at reset defaults. WCH's startup_ch32h417_v3f.S explicitly
writes all four CSRs with non-default values, so reusing _v3 verbatim
for V3F would leave the core under-initialized.

Add a parallel `v3f` feature (mirroring the v3a / v3b topology, pulls
in _v3) and a dedicated setup_interrupts branch that performs the
full CSR sequence taken from startup_ch32h417_v3f.S lines 541-551:

  corecfgr (0xBC0) = 0x123703E1   # pipeline + branch prediction
  nest level (0xBC1) = 0x01       # 2-level interrupt nesting
  intsyscr (0x804) = 0x07         # hwstk + nesten + global ihwstk
  mstatus           = 0x6088      # FS=Dirty, MPP=U, MPIE=1, MIE=1

The existing _v3 block is gated to `not(v3f)` so exactly one branch
runs per build. mtvec setup and the post-block FP-enable logic are
unchanged - they handle V3F correctly as-is. FS gets downgraded from
Dirty to Initial by the riscvf/d post-block, which is fine: the first
FP register write will bump it back to Dirty automatically.

Verified clean build for v2 / v3a / v3b / v3f / v3f+u-mode+highcode
/ v4 on riscv32imac-unknown-none-elf.
Mirrors the v3f topology: parallel `_v5` umbrella + concrete `v5f`
feature that pulls in qingke/v5f. The setup_interrupts branch writes
the CSR sequence taken from startup_ch32h417_v5f.S:481-491:

  corecfgr (0xBC0) = 0x1237B3E0   # FP clk divs + NLP_EN + reserved
  nest level (0xBC1) = 0x07       # NEST_LVL = 8 (max for V5)
  intsyscr (0x804) = 0x0F         # hwstk + nesten + pmtcfg=11
  mstatus           = 0x6088      # FS=Dirty, MPP=U, MPIE=1, MIE=1

The fallback path's `not(any(...))` list is extended with `_v5` so
that v5f-only builds don't fall through into the v4 setup.

ICache + PMP setup that the WCH SDK does immediately after this
sequence (lines 498-521 of the same .S, using `_cache_beg` /
`_cache_end` linker symbols) is intentionally NOT included: it
requires user linker-script symbols and is better delivered as a
separate opt-in feature later.

Removed the stale `# v5 is not released yet` comment in
qingke-rt/Cargo.toml.

Verified clean build for v2 / v3a / v3b / v3f / v4 / v5f and the
v3f+u-mode, v5f+u-mode, v5f+highcode combinations on
riscv32imac-unknown-none-elf.
`PFIC_WAKEIPx` (0xE000E720 / E724) is not documented in any generic
QingKe IP manual (V2 / V3 / V4 / V5); it is a chip-level multi-core
extension currently described only in the CH32H417 RM
§4.7.5.51-52. Writing those addresses on chips that don't decode
them is undefined behavior — the bus may hit a different register or
fault.

`HART_ID` at PFIC_SCTLR[23:16] is similarly only defined in the
QingKe V5 IP manual (V2 / V3 / V4 leave that field reserved).

Gate `HartId`, `wake_other_core`, and the `PFIC_WAKEIP0/1` constants
behind a new `dual-core` Cargo feature so single-core HAL crates
cannot accidentally call them. `qingke/v3f` and `qingke/v5f` do not
auto-imply `dual-core` — a downstream chip-specific HAL must opt in
explicitly when it wants the multi-core API.

Existing build matrix (v2 / v3a / v3b / v3f / v5f / v4) still passes;
also verified v3f+dual-core, v5f+dual-core, and dual-core alone.
`inestcr` (CSR 0xBC1) controls the interrupt nest depth on QingKe
V3F and V5F (and earlier V3 silicon, though the field set is sparser
there). Until now the qingke-rt startup paths wrote it via raw
`csrw 0xBC1, imm` because there was no typed wrapper.

The new module exposes:
- `Inestcr` view type (mirrors `intsyscr.rs` style: struct with
  `bits: usize`, accessors per field).
- `NestLevel` enum for `NEST_LVL[2:0]` (Disabled .. Eight); V3 only
  supports up to `Two`, V5 covers the full range.
- `nest_depth()` decoding the unary-encoded `NEST_STA[11:8]` to a
  plain integer (0 .. 8).
- `read()` via `read_csr_as!`, `write()` raw, `set_max_level()`
  read-modify-write helper, and W1C helpers `clear_nest_overflow()`
  / `clear_lsu_nmi()` for the two status latches.

Field layout taken from QingKe V5 IP manual §8.1. V3 manuals
describe a subset; the V5-derived wrapper is a superset and the
extra accessors just return reserved-zero bits on V3, which is
harmless.

No call sites updated yet — qingke-rt's setup_interrupts still uses
raw asm for now (changing it would touch the V3F/V5F asm blocks and
make the diff harder to review). A follow-up commit can migrate.
…etup

CSR 0xBC1 (inestcr / interrupt nest control) is documented only in
the QingKe V5 IP manual; the V2 / V3 / V4 generic manuals do not
describe it, and accessing it on those cores raises an
illegal-instruction trap. The CH32H417 V3F variant wires it up the
same way (its startup file writes 0xBC1 directly).

Introduce an internal `_inestcr` umbrella feature pulled in by `v3f`
and `_v5` only, and gate `pub mod inestcr` on it. V2 / V3A / V3B /
V4 builds no longer see the module at all, preventing accidental
misuse on cores that lack the CSR.

Migrate qingke-rt's V3F and V5F setup_interrupts paths to use
`inestcr::write(NestLevel::Two as usize)` / `NestLevel::Eight as
usize` instead of inlining `li t0, 0x01; csrw 0xBC1, t0` in the
asm! block. The typed form documents the intent and the discriminant
of NestLevel matches the hardware encoding bit-for-bit
(Two=0b001=1, Eight=0b111=7), so the generated code is identical.

Verified clean build for v2 / v3a / v3b / v3f / v4 / v5f and the
v3f+u-mode, v5f+u-mode, v5f+highcode, v3f+dual-core, v5f+dual-core
combinations on riscv32imac-unknown-none-elf.
…/ opcache_ctlr)

Three CSR modules covering the QingKe V5 instruction-cache control
plane, all gated behind `_v5` (already implied by the `v5f` feature):

- `cache_strtg_ctlr` (CSR 0xBC2): the master ICache enable
  (`ic_disable`, bit 1) plus four per-address-space cacheability
  bits (`ic_code_strtg`, `ic_sram_strtg`, `ic_mem0_strtg`,
  `ic_mem1_strtg`). Provides typed `CacheRegion` enum and atomic
  set/clear helpers (`enable_icache`, `enable_region`, etc.).

- `cache_pmp_ovr` (CSR 0xBC3): per-PMP-channel override of the
  address-space policy. Typed `PmpChannel` enum exposes the four
  channels (PMP0 at bit 0, PMP1 at bit 4, PMP2 at bit 8, PMP3 at
  bit 12).

- `opcache_ctlr` (CSR 0xBD0): write-only maintenance register.
  Provides `Opcode::Invalidate`, `AddressMode::{Address,Index}`,
  plus convenience `invalidate_all()` (matches the `csrw 0xBD0, 0x4`
  startup pattern from CH32H417 SDK) and `invalidate_addr(vaddr)`.

Field layouts from QingKe V5 IP manual §8.1. None of these have
call sites yet — they are the building blocks for the upcoming
`v5f-icache` opt-in feature that will set up ICache + PMP regions
in `qingke_setup_interrupts` for CH32H417 V5F builds.

Verified clean build for qingke + qingke-rt across v2 / v3a / v3b /
v3f / v4 / v5f and the u-mode / highcode / dual-core combinations
on riscv32imac-unknown-none-elf.
The QingKe family selection features (v2 / _v3 / v4 / _v5, plus their
concrete leaves v3a/v3b/v3f/v5f) drive mutually-exclusive startup asm
in qingke-rt. Before this guard, enabling e.g. v3f together with v5f
would silently emit both `qingke_setup_interrupts` asm blocks and the
second one's mstatus/intsyscr writes would clobber the first.

The new compile_error fires on any cross-family pair while still
allowing intra-family combinations like v3a+v3b (which share the same
startup path). Catches the H4 dual-core scenario where a workspace
might accidentally feature-unify both V3F and V5F into one build.

Verified:
- v2 / v3a / v3f / v4 / v5f alone: still build
- v3a+v3b (same family): still builds
- v2+v3a / v3f+v4 / v3a+v5f / v4+v5f / v3f+v5f / v2+v4: all blocked
@andelf

andelf commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

Review follow-up

Issue #1 — mutual-exclusivity guard (now fixed)

Pushed 3e4d648 guard: forbid enabling two core-family features at once. The new compile_error! in src/lib.rs blocks all 6 cross-family pairs (v2/_v3/v4/_v5) while still allowing intra-family combos like v3a+v3b (which share the same startup path).

Verified locally:

  • 5 single-family builds (v2, v3a, v3f, v4, v5f) — all clean
  • 6 cross-family pairs (v2+v3a, v3f+v4, v3a+v5f, v4+v5f, v3f+v5f, v2+v4) — all hit the new error
  • v3a+v3b — still builds (same family, harmless overlap)

Field/CSR cross-check vs. WCH SDK headers

Reviewer asked for human verification of several register layouts; I went through the WCH SDK headers and startup_ch32h417_v{3,5}f.S and everything in this PR matches:

Item qingke claim SDK source OK
PFIC_WAKEIP0/1 0xE000E720 / 0xE000E724 core_riscv.h:161 (PFIC base 0xE000E000) + struct offset 0x720
PFIC_SCTLR 0xE000ED10 same base + struct offset 0xD10
HART_ID source SCTLR & (1 << 16) core_riscv.h:556 does exactly that
V3F corecfgr 0x123703E1 startup_ch32h417_v3f.S:541
V5F corecfgr 0x1237B3E0 startup_ch32h417_v5f.S:481
V3F inestcr NestLevel::Two = 0x01 startup_ch32h417_v3f.S:544
V5F inestcr NestLevel::Eight = 0x07 startup_ch32h417_v5f.S:484
V3F intsyscr (0x804) 0x07 startup_ch32h417_v3f.S:548
V5F intsyscr (0x804) 0x0F startup_ch32h417_v5f.S:488
mstatus 0x6088 (both cores) startup :550 / :491
ICache CSR cache_strtg_ctlr 0xBC2 CPU/ICache/.../startup_ch32h417_v5f.S:521 (csrc 0xbc2)
ICache CSR cache_pmp_ovr 0xBC3 same startup :510 (csrw 0xbc3)
ICache CSR opcache_ctlr 0xBD0 same startup :517 (csrw 0xbd0)

Still pending hardware-side verification

  • mhartid behavior on CH32H417 — the WCH SDK relies on PFIC_SCTLR[16] for hart-id detection, but I haven't booted both harts to confirm mhartid reads 0 on both. The PR's HartId::current() uses SCTLR, so this is informational rather than blocking.
  • wake_other_core race when secondary is mid-reset — needs a hardware repro to truly validate; the existing debug_assert on 1KB alignment is the only soft check today.

Minor items from the review left for follow-up (not in this PR)

  • Atomics guard wording still says "As tested on QingKe V4" — generalising to mention V3F/V5F can be a one-liner in a separate commit.
  • inestcr::from_status doc says "trailing ones plus 0" — drop the redundant phrase.
  • HartId doc references the single-core read behaviour even though the type is now gated behind dual-core.
  • V5F boot path doesn't invalidate/enable ICache; reset state has it disabled so this is functionally safe, but worth a tracking issue for a future v5f-icache feature.

Happy to fold any of the doc/wording cleanups in here if reviewers prefer one PR.

mhartid is also a valid hart-id source on CH32H417 (V3F=0, V5F=1) per
WCH SDK FreeRTOS port. Note both options in the HartId doc so readers
don't infer that mhartid is broken; explain why we pick SCTLR (PFIC-
based convention is more portable across future dual-core QingKe parts).
@andelf

andelf commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

Correction on mhartid (re: my earlier comment)

I claimed mhartid behavior on CH32H417 was "informational rather than blocking" and that it might read 0 on both harts per WCH convention. That was wrong — the SDK itself proves otherwise:

  • EVT/SRC/Core/core_riscv.c:369-377 exposes __get_MHARTID() as a public API, doing exactly csrr mhartid
  • More tellingly, EVT/EXAM/CPU/OS/FreeRTOS/.../port.c:141 (in BOTH the V3F and V5F FreeRTOS ports) reads mhartid and uses it to index pullMachineTimerCompareRegister[hartid * 8] — that only works if V3F reads 0 and V5F reads 1

So on CH32H417 mhartid follows standard RISC-V semantics: V3F (boot hart) = 0, V5F (secondary) = 1. It's an equivalent hart-id source to PFIC_SCTLR[16].

Pushed 5973481 docs(pfic): clarify HartId vs RISC-V mhartid on CH32H417 to update the HartId doc accordingly. The implementation still reads SCTLR (PFIC-based convention is documented at the QingKe IP level and should port to future dual-core QingKe parts even if their mhartid story differs), but the doc now tells readers that mhartid is also a fine choice on this specific chip.

The function name and doc both claim "Write to corecfgr", but the
inline asm emitted `csrs 0xBC0` — an OR-style set, never a true
register write. Callers using `corecfgr::set_default()` (which forwards
to `write(0x1f)`) silently get OR-with-0x1f instead of an unconditional
write to 0x1f, leaving any previously set bits intact rather than
matching the WCH EVT SDK startup which does `csrw 0xBC0, 0x1f`.

Drive-by while landing the V3F/V5F startup work: the new v3f/v5f asm
blocks already bypass this helper with raw `csrw 0xBC0`, so no caller
in qingke or downstream (ch32-hal) depends on the old OR semantics
(verified via grep across the WCH workspace — zero hits).
andelf added a commit to ch32-rs/ch32-hal that referenced this pull request Aug 20, 2026
- Pin qingke/qingke-rt patches to ch32-rs/qingke#23 rev 6043830
  instead of machine-local paths
- Delete dead link-v5f.x (build uses qingke-rt's link.x + custom memory.x)
- Delete 5 sram_* debug probe binaries (diagnostics folded into findings doc)
- Delete blinky_sdi (wlink has no H4 SDI support) and blinky_defmt
  (RTT output not consumable via WCH-Link backend yet)
- Restore blinky to a clean LED toggle; document dual-core flash
  layout and wake caveat in dualcore.rs
- Verified on hardware: V3F counter increments, V5F marker 0xDEADBEEF
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.

2 participants