You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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)):
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.
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.
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:
(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.)
ci/board_families.json ? update every family's crate_paths to the real location. Current layout:
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:
but fix all families in the same pass ? it is the same mechanical edit.
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.
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.
In ch32v_linker.rs, first refactor for testability: extract the argv construction out of link() into a pure method and have link() call it:
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.
In orchestrator.rs, where Ch32vLinker::new(...) is constructed (search for Ch32vLinker::new), build the flags from the board config and attach them:
letmut memory_defsyms = Vec::new();iflet(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).
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).
Make a throwaway project that uses the small part:
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.
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.ifmatches!(series,"ch32v002" | "ch32v004" | "ch32v005" | "ch32v006" | "ch32v007" | "ch32m007"){return"CH32VM00X".to_string();}
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.
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.
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.
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.
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).
In the deployer match platform block, add an explicit arm above the _ => catch-all:
/// CH32V flashing is tracked in FastLED/fbuild#1105.fnch32v_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())}
Unit test: the message contains "wlink", "#1105", and the firmware path.
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:
New module crates/fbuild-deploy/src/ch32v.rs (module in the existing crate ? do NOT create a new crate; monocrate policy).
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/.
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.
Dispatch: replace the Phase-1 error arm with construction of Ch32vDeployer.
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.
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.
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.
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>,
db.rs (~line 252, inside the build section extraction) ? copy the extra_flags pattern for march and mabi:
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).
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).pubfnnormalize_march(march:&str) -> String{letmut m = march.to_ascii_lowercase();ifletSome(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).pubfnapply_board_isa(config:&mutCh32vMcuConfig,march:Option<&str>,mabi:Option<&str>){fnreplace(flags:&mut[String],prefix:&str,value:&str){for f in flags.iter_mut(){if f.starts_with(prefix){*f = format!("{prefix}{value}");}}}ifletSome(m) = march {let m = normalize_march(m);replace(&mut config.compiler_flags.common,"-march=",&m);replace(&mut config.linker_flags,"-march=",&m);}ifletSome(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).
orchestrator.rs ? where let mcu_config = super::mcu_config::get_ch32v_config_for_mcu(&series)?; is, change to:
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.
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 bothcompiler_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).
soldr cargo run -p fbuild-cli -- build tests/platform/ch32v203 -e ch32v203 --verbose ? the compile log shows -march=rv32imac_zicsr -mabi=ilp32.
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).
tests/platform/ch32v003 still builds and its flags still say rv32ec_zicsr/ilp32e.
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.
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.
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).fnvalidate_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(¶ms.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.
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).
Add "arduino" to the frameworks arrays of genericCH32V208WBU6.json and genericCH32V303VCT6.json (the only two CH32V boards missing it).
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.
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.)
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.
docs/BOARD_STATUS.md CH32V section:
add the three missing badges (build-ch32v006.yml, build-ch32l103.yml, build-ch32x035.yml) in the same format as the existing six;
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.)
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.
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)):__FLASH_SIZE/__RAM_SIZEdefsyms at link time (stack lands outside RAM on CH32V203C6/G6)ch32v006tosystem/CH32VM00X(currently builds as a CH32V003, 16K/2K)march/mabi(everything compiles as RV32EC today)frameworkfield (noneos-sdk silently builds the Arduino core)Ground rules (apply to every PR)
soldr cargo check --workspace --all-targets,soldr cargo clippy --workspace --all-targets -- -D warnings. Python throughuv run python .... Never barecargo/python(hooks block them).bash test -p fbuild-build-mcu(or the crate you touched) before pushing.FBUILD_DEV_MODE=1for any local build/deploy experiment so caches go to~/.fbuild/dev/.soldr cargo run -p fbuild-cli -- build tests/platform/ch32v203 -e ch32v203Build output lands in
tests/platform/ch32v203/.fbuild/build/ch32v203/(firmware.elf,firmware.bin,firmware.map). Thefirmware.mapfile is how you verify memory-layout claims: search it for_eusrstack.0x20000000on 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.txtandci/board_families.jsonstill listcrates/fbuild-build/src/...files. After the crate split,crates/fbuild-build/src/contains onlylib.rs,compile_many.rs,README.md. The CH32V code is incrates/fbuild-build-mcu/src/ch32v/, the shared pipeline incrates/fbuild-build-engine/. So a PR touching CH32V build code triggers zerobuild-ch32*.ymlruns. The drift gate cannot catch it because the renderer's inputs are stale.Files to edit.
ci/ci_common_paths.txt? replace the stalecrates/fbuild-build/src/*.rsenumeration. The file's own comment says "Bias: be BROAD", so use whole-crate globs:fbuild-build-mcu/fbuild-build-esp/fbuild-build-armOUT of the common list ? those stay family-scoped inboard_families.json.)ci/board_families.json? update every family'scrate_pathsto the real location. Current layout:crates/fbuild-build-mcu/src/containsavr/,ch32v/crates/fbuild-build-esp/src/containsesp32/,esp8266/crates/fbuild-build-arm/src/contains the rest ? runls 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:
ci/render_workflows.py: before rendering, for every path/glob inci_common_paths.txtand everycrate_pathsentry, runglob.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.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*.ymllists all 9 CH32V workflows..github/workflows/ci-workflow-drift.ymlpasses (renderer output == committed files).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.ldin the pinned core begins:fbuild never defines those symbols, so every V20x/V208 board links as 64K/20K. On 10K-RAM parts (genericCH32V203C6T6, genericCH32V203G6U6) the stack top
_eusrstack=0x20005000is 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.rscrates/fbuild-build-mcu/src/ch32v/orchestrator.rsSteps.
ch32v_linker.rs, first refactor for testability: extract the argv construction out oflink()into a pure method and havelink()call it:memory_defsyms: Vec<String>toCh32vLinkerwith a builderpub fn with_memory_defsyms(mut self, flags: Vec<String>) -> Self. Inbuild_link_args, appendmemory_defsymsimmediately afterself.mcu_config.linker_flags.orchestrator.rs, whereCh32vLinker::new(...)is constructed (search forCh32vLinker::new), build the flags from the board config and attach them:board.max_flash/max_ramcome from the board JSONrom/ramfields, which are correct for all 17 CH32V boards (verified against WCH datasheets ? see the reference table at the bottom).Tests (write first).
build_link_args: construct aCh32vLinkerwithwith_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-Tscript flag.--defsym.Acceptance (manual).
soldr cargo run -p fbuild-cli -- build tests/platform/ch32v203 -e ch32v203still succeeds;grep _eusrstack tests/platform/ch32v203/.fbuild/build/ch32v203/firmware.mapshows0x20005000(C8T6 is a 64K/20K part ? same as the old defaults, so no behavior change).grep _eusrstack /tmp/c6test/.fbuild/build/c6/firmware.mapmust now show0x20002800(top of the real 10K), not0x20005000.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, notsystem/CH32V00xProblem.
series_to_system_dir()incrates/fbuild-build-mcu/src/ch32v/orchestrator.rsturnsch32v006intoCH32V00x("replace last digit with x"). Upstream, the V006 family lives in its own dirsystem/CH32VM00X/(present at the pinned commit) whoseLink.ldhas 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_includesonly 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.jsonSteps.
series_to_system_dir, add an explicit mapping before the generic rule:ls ~/.fbuild/dev/cache/platforms/ | grep ch32v-core, thengrep -rl CH32VM00X <that-dir>/cores/arduino | head. The orchestrator insertsdefines.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.genericCH32V006K8U6.json:extra_flagscurrently has-DCH32VM0X -DCH32VM0x(each missing a0). Change to-DCH32VM00X -DCH32VM00xto match the spelling found in step 2.resolve_variant_dirspecial case that redirectsCH32VM00X/CH32V006K8toCH32V00x/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 ch32v006PB_*pin errors: the upstreamvariants/CH32VM00X/CH32V006K8/variant_CH32V006K8.hreferences 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.test_series_to_system_dir: addassert_eq!(series_to_system_dir("ch32v006"), "CH32VM00X");test_resolve_variant_dir_falls_back_to_family_variantandtest_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 ch32v006succeeds.grep _eusrstack tests/platform/ch32v006/.fbuild/build/ch32v006/firmware.mapshows0x20002000(8K RAM), not0x20000800(2K)._eusrstack=0x20000800).#1105 ? deploy unimplemented but advertised
Problem.
Platform::Ch32vfalls into the_ =>catch-all incrates/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"]anddocs/BOARD_STATUS.mdsays the platform is Supported.Phase 1 (do now, small).
match platformblock, add an explicit arm above the_ =>catch-all:"wlink","#1105", and the firmware path.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:crates/fbuild-deploy/src/ch32v.rs(module in the existing crate ? do NOT create a new crate; monocrate policy).sha256sum <file>), and hardcode a release-asset table exactly likeprobe_rs_release_asset_for_host(). Managed install dir:~/.fbuild/{prod|dev}/tools/wlink/.Ch32vDeployerimplementing theDeployertrait: argvwlink flash <firmware.bin>(wlink auto-detects the chip via the probe; verify withwlink --helpwhether--chipis needed for the CH32V003's 1-wire SDI mode). Wrap the invocation withrun_command_blocking+ timeout likerun_probe_rs_downloaddoes.Ch32vDeployer.1a86:8010RV mode,1a86:8012DAP mode) must come from the FastLED/boards registry viafbuild_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.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/mabiare dead fieldsProblem. Every CH32V board JSON declares its ISA (
"march": "rv32imacxw", "mabi": "ilp32"on V103/V203/V208/V303/V307/X035/L103), but no code readsbuild.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.rscrates/fbuild-config/src/board/db.rscrates/fbuild-config/src/board/loaders.rscrates/fbuild-build-mcu/src/ch32v/mcu_config.rscrates/fbuild-build-mcu/src/ch32v/orchestrator.rsSteps.
types.rs? add two fields toBoardConfigright afterextra_flags(~line 75), mirroring its serde style:db.rs(~line 252, inside thebuildsection extraction) ? copy theextra_flagspattern formarchandmabi:loaders.rs? there are twoBoardConfig { ... }construction sites; find both by searching forextra_flags:(~line 177 and ~line 347). At each, addmarch: get("march"), mabi: get("mabi"),(at the second site, mirror howeverextra_flagsmerges overrides vs defaults there).mcu_config.rs? add two pub functions plus fixes:get_ch32v_config_for_mcu, and rename its parametermcu->series(it is called with the series string).orchestrator.rs? wherelet mcu_config = super::mcu_config::get_ch32v_config_for_mcu(&series)?;is, change to:march/mabiback out ofmcu_config.compiler_flags.commonto compute the C++-isystemmultilib path (get_cxx_system_includes) ? that block then automatically picks the right multilib. No other orchestrator change is needed.mcu_config.rsandch32v_compiler.rstest modules):normalize_marchtable: the three examples above plus"rv32ecxw"->"rv32ec_zicsr".apply_board_isareplaces the pair in bothcompiler_flags.commonandlinker_flags, and is a no-op when both args areNone.Ch32vCompilerfrom a config that went throughapply_board_isawithSome("rv32imacxw")/Some("ilp32")and assertc_flags()contains-march=rv32imac_zicsrand-mabi=ilp32.rv32ec_zicsr/ilp32e, so their flags are unchanged).Acceptance (manual).
soldr cargo run -p fbuild-cli -- build tests/platform/ch32v203 -e ch32v203 --verbose? the compile log shows-march=rv32imac_zicsr -mabi=ilp32.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 (hardwaremulnow used; before the fix libgcc's__mulsi3is present).tests/platform/ch32v003still builds and its flags still sayrv32ec_zicsr/ilp32e.cannot find -lc_nanofor the new arch), runriscv-none-elf-gcc --print-multi-liband report ? the xPack toolchain shipsrv32imac/ilp32, so this is not expected.#1108 ?
frameworkfield is silently ignoredProblem. 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.
crates/fbuild-build-mcu/src/ch32v/orchestrator.rs? add a small pure function + call it early inbuild()(right afterBuildContext::new):platform_packagesoverride a few lines below does:ctx.config.get_env_config(¶ms.env_name)returns the env map ? check howpackage_override::resolve_overrideconsumes it and use the same accessor to pull theframeworkkey. If the value can be a comma-separated list, reject any list that is not exactlyarduino.framework = arduinoin theirplatformio.inifiles (they were building the Arduino core all along; this makes the label match reality)."arduino"to theframeworksarrays ofgenericCH32V208WBU6.jsonandgenericCH32V303VCT6.json(the only two CH32V boards missing it).validate_ch32v_framework?Noneok,"arduino"ok," arduino "ok,"noneos-sdk"errors and the message contains1108.Acceptance. All nine
tests/platform/ch32*envs build; a scratch project withframework = noneos-sdkfails fast with the #1108 message instead of silently building Arduino.#1109 ? data/docs nits (batch into one PR)
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.)genericCH32V208WBU6.jsonreuses"variant": "CH32V20x/CH32V203C8"+variant_h. Check the installed core'svariants/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.docs/BOARD_STATUS.mdCH32V section:build-ch32v006.yml,build-ch32l103.yml,build-ch32x035.yml) in the same format as the existing six;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 resolvessystem/<series>/SRC/Ld/Link.lditself; this variant-dir strategy was never used and contradicts it.)wch/folder. Addwch/mcu/CH32V003/,CH32V006/,CH32V103/,CH32V203/,CH32V208/,CH32V30x/,CH32X035/,CH32L103/withdatasheet.pdf+reference-manual.pdf+SOURCES.mdfollowing theespressif/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)
_eusrstackwhen correct0x20000800system/CH32V00x(hardcoded 16K/2K)0x20002000system/CH32VM00X(hardcoded 62K/8K active block)0x20005000system/CH32V10x(hardcoded 64K/20K)0x20002800system/CH32V20x(parameterized, defaults 64K/20K)0x200050000x200100000x20010000system/CH32V30x(hardcoded 256K/64K active block)0x20005000system/CH32X035(hardcoded 62K/20K)0x20005000system/CH32L10x(hardcoded 64K/20K)Pinned core:
https://github.com/openwch/arduino_core_ch32@d76716239cdf8a084a5045c3dfd3151b3f69eeec(seeCH32V_CORE_URLincrates/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.