Skip to content

meta: CH32V deployment-pipeline audit — 7 defects from board JSON to deploy #1102

Description

@zackees

Audit of the CH32V (WCH RISC-V) pipeline from board JSON -> build -> link -> size report -> deploy, cross-checked against the pinned OpenWCH core (arduino_core_ch32 @ d76716239cdf8a084a5045c3dfd3151b3f69eeec, the exact commit fbuild downloads) and WCH's published chip specs.

Each finding is a sub-issue. This body is the implementation spec ? each section below is written so it can be executed as-is: exact files, exact edits, tests to write first, and how to prove it worked.

Sub-issues and recommended order

Work them in this order, one PR per sub-issue, referencing the sub-issue number in the commit (fix(ch32v): ... (#1103)):

  1. ci: build-ch32*.yml path filters point at pre-split crates/fbuild-build/src/ch32v/** — CH32V build-code PRs trigger no platform builds #1107 ? MEDIUM ? fix CI path filters first (otherwise none of the later PRs trigger the CH32V builds)
  2. ch32v: V20x links against Link.ld defaults (64K/20K) — stack pointer beyond physical RAM on CH32V203C6/G6, RB/208 capped at half #1103 ? HIGH ? pass __FLASH_SIZE/__RAM_SIZE defsyms at link time (stack lands outside RAM on CH32V203C6/G6)
  3. ch32v: CH32V006 builds as a CH32V003 — series mapped to system/CH32V00x instead of CH32VM00X (16K/2K linker script on a 62K/8K chip) #1104 ? HIGH ? map ch32v006 to system/CH32VM00X (currently builds as a CH32V003, 16K/2K)
  4. ch32v: board JSON march/mabi are dead — every series compiles as RV32EC/ilp32e, IMAC parts get software multiply #1106 ? HIGH ? wire up board JSON march/mabi (everything compiles as RV32EC today)
  5. ch32v: framework field is ignored — noneos-sdk envs silently build the OpenWCH Arduino core (4 of 9 CI envs affected) #1108 ? MEDIUM ? validate the framework field (noneos-sdk silently builds the Arduino core)
  6. ch32v: deploy is unimplemented while board JSONs advertise wch-link/minichlink/isp/wlink and docs say Supported #1105 ? HIGH ? deploy is unimplemented; Phase 1 = honest error message, Phase 2 = wlink deployer
  7. ch32v: board-data and docs nits — X035 clock_source, V208 variant reuse, BOARD_STATUS drift, dead get_linker_script, missing WCH datasheets #1109 ? LOW ? data/docs nits

Ground rules (apply to every PR)

  • Rust commands go through globally-installed soldr: soldr cargo check --workspace --all-targets, soldr cargo clippy --workspace --all-targets -- -D warnings. Python through uv run python .... Never bare cargo/python (hooks block them).
  • TDD: write the failing test first, then the fix. Run bash test -p fbuild-build-mcu (or the crate you touched) before pushing.
  • Set FBUILD_DEV_MODE=1 for any local build/deploy experiment so caches go to ~/.fbuild/dev/.
  • To build a CH32V test project locally:
    soldr cargo run -p fbuild-cli -- build tests/platform/ch32v203 -e ch32v203
    Build output lands in tests/platform/ch32v203/.fbuild/build/ch32v203/ (firmware.elf, firmware.bin, firmware.map). The firmware.map file is how you verify memory-layout claims: search it for _eusrstack.
  • Useful constant: RAM starts at 0x20000000 on all CH32V parts, so _eusrstack (top of stack) = 0x20000000 + declared RAM size. 20K -> 0x20005000, 10K -> 0x20002800, 8K -> 0x20002000, 2K -> 0x20000800.

#1107 ? CI path filters point at pre-split paths

Problem. ci/ci_common_paths.txt and ci/board_families.json still list crates/fbuild-build/src/... files. After the crate split, crates/fbuild-build/src/ contains only lib.rs, compile_many.rs, README.md. The CH32V code is in crates/fbuild-build-mcu/src/ch32v/, the shared pipeline in crates/fbuild-build-engine/. So a PR touching CH32V build code triggers zero build-ch32*.yml runs. The drift gate cannot catch it because the renderer's inputs are stale.

Files to edit.

  1. ci/ci_common_paths.txt ? replace the stale crates/fbuild-build/src/*.rs enumeration. The file's own comment says "Bias: be BROAD", so use whole-crate globs:
    crates/fbuild-build/**
    crates/fbuild-build-engine/**
    crates/fbuild-build-mcu/src/lib.rs
    crates/fbuild-build-mcu/Cargo.toml
    crates/fbuild-toolchain/**
    crates/fbuild-library/**
    crates/fbuild-packages-fetch/**
    
    (Keep the per-family subdirs of fbuild-build-mcu/fbuild-build-esp/fbuild-build-arm OUT of the common list ? those stay family-scoped in board_families.json.)
  2. ci/board_families.json ? update every family's crate_paths to the real location. Current layout:
    • crates/fbuild-build-mcu/src/ contains avr/, ch32v/
    • crates/fbuild-build-esp/src/ contains esp32/, esp8266/
    • crates/fbuild-build-arm/src/ contains the rest ? run ls crates/fbuild-build-arm/src/ and map each family dir you find (stm32, teensy, nxplpc, rp2040, sam, nrf52, silabs, renesas, apollo3, generic_arm).
      For this issue's scope the critical one is:
    "ch32v": { "crate_paths": ["crates/fbuild-build-mcu/src/ch32v/**"] }
    but fix all families in the same pass ? it is the same mechanical edit.
  3. Add a guard to ci/render_workflows.py: before rendering, for every path/glob in ci_common_paths.txt and every crate_paths entry, run glob.glob(pattern, recursive=True) (strip a trailing /** to check the directory exists) and fail with a list of dead paths if any matches nothing. This makes the next crate move fail loudly instead of silently.
  4. Re-render: uv run python ci/render_workflows.py.

Acceptance.

  • grep -r "fbuild-build/src/ch32v" .github/workflows ci/ returns nothing.
  • grep -l "fbuild-build-mcu/src/ch32v" .github/workflows/build-ch32*.yml lists all 9 CH32V workflows.
  • .github/workflows/ci-workflow-drift.yml passes (renderer output == committed files).
  • Deliberately add a bogus path to ci_common_paths.txt, run the renderer, confirm it fails; remove it.

#1103 ? pass board memory to the V20x linker script

Problem. system/CH32V20x/SRC/Ld/Link.ld in the pinned core begins:

__flash_size = DEFINED(__FLASH_SIZE) ? __FLASH_SIZE : 64K;
__ram_size   = DEFINED(__RAM_SIZE)   ? __RAM_SIZE   : 20K;

fbuild never defines those symbols, so every V20x/V208 board links as 64K/20K. On 10K-RAM parts (genericCH32V203C6T6, genericCH32V203G6U6) the stack top _eusrstack = 0x20005000 is past physical RAM (0x20002800) -> firmware faults on first stack use. On 128K/64K parts (RBT6, V208WB) flash/RAM are halved.

Files to edit.

  • crates/fbuild-build-mcu/src/ch32v/ch32v_linker.rs
  • crates/fbuild-build-mcu/src/ch32v/orchestrator.rs

Steps.

  1. In ch32v_linker.rs, first refactor for testability: extract the argv construction out of link() into a pure method and have link() call it:
    fn build_link_args(&self, objects: &[PathBuf], archives: &[PathBuf],
                       elf_path: &Path, map_path: &Path, extra: &LinkExtraArgs) -> Vec<String>
  2. Add a field memory_defsyms: Vec<String> to Ch32vLinker with a builder pub fn with_memory_defsyms(mut self, flags: Vec<String>) -> Self. In build_link_args, append memory_defsyms immediately after self.mcu_config.linker_flags.
  3. In orchestrator.rs, where Ch32vLinker::new(...) is constructed (search for Ch32vLinker::new), build the flags from the board config and attach them:
    let mut memory_defsyms = Vec::new();
    if let (Some(flash), Some(ram)) = (ctx.board.max_flash, ctx.board.max_ram) {
        // Decimal byte values ? ld's --defsym does not reliably take "64K" on the CLI.
        memory_defsyms.push(format!("-Wl,--defsym=__FLASH_SIZE={flash}"));
        memory_defsyms.push(format!("-Wl,--defsym=__RAM_SIZE={ram}"));
    }
    let linker = Ch32vLinker::new(/* existing args */).with_memory_defsyms(memory_defsyms);
    Pass the defsyms for all CH32V series unconditionally: linker scripts that never reference the symbols (V00x, V10x, L10x, X035) just get two harmless absolute symbols in the ELF. Only the V20x script consumes them. board.max_flash/max_ram come from the board JSON rom/ram fields, which are correct for all 17 CH32V boards (verified against WCH datasheets ? see the reference table at the bottom).

Tests (write first).

  • Unit test on build_link_args: construct a Ch32vLinker with with_memory_defsyms(vec!["-Wl,--defsym=__FLASH_SIZE=32768".into(), "-Wl,--defsym=__RAM_SIZE=10240".into()]) and assert both strings are present in the argv, positioned before the -T script flag.
  • Unit test: no defsyms configured -> argv contains no --defsym.

Acceptance (manual).

  1. soldr cargo run -p fbuild-cli -- build tests/platform/ch32v203 -e ch32v203 still succeeds; grep _eusrstack tests/platform/ch32v203/.fbuild/build/ch32v203/firmware.map shows 0x20005000 (C8T6 is a 64K/20K part ? same as the old defaults, so no behavior change).
  2. Make a throwaway project that uses the small part:
    mkdir -p /tmp/c6test/src && cp tests/platform/ch32v203/src/* /tmp/c6test/src/
    printf '[env:c6]\nplatform = ch32v\nboard = genericCH32V203C6T6\nframework = arduino\n' > /tmp/c6test/platformio.ini
    soldr cargo run -p fbuild-cli -- build /tmp/c6test -e c6
    
    grep _eusrstack /tmp/c6test/.fbuild/build/c6/firmware.map must now show 0x20002800 (top of the real 10K), not 0x20005000.

Do not try to fix the other series' scripts this way ? their MEMORY blocks are hardcoded and correct (V00x 16K/2K, V10x 64K/20K, L10x 64K/20K, X035 62K/20K, V30x 256K/64K). The V006 case is #1104.


#1104 ? CH32V006 must use system/CH32VM00X, not system/CH32V00x

Problem. series_to_system_dir() in crates/fbuild-build-mcu/src/ch32v/orchestrator.rs turns ch32v006 into CH32V00x ("replace last digit with x"). Upstream, the V006 family lives in its own dir system/CH32VM00X/ (present at the pinned commit) whose Link.ld has the correct active MEMORY: FLASH 62K, RAM 8K. Today the V006 board gets the CH32V003 HAL and the 16K/2K V003 linker script on a 62K/8K chip. discover_system_includes only logs at debug level when a mapping is wrong, and the linker-script path is built unconditionally, so it fails silently.

Files to edit.

  • crates/fbuild-build-mcu/src/ch32v/orchestrator.rs (series_to_system_dir, resolve_variant_dir, tests)
  • crates/fbuild-config/assets/boards/json/genericCH32V006K8U6.json

Steps.

  1. In series_to_system_dir, add an explicit mapping before the generic rule:
    // The CH32V002/004/005/006/007 + CH32M007 family lives in its own
    // upstream dir, not in the CH32V003's CH32V00x family dir.
    if matches!(series, "ch32v002" | "ch32v004" | "ch32v005" | "ch32v006" | "ch32v007" | "ch32m007") {
        return "CH32VM00X".to_string();
    }
  2. Find out which family macro the core expects: after any ch32v build, the core is cached ? ls ~/.fbuild/dev/cache/platforms/ | grep ch32v-core, then grep -rl CH32VM00X <that-dir>/cores/arduino | head. The orchestrator inserts defines.insert(system_series, "1"), which after step 1 yields -DCH32VM00X=1 ? confirm that macro (exact spelling/case) is what the core's #if defined(...) guards use.
  3. Fix the typo'd defines in genericCH32V006K8U6.json: extra_flags currently has -DCH32VM0X -DCH32VM0x (each missing a 0). Change to -DCH32VM00X -DCH32VM00x to match the spelling found in step 2.
  4. Delete the resolve_variant_dir special case that redirects CH32VM00X/CH32V006K8 to CH32V00x/CH32V003F4 (the block at the top of the function with the "PB_* pins" comment), then build:
    soldr cargo run -p fbuild-cli -- build tests/platform/ch32v006 -e ch32v006
    • If it passes: keep the deletion.
    • If it fails with undefined PB_* pin errors: the upstream variants/CH32VM00X/CH32V006K8/variant_CH32V006K8.h references port-B pins. Check whether the failure was actually caused by the old wrong system dir (the V003 HAL has no port-B defs; the CH32VM00X HAL may well have them ? this is the expected outcome). If it still fails, restore the fallback for the variant only and keep the CH32VM00X system dir (HAL + Link.ld) ? that alone fixes the memory map and HAL. Record which of the two outcomes happened in the PR description.
  5. Update tests in the same file:
    • test_series_to_system_dir: add assert_eq!(series_to_system_dir("ch32v006"), "CH32VM00X");
    • test_resolve_variant_dir_falls_back_to_family_variant and test_resolve_variant_dir_skips_broken_ch32v006_upstream_variant: rewrite or delete to match the step-4 outcome.

Acceptance.

  • soldr cargo run -p fbuild-cli -- build tests/platform/ch32v006 -e ch32v006 succeeds.
  • grep _eusrstack tests/platform/ch32v006/.fbuild/build/ch32v006/firmware.map shows 0x20002000 (8K RAM), not 0x20000800 (2K).
  • Sanity: the ch32v003 build is unchanged (_eusrstack = 0x20000800).

#1105 ? deploy unimplemented but advertised

Problem. Platform::Ch32v falls into the _ => catch-all in crates/fbuild-daemon/src/handlers/operations/deploy.rs ("deployer for Ch32v not yet implemented"), while all 17 CH32V board JSONs advertise "protocols": ["wch-link", "minichlink", "isp", "wlink"] and docs/BOARD_STATUS.md says the platform is Supported.

Phase 1 (do now, small).

  1. In the deployer match platform block, add an explicit arm above the _ => catch-all:
    fbuild_core::Platform::Ch32v => {
        return Err(fbuild_core::FbuildError::DeployFailed(
            ch32v_deploy_unimplemented_message(&deploy_fw),
        ));
    }
    with, next to the handler:
    /// CH32V flashing is tracked in FastLED/fbuild#1105.
    fn ch32v_deploy_unimplemented_message(firmware: &std::path::Path) -> String {
        format!(
            "CH32V flashing is not implemented yet (FastLED/fbuild#1105). \
             Firmware was built at {}. Flash it manually with a WCH-Link probe: \
             `wlink flash {}` (https://github.com/ch32-rs/wlink) or \
             `minichlink -w {} flash -b`.",
            firmware.display(), firmware.display(), firmware.display()
        )
    }
  2. Unit test: the message contains "wlink", "#1105", and the firmware path.
  3. docs/BOARD_STATUS.md: change the CH32V heading line to **CH32V (RISC-V) Platform** - Supported (build only ? flashing tracked in #1105).

Phase 2 (separate PR, larger ? outline). Model 1:1 on crates/fbuild-deploy/src/probe_rs.rs, which is the existing "pinned external flasher binary" pattern:

  1. New module crates/fbuild-deploy/src/ch32v.rs (module in the existing crate ? do NOT create a new crate; monocrate policy).
  2. Pin a release of ch32-rs/wlink: pick the latest tag, download each per-OS/arch asset once, record its sha256 (sha256sum <file>), and hardcode a release-asset table exactly like probe_rs_release_asset_for_host(). Managed install dir: ~/.fbuild/{prod|dev}/tools/wlink/.
  3. Ch32vDeployer implementing the Deployer trait: argv wlink flash <firmware.bin> (wlink auto-detects the chip via the probe; verify with wlink --help whether --chip is needed for the CH32V003's 1-wire SDI mode). Wrap the invocation with run_command_blocking + timeout like run_probe_rs_download does.
  4. Dispatch: replace the Phase-1 error arm with construction of Ch32vDeployer.
  5. Probe presence/identity: do not hardcode VID/PIDs in fbuild (repo rule). WCH-Link identities (1a86:8010 RV mode, 1a86:8012 DAP mode) must come from the FastLED/boards registry via fbuild_core::usb::profiles::profiles_for ? check whether the vid-ingest: WCH / QinHeng (0x1A86 — CH32V/X + CH340 bridges) #730 vid-ingest already added probe-role entries for them; if not, land that in FastLED/boards first.
  6. crates/fbuild-serial/src/boards.rs (~line 602): (0x1A86, _) => Some(Esp32ExternalUart) swallows WCH-Link probes. Add specific probe matches before the wildcard (driven by the registry profiles, not literals) so a WCH-Link is not presented as an ESP32 UART bridge. Keep the wildcard itself ? CH340-based ESP32 boards depend on it.

#1106 ? board JSON march/mabi are dead fields

Problem. Every CH32V board JSON declares its ISA ("march": "rv32imacxw", "mabi": "ilp32" on V103/V203/V208/V303/V307/X035/L103), but no code reads build.march/build.mabi. get_ch32v_config_for_mcu() returns the CH32V003 config (-march=rv32ec_zicsr -mabi=ilp32e) for every series ? its comment even claims the board JSON overrides it, which is false. The RV32IMAC parts get soft multiply/divide from libgcc and only use registers x0?x15. Builds pass because RV32EC code runs on IMAC silicon ? just slower.

Files to edit.

  • crates/fbuild-config/src/board/types.rs
  • crates/fbuild-config/src/board/db.rs
  • crates/fbuild-config/src/board/loaders.rs
  • crates/fbuild-build-mcu/src/ch32v/mcu_config.rs
  • crates/fbuild-build-mcu/src/ch32v/orchestrator.rs

Steps.

  1. types.rs ? add two fields to BoardConfig right after extra_flags (~line 75), mirroring its serde style:
    /// RISC-V ISA string from board JSON `build.march` (CH32V boards).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub march: Option<String>,
    /// RISC-V ABI string from board JSON `build.mabi` (CH32V boards).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub mabi: Option<String>,
  2. db.rs (~line 252, inside the build section extraction) ? copy the extra_flags pattern for march and mabi:
    if let Some(v) = build.get("march").and_then(|v| v.as_str()) { d.insert("march".into(), v.to_string()); }
    if let Some(v) = build.get("mabi").and_then(|v| v.as_str()) { d.insert("mabi".into(), v.to_string()); }
  3. loaders.rs ? there are two BoardConfig { ... } construction sites; find both by searching for extra_flags: (~line 177 and ~line 347). At each, add march: get("march"), mabi: get("mabi"), (at the second site, mirror however extra_flags merges overrides vs defaults there).
  4. mcu_config.rs ? add two pub functions plus fixes:
    /// Normalize a board-JSON march string for the xPack riscv-none-elf GCC:
    /// the WCH `xw` compressed extension is a MounRiver-toolchain-only ISA
    /// extension that upstream GCC rejects, and modern binutils needs an
    /// explicit `_zicsr` for CSR instructions.
    /// "rv32imacxw" -> "rv32imac_zicsr"; "rv32imac" -> "rv32imac_zicsr";
    /// "rv32ec_zicsr" -> "rv32ec_zicsr" (unchanged).
    pub fn normalize_march(march: &str) -> String {
        let mut m = march.to_ascii_lowercase();
        if let Some(stripped) = m.strip_suffix("xw") { m = stripped.to_string(); }
        if !m.contains("zicsr") { m.push_str("_zicsr"); }
        m
    }
    
    /// Replace the -march=/-mabi= entries in both compiler and linker flag
    /// sets with the board's values (when present).
    pub fn apply_board_isa(config: &mut Ch32vMcuConfig, march: Option<&str>, mabi: Option<&str>) {
        fn replace(flags: &mut [String], prefix: &str, value: &str) {
            for f in flags.iter_mut() {
                if f.starts_with(prefix) { *f = format!("{prefix}{value}"); }
            }
        }
        if let Some(m) = march {
            let m = normalize_march(m);
            replace(&mut config.compiler_flags.common, "-march=", &m);
            replace(&mut config.linker_flags, "-march=", &m);
        }
        if let Some(abi) = mabi {
            replace(&mut config.compiler_flags.common, "-mabi=", abi);
            replace(&mut config.linker_flags, "-mabi=", abi);
        }
    }
    Also: delete the false "overridden from the board JSON extra_flags" comment in get_ch32v_config_for_mcu, and rename its parameter mcu -> series (it is called with the series string).
  5. orchestrator.rs ? where let mcu_config = super::mcu_config::get_ch32v_config_for_mcu(&series)?; is, change to:
    let mut mcu_config = super::mcu_config::get_ch32v_config_for_mcu(&series)?;
    super::mcu_config::apply_board_isa(&mut mcu_config, ctx.board.march.as_deref(), ctx.board.mabi.as_deref());
    Placement matters: this must run before the block a few lines down that extracts march/mabi back out of mcu_config.compiler_flags.common to compute the C++ -isystem multilib path (get_cxx_system_includes) ? that block then automatically picks the right multilib. No other orchestrator change is needed.
  6. Tests (write first, in mcu_config.rs and ch32v_compiler.rs test modules):
    • normalize_march table: the three examples above plus "rv32ecxw" -> "rv32ec_zicsr".
    • apply_board_isa replaces the pair in both compiler_flags.common and linker_flags, and is a no-op when both args are None.
    • Compiler-level: build a Ch32vCompiler from a config that went through apply_board_isa with Some("rv32imacxw")/Some("ilp32") and assert c_flags() contains -march=rv32imac_zicsr and -mabi=ilp32.
    • Existing rv32ec tests must keep passing (V003 boards declare rv32ec_zicsr/ilp32e, so their flags are unchanged).

Acceptance (manual).

  1. soldr cargo run -p fbuild-cli -- build tests/platform/ch32v203 -e ch32v203 --verbose ? the compile log shows -march=rv32imac_zicsr -mabi=ilp32.
  2. Find the toolchain's nm: ls ~/.fbuild/dev/cache/toolchains/ (xPack riscv-none-elf). Then:
    riscv-none-elf-nm tests/platform/ch32v203/.fbuild/build/ch32v203/firmware.elf | grep -i mulsi3 -> must be empty (hardware mul now used; before the fix libgcc's __mulsi3 is present).
  3. tests/platform/ch32v003 still builds and its flags still say rv32ec_zicsr/ilp32e.
  4. If the link fails with a multilib error (cannot find -lc_nano for the new arch), run riscv-none-elf-gcc --print-multi-lib and report ? the xPack toolchain ships rv32imac/ilp32, so this is not expected.

#1108 ? framework field is silently ignored

Problem. The CH32V orchestrator always builds the OpenWCH Arduino core. Four of nine CI envs declare framework = noneos-sdk (tests/platform/ch32v103, ch32v208, ch32v303, ch32x035) ? what those badges actually certify is an Arduino build. grep -r noneos crates/ has zero hits.

Decision: validate + make CI honest (implementing a real noneos-sdk build mode is out of scope).

Steps.

  1. crates/fbuild-build-mcu/src/ch32v/orchestrator.rs ? add a small pure function + call it early in build() (right after BuildContext::new):
    /// Only the Arduino framework is implemented for CH32V (FastLED/fbuild#1108).
    fn validate_ch32v_framework(framework: Option<&str>) -> fbuild_core::Result<()> {
        match framework.map(str::trim) {
            None | Some("") | Some("arduino") => Ok(()),
            Some(other) => Err(fbuild_core::FbuildError::ConfigError(format!(
                "ch32v: framework = {other} is not supported yet; only `framework = arduino` \
                 builds today (FastLED/fbuild#1108)"))),
        }
    }
    Read the env's framework the same way the platform_packages override a few lines below does: ctx.config.get_env_config(&params.env_name) returns the env map ? check how package_override::resolve_override consumes it and use the same accessor to pull the framework key. If the value can be a comma-separated list, reject any list that is not exactly arduino.
  2. Flip the four test envs to framework = arduino in their platformio.ini files (they were building the Arduino core all along; this makes the label match reality).
  3. Add "arduino" to the frameworks arrays of genericCH32V208WBU6.json and genericCH32V303VCT6.json (the only two CH32V boards missing it).
  4. Tests: unit tests on validate_ch32v_framework ? None ok, "arduino" ok, " arduino " ok, "noneos-sdk" errors and the message contains 1108.

Acceptance. All nine tests/platform/ch32* envs build; a scratch project with framework = noneos-sdk fails fast with the #1108 message instead of silently building Arduino.


#1109 ? data/docs nits (batch into one PR)

  1. genericCH32X035C8T6.json: "clock_source": "hsi+pll" -> "hsi". The CH32X035 has no PLL and no HSE; it runs the 48 MHz HSI directly. (Field is currently unread ? this is reference-data hygiene.)
  2. genericCH32V208WBU6.json reuses "variant": "CH32V20x/CH32V203C8" + variant_h. Check the installed core's variants/CH32V20x/ for any V208 directory; at the pinned commit there is none, so leave the value but state in the PR description that the V208 intentionally borrows the V203C8 pin map until upstream ships a variant.
  3. docs/BOARD_STATUS.md CH32V section:
  4. Delete dead code in crates/fbuild-library/src/library/ch32v_core.rs: get_linker_script(), find_ld_file(), and their tests (test_get_linker_script_with_ld_file, test_get_linker_script_fallback). First confirm zero callers: grep -rn get_linker_script crates/ must only hit this file. (The orchestrator resolves system/<series>/SRC/Ld/Link.ld itself; this variant-dir strategy was never used and contradicts it.)
  5. Stretch (separate repo): FastLED/datasheets has no wch/ folder. Add wch/mcu/CH32V003/, CH32V006/, CH32V103/, CH32V203/, CH32V208/, CH32V30x/, CH32X035/, CH32L103/ with datasheet.pdf + reference-manual.pdf + SOURCES.md following the espressif/soc/* layout (PDFs from wch-ic.com product pages), so board JSONs and future audits can cite pinned documents.

Verified reference facts (so implementers don't have to re-derive them)

Part Flash RAM _eusrstack when correct Upstream Link.ld
CH32V003x4 16K 2K 0x20000800 system/CH32V00x (hardcoded 16K/2K)
CH32V006K8 62K 8K 0x20002000 system/CH32VM00X (hardcoded 62K/8K active block)
CH32V103C8/R8 64K 20K 0x20005000 system/CH32V10x (hardcoded 64K/20K)
CH32V203C6/G6 32K 10K 0x20002800 system/CH32V20x (parameterized, defaults 64K/20K)
CH32V203C8/G8 64K 20K 0x20005000 same script
CH32V203RB / CH32V208WB 128K 64K 0x20010000 same script
CH32V303VC / CH32V307VC 256K 64K 0x20010000 system/CH32V30x (hardcoded 256K/64K active block)
CH32X035C8 62K 20K 0x20005000 system/CH32X035 (hardcoded 62K/20K)
CH32L103C8 64K 20K 0x20005000 system/CH32L10x (hardcoded 64K/20K)

Pinned core: https://github.com/openwch/arduino_core_ch32 @ d76716239cdf8a084a5045c3dfd3151b3f69eeec (see CH32V_CORE_URL in crates/fbuild-library/src/library/ch32v_core.rs). All Link.ld facts above were read from that exact commit.

Found during a CH32V deployment-pipeline review on 2026-07-21.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions