feat: V3F/V5F (CH32H4) dual-core support - #23
Conversation
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
Review follow-upIssue #1 — mutual-exclusivity guard (now fixed)Pushed Verified locally:
Field/CSR cross-check vs. WCH SDK headersReviewer asked for human verification of several register layouts; I went through the WCH SDK headers and
Still pending hardware-side verification
Minor items from the review left for follow-up (not in this PR)
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).
Correction on
|
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).
- 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
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: newv3f/v5fcargo features that select the right boot stub and ISR table per hartqingke::pfic:HartId+wake_other_corehelpers (gated behind a dual-core feature so single-hart V3 / V4 builds keep their existing surface)qingke::register: typed wrapper forinestcr(interrupt-nest control), gated behind a_inestcrfeature; `setup` now calls into it instead of a raw CSR writeqingke::register: V5-only ICache CSRscache_strtg_ctlr/cache_pmp_ovr/opcache_ctlrso H4's V5F hart can configure its icachepfic::disable_vtf: wrap the inner ptr ops in `unsafe { }` to satisfy Rust 2024's `unsafe_op_in_unsafe_fn` lintTest plan
Notes