Skip to content
arwidcoolPublic

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

WCHCube

CI license: MIT no dependencies one file

An STM32CubeMX-style pinout, peripheral and clock configurator for WCH RISC-V microcontrollers.

Pick a part, click a pin, choose a signal. The app shows you which peripherals can reach which pins on that exact package, colours the conflicts orange, and tells you before you click when a choice would collide with something you have already set.

The pinout view: CH32V006 on TSSOP20 with USART1 and SPI1 assigned

Read this before you trust it

This project is mostly vibe-coded. The application was written by AI coding agents under a written working agreement, with a human setting direction. It is fast and internally consistent, and it has one specific, well-documented blind spot: an AI will confidently write plausible-looking code for a chip it has never seen. It has shipped exactly that bug three times — a wrong SDK header, a GPIO speed macro that does not exist, and a drive mode the silicon cannot do. All three were caught by building something, never by reading code.

The same blind spot has a second shape, and it is the one to worry about: correct code that never runs. On CH32H417 the DMA init struct was filled with every field the user chose and then never applied — no DMA_Init(), no DMA_Cmd(). A configured DMA transfer compiled clean, reported zero conflicts, and moved nothing, with the coverage ledger at zero, the validator at zero errors and every test green over it. It was one of three instances of that shape found in a single cycle. The cause is structural: most of what each part models has never been generated-and-compiled by any fixture — CH32X035's one fixture configures zero of its 27 peripherals — so a green suite says the files are consistent, never that the code runs. See tests/evidence/round6/ for the per-part map of what has actually been exercised.

And nothing here has ever been flashed. Every green result in this repository is a compile. No board has been attached. Hardware behaviour is unverified.

→ docs/HOW-IT-WORKS.md explains both in detail, and is worth ten minutes before you rely on anything below.

What it does

Four tabs, and each answers a different question.

Pinout & Configuration — the chip drawn to scale for the package, the peripheral tree, and the GPIO table. Hovering a peripheral says what its name stands for (SDIO → SD input/output interface) and selecting it describes what that kind of block does; hovering a pin says what the pad is, what voltage it takes and what to connect to it (VDDIO, VREFP, VSSA). The tree is grouped by category and sorted, so USART1–USART8 sit together and TIM2 comes before TIM10. The Power setup under System Core lists every supply domain with its pads, its range and its citation — including the constraints a pin table cannot carry, such as VDDA never exceeding VDD.

Clock Configuration — the clock tree, drawn and computed. On CH32H417 that means five PLL blocks (the SYS PLL plus USBHS, Ethernet, USB SS and SerDes) and all eight per-peripheral source muxes (USBFS, RNG, I2S2, I2S3, HSADC, UHSIF, LTDC, ETH1G). Every tap either reports a number or says why it cannot; a tap that would silently ignore a mux does not render a plausible figure instead.

Project Manager — everything that will be generated, previewed file by file before you commit to it, with the generator's own TODOs marked in the preview. One Generate button.

Tools — what this MCU file does not say: pads nobody has extracted yet and who owns them, signals that are routed but no setting can claim, peripherals with no description, and every place a value falls back to a default because the file is silent (no gpio.speeds, no codegen: block, a part number with no PlatformIO board). It reads none on a finished part, and its job is to make sure it keeps saying so.

Documentation

Document Read it when
docs/HOW-IT-WORKS.md You want to understand the design and exactly how much of it to trust. Start here.
docs/ADDING-A-PART.md Your microcontroller is not supported. The whole process: drop the vendor files in, let an AI extract them, verify.
docs/COVERAGE.md You are extracting a part, or asking whether one is finished. The coverage ledger: every function on every pin the datasheet lists is modelled, declared absent with a citation, or open — and a part is not done while a row is open.
docs/WCH-MCU-CATALOG.md You are asking which part to add next, or whether one is already modelled. Every MCU WCH publishes, read from the vendor's own product API and joined against data/mcus/. A pointer, never a citation. Its progress table says which parts are done, which two are queued, and which have not been started — 11 of WCH's 87 catalogue rows have data.
data/FORMAT.md You are writing or editing an MCU file. The field-by-field schema.
PROGRESS.md What is actually done, broken, and never tested.
docs/ Index of all of the above.

Supported parts — the list is data/mcus/, and adding to it does not require touching app/. Six, across three families. That is 11 of the 87 rows in WCH's own catalogue; docs/WCH-MCU-CATALOG.md has the full progress table — what is done, what is queued, and what has not been started.

Peripheral and package counts below are what the engine reports after loading each file, not what a header says:

Part Peripherals Packages Why it is in the list
CH32V006 19 5 the reference part; every other one is checked against it and none is copied from it
CH32V005 17 4 defined as a delta on CH32V006 — its own file holds one peripheral and the rest is inherited
CH32V003 16 4 the same family, a different die: no GPIOB at all, and no OPA interrupt where CH32V006 has one
CH32X035 27 7 a deliberately different family (24-bit ports, no HSE, named remap macros) that exists to prove the engine is data-driven rather than shaped around one chip
CH32L103 33 5 low-power, with USB PD and three comparators sharing one vector
CH32H417 78 3 dual-core, with SerDes, Ethernet, LTDC and USB SS — the part that stops "it works on the small ones" being the whole claim

All six read status: complete in the coverage ledger (python tools/coverage.py --quiet): every function the datasheet puts on a pin is modelled, or declared absent with a file:line. That is not the same as every peripheral being configurable to the same depth — a peripheral with no params: still has its mode choices, but no numbers to set, and some of those are deliberate (a CRC unit has no init struct to fill):

Part Peripherals with params: Deliberately without
CH32V006 14 of 19 SYS, RCC, DMA1, EXTI, TKEY — choices only
CH32V005 13 of 17 the same four as CH32V006, inherited with the rest of the part
CH32V003 11 of 16 as CH32V006, plus OPA1
CH32X035 14 of 27 the comparators, op-amps and PIOC are choice-driven; FLASH, USBFS, USBPD declared absent
CH32L103 26 of 33 SYS, RCC, EXTI, DMA1, USBFS, USBPD, OPA1
CH32H417 67 of 78 9 are ABSENT with a citation; DMA1/DMA2 are the remaining gap

The per-peripheral table is tests/evidence/round6/2026-09-13-h417-peripheral-map.md.

WCH_CubeMX/
├── dist/index.html          BUILT app - open it in any browser, no server, no network
├── app/
│   ├── template.html        the page: CSS, markup, and the render/UI script
│   ├── engine/*.js          the engine as ES modules (model, conflicts, clock, project, export)
│   └── vendor/js-yaml.js    bundled YAML parser (MIT), so the app works offline
├── data/
│   ├── mcus/*.yaml          one file per MCU - the whole part definition
│   ├── packages/packages.yaml   package geometries (SOP, TSSOP, QFN, LQFP...)
│   ├── sources/<PART>/      the DS, the RM and the EVT package
│   └── firmware/            PlatformIO project - compiles the generated C for real silicon
├── docs/                    how it works, how to add a part, the coverage ledger (with pictures)
├── build.py                 inlines the engine and the YAML into dist/index.html
├── tests/                   QA suites + the runner (npm test)
├── app/tests/               engine unit tests
├── tools/                   the gates and the headless CLI: validate_mcu.py, coverage.py,
│                            ledger.py, verify_sdk_names.py, wchcube_cli.js
├── graft/                   the context graph for AI agents (git-ignored, regenerable)
├── src-tauri/               desktop shell (Tauri 2)
├── PROGRESS.md              where the project stands, long form - read this first
├── TASKS.md                 what is claimed, done and next
└── agents/                  README (the agreement) + STATUS (the single source of
                            truth) + BOARD (the log). See "How this repo is built"

Run it in a browser

Open dist/index.html. That is the whole instruction — it is a single self-contained file with no external requests, so it works from a USB stick or an offline machine.

After changing anything in app/ or data/, rebuild:

python build.py

(python3 on macOS and Linux. Never edit dist/index.html by hand — it is generated.)

Run it as a desktop app

The desktop build is the same page in a native window, plus the things a browser cannot do: it reads the MCU folder from disk, uses native file dialogs, and reloads a part the moment you save its YAML in another editor.

cd src-tauri
cargo run              # development
cargo tauri build      # installers in src-tauri/target/release/bundle/

You need Rust and the Tauri prerequisites for your OS. build.py runs automatically before a release build.

MCU files are read from two places, and the second wins:

Folder What it is
the app's bundled data/mcus/ the parts that ship with the release
~/.wch_cubemx/mcus/ your own parts and overrides

Drop a YAML in the second folder and it appears in the MCU list. Edit one while the app is open and the part reloads in place, keeping your package selection.

Projects

New project — pick the MCU, then the exact part number and package. The dialog shows flash, SRAM, I/O count and temperature grade for each variant.

Save project (Ctrl+S) writes <name>.wchproj: readable YAML holding the MCU, variant, package and every setting, remap and pin. In the browser it downloads; on the desktop you get a native save dialog.

Open project restores all of it. One project is one MCU on one package. The matching MCU file has to be loaded first — bundled, in your override folder, or via Open MCU file….

Generate

One button, on the Project Manager tab, producing one of two scopes.

The project (default) is a PlatformIO folder you can open and flash:

<name>/
├── platformio.ini, README.md, .gitignore
├── src/main.c
├── lib/wchcube_generated/include/{wchcube_init.h, BoardPins.h}
├── lib/wchcube_generated/src/wchcube_init.c
└── docs/            pin table (md + csv), KiCad pin table, pinout SVG, clock summary

Only the pin map turns off everything above and outputs BoardPins.h alone. Use it when you already have a project of your own: the file names every pad the configuration uses, so you can write BOARD_USART1_TX instead of GPIO_Pin_9. It configures nothing — it is the map, not the driver.

#define BOARD_USART1_TX_PORT  GPIOA
#define BOARD_USART1_TX_PIN   GPIO_Pin_9
#define BOARD_USART1_TX_AF    GPIO_AF7
#define BOARD_STATUS_LED_PIN  GPIO_Pin_13      /* your own label */

Because that scope needs no board, it also works on a package no PlatformIO board exists for — a part number is only needed to pick one.

Everything the app can emit is also reachable from a script, which is how CI drives it:

node tools/wchcube_cli.js CH32V006 --package TSSOP20 --format all --out build/
--format What it writes
pins-md, pins-csv the pin table as Markdown, and as CSV
pins-kicad a KiCad Symbol Editor pin table, for pasting into a symbol
pins-svg a standalone pinout diagram, self-contained and self-styled
pins-h BoardPins.h — the pin map as C macros
clocks-md the clock tree and the frequency at each tap
c wchcube_init.c/.h, the initialisation code
json machine-readable state: pins, conflicts, clocks, peripheral status

(all is every one of those except json, which CI asks for by name.)

--option <key>=<value> sets any generator option for one run (--option list prints them all), and --strict exits non-zero if the configuration conflicts or the generated C carries a TODO — that is the flag CI gates on.

The pinout SVG is also a button on the chip toolbar (⤓), and the same renderer backs the browser's print view, so what you print is the drawing you were looking at rather than a second implementation of it.

Add an MCU

  1. Start from data/mcus/CH32V006.yaml — a complete real part with datasheet citations on every block — and read data/FORMAT.md for the schema. (data/mcus/ holds real silicon only; the synthetic layout fixture lives in tests/fixtures/mcus/.)

  2. Fill in mcu:, the packages: pin tables, the pins: list and the peripherals:.

  3. Check it:

    python tools/validate_mcu.py data/mcus/YOUR_PART.yaml
  4. python build.py, then reload the page. Or skip the rebuild entirely and use Open MCU file… to load the YAML straight from disk.

The one idea worth knowing: alternate functions are never listed per pin. Each peripheral carries a remaps: table saying which pin each signal lands on for each value of the AFIO remap field. The app derives every pin's signal list, and every conflict, from those tables. One place to edit, and the conflicts are computed rather than maintained.

  1. Close the coverage ledger — python tools/coverage.py YOUR_PART lists every function the datasheet puts on a pin that the file does not model, every RM chapter and every SDK instance it does not account for, and exits 1 while any remain. Zero open rows is what "done" means here; docs/COVERAGE.md is the loop.

tools/validate_mcu.py checks schema, that every package has a geometry to draw it with, that pin numbers run 1..N with no gaps, that every signal a mode can request is actually routed somewhere, that remap indices are in range, and that each variant's datasheet I/O count matches the pin table. Use --strict to treat warnings as errors.

Running the tests

npm install     # jsdom + js-yaml, test-only
npm test        # builds, then runs every suite

npm test runs node tests/run.js, which executes the engine unit tests in app/tests/ and the QA suites in tests/ — around seventy suites in all. The ones worth knowing about:

Suite What it protects
app/tests/* the engine: model, conflicts, clock, codegen, project round-trip, undo, the pin map
tests/coverage.test.js the coverage ledger — every part meets the status it declares, and the ledger's own tool is proven to catch every planted break
tests/strict.test.js --strict exits 0 on every fixture, and exits 2 on a planted conflict
tests/completeness.test.js every peripheral of every part is reachable: a setting, a clock bit, a vector, and no routed signal that no choice can claim
tests/codegen_compile.test.js the generated C for each part actually compiles against the vendor SDK
tests/generated_project.test.js a generated project is self-contained and builds with pio run
tests/cross_loader.test.js the Python gates and the app's YAML parser agree about the same file
tests/data.test.js every MCU file is valid, drawable and loadable
tests/build.test.js dist/index.html is complete, current, offline, and has no duplicate declarations
tests/layout.test.js the CubeMX layout, the chip fitting its canvas, and that no panel grid is mis-sized
tests/desktop.test.js the Tauri config and bridge — including that it does nothing at all in a browser
tests/smoke.js every MCU on every package: load, switch, click, assign, zero console output

Filter with node tests/run.js <text>, e.g. node tests/run.js smoke.

If npm install cannot write to this folder — a known npm failure on Google Drive and network shares (EBADF) — install the two test dependencies anywhere and the tests will find them:

npm install --prefix "%LOCALAPPDATA%\wchcube-deps" jsdom@25 js-yaml@4

Set WCHCUBE_DEPS to point somewhere else. The Rust in src-tauri/ is compiled by CI; tests/desktop.test.js covers everything about the shell that does not need a toolchain.

Build the firmware

data/firmware/ is a PlatformIO project that compiles for real CH32V003 / CH32V005 / CH32V006 / CH32X035 / CH32L103 / CH32H417 silicon with the WCH EVT NoneOS SDK. It is where the configurator's generated wchcube_init.c/.h becomes an ELF — and the only place the claim "the generated C compiles" can be checked instead of asserted.

cd data/firmware
pio run                     # default environment: CH32V006F8P6 (TSSOP20)
pio run -t upload           # over WCH-Link

Eight part environments ship — CH32V006F8P6, CH32V006K8U6, CH32V005F6P6, CH32V003F4P6, CH32X035G8U6, CH32L103K8U6, CH32H417QEU6, CH32H417WEU6 — plus native, which compiles the HAL on the host so its logic can be tested without a board. pio run -e <env> picks one.

In VS Code, open data/firmware as the folder (PlatformIO needs platformio.ini at the workspace root), or open data/firmware/wchcube-firmware.code-workspace to get the repo and the firmware side by side.

Feed it a configuration from the repo root:

node tools/wchcube_cli.js CH32V006 --package TSSOP20 --format c      --out data/firmware/lib/wchcube_generated/src

src/main.c picks it up through __has_include and calls WCHCube_Init(); with the drop zone empty the firmware still builds and says so at startup. data/firmware/README.md has the details and data/firmware/ARCHITECTURE.md has the layering and ownership rules.

How this repo is built

Three Claude Code agents work on it in parallel — DATA, APP and QA + RELEASE — each owning a part of the tree so they never edit the same file. agents/ is the single agent working directory and it is three files, which every agent reads every time:

agents/README.md the working agreement — what the product is, who owns what, the work cycle, the gates, the handoff protocol
agents/STATUS.md the single source of truth — the measured state with the command that printed each number, the open list with an owner per row, what each agent has in flight right now, the acceptance script, what only a human can do, the backlog
agents/BOARD.md the append-only log: decisions, findings, requests between agents

Work is claimed and ticked in TASKS.md, and QA is the only agent that ticks a line in STATUS.md §2 — on evidence, named on the line. Closed rounds are archived under agents/history/ and are never read during a work cycle. If you are picking the project up by hand, read agents/STATUS.md first; it is written so that somebody who has never seen the repo can resume whatever was in flight when the last session ended.

The repo is indexed by graft, which builds a context graph as linked markdown under the git-ignored graft/ directory — a local, regenerable cache that agents query instead of re-reading source files. graft build after a big change; graft ask "<question>", graft callers <symbol> and graft map are the entry points. It is a development convenience and nothing in app/, data/ or the tests depends on it.

Contributing

The two things that would help most, in order:

  1. Flash one generated project and report what happened. It is a five-minute job with any CH32V003/CH32V005/CH32V006/CH32X035/CH32L103/CH32H417 board and a WCH-Link, and it is the only claim in this repository that nobody here can close. See agents/STATUS.md §6.
  2. Add a part. If your microcontroller is missing, docs/ADDING-A-PART.md is the whole process — it is a data job, not a programming job, and it does not require understanding the engine.

Before opening a PR, the gate is two commands and both must be green:

python build.py && node tests/run.js

Beyond that, the rules that matter and the reasons for them are in agents/README.md. The short version:

  • A hardware fact needs a citation — a datasheet table, an RM section, or an EVT file:line. Precedence is EVT → Reference Manual → Datasheet. "The other CH32 parts have it" is not a citation, and it is precisely how all three of the shipped data defects happened.
  • If you changed what the generator emits, compile it on a configuration that assigns pins. A default configuration generates an empty function and proves nothing.
  • If you changed the UI, look at it in a real browser. jsdom has no layout engine and cannot see clipped text; it is necessary, not sufficient.
  • Never delete a data file or a test, and never lower a threshold to make something pass.
  • dist/index.html is generated — never hand-edit it.
  • Nothing in app/ may name a part. If behaviour differs between parts, it belongs in the data.

The PR template walks through these. Answering "I did not run that" is much more useful here than a guess.

Stack

Plain JavaScript, SVG and YAML. No framework and no bundler — build.py concatenates the engine modules and inlines the data, which is why the result is one file you can email to someone. The desktop wrapper is Tauri 2 (Rust), and it loads the very same file.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages